diff --git a/src/main.cpp b/src/main.cpp index 53704e6..bafd0ec 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -58,6 +58,15 @@ Audio audio; uint volume = 7; + // Folder-play tracking: flattened list of files inside a mapped folder and current index + // Used when a mapping targets a folder (play folder once or loop folder) + #include + static std::vector> folderFlatList; + static int folderFlatIndex = -1; + static String folderRootPath = ""; + // Pointer to the root DirectoryNode for active folder-mode playback + DirectoryNode* folderRootNode = nullptr; + AsyncWebServer server(80); DNSServer dns; @@ -441,21 +450,103 @@ void playSongByRFID(String id) return; } - auto songit = rfid_map.find(id); - if (songit == rfid_map.end()) + auto it = rfid_map.find(id); + if (it == rfid_map.end()) { Serial.println("Song for UID not found: " + id); return; } - if (songit->second.length() == 0) + MappingEntry entry = it->second; + if (entry.target.length() == 0) { - Serial.println("Empty song name mapped to: " + id); + Serial.println("Empty mapping target for UID: " + id); return; } - Serial.println("Searching for song: " + songit->second); - playSongByName(songit->second); + Serial.print("RFID mapping found. Target: "); + Serial.print(entry.target); + Serial.print(" Mode: "); + Serial.println(entry.mode); + + // Reset folder tracking + folderFlatList.clear(); + folderFlatIndex = -1; + folderRootPath = ""; + folderModeActive = false; + + // Set continuous mode based on mapping ('c' => continuous, otherwise not) + continuousMode = (entry.mode == 'c'); + + // Try to locate the target in the directory tree + currentNode = rootNode.advanceToMP3(&entry.target); + if (currentNode == nullptr || currentNode->getCurrentPlaying() == nullptr) + { + Serial.println("No node/file found for mapping target: " + entry.target); + return; + } + + String mp3File = currentNode->getCurrentPlayingFilePath(); + if (mp3File.length() == 0) + { + Serial.println("Empty file path for mapping target: " + entry.target); + return; + } + + // Detect whether the mapping targeted a folder (matching a subdirectory name). + // advanceToMP3 returns the directory node if a subdirectory name was matched. + bool targetIsFolder = false; + if (!entry.target.startsWith("/") && entry.target == currentNode->getName()) + { + targetIsFolder = true; + } + + // If the mapping targets a folder (or explicitly 'f' mode), activate folder tracking + if (targetIsFolder || entry.mode == 'f') + { + folderModeActive = true; + folderRootNode = currentNode; + // Build flat list of files inside this folder for sequential/looped playback + folderFlatList.clear(); + folderRootNode->buildFlatMP3List(folderFlatList); + + // Find index of current playing file within the folder list + for (size_t i = 0; i < folderFlatList.size(); i++) + { + DirectoryNode *node = folderFlatList[i].first; + int fileIdx = folderFlatList[i].second; + if (node->getCurrentPlayingFilePath() == mp3File) + { + folderFlatIndex = (int)i; + break; + } + } + + // Compute root path for safety checks (path up to last '/') + int lastSlash = mp3File.lastIndexOf('/'); + if (lastSlash >= 0) + { + folderRootPath = mp3File.substring(0, lastSlash + 1); // include trailing slash + } + } + + Serial.print("Playing mapped target: "); + Serial.println(mp3File); + + deactivateRFID(); + activateSD(); + + if (!playFile(mp3File.c_str())) + { + Serial.println("Failed to play mapped file: " + mp3File); + currentNode = nullptr; + activateRFID(); + deactivateSD(); + return; + } + + activateRFID(); + deactivateSD(); } /** @@ -480,7 +571,7 @@ bool playFile(const char *filename, uint32_t resumeFilePos) void playNextMp3() { stop(); - continuousMode = true; + // Do not force continuous mode here; respect current global state. if (currentNode == nullptr) { currentNode = rootNode.findFirstDirectoryWithMP3s(); @@ -661,9 +752,12 @@ void saveMappingToFile(const String filename) { for (const auto &pair : rfid_map) { + // Format: UID=target|mode file.print(pair.first); - file.print("="); // Using F() macro - file.println(pair.second); + file.print("="); + file.print(pair.second.target); + file.print("|"); + file.println(pair.second.mode); } file.close(); Serial.println("Mapping saved to file."); @@ -683,7 +777,16 @@ void editMapping(AsyncWebServerRequest *request) String song = request->getParam("song", true)->value(); rfid.trim(); song.trim(); - rfid_map[rfid] = song; + + char mode = 's'; + if (request->hasParam("mode", true)) + { + String mStr = request->getParam("mode", true)->value(); + if (mStr.length() > 0) + mode = mStr.charAt(0); + } + + rfid_map[rfid] = MappingEntry(song, mode); saveMappingToFile(getSysDir(mapping_file)); request->send(200, "text/plain", "Mapping updated"); } @@ -693,7 +796,7 @@ void editMapping(AsyncWebServerRequest *request) } } -std::map readDataFromFile(String filename) +void readDataFromFile(String filename) { File file = SD.open(filename); @@ -702,19 +805,33 @@ std::map readDataFromFile(String filename) { while (file.available()) { - // Read key and value from the file + // Read key and raw 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(); + // Extract key and raw value + String key = line.substring(0, separatorIndex); + String raw = line.substring(separatorIndex + 1); key.trim(); - value.trim(); - Serial.println("found rfid mapping for " + value); + raw.trim(); + + // Support optional mode delimited by '|' => target|mode + String target = raw; + char mode = 's'; + int delim = raw.indexOf('|'); + if (delim != -1) + { + target = raw.substring(0, delim); + String mstr = raw.substring(delim + 1); + mstr.trim(); + if (mstr.length() > 0) + mode = mstr.charAt(0); + } + + Serial.println("found rfid mapping for " + target + " mode " + String(mode)); // Add key-value pair to the map - rfid_map[key] = value; + rfid_map[key] = MappingEntry(target, mode); } } file.close(); @@ -724,8 +841,6 @@ std::map readDataFromFile(String filename) Serial.print("Error opening file "); Serial.println(filename); } - - return rfid_map; } String processor(const String &var) @@ -744,7 +859,7 @@ String processor(const String &var) { char c = s[i]; if (c == '&') - out += "&"; + out += "&"; else if (c == '<') out += ""; else if (c == '>') @@ -766,7 +881,11 @@ String processor(const String &var) html.concat(F("")); html.concat(htmlEscape(pair.first)); html.concat(F("")); - html.concat(htmlEscape(pair.second)); + // Show target and mode (e.g. "mysong.mp3|s") + String mappingVal = pair.second.target; + mappingVal += "|"; + mappingVal += pair.second.mode; + html.concat(htmlEscape(mappingVal)); html.concat(""); } html.concat(""); @@ -915,6 +1034,77 @@ void previous() void audio_eof_mp3(const char *info) { Serial.println("audio file ended."); + if (prepareSleepMode) + return; + + // If folder-mode is active, advance only inside that folder. + if (folderModeActive && folderRootNode != nullptr) + { + // Ensure flat list is built + if (folderFlatList.empty()) + folderRootNode->buildFlatMP3List(folderFlatList); + + // Try to find current index if not set + if (folderFlatIndex < 0) + { + String cur = currentNode ? currentNode->getCurrentPlayingFilePath() : String(); + for (size_t i = 0; i < folderFlatList.size(); i++) + { + if (folderFlatList[i].first->getCurrentPlayingFilePath() == cur) + { + folderFlatIndex = (int)i; + break; + } + } + } + + if (folderFlatIndex >= 0 && folderFlatIndex < (int)folderFlatList.size() - 1) + { + // Advance to next file in the folder + folderFlatIndex++; + DirectoryNode *nextNode = folderFlatList[folderFlatIndex].first; + int fileIdx = folderFlatList[folderFlatIndex].second; + nextNode->setCurrentPlaying(&nextNode->getMP3Files()[fileIdx]); + currentNode = nextNode; + currentNode->setSecondsPlayed(0); + deactivateRFID(); + activateSD(); + playFile(currentNode->getCurrentPlayingFilePath().c_str()); + activateRFID(); + deactivateSD(); + } + else + { + // Reached end of folder list + if (continuousMode && !folderFlatList.empty()) + { + // Loop back to first in folder + folderFlatIndex = 0; + DirectoryNode *nextNode = folderFlatList[folderFlatIndex].first; + int fileIdx = folderFlatList[folderFlatIndex].second; + nextNode->setCurrentPlaying(&nextNode->getMP3Files()[fileIdx]); + currentNode = nextNode; + currentNode->setSecondsPlayed(0); + deactivateRFID(); + activateSD(); + playFile(currentNode->getCurrentPlayingFilePath().c_str()); + activateRFID(); + deactivateSD(); + } + else + { + // Stop playback and clear folder mode + folderModeActive = false; + folderRootNode = nullptr; + folderFlatList.clear(); + folderFlatIndex = -1; + stop(); + } + } + return; + } + + // Default behavior: if continuous mode is enabled, go to next globally if (continuousMode && !prepareSleepMode) playNextMp3(); } diff --git a/src/main.h b/src/main.h index cc9a9f6..d43312e 100644 --- a/src/main.h +++ b/src/main.h @@ -80,6 +80,23 @@ boolean continuePlaying = false; boolean prepareSleepMode = false; -std::map rfid_map; +class DirectoryNode; -#endif \ No newline at end of file +// Mapping entry that stores target (file or folder) and playback mode: +// 's' = single (default) - play only the selected song (or single file in folder) +// 'f' = folder - play files inside the selected folder, then stop +// 'c' = continuous - continuously play (like previous continuousMode) +struct MappingEntry { + String target; + char mode; + MappingEntry() : target(""), mode('s') {} + MappingEntry(const String& t, char m) : target(t), mode(m) {} +}; + +std::map rfid_map; + +// Folder-play helper: when a mapping requests "folder only" playback we keep +// track of the folder root node so EOF handling can advance only inside that folder. +bool folderModeActive = false; + +#endif diff --git a/web/index.html b/web/index.html index b6b12bc..367601d 100644 --- a/web/index.html +++ b/web/index.html @@ -117,6 +117,14 @@ +
+ + +
diff --git a/web/script.js b/web/script.js index 10d7fa8..80e0141 100644 --- a/web/script.js +++ b/web/script.js @@ -164,13 +164,17 @@ function playNamedSong(song) { function editMapping() { var rfid = document.getElementById('rfid').value; var song = document.getElementById('song').value; + var modeEl = document.getElementById('mode'); + var mode = modeEl ? modeEl.value : 's'; var xhr = new XMLHttpRequest(); xhr.open("POST", "/edit_mapping", true); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); - xhr.send("rfid=" + encodeURIComponent(rfid) + "&song=" + encodeURIComponent(song)); + xhr.send("rfid=" + encodeURIComponent(rfid) + "&song=" + encodeURIComponent(song) + "&mode=" + encodeURIComponent(mode)); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { alert("Mapping updated successfully!"); + } else if (xhr.readyState === 4) { + alert("Failed to update mapping: " + (xhr.responseText || xhr.status)); } }; }