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

262 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<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