hannabox/src/main.cpp

1403 lines
33 KiB
C++

#if defined(ESP8266)
#include <ESP8266WiFi.h> //https://github.com/esp8266/Arduino
#else
#include <WiFi.h>
#endif
#include <ESPAsyncWebServer.h> //Local WebServer used to serve the configuration portal
#include <ESPAsyncWiFiManager.h> //https://github.com/tzapu/WiFiManager WiFi Configuration Magic
#include <ArduinoJson.h>
#include "Audio.h"
#include <SPI.h>
#include <SD.h>
#include <MFRC522.h> //RFID Reader
#include <map>
// 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<String, String> 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 += "&amp;";
else if (c == '<')
out += "";
else if (c == '>')
out += "";
else if (c == '"')
out += "";
else if (c == '\'')
out += "";
else
out += c;
}
return out;
};
String html = "<table style='width:100%;border-collapse:collapse;'><tr><th style='border:1px solid #ccc;padding:4px;'>RFID</th><th style='border:1px solid #ccc;padding:4px;'>Song</th></tr>";
for (const auto &pair : rfid_map)
{
html += "<tr><td style='border:1px solid #ccc;padding:4px;'>" + htmlEscape(pair.first) + "</td><td style='border:1px solid #ccc;padding:4px;'>" + htmlEscape(pair.second) + "</td></tr>";
}
html += "</table>";
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;
}