Compare commits

...

8 Commits

12 changed files with 242 additions and 86 deletions

View File

@ -37,7 +37,9 @@ and dashes an alike are okay. Put the SD card into the SD card slot.
Copy `include/config.sample.h` to `include/config.h`. Modify it to at
least contain the correct login details for your WiFi.
The code then should compile in PlatformIO without errors.
The code then should compile in PlatformIO without errors. Upload it
to your ESP32. After that, upload static files using PlatformIO's task
"Upload file system image".
The serial console in PlatformIO should give you more or less useful
messages about what's going on. There will also be a line saying
@ -142,4 +144,4 @@ will only play in December. On December 1st, only track 1
will play. On December 2nd, track 2 followed by track 1. On
December 3rd, tracks 3, 1 and 2. From December 24th on, track
24 followed by tracks 1-23. So your kid will get the "daily track"
first, followed by all previous tags in the right order.
first, followed by all previous tags in the right order.

View File

@ -18,7 +18,6 @@ class Controller {
private:
MFRC522* _rfid;
HTTPServer* _http_server;
PlaylistManager* _pm;
ControllerState _state = NORMAL;
bool _rfid_enabled = true;
void _check_rfid();
@ -30,7 +29,7 @@ private:
bool _rfid_present = false;
String _last_rfid_uid = "";
String _last_rfid_data = "";
Player* _player;
unsigned long _last_rfid_scan_at = 0;
unsigned long _last_position_info_at = 0;
String _serial_buffer = String();
@ -42,6 +41,8 @@ private:
bool _check_button(uint8_t btn);
public:
Controller(Player* p, PlaylistManager* pm);
PlaylistManager* pm;
Player* player;
void register_http_server(HTTPServer* h);
void loop();
void send_controller_status();

55
include/data_sources.h Normal file
View File

@ -0,0 +1,55 @@
#pragma once
#include <Arduino.h>
#include <SD.h>
#include "config.h"
#include <HTTPClient.h>
class DataSource {
private:
public:
DataSource() {};
virtual ~DataSource() {};
virtual size_t read(uint8_t* buf, size_t len) = 0;
virtual uint8_t 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);
uint8_t 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;
HTTPClient* _http = NULL;
uint32_t _length;
uint32_t _position;
public:
HTTPSDataSource(String url, uint32_t offset=0);
~HTTPSDataSource();
size_t read(uint8_t* buf, size_t len);
uint8_t read();
size_t position();
void seek(size_t position);
size_t size();
void close();
bool usable();
};

View File

@ -4,6 +4,7 @@
#include <SD.h>
#include "spi_master.h"
#include "playlist.h"
#include "data_sources.h"
class Player;
@ -55,7 +56,6 @@ private:
void _flush_and_cancel();
int8_t _get_endbyte();
void _flush(uint count, int8_t fill_byte);
uint32_t _id3_tag_offset(File f);
void _play_file(String filename, uint32_t offset);
void _finish_playing();
void _finish_stopping(bool turn_speaker_off);
@ -72,7 +72,7 @@ private:
SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0);
SPISettings* _spi_settings = &_spi_settings_slow;
File _file;
DataSource* _file;
uint32_t _file_size = 0;
uint8_t _buffer[32];
uint32_t _current_play_position = 0;

View File

@ -11,7 +11,7 @@ private:
bool _shuffled = false;
std::vector<String> _files;
public:
Playlist(String path);
Playlist(String path, bool is_url=false);
void start();
bool has_track_next();
bool has_track_prev();

View File

@ -19,4 +19,5 @@ public:
void scan_files();
String json();
bool add_mapping(String id, String folder);
String create_mapping_txt();
};

View File

@ -5,12 +5,12 @@
#include "http_server.h"
#include <ArduinoJson.h>
Controller::Controller(Player* p, PlaylistManager* pm) {
_player = p;
_pm = pm;
Controller::Controller(Player* p, PlaylistManager* playlist_manager) {
player = p;
pm = playlist_manager;
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
_player->register_controller(this);
player->register_controller(this);
BTN_NEXT_SETUP();
BTN_PREV_SETUP();
@ -87,7 +87,7 @@ void Controller::_check_rfid() {
_rfid_present = false;
INFO("No more RFID card.\n");
if (_state != LOCKED) {
_player->stop();
player->stop();
}
send_controller_status();
} else {
@ -106,7 +106,7 @@ void Controller::_check_rfid() {
String data = _read_rfid_data();
_last_rfid_data = data;
Playlist* pl = _pm->get_playlist_for_id(s_uid);
Playlist* pl = pm->get_playlist_for_id(s_uid);
if (data.indexOf("[lock]") != -1) {
if (_state == LOCKED) {
_state = NORMAL;
@ -152,7 +152,7 @@ void Controller::_check_rfid() {
DEBUG("ControllerState is now LOCKED.\n");
}
_player->play(pl);
player->play(pl);
//send_playlist_manager_status();
send_controller_status();
}
@ -248,38 +248,38 @@ 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);
Playlist* p = pm->get_playlist_for_folder(cmd.substring(5));
player->play(p);
//} else if (cmd.equals("ls")) {
// _execute_command_ls("/");
//} else if (cmd.startsWith("ls ")) {
// _execute_command_ls(cmd.substring(3));
} else if (cmd.equals("play")) {
_player->play();
player->play();
} else if (cmd.equals("stop")) {
_player->stop();
player->stop();
} else if (cmd.equals("help")) {
_execute_command_help();
} else if (cmd.equals("-")) {
_player->vol_down();
player->vol_down();
} else if (cmd.equals("+")) {
_player->vol_up();
player->vol_up();
} else if (cmd.startsWith("volume=")) {
uint8_t vol = cmd.substring(7).toInt();
_player->set_volume(vol);
player->set_volume(vol);
} else if (cmd.equals("track_prev")) {
_player->track_prev();
player->track_prev();
} else if (cmd.equals("track_next")) {
_player->track_next();
player->track_next();
} else if (cmd.startsWith("track=")) {
uint8_t track = cmd.substring(6).toInt();
_player->set_track(track);
player->set_track(track);
} else if (cmd.equals("ids")) {
_pm->dump_ids();
pm->dump_ids();
} else if (cmd.equals("reset_vs1053")) {
_player->stop();
_player->init();
player->stop();
player->init();
} else if (cmd.equals("reboot")) {
ESP.restart();
} else if (cmd.startsWith("add_mapping=")) {
@ -287,7 +287,7 @@ bool Controller::process_message(String cmd) {
uint8_t idx = rest.indexOf('=');
String id = rest.substring(0, idx);
String folder = rest.substring(idx + 1);
_pm->add_mapping(id, folder);
pm->add_mapping(id, folder);
send_playlist_manager_status();
} else {
ERROR("Unknown command: %s\n", cmd.c_str());
@ -299,7 +299,7 @@ bool Controller::process_message(String cmd) {
void Controller::_execute_command_ls(String path) {
INFO("Listing contents of %s:\n", path.c_str());
// TODO
//std::list<String> files = _player->ls(path);
//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());
//}
@ -321,17 +321,17 @@ void Controller::_check_buttons() {
if (BTN_PREV() && _debounce_button(0)) {
if (_state == NORMAL) {
_player->track_prev();
player->track_prev();
} else {
DEBUG("Ignoring btn_prev because state is LOCKED.\n");
}
} else if (BTN_VOL_UP() && _debounce_button(1)) {
_player->vol_up();
player->vol_up();
} else if (BTN_VOL_DOWN() && _debounce_button(2)) {
_player->vol_down();
player->vol_down();
} else if (BTN_NEXT() && _debounce_button(3)) {
if (_state == NORMAL) {
_player->track_next();
player->track_next();
} else {
DEBUG("Ignoring btn_next because state is LOCKED.\n");
}
@ -360,6 +360,8 @@ String Controller::json() {
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();
return json.as<String>();
}
@ -367,22 +369,22 @@ 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());
_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());
_http_server->ws->textAll(pm->json());
}
}
void Controller::send_position() {
TRACE("In send_position()...\n");
if (_http_server->ws->count() > 0 && _player->is_playing()) {
_http_server->ws->textAll(_player->position_json());
if (_http_server->ws->count() > 0) {
_http_server->ws->textAll(player->position_json());
}
_last_position_info_at = millis();
}
@ -396,11 +398,11 @@ void Controller::send_controller_status() {
void Controller::inform_new_client(AsyncWebSocketClient* client) {
String s;
s += _pm->json();
s += pm->json();
s += '\n';
s += _player->json();
s += player->json();
s += '\n';
s += _player->position_json();
s += player->position_json();
s += '\n';
s += json();
client->text(s);
@ -412,6 +414,6 @@ void Controller::queue_command(String s) {
}
void Controller::update_playlist_manager() {
_pm->scan_files();
pm->scan_files();
send_playlist_manager_status();
}
}

93
src/data_sources.cpp Normal file
View File

@ -0,0 +1,93 @@
#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); }
uint8_t 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) {
uint8_t tries_left = 5;
int status;
do {
if (tries_left == 0) {
ERROR("Redirection loop? Cancelling!\n");
return;
}
tries_left--;
DEBUG("Connecting to %s...\n", url.c_str());
if (_http) delete _http;
_http = new HTTPClient();
_http->setUserAgent("PodBox 0.1");
const char* headers[] = {"Location"};
_http->collectHeaders(headers, 1);
bool result = _http->begin(url);
DEBUG("HTTP->begin result: %d\n", result);
if (!result) return;
status = _http->GET();
DEBUG("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");
} else {
ERROR("Got redirection HTTP code, but could not find Location header.\n");
for(int i=0; i<_http->headers(); i++) {
DEBUG(" Header: %s=%s\n", _http->headerName(i).c_str(), _http->header(i).c_str());
}
return;
}
} else if (status != HTTP_CODE_OK) {
DEBUG("Unexpected HTTP return code. Cancelling.\n");
return;
}
} while (status != HTTP_CODE_OK);
_length = _http->getSize();
DEBUG("Content-Length: %d\n", _length);
_stream = _http->getStreamPtr();
}
HTTPSDataSource::~HTTPSDataSource() {
if (_stream) _stream->stop();
_http->end();
delete _stream;
delete _http;
}
bool HTTPSDataSource::usable() { return _http && _stream; }
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _stream->read(buf, len); _position += result; return result; }
uint8_t HTTPSDataSource::read() { _position++; return _stream->read(); }
size_t HTTPSDataSource::position() { return _position; }
void HTTPSDataSource::seek(size_t position) { return; /* TODO */ }
size_t HTTPSDataSource::size() { return _length; }
void HTTPSDataSource::close() { _stream->stop(); }

View File

@ -10,8 +10,15 @@ HTTPServer::HTTPServer(Player* p, Controller* c) {
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("/", [&](AsyncWebServerRequest* req) {req->send(SPIFFS, "/index.html", "text/html");});
_server->on("/", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(SPIFFS, "/index.html", "text/html");});
_server->on("/upload", HTTP_POST, [](AsyncWebServerRequest* req) {req->send(200); }, ([&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){this->_handle_upload(request, filename, index, data, len, final);}));
_server->on("/_mapping.txt", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "text/plain", _controller->pm->create_mapping_txt());});
_server->on("/player.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->player->json());});
_server->on("/playlist_manager.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->pm->json());});
_server->on("/controller.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->json());});
_server->on("/position.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->player->position_json());});
_server->on("/cmd", HTTP_POST, [&](AsyncWebServerRequest *req) {req->send(200); }, NULL, [&](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {_controller->queue_command((char*)data);});
_server->begin();
MDNS.addService("http", "tcp", 80);
}
@ -133,4 +140,4 @@ void HTTPServer::_onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client
_controller->queue_command((char*)data);
}
}
}
}

View File

@ -463,12 +463,14 @@ void Player::set_volume(uint8_t vol, bool save) {
}
void Player::vol_up() {
if (!is_playing()) return;
uint8_t vol = _volume + VOLUME_STEP;
if (vol > VOLUME_MAX) vol=VOLUME_MAX;
set_volume(vol);
}
void Player::vol_down() {
if (!is_playing()) return;
uint8_t vol = _volume - VOLUME_STEP;
if (vol < VOLUME_MIN) vol=VOLUME_MIN;
set_volume(vol);
@ -543,10 +545,16 @@ bool Player::play() {
void Player::_play_file(String file, uint32_t file_offset) {
INFO("play_file('%s', %d)\n", file.c_str(), file_offset);
_spi->select_sd();
_file = SD.open(file);
_file_size = _file.size();
if (file.startsWith("/")) {
_file = new SDDataSource(file);
} else if (file.startsWith("https://")) {
_file = new HTTPSDataSource(file);
} else {
return;
}
_file_size = _file->size();
_spi->select_sd(false);
if (!_file) {
if (!_file || !_file->usable()) {
DEBUG("Could not open file %s", file.c_str());
return;
}
@ -559,10 +567,10 @@ void Player::_play_file(String file, uint32_t file_offset) {
_spi->select_sd();
if (file_offset == 0) {
_file.seek(_id3_tag_offset(_file));
_file->skip_id3_tag();
}
_refills = 0;
_current_play_position = _file.position();
_current_play_position = _file->position();
_spi->select_sd(false);
_skip_to = file_offset;
if (_skip_to>0) _mute();
@ -571,30 +579,6 @@ void Player::_play_file(String file, uint32_t file_offset) {
_controller->send_player_status();
}
uint32_t Player::_id3_tag_offset(File f) {
uint32_t original_position = f.position();
uint32_t offset = 0;
if (f.read()=='I' && f.read()=='D' && f.read()=='3') {
DEBUG("ID3 tag found\n");
// Skip ID3 tag version
f.read(); f.read();
byte tags = f.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 & f.read());
}
offset += 10;
if (footer_present) offset += 10;
DEBUG("ID3 tag length is %d bytes.\n", offset);
} else {
DEBUG("No ID3 tag found\n");
}
f.seek(original_position);
return offset;
}
void Player::_flush(uint count, int8_t byte) {
_spi->select_vs1053_xdcs();
SPI.beginTransaction(*_spi_settings);
@ -650,7 +634,8 @@ void Player::_finish_stopping(bool turn_speaker_off) {
_state = idle;
_stopped_at = millis();
if (_file) {
_file.close();
_file->close();
delete _file;
}
_current_play_position = 0;
_file_size = 0;
@ -662,7 +647,7 @@ void Player::_refill() {
_spi->select_sd();
_refills++;
if (_refills % 1000 == 0) DEBUG(".");
uint8_t result = _file.read(_buffer, sizeof(_buffer));
uint8_t result = _file->read(_buffer, sizeof(_buffer));
_spi->select_sd(false);
if (result == 0) {
// File is over.
@ -684,13 +669,13 @@ void Player::_refill() {
_write_data(_buffer);
if (_skip_to > 0) {
if (_skip_to > _file.position()) {
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);
_file->seek(_skip_to);
_spi->select_sd(false);
_skip_to = 0;
_unmute();
@ -755,6 +740,7 @@ String Player::json() {
}
String Player::position_json() {
if (!is_playing()) return "null";
DynamicJsonDocument json(200);
json["_type"] = "position";
json["position"] = _current_play_position;

View File

@ -5,7 +5,11 @@
#include <algorithm>
#include <ArduinoJson.h>
Playlist::Playlist(String path) {
Playlist::Playlist(String path, bool is_url) {
if (is_url) {
_files.push_back(path);
return;
}
// Add files to _files
SPIMaster::select_sd();
TRACE("Examining folder %s...\n", path.c_str());

View File

@ -143,6 +143,16 @@ bool PlaylistManager::add_mapping(String id, String folder) {
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;
@ -150,10 +160,5 @@ void PlaylistManager::_save_mapping() {
s += it->second;
s += '\n';
}
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;
}
return s;
}