[ai] Many (memory) improvements, cleanup script, still problems

This commit is contained in:
Stefan Ostermann 2025-10-06 23:13:26 +02:00
parent 083dfd6e2a
commit abfe564891
7 changed files with 190 additions and 194 deletions

2
.gitignore vendored
View File

@ -7,5 +7,5 @@
schema/hannabox/hannabox-backups/ schema/hannabox/hannabox-backups/
schema/hannabox/*.lck schema/hannabox/*.lck
.copilot .copilot
web/cleaned
.codegpt .codegpt

View File

@ -111,7 +111,7 @@ void DirectoryNode::buildDirectoryTree(const char *currentPath)
break; break;
} }
if (entry.isDirectory() && entry.name()[0] != '.' && strcmp(entry.name(), sys_dir.c_str())) if (entry.isDirectory() && entry.name()[0] != '.' && strcmp(entry.name(), sys_dir))
{ {
dirNames.push_back(String(entry.name())); dirNames.push_back(String(entry.name()));
} }
@ -533,50 +533,6 @@ DirectoryNode *DirectoryNode::advanceToNextMP3(const String *currentGlobal)
return this; return this;
} }
String DirectoryNode::getDirectoryStructureHTML() const {
// Calculate required size first (prevents reallocations)
size_t htmlSize = calculateHTMLSize();
String html;
html.reserve(htmlSize); // Precise allocation - no wasted RAM
// Helper lambda to append without temporary Strings
auto append = [&html](const __FlashStringHelper* fstr) {
html += fstr;
};
auto appendId = [&html](uint32_t id) {
html += id; // Direct numeric append (NO temporary String)
};
if (name == "/") {
append(F("<ul>\n"));
}
if (name != "/") {
append(F("<li data-id=\""));
appendId(id);
append(F("\"><b>"));
html += name; // Still uses String, but unavoidable for dynamic content
append(F("</b></li>\n"));
}
for (size_t i = 0; i < mp3Files.size(); i++) {
append(F("<li data-id=\""));
appendId(ids[i]);
append(F("\">"));
html += mp3Files[i]; // Dynamic file name
append(F("</li>\n"));
}
for (DirectoryNode* child : subdirectories) {
html += child->getDirectoryStructureHTML();
}
if (name == "/") {
append(F("</ul>\n"));
}
return html;
}
void DirectoryNode::streamDirectoryHTML(Print &out) const { void DirectoryNode::streamDirectoryHTML(Print &out) const {
if (name == "/") { if (name == "/") {
@ -616,33 +572,6 @@ void DirectoryNode::streamDirectoryHTML(Print &out) const {
} }
} }
// NEW: Calculate exact required size first
size_t DirectoryNode::calculateHTMLSize() const {
size_t size = 0;
// Opening/closing tags
if (name == "/") size += 6; // <ul>\n
// Current directory entry
if (name != "/") {
size += 22 + name.length() + 10; // <li...><b></b></li>\n + ID digits (est)
}
// MP3 files
for (size_t i = 0; i < mp3Files.size(); i++) {
size += 16 + mp3Files[i].length() + 10; // <li...></li>\n + ID digits
}
// Subdirectories
for (DirectoryNode* child : subdirectories) {
size += child->calculateHTMLSize();
}
// Closing tag
if (name == "/") size += 7; // </ul>\n
return size;
}
void DirectoryNode::appendIndentation(String &html, int level) const void DirectoryNode::appendIndentation(String &html, int level) const
{ {

View File

@ -55,9 +55,7 @@ public:
void buildFlatMP3List(std::vector<std::pair<DirectoryNode*, int>>& allMP3s); void buildFlatMP3List(std::vector<std::pair<DirectoryNode*, int>>& allMP3s);
DirectoryNode* advanceToMP3(const uint16_t id); DirectoryNode* advanceToMP3(const uint16_t id);
void advanceToFirstMP3InThisNode(); void advanceToFirstMP3InThisNode();
String getDirectoryStructureHTML() const;
void streamDirectoryHTML(Print &out) const; void streamDirectoryHTML(Print &out) const;
size_t calculateHTMLSize() const;
void appendIndentation(String& html, int level) const; void appendIndentation(String& html, int level) const;
DirectoryNode* findFirstDirectoryWithMP3s(); DirectoryNode* findFirstDirectoryWithMP3s();
String getCurrentPlayingFilePath() const; String getCurrentPlayingFilePath() const;

View File

@ -6,7 +6,15 @@
Config config; Config config;
const String getConfigFilePath() { const String getConfigFilePath() {
return "/" + sys_dir + "/config.txt"; static String config_dir;
if (config_dir.isEmpty()) {
config_dir.concat("/");
config_dir.concat(sys_dir);
config_dir.concat("/");
config_dir.concat(config_dir);
}
return config_dir;
} }
void setDefaultConfig() { void setDefaultConfig() {
@ -27,14 +35,14 @@ bool loadConfig() {
String configPath = getConfigFilePath(); String configPath = getConfigFilePath();
if (!SD.exists(configPath)) { if (!SD.exists(configPath)) {
Serial.println("Config file not found, using defaults"); Serial.println(F("Config file not found, using defaults"));
setDefaultConfig(); setDefaultConfig();
return saveConfig(); // Create config file with defaults return saveConfig(); // Create config file with defaults
} }
File file = SD.open(configPath, FILE_READ); File file = SD.open(configPath, FILE_READ);
if (!file) { if (!file) {
Serial.println("Failed to open config file"); Serial.println(F("Failed to open config file"));
setDefaultConfig(); setDefaultConfig();
return false; return false;
} }
@ -106,46 +114,45 @@ bool saveConfig() {
File file = SD.open(configPath, FILE_WRITE); File file = SD.open(configPath, FILE_WRITE);
if (!file) { if (!file) {
Serial.println("Failed to create config file"); Serial.println(F("Failed to create config file"));
return false; return false;
} }
// Write config file with comments for user reference // Write config file with comments for user reference
file.println("# HannaBox Configuration File"); file.println("# HannaBox Conf File");
file.println("# Values are in the format: key=value"); file.println("# format: key=value");
file.println("# Lines starting with # are comments");
file.println(""); file.println("");
file.println("# Audio Settings"); file.println("# Audio");
file.print("initialVolume="); file.println(config.initialVolume); file.print("initialVolume="); file.println(config.initialVolume);
file.print("maxVolume="); file.println(config.maxVolume); file.print("maxVolume="); file.println(config.maxVolume);
file.println(""); file.println("");
file.println("# Power Management (times in milliseconds)"); file.println("# Power Management (in milliseconds)");
file.print("sleepTime="); file.println(config.sleepTime); file.print("sleepTime="); file.println(config.sleepTime);
file.print("sleepDelay="); file.println(config.sleepDelay); file.print("sleepDelay="); file.println(config.sleepDelay);
file.print("sleepMessageDelay="); file.println(config.sleepMessageDelay); file.print("sleepMessageDelay="); file.println(config.sleepMessageDelay);
file.println(""); file.println("");
file.println("# Battery Settings (voltage in millivolts)"); file.println("# Battery (in millivolts)");
file.print("minVoltage="); file.println(config.minVoltage); file.print("minVoltage="); file.println(config.minVoltage);
file.print("voltage100Percent="); file.println(config.voltage100Percent); file.print("voltage100Percent="); file.println(config.voltage100Percent);
file.println(""); file.println("");
file.println("# RFID Settings"); file.println("# RFID");
file.print("rfidLoopInterval="); file.println(config.rfidLoopInterval); file.print("rfidLoopInterval="); file.println(config.rfidLoopInterval);
file.println(""); file.println("");
file.println("# Playback Settings"); file.println("# Playback");
file.print("startAtStoredProgress="); file.println(config.startAtStoredProgress ? "true" : "false"); file.print("startAtStoredProgress="); file.println(config.startAtStoredProgress ? "true" : "false");
file.println(""); file.println("");
file.println("# WiFi Settings (leave empty to use current WiFiManager)"); file.println("# WiFi (leave empty to use current WiFiManager)");
file.print("wifiSSID="); file.println(config.wifiSSID); file.print("wifiSSID="); file.println(config.wifiSSID);
file.print("wifiPassword="); file.println(config.wifiPassword); file.print("wifiPassword="); file.println(config.wifiPassword);
file.close(); file.close();
Serial.println("Config saved successfully"); Serial.println(F("Config saved successfully"));
return true; return true;
} }

View File

@ -1,24 +1,39 @@
#ifndef GLOBALS_H_ #ifndef GLOBALS_H_
#define GLOBALS_H_ #define GLOBALS_H_
static const char* sys_dir = "system";
static const char* sleep_sound = "sleep.mp3";
static const char* startup_sound = "start.mp3";
static const char* index_file = "index.html";
static const char* style_file = "style.css";
static const char* script_file = "script.js";
static const char* mapping_file = "mapping.txt";
static const char* progress_file = "progress.txt";
static const char* config_file = "config.txt";
static const char* txt_html_charset = "text/html; charset=UTF-8";
static const char* txt_plain = "text/plain; charset=UTF-8";
static const char* hdr_cache_control_key = "Cache-Control";
static const char* hdr_cache_control_val = "no-store";
static const char* hdr_connection_key = "Connection";
static const char* hdr_connection_val = "close";
const String sys_dir = "system";
const String sleep_sound = "sleep.mp3";
const String startup_sound = "start.mp3";
const String mapping_file = "mapping.txt";
const String progress_file = "progress.txt";
/* /*
const long sleepMessageDelay = 28000; const long sleepMessageDelay = 28000;

View File

@ -124,7 +124,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
// Validate filename and file extension // Validate filename and file extension
if (filename.length() == 0) if (filename.length() == 0)
{ {
request->send(400, "text/plain", "Invalid filename"); request->send(400, txt_plain, F("Invalid filename"));
return; return;
} }
@ -133,7 +133,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
if (!lowerFilename.endsWith(".mp3") && !lowerFilename.endsWith(".wav") && if (!lowerFilename.endsWith(".mp3") && !lowerFilename.endsWith(".wav") &&
!lowerFilename.endsWith(".m4a") && !lowerFilename.endsWith(".ogg")) !lowerFilename.endsWith(".m4a") && !lowerFilename.endsWith(".ogg"))
{ {
request->send(400, "text/plain", "Invalid file type. Only audio files are allowed."); request->send(400, txt_plain, F("Invalid file type. Only audio files are allowed."));
return; return;
} }
@ -142,7 +142,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
if (freeSpace < 10) if (freeSpace < 10)
{ // Less than 10MB free { // Less than 10MB free
request->send(507, "text/plain", "Insufficient storage"); request->send(507, txt_plain, F("Insufficient storage"));
return; return;
} }
@ -174,10 +174,10 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
if (counter >= 100) if (counter >= 100)
{ {
request->send(409, "text/plain", "Too many files with similar names"); request->send(409, txt_plain, F("Too many files w sim names"));
return; return;
} }
Serial.print("File exists, using: "); Serial.print(F("File exists, using: "));
Serial.println(filepath); Serial.println(filepath);
} }
@ -187,7 +187,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
if (!request->_tempFile) if (!request->_tempFile)
{ {
sd_lock_release(); sd_lock_release();
request->send(500, "text/plain", "Failed to create file on SD card"); request->send(500, txt_plain, F("Failed to create file"));
return; return;
} }
sd_lock_release(); sd_lock_release();
@ -198,7 +198,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
// Check if file handle is valid // Check if file handle is valid
if (!request->_tempFile) if (!request->_tempFile)
{ {
request->send(500, "text/plain", "File handle invalid"); request->send(500, txt_plain, F("File handle invalid"));
return; return;
} }
@ -210,7 +210,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
// ensure we close while holding the lock to keep SD state consistent // ensure we close while holding the lock to keep SD state consistent
request->_tempFile.close(); request->_tempFile.close();
sd_lock_release(); sd_lock_release();
request->send(500, "text/plain", "Write error - SD card may be full"); request->send(500, txt_plain, "Write error");
return; return;
} }
@ -252,11 +252,11 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
rootNode.buildDirectoryTree("/"); rootNode.buildDirectoryTree("/");
dir_lock_release(); dir_lock_release();
request->send(200, "text/plain", "Upload successful"); request->send(200, txt_plain, "Upload successful");
} }
else else
{ {
request->send(500, "text/plain", "Upload failed - file handle was invalid"); request->send(500, txt_plain, "Upload failed");
} }
} }
} }
@ -278,12 +278,12 @@ void handleMoveFile(AsyncWebServerRequest *request)
dir_lock_acquire(); dir_lock_acquire();
rootNode.buildDirectoryTree("/"); rootNode.buildDirectoryTree("/");
dir_lock_release(); dir_lock_release();
request->send(200, "text/plain", "File moved successfully."); request->send(200, txt_plain, F("File moved successfully."));
} }
else else
{ {
Serial.println("File not found: " + from); Serial.println("File not found: " + from);
request->send(404, "text/plain", "File not found."); request->send(404, txt_plain, "File not found.");
} }
} }
@ -303,12 +303,12 @@ void handleDeleteFile(AsyncWebServerRequest *request)
dir_lock_acquire(); dir_lock_acquire();
rootNode.buildDirectoryTree("/"); rootNode.buildDirectoryTree("/");
dir_lock_release(); dir_lock_release();
request->send(200, "text/plain", "File deleted successfully."); request->send(200, txt_plain, "File deleted.");
} }
else else
{ {
Serial.println("File not found: " + filename); Serial.println("File not found: " + filename);
request->send(404, "text/plain", "File not found."); request->send(404, txt_plain, "File not found.");
} }
} }
@ -372,14 +372,15 @@ void playSongByName(String song)
{ {
if (song.length() == 0) if (song.length() == 0)
{ {
Serial.println("Empty song name provided"); Serial.println(F("Empty song name provided"));
return; return;
} }
currentNode = rootNode.advanceToMP3(&song); currentNode = rootNode.advanceToMP3(&song);
if (currentNode == nullptr) if (currentNode == nullptr)
{ {
Serial.println("No node found for song: " + song); Serial.print(F("No node found for song: "));
Serial.println(song);
return; return;
} }
@ -387,7 +388,8 @@ void playSongByName(String song)
if (currentNode->getCurrentPlaying() == nullptr) if (currentNode->getCurrentPlaying() == nullptr)
{ {
currentNode = nullptr; currentNode = nullptr;
Serial.println("No song found for name: " + song); Serial.print(F("No song found for name: "));
Serial.println(song);
return; return;
} }
@ -395,7 +397,8 @@ void playSongByName(String song)
if (mp3File.length() == 0) if (mp3File.length() == 0)
{ {
currentNode = nullptr; currentNode = nullptr;
Serial.println("Empty file path for song: " + song); Serial.print(F("Empty file path for song: "));
Serial.println(song);
return; return;
} }
@ -420,7 +423,7 @@ void playSongByRFID(String id)
{ {
if (id.length() == 0) if (id.length() == 0)
{ {
Serial.println("Empty RFID ID provided"); Serial.println(F("Empty RFID ID provided"));
return; return;
} }
@ -438,7 +441,7 @@ void playSongByRFID(String id)
return; return;
} }
Serial.print("RFID mapping found. Target: "); Serial.print(F("RFID mapping found. Target: "));
Serial.print(entry.target); Serial.print(entry.target);
Serial.print(" Mode: "); Serial.print(" Mode: ");
Serial.println(entry.mode); Serial.println(entry.mode);
@ -456,14 +459,16 @@ void playSongByRFID(String id)
currentNode = rootNode.advanceToMP3(&entry.target); currentNode = rootNode.advanceToMP3(&entry.target);
if (currentNode == nullptr || currentNode->getCurrentPlaying() == nullptr) if (currentNode == nullptr || currentNode->getCurrentPlaying() == nullptr)
{ {
Serial.println("No node/file found for mapping target: " + entry.target); Serial.print(F("No node/file found for mapping target: "));
Serial.println(entry.target);
return; return;
} }
String mp3File = currentNode->getCurrentPlayingFilePath(); String mp3File = currentNode->getCurrentPlayingFilePath();
if (mp3File.length() == 0) if (mp3File.length() == 0)
{ {
Serial.println("Empty file path for mapping target: " + entry.target); Serial.print(F("Empty file path for mapping target: "));
Serial.println(entry.target);
return; return;
} }
@ -716,8 +721,11 @@ boolean readSongProgress(const char *filename)
currentSongId = (uint16_t)tempId; currentSongId = (uint16_t)tempId;
currentSongSeconds = (uint32_t)tempSeconds; currentSongSeconds = (uint32_t)tempSeconds;
#ifdef DEBUG
Serial.println("Data read from file: " + data); Serial.println("Data read from file: " + data);
Serial.println("Parsed ID: " + String(currentSongId) + ", s: " + String(currentSongSeconds)); Serial.println("Parsed ID: " + String(currentSongId) + ", s: " + String(currentSongSeconds));
#endif
return true; return true;
} }
@ -727,11 +735,9 @@ String getState()
static DynamicJsonDocument jsonState(512); static DynamicJsonDocument jsonState(512);
static String output; static String output;
output.reserve(512); // Pre-allocate string buffer
output.clear(); output.clear();
output.reserve(512); // Pre-allocate string buffer
jsonState.clear(); // Clear previous data
jsonState["playing"] = audio.isRunning(); jsonState["playing"] = audio.isRunning();
if (currentNode != nullptr) if (currentNode != nullptr)
@ -752,6 +758,7 @@ String getState()
jsonState["heap"] = free_heap; jsonState["heap"] = free_heap;
serializeJson(jsonState, output); serializeJson(jsonState, output);
jsonState.clear();
return output; return output;
} }
@ -772,11 +779,11 @@ void saveMappingToFile(const String filename)
file.println(pair.second.mode); file.println(pair.second.mode);
} }
file.close(); file.close();
Serial.println("Mapping saved to file."); Serial.println(F("Mapping saved to file."));
} }
else else
{ {
Serial.println("Error opening file for writing."); Serial.println(F("Error opening file for writing."));
} }
} }
@ -802,11 +809,11 @@ void editMapping(AsyncWebServerRequest *request)
rfid_map[rfid] = MappingEntry(song, mode); rfid_map[rfid] = MappingEntry(song, mode);
saveMappingToFile(getSysDir(mapping_file)); saveMappingToFile(getSysDir(mapping_file));
request->send(200, "text/plain", "Mapping updated"); request->send(200, txt_plain, "Mapping updated");
} }
else else
{ {
request->send(400, "text/plain", "Invalid parameters"); request->send(400, txt_plain, "Invalid parameters");
} }
} }
@ -842,8 +849,9 @@ void readDataFromFile(String filename)
if (mstr.length() > 0) if (mstr.length() > 0)
mode = mstr.charAt(0); mode = mstr.charAt(0);
} }
#ifdef DEBUG
Serial.println("found rfid mapping for " + target + " mode " + String(mode)); Serial.println("found rfid mapping for " + target + " mode " + String(mode));
#endif
// Add key-value pair to the map // Add key-value pair to the map
rfid_map[key] = MappingEntry(target, mode); rfid_map[key] = MappingEntry(target, mode);
} }
@ -852,20 +860,13 @@ void readDataFromFile(String filename)
} }
else else
{ {
Serial.print("Error opening file "); Serial.print(F("Error opening file "));
Serial.println(filename); Serial.println(filename);
} }
} }
String processor(const String &var) String processor(const String &var)
{ {
if (var == "DIRECTORY")
{
dir_lock_acquire();
String out = rootNode.getDirectoryStructureHTML();
dir_lock_release();
return out;
}
if (var == "MAPPING") if (var == "MAPPING")
{ {
@ -903,9 +904,9 @@ String processor(const String &var)
mappingVal += "|"; mappingVal += "|";
mappingVal += pair.second.mode; mappingVal += pair.second.mode;
html.concat(htmlEscape(mappingVal)); html.concat(htmlEscape(mappingVal));
html.concat("</td></tr>"); html.concat(F("</td></tr>"));
} }
html.concat("</table>"); html.concat(F("</table>"));
return html; return html;
} }
@ -961,7 +962,7 @@ void previous()
lastInteraction = millis(); lastInteraction = millis();
if (currentNode == NULL) if (currentNode == NULL)
{ {
Serial.println("previous(): currentNode is null"); Serial.println(F("previous(): currentNode is null"));
return; return;
} }
@ -970,7 +971,9 @@ void previous()
const String *currentSong = currentNode->getCurrentPlaying(); const String *currentSong = currentNode->getCurrentPlaying();
if (currentSong == NULL) if (currentSong == NULL)
{ {
Serial.println("previous(): currentPlaying is null, cannot go to previous"); #ifdef DEBUG
Serial.println(F("previous(): currentPlaying is null, cannot go to previous"));
#endif
return; return;
} }
@ -1013,7 +1016,9 @@ void previous()
else else
{ {
// Need to find previous song globally (across directories) // Need to find previous song globally (across directories)
Serial.println("previous(): Looking for previous song globally"); #ifdef DEBUG
Serial.println(F("previous(): Looking for previous song globally"));
#endif
DirectoryNode *globalPrevNode = rootNode.findPreviousMP3Globally(currentSong); DirectoryNode *globalPrevNode = rootNode.findPreviousMP3Globally(currentSong);
if (globalPrevNode != NULL) if (globalPrevNode != NULL)
@ -1021,20 +1026,24 @@ void previous()
const String *globalPrevSong = globalPrevNode->getCurrentPlaying(); const String *globalPrevSong = globalPrevNode->getCurrentPlaying();
if (globalPrevSong != NULL) if (globalPrevSong != NULL)
{ {
Serial.print("previous(): Found previous song globally: "); Serial.print(F("previous(): Found previous song globally: "));
Serial.println(*globalPrevSong); Serial.println(*globalPrevSong);
currentNode = globalPrevNode; currentNode = globalPrevNode;
stop(); stop();
playFile(currentNode->getCurrentPlayingFilePath().c_str()); playFile(currentNode->getCurrentPlayingFilePath().c_str());
} }
#ifdef DEBUG
else else
{ {
Serial.println("prev: Global previous song is null"); Serial.println(F("prev: Global previous song is null"));
} }
#endif
} }
else else
{ {
Serial.println("prev: No previous song found, beginning again"); #ifdef DEBUG
Serial.println(F("prev: No previous song found, beginning again"));
#endif
// Optionally restart current song or do nothing // Optionally restart current song or do nothing
audio.setAudioPlayPosition(0); audio.setAudioPlayPosition(0);
currentNode->setSecondsPlayed(0); currentNode->setSecondsPlayed(0);
@ -1150,17 +1159,26 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); });
deactivateRFID(); deactivateRFID();
activateSD(); activateSD();
String htmlPath = getSysDir("index.html"); String htmlPath = getSysDir(index_file);
if (SD.exists(htmlPath)) if (SD.exists(htmlPath))
{ {
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html"); uint32_t fsize = 0;
response->addHeader("Cache-Control", "no-store"); {
File f = SD.open(htmlPath);
if (f) { fsize = f.size(); f.close(); }
}
#ifdef DEBUG
Serial.printf("Serving %s size=%u heap=%u webreq_cnt=%u\n", htmlPath.c_str(), (unsigned)fsize, (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt);
#endif
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, txt_html_charset);
response->addHeader(hdr_cache_control_key, hdr_cache_control_val);
response->addHeader(hdr_connection_key, hdr_connection_val);
request->send(response); request->send(response);
} }
else else
{ {
// Fallback: serve minimal error if file not found // Fallback: serve minimal error if file not found
request->send(404, "text/plain", "ERROR: /system/index.html not found!"); request->send(404, txt_plain, F("ERROR: /system/index.html not found!"));
} }
}); });
@ -1172,19 +1190,28 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
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.css"); String cssPath = getSysDir(style_file);
if (SD.exists(cssPath)) if (SD.exists(cssPath))
{ {
uint32_t fsize = 0;
{ {
AsyncWebServerResponse *resp = request->beginResponse(SD, cssPath, "text/css"); File f = SD.open(cssPath);
resp->addHeader("Cache-Control", "public, max-age=300"); if (f) { fsize = f.size(); f.close(); }
}
#ifdef DEBUG
Serial.printf("Serving %s size=%u heap=%u webreq_cnt=%u\n", cssPath.c_str(), (unsigned)fsize, (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt);
#endif
{
AsyncWebServerResponse *resp = request->beginResponse(SD, cssPath, F("text/css"));
resp->addHeader(hdr_cache_control_key, F("public, max-age=300"));
resp->addHeader(hdr_connection_key, hdr_connection_val);
request->send(resp); request->send(resp);
} }
} }
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 not found!"); request->send(404, txt_plain, F("ERROR: /system/style.css not found!"));
} }
}); });
@ -1195,19 +1222,28 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); });
deactivateRFID(); deactivateRFID();
activateSD(); activateSD();
String jsPath = getSysDir("script.js"); String jsPath = getSysDir(script_file);
if (SD.exists(jsPath)) if (SD.exists(jsPath))
{ {
uint32_t fsize = 0;
{ {
AsyncWebServerResponse *resp = request->beginResponse(SD, jsPath, "application/javascript"); File f = SD.open(jsPath);
resp->addHeader("Cache-Control", "public, max-age=300"); if (f) { fsize = f.size(); f.close(); }
}
#ifdef DEBUG
Serial.printf("Serving %s size=%u heap=%u webreq_cnt=%u\n", jsPath.c_str(), (unsigned)fsize, (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt);
#endif
{
AsyncWebServerResponse *resp = request->beginResponse(SD, jsPath, F("application/javascript"));
resp->addHeader(hdr_cache_control_key, F("public, max-age=300"));
resp->addHeader(hdr_connection_key, hdr_connection_val);
request->send(resp); request->send(resp);
} }
} }
else else
{ {
// Fallback: serve minimal JS if file not found // Fallback: serve minimal JS if file not found
request->send(404, "text/plain", "ERROR: /system/script.js not found!"); request->send(404, txt_plain, F("ERROR: /system/script.js not found!"));
} }
}); });
@ -1218,9 +1254,12 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); });
// 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("text/html; charset=UTF-8"); AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset);
stream->addHeader("Cache-Control", "no-store"); #ifdef DEBUG
stream->addHeader("Connection", "close"); Serial.printf("Serving /directory heap=%u webreq_cnt=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt);
#endif
stream->addHeader(hdr_cache_control_key, hdr_cache_control_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(); dir_lock_acquire();
rootNode.streamDirectoryHTML(*stream); rootNode.streamDirectoryHTML(*stream);
@ -1233,9 +1272,12 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); });
// 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("text/html; charset=UTF-8"); AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset);
stream->addHeader("Cache-Control", "no-store"); #ifdef DEBUG
stream->addHeader("Connection", "close"); Serial.printf("Serving /mapping heap=%u webreq_cnt=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt);
#endif
stream->addHeader(hdr_cache_control_key, hdr_cache_control_val);
stream->addHeader(hdr_connection_key, hdr_connection_val);
String html = processor(String("MAPPING")); String html = processor(String("MAPPING"));
stream->print(html); stream->print(html);
request->send(stream); request->send(stream);
@ -1246,44 +1288,48 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); });
String state = getState(); String state = getState();
AsyncWebServerResponse* resp = request->beginResponse(200, "application/json; charset=UTF-8", state); #ifdef DEBUG
resp->addHeader("Cache-Control", "no-store"); Serial.printf("Serving /state heap=%u webreq_cnt=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt);
resp->addHeader("Connection", "close"); #endif
AsyncWebServerResponse* resp = request->beginResponse(200, F("application/json; charset=UTF-8"), state);
resp->addHeader(hdr_cache_control_key, hdr_cache_control_val);
resp->addHeader(hdr_connection_key, hdr_connection_val);
request->send(resp); }); request->send(resp); });
server.on("/start", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/start", HTTP_GET, [](AsyncWebServerRequest *request)
{ {
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain; charset=UTF-8", "start"); request->send(200, txt_plain, "start");
start(); }); start(); });
server.on("/toggleplaypause", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/toggleplaypause", HTTP_GET, [](AsyncWebServerRequest *request)
{ {
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain; charset=UTF-8", "toggleplaypause"); request->send(200, txt_plain, "toggleplaypause");
togglePlayPause(); }); togglePlayPause(); });
server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request)
{ {
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain", "stop"); request->send(200, txt_plain, "stop");
stop(); }); stop(); });
server.on("/next", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/next", HTTP_GET, [](AsyncWebServerRequest *request)
{ {
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain", "next"); request->send(200, txt_plain, "next");
next(); }); next(); });
server.on("/previous", HTTP_GET, [](AsyncWebServerRequest *request) server.on("/previous", HTTP_GET, [](AsyncWebServerRequest *request)
{ {
webreq_enter(); webreq_enter();
request->onDisconnect([](){ webreq_exit(); }); request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain", "previous"); request->send(200, txt_plain, "previous");
previous(); }); previous(); });
server.on("/playbyid", HTTP_GET, id_song_action); server.on("/playbyid", HTTP_GET, id_song_action);
@ -1327,9 +1373,9 @@ void setup()
RFIDActive = false; RFIDActive = false;
SDActive = false; SDActive = false;
Serial.print("Initializing SD card..."); Serial.print(F("Initializing SD card..."));
activateSD(); activateSD();
Serial.println("SD initialization done."); Serial.println(F("SD initialization done."));
// Seed RNG for shuffle mode // Seed RNG for shuffle mode
#if defined(ESP32) #if defined(ESP32)
@ -1339,7 +1385,7 @@ void setup()
#endif #endif
// Load configuration from SD card // Load configuration from SD card
Serial.println("Loading configuration..."); Serial.println(F("Loading configuration..."));
loadConfig(); loadConfig();
// deep sleep wakeup // deep sleep wakeup
@ -1406,7 +1452,9 @@ void setup()
wifiManager.setConnectTimeout(15); // Faster connection attempts wifiManager.setConnectTimeout(15); // Faster connection attempts
wifiManager.setConfigPortalTimeout(60); // Shorter portal timeout wifiManager.setConfigPortalTimeout(60); // Shorter portal timeout
Serial.println("Deactivating Brownout detector..."); #ifdef DEBUG
Serial.println(F("Deactivating Brownout detector..."));
#endif
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // disable brownout detector WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // disable brownout detector
// wifiManager.resetSettings(); // wifiManager.resetSettings();
@ -1419,10 +1467,10 @@ void setup()
} }
else else
{ {
Serial.println("Wifi timed out. Fallback."); Serial.println(F("Wifi timed out. Fallback."));
} }
Serial.println("Activating Brownout detector..."); Serial.println(F("Activating Brownout detector..."));
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); // enable brownout detector WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 1); // enable brownout detector
xTaskCreatePinnedToCore( xTaskCreatePinnedToCore(
@ -1452,7 +1500,7 @@ void id_song_action(AsyncWebServerRequest *request)
} }
} }
lastInteraction = millis(); lastInteraction = millis();
request->send_P(200, "text/plain", "ok"); request->send_P(200, txt_plain, "ok");
} }
void progress_action(AsyncWebServerRequest *request) void progress_action(AsyncWebServerRequest *request)
@ -1470,7 +1518,7 @@ void progress_action(AsyncWebServerRequest *request)
} }
} }
lastInteraction = millis(); lastInteraction = millis();
request->send_P(200, "text/plain", "ok"); request->send_P(200, txt_plain, "ok");
} }
void volume_action(AsyncWebServerRequest *request) void volume_action(AsyncWebServerRequest *request)
@ -1488,7 +1536,7 @@ void volume_action(AsyncWebServerRequest *request)
} }
} }
lastInteraction = millis(); lastInteraction = millis();
request->send_P(200, "text/plain", "ok"); request->send_P(200, txt_plain, "ok");
} }
const String getSysDir(const String filename) const String getSysDir(const String filename)
@ -1505,7 +1553,7 @@ const String getSysDir(const String filename)
void loop() void loop()
{ {
if (webreq_cnt > 0 && webrequest_blockings > MAX_WEBREQUEST_BLOCKINGS) { if (webreq_cnt > 0 && webrequest_blockings > MAX_WEBREQUEST_BLOCKINGS) {
Serial.println("excessive webrequest blocking - suppress reset"); Serial.println(F("excessive webrequest blocking - suppress reset"));
// Avoid resetting server mid-response to prevent mixing headers/body or truncation // Avoid resetting server mid-response to prevent mixing headers/body or truncation
webreq_cnt = 0; webreq_cnt = 0;
webrequest_blockings = 0; webrequest_blockings = 0;
@ -1574,7 +1622,7 @@ void loop()
if (now - lastInteraction > config.sleepDelay) if (now - lastInteraction > config.sleepDelay)
{ {
Serial.println("entering deep sleep.."); Serial.println(F("entering deep sleep.."));
deactivateRFID(); deactivateRFID();
deactivateSD(); deactivateSD();
esp_deep_sleep_start(); esp_deep_sleep_start();
@ -1676,7 +1724,7 @@ void loop()
{ {
if (voltage_threshold_counter > 3) if (voltage_threshold_counter > 3)
{ {
Serial.println("deep sleep due to low volts.."); Serial.println(F("deep sleep due to low volts.."));
lastInteraction = millis() - config.sleepMessageDelay; lastInteraction = millis() - config.sleepMessageDelay;
voltage_threshold_counter = 0; voltage_threshold_counter = 0;
} }

View File

@ -245,7 +245,7 @@ function displayState(state) {
var uidEl = document.getElementById("uid"); var uidEl = document.getElementById("uid");
if (uidEl) uidEl.innerHTML = 'Last NFC ID: ' + (state['uid'] || ''); if (uidEl) uidEl.innerHTML = 'Last NFC ID: ' + (state['uid'] || '');
/* ==== Autofill convenience fields ==== */ /* Autofill convenience fields */
var fm = document.getElementById('fileManager'); var fm = document.getElementById('fileManager');
if (state['filepath'] && fm && fm.style.display == 'none') { if (state['filepath'] && fm && fm.style.display == 'none') {
var moveFrom = document.getElementById('moveFrom'); var moveFrom = document.getElementById('moveFrom');
@ -301,7 +301,6 @@ function playNamedSong(song) {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("POST", "/playnamed"); xhr.open("POST", "/playnamed");
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
//application/x-www-form-urlencoded
var body = song; var body = song;
xhr.send("title="+encodeURIComponent(body)); xhr.send("title="+encodeURIComponent(body));
} }
@ -327,8 +326,8 @@ 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', 'audio/flac', 'audio/mp4', 'audio/ogg']; var allowedTypes = ['audio/mpeg', 'audio/wav'];
var allowedExtensions = ['.mp3', '.wav', '.flac', '.m4a', '.ogg']; var allowedExtensions = ['.mp3'];
if (file.size > maxSize) { if (file.size > maxSize) {
return 'File too large. Maximum size is 50MB.'; return 'File too large. Maximum size is 50MB.';
@ -338,7 +337,7 @@ function validateFile(file) {
var hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext)); var hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext));
if (!hasValidExtension) { if (!hasValidExtension) {
return 'Invalid file type. Only audio files (.mp3, .wav, .flac, .m4a, .ogg) are allowed.'; return 'Invalid file type';
} }
return null; // No error return null; // No error
@ -443,7 +442,7 @@ function resetUploadForm() {
document.getElementById('uploadFile').value = ''; document.getElementById('uploadFile').value = '';
} }
/* ================= File Manager Functions ================= */ /* File Manager Functions */
function toggleFileManager() { function toggleFileManager() {
var fm = document.getElementById('fileManager'); var fm = document.getElementById('fileManager');