cleanups, memory savings
This commit is contained in:
parent
cfa3feb7d2
commit
7e20aa65e1
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
84
src/main.cpp
84
src/main.cpp
|
|
@ -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
|
||||||
|
|
|
||||||
19
src/main.h
19
src/main.h
|
|
@ -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]) + " " +
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
})();
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue