# ESP Jardin — Plan d'implémentation firmware > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal :** Firmware ESP32 complet — acquisition 3× DS18B20, historique 24h RAM, interface web temps réel (WebSocket + Chart.js), API REST, publication MQTT avec deadband, WiFi hybride STA/AP, OTA. **Architecture :** Modules C++ indépendants (fichiers .h/.cpp séparés) communiquant via structures globales déclarées dans `config.h`. Pas de `delay()` — tout timing via `millis()`. `main.cpp` contient uniquement `setup()` + `loop()` qui appelle `update()` sur chaque module. **Tech Stack :** PlatformIO / Arduino framework, ESP32, ESPAsyncWebServer + AsyncTCP, DallasTemperature + OneWire, PubSubClient, LittleFS, ArduinoOTA, ESPmDNS. **Note sur les tests :** Développement sans hardware — la vérification se fait par compilation (`pio run`) et lecture des logs série (`pio device monitor`) une fois flashé. Chaque tâche se termine par une compilation propre. **Spec de référence :** `docs/superpowers/specs/2026-05-23-esp-jardin-firmware-design.md` --- ## Fichiers créés / modifiés | Fichier | Rôle | |---|---| | `platformio.ini` | Configuration board, dépendances, LittleFS, OTA | | `parametrage.md` | Source de vérité des constantes (SSID, MQTT, sondes) | | `include/config.h` | Structs globales, constantes, extern declarations | | `include/network.h` | Déclarations WiFi STA/AP, mDNS, OTA | | `src/network.cpp` | Implémentation réseau non-bloquante | | `include/sensors.h` | Déclarations OneWire, buffer circulaire | | `src/sensors.cpp` | Acquisition DS18B20, validation, historique | | `include/web_server.h` | Déclarations HTTP routes + WebSocket | | `src/web_server.cpp` | Routes REST, push WebSocket | | `include/mqtt_manager.h` | Déclarations client MQTT | | `src/mqtt_manager.cpp` | PubSubClient, deadband, reconnexion | | `src/main.cpp` | setup() + loop() machine à états | | `data/index.html` | Interface web complète (HTML + CSS + JS inline) | --- ## Phase 1 — Infrastructure C++ --- ### Tâche 1 : platformio.ini + parametrage.md **Fichiers :** - Modifier : `platformio.ini` - Créer : `parametrage.md` - [ ] **Étape 1 : Mettre à jour platformio.ini** Remplacer le contenu par : ```ini ; PlatformIO Project Configuration File [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 bblanchon/ArduinoJson @ ^7 upload_flags = --auth=Jardin2026 ``` - [ ] **Étape 2 : Créer parametrage.md** ```markdown # Paramétrage Initial — esp_jardin ## Connexion WiFi - Mode Station (STA) : - SSID: "Mon_Reseau_WiFi" - PASS: "Mon_Mot_De_Passe_Securise" - Mode Access Point (AP de secours) : - AP_SSID: "ESP_CHEF_JARDIN" - AP_PASS: "Jardin2026" - Connection_Timeout: 30000 ms ## Acquisition & Fréquences - Fréquence de mesure : 10 000 ms - Taille historique RAM : 288 points par acquisition ## Broker MQTT - IP Broker: "10.0.0.3" - Port: 1883 - MQTT_User: "" - MQTT_Pass: "" ## Sondes - Sonde 0 — T°C Ext | topic: maison/jardin/ext/temperature | intervalle: 60 000 ms | deadband: 0.2 - Sonde 1 — T°C Serre | topic: maison/jardin/serre/temperature | intervalle: 60 000 ms | deadband: 0.1 - Sonde 2 — T°C Sol | topic: maison/jardin/sol/temperature | intervalle: 60 000 ms | deadband: 0.1 ``` - [ ] **Étape 3 : Vérifier la compilation** ```bash pio run ``` Résultat attendu : `SUCCESS` (le `main.cpp` de base compile). --- ### Tâche 2 : config.h — Structures globales et constantes **Fichiers :** - Créer : `include/config.h` - Modifier : `src/main.cpp` - [ ] **Étape 1 : Créer include/config.h** ```cpp #pragma once #include // ── Constantes matérielles ────────────────────────────────────────── #define ONE_WIRE_BUS 4 // GPIO 4 — bus OneWire DS18B20 #define NB_SONDES 3 #define HIST_TAILLE 288 // 24h × (3600/10/300 × 60) = 288 pts #define MESURE_INTERVALLE 10000 // ms entre deux acquisitions // ── Constantes réseau ─────────────────────────────────────────────── #define WIFI_SSID "Mon_Reseau_WiFi" #define WIFI_PASS "Mon_Mot_De_Passe_Securise" #define AP_SSID "ESP_CHEF_JARDIN" #define AP_PASS "Jardin2026" #define WIFI_TIMEOUT_MS 30000 // timeout avant bascule AP #define WIFI_RETRY_MS 30000 // délai entre retries STA #define MDNS_NOM "esp_jardin" #define OTA_PASS "Jardin2026" // ── Constantes MQTT ───────────────────────────────────────────────── #define MQTT_BROKER "10.0.0.3" #define MQTT_PORT 1883 #define MQTT_USER "" #define MQTT_PASS "" #define MQTT_CLIENT_ID "esp_jardin" #define MQTT_RETRY_MS 5000 // ── Structure : configuration d'une sonde (immuable) ──────────────── struct SondeConfig { const char* nom; const char* topic; uint32_t intervalleMs; float deadband; }; // ── Structure : état runtime d'une sonde ─────────────────────────── struct SondeEtat { float tempActuelle; // NAN si erreur float dernierPublie; // dernière valeur publiée MQTT uint32_t dernierPubliMs; // timestamp dernière publication bool erreur; }; // ── Structure : point d'historique ───────────────────────────────── struct PointHistorique { uint32_t timestamp; float temps[NB_SONDES]; // NAN si sonde en erreur }; // ── Structure : état réseau ───────────────────────────────────────── struct NetworkStatus { bool wifiConnecte; bool modeAP; bool mqttConnecte; int8_t rssi; uint32_t uptimeDemarrage; // millis() au moment de la connexion STA }; // ── Déclarations extern (définies dans main.cpp) ──────────────────── extern SondeConfig sondesConfig[NB_SONDES]; extern SondeEtat sondesEtat[NB_SONDES]; extern PointHistorique historique[HIST_TAILLE]; extern uint16_t histIdx; extern NetworkStatus netStatus; ``` - [ ] **Étape 2 : Mettre à jour src/main.cpp** ```cpp #include #include "config.h" // ── Définitions des variables globales ───────────────────────────── SondeConfig sondesConfig[NB_SONDES] = { { "T°C Ext", "maison/jardin/ext/temperature", 60000, 0.2f }, { "T°C Serre", "maison/jardin/serre/temperature", 60000, 0.1f }, { "T°C Sol", "maison/jardin/sol/temperature", 60000, 0.1f }, }; SondeEtat sondesEtat[NB_SONDES] = {}; PointHistorique historique[HIST_TAILLE] = {}; uint16_t histIdx = 0; NetworkStatus netStatus = {}; void setup() { Serial.begin(115200); Serial.println("[BOOT] esp_jardin démarrage..."); } void loop() { } ``` - [ ] **Étape 3 : Compiler** ```bash pio run ``` Résultat attendu : `SUCCESS` — toutes les structs compilent sans erreur. --- ### Tâche 3 : Module réseau (network.h + network.cpp) **Fichiers :** - Créer : `include/network.h` - Créer : `src/network.cpp` - Modifier : `src/main.cpp` - [ ] **Étape 1 : Créer include/network.h** ```cpp #pragma once // Initialise WiFi (STA d'abord, AP en fallback), mDNS et OTA void network_init(); // À appeler à chaque loop() : gère OTA, reconnexion WiFi non-bloquante void network_update(); ``` - [ ] **Étape 2 : Créer src/network.cpp** ```cpp #include "network.h" #include "config.h" #include #include #include static uint32_t _dernierRetryMs = 0; static uint32_t _debutConnexionMs = 0; static bool _connexionEnCours = false; // Démarre la tentative de connexion STA (non-bloquant) static void _demarrerSTA() { Serial.printf("[WIFI] Connexion STA → SSID: %s\n", WIFI_SSID); WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); _debutConnexionMs = millis(); _connexionEnCours = true; } static void _demarrerAP() { Serial.println("[WIFI] Bascule AP → ESP_CHEF_JARDIN"); WiFi.mode(WIFI_AP); WiFi.softAP(AP_SSID, AP_PASS); netStatus.modeAP = true; netStatus.wifiConnecte = false; Serial.printf("[WIFI] AP IP: %s\n", WiFi.softAPIP().toString().c_str()); } static void _configurerMDNS() { if (MDNS.begin(MDNS_NOM)) { MDNS.addService("http", "tcp", 80); Serial.printf("[mDNS] Accessible via http://%s.local\n", MDNS_NOM); } } static void _configurerOTA() { ArduinoOTA.setPassword(OTA_PASS); ArduinoOTA.onStart([]() { Serial.println("[OTA] Mise à jour démarrée"); }); ArduinoOTA.onEnd([]() { Serial.println("[OTA] Terminée — redémarrage"); }); ArduinoOTA.onError([](ota_error_t err) { Serial.printf("[OTA] Erreur [%u]\n", err); }); ArduinoOTA.begin(); Serial.println("[OTA] Prêt"); } void network_init() { _demarrerSTA(); } void network_update() { // ── Gestion OTA ───────────────────────────────────────────────── if (netStatus.wifiConnecte && !netStatus.modeAP) { ArduinoOTA.handle(); } // ── Connexion STA en cours ─────────────────────────────────────── if (_connexionEnCours) { if (WiFi.status() == WL_CONNECTED) { _connexionEnCours = false; netStatus.wifiConnecte = true; netStatus.modeAP = false; netStatus.uptimeDemarrage = millis(); Serial.printf("[WIFI] Connecté — IP: %s\n", WiFi.localIP().toString().c_str()); _configurerMDNS(); _configurerOTA(); } else if (millis() - _debutConnexionMs > WIFI_TIMEOUT_MS) { _connexionEnCours = false; Serial.println("[WIFI] Timeout STA"); _demarrerAP(); } return; } // ── STA connecté : surveiller les déconnexions ────────────────── if (netStatus.wifiConnecte && !netStatus.modeAP) { if (WiFi.status() != WL_CONNECTED) { netStatus.wifiConnecte = false; Serial.println("[WIFI] Déconnexion détectée — retry dans 30s"); _dernierRetryMs = millis(); } else { netStatus.rssi = WiFi.RSSI(); } return; } // ── Mode AP : retry STA toutes les 60s ────────────────────────── if (netStatus.modeAP) { if (millis() - _dernierRetryMs > 60000) { Serial.println("[WIFI] Mode AP — retry STA..."); _dernierRetryMs = millis(); _demarrerSTA(); } return; } // ── STA déconnecté (hors AP) : retry toutes les 30s ───────────── if (!netStatus.wifiConnecte && millis() - _dernierRetryMs > WIFI_RETRY_MS) { _dernierRetryMs = millis(); _demarrerSTA(); } } ``` - [ ] **Étape 3 : Intégrer dans main.cpp** ```cpp #include #include "config.h" #include "network.h" SondeConfig sondesConfig[NB_SONDES] = { { "T°C Ext", "maison/jardin/ext/temperature", 60000, 0.2f }, { "T°C Serre", "maison/jardin/serre/temperature", 60000, 0.1f }, { "T°C Sol", "maison/jardin/sol/temperature", 60000, 0.1f }, }; SondeEtat sondesEtat[NB_SONDES] = {}; PointHistorique historique[HIST_TAILLE] = {}; uint16_t histIdx = 0; NetworkStatus netStatus = {}; void setup() { Serial.begin(115200); Serial.println("[BOOT] esp_jardin démarrage..."); network_init(); } void loop() { network_update(); } ``` - [ ] **Étape 4 : Compiler** ```bash pio run ``` Résultat attendu : `SUCCESS`. --- ### Tâche 4 : Module capteurs (sensors.h + sensors.cpp) **Fichiers :** - Créer : `include/sensors.h` - Créer : `src/sensors.cpp` - Modifier : `src/main.cpp` - [ ] **Étape 1 : Créer include/sensors.h** ```cpp #pragma once // Initialise le bus OneWire et les capteurs DallasTemperature void sensors_init(); // À appeler à chaque loop() : acquisition non-bloquante toutes les MESURE_INTERVALLE ms // Retourne true si une nouvelle mesure vient d'être enregistrée bool sensors_update(); ``` - [ ] **Étape 2 : Créer src/sensors.cpp** ```cpp #include "sensors.h" #include "config.h" #include #include static OneWire _oneWire(ONE_WIRE_BUS); static DallasTemperature _sensors(&_oneWire); // États de la machine non-bloquante static uint32_t _derniereMesureMs = 0; static uint32_t _demandeMs = 0; static bool _demandeEnCours = false; // Conversion 12 bits = 750 ms static const uint32_t CONVERSION_MS = 750; void sensors_init() { _sensors.begin(); _sensors.setWaitForConversion(false); // mode non-bloquant uint8_t nb = _sensors.getDeviceCount(); Serial.printf("[SONDES] %u capteur(s) DS18B20 détecté(s) sur GPIO %d\n", nb, ONE_WIRE_BUS); } bool sensors_update() { uint32_t maintenant = millis(); // ── Lancement de la demande de conversion ──────────────────────── if (!_demandeEnCours && maintenant - _derniereMesureMs >= MESURE_INTERVALLE) { _sensors.requestTemperatures(); _demandeMs = maintenant; _demandeEnCours = true; return false; } // ── Attente de la fin de conversion (750 ms) ───────────────────── if (_demandeEnCours && maintenant - _demandeMs >= CONVERSION_MS) { _demandeEnCours = false; _derniereMesureMs = maintenant; // Lecture et validation de chaque sonde for (uint8_t i = 0; i < NB_SONDES; i++) { float t = _sensors.getTempCByIndex(i); // Rejeter les valeurs d'erreur DS18B20 if (t == DEVICE_DISCONNECTED_C || t == 85.0f) { sondesEtat[i].erreur = true; sondesEtat[i].tempActuelle = NAN; Serial.printf("[SONDE %u] ERREUR — valeur rejetée: %.1f\n", i, t); } else { sondesEtat[i].erreur = false; sondesEtat[i].tempActuelle = t; Serial.printf("[SONDE %u] %s = %.1f°C\n", i, sondesConfig[i].nom, t); } } // Enregistrement dans le buffer circulaire historique[histIdx].timestamp = maintenant; for (uint8_t i = 0; i < NB_SONDES; i++) { historique[histIdx].temps[i] = sondesEtat[i].tempActuelle; // NAN si erreur } histIdx = (histIdx + 1) % HIST_TAILLE; return true; // nouvelle mesure disponible } return false; } ``` - [ ] **Étape 3 : Intégrer dans main.cpp** ```cpp #include #include "config.h" #include "network.h" #include "sensors.h" SondeConfig sondesConfig[NB_SONDES] = { { "T°C Ext", "maison/jardin/ext/temperature", 60000, 0.2f }, { "T°C Serre", "maison/jardin/serre/temperature", 60000, 0.1f }, { "T°C Sol", "maison/jardin/sol/temperature", 60000, 0.1f }, }; SondeEtat sondesEtat[NB_SONDES] = {}; PointHistorique historique[HIST_TAILLE] = {}; uint16_t histIdx = 0; NetworkStatus netStatus = {}; void setup() { Serial.begin(115200); Serial.println("[BOOT] esp_jardin démarrage..."); network_init(); sensors_init(); } void loop() { network_update(); sensors_update(); } ``` - [ ] **Étape 4 : Compiler** ```bash pio run ``` Résultat attendu : `SUCCESS`. --- ### Tâche 5 : Module serveur web + WebSocket (web_server.h + web_server.cpp) **Fichiers :** - Créer : `include/web_server.h` - Créer : `src/web_server.cpp` - Modifier : `src/main.cpp` - [ ] **Étape 1 : Créer include/web_server.h** ```cpp #pragma once // Initialise LittleFS, configure les routes REST et le WebSocket void web_server_init(); // Pousse les données temps réel à tous les clients WebSocket connectés void web_server_notify_clients(); ``` - [ ] **Étape 2 : Créer src/web_server.cpp** ```cpp #include "web_server.h" #include "config.h" #include #include #include static AsyncWebServer _server(80); static AsyncWebSocket _ws("/ws"); // ── Construction JSON temps réel ───────────────────────────────────── static String _buildJsonSondes() { JsonDocument doc; JsonArray arr = doc["sondes"].to(); for (uint8_t i = 0; i < NB_SONDES; i++) { JsonObject s = arr.add(); s["nom"] = sondesConfig[i].nom; s["erreur"] = sondesEtat[i].erreur; if (!sondesEtat[i].erreur) { s["temp"] = serialized(String(sondesEtat[i].tempActuelle, 1)); } else { s["temp"] = nullptr; } } doc["uptime"] = (millis() - netStatus.uptimeDemarrage) / 1000; doc["rssi"] = netStatus.rssi; String out; serializeJson(doc, out); return out; } // ── Construction JSON historique ───────────────────────────────────── static String _buildJsonHistory() { JsonDocument doc; JsonArray arr = doc.to(); // Parcourir le buffer dans l'ordre chronologique for (uint16_t i = 0; i < HIST_TAILLE; i++) { uint16_t idx = (histIdx + i) % HIST_TAILLE; if (historique[idx].timestamp == 0) continue; // case vide JsonObject pt = arr.add(); pt["ts"] = historique[idx].timestamp; JsonArray t = pt["t"].to(); for (uint8_t j = 0; j < NB_SONDES; j++) { if (isnan(historique[idx].temps[j])) { t.add(nullptr); } else { t.add(serialized(String(historique[idx].temps[j], 1))); } } } String out; serializeJson(doc, out); return out; } // ── Nettoyage des clients WebSocket déconnectés ────────────────────── static void _onWsEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) { if (type == WS_EVT_CONNECT) { Serial.printf("[WS] Client #%u connecté\n", client->id()); } else if (type == WS_EVT_DISCONNECT) { Serial.printf("[WS] Client #%u déconnecté\n", client->id()); } } void web_server_init() { // Montage LittleFS if (!LittleFS.begin()) { Serial.println("[FS] Erreur montage LittleFS !"); return; } Serial.println("[FS] LittleFS monté"); // WebSocket _ws.onEvent(_onWsEvent); _server.addHandler(&_ws); // ── Routes REST ────────────────────────────────────────────────── // GET /api/status _server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest* req) { JsonDocument doc; doc["rssi"] = netStatus.rssi; doc["uptime"] = (millis() - netStatus.uptimeDemarrage) / 1000; doc["ramLibre"] = ESP.getFreeHeap(); doc["modeAP"] = netStatus.modeAP; doc["mqttConnecte"] = netStatus.mqttConnecte; String out; serializeJson(doc, out); req->send(200, "application/json", out); }); // GET /api/temperatures _server.on("/api/temperatures", HTTP_GET, [](AsyncWebServerRequest* req) { JsonDocument doc; for (uint8_t i = 0; i < NB_SONDES; i++) { String key = "sonde_" + String(i + 1); if (!sondesEtat[i].erreur) { doc[key] = serialized(String(sondesEtat[i].tempActuelle, 1)); } else { doc[key] = nullptr; } } doc["unit"] = "C"; String out; serializeJson(doc, out); req->send(200, "application/json", out); }); // GET /api/history _server.on("/api/history", HTTP_GET, [](AsyncWebServerRequest* req) { req->send(200, "application/json", _buildJsonHistory()); }); // POST /api/config _server.on("/api/config", HTTP_POST, [](AsyncWebServerRequest* req) {}, nullptr, [](AsyncWebServerRequest* req, uint8_t* data, size_t len, size_t, size_t) { JsonDocument doc; DeserializationError err = deserializeJson(doc, data, len); if (err) { req->send(400, "application/json", "{\"erreur\":\"JSON invalide\"}"); return; } // Application immédiate en RAM (non persistant au redémarrage) if (doc["intervalleMs"].is()) { // MESURE_INTERVALLE est une constante — pour rendre dynamique, // utiliser une variable globale dans config.h si besoin en V2 Serial.printf("[CONFIG] intervalleMs reçu: %u\n", (uint32_t)doc["intervalleMs"]); } req->send(200, "application/json", "{\"ok\":true}"); } ); // Servir index.html depuis LittleFS _server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html"); _server.begin(); Serial.println("[HTTP] Serveur web démarré sur port 80"); } void web_server_notify_clients() { if (_ws.count() == 0) return; _ws.cleanupClients(); _ws.textAll(_buildJsonSondes()); } ``` - [ ] **Étape 3 : Intégrer dans main.cpp** ```cpp #include #include "config.h" #include "network.h" #include "sensors.h" #include "web_server.h" SondeConfig sondesConfig[NB_SONDES] = { { "T°C Ext", "maison/jardin/ext/temperature", 60000, 0.2f }, { "T°C Serre", "maison/jardin/serre/temperature", 60000, 0.1f }, { "T°C Sol", "maison/jardin/sol/temperature", 60000, 0.1f }, }; SondeEtat sondesEtat[NB_SONDES] = {}; PointHistorique historique[HIST_TAILLE] = {}; uint16_t histIdx = 0; NetworkStatus netStatus = {}; void setup() { Serial.begin(115200); Serial.println("[BOOT] esp_jardin démarrage..."); network_init(); sensors_init(); web_server_init(); } void loop() { network_update(); bool nouvelleMesure = sensors_update(); if (nouvelleMesure) { web_server_notify_clients(); } } ``` - [ ] **Étape 4 : Compiler** ```bash pio run ``` Résultat attendu : `SUCCESS`. Note : ArduinoJson est une dépendance indirecte d'ESPAsyncWebServer — si la compilation échoue sur ArduinoJson, ajouter `bblanchon/ArduinoJson @ ^7` dans `lib_deps`. --- ### Tâche 6 : Module MQTT (mqtt_manager.h + mqtt_manager.cpp) **Fichiers :** - Créer : `include/mqtt_manager.h` - Créer : `src/mqtt_manager.cpp` - Modifier : `src/main.cpp` - [ ] **Étape 1 : Créer include/mqtt_manager.h** ```cpp #pragma once // Initialise le client MQTT void mqtt_init(); // À appeler à chaque loop() : reconnexion non-bloquante + publication deadband void mqtt_update(); ``` - [ ] **Étape 2 : Créer src/mqtt_manager.cpp** ```cpp #include "mqtt_manager.h" #include "config.h" #include #include static WiFiClient _wifiClient; static PubSubClient _mqtt(_wifiClient); static uint32_t _dernierRetryMs = 0; static bool _connecter() { Serial.printf("[MQTT] Connexion → %s:%d\n", MQTT_BROKER, MQTT_PORT); bool ok; if (strlen(MQTT_USER) > 0) { ok = _mqtt.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASS); } else { ok = _mqtt.connect(MQTT_CLIENT_ID); } if (ok) { Serial.println("[MQTT] Connecté"); netStatus.mqttConnecte = true; } else { Serial.printf("[MQTT] Échec, état=%d\n", _mqtt.state()); netStatus.mqttConnecte = false; } return ok; } void mqtt_init() { _mqtt.setServer(MQTT_BROKER, MQTT_PORT); } void mqtt_update() { // Pas de WiFi STA = pas de MQTT if (!netStatus.wifiConnecte || netStatus.modeAP) return; // Reconnexion non-bloquante if (!_mqtt.connected()) { netStatus.mqttConnecte = false; if (millis() - _dernierRetryMs > MQTT_RETRY_MS) { _dernierRetryMs = millis(); _connecter(); } return; } _mqtt.loop(); // Publication par sonde avec filtre deadband for (uint8_t i = 0; i < NB_SONDES; i++) { if (sondesEtat[i].erreur) continue; // jamais de valeur d'erreur en MQTT float delta = fabsf(sondesEtat[i].tempActuelle - sondesEtat[i].dernierPublie); uint32_t ecart = millis() - sondesEtat[i].dernierPubliMs; bool deadbandOk = delta >= sondesConfig[i].deadband; bool intervalleOk = ecart >= sondesConfig[i].intervalleMs; if (deadbandOk || intervalleOk) { char payload[8]; snprintf(payload, sizeof(payload), "%.1f", sondesEtat[i].tempActuelle); bool ok = _mqtt.publish(sondesConfig[i].topic, payload, true); // retain=true if (ok) { sondesEtat[i].dernierPublie = sondesEtat[i].tempActuelle; sondesEtat[i].dernierPubliMs = millis(); Serial.printf("[MQTT] %s → %s °C\n", sondesConfig[i].topic, payload); } } } } ``` - [ ] **Étape 3 : Intégrer dans main.cpp (version finale)** ```cpp #include #include "config.h" #include "network.h" #include "sensors.h" #include "web_server.h" #include "mqtt_manager.h" SondeConfig sondesConfig[NB_SONDES] = { { "T°C Ext", "maison/jardin/ext/temperature", 60000, 0.2f }, { "T°C Serre", "maison/jardin/serre/temperature", 60000, 0.1f }, { "T°C Sol", "maison/jardin/sol/temperature", 60000, 0.1f }, }; SondeEtat sondesEtat[NB_SONDES] = {}; PointHistorique historique[HIST_TAILLE] = {}; uint16_t histIdx = 0; NetworkStatus netStatus = {}; void setup() { Serial.begin(115200); delay(500); // unique delay autorisé : laisser le port série s'ouvrir au boot Serial.println("\n[BOOT] esp_jardin v1.0 — démarrage..."); network_init(); sensors_init(); web_server_init(); mqtt_init(); Serial.println("[BOOT] Initialisation terminée"); } void loop() { network_update(); bool nouvelleMesure = sensors_update(); if (nouvelleMesure) { web_server_notify_clients(); } mqtt_update(); } ``` - [ ] **Étape 4 : Compilation finale du firmware** ```bash pio run ``` Résultat attendu : `SUCCESS` — firmware C++ complet compilé. --- ## Phase 2 — Interface web --- ### Tâche 7 : Créer data/index.html **Fichiers :** - Créer : `data/index.html` La page est auto-contenue (HTML + CSS + JS inline) et embarquée dans LittleFS. Elle utilise le design system Gruvbox Seventies via des variables CSS définies localement (pas de CDN pour les tokens — trop lent en réseau local IoT). Chart.js est chargé via CDN. - [ ] **Étape 1 : Créer data/index.html** ```html ESP Jardin — Monitoring
ESP_JARDIN
Station de monitoring
Connexion... |
Historique 24 heures
Statut système
WiFi
IP
RSSI
MQTT
RAM libre
Uptime
Configuration
Intervalle mesure (ms)
Broker MQTT
Port MQTT
MQTT
— sondes
esp_jardin.local
``` - [ ] **Étape 2 : Flasher le filesystem** ```bash pio run -t uploadfs ``` Résultat attendu : `SUCCESS` — `index.html` uploadé dans LittleFS. --- ### Tâche 8 : Flash complet et validation série **Cette tâche nécessite le hardware.** - [ ] **Étape 1 : Flash du firmware** ```bash pio run -t upload ``` - [ ] **Étape 2 : Ouvrir le moniteur série** ```bash pio device monitor ``` - [ ] **Étape 3 : Vérifier la séquence de boot attendue** ``` [BOOT] esp_jardin v1.0 — démarrage... [WIFI] Connexion STA → SSID: Mon_Reseau_WiFi [WIFI] Connecté — IP: 10.0.0.42 [mDNS] Accessible via http://esp_jardin.local [OTA] Prêt [FS] LittleFS monté [HTTP] Serveur web démarré sur port 80 [SONDES] 3 capteur(s) DS18B20 détecté(s) sur GPIO 4 [BOOT] Initialisation terminée ``` - [ ] **Étape 4 : Vérifier les acquisitions (après 10s)** ``` [SONDE 0] T°C Ext = 19.3°C [SONDE 1] T°C Serre = 28.7°C [SONDE 2] T°C Sol = 12.1°C [WS] Client #1 connecté ``` - [ ] **Étape 5 : Tester l'API REST** ```bash curl http://esp_jardin.local/api/status curl http://esp_jardin.local/api/temperatures curl http://esp_jardin.local/api/history | head -c 200 ``` - [ ] **Étape 6 : Tester l'interface web** Ouvrir `http://esp_jardin.local` dans un navigateur. Vérifier : - Les 3 cartes sondes affichent les températures - Le graphique se construit au fil des acquisitions - Le panneau droit affiche WiFi / MQTT / RAM / uptime - [ ] **Étape 7 : Simuler une erreur sonde** Débrancher une sonde physique. Le log série doit afficher : ``` [SONDE 2] ERREUR — valeur rejetée: -127.0 ``` L'interface web doit afficher la carte en rouge avec "Sonde déconnectée". --- ## Protocole de validation de robustesse (post-déploiement) ### Test WiFi Éteindre le routeur WiFi pendant 60s. L'ESP doit basculer en mode AP (`ESP_CHEF_JARDIN`), rester accessible, puis se reconnecter automatiquement au retour du WiFi. Aucun crash, aucun blocage. ### Test MQTT Arrêter le broker Mosquitto (`sudo systemctl stop mosquitto`). Le log doit afficher des tentatives de reconnexion toutes les 5s sans bloquer le WebSocket ni les acquisitions. Redémarrer Mosquitto → reconnexion automatique. ### Test OTA ```bash pio run -t upload --upload-port 10.0.0.42 ``` Le firmware doit se mettre à jour sans liaison USB.