Started rewrite for using an I2S amplifier with less unnecessary features.

This commit is contained in:
Fabian Schlenz 2022-08-19 10:37:03 +02:00
parent dcca828197
commit 978b25c34d
25 changed files with 218 additions and 3379 deletions

View File

@ -1,84 +0,0 @@
#pragma once
#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_TRACE
#define FTP_DEBUG
#define DELAY_AFTER_DEBUG_AND_TRACE 0
// 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
#define DEBOUNCE_MILLIS 200
#define VOLUME_DEFAULT 230
#define VOLUME_MIN 190
#define VOLUME_MAX 255
#define VOLUME_STEP 0x08
#define RFID_SCAN_INTERVAL 100
#define NUM_BUTTONS 4
#define PIN_SD_CS(x) (digitalWrite(16, x))
#define PIN_SD_CS_SETUP() (pinMode(16, OUTPUT))
#define PIN_VS1053_XCS(x) (digitalWrite(4, x))
#define PIN_VS1053_XCS_SETUP() (pinMode(4, OUTPUT))
#define PIN_VS1053_XRESET(x) (digitalWrite(0, x))
#define PIN_VS1053_XRESET_SETUP() (pinMode(0, OUTPUT))
#define PIN_VS1053_XDCS(x) (digitalWrite(2, x))
#define PIN_VS1053_XDCS_SETUP() (pinMode(2, OUTPUT))
#define PIN_VS1053_DREQ() (digitalRead(15))
#define PIN_VS1053_DREQ_SETUP() (pinMode(15, INPUT))
#define PIN_RC522_CS(x) (digitalWrite(17, x))
#define PIN_RC522_CS_SETUP() (pinMode(17, OUTPUT))
#define PIN_SPEAKER_L(x) (digitalWrite(27, x))
#define PIN_SPEAKER_L_SETUP() (pinMode(27, OUTPUT))
#define PIN_SPEAKER_R(x) (digitalWrite(26, x))
#define PIN_SPEAKER_R_SETUP() (pinMode(26, OUTPUT))
#define BTN_PREV() ( ! digitalRead(22))
#define BTN_PREV_SETUP() (pinMode(22, INPUT_PULLUP))
#define BTN_VOL_UP() ( ! digitalRead(21))
#define BTN_VOL_UP_SETUP() (pinMode(21, INPUT_PULLUP))
#define BTN_VOL_DOWN() ( ! digitalRead(32))
#define BTN_VOL_DOWN_SETUP() (pinMode(32, INPUT_PULLUP))
#define BTN_NEXT() ( ! digitalRead(33))
#define BTN_NEXT_SETUP() (pinMode(33, INPUT_PULLUP))
// Other definitions
#define INFO(x, ...) Serial.printf(x, ##__VA_ARGS__)
#define ERROR(x, ...) Serial.printf(x, ##__VA_ARGS__)
#ifdef SHOW_DEBUG
#define DEBUG(x, ...) {Serial.printf(x, ##__VA_ARGS__); delay(DELAY_AFTER_DEBUG_AND_TRACE);}
#else
#define DEBUG(x, ...) while(0) {}
#endif
#ifdef SHOW_TRACE
#define TRACE(x, ...) {Serial.printf(x, ##__VA_ARGS__); delay(DELAY_AFTER_DEBUG_AND_TRACE);}
#else
#define TRACE(x, ...) while(0) {}
#endif

View File

@ -1,61 +1,13 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include "config.h"
class Controller;
#include "player.h"
#include "playlist.h"
#include "playlist_manager.h"
#include "http_server.h"
#undef DEPRECATED
#include <MFRC522.h>
enum ControllerState { NORMAL, LOCKING, LOCKED };
class Controller { class Controller {
private: private:
MFRC522* _rfid; void handle_buttons();
HTTPServer* _http_server; void handle_rfid();
ControllerState _state = NORMAL;
bool _rfid_enabled = true;
void _check_rfid();
void _check_serial();
void _check_buttons();
bool _debounce_button(uint8_t index);
uint32_t _get_rfid_card_uid();
String _read_rfid_data();
bool _rfid_present = false;
String _last_rfid_uid = "";
String _last_rfid_data = "";
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);
void _execute_command_ids();
void _execute_command_help();
unsigned long _button_last_pressed_at[NUM_BUTTONS];
bool _check_button(uint8_t btn);
public: public:
Controller(Player* p, PlaylistManager* pm); void handle();
PlaylistManager* pm;
Player* player;
void register_http_server(HTTPServer* h);
void loop();
void send_controller_status();
void send_player_status();
void send_playlist_manager_status();
void send_position();
void inform_new_client(AsyncWebSocketClient* client);
String json();
bool process_message(String m);
void queue_command(String cmd);
void update_playlist_manager();
}; };

View File

@ -1,54 +0,0 @@
#pragma once
#include <Arduino.h>
#include <SD.h>
#include "config.h"
#include "http_client_wrapper.h"
class DataSource {
private:
public:
DataSource() {};
virtual ~DataSource() {};
virtual size_t read(uint8_t* buf, size_t len) = 0;
virtual int read() = 0;
virtual size_t position() = 0;
virtual void seek(size_t position) = 0;
virtual size_t size() = 0;
virtual void close() = 0;
virtual void skip_id3_tag() {};
virtual bool usable() = 0;
};
class SDDataSource : public DataSource {
private:
File _file;
public:
SDDataSource(String file);
~SDDataSource();
size_t read(uint8_t* buf, size_t len);
int read();
size_t position();
void seek(size_t position);
size_t size();
void close();
void skip_id3_tag();
bool usable();
};
class HTTPSDataSource : public DataSource {
private:
WiFiClient* _stream = NULL;
HTTPClientWrapper* _http = NULL;
uint32_t _position;
public:
HTTPSDataSource(String url, uint32_t offset=0);
~HTTPSDataSource();
size_t read(uint8_t* buf, size_t len);
int read();
size_t position();
void seek(size_t position);
size_t size();
void close();
bool usable();
};

11
include/esmp3.h Normal file
View File

@ -0,0 +1,11 @@
#pragma once
#include "controller.h"
#define PIN_CS_SD 16
#define PIN_CS_RFID 17
#define I2S_DOUT 25
#define I2S_BCLK 27
#define I2S_LRC 26
extern Controller controller;

View File

@ -1,37 +0,0 @@
#pragma once
#include <HTTPClient.h>
#include "config.h"
class HTTPClientWrapper {
private:
HTTPClient* _http;
uint8_t* _buffer;
uint16_t _buffer_size;
uint16_t _buffer_length;
uint16_t _buffer_position;
uint32_t _chunk_length;
bool _connected = false;
String _content_type;
uint32_t _length;
bool _request(String method, String url, uint32_t offset=0, uint8_t redirection_count=0);
WiFiClient* _stream;
bool _is_chunked;
void _read_next_chunk_header(bool first);
uint16_t _fill_buffer();
public:
HTTPClientWrapper();
~HTTPClientWrapper();
bool get(String url, uint32_t offset=0, uint8_t redirection_count=0);
bool head(String url, uint32_t offset=0, uint8_t redirection_count=0);
String getContentType();
String getString();
int read();
uint32_t read(uint8_t* dst, uint32_t len);
void close();
uint32_t getSize();
String readUntil(String sep);
String readLine();
};

View File

@ -1,29 +0,0 @@
#pragma once
class HTTPServer;
#include "player.h"
#include "controller.h"
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
class HTTPServer {
private:
AsyncWebServer* _server;
Player* _player;
Controller* _controller;
void _handle_upload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
uint16_t _chunk_length;
uint8_t* _chunk;
File _upload_file;
uint32_t _file_size;
uint32_t _file_size_done;
bool _need_header;
uint32_t _upload_position;
void _onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len);
void _handle_index(AsyncWebServerRequest* req);
public:
HTTPServer(Player* p, Controller* c);
AsyncWebSocket* ws;
};

View File

@ -1,9 +0,0 @@
#pragma once
#include <Preferences.h>
void wifi_connect();
extern const uint8_t file_index_html_start[] asm("_binary_src_index_html_start");
extern bool debug_enabled;
extern bool trace_enabled;
extern Preferences prefs;

View File

@ -1,104 +0,0 @@
#pragma once
#include "config.h"
#include <SPI.h>
#include <SD.h>
#include "spi_master.h"
#include "playlist.h"
#include "data_sources.h"
class Player;
#include "controller.h"
#define SCI_MODE 0x00
#define SCI_STATUS 0x01
#define SCI_BASS 0x02
#define SCI_CLOCKF 0x03
#define SCI_DECODE_TIME 0x04
#define SCI_AUDATA 0x05
#define SCI_VOL 0x0B
#define SCI_WRAMADDR 0x07
#define SCI_WRAM 0x06
#define SCI_HDAT0 0x08
#define SCI_HDAT1 0x09
#define SCI_AIADDR 0x0A
#define SCI_AICTRL0 0x0C
#define SCI_AICTRL1 0x0D
#define SCI_AICTRL2 0x0E
#define SCI_AICTRL3 0x0F
#define CMD_WRITE 0x02
#define CMD_READ 0x03
#define ADDR_ENDBYTE 0x1E06
#define SM_LAYER12 0x0001
#define SM_RESET 0x0004
#define SM_CANCEL 0x0008
#define SM_SDINEW 0x0800
#define SM_ADPCM 0x1000
#define SS_DO_NOT_JUMP 0x8000
class Player {
private:
enum state { uninitialized, idle, playing, stopping,
sleeping, recording };
void _reset();
void _wait();
uint16_t _read_control_register(uint8_t address, bool do_wait=true);
void _write_control_register(uint8_t address, uint16_t value, bool do_wait=true);
void _write_direct(uint8_t address, uint16_t value);
void _write_data(uint8_t* data);
uint16_t _read_wram(uint16_t address);
state _state = state::uninitialized;
void _refill();
bool _refill_needed();
void _flush_and_cancel();
int8_t _get_endbyte();
void _flush(uint count, int8_t fill_byte);
void _play_file(String filename, uint32_t offset);
void _finish_playing();
void _finish_stopping(bool turn_speaker_off);
void _mute();
void _unmute();
void _sleep();
void _wakeup();
void _record();
void _patch_adpcm();
void _speaker_off();
void _speaker_on();
SPISettings _spi_settings_slow = SPISettings(250000, MSBFIRST, SPI_MODE0);
SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0);
SPISettings* _spi_settings = &_spi_settings_slow;
DataSource* _file;
uint32_t _file_size = 0;
uint8_t _buffer[32];
uint32_t _current_play_position = 0;
Playlist* _current_playlist = NULL;
uint _refills;
uint8_t _volume;
uint16_t _stop_delay;
uint32_t _skip_to;
SPIMaster* _spi;
Controller* _controller;
unsigned long _stopped_at;
public:
Player(SPIMaster* s);
void init();
void register_controller(Controller* c);
void vol_up();
void vol_down();
void track_next();
void track_prev();
void set_track(uint8_t track);
bool is_playing();
bool play();
bool play(Playlist* p);
void stop(bool turn_speaker_off=true);
bool loop();
void set_volume(uint8_t vol, bool save = true);
String position_json();
String json();
};

View File

@ -1,59 +1,15 @@
#pragma once #pragma once
#include <Arduino.h>
#include <vector> #include <vector>
#include <ArduinoJson.h> #include <Arduino.h>
#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; }
};
class Playlist { class Playlist {
private: private:
uint32_t _position = 0; std::vector<String> files;
uint32_t _current_track = 0; uint8_t current_file = 0;
bool _started = false; uint16_t current_time = 0;
bool _shuffled = false;
std::vector<PlaylistEntry> _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: public:
PlaylistPersistence persistence = PERSIST_TEMPORARY; void add_file(String filename);
Playlist(String path); void sort();
void start();
uint16_t get_file_count();
bool has_track_next();
bool has_track_prev();
bool track_next();
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();
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);
void advent_shuffle(uint8_t day);
bool is_fresh();
void dump();
void json(JsonObject json);
}; };

View File

@ -1,24 +1,21 @@
#pragma once #pragma once
#include <map>
#include <vector> #include <vector>
#include <map>
#include <Arduino.h>
#include "playlist.h" #include "playlist.h"
class PlaylistManager { class PlaylistManager {
private: private:
std::map<String, String> _map; Playlist get_playlist_for_tag_id(String id);
std::map<String, Playlist*> _playlists; String current_rfid_tag_id;
std::vector<String> _unmapped_folders; uint32_t audio_current_time = 0;
void _check_for_special_chars(String s);
void _save_mapping();
public: public:
PlaylistManager(); PlaylistManager();
Playlist* get_playlist_for_id(String id); std::vector<String> dirs;
Playlist* get_playlist_for_folder(String folder); std::map<String, String> map;
void dump_ids(); Playlist get_playlist(String rfid_id);
void scan_files(); Playlist current_playlist;
String json(); void set_audio_current_time(uint32_t time);
bool add_mapping(String id, String folder);
String create_mapping_txt();
void persist(Playlist* p);
}; };

View File

@ -1,70 +1,9 @@
#pragma once #pragma once
#include <Arduino.h>
#include <SPI.h>
#include "config.h"
class SPIMaster { class SPIMaster {
public: public:
static uint8_t state; static void enable_sd();
static void enable_rfid();
static void init() { static void disable_all();
PIN_SD_CS_SETUP(); static void initialize();
PIN_VS1053_XCS_SETUP();
PIN_VS1053_XDCS_SETUP();
PIN_RC522_CS_SETUP();
disable();
}
static void select_sd(bool enabled=true) {
PIN_SD_CS(enabled ? LOW : HIGH);
if (enabled) {
state |= 1;
} else {
state &= ~1;
}
}
static void select_vs1053_xcs(bool enabled=true) {
PIN_VS1053_XCS(enabled ? LOW : HIGH);
if (enabled) {
state |= 2;
} else {
state &= ~2;
}
}
static void select_vs1053_xdcs(bool enabled=true) {
PIN_VS1053_XDCS(enabled ? LOW : HIGH);
if (enabled) {
state |= 4;
} else {
state &= ~4;
}
}
static void select_rc522(bool enabled=true) {
PIN_RC522_CS(enabled ? LOW : HIGH);
if (enabled) {
state |= 8;
} else {
state &= ~8;
}
}
static void set_state(uint8_t s) {
disable();
if (s & 1) select_sd();
if (s & 2) select_vs1053_xcs();
if (s & 4) select_vs1053_xdcs();
if (s & 8) select_rc522();
}
static void disable() {
PIN_SD_CS(HIGH);
PIN_VS1053_XCS(HIGH);
PIN_VS1053_XDCS(HIGH);
PIN_RC522_CS(HIGH);
state = 0;
}
}; };

View File

@ -1,10 +0,0 @@
#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);
};

View File

@ -13,10 +13,6 @@ default_envs = esp32
[extra] [extra]
lib_deps = lib_deps =
63 @ 1.4.5 ; MFRC522
https://github.com/me-no-dev/ESPAsyncWebServer.git
ArduinoJSON
6691 ; TinyXML
[env:esp32] [env:esp32]
platform = espressif32 platform = espressif32
@ -24,17 +20,19 @@ board = esp-wrover-kit
framework = arduino framework = arduino
upload_speed = 115200 upload_speed = 115200
build_flags = !./build_version.sh build_flags = !./build_version.sh
lib_deps = ${extra.lib_deps} lib_deps =
;upload_port = 10.10.2.108 ; /dev/cu.SLAB_USBtoUART ${extra.lib_deps}
esphome/ESP32-audioI2S@^2.1.0
upload_port = 10.10.2.108
monitor_speed = 115200 monitor_speed = 115200
board_build.embed_txtfiles = src/index.html monitor_port = /dev/cu.usbserial-0001
;board_build.partitions = partitions.csv
;monitor_port = /dev/cu.wchusbserial1420
[env:deploy] [env:deploy]
platform = espressif32 platform = espressif32
board = esp-wrover-kit board = esp-wrover-kit
framework = arduino framework = arduino
lib_deps = ${extra.lib_deps} lib_deps =
${extra.lib_deps}
esphome/ESP32-audioI2S@^2.1.0
board_build.embed_txtfiles = src/index.html board_build.embed_txtfiles = src/index.html
board_build.partitions = partitions.csv board_build.partitions = partitions.csv

View File

@ -1,463 +1,13 @@
#include "controller.h" #include "controller.h"
#include "main.h"
#include "spi_master.h"
#include "config.h"
#include "playlist.h"
#include "http_server.h"
#include "updater.h"
#include <ArduinoJson.h>
Controller::Controller(Player* p, PlaylistManager* playlist_manager) { void Controller::handle() {
player = p;
pm = playlist_manager;
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
player->register_controller(this);
BTN_NEXT_SETUP();
BTN_PREV_SETUP();
BTN_VOL_UP_SETUP();
BTN_VOL_DOWN_SETUP();
SPIMaster::select_rc522();
DEBUG("Initializing RC522...\n");
_rfid->PCD_Init();
#ifdef SHOW_DEBUG
_rfid->PCD_DumpVersionToSerial();
#endif
SPIMaster::select_rc522(false);
INFO("RC522 initialized.\n");
for (uint8_t i=0; i<NUM_BUTTONS; i++) _button_last_pressed_at[i]=0;
} }
void Controller::register_http_server(HTTPServer* h) { void Controller::handle_buttons() {
_http_server = h;
} }
void Controller::loop() { void Controller::handle_rfid() {
unsigned long now = millis();
if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) {
_check_rfid();
_last_rfid_scan_at = now;
}
if ((_last_position_info_at < now - POSITION_SEND_INTERVAL) || (now < _last_position_info_at)) {
send_position();
_last_position_info_at = now;
}
_check_serial();
_check_buttons();
if (_cmd_queue.length() > 0) {
process_message(_cmd_queue);
_cmd_queue = "";
}
#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() {
SPIMaster::select_rc522();
if (!_rfid->PICC_ReadCardSerial()) {
if (!_rfid->PICC_IsNewCardPresent()) {
return 0;
}
if (!_rfid->PICC_ReadCardSerial()) {
return 0;
}
}
SPIMaster::select_rc522(false);
uint32_t uid = _rfid->uid.uidByte[0]<<24 | _rfid->uid.uidByte[1]<<16 | _rfid->uid.uidByte[2]<<8 | _rfid->uid.uidByte[3];
return uid;
}
void Controller::_check_rfid() {
//TRACE("check_rfid running...\n");
MFRC522::StatusCode status;
if (_rfid_present) {
byte buffer[2];
byte buffer_size = 2;
SPIMaster::select_rc522();
status = _rfid->PICC_WakeupA(buffer, &buffer_size);
if (status == MFRC522::STATUS_OK) {
// Card is still present.
_rfid->PICC_HaltA();
SPIMaster::select_rc522(false);
return;
}
SPIMaster::select_rc522(false);
// Card is now gone
_rfid_present = false;
INFO("No more RFID card.\n");
if (_state != LOCKED) {
player->stop();
}
send_controller_status();
} else {
uint32_t uid = _get_rfid_card_uid();
if (uid > 0) {
String temp = String(uid, HEX);
String s_uid = "";
for (int i=0; i<(8-temp.length()); i++) {
s_uid.concat("0");
}
s_uid.concat(temp);
INFO("New RFID card uid: %s\n", s_uid.c_str());
_last_rfid_uid = s_uid;
_rfid_present = true;
String data = _read_rfid_data();
_last_rfid_data = data;
Playlist* pl = pm->get_playlist_for_id(s_uid);
if (data.indexOf("[lock]") != -1) {
if (_state == LOCKED) {
_state = NORMAL;
DEBUG("ControllerState is now UNLOCKED\n");
} else {
DEBUG("ControllerState is now LOCKING\n");
_state = LOCKING;
}
}
if (pl==NULL) {
INFO("Could not find album for id '%s'.\n", s_uid.c_str());
send_controller_status();
return;
}
int index;
if (data.indexOf("[advent]") != -1 && pl->is_fresh()) {
struct tm time;
getLocalTime(&time);
if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december.
pl->advent_shuffle(time.tm_mday);
} else {
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();
} else if ((index=data.indexOf("[random:")) != -1 && pl->is_fresh()) {
String temp = data.substring(index + 8);
index = temp.indexOf("]");
TRACE("temp: %s, temp.substring(0, %d): %s\n", temp.c_str(), index, temp.substring(0, index).c_str());
if (index>0) {
uint8_t random_offset = temp.substring(0, index).toInt();
pl->shuffle(random_offset);
}
}
if (_state == LOCKED) {
DEBUG("ControllerState is LOCKED, ignoring card.\n");
return;
}
if (_state == LOCKING) {
_state = LOCKED;
DEBUG("ControllerState is now LOCKED.\n");
}
player->play(pl);
//send_playlist_manager_status();
send_controller_status();
}
}
}
String Controller::_read_rfid_data() {
TRACE("_read_rfid_data() running...\n");
static MFRC522::MIFARE_Key keys[8] = {
{{0xd3, 0xf7, 0xd3, 0xf7, 0xd3, 0xf7}}, // D3 F7 D3 F7 D3 F7
{{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}, // FF FF FF FF FF FF = factory default
{{0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5}}, // A0 A1 A2 A3 A4 A5
{{0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5}}, // B0 B1 B2 B3 B4 B5
{{0x4d, 0x3a, 0x99, 0xc3, 0x51, 0xdd}}, // 4D 3A 99 C3 51 DD
{{0x1a, 0x98, 0x2c, 0x7e, 0x45, 0x9a}}, // 1A 98 2C 7E 45 9A
{{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}}, // AA BB CC DD EE FF
{{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}} // 00 00 00 00 00 00
};
SPIMaster::select_rc522();
DEBUG("Trying to read RFID data...\n");
String data = "";
MFRC522::PICC_Type type = _rfid->PICC_GetType(_rfid->uid.sak);
uint8_t sectors = 0;
switch(type) {
case MFRC522::PICC_TYPE_MIFARE_MINI: sectors = 5; break;
case MFRC522::PICC_TYPE_MIFARE_1K: sectors = 16; break;
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<sectors; sector++) {
uint8_t blocks = (sector < 32) ? 4 : 16;
uint8_t block_offset = (sector < 32) ? sector * 4 : 128 + (sector - 32) * 16;
MFRC522::StatusCode status;
for (int i=0; i<8; i++) {
MFRC522::MIFARE_Key *k = &keys[i];
TRACE("Trying MIFARE key %02X %02X %02X %02X %02X %02X...\n", k->keyByte[0], k->keyByte[1], k->keyByte[2], k->keyByte[3], k->keyByte[4], k->keyByte[5]);
status = _rfid->PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, block_offset, k, &_rfid->uid);
if (status == MFRC522::STATUS_OK) {
TRACE("Authentication succeeded with key #%d\n", i);
good_key_index = i;
break;
}
}
if (good_key_index == -1) {
TRACE("Could not find a valid MIFARE key.\n");
} else {
for (uint8_t block=0; block<blocks-1; block++) {
byte buffer[18];
uint8_t byte_count = 18;
status = _rfid->MIFARE_Read(block_offset + block, buffer, &byte_count);
if (status != MFRC522::STATUS_OK) {
DEBUG("MIFARE_Read() failed: %s\n", String(_rfid->GetStatusCodeName(status)).c_str());
continue;
}
for (int i=0; i<16; i++) {
if (buffer[i]>=0x20 && buffer[i]<0x7F) data.concat((char)buffer[i]);
}
}
}
}
_rfid->PICC_HaltA();
_rfid->PCD_StopCrypto1();
DEBUG("Data from RFID: %s\n", data.c_str());
SPIMaster::select_rc522(false);
return data;
}
void Controller::_check_serial() {
if (Serial.available() > 0) {
char c = Serial.read();
Serial.printf("%c", c);
if (c==10 || c==13) {
if (_serial_buffer.length()>0) {
process_message(_serial_buffer);
_serial_buffer = String();
}
} else {
_serial_buffer.concat(c);
}
}
}
bool Controller::process_message(String cmd) {
DEBUG("Executing command: %s\n", cmd.c_str());
if (cmd.startsWith("play ")) {
Playlist* p = pm->get_playlist_for_folder(cmd.substring(5));
player->play(p);
} else if (cmd.equals("play")) {
player->play();
} else if (cmd.equals("stop")) {
player->stop();
} else if (cmd.equals("help")) {
_execute_command_help();
} else if (cmd.equals("-")) {
player->vol_down();
} else if (cmd.equals("+")) {
player->vol_up();
} else if (cmd.startsWith("volume=")) {
uint8_t vol = cmd.substring(7).toInt();
player->set_volume(vol);
} else if (cmd.equals("track_prev")) {
player->track_prev();
} else if (cmd.equals("track_next")) {
player->track_next();
} else if (cmd.startsWith("track=")) {
uint8_t track = cmd.substring(6).toInt();
player->set_track(track);
} else if (cmd.equals("ids")) {
pm->dump_ids();
} else if (cmd.equals("reset_vs1053")) {
player->stop();
player->init();
} else if (cmd.equals("reboot")) {
ESP.restart();
} else if (cmd.startsWith("add_mapping=")) {
String rest = cmd.substring(12);
uint8_t idx = rest.indexOf('=');
String id = rest.substring(0, idx);
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 if (cmd.startsWith("trace=")) {
int val = cmd.substring(6).toInt();
if (val==0) {
trace_enabled = false;
prefs.putBool("trace_enabled", false);
} else if (val==1) {
trace_enabled = true;
prefs.putBool("trace_enabled", true);
}
} else if (cmd.startsWith("debug=")) {
int val = cmd.substring(6).toInt();
if (val==0) {
debug_enabled = false;
prefs.putBool("debug_enabled", false);
} else if (val==1) {
debug_enabled = true;
prefs.putBool("debug_enabled", true);
}
} else {
ERROR("Unknown command: %s\n", cmd.c_str());
return false;
}
return true;
}
void Controller::_execute_command_ls(String path) {
INFO("Listing contents of %s:\n", path.c_str());
// TODO
//std::list<String> files = player->ls(path);
//for(std::list<String>::iterator it=files.begin(); it!=files.end(); ++it) {
// INFO(" %s\n", (*it).c_str());
//}
}
void Controller::_execute_command_help() {
INFO("Valid commands are:");
INFO(" help - Displays this help\n");
//INFO(" ls [dir] - Lists the contents of [dir] or, if not given, of /\n");
INFO(" ids - Lists all known ID-to-folder mappings\n");
INFO(" play [id] - Plays the album with the given id\n");
INFO(" stop - Stops playback\n");
INFO(" - / + - Decrease or increase the volume\n");
INFO(" p / n - Previous or next track\n");
}
void Controller::_check_buttons() {
if (BTN_PREV() && _debounce_button(0)) {
if (_state == NORMAL) {
player->track_prev();
} else {
DEBUG("Ignoring btn_prev because state is LOCKED.\n");
}
} else if (BTN_VOL_UP() && _debounce_button(1)) {
player->vol_up();
} else if (BTN_VOL_DOWN() && _debounce_button(2)) {
player->vol_down();
} else if (BTN_NEXT() && _debounce_button(3)) {
if (_state == NORMAL) {
player->track_next();
} else {
DEBUG("Ignoring btn_next because state is LOCKED.\n");
}
}
}
bool Controller::_debounce_button(uint8_t index) {
bool ret = false;
if (_button_last_pressed_at[index] + DEBOUNCE_MILLIS < millis()) {
DEBUG("Button %d pressed.\n", index);
ret = true;
}
_button_last_pressed_at[index] = millis();
return ret;
}
String Controller::json() {
DynamicJsonDocument json(1024);
json["_type"] = "controller";
switch(_state) {
case LOCKED: json["state"] = "locked"; break;
case LOCKING: json["state"] = "locking"; break;
case NORMAL: json["state"] = "normal"; break;
}
json["is_rfid_present"] = _rfid_present;
JsonObject rfid = json.createNestedObject("last_rfid");
rfid["uid"] = _last_rfid_uid;
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<String>();
}
void Controller::send_player_status() {
TRACE("In send_player_status()...\n");
if (_http_server->ws->count() > 0) {
_http_server->ws->textAll(player->json());
_http_server->ws->textAll(player->position_json());
}
}
void Controller::send_playlist_manager_status() {
TRACE("In send_playlist_manager_status()...\n");
if (_http_server->ws->count() > 0) {
_http_server->ws->textAll(pm->json());
}
}
void Controller::send_position() {
TRACE("In send_position()...\n");
if (_http_server->ws->count() > 0) {
_http_server->ws->textAll(player->position_json());
}
_last_position_info_at = millis();
}
void Controller::send_controller_status() {
TRACE("In send_controller_status()...\n");
if (_http_server->ws->count() > 0) {
_http_server->ws->textAll(json());
}
}
void Controller::inform_new_client(AsyncWebSocketClient* client) {
String s;
s += pm->json();
s += '\n';
s += player->json();
s += '\n';
s += player->position_json();
s += '\n';
s += json();
client->text(s);
}
void Controller::queue_command(String s) {
DEBUG("Enqeueing command '%s'.\n", s.c_str());
_cmd_queue = s;
}
void Controller::update_playlist_manager() {
pm->scan_files();
send_playlist_manager_status();
} }

View File

@ -1,57 +0,0 @@
#include "data_sources.h"
////////////// SDDataSource //////////////
SDDataSource::SDDataSource(String file) { _file = SD.open(file, "r"); }
SDDataSource::~SDDataSource() { if (_file) _file.close(); }
size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); }
int SDDataSource::read() { return _file.read(); }
size_t SDDataSource::position() { return _file.position(); }
void SDDataSource::seek(size_t position) { _file.seek(position); }
size_t SDDataSource::size() { return _file.size(); }
void SDDataSource::close() { _file.close(); }
bool SDDataSource::usable() { return _file; }
void SDDataSource::skip_id3_tag() {
uint32_t original_position = _file.position();
uint32_t offset = 0;
if (_file.read()=='I' && _file.read()=='D' && _file.read()=='3') {
DEBUG("ID3 tag found\n");
// Skip ID3 tag version
_file.read(); _file.read();
byte tags = _file.read();
bool footer_present = tags & 0x10;
DEBUG("ID3 footer found: %d\n", footer_present);
for (byte i=0; i<4; i++) {
offset <<= 7;
offset |= (0x7F & _file.read());
}
offset += 10;
if (footer_present) offset += 10;
DEBUG("ID3 tag length is %d bytes.\n", offset);
_file.seek(offset);
} else {
DEBUG("No ID3 tag found\n");
_file.seek(original_position);
}
}
////////////// HTTPSDataSource //////////////
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
_http = new HTTPClientWrapper();
if (!_http->get(url, offset)) return;
_position = 0;
}
HTTPSDataSource::~HTTPSDataSource() {
_http->close();
delete _http;
}
bool HTTPSDataSource::usable() { return _http; }
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; }
int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; }
size_t HTTPSDataSource::position() { return _position; }
void HTTPSDataSource::seek(size_t position) { return; /* TODO */ }
size_t HTTPSDataSource::size() { return _http->getSize(); }
void HTTPSDataSource::close() { _http->close(); }

68
src/esmp3.cpp Normal file
View File

@ -0,0 +1,68 @@
#include <WiFi.h>
#include <ArduinoOTA.h>
#include <SD.h>
#include "spi_master.h"
#include "playlist_manager.h"
#include "controller.h"
#include <Audio.h>
#include "esmp3.h"
#include <Ticker.h>
Controller controller;
Audio audio;
Ticker ticker_save_audio_current_time;
PlaylistManager* pm;
void setup() {
Serial.begin(115200);
WiFi.begin("Schlenz", "1410WischlingenPanda");
Serial.print("Connecting to WiFi...");
while(WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.println();
Serial.print("Connected to WiFi. IP address: ");
Serial.println(WiFi.localIP());
ArduinoOTA.begin();
Serial.println("Initializing SPI...");
SPI.begin();
SPI.setHwCs(false);
SPIMaster::initialize();
Serial.print("Initializing SD card...");
SPIMaster::enable_sd();
while(!SD.begin(14, SPI, 25000000)) {
for(int i=0; i<10; i++) {
if(SPI.transfer(0xFF)==0xFF) break;
delay(10);
}
Serial.print(".");
delay(100);
}
Serial.println();
Serial.println("Initializing PlaylistManager...");
pm = new PlaylistManager();
Serial.println("Setting up audio...");
audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
audio.setVolume(15);
audio.forceMono(true);
Serial.println("Setting up tickers...");
ticker_save_audio_current_time.attach(5, save_audio_current_time);
Serial.println("Setup finished.");
}
void loop() {
ArduinoOTA.handle();
controller.handle();
audio.loop();
}
void save_audio_current_time() {
pm->set_audio_current_time(audio.getAudioCurrentTime());
}

View File

@ -1,213 +0,0 @@
#include "http_client_wrapper.h"
#include <WiFiClientSecure.h>
HTTPClientWrapper::HTTPClientWrapper() {
_buffer = new uint8_t[512];
_buffer_size = 512;
}
HTTPClientWrapper::~HTTPClientWrapper() {
if (_http) {
_http->end();
delete _http;
}
delete _buffer;
}
void HTTPClientWrapper::close() {
_http->end();
_connected = false;
}
bool HTTPClientWrapper::get(String url, uint32_t offset, uint8_t redirection_count) { return _request("GET", url, offset, redirection_count); }
bool HTTPClientWrapper::head(String url, uint32_t offset, uint8_t redirection_count) { return _request("HEAD", url, offset, redirection_count); }
bool HTTPClientWrapper::_request(String method, String url, uint32_t offset, uint8_t redirection_count) {
if (redirection_count>=5) return false;
//if (_http) delete _http;
DEBUG("%s %s\n", method.c_str(), url.c_str());
_http = new HTTPClient();
_http->setUserAgent("PodBox/0.1");
if (offset) {
String s = "bytes=";
s += offset;
s += "-";
_http->addHeader("Range: ", s);
}
const char* headers[] = {"Location", "Content-Type", "Transfer-Encoding"};
_http->collectHeaders(headers, 3);
bool result;
/*if (url.startsWith("https:")) {
BearSSL::WiFiClientSecure* client = new BearSSL::WiFiClientSecure();
client->setInsecure();
result = _http->begin(*client, url);
} else {
result = _http->begin(url);
}*/
result = _http->begin(url);
TRACE("HTTP->begin result: %d\n", result);
if (!result) return false;
int status = _http->sendRequest(method.c_str());
TRACE("HTTP Status code: %d\n", status);
if (status == HTTP_CODE_FOUND || status==HTTP_CODE_MOVED_PERMANENTLY || status==HTTP_CODE_TEMPORARY_REDIRECT) {
if (_http->hasHeader("Location")) {
url = _http->header("Location");
_http->end();
delete _http;
_http = NULL;
return _request(method, url, offset, redirection_count+1);
} else {
ERROR("Got redirection HTTP code, but no Location header.\n");
delete _http;
_http = NULL;
return false;
}
} else if (status != HTTP_CODE_OK) {
DEBUG("Unexpected HTTP return code %d. Cancelling.\n", status);
return false;
}
_buffer_position = 0;
_buffer_length = 0;
_connected = true;
_length = _http->getSize() + offset;
if (_http->hasHeader("Content-Type")) {
_content_type = _http->header("Content-Type");
} else {
_content_type = "";
}
_is_chunked = (_http->hasHeader("Transfer-Encoding")) && (_http->header("Transfer-Encoding").indexOf("chunked")!=-1);
_stream = _http->getStreamPtr();
if (_is_chunked) {
_read_next_chunk_header(true);
}
return true;
}
void HTTPClientWrapper::_read_next_chunk_header(bool first) {
if (!_connected) {
_chunk_length = 0;
return;
}
if (!first) {
// read() returns an error if no bytes is available right at this moment.
// So we wait until 2 bytes are available or the connection times out.
while (_stream->connected() && !_stream->available()) { delay(1); }
int c1 = _stream->read();
while (_stream->connected() && !_stream->available()) { delay(1); }
int c2 = _stream->read();
if (c1==-1 || c2==-1) {
ERROR("Connection timeout.\n");
DEBUG("_stream.connected() returns %d\n", _stream->connected());
_chunk_length = 0;
_connected = false;
return;
} else if (c1!='\r' || c2!='\n') {
ERROR("Invalid chunk border found. Found: 0x%02X 0x%02X\n", c1, c2);
_chunk_length = 0;
_connected = false;
return;
}
}
String chunk_header = _stream->readStringUntil('\n');
chunk_header.trim();
_chunk_length = strtol(chunk_header.c_str(), NULL, 16);
if (_chunk_length == 0) {
_connected = false;
TRACE("Empty chunk found -> EOF reached.\n");
} else {
TRACE("Chunk found. Length: %d\n", _chunk_length);
}
}
uint16_t HTTPClientWrapper::_fill_buffer() {
if (!_connected) {
_buffer_position = 0;
_buffer_length = 0;
return 0;
}
uint16_t bytes_to_fill = _buffer_size;
uint16_t bytes_filled = 0;
while (bytes_to_fill > 0) {
uint16_t bytes_to_request = bytes_to_fill;
if (_is_chunked && _chunk_length < bytes_to_fill) bytes_to_request = _chunk_length;
TRACE("fill_buffer loop. _is_chunked: %d, _chunk_length: %d, _buffer_size: %d, bytes_filled: %d, bytes_to_fill: %d, bytes_to_request: %d", _is_chunked, _chunk_length, _buffer_size, bytes_filled, bytes_to_fill, bytes_to_request);
uint16_t result = _stream->readBytes(_buffer + bytes_filled, bytes_to_request);
TRACE(", result: %d\n", result);
bytes_filled += result;
bytes_to_fill -= result;
if (_is_chunked) {
_chunk_length -= result;
if (_chunk_length == 0) _read_next_chunk_header(false);
}
if (result == 0) {
_connected = false;
break;
}
}
_buffer_position = 0;
_buffer_length = bytes_filled;
TRACE("Buffer filled. _buffer_length: %d\n", _buffer_length);
return bytes_filled;
}
String HTTPClientWrapper::getContentType() {
return _content_type;
}
int HTTPClientWrapper::read() {
if (_buffer_position >= _buffer_length) _fill_buffer();
if (_buffer_position >= _buffer_length) return -1;
return _buffer[_buffer_position++];
}
uint32_t HTTPClientWrapper::read(uint8_t* dst, uint32_t len) {
TRACE("Reading %d bytes...\n", len);
uint32_t bytes_filled = 0;
while (1) {
if (_buffer_position >= _buffer_length) _fill_buffer();
if (_buffer_position >= _buffer_length) break;
uint32_t bytes_to_fill = len;
if (bytes_to_fill > _buffer_length - _buffer_position) bytes_to_fill = _buffer_length - _buffer_position;
TRACE("read_loop: _buffer_length=%d, _buffer_position=%d, len=%d, bytes_to_fill=%d\n", _buffer_length, _buffer_position, len, bytes_to_fill);
memcpy(dst + bytes_filled, _buffer + _buffer_position, bytes_to_fill);
_buffer_position += bytes_to_fill;
bytes_filled += bytes_to_fill;
len -= bytes_to_fill;
if (bytes_to_fill==0 || len==0) break;
}
return bytes_filled;
}
uint32_t HTTPClientWrapper::getSize() {return _length; }
String HTTPClientWrapper::readUntil(String sep) {
String result = "";
while(true) {
int i = read();
if (i==-1) break;
char c = i;
if (sep.indexOf(c)!=-1) {
// separator
if (result.length()>0) break;
} else {
result.concat(c);
}
}
return result;
}
String HTTPClientWrapper::readLine() {
return readUntil("\n\r");
}

View File

@ -1,167 +0,0 @@
#include "http_server.h"
#include "main.h"
#include "spi_master.h"
#include <ESPmDNS.h>
HTTPServer::HTTPServer(Player* p, Controller* c) {
_player = p;
_controller = c;
_server = new AsyncWebServer(80);
ws = new AsyncWebSocket("/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);});
_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);
}
void HTTPServer::_handle_upload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) {
// https://www.gnu.org/software/tar/manual/html_node/Standard.html
// https://www.mkssoftware.com/docs/man4/tar.4.asp
if (index == 0) { // Starting upload
_chunk = new uint8_t[512];
_chunk_length = 0;
_upload_position = 0;
_file_size = 0;
_file_size_done = 0;
_need_header = true;
}
uint32_t upload_offset = 0;
while (upload_offset < len) {
// Load a chunk
if (_chunk_length < 512 && len > upload_offset) {
uint16_t needed = 512 - _chunk_length;
if (needed > len - upload_offset) needed = len - upload_offset;
memcpy(_chunk + _chunk_length, data + upload_offset, needed);
_chunk_length += needed;
upload_offset += needed;
_upload_position += needed;
if (_chunk_length == 512) {
// Process chunk
DEBUG(".");
if (_need_header) {
if (_chunk[257]=='u'&&_chunk[258]=='s'&&_chunk[259]=='t'&&_chunk[260]=='a'&&_chunk[261]=='r') {
DEBUG("It is a valid header, starting at 0x%X!\n", _upload_position-512);
char filename[200];
strncpy(filename, (char*)_chunk, 100);
DEBUG("filename: %s\n", filename);
_file_size = 0;
_file_size_done = 0;
for (int i=0; i<11; i++) {
//Serial.print(_header_buffer[124 + i]);
_file_size = (_file_size<<3) + (_chunk[124 + i] - '0');
}
DEBUG("filesize: %d\n", _file_size);
uint8_t type = _chunk[156] - '0';
if (type==0) {
String path = "/";
path += filename;
DEBUG("Opening file %s\n", path.c_str());
uint8_t state = SPIMaster::state;
SPIMaster::disable();
SPIMaster::select_sd();
// Better safe than sorry. ;-)
_upload_file.close();
_upload_file = SD.open(path, "w");
SPIMaster::set_state(state);
} else if (type==5) {
String dirname = "/";
dirname += filename;
dirname.remove(dirname.length()-1);
uint8_t state = SPIMaster::state;
SPIMaster::disable();
SPIMaster::select_sd();
bool res = SD.mkdir(dirname);
SPIMaster::set_state(state);
DEBUG("Creating folder '%s' returned %d.\n", dirname.c_str(), res);
} else {
ERROR("Unknown file type %d\n", type);
}
_need_header = (type==5 || _file_size==0); // No chunks needed for directories.
} else {
bool byte_found = false;
for (int i=0; i<512; i++) byte_found = byte_found || _chunk[i]>0;
if (!byte_found) {
DEBUG("Empty chunk while looking for header -> ignoring.\n");
} else {
ERROR("Invalid tar header: %c %c %c %c %c. Looking at header start offset 0x%X.\n", _chunk[257], _chunk[258], _chunk[259], _chunk[260], _chunk[261], _upload_position-512);
}
}
} else {
uint32_t bytes_to_write = _file_size - _file_size_done;
if (bytes_to_write > 512) bytes_to_write=512;
uint8_t state = SPIMaster::state;
SPIMaster::disable();
SPIMaster::select_sd();
_upload_file.write(_chunk, bytes_to_write);
_file_size_done += bytes_to_write;
if (_file_size_done >= _file_size) {
_upload_file.close();
_need_header = true;
}
SPIMaster::set_state(state);
}
_chunk_length = 0;
}
}
}
if (final == true) {
uint8_t state = SPIMaster::state;
SPIMaster::disable();
SPIMaster::select_sd();
_upload_file.close();
SPIMaster::set_state(state);
delete _chunk;
_controller->update_playlist_manager();
return;
}
}
void HTTPServer::_onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) {
if (type==WS_EVT_CONNECT) {
_controller->inform_new_client(client);
} else if (type==WS_EVT_DATA) {
AwsFrameInfo* info = (AwsFrameInfo*) arg;
if (info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT) {
data[len]='\0';
DEBUG("Received ws message: %s\n", (char*)data);
_controller->queue_command((char*)data);
}
}
}

View File

@ -1,359 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>ESMP3</title>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="https://kit.fontawesome.com/272149490a.js" crossorigin="anonymous"></script>
<style>
.overlay {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
position: absolute;
z-index: 1;
margin: auto 0px;
vertical-align: middle;
color: white;
font-size: 60px;
text-align: center;
}
</style>
</head>
<body>
<div id="overlay" class="overlay">Not connected...</div>
<div class="container bg-dark text-light">
<div class="row">
<div class="col-sm-1">
<h1 id="play_state_icon"><i class="fa fa-stop"></i></h1>
</div>
<div class="col-sm-11">
<h2><i class="fa fa-compact-disc"></i> <span id="album"></span></h2>
<h2><i class="fa fa-scroll"></i> <span id="track"></span></h2>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col">
<input type="range" class="custom-range" id="position_slider" disabled>
</div>
</div>
<div class="row">
<div class="col-sm-6">
</div>
<div class="col-sm-1">
<h3><i class="fa fa-volume-down"></i></h3>
</div>
<div class="col-sm-4">
<input type="range" class="custom-range" id="volume_slider">
</div>
<div class="col-sm-1">
<h3><i class="fa fa-volume-up"></i></h3>
</div>
</div>
<div class="row">
<div class="col">
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_track_prev"><i class="fa fa-step-backward"></i></button>
</div>
<div class="col">
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_stop"><i class="fa fa-stop"></i></button>
</div>
<div class="col">
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_play"><i class="fa fa-play"></i></button>
</div>
<div class="col">
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_track_next"><i class="fa fa-step-forward"></i></button>
</div>
<div class="col">
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_lock"><i class="fa fa-lock-open"></i></button>
</div>
<div class="col">
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_open"><i class="fa fa-eject"></i></button>
</div>
<div class="col">
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_settings"><i class="fa fa-cog"></i></button>
</div>
</div>
</div>
<div class="container">
<table class="table table-hover table-sm" id="track_list_table">
<thead class="thead-light">
<tr>
<th>Nr.</th>
<th>Status</th>
<th>Track</th>
</tr>
</thead>
<tbody class="" id="track_list">
</tbody>
</table>
</div>
<div class="modal fade" id="openModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Album öffnen</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<h6>Open URL</h6>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="fa fa-link"></i>
</span>
</div>
<input type="text" class="form-control" id="input_url" />
<div class="input-group-append">
<button class="btn btn-primary" id="button_url_open">Go</button>
<button class="btn btn-warning" id="button_url_add_mapping" style="display: none;"><i class="fa fa-arrows-alt-h"></i></button>
</div>
</div>
<div class="modal-body">
<div id="albums_without_id_area">
<h6>Albums without RFID card</h6>
<table class="table table-hover table-sm">
<tbody id="albums_without_id">
</tbody>
</table>
</div>
<h6>Albums with RFID</h6>
<table class="table table-hover table-sm">
<tbody id="albums_with_id">
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="settingsModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Settings</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<h6>Last RFID id:</h6>
<span id="last_rfid_id"></span> <button class="btn btn-warning" id="button_add_mapping"><i class="fa fa-arrows-alt-h"></i></button>
<h6>Last RFID data:</h6>
<span id="last_rfid_data"></span>
<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_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 class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</body>
<script>
update_player = function(data) {
$('#play_state_icon i').removeClass('fa-stop', 'fa-play').addClass(data.playing ? 'fa-play' : 'fa-stop');
if (data.playing) {
$('#button_play').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
$('#button_stop').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
} else if (data.playlist) {
$('#button_play').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
$('#button_stop').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
} else {
$('#button_play').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
$('#button_stop').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
}
$('#volume_slider').attr('min', data.volume.min).attr('max', data.volume.max).val(data.volume.current);
if (data.playlist) update_playlist(data.playlist);
}
update_playlist = function(data) {
$('#track_list tr').remove();
for (var i=0; i<data.files.length; i++) {
tr = $('<tr>').data('track', i);
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.files[i].title));
$('#track_list').append(tr);
}
if (data.has_track_next) {
$('#button_track_next').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
} else {
$('#button_track_next').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
}
if (data.has_track_prev) {
$('#button_track_prev').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
} else {
$('#button_track_prev').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
}
$('#album').html(data.title);
var file = data.files[data.current_track];
if (file) {
$('#track').html(file.title);
}
}
update_controller = function(data) {
if (data.lock_state == "locked") {
$('#button_lock').removeClass('btn-primary', 'btn-warning').addClass('btn-danger');
$('#button_lock i').removeClass('fa-lock-open').addClass('fa-lock');
} else if (data.lock_state == "locking") {
$('#button_lock').removeClass('btn-primary', 'btn-danger').addClass('btn-warning');
$('#button_lock i').removeClass('fa-lock-open').addClass('fa-lock');
} else {
$('#button_lock').removeClass('btn-danger', 'btn-warning').addClass('btn-primary');
$('#button_lock i').removeClass('fa-lock').addClass('fa-lock-open');
}
$('#button_add_mapping').toggle(data.last_rfid.uid.length>0);
$('#last_rfid_id').html(data.last_rfid.uid);
$('#last_rfid_data').html(data.last_rfid.data);
}
update_playlist_manager = function(data) {
if (data.unmapped.length > 0) {
$('#albums_without_id_area').show();
$('#albums_without_id tr').remove();
data.unmapped = data.unmapped.sort();
for (var i=0; i<data.unmapped.length; i++) {
var tr = $('<tr>').attr('data-folder', data.unmapped[i]);
tr.append($('<td>').html(data.unmapped[i].substr(1)));
tr.append($('<td>').append($('<button>').addClass('button btn-warning add_mapping_button').hide().append($('<i>').addClass('fa fa-arrows-alt-h'))));
$('#albums_without_id').append(tr);
}
} else {
$('#albums_without_id_area').hide();
}
var folders = Object.keys(data.folders).sort();
for (var i in folders) {
var folder = folders[i];
var tr = $('<tr>').attr('data-folder', folder);
tr.append($('<td>').html(folder.substr(1)));
tr.append($('<td>').append($('<button>').addClass('button btn-danger add_mapping_button').hide().append($('<i>').addClass('fa fa-arrows-alt-h'))));
$('#albums_with_id').append(tr);
}
}
update_position = function(data) {
$('#position_slider').attr('max', data.file_size).val(data.position);
}
process_ws_message = function(event) {
var data = event.data.split("\n");;
for (var i=0; i<data.length; i++) {
var json = JSON.parse(data[i]);
console.log(json);
if (json === null) continue;
switch(json["_type"]) {
case "position": update_position(json); break;
case "player": update_player(json); break;
case "playlist_manager": update_playlist_manager(json); break;
case "controller": update_controller(json); break;
}
}
}
var play_on_click = true;
interval = null;
ws = null;
var start_reconnect_timer = function() {
console.log("start_reconnect_timer() running...");
$('#overlay').show();
interval = setInterval(connect_to_ws, 2500);
}
var connect_to_ws = function() {
if (!ws || ws.readyState >= ws.CLOSING) {
ws = new WebSocket("ws://" + location.host + "/ws");
ws.onopen = function() {
console.log("on_open() running...");
clearInterval(interval);
$('#overlay').hide();
};
ws.onmessage = process_ws_message;
ws.onclose = start_reconnect_timer;
}
}
$(function() {
start_reconnect_timer();
$('#volume_slider').change(function(e) { ws.send("volume=" + e.target.value); });
$('#button_play').click(function(e) { ws.send("play"); });
$('#button_stop').click(function(e) { ws.send("stop"); });
$('#button_track_next').click(function(e) { ws.send("track_next"); });
$('#button_track_prev').click(function(e) { ws.send("track_prev"); });
$('#button_open').click(function(e) { $('#openModal').modal('show'); });
$('#track_list').on('click', 'tr', function(e) { ws.send("track=" + $(e.target).parent().data('track')); });
$('#albums_without_id, #albums_with_id').on('click', 'tr', function(e) { if (play_on_click) {ws.send("play " + $(e.target).parents('tr').data('folder')); $('#openModal').modal('hide');} });
$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); });
$('#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_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_add_mapping').click(function(e) {
$('#settingsModal').modal('hide');
$('#openModal').modal('show');
$('.add_mapping_button').show();
$('#button_url_open').hide();
$('#button_url_add_mapping').show();
play_on_click = false;
});
$('#openModal').on('click', '.add_mapping_button', function(e) {
ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $(e.target).parents('tr').data('folder'));
$('#openModal').modal('hide');
$('.add_mapping_button').hide();
$('#button_url_open').hide();
$('#button_url_add_mapping').show();
e.stopPropagation();
play_on_click=true;
return false;
});
$('#button_url_add_mapping').click(function(e) {
ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $('#input_url').val());
$('#openModal').modal('hide');
$('.add_mapping_button').hide();
$('#button_url_open').hide();
$('#button_url_add_mapping').show();
e.stopPropagation();
play_on_click=true;
return false;
});
});
</script>
</html>

View File

@ -1,149 +0,0 @@
#include <Arduino.h>
#include <ArduinoOTA.h>
#include <SPI.h>
#include <SD.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <ESPmDNS.h>
#include <Preferences.h>
#include "main.h"
#include "config.h"
#include "controller.h"
#include "player.h"
#include "spi_master.h"
#include "http_server.h"
#include "playlist_manager.h"
#include "updater.h"
Controller* controller;
Player* player;
PlaylistManager* pm;
HTTPServer* http_server;
uint8_t SPIMaster::state = 0;
bool debug_enabled = true;
bool trace_enabled = false;
Preferences prefs;
void wifi_connect() {
INFO("Connecting to WiFi...\n");
WiFiMulti wifi;
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) {
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 {
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) {
INFO("Connected to WiFi \"%s\".\n", WiFi.SSID().c_str());
INFO("Local IP address: %s\n", WiFi.localIP().toString().c_str());
} else {
DEBUG("No WiFi connection!\n");
}
}
void setup() {
// Small delay to give the Serial console a bit of time to connect.
delay(1000);
Serial.begin(115200);
Serial.println("Starting...");
Serial.println("Started.");
INFO("Starting.\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("Initializing...\n");
prefs.begin("esmp3");
debug_enabled = prefs.getBool("debug_enabled", true);
trace_enabled = prefs.getBool("trace_enabled", false);
delay(1000);
PIN_SPEAKER_L_SETUP();delay(1000);
PIN_SPEAKER_R_SETUP();delay(1000);
PIN_SPEAKER_L(LOW);delay(1000);
PIN_SPEAKER_R(LOW);delay(1000);
DEBUG("Setting up SPI...\n");delay(1000);
SPI.begin();
SPI.setHwCs(false);
SPIMaster::init();
SPIMaster* spi = new SPIMaster();
INFO("SPI initialized.\n");
DEBUG("Setting up SD card...\n");
spi->select_sd();
if (SD.begin(14, SPI, 25000000)) {
INFO("SD card initialized.\n");
} else {
ERROR("Could not initialize SD card.\n");
}
spi->select_sd(false);
DEBUG("Initializing PlaylistManager...\n");
pm = new PlaylistManager();
DEBUG("Initializing Player and Controller...\n");
player = new Player(spi);
controller = new Controller(player, pm);
INFO("Player and controller initialized.\n");
wifi_connect();
ArduinoOTA.begin();
MDNS.begin("esmp3");
DEBUG("Setting up HTTP server...\n");
http_server = new HTTPServer(player, controller);
controller->register_http_server(http_server);
DEBUG("Starting NTP client...\n");
// Taken from https://github.com/esp8266/Arduino/blob/master/cores/esp8266/TZ.h
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "europe.pool.ntp.org");
struct tm time;
if (getLocalTime(&time, 10000)) {
char buffer[100];
strftime(buffer, 100, "%Y-%m-%d %H:%M:%S", &time);
DEBUG("Got time: %s\n", buffer);
} else {
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");
}
void loop() {
bool more_data_needed = player->loop();
if (more_data_needed) return;
controller->loop();
ArduinoOTA.handle();
}

View File

@ -1,753 +0,0 @@
// Based on https://github.com/mpflaga/Arduino_Library-vs1053_for_SdFat/blob/master/src/vs1053_SdFat.cpp
#include "player.h"
#include "spi_master.h"
#include <ArduinoJson.h>
//Player::_spi_settings
Player::Player(SPIMaster* s) {
_spi = s;
PIN_VS1053_XRESET_SETUP();
PIN_VS1053_XRESET(HIGH);
_speaker_off();
_spi->disable();
PIN_VS1053_DREQ_SETUP();
init();
}
void Player::register_controller(Controller* c) {
_controller = c;
}
void Player::_reset() {
PIN_VS1053_XRESET(LOW);
delay(100);
PIN_VS1053_XRESET(HIGH);
delay(100);
_state = uninitialized;
_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow
}
void Player::init() {
DEBUG("Resetting VS1053...\n");
_reset();
uint16_t result = _read_control_register(SCI_MODE);
DEBUG("SCI_MODE: 0x%04X\n", result);
if (result != 0x4800) {
ERROR("SCI_MODE was 0x%04X, expected was 0x4800. Rebooting.\n", result);
delay(500);
ESP.restart();
}
result = _read_control_register(SCI_STATUS);
DEBUG("SCI_STATUS: 0x%04X\n", result);
if (result != 0x0040 && result != 0x0048) {
ERROR("SCI_STATUS was 0x%04X, expected was 0x0040 or 0x0048. Rebooting.\n", result);
delay(500);
ESP.restart();
}
result = _read_control_register(SCI_CLOCKF);
DEBUG("SCI_CLOCKF: 0x%04X\n", result);
DEBUG("VS1053 Init looking good.\n");
DEBUG("Upping VS1053 multiplier...\n");
_write_control_register(SCI_CLOCKF, 0xC000);
delay(10);
_spi_settings = &_spi_settings_fast;
result = _read_control_register(SCI_CLOCKF);
DEBUG("SCI_CLOCKF: 0x%04X\n", result);
if (result != 0xC000) {
ERROR("Error: SCI_CLOCKF was 0x%04X, expected was 0xC000. Rebooting.\n", result);
delay(500);
ESP.restart();
}
set_volume(VOLUME_DEFAULT);
INFO("VS1053 initialization completed.\n");
_state = idle;
}
void Player::_speaker_off() {
DEBUG("Speaker off\n");
PIN_SPEAKER_L(LOW);
PIN_SPEAKER_R(LOW);
}
void Player::_speaker_on() {
DEBUG("Speaker on\n");
PIN_SPEAKER_L(HIGH);
PIN_SPEAKER_R(HIGH);
}
void Player::_sleep() {
DEBUG("VS1053 going to sleep.\n");
_speaker_off();
_write_control_register(SCI_CLOCKF, 0x0000);
_spi_settings = &_spi_settings_slow;
_write_control_register(SCI_AUDATA, 0x0010);
set_volume(0, false);
_state = sleeping;
TRACE("VS1053 is sleeping now.\n");
}
void Player::_wakeup() {
if (_state != sleeping && _state != recording) return;
_stopped_at = millis();
DEBUG("Waking VS1053...\n");
set_volume(_volume, false);
_write_control_register(SCI_AUDATA, 0x0000);
_write_control_register(SCI_CLOCKF, 0x6000);
_write_control_register(SCI_MODE, 0x4800 | SM_RESET);
delay(10);
//_speaker_on();
_spi_settings = &_spi_settings_fast;
_state = idle;
}
void Player::_record() {
// http://www.vlsi.fi/fileadmin/software/VS10XX/VS1053_VS1063_PcmRecorder.pdf
DEBUG("Starting recording.\n");
set_volume(1, false);
// Disable SCI_BASS
_write_control_register(SCI_BASS, 0);
// Disable user applications
_write_control_register(SCI_AIADDR, 0);
// Disable interrupts
_write_control_register(SCI_WRAMADDR, 0xC01A);
_write_control_register(SCI_WRAM, 0x0002);
_patch_adpcm();
_write_control_register(SCI_MODE, SM_ADPCM);
_write_control_register(SCI_AICTRL0, 0x8000); // Mono VU meter
_write_control_register(SCI_AICTRL1, 1024); // Manual gain, 1x
_write_control_register(SCI_AICTRL2, 0); // Maximum gain for autogain - ignored
_write_control_register(SCI_AICTRL3, 0); // status: record
_write_control_register(SCI_AIADDR, 0x0034, false);
delay(1);
DEBUG("Recording.\n");
delay(10);
_state = recording;
}
inline void Player::_wait() {
while(!PIN_VS1053_DREQ());
}
uint16_t Player::_read_control_register(uint8_t address, bool do_wait) {
if (do_wait) _wait();
_spi->select_vs1053_xcs();
SPI.beginTransaction(*_spi_settings);
SPI.transfer(CMD_READ);
SPI.transfer(address);
uint8_t b1 = SPI.transfer(0xFF);
_wait();
uint8_t b2 = SPI.transfer(0xFF);
_wait();
SPI.endTransaction();
_spi->select_vs1053_xcs(false);
return (b1 << 8) | b2;
}
void Player::_write_control_register(uint8_t address, uint16_t value, bool do_wait) {
_wait();
_spi->select_vs1053_xcs();
SPI.beginTransaction(*_spi_settings);
SPI.transfer(CMD_WRITE);
SPI.transfer(address);
SPI.transfer(value >> 8);
SPI.transfer(value & 0xFF);
SPI.endTransaction();
_spi->select_vs1053_xcs(false);
if (do_wait) _wait();
}
void Player::_patch_adpcm() {
static const uint16_t patch_data[] = {
0x0007, 0x0001, 0xc01a, 0x0006, 0x0001, 0x0002, 0x0007, 0x0001, /* 0 */
0x0008, 0x0006, 0x8002, 0x0000, 0x0007, 0x0001, 0x000c, 0x0006, /* 8 */
0x0002, 0x7000, 0x0017, 0x0007, 0x0001, 0x8034, 0x0006, 0x0022, /* 10 */
0x0030, 0x0490, 0xb080, 0x0024, 0x3800, 0x0024, 0x0000, 0x1090, /* 18 */
0xf400, 0x5404, 0x0000, 0x0851, 0xf400, 0x5648, 0xf400, 0x5404, /* 20 */
0xf400, 0x5658, 0xf400, 0x5404, 0xf400, 0x5640, 0x0000, 0x800a, /* 28 */
0x2900, 0x9180, 0x0006, 0x2016, 0x2a00, 0x1bce, 0x2a00, 0x114e, /* 30 */
0x2a00, 0x168e, 0x0007, 0x0001, 0x1800, 0x0006, 0x8006, 0x0000, /* 38 */
0x0007, 0x0001, 0x8045, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, /* 40 */
0x7808, 0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, /* 48 */
0x7800, 0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x3e80, 0x3e10, /* 50 */
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, /* 58 */
0x4024, 0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, /* 60 */
0x8024, 0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, /* 68 */
0x0001, 0x805a, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, 0x7808, /* 70 */
0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, 0x7800, /* 78 */
0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x5e40, 0x3e10, 0x7800, /* 80 */
0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, 0x4024, /* 88 */
0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, /* 90 */
0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, /* 98 */
0x806f, 0x0006, 0x0030, 0x3e12, 0xb817, 0x3e12, 0x7808, 0x3e18, /* a0 */
0x3821, 0x3e18, 0xb823, 0x3e10, 0x7800, 0xb880, 0x3855, 0x0030, /* a8 */
0x0497, 0x48b2, 0x3c00, 0x0000, 0x800a, 0x2900, 0x7300, 0x3e10, /* b0 */
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x6890, 0x1bd5, 0x0030, /* b8 */
0x0497, 0x3f00, 0x0024, 0x36f0, 0x5800, 0x36f8, 0x9823, 0x36f8, /* c0 */
0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, 0x0030, 0x0717, 0x2100, /* c8 */
0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, 0x8010, 0x0006, 0x000e, /* d0 */
0x3e02, 0x8024, 0x0001, 0x000a, 0x6012, 0x0024, 0xfea2, 0x0024, /* d8 */
0x48b2, 0x1bca, 0x2000, 0x0000, 0x4180, 0x0024, 0x0007, 0x0001, /* e0 */
0x8087, 0x0006, 0x00e6, 0x3e00, 0x7843, 0x3e01, 0x3845, 0x3e04, /* e8 */
0x3812, 0x0006, 0x08d0, 0x3000, 0x4024, 0x6182, 0x0024, 0x0030, /* f0 */
0x06d0, 0x2800, 0x2655, 0xb882, 0x0024, 0x0000, 0x0201, 0x0000, /* f8 */
0x0005, 0x0030, 0x0210, 0xa016, 0x4004, 0x1fff, 0xfe01, 0xae1a, /* 100 */
0x0024, 0xc342, 0x0024, 0xb882, 0x2001, 0x0030, 0x06d0, 0x3800, /* 108 */
0x4024, 0x0006, 0x0890, 0x3004, 0x0024, 0x3000, 0x4024, 0x0006, /* 110 */
0x12d0, 0x6182, 0x0024, 0x3000, 0x4024, 0x2800, 0x3e05, 0xf400, /* 118 */
0x4050, 0x3009, 0x2000, 0x0006, 0x08d0, 0x0006, 0x0892, 0x3000, /* 120 */
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x0030, 0x0250, 0xb882, /* 128 */
0x2001, 0x0030, 0x0710, 0x3800, 0x4024, 0x0006, 0x12d0, 0x3000, /* 130 */
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x3204, 0x0024, 0x3023, /* 138 */
0x0024, 0x30e0, 0xc024, 0x6312, 0x0024, 0x0000, 0x00c3, 0x2800, /* 140 */
0x3141, 0x0000, 0x0024, 0x3033, 0x0024, 0x3a04, 0x0024, 0x3000, /* 148 */
0x4024, 0x6182, 0x0024, 0x0006, 0x0890, 0x2800, 0x2fd8, 0x0006, /* 150 */
0x0301, 0x3a00, 0x4024, 0x0000, 0x00c3, 0x3004, 0x0024, 0x3013, /* 158 */
0x0024, 0x3000, 0x4024, 0x0006, 0x12d0, 0x3800, 0x4024, 0x0030, /* 160 */
0x0310, 0xf000, 0x0001, 0x6236, 0x0024, 0x001f, 0xffc3, 0x2800, /* 168 */
0x3395, 0x0000, 0x0024, 0x0000, 0x0203, 0xa132, 0x0024, 0x001f, /* 170 */
0xffc3, 0xb136, 0x0024, 0x6306, 0x0024, 0x0000, 0x0024, 0x2800, /* 178 */
0x3611, 0x0000, 0x0024, 0x0020, 0x0003, 0xb132, 0x0024, 0x0000, /* 180 */
0x0024, 0x2800, 0x3a85, 0x0000, 0x0024, 0x0000, 0x0081, 0xb212, /* 188 */
0x0024, 0x0000, 0x0024, 0x2800, 0x3a05, 0x0000, 0x0024, 0x6892, /* 190 */
0x0024, 0xb212, 0x0024, 0x0000, 0x0005, 0x2800, 0x3c55, 0x0030, /* 198 */
0x0310, 0x0000, 0x3fc1, 0x3000, 0x8024, 0xb214, 0x0024, 0x003f, /* 1a0 */
0xc001, 0xb010, 0x0024, 0xc200, 0x0024, 0x0030, 0x0310, 0x3800, /* 1a8 */
0x0024, 0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x5803, 0x2000, /* 1b0 */
0x0000, 0x0000, 0x0024, 0x0030, 0x0310, 0x0000, 0x0005, 0x003f, /* 1b8 */
0xc001, 0x4088, 0x0002, 0xb214, 0x0024, 0x1fff, 0xfe01, 0xae12, /* 1c0 */
0x0024, 0x2800, 0x3a00, 0xc200, 0x0024, 0x2800, 0x28c0, 0x3800, /* 1c8 */
0x0024, 0x0007, 0x0001, 0x80fa, 0x0006, 0x00fe, 0x3e12, 0x0024, /* 1d0 */
0x3e05, 0xb814, 0x3615, 0x0024, 0x3e00, 0x3841, 0x3e00, 0xb843, /* 1d8 */
0x3e01, 0x3845, 0x3e04, 0x3851, 0x0030, 0x10d0, 0x3e04, 0x8024, /* 1e0 */
0x3010, 0x0024, 0x3000, 0x8024, 0x0006, 0x1190, 0x3000, 0x4024, /* 1e8 */
0x6182, 0x0024, 0x0000, 0x0024, 0x2800, 0x5dd5, 0x0000, 0x0024, /* 1f0 */
0x0030, 0x03d0, 0x0000, 0x00c1, 0x3000, 0xc024, 0xb318, 0x0024, /* 1f8 */
0x6896, 0x0024, 0x6436, 0x0024, 0x0020, 0x0003, 0x2800, 0x59c5, /* 200 */
0x0000, 0x0024, 0x0006, 0x1150, 0x3000, 0x4024, 0x6136, 0x0024, /* 208 */
0x0000, 0x0024, 0x2800, 0x4741, 0x0000, 0x0024, 0x0000, 0x0803, /* 210 */
0x4132, 0x0024, 0x3800, 0x4024, 0x0006, 0x0190, 0x0006, 0xf011, /* 218 */
0x2900, 0xb500, 0x3613, 0x0024, 0x0006, 0xf011, 0x0006, 0x1152, /* 220 */
0x0006, 0x0250, 0x4082, 0x0800, 0xfe82, 0x184c, 0x1fff, 0xfc41, /* 228 */
0x48ba, 0x0024, 0xae1a, 0x0024, 0x2900, 0xb500, 0x4280, 0x4103, /* 230 */
0x0006, 0x1110, 0x4084, 0x0800, 0xfe84, 0x0002, 0x48ba, 0x0024, /* 238 */
0xae12, 0x0024, 0xf400, 0x4001, 0x0000, 0x0180, 0x6200, 0x0024, /* 240 */
0x0000, 0x0080, 0x2800, 0x5241, 0x4200, 0x0024, 0x3800, 0x0024, /* 248 */
0x0006, 0x1090, 0x3004, 0x8024, 0xf400, 0x4491, 0x3113, 0x0024, /* 250 */
0x3804, 0x4024, 0x3a00, 0xc024, 0x3004, 0x8024, 0xf400, 0x4491, /* 258 */
0x3113, 0x0024, 0x3804, 0x4024, 0x3a00, 0x4024, 0x0006, 0x1081, /* 260 */
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x0f00, 0x2800, 0x5248, /* 268 */
0x0000, 0x0024, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0080, /* 270 */
0x3000, 0x4024, 0x0030, 0x0710, 0xb104, 0x0024, 0x0000, 0x0001, /* 278 */
0x3800, 0x4024, 0x0006, 0x08d0, 0x3001, 0x0024, 0x0006, 0x0910, /* 280 */
0x3000, 0x4024, 0x6100, 0x0024, 0x6042, 0x0024, 0x0030, 0x06d0, /* 288 */
0x2800, 0x5711, 0xb880, 0x0024, 0x2900, 0x21c0, 0x4380, 0x184c, /* 290 */
0xb880, 0x0024, 0x3800, 0x0024, 0x36f4, 0x8024, 0x36f4, 0x1811, /* 298 */
0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, /* 2a0 */
0x36f3, 0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, /* 2a8 */
0x0006, 0x1152, 0x0000, 0x0804, 0x3200, 0xc024, 0x6346, 0x0024, /* 2b0 */
0x6386, 0x2803, 0x0000, 0x0024, 0x2800, 0x4755, 0x0000, 0x0024, /* 2b8 */
0x3800, 0x4024, 0x0030, 0x0690, 0x0000, 0x0081, 0xb882, 0x22c1, /* 2c0 */
0x3800, 0x4024, 0x0030, 0x0590, 0x2800, 0x4740, 0x3800, 0x4024, /* 2c8 */
0x2800, 0x5700, 0x4190, 0x0024, 0x0007, 0x0001, 0x8179, 0x0006, /* 2d0 */
0x00a6, 0x3e12, 0x0024, 0x3e05, 0xb814, 0x3625, 0x0024, 0x3e00, /* 2d8 */
0x3841, 0x3e00, 0xb843, 0x3e04, 0x3851, 0x0006, 0x1110, 0x3e04, /* 2e0 */
0xb813, 0x3000, 0x0024, 0x6080, 0x0024, 0x0006, 0x11d2, 0x2800, /* 2e8 */
0x70c5, 0x0000, 0x0081, 0x6010, 0x984c, 0x3800, 0x0024, 0x0006, /* 2f0 */
0x10d0, 0x3200, 0x0024, 0xf100, 0x0011, 0xf100, 0x0024, 0xf102, /* 2f8 */
0x0400, 0x0006, 0x1311, 0x2900, 0x0400, 0x3100, 0x8024, 0x0030, /* 300 */
0x1293, 0x3413, 0x184c, 0x3c04, 0x4024, 0x3b00, 0x0024, 0x3004, /* 308 */
0xc024, 0xf400, 0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3310, /* 310 */
0x0024, 0x3a00, 0x0024, 0x0006, 0x1212, 0x3200, 0x0024, 0xf100, /* 318 */
0x13d1, 0xf100, 0x0402, 0x2900, 0x0400, 0xf102, 0x0c00, 0x0030, /* 320 */
0x12d1, 0x0006, 0x1081, 0x3900, 0x0024, 0x3004, 0xc024, 0xf400, /* 328 */
0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3300, 0x0024, 0x3a00, /* 330 */
0x0024, 0xf400, 0x4440, 0x6010, 0x0024, 0x1fee, 0xe002, 0x2800, /* 338 */
0x6bc8, 0x0006, 0x0f00, 0x3800, 0x0024, 0x0006, 0x0010, 0xb886, /* 340 */
0x0040, 0x30f0, 0x4024, 0x6c92, 0x40c3, 0x3810, 0x0024, 0xb182, /* 348 */
0x23c1, 0x0006, 0x0950, 0x3000, 0x0024, 0x6090, 0x0024, 0x6cd2, /* 350 */
0x2000, 0x0000, 0x0000, 0x2800, 0x70c8, 0x0000, 0x0024, 0x3800, /* 358 */
0x0024, 0x0000, 0x0210, 0x3010, 0x0024, 0x30f0, 0x4024, 0x6c92, /* 360 */
0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, 0x36f4, 0x9813, 0x36f4, /* 368 */
0x1811, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, 0x36f3, /* 370 */
0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, 0x0007, /* 378 */
0x0001, 0x81cc, 0x0006, 0x00f4, 0x3e00, 0x3841, 0x0000, 0x0201, /* 380 */
0x3e00, 0xb843, 0x3e01, 0x3845, 0x3e04, 0x3812, 0x0030, 0x0410, /* 388 */
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x08d0, 0x2800, 0x8045, /* 390 */
0x0000, 0x0181, 0x6012, 0x0024, 0x0006, 0x1250, 0x2800, 0x7e45, /* 398 */
0x0000, 0x05c1, 0x6012, 0x0024, 0x0030, 0x01d0, 0x2800, 0x7c45, /* 3a0 */
0x0000, 0x0581, 0x6010, 0x03cc, 0x0000, 0x0024, 0x2800, 0x7a95, /* 3a8 */
0x0000, 0x0024, 0x3000, 0x8024, 0x0006, 0x1250, 0x3000, 0x0024, /* 3b0 */
0x6092, 0x0024, 0x3800, 0x4024, 0xf400, 0x4010, 0x3800, 0x8024, /* 3b8 */
0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, /* 3c0 */
0x2000, 0x0000, 0x0000, 0x0024, 0x0030, 0x01d0, 0x3000, 0x0024, /* 3c8 */
0x0006, 0x1250, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3d0 */
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3d8 */
0x6090, 0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3e0 */
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3e8 */
0x6080, 0x0024, 0x0000, 0x0024, 0x2800, 0x8515, 0x0000, 0x0024, /* 3f0 */
0x0006, 0x1350, 0x0000, 0x0082, 0x0030, 0x0352, 0xb886, 0x0040, /* 3f8 */
0x30f0, 0x4024, 0x4cd2, 0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, /* 400 */
0x3a00, 0x0024, 0x3010, 0x0024, 0x30f0, 0x4024, 0x0030, 0x0390, /* 408 */
0x2800, 0x7a80, 0x4180, 0x2001, 0x4090, 0x0024, 0x3800, 0x0024, /* 410 */
0x0030, 0x0250, 0x3800, 0x0024, 0x0006, 0x1290, 0x3000, 0x0024, /* 418 */
0x6090, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x8024, /* 420 */
0x3223, 0x0024, 0x32e0, 0x4024, 0x6100, 0x0024, 0x0000, 0x0024, /* 428 */
0x2800, 0x8c81, 0x0000, 0x0024, 0x3233, 0x0024, 0x3804, 0x8024, /* 430 */
0x3200, 0x0024, 0x6080, 0x0024, 0x0006, 0x0300, 0x2800, 0x8b18, /* 438 */
0x0000, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x0024, /* 440 */
0x3013, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, 0x3800, 0x0024, /* 448 */
0x0006, 0x0850, 0x3004, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, /* 450 */
0x6080, 0x0024, 0x3000, 0x0024, 0x2800, 0x9115, 0xf400, 0x4010, /* 458 */
0x3000, 0x0024, 0x0000, 0x0201, 0x0000, 0x0005, 0x0030, 0x0210, /* 460 */
0xa014, 0x4004, 0x1fff, 0xfe01, 0xae12, 0x0024, 0xc200, 0x0024, /* 468 */
0x2800, 0x8180, 0x3800, 0x0024, 0x2800, 0x8ec0, 0x3009, 0x0000, /* 470 */
0x0007, 0x0001, 0x8246, 0x0006, 0x0104, 0x0030, 0x1092, 0x0007, /* 478 */
0x9250, 0x003f, 0xfc42, 0xb880, 0x184c, 0x3e12, 0x0024, 0x3800, /* 480 */
0x0024, 0x0030, 0x0290, 0x38f0, 0x0024, 0x3800, 0x0024, 0x0030, /* 488 */
0x0050, 0x3000, 0x4024, 0xb122, 0x0024, 0x6894, 0x2001, 0x0000, /* 490 */
0x0141, 0x3a70, 0x4024, 0x0004, 0x1fc1, 0x3a00, 0x4024, 0x0030, /* 498 */
0x00d2, 0x0030, 0x0001, 0x3a00, 0x4024, 0x0030, 0x0552, 0x3a10, /* 4a0 */
0x0024, 0x3a00, 0x0024, 0x3000, 0x4024, 0xc122, 0x0024, 0x3800, /* 4a8 */
0x4024, 0x0030, 0x05d0, 0x0000, 0x03c1, 0x3820, 0x4024, 0x3800, /* 4b0 */
0x0024, 0x0000, 0x0310, 0x3010, 0x0024, 0x30f0, 0x4024, 0xf2c2, /* 4b8 */
0x0024, 0x3810, 0x0024, 0x0000, 0x3fc0, 0x38f0, 0x4024, 0x0030, /* 4c0 */
0x02d0, 0x3000, 0x4024, 0x2912, 0x1400, 0xb104, 0x0024, 0x0006, /* 4c8 */
0x1312, 0x6802, 0x0024, 0x000d, 0xac00, 0x6012, 0x2801, 0x0000, /* 4d0 */
0x0024, 0x2800, 0x9dc1, 0x0000, 0x0024, 0x3a00, 0x0024, 0x2909, /* 4d8 */
0x1b40, 0x3613, 0x0024, 0x0000, 0x0084, 0x0000, 0x1905, 0x2908, /* 4e0 */
0xbe80, 0x3613, 0x0024, 0x0000, 0x0000, 0x0006, 0x0302, 0x4002, /* 4e8 */
0x0024, 0x4012, 0x0024, 0x4212, 0x0024, 0xf400, 0x4050, 0x3000, /* 4f0 */
0x4024, 0x6182, 0x0024, 0x0006, 0x0350, 0x2800, 0xa6c8, 0x0000, /* 4f8 */
0x0024, 0x4002, 0x0024, 0x4014, 0x0024, 0x0006, 0x0301, 0x4124, /* 500 */
0x0024, 0x0000, 0x0081, 0x4212, 0x0024, 0x4002, 0x4050, 0x4014, /* 508 */
0x0003, 0x0006, 0x0301, 0x4122, 0x0024, 0x6192, 0x0024, 0x6090, /* 510 */
0x4050, 0x3000, 0x4024, 0x0006, 0x0910, 0x6312, 0x0024, 0x6194, /* 518 */
0x0001, 0x4122, 0x0024, 0x2800, 0x9f80, 0x3800, 0x4024, 0x0006, /* 520 */
0x12d2, 0x0006, 0x0991, 0x3000, 0x0024, 0x0006, 0x1290, 0x3a00, /* 528 */
0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x2900, 0xb200, 0x0000, /* 530 */
0x0580, 0x0030, 0x0210, 0x0014, 0x9240, 0x003f, 0xf502, 0x003f, /* 538 */
0xffc3, 0x3800, 0x0024, 0x0000, 0x0580, 0x0006, 0x1350, 0x3200, /* 540 */
0x4024, 0x4102, 0x0024, 0x3a00, 0x4024, 0x3810, 0x8024, 0x38f0, /* 548 */
0xc024, 0x0006, 0x08d0, 0x3800, 0x0024, 0x0030, 0x0690, 0x0000, /* 550 */
0x8280, 0xb880, 0x2080, 0x3800, 0x0024, 0x6890, 0x2000, 0x0030, /* 558 */
0x0490, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0100, 0x3000, /* 560 */
0x584c, 0xb100, 0x0024, 0x0000, 0x0024, 0x2800, 0xb185, 0x0000, /* 568 */
0x0024, 0x003f, 0xfec1, 0x3000, 0x1bcc, 0xb010, 0x0024, 0x2908, /* 570 */
0x0b80, 0x3800, 0x0024, 0x3613, 0x0024, 0x2910, 0x0180, 0x0000, /* 578 */
0xae48, 0x0007, 0x0001, 0x1806, 0x0006, 0x8007, 0x0000, 0x0006, /* 580 */
0x002f, 0x0010, 0x17ff, 0x0000, 0x1a00, 0x1dff, 0x0000, 0x1f00, /* 588 */
0x3fff, 0x0001, 0x0000, 0x17ff, 0x0001, 0x1c00, 0x3fff, 0x0001, /* 590 */
0xe000, 0xfffd, 0xffff, 0x0000, 0x0000, 0x180c, 0x180c, 0x0000, /* 598 */
0x0000, 0x0000, 0x4952, 0x4646, 0xffff, 0xffff, 0x4157, 0x4556, /* 5a0 */
0x6d66, 0x2074, 0x0010, 0x0000, 0x0001, 0x0001, 0xbb80, 0x0000, /* 5a8 */
0x7700, 0x0001, 0x0002, 0x0010, 0x6164, 0x6174, 0xffff, 0xffff, /* 5b0 */
0x0006, 0x8006, 0x0000, 0x0006, 0x0005, 0x183c, 0x183c, 0x0000, /* 5b8 */
0x0020, 0x0040, 0x0006, 0x8003, 0x0000, 0x0007, 0x0001, 0x5bc0, /* 5c0 */
0x0006, 0x0009, 0x801c, 0x7fe4, 0x8039, 0x804e, 0x7fb2, 0x809d, /* 5c8 */
0x809c, 0x7f64, 0x8139, 0x0007, 0x0001, 0x82c8, 0x0006, 0x0018, /* 5d0 */
0x4080, 0x184c, 0x3e13, 0x780f, 0x2800, 0xb405, 0x4090, 0x380e, /* 5d8 */
0x2400, 0xb3c0, 0xf400, 0x4417, 0x3110, 0x0024, 0x3f10, 0x0024, /* 5e0 */
0x36f3, 0x8024, 0x36f3, 0x580f, 0x2000, 0x0000, 0x0000, 0x0024, /* 5e8 */
0x0007, 0x0001, 0x82d4, 0x0006, 0x002a, 0x3e11, 0xb807, 0x3009, /* 5f0 */
0x384a, 0x3e11, 0x3805, 0x3e10, 0xb803, 0x3e00, 0x4442, 0x0001, /* 5f8 */
0x800a, 0xbf8e, 0x8443, 0xfe06, 0x0045, 0x3011, 0x0401, 0x545e, /* 600 */
0x0385, 0x525e, 0x2040, 0x72ce, 0x1bc1, 0x48ba, 0x9803, 0x4588, /* 608 */
0x4885, 0x6fee, 0x1bc2, 0x4ffe, 0x9805, 0xf6fe, 0x1bc4, 0xf7f0, /* 610 */
0x2046, 0x3801, 0xdbca, 0x2000, 0x0000, 0x36f1, 0x9807,
};
const uint16_t patch_size = 1567;
DEBUG("Patching...\n");
_spi->select_vs1053_xcs();
SPI.beginTransaction(*_spi_settings);
for (int i=0; i<patch_size; i++) {
DEBUG(" %d\n", i);
unsigned short addr, n, val;
addr = patch_data[i++];
n = patch_data[i++];
SPI.transfer(CMD_WRITE);
SPI.transfer(addr & 0XFF);
if (n & 0x8000U) { /* RLE run, replicate n samples */
n &= 0x7FFF;
val = patch_data[i++];
while (n--) {
SPI.transfer(val >> 8);
SPI.transfer(val & 0xFF);
_wait();
}
} else { /* Copy run, copy n samples */
while (n--) {
val = patch_data[i++];
SPI.transfer(val >> 8);
SPI.transfer(val & 0xFF);
_wait();
}
}
}
SPI.endTransaction();
_spi->select_vs1053_xcs(false);
DEBUG("Patch sent.\n");
}
void Player::_write_data(uint8_t* buffer) {
_spi->select_vs1053_xdcs();
SPI.beginTransaction(*_spi_settings);
for (uint i=0; i<sizeof(_buffer); i++) {
SPI.transfer(_buffer[i]);
}
SPI.endTransaction();
_spi->select_vs1053_xdcs(false);
}
uint16_t Player::_read_wram(uint16_t address) {
DEBUG("Reading WRAM address 0x%04X...\n", address);
_write_control_register(SCI_WRAMADDR, address);
uint16_t r1 = _read_control_register(SCI_WRAM);
_write_control_register(SCI_WRAMADDR, address);
uint16_t r2 = _read_control_register(SCI_WRAM);
if (r1 == r2) return r1;
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
_write_control_register(SCI_WRAMADDR, address);
r1 = _read_control_register(SCI_WRAM);
if (r1 == r2) return r1;
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
_write_control_register(SCI_WRAMADDR, address);
r2 = _read_control_register(SCI_WRAM);
if (r1 == r2) return r1;
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
DEBUG("Returning last value (0x%04X)...\n", r2);
return r2;
}
int8_t Player::_get_endbyte() {
int8_t endbyte = _read_wram(ADDR_ENDBYTE) & 0xFF;
DEBUG("Endbyte is 0x%02X.\n", endbyte);
return endbyte;
}
void Player::set_volume(uint8_t vol, bool save) {
if (save) {
_volume = vol;
}
INFO("Setting volume to %d\n", vol);
vol = 0xFF - vol;
uint16_t value = (vol<<8)|vol;
DEBUG("Setting volume register to 0x%04X\n", value);
_write_control_register(SCI_VOL, value);
}
void Player::vol_up() {
if (!is_playing()) return;
uint16_t vol = _volume + VOLUME_STEP;
if (vol > VOLUME_MAX) vol=VOLUME_MAX;
set_volume(vol);
}
void Player::vol_down() {
if (!is_playing()) return;
int16_t vol = _volume - VOLUME_STEP;
if (vol < VOLUME_MIN) vol=VOLUME_MIN;
set_volume(vol);
}
void Player::_mute() {
INFO("Muting.\n");
_speaker_off();
set_volume(1, false);
}
void Player::_unmute() {
INFO("Unmuting.\n");
set_volume(_volume, false);
_speaker_on();
}
void Player::track_next() {
if (_state != playing) return;
if (!_current_playlist->has_track_next()) {
return;
}
stop();
_current_playlist->track_next();
play();
}
void Player::track_prev() {
if (_state != playing) return;
if (_current_play_position > 100000) {
stop();
_current_playlist->track_restart();
play();
} else {
if (!_current_playlist->has_track_prev()) {
return;
}
stop();
_current_playlist->track_prev();
play();
}
}
void Player::set_track(uint8_t id) {
stop();
_current_playlist->set_track(id);
play();
}
bool Player::is_playing() {
return _state == playing;
}
bool Player::play(Playlist* p) {
_current_playlist = p;
return play();
}
bool Player::play() {
if (_state == sleeping || _state == recording) _wakeup();
if (_state != idle) return false;
if (_current_playlist == NULL) return false;
if (_current_playlist->get_file_count()==0) return false;
_speaker_on();
_current_playlist->start();
String file;
if (!_current_playlist->get_current_file(&file)) {
return false;
}
uint32_t position = _current_playlist->get_position();
_state = playing;
_play_file(file, position);
_controller->send_player_status();
return true;
}
void Player::_play_file(String file, uint32_t file_offset) {
INFO("play_file('%s', %d)\n", file.c_str(), file_offset);
_spi->select_sd();
if (file.startsWith("/")) {
_file = new SDDataSource(file);
} else if (file.startsWith("http")) {
_file = new HTTPSDataSource(file);
} else {
return;
}
_file_size = _file->size();
_spi->select_sd(false);
if (!_file || !_file->usable()) {
DEBUG("Could not open file %s", file.c_str());
return;
}
DEBUG("Resetting SCI_DECODE_TIME...\n");
_write_control_register(SCI_DECODE_TIME, 0);
DEBUG("Resetting SS_DO_NOT_JUMP...\n");
_write_control_register(SCI_STATUS, _read_control_register(SCI_STATUS) & ~SS_DO_NOT_JUMP);
delay(100);
_spi->select_sd();
if (file_offset == 0) {
_file->skip_id3_tag();
}
_refills = 0;
_current_play_position = _file->position();
_spi->select_sd(false);
_skip_to = file_offset;
if (_skip_to>0) _mute();
else _speaker_on();
INFO("Now playing.\n");
_controller->send_player_status();
}
void Player::_flush(uint count, int8_t byte) {
_spi->select_vs1053_xdcs();
SPI.beginTransaction(*_spi_settings);
for(uint i=0; i<count; i++) {
_wait();
SPI.transfer(byte);
}
SPI.endTransaction();
_spi->select_vs1053_xdcs(false);
}
void Player::_finish_playing() {
uint8_t endbyte = _get_endbyte();
_flush(2052, endbyte);
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
for (int i=0; i<64; i++) {
_flush(32, endbyte);
uint16_t mode = _read_control_register(SCI_MODE);
if ((mode & SM_CANCEL) == 0) return;
}
// If we reached this, the Chip didn't stop. That should not happen.
// (That's written in the manual.)
// Reset the chip.
init();
}
void Player::stop(bool turn_speaker_off) {
if (_state != playing) return;
INFO("Stopping...\n");
_current_playlist->set_position(_current_play_position);
_controller->pm->persist(_current_playlist);
_state = stopping;
_stop_delay = 0;
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
uint8_t endbyte = _get_endbyte();
while (true) {
_refill();
uint16_t mode = _read_control_register(SCI_MODE);
if ((mode & SM_CANCEL) == 0) {
_flush(2052, endbyte);
_finish_stopping(turn_speaker_off);
break;
} else if (_stop_delay > 2048) {
init();
break;
}
_stop_delay++;
}
}
void Player::_finish_stopping(bool turn_speaker_off) {
if (turn_speaker_off) _speaker_off();
_state = idle;
_stopped_at = millis();
if (_file) {
_file->close();
delete _file;
}
_current_play_position = 0;
_file_size = 0;
INFO("Stopped.\n");
_controller->send_player_status();
}
void Player::_refill() {
_spi->select_sd();
_refills++;
if (_refills % 1000 == 0) DEBUG(".");
uint8_t result = _file->read(_buffer, sizeof(_buffer));
_spi->select_sd(false);
if (result == 0) {
// File is over.
DEBUG("EOF reached.\n");
_skip_to = 0;
_finish_playing();
_finish_stopping(false);
if (_current_playlist->has_track_next()) {
_current_playlist->track_next();
play();
} else {
_current_playlist->reset();
_controller->send_player_status();
}
return;
}
_current_play_position+=result;
_write_data(_buffer);
if (_skip_to > 0) {
if (_skip_to > _file->position()) {
uint16_t status = _read_control_register(SCI_STATUS);
if ((status & SS_DO_NOT_JUMP) == 0) {
DEBUG("Skipping to %d.\n", _skip_to);
_flush(2048, _get_endbyte());
_spi->select_sd();
_file->seek(_skip_to);
_spi->select_sd(false);
_skip_to = 0;
_unmute();
_controller->send_position();
}
} else {
_skip_to = 0;
_unmute();
}
}
}
bool Player::_refill_needed() {
return _state==playing || _state==stopping;
}
bool Player::loop() {
if (PIN_VS1053_DREQ() && _refill_needed()) {
_refill();
return true;
}
if (_state == recording) {
DEBUG("r");
uint16_t samples_available = _read_control_register(SCI_HDAT1, false);
uint16_t vu_value = _read_control_register(SCI_AICTRL0, false);
DEBUG("Samples available: %4d, VU meter: 0x%04X\n", samples_available, vu_value);
if (samples_available >= 500) {
unsigned long sum = 0;
for (int i=0; i<500; i++) {
uint16_t sample = _read_control_register(SCI_HDAT0, false);
sum += sample * sample;
}
double result = sqrt(sum / 500);
DEBUG("Loudness: %f", result);
}
}
if (_state == idle && _stopped_at < millis() - VS1053_SLEEP_DELAY) {
_sleep();
//_record();
}
return false;
}
String Player::json() {
DynamicJsonDocument json(10240);
json["_type"] = "player";
json["playing"] = is_playing();
if (_current_playlist) {
JsonObject playlist = json.createNestedObject("playlist");
_current_playlist->json(playlist);
} else {
json["playlist"] = nullptr;
}
JsonObject volume = json.createNestedObject("volume");
volume["current"] = _volume;
volume["min"] = VOLUME_MIN;
volume["max"] = VOLUME_MAX;
volume["step"] = VOLUME_STEP;
return json.as<String>();
}
String Player::position_json() {
if (!is_playing()) return "null";
DynamicJsonDocument json(200);
json["_type"] = "position";
json["position"] = _current_play_position;
json["file_size"] = _file_size;
return json.as<String>();
}

View File

@ -1,399 +1,9 @@
#include <playlist.h> #include "playlist.h"
#include "spi_master.h"
#include "config.h"
#include <SD.h>
#include <algorithm>
#include <ArduinoJson.h>
#include <TinyXML.h>
#include "main.h"
Playlist::Playlist(String path) { void Playlist::add_file(String filename) {
_path = path; files.push_back(filename);
if (path.startsWith("/")) {
persistence = PERSIST_TEMPORARY;
_add_path(path);
} else if (path.startsWith("http")) {
_examine_http_url(path);
}
if (_title.length()==0) _title=path;
} }
void Playlist::_add_path(String path) { void Playlist::sort() {
SPIMaster::select_sd(); std::sort(files.begin(), files.end());
TRACE("Examining folder %s...\n", path.c_str());
if (!path.startsWith("/")) path = String("/") + path;
if (!SD.exists(path)) {
DEBUG("Could not open path '%s'.\n", path.c_str());
SPIMaster::select_sd(false);
return;
}
_title = path.substring(1);
int idx = _title.indexOf('/');
if (idx>0) {
_title.remove(idx);
}
File dir = SD.open(path);
File entry;
while (entry = dir.openNextFile()) {
String filename = entry.name();
filename = filename.substring(path.length() + 1);
String ext = filename.substring(filename.length() - 4);
if (!entry.isDirectory() &&
!filename.startsWith(".") &&
( ext.equals(".mp3") ||
ext.equals(".ogg") ||
ext.equals(".wma") ||
ext.equals(".mp4") ||
ext.equals(".mpa"))) {
TRACE(" Adding entry %s\n", entry.name());
String title = filename.substring(0, filename.length() - 4);
_files.push_back({.filename=entry.name(), .title=title, .id=String(_files.size())});
bool non_ascii_chars = false;
for(int i=0; i<filename.length(); i++) {
char c = filename.charAt(i);
if (c < 0x20 || c >= 0x7F) {
non_ascii_chars = true;
break;
}
}
if (non_ascii_chars) {
ERROR("WARNING: File '%s' contains non-ascii chars!\n", filename.c_str());
}
} else {
TRACE(" Ignoring entry %s\n", filename.c_str());
}
entry.close();
}
dir.close();
SPIMaster::select_sd(false);
std::sort(_files.begin(), _files.end());
}
void Playlist::_examine_http_url(String url) {
HTTPClientWrapper* http = new HTTPClientWrapper();
if (!http->get(url)) {
DEBUG("Could not GET %s.\n", url.c_str());
return;
}
String ct = http->getContentType();
DEBUG("Content-Type is %s.\n", ct.c_str());
if (ct.startsWith("audio/x-mpegurl")) {
_parse_m3u(http);
} else if (ct.startsWith("audio/")) {
persistence = PERSIST_NONE;
_files.push_back({.filename=url, .title=url, .id="none"});
} else if (ct.startsWith("application/rss+xml")) {
persistence = PERSIST_PERMANENTLY;
_parse_rss(http);
} else if (ct.startsWith("application/pls+xml")) {
persistence = PERSIST_PERMANENTLY;
_parse_pls(http);
} else {
ERROR("Unknown content type %s.\n", ct.c_str());
}
http->close();
delete http;
}
std::vector<PlaylistEntry>* xml_files_ptr = NULL;
String xml_last_tag = "";
String xml_title = "";
String xml_album_title = "";
String xml_url = "";
String xml_enclosure_url = "";
String xml_guid = "";
bool xml_enclosure_is_audio = false;
void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t dataLen) {
String tag(tagName);
if (status & STATUS_START_TAG) xml_last_tag = tag;
if (trace_enabled) {
if (status & STATUS_START_TAG) {
TRACE("Start of tag: %s\n", tagName);
} else if (status & STATUS_END_TAG) {
TRACE("End of tag: %s\n", tagName);
}
}
if (tag.equals("/rss/channel/title") && (status & STATUS_TAG_TEXT)) {
xml_album_title = data;
} else if (tag.endsWith("/title") && (status & STATUS_TAG_TEXT)) {
xml_title = String(data);
} else if (tag.endsWith("/guid") && (status & STATUS_TAG_TEXT)) {
xml_guid = data;
//} else if (xml_last_tag.endsWith("/item/enclosure") && (status & STATUS_ATTR_TEXT)) {
// DEBUG("tag: %s, data: %s\n", tag.c_str(), data);
} else if (xml_last_tag.endsWith("/enclosure") && tag.equals("type") && (status & STATUS_ATTR_TEXT) && String(data).indexOf("audio/")>=0) {
DEBUG("enclosure is audio\n");
xml_enclosure_is_audio = true;
} else if (xml_last_tag.endsWith("/enclosure") && tag.equals("url") && (status & STATUS_ATTR_TEXT)) {
DEBUG("found url\n");
xml_enclosure_url = String(data);
} else if (tag.endsWith("/enclosure") && (status & STATUS_END_TAG)) {
DEBUG("end of enclosure. xml_enclosure_is_audio: %d, xml_enclosure_url: %s\n", xml_enclosure_is_audio, xml_enclosure_url.c_str());
if (xml_enclosure_is_audio && xml_enclosure_url.length()>0) {
xml_url = xml_enclosure_url;
}
xml_enclosure_is_audio = false;
xml_enclosure_url = "";
} else if (tag.endsWith("/item") && (status & STATUS_END_TAG || status & STATUS_START_TAG)) {
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());
xml_files_ptr->insert(xml_files_ptr->begin(), {.filename=xml_url, .title=xml_title, .id=xml_guid});
}
xml_title = "";
xml_url = "";
xml_guid = "";
}
}
void Playlist::_parse_rss(HTTPClientWrapper* http) {
DEBUG("RSS parser running.\n");
// http is already initialized
int i;
TinyXML xml;
uint8_t* buffer = new uint8_t[150];
xml.init(buffer, 150, &xmlcb);
xml_files_ptr = &_files;
xml_title = "";
xml_album_title = "";
xml_url = "";
xml_enclosure_is_audio = false;
xml_enclosure_url = "";
while ((i = http->read()) >= 0) {
xml.processChar(i);
}
_current_track = _files.size()-1;
xml_files_ptr = NULL;
if (xml_album_title.length()>0) {
_title = xml_album_title;
}
xml_album_title = "";
delete buffer;
// don't close http at the end
DEBUG("RSS parser finished.\n");
}
void Playlist::_parse_m3u(HTTPClientWrapper* http) {
// http is already initialized
String line = "";
String title = "";
int i;
do {
i = http->read();
char c = i;
if (i>=0 && c!='\r' && c!='\n') {
line += c;
} else {
if (line.equals("#EXTM3U")) {
// Do nothing
} else if (line.startsWith("#EXTINF")) {
int idx = line.indexOf(",");
if (idx>4) {
// Get the title
title = line.substring(idx+1);
if (_title.length()==0) _title=title;
}
} else if (line.startsWith("http")) {
if (title.length()==0) title = line;
_files.push_back({.filename=line, .title=title, .id="none"});
title = "";
}
line = "";
}
} while (i>=0);
// don't close http at the end
}
void Playlist::_parse_pls(HTTPClientWrapper* http) {
// http is already initialized
String line;
String title = "";
String url = "";
int last_index = -1;
int index;
while(true) {
line = http->readLine();
if (line.startsWith("Title")) {
uint8_t eq_idx = line.indexOf('=');
if (eq_idx==-1) continue;
index = line.substring(5, eq_idx-4).toInt();
title = line.substring(eq_idx+1);
if (index != last_index) {
url = "";
last_index = index;
}
} else if (line.startsWith("File")) {
uint8_t eq_idx = line.indexOf('=');
if (eq_idx==-1) continue;
index = line.substring(5, eq_idx-4).toInt();
url = line.substring(eq_idx+1);
if (index != last_index) {
title = "";
last_index = index;
}
}
if (title.length()>0 && url.length()>0) {
_files.push_back({.filename=url, .title=title, .id="none"});
last_index = -1;
title = "";
url = "";
}
}
// don't close http at the end
}
String Playlist::path() {
return _path;
}
uint16_t Playlist::get_file_count() {
return _files.size();
}
void Playlist::start() {
_started = true;
}
bool Playlist::has_track_prev() {
return _current_track > 0;
}
bool Playlist::has_track_next() {
return _current_track < _files.size()-1;
}
bool Playlist::track_prev() {
if (_current_track > 0) {
_current_track--;
_position = 0;
return true;
}
return false;
}
bool Playlist::track_next() {
if (_current_track < _files.size()-1) {
_current_track++;
_position = 0;
return true;
}
return false;
}
bool Playlist::set_track(uint8_t track) {
if (track < _files.size()) {
_current_track = track;
_position = 0;
return true;
}
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() {
_position = 0;
}
void Playlist::shuffle(uint8_t random_offset) {
DEBUG("Shuffling the playlist with an offset of %d...\n", random_offset);
for (int i=random_offset; i<_files.size(); i++) {
int j = random(random_offset, _files.size()-1);
if (i!=j) {
TRACE(" Swapping elements %d and %d.\n", i, j);
PlaylistEntry temp = _files[i];
_files[i] = _files[j];
_files[j] = temp;
}
}
_shuffled = true;
TRACE("Done.\n");
}
void Playlist::advent_shuffle(uint8_t day) {
TRACE("advent_shuffle running...\n");
// 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.erase(_files.begin() + day, _files.end());
}
void Playlist::reset() {
std::sort(_files.begin(), _files.end());
_current_track = 0;
_position = 0;
_shuffled = false;
_started = false;
}
String Playlist::get_current_track_id() {
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() {
return _position;
}
void Playlist::set_position(uint32_t p) {
_position = p;
}
bool Playlist::is_fresh() {
return !_shuffled && !_started && _position==0 && _current_track==0;
}
void Playlist::dump() {
for (int i=0; i<_files.size(); i++) {
DEBUG(" %02d %2s %s\n", i+1, (i==_current_track) ? "->" : "", _files[i].filename.c_str());
}
}
void Playlist::json(JsonObject json) {
json["_type"] = "playlist";
json["title"] = _title;
JsonArray files = json.createNestedArray("files");
for (PlaylistEntry entry: _files) {
JsonObject o = files.createNestedObject();
o["filename"] = entry.filename;
o["title"] = entry.title;
o["id"] = entry.id;
}
json["current_track"] = _current_track;
json["has_track_next"] = has_track_next();
json["has_track_prev"] = has_track_prev();
} }

View File

@ -1,33 +1,29 @@
#include "playlist_manager.h" #include "playlist_manager.h"
#include <SD.h>
#include "spi_master.h" #include "spi_master.h"
#include <ArduinoJson.h> #include <SD.h>
PlaylistManager::PlaylistManager() { PlaylistManager::PlaylistManager() {
scan_files(); SPIMaster::enable_sd();
} current_rfid_tag_id = String("");
void PlaylistManager::scan_files() {
SPIMaster::select_sd();
std::vector<String> folders;
File root = SD.open("/"); File root = SD.open("/");
File entry; while(File entry = root.openNextFile()) {
while (entry = root.openNextFile()) { if (entry.isDirectory()) {
Serial.println(entry.name()); String filename("/");
String foldername = entry.name(); filename.concat(entry.name());
if (foldername.startsWith("/.")) continue; if (!filename.startsWith("/.")) {
foldername.remove(foldername.length()); Serial.println(filename);
folders.push_back(foldername); dirs.push_back(filename);
_check_for_special_chars(foldername); }
entry.close(); }
} }
_map.clear(); if (!SD.exists("/_mapping.txt")) {
if (!SD.exists("/_mapping.txt\n")) { Serial.println("WARNING: /_mapping.txt not found!");
ERROR("WARNING: File /_mapping.txt not found.\n");
} else { } else {
map.clear();
File f = SD.open("/_mapping.txt"); File f = SD.open("/_mapping.txt");
DEBUG("Reading /_mapping.txt...\n"); Serial.println(" Reading /_mapping.txt...");
while (f.available()) { while (f.available()) {
char buffer[512]; char buffer[512];
size_t pos = f.readBytesUntil('\n', buffer, 511); size_t pos = f.readBytesUntil('\n', buffer, 511);
@ -38,19 +34,19 @@ void PlaylistManager::scan_files() {
if (eq>0 && eq<data.length()-1) { if (eq>0 && eq<data.length()-1) {
String rfid_id = data.substring(0, eq); String rfid_id = data.substring(0, eq);
String folder = data.substring(eq + 1); String folder = data.substring(eq + 1);
TRACE(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str()); Serial.printf(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
_map[rfid_id] = folder; map[rfid_id] = folder;
if (folder.charAt(0)=='/') { if (folder.charAt(0)=='/') {
bool found=false; bool found=false;
for (String f: folders) { for (String f: dirs) {
if (f.equals(folder)) { if (f.equals(folder)) {
found = true; found = true;
break; break;
} }
} }
if (!found) { if (!found) {
INFO("WARNING: Found mapping for RFID id %s which maps to non-existing folder %s!\n", rfid_id.c_str(), folder.c_str()); Serial.printf("WARNING: Found mapping for RFID id %s which maps to non-existing folder %s!\n", rfid_id.c_str(), folder.c_str());
} }
} }
} }
@ -58,183 +54,40 @@ void PlaylistManager::scan_files() {
f.close(); f.close();
} }
root.close(); root.close();
}
_unmapped_folders.clear(); Playlist PlaylistManager::get_playlist(String rfid_id) {
for (String folder: folders) { if (rfid_id.equals(current_rfid_tag_id)) {
bool found = false; return current_playlist;
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) { } else {
if (it->second.equals(folder)) { if (map.count(rfid_id)==0) {
found = true; Serial.printf("No known playlist for id %s.\n", rfid_id);
break; return current_playlist;
} } else {
} current_playlist = Playlist();
if (!found) { String entry = map[rfid_id];
// Checking folder for media files if (entry.startsWith("/")) {
File dir = SD.open(folder); File dir = SD.open(entry);
while (entry = dir.openNextFile()) { while(File entry = dir.openNextFile()) {
String filename = entry.name(); String filename = entry.name();
filename = filename.substring(folder.length() + 1); String ext = filename.substring(filename.length()-4);
if (!filename.startsWith(".") && filename.endsWith(".mp3")) { if (!entry.isDirectory() &&
found = true; !filename.startsWith(".") &&
ext.equals(".mp3")) {
Serial.printf("Adding %s to the list of files\n", filename);
current_playlist.add_file(filename);
} }
entry.close(); entry.close();
if (found) break;
} }
if (found) { dir.close();
_unmapped_folders.push_back(folder);
} }
} current_playlist.sort();
} current_rfid_tag_id = rfid_id;
SPIMaster::select_sd(false); return current_playlist;
}
void PlaylistManager::_check_for_special_chars(String s) {
for(int i=0; i<s.length(); i++) {
char c = s.charAt(i);
if (c < 0x20 || c >= 0x7F) {
ERROR("WARNING: Folder / file '%s' contains non-ascii chars!\n", s.c_str());
return;
} }
} }
} }
Playlist* PlaylistManager::get_playlist_for_id(String id) { void PlaylistManager::set_audio_current_time(uint32_t time) {
if (!_map.count(id)) return NULL; audio_current_time = time;
String folder = _map[id];
return get_playlist_for_folder(folder);
}
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
Playlist* p;
if (!_playlists.count(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 p;
}
void PlaylistManager::dump_ids() {
for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) {
INFO(" %s -> %s\n", it->first.c_str(), it->second.c_str());
}
}
String PlaylistManager::json() {
DynamicJsonDocument json(10240);
json["_type"] = "playlist_manager";
JsonObject folders = json.createNestedObject("folders");
for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) {
folders[it->second] = it->first;
}
JsonArray started = json.createNestedArray("started");
for (std::map<String, Playlist*>::iterator it = _playlists.begin(); it!=_playlists.end(); it++) {
if (it->second->is_fresh()) continue;
started.add(it->first);
}
JsonArray unmapped = json.createNestedArray("unmapped");
for(String folder : _unmapped_folders) {
unmapped.add(folder);
}
return json.as<String>();
}
bool PlaylistManager::add_mapping(String id, String folder) {
DEBUG("Adding mapping: %s=>%s\n", id.c_str(), folder.c_str());
_map[id] = folder;
_save_mapping();
return true;
}
void PlaylistManager::_save_mapping() {
SPIMaster::select_sd();
File f = SD.open("/_mapping.txt", "w");
String s = create_mapping_txt();
unsigned char* buf = new unsigned char[s.length()];
s.getBytes(buf, s.length());
f.write(buf, s.length()-1);
f.close();
SPIMaster::select_sd(false);
delete buf;
}
String PlaylistManager::create_mapping_txt() {
String s;
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) {
s += it->first;
s += "=";
s += it->second;
s += '\n';
}
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());
}
} }

26
src/spi_master.cpp Normal file
View File

@ -0,0 +1,26 @@
#include "spi_master.h"
#include <Arduino.h>
#include "esmp3.h"
void SPIMaster::initialize() {
pinMode(PIN_CS_SD, OUTPUT);
pinMode(PIN_CS_RFID, OUTPUT);
disable_all();
}
void SPIMaster::disable_all() {
digitalWrite(PIN_CS_SD, HIGH);
digitalWrite(PIN_CS_RFID, HIGH);
}
void SPIMaster::enable_rfid() {
disable_all();
digitalWrite(PIN_CS_RFID, LOW);
delay(5);
}
void SPIMaster::enable_sd() {
disable_all();
digitalWrite(PIN_CS_SD, LOW);
delay(5);
}

View File

@ -1,96 +0,0 @@
#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;
}