cleanups, memory savings

This commit is contained in:
Stefan Ostermann 2025-10-07 21:35:39 +02:00
parent cfa3feb7d2
commit 7e20aa65e1
5 changed files with 56 additions and 154 deletions

View File

@ -470,6 +470,11 @@ void DirectoryNode::buildFlatMP3List(std::vector<std::pair<DirectoryNode *, int>
} }
} }
const size_t DirectoryNode::getNumOfFiles()
{
return subdirectories.size();
}
DirectoryNode *DirectoryNode::advanceToNextMP3(const String *currentGlobal) DirectoryNode *DirectoryNode::advanceToNextMP3(const String *currentGlobal)
{ {
bool useFirst = false; bool useFirst = false;
@ -537,7 +542,6 @@ DirectoryNode *DirectoryNode::advanceToNextMP3(const String *currentGlobal)
void DirectoryNode::streamDirectoryHTML(Print &out) const { void DirectoryNode::streamDirectoryHTML(Print &out) const {
if (name == "/") { if (name == "/") {
out.println(F("<ul>")); out.println(F("<ul>"));
delay(0); // yield to WiFi/other tasks
} }
if (name != "/") { if (name != "/") {
@ -546,29 +550,24 @@ void DirectoryNode::streamDirectoryHTML(Print &out) const {
out.print(F("\"><b>")); out.print(F("\"><b>"));
out.print(name); out.print(name);
out.println(F("</b></li>")); out.println(F("</b></li>"));
delay(0); // yield periodically while streaming
} }
delay(0);
for (size_t i = 0; i < mp3Files.size(); i++) { for (size_t i = 0; i < mp3Files.size(); i++) {
out.print(F("<li data-id=\"")); out.print(F("<li data-id=\""));
out.print(ids[i]); out.print(ids[i]);
out.print(F("\">")); out.print(F("\">"));
out.print(mp3Files[i]); out.print(mp3Files[i]);
out.println(F("</li>")); out.println(F("</li>"));
if ((i & 0x0F) == 0) { // yield every ~16 items
delay(0);
}
} }
for (DirectoryNode* child : subdirectories) { for (DirectoryNode* child : subdirectories) {
delay(0); // yield before descending
child->streamDirectoryHTML(out); child->streamDirectoryHTML(out);
delay(0); // and after returning
} }
if (name == "/") { if (name == "/") {
out.println(F("</ul>")); out.println(F("</ul>"));
delay(0);
} }
} }

View File

@ -35,6 +35,8 @@ public:
const std::vector<DirectoryNode*>& getSubdirectories() const; const std::vector<DirectoryNode*>& getSubdirectories() const;
const std::vector<String>& getMP3Files() const; const std::vector<String>& getMP3Files() const;
const size_t getNumOfFiles();
void setCurrentPlaying(const String* mp3File); void setCurrentPlaying(const String* mp3File);
const String* getCurrentPlaying() const; const String* getCurrentPlaying() const;
const uint16_t getCurrentPlayingId() const; const uint16_t getCurrentPlayingId() const;

View File

@ -28,39 +28,6 @@
/* Simple spinlock using older GCC sync builtins (no libatomic required).
sd_lock_acquire() will block (with a small delay) until the lock is free.
sd_lock_release() releases the lock. This is sufficient for short SD ops. */
static inline void sd_lock_acquire()
{
while (__sync_lock_test_and_set(&sd_lock_flag, 1))
{
delay(1);
}
}
static inline void sd_lock_release()
{
__sync_lock_release(&sd_lock_flag);
}
volatile uint8_t dir_lock_flag = 0;
/* Lightweight spinlock to guard directory tree access (rootNode) */
static inline void dir_lock_acquire()
{
while (__sync_lock_test_and_set(&dir_lock_flag, 1))
{
delay(1);
}
}
static inline void dir_lock_release()
{
__sync_lock_release(&dir_lock_flag);
}
// webrequest_blockings is a simple watchdog counter that tracks how long at least one HTTP request has been “active” (not yet disconnected) according to the AsyncWebServer. // webrequest_blockings is a simple watchdog counter that tracks how long at least one HTTP request has been “active” (not yet disconnected) according to the AsyncWebServer.
int webrequest_blockings = 0; int webrequest_blockings = 0;
@ -248,9 +215,9 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
logBuffer.clear(); logBuffer.clear();
// Rebuild directory tree to include new file (guarded) // Rebuild directory tree to include new file (guarded)
dir_lock_acquire(); sd_lock_acquire();
rootNode.buildDirectoryTree("/"); rootNode.buildDirectoryTree("/");
dir_lock_release(); sd_lock_release();
request->send(200, txt_plain, "Upload successful"); request->send(200, txt_plain, "Upload successful");
} }
@ -275,9 +242,9 @@ void handleMoveFile(AsyncWebServerRequest *request)
sd_lock_release(); sd_lock_release();
Serial.println("Moved file: " + from + " to " + to); Serial.println("Moved file: " + from + " to " + to);
// Rebuild directory tree to update file list (guarded) // Rebuild directory tree to update file list (guarded)
dir_lock_acquire(); sd_lock_acquire();
rootNode.buildDirectoryTree("/"); rootNode.buildDirectoryTree("/");
dir_lock_release(); sd_lock_release();
request->send(200, txt_plain, F("File moved successfully.")); request->send(200, txt_plain, F("File moved successfully."));
} }
else else
@ -300,9 +267,9 @@ void handleDeleteFile(AsyncWebServerRequest *request)
sd_lock_release(); sd_lock_release();
Serial.println("Deleted file: " + filename); Serial.println("Deleted file: " + filename);
// Rebuild directory tree to update file list (guarded) // Rebuild directory tree to update file list (guarded)
dir_lock_acquire(); sd_lock_acquire();
rootNode.buildDirectoryTree("/"); rootNode.buildDirectoryTree("/");
dir_lock_release(); sd_lock_release();
request->send(200, txt_plain, "File deleted."); request->send(200, txt_plain, "File deleted.");
} }
else else
@ -430,14 +397,16 @@ void playSongByRFID(String id)
auto it = rfid_map.find(id); auto it = rfid_map.find(id);
if (it == rfid_map.end()) if (it == rfid_map.end())
{ {
Serial.println("Song for UID not found: " + id); Serial.print(F("Song for UID not found: "));
Serial.println(id);
return; return;
} }
MappingEntry entry = it->second; MappingEntry entry = it->second;
if (entry.target.length() == 0) if (entry.target.length() == 0)
{ {
Serial.println("Empty mapping target for UID: " + id); Serial.print(F("Empty mapping target for UID: "));
Serial.println(id);
return; return;
} }
@ -544,7 +513,7 @@ void playSongByRFID(String id)
} }
} }
Serial.print("Playing mapped target: "); Serial.print(F("Playing mapped target: "));
Serial.println(mp3File); Serial.println(mp3File);
deactivateRFID(); deactivateRFID();
@ -552,7 +521,8 @@ void playSongByRFID(String id)
if (!playFile(mp3File.c_str())) if (!playFile(mp3File.c_str()))
{ {
Serial.println("Failed to play mapped file: " + mp3File); Serial.print(F("Failed to play mapped file: "));
Serial.println( mp3File);
currentNode = nullptr; currentNode = nullptr;
return; return;
@ -571,7 +541,7 @@ bool playFile(const char *filename, uint32_t resumeFilePos)
{ {
if (filename == nullptr || strlen(filename) == 0) if (filename == nullptr || strlen(filename) == 0)
{ {
Serial.println("filename empty."); Serial.println(F("filename empty."));
return false; return false;
} }
// Serialize access to SD when audio opens the file (short critical section) // Serialize access to SD when audio opens the file (short critical section)
@ -690,7 +660,7 @@ boolean readSongProgress(const char *filename)
data.trim(); data.trim();
if (data.length() == 0) if (data.length() == 0)
{ {
Serial.println("Progress file empty"); Serial.println(F("Progress file empty"));
return false; return false;
} }
@ -701,7 +671,8 @@ boolean readSongProgress(const char *filename)
if (parsed != 2) if (parsed != 2)
{ {
Serial.println("Failed to parse progress data: " + data); Serial.print(F("Failed to parse progress data: "));
Serial.println(data);
return false; return false;
} }
@ -1156,9 +1127,10 @@ void init_webserver() {
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ {
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); sd_lock_release();});
deactivateRFID(); deactivateRFID();
activateSD(); activateSD();
sd_lock_acquire();
String htmlPath = getSysDir(index_file); String htmlPath = getSysDir(index_file);
if (SD.exists(htmlPath)) if (SD.exists(htmlPath))
{ {
@ -1186,15 +1158,15 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request)
{ {
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); sd_lock_release();});
deactivateRFID(); deactivateRFID();
activateSD(); activateSD();
// Ensure SD is active and RFID is deactivated while serving files. // Ensure SD is active and RFID is deactivated while serving files.
String cssPath = getSysDir(style_file); String cssPath = getSysDir(style_file);
if (SD.exists(cssPath)) if (SD.exists(cssPath))
{ {
uint32_t fsize = 0; uint32_t fsize = 0;
{ {
File f = SD.open(cssPath); File f = SD.open(cssPath);
if (f) { fsize = f.size(); f.close(); } if (f) { fsize = f.size(); f.close(); }
} }
@ -1219,9 +1191,10 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request)
{ {
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); sd_lock_release();});
deactivateRFID(); deactivateRFID();
activateSD(); activateSD();
sd_lock_acquire();
String jsPath = getSysDir(script_file); String jsPath = getSysDir(script_file);
if (SD.exists(jsPath)) if (SD.exists(jsPath))
{ {
@ -1256,21 +1229,20 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
// Stream the response directly from the directory tree to avoid large temporary Strings // Stream the response directly from the directory tree to avoid large temporary Strings
AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset); AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset);
#ifdef DEBUG #ifdef DEBUG
Serial.printf("Serving /directory heap=%u webreq_cnt=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt); Serial.printf("Serving /directory heap=%u webreq_cnt=%u numOfFiles=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt, rootNode.getNumOfFiles());
#endif #endif
stream->addHeader(hdr_cache_control_key, hdr_cache_control_val); stream->addHeader(hdr_cache_control_key, hdr_cache_control_val);
stream->addHeader(hdr_connection_key, hdr_connection_val); stream->addHeader(hdr_connection_key, hdr_connection_val);
// Generate HTML directly into the stream under lock // Generate HTML directly into the stream under lock
dir_lock_acquire(); rootNode.streamDirectoryHTML(*stream);
rootNode.streamDirectoryHTML(*stream);
dir_lock_release();
request->send(stream); request->send(stream);
}); });
server.on("/mapping", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/mapping", HTTP_GET, [](AsyncWebServerRequest *request)
{ {
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); sd_lock_release();});
sd_lock_acquire();
// Stream mapping to avoid Content-Length mismatches and reduce heap spikes // Stream mapping to avoid Content-Length mismatches and reduce heap spikes
AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset); AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset);
#ifdef DEBUG #ifdef DEBUG

View File

@ -34,6 +34,8 @@
#define MAX_VOL 15 #define MAX_VOL 15
//#define DEBUG TRUE
File root; File root;
File mp3File; File mp3File;
@ -125,6 +127,23 @@ void dump_byte_array(byte *buffer, byte bufferSize)
} }
} }
/* Simple spinlock using older GCC sync builtins (no libatomic required).
sd_lock_acquire() will block (with a small delay) until the lock is free.
sd_lock_release() releases the lock. This is sufficient for short SD ops. */
static inline void sd_lock_acquire()
{
while (__sync_lock_test_and_set(&sd_lock_flag, 1))
{
delay(1);
}
}
static inline void sd_lock_release()
{
__sync_lock_release(&sd_lock_flag);
}
String getRFIDString(byte uidByte[10]) String getRFIDString(byte uidByte[10])
{ {
String uidString = String(uidByte[0]) + " " + String(uidByte[1]) + " " + String uidString = String(uidByte[0]) + " " + String(uidByte[1]) + " " +

View File

@ -326,7 +326,6 @@ function editMapping() {
// Validate file before upload // Validate file before upload
function validateFile(file) { function validateFile(file) {
var maxSize = 50 * 1024 * 1024; // 50MB limit var maxSize = 50 * 1024 * 1024; // 50MB limit
var allowedTypes = ['audio/mpeg', 'audio/wav'];
var allowedExtensions = ['.mp3']; var allowedExtensions = ['.mp3'];
if (file.size > maxSize) { if (file.size > maxSize) {
@ -499,92 +498,3 @@ function deleteFileOnServer() {
xhr.send(); xhr.send();
} }
/* Ensure the site stylesheet loads reliably retry loader if necessary
Improved detection: verify a computed style from CSS is applied (safer than just checking stylesheet href).
Retries with exponential backoff and deduplicates link tags we add. */
(function ensureCssLoaded(){
var retries = 0;
var maxRetries = 6;
// Check a computed style that the stylesheet defines.
// .status color in CSS is --muted: #6b7280 -> rgb(107, 114, 128)
function isStyleApplied() {
var el = document.querySelector('.status') || document.querySelector('.topbar');
if (!el) return false;
try {
var color = getComputedStyle(el).color;
// Expect "rgb(107, 114, 128)" when CSS is applied
if (!color) return false;
// Loose check for the three numeric components to be present
return color.indexOf('107') !== -1 && color.indexOf('114') !== -1 && color.indexOf('128') !== -1;
} catch (e) {
return false;
}
}
function removeOldRetryLinks() {
var links = Array.prototype.slice.call(document.querySelectorAll('link[data-retry-css]'));
links.forEach(function(l){ l.parentNode.removeChild(l); });
}
function tryLoad() {
if (isStyleApplied()) {
console.log('style.css appears applied');
return;
}
if (retries >= maxRetries) {
console.warn('style.css failed to apply after ' + retries + ' attempts');
return;
}
retries++;
// Remove previous retry-inserted links to avoid piling them up
removeOldRetryLinks();
var link = document.createElement('link');
link.rel = 'stylesheet';
link.setAttribute('data-retry-css', '1');
// cache-busting query to force a fresh fetch when retrying
link.href = 'style.css?cb=' + Date.now();
var timeout = 800 + retries * 300; // increasing timeout per attempt
var done = false;
function success() {
if (done) return;
done = true;
// Give browser a short moment to apply rules
setTimeout(function(){
if (isStyleApplied()) {
console.log('style.css loaded and applied (attempt ' + retries + ')');
} else {
console.warn('style.css loaded but styles not applied — retrying...');
setTimeout(tryLoad, timeout);
}
}, 200);
}
link.onload = success;
link.onerror = function() {
if (done) return;
done = true;
console.warn('style.css load error (attempt ' + retries + '), retrying...');
setTimeout(tryLoad, timeout);
};
// Append link to head
document.head.appendChild(link);
// Safety check: if onload/onerror doesn't fire, verify computed style after timeout
setTimeout(function(){
if (done) return;
if (isStyleApplied()) {
console.log('style.css appears applied (delayed check)');
} else {
console.warn('style.css still not applied after timeout (attempt ' + retries + '), retrying...');
setTimeout(tryLoad, timeout);
}
}, timeout + 300);
}
// Start after a short delay to let the browser initiate initial requests
setTimeout(tryLoad, 150);
})();