[ai] Fixed CSS not loaded error, volume via buttons,...

This commit is contained in:
Stefan Ostermann 2025-08-17 20:15:51 +02:00
parent 02cd5da886
commit b8e1263fb3
2 changed files with 188 additions and 45 deletions

View File

@ -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,8 +403,6 @@ void playSongById(uint16_t id, uint32_t continueSeconds = 0)
{
Serial.println("Failed to play file: " + mp3File);
currentNode = nullptr;
activateRFID();
deactivateSD();
return;
}
@ -385,8 +410,6 @@ void playSongById(uint16_t id, uint32_t continueSeconds = 0)
{
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,6 +1503,20 @@ void loop()
else if (asyncNext)
{
asyncNext = false;
// If the play/start button is held, treat NEXT as volume up
if (playButtonDown)
{
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
{
if (audio.isRunning())
{
next();
@ -1497,12 +1529,28 @@ void loop()
vol++;
}
audio.setVolume(vol);
volume = vol;
playSongByPath(getSysDir(startup_sound));
}
}
}
else if (asyncPrev)
{
asyncPrev = false;
// If the play/start button is held, treat PREV as volume down
if (playButtonDown)
{
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
{
if (audio.isRunning())
{
previous();
@ -1515,9 +1563,11 @@ void loop()
vol--;
}
audio.setVolume(vol);
volume = vol;
playSongByPath(getSysDir(startup_sound));
}
}
}
if (loopCounter % config.rfidLoopInterval == 0 && !webrequestActive)
{
@ -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))
{

View File

@ -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);
})();