7aa8cd2a1c
Spec, plan d'implémentation, design system, documentation de déploiement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
262 lines
10 KiB
Markdown
262 lines
10 KiB
Markdown
# 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
|