La fiche disait 40 kbit/s minimum, la chaudière parlait à 20 et n’avait pas l’air désolée

Un adaptateur USB-CAN officiellement trop rapide a écouté puis interrogé le bus Elster d’une pompe à chaleur ; le vrai piège se cachait dans un drapeau `listen-only` que Linux avait poliment conservé.

Partager
Une vieille chaudière émet de minuscules impulsions CAN sous une barrière de débit trop haute, tandis qu’un adaptateur USB se glisse dessous avec un oscilloscope.

Le CAN est né chez Bosch dans les années 1980 pour éviter qu’une automobile ne devienne un élevage de fils électriques. Il a ensuite colonisé les machines industrielles, les ascenseurs et les chaudières, c’est-à-dire tous les endroits où une trame perdue peut produire davantage qu’une roue qui tourne dans un navigateur. La petite histoire qu’en donne encore Bosch parle aujourd’hui de plusieurs mégabits par seconde. Dans la chaufferie qui m’intéressait, le progrès circulait à 20 kbit/s.

Une pompe à chaleur, son gestionnaire et la chaudière d’appoint échangeaient sur ce bus depuis des années. Je voulais lire les températures, les états et, si le matériel consentait à les révéler, les compteurs nécessaires au calcul du rendement. Le Raspberry Pi qui pilotait déjà quatre relais pouvait héberger le lecteur. Son GPIO était occupé et un retour de terrain promettait des disparitions après redémarrage derrière un hub USB ; l’adaptateur serait donc un Korlan USB2CAN branché directement au contrôleur racine.

La fiche dont je disposais annonçait 40 kbit/s comme plancher. Le bus en parlait 20. Le guide Linux proposait de compiler un pilote pour le Raspberry Pi. Le noyau en possédait déjà un. Tous les éléments étaient donc réunis pour une séance d’informatique embarquée traditionnelle : commencer par désobéir à la documentation, mais très prudemment, parce que le radiateur appartient à de vraies personnes.

Le noyau savait déjà faire ce que le guide expliquait comment compiler

L’adaptateur s’est présenté avec l’identifiant USB 0483:1234, son fabricant 8devices et un numéro de série. Le module usb_8dev était déjà chargé. Aucun pilote externe, aucun DKMS et aucun rituel de compilation n’étaient nécessaires. Le fabricant conserve d’ailleurs une page intitulée « Compile Driver for Linux (Raspberry Pi) », tandis que sa fiche actuelle annonce désormais 20 à 2 000 kbit/s. Les spécifications finissent par rejoindre le matériel ; elles prennent seulement parfois le chemin touristique.

Sous Linux, SocketCAN expose un contrôleur CAN comme une interface réseau. La documentation du noyau permet donc d’ouvrir une socket PF_CAN, de la lier à can0, puis de lire les trames avec les mêmes primitives qui servent ailleurs à faire croire que tous les réseaux se ressemblent.

La première montée de l’interface était volontairement muette :

ip link set can0 down
ip link set can0 type can bitrate 20000 listen-only on
ip link set can0 up
ip -details -statistics link show can0

Le détail important n’est pas que ip accepte le nombre. Une configuration acceptée ne mesure jamais le signal qui passe dans le cuivre. Pendant la capture, 1 515 trames ont été reçues, sans bus-error ni arbit-lost ; l’état est resté ERROR-ACTIVE, avec un débit calculé à 20 000 bit/s, un point d’échantillonnage à 0,875 et brp 100. Le débit interdit était donc non seulement accepté par le contrôleur, mais correctement aligné sur le bus réel.

listen-only n’était pas une décoration rassurante. Un contrôleur CAN normal acquitte les trames qu’il reçoit en forçant un bit dominant. Le simple fait d’écouter revient alors à participer électriquement à la conversation. En écoute stricte, l’adaptateur ne conduit jamais la ligne, même pour signaler une erreur. Une mauvaise hypothèse de débit aurait produit du silence dans mon terminal, pas une nouvelle panne de chauffage à raconter.

Six minutes de silence ont rendu vingt et une grandeurs

Le protocole applicatif était Elster. Les identifiants CAN désignaient les participants ; les octets transportaient un index de grandeur, une opération et une valeur. Une capture passive de six minutes a livré vingt et une valeurs spontanées : températures, états, horloge et quelques consignes. Ni énergie, ni durée de marche, ni température du circuit frigorifique n’étaient diffusées.

Le tableau de bord pouvait donc afficher que la machine existait, mais pas répondre à la question intéressante : combien de chaleur avait-elle produite pour l’électricité absorbée ? Les captures publiées d’une autre installation montraient des index prometteurs. L’hypothèse devenait testable, à condition de quitter l’écoute stricte et de poser les questions au bus.

Cette transition changeait la nature du chantier. Lire sans acquitter est physiquement passif ; émettre une requête Elster ajoute un participant sur le réseau de régulation. Une cadence d’une requête toutes les 250 ms a donc été retenue. Les 32 grandeurs utiles tenaient dans un cycle d’environ une minute, soit à peu près 1 % d’occupation du bus. Les compteurs d’erreur seraient relus à chaque cycle ; à la première hausse, le service cesserait d’interroger et attendrait un redémarrage explicite.

La requête est reconnaissable à son quartet bas égal à 1 : elle demande une valeur et n’en joint aucune. Une écriture employerait 2, puis la valeur. Le service ne possède aucune fonction capable de former cette seconde variante, et ses 32 requêtes sont testées. Ce n’est pourtant pas une barrière matérielle. N’importe quel autre programme ayant accès à can0 peut produire les sept octets d’écriture. La chaudière n’est pas devenue inoffensive parce que mon code a de bonnes manières ; elle a seulement rencontré un processus qui se tient bien pour l’instant.

send() a réussi, donc aucun paquet n’est parti

Pour interroger, l’interface devait perdre son mode d’écoute stricte. La séquence paraissait évidente :

ip link set can0 down
ip link set can0 up type can bitrate 20000

La socket s’est ouverte. send() a rendu un succès. Le programme a annoncé une émission. Le bus n’a répondu à rien, parce que Linux avait conservé listen-only.

Omettre un mode dans une commande ip link set ne demande pas toujours de le remettre à sa valeur par défaut. Ici, le drapeau précédent a survécu à la descente puis à la remontée de l’interface. Le noyau acceptait les octets côté socket et le contrôleur, toujours tenu au silence, comptait TX packets 0, dropped 1. L’appel système décrivait l’entrée dans une file, pas l’arrivée d’un front électrique sur le câble.

La commande qui compte possède un mot de plus :

ip link set can0 down
ip link set can0 type can bitrate 20000 listen-only off
ip link set can0 up
ip -details -statistics link show can0

La syntaxe SocketCAN documentée par le noyau expose bien listen-only { on | off }. Ce que la documentation ne promet pas, c’est que l’absence de l’option signifie off. La relecture de <LISTEN-ONLY> et des compteurs TX a trouvé le problème que le retour de send() avait parfaitement raté. Par chance, l’état conservé était celui qui empêchait d’émettre. Les états implicites ont rarement cette délicatesse deux fois de suite.

La pompe répondait très précisément qu’elle n’avait rien à dire

Une fois les requêtes réellement parties, l’adresse 0x500 a fourni un témoin positif propre. Elle rendait 0x1201 pour l’identifiant de l’appareil, 18 pour son numéro logiciel, 1 pour sa version et une température d’évaporation qui variait entre deux relevés. La chaîne d’interrogation, le format des requêtes et le décodage général fonctionnaient.

Les index d’énergie, les compteurs de marche et les températures frigorifiques ont ensuite été demandés aux onze destinataires connus depuis les trois identifiants d’émission autorisés. Au lieu de ne pas répondre, 0x500 a rendu la sentinelle 0x00ff quarante et une fois. Cette constance est une preuve plus utile qu’un vide : le même appareil distinguait les grandeurs implantées des grandeurs absentes.

La valeur brute 0x00ff se décode volontiers en 25,5 °C si on lui applique la mauvaise fonction. Une température crédible est ainsi apparue pour deux index différents, pendant qu’un autre participant annonçait 32 °C au même instant. Le tableau de bord refuse ces nombres. Il refuse aussi une température d’évaporation à −64 °C dont le format reste ambigu et un code d’erreur 69 dont la table ne précise pas l’encodage. Une case vide avoue une limite ; un 25,5 °C inventé obtient une légende et commence une carrière.

Le COP n’est donc pas calculable sur cette installation avec les valeurs exposées par le bus. Le service publie les températures, consignes, états et trames brutes qu’il sait défendre. Il interroge lentement, surveille les erreurs et retire l’interface lorsqu’il s’arrête. Le visualiseur conserve aussi les trames inconnues au lieu de leur attribuer une biographie.

Une limite franchie, deux limites mieux dessinées

La limite de 40 kbit/s n’existait pas dans le silicium présent sur la table. Le pilote à compiler existait déjà dans le noyau. En revanche, la passivité du montage, la capacité d’écriture du protocole et l’absence des compteurs énergétiques étaient bien réelles. Désobéir à une fiche technique n’autorise pas à négocier de la même façon avec toutes les autres limites rencontrées en route.

Il reste un condensé assez court pour refaire le trajet : brancher l’adaptateur hors hub, vérifier usb_8dev, monter can0 à 20 000 bit/s en listen-only, mesurer des trames sans erreurs, retirer ensuite le mode avec un off explicite, relire les drapeaux et les compteurs, puis conserver un témoin connu avant d’interpréter les sentinelles. Pour Elster, le quartet 1 interroge et le quartet 2 écrit ; cette différence mérite davantage qu’un commentaire dans un script.

Le chantier a coûté un adaptateur, quelques heures de capture et un calcul de rendement qu’il a finalement fallu ne pas afficher. La pompe à chaleur parle maintenant dans une page lisible, à la vitesse qu’elle a choisie. La fiche technique a depuis appris à annoncer 20 kbit/s. Le chauffage, lui, n’avait pas attendu la mise à jour du site web pour avoir raison.