Jouons au Mölkky sans avoir à compter les points nous-mêmes
Si vous avez déjà joué au Mölkky, vous avez surement déjà prononcé ou entendu cette phrase ou un équivalent « On en est a combien ? ». Si vous ne savez pas ce qu’est le Mölkky : il s’agit d’un jeu de quilles finlandais dont le but est de marquer des points pour atteindre 50, en renversant les quilles avec un bâton lanceur. Généralement on est nombreux, c’est l’heure de l’apéritif et très rapidement personne ne sait où en est la partie.
Alors j’ai pensé : « Et si quelque chose comptait les points pour nous ? »
J’ai donc fait un prototype avec des beacons dotés d’un accéléromètre, et on l’a mis a l’épreuve en novembre dernier pendant la Xebicon 19.
On vous explique tout dans cet article.
Définir le proto
Les règles du jeu
Le principe du jeu est de faire tomber des quilles en bois à l’aide d’un bâton lanceur. Les quilles sont marquées de 1 à 12. La première équipe arrivant à totaliser exactement 50 points gagne la partie.
Au début du jeu, les quilles sont positionnées comme sur le schéma suivant :
Pour renverser les quilles, le joueur de la première équipe jette le bâton lanceur sur les quilles.
Trois situations sont alors possibles :
- plusieurs quilles sont tombées : l’équipe gagne autant de points que de quilles renversées
- une seule quille est tombée : l’équipe gagne le nombre de points inscrits sur la quille
- aucune quille n’est tombée : le joueur réessaie dans la limite de 3 lancers ratés (cette règle diffère souvent)
On redresse ensuite les quilles à l’endroit où elles étaient tombées. Ainsi elles s’éparpillent au cours du jeu. C’est ensuite à l’équipe suivante de jouer.
Si une équipe dépasse 50 points, son score redescend à 25. Il faut en effet atteindre exactement un total de 50, pas plus.
Game play cible versus objectif du proto
Pour notre proto, on acceptera que pour arriver au résultat idéal, il nous faudra itérer. On part donc sur un scénario, qui pourrait se dérouler ainsi, avec des étapes supplémentaires au scénario idéal :
Le Hardware
Quelle thing et quelle techno pour notre solution IOT
Gyroscope, accéléromètre : de quels capteurs avons-nous besoin ?
La première chose qui vient à l’esprit pour connaitre la position d’une quille c’est « il nous faut un gyroscope dans la quille comme dans les smartphones ! ».
Après quelques recherches on comprend qu’en réalité un smartphone combine les données de plusieurs capteurs pour estimer sa position. Contrairement à ce qu’on imagine, notre smartphone n’a pas de gyroscope. Il a un gyromètre, un accéléromètre et un magnétomètre. Le smartphone les utilise pour obtenir la valeur absolue du tangage (mouvement de l’avant vers l’arrière) et du roulis (mouvement de droite à gauche). [source]
Alors, j’ai cherché un objet doté de ces trois capteurs. En gros j’ai recherché sur Google « gyroscope sensors ». J’ai exploré un bon nombre de pistes. J’ai regardé du côté d’Arduino et de Raspberry. Je suis aussi souvent tombée sur des capteurs qui semblaient parfaits comme celui-ci :
Mais voilà je voulais plus simple : hors de question de me mettre à faire des soudures pour brancher un circuit électronique à une batterie à l’intérieur d’une quille.
Des capteurs BLE et leurs contraintes
De fil en aiguille, j’ai découvert Espruino qui commençait à beaucoup me plaire. Ça parlait JavaScript et applications mobiles, pour la développeuse front que je suis c’était plutôt séduisant. Alors j’ai cherché des sensors Bluetooth. J’ai découvert des frameworks qui donnaient envie : Evothings et Johnny-Five par exemple.
Ma recherche s’est de plus en plus affinée et puis, je me suis dit que des sensors BLE (Bluetooth Low Energy) étaient effectivement la clé : il nous fallait des balises avec un accéléromètre ! Comme c’est basse consommation, une pile dans notre petit objet serait parfaite. Plusieurs solutions s’offraient à nous : Kontact.io, Blue Net Beacon, Sensoro et surtout Estimote.
Et puis en discutant avec les collègues, un jour l’un d’entre eux me dit « tu vas avoir une limite : les spécs de Bluetooth, c’est pas plus de 7 devices connectés simultanément à un device ! »
Effectivement :
A master BR/EDR Bluetooth device can communicate with a maximum of seven devices in a piconet (an ad-hoc computer network using Bluetooth technology), though not all devices reach this maximum.
[source]
Qu’à cela ne tienne, je passerai par une passerelle !
Une gateway et 12 beacons : c’est Minew !
En faisant toutes ces recherches, je finissais toujours par tomber sur des beacons très peu documentés, mais aussi bien moins chers, vendus sur Alibaba par l’entreprise Minew. J’ai donc décidé de commander et d’essayer :
Les beacons transmettent les données de l’accéléromètre à la gateway par Bluetooth. Celle-ci communique ensuite ces données à l’adresse d’un serveur (cloud, local ou hébergé) en HTTP ou en MQTT. Elle a donc besoin d’être connectée par WiFi (ou par Ethernet).
Des beacons aux positions
Configurer les beacons
Pour allumer un beacon, il faut l’ouvrir et presser 3 secondes le bouton on. Il se met alors à clignoter quelques secondes puis à émettre des données. À partir de ce moment, il est actif et contactable.
Nous allons voir quelles sont ces données et comment les intercepter.
Pour le configurer, Minew a mis à disposition une application mobile téléchargeable (BeaconSET sur Google Play / BeaconSET sur l’Apple store). L’entreprise nous a également envoyé les sources de l’application native Android, qui peut servir de base pour un usage qui serait différent du nôtre.
Changer le mot de passe
Par défaut le mot de passe est minew123, il est vivement conseillé d’en choisir un nouveau.
Émettre les données de l’accéléromètre
Un beacon est conçu pour de multiples usages, notamment dans le marketing. Plusieurs « slots » sont alors configurables dans l’interface de configuration du beacon. Ici un slot en particulier nous intéresse c’est l’accéléromètre (ACC), que l’on souhaite actif en continu sans avoir besoin de le déclencher manuellement avec le bouton. Pour ce faire on positionne le trigger à motion (cf. la capture ci-dessous). On supprime donc les autres, à l’exception de INFO, qui est utile au beacon en lui-même et on le configure.
La home de l’application nous permet alors de vérifier que le beacon transmet les valeurs de mouvement du beacon selon 3 axes orthogonaux : X (abscisse), Y (ordonnée) et surtout Z (cote).
On effectue ces opérations sur chacun des 11 beacons restants :
Configurer la gateway
Réseau local et wifi
Une fois allumée, la gateway diffuse un réseau WiFi « GW-XXXXXXXXXXXX », il n’y a pas de mot de passe par défaut. Lorsqu’on est connecté à ce WiFi, l’interface se trouve à l’adresse http://192.168.99.1
La première chose à faire est de changer et sécuriser le SSID que diffuse la gateway. Il faut aussi mettre un mot de passe sur l’interface.
Ensuite notre gateway a besoin d’être elle-même reliée à Internet pour pouvoir communiquer à des services cloud par exemple. Pour cela, on peut la connecter avec une prise Ethernet ou configurer un réseau Internet domestique dans l’onglet Network :
Après redémarrage, on sait si le routeur est bien connecté à Internet lorsque une adresse IP lui a été attribuée :
Le fait d’avoir sélectionné le mode répétiteur sur la gateway, nous permet d’utiliser la connexion Internet à laquelle elle est elle-même connecté, lorsqu’on se connecte à son propre réseau WiFi.
Réception et émission des données
A priori, notre gateway reçoit les données des beacons allumés et doit pouvoir les transmettre à n’importe quel service. Comment vérifier ?
Dans un premier temps, utilisons la plateforme IOT uBeac, un service d’agrégation et de visualisation de données. Celle-ci nous permettra de vérifier que notre gateway transmet correctement ce qu’elle reçoit. Après avoir créé un compte, nous configurons une gateway et sélectionnons la nôtre : Minew.
L’onglet HTTP nous donne le endpoint à copier et qui va nous servir à configurer le service sur l’interface de notre gateway :
Nous pouvons maintenant nous rendre sur l’interface de la gateway dans l’onglet Services et lui fournir le endpoint d’uBeac, sélectionnons également le format JSON :
Si on bouge nos beacons, on peut alors voir apparaitre les requêtes en temps réel :
Et voir à quoi ressemble la data reçue pour un beacon :
Nous verrons plus tard comment interpréter ce JSON.
Le Software
Le materiel fonctionne. Il nous reste plus qu’à développer la partie software de notre Mölkky.
Créer notre propre endpoint pour recevoir les données
Mise en place d’un serveur Node
À la manière d’uBeac, nous créons un serveur qui, sur un endpoint en POST reçoit les données en continu pour les traiter. Pour cela nous utilisons le framework Express.
On créé une route /api/minew
:
import express from 'express'; const router = express.Router(); router.post('/', async (req, res) => { const { body } = req; console.log({ body }); res.end(); }); module.exports = router;
On peut déployer notre serveur sur un Raspberry ou chez un hébergeur. Notre endpoint sera donc http://192.168.99.103:8888/api/minew en local et nous pouvons le fournir à la gateway :
C’est parti, notre serveur reçoit alors beaucoup d’informations, on note qu’il reçoit bien les informations de la gateway en elle-même mais aussi des informations de potentiels devices BLE autour :
Améliorons la configuration de notre gateway pour filtrer et recevoir uniquement les informations de nos sensors. Désactivons l’envoie des données de la gateway et filtrons par adresse MAC. Ici l’exemple avec un seul beacon :
À présent notre serveur ne reçoit qu’un objet vide si on ne bouge pas les beacons en question et sinon il reçoit les données des beacons qu’on a indiqué à la gateway :
Interprétation des données reçues
Pour l’instant ce qu’on reçoit ne sont que des données brutes dans la clé rawData
du body. Il s’agit d’un format de diffusion de paquet. Pour en savoir plus, je vous conseille l’article « Understanding the different types of BLE Beacons ».
ReelyActive nous fournit par exemple un outil en ligne pour déchiffrer ces données à partir de la valeur de rawData
et en sélectionnant le bon sensor :
Et voici, nous voyons enfin apparaitre les valeurs de nos accélérations :
Et en plus ReelyActive propose en open source une librairie qu’on va pouvoir utiliser : Advlib.
Transformons le log de notre route :
import advlib from 'advlib'; import express from 'express'; import isArray from 'lodash/isArray'; const router = express.Router(); router.post('/', async (req, res) => { const { body } = req; if( isArray(body) && body.length > 0 ) { body.forEach(({ rawData }) =>; { if( rawData ) { console.log(advlib.ble.data.process(rawData)); } }); } res.end(); }); module.exports = router;
Résultat :
POST /api/minew 200 0.650 ms - - { flags: [ 'LE General Discoverable Mode', 'BR/EDR Not Supported' ], complete16BitUUIDs: 'ffe1', serviceData: { uuid: 'ffe1', data: 'a10364000a000000f0a246a23f23ac', minew: { frameType: 'a1', productModel: 3, batteryPercent: 100, accelerationX: 0.0390625, accelerationY: 0, accelerationZ: 0.9375, macAddress: 'ac:23:3f:a2:46:a2' } } }
Nettoyons les données inutiles en filtrant ce qu’on reçoit de façon à ne garder que batteryPercent
et surtout mac
et accelerationZ
. Ici SKITTLES
est une constante importée qui associe chaque adresse MAC à un numéro de quilles :
router.post('/', async (req, res) => { const { body } = req; // Look for data concerning skittles sensors if (body && isArray(body) && body.length > 0) { const skittles = body .filter((sensor) => Object .keys(SKITTLES) .includes(sensor.mac)); // Look for skittles with raw data and transform raw data if (skittles.length > 0) { const skittlesInfo = skittles .filter((skittle) => !isUndefined(skittle.rawData) && !isNull(skittle.rawData) && skittle.rawData.length > 0) .map(({ mac, rawData }) => ({ mac, ...advlib.ble.data.process(rawData), })) .filter((skittle) => hasIn(skittle, 'serviceData.uuid') && hasIn(skittle, 'serviceData.data') && hasIn(skittle, 'serviceData.minew')) .map(({ serviceData: { uuid, data, minew }, ...rest }) => ({ uuid, data, ...minew, ...rest, })) .map((data) =>; omit(data, [ 'data', 'frameType', 'productModel', 'accelerationX', 'accelerationY', 'macAddress', 'flags', 'complete16BitUUIDs', 'uuid', ])); console.log({ skittlesInfo }); } else { debug('No skittle sensors', skittles); } } else { debug('No Body or Empty body'); } res.end(); });
Nous obtenons donc un tableau d’objets pour chaque beacon reçu :
{ skittlesInfo: [ { batteryPercent: 100, accelerationZ: 0.9296875, mac: 'AC233FA246A2' } ] }
De l’accélération à la position
L’accéléromètre permet de savoir dans quelle direction le beacon se déplace, il ne détecte pas une position mais une accélération sur l’un des trois axes X, Y, Z.
Donc ce que chaque beacon va nous envoyer, ce sont ses changements de vitesses et ses mouvements de translation.
Ce n’est pas suffisant pour ce que nous voulons : savoir si le beacon est horizontal ou vertical ! Heureusement, l’accéléromètre permet aussi de détecter la force de gravité générée par la Terre. C’est en fait ce détail qui va nous être très utile, beaucoup plus que tout le reste. En effet, par chance, la valeur de l’accélération sur l’axe Z est toujours proche de 1 lorsque le beacon ne bouge pas sur cet axe, c’est sa force de gravité. Ainsi nous pouvons déduire qu’il est en position debout.
Nous voulons obtenir un objet avec la structure suivante pour chaque réception :
{ XXXXXXXXXXX: { // adresse MAC battery: 100, position: 'KNOCKED_OVER', // 'KNOCKED_OVER' ou 'UPRIGHT' z: 0.9296875, value: 12 // numero de la quille }, ... }
Si on stocke sur le serveur la dernière position de chaque quille (quasiment en temps réel selon l’intervalle d’envoi réglé sur la gateway), alors nous avons tout ce qu’il faut pour créer une partie de Mölkky.
Considérons donc un objet global sur notre serveur qu’on appelle lastState
. Donnons une marge d’erreur via POSITION_LEVEL
d’une valeur de 0.8 et ajoutons à la suite de notre code :
skittlesInfo.forEach(({ mac, accelerationZ: z, batteryPercent: battery }) =>; { lastState[mac] = { ...lastState[mac], ...(battery &&; { battery }), ...(z && { position: z && = POSITION_LEVEL ? 'UPRIGHT' : 'KNOCKED_OVER', z, }), value: SKITTLES[mac], }; });
Du socket pour le temps réel
Pour qu’un client puisse se connecter à notre serveur et avoir les données en temps réel, nous utilisons Socket.io pour émettre notre set de quilles avec leur position :
const io = req.app.get('socketio'); io.emit('UPDATE', lastState);
Développer l’application
Le serveur
L’API pour la gestion du jeu
Elle fournit 4 endpoints accessible en POST :
- /start : pour démarrer une partie
- /score : pour envoyer le score de l’équipe qui vient de jouer
- /miss : si une équipe a joué mais n’a pas touché de quilles
- /reset : pour recommencer la partie ou pour réinitialiser la partie quand elle est terminée
Endpoint | Body | Réponse |
---|---|---|
/api/molkky/start |
{ "teams": [ "cat", "dog" ], "playingTeam": "dog" } |
{ "scores": { "cat": { "score": 0, "left": 50 }, "dog": { "score": 0, "left": 50 } }, "currentTurn": { "isPlaying": "dog", "remain": 3 } } |
/api/molkky/score |
{ "team": "cat", "points": 10 } |
{ "scores": { "cat": { "score": 0, "left": 50 }, "dog": { "score": 10, "left": 40 } }, "currentTurn": { "isPlaying": "cat", "remain": 3, "wining": "dog" } } |
/api/molkky/miss |
{ "team": "dog" } |
{ "scores": { "cat": { "score": 0, "left": 50 }, "dog": { "score": 6, "left": 44 } }, "currentTurn": { "isPlaying": "cat", "remain": 2 } } |
/api/molkky/reset |
{ "scores": null, "currentTurn": null } |
Nous avons documenté ces endpoints et leurs réponses potentielles sur Postman.
Un singleton pour la partie en cours
Un singleton CurrentGame
est instancié lorsque le endpoint /start est appelé. C’est cette instance qui contient toutes les infos de la partie en cours.
Les routes pour servir le front
Enfin les routes /molkky/game
et /molkky/play
rendent le composant front Game
en lui fournissant le currentGame
en contexte.
Le client pour les joueurs
Ici nous avons réalisé l’application front avec React. Le composant Game (que nous avons vu plus tôt, en server side rendering) :
- Un
DataContext
qui contient les données des quilles, récupérées via Socket.io. - Un
PlayContext
qui contient les données propres à la partie. - Selon la route :
- Un composant
StartScreen
qui permet à l’utilisateur de sélectionner la team qui va jouer et vérifier que toutes les quilles soient détectées et en position debout avant de pouvoir commencer la partie. - Un composant
PlayScreen
qui contient les composants du jeu : affichage des quilles, score, bouton de validation du score, modale pour modifier éventuellement le score.
- Un composant
Screenshots d’une partie
|
|
|
|
|
|
Le Mölkky en situation réelle
Intégrer nos beacons à nos quilles
Les essais
Avant d’installer nos beacons dans des quilles en bois, nous avons fait des tests. Les images se passent de commentaires.
Les quilles en bois
Un collègue doué en bricolage + un set de Mölkky en bois = le tour est joué !
Bilan
Il faut améliorer la fiabilité
À l’usage, sur un stand de la Xebicon, nous avons observé que parfois les beacons ne captaient pas le fait d’avoir été renversés ou relevés. Est-ce dû au choc ? Faut-il améliorer les réglages des beacons ?
De plus, le fait d’avoir un système où l’envoi de POST est multiple en permanence, n’est pas des plus performants. On aurait préféré une solution qui va chercher l’information sur le device bluetooth, plutôt que celui-ci n’émette en permanence. D’autres technologies sont peut-être plus appropriées. Aussi faudrait-il approfondir et surveiller ce que donne l’API Web Bluetooth.
En conclusion, on peut dire que pour des beacons, la fiabilité est:
- raisonnable car nous avons pu faire plusieurs parties
- insuffisante car nous avons du plusieurs fois corriger la détection
Il faut itérer pour développer des features
Nous avons implémenté un leaderboard pour la journée sur le stand de la Xebicon pour enregistrer les scores via Firebase.
Beaucoup d’améliorations et de nouvelles features sont envisageables :
- settings des règles de jeu
- prendre en compte des joueurs individuels et plus de 2 teams
- jouer avec les statistiques de jeu
- etc.
Si l’envie vous prend de vouloir tester, les sources sont disponibles sur le projet Github Molkky connecté, avec un menu qui permet de simuler des beacons si vous n’en avez pas.
Le mot de la fin
On ne va pas se mentir : notre Mölkky est loin d’être prêt à être commercialisé. Se balader sur les terrains de jeu avec une gateway branchée qui a besoin d’Internet c’est pas l’idéal. Ce n’est pas facilement portable et si vous êtes à Poiseul-la-Grange c’est même un peu difficile de trouver de la 3G.
Mais chez Publicis Sapient Engineering, on aime les projets impossibles et surtout on aime chercher des solutions. La Xebicon c’est l’occasion de partager la recherche qu’on fait grâce à ces projets insensés qui nous passionnent. Et qui sait, peut-être vous donner envie de participer pour les améliorer ?