[ai] new option on how to continue for mapping

This commit is contained in:
Stefan Ostermann 2025-08-09 22:59:40 +02:00
parent ce863f4d02
commit 02cd5da886
4 changed files with 244 additions and 25 deletions

View File

@ -58,6 +58,15 @@ Audio audio;
uint volume = 7; 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 <vector>
static std::vector<std::pair<DirectoryNode*, int>> folderFlatList;
static int folderFlatIndex = -1;
static String folderRootPath = "";
// Pointer to the root DirectoryNode for active folder-mode playback
DirectoryNode* folderRootNode = nullptr;
AsyncWebServer server(80); AsyncWebServer server(80);
DNSServer dns; DNSServer dns;
@ -441,21 +450,103 @@ void playSongByRFID(String id)
return; return;
} }
auto songit = rfid_map.find(id); auto it = rfid_map.find(id);
if (songit == rfid_map.end()) if (it == rfid_map.end())
{ {
Serial.println("Song for UID not found: " + id); Serial.println("Song for UID not found: " + id);
return; 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; return;
} }
Serial.println("Searching for song: " + songit->second); Serial.print("RFID mapping found. Target: ");
playSongByName(songit->second); 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() void playNextMp3()
{ {
stop(); stop();
continuousMode = true; // Do not force continuous mode here; respect current global state.
if (currentNode == nullptr) if (currentNode == nullptr)
{ {
currentNode = rootNode.findFirstDirectoryWithMP3s(); currentNode = rootNode.findFirstDirectoryWithMP3s();
@ -661,9 +752,12 @@ void saveMappingToFile(const String filename)
{ {
for (const auto &pair : rfid_map) for (const auto &pair : rfid_map)
{ {
// Format: UID=target|mode
file.print(pair.first); file.print(pair.first);
file.print("="); // Using F() macro file.print("=");
file.println(pair.second); file.print(pair.second.target);
file.print("|");
file.println(pair.second.mode);
} }
file.close(); file.close();
Serial.println("Mapping saved to file."); Serial.println("Mapping saved to file.");
@ -683,7 +777,16 @@ void editMapping(AsyncWebServerRequest *request)
String song = request->getParam("song", true)->value(); String song = request->getParam("song", true)->value();
rfid.trim(); rfid.trim();
song.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)); saveMappingToFile(getSysDir(mapping_file));
request->send(200, "text/plain", "Mapping updated"); request->send(200, "text/plain", "Mapping updated");
} }
@ -693,7 +796,7 @@ void editMapping(AsyncWebServerRequest *request)
} }
} }
std::map<String, String> readDataFromFile(String filename) void readDataFromFile(String filename)
{ {
File file = SD.open(filename); File file = SD.open(filename);
@ -702,19 +805,33 @@ std::map<String, String> readDataFromFile(String filename)
{ {
while (file.available()) while (file.available())
{ {
// Read key and value from the file // Read key and raw value from the file
String line = file.readStringUntil('\n'); String line = file.readStringUntil('\n');
int separatorIndex = line.indexOf('='); int separatorIndex = line.indexOf('=');
if (separatorIndex != -1) if (separatorIndex != -1)
{ {
// Extract key and value // Extract key and raw value
String key = line.substring(0, separatorIndex).c_str(); String key = line.substring(0, separatorIndex);
String value = line.substring(separatorIndex + 1).c_str(); String raw = line.substring(separatorIndex + 1);
key.trim(); key.trim();
value.trim(); raw.trim();
Serial.println("found rfid mapping for " + value);
// 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 // Add key-value pair to the map
rfid_map[key] = value; rfid_map[key] = MappingEntry(target, mode);
} }
} }
file.close(); file.close();
@ -724,8 +841,6 @@ std::map<String, String> readDataFromFile(String filename)
Serial.print("Error opening file "); Serial.print("Error opening file ");
Serial.println(filename); Serial.println(filename);
} }
return rfid_map;
} }
String processor(const String &var) String processor(const String &var)
@ -744,7 +859,7 @@ String processor(const String &var)
{ {
char c = s[i]; char c = s[i];
if (c == '&') if (c == '&')
out += "&amp;"; out += "&";
else if (c == '<') else if (c == '<')
out += ""; out += "";
else if (c == '>') else if (c == '>')
@ -766,7 +881,11 @@ String processor(const String &var)
html.concat(F("<tr><td style='border:1px solid #ccc;padding:4px;'>")); html.concat(F("<tr><td style='border:1px solid #ccc;padding:4px;'>"));
html.concat(htmlEscape(pair.first)); html.concat(htmlEscape(pair.first));
html.concat(F("</td><td style='border:1px solid #ccc;padding:4px;'>")); html.concat(F("</td><td style='border:1px solid #ccc;padding:4px;'>"));
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("</td></tr>"); html.concat("</td></tr>");
} }
html.concat("</table>"); html.concat("</table>");
@ -915,6 +1034,77 @@ void previous()
void audio_eof_mp3(const char *info) void audio_eof_mp3(const char *info)
{ {
Serial.println("audio file ended."); 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) if (continuousMode && !prepareSleepMode)
playNextMp3(); playNextMp3();
} }

View File

@ -80,6 +80,23 @@ boolean continuePlaying = false;
boolean prepareSleepMode = false; boolean prepareSleepMode = false;
std::map<String, String> rfid_map; class DirectoryNode;
#endif // 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<String, MappingEntry> 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

View File

@ -117,6 +117,14 @@
<label for="song">Song:</label> <label for="song">Song:</label>
<input type="text" id="song" name="song" required> <input type="text" id="song" name="song" required>
</div> </div>
<div>
<label for="mode">Mode:</label>
<select id="mode" name="mode">
<option value="s">Single (play selected song / file)</option>
<option value="f">Folder (play selected folder, then stop)</option>
<option value="c">Continuous (continuous playback / loop folder)</option>
</select>
</div>
<button type="button" class="action-btn" style="grid-column: 1 / -1;" onclick="editMapping()">Update Mapping</button> <button type="button" class="action-btn" style="grid-column: 1 / -1;" onclick="editMapping()">Update Mapping</button>
</form> </form>

View File

@ -164,13 +164,17 @@ function playNamedSong(song) {
function editMapping() { function editMapping() {
var rfid = document.getElementById('rfid').value; var rfid = document.getElementById('rfid').value;
var song = document.getElementById('song').value; var song = document.getElementById('song').value;
var modeEl = document.getElementById('mode');
var mode = modeEl ? modeEl.value : 's';
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("POST", "/edit_mapping", true); xhr.open("POST", "/edit_mapping", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); 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() { xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) { if (xhr.readyState === 4 && xhr.status === 200) {
alert("Mapping updated successfully!"); alert("Mapping updated successfully!");
} else if (xhr.readyState === 4) {
alert("Failed to update mapping: " + (xhr.responseText || xhr.status));
} }
}; };
} }