Skip to content

Feat/link layer and imu bench - #2

Merged
WilliamH07 merged 33 commits into
mainfrom
feat/link-layer-and-imu-bench
Oct 7, 2026
Merged

WilliamH07 merged 33 commits into
mainfrom
feat/link-layer-and-imu-bench

Conversation

@WilliamH07

Copy link
Copy Markdown
Owner

No description provided.

WilliamH07 and others added 30 commits September 11, 2026 12:31
… ROS 2

Le dossier tranche que le bus temps réel est un bus CAN. La carte de
distribution n'existe pas, et le logiciel n'a pas à l'attendre : on sépare
le protocole de son transport et on développe en série en écrivant le code
comme si c'était déjà du CAN.

Protocole
  protocol.yaml devient la source unique. Le générateur produit l'en-tête C
  du firmware, l'en-tête C++ de ROS 2 et la table de documentation. Un hash
  calculé sur le SENS du protocole — pas sur le texte du fichier — voyage
  dans chaque heartbeat : un nœud et un calculateur qui ne s'accordent pas
  se le disent, au lieu d'échanger des valeurs plausibles et fausses.

Liaison
  Trame canonique de 8 octets et identifiant 11 bits, en série comme en CAN.
  Cadrage COBS avec CRC-16/CCITT-FALSE, 6 octets de surcoût, resynchronisation
  au délimiteur. Les deux transports CAN sont déjà écrits — un fichier de
  chaque côté, ce qui est la mesure de ce que l'abstraction vaut.

  La série ne fournit ni arbitrage, ni acquittement, ni retransmission, ni
  confinement de défaut. Aucune fonction de sécurité n'est validée dessus ;
  N4 et N5 restent matériels.

Capteur
  BNO085 en SPI, pile SH-2 de CEVA en sous-module (Apache 2.0, non modifiée) ;
  le portage ESP-IDF est à nous. Le firmware publie ce que le capteur mesure,
  dans les axes du capteur : la rotation de montage est décrite dans l'URDF,
  pas dans le firmware.

ROS 2
  /imu/data en sensor_msgs/Imu conforme REP-103 et REP-145 — quaternion
  normalisé, accélération gravité comprise, magnétomètre en teslas,
  covariances renseignées à partir de la qualité rapportée par le capteur.
  Une covariance nulle ne signifie pas « inconnue » mais « confiance
  infinie », et c'est la manière la plus efficace de faire diverger un EKF.

  Journaux du firmware tunnellisés jusqu'à /rosout, diagnostics sur la
  liaison et sur le capteur, aides de banc pour Foxglove.

Vérification
  30 579 vérifications de cadrage et de protocole sur hôte, sans matériel.
  Les trois implémentations — C généré, C++ généré, Python lisant le YAML —
  comparées octet par octet sur les 28 trames. Vecteurs d'or pour figer le
  format du fil.

Document : docs/architecture/13-architecture-logicielle-liaison.md, §AA à §AH.
Recette du banc B1 en §AG.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Deux bloquants de compilation, un défaut SPI qui aurait bloqué la chaîne IMU
en silence, et une série de points où le code et le commentaire disaient
deux choses différentes.

Bloquants
  board_config.h : SPI3_HOST n'était déclaré nulle part.
  main.c : CONFIG_RETRIEVER_IMU_ENABLE_MAG utilisé dans une expression C —
  Kconfig ne définit un booléen que lorsqu'il vaut y, donc l'option à n
  cassait la compilation.
  retriever_protocol : rt_framing.h inclut "retriever_protocol.h" sans
  préfixe, ce que le chemin d'inclusion du paquet ROS ne fournissait pas.
  retriever_link : ament_target_dependencies sans mot-clé mélangeait les
  deux signatures de target_link_libraries — erreur fatale à la
  configuration CMake.

SPI
  spi_device_release_bus() ne désasserte pas CS, et une transaction de
  longueur nulle n'en produit aucune. Tout en-tête SHTP sans corps — le cas
  normal quand le BNO085 asserte H_INTN sans charge utile — laissait donc CS
  bas indéfiniment, et la liaison se désynchronisait sans qu'aucune fonction
  ne retourne d'erreur. Libération explicite ajoutée sur tous les chemins.
  Tampon de transit statique aligné : les tampons de la pile SH-2 ne le sont
  pas, et le pilote allouait un rebond au tas à chaque lecture, à 100 Hz.

CAN
  twai_initiate_recovery() ne redémarre pas le contrôleur : il le laisse
  arrêté. Sans twai_start(), un nœud parti en bus-off se taisait pour de
  bon, en annonçant que tout allait bien. La reprise passe dans la boucle de
  réception — une fonction de statistiques ne doit pas avoir d'effet de
  bord. Le débit demandé est désormais réellement appliqué.

Données
  Le game rotation vector publiait une précision d'orientation nulle là où
  le commentaire annonçait une valeur pessimiste. Zéro, pour un EKF, ne veut
  pas dire « inconnu » mais « parfait ». Sentinelle explicite, reconnue des
  deux côtés, avec son test.
  Le triplet IMU pouvait publier un quaternion périmé avec un gyromètre et
  une accélération plus récents : IMU_QUAT n'a pas de compteur, ses huit
  octets sont pleins. Son âge est maintenant borné, et les drapeaux de
  validité du firmware sont enfin lus.
  NodeStatus annonçait toujours un âge de battement nul, sur un topic
  latché : un panneau ouvert après une panne voyait un nœud en pleine santé.

Concurrence
  Horodatages et compteurs partagés entre le fil de lecture et les
  minuteries : atomiques côté ROS, section critique côté firmware. Volatile
  ne donne pas l'atomicité d'un 64 bits sur un cœur 32 bits.
  SerialTransport::send tournait en attente active sans plafond sur un
  descripteur non bloquant, depuis le fil de l'exécuteur.

Honnêteté
  Le tunnel de journalisation ne désactive pas la console : il détourne les
  ESP_LOGx et rien d'autre. Le bootloader, le gestionnaire de panique et
  tout printf continuent d'écrire en clair. La documentation le disait de
  travers ; elle le dit maintenant, avec ce que ça coûte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
git submodule update --init ne fait rien, et ne dit rien, quand le lien vers
le sous-module a disparu de l'index. L'erreur ne se manifeste qu'au premier
idf.py build, plusieurs minutes plus tard et plusieurs couches plus bas.

Le script tente le sous-module, vérifie que sh2.c est bien là, se replie sur
un clonage direct au tag épinglé sinon, et refuse de rendre la main sans
avoir vérifié. Il signale aussi le cas où la compilation marchera localement
alors qu'un clone neuf repartirait sans la bibliothèque.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Les deux arrivaient par transitivité depuis driver/gpio.h. Une dépendance
d'inclusion qu'on n'a pas écrite est une dépendance qu'une mise à jour d'IDF
peut retirer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Le commentaire de board_config.h associait SDA à MOSI. C'est l'inverse : la
datasheet BNO08x donne la broche 20 (H_SDA en I2C) comme H_MISO, et la broche
17 (SA0, la sélection d'adresse I2C) comme H_MOSI. Selon les cartes, cette
dernière est sérigraphiée ADDR, ADR, SA0, DI ou SI.

L'erreur ne produit rien d'observable : le capteur ne répond simplement
jamais, et on cherche du côté du mode SPI ou de la séquence PS0/PS1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Deux défauts révélés par le premier banc réel.

La file urgente ne servait à rien. La tâche d'émission bloquait 20 ms sur la
file normale, donc une trame urgente déposée juste après le début de cette
attente y restait jusqu'à son expiration. Mesuré sur le banc : 52 ms
d'aller-retour au lieu de quelques millisecondes — 20 ms d'émission, 20 ms de
réception, le reste côté hôte. Ces 26 ms se seraient retrouvés, par
latency_offset_ms, dans l'horodatage de chaque échantillon IMU.

L'émetteur réveille maintenant la tâche par notification, et l'urgente
repasse devant entre chaque trame normale. Le délai de scrutation de la
réception passe de 20 ms à 2 ms : uart_read_bytes rend la main à expiration,
donc ce délai EST la latence.

Et le moniteur effaçait les journaux du firmware. Ils étaient émis, reçus,
décodés, imprimés — puis le tableau repeignait l'écran par-dessus toutes les
500 ms. L'information la plus utile du banc était invisible. Elle s'affiche
désormais sous le tableau, avec l'état de santé de l'IMU décodé et le nombre
d'erreurs des dix dernières secondes, pour que le bruit du journal de
démarrage à 115200 ne masque plus une ligne qui va vraiment mal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Le capteur répond à son identification mais n'émet aucun rapport. Le journal
dit « ecriture refusee : pas de reveil » : le protocole veut que l'hôte tire
PS0/WAKE à la masse et attende que le capteur réponde en abaissant INT, et
cette réponse n'arrive pas. Les lectures, elles, fonctionnent.

Plutôt que de conclure par raisonnement, le portage compte désormais ses
lectures, ses paquets, ses en-têtes vides, ses écritures et ses réveils sans
réponse ; le pilote compte les événements décodés par grandeur. Un relevé
toutes les cinq secondes dit où la chaîne s'arrête au lieu de le laisser
déduire. L'avertissement de réveil est limité à une occurrence : sur une
ligne PS0 non câblée, il noyait tout le reste.

Et les 52 ms d'aller-retour venaient du moniteur, pas de la carte. read(4096)
avec un délai de 50 ms attend systématiquement les 50 ms sur une liaison peu
chargée. Une médiane à 52,09 avec 3 ms de dispersion était la signature d'une
constante, pas d'un transport. Délai ramené à 5 ms, lecture de ce qui est
déjà arrivé.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Le banc rend 23287 paquets à la pile SHTP en cinq secondes et zéro événement
capteur, INT restant bas en permanence. C'est la signature d'un paquet que le
capteur repropose parce qu'on ne le consomme jamais vraiment.

Deux causes possibles, indiscernables par le raisonnement : on lit du vrai
SHTP avec une mauvaise longueur, ou on lit de la bouillie qui parse comme une
longueur plausible. Un en-tête SHTP porte son canal — 0 à 5 — et un numéro de
séquence : les afficher tranche en un flash.

Les six premiers paquets, puis un sur dix mille.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Le banc rendait 23000 paquets par seconde à la pile et zéro événement. Le
vidage des en-têtes a tranché : du SHTP parfaitement valide, canal 0, charge
utile 01 03 03 03 — le rapport de liste d'erreurs du composant, qui
s'allongeait. Et la séquence ne passait que de 5 à 13 entre le paquet 6 et le
paquet 10000 : le même paquet relu un millier de fois, jamais consommé.

La cause est le motif de transfert. Le portage lisait l'en-tête puis le corps
en gardant CS bas entre les deux. Le composant n'y voit pas une lecture en
deux temps : il y voit une lecture suivie d'une ÉCRITURE, refuse le paquet
malformé, empile une erreur, et ne consomme jamais ce qui attend.

Le protocole réel, tel que l'implémente le pilote Adafruit qui fonctionne :
une transaction complète pour l'en-tête, CS remonte, on attend que H_INTN se
réabaisse, puis une seconde transaction complète qui relit le paquet ENTIER,
en-tête compris. Quatre octets de plus par paquet, et un composant qui
répond.

Le bandeau du fichier décrit désormais ce motif en toutes lettres, avec le
symptôme, pour que personne ne refasse l'optimisation qui paraît évidente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Le SPI est full-duplex : il n'existe pas de transaction de lecture pour
le BNO08x, seulement des echanges. Ce que le maitre sort sur MOSI pendant
qu'il rentre un paquet, le composant le lit comme un en-tete SHTP venant
de l'hote.

ESP-IDF, quand tx_buffer vaut NULL, ne garnit pas la FIFO d'emission :
MOSI rejoue le contenu precedent. Le composant voyait donc un paquet
malforme a chaque lecture, en empilait une erreur sur le canal 0, ce qui
maintenait H_INTN bas, ce qui relancait une lecture, donc une erreur de
plus. Cela explique l'asymetrie observee au banc — l'annonce SHTP de 276
octets lue sans une faute, et pas un seul rapport capteur — que ni le
cablage ni la sequence de reset n'expliquaient.

Le pilote de reference Adafruit passe explicitement sendvalue = 0x00 a
chaque lecture. Le tampon de zeros ajoute ici est le meme geste.

Trois autres corrections dans le meme televersement :

- l'en-tete est lu dans s_staging, aligne 32 bits, et non dans un tableau
  local : le DMA d'ESP-IDF exige une destination alignee et arrondit la
  longueur au multiple de 4 superieur ;
- hal_write ne touche plus a PS0/WAKE, comme le pilote Adafruit : le
  composant n'est jamais mis en veille par ce firmware, et PS0 est aussi
  la broche de selection de protocole ;
- l'horloge SPI passe de 3 MHz a 1 MHz, la valeur du pilote Adafruit. A
  100 Hz la marge ne coute rien.

Le portage decode desormais les listes d'erreurs du canal 0 et les
journalise : la pile SH-2 vendue ne s'abonne pas a ce canal et les
jetait en silence. Le compteur err_shtp apparait dans le diagnostic.

Corrige aussi deux champs de compteur absents de l'en-tete
(repeat_timeouts, oversize) qui empechaient la compilation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Apres le reset, le concentrateur dort (datasheet section 5.2.2) : il
n'assertera H_INTN que si l'hote le lui demande, en tirant H_WAKEN — la
broche PS0, repurposee apres le reset — a la masse. Attendre H_INTN sans
reveiller, c'est attendre une fenetre qui ne s'ouvre que par hasard,
quand le composant avait de lui-meme quelque chose a dire.

Ce reveil avait ete retire au commit precedent, a tort. L'explication
n'est venue qu'apres : tant que le defaut MOSI etait present, le
composant avait toujours une erreur a signaler, donc H_INTN restait bas
en permanence, donc le reveil ne servait a rien — et son absence ne se
voyait pas. Les deux defauts se masquaient l'un l'autre. Le message
« ecriture : H_INTN jamais bas » n'est apparu qu'une fois MOSI corrige.

Le semaphore d'interruption est vide AVANT de tirer PS0 bas : une
assertion anterieure, deja consommee par une lecture, ferait croire a un
reveil instantane et l'ecriture partirait dans le vide. H_WAKEN est
relache APRES le transfert, comme le demande la datasheet.

Le moniteur, lui, mesurait toujours 53 ms d'aller-retour parce que le
correctif de son delai de lecture n'avait jamais ete televerse : c'est
le delai de pyserial qui etait mesure, pas la liaison. 5 ms, et lecture
de in_waiting octets plutot que d'un bloc de 4096.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Aller-retour median 3,14 ms, max 3,88 ms, sur 100 echantillons a 921600
bauds, mesure au banc avec tools/link_monitor.py une fois le moniteur
corrige. La valeur de 1,5 ms qui figurait la etait un ordre de grandeur
pose faute de mesure ; elle se trouvait juste, ce qui ne la rendait pas
moins inventee.

Point 5 de la recette du banc B1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Deux documents produits hors de ce depot et restes non suivis : le
handoff de saisie du schema safety_power et le plan de conception de la
feuille P2 « rails d'alimentation ». Ils decrivent l'etat fige au 16
septembre 2026 et les points GATE qui bloquent la saisie EasyEDA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
docs/DEMARRAGE.md part d'une machine vierge et s'arrete quand /imu/data
sort a 100 Hz dans Foxglove. Il manquait : tout ce qui a ete appris au
banc vivait dans des en-tetes de fichiers source ou dans un historique
de conversation, ce qui ne se retrouve pas quand on reprend le projet
trois semaines plus tard, et pas du tout depuis une autre machine.

Contenu : les trois roles d'outillage et pourquoi on ne les melange pas,
le cablage du BNO085 avec le piege de la broche ADDR qui devient MOSI,
l'installation d'ESP-IDF v5.5 sur macOS et Linux, la construction et le
flash, la verification sans ROS avec link_monitor.py, l'installation de
ROS 2 Jazzy et la construction de l'espace de travail, le lancement du
banc et sa verification, les tests hote, et un tableau de depannage des
pannes reellement rencontrees.

Les deux README pointent dessus.

Trois points qui ont chacun coute du temps y figurent explicitement :
PS0 et PS1 hauts avant le reset sinon le composant demarre en I2C sans
le dire ; les erreurs de format des premieres secondes qui ne sont que
le journal du bootloader en clair a 115200 ; et l'orientation annoncee
« nulle » qui est un indice de confiance, pas une panne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Le firmware n'appelait jamais sh2_setCalConfig. Le BNO085 sait corriger
ses biais en fonctionnement — c'est son interet principal face a une
centrale brute — mais il ne le fait que si on le lui demande, et il ne
conserve le resultat d'une mise sous tension a l'autre que si c'est ecrit
en flash. Sans les deux, il sort ses valeurs d'usine.

Mesure au banc du 20 septembre 2026, trois orientations franches :

    a plat        (0,270 ; -0,230 ; -10,152)  norme 10,158
    sur un chant  (0,382 ; -10,188 ; -0,304)  norme 10,200
    sur l'autre   (-0,270 ; 9,460 ; -1,266)   norme  9,548

Les normes ne sont pas egales, donc ce n'est pas un facteur d'echelle.
Un seul vecteur de biais b = (+0,27 ; -0,378 ; -0,34) m/s^2 recale les
deux premieres a 3 millemes de 9,81 et la troisieme — la position
penchee, la moins stable — a 0,9 %. Norme du biais : 0,58 m/s^2. La meme
omission explique le cap : le capteur annoncait lui-meme pi radians
d'incertitude, soit 180 degres, faute de magnetometre jamais etalonne.

Ce que ce commit ajoute :

- sh2_setCalConfig sur accel, gyro et mag au demarrage, plus
  sh2_setDcdAutoSave(true). Reinstalle apres chaque reset du capteur :
  un reset efface cette configuration comme il efface les rapports, et
  l'oublier donnerait un capteur qui s'etalonne jusqu'au premier reset
  puis plus jamais, sans que rien ne le signale ;
- deux trames, IMU_CAL (0x215, 10 Hz) qui dit ce qui est actif et
  combien de fois le DCD a ete ecrit, et IMU_CAL_CMD (0x321) qui permet
  d'activer, figer, sauvegarder ou effacer depuis le banc ;
- tools/imu_cal.py, qui envoie ces commandes et affiche l'etat en
  continu pendant les manipulations.

Deux points de conception qui ne sautent pas aux yeux :

- la commande arrive dans la tache de reception de la liaison mais
  n'est qu'y deposee ; elle s'execute dans la tache de l'IMU. La pile
  SH-2 n'est pas reentrante, et l'appeler depuis deux taches corromprait
  l'etat SHTP au milieu d'un transfert — symptome : un capteur muet, sans
  erreur ;
- SAVE utilise sh2_saveDcdNow et non sh2_saveDcdAndReset, qui
  redemarre le capteur et coupe le flux une demi-seconde.

IMU_CAL_CMD porte un garde 0xCA, sur le modele de CONFIG. Effacer un DCD
coute dix minutes de manipulations a refaire ; ce n'est pas une chose
qu'une trame corrompue doit pouvoir declencher.

Le hash du protocole passe de 0xC1214F10 a 0xE8391C47 : firmware ET
espace de travail ROS sont a reconstruire tous les deux.

30 610 verifications de cadrage, 30 trames coherentes entre C, C++ et
Python, generation idempotente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Le dossier installe s'appelle esp-idf-v5.5, pas esp-idf : la version est
dans le nom pour que deux versions puissent coexister, ce qui arrive des
qu'un projet tiers en exige une autre. Le guide donnait
~/esp/esp-idf/export.sh, qui echoue silencieusement — le shell ne trouve
pas le fichier, et l'erreur suivante est un idf.py introuvable, plusieurs
commandes plus loin.

Ajoute aussi l'alias recommande par Espressif plutot qu'une ligne dans le
.zshrc, qui ralentit l'ouverture de chaque terminal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Le composant retriever_imu ne dependait que de driver et esp_timer, et
c'est voulu : il parle rt_imu_sample_t, et c'est main.c qui traduit vers
le bus. Le commit precedent utilisait pourtant l'enumeration generee
rt_imu_cal_action_e directement dans imu_bno085.c — la compilation le
refuse, et elle a raison : lui faire inclure l'en-tete du protocole
ferait du pilote de capteur un client du bus.

Le composant a donc maintenant sa propre enumeration, aux memes valeurs.
Une duplication de ce genre derive tot ou tard, alors elle n'est pas
laissee a la vigilance : main.c, seul endroit qui voit les deux
en-tetes, verifie leur accord par _Static_assert. Une divergence casse
la compilation au lieu de passer inapercue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
…auve

Le commit qui a ajoute IMU_CAL au firmware n'a rien donne au noeud ROS
pour la lire : la trame arrivait toutes les 100 ms et tombait dans
unknown_frames. Le hash du protocole, lui, concordait — les deux cotes
ont bien la meme definition — ce qui montre exactement ce que ce compteur
attrape et que le hash ne peut pas voir : une trame connue des deux
cotes mais traitee d'aucun.

ImuStatus gagne cal_enabled, cal_saves, cal_autosave et cal_last_result.
Les deux premiers disent des choses differentes et les confondre coute
une seance de banc : l'un dit que le capteur corrige ses biais EN CE
MOMENT, l'autre que la correction a ete ecrite en flash et survivra a la
mise hors tension.

C'est aussi IMU_CAL, et non IMU_MAG, qui fait desormais autorite sur la
qualite du magnetometre cote ROS : elle arrive meme quand le
magnetometre est desactive, et c'est precisement dans ce cas qu'on veut
savoir pourquoi le cap est mauvais.

Deux avertissements de diagnostic, parce qu'aucun des deux etats ne
ressemble a une panne dans les donnees :

  - etalonnage dynamique inactif — les biais ne sont pas corriges
  - etalonnage non sauvegarde — il sera perdu a l'extinction

Le hash du protocole ne change pas : 0xE8391C47, deja publie par le
commit precedent. Seul l'espace de travail ROS est a reconstruire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Le capteur annonce la fin de son initialisation par un evenement
SH2_RESET, a chaque demarrage, sans exception. Le firmware le comptait
comme les autres, donc sensor_resets valait 1 en permanence — et le
diagnostic ROS passe en ERREUR des qu'un reset est signale. L'IMU
apparaissait donc au rouge des le boot, sur une machine parfaitement
saine.

Un voyant toujours rouge ne dit plus rien. C'est pire que pas de voyant :
le jour ou le capteur se reinitialise vraiment — alimentation instable,
cablage SPI marginal —, personne ne le remarquera.

Le drapeau s_running separe la sequence de demarrage du fonctionnement
normal. Pendant rt_imu_init(), un reset est attendu et seulement
journalise ; apres, c'est un incident, compte et publie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
La premiere version affichait « en attente d'une trame IMU_CAL » aussi
bien devant un port muet que devant un flux valide dont elle ratait la
trame. Les deux cas demandent des actions opposees et l'outil ne les
distinguait pas — il a fallu lancer link_monitor a cote pour savoir
lequel des deux on avait.

Il compte desormais les octets recus, les trames valides, les erreurs de
CRC et de format, et liste les identifiants effectivement vus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Sur une DevKitC, DTR et RTS ne sont pas decoratifs : ils pilotent EN
(reset) et IO0 (mode de demarrage) a travers le circuit d'auto-reset du
montage. Le noyau les assertit a l'ouverture du port et les abaisse a la
fermeture (HUPCL), et pyserial fait pareil. La carte redemarre donc a
chaque ouverture — et selon l'ordre des transitions elle peut rester dans
le bootloader ROM, muette, sans la moindre erreur cote hote.

C'est ce qu'on observait depuis ce matin, sur trois programmes differents
et sans jamais le nommer : la liaison marchait apres un rebranchement
physique, puis plus rien des qu'un second programme rouvrait le port. On
a mis ca sur le compte du magnetometre, puis d'un bug de decodage, puis
de l'alimentation USB. C'etait le port lui-meme.

Le transport C++ enleve HUPCL et desassertit DTR et RTS par TIOCMBIC
apres configuration ; si le pilote ne l'implemente pas, il le dit et
continue plutot que de refuser un port qui marche peut-etre. Les outils
Python passent par retriever_wire.open_port(), qui fait la meme chose et
demande en plus un verrou exclusif : deux programmes lisant le meme port
se partagent les octets et chacun signale des erreurs de format sur un
flux tronque. Mieux vaut un refus net.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Une mise en page Foxglove vit dans le stockage local de l'application :
elle n'est ni partagee entre machines, ni sauvegardee, et sa suppression
est definitive — il n'y a pas d'annulation. Six panneaux reconstruits a
la main apres chaque mise a jour, changement de poste ou fausse manoeuvre,
c'est exactement le travail qu'un fichier versionne supprime.

docs/foxglove/bench_imu.json contient les six panneaux de la recette B1 :
vue 3D sur imu_world, les deux traces de /imu/data, les diagnostics, et
les deux etats bruts imu_status et link_status. Il s'importe par le menu
des mises en page, « Import from file… ».

Le README qui l'accompagne dit dans quel ordre les lire, et rappelle que
le panneau 3D ne montre rien sans bench.publish_tf — une aide de banc, a
desactiver le jour ou l'EKF publiera cette meme arete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Relacher DTR et RTS ne suffisait pas. Une carte deja partie dans le
bootloader ROM y reste jusqu'a une coupure d'alimentation : elle y
attend un televersement, a 115200, et reste donc muette pour un hote qui
lit a 921600 — sans qu'aucune erreur ne soit levee nulle part.

Le banc l'a montre : apres une impulsion de reset explicite lancee a la
main, la donnee revenait ; en relancant le noeud sans toucher au cable,
le diagnostic repassait de « aucune trame recue » a « taux d'erreur
superieur a 1 % », signature d'octets recus a un debit qui n'est pas le
bon.

Le transport execute donc la sequence d'esptool, a l'envers du mode
telechargement, et l'ordre est impose par le materiel :

    IO0 haut (DTR relache)  →  « demarre l'application »
    EN  bas  (RTS asserte)  →  reset
    EN  haut (RTS relache)  →  demarrage

Suivie d'une attente de 400 ms et d'une purge : ce que les deux
chargeurs emettent sort en clair a 115200 et ressemble a du bruit a
921600. Ca supprime au passage la bordee d'erreurs de cadrage qu'on
voyait a chaque demarrage du noeud.

Le parametre serial.reset_on_open permet de desactiver la sequence.
Redemarrer un noeud de securite parce que le calculateur ouvre un port
est une mauvaise idee sur un robot ; sur le banc c'est au contraire ce
qui garantit un etat connu. Sur CAN, la question disparait.

retriever_wire.open_port() fait la meme chose pour link_monitor.py et
imu_cal.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Deux lignes de depannage, parce que ce defaut a coute une demi-journee
et qu'il ne ressemble a rien de ce qu'on cherche : la carte demarre en
mode telechargement, attend un televersement a 115200, et reste muette
pour un hote qui lit a 921600 — sans qu'aucune erreur ne soit levee
nulle part.

La deuxieme ligne note que le silence complet et le bruit illisible sont
deux etats du meme probleme, pas deux pannes differentes. On a perdu du
temps a les traiter separement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
docs/HANDOFF-banc-imu.md existe pour qu'on puisse reprendre ce travail
dans trois semaines sans relire vingt-cinq commits : ce qui marche et
avec quels chiffres, ce qui a ete casse et par quel mecanisme, ce qui
reste et comment le faire.

La partie qui a de la valeur est la deuxieme. Chacun des defauts trouves
etait silencieux et aucun ne ressemblait a sa cause : le SPI full-duplex
ou MOSI rejoue la transaction precedente, le reveil PS0 dont l'absence
etait masquee par ce premier defaut, l'etalonnage jamais active qui
donnait un biais de 0,58 m/s2 sans rien signaler, et le bootloader ROM
ou la carte attend un televersement a 115200 en restant muette pour un
hote qui lit a 921600.

La troisieme partie note que tous les points restants — etalonnage,
covariances mesurees, endurance, caracterisation magnetique — demandent
la carte montee sur le robot. C'est une decision, pas un oubli : un
etalonnage magnetique fait sur un banc serait a refaire une fois le
capteur pres des moteurs.

Les deux README y renvoient.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Rien n'est branche encore — tout ce qui peut etre ecrit avant de
brancher l'est, pour que la seance de banc porte sur le capteur et non
sur la configuration.

retriever.repos epingle le pilote tiers sur la branche humble du depot
officiel : master y est marquee « old version », et il n'existe pas de
release binaire Jazzy. Meme discipline que le sous-module sh2 du
firmware. YDLidar-SDK n'y figure pas : c'est une bibliotheque CMake, pas
un paquet ROS, et le pilote la cherche par find_package.

Les parametres sont ceux du X4 CLASSIQUE, releves dans la table par
modele du pilote officiel : 128000 bauds, echantillonnage 5 kHz,
0,12-10 m, isSingleChannel false, moteur pilote par DTR. Le X4 Pro est
mono-canal, ne pilote pas le moteur par DTR et porte a 12 m ; confondre
les deux donne un lidar qui tourne sans jamais publier.

Trois choix qui meritent d'etre dits :

- frequence a 7 Hz et non 10. C'est la valeur du paragraphe 03, et la
  frequence s'obtient en pilotant le moteur, donc en consommant. Tant
  que USB_PWR n'a pas son alimentation 5 V dediee, inutile de demander
  au port USB du PC plus que le necessaire ;
- invalid_range_is_inf a true. REP-117 : une mesure hors portee se dit
  +inf, pas 0.0. A false le pilote sort des zeros, sous range_min, que
  chaque consommateur doit filtrer — et celui qui oublie voit un
  obstacle colle au capteur ;
- le nom du noeud est force dans le fichier de lancement, parce qu'il
  doit rester identique a la cle de premier niveau du YAML.

La verification d'orientation est une mesure physique, pas une lecture :
reversion et inverted sont exactement ce qui retourne ou miroite le
scan. La procedure est dans le guide.

La transformation base_link vers laser est une aide de banc, signalee
comme telle : sur le robot c'est l'URDF qui decrit le montage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Une apres-midi d'hypotheses successives — alimentation, DTR, resolution
fixe, frequence, intensite — chacune plausible, chacune fausse ou
confondue avec une autre. Ce qui a fini par marcher : reproduire a
l'identique tri_test, le programme d'exemple du SDK qui recevait des
scans, puis s'en ecarter un parametre a la fois.

Trois ecarts avec la documentation, tous mesures sur l'exemplaire du
banc (serie 2021090300040777, firmware 1.10) :

- isSingleChannel true, alors que la table du constructeur dit false
  pour le X4. Cet exemplaire livre ses informations produit comme un
  double canal mais ne delivre ses scans qu'en mono-canal ;
- intensity_bit 10, qui n'est pas une largeur mais « cherche ». C'est ce
  qui declenche le sondage 16 → 8 → 0 et la resynchronisation du flux.
  A 0, le SDK attend des trames qui n'arrivent jamais ;
- sample_rate 5, la ou le pilote ROS met 9 par defaut.

Et une retractation. Le commit precedent affirmait support_motor_dtr
false comme un fait etabli, parce que le moteur ne demarrait pas avec
true. Mais isSingleChannel etait encore a false a ce moment-la : les
deux avaient change ensemble et le resultat a ete attribue au mauvais.
tri_test tourne avec true, le moteur va bien. C'est precisement l'erreur
que la regle « un parametre a la fois » existe pour eviter, et elle a
ete enfreinte deux fois dans la meme journee.

Chaque valeur du fichier porte desormais sa provenance : prouvee dans la
configuration qui marche, ecart volontaire inoffensif, ou a trancher par
une mesure physique. reversion et inverted sont dans la troisieme
categorie : ils ne changent rien a l'acquisition, ils retournent ou
miroitent le scan, et seul un objet a un metre droit devant peut dire
lesquels sont justes.

Le tableau de depannage du guide est refait dans le meme esprit, avec la
regle qui a debloque la journee en derniere ligne : quand on ne sait
plus quel parametre a change, revenir a un temoin qui marche.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
Mesure au banc : avec invalid_range_is_inf a true, les mesures sans echo
sortent quand meme a 0.0 — 221 zeros sur 667 points, aucun +inf. Le
reglage promet REP-117 et ne le tient pas.

Deux consequences ecrites la ou on les cherchera :

- tout consommateur de /scan doit filtrer sous range_min lui-meme.
  slam_toolbox et Nav2 le font ; un noeud maison qui l'oublierait verrait
  221 obstacles colles au capteur ;
- le taux de couverture se compte sous range_min, pas par isfinite(),
  qui donne 100 % et ne mesure rien. Le guide avait le mauvais critere.

Releve du banc : 67 % de couverture, 0,31 a 5,19 m. Au-dessus du seuil
de 60 % du paragraphe 03. Le tiers manquant est l'environnement, pas le
capteur.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
…ncer

Trois documents pour trois usages.

docs/HANDOFF-banc-lidar.md, pendant du handoff IMU : les chiffres du
banc (667 points par tour, ~7,5 Hz libre, 67 % de couverture, 0,31 a
5,19 m, orientation validee), les six defauts avec leur mecanisme, et
ce qui reste — alimentation USB_PWR, diagnostics sur /scan, URDF,
ignore_array, remplacement en phase 4. La section 2.1 consigne la
methode qui a debloque la journee, parce qu'elle vaut plus que chaque
correction prise separement : quand plus rien ne marche, revenir a un
temoin qui marche et s'en ecarter un parametre a la fois.

docs/DEMARRAGE.md gagne une fiche de lancement en tete — les deux
commandes, IMU et lidar, la regle du seul pont Foxglove, et le piege du
setup.bash regenere a chaque build qui fait croire a un paquet absent.
C'etait la question posee : « la commande pour lancer », et pour l'IMU
aussi. Elle etait dans le guide, mais en section 4, pas la ou on la
cherche.

Le dossier d'architecture recoit au paragraphe AH les corrections que
le banc impose au texte — sept lignes, chacune une mesure qui contredit
ce qui etait ecrit, pas une preference — et l'etat des mesures L1 a L6.
La feuille de route AH.4 marque B1 et B1' faits.

L'orientation du lidar, validee physiquement, passe de « a trancher »
a « validee » dans la configuration, avec la consigne de refaire le
test si la carte adaptatrice change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
WilliamH07 and others added 3 commits September 21, 2026 06:44
…Foxglove

Protocole : MOTOR_CMD 0x110 (quatre consignes i16 ×0,001, 50 Hz), MOTOR_ENABLE
0x111 (magic 0xEB), MOTOR_STATE 0x1A0 (appliqué, masque, drapeaux, âge), enum
motor_flag ; HEARTBEAT_MOTION_FRONT passe en bench. Hash 0x670192A7.

Firmware esp32_motion : table de quatre moteurs dans board_config.h (PWM 20 kHz
+ RC vers VR, DIR/STOP open-drain), CONFIG_RETRIEVER_MOTOR_COUNT=3 aujourd'hui,
4 quand m3 est câblé. Tâche 200 Hz, chien de garde 500 ms, pente à
l'accélération, rien ne tourne sans MOTOR_ENABLE.

ROS : messages MotorCommand/MotorEnable/MotorState ; le pont prend link.peer,
imu.enabled et motors.* — un seul exécutable pour les deux bancs. Republication
à 50 Hz avec délai côté PC, ESTOP sur /retriever/estop, diagnostics Moteurs.
bench_motors.launch.py + motors_bench.yaml, mise en page Foxglove,
tools/motor_bench.py sans ROS, DEMARRAGE §4 quater, HANDOFF-banc-moteurs.

firmware/test/Makefile : dépendance explicite sur l'en-tête généré (un binaire
périmé donnait 22 faux échecs croisés).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019iSUBG2bteBnbqvcotEM9D
@WilliamH07
WilliamH07 merged commit 0d87ef6 into main Oct 7, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant