34 Commits

Author SHA1 Message Date
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
97205ac43f Added balance API functionality 2023-12-10 18:19:57 +01:00
d67dea275e Improved volume control 2023-12-10 17:29:15 +01:00
06da06c519 Updated the Roadmap 2023-12-10 16:03:00 +00:00
763254b603 Added .../playback/<index> endpoint 2023-12-10 17:01:54 +01:00
d06f2da73d Added multiple stream types to play from playlist 2023-12-10 16:45:17 +01:00
cd62f273c1 Added file handler for id3 data 2023-12-10 16:16:41 +01:00
90bd0f68c3 Updated Credits section 2023-12-10 14:38:38 +00:00
af662aa327 Improved Mirrors section in README.md 2023-12-10 14:37:25 +00:00
f53ef3c5dc Update README.md 2023-12-10 14:34:11 +00:00
425e579211 Improved the Raodmap in README.md 2023-12-10 12:39:37 +00:00
0fe0668659 Fixed typo in README.md 2023-12-10 12:05:31 +00:00
3b12974ce2 Update README.md 2023-12-10 12:05:16 +00:00
5ee15af74d Added Wiki hint in README.md 2023-12-09 22:53:09 +00:00
f2988c2eb5 Removed sending unnecessary data on the .../playback/next and .../previous endpoint 2023-12-09 23:36:00 +01:00
87042e72e0 Changed example structure 2023-12-09 22:25:43 +01:00
98a91e4974 Actually added now. Whoops :) 2023-12-09 21:54:05 +01:00
e3c5d4b6c9 Added more images in the diy project example 2023-12-09 21:53:32 +01:00
8904840428 Don't unmute when volume is set 2023-12-09 21:39:34 +01:00
c4362f998c Improved mute/unmute api endpoint functionality 2023-12-09 16:15:11 +01:00
ddb9a1e49e Updated Mirror section in README.md 2023-12-09 09:52:01 +00:00
61e167d036 Update README.md 2023-12-09 09:24:31 +00:00
da55732450 Fixed wrong rule in .gitignore 2023-12-09 08:52:43 +00:00
d68d773090 Added some badges to README.md for better readability 2023-12-09 08:34:22 +00:00
2d41b65ca8 Added more features to Roadmap 2023-12-09 08:24:14 +00:00
b1a677ab35 Added playlist play and create methods to API_DOC.txt 2023-12-08 22:05:28 +00:00
3b6811073a Added playlist play endpoint (POST) 2023-12-08 23:01:59 +01:00
72e60c34e0 Added playlist create endpoint 2023-12-08 22:54:17 +01:00
17 changed files with 462 additions and 137 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
# ignore IDE specific files and folders # ignore IDE specific files and folders
/.theia/ .theia/

View File

@@ -1,33 +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/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 Set volume to 0
/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

@@ -1,5 +1,11 @@
# NetSpeaker # NetSpeaker
[![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/)
[![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)]()
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.
There are two operating modes, to adapt to the specific use case - default mode is 0. The mode can be set at compile time There are two operating modes, to adapt to the specific use case - default mode is 0. The mode can be set at compile time
@@ -8,6 +14,13 @@ via the constant operation_mode. Valid choices are:
- 0: interconnected (HTTP API and WiFi, no buttons); useful for bigger projects - 0: interconnected (HTTP API and WiFi, no buttons); useful for bigger projects
- 1: standalone (Buttons, no WiFi and HTTP API); useful if you want to build a small player - 1: standalone (Buttons, no WiFi and HTTP API); useful if you want to build a small player
Any questions? Hopefully a look into the **Wiki** will help.
## Audio compatibility
For audio compatibilty, please have a look at [schreibfaul1's git repository's wiki](https://github.com/schreibfaul1/ESP32-audioI2S/wiki#which-external-dacs-can-be-used), where he goes into that in detail.
## Roadmap ## Roadmap
@@ -22,25 +35,41 @@ Features, already implemented, or still in progress for the v1.0.0 release!
- [X] /api/v1/playback/pause - [X] /api/v1/playback/pause
- [X] /api/v1/playback/next - [X] /api/v1/playback/next
- [X] /api/v1/playback/previous - [X] /api/v1/playback/previous
- [X] /api/v1/playback/&lt;index&gt;
- [X] /api/v1/playback/info - [X] /api/v1/playback/info
- [ ] /api/v1/playback/id3_image - [ ] /api/v1/playback/id3_image
- [X] /api/v1/playlist/get - [X] /api/v1/playlist/get
- [ ] /api/v1/playlist/append
- [ ] /api/v1/playlist/remove
- [X] /api/v1/volume/get - [X] /api/v1/volume/get
- [X] /api/v1/volume/up - [X] /api/v1/volume/up
- [X] /api/v1/volume/down - [X] /api/v1/volume/down
- [X] /api/v1/volume/mute - [X] /api/v1/volume/mute
- [X] /api/v1/volume/<0-20> - [X] /api/v1/volume/&lt;0-20&gt;
- [X] /api/v1/settings/restart/ - [X] /api/v1/balance/&lt;0-32&gt;
- [X] /api/v1/balance/get
- [X] /api/v1/eq/get
- [X] /api/v1/eq/low/get
- [X] /api/v1/eq/low/&lt;0-46&gt;
- [X] /api/v1/eq/mid/get
- [X] /api/v1/eq/mid/&lt;0-46&gt;
- [X] /api/v1/eq/high/get
- [X] /api/v1/eq/high/&lt;0-46&gt;
- [X] /api/v1/system/restart/
- [ ] /api/v1/system/name
- [ ] /api/v1/system/info
- [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] 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
- [ ] Implement a configuration server running on port 8080 - [ ] Implement a configuration server running on port 8080
- [ ] Edit wifi connection - [ ] Edit wifi connection
- [ ] Edit friendly name of the NetSpeaker - [ ] Edit friendly name of the NetSpeaker
- [X] Improve the volume endpoint handler (currently pretty undynamic - not anymore :)
- [ ] Add better encoding as umlauts are not shown **sometimes**
## Credits ## Credits & Acknowledgements
Thanks to... Thanks to...
@@ -48,6 +77,11 @@ Thanks to...
- 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
External librarys used:
- ESP32-audioI2S library (https://github.com/schreibfaul1/ESP32-audioI2S as of 2023/12)
- Arduino ESP32-specific librarys (https://github.com/espressif/arduino-esp32/ as of 2023/12)
## Contributing ## Contributing
@@ -55,11 +89,10 @@ Pull requests are welcome. For major changes, please open an issue first
to discuss what you would like to change. to discuss what you would like to change.
Please also make sure to test things carefully before contributing them. Please also make sure to test things carefully before contributing them.
## Mirrors
## External librarys used: - [Privacynerd's Gitea (main)](https://git.privacynerd.de/NetSpeaker/NetSpeaker/)
- [Codeberg](https://codeberg.org/NetSpeaker/NetSpeaker)
- ESP32-audioI2S library (https://github.com/schreibfaul1/ESP32-audioI2S as of 2023/12)
- Arduino ESP32-specific librarys (https://github.com/espressif/arduino-esp32/ as of 2023/12)
## License ## License

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -21,7 +21,7 @@ For more information, please refer to <http://unlicense.org/>
// define all constants // define all constants
const String version = "NetSpeaker v0.2.0-dev"; // version string used e.g. for access point ssid when wifi couldn't connect const String version = "NetSpeaker v0.2.5-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 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 SD_CS = 5; // BOARD SPECIFIC
const int SPI_MISO = 19; // BOARD SPECIFIC const int SPI_MISO = 19; // BOARD SPECIFIC
@@ -48,9 +48,15 @@ const String apSSID = version; // ssid of access point open
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
// create all needed variables // create all needed variables
int currentVolume = 20; // variable where current volume (0...20) is stored 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)
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)
String currentSongPath; // path to currently playing song String currentSongPath; // path to currently playing song
bool audioPlaying = true; // play song or not? bool audioPlaying = false; // play song or not?
Audio audio; // Audio object (for playing audio, decoding mp3, ...) Audio audio; // Audio object (for playing audio, decoding mp3, ...)
int currentPlaylistPosition = -1; // the current position in the current playlist int currentPlaylistPosition = -1; // the current position in the current playlist
String currentPlaylist = "/audio/" + directoryPlaylistName + playlistExtension; // path to current playlist String currentPlaylist = "/audio/" + directoryPlaylistName + playlistExtension; // path to current playlist
@@ -65,12 +71,12 @@ struct playbackInfo {
String resourcePath; // e.g. /audio/XXX.mp3 String resourcePath; // e.g. /audio/XXX.mp3
String artist; // e.g. Blender Foundation String artist; // e.g. Blender Foundation
String track; // e.g. 01/02 or 01 String track; // e.g. 01/02 or 01
String album; String album; // album
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)
}; };
struct playbackInfo pbInfo = { "", "", "", "", "", "" }; struct playbackInfo pbInfo = { "", "", "", "", "", "", "", "", "" };
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

View File

@@ -13,24 +13,33 @@ For more information, please refer to <http://unlicense.org/>
void setupAudio() { void setupAudio() {
audio.setPinout(I2S_BLCK, I2S_LRC, I2S_DOUT); // tell the audio library what output pins to use audio.setPinout(I2S_BLCK, I2S_LRC, I2S_DOUT); // tell the audio library what output pins to use
audio.setVolumeSteps(20); audio.setVolumeSteps(maxVolume);
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);
} }
String nextAudio() { String nextAudio() {
String songPath; String url;
currentPlaylistPosition++; // increment once currentPlaylistPosition++; // increment once
songPath = getSongFromPlaylist(currentPlaylist, currentPlaylistPosition); url = getURLFromPlaylist(currentPlaylist, currentPlaylistPosition);
if (songPath == "") { // 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 currentPlaylistPosition = -1; // the next time "nextAudio()" is called it will increment to 0
nextAudio(); // recursive call nextAudio(); // recursive call
return ""; // exit return ""; // exit
} }
Serial.printf("[INFO] Starting next audio from playlist (ID: %d, Path: %s)\n", currentPlaylistPosition, songPath.c_str());
audio.connecttoFS(SD, songPath.c_str()); // play the next song
pbInfo.resourcePath = songPath; if (url.startsWith("http")) {
Serial.printf("[INFO] Starting stream from playlist (ID: %d, URL: %s)\n", currentPlaylistPosition, url.c_str());
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
} 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"; pbInfo.type = "local";
// clear everything (if a file hadn't got id3 tags so that there won't keep the old names) // clear everything (if a file hadn't got id3 tags so that there won't keep the old names)
@@ -43,20 +52,28 @@ String nextAudio() {
pbInfo.genre = ""; pbInfo.genre = "";
pbInfo.copyright = ""; pbInfo.copyright = "";
return songPath; return url;
} }
String previousAudio() { String previousAudio() {
String songPath; String url;
currentPlaylistPosition--; currentPlaylistPosition--;
if (currentPlaylistPosition < 0) currentPlaylistPosition = 0; if (currentPlaylistPosition < 0) currentPlaylistPosition = 0;
songPath = getSongFromPlaylist(currentPlaylist, currentPlaylistPosition); url = getURLFromPlaylist(currentPlaylist, currentPlaylistPosition);
Serial.printf("[INFO] Starting previous audio from playlist (ID: %d, Path: %s)\n", currentPlaylistPosition, songPath.c_str()); if (url.startsWith("http")) {
audio.connecttoFS(SD, songPath.c_str()); // play the previous song Serial.printf("[INFO] Starting stream from playlist (ID: %d, URL: %s)\n", currentPlaylistPosition, url.c_str());
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
} 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 = songPath; pbInfo.resourcePath = url;
pbInfo.type = "local"; pbInfo.type = "local";
// clear everything (if a file hadn't got id3 tags so that there won't keep the old names) // clear everything (if a file hadn't got id3 tags so that there won't keep the old names)
@@ -69,7 +86,7 @@ String previousAudio() {
pbInfo.genre = ""; pbInfo.genre = "";
pbInfo.copyright = ""; pbInfo.copyright = "";
return songPath; return url;
} }
void switchPlaybackIfButtonClicked() { void switchPlaybackIfButtonClicked() {
@@ -98,11 +115,11 @@ void backwardButtonHandler() {
} }
void setAudioVolume() { void setAudioVolume() {
int newCurrentVolume = analogRead(audioVolumePin) / 204.75; // read voltage from audioVolumePin and divide by 204.75 (min. volume is 0, max. 20; 20 fits 204.75 times into 4095 (maximum input)) 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 if (currentVolume != newCurrentVolume) { // just do it if the volume changed
currentVolume = newCurrentVolume; currentVolume = newCurrentVolume;
audio.setVolume(currentVolume); // set volume audio.setVolume(currentVolume); // set volume
Serial.printf("[INFO] Set volume to %d/20!\n", currentVolume); Serial.printf("[INFO] Set volume to %d/%d!\n", currentVolume, maxVolume);
} }
} }
@@ -121,9 +138,14 @@ 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);
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")) {
nextAudio();
}
} }
void audio_id3data(const char *info) { // id3 metadata void audio_id3data(const char *info) { // id3 metadata
Serial.printf("[Audio.h] ID3Data %s\n", info); Serial.printf("[Audio.h] ID3Data %s\n", info);
@@ -164,3 +186,46 @@ void audio_icyurl(const char *info) { // Name of radio station
void audio_lasthost(const char *info) { // stream URL played void audio_lasthost(const char *info) { // stream URL played
Serial.printf("[Audio.h] Lasthost %s\n", info); Serial.printf("[Audio.h] Lasthost %s\n", info);
} }
// *********************************************************************************** //
// ******** 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];
file.seek(pos + 1); // skip 1 byte encoding
char mimeType[255]; // mime-type (null terminated)
for (uint8_t i = 0u; i < 255; i++) {
mimeType[i] = file.read();
if (uint8_t(mimeType[i]) == 0) break;
}
Serial.printf("[Audio.h] ID3Image MimeType: %s\n", mimeType);
uint8_t imageType = file.read(); // image type (1 Byte)
Serial.printf("[Audio.h] ID3Image ImageType: %d\n", imageType);
for (uint8_t i = 0u; i < 255; i++) { // description (null terminated)
buf[i] = file.read();
if (uint8_t(buf[i]) == 0) break;
}
// raw image data
String imgPath = "/.current";
if (String(mimeType) == "image/jpeg") imgPath += ".jpg";
else if (String(mimeType) == "image/png") imgPath += ".png";
else if (String(mimeType) == "image/svg") imgPath += ".svg";
else imgPath += ".img";
File coverFile = SD.open(imgPath, FILE_WRITE);
size_t len = size;
while (len) {
uint16_t bytesRead = file.read(buf, sizeof(buf));
if (len >= bytesRead) len -= bytesRead;
else {
bytesRead = len;
len = 0;
}
coverFile.write(buf, bytesRead);
}
// write changes to SD card
coverFile.flush();
coverFile.close();
Serial.printf("[Audio.h] ID3Image Cover file written to '%s'\n", imgPath.c_str());
}

View File

@@ -20,6 +20,21 @@ 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) return ret; // returns 1 if everything is OK, 0 if an error occured (eg. wiring not correct)
} }
bool writeWiFiSecrets(String confPath, String ssid, String psk) {
File wifiSecrets = SD.open(confPath, FILE_WRITE);
wifiSecrets.printf("%s\n", ssid.c_str());
wifiSecrets.printf("%s", psk.c_str());
wifiSecrets.flush();
wifiSecrets.close();
return true;
}
bool writeWiFiSecrets(String ssid, String psk) {
return writeWiFiSecrets(wifiConfigPath, ssid, psk);
}
String getWiFiSSID(String confPath) { String getWiFiSSID(String confPath) {
String ssid; String ssid;
char new_char; char new_char;
@@ -28,7 +43,7 @@ String getWiFiSSID(String confPath) {
File confFile = SD.open(confPath); File confFile = SD.open(confPath);
ssid = (char)confFile.read(); ssid = (char)confFile.read();
while (ssid[ssid.length() - 1] != '\n' && confFile.available()) { while (ssid[ssid.length() - 1] != '\n' || ssid[ssid.length() - 1] != '\r' && confFile.available()) {
new_char = (char)confFile.read(); new_char = (char)confFile.read();
if (new_char == '\n') break; if (new_char == '\n') break;
ssid += new_char; ssid += new_char;
@@ -55,7 +70,7 @@ String getWiFiPSK(String confPath) {
psk = (char)confFile.read(); psk = (char)confFile.read();
while (confFile.available()) { while (confFile.available()) {
new_char = (char)confFile.read(); new_char = (char)confFile.read();
if (new_char == '\n') break; if (new_char == '\n' || new_char == '\r') break;
psk += new_char; psk += new_char;
} }
@@ -95,7 +110,7 @@ bool createPlaylistFromDirectory(String folderpath) { // create a .m3u playlist
return true; return true;
} }
String getSongFromPlaylist(String path, int position) { String getURLFromPlaylist(String path, int position) {
String song; String song;
File playlist; File playlist;
char currentChar; char currentChar;

View File

@@ -32,7 +32,12 @@ 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 String generate_api_json(bool success, String content) { // when info is to be embedded
return generate_api_json(success, content, false); return generate_api_json(success, content, false);
} }
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 configRoot() { void configRoot() {
Serial.println("[HTTP] [Config] 200 - GET '/'"); Serial.println("[HTTP] [Config] 200 - GET '/'");
@@ -84,29 +89,51 @@ void api_v1_playback_pause() {
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 - GET '/api/v1/playback/next'");
String songPath = nextAudio(); nextAudio();
String content = "\"resource_path\": \""; String content = "\"resource_playlist_index\": ";
content += songPath;
content += "\", \"resource_playlist_index\": ";
content += String(currentPlaylistPosition); content += String(currentPlaylistPosition);
api_server.send(200, "application/json", generate_api_json(true, content)); api_server.send(200, "application/json", generate_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 - GET '/api/v1/playback/previous'");
String songPath = previousAudio(); previousAudio();
String content = "\"resource_path\": \""; String content = "\"resource_playlist_index\": ";
content += songPath;
content += "\", \"resource_playlist_index\": ";
content += String(currentPlaylistPosition); content += String(currentPlaylistPosition);
api_server.send(200, "application/json", generate_api_json(true, content)); api_server.send(200, "application/json", generate_api_json(true, content));
} }
void api_v1_playback_volume() { void api_v1_playback_byindex() {
String option = api_server.pathArg(0);
String toIntStr;
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/playback/%s'\n", option);
// convert string to int
for (int i = 0; i < option.length(); i++) {
if (isDigit((char)option[i])) toIntStr += option[i];
else {
api_server.send(200, "application/json", generate_api_json(false));
return;
}
}
int index = toIntStr.toInt();
if (getURLFromPlaylist(currentPlaylist, index) != "") { // if this index exists
currentPlaylistPosition = index - 1;
nextAudio(); // the clean way of playing the next resource
} else {
api_server.send(200, "application/json", generate_api_json(false));
}
String content = "\"resource_playlist_index\": ";
content += String(currentPlaylistPosition);
api_server.send(200, "application/json", generate_api_json(true, content));
}
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 - GET '/api/v1/volume/%s'\n", option);
@@ -118,58 +145,170 @@ void api_v1_playback_volume() {
currentVolume--; currentVolume--;
if (currentVolume < 0) currentVolume = 0; if (currentVolume < 0) currentVolume = 0;
} else if (option == "mute") { } else if (option == "mute") {
currentVolume = 0; // TODO: remember the previous volume and just mute it; unmute has to be implemented then muted = true;
} else if (option == "unmute") {
muted = false;
} 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 == "0") {
currentVolume = 0;
} else if (option == "1") {
currentVolume = 1;
} else if (option == "2") {
currentVolume = 2;
} else if (option == "3") {
currentVolume = 3;
} else if (option == "4") {
currentVolume = 4;
} else if (option == "5") {
currentVolume = 5;
} else if (option == "6") {
currentVolume = 6;
} else if (option == "7") {
currentVolume = 7;
} else if (option == "8") {
currentVolume = 8;
} else if (option == "9") {
currentVolume = 9;
} else if (option == "10") {
currentVolume = 10;
} else if (option == "11") {
currentVolume = 11;
} else if (option == "12") {
currentVolume = 12;
} else if (option == "13") {
currentVolume = 13;
} else if (option == "14") {
currentVolume = 14;
} else if (option == "15") {
currentVolume = 15;
} else if (option == "16") {
currentVolume = 16;
} else if (option == "17") {
currentVolume = 17;
} else if (option == "18") {
currentVolume = 18;
} else if (option == "19") {
currentVolume = 19;
} else if (option == "20") {
currentVolume = 20;
} else { } else {
success = false; String toIntStr;
int volumeLevel;
// convert string to int
for (int i = 0; i < option.length(); i++) {
if (isDigit((char)option[i])) toIntStr += option[i];
else success = false;
}
if (success && toIntStr.toInt() <= maxVolume) currentVolume = toIntStr.toInt();
else success = false;
} }
audio.setVolume(currentVolume); // set volume if (!muted) audio.setVolume(currentVolume); // set volume if not muted and in range
else audio.setVolume(0); // if muted, set volume to 0
String content = "\"volume\": "; // prepare the http response String content = "\"volume\": "; // prepare the http response
content += String(currentVolume); content += String(currentVolume);
content += ", \"muted\": ";
content += muted ? "true" : "false";
api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it
}
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);
if (option == "right") {
balanceLevel--;
if (balanceLevel < -16) balanceLevel = -16;
} else if (option == "left") {
balanceLevel++;
if (balanceLevel > 16) balanceLevel = 16;
} else if (option == "get") {
// just here that no 'success: false' is sent
} else {
String toIntStr; // between 0 and 32
// convert string to int
for (int i = 0; i < option.length(); i++) {
if (isDigit((char)option[i])) toIntStr += option[i];
else success = false;
}
if (success && toIntStr.toInt() <= 32) balanceLevel = toIntStr.toInt() - 16; // as the input string is between 0 and 32
else success = false;
}
audio.setBalance(balanceLevel); // set volume if not muted and in range
String content = "\"balance\": "; // prepare the http response
content += String(balanceLevel);
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 api_server.send(200, "application/json", generate_api_json(success, content)); // generate json and send it
} }
@@ -199,7 +338,7 @@ void api_v1_playlist_get() {
bool firstRun = true; bool firstRun = true;
while (true) { while (true) {
currentSong = getSongFromPlaylist(currentPlaylist, index); currentSong = getURLFromPlaylist(currentPlaylist, index);
if (currentSong == "") { if (currentSong == "") {
content += "\""; content += "\"";
break; break;
@@ -218,15 +357,102 @@ void api_v1_playlist_get() {
api_server.send(200, "application/json", generate_api_json(true, content)); api_server.send(200, "application/json", generate_api_json(true, content));
} }
void api_v1_settings_restart() { void api_v1_playlist_create() {
Serial.println("[HTTP] [API] 200 - POST '/api/v1/playlist/create'");
// get the right POST param
String folderPath;
for (int i = 0; i < api_server.args(); i++) {
if (api_server.argName(i) == "folderPath") {
folderPath = api_server.arg(i);
break;
}
}
bool code = createPlaylistFromDirectory(folderPath);
if (code) { // if creating the playlist was successful
String playlistPath = folderPath + "/" + directoryPlaylistName + playlistExtension;
String content = "\"playlist_path\": \"" + playlistPath + "\"";
api_server.send(200, "application/json", generate_api_json(true, content));
} else {
api_server.send(200, "application/json", generate_api_json(false));
}
}
void api_v1_playlist_play() {
Serial.println("[HTTP] [API] 200 - POST '/api/v1/playlist/play'");
// get the right POST param
String playlistPath;
for (int i = 0; i < api_server.args(); i++) {
if (api_server.argName(i) == "playlistPath") {
playlistPath = api_server.arg(i);
break;
}
}
if (getURLFromPlaylist(playlistPath, 0) == "") { // if playlist is empty or nonexistent
api_server.send(200, "application/json", generate_api_json(false)); // send no success info
} else {
currentPlaylist = playlistPath;
currentPlaylistPosition = -1;
nextAudio();
api_server.send(200, "application/json", generate_api_json(true)); // just return the success info
}
}
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 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);
delay(5000);
api_server.send(200, "application/json", "{\"restart\": true, \"wait_time\": 5000}"); api_server.send(200, "application/json", "{\"restart\": true, \"wait_time\": 5000}");
delay(5000);
ESP.restart(); // reset everything and restart the program ESP.restart(); // reset everything and restart the program
} }
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\": \"" + getWiFiSSID() + "\""));
}
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
writeWiFiSecrets(ssid, psk);
} else if (ssid != "" && psk == "") { // just the ssid is given
writeWiFiSecrets(ssid, getWiFiPSK());
} else if (ssid == "" && psk != "") { // just the psk is given
writeWiFiSecrets(getWiFiSSID(), 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 setupConfigWeb() { void setupConfigWeb() {
if (runConfServer) { if (runConfServer) {
conf_server.onNotFound([]() { conf_server.onNotFound([]() {
@@ -247,15 +473,26 @@ void setupApiWeb() {
api_server.send(404, "application/json", "{\"code\": 404, \"message\": \"Resource not found.\"}"); api_server.send(404, "application/json", "{\"code\": 404, \"message\": \"Resource not found.\"}");
}); });
api_server.on("/", apiRoot); api_server.on("/", apiRoot);
api_server.on("/api/v1/playback/toggle", api_v1_playback_toggle); api_server.on("/api/v1/playback/toggle", HTTP_GET, api_v1_playback_toggle);
api_server.on("/api/v1/playback/play", api_v1_playback_play); api_server.on("/api/v1/playback/play", HTTP_GET, api_v1_playback_play);
api_server.on("/api/v1/playback/pause", api_v1_playback_pause); api_server.on("/api/v1/playback/pause", HTTP_GET, api_v1_playback_pause);
api_server.on("/api/v1/playback/next", api_v1_playback_next); api_server.on("/api/v1/playback/next", HTTP_GET, api_v1_playback_next);
api_server.on("/api/v1/playback/previous", api_v1_playback_previous); api_server.on("/api/v1/playback/previous", HTTP_GET, api_v1_playback_previous);
api_server.on("/api/v1/playback/info", api_v1_playback_info); api_server.on("/api/v1/playback/info", HTTP_GET, api_v1_playback_info);
api_server.on("/api/v1/playlist/get", api_v1_playlist_get); api_server.on(UriBraces("/api/v1/playback/{}"), HTTP_GET, api_v1_playback_byindex);
api_server.on(UriBraces("/api/v1/volume/{}"), api_v1_playback_volume); api_server.on("/api/v1/playlist/get", HTTP_GET, api_v1_playlist_get);
api_server.on("/api/v1/settings/restart", api_v1_settings_restart); 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/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);
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

@@ -32,11 +32,13 @@ void setupWiFi() {
for(int i = 0; i<33; i++) { if(WiFiSSIDstr.length() >= i) {WiFiSSID[i] = WiFiSSIDstr[i];} else break; } 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; } 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, WiFiPSK); // kept here fordebugging wifi problems
WiFi.disconnect(); WiFi.disconnect();
WiFi.mode(WIFI_AP_STA); WiFi.mode(WIFI_AP_STA);
WiFi.begin(WiFiSSID, WiFiPSK); WiFi.begin(WiFiSSID, WiFiPSK);
// wait for 10 seconds for wifi to connect // 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) { if(WiFi.status() != WL_CONNECTED) {
currentWiFiMode = 1; currentWiFiMode = 1;
Serial.printf("[WiFi] Unable to connect to %s\n", WiFiSSID); Serial.printf("[WiFi] Unable to connect to %s\n", WiFiSSID);