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:
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
|
||||
Reference in New Issue
Block a user