Spec, plan d'implémentation, design system, documentation de déploiement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
10 KiB
Design — Firmware esp_jardin
Date : 2026-05-23 Statut : Approuvé
1. Contexte
Firmware embarqué pour une station de monitoring environnemental basée sur un ESP32. La station acquiert les températures de 3 sondes DS18B20 sur un bus OneWire, conserve un historique glissant de 24h en RAM et expose deux interfaces d'accès :
- Une interface web locale temps réel (WebSocket + Chart.js) servie depuis LittleFS.
- Une API REST pour l'intégration externe (Home Assistant, scripts, MCP).
- Une publication MQTT par sonde avec filtre deadband.
Développement sans hardware disponible — les logs série (115200 baud) sont le principal outil de validation.
2. Stack technique
Bibliothèques PlatformIO
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
board_build.filesystem = littlefs
lib_deps =
esp32async/ESPAsyncWebServer
esp32async/AsyncTCP
paulstoffregen/OneWire @ ^2.3.7
milesburton/DallasTemperature @ ^3.11.0
knolleary/PubSubClient @ ^2.8
upload_flags = --auth=Jardin2026
Incluses dans le framework (pas de lib_deps) : ArduinoOTA, ESPmDNS, LittleFS, WiFi.
Justifications des choix
- ESPAsyncWebServer (esp32async) : fork actif depuis jan. 2025, remplace
me-no-dev(abandonné) etmathieucarbou(archivé). Gère HTTP + WebSocket dans la même instance, non-bloquant par nature. - DallasTemperature + OneWire : combo standard, bien documenté, supporte l'adressage par index sur un bus multi-sondes.
- PubSubClient : le plus documenté, pattern non-bloquant avec
millis()+client.loop()éprouvé. - LittleFS : filesystem recommandé pour ESP32 (SPIFFS déprécié), stockage de
index.htmlet assets web.
3. Architecture logicielle
Principe
Architecture modulaire avec structures globales partagées — idiome Arduino classique, le plus adapté au développement sans hardware et au debugging via Serial.
Chaque module expose deux fonctions publiques : init() appelé dans setup() et update() appelé dans loop(). Aucun delay() dans le code.
Arborescence cible
include/
config.h ← structs globales, constantes depuis parametrage.md
network.h ← WiFi hybride STA/AP, mDNS, OTA
sensors.h ← OneWire, DallasTemperature, buffer circulaire
web_server.h ← ESPAsyncWebServer, routes REST, WebSocket
mqtt_manager.h ← PubSubClient, deadband, reconnexion
src/
main.cpp ← setup() + loop() machine à états uniquement
network.cpp
sensors.cpp
web_server.cpp
mqtt_manager.cpp
data/
index.html ← interface web, flashée avec `pio run -t uploadfs`
4. Structures de données globales
Déclarées dans config.h, définies dans main.cpp, accessibles via extern dans tous les modules.
// Configuration immuable d'une sonde (initialisée depuis parametrage.md)
struct SondeConfig {
const char* nom; // "T°C Ext"
const char* topic; // "maison/jardin/ext/temperature"
uint32_t intervalleMs; // 60 000 ms
float deadband; // 0.2 °C
};
// État runtime d'une sonde
struct SondeEtat {
float tempActuelle; // NAN si erreur capteur
float dernierPublie; // dernière valeur publiée MQTT
uint32_t dernierPubliMs; // timestamp dernière publication
bool erreur; // true si -127.0 ou 85.0 reçu
};
// Point d'historique (tampon circulaire × 288)
struct PointHistorique {
uint32_t timestamp; // millis() au moment de l'acquisition
float temps[3]; // NAN si sonde en erreur à ce moment
};
// État réseau global
struct NetworkStatus {
bool wifiConnecte;
bool modeAP; // true = fallback AP actif
bool mqttConnecte;
int8_t rssi;
uint32_t uptimeMs;
};
// Instances globales
extern SondeConfig sondesConfig[3];
extern SondeEtat sondesEtat[3];
extern PointHistorique historique[288];
extern uint16_t histIdx; // tête du buffer circulaire
extern NetworkStatus netStatus;
Budget mémoire historique : 288 × (4 + 3×4) = 288 × 16 = 4 608 octets ≈ 4,5 Ko sur 520 Ko SRAM → < 1%.
5. Machine à états réseau
BOOT
└─ tentative WiFi STA (30 000 ms timeout)
├─ succès → WIFI_STA_OK
│ ├─ mDNS (esp_jardin.local)
│ ├─ ArduinoOTA
│ ├─ WebServer + WebSocket
│ └─ connexion MQTT (retry 5s non-bloquant)
│
└─ échec → WIFI_AP_FALLBACK
├─ AP "ESP_CHEF_JARDIN" / "Jardin2026"
├─ WebServer actif (pas de MQTT)
└─ retry STA toutes les 60s (non-bloquant)
WIFI_STA_OK
└─ déconnexion détectée → retry STA (non-bloquant, toutes les 30s)
6. Flux de données dans loop()
loop()
├─ network.update() → ArduinoOTA.handle(), vérification WiFi, retry
├─ sensors.update() → toutes les 10 000 ms :
│ ├─ requestTemperatures() (non-bloquant avec setWaitForConversion(false))
│ ├─ validation (-127.0 et 85.0 rejetés)
│ ├─ écriture sondesEtat[]
│ ├─ écriture historique[histIdx] + incrémentation circulaire
│ └─ notifyClients() → push JSON WebSocket à tous les clients connectés
└─ mqtt.update() → client.loop(), reconnexion, publication deadband
Format JSON WebSocket (push temps réel)
{
"sondes": [
{ "nom": "T°C Ext", "temp": 19.3, "erreur": false },
{ "nom": "T°C Serre", "temp": 28.7, "erreur": false },
{ "nom": "T°C Sol", "temp": null, "erreur": true }
],
"uptime": 3600,
"rssi": -62
}
Endpoints REST
| Méthode | Chemin | Réponse |
|---|---|---|
| GET | /api/status |
{ "rssi": -62, "uptime": 3600, "ramLibre": 187432, "modeAP": false, "mqttConnecte": true } |
| GET | /api/temperatures |
{ "sonde_1": 19.3, "sonde_2": 28.7, "sonde_3": null, "unit": "C" } |
| GET | /api/history |
Tableau de 288 objets { "ts": 12345, "t": [19.3, 28.7, null] } |
| POST | /api/config |
Body : { "intervalleMs": 10000, "mqttBroker": "10.0.0.3", "mqttPort": 1883 } — appliqué immédiatement en RAM |
7. Publication MQTT
Chaque sonde publie indépendamment selon deux conditions cumulatives :
abs(tempActuelle - dernierPublie) >= deadbandOUmillis() - dernierPubliMs >= intervalleMax- WiFi STA connecté ET MQTT connecté
Reconnexion MQTT non-bloquante : tentative toutes les 5 000 ms via millis().
8. Interface web (data/index.html)
Page unique auto-contenue, servie depuis LittleFS. Chargement initial :
- Fetch
GET /api/history→ initialise le graphique Chart.js - Connexion WebSocket
ws://[ip]/ws→ réceptions push temps réel
Layout (Gruvbox Seventies design system)
┌─ Header : titre + statut WiFi/RSSI + bouton config ──────────────────┐
├─────────────────────────────────────────────────┬────────────────────┤
│ 3 cartes sondes (grid 3 colonnes) │ Statut système │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ WiFi / IP / RSSI │
│ │ 19.3 °C │ │ 28.7 °C │ │ ERREUR │ │ MQTT / RAM / uptime│
│ │ sparkline│ │ sparkline│ │ câblage? │ ├────────────────────┤
│ └──────────┘ └──────────┘ └──────────┘ │ Configuration │
│ │ intervalle / MQTT │
│ Graphique 24h (Chart.js, 3 courbes) │ [Appliquer] │
│ │ │
├─────────────────────────────────────────────────┴────────────────────┤
│ Status bar : MODE | ● MQTT | n sondes | esp_jardin.local | HH:MM:SS │
└──────────────────────────────────────────────────────────────────────┘
Règles design system :
- Variables CSS uniquement (
var(--accent), jamais de hex en dur) data-theme="dark"sur<html>- Polices : Inter (UI), JetBrains Mono (valeurs numériques), Share Tech Mono (status bar, logs)
- Tuiles :
border-radius: 12px, classeglass - Sonde en erreur : bordure
var(--err)+ LED pulsante rouge
9. Gestion des erreurs
| Cas | Comportement firmware | Rendu UI |
|---|---|---|
| Sonde −127°C ou 85°C | erreur = true, valeur non écrite en RAM ni publiée MQTT |
Carte rouge + "Sonde déconnectée" |
| WiFi STA perdu | Retry non-bloquant toutes les 30s | LED header warn, barre "STA reconnexion…" |
| MQTT injoignable | Retry non-bloquant toutes les 5s | Indicateur MQTT rouge |
| AP fallback actif | WebServer opérationnel, MQTT désactivé | Barre de statut mode "AP" en orange |
| WebSocket client déco | Cleanup automatique ESPAsyncWebServer | — |
Logs série : chaque événement logué avec millis() — acquisition, erreur sonde, changement WiFi, pub MQTT, connexion WS.
10. OTA
- Mot de passe firmware :
Jardin2026(défini viaArduinoOTA.setPassword("Jardin2026")) - Mot de passe
platformio.ini:upload_flags = --auth=Jardin2026 - Upload firmware :
pio run -t upload --upload-port [ip] - Upload filesystem :
pio run -t uploadfs --upload-port [ip]
11. Hors scope V1
- Deep sleep / économie d'énergie (noté dans
amelioration.md) - NTP (timestamps relatifs
millis()en V1, epoch en V2) - Capteurs I2C (BH1750, SHT31), ADC sol, relais, pluviomètre
- Authentification interface web