Merge branch 'feature/rewrite-i2s'

This commit is contained in:
Fabian Schlenz 2022-08-25 14:53:43 +02:00
commit ab51af637e
27 changed files with 587 additions and 3499 deletions

178
README.md
View File

@ -1,174 +1,8 @@
# ESMP3
## What you need
Please note: This list is a "things I used", neither
"these are the best things for this stuff" nor "you
can only use these things". But please be aware that
using other stuff may lead to you having to make
more or less easy modifications.
Prizes are more or less the cheapest I could find on
Aliexpress.
| What? | For what? | Price (approx) |
|-------|-----------|----------------|
| ESP-32-WROOM-32D | Controlling everything |  4€ |
| WS1053B on a PCB with SD card slot | Play the MP3 files; provide an SD card slot |  5€ |
| MFRC522 | RFID reader | 1€ |
| 5V Amplifier(s) (e.g. 2x PAM-8302) | Single-channel Amplifier |  2€ |
| Speaker(s) matching your amp (e.g. 2pcs 4 Ohm 5W) | Enabling you to hear the sounds |  4€ |
| RFID tags (ISO14443A) - e.g. 10 cards | You can also get Keyfobs or stickers |  4€ |
| 4 buttons | Prev/Next track, Volume up/down |  1€ |
You'all also need an SD card, some breadboard(s), jumper cables and a soldering iron.
Also, some kind of box for the finished player.
## How to connect
Schematics coming soon...ish...
## How to install
Format your SD card with FAT32 and put files on it: Every album has
to go into its own folder in the root of the SD card. Folders and files
should not contain special characters (meaning stuff like äöüß). Spaces
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. 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
"WiFi connected. IP address: xxx.xxx.xxx.xxx" when the connection to
your WiFi succeeded.
In your browser, enter "http://xxx.xxx.xxx.xxx/" (using the IP address)
from above. From there you can define mappings between RFID tag IDs and
folders on the SD card.
## RFID-folder-mappings
### Via webinterface
To create a new mapping between an RFID tag and an folder, you can use
the web interface. Click the button with the cogs icon. After putting
your rfid tag on the reader (and possibly removing it again), its ID
will be shown in the dialog. Click the button with the arrows behind
the ID to start the mapping mode.
The dialog showing all folders with media files will be shown. Click the
button with the arrows behind the correct folder, to create the mapping.
### Manually
Mapping are stored on the SD card in the file `/_mapping.txt`. Every
mapping goes on its own line. Lines should be separated by \n (Unix-
style line endings); the last line should also end with a newline.
Format of a line is `<RFID id>=<folder>`. RFID id is the UID of an
RFID tag, expressed as 8 lowercase characters with leading 0 (if
necessary). Folder is the foldername to play; starting with a slash and
ending without one.
A valid `_mapping.txt` could look like this:
```
1a2b3c4d=/Christmas Music Vol. 17
003aab7f=/Let it go
b691a22c=/Frozen Audiobook
22cb6ae9=/Let it go
```
(Yes, more than one tag can map to a folder.)
## Technical details
### Ports
| Device | Port | Connected to |
| ------ | ---- | ------------ |
| VS1053 | CS | 16 |
| VS1053 | MISO | 19 |
| VS1053 | MOSI | 23 |
| VS1053 | SCK | 18 |
| VS1053 | XCS | 4 |
| VS1053 | XRESET | 0 |
| VS1053 | XDCS | 2 |
| VS1053 | DREQ | 15 |
| RC522 | SDA | 17 |
| RC522 | SCK | 18 |
| RC522 | MOSI | 23 |
| RC522 | MISO | 19 |
| AMP_L | SD | 27 |
| AMP_R | SD | 26 |
| BTN_PREV | | 22 |
| BTN_NEXT | | 33 |
| BTN_VOL_UP | | 21 |
| BTN_VOL_DOWN | | 32 |
Buttons pull to GND if pushed -> Internal Pull-Up needed!
### RFID tags
The mapping of rfid tags to files uses the ID of the
tag. A file called `_mapping.txt` in the root folder of
the SD card defines the mappings between RFID tag ids and
folders to play.
The easiest way to create this file is to use the mapping
functionality of the webinterface.
#### Special modes
You can also save data on the tags to further manipulate
the system. Position of the data is irrelevant, the whole
tag will be searched.
Using `[random]` will play the files in a random order.
`[random:2]` will randomize everything except the first 2
files. This can be useful for having the favorite song of
your kids playing, but after that getting a bit of randomness.
Using `[lock]` will turn this key into a key for the locking
mode. Scanning the tag enables locking mode. The next album
started will keep running until the end. Removing the tag
will be deactivated, as are the buttons for prev and next
track. You can disable locking mode by again scanning the
lock tag again.
`[advent]` is used for christmas time. An album with this tag
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.
#### API
You can send commands to ESMP3 using three different ways:
* Through a websocket connection to `ws://<IP>/ws`.
* Through the serial console using an USB cable.
* Via HTTP POST request to `http://<IP>/cmd`, having the
command in the variable `cmd`.
Supported commands are:
| Command | Action |
|---------|--------|
| `play <PATH>` | Starts playing the given path. Path may be a path on the
sd card or a http(s) URL of a webstream (direct links to mp3/4/ogg streams,
PLS files, M3U files or podcast XML feeds are supported). |
| `play` | Continues playing the previously played thing. |
| `stop` | Stops playing. |
| `volume=<X>` | Sets the volume to X (0-255). |
| `track_prev` | Starts the previous track, if available. |
| `track_next` | Starts the next track, if available. |
| `track=<X>` | Starts playing track no. X of the currently playing album. |
| `reset_vs1053` | Resets the VS1053 audio chip. |
| `reboot` | Reboots ESMP3. |
| `add_mapping=<ID>=<PATH>` | Adds a mapping between RFID card <ID> and path
<PATH>. See `play` for valid path formats. |
| `update` | Runs an update check. |
| `debug=<0|1>` | Enables / disables debug messages. This value is persisted across reboots. |
| `trace=<0|1>` | Enables / disables tracing messages. This value is also persisted across reboots. |
## Audio files
System messages are created using:
* https://ttsmp3.com/
* German / Vicki
* "Dies ist ein Text.<break time="1s"/>"
* Download as MP3.

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,32 @@
#pragma once
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include "config.h"
class Controller;
#include "player.h"
#include <MFRC522v2.h>
#include <MFRC522Debug.h>
#include "esmp3.h"
#include "playlist.h"
#include "playlist_manager.h"
#include "http_server.h"
#undef DEPRECATED
#include <MFRC522.h>
enum ControllerState { NORMAL, LOCKING, LOCKED };
class Controller {
private:
MFRC522* _rfid;
HTTPServer* _http_server;
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:
Controller(Player* p, PlaylistManager* pm);
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();
};
private:
void handle_buttons();
void handle_rfid();
bool is_button_pressed(uint8_t pin);
Playlist current_playlist;
bool is_rfid_present = false;
unsigned long last_rfid_check = 0;
unsigned long last_button_check = 0;
unsigned long last_position_save = 0;
uint8_t button_pressed = 0;
unsigned long button_pressed_since = 0;
bool button_already_processed = false;
String read_rfid_data();
public:
void handle();
void next_track();
void prev_track();
void play();
void play(String rfid_id, bool shuffle=false);
void stop();
void eof_mp3(String info);
};

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();
};

26
include/esmp3.h Normal file
View File

@ -0,0 +1,26 @@
#pragma once
#include "controller.h"
#include "playlist_manager.h"
#include <Audio.h>
#define PIN_CS_SD 22
#define PIN_CS_RFID 21
#define PIN_BTN_VOL_UP 32
#define PIN_BTN_VOL_DOWN 33
#define PIN_BTN_TRACK_NEXT 17
#define PIN_BTN_TRACK_PREV 16
#define I2S_DOUT 25
#define I2S_BCLK 26
#define I2S_LRC 27
class Controller;
extern Controller controller;
extern Audio audio;
extern PlaylistManager* pm;
extern MFRC522* rfid;
void save_audio_current_time();

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

@ -0,0 +1,9 @@
#pragma once
struct PersistedPlaylist {
String dir;
uint16_t file = 0;
uint32_t position = 0;
PersistedPlaylist(String s="") : dir(s) {}
};

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,30 @@
#pragma once
#include <Arduino.h>
#include <vector>
#include <ArduinoJson.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; }
};
#include <Arduino.h>
#include "persisted_playlist.h"
class Playlist {
private:
uint32_t _position = 0;
uint32_t _current_track = 0;
bool _started = false;
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:
PlaylistPersistence persistence = PERSIST_TEMPORARY;
Playlist(String path);
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);
private:
std::vector<String> files;
uint8_t current_file = 0;
uint32_t current_time = 0;
String rfid_id;
PersistedPlaylist* pp;
public:
Playlist();
Playlist(String rfid_id, PersistedPlaylist* p);
void add_file(String filename);
void sort();
String get_rfid_id();
String get_current_file_name();
bool next_track();
bool prev_track();
void restart();
void set_current_time(uint32_t time);
uint32_t get_current_time();
void shuffle();
void set_current_position(uint8_t file, uint32_t position=0);
void save_current_position(uint32_t position=0);
};

View File

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

View File

@ -1,70 +1,9 @@
#pragma once
#include <Arduino.h>
#include <SPI.h>
#include "config.h"
class SPIMaster {
public:
static uint8_t state;
static void init() {
PIN_SD_CS_SETUP();
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;
}
};
public:
static void enable_sd();
static void enable_rfid();
static void disable_all();
static void initialize();
};

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

@ -12,29 +12,31 @@
default_envs = esp32
[extra]
lib_deps =
63 @ 1.4.5 ; MFRC522
https://github.com/me-no-dev/ESPAsyncWebServer.git
ArduinoJSON
6691 ; TinyXML
lib_deps =
[env:esp32]
platform = espressif32
board = esp-wrover-kit
framework = arduino
upload_speed = 115200
build_flags=!./build_version.sh
lib_deps = ${extra.lib_deps}
;upload_port = 10.10.2.108 ; /dev/cu.SLAB_USBtoUART
upload_speed = 921600
build_flags = -DCORE_DEBUG_LEVEL=5 -DCONFIG_ARDUHAL_LOG_COLORS=1 ; !./build_version.sh
lib_deps =
${extra.lib_deps}
esphome/ESP32-audioI2S@^2.1.0
computer991/Arduino_MFRC522v2@^2.0.1
https://github.com/dplasa/FTPClientServer
;upload_port = 10.10.2.108
monitor_speed = 115200
board_build.embed_txtfiles = src/index.html
;board_build.partitions = partitions.csv
;monitor_port = /dev/cu.wchusbserial1420
monitor_port = /dev/cu.usbserial-0001
monitor_filters = esp32_exception_decoder
[env:deploy]
platform = espressif32
board = esp-wrover-kit
framework = arduino
lib_deps = ${extra.lib_deps}
lib_deps =
${extra.lib_deps}
esphome/ESP32-audioI2S@^2.1.0
computer991/Arduino_MFRC522v2@^2.0.1
board_build.embed_txtfiles = src/index.html
board_build.partitions = partitions.csv

View File

@ -1,463 +1,226 @@
#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>
#include "esmp3.h"
Controller::Controller(Player* p, PlaylistManager* playlist_manager) {
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) {
_http_server = h;
}
void Controller::loop() {
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;
void Controller::handle() {
if (last_rfid_check + 500 < millis() || last_rfid_check > millis()) {
handle_rfid();
last_rfid_check = millis();
}
if ((_last_position_info_at < now - POSITION_SEND_INTERVAL) || (now < _last_position_info_at)) {
send_position();
_last_position_info_at = now;
if (last_button_check + 10 < millis() || last_button_check > millis()) {
handle_buttons();
last_button_check = millis();
}
_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;
if (last_position_save + 10000 < millis() || last_position_save > millis()) {
current_playlist.save_current_position(audio.getFilePos());
last_position_save = millis();
//Serial.println(pm->pp_to_String().c_str());
}
}
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;
void Controller::handle_buttons() {
if (is_button_pressed(PIN_BTN_VOL_UP)) {
log_i("BTN_VOL_UP pressed");
uint8_t vol = min(audio.getVolume()+2, 21);
log_d("Setting new volume %d", vol);
audio.setVolume(vol);
} else if (is_button_pressed(PIN_BTN_VOL_DOWN)) {
log_i("BTN_VOL_DOWN pressed");
uint8_t vol;
if ((vol = audio.getVolume()) >= 3) {
vol -= 2;
} else {
vol = 1;
}
log_d("Setting new volume %d", vol);
audio.setVolume(vol);
} else if (is_button_pressed(PIN_BTN_TRACK_NEXT)) {
log_i("BTN_TRACK_NEXT pressed");
next_track();
} else if (is_button_pressed(PIN_BTN_TRACK_PREV)) {
log_i("BTN_TRACK_PREV pressed");
prev_track();
}
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) {
void Controller::handle_rfid() {
if (is_rfid_present) {
byte buffer[2];
byte buffer_size = 2;
SPIMaster::select_rc522();
status = _rfid->PICC_WakeupA(buffer, &buffer_size);
if (status == MFRC522::STATUS_OK) {
MFRC522Constants::StatusCode status = rfid->PICC_WakeupA(buffer, &buffer_size);
if (status == MFRC522Constants::STATUS_OK) {
// Card is still present.
_rfid->PICC_HaltA();
SPIMaster::select_rc522(false);
return;
rfid->PICC_HaltA();
} else {
Serial.printf("RFID status is %s\n", MFRC522Debug::GetStatusCodeName(status));
is_rfid_present = false;
Serial.println("No more RFID card.\n");
stop();
}
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;
if (rfid->PICC_IsNewCardPresent()) {
if (rfid->PICC_ReadCardSerial()) {
uint32_t uid = rfid->uid.uidByte[0]<<24 | rfid->uid.uidByte[1]<<16 | rfid->uid.uidByte[2]<<8 | rfid->uid.uidByte[3];
Serial.printf("Found new rfid card with uid %x\n", uid);
is_rfid_present = true;
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);
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;
String data = read_rfid_data();
play(s_uid, data.indexOf("[random]")>=0);
}
rfid->PICC_HaltA();
}
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;
String Controller::read_rfid_data() {
log_v("read_rfid_data() running...");
MFRC522::StatusCode status;
MFRC522::PICC_Type type = rfid->PICC_GetType(rfid->uid.sak);
uint16_t pageStart = 0;
uint16_t pages = 4;
uint16_t pageSize = 1;
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;
case MFRC522Constants::PICC_TYPE_MIFARE_MINI:
case MFRC522Constants::PICC_TYPE_MIFARE_1K:
case MFRC522Constants::PICC_TYPE_MIFARE_4K: {
log_v("Trying to authenticate Mifare card.");
MFRC522::MIFARE_Key key = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
status = rfid->PCD_Authenticate(MFRC522Constants::PICC_CMD_MF_AUTH_KEY_A, 4, &key, &rfid->uid);
if (status == MFRC522Constants::STATUS_OK) {
log_v("Authentication succeeded.");
} else {
log_v("Authentication failed. Trying to read anyway.");
}
pageStart = 4;
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]);
}
}
case MFRC522Constants::PICC_TYPE_MIFARE_UL:
log_v("PICC type is Mifare Ultralight. No authentication necessary.");
pages = 16;
pageSize = 4;
break;
default:
log_v("Unexpected rfid card type %s. Trying to read anyway.", MFRC522Debug::PICC_GetTypeName(type));
}
String data = "";
for (uint8_t block=pageStart; block<pages+pageStart; block+=pageSize) {
byte buffer[18];
uint8_t byte_count = 18;
status = rfid->MIFARE_Read(block, buffer, &byte_count);
if (status != MFRC522Constants::STATUS_OK) {
log_d("MIFARE_Read() failed: %s\n", String(MFRC522Debug::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);
rfid->PCD_StopCrypto1();
log_v("Read rfid data: '%s'", data.c_str());
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();
void Controller::play(String rfid_id, bool shuffle) {
if (!rfid_id.equals(current_playlist.get_rfid_id())) {
if (pm->has_playlist(rfid_id)) {
current_playlist = pm->get_playlist(rfid_id);
if (shuffle) {
log_i("Shuffling the playlist.");
current_playlist.shuffle();
}
play();
} else {
Serial.printf("There is no playlist for rfid_id %s\n", rfid_id.c_str());
// This is working more or less, but downloading files is really, REALLY slow. (About 4 minutes for 10 MBytes).
//download_album(rfid_id);
audio.connecttoFS(SD, "/system/sys_unknown_card.mp3");
}
} else {
if (!audio.isRunning()) {
play();
}
}
}
void Controller::play() {
String file = current_playlist.get_current_file_name();
if (file.startsWith("/")) {
log_i("Playing file %s via connecttoFS", file.c_str());
audio.connecttoFS(SD, file.c_str(), current_playlist.get_current_time());
} else if (file.startsWith("http")) {
log_i("Playing URL %s via connecttohost", file.c_str());
audio.connecttoFS(SD, "/system/sys_connecting.mp3");
while (audio.isRunning()) {
yield();
audio.loop();
}
audio.connecttohost(file.c_str());
}
}
void Controller::next_track() {
if (current_playlist.next_track()) {
play();
}
}
void Controller::prev_track() {
uint32_t time = audio.getAudioCurrentTime();
log_d("prev_track() called. getAudioCurrentTime() returns %d", time);
if (time >= 5) {
log_d("Restarting current track.");
current_playlist.restart();
play();
} else {
if (current_playlist.prev_track()) {
play();
}
}
}
void Controller::stop() {
if (audio.isRunning()) {
current_playlist.set_current_time(audio.stopSong());
}
}
bool Controller::is_button_pressed(uint8_t pin) {
//log_d("Button %d state is %d", pin, digitalRead(pin));
if (!digitalRead(pin)) {
// Button is pressed - let's debounce it.
if (button_pressed == pin) {
if (button_pressed_since + 150 < millis() && !button_already_processed) {
button_already_processed = true;
return true;
}
} 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);
button_pressed = pin;
button_pressed_since = millis();
button_already_processed = false;
}
} 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");
if (button_pressed == pin) {
button_pressed = 0;
}
}
return false;
}
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;