14 Commits

3 changed files with 344 additions and 86 deletions

View File

@@ -61,18 +61,16 @@ Features, already implemented, or still in progress for the v1.0.0 release!
- [X] /api/v1/system/restart/
- [X] /api/v1/system/name
- [X] /api/v1/system/name/change
- [ ] /api/v1/system/save_state
- [ ] /api/v1/system/save_playing
- [ ] /api/v1/system/version
- [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 displayed correctly **sometimes**
@@ -84,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

@@ -22,7 +22,7 @@ For more information, please refer to <http://unlicense.org/>
// define all constants
const String version = "NetSpeaker v0.2.5-dev"; // version string used e.g. for access point ssid when wifi couldn't connect
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
@@ -39,11 +39,9 @@ const int backwardButtonPin = 27; // pin for ba
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 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 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)
@@ -65,24 +63,9 @@ const char PREFERENCES_KEY_MUTED[] = "muted"; // preference
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 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; // path to current playlist
int currentPlaylistPosition; // the current position in the current playlist
bool restore_old_state; // restore the old state of EEPROM?
// all other variables needed
Audio audio; // Audio object (for playing audio, decoding mp3, ...)
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)
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
@@ -90,6 +73,19 @@ bool apON = false;
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
@@ -121,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);
@@ -147,7 +142,7 @@ void loop() {
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
}
} else if (operation_mode == 1) { // things only need to be done if in standalone mode
if (audioPlaying) audio.loop(); // play audio if not paused
@@ -166,7 +161,7 @@ void loop() {
}
if (millis() - lastTimeEEPROMwritten >= EEPROM_WRITE_INTERVAL) {
writeLastState(); // save current state to eeprom
writeLastState(); // save current state to eeprom
lastTimeEEPROMwritten = millis();
}
}

View File

@@ -39,22 +39,258 @@ bool isStringConvertable2Int(String toIntStr) {
return true;
}
void configRoot() {
void api_root() {
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>";
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'>";
conf_server.send(200, "text/html", html);
}
html += "<p>Welcome to</p>";
html += "<h1>" + configuration.getString(PREFERENCES_KEY_FRIENDLY_NAME, "NetSpeaker") + "</h1>";
html += "<code>" + version + "</code>";
void apiRoot() {
Serial.println("[HTTP] [API] 200 - GET '/'");
html += "<hr style='margin: 3em 0em;'>";
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>";
// 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);
}
@@ -441,7 +677,7 @@ void api_v1_system_friendlyname_change() {
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());
@@ -449,6 +685,51 @@ void api_v1_system_friendlyname_change() {
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'");
@@ -491,56 +772,39 @@ void api_v1_system_wifi_change() {
api_server.send(200, "application/json", generate_api_json(true, "\"ssid\": \"" + ssid + "\", \"psk\": \"" + psk + "\""));
}
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);
Serial.println("[HTTP] [Config] Starting config server (http) on port " + String(webport_config));
conf_server.begin();
Serial.println("[HTTP] [Config] Started Config server");
}
}
void setupApiWeb() {
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_volume);
api_server.on(UriBraces("/api/v1/balance/{}"), HTTP_GET, api_v1_balance);
api_server.on("/api/v1/eq/get", HTTP_GET, api_v1_eq_get);
api_server.on(UriBraces("/api/v1/eq/reset"), HTTP_GET, api_v1_eq_reset);
api_server.on(UriBraces("/api/v1/eq/low/{}"), HTTP_GET, api_v1_eq_low);
api_server.on(UriBraces("/api/v1/eq/mid/{}"), HTTP_GET, api_v1_eq_mid);
api_server.on(UriBraces("/api/v1/eq/high/{}"), HTTP_GET, api_v1_eq_high);
api_server.on("/api/v1/system/restart", HTTP_GET, api_v1_system_restart);
api_server.on("/api/v1/system/name", HTTP_GET, api_v1_system_friendlyname_get);
api_server.on("/api/v1/system/name/change", HTTP_POST, api_v1_system_friendlyname_change);
api_server.on("/api/v1/system/wifi/change", HTTP_POST, api_v1_system_wifi_change);
api_server.on("/api/v1/system/wifi/get_ssid", HTTP_GET, api_v1_system_wifi_getssid);
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");
}