[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))
|
if (SD.exists(from))
|
||||||
{
|
{
|
||||||
|
sd_lock_acquire();
|
||||||
SD.rename(from, to);
|
SD.rename(from, to);
|
||||||
|
sd_lock_release();
|
||||||
Serial.println("Moved file: " + from + " to " + to);
|
Serial.println("Moved file: " + from + " to " + to);
|
||||||
// Rebuild directory tree to update file list
|
// Rebuild directory tree to update file list
|
||||||
rootNode.buildDirectoryTree("/");
|
rootNode.buildDirectoryTree("/");
|
||||||
|
|
@ -351,7 +353,9 @@ void handleDeleteFile(AsyncWebServerRequest *request)
|
||||||
|
|
||||||
if (SD.exists(filename))
|
if (SD.exists(filename))
|
||||||
{
|
{
|
||||||
|
sd_lock_acquire();
|
||||||
SD.remove(filename.c_str());
|
SD.remove(filename.c_str());
|
||||||
|
sd_lock_release();
|
||||||
Serial.println("Deleted file: " + filename);
|
Serial.println("Deleted file: " + filename);
|
||||||
// Rebuild directory tree to update file list
|
// Rebuild directory tree to update file list
|
||||||
rootNode.buildDirectoryTree("/");
|
rootNode.buildDirectoryTree("/");
|
||||||
|
|
@ -800,6 +804,8 @@ void saveMappingToFile(const String filename)
|
||||||
// Function to handle edit requests
|
// Function to handle edit requests
|
||||||
void editMapping(AsyncWebServerRequest *request)
|
void editMapping(AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
if (request->hasParam("rfid", true) && request->hasParam("song", true))
|
if (request->hasParam("rfid", true) && request->hasParam("song", true))
|
||||||
{
|
{
|
||||||
String rfid = request->getParam("rfid", true)->value();
|
String rfid = request->getParam("rfid", true)->value();
|
||||||
|
|
@ -1166,6 +1172,7 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
if (SD.exists(htmlPath))
|
if (SD.exists(htmlPath))
|
||||||
{
|
{
|
||||||
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html");
|
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html");
|
||||||
|
response->addHeader("Cache-Control", "no-store");
|
||||||
request->send(response);
|
request->send(response);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -1186,7 +1193,11 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
String cssPath = getSysDir("style.css");
|
String cssPath = getSysDir("style.css");
|
||||||
if (SD.exists(cssPath))
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1205,7 +1216,11 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
String jsPath = getSysDir("script.js");
|
String jsPath = getSysDir("script.js");
|
||||||
if (SD.exists(jsPath))
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1218,48 +1233,59 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
// Dynamic endpoints to avoid template processing heap spikes
|
// Dynamic endpoints to avoid template processing heap spikes
|
||||||
server.on("/directory", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/directory", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
String html = processor(String("DIRECTORY"));
|
String html = processor(String("DIRECTORY"));
|
||||||
request->send(200, "text/html; charset=UTF-8", html);
|
request->send(200, "text/html; charset=UTF-8", html);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("/mapping", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/mapping", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
String html = processor(String("MAPPING"));
|
String html = processor(String("MAPPING"));
|
||||||
request->send(200, "text/html; charset=UTF-8", html);
|
request->send(200, "text/html; charset=UTF-8", html);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("/state", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/state", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
String state = getState();
|
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)
|
server.on("/start", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
request->send(200, "text/plain charset=UTF-8", "start");
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
|
request->send(200, "text/plain; charset=UTF-8", "start");
|
||||||
start(); });
|
start(); });
|
||||||
|
|
||||||
server.on("/toggleplaypause", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/toggleplaypause", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
request->send(200, "text/plain charset=UTF-8", "toggleplaypause");
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
|
request->send(200, "text/plain; charset=UTF-8", "toggleplaypause");
|
||||||
togglePlayPause(); });
|
togglePlayPause(); });
|
||||||
|
|
||||||
server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
request->send(200, "text/plain", "stop");
|
request->send(200, "text/plain", "stop");
|
||||||
stop(); });
|
stop(); });
|
||||||
|
|
||||||
server.on("/next", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/next", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
request->send(200, "text/plain", "next");
|
request->send(200, "text/plain", "next");
|
||||||
next(); });
|
next(); });
|
||||||
|
|
||||||
server.on("/previous", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/previous", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
request->send(200, "text/plain", "previous");
|
request->send(200, "text/plain", "previous");
|
||||||
previous(); });
|
previous(); });
|
||||||
|
|
||||||
|
|
@ -1410,6 +1436,8 @@ void setup()
|
||||||
|
|
||||||
void id_song_action(AsyncWebServerRequest *request)
|
void id_song_action(AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
int params = request->params();
|
int params = request->params();
|
||||||
for (int i = 0; i < params; i++)
|
for (int i = 0; i < params; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -1425,6 +1453,8 @@ void id_song_action(AsyncWebServerRequest *request)
|
||||||
|
|
||||||
void progress_action(AsyncWebServerRequest *request)
|
void progress_action(AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
|
|
||||||
int params = request->params();
|
int params = request->params();
|
||||||
for (int i = 0; i < params; i++)
|
for (int i = 0; i < params; i++)
|
||||||
|
|
@ -1441,6 +1471,8 @@ void progress_action(AsyncWebServerRequest *request)
|
||||||
|
|
||||||
void volume_action(AsyncWebServerRequest *request)
|
void volume_action(AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
|
webreq_enter();
|
||||||
|
request->onDisconnect([](){ webreq_exit(); });
|
||||||
|
|
||||||
int params = request->params();
|
int params = request->params();
|
||||||
for (int i = 0; i < params; i++)
|
for (int i = 0; i < params; i++)
|
||||||
|
|
|
||||||
104
web/script.js
104
web/script.js
|
|
@ -1,6 +1,88 @@
|
||||||
setInterval(getState, 4000);
|
setInterval(getState, 4000);
|
||||||
setInterval(updateProgress, 500); // Update progress every second
|
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) */
|
/* Dynamic content loaders for playlist and mapping (avoid heavy template processing on server) */
|
||||||
function bindPlaylistClicks() {
|
function bindPlaylistClicks() {
|
||||||
var container = document.getElementById('playlistContainer');
|
var container = document.getElementById('playlistContainer');
|
||||||
|
|
@ -107,16 +189,22 @@ function getState() {
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.onreadystatechange = function() {
|
xhr.onreadystatechange = function() {
|
||||||
if (xhr.readyState === 4) {
|
if (xhr.readyState === 4) {
|
||||||
var state = JSON.parse(xhr.response);
|
if (xhr.status >= 200 && xhr.status < 300) {
|
||||||
isPlaying = state['playing'];
|
try {
|
||||||
if (isPlaying) {
|
var state = JSON.parse(xhr.responseText || xhr.response || '{}');
|
||||||
songStartTime = Date.now() - state['time'] * 1000;
|
isPlaying = !!state['playing'];
|
||||||
currentSongLength = state['length'] * 1000;
|
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.open("GET","/state", true);
|
||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue