Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 105 additions & 23 deletions include/net/webserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ const char html_config_form[] PROGMEM = R"rawliteral(
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Yokis-Hack configuration page</title>
<title>Yokis-Hack</title>
<style>
body { font-family: Arial, sans-serif; max-width: 700px; margin: 0 auto; padding: 10px; }
input[type=text], input[type=password], select {
width: 100%%;
padding: 12px 20px;
Expand All @@ -28,7 +29,6 @@ const char html_config_form[] PROGMEM = R"rawliteral(
border-radius: 4px;
box-sizing: border-box;
}

input[type=submit] {
width: 100%%;
background-color: #4CAF50;
Expand All @@ -39,18 +39,13 @@ const char html_config_form[] PROGMEM = R"rawliteral(
border-radius: 4px;
cursor: pointer;
}

input[type=submit]:hover {
background-color: #45a049;
}

div {
input[type=submit]:hover { background-color: #45a049; }
.section {
border-radius: 5px;
background-color: #f2f2f2;
padding: 20px;
margin-bottom: 15px;
}

/* The alert message box */
.alert {
padding: 20px;
background-color: #f44336;
Expand All @@ -59,12 +54,9 @@ const char html_config_form[] PROGMEM = R"rawliteral(
transition: opacity 0.6s;
margin-bottom: 15px;
}

.alert.success {background-color: #4CAF50;}
.alert.info {background-color: #2196F3;}
.alert.warning {background-color: #ff9800;}

/* The close button */
.alert.success { background-color: #4CAF50; }
.alert.info { background-color: #2196F3; }
.alert.warning { background-color: #ff9800; }
.closebtn {
margin-left: 15px;
color: white;
Expand All @@ -75,11 +67,26 @@ const char html_config_form[] PROGMEM = R"rawliteral(
cursor: pointer;
transition: 0.3s;
}

/* When moving the mouse over the close button */
.closebtn:hover {
color: black;
.closebtn:hover { color: black; }
.status-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #ddd; }
.status-row:last-child { border-bottom: none; }
.status-label { font-weight: bold; }
.badge {
display: inline-block;
padding: 2px 10px;
border-radius: 12px;
color: white;
font-size: 0.85em;
}
.badge-ok { background-color: #4CAF50; }
.badge-err { background-color: #f44336; }
.badge-warn { background-color: #ff9800; }
table { width: 100%%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }
th { background-color: #4CAF50; color: white; }
tr:hover { background-color: #e9e9e9; }
.signal-bar { display: inline-block; width: 4px; margin-right: 1px; background: #ccc; vertical-align: bottom; }
.signal-bar.active { background: #4CAF50; }
</style>
<script>
function getMessageBox() {
Expand All @@ -89,14 +96,89 @@ const char html_config_form[] PROGMEM = R"rawliteral(
document.getElementById("message").innerHTML = "<div class=\"alert success\"><span class=\"closebtn\" onclick=\"this.parentElement.style.display='none';\">&times;</span>" + message + "</div>";
}
}
function signalBars(rssi) {
var bars = 0;
if (rssi > -50) bars = 4;
else if (rssi > -60) bars = 3;
else if (rssi > -70) bars = 2;
else if (rssi > -80) bars = 1;
var html = '';
for (var i = 1; i <= 4; i++) {
html += '<span class="signal-bar' + (i <= bars ? ' active' : '') + '" style="height:' + (i*5) + 'px"></span>';
}
return html + ' ' + rssi + ' dBm';
}
function refreshStatus() {
fetch('/api/status').then(function(r){return r.json()}).then(function(d) {
var ws = document.getElementById('wifi-status');
if (d.wifi.connected) {
ws.innerHTML = '<span class="badge badge-ok">Connected</span> to ' + d.wifi.ssid;
document.getElementById('wifi-ip').innerHTML = d.wifi.ip;
document.getElementById('wifi-rssi').innerHTML = signalBars(d.wifi.rssi);
} else {
ws.innerHTML = '<span class="badge badge-err">Disconnected</span>';
document.getElementById('wifi-ip').innerHTML = '-';
document.getElementById('wifi-rssi').innerHTML = '-';
}
var ms = document.getElementById('mqtt-status');
if (d.mqtt.connected) {
ms.innerHTML = '<span class="badge badge-ok">Connected</span> to ' + d.mqtt.host + ':' + d.mqtt.port;
} else if (d.mqtt.configured) {
ms.innerHTML = '<span class="badge badge-err">Disconnected</span>';
} else {
ms.innerHTML = '<span class="badge badge-warn">Not configured</span>';
}
document.getElementById('uptime').innerHTML = formatUptime(d.uptime);
document.getElementById('heap').innerHTML = d.heap + ' bytes';
var dt = document.getElementById('device-tbody');
if (d.devices.length === 0) {
dt.innerHTML = '<tr><td colspan="4" style="text-align:center">No devices paired</td></tr>';
} else {
var h = '';
for (var i = 0; i < d.devices.length; i++) {
var dev = d.devices[i];
var avail = dev.availability === 'Online'
? '<span class="badge badge-ok">Online</span>'
: '<span class="badge badge-err">Offline</span>';
h += '<tr><td>' + dev.name + '</td><td>' + dev.mode + '</td><td>' + dev.status + '</td><td>' + avail + '</td></tr>';
}
dt.innerHTML = h;
}
}).catch(function(){});
}
function formatUptime(s) {
var d = Math.floor(s / 86400);
var h = Math.floor((s %% 86400) / 3600);
var m = Math.floor((s %% 3600) / 60);
return (d > 0 ? d + 'd ' : '') + h + 'h ' + m + 'm';
}
window.onload = function() { getMessageBox(); refreshStatus(); setInterval(refreshStatus, 5000); };
</script>
</head>
<body onload="getMessageBox()">
<h1>Yokis-Hack configuration page</h1>
<body>
<h1>Yokis-Hack</h1>

<div id="message"></div>

<div>
<div class="section">
<h2>Status</h2>
<div class="status-row"><span class="status-label">WiFi:</span><span id="wifi-status">...</span></div>
<div class="status-row"><span class="status-label">IP:</span><span id="wifi-ip">...</span></div>
<div class="status-row"><span class="status-label">Signal:</span><span id="wifi-rssi">...</span></div>
<div class="status-row"><span class="status-label">MQTT:</span><span id="mqtt-status">...</span></div>
<div class="status-row"><span class="status-label">Uptime:</span><span id="uptime">...</span></div>
<div class="status-row"><span class="status-label">Free memory:</span><span id="heap">...</span></div>
</div>

<div class="section">
<h2>Devices</h2>
<table>
<thead><tr><th>Name</th><th>Type</th><th>Status</th><th>Availability</th></tr></thead>
<tbody id="device-tbody"><tr><td colspan="4" style="text-align:center">Loading...</td></tr></tbody>
</table>
</div>

<div class="section">
<form action="/save_config">

<h2>WiFi</h2>
Expand Down
51 changes: 51 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#if defined(ESP8266) || defined(ESP32)
#include <ArduinoOTA.h>
#include <DNSServer.h>
Copy link
Copy Markdown
Owner

@nmaupu nmaupu May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure to understand what's used for.
It adds a dependency (not declared btw) and make the binary bigger.

#include <PubSubClient.h>
#include <Ticker.h>
#include <WiFiUdp.h>
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
64 changes: 64 additions & 0 deletions src/net/webserver.cpp
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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() {}
Expand Down
5 changes: 5 additions & 0 deletions src/net/wifi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down Expand Up @@ -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() {
Expand Down