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 |
|---|---|---|
|
|
Numéro |
|
|
Mettre à |
|
|
Périphérique série relié à la carte STM32 (connecteur J1). Voir Communication avec la carte STM32 pour la préparation de la Raspberry. |
|
|
Débit de la liaison série Pi ↔ STM32. Doit correspondre à
|
|
|
Fusionne odométrie roues + IMU (EKF |
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é parratbot install(étapelidar, règle udev appliquée aussitôt parudevadm 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_ros2.1.4 retombe sur l’ancien mode mais publierange_maxnon 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’avancevx+ lacetvyaw;imu/data_raw(gyroscope) → lacetvyaw(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 |
|---|---|---|
|
topic |
odométrie de roues brute, publiée par la carte. Aucune fusion |
|
topic |
la fusion roues + centrale, produite par l’EKF |
|
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/odomen topic seulement, il ne doit pas publier la TFodom → base.Simulation : la TF du DiffDrive de Gz n’est pas bridgée quand
use_ekf:=true(géré dansspawn_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 |
|
192.168.1.171 |
ratbot_2 |
|
192.168.1.172 |
ratbot_3 |
|
192.168.1.173 |
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 |
|---|---|---|
|
une fois |
pose le drapeau |
|
permanent |
toute la pile embarquée : agent micro-ROS, signalisation, chaîne de
commande. |
|
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 |
|
attendre que la carte soit là |
|
puis le reste |
|
la signalisation |
|
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).