Compare commits
25 Commits
3b0410f560
...
f73d45404f
Author | SHA1 | Date | |
---|---|---|---|
f73d45404f | |||
ecc7c46b8d | |||
0dd5937707 | |||
547080acf5 | |||
d3c699aefa | |||
a8d19cd6e1 | |||
38d48ab0e4 | |||
51bef05465 | |||
4eef69516e | |||
9175193b67 | |||
65118fbc42 | |||
076f0e9dfd | |||
571e969bc4 | |||
8e15f87cd3 | |||
dd9e1538c8 | |||
001e275131 | |||
196021bef5 | |||
63b9616677 | |||
d4c9a6d582 | |||
5fe66fdaef | |||
6445dc0fb8 | |||
7a20cf4b04 | |||
bbf77c6b1e | |||
b805d1b183 | |||
07b1ea3a5c |
25
README.md
25
README.md
@ -145,3 +145,28 @@ will play. On December 2nd, track 2 followed by track 1. On
|
|||||||
December 3rd, tracks 3, 1 and 2. From December 24th on, track
|
December 3rd, tracks 3, 1 and 2. From December 24th on, track
|
||||||
24 followed by tracks 1-23. So your kid will get the "daily track"
|
24 followed by tracks 1-23. So your kid will get the "daily track"
|
||||||
first, followed by all previous tags in the right order.
|
first, followed by all previous tags in the right order.
|
||||||
|
|
||||||
|
#### API
|
||||||
|
You can send commands to ESMP3 using three different ways:
|
||||||
|
* Through a websocket connection to `ws://<IP>/ws`.
|
||||||
|
* Through the serial console using an USB cable.
|
||||||
|
* Via HTTP POST request to `http://<IP>/cmd`, having the
|
||||||
|
command in the variable `cmd`.
|
||||||
|
|
||||||
|
Supported commands are:
|
||||||
|
| Command | Action |
|
||||||
|
|---------|--------|
|
||||||
|
| `play <PATH>` | Starts playing the given path. Path may be a path on the
|
||||||
|
sd card or a http(s) URL of a webstream (direct links to mp3/4/ogg streams,
|
||||||
|
PLS files, M3U files or podcast XML feeds are supported). |
|
||||||
|
| `play` | Continues playing the previously played thing. |
|
||||||
|
| `stop` | Stops playing. |
|
||||||
|
| `volume=<X>` | Sets the volume to X (0-255). |
|
||||||
|
| `track_prev` | Starts the previous track, if available. |
|
||||||
|
| `track_next` | Starts the next track, if available. |
|
||||||
|
| `track=<X>` | Starts playing track no. X of the currently playing album. |
|
||||||
|
| `reset_vs1053` | Resets the VS1053 audio chip. |
|
||||||
|
| `reboot` | Reboots ESMP3. |
|
||||||
|
| `add_mapping=<ID>=<PATH>` | Adds a mapping between RFID card <ID> and path
|
||||||
|
<PATH>. See `play` for valid path formats. |
|
||||||
|
| `update` | Runs an update check. |
|
||||||
|
53
deploy.sh
Executable file
53
deploy.sh
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -x
|
||||||
|
#set -e
|
||||||
|
|
||||||
|
if ! git diff-index --quiet HEAD ; then
|
||||||
|
echo "Git isn't clean. Cant deploy."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
branch_name=$(git symbolic-ref -q HEAD)
|
||||||
|
branch_name=${branch_name##refs/heads/}
|
||||||
|
branch_name=${branch_name:-HEAD}
|
||||||
|
|
||||||
|
if [ "$branch_name" != "master" ]; then
|
||||||
|
echo "We are not on master branch. Can't deploy."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo
|
||||||
|
echo "Last tags:"
|
||||||
|
vers=`git tag --sort=-version:refname | head -n 5`
|
||||||
|
while read version; do
|
||||||
|
echo " $version"
|
||||||
|
done <<< "$vers"
|
||||||
|
|
||||||
|
read -p "Version to generate: " VERSION
|
||||||
|
|
||||||
|
OTA_VERSION=`grep "VERSION=" bin/update.manifest | cut -d"=" -f2`
|
||||||
|
OTA_VERSION=$(( "$OTA_VERSION" + 1 ))
|
||||||
|
|
||||||
|
sed -i.bak "s/#define OTA_VERSION .*/#define OTA_VERSION $OTA_VERSION/" include/config.h include/config.sample.h
|
||||||
|
rm include/config.h.bak include/config.sample.h.bak
|
||||||
|
|
||||||
|
PLATFORMIO_BUILD_FLAGS='-DVERSION=\"$VERSION\"' pio run -e deploy -t buildprog || exit 1
|
||||||
|
|
||||||
|
cp .pio/build/deploy/firmware.bin bin/firmware.bin || exit 1
|
||||||
|
|
||||||
|
sed -i.bak "s/VERSION=.*/VERSION=$OTA_VERSION/" bin/update.manifest
|
||||||
|
MD5=`md5sum --binary bin/firmware.bin | cut -d" " -f1`
|
||||||
|
sed -i.bak "s/IMAGE_MD5=.*/IMAGE_MD5=$MD5/" bin/update.manifest
|
||||||
|
rm bin/update.manifest.bak
|
||||||
|
|
||||||
|
echo; echo; echo; echo; echo
|
||||||
|
echo "Please check the git diff, if everything looks okay:"
|
||||||
|
git diff
|
||||||
|
|
||||||
|
read -p "Press ENTER to continue, Ctrl-C to abort. " foo
|
||||||
|
|
||||||
|
git add bin/firmware.bin bin/update.manifest
|
||||||
|
git commit -m "Deploying version $VERSION."
|
||||||
|
git tag -a -m "Deploying version $VERSION" $VERSION
|
||||||
|
git push --follow-tags
|
@ -1,13 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// This is a simple number indicating the version for the HTTP Updater.
|
||||||
|
#define OTA_VERSION 1
|
||||||
|
// Comment out to prevent automatic updates.
|
||||||
|
#define OTA_UPDATE_URL "https://files.schle.nz/esmp3/update.manifest"
|
||||||
|
#define OTA_CHECK_INTERVAL 12*60*60*1000 // 12 hours
|
||||||
|
|
||||||
#define SHOW_DEBUG
|
#define SHOW_DEBUG
|
||||||
//#define SHOW_TRACE
|
//#define SHOW_TRACE
|
||||||
#define FTP_DEBUG
|
#define FTP_DEBUG
|
||||||
#define DELAY_AFTER_DEBUG_AND_TRACE 0
|
#define DELAY_AFTER_DEBUG_AND_TRACE 0
|
||||||
|
|
||||||
#define WIFI_SSID "---CHANGEME---"
|
// Here you can define WiFi data to use. But actually, the better way to do
|
||||||
#define WIFI_PASS "---CHANGEME---"
|
// this is by using /_wifi.txt on the sd card.
|
||||||
|
//#define WIFI_SSID "---CHANGEME---"
|
||||||
|
//#define WIFI_PASS "---CHANGEME---"
|
||||||
|
|
||||||
#define VS1053_SLEEP_DELAY 5000
|
#define VS1053_SLEEP_DELAY 5000
|
||||||
#define POSITION_SEND_INTERVAL 5000
|
#define POSITION_SEND_INTERVAL 5000
|
||||||
|
@ -10,6 +10,8 @@ class Controller;
|
|||||||
#include "playlist.h"
|
#include "playlist.h"
|
||||||
#include "playlist_manager.h"
|
#include "playlist_manager.h"
|
||||||
#include "http_server.h"
|
#include "http_server.h"
|
||||||
|
|
||||||
|
#undef DEPRECATED
|
||||||
#include <MFRC522.h>
|
#include <MFRC522.h>
|
||||||
|
|
||||||
enum ControllerState { NORMAL, LOCKING, LOCKED };
|
enum ControllerState { NORMAL, LOCKING, LOCKED };
|
||||||
@ -32,6 +34,8 @@ private:
|
|||||||
|
|
||||||
unsigned long _last_rfid_scan_at = 0;
|
unsigned long _last_rfid_scan_at = 0;
|
||||||
unsigned long _last_position_info_at = 0;
|
unsigned long _last_position_info_at = 0;
|
||||||
|
unsigned long _last_update_check_at = 0;
|
||||||
|
unsigned long _last_wifi_try_at = 0;
|
||||||
String _serial_buffer = String();
|
String _serial_buffer = String();
|
||||||
String _cmd_queue = "";
|
String _cmd_queue = "";
|
||||||
void _execute_command_ls(String path);
|
void _execute_command_ls(String path);
|
||||||
|
5
include/main.h
Normal file
5
include/main.h
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
void wifi_connect();
|
||||||
|
|
||||||
|
extern const uint8_t file_index_html_start[] asm("_binary_src_index_html_start");
|
@ -4,9 +4,16 @@
|
|||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include "http_client_wrapper.h"
|
#include "http_client_wrapper.h"
|
||||||
|
|
||||||
|
enum PlaylistPersistence {
|
||||||
|
PERSIST_NONE,
|
||||||
|
PERSIST_TEMPORARY,
|
||||||
|
PERSIST_PERMANENTLY
|
||||||
|
};
|
||||||
|
|
||||||
struct PlaylistEntry {
|
struct PlaylistEntry {
|
||||||
String filename;
|
String filename;
|
||||||
String title;
|
String title;
|
||||||
|
String id;
|
||||||
|
|
||||||
bool operator<(PlaylistEntry p) { return title < p.title; }
|
bool operator<(PlaylistEntry p) { return title < p.title; }
|
||||||
};
|
};
|
||||||
@ -19,12 +26,14 @@ private:
|
|||||||
bool _shuffled = false;
|
bool _shuffled = false;
|
||||||
std::vector<PlaylistEntry> _files;
|
std::vector<PlaylistEntry> _files;
|
||||||
String _title = "";
|
String _title = "";
|
||||||
|
String _path;
|
||||||
void _add_path(String path);
|
void _add_path(String path);
|
||||||
void _examine_http_url(String url);
|
void _examine_http_url(String url);
|
||||||
void _parse_rss(HTTPClientWrapper* http);
|
void _parse_rss(HTTPClientWrapper* http);
|
||||||
void _parse_m3u(HTTPClientWrapper* http);
|
void _parse_m3u(HTTPClientWrapper* http);
|
||||||
void _parse_pls(HTTPClientWrapper* http);
|
void _parse_pls(HTTPClientWrapper* http);
|
||||||
public:
|
public:
|
||||||
|
PlaylistPersistence persistence = PERSIST_TEMPORARY;
|
||||||
Playlist(String path);
|
Playlist(String path);
|
||||||
void start();
|
void start();
|
||||||
uint16_t get_file_count();
|
uint16_t get_file_count();
|
||||||
@ -34,9 +43,12 @@ public:
|
|||||||
bool track_prev();
|
bool track_prev();
|
||||||
void track_restart();
|
void track_restart();
|
||||||
bool set_track(uint8_t track);
|
bool set_track(uint8_t track);
|
||||||
|
void set_track_by_id(String id);
|
||||||
void reset();
|
void reset();
|
||||||
|
String path();
|
||||||
bool is_empty();
|
bool is_empty();
|
||||||
String get_current_file();
|
bool get_current_file(String* dst);
|
||||||
|
String get_current_track_id();
|
||||||
uint32_t get_position();
|
uint32_t get_position();
|
||||||
void set_position(uint32_t p);
|
void set_position(uint32_t p);
|
||||||
void shuffle(uint8_t random_offset=0);
|
void shuffle(uint8_t random_offset=0);
|
||||||
|
@ -20,4 +20,5 @@ public:
|
|||||||
String json();
|
String json();
|
||||||
bool add_mapping(String id, String folder);
|
bool add_mapping(String id, String folder);
|
||||||
String create_mapping_txt();
|
String create_mapping_txt();
|
||||||
|
void persist(Playlist* p);
|
||||||
};
|
};
|
||||||
|
10
include/updater.h
Normal file
10
include/updater.h
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "http_client_wrapper.h"
|
||||||
|
|
||||||
|
class Updater {
|
||||||
|
public:
|
||||||
|
static void run();
|
||||||
|
static bool do_update(int cmd, String url, String expected_md5);
|
||||||
|
static bool read_line(String* dst, HTTPClientWrapper* http, String expected_key);
|
||||||
|
};
|
6
partitions.csv
Normal file
6
partitions.csv
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Custom partition table without SPIFFS.
|
||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
|
otadata, data, ota, 0xe000, 0x2000,
|
||||||
|
app0, app, ota_0, 0x10000, 0x220000,
|
||||||
|
app1, app, ota_1, 0x230000,0x220000,
|
|
@ -8,16 +8,33 @@
|
|||||||
; Please visit documentation for the other options and examples
|
; Please visit documentation for the other options and examples
|
||||||
; https://docs.platformio.org/page/projectconf.html
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[platformio]
|
||||||
|
default_envs = esp32
|
||||||
|
|
||||||
|
[extra]
|
||||||
|
lib_deps =
|
||||||
|
63 ; MFRC522
|
||||||
|
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
||||||
|
ArduinoJSON
|
||||||
|
6691 ; TinyXML
|
||||||
|
|
||||||
[env:esp32]
|
[env:esp32]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp-wrover-kit
|
board = esp-wrover-kit
|
||||||
framework = arduino
|
framework = arduino
|
||||||
upload_speed = 512000
|
upload_speed = 512000
|
||||||
build_flags=!./build_version.sh
|
build_flags=!./build_version.sh
|
||||||
lib_deps = MFRC522
|
lib_deps = ${extra.lib_deps}
|
||||||
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
|
||||||
ArduinoJSON
|
|
||||||
6691 ; TinyXML
|
|
||||||
upload_port = /dev/cu.SLAB_USBtoUART
|
upload_port = /dev/cu.SLAB_USBtoUART
|
||||||
monitor_speed = 74480
|
monitor_speed = 115200
|
||||||
|
board_build.embed_txtfiles = src/index.html
|
||||||
|
;board_build.partitions = partitions.csv
|
||||||
;monitor_port = /dev/cu.wchusbserial1420
|
;monitor_port = /dev/cu.wchusbserial1420
|
||||||
|
|
||||||
|
[env:deploy]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp-wrover-kit
|
||||||
|
framework = arduino
|
||||||
|
lib_deps = ${extra.lib_deps}
|
||||||
|
board_build.embed_txtfiles = src/index.html
|
||||||
|
board_build.partitions = partitions.csv
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
#include "main.h"
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "playlist.h"
|
#include "playlist.h"
|
||||||
#include "http_server.h"
|
#include "http_server.h"
|
||||||
|
#include "updater.h"
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
Controller::Controller(Player* p, PlaylistManager* playlist_manager) {
|
Controller::Controller(Player* p, PlaylistManager* playlist_manager) {
|
||||||
@ -34,7 +36,6 @@ void Controller::register_http_server(HTTPServer* h) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Controller::loop() {
|
void Controller::loop() {
|
||||||
TRACE("Controller::loop()...\n");
|
|
||||||
unsigned long now = millis();
|
unsigned long now = millis();
|
||||||
if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) {
|
if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) {
|
||||||
_check_rfid();
|
_check_rfid();
|
||||||
@ -50,7 +51,20 @@ void Controller::loop() {
|
|||||||
process_message(_cmd_queue);
|
process_message(_cmd_queue);
|
||||||
_cmd_queue = "";
|
_cmd_queue = "";
|
||||||
}
|
}
|
||||||
TRACE("Controller::loop() done.\n");
|
|
||||||
|
#ifdef OTA_UPDATE_URL
|
||||||
|
if (!player->is_playing() && _last_update_check_at < now && _last_update_check_at + OTA_CHECK_INTERVAL < now) {
|
||||||
|
Updater::run();
|
||||||
|
} else {
|
||||||
|
_last_update_check_at = now;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (!player->is_playing() && !WiFi.isConnected() && _last_wifi_try_at < now && _last_wifi_try_at + 5*60*1000 < now) {
|
||||||
|
wifi_connect();
|
||||||
|
} else {
|
||||||
|
_last_wifi_try_at = now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Controller::_get_rfid_card_uid() {
|
uint32_t Controller::_get_rfid_card_uid() {
|
||||||
@ -69,7 +83,7 @@ uint32_t Controller::_get_rfid_card_uid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Controller::_check_rfid() {
|
void Controller::_check_rfid() {
|
||||||
TRACE("check_rfid running...\n");
|
//TRACE("check_rfid running...\n");
|
||||||
MFRC522::StatusCode status;
|
MFRC522::StatusCode status;
|
||||||
if (_rfid_present) {
|
if (_rfid_present) {
|
||||||
byte buffer[2];
|
byte buffer[2];
|
||||||
@ -128,7 +142,8 @@ void Controller::_check_rfid() {
|
|||||||
if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december.
|
if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december.
|
||||||
pl->advent_shuffle(time.tm_mday);
|
pl->advent_shuffle(time.tm_mday);
|
||||||
} else {
|
} else {
|
||||||
// TODO
|
DEBUG("Album is in advent mode, but it isn't december (yet). Not playing.\n");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else if (data.indexOf("[random]") != -1 && pl->is_fresh()) {
|
} else if (data.indexOf("[random]") != -1 && pl->is_fresh()) {
|
||||||
pl->shuffle();
|
pl->shuffle();
|
||||||
@ -184,7 +199,7 @@ String Controller::_read_rfid_data() {
|
|||||||
case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break;
|
case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break;
|
||||||
default: INFO("Unknown PICC type %s\n", String(MFRC522::PICC_GetTypeName(type)).c_str());
|
default: INFO("Unknown PICC type %s\n", String(MFRC522::PICC_GetTypeName(type)).c_str());
|
||||||
}
|
}
|
||||||
|
sectors = 2; // Pretend we have only two sectors, so we read only sector #1.
|
||||||
int good_key_index = -1;
|
int good_key_index = -1;
|
||||||
for (uint8_t sector=1; sector<sectors; sector++) {
|
for (uint8_t sector=1; sector<sectors; sector++) {
|
||||||
uint8_t blocks = (sector < 32) ? 4 : 16;
|
uint8_t blocks = (sector < 32) ? 4 : 16;
|
||||||
@ -228,8 +243,6 @@ String Controller::_read_rfid_data() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Controller::_check_serial() {
|
void Controller::_check_serial() {
|
||||||
TRACE("check_serial running...\n");
|
|
||||||
|
|
||||||
if (Serial.available() > 0) {
|
if (Serial.available() > 0) {
|
||||||
char c = Serial.read();
|
char c = Serial.read();
|
||||||
Serial.printf("%c", c);
|
Serial.printf("%c", c);
|
||||||
@ -250,13 +263,8 @@ bool Controller::process_message(String cmd) {
|
|||||||
if (cmd.startsWith("play ")) {
|
if (cmd.startsWith("play ")) {
|
||||||
Playlist* p = pm->get_playlist_for_folder(cmd.substring(5));
|
Playlist* p = pm->get_playlist_for_folder(cmd.substring(5));
|
||||||
player->play(p);
|
player->play(p);
|
||||||
//} else if (cmd.equals("ls")) {
|
|
||||||
// _execute_command_ls("/");
|
|
||||||
//} else if (cmd.startsWith("ls ")) {
|
|
||||||
// _execute_command_ls(cmd.substring(3));
|
|
||||||
} else if (cmd.equals("play")) {
|
} else if (cmd.equals("play")) {
|
||||||
player->play();
|
player->play();
|
||||||
|
|
||||||
} else if (cmd.equals("stop")) {
|
} else if (cmd.equals("stop")) {
|
||||||
player->stop();
|
player->stop();
|
||||||
} else if (cmd.equals("help")) {
|
} else if (cmd.equals("help")) {
|
||||||
@ -289,6 +297,10 @@ bool Controller::process_message(String cmd) {
|
|||||||
String folder = rest.substring(idx + 1);
|
String folder = rest.substring(idx + 1);
|
||||||
pm->add_mapping(id, folder);
|
pm->add_mapping(id, folder);
|
||||||
send_playlist_manager_status();
|
send_playlist_manager_status();
|
||||||
|
#ifdef OTA_UPDATE_URL
|
||||||
|
} else if (cmd.equals("update")) {
|
||||||
|
Updater::run();
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
ERROR("Unknown command: %s\n", cmd.c_str());
|
ERROR("Unknown command: %s\n", cmd.c_str());
|
||||||
return false;
|
return false;
|
||||||
@ -317,8 +329,6 @@ void Controller::_execute_command_help() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Controller::_check_buttons() {
|
void Controller::_check_buttons() {
|
||||||
TRACE("check_buttons running...\n");
|
|
||||||
|
|
||||||
if (BTN_PREV() && _debounce_button(0)) {
|
if (BTN_PREV() && _debounce_button(0)) {
|
||||||
if (_state == NORMAL) {
|
if (_state == NORMAL) {
|
||||||
player->track_prev();
|
player->track_prev();
|
||||||
@ -362,6 +372,22 @@ String Controller::json() {
|
|||||||
rfid["data"] = _last_rfid_data;
|
rfid["data"] = _last_rfid_data;
|
||||||
json["uptime"] = millis() / 1000;
|
json["uptime"] = millis() / 1000;
|
||||||
json["free_heap"] = ESP.getFreeHeap();
|
json["free_heap"] = ESP.getFreeHeap();
|
||||||
|
JsonObject versions = json.createNestedObject("versions");
|
||||||
|
versions["ota"] = OTA_VERSION;
|
||||||
|
#ifdef VERSION
|
||||||
|
versions["release"] = VERSION;
|
||||||
|
#else
|
||||||
|
versions["release"] = "unknown";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
JsonObject wifi = json.createNestedObject("wifi");
|
||||||
|
if (WiFi.isConnected()) {
|
||||||
|
wifi["connected"] = true;
|
||||||
|
wifi["ssid"] = WiFi.SSID();
|
||||||
|
wifi["rssi"] = WiFi.RSSI();
|
||||||
|
} else {
|
||||||
|
wifi["connected"] = false;
|
||||||
|
}
|
||||||
return json.as<String>();
|
return json.as<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,9 @@ bool HTTPClientWrapper::_request(String method, String url, uint32_t offset, uin
|
|||||||
DEBUG("Unexpected HTTP return code %d. Cancelling.\n", status);
|
DEBUG("Unexpected HTTP return code %d. Cancelling.\n", status);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buffer_position = 0;
|
||||||
|
_buffer_length = 0;
|
||||||
|
|
||||||
_connected = true;
|
_connected = true;
|
||||||
_length = _http->getSize() + offset;
|
_length = _http->getSize() + offset;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#include "http_server.h"
|
#include "http_server.h"
|
||||||
|
#include "main.h"
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
#include <ESPmDNS.h>
|
#include <ESPmDNS.h>
|
||||||
#include <SPIFFS.h>
|
|
||||||
|
|
||||||
HTTPServer::HTTPServer(Player* p, Controller* c) {
|
HTTPServer::HTTPServer(Player* p, Controller* c) {
|
||||||
_player = p;
|
_player = p;
|
||||||
@ -11,14 +11,37 @@ HTTPServer::HTTPServer(Player* p, Controller* c) {
|
|||||||
_server->addHandler(ws);
|
_server->addHandler(ws);
|
||||||
ws->onEvent([&](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){this->_onEvent(server, client, type, arg, data, len);});
|
ws->onEvent([&](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){this->_onEvent(server, client, type, arg, data, len);});
|
||||||
|
|
||||||
_server->on("/", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(SPIFFS, "/index.html", "text/html");});
|
_server->on("/", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
_server->on("/upload", HTTP_POST, [](AsyncWebServerRequest* req) {req->send(200); }, ([&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){this->_handle_upload(request, filename, index, data, len, final);}));
|
req->send(200, "text/html", (const char*)file_index_html_start);
|
||||||
_server->on("/_mapping.txt", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "text/plain", _controller->pm->create_mapping_txt());});
|
});
|
||||||
_server->on("/player.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->player->json());});
|
_server->on("/upload", HTTP_POST, [](AsyncWebServerRequest* req) {
|
||||||
_server->on("/playlist_manager.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->pm->json());});
|
req->send(200);
|
||||||
_server->on("/controller.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->json());});
|
}, [&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||||
_server->on("/position.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->player->position_json());});
|
this->_handle_upload(request, filename, index, data, len, final);
|
||||||
_server->on("/cmd", HTTP_POST, [&](AsyncWebServerRequest *req) {req->send(200); }, NULL, [&](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {_controller->queue_command((char*)data);});
|
});
|
||||||
|
_server->on("/_mapping.txt", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200, "text/plain", _controller->pm->create_mapping_txt());
|
||||||
|
});
|
||||||
|
_server->on("/player.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200, "application/json", _controller->player->json());
|
||||||
|
});
|
||||||
|
_server->on("/playlist_manager.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200, "application/json", _controller->pm->json());
|
||||||
|
});
|
||||||
|
_server->on("/controller.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200, "application/json", _controller->json());
|
||||||
|
});
|
||||||
|
_server->on("/position.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||||
|
req->send(200, "application/json", _controller->player->position_json());
|
||||||
|
});
|
||||||
|
_server->on("/cmd", HTTP_POST, [&](AsyncWebServerRequest *req) {
|
||||||
|
if (req->hasParam("cmd", true)) {
|
||||||
|
_controller->queue_command(req->getParam("cmd", true)->value());
|
||||||
|
req->send(200);
|
||||||
|
} else {
|
||||||
|
req->send(400);
|
||||||
|
}
|
||||||
|
});
|
||||||
_server->begin();
|
_server->begin();
|
||||||
MDNS.addService("http", "tcp", 80);
|
MDNS.addService("http", "tcp", 80);
|
||||||
}
|
}
|
||||||
|
@ -151,6 +151,7 @@
|
|||||||
<h6>Actions</h6>
|
<h6>Actions</h6>
|
||||||
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reset_vs1053">Reset VS1053 chip</button>
|
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reset_vs1053">Reset VS1053 chip</button>
|
||||||
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reboot">Reboot ESMP3</button>
|
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reboot">Reboot ESMP3</button>
|
||||||
|
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_update">Check for and install update</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@ -187,7 +188,7 @@ update_playlist = function(data) {
|
|||||||
tr = $('<tr>').data('track', i);
|
tr = $('<tr>').data('track', i);
|
||||||
tr.append($('<td>').html(i + 1));
|
tr.append($('<td>').html(i + 1));
|
||||||
tr.append($('<td>').html(data.current_track==i ? '<i class="fa fa-play"></i>' : ''));
|
tr.append($('<td>').html(data.current_track==i ? '<i class="fa fa-play"></i>' : ''));
|
||||||
tr.append($('<td>').html(data.files[i].substr(data.files[i].title)));
|
tr.append($('<td>').html(data.files[i].title));
|
||||||
$('#track_list').append(tr);
|
$('#track_list').append(tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,6 +289,7 @@ $(function() {
|
|||||||
$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); });
|
$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); });
|
||||||
$('#button_reset_vs1053').click(function(e) { ws.send("reset_vs1053"); $('#settingsModal').modal('hide'); });
|
$('#button_reset_vs1053').click(function(e) { ws.send("reset_vs1053"); $('#settingsModal').modal('hide'); });
|
||||||
$('#button_reboot').click(function(e) { ws.send("reboot"); $('#settingsModal').modal('hide'); });
|
$('#button_reboot').click(function(e) { ws.send("reboot"); $('#settingsModal').modal('hide'); });
|
||||||
|
$('#button_update').click(function(e) { ws.send("update"); $('#settingsModal').modal('hide'); });
|
||||||
$('#button_url_open').click(function(e) { ws.send("play " + $('#input_url').val()); $('#openModal').modal('hide');});
|
$('#button_url_open').click(function(e) { ws.send("play " + $('#input_url').val()); $('#openModal').modal('hide');});
|
||||||
$('#button_add_mapping').click(function(e) {
|
$('#button_add_mapping').click(function(e) {
|
||||||
$('#settingsModal').modal('hide');
|
$('#settingsModal').modal('hide');
|
93
src/main.cpp
93
src/main.cpp
@ -2,14 +2,16 @@
|
|||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
#include <WiFiMulti.h>
|
||||||
#include <ESPmDNS.h>
|
#include <ESPmDNS.h>
|
||||||
#include <SPIFFS.h>
|
#include "main.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "player.h"
|
#include "player.h"
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
#include "http_server.h"
|
#include "http_server.h"
|
||||||
#include "playlist_manager.h"
|
#include "playlist_manager.h"
|
||||||
|
#include "updater.h"
|
||||||
|
|
||||||
Controller* controller;
|
Controller* controller;
|
||||||
Player* player;
|
Player* player;
|
||||||
@ -18,29 +20,53 @@ HTTPServer* http_server;
|
|||||||
|
|
||||||
uint8_t SPIMaster::state = 0;
|
uint8_t SPIMaster::state = 0;
|
||||||
|
|
||||||
bool connect_to_wifi(String ssid, String pass) {
|
void wifi_connect() {
|
||||||
TRACE("Connecting to wifi \"%s\"...\n", ssid.c_str());
|
INFO("Connecting to WiFi...\n");
|
||||||
WiFi.mode(WIFI_AP_STA);
|
WiFiMulti wifi;
|
||||||
WiFi.begin(ssid.c_str(), pass.c_str());
|
SPIMaster::select_sd();
|
||||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
if (SD.exists("/_wifis.txt")) {
|
||||||
DEBUG("Could not connect to wifi \"%s\".\n", ssid.c_str());
|
DEBUG("Reading /_wifis.txt\n");
|
||||||
return false;
|
File f = SD.open("/_wifis.txt", "r");
|
||||||
|
while (String line = f.readStringUntil('\n')) {
|
||||||
|
if (line.length()==0) {
|
||||||
|
break;
|
||||||
|
} else if (line.startsWith("#") || line.indexOf('=')==-1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String ssid = line.substring(0, line.indexOf('='));
|
||||||
|
String pass = line.substring(line.indexOf('=')+1);
|
||||||
|
wifi.addAP(ssid.c_str(), pass.c_str());
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
} else {
|
} else {
|
||||||
INFO("Connected to \"%s\". IP address: %s\n", ssid.c_str(), WiFi.localIP().toString().c_str());
|
File f = SD.open("/_wifis.txt", "w");
|
||||||
|
f.print("# WiFi definitions. Syntax: <SSID>=<PASS>. Lines starting with # are ignored. Example:\n# My WiFi=VerySecretPassword\n");
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
SPIMaster::select_sd(false);
|
||||||
|
|
||||||
|
#if defined(WIFI_SSID) and defined(WIFI_PASS)
|
||||||
|
wifi.addAP(WIFI_SSID, WIFI_PASS);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (wifi.run() == WL_CONNECTED) {
|
||||||
|
DEBUG("Connected to WiFi \"%s\".\n", WiFi.SSID().c_str());
|
||||||
|
} else {
|
||||||
|
DEBUG("No WiFi connection!\n");
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
delay(500);
|
// Small delay to give the Serial console a bit of time to connect.
|
||||||
Serial.begin(74880);
|
delay(1000);
|
||||||
|
Serial.begin(115200);
|
||||||
Serial.println("Starting...");
|
Serial.println("Starting...");
|
||||||
Serial.println("Started.");
|
Serial.println("Started.");
|
||||||
INFO("Starting.\n");
|
INFO("Starting.\n");
|
||||||
#ifdef VERSION
|
#ifdef VERSION
|
||||||
INFO("ESMP3 version %s\n", VERSION);
|
INFO("ESMP3 version %s (OTA_VERSION %d)\n", VERSION, OTA_VERSION);
|
||||||
#else
|
#else
|
||||||
INFO("ESMP3, version unknown\n");
|
INFO("ESMP3, version unknown (OTA_VERSION %d)\n", OTA_VERSION);
|
||||||
#endif
|
#endif
|
||||||
INFO("Initializing...\n");
|
INFO("Initializing...\n");
|
||||||
|
|
||||||
@ -60,9 +86,6 @@ void setup() {
|
|||||||
}
|
}
|
||||||
spi->select_sd(false);
|
spi->select_sd(false);
|
||||||
|
|
||||||
DEBUG("Starting SPIFFS...\n");
|
|
||||||
SPIFFS.begin(true);
|
|
||||||
|
|
||||||
DEBUG("Initializing PlaylistManager...\n");
|
DEBUG("Initializing PlaylistManager...\n");
|
||||||
pm = new PlaylistManager();
|
pm = new PlaylistManager();
|
||||||
|
|
||||||
@ -71,35 +94,7 @@ void setup() {
|
|||||||
controller = new Controller(player, pm);
|
controller = new Controller(player, pm);
|
||||||
INFO("Player and controller initialized.\n");
|
INFO("Player and controller initialized.\n");
|
||||||
|
|
||||||
bool connected = false;
|
wifi_connect();
|
||||||
INFO("Connecting to WiFi...\n");
|
|
||||||
SPIMaster::select_sd();
|
|
||||||
if (SD.exists("/_wifis.txt")) {
|
|
||||||
DEBUG("Reading /_wifis.txt\n");
|
|
||||||
File f = SD.open("/_wifis.txt", "r");
|
|
||||||
while (String line = f.readStringUntil('\n')) {
|
|
||||||
if (line.length()==0 || line.startsWith("#") || line.indexOf('=')==-1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String ssid = line.substring(0, line.indexOf('='));
|
|
||||||
String pass = line.substring(line.indexOf('=')+1);
|
|
||||||
connected = connect_to_wifi(ssid, pass);
|
|
||||||
if (connected) break;
|
|
||||||
}
|
|
||||||
f.close();
|
|
||||||
} else {
|
|
||||||
File f = SD.open("/_wifis.txt", "w");
|
|
||||||
f.print("# WiFi definitions. Syntax: <SSID>=<PASS>. Lines starting with # are ignored. Example:\n# My WiFi=VerySecretPassword\n");
|
|
||||||
f.close();
|
|
||||||
}
|
|
||||||
SPIMaster::select_sd(false);
|
|
||||||
if (!connected) {
|
|
||||||
DEBUG("Trying hardcoded WiFi data...\n");
|
|
||||||
connected = connect_to_wifi(WIFI_SSID, WIFI_PASS);
|
|
||||||
}
|
|
||||||
if (!connected) {
|
|
||||||
INFO("No WiFi connection!\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
MDNS.begin("esmp3");
|
MDNS.begin("esmp3");
|
||||||
|
|
||||||
@ -119,6 +114,12 @@ void setup() {
|
|||||||
INFO("Could not fetch current time via NTP.\n");
|
INFO("Could not fetch current time via NTP.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef VERSION
|
||||||
|
INFO("ESMP3 version %s (OTA_VERSION %d)\n", VERSION, OTA_VERSION);
|
||||||
|
#else
|
||||||
|
INFO("ESMP3, version unknown (OTA_VERSION %d)\n", OTA_VERSION);
|
||||||
|
#endif
|
||||||
|
|
||||||
INFO("Initialization completed.\n");
|
INFO("Initialization completed.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,14 +464,14 @@ void Player::set_volume(uint8_t vol, bool save) {
|
|||||||
|
|
||||||
void Player::vol_up() {
|
void Player::vol_up() {
|
||||||
if (!is_playing()) return;
|
if (!is_playing()) return;
|
||||||
uint8_t vol = _volume + VOLUME_STEP;
|
uint16_t vol = _volume + VOLUME_STEP;
|
||||||
if (vol > VOLUME_MAX) vol=VOLUME_MAX;
|
if (vol > VOLUME_MAX) vol=VOLUME_MAX;
|
||||||
set_volume(vol);
|
set_volume(vol);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::vol_down() {
|
void Player::vol_down() {
|
||||||
if (!is_playing()) return;
|
if (!is_playing()) return;
|
||||||
uint8_t vol = _volume - VOLUME_STEP;
|
int16_t vol = _volume - VOLUME_STEP;
|
||||||
if (vol < VOLUME_MIN) vol=VOLUME_MIN;
|
if (vol < VOLUME_MIN) vol=VOLUME_MIN;
|
||||||
set_volume(vol);
|
set_volume(vol);
|
||||||
}
|
}
|
||||||
@ -535,7 +535,10 @@ bool Player::play() {
|
|||||||
if (_current_playlist == NULL) return false;
|
if (_current_playlist == NULL) return false;
|
||||||
if (_current_playlist->get_file_count()==0) return false;
|
if (_current_playlist->get_file_count()==0) return false;
|
||||||
_current_playlist->start();
|
_current_playlist->start();
|
||||||
String file = _current_playlist->get_current_file();
|
String file;
|
||||||
|
if (!_current_playlist->get_current_file(&file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
uint32_t position = _current_playlist->get_position();
|
uint32_t position = _current_playlist->get_position();
|
||||||
_state = playing;
|
_state = playing;
|
||||||
_play_file(file, position);
|
_play_file(file, position);
|
||||||
@ -610,7 +613,8 @@ void Player::stop(bool turn_speaker_off) {
|
|||||||
if (_state != playing) return;
|
if (_state != playing) return;
|
||||||
INFO("Stopping...\n");
|
INFO("Stopping...\n");
|
||||||
_current_playlist->set_position(_current_play_position);
|
_current_playlist->set_position(_current_play_position);
|
||||||
|
_controller->pm->persist(_current_playlist);
|
||||||
|
|
||||||
_state = stopping;
|
_state = stopping;
|
||||||
_stop_delay = 0;
|
_stop_delay = 0;
|
||||||
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
|
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
|
||||||
|
@ -7,7 +7,9 @@
|
|||||||
#include <TinyXML.h>
|
#include <TinyXML.h>
|
||||||
|
|
||||||
Playlist::Playlist(String path) {
|
Playlist::Playlist(String path) {
|
||||||
|
_path = path;
|
||||||
if (path.startsWith("/")) {
|
if (path.startsWith("/")) {
|
||||||
|
persistence = PERSIST_TEMPORARY;
|
||||||
_add_path(path);
|
_add_path(path);
|
||||||
} else if (path.startsWith("http")) {
|
} else if (path.startsWith("http")) {
|
||||||
_examine_http_url(path);
|
_examine_http_url(path);
|
||||||
@ -44,7 +46,7 @@ void Playlist::_add_path(String path) {
|
|||||||
ext.equals(".mpa"))) {
|
ext.equals(".mpa"))) {
|
||||||
TRACE(" Adding entry %s\n", entry.name());
|
TRACE(" Adding entry %s\n", entry.name());
|
||||||
String title = filename.substring(0, filename.length() - 4);
|
String title = filename.substring(0, filename.length() - 4);
|
||||||
_files.push_back({.filename=entry.name(), .title=title});
|
_files.push_back({.filename=entry.name(), .title=title, .id=String(_files.size())});
|
||||||
bool non_ascii_chars = false;
|
bool non_ascii_chars = false;
|
||||||
for(int i=0; i<filename.length(); i++) {
|
for(int i=0; i<filename.length(); i++) {
|
||||||
char c = filename.charAt(i);
|
char c = filename.charAt(i);
|
||||||
@ -75,12 +77,16 @@ void Playlist::_examine_http_url(String url) {
|
|||||||
String ct = http->getContentType();
|
String ct = http->getContentType();
|
||||||
DEBUG("Content-Type is %s.\n", ct.c_str());
|
DEBUG("Content-Type is %s.\n", ct.c_str());
|
||||||
if (ct.startsWith("audio/x-mpegurl")) {
|
if (ct.startsWith("audio/x-mpegurl")) {
|
||||||
|
|
||||||
_parse_m3u(http);
|
_parse_m3u(http);
|
||||||
} else if (ct.startsWith("audio/")) {
|
} else if (ct.startsWith("audio/")) {
|
||||||
_files.push_back({.filename=url, .title=url});
|
persistence = PERSIST_NONE;
|
||||||
|
_files.push_back({.filename=url, .title=url, .id="none"});
|
||||||
} else if (ct.startsWith("application/rss+xml")) {
|
} else if (ct.startsWith("application/rss+xml")) {
|
||||||
|
persistence = PERSIST_PERMANENTLY;
|
||||||
_parse_rss(http);
|
_parse_rss(http);
|
||||||
} else if (ct.startsWith("application/pls+xml")) {
|
} else if (ct.startsWith("application/pls+xml")) {
|
||||||
|
persistence = PERSIST_PERMANENTLY;
|
||||||
_parse_pls(http);
|
_parse_pls(http);
|
||||||
} else {
|
} else {
|
||||||
ERROR("Unknown content type %s.\n", ct.c_str());
|
ERROR("Unknown content type %s.\n", ct.c_str());
|
||||||
@ -95,6 +101,7 @@ String xml_title = "";
|
|||||||
String xml_album_title = "";
|
String xml_album_title = "";
|
||||||
String xml_url = "";
|
String xml_url = "";
|
||||||
String xml_enclosure_url = "";
|
String xml_enclosure_url = "";
|
||||||
|
String xml_guid = "";
|
||||||
bool xml_enclosure_is_audio = false;
|
bool xml_enclosure_is_audio = false;
|
||||||
|
|
||||||
void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t dataLen) {
|
void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t dataLen) {
|
||||||
@ -106,8 +113,11 @@ void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t
|
|||||||
} else if (tag.endsWith("/item") && (status & STATUS_START_TAG)) {
|
} else if (tag.endsWith("/item") && (status & STATUS_START_TAG)) {
|
||||||
xml_title = "";
|
xml_title = "";
|
||||||
xml_url = "";
|
xml_url = "";
|
||||||
|
xml_guid = "";
|
||||||
} else if (tag.endsWith("/item/title") && (status & STATUS_TAG_TEXT)) {
|
} else if (tag.endsWith("/item/title") && (status & STATUS_TAG_TEXT)) {
|
||||||
xml_title = String(data);
|
xml_title = String(data);
|
||||||
|
} else if (tag.endsWith("/item/guid") && (status & STATUS_TAG_TEXT)) {
|
||||||
|
xml_guid = data;
|
||||||
//} else if (xml_last_tag.endsWith("/item/enclosure") && (status & STATUS_ATTR_TEXT)) {
|
//} else if (xml_last_tag.endsWith("/item/enclosure") && (status & STATUS_ATTR_TEXT)) {
|
||||||
// DEBUG("tag: %s, data: %s\n", tag.c_str(), data);
|
// DEBUG("tag: %s, data: %s\n", tag.c_str(), data);
|
||||||
} else if (xml_last_tag.endsWith("/item/enclosure") && tag.equals("type") && (status & STATUS_ATTR_TEXT) && String(data).indexOf("audio/")>=0) {
|
} else if (xml_last_tag.endsWith("/item/enclosure") && tag.equals("type") && (status & STATUS_ATTR_TEXT) && String(data).indexOf("audio/")>=0) {
|
||||||
@ -125,8 +135,9 @@ void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t
|
|||||||
xml_enclosure_url = "";
|
xml_enclosure_url = "";
|
||||||
} else if (tag.endsWith("/item") && (status & STATUS_END_TAG)) {
|
} else if (tag.endsWith("/item") && (status & STATUS_END_TAG)) {
|
||||||
if (xml_title.length()>0 && xml_url.length()>0) {
|
if (xml_title.length()>0 && xml_url.length()>0) {
|
||||||
|
if (xml_files_ptr->size() > 20) return;
|
||||||
DEBUG("Adding playlist entry: '%s' => '%s'\n", xml_title.c_str(), xml_url.c_str());
|
DEBUG("Adding playlist entry: '%s' => '%s'\n", xml_title.c_str(), xml_url.c_str());
|
||||||
xml_files_ptr->push_back({xml_url, xml_title});
|
xml_files_ptr->insert(xml_files_ptr->begin(), {.filename=xml_url, .title=xml_title, .id=xml_guid});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,6 +159,7 @@ void Playlist::_parse_rss(HTTPClientWrapper* http) {
|
|||||||
while ((i = http->read()) >= 0) {
|
while ((i = http->read()) >= 0) {
|
||||||
xml.processChar(i);
|
xml.processChar(i);
|
||||||
}
|
}
|
||||||
|
_current_track = _files.size()-1;
|
||||||
xml_files_ptr = NULL;
|
xml_files_ptr = NULL;
|
||||||
if (xml_album_title.length()>0) {
|
if (xml_album_title.length()>0) {
|
||||||
_title = xml_album_title;
|
_title = xml_album_title;
|
||||||
@ -180,7 +192,7 @@ void Playlist::_parse_m3u(HTTPClientWrapper* http) {
|
|||||||
}
|
}
|
||||||
} else if (line.startsWith("http")) {
|
} else if (line.startsWith("http")) {
|
||||||
if (title.length()==0) title = line;
|
if (title.length()==0) title = line;
|
||||||
_files.push_back({.filename=line, .title=title});
|
_files.push_back({.filename=line, .title=title, .id="none"});
|
||||||
title = "";
|
title = "";
|
||||||
}
|
}
|
||||||
line = "";
|
line = "";
|
||||||
@ -222,7 +234,7 @@ void Playlist::_parse_pls(HTTPClientWrapper* http) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (title.length()>0 && url.length()>0) {
|
if (title.length()>0 && url.length()>0) {
|
||||||
_files.push_back({.filename=url, .title=title});
|
_files.push_back({.filename=url, .title=title, .id="none"});
|
||||||
last_index = -1;
|
last_index = -1;
|
||||||
title = "";
|
title = "";
|
||||||
url = "";
|
url = "";
|
||||||
@ -231,6 +243,10 @@ void Playlist::_parse_pls(HTTPClientWrapper* http) {
|
|||||||
// don't close http at the end
|
// don't close http at the end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String Playlist::path() {
|
||||||
|
return _path;
|
||||||
|
}
|
||||||
|
|
||||||
uint16_t Playlist::get_file_count() {
|
uint16_t Playlist::get_file_count() {
|
||||||
return _files.size();
|
return _files.size();
|
||||||
}
|
}
|
||||||
@ -274,6 +290,15 @@ bool Playlist::set_track(uint8_t track) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Playlist::set_track_by_id(String id) {
|
||||||
|
for (int i=0; i<_files.size(); i++) {
|
||||||
|
if (id.equals(_files[i].id)) {
|
||||||
|
set_track(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Playlist::track_restart() {
|
void Playlist::track_restart() {
|
||||||
_position = 0;
|
_position = 0;
|
||||||
}
|
}
|
||||||
@ -294,12 +319,19 @@ void Playlist::shuffle(uint8_t random_offset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::advent_shuffle(uint8_t day) {
|
void Playlist::advent_shuffle(uint8_t day) {
|
||||||
if (day > 24) day = 24;
|
TRACE("advent_shuffle running...\n");
|
||||||
|
|
||||||
if (day > _files.size()) return;
|
// Not enough songs till the current day? Play all songs in the default order.
|
||||||
|
if (day > _files.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are in the "different playlist every day" mode. So we don't persist it in order to not miss changes.
|
||||||
|
persistence = PERSIST_NONE;
|
||||||
|
|
||||||
|
|
||||||
_files.insert(_files.begin(), _files[day - 1]);
|
_files.insert(_files.begin(), _files[day - 1]);
|
||||||
_files.erase(_files.begin() + day - 1, _files.end());
|
_files.erase(_files.begin() + day, _files.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::reset() {
|
void Playlist::reset() {
|
||||||
@ -310,8 +342,20 @@ void Playlist::reset() {
|
|||||||
_started = false;
|
_started = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String Playlist::get_current_file() {
|
String Playlist::get_current_track_id() {
|
||||||
return _files[_current_track].filename;
|
if (_current_track > _files.size()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return _files[_current_track].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Playlist::get_current_file(String* dst) {
|
||||||
|
if (_current_track > _files.size()) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
dst->concat(_files[_current_track].filename);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Playlist::get_position() {
|
uint32_t Playlist::get_position() {
|
||||||
@ -340,6 +384,7 @@ void Playlist::json(JsonObject json) {
|
|||||||
JsonObject o = files.createNestedObject();
|
JsonObject o = files.createNestedObject();
|
||||||
o["filename"] = entry.filename;
|
o["filename"] = entry.filename;
|
||||||
o["title"] = entry.title;
|
o["title"] = entry.title;
|
||||||
|
o["id"] = entry.id;
|
||||||
}
|
}
|
||||||
json["current_track"] = _current_track;
|
json["current_track"] = _current_track;
|
||||||
json["has_track_next"] = has_track_next();
|
json["has_track_next"] = has_track_next();
|
||||||
|
@ -104,10 +104,40 @@ Playlist* PlaylistManager::get_playlist_for_id(String id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
|
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
|
||||||
|
Playlist* p;
|
||||||
if (!_playlists.count(folder)) {
|
if (!_playlists.count(folder)) {
|
||||||
_playlists[folder] = new Playlist(folder);
|
p = new Playlist(folder);
|
||||||
|
_playlists[folder] = p;
|
||||||
|
if (p->persistence == PERSIST_PERMANENTLY) {
|
||||||
|
String search = folder;
|
||||||
|
search += "=";
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
if (SD.exists("/_positions.txt")) {
|
||||||
|
File f = SD.open("/_positions.txt", "r");
|
||||||
|
while (true) {
|
||||||
|
String s = f.readStringUntil('\n');
|
||||||
|
if (s.length()==0) break;
|
||||||
|
if (s.startsWith(search)) {
|
||||||
|
s = s.substring(search.length());
|
||||||
|
int idx = s.indexOf(',');
|
||||||
|
String title_index = s.substring(0, idx);
|
||||||
|
uint32_t position = s.substring(idx+1).toInt();
|
||||||
|
p->set_track_by_id(title_index);
|
||||||
|
p->set_position(position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
SPIMaster::select_sd(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p = _playlists[folder];
|
||||||
|
if (p->persistence == PERSIST_NONE) {
|
||||||
|
p->reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return _playlists[folder];
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaylistManager::dump_ids() {
|
void PlaylistManager::dump_ids() {
|
||||||
@ -164,3 +194,46 @@ String PlaylistManager::create_mapping_txt() {
|
|||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlaylistManager::persist(Playlist* p) {
|
||||||
|
if (p->persistence == PERSIST_NONE) {
|
||||||
|
_playlists.erase(p->path());
|
||||||
|
return;
|
||||||
|
} else if (p->persistence == PERSIST_PERMANENTLY) {
|
||||||
|
|
||||||
|
String search = p->path();
|
||||||
|
search += '=';
|
||||||
|
|
||||||
|
bool old_file_existed = false;
|
||||||
|
|
||||||
|
SPIMaster::select_sd();
|
||||||
|
if (SD.exists("_positions.txt")) {
|
||||||
|
SD.rename("/_positions.txt", "/_positions.temp.txt");
|
||||||
|
old_file_existed = true;
|
||||||
|
}
|
||||||
|
File dst = SD.open("/_positions.txt", "w");
|
||||||
|
|
||||||
|
if (old_file_existed) {
|
||||||
|
File src = SD.open("/_positions.temp.txt", "r");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
String line = src.readStringUntil('\n');
|
||||||
|
line.trim();
|
||||||
|
if (line.startsWith(search)) continue;
|
||||||
|
dst.println(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
src.close();
|
||||||
|
SD.remove("/_positions.temp.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
dst.print(search);
|
||||||
|
dst.print(p->get_current_track_id());
|
||||||
|
dst.print(',');
|
||||||
|
dst.println(p->get_position());
|
||||||
|
dst.close();
|
||||||
|
SPIMaster::select_sd(false);
|
||||||
|
|
||||||
|
_playlists.erase(p->path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
96
src/updater.cpp
Normal file
96
src/updater.cpp
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <Update.h>
|
||||||
|
#include "config.h"
|
||||||
|
#include "updater.h"
|
||||||
|
#include "http_client_wrapper.h"
|
||||||
|
|
||||||
|
void Updater::run() {
|
||||||
|
DEBUG("Updater is running...\n");
|
||||||
|
HTTPClientWrapper* http = new HTTPClientWrapper();
|
||||||
|
DEBUG("Requesting update info...\n");
|
||||||
|
bool result = http->get(OTA_UPDATE_URL);
|
||||||
|
if (!result) {
|
||||||
|
ERROR("Updater failed requesting %s.\n", OTA_UPDATE_URL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String line_str = "";
|
||||||
|
if (!read_line(&line_str, http, "VERSION")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint16_t version = line_str.toInt();
|
||||||
|
if (version==0) {
|
||||||
|
ERROR("Could not parse version number.\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DEBUG("Found version %d. My version is %d.\n", version, OTA_VERSION);
|
||||||
|
if (version <= OTA_VERSION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String image_path = "";
|
||||||
|
if (!read_line(&image_path, http, "IMAGE_PATH")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String image_md5 = "";
|
||||||
|
if (!read_line(&image_md5, http, "IMAGE_MD5")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
http->close();
|
||||||
|
delete http;
|
||||||
|
|
||||||
|
if(do_update(U_FLASH, image_path, image_md5)) {
|
||||||
|
DEBUG("Update done. Rebooting...\n");
|
||||||
|
} else {
|
||||||
|
DEBUG("Update failed. Rebooting...\n");
|
||||||
|
}
|
||||||
|
delay(1000);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Updater::read_line(String* dst, HTTPClientWrapper* http, String expected_key) {
|
||||||
|
expected_key += "=";
|
||||||
|
String line = http->readUntil("\n");
|
||||||
|
if (!line.startsWith(expected_key)) {
|
||||||
|
ERROR("Expected line start with '%s', but it started with '%s'.\n", expected_key.c_str(), line.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
line = line.substring(expected_key.length());
|
||||||
|
line.trim();
|
||||||
|
dst->concat(line);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Updater::do_update(int command, String url, String expected_md5) {
|
||||||
|
HTTPClientWrapper* http = new HTTPClientWrapper();
|
||||||
|
bool result = http->get(url);
|
||||||
|
if (!result) {
|
||||||
|
ERROR("Updater failed requesting %s.\n", url.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Update.begin(http->getSize(), command);
|
||||||
|
if (!result) {
|
||||||
|
ERROR("Update could not be started.\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Update.setMD5(expected_md5.c_str());
|
||||||
|
uint8_t buf[512];
|
||||||
|
uint16_t len;
|
||||||
|
while((len = http->read(buf, 512))) {
|
||||||
|
Update.write(buf, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
http->close();
|
||||||
|
delete http;
|
||||||
|
|
||||||
|
result = Update.end();
|
||||||
|
if (!result) {
|
||||||
|
const char* error = Update.errorString();
|
||||||
|
ERROR("Writing the update failed. The error was: %s\n", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user