[ai] Many (memory) improvements, cleanup script, still problems
This commit is contained in:
parent
083dfd6e2a
commit
abfe564891
|
|
@ -7,5 +7,5 @@
|
|||
schema/hannabox/hannabox-backups/
|
||||
schema/hannabox/*.lck
|
||||
.copilot
|
||||
|
||||
web/cleaned
|
||||
.codegpt
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
218
src/main.cpp
218
src/main.cpp
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue