Compare commits
58 Commits
Author | SHA1 | Date | |
---|---|---|---|
d06f2da73d | |||
cd62f273c1 | |||
90bd0f68c3 | |||
af662aa327 | |||
f53ef3c5dc | |||
425e579211 | |||
0fe0668659 | |||
3b12974ce2 | |||
5ee15af74d | |||
f2988c2eb5 | |||
87042e72e0 | |||
98a91e4974 | |||
e3c5d4b6c9 | |||
8904840428 | |||
c4362f998c | |||
ddb9a1e49e | |||
61e167d036 | |||
da55732450 | |||
d68d773090 | |||
2d41b65ca8 | |||
b1a677ab35 | |||
3b6811073a | |||
72e60c34e0 | |||
d7750efaa6 | |||
4a92cf4193 | |||
95281b47f5 | |||
9e9ca41b87 | |||
b8ee6a28c7 | |||
8b15c8d5f4 | |||
9bd76db74c | |||
9e307010cc | |||
f613f0e6e6 | |||
5c706399ad | |||
775a3bc3fb | |||
f8798535de | |||
f86e960b62 | |||
0b95a24eb4 | |||
6708b65b8a | |||
022c96ea13 | |||
072ec79050 | |||
8f31f9036c | |||
ff0873e37e | |||
7b025bbc16 | |||
28a025f499 | |||
1811764655 | |||
4f883933e8 | |||
c431e665fe | |||
1d45610ae7 | |||
4e8fe0c5b0 | |||
a9e067cf44 | |||
665e6aefbf | |||
4df80de44d | |||
bbe632621c | |||
f87df41031 | |||
67382805a3 | |||
1303199474 | |||
3ea3f91b43 | |||
6ac82e8d2c |
2
.gitignore
vendored
Normal file → Executable file
2
.gitignore
vendored
Normal file → Executable file
@@ -1,2 +1,2 @@
|
||||
# ignore IDE specific files and folders
|
||||
/.theia/
|
||||
.theia/
|
36
API_DOC.txt
Executable file
36
API_DOC.txt
Executable file
@@ -0,0 +1,36 @@
|
||||
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)
|
||||
|
0
LICENSE → LICENSE.md
Normal file → Executable file
0
LICENSE → LICENSE.md
Normal file → Executable file
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
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
|
||||
|
||||
|
||||
// define all constants
|
||||
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 String playlistExtension = ".m3u"; // extension for playlist files
|
||||
const String directoryPlaylistName = ".directory"; // name for directory playlists (not a path, just a filename without ext.!)
|
||||
|
||||
// create all needed variables
|
||||
int currentVolume; // variable where current volume (0...21) is stored
|
||||
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 = 0;
|
||||
String currentPlaylist = "/audio/" + directoryPlaylistName + playlistExtension;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200); // setup Serial console (over USB) with baud 115200
|
||||
|
||||
// setup all pins (not SPI and other buses!)
|
||||
pinMode(sdCardEjectPin, INPUT);
|
||||
pinMode(pausePlaybackPin, INPUT);
|
||||
pinMode(readyPin, OUTPUT);
|
||||
|
||||
// connect to sd card reader
|
||||
Serial.println("[DEBUG] Connecting to SD-Card reader");
|
||||
while(!setupSD(SD_CS, SPI_MISO, SPI_MOSI, SPI_SCK)) { Serial.printf("[DEBUG] Can't connect to SD card reader! Waiting for %d milliseconds...\n", retrySDMountTreshold); delay(retrySDMountTreshold); }
|
||||
Serial.println("[DEBUG] Connected to SD-Card reader");
|
||||
|
||||
setupAudio(); // setup audio library
|
||||
createPlaylistFromDirectory("/audio"); // create playlist from default dir ("/audio")
|
||||
|
||||
audio.connecttoFS(SD, getSongFromPlaylist(currentPlaylist, currentPlaylistPosition).c_str()); // play first element of the playlist
|
||||
|
||||
digitalWrite(readyPin, HIGH); // show that startup is done and everything works fine
|
||||
}
|
||||
|
||||
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
|
||||
digitalWrite(readyPin, LOW); // indicate that SD card can get removed (the ready-LED will go off)
|
||||
Serial.printf("[DEBUG] 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
|
||||
}
|
||||
loopAudio(); // play audio(-snippets) when not paused and pause if the therefor intended btn is clicked!
|
||||
}
|
120
README.md
Normal file → Executable file
120
README.md
Normal file → Executable file
@@ -1,16 +1,104 @@
|
||||
# NetSpeaker
|
||||
|
||||
Code of the NetSpeaker project written in C++
|
||||
|
||||
# License
|
||||
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
# NetSpeaker
|
||||
|
||||
[](https://unlicense.org/)
|
||||
[](https://opensource.org/)
|
||||
[](https://www.espressif.com/en/products/socs/esp32)
|
||||
[]()
|
||||
|
||||
|
||||
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
|
||||
via the constant operation_mode. Valid choices are:
|
||||
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
Features, already implemented, or still in progress for the v1.0.0 release!
|
||||
|
||||
- [x] SD card support for playing audio files
|
||||
- [x] Two operating modes, standalone mode fully implemented
|
||||
- [x] API in mode 0
|
||||
- [x] API methods:
|
||||
- [X] /api/v1/playback/toggle
|
||||
- [X] /api/v1/playback/play
|
||||
- [X] /api/v1/playback/pause
|
||||
- [X] /api/v1/playback/next
|
||||
- [X] /api/v1/playback/previous
|
||||
- [ ] /api/v1/playback/<index>
|
||||
- [X] /api/v1/playback/info
|
||||
- [ ] /api/v1/playback/id3_image
|
||||
- [X] /api/v1/playlist/get
|
||||
- [ ] /api/v1/playlist/append
|
||||
- [ ] /api/v1/playlist/remove
|
||||
- [X] /api/v1/volume/get
|
||||
- [X] /api/v1/volume/up
|
||||
- [X] /api/v1/volume/down
|
||||
- [X] /api/v1/volume/mute
|
||||
- [X] /api/v1/volume/<0-20>
|
||||
- [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
|
||||
- [ ] /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
|
||||
- [X] Improve the volume endpoint handler (currently pretty undynamic - not anymore :)
|
||||
- [ ] Add better encoding as umlauts are not shown **sometimes**
|
||||
|
||||
|
||||
## Credits & Acknowledgements
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
Pull requests are welcome. For major changes, please open an issue first
|
||||
to discuss what you would like to change.
|
||||
Please also make sure to test things carefully before contributing them.
|
||||
|
||||
## Mirrors
|
||||
|
||||
- [Privacynerd's Gitea (main)](https://git.privacynerd.de/NetSpeaker/NetSpeaker/)
|
||||
- [Codeberg](https://codeberg.org/NetSpeaker/NetSpeaker)
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
|
||||
|
85
audio.ino
85
audio.ino
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
void nextAudio() {
|
||||
String songPath;
|
||||
|
||||
currentPlaylistPosition++; // increment once
|
||||
songPath = getSongFromPlaylist(currentPlaylist, currentPlaylistPosition);
|
||||
if(songPath == "") {
|
||||
currentPlaylistPosition = -1; // the next time "nextAudio()" is called it will increment to 0
|
||||
nextAudio(); // recursive call
|
||||
return; // exit
|
||||
}
|
||||
audio.connecttoFS(SD, songPath.c_str()); // play the next song
|
||||
|
||||
delay(100); // button has to be pressed for at least 0.1s!
|
||||
while(analogRead(pausePlaybackPin) > 4000) {} // wait until btn is released
|
||||
}
|
||||
|
||||
void previousAudio() {
|
||||
String songPath;
|
||||
|
||||
currentPlaylistPosition--;
|
||||
if(currentPlaylistPosition < 0) currentPlaylistPosition = 0;
|
||||
|
||||
songPath = getSongFromPlaylist(currentPlaylist, currentPlaylistPosition);
|
||||
audio.connecttoFS(SD, songPath.c_str()); // play the previous song
|
||||
|
||||
delay(100); // button has to be pressed for at least 0.1s!
|
||||
while(analogRead(pausePlaybackPin) > 4000) {} // wait until btn is released
|
||||
}
|
||||
|
||||
void audio_eof_mp3(const char *info){ // MUST HAVE THIS NAME (audio_eof_mp3) because it's a callback function for the Audio.h library
|
||||
nextAudio();
|
||||
}
|
||||
|
||||
void switchPlaybackIfButtonClicked() {
|
||||
if(analogRead(pausePlaybackPin) > 4000) { // check if btn was clicked
|
||||
audioPlaying = !audioPlaying; // play if paused, pause if playing
|
||||
Serial.printf("[INFO] Playback has switched: %s\n", audioPlaying ? "Playing" : "Paused");
|
||||
delay(100); // button has to be pressed for at least 0.1s!
|
||||
while(analogRead(pausePlaybackPin) > 4000) {} // wait until btn is released
|
||||
}
|
||||
}
|
||||
|
||||
void forwardButtonHandler() {
|
||||
if(analogRead(forwardButtonPin) > 4000) {
|
||||
nextAudio(); // play next audio file from playlist
|
||||
}
|
||||
}
|
||||
|
||||
void backwardButtonHandler() {
|
||||
if(analogRead(backwardButtonPin) > 4000) {
|
||||
previousAudio(); // play previous audio file from playlist
|
||||
}
|
||||
}
|
||||
|
||||
void setAudioVolume() {
|
||||
currentVolume = analogRead(audioVolumePin) / 195; // read voltage from audioVolumePin and divide by 195 (min. volume is 0, max. 21; 21 fits 195 times into 4095 (maximum input))
|
||||
audio.setVolume(currentVolume); // set volume
|
||||
}
|
||||
|
||||
void loopAudio() {
|
||||
if(audioPlaying) audio.loop(); // play audio if not paused
|
||||
|
||||
// some thing for pausing etc.
|
||||
switchPlaybackIfButtonClicked(); // pause/play if wanted
|
||||
forwardButtonHandler(); // handle clicks on forward button
|
||||
backwardButtonHandler(); // handle clicks on backward button
|
||||
setAudioVolume(); // set audio volume
|
||||
}
|
BIN
examples-diy/standalone/NetSpeaker-Case_Back.jpg
Normal file
BIN
examples-diy/standalone/NetSpeaker-Case_Back.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 MiB |
BIN
examples-diy/standalone/NetSpeaker-Case_Front.jpg
Normal file
BIN
examples-diy/standalone/NetSpeaker-Case_Front.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
BIN
examples-diy/standalone/NetSpeaker-Case_Part1.FCStd
Executable file
BIN
examples-diy/standalone/NetSpeaker-Case_Part1.FCStd
Executable file
Binary file not shown.
BIN
examples-diy/standalone/NetSpeaker-Case_Part1.png
Normal file
BIN
examples-diy/standalone/NetSpeaker-Case_Part1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
examples-diy/standalone/NetSpeaker-Case_Part1.stl
Executable file
BIN
examples-diy/standalone/NetSpeaker-Case_Part1.stl
Executable file
Binary file not shown.
BIN
examples-diy/standalone/NetSpeaker-Case_Part1_normal.mp4
Executable file
BIN
examples-diy/standalone/NetSpeaker-Case_Part1_normal.mp4
Executable file
Binary file not shown.
BIN
examples-diy/standalone/NetSpeaker-Case_Part2.FCStd
Executable file
BIN
examples-diy/standalone/NetSpeaker-Case_Part2.FCStd
Executable file
Binary file not shown.
BIN
examples-diy/standalone/NetSpeaker-Case_Part2.png
Normal file
BIN
examples-diy/standalone/NetSpeaker-Case_Part2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
BIN
examples-diy/standalone/NetSpeaker-Case_Part2.stl
Executable file
BIN
examples-diy/standalone/NetSpeaker-Case_Part2.stl
Executable file
Binary file not shown.
74
fs.ino
74
fs.ino
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
*/
|
||||
|
||||
int setupSD(int SD_CS, int SPI_MISO, int SPI_MOSI, int SPI_SCK) {
|
||||
pinMode(SD_CS, OUTPUT); digitalWrite(SD_CS, HIGH); // make microSD-card reader board listen to the ESP (over SPI)
|
||||
SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI); // start SPI
|
||||
return SD.begin(SD_CS); // returns 1 if everything is OK, 0 if an error occured (eg. wiring not correct)
|
||||
}
|
||||
|
||||
bool createPlaylistFromDirectory(String folderpath) { // create a .m3u playlist from all directory contents (the directory 'folderpath' is used)
|
||||
File folder;
|
||||
File playlist;
|
||||
|
||||
if(folderpath[folderpath.length() - 1] == '/' && folderpath.length() > 1){ // check if file is ending with a "/" - because it wont work otherwise
|
||||
Serial.println("[ERROR] Can't use a folder path with trailing /");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(SD.exists(folderpath)) { // only open if the folder exists,
|
||||
folder = SD.open(folderpath);
|
||||
} else { // ...else print error and exit function (with return false)
|
||||
Serial.printf("[ERROR] Can't open \"%s\" because it doesn't exist!\n", folderpath.c_str());
|
||||
return false;
|
||||
}
|
||||
playlist = SD.open(folderpath + "/" + directoryPlaylistName + playlistExtension, FILE_WRITE); // open a (new) playlist file
|
||||
|
||||
while(true) {
|
||||
File entry = folder.openNextFile();
|
||||
if(!entry) break; // end the while(true) when no files left!
|
||||
if(!entry.isDirectory() && String(entry.name()) != String(folderpath + "/" + directoryPlaylistName + playlistExtension).c_str()) { // only if not a directory or the playlist itself
|
||||
playlist.println(entry.name());
|
||||
}
|
||||
}
|
||||
playlist.flush(); // write changes to sd card
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
String getSongFromPlaylist(String path, int position) {
|
||||
String song;
|
||||
File playlist;
|
||||
char currentChar;
|
||||
int currentPosition = 0;
|
||||
|
||||
if(!SD.exists(path) or SD.open(path).isDirectory()) {
|
||||
Serial.printf("[ERROR] Can't open \"%s\" because it either doesn't exist or is a directory!\n", path.c_str());
|
||||
return "";
|
||||
}
|
||||
playlist = SD.open(path);
|
||||
|
||||
while(playlist.available()) {
|
||||
if(currentPosition == position) { // if the right position is reached
|
||||
song = (char) playlist.read(); // read character...
|
||||
while(song[song.length() - 1] != '\n' && playlist.available()) song += (char) playlist.read(); // ... as long as a newline hits
|
||||
song = song.c_str(); song[song.length()-1] = '\0'; song = String(song); // remove the trailing '\n'
|
||||
return song; // return the result
|
||||
}
|
||||
currentChar = playlist.read(); // read character...
|
||||
while(currentChar != '\n') currentChar = playlist.read(); // ... as long as a newline hits
|
||||
currentPosition++;
|
||||
}
|
||||
|
||||
return song; // return an empty string if out of index or whatever
|
||||
}
|
130
src/NetSpeaker/NetSpeaker.ino
Executable file
130
src/NetSpeaker/NetSpeaker.ino
Executable file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
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 <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
|
||||
|
||||
|
||||
// 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 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
|
||||
|
||||
// create all needed variables
|
||||
int currentVolume = 20; // variable where current volume (0...20) is stored
|
||||
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)
|
||||
|
||||
struct playbackInfo {
|
||||
String title; // e.g. Big Buck Bunny
|
||||
String type; // e.g. local
|
||||
String resourcePath; // e.g. /audio/XXX.mp3
|
||||
String artist; // e.g. Blender Foundation
|
||||
String track; // e.g. 01/02 or 01
|
||||
String album; // album
|
||||
String year; // id3 tag year
|
||||
String genre; // id3 tag content type
|
||||
String copyright; // id3 tag (special)
|
||||
};
|
||||
struct playbackInfo pbInfo = { "", "", "", "", "", "", "", "", "" };
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200); // setup Serial console (over USB) with baud 115200
|
||||
|
||||
// connect to sd card reader
|
||||
Serial.println("[SETUP] Setting up SD-Card reader");
|
||||
while (!setupSD(SD_CS, SPI_MISO, SPI_MOSI, SPI_SCK)) {
|
||||
Serial.printf("[SETUP] Can't connect to SD card reader! Waiting for %d milliseconds...\n", retrySDMountTreshold);
|
||||
delay(retrySDMountTreshold);
|
||||
}
|
||||
|
||||
if (operation_mode == 0) { // things only need to be done if in interconnected mode
|
||||
setupWiFi();
|
||||
setupConfigWeb();
|
||||
setupApiWeb();
|
||||
} 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);
|
||||
pinMode(backwardButtonPin, INPUT);
|
||||
pinMode(pausePlaybackPin, INPUT);
|
||||
pinMode(forwardButtonPin, INPUT);
|
||||
pinMode(readyPin, OUTPUT);
|
||||
} else {
|
||||
Serial.println("[FATAL] PLEASE CHOOSE A OPERATION MODE! VALID OPTIONS: 0; 1. SLEEPING FOREVER.");
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if (operation_mode == 1) { // things only need to be done if in standalone mode
|
||||
if (audioPlaying) audio.loop(); // play audio if not paused
|
||||
|
||||
loopBtnListeners();
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
232
src/NetSpeaker/audio.ino
Executable file
232
src/NetSpeaker/audio.ino
Executable file
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
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(20);
|
||||
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 url;
|
||||
|
||||
currentPlaylistPosition++; // increment once
|
||||
url = getURLFromPlaylist(currentPlaylist, currentPlaylistPosition);
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
// 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.artist = "";
|
||||
pbInfo.year = "";
|
||||
pbInfo.genre = "";
|
||||
pbInfo.copyright = "";
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
String previousAudio() {
|
||||
String url;
|
||||
|
||||
currentPlaylistPosition--;
|
||||
if (currentPlaylistPosition < 0) currentPlaylistPosition = 0;
|
||||
|
||||
url = getURLFromPlaylist(currentPlaylist, currentPlaylistPosition);
|
||||
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";
|
||||
|
||||
// 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.artist = "";
|
||||
pbInfo.year = "";
|
||||
pbInfo.genre = "";
|
||||
pbInfo.copyright = "";
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
void switchPlaybackIfButtonClicked() {
|
||||
if (analogRead(pausePlaybackPin) > 4000) { // check if btn was clicked
|
||||
audioPlaying = !audioPlaying; // play if paused, pause if playing
|
||||
Serial.printf("[INFO] Playback has switched: %s\n", audioPlaying ? "Playing" : "Paused");
|
||||
delay(100); // button has to be pressed for at least 0.1s!
|
||||
while (analogRead(pausePlaybackPin) > 4000) {} // wait until btn is released
|
||||
}
|
||||
}
|
||||
|
||||
void forwardButtonHandler() {
|
||||
if (analogRead(forwardButtonPin) > 4000) {
|
||||
nextAudio(); // play next audio file from playlist
|
||||
delay(100); // button has to be pressed for at least 0.1s!
|
||||
while (analogRead(pausePlaybackPin) > 4000) {} // wait until btn is released
|
||||
}
|
||||
}
|
||||
|
||||
void backwardButtonHandler() {
|
||||
if (analogRead(backwardButtonPin) > 4000) {
|
||||
previousAudio(); // play previous audio file from playlist
|
||||
delay(100); // button has to be pressed for at least 0.1s!
|
||||
while (analogRead(pausePlaybackPin) > 4000) {} // wait until btn is released
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
if (currentVolume != newCurrentVolume) { // just do it if the volume changed
|
||||
currentVolume = newCurrentVolume;
|
||||
audio.setVolume(currentVolume); // set volume
|
||||
Serial.printf("[INFO] Set volume to %d/20!\n", currentVolume);
|
||||
}
|
||||
}
|
||||
|
||||
void loopBtnListeners() {
|
||||
// some thing for pausing etc.
|
||||
switchPlaybackIfButtonClicked(); // pause/play if wanted
|
||||
forwardButtonHandler(); // handle clicks on forward button
|
||||
backwardButtonHandler(); // handle clicks on backward button
|
||||
setAudioVolume(); // set audio volume
|
||||
}
|
||||
|
||||
|
||||
void audio_eof_mp3(const char *info) { // MUST HAVE THIS NAME (audio_eof_mp3) because it's a callback function for the Audio.h library
|
||||
Serial.printf("[Audio.h] EOF_MP3 %s\n", info);
|
||||
nextAudio();
|
||||
}
|
||||
void audio_eof_speech(const char *info) {
|
||||
Serial.printf("[Audio.h] EOF_Speech %s\n", info);
|
||||
nextAudio();
|
||||
}
|
||||
void audio_info(const char *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
|
||||
Serial.printf("[Audio.h] ID3Data %s\n", info);
|
||||
|
||||
// extract info and write it to the pbInfo struct (for the Web API if used)
|
||||
String info_str = String(info);
|
||||
if (info_str.startsWith("Album: ")) {
|
||||
pbInfo.album = info_str.substring(7);
|
||||
} else if (info_str.startsWith("Title: ")) {
|
||||
pbInfo.title = info_str.substring(7);
|
||||
} else if (info_str.startsWith("Track: ")) {
|
||||
pbInfo.track = info_str.substring(7);
|
||||
} else if (info_str.startsWith("Artist: ")) {
|
||||
pbInfo.artist = info_str.substring(8);
|
||||
} else if (info_str.startsWith("ContentType: ")) {
|
||||
pbInfo.genre = info_str.substring(13);
|
||||
} else if (info_str.startsWith("Year: ")) {
|
||||
pbInfo.year = info_str.substring(6);
|
||||
} else if (info_str.startsWith("Copyright: ")) {
|
||||
pbInfo.copyright = info_str.substring(11);
|
||||
}
|
||||
}
|
||||
void audio_showstation(const char *info) {
|
||||
Serial.printf("[Audio.h] Station %s\n", info);
|
||||
}
|
||||
void audio_showstreamtitle(const char *info) {
|
||||
Serial.printf("[Audio.h] StreamTitle %s\n", info);
|
||||
}
|
||||
void audio_bitrate(const char *info) {
|
||||
Serial.printf("[Audio.h] Bitrate %s\n", info);
|
||||
}
|
||||
void audio_commercial(const char *info) { // duration of ad in sec
|
||||
Serial.printf("[Audio.h] Commercial %s\n", info);
|
||||
}
|
||||
void audio_icyurl(const char *info) { // Name of radio station
|
||||
Serial.printf("[Audio.h] IcyURL %s\n", info);
|
||||
}
|
||||
void audio_lasthost(const char *info) { // stream URL played
|
||||
Serial.printf("[Audio.h] Lasthost %s\n", info);
|
||||
}
|
||||
|
||||
// *********************************************************************************** //
|
||||
// *************** Code 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());
|
||||
}
|
129
src/NetSpeaker/fs.ino
Executable file
129
src/NetSpeaker/fs.ino
Executable file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
*/
|
||||
|
||||
int setupSD(int SD_CS, int SPI_MISO, int SPI_MOSI, int SPI_SCK) {
|
||||
pinMode(SD_CS, OUTPUT);
|
||||
digitalWrite(SD_CS, HIGH); // make microSD-card reader board listen to the ESP (over SPI)
|
||||
SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI); // start SPI
|
||||
int ret = SD.begin(SD_CS);
|
||||
if (ret == 1) Serial.printf("[SETUP] Set up SD card successfully (Pins: MISO %d | MOSI %d | SCK %d | CS %d)\n", SPI_MISO, SPI_MOSI, SPI_SCK, SD_CS);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
String getWiFiPSK() {
|
||||
return getWiFiPSK(wifiConfigPath);
|
||||
}
|
||||
|
||||
|
||||
bool createPlaylistFromDirectory(String folderpath) { // create a .m3u playlist from all directory contents (the directory 'folderpath' is used)
|
||||
File folder;
|
||||
File playlist;
|
||||
|
||||
if (folderpath[folderpath.length() - 1] == '/' && folderpath.length() > 1) { // check if file is ending with a "/" - because it wont work otherwise
|
||||
Serial.printf("[ERROR] Can't use a folder path with trailing /\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SD.exists(folderpath)) { // only open if the folder exists,
|
||||
folder = SD.open(folderpath);
|
||||
} else { // ...else print error and exit function (with return false)
|
||||
Serial.printf("[ERROR] Can't open \"%s\" because it doesn't exist!\n", folderpath.c_str());
|
||||
return false;
|
||||
}
|
||||
playlist = SD.open(folderpath + "/" + directoryPlaylistName + playlistExtension, FILE_WRITE); // open a (new) playlist file
|
||||
|
||||
while (true) {
|
||||
File entry = folder.openNextFile();
|
||||
if (!entry) break; // end the while(true) when no files left!
|
||||
if (!entry.isDirectory() && String(entry.name()) != String(directoryPlaylistName + playlistExtension).c_str()) { // only if not a directory or the playlist itself
|
||||
playlist.println(folderpath + "/" + String(entry.name()));
|
||||
}
|
||||
}
|
||||
playlist.flush(); // write changes to sd card
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
String getURLFromPlaylist(String path, int position) {
|
||||
String song;
|
||||
File playlist;
|
||||
char currentChar;
|
||||
int currentPosition = 0;
|
||||
|
||||
if (!SD.exists(path) or SD.open(path).isDirectory()) {
|
||||
Serial.printf("[ERROR] Can't open \"%s\" because it either doesn't exist or is a directory!\n", path.c_str());
|
||||
return "";
|
||||
}
|
||||
playlist = SD.open(path);
|
||||
|
||||
while (playlist.available()) {
|
||||
if (currentPosition == position) { // if the right position is reached
|
||||
song = (char)playlist.read(); // read character...
|
||||
while (playlist.available()) {
|
||||
currentChar = (char)playlist.read();
|
||||
if (currentChar != '\n' && currentChar != '\r') {
|
||||
song += currentChar; // ... except if and as long as a newline hits
|
||||
} else {
|
||||
break; // exit while loop
|
||||
}
|
||||
}
|
||||
return song; // return the result
|
||||
}
|
||||
currentChar = playlist.read(); // read character...
|
||||
while (currentChar != '\n') currentChar = playlist.read(); // ... as long as a newline hits
|
||||
currentPosition++;
|
||||
}
|
||||
|
||||
return song; // return an empty string if out of index or whatever
|
||||
}
|
316
src/NetSpeaker/webserver.ino
Executable file
316
src/NetSpeaker/webserver.ino
Executable file
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
*/
|
||||
|
||||
String generate_api_json(bool success, String content, bool plain) {
|
||||
String json = "{\"success\": "; // success
|
||||
json += success ? "true" : "false";
|
||||
json += ", ";
|
||||
json += "\"state\": \""; // playing or paused?
|
||||
json += audioPlaying ? "playing" : "paused";
|
||||
json += "\"";
|
||||
if (!plain) {
|
||||
json += ", ";
|
||||
json += content;
|
||||
}
|
||||
json += "}";
|
||||
|
||||
return json;
|
||||
}
|
||||
String generate_api_json(bool success) { // if you just want the basic json frame with the basic info
|
||||
return generate_api_json(success, "", true);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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>";
|
||||
|
||||
api_server.send(200, "text/html", html);
|
||||
}
|
||||
|
||||
void api_v1_playback_toggle() {
|
||||
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/toggle'");
|
||||
|
||||
audioPlaying = !audioPlaying;
|
||||
Serial.printf("[INFO] Playback has switched: %s\n", audioPlaying ? "Playing" : "Paused");
|
||||
|
||||
api_server.send(200, "application/json", generate_api_json(true));
|
||||
}
|
||||
|
||||
void api_v1_playback_play() {
|
||||
Serial.println("[HTTP] [API] 200 - GET '/_api/v1/playback/play'");
|
||||
|
||||
audioPlaying = true;
|
||||
Serial.printf("[INFO] Playback has switched: Playing\n");
|
||||
|
||||
api_server.send(200, "application/json", generate_api_json(true));
|
||||
}
|
||||
|
||||
void api_v1_playback_pause() {
|
||||
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/pause'");
|
||||
|
||||
audioPlaying = false;
|
||||
Serial.printf("[INFO] Playback has switched: Paused\n");
|
||||
|
||||
api_server.send(200, "application/json", generate_api_json(true));
|
||||
}
|
||||
|
||||
void api_v1_playback_next() {
|
||||
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/next'");
|
||||
|
||||
nextAudio();
|
||||
String content = "\"resource_playlist_index\": ";
|
||||
content += String(currentPlaylistPosition);
|
||||
|
||||
|
||||
api_server.send(200, "application/json", generate_api_json(true, content));
|
||||
}
|
||||
|
||||
void api_v1_playback_previous() {
|
||||
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/previous'");
|
||||
|
||||
previousAudio();
|
||||
String content = "\"resource_playlist_index\": ";
|
||||
content += String(currentPlaylistPosition);
|
||||
|
||||
api_server.send(200, "application/json", generate_api_json(true, content));
|
||||
}
|
||||
|
||||
void api_v1_playback_volume() {
|
||||
String option = api_server.pathArg(0);
|
||||
bool success = true;
|
||||
Serial.printf("[HTTP] [API] 200 - GET '/api/v1/volume/%s'\n", option);
|
||||
|
||||
if (option == "up") {
|
||||
currentVolume++;
|
||||
if (currentVolume > 20) currentVolume = 20;
|
||||
} else if (option == "down") {
|
||||
currentVolume--;
|
||||
if (currentVolume < 0) currentVolume = 0;
|
||||
} else if (option == "mute") {
|
||||
muted = true;
|
||||
} else if (option == "unmute") {
|
||||
muted = false;
|
||||
} else if (option == "get") {
|
||||
// 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 {
|
||||
success = false;
|
||||
}
|
||||
|
||||
if (!muted) audio.setVolume(currentVolume); // set volume if not muted
|
||||
else audio.setVolume(0); // if muted, set volume to 0
|
||||
|
||||
String content = "\"volume\": "; // prepare the http response
|
||||
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_playback_info() {
|
||||
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playback/info'");
|
||||
String content = "\"resource_path\": \"" + pbInfo.resourcePath + "\", "; // resource's path
|
||||
content += "\"resource_type\": \"" + pbInfo.type + "\", "; // type
|
||||
content += "\"resource_title\": \"" + pbInfo.title + "\", "; // title
|
||||
content += "\"resource_album\": \"" + pbInfo.album + "\", "; // album
|
||||
content += "\"resource_artist\": \"" + pbInfo.artist + "\", "; // artist
|
||||
content += "\"resource_track\": \"" + pbInfo.track + "\", "; // track
|
||||
content += "\"resource_year\": \"" + pbInfo.year + "\", "; // year
|
||||
content += "\"resource_genre\": \"" + pbInfo.genre + "\", "; // genre
|
||||
content += "\"resource_copyright\": \"" + pbInfo.copyright + "\", "; // copyright
|
||||
content += "\"resource_playlist_path\": \"" + currentPlaylist + "\", "; // playlist path
|
||||
content += "\"resource_playlist_index\": " + String(currentPlaylistPosition); // playlist index (starting from 0)
|
||||
|
||||
api_server.send(200, "application/json", generate_api_json(true, content));
|
||||
}
|
||||
|
||||
void api_v1_playlist_get() {
|
||||
Serial.println("[HTTP] [API] 200 - GET '/api/v1/playlist/get'");
|
||||
|
||||
int index = 0;
|
||||
String content = "\"playlist\": {";
|
||||
String currentSong;
|
||||
bool firstRun = true;
|
||||
|
||||
while (true) {
|
||||
currentSong = getURLFromPlaylist(currentPlaylist, index);
|
||||
if (currentSong == "") {
|
||||
content += "\"";
|
||||
break;
|
||||
} else if (!firstRun) {
|
||||
content += "\", ";
|
||||
}
|
||||
content += "\"" + String(index);
|
||||
content += "\": \"";
|
||||
content += currentSong;
|
||||
firstRun = false;
|
||||
|
||||
index++;
|
||||
}
|
||||
content += "}";
|
||||
|
||||
api_server.send(200, "application/json", generate_api_json(true, content));
|
||||
}
|
||||
|
||||
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_settings_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}");
|
||||
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);
|
||||
|
||||
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() {
|
||||
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("/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("/api/v1/settings/restart", HTTP_GET, api_v1_settings_restart);
|
||||
|
||||
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();
|
||||
}
|
61
src/NetSpeaker/wifi.ino
Executable file
61
src/NetSpeaker/wifi.ino
Executable file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
*/
|
||||
|
||||
|
||||
void setupWiFi() {
|
||||
if(WiFi.status() == WL_CONNECTED) { return; } // return if not connected
|
||||
switch(currentWiFiMode) {
|
||||
case 0: {
|
||||
Serial.println("[WiFi] Connecting to WiFi...");
|
||||
String WiFiSSIDstr = getWiFiSSID();
|
||||
String WiFiPSKstr = getWiFiPSK();
|
||||
if(WiFiPSKstr == String() || WiFiSSIDstr == 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; }
|
||||
|
||||
WiFi.disconnect();
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
WiFi.begin(WiFiSSID, WiFiPSK);
|
||||
// wait for 10 seconds for wifi to connect
|
||||
int start_timer = millis(); while(WiFi.status() != WL_CONNECTED) { if((millis()-start_timer) > 20000) break; }
|
||||
if(WiFi.status() != WL_CONNECTED) {
|
||||
currentWiFiMode = 1;
|
||||
Serial.printf("[WiFi] Unable to connect to %s\n", WiFiSSID);
|
||||
setupWiFi(); // recursive call to reach the 'case 1' statement
|
||||
break;
|
||||
}
|
||||
Serial.printf("[WiFi] Connected to WiFi %s, got IP-Adress ", WiFiSSID);
|
||||
Serial.println(WiFi.localIP());
|
||||
}
|
||||
break;
|
||||
case 1: {
|
||||
if(!apON) {
|
||||
Serial.printf("[WiFi] Opening AP\n[WiFi] SSID: %s\n[WiFi] PSK: %s\n", apSSID.c_str(), apPSK.c_str());
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(apSSID, apPSK);
|
||||
apON = true;
|
||||
Serial.printf("[WiFi] Opened AP successfully\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
*/
|
12
wifi.ino
12
wifi.ino
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
*/
|
Reference in New Issue
Block a user