Club robotique de Sophia-Antipolis

Accueil > Projets, études > Nos réalisations > CyberP4 > Le logiciel

Le logiciel

jeudi 17 novembre 2022, par Eric P.

Comme mentionné dans la présentation générale, un des éléments importants du cahier des charges est de fournir une solution extensible.

L’approche de principe

Dans ce genre de situation, le mot qu’il faut bannir est "monolithe". Il n’est pas question ici des menhirs d’Obélix, mais des applications développées comme un seul objet technique, c’est à dire un gros exécutable qui fait tout. L’approche à privilégier est celle des micro-services, consistant à limiter chaque objet technique à une seule fonction et à utiliser les outils les mieux appropriés à chacun pour le développer.

J’en entends déjà dans le fond qui sont en train de se moquer en pensant que je vais leur pondre une usine à gaz façon application dans le Cloud, mais ils font erreur. En effet, même si on n’utilise en général pas le terme dans ce contexte, un OS fonctionne comme cela. Vous n’avez qu’à constater la collection de process qui tournent en utilisant la commande adaptée à votre système (ps aux sous Linux par exemple).

Maintenant que les moqueurs sont calmés ou qu’ils sont partis, faisons le tour de la question afin d’identifier les composants à prévoir.

De quoi avons-nous besoin ?

En premier lieu, d’un composant qui va interagir directement avec le hardware pour contrôler le moteur pas à pas et les servo-moteurs, et qui va également lire les différents capteurs installés, à savoir :

  • les détecteurs de passage de jeton
  • le détecteur d’ouverture de la trappe de vidage au bas du plateau de jeu
  • le bouton poussoir de lancement de la partie

Il nous faut ensuite le cerveau du joueur cybernétique, que certains appelleront pompeusement IA comme déjà évoqué. Lorsque le contrôleur aura détecté que l’humain a joué son jeton, il va lui demander quel coup jouer à son tour (pour mettre la misère à l’adversaire comme il se doit). Pour cela, il va lui transmettre la composition actuelle du plateau de jeu et recevra en retour le numéro de la colonne à jouer.

Nous en avons déjà assez avec cela pour donner corps à notre système. Mais comme nous sommes joueurs, nous allons également humaniser notre système en lui donnant la parole. Un composant se charge de prononcer des phrases en rapport avec les événements signalés par le contrôleur : c’est au tour de l’humain de jouer, la machine a gagné, l’humain à gagné (on ne sait jamais),... Voilà donc le troisième composant de la version actuelle de notre CyberP4.

Un petit dessin valant plus qu’un long discours, voici la représentation schématique de l’ensemble de l’architecture (cliquer sur l’image pour zoomer) :

Nous y retrouvons en rouge les 3 composants évoqués plus haut :

  • cp4-controller le contrôleur, en interface avec le matériel et jouant le rôle de chef d’orchestre de son fonctionnement
  • cp4-ia, le calcul de stratégie
  • cp4-voice, le composant de vocalisation des événements de la partie

Sur le plan logiciel, ces 3 composants sont strictement indépendants et peuvent être développés avec des langages et des technologies totalement différents si besoin.

Comment communiquent-ils ?

C’est selon.

Les événements asynchrones qui surviennent au cours de la partie circulent sur un bus de message ou bus informatique. Plusieurs protocoles existent à ce niveau, et nous avons choisi AMQP du fait de son adaptation à nos besoins (traffic modéré, consommation faible, fiabilité,...). Parmi les multiples implémentations libres disponibles en termes de broker pour un bus de messages, RabbitMQ est la mieux adaptée car très légère en termes d’utilisation de ressources et totalement opérationnelle sur des cibles comme la Raspberry Pi, notre sélection pour héberger la partie logicielle du démonstrateur.

Le contrôleur publie donc les messages notifiant les divers événements du cycle de vie d’une partie via RabbitMQ.

Le composant de vocalisation s’enregistre de son côté auprès du broker RabbitMQ pour écouter tout ou partie de ces messages et les vocaliser en conséquence.

L’autre type de communication est de type synchrone, et prend place entre le contrôleur et le calcul de stratégie. Il s’agit ici d’une communication synchrone, car le contrôleur a besoin de la réponse du calcul pour poursuivre la partie. Cette dépendance n’existait pas dans le cas de la notification des événements car peu importe si quelqu’un les exploite ou pas.

La solution retenue dans ce cas est un simple serveur HTTP fournissant une API sous forme de service REST.

A noter qu’une autre communication est représentée sur le schéma, entre un utilisateur (l’animateur) et le contrôleur. Il s’agit ici d’une facilité mise à disposition pour la mise au point ou le dépannage, sous forme d’un accès manuel à toutes les actions gérées par le contrôleur et à la lecture des différents capteurs. Son utilisation n’est absolument pas requise en mode normal, d’où la représentation en pointillés. Elle est fournie par une partie spécifique du contrôleur, sous forme d’un serveur HTTP exposant une API REST.

Les présentations ayant été faites, intéressons-nous maintenant à l’implémentation des différents composants. Python a été choisi pour en implémenter les versions actuelles, du fait de sa simplicité et de l’immense écosystème de librairies disponibles pour fournir les services requis (support du protocole AMQP, interfaçage hardware, implémentation de serveurs HTTP,...). Et puis j’aime beaucoup Python, et c’est moi qui décide. Na  😄

Ce choix n’est en rien bloquant car, comme mentionné précédemment, chaque composant peut utiliser les technologies de son choix. Il n’est d’ailleurs pas exclu qu’une version ultérieur du calcul de stratégie soit codée en un langage plus performant comme Go par exemple, afin d’augmenter la profondeur de calcul et d’être encore plus invincible.

Le contrôleur

Il assure plusieurs fonctions :

  • récupérer les signaux fournis par les capteurs, pour détecter le coup joué, l’ouverture de la trappe de vidage et l’utilisation du bouton de lancement de la partie
  • contrôler le moteur pas à pas de déplacement du godet, en dialoguant avec le module L6470 via le bus SPI
  • contrôler les servo-moteurs du réservoir de jetons et de la trappe du godet
  • gérer le déroulement de la partie
  • publier les notifications des événements du cycle de vie du jeu
  • répondre aux requêtes envoyées à l’API via le port HTTP

Il utilise pour ce faire les librairies suivantes :

  • pigpio, préférée aux autres librairies couramment utilisées du fait de ses mécanismes de callbacks et de l’absence des défauts rencontrés au niveau SPI dans le cas particulier lié aux communications avec le L6470.
  • pika pour le support du protocole AMQP
  • le package standard http.server pour l’implémentation du serveur de l’API REST. Bien que non recommandé pour un contexte de production selon l’avertissement en début de sa documentation, il est parfaitement valide pour l’utilisation qui nous concerne.

Etant donné que nous fonctionnons en mode broadcast pour les notifications d’événement, nous utilisons le type d’échange topic de AMQP. Il permet d’attacher un topic structuré (ex foo.bar.baz) permettant aux écouteurs de ne recevoir qu’un sous-ensemble des messages par filtrage sur le topic au moyen d’expressions utilisant des wildcards (ex : foo.*). Se reporter à la section consacrée à ce sujet dans les tutoriaux de RabbitMQ

Nous n’allons pas détailler le code source ici, le mieux étant de consulter le repository GitLab du projet pour cela. A noter que le code et sa documentation sont rédigés en Anglais pour une plus grande diffusion sur ce type de plate-forme de partage. Et puis c’est aussi par déformation professionnelle 🙂.

Le calcul de stratégie

C’est un simple serveur HTTP implémentant une API REST conforme à une spécification établie pour tout service fournissant un calcul de stratégie.

Cette spécification se résume à une unique requête GET /move?b=<board_configuration>.

La configuration est une représentation sérialisée du plateau de jeu, colonne par colonne en partant du coin inférieur gauche (idem repère cartésien XY). Chaque caractère encode le contenu de la cellule correspondante, avec la convention suivante :

  • ’0’ : cellule vide
  • ’h’, ’p’ ou ’1’ : humain
  • ’m’, ’a’ ou ’2’ : machine

La réponse, au Content-Type application/json, contient simplement le numéro (entre 1 et 7) de la colonne à jouer. Rien de plus compliqué.

L’implémentation fournie est basée sur une recherche de type MinMax au sujet de laquelle une abondante littérature existe. La combinatoire du jeu étant explosive, la recherche est optimisée par la méthode d’élagage ("pruning" en Anglais) dite alpha/beta. Là encore, il existe énormément de documentation sur la question. Parmi cette profusion, une introduction assez claire et appliqué au problème qui nous concerne est donnée dans cet article publié sur Medium.

L’algorithme du MinMax n’est de loin pas la partie la plus difficile, car son implémentation avec le pruning alpha/beta ne tient que sur 50 lignes, commentaires et lignes blanches comprises.

La définition de l’heuristique permettant d’établir le score d’une configuration donnée du jeu est plus délicate, car elle conditionne beaucoup la qualité des coups proposés. Une article assez intéressant sur la question est disponible en ligne sur Research Gate. L’ensemble des méthodes composant l’implémentation de l’approche retenue représente environ 80 lignes, blanches et commentaires inclus. Ce n’est certainement pas la plus sioux ni top of the art, mais c’est un bon compromis.

Afin de maintenir le temps d’attente à un niveau compatible avec l’interactivité (soit au plus 1 ou 2 secondes sur notre Raspberry Pi modèle 3B+), la profondeur d’exploration a été limitée à 4 coups, soit 2 tours de jeu, puisqu’on évalue les options prises par les deux joueurs. Passer à 6 coups multiplie le temps de calcul et le fait parfois atteindre des ordres de grandeurs trop longs (i.e. jusqu’à plusieurs dizaines de secondes). L’expérience a montré lors de la dernière Fête de la Science que même à sa profondeur minimale, la machine a été invaincue, alors que plusieurs joueurs visiblement expérimentés l’ont défiée à plusieurs reprises.

Les sources sont disponibles sur GitLab.

A noter qu’il est prévu d’écrire une version compilée du code afin de pousser la profondeur à son maximum. Ce sera certainement en Go car ça fait un moment que je cherchais un prétexte pour m’initier au langage. On vous tiendra au courant  😉

La voix de son maître

Ce composant est totalement facultatif et a été développé pour le fun d’une part, mais aussi pour démontrer ce que l’architecture retenue permet de faire, notamment grâce au bus de message.

Il est à l’écoute des messages publiés par le contrôleur pour notifier les changement d’état du système. Une table associe un ou plusieurs messages vocaux aux événements pour lequel une annonce vocale doit être faite.

La qualité sonore de la vocalisation en temps réel fournie par mbrola, espeak et consorts n’étant vraiment pas convaincante, nous y avons préféré la génération d’échantillons audio par le biais de l’API Google Translate. Elle n’est pas officielle, mais est suffisamment documentée pour être facilement utilisée.

La collection de fichiers audio des messages prononcés par la machine est générée en série en une dizaine de secondes par un petit script qui automatise cela et sont déployés sur la Raspberry. Ce script fournit d’ailleurs une illustration des différentes possibilités pour exploiter l’API Google Translate.

La table de correspondance mentionnée précédemment associe les noms de fichier avec les l’événement. Histoire de rompre la monotonie, plusieurs messages vocaux peuvent être associés à un même événement, le vocalisateur en sélectionnant un au hasard. On peut ainsi relancer le joueur s’il met trop de temps à réfléchir, en le titillant un peu au passage histoire de le déstabiliser 😉

Les sources sont disponibles sur GitLab.

Intégration système

Afin que tout cela démarre automatiquement à la mise sous tension de la Raspberry, les différents composants sont déployés sous forme de services systemd. Une introduction simple est disponible sur Wikipedia et le mieux est d’aller approfondir le sujet avec la documentation officielle.

Le code source des différents projets contient les fichiers de déclaration des services concernés.

Divers

Shutdown propre

Afin d’éteindre proprement le système et d’éviter les corruptions de carte SD qu’une coupure sauvage de l’alimentation peut entraîner, la procédure de shutdown est déclenchée par un appui prolongé du bouton permettant le lancement des parties en temps normal.

Cette fonction est intégrée dans le contrôleur, étant donné que c’est lui qui gère le hardware, et donc le bouton.

Reset de partie

Pour notifier le reset de la partie, soit une fois terminée normalement par une victoire ou un nul, ou bien si on souhaite abandonner celle en cours, un fin de course a été installé sur le plateau de jeu de manière à être actionné par la trappe de vidage située au bas.

La détection de son ouverture déclenche la remise à zéro du système, à commencer par le déplacement du godet de dépose à l’écart du réservoir à jetons, afin d’en dégager l’accès pour faciliter le remplissage.

Cette fonction est bien entendue réalisée par le contrôleur.

Et voilà

Vous savez maintenant à peu près tout sur CyberP4.


Crédits image logo : https://freesvg.org/

Un message, un commentaire ?

modération a priori

Attention, votre message n’apparaîtra qu’après avoir été relu et approuvé.

Qui êtes-vous ?

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici

Ce champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.