# 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 : 1. Une interface web locale temps réel (WebSocket + Chart.js) servie depuis LittleFS. 2. Une API REST pour l'intégration externe (Home Assistant, scripts, MCP). 3. 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 ```ini [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é) et `mathieucarbou` (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.html` et 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. ```cpp // 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) ```json { "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 : 1. `abs(tempActuelle - dernierPublie) >= deadband` **OU** `millis() - dernierPubliMs >= intervalleMax` 2. 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 : 1. Fetch `GET /api/history` → initialise le graphique Chart.js 2. 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 `` - Polices : Inter (UI), JetBrains Mono (valeurs numériques), Share Tech Mono (status bar, logs) - Tuiles : `border-radius: 12px`, classe `glass` - 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 via `ArduinoOTA.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