#if defined(ESP8266) #include //https://github.com/esp8266/Arduino #else #include #endif #include //Local WebServer used to serve the configuration portal #include //https://github.com/tzapu/WiFiManager WiFi Configuration Magic #include #include "Audio.h" #include #include #include //RFID Reader #include // define pins for RFID #define CS_RFID 32 // SIC, tried 4 and 32 but only this worked! #define RST_RFID 33 // this does not work as the irq pin is input only: #define IRQ_RFID 34 // Audio DAC #define I2S_DOUT 26 // connect to DAC pin DIN #define I2S_BCLK 27 // connect to DAC pin BCK #define I2S_LRC 25 // connect to DAC pin LCK #define BTN_START_STOP 4 // Button on XX and GND #define BTN_NEXT 17 #define BTN_PREV 16 #define CS_SDCARD 22 #define BAT_VOLTAGE_PIN 35 #define RFID_LOOP_INTERVAL 25 #define VOLTAGE_LOOP_INTERVAL 5000 #define VOLTAGE_THRESHOLD 0 #define SHORT_PRESS_TIME 250 #define LONG_PRESS_TIME 1000 #define MAX_VOL 15 #include "globals.h" #include "main.h" #include "DirectoryNode.h" #include "config.h" File root; File mp3File; Audio audio; uint volume = 7; AsyncWebServer server(80); DNSServer dns; // 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. TaskHandle_t RfidTask; bool asyncStop = false; bool asyncStart = false; bool asyncTogglePlayPause = false; bool asyncNext = false; bool asyncPrev = false; bool SDActive = false; bool RFIDActive = false; bool webrequestActive = false; uint16_t voltage_threshold_counter = 0; size_t free_heap = 0; void activateSD() { if (SDActive) return; if (!SD.begin(CS_SDCARD)) { Serial.println("SD initialization failed!"); } SDActive = true; } void deactivateSD() { if (SDActive) { digitalWrite(CS_SDCARD, HIGH); SDActive = false; } } void activateRFID() { SPI.begin(-1, -1, -1, CS_RFID); rfid.PCD_Init(CS_RFID, RST_RFID); RFIDActive = true; } void deactivateRFID() { if (RFIDActive) { digitalWrite(CS_RFID, HIGH); RFIDActive = false; } } // 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"; } 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) { 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")) { 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"); return; } // Pre-allocate log buffer logBuffer.reserve(128); logBuffer = "Upload Start: "; logBuffer += filename; logBuffer += " (Free: "; logBuffer += String(freeSpace); logBuffer += "MB)"; Serial.println(logBuffer); 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)) { String baseName = filename.substring(0, filename.lastIndexOf('.')); String extension = filename.substring(filename.lastIndexOf('.')); int counter = 1; do { filepath = "/" + baseName + "_" + String(counter) + extension; counter++; } while (SD.exists(filepath) && 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) { request->send(500, "text/plain", "Failed to create file on SD card"); return; } } if (len) { // Check if file handle is valid 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) { 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 request->_tempFile.flush(); } // Reduce logging frequency to save memory - log every 200KB instead of 100KB if (len && (index % 204800 == 0)) { logBuffer = "Upload: "; logBuffer += humanReadableSize(index + len); Serial.println(logBuffer); logBuffer.clear(); } } 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 { request->send(500, "text/plain", "Upload failed - file handle was invalid"); } } } void handleMoveFile(AsyncWebServerRequest *request) { String from = request->arg("from"); String to = request->arg("to"); 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 { Serial.println("File not found: " + from); request->send(404, "text/plain", "File not found."); } } void handleDeleteFile(AsyncWebServerRequest *request) { String filename = request->arg("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 { Serial.println("File not found: " + filename); request->send(404, "text/plain", "File not found."); } } uint32_t getBatteryVoltageMv() { uint32_t voltage = analogReadMilliVolts(BAT_VOLTAGE_PIN); 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) { Serial.println("No node found for ID: " + String(id)); return; } // Check if the current playing song is valid if (currentNode->getCurrentPlaying() == nullptr) { currentNode = nullptr; Serial.println("No song found for ID: " + String(id)); return; } String mp3File = currentNode->getCurrentPlayingFilePath(); if (mp3File.length() == 0) { currentNode = nullptr; Serial.println("Empty file path for ID: " + String(id)); return; } Serial.print("Playing by ID: "); Serial.print(id); Serial.print(" "); Serial.println(continueSeconds); Serial.println(mp3File.c_str()); deactivateRFID(); activateSD(); if (!playFile(mp3File.c_str())) { Serial.println("Failed to play file: " + mp3File); currentNode = nullptr; activateRFID(); deactivateSD(); return; } if (continueSeconds != 0) { audio.setAudioPlayPosition(continueSeconds); } activateRFID(); deactivateSD(); } void playSongByName(String song) { if (song.length() == 0) { Serial.println("Empty song name provided"); return; } currentNode = rootNode.advanceToMP3(&song); if (currentNode == nullptr) { Serial.println("No node found for song: " + song); return; } // Check if the current playing song is valid if (currentNode->getCurrentPlaying() == nullptr) { currentNode = nullptr; Serial.println("No song found for name: " + song); return; } String mp3File = currentNode->getCurrentPlayingFilePath(); if (mp3File.length() == 0) { currentNode = nullptr; Serial.println("Empty file path for song: " + song); return; } Serial.println("Playing song: " + mp3File); deactivateRFID(); activateSD(); if (!playFile(mp3File.c_str())) { Serial.println("Failed to play file: " + mp3File); currentNode = nullptr; activateRFID(); deactivateSD(); return; } activateRFID(); deactivateSD(); } void playSongByPath(String path) { playFile(path.c_str()); } void playSongByRFID(String id) { if (id.length() == 0) { Serial.println("Empty RFID ID provided"); return; } auto songit = rfid_map.find(id); 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: " + id); return; } Serial.println("Searching for song: " + songit->second); playSongByName(songit->second); } /** * @brief Wrapper, so that we can intercept each call for other stuff. * * @param filename * @param resumeFilePos * @return true * @return false */ bool playFile(const char *filename, uint32_t resumeFilePos) { if (filename == nullptr || strlen(filename) == 0) { Serial.println("filename empty."); return false; } // return audio.connecttoFS(filename, resumeFilePos); return audio.connecttoFS(SD, filename, resumeFilePos); } void playNextMp3() { stop(); continuousMode = true; if (currentNode == NULL) { currentNode = rootNode.findFirstDirectoryWithMP3s(); if (currentNode) { currentNode->advanceToFirstMP3InThisNode(); } } else { currentNode = rootNode.advanceToNextMP3(currentNode->getCurrentPlaying()); } if (currentNode != NULL) { currentNode->setSecondsPlayed(0); } Serial.print("Now advancing to "); String mp3File = currentNode->getCurrentPlayingFilePath(); Serial.println(mp3File.c_str()); deactivateRFID(); activateSD(); playFile(mp3File.c_str()); activateRFID(); deactivateSD(); } void audio_info(const char *info) { // Serial.print("info "); Serial.println(info); } void mute() { if (audio.getVolume() != 0) { volume = audio.getVolume(); } audio.setVolume(0); } void unmute() { audio.setVolume(volume); } void writeSongProgress(const char *filename, uint16_t id, uint32_t seconds) { File file = SD.open(filename, FILE_WRITE); if (file) { file.print(id); file.print(" "); file.println(seconds); file.close(); 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) { File file = SD.open(filename); 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["playing"] = audio.isRunning(); if (currentNode != nullptr) jsonState["title"] = *currentNode->getCurrentPlaying(); else jsonState["title"] = "Angehalten"; if (currentNode != nullptr) jsonState["filepath"] = currentNode->getCurrentPlayingFilePath(); else jsonState["filepath"] = ""; jsonState["time"] = audio.getAudioCurrentTime(); jsonState["volume"] = audio.getVolume(); jsonState["length"] = audio.getAudioFileDuration(); jsonState["voltage"] = lastVoltage; jsonState["uid"] = lastUid; jsonState["heap"] = free_heap; String output; 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) { File file = SD.open(filename, FILE_WRITE); if (file) { for (const auto &pair : rfid_map) { file.println(pair.first + "=" + pair.second); } file.close(); Serial.println("Mapping saved to file."); } 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)) { String rfid = request->getParam("rfid", true)->value(); String song = request->getParam("song", true)->value(); rfid.trim(); song.trim(); rfid_map[rfid] = song; saveMappingToFile(getSysDir(mapping_file)); request->send(200, "text/plain", "Mapping updated"); } else { request->send(400, "text/plain", "Invalid parameters"); } } std::map readDataFromFile(String filename) { File file = SD.open(filename); if (file) { while (file.available()) { // Read key and value from the file String line = file.readStringUntil('\n'); int separatorIndex = line.indexOf('='); 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); // Add key-value pair to the map rfid_map[key] = value; } } file.close(); } else { Serial.print("Error opening file "); Serial.println(filename); } return rfid_map; } String processor(const String &var) { if (var == "DIRECTORY") { return rootNode.getDirectoryStructureHTML(); } if (var == "MAPPING") { auto htmlEscape = [](const String &s) -> String { String out; 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; } return out; }; String html = ""; for (const auto &pair : rfid_map) { html += ""; } html += "
RFIDSong
" + htmlEscape(pair.first) + "" + htmlEscape(pair.second) + "
"; return html; } return String(); // Return empty string instead of creating new String } void stop() { if (audio.isRunning()) { Serial.println("stopping audio."); audio.stopSong(); if (currentNode != NULL) { currentNode->setSecondsPlayed(0); } } } void start() { if (currentNode != NULL) { currentNode->setCurrentPlaying(NULL); currentNode = NULL; } playNextMp3(); } void togglePlayPause() { if (currentNode != NULL) { writeSongProgress(getSysDir(progress_file).c_str(), currentNode->getCurrentPlayingId(), currentNode->getSecondsPlayed()); audio.pauseResume(); } else { playNextMp3(); } lastInteraction = millis(); } void next() { playNextMp3(); lastInteraction = millis(); } void previous() { lastInteraction = millis(); if (currentNode == NULL) { Serial.println("previous(): currentNode is null"); return; } // Validate current state const String *currentSong = currentNode->getCurrentPlaying(); if (currentSong == NULL) { Serial.println("previous(): currentPlaying is null, cannot go to previous"); 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"); // 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); } } } void audio_eof_mp3(const char *info) { Serial.println("audio file ended."); if (continuousMode && !prepareSleepMode) playNextMp3(); } /* not working, FIXME remove me! */ void IRAM_ATTR rfid_interrupt() { newRfidInt = true; } void readRFID() { rfid.PICC_ReadCardSerial(); String newUid = getRFIDString(rfid.uid.uidByte); if (newUid == lastUid) { return; } stop(); lastUid = newUid; Serial.print("Card UID: "); Serial.println(lastUid); // rfid.PICC_DumpDetailsToSerial(&(rfid.uid)); playSongByRFID(lastUid); lastInteraction = millis(); } void setup() { 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(CS_RFID, OUTPUT); pinMode(CS_SDCARD, OUTPUT); digitalWrite(CS_RFID, HIGH); digitalWrite(CS_SDCARD, HIGH); RFIDActive = false; SDActive = false; Serial.print("Initializing SD card..."); activateSD(); Serial.println("SD initialization done."); // 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 = config.startAtStoredProgress && readSongProgress(progressPath.c_str()); if (continuePlaying) { Serial.print("deleting "); Serial.println(progressPath); SD.remove(progressPath); } deactivateSD(); activateRFID(); Serial.println("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!"); } */ audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT); 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!) Serial.println("Audio initialized."); lastVoltage = getBatteryVoltageMv(); free_heap = xPortGetFreeHeapSize(); 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 // Reduce timeouts to free memory faster 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 // wifiManager.resetSettings(); if (wifiManager.autoConnect("HannaBox")) { server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { webrequestActive = true; String htmlPath = getSysDir("index.html"); 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 { // 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)) { request->send(SD, cssPath, "text/css"); } 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)) { request->send(SD, jsPath, "application/javascript"); } 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) { String state = getState(); request->send(200, "application/json charset=UTF-8", state.c_str()); }); server.on("/start", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "text/plain charset=UTF-8", "start"); start(); }); server.on("/toggleplaypause", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "text/plain charset=UTF-8", "toggleplaypause"); togglePlayPause(); }); server.on("/stop", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "text/plain", "stop"); stop(); }); server.on("/next", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "text/plain", "next"); next(); }); server.on("/previous", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "text/plain", "previous"); previous(); }); server.on("/playbyid", HTTP_GET, id_song_action); server.on("/progress", HTTP_POST, progress_action); server.on("/volume", HTTP_POST, volume_action); 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); server.on("/move_file", HTTP_GET, handleMoveFile); server.on("/delete_file", HTTP_GET, handleDeleteFile); server.begin(); Serial.println("Wifi initialized."); } 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 xTaskCreatePinnedToCore( loop2, /* Function to implement the task */ "RFIDTask", /* Name of the task */ 4096, /* Stack size in words - reduced from 10000 to 4096 (optimization 2) */ NULL, /* Task input parameter */ 0, /* Priority of the task */ &RfidTask, /* Task handle. */ 0); /* Core where the task should run */ lastInteraction = millis(); Serial.println("initialization done."); } void id_song_action(AsyncWebServerRequest *request) { Serial.println("song by id!"); int params = request->params(); for (int i = 0; i < params; i++) { const AsyncWebParameter *p = request->getParam(i); if (p->name() == "id") { playSongById(atoi(p->value().c_str())); } } lastInteraction = millis(); request->send_P(200, "text/plain", "ok"); } void progress_action(AsyncWebServerRequest *request) { int params = request->params(); for (int i = 0; i < params; i++) { const AsyncWebParameter *p = request->getParam(i); if (p->name() == "value") { audio.setAudioPlayPosition(atoi(p->value().c_str())); } } lastInteraction = millis(); request->send_P(200, "text/plain", "ok"); } void volume_action(AsyncWebServerRequest *request) { int params = request->params(); for (int i = 0; i < params; i++) { const AsyncWebParameter *p = request->getParam(i); if (p->name() == "value") { audio.setVolume(atoi(p->value().c_str())); } } lastInteraction = millis(); request->send_P(200, "text/plain", "ok"); } const String getSysDir(const String filename) { return "/" + sys_dir + "/" + filename; } void loop() { if (audio.isRunning()) { if (asyncStop) { asyncStop = false; stop(); } audio.loop(); if (currentNode != nullptr && !prepareSleepMode) { currentNode->setSecondsPlayed(audio.getAudioCurrentTime()); } } else if (asyncStart) { asyncStart = false; start(); } if (continuePlaying && !webrequestActive) { continuePlaying = false; startupSoundPlayed = true; playSongById(currentSongId, currentSongSeconds); } else if (!startupSoundPlayed) { startupSoundPlayed = true; playSongByPath(getSysDir(startup_sound)); } // send device to sleep: long now = millis(); if (!sleepSoundPlayed && now - lastInteraction > config.sleepMessageDelay) { sleepSoundPlayed = true; prepareSleepMode = true; if (currentNode != NULL) { String progressPath = getSysDir(progress_file); writeSongProgress(progressPath.c_str(), currentNode->getCurrentPlayingId(), currentNode->getSecondsPlayed()); } String tempPath = getSysDir(sleep_sound); playSongByPath(tempPath.c_str()); } if (now - lastInteraction > config.sleepDelay) { Serial.println("entering deep sleep..."); deactivateRFID(); deactivateSD(); esp_deep_sleep_start(); } if (asyncTogglePlayPause) { asyncTogglePlayPause = false; togglePlayPause(); } else if (asyncNext) { asyncNext = false; if (audio.isRunning()) { next(); } else { uint8_t vol = audio.getVolume(); if (vol != config.maxVolume) { vol++; } audio.setVolume(vol); playSongByPath(getSysDir(startup_sound)); } } else if (asyncPrev) { asyncPrev = false; if (audio.isRunning()) { previous(); } else { uint8_t vol = audio.getVolume(); if (vol != 0) { vol--; } audio.setVolume(vol); playSongByPath(getSysDir(startup_sound)); } } if (loopCounter % config.rfidLoopInterval == 0 && !webrequestActive) { deactivateSD(); activateRFID(); if (rfid.PICC_IsNewCardPresent()) { readRFID(); } deactivateRFID(); activateSD(); } if (loopCounter % VOLTAGE_LOOP_INTERVAL == 0) { lastVoltage = getBatteryVoltageMv(); free_heap = xPortGetFreeHeapSize(); if (lastVoltage < config.minVoltage) { if (voltage_threshold_counter > 3) { Serial.println("entering deep sleep due to low voltage..."); lastInteraction = millis() - config.sleepMessageDelay; voltage_threshold_counter = 0; } else { voltage_threshold_counter++; } } else { voltage_threshold_counter = 0; } } loopCounter++; } void loop2(void *parameter) { bool loggingDone = false; for (;;) { if (buttonPressed(BTN_NEXT)) { asyncNext = true; } if (buttonPressed(BTN_PREV)) { asyncPrev = true; } if (buttonPressed(BTN_START_STOP)) { asyncTogglePlayPause = true; } if (!loggingDone) { Serial.println("loop2 started"); loggingDone = true; } // vTaskDelay(1); } } boolean buttonPressed(const uint8_t pin) { if (digitalRead(pin) == LOW && buttontoignore != pin) { unsigned long now = millis(); if (now - lastStart > SHORT_PRESS_TIME) { lastStart = now; buttontoignore = pin; lastInteraction = now; return true; } } else if (digitalRead(pin) == HIGH && buttontoignore == pin) { buttontoignore = 0; } return false; }