guest@6admin.io:~$

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 principal475 lignes119 lignes
delays bloquants20 s par état0 (config MAIN_LOOP_INTERVAL=0)
parseurpointeur pendant, UB à l’expirationétat static, garde overflow, chiffre strict
configIP/MAC hardcodéesprofiles à la compilation, coûts documentés
logging65 prints, bloque sur le serialfiltré 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 .