Compare commits
22 Commits
fa208858d9
...
ab51af637e
Author | SHA1 | Date | |
---|---|---|---|
ab51af637e | |||
913a64d465 | |||
b2cf9d6277 | |||
076a6993c7 | |||
13e62fea19 | |||
15a65f7391 | |||
b9df55012f | |||
fb6b5bced6 | |||
3718f45983 | |||
7dcb0cb673 | |||
6989248970 | |||
cf433a48b2 | |||
9a39b00a65 | |||
aed9c416bf | |||
2908d23e60 | |||
3272921db2 | |||
6ddf1efd62 | |||
45dfe0cfe0 | |||
4840c150c2 | |||
9c31f70c57 | |||
978b25c34d | |||
dcca828197 |
178
README.md
178
README.md
@ -1,174 +1,8 @@
|
|||||||
# ESMP3
|
# ESMP3
|
||||||
|
|
||||||
## What you need
|
## Audio files
|
||||||
Please note: This list is a "things I used", neither
|
System messages are created using:
|
||||||
"these are the best things for this stuff" nor "you
|
* https://ttsmp3.com/
|
||||||
can only use these things". But please be aware that
|
* German / Vicki
|
||||||
using other stuff may lead to you having to make
|
* "Dies ist ein Text.<break time="1s"/>"
|
||||||
more or less easy modifications.
|
* Download as MP3.
|
||||||
|
|
||||||
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. |
|
|
@ -1,84 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
// This is a simple number indicating the version for the HTTP Updater.
|
|
||||||
#define OTA_VERSION 1
|
|
||||||
// Comment out to prevent automatic updates.
|
|
||||||
#define OTA_UPDATE_URL "https://files.schle.nz/esmp3/update.manifest"
|
|
||||||
#define OTA_CHECK_INTERVAL 12*60*60*1000 // 12 hours
|
|
||||||
|
|
||||||
#define SHOW_DEBUG
|
|
||||||
//#define SHOW_TRACE
|
|
||||||
#define FTP_DEBUG
|
|
||||||
#define DELAY_AFTER_DEBUG_AND_TRACE 0
|
|
||||||
|
|
||||||
// Here you can define WiFi data to use. But actually, the better way to do
|
|
||||||
// this is by using /_wifi.txt on the sd card.
|
|
||||||
//#define WIFI_SSID "---CHANGEME---"
|
|
||||||
//#define WIFI_PASS "---CHANGEME---"
|
|
||||||
|
|
||||||
#define VS1053_SLEEP_DELAY 5000
|
|
||||||
#define POSITION_SEND_INTERVAL 5000
|
|
||||||
|
|
||||||
#define DEBOUNCE_MILLIS 200
|
|
||||||
#define VOLUME_DEFAULT 230
|
|
||||||
#define VOLUME_MIN 190
|
|
||||||
#define VOLUME_MAX 255
|
|
||||||
#define VOLUME_STEP 0x08
|
|
||||||
|
|
||||||
#define RFID_SCAN_INTERVAL 100
|
|
||||||
|
|
||||||
#define NUM_BUTTONS 4
|
|
||||||
|
|
||||||
#define PIN_SD_CS(x) (digitalWrite(16, x))
|
|
||||||
#define PIN_SD_CS_SETUP() (pinMode(16, OUTPUT))
|
|
||||||
|
|
||||||
#define PIN_VS1053_XCS(x) (digitalWrite(4, x))
|
|
||||||
#define PIN_VS1053_XCS_SETUP() (pinMode(4, OUTPUT))
|
|
||||||
|
|
||||||
#define PIN_VS1053_XRESET(x) (digitalWrite(0, x))
|
|
||||||
#define PIN_VS1053_XRESET_SETUP() (pinMode(0, OUTPUT))
|
|
||||||
|
|
||||||
#define PIN_VS1053_XDCS(x) (digitalWrite(2, x))
|
|
||||||
#define PIN_VS1053_XDCS_SETUP() (pinMode(2, OUTPUT))
|
|
||||||
|
|
||||||
#define PIN_VS1053_DREQ() (digitalRead(15))
|
|
||||||
#define PIN_VS1053_DREQ_SETUP() (pinMode(15, INPUT))
|
|
||||||
|
|
||||||
#define PIN_RC522_CS(x) (digitalWrite(17, x))
|
|
||||||
#define PIN_RC522_CS_SETUP() (pinMode(17, OUTPUT))
|
|
||||||
|
|
||||||
#define PIN_SPEAKER_L(x) (digitalWrite(27, x))
|
|
||||||
#define PIN_SPEAKER_L_SETUP() (pinMode(27, OUTPUT))
|
|
||||||
|
|
||||||
#define PIN_SPEAKER_R(x) (digitalWrite(26, x))
|
|
||||||
#define PIN_SPEAKER_R_SETUP() (pinMode(26, OUTPUT))
|
|
||||||
|
|
||||||
#define BTN_PREV() ( ! digitalRead(22))
|
|
||||||
#define BTN_PREV_SETUP() (pinMode(22, INPUT_PULLUP))
|
|
||||||
|
|
||||||
#define BTN_VOL_UP() ( ! digitalRead(21))
|
|
||||||
#define BTN_VOL_UP_SETUP() (pinMode(21, INPUT_PULLUP))
|
|
||||||
|
|
||||||
#define BTN_VOL_DOWN() ( ! digitalRead(32))
|
|
||||||
#define BTN_VOL_DOWN_SETUP() (pinMode(32, INPUT_PULLUP))
|
|
||||||
|
|
||||||
#define BTN_NEXT() ( ! digitalRead(33))
|
|
||||||
#define BTN_NEXT_SETUP() (pinMode(33, INPUT_PULLUP))
|
|
||||||
|
|
||||||
|
|
||||||
// Other definitions
|
|
||||||
#define INFO(x, ...) Serial.printf(x, ##__VA_ARGS__)
|
|
||||||
#define ERROR(x, ...) Serial.printf(x, ##__VA_ARGS__)
|
|
||||||
|
|
||||||
#ifdef SHOW_DEBUG
|
|
||||||
#define DEBUG(x, ...) {Serial.printf(x, ##__VA_ARGS__); delay(DELAY_AFTER_DEBUG_AND_TRACE);}
|
|
||||||
#else
|
|
||||||
#define DEBUG(x, ...) while(0) {}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef SHOW_TRACE
|
|
||||||
#define TRACE(x, ...) {Serial.printf(x, ##__VA_ARGS__); delay(DELAY_AFTER_DEBUG_AND_TRACE);}
|
|
||||||
#else
|
|
||||||
#define TRACE(x, ...) while(0) {}
|
|
||||||
#endif
|
|
@ -1,61 +1,32 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <ESPAsyncWebServer.h>
|
#include <MFRC522v2.h>
|
||||||
#include "config.h"
|
#include <MFRC522Debug.h>
|
||||||
|
#include "esmp3.h"
|
||||||
class Controller;
|
|
||||||
|
|
||||||
#include "player.h"
|
|
||||||
#include "playlist.h"
|
#include "playlist.h"
|
||||||
#include "playlist_manager.h"
|
|
||||||
#include "http_server.h"
|
|
||||||
|
|
||||||
#undef DEPRECATED
|
|
||||||
#include <MFRC522.h>
|
|
||||||
|
|
||||||
enum ControllerState { NORMAL, LOCKING, LOCKED };
|
|
||||||
|
|
||||||
class Controller {
|
class Controller {
|
||||||
private:
|
private:
|
||||||
MFRC522* _rfid;
|
void handle_buttons();
|
||||||
HTTPServer* _http_server;
|
void handle_rfid();
|
||||||
ControllerState _state = NORMAL;
|
bool is_button_pressed(uint8_t pin);
|
||||||
bool _rfid_enabled = true;
|
Playlist current_playlist;
|
||||||
void _check_rfid();
|
bool is_rfid_present = false;
|
||||||
void _check_serial();
|
unsigned long last_rfid_check = 0;
|
||||||
void _check_buttons();
|
unsigned long last_button_check = 0;
|
||||||
bool _debounce_button(uint8_t index);
|
unsigned long last_position_save = 0;
|
||||||
uint32_t _get_rfid_card_uid();
|
uint8_t button_pressed = 0;
|
||||||
String _read_rfid_data();
|
unsigned long button_pressed_since = 0;
|
||||||
bool _rfid_present = false;
|
bool button_already_processed = false;
|
||||||
String _last_rfid_uid = "";
|
String read_rfid_data();
|
||||||
String _last_rfid_data = "";
|
|
||||||
|
|
||||||
unsigned long _last_rfid_scan_at = 0;
|
public:
|
||||||
unsigned long _last_position_info_at = 0;
|
void handle();
|
||||||
unsigned long _last_update_check_at = 0;
|
void next_track();
|
||||||
unsigned long _last_wifi_try_at = 0;
|
void prev_track();
|
||||||
String _serial_buffer = String();
|
void play();
|
||||||
String _cmd_queue = "";
|
void play(String rfid_id, bool shuffle=false);
|
||||||
void _execute_command_ls(String path);
|
void stop();
|
||||||
void _execute_command_ids();
|
void eof_mp3(String info);
|
||||||
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();
|
|
||||||
};
|
};
|
@ -1,54 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <SD.h>
|
|
||||||
#include "config.h"
|
|
||||||
#include "http_client_wrapper.h"
|
|
||||||
|
|
||||||
class DataSource {
|
|
||||||
private:
|
|
||||||
public:
|
|
||||||
DataSource() {};
|
|
||||||
virtual ~DataSource() {};
|
|
||||||
virtual size_t read(uint8_t* buf, size_t len) = 0;
|
|
||||||
virtual int read() = 0;
|
|
||||||
virtual size_t position() = 0;
|
|
||||||
virtual void seek(size_t position) = 0;
|
|
||||||
virtual size_t size() = 0;
|
|
||||||
virtual void close() = 0;
|
|
||||||
virtual void skip_id3_tag() {};
|
|
||||||
virtual bool usable() = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
class SDDataSource : public DataSource {
|
|
||||||
private:
|
|
||||||
File _file;
|
|
||||||
public:
|
|
||||||
SDDataSource(String file);
|
|
||||||
~SDDataSource();
|
|
||||||
size_t read(uint8_t* buf, size_t len);
|
|
||||||
int read();
|
|
||||||
size_t position();
|
|
||||||
void seek(size_t position);
|
|
||||||
size_t size();
|
|
||||||
void close();
|
|
||||||
void skip_id3_tag();
|
|
||||||
bool usable();
|
|
||||||
};
|
|
||||||
|
|
||||||
class HTTPSDataSource : public DataSource {
|
|
||||||
private:
|
|
||||||
WiFiClient* _stream = NULL;
|
|
||||||
HTTPClientWrapper* _http = NULL;
|
|
||||||
uint32_t _position;
|
|
||||||
public:
|
|
||||||
HTTPSDataSource(String url, uint32_t offset=0);
|
|
||||||
~HTTPSDataSource();
|
|
||||||
size_t read(uint8_t* buf, size_t len);
|
|
||||||
int read();
|
|
||||||
size_t position();
|
|
||||||
void seek(size_t position);
|
|
||||||
size_t size();
|
|
||||||
void close();
|
|
||||||
bool usable();
|
|
||||||
};
|
|
26
include/esmp3.h
Normal file
26
include/esmp3.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "controller.h"
|
||||||
|
#include "playlist_manager.h"
|
||||||
|
#include <Audio.h>
|
||||||
|
|
||||||
|
#define PIN_CS_SD 22
|
||||||
|
#define PIN_CS_RFID 21
|
||||||
|
|
||||||
|
#define PIN_BTN_VOL_UP 32
|
||||||
|
#define PIN_BTN_VOL_DOWN 33
|
||||||
|
#define PIN_BTN_TRACK_NEXT 17
|
||||||
|
#define PIN_BTN_TRACK_PREV 16
|
||||||
|
|
||||||
|
#define I2S_DOUT 25
|
||||||
|
#define I2S_BCLK 26
|
||||||
|
#define I2S_LRC 27
|
||||||
|
|
||||||
|
class Controller;
|
||||||
|
|
||||||
|
extern Controller controller;
|
||||||
|
extern Audio audio;
|
||||||
|
extern PlaylistManager* pm;
|
||||||
|
extern MFRC522* rfid;
|
||||||
|
|
||||||
|
void save_audio_current_time();
|
@ -1,37 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <HTTPClient.h>
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
class HTTPClientWrapper {
|
|
||||||
private:
|
|
||||||
HTTPClient* _http;
|
|
||||||
uint8_t* _buffer;
|
|
||||||
uint16_t _buffer_size;
|
|
||||||
uint16_t _buffer_length;
|
|
||||||
uint16_t _buffer_position;
|
|
||||||
uint32_t _chunk_length;
|
|
||||||
|
|
||||||
bool _connected = false;
|
|
||||||
String _content_type;
|
|
||||||
uint32_t _length;
|
|
||||||
bool _request(String method, String url, uint32_t offset=0, uint8_t redirection_count=0);
|
|
||||||
WiFiClient* _stream;
|
|
||||||
bool _is_chunked;
|
|
||||||
void _read_next_chunk_header(bool first);
|
|
||||||
uint16_t _fill_buffer();
|
|
||||||
|
|
||||||
public:
|
|
||||||
HTTPClientWrapper();
|
|
||||||
~HTTPClientWrapper();
|
|
||||||
bool get(String url, uint32_t offset=0, uint8_t redirection_count=0);
|
|
||||||
bool head(String url, uint32_t offset=0, uint8_t redirection_count=0);
|
|
||||||
String getContentType();
|
|
||||||
String getString();
|
|
||||||
int read();
|
|
||||||
uint32_t read(uint8_t* dst, uint32_t len);
|
|
||||||
void close();
|
|
||||||
uint32_t getSize();
|
|
||||||
String readUntil(String sep);
|
|
||||||
String readLine();
|
|
||||||
};
|
|
@ -1,29 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
class HTTPServer;
|
|
||||||
|
|
||||||
#include "player.h"
|
|
||||||
#include "controller.h"
|
|
||||||
#include <AsyncTCP.h>
|
|
||||||
#include <ESPAsyncWebServer.h>
|
|
||||||
|
|
||||||
class HTTPServer {
|
|
||||||
private:
|
|
||||||
AsyncWebServer* _server;
|
|
||||||
|
|
||||||
Player* _player;
|
|
||||||
Controller* _controller;
|
|
||||||
void _handle_upload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
|
|
||||||
uint16_t _chunk_length;
|
|
||||||
uint8_t* _chunk;
|
|
||||||
File _upload_file;
|
|
||||||
uint32_t _file_size;
|
|
||||||
uint32_t _file_size_done;
|
|
||||||
bool _need_header;
|
|
||||||
uint32_t _upload_position;
|
|
||||||
void _onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len);
|
|
||||||
void _handle_index(AsyncWebServerRequest* req);
|
|
||||||
public:
|
|
||||||
HTTPServer(Player* p, Controller* c);
|
|
||||||
AsyncWebSocket* ws;
|
|
||||||
};
|
|
@ -1,9 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <Preferences.h>
|
|
||||||
|
|
||||||
void wifi_connect();
|
|
||||||
|
|
||||||
extern const uint8_t file_index_html_start[] asm("_binary_src_index_html_start");
|
|
||||||
extern bool debug_enabled;
|
|
||||||
extern bool trace_enabled;
|
|
||||||
extern Preferences prefs;
|
|
9
include/persisted_playlist.h
Normal file
9
include/persisted_playlist.h
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
struct PersistedPlaylist {
|
||||||
|
String dir;
|
||||||
|
uint16_t file = 0;
|
||||||
|
uint32_t position = 0;
|
||||||
|
PersistedPlaylist(String s="") : dir(s) {}
|
||||||
|
};
|
104
include/player.h
104
include/player.h
@ -1,104 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include "config.h"
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <SD.h>
|
|
||||||
#include "spi_master.h"
|
|
||||||
#include "playlist.h"
|
|
||||||
#include "data_sources.h"
|
|
||||||
|
|
||||||
class Player;
|
|
||||||
|
|
||||||
#include "controller.h"
|
|
||||||
|
|
||||||
#define SCI_MODE 0x00
|
|
||||||
#define SCI_STATUS 0x01
|
|
||||||
#define SCI_BASS 0x02
|
|
||||||
#define SCI_CLOCKF 0x03
|
|
||||||
#define SCI_DECODE_TIME 0x04
|
|
||||||
#define SCI_AUDATA 0x05
|
|
||||||
#define SCI_VOL 0x0B
|
|
||||||
#define SCI_WRAMADDR 0x07
|
|
||||||
#define SCI_WRAM 0x06
|
|
||||||
#define SCI_HDAT0 0x08
|
|
||||||
#define SCI_HDAT1 0x09
|
|
||||||
#define SCI_AIADDR 0x0A
|
|
||||||
#define SCI_AICTRL0 0x0C
|
|
||||||
#define SCI_AICTRL1 0x0D
|
|
||||||
#define SCI_AICTRL2 0x0E
|
|
||||||
#define SCI_AICTRL3 0x0F
|
|
||||||
|
|
||||||
#define CMD_WRITE 0x02
|
|
||||||
#define CMD_READ 0x03
|
|
||||||
|
|
||||||
#define ADDR_ENDBYTE 0x1E06
|
|
||||||
|
|
||||||
#define SM_LAYER12 0x0001
|
|
||||||
#define SM_RESET 0x0004
|
|
||||||
#define SM_CANCEL 0x0008
|
|
||||||
#define SM_SDINEW 0x0800
|
|
||||||
#define SM_ADPCM 0x1000
|
|
||||||
#define SS_DO_NOT_JUMP 0x8000
|
|
||||||
|
|
||||||
class Player {
|
|
||||||
private:
|
|
||||||
enum state { uninitialized, idle, playing, stopping,
|
|
||||||
sleeping, recording };
|
|
||||||
void _reset();
|
|
||||||
void _wait();
|
|
||||||
uint16_t _read_control_register(uint8_t address, bool do_wait=true);
|
|
||||||
void _write_control_register(uint8_t address, uint16_t value, bool do_wait=true);
|
|
||||||
void _write_direct(uint8_t address, uint16_t value);
|
|
||||||
void _write_data(uint8_t* data);
|
|
||||||
uint16_t _read_wram(uint16_t address);
|
|
||||||
state _state = state::uninitialized;
|
|
||||||
void _refill();
|
|
||||||
bool _refill_needed();
|
|
||||||
void _flush_and_cancel();
|
|
||||||
int8_t _get_endbyte();
|
|
||||||
void _flush(uint count, int8_t fill_byte);
|
|
||||||
void _play_file(String filename, uint32_t offset);
|
|
||||||
void _finish_playing();
|
|
||||||
void _finish_stopping(bool turn_speaker_off);
|
|
||||||
void _mute();
|
|
||||||
void _unmute();
|
|
||||||
void _sleep();
|
|
||||||
void _wakeup();
|
|
||||||
void _record();
|
|
||||||
void _patch_adpcm();
|
|
||||||
void _speaker_off();
|
|
||||||
void _speaker_on();
|
|
||||||
|
|
||||||
SPISettings _spi_settings_slow = SPISettings(250000, MSBFIRST, SPI_MODE0);
|
|
||||||
SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
|
||||||
SPISettings* _spi_settings = &_spi_settings_slow;
|
|
||||||
|
|
||||||
DataSource* _file;
|
|
||||||
uint32_t _file_size = 0;
|
|
||||||
uint8_t _buffer[32];
|
|
||||||
uint32_t _current_play_position = 0;
|
|
||||||
Playlist* _current_playlist = NULL;
|
|
||||||
uint _refills;
|
|
||||||
uint8_t _volume;
|
|
||||||
uint16_t _stop_delay;
|
|
||||||
uint32_t _skip_to;
|
|
||||||
SPIMaster* _spi;
|
|
||||||
Controller* _controller;
|
|
||||||
unsigned long _stopped_at;
|
|
||||||
public:
|
|
||||||
Player(SPIMaster* s);
|
|
||||||
void init();
|
|
||||||
void register_controller(Controller* c);
|
|
||||||
void vol_up();
|
|
||||||
void vol_down();
|
|
||||||
void track_next();
|
|
||||||
void track_prev();
|
|
||||||
void set_track(uint8_t track);
|
|
||||||
bool is_playing();
|
|
||||||
bool play();
|
|
||||||
bool play(Playlist* p);
|
|
||||||
void stop(bool turn_speaker_off=true);
|
|
||||||
bool loop();
|
|
||||||
void set_volume(uint8_t vol, bool save = true);
|
|
||||||
String position_json();
|
|
||||||
String json();
|
|
||||||
};
|
|
@ -1,59 +1,30 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Arduino.h>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <ArduinoJson.h>
|
#include <Arduino.h>
|
||||||
#include "http_client_wrapper.h"
|
#include "persisted_playlist.h"
|
||||||
|
|
||||||
enum PlaylistPersistence {
|
|
||||||
PERSIST_NONE,
|
|
||||||
PERSIST_TEMPORARY,
|
|
||||||
PERSIST_PERMANENTLY
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PlaylistEntry {
|
|
||||||
String filename;
|
|
||||||
String title;
|
|
||||||
String id;
|
|
||||||
|
|
||||||
bool operator<(PlaylistEntry p) { return title < p.title; }
|
|
||||||
};
|
|
||||||
|
|
||||||
class Playlist {
|
class Playlist {
|
||||||
private:
|
private:
|
||||||
uint32_t _position = 0;
|
std::vector<String> files;
|
||||||
uint32_t _current_track = 0;
|
uint8_t current_file = 0;
|
||||||
bool _started = false;
|
uint32_t current_time = 0;
|
||||||
bool _shuffled = false;
|
String rfid_id;
|
||||||
std::vector<PlaylistEntry> _files;
|
PersistedPlaylist* pp;
|
||||||
String _title = "";
|
|
||||||
String _path;
|
public:
|
||||||
void _add_path(String path);
|
Playlist();
|
||||||
void _examine_http_url(String url);
|
Playlist(String rfid_id, PersistedPlaylist* p);
|
||||||
void _parse_rss(HTTPClientWrapper* http);
|
void add_file(String filename);
|
||||||
void _parse_m3u(HTTPClientWrapper* http);
|
void sort();
|
||||||
void _parse_pls(HTTPClientWrapper* http);
|
String get_rfid_id();
|
||||||
public:
|
String get_current_file_name();
|
||||||
PlaylistPersistence persistence = PERSIST_TEMPORARY;
|
bool next_track();
|
||||||
Playlist(String path);
|
bool prev_track();
|
||||||
void start();
|
void restart();
|
||||||
uint16_t get_file_count();
|
void set_current_time(uint32_t time);
|
||||||
bool has_track_next();
|
uint32_t get_current_time();
|
||||||
bool has_track_prev();
|
void shuffle();
|
||||||
bool track_next();
|
void set_current_position(uint8_t file, uint32_t position=0);
|
||||||
bool track_prev();
|
void save_current_position(uint32_t position=0);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <Arduino.h>
|
||||||
#include "playlist.h"
|
#include "playlist.h"
|
||||||
|
#include "persisted_playlist.h"
|
||||||
|
|
||||||
|
class Playlist;
|
||||||
|
|
||||||
class PlaylistManager {
|
class PlaylistManager {
|
||||||
private:
|
private:
|
||||||
std::map<String, String> _map;
|
Playlist get_playlist_for_tag_id(String id);
|
||||||
std::map<String, Playlist*> _playlists;
|
String current_rfid_tag_id;
|
||||||
std::vector<String> _unmapped_folders;
|
uint32_t audio_current_time = 0;
|
||||||
void _check_for_special_chars(String s);
|
|
||||||
void _save_mapping();
|
public:
|
||||||
public:
|
|
||||||
PlaylistManager();
|
PlaylistManager();
|
||||||
Playlist* get_playlist_for_id(String id);
|
std::map<String, PersistedPlaylist> map;
|
||||||
Playlist* get_playlist_for_folder(String folder);
|
Playlist get_playlist(String rfid_id);
|
||||||
void dump_ids();
|
bool has_playlist(String rfid_id);
|
||||||
void scan_files();
|
Playlist current_playlist;
|
||||||
String json();
|
void set_audio_current_time(uint32_t time);
|
||||||
bool add_mapping(String id, String folder);
|
String pp_to_String();
|
||||||
String create_mapping_txt();
|
|
||||||
void persist(Playlist* p);
|
|
||||||
};
|
};
|
@ -1,70 +1,9 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
#include <SPI.h>
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
class SPIMaster {
|
class SPIMaster {
|
||||||
public:
|
public:
|
||||||
static uint8_t state;
|
static void enable_sd();
|
||||||
|
static void enable_rfid();
|
||||||
static void init() {
|
static void disable_all();
|
||||||
PIN_SD_CS_SETUP();
|
static void initialize();
|
||||||
PIN_VS1053_XCS_SETUP();
|
|
||||||
PIN_VS1053_XDCS_SETUP();
|
|
||||||
PIN_RC522_CS_SETUP();
|
|
||||||
disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void select_sd(bool enabled=true) {
|
|
||||||
PIN_SD_CS(enabled ? LOW : HIGH);
|
|
||||||
if (enabled) {
|
|
||||||
state |= 1;
|
|
||||||
} else {
|
|
||||||
state &= ~1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void select_vs1053_xcs(bool enabled=true) {
|
|
||||||
PIN_VS1053_XCS(enabled ? LOW : HIGH);
|
|
||||||
if (enabled) {
|
|
||||||
state |= 2;
|
|
||||||
} else {
|
|
||||||
state &= ~2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void select_vs1053_xdcs(bool enabled=true) {
|
|
||||||
PIN_VS1053_XDCS(enabled ? LOW : HIGH);
|
|
||||||
if (enabled) {
|
|
||||||
state |= 4;
|
|
||||||
} else {
|
|
||||||
state &= ~4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void select_rc522(bool enabled=true) {
|
|
||||||
PIN_RC522_CS(enabled ? LOW : HIGH);
|
|
||||||
if (enabled) {
|
|
||||||
state |= 8;
|
|
||||||
} else {
|
|
||||||
state &= ~8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void set_state(uint8_t s) {
|
|
||||||
disable();
|
|
||||||
if (s & 1) select_sd();
|
|
||||||
if (s & 2) select_vs1053_xcs();
|
|
||||||
if (s & 4) select_vs1053_xdcs();
|
|
||||||
if (s & 8) select_rc522();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void disable() {
|
|
||||||
PIN_SD_CS(HIGH);
|
|
||||||
PIN_VS1053_XCS(HIGH);
|
|
||||||
PIN_VS1053_XDCS(HIGH);
|
|
||||||
PIN_RC522_CS(HIGH);
|
|
||||||
state = 0;
|
|
||||||
}
|
|
||||||
};
|
};
|
@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "http_client_wrapper.h"
|
|
||||||
|
|
||||||
class Updater {
|
|
||||||
public:
|
|
||||||
static void run();
|
|
||||||
static bool do_update(int cmd, String url, String expected_md5);
|
|
||||||
static bool read_line(String* dst, HTTPClientWrapper* http, String expected_key);
|
|
||||||
};
|
|
@ -13,28 +13,30 @@ default_envs = esp32
|
|||||||
|
|
||||||
[extra]
|
[extra]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
63 ; MFRC522
|
|
||||||
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
|
||||||
ArduinoJSON
|
|
||||||
6691 ; TinyXML
|
|
||||||
|
|
||||||
[env:esp32]
|
[env:esp32]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp-wrover-kit
|
board = esp-wrover-kit
|
||||||
framework = arduino
|
framework = arduino
|
||||||
upload_speed = 512000
|
upload_speed = 921600
|
||||||
build_flags=!./build_version.sh
|
build_flags = -DCORE_DEBUG_LEVEL=5 -DCONFIG_ARDUHAL_LOG_COLORS=1 ; !./build_version.sh
|
||||||
lib_deps = ${extra.lib_deps}
|
lib_deps =
|
||||||
upload_port = /dev/cu.SLAB_USBtoUART
|
${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_speed = 115200
|
||||||
board_build.embed_txtfiles = src/index.html
|
monitor_port = /dev/cu.usbserial-0001
|
||||||
;board_build.partitions = partitions.csv
|
monitor_filters = esp32_exception_decoder
|
||||||
;monitor_port = /dev/cu.wchusbserial1420
|
|
||||||
|
|
||||||
[env:deploy]
|
[env:deploy]
|
||||||
platform = espressif32
|
platform = espressif32
|
||||||
board = esp-wrover-kit
|
board = esp-wrover-kit
|
||||||
framework = arduino
|
framework = arduino
|
||||||
lib_deps = ${extra.lib_deps}
|
lib_deps =
|
||||||
|
${extra.lib_deps}
|
||||||
|
esphome/ESP32-audioI2S@^2.1.0
|
||||||
|
computer991/Arduino_MFRC522v2@^2.0.1
|
||||||
board_build.embed_txtfiles = src/index.html
|
board_build.embed_txtfiles = src/index.html
|
||||||
board_build.partitions = partitions.csv
|
board_build.partitions = partitions.csv
|
||||||
|
@ -1,463 +1,226 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "main.h"
|
#include "esmp3.h"
|
||||||
#include "spi_master.h"
|
|
||||||
#include "config.h"
|
|
||||||
#include "playlist.h"
|
|
||||||
#include "http_server.h"
|
|
||||||
#include "updater.h"
|
|
||||||
#include <ArduinoJson.h>
|
|
||||||
|
|
||||||
Controller::Controller(Player* p, PlaylistManager* playlist_manager) {
|
void Controller::handle() {
|
||||||
player = p;
|
if (last_rfid_check + 500 < millis() || last_rfid_check > millis()) {
|
||||||
pm = playlist_manager;
|
handle_rfid();
|
||||||
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
|
last_rfid_check = millis();
|
||||||
|
|
||||||
player->register_controller(this);
|
|
||||||
|
|
||||||
BTN_NEXT_SETUP();
|
|
||||||
BTN_PREV_SETUP();
|
|
||||||
BTN_VOL_UP_SETUP();
|
|
||||||
BTN_VOL_DOWN_SETUP();
|
|
||||||
|
|
||||||
SPIMaster::select_rc522();
|
|
||||||
DEBUG("Initializing RC522...\n");
|
|
||||||
_rfid->PCD_Init();
|
|
||||||
#ifdef SHOW_DEBUG
|
|
||||||
_rfid->PCD_DumpVersionToSerial();
|
|
||||||
#endif
|
|
||||||
SPIMaster::select_rc522(false);
|
|
||||||
INFO("RC522 initialized.\n");
|
|
||||||
|
|
||||||
for (uint8_t i=0; i<NUM_BUTTONS; i++) _button_last_pressed_at[i]=0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Controller::register_http_server(HTTPServer* h) {
|
|
||||||
_http_server = h;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Controller::loop() {
|
|
||||||
unsigned long now = millis();
|
|
||||||
if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) {
|
|
||||||
_check_rfid();
|
|
||||||
_last_rfid_scan_at = now;
|
|
||||||
}
|
}
|
||||||
if ((_last_position_info_at < now - POSITION_SEND_INTERVAL) || (now < _last_position_info_at)) {
|
if (last_button_check + 10 < millis() || last_button_check > millis()) {
|
||||||
send_position();
|
handle_buttons();
|
||||||
_last_position_info_at = now;
|
last_button_check = millis();
|
||||||
}
|
}
|
||||||
_check_serial();
|
if (last_position_save + 10000 < millis() || last_position_save > millis()) {
|
||||||
_check_buttons();
|
current_playlist.save_current_position(audio.getFilePos());
|
||||||
if (_cmd_queue.length() > 0) {
|
last_position_save = millis();
|
||||||
process_message(_cmd_queue);
|
//Serial.println(pm->pp_to_String().c_str());
|
||||||
_cmd_queue = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef OTA_UPDATE_URL
|
|
||||||
if (!player->is_playing() && _last_update_check_at < now && _last_update_check_at + OTA_CHECK_INTERVAL < now) {
|
|
||||||
Updater::run();
|
|
||||||
} else {
|
|
||||||
_last_update_check_at = now;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!player->is_playing() && !WiFi.isConnected() && _last_wifi_try_at < now && _last_wifi_try_at + 5*60*1000 < now) {
|
|
||||||
wifi_connect();
|
|
||||||
} else {
|
|
||||||
_last_wifi_try_at = now;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Controller::_get_rfid_card_uid() {
|
void Controller::handle_buttons() {
|
||||||
SPIMaster::select_rc522();
|
if (is_button_pressed(PIN_BTN_VOL_UP)) {
|
||||||
if (!_rfid->PICC_ReadCardSerial()) {
|
log_i("BTN_VOL_UP pressed");
|
||||||
if (!_rfid->PICC_IsNewCardPresent()) {
|
uint8_t vol = min(audio.getVolume()+2, 21);
|
||||||
return 0;
|
log_d("Setting new volume %d", vol);
|
||||||
}
|
audio.setVolume(vol);
|
||||||
if (!_rfid->PICC_ReadCardSerial()) {
|
} else if (is_button_pressed(PIN_BTN_VOL_DOWN)) {
|
||||||
return 0;
|
log_i("BTN_VOL_DOWN pressed");
|
||||||
|
uint8_t vol;
|
||||||
|
if ((vol = audio.getVolume()) >= 3) {
|
||||||
|
vol -= 2;
|
||||||
|
} else {
|
||||||
|
vol = 1;
|
||||||
}
|
}
|
||||||
|
log_d("Setting new volume %d", vol);
|
||||||
|
audio.setVolume(vol);
|
||||||
|
} else if (is_button_pressed(PIN_BTN_TRACK_NEXT)) {
|
||||||
|
log_i("BTN_TRACK_NEXT pressed");
|
||||||
|
next_track();
|
||||||
|
} else if (is_button_pressed(PIN_BTN_TRACK_PREV)) {
|
||||||
|
log_i("BTN_TRACK_PREV pressed");
|
||||||
|
prev_track();
|
||||||
}
|
}
|
||||||
SPIMaster::select_rc522(false);
|
|
||||||
uint32_t uid = _rfid->uid.uidByte[0]<<24 | _rfid->uid.uidByte[1]<<16 | _rfid->uid.uidByte[2]<<8 | _rfid->uid.uidByte[3];
|
|
||||||
return uid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::_check_rfid() {
|
void Controller::handle_rfid() {
|
||||||
//TRACE("check_rfid running...\n");
|
if (is_rfid_present) {
|
||||||
MFRC522::StatusCode status;
|
|
||||||
if (_rfid_present) {
|
|
||||||
byte buffer[2];
|
byte buffer[2];
|
||||||
byte buffer_size = 2;
|
byte buffer_size = 2;
|
||||||
SPIMaster::select_rc522();
|
MFRC522Constants::StatusCode status = rfid->PICC_WakeupA(buffer, &buffer_size);
|
||||||
status = _rfid->PICC_WakeupA(buffer, &buffer_size);
|
if (status == MFRC522Constants::STATUS_OK) {
|
||||||
if (status == MFRC522::STATUS_OK) {
|
|
||||||
// Card is still present.
|
// Card is still present.
|
||||||
_rfid->PICC_HaltA();
|
rfid->PICC_HaltA();
|
||||||
SPIMaster::select_rc522(false);
|
} else {
|
||||||
return;
|
Serial.printf("RFID status is %s\n", MFRC522Debug::GetStatusCodeName(status));
|
||||||
|
is_rfid_present = false;
|
||||||
|
Serial.println("No more RFID card.\n");
|
||||||
|
stop();
|
||||||
}
|
}
|
||||||
SPIMaster::select_rc522(false);
|
|
||||||
// Card is now gone
|
|
||||||
_rfid_present = false;
|
|
||||||
INFO("No more RFID card.\n");
|
|
||||||
if (_state != LOCKED) {
|
|
||||||
player->stop();
|
|
||||||
}
|
|
||||||
send_controller_status();
|
|
||||||
} else {
|
} else {
|
||||||
uint32_t uid = _get_rfid_card_uid();
|
if (rfid->PICC_IsNewCardPresent()) {
|
||||||
if (uid > 0) {
|
if (rfid->PICC_ReadCardSerial()) {
|
||||||
String temp = String(uid, HEX);
|
uint32_t uid = rfid->uid.uidByte[0]<<24 | rfid->uid.uidByte[1]<<16 | rfid->uid.uidByte[2]<<8 | rfid->uid.uidByte[3];
|
||||||
String s_uid = "";
|
Serial.printf("Found new rfid card with uid %x\n", uid);
|
||||||
for (int i=0; i<(8-temp.length()); i++) {
|
is_rfid_present = true;
|
||||||
s_uid.concat("0");
|
if (uid > 0) {
|
||||||
}
|
String temp = String(uid, HEX);
|
||||||
s_uid.concat(temp);
|
String s_uid = "";
|
||||||
INFO("New RFID card uid: %s\n", s_uid.c_str());
|
for (int i=0; i<(8-temp.length()); i++) {
|
||||||
_last_rfid_uid = s_uid;
|
s_uid.concat("0");
|
||||||
_rfid_present = true;
|
}
|
||||||
|
s_uid.concat(temp);
|
||||||
|
|
||||||
String data = _read_rfid_data();
|
String data = read_rfid_data();
|
||||||
_last_rfid_data = data;
|
|
||||||
|
|
||||||
Playlist* pl = pm->get_playlist_for_id(s_uid);
|
play(s_uid, data.indexOf("[random]")>=0);
|
||||||
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() {
|
String Controller::read_rfid_data() {
|
||||||
TRACE("_read_rfid_data() running...\n");
|
log_v("read_rfid_data() running...");
|
||||||
static MFRC522::MIFARE_Key keys[8] = {
|
MFRC522::StatusCode status;
|
||||||
{{0xd3, 0xf7, 0xd3, 0xf7, 0xd3, 0xf7}}, // D3 F7 D3 F7 D3 F7
|
MFRC522::PICC_Type type = rfid->PICC_GetType(rfid->uid.sak);
|
||||||
{{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}, // FF FF FF FF FF FF = factory default
|
uint16_t pageStart = 0;
|
||||||
{{0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5}}, // A0 A1 A2 A3 A4 A5
|
uint16_t pages = 4;
|
||||||
{{0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5}}, // B0 B1 B2 B3 B4 B5
|
uint16_t pageSize = 1;
|
||||||
{{0x4d, 0x3a, 0x99, 0xc3, 0x51, 0xdd}}, // 4D 3A 99 C3 51 DD
|
|
||||||
{{0x1a, 0x98, 0x2c, 0x7e, 0x45, 0x9a}}, // 1A 98 2C 7E 45 9A
|
|
||||||
{{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}}, // AA BB CC DD EE FF
|
|
||||||
{{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}} // 00 00 00 00 00 00
|
|
||||||
};
|
|
||||||
SPIMaster::select_rc522();
|
|
||||||
DEBUG("Trying to read RFID data...\n");
|
|
||||||
|
|
||||||
String data = "";
|
|
||||||
MFRC522::PICC_Type type = _rfid->PICC_GetType(_rfid->uid.sak);
|
|
||||||
|
|
||||||
uint8_t sectors = 0;
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case MFRC522::PICC_TYPE_MIFARE_MINI: sectors = 5; break;
|
case MFRC522Constants::PICC_TYPE_MIFARE_MINI:
|
||||||
case MFRC522::PICC_TYPE_MIFARE_1K: sectors = 16; break;
|
case MFRC522Constants::PICC_TYPE_MIFARE_1K:
|
||||||
case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break;
|
case MFRC522Constants::PICC_TYPE_MIFARE_4K: {
|
||||||
default: INFO("Unknown PICC type %s\n", String(MFRC522::PICC_GetTypeName(type)).c_str());
|
log_v("Trying to authenticate Mifare card.");
|
||||||
}
|
MFRC522::MIFARE_Key key = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
|
||||||
sectors = 2; // Pretend we have only two sectors, so we read only sector #1.
|
status = rfid->PCD_Authenticate(MFRC522Constants::PICC_CMD_MF_AUTH_KEY_A, 4, &key, &rfid->uid);
|
||||||
int good_key_index = -1;
|
if (status == MFRC522Constants::STATUS_OK) {
|
||||||
for (uint8_t sector=1; sector<sectors; sector++) {
|
log_v("Authentication succeeded.");
|
||||||
uint8_t blocks = (sector < 32) ? 4 : 16;
|
} else {
|
||||||
uint8_t block_offset = (sector < 32) ? sector * 4 : 128 + (sector - 32) * 16;
|
log_v("Authentication failed. Trying to read anyway.");
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
pageStart = 4;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (good_key_index == -1) {
|
case MFRC522Constants::PICC_TYPE_MIFARE_UL:
|
||||||
TRACE("Could not find a valid MIFARE key.\n");
|
log_v("PICC type is Mifare Ultralight. No authentication necessary.");
|
||||||
} else {
|
pages = 16;
|
||||||
for (uint8_t block=0; block<blocks-1; block++) {
|
pageSize = 4;
|
||||||
byte buffer[18];
|
break;
|
||||||
uint8_t byte_count = 18;
|
default:
|
||||||
status = _rfid->MIFARE_Read(block_offset + block, buffer, &byte_count);
|
log_v("Unexpected rfid card type %s. Trying to read anyway.", MFRC522Debug::PICC_GetTypeName(type));
|
||||||
if (status != MFRC522::STATUS_OK) {
|
}
|
||||||
DEBUG("MIFARE_Read() failed: %s\n", String(_rfid->GetStatusCodeName(status)).c_str());
|
String data = "";
|
||||||
continue;
|
for (uint8_t block=pageStart; block<pages+pageStart; block+=pageSize) {
|
||||||
}
|
byte buffer[18];
|
||||||
for (int i=0; i<16; i++) {
|
uint8_t byte_count = 18;
|
||||||
if (buffer[i]>=0x20 && buffer[i]<0x7F) data.concat((char)buffer[i]);
|
status = rfid->MIFARE_Read(block, buffer, &byte_count);
|
||||||
}
|
if (status != MFRC522Constants::STATUS_OK) {
|
||||||
}
|
log_d("MIFARE_Read() failed: %s\n", String(MFRC522Debug::GetStatusCodeName(status)).c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int i=0; i<16; i++) {
|
||||||
|
if (buffer[i]>=0x20 && buffer[i]<0x7F) data.concat((char)buffer[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
rfid->PCD_StopCrypto1();
|
||||||
_rfid->PICC_HaltA();
|
log_v("Read rfid data: '%s'", data.c_str());
|
||||||
_rfid->PCD_StopCrypto1();
|
|
||||||
DEBUG("Data from RFID: %s\n", data.c_str());
|
|
||||||
SPIMaster::select_rc522(false);
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Controller::_check_serial() {
|
void Controller::play(String rfid_id, bool shuffle) {
|
||||||
if (Serial.available() > 0) {
|
if (!rfid_id.equals(current_playlist.get_rfid_id())) {
|
||||||
char c = Serial.read();
|
if (pm->has_playlist(rfid_id)) {
|
||||||
Serial.printf("%c", c);
|
current_playlist = pm->get_playlist(rfid_id);
|
||||||
if (c==10 || c==13) {
|
if (shuffle) {
|
||||||
if (_serial_buffer.length()>0) {
|
log_i("Shuffling the playlist.");
|
||||||
process_message(_serial_buffer);
|
current_playlist.shuffle();
|
||||||
_serial_buffer = String();
|
}
|
||||||
|
play();
|
||||||
|
} else {
|
||||||
|
Serial.printf("There is no playlist for rfid_id %s\n", rfid_id.c_str());
|
||||||
|
// This is working more or less, but downloading files is really, REALLY slow. (About 4 minutes for 10 MBytes).
|
||||||
|
//download_album(rfid_id);
|
||||||
|
audio.connecttoFS(SD, "/system/sys_unknown_card.mp3");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!audio.isRunning()) {
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::play() {
|
||||||
|
String file = current_playlist.get_current_file_name();
|
||||||
|
|
||||||
|
if (file.startsWith("/")) {
|
||||||
|
log_i("Playing file %s via connecttoFS", file.c_str());
|
||||||
|
audio.connecttoFS(SD, file.c_str(), current_playlist.get_current_time());
|
||||||
|
} else if (file.startsWith("http")) {
|
||||||
|
log_i("Playing URL %s via connecttohost", file.c_str());
|
||||||
|
audio.connecttoFS(SD, "/system/sys_connecting.mp3");
|
||||||
|
while (audio.isRunning()) {
|
||||||
|
yield();
|
||||||
|
audio.loop();
|
||||||
|
}
|
||||||
|
audio.connecttohost(file.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::next_track() {
|
||||||
|
if (current_playlist.next_track()) {
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::prev_track() {
|
||||||
|
uint32_t time = audio.getAudioCurrentTime();
|
||||||
|
log_d("prev_track() called. getAudioCurrentTime() returns %d", time);
|
||||||
|
if (time >= 5) {
|
||||||
|
log_d("Restarting current track.");
|
||||||
|
current_playlist.restart();
|
||||||
|
play();
|
||||||
|
} else {
|
||||||
|
if (current_playlist.prev_track()) {
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Controller::stop() {
|
||||||
|
if (audio.isRunning()) {
|
||||||
|
current_playlist.set_current_time(audio.stopSong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Controller::is_button_pressed(uint8_t pin) {
|
||||||
|
//log_d("Button %d state is %d", pin, digitalRead(pin));
|
||||||
|
if (!digitalRead(pin)) {
|
||||||
|
// Button is pressed - let's debounce it.
|
||||||
|
if (button_pressed == pin) {
|
||||||
|
if (button_pressed_since + 150 < millis() && !button_already_processed) {
|
||||||
|
button_already_processed = true;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_serial_buffer.concat(c);
|
button_pressed = pin;
|
||||||
}
|
button_pressed_since = millis();
|
||||||
}
|
button_already_processed = false;
|
||||||
}
|
|
||||||
|
|
||||||
bool Controller::process_message(String cmd) {
|
|
||||||
DEBUG("Executing command: %s\n", cmd.c_str());
|
|
||||||
|
|
||||||
if (cmd.startsWith("play ")) {
|
|
||||||
Playlist* p = pm->get_playlist_for_folder(cmd.substring(5));
|
|
||||||
player->play(p);
|
|
||||||
} else if (cmd.equals("play")) {
|
|
||||||
player->play();
|
|
||||||
} else if (cmd.equals("stop")) {
|
|
||||||
player->stop();
|
|
||||||
} else if (cmd.equals("help")) {
|
|
||||||
_execute_command_help();
|
|
||||||
} else if (cmd.equals("-")) {
|
|
||||||
player->vol_down();
|
|
||||||
} else if (cmd.equals("+")) {
|
|
||||||
player->vol_up();
|
|
||||||
} else if (cmd.startsWith("volume=")) {
|
|
||||||
uint8_t vol = cmd.substring(7).toInt();
|
|
||||||
player->set_volume(vol);
|
|
||||||
} else if (cmd.equals("track_prev")) {
|
|
||||||
player->track_prev();
|
|
||||||
} else if (cmd.equals("track_next")) {
|
|
||||||
player->track_next();
|
|
||||||
} else if (cmd.startsWith("track=")) {
|
|
||||||
uint8_t track = cmd.substring(6).toInt();
|
|
||||||
player->set_track(track);
|
|
||||||
} else if (cmd.equals("ids")) {
|
|
||||||
pm->dump_ids();
|
|
||||||
} else if (cmd.equals("reset_vs1053")) {
|
|
||||||
player->stop();
|
|
||||||
player->init();
|
|
||||||
} else if (cmd.equals("reboot")) {
|
|
||||||
ESP.restart();
|
|
||||||
} else if (cmd.startsWith("add_mapping=")) {
|
|
||||||
String rest = cmd.substring(12);
|
|
||||||
uint8_t idx = rest.indexOf('=');
|
|
||||||
String id = rest.substring(0, idx);
|
|
||||||
String folder = rest.substring(idx + 1);
|
|
||||||
pm->add_mapping(id, folder);
|
|
||||||
send_playlist_manager_status();
|
|
||||||
#ifdef OTA_UPDATE_URL
|
|
||||||
} else if (cmd.equals("update")) {
|
|
||||||
Updater::run();
|
|
||||||
#endif
|
|
||||||
} else if (cmd.startsWith("trace=")) {
|
|
||||||
int val = cmd.substring(6).toInt();
|
|
||||||
if (val==0) {
|
|
||||||
trace_enabled = false;
|
|
||||||
prefs.putBool("trace_enabled", false);
|
|
||||||
} else if (val==1) {
|
|
||||||
trace_enabled = true;
|
|
||||||
prefs.putBool("trace_enabled", true);
|
|
||||||
}
|
|
||||||
} else if (cmd.startsWith("debug=")) {
|
|
||||||
int val = cmd.substring(6).toInt();
|
|
||||||
if (val==0) {
|
|
||||||
debug_enabled = false;
|
|
||||||
prefs.putBool("debug_enabled", false);
|
|
||||||
} else if (val==1) {
|
|
||||||
debug_enabled = true;
|
|
||||||
prefs.putBool("debug_enabled", true);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ERROR("Unknown command: %s\n", cmd.c_str());
|
if (button_pressed == pin) {
|
||||||
return false;
|
button_pressed = 0;
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Controller::_execute_command_ls(String path) {
|
|
||||||
INFO("Listing contents of %s:\n", path.c_str());
|
|
||||||
// TODO
|
|
||||||
//std::list<String> files = player->ls(path);
|
|
||||||
//for(std::list<String>::iterator it=files.begin(); it!=files.end(); ++it) {
|
|
||||||
// INFO(" %s\n", (*it).c_str());
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Controller::_execute_command_help() {
|
|
||||||
INFO("Valid commands are:");
|
|
||||||
INFO(" help - Displays this help\n");
|
|
||||||
//INFO(" ls [dir] - Lists the contents of [dir] or, if not given, of /\n");
|
|
||||||
INFO(" ids - Lists all known ID-to-folder mappings\n");
|
|
||||||
INFO(" play [id] - Plays the album with the given id\n");
|
|
||||||
INFO(" stop - Stops playback\n");
|
|
||||||
INFO(" - / + - Decrease or increase the volume\n");
|
|
||||||
INFO(" p / n - Previous or next track\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Controller::_check_buttons() {
|
|
||||||
if (BTN_PREV() && _debounce_button(0)) {
|
|
||||||
if (_state == NORMAL) {
|
|
||||||
player->track_prev();
|
|
||||||
} else {
|
|
||||||
DEBUG("Ignoring btn_prev because state is LOCKED.\n");
|
|
||||||
}
|
|
||||||
} else if (BTN_VOL_UP() && _debounce_button(1)) {
|
|
||||||
player->vol_up();
|
|
||||||
} else if (BTN_VOL_DOWN() && _debounce_button(2)) {
|
|
||||||
player->vol_down();
|
|
||||||
} else if (BTN_NEXT() && _debounce_button(3)) {
|
|
||||||
if (_state == NORMAL) {
|
|
||||||
player->track_next();
|
|
||||||
} else {
|
|
||||||
DEBUG("Ignoring btn_next because state is LOCKED.\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Controller::_debounce_button(uint8_t index) {
|
void Controller::eof_mp3(String info) {
|
||||||
bool ret = false;
|
log_d("Handling eof. Keep playing until the file is finished.");
|
||||||
if (_button_last_pressed_at[index] + DEBOUNCE_MILLIS < millis()) {
|
while(audio.isRunning()) { audio.loop(); yield; }
|
||||||
DEBUG("Button %d pressed.\n", index);
|
if (info.startsWith("sys_")) {
|
||||||
ret = true;
|
log_d("File ending was a system audio file. Not running next_track.");
|
||||||
}
|
|
||||||
_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 {
|
} else {
|
||||||
wifi["connected"] = false;
|
next_track();
|
||||||
}
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
#include "data_sources.h"
|
|
||||||
|
|
||||||
////////////// SDDataSource //////////////
|
|
||||||
SDDataSource::SDDataSource(String file) { _file = SD.open(file, "r"); }
|
|
||||||
SDDataSource::~SDDataSource() { if (_file) _file.close(); }
|
|
||||||
size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); }
|
|
||||||
int SDDataSource::read() { return _file.read(); }
|
|
||||||
size_t SDDataSource::position() { return _file.position(); }
|
|
||||||
void SDDataSource::seek(size_t position) { _file.seek(position); }
|
|
||||||
size_t SDDataSource::size() { return _file.size(); }
|
|
||||||
void SDDataSource::close() { _file.close(); }
|
|
||||||
bool SDDataSource::usable() { return _file; }
|
|
||||||
|
|
||||||
void SDDataSource::skip_id3_tag() {
|
|
||||||
uint32_t original_position = _file.position();
|
|
||||||
uint32_t offset = 0;
|
|
||||||
if (_file.read()=='I' && _file.read()=='D' && _file.read()=='3') {
|
|
||||||
DEBUG("ID3 tag found\n");
|
|
||||||
// Skip ID3 tag version
|
|
||||||
_file.read(); _file.read();
|
|
||||||
byte tags = _file.read();
|
|
||||||
bool footer_present = tags & 0x10;
|
|
||||||
DEBUG("ID3 footer found: %d\n", footer_present);
|
|
||||||
for (byte i=0; i<4; i++) {
|
|
||||||
offset <<= 7;
|
|
||||||
offset |= (0x7F & _file.read());
|
|
||||||
}
|
|
||||||
offset += 10;
|
|
||||||
if (footer_present) offset += 10;
|
|
||||||
DEBUG("ID3 tag length is %d bytes.\n", offset);
|
|
||||||
_file.seek(offset);
|
|
||||||
} else {
|
|
||||||
DEBUG("No ID3 tag found\n");
|
|
||||||
_file.seek(original_position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
////////////// HTTPSDataSource //////////////
|
|
||||||
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
|
|
||||||
_http = new HTTPClientWrapper();
|
|
||||||
if (!_http->get(url, offset)) return;
|
|
||||||
_position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTPSDataSource::~HTTPSDataSource() {
|
|
||||||
_http->close();
|
|
||||||
delete _http;
|
|
||||||
}
|
|
||||||
bool HTTPSDataSource::usable() { return _http; }
|
|
||||||
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; }
|
|
||||||
int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; }
|
|
||||||
size_t HTTPSDataSource::position() { return _position; }
|
|
||||||
void HTTPSDataSource::seek(size_t position) { return; /* TODO */ }
|
|
||||||
size_t HTTPSDataSource::size() { return _http->getSize(); }
|
|
||||||
void HTTPSDataSource::close() { _http->close(); }
|
|
139
src/esmp3.cpp
Normal file
139
src/esmp3.cpp
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
#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);
|
||||||
|
}
|
@ -1,213 +0,0 @@
|
|||||||
#include "http_client_wrapper.h"
|
|
||||||
#include <WiFiClientSecure.h>
|
|
||||||
|
|
||||||
HTTPClientWrapper::HTTPClientWrapper() {
|
|
||||||
_buffer = new uint8_t[512];
|
|
||||||
_buffer_size = 512;
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTPClientWrapper::~HTTPClientWrapper() {
|
|
||||||
if (_http) {
|
|
||||||
_http->end();
|
|
||||||
delete _http;
|
|
||||||
}
|
|
||||||
delete _buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HTTPClientWrapper::close() {
|
|
||||||
_http->end();
|
|
||||||
_connected = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HTTPClientWrapper::get(String url, uint32_t offset, uint8_t redirection_count) { return _request("GET", url, offset, redirection_count); }
|
|
||||||
bool HTTPClientWrapper::head(String url, uint32_t offset, uint8_t redirection_count) { return _request("HEAD", url, offset, redirection_count); }
|
|
||||||
|
|
||||||
bool HTTPClientWrapper::_request(String method, String url, uint32_t offset, uint8_t redirection_count) {
|
|
||||||
if (redirection_count>=5) return false;
|
|
||||||
|
|
||||||
//if (_http) delete _http;
|
|
||||||
|
|
||||||
DEBUG("%s %s\n", method.c_str(), url.c_str());
|
|
||||||
_http = new HTTPClient();
|
|
||||||
|
|
||||||
|
|
||||||
_http->setUserAgent("PodBox/0.1");
|
|
||||||
if (offset) {
|
|
||||||
String s = "bytes=";
|
|
||||||
s += offset;
|
|
||||||
s += "-";
|
|
||||||
_http->addHeader("Range: ", s);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* headers[] = {"Location", "Content-Type", "Transfer-Encoding"};
|
|
||||||
_http->collectHeaders(headers, 3);
|
|
||||||
bool result;
|
|
||||||
/*if (url.startsWith("https:")) {
|
|
||||||
BearSSL::WiFiClientSecure* client = new BearSSL::WiFiClientSecure();
|
|
||||||
client->setInsecure();
|
|
||||||
result = _http->begin(*client, url);
|
|
||||||
} else {
|
|
||||||
result = _http->begin(url);
|
|
||||||
}*/
|
|
||||||
result = _http->begin(url);
|
|
||||||
TRACE("HTTP->begin result: %d\n", result);
|
|
||||||
if (!result) return false;
|
|
||||||
|
|
||||||
int status = _http->sendRequest(method.c_str());
|
|
||||||
TRACE("HTTP Status code: %d\n", status);
|
|
||||||
if (status == HTTP_CODE_FOUND || status==HTTP_CODE_MOVED_PERMANENTLY || status==HTTP_CODE_TEMPORARY_REDIRECT) {
|
|
||||||
if (_http->hasHeader("Location")) {
|
|
||||||
url = _http->header("Location");
|
|
||||||
_http->end();
|
|
||||||
delete _http;
|
|
||||||
_http = NULL;
|
|
||||||
return _request(method, url, offset, redirection_count+1);
|
|
||||||
} else {
|
|
||||||
ERROR("Got redirection HTTP code, but no Location header.\n");
|
|
||||||
delete _http;
|
|
||||||
_http = NULL;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (status != HTTP_CODE_OK) {
|
|
||||||
DEBUG("Unexpected HTTP return code %d. Cancelling.\n", status);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_buffer_position = 0;
|
|
||||||
_buffer_length = 0;
|
|
||||||
|
|
||||||
_connected = true;
|
|
||||||
_length = _http->getSize() + offset;
|
|
||||||
if (_http->hasHeader("Content-Type")) {
|
|
||||||
_content_type = _http->header("Content-Type");
|
|
||||||
} else {
|
|
||||||
_content_type = "";
|
|
||||||
}
|
|
||||||
_is_chunked = (_http->hasHeader("Transfer-Encoding")) && (_http->header("Transfer-Encoding").indexOf("chunked")!=-1);
|
|
||||||
_stream = _http->getStreamPtr();
|
|
||||||
if (_is_chunked) {
|
|
||||||
_read_next_chunk_header(true);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HTTPClientWrapper::_read_next_chunk_header(bool first) {
|
|
||||||
if (!_connected) {
|
|
||||||
_chunk_length = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!first) {
|
|
||||||
// read() returns an error if no bytes is available right at this moment.
|
|
||||||
// So we wait until 2 bytes are available or the connection times out.
|
|
||||||
while (_stream->connected() && !_stream->available()) { delay(1); }
|
|
||||||
int c1 = _stream->read();
|
|
||||||
while (_stream->connected() && !_stream->available()) { delay(1); }
|
|
||||||
int c2 = _stream->read();
|
|
||||||
if (c1==-1 || c2==-1) {
|
|
||||||
ERROR("Connection timeout.\n");
|
|
||||||
DEBUG("_stream.connected() returns %d\n", _stream->connected());
|
|
||||||
_chunk_length = 0;
|
|
||||||
_connected = false;
|
|
||||||
return;
|
|
||||||
} else if (c1!='\r' || c2!='\n') {
|
|
||||||
ERROR("Invalid chunk border found. Found: 0x%02X 0x%02X\n", c1, c2);
|
|
||||||
_chunk_length = 0;
|
|
||||||
_connected = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String chunk_header = _stream->readStringUntil('\n');
|
|
||||||
chunk_header.trim();
|
|
||||||
_chunk_length = strtol(chunk_header.c_str(), NULL, 16);
|
|
||||||
if (_chunk_length == 0) {
|
|
||||||
_connected = false;
|
|
||||||
TRACE("Empty chunk found -> EOF reached.\n");
|
|
||||||
} else {
|
|
||||||
TRACE("Chunk found. Length: %d\n", _chunk_length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t HTTPClientWrapper::_fill_buffer() {
|
|
||||||
if (!_connected) {
|
|
||||||
_buffer_position = 0;
|
|
||||||
_buffer_length = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t bytes_to_fill = _buffer_size;
|
|
||||||
uint16_t bytes_filled = 0;
|
|
||||||
while (bytes_to_fill > 0) {
|
|
||||||
uint16_t bytes_to_request = bytes_to_fill;
|
|
||||||
if (_is_chunked && _chunk_length < bytes_to_fill) bytes_to_request = _chunk_length;
|
|
||||||
TRACE("fill_buffer loop. _is_chunked: %d, _chunk_length: %d, _buffer_size: %d, bytes_filled: %d, bytes_to_fill: %d, bytes_to_request: %d", _is_chunked, _chunk_length, _buffer_size, bytes_filled, bytes_to_fill, bytes_to_request);
|
|
||||||
uint16_t result = _stream->readBytes(_buffer + bytes_filled, bytes_to_request);
|
|
||||||
TRACE(", result: %d\n", result);
|
|
||||||
bytes_filled += result;
|
|
||||||
bytes_to_fill -= result;
|
|
||||||
if (_is_chunked) {
|
|
||||||
_chunk_length -= result;
|
|
||||||
if (_chunk_length == 0) _read_next_chunk_header(false);
|
|
||||||
}
|
|
||||||
if (result == 0) {
|
|
||||||
_connected = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_buffer_position = 0;
|
|
||||||
_buffer_length = bytes_filled;
|
|
||||||
TRACE("Buffer filled. _buffer_length: %d\n", _buffer_length);
|
|
||||||
return bytes_filled;
|
|
||||||
}
|
|
||||||
|
|
||||||
String HTTPClientWrapper::getContentType() {
|
|
||||||
return _content_type;
|
|
||||||
}
|
|
||||||
|
|
||||||
int HTTPClientWrapper::read() {
|
|
||||||
if (_buffer_position >= _buffer_length) _fill_buffer();
|
|
||||||
if (_buffer_position >= _buffer_length) return -1;
|
|
||||||
return _buffer[_buffer_position++];
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t HTTPClientWrapper::read(uint8_t* dst, uint32_t len) {
|
|
||||||
TRACE("Reading %d bytes...\n", len);
|
|
||||||
uint32_t bytes_filled = 0;
|
|
||||||
while (1) {
|
|
||||||
if (_buffer_position >= _buffer_length) _fill_buffer();
|
|
||||||
if (_buffer_position >= _buffer_length) break;
|
|
||||||
|
|
||||||
uint32_t bytes_to_fill = len;
|
|
||||||
if (bytes_to_fill > _buffer_length - _buffer_position) bytes_to_fill = _buffer_length - _buffer_position;
|
|
||||||
|
|
||||||
TRACE("read_loop: _buffer_length=%d, _buffer_position=%d, len=%d, bytes_to_fill=%d\n", _buffer_length, _buffer_position, len, bytes_to_fill);
|
|
||||||
memcpy(dst + bytes_filled, _buffer + _buffer_position, bytes_to_fill);
|
|
||||||
_buffer_position += bytes_to_fill;
|
|
||||||
bytes_filled += bytes_to_fill;
|
|
||||||
len -= bytes_to_fill;
|
|
||||||
if (bytes_to_fill==0 || len==0) break;
|
|
||||||
}
|
|
||||||
return bytes_filled;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t HTTPClientWrapper::getSize() {return _length; }
|
|
||||||
|
|
||||||
String HTTPClientWrapper::readUntil(String sep) {
|
|
||||||
String result = "";
|
|
||||||
while(true) {
|
|
||||||
int i = read();
|
|
||||||
if (i==-1) break;
|
|
||||||
char c = i;
|
|
||||||
if (sep.indexOf(c)!=-1) {
|
|
||||||
// separator
|
|
||||||
if (result.length()>0) break;
|
|
||||||
} else {
|
|
||||||
result.concat(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
String HTTPClientWrapper::readLine() {
|
|
||||||
return readUntil("\n\r");
|
|
||||||
}
|
|
@ -1,167 +0,0 @@
|
|||||||
#include "http_server.h"
|
|
||||||
#include "main.h"
|
|
||||||
#include "spi_master.h"
|
|
||||||
#include <ESPmDNS.h>
|
|
||||||
|
|
||||||
HTTPServer::HTTPServer(Player* p, Controller* c) {
|
|
||||||
_player = p;
|
|
||||||
_controller = c;
|
|
||||||
_server = new AsyncWebServer(80);
|
|
||||||
ws = new AsyncWebSocket("/ws");
|
|
||||||
_server->addHandler(ws);
|
|
||||||
ws->onEvent([&](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){this->_onEvent(server, client, type, arg, data, len);});
|
|
||||||
|
|
||||||
_server->on("/", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
|
||||||
req->send(200, "text/html", (const char*)file_index_html_start);
|
|
||||||
});
|
|
||||||
_server->on("/upload", HTTP_POST, [](AsyncWebServerRequest* req) {
|
|
||||||
req->send(200);
|
|
||||||
}, [&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
|
||||||
this->_handle_upload(request, filename, index, data, len, final);
|
|
||||||
});
|
|
||||||
_server->on("/_mapping.txt", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
|
||||||
req->send(200, "text/plain", _controller->pm->create_mapping_txt());
|
|
||||||
});
|
|
||||||
_server->on("/player.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
|
||||||
req->send(200, "application/json", _controller->player->json());
|
|
||||||
});
|
|
||||||
_server->on("/playlist_manager.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
|
||||||
req->send(200, "application/json", _controller->pm->json());
|
|
||||||
});
|
|
||||||
_server->on("/controller.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
|
||||||
req->send(200, "application/json", _controller->json());
|
|
||||||
});
|
|
||||||
_server->on("/position.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
|
||||||
req->send(200, "application/json", _controller->player->position_json());
|
|
||||||
});
|
|
||||||
_server->on("/cmd", HTTP_POST, [&](AsyncWebServerRequest *req) {
|
|
||||||
if (req->hasParam("cmd", true)) {
|
|
||||||
_controller->queue_command(req->getParam("cmd", true)->value());
|
|
||||||
req->send(200);
|
|
||||||
} else {
|
|
||||||
req->send(400);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
_server->begin();
|
|
||||||
MDNS.addService("http", "tcp", 80);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HTTPServer::_handle_upload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) {
|
|
||||||
// https://www.gnu.org/software/tar/manual/html_node/Standard.html
|
|
||||||
// https://www.mkssoftware.com/docs/man4/tar.4.asp
|
|
||||||
|
|
||||||
if (index == 0) { // Starting upload
|
|
||||||
_chunk = new uint8_t[512];
|
|
||||||
_chunk_length = 0;
|
|
||||||
_upload_position = 0;
|
|
||||||
_file_size = 0;
|
|
||||||
_file_size_done = 0;
|
|
||||||
_need_header = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t upload_offset = 0;
|
|
||||||
while (upload_offset < len) {
|
|
||||||
// Load a chunk
|
|
||||||
if (_chunk_length < 512 && len > upload_offset) {
|
|
||||||
uint16_t needed = 512 - _chunk_length;
|
|
||||||
if (needed > len - upload_offset) needed = len - upload_offset;
|
|
||||||
memcpy(_chunk + _chunk_length, data + upload_offset, needed);
|
|
||||||
_chunk_length += needed;
|
|
||||||
upload_offset += needed;
|
|
||||||
_upload_position += needed;
|
|
||||||
|
|
||||||
if (_chunk_length == 512) {
|
|
||||||
// Process chunk
|
|
||||||
DEBUG(".");
|
|
||||||
if (_need_header) {
|
|
||||||
if (_chunk[257]=='u'&&_chunk[258]=='s'&&_chunk[259]=='t'&&_chunk[260]=='a'&&_chunk[261]=='r') {
|
|
||||||
DEBUG("It is a valid header, starting at 0x%X!\n", _upload_position-512);
|
|
||||||
char filename[200];
|
|
||||||
strncpy(filename, (char*)_chunk, 100);
|
|
||||||
DEBUG("filename: %s\n", filename);
|
|
||||||
_file_size = 0;
|
|
||||||
_file_size_done = 0;
|
|
||||||
for (int i=0; i<11; i++) {
|
|
||||||
//Serial.print(_header_buffer[124 + i]);
|
|
||||||
_file_size = (_file_size<<3) + (_chunk[124 + i] - '0');
|
|
||||||
}
|
|
||||||
DEBUG("filesize: %d\n", _file_size);
|
|
||||||
uint8_t type = _chunk[156] - '0';
|
|
||||||
if (type==0) {
|
|
||||||
String path = "/";
|
|
||||||
path += filename;
|
|
||||||
DEBUG("Opening file %s\n", path.c_str());
|
|
||||||
uint8_t state = SPIMaster::state;
|
|
||||||
SPIMaster::disable();
|
|
||||||
SPIMaster::select_sd();
|
|
||||||
// Better safe than sorry. ;-)
|
|
||||||
_upload_file.close();
|
|
||||||
_upload_file = SD.open(path, "w");
|
|
||||||
SPIMaster::set_state(state);
|
|
||||||
} else if (type==5) {
|
|
||||||
String dirname = "/";
|
|
||||||
dirname += filename;
|
|
||||||
dirname.remove(dirname.length()-1);
|
|
||||||
uint8_t state = SPIMaster::state;
|
|
||||||
SPIMaster::disable();
|
|
||||||
SPIMaster::select_sd();
|
|
||||||
bool res = SD.mkdir(dirname);
|
|
||||||
SPIMaster::set_state(state);
|
|
||||||
DEBUG("Creating folder '%s' returned %d.\n", dirname.c_str(), res);
|
|
||||||
} else {
|
|
||||||
ERROR("Unknown file type %d\n", type);
|
|
||||||
}
|
|
||||||
_need_header = (type==5 || _file_size==0); // No chunks needed for directories.
|
|
||||||
} else {
|
|
||||||
bool byte_found = false;
|
|
||||||
for (int i=0; i<512; i++) byte_found = byte_found || _chunk[i]>0;
|
|
||||||
if (!byte_found) {
|
|
||||||
DEBUG("Empty chunk while looking for header -> ignoring.\n");
|
|
||||||
} else {
|
|
||||||
ERROR("Invalid tar header: %c %c %c %c %c. Looking at header start offset 0x%X.\n", _chunk[257], _chunk[258], _chunk[259], _chunk[260], _chunk[261], _upload_position-512);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uint32_t bytes_to_write = _file_size - _file_size_done;
|
|
||||||
if (bytes_to_write > 512) bytes_to_write=512;
|
|
||||||
uint8_t state = SPIMaster::state;
|
|
||||||
SPIMaster::disable();
|
|
||||||
SPIMaster::select_sd();
|
|
||||||
_upload_file.write(_chunk, bytes_to_write);
|
|
||||||
_file_size_done += bytes_to_write;
|
|
||||||
if (_file_size_done >= _file_size) {
|
|
||||||
_upload_file.close();
|
|
||||||
_need_header = true;
|
|
||||||
}
|
|
||||||
SPIMaster::set_state(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
_chunk_length = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (final == true) {
|
|
||||||
uint8_t state = SPIMaster::state;
|
|
||||||
SPIMaster::disable();
|
|
||||||
SPIMaster::select_sd();
|
|
||||||
_upload_file.close();
|
|
||||||
SPIMaster::set_state(state);
|
|
||||||
delete _chunk;
|
|
||||||
_controller->update_playlist_manager();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HTTPServer::_onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) {
|
|
||||||
if (type==WS_EVT_CONNECT) {
|
|
||||||
_controller->inform_new_client(client);
|
|
||||||
} else if (type==WS_EVT_DATA) {
|
|
||||||
AwsFrameInfo* info = (AwsFrameInfo*) arg;
|
|
||||||
if (info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT) {
|
|
||||||
data[len]='\0';
|
|
||||||
DEBUG("Received ws message: %s\n", (char*)data);
|
|
||||||
_controller->queue_command((char*)data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
359
src/index.html
359
src/index.html
@ -1,359 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
<title>ESMP3</title>
|
|
||||||
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8=" crossorigin="anonymous"></script>
|
|
||||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://kit.fontawesome.com/272149490a.js" crossorigin="anonymous"></script>
|
|
||||||
<style>
|
|
||||||
.overlay {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.85);
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
margin: auto 0px;
|
|
||||||
vertical-align: middle;
|
|
||||||
color: white;
|
|
||||||
font-size: 60px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="overlay" class="overlay">Not connected...</div>
|
|
||||||
<div class="container bg-dark text-light">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-1">
|
|
||||||
<h1 id="play_state_icon"><i class="fa fa-stop"></i></h1>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-11">
|
|
||||||
<h2><i class="fa fa-compact-disc"></i> <span id="album"></span></h2>
|
|
||||||
<h2><i class="fa fa-scroll"></i> <span id="track"></span></h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<input type="range" class="custom-range" id="position_slider" disabled>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-6">
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-1">
|
|
||||||
<h3><i class="fa fa-volume-down"></i></h3>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input type="range" class="custom-range" id="volume_slider">
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-1">
|
|
||||||
<h3><i class="fa fa-volume-up"></i></h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_track_prev"><i class="fa fa-step-backward"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_stop"><i class="fa fa-stop"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_play"><i class="fa fa-play"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_track_next"><i class="fa fa-step-forward"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_lock"><i class="fa fa-lock-open"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_open"><i class="fa fa-eject"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="col">
|
|
||||||
<button type="button" class="btn btn-primary btn-lg btn-block" id="button_settings"><i class="fa fa-cog"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<table class="table table-hover table-sm" id="track_list_table">
|
|
||||||
<thead class="thead-light">
|
|
||||||
<tr>
|
|
||||||
<th>Nr.</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Track</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="" id="track_list">
|
|
||||||
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="modal fade" id="openModal" tabindex="-1" role="dialog">
|
|
||||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Album öffnen</h5>
|
|
||||||
<button type="button" class="close" data-dismiss="modal">
|
|
||||||
<span>×</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>×</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>
|
|
144
src/main.cpp
144
src/main.cpp
@ -1,144 +0,0 @@
|
|||||||
#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
753
src/player.cpp
@ -1,753 +0,0 @@
|
|||||||
// Based on https://github.com/mpflaga/Arduino_Library-vs1053_for_SdFat/blob/master/src/vs1053_SdFat.cpp
|
|
||||||
|
|
||||||
#include "player.h"
|
|
||||||
#include "spi_master.h"
|
|
||||||
#include <ArduinoJson.h>
|
|
||||||
|
|
||||||
//Player::_spi_settings
|
|
||||||
|
|
||||||
Player::Player(SPIMaster* s) {
|
|
||||||
_spi = s;
|
|
||||||
PIN_VS1053_XRESET_SETUP();
|
|
||||||
PIN_VS1053_XRESET(HIGH);
|
|
||||||
_speaker_off();
|
|
||||||
_spi->disable();
|
|
||||||
PIN_VS1053_DREQ_SETUP();
|
|
||||||
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::register_controller(Controller* c) {
|
|
||||||
_controller = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_reset() {
|
|
||||||
PIN_VS1053_XRESET(LOW);
|
|
||||||
delay(100);
|
|
||||||
PIN_VS1053_XRESET(HIGH);
|
|
||||||
delay(100);
|
|
||||||
_state = uninitialized;
|
|
||||||
_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::init() {
|
|
||||||
DEBUG("Resetting VS1053...\n");
|
|
||||||
_reset();
|
|
||||||
|
|
||||||
uint16_t result = _read_control_register(SCI_MODE);
|
|
||||||
DEBUG("SCI_MODE: 0x%04X\n", result);
|
|
||||||
if (result != 0x4800) {
|
|
||||||
ERROR("SCI_MODE was 0x%04X, expected was 0x4800. Rebooting.\n", result);
|
|
||||||
delay(500);
|
|
||||||
ESP.restart();
|
|
||||||
}
|
|
||||||
result = _read_control_register(SCI_STATUS);
|
|
||||||
DEBUG("SCI_STATUS: 0x%04X\n", result);
|
|
||||||
if (result != 0x0040 && result != 0x0048) {
|
|
||||||
ERROR("SCI_STATUS was 0x%04X, expected was 0x0040 or 0x0048. Rebooting.\n", result);
|
|
||||||
delay(500);
|
|
||||||
ESP.restart();
|
|
||||||
}
|
|
||||||
result = _read_control_register(SCI_CLOCKF);
|
|
||||||
DEBUG("SCI_CLOCKF: 0x%04X\n", result);
|
|
||||||
|
|
||||||
DEBUG("VS1053 Init looking good.\n");
|
|
||||||
DEBUG("Upping VS1053 multiplier...\n");
|
|
||||||
|
|
||||||
_write_control_register(SCI_CLOCKF, 0xC000);
|
|
||||||
delay(10);
|
|
||||||
|
|
||||||
_spi_settings = &_spi_settings_fast;
|
|
||||||
|
|
||||||
result = _read_control_register(SCI_CLOCKF);
|
|
||||||
DEBUG("SCI_CLOCKF: 0x%04X\n", result);
|
|
||||||
if (result != 0xC000) {
|
|
||||||
ERROR("Error: SCI_CLOCKF was 0x%04X, expected was 0xC000. Rebooting.\n", result);
|
|
||||||
delay(500);
|
|
||||||
ESP.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
set_volume(VOLUME_DEFAULT);
|
|
||||||
|
|
||||||
INFO("VS1053 initialization completed.\n");
|
|
||||||
|
|
||||||
_state = idle;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_speaker_off() {
|
|
||||||
DEBUG("Speaker off\n");
|
|
||||||
PIN_SPEAKER_L(LOW);
|
|
||||||
PIN_SPEAKER_R(LOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_speaker_on() {
|
|
||||||
DEBUG("Speaker on\n");
|
|
||||||
PIN_SPEAKER_L(HIGH);
|
|
||||||
PIN_SPEAKER_R(HIGH);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_sleep() {
|
|
||||||
DEBUG("VS1053 going to sleep.\n");
|
|
||||||
_speaker_off();
|
|
||||||
_write_control_register(SCI_CLOCKF, 0x0000);
|
|
||||||
_spi_settings = &_spi_settings_slow;
|
|
||||||
_write_control_register(SCI_AUDATA, 0x0010);
|
|
||||||
set_volume(0, false);
|
|
||||||
_state = sleeping;
|
|
||||||
TRACE("VS1053 is sleeping now.\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_wakeup() {
|
|
||||||
if (_state != sleeping && _state != recording) return;
|
|
||||||
_stopped_at = millis();
|
|
||||||
DEBUG("Waking VS1053...\n");
|
|
||||||
set_volume(_volume, false);
|
|
||||||
_write_control_register(SCI_AUDATA, 0x0000);
|
|
||||||
_write_control_register(SCI_CLOCKF, 0x6000);
|
|
||||||
_write_control_register(SCI_MODE, 0x4800 | SM_RESET);
|
|
||||||
delay(10);
|
|
||||||
//_speaker_on();
|
|
||||||
_spi_settings = &_spi_settings_fast;
|
|
||||||
_state = idle;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_record() {
|
|
||||||
// http://www.vlsi.fi/fileadmin/software/VS10XX/VS1053_VS1063_PcmRecorder.pdf
|
|
||||||
DEBUG("Starting recording.\n");
|
|
||||||
set_volume(1, false);
|
|
||||||
|
|
||||||
// Disable SCI_BASS
|
|
||||||
_write_control_register(SCI_BASS, 0);
|
|
||||||
|
|
||||||
// Disable user applications
|
|
||||||
_write_control_register(SCI_AIADDR, 0);
|
|
||||||
|
|
||||||
// Disable interrupts
|
|
||||||
_write_control_register(SCI_WRAMADDR, 0xC01A);
|
|
||||||
_write_control_register(SCI_WRAM, 0x0002);
|
|
||||||
|
|
||||||
_patch_adpcm();
|
|
||||||
|
|
||||||
_write_control_register(SCI_MODE, SM_ADPCM);
|
|
||||||
|
|
||||||
_write_control_register(SCI_AICTRL0, 0x8000); // Mono VU meter
|
|
||||||
_write_control_register(SCI_AICTRL1, 1024); // Manual gain, 1x
|
|
||||||
_write_control_register(SCI_AICTRL2, 0); // Maximum gain for autogain - ignored
|
|
||||||
_write_control_register(SCI_AICTRL3, 0); // status: record
|
|
||||||
|
|
||||||
_write_control_register(SCI_AIADDR, 0x0034, false);
|
|
||||||
delay(1);
|
|
||||||
|
|
||||||
DEBUG("Recording.\n");
|
|
||||||
delay(10);
|
|
||||||
_state = recording;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Player::_wait() {
|
|
||||||
while(!PIN_VS1053_DREQ());
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t Player::_read_control_register(uint8_t address, bool do_wait) {
|
|
||||||
if (do_wait) _wait();
|
|
||||||
_spi->select_vs1053_xcs();
|
|
||||||
SPI.beginTransaction(*_spi_settings);
|
|
||||||
SPI.transfer(CMD_READ);
|
|
||||||
SPI.transfer(address);
|
|
||||||
uint8_t b1 = SPI.transfer(0xFF);
|
|
||||||
_wait();
|
|
||||||
uint8_t b2 = SPI.transfer(0xFF);
|
|
||||||
_wait();
|
|
||||||
SPI.endTransaction();
|
|
||||||
_spi->select_vs1053_xcs(false);
|
|
||||||
|
|
||||||
return (b1 << 8) | b2;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_write_control_register(uint8_t address, uint16_t value, bool do_wait) {
|
|
||||||
_wait();
|
|
||||||
_spi->select_vs1053_xcs();
|
|
||||||
SPI.beginTransaction(*_spi_settings);
|
|
||||||
SPI.transfer(CMD_WRITE);
|
|
||||||
SPI.transfer(address);
|
|
||||||
SPI.transfer(value >> 8);
|
|
||||||
SPI.transfer(value & 0xFF);
|
|
||||||
SPI.endTransaction();
|
|
||||||
_spi->select_vs1053_xcs(false);
|
|
||||||
if (do_wait) _wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_patch_adpcm() {
|
|
||||||
static const uint16_t patch_data[] = {
|
|
||||||
0x0007, 0x0001, 0xc01a, 0x0006, 0x0001, 0x0002, 0x0007, 0x0001, /* 0 */
|
|
||||||
0x0008, 0x0006, 0x8002, 0x0000, 0x0007, 0x0001, 0x000c, 0x0006, /* 8 */
|
|
||||||
0x0002, 0x7000, 0x0017, 0x0007, 0x0001, 0x8034, 0x0006, 0x0022, /* 10 */
|
|
||||||
0x0030, 0x0490, 0xb080, 0x0024, 0x3800, 0x0024, 0x0000, 0x1090, /* 18 */
|
|
||||||
0xf400, 0x5404, 0x0000, 0x0851, 0xf400, 0x5648, 0xf400, 0x5404, /* 20 */
|
|
||||||
0xf400, 0x5658, 0xf400, 0x5404, 0xf400, 0x5640, 0x0000, 0x800a, /* 28 */
|
|
||||||
0x2900, 0x9180, 0x0006, 0x2016, 0x2a00, 0x1bce, 0x2a00, 0x114e, /* 30 */
|
|
||||||
0x2a00, 0x168e, 0x0007, 0x0001, 0x1800, 0x0006, 0x8006, 0x0000, /* 38 */
|
|
||||||
0x0007, 0x0001, 0x8045, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, /* 40 */
|
|
||||||
0x7808, 0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, /* 48 */
|
|
||||||
0x7800, 0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x3e80, 0x3e10, /* 50 */
|
|
||||||
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, /* 58 */
|
|
||||||
0x4024, 0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, /* 60 */
|
|
||||||
0x8024, 0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, /* 68 */
|
|
||||||
0x0001, 0x805a, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, 0x7808, /* 70 */
|
|
||||||
0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, 0x7800, /* 78 */
|
|
||||||
0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x5e40, 0x3e10, 0x7800, /* 80 */
|
|
||||||
0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, 0x4024, /* 88 */
|
|
||||||
0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, /* 90 */
|
|
||||||
0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, /* 98 */
|
|
||||||
0x806f, 0x0006, 0x0030, 0x3e12, 0xb817, 0x3e12, 0x7808, 0x3e18, /* a0 */
|
|
||||||
0x3821, 0x3e18, 0xb823, 0x3e10, 0x7800, 0xb880, 0x3855, 0x0030, /* a8 */
|
|
||||||
0x0497, 0x48b2, 0x3c00, 0x0000, 0x800a, 0x2900, 0x7300, 0x3e10, /* b0 */
|
|
||||||
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x6890, 0x1bd5, 0x0030, /* b8 */
|
|
||||||
0x0497, 0x3f00, 0x0024, 0x36f0, 0x5800, 0x36f8, 0x9823, 0x36f8, /* c0 */
|
|
||||||
0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, 0x0030, 0x0717, 0x2100, /* c8 */
|
|
||||||
0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, 0x8010, 0x0006, 0x000e, /* d0 */
|
|
||||||
0x3e02, 0x8024, 0x0001, 0x000a, 0x6012, 0x0024, 0xfea2, 0x0024, /* d8 */
|
|
||||||
0x48b2, 0x1bca, 0x2000, 0x0000, 0x4180, 0x0024, 0x0007, 0x0001, /* e0 */
|
|
||||||
0x8087, 0x0006, 0x00e6, 0x3e00, 0x7843, 0x3e01, 0x3845, 0x3e04, /* e8 */
|
|
||||||
0x3812, 0x0006, 0x08d0, 0x3000, 0x4024, 0x6182, 0x0024, 0x0030, /* f0 */
|
|
||||||
0x06d0, 0x2800, 0x2655, 0xb882, 0x0024, 0x0000, 0x0201, 0x0000, /* f8 */
|
|
||||||
0x0005, 0x0030, 0x0210, 0xa016, 0x4004, 0x1fff, 0xfe01, 0xae1a, /* 100 */
|
|
||||||
0x0024, 0xc342, 0x0024, 0xb882, 0x2001, 0x0030, 0x06d0, 0x3800, /* 108 */
|
|
||||||
0x4024, 0x0006, 0x0890, 0x3004, 0x0024, 0x3000, 0x4024, 0x0006, /* 110 */
|
|
||||||
0x12d0, 0x6182, 0x0024, 0x3000, 0x4024, 0x2800, 0x3e05, 0xf400, /* 118 */
|
|
||||||
0x4050, 0x3009, 0x2000, 0x0006, 0x08d0, 0x0006, 0x0892, 0x3000, /* 120 */
|
|
||||||
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x0030, 0x0250, 0xb882, /* 128 */
|
|
||||||
0x2001, 0x0030, 0x0710, 0x3800, 0x4024, 0x0006, 0x12d0, 0x3000, /* 130 */
|
|
||||||
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x3204, 0x0024, 0x3023, /* 138 */
|
|
||||||
0x0024, 0x30e0, 0xc024, 0x6312, 0x0024, 0x0000, 0x00c3, 0x2800, /* 140 */
|
|
||||||
0x3141, 0x0000, 0x0024, 0x3033, 0x0024, 0x3a04, 0x0024, 0x3000, /* 148 */
|
|
||||||
0x4024, 0x6182, 0x0024, 0x0006, 0x0890, 0x2800, 0x2fd8, 0x0006, /* 150 */
|
|
||||||
0x0301, 0x3a00, 0x4024, 0x0000, 0x00c3, 0x3004, 0x0024, 0x3013, /* 158 */
|
|
||||||
0x0024, 0x3000, 0x4024, 0x0006, 0x12d0, 0x3800, 0x4024, 0x0030, /* 160 */
|
|
||||||
0x0310, 0xf000, 0x0001, 0x6236, 0x0024, 0x001f, 0xffc3, 0x2800, /* 168 */
|
|
||||||
0x3395, 0x0000, 0x0024, 0x0000, 0x0203, 0xa132, 0x0024, 0x001f, /* 170 */
|
|
||||||
0xffc3, 0xb136, 0x0024, 0x6306, 0x0024, 0x0000, 0x0024, 0x2800, /* 178 */
|
|
||||||
0x3611, 0x0000, 0x0024, 0x0020, 0x0003, 0xb132, 0x0024, 0x0000, /* 180 */
|
|
||||||
0x0024, 0x2800, 0x3a85, 0x0000, 0x0024, 0x0000, 0x0081, 0xb212, /* 188 */
|
|
||||||
0x0024, 0x0000, 0x0024, 0x2800, 0x3a05, 0x0000, 0x0024, 0x6892, /* 190 */
|
|
||||||
0x0024, 0xb212, 0x0024, 0x0000, 0x0005, 0x2800, 0x3c55, 0x0030, /* 198 */
|
|
||||||
0x0310, 0x0000, 0x3fc1, 0x3000, 0x8024, 0xb214, 0x0024, 0x003f, /* 1a0 */
|
|
||||||
0xc001, 0xb010, 0x0024, 0xc200, 0x0024, 0x0030, 0x0310, 0x3800, /* 1a8 */
|
|
||||||
0x0024, 0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x5803, 0x2000, /* 1b0 */
|
|
||||||
0x0000, 0x0000, 0x0024, 0x0030, 0x0310, 0x0000, 0x0005, 0x003f, /* 1b8 */
|
|
||||||
0xc001, 0x4088, 0x0002, 0xb214, 0x0024, 0x1fff, 0xfe01, 0xae12, /* 1c0 */
|
|
||||||
0x0024, 0x2800, 0x3a00, 0xc200, 0x0024, 0x2800, 0x28c0, 0x3800, /* 1c8 */
|
|
||||||
0x0024, 0x0007, 0x0001, 0x80fa, 0x0006, 0x00fe, 0x3e12, 0x0024, /* 1d0 */
|
|
||||||
0x3e05, 0xb814, 0x3615, 0x0024, 0x3e00, 0x3841, 0x3e00, 0xb843, /* 1d8 */
|
|
||||||
0x3e01, 0x3845, 0x3e04, 0x3851, 0x0030, 0x10d0, 0x3e04, 0x8024, /* 1e0 */
|
|
||||||
0x3010, 0x0024, 0x3000, 0x8024, 0x0006, 0x1190, 0x3000, 0x4024, /* 1e8 */
|
|
||||||
0x6182, 0x0024, 0x0000, 0x0024, 0x2800, 0x5dd5, 0x0000, 0x0024, /* 1f0 */
|
|
||||||
0x0030, 0x03d0, 0x0000, 0x00c1, 0x3000, 0xc024, 0xb318, 0x0024, /* 1f8 */
|
|
||||||
0x6896, 0x0024, 0x6436, 0x0024, 0x0020, 0x0003, 0x2800, 0x59c5, /* 200 */
|
|
||||||
0x0000, 0x0024, 0x0006, 0x1150, 0x3000, 0x4024, 0x6136, 0x0024, /* 208 */
|
|
||||||
0x0000, 0x0024, 0x2800, 0x4741, 0x0000, 0x0024, 0x0000, 0x0803, /* 210 */
|
|
||||||
0x4132, 0x0024, 0x3800, 0x4024, 0x0006, 0x0190, 0x0006, 0xf011, /* 218 */
|
|
||||||
0x2900, 0xb500, 0x3613, 0x0024, 0x0006, 0xf011, 0x0006, 0x1152, /* 220 */
|
|
||||||
0x0006, 0x0250, 0x4082, 0x0800, 0xfe82, 0x184c, 0x1fff, 0xfc41, /* 228 */
|
|
||||||
0x48ba, 0x0024, 0xae1a, 0x0024, 0x2900, 0xb500, 0x4280, 0x4103, /* 230 */
|
|
||||||
0x0006, 0x1110, 0x4084, 0x0800, 0xfe84, 0x0002, 0x48ba, 0x0024, /* 238 */
|
|
||||||
0xae12, 0x0024, 0xf400, 0x4001, 0x0000, 0x0180, 0x6200, 0x0024, /* 240 */
|
|
||||||
0x0000, 0x0080, 0x2800, 0x5241, 0x4200, 0x0024, 0x3800, 0x0024, /* 248 */
|
|
||||||
0x0006, 0x1090, 0x3004, 0x8024, 0xf400, 0x4491, 0x3113, 0x0024, /* 250 */
|
|
||||||
0x3804, 0x4024, 0x3a00, 0xc024, 0x3004, 0x8024, 0xf400, 0x4491, /* 258 */
|
|
||||||
0x3113, 0x0024, 0x3804, 0x4024, 0x3a00, 0x4024, 0x0006, 0x1081, /* 260 */
|
|
||||||
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x0f00, 0x2800, 0x5248, /* 268 */
|
|
||||||
0x0000, 0x0024, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0080, /* 270 */
|
|
||||||
0x3000, 0x4024, 0x0030, 0x0710, 0xb104, 0x0024, 0x0000, 0x0001, /* 278 */
|
|
||||||
0x3800, 0x4024, 0x0006, 0x08d0, 0x3001, 0x0024, 0x0006, 0x0910, /* 280 */
|
|
||||||
0x3000, 0x4024, 0x6100, 0x0024, 0x6042, 0x0024, 0x0030, 0x06d0, /* 288 */
|
|
||||||
0x2800, 0x5711, 0xb880, 0x0024, 0x2900, 0x21c0, 0x4380, 0x184c, /* 290 */
|
|
||||||
0xb880, 0x0024, 0x3800, 0x0024, 0x36f4, 0x8024, 0x36f4, 0x1811, /* 298 */
|
|
||||||
0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, /* 2a0 */
|
|
||||||
0x36f3, 0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, /* 2a8 */
|
|
||||||
0x0006, 0x1152, 0x0000, 0x0804, 0x3200, 0xc024, 0x6346, 0x0024, /* 2b0 */
|
|
||||||
0x6386, 0x2803, 0x0000, 0x0024, 0x2800, 0x4755, 0x0000, 0x0024, /* 2b8 */
|
|
||||||
0x3800, 0x4024, 0x0030, 0x0690, 0x0000, 0x0081, 0xb882, 0x22c1, /* 2c0 */
|
|
||||||
0x3800, 0x4024, 0x0030, 0x0590, 0x2800, 0x4740, 0x3800, 0x4024, /* 2c8 */
|
|
||||||
0x2800, 0x5700, 0x4190, 0x0024, 0x0007, 0x0001, 0x8179, 0x0006, /* 2d0 */
|
|
||||||
0x00a6, 0x3e12, 0x0024, 0x3e05, 0xb814, 0x3625, 0x0024, 0x3e00, /* 2d8 */
|
|
||||||
0x3841, 0x3e00, 0xb843, 0x3e04, 0x3851, 0x0006, 0x1110, 0x3e04, /* 2e0 */
|
|
||||||
0xb813, 0x3000, 0x0024, 0x6080, 0x0024, 0x0006, 0x11d2, 0x2800, /* 2e8 */
|
|
||||||
0x70c5, 0x0000, 0x0081, 0x6010, 0x984c, 0x3800, 0x0024, 0x0006, /* 2f0 */
|
|
||||||
0x10d0, 0x3200, 0x0024, 0xf100, 0x0011, 0xf100, 0x0024, 0xf102, /* 2f8 */
|
|
||||||
0x0400, 0x0006, 0x1311, 0x2900, 0x0400, 0x3100, 0x8024, 0x0030, /* 300 */
|
|
||||||
0x1293, 0x3413, 0x184c, 0x3c04, 0x4024, 0x3b00, 0x0024, 0x3004, /* 308 */
|
|
||||||
0xc024, 0xf400, 0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3310, /* 310 */
|
|
||||||
0x0024, 0x3a00, 0x0024, 0x0006, 0x1212, 0x3200, 0x0024, 0xf100, /* 318 */
|
|
||||||
0x13d1, 0xf100, 0x0402, 0x2900, 0x0400, 0xf102, 0x0c00, 0x0030, /* 320 */
|
|
||||||
0x12d1, 0x0006, 0x1081, 0x3900, 0x0024, 0x3004, 0xc024, 0xf400, /* 328 */
|
|
||||||
0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3300, 0x0024, 0x3a00, /* 330 */
|
|
||||||
0x0024, 0xf400, 0x4440, 0x6010, 0x0024, 0x1fee, 0xe002, 0x2800, /* 338 */
|
|
||||||
0x6bc8, 0x0006, 0x0f00, 0x3800, 0x0024, 0x0006, 0x0010, 0xb886, /* 340 */
|
|
||||||
0x0040, 0x30f0, 0x4024, 0x6c92, 0x40c3, 0x3810, 0x0024, 0xb182, /* 348 */
|
|
||||||
0x23c1, 0x0006, 0x0950, 0x3000, 0x0024, 0x6090, 0x0024, 0x6cd2, /* 350 */
|
|
||||||
0x2000, 0x0000, 0x0000, 0x2800, 0x70c8, 0x0000, 0x0024, 0x3800, /* 358 */
|
|
||||||
0x0024, 0x0000, 0x0210, 0x3010, 0x0024, 0x30f0, 0x4024, 0x6c92, /* 360 */
|
|
||||||
0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, 0x36f4, 0x9813, 0x36f4, /* 368 */
|
|
||||||
0x1811, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, 0x36f3, /* 370 */
|
|
||||||
0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, 0x0007, /* 378 */
|
|
||||||
0x0001, 0x81cc, 0x0006, 0x00f4, 0x3e00, 0x3841, 0x0000, 0x0201, /* 380 */
|
|
||||||
0x3e00, 0xb843, 0x3e01, 0x3845, 0x3e04, 0x3812, 0x0030, 0x0410, /* 388 */
|
|
||||||
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x08d0, 0x2800, 0x8045, /* 390 */
|
|
||||||
0x0000, 0x0181, 0x6012, 0x0024, 0x0006, 0x1250, 0x2800, 0x7e45, /* 398 */
|
|
||||||
0x0000, 0x05c1, 0x6012, 0x0024, 0x0030, 0x01d0, 0x2800, 0x7c45, /* 3a0 */
|
|
||||||
0x0000, 0x0581, 0x6010, 0x03cc, 0x0000, 0x0024, 0x2800, 0x7a95, /* 3a8 */
|
|
||||||
0x0000, 0x0024, 0x3000, 0x8024, 0x0006, 0x1250, 0x3000, 0x0024, /* 3b0 */
|
|
||||||
0x6092, 0x0024, 0x3800, 0x4024, 0xf400, 0x4010, 0x3800, 0x8024, /* 3b8 */
|
|
||||||
0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, /* 3c0 */
|
|
||||||
0x2000, 0x0000, 0x0000, 0x0024, 0x0030, 0x01d0, 0x3000, 0x0024, /* 3c8 */
|
|
||||||
0x0006, 0x1250, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3d0 */
|
|
||||||
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3d8 */
|
|
||||||
0x6090, 0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3e0 */
|
|
||||||
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3e8 */
|
|
||||||
0x6080, 0x0024, 0x0000, 0x0024, 0x2800, 0x8515, 0x0000, 0x0024, /* 3f0 */
|
|
||||||
0x0006, 0x1350, 0x0000, 0x0082, 0x0030, 0x0352, 0xb886, 0x0040, /* 3f8 */
|
|
||||||
0x30f0, 0x4024, 0x4cd2, 0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, /* 400 */
|
|
||||||
0x3a00, 0x0024, 0x3010, 0x0024, 0x30f0, 0x4024, 0x0030, 0x0390, /* 408 */
|
|
||||||
0x2800, 0x7a80, 0x4180, 0x2001, 0x4090, 0x0024, 0x3800, 0x0024, /* 410 */
|
|
||||||
0x0030, 0x0250, 0x3800, 0x0024, 0x0006, 0x1290, 0x3000, 0x0024, /* 418 */
|
|
||||||
0x6090, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x8024, /* 420 */
|
|
||||||
0x3223, 0x0024, 0x32e0, 0x4024, 0x6100, 0x0024, 0x0000, 0x0024, /* 428 */
|
|
||||||
0x2800, 0x8c81, 0x0000, 0x0024, 0x3233, 0x0024, 0x3804, 0x8024, /* 430 */
|
|
||||||
0x3200, 0x0024, 0x6080, 0x0024, 0x0006, 0x0300, 0x2800, 0x8b18, /* 438 */
|
|
||||||
0x0000, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x0024, /* 440 */
|
|
||||||
0x3013, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, 0x3800, 0x0024, /* 448 */
|
|
||||||
0x0006, 0x0850, 0x3004, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, /* 450 */
|
|
||||||
0x6080, 0x0024, 0x3000, 0x0024, 0x2800, 0x9115, 0xf400, 0x4010, /* 458 */
|
|
||||||
0x3000, 0x0024, 0x0000, 0x0201, 0x0000, 0x0005, 0x0030, 0x0210, /* 460 */
|
|
||||||
0xa014, 0x4004, 0x1fff, 0xfe01, 0xae12, 0x0024, 0xc200, 0x0024, /* 468 */
|
|
||||||
0x2800, 0x8180, 0x3800, 0x0024, 0x2800, 0x8ec0, 0x3009, 0x0000, /* 470 */
|
|
||||||
0x0007, 0x0001, 0x8246, 0x0006, 0x0104, 0x0030, 0x1092, 0x0007, /* 478 */
|
|
||||||
0x9250, 0x003f, 0xfc42, 0xb880, 0x184c, 0x3e12, 0x0024, 0x3800, /* 480 */
|
|
||||||
0x0024, 0x0030, 0x0290, 0x38f0, 0x0024, 0x3800, 0x0024, 0x0030, /* 488 */
|
|
||||||
0x0050, 0x3000, 0x4024, 0xb122, 0x0024, 0x6894, 0x2001, 0x0000, /* 490 */
|
|
||||||
0x0141, 0x3a70, 0x4024, 0x0004, 0x1fc1, 0x3a00, 0x4024, 0x0030, /* 498 */
|
|
||||||
0x00d2, 0x0030, 0x0001, 0x3a00, 0x4024, 0x0030, 0x0552, 0x3a10, /* 4a0 */
|
|
||||||
0x0024, 0x3a00, 0x0024, 0x3000, 0x4024, 0xc122, 0x0024, 0x3800, /* 4a8 */
|
|
||||||
0x4024, 0x0030, 0x05d0, 0x0000, 0x03c1, 0x3820, 0x4024, 0x3800, /* 4b0 */
|
|
||||||
0x0024, 0x0000, 0x0310, 0x3010, 0x0024, 0x30f0, 0x4024, 0xf2c2, /* 4b8 */
|
|
||||||
0x0024, 0x3810, 0x0024, 0x0000, 0x3fc0, 0x38f0, 0x4024, 0x0030, /* 4c0 */
|
|
||||||
0x02d0, 0x3000, 0x4024, 0x2912, 0x1400, 0xb104, 0x0024, 0x0006, /* 4c8 */
|
|
||||||
0x1312, 0x6802, 0x0024, 0x000d, 0xac00, 0x6012, 0x2801, 0x0000, /* 4d0 */
|
|
||||||
0x0024, 0x2800, 0x9dc1, 0x0000, 0x0024, 0x3a00, 0x0024, 0x2909, /* 4d8 */
|
|
||||||
0x1b40, 0x3613, 0x0024, 0x0000, 0x0084, 0x0000, 0x1905, 0x2908, /* 4e0 */
|
|
||||||
0xbe80, 0x3613, 0x0024, 0x0000, 0x0000, 0x0006, 0x0302, 0x4002, /* 4e8 */
|
|
||||||
0x0024, 0x4012, 0x0024, 0x4212, 0x0024, 0xf400, 0x4050, 0x3000, /* 4f0 */
|
|
||||||
0x4024, 0x6182, 0x0024, 0x0006, 0x0350, 0x2800, 0xa6c8, 0x0000, /* 4f8 */
|
|
||||||
0x0024, 0x4002, 0x0024, 0x4014, 0x0024, 0x0006, 0x0301, 0x4124, /* 500 */
|
|
||||||
0x0024, 0x0000, 0x0081, 0x4212, 0x0024, 0x4002, 0x4050, 0x4014, /* 508 */
|
|
||||||
0x0003, 0x0006, 0x0301, 0x4122, 0x0024, 0x6192, 0x0024, 0x6090, /* 510 */
|
|
||||||
0x4050, 0x3000, 0x4024, 0x0006, 0x0910, 0x6312, 0x0024, 0x6194, /* 518 */
|
|
||||||
0x0001, 0x4122, 0x0024, 0x2800, 0x9f80, 0x3800, 0x4024, 0x0006, /* 520 */
|
|
||||||
0x12d2, 0x0006, 0x0991, 0x3000, 0x0024, 0x0006, 0x1290, 0x3a00, /* 528 */
|
|
||||||
0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x2900, 0xb200, 0x0000, /* 530 */
|
|
||||||
0x0580, 0x0030, 0x0210, 0x0014, 0x9240, 0x003f, 0xf502, 0x003f, /* 538 */
|
|
||||||
0xffc3, 0x3800, 0x0024, 0x0000, 0x0580, 0x0006, 0x1350, 0x3200, /* 540 */
|
|
||||||
0x4024, 0x4102, 0x0024, 0x3a00, 0x4024, 0x3810, 0x8024, 0x38f0, /* 548 */
|
|
||||||
0xc024, 0x0006, 0x08d0, 0x3800, 0x0024, 0x0030, 0x0690, 0x0000, /* 550 */
|
|
||||||
0x8280, 0xb880, 0x2080, 0x3800, 0x0024, 0x6890, 0x2000, 0x0030, /* 558 */
|
|
||||||
0x0490, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0100, 0x3000, /* 560 */
|
|
||||||
0x584c, 0xb100, 0x0024, 0x0000, 0x0024, 0x2800, 0xb185, 0x0000, /* 568 */
|
|
||||||
0x0024, 0x003f, 0xfec1, 0x3000, 0x1bcc, 0xb010, 0x0024, 0x2908, /* 570 */
|
|
||||||
0x0b80, 0x3800, 0x0024, 0x3613, 0x0024, 0x2910, 0x0180, 0x0000, /* 578 */
|
|
||||||
0xae48, 0x0007, 0x0001, 0x1806, 0x0006, 0x8007, 0x0000, 0x0006, /* 580 */
|
|
||||||
0x002f, 0x0010, 0x17ff, 0x0000, 0x1a00, 0x1dff, 0x0000, 0x1f00, /* 588 */
|
|
||||||
0x3fff, 0x0001, 0x0000, 0x17ff, 0x0001, 0x1c00, 0x3fff, 0x0001, /* 590 */
|
|
||||||
0xe000, 0xfffd, 0xffff, 0x0000, 0x0000, 0x180c, 0x180c, 0x0000, /* 598 */
|
|
||||||
0x0000, 0x0000, 0x4952, 0x4646, 0xffff, 0xffff, 0x4157, 0x4556, /* 5a0 */
|
|
||||||
0x6d66, 0x2074, 0x0010, 0x0000, 0x0001, 0x0001, 0xbb80, 0x0000, /* 5a8 */
|
|
||||||
0x7700, 0x0001, 0x0002, 0x0010, 0x6164, 0x6174, 0xffff, 0xffff, /* 5b0 */
|
|
||||||
0x0006, 0x8006, 0x0000, 0x0006, 0x0005, 0x183c, 0x183c, 0x0000, /* 5b8 */
|
|
||||||
0x0020, 0x0040, 0x0006, 0x8003, 0x0000, 0x0007, 0x0001, 0x5bc0, /* 5c0 */
|
|
||||||
0x0006, 0x0009, 0x801c, 0x7fe4, 0x8039, 0x804e, 0x7fb2, 0x809d, /* 5c8 */
|
|
||||||
0x809c, 0x7f64, 0x8139, 0x0007, 0x0001, 0x82c8, 0x0006, 0x0018, /* 5d0 */
|
|
||||||
0x4080, 0x184c, 0x3e13, 0x780f, 0x2800, 0xb405, 0x4090, 0x380e, /* 5d8 */
|
|
||||||
0x2400, 0xb3c0, 0xf400, 0x4417, 0x3110, 0x0024, 0x3f10, 0x0024, /* 5e0 */
|
|
||||||
0x36f3, 0x8024, 0x36f3, 0x580f, 0x2000, 0x0000, 0x0000, 0x0024, /* 5e8 */
|
|
||||||
0x0007, 0x0001, 0x82d4, 0x0006, 0x002a, 0x3e11, 0xb807, 0x3009, /* 5f0 */
|
|
||||||
0x384a, 0x3e11, 0x3805, 0x3e10, 0xb803, 0x3e00, 0x4442, 0x0001, /* 5f8 */
|
|
||||||
0x800a, 0xbf8e, 0x8443, 0xfe06, 0x0045, 0x3011, 0x0401, 0x545e, /* 600 */
|
|
||||||
0x0385, 0x525e, 0x2040, 0x72ce, 0x1bc1, 0x48ba, 0x9803, 0x4588, /* 608 */
|
|
||||||
0x4885, 0x6fee, 0x1bc2, 0x4ffe, 0x9805, 0xf6fe, 0x1bc4, 0xf7f0, /* 610 */
|
|
||||||
0x2046, 0x3801, 0xdbca, 0x2000, 0x0000, 0x36f1, 0x9807,
|
|
||||||
};
|
|
||||||
const uint16_t patch_size = 1567;
|
|
||||||
DEBUG("Patching...\n");
|
|
||||||
|
|
||||||
_spi->select_vs1053_xcs();
|
|
||||||
SPI.beginTransaction(*_spi_settings);
|
|
||||||
|
|
||||||
for (int i=0; i<patch_size; i++) {
|
|
||||||
DEBUG(" %d\n", i);
|
|
||||||
unsigned short addr, n, val;
|
|
||||||
addr = patch_data[i++];
|
|
||||||
n = patch_data[i++];
|
|
||||||
SPI.transfer(CMD_WRITE);
|
|
||||||
SPI.transfer(addr & 0XFF);
|
|
||||||
if (n & 0x8000U) { /* RLE run, replicate n samples */
|
|
||||||
n &= 0x7FFF;
|
|
||||||
val = patch_data[i++];
|
|
||||||
while (n--) {
|
|
||||||
SPI.transfer(val >> 8);
|
|
||||||
SPI.transfer(val & 0xFF);
|
|
||||||
_wait();
|
|
||||||
}
|
|
||||||
} else { /* Copy run, copy n samples */
|
|
||||||
while (n--) {
|
|
||||||
val = patch_data[i++];
|
|
||||||
SPI.transfer(val >> 8);
|
|
||||||
SPI.transfer(val & 0xFF);
|
|
||||||
_wait();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SPI.endTransaction();
|
|
||||||
_spi->select_vs1053_xcs(false);
|
|
||||||
|
|
||||||
DEBUG("Patch sent.\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_write_data(uint8_t* buffer) {
|
|
||||||
_spi->select_vs1053_xdcs();
|
|
||||||
SPI.beginTransaction(*_spi_settings);
|
|
||||||
for (uint i=0; i<sizeof(_buffer); i++) {
|
|
||||||
SPI.transfer(_buffer[i]);
|
|
||||||
}
|
|
||||||
SPI.endTransaction();
|
|
||||||
_spi->select_vs1053_xdcs(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t Player::_read_wram(uint16_t address) {
|
|
||||||
DEBUG("Reading WRAM address 0x%04X...\n", address);
|
|
||||||
_write_control_register(SCI_WRAMADDR, address);
|
|
||||||
uint16_t r1 = _read_control_register(SCI_WRAM);
|
|
||||||
_write_control_register(SCI_WRAMADDR, address);
|
|
||||||
uint16_t r2 = _read_control_register(SCI_WRAM);
|
|
||||||
if (r1 == r2) return r1;
|
|
||||||
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
|
|
||||||
_write_control_register(SCI_WRAMADDR, address);
|
|
||||||
r1 = _read_control_register(SCI_WRAM);
|
|
||||||
if (r1 == r2) return r1;
|
|
||||||
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
|
|
||||||
_write_control_register(SCI_WRAMADDR, address);
|
|
||||||
r2 = _read_control_register(SCI_WRAM);
|
|
||||||
if (r1 == r2) return r1;
|
|
||||||
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
|
|
||||||
DEBUG("Returning last value (0x%04X)...\n", r2);
|
|
||||||
return r2;
|
|
||||||
}
|
|
||||||
|
|
||||||
int8_t Player::_get_endbyte() {
|
|
||||||
int8_t endbyte = _read_wram(ADDR_ENDBYTE) & 0xFF;
|
|
||||||
DEBUG("Endbyte is 0x%02X.\n", endbyte);
|
|
||||||
return endbyte;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void Player::set_volume(uint8_t vol, bool save) {
|
|
||||||
if (save) {
|
|
||||||
_volume = vol;
|
|
||||||
}
|
|
||||||
INFO("Setting volume to %d\n", vol);
|
|
||||||
vol = 0xFF - vol;
|
|
||||||
uint16_t value = (vol<<8)|vol;
|
|
||||||
DEBUG("Setting volume register to 0x%04X\n", value);
|
|
||||||
_write_control_register(SCI_VOL, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::vol_up() {
|
|
||||||
if (!is_playing()) return;
|
|
||||||
uint16_t vol = _volume + VOLUME_STEP;
|
|
||||||
if (vol > VOLUME_MAX) vol=VOLUME_MAX;
|
|
||||||
set_volume(vol);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::vol_down() {
|
|
||||||
if (!is_playing()) return;
|
|
||||||
int16_t vol = _volume - VOLUME_STEP;
|
|
||||||
if (vol < VOLUME_MIN) vol=VOLUME_MIN;
|
|
||||||
set_volume(vol);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_mute() {
|
|
||||||
INFO("Muting.\n");
|
|
||||||
_speaker_off();
|
|
||||||
set_volume(1, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_unmute() {
|
|
||||||
INFO("Unmuting.\n");
|
|
||||||
set_volume(_volume, false);
|
|
||||||
_speaker_on();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::track_next() {
|
|
||||||
if (_state != playing) return;
|
|
||||||
if (!_current_playlist->has_track_next()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stop();
|
|
||||||
_current_playlist->track_next();
|
|
||||||
play();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::track_prev() {
|
|
||||||
if (_state != playing) return;
|
|
||||||
if (_current_play_position > 100000) {
|
|
||||||
stop();
|
|
||||||
_current_playlist->track_restart();
|
|
||||||
play();
|
|
||||||
} else {
|
|
||||||
if (!_current_playlist->has_track_prev()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stop();
|
|
||||||
_current_playlist->track_prev();
|
|
||||||
play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::set_track(uint8_t id) {
|
|
||||||
stop();
|
|
||||||
_current_playlist->set_track(id);
|
|
||||||
play();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Player::is_playing() {
|
|
||||||
return _state == playing;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Player::play(Playlist* p) {
|
|
||||||
_current_playlist = p;
|
|
||||||
return play();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Player::play() {
|
|
||||||
if (_state == sleeping || _state == recording) _wakeup();
|
|
||||||
if (_state != idle) return false;
|
|
||||||
if (_current_playlist == NULL) return false;
|
|
||||||
if (_current_playlist->get_file_count()==0) return false;
|
|
||||||
_speaker_on();
|
|
||||||
_current_playlist->start();
|
|
||||||
String file;
|
|
||||||
if (!_current_playlist->get_current_file(&file)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
uint32_t position = _current_playlist->get_position();
|
|
||||||
_state = playing;
|
|
||||||
_play_file(file, position);
|
|
||||||
_controller->send_player_status();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_play_file(String file, uint32_t file_offset) {
|
|
||||||
INFO("play_file('%s', %d)\n", file.c_str(), file_offset);
|
|
||||||
_spi->select_sd();
|
|
||||||
if (file.startsWith("/")) {
|
|
||||||
_file = new SDDataSource(file);
|
|
||||||
} else if (file.startsWith("http")) {
|
|
||||||
_file = new HTTPSDataSource(file);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_file_size = _file->size();
|
|
||||||
_spi->select_sd(false);
|
|
||||||
if (!_file || !_file->usable()) {
|
|
||||||
DEBUG("Could not open file %s", file.c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DEBUG("Resetting SCI_DECODE_TIME...\n");
|
|
||||||
_write_control_register(SCI_DECODE_TIME, 0);
|
|
||||||
DEBUG("Resetting SS_DO_NOT_JUMP...\n");
|
|
||||||
_write_control_register(SCI_STATUS, _read_control_register(SCI_STATUS) & ~SS_DO_NOT_JUMP);
|
|
||||||
delay(100);
|
|
||||||
|
|
||||||
_spi->select_sd();
|
|
||||||
if (file_offset == 0) {
|
|
||||||
_file->skip_id3_tag();
|
|
||||||
}
|
|
||||||
_refills = 0;
|
|
||||||
_current_play_position = _file->position();
|
|
||||||
_spi->select_sd(false);
|
|
||||||
_skip_to = file_offset;
|
|
||||||
if (_skip_to>0) _mute();
|
|
||||||
else _speaker_on();
|
|
||||||
INFO("Now playing.\n");
|
|
||||||
_controller->send_player_status();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_flush(uint count, int8_t byte) {
|
|
||||||
_spi->select_vs1053_xdcs();
|
|
||||||
SPI.beginTransaction(*_spi_settings);
|
|
||||||
for(uint i=0; i<count; i++) {
|
|
||||||
_wait();
|
|
||||||
SPI.transfer(byte);
|
|
||||||
}
|
|
||||||
SPI.endTransaction();
|
|
||||||
_spi->select_vs1053_xdcs(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_finish_playing() {
|
|
||||||
uint8_t endbyte = _get_endbyte();
|
|
||||||
_flush(2052, endbyte);
|
|
||||||
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
|
|
||||||
for (int i=0; i<64; i++) {
|
|
||||||
_flush(32, endbyte);
|
|
||||||
uint16_t mode = _read_control_register(SCI_MODE);
|
|
||||||
if ((mode & SM_CANCEL) == 0) return;
|
|
||||||
}
|
|
||||||
// If we reached this, the Chip didn't stop. That should not happen.
|
|
||||||
// (That's written in the manual.)
|
|
||||||
// Reset the chip.
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::stop(bool turn_speaker_off) {
|
|
||||||
if (_state != playing) return;
|
|
||||||
INFO("Stopping...\n");
|
|
||||||
_current_playlist->set_position(_current_play_position);
|
|
||||||
_controller->pm->persist(_current_playlist);
|
|
||||||
|
|
||||||
_state = stopping;
|
|
||||||
_stop_delay = 0;
|
|
||||||
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
|
|
||||||
uint8_t endbyte = _get_endbyte();
|
|
||||||
while (true) {
|
|
||||||
_refill();
|
|
||||||
uint16_t mode = _read_control_register(SCI_MODE);
|
|
||||||
if ((mode & SM_CANCEL) == 0) {
|
|
||||||
_flush(2052, endbyte);
|
|
||||||
_finish_stopping(turn_speaker_off);
|
|
||||||
break;
|
|
||||||
} else if (_stop_delay > 2048) {
|
|
||||||
init();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_stop_delay++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_finish_stopping(bool turn_speaker_off) {
|
|
||||||
if (turn_speaker_off) _speaker_off();
|
|
||||||
_state = idle;
|
|
||||||
_stopped_at = millis();
|
|
||||||
if (_file) {
|
|
||||||
_file->close();
|
|
||||||
delete _file;
|
|
||||||
}
|
|
||||||
_current_play_position = 0;
|
|
||||||
_file_size = 0;
|
|
||||||
INFO("Stopped.\n");
|
|
||||||
_controller->send_player_status();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Player::_refill() {
|
|
||||||
_spi->select_sd();
|
|
||||||
_refills++;
|
|
||||||
if (_refills % 1000 == 0) DEBUG(".");
|
|
||||||
uint8_t result = _file->read(_buffer, sizeof(_buffer));
|
|
||||||
_spi->select_sd(false);
|
|
||||||
if (result == 0) {
|
|
||||||
// File is over.
|
|
||||||
DEBUG("EOF reached.\n");
|
|
||||||
_skip_to = 0;
|
|
||||||
_finish_playing();
|
|
||||||
_finish_stopping(false);
|
|
||||||
if (_current_playlist->has_track_next()) {
|
|
||||||
_current_playlist->track_next();
|
|
||||||
play();
|
|
||||||
} else {
|
|
||||||
_current_playlist->reset();
|
|
||||||
_controller->send_player_status();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_current_play_position+=result;
|
|
||||||
_write_data(_buffer);
|
|
||||||
|
|
||||||
if (_skip_to > 0) {
|
|
||||||
if (_skip_to > _file->position()) {
|
|
||||||
uint16_t status = _read_control_register(SCI_STATUS);
|
|
||||||
if ((status & SS_DO_NOT_JUMP) == 0) {
|
|
||||||
DEBUG("Skipping to %d.\n", _skip_to);
|
|
||||||
_flush(2048, _get_endbyte());
|
|
||||||
_spi->select_sd();
|
|
||||||
_file->seek(_skip_to);
|
|
||||||
_spi->select_sd(false);
|
|
||||||
_skip_to = 0;
|
|
||||||
_unmute();
|
|
||||||
_controller->send_position();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_skip_to = 0;
|
|
||||||
_unmute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Player::_refill_needed() {
|
|
||||||
return _state==playing || _state==stopping;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Player::loop() {
|
|
||||||
if (PIN_VS1053_DREQ() && _refill_needed()) {
|
|
||||||
_refill();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_state == recording) {
|
|
||||||
DEBUG("r");
|
|
||||||
uint16_t samples_available = _read_control_register(SCI_HDAT1, false);
|
|
||||||
uint16_t vu_value = _read_control_register(SCI_AICTRL0, false);
|
|
||||||
DEBUG("Samples available: %4d, VU meter: 0x%04X\n", samples_available, vu_value);
|
|
||||||
if (samples_available >= 500) {
|
|
||||||
unsigned long sum = 0;
|
|
||||||
for (int i=0; i<500; i++) {
|
|
||||||
uint16_t sample = _read_control_register(SCI_HDAT0, false);
|
|
||||||
sum += sample * sample;
|
|
||||||
}
|
|
||||||
double result = sqrt(sum / 500);
|
|
||||||
DEBUG("Loudness: %f", result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_state == idle && _stopped_at < millis() - VS1053_SLEEP_DELAY) {
|
|
||||||
_sleep();
|
|
||||||
//_record();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String Player::json() {
|
|
||||||
DynamicJsonDocument json(10240);
|
|
||||||
json["_type"] = "player";
|
|
||||||
json["playing"] = is_playing();
|
|
||||||
if (_current_playlist) {
|
|
||||||
JsonObject playlist = json.createNestedObject("playlist");
|
|
||||||
_current_playlist->json(playlist);
|
|
||||||
} else {
|
|
||||||
json["playlist"] = nullptr;
|
|
||||||
}
|
|
||||||
JsonObject volume = json.createNestedObject("volume");
|
|
||||||
volume["current"] = _volume;
|
|
||||||
volume["min"] = VOLUME_MIN;
|
|
||||||
volume["max"] = VOLUME_MAX;
|
|
||||||
volume["step"] = VOLUME_STEP;
|
|
||||||
return json.as<String>();
|
|
||||||
}
|
|
||||||
|
|
||||||
String Player::position_json() {
|
|
||||||
if (!is_playing()) return "null";
|
|
||||||
DynamicJsonDocument json(200);
|
|
||||||
json["_type"] = "position";
|
|
||||||
json["position"] = _current_play_position;
|
|
||||||
json["file_size"] = _file_size;
|
|
||||||
return json.as<String>();
|
|
||||||
}
|
|
424
src/playlist.cpp
424
src/playlist.cpp
@ -1,398 +1,82 @@
|
|||||||
#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(String path) {
|
Playlist::Playlist() {}
|
||||||
_path = path;
|
|
||||||
if (path.startsWith("/")) {
|
Playlist::Playlist(String id, PersistedPlaylist* p) {
|
||||||
persistence = PERSIST_TEMPORARY;
|
rfid_id = id;
|
||||||
_add_path(path);
|
pp = p;
|
||||||
} else if (path.startsWith("http")) {
|
|
||||||
_examine_http_url(path);
|
|
||||||
}
|
|
||||||
if (_title.length()==0) _title=path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::_add_path(String path) {
|
String Playlist::get_rfid_id() {
|
||||||
SPIMaster::select_sd();
|
return rfid_id;
|
||||||
TRACE("Examining folder %s...\n", path.c_str());
|
|
||||||
if (!path.startsWith("/")) path = String("/") + path;
|
|
||||||
if (!SD.exists(path)) {
|
|
||||||
DEBUG("Could not open path '%s'.\n", path.c_str());
|
|
||||||
SPIMaster::select_sd(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_title = path.substring(1);
|
|
||||||
int idx = _title.indexOf('/');
|
|
||||||
if (idx>0) {
|
|
||||||
_title.remove(idx);
|
|
||||||
}
|
|
||||||
File dir = SD.open(path);
|
|
||||||
File entry;
|
|
||||||
while (entry = dir.openNextFile()) {
|
|
||||||
String filename = entry.name();
|
|
||||||
filename = filename.substring(path.length() + 1);
|
|
||||||
String ext = filename.substring(filename.length() - 4);
|
|
||||||
if (!entry.isDirectory() &&
|
|
||||||
!filename.startsWith(".") &&
|
|
||||||
( ext.equals(".mp3") ||
|
|
||||||
ext.equals(".ogg") ||
|
|
||||||
ext.equals(".wma") ||
|
|
||||||
ext.equals(".mp4") ||
|
|
||||||
ext.equals(".mpa"))) {
|
|
||||||
TRACE(" Adding entry %s\n", entry.name());
|
|
||||||
String title = filename.substring(0, filename.length() - 4);
|
|
||||||
_files.push_back({.filename=entry.name(), .title=title, .id=String(_files.size())});
|
|
||||||
bool non_ascii_chars = false;
|
|
||||||
for(int i=0; i<filename.length(); i++) {
|
|
||||||
char c = filename.charAt(i);
|
|
||||||
if (c < 0x20 || c >= 0x7F) {
|
|
||||||
non_ascii_chars = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (non_ascii_chars) {
|
|
||||||
ERROR("WARNING: File '%s' contains non-ascii chars!\n", filename.c_str());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TRACE(" Ignoring entry %s\n", filename.c_str());
|
|
||||||
}
|
|
||||||
entry.close();
|
|
||||||
}
|
|
||||||
dir.close();
|
|
||||||
SPIMaster::select_sd(false);
|
|
||||||
std::sort(_files.begin(), _files.end());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::_examine_http_url(String url) {
|
void Playlist::add_file(String filename) {
|
||||||
HTTPClientWrapper* http = new HTTPClientWrapper();
|
files.push_back(filename);
|
||||||
if (!http->get(url)) {
|
|
||||||
DEBUG("Could not GET %s.\n", url.c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String ct = http->getContentType();
|
|
||||||
DEBUG("Content-Type is %s.\n", ct.c_str());
|
|
||||||
if (ct.startsWith("audio/x-mpegurl")) {
|
|
||||||
_parse_m3u(http);
|
|
||||||
} else if (ct.startsWith("audio/")) {
|
|
||||||
persistence = PERSIST_NONE;
|
|
||||||
_files.push_back({.filename=url, .title=url, .id="none"});
|
|
||||||
} else if (ct.startsWith("application/rss+xml")) {
|
|
||||||
persistence = PERSIST_PERMANENTLY;
|
|
||||||
_parse_rss(http);
|
|
||||||
} else if (ct.startsWith("application/pls+xml")) {
|
|
||||||
persistence = PERSIST_PERMANENTLY;
|
|
||||||
_parse_pls(http);
|
|
||||||
} else {
|
|
||||||
ERROR("Unknown content type %s.\n", ct.c_str());
|
|
||||||
}
|
|
||||||
http->close();
|
|
||||||
delete http;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<PlaylistEntry>* xml_files_ptr = NULL;
|
void Playlist::sort() {
|
||||||
String xml_last_tag = "";
|
std::sort(files.begin(), files.end());
|
||||||
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) {
|
void Playlist::set_current_position(uint8_t file, uint32_t bytes) {
|
||||||
String tag(tagName);
|
log_d("Setting position: File %d, bytes %d.", file, bytes);
|
||||||
if (status & STATUS_START_TAG) xml_last_tag = tag;
|
current_file = file;
|
||||||
|
current_time = bytes;
|
||||||
|
save_current_position();
|
||||||
|
}
|
||||||
|
|
||||||
if (trace_enabled) {
|
void Playlist::save_current_position(uint32_t position) {
|
||||||
if (status & STATUS_START_TAG) {
|
if (position==0) {
|
||||||
TRACE("Start of tag: %s\n", tagName);
|
position = current_time;
|
||||||
} else if (status & STATUS_END_TAG) {
|
|
||||||
TRACE("End of tag: %s\n", tagName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
log_d("Saving current position: File %d, bytes %d.", current_file, position);
|
||||||
if (tag.equals("/rss/channel/title") && (status & STATUS_TAG_TEXT)) {
|
if (pp != NULL) {
|
||||||
xml_album_title = data;
|
pp->file = current_file;
|
||||||
} else if (tag.endsWith("/title") && (status & STATUS_TAG_TEXT)) {
|
pp->position = position;
|
||||||
xml_title = String(data);
|
|
||||||
} else if (tag.endsWith("/guid") && (status & STATUS_TAG_TEXT)) {
|
|
||||||
xml_guid = data;
|
|
||||||
//} else if (xml_last_tag.endsWith("/item/enclosure") && (status & STATUS_ATTR_TEXT)) {
|
|
||||||
// DEBUG("tag: %s, data: %s\n", tag.c_str(), data);
|
|
||||||
} else if (xml_last_tag.endsWith("/enclosure") && tag.equals("type") && (status & STATUS_ATTR_TEXT) && String(data).indexOf("audio/")>=0) {
|
|
||||||
DEBUG("enclosure is audio\n");
|
|
||||||
xml_enclosure_is_audio = true;
|
|
||||||
} else if (xml_last_tag.endsWith("/enclosure") && tag.equals("url") && (status & STATUS_ATTR_TEXT)) {
|
|
||||||
DEBUG("found url\n");
|
|
||||||
xml_enclosure_url = String(data);
|
|
||||||
} else if (tag.endsWith("/enclosure") && (status & STATUS_END_TAG)) {
|
|
||||||
DEBUG("end of enclosure. xml_enclosure_is_audio: %d, xml_enclosure_url: %s\n", xml_enclosure_is_audio, xml_enclosure_url.c_str());
|
|
||||||
if (xml_enclosure_is_audio && xml_enclosure_url.length()>0) {
|
|
||||||
xml_url = xml_enclosure_url;
|
|
||||||
}
|
|
||||||
xml_enclosure_is_audio = false;
|
|
||||||
xml_enclosure_url = "";
|
|
||||||
} else if (tag.endsWith("/item") && (status & STATUS_END_TAG || status & STATUS_START_TAG)) {
|
|
||||||
if (xml_title.length()>0 && xml_url.length()>0) {
|
|
||||||
if (xml_files_ptr->size() > 20) return;
|
|
||||||
DEBUG("Adding playlist entry: '%s' => '%s'\n", xml_title.c_str(), xml_url.c_str());
|
|
||||||
xml_files_ptr->insert(xml_files_ptr->begin(), {.filename=xml_url, .title=xml_title, .id=xml_guid});
|
|
||||||
}
|
|
||||||
xml_title = "";
|
|
||||||
xml_url = "";
|
|
||||||
xml_guid = "";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::_parse_rss(HTTPClientWrapper* http) {
|
String Playlist::get_current_file_name() {
|
||||||
DEBUG("RSS parser running.\n");
|
if (current_file >= files.size()) {
|
||||||
// http is already initialized
|
Serial.printf("Requested a file number %d, which is not available in this playlist. Starting over.\n", current_file);
|
||||||
int i;
|
set_current_position(0);
|
||||||
|
|
||||||
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;
|
return files[current_file];
|
||||||
xml_files_ptr = NULL;
|
|
||||||
if (xml_album_title.length()>0) {
|
|
||||||
_title = xml_album_title;
|
|
||||||
}
|
|
||||||
xml_album_title = "";
|
|
||||||
delete buffer;
|
|
||||||
// don't close http at the end
|
|
||||||
DEBUG("RSS parser finished.\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::_parse_m3u(HTTPClientWrapper* http) {
|
bool Playlist::next_track() {
|
||||||
// http is already initialized
|
if (files.size() <= current_file + 1) {
|
||||||
String line = "";
|
Serial.println("next_track does not exist. Resetting playlist and returning false.");
|
||||||
String title = "";
|
set_current_position(0, 0);
|
||||||
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;
|
return false;
|
||||||
|
}
|
||||||
|
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 {
|
} else {
|
||||||
dst->concat(_files[_current_track].filename);
|
set_current_position(current_file - 1, 0);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
return files.size()>0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t Playlist::get_position() {
|
void Playlist::restart() {
|
||||||
return _position;
|
current_time = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::set_position(uint32_t p) {
|
void Playlist::set_current_time(uint32_t pos) {
|
||||||
_position = p;
|
set_current_position(current_file, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Playlist::is_fresh() {
|
uint32_t Playlist::get_current_time() {
|
||||||
return !_shuffled && !_started && _position==0 && _current_track==0;
|
return current_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Playlist::dump() {
|
void Playlist::shuffle() {
|
||||||
for (int i=0; i<_files.size(); i++) {
|
std::random_shuffle(files.begin(), files.end());
|
||||||
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();
|
|
||||||
}
|
}
|
@ -1,32 +1,17 @@
|
|||||||
#include "playlist_manager.h"
|
#include "playlist_manager.h"
|
||||||
#include <SD.h>
|
|
||||||
#include "spi_master.h"
|
#include "spi_master.h"
|
||||||
#include <ArduinoJson.h>
|
#include <SD.h>
|
||||||
|
|
||||||
PlaylistManager::PlaylistManager() {
|
PlaylistManager::PlaylistManager() {
|
||||||
scan_files();
|
SPIMaster::enable_sd();
|
||||||
}
|
current_rfid_tag_id = String("");
|
||||||
|
|
||||||
void PlaylistManager::scan_files() {
|
if (!SD.exists("/_mapping.txt")) {
|
||||||
SPIMaster::select_sd();
|
Serial.println("WARNING: /_mapping.txt not found!");
|
||||||
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 {
|
} else {
|
||||||
|
map.clear();
|
||||||
File f = SD.open("/_mapping.txt");
|
File f = SD.open("/_mapping.txt");
|
||||||
DEBUG("Reading /_mapping.txt...\n");
|
Serial.println(" Reading /_mapping.txt...");
|
||||||
while (f.available()) {
|
while (f.available()) {
|
||||||
char buffer[512];
|
char buffer[512];
|
||||||
size_t pos = f.readBytesUntil('\n', buffer, 511);
|
size_t pos = f.readBytesUntil('\n', buffer, 511);
|
||||||
@ -37,203 +22,64 @@ void PlaylistManager::scan_files() {
|
|||||||
if (eq>0 && eq<data.length()-1) {
|
if (eq>0 && eq<data.length()-1) {
|
||||||
String rfid_id = data.substring(0, eq);
|
String rfid_id = data.substring(0, eq);
|
||||||
String folder = data.substring(eq + 1);
|
String folder = data.substring(eq + 1);
|
||||||
TRACE(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
|
Serial.printf(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
|
||||||
_map[rfid_id] = folder;
|
map[rfid_id] = PersistedPlaylist(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();
|
f.close();
|
||||||
}
|
}
|
||||||
root.close();
|
|
||||||
|
|
||||||
_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();
|
|
||||||
filename = filename.substring(folder.length() + 1);
|
|
||||||
if (!filename.startsWith(".") && filename.endsWith(".mp3")) {
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
entry.close();
|
|
||||||
if (found) break;
|
|
||||||
}
|
|
||||||
if (found) {
|
|
||||||
_unmapped_folders.push_back(folder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SPIMaster::select_sd(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaylistManager::_check_for_special_chars(String s) {
|
Playlist PlaylistManager::get_playlist(String rfid_id) {
|
||||||
for(int i=0; i<s.length(); i++) {
|
if (rfid_id.equals(current_rfid_tag_id)) {
|
||||||
char c = s.charAt(i);
|
return current_playlist;
|
||||||
if (c < 0x20 || c >= 0x7F) {
|
|
||||||
ERROR("WARNING: Folder / file '%s' contains non-ascii chars!\n", s.c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Playlist* PlaylistManager::get_playlist_for_id(String id) {
|
|
||||||
if (!_map.count(id)) return NULL;
|
|
||||||
String folder = _map[id];
|
|
||||||
return get_playlist_for_folder(folder);
|
|
||||||
}
|
|
||||||
|
|
||||||
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
|
|
||||||
Playlist* p;
|
|
||||||
if (!_playlists.count(folder)) {
|
|
||||||
p = new Playlist(folder);
|
|
||||||
_playlists[folder] = p;
|
|
||||||
if (p->persistence == PERSIST_PERMANENTLY) {
|
|
||||||
String search = folder;
|
|
||||||
search += "=";
|
|
||||||
SPIMaster::select_sd();
|
|
||||||
if (SD.exists("/_positions.txt")) {
|
|
||||||
File f = SD.open("/_positions.txt", "r");
|
|
||||||
while (true) {
|
|
||||||
String s = f.readStringUntil('\n');
|
|
||||||
if (s.length()==0) break;
|
|
||||||
if (s.startsWith(search)) {
|
|
||||||
s = s.substring(search.length());
|
|
||||||
int idx = s.indexOf(',');
|
|
||||||
String title_index = s.substring(0, idx);
|
|
||||||
uint32_t position = s.substring(idx+1).toInt();
|
|
||||||
p->set_track_by_id(title_index);
|
|
||||||
p->set_position(position);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
f.close();
|
|
||||||
}
|
|
||||||
SPIMaster::select_sd(false);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
p = _playlists[folder];
|
if (map.count(rfid_id)==0) {
|
||||||
if (p->persistence == PERSIST_NONE) {
|
Serial.printf("No known playlist for id %s.\n", rfid_id);
|
||||||
p->reset();
|
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()) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
entry.close();
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
current_playlist.sort();
|
||||||
|
current_rfid_tag_id = rfid_id;
|
||||||
|
return current_playlist;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return p;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaylistManager::dump_ids() {
|
void PlaylistManager::set_audio_current_time(uint32_t time) {
|
||||||
for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) {
|
audio_current_time = time;
|
||||||
INFO(" %s -> %s\n", it->first.c_str(), it->second.c_str());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String PlaylistManager::json() {
|
bool PlaylistManager::has_playlist(String rfid_id) {
|
||||||
DynamicJsonDocument json(10240);
|
return map.count(rfid_id) == 1;
|
||||||
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) {
|
String PlaylistManager::pp_to_String() {
|
||||||
DEBUG("Adding mapping: %s=>%s\n", id.c_str(), folder.c_str());
|
String s = "";
|
||||||
_map[id] = folder;
|
for(const auto& kv : map) {
|
||||||
_save_mapping();
|
s += kv.first + "=" + kv.second.file + "," + kv.second.position + '\n';
|
||||||
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;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlaylistManager::persist(Playlist* p) {
|
|
||||||
if (p->persistence == PERSIST_NONE) {
|
|
||||||
_playlists.erase(p->path());
|
|
||||||
return;
|
|
||||||
} else if (p->persistence == PERSIST_PERMANENTLY) {
|
|
||||||
|
|
||||||
String search = p->path();
|
|
||||||
search += '=';
|
|
||||||
|
|
||||||
bool old_file_existed = false;
|
|
||||||
|
|
||||||
SPIMaster::select_sd();
|
|
||||||
if (SD.exists("_positions.txt")) {
|
|
||||||
SD.rename("/_positions.txt", "/_positions.temp.txt");
|
|
||||||
old_file_existed = true;
|
|
||||||
}
|
|
||||||
File dst = SD.open("/_positions.txt", "w");
|
|
||||||
|
|
||||||
if (old_file_existed) {
|
|
||||||
File src = SD.open("/_positions.temp.txt", "r");
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
String line = src.readStringUntil('\n');
|
|
||||||
line.trim();
|
|
||||||
if (line.startsWith(search)) continue;
|
|
||||||
dst.println(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
src.close();
|
|
||||||
SD.remove("/_positions.temp.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
dst.print(search);
|
|
||||||
dst.print(p->get_current_track_id());
|
|
||||||
dst.print(',');
|
|
||||||
dst.println(p->get_position());
|
|
||||||
dst.close();
|
|
||||||
SPIMaster::select_sd(false);
|
|
||||||
|
|
||||||
_playlists.erase(p->path());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
26
src/spi_master.cpp
Normal file
26
src/spi_master.cpp
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#include "spi_master.h"
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "esmp3.h"
|
||||||
|
|
||||||
|
void SPIMaster::initialize() {
|
||||||
|
pinMode(PIN_CS_SD, OUTPUT);
|
||||||
|
pinMode(PIN_CS_RFID, OUTPUT);
|
||||||
|
disable_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPIMaster::disable_all() {
|
||||||
|
digitalWrite(PIN_CS_SD, HIGH);
|
||||||
|
digitalWrite(PIN_CS_RFID, HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPIMaster::enable_rfid() {
|
||||||
|
disable_all();
|
||||||
|
digitalWrite(PIN_CS_RFID, LOW);
|
||||||
|
delay(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SPIMaster::enable_sd() {
|
||||||
|
disable_all();
|
||||||
|
digitalWrite(PIN_CS_SD, LOW);
|
||||||
|
delay(5);
|
||||||
|
}
|
@ -1,96 +0,0 @@
|
|||||||
#include <Arduino.h>
|
|
||||||
#include <Update.h>
|
|
||||||
#include "config.h"
|
|
||||||
#include "updater.h"
|
|
||||||
#include "http_client_wrapper.h"
|
|
||||||
|
|
||||||
void Updater::run() {
|
|
||||||
DEBUG("Updater is running...\n");
|
|
||||||
HTTPClientWrapper* http = new HTTPClientWrapper();
|
|
||||||
DEBUG("Requesting update info...\n");
|
|
||||||
bool result = http->get(OTA_UPDATE_URL);
|
|
||||||
if (!result) {
|
|
||||||
ERROR("Updater failed requesting %s.\n", OTA_UPDATE_URL);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String line_str = "";
|
|
||||||
if (!read_line(&line_str, http, "VERSION")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint16_t version = line_str.toInt();
|
|
||||||
if (version==0) {
|
|
||||||
ERROR("Could not parse version number.\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DEBUG("Found version %d. My version is %d.\n", version, OTA_VERSION);
|
|
||||||
if (version <= OTA_VERSION) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String image_path = "";
|
|
||||||
if (!read_line(&image_path, http, "IMAGE_PATH")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String image_md5 = "";
|
|
||||||
if (!read_line(&image_md5, http, "IMAGE_MD5")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
http->close();
|
|
||||||
delete http;
|
|
||||||
|
|
||||||
if(do_update(U_FLASH, image_path, image_md5)) {
|
|
||||||
DEBUG("Update done. Rebooting...\n");
|
|
||||||
} else {
|
|
||||||
DEBUG("Update failed. Rebooting...\n");
|
|
||||||
}
|
|
||||||
delay(1000);
|
|
||||||
ESP.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Updater::read_line(String* dst, HTTPClientWrapper* http, String expected_key) {
|
|
||||||
expected_key += "=";
|
|
||||||
String line = http->readUntil("\n");
|
|
||||||
if (!line.startsWith(expected_key)) {
|
|
||||||
ERROR("Expected line start with '%s', but it started with '%s'.\n", expected_key.c_str(), line.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
line = line.substring(expected_key.length());
|
|
||||||
line.trim();
|
|
||||||
dst->concat(line);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Updater::do_update(int command, String url, String expected_md5) {
|
|
||||||
HTTPClientWrapper* http = new HTTPClientWrapper();
|
|
||||||
bool result = http->get(url);
|
|
||||||
if (!result) {
|
|
||||||
ERROR("Updater failed requesting %s.\n", url.c_str());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = Update.begin(http->getSize(), command);
|
|
||||||
if (!result) {
|
|
||||||
ERROR("Update could not be started.\n");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Update.setMD5(expected_md5.c_str());
|
|
||||||
uint8_t buf[512];
|
|
||||||
uint16_t len;
|
|
||||||
while((len = http->read(buf, 512))) {
|
|
||||||
Update.write(buf, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
http->close();
|
|
||||||
delete http;
|
|
||||||
|
|
||||||
result = Update.end();
|
|
||||||
if (!result) {
|
|
||||||
const char* error = Update.errorString();
|
|
||||||
ERROR("Writing the update failed. The error was: %s\n", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user