Files
esp_jardin/docs/superpowers/specs/2026-05-23-esp-jardin-firmware-design.md
T
gilles 7aa8cd2a1c init: structure initiale du projet esp_jardin
Spec, plan d'implémentation, design system, documentation de déploiement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 14:58:24 +02:00

10 KiB
Raw Blame History

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

[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.

// 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 :

  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 <html>
  • 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