Display total runtime in Web UI

This commit is contained in:
M-Factory
2025-11-03 18:31:15 +09:00
parent a3b60d4868
commit b5edbe6658
4 changed files with 175 additions and 6 deletions

View File

@@ -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");

View File

@@ -630,6 +630,7 @@ textarea::placeholder {
<div class="footer">
Project by: <a href="https://www.instagram.com/mfactory.osaka" target="_blank" rel="noopener noreferrer">M-Factory</a><br>
Device uptime: <span id="uptimeDisplay">Loading...</span>
</div>
<div id="savingMessage"></div>
@@ -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();
</script>
</body>
</html>

View File

@@ -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) {

View File

@@ -630,6 +630,7 @@ textarea::placeholder {
<div class="footer">
Project by: <a href="https://www.instagram.com/mfactory.osaka" target="_blank" rel="noopener noreferrer">M-Factory</a><br>
Device uptime: <span id="uptimeDisplay">Loading...</span>
</div>
<div id="savingMessage"></div>
@@ -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();
</script>
</body>
</html>