[ai] Fixed CSS not loaded error, volume via buttons,...
This commit is contained in:
parent
02cd5da886
commit
b8e1263fb3
143
src/main.cpp
143
src/main.cpp
|
|
@ -77,6 +77,24 @@ DirectoryNode rootNode("/");
|
||||||
DirectoryNode *currentNode = nullptr;
|
DirectoryNode *currentNode = nullptr;
|
||||||
|
|
||||||
volatile bool newRfidInt = false;
|
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.
|
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);
|
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);
|
request->_tempFile = SD.open(filepath, FILE_WRITE);
|
||||||
if (!request->_tempFile)
|
if (!request->_tempFile)
|
||||||
{
|
{
|
||||||
|
sd_lock_release();
|
||||||
request->send(500, "text/plain", "Failed to create file on SD card");
|
request->send(500, "text/plain", "Failed to create file on SD card");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
sd_lock_release();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len)
|
if (len)
|
||||||
|
|
@ -237,11 +258,14 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
|
||||||
return;
|
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);
|
size_t bytesWritten = request->_tempFile.write(data, len);
|
||||||
if (bytesWritten != len)
|
if (bytesWritten != len)
|
||||||
{
|
{
|
||||||
|
// ensure we close while holding the lock to keep SD state consistent
|
||||||
request->_tempFile.close();
|
request->_tempFile.close();
|
||||||
|
sd_lock_release();
|
||||||
request->send(500, "text/plain", "Write error - SD card may be full");
|
request->send(500, "text/plain", "Write error - SD card may be full");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -251,6 +275,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
|
||||||
{ // Flush every 2KB
|
{ // Flush every 2KB
|
||||||
request->_tempFile.flush();
|
request->_tempFile.flush();
|
||||||
}
|
}
|
||||||
|
sd_lock_release();
|
||||||
|
|
||||||
// Reduce logging frequency to save memory - log every 200KB instead of 100KB
|
// Reduce logging frequency to save memory - log every 200KB instead of 100KB
|
||||||
if (len && (index % 204800 == 0))
|
if (len && (index % 204800 == 0))
|
||||||
|
|
@ -266,8 +291,10 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
|
||||||
{
|
{
|
||||||
if (request->_tempFile)
|
if (request->_tempFile)
|
||||||
{
|
{
|
||||||
|
sd_lock_acquire();
|
||||||
request->_tempFile.flush(); // Ensure all data is written
|
request->_tempFile.flush(); // Ensure all data is written
|
||||||
request->_tempFile.close();
|
request->_tempFile.close();
|
||||||
|
sd_lock_release();
|
||||||
|
|
||||||
logBuffer = "Upload Complete: ";
|
logBuffer = "Upload Complete: ";
|
||||||
logBuffer += filename;
|
logBuffer += filename;
|
||||||
|
|
@ -376,17 +403,13 @@ void playSongById(uint16_t id, uint32_t continueSeconds = 0)
|
||||||
{
|
{
|
||||||
Serial.println("Failed to play file: " + mp3File);
|
Serial.println("Failed to play file: " + mp3File);
|
||||||
currentNode = nullptr;
|
currentNode = nullptr;
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (continueSeconds != 0)
|
if (continueSeconds != 0)
|
||||||
{
|
{
|
||||||
audio.setAudioPlayPosition(continueSeconds);
|
audio.setAudioPlayPosition(continueSeconds);
|
||||||
}
|
}
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void playSongByName(String song)
|
void playSongByName(String song)
|
||||||
|
|
@ -428,13 +451,8 @@ void playSongByName(String song)
|
||||||
{
|
{
|
||||||
Serial.println("Failed to play file: " + mp3File);
|
Serial.println("Failed to play file: " + mp3File);
|
||||||
currentNode = nullptr;
|
currentNode = nullptr;
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void playSongByPath(String path)
|
void playSongByPath(String path)
|
||||||
|
|
@ -540,13 +558,10 @@ void playSongByRFID(String id)
|
||||||
{
|
{
|
||||||
Serial.println("Failed to play mapped file: " + mp3File);
|
Serial.println("Failed to play mapped file: " + mp3File);
|
||||||
currentNode = nullptr;
|
currentNode = nullptr;
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -564,8 +579,12 @@ bool playFile(const char *filename, uint32_t resumeFilePos)
|
||||||
Serial.println("filename empty.");
|
Serial.println("filename empty.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// return audio.connecttoFS(filename, resumeFilePos);
|
// Serialize access to SD when audio opens the file (short critical section)
|
||||||
return audio.connecttoFS(SD, filename, resumeFilePos);
|
bool result = false;
|
||||||
|
sd_lock_acquire();
|
||||||
|
result = audio.connecttoFS(SD, filename, resumeFilePos);
|
||||||
|
sd_lock_release();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void playNextMp3()
|
void playNextMp3()
|
||||||
|
|
@ -592,12 +611,16 @@ void playNextMp3()
|
||||||
|
|
||||||
Serial.print("Advancing to ");
|
Serial.print("Advancing to ");
|
||||||
String mp3File = currentNode->getCurrentPlayingFilePath();
|
String mp3File = currentNode->getCurrentPlayingFilePath();
|
||||||
|
//FIXME crash here if last song.
|
||||||
|
if (mp3File.isEmpty()) {
|
||||||
|
|
||||||
|
currentNode = rootNode.findFirstDirectoryWithMP3s();
|
||||||
|
return;
|
||||||
|
}
|
||||||
Serial.println(mp3File);
|
Serial.println(mp3File);
|
||||||
deactivateRFID();
|
deactivateRFID();
|
||||||
activateSD();
|
activateSD();
|
||||||
playFile(mp3File.c_str());
|
playFile(mp3File.c_str());
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void audio_info(const char *info)
|
void audio_info(const char *info)
|
||||||
|
|
@ -991,8 +1014,6 @@ void previous()
|
||||||
deactivateRFID();
|
deactivateRFID();
|
||||||
activateSD();
|
activateSD();
|
||||||
playFile(currentNode->getCurrentPlayingFilePath().c_str());
|
playFile(currentNode->getCurrentPlayingFilePath().c_str());
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -1010,11 +1031,7 @@ void previous()
|
||||||
Serial.println(*globalPrevSong);
|
Serial.println(*globalPrevSong);
|
||||||
currentNode = globalPrevNode;
|
currentNode = globalPrevNode;
|
||||||
stop();
|
stop();
|
||||||
deactivateRFID();
|
|
||||||
activateSD();
|
|
||||||
playFile(currentNode->getCurrentPlayingFilePath().c_str());
|
playFile(currentNode->getCurrentPlayingFilePath().c_str());
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1070,8 +1087,6 @@ void audio_eof_mp3(const char *info)
|
||||||
deactivateRFID();
|
deactivateRFID();
|
||||||
activateSD();
|
activateSD();
|
||||||
playFile(currentNode->getCurrentPlayingFilePath().c_str());
|
playFile(currentNode->getCurrentPlayingFilePath().c_str());
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1088,8 +1103,6 @@ void audio_eof_mp3(const char *info)
|
||||||
deactivateRFID();
|
deactivateRFID();
|
||||||
activateSD();
|
activateSD();
|
||||||
playFile(currentNode->getCurrentPlayingFilePath().c_str());
|
playFile(currentNode->getCurrentPlayingFilePath().c_str());
|
||||||
activateRFID();
|
|
||||||
deactivateSD();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1238,6 +1251,8 @@ void setup()
|
||||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
webrequestActive = true;
|
webrequestActive = true;
|
||||||
|
deactivateRFID();
|
||||||
|
activateSD();
|
||||||
String htmlPath = getSysDir("index.html");
|
String htmlPath = getSysDir("index.html");
|
||||||
if (SD.exists(htmlPath))
|
if (SD.exists(htmlPath))
|
||||||
{
|
{
|
||||||
|
|
@ -1256,6 +1271,9 @@ void setup()
|
||||||
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request)
|
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request)
|
||||||
{
|
{
|
||||||
webrequestActive = true;
|
webrequestActive = true;
|
||||||
|
deactivateRFID();
|
||||||
|
activateSD();
|
||||||
|
// Ensure SD is active and RFID is deactivated while serving files.
|
||||||
String cssPath = getSysDir("style.css");
|
String cssPath = getSysDir("style.css");
|
||||||
if (SD.exists(cssPath))
|
if (SD.exists(cssPath))
|
||||||
{
|
{
|
||||||
|
|
@ -1264,7 +1282,7 @@ void setup()
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Fallback: serve minimal CSS if file not found
|
// 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;
|
webrequestActive = false;
|
||||||
});
|
});
|
||||||
|
|
@ -1485,37 +1503,69 @@ void loop()
|
||||||
else if (asyncNext)
|
else if (asyncNext)
|
||||||
{
|
{
|
||||||
asyncNext = false;
|
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
|
else
|
||||||
{
|
{
|
||||||
uint8_t vol = audio.getVolume();
|
if (audio.isRunning())
|
||||||
if (vol != config.maxVolume)
|
|
||||||
{
|
{
|
||||||
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)
|
else if (asyncPrev)
|
||||||
{
|
{
|
||||||
asyncPrev = false;
|
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
|
else
|
||||||
{
|
{
|
||||||
uint8_t vol = audio.getVolume();
|
if (audio.isRunning())
|
||||||
if (vol != 0)
|
|
||||||
{
|
{
|
||||||
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 (;;)
|
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))
|
if (buttonPressed(BTN_NEXT))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -354,3 +354,93 @@ 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