1121 lines
27 KiB
C++
1121 lines
27 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
|
|
#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 "WebContent.h"
|
|
#include "DirectoryNode.h"
|
|
|
|
#define SOUND_STARTUP "start.mp3"
|
|
#define SOUND_SLEEP "sleep.mp3"
|
|
|
|
|
|
|
|
|
|
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 space");
|
|
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 % 4096 == 0) { // Flush every 4KB
|
|
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.c_str(), to.c_str());
|
|
Serial.println("Moved file: " + from + " to " + to);
|
|
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);
|
|
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) {
|
|
return;
|
|
}
|
|
|
|
String mp3File = currentNode->getCurrentPlayingFilePath();
|
|
Serial.print("playing by id: ");
|
|
Serial.print(id);Serial.print(" ");Serial.println(continueSeconds);
|
|
|
|
Serial.println(mp3File.c_str());
|
|
deactivateRFID();
|
|
activateSD();
|
|
|
|
if (currentNode != nullptr && currentNode->getCurrentPlaying()==nullptr) {
|
|
currentNode = nullptr;
|
|
Serial.println("no node by id found, exiting playSongById");
|
|
return;
|
|
}
|
|
|
|
playFile(mp3File.c_str());
|
|
if (continueSeconds!=0) {
|
|
audio.setAudioPlayPosition(continueSeconds);
|
|
}
|
|
activateRFID();
|
|
deactivateSD();
|
|
}
|
|
|
|
void playSongByName(String song)
|
|
{
|
|
currentNode = rootNode.advanceToMP3(&song);
|
|
String mp3File = currentNode->getCurrentPlayingFilePath();
|
|
Serial.println(mp3File.c_str());
|
|
deactivateRFID();
|
|
activateSD();
|
|
playFile(mp3File.c_str());
|
|
}
|
|
|
|
void playSongByPath(String path)
|
|
{
|
|
playFile(path.c_str());
|
|
}
|
|
|
|
void playSongByRFID(String id)
|
|
{
|
|
auto songit = rfid_map.find(id);
|
|
if (songit==rfid_map.end()) {
|
|
Serial.println("song for uid not found.");
|
|
return;
|
|
}
|
|
Serial.println("searching for ");
|
|
Serial.println(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 is 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("Data written to file: ID-" + String(id) + ", Number-" + String(seconds));
|
|
} else {
|
|
Serial.println("Error opening file for writing.");
|
|
}
|
|
}
|
|
|
|
boolean readSongProgress(const char *filename) {
|
|
String data = "";
|
|
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.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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"; // Store in flash
|
|
|
|
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(256); // Pre-allocate string buffer
|
|
serializeJson(jsonState, output);
|
|
|
|
return output;
|
|
}
|
|
|
|
// Function to save the rfid_map to the mapping file
|
|
void saveMappingToFile(const char *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_map[rfid] = song;
|
|
saveMappingToFile(mapping_file.c_str());
|
|
request->send(200, "text/plain", "Mapping updated");
|
|
} else {
|
|
request->send(400, "text/plain", "Invalid parameters");
|
|
}
|
|
}
|
|
|
|
std::map<String, String> readDataFromFile(const char *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();
|
|
// 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();
|
|
}
|
|
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)
|
|
{
|
|
audio.pauseResume();
|
|
}
|
|
else
|
|
{
|
|
playNextMp3();
|
|
}
|
|
lastInteraction = millis();
|
|
}
|
|
|
|
void next()
|
|
{
|
|
playNextMp3();
|
|
lastInteraction = millis();
|
|
}
|
|
|
|
void previous()
|
|
{
|
|
if (currentNode != NULL) {
|
|
|
|
const String* curr = currentNode->getCurrentPlaying();
|
|
DirectoryNode* newNode = currentNode->goToPreviousMP3();
|
|
|
|
if (newNode != NULL) {
|
|
if (curr == newNode->getCurrentPlaying()) {
|
|
// reset to 0 seconds playtime:
|
|
audio.setAudioPlayPosition(0);
|
|
} else {
|
|
// Update the current node and start playing the previous song
|
|
Serial.println("");
|
|
Serial.print("Playing previous song: ");
|
|
Serial.println(currentNode->getCurrentPlayingFilePath());
|
|
currentNode = newNode;
|
|
stop();
|
|
deactivateRFID();
|
|
activateSD();
|
|
playSongByPath(currentNode->getCurrentPlayingFilePath());
|
|
deactivateSD();
|
|
activateRFID();
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
lastInteraction = millis();
|
|
}
|
|
|
|
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()
|
|
{
|
|
// 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*/
|
|
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.");
|
|
|
|
|
|
//deep sleep wakeup
|
|
esp_sleep_enable_ext0_wakeup((gpio_num_t)BTN_START_STOP, LOW);
|
|
|
|
|
|
|
|
rootNode.buildDirectoryTree("/");
|
|
rootNode.printDirectoryTree();
|
|
|
|
readDataFromFile(mapping_file.c_str());
|
|
|
|
|
|
|
|
String progressPath = "/"+sys_dir+"/"+progress_file;
|
|
|
|
continuePlaying = readSongProgress(progressPath.c_str());
|
|
|
|
if (continuePlaying) {
|
|
Serial.print("deleting ");
|
|
Serial.println(progressPath.c_str());
|
|
SD.remove(progressPath.c_str());
|
|
}
|
|
|
|
|
|
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(volume); // 0...21
|
|
|
|
// 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(false); // 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)
|
|
{ request->send_P(200, "text/html charset=UTF-8", index_html, processor); });
|
|
*/
|
|
|
|
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
|
|
{
|
|
webrequestActive = true;
|
|
deactivateRFID();
|
|
activateSD();
|
|
String htmlPath = "/system/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(200, "text/plain", "ERROR: /system/index.html on SD Card not found!");
|
|
}
|
|
webrequestActive = false;
|
|
|
|
});
|
|
|
|
server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request)
|
|
{
|
|
webrequestActive = true;
|
|
deactivateRFID();
|
|
activateSD();
|
|
String cssPath = "/system/style.css";
|
|
if (SD.exists(cssPath)) {
|
|
request->send(SD, cssPath, "text/css");
|
|
} else {
|
|
// Fallback: serve minimal CSS if file not found
|
|
request->send(200, "text/css", "body{font-family:Arial;text-align:center;padding:20px;}");
|
|
}
|
|
webrequestActive = false;
|
|
|
|
});
|
|
|
|
server.on("/script.js", HTTP_GET, [](AsyncWebServerRequest *request)
|
|
{
|
|
webrequestActive = true;
|
|
deactivateRFID();
|
|
activateSD();
|
|
String jsPath = "/system/script.js";
|
|
if (SD.exists(jsPath)) {
|
|
request->send(SD, jsPath, "application/javascript");
|
|
} else {
|
|
// Fallback: serve minimal JS if file not found
|
|
request->send(200, "application/javascript", "console.log('JavaScript file not found on SD card');");
|
|
}
|
|
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");
|
|
}
|
|
|
|
String getStartupSoundDir() {
|
|
static String tempPath = "/"+sys_dir+"/"+startup_sound;
|
|
return tempPath;
|
|
}
|
|
|
|
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(getStartupSoundDir().c_str());
|
|
}
|
|
|
|
|
|
|
|
// send device to sleep:
|
|
long now = millis();
|
|
|
|
if (!sleepSoundPlayed && now - lastInteraction > sleepMessageDelay) {
|
|
sleepSoundPlayed = true;
|
|
prepareSleepMode = true;
|
|
if (currentNode != NULL) {
|
|
String progressPath = "/"+sys_dir+"/"+progress_file;
|
|
|
|
writeSongProgress(progressPath.c_str(),currentNode->getCurrentPlayingId(),currentNode->getSecondsPlayed());
|
|
}
|
|
|
|
String tempPath = "/"+sys_dir+"/"+sleep_sound;
|
|
playSongByPath(tempPath.c_str());
|
|
}
|
|
|
|
|
|
if (now - lastInteraction > 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!=MAX_VOL) {
|
|
vol++;
|
|
}
|
|
audio.setVolume(vol);
|
|
playSongByPath(getStartupSoundDir().c_str());
|
|
|
|
}
|
|
|
|
}
|
|
else if (asyncPrev)
|
|
{
|
|
asyncPrev = false;
|
|
if (audio.isRunning()) {
|
|
previous();
|
|
} else {
|
|
uint8_t vol = audio.getVolume();
|
|
if (vol!=0) {
|
|
vol--;
|
|
}
|
|
audio.setVolume(vol);
|
|
playSongByPath(getStartupSoundDir().c_str());
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (loopCounter % RFID_LOOP_INTERVAL == 0 && !webrequestActive)
|
|
{
|
|
deactivateSD();
|
|
activateRFID();
|
|
if (rfid.PICC_IsNewCardPresent())
|
|
{
|
|
readRFID();
|
|
}
|
|
deactivateRFID();
|
|
activateSD();
|
|
}
|
|
|
|
if (loopCounter % VOLTAGE_LOOP_INTERVAL == 0)
|
|
{
|
|
lastVoltage = getBatteryVoltageMv();
|
|
free_heap = xPortGetFreeHeapSize();
|
|
if (lastVoltage<VOLTAGE_THRESHOLD) {
|
|
if (voltage_threshold_counter>3) {
|
|
Serial.println("entering deep sleep due to low voltage...");
|
|
lastInteraction = millis() - 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;
|
|
Serial.println("button pressed.");
|
|
buttontoignore = pin;
|
|
lastInteraction = now;
|
|
return true;
|
|
}
|
|
} else if (digitalRead(pin) == HIGH && buttontoignore == pin) {
|
|
buttontoignore = 0;
|
|
}
|
|
|
|
return false;
|
|
}
|