Compare commits

..

8 Commits

30 changed files with 4027 additions and 602 deletions

178
README.md
View File

@ -1,8 +1,174 @@
# ESMP3
## Audio files
System messages are created using:
* https://ttsmp3.com/
* German / Vicki
* "Dies ist ein Text.<break time="1s"/>"
* Download as MP3.
## 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. |

84
include/config.sample.h Normal file
View File

@ -0,0 +1,84 @@
#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,32 +1,61 @@
#pragma once
#include <Arduino.h>
#include <MFRC522v2.h>
#include <MFRC522Debug.h>
#include "esmp3.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 {
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();
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 = "";
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);
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();
};

58
include/data_sources.h Normal file
View File

@ -0,0 +1,58 @@
#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 bool usable() = 0;
virtual int peek(int offset) = 0;
void skip_id3_tag();
};
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();
bool usable();
int peek(int offset=0);
};
class HTTPSDataSource : public DataSource {
private:
WiFiClient* _stream = NULL;
HTTPClientWrapper* _http = NULL;
uint32_t _position;
String _url;
void _init(String url, uint32_t offset);
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();
int peek(int offset=0);
};

View File

@ -1,26 +0,0 @@
#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

@ -0,0 +1,38 @@
#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();
int peek(int offset=0);
};

29
include/http_server.h Normal file
View File

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

13
include/main.h Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include <Preferences.h>
void wifi_connect();
extern bool debug_enabled;
extern bool trace_enabled;
extern Preferences prefs;
extern const uint8_t file_index_html_start[] asm("_binary_src_webinterface_index_html_gz_start");
extern const size_t file_index_html_size asm("_binary_src_webinterface_index_html_gz_size");
extern const uint8_t file_timezones_json_start[] asm("_binary_src_webinterface_timezones_json_gz_start");
extern const size_t file_timezones_json_size asm("_binary_src_webinterface_timezones_json_gz_size");

View File

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

104
include/player.h Normal file
View File

@ -0,0 +1,104 @@
#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,30 +1,60 @@
#pragma once
#include <vector>
#include <Arduino.h>
#include "persisted_playlist.h"
#include <vector>
#include <ArduinoJson.h>
#include "main.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 {
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);
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);
};

View File

@ -1,25 +1,24 @@
#pragma once
#include <vector>
#include <map>
#include <Arduino.h>
#include <vector>
#include "playlist.h"
#include "persisted_playlist.h"
class Playlist;
class PlaylistManager {
private:
Playlist get_playlist_for_tag_id(String id);
String current_rfid_tag_id;
uint32_t audio_current_time = 0;
public:
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:
PlaylistManager();
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();
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);
};

View File

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

10
include/updater.h Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include "http_client_wrapper.h"
class Updater {
public:
static void run();
static bool do_update(int cmd, String url, String expected_md5);
static bool read_line(String* dst, HTTPClientWrapper* http, String expected_key);
};

View File

@ -11,32 +11,27 @@
[platformio]
default_envs = esp32
[extra]
[env]
platform = espressif32
board = esp-wrover-kit
framework = arduino
upload_speed = 512000
lib_deps =
63 ; MFRC522
https://github.com/me-no-dev/ESPAsyncWebServer.git
64 ; ArduinoJSON
6691 ; TinyXML
monitor_speed = 115200
board_build.embed_files =
src/webinterface/timezones.json.gz
src/webinterface/index.html.gz
;board_build.partitions = partitions.csv
;monitor_port = /dev/cu.wchusbserial1420
extra_scripts =
post:tools/post_build.py
[env:esp32]
platform = espressif32
board = esp-wrover-kit
framework = arduino
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
monitor_port = /dev/cu.usbserial-0001
monitor_filters = esp32_exception_decoder
build_flags=!./build_version.sh
upload_port = /dev/cu.SLAB_USBtoUART
[env:deploy]
platform = espressif32
board = esp-wrover-kit
framework = arduino
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,67 +1,111 @@
#include "controller.h"
#include "esmp3.h"
#include "main.h"
#include "spi_master.h"
#include "config.h"
#include "playlist.h"
#include "http_server.h"
#include "updater.h"
#include <ArduinoJson.h>
void Controller::handle() {
if (last_rfid_check + 500 < millis() || last_rfid_check > millis()) {
handle_rfid();
last_rfid_check = millis();
}
if (last_button_check + 10 < millis() || last_button_check > millis()) {
handle_buttons();
last_button_check = millis();
}
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());
}
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::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;
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;
}
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 {
vol = 1;
_last_update_check_at = now;
}
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();
#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;
}
}
void Controller::handle_rfid() {
if (is_rfid_present) {
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;
MFRC522Constants::StatusCode status = rfid->PICC_WakeupA(buffer, &buffer_size);
if (status == MFRC522Constants::STATUS_OK) {
SPIMaster::select_rc522();
status = _rfid->PICC_WakeupA(buffer, &buffer_size);
if (status == MFRC522::STATUS_OK) {
// Card is still present.
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();
_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 {
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;
uint32_t uid = _get_rfid_card_uid();
if (uid > 0) {
String temp = String(uid, HEX);
String s_uid = "";
@ -69,158 +113,351 @@ void Controller::handle_rfid() {
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();
String data = _read_rfid_data();
_last_rfid_data = data;
play(s_uid, data.indexOf("[random]")>=0);
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;
}
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() {
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 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;
}
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 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 = "";
for (uint8_t block=pageStart; block<pages+pageStart; block+=pageSize) {
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, buffer, &byte_count);
if (status != MFRC522Constants::STATUS_OK) {
log_d("MIFARE_Read() failed: %s\n", String(MFRC522Debug::GetStatusCodeName(status)).c_str());
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->PCD_StopCrypto1();
log_v("Read rfid data: '%s'", data.c_str());
}
}
_rfid->PICC_HaltA();
_rfid->PCD_StopCrypto1();
DEBUG("Data from RFID: %s\n", data.c_str());
SPIMaster::select_rc522(false);
return data;
}
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");
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 {
if (!audio.isRunning()) {
play();
_serial_buffer.concat(c);
}
}
}
void Controller::play() {
String file = current_playlist.get_current_file_name();
bool Controller::process_message(String cmd) {
DEBUG("Executing command: %s\n", cmd.c_str());
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();
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);
}
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 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 {
button_pressed = pin;
button_pressed_since = millis();
button_already_processed = false;
}
} else {
if (button_pressed == pin) {
button_pressed = 0;
}
}
ERROR("Unknown command: %s\n", cmd.c_str());
return false;
}
return true;
}
void Controller::eof_mp3(String info) {
log_d("Handling eof. Keep playing until the file is finished.");
while(audio.isRunning()) { audio.loop(); yield; }
if (info.startsWith("sys_")) {
log_d("File ending was a system audio file. Not running next_track.");
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 {
next_track();
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();
}

71
src/data_sources.cpp Normal file
View File

@ -0,0 +1,71 @@
#include "data_sources.h"
void DataSource::skip_id3_tag() {
if (peek(0)=='I' && peek(1)=='D' && peek(2)=='3') {
DEBUG("ID3 tag found\n");
// Skip ID3 tag marker
read(); read(); read();
// Skip ID3 tag version
read(); read();
byte tags = read();
bool footer_present = tags & 0x10;
DEBUG("ID3 footer found: %d\n", footer_present);
uint32_t offset = 0;
for (byte i=0; i<4; i++) {
offset <<= 7;
offset |= (0x7F & read());
}
offset += 10;
if (footer_present) offset += 10;
DEBUG("ID3 tag length is %d bytes.\n", offset);
seek(offset);
} else {
DEBUG("No ID3 tag found\n");
}
}
////////////// 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; }
int SDDataSource::peek(int offset) {
if (offset==0) return _file.peek();
size_t start_position = _file.position();
_file.seek(start_position + offset);
int result = _file.peek();
_file.seek(start_position);
return result;
}
////////////// HTTPSDataSource //////////////
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
_url = url;
_init(url, offset);
}
void HTTPSDataSource::_init(String url, uint32_t offset) {
_url = url;
_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) { _http->close(); delete _http; _init(_url, position); }
size_t HTTPSDataSource::size() { return _http->getSize(); }
void HTTPSDataSource::close() { _http->close(); }
int HTTPSDataSource::peek(int offset) { return _http->peek(offset); }

View File

@ -1,139 +0,0 @@
#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>
#include <MFRC522v2.h>
#include <MFRC522DriverSPI.h>
#include <MFRC522DriverPinSimple.h>
#include <MFRC522Debug.h>
#include <Arduino.h>
#include <Wire.h>
#include <FTPServer.h>
Controller controller;
Audio audio;
PlaylistManager* pm;
MFRC522* rfid;
FTPServer ftp(SD);
void setup() {
pinMode(PIN_CS_SD, OUTPUT); digitalWrite(PIN_CS_SD, HIGH);
pinMode(PIN_CS_RFID, OUTPUT); digitalWrite(PIN_CS_RFID, HIGH);
Serial.begin(115200);
WiFi.begin("Schlenz", "1410WischlingenPanda");
log_i("Connecting to WiFi...");
uint8_t i=9;
while(WiFi.status() != WL_CONNECTED) {
Serial.print(i);
Serial.print("... ");
delay(1000);
i--;
if (i==0) {
Serial.println("Could not connect to WiFi. Restarting in 1s.");
delay(1000);
ESP.restart();
}
}
Serial.println();
Serial.print("Connected to WiFi. IP address: ");
Serial.println(WiFi.localIP());
ArduinoOTA.begin();
log_i("Waiting for OTA...");
for(int i=0; i<20; i++) {
ArduinoOTA.handle();
delay(100);
}
Serial.println("Setting up audio...");
audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
audio.setVolume(1);
audio.forceMono(true);
audio.setBufsize(30000, -1);
audio.setConnectionTimeout(1000, 1000);
Serial.println("Initializing SPI...");
SPI.begin();
//SPI.setHwCs(false);
//SPIMaster::initialize();
Serial.print("Initializing SD card...");
//SPIMaster::enable_sd();
while(!SD.begin(PIN_CS_SD, 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 rfid reader...");
pinMode(PIN_CS_RFID, OUTPUT);
MFRC522DriverPin* pin = new MFRC522DriverPinSimple(PIN_CS_RFID);
MFRC522Driver* spi = new MFRC522DriverSPI(*pin);
rfid = new MFRC522(*spi);
rfid->PCD_Init();
MFRC522Debug::PCD_DumpVersionToSerial(*rfid, Serial);
Serial.println("Setting up buttons...");
pinMode(PIN_BTN_VOL_UP, INPUT_PULLUP);
pinMode(PIN_BTN_VOL_DOWN, INPUT_PULLUP);
pinMode(PIN_BTN_TRACK_NEXT, INPUT_PULLUP);
pinMode(PIN_BTN_TRACK_PREV, INPUT_PULLUP);
Serial.println("Setup finished.");
audio.setVolume(12);
audio.connecttoFS(SD, "/system/sys_ready.mp3");
ftp.begin("", "");
}
void loop() {
ArduinoOTA.handle();
controller.handle();
audio.loop();
ftp.handleFTP();
}
void audio_info(const char *info){
Serial.print("info "); Serial.println(info);
}
void audio_id3data(const char *info){ //id3 metadata
Serial.print("id3data ");Serial.println(info);
}
void audio_eof_mp3(const char *info){ //end of file
Serial.print("eof_mp3 ");Serial.println(info);
controller.eof_mp3(info);
}
void audio_showstation(const char *info){
Serial.print("station ");Serial.println(info);
}
void audio_showstreamtitle(const char *info){
Serial.print("streamtitle ");Serial.println(info);
}
void audio_bitrate(const char *info){
Serial.print("bitrate ");Serial.println(info);
}
void audio_commercial(const char *info){ //duration in sec
Serial.print("commercial ");Serial.println(info);
}
void audio_icyurl(const char *info){ //homepage
Serial.print("icyurl ");Serial.println(info);
}
void audio_lasthost(const char *info){ //stream URL played
Serial.print("lasthost ");Serial.println(info);
}
void audio_eof_speech(const char *info){
Serial.print("eof_speech ");Serial.println(info);
}

222
src/http_client_wrapper.cpp Normal file
View File

@ -0,0 +1,222 @@
#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");
}
int HTTPClientWrapper::peek(int offset) {
if (_buffer_position >= _buffer_length) _fill_buffer();
if (_buffer_position + offset < 0 || _buffer_position + offset >= _buffer_length) {
return -1;
}
return _buffer[_buffer_position + offset];
}

172
src/http_server.cpp Normal file
View File

@ -0,0 +1,172 @@
#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_P(200, "text/html", file_index_html_start, file_index_html_size);
});
_server->on("/timezone.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
AsyncWebServerResponse* res = req->beginResponse_P(200, "application/json", file_timezones_json_start, file_timezones_json_size);
res->addHeader("Content-Encoding", "gzip");
req->send(res);
});
_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);
}
}
}

144
src/main.cpp Normal file
View File

@ -0,0 +1,144 @@
#include <Arduino.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) {
DEBUG("Connected to WiFi \"%s\".\n", WiFi.SSID().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);
PIN_SPEAKER_L_SETUP();
PIN_SPEAKER_R_SETUP();
PIN_SPEAKER_L(LOW);
PIN_SPEAKER_R(LOW);
DEBUG("Setting up SPI...\n");
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(42, 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();
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();
}

753
src/player.cpp Normal file
View File

@ -0,0 +1,753 @@
// 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,82 +1,398 @@
#include "playlist.h"
#include <playlist.h>
#include "spi_master.h"
#include "config.h"
#include <SD.h>
#include <algorithm>
#include <ArduinoJson.h>
#include <TinyXML.h>
Playlist::Playlist() {}
Playlist::Playlist(String id, PersistedPlaylist* p) {
rfid_id = id;
pp = p;
}
String Playlist::get_rfid_id() {
return rfid_id;
}
void Playlist::add_file(String filename) {
files.push_back(filename);
}
void Playlist::sort() {
std::sort(files.begin(), files.end());
}
void Playlist::set_current_position(uint8_t file, uint32_t bytes) {
log_d("Setting position: File %d, bytes %d.", file, bytes);
current_file = file;
current_time = bytes;
save_current_position();
}
void Playlist::save_current_position(uint32_t position) {
if (position==0) {
position = current_time;
}
log_d("Saving current position: File %d, bytes %d.", current_file, position);
if (pp != NULL) {
pp->file = current_file;
pp->position = position;
Playlist::Playlist(String path) {
_path = path;
if (path.startsWith("/")) {
persistence = PERSIST_TEMPORARY;
_add_path(path);
} else if (path.startsWith("http")) {
_examine_http_url(path);
}
if (_title.length()==0) _title=path;
}
String Playlist::get_current_file_name() {
if (current_file >= files.size()) {
Serial.printf("Requested a file number %d, which is not available in this playlist. Starting over.\n", current_file);
set_current_position(0);
void Playlist::_add_path(String path) {
SPIMaster::select_sd();
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;
}
return files[current_file];
}
bool Playlist::next_track() {
if (files.size() <= current_file + 1) {
Serial.println("next_track does not exist. Resetting playlist and returning false.");
set_current_position(0, 0);
return false;
_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());
}
set_current_position(current_file + 1, 0);
return true;
}
bool Playlist::prev_track() {
log_d("Playlist::prev_track called. current_file is %d", current_file);
if (current_file == 0) {
set_current_position(0, 0);
} else {
set_current_position(current_file - 1, 0);
TRACE(" Ignoring entry %s\n", filename.c_str());
}
return files.size()>0;
entry.close();
}
dir.close();
SPIMaster::select_sd(false);
std::sort(_files.begin(), _files.end());
}
void Playlist::restart() {
current_time = 0;
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") || ct.startsWith("application/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;
}
void Playlist::set_current_time(uint32_t pos) {
set_current_position(current_file, pos);
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 = "";
}
}
uint32_t Playlist::get_current_time() {
return current_time;
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::shuffle() {
std::random_shuffle(files.begin(), files.end());
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,17 +1,32 @@
#include "playlist_manager.h"
#include "spi_master.h"
#include <SD.h>
#include "spi_master.h"
#include <ArduinoJson.h>
PlaylistManager::PlaylistManager() {
SPIMaster::enable_sd();
current_rfid_tag_id = String("");
scan_files();
}
if (!SD.exists("/_mapping.txt")) {
Serial.println("WARNING: /_mapping.txt not found!");
void PlaylistManager::scan_files() {
SPIMaster::select_sd();
std::vector<String> folders;
File root = SD.open("/");
File entry;
while (entry = root.openNextFile()) {
String foldername = entry.name();
if (foldername.startsWith("/.")) continue;
foldername.remove(foldername.length());
folders.push_back(foldername);
_check_for_special_chars(foldername);
entry.close();
}
_map.clear();
if (!SD.exists("/_mapping.txt\n")) {
ERROR("WARNING: File /_mapping.txt not found.\n");
} else {
map.clear();
File f = SD.open("/_mapping.txt");
Serial.println(" Reading /_mapping.txt...");
DEBUG("Reading /_mapping.txt...\n");
while (f.available()) {
char buffer[512];
size_t pos = f.readBytesUntil('\n', buffer, 511);
@ -22,64 +37,203 @@ PlaylistManager::PlaylistManager() {
if (eq>0 && eq<data.length()-1) {
String rfid_id = data.substring(0, eq);
String folder = data.substring(eq + 1);
Serial.printf(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
map[rfid_id] = PersistedPlaylist(folder);
TRACE(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
_map[rfid_id] = folder;
if (folder.charAt(0)=='/') {
bool found=false;
for (String f: folders) {
if (f.equals(folder)) {
found = true;
break;
}
}
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());
}
}
}
}
f.close();
}
}
root.close();
Playlist PlaylistManager::get_playlist(String rfid_id) {
if (rfid_id.equals(current_rfid_tag_id)) {
return current_playlist;
} else {
if (map.count(rfid_id)==0) {
Serial.printf("No known playlist for id %s.\n", rfid_id);
return current_playlist;
} else {
PersistedPlaylist* ap = &(map[rfid_id]);
log_d("PP status is: File %d, bytes %d.", ap->file, ap->position);
current_playlist = Playlist(rfid_id, ap);
String path = ap->dir;
if (path.startsWith("/")) {
File dir = SD.open(path);
while(File entry = dir.openNextFile()) {
_unmapped_folders.clear();
for (String folder: folders) {
bool found = false;
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) {
if (it->second.equals(folder)) {
found = true;
break;
}
}
if (!found) {
// Checking folder for media files
File dir = SD.open(folder);
while (entry = dir.openNextFile()) {
String filename = entry.name();
String ext = filename.substring(filename.length()-4);
if (!entry.isDirectory() &&
!filename.startsWith(".") &&
ext.equals(".mp3")) {
Serial.printf("Adding %s to the list of files\n", (path + "/" + filename).c_str());
current_playlist.add_file(path + "/" + filename);
filename = filename.substring(folder.length() + 1);
if (!filename.startsWith(".") && filename.endsWith(".mp3")) {
found = true;
}
entry.close();
if (found) break;
}
dir.close();
current_playlist.set_current_position(ap->file, ap->position);
} else if (path.startsWith("http")) {
Serial.printf("Adding URL %s to the list of files\n", path.c_str());
current_playlist.add_file(path);
if (found) {
_unmapped_folders.push_back(folder);
}
current_playlist.sort();
current_rfid_tag_id = rfid_id;
return current_playlist;
}
}
SPIMaster::select_sd(false);
}
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;
}
}
}
void PlaylistManager::set_audio_current_time(uint32_t time) {
audio_current_time = time;
Playlist* PlaylistManager::get_playlist_for_id(String id) {
if (!_map.count(id)) return NULL;
String folder = _map[id];
return get_playlist_for_folder(folder);
}
bool PlaylistManager::has_playlist(String rfid_id) {
return map.count(rfid_id) == 1;
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;
}
String PlaylistManager::pp_to_String() {
String s = "";
for(const auto& kv : map) {
s += kv.first + "=" + kv.second.file + "," + kv.second.position + '\n';
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());
}
}

View File

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

96
src/updater.cpp Normal file
View File

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

359
src/webinterface/index.html Normal file
View File

@ -0,0 +1,359 @@
<!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

@ -0,0 +1,462 @@
{"timezones": {
"Africa/Abidjan":"GMT0",
"Africa/Accra":"GMT0",
"Africa/Addis_Ababa":"EAT-3",
"Africa/Algiers":"CET-1",
"Africa/Asmara":"EAT-3",
"Africa/Bamako":"GMT0",
"Africa/Bangui":"WAT-1",
"Africa/Banjul":"GMT0",
"Africa/Bissau":"GMT0",
"Africa/Blantyre":"CAT-2",
"Africa/Brazzaville":"WAT-1",
"Africa/Bujumbura":"CAT-2",
"Africa/Cairo":"EET-2",
"Africa/Casablanca":"<+01>-1",
"Africa/Ceuta":"CET-1CEST,M3.5.0,M10.5.0/3",
"Africa/Conakry":"GMT0",
"Africa/Dakar":"GMT0",
"Africa/Dar_es_Salaam":"EAT-3",
"Africa/Djibouti":"EAT-3",
"Africa/Douala":"WAT-1",
"Africa/El_Aaiun":"<+01>-1",
"Africa/Freetown":"GMT0",
"Africa/Gaborone":"CAT-2",
"Africa/Harare":"CAT-2",
"Africa/Johannesburg":"SAST-2",
"Africa/Juba":"EAT-3",
"Africa/Kampala":"EAT-3",
"Africa/Khartoum":"CAT-2",
"Africa/Kigali":"CAT-2",
"Africa/Kinshasa":"WAT-1",
"Africa/Lagos":"WAT-1",
"Africa/Libreville":"WAT-1",
"Africa/Lome":"GMT0",
"Africa/Luanda":"WAT-1",
"Africa/Lubumbashi":"CAT-2",
"Africa/Lusaka":"CAT-2",
"Africa/Malabo":"WAT-1",
"Africa/Maputo":"CAT-2",
"Africa/Maseru":"SAST-2",
"Africa/Mbabane":"SAST-2",
"Africa/Mogadishu":"EAT-3",
"Africa/Monrovia":"GMT0",
"Africa/Nairobi":"EAT-3",
"Africa/Ndjamena":"WAT-1",
"Africa/Niamey":"WAT-1",
"Africa/Nouakchott":"GMT0",
"Africa/Ouagadougou":"GMT0",
"Africa/Porto-Novo":"WAT-1",
"Africa/Sao_Tome":"GMT0",
"Africa/Tripoli":"EET-2",
"Africa/Tunis":"CET-1",
"Africa/Windhoek":"CAT-2",
"America/Adak":"HST10HDT,M3.2.0,M11.1.0",
"America/Anchorage":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Anguilla":"AST4",
"America/Antigua":"AST4",
"America/Araguaina":"<-03>3",
"America/Argentina/Buenos_Aires":"<-03>3",
"America/Argentina/Catamarca":"<-03>3",
"America/Argentina/Cordoba":"<-03>3",
"America/Argentina/Jujuy":"<-03>3",
"America/Argentina/La_Rioja":"<-03>3",
"America/Argentina/Mendoza":"<-03>3",
"America/Argentina/Rio_Gallegos":"<-03>3",
"America/Argentina/Salta":"<-03>3",
"America/Argentina/San_Juan":"<-03>3",
"America/Argentina/San_Luis":"<-03>3",
"America/Argentina/Tucuman":"<-03>3",
"America/Argentina/Ushuaia":"<-03>3",
"America/Aruba":"AST4",
"America/Asuncion":"<-04>4<-03>,M10.1.0/0,M3.4.0/0",
"America/Atikokan":"EST5",
"America/Bahia":"<-03>3",
"America/Bahia_Banderas":"CST6CDT,M4.1.0,M10.5.0",
"America/Barbados":"AST4",
"America/Belem":"<-03>3",
"America/Belize":"CST6",
"America/Blanc-Sablon":"AST4",
"America/Boa_Vista":"<-04>4",
"America/Bogota":"<-05>5",
"America/Boise":"MST7MDT,M3.2.0,M11.1.0",
"America/Cambridge_Bay":"MST7MDT,M3.2.0,M11.1.0",
"America/Campo_Grande":"<-04>4",
"America/Cancun":"EST5",
"America/Caracas":"<-04>4",
"America/Cayenne":"<-03>3",
"America/Cayman":"EST5",
"America/Chicago":"CST6CDT,M3.2.0,M11.1.0",
"America/Chihuahua":"MST7MDT,M4.1.0,M10.5.0",
"America/Costa_Rica":"CST6",
"America/Creston":"MST7",
"America/Cuiaba":"<-04>4",
"America/Curacao":"AST4",
"America/Danmarkshavn":"GMT0",
"America/Dawson":"PST8PDT,M3.2.0,M11.1.0",
"America/Dawson_Creek":"MST7",
"America/Denver":"MST7MDT,M3.2.0,M11.1.0",
"America/Detroit":"EST5EDT,M3.2.0,M11.1.0",
"America/Dominica":"AST4",
"America/Edmonton":"MST7MDT,M3.2.0,M11.1.0",
"America/Eirunepe":"<-05>5",
"America/El_Salvador":"CST6",
"America/Fortaleza":"<-03>3",
"America/Fort_Nelson":"MST7",
"America/Glace_Bay":"AST4ADT,M3.2.0,M11.1.0",
"America/Godthab":"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1",
"America/Goose_Bay":"AST4ADT,M3.2.0,M11.1.0",
"America/Grand_Turk":"EST5EDT,M3.2.0,M11.1.0",
"America/Grenada":"AST4",
"America/Guadeloupe":"AST4",
"America/Guatemala":"CST6",
"America/Guayaquil":"<-05>5",
"America/Guyana":"<-04>4",
"America/Halifax":"AST4ADT,M3.2.0,M11.1.0",
"America/Havana":"CST5CDT,M3.2.0/0,M11.1.0/1",
"America/Hermosillo":"MST7",
"America/Indiana/Indianapolis":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Knox":"CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Marengo":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Petersburg":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Tell_City":"CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Vevay":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Vincennes":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Winamac":"EST5EDT,M3.2.0,M11.1.0",
"America/Inuvik":"MST7MDT,M3.2.0,M11.1.0",
"America/Iqaluit":"EST5EDT,M3.2.0,M11.1.0",
"America/Jamaica":"EST5",
"America/Juneau":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Kentucky/Louisville":"EST5EDT,M3.2.0,M11.1.0",
"America/Kentucky/Monticello":"EST5EDT,M3.2.0,M11.1.0",
"America/Kralendijk":"AST4",
"America/La_Paz":"<-04>4",
"America/Lima":"<-05>5",
"America/Los_Angeles":"PST8PDT,M3.2.0,M11.1.0",
"America/Lower_Princes":"AST4",
"America/Maceio":"<-03>3",
"America/Managua":"CST6",
"America/Manaus":"<-04>4",
"America/Marigot":"AST4",
"America/Martinique":"AST4",
"America/Matamoros":"CST6CDT,M3.2.0,M11.1.0",
"America/Mazatlan":"MST7MDT,M4.1.0,M10.5.0",
"America/Menominee":"CST6CDT,M3.2.0,M11.1.0",
"America/Merida":"CST6CDT,M4.1.0,M10.5.0",
"America/Metlakatla":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Mexico_City":"CST6CDT,M4.1.0,M10.5.0",
"America/Miquelon":"<-03>3<-02>,M3.2.0,M11.1.0",
"America/Moncton":"AST4ADT,M3.2.0,M11.1.0",
"America/Monterrey":"CST6CDT,M4.1.0,M10.5.0",
"America/Montevideo":"<-03>3",
"America/Montreal":"EST5EDT,M3.2.0,M11.1.0",
"America/Montserrat":"AST4",
"America/Nassau":"EST5EDT,M3.2.0,M11.1.0",
"America/New_York":"EST5EDT,M3.2.0,M11.1.0",
"America/Nipigon":"EST5EDT,M3.2.0,M11.1.0",
"America/Nome":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Noronha":"<-02>2",
"America/North_Dakota/Beulah":"CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/Center":"CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/New_Salem":"CST6CDT,M3.2.0,M11.1.0",
"America/Ojinaga":"MST7MDT,M3.2.0,M11.1.0",
"America/Panama":"EST5",
"America/Pangnirtung":"EST5EDT,M3.2.0,M11.1.0",
"America/Paramaribo":"<-03>3",
"America/Phoenix":"MST7",
"America/Port-au-Prince":"EST5EDT,M3.2.0,M11.1.0",
"America/Port_of_Spain":"AST4",
"America/Porto_Velho":"<-04>4",
"America/Puerto_Rico":"AST4",
"America/Punta_Arenas":"<-03>3",
"America/Rainy_River":"CST6CDT,M3.2.0,M11.1.0",
"America/Rankin_Inlet":"CST6CDT,M3.2.0,M11.1.0",
"America/Recife":"<-03>3",
"America/Regina":"CST6",
"America/Resolute":"CST6CDT,M3.2.0,M11.1.0",
"America/Rio_Branco":"<-05>5",
"America/Santarem":"<-03>3",
"America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24",
"America/Santo_Domingo":"AST4",
"America/Sao_Paulo":"<-03>3",
"America/Scoresbysund":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
"America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0",
"America/St_Barthelemy":"AST4",
"America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0",
"America/St_Kitts":"AST4",
"America/St_Lucia":"AST4",
"America/St_Thomas":"AST4",
"America/St_Vincent":"AST4",
"America/Swift_Current":"CST6",
"America/Tegucigalpa":"CST6",
"America/Thule":"AST4ADT,M3.2.0,M11.1.0",
"America/Thunder_Bay":"EST5EDT,M3.2.0,M11.1.0",
"America/Tijuana":"PST8PDT,M3.2.0,M11.1.0",
"America/Toronto":"EST5EDT,M3.2.0,M11.1.0",
"America/Tortola":"AST4",
"America/Vancouver":"PST8PDT,M3.2.0,M11.1.0",
"America/Whitehorse":"PST8PDT,M3.2.0,M11.1.0",
"America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0",
"America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0",
"Antarctica/Casey":"<+08>-8",
"Antarctica/Davis":"<+07>-7",
"Antarctica/DumontDUrville":"<+10>-10",
"Antarctica/Macquarie":"<+11>-11",
"Antarctica/Mawson":"<+05>-5",
"Antarctica/McMurdo":"NZST-12NZDT,M9.5.0,M4.1.0/3",
"Antarctica/Palmer":"<-03>3",
"Antarctica/Rothera":"<-03>3",
"Antarctica/Syowa":"<+03>-3",
"Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
"Antarctica/Vostok":"<+06>-6",
"Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3",
"Asia/Aden":"<+03>-3",
"Asia/Almaty":"<+06>-6",
"Asia/Amman":"EET-2EEST,M3.5.4/24,M10.5.5/1",
"Asia/Anadyr":"<+12>-12",
"Asia/Aqtau":"<+05>-5",
"Asia/Aqtobe":"<+05>-5",
"Asia/Ashgabat":"<+05>-5",
"Asia/Atyrau":"<+05>-5",
"Asia/Baghdad":"<+03>-3",
"Asia/Bahrain":"<+03>-3",
"Asia/Baku":"<+04>-4",
"Asia/Bangkok":"<+07>-7",
"Asia/Barnaul":"<+07>-7",
"Asia/Beirut":"EET-2EEST,M3.5.0/0,M10.5.0/0",
"Asia/Bishkek":"<+06>-6",
"Asia/Brunei":"<+08>-8",
"Asia/Chita":"<+09>-9",
"Asia/Choibalsan":"<+08>-8",
"Asia/Colombo":"<+0530>-5:30",
"Asia/Damascus":"EET-2EEST,M3.5.5/0,M10.5.5/0",
"Asia/Dhaka":"<+06>-6",
"Asia/Dili":"<+09>-9",
"Asia/Dubai":"<+04>-4",
"Asia/Dushanbe":"<+05>-5",
"Asia/Famagusta":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Gaza":"EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Hebron":"EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Ho_Chi_Minh":"<+07>-7",
"Asia/Hong_Kong":"HKT-8",
"Asia/Hovd":"<+07>-7",
"Asia/Irkutsk":"<+08>-8",
"Asia/Jakarta":"WIB-7",
"Asia/Jayapura":"WIT-9",
"Asia/Jerusalem":"IST-2IDT,M3.4.4/26,M10.5.0",
"Asia/Kabul":"<+0430>-4:30",
"Asia/Kamchatka":"<+12>-12",
"Asia/Karachi":"PKT-5",
"Asia/Kathmandu":"<+0545>-5:45",
"Asia/Khandyga":"<+09>-9",
"Asia/Kolkata":"IST-5:30",
"Asia/Krasnoyarsk":"<+07>-7",
"Asia/Kuala_Lumpur":"<+08>-8",
"Asia/Kuching":"<+08>-8",
"Asia/Kuwait":"<+03>-3",
"Asia/Macau":"CST-8",
"Asia/Magadan":"<+11>-11",
"Asia/Makassar":"WITA-8",
"Asia/Manila":"PST-8",
"Asia/Muscat":"<+04>-4",
"Asia/Nicosia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Novokuznetsk":"<+07>-7",
"Asia/Novosibirsk":"<+07>-7",
"Asia/Omsk":"<+06>-6",
"Asia/Oral":"<+05>-5",
"Asia/Phnom_Penh":"<+07>-7",
"Asia/Pontianak":"WIB-7",
"Asia/Pyongyang":"KST-9",
"Asia/Qatar":"<+03>-3",
"Asia/Qyzylorda":"<+05>-5",
"Asia/Riyadh":"<+03>-3",
"Asia/Sakhalin":"<+11>-11",
"Asia/Samarkand":"<+05>-5",
"Asia/Seoul":"KST-9",
"Asia/Shanghai":"CST-8",
"Asia/Singapore":"<+08>-8",
"Asia/Srednekolymsk":"<+11>-11",
"Asia/Taipei":"CST-8",
"Asia/Tashkent":"<+05>-5",
"Asia/Tbilisi":"<+04>-4",
"Asia/Tehran":"<+0330>-3:30<+0430>,J79/24,J263/24",
"Asia/Thimphu":"<+06>-6",
"Asia/Tokyo":"JST-9",
"Asia/Tomsk":"<+07>-7",
"Asia/Ulaanbaatar":"<+08>-8",
"Asia/Urumqi":"<+06>-6",
"Asia/Ust-Nera":"<+10>-10",
"Asia/Vientiane":"<+07>-7",
"Asia/Vladivostok":"<+10>-10",
"Asia/Yakutsk":"<+09>-9",
"Asia/Yangon":"<+0630>-6:30",
"Asia/Yekaterinburg":"<+05>-5",
"Asia/Yerevan":"<+04>-4",
"Atlantic/Azores":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
"Atlantic/Bermuda":"AST4ADT,M3.2.0,M11.1.0",
"Atlantic/Canary":"WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Cape_Verde":"<-01>1",
"Atlantic/Faroe":"WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Madeira":"WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Reykjavik":"GMT0",
"Atlantic/South_Georgia":"<-02>2",
"Atlantic/Stanley":"<-03>3",
"Atlantic/St_Helena":"GMT0",
"Australia/Adelaide":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Brisbane":"AEST-10",
"Australia/Broken_Hill":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Currie":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Darwin":"ACST-9:30",
"Australia/Eucla":"<+0845>-8:45",
"Australia/Hobart":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Lindeman":"AEST-10",
"Australia/Lord_Howe":"<+1030>-10:30<+11>-11,M10.1.0,M4.1.0",
"Australia/Melbourne":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Perth":"AWST-8",
"Australia/Sydney":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Europe/Amsterdam":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Andorra":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Astrakhan":"<+04>-4",
"Europe/Athens":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Belgrade":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Berlin":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bratislava":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Brussels":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bucharest":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Budapest":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Busingen":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Chisinau":"EET-2EEST,M3.5.0,M10.5.0/3",
"Europe/Copenhagen":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Dublin":"IST-1GMT0,M10.5.0,M3.5.0/1",
"Europe/Gibraltar":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Guernsey":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Helsinki":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Isle_of_Man":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Istanbul":"<+03>-3",
"Europe/Jersey":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Kaliningrad":"EET-2",
"Europe/Kiev":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Kirov":"<+03>-3",
"Europe/Lisbon":"WET0WEST,M3.5.0/1,M10.5.0",
"Europe/Ljubljana":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/London":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Luxembourg":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Madrid":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Malta":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Mariehamn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Minsk":"<+03>-3",
"Europe/Monaco":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Moscow":"MSK-3",
"Europe/Oslo":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Paris":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Podgorica":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Prague":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Riga":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Rome":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Samara":"<+04>-4",
"Europe/San_Marino":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sarajevo":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Saratov":"<+04>-4",
"Europe/Simferopol":"MSK-3",
"Europe/Skopje":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sofia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Stockholm":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Tallinn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Tirane":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Ulyanovsk":"<+04>-4",
"Europe/Uzhgorod":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Vaduz":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vatican":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vienna":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vilnius":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Volgograd":"<+04>-4",
"Europe/Warsaw":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zagreb":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zaporozhye":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Zurich":"CET-1CEST,M3.5.0,M10.5.0/3",
"Indian/Antananarivo":"EAT-3",
"Indian/Chagos":"<+06>-6",
"Indian/Christmas":"<+07>-7",
"Indian/Cocos":"<+0630>-6:30",
"Indian/Comoro":"EAT-3",
"Indian/Kerguelen":"<+05>-5",
"Indian/Mahe":"<+04>-4",
"Indian/Maldives":"<+05>-5",
"Indian/Mauritius":"<+04>-4",
"Indian/Mayotte":"EAT-3",
"Indian/Reunion":"<+04>-4",
"Pacific/Apia":"<+13>-13<+14>,M9.5.0/3,M4.1.0/4",
"Pacific/Auckland":"NZST-12NZDT,M9.5.0,M4.1.0/3",
"Pacific/Bougainville":"<+11>-11",
"Pacific/Chatham":"<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45",
"Pacific/Chuuk":"<+10>-10",
"Pacific/Easter":"<-06>6<-05>,M9.1.6/22,M4.1.6/22",
"Pacific/Efate":"<+11>-11",
"Pacific/Enderbury":"<+13>-13",
"Pacific/Fakaofo":"<+13>-13",
"Pacific/Fiji":"<+12>-12<+13>,M11.2.0,M1.2.3/99",
"Pacific/Funafuti":"<+12>-12",
"Pacific/Galapagos":"<-06>6",
"Pacific/Gambier":"<-09>9",
"Pacific/Guadalcanal":"<+11>-11",
"Pacific/Guam":"ChST-10",
"Pacific/Honolulu":"HST10",
"Pacific/Kiritimati":"<+14>-14",
"Pacific/Kosrae":"<+11>-11",
"Pacific/Kwajalein":"<+12>-12",
"Pacific/Majuro":"<+12>-12",
"Pacific/Marquesas":"<-0930>9:30",
"Pacific/Midway":"SST11",
"Pacific/Nauru":"<+12>-12",
"Pacific/Niue":"<-11>11",
"Pacific/Norfolk":"<+11>-11<+12>,M10.1.0,M4.1.0/3",
"Pacific/Noumea":"<+11>-11",
"Pacific/Pago_Pago":"SST11",
"Pacific/Palau":"<+09>-9",
"Pacific/Pitcairn":"<-08>8",
"Pacific/Pohnpei":"<+11>-11",
"Pacific/Port_Moresby":"<+10>-10",
"Pacific/Rarotonga":"<-10>10",
"Pacific/Saipan":"ChST-10",
"Pacific/Tahiti":"<-10>10",
"Pacific/Tarawa":"<+12>-12",
"Pacific/Tongatapu":"<+13>-13",
"Pacific/Wake":"<+12>-12",
"Pacific/Wallis":"<+12>-12",
"Etc/GMT":"GMT0",
"Etc/GMT-0":"GMT0",
"Etc/GMT-1":"<+01>-1",
"Etc/GMT-2":"<+02>-2",
"Etc/GMT-3":"<+03>-3",
"Etc/GMT-4":"<+04>-4",
"Etc/GMT-5":"<+05>-5",
"Etc/GMT-6":"<+06>-6",
"Etc/GMT-7":"<+07>-7",
"Etc/GMT-8":"<+08>-8",
"Etc/GMT-9":"<+09>-9",
"Etc/GMT-10":"<+10>-10",
"Etc/GMT-11":"<+11>-11",
"Etc/GMT-12":"<+12>-12",
"Etc/GMT-13":"<+13>-13",
"Etc/GMT-14":"<+14>-14",
"Etc/GMT0":"GMT0",
"Etc/GMT+0":"GMT0",
"Etc/GMT+1":"<-01>1",
"Etc/GMT+2":"<-02>2",
"Etc/GMT+3":"<-03>3",
"Etc/GMT+4":"<-04>4",
"Etc/GMT+5":"<-05>5",
"Etc/GMT+6":"<-06>6",
"Etc/GMT+7":"<-07>7",
"Etc/GMT+8":"<-08>8",
"Etc/GMT+9":"<-09>9",
"Etc/GMT+10":"<-10>10",
"Etc/GMT+11":"<-11>11",
"Etc/GMT+12":"<-12>12",
"Etc/UCT":"UTC0",
"Etc/UTC":"UTC0",
"Etc/Greenwich":"GMT0",
"Etc/Universal":"UTC0",
"Etc/Zulu":"UTC0"
}}

14
tools/create_tz_json.sh Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/bash
URL="https://raw.githubusercontent.com/nayarsystems/posix_tz_db/master/zones.csv"
(
first=1
echo "{\"timezones\": {"
curl --silent "$URL" | while read line; do
[ $first -ne 1 ] && echo ","
first=0
echo -n "${line/\",\"/\":\"}"
done
echo
echo "}}"
) > src/webinterface/timezones.json

9
tools/post_build.py Normal file
View File

@ -0,0 +1,9 @@
Import("env")
env.Execute("gzip -9 < src/webinterface/index.html > src/webinterface/index.html.gz")
env.Execute("gzip -9 < src/webinterface/timezones.json > src/webinterface/timezones.json.gz")
def build(source, target, env):
env.Execute("rm src/webinterface/index.html.gz src/webinterface/timezones.json.gz")
env.AddPostAction("buildprog", build)