36 Commits

Author SHA1 Message Date
ccc596dcba New version. Improved API endpoints to validate hostname, added german translation for tell address and made it accessible to the API 2024-03-30 13:18:57 +01:00
653d3ed2b4 Added API endpoint to change the language of the tell address 2024-03-30 13:16:57 +01:00
ba484df8ed Added language translation to german for the address telling in OP modes 1,2,3 2024-03-30 13:08:12 +01:00
3b80369739 Improved telling the address: now always telling whole numbers via Google TTS (not only digits) 2024-03-30 12:53:07 +01:00
795081cb57 Improved /api/v1/system/network_name/change endpoint to check wether it's a valid label 2024-03-30 12:40:18 +01:00
ec6e87b376 Fixed wrong default operation mode 2024-03-29 13:08:41 +01:00
0804478e2c Version 0.4.1-dev: added mDNS responder for better discovery, added telling the mDNS name via TTS at startup; and API endpoints to control these two new features 2024-03-29 13:07:27 +01:00
4a65ca3612 Fixed bug causing only operation_mode 0 and 2 to be valid 2024-03-29 13:04:57 +01:00
d630622863 Implemented /api/v1/system/tell_address API endpoint and fixed a compile error caused by last commit 2024-03-29 12:56:44 +01:00
e95b8d0051 Implemented /api/v1/system/network_name and .../change API endpoints 2024-03-29 12:46:31 +01:00
5353530f06 Fixed bug that requesting / on the web API results in HTTP 404 2024-03-29 12:10:40 +01:00
9397abafe3 Implemented tellAddressInfo and added the Preferences keys for the network name and tell_address 2024-03-29 12:08:07 +01:00
d6f64ef107 Added a mDNS responder for better discovery 2024-03-29 11:07:46 +01:00
2ef668d614 Code refactoring. 2024-03-26 23:28:29 +01:00
b672bc4535 Fixed little bug causing NetSpeaker to not recognize the end of a playlist 2024-03-26 22:18:02 +01:00
ccd327e87e Mentioned the two new 'operation modes' in README.md 2024-03-26 22:17:33 +01:00
6301829e37 Added two more operation modes; these are simple said modes where the user can choose, if he wants to use it standalone or interconnected 2024-03-26 22:15:55 +01:00
7608af2b06 Removed strange unneccessary variable 2024-03-26 21:09:04 +01:00
4b8e126ad2 Added new API endpoing (get_ap_creds) and completed some other endpoints for production use 2024-03-26 19:16:13 +01:00
fbcf3fd6b9 Added CORS header on all API endpoints so that external sites can access the API 2024-03-26 13:10:31 +01:00
d1eecbe7a0 Added helper script 2024-03-26 12:22:50 +01:00
ad29e11a93 Updated State badge in README.md 2024-03-26 09:49:47 +00:00
507f400a6c Updated links to the API demo repo 2024-03-26 10:48:27 +01:00
3c677bf04f Fixed wrong log messages; did reformatting of the LICENSE texts 2024-03-26 10:44:30 +01:00
6176b656c7 New minor version: moved the API demo to another repo (see README), also optimized the code and fixed an infinite loop bug 2024-03-26 10:25:11 +01:00
bf113272ee Also removed the Bootstrap CSS credit (as not used anymore) 2024-03-26 09:13:35 +00:00
eee4d6c0f1 Added helpful links section to README.md 2024-03-26 09:12:29 +00:00
8815daab57 Added notice about the API demo removal and its new location 2024-03-26 09:08:08 +00:00
35138308ef Replaced the huge html file served at / by a simple note that the API exists. The former API demo that has been there will move to an external server running on normal linux machines. 2024-03-26 09:58:46 +01:00
9bdc57633b Refactored code; REMOVED the web ui as the project would get too big for the ESP32's flash! There will be one or another solution for this in the near future. 2024-03-26 01:55:07 +01:00
444d5a6152 Corrected little convenience issue with the last fix (see last commit) 2024-03-26 00:02:13 +01:00
0c3868d0b8 Fixed potential INFINITE LOOP issue 2024-03-26 00:00:28 +01:00
bc7efeb509 Implemented playlist api demos 2024-03-25 23:49:07 +01:00
c2216f4a67 Implemented another API demo: changing the WiFi credentials. 2024-03-25 22:49:40 +01:00
7458d093b5 Improved log messages of web server; don't output HTTP Methods anymore as there's no check whether it's really THIS method used for access... 2024-03-25 22:26:36 +01:00
a3a06a451d Improved the API demo ('Web UI') 2024-03-25 22:20:10 +01:00
7 changed files with 550 additions and 512 deletions

View File

@@ -3,7 +3,7 @@
[![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg?style=for-the-badge)](https://unlicense.org/) [![License: Unlicense](https://img.shields.io/badge/License-Unlicense-blue.svg?style=for-the-badge)](https://unlicense.org/)
[![Open Source](https://img.shields.io/badge/Open_Source-gray?style=for-the-badge&logo=opensourceinitiative&logoColor=3DA639)](https://opensource.org/) [![Open Source](https://img.shields.io/badge/Open_Source-gray?style=for-the-badge&logo=opensourceinitiative&logoColor=3DA639)](https://opensource.org/)
[![ESP32](https://img.shields.io/badge/ESP32-FCC624?style=for-the-badge&logo=esphome&logoColor=black)](https://www.espressif.com/en/products/socs/esp32) [![ESP32](https://img.shields.io/badge/ESP32-FCC624?style=for-the-badge&logo=esphome&logoColor=black)](https://www.espressif.com/en/products/socs/esp32)
[![State: Unstable](https://img.shields.io/badge/State-Unstable-red.svg?style=for-the-badge)]() [![State: Development](https://img.shields.io/badge/State-Development-orange.svg?style=for-the-badge)]()
NetSpeaker is a project that aims to make it easier to build your own sound system, just like sqeezebox, for example. NetSpeaker is a project that aims to make it easier to build your own sound system, just like sqeezebox, for example.
@@ -16,6 +16,15 @@ via the constant operation_mode. Valid choices are:
Any questions? Hopefully a look into the **Wiki** will help. Any questions? Hopefully a look into the **Wiki** will help.
## API demo
One thing I have learnt while developing NetSpeaker is that implementing a web server
which servers hundreds of lines of html is... well sort of dumb (there previously was an API demo running
directly on the NetSpeaker machine which took loads of resources when requested).
You can see all this in [that commit](https://git.privacynerd.de/NetSpeaker/NetSpeaker/commit/35138308ef90ea1fccbf1b292eeb69a7d4e8a084).
This local API demo has been replaced by another [repository](https://git.privacynerd.de/NetSpeaker/NetSpeaker-API-Demo) containing
the Demo as HTML files (you can just open these in your web browser and type in you NetSpeaker's IP address and start playing).
## Audio compatibility ## Audio compatibility
@@ -28,6 +37,7 @@ Features, already implemented, or still in progress for the v1.0.0 release!
- [x] SD card support for playing audio files - [x] SD card support for playing audio files
- [x] Two operating modes, standalone mode fully implemented - [x] Two operating modes, standalone mode fully implemented
- [x] two further operation modes to choose whether to run as standalone or interconnected at startup by pressing buttons
- [x] API in mode 0 - [x] API in mode 0
- [x] API methods: - [x] API methods:
- [X] /api/v1/playback/toggle - [X] /api/v1/playback/toggle
@@ -63,19 +73,25 @@ Features, already implemented, or still in progress for the v1.0.0 release!
- [X] /api/v1/system/restart/ - [X] /api/v1/system/restart/
- [X] /api/v1/system/name - [X] /api/v1/system/name
- [X] /api/v1/system/name/change - [X] /api/v1/system/name/change
- [X] /api/v1/system/network_name
- [X] /api/v1/system/network_name/change
- [X] /api/v1/system/restore_state/{on,off,get} - [X] /api/v1/system/restore_state/{on,off,get}
- [X] /api/v1/system/restore_playing/{on,off,get} - [X] /api/v1/system/restore_playing/{on,off,get}
- [X] /api/v1/system/tell_address/{on,off,get}
- [X] /api/v1/system/tell_address/lang/{en,de,get}
- [X] /api/v1/system/version - [X] /api/v1/system/version
- [X] /api/v1/system/wifi/change - [X] /api/v1/system/wifi/change
- [X] /api/v1/system/wifi/get_ssid - [X] /api/v1/system/wifi/get_ssid
- [X] /api/v1/system/wifi/get_ap_creds
- [ ] /api/v1/files/get - [ ] /api/v1/files/get
- [ ] /api/v1/files/upload - [ ] /api/v1/files/upload
- [X] Automatic WiFi connection - [X] Automatic WiFi connection
- [X] Access Point opened when no WiFi connection could be established - [X] Access Point opened when no WiFi connection could be established
- [X] Implement a simple web interface running on location /
- [X] Improve the volume endpoint handler (currently pretty undynamic - not anymore :) - [X] Improve the volume endpoint handler (currently pretty undynamic - not anymore :)
- [ ] Add better encoding as umlauts are not displayed correctly **sometimes** - [ ] Add better encoding as umlauts are not displayed correctly **sometimes**
Please note that all API endpoints may not end with a `/`.
## Credits & Acknowledgements ## Credits & Acknowledgements
@@ -84,7 +100,6 @@ Thanks to...
- the makers of Arduino IDE - the makers of Arduino IDE
- schreibfaul1 (github) for creating the audio library used in this project - schreibfaul1 (github) for creating the audio library used in this project
- espressif for making the best microprocessor I've ever seen - espressif for making the best microprocessor I've ever seen
- the authors of Bootstrap which is being used for the simple web UI
External librarys used: External librarys used:
@@ -92,6 +107,13 @@ External librarys used:
- Arduino ESP32-specific librarys (https://github.com/espressif/arduino-esp32/ as of 2023/12) - Arduino ESP32-specific librarys (https://github.com/espressif/arduino-esp32/ as of 2023/12)
## Helpful links
- [Seperate audio task example which may solve the issue that when running the API server no IO is accesible (no buttons, status led, ...)](https://github.com/schreibfaul1/ESP32-audioI2S/tree/master/examples/separate_audiotask)
- [Wiki of the projects Audio library](https://github.com/schreibfaul1/ESP32-audioI2S/wiki)
- [API demo showing how to use the NetSpeaker's API](https://git.privacynerd.de/NetSpeaker/NetSpeaker-API-Demo)
## Contributing ## Contributing
Pull requests are welcome. For major changes, please open an issue first Pull requests are welcome. For major changes, please open an issue first
@@ -116,4 +138,3 @@ successors. We intend this dedication to be an overt act of relinquishment in pe
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/> For more information, please refer to <http://unlicense.org/>

18
initial_config.ino Normal file
View File

@@ -0,0 +1,18 @@
#import <Preferences.h>
const char PREFERENCES_KEY_WIFI_SSID[] = "wifi_ssid"; // preferences key name for wifi ssid; DO NOT CHANGE
const char PREFERENCES_KEY_WIFI_PSK[] = "wifi_psk"; // preferences key name for wifi psk; DO NOT CHANGE
const char WIFI_SSID[] = ""; // type in the wifi's ssid here
const char WIFI_PSK[] = ""; // and the wifi's passkey
Preferences config;
void setup() {
config.begin("netspeaker", false);
config.clear();
config.putString(PREFERENCES_KEY_WIFI_SSID, WIFI_SSID);
config.putString(PREFERENCES_KEY_WIFI_PSK, WIFI_PSK);
}
void loop() {
// empty loop
}

View File

@@ -1,12 +1,18 @@
/* /*
This is free and unencumbered software released into the public domain. This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in
source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. all copyright interest in the software to the public domain. We make this dedication for the benefit of
the public at large and to the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/> For more information, please refer to <http://unlicense.org/>
*/ */
@@ -19,11 +25,15 @@ For more information, please refer to <http://unlicense.org/>
#include <WebServer.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer #include <WebServer.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer
#include <uri/UriBraces.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer #include <uri/UriBraces.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer
#include <Preferences.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/Preferences/ #include <Preferences.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/Preferences/
#include <ESPmDNS.h> // https://github.com/espressif/arduino-esp32/blob/master/libraries/ESPmDNS/
// define all constants // define all constants
const String version = "NetSpeaker v0.3.4-dev"; // version string used e.g. for access point ssid when wifi couldn't connect const String version_tag = "v0.4.2-dev"; // version tag (with -dev appendix for dev versions)
const int operation_mode = 0; // 0: interconnected (no buttons, but api and wifi); 1: standalone (no wifi; no api) const String version = "NetSpeaker " + version_tag; // version string used e.g. for access point ssid when wifi couldn't connect
const int operation_mode = 2; // 0: interconnected (no buttons, but api and wifi); 1: standalone (no wifi; no api);
// 2: choose by pressing any button at startup (not pressed: interconnected; pressed: standalone)
// 3: same as 2 but if not pressed: standalone; pressed: interconnected
const int SD_CS = 5; // BOARD SPECIFIC const int SD_CS = 5; // BOARD SPECIFIC
const int SPI_MISO = 19; // BOARD SPECIFIC const int SPI_MISO = 19; // BOARD SPECIFIC
const int SPI_MOSI = 23; // BOARD SPECIFIC const int SPI_MOSI = 23; // BOARD SPECIFIC
@@ -33,7 +43,7 @@ const int I2S_BLCK = 4; // can be cha
const int I2S_LRC = 15; // can be changed on need const int I2S_LRC = 15; // can be changed on need
const int sdCardEjectPin = 13; // pin which is used to tell the program to eject the sd card (so that the file system doesn't brake) const int sdCardEjectPin = 13; // pin which is used to tell the program to eject the sd card (so that the file system doesn't brake)
const int audioVolumePin = 25; // pin where the poti is connected to (wired as voltage divider) const int audioVolumePin = 25; // pin where the poti is connected to (wired as voltage divider)
const int pausePlaybackPin = 12; // switch (the switching is done digitally, a button has to be connected, NO switch) between play and pause const int pausePlaybackPin = 12; // switch (the switching is done digitally, a button has to be connected, NO switch) between play and pause)
const int forwardButtonPin = 14; // pin for forward button const int forwardButtonPin = 14; // pin for forward button
const int backwardButtonPin = 27; // pin for backward btn const int backwardButtonPin = 27; // pin for backward btn
const int waitOnSDCardEject = 5000; // defines how long to wait (in ms) after the button for SD card eject was pressed (on pin 'sdCardEjectPin'!) const int waitOnSDCardEject = 5000; // defines how long to wait (in ms) after the button for SD card eject was pressed (on pin 'sdCardEjectPin'!)
@@ -43,15 +53,17 @@ const int webport_api = 80; // port for t
const int maxVolume = 100; // defines the volume steps (max. 255) const int maxVolume = 100; // defines the volume steps (max. 255)
const int EEPROM_WRITE_INTERVAL = 10000; // how long to wait until the next EEPROM saving process, in ms const int EEPROM_WRITE_INTERVAL = 10000; // how long to wait until the next EEPROM saving process, in ms
const int WEB_RESTART_DELAY = 5000; // how long (in ms) to wait when a restart is requested by the api const int WEB_RESTART_DELAY = 5000; // how long (in ms) to wait when a restart is requested by the api
const int startupChooserWaitTime = 5000; // how long to wait for user to choose the mode (in operation_modes 2 and 3)
const int startupChooserBlinkDelay = 500; // how long to wait for user to choose the mode (in operation_modes 2 and 3)
const String playlistExtension = ".m3u"; // extension for playlist files const String playlistExtension = ".m3u"; // extension for playlist files
const String directoryPlaylistName = ".directory"; // name for directory playlists (not a path, just a filename without ext.!) const String directoryPlaylistName = ".directory"; // name for directory playlists (not a path, just a filename without ext.!)
const String wifiConfigPath = "/.wifi.conf"; // MAX LENGTH: 32 resp. 63 chars; path to configuration file; content: <WiFi SSID>\n<WiFi PSK> (ssid and password divided by an newline)
const String apSSID = version; // ssid of access point opened when not able to connect to wifi const String apSSID = version; // ssid of access point opened when not able to connect to wifi
const String apPSK = "aA16161Aa"; // pre-shared-key of access point opened when not able to connect to wifi const String apPSK = "aA16161Aa"; // pre-shared-key of access point opened when not able to connect to wifi
const char PREFERENCES_NAMESPACE[] = "netspeaker"; // namespace for preferences library (stores config data in the ESP32's built-in EEPROM) const char PREFERENCES_NAMESPACE[] = "netspeaker"; // namespace for preferences library (stores config data in the ESP32's built-in EEPROM)
const char PREFERENCES_KEY_WIFI_SSID[] = "wifi_ssid"; // preferences key name for wifi ssid const char PREFERENCES_KEY_WIFI_SSID[] = "wifi_ssid"; // preferences key name for wifi ssid
const char PREFERENCES_KEY_WIFI_PSK[] = "wifi_psk"; // preferences key name for wifi psk const char PREFERENCES_KEY_WIFI_PSK[] = "wifi_psk"; // preferences key name for wifi psk
const char PREFERENCES_KEY_FRIENDLY_NAME[] = "friendly_name"; // preferences key name for the NetSpeaker's friendly name const char PREFERENCES_KEY_FRIENDLY_NAME[] = "friendly_name"; // preferences key name for the NetSpeaker's friendly name
const char PREFERENCES_KEY_NETWORK_NAME[] = "network_name"; // preferences key name for the NetSpeaker's network name (<Network name>.local)
const char PREFERENCES_KEY_RESTORE_OLD_STATE[] = "restore_state"; // preferences key name for getting if the old state should be restored or not const char PREFERENCES_KEY_RESTORE_OLD_STATE[] = "restore_state"; // preferences key name for getting if the old state should be restored or not
const char PREFERENCES_KEY_RESTORE_PLAYING[] = "restore_playing"; // preferences key name for getting if the old state should be restored or not const char PREFERENCES_KEY_RESTORE_PLAYING[] = "restore_playing"; // preferences key name for getting if the old state should be restored or not
const char PREFERENCES_KEY_VOLUME[] = "volume"; // preferences key name for old volume (for state restore) const char PREFERENCES_KEY_VOLUME[] = "volume"; // preferences key name for old volume (for state restore)
@@ -63,6 +75,8 @@ const char PREFERENCES_KEY_PLAYING[] = "playing"; // preference
const char PREFERENCES_KEY_MUTED[] = "muted"; // preferences key name for info if currently muted const char PREFERENCES_KEY_MUTED[] = "muted"; // preferences key name for info if currently muted
const char PREFERENCES_KEY_PLAYLIST_PATH[] = "playlist"; // preferences key name for playlist path (for state restore) const char PREFERENCES_KEY_PLAYLIST_PATH[] = "playlist"; // preferences key name for playlist path (for state restore)
const char PREFERENCES_KEY_PLAYLIST_INDEX[] = "pl-index"; // preferences key name for current playlist index (for state restore) const char PREFERENCES_KEY_PLAYLIST_INDEX[] = "pl-index"; // preferences key name for current playlist index (for state restore)
const char PREFERENCES_KEY_TELL_ADDRESS_AT_STARTUP[] = "tell_address"; // preferences key name for turning the telling of the NetSpeaker at startup on or off
const char PREFERENCES_KEY_TELL_ADDRESS_LANG[] = "tellAddressLang"; // preferences key name to determine what the language of the address telling at startup should be (valid: en, de)
// all other variables needed // all other variables needed
Audio audio; // Audio object (for playing audio, decoding mp3, ...) Audio audio; // Audio object (for playing audio, decoding mp3, ...)
@@ -73,6 +87,10 @@ int currentWiFiMode = 0;
bool apON = false; // DON'T CHANGE IF NOT KNOWING WHAT YOU'RE DOING; is the access point opened? bool apON = false; // DON'T CHANGE IF NOT KNOWING WHAT YOU'RE DOING; is the access point opened?
Preferences configuration; Preferences configuration;
long lastTimeEEPROMwritten = 0; // used to limit the number of eeprom write cycles long lastTimeEEPROMwritten = 0; // used to limit the number of eeprom write cycles
int operationModeChosen = operation_mode; // if in operation modes 2 or 3, you can choose between "interconnected" (= 0) and "standalone" (=1). This variable is used to store the decision (made at startup!)
bool eof_speech_reached = false; // used to recognize the end of an tts speech (e.g. used for telling the address of the netspeaker)
bool wait_after_eof_speech = false; // used for the audio_eof_speech handler to determine what to do after reaching eof_speach
String default_network_name = "netspeaker"; // default network name (the part of the mDNS domain before .local (<networkName>.local will be registered))
// create all variables for playback (will be set in 'void setup()' by 'restoreLastState();' call) // create all variables for playback (will be set in 'void setup()' by 'restoreLastState();' call)
int currentVolume = 100; // variable where current volume (0...maxVolume) is stored int currentVolume = 100; // variable where current volume (0...maxVolume) is stored
@@ -85,7 +103,7 @@ bool audioPlaying = false; // play song or not?; don't play by s
String currentSongPath; // path to currently playing song String currentSongPath; // path to currently playing song
String currentPlaylist = defaultPlaylist; // path to current playlist String currentPlaylist = defaultPlaylist; // path to current playlist
int currentPlaylistPosition = -1; // the current position in the current playlist int currentPlaylistPosition = -1; // the current position in the current playlist
bool restore_old_state; // restore the old state of EEPROM?
// struct for playback info (especially when playing mp3 files with id3 tags) // struct for playback info (especially when playing mp3 files with id3 tags)
struct playbackInfo { struct playbackInfo {
@@ -98,54 +116,143 @@ struct playbackInfo {
String year; // id3 tag year String year; // id3 tag year
String genre; // id3 tag content type String genre; // id3 tag content type
String copyright; // id3 tag (special) String copyright; // id3 tag (special)
String tts_language; // text-to-speech language
}; };
struct playbackInfo pbInfo = { "", "", "", "", "", "", "", "", "" }; struct playbackInfo pbInfo = { "", "", "", "", "", "", "", "", "", "" };
// general helper when an system-halting error occurs (to show this, the LED blinks fast)
bool indicate_system_error() {
pinMode(readyPin, OUTPUT);
while(true) {
digitalWrite(readyPin, LOW);
delay(150);
digitalWrite(readyPin, HIGH);
delay(150);
}
}
// helper for operation mode 2 and 3
bool wait_for_btn_press_and_blink(int blink_pin, int blink_delay, int wait_time) { // returns true for button pressed and false for button not pressed
pinMode(sdCardEjectPin, INPUT);
pinMode(backwardButtonPin, INPUT);
pinMode(pausePlaybackPin, INPUT);
pinMode(forwardButtonPin, INPUT);
pinMode(readyPin, OUTPUT);
int wait_timer_start = millis();
int blink_timer_start;
while ((millis() - wait_timer_start) < wait_time) { // wait_time in milliseconds!
// turn led on and wait for blink_delay while checking for button presses
digitalWrite(blink_pin, HIGH); // turn the LED on (HIGH is the voltage level)
blink_timer_start = millis();
while((millis() - blink_timer_start) < blink_delay) { // wait a given time period (delay in milliseconds!)
if(analogRead(sdCardEjectPin) > 4000 || analogRead(pausePlaybackPin) > 4000 || analogRead(forwardButtonPin) > 4000 || analogRead(backwardButtonPin) > 4000) return true;
}
// turn led off and wait for blink_delay while checking for button presses
digitalWrite(blink_pin, LOW);
blink_timer_start = millis();
while((millis() - blink_timer_start) < blink_delay) {
if(analogRead(sdCardEjectPin) > 4000 || analogRead(pausePlaybackPin) > 4000 || analogRead(forwardButtonPin) > 4000 || analogRead(backwardButtonPin) > 4000) return true;
}
}
return false;
}
void setup() { void setup() {
Serial.begin(115200); // setup Serial console (over USB) with baud 115200 Serial.begin(115200); // setup Serial console (over USB) with baud 115200
configuration.begin(PREFERENCES_NAMESPACE, false); // Initialize preferences library in RW mode configuration.begin(PREFERENCES_NAMESPACE, false); // Initialize preferences library in RW mode
// restore process if configured so // restore process if configured so
restore_old_state = configuration.getBool(PREFERENCES_KEY_RESTORE_OLD_STATE, true); if (configuration.getBool(PREFERENCES_KEY_RESTORE_OLD_STATE, true)) restoreLastState(); // standard of restore = true
if (restore_old_state) restoreLastState(); // standard of restore = true
// connect to sd card reader // connect to sd card reader
Serial.println("[SETUP] Setting up SD-Card reader"); Serial.println(F("[SETUP] Setting up SD-Card reader"));
while (!setupSD(SD_CS, SPI_MISO, SPI_MOSI, SPI_SCK)) { while (!setupSD(SD_CS, SPI_MISO, SPI_MOSI, SPI_SCK)) {
Serial.printf("[SETUP] Can't connect to SD card reader! Waiting for %d milliseconds...\n", retrySDMountTreshold); Serial.printf("[SETUP] Can't connect to SD card reader! Waiting for %d milliseconds...\n", retrySDMountTreshold);
delay(retrySDMountTreshold); delay(retrySDMountTreshold);
} }
if (operation_mode == 0) { // things only need to be done if in interconnected mode setupAudio(); // setup audio library
setupWiFi();
setupWeb();
} else if (operation_mode == 1) { // things only need to be done if in standalone mode // DEPENDING ON THE OPERATION MODE, DIFFERENT OPERATIONS NEED TO BE DONE
// BEFORE STARTING. This is what the following code does.
// operation_mode 0: see below
if (operation_mode == 1) { // things only need to be done if in standalone mode
// setup all pins (not SPI and other buses!) // setup all pins (not SPI and other buses!)
pinMode(sdCardEjectPin, INPUT); pinMode(sdCardEjectPin, INPUT);
pinMode(backwardButtonPin, INPUT); pinMode(backwardButtonPin, INPUT);
pinMode(pausePlaybackPin, INPUT); pinMode(pausePlaybackPin, INPUT);
pinMode(forwardButtonPin, INPUT); pinMode(forwardButtonPin, INPUT);
pinMode(readyPin, OUTPUT); pinMode(readyPin, OUTPUT);
} else if (operation_mode == 2) { // if enabled choosing (mode 2: no press = interconnected, press = standalone)
bool runStandalone = wait_for_btn_press_and_blink(readyPin, startupChooserBlinkDelay, startupChooserWaitTime);
if (runStandalone) {
Serial.println(F("[SETUP] User pressed button; Running in 'Standalone' mode now!"));
operationModeChosen = 1;
digitalWrite(readyPin, HIGH);
} else { } else {
Serial.println("[FATAL] PLEASE CHOOSE A OPERATION MODE! VALID OPTIONS: 0; 1. SLEEPING FOREVER."); Serial.println(F("[SETUP] User didn't press any button; Running in 'Interconnected' mode now!"));
while (true) delay(100); operationModeChosen = 0;
digitalWrite(readyPin, HIGH);
} }
setupAudio(); // setup audio library } else if (operation_mode == 3) { // if enabled choosing (mode 3: no press = standalone, press = interconnected)
bool runInterconnected = wait_for_btn_press_and_blink(readyPin, startupChooserBlinkDelay, startupChooserWaitTime);
if (runInterconnected) {
Serial.println(F("[SETUP] User pressed button; Running in 'Interconnected' mode now!"));
operationModeChosen = 0;
digitalWrite(readyPin, HIGH);
} else {
Serial.println(F("[SETUP] User didn't press any button; Running in 'Standalone' mode now!"));
operationModeChosen = 1;
digitalWrite(readyPin, HIGH);
}
}
// this function is placed here because it could be that the operation mode is chosen to be interconnected - then this check needs to run afterwards!
if (operation_mode == 0 || operationModeChosen == 0) { // things only need to be done if in - or choosen to - interconnected mode
setupWiFi();
// set up the mDNS responder
setupMDNS();
// set up the API web server
setupWeb();
// now tell the address (can be turned off using the web API, state stored in the EEPROM)
tellAddressInfo(); // helper which uses google TTS to tell the address
}
if (operation_mode != 0 && operation_mode != 1 && operation_mode != 2 && operation_mode != 3) {
Serial.println(F("[FATAL] PLEASE CHOOSE A OPERATION MODE! VALID OPTIONS: 0; 1; 2; 3. SLEEPING FOREVER."));
indicate_system_error(); // halt system and blink readyPin
}
// AND start the current track (if restored from EEPROM, only nextAudio is doing something important)
// TODO: move createPlaylistFromDirectory to some place where it fits better and is not called uselessly
createPlaylistFromDirectory(defaultPlaylistFolder); // create playlist from default dir createPlaylistFromDirectory(defaultPlaylistFolder); // create playlist from default dir
nextAudio(); // play first element of the playlist nextAudio(); // play first element of the playlist
digitalWrite(readyPin, HIGH); // show that startup is done and everything works fine digitalWrite(readyPin, HIGH); // show that startup is done and everything works fine
} }
void loop() { void loop() {
if (operation_mode == 0) { // things only need to be done if in interconnected mode if (operation_mode == 0 || operationModeChosen == 0) { // things only need to be done if in interconnected mode
setupWiFi(); // if connection was lost setupWiFi(); // if connection was lost
if (audioPlaying) audio.loop(); // play audio if not paused if (audioPlaying) audio.loop(); // play audio if not paused
if (esp_timer_get_time() % 60 == 0) { // REALLY NEEDED; audio playing won't work else! if (esp_timer_get_time() % 60 == 0) { // REALLY NEEDED; audio playing won't work else!
api_server.handleClient(); // listen on http ports all 60 ms api_server.handleClient(); // listen on http ports all 60 ms
} }
} else if (operation_mode == 1) { // things only need to be done if in standalone mode }
if (operation_mode == 1 || operationModeChosen == 1) { // things only need to be done if in standalone mode
if (audioPlaying) audio.loop(); // play audio if not paused if (audioPlaying) audio.loop(); // play audio if not paused
loopBtnListeners(); loopBtnListeners();

View File

@@ -1,12 +1,18 @@
/* /*
This is free and unencumbered software released into the public domain. This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in
source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. all copyright interest in the software to the public domain. We make this dedication for the benefit of
the public at large and to the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/> For more information, please refer to <http://unlicense.org/>
*/ */
@@ -21,13 +27,60 @@ void setupAudio() {
Serial.printf("[SETUP] Set up audio card successfully (Pins: BLCK %d | LRC %d | DOUT %d)\n", I2S_BLCK, I2S_LRC, I2S_DOUT); Serial.printf("[SETUP] Set up audio card successfully (Pins: BLCK %d | LRC %d | DOUT %d)\n", I2S_BLCK, I2S_LRC, I2S_DOUT);
} }
void tellAddressInfo() {
// TODO: make this Preference accessible via the Web API
if(configuration.getBool(PREFERENCES_KEY_TELL_ADDRESS_AT_STARTUP, true) && apON == false) { // only tell the name if the key is set (do it by default) and connected to wifi (for internet)
String tellAddressLang = configuration.getString(PREFERENCES_KEY_TELL_ADDRESS_LANG, "en");
String tell_address_string;
IPAddress ip = WiFi.localIP();
if(tellAddressLang == "en") {
tell_address_string = "Connected. Head over to ";
tell_address_string += configuration.getString(PREFERENCES_KEY_NETWORK_NAME, default_network_name);
tell_address_string += " dot local.";
tell_address_string += " Or "; // this is needed for a little gap between the hostname and ip telling
tell_address_string += String(ip[0]) + " dot ";
tell_address_string += String(ip[1]) + " dot ";
tell_address_string += String(ip[2]) + " dot ";
tell_address_string += String(ip[3]);
} else if (tellAddressLang == "de") {
tell_address_string = "Verbunden. Gehe zu ";
tell_address_string += configuration.getString(PREFERENCES_KEY_NETWORK_NAME, default_network_name);
tell_address_string += " punkt local.";
tell_address_string += " Oder "; // this is needed for a little gap between the hostname and ip telling
tell_address_string += String(ip[0]) + " punkt ";
tell_address_string += String(ip[1]) + " punkt ";
tell_address_string += String(ip[2]) + " punkt ";
tell_address_string += String(ip[3]);
} else {
Serial.println(F("[FATAL] PLEASE CHOOSE A VALID TELL_STRING LANGUAGE (valid options: en; de). SLEEPING FOREVER."));
indicate_system_error(); // halt system and blink readyPin
}
Serial.printf("[SETUP] Telling address now. String: %s\n", tell_address_string);
audio.connecttospeech(tell_address_string.c_str(), tellAddressLang.c_str());
wait_after_eof_speech = true; // to prevent the eof_speech handler of the Audio.h library (defined below) to play next audio (as there's none at startup!)
while(!eof_speech_reached) { audio.loop(); } // wait till end of speech
wait_after_eof_speech = false; // turn calling nextAudio on again (the default behaviour)
eof_speech_reached = false; // reset the variable
Serial.println("[SETUP] Told the address.");
} else {
Serial.println("[SETUP] Did not tell my address because either not connected to WiFi or it's not configured so.");
}
}
String nextAudio() { String nextAudio() {
String url; String url;
currentPlaylistPosition++; // increment once currentPlaylistPosition++; // increment once
url = getURLFromPlaylist(currentPlaylist, currentPlaylistPosition); url = getURLFromPlaylist(currentPlaylist, currentPlaylistPosition);
if (url == "") { // if the end of the playlist is reached go to start if (url == "") { // if the end of the playlist is reached go to start (typically)
if(getURLFromPlaylist(currentPlaylist, 0) == "") { // untypically: prevent infinite loop if playlist doesn't exist
Serial.println(F("[ERROR] It seems like the current playlist does not exist! Defaulting to the default playlist!"));
currentPlaylist = defaultPlaylist;
currentPlaylistPosition = 0;
return "";
}
currentPlaylistPosition = -1; // the next time "nextAudio()" is called it will increment to 0 currentPlaylistPosition = -1; // the next time "nextAudio()" is called it will increment to 0
nextAudio(); // recursive call nextAudio(); // recursive call
return ""; // exit return ""; // exit
@@ -45,6 +98,7 @@ String nextAudio() {
pbInfo.year = ""; pbInfo.year = "";
pbInfo.genre = ""; pbInfo.genre = "";
pbInfo.copyright = ""; pbInfo.copyright = "";
pbInfo.tts_language = "";
pbInfo.resourcePath = url; pbInfo.resourcePath = url;
pbInfo.type = ""; pbInfo.type = "";
@@ -59,9 +113,13 @@ String nextAudio() {
audio.connecttoFS(SD, url.c_str()); // play the next song audio.connecttoFS(SD, url.c_str()); // play the next song
pbInfo.type = "local"; pbInfo.type = "local";
} else if (url.startsWith("speech://")) { // format: speech://CC@TEXT where CC is the country code, e.g. en or de } else if (url.startsWith("speech://")) { // format: speech://CC@TEXT where CC is the country code, e.g. en or de
Serial.printf("[INFO] Starting speech from playlist (ID: %d, Language: %s, Text: %s)\n", currentPlaylistPosition, url.substring(9, 11).c_str(), url.substring(12).c_str()); String language = url.substring(9, 11).c_str();
String speech_path = url.substring(12).c_str();
Serial.printf("[INFO] Starting speech from playlist (ID: %d, Language: %s, Text: %s)\n", currentPlaylistPosition, language, speech_path);
audio.connecttospeech(url.substring(12).c_str(), url.substring(9, 11).c_str()); audio.connecttospeech(url.substring(12).c_str(), url.substring(9, 11).c_str());
pbInfo.type = "google-tts"; pbInfo.type = "google-tts";
pbInfo.track = speech_path;
pbInfo.tts_language = language;
} }
return url; return url;
@@ -86,6 +144,7 @@ String previousAudio() {
pbInfo.year = ""; pbInfo.year = "";
pbInfo.genre = ""; pbInfo.genre = "";
pbInfo.copyright = ""; pbInfo.copyright = "";
pbInfo.tts_language = "";
pbInfo.resourcePath = url; pbInfo.resourcePath = url;
pbInfo.type = ""; pbInfo.type = "";
@@ -105,6 +164,7 @@ String previousAudio() {
audio.connecttospeech(url.substring(12).c_str(), url.substring(9, 11).c_str()); audio.connecttospeech(url.substring(12).c_str(), url.substring(9, 11).c_str());
pbInfo.type = "google-tts"; pbInfo.type = "google-tts";
pbInfo.track = speech_path; pbInfo.track = speech_path;
pbInfo.tts_language = language;
} }
return url; return url;
@@ -159,12 +219,17 @@ void audio_eof_mp3(const char *info) { // MUST HAVE THIS NAME (audio_eof_mp3) b
} }
void audio_eof_speech(const char *info) { void audio_eof_speech(const char *info) {
Serial.printf("[Audio.h] EOF_Speech %s\n", info); Serial.printf("[Audio.h] EOF_Speech %s\n", info);
if (wait_after_eof_speech) {
eof_speech_reached = true;
} else {
nextAudio(); nextAudio();
}
} }
void audio_info(const char *info) { void audio_info(const char *info) {
Serial.printf("[Audio.h] Info %s\n", info); Serial.printf("[Audio.h] Info %s\n", info);
if (String(info).startsWith("End of webstream")) { if (String(info).startsWith(F("End of webstream"))) {
nextAudio(); nextAudio();
} }
} }
@@ -175,17 +240,17 @@ void audio_id3data(const char *info) { // id3 metadata
String info_str = String(info); String info_str = String(info);
if (info_str.startsWith("Album: ")) { if (info_str.startsWith("Album: ")) {
pbInfo.album = info_str.substring(7); pbInfo.album = info_str.substring(7);
} else if (info_str.startsWith("Title: ")) { } else if (info_str.startsWith(F("Title: "))) {
pbInfo.title = info_str.substring(7); pbInfo.title = info_str.substring(7);
} else if (info_str.startsWith("Track: ")) { } else if (info_str.startsWith(F("Track: "))) {
pbInfo.track = info_str.substring(7); pbInfo.track = info_str.substring(7);
} else if (info_str.startsWith("Artist: ")) { } else if (info_str.startsWith(F("Artist: "))) {
pbInfo.artist = info_str.substring(8); pbInfo.artist = info_str.substring(8);
} else if (info_str.startsWith("ContentType: ")) { } else if (info_str.startsWith(F("ContentType: "))) {
pbInfo.genre = info_str.substring(13); pbInfo.genre = info_str.substring(13);
} else if (info_str.startsWith("Year: ")) { } else if (info_str.startsWith(F("Year: "))) {
pbInfo.year = info_str.substring(6); pbInfo.year = info_str.substring(6);
} else if (info_str.startsWith("Copyright: ")) { } else if (info_str.startsWith(F("Copyright: "))) {
pbInfo.copyright = info_str.substring(11); pbInfo.copyright = info_str.substring(11);
} }
} }

View File

@@ -1,12 +1,18 @@
/* /*
This is free and unencumbered software released into the public domain. This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in
source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. all copyright interest in the software to the public domain. We make this dedication for the benefit of
the public at large and to the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/> For more information, please refer to <http://unlicense.org/>
*/ */
@@ -22,7 +28,7 @@ int setupSD(int SD_CS, int SPI_MISO, int SPI_MOSI, int SPI_SCK) {
// write preferences via the Preferences.h library // write preferences via the Preferences.h library
void writeLastState() { void writeLastState() {
if (restore_old_state) { // only save if restoration is wanted (to avoid unnecessary write cycles on eeprom) if (configuration.getBool(PREFERENCES_KEY_RESTORE_OLD_STATE, true)) { // only save if restoration is wanted (to avoid unnecessary write cycles on eeprom)
// always check if the value is different on the EEPROM so that it has to be updated // always check if the value is different on the EEPROM so that it has to be updated
Serial.printf("[RESTORE] Saving last state to EEPROM.\n"); Serial.printf("[RESTORE] Saving last state to EEPROM.\n");
if (configuration.getInt(PREFERENCES_KEY_VOLUME, maxVolume) != currentVolume) configuration.putInt(PREFERENCES_KEY_VOLUME, currentVolume); if (configuration.getInt(PREFERENCES_KEY_VOLUME, maxVolume) != currentVolume) configuration.putInt(PREFERENCES_KEY_VOLUME, currentVolume);
@@ -60,7 +66,8 @@ void restoreLastState() {
} }
currentPlaylistPosition = configuration.getInt(PREFERENCES_KEY_PLAYLIST_INDEX, 0) - 1; // -1 as default playlist position (is incremented with the first nextAudio() call) currentPlaylistPosition = configuration.getInt(PREFERENCES_KEY_PLAYLIST_INDEX, 0) - 1; // -1 as default playlist position (is incremented with the first nextAudio() call)
Serial.printf("[RESTORE] PREFERENCES_KEY_PLAYLIST_INDEX = %i\n", currentPlaylistPosition); Serial.printf("[RESTORE] PREFERENCES_KEY_PLAYLIST_INDEX = %i\n", currentPlaylistPosition);
Serial.println("[RESTORE] Restored.");
Serial.println(F("[RESTORE] Restored."));
} }
bool createPlaylistFromDirectory(String folderpath) { // create a .m3u playlist from all directory contents (the directory 'folderpath' is used) bool createPlaylistFromDirectory(String folderpath) { // create a .m3u playlist from all directory contents (the directory 'folderpath' is used)
@@ -68,7 +75,7 @@ bool createPlaylistFromDirectory(String folderpath) { // create a .m3u playlist
File playlist; File playlist;
if (folderpath[folderpath.length() - 1] == '/' && folderpath.length() > 1) { // check if file is ending with a "/" - because it wont work otherwise if (folderpath[folderpath.length() - 1] == '/' && folderpath.length() > 1) { // check if file is ending with a "/" - because it wont work otherwise
Serial.printf("[ERROR] Can't use a folder path with trailing /\n"); Serial.println(F("[ERROR] Can't use a folder path with trailing /"));
return false; return false;
} }

View File

@@ -1,17 +1,23 @@
/* /*
This is free and unencumbered software released into the public domain. This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in
source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. all copyright interest in the software to the public domain. We make this dedication for the benefit of
the public at large and to the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/> For more information, please refer to <http://unlicense.org/>
*/ */
String generate_api_json(bool success, String content, bool plain) { String send_api_json(bool success, String content, bool plain) {
String json = "{\"success\": "; // success String json = "{\"success\": "; // success
json += success ? "true" : "false"; json += success ? "true" : "false";
json += ", "; json += ", ";
@@ -24,13 +30,15 @@ String generate_api_json(bool success, String content, bool plain) {
} }
json += "}"; json += "}";
api_server.sendHeader("Access-Control-Allow-Origin", "*");
api_server.send(200, "application/json", json);
return json; return json;
} }
String generate_api_json(bool success) { // if you just want the basic json frame with the basic info String send_api_json(bool success) { // if you just want the basic json frame with the basic info
return generate_api_json(success, "", true); return send_api_json(success, "", true);
} }
String generate_api_json(bool success, String content) { // when info is to be embedded String send_api_json(bool success, String content) { // when info is to be embedded
return generate_api_json(success, content, false); return send_api_json(success, content, false);
} }
bool isStringConvertable2Int(String toIntStr) { bool isStringConvertable2Int(String toIntStr) {
for (int i = 0; i < toIntStr.length(); i++) { // check if a non-digit character is in the given string for (int i = 0; i < toIntStr.length(); i++) { // check if a non-digit character is in the given string
@@ -38,399 +46,83 @@ bool isStringConvertable2Int(String toIntStr) {
} }
return true; return true;
} }
bool isValidNetworkName(String to_validate) {
// if too long
if (to_validate.length() > 63) return false; // see here under labels: https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4
// check if it only contains a-z, A-Z, 0-9 and -
char currentChar;
void api_root() { for (int i = 0; i < to_validate.length(); i++) {
Serial.println("[HTTP] [Config] 200 - GET '/'"); currentChar = (char)to_validate[i];
if (!isAlphaNumeric(currentChar) && currentChar != '-') {
String html = "<!doctype html>"; return false; // if not, return false
html += "<html><head>"; }
html += "<meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"; }
html += "<title>Web UI | " + version + "</title>"; // then return true
html += "<link href='https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css' rel='stylesheet' integrity='sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN' crossorigin='anonymous'>"; return true;
html += "<style>#playbackMuteButton:hover { background-color: #00000000; }</style>";
html += "</head><body data-bs-theme='dark'>";
html += "<div class='text-center container-sm' style='padding: 4em 0em'>";
html += "<p>Welcome to</p>";
html += "<h1>" + configuration.getString(PREFERENCES_KEY_FRIENDLY_NAME, "NetSpeaker") + "</h1>";
html += "<p style='font-size: .875em; color: var(--bs-code-color); padding-top: .5rem'>" + version + "</p>";
html += "<hr style='margin: 3em 0em;'>";
// informations accordion
html += "<div class='accordion text-start' id='accordion-info'>";
html += " <div class='accordion-item'>";
html += " <h2 class='accordion-header'>";
html += " <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-info-playback' aria-expanded='true' aria-controls='accordion-info-playback'>";
html += " Playback information";
html += " </button>";
html += " </h2>";
html += " <div id='accordion-info-playback' class='accordion-collapse collapse' data-bs-parent='#accordion-info'>";
html += " <div class='accordion-body'>";
html += " <table class='table table-striped'><tbody><tr>";
html += " <td>Playing</td>";
html += " <td class='text-end'>";
html += audioPlaying ? "yes" : "no";
html += " </td>";
html += " </tr><tr>";
html += " <td>Volume</td>";
html += " <td class='text-end'>" + String(currentVolume) + "/" + String(maxVolume) + "%</td>";
html += " </tr><tr>";
html += " <td>Muted</td>";
html += " <td class='text-end'>";
html += muted ? "yes" : "no";
html += " </td>";
html += " </tr><tr>";
html += " <td>Equalizer Low</td>";
html += " <td class='text-end'>" + String(eqLow) + "dB</td>";
html += " </tr><tr>";
html += " <td>Equalizer Mid</td>";
html += " <td class='text-end'>" + String(eqMid) + "dB</td>";
html += " </tr><tr>";
html += " <td>Equalizer High</td>";
html += " <td class='text-end'>" + String(eqHigh) + "dB</td>";
html += " </tr><tr>";
html += " <td>Balance (range -16 | +16)</td>";
html += " <td class='text-end'>" + String(balanceLevel) + "</td>";
html += " </tr><tr>";
html += " <td>Path to playlist</td>";
html += " <td class='text-end'>" + currentPlaylist + "</td>";
html += " </tr><tr>";
html += " <td>Index in playlist (starting from 0)</td>";
html += " <td class='text-end'>" + String(currentPlaylistPosition) + "</td>";
html += " </tr><tr>";
html += " <td>Resource path</td>";
html += " <td class='text-end'>" + pbInfo.resourcePath + "</td>";
html += " </tr><tr>";
html += " <td>Resource type</td>";
html += " <td class='text-end'>" + pbInfo.type + "</td>";
html += " </tr><tr>";
html += " <td>Resource title</td>";
html += " <td class='text-end'>" + pbInfo.title + "</td>";
html += " </tr><tr>";
html += " <td>Resource album</td>";
html += " <td class='text-end'>" + pbInfo.album + "</td>";
html += " </tr><tr>";
html += " <td>Resource artist</td>";
html += " <td class='text-end'>" + pbInfo.artist + "</td>";
html += " </tr><tr>";
html += " <td>Resource track number</td>";
html += " <td class='text-end'>" + pbInfo.track + "</td>";
html += " </tr><tr>";
html += " <td>Resource year</td>";
html += " <td class='text-end'>" + pbInfo.year + "</td>";
html += " </tr><tr>";
html += " <td>Resource genre</td>";
html += " <td class='text-end'>" + pbInfo.genre + "</td>";
html += " </tr></tbody></table>";
html += " </div>";
html += " </div>";
html += " </div>";
html += " <div class='accordion-item'>";
html += " <h2 class='accordion-header'>";
html += " <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-info-general' aria-expanded='true' aria-controls='accordion-info-general'>";
html += " General information";
html += " </button>";
html += " </h2>";
html += " <div id='accordion-info-general' class='accordion-collapse collapse' data-bs-parent='#accordion-info'>";
html += " <div class='accordion-body'>";
html += " <table class='table table-striped'><tbody><tr>";
html += " <td>Version</td>";
html += " <td class='text-end'>" + version + "</td>";
html += " </tr><tr>";
html += " <td>Friendly name</td>";
html += " <td class='text-end'>" + configuration.getString(PREFERENCES_KEY_FRIENDLY_NAME, "<not_given>") + "</td>";
html += " </tr><tr>";
html += " <td>Save & Restore state</td>";
html += " <td class='text-end'>";
html += configuration.getBool(PREFERENCES_KEY_RESTORE_OLD_STATE, false) ? "yes" : "no";
html += " </td>";
html += " </tr><tr>";
html += " <td>Save & Restore playing state (default no)</td>";
html += " <td class='text-end'>";
html += configuration.getBool(PREFERENCES_KEY_RESTORE_PLAYING, false) ? "yes" : "no";
html += " </td>";
html += " </tr><tr>";
html += " <td>WiFi SSID</td>";
html += " <td class='text-end'>" + configuration.getString(PREFERENCES_KEY_WIFI_SSID, "") + "</td>";
html += " </tr></tbody></table>";
html += " </div>";
html += " </div>";
html += " </div>";
html += "</div>";
html += "<hr style='margin: 3em 0em;'>";
/* ----------------------------
-- API FUNCTIONS ACCODRION --
---------------------------- */
html += "<div class='accordion text-start' id='accordion'>";
// PLAYBACK Collapsible
html += " <div class='accordion-item'>";
html += " <h2 class='accordion-header'>";
html += " <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-playback' aria-expanded='true' aria-controls='accordion-collapse-playback'>";
html += " Playback";
html += " &nbsp;<span class='badge text-bg-success'>/api/v1/playback/</span>";
html += " &nbsp;<span class='badge text-bg-info'>/api/v1/volume/</span>";
html += " </button>";
html += " </h2>";
html += " <div id='accordion-collapse-playback' class='accordion-collapse collapse' data-bs-parent='#accordion'>";
html += " <div class='accordion-body'>";
html += " <table class='table table-striped'><tbody><tr>";
html += " <td>Playing</td>";
html += " <td class='text-end' id='playbackTable_audioPlayingValue'>";
html += audioPlaying ? "yes" : "no";
html += " </td>";
html += " </tr><tr>";
html += " <td>Volume</td>";
html += " <td class='text-end' id='playbackTable_volumeValue'>" + String(currentVolume) + "/" + String(maxVolume) + "%</td>";
html += " </tr><tr>";
html += " <td>Muted</td>";
html += " <td class='text-end' id='playbackTable_mutedValue'>";
html += muted ? "yes" : "no";
html += " </td>";
html += " </tr></tbody></table>";
html += " <hr><div class='btn-group' role='group' aria-label='Basic playback option (play, pause, next, previous)'>";
html += " <button type='button' class='btn btn-secondary' onclick=\"http = new XMLHttpRequest();http.open('GET', '/api/v1/playback/previous');http.send();\">";
html += " <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-rewind-circle' viewBox='0 0 16 16'>";
html += " <path d='M7.729 5.055a.5.5 0 0 0-.52.038l-3.5 2.5a.5.5 0 0 0 0 .814l3.5 2.5A.5.5 0 0 0 8 10.5V8.614l3.21 2.293A.5.5 0 0 0 12 10.5v-5a.5.5 0 0 0-.79-.407L8 7.386V5.5a.5.5 0 0 0-.271-.445'/>";
html += " <path d='M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0M1 8a7 7 0 1 0 14 0A7 7 0 0 0 1 8'/>";
html += " </svg>";
html += " </button>";
html += " <button type='button' class='btn btn-success' onclick=\"http = new XMLHttpRequest();http.open('GET', '/api/v1/playback/play');http.send();document.getElementById('playbackTable_audioPlayingValue').innerHTML = 'yes';\">";
html += " <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-play-circle' viewBox='0 0 16 16'>";
html += " <path d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'></path>";
html += " <path d='M6.271 5.055a.5.5 0 0 1 .52.038l3.5 2.5a.5.5 0 0 1 0 .814l-3.5 2.5A.5.5 0 0 1 6 10.5v-5a.5.5 0 0 1 .271-.445'></path>";
html += " </svg>";
html += " </button>";
html += " <button type='button' class='btn btn-success' onclick=\"http = new XMLHttpRequest();http.open('GET', '/api/v1/playback/pause');http.send();document.getElementById('playbackTable_audioPlayingValue').innerHTML = 'no';\">";
html += " <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-pause-circle' viewBox='0 0 16 16'>";
html += " <path d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/>";
html += " <path d='M5 6.25a1.25 1.25 0 1 1 2.5 0v3.5a1.25 1.25 0 1 1-2.5 0zm3.5 0a1.25 1.25 0 1 1 2.5 0v3.5a1.25 1.25 0 1 1-2.5 0z'/>";
html += " </svg>";
html += " </button>";
html += " <button type='button' class='btn btn-secondary' onclick=\"http = new XMLHttpRequest();http.open('GET', '/api/v1/playback/next');http.send();\">";
html += " <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-skip-forward-circle' viewBox='0 0 16 16'>";
html += " <path d='M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16'/>";
html += " <path d='M4.271 5.055a.5.5 0 0 1 .52.038L7.5 7.028V5.5a.5.5 0 0 1 .79-.407L11 7.028V5.5a.5.5 0 0 1 1 0v5a.5.5 0 0 1-1 0V8.972l-2.71 1.935a.5.5 0 0 1-.79-.407V8.972l-2.71 1.935A.5.5 0 0 1 4 10.5v-5a.5.5 0 0 1 .271-.445'/>";
html += " </svg>";
html += " </button>";
html += " </div>";
html += " <br><hr><br><label for='volumeRange' class='form-label'>Volume</label>";
html += " <input type='range' class='form-range' value='" + String(currentVolume) + "' min='0' max='" + String(maxVolume) + "' id='volumeRange' onchange='value = document.getElementById(\"volumeRange\").value; http = new XMLHttpRequest();http.open(\"GET\", \"/api/v1/volume/\" + value);http.send();document.getElementById(\"playbackTable_volumeValue\").innerHTML = value + \"/" + String(maxVolume) + "%\"'>";
html += " <button type='button' id='playbackMuteButton' onclick=";
html += " \"http = new XMLHttpRequest();";
html += " http.open('GET', '/api/v1/volume/toggle_mute');";
html += " http.send(); button = document.getElementById('playbackMuteButton');";
html += " button.classList.contains('btn-outline-danger') ? button.className = 'btn btn-danger' : button.className = 'btn btn-outline-danger';";
html += " mutedValueHTML = document.getElementById('playbackTable_mutedValue');";
html += " button.classList.contains('btn-outline-danger') ? mutedValueHTML.innerHTML = 'no' : mutedValueHTML.innerHTML = 'yes'\" class='btn ";
html += muted ? "btn-danger'>" : "btn-outline-danger'>";
html += " <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-volume-mute' viewBox='0 0 16 16'>";
html += " <path d='M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06zM6 5.04 4.312 6.39A.5.5 0 0 1 4 6.5H2v3h2a.5.5 0 0 1 .312.11L6 10.96zm7.854.606a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0z'></path>";
html += " </svg> Mute/Unmute</button>";
html += " </div>";
html += " </div>";
html += " </div>";
// PLAYLIST Collapsible
html += " <div class='accordion-item'>";
html += " <h2 class='accordion-header'>";
html += " <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-playlist' aria-expanded='true' aria-controls='accordion-collapse-playlist'>";
html += " Playlist";
html += " &nbsp;<span class='badge text-bg-success'>/api/v1/playlist/</span>";
html += " </button>";
html += " </h2>";
html += " <div id='accordion-collapse-playlist' class='accordion-collapse collapse' data-bs-parent='#accordion'>";
html += " <div class='accordion-body'>";
html += " <strong>Work in progress</strong>";
html += " </div>";
html += " </div>";
html += " </div>";
// BALANCE Collapsible
html += " <div class='accordion-item'>";
html += " <h2 class='accordion-header'>";
html += " <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-balance' aria-expanded='true' aria-controls='accordion-collapse-balance'>";
html += " Balance";
html += " &nbsp;<span class='badge text-bg-info'>/api/v1/balance/</span>";
html += " </button>";
html += " </h2>";
html += " <div id='accordion-collapse-balance' class='accordion-collapse collapse' data-bs-parent='#accordion'>";
html += " <div class='accordion-body'>";
html += " <p>The Audio library used maps -16 to most right and 16 to most left. Don't be confused by the appearently wrong direction the slider moves the balance to right and left.</p>";
html += " <table class='table table-striped'><tbody><tr>";
html += " <td>Balance (range -16 | +16)</td>";
html += " <td class='text-end' id='balanceChangeTable_balanceValue'>" + String(balanceLevel) + "</td>";
html += " </tr></tbody></table>";
html += " <br><hr><label for='balanceRange' class='form-label'>Balance (-16 to 16)</label>";
html += " <input type='range' class='form-range' value='" + String(balanceLevel+16) + "' min='0' max='32' id='balanceRange' onchange='value = document.getElementById(\"balanceRange\").value; http = new XMLHttpRequest();http.open(\"GET\", \"/api/v1/balance/\" + value);http.send(); document.getElementById(\"balanceChangeTable_balanceValue\").innerHTML = value-16;'>";
html += " </div>";
html += " </div>";
html += " </div>";
// EQUALIZER Collapsible
html += " <div class='accordion-item'>";
html += " <h2 class='accordion-header'>";
html += " <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-eq' aria-expanded='true' aria-controls='accordion-collapse-eq'>";
html += " Equalizer";
html += " &nbsp;<span class='badge text-bg-info'>/api/v1/eq/</span>";
html += " </button>";
html += " </h2>";
html += " <div id='accordion-collapse-eq' class='accordion-collapse collapse' data-bs-parent='#accordion'>";
html += " <div class='accordion-body'>";
html += " <table class='table table-striped'><tbody><tr>";
html += " <td>Equalizer Low (-40dB to 6dB)</td>";
html += " <td class='text-end' id='eqChangeTable_eqLowValue'>" + String(eqLow) + "dB</td>";
html += " </tr><tr>";
html += " <td>Equalizer Mid (-40dB to 6dB)</td>";
html += " <td class='text-end' id='eqChangeTable_eqMidValue'>" + String(eqMid) + "dB</td>";
html += " </tr><tr>";
html += " <td>Equalizer High (-40dB to 6dB)</td>";
html += " <td class='text-end' id='eqChangeTable_eqHighValue'>" + String(eqHigh) + "dB</td>";
html += " </tr></tbody></table>";
html += " <br><hr><label for='eqLowRange' class='form-label'>Equalizer for lows (-40dB to 6dB)</label>";
html += " <input type='range' class='form-range' value='" + String(eqLow+40) + "' min='0' max='46' id='eqLowRange' onchange='value = document.getElementById(\"eqLowRange\").value; http = new XMLHttpRequest();http.open(\"GET\", \"/api/v1/eq/low/\" + value);http.send(); document.getElementById(\"eqChangeTable_eqLowValue\").innerHTML = value-40 + \"dB\";'>";
html += " <br><hr><br><label for='eqMidRange' class='form-label'>Equalizer for mids (-40dB to 6dB)</label>";
html += " <input type='range' class='form-range' value='" + String(eqMid+40) + "' min='0' max='46' id='eqMidRange' onchange='value = document.getElementById(\"eqMidRange\").value; http = new XMLHttpRequest();http.open(\"GET\", \"/api/v1/eq/mid/\" + value);http.send(); document.getElementById(\"eqChangeTable_eqMidValue\").innerHTML = value-40 + \"dB\";'>";
html += " <br><hr><br><label for='eqHighRange' class='form-label'>Equalizer for highs (-40dB to 6dB)</label>";
html += " <input type='range' class='form-range' value='" + String(eqHigh+40) + "' min='0' max='46' id='eqHighRange' onchange='value = document.getElementById(\"eqHighRange\").value; http = new XMLHttpRequest();http.open(\"GET\", \"/api/v1/eq/high/\" + value);http.send(); document.getElementById(\"eqChangeTable_eqHighValue\").innerHTML = value-40 + \"dB\";'>";
html += " </div>";
html += " </div>";
html += " </div>";
// FRIENDLY NAME Collapsible
html += " <div class='accordion-item'>";
html += " <h2 class='accordion-header'>";
html += " <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-frname' aria-expanded='true' aria-controls='accordion-collapse-frname'>";
html += " Friendly name";
html += " &nbsp;<span class='badge text-bg-warning'>/api/v1/system/name</span>"; // explicitly no trailing / as there are the two endpoints /name and /name/change
html += " </button>";
html += " </h2>";
html += " <div id='accordion-collapse-frname' class='accordion-collapse collapse' data-bs-parent='#accordion'>";
html += " <div class='accordion-body'>";
html += " <strong>Work in progress</strong>";
html += " </div>";
html += " </div>";
html += " </div>";
// WiFi CONFIGURATION Collapsible
html += " <div class='accordion-item'>";
html += " <h2 class='accordion-header'>";
html += " <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-wifi' aria-expanded='true' aria-controls='accordion-collapse-wifi'>";
html += " WiFi configuration";
html += " &nbsp;<span class='badge text-bg-warning'>/api/v1/system/wifi/</span>";
html += " </button>";
html += " </h2>";
html += " <div id='accordion-collapse-wifi' class='accordion-collapse collapse' data-bs-parent='#accordion'>";
html += " <div class='accordion-body'>";
html += " <strong>Work in progress</strong>";
html += " </div>";
html += " </div>";
html += " </div>";
// SAVE AND RESTORE Collapsible
html += " <div class='accordion-item'>";
html += " <h2 class='accordion-header'>";
html += " <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-restore' aria-expanded='true' aria-controls='accordion-collapse-restore'>";
html += " Save & Restore";
html += " &nbsp;<span class='badge text-bg-warning'>/api/v1/system/restore_state/</span>";
html += " &nbsp;<span class='badge text-bg-warning'>/api/v1/system/restore_playing/</span>";
html += " </button>";
html += " </h2>";
html += " <div id='accordion-collapse-restore' class='accordion-collapse collapse' data-bs-parent='#accordion'>";
html += " <div class='accordion-body'>";
html += " <strong>Work in progress</strong>";
html += " </div>";
html += " </div>";
html += " </div>";
// RESTART Collapsible
html += " <div class='accordion-item'>";
html += " <h2 class='accordion-header'>";
html += " <button class='accordion-button collapsed' type='button' data-bs-toggle='collapse' data-bs-target='#accordion-collapse-restart' aria-expanded='true' aria-controls='accordion-collapse-restart'>";
html += " Restart";
html += " &nbsp;<span class='badge text-bg-danger'>/api/v1/system/restart</span>";
html += " </button>";
html += " </h2>";
html += " <div id='accordion-collapse-restart' class='accordion-collapse collapse' data-bs-parent='#accordion'>";
html += " <div class='accordion-body'>";
html += " <button type='button' class='btn btn-danger' onclick=\"function wait(ms){var start = new Date().getTime();var end=start;while(end<start+ms){end = new Date().getTime();}}http = new XMLHttpRequest();http.open('GET', '/api/v1/system/restart');http.send();wait(3000);window.location.reload();\">";
html += " <svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-bootstrap-reboot' viewBox='0 0 16 16'>";
html += " <path d='M1.161 8a6.84 6.84 0 1 0 6.842-6.84.58.58 0 1 1 0-1.16 8 8 0 1 1-6.556 3.412l-.663-.577a.58.58 0 0 1 .227-.997l2.52-.69a.58.58 0 0 1 .728.633l-.332 2.592a.58.58 0 0 1-.956.364l-.643-.56A6.812 6.812 0 0 0 1.16 8z'></path>";
html += " <path d='M6.641 11.671V8.843h1.57l1.498 2.828h1.314L9.377 8.665c.897-.3 1.427-1.106 1.427-2.1 0-1.37-.943-2.246-2.456-2.246H5.5v7.352zm0-3.75V5.277h1.57c.881 0 1.416.499 1.416 1.32 0 .84-.504 1.324-1.386 1.324h-1.6z'></path>";
html += " </svg> Restart";
html += " </button>";
html += " </div>";
html += " </div>";
html += " </div>";
html += "</div></div>";
html += "<script src='https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js' integrity='sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL' crossorigin='anonymous'></script>";
html += "</body></html>";
api_server.send(200, "text/html", html);
} }
/* ----------------------
---- API FUNCTIONS ----
-----------------------*/
void api_v1_playback_toggle() { void api_v1_playback_toggle() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/toggle'"); Serial.println("[HTTP] [API] 200 - '/api/v1/playback/toggle'");
audioPlaying = !audioPlaying; audioPlaying = !audioPlaying;
Serial.printf("[INFO] Playback has switched: %s\n", audioPlaying ? "Playing" : "Paused"); Serial.printf("[INFO] Playback has switched: %s\n", audioPlaying ? "Playing" : "Paused");
api_server.send(200, "application/json", generate_api_json(true)); send_api_json(true);
} }
void api_v1_playback_play() { void api_v1_playback_play() {
Serial.println("[HTTP] [API] 200 - GET '/_api/v1/playback/play'"); Serial.println("[HTTP] [API] 200 - '/_api/v1/playback/play'");
audioPlaying = true; audioPlaying = true;
Serial.printf("[INFO] Playback has switched: Playing\n"); Serial.printf("[INFO] Playback has switched: Playing\n");
api_server.send(200, "application/json", generate_api_json(true)); send_api_json(true);
} }
void api_v1_playback_pause() { void api_v1_playback_pause() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/pause'"); Serial.println("[HTTP] [API] 200 - '/api/v1/playback/pause'");
audioPlaying = false; audioPlaying = false;
Serial.printf("[INFO] Playback has switched: Paused\n"); Serial.printf("[INFO] Playback has switched: Paused\n");
api_server.send(200, "application/json", generate_api_json(true)); send_api_json(true);
} }
void api_v1_playback_next() { void api_v1_playback_next() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/next'"); Serial.println("[HTTP] [API] 200 - '/api/v1/playback/next'");
nextAudio(); nextAudio();
String content = "\"resource_playlist_index\": "; String content = "\"resource_playlist_index\": ";
content += String(currentPlaylistPosition); content += String(currentPlaylistPosition);
api_server.send(200, "application/json", generate_api_json(true, content)); send_api_json(true, content);
} }
void api_v1_playback_previous() { void api_v1_playback_previous() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/previous'"); Serial.println("[HTTP] [API] 200 - '/api/v1/playback/previous'");
previousAudio(); previousAudio();
String content = "\"resource_playlist_index\": "; String content = "\"resource_playlist_index\": ";
content += String(currentPlaylistPosition); content += String(currentPlaylistPosition);
api_server.send(200, "application/json", generate_api_json(true, content)); send_api_json(true, content);
} }
void api_v1_playback_byindex() { void api_v1_playback_byindex() {
String option = api_server.pathArg(0); String option = api_server.pathArg(0);
String toIntStr; String toIntStr;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/playback/%s'\n", option); Serial.printf("[HTTP] [API] 200 - '/api/v1/playback/%s'\n", option);
// convert string to int // convert string to int
for (int i = 0; i < option.length(); i++) { for (int i = 0; i < option.length(); i++) {
if (isDigit((char)option[i])) toIntStr += option[i]; if (isDigit((char)option[i])) toIntStr += option[i];
else { else {
api_server.send(200, "application/json", generate_api_json(false)); send_api_json(false);
return; return;
} }
} }
@@ -440,18 +132,18 @@ void api_v1_playback_byindex() {
currentPlaylistPosition = index - 1; currentPlaylistPosition = index - 1;
nextAudio(); // the clean way of playing the next resource nextAudio(); // the clean way of playing the next resource
} else { } else {
api_server.send(200, "application/json", generate_api_json(false)); send_api_json(false);
} }
String content = "\"resource_playlist_index\": "; String content = "\"resource_playlist_index\": ";
content += String(currentPlaylistPosition); content += String(currentPlaylistPosition);
api_server.send(200, "application/json", generate_api_json(true, content)); send_api_json(true, content);
} }
void api_v1_volume() { void api_v1_volume() {
String option = api_server.pathArg(0); String option = api_server.pathArg(0);
bool success = true; bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/volume/%s'\n", option); Serial.printf("[HTTP] [API] 200 - '/api/v1/volume/%s'\n", option);
if (option == "up") { if (option == "up") {
currentVolume++; currentVolume++;
@@ -468,7 +160,7 @@ void api_v1_volume() {
} else if (option == "get") { } else if (option == "get") {
// just here that no 'success: false' is sent // just here that no 'success: false' is sent
} else if (option == "get_max") { } else if (option == "get_max") {
api_server.send(200, "application/json", generate_api_json(success, "\"volume_max\": " + String(maxVolume))); send_api_json(success, "\"volume_max\": " + String(maxVolume));
} else { } else {
String toIntStr; String toIntStr;
int volumeLevel; int volumeLevel;
@@ -488,13 +180,14 @@ void api_v1_volume() {
content += String(currentVolume); content += String(currentVolume);
content += ", \"muted\": "; content += ", \"muted\": ";
content += muted ? "true" : "false"; content += muted ? "true" : "false";
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it content += ", \"volume_max\": " + String(maxVolume);
send_api_json(success, content); // generate json and send it
} }
void api_v1_balance() { void api_v1_balance() {
String option = api_server.pathArg(0); String option = api_server.pathArg(0);
bool success = true; bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/balance/%s'\n", option); Serial.printf("[HTTP] [API] 200 - '/api/v1/balance/%s'\n", option);
if (option == "right") { if (option == "right") {
balanceLevel--; balanceLevel--;
@@ -519,11 +212,11 @@ void api_v1_balance() {
String content = "\"balance\": "; // prepare the http response String content = "\"balance\": "; // prepare the http response
content += String(balanceLevel); content += String(balanceLevel);
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it send_api_json(success, content); // generate json and send it
} }
void api_v1_eq_get() { void api_v1_eq_get() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/eq/get"); Serial.println("[HTTP] [API] 200 - '/api/v1/eq/get");
String content = "\"equalizer_low\": "; // prepare the http response String content = "\"equalizer_low\": "; // prepare the http response
content += String(eqLow); content += String(eqLow);
@@ -532,11 +225,11 @@ void api_v1_eq_get() {
content += ", \"equalizer_high\": "; // prepare the http response content += ", \"equalizer_high\": "; // prepare the http response
content += String(eqHigh); content += String(eqHigh);
api_server.send(200, "application/json", generate_api_json(true, content)); // generate json and send it send_api_json(true, content); // generate json and send it
} }
void api_v1_eq_reset() { void api_v1_eq_reset() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/eq/reset"); Serial.println("[HTTP] [API] 200 - '/api/v1/eq/reset");
eqLow = 0; eqLow = 0;
eqMid = 0; eqMid = 0;
@@ -552,13 +245,13 @@ void api_v1_eq_reset() {
content += ", \"equalizer_high\": "; // prepare the http response content += ", \"equalizer_high\": "; // prepare the http response
content += String(eqHigh); content += String(eqHigh);
api_server.send(200, "application/json", generate_api_json(true, content)); // generate json and send it send_api_json(true, content); // generate json and send it
} }
void api_v1_eq_low() { void api_v1_eq_low() {
String option = api_server.pathArg(0); String option = api_server.pathArg(0);
bool success = true; bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/eq/low/%s'\n", option); Serial.printf("[HTTP] [API] 200 - '/api/v1/eq/low/%s'\n", option);
if (option == "get") { if (option == "get") {
// just here to make /get available // just here to make /get available
@@ -578,12 +271,12 @@ void api_v1_eq_low() {
String content = "\"equalizer_low\": "; // prepare the http response String content = "\"equalizer_low\": "; // prepare the http response
content += String(eqLow); content += String(eqLow);
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it send_api_json(success, content); // generate json and send it
} }
void api_v1_eq_mid() { void api_v1_eq_mid() {
String option = api_server.pathArg(0); String option = api_server.pathArg(0);
bool success = true; bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/eq/mid/%s'\n", option); Serial.printf("[HTTP] [API] 200 - '/api/v1/eq/mid/%s'\n", option);
if (option == "get") { if (option == "get") {
// just here to make /get available // just here to make /get available
@@ -603,12 +296,12 @@ void api_v1_eq_mid() {
String content = "\"equalizer_mid\": "; // prepare the http response String content = "\"equalizer_mid\": "; // prepare the http response
content += String(eqMid); content += String(eqMid);
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it send_api_json(success, content); // generate json and send it
} }
void api_v1_eq_high() { void api_v1_eq_high() {
String option = api_server.pathArg(0); String option = api_server.pathArg(0);
bool success = true; bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/eq/high/%s'\n", option); Serial.printf("[HTTP] [API] 200 - '/api/v1/eq/high/%s'\n", option);
if (option == "get") { if (option == "get") {
// just here to make /get available // just here to make /get available
@@ -628,11 +321,11 @@ void api_v1_eq_high() {
String content = "\"equalizer_high\": "; // prepare the http response String content = "\"equalizer_high\": "; // prepare the http response
content += String(eqHigh); content += String(eqHigh);
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it send_api_json(success, content); // generate json and send it
} }
void api_v1_playback_info() { void api_v1_playback_info() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/info'"); Serial.println("[HTTP] [API] 200 - '/api/v1/playback/info'");
String content = "\"resource_path\": \"" + pbInfo.resourcePath + "\", "; // resource's path String content = "\"resource_path\": \"" + pbInfo.resourcePath + "\", "; // resource's path
content += "\"resource_type\": \"" + pbInfo.type + "\", "; // type content += "\"resource_type\": \"" + pbInfo.type + "\", "; // type
content += "\"resource_title\": \"" + pbInfo.title + "\", "; // title content += "\"resource_title\": \"" + pbInfo.title + "\", "; // title
@@ -642,14 +335,15 @@ void api_v1_playback_info() {
content += "\"resource_year\": \"" + pbInfo.year + "\", "; // year content += "\"resource_year\": \"" + pbInfo.year + "\", "; // year
content += "\"resource_genre\": \"" + pbInfo.genre + "\", "; // genre content += "\"resource_genre\": \"" + pbInfo.genre + "\", "; // genre
content += "\"resource_copyright\": \"" + pbInfo.copyright + "\", "; // copyright content += "\"resource_copyright\": \"" + pbInfo.copyright + "\", "; // copyright
content += "\"resource_tts_language\": \"" + pbInfo.tts_language + "\", "; // language
content += "\"resource_playlist_path\": \"" + currentPlaylist + "\", "; // playlist path content += "\"resource_playlist_path\": \"" + currentPlaylist + "\", "; // playlist path
content += "\"resource_playlist_index\": " + String(currentPlaylistPosition); // playlist index (starting from 0) content += "\"resource_playlist_index\": " + String(currentPlaylistPosition); // playlist index (starting from 0)
api_server.send(200, "application/json", generate_api_json(true, content)); send_api_json(true, content);
} }
void api_v1_playlist_get() { void api_v1_playlist_get() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playlist/get'"); Serial.println("[HTTP] [API] 200 - '/api/v1/playlist/get'");
int index = 0; int index = 0;
String content = "\"playlist\": {"; String content = "\"playlist\": {";
@@ -673,16 +367,16 @@ void api_v1_playlist_get() {
} }
content += "}"; content += "}";
api_server.send(200, "application/json", generate_api_json(true, content)); send_api_json(true, content);
} }
void api_v1_playlist_create() { void api_v1_playlist_create() {
Serial.println("[HTTP] [API] 200 - POST '/api/v1/playlist/create'"); Serial.println("[HTTP] [API] 200 - '/api/v1/playlist/create'");
// get the right POST param // get the right POST param
String folderPath; String folderPath;
for (int i = 0; i < api_server.args(); i++) { for (int i = 0; i < api_server.args(); i++) {
if (api_server.argName(i) == "folderPath") { if (api_server.argName(i) == "folder_path") {
folderPath = api_server.arg(i); folderPath = api_server.arg(i);
break; break;
} }
@@ -692,53 +386,54 @@ void api_v1_playlist_create() {
if (code) { // if creating the playlist was successful if (code) { // if creating the playlist was successful
String playlistPath = folderPath + "/" + directoryPlaylistName + playlistExtension; String playlistPath = folderPath + "/" + directoryPlaylistName + playlistExtension;
String content = "\"playlist_path\": \"" + playlistPath + "\""; String content = "\"playlist_path\": \"" + playlistPath + "\"";
api_server.send(200, "application/json", generate_api_json(true, content)); send_api_json(true, content);
} else { } else {
api_server.send(200, "application/json", generate_api_json(false)); send_api_json(false);
} }
} }
void api_v1_playlist_play() { void api_v1_playlist_play() {
Serial.println("[HTTP] [API] 200 - POST '/api/v1/playlist/play'"); Serial.println("[HTTP] [API] 200 - '/api/v1/playlist/play'");
// get the right POST param // get the right POST param
String playlistPath; String playlistPath;
for (int i = 0; i < api_server.args(); i++) { for (int i = 0; i < api_server.args(); i++) {
if (api_server.argName(i) == "playlistPath") { if (api_server.argName(i) == "playlist_path") {
playlistPath = api_server.arg(i); playlistPath = api_server.arg(i);
break; break;
} }
} }
if (getURLFromPlaylist(playlistPath, 0) == "") { // if playlist is empty or nonexistent if (getURLFromPlaylist(playlistPath, 0) == "") { // get the first item from the playlist to check whether the pl is empty or nonexistent
api_server.send(200, "application/json", generate_api_json(false)); // send no success info send_api_json(false); // send no success info
} else { } else {
currentPlaylist = playlistPath; currentPlaylist = playlistPath;
currentPlaylistPosition = -1; currentPlaylistPosition = -1;
nextAudio(); nextAudio();
api_server.send(200, "application/json", generate_api_json(true)); // just return the success info send_api_json(true); // just return the success info
} }
} }
void api_v1_system_restart() { void api_v1_system_restart() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/system/restart'"); Serial.println("[HTTP] [API] 200 - '/api/v1/system/restart'");
SD.end(); // delete SD object and sync its cache to flash SD.end(); // delete SD object and sync its cache to flash
WiFi.disconnect(); // disconnect wifi WiFi.disconnect(); // disconnect wifi
Serial.printf("[INFO] Restarting after 5000ms.\n", waitOnSDCardEject, retrySDMountTreshold); Serial.printf("[INFO] Restarting after 5000ms.\n", waitOnSDCardEject, retrySDMountTreshold);
api_server.sendHeader("Access-Control-Allow-Origin", "*");
api_server.send(200, "application/json", "{\"restart\": true, \"wait_time\": 5000}"); api_server.send(200, "application/json", "{\"restart\": true, \"wait_time\": 5000}");
delay(WEB_RESTART_DELAY); delay(WEB_RESTART_DELAY);
ESP.restart(); // reset everything and restart the program ESP.restart(); // reset everything and restart the program
} }
void api_v1_system_friendlyname_get() { void api_v1_system_friendlyname_get() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/system/name'"); Serial.println("[HTTP] [API] 200 - '/api/v1/system/name'");
api_server.send(200, "application/json", generate_api_json(true, "\"friendly_name\": \"" + configuration.getString(PREFERENCES_KEY_FRIENDLY_NAME) + "\"")); send_api_json(true, "\"friendly_name\": \"" + configuration.getString(PREFERENCES_KEY_FRIENDLY_NAME) + "\"");
} }
void api_v1_system_friendlyname_change() { void api_v1_system_friendlyname_change() {
Serial.println("[HTTP] [API] 200 - POST '/api/v1/system/name/change'"); Serial.println("[HTTP] [API] 200 - '/api/v1/system/name/change'");
String currentFriendlyName = configuration.getString(PREFERENCES_KEY_FRIENDLY_NAME, ""); String currentFriendlyName = configuration.getString(PREFERENCES_KEY_FRIENDLY_NAME, "");
String newFriendlyName; String newFriendlyName;
@@ -753,23 +448,62 @@ void api_v1_system_friendlyname_change() {
} }
// define what to do if nothing really changed (or no argument was given) // define what to do if nothing really changed (or no argument was given)
if (newFriendlyName == currentFriendlyName || !arg_given) { if (newFriendlyName == currentFriendlyName || !arg_given) {
String content; send_api_json(false, "\"friendly_name\": \"" + currentFriendlyName + "\""); // return with no success ("false")
content = "\"friendly_name\": \"" + currentFriendlyName + "\"";
api_server.send(200, "application/json", generate_api_json(false, content)); // return with no success ("false")
return; return;
} }
configuration.putString(PREFERENCES_KEY_FRIENDLY_NAME, newFriendlyName); configuration.putString(PREFERENCES_KEY_FRIENDLY_NAME, newFriendlyName);
Serial.printf("[INFO] Changed friendly name from \"%s\" to \"%s\".\n", currentFriendlyName.c_str(), newFriendlyName.c_str()); Serial.printf("[INFO] Changed friendly name from \"%s\" to \"%s\".\n", currentFriendlyName, newFriendlyName);
api_server.send(200, "application/json", generate_api_json(true, "\"friendly_name\": \"" + newFriendlyName + "\"")); send_api_json(true, "\"friendly_name\": \"" + newFriendlyName + "\"");
}
void api_v1_system_networkname_get() {
Serial.println("[HTTP] [API] 200 - '/api/v1/system/network_name'");
send_api_json(true, "\"network_name\": \"" + configuration.getString(PREFERENCES_KEY_NETWORK_NAME, default_network_name) + "\"");
}
void api_v1_system_networkname_change() {
Serial.println("[HTTP] [API] 200 - '/api/v1/system/network_name/change'");
String currentNetworkName = configuration.getString(PREFERENCES_KEY_NETWORK_NAME, default_network_name);
String newNetworkName;
bool arg_given = false;
bool apply_changes = false; // whether changes should be applied directly
// get the right post param (by key)
for (int i = 0; i < api_server.args(); i++) {
if (api_server.argName(i) == "apply_changes") {
apply_changes = true;
}
if (api_server.argName(i) == "network_name") {
arg_given = true;
newNetworkName = api_server.arg(i);
}
}
// define what to do if nothing really changed (or no argument was given)
if (!isValidNetworkName(newNetworkName) || !arg_given) {
send_api_json(false, "\"network_name\": \"" + currentNetworkName + "\", \"apply_changes\": false"); // return with no success ("false")
return;
}
// if the network name hasn't changed, just return success (as no further action's required)
if (newNetworkName == currentNetworkName) send_api_json(true, "\"network_name\": \"" + newNetworkName + "\", \"apply_changes\": " + (apply_changes ? "true" : "false"));
configuration.putString(PREFERENCES_KEY_NETWORK_NAME, newNetworkName);
Serial.printf("[INFO] Changed network name from \"%s\" to \"%s\".\n", currentNetworkName, newNetworkName);
if(apply_changes) {
setupMDNS();
}
send_api_json(true, "\"network_name\": \"" + newNetworkName + "\", \"apply_changes\": " + (apply_changes ? "true" : "false"));
} }
void api_v1_system_restore_state() { void api_v1_system_restore_state() {
String option = api_server.pathArg(0); String option = api_server.pathArg(0);
bool success = true; bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/system/restore_state/%s'\n", option); Serial.printf("[HTTP] [API] 200 - '/api/v1/system/restore_state/%s'\n", option);
if (option == "get") { if (option == "get") {
// just here that calling .../get wont respond 404 // just here that calling .../get wont respond 404
@@ -777,19 +511,21 @@ void api_v1_system_restore_state() {
configuration.putBool(PREFERENCES_KEY_RESTORE_OLD_STATE, true); configuration.putBool(PREFERENCES_KEY_RESTORE_OLD_STATE, true);
} else if (option == "off") { } else if (option == "off") {
configuration.putBool(PREFERENCES_KEY_RESTORE_OLD_STATE, false); configuration.putBool(PREFERENCES_KEY_RESTORE_OLD_STATE, false);
} else if (option == "toggle") { // toggle; default to true
configuration.putBool(PREFERENCES_KEY_RESTORE_OLD_STATE, !configuration.getBool(PREFERENCES_KEY_RESTORE_OLD_STATE, false));
} else { } else {
success = false; success = false;
} }
String content = "\"restore_state\": "; // prepare the http response String content = "\"restore_state\": "; // prepare the http response
content += configuration.getBool(PREFERENCES_KEY_RESTORE_OLD_STATE, false) ? "true" : "false"; content += configuration.getBool(PREFERENCES_KEY_RESTORE_OLD_STATE, false) ? "true" : "false";
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it send_api_json(success, content); // generate json and send it
} }
void api_v1_system_restore_playing() { void api_v1_system_restore_playing() {
String option = api_server.pathArg(0); String option = api_server.pathArg(0);
bool success = true; bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/system/restore_playing/%s'\n", option); Serial.printf("[HTTP] [API] 200 - '/api/v1/system/restore_playing/%s'\n", option);
if (option == "get") { if (option == "get") {
// just here that calling .../get wont respond 404 // just here that calling .../get wont respond 404
@@ -797,29 +533,83 @@ void api_v1_system_restore_playing() {
configuration.putBool(PREFERENCES_KEY_RESTORE_PLAYING, true); configuration.putBool(PREFERENCES_KEY_RESTORE_PLAYING, true);
} else if (option == "off") { } else if (option == "off") {
configuration.putBool(PREFERENCES_KEY_RESTORE_PLAYING, false); configuration.putBool(PREFERENCES_KEY_RESTORE_PLAYING, false);
} else if (option == "toggle") { // toggle; default to false (do not restore playing by default)
configuration.putBool(PREFERENCES_KEY_RESTORE_PLAYING, !configuration.getBool(PREFERENCES_KEY_RESTORE_PLAYING, true));
} else { } else {
success = false; success = false;
} }
String content = "\"restore_playing\": "; // prepare the http response String content = "\"restore_playing\": "; // prepare the http response
content += configuration.getBool(PREFERENCES_KEY_RESTORE_PLAYING, false) ? "true" : "false"; content += configuration.getBool(PREFERENCES_KEY_RESTORE_PLAYING, false) ? "true" : "false";
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it send_api_json(success, content); // generate json and send it
} }
void api_v1_system_version() {
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/system/version'\n");
api_server.send(200, "application/json", generate_api_json(true, "\"version\": \"" + version + "\"")); void api_v1_system_tell_address() {
String option = api_server.pathArg(0);
bool success = true;
Serial.printf("[HTTP] [API] 200 - '/api/v1/system/tell_address/%s'\n", option);
if (option == "get") {
// just here that calling .../get wont respond 404
} else if (option == "on") {
configuration.putBool(PREFERENCES_KEY_TELL_ADDRESS_AT_STARTUP, true);
} else if (option == "off") {
configuration.putBool(PREFERENCES_KEY_TELL_ADDRESS_AT_STARTUP, false);
} else if (option == "toggle") { // toggle; default to true (tell by default)
configuration.putBool(PREFERENCES_KEY_TELL_ADDRESS_AT_STARTUP, !configuration.getBool(PREFERENCES_KEY_TELL_ADDRESS_AT_STARTUP, false));
} else {
success = false;
}
String content = "\"tell_address\": "; // prepare the http response
content += configuration.getBool(PREFERENCES_KEY_TELL_ADDRESS_AT_STARTUP, true) ? "true" : "false";
send_api_json(success, content); // generate json and send it
}
void api_v1_system_tell_address_lang() {
String option = api_server.pathArg(0);
bool success = true;
Serial.printf("[HTTP] [API] 200 - '/api/v1/system/tell_address/lang/%s'\n", option);
if (option == "get") {
// just here that calling .../get wont respond 404
} else if (option == "en") {
configuration.putString(PREFERENCES_KEY_TELL_ADDRESS_LANG, "en");
} else if (option == "de") {
configuration.putString(PREFERENCES_KEY_TELL_ADDRESS_LANG, "de");
} else {
success = false;
}
String content = "\"tell_address_lang\": \""; // prepare the http response
content += configuration.getString(PREFERENCES_KEY_TELL_ADDRESS_LANG, "en") + "\"";
send_api_json(success, content); // generate json and send it
}
void api_v1_system_version() {
Serial.printf("[HTTP] [API] 200 - '/api/v1/system/version'\n");
send_api_json(true, "\"version\": \"" + version + "\", " + "\"version_tag\": \"" + version_tag + "\"");
} }
void api_v1_system_wifi_getssid() { void api_v1_system_wifi_getssid() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/system/wifi/get_ssid'"); Serial.println("[HTTP] [API] 200 - '/api/v1/system/wifi/get_ssid'");
api_server.send(200, "application/json", generate_api_json(true, "\"wifi_ssid\": \"" + configuration.getString(PREFERENCES_KEY_WIFI_SSID) + "\"")); send_api_json(true, "\"wifi_ssid\": \"" + configuration.getString(PREFERENCES_KEY_WIFI_SSID) + "\"");
}
void
api_v1_system_wifi_get_ap_creds() {
Serial.println("[HTTP] [API] 200 - '/api/v1/system/wifi/get_ap_creds'");
send_api_json(true, "\"ap_ssid\": \"" + apSSID + "\", \"ap_psk\": \"" + apPSK + "\"");
} }
void api_v1_system_wifi_change() { void api_v1_system_wifi_change() {
Serial.println("[HTTP] [API] 200 - POST '/api/v1/system/wifi/change'"); Serial.println("[HTTP] [API] 200 - '/api/v1/system/wifi/change'");
// get the right POST params // get the right POST params
String ssid = ""; String ssid = "";
@@ -845,20 +635,26 @@ void api_v1_system_wifi_change() {
} else if (ssid == "" && psk != "") { // just the psk is given } else if (ssid == "" && psk != "") { // just the psk is given
configuration.putString(PREFERENCES_KEY_WIFI_PSK, psk); configuration.putString(PREFERENCES_KEY_WIFI_PSK, psk);
} else { // none of ssid or psk is given } else { // none of ssid or psk is given
api_server.send(200, "application/json", generate_api_json(false)); send_api_json(false);
return; return;
} }
Serial.printf("[INFO] Changed wifi credentials (SSID: '%s' | PSK: '%s')\n", ssid.c_str(), psk.c_str()); Serial.printf("[INFO] Changed wifi credentials (SSID: '%s' | PSK: '%s')\n", ssid.c_str(), psk.c_str());
api_server.send(200, "application/json", generate_api_json(true, "\"ssid\": \"" + ssid + "\", \"psk\": \"" + psk + "\"")); send_api_json(true, "\"ssid\": \"" + ssid + "\", \"psk\": \"" + psk + "\"");
} }
void setupWeb() { void setupWeb() {
api_server.on("/", api_root); api_server.on("/", []() {
Serial.println("[HTTP] [API] 200 - '/'");
char message[] = "{\"message\": \"Welcome to your NetSpeaker's API! More info can be found at https://git.privacynerd.de/NetSpeaker/NetSpeaker\"}";
api_server.sendHeader("Access-Control-Allow-Origin", "*");
api_server.send(200, "application/json", message);
}); // on /
api_server.onNotFound([]() { api_server.onNotFound([]() {
Serial.println("[HTTP] [API] 404: Not Found"); Serial.println("[HTTP] [API] 404: Not Found");
api_server.sendHeader("Access-Control-Allow-Origin", "*");
api_server.send(404, "application/json", "{\"code\": 404, \"message\": \"Resource not found.\"}"); api_server.send(404, "application/json", "{\"code\": 404, \"message\": \"Resource not found.\"}");
}); }); // on NotFound (the fallback if nothing else configured for the requested URL)
api_server.on("/api/v1/playback/toggle", api_v1_playback_toggle); api_server.on("/api/v1/playback/toggle", api_v1_playback_toggle);
api_server.on("/api/v1/playback/play", api_v1_playback_play); api_server.on("/api/v1/playback/play", api_v1_playback_play);
api_server.on("/api/v1/playback/pause", api_v1_playback_pause); api_server.on("/api/v1/playback/pause", api_v1_playback_pause);
@@ -872,18 +668,23 @@ void setupWeb() {
api_server.on(UriBraces("/api/v1/volume/{}"), api_v1_volume); api_server.on(UriBraces("/api/v1/volume/{}"), api_v1_volume);
api_server.on(UriBraces("/api/v1/balance/{}"), api_v1_balance); api_server.on(UriBraces("/api/v1/balance/{}"), api_v1_balance);
api_server.on("/api/v1/eq/get", api_v1_eq_get); api_server.on("/api/v1/eq/get", api_v1_eq_get);
api_server.on(UriBraces("/api/v1/eq/reset"), api_v1_eq_reset); api_server.on("/api/v1/eq/reset", api_v1_eq_reset);
api_server.on(UriBraces("/api/v1/eq/low/{}"), api_v1_eq_low); api_server.on(UriBraces("/api/v1/eq/low/{}"), api_v1_eq_low);
api_server.on(UriBraces("/api/v1/eq/mid/{}"), api_v1_eq_mid); api_server.on(UriBraces("/api/v1/eq/mid/{}"), api_v1_eq_mid);
api_server.on(UriBraces("/api/v1/eq/high/{}"), api_v1_eq_high); api_server.on(UriBraces("/api/v1/eq/high/{}"), api_v1_eq_high);
api_server.on("/api/v1/system/restart", api_v1_system_restart); api_server.on("/api/v1/system/restart", api_v1_system_restart);
api_server.on("/api/v1/system/name", api_v1_system_friendlyname_get); api_server.on("/api/v1/system/name", api_v1_system_friendlyname_get);
api_server.on("/api/v1/system/name/change", api_v1_system_friendlyname_change); api_server.on("/api/v1/system/name/change", api_v1_system_friendlyname_change);
api_server.on("/api/v1/system/network_name", api_v1_system_networkname_get);
api_server.on("/api/v1/system/network_name/change", api_v1_system_networkname_change);
api_server.on(UriBraces("/api/v1/system/restore_state/{}"), api_v1_system_restore_state); api_server.on(UriBraces("/api/v1/system/restore_state/{}"), api_v1_system_restore_state);
api_server.on(UriBraces("/api/v1/system/restore_playing/{}"), api_v1_system_restore_playing); api_server.on(UriBraces("/api/v1/system/restore_playing/{}"), api_v1_system_restore_playing);
api_server.on(UriBraces("/api/v1/system/tell_address/{}"), api_v1_system_tell_address);
api_server.on(UriBraces("/api/v1/system/tell_address/lang/{}"), api_v1_system_tell_address_lang);
api_server.on("/api/v1/system/version", api_v1_system_version); api_server.on("/api/v1/system/version", api_v1_system_version);
api_server.on("/api/v1/system/wifi/change", api_v1_system_wifi_change); api_server.on("/api/v1/system/wifi/change", api_v1_system_wifi_change);
api_server.on("/api/v1/system/wifi/get_ssid", api_v1_system_wifi_getssid); api_server.on("/api/v1/system/wifi/get_ssid", api_v1_system_wifi_getssid);
api_server.on("/api/v1/system/wifi/get_ap_creds", api_v1_system_wifi_get_ap_creds);
Serial.println("[HTTP] [API] Starting API server (http) on port " + String(webport_api)); Serial.println("[HTTP] [API] Starting API server (http) on port " + String(webport_api));
api_server.begin(); api_server.begin();

View File

@@ -1,22 +1,27 @@
/* /*
This is free and unencumbered software released into the public domain. This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in
source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. all copyright interest in the software to the public domain. We make this dedication for the benefit of
the public at large and to the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/> For more information, please refer to <http://unlicense.org/>
*/ */
void setupWiFi() { void setupWiFi() {
if(WiFi.status() == WL_CONNECTED) { return; } // return if not connected if(WiFi.status() == WL_CONNECTED) { return; } // return if not connected
switch(currentWiFiMode) { switch(currentWiFiMode) {
case 0: { case 0: {
Serial.println("[WiFi] Connecting to WiFi..."); Serial.println(F("[WiFi] Connecting to WiFi..."));
String WiFiSSID = configuration.getString(PREFERENCES_KEY_WIFI_SSID); String WiFiSSID = configuration.getString(PREFERENCES_KEY_WIFI_SSID);
String WiFiPSK = configuration.getString(PREFERENCES_KEY_WIFI_PSK); String WiFiPSK = configuration.getString(PREFERENCES_KEY_WIFI_PSK);
if(WiFiPSK == String() || WiFiSSID == String()) { if(WiFiPSK == String() || WiFiSSID == String()) {
@@ -49,9 +54,23 @@ void setupWiFi() {
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);
WiFi.softAP(apSSID, apPSK); WiFi.softAP(apSSID, apPSK);
apON = true; apON = true;
Serial.printf("[WiFi] Opened AP successfully\n"); Serial.println(F("[WiFi] Opened AP successfully"));
} }
} }
break; break;
} }
} }
void setupMDNS() {
MDNS.end();
// start the mDNS responder
Serial.println(F("[SETUP] Starting mDNS responder"));
if (!MDNS.begin(configuration.getString(PREFERENCES_KEY_NETWORK_NAME, default_network_name))) { // get network name from EEPROM (default to default_network_name)
Serial.println(F("[SETUP] Error setting up mDNS responder!"));
indicate_system_error(); // halt system and blink readyPin
}
Serial.println(F("[SETUP] Started mDNS responder"));
// Add service to MDNS-SD
MDNS.addService("http", "tcp", 80);
}