diff --git a/example_config.txt b/example_config.txt new file mode 100644 index 0000000..b4d4775 --- /dev/null +++ b/example_config.txt @@ -0,0 +1,26 @@ +# HannaBox Configuration File +# Values are in the format: key=value +# Lines starting with # are comments + +# Audio Settings +initialVolume=7 +maxVolume=15 + +# Power Management (times in milliseconds) +sleepTime=1800000 +sleepDelay=1800000 +sleepMessageDelay=1798000 + +# Battery Settings (voltage in millivolts) +minVoltage=3200 +voltage100Percent=4200 + +# RFID Settings +rfidLoopInterval=25 + +# Playback Settings +startAtStoredProgress=true + +# WiFi Settings (leave empty to use current WiFiManager) +wifiSSID= +wifiPassword= diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..78525d5 --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,151 @@ +#include "config.h" +#include "globals.h" +#include + +// Global config instance +Config config; + +const String getConfigFilePath() { + return "/" + sys_dir + "/config.txt"; +} + +void setDefaultConfig() { + config.initialVolume = 7; + config.maxVolume = 15; + config.sleepTime = 1800000; + config.minVoltage = 3200; + config.voltage100Percent = 4200; + config.sleepDelay = 1800000; + config.sleepMessageDelay = 1798000; + config.rfidLoopInterval = 25; + config.startAtStoredProgress = true; + config.wifiSSID[0] = '\0'; + config.wifiPassword[0] = '\0'; +} + +bool loadConfig() { + String configPath = getConfigFilePath(); + + if (!SD.exists(configPath)) { + Serial.println("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"); + setDefaultConfig(); + return false; + } + + // Set defaults first + setDefaultConfig(); + + // Parse config file line by line + while (file.available()) { + String line = file.readStringUntil('\n'); + line.trim(); + + // Skip empty lines and comments + if (line.length() == 0 || line.startsWith("#")) { + continue; + } + + int separatorIndex = line.indexOf('='); + if (separatorIndex == -1) { + continue; + } + + String key = line.substring(0, separatorIndex); + String value = line.substring(separatorIndex + 1); + key.trim(); + value.trim(); + + // Parse each configuration value + if (key == "initialVolume") { + config.initialVolume = constrain(value.toInt(), 0, 21); + } else if (key == "maxVolume") { + config.maxVolume = constrain(value.toInt(), 1, 21); + } else if (key == "sleepTime") { + config.sleepTime = value.toInt(); + } else if (key == "minVoltage") { + config.minVoltage = value.toInt(); + } else if (key == "voltage100Percent") { + config.voltage100Percent = value.toInt(); + } else if (key == "sleepDelay") { + config.sleepDelay = value.toInt(); + } else if (key == "sleepMessageDelay") { + config.sleepMessageDelay = value.toInt(); + } else if (key == "rfidLoopInterval") { + config.rfidLoopInterval = constrain(value.toInt(), 1, 255); + } else if (key == "startAtStoredProgress") { + config.startAtStoredProgress = (value == "1" || value.equalsIgnoreCase("true")); + } else if (key == "wifiSSID") { + strncpy(config.wifiSSID, value.c_str(), sizeof(config.wifiSSID) - 1); + config.wifiSSID[sizeof(config.wifiSSID) - 1] = '\0'; + } else if (key == "wifiPassword") { + strncpy(config.wifiPassword, value.c_str(), sizeof(config.wifiPassword) - 1); + config.wifiPassword[sizeof(config.wifiPassword) - 1] = '\0'; + } + } + + file.close(); + + Serial.println("Config loaded successfully"); + Serial.print("Initial Volume: "); Serial.println(config.initialVolume); + Serial.print("Max Volume: "); Serial.println(config.maxVolume); + Serial.print("Sleep Delay: "); Serial.println(config.sleepDelay); + Serial.print("RFID Interval: "); Serial.println(config.rfidLoopInterval); + + return true; +} + +bool saveConfig() { + String configPath = getConfigFilePath(); + + File file = SD.open(configPath, FILE_WRITE); + if (!file) { + Serial.println("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(""); + + file.println("# Audio Settings"); + file.print("initialVolume="); file.println(config.initialVolume); + file.print("maxVolume="); file.println(config.maxVolume); + file.println(""); + + file.println("# Power Management (times 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.print("minVoltage="); file.println(config.minVoltage); + file.print("voltage100Percent="); file.println(config.voltage100Percent); + file.println(""); + + file.println("# RFID Settings"); + file.print("rfidLoopInterval="); file.println(config.rfidLoopInterval); + file.println(""); + + file.println("# Playback Settings"); + file.print("startAtStoredProgress="); file.println(config.startAtStoredProgress ? "true" : "false"); + file.println(""); + + file.println("# WiFi Settings (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"); + return true; +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..06c2268 --- /dev/null +++ b/src/config.h @@ -0,0 +1,30 @@ +#ifndef CONFIG_H_ +#define CONFIG_H_ + +#include + +// Configuration structure - keep it minimal for memory efficiency +struct Config { + uint8_t initialVolume = 7; + uint8_t maxVolume = 15; + uint32_t sleepTime = 1800000; // 30 minutes in ms + uint16_t minVoltage = 3200; // mV - minimum voltage before sleep + uint16_t voltage100Percent = 4200; // mV - voltage representing 100% battery + uint32_t sleepDelay = 1800000; // 30 minutes in ms + uint32_t sleepMessageDelay = 1798000; // 2 seconds before sleep + uint8_t rfidLoopInterval = 25; // RFID check interval + bool startAtStoredProgress = true; // Resume from last position + char wifiSSID[32] = ""; // WiFi SSID (empty = use current mechanism) + char wifiPassword[64] = ""; // WiFi password (empty = use current mechanism) +}; + +// Global config instance +extern Config config; + +// Function declarations +bool loadConfig(); +bool saveConfig(); +void setDefaultConfig(); +const String getConfigFilePath(); + +#endif diff --git a/src/main.cpp b/src/main.cpp index a3ef2fa..af79e7e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -49,7 +49,7 @@ #include "globals.h" #include "main.h" #include "DirectoryNode.h" - +#include "config.h" File root; File mp3File; @@ -61,14 +61,12 @@ uint volume = 7; AsyncWebServer server(80); DNSServer dns; -//static variable has to be instantiated outside of class definition: +// static variable has to be instantiated outside of class definition: uint16_t DirectoryNode::idCounter = 0; DirectoryNode rootNode("/"); DirectoryNode *currentNode = nullptr; - - volatile bool newRfidInt = false; MFRC522 rfid(CS_RFID, RST_RFID); // instatiate a MFRC522 reader object. @@ -94,7 +92,6 @@ bool webrequestActive = false; uint16_t voltage_threshold_counter = 0; size_t free_heap = 0; - void activateSD() { @@ -110,7 +107,8 @@ void activateSD() void deactivateSD() { - if (SDActive) { + if (SDActive) + { digitalWrite(CS_SDCARD, HIGH); SDActive = false; } @@ -125,7 +123,8 @@ void activateRFID() void deactivateRFID() { - if (RFIDActive) { + if (RFIDActive) + { digitalWrite(CS_RFID, HIGH); RFIDActive = false; } @@ -133,39 +132,49 @@ void deactivateRFID() // Make size of files human readable // source: https://github.com/CelliesProjects/minimalUploadAuthESP32 -String humanReadableSize(const size_t bytes) { - if (bytes < 1024) return String(bytes) + " B"; - else if (bytes < (1024 * 1024)) return String(bytes / 1024.0) + " KB"; - else if (bytes < (1024 * 1024 * 1024)) return String(bytes / 1024.0 / 1024.0) + " MB"; - else return String(bytes / 1024.0 / 1024.0 / 1024.0) + " GB"; +String humanReadableSize(const size_t bytes) +{ + if (bytes < 1024) + return String(bytes) + " B"; + else if (bytes < (1024 * 1024)) + return String(bytes / 1024.0) + " KB"; + else if (bytes < (1024 * 1024 * 1024)) + return String(bytes / 1024.0 / 1024.0) + " MB"; + else + return String(bytes / 1024.0 / 1024.0 / 1024.0) + " GB"; } -void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { - static String logBuffer; // Static to avoid repeated allocations - - if (!index) { +void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) +{ + static String logBuffer; // Static to avoid repeated allocations + + if (!index) + { // Validate filename and file extension - if (filename.length() == 0) { + if (filename.length() == 0) + { request->send(400, "text/plain", "Invalid filename"); return; } - + // Use const reference to avoid string copies - const String& lowerFilename = filename; - if (!lowerFilename.endsWith(".mp3") && !lowerFilename.endsWith(".wav") && - !lowerFilename.endsWith(".m4a") && !lowerFilename.endsWith(".ogg")) { + const String &lowerFilename = filename; + 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."); return; } - + // More efficient space check using bit shift - uint32_t freeSpace = (SD.cardSize() - SD.usedBytes()) >> 20; // Bit shift instead of division - - if (freeSpace < 10) { // Less than 10MB free - request->send(507, "text/plain", "Insufficient storage space"); + uint32_t freeSpace = (SD.cardSize() - SD.usedBytes()) >> 20; // Bit shift instead of division + + if (freeSpace < 10) + { // Less than 10MB free + request->send(507, "text/plain", "Insufficient storage"); return; } - + // Pre-allocate log buffer logBuffer.reserve(128); logBuffer = "Upload Start: "; @@ -174,60 +183,69 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, logBuffer += String(freeSpace); logBuffer += "MB)"; Serial.println(logBuffer); - logBuffer.clear(); // Free memory immediately - + logBuffer.clear(); // Free memory immediately + // Ensure SD is active activateSD(); - + // Check if file already exists and create backup name if needed String filepath = "/" + filename; - if (SD.exists(filepath)) { + if (SD.exists(filepath)) + { String baseName = filename.substring(0, filename.lastIndexOf('.')); String extension = filename.substring(filename.lastIndexOf('.')); int counter = 1; - do { + do + { filepath = "/" + baseName + "_" + String(counter) + extension; counter++; } while (SD.exists(filepath) && counter < 100); - - if (counter >= 100) { + + if (counter >= 100) + { request->send(409, "text/plain", "Too many files with similar names"); return; } Serial.print("File exists, using: "); Serial.println(filepath); } - + // Open the file for writing request->_tempFile = SD.open(filepath, FILE_WRITE); - if (!request->_tempFile) { + if (!request->_tempFile) + { request->send(500, "text/plain", "Failed to create file on SD card"); return; } } - if (len) { + if (len) + { // Check if file handle is valid - if (!request->_tempFile) { + if (!request->_tempFile) + { request->send(500, "text/plain", "File handle invalid"); return; } - + // Write data and verify bytes written size_t bytesWritten = request->_tempFile.write(data, len); - if (bytesWritten != len) { + if (bytesWritten != len) + { request->_tempFile.close(); request->send(500, "text/plain", "Write error - SD card may be full"); return; } - + // Flush data periodically to ensure it's written - if (index % 2048 == 0) { // Flush every 2KB + if (index % 2048 == 0) + { // Flush every 2KB request->_tempFile.flush(); } - + // Reduce logging frequency to save memory - log every 200KB instead of 100KB - if (len && (index % 204800 == 0)) { + if (len && (index % 204800 == 0)) + { logBuffer = "Upload: "; logBuffer += humanReadableSize(index + len); Serial.println(logBuffer); @@ -235,87 +253,102 @@ void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, } } - if (final) { - if (request->_tempFile) { + if (final) + { + if (request->_tempFile) + { request->_tempFile.flush(); // Ensure all data is written request->_tempFile.close(); - + logBuffer = "Upload Complete: "; logBuffer += filename; logBuffer += ", size: "; logBuffer += humanReadableSize(index + len); Serial.println(logBuffer); logBuffer.clear(); - + // Rebuild directory tree to include new file rootNode.buildDirectoryTree("/"); - + request->send(200, "text/plain", "Upload successful"); - } else { + } + else + { request->send(500, "text/plain", "Upload failed - file handle was invalid"); } } } -void handleMoveFile(AsyncWebServerRequest *request) { +void handleMoveFile(AsyncWebServerRequest *request) +{ String from = request->arg("from"); String to = request->arg("to"); - if (SD.exists(from)) { + if (SD.exists(from)) + { SD.rename(from, to); Serial.println("Moved file: " + from + " to " + to); // Rebuild directory tree to update file list rootNode.buildDirectoryTree("/"); request->send(200, "text/plain", "File moved successfully."); - } else { + } + else + { Serial.println("File not found: " + from); request->send(404, "text/plain", "File not found."); } } -void handleDeleteFile(AsyncWebServerRequest *request) { +void handleDeleteFile(AsyncWebServerRequest *request) +{ String filename = request->arg("filename"); - if (SD.exists(filename)) { + if (SD.exists(filename)) + { SD.remove(filename.c_str()); Serial.println("Deleted file: " + filename); // Rebuild directory tree to update file list rootNode.buildDirectoryTree("/"); request->send(200, "text/plain", "File deleted successfully."); - } else { + } + else + { Serial.println("File not found: " + filename); request->send(404, "text/plain", "File not found."); } } -uint32_t getBatteryVoltageMv() { +uint32_t getBatteryVoltageMv() +{ uint32_t voltage = analogReadMilliVolts(BAT_VOLTAGE_PIN); - voltage *= 2;//*2 because of the voltage divider. + voltage *= 2; //*2 because of the voltage divider. Serial.print("Battery Voltage: "); Serial.println(voltage); Serial.println(" mV"); return voltage; } - void playSongById(uint16_t id, uint32_t continueSeconds = 0) { currentNode = rootNode.advanceToMP3(id); - if (currentNode == nullptr) { + if (currentNode == nullptr) + { Serial.println("No node found for ID: " + String(id)); return; } // Check if the current playing song is valid - if (currentNode->getCurrentPlaying() == nullptr) { + if (currentNode->getCurrentPlaying() == nullptr) + { currentNode = nullptr; Serial.println("No song found for ID: " + String(id)); return; } String mp3File = currentNode->getCurrentPlayingFilePath(); - if (mp3File.length() == 0) { + if (mp3File.length() == 0) + { currentNode = nullptr; Serial.println("Empty file path for ID: " + String(id)); return; @@ -326,11 +359,12 @@ void playSongById(uint16_t id, uint32_t continueSeconds = 0) Serial.print(" "); Serial.println(continueSeconds); Serial.println(mp3File.c_str()); - + deactivateRFID(); activateSD(); - if (!playFile(mp3File.c_str())) { + if (!playFile(mp3File.c_str())) + { Serial.println("Failed to play file: " + mp3File); currentNode = nullptr; activateRFID(); @@ -338,7 +372,8 @@ void playSongById(uint16_t id, uint32_t continueSeconds = 0) return; } - if (continueSeconds != 0) { + if (continueSeconds != 0) + { audio.setAudioPlayPosition(continueSeconds); } activateRFID(); @@ -347,26 +382,30 @@ void playSongById(uint16_t id, uint32_t continueSeconds = 0) void playSongByName(String song) { - if (song.length() == 0) { + if (song.length() == 0) + { Serial.println("Empty song name provided"); return; } currentNode = rootNode.advanceToMP3(&song); - if (currentNode == nullptr) { + if (currentNode == nullptr) + { Serial.println("No node found for song: " + song); return; } // Check if the current playing song is valid - if (currentNode->getCurrentPlaying() == nullptr) { + if (currentNode->getCurrentPlaying() == nullptr) + { currentNode = nullptr; Serial.println("No song found for name: " + song); return; } String mp3File = currentNode->getCurrentPlayingFilePath(); - if (mp3File.length() == 0) { + if (mp3File.length() == 0) + { currentNode = nullptr; Serial.println("Empty file path for song: " + song); return; @@ -375,15 +414,16 @@ void playSongByName(String song) Serial.println("Playing song: " + mp3File); deactivateRFID(); activateSD(); - - if (!playFile(mp3File.c_str())) { + + if (!playFile(mp3File.c_str())) + { Serial.println("Failed to play file: " + mp3File); currentNode = nullptr; activateRFID(); deactivateSD(); return; } - + activateRFID(); deactivateSD(); } @@ -395,19 +435,22 @@ void playSongByPath(String path) void playSongByRFID(String id) { - if (id.length() == 0) { + if (id.length() == 0) + { Serial.println("Empty RFID ID provided"); return; } auto songit = rfid_map.find(id); - if (songit == rfid_map.end()) { + if (songit == rfid_map.end()) + { Serial.println("Song for UID not found: " + id); return; } - if (songit->second.length() == 0) { - Serial.println("Empty song name mapped to RFID: " + id); + if (songit->second.length() == 0) + { + Serial.println("Empty song name mapped to: " + id); return; } @@ -425,12 +468,13 @@ void playSongByRFID(String id) */ bool playFile(const char *filename, uint32_t resumeFilePos) { - if (filename == nullptr || strlen(filename)==0) { - Serial.println("filename is empty."); + if (filename == nullptr || strlen(filename) == 0) + { + Serial.println("filename empty."); return false; } - //return audio.connecttoFS(filename, resumeFilePos); - return audio.connecttoFS(SD,filename,resumeFilePos); + // return audio.connecttoFS(filename, resumeFilePos); + return audio.connecttoFS(SD, filename, resumeFilePos); } void playNextMp3() @@ -485,45 +529,99 @@ void unmute() audio.setVolume(volume); } -void writeSongProgress(const char *filename, uint16_t id, uint32_t seconds) { +void writeSongProgress(const char *filename, uint16_t id, uint32_t seconds) +{ File file = SD.open(filename, FILE_WRITE); - if (file) { + if (file) + { file.print(id); file.print(" "); file.println(seconds); file.close(); - Serial.println("Data written to file: ID-" + String(id) + ", Number-" + String(seconds)); - } else { - Serial.println("Error opening file for writing."); + Serial.println("Progress written: ID " + String(id) + ", s " + String(seconds)); + } + else + { + Serial.print("Error opening file for writing: "); + Serial.println(filename); } } -boolean readSongProgress(const char *filename) { - String data = ""; +boolean readSongProgress(const char *filename) +{ File file = SD.open(filename); - if (file) { - while (file.available()) { - char character = file.read(); - data += character; - } - file.close(); - // Parse the string into ID and number - sscanf(data.c_str(), "%d %d", ¤tSongId, ¤tSongSeconds); - Serial.println("Data read from file: " + data); - return true; - } else { - Serial.println("Error opening file for reading."); + if (!file) + { + Serial.print("Error opening file for reading: "); + Serial.println(filename); return false; } + + // Read file with size limit to prevent buffer overflow + String data; + data.reserve(64); // Increased reserve size for safety + size_t bytesRead = 0; + const size_t maxBytes = 50; // Limit file read size + + while (file.available() && bytesRead < maxBytes) + { + char character = file.read(); + if (character == '\n' || character == '\r') + { + break; // Stop at first line ending + } + data += character; + bytesRead++; + } + file.close(); + + // Validate data before parsing + data.trim(); + if (data.length() == 0) + { + Serial.println("Progress file empty"); + return false; + } + + // Use safer parsing with proper type specifiers + int tempId = 0; + unsigned long tempSeconds = 0; + int parsed = sscanf(data.c_str(), "%d %lu", &tempId, &tempSeconds); + + if (parsed != 2) + { + Serial.println("Failed to parse progress data: " + data); + return false; + } + + // Validate ranges before assignment + if (tempId < 0 || tempId > 65535) + { + Serial.println("Invalid song in progress: " + String(tempId)); + return false; + } + + if (tempSeconds > 4294967295UL) + { + Serial.println("Invalid seconds in progress: " + String(tempSeconds)); + return false; + } + + currentSongId = (uint16_t)tempId; + currentSongSeconds = (uint32_t)tempSeconds; + + Serial.println("Data read from file: " + data); + Serial.println("Parsed ID: " + String(currentSongId) + ", s: " + String(currentSongSeconds)); + return true; } String getState() { // Use static buffer to avoid repeated allocations static DynamicJsonDocument jsonState(512); - jsonState.clear(); // Clear previous data - + jsonState.clear(); // Clear previous data + jsonState["playing"] = audio.isRunning(); if (currentNode != nullptr) @@ -542,31 +640,38 @@ String getState() jsonState["voltage"] = lastVoltage; jsonState["uid"] = lastUid; jsonState["heap"] = free_heap; - + String output; - output.reserve(512); // Pre-allocate string buffer + output.reserve(512); // Pre-allocate string buffer serializeJson(jsonState, output); return output; } // Function to save the rfid_map to the mapping file -void saveMappingToFile(const String filename) { +void saveMappingToFile(const String filename) +{ File file = SD.open(filename, FILE_WRITE); - if (file) { - for (const auto &pair : rfid_map) { - file.println(pair.first+ "=" + pair.second); + if (file) + { + for (const auto &pair : rfid_map) + { + file.println(pair.first + "=" + pair.second); } file.close(); Serial.println("Mapping saved to file."); - } else { + } + else + { Serial.println("Error opening file for writing."); } } // Function to handle edit requests -void editMapping(AsyncWebServerRequest *request) { - if (request->hasParam("rfid", true) && request->hasParam("song", true)) { +void editMapping(AsyncWebServerRequest *request) +{ + if (request->hasParam("rfid", true) && request->hasParam("song", true)) + { String rfid = request->getParam("rfid", true)->value(); String song = request->getParam("song", true)->value(); rfid.trim(); @@ -574,33 +679,41 @@ void editMapping(AsyncWebServerRequest *request) { rfid_map[rfid] = song; saveMappingToFile(getSysDir(mapping_file)); request->send(200, "text/plain", "Mapping updated"); - } else { + } + else + { request->send(400, "text/plain", "Invalid parameters"); } } -std::map readDataFromFile(String filename) { - +std::map readDataFromFile(String filename) +{ + File file = SD.open(filename); - if (file) { - while (file.available()) { + if (file) + { + while (file.available()) + { // Read key and value from the file String line = file.readStringUntil('\n'); int separatorIndex = line.indexOf('='); - if (separatorIndex != -1) { + if (separatorIndex != -1) + { // Extract key and value String key = line.substring(0, separatorIndex).c_str(); String value = line.substring(separatorIndex + 1).c_str(); key.trim(); value.trim(); - Serial.println("found rfid mapping for "+value); + Serial.println("found rfid mapping for " + value); // Add key-value pair to the map rfid_map[key] = value; } } file.close(); - } else { + } + else + { Serial.print("Error opening file "); Serial.println(filename); } @@ -610,34 +723,44 @@ std::map readDataFromFile(String filename) { String processor(const String &var) { - if (var == "DIRECTORY") + if (var == "DIRECTORY") { return rootNode.getDirectoryStructureHTML(); } - if (var == "MAPPING") { - auto htmlEscape = [](const String& s) -> String { + if (var == "MAPPING") + { + auto htmlEscape = [](const String &s) -> String + { String out; - for (size_t i = 0; i < s.length(); ++i) { + for (size_t i = 0; i < s.length(); ++i) + { char c = s[i]; - if (c == '&') out += "&"; - else if (c == '<') out += ""; - else if (c == '>') out += ""; - else if (c == '"') out += ""; - else if (c == '\'') out += ""; - else out += c; + if (c == '&') + out += "&"; + else if (c == '<') + out += ""; + else if (c == '>') + out += ""; + else if (c == '"') + out += ""; + else if (c == '\'') + out += ""; + else + out += c; } return out; }; String html = ""; - for (const auto& pair : rfid_map) { - html += ""; + for (const auto &pair : rfid_map) + { + html += ""; } html += "
RFIDSong
" + htmlEscape(pair.first) + "" + htmlEscape(pair.second) + "
" + htmlEscape(pair.first) + "" + htmlEscape(pair.second) + "
"; return html; } - return String(); // Return empty string instead of creating new String + return String(); // Return empty string instead of creating new String } void stop() @@ -668,6 +791,7 @@ void togglePlayPause() { if (currentNode != NULL) { + writeSongProgress(getSysDir(progress_file).c_str(), currentNode->getCurrentPlayingId(), currentNode->getSecondsPlayed()); audio.pauseResume(); } else @@ -685,85 +809,94 @@ void next() void previous() { - Serial.println("=== PREVIOUS FUNCTION CALLED ==="); - - if (currentNode == NULL) { - Serial.println("previous(): currentNode is null, cannot go to previous"); - lastInteraction = millis(); - return; - } + lastInteraction = millis(); + if (currentNode == NULL) + { + Serial.println("previous(): currentNode is null"); - // Validate current state - const String* currentSong = currentNode->getCurrentPlaying(); - if (currentSong == NULL) { - Serial.println("previous(): currentPlaying is null, cannot go to previous"); - lastInteraction = millis(); - return; - } + return; + } - Serial.print("previous(): Current song: "); - Serial.println(*currentSong); - - // Use audio library's current time instead of tracked seconds for more accuracy - uint32_t currentAudioTime = audio.getAudioCurrentTime(); - Serial.print("previous(): Current audio time: "); - Serial.print(currentAudioTime); - Serial.println(" seconds"); + // Validate current state + const String *currentSong = currentNode->getCurrentPlaying(); + if (currentSong == NULL) + { + Serial.println("previous(): currentPlaying is null, cannot go to previous"); + return; + } - // Try to go to previous within current directory first - DirectoryNode* newNode = currentNode->goToPreviousMP3(2); // Use 2 second threshold - - if (newNode != NULL) { - // Check if we're restarting the same song or moving to a different song - const String* newSong = newNode->getCurrentPlaying(); - - if (newSong != NULL && currentSong == newSong && currentAudioTime > 2) { - // Restart current song if it's been playing for more than 2 seconds - Serial.println("previous(): Restarting current song"); - audio.setAudioPlayPosition(0); - currentNode->setSecondsPlayed(0); - } else if (newSong != NULL && currentSong != newSong) { - // Move to previous song in same directory - Serial.print("previous(): Moving to previous song in directory: "); - Serial.println(*newSong); - currentNode = newNode; - stop(); - deactivateRFID(); - activateSD(); - playFile(currentNode->getCurrentPlayingFilePath().c_str()); - activateRFID(); - deactivateSD(); - } - } else { - // Need to find previous song globally (across directories) - Serial.println("previous(): Looking for previous song globally"); - DirectoryNode* globalPrevNode = rootNode.findPreviousMP3Globally(currentSong); - - if (globalPrevNode != NULL) { - const String* globalPrevSong = globalPrevNode->getCurrentPlaying(); - if (globalPrevSong != NULL) { - Serial.print("previous(): Found previous song globally: "); - Serial.println(*globalPrevSong); - currentNode = globalPrevNode; - stop(); - deactivateRFID(); - activateSD(); - playFile(currentNode->getCurrentPlayingFilePath().c_str()); - activateRFID(); - deactivateSD(); - } else { - Serial.println("previous(): Global previous song is null"); - } - } else { - Serial.println("previous(): No previous song found globally - at beginning of playlist"); - // Optionally restart current song or do nothing - audio.setAudioPlayPosition(0); - currentNode->setSecondsPlayed(0); - } + Serial.print("previous(): Current song: "); + Serial.println(*currentSong); + + // Use audio library's current time instead of tracked seconds for more accuracy + uint32_t currentAudioTime = audio.getAudioCurrentTime(); + Serial.print("previous(): Current audio time: "); + Serial.print(currentAudioTime); + Serial.println(" seconds"); + + // Try to go to previous within current directory first + DirectoryNode *newNode = currentNode->goToPreviousMP3(2); // Use 2 second threshold + + if (newNode != NULL) + { + // Check if we're restarting the same song or moving to a different song + const String *newSong = newNode->getCurrentPlaying(); + + if (newSong != NULL && currentSong == newSong && currentAudioTime > 2) + { + // Restart current song if it's been playing for more than 2 seconds + Serial.println("previous(): Restarting current song"); + audio.setAudioPlayPosition(0); + currentNode->setSecondsPlayed(0); } - - lastInteraction = millis(); - Serial.println("=== PREVIOUS FUNCTION COMPLETED ==="); + else if (newSong != NULL && currentSong != newSong) + { + // Move to previous song in same directory + Serial.print("previous(): Moving to previous song in directory: "); + Serial.println(*newSong); + currentNode = newNode; + stop(); + deactivateRFID(); + activateSD(); + playFile(currentNode->getCurrentPlayingFilePath().c_str()); + activateRFID(); + deactivateSD(); + } + } + else + { + // Need to find previous song globally (across directories) + Serial.println("previous(): Looking for previous song globally"); + DirectoryNode *globalPrevNode = rootNode.findPreviousMP3Globally(currentSong); + + if (globalPrevNode != NULL) + { + const String *globalPrevSong = globalPrevNode->getCurrentPlaying(); + if (globalPrevSong != NULL) + { + Serial.print("previous(): Found previous song globally: "); + Serial.println(*globalPrevSong); + currentNode = globalPrevNode; + stop(); + deactivateRFID(); + activateSD(); + playFile(currentNode->getCurrentPlayingFilePath().c_str()); + activateRFID(); + deactivateSD(); + } + else + { + Serial.println("previous(): Global previous song is null"); + } + } + else + { + Serial.println("previous(): No previous song found globally - at beginning of playlist"); + // Optionally restart current song or do nothing + audio.setAudioPlayPosition(0); + currentNode->setSecondsPlayed(0); + } + } } void audio_eof_mp3(const char *info) @@ -782,9 +915,10 @@ void IRAM_ATTR rfid_interrupt() void readRFID() { rfid.PICC_ReadCardSerial(); - + String newUid = getRFIDString(rfid.uid.uidByte); - if (newUid==lastUid) { + if (newUid == lastUid) + { return; } stop(); @@ -793,8 +927,7 @@ void readRFID() Serial.print("Card UID: "); Serial.println(lastUid); - //rfid.PICC_DumpDetailsToSerial(&(rfid.uid)); - + // rfid.PICC_DumpDetailsToSerial(&(rfid.uid)); playSongByRFID(lastUid); lastInteraction = millis(); @@ -802,79 +935,76 @@ void readRFID() void setup() { - // put your setup code here, to run once: Serial.begin(115200); - pinMode(BTN_START_STOP, INPUT_PULLUP); pinMode(BTN_NEXT, INPUT_PULLUP); pinMode(BTN_PREV, INPUT_PULLUP); /* setup the IRQ pin, not working because the pin is input only:*/ - //pinMode(IRQ_RFID, INPUT_PULLUP); + // pinMode(IRQ_RFID, INPUT_PULLUP); pinMode(CS_RFID, OUTPUT); pinMode(CS_SDCARD, OUTPUT); - digitalWrite(CS_RFID,HIGH); - digitalWrite(CS_SDCARD,HIGH); + digitalWrite(CS_RFID, HIGH); + digitalWrite(CS_SDCARD, HIGH); RFIDActive = false; SDActive = false; - Serial.print("Initializing SD card..."); activateSD(); Serial.println("SD initialization done."); - - //deep sleep wakeup + // Load configuration from SD card + Serial.println("Loading configuration..."); + loadConfig(); + + // deep sleep wakeup esp_sleep_enable_ext0_wakeup((gpio_num_t)BTN_START_STOP, LOW); - - rootNode.buildDirectoryTree("/"); rootNode.printDirectoryTree(); readDataFromFile(getSysDir(mapping_file)); - - String progressPath = getSysDir(progress_file); - continuePlaying = readSongProgress(progressPath.c_str()); + continuePlaying = config.startAtStoredProgress && readSongProgress(progressPath.c_str()); - if (continuePlaying) { + if (continuePlaying) + { Serial.print("deleting "); - Serial.println(progressPath.c_str()); - SD.remove(progressPath.c_str()); + Serial.println(progressPath); + SD.remove(progressPath); } - deactivateSD(); activateRFID(); Serial.println("RFID"); - //Init MFRC522 - //Init SPI bus - //SPI.begin(-1, -1, -1, CS_RFID); + // Init MFRC522 + // Init SPI bus + // SPI.begin(-1, -1, -1, CS_RFID); rfid.PCD_Init(CS_RFID, RST_RFID); - //somehow this test stops rfid from working! - /* - if (rfid.PCD_PerformSelfTest()) - { - Serial.println("RFID OK"); - } - else - { - Serial.println("RFID Self Test failed!"); - } -*/ + // somehow this test stops rfid from working! + /* + if (rfid.PCD_PerformSelfTest()) + { + Serial.println("RFID OK"); + } + else + { + Serial.println("RFID Self Test failed!"); + } + */ audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); - audio.setVolume(volume); // 0...21 - + audio.setVolume(config.initialVolume); // Use config value + volume = config.initialVolume; // Update global volume variable + // Optimize audio buffer size to save memory (ESP32-audioI2S optimization) - audio.setBufferSize(8192); // Reduced from default large buffer (saves 40-600KB!) + audio.setBufferSize(8192); // Reduced from default large buffer (saves 40-600KB!) Serial.println("Audio initialized."); @@ -885,73 +1015,73 @@ void setup() AsyncWiFiManager wifiManager(&server, &dns); // Memory optimizations for WiFiManager - wifiManager.setDebugOutput(true); // Disable debug strings - wifiManager.setMinimumSignalQuality(20); // Reduce AP scan results - wifiManager.setRemoveDuplicateAPs(true); // Remove duplicate APs from memory + wifiManager.setDebugOutput(true); // Disable debug strings + wifiManager.setMinimumSignalQuality(20); // Reduce AP scan results + wifiManager.setRemoveDuplicateAPs(true); // Remove duplicate APs from memory - // Reduce timeouts to free memory faster - wifiManager.setTimeout(60); // Reduced from 180 - wifiManager.setConnectTimeout(15); // Faster connection attempts - wifiManager.setConfigPortalTimeout(60); // Shorter portal timeout - + wifiManager.setTimeout(60); // Reduced from 180 + wifiManager.setConnectTimeout(15); // Faster connection attempts + wifiManager.setConfigPortalTimeout(60); // Shorter portal timeout Serial.println("Deactivating Brownout detector..."); - 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(); if (wifiManager.autoConnect("HannaBox")) { - /* - server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) - { request->send_P(200, "text/html charset=UTF-8", index_html, processor); }); - */ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) - { + { webrequestActive = true; String htmlPath = getSysDir("index.html"); - if (SD.exists(htmlPath)) { + if (SD.exists(htmlPath)) + { AsyncWebServerResponse *response = request->beginResponse(SD, htmlPath, "text/html", false, processor); response->addHeader("Content-Type", "text/html; charset=UTF-8"); request->send(response); - } else { + } + else + { // Fallback: serve minimal error if file not found request->send(404, "text/plain", "ERROR: /system/index.html on SD Card not found!"); } webrequestActive = false; - - }); + }); server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request) - { + { webrequestActive = true; String cssPath = getSysDir("style.css"); - if (SD.exists(cssPath)) { + if (SD.exists(cssPath)) + { request->send(SD, cssPath, "text/css"); - } else { + } + else + { // Fallback: serve minimal CSS if file not found request->send(404, "text/plain", "ERROR: /system/style.css on SD Card not found!"); } webrequestActive = false; - }); server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request) - { + { webrequestActive = true; deactivateRFID(); activateSD(); String jsPath = getSysDir("script.js"); - if (SD.exists(jsPath)) { + if (SD.exists(jsPath)) + { request->send(SD, jsPath, "application/javascript"); - } else { + } + else + { // Fallback: serve minimal JS if file not found request->send(404, "text/plain", "ERROR: /system/script.js on SD Card not found!"); } webrequestActive = false; - }); server.on("/state", HTTP_GET, [](AsyncWebServerRequest *request) @@ -987,7 +1117,7 @@ void setup() { request->send(200, "text/plain", "previous"); - previous(); }); + previous(); }); server.on("/playbyid", HTTP_GET, id_song_action); @@ -997,22 +1127,23 @@ void setup() server.on("/edit_mapping", HTTP_POST, editMapping); - // run handleUpload function when any file is uploaded - server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request) { - request->send(200); - }, handleUpload); + // run handleUpload function when any file is uploaded + server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request) + { request->send(200); }, handleUpload); server.on("/move_file", HTTP_GET, handleMoveFile); server.on("/delete_file", HTTP_GET, handleDeleteFile); server.begin(); Serial.println("Wifi initialized."); - } else { + } + else + { Serial.println("Wifi timed out. Fallback no Wifi."); } Serial.println("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( loop2, /* Function to implement the task */ @@ -1027,7 +1158,7 @@ void setup() Serial.println("initialization done."); } -void id_song_action(AsyncWebServerRequest *request) +void id_song_action(AsyncWebServerRequest *request) { Serial.println("song by id!"); @@ -1044,7 +1175,6 @@ void id_song_action(AsyncWebServerRequest *request) request->send_P(200, "text/plain", "ok"); } - void progress_action(AsyncWebServerRequest *request) { @@ -1077,9 +1207,9 @@ void volume_action(AsyncWebServerRequest *request) request->send_P(200, "text/plain", "ok"); } - -const String getSysDir(const String filename) { - return "/"+sys_dir+"/"+filename; +const String getSysDir(const String filename) +{ + return "/" + sys_dir + "/" + filename; } void loop() @@ -1103,37 +1233,39 @@ void loop() start(); } - if (continuePlaying && !webrequestActive) { + if (continuePlaying && !webrequestActive) + { continuePlaying = false; startupSoundPlayed = true; - playSongById(currentSongId,currentSongSeconds); - } else if (!startupSoundPlayed) { + playSongById(currentSongId, currentSongSeconds); + } + else if (!startupSoundPlayed) + { startupSoundPlayed = true; playSongByPath(getSysDir(startup_sound)); } - - // send device to sleep: long now = millis(); - if (!sleepSoundPlayed && now - lastInteraction > sleepMessageDelay) { + if (!sleepSoundPlayed && now - lastInteraction > config.sleepMessageDelay) + { sleepSoundPlayed = true; prepareSleepMode = true; - if (currentNode != NULL) { + if (currentNode != NULL) + { String progressPath = getSysDir(progress_file); - - writeSongProgress(progressPath.c_str(),currentNode->getCurrentPlayingId(),currentNode->getSecondsPlayed()); + writeSongProgress(progressPath.c_str(), currentNode->getCurrentPlayingId(), currentNode->getSecondsPlayed()); } String tempPath = getSysDir(sleep_sound); playSongByPath(tempPath.c_str()); - } - - - if (now - lastInteraction > sleepDelay) { + } + + if (now - lastInteraction > config.sleepDelay) + { Serial.println("entering deep sleep..."); - deactivateRFID(); + deactivateRFID(); deactivateSD(); esp_deep_sleep_start(); } @@ -1146,38 +1278,41 @@ void loop() else if (asyncNext) { asyncNext = false; - if (audio.isRunning()) { + if (audio.isRunning()) + { next(); - } else { + } + else + { uint8_t vol = audio.getVolume(); - if (vol!=MAX_VOL) { + if (vol != config.maxVolume) + { vol++; } audio.setVolume(vol); playSongByPath(getSysDir(startup_sound)); - } - } else if (asyncPrev) { asyncPrev = false; - if (audio.isRunning()) { + if (audio.isRunning()) + { previous(); - } else { + } + else + { uint8_t vol = audio.getVolume(); - if (vol!=0) { + if (vol != 0) + { vol--; } audio.setVolume(vol); playSongByPath(getSysDir(startup_sound)); } - } - - - if (loopCounter % RFID_LOOP_INTERVAL == 0 && !webrequestActive) + if (loopCounter % config.rfidLoopInterval == 0 && !webrequestActive) { deactivateSD(); activateRFID(); @@ -1193,18 +1328,23 @@ void loop() { lastVoltage = getBatteryVoltageMv(); free_heap = xPortGetFreeHeapSize(); - if (lastVoltage3) { + if (lastVoltage < config.minVoltage) + { + if (voltage_threshold_counter > 3) + { Serial.println("entering deep sleep due to low voltage..."); - lastInteraction = millis() - sleepMessageDelay; + lastInteraction = millis() - config.sleepMessageDelay; voltage_threshold_counter = 0; - } else { + } + else + { voltage_threshold_counter++; } - } else { + } + else + { voltage_threshold_counter = 0; } - } loopCounter++; @@ -1213,10 +1353,10 @@ void loop() void loop2(void *parameter) { bool loggingDone = false; - + for (;;) { - + if (buttonPressed(BTN_NEXT)) { asyncNext = true; @@ -1229,13 +1369,13 @@ void loop2(void *parameter) { asyncTogglePlayPause = true; } - if (!loggingDone) { + if (!loggingDone) + { Serial.println("loop2 started"); loggingDone = true; - } - //vTaskDelay(1); + } + // vTaskDelay(1); } - } boolean buttonPressed(const uint8_t pin) @@ -1248,14 +1388,15 @@ boolean buttonPressed(const uint8_t pin) if (now - lastStart > SHORT_PRESS_TIME) { lastStart = now; - Serial.println("button pressed."); buttontoignore = pin; lastInteraction = now; return true; } - } else if (digitalRead(pin) == HIGH && buttontoignore == pin) { - buttontoignore = 0; } - + else if (digitalRead(pin) == HIGH && buttontoignore == pin) + { + buttontoignore = 0; + } + return false; }