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 : .. code-block:: text 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 ------------------------- .. graphviz:: :caption: Les quatre filets successifs, du plus fiable au dernier recours. 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 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``. .. list-table:: :header-rows: 1 :widths: 34 66 * - 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 : .. code-block:: text ==> 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 : .. code-block:: text 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 : .. code-block:: ini 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 : .. code-block:: text 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é : .. code-block:: text 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 :doc:`../ros2/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 :doc:`../ros2/decouverte`. 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 : .. code-block:: text 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** : .. code-block:: text 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 -------------------- .. code-block:: bash # 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 :doc:`application`, 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 ------------ - :doc:`application` — la RTC, le quartz LSE et l'erratum ES0396. - :doc:`../ros2/bringup` — les services systemd du robot. - Côté utilisateur : `L'heure du robot `_.