Massive changes. Added a quite nice webinterface with live data using WebSockets. Removed the FTP server (wasn't that useful anyways). JSON creating using ArduinoJson instead of String concatenation. Ans, and, and.

This commit is contained in:
2019-11-16 23:03:13 +01:00
parent e471a57578
commit b9a4770ff2
21 changed files with 620 additions and 2806 deletions

View File

@ -2,11 +2,15 @@
#include "spi_master.h"
#include "config.h"
#include "playlist.h"
#include "http_server.h"
#include <ArduinoJson.h>
Controller::Controller(Player* p, PlaylistManager* pm) {
_player = p;
_pm = pm;
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
_player->register_controller(this);
BTN_NEXT_SETUP();
BTN_PREV_SETUP();
@ -29,19 +33,28 @@ void Controller::set_mqtt_client(MQTTClient* m) {
_mqtt_client = m;
}
void Controller::register_http_server(HTTPServer* h) {
_http_server = h;
}
void Controller::loop() {
TRACE("Controller::loop()...\n");
unsigned long now = millis();
if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) {
_check_rfid();
_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 ((_last_mqtt_report_at < now - MQTT_REPORT_INTERVAL) || (now < _last_mqtt_report_at)) {
_send_mqtt_report();
_last_mqtt_report_at = now;
if (_cmd_queue.length() > 0) {
process_message(_cmd_queue);
_cmd_queue = "";
}
TRACE("Controller::loop() done.\n");
}
uint32_t Controller::_get_rfid_card_uid() {
@ -60,6 +73,7 @@ uint32_t Controller::_get_rfid_card_uid() {
}
void Controller::_check_rfid() {
TRACE("check_rfid running...\n");
MFRC522::StatusCode status;
if (_rfid_present) {
byte buffer[2];
@ -91,9 +105,11 @@ void Controller::_check_rfid() {
}
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) {
@ -110,7 +126,15 @@ void Controller::_check_rfid() {
return;
}
int index;
if (data.indexOf("[random]") != -1 && pl->is_fresh()) {
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 {
// TODO
}
} 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);
@ -133,11 +157,13 @@ void Controller::_check_rfid() {
}
_player->play(pl);
send_playlist_manager_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
@ -205,12 +231,14 @@ String Controller::_read_rfid_data() {
}
void Controller::_check_serial() {
TRACE("check_serial running...\n");
if (Serial.available() > 0) {
char c = Serial.read();
Serial.printf("%c", c);
if (c==10 || c==13) {
if (_serial_buffer.length()>0) {
_execute_serial_command(_serial_buffer);
process_message(_serial_buffer);
_serial_buffer = String();
}
} else {
@ -219,18 +247,19 @@ void Controller::_check_serial() {
}
}
void Controller::_execute_serial_command(String cmd) {
bool Controller::process_message(String cmd) {
DEBUG("Executing command: %s\n", cmd.c_str());
if (cmd.startsWith("play ")) {
Playlist* p = _pm->get_playlist_for_id(cmd.substring(5));
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_random_album();
} else if (cmd.equals("play")) {
_player->play();
} else if (cmd.equals("stop")) {
_player->stop();
} else if (cmd.equals("help")) {
@ -239,16 +268,28 @@ void Controller::_execute_serial_command(String cmd) {
_player->vol_down();
} else if (cmd.equals("+")) {
_player->vol_up();
} else if (cmd.equals("p")) {
} 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("n")) {
} 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 {
ERROR("Unknown command: %s\n", cmd.c_str());
return false;
}
return;
return true;
}
void Controller::_execute_command_ls(String path) {
@ -272,6 +313,8 @@ void Controller::_execute_command_help() {
}
void Controller::_check_buttons() {
TRACE("check_buttons running...\n");
if (BTN_PREV() && _debounce_button(0)) {
if (_state == NORMAL) {
_player->track_prev();
@ -300,33 +343,55 @@ bool Controller::_debounce_button(uint8_t index) {
_button_last_pressed_at[index] = millis();
return ret;
}
String Controller::get_status_json() {
String response = String("{");
response.concat("\"state\": \"");
response.concat(_player->is_playing() ? "playing" : "idle");
response.concat("\", ");
if (_player->is_playing()) {
// TODO
//response.concat("\"album\": \"");
//response.concat(_player->album());
//response.concat("\", \"track\": ");
//response.concat(_player->track());
//response.concat(", \"position\": ");
//response.concat(_player->position());
//response.concat(", ");
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;
}
response.concat("\"volume\": ");
response.concat(_player->volume());
response.concat(", \"volume_max\": ");
response.concat(VOLUME_MAX);
response.concat(", \"volume_min\": ");
response.concat(VOLUME_MIN);
response.concat(", \"rfid_uid\": ");
response.concat(String(_last_rfid_card_uid, HEX));
response.concat("}\n");
return response;
JsonObject rfid = json.createNestedObject("last_rfid");
rfid["uid"] = _last_rfid_uid;
rfid["data"] = _last_rfid_data;
return json.as<String>();
}
void Controller::_send_mqtt_report() {
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::inform_new_client(AsyncWebSocketClient* client) {
String s;
s += _pm->json();
s += '\n';
s += _player->json();
s += '\n';
s += _player->position_json();
client->text(s);
}
void Controller::queue_command(String s) {
_cmd_queue = s;
}

View File

@ -1,32 +1,23 @@
/*
#include "http_server.h"
#include <ESPmDNS.h>
HTTPServer::HTTPServer(Player* p, Controller* c) {
_player = p;
_controller = c;
_http_server = new ESP8266WebServer(80);
//_http_server->onFileUpload([&]() { _handle_upload(); yield();});
_http_server->on("/upload", HTTP_POST, [&]() {
_http_server->sendHeader("Connection", "close");
_http_server->send(200, "text/plain", "OK");
}, [&]() {
_handle_upload();
yield();
});
_http_server->on("/", HTTP_GET, [&](){ _handle_index(); });
_http_server->on("/status", HTTP_GET, [&](){ _handle_status(); });
_http_server->begin();
_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("/", [&](AsyncWebServerRequest* req) {_handle_index(req);});
_server->begin();
MDNS.addService("http", "tcp", 80);
}
void HTTPServer::_handle_upload() {
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
HTTPUpload* upload = &_http_server->upload();
DEBUG("_handle_upload Status: %d, length: %d\n", upload->status, upload->currentSize);
if (upload->status == UPLOAD_FILE_START) {
if (index == 0) { // Starting upload
_chunk = new uint8_t[512];
_chunk_length = 0;
_upload_position = 0;
@ -35,19 +26,13 @@ void HTTPServer::_handle_upload() {
_need_header = true;
}
if (upload->status == UPLOAD_FILE_END || upload->status == UPLOAD_FILE_ABORTED) {
// Close the file
delete _chunk;
return;
}
uint32_t upload_offset = 0;
while (upload_offset < upload->currentSize) {
while (upload_offset < len) {
// Load a chunk
if (_chunk_length < 512 && upload->currentSize > upload_offset) {
if (_chunk_length < 512 && len > upload_offset) {
uint16_t needed = 512 - _chunk_length;
if (needed > upload->currentSize - upload_offset) needed = upload->currentSize - upload_offset;
memcpy(_chunk + _chunk_length, upload->buf + upload_offset, needed);
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;
@ -97,29 +82,30 @@ void HTTPServer::_handle_upload() {
_chunk_length = 0;
}
}
}
if (final == true) {
// Close the file
delete _chunk;
return;
}
}
void HTTPServer::_handle_index() {
String response = String("<html><head><title>ESMP3</title><script>function play_album(e) {}</script></head><body>");
response.concat("Albums on SD card:<table>");
std::list<String> files = _player->ls("/", false, true, false);
for(std::list<String>::iterator it=files.begin(); it!=files.end(); it++) {
response.concat("<tr><td>");
response.concat(*it);
response.concat("</td><td><a href='#' onclick='play_album();'>Play</a></td></tr>\n");
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) {
DEBUG("Received ws message: %s\n", (char*)data);
_controller->queue_command((char*)data);
}
}
response.concat("</table></body></html>");
_http_server->send(200, "text/html", response);
}
void HTTPServer::_handle_status() {
_http_server->send(200, "application/json", _controller->get_status_json());
}
void HTTPServer::_handle_index(AsyncWebServerRequest* r) {
void HTTPServer::loop() {
_http_server->handleClient();
MDNS.update();
}
*/
#include "index.html"
r->send(200, "text/html", html);
}

View File

@ -2,6 +2,7 @@
#include <SPI.h>
#include <SD.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include "config.h"
#include "controller.h"
#include "player.h"
@ -9,14 +10,13 @@
#include "http_server.h"
#include "mqtt_client.h"
#include "playlist_manager.h"
#include <ESP8266FtpServer.h>
Controller* controller;
Player* player;
PlaylistManager* pm;
//HTTPServer* http_server;
FtpServer* ftp_server;
HTTPServer* http_server;
MQTTClient* mqtt_client;
unsigned long last_mqtt_report = 0;
void setup() {
@ -65,18 +65,28 @@ void setup() {
delay(1000);
ESP.restart();
}
INFO("WiFi connected.\n");
INFO("WiFi connected. IP address: %s\n", WiFi.localIP().toString().c_str());
mqtt_client = new MQTTClient();
//MDNS.begin("esmp3");
MDNS.begin("esmp3");
controller->set_mqtt_client(mqtt_client);
DEBUG("Setting up WiFi and web server...\n");
//http_server = new HTTPServer(player, controller);
DEBUG("Setting up HTTP server...\n");
http_server = new HTTPServer(player, controller);
controller->register_http_server(http_server);
ftp_server = new FtpServer();
ftp_server->begin("user", "pass");
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");
}
INFO("Initialization completed.\n");
}
@ -86,11 +96,9 @@ void loop() {
if (more_data_needed) return;
controller->loop();
//http_server->loop();
ftp_server->handleFTP();
mqtt_client->loop();
if ((last_mqtt_report + 10000 < millis()) || last_mqtt_report > millis()) {
last_mqtt_report = millis();
mqtt_client->publish_status(controller->get_status_json());
mqtt_client->publish_status(controller->json());
}
}

View File

@ -2,6 +2,7 @@
#include "player.h"
#include "spi_master.h"
#include <ArduinoJson.h>
//Player::_spi_settings
@ -15,7 +16,11 @@ Player::Player(SPIMaster* s) {
_spi->disable();
PIN_VS1053_DREQ_SETUP();
_init();
init();
}
void Player::register_controller(Controller* c) {
_controller = c;
}
void Player::_reset() {
@ -27,7 +32,7 @@ void Player::_reset() {
_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow
}
void Player::_init() {
void Player::init() {
DEBUG("Resetting VS1053...\n");
_reset();
@ -91,6 +96,7 @@ void Player::_sleep() {
_write_control_register(SCI_AUDATA, 0x0010);
set_volume(0, false);
_state = sleeping;
TRACE("VS1053 is sleeping now.\n");
}
void Player::_wakeup() {
@ -506,6 +512,12 @@ void Player::track_prev() {
}
}
void Player::set_track(uint8_t id) {
stop();
_current_playlist->set_track(id);
play();
}
bool Player::is_playing() {
return _state == playing;
}
@ -518,10 +530,13 @@ bool Player::play(Playlist* p) {
bool Player::play() {
if (_state == sleeping || _state == recording) _wakeup();
if (_state != idle) return false;
if (_current_playlist == NULL) return false;
_current_playlist->start();
String file = _current_playlist->get_current_file();
uint32_t position = _current_playlist->get_position();
_state = playing;
_play_file(file, position);
_controller->send_player_status();
return true;
}
@ -529,6 +544,7 @@ 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();
_spi->select_sd(false);
if (!_file) {
DEBUG("Could not open file %s", file.c_str());
@ -552,6 +568,7 @@ void Player::_play_file(String file, uint32_t file_offset) {
if (_skip_to>0) _mute();
else _speaker_on();
INFO("Now playing.\n");
_controller->send_player_status();
}
uint32_t Player::_id3_tag_offset(File f) {
@ -601,7 +618,7 @@ void Player::_finish_playing() {
// If we reached this, the Chip didn't stop. That should not happen.
// (That's written in the manual.)
// Reset the chip.
_init();
init();
}
void Player::stop(bool turn_speaker_off) {
@ -635,7 +652,10 @@ void Player::_finish_stopping(bool turn_speaker_off) {
if (_file) {
_file.close();
}
_current_play_position = 0;
_file_size = 0;
INFO("Stopped.\n");
_controller->send_player_status();
}
void Player::_refill() {
@ -655,7 +675,9 @@ void Player::_refill() {
play();
} else {
_current_playlist->reset();
_controller->send_player_status();
}
return;
}
_current_play_position+=result;
@ -672,6 +694,7 @@ void Player::_refill() {
_spi->select_sd(false);
_skip_to = 0;
_unmute();
_controller->send_position();
}
} else {
_skip_to = 0;
@ -712,3 +735,29 @@ bool Player::loop() {
}
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() {
DynamicJsonDocument json(200);
json["_type"] = "position";
json["position"] = _current_play_position;
json["file_size"] = _file_size;
return json.as<String>();
}

View File

@ -3,6 +3,7 @@
#include "config.h"
#include <SD.h>
#include <algorithm>
#include <ArduinoJson.h>
Playlist::Playlist(String path) {
// Add files to _files
@ -29,6 +30,17 @@ Playlist::Playlist(String path) {
ext.equals(".mpa"))) {
TRACE(" Adding entry %s\n", entry.name());
_files.push_back(entry.name());
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());
}
@ -39,6 +51,10 @@ Playlist::Playlist(String path) {
std::sort(_files.begin(), _files.end());
}
void Playlist::start() {
_started = true;
}
bool Playlist::has_track_prev() {
return _current_track > 0;
}
@ -65,6 +81,14 @@ bool Playlist::track_next() {
return false;
}
bool Playlist::set_track(uint8_t track) {
if (track < _files.size()) {
_current_track = track;
return true;
}
return false;
}
void Playlist::track_restart() {
_position = 0;
}
@ -84,11 +108,21 @@ void Playlist::shuffle(uint8_t random_offset) {
TRACE("Done.\n");
}
void Playlist::advent_shuffle(uint8_t day) {
if (day > 24) day = 24;
if (day > _files.size()) return;
_files.insert(_files.begin(), _files[day - 1]);
_files.erase(_files.begin() + day - 1, _files.end());
}
void Playlist::reset() {
std::sort(_files.begin(), _files.end());
_current_track = 0;
_position = 0;
_shuffled = false;
_started = false;
}
String Playlist::get_current_file() {
@ -104,5 +138,22 @@ void Playlist::set_position(uint32_t p) {
}
bool Playlist::is_fresh() {
return !_shuffled && _position==0 && _current_track==0;
}
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].c_str());
}
}
void Playlist::json(JsonObject json) {
json["_type"] = "playlist";
JsonArray files = json.createNestedArray("files");
for (String file: _files) {
files.add(file);
}
json["current_track"] = _current_track;
json["has_track_next"] = has_track_next();
json["has_track_prev"] = has_track_prev();
}

View File

@ -1,6 +1,7 @@
#include "playlist_manager.h"
#include <SD.h>
#include "spi_master.h"
#include <ArduinoJson.h>
PlaylistManager::PlaylistManager() {
SPIMaster::select_sd();
@ -8,12 +9,40 @@ PlaylistManager::PlaylistManager() {
File entry;
while (entry = root.openNextFile()) {
String foldername = entry.name();
if (!entry.isDirectory() || foldername.startsWith("/.")) continue;
// Remove trailing slash
foldername.remove(foldername.length());
TRACE("Looking at %s...", foldername.c_str());
if (!entry.isDirectory() || foldername.startsWith("/.")) continue;
DEBUG(" Checking %s...\n", foldername.c_str());
bool non_ascii_chars = false;
for(int i=0; i<foldername.length(); i++) {
char c = foldername.charAt(i);
if (c < 0x20 || c >= 0x7F) {
non_ascii_chars = true;
break;
}
}
if (non_ascii_chars) {
ERROR("WARNING: Folder '%s' contains non-ascii chars!\n", foldername.c_str());
}
if (!SD.exists(foldername + "/ids.txt")) {
TRACE("No ids.txt -> ignoring\n");
TRACE("No ids.txt -> checking for media files...\n");
File file;
bool media_files = false;
while(file = entry.openNextFile()) {
String filename = file.name();
filename = filename.substring(foldername.length() + 1);
if (!filename.startsWith(".") && filename.endsWith(".mp3")) {
media_files = true;
}
file.close();
if (media_files) break;
}
if (media_files) {
_unmapped_folders.push_back(foldername);
}
continue;
}
File f = SD.open(foldername + "/ids.txt");
@ -47,6 +76,10 @@ PlaylistManager::PlaylistManager() {
Playlist* PlaylistManager::get_playlist_for_id(String id) {
if (!_map.count(id)) return NULL;
String folder = _map[id];
return get_playlist_for_folder(folder);
}
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
if (!_playlists.count(folder)) {
_playlists[folder] = new Playlist(folder);
}
@ -58,3 +91,22 @@ void PlaylistManager::dump_ids() {
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] = folders[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>();
}