Bringup

Le paquet ratbot_bringup (dans ros2_ws/src/) rassemble les fichiers de lancement du robot : tout ce que la Raspberry Pi doit démarrer pour que le RatBot soit opérationnel.

Mettre en service une machine neuve

./scripts/ratbot install <fiche.yaml> amène une Raspberry Pi de l’état « carte fraîchement écrite » à « robot du parc ». Voir deploy/ratbot.example.yaml pour la fiche d’identité, et le guide utilisateur pour la marche à suivre.

Ce qu’il faut savoir côté conception :

Il ne réimplémente rien. Les étapes qui existent déjà — deps, network, build, services — sont appelées telles quelles. Le script n’ajoute que ce qui manquait : identité, UART, source de temps, lidar.

Il est idempotent, et c’est le mode d’emploi. Chaque étape regarde d’abord si le travail est fait. Relancer le script après un redémarrage est la façon normale de vérifier qu’une installation a pris.

Il ne redémarre jamais. Il accumule les raisons de redémarrer et les affiche à la fin. Une reprise automatique en deux passes laisserait une machine à moitié configurée si elle échouait au milieu.

Les quatre choses que lui seul fait

L’identité, et l’effacement d’un clone. Cloner une carte SD est la façon rapide d’équiper un parc, mais la copie porte l’identité de l’original. Le script régénère /etc/machine-id et les clés d’hôte SSH dès que le nom courant commence par ratbot- et ne correspond pas à la fiche. Deux machines au même machine-id obtiennent le même bail DHCP et se coupent l’une l’autre : la panne est intermittente et ne désigne rien.

L’UART. Sur une Raspberry neuve, ttyAMA0 est pris par la console de connexion et par le Bluetooth. Il faut enable_uart=1, dtoverlay=disable-bt, retirer console=serial0 de cmdline.txt et désactiver serial-getty@ttyAMA0. Sans les quatre, la carte est injoignable — et le robot démarre quand même, simplement muet.

Danger

cmdline.txt doit tenir sur une seule ligne : le chargeur de démarrage ne lit que la première. Une substitution qui laisse traîner un retour à la ligne rend la Pi non amorçable, carte SD à sortir pour la réparer.

Le script reconstruit donc la ligne mot à mot, vérifie que root= survit, et refuse d’écrire si le résultat lui paraît douteux. Les deux fichiers de démarrage sont sauvegardés en .avant-ratbot avant la première modification.

La source de temps, écrite depuis la fiche — voir L’heure : une chaîne à trois maillons.

Le lidar, et ici une règle qui vaut pour tout le script : il ne devine pas. La règle udev est écrite avec l’identifiant USB de l’adaptateur réellement branché, lu par lsusb. Si rien n’est détecté, il prévient et ne pose rien plutôt que d’écrire une règle plausible qui ne nommerait aucun matériel — on croirait le lidar configuré. La fiche accepte un lidar_usb_id explicite pour installer une machine dont le lidar n’est pas encore là.

Ce qu’il ne fait pas

  • Le WiFi : posé par Raspberry Pi Imager à l’écriture de la carte. Aucun secret n’entre dans la fiche d’identité.

  • ``micro_ros_agent`` : il n’existe pas en paquet apt et se construit depuis les sources. Le script le rappelle, l’étape reste manuelle.

  • Le redémarrage, déjà dit.

Note

Écrit et éprouvé pour une Raspberry Pi 4. Sur un autre modèle le script prévient et continue : le découpage des ports série diffère. Ce qui bloque, en revanche, est une distribution ROS autre que celle déclarée — deux distributions sur un parc rendent les pannes impossibles à rejouer.

Le vrai test a eu lieu le 2026-09-26 sur ratbot-1, en --dry-run : les quatre vérifications d’identité et d’UART sont passées au vert sur une machine déjà en service, ce qui est exactement ce qu’on attend d’un script idempotent.

Le workspace

Le workspace ROS 2 du robot est le dossier ros2_ws/ du dépôt :

cd ros2_ws
colcon build
source install/local_setup.bash

Prérequis sur la Raspberry Pi :

Warning

ros-humble-micro-ros-agent n’existe pas dans les depots (verifie sur ratbot-1 le 2026-09-05). L’agent se construit depuis les sources : il est declare dans ros2_ws/ratbot.repos, comme explore_lite.

cd ros2_ws
vcs import src < ratbot.repos
colcon build --packages-select micro_ros_msgs micro_ros_agent

Mettre le système à jour — à la main, jamais tout seul

Les mises à jour automatiques d’Ubuntu sont désactivées sur les robots (./scripts/ratbot services s’en charge : apt-daily.timer et apt-daily-upgrade.timer arrêtés, et /etc/apt/apt.conf.d/99-ratbot-pas-de-maj-auto qui met les quatre réglages APT::Periodic à zéro).

Pourquoi : une bibliothèque ROS qui change toute seule entre deux séances rend une panne impossible à rejouer, au labo il n’y a pas d’Internet, et la minuterie se déclenchait précisément au démarrage du robot — 2 min 28 d’apt et 1 min 21 de processeur mesurées sur ratbot-1 le 2026-09-12, pendant que le robot démarrait.

La mise à jour se fait donc quand on le décide, robot au repos et sur un réseau qui a Internet :

sudo apt update                 # rafraîchit les listes, n'installe rien
apt list --upgradable           # ce qui changerait : à LIRE avant
sudo apt upgrade                # installe, sans jamais retirer de paquet
sudo reboot                     # si le noyau ou une libc a bougé

Warning

``apt full-upgrade`` peut retirer des paquets pour résoudre un conflit — y compris une brique ROS dont dépend le robot. Sur un robot, s’en tenir à apt upgrade, et lire ce qu’il annonce avant de valider.

Après une mise à jour qui touche à ROS, reconstruire l’espace de travail et vérifier que le robot parle encore :

./scripts/ratbot build
ros2 node list                  # /ratbot_N/ratbot_controller doit être là
./scripts/ratbot log        # et le journal de bord doit se lire

Pour installer ou réinstaller les dépendances ROS d’un robot neuf : ./scripts/ratbot deps.

Démarrer le robot

robot.launch.py démarre tout ce qu’une Pi doit lancer : le robot_state_publisher (URDF, frames TF préfixées), le garde-barrière cmd_vel_gate (arrêt d’urgence global, voir Les topics du robot) et l’agent micro-ROS, le tout dans le namespace du robot :

ros2 launch ratbot_bringup robot.launch.py robot_id:=1

Arguments :

Argument

Défaut

Rôle

robot_id

1

Numéro N du robot : namespace ratbot_N et frames TF préfixées ratbot_N/.

agent

true

Mettre à false pour travailler sans la carte STM32 (ne lance pas l’agent micro-ROS).

dev

/dev/ttyAMA0

Périphérique série relié à la carte STM32 (connecteur J1). Voir Communication avec la carte STM32 pour la préparation de la Raspberry.

baudrate

3000000

Débit de la liaison série Pi ↔ STM32. Doit correspondre à huart5 dans le firmware (Core/Src/usart.c) : les deux côtés muets l’un pour l’autre est le symptôme d’un désaccord ici.

use_ekf

true

Fusionne odométrie roues + IMU (EKF robot_localization). L’EKF devient la source unique de la TF odom → base_footprint (voir la section suivante).

L’agent micro-ROS seul (pont carte ↔ graphe ROS 2, voir Communication avec la carte STM32) peut aussi être lancé isolément :

./scripts/ratbot agent            # sur le robot : numéro tiré du hostname
ros2 launch ratbot_bringup micro_ros_agent.launch.py robot_id:=1

micro_ros_agent.launch.py est la seule définition de l’agent : boot.launch.py (le service), robot.launch.py (agent:=true) et ratbot agent l’incluent tous. Il place le nœud dans /ratbot_N et le relance s’il meurt (carte reflashée). Il y en avait trois copies, qui avaient divergé : l’une visait encore /dev/ttyACM0, l’ancien lien USB, une autre n’avait pas d’espace de noms. Il pose lui-même /ratbot_N : l’inclure hors de tout PushRosNamespace, sinon il finit dans /ratbot_N/ratbot_N.

Lidar

Hors simulation (use_sim_time:=false) et si use_lidar est vrai, robot.launch.py lance aussi le lidar :

rplidar_node (ratbot_lidar) ── scan_raw ──► ratbot_scan_fix ── scan ──► costmaps, AMCL, slam_toolbox
  • port : argument lidar_port, par défaut /dev/ratbot_lidar — nom stable posé par ratbot install (étape lidar, règle udev appliquée aussitôt par udevadm trigger) ;

  • repère : ratbot_N/laser_link, à l’aplomb de la centrale, 45 mm derrière l’axe des roues (voir Description du robot) ;

  • relais ratbot_scan_fix (ratbot_bringup/scan_fix.py, testé) : le RPLidar A1 de ratbot-1 porte le firmware 1.20, trop ancien pour l’API des modes de mesure ; rplidar_ros 2.1.4 retombe sur l’ancien mode mais publie range_max non initialisé (5,4e-42). Le relais remet 0,15 / 12 m et ne touche pas aux mesures. Une borne déjà sensée est gardée : le jour où un pilote ou un firmware la renseigne, le relais devient transparent ;

  • cadence : 6,5 Hz mesurés dans la pile (10 Hz en simulation).

Warning

Le pilote arrête le moteur et la mesure en quittant. Un second pilote lancé à la main sur le même port — un essai oublié — se dispute la liaison avec celui de la pile (cadence divisée), puis, en s’arrêtant, coupe la mesure pour tout le monde. Après un essai manuel, vérifier avec ps qu’il ne reste qu’un rplidar_node, et redémarrer ratbot.service au moindre doute.

Odométrie fusionnée (EKF)

robot.launch.py démarre un filtre de Kalman étendu (robot_localization, nœud ekf_filter_node) qui fusionne, dans le namespace du robot :

  • base/odom (roues) → vitesse d’avance vx + lacet vyaw ;

  • imu/data_raw (gyroscope) → lacet vyaw (plus fiable que les roues quand elles patinent en rotation).

Il produit odometry/filtered et publie la TF <ns>/odom → <ns>/base_footprint. Configuration : ratbot_bringup/config/ekf.yaml (mode planaire two_d_mode, fusion de vitesses uniquement — la pose est intégrée par le filtre ; aucune orientation absolue n’est fusionnée, l’IMU 6 axes sans magnétomètre n’ayant pas de cap absolu).

« odom » désigne deux choses différentes

C’est la confusion classique, et elle mérite d’être levée une fois pour toutes.

Nom

Nature

Contenu

base/odom

topic

odométrie de roues brute, publiée par la carte. Aucune fusion

odometry/filtered

topic

la fusion roues + centrale, produite par l’EKF

<ns>/odom

repère TF

produit par l’EKF, donc fusionné

La norme REP 105 définit la chaîne map → odom → base_link et exige que le repère odom soit continu et localement juste. Elle ne dit rien de qui produit cette transformée ni avec quels capteurs.

Autrement dit : le repère ``odom`` que consomment Nav2 et SLAM est bien fusionné, conformément à l’usage. Ce qui ne l’est pas, c’est le topic base/odom — la mesure brute de la carte, volontairement laissée telle quelle.

Pourquoi garder la mesure brute séparée

Deux raisons, et les deux ont servi.

La calibration l’exige. UMBmark demande que le robot soit piloté sur sa propre odométrie de roues, sans référence extérieure. Si base/odom était déjà fusionné avec la centrale, la mesure ne dirait plus rien sur les roues et la méthode perdrait son sens — voir Traction : estimateur de roue et odométrie.

La centrale devient un témoin indépendant. Comparer base/odom à sensors/imu/data_raw a démasqué deux défauts en une semaine : les 12° de dépassement par virage dus à la QoS de cmd_vel, et les 6,9° du secteur aveugle. Deux signaux fusionnés n’auraient rien montré — ils se seraient accordés sur la même erreur.

Important

L’EKF est la source UNIQUE de la TF ``odom → base_footprint``. Toute autre source de cette TF donnerait à base_footprint un second parent et casserait TF. Conséquences :

  • Robot réel : le firmware publie base/odom en topic seulement, il ne doit pas publier la TF odom → base.

  • Simulation : la TF du DiffDrive de Gz n’est pas bridgée quand use_ekf:=true (géré dans spawn_robot.launch.py).

Cette TF s’insère sous la localisation globale : le SLAM ou AMCL fournit map → odom, l’EKF fournit odom → base_footprint — chaîne complète map → odom → base_footprint, un seul parent par frame.

Repli : use_ekf:=false désactive l’EKF et rend la TF odom → base à la source brute (DiffDrive en sim, firmware sur le vrai robot).

Vérifier (en simulation, robot 1) :

ros2 topic echo /ratbot_1/odometry/filtered --once     # pose fusionnée
ros2 run tf2_ros tf2_echo ratbot_1/odom ratbot_1/base_footprint

Chaîne de commande (arbitrage, limite, arrêt d’urgence)

Toutes les commandes de vitesse convergent vers un topic interne unique, base/cmd_vel_safe (celui auquel le firmware — ou la simulation — s’abonne), en traversant trois étages lancés par robot.launch.py :

base/cmd_vel_teleop (100) ┐
base/cmd_vel        (50)  ┼─► twist_mux ─► cmd_vel_limiter ─► cmd_vel_gate ─► base/cmd_vel_safe
base/cmd_vel_nav    (10)  ┘   arbitrage      borne v/w         arret d'urgence
  • ``twist_mux`` — arbitre par priorité : la téléop surpasse l’hôte externe (base/cmd_vel), qui surpasse la navigation. Config : config/twist_mux.yaml.

  • ``cmd_vel_limiter`` — borne dure la vitesse d’avance et de rotation (défaut 0,4 m/s et 1,5 rad/s). Appliquée en simulation ET sur le robot réel. Config : config/velocity_limits.yaml. Simple écrêtage — pas de lissage d’accélération ni de jerk (laissé à la boucle moteur du firmware) — ce qui préserve la cadence des messages, donc le watchdog reste opérant.

  • ``cmd_vel_gate`` — force la commande à zéro sur /ratbot_fleet/emergency_stop (voir Les topics du robot). Placé en dernier pour arrêter net, sans lissage.

Important

Défense en profondeur. La borne du cmd_vel_limiter est une couche de cohérence (contournable si la Pi plante) : elle sert surtout à brider le robot de façon identique en simu et en dev. La limite dure et non contournable (v_max, a_max, jerk) reste au firmware STM32, au plus près des moteurs.

Téléopération. Deux entrées, toutes deux sur base/cmd_vel_teleop (priorité haute : elles surpassent les programmes et la navigation) : le clavier (ratbot key N, key_teleop en commande retenue depuis le 2026-10-19 ; avant, teleop_twist_keyboard, qui publiait sur base/cmd_vel, priorité moyenne, jusqu’au 2026-10-15) et le joystick (joy_teleop.launch.py). Le joystick réutilise la stack standard joy + teleop_twist_joy avec un bouton homme-mort ; mapping dans config/joystick.yaml (à caler selon la manette via ros2 topic echo /ratbot_N/joy).

Réseau

Plan d’adressage des Raspberry Pi des robots (réseau 192.168.1.0/24, partagé avec les autres machines du labo) :

Robot

Hostname

IP fixe

ratbot_1

ratbot-1

192.168.1.171

ratbot_2

ratbot-2

192.168.1.172

ratbot_3

ratbot-3

192.168.1.173

ratbot_4

ratbot-4

192.168.1.174

L’adresse fixe se pose avec sudo ./scripts/ratbot network, qui la déduit du numéro du robot (ratbot-N → 192.168.1.(170+N), N de 1 à 19, soit .171 à .189 sous le serveur de temps en .190). Les Pi sont en WiFi ; le point d’accès et son mot de passe restent dans le 50-cloud-init.yaml posé à l’installation, que la commande ne touche pas : elle ajoute un 60-ratbot.yaml qui ne surcharge que l’adressage, et désactive la régénération du réseau par cloud-init. Passerelle et DNS : la box, 192.168.1.254.

Warning

Après un changement d’adresse, relancer l’agent (ou redémarrer). Ses entités DDS annoncent encore l’ancienne adresse : la carte reste connectée, mais plus rien n’apparaît dans le graphe ROS. Même cause, même remède que pour un saut d’horloge.

Et vérifier dans l’interface de la box que 192.168.1.171–174 et .190 sont hors de la plage DHCP — sinon la box finira par attribuer l’une de ces adresses à un autre appareil.

  • Synchronisation du temps : chaque Pi est client chrony du serveur de temps du labo (192.168.1.190), et de lui seul — voir Architecture de l’application firmware pour la chaîne complète jusqu’à la RTC de la carte.

  • ROS_DOMAIN_ID : la même valeur sur toutes les machines qui doivent se voir (robots, caméras, centrale).

Warning

Le domaine ne suffit pas à se voir. Deux nœuds du même domaine doivent encore se découvrir, et le chemin d’annonce diffère selon qu’ils partagent une machine ou non. Sur ratbot-1, lo sans le drapeau MULTICAST rendait la Pi aveugle à sa propre carte tout en voyant parfaitement le PC : la manette ne pouvait rien commander, sans le moindre message d’erreur.

Cause, diagnostic en une commande et correctif : La découverte DDS : qui voit qui.

Warning

Ne pas attendre une interface sans câble. Par défaut, systemd-networkd-wait-online attend que toutes les interfaces gérées soient en ligne. Sur un robot en Wi-Fi, eth0 reste sans câble : l’attente va jusqu’à son délai de deux minutes, et elle bloque cloud-init, donc sysinit.target, donc tout le démarrage.

Mesure sur ratbot-1 le 2026-09-12 : 4 min 30 de démarrage, dont 2 min d’attente et un multi-user.target atteint seulement à 2 min 32.

./scripts/ratbot services installe le correctif : un fichier systemd-networkd-wait-online.service.d/ratbot.conf qui passe --any — une interface en ligne suffit — avec un garde-fou à 10 s. Le Wi-Fi de la Pi met plus de vingt secondes à s’associer, et rien de vital n’attend le réseau : l’agent parle à la carte par la liaison série, chrony se rattrape seul. Résultat mesuré : multi-user.target à 2 min 32, puis à 38 s.

Ce qui démarre tout seul

./scripts/ratbot services installe trois unités systemd. Toutes dérivent robot_id du hostname (ratbot-N), donc rien à configurer par robot.

unité

type

rôle

ratbot-loopback-multicast.service

une fois

pose le drapeau MULTICAST sur lo. Sans lui, les services du robot sont invisibles depuis le robot lui-même — voir La découverte DDS : qui voit qui. Ordonnée Before la pile, parce qu’un participant DDS lit l’état des interfaces à sa création

ratbot.service

permanent

toute la pile embarquée : agent micro-ROS, signalisation, chaîne de commande. Restart=always, et elle attend l’heure (chronyc waitsync) avant de démarrer

ratbot-time.service

une fois

reprend l’heure de la RTC de la carte si la Pi est en retard — le troisième filet, quand ni le réseau ni le serveur du labo ne répondent

Une seule unité pour la pile, et pourquoi

Il y en avait deux auparavant, ratbot-agent et ratbot-signals, la seconde déclarant After=ratbot-agent.service. Ça paraît juste, et c’est faux :

Important

After= ordonne le lancement des processus, pas leur disponibilité.

L’agent étant Type=simple, systemd le considère démarré dès le fork. Mesuré sur ratbot-1 le 2026-09-26 :

agent   : 15:44:19
signals : 15:44:19      <- la meme seconde

Or ce dont status_manager a besoin — session micro-ROS ouverte, puis abonnements créés par la carte — arrive plusieurs secondes plus tard : ses premiers messages partaient avant que quelqu’un ne les écoute.

Note

Ce n’était pas la cause du voyant éteint du 2026-09-26, comme on l’a cru un temps. La vraie cause était logind, qui effaçait la mémoire partagée de la pile à chaque déconnexion SSH — voir La découverte DDS : qui voit qui. Le séquencement ci-dessous reste juste pour lui-même, mais il ne suffisait pas : la sentinelle trouvait l’abonné de la carte en 1,5 s, et le voyant restait éteint.

Et systemd ne peut pas faire mieux : l’ouverture d’une session micro-ROS n’est observable que depuis ROS. Le découpage juste est donc :

systemd  ->  demarre UN processus, et le releve s'il meurt
launch   ->  sequence le reste sur une condition OBSERVEE

Tout le séquencement vit dans boot.launch.py, avec les mécanismes standard de ros2 launch :

étape

mécanisme

l’agent, tout de suite

Node(respawn=True) — il se relève si on reflashe la carte

attendre que la carte soit là

wait_for_board, qui attend un abonné sur status/led

puis le reste

RegisterEventHandler(OnProcessExit(...)) sur la sentinelle, qui inclut robot.launch.py (avec agent:=false) et status.launch.py

la signalisation

status_manager est lui aussi en respawn=True. La carte garde la dernière couleur reçue sur status/led, sans limite de durée : mort, il laisserait le voyant figé — arrêt d’urgence et batterie faible ne s’afficheraient plus — jusqu’au prochain redémarrage du service. Relevé en 2 s (vérifié par kill -9)

Note

Une condition, pas un délai. Un sleep fixe marche sur l’établi et casse au premier démarrage à froid avec une carte SD lente, ou quand la carte vient d’être reflashée. Le temps ne sert que de garde-fou : à son expiration (wait_timeout, 60 s) la suite démarre quand même, dégradé mais vivant — le même choix que le - devant chronyc waitsync.

Note

Pourquoi la signalisation démarre avec la pile et non avec ratbot up : un robot allumé doit montrer son état même si personne n’a rien lancé — batterie, arrêt d’urgence, agent perdu. La lier à la navigation la ferait disparaître exactement quand on en a le plus besoin.

ratbot-time est voulu par la pile (WantedBy=ratbot.service) et non par multi-user.target : ratbot.service est déjà ordonné After=multi-user.target, et une cible s’ordonne après ce qu’elle veut. Avec WantedBy=multi-user, on obtient le cycle multi-user -> time -> pile -> multi-user, que systemd casse en supprimant un démarrage au hasard. C’est arrivé, et la Pi est restée 44 jours en retard.

À la main

./scripts/ratbot boot        # la meme chose qu'au demarrage, au premier plan
systemctl status ratbot      # etat de la pile
journalctl -u ratbot -f      # ce qu'elle raconte

TODO

  • namespace du nœud micro-ROS côté firmware (la carte doit connaître son numéro — voir Les topics du robot) ;

  • procédure d’installation d’une Pi (IP fixe nmcli, chrony client, ROS_DOMAIN_ID).