Les topics du robot
Le contrat d’interface
Le RatBot expose son matériel exclusivement sous forme de topics ROS 2
standards (geometry_msgs, nav_msgs, sensor_msgs,
std_msgs…). C’est son contrat d’interface : tout projet hôte peut le
piloter et l’observer sans dépendre du moindre paquet du RatBot, et
réciproquement le RatBot ne dépend d’aucun paquet du projet hôte.
Le schéma à trois niveaux
Les topics suivent un nommage à trois niveaux porteurs de sens :
/<instance>/<sous-système>/<interface>
ratbot_N base, imu, cmd_vel, odom,
health, ui temperature, ...
Les sous-systèmes reflètent l’organisation matérielle du robot (et les threads FreeRTOS du firmware) :
/ratbot_1/
├── base/ la traction
│ ├── cmd_vel ⬇ geometry_msgs/Twist (contrat public)
│ ├── cmd_vel_safe ⬇ geometry_msgs/Twist (interne, vers la carte)
│ ├── odom ⬆ nav_msgs/Odometry
│ └── joint_states ⬆ sensor_msgs/JointState
├── sensors/ ce que la carte mesure
│ ├── imu/data_raw ⬆ sensor_msgs/Imu
│ ├── temperature ⬆ sensor_msgs/Temperature
│ ├── battery ⬆ sensor_msgs/BatteryState
│ └── tof/
│ ├── depth ⬆ sensor_msgs/Image (rien sans capteur)
│ └── points ⬆ sensor_msgs/PointCloud2 (calculé sur la Pi)
├── status/ état synthétisé (côté Pi) et de la carte
│ ├── led ⬇ std_msgs/ColorRGBA
│ ├── sound ⬇ std_msgs/UInt8
│ ├── emergency_stop ⬆ std_msgs/Bool (la CARTE, verrou matériel)
│ └── state ⬆ std_msgs/String
├── screen/ écran OLED
│ ├── command ⬇ ratbot_msgs/ScreenCommand
│ ├── heartbeat ⬇ std_msgs/Empty
│ └── button ⬆ std_msgs/UInt8
└── ui/ boutons et voyants utilisateur
├── buttons ⬆ sensor_msgs/Joy
└── leds ⬇ std_msgs/UInt8
Règles lexicales : topics = substantifs, tout en snake_case ; les
données remontent (⬆) et les commandes descendent (⬇) dans le
namespace de l’entité.
Ce que publie et reçoit la carte STM32
Ces topics sont les seuls qui consomment le budget d’entités micro-ROS (voir Communication avec la carte STM32). Tout ce qui vit côté Raspberry est gratuit.
Topic |
Type |
Sens |
Cadence |
Rôle |
|---|---|---|---|---|
|
|
⬇ |
à la demande |
Consigne de vitesse effective : la sortie du garde-barrière, pas le contrat public. Watchdog dans le firmware : arrêt des moteurs si silence de plus de ~500 ms. |
|
|
⬆ |
50 Hz |
Odométrie intégrée depuis la position des servos. Cadence tenue par le compteur de pas de l’asservissement, un pas sur deux, et non par une horloge : elle ne peut donc pas dériver. Chaque message est daté de la mesure, pas de la publication — l’instantané transporte le tick du pas qui l’a produit. |
|
|
⬆ |
10 Hz |
Position/vitesse des roues (animation du modèle dans RViz). |
|
|
⬆ |
25 Hz |
Gyroscope + accéléromètre bruts, moyenne de 4 échantillons pris à 100 Hz. Voir la section IMU ci-dessous. |
|
|
⬆ |
1 Hz |
Heure de la RTC sur pile de la carte ( |
|
|
⬆ |
1 Hz |
Pont diviseur sur |
|
|
⬆ |
1 Hz |
TMP102 sur I2C2, placé entre les deux régulateurs. |
|
|
⬆ |
15 Hz |
Capteur de distance avant, 8 × 8 distances en mm, pilote ST (ULD 2.0.1). Rien n’est publié sans capteur ; l’image d’essai de l’étape 2 ne sert plus qu’au banc (voir plus bas). |
|
|
⬇ |
événement |
Voyant RGB d’état. Les trois canaux sont modulables — voir ci-dessous. |
|
|
⬇ |
événement |
Buzzer passif : identifiant de mélodie — voir ci-dessous. |
|
|
⬆ |
changement + 1 Hz |
Verrou d’arrêt d’urgence de la carte. À ne pas confondre avec
|
|
|
⬆ |
événement |
Les 4 boutons utilisateur |
|
|
⬇ |
événement |
Bitmask des 4 LED de façade. |
|
|
⬇ |
événement |
Ce qu’il faut afficher sur l’écran OLED. |
|
|
⬇ |
2 Hz |
« ROS est vivant ». Son absence fait basculer l’écran sur son animation de repos autonome. |
|
|
⬆ |
événement |
Les 6 boutons de navigation de l’écran (1 = haut … 6 = ESC). |
Bilan : 12 publishers, 6 abonnements, 5 services (hors serveur de paramètres), sur un budget de 24 / 16 / 8. Recompté dans le code le 2026-10-14 ; la page annonçait 7 publishers et 1 service.
Budget en messages par seconde
La contrainte réelle n’est pas la bande passante mais le nombre de messages
par seconde — plafond mesuré à ~240 (voir Communication avec la carte STM32),
avant que le chemin de publication passe en ITCM (2026-10-14) : il est
désormais plus haut, mais n’a pas été remesuré. Chaque publication coûte
moins cher en calcul ; l’attente de fin d’envoi DMA, elle, reste (~20 % de
la boucle, compteurs du bloc serial link).
base/odom 50 Hz -> 50 mesure
sensors/imu/data_raw 25 Hz -> 25 mesure
base/joint_states 10 Hz -> 10
sensors/tof/depth 15 Hz -> 15
sensors/*, status/* 1 Hz -> 4
/diagnostics -> 9 (18 blocs, 2 par message)
evenements -> ~0
------
~110
Qui fixe la cadence, et où
Aucune de ces périodes n’est écrite dans la couche ROS du firmware. Le
matériel vit dans ses propres tâches — App/traction.c et App/sensors.c,
toutes deux à 100 Hz — qui déposent leurs mesures dans un instantané et
incrémentent un compteur. La tâche micro-ROS publie un topic quand son
compteur a changé, et rien d’autre.
La cadence d’un topic a donc une seule définition, dans le module qui fait la mesure ; et une mesure ne peut être ni republiée deux fois, ni sautée sans qu’on le voie.
Le mécanisme complet — les quatre tâches, la hiérarchie de priorités, le
seqlock, l’horodatage à la mesure — est décrit dans
Architecture de l’application firmware. C’est aussi là que se trouve la méthode de
mesure : ros2 topic hz échantillonne des arrivées côté Raspberry et donne
±10 % sur des versions identiques du firmware, ce qui ne permet de conclure
rien.
La centrale inertielle
Le modèle retenu est un LSM6DSOX (6 axes, I2C), à brancher sur le connecteur J9 — le bus I2C3 lui est entièrement dédié, sans partage ni conflit d’adresse avec le TMP102 (qui est sur I2C2).
Elle est lue à 100 Hz, sa cadence d’échantillonnage naturelle (ODR 104 Hz), par la tâche capteurs — et chaque message porte la moyenne de quatre échantillons, pas un échantillon sur quatre. La différence n’est pas cosmétique : décimer replie le bruit dans la bande utile, moyenner le divise par deux. Le filtre passe-bas interne du gyroscope (LPF1, ~11,5 Hz) reste en place ; il n’est plus le seul rempart contre le repliement.
Le bus I2C tourne à 400 kHz et non aux 100 kHz générés par CubeMX. À
100 kHz la rafale de douze octets dure 1,4 ms, pendant lesquelles le pilote HAL
scrute : à 100 Hz cela faisait 14 % du processeur pris à micro-ROS, et
base/odom tombait de 22,9 à 16,4 Hz. Le réglage n’existe pas dans le
.ioc : il est vérifié par scripts/firmware-fixes.
Le critère de choix a été la stabilité du biais du gyroscope : c’est la seule grandeur que l’EKF fusionne (la vitesse de lacet), et c’est elle qui détermine la dérive du cap. Le reste — accéléromètre, orientation absolue — n’est pas utilisé.
Important
Si l’IMU ne répond pas au démarrage, lancer le robot réel avec
use_ekf:=false : sinon l’EKF attend un sensors/imu/data_raw qui
n’arrivera jamais. Le bloc capteurs de /diagnostics le dit tout de
suite. En simulation Gz l’IMU existe, donc rien à changer de ce côté — c’est
le genre d’écart qui fait « marcher en simu et pas sur le robot ».
Le capteur de distance avant (ToF)
Un VL53L5CX (8 × 8 zones, champ de 45° × 45°), à l’avant, à 10 cm du sol, sur J4 (I2C2, partagé avec le TMP102). Il voit les obstacles trop bas pour le lidar, dont le pied des tables à pied central. Décidé le 2026-10-14 ; la simulation est faite, la carte suivra (Feuille de route).
Le contrat est sensors/tof/depth, une sensor_msgs/Image standard :
8 × 8 pixels, codage
16UC1, une distance en millimètres par zone, 0 pour une zone sans mesure valide — la convention ROS des images de profondeur (REP 118). La carte réduit la dizaine de statuts du capteur à valide / invalide ;repère
ratbot_N/tof_optical_link(repère caméra : z devant, x à droite, y en bas) ; ligne 0 = haut de la scène, colonne 0 = sa gauche, vue de derrière le robot. La lentille du capteur renverse l’image (fiche technique DS13754, § 7.1.3) : la remettre dans ce sens est le travail de la carte ;15 Hz, best effort, ~200 octets par message : +15 messages par seconde sur un plafond de ~240, ~1 % du lien à 3 Mbaud.
Un message standard plutôt qu’un message à nous : micro-ROS connaît déjà
sensor_msgs, et un type nouveau imposerait de reconstruire sa bibliothèque.
Côté Pi, ratbot_tof_cloud (ratbot_bringup, lancé par
robot.launch.py quand use_tof:=true) en fait sensors/tof/points, le
nuage que lit Nav2. Il reconnaît le sol zone par zone. Une zone qui regarde
le sol couvre un cône de 5,6°, rend la distance la plus proche de ce cône et le
point est placé sur l’axe de la zone : plus haut que le sol, jusqu’à 5 cm vers
1 m pour la rangée sous l’horizon. Un seuil de hauteur en ferait des obstacles.
Connaissant la pose du capteur (TF de l’URDF), le nœud calcule où chaque zone
commence à voir le sol ; une mesure à cette distance, à 12 % près (trois fois
le bruit), est du sol, ramené à z = 0 : il sert à effacer, pas à marquer
(ratbot_bringup/tof_geometry.py, testé dans test/test_tof.py).
Le tangage au freinage. Ce début du sol est calculé pour un robot qui
pique du nez de 1,5° (floor_pitch_deg), et non à plat. La pose du
capteur est celle de l’URDF ; or le robot pique du nez au freinage, de 1° en
simulation, et dès 0,8° le sol de la rangée sous l’horizon arrivait plus tôt
que prévu et devenait un obstacle de 6 cm à 85 cm devant (décor lisv,
2026-10-25 : une marque sur 79 % des images prises entre 0,5 et 1° de
tangage, 0,9 % à plat ; après, 0 % sur 609 images). Élargir la marge à 25 %
réglait le tangage mais rendait invisible un socle de 4 cm à 40 cm, vu par une
seule rangée à 82 % de son début de sol : fausse piste, gardée au banc. Avec
1,5° : rien jusqu’à 2° de tangage, un socle de 4 cm vu jusqu’à 70 cm au lieu
de 78. Sur le vrai robot, le tangage au freinage reste à mesurer.
Distance radiale ou perpendiculaire : la fiche technique ne dit pas si la
distance d’une zone est mesurée le long du rayon ou selon l’axe du capteur.
Le paramètre distance_mode (radial par défaut, ou perpendicular)
couvre les deux ; l’écart atteint 11 % dans les zones des coins. À trancher
devant un mur, le capteur monté.
L’effacement passe par un second nuage, sensors/tof/clearing. Nav2
n’efface que le long des rayons des points reçus ; or une zone qui ne voit rien
à portée (1,5 m) ne rend aucun point, et dans une pièce ce sont les zones du
haut. Il ne restait que les rayons du bas, vers le sol proche : une marque
fausse à 15-30 cm de haut n’était plus jamais traversée, et les marques
accumulées bloquaient le robot (décor lisv, 2026-10-25 ; couche ToF
coupée, plus aucune tache). Chaque zone mesure l’obstacle le plus proche de son
cône : tout le cône est libre jusqu’à cette distance, ou jusqu’à
clear_range (1,5 m) sans mesure. clearing_points y trace 3 × 3 rayons
par zone (à 1 m, deux axes voisins sont à 10 cm, plus que les cubes de 5 cm de
la couche), arrêtés au sol. Sur un même trajet de 6 buts dans lisv : cases
fausses de la carte globale 4,4 en moyenne et 12 au plus avant, 1,5 et 6
après, 0 à la fin. Il en apparaît encore quelques-unes : la cause de ces
marques reste à trouver. Prix à payer sur le vrai robot : une zone en erreur,
ou un obstacle trop sombre pour être vu, efface ce qui est sur son cône.
Dans Nav2, une couche tof_layer à part, en voxels
(ratbot_nav/config/nav2.yaml) : en 2D, un rayon qui touche le sol plus loin
passerait « au-dessus » du pied d’une table et l’effacerait dès que le robot,
trop près, ne le voit plus ; et le lidar effacerait ce que le ToF a marqué.
Le voyant RGB et son canal bleu
status/led accepte un ColorRGBA et les trois canaux sont
modulables. Ils ne le sont pas de la même façon, et la différence mérite d’être
connue.
Rouge (PE9) et vert (PE11) sont sur TIM1_CH1 et CH2 : PWM
matériel, le timer fait tout.
Le bleu est sur PC5, qui n’a aucun canal de timer — un défaut de
conception à corriger sur la prochaine révision de la carte. Il est donc piloté
par un PWM logiciel cadencé par le matériel : TIM1 tourne déjà pour les
deux autres canaux (PSC = 0, ARR = 65535, soit ~1,8 kHz) et son
canal 3 est libre. On s’en sert comme simple comparateur :
interruption UPDATE (fin de periode) -> PC5 a 1
interruption COMPARE CH3 (a b x ARR) -> PC5 a 0
Important
TIM1_CH3 n’est pas activé en sortie : seul son drapeau
d’interruption est utilisé. Ce point n’est pas cosmétique — la sortie de ce
canal tombe sur PA10, qui est le /OE du bus dynamixel 1. L’activer
ferait piloter le bus moteur par le voyant bleu.
Coût : deux interruptions par période, soit 3,7 kHz, environ 0,05 % du CPU.
Elles n’écrivent qu’un registre BSRR et sont à une priorité basse — un
voyant ne doit jamais retarder le bus moteur ni le lien micro-ROS.
Aux extrémités (b à 0 ou 1) les interruptions sont coupées et le niveau
forcé : à rapport cyclique nul le comparateur déclencherait au même instant que
l’update, et à rapport plein il ne servirait à rien.
Les mélodies du buzzer
ros2 topic pub --once /ratbot_1/status/sound std_msgs/msg/UInt8 "data: 2"
id |
Nom |
Usage |
|---|---|---|
0 |
silence |
coupe immédiatement ce qui joue |
1 |
bip |
accusé de réception |
2 |
ok |
deux notes montantes |
3 |
erreur |
deux notes descendantes |
4 |
démarrage |
arpège, joué à la mise sous tension |
5 |
alerte |
sirène, deux tons alternés |
6 |
urgence |
aigu et haché, joué tout seul quand l’arrêt d’urgence se déclenche |
Un identifiant inconnu est compté et signalé en WARN dans /diagnostics,
il ne coupe pas le son en cours.
Note
Seul le buzzer passif (PB9, TIM17_CH1) est utilisé : il sait
faire des notes, donc il sait tout faire. L’actif (PC13) ne produit
qu’une fréquence fixe — et cette broche est à courant limité (~3 mA) :
elle n’attaque ici qu’une grille de MOSFET à travers 100 Ω, ce qui est dans
les clous, mais jamais de charge directe.
Les mélodies sont non bloquantes : buzzer_play() arme,
buzzer_spin() avance à 100 Hz depuis la tâche micro-ROS. Une mélodie
bloquante figerait le nœud le temps de jouer.
L’arrêt d’urgence s’entend : celui qui appuie sur le champignon ne regarde
pas forcément l’écran, et personne ne lit /diagnostics dans l’urgence.
Arrêt d’urgence : trois couches indépendantes
La mise en sécurité n’est pas un mécanisme mais trois, empilés. Chacune a un seul propriétaire, et la chute de l’une n’emporte pas les autres.
# |
Où |
Effet |
Survit à |
|---|---|---|---|
1 |
firmware — contact |
coupe le couple des servos, et verrouille |
tout : Pi éteint, réseau coupé, agent mort, ROS absent |
2 |
firmware — watchdog |
arrête si le flux se tait plus de 500 ms |
perte du Pi, du réseau ou de l’agent |
3 |
Pi — |
force les commandes à zéro |
rien de matériel : c’est un arrêt de confort |
Important
La couche 1 verrouille. Une fois déclenchée elle le reste, même contact relâché. C’est délibéré : un arrêt d’urgence qui se relève tout seul quand on relâche le champignon n’est pas un arrêt d’urgence.
Le réarmement se fait de deux façons, qui refusent toutes deux tant que le contact est encore actif — on ne redémarre pas un robot sous les mains de quelqu’un :
devant le robot : 5 appuis sur SW1 en moins de 4 s, traités par la tâche temps réel, sans ROS — voir
App/rearm_gesture.h;à distance : le service
rearm.
Jamais par le bouton d’arrêt lui-même : dans une urgence, on le tape plusieurs fois, et ce geste de panique ne doit pas relancer le robot.
Sur le front, la carte coupe le couple des dynamixels, pas seulement la consigne de vitesse. C’est plus fort : même si la boucle d’asservissement partait en vrille, les moteurs restent hors tension.
L’état est publié sur status/emergency_stop (std_msgs/Bool), sur
changement et rafraîchi à 1 Hz. Le rafraîchissement remplace un
transient_local : celui-ci suppose que l’émetteur reste vivant, ce qui est
justement l’hypothèse qu’un arrêt d’urgence ne doit pas faire. Un abonné qui
démarre en retard converge en une seconde.
La polarité du contact
ARR_URG est un contact externe sur J8 (PE12 = J8.2, tirage bas par
R42), câblé entre le 3V3 et PE12.
La carte a été conçue pour un contact normalement fermé : circuit fermé au
repos, et c’est l’ouverture qui déclenche. Un fil cassé met alors en sécurité.
C’est la sécurité positive, et c’est le bon montage : depuis le 2026-10-27, les
trois robots portent un champignon d’arrêt d’urgence 1NC à accrochage
(mesuré sur ratbot-1 : PE12 à 1 relâché, à 0 enfoncé).
ESTOP_ASSERTED_LEVEL vaut 0 dans safety.c et le bloc
emergency stop porte failsafe: 1.
Note
Du 2026-09-26 au 2026-10-27, ratbot-1 portait un poussoir 1NO
momentané : polarité inversée (ESTOP_ASSERTED_LEVEL à 1), et un fil
cassé ou un J8 vide lus comme « tout va bien » (failsafe: 0). Un firmware
de cette période, sur un robot muni du champignon 1NC, reste à l’arrêt
bouton relâché, et bipe.
circuit FERME -> PE12 a 1 -> tout va bien
circuit OUVERT -> PE12 a 0 -> ARRET
C’est l’arrêt qui est déclenché par l’ouverture du circuit, et ce n’est pas un détail de câblage : il part aussi bien quand on appuie sur le champignon que quand un fil casse, qu’un connecteur se débranche, ou que la carte perd le contact. Une panne de câblage arrête le robot au lieu de le laisser filer.
Important
Conséquence assumée : sans rien branché sur J8, le robot démarre à l’arrêt et refuse de bouger. Tant que le champignon n’est pas installé, mettre un cavalier sur J8 pour fermer le contact.
Un cavalier se voit et s’enlève forcément pour brancher le vrai bouton ; un contournement logiciel s’oublie. C’est pour ça qu’il n’y en a pas.
La constante ESTOP_ASSERTED_LEVEL de safety.c est le seul endroit où
cette polarité est écrite.
Services
Deux aujourd’hui :
rearm(std_srvs/Trigger) — lève le verrou d’arrêt d’urgence. Renvoiesuccess=falseavec le motif si le contact est encore actif. Le même réarmement existe sans ROS : 5 appuis sur SW1.reset_odometry(std_srvs/Trigger) — remet l’odométrie à zéro sans redémarrer la carte.
Le budget en autorise 8. Candidats pour la suite :
get_firmware_version (savoir ce qui tourne sans ST-LINK) et
clear_screen.
Règle de partage topic / service : un topic pour un flux ou une commande qui peut se répéter, un service pour une action ponctuelle qui appelle une réponse. Une LED se commande fréquemment, donc c’est un topic ; remettre l’odométrie à zéro se fait une fois et on veut savoir si ça a marché, donc c’est un service.
Paramètres de la carte
Certains réglages doivent pouvoir changer sans reflasher : ce sont ceux qu’on calibre. L’entraxe des roues en est l’exemple type — on le règle en faisant tourner le robot sur lui-même et en mesurant l’erreur d’angle, ce qui demande plusieurs essais.
La carte expose donc un serveur de paramètres ROS 2 (rclc_parameter) :
ros2 param list /ratbot_1/ratbot_controller
ros2 param set /ratbot_1/ratbot_controller wheel_separation 0.140
ros2 param get /ratbot_1/ratbot_controller wheel_radius_left
Paramètre |
Défaut |
Rôle |
|---|---|---|
|
1 |
1 à 99. Donne l’espace de noms |
|
0.02597 |
un rayon par roue depuis le 2026-09-26. Deux roues du même modèle n’ont pas le même diamètre effectif, et l’écart — de l’ordre du pour cent — fait dériver une ligne droite. C’est l’une des deux erreurs que la calibration UMBmark sépare. Avant calibration, les deux portent la même valeur mesurée au pied à coulisse |
|
0.140 |
l’entraxe effectif, celui qui fait tomber les rotations juste, pas
la cote mécanique. Seconde erreur corrigée par UMBmark. La flash fait
foi, et |
|
500 |
watchdog : arrêt des moteurs si le silence dure. Borné à 50–5000 ms |
|
0.18 / 1.5 |
borne logicielle appliquée dans |
|
14 / 10 |
les deux servos partagent un bus : le serveur refuse de leur donner le même numéro. Les deux valeurs ont été échangées le 2026-09-24 — une consigne à gauche faisait tourner à droite, et l’odométrie voyait chaque dérive du côté opposé |
|
100 / 100 |
vitesse de chaque ventilateur, 0 à 100 %, 0 l’arrête. Réglable à chaud depuis le 2026-09-26 ; c’était une constante compilée. Le pilotage est dans la tâche IHM, pas dans micro-ROS : le ventilateur doit tourner même agent mort |
|
18 |
servos de roue, par numéro de modèle ROBOTIS : 18 (AX-18A, protocole
1.0) ou 1060 (XL430-W250, protocole 2.0) ; toute autre valeur est
refusée. Pris en compte au redémarrage : |
|
0 |
sans objet en traction dynamixel — la carte est générique, le champ existe pour un robot à moteurs CC. Pas exposé en paramètre ROS |
Treize paramètres sont exposés (max_params à 14). Il doit suivre : il
était à 8 pour 8 paramètres déclarés — marge nulle — et toute déclaration
supplémentaire aurait fait échouer ros_params_init(), donc le nœud entier.
Toute valeur hors bornes est refusée, et ros2 param set le dit. Valider
ici plutôt que de laisser passer évite les pannes qui ne se voient qu’à
l’usage : un entraxe nul, par exemple, ferait diverger toute rotation.
Important
``robot_id`` ne prend effet qu’au redémarrage. L’espace de noms est figé à la création du nœud ROS ; le firmware lit donc la flash avant de créer le nœud. La nouvelle valeur est acceptée et écrite tout de suite, mais le robot continue de publier sous l’ancien nom jusqu’au prochain reset.
ros2 param set /ratbot_1/ratbot_controller robot_id 2
# puis reset de la carte -> tout bascule sous /ratbot_2/
Note
Le serveur de paramètres consomme 5 services (get, get_types,
set, list, describe), 1 publisher et 5 handles
d’exécuteur. C’est ce qui a fait passer RMW_UXRCE_MAX_SERVICES de 8 à
16 et ROS_EXECUTOR_HANDLES de 8 à 16 — voir
Communication avec la carte STM32.
Persistance : la flash de données
Un paramètre posé à chaud est perdu au redémarrage. La carte porte deux flash externes de 8 Mio, et c’est bien la seconde qu’il faut utiliser ici :
Puce |
Bus |
Usage |
|---|---|---|
U6 |
QUADSPI, CS |
les programmes — on exécute dedans (voir Bootloader et exécution depuis la flash externe) |
U7 |
SPI1, CS |
les données — c’est ici que va la configuration |
Écrire dans U7 n’a aucune des contraintes de U6 : on ne l’exécute pas, donc ni mode memory-mapped à quitter, ni secteur interdit. Le bring-up l’a validée de bout en bout (effacement 4 Kio en 57 ms, écriture d’une page en 1 ms, relecture conforme).
La configuration tient dans un secteur de 4 Kio :
typedef struct {
uint32_t magic; /* 'RBOT' : secteur valide ? */
uint16_t version; /* evolution du format */
float wheel_radius_left;
float wheel_radius_right;
float wheel_separation;
float gyro_scale; /* depuis la version 4 */
uint32_t encoder_ticks_per_rev;
uint16_t cmd_vel_timeout_ms;
float max_linear, max_angular;
uint8_t robot_id;
uint8_t dxl_id_left, dxl_id_right;
uint8_t fan1_pct, fan2_pct;
uint16_t wheel_servo_model; /* 18 = AX-18A, 1060 = XL430 */
uint32_t crc32; /* DOIT rester le dernier champ */
} ratbot_config_t;
Au démarrage : lecture, vérification de la signature et du CRC, repli sur les valeurs par défaut au moindre doute.
L’écriture est temporisée de 3 secondes après la dernière modification
(SAVE_SETTLE_MS). Régler dix paramètres à la suite ne doit coder qu’un seul
cycle d’effacement : une flash s’use, et c’est exactement ce qu’on fait pendant
une calibration. Conséquence à connaître : un reset dans la seconde qui suit un
param set perd la dernière valeur.
config_defaults() fait un memset avant de remplir, parce que la
structure contient du bourrage entre les champs et que ce bourrage entre dans
le CRC.
Danger
Changer ``RATBOT_CONFIG_VERSION`` efface la configuration stockée, en
silence. config_load() compare la version lue à celle attendue et
retombe sur les valeurs par défaut dès qu’elles diffèrent — identifiants des
moteurs compris. C’est délibéré : relire une structure dont on ignore la
disposition serait pire.
Vérifié sur le robot le 2026-09-26, lors du passage en version 2 :
/diagnostics a bien annoncé defaults in use: blank flash or bad CRC
avec loaded_from_flash: 0. Les valeurs paraissaient pourtant intactes,
simplement parce que les défauts compilés portaient déjà les mesures du
robot. La perte était réelle mais invisible.
La parade n’est pas dans le firmware, elle est dans l’outillage :
./scripts/ratbot config 1 save avant_flash.yaml
# ... flash ...
./scripts/ratbot config 1 load avant_flash.yaml
ratbot config (script scripts/ratbot-config) lit et réécrit les
paramètres par l’interface ROS. Il refuse un fichier venant d’un autre
robot, ne restaure jamais robot_id — c’est l’identité, pas un réglage —
et ignore en les nommant les paramètres que le firmware ne connaît plus,
ce qui est précisément le cas après un changement de version. Il relit
ensuite la carte pour vérifier que tout a été appliqué : ros2 param load
peut rendre un succès alors que la carte a refusé une valeur hors bornes.
Warning
Le CRC n’est pas décoratif. Une coupure d’alimentation pendant l’écriture laisserait un secteur à moitié écrit ; sans contrôle d’intégrité, le robot redémarrerait avec un entraxe aberrant et une odométrie fausse, sans rien signaler.
La W25Q64 tient environ 100 000 cycles d’effacement : sans commune mesure avec le nombre de sauvegardes qu’on fera. Et il reste 8 Mio moins un secteur pour la suite — journal persistant, courbes de calibration moteur, cartes.
Ce qui vit sur la Raspberry
Ces topics ne passent pas par la liaison série : ils sont produits et consommés sur le Pi, ne consomment aucune entité micro-ROS, et ne comptent pas dans le budget de messages.
Topic |
Type |
Rôle |
|---|---|---|
|
|
Le contrat public. Entrée du |
|
|
Entrées prioritaires du |
|
|
État courant de la machine à états ( |
|
|
Permet à n’importe quel nœud d’imposer un état par son nom. |
|
|
Santé agrégée, topic global (convention ROS, lu par
|
|
|
Intentions haut niveau déclenchées par le menu de l’écran. |
|
|
Rendu de l’écran pour RViz, en simulation uniquement. |
Arrêt global
Unique exception au nommage par namespace : le topic absolu
/ratbot_fleet/emergency_stop (std_msgs/Bool), commun à tous les robots.
true= tous les robots forcent leur commande de vitesse à zéro, et restent arrêtés tant quefalsen’a pas été publié ;côté robot, le nœud
cmd_vel_gate(paquetratbot_bringup, lancé parrobot.launch.py) filtrebase/cmd_vel→base/cmd_vel_safe, le topic interne auquel le firmware s’abonne.
QoS : reliable + transient_local (latché). La souscription du
cmd_vel_gate est durable : un robot apparié en retard — découverte DDS
en cours (un --once volatile pouvait n’atteindre qu’une partie des
robots), ou robot démarré pendant un arrêt d’urgence — reçoit le dernier
état publié. L’émetteur doit donc publier en ``transient_local``,
sinon l’appariement échoue silencieusement :
ros2 topic pub --qos-reliability reliable --qos-durability transient_local \
/ratbot_fleet/emergency_stop std_msgs/msg/Bool "data: true" # Ctrl-C pour rendre la main
Limitation résiduelle : l’état ne survit que tant qu’un publisher est vivant (pas de service de durabilité DDS). Pour un e-stop opéré depuis la machine centrale, un nœud qui maintient un publisher latché en vie fait office de mémoire.
Important
Ce n’est pas un arrêt d’urgence au sens sécurité : il transite par
le WiFi et du logiciel. La mise en sécurité réelle est assurée en
dessous, par le watchdog du firmware (silence de cmd_vel > ~500 ms
→ arrêt des moteurs), qui fonctionne même WiFi coupé.
Pas de paquet de messages custom
Le contrat est presque entièrement en messages standards, et c’est volontaire : un message custom recrée la dépendance que le contrat cherche à éliminer — un projet hôte devrait compiler les interfaces du RatBot juste pour lui parler — et chaque message custom impose de régénérer la bibliothèque micro-ROS du firmware.
Une seule exception, assumée : ratbot_msgs/ScreenCommand. Un écran ne
se décrit pas avec un type standard, et découper l’affichage en plusieurs
topics std_msgs aurait produit des états incohérents (titre d’un message,
lignes d’un autre). Le paquet ratbot_msgs ne contient que des .msg,
jamais de code.
Les autres tentations ont été écartées : les boutons passent en
sensor_msgs/Joy, les LED en std_msgs/UInt8 (bitmask), le voyant RGB en
std_msgs/ColorRGBA. Toute nouvelle interface doit d’abord chercher son type
standard.
Hors contrat : la sécurité interne
Le buzzer actif (alertes température, batterie, danger) n’apparaît pas
dans le contrat : c’est une décision autonome du firmware, qui doit
fonctionner même si la Raspberry Pi ou ROS est hors service. ROS n’en voit
que le reflet dans /diagnostics, jamais la commande. Même
philosophie pour le watchdog de cmd_vel : la mise en sécurité des
moteurs ne dépend d’aucun logiciel extérieur à la carte.
Les 6 boutons de navigation de l’écran (haut, bas, gauche, droite, OK, ESC)
sont en revanche publiés vers ROS sur screen/button : le menu de
l’écran est piloté côté Raspberry (voir Écran OLED et interface utilisateur (HMI)). Les 4 boutons
utilisateur (sw1-sw4) et 4 LEDs (led1-led4) passent, eux, par
ui/buttons / ui/leds.
Arbre TF
/tf est un topic global partagé : la séparation entre robots s’y fait
par les noms de frames, préfixés par l’identité du robot
(paramètre frame_prefix de robot_state_publisher) :
ratbot_1/odom ──► ratbot_1/base_footprint ──► ratbot_1/base_link ──► …
ratbot_N/odom → ratbot_N/base_footprint: publié par la Pi, pas par le ratbot_controller.Trois raisons. Un message TF est de taille variable, donc coûteux sur la liaison série déjà chargée par
base/odom— 750 octets à 27 Hz mesurés. La Pi fait déjà tournerrobot_state_publisherpour les transformations internes. Et surtout, dès qu’un EKF (robot_localization) fusionnera l’odométrie et l’IMU, c’est lui qui publiera cette transformation : deux émetteurs pour la même transformation est un conflit classique, silencieux et pénible à diagnostiquer.La carte publie donc l’odométrie, et rien d’autre.
Les transformations internes au robot sont publiées par
robot_state_publisherà partir de l’URDF.
Multi-robot : les namespaces
ROS 2 est conçu pour faire cohabiter plusieurs robots identiques : le code
utilise des noms de topics relatifs (base/odom, jamais
/base/odom), et c’est au lancement qu’on pousse les nœuds dans le
namespace du robot :
ros2 launch ratbot_bringup robot.launch.py robot_id:=1 # → /ratbot_1/…
ros2 launch ratbot_bringup robot.launch.py robot_id:=2 # → /ratbot_2/…
Le même code, non modifié, produit des graphes disjoints. Points d’attention :
Firmware : le namespace d’un nœud micro-ROS se fixe à sa création ; la carte doit donc connaître son numéro
N(TODO : DIP switch, option bytes ou configuration au boot). Côté Pi, le plus simple est de nommer les machinesratbot-1,ratbot-2… et de dériverrobot_iddu hostname.ROS_DOMAIN_ID : il ne sépare pas les robots — au contraire, toutes les machines qui doivent se voir partagent la même valeur. C’est le namespace qui sépare, le domaine qui réunit.
Projets hôtes : si un hôte attend un autre préfixe (par exemple
/hote/agents/ratbot_1/cmd_vel), le remapping au lancement fait la traduction sans modifier le code du robot.
Note
Certains outils de l’écosystème (nav2, teleop_twist_keyboard…)
s’attendent par défaut à trouver cmd_vel et odom directement à
la racine du namespace du robot. Le choix du sous-système base/ est
assumé pour la lisibilité : nos launch files portent les quelques
remappings nécessaires.