From 3d21c8dc781f637544b41e4b9180cbbee438b3a9 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Sat, 23 May 2026 16:31:48 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20serveur=20web=20HTTP=20+=20WebSocket,?= =?UTF-8?q?=20API=20REST=20compl=C3=A8te=20(status/temperatures/history/co?= =?UTF-8?q?nfig)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- include/web_server.h | 7 +++ src/main.cpp | 7 ++- src/web_server.cpp | 137 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 include/web_server.h create mode 100644 src/web_server.cpp diff --git a/include/web_server.h b/include/web_server.h new file mode 100644 index 0000000..80af379 --- /dev/null +++ b/include/web_server.h @@ -0,0 +1,7 @@ +#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(); diff --git a/src/main.cpp b/src/main.cpp index edcb79d..f37d8d4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,7 @@ #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 }, @@ -18,9 +19,13 @@ void setup() { Serial.println("[BOOT] esp_jardin démarrage..."); network_init(); sensors_init(); + web_server_init(); } void loop() { network_update(); - sensors_update(); + bool nouvelleMesure = sensors_update(); + if (nouvelleMesure) { + web_server_notify_clients(); + } } diff --git a/src/web_server.cpp b/src/web_server.cpp new file mode 100644 index 0000000..3ee0070 --- /dev/null +++ b/src/web_server.cpp @@ -0,0 +1,137 @@ +#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(); + for (uint16_t i = 0; i < HIST_TAILLE; i++) { + uint16_t idx = (histIdx + i) % HIST_TAILLE; + if (historique[idx].timestamp == 0) continue; + 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; +} + +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() { + if (!LittleFS.begin()) { + Serial.println("[FS] Erreur montage LittleFS !"); + return; + } + Serial.println("[FS] LittleFS monté"); + + _ws.onEvent(_onWsEvent); + _server.addHandler(&_ws); + + // 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 — body: {"intervalleMs":10000,"mqttBroker":"10.0.0.3","mqttPort":1883} + _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; + } + if (doc["intervalleMs"].is()) { + Serial.printf("[CONFIG] intervalleMs: %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()); +}