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

digraph heure { rankdir=TB; bgcolor="transparent"; node [shape=box, style="rounded,filled", fontname="sans-serif", fontsize=10, margin="0.2,0.12"]; edge [fontname="sans-serif", fontsize=9]; serveur [label="1. Serveur de temps du labo\n192.168.1.190\n\nune Pi + RTC DS3232 sur pile\nla référence du parc", fillcolor="#d5e8d4", color="#82b366"]; pi [label="2. Raspberry Pi du robot\nPAS DE RTC\n\nchrony, makestep 1 3\nl'agent attend : chronyc waitsync 6 0.5 0 2", fillcolor="#dae8fc", color="#6c8ebf"]; carte [label="3. ratbot_controller\nRTC + pile VBAT\n\nà l'ouverture de session :\nLA PLUS AVANCÉE GAGNE", fillcolor="#ffe6cc", color="#d79b00"]; secours [label="4. ratbot-time.service\nAfter = ratbot.service\n\nlit status/rtc et AVANCE l'horloge de la Pi\njamais en arrière", fillcolor="#f8cecc", color="#b85450"]; serveur -> pi [label=" NTP, si le réseau est là"]; pi -> carte [label=" rmw_uros_sync_session()"]; carte -> secours [label=" la Pi n'a toujours pas d'heure"]; secours -> pi [label=" heure de la carte", style=dashed, constraint=false]; }

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, agent_implausible++. Le plancher est la date du dernier commit, injectée à la compilation (RATBOT_BUILD_EPOCH) : la carte ne peut pas croire un instant où son propre programme n’existait pas

RTC en avance de plus de 60 s (BEHIND_MARGIN_MS)

la RTC gagne, pi_behind = true. C’est le signal que ratbot-time.service attend

sinon, à l’ouverture

la Pi devient la référence (agent_trusted)

une Pi déjà fiable recule de plus de 60 s

la confiance est retirée, trust_lost++, et la règle de l’ouverture s’applique à nouveau

écart RTC / agent supérieur à 1 s (REWRITE_MS)

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