From b8e1263fb3be46f9d03a254a28b409c28bb1e1ec Mon Sep 17 00:00:00 2001 From: Stefan Ostermann Date: Sun, 17 Aug 2025 20:15:51 +0200 Subject: [PATCH] [ai] Fixed CSS not loaded error, volume via buttons,... --- src/main.cpp | 143 ++++++++++++++++++++++++++++++++++---------------- web/script.js | 90 +++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 45 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index bafd0ec..810f73c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -77,6 +77,24 @@ DirectoryNode rootNode("/"); DirectoryNode *currentNode = nullptr; volatile bool newRfidInt = false; +volatile bool playButtonDown = false; +volatile uint8_t sd_lock_flag = 0; + +/* 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); +} MFRC522 rfid(CS_RFID, RST_RFID); // instatiate a MFRC522 reader object. @@ -219,13 +237,16 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, Serial.println(filepath); } - // Open the file for writing + // Open the file for writing (guard with simple mutex) + sd_lock_acquire(); request->_tempFile = SD.open(filepath, FILE_WRITE); if (!request->_tempFile) { + sd_lock_release(); request->send(500, "text/plain", "Failed to create file on SD card"); return; } + sd_lock_release(); } if (len) @@ -237,11 +258,14 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, return; } - // Write data and verify bytes written + // Write data and verify bytes written (guard writes with simple mutex) + sd_lock_acquire(); size_t bytesWritten = request->_tempFile.write(data, len); if (bytesWritten != len) { + // ensure we close while holding the lock to keep SD state consistent request->_tempFile.close(); + sd_lock_release(); request->send(500, "text/plain", "Write error - SD card may be full"); return; } @@ -251,6 +275,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, { // Flush every 2KB request->_tempFile.flush(); } + sd_lock_release(); // Reduce logging frequency to save memory - log every 200KB instead of 100KB if (len && (index % 204800 == 0)) @@ -266,8 +291,10 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, { if (request->_tempFile) { + sd_lock_acquire(); request->_tempFile.flush(); // Ensure all data is written request->_tempFile.close(); + sd_lock_release(); logBuffer = "Upload Complete: "; logBuffer += filename; @@ -376,17 +403,13 @@ void playSongById(uint16_t id, uint32_t continueSeconds = 0) { Serial.println("Failed to play file: " + mp3File); currentNode = nullptr; - activateRFID(); - deactivateSD(); return; } if (continueSeconds != 0) { - audio.setAudioPlayPosition(continueSeconds); + audio.setAudioPlayPosition(continueSeconds); } - activateRFID(); - deactivateSD(); } void playSongByName(String song) @@ -428,13 +451,8 @@ void playSongByName(String song) { Serial.println("Failed to play file: " + mp3File); currentNode = nullptr; - activateRFID(); - deactivateSD(); return; } - - activateRFID(); - deactivateSD(); } void playSongByPath(String path) @@ -540,13 +558,10 @@ void playSongByRFID(String id) { Serial.println("Failed to play mapped file: " + mp3File); currentNode = nullptr; - activateRFID(); - deactivateSD(); + return; } - activateRFID(); - deactivateSD(); } /** @@ -564,8 +579,12 @@ bool playFile(const char *filename, uint32_t resumeFilePos) Serial.println("filename empty."); return false; } - // return audio.connecttoFS(filename, resumeFilePos); - return audio.connecttoFS(SD, filename, resumeFilePos); + // Serialize access to SD when audio opens the file (short critical section) + bool result = false; + sd_lock_acquire(); + result = audio.connecttoFS(SD, filename, resumeFilePos); + sd_lock_release(); + return result; } void playNextMp3() @@ -592,12 +611,16 @@ void playNextMp3() Serial.print("Advancing to "); String mp3File = currentNode->getCurrentPlayingFilePath(); + //FIXME crash here if last song. + if (mp3File.isEmpty()) { + + currentNode = rootNode.findFirstDirectoryWithMP3s(); + return; + } Serial.println(mp3File); deactivateRFID(); activateSD(); playFile(mp3File.c_str()); - activateRFID(); - deactivateSD(); } void audio_info(const char *info) @@ -991,8 +1014,6 @@ void previous() deactivateRFID(); activateSD(); playFile(currentNode->getCurrentPlayingFilePath().c_str()); - activateRFID(); - deactivateSD(); } } else @@ -1010,11 +1031,7 @@ void previous() Serial.println(*globalPrevSong); currentNode = globalPrevNode; stop(); - deactivateRFID(); - activateSD(); playFile(currentNode->getCurrentPlayingFilePath().c_str()); - activateRFID(); - deactivateSD(); } else { @@ -1070,8 +1087,6 @@ void audio_eof_mp3(const char *info) deactivateRFID(); activateSD(); playFile(currentNode->getCurrentPlayingFilePath().c_str()); - activateRFID(); - deactivateSD(); } else { @@ -1088,8 +1103,6 @@ void audio_eof_mp3(const char *info) deactivateRFID(); activateSD(); playFile(currentNode->getCurrentPlayingFilePath().c_str()); - activateRFID(); - deactivateSD(); } else { @@ -1238,6 +1251,8 @@ void setup() server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { webrequestActive = true; + deactivateRFID(); + activateSD(); String htmlPath = getSysDir("index.html"); if (SD.exists(htmlPath)) { @@ -1256,6 +1271,9 @@ void setup() 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)) { @@ -1264,7 +1282,7 @@ void setup() else { // Fallback: serve minimal CSS if file not found - request->send(404, "text/plain", "ERROR: /system/style.css ot found!"); + request->send(404, "text/plain", "ERROR: /system/style.css not found!"); } webrequestActive = false; }); @@ -1485,37 +1503,69 @@ void loop() else if (asyncNext) { asyncNext = false; - if (audio.isRunning()) + // If the play/start button is held, treat NEXT as volume up + if (playButtonDown) { - next(); + uint8_t vol = audio.getVolume(); + if (vol < config.maxVolume) + { + vol++; + audio.setVolume(vol); + volume = vol; // update stored volume for mute/unmute + } + // do not play the startup sound when changing volume while holding play } else { - uint8_t vol = audio.getVolume(); - if (vol != config.maxVolume) + if (audio.isRunning()) { - vol++; + next(); + } + else + { + uint8_t vol = audio.getVolume(); + if (vol != config.maxVolume) + { + vol++; + } + audio.setVolume(vol); + volume = vol; + playSongByPath(getSysDir(startup_sound)); } - audio.setVolume(vol); - playSongByPath(getSysDir(startup_sound)); } } else if (asyncPrev) { asyncPrev = false; - if (audio.isRunning()) + // If the play/start button is held, treat PREV as volume down + if (playButtonDown) { - previous(); + uint8_t vol = audio.getVolume(); + if (vol > 0) + { + vol--; + audio.setVolume(vol); + volume = vol; // update stored volume for mute/unmute + } + // do not play the startup sound when changing volume while holding play } else { - uint8_t vol = audio.getVolume(); - if (vol != 0) + if (audio.isRunning()) { - vol--; + previous(); + } + else + { + uint8_t vol = audio.getVolume(); + if (vol != 0) + { + vol--; + } + audio.setVolume(vol); + volume = vol; + playSongByPath(getSysDir(startup_sound)); } - audio.setVolume(vol); - playSongByPath(getSysDir(startup_sound)); } } @@ -1563,6 +1613,9 @@ void loop2(void *parameter) for (;;) { + // Track whether the play/start button is currently held down so the main loop + // can interpret NEXT/PREV as volume changes while play is held. + playButtonDown = (digitalRead(BTN_START_STOP) == LOW); if (buttonPressed(BTN_NEXT)) { diff --git a/web/script.js b/web/script.js index 80e0141..cbe2650 100644 --- a/web/script.js +++ b/web/script.js @@ -354,3 +354,93 @@ function deleteFileOnServer() { }; 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); +})();