diff --git a/Hardware-Plaforms/ESP32-A1S-Audiokit/README.md b/Hardware-Plaforms/ESP32-A1S-Audiokit/README.md new file mode 100644 index 0000000..73fe6aa --- /dev/null +++ b/Hardware-Plaforms/ESP32-A1S-Audiokit/README.md @@ -0,0 +1,56 @@ +# Tonuino based on ESP32-A1S Audio-Development-Kit + +AI-Tinker released (almost two years ago or so) a development-board that contains an ESP32-A1S, which means a DAC is already included upfront. It's sorta ready to use but in the end, to make it compatible with my Tonuino-fork, there's some work waiting. + +## Things to know +* As there's a lack of GPIOs, it's necessary to share a single SPI-instance by SD and RFID. +* As per default, [RFID-Lib](https://github.com/miguelbalboa/rfid/) doesn't support to run on non-default-SPI-GPIOs. So that's why I used a [modified one](https://github.com/madias123/STM32duino-RFID-Mp3-Player/tree/master/3d%20party%20libraries%20(modified)/MFRC522). +* The board provides 6 keys but due to lack of free GPIOs, we need them for other purposes. Additionaly the problem is, that all keys are equipped with capacitors (maybe to debounce) which makes it hard to use those GPIOs for other purposes. That's why I unsoldered R66, 67, 68, 69, 70 (all 0 Ohms) to free these GPIOs from the capacitors. +* Please note: key1 still works but if you additionaly want to use keys2-6, you can use GPIO36 along with analogRead() by using voltage-dividers. But first one has to calculate + solder resistor-pairs 56/61, 57/62, 58/63, 59/64 to 'build' those voltage-dividers. Without doing that online Key1 is usable as it doesn't need resistors. However, didn't solder/test dividers so far. +* When switching over to use analogRead() one has to modify buttonHandler() in my code. +* Additionaly I unsoldered resistor R14 in order to deactivate LED D4 (probably not necessary) + +## GPIOS +Please note: You need to unsolder R66, 67, 68, 69, 70 first to use the board that way! + +| GPIO | Usage | Pin | Comment | +| ------------- | -----------------| ----------- | ------------------------------------------------------------ | +| 0 | internal (I2S) | MCLK | | +| 2 | SD (int.) + RFID | SPI.MISO | Solder wire to SD.7 for RFID | +| 4 | Rot. Encoder | BUTTON | Solder wire to SD.8 | +| 5 | Rot. Encoder | CLK | Via 14-pin-header | +| 12 | RFID | SPI.CS | Via JT_MTDI | +| 13 | internal (SD) | SPI.CS | CS for SD | +| 14 | SD (int.) + RFID | SPI.SCK | Via JT_MTMS | +| 15 | SD (int.) + RFID | SPI.MOSI | Via JT_MTDO | +| 18 | Rot. Encoder | DT | Via 14-pin-header | +| 19 | Power | | For switch on/off external hardware via MOSFET (optional) | +| 21 | internal (amp) | | To switch on/off amp | +| 22 | unused? | | Via 14-pin-header | +| 23 | Neopixel | DI | Via 14-pin-header | +| 25 | internal (I2S) | DSIN | | +| 26 | internal (I2S) | LRC | | +| 27 | internal (I2S) | BCLK | | +| 32 | internal (I2C) | CLK | | +| 33 | internal (I2C) | DATA | | +| 34 | internal (SD) | DETECT | To detect if SD is present (not used) | +| 35 | internal (I2S) | DOUT | | +| 36 | internal (Key) | A/D | Analog read for keys 1-6 or digial read for only key 1 | +| 39 | internal (HP) | DETECT | Detects if headphone is plugged in | + + +Now, question is what to do with GPIO22. Could be used to externally connect keys for analogRead() via voltage-dividers. + +## Power-consumption +* IDLE: 105 mA +* Deepsleep (without RFID + Neopixel): 30 mA + +## Things to consider +* I didn't battery-power this board (guess it works but didn't test). But as current in deepsleep still is 30 mA, this probably doesn't make sense. +* When using a switch (instead of putting 'only' to deepsleep), GPIO 19 (power) could be omitted. Along with GPIO 22, these buttons could be used as buttons 'previous' and 'next'. Third button could be omitted by assigning short-press-action of rotary-encoder for 'pause/play'. +* In order to safe energy consider the unsolder LEDs. + +## Please note +* This one is more or less a project to test whether it's possible to run my fork on this special board. It's not really maintained actively but feel free to contact me in case of questions. +* Some additional informations regarding this board can be found [here](https://www.mikrocontroller.net/topic/474383?goto=6429727) and [here] (https://www.mikrocontroller.net/topic/492149). +* I discovered RFID.RST is uninportant. So you need to initialize it but doesn't matter whether it's connected or not. So I used just anything. \ No newline at end of file diff --git a/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/A1S.pdf b/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/A1S.pdf new file mode 100644 index 0000000..00ccbb6 Binary files /dev/null and b/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/A1S.pdf differ diff --git a/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/ESP32-A1S Product Specification.pdf b/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/ESP32-A1S Product Specification.pdf new file mode 100644 index 0000000..19421ca Binary files /dev/null and b/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/ESP32-A1S Product Specification.pdf differ diff --git a/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/ESP32-A1S-Pinout.jpg b/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/ESP32-A1S-Pinout.jpg new file mode 100644 index 0000000..4fcbad0 Binary files /dev/null and b/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/ESP32-A1S-Pinout.jpg differ diff --git a/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/ESP32-Audio-Kit2.2 ff.png b/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/ESP32-Audio-Kit2.2 ff.png new file mode 100644 index 0000000..b4ccc0a Binary files /dev/null and b/Hardware-Plaforms/ESP32-A1S-Audiokit/docs/ESP32-Audio-Kit2.2 ff.png differ diff --git a/Hardware-Plaforms/ESP32-A1S-Audiokit/src/main.cpp b/Hardware-Plaforms/ESP32-A1S-Audiokit/src/main.cpp new file mode 100644 index 0000000..1ca5d91 --- /dev/null +++ b/Hardware-Plaforms/ESP32-A1S-Audiokit/src/main.cpp @@ -0,0 +1,3425 @@ +// Define modules to compile: +#define MQTT_ENABLE +#define FTP_ENABLE +#define NEOPIXEL_ENABLE // Don't forget configuration of NUM_LEDS +#define NEOPIXEL_REVERSE_ROTATION // Some Neopixels are adressed/soldered counter-clockwise. This can be configured here. +#define LANGUAGE 1 // 1 = deutsch; 2 = english + +#include +#include "Arduino.h" +#include +#ifdef FTP_ENABLE + #include "ESP32FtpServer.h" +#endif +#include "Audio.h" +#include "AC101.h" +#include "SPI.h" +#include "SD.h" +#include "FS.h" +#include "esp_task_wdt.h" +#include // https://github.com/madias123/STM32duino-RFID-Mp3-Player/tree/master/3d%20party%20libraries%20(modified)/MFRC522 +#include +#ifdef MQTT_ENABLE + #include +#endif +#include +#ifdef NEOPIXEL_ENABLE + #include +#endif + +#if LANGUAGE == 1 + #include "logmessages.h" + #include "websiteMgmt.h" + #include "websiteBasic.h" +#endif +#if LANGUAGE == 2 + #include "logmessages_EN.h" + #include "websiteMgmt_EN.h" + #include "websiteBasic_EN.h" +#endif + +#include +#include +#include +#include + + + +// Info-docs: +// https://docs.aws.amazon.com/de_de/freertos-kernel/latest/dg/queue-management.html +// https://arduino-esp8266.readthedocs.io/en/latest/PROGMEM.html#how-do-i-declare-a-global-flash-string-and-use-it + +// Loglevels +#define LOGLEVEL_ERROR 1 // only errors +#define LOGLEVEL_NOTICE 2 // errors + important messages +#define LOGLEVEL_INFO 3 // infos + errors + important messages +#define LOGLEVEL_DEBUG 4 // almost everything + +// Serial-logging-configuration +const uint8_t serialDebug = LOGLEVEL_INFO; // Current loglevel for serial console + +// Serial-logging buffer +char logBuf[160]; // Buffer for all log-messages + +// GPIOs (uSD card-reader) +#define SD_CS 13 // internal +#define SPI_MOSI 15 // JT_MTDO +#define SPI_MISO 2 // Solder to pin7 of SD +#define SPI_SCK 14 // JT_MTMS + +// GPIOs (RFID-readercurrentRfidTagId) +#define MFRC522_RST_PIN 35 // 14-pin-header +#define MFRC522_CS_PIN 12 // JT_MTDI +extern SPIClass SPI_MFRC; +MFRC522 mfrc522(MFRC522_CS_PIN, MFRC522_RST_PIN); + +// DAC (internal) +#define I2S_DSIN 25 // internal +#define I2S_BCLK 27 // internal +#define I2S_LRC 26 // internal +#define I2S_MCLK 0 // internal +#define I2S_DOUT 35 // internal + +// I2C GPIOs +#define IIC_CLK 32 // internal +#define IIC_DATA 33 // internal + +// GPIO used to trigger transistor-circuit / RFID-reader +#define POWER 19 + +// GPIOs (Rotary encoder) +#define DREHENCODER_CLK 5 +#define DREHENCODER_DT 18 +#define DREHENCODER_BUTTON 4 // Solder to pin8 of SD + +// GPIOs (Control-buttons) // Currently deactivated; please read README.md +/*#define PAUSEPLAY_BUTTON 5 +#define NEXT_BUTTON 4 +#define PREVIOUS_BUTTON 33*/ + +// Amp enable +#define GPIO_PA_EN 21 // internal + +// Headphone? +#define HEADPHONE_PLUGGED_IN 39 // internal + +// GPIOs (LEDs) +#define LED_PIN 23 + +// Neopixel-configuration +#ifdef NEOPIXEL_ENABLE + #define NUM_LEDS 24 // number of LEDs + #define CHIPSET WS2812B // type of Neopixel + #define COLOR_ORDER GRB +#endif + +// Track-Control +#define STOP 1 // Stop play +#define PLAY 2 // Start play (currently not used) +#define PAUSEPLAY 3 // Pause/play +#define NEXTTRACK 4 // Next track of playlist +#define PREVIOUSTRACK 5 // Previous track of playlist +#define FIRSTTRACK 6 // First track of playlist +#define LASTTRACK 7 // Last track of playlist + +// Playmodes +#define NO_PLAYLIST 0 // If no playlist is active +#define SINGLE_TRACK 1 // Play a single track +#define SINGLE_TRACK_LOOP 2 // Play a single track in infinite-loop +#define AUDIOBOOK 3 // Single track, can save last play-position +#define AUDIOBOOK_LOOP 4 // Single track as infinite-loop, can save last play-position +#define ALL_TRACKS_OF_DIR_SORTED 5 // Play all files of a directory (alph. sorted) +#define ALL_TRACKS_OF_DIR_RANDOM 6 // Play all files of a directory (randomized) +#define ALL_TRACKS_OF_DIR_SORTED_LOOP 7 // Play all files of a directory (alph. sorted) in infinite-loop +#define ALL_TRACKS_OF_DIR_RANDOM_LOOP 9 // Play all files of a directory (randomized) in infinite-loop +#define WEBSTREAM 8 // Play webradio-stream +#define BUSY 10 // Used if playlist is created + +// RFID-modifcation-types +#define LOCK_BUTTONS_MOD 100 // Locks all buttons and rotary encoder +#define SLEEP_TIMER_MOD_15 101 // Puts uC into deepsleep after 15 minutes + LED-DIMM +#define SLEEP_TIMER_MOD_30 102 // Puts uC into deepsleep after 30 minutes + LED-DIMM +#define SLEEP_TIMER_MOD_60 103 // Puts uC into deepsleep after 60 minutes + LED-DIMM +#define SLEEP_TIMER_MOD_120 104 // Puts uC into deepsleep after 120 minutes + LED-DIMM +#define SLEEP_AFTER_END_OF_TRACK 105 // Puts uC into deepsleep after track is finished + LED-DIMM +#define SLEEP_AFTER_END_OF_PLAYLIST 106 // Puts uC into deepsleep after playlist is finished + LED-DIMM +#define SLEEP_AFTER_5_TRACKS 107 // Puts uC into deepsleep after five tracks +#define REPEAT_PLAYLIST 110 // Changes active playmode to endless-loop (for a playlist) +#define REPEAT_TRACK 111 // Changes active playmode to endless-loop (for a single track) +#define DIMM_LEDS_NIGHTMODE 120 // Changes LED-brightness + +// Repeat-Modes +#define NO_REPEAT 0 +#define TRACK 1 +#define PLAYLIST 2 +#define TRACK_N_PLAYLIST 3 + +typedef struct { // Bit field + uint8_t playMode: 4; // playMode + char **playlist; // playlist + bool repeatCurrentTrack: 1; // If current track should be looped + bool repeatPlaylist: 1; // If whole playlist should be looped + uint16_t currentTrackNumber: 9; // Current tracknumber + uint16_t numberOfTracks: 9; // Number of tracks in playlist + unsigned long startAtFilePos; // Offset to start play (in bytes) + uint8_t currentRelPos: 7; // Current relative playPosition (in %) + bool sleepAfterCurrentTrack: 1; // If uC should go to sleep after current track + bool sleepAfterPlaylist: 1; // If uC should go to sleep after whole playlist + bool saveLastPlayPosition: 1; // If playposition/current track should be saved (for AUDIOBOOK) + char playRfidTag[13]; // ID of RFID-tag that started playlist + bool pausePlay: 1; // If pause is active + bool trackFinished: 1; // If current track is finished + bool playlistFinished: 1; // If whole playlist is finished + uint8_t playUntilTrackNumber: 6; // Number of tracks to play after which uC goes to sleep +} playProps; +playProps playProperties; + +typedef struct { + char nvsKey[13]; + char nvsEntry[275]; +} nvs_t; + +// Configuration of initial values (for the first start) goes here.... +// There's no need to change them here as they can be configured via webinterface +// Neopixel +uint8_t initialLedBrightness = 16; // Initial brightness of Neopixel +uint8_t ledBrightness = initialLedBrightness; +uint8_t nightLedBrightness = 2; // Brightness of Neopixel in nightmode + +// MQTT +bool enableMqtt = true; +#ifdef MQTT_ENABLE + uint8_t mqttFailCount = 3; // Number of times mqtt-reconnect is allowed to fail. If >= mqttFailCount to further reconnects take place + uint8_t const stillOnlineInterval = 60; // Interval 'I'm still alive' is sent via MQTT (in seconds) +#endif +// RFID +#define RFID_SCAN_INTERVAL 300 //in ms +uint8_t const cardIdSize = 4; // RFID +// Volume +uint8_t maxVolume = 21; // Maximum volume that can be adjusted +uint8_t minVolume = 0; // Lowest volume that can be adjusted +uint8_t initVolume = 3; // 0...21 (If not found in NVS, this one will be taken) +// Sleep +uint8_t maxInactivityTime = 10; // Time in minutes, after uC is put to deep sleep because of inactivity +uint8_t sleepTimer = 30; // Sleep timer in minutes that can be optionally used (and modified later via MQTT or RFID) +// FTP +char ftpUser[10] = "esp32"; // FTP-user +char ftpPassword[15] = "esp32"; // FTP-password + + +// Button-configuration (change according your needs) +uint8_t buttonDebounceInterval = 50; // Interval in ms to software-debounce buttons +uint16_t intervalToLongPress = 700; // Interval in ms to distinguish between short and long press of previous/next-button + +// Where to store the backup-file +static const char backupFile[] PROGMEM = "/backup.txt"; // File is written every time a (new) RFID-assignment via GUI is done + +// Don't change anything here unless you know what you're doing +// HELPER // +// WiFi +unsigned long wifiCheckLastTimestamp = 0; +// Neopixel +#ifdef NEOPIXEL_ENABLE + bool showLedError = false; + bool showLedOk = false; + bool showPlaylistProgress = false; + bool showRewind = false; +#endif +// MQTT +#ifdef MQTT_ENABLE + unsigned long lastOnlineTimestamp = 0; +#endif +// RFID +unsigned long lastRfidCheckTimestamp = 0; +char *currentRfidTagId = NULL; +// Sleep +unsigned long lastTimeActiveTimestamp = 0; // Timestamp of last user-interaction +unsigned long sleepTimerStartTimestamp = 0; // Flag if sleep-timer is active +bool gotoSleep = false; // Flag for turning uC immediately into deepsleep +bool lockControls = false; // Flag if buttons and rotary encoder is locked +bool bootComplete = false; +// Rotary encoder-helper +int32_t lastEncoderValue; +int32_t currentEncoderValue; +int32_t lastVolume = -1; // Don't change -1 as initial-value! +uint8_t currentVolume = initVolume; +//////////// + +// AP-WiFi +static const char accessPointNetworkSSID[] PROGMEM = "Tonuino"; // Access-point's SSID +IPAddress apIP(192, 168, 4, 1); // Access-point's static IP +IPAddress apNetmask(255, 255, 255, 0); // Access-point's netmask +bool accessPointStarted = false; + + +// MQTT-configuration +char mqtt_server[16] = "192.168.2.43"; // IP-address of MQTT-server (if not found in NVS this one will be taken) +char mqttUser[16] = "mqtt-user"; // MQTT-user +char mqttPassword[16] = "mqtt-password"; // MQTT-password +#ifdef MQTT_ENABLE + #define DEVICE_HOSTNAME "ESP32-Tonuino" // Name that that is used for MQTT + static const char topicSleepCmnd[] PROGMEM = "Cmnd/Tonuino/Sleep"; + static const char topicSleepState[] PROGMEM = "State/Tonuino/Sleep"; + static const char topicTrackCmnd[] PROGMEM = "Cmnd/Tonuino/Track"; + static const char topicTrackState[] PROGMEM = "State/Tonuino/Track"; + static const char topicTrackControlCmnd[] PROGMEM = "Cmnd/Tonuino/TrackControl"; + static const char topicLoudnessCmnd[] PROGMEM = "Cmnd/Tonuino/Loudness"; + static const char topicLoudnessState[] PROGMEM = "State/Tonuino/Loudness"; + static const char topicSleepTimerCmnd[] PROGMEM = "Cmnd/Tonuino/SleepTimer"; + static const char topicSleepTimerState[] PROGMEM = "State/Tonuino/SleepTimer"; + static const char topicState[] PROGMEM = "State/Tonuino/State"; + static const char topicCurrentIPv4IP[] PROGMEM = "State/Tonuino/IPv4"; + static const char topicLockControlsCmnd[] PROGMEM ="Cmnd/Tonuino/LockControls"; + static const char topicLockControlsState[] PROGMEM ="State/Tonuino/LockControls"; + static const char topicPlaymodeState[] PROGMEM = "State/Tonuino/Playmode"; + static const char topicRepeatModeCmnd[] PROGMEM = "Cmnd/Tonuino/RepeatMode"; + static const char topicRepeatModeState[] PROGMEM = "State/Tonuino/RepeatMode"; + static const char topicLedBrightnessCmnd[] PROGMEM = "Cmnd/Tonuino/LedBrightness"; + static const char topicLedBrightnessState[] PROGMEM = "State/Tonuino/LedBrightness"; +#endif + +char stringDelimiter[] = "#"; // Character used to encapsulate data in linear NVS-strings (don't change) +char stringOuterDelimiter[] = "^"; // Character used to encapsulate encapsulated data along with RFID-ID in backup-file + + +void notFound(AsyncWebServerRequest *request) { + request->send(404, "text/plain", "Not found"); +} +AsyncWebServer wServer(80); +AsyncWebSocket ws("/ws"); +AsyncEventSource events("/events"); + +static const char backupRecoveryWebsite[] PROGMEM = "

Das Backup-File wird eingespielt...
Zur letzten Seite zurückkehren.

"; +static const char restartWebsite[] PROGMEM = "

Der Tonuino wird neu gestartet...
Zur letzten Seite zurückkehren.

"; + + +// Audio/mp3 +SPIClass spiSD(HSPI); +TaskHandle_t mp3Play; +TaskHandle_t rfid; +#ifdef NEOPIXEL_ENABLE + TaskHandle_t LED; +#endif + +// FTP +#ifdef FTP_ENABLE + FtpServer ftpSrv; +#endif + +// Info: SSID / password are stored in NVS +WiFiClient wifiClient; +IPAddress myIP; + +// MQTT-helper +#ifdef MQTT_ENABLE + PubSubClient MQTTclient(wifiClient); +#endif + +// Rotary encoder-configuration +ESP32Encoder encoder; + +// HW-Timer +hw_timer_t *timer = NULL; +volatile SemaphoreHandle_t timerSemaphore; + +// Button-helper +typedef struct { + bool lastState; + bool currentState; + bool isPressed; + bool isReleased; + unsigned long lastPressedTimestamp; + unsigned long lastReleasedTimestamp; +} t_button; +t_button buttons[4]; + +Preferences prefsRfid; +Preferences prefsSettings; +static const char prefsRfidNamespace[] PROGMEM = "rfidTags"; // Namespace used to save IDs of rfid-tags +static const char prefsSettingsNamespace[] PROGMEM = "settings"; // Namespace used for generic settings + +QueueHandle_t volumeQueue; +QueueHandle_t trackQueue; +QueueHandle_t trackControlQueue; +QueueHandle_t rfidCardQueue; + + +// Prototypes +void accessPointStart(const char *SSID, IPAddress ip, IPAddress netmask); +static int arrSortHelper(const void* a, const void* b); +#ifdef MQTT_ENABLE + void callback(const char *topic, const byte *payload, uint32_t length); +#endif +void buttonHandler(); +void deepSleepManager(void); +void doButtonActions(void); +void doRfidCardModifications(const uint32_t mod); +bool dumpNvsToSd(char *_namespace, char *_destFile); +bool endsWith (const char *str, const char *suf); +bool fileValid(const char *_fileItem); +void freeMultiCharArray(char **arr, const uint32_t cnt); +uint8_t getRepeatMode(void); +bool isNumber(const char *str); +void loggerNl(const char *str, const uint8_t logLevel); +void logger(const char *str, const uint8_t logLevel); +#ifdef MQTT_ENABLE + bool publishMqtt(const char *topic, const char *payload, bool retained); +#endif +size_t nvsRfidWriteWrapper (const char *_rfidCardId, const char *_track, const uint32_t _playPosition, const uint8_t _playMode, const uint16_t _trackLastPlayed, const uint16_t _numberOfTracks); +void onWebsocketEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len); +#ifdef MQTT_ENABLE + void postHeartbeatViaMqtt(void); +#endif +bool processJsonRequest(char *_serialJson); +void randomizePlaylist (char *str[], const uint32_t count); +#ifdef MQTT_ENABLE + bool reconnect(); +#endif +char ** returnPlaylistFromWebstream(const char *_webUrl); +char ** returnPlaylistFromSD(File _fileOrDirectory); +void rfidScanner(void *parameter); +void sleepHandler(void) ; +void sortPlaylist(const char** arr, int n); +bool startsWith(const char *str, const char *pre); +String templateProcessor(const String& templ); +void trackControlToQueueSender(const uint8_t trackCommand); +void rfidPreferenceLookupHandler (void); +void sendWebsocketData(uint32_t client, uint8_t code); +void trackQueueDispatcher(const char *_sdFile, const uint32_t _lastPlayPos, const uint32_t _playMode, const uint16_t _trackLastPlayed); +void volumeHandler(const int32_t _minVolume, const int32_t _maxVolume); +void volumeToQueueSender(const int32_t _newVolume); +wl_status_t wifiManager(void); + + +/* Wrapper-Funktion for Serial-logging (with newline) */ +void loggerNl(const char *str, const uint8_t logLevel) { + if (serialDebug >= logLevel) { + Serial.println(str); + } +} + +/* Wrapper-Funktion for Serial-Logging (without newline) */ +void logger(const char *str, const uint8_t logLevel) { + if (serialDebug >= logLevel) { + Serial.print(str); + } +} + + +int countChars(const char* string, char ch) { + int count = 0; + int length = strlen(string); + + for (uint8_t i = 0; i < length; i++) { + if (string[i] == ch) { + count++; + } + } + + return count; +} + + +// Used to print content of sd-card (currently not used, maybe later :-)) +/*void printSdContent(File dir, uint16_t allocSize, uint8_t allocCount, char *sdContent, uint8_t depth) { + while (true) { + File entry = dir.openNextFile(); + if (!entry) { + dir.rewindDirectory(); + break; + } + + if (countChars(entry.name(), '/') > depth+1) { + continue; + } + + Serial.println(entry.name()); + + if ((strlen(sdContent) + strlen(entry.name()) + 2) >= allocCount * allocSize) { + sdContent = (char*) realloc(sdContent, ++allocCount * allocSize); + Serial.printf("Free heap: %u", ESP.getFreeHeap()); + Serial.printf("realloc! -%d-\n", allocCount); + if (sdContent == NULL) { + return; + } + } + strcat(sdContent, stringDelimiter); + strcat(sdContent, entry.name()); + + if (entry.isDirectory()) { + printSdContent(entry, allocSize, allocCount, sdContent, depth); + } + entry.close(); + } +}*/ + +void IRAM_ATTR onTimer() { + xSemaphoreGiveFromISR(timerSemaphore, NULL); +} + + +// If timer-semaphore is set, read buttons (unless controls are locked) +void buttonHandler() { + if (xSemaphoreTake(timerSemaphore, 0) == pdTRUE) { + if (lockControls) { + return; + } + unsigned long currentTimestamp = millis(); + //buttons[0].currentState = digitalRead(NEXT_BUTTON); + //buttons[1].currentState = digitalRead(PREVIOUS_BUTTON); + //buttons[2].currentState = digitalRead(PAUSEPLAY_BUTTON); + buttons[3].currentState = digitalRead(DREHENCODER_BUTTON); + + // Iterate over all buttons in struct-array + for (uint8_t i=0; i < sizeof(buttons) / sizeof(buttons[0]); i++) { + if (buttons[i].currentState != buttons[i].lastState && currentTimestamp - buttons[i].lastPressedTimestamp > buttonDebounceInterval) { + if (!buttons[i].currentState) { + buttons[i].isPressed = true; + buttons[i].lastPressedTimestamp = currentTimestamp; + } else { + buttons[i].isReleased = true; + buttons[i].lastReleasedTimestamp = currentTimestamp; + } + } + buttons[i].lastState = buttons[i].currentState; + } + } +} + + +// Do corresponding actions for all buttons +void doButtonActions(void) { + if (lockControls) { + return; // Avoid button-handling if buttons are locked + } + + for (uint8_t i=0; i < sizeof(buttons) / sizeof(buttons[0]); i++) { + if (buttons[i].isPressed) { + if (buttons[i].lastReleasedTimestamp > buttons[i].lastPressedTimestamp) { + if (buttons[i].lastReleasedTimestamp - buttons[i].lastPressedTimestamp >= intervalToLongPress) { + switch (i) // Long-press-actions + { + case 0: + trackControlToQueueSender(LASTTRACK); + buttons[i].isPressed = false; + break; + + case 1: + trackControlToQueueSender(FIRSTTRACK); + buttons[i].isPressed = false; + break; + + case 2: + trackControlToQueueSender(PAUSEPLAY); + buttons[i].isPressed = false; + break; + + case 3: + gotoSleep = true; + break; + } + } else { + switch (i) // Short-press-actions + { + case 0: + trackControlToQueueSender(NEXTTRACK); + buttons[i].isPressed = false; + break; + + case 1: + trackControlToQueueSender(PREVIOUSTRACK); + buttons[i].isPressed = false; + break; + + case 2: + trackControlToQueueSender(PAUSEPLAY); + buttons[i].isPressed = false; + break; + + case 3: + //gotoSleep = true; + break; + } + } + } + } + } +} + + +/* Wrapper-functions for MQTT-publish */ +#ifdef MQTT_ENABLE +bool publishMqtt(const char *topic, const char *payload, bool retained) { + if (strcmp(topic, "") != 0) { + if (MQTTclient.connected()) { + MQTTclient.publish(topic, payload, retained); + delay(100); + return true; + } + } + return false; +} + + +bool publishMqtt(const char *topic, int32_t payload, bool retained) { + char buf[11]; + snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%d", payload); + return publishMqtt(topic, buf, retained); +} + +bool publishMqtt(const char *topic, unsigned long payload, bool retained) { + char buf[11]; + snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%lu", payload); + return publishMqtt(topic, buf, retained); +} + +bool publishMqtt(const char *topic, uint32_t payload, bool retained) { + char buf[11]; + snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%u", payload); + return publishMqtt(topic, buf, retained); +} + + +/* Cyclic posting via MQTT that ESP is still alive */ +void postHeartbeatViaMqtt(void) { + if (millis() - lastOnlineTimestamp >= stillOnlineInterval*1000) { + lastOnlineTimestamp = millis(); + if (publishMqtt((char *) FPSTR(topicState), "Online", false)) { + loggerNl((char *) FPSTR(stillOnlineMqtt), LOGLEVEL_DEBUG); + } + } +} + + +/* Connects/reconnects to MQTT-Broker unless connection is not already available. + Manages MQTT-subscriptions. +*/ +bool reconnect() { + uint8_t maxRetries = 10; + uint8_t connect = false; + + while (!MQTTclient.connected() && mqttFailCount < maxRetries) { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s %s", (char *) FPSTR(tryConnectMqttS), mqtt_server); + loggerNl(logBuf, LOGLEVEL_NOTICE); + + // Try to connect to MQTT-server. If username AND password are set, they'll be used + if (strlen(mqttUser) < 1 || strlen(mqttPassword) < 1) { + loggerNl((char *) FPSTR(mqttWithoutPwd), LOGLEVEL_NOTICE); + if (MQTTclient.connect(DEVICE_HOSTNAME)) { + connect = true; + } + } else { + loggerNl((char *) FPSTR(mqttWithPwd), LOGLEVEL_NOTICE); + if (MQTTclient.connect(DEVICE_HOSTNAME, mqttUser, mqttPassword)) { + connect = true; + } + } + if (connect) { + loggerNl((char *) FPSTR(mqttOk), LOGLEVEL_NOTICE); + + // Deepsleep-subscription + MQTTclient.subscribe((char *) FPSTR(topicSleepCmnd)); + + // Trackname-subscription + MQTTclient.subscribe((char *) FPSTR(topicTrackCmnd)); + + // Loudness-subscription + MQTTclient.subscribe((char *) FPSTR(topicLoudnessCmnd)); + + // Sleep-Timer-subscription + MQTTclient.subscribe((char *) FPSTR(topicSleepTimerCmnd)); + + // Next/previous/stop/play-track-subscription + MQTTclient.subscribe((char *) FPSTR(topicTrackControlCmnd)); + + // Lock controls + MQTTclient.subscribe((char *) FPSTR(topicLockControlsCmnd)); + + // Current repeat-Mode + MQTTclient.subscribe((char *) FPSTR(topicRepeatModeCmnd)); + + // LED-brightness + MQTTclient.subscribe((char *) FPSTR(topicLedBrightnessCmnd)); + + // Publish some stuff + publishMqtt((char *) FPSTR(topicState), "Online", false); + publishMqtt((char *) FPSTR(topicTrackState), "---", false); + publishMqtt((char *) FPSTR(topicLoudnessState), currentVolume, false); + publishMqtt((char *) FPSTR(topicSleepTimerState), sleepTimerStartTimestamp, false); + publishMqtt((char *) FPSTR(topicLockControlsState), "OFF", false); + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + publishMqtt((char *) FPSTR(topicLedBrightnessState), ledBrightness, false); + publishMqtt((char *) FPSTR(topicRepeatModeState), 0, false); + + char currentIPString[16]; + sprintf(currentIPString, "%d.%d.%d.%d", myIP[0], myIP[1], myIP[2], myIP[3]); + publishMqtt((char *) FPSTR(topicCurrentIPv4IP), currentIPString, false); + + return MQTTclient.connected(); + } else { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: rc=%i (%d / %d)", (char *) FPSTR(mqttConnFailed), MQTTclient.state(), mqttFailCount+1, maxRetries); + loggerNl(logBuf, LOGLEVEL_ERROR); + mqttFailCount++; + delay(500); + } + } + return false; +} + + +// Is called if there's a new MQTT-message for us +void callback(const char *topic, const byte *payload, uint32_t length) { + char *receivedString = strndup((char*)payload, length); + char *mqttTopic = strdup(topic); + + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "MQTT-Nachricht empfangen: [Topic: %s] [Kommando: %s]", mqttTopic, receivedString); + loggerNl(logBuf, LOGLEVEL_INFO); + + // Go to sleep? + if (strcmp_P(topic, topicSleepCmnd) == 0) { + if ((strcmp(receivedString, "OFF") == 0) || (strcmp(receivedString, "0") == 0)) { + gotoSleep = true; + } + } + + // New track to play? Take RFID-ID as input + else if (strcmp_P(topic, topicTrackCmnd) == 0) { + char *_rfidId = strdup(receivedString); + xQueueSend(rfidCardQueue, &_rfidId, 0); + free(_rfidId); + } + // Loudness to change? + else if (strcmp_P(topic, topicLoudnessCmnd) == 0) { + unsigned long vol = strtoul(receivedString, NULL, 10); + volumeToQueueSender(vol); + encoder.clearCount(); + encoder.setCount(vol * 2); // Update encoder-value to keep it in-sync with MQTT-updates + } + // Modify sleep-timer? + else if (strcmp_P(topic, topicSleepTimerCmnd) == 0) { + if (playProperties.playMode == NO_PLAYLIST) { // Don't allow sleep-modications if no playlist is active + loggerNl((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_INFO); + publishMqtt((char *) FPSTR(topicSleepState), 0, false); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + return; + } + if (strcmp(receivedString, "EOP") == 0) { + playProperties.sleepAfterPlaylist = true; + loggerNl((char *) FPSTR(sleepTimerEOP), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + return; + } else if (strcmp(receivedString, "EOT") == 0) { + playProperties.sleepAfterCurrentTrack = true; + loggerNl((char *) FPSTR(sleepTimerEOT), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + return; + } else if (strcmp(receivedString, "EO5T") == 0) { + if ((playProperties.numberOfTracks - 1) >= (playProperties.currentTrackNumber + 5)) { + playProperties.playUntilTrackNumber = playProperties.currentTrackNumber + 5; + } else { + playProperties.sleepAfterPlaylist = true; + } + loggerNl((char *) FPSTR(sleepTimerEO5), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + return; + } else if (strcmp(receivedString, "0") == 0) { + if (sleepTimerStartTimestamp) { + sleepTimerStartTimestamp = 0; + loggerNl((char *) FPSTR(sleepTimerStop), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + publishMqtt((char *) FPSTR(topicSleepState), 0, false); + return; + } else { + loggerNl((char *) FPSTR(sleepTimerAlreadyStopped), LOGLEVEL_INFO); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + return; + } + } + sleepTimer = strtoul(receivedString, NULL, 10); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %u Minute(n)", (char *) FPSTR(sleepTimerSetTo), sleepTimer); + loggerNl(logBuf, LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + + sleepTimerStartTimestamp = millis(); // Activate timer + playProperties.sleepAfterPlaylist = false; + playProperties.sleepAfterCurrentTrack = false; + } + // Track-control (pause/play, stop, first, last, next, previous) + else if (strcmp_P(topic, topicTrackControlCmnd) == 0) { + uint8_t controlCommand = strtoul(receivedString, NULL, 10); + trackControlToQueueSender(controlCommand); + } + + // Check if controls should be locked + else if (strcmp_P(topic, topicLockControlsCmnd) == 0) { + if (strcmp(receivedString, "OFF") == 0) { + lockControls = false; + loggerNl((char *) FPSTR(allowButtons), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + + } else if (strcmp(receivedString, "ON") == 0) { + lockControls = true; + loggerNl((char *) FPSTR(lockButtons), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + } + } + + // Check if playmode should be adjusted + else if (strcmp_P(topic, topicRepeatModeCmnd) == 0) { + char rBuf[2]; + uint8_t repeatMode = strtoul(receivedString, NULL, 10); + Serial.printf("Repeat: %d" , repeatMode); + if (playProperties.playMode != NO_PLAYLIST) { + if (playProperties.playMode == NO_PLAYLIST) { + snprintf(rBuf, 2, "%u", getRepeatMode()); + publishMqtt((char *) FPSTR(topicRepeatModeState), rBuf, false); + loggerNl((char *) FPSTR(noPlaylistNotAllowedMqtt), LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + } else { + switch (repeatMode) { + case NO_REPEAT: + playProperties.repeatCurrentTrack = false; + playProperties.repeatPlaylist = false; + snprintf(rBuf, 2, "%u", getRepeatMode()); + publishMqtt((char *) FPSTR(topicRepeatModeState), rBuf, false); + loggerNl((char *) FPSTR(modeRepeatNone), LOGLEVEL_INFO); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + case TRACK: + playProperties.repeatCurrentTrack = true; + playProperties.repeatPlaylist = false; + snprintf(rBuf, 2, "%u", getRepeatMode()); + publishMqtt((char *) FPSTR(topicRepeatModeState), rBuf, false); + loggerNl((char *) FPSTR(modeRepeatTrack), LOGLEVEL_INFO); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + case PLAYLIST: + playProperties.repeatCurrentTrack = false; + playProperties.repeatPlaylist = true; + snprintf(rBuf, 2, "%u", getRepeatMode()); + publishMqtt((char *) FPSTR(topicRepeatModeState), rBuf, false); + loggerNl((char *) FPSTR(modeRepeatPlaylist), LOGLEVEL_INFO); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + case TRACK_N_PLAYLIST: + playProperties.repeatCurrentTrack = true; + playProperties.repeatPlaylist = true; + snprintf(rBuf, 2, "%u", getRepeatMode()); + publishMqtt((char *) FPSTR(topicRepeatModeState), rBuf, false); + loggerNl((char *) FPSTR(modeRepeatTracknPlaylist), LOGLEVEL_INFO); + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + default: + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + snprintf(rBuf, 2, "%u", getRepeatMode()); + publishMqtt((char *) FPSTR(topicRepeatModeState), rBuf, false); + break; + } + } + } + } + + // Check if LEDs should be dimmed + else if (strcmp_P(topic, topicLedBrightnessCmnd) == 0) { + ledBrightness = strtoul(receivedString, NULL, 10); + } + + // Requested something that isn't specified? + else { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(noValidTopic), topic); + loggerNl(logBuf, LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + } + + free(receivedString); + free(mqttTopic); +} +#endif + + +// Returns current repeat-mode (mix of repeat current track and current playlist) +uint8_t getRepeatMode(void) { + if (playProperties.repeatPlaylist && playProperties.repeatCurrentTrack) { + return TRACK_N_PLAYLIST; + } else if (playProperties.repeatPlaylist && !playProperties.repeatCurrentTrack) { + return PLAYLIST; + } else if (!playProperties.repeatPlaylist && playProperties.repeatCurrentTrack) { + return TRACK; + } else { + return NO_REPEAT; + } +} + + +// Checks if string starts with prefix +// Returns true if so +bool startsWith(const char *str, const char *pre) { + if (strlen(pre) < 1) { + return false; + } + + return !strncmp(str, pre, strlen(pre)); +} + + +// Checks if string ends with suffix +// Returns true if so +bool endsWith (const char *str, const char *suf) { + const char *a = str + strlen(str); + const char *b = suf + strlen(suf); + + while (a != str && b != suf) { + if (*--a != *--b) break; + } + + return b == suf && *a == *b; +} + + +// Release previously allocated memory +void freeMultiCharArray(char **arr, const uint32_t cnt) { + for (uint32_t i=0; i<=cnt; i++) { + /*snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(freePtr), *(arr+i)); + loggerNl(logBuf, LOGLEVEL_DEBUG);*/ + free(*(arr+i)); + } + *arr = NULL; +} + + +// Knuth-Fisher-Yates-algorithm to randomize playlist +void randomizePlaylist (char *str[], const uint32_t count) { + if (count < 1) { + return; + } + + uint32_t i, r; + char *swap = NULL; + uint32_t max = count-1; + + for (i=0; i 0) { + r = rand() % max; + } else { + r = 0; + } + swap = *(str+max); + *(str+max) = *(str+r); + *(str+r) = swap; + max--; + } +} + + +// Helper to sort playlist alphabetically +static int arrSortHelper(const void* a, const void* b) { + return strcmp(*(const char**)a, *(const char**)b); +} + +// Sort playlist alphabetically +void sortPlaylist(const char** arr, int n) { + qsort(arr, n, sizeof(const char*), arrSortHelper); +} + + +// Check if file-type is correct +bool fileValid(const char *_fileItem) { + const char ch = '/'; + char *subst; + subst = strrchr(_fileItem, ch); // Don't use files that start with . + + return (!startsWith(subst, (char *) "/.")) && + (endsWith(_fileItem, ".mp3") || endsWith(_fileItem, ".MP3") || + endsWith(_fileItem, ".aac") || endsWith(_fileItem, ".AAC") || + endsWith(_fileItem, ".m3u") || endsWith(_fileItem, ".M3U") || + endsWith(_fileItem, ".asx") || endsWith(_fileItem, ".ASX")); +} + + +// Adds webstream to playlist; same like returnPlaylistFromSD() but always only one entry +char ** returnPlaylistFromWebstream(const char *_webUrl) { + char *webUrl = strdup(_webUrl); + static char **url; + + if (url != NULL) { + --url; + freeMultiCharArray(url, strtoul(*url, NULL, 10)); + } + + url = (char **) malloc(sizeof(char *) * 2); + url[0] = strdup("1"); // Number of files is always 1 in url-mode + url[1] = strdup(webUrl); + + free(webUrl); + return ++url; +} + + +/* Puts SD-file(s) or directory into a playlist + First element of array always contains the number of payload-items. */ +char ** returnPlaylistFromSD(File _fileOrDirectory) { + static char **files; + char fileNameBuf[255]; + + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %u", (char *) FPSTR(freeMemory), ESP.getFreeHeap()); + loggerNl(logBuf, LOGLEVEL_DEBUG); + + if (files != NULL) { // If **ptr already exists, de-allocate its memory + loggerNl((char *) FPSTR(releaseMemoryOfOldPlaylist), LOGLEVEL_DEBUG); + --files; + freeMultiCharArray(files, strtoul(*files, NULL, 10)); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %u", (char *) FPSTR(freeMemoryAfterFree), ESP.getFreeHeap()); + loggerNl(logBuf, LOGLEVEL_DEBUG); + } + + if (!_fileOrDirectory) { + loggerNl((char *) FPSTR(dirOrFileDoesNotExist), LOGLEVEL_ERROR); + return NULL; + } + + // File-mode + if (!_fileOrDirectory.isDirectory()) { + files = (char **) malloc(sizeof(char *) * 2); // +1 because [0] is used for number of elements; [1] -> [n] is used for payload + if (files == NULL) { + loggerNl((char *) FPSTR(unableToAllocateMemForPlaylist), LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + return NULL; + } + loggerNl((char *) FPSTR(fileModeDetected), LOGLEVEL_INFO); + strncpy(fileNameBuf, (char *) _fileOrDirectory.name(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0])); + if (fileValid(fileNameBuf)) { + files = (char **) malloc(sizeof(char *) * 2); + files[1] = strdup(fileNameBuf); + } + files[0] = strdup("1"); // Number of files is always 1 in file-mode + + return ++files; + } + + // Directory-mode + uint16_t allocCount = 1; + uint16_t allocSize = 512; + char *serializedPlaylist = (char*) calloc(allocSize, sizeof(char)); + + while (true) { + File fileItem = _fileOrDirectory.openNextFile(); + if (!fileItem) { + break; + } + if (fileItem.isDirectory()) { + continue; + } else { + strncpy(fileNameBuf, (char *) fileItem.name(), sizeof(fileNameBuf) / sizeof(fileNameBuf[0])); + + // Don't support filenames that start with "." and only allow .mp3 + if (fileValid(fileNameBuf)) { + /*snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(nameOfFileFound), fileNameBuf); + loggerNl(logBuf, LOGLEVEL_INFO);*/ + if ((strlen(serializedPlaylist) + strlen(fileNameBuf) + 2) >= allocCount * allocSize) { + serializedPlaylist = (char*) realloc(serializedPlaylist, ++allocCount * allocSize); + loggerNl((char *) FPSTR(reallocCalled), LOGLEVEL_DEBUG); + if (serializedPlaylist == NULL) { + loggerNl((char *) FPSTR(unableToAllocateMemForLinearPlaylist), LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + return files; + } + } + strcat(serializedPlaylist, stringDelimiter); + strcat(serializedPlaylist, fileNameBuf); + } + } + } + + // Get number of elements out of serialized playlist + uint32_t cnt = 0; + for (uint32_t k=0; k<(strlen(serializedPlaylist)); k++) { + if (serializedPlaylist[k] == '#') { + cnt++; + } + } + + // Alloc only necessary number of playlist-pointers + files = (char **) malloc(sizeof(char *) * cnt + 1); + if (files == NULL) { + loggerNl((char *) FPSTR(unableToAllocateMemForPlaylist), LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + free(serializedPlaylist); + return NULL; + } + + // Extract elements out of serialized playlist and copy to playlist + char *token; + token = strtok(serializedPlaylist, stringDelimiter); + uint32_t pos = 1; + while (token != NULL) { + files[pos++] = strdup(token); + token = strtok(NULL, stringDelimiter); + } + + free(serializedPlaylist); + + files[0] = (char *) malloc(sizeof(char) * 5); + if (files[0] == NULL) { + loggerNl((char *) FPSTR(unableToAllocateMemForPlaylist), LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + return NULL; + } + sprintf(files[0], "%u", cnt); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %d", (char *) FPSTR(numberOfValidFiles), cnt); + loggerNl(logBuf, LOGLEVEL_NOTICE); + + return ++files; // return ptr+1 (starting at 1st payload-item) +} + + +/* Wraps putString for writing settings into NVS for RFID-cards. + Returns number of characters written. */ +size_t nvsRfidWriteWrapper (const char *_rfidCardId, const char *_track, const uint32_t _playPosition, const uint8_t _playMode, const uint16_t _trackLastPlayed, const uint16_t _numberOfTracks) { + char prefBuf[275]; + char trackBuf[255]; + snprintf(trackBuf, sizeof(trackBuf) / sizeof(trackBuf[0]), _track); + + // If it's a directory we just want to play/save basename(path) + if (_numberOfTracks > 1) { + const char s = '/'; + char *last = strrchr(_track, s); + char *first = strchr(_track, s); + unsigned long substr = last-first+1; + if (substr <= sizeof(trackBuf) / sizeof(trackBuf[0])) { + snprintf(trackBuf, substr, _track); // save substring basename(_track) + } else { + return 0; // Filename too long! + } + } + + snprintf(prefBuf, sizeof(prefBuf) / sizeof(prefBuf[0]), "%s%s%s%u%s%d%s%u", stringDelimiter, trackBuf, stringDelimiter, _playPosition, stringDelimiter, _playMode, stringDelimiter, _trackLastPlayed); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "Schreibe '%s' in NVS für RFID-Card-ID %s mit playmode %d und letzter Track %u\n", prefBuf, _rfidCardId, _playMode, _trackLastPlayed); + logger(logBuf, LOGLEVEL_INFO); + loggerNl(prefBuf, LOGLEVEL_INFO); + return prefsRfid.putString(_rfidCardId, prefBuf); +} + + +// Function to play music as task +void playAudio(void *parameter) { + static Audio audio; + static AC101 ac; + static bool currentHeadphoneState = digitalRead(HEADPHONE_PLUGGED_IN); + static bool lastHeadphoneState = currentHeadphoneState; + static uint32_t lastHeadphoneStateTimestamp = 0; + + while (!ac.begin(IIC_DATA, IIC_CLK)) { + Serial.printf("Failed!\n"); + delay(1000); + } + + audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DSIN); + audio.setVolume(initVolume); + + pinMode(GPIO_PA_EN, OUTPUT); + digitalWrite(GPIO_PA_EN, HIGH); + + uint8_t currentVolume; + static BaseType_t trackQStatus; + static uint8_t trackCommand = 0; + + for (;;) { + // GPIO is 0 if headphone is plugged in. So turn of speaker if so and vice versa. + currentHeadphoneState = digitalRead(HEADPHONE_PLUGGED_IN); + if (((lastHeadphoneState != currentHeadphoneState) && (millis() - lastHeadphoneStateTimestamp >= 1000)) || !lastHeadphoneStateTimestamp) { + if (!currentHeadphoneState) { + ac.SetVolumeHeadphone(100); + ac.SetVolumeSpeaker(0); + } else { + ac.SetVolumeHeadphone(0); + ac.SetVolumeSpeaker(50); + } + lastHeadphoneState = currentHeadphoneState; + lastHeadphoneStateTimestamp = millis(); + } + + if (xQueueReceive(volumeQueue, ¤tVolume, 0) == pdPASS ) { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %d", (char *) FPSTR(newLoudnessReceivedQueue), currentVolume); + loggerNl(logBuf, LOGLEVEL_INFO); + audio.setVolume(currentVolume); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLoudnessState), currentVolume, false); + #endif + } + + if (xQueueReceive(trackControlQueue, &trackCommand, 0) == pdPASS) { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %d", (char *) FPSTR(newCntrlReceivedQueue), trackCommand); + loggerNl(logBuf, LOGLEVEL_INFO); + } + + trackQStatus = xQueueReceive(trackQueue, &playProperties.playlist, 0); + if (trackQStatus == pdPASS || playProperties.trackFinished || trackCommand != 0) { + if (trackQStatus == pdPASS) { + if (playProperties.pausePlay) { + playProperties.pausePlay = !playProperties.pausePlay; + } + audio.stopSong(); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s mit %d Titel(n)", (char *) FPSTR(newPlaylistReceived), playProperties.numberOfTracks); + loggerNl(logBuf, LOGLEVEL_NOTICE); + + // If we're in audiobook-mode and apply a modification-card, we don't + // want to save lastPlayPosition for the mod-card but for the card that holds the playlist + strncpy(playProperties.playRfidTag, currentRfidTagId, sizeof(playProperties.playRfidTag) / sizeof(playProperties.playRfidTag[0])); + } + if (playProperties.trackFinished) { + playProperties.trackFinished = false; + if (playProperties.playMode == NO_PLAYLIST) { + playProperties.playlistFinished = true; + continue; + } + if (playProperties.saveLastPlayPosition) { // Don't save for AUDIOBOOK_LOOP because not necessary + if (playProperties.currentTrackNumber + 1 < playProperties.numberOfTracks) { + // Only save if there's another track, otherwise it will be saved at end of playlist anyway + nvsRfidWriteWrapper(playProperties.playRfidTag, *(playProperties.playlist + playProperties.currentTrackNumber), 0, playProperties.playMode, playProperties.currentTrackNumber+1, playProperties.numberOfTracks); + } + } + if (playProperties.sleepAfterCurrentTrack) { // Go to sleep if "sleep after track" was requested + gotoSleep = true; + break; + } + if (!playProperties.repeatCurrentTrack) { // If endless-loop requested, track-number will not be incremented + playProperties.currentTrackNumber++; + } else { + loggerNl((char *) FPSTR(repeatTrackDueToPlaymode), LOGLEVEL_INFO); + #ifdef NEOPIXEL_ENABLE + showRewind = true; + #endif + } + } + + if (playProperties.playlistFinished && trackCommand != 0) { + loggerNl((char *) FPSTR(noPlaymodeChangeIfIdle), LOGLEVEL_NOTICE); + trackCommand = 0; + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + continue; + } + /* Check if track-control was called + (stop, start, next track, prev. track, last track, first track...) */ + switch (trackCommand) { + case STOP: + audio.stopSong(); + trackCommand = 0; + loggerNl((char *) FPSTR(cmndStop), LOGLEVEL_INFO); + playProperties.pausePlay = true; + continue; + + case PAUSEPLAY: + audio.pauseResume(); + trackCommand = 0; + loggerNl((char *) FPSTR(cmndPause), LOGLEVEL_INFO); + if (playProperties.saveLastPlayPosition && !playProperties.pausePlay) { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "Titel wurde bei Position %u pausiert.", audio.getFilePos()); + loggerNl(logBuf, LOGLEVEL_INFO); + nvsRfidWriteWrapper(playProperties.playRfidTag, *(playProperties.playlist + playProperties.currentTrackNumber), audio.getFilePos(), playProperties.playMode, playProperties.currentTrackNumber, playProperties.numberOfTracks); + } + playProperties.pausePlay = !playProperties.pausePlay; + continue; + + case NEXTTRACK: + if (playProperties.pausePlay) { + audio.pauseResume(); + playProperties.pausePlay = !playProperties.pausePlay; + } + if (playProperties.repeatCurrentTrack) { // End loop if button was pressed + playProperties.repeatCurrentTrack = !playProperties.repeatCurrentTrack; + char rBuf[2]; + snprintf(rBuf, 2, "%u", getRepeatMode()); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicRepeatModeState), rBuf, false); + #endif + } + if (playProperties.currentTrackNumber+1 < playProperties.numberOfTracks) { + playProperties.currentTrackNumber++; + if (playProperties.saveLastPlayPosition) { + nvsRfidWriteWrapper(playProperties.playRfidTag, *(playProperties.playlist + playProperties.currentTrackNumber), 0, playProperties.playMode, playProperties.currentTrackNumber, playProperties.numberOfTracks); + loggerNl((char *) FPSTR(trackStartAudiobook), LOGLEVEL_INFO); + } + loggerNl((char *) FPSTR(cmndNextTrack), LOGLEVEL_INFO); + if (!playProperties.playlistFinished) { + audio.stopSong(); + } + } else { + loggerNl((char *) FPSTR(lastTrackAlreadyActive), LOGLEVEL_NOTICE); + trackCommand = 0; + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + continue; + } + trackCommand = 0; + break; + + case PREVIOUSTRACK: + if (playProperties.pausePlay) { + audio.pauseResume(); + playProperties.pausePlay = !playProperties.pausePlay; + } + if (playProperties.repeatCurrentTrack) { // End loop if button was pressed + playProperties.repeatCurrentTrack = !playProperties.repeatCurrentTrack; + char rBuf[2]; + snprintf(rBuf, 2, "%u", getRepeatMode()); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicRepeatModeState), rBuf, false); + #endif + } + if (playProperties.currentTrackNumber > 0) { + playProperties.currentTrackNumber--; + if (playProperties.saveLastPlayPosition) { + nvsRfidWriteWrapper(playProperties.playRfidTag, *(playProperties.playlist + playProperties.currentTrackNumber), 0, playProperties.playMode, playProperties.currentTrackNumber, playProperties.numberOfTracks); + loggerNl((char *) FPSTR(trackStartAudiobook), LOGLEVEL_INFO); + } + + loggerNl((char *) FPSTR(cmndPrevTrack), LOGLEVEL_INFO); + if (!playProperties.playlistFinished) { + audio.stopSong(); + } + } else { + if (playProperties.playMode == WEBSTREAM) { + loggerNl((char *) FPSTR(trackChangeWebstream), LOGLEVEL_INFO); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + trackCommand = 0; + continue; + } + if (playProperties.saveLastPlayPosition) { + nvsRfidWriteWrapper(playProperties.playRfidTag, *(playProperties.playlist + playProperties.currentTrackNumber), 0, playProperties.playMode, playProperties.currentTrackNumber, playProperties.numberOfTracks); + } + audio.stopSong(); + #ifdef NEOPIXEL_ENABLE + showRewind = true; + #endif + audio.connecttoSD(*(playProperties.playlist + playProperties.currentTrackNumber)); + loggerNl((char *) FPSTR(trackStart), LOGLEVEL_INFO); + trackCommand = 0; + continue; + } + trackCommand = 0; + break; + + case FIRSTTRACK: + if (playProperties.pausePlay) { + audio.pauseResume(); + playProperties.pausePlay = !playProperties.pausePlay; + } + if (playProperties.currentTrackNumber > 0) { + playProperties.currentTrackNumber = 0; + if (playProperties.saveLastPlayPosition) { + nvsRfidWriteWrapper(playProperties.playRfidTag, *(playProperties.playlist + playProperties.currentTrackNumber), 0, playProperties.playMode, playProperties.currentTrackNumber, playProperties.numberOfTracks); + loggerNl((char *) FPSTR(trackStartAudiobook), LOGLEVEL_INFO); + } + loggerNl((char *) FPSTR(cmndFirstTrack), LOGLEVEL_INFO); + if (!playProperties.playlistFinished) { + audio.stopSong(); + } + } else { + loggerNl((char *) FPSTR(firstTrackAlreadyActive), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + trackCommand = 0; + continue; + } + trackCommand = 0; + break; + + case LASTTRACK: + if (playProperties.pausePlay) { + audio.pauseResume(); + playProperties.pausePlay = !playProperties.pausePlay; + } + if (playProperties.currentTrackNumber+1 < playProperties.numberOfTracks) { + playProperties.currentTrackNumber = playProperties.numberOfTracks-1; + if (playProperties.saveLastPlayPosition) { + nvsRfidWriteWrapper(playProperties.playRfidTag, *(playProperties.playlist + playProperties.currentTrackNumber), 0, playProperties.playMode, playProperties.currentTrackNumber, playProperties.numberOfTracks); + loggerNl((char *) FPSTR(trackStartAudiobook), LOGLEVEL_INFO); + } + loggerNl((char *) FPSTR(cmndLastTrack), LOGLEVEL_INFO); + if (!playProperties.playlistFinished) { + audio.stopSong(); + } + } else { + loggerNl((char *) FPSTR(lastTrackAlreadyActive), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + trackCommand = 0; + continue; + } + trackCommand = 0; + break; + + case 0: + break; + + default: + trackCommand = 0; + loggerNl((char *) FPSTR(cmndDoesNotExist), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + continue; + } + + if (playProperties.playUntilTrackNumber == playProperties.currentTrackNumber && playProperties.playUntilTrackNumber > 0) { + if (playProperties.saveLastPlayPosition) { + nvsRfidWriteWrapper(playProperties.playRfidTag, *(playProperties.playlist + playProperties.currentTrackNumber), 0, playProperties.playMode, 0, playProperties.numberOfTracks); + } + playProperties.playlistFinished = true; + playProperties.playMode = NO_PLAYLIST; + gotoSleep = true; + continue; + } + + if (playProperties.currentTrackNumber >= playProperties.numberOfTracks) { // Check if last element of playlist is already reached + loggerNl((char *) FPSTR(endOfPlaylistReached), LOGLEVEL_NOTICE); + if (!playProperties.repeatPlaylist) { + if (playProperties.saveLastPlayPosition) { + // Set back to first track + nvsRfidWriteWrapper(playProperties.playRfidTag, *(playProperties.playlist + 0), 0, playProperties.playMode, 0, playProperties.numberOfTracks); + } + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicTrackState), "", false); + #endif + playProperties.playlistFinished = true; + playProperties.playMode = NO_PLAYLIST; + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + #endif + playProperties.currentTrackNumber = 0; + playProperties.numberOfTracks = 0; + if (playProperties.sleepAfterPlaylist) { + gotoSleep = true; + } + continue; + } else { // Check if sleep after current track/playlist was requested + if (playProperties.sleepAfterPlaylist || playProperties.sleepAfterCurrentTrack) { + playProperties.playlistFinished = true; + playProperties.playMode = NO_PLAYLIST; + gotoSleep = true; + continue; + } // Repeat playlist; set current track number back to 0 + loggerNl((char *) FPSTR(repeatPlaylistDueToPlaymode), LOGLEVEL_NOTICE); + playProperties.currentTrackNumber = 0; + if (playProperties.saveLastPlayPosition) { + nvsRfidWriteWrapper(playProperties.playRfidTag, *(playProperties.playlist + 0), 0, playProperties.playMode, playProperties.currentTrackNumber, playProperties.numberOfTracks); + } + } + } + + if (playProperties.playMode == WEBSTREAM) { // Webstream + audio.connecttohost(*(playProperties.playlist + playProperties.currentTrackNumber)); + playProperties.playlistFinished = false; + } else { + // Files from SD + if (!SD.exists(*(playProperties.playlist + playProperties.currentTrackNumber))) { // Check first if file/folder exists + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "Datei/Ordner '%s' existiert nicht", *(playProperties.playlist + playProperties.currentTrackNumber)); + loggerNl(logBuf, LOGLEVEL_ERROR); + playProperties.trackFinished = true; + continue; + } else { + audio.connecttoSD(*(playProperties.playlist + playProperties.currentTrackNumber)); + #ifdef NEOPIXEL_ENABLE + showPlaylistProgress = true; + #endif + if (playProperties.startAtFilePos > 0) { + audio.setFilePos(playProperties.startAtFilePos); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s %u", (char *) FPSTR(trackStartatPos), audio.getFilePos()); + loggerNl(logBuf, LOGLEVEL_NOTICE); + } + char buf[255]; + snprintf(buf, sizeof(buf)/sizeof(buf[0]), "(%d/%d) %s", (playProperties.currentTrackNumber+1), playProperties.numberOfTracks, (const char*) *(playProperties.playlist + playProperties.currentTrackNumber)); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicTrackState), buf, false); + #endif + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "'%s' wird abgespielt (%d von %d)", *(playProperties.playlist + playProperties.currentTrackNumber), (playProperties.currentTrackNumber+1) , playProperties.numberOfTracks); + loggerNl(logBuf, LOGLEVEL_NOTICE); + playProperties.playlistFinished = false; + } + } + } + + // Calculate relative position in file (for neopixel) for SD-card-mode + #ifdef NEOPIXEL_ENABLE + if (!playProperties.playlistFinished && playProperties.playMode != WEBSTREAM) { + double fp = (double) audio.getFilePos() / (double) audio.getFileSize(); + if (millis() % 100 == 0) { + playProperties.currentRelPos = fp * 100; + } + } else { + playProperties.currentRelPos = 0; + } + #endif + + audio.loop(); + + if (playProperties.playlistFinished || playProperties.pausePlay) { + vTaskDelay(portTICK_PERIOD_MS*10); // Waste some time if playlist is not active + } else { + lastTimeActiveTimestamp = millis(); // Refresh if playlist is active so uC will not fall asleep due to reaching inactivity-time + } + + esp_task_wdt_reset(); // Don't forget to feed the dog! + } + vTaskDelete(NULL); +} + + +// Instructs RFID-scanner to scan for new RFID-tags +void rfidScanner(void *parameter) { + //static MFRC522 mfrc522(MFRC522_CS_PIN, MFRC522_RST_PIN); + //static MFRC522 mfrc522(RFID_CS, RST_PIN); + //SPI.begin(); + pinMode(MFRC522_CS_PIN, OUTPUT); + digitalWrite(MFRC522_CS_PIN, HIGH); + mfrc522.PCD_Init(); + mfrc522.PCD_DumpVersionToSerial(); // Show details of PCD - MFRC522 Card Reader detail + delay(4); + loggerNl((char *) FPSTR(rfidScannerReady), LOGLEVEL_DEBUG); + byte cardId[cardIdSize]; + char *cardIdString; + + for (;;) { + esp_task_wdt_reset(); + vTaskDelay(10); + if ((millis() - lastRfidCheckTimestamp) >= RFID_SCAN_INTERVAL) { + lastRfidCheckTimestamp = millis(); + // Reset the loop if no new card is present on the sensor/reader. This saves the entire process when idle. + + if (!mfrc522.PICC_IsNewCardPresent()) { + continue; + } + + // Select one of the cards + if (!mfrc522.PICC_ReadCardSerial()) { + continue; + } + + //mfrc522.PICC_DumpToSerial(&(mfrc522.uid)); + mfrc522.PICC_HaltA(); + mfrc522.PCD_StopCrypto1(); + + cardIdString = (char *) malloc(cardIdSize*3 +1); + if (cardIdString == NULL) { + logger((char *) FPSTR(unableToAllocateMem), LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + continue; + } + + uint8_t n = 0; + logger((char *) FPSTR(rfidTagDetected), LOGLEVEL_NOTICE); + for (uint8_t i=0; i(leds, NUM_LEDS).setCorrection( TypicalSMD5050 ); + FastLED.setBrightness(ledBrightness); + + for (;;) { + if (!bootComplete) { // Rotates orange unless boot isn't complete + FastLED.clear(); + for (uint8_t led = 0; led < NUM_LEDS; led++) { + if (showEvenError) { + if (ledAddress(led) % 2 == 0) { + if (millis() <= 10000) { + leds[ledAddress(led)] = CRGB::Orange; + } else { + leds[ledAddress(led)] = CRGB::Red; + } + } + } else { + if (millis() >= 10000) { // Flashes red after 10s (will remain forever if SD cannot be mounted) + leds[ledAddress(led)] = CRGB::Red; + } else { + if (ledAddress(led) % 2 == 1) { + leds[ledAddress(led)] = CRGB::Orange; + } + } + } + } + FastLED.show(); + showEvenError = !showEvenError; + vTaskDelay(portTICK_RATE_MS*500); + esp_task_wdt_reset(); + continue; + } + + if (lastLedBrightness != ledBrightness) { + FastLED.setBrightness(ledBrightness); + lastLedBrightness = ledBrightness; + } + + if (!buttons[3].currentState) { + FastLED.clear(); + for (uint8_t led = 0; led < NUM_LEDS; led++) { + leds[ledAddress(led)] = CRGB::Red; + if (buttons[3].currentState) { + FastLED.clear(); + FastLED.show(); + delay(5); + deepSleepManager(); + break; + } + FastLED.show(); + vTaskDelay(intervalToLongPress / NUM_LEDS * portTICK_RATE_MS); + } + } + + if (showLedError) { // If error occured (e.g. RFID-modification not accepted) + showLedError = false; + notificationShown = true; + FastLED.clear(); + + for (uint8_t led = 0; led < NUM_LEDS; led++) { + leds[ledAddress(led)] = CRGB::Red; + } + FastLED.show(); + vTaskDelay(portTICK_RATE_MS * 200); + } + + if (showLedOk) { // If action was accepted + showLedOk = false; + notificationShown = true; + FastLED.clear(); + + for (uint8_t led = 0; led < NUM_LEDS; led++) { + leds[ledAddress(led)] = CRGB::Green; + } + FastLED.show(); + vTaskDelay(portTICK_RATE_MS * 400); + } + + if (hlastVolume != currentVolume) { // If volume has been changed + uint8_t numLedsToLight = map(currentVolume, 0, maxVolume, 0, NUM_LEDS); + hlastVolume = currentVolume; + volumeChangeShown = true; + FastLED.clear(); + + for (int led = 0; led < numLedsToLight; led++) { // (Inverse) color-gradient from green (85) back to (still) red (245) using unsigned-cast + leds[ledAddress(led)].setHue((uint8_t) (85 - ((double) 95 / NUM_LEDS) * led)); + } + FastLED.show(); + + for (uint8_t i=0; i<=50; i++) { + if (hlastVolume != currentVolume || showLedError || showLedOk || !buttons[3].currentState) { + if (hlastVolume != currentVolume) { + volumeChangeShown = false; + } + break; + } + + vTaskDelay(portTICK_RATE_MS*20); + } + } + + if (showRewind) { + showRewind = false; + for (uint8_t i=NUM_LEDS-1; i>0; i--) { + leds[ledAddress(i)] = CRGB::Black; + FastLED.show(); + if (hlastVolume != currentVolume || lastLedBrightness != ledBrightness || showLedError || showLedOk || !buttons[3].currentState) { + break; + } else { + vTaskDelay(portTICK_RATE_MS*30); + } + } + } + + if (showPlaylistProgress) { + showPlaylistProgress = false; + if (playProperties.numberOfTracks > 1 && playProperties.currentTrackNumber < playProperties.numberOfTracks) { + uint8_t numLedsToLight = map(playProperties.currentTrackNumber, 0, playProperties.numberOfTracks-1, 0, NUM_LEDS); + FastLED.clear(); + for (uint8_t i=0; i < numLedsToLight; i++) { + leds[ledAddress(i)] = CRGB::Blue; + FastLED.show(); + if (hlastVolume != currentVolume || lastLedBrightness != ledBrightness || showLedError || showLedOk || !buttons[3].currentState) { + break; + } else { + vTaskDelay(portTICK_RATE_MS*30); + } + } + + for (uint8_t i=0; i<=100; i++) { + if (hlastVolume != currentVolume || lastLedBrightness != ledBrightness || showLedError || showLedOk || !buttons[3].currentState) { + break; + } else { + vTaskDelay(portTICK_RATE_MS*15); + } + } + + for (uint8_t i=numLedsToLight; i>0; i--) { + leds[ledAddress(i)-1] = CRGB::Black; + FastLED.show(); + if (hlastVolume != currentVolume || lastLedBrightness != ledBrightness || showLedError || showLedOk || !buttons[3].currentState) { + break; + } else { + vTaskDelay(portTICK_RATE_MS*30); + } + } + } + } + + switch (playProperties.playMode) { + case NO_PLAYLIST: // If no playlist is active (idle) + if (hlastVolume == currentVolume && lastLedBrightness == ledBrightness) { + for (uint8_t i=0; i= ledSwitchInterval * 1000) || redrawProgress) { + redrawProgress = false; + lastSwitchTimestamp = millis(); + FastLED.clear(); + if (ledPosWebstream + 1 < NUM_LEDS) { + ledPosWebstream++; + } else { + ledPosWebstream = 0; + } + if (lockControls) { + leds[ledAddress(ledPosWebstream)] = CRGB::Red; + leds[(ledAddress(ledPosWebstream)+NUM_LEDS/2) % NUM_LEDS] = CRGB::Red; + } else if (!playProperties.pausePlay) { + leds[ledAddress(ledPosWebstream)].setHue(webstreamColor); + leds[(ledAddress(ledPosWebstream)+NUM_LEDS/2) % NUM_LEDS].setHue(webstreamColor++); + } else if (playProperties.pausePlay) { + leds[ledAddress(ledPosWebstream)] = CRGB::Orange; + leds[(ledAddress(ledPosWebstream)+NUM_LEDS/2) % NUM_LEDS] = CRGB::Orange; + } + } + } + FastLED.show(); + vTaskDelay(portTICK_RATE_MS * 5); + } + } + esp_task_wdt_reset(); + } + vTaskDelete(NULL); +} +#endif + + +// Sets deep-sleep-flag if max. inactivity-time is reached +void sleepHandler(void) { + unsigned long m = millis(); + if (m >= lastTimeActiveTimestamp && (m - lastTimeActiveTimestamp >= maxInactivityTime * 1000 * 60)) { + loggerNl((char *) FPSTR(goToSleepDueToIdle), LOGLEVEL_INFO); + gotoSleep = true; + } else if (sleepTimerStartTimestamp > 0) { + if (m - sleepTimerStartTimestamp >= sleepTimer * 1000 * 60) { + loggerNl((char *) FPSTR(goToSleepDueToTimer), LOGLEVEL_INFO); + gotoSleep = true; + } + } +} + + +// Puts uC to deep-sleep if flag is set +void deepSleepManager(void) { + if (gotoSleep) { + loggerNl((char *) FPSTR(goToSleepNow), LOGLEVEL_NOTICE); + Serial.flush(); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicState), "Offline", false); + publishMqtt((char *) FPSTR(topicTrackState), "---", false); + MQTTclient.disconnect(); + #endif + #ifdef NEOPIXEL_ENABLE + FastLED.clear(); + FastLED.show(); + #endif + /*SPI.end(); + spiSD.end();*/ + digitalWrite(POWER, LOW); + digitalWrite(GPIO_PA_EN, LOW); + delay(200); + esp_deep_sleep_start(); + } +} + + +// Adds new volume-entry to volume-queue +void volumeToQueueSender(const int32_t _newVolume) { + uint32_t _volume; + if (_newVolume <= minVolume) { + _volume = minVolume; + } else if (_newVolume > maxVolume) { + _volume = maxVolume; + } else { + _volume = _newVolume; + } + xQueueSend(volumeQueue, &_volume, 0); +} + + +// Adds new control-command to control-queue +void trackControlToQueueSender(const uint8_t trackCommand) { + xQueueSend(trackControlQueue, &trackCommand, 0); +} + + +// Handles volume directed by rotary encoder +void volumeHandler(const int32_t _minVolume, const int32_t _maxVolume) { + if (lockControls) { + encoder.clearCount(); + encoder.setCount(currentVolume*2); + return; + } + + currentEncoderValue = encoder.getCount(); + // Only if initial run or value has changed. And only after "full step" of rotary encoder + if (((lastEncoderValue != currentEncoderValue) || lastVolume == -1) && (currentEncoderValue % 2 == 0)) { + lastTimeActiveTimestamp = millis(); // Set inactive back if rotary encoder was used + if (_maxVolume * 2 < currentEncoderValue) { + encoder.clearCount(); + encoder.setCount(_maxVolume * 2); + loggerNl((char *) FPSTR(maxLoudnessReached), LOGLEVEL_INFO); + currentEncoderValue = encoder.getCount(); + } else if (currentEncoderValue < _minVolume) { + encoder.clearCount(); + encoder.setCount(_minVolume); + loggerNl((char *) FPSTR(minLoudnessReached), LOGLEVEL_INFO); + currentEncoderValue = encoder.getCount(); + } + lastEncoderValue = currentEncoderValue; + currentVolume = lastEncoderValue / 2; + if (currentVolume != lastVolume) { + lastVolume = currentVolume; + volumeToQueueSender(currentVolume); + } + } +} + + +// Receives de-serialized RFID-data (from NVS) and dispatches playlists for the given +// playmode to the track-queue. +void trackQueueDispatcher(const char *_itemToPlay, const uint32_t _lastPlayPos, const uint32_t _playMode, const uint16_t _trackLastPlayed) { + char *filename = (char *) malloc(sizeof(char) * 255); + strncpy(filename, _itemToPlay, 255); + playProperties.startAtFilePos = _lastPlayPos; + playProperties.currentTrackNumber = _trackLastPlayed; + char **musicFiles; + playProperties.playMode = BUSY; // Show @Neopixel, if uC is busy with creating playlist + + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLedBrightnessState), 0, false); + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + #endif + if (_playMode != WEBSTREAM) { + musicFiles = returnPlaylistFromSD(SD.open(filename)); + } else { + musicFiles = returnPlaylistFromWebstream(filename); + } + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLedBrightnessState), ledBrightness, false); + #endif + + if (musicFiles == NULL) { + loggerNl((char *) FPSTR(errorOccured), LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + playProperties.playMode = NO_PLAYLIST; + return; + } else if (!strcmp(*(musicFiles-1), "0")) { + loggerNl((char *) FPSTR(noMp3FilesInDir), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + playProperties.playMode = NO_PLAYLIST; + free (filename); + return; + } + + playProperties.playMode = _playMode; + playProperties.numberOfTracks = strtoul(*(musicFiles-1), NULL, 10); + // Set some default-values + playProperties.repeatCurrentTrack = false; + playProperties.repeatPlaylist = false; + playProperties.sleepAfterCurrentTrack = false; + playProperties.sleepAfterPlaylist = false; + playProperties.saveLastPlayPosition = false; + playProperties.playUntilTrackNumber = 0; + + switch(playProperties.playMode) { + case SINGLE_TRACK: { + loggerNl((char *) FPSTR(modeSingleTrack), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + publishMqtt((char *) FPSTR(topicRepeatModeState), NO_REPEAT, false); + #endif + xQueueSend(trackQueue, &(musicFiles), 0); + break; + } + + case SINGLE_TRACK_LOOP: { + playProperties.repeatCurrentTrack = true; + loggerNl((char *) FPSTR(modeSingleTrackLoop), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + publishMqtt((char *) FPSTR(topicRepeatModeState), TRACK, false); + #endif + xQueueSend(trackQueue, &(musicFiles), 0); + break; + } + + case AUDIOBOOK: { // Tracks need to be alph. sorted! + playProperties.saveLastPlayPosition = true; + loggerNl((char *) FPSTR(modeSingleAudiobook), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + publishMqtt((char *) FPSTR(topicRepeatModeState), NO_REPEAT, false); + #endif + sortPlaylist((const char**) musicFiles, strtoul(*(musicFiles-1), NULL, 10)); + xQueueSend(trackQueue, &(musicFiles), 0); + break; + } + + case AUDIOBOOK_LOOP: { // Tracks need to be alph. sorted! + playProperties.repeatPlaylist = true; + playProperties.saveLastPlayPosition = true; + loggerNl((char *) FPSTR(modeSingleAudiobookLoop), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + publishMqtt((char *) FPSTR(topicRepeatModeState), PLAYLIST, false); + #endif + sortPlaylist((const char**) musicFiles, strtoul(*(musicFiles-1), NULL, 10)); + xQueueSend(trackQueue, &(musicFiles), 0); + break; + } + + case ALL_TRACKS_OF_DIR_SORTED: { + snprintf(logBuf, sizeof(logBuf)/sizeof(logBuf[0]), "%s '%s' ", (char *) FPSTR(modeAllTrackAlphSorted), filename); + loggerNl(logBuf, LOGLEVEL_NOTICE); + sortPlaylist((const char**) musicFiles, strtoul(*(musicFiles-1), NULL, 10)); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + publishMqtt((char *) FPSTR(topicRepeatModeState), NO_REPEAT, false); + #endif + xQueueSend(trackQueue, &(musicFiles), 0); + break; + } + + case ALL_TRACKS_OF_DIR_RANDOM: { + loggerNl((char *) FPSTR(modeAllTrackRandom), LOGLEVEL_NOTICE); + randomizePlaylist(musicFiles, strtoul(*(musicFiles-1), NULL, 10)); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + publishMqtt((char *) FPSTR(topicRepeatModeState), NO_REPEAT, false); + #endif + xQueueSend(trackQueue, &(musicFiles), 0); + break; + } + + case ALL_TRACKS_OF_DIR_SORTED_LOOP: { + playProperties.repeatPlaylist = true; + loggerNl((char *) FPSTR(modeAllTrackAlphSortedLoop), LOGLEVEL_NOTICE); + sortPlaylist((const char**) musicFiles, strtoul(*(musicFiles-1), NULL, 10)); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + publishMqtt((char *) FPSTR(topicRepeatModeState), PLAYLIST, false); + #endif + xQueueSend(trackQueue, &(musicFiles), 0); + break; + } + + case ALL_TRACKS_OF_DIR_RANDOM_LOOP: { + playProperties.repeatPlaylist = true; + loggerNl((char *) FPSTR(modeAllTrackRandomLoop), LOGLEVEL_NOTICE); + randomizePlaylist(musicFiles, strtoul(*(musicFiles-1), NULL, 10)); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + publishMqtt((char *) FPSTR(topicRepeatModeState), PLAYLIST, false); + #endif + xQueueSend(trackQueue, &(musicFiles), 0); + break; + } + + case WEBSTREAM: { // This is always just one "track" + loggerNl((char *) FPSTR(modeWebstream), LOGLEVEL_NOTICE); + if (wifiManager() == WL_CONNECTED) { + xQueueSend(trackQueue, &(musicFiles), 0); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicPlaymodeState), playProperties.playMode, false); + publishMqtt((char *) FPSTR(topicRepeatModeState), NO_REPEAT, false); + #endif + } else { + loggerNl((char *) FPSTR(webstreamNotAvailable), LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + playProperties.playMode = NO_PLAYLIST; + } + break; + } + + default: + loggerNl((char *) FPSTR(modeDoesNotExist), LOGLEVEL_ERROR); + playProperties.playMode = NO_PLAYLIST; + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + } + free (filename); +} + + +// Modification-cards can change some settings (e.g. introducing track-looping or sleep after track/playlist). +// This function handles them. +void doRfidCardModifications(const uint32_t mod) { + switch (mod) { + case LOCK_BUTTONS_MOD: // Locks/unlocks all buttons + lockControls = !lockControls; + if (lockControls) { + loggerNl((char *) FPSTR(modificatorAllButtonsLocked), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLockControlsState), "ON", false); + #endif + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + } else { + loggerNl((char *) FPSTR(modificatorAllButtonsUnlocked), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLockControlsState), "OFF", false); + #endif + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + } + break; + + case SLEEP_TIMER_MOD_15: // Puts/undo uC to sleep after 15 minutes + if (sleepTimer == 15) { + sleepTimerStartTimestamp = 0; + #ifdef NEOPIXEL_ENABLE + ledBrightness = initialLedBrightness; + loggerNl((char *) FPSTR(modificatorSleepd), LOGLEVEL_NOTICE); + #endif + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLedBrightnessState), ledBrightness, false); + #endif + + } else { + sleepTimer = 15; + sleepTimerStartTimestamp = millis(); + #ifdef NEOPIXEL_ENABLE + ledBrightness = nightLedBrightness; + loggerNl((char *) FPSTR(ledsDimmedToNightmode), LOGLEVEL_INFO); + #endif + loggerNl((char *) FPSTR(modificatorSleepTimer15), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), sleepTimer, false); + publishMqtt((char *) FPSTR(topicLedBrightnessState), nightLedBrightness, false); + #endif + } + + playProperties.sleepAfterCurrentTrack = false; // deactivate/overwrite if already active + playProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active + playProperties.playUntilTrackNumber = 0; + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + case SLEEP_TIMER_MOD_30: // Puts/undo uC to sleep after 30 minutes + if (sleepTimer == 30) { + sleepTimerStartTimestamp = 0; + #ifdef NEOPIXEL_ENABLE + ledBrightness = initialLedBrightness; + loggerNl((char *) FPSTR(modificatorSleepd), LOGLEVEL_NOTICE); + #endif + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLedBrightnessState), ledBrightness, false); + #endif + + } else { + sleepTimer = 30; + sleepTimerStartTimestamp = millis(); + #ifdef NEOPIXEL_ENABLE + ledBrightness = nightLedBrightness; + loggerNl((char *) FPSTR(ledsDimmedToNightmode), LOGLEVEL_INFO); + #endif + loggerNl((char *) FPSTR(modificatorSleepTimer30), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), sleepTimer, false); + publishMqtt((char *) FPSTR(topicLedBrightnessState), nightLedBrightness, false); + #endif + } + + playProperties.sleepAfterCurrentTrack = false; // deactivate/overwrite if already active + playProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active + playProperties.playUntilTrackNumber = 0; + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + case SLEEP_TIMER_MOD_60: // Puts/undo uC to sleep after 60 minutes + if (sleepTimer == 60) { + sleepTimerStartTimestamp = 0; + #ifdef NEOPIXEL_ENABLE + ledBrightness = initialLedBrightness; + #endif + loggerNl((char *) FPSTR(modificatorSleepd), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLedBrightnessState), ledBrightness, false); + #endif + + } else { + sleepTimer = 60; + sleepTimerStartTimestamp = millis(); + #ifdef NEOPIXEL_ENABLE + ledBrightness = nightLedBrightness; + loggerNl((char *) FPSTR(ledsDimmedToNightmode), LOGLEVEL_INFO); + #endif + loggerNl((char *) FPSTR(modificatorSleepTimer60), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), sleepTimer, false); + publishMqtt((char *) FPSTR(topicLedBrightnessState), nightLedBrightness, false); + #endif + } + + playProperties.sleepAfterCurrentTrack = false; // deactivate/overwrite if already active + playProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active + playProperties.playUntilTrackNumber = 0; + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + case SLEEP_TIMER_MOD_120: // Puts/undo uC to sleep after 2 hrs + if (sleepTimer == 120) { + sleepTimerStartTimestamp = 0; + #ifdef NEOPIXEL_ENABLE + ledBrightness = initialLedBrightness; + #endif + loggerNl((char *) FPSTR(modificatorSleepd), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLedBrightnessState), ledBrightness, false); + #endif + + } else { + sleepTimer = 120; + sleepTimerStartTimestamp = millis(); + #ifdef NEOPIXEL_ENABLE + ledBrightness = nightLedBrightness; + loggerNl((char *) FPSTR(ledsDimmedToNightmode), LOGLEVEL_INFO); + #endif + loggerNl((char *) FPSTR(modificatorSleepTimer120), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), sleepTimer, false); + publishMqtt((char *) FPSTR(topicLedBrightnessState), nightLedBrightness, false); + #endif + } + + playProperties.sleepAfterCurrentTrack = false; // deactivate/overwrite if already active + playProperties.sleepAfterPlaylist = false; // deactivate/overwrite if already active + playProperties.playUntilTrackNumber = 0; + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + case SLEEP_AFTER_END_OF_TRACK: // Puts uC to sleep after end of current track + if (playProperties.playMode == NO_PLAYLIST) { + loggerNl((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + return; + } + if (playProperties.sleepAfterCurrentTrack) { + loggerNl((char *) FPSTR(modificatorSleepAtEOTd), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), "0", false); + #endif + #ifdef NEOPIXEL_ENABLE + ledBrightness = initialLedBrightness; + #endif + } else { + loggerNl((char *) FPSTR(modificatorSleepAtEOT), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), "EOT", false); + #endif + #ifdef NEOPIXEL_ENABLE + ledBrightness = nightLedBrightness; + loggerNl((char *) FPSTR(ledsDimmedToNightmode), LOGLEVEL_INFO); + #endif + } + playProperties.sleepAfterCurrentTrack = !playProperties.sleepAfterCurrentTrack; + playProperties.sleepAfterPlaylist = false; + sleepTimerStartTimestamp = 0; + playProperties.playUntilTrackNumber = 0; + + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLedBrightnessState), ledBrightness, false); + #endif + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + case SLEEP_AFTER_END_OF_PLAYLIST: // Puts uC to sleep after end of whole playlist (can take a while :->) + if (playProperties.playMode == NO_PLAYLIST) { + loggerNl((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + return; + } + if (playProperties.sleepAfterCurrentTrack) { + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), "0", false); + #endif + #ifdef NEOPIXEL_ENABLE + ledBrightness = initialLedBrightness; + #endif + loggerNl((char *) FPSTR(modificatorSleepAtEOPd), LOGLEVEL_NOTICE); + } else { + #ifdef NEOPIXEL_ENABLE + ledBrightness = nightLedBrightness; + loggerNl((char *) FPSTR(ledsDimmedToNightmode), LOGLEVEL_INFO); + #endif + loggerNl((char *) FPSTR(modificatorSleepAtEOP), LOGLEVEL_NOTICE); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), "EOP", false); + #endif + } + + playProperties.sleepAfterCurrentTrack = false; + playProperties.sleepAfterPlaylist = !playProperties.sleepAfterPlaylist; + sleepTimerStartTimestamp = 0; + playProperties.playUntilTrackNumber = 0; + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLedBrightnessState), ledBrightness, false); + #endif + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + case SLEEP_AFTER_5_TRACKS: + if (playProperties.playMode == NO_PLAYLIST) { + loggerNl((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + return; + } + + playProperties.sleepAfterCurrentTrack = false; + playProperties.sleepAfterPlaylist = false; + sleepTimerStartTimestamp = 0; + + if (playProperties.playUntilTrackNumber > 0) { + playProperties.playUntilTrackNumber = 0; + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), "0", false); + #endif + #ifdef NEOPIXEL_ENABLE + ledBrightness = initialLedBrightness; + #endif + loggerNl((char *) FPSTR(modificatorSleepd), LOGLEVEL_NOTICE); + } else { + if (playProperties.currentTrackNumber + 5 > playProperties.numberOfTracks) { // If currentTrack + 5 exceeds number of tracks in playlist, sleep after end of playlist + playProperties.sleepAfterPlaylist = true; + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), "EOP", false); + #endif + } else { + playProperties.playUntilTrackNumber = playProperties.currentTrackNumber + 5; + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicSleepTimerState), "EO5T", false); + #endif + } + #ifdef NEOPIXEL_ENABLE + ledBrightness = nightLedBrightness; + #endif + loggerNl((char *) FPSTR(sleepTimerEO5), LOGLEVEL_NOTICE); + } + + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLedBrightnessState), ledBrightness, false); + #endif + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + break; + + case REPEAT_PLAYLIST: + if (playProperties.playMode == NO_PLAYLIST) { + loggerNl((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + } else { + if (playProperties.repeatPlaylist) { + loggerNl((char *) FPSTR(modificatorPlaylistLoopDeactive), LOGLEVEL_NOTICE); + } else { + loggerNl((char *) FPSTR(modificatorPlaylistLoopActive), LOGLEVEL_NOTICE); + } + playProperties.repeatPlaylist = !playProperties.repeatPlaylist; + char rBuf[2]; + snprintf(rBuf, 2, "%u", getRepeatMode()); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicRepeatModeState), rBuf, false); + #endif + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + } + break; + + case REPEAT_TRACK: // Introduces looping for track-mode + if (playProperties.playMode == NO_PLAYLIST) { + loggerNl((char *) FPSTR(modificatorNotallowedWhenIdle), LOGLEVEL_NOTICE); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + } else { + if (playProperties.repeatCurrentTrack) { + loggerNl((char *) FPSTR(modificatorTrackDeactive), LOGLEVEL_NOTICE); + } else { + loggerNl((char *) FPSTR(modificatorTrackActive), LOGLEVEL_NOTICE); + } + playProperties.repeatCurrentTrack = !playProperties.repeatCurrentTrack; + char rBuf[2]; + snprintf(rBuf, 2, "%u", getRepeatMode()); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicRepeatModeState), rBuf, false); + #endif + #ifdef NEOPIXEL_ENABLE + showLedOk = true; + #endif + } + break; + + case DIMM_LEDS_NIGHTMODE: + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicLedBrightnessState), ledBrightness, false); + #endif + loggerNl((char *) FPSTR(ledsDimmedToNightmode), LOGLEVEL_INFO); + #ifdef NEOPIXEL_ENABLE + ledBrightness = nightLedBrightness; + showLedOk = true; + #endif + break; + + default: + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s %d !", (char *) FPSTR(modificatorDoesNotExist), mod); + loggerNl(logBuf, LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + } +} + + +// Tries to lookup RFID-tag-string in NVS and extracts parameter from it if found +void rfidPreferenceLookupHandler (void) { + BaseType_t rfidStatus; + char *rfidTagId; + char _file[255]; + uint32_t _lastPlayPos = 0; + uint16_t _trackLastPlayed = 0; + uint32_t _playMode = 1; + + rfidStatus = xQueueReceive(rfidCardQueue, &rfidTagId, 0); + if (rfidStatus == pdPASS) { + lastTimeActiveTimestamp = millis(); + snprintf(logBuf, sizeof(logBuf)/sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(rfidTagReceived), rfidTagId); + currentRfidTagId = strdup(rfidTagId); + sendWebsocketData(0, 10); // Push new rfidTagId to all websocket-clients + loggerNl(logBuf, LOGLEVEL_INFO); + + String s = prefsRfid.getString(rfidTagId, "-1"); // Try to lookup rfidId in NVS + if (!s.compareTo("-1")) { + loggerNl((char *) FPSTR(rfidTagUnknownInNvs), LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + return; + } + + char *token; + uint8_t i = 1; + token = strtok((char *) s.c_str(), stringDelimiter); + while (token != NULL) { // Try to extract data from string after lookup + if (i == 1) { + strncpy(_file, token, sizeof(_file) / sizeof(_file[0])); + } else if (i == 2) { + _lastPlayPos = strtoul(token, NULL, 10); + } else if (i == 3) { + _playMode = strtoul(token, NULL, 10); + }else if (i == 4) { + _trackLastPlayed = strtoul(token, NULL, 10); + } + i++; + token = strtok(NULL, stringDelimiter); + } + + if (i != 5) { + loggerNl((char *) FPSTR(errorOccuredNvs), LOGLEVEL_ERROR); + #ifdef NEOPIXEL_ENABLE + showLedError = true; + #endif + } else { + // Only pass file to queue if strtok revealed 3 items + if (_playMode >= 100) { + doRfidCardModifications(_playMode); + } else { + trackQueueDispatcher(_file, _lastPlayPos, _playMode, _trackLastPlayed); + } + } + } +} + + +// Initialize soft access-point +void accessPointStart(const char *SSID, IPAddress ip, IPAddress netmask) { + WiFi.mode(WIFI_AP); + WiFi.softAPConfig(ip, ip, netmask); + WiFi.softAP(SSID); + delay(500); + + loggerNl((char *) FPSTR(apReady), LOGLEVEL_NOTICE); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "IP-Adresse: %d.%d.%d.%d", apIP[0],apIP[1], apIP[2], apIP[3]); + loggerNl(logBuf, LOGLEVEL_NOTICE); + + wServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/html", basicWebsite); + }); + + wServer.on("/init", HTTP_POST, [] (AsyncWebServerRequest *request) { + if (request->hasParam("ssid", true) && request->hasParam("pwd", true) && request->hasParam("hostname", true)) { + Serial.println(request->getParam("ssid", true)->value()); + Serial.println(request->getParam("pwd", true)->value()); + Serial.println(request->getParam("hostname", true)->value()); + prefsSettings.putString("SSID", request->getParam("ssid", true)->value()); + prefsSettings.putString("Password", request->getParam("pwd", true)->value()); + prefsSettings.putString("Hostname", request->getParam("hostname", true)->value()); + } + request->send_P(200, "text/html", basicWebsite); + }); + + wServer.on("/restart", HTTP_GET, [] (AsyncWebServerRequest *request) { + request->send(200, "text/html", "ESP wird neu gestartet..."); + Serial.flush(); + ESP.restart(); + }); + + wServer.begin(); + loggerNl((char *) FPSTR(httpReady), LOGLEVEL_NOTICE); + accessPointStarted = true; +} + + +// Provides management for WiFi +wl_status_t wifiManager(void) { + if (wifiCheckLastTimestamp == 0) { + // Get credentials from NVS + String strSSID = prefsSettings.getString("SSID", "-1"); + if (!strSSID.compareTo("-1")) { + loggerNl((char *) FPSTR(ssidNotFoundInNvs), LOGLEVEL_ERROR); + } + String strPassword = prefsSettings.getString("Password", "-1"); + if (!strPassword.compareTo("-1")) { + loggerNl((char *) FPSTR(wifiPwdNotFoundInNvs), LOGLEVEL_ERROR); + } + const char *_ssid = strSSID.c_str(); + const char *_pwd = strPassword.c_str(); + + /* + // Get (optional) static-IP-configration from NVS + String strStaticIp = prefsSettings.getString("staticIP", "-1"); + String strStaticIpGw = prefsSettings.getString("staticIPGw", "-1"); + String strStaticIpNetmask = prefsSettings.getString("staticIPNetmask", "-1"); + if (!strStaticIp.compareTo("-1") || !strStaticIpGw.compareTo("-1") || !strStaticIpNetmask.compareTo("-1")) { + loggerNl((char *) FPSTR(wifiStaticIpConfigNotFoundInNvs), LOGLEVEL_INFO); + } else { + IPAddress staticWifiIp; + IPAddress staticWifiIpGw; + IPAddress staticWifiIpNetmask; + + if (strStaticIp.length() >= 7 && strStaticIpGw.length() >= 7 && strStaticIpNetmask.length() >= 7) { + staticWifiIp.fromString(strStaticIp.c_str()); + staticWifiIpGw.fromString(strStaticIpGw.c_str()); + staticWifiIpNetmask.fromString(strStaticIpNetmask.c_str()); + WiFi.config(staticWifiIp, staticWifiIpGw, staticWifiIpNetmask); + } else { + Serial.println("IP-config nicht gueltig!"); + } + }*/ + + // Get (optional) hostname-configration from NVS + String hostname = prefsSettings.getString("Hostname", "-1"); + if (hostname.compareTo("-1")) { + WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); + WiFi.setHostname(hostname.c_str()); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(restoredHostnameFromNvs), hostname.c_str()); + loggerNl(logBuf, LOGLEVEL_INFO); + } else { + loggerNl((char *) FPSTR(wifiHostnameNotSet), LOGLEVEL_INFO); + } + // ...and create a connection with it. If not successful, an access-point is opened + WiFi.begin(_ssid, _pwd); + + uint8_t tryCount=0; + while (WiFi.status() != WL_CONNECTED && tryCount <= 4) { + delay(500); + Serial.print(F(".")); + tryCount++; + wifiCheckLastTimestamp = millis(); + if (tryCount >= 4 && WiFi.status() == WL_CONNECT_FAILED) { + WiFi.begin(_ssid, _pwd); // ESP32-workaround (otherwise WiFi-connection sometimes fails) + } + } + + if (WiFi.status() == WL_CONNECTED) { + myIP = WiFi.localIP(); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "Aktuelle IP: %d.%d.%d.%d", myIP[0], myIP[1], myIP[2], myIP[3]); + loggerNl(logBuf, LOGLEVEL_NOTICE); + #ifdef FTP_ENABLE + ftpSrv.begin(ftpUser, ftpPassword); + #endif + } else { // Starts AP if WiFi-connect wasn't successful + accessPointStart((char *) FPSTR(accessPointNetworkSSID), apIP, apNetmask); + } + } + + return WiFi.status(); +} + + +// Used for substitution of some variables/templates of html-files. Is called by webserver's template-engine +String templateProcessor(const String& templ) { + if (templ == "FTP_USER") { + return prefsSettings.getString("ftpuser", "-1"); + } else if (templ == "FTP_PWD") { + return prefsSettings.getString("ftppassword", "-1"); + } else if (templ == "INIT_LED_BRIGHTBESS") { + return String(prefsSettings.getUChar("iLedBrightness", 0)); + } else if (templ == "NIGHT_LED_BRIGHTBESS") { + return String(prefsSettings.getUChar("nLedBrightness", 0)); + } else if (templ == "MAX_INACTIVITY") { + return String(prefsSettings.getUInt("mInactiviyT", 0)); + } else if (templ == "INIT_VOLUME") { + return String(prefsSettings.getUInt("initVolume", 0)); + } else if (templ == "MAX_VOLUME") { + return String(prefsSettings.getUInt("maxVolume", 0)); + } else if (templ == "MQTT_SERVER") { + return prefsSettings.getString("mqttServer", "-1"); + } else if (templ == "MQTT_ENABLE") { + if (enableMqtt) { + return String("checked=\"checked\""); + } else { + return String(); + } + } else if (templ == "MQTT_USER") { + return prefsSettings.getString("mqttUser", "-1"); + } else if (templ == "MQTT_PWD") { + return prefsSettings.getString("mqttPassword", "-1"); + } else if (templ == "IPv4") { + myIP = WiFi.localIP(); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%d.%d.%d.%d", myIP[0], myIP[1], myIP[2], myIP[3]); + return String(logBuf); + } else if (templ == "RFID_TAG_ID") { + return String(currentRfidTagId); + /*} else if (templ == "STATIC_IP") { + return prefsSettings.getString("staticIP", "-1"); + } else if (templ == "STATIC_IP_GW") { + return prefsSettings.getString("staticIPGw", "-1"); + } else if (templ == "STATIC_IP_NETMASK") { + return prefsSettings.getString("staticIPNetmask", "-1");*/ + } else if (templ == "HOSTNAME") { + return prefsSettings.getString("Hostname", "-1"); + } + + return String(); +} + + +// Takes inputs from webgui, parses JSON and saves values in NVS +// If operation was successful (NVS-write is verified) true is returned +bool processJsonRequest(char *_serialJson) { + StaticJsonDocument<1000> doc; + DeserializationError error = deserializeJson(doc, _serialJson); + JsonObject object = doc.as(); + + if (error) { + Serial.print(F("deserializeJson() failed: ")); + Serial.println(error.c_str()); + return false; + } + + if (doc.containsKey("general")) { + uint8_t iVol = doc["general"]["iVol"].as(); + uint8_t mVol = doc["general"]["mVol"].as(); + uint8_t iBright = doc["general"]["iBright"].as(); + uint8_t nBright = doc["general"]["nBright"].as(); + uint8_t iTime = doc["general"]["iTime"].as(); + + prefsSettings.putUInt("initVolume", iVol); + prefsSettings.putUInt("maxVolume", mVol); + prefsSettings.putUChar("iLedBrightness", iBright); + prefsSettings.putUChar("nLedBrightness", nBright); + prefsSettings.putUInt("mInactiviyT", iTime); + + // Check if settings were written successfully + if (prefsSettings.getUInt("initVolume", 0) != iVol || + prefsSettings.getUInt("maxVolume", 0) != mVol || + prefsSettings.getUChar("iLedBrightness", 0) != iBright || + prefsSettings.getUChar("nLedBrightness", 0) != nBright || + prefsSettings.getUInt("mInactiviyT", 0) != iTime) { + Serial.println("net gut!"); + return false; + } + + } else if (doc.containsKey("ftp")) { + const char *_ftpUser = doc["ftp"]["ftpUser"]; + const char *_ftpPwd = doc["ftp"]["ftpPwd"]; + + prefsSettings.putString("ftpuser", (String) _ftpUser); + prefsSettings.putString("ftppassword", (String) _ftpPwd); + + if (!(String(_ftpUser).equals(prefsSettings.getString("ftpuser", "-1")) || + String(_ftpPwd).equals(prefsSettings.getString("ftppassword", "-1")))) { + Serial.println("net gut2!"); + return false; + } + + } else if (doc.containsKey("mqtt")) { + uint8_t _mqttEnable = doc["mqtt"]["mqttEnable"].as(); + const char *_mqttServer = object["mqtt"]["mqttServer"]; + prefsSettings.putUChar("enableMQTT", _mqttEnable); + prefsSettings.putString("mqttServer", (String) _mqttServer); + const char *_mqttUser = doc["mqtt"]["mqttUser"]; + const char *_mqttPwd = doc["mqtt"]["mqttPwd"]; + + prefsSettings.putUChar("enableMQTT", _mqttEnable); + prefsSettings.putUChar("enableMQTT", _mqttEnable); + prefsSettings.putString("mqttServer", (String) _mqttServer); + prefsSettings.putString("mqttServer", (String) _mqttServer); + prefsSettings.putString("mqttUser", (String) _mqttUser); + prefsSettings.putString("mqttPassword", (String) _mqttPwd); + + if ((prefsSettings.getUChar("enableMQTT", 99) != _mqttEnable) || + (!String(_mqttServer).equals(prefsSettings.getString("mqttServer", "-1")))) { + return false; + } + + /*} else if (doc.containsKey("staticIP")) { + const char *_staticIp = object["ip"]["staticIP"]; + const char *_staticIpGW = doc["ip"]["staticIPGW"]; + const char *_staticIpNM = doc["ip"]["staticIPNM"]; + + prefsSettings.putString("staticIP", (String) _staticIp); + prefsSettings.putString("staticIPGw", (String) _staticIpGW); + prefsSettings.putString("staticIPNetmask", (String) _staticIpNM);*/ + + } else if (doc.containsKey("rfidMod")) { + const char *_rfidIdModId = object["rfidMod"]["rfidIdMod"]; + uint8_t _modId = object["rfidMod"]["modId"]; + char rfidString[12]; + snprintf(rfidString, sizeof(rfidString) / sizeof(rfidString[0]), "%s0%s0%s%u%s0", stringDelimiter, stringDelimiter, stringDelimiter, _modId, stringDelimiter); + prefsRfid.putString(_rfidIdModId, rfidString); + + String s = prefsRfid.getString(_rfidIdModId, "-1"); + if (s.compareTo(rfidString)) { + return false; + } + dumpNvsToSd("rfidTags", (char *) FPSTR(backupFile)); // Store backup-file every time when a new rfid-tag is programmed + + } else if (doc.containsKey("rfidAssign")) { + const char *_rfidIdAssinId = object["rfidAssign"]["rfidIdMusic"]; + const char *_fileOrUrl = object["rfidAssign"]["fileOrUrl"]; + uint8_t _playMode = object["rfidAssign"]["playMode"]; + char rfidString[275]; + snprintf(rfidString, sizeof(rfidString) / sizeof(rfidString[0]), "%s%s%s0%s%u%s0", stringDelimiter, _fileOrUrl, stringDelimiter, stringDelimiter, _playMode, stringDelimiter); + prefsRfid.putString(_rfidIdAssinId, rfidString); + Serial.println(_rfidIdAssinId); + Serial.println(rfidString); + + String s = prefsRfid.getString(_rfidIdAssinId, "-1"); + if (s.compareTo(rfidString)) { + return false; + } + dumpNvsToSd("rfidTags", (char *) FPSTR(backupFile)); // Store backup-file every time when a new rfid-tag is programmed + + } else if (doc.containsKey("wifiConfig")) { + const char *_ssid = object["wifiConfig"]["ssid"]; + const char *_pwd = object["wifiConfig"]["pwd"]; + const char *_hostname = object["wifiConfig"]["hostname"]; + + prefsSettings.putString("SSID", _ssid); + prefsSettings.putString("Password", _pwd); + prefsSettings.putString("Hostname", (String) _hostname); + + String sSsid = prefsSettings.getString("SSID", "-1"); + String sPwd = prefsSettings.getString("Password", "-1"); + String sHostname = prefsSettings.getString("Hostname", "-1"); + + if (sSsid.compareTo(_ssid) || sPwd.compareTo(_pwd)) { + return false; + } + } else if (doc.containsKey("ping")) { + sendWebsocketData(0, 20); + } + + return true; +} + + +// Sends JSON-answers via websocket +void sendWebsocketData(uint32_t client, uint8_t code) { + const size_t CAPACITY = JSON_OBJECT_SIZE(1) + 20; + StaticJsonDocument doc; + JsonObject object = doc.to(); + + if (code == 1) { + object["status"] = "ok"; + } else if (code == 2) { + object["status"] = "error"; + } else if (code == 10) { + object["rfidId"] = currentRfidTagId; + } else if (code == 20) { + object["pong"] = "pong"; + } + char jBuf[50]; + serializeJson(doc, jBuf, sizeof(jBuf) / sizeof(jBuf[0])); + + if (client == 0) { + ws.printfAll(jBuf); + } else { + ws.printf(client, jBuf); + } +} + + +// Processes websocket-requests +void onWebsocketEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len){ + if (type == WS_EVT_CONNECT){ + //client connected + Serial.printf("ws[%s][%u] connect\n", server->url(), client->id()); + //client->printf("Hello Client %u :)", client->id()); + client->ping(); + } else if (type == WS_EVT_DISCONNECT) { + //client disconnected + Serial.printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id()); + } else if (type == WS_EVT_ERROR) { + //error was received from the other end + Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data); + } else if (type == WS_EVT_PONG) { + //pong message was received (in response to a ping request maybe) + Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:""); + } else if (type == WS_EVT_DATA) { + //data packet + AwsFrameInfo * info = (AwsFrameInfo*)arg; + if (info->final && info->index == 0 && info->len == len) { + //the whole message is in a single frame and we got all of it's data + Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len); + uint8_t returnCode; + + if (processJsonRequest((char*)data)) { + returnCode = 1; + } else { + returnCode = 0; + } + sendWebsocketData(client->id(), 1); + + if (info->opcode == WS_TEXT) { + data[len] = 0; + Serial.printf("%s\n", (char*)data); + } else { + for (size_t i=0; i < info->len; i++){ + Serial.printf("%02x ", data[i]); + } + Serial.printf("\n"); + } + } + } +} + + +bool isNumber(const char *str) { + byte i = 0; + + while (*(str + i) != '\0') { + if (!isdigit(*(str + i++))) { + return false; + } + } + + if (i>0) { + return true; + } else { + return false; + } + +} + + +// Dumps all RFID-entries from NVS into a file on SD-card +bool dumpNvsToSd(char *_namespace, char *_destFile) { + esp_partition_iterator_t pi; // Iterator for find + const esp_partition_t* nvs; // Pointer to partition struct + esp_err_t result = ESP_OK; + const char* partname = "nvs"; + uint8_t pagenr = 0; // Page number in NVS + uint8_t i; // Index in Entry 0..125 + uint8_t bm; // Bitmap for an entry + uint32_t offset = 0; // Offset in nvs partition + uint8_t namespace_ID; // Namespace ID found + + pi = esp_partition_find ( ESP_PARTITION_TYPE_DATA, // Get partition iterator for + ESP_PARTITION_SUBTYPE_ANY, // this partition + partname ) ; + if (pi) { + nvs = esp_partition_get(pi); // Get partition struct + esp_partition_iterator_release(pi); // Release the iterator + dbgprint ( "Partition %s found, %d bytes", + partname, + nvs->size ) ; + } else { + Serial.printf("Partition %s not found!", partname) ; + return NULL; + } + namespace_ID = FindNsID (nvs, _namespace) ; // Find ID of our namespace in NVS + + File backupFile = SD.open(_destFile, FILE_WRITE); + if (!backupFile) { + return false; + } + while (offset < nvs->size) { + result = esp_partition_read (nvs, offset, // Read 1 page in nvs partition + &buf, + sizeof(nvs_page)); + if (result != ESP_OK) { + Serial.println(F("Error reading NVS!")); + return false; + } + + i = 0; + + while (i < 126) { + bm = (buf.Bitmap[i/4] >> ((i % 4) * 2 )) & 0x03; // Get bitmap for this entry + if (bm == 2) { + if ((namespace_ID == 0xFF) || // Show all if ID = 0xFF + (buf.Entry[i].Ns == namespace_ID)) { // otherwise just my namespace + if (isNumber(buf.Entry[i].Key)) { + String s = prefsRfid.getString((const char *)buf.Entry[i].Key); + backupFile.printf("%s%s%s%s\n", stringOuterDelimiter, buf.Entry[i].Key, stringOuterDelimiter, s.c_str()); + } + } + i += buf.Entry[i].Span; // Next entry + } else { + i++; + } + } + offset += sizeof(nvs_page); // Prepare to read next page in nvs + pagenr++; + } + + backupFile.close(); + return true; +} + + +// Handles uploaded backup-file and writes valid entries into NVS +void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + char ebuf[290]; + uint16_t j=0; + char *token; + uint8_t count=0; + nvs_t nvsEntry[1]; + + for (size_t i=0; i %s", (char *) FPSTR(writeEntryToNvs), nvsEntry[0].nvsKey, nvsEntry[0].nvsEntry); + loggerNl(logBuf, LOGLEVEL_NOTICE); + prefsRfid.putString(nvsEntry[0].nvsKey, nvsEntry[0].nvsEntry); + } + } + } +} + + +void setup() { + Serial.begin(115200); + srand(esp_random()); + pinMode(POWER, OUTPUT); + digitalWrite(POWER, HIGH); + pinMode(HEADPHONE_PLUGGED_IN, INPUT); + + prefsRfid.begin((char *) FPSTR(prefsRfidNamespace)); + prefsSettings.begin((char *) FPSTR(prefsSettingsNamespace)); + + SPI_MFRC.begin(SPI_SCK, SPI_MISO, SPI_MOSI); + SPI_MFRC.setFrequency(1000000); + + playProperties.playMode = NO_PLAYLIST; + playProperties.playlist = NULL; + playProperties.repeatCurrentTrack = false; + playProperties.repeatPlaylist = false; + playProperties.currentTrackNumber = 0; + playProperties.numberOfTracks = 0; + playProperties.startAtFilePos = 0; + playProperties.currentRelPos = 0; + playProperties.sleepAfterCurrentTrack = false; + playProperties.sleepAfterPlaylist = false; + playProperties.saveLastPlayPosition = false; + playProperties.pausePlay = false; + playProperties.trackFinished = NULL; + playProperties.playlistFinished = true; + + // Examples for serialized RFID-actions that are stored in NVS + // #### + // Please note: There's no need to do this manually (unless you want to) + /*prefsRfid.putString("215123125075", "#/mp3/Kinderlieder#0#6#0"); + prefsRfid.putString("169239075184", "#http://radio.koennmer.net/evosonic.mp3#0#8#0"); + prefsRfid.putString("244105171042", "#0#0#111#0"); // modification-card (repeat track) + prefsRfid.putString("228064156042", "#0#0#110#0"); // modification-card (repeat playlist) + prefsRfid.putString("212130160042", "#/mp3/Hoerspiele/Yakari/Sammlung2#0#3#0");*/ + +#ifdef NEOPIXEL_ENABLE + xTaskCreatePinnedToCore( + showLed, /* Function to implement the task */ + "LED", /* Name of the task */ + 2000, /* Stack size in words */ + NULL, /* Task input parameter */ + 1 | portPRIVILEGE_BIT, /* Priority of the task */ + &LED, /* Task handle. */ + 0 /* Core where the task should run */ + ); +#endif + + // Init uSD-SPI + pinMode(SD_CS, OUTPUT); + digitalWrite(SD_CS, HIGH); + /*SPI_MFRC.begin(SPI_SCK, SPI_MISO, SPI_MOSI); + SPI_MFRC.setFrequency(1000000);*/ + while (!SD.begin(SD_CS, SPI_MFRC)) { + loggerNl((char *) FPSTR(unableToMountSd), LOGLEVEL_ERROR); + delay(500); + } + + // Create queues + volumeQueue = xQueueCreate(1, sizeof(int)); + if (volumeQueue == NULL) { + loggerNl((char *) FPSTR(unableToCreateVolQ), LOGLEVEL_ERROR); + } + + rfidCardQueue = xQueueCreate(1, (cardIdSize + 1) * sizeof(char)); + if (rfidCardQueue == NULL) { + loggerNl((char *) FPSTR(unableToCreateRfidQ), LOGLEVEL_ERROR); + } + + trackControlQueue = xQueueCreate(1, sizeof(uint8_t)); + if (trackControlQueue == NULL) { + loggerNl((char *) FPSTR(unableToCreateMgmtQ), LOGLEVEL_ERROR); + } + + char **playlistArray; + trackQueue = xQueueCreate(1, sizeof(playlistArray)); + if (trackQueue == NULL) { + loggerNl((char *) FPSTR(unableToCreatePlayQ), LOGLEVEL_ERROR); + } + + // Get some stuff from NVS... + // Get initial LED-brightness from NVS + uint8_t nvsILedBrightness = prefsSettings.getUChar("iLedBrightness", 0); + if (nvsILedBrightness) { + initialLedBrightness = nvsILedBrightness; + ledBrightness = nvsILedBrightness; + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %d", (char *) FPSTR(initialBrightnessfromNvs), nvsILedBrightness); + loggerNl(logBuf, LOGLEVEL_INFO); + } else { + prefsSettings.putUChar("iLedBrightness", initialLedBrightness); + loggerNl((char *) FPSTR(wroteInitialBrightnessToNvs), LOGLEVEL_ERROR); + } + + // Get night LED-brightness from NVS + uint8_t nvsNLedBrightness = prefsSettings.getUChar("nLedBrightness", 0); + if (nvsNLedBrightness) { + nightLedBrightness = nvsNLedBrightness; + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %d", (char *) FPSTR(restoredInitialBrightnessForNmFromNvs), nvsNLedBrightness); + loggerNl(logBuf, LOGLEVEL_INFO); + } else { + prefsSettings.putUChar("nLedBrightness", nightLedBrightness); + loggerNl((char *) FPSTR(wroteNmBrightnessToNvs), LOGLEVEL_ERROR); + } + + // Get FTP-user from NVS + String nvsFtpUser = prefsSettings.getString("ftpuser", "-1"); + if (!nvsFtpUser.compareTo("-1")) { + prefsSettings.putString("ftpuser", (String) ftpUser); + loggerNl((char *) FPSTR(wroteFtpUserToNvs), LOGLEVEL_ERROR); + } else { + strncpy(ftpUser, nvsFtpUser.c_str(), sizeof(ftpUser)/sizeof(ftpUser[0])); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(restoredFtpUserFromNvs), nvsFtpUser.c_str()); + loggerNl(logBuf, LOGLEVEL_INFO); + } + + // Get FTP-password from NVS + String nvsFtpPassword = prefsSettings.getString("ftppassword", "-1"); + if (!nvsFtpPassword.compareTo("-1")) { + prefsSettings.putString("ftppassword", (String) ftpPassword); + loggerNl((char *) FPSTR(wroteFtpPwdToNvs), LOGLEVEL_ERROR); + } else { + strncpy(ftpPassword, nvsFtpPassword.c_str(), sizeof(ftpPassword)/sizeof(ftpPassword[0])); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(restoredFtpPwdFromNvs), nvsFtpPassword.c_str()); + loggerNl(logBuf, LOGLEVEL_INFO); + } + + // Get maximum inactivity-time from NVS + uint32_t nvsMInactivityTime = prefsSettings.getUInt("mInactiviyT", 0); + if (nvsMInactivityTime) { + maxInactivityTime = nvsMInactivityTime; + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %u", (char *) FPSTR(restoredMaxInactivityFromNvs), nvsMInactivityTime); + loggerNl(logBuf, LOGLEVEL_INFO); + } else { + prefsSettings.putUInt("mInactiviyT", maxInactivityTime); + loggerNl((char *) FPSTR(wroteMaxInactivityToNvs), LOGLEVEL_ERROR); + } + + // Get initial volume from NVS + uint32_t nvsInitialVolume = prefsSettings.getUInt("initVolume", 0); + if (nvsInitialVolume) { + initVolume = nvsInitialVolume; + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %u", (char *) FPSTR(restoredInitialLoudnessFromNvs), nvsInitialVolume); + loggerNl(logBuf, LOGLEVEL_INFO); + } else { + prefsSettings.putUInt("initVolume", initVolume); + loggerNl((char *) FPSTR(wroteInitialLoudnessToNvs), LOGLEVEL_ERROR); + } + + // Get maximum volume from NVS + uint32_t nvsMaxVolume = prefsSettings.getUInt("maxVolume", 0); + if (nvsMaxVolume) { + maxVolume = nvsMaxVolume; + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %u", (char *) FPSTR(restoredMaxLoudnessFromNvs), nvsMaxVolume); + loggerNl(logBuf, LOGLEVEL_INFO); + } else { + prefsSettings.putUInt("maxVolume", maxVolume); + loggerNl((char *) FPSTR(wroteMaxLoudnessToNvs), LOGLEVEL_ERROR); + } + + // Get MQTT-enable from NVS + uint8_t nvsEnableMqtt = prefsSettings.getUChar("enableMQTT", 99); + switch (nvsEnableMqtt) { + case 99: + prefsSettings.putUChar("enableMQTT", enableMqtt); + loggerNl((char *) FPSTR(wroteMqttFlagToNvs), LOGLEVEL_ERROR); + break; + case 1: + //prefsSettings.putUChar("enableMQTT", enableMqtt); + enableMqtt = nvsEnableMqtt; + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %u", (char *) FPSTR(restoredMqttActiveFromNvs), nvsEnableMqtt); + loggerNl(logBuf, LOGLEVEL_INFO); + break; + case 0: + enableMqtt = nvsEnableMqtt; + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %u", (char *) FPSTR(restoredMqttDeactiveFromNvs), nvsEnableMqtt); + loggerNl(logBuf, LOGLEVEL_INFO); + break; + } + + // Get MQTT-server from NVS + String nvsMqttServer = prefsSettings.getString("mqttServer", "-1"); + if (!nvsMqttServer.compareTo("-1")) { + prefsSettings.putString("mqttServer", (String) mqtt_server); + loggerNl((char*) FPSTR(wroteMqttServerToNvs), LOGLEVEL_ERROR); + } else { + strncpy(mqtt_server, nvsMqttServer.c_str(), sizeof(mqtt_server)/sizeof(mqtt_server[0])); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(restoredMqttServerFromNvs), nvsMqttServer.c_str()); + loggerNl(logBuf, LOGLEVEL_INFO); + } + + // Get MQTT-user from NVS + String nvsMqttUser = prefsSettings.getString("mqttUser", "-1"); + if (!nvsMqttUser.compareTo("-1")) { + prefsSettings.putString("mqttUser", (String) mqttUser); + loggerNl((char *) FPSTR(wroteMqttUserToNvs), LOGLEVEL_ERROR); + } else { + strncpy(mqttUser, nvsMqttUser.c_str(), sizeof(mqttUser)/sizeof(mqttUser[0])); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(restoredMqttUserFromNvs), nvsMqttUser.c_str()); + loggerNl(logBuf, LOGLEVEL_INFO); + } + + // Get MQTT-password from NVS + String nvsMqttPassword = prefsSettings.getString("mqttPassword", "-1"); + if (!nvsMqttPassword.compareTo("-1")) { + prefsSettings.putString("mqttPassword", (String) mqttPassword); + loggerNl((char *) FPSTR(wroteMqttPwdToNvs), LOGLEVEL_ERROR); + } else { + strncpy(mqttPassword, nvsMqttPassword.c_str(), sizeof(mqttPassword)/sizeof(mqttPassword[0])); + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "%s: %s", (char *) FPSTR(restoredMqttPwdFromNvs), nvsMqttPassword.c_str()); + loggerNl(logBuf, LOGLEVEL_INFO); + } + + // Create 1000Hz-HW-Timer (currently only used for buttons) + timerSemaphore = xSemaphoreCreateBinary(); + timer = timerBegin(0, 240, true); // Prescaler: CPU-clock in MHz + timerAttachInterrupt(timer, &onTimer, true); + timerAlarmWrite(timer, 1000, true); // 1000 Hz + timerAlarmEnable(timer); + + // Create tasks + xTaskCreatePinnedToCore( + rfidScanner, /* Function to implement the task */ + "rfidhandling", /* Name of the task */ + 2000, /* Stack size in words */ + NULL, /* Task input parameter */ + 1, /* Priority of the task */ + &rfid, /* Task handle. */ + 0 /* Core where the task should run */ + ); + + xTaskCreatePinnedToCore( + playAudio, /* Function to implement the task */ + "mp3play", /* Name of the task */ + 11000, /* Stack size in words */ + NULL, /* Task input parameter */ + 2 | portPRIVILEGE_BIT, /* Priority of the task */ + &mp3Play, /* Task handle. */ + 1 /* Core where the task should run */ + ); + + + esp_sleep_enable_ext0_wakeup((gpio_num_t) DREHENCODER_BUTTON, 0); + + // Activate internal pullups for all buttons + pinMode(DREHENCODER_BUTTON, INPUT_PULLUP); + /*pinMode(PAUSEPLAY_BUTTON, INPUT_PULLUP); + pinMode(NEXT_BUTTON, INPUT_PULLUP); + pinMode(PREVIOUS_BUTTON, INPUT_PULLUP);*/ + + // Init rotary encoder + encoder.attachHalfQuad(DREHENCODER_CLK, DREHENCODER_DT); + encoder.clearCount(); + encoder.setCount(initVolume*2); // Ganzes Raster ist immer +2, daher initiale Lautstärke mit 2 multiplizieren + + // Only enable MQTT if requested + #ifdef MQTT_ENABLE + if (enableMqtt) { + MQTTclient.setServer(mqtt_server, 1883); + MQTTclient.setCallback(callback); + } + #endif + + wifiManager(); + + lastTimeActiveTimestamp = millis(); // initial set after boot + + if (wifiManager() == WL_CONNECTED) { + // attach AsyncWebSocket for Mgmt-Interface + ws.onEvent(onWebsocketEvent); + wServer.addHandler(&ws); + + // attach AsyncEventSource + wServer.addHandler(&events); + + wServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send_P(200, "text/html", mgtWebsite, templateProcessor); + }); + + wServer.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/html", backupRecoveryWebsite); + }, handleUpload); + + wServer.on("/restart", HTTP_GET, [] (AsyncWebServerRequest *request) { + request->send_P(200, "text/html", restartWebsite); + Serial.flush(); + ESP.restart(); + }); + + wServer.onNotFound(notFound); + wServer.begin(); + } + bootComplete = true; + + /*char *sdC = (char *) calloc(16384, sizeof(char)); + printSdContent(SD.open("/", FILE_READ), 16384, 1, sdC, 2); + printSdContent(SD.open("/", FILE_READ), 16384, 1, sdC, 2); + Serial.println(sdC); + Serial.println(strlen(sdC)); + Serial.println(ESP.getFreeHeap()); + free (sdC); + Serial.println(ESP.getFreeHeap());*/ +} + + +void loop() { + volumeHandler(minVolume, maxVolume); + buttonHandler(); + doButtonActions(); + sleepHandler(); + deepSleepManager(); + rfidPreferenceLookupHandler(); + if (wifiManager() == WL_CONNECTED) { + #ifdef MQTT_ENABLE + if (enableMqtt) { + reconnect(); + MQTTclient.loop(); + postHeartbeatViaMqtt(); + } + #endif + #ifdef FTP_ENABLE + ftpSrv.handleFTP(); + #endif + } + #ifdef FTP_ENABLE + if (ftpSrv.isConnected()) { + lastTimeActiveTimestamp = millis(); // Re-adjust timer while client is connected to avoid ESP falling asleep + } + #endif +} + + +// Some mp3-lib-stuff (slightly changed from default) +void audio_info(const char *info) { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "info : %s", info); + loggerNl(logBuf, LOGLEVEL_INFO); +} +void audio_id3data(const char *info) { //id3 metadata + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "id3data : %s", info); + loggerNl(logBuf, LOGLEVEL_INFO); +} +void audio_eof_mp3(const char *info) { //end of file + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "eof_mp3 : %s", info); + loggerNl(logBuf, LOGLEVEL_INFO); + playProperties.trackFinished = true; +} +void audio_showstation(const char *info) { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "station : %s", info); + loggerNl(logBuf, LOGLEVEL_NOTICE); + char buf[255]; + snprintf(buf, sizeof(buf)/sizeof(buf[0]), "Webradio: %s", info); + #ifdef MQTT_ENABLE + publishMqtt((char *) FPSTR(topicTrackState), buf, false); + #endif +} +void audio_showstreaminfo(const char *info) { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "streaminfo : %s", info); + loggerNl(logBuf, LOGLEVEL_INFO); +} +void audio_showstreamtitle(const char *info) { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "streamtitle : %s", info); + loggerNl(logBuf, LOGLEVEL_INFO); +} +void audio_bitrate(const char *info) { + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "bitrate : %s", info); + loggerNl(logBuf, LOGLEVEL_INFO); +} +void audio_commercial(const char *info) { //duration in sec + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "commercial : %s", info); + loggerNl(logBuf, LOGLEVEL_INFO); +} +void audio_icyurl(const char *info) { //homepage + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "icyurl : %s", info); + loggerNl(logBuf, LOGLEVEL_INFO); +} +void audio_lasthost(const char *info) { //stream URL played + snprintf(logBuf, sizeof(logBuf) / sizeof(logBuf[0]), "lasthost : %s", info); + loggerNl(logBuf, LOGLEVEL_INFO); +} \ No newline at end of file