diff --git a/src/main.cpp b/src/main.cpp index 63463f2..204673d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -114,7 +114,11 @@ bool SDActive = 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; @@ -319,6 +323,8 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, void handleMoveFile(AsyncWebServerRequest *request) { + webreq_enter(); + request->onDisconnect([](){ webreq_exit(); }); String from = request->arg("from"); String to = request->arg("to"); @@ -339,6 +345,8 @@ void handleMoveFile(AsyncWebServerRequest *request) void handleDeleteFile(AsyncWebServerRequest *request) { + webreq_enter(); + request->onDisconnect([](){ webreq_exit(); }); String filename = request->arg("filename"); if (SD.exists(filename)) @@ -1147,6 +1155,136 @@ void readRFID() 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() { Serial.begin(115200); @@ -1245,111 +1383,7 @@ void setup() if (wifiManager.autoConnect("HannaBox")) { - - 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); - + init_webserver(); server.begin(); Serial.println("Wifi init"); } @@ -1434,15 +1468,16 @@ const String getSysDir(const String filename) void loop() { - if (webrequestActive && webrequest_blockings > 5000) { + if (webreq_cnt > 0 && webrequest_blockings > 5000) { Serial.println("excessive webrequest blocking!"); + webreq_cnt = 0; webrequest_blockings = 0; - webrequestActive = false; - server.end(); + server.reset(); + init_webserver(); server.begin(); } - if (audio.isRunning() && !webrequestActive) + if (audio.isRunning() && webreq_cnt == 0) { if (asyncStop) { @@ -1465,13 +1500,13 @@ void loop() pendingSeek = false; } } - else if (asyncStart && !webrequestActive) + else if (asyncStart && webreq_cnt == 0) { asyncStart = false; start(); } - if (continuePlaying && !webrequestActive) + if (continuePlaying && webreq_cnt == 0) { continuePlaying = false; startupSoundPlayed = true; @@ -1516,7 +1551,7 @@ void loop() asyncTogglePlayPause = false; togglePlayPause(); } - else if (asyncNext && !webrequestActive) + else if (asyncNext && webreq_cnt == 0) { asyncNext = false; // 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(); activateRFID(); @@ -1597,7 +1632,7 @@ void loop() activateSD(); } - if (loopCounter % VOLTAGE_LOOP_INTERVAL == 0 && !webrequestActive) + if (loopCounter % VOLTAGE_LOOP_INTERVAL == 0 && webreq_cnt == 0) { lastVoltage = getBatteryVoltageMv(); free_heap = xPortGetFreeHeapSize(); @@ -1620,8 +1655,10 @@ void loop() } } - if (webrequestActive) { + if (webreq_cnt>0) { webrequest_blockings++; + } else { + webrequest_blockings = 0; } loopCounter++; diff --git a/src/main.h b/src/main.h index 55a66f1..54051cf 100644 --- a/src/main.h +++ b/src/main.h @@ -15,6 +15,8 @@ void progress_action(AsyncWebServerRequest *request); void volume_action(AsyncWebServerRequest *request); +void init_webserver(); + boolean buttonPressed(const uint8_t pin); const String getSysDir(const String filename); diff --git a/web/index.html b/web/index.html index 367601d..1e07812 100644 --- a/web/index.html +++ b/web/index.html @@ -75,9 +75,7 @@

Playlist

-
- %DIRECTORY% -
+
@@ -106,7 +104,7 @@

Edit RFID Mapping

Hint: Use a folder or filename, not the absolute file path!

-
%MAPPING%
+
diff --git a/web/script.js b/web/script.js index 3e2c12a..0795d79 100644 --- a/web/script.js +++ b/web/script.js @@ -1,8 +1,59 @@ setInterval(getState, 4000); setInterval(updateProgress, 500); // Update progress every second -// Get the
  • elements -var liElements = document.querySelectorAll('ul li'); +/* Dynamic content loaders for playlist and mapping (avoid heavy template processing on server) */ +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 = '
    Failed to load playlist.
    '; + } + } + }; + 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 = '
    Failed to load mapping.
    '; + } + } + }; + xhr.send(); +} + +/* Kick off dynamic loads on DOM ready */ +document.addEventListener('DOMContentLoaded', function() { + loadDirectory(); + loadMapping(); + getState(); +}); var lastChange = 0; @@ -24,13 +75,7 @@ function formatTime(totalSec) { } // Add click event listener to each
  • element -liElements.forEach(function(li) { - li.addEventListener('click', function() { - //var liText = this.innerText; - var id = this.dataset.id; - playSongById(id); - }); -}); +/* Clicks are handled via event delegation in bindPlaylistClicks() */ function simpleGetCall(endpoint) { var xhr = new XMLHttpRequest();