From b5edbe66583fd0152ad714a90b8f860780a399cd Mon Sep 17 00:00:00 2001 From: M-Factory Date: Mon, 3 Nov 2025 18:31:15 +0900 Subject: [PATCH] Display total runtime in Web UI --- ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino | 36 +++++++++++-- ESPTimeCast_ESP32/data/index.html | 56 +++++++++++++++++++++ ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino | 33 +++++++++++- ESPTimeCast_ESP8266/data/index.html | 56 +++++++++++++++++++++ 4 files changed, 175 insertions(+), 6 deletions(-) diff --git a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino index bfcf4cf..fc87c63 100644 --- a/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino +++ b/ESPTimeCast_ESP32/ESPTimeCast_ESP32.ino @@ -1102,6 +1102,26 @@ void setupWebServer() { request->send(200, "application/json", "{\"ok\":true}"); }); + server.on("/uptime", HTTP_GET, [](AsyncWebServerRequest *request) { + if (!LittleFS.exists("/uptime.dat")) { + request->send(200, "text/plain", "No uptime recorded yet."); + return; + } + + File f = LittleFS.open("/uptime.dat", "r"); + if (!f) { + request->send(500, "text/plain", "Error reading uptime file."); + return; + } + + String content = f.readString(); + f.close(); + + unsigned long seconds = content.toInt(); + String formatted = formatUptime(seconds); + request->send(200, "text/plain", formatted); + }); + server.on("/generate_204", HTTP_GET, handleCaptivePortal); // Android server.on("/fwlink", HTTP_GET, handleCaptivePortal); // Windows server.on("/hotspot-detect.html", HTTP_GET, handleCaptivePortal); // iOS/macOS @@ -1111,13 +1131,21 @@ void setupWebServer() { } void handleCaptivePortal(AsyncWebServerRequest *request) { - Serial.print(F("[WEBSERVER] Captive Portal Redirecting: ")); + Serial.print(F("[WEBSERVER] Captive Portal triggered for URL: ")); Serial.println(request->url()); - request->redirect(String("http://") + WiFi.softAPIP().toString() + "/"); + + if (isAPMode) { + IPAddress apIP = WiFi.softAPIP(); + String redirectUrl = "http://" + apIP.toString() + "/"; + Serial.print(F("[WEBSERVER] Redirecting to captive portal: ")); + Serial.println(redirectUrl); + request->redirect(redirectUrl); + } else { + Serial.println(F("[WEBSERVER] Not in AP mode — sending 404")); + request->send(404, "text/plain", "Not found"); + } } - - String normalizeWeatherDescription(String str) { // Serbian Cyrillic → Latin str.replace("а", "a"); diff --git a/ESPTimeCast_ESP32/data/index.html b/ESPTimeCast_ESP32/data/index.html index 99d6893..259b5cb 100644 --- a/ESPTimeCast_ESP32/data/index.html +++ b/ESPTimeCast_ESP32/data/index.html @@ -630,6 +630,7 @@ textarea::placeholder {
@@ -1355,6 +1356,61 @@ apiInput.addEventListener('blur', () => { } } }); + +//Uptime + +let uptimeSeconds = 0; +let uptimeTimer; + +// Fetch uptime from ESP +function fetchUptime() { + fetch('/uptime') + .then(res => res.text()) + .then(text => { + //console.log('Uptime response:', text); + uptimeSeconds = parseUptimeToSeconds(text); + updateUptimeDisplay(); + if (uptimeTimer) clearInterval(uptimeTimer); + uptimeTimer = setInterval(() => { + uptimeSeconds++; + updateUptimeDisplay(); + }, 1000); + }) + .catch(err => console.error('Error fetching /uptime:', err)); +} + +// Convert "14:56:54" or "1 day 3:12:33" → total seconds +function parseUptimeToSeconds(text) { + let days = 0, h = 0, m = 0, s = 0; + const dayMatch = text.match(/(\d+)\s*day/); + if (dayMatch) days = parseInt(dayMatch[1]); + const timeMatch = text.match(/(\d+):(\d+):(\d+)/); + if (timeMatch) { + h = parseInt(timeMatch[1]); + m = parseInt(timeMatch[2]); + s = parseInt(timeMatch[3]); + } + return days * 86400 + h * 3600 + m * 60 + s; +} + +// Format seconds → same "D days HH:MM:SS" style +function formatUptime(seconds) { + const days = Math.floor(seconds / 86400); + seconds %= 86400; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return `${days > 0 ? days + ' day' + (days > 1 ? 's ' : ' ') : ''}${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; +} + +// Update the text on screen +function updateUptimeDisplay() { + document.getElementById('uptimeDisplay').textContent = formatUptime(uptimeSeconds); +} + +// Start it up +fetchUptime(); + \ No newline at end of file diff --git a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino index 1e92b9a..d2f4c4b 100644 --- a/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino +++ b/ESPTimeCast_ESP8266/ESPTimeCast_ESP8266.ino @@ -1105,6 +1105,25 @@ void setupWebServer() { request->send(200, "application/json", "{\"ok\":true}"); }); + server.on("/uptime", HTTP_GET, [](AsyncWebServerRequest *request) { + if (!LittleFS.exists("/uptime.dat")) { + request->send(200, "text/plain", "No uptime recorded yet."); + return; + } + + File f = LittleFS.open("/uptime.dat", "r"); + if (!f) { + request->send(500, "text/plain", "Error reading uptime file."); + return; + } + + String content = f.readString(); + + unsigned long seconds = content.toInt(); + String formatted = formatUptime(seconds); + request->send(200, "text/plain", formatted); + }); + server.on("/generate_204", HTTP_GET, handleCaptivePortal); // Android server.on("/fwlink", HTTP_GET, handleCaptivePortal); // Windows server.on("/hotspot-detect.html", HTTP_GET, handleCaptivePortal); // iOS/macOS @@ -1114,9 +1133,19 @@ void setupWebServer() { } void handleCaptivePortal(AsyncWebServerRequest *request) { - Serial.print(F("[WEBSERVER] Captive Portal Redirecting: ")); + Serial.print(F("[WEBSERVER] Captive Portal triggered for URL: ")); Serial.println(request->url()); - request->redirect(String("http://") + WiFi.softAPIP().toString() + "/"); + + if (isAPMode) { + IPAddress apIP = WiFi.softAPIP(); + String redirectUrl = "http://" + apIP.toString() + "/"; + Serial.print(F("[WEBSERVER] Redirecting to captive portal: ")); + Serial.println(redirectUrl); + request->redirect(redirectUrl); + } else { + Serial.println(F("[WEBSERVER] Not in AP mode — sending 404")); + request->send(404, "text/plain", "Not found"); + } } String normalizeWeatherDescription(String str) { diff --git a/ESPTimeCast_ESP8266/data/index.html b/ESPTimeCast_ESP8266/data/index.html index 99d6893..259b5cb 100644 --- a/ESPTimeCast_ESP8266/data/index.html +++ b/ESPTimeCast_ESP8266/data/index.html @@ -630,6 +630,7 @@ textarea::placeholder {
@@ -1355,6 +1356,61 @@ apiInput.addEventListener('blur', () => { } } }); + +//Uptime + +let uptimeSeconds = 0; +let uptimeTimer; + +// Fetch uptime from ESP +function fetchUptime() { + fetch('/uptime') + .then(res => res.text()) + .then(text => { + //console.log('Uptime response:', text); + uptimeSeconds = parseUptimeToSeconds(text); + updateUptimeDisplay(); + if (uptimeTimer) clearInterval(uptimeTimer); + uptimeTimer = setInterval(() => { + uptimeSeconds++; + updateUptimeDisplay(); + }, 1000); + }) + .catch(err => console.error('Error fetching /uptime:', err)); +} + +// Convert "14:56:54" or "1 day 3:12:33" → total seconds +function parseUptimeToSeconds(text) { + let days = 0, h = 0, m = 0, s = 0; + const dayMatch = text.match(/(\d+)\s*day/); + if (dayMatch) days = parseInt(dayMatch[1]); + const timeMatch = text.match(/(\d+):(\d+):(\d+)/); + if (timeMatch) { + h = parseInt(timeMatch[1]); + m = parseInt(timeMatch[2]); + s = parseInt(timeMatch[3]); + } + return days * 86400 + h * 3600 + m * 60 + s; +} + +// Format seconds → same "D days HH:MM:SS" style +function formatUptime(seconds) { + const days = Math.floor(seconds / 86400); + seconds %= 86400; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + return `${days > 0 ? days + ' day' + (days > 1 ? 's ' : ' ') : ''}${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; +} + +// Update the text on screen +function updateUptimeDisplay() { + document.getElementById('uptimeDisplay').textContent = formatUptime(uptimeSeconds); +} + +// Start it up +fetchUptime(); + \ No newline at end of file