FluoLight : réécrire un firmware Arduino hérité en quelque chose de défendable
Deuxième épisode de la série FluoBoard, après l’autopsie de la carte d’origine
. J’y promettais une suite, la voici et elle ne s’est pas faite en une nuit. Dès 2021, j’ai attaqué par le manque le plus criant: découper le monolithe en plusieurs fichiers .ino par responsabilité, parce qu’un sketch unique ne se maintient pas, en corrigeant les bugs au passage et en posant les #define qui manquaient. Est venue ensuite une boucle d’événements écrite à la main, ma première manière de cadencer des actions dans le temps, avant que je ne reprenne la bibliothèque TimedAction pour le faire proprement. Le gros du travail s’est fait en 2021-2022, les retouches finales venant plus tard [edit: 2025], le tout suivable commit par commit. Même famille de boards, même protocole /lights/<mac> dont la réponse tient dans un chiffre entre <...>. Ce qui change n’est pas la liste des fonctionnalités, c’est que la boucle ne s’arrête jamais pour attendre.
La seule règle: ne jamais bloquer
Le péché cardinal de l’ancien code était delay(show_delay * 1000) dans chaque branche. La promesse centrale de la réécriture: rien ne dort jamais quand quelque chose pourrait arriver. La boucle principale entière:
void loop() {
ethLinkStatus.check();
if (isEthLinkActive) {
httpRequest.check();
httpProcess.check();
}
dispatchEvents.check();
animTick();
wdt_reset();
}
Cinq appels .check() sur des timers TimedAction
: statut du lien toutes les 500 ms, requête HTTP toutes les 3 s, lecture HTTP toutes les 600 ms, dispatch des événements toutes les 50 ms, tick d’animation à chaque passage. La board poll aussi souvent que configuré et reste réactive entre les deux: les animations, le watchdog continuent de respirer. delay() existe toujours dans le code, mais seulement sous forme de MAIN_LOOP_INTERVAL, à zéro par défaut.
Un parseur qui survit à la fragmentation TCP
Le vieux readPage() retournait un pointeur vers un buffer local et tombait hors du bout à l’expiration. Le nouveau http.ino garde l’état du parseur en statics de portée fichier pour qu’une coupure de paquet au milieu de <6 ne perde pas le chiffre:
} else if (httpTokenPos < sizeof(httpToken) - 1) {
httpToken[httpTokenPos++] = c;
} else {
httpCapturing = false; // token overflow: abandon current capture
}
Le débordement est abandonné, pas tronqué, pour qu’une charge hostile ou bugée ne puisse pas boucler pour toujours. Et le token accepté est strictement un chiffre (httpTokenPos == 1 && isDigit(httpToken[0])): le protocole dit <N>, tout le reste est du bruit et est ignoré au lieu d’être matérialisé par un toInt().
La configuration comme profiles à la compilation
Tout ce que je hardcodais dans les sources vit maintenant dans config.h et chaque option documente son prix en flash: VERBOSE 0-3 (0 production durcie à 3 dev only, ~+11% de flash au niveau 3), MAC depuis l’EEPROM ou manuelle, DHCP ou statique, proxy ou direct, check du lien externe on/off. Deux filets de sécurité avec un contrat honnête dans le commentaire: au moins un de WATCHDOG ou RESET_ON_FAIL doit être activé pour un usage production sans surveillance; le watchdog couvre le freeze CPU, RESET_ON_FAIL couvre les boucles d’erreurs réseau, activer les deux est sûr.
C’est la réponse à la section “config pour gens non techniques” du vieux code: une section config où chaque interrupteur a un coût documenté et un mode d’échec documenté et où livrer une board sans surveillance avec zéro filet de sécurité est une décision de compilation qu’il faut lire pour la prendre.
Des commentaires qui ne mentent plus
Le vieux fichier avait douze constantes de couleur, six commentées // red, bleus inclus. Le nouveau code met la vérité à exactement un endroit, à côté de la ligne qui ne doit pas être touchée:
// Color order: the strip is physically wired GRB (not RGB).
// NEO_RGB tells the driver to send bytes in G-R-B order, which matches the hardware.
// All Color() / setPixelColor() calls use Color(G, R, B) convention - do NOT change this.
La war story GRB mérite son propre paragraphe et elle en aura un dans l’épisode suivant (le câblage m’a repris, d’une autre façon). La machine à états des événements (events.ino) s’ouvre sur un bloc de doc expliquant ce que fait chaque événement et renvoyant à la machine à états d’animation non-bloquante dans neopixel.ino, pour que le prochain lecteur ait la carte avant le code.
Le logging avec un budget
Le vieux sketch avait 65 appels Serial.print, un par caractère reçu. Le nouveau route tout via un serialMessage(level, tag, msg) avec chaînes F() en flash, filtré par le profile VERBOSE: production au niveau 1 ne logge que les erreurs et paie ~1% de flash pour ça; un échec de connexion ne logge KO qu’après le troisième raté consécutif (HTTP_FAIL_THRESHOLD). Le debug existe, mais on doit l’activer à la compilation, pas le livrer par accident.
Le tableau des scores
| Origine (2019) | FluoLight (2021-2022) | |
|---|---|---|
| sketch principal | 475 lignes | 119 lignes |
| delays bloquants | 20 s par état | 0 (config MAIN_LOOP_INTERVAL=0) |
| parseur | pointeur pendant, UB à l’expiration | état static, garde overflow, chiffre strict |
| config | IP/MAC hardcodées | profiles à la compilation, coûts documentés |
| logging | 65 prints, bloque sur le serial | filtré par niveau, avec seuil, chaînes flash |
Ce que FluoLight n’avait toujours pas à ce stade: de l’intégration continue et le serveur Python qui en fait une veilleuse de CI physique. C’est l’épisode trois .