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>
This commit is contained in:
2026-05-23 14:58:24 +02:00
commit 7aa8cd2a1c
18 changed files with 5503 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,261 @@
# 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