[ai] fixed web server issues, still some quirky behaviour. More robust.

This commit is contained in:
Stefan Ostermann 2025-08-18 00:20:59 +02:00
parent b820f3fc8d
commit 1144b95349
4 changed files with 211 additions and 129 deletions

View File

@ -114,7 +114,11 @@ bool SDActive = false;
bool RFIDActive = false; bool RFIDActive = false;
bool webrequestActive = false;
// Web request concurrency counter and helpers (atomic via GCC builtins)
volatile uint32_t webreq_cnt = 0;
static inline void webreq_enter() { __sync_add_and_fetch(&webreq_cnt, 1); }
static inline void webreq_exit() { __sync_sub_and_fetch(&webreq_cnt, 1); }
uint16_t voltage_threshold_counter = 0; uint16_t voltage_threshold_counter = 0;
@ -319,6 +323,8 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
void handleMoveFile(AsyncWebServerRequest *request) void handleMoveFile(AsyncWebServerRequest *request)
{ {
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
String from = request->arg("from"); String from = request->arg("from");
String to = request->arg("to"); String to = request->arg("to");
@ -339,6 +345,8 @@ void handleMoveFile(AsyncWebServerRequest *request)
void handleDeleteFile(AsyncWebServerRequest *request) void handleDeleteFile(AsyncWebServerRequest *request)
{ {
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
String filename = request->arg("filename"); String filename = request->arg("filename");
if (SD.exists(filename)) if (SD.exists(filename))
@ -1147,6 +1155,136 @@ void readRFID()
lastInteraction = millis(); lastInteraction = millis();
} }
void init_webserver() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
deactivateRFID();
activateSD();
String htmlPath = getSysDir("index.html");
if (SD.exists(htmlPath))
{
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html");
request->send(response);
}
else
{
// Fallback: serve minimal error if file not found
request->send(404, "text/plain", "ERROR: /system/index.html not found!");
}
});
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
deactivateRFID();
activateSD();
// Ensure SD is active and RFID is deactivated while serving files.
String cssPath = getSysDir("style.css");
if (SD.exists(cssPath))
{
request->send(SD, cssPath, "text/css");
}
else
{
// Fallback: serve minimal CSS if file not found
request->send(404, "text/plain", "ERROR: /system/style.css not found!");
}
});
server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
deactivateRFID();
activateSD();
String jsPath = getSysDir("script.js");
if (SD.exists(jsPath))
{
request->send(SD, jsPath, "application/javascript");
}
else
{
// Fallback: serve minimal JS if file not found
request->send(404, "text/plain", "ERROR: /system/script.js not found!");
}
});
// Dynamic endpoints to avoid template processing heap spikes
server.on("/directory", HTTP_GET, [](AsyncWebServerRequest *request)
{
String html = processor(String("DIRECTORY"));
request->send(200, "text/html; charset=UTF-8", html);
});
server.on("/mapping", HTTP_GET, [](AsyncWebServerRequest *request)
{
String html = processor(String("MAPPING"));
request->send(200, "text/html; charset=UTF-8", html);
});
server.on("/state", HTTP_GET, [](AsyncWebServerRequest *request)
{
String state = getState();
request->send(200, "application/json charset=UTF-8", state.c_str()); });
server.on("/start", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain charset=UTF-8", "start");
start(); });
server.on("/toggleplaypause", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain charset=UTF-8", "toggleplaypause");
togglePlayPause(); });
server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain", "stop");
stop(); });
server.on("/next", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain", "next");
next(); });
server.on("/previous", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain", "previous");
previous(); });
server.on("/playbyid", HTTP_GET, id_song_action);
server.on("/progress", HTTP_POST, progress_action);
server.on("/volume", HTTP_POST, volume_action);
server.on("/edit_mapping", HTTP_POST, editMapping);
// run handleUpload function when any file is uploaded
server.on("/upload", HTTP_POST,
[](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200);
},
handleUpload);
server.on("/move_file", HTTP_GET, handleMoveFile);
server.on("/delete_file", HTTP_GET, handleDeleteFile);
}
void setup() void setup()
{ {
Serial.begin(115200); Serial.begin(115200);
@ -1245,111 +1383,7 @@ void setup()
if (wifiManager.autoConnect("HannaBox")) if (wifiManager.autoConnect("HannaBox"))
{ {
init_webserver();
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{
webrequestActive = true;
deactivateRFID();
activateSD();
String htmlPath = getSysDir("index.html");
if (SD.exists(htmlPath))
{
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html", false, processor);
response->addHeader("Content-Type", "text/html; charset=UTF-8");
request->send(response);
}
else
{
// Fallback: serve minimal error if file not found
request->send(404, "text/plain", "ERROR: /system/index.html not found!");
}
webrequestActive = false; });
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request)
{
webrequestActive = true;
deactivateRFID();
activateSD();
// Ensure SD is active and RFID is deactivated while serving files.
String cssPath = getSysDir("style.css");
if (SD.exists(cssPath))
{
request->send(SD, cssPath, "text/css");
}
else
{
// Fallback: serve minimal CSS if file not found
request->send(404, "text/plain", "ERROR: /system/style.css not found!");
}
webrequestActive = false; });
server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request)
{
webrequestActive = true;
deactivateRFID();
activateSD();
String jsPath = getSysDir("script.js");
if (SD.exists(jsPath))
{
request->send(SD, jsPath, "application/javascript");
}
else
{
// Fallback: serve minimal JS if file not found
request->send(404, "text/plain", "ERROR: /system/script.js not found!");
}
webrequestActive = false; });
server.on("/state", HTTP_GET, [](AsyncWebServerRequest *request)
{
String state = getState();
request->send(200, "application/json charset=UTF-8", state.c_str()); });
server.on("/start", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain charset=UTF-8", "start");
start(); });
server.on("/toggleplaypause", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain charset=UTF-8", "toggleplaypause");
togglePlayPause(); });
server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain", "stop");
stop(); });
server.on("/next", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain", "next");
next(); });
server.on("/previous", HTTP_GET, [](AsyncWebServerRequest *request)
{
request->send(200, "text/plain", "previous");
previous(); });
server.on("/playbyid", HTTP_GET, id_song_action);
server.on("/progress", HTTP_POST, progress_action);
server.on("/volume", HTTP_POST, volume_action);
server.on("/edit_mapping", HTTP_POST, editMapping);
// run handleUpload function when any file is uploaded
server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
{ request->send(200); }, handleUpload);
server.on("/move_file", HTTP_GET, handleMoveFile);
server.on("/delete_file", HTTP_GET, handleDeleteFile);
server.begin(); server.begin();
Serial.println("Wifi init"); Serial.println("Wifi init");
} }
@ -1434,15 +1468,16 @@ const String getSysDir(const String filename)
void loop() void loop()
{ {
if (webrequestActive && webrequest_blockings > 5000) { if (webreq_cnt > 0 && webrequest_blockings > 5000) {
Serial.println("excessive webrequest blocking!"); Serial.println("excessive webrequest blocking!");
webreq_cnt = 0;
webrequest_blockings = 0; webrequest_blockings = 0;
webrequestActive = false; server.reset();
server.end(); init_webserver();
server.begin(); server.begin();
} }
if (audio.isRunning() && !webrequestActive) if (audio.isRunning() && webreq_cnt == 0)
{ {
if (asyncStop) if (asyncStop)
{ {
@ -1465,13 +1500,13 @@ void loop()
pendingSeek = false; pendingSeek = false;
} }
} }
else if (asyncStart && !webrequestActive) else if (asyncStart && webreq_cnt == 0)
{ {
asyncStart = false; asyncStart = false;
start(); start();
} }
if (continuePlaying && !webrequestActive) if (continuePlaying && webreq_cnt == 0)
{ {
continuePlaying = false; continuePlaying = false;
startupSoundPlayed = true; startupSoundPlayed = true;
@ -1516,7 +1551,7 @@ void loop()
asyncTogglePlayPause = false; asyncTogglePlayPause = false;
togglePlayPause(); togglePlayPause();
} }
else if (asyncNext && !webrequestActive) else if (asyncNext && webreq_cnt == 0)
{ {
asyncNext = false; asyncNext = false;
// If the play/start button is held, treat NEXT as volume up // If the play/start button is held, treat NEXT as volume up
@ -1585,7 +1620,7 @@ void loop()
} }
} }
if (loopCounter % config.rfidLoopInterval == 0 && !webrequestActive) if (loopCounter % config.rfidLoopInterval == 0 && webreq_cnt == 0)
{ {
deactivateSD(); deactivateSD();
activateRFID(); activateRFID();
@ -1597,7 +1632,7 @@ void loop()
activateSD(); activateSD();
} }
if (loopCounter % VOLTAGE_LOOP_INTERVAL == 0 && !webrequestActive) if (loopCounter % VOLTAGE_LOOP_INTERVAL == 0 && webreq_cnt == 0)
{ {
lastVoltage = getBatteryVoltageMv(); lastVoltage = getBatteryVoltageMv();
free_heap = xPortGetFreeHeapSize(); free_heap = xPortGetFreeHeapSize();
@ -1620,8 +1655,10 @@ void loop()
} }
} }
if (webrequestActive) { if (webreq_cnt>0) {
webrequest_blockings++; webrequest_blockings++;
} else {
webrequest_blockings = 0;
} }
loopCounter++; loopCounter++;

View File

@ -15,6 +15,8 @@ void progress_action(AsyncWebServerRequest *request);
void volume_action(AsyncWebServerRequest *request); void volume_action(AsyncWebServerRequest *request);
void init_webserver();
boolean buttonPressed(const uint8_t pin); boolean buttonPressed(const uint8_t pin);
const String getSysDir(const String filename); const String getSysDir(const String filename);

View File

@ -75,9 +75,7 @@
<h2>Playlist</h2> <h2>Playlist</h2>
<button class="action-btn small" onclick="location.reload()">Refresh</button> <button class="action-btn small" onclick="location.reload()">Refresh</button>
</div> </div>
<div class="playlist-container"> <div class="playlist-container" id="playlistContainer"></div>
%DIRECTORY%
</div>
<div class="manager-toggle"> <div class="manager-toggle">
<button id="toggleFileManagerButton" class="action-btn" onclick="toggleFileManager()">Toggle Manager</button> <button id="toggleFileManagerButton" class="action-btn" onclick="toggleFileManager()">Toggle Manager</button>
@ -106,7 +104,7 @@
<h4>Edit RFID Mapping</h4> <h4>Edit RFID Mapping</h4>
<p class="hint">Hint: Use a folder or filename, not the absolute file path!</p> <p class="hint">Hint: Use a folder or filename, not the absolute file path!</p>
<div class="mapping-list">%MAPPING%</div> <div class="mapping-list" id="mappingList"></div>
<form id="editMappingForm" class="form form-grid"> <form id="editMappingForm" class="form form-grid">
<div> <div>

View File

@ -1,8 +1,59 @@
setInterval(getState, 4000); setInterval(getState, 4000);
setInterval(updateProgress, 500); // Update progress every second setInterval(updateProgress, 500); // Update progress every second
// Get the <li> elements /* Dynamic content loaders for playlist and mapping (avoid heavy template processing on server) */
var liElements = document.querySelectorAll('ul li'); function bindPlaylistClicks() {
var container = document.getElementById('playlistContainer');
if (!container) return;
container.onclick = function(e) {
var li = e.target.closest('li');
if (!li || !container.contains(li)) return;
var id = li.dataset && li.dataset.id;
if (id) playSongById(id);
};
}
function loadDirectory() {
var container = document.getElementById('playlistContainer');
if (!container) return;
var xhr = new XMLHttpRequest();
xhr.open('GET', '/directory', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
container.innerHTML = xhr.responseText || '';
bindPlaylistClicks();
} else {
container.innerHTML = '<div class="hint">Failed to load playlist.</div>';
}
}
};
xhr.send();
}
function loadMapping() {
var el = document.getElementById('mappingList');
if (!el) return;
var xhr = new XMLHttpRequest();
xhr.open('GET', '/mapping', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
el.innerHTML = xhr.responseText || '';
} else {
el.innerHTML = '<div class="hint">Failed to load mapping.</div>';
}
}
};
xhr.send();
}
/* Kick off dynamic loads on DOM ready */
document.addEventListener('DOMContentLoaded', function() {
loadDirectory();
loadMapping();
getState();
});
var lastChange = 0; var lastChange = 0;
@ -24,13 +75,7 @@ function formatTime(totalSec) {
} }
// Add click event listener to each <li> element // Add click event listener to each <li> element
liElements.forEach(function(li) { /* Clicks are handled via event delegation in bindPlaylistClicks() */
li.addEventListener('click', function() {
//var liText = this.innerText;
var id = this.dataset.id;
playSongById(id);
});
});
function simpleGetCall(endpoint) { function simpleGetCall(endpoint) {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();