Stéphane.
AutomationArchivé

Carb0t — superviser une flotte de robots depuis un master

Banc d’essai de 2020 : un master Python, Symfony et MariaDB pilotant quatre robots repérés au NFC et au Bluetooth. Deux semaines de code, un protocole maison, et un arrêt assumé.

Technologies employées

  • Python 3.7
  • Supervisor
  • Symfony 5
  • PHP 7.3
  • nginx
  • MariaDB 10.3
  • Django
  • Flask
  • bluepy
  • libnfc
  • Bash

Au printemps 2020, j’ai voulu savoir ce qu’il fallait pour piloter plusieurs machines depuis une seule. Pas en lisant sur le sujet : en le construisant. Carb0t est le banc d’essai qui en est sorti — un master qui empile une base, une API, un front et un écouteur réseau, et quatre robots censés se repérer dans un bâtiment au badge NFC et à la puissance du signal Bluetooth. Deux semaines de travail, deux dépôts, puis un arrêt net. Cette page raconte ce que j’ai réellement construit, ce que j’ai appris, et pourquoi je n’ai pas fini.

D’où venait l’idée

Le nom interne du projet, BotCarLunch, dit la cible : des petits véhicules qui circulent dans un bâtiment pour porter des repas d’un point à un autre. Le domaine que j’avais posé, stenzew.local, reprenait celui de mon R2-D2 taille réelle — le même terrain d’expérimentation, deux ans plus tard, avec une question différente.

Sur le R2-D2, la question était mécanique et électronique : faire bouger une masse de 55 kg. Ici, elle était d’infrastructure : comment un serveur sait-il ce que font quatre machines qui ne sont pas lui ? C’est la question que je me pose encore aujourd’hui, à une autre échelle et avec d’autres outils.

Deux dépôts, deux rôles

Le découpage a été le premier choix, et c’est celui qui a le mieux tenu. Un dépôt pour ce qui tourne sur un robot, un dépôt pour ce qui tourne sur le serveur. Rien de partagé entre les deux, hormis un script utilitaire recopié.

DépôtTourne surContientCommits
carb0tchaque robotlecture NFC, mesure BLE, écouteur de statut, mini-serveur Flask, conf nginx41
mastercarb0tle serveur centralAPI Symfony, front Django, script maestro, schémas d’architecture21

Les deux dépôts vivent sur ma forge GitLab auto-hébergée, sur un domaine d’infrastructure privé. Je ne publie pas leur adresse ici : désigner une surface interne depuis un site public n’apporte rien à qui lit, et coûte à qui héberge.

L’architecture du master

Le master n’est pas un service, c’est quatre étages empilés sur une même machine. Le schéma ci-dessous est la version redessinée de celui que j’avais tracé à l’époque, et il porte le seul point qui compte vraiment.

Master et flotte de robots Carb0tLe master réunit quatre étages : une base MariaDB, une API Symfony servie par nginx et PHP, un front Django, et un listener Python. Seul le listener communique avec la flotte : il ouvre un socket TCP vers les quatre robots, qui exécutent chacun un agent Python sous Supervisor. La base, l’API et le front n’ont aucun lien direct avec les robots.MASTERFLOTTEMariaDB 10.3:3306nginx · PHP 7.3 · Symfony 5:80 — apimaster.stenzew.localfront Django Material:4200listener Python 3.7socket TCP :8000carb0t 1agent Python · Supervisorcarb0t 2NFC · BLE · :9000carb0t 3NFC · BLE · :9000carb0t 4NFC · BLE · :9000[GET][INFO] → état JSON
Quatre étages sur le master, un seul sort : le listener. Les robots ne connaissent ni l’API, ni la base — ils répondent à un socket.

Ce point, c’est l’asymétrie. Sur les quatre étages du master, un seul parle aux robots : l’écouteur Python, sur un socket TCP en 8000. La base ne les connaît pas. L’API Symfony ne les connaît pas. Le front ne les connaît pas davantage. Un robot qui tombe ne peut donc pas entraîner l’API avec lui — pas par conception défensive, mais parce que rien ne les relie.

C’est le seul choix d’architecture de ce projet que je referais tel quel.

Ce que sait faire un robot

Trois programmes indépendants, chacun tenant en moins de soixante-dix lignes.

Se repérer au badge. Un lecteur NFC interrogé toutes les demi-secondes par nfc-list, dont la sortie est passée au filtre d’une liste d’identifiants de tags. Trois tags connus, trois zones — A, B, C. La position n’est pas calculée, elle est lue : le robot ne sait pas où il est, il sait sur quoi il passe.

Estimer une distance. Un balayage Bluetooth basse consommation, et pour chaque balise reconnue la conversion de la puissance reçue en distance, par la formule empirique des balises iBeacon. Deux adresses en dur, dont une commençant par b8:27:eb — le préfixe constructeur de la Raspberry Pi Foundation. La balise de la zone A était donc un Raspberry Pi posé là.

Répondre quand on l’interroge. Un écouteur qui accepte une connexion, lit la requête et renvoie un état.

Le protocole, ou plutôt son absence

C’est ici que le projet montre son âge, et je le laisse tel quel parce qu’il est instructif.

carb0t/00-scripts/python_multitreading.py
data = connection.recv(16)
s = data.decode()
if data:
    if "[GET]" in s:
        print(" ** Route [GET] detected ** ")
        if "[INFO]" in s:
            print("Build info, object json")
            response_str = info_default()
            connection.sendall(response_str.encode())

Un socket brut, des jetons [GET] et [INFO] cherchés dans la chaîne reçue, et une réponse JSON écrite en dur. J’avais réinventé une requête et une route, en moins bien : la lecture est bornée à seize octets, donc une requête plus longue arrive en morceaux et aucun ne contient les deux jetons ; il n’y a ni code de retour, ni délai d’attente, ni gestion d’une connexion qui se ferme au mauvais moment.

Le contenu de la réponse, en revanche, était juste : identifiant du robot, disponibilité, destination, charge processeur, mémoire, disque, latence réseau, état des capteurs. C’est, à peu de chose près, ce que j’attends encore d’un point de contrôle de santé.

Superviser plutôt que lancer

L’autre moitié du projet, et de loin la plus utile pour la suite, portait sur une question : qui redémarre les programmes quand ils s’arrêtent ?

La réponse a été Supervisor sur chaque machine, et au-dessus un script Python que j’appelais le maestro. Il vérifie ses propres dépendances avant de démarrer, cherche les scripts de supervision sur le disque, les arrête, les relance, et écrit l’état obtenu dans un fichier .state. Un --deploy qui converge vers un état décrit, en somme.

Schéma : le service Supervisor alimente un script maître Python, qui commande cinq scripts superviseurs — deux Python, un Bash, un PHP, un Go — chacun ouvrant plusieurs processus enfants.
Le schéma de supervision tel que je l'avais tracé en avril 2020 : un script maître, un enfant par langage, et leurs processus.

Le schéma prévoyait un superviseur par langage : Python, Bash, PHP, Go. Seule la branche Python a existé. Les trois autres cases sont restées des cases — c’est la trace d’une ambition, pas d’une réalisation, et il vaut mieux le dire que de laisser croire l’inverse.

Un détail mérite d’être sorti du lot, parce que je le regarde différemment aujourd’hui : un programme supervisé bouclait toutes les cinq secondes sur /srv, faisait un git pull de la branche develop dans chaque dépôt trouvé, et redémarrait ce qui devait l’être. C’était, sans que je connaisse le mot, une boucle de réconciliation depuis un dépôt Git — le principe exact que j’applique aujourd’hui avec Argo CD. En version dangereuse : sans signature, sans revue, sans possibilité de retour arrière, et avec un git pull automatique en production.

Où le projet s’est arrêté

Vingt-neuf avril, premier commit. Onze mai, dernier commit. Rien après.

ComposantÉtat en mai 2020Manquait pour aller plus loin
Lecture NFC des zonesfonctionnelle, trois tags reconnusla carte du bâtiment, absente
Mesure de distance BLEfonctionnelle, deux balisesun étalonnage : la formule sortait du brut
Écouteur de statutrépondait, mais un état figéles vraies métriques de la machine
API Symfonyune route renvoyant une constantetout le reste
Frontla démo Django Material, installée telle quelleune interface écrite pour ce projet
Base de donnéesprévue au schémajamais créée
Robotaucunle robot

La dernière ligne est la vraie raison de l’arrêt. J’avais construit l’infrastructure d’une flotte qui n’existait pas, et plus j’avançais côté serveur, plus l’écart avec le matériel devenait visible. Le printemps 2020 a également ramené mon temps libre vers des sujets plus immédiats : trois mois plus tard, ce temps est allé dans FoodQard, qui avait le mérite d’avoir des utilisateurs réels dès la première semaine.

Enseignements

  1. Un dépôt par rôle d’exécution — l’agent d’un côté, le serveur de l’autre — est le choix qui a le mieux vieilli : je découpe encore ainsi.
  2. Restreindre le dialogue avec la flotte à un seul étage a limité la surface de panne sans que je le cherche. C’est devenu délibéré.
  3. Le socket maison a coûté plus cher que n’aurait coûté une route de plus sur l’API qui tournait déjà à côté.
  4. Un « git pull » en boucle sur un serveur, c’est l’idée du GitOps sans aucune de ses protections : ni revue, ni version épinglée, ni retour arrière.
  5. Construire l’infrastructure avant l’usage : sans robot à piloter, rien ne venait dire quelle décision était bonne.

Carb0t reste archivé et le restera. Sa valeur n’est pas dans son code, qui est celui d’un développeur qui apprend, mais dans l’endroit exact où il s’est arrêté — et dans le fait que la moitié des réflexes que j’utilise aujourd’hui en exploitation sont nés de ces deux semaines-là.