From 9ce3499c79fc20b426f4743ed2e338590facfae0 Mon Sep 17 00:00:00 2001 From: totolouis Date: Wed, 15 Apr 2026 09:16:33 +0200 Subject: [PATCH] Add LED feedback, captive portal, and status page - LED blink patterns indicate WiFi/MQTT connection state - DNS captive portal when in WiFi AP mode - Status page on webserver --- include/net/webserver.h | 128 ++++++++++++++++++++++++++++++++-------- src/main.cpp | 51 ++++++++++++++++ src/net/webserver.cpp | 64 ++++++++++++++++++++ src/net/wifi.cpp | 5 ++ 4 files changed, 225 insertions(+), 23 deletions(-) diff --git a/include/net/webserver.h b/include/net/webserver.h index 0dcba99..c85a834 100644 --- a/include/net/webserver.h +++ b/include/net/webserver.h @@ -17,8 +17,9 @@ const char html_config_form[] PROGMEM = R"rawliteral( - Yokis-Hack configuration page + Yokis-Hack - -

Yokis-Hack configuration page

+ +

Yokis-Hack

-
+
+

Status

+
WiFi:...
+
IP:...
+
Signal:...
+
MQTT:...
+
Uptime:...
+
Free memory:...
+
+ +
+

Devices

+ + + +
NameTypeStatusAvailability
Loading...
+
+ +

WiFi

diff --git a/src/main.cpp b/src/main.cpp index 5b36b77..dc3cdd6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,6 +12,7 @@ #if defined(ESP8266) || defined(ESP32) #include +#include #include #include #include @@ -70,6 +71,45 @@ WebServer webserver(80); Ticker* g_deviceStatusPollers[MAX_YOKIS_DEVICES_NUM]; +#if WIFI_ENABLED +DNSServer dnsServer; +bool g_apMode = false; +#endif + +// LED feedback: blink patterns based on device state +// LED is inverted: LOW = ON, HIGH = OFF +void updateStatusLED() { + static unsigned long lastToggle = 0; + static bool ledState = false; + unsigned long now = millis(); + unsigned long interval; + + #if WIFI_ENABLED + if (WiFi.status() != WL_CONNECTED) { + interval = 200; // Fast blink: no WiFi + } + #if MQTT_ENABLED + else if (!g_mqtt->connected()) { + interval = 1000; // Slow blink: WiFi OK, no MQTT + } + #endif + else { + // All connected: LED off + digitalWrite(STATUS_LED, HIGH); + return; + } + #else + digitalWrite(STATUS_LED, HIGH); + return; + #endif + + if (now - lastToggle >= interval) { + lastToggle = now; + ledState = !ledState; + digitalWrite(STATUS_LED, ledState ? LOW : HIGH); + } +} + // polling void pollForStatus(Device* device); @@ -122,6 +162,10 @@ void setup() { #if WEBSERVER_ENABLED // Starting webserver webserver.begin(); + // Start captive portal DNS if in AP mode + if (g_apMode) { + dnsServer.start(53, "*", WiFi.softAPIP()); + } #endif #if MQTT_ENABLED @@ -179,6 +223,13 @@ void loop() { #if defined(ESP8266) || defined(ESP32) LOG.handle(); // telnetspy handling ArduinoOTA.handle(); + updateStatusLED(); + + #if WIFI_ENABLED && WEBSERVER_ENABLED + if (g_apMode) { + dnsServer.processNextRequest(); + } + #endif #if MQTT_ENABLED g_mqtt->loop(); diff --git a/src/net/webserver.cpp b/src/net/webserver.cpp index 8d376f6..7a68dc3 100644 --- a/src/net/webserver.cpp +++ b/src/net/webserver.cpp @@ -1,6 +1,7 @@ #if WIFI_ENABLED && (defined(ESP8266) || defined(ESP32)) && WEBSERVER_ENABLED #include "net/webserver.h" #include "globals.h" +#include "RF/device.h" WebServer::WebServer(uint16_t port) : AsyncWebServer(port) { this->on("/", HTTP_GET, [](AsyncWebServerRequest* request) { @@ -81,6 +82,69 @@ WebServer::WebServer(uint16_t port) : AsyncWebServer(port) { request->redirect("/?message=Configuration saved successfully"); }); + + // JSON API for status dashboard + this->on("/api/status", HTTP_GET, [](AsyncWebServerRequest* request) { + String json = "{"; + + // WiFi status + json += "\"wifi\":{"; + json += "\"connected\":"; + json += (WiFi.status() == WL_CONNECTED) ? "true" : "false"; + json += ",\"ssid\":\"" + WiFi.SSID() + "\""; + json += ",\"ip\":\"" + WiFi.localIP().toString() + "\""; + json += ",\"rssi\":" + String(WiFi.RSSI()); + json += "}"; + + // MQTT status + #if MQTT_ENABLED + json += ",\"mqtt\":{"; + json += "\"connected\":"; + json += g_mqtt->connected() ? "true" : "false"; + json += ",\"configured\":"; + json += g_mqtt->MqttConfig::isEmpty() ? "false" : "true"; + json += ",\"host\":\""; + json += g_mqtt->getHost(); + json += "\",\"port\":"; + json += String(g_mqtt->getPort()); + json += "}"; + #else + json += ",\"mqtt\":{\"connected\":false,\"configured\":false,\"host\":\"\",\"port\":0}"; + #endif + + // Uptime & heap + json += ",\"uptime\":" + String(millis() / 1000); + #if defined(ESP8266) + json += ",\"heap\":" + String(ESP.getFreeHeap()); + #elif defined(ESP32) + json += ",\"heap\":" + String(ESP.getFreeHeap()); + #endif + + // Devices + json += ",\"devices\":["; + for (uint8_t i = 0; i < g_nb_devices; i++) { + if (g_devices[i] != NULL) { + if (i > 0) json += ","; + json += "{\"name\":\""; + json += g_devices[i]->getName(); + json += "\",\"mode\":\""; + json += Device::getModeAsString(g_devices[i]->getMode()); + json += "\",\"status\":\""; + json += Device::getStatusAsString(g_devices[i]->getStatus()); + json += "\",\"availability\":\""; + json += Device::getAvailabilityAsString(g_devices[i]->getAvailability()); + json += "\"}"; + } + } + json += "]}"; + + request->send(200, "application/json", json); + }); + + // Captive portal: redirect all unknown requests to the config page + this->onNotFound([](AsyncWebServerRequest* request) { + request->redirect("/"); + }); } WebServer::~WebServer() {} diff --git a/src/net/wifi.cpp b/src/net/wifi.cpp index c44666a..6e71093 100644 --- a/src/net/wifi.cpp +++ b/src/net/wifi.cpp @@ -2,6 +2,8 @@ #include "net/wifi.h" #include "globals.h" +extern bool g_apMode; + void setupWifi(String ssid, String password) { // Configuration changed if (strcmp(WiFi.SSID().c_str(), ssid.c_str()) != 0 || @@ -100,10 +102,13 @@ void setupWifiAP() { WiFi.waitForConnectResult(); WiFi.persistent(false); + g_apMode = true; + LOG.print("WiFi AP mode started. SSID: "); LOG.println(ssid); LOG.print("YokisHack IP: "); LOG.println(WiFi.softAPIP()); + LOG.println("Captive portal active - connect to AP and a browser should open automatically."); } bool resetWifiConfig() {