diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..6120bc0 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,3 @@ +# Deploying a new version + +* Use `deploy.sh`. diff --git a/README.md b/README.md index 202c904..b1c51b2 100644 --- a/README.md +++ b/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 24 followed by tracks 1-23. So your kid will get the "daily track" 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:///ws`. +* Through the serial console using an USB cable. +* Via HTTP POST request to `http:///cmd`, having the + command in the variable `cmd`. + +Supported commands are: +| Command | Action | +|---------|--------| +| `play ` | 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=` | Sets the volume to X (0-255). | +| `track_prev` | Starts the previous track, if available. | +| `track_next` | Starts the next track, if available. | +| `track=` | Starts playing track no. X of the currently playing album. | +| `reset_vs1053` | Resets the VS1053 audio chip. | +| `reboot` | Reboots ESMP3. | +| `add_mapping==` | Adds a mapping between RFID card and path +. See `play` for valid path formats. | +| `update` | Runs an update check. | diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..d5c9a27 --- /dev/null +++ b/deploy.sh @@ -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 diff --git a/include/config.sample.h b/include/config.sample.h index d13fff6..05fe964 100644 --- a/include/config.sample.h +++ b/include/config.sample.h @@ -1,13 +1,21 @@ #pragma once #include +// 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_TRACE #define FTP_DEBUG #define DELAY_AFTER_DEBUG_AND_TRACE 0 -#define WIFI_SSID "---CHANGEME---" -#define WIFI_PASS "---CHANGEME---" +// Here you can define WiFi data to use. But actually, the better way to do +// this is by using /_wifi.txt on the sd card. +//#define WIFI_SSID "---CHANGEME---" +//#define WIFI_PASS "---CHANGEME---" #define VS1053_SLEEP_DELAY 5000 #define POSITION_SEND_INTERVAL 5000 diff --git a/include/controller.h b/include/controller.h index 00ce87e..3989caf 100644 --- a/include/controller.h +++ b/include/controller.h @@ -10,6 +10,8 @@ class Controller; #include "playlist.h" #include "playlist_manager.h" #include "http_server.h" + +#undef DEPRECATED #include enum ControllerState { NORMAL, LOCKING, LOCKED }; @@ -32,6 +34,8 @@ private: unsigned long _last_rfid_scan_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 _cmd_queue = ""; void _execute_command_ls(String path); diff --git a/include/main.h b/include/main.h new file mode 100644 index 0000000..0117185 --- /dev/null +++ b/include/main.h @@ -0,0 +1,5 @@ +#pragma once + +void wifi_connect(); + +extern const uint8_t file_index_html_start[] asm("_binary_src_index_html_start"); diff --git a/include/playlist.h b/include/playlist.h index 0da83e6..38e1f97 100644 --- a/include/playlist.h +++ b/include/playlist.h @@ -4,9 +4,16 @@ #include #include "http_client_wrapper.h" +enum PlaylistPersistence { + PERSIST_NONE, + PERSIST_TEMPORARY, + PERSIST_PERMANENTLY +}; + struct PlaylistEntry { String filename; String title; + String id; bool operator<(PlaylistEntry p) { return title < p.title; } }; @@ -19,12 +26,14 @@ private: bool _shuffled = false; std::vector _files; String _title = ""; + String _path; void _add_path(String path); void _examine_http_url(String url); void _parse_rss(HTTPClientWrapper* http); void _parse_m3u(HTTPClientWrapper* http); void _parse_pls(HTTPClientWrapper* http); public: + PlaylistPersistence persistence = PERSIST_TEMPORARY; Playlist(String path); void start(); uint16_t get_file_count(); @@ -34,9 +43,12 @@ public: bool track_prev(); void track_restart(); bool set_track(uint8_t track); + void set_track_by_id(String id); void reset(); + String path(); bool is_empty(); - String get_current_file(); + bool get_current_file(String* dst); + String get_current_track_id(); uint32_t get_position(); void set_position(uint32_t p); void shuffle(uint8_t random_offset=0); diff --git a/include/playlist_manager.h b/include/playlist_manager.h index 920728c..42c88a0 100644 --- a/include/playlist_manager.h +++ b/include/playlist_manager.h @@ -20,4 +20,5 @@ public: String json(); bool add_mapping(String id, String folder); String create_mapping_txt(); + void persist(Playlist* p); }; diff --git a/include/updater.h b/include/updater.h new file mode 100644 index 0000000..9d5a3a8 --- /dev/null +++ b/include/updater.h @@ -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); +}; diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..5ed782b --- /dev/null +++ b/partitions.csv @@ -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, diff --git a/platformio.ini b/platformio.ini index a3cfd34..9f26dbb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -8,16 +8,33 @@ ; Please visit documentation for the other options and examples ; 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] platform = espressif32 board = esp-wrover-kit framework = arduino upload_speed = 512000 build_flags=!./build_version.sh -lib_deps = MFRC522 - https://github.com/me-no-dev/ESPAsyncWebServer.git - ArduinoJSON - 6691 ; TinyXML +lib_deps = ${extra.lib_deps} 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 + +[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 diff --git a/src/controller.cpp b/src/controller.cpp index 1326620..5a30cbb 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -1,8 +1,10 @@ #include "controller.h" +#include "main.h" #include "spi_master.h" #include "config.h" #include "playlist.h" #include "http_server.h" +#include "updater.h" #include Controller::Controller(Player* p, PlaylistManager* playlist_manager) { @@ -34,7 +36,6 @@ void Controller::register_http_server(HTTPServer* h) { } void Controller::loop() { - TRACE("Controller::loop()...\n"); unsigned long now = millis(); if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) { _check_rfid(); @@ -50,7 +51,20 @@ void Controller::loop() { process_message(_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() { @@ -69,7 +83,7 @@ uint32_t Controller::_get_rfid_card_uid() { } void Controller::_check_rfid() { - TRACE("check_rfid running...\n"); + //TRACE("check_rfid running...\n"); MFRC522::StatusCode status; if (_rfid_present) { 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. pl->advent_shuffle(time.tm_mday); } 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()) { pl->shuffle(); @@ -184,7 +199,7 @@ String Controller::_read_rfid_data() { case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break; 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; for (uint8_t sector=1; sector 0) { char c = Serial.read(); Serial.printf("%c", c); @@ -250,13 +263,8 @@ bool Controller::process_message(String cmd) { if (cmd.startsWith("play ")) { Playlist* p = pm->get_playlist_for_folder(cmd.substring(5)); 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")) { player->play(); - } else if (cmd.equals("stop")) { player->stop(); } else if (cmd.equals("help")) { @@ -289,6 +297,10 @@ bool Controller::process_message(String cmd) { String folder = rest.substring(idx + 1); pm->add_mapping(id, folder); send_playlist_manager_status(); + #ifdef OTA_UPDATE_URL + } else if (cmd.equals("update")) { + Updater::run(); + #endif } else { ERROR("Unknown command: %s\n", cmd.c_str()); return false; @@ -317,8 +329,6 @@ void Controller::_execute_command_help() { } void Controller::_check_buttons() { - TRACE("check_buttons running...\n"); - if (BTN_PREV() && _debounce_button(0)) { if (_state == NORMAL) { player->track_prev(); @@ -362,6 +372,22 @@ String Controller::json() { rfid["data"] = _last_rfid_data; json["uptime"] = millis() / 1000; 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(); } diff --git a/src/http_client_wrapper.cpp b/src/http_client_wrapper.cpp index c33203c..bb1a90a 100644 --- a/src/http_client_wrapper.cpp +++ b/src/http_client_wrapper.cpp @@ -72,6 +72,9 @@ bool HTTPClientWrapper::_request(String method, String url, uint32_t offset, uin DEBUG("Unexpected HTTP return code %d. Cancelling.\n", status); return false; } + + _buffer_position = 0; + _buffer_length = 0; _connected = true; _length = _http->getSize() + offset; diff --git a/src/http_server.cpp b/src/http_server.cpp index 4d8ac1b..087d9f7 100644 --- a/src/http_server.cpp +++ b/src/http_server.cpp @@ -1,7 +1,7 @@ #include "http_server.h" +#include "main.h" #include "spi_master.h" #include -#include HTTPServer::HTTPServer(Player* p, Controller* c) { _player = p; @@ -11,14 +11,37 @@ HTTPServer::HTTPServer(Player* p, Controller* c) { _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);}); - _server->on("/", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(SPIFFS, "/index.html", "text/html");}); - _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);})); - _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) {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("/", HTTP_GET, [&](AsyncWebServerRequest* req) { + req->send(200, "text/html", (const char*)file_index_html_start); + }); + _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); + }); + _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(); MDNS.addService("http", "tcp", 80); } diff --git a/data/index.html b/src/index.html similarity index 97% rename from data/index.html rename to src/index.html index 8b1c53f..5240c4c 100644 --- a/data/index.html +++ b/src/index.html @@ -151,6 +151,7 @@
Actions
+