34 Commits

Author SHA1 Message Date
3a5b88a2d1 Updated version string 2023-12-16 22:16:42 +01:00
fb31d82fbf Added general system information accordion item on web UI 2023-12-16 22:15:20 +01:00
6668d56171 Added general playback information on web UI 2023-12-16 22:10:26 +01:00
9009e55d4f Created the template for accessing all the api functionalities via web ui 2023-12-16 21:28:45 +01:00
59fcb3335e Added Bootstrap to credits section in README.md 2023-12-16 19:28:00 +01:00
1ab1e2e1d0 Added bootstrap cdn to html 2023-12-16 19:26:10 +01:00
78b6eec8cb Adapted README.md to new web ui plans 2023-12-16 19:17:48 +01:00
e85a045fe6 Deleted all old fragments for the obsolete config server on port 8080 2023-12-16 19:16:34 +01:00
e744d92b0e Prepared for simple web interface implementation 2023-12-16 19:11:54 +01:00
ae08930bca Updated README.md to display the newest API functionality 2023-12-16 19:03:49 +01:00
77eef80da0 Updated version string 2023-12-16 19:01:29 +01:00
ce7db7b0fd Implemented the version getter endpoint 2023-12-16 18:56:34 +01:00
8f90d6ff05 Implemented the restore_playing endpoint 2023-12-16 18:56:23 +01:00
2cce64b11c Implemented the restore_state endpoint 2023-12-16 18:56:05 +01:00
0dbcd45a1e Added more configurability (if such a word exists :) 2023-12-15 22:55:07 +01:00
e23db963cb Changed Roadmap endpoint name for convenience 2023-12-15 22:49:56 +01:00
e922af6c2b Implemented friendly name endpoints (getter and setter) 2023-12-15 22:47:07 +01:00
ab05d31006 Added more enpoints to ROADMAP in README.md 2023-12-12 19:49:00 +01:00
3a26e00319 Reimplemted saving last state to eeprom; some restructuring; all 10s last state will be saved now 2023-12-12 19:46:59 +01:00
05a9ce083b Modularized the restoration process of last saved eeprom state 2023-12-12 18:56:19 +01:00
85b306a6b5 Implemented saving last state for standalone mode 2023-12-11 22:54:26 +01:00
0ecb3099d4 Changed wifi management from SD card file to EEPROM (via espressif's Preferences.h library) 2023-12-11 22:17:59 +01:00
edb2c80e5e Added boilerplate for Preferences.h usage (new config management) 2023-12-11 21:49:54 +01:00
27a472e85c Added a .../volume/get_max endpoint for getting the configured maximum volume 2023-12-11 19:08:13 +01:00
f03cb6fb54 Added another point to the Roadmap
Signed-off-by: BlueFox <bluefox@privacynerd.de>
2023-12-10 20:20:52 +00:00
cee502c3f1 Fixed wrong order in Roadmap listing in README.md 2023-12-10 21:18:28 +01:00
7bb65c355d Added forgotten API endpoints. Whoops. 2023-12-10 19:57:14 +00:00
f9f8f17803 Updated version string 2023-12-10 20:52:10 +01:00
58ddb4fb74 Added wifi change endpoint; deleted obsolte API_DOC.txt 2023-12-10 20:50:39 +01:00
c940a2afec Implemented .../wifi/get_ssid endpoint 2023-12-10 19:26:40 +01:00
0c42b53902 Changed restart endpoint from .../settings to .../system 2023-12-10 19:20:23 +01:00
566ee35935 Small change in credits (in audio.ino) for convenience 2023-12-10 19:14:41 +01:00
40f4d615d6 Updated README.md to contain the latest api endpoints 2023-12-10 19:07:38 +01:00
676e5fcadb Added simple equalizer endpoints (low, mid, high) 2023-12-10 19:05:04 +01:00
7 changed files with 703 additions and 224 deletions

View File

@@ -1,36 +0,0 @@
API Location Method Returns Redirects to Description
---------------------------------------------------------------------------------------------------------------------
/api/ API root
----------------
/api/v1/playback/ Playback functions
----------------
/api/v1/playback/toggle GET JSON object Toggle Playback
/api/v1/playback/play GET JSON object Start playback
/api/v1/playback/pause GET JSON object Pause playback
/api/v1/playback/next GET JSON object Play next audio
/api/v1/playback/previous GET JSON object Play previous audio
/api/v1/playback/info GET JSON object Get the title, artist, album, path, type, ... of the current played resource
/api/v1/playlist/
----------------
/api/v1/playlist/get GET JSON object Returns the whole current playlist
/api/v1/playlist/create POST JSON object Creates a playlist of the given directory
/api/v1/playlist/play POST JSON object Plays a playlist from the SD card
/api/v1/volume/
----------------
/api/v1/volume/up GET JSON object Higher the volume (max. 20)
/api/v1/volume/down GET JSON object Lower the volume (min. 0)
/api/v1/volume/mute GET JSON object Mute (does not affect volume)
/api/v1/volume/unmute GET JSON object Unmute (set volume to current volume)
/api/v1/volume/get GET JSON object Do nothing, just get the volume
/api/v1/volume/<0-20> GET JSON object Set volume to a specific value between 0 and 20
/api/v1/settings/
----------------
/api/v1/settings/restart GET JSON object Performs a reboot of the microcontroller (after waiting 5000ms)

View File

@@ -35,31 +35,44 @@ Features, already implemented, or still in progress for the v1.0.0 release!
- [X] /api/v1/playback/pause
- [X] /api/v1/playback/next
- [X] /api/v1/playback/previous
- [X] /api/v1/playback/&lt;index&gt;
- [X] /api/v1/playback/info
- [ ] /api/v1/playback/id3_image
- [X] /api/v1/playback/&lt;index&gt;
- [X] /api/v1/playlist/get
- [X] /api/v1/volume/get
- [X] /api/v1/volume/get_max
- [X] /api/v1/volume/up
- [X] /api/v1/volume/down
- [X] /api/v1/volume/mute
- [X] /api/v1/volume/&lt;0-20&gt;
- [X] /api/v1/balance/get
- [X] /api/v1/balance/&lt;0-32&gt;
- [X] /api/v1/settings/restart/
- [ ] /api/v1/system/name
- [ ] /api/v1/system/info
- [ ] /api/v1/system/name
- [ ] /api/v1/system/wifi/change
- [ ] /api/v1/system/wifi/get_ssid
- [X] /api/v1/eq/get
- [X] /api/v1/eq/reset
- [X] /api/v1/eq/low/get
- [X] /api/v1/eq/low/reset
- [X] /api/v1/eq/low/&lt;0-46&gt;
- [X] /api/v1/eq/mid/get
- [X] /api/v1/eq/mid/reset
- [X] /api/v1/eq/mid/&lt;0-46&gt;
- [X] /api/v1/eq/high/get
- [X] /api/v1/eq/high/reset
- [X] /api/v1/eq/high/&lt;0-46&gt;
- [X] /api/v1/system/restart/
- [X] /api/v1/system/name
- [X] /api/v1/system/name/change
- [X] /api/v1/system/restore_state/{on,off,get}
- [X] /api/v1/system/restore_playing/{on,off,get}
- [X] /api/v1/system/version
- [X] /api/v1/system/wifi/change
- [X] /api/v1/system/wifi/get_ssid
- [ ] /api/v1/files/get
- [ ] /api/v1/files/upload
- [X] Automatic WiFi connection
- [X] Access Point opened when no WiFi connection could be established
- [ ] Implement a configuration server running on port 8080
- [ ] Edit wifi connection
- [ ] Edit friendly name of the NetSpeaker
- [ ] Implement a simple web interface running on location /
- [X] Improve the volume endpoint handler (currently pretty undynamic - not anymore :)
- [ ] Add better encoding as umlauts are not shown **sometimes**
- [ ] Add better encoding as umlauts are not displayed correctly **sometimes**
## Credits & Acknowledgements
@@ -69,6 +82,7 @@ Thanks to...
- the makers of Arduino IDE
- schreibfaul1 (github) for creating the audio library used in this project
- 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:

View File

@@ -11,57 +11,82 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
For more information, please refer to <http://unlicense.org/>
*/
#include "WiFi.h" // https://github.com/espressif/arduino-esp32/tree/master/libraries/WiFi/src
#include "Audio.h" // https://github.com/schreibfaul1/ESP32-audioI2S
#include "SPI.h" // https://github.com/espressif/arduino-esp32/tree/master/libraries/SPI/src
#include "SD.h" // https://github.com/espressif/arduino-esp32/tree/master/libraries/SD/src
#include "FS.h" // https://github.com/espressif/arduino-esp32/tree/master/libraries/FS/src
#include <WiFi.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/WiFi/src
#include <Audio.h> // https://github.com/schreibfaul1/ESP32-audioI2S
#include <SPI.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/SPI/src
#include <SD.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/SD/src
#include <FS.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/FS/src
#include <WebServer.h> // https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer
#include <uri/UriBraces.h> // https://github.com/espressif/arduino-esp32/blob/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/
// define all constants
const String version = "NetSpeaker v0.2.2-dev"; // version string used e.g. for access point ssid when wifi couldn't connect
const int operation_mode = 0; // 0: interconnected (no buttons, but api and wifi); 1: standalone (no wifi; no api)
const int SD_CS = 5; // BOARD SPECIFIC
const int SPI_MISO = 19; // BOARD SPECIFIC
const int SPI_MOSI = 23; // BOARD SPECIFIC
const int SPI_SCK = 18; // BOARD SPECIFIC
const int I2S_DOUT = 2; // can be changed on need
const int I2S_BLCK = 4; // 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 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 forwardButtonPin = 14; // pin for forward button
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 retrySDMountTreshold = 1000; // defines how long to wait (in ms) to the next try of mounting the sd card
const int readyPin = 32; // for an LED that shows if everything started up correctly (SD card mounted, wifi connected, ...)
const int webport_config = 8080; // port for the configuration interface
const int webport_api = 80; // port for the api (can't be the same as 'webport_config', will throw an error)
const bool runConfServer = true; // variable if the config server should be setup and run
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 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 apPSK = "aA16161Aa"; // pre-shared-key of access point opened when not able to connect to wifi
const String version = "NetSpeaker v0.3.3-dev"; // version string used e.g. for access point ssid when wifi couldn't connect
const int operation_mode = 0; // 0: interconnected (no buttons, but api and wifi); 1: standalone (no wifi; no api)
const int SD_CS = 5; // BOARD SPECIFIC
const int SPI_MISO = 19; // BOARD SPECIFIC
const int SPI_MOSI = 23; // BOARD SPECIFIC
const int SPI_SCK = 18; // BOARD SPECIFIC
const int I2S_DOUT = 2; // can be changed on need
const int I2S_BLCK = 4; // 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 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 forwardButtonPin = 14; // pin for forward button
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 retrySDMountTreshold = 1000; // defines how long to wait (in ms) to the next try of mounting the sd card
const int readyPin = 32; // for an LED that shows if everything started up correctly (SD card mounted, wifi connected, ...)
const int webport_api = 80; // port for the api (can't be the same as 'webport_config', will throw an error)
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 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 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 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_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_FRIENDLY_NAME[] = "friendly_name"; // preferences key name for the NetSpeaker's friendly name
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_VOLUME[] = "volume"; // preferences key name for old volume (for state restore)
const char PREFERENCES_KEY_BALANCE[] = "balance"; // preferences key name for old balance (for state restore)
const char PREFERENCES_KEY_EQ_LOW[] = "eq_low"; // preferences key name for old low equalizer value (for state restore)
const char PREFERENCES_KEY_EQ_MID[] = "eq_mid"; // preferences key name for old mid equalizer value (for state restore)
const char PREFERENCES_KEY_EQ_HIGH[] = "eq_high"; // preferences key name for old high equalizer value (for state restore)
const char PREFERENCES_KEY_PLAYING[] = "playing"; // preferences key name for info if currently playing (for state restore)
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_INDEX[] = "pl-index"; // preferences key name for current playlist index (for state restore)
// create all needed variables
int currentVolume = 50; // variable where current volume (0...20) is stored
int maxVolume = 100; // defines the volume steps (max. 255)
int balanceLevel = 0; // left-right balance between -16 to 16 inclusive (both)
bool muted = false; // currently muted (does not affect currentVolume)
String currentSongPath; // path to currently playing song
bool audioPlaying = false; // play song or not?
Audio audio; // Audio object (for playing audio, decoding mp3, ...)
int currentPlaylistPosition = -1; // the current position in the current playlist
String currentPlaylist = "/audio/" + directoryPlaylistName + playlistExtension; // path to current playlist
int currentWiFiMode = 0; // DON'T CHANGE IF NOT KNOWING WHAT YOU'RE DOING; selector for wether an AP (1) should be set up or a wifi (0) should be connected
bool apON = false; // DON'T CHANGE IF NOT KNOWING WHAT YOU'RE DOING; is the access point opened?
WebServer conf_server(webport_config); // web server for configuration (e.g. WiFi password setting)
WebServer api_server(webport_api); // web server for api (e.g. pause/play)
// all other variables needed
Audio audio; // Audio object (for playing audio, decoding mp3, ...)
WebServer api_server(webport_api); // web server for api (e.g. pause/play) + simple web UI
String defaultPlaylistFolder = "/audio"; // default playlist folder
String defaultPlaylist = defaultPlaylistFolder + "/" + directoryPlaylistName + playlistExtension; // default path to playlist
int currentWiFiMode = 0; // DON'T CHANGE IF NOT KNOWING WHAT YOU'RE DOING; selector for wether an AP (1) should be set up or a wifi (0) should be connected
bool apON = false; // DON'T CHANGE IF NOT KNOWING WHAT YOU'RE DOING; is the access point opened?
Preferences configuration;
long lastTimeEEPROMwritten = 0; // used to limit the number of eeprom write cycles
// 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 balanceLevel = 0; // left-right balance between -16 to 16 inclusive (both)
int eqLow = 0; // equalizer low value between -40 and 6dB
int eqMid = 0; // equalizer mid value between -40 and 6dB
int eqHigh = 0; // equalizer high value between -40 and 6dB
bool muted = false; // currently muted (does not affect currentVolume)
bool audioPlaying = false; // play song or not?; don't play by standard
String currentSongPath; // path to currently playing song
String currentPlaylist = defaultPlaylist; // path to 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 playbackInfo {
String title; // e.g. Big Buck Bunny
String type; // e.g. local
@@ -76,7 +101,12 @@ struct playbackInfo {
struct playbackInfo pbInfo = { "", "", "", "", "", "", "", "", "" };
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
// restore process if configured so
restore_old_state = configuration.getBool(PREFERENCES_KEY_RESTORE_OLD_STATE, true);
if (restore_old_state) restoreLastState(); // standard of restore = true
// connect to sd card reader
Serial.println("[SETUP] Setting up SD-Card reader");
@@ -87,8 +117,7 @@ void setup() {
if (operation_mode == 0) { // things only need to be done if in interconnected mode
setupWiFi();
setupConfigWeb();
setupApiWeb();
setupWeb();
} else if (operation_mode == 1) { // things only need to be done if in standalone mode
// setup all pins (not SPI and other buses!)
pinMode(sdCardEjectPin, INPUT);
@@ -101,21 +130,21 @@ void setup() {
while (true) delay(100);
}
setupAudio(); // setup audio library
createPlaylistFromDirectory("/audio"); // create playlist from default dir ("/audio")
nextAudio(); // play first element of the playlist
digitalWrite(readyPin, HIGH); // show that startup is done and everything works fine
setupAudio(); // setup audio library
createPlaylistFromDirectory(defaultPlaylistFolder); // create playlist from default dir
nextAudio(); // play first element of the playlist
digitalWrite(readyPin, HIGH); // show that startup is done and everything works fine
}
void loop() {
if (operation_mode == 0) { // things only need to be done if in interconnected mode
setupWiFi(); // if connection was lost
if (audioPlaying) audio.loop(); // play audio if not paused
if (esp_timer_get_time() % 60 == 0) { // REALLY NEEDED; audio playing won't work else!
loopServer(); // listen on http ports all 60 ms
api_server.handleClient(); // listen on http ports all 60 ms
}
}
if (operation_mode == 1) { // things only need to be done if in standalone mode
} else if (operation_mode == 1) { // things only need to be done if in standalone mode
if (audioPlaying) audio.loop(); // play audio if not paused
loopBtnListeners();
@@ -123,10 +152,16 @@ void loop() {
// for SD card eject
if (analogRead(sdCardEjectPin) > 4000) { // bigger than 4000: so that it's not affected by a little touch of fingers (4095 is maximum; 0 minimum)
SD.end(); // delete SD object and sync its cache to flash
writeLastState(); // save playback infos (volume, balance, equalizer, ...)
digitalWrite(readyPin, LOW); // indicate that SD card can get removed (the ready-LED will go off)
Serial.printf("[SETUP] Unmounting SD-Card, waiting for %dms and trying to remount (every %d milliseconds)...\n", waitOnSDCardEject, retrySDMountTreshold);
delay(waitOnSDCardEject);
ESP.restart(); // reset everything and restart the program
}
}
if (millis() - lastTimeEEPROMwritten >= EEPROM_WRITE_INTERVAL) {
writeLastState(); // save current state to eeprom
lastTimeEEPROMwritten = millis();
}
}

View File

@@ -14,6 +14,10 @@ For more information, please refer to <http://unlicense.org/>
void setupAudio() {
audio.setPinout(I2S_BLCK, I2S_LRC, I2S_DOUT); // tell the audio library what output pins to use
audio.setVolumeSteps(maxVolume);
if (!muted) audio.setVolume(currentVolume);
else audio.setVolume(0); // mute if muted is true
audio.setBalance(balanceLevel);
audio.setTone(eqLow, eqMid, eqHigh);
Serial.printf("[SETUP] Set up audio card successfully (Pins: BLCK %d | LRC %d | DOUT %d)\n", I2S_BLCK, I2S_LRC, I2S_DOUT);
}
@@ -23,7 +27,7 @@ String nextAudio() {
currentPlaylistPosition++; // increment once
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
currentPlaylistPosition = -1; // the next time "nextAudio()" is called it will increment to 0
nextAudio(); // recursive call
return ""; // exit
@@ -34,24 +38,26 @@ String nextAudio() {
audio.connecttohost(url.c_str());
} else if (url.startsWith("/")) {
Serial.printf("[INFO] Starting audio from playlist (ID: %d, Path: %s)\n", currentPlaylistPosition, url.c_str());
audio.connecttoFS(SD, url.c_str()); // play the next song
audio.connecttoFS(SD, url.c_str()); // play the next song
} 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());
audio.connecttospeech(url.substring(12).c_str(), url.substring(9, 11).c_str());
}
pbInfo.resourcePath = url;
pbInfo.type = "local";
// clear everything (if a file hadn't got id3 tags so that there won't keep the old names)
// these attributes are set by the audio_id3data function
pbInfo.title = "";
pbInfo.album = "";
pbInfo.track = "";
pbInfo.type = "";
pbInfo.resourcePath = "";
pbInfo.artist = "";
pbInfo.track = "";
pbInfo.album = "";
pbInfo.year = "";
pbInfo.genre = "";
pbInfo.copyright = "";
pbInfo.resourcePath = url;
pbInfo.type = "local";
return url;
}
@@ -67,25 +73,26 @@ String previousAudio() {
audio.connecttohost(url.c_str());
} else if (url.startsWith("/")) {
Serial.printf("[INFO] Starting audio from playlist (ID: %d, Path: %s)\n", currentPlaylistPosition, url.c_str());
audio.connecttoFS(SD, url.c_str()); // play the next song
audio.connecttoFS(SD, url.c_str()); // play the next song
} 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());
audio.connecttospeech(url.substring(12).c_str(), url.substring(9, 11).c_str());
}
pbInfo.resourcePath = url;
pbInfo.type = "local";
// clear everything (if a file hadn't got id3 tags so that there won't keep the old names)
// these attributes are set by the audio_id3data function
pbInfo.title = "";
pbInfo.album = "";
pbInfo.track = "";
pbInfo.type = "";
pbInfo.resourcePath = "";
pbInfo.artist = "";
pbInfo.track = "";
pbInfo.album = "";
pbInfo.year = "";
pbInfo.genre = "";
pbInfo.copyright = "";
pbInfo.resourcePath = url;
pbInfo.type = "local";
return url;
}
@@ -115,8 +122,8 @@ void backwardButtonHandler() {
}
void setAudioVolume() {
int newCurrentVolume = analogRead(audioVolumePin) / (4095/maxVolume); // read voltage from audioVolumePin and divide by the calculated steps (4095 is the maximum input)
if (currentVolume != newCurrentVolume) { // just do it if the volume changed
int newCurrentVolume = analogRead(audioVolumePin) / (4095 / maxVolume); // read voltage from audioVolumePin and divide by the calculated steps (4095 is the maximum input)
if (currentVolume != newCurrentVolume) { // just do it if the volume changed
currentVolume = newCurrentVolume;
audio.setVolume(currentVolume); // set volume
Serial.printf("[INFO] Set volume to %d/%d!\n", currentVolume, maxVolume);
@@ -143,7 +150,7 @@ void audio_eof_speech(const char *info) {
void audio_info(const char *info) {
Serial.printf("[Audio.h] Info %s\n", info);
if(String(info).startsWith("End of webstream")) {
if (String(info).startsWith("End of webstream")) {
nextAudio();
}
}
@@ -188,10 +195,9 @@ void audio_lasthost(const char *info) { // stream URL played
}
// *********************************************************************************** //
// *************** Code heavily inspired by schreibfaul1's wiki entry ************** //
// ******** Following function heavily inspired by schreibfaul1's wiki entry ******** //
// * https://github.com/schreibfaul1/ESP32-audioI2S/wiki#what-audio-events-are-there * //
// *********************************************************************************** //
void audio_id3image(File &file, const size_t pos, const size_t size) { // cover image
Serial.printf("[Audio.h] ID3Image Found at position: %u | Length: %u\n", pos, size);
uint8_t buf[1024];

View File

@@ -20,51 +20,46 @@ int setupSD(int SD_CS, int SPI_MISO, int SPI_MOSI, int SPI_SCK) {
return ret; // returns 1 if everything is OK, 0 if an error occured (eg. wiring not correct)
}
String getWiFiSSID(String confPath) {
String ssid;
char new_char;
if (!SD.exists(confPath)) return ""; // if the config file doesn't exist, return nothing and exit
File confFile = SD.open(confPath);
ssid = (char)confFile.read();
while (ssid[ssid.length() - 1] != '\n' && confFile.available()) {
new_char = (char)confFile.read();
if (new_char == '\n') break;
ssid += new_char;
// write preferences via the Preferences.h library
void writeLastState() {
if (restore_old_state) { // 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
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_BALANCE, 0) != balanceLevel) configuration.putInt(PREFERENCES_KEY_BALANCE, balanceLevel);
if (configuration.getInt(PREFERENCES_KEY_EQ_LOW, 0) != eqLow) configuration.putInt(PREFERENCES_KEY_EQ_LOW, eqLow);
if (configuration.getInt(PREFERENCES_KEY_EQ_MID, 0) != eqMid) configuration.putInt(PREFERENCES_KEY_EQ_MID, eqMid);
if (configuration.getInt(PREFERENCES_KEY_EQ_HIGH, 0) != eqHigh) configuration.putInt(PREFERENCES_KEY_EQ_HIGH, eqHigh);
if (configuration.getBool(PREFERENCES_KEY_PLAYING, false) != audioPlaying) configuration.putBool(PREFERENCES_KEY_PLAYING, audioPlaying);
if (configuration.getBool(PREFERENCES_KEY_MUTED, false) != muted) configuration.putBool(PREFERENCES_KEY_MUTED, muted);
if (configuration.getString(PREFERENCES_KEY_PLAYLIST_PATH, defaultPlaylist) != currentPlaylist) configuration.putString(PREFERENCES_KEY_PLAYLIST_PATH, currentPlaylist);
if ((configuration.getInt(PREFERENCES_KEY_PLAYLIST_INDEX, 0) - 1) != currentPlaylistPosition) configuration.putInt(PREFERENCES_KEY_PLAYLIST_INDEX, currentPlaylistPosition);
Serial.printf("[RESTORE] Saved.\n");
}
return ssid;
}
String getWiFiSSID() {
return getWiFiSSID(wifiConfigPath);
}
String getWiFiPSK(String confPath) {
String line1;
String psk;
char new_char;
if (!SD.exists(confPath)) return ""; // if the config file doesn't exist, return nothing and exit
File confFile = SD.open(confPath);
// skip the first line
line1 = (char)confFile.read(); // read character...
while (line1[line1.length() - 1] != '\n' && confFile.available()) line1 += (char)confFile.read(); // ... as long as a newline hits
psk = (char)confFile.read();
while (confFile.available()) {
new_char = (char)confFile.read();
if (new_char == '\n') break;
psk += new_char;
}
return psk;
void restoreLastState() {
Serial.println("[RESTORE] Starting to restore the last saved state...");
currentVolume = configuration.getInt(PREFERENCES_KEY_VOLUME, maxVolume); // maximum volume as default
Serial.printf("[RESTORE] PREFERENCES_KEY_VOLUME = %i\n", currentVolume);
balanceLevel = configuration.getInt(PREFERENCES_KEY_BALANCE, 0); // 0 (middle) as default
Serial.printf("[RESTORE] PREFERENCES_KEY_BALANCE = %i\n", balanceLevel);
eqLow = configuration.getInt(PREFERENCES_KEY_EQ_LOW, 0); // 0dB (normal) as default
Serial.printf("[RESTORE] PREFERENCES_KEY_EQ_LOW = %i\n", eqLow);
eqMid = configuration.getInt(PREFERENCES_KEY_EQ_MID, 0); // 0dB (normal) as default
Serial.printf("[RESTORE] PREFERENCES_KEY_EQ_MID = %i\n", eqMid);
eqHigh = configuration.getInt(PREFERENCES_KEY_EQ_HIGH, 0); // 0dB (normal) as default
Serial.printf("[RESTORE] PREFERENCES_KEY_EQ_MID = %i\n", eqHigh);
if (configuration.getBool(PREFERENCES_KEY_RESTORE_PLAYING, false)) audioPlaying = configuration.getBool(PREFERENCES_KEY_PLAYING, false); // only restore playing if wanted; not playing as default
Serial.printf("[RESTORE] PREFERENCES_KEY_PLAYING = %s\n", audioPlaying ? "true" : "false");
muted = configuration.getBool(PREFERENCES_KEY_MUTED, false);
Serial.printf("[RESTORE] PREFERENCES_KEY_MUTED = %s\n", muted ? "true" : "false");
currentPlaylist = configuration.getString(PREFERENCES_KEY_PLAYLIST_PATH, defaultPlaylist); // default folder
Serial.printf("[RESTORE] PREFERENCES_KEY_PLAYLIST_PATH = %s\n", currentPlaylist.c_str());
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.println("[RESTORE] Restored.");
}
String getWiFiPSK() {
return getWiFiPSK(wifiConfigPath);
}
bool createPlaylistFromDirectory(String folderpath) { // create a .m3u playlist from all directory contents (the directory 'folderpath' is used)
File folder;

View File

@@ -32,24 +32,265 @@ String generate_api_json(bool success) { // if you just want the basic json fra
String generate_api_json(bool success, String content) { // when info is to be embedded
return generate_api_json(success, content, false);
}
void configRoot() {
Serial.println("[HTTP] [Config] 200 - GET '/'");
String html = "<html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>Configuration | ";
html += version;
html += "</title></head><body><div style='text-align:center;'><br><br><p>Configuration of your</p><h3>NetSpeaker</h3><br><hr><br><p>Work in progress!</p></div></body></html>";
conf_server.send(200, "text/html", html);
bool isStringConvertable2Int(String toIntStr) {
for (int i = 0; i < toIntStr.length(); i++) { // check if a non-digit character is in the given string
if (!isDigit((char)toIntStr[i])) return false;
}
return true;
}
void apiRoot() {
Serial.println("[HTTP] [API] 200 - GET '/'");
String html = "<html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>API | ";
html += version;
html += "</title></head><body><div style='text-align:center;'><br><br><p>API of your</p><br><h3>NetSpeaker</h3><br><hr><br><p>Work in progress!</p></div></body></html>";
void api_root() {
Serial.println("[HTTP] [Config] 200 - GET '/'");
String html = "<!doctype html>";
html += "<html><head>";
html += "<meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>";
html += "<title>Web UI | " + version + "</title>";
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'>";
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 += "<code>" + version + "</code>";
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-40) + "dB</td>";
html += " </tr><tr>";
html += " <td>Equalizer Mid</td>";
html += " <td class='text-end'>" + String(eqMid-40) + "dB</td>";
html += " </tr><tr>";
html += " <td>Equalizer High</td>";
html += " <td class='text-end'>" + String(eqHigh-40) + "dB</td>";
html += " </tr><tr>";
html += " <td>Balance (range -16 | +16)</td>";
html += " <td class='text-end'>" + String(balanceLevel-16) + "</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 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 accordion
html += "<div class='accordion text-start' id='accordion'>";
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 += " </button>";
html += " </h2>";
html += " <div id='accordion-collapse-playback' 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>";
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-volume' aria-expanded='true' aria-controls='accordion-collapse-volume'>";
html += " Volume";
html += " &nbsp;<span class='badge text-bg-info'>/api/v1/volume/</span>";
html += " </button>";
html += " </h2>";
html += " <div id='accordion-collapse-volume' 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>";
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>";
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 += " <strong>Work in progress</strong>";
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-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 += " <strong>Work in progress</strong>";
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-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>";
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>";
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>";
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 += " <strong>Work in progress</strong>";
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);
}
@@ -128,7 +369,7 @@ void api_v1_playback_byindex() {
api_server.send(200, "application/json", generate_api_json(true, content));
}
void api_v1_playback_volume() {
void api_v1_volume() {
String option = api_server.pathArg(0);
bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/volume/%s'\n", option);
@@ -145,6 +386,8 @@ void api_v1_playback_volume() {
muted = false;
} else if (option == "get") {
// just here that no 'success: false' is sent
} else if (option == "get_max") {
api_server.send(200, "application/json", generate_api_json(success, "\"volume_max\": " + String(maxVolume)));
} else {
String toIntStr;
int volumeLevel;
@@ -167,7 +410,7 @@ void api_v1_playback_volume() {
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it
}
void api_v1_playback_balance() {
void api_v1_balance() {
String option = api_server.pathArg(0);
bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/balance/%s'\n", option);
@@ -198,6 +441,115 @@ void api_v1_playback_balance() {
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it
}
void api_v1_eq_get() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/eq/get");
String content = "\"equalizer_low\": "; // prepare the http response
content += String(eqLow);
content += ", \"equalizer_mid\": "; // prepare the http response
content += String(eqMid);
content += ", \"equalizer_high\": "; // prepare the http response
content += String(eqHigh);
api_server.send(200, "application/json", generate_api_json(true, content)); // generate json and send it
}
void api_v1_eq_reset() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/eq/reset");
eqLow = 0;
eqMid = 0;
eqHigh = 0;
audio.setTone(0, 0, 0);
Serial.printf("[INFO] Set balance to %ddB | %ddB | %ddB!\n", eqLow, eqMid, eqHigh);
String content = "\"equalizer_low\": "; // prepare the http response
content += String(eqLow);
content += ", \"equalizer_mid\": "; // prepare the http response
content += String(eqMid);
content += ", \"equalizer_high\": "; // prepare the http response
content += String(eqHigh);
api_server.send(200, "application/json", generate_api_json(true, content)); // generate json and send it
}
void api_v1_eq_low() {
String option = api_server.pathArg(0);
bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/eq/low/%s'\n", option);
if (option == "get") {
// just here to make /get available
} else if (option == "reset") {
eqLow = 0;
} else if (isStringConvertable2Int(option)) {
int res = option.toInt();
if (res > 46) { // the maximum is 6dB and there will be 40 subtracted one line beneath
success = false;
} else {
eqLow = res - 40;
Serial.printf("[INFO] Set balance to %ddB | %ddB | %ddB!\n", eqLow, eqMid, eqHigh);
}
} else success = false;
audio.setTone(eqLow, eqMid, eqHigh); // set volume if not muted and in range
String content = "\"equalizer_low\": "; // prepare the http response
content += String(eqLow);
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it
}
void api_v1_eq_mid() {
String option = api_server.pathArg(0);
bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/eq/mid/%s'\n", option);
if (option == "get") {
// just here to make /get available
} else if (option == "reset") {
eqMid = 0;
} else if (isStringConvertable2Int(option)) {
int res = option.toInt();
if (res > 46) { // the maximum is 6dB and there will be 40 subtracted one line beneath
success = false;
} else {
eqMid = res - 40;
Serial.printf("[INFO] Set balance to %ddB | %ddB | %ddB!\n", eqLow, eqMid, eqHigh);
}
} else success = false;
audio.setTone(eqLow, eqMid, eqHigh); // set volume if not muted and in range
String content = "\"equalizer_mid\": "; // prepare the http response
content += String(eqMid);
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it
}
void api_v1_eq_high() {
String option = api_server.pathArg(0);
bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/eq/high/%s'\n", option);
if (option == "get") {
// just here to make /get available
} else if (option == "reset") {
eqHigh = 0;
} else if (isStringConvertable2Int(option)) {
int res = option.toInt();
if (res > 46) { // the maximum is 6dB and there will be 40 subtracted one line beneath
success = false;
} else {
eqHigh = res - 40;
Serial.printf("[INFO] Set balance to %dB | %ddB | %ddB!\n", eqLow, eqMid, eqHigh);
}
} else success = false;
audio.setTone(eqLow, eqMid, eqHigh); // set volume if not muted and in range
String content = "\"equalizer_high\": "; // prepare the http response
content += String(eqHigh);
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it
}
void api_v1_playback_info() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/info'");
String content = "\"resource_path\": \"" + pbInfo.resourcePath + "\", "; // resource's path
@@ -287,55 +639,172 @@ void api_v1_playlist_play() {
}
}
void api_v1_settings_restart() {
void api_v1_system_restart() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/system/restart'");
SD.end(); // delete SD object and sync its cache to flash
WiFi.disconnect(); // disconnect wifi
Serial.printf("[INFO] Restarting after 5000ms.\n", waitOnSDCardEject, retrySDMountTreshold);
delay(5000);
api_server.send(200, "application/json", "{\"restart\": true, \"wait_time\": 5000}");
delay(5000);
ESP.restart(); // reset everything and restart the program
}
void setupConfigWeb() {
if (runConfServer) {
conf_server.onNotFound([]() {
Serial.println("[HTTP] [Config] 404: Not Found");
conf_server.send(404, "text/html", "<html><head><title>Not Found</title></head><body><h1>Not Found</h1><p>This resource does not exist on this server.</p></body></html>");
});
conf_server.on("/", configRoot);
void api_v1_system_friendlyname_get() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/system/name'");
Serial.println("[HTTP] [Config] Starting config server (http) on port " + String(webport_config));
conf_server.begin();
Serial.println("[HTTP] [Config] Started Config server");
}
api_server.send(200, "application/json", generate_api_json(true, "\"friendly_name\": \"" + configuration.getString(PREFERENCES_KEY_FRIENDLY_NAME) + "\""));
}
void setupApiWeb() {
void api_v1_system_friendlyname_change() {
Serial.println("[HTTP] [API] 200 - POST '/api/v1/system/name/change'");
String currentFriendlyName = configuration.getString(PREFERENCES_KEY_FRIENDLY_NAME, "");
String newFriendlyName;
bool arg_given = false;
// get the right post param (by key)
for (int i = 0; i < api_server.args(); i++) {
if (api_server.argName(i) == "friendly_name") {
arg_given = true;
newFriendlyName = api_server.arg(i);
break;
}
}
// define what to do if nothing really changed (or no argument was given)
if (newFriendlyName == currentFriendlyName || !arg_given) {
String content;
content = "\"friendly_name\": \"" + currentFriendlyName + "\"";
api_server.send(200, "application/json", generate_api_json(false, content)); // return with no success ("false")
return;
}
configuration.putString(PREFERENCES_KEY_FRIENDLY_NAME, newFriendlyName);
Serial.printf("[INFO] Changed friendly name from \"%s\" to \"%s\".\n", currentFriendlyName.c_str(), newFriendlyName.c_str());
api_server.send(200, "application/json", generate_api_json(true, "\"friendly_name\": \"" + newFriendlyName + "\""));
}
void api_v1_system_restore_state() {
String option = api_server.pathArg(0);
bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/system/restore_state/%s'\n", option);
if (option == "get") {
// just here that calling .../get wont respond 404
} else if (option == "on") {
configuration.putBool(PREFERENCES_KEY_RESTORE_OLD_STATE, true);
} else if (option == "off") {
configuration.putBool(PREFERENCES_KEY_RESTORE_OLD_STATE, false);
} else {
success = false;
}
String content = "\"restore_state\": "; // prepare the http response
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
}
void api_v1_system_restore_playing() {
String option = api_server.pathArg(0);
bool success = true;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/system/restore_playing/%s'\n", option);
if (option == "get") {
// just here that calling .../get wont respond 404
} else if (option == "on") {
configuration.putBool(PREFERENCES_KEY_RESTORE_PLAYING, true);
} else if (option == "off") {
configuration.putBool(PREFERENCES_KEY_RESTORE_PLAYING, false);
} else {
success = false;
}
String content = "\"restore_playing\": "; // prepare the http response
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
}
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_wifi_getssid() {
Serial.println("[HTTP] [API] 200 - GET '/api/v1/system/wifi/get_ssid'");
api_server.send(200, "application/json", generate_api_json(true, "\"wifi_ssid\": \"" + configuration.getString(PREFERENCES_KEY_WIFI_SSID) + "\""));
}
void api_v1_system_wifi_change() {
Serial.println("[HTTP] [API] 200 - POST '/api/v1/system/wifi/change'");
// get the right POST params
String ssid = "";
String psk = "";
for (int i = 0; i < api_server.args(); i++) {
if (api_server.argName(i) == "ssid") {
ssid = api_server.arg(i);
break;
}
}
for (int i = 0; i < api_server.args(); i++) {
if (api_server.argName(i) == "psk") {
psk = api_server.arg(i);
break;
}
}
if (ssid != "" && psk != "") { // both ssid and psk are given
configuration.putString(PREFERENCES_KEY_WIFI_SSID, ssid);
configuration.putString(PREFERENCES_KEY_WIFI_PSK, psk);
} else if (ssid != "" && psk == "") { // just the ssid is given
configuration.putString(PREFERENCES_KEY_WIFI_SSID, ssid);
} else if (ssid == "" && psk != "") { // just the psk is given
configuration.putString(PREFERENCES_KEY_WIFI_PSK, psk);
} else { // none of ssid or psk is given
api_server.send(200, "application/json", generate_api_json(false));
return;
}
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 + "\""));
}
void setupWeb() {
api_server.on("/", api_root);
api_server.onNotFound([]() {
Serial.println("[HTTP] [API] 404: Not Found");
api_server.send(404, "application/json", "{\"code\": 404, \"message\": \"Resource not found.\"}");
});
api_server.on("/", apiRoot);
api_server.on("/api/v1/playback/toggle", HTTP_GET, api_v1_playback_toggle);
api_server.on("/api/v1/playback/play", HTTP_GET, api_v1_playback_play);
api_server.on("/api/v1/playback/pause", HTTP_GET, api_v1_playback_pause);
api_server.on("/api/v1/playback/next", HTTP_GET, api_v1_playback_next);
api_server.on("/api/v1/playback/previous", HTTP_GET, api_v1_playback_previous);
api_server.on("/api/v1/playback/info", HTTP_GET, api_v1_playback_info);
api_server.on(UriBraces("/api/v1/playback/{}"), HTTP_GET, api_v1_playback_byindex);
api_server.on("/api/v1/playlist/get", HTTP_GET, api_v1_playlist_get);
api_server.on("/api/v1/playlist/create", HTTP_POST, api_v1_playlist_create);
api_server.on("/api/v1/playlist/play", HTTP_POST, api_v1_playlist_play);
api_server.on(UriBraces("/api/v1/volume/{}"), HTTP_GET, api_v1_playback_volume);
api_server.on(UriBraces("/api/v1/balance/{}"), HTTP_GET, api_v1_playback_balance);
api_server.on("/api/v1/settings/restart", HTTP_GET, api_v1_settings_restart);
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/pause", api_v1_playback_pause);
api_server.on("/api/v1/playback/next", api_v1_playback_next);
api_server.on("/api/v1/playback/previous", api_v1_playback_previous);
api_server.on("/api/v1/playback/info", api_v1_playback_info);
api_server.on(UriBraces("/api/v1/playback/{}"), api_v1_playback_byindex);
api_server.on("/api/v1/playlist/get", api_v1_playlist_get);
api_server.on("/api/v1/playlist/create", api_v1_playlist_create);
api_server.on("/api/v1/playlist/play", api_v1_playlist_play);
api_server.on(UriBraces("/api/v1/volume/{}"), api_v1_volume);
api_server.on(UriBraces("/api/v1/balance/{}"), api_v1_balance);
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(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/high/{}"), api_v1_eq_high);
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/change", api_v1_system_friendlyname_change);
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("/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/get_ssid", api_v1_system_wifi_getssid);
Serial.println("[HTTP] [API] Starting API server (http) on port " + String(webport_api));
api_server.begin();
Serial.println("[HTTP] [API] Started config server");
}
void loopServer() {
api_server.handleClient();
if (runConfServer) conf_server.handleClient();
Serial.println("[HTTP] [API] Started API server");
}

View File

@@ -17,26 +17,22 @@ void setupWiFi() {
switch(currentWiFiMode) {
case 0: {
Serial.println("[WiFi] Connecting to WiFi...");
String WiFiSSIDstr = getWiFiSSID();
String WiFiPSKstr = getWiFiPSK();
if(WiFiPSKstr == String() || WiFiSSIDstr == String()) {
String WiFiSSID = configuration.getString(PREFERENCES_KEY_WIFI_SSID);
String WiFiPSK = configuration.getString(PREFERENCES_KEY_WIFI_PSK);
if(WiFiPSK == String() || WiFiSSID == String()) {
currentWiFiMode = 1;
Serial.printf("[WiFi] No WiFi configured\n");
setupWiFi(); // recursive call to reach the 'case 1' statement
break;
}
char WiFiSSID[33];
char WiFiPSK[64];
// copy to char arrays as the WiFi library needs these (maximum length 32/63 chars)
for(int i = 0; i<33; i++) { if(WiFiSSIDstr.length() >= i) {WiFiSSID[i] = WiFiSSIDstr[i];} else break; }
for(int i = 0; i<64; i++) { if(WiFiPSKstr.length() >= i) {WiFiPSK[i] = WiFiPSKstr[i];} else break; }
//Serial.printf("[WiFi] Credentials: SSID '%s' | PSK '%s'\n", WiFiSSID.c_str(), WiFiPSK.c_str()); // kept here for debugging wifi problems
WiFi.disconnect();
WiFi.mode(WIFI_AP_STA);
WiFi.begin(WiFiSSID, WiFiPSK);
WiFi.begin(WiFiSSID.c_str(), WiFiPSK.c_str());
// wait for 10 seconds for wifi to connect
int start_timer = millis(); while(WiFi.status() != WL_CONNECTED) { if((millis()-start_timer) > 20000) break; }
int start_timer = millis(); while(WiFi.status() != WL_CONNECTED) { if((millis()-start_timer) > 20000) break; delay(10); }
if(WiFi.status() != WL_CONNECTED) {
currentWiFiMode = 1;
Serial.printf("[WiFi] Unable to connect to %s\n", WiFiSSID);