[ai] improve web stability
This commit is contained in:
parent
b6ac157207
commit
0a08709160
52
src/main.cpp
52
src/main.cpp
|
|
@ -330,7 +330,9 @@ void handleMoveFile(AsyncWebServerRequest *request)
|
|||
|
||||
if (SD.exists(from))
|
||||
{
|
||||
sd_lock_acquire();
|
||||
SD.rename(from, to);
|
||||
sd_lock_release();
|
||||
Serial.println("Moved file: " + from + " to " + to);
|
||||
// Rebuild directory tree to update file list
|
||||
rootNode.buildDirectoryTree("/");
|
||||
|
|
@ -351,7 +353,9 @@ void handleDeleteFile(AsyncWebServerRequest *request)
|
|||
|
||||
if (SD.exists(filename))
|
||||
{
|
||||
sd_lock_acquire();
|
||||
SD.remove(filename.c_str());
|
||||
sd_lock_release();
|
||||
Serial.println("Deleted file: " + filename);
|
||||
// Rebuild directory tree to update file list
|
||||
rootNode.buildDirectoryTree("/");
|
||||
|
|
@ -800,6 +804,8 @@ void saveMappingToFile(const String filename)
|
|||
// Function to handle edit requests
|
||||
void editMapping(AsyncWebServerRequest *request)
|
||||
{
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
if (request->hasParam("rfid", true) && request->hasParam("song", true))
|
||||
{
|
||||
String rfid = request->getParam("rfid", true)->value();
|
||||
|
|
@ -1166,6 +1172,7 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
|||
if (SD.exists(htmlPath))
|
||||
{
|
||||
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html");
|
||||
response->addHeader("Cache-Control", "no-store");
|
||||
request->send(response);
|
||||
}
|
||||
else
|
||||
|
|
@ -1186,7 +1193,11 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
|||
String cssPath = getSysDir("style.css");
|
||||
if (SD.exists(cssPath))
|
||||
{
|
||||
request->send(SD, cssPath, "text/css");
|
||||
{
|
||||
AsyncWebServerResponse *resp = request->beginResponse(SD, cssPath, "text/css");
|
||||
resp->addHeader("Cache-Control", "public, max-age=300");
|
||||
request->send(resp);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1205,7 +1216,11 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
|||
String jsPath = getSysDir("script.js");
|
||||
if (SD.exists(jsPath))
|
||||
{
|
||||
request->send(SD, jsPath, "application/javascript");
|
||||
{
|
||||
AsyncWebServerResponse *resp = request->beginResponse(SD, jsPath, "application/javascript");
|
||||
resp->addHeader("Cache-Control", "public, max-age=300");
|
||||
request->send(resp);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1218,48 +1233,59 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
|||
// Dynamic endpoints to avoid template processing heap spikes
|
||||
server.on("/directory", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
String html = processor(String("DIRECTORY"));
|
||||
request->send(200, "text/html; charset=UTF-8", html);
|
||||
});
|
||||
|
||||
server.on("/mapping", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
String html = processor(String("MAPPING"));
|
||||
request->send(200, "text/html; charset=UTF-8", html);
|
||||
});
|
||||
|
||||
server.on("/state", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
String state = getState();
|
||||
request->send(200, "application/json charset=UTF-8", state.c_str()); });
|
||||
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");
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
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");
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
request->send(200, "text/plain; charset=UTF-8", "toggleplaypause");
|
||||
togglePlayPause(); });
|
||||
|
||||
server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
request->send(200, "text/plain", "stop");
|
||||
stop(); });
|
||||
|
||||
server.on("/next", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
request->send(200, "text/plain", "next");
|
||||
next(); });
|
||||
|
||||
server.on("/previous", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||
{
|
||||
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
request->send(200, "text/plain", "previous");
|
||||
previous(); });
|
||||
|
||||
|
|
@ -1410,6 +1436,8 @@ void setup()
|
|||
|
||||
void id_song_action(AsyncWebServerRequest *request)
|
||||
{
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
int params = request->params();
|
||||
for (int i = 0; i < params; i++)
|
||||
{
|
||||
|
|
@ -1425,6 +1453,8 @@ void id_song_action(AsyncWebServerRequest *request)
|
|||
|
||||
void progress_action(AsyncWebServerRequest *request)
|
||||
{
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
|
||||
int params = request->params();
|
||||
for (int i = 0; i < params; i++)
|
||||
|
|
@ -1441,6 +1471,8 @@ void progress_action(AsyncWebServerRequest *request)
|
|||
|
||||
void volume_action(AsyncWebServerRequest *request)
|
||||
{
|
||||
webreq_enter();
|
||||
request->onDisconnect([](){ webreq_exit(); });
|
||||
|
||||
int params = request->params();
|
||||
for (int i = 0; i < params; i++)
|
||||
|
|
|
|||
104
web/script.js
104
web/script.js
|
|
@ -1,6 +1,88 @@
|
|||
setInterval(getState, 4000);
|
||||
setInterval(updateProgress, 500); // Update progress every second
|
||||
|
||||
/* Global single-flight queue for XMLHttpRequest
|
||||
- Serializes all XHR to 1 at a time
|
||||
- Adds timeouts (GET ~3.5s, POST ~6s, /upload 10min)
|
||||
- Deduplicates idempotent GETs to same URL (drops duplicates)
|
||||
This reduces concurrent load on the ESP32 web server and SD card. */
|
||||
(function(){
|
||||
var origOpen = XMLHttpRequest.prototype.open;
|
||||
var origSend = XMLHttpRequest.prototype.send;
|
||||
|
||||
var queue = [];
|
||||
var active = null;
|
||||
var inflightKeys = new Set();
|
||||
|
||||
function keyOf(xhr){ return (xhr.__method || 'GET') + ' ' + (xhr.__url || ''); }
|
||||
|
||||
function startNext(){
|
||||
if (active || queue.length === 0) return;
|
||||
var item = queue.shift();
|
||||
active = item;
|
||||
inflightKeys.add(item.key);
|
||||
|
||||
var xhr = item.xhr;
|
||||
var timeoutMs = item.timeoutMs;
|
||||
var timer = null;
|
||||
|
||||
function cleanup() {
|
||||
active = null;
|
||||
inflightKeys.delete(item.key);
|
||||
if (timer) { clearTimeout(timer); timer = null; }
|
||||
setTimeout(startNext, 0);
|
||||
}
|
||||
|
||||
xhr.addEventListener('loadend', cleanup);
|
||||
|
||||
if (timeoutMs > 0 && !xhr.__skipTimeout) {
|
||||
timer = setTimeout(function(){
|
||||
try { xhr.abort(); } catch(e){}
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
item.origSend.call(xhr, item.body);
|
||||
}
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method, url, async){
|
||||
this.__method = (method || 'GET').toUpperCase();
|
||||
this.__url = url || '';
|
||||
return origOpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function(body){
|
||||
var key = keyOf(this);
|
||||
var isIdempotentGET = (this.__method === 'GET');
|
||||
|
||||
var timeoutMs;
|
||||
if ((this.__url || '').indexOf('/upload') !== -1) {
|
||||
timeoutMs = 600000; // 10 minutes for uploads
|
||||
} else if (this.__method === 'GET') {
|
||||
timeoutMs = 3500;
|
||||
} else {
|
||||
timeoutMs = 6000;
|
||||
}
|
||||
|
||||
if (isIdempotentGET && inflightKeys.has(key)) {
|
||||
// Drop duplicate GET to same resource
|
||||
return;
|
||||
}
|
||||
if (isIdempotentGET) {
|
||||
for (var i = 0; i < queue.length; i++) {
|
||||
if (queue[i].key === key) {
|
||||
// Already queued; keep most recent body if any
|
||||
queue[i].body = body;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var item = { xhr: this, body: body, key: key, timeoutMs: timeoutMs, origSend: origSend };
|
||||
queue.push(item);
|
||||
startNext();
|
||||
};
|
||||
})();
|
||||
|
||||
/* Dynamic content loaders for playlist and mapping (avoid heavy template processing on server) */
|
||||
function bindPlaylistClicks() {
|
||||
var container = document.getElementById('playlistContainer');
|
||||
|
|
@ -107,16 +189,22 @@ function getState() {
|
|||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
var state = JSON.parse(xhr.response);
|
||||
isPlaying = state['playing'];
|
||||
if (isPlaying) {
|
||||
songStartTime = Date.now() - state['time'] * 1000;
|
||||
currentSongLength = state['length'] * 1000;
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
var state = JSON.parse(xhr.responseText || xhr.response || '{}');
|
||||
isPlaying = !!state['playing'];
|
||||
if (isPlaying) {
|
||||
songStartTime = Date.now() - ((state['time'] || 0) * 1000);
|
||||
currentSongLength = ((state['length'] || 0) * 1000);
|
||||
}
|
||||
lastStateUpdateTime = Date.now();
|
||||
displayState(state);
|
||||
} catch (e) {
|
||||
// Ignore parse errors; will retry on next poll
|
||||
}
|
||||
}
|
||||
lastStateUpdateTime = Date.now();
|
||||
displayState(state);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.open("GET","/state", true);
|
||||
xhr.send();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue