L’heure : une chaîne à trois maillons
Le robot doit connaître l’heure, et surtout la même que les autres machines. Cette page décrit comment elle est établie au démarrage, qui fait autorité sur qui, et pourquoi l’ordre des maillons compte.
Pourquoi ça compte vraiment
Ce n’est pas une question de confort. Deux raisons, dans cet ordre.
Le parc doit être d’accord avec lui-même. Dans un usage multi-robots, ce qui compte n’est pas d’avoir l’heure juste dans l’absolu : c’est que toutes les machines aient la même. Un système qui compare des horodatages venus de plusieurs machines — pour décider de la fraîcheur d’une observation, par exemple — est faussé dès que leurs horloges divergent. Deux horloges qui divergent de quelques millisecondes faussent cette comparaison directement, et rien ne le signale.
C’est pour cela que le serveur du labo est déclaré prefer et que les pools
Internet sont désactivés :
server 192.168.1.190 iburst prefer minpoll 4 maxpoll 6
Un robot qui joindrait un pool Internet alors que le serveur du labo ne le peut pas serait plus juste dans l’absolu et en désaccord avec les caméras. Ce serait le pire des deux mondes.
Les mesures se datent. Chaque message publié par la carte est daté de la mesure, pas de la publication ; le journal de bord garde 256 événements datés. Une heure fausse rend tout cela illisible après coup — et c’est arrivé, voir plus bas.
Le problème : la Raspberry n’a pas d’horloge
Important
La Raspberry Pi n’a aucune horloge matérielle. Pas de pile, pas de quartz sauvegardé, rien. Hors tension, elle oublie l’heure.
Au démarrage, elle repart de la date de son dernier arrêt, restaurée par systemd. Elle est donc toujours en retard, jamais en avance — et ce retard peut être considérable.
La carte, elle, a une RTC sur pile. C’est le seul élément du robot qui garde l’heure hors tension. D’où l’inversion de rôle qui surprend : c’est la carte qui dépanne la Raspberry, et non l’inverse.
La chaîne, au démarrage
Les quatre filets successifs, du plus fiable au dernier recours.
Les quatre maillons sont des filets successifs, pas des alternatives. Le premier suffit presque toujours ; les suivants existent pour les cas où il manque.
La règle de la carte, en détail
rtc_on_agent_time() dans App/rtc_time.c.
Situation |
Décision |
|---|---|
heure de l’agent antérieure au firmware |
refusée, |
RTC en avance de plus de 60 s ( |
la RTC gagne, |
sinon, à l’ouverture |
la Pi devient la référence ( |
une Pi déjà fiable recule de plus de 60 s |
la confiance est retirée, |
écart RTC / agent supérieur à 1 s ( |
la RTC est réécrite, et l’événement va au journal de bord |
Une fois la confiance établie, la carte se recale sur l’agent à chaque
synchronisation. Tant qu’elle ne l’est pas, elle tient seule l’heure du
robot et la publie sur status/rtc pour que la Pi vienne s’y servir.
Quand les trois références tombent ensemble
Le 2026-09-26, le serveur de temps était éteint et la Pi repartait au 29 juillet 2026 à chaque démarrage. La carte a accepté cette date et écrasé sa propre RTC.
Pourquoi la Pi était bloquée sur juillet. Le paramètre noyau fixrtc, dans
cmdline.txt, règle l’horloge sur la date de dernier montage du système de
fichiers — laquelle est réécrite au démarrage suivant avec l’heure fausse. La
boucle s’auto-entretient, et seule une source extérieure peut la rompre.
Pourquoi la carte a suivi. Son plancher de vraisemblance valait alors
2025-01-01. Juillet 2026 étant postérieur, l’heure passait. Or les cartes
ont été produites en septembre : juillet leur est physiquement impossible.
Pourquoi rien ne l’a détecté. Une fois les deux horloges d’accord sur la même date fausse, il n’y a plus de désaccord à observer — deux horloges fausses qui concordent ont l’air justes. Le troisième témoin était absent.
Important
Le défaut n’était pas dans le logiciel, mais dans l’absence de pile. Tant que la RTC repartait vide à chaque coupure, la carte n’avait rien à opposer à la Pi : la règle « la plus avancée gagne » n’avait pas de terme à comparer.
Test refait le jour même, pile en place, serveur de temps volontairement éteint :
==> horloge reglee sur la RTC de la carte : 2026-09-26T12:30:01Z
==> agent relance pour recreer ses entites a la bonne date
La chaîne de secours a fonctionné — pour la première fois depuis qu’elle existe.
Deux dégâts collatéraux à connaître
Une horloge qui recule ne casse pas que les horodatages.
``journalctl`` devient illisible. Il trie par horodatage : les entrées du
jour se rangent avant celles de la veille. Un journalctl -n 12 rend des
lignes vieilles de deux jours, et on cherche la panne dans le mauvais passé.
Le démon de ``ros2`` plante (!rclpy.ok()) et la découverte cesse de
fonctionner sur la machine — alors qu’une autre machine du réseau voit les
nœuds normalement. Symptôme déroutant : le robot publie, mais lui-même ne se
voit pas.
Deux pièges déjà payés
L’agent démarrait avant chrony
Relevé dans le journal d’un démarrage du 2026-09-10 :
15:44:17 Started Agent micro-ROS RatBot
15:44:23 chronyd: System clock wrong by 3726530.468641 seconds
L’agent a démarré six secondes avant que chrony ne se synchronise, alors que
l’horloge de la Pi était fausse de 43 jours. L’ouverture de session appelle
rmw_uros_sync_session() : tout ce que la carte a publié dans cette fenêtre
était daté de 43 jours en arrière.
D’où, dans ratbot.service — l’unité qui porte l’agent :
After=chrony.service
Wants=chrony.service
ExecStartPre=-/usr/bin/chronyc waitsync 6 0.5 0 2
Note
Le tiret devant la commande est le point important. Sans lui, un robot sans réseau ne démarrerait jamais son agent. Avec, on attend ce qu’on peut — douze secondes au pire — puis on part quand même, dégradé mais vivant. La carte, de son côté, refusera l’heure invraisemblable et gardera sa RTC.
Le cycle d’ordonnancement systemd
ratbot-time.service est ordonné After=ratbot.service — logique, c’est
par l’agent, lancé par cette unité, qu’arrive status/rtc.
Avec un WantedBy=multi-user.target, on obtenait :
multi-user → heure → pile → multi-user ← cycle
parce qu’une cible s’ordonne automatiquement après ce qu’elle veut. systemd casse un cycle en supprimant un démarrage, et c’est celui-ci qu’il a supprimé :
Job ratbot-time.service/start deleted to break ordering cycle
Résultat : Pi restée 44 jours en retard, sans message d’erreur. La
correction est WantedBy=ratbot.service : voulu par la pile, il démarre avec
elle, sans cycle.
Note
Les deux unités ratbot-agent.service et ratbot-signals.service ont été
remplacées le 2026-09-26 par une seule, ratbot.service, qui séquence tout
dans boot.launch.py. Le raisonnement sur le cycle reste identique, seul le
nom change — voir Bringup, section Ce qui démarre tout seul.
Un troisième piège : une relance qui ne relançait rien
ratbot time ne se contente pas d’avancer l’horloge : il relance la pile
juste après, pour qu’elle recrée ses entités ROS à la bonne date.
Warning
Une relance ``systemd-run –no-block`` ne vérifie rien. Une version de
ratbot time relançait ratbot-agent.service, unité supprimée lors du
passage à l’unité unique : la commande rendait la main aussitôt, annonçait
« agent relancé » et retournait 0, alors que rien n’avait été relancé.
Trouvé par une revue de code, pas par un essai — précisément parce qu’il n’y avait rien à voir.
Deux leçons, plus générales que ce bug :
si ce qu’une relance déclenche est vital, il faut le constater autrement qu’en supposant qu’elle a réussi ;
supprimer une unité systemd oblige à chercher toutes ses mentions, y compris dans du code qui ne ressemble pas à de la configuration.
Note
Ce que le saut d’horloge ne fait PAS. On a longtemps soupçonné qu’un
participant DDS né avant le saut restait cassé, et que c’était la cause du
voyant éteint du 2026-09-26. C’est faux, et mesuré comme tel : serveur de
temps allumé, chronyc waitsync corrige l’horloge avant que la pile ne
démarre — saut à 00:22:26, pile démarrée à 00:22:30, un seul
démarrage — et le voyant reste éteint quand même.
La relance ci-dessus garde son intérêt pour les horodatages (voir plus
haut, la fenêtre de 43 jours), mais elle n’explique pas ce défaut-là. La vraie
cause était logind, qui effaçait la mémoire partagée de la pile — voir
La découverte DDS : qui voit qui.
Détail qui a longtemps brouillé les pistes : quand le serveur de temps était éteint, cette relance automatique recréait la mémoire partagée vers 57 s après le démarrage, et masquait la panne. Le robot ne tombait en panne que lorsqu’il était bien configuré.
Ce qui est versionné, et ce qui ne l’est pas
Le dépôt porte la source de temps, pas toute la configuration de chrony :
deploy/ratbot-time.sources -> /etc/chrony/sources.d/ratbot-time.sources
Le reste — makestep 1 3, rtcsync, les pools — vient de la
distribution. ./scripts/ratbot services pose la source et commente les
pools.
Important
Un fichier déposé dans ``conf.d/`` ne peut rien redéfinir. confdir
/etc/chrony/conf.d est la première ligne de chrony.conf, donc les
fichiers de conf.d sont lus avant tout le reste — et chez chrony,
seule la dernière occurrence d’une directive prend effet.
Un makestep posé dans conf.d serait donc écrasé par celui de la
distribution, silencieusement. Les pools ne se neutralisent que dans
chrony.conf lui-même : le sed du script n’est pas de la paresse,
c’est la seule voie.
Corollaire : une mise à jour du paquet chrony peut reposer son
``chrony.conf`` et ramener les pools. Le script re-vérifie donc à chaque
passage, et cherche dans chrony.conf et dans conf.d/.
Vérifier plutôt que supposer
Une configuration juste sur le papier ne prouve rien. Après le redémarrage de
chrony, ratbot services regarde ce que le démon utilise réellement :
verifie : une seule source, 192.168.1.190
Toute autre sortie est un avertissement. Deux sources peuvent apparaître sans
qu’on l’ait demandé : un fichier oublié dans conf.d/, ou une source poussée
par le DHCP — chrony lit aussi /run/chrony-dhcp. Dans les deux cas le robot
diverge des caméras, et rien d’autre ne le signalerait.
Et les certificats ?
Une machine sans horloge qui démarre dans le passé échoue toutes ses
vérifications TLS : les certificats du serveur paraissent pas encore
valides. apt, git clone en HTTPS, tout ce qui chiffre tombe — avec des
messages qui parlent de certificat et jamais d’horloge, ce qui envoie chercher
la panne du mauvais côté.
Note
Cela ne bloque pas la synchronisation ici, et c’est un choix : le robot
utilise du NTP simple, qui n’est pas chiffré et ne vérifie aucun
certificat. La ligne server 192.168.1.190 iburst prefer ne mentionne pas
nts.
Avec NTS (Network Time Security, du NTP sur TLS), on aurait un problème d’amorçage classique : il faut l’heure juste pour valider le certificat, et le certificat pour obtenir l’heure. Sur un réseau de labo fermé, sans Internet, NTS n’apporterait rien et coûterait cette fragilité.
Conséquence pratique : sur une Pi qui vient d’être installée et n’a jamais vu le serveur de temps, régler l’heure avant de tenter quoi que ce soit en HTTPS. Le robot le fait tout seul au démarrage grâce à la RTC de sa carte ; une machine de développement neuve, non.
Vérifier la chaîne
# 1. la Pi est-elle synchronisée, et sur quoi ?
chronyc tracking
chronyc sources -v
# 2. que dit la carte de son horloge ?
ros2 topic echo --once /diagnostics | grep -A12 "clock"
# 3. l'heure de la carte, brute
ros2 topic echo --once /ratbot_N/status/rtc
Le bloc clock porte notamment boot_backup_blank (la pile a-t-elle tenu),
boot_lse_ready (le quartz a-t-il démarré) et les compteurs de réécriture.
Voir Architecture de l’application firmware, section Horloge sur pile, pour ce que chacun
signifie et pour l’erratum du CSS du LSE — à ne jamais armer sur cette
carte.
Voir aussi
Architecture de l’application firmware — la RTC, le quartz LSE et l’erratum ES0396.
Bringup — les services systemd du robot.
Côté utilisateur : L’heure du robot.