T7 Pièges du runtime

Dix comportements du moteur qui ne se devinent pas. Classés par ce qu'ils ont coûté.
Ces erreurs sont silencieuses ou trompeuses

Du code qui paraît juste ne s'exécute jamais. Une erreur 500 ne nomme pas le node fautif. Un drapeau de désactivation est ignoré. Aucun de ces pièges ne se voit à la lecture.

Vue d'ensemble

PiègeCe qu'il a coûté
1Une sortie ne porte qu'un message48 h sans renouvellement de jetons
2Pas d'analyseur JSONTout se fait par motifs, à la main
3Drapeau de désactivation ignoréUn node cru neutralisé s'exécute
4Deux champs obligatoiresErreur 500 anonyme
5L'adresse vient du messageDéroutant à la lecture
6Outils système réduitsCommandes qui répondent « rien » à tort
7Fichiers appartenant à rootDétour obligatoire pour tout dépôt
8Envoyer le flux redémarre toutPasserelle hors service le 6 juillet
9Le flux en service contient plusUn capteur supprimé le 29 août
10Configuration orpheline muetteUne file de rejeu qui n'existe pas

1 · Une sortie ne porte qu'un seul message

Le retour est lu comme un message par sortie. Une liste de messages n'est pas comprise comme une série, mais comme un message unique — invalide.

-- NON : le moteur lit une liste, pas un message
return { msgs }

-- OUI : un message
return msg
-- OUI : un message par sortie
return { msg_sortie1, msg_sortie2 }
Symptômenot a valid message dans le journal, sans indication du node
CoûtRenouvellement des jetons interrompu 48 heures, du 29 au 31 août 2026
Le piège dans le piègeDeux autres nodes portaient le même motif sans planter — parce qu'ils n'étaient jamais déclenchés. Les observer menait à la conclusion inverse.
ContournerChercher la structure qui accepte plusieurs éléments dans un message. Pour le downlink : la file du moteur LoRa, indexée par équipement. Pour un déclencheur périodique : un déclencheur par cible.

2 · Pas d'analyseur JSON garanti

Le bac à sable Lua n'expose pas de lecteur JSON fiable. Tout se fait par motifs, dont %b{} qui capture une accolade équilibrée :

local section = corps:match('"device_list"%s*:%s*(%b{})')
for cle, val in section:gmatch('"([^"]+)"%s*:%s*"([^"]*)"') do t[cle] = val end

À l'écriture, le JSON est construit par concaténation : une accolade oubliée casse toute la réponse. Les tests vérifient donc l'équilibre du résultat, pas seulement son contenu.

3 · Le drapeau de désactivation est ignoré

Marquer un node désactivé n'a aucun effet : il continue de s'exécuter. Pour désactiver réellement, couper la liaison sortante qui y mène.

Un node qu'on croit neutralisé peut donc consommer un jeton ou écrire un fichier.

4 · Deux champs obligatoires sur tout nouveau node

Chaque node doit porter __package et __version. Sans eux, l'envoi du flux échoue avec une erreur 500 qui ne nomme pas le node fautif.

Parade : ne jamais écrire un node de zéro. Copier un node existant du même type, changer identifiant, nom et liaisons.

5 · L'adresse d'une requête ne vient pas du node

Les nodes de requête HTTP ont leur champ d'adresse vide : elle est posée par la fonction en amont, dans le message.

C'est ce qui a permis de servir deux environnements sans dupliquer une requête. Mais en lisant le flux, la destination d'un appel est toujours dans la fonction qui précède.

6 · Un système ancien, aux outils réduits

AttenduRéalité
bashAbsent. Passer les commandes en une seule chaîne.
grep motif fichier grep -a obligatoire sur les journaux. Sans l'option : « binary file matches » et rien d'affiché — on croit qu'il n'y a aucune occurrence.
\sNon reconnu. Zéro résultat en silence. Utiliser [[:space:]].
SSH standardRefusé : réactiver explicitement les algorithmes anciens.
Ping sans réponse = panneFaux : l'ICMP entrant est filtré. Tester en HTTP.

7 · Les fichiers appartiennent à root

Tout ce que le flux écrit appartient à root, alors que le compte SSH ne l'est pas. On ne peut pas déposer un fichier à son emplacement final.

Le chemin : déposer dans le répertoire temporaire, puis déclencher un node du flux — qui, lui, s'exécute en root — pour la copie.

Ce détour rend l'exploitation autonome

Toute réparation d'état est faisable sans intervention d'un tiers. C'est ce que vérifie la répétition de restauration avant chaque déploiement.

8 · Envoyer le flux redémarre tout

Pas de rechargement partiel : toutes les chaînes redémarrent — radio, envois, authentification.

Conséquence 1Tout redémarrage rejoue la restauration de l'authentification, donc peut déclencher un renouvellement. Ces jetons sont à usage unique : deux renouvellements rapprochés en perdent un.
Conséquence 2Un ajout de capteur passe par cette réécriture : ajouter un capteur redémarre tout. Un ajout lancé pendant un renouvellement a mis la passerelle hors service le 6 juillet 2026.

9 · Le flux en service peut contenir plus que votre copie

Un capteur déclaré depuis le cockpit n'existe que dans le flux de la passerelle. Envoyer un instantané antérieur le supprime sans rien signaler.

Arrivé le 29 août 2026, failli se reproduire le 4 septembre. Le contrôle préalable compare désormais et refuse tout envoi qui retirerait un node.

10 · Une configuration orpheline ne se signale pas

Deux requêtes portent une option de file de rejeu pointant vers une configuration qui n'existe plus. Ni erreur, ni avertissement : la file ne fonctionne simplement pas.

Le moteur ne vérifie pas que les références d'un node existent — d'où le contrôle de câblage ajouté aux tests.

À retenir

Le silence n'est pas une preuve Un node qui ne plante pas peut n'avoir jamais été exécuté
Le flux en service fait autorité Pas votre copie. Comparer avant d'envoyer
Tout redémarrage touche aux jetons Ils sont à usage unique. La séquence de déploiement existe pour ça