[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/*.lck
.copilot
web/cleaned
.codegpt

View File

@ -111,7 +111,7 @@ void DirectoryNode::buildDirectoryTree(const char *currentPath)
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()));
}
@ -533,50 +533,6 @@ DirectoryNode *DirectoryNode::advanceToNextMP3(const String *currentGlobal)
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 {
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
{

View File

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

View File

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

View File

@ -1,24 +1,39 @@
#ifndef 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;

View File

@ -124,7 +124,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
// Validate filename and file extension
if (filename.length() == 0)
{
request->send(400, "text/plain", "Invalid filename");
request->send(400, txt_plain, F("Invalid filename"));
return;
}
@ -133,7 +133,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
if (!lowerFilename.endsWith(".mp3") && !lowerFilename.endsWith(".wav") &&
!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;
}
@ -142,7 +142,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
if (freeSpace < 10)
{ // Less than 10MB free
request->send(507, "text/plain", "Insufficient storage");
request->send(507, txt_plain, F("Insufficient storage"));
return;
}
@ -174,10 +174,10 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
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;
}
Serial.print("File exists, using: ");
Serial.print(F("File exists, using: "));
Serial.println(filepath);
}
@ -187,7 +187,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
if (!request->_tempFile)
{
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;
}
sd_lock_release();
@ -198,7 +198,7 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
// Check if file handle is valid
if (!request->_tempFile)
{
request->send(500, "text/plain", "File handle invalid");
request->send(500, txt_plain, F("File handle invalid"));
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
request->_tempFile.close();
sd_lock_release();
request->send(500, "text/plain", "Write error - SD card may be full");
request->send(500, txt_plain, "Write error");
return;
}
@ -252,11 +252,11 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index,
rootNode.buildDirectoryTree("/");
dir_lock_release();
request->send(200, "text/plain", "Upload successful");
request->send(200, txt_plain, "Upload successful");
}
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();
rootNode.buildDirectoryTree("/");
dir_lock_release();
request->send(200, "text/plain", "File moved successfully.");
request->send(200, txt_plain, F("File moved successfully."));
}
else
{
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();
rootNode.buildDirectoryTree("/");
dir_lock_release();
request->send(200, "text/plain", "File deleted successfully.");
request->send(200, txt_plain, "File deleted.");
}
else
{
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)
{
Serial.println("Empty song name provided");
Serial.println(F("Empty song name provided"));
return;
}
currentNode = rootNode.advanceToMP3(&song);
if (currentNode == nullptr)
{
Serial.println("No node found for song: " + song);
Serial.print(F("No node found for song: "));
Serial.println(song);
return;
}
@ -387,7 +388,8 @@ void playSongByName(String song)
if (currentNode->getCurrentPlaying() == nullptr)
{
currentNode = nullptr;
Serial.println("No song found for name: " + song);
Serial.print(F("No song found for name: "));
Serial.println(song);
return;
}
@ -395,7 +397,8 @@ void playSongByName(String song)
if (mp3File.length() == 0)
{
currentNode = nullptr;
Serial.println("Empty file path for song: " + song);
Serial.print(F("Empty file path for song: "));
Serial.println(song);
return;
}
@ -420,7 +423,7 @@ void playSongByRFID(String id)
{
if (id.length() == 0)
{
Serial.println("Empty RFID ID provided");
Serial.println(F("Empty RFID ID provided"));
return;
}
@ -438,7 +441,7 @@ void playSongByRFID(String id)
return;
}
Serial.print("RFID mapping found. Target: ");
Serial.print(F("RFID mapping found. Target: "));
Serial.print(entry.target);
Serial.print(" Mode: ");
Serial.println(entry.mode);
@ -456,14 +459,16 @@ void playSongByRFID(String id)
currentNode = rootNode.advanceToMP3(&entry.target);
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;
}
String mp3File = currentNode->getCurrentPlayingFilePath();
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;
}
@ -716,8 +721,11 @@ boolean readSongProgress(const char *filename)
currentSongId = (uint16_t)tempId;
currentSongSeconds = (uint32_t)tempSeconds;
#ifdef DEBUG
Serial.println("Data read from file: " + data);
Serial.println("Parsed ID: " + String(currentSongId) + ", s: " + String(currentSongSeconds));
#endif
return true;
}
@ -727,11 +735,9 @@ String getState()
static DynamicJsonDocument jsonState(512);
static String output;
output.reserve(512); // Pre-allocate string buffer
output.clear();
jsonState.clear(); // Clear previous data
output.reserve(512); // Pre-allocate string buffer
jsonState["playing"] = audio.isRunning();
if (currentNode != nullptr)
@ -752,6 +758,7 @@ String getState()
jsonState["heap"] = free_heap;
serializeJson(jsonState, output);
jsonState.clear();
return output;
}
@ -772,11 +779,11 @@ void saveMappingToFile(const String filename)
file.println(pair.second.mode);
}
file.close();
Serial.println("Mapping saved to file.");
Serial.println(F("Mapping saved to file."));
}
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);
saveMappingToFile(getSysDir(mapping_file));
request->send(200, "text/plain", "Mapping updated");
request->send(200, txt_plain, "Mapping updated");
}
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)
mode = mstr.charAt(0);
}
#ifdef DEBUG
Serial.println("found rfid mapping for " + target + " mode " + String(mode));
#endif
// Add key-value pair to the map
rfid_map[key] = MappingEntry(target, mode);
}
@ -852,20 +860,13 @@ void readDataFromFile(String filename)
}
else
{
Serial.print("Error opening file ");
Serial.print(F("Error opening file "));
Serial.println(filename);
}
}
String processor(const String &var)
{
if (var == "DIRECTORY")
{
dir_lock_acquire();
String out = rootNode.getDirectoryStructureHTML();
dir_lock_release();
return out;
}
if (var == "MAPPING")
{
@ -903,9 +904,9 @@ String processor(const String &var)
mappingVal += "|";
mappingVal += pair.second.mode;
html.concat(htmlEscape(mappingVal));
html.concat("</td></tr>");
html.concat(F("</td></tr>"));
}
html.concat("</table>");
html.concat(F("</table>"));
return html;
}
@ -961,7 +962,7 @@ void previous()
lastInteraction = millis();
if (currentNode == NULL)
{
Serial.println("previous(): currentNode is null");
Serial.println(F("previous(): currentNode is null"));
return;
}
@ -970,7 +971,9 @@ void previous()
const String *currentSong = currentNode->getCurrentPlaying();
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;
}
@ -1013,7 +1016,9 @@ void previous()
else
{
// 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);
if (globalPrevNode != NULL)
@ -1021,20 +1026,24 @@ void previous()
const String *globalPrevSong = globalPrevNode->getCurrentPlaying();
if (globalPrevSong != NULL)
{
Serial.print("previous(): Found previous song globally: ");
Serial.print(F("previous(): Found previous song globally: "));
Serial.println(*globalPrevSong);
currentNode = globalPrevNode;
stop();
playFile(currentNode->getCurrentPlayingFilePath().c_str());
}
#ifdef DEBUG
else
{
Serial.println("prev: Global previous song is null");
Serial.println(F("prev: Global previous song is null"));
}
#endif
}
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
audio.setAudioPlayPosition(0);
currentNode->setSecondsPlayed(0);
@ -1150,17 +1159,26 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
request->onDisconnect([](){ webreq_exit(); });
deactivateRFID();
activateSD();
String htmlPath = getSysDir("index.html");
String htmlPath = getSysDir(index_file);
if (SD.exists(htmlPath))
{
AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html");
response->addHeader("Cache-Control", "no-store");
uint32_t fsize = 0;
{
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);
}
else
{
// 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();
activateSD();
// 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))
{
uint32_t fsize = 0;
{
AsyncWebServerResponse *resp = request->beginResponse(SD, cssPath, "text/css");
resp->addHeader("Cache-Control", "public, max-age=300");
File f = SD.open(cssPath);
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);
}
}
else
{
// 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(); });
deactivateRFID();
activateSD();
String jsPath = getSysDir("script.js");
String jsPath = getSysDir(script_file);
if (SD.exists(jsPath))
{
uint32_t fsize = 0;
{
AsyncWebServerResponse *resp = request->beginResponse(SD, jsPath, "application/javascript");
resp->addHeader("Cache-Control", "public, max-age=300");
File f = SD.open(jsPath);
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);
}
}
else
{
// 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();
request->onDisconnect([](){ webreq_exit(); });
// Stream the response directly from the directory tree to avoid large temporary Strings
AsyncResponseStream* stream = request->beginResponseStream("text/html; charset=UTF-8");
stream->addHeader("Cache-Control", "no-store");
stream->addHeader("Connection", "close");
AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset);
#ifdef DEBUG
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
dir_lock_acquire();
rootNode.streamDirectoryHTML(*stream);
@ -1233,9 +1272,12 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
// Stream mapping to avoid Content-Length mismatches and reduce heap spikes
AsyncResponseStream* stream = request->beginResponseStream("text/html; charset=UTF-8");
stream->addHeader("Cache-Control", "no-store");
stream->addHeader("Connection", "close");
AsyncResponseStream* stream = request->beginResponseStream(txt_html_charset);
#ifdef DEBUG
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"));
stream->print(html);
request->send(stream);
@ -1246,44 +1288,48 @@ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
String state = getState();
AsyncWebServerResponse* resp = request->beginResponse(200, "application/json; charset=UTF-8", state);
resp->addHeader("Cache-Control", "no-store");
resp->addHeader("Connection", "close");
#ifdef DEBUG
Serial.printf("Serving /state heap=%u webreq_cnt=%u\n", (unsigned)xPortGetFreeHeapSize(), (unsigned)webreq_cnt);
#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); });
server.on("/start", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain; charset=UTF-8", "start");
request->send(200, txt_plain, "start");
start(); });
server.on("/toggleplaypause", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain; charset=UTF-8", "toggleplaypause");
request->send(200, txt_plain, "toggleplaypause");
togglePlayPause(); });
server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain", "stop");
request->send(200, txt_plain, "stop");
stop(); });
server.on("/next", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain", "next");
request->send(200, txt_plain, "next");
next(); });
server.on("/previous", HTTP_GET, [](AsyncWebServerRequest *request)
{
webreq_enter();
request->onDisconnect([](){ webreq_exit(); });
request->send(200, "text/plain", "previous");
request->send(200, txt_plain, "previous");
previous(); });
server.on("/playbyid", HTTP_GET, id_song_action);
@ -1327,9 +1373,9 @@ void setup()
RFIDActive = false;
SDActive = false;
Serial.print("Initializing SD card...");
Serial.print(F("Initializing SD card..."));
activateSD();
Serial.println("SD initialization done.");
Serial.println(F("SD initialization done."));
// Seed RNG for shuffle mode
#if defined(ESP32)
@ -1339,7 +1385,7 @@ void setup()
#endif
// Load configuration from SD card
Serial.println("Loading configuration...");
Serial.println(F("Loading configuration..."));
loadConfig();
// deep sleep wakeup
@ -1406,7 +1452,9 @@ void setup()
wifiManager.setConnectTimeout(15); // Faster connection attempts
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
// wifiManager.resetSettings();
@ -1419,10 +1467,10 @@ void setup()
}
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
xTaskCreatePinnedToCore(
@ -1452,7 +1500,7 @@ void id_song_action(AsyncWebServerRequest *request)
}
}
lastInteraction = millis();
request->send_P(200, "text/plain", "ok");
request->send_P(200, txt_plain, "ok");
}
void progress_action(AsyncWebServerRequest *request)
@ -1470,7 +1518,7 @@ void progress_action(AsyncWebServerRequest *request)
}
}
lastInteraction = millis();
request->send_P(200, "text/plain", "ok");
request->send_P(200, txt_plain, "ok");
}
void volume_action(AsyncWebServerRequest *request)
@ -1488,7 +1536,7 @@ void volume_action(AsyncWebServerRequest *request)
}
}
lastInteraction = millis();
request->send_P(200, "text/plain", "ok");
request->send_P(200, txt_plain, "ok");
}
const String getSysDir(const String filename)
@ -1505,7 +1553,7 @@ const String getSysDir(const String filename)
void loop()
{
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
webreq_cnt = 0;
webrequest_blockings = 0;
@ -1574,7 +1622,7 @@ void loop()
if (now - lastInteraction > config.sleepDelay)
{
Serial.println("entering deep sleep..");
Serial.println(F("entering deep sleep.."));
deactivateRFID();
deactivateSD();
esp_deep_sleep_start();
@ -1676,7 +1724,7 @@ void loop()
{
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;
voltage_threshold_counter = 0;
}

View File

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