Compare commits
56 Commits
01f513c97b
...
develop
Author | SHA1 | Date | |
---|---|---|---|
1bb358c961 | |||
12a8391cd7 | |||
b6dc04920a | |||
aca1736201 | |||
fdf986a61e | |||
5c0822b704 | |||
fad4f2c707 | |||
84530f76fd | |||
fa208858d9 | |||
6d452ecbc0 | |||
23fbddb055 | |||
fe2a209e44 | |||
82905a8cdd | |||
3751904cb4 | |||
bcf7625285 | |||
4a3e79f02e | |||
68e1073858 | |||
f73d45404f | |||
ecc7c46b8d | |||
0dd5937707 | |||
547080acf5 | |||
d3c699aefa | |||
a8d19cd6e1 | |||
38d48ab0e4 | |||
51bef05465 | |||
4eef69516e | |||
9175193b67 | |||
65118fbc42 | |||
076f0e9dfd | |||
571e969bc4 | |||
8e15f87cd3 | |||
dd9e1538c8 | |||
001e275131 | |||
196021bef5 | |||
63b9616677 | |||
d4c9a6d582 | |||
5fe66fdaef | |||
6445dc0fb8 | |||
7a20cf4b04 | |||
bbf77c6b1e | |||
b805d1b183 | |||
07b1ea3a5c | |||
3b0410f560 | |||
8f19b990ff | |||
519ac0e3bd | |||
651843fb06 | |||
fcbbdce118 | |||
6f8683ba9d | |||
710b8a2cdc | |||
b989784fb9 | |||
94489618ca | |||
82d8f07eea | |||
20041dd483 | |||
4f9174d362 | |||
68ecc05712 | |||
5fad39ee0e |
31
README.md
31
README.md
@ -37,7 +37,9 @@ and dashes an alike are okay. Put the SD card into the SD card slot.
|
||||
Copy `include/config.sample.h` to `include/config.h`. Modify it to at
|
||||
least contain the correct login details for your WiFi.
|
||||
|
||||
The code then should compile in PlatformIO without errors.
|
||||
The code then should compile in PlatformIO without errors. Upload it
|
||||
to your ESP32. After that, upload static files using PlatformIO's task
|
||||
"Upload file system image".
|
||||
|
||||
The serial console in PlatformIO should give you more or less useful
|
||||
messages about what's going on. There will also be a line saying
|
||||
@ -143,3 +145,30 @@ 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. |
|
3
bin/update.manifest
Normal file
3
bin/update.manifest
Normal file
@ -0,0 +1,3 @@
|
||||
VERSION=1
|
||||
IMAGE_PATH=https://files.schle.nz/esmp3/firmware.bin
|
||||
IMAGE_MD5=00000000000000000000000000000000
|
53
deploy.sh
Executable file
53
deploy.sh
Executable file
@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
#set -x
|
||||
set -e
|
||||
|
||||
if ! git diff-index --quiet HEAD ; then
|
||||
echo "Git isn't clean. Cant deploy."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
branch_name=$(git symbolic-ref -q HEAD)
|
||||
branch_name=${branch_name##refs/heads/}
|
||||
branch_name=${branch_name:-HEAD}
|
||||
|
||||
if [ "$branch_name" != "master" ]; then
|
||||
echo "We are not on master branch. Can't deploy."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo
|
||||
echo "Last tags:"
|
||||
vers=`git tag --sort=-version:refname | head -n 5`
|
||||
while read version; do
|
||||
echo " $version"
|
||||
done <<< "$vers"
|
||||
|
||||
read -p "Version to generate: " VERSION
|
||||
|
||||
OTA_VERSION=`grep "VERSION=" bin/update.manifest | cut -d"=" -f2`
|
||||
OTA_VERSION=$(( $OTA_VERSION + 1 ))
|
||||
|
||||
sed -i.bak "s/#define OTA_VERSION .*/#define OTA_VERSION $OTA_VERSION/" include/config.h include/config.sample.h
|
||||
rm include/config.h.bak include/config.sample.h.bak
|
||||
|
||||
PLATFORMIO_BUILD_FLAGS='-DVERSION=\"$VERSION\"' pio run -e deploy -t buildprog || exit 1
|
||||
|
||||
cp .pio/build/deploy/firmware.bin bin/firmware.bin || exit 1
|
||||
|
||||
sed -i.bak "s/VERSION=.*/VERSION=$OTA_VERSION/" bin/update.manifest
|
||||
MD5=`md5sum --binary bin/firmware.bin | cut -d" " -f1`
|
||||
sed -i.bak "s/IMAGE_MD5=.*/IMAGE_MD5=$MD5/" bin/update.manifest
|
||||
rm bin/update.manifest.bak
|
||||
|
||||
echo; echo; echo; echo; echo
|
||||
echo "Please check the git diff, if everything looks okay:"
|
||||
git diff
|
||||
|
||||
read -p "Press ENTER to continue, Ctrl-C to abort. " foo
|
||||
|
||||
git add bin/firmware.bin bin/update.manifest
|
||||
git commit -m "Deploying version $VERSION."
|
||||
git tag -a -m "Deploying version $VERSION" $VERSION
|
||||
git push --follow-tags
|
@ -1,13 +1,21 @@
|
||||
#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
|
||||
|
||||
#define WIFI_SSID "---CHANGEME---"
|
||||
#define WIFI_PASS "---CHANGEME---"
|
||||
// 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
|
||||
|
@ -10,6 +10,8 @@ class Controller;
|
||||
#include "playlist.h"
|
||||
#include "playlist_manager.h"
|
||||
#include "http_server.h"
|
||||
|
||||
#undef DEPRECATED
|
||||
#include <MFRC522.h>
|
||||
|
||||
enum ControllerState { NORMAL, LOCKING, LOCKED };
|
||||
@ -18,7 +20,6 @@ class Controller {
|
||||
private:
|
||||
MFRC522* _rfid;
|
||||
HTTPServer* _http_server;
|
||||
PlaylistManager* _pm;
|
||||
ControllerState _state = NORMAL;
|
||||
bool _rfid_enabled = true;
|
||||
void _check_rfid();
|
||||
@ -30,9 +31,11 @@ private:
|
||||
bool _rfid_present = false;
|
||||
String _last_rfid_uid = "";
|
||||
String _last_rfid_data = "";
|
||||
Player* _player;
|
||||
|
||||
unsigned long _last_rfid_scan_at = 0;
|
||||
unsigned long _last_position_info_at = 0;
|
||||
unsigned long _last_update_check_at = 0;
|
||||
unsigned long _last_wifi_try_at = 0;
|
||||
String _serial_buffer = String();
|
||||
String _cmd_queue = "";
|
||||
void _execute_command_ls(String path);
|
||||
@ -42,6 +45,8 @@ private:
|
||||
bool _check_button(uint8_t btn);
|
||||
public:
|
||||
Controller(Player* p, PlaylistManager* pm);
|
||||
PlaylistManager* pm;
|
||||
Player* player;
|
||||
void register_http_server(HTTPServer* h);
|
||||
void loop();
|
||||
void send_controller_status();
|
||||
|
58
include/data_sources.h
Normal file
58
include/data_sources.h
Normal file
@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include "config.h"
|
||||
#include "http_client_wrapper.h"
|
||||
|
||||
class DataSource {
|
||||
private:
|
||||
public:
|
||||
DataSource() {};
|
||||
virtual ~DataSource() {};
|
||||
virtual size_t read(uint8_t* buf, size_t len) = 0;
|
||||
virtual int read() = 0;
|
||||
virtual size_t position() = 0;
|
||||
virtual void seek(size_t position) = 0;
|
||||
virtual size_t size() = 0;
|
||||
virtual void close() = 0;
|
||||
virtual bool usable() = 0;
|
||||
virtual int peek(int offset) = 0;
|
||||
void skip_id3_tag();
|
||||
};
|
||||
|
||||
class SDDataSource : public DataSource {
|
||||
private:
|
||||
File _file;
|
||||
public:
|
||||
SDDataSource(String file);
|
||||
~SDDataSource();
|
||||
size_t read(uint8_t* buf, size_t len);
|
||||
int read();
|
||||
size_t position();
|
||||
void seek(size_t position);
|
||||
size_t size();
|
||||
void close();
|
||||
bool usable();
|
||||
int peek(int offset=0);
|
||||
};
|
||||
|
||||
class HTTPSDataSource : public DataSource {
|
||||
private:
|
||||
WiFiClient* _stream = NULL;
|
||||
HTTPClientWrapper* _http = NULL;
|
||||
uint32_t _position;
|
||||
String _url;
|
||||
void _init(String url, uint32_t offset);
|
||||
public:
|
||||
HTTPSDataSource(String url, uint32_t offset=0);
|
||||
~HTTPSDataSource();
|
||||
size_t read(uint8_t* buf, size_t len);
|
||||
int read();
|
||||
size_t position();
|
||||
void seek(size_t position);
|
||||
size_t size();
|
||||
void close();
|
||||
bool usable();
|
||||
int peek(int offset=0);
|
||||
};
|
38
include/http_client_wrapper.h
Normal file
38
include/http_client_wrapper.h
Normal file
@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <HTTPClient.h>
|
||||
#include "config.h"
|
||||
|
||||
class HTTPClientWrapper {
|
||||
private:
|
||||
HTTPClient* _http;
|
||||
uint8_t* _buffer;
|
||||
uint16_t _buffer_size;
|
||||
uint16_t _buffer_length;
|
||||
uint16_t _buffer_position;
|
||||
uint32_t _chunk_length;
|
||||
|
||||
bool _connected = false;
|
||||
String _content_type;
|
||||
uint32_t _length;
|
||||
bool _request(String method, String url, uint32_t offset=0, uint8_t redirection_count=0);
|
||||
WiFiClient* _stream;
|
||||
bool _is_chunked;
|
||||
void _read_next_chunk_header(bool first);
|
||||
uint16_t _fill_buffer();
|
||||
|
||||
public:
|
||||
HTTPClientWrapper();
|
||||
~HTTPClientWrapper();
|
||||
bool get(String url, uint32_t offset=0, uint8_t redirection_count=0);
|
||||
bool head(String url, uint32_t offset=0, uint8_t redirection_count=0);
|
||||
String getContentType();
|
||||
String getString();
|
||||
int read();
|
||||
uint32_t read(uint8_t* dst, uint32_t len);
|
||||
void close();
|
||||
uint32_t getSize();
|
||||
String readUntil(String sep);
|
||||
String readLine();
|
||||
int peek(int offset=0);
|
||||
};
|
13
include/main.h
Normal file
13
include/main.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <Preferences.h>
|
||||
|
||||
void wifi_connect();
|
||||
|
||||
extern bool debug_enabled;
|
||||
extern bool trace_enabled;
|
||||
extern Preferences prefs;
|
||||
|
||||
extern const uint8_t file_index_html_start[] asm("_binary_src_webinterface_index_html_gz_start");
|
||||
extern const size_t file_index_html_size asm("_binary_src_webinterface_index_html_gz_size");
|
||||
extern const uint8_t file_timezones_json_start[] asm("_binary_src_webinterface_timezones_json_gz_start");
|
||||
extern const size_t file_timezones_json_size asm("_binary_src_webinterface_timezones_json_gz_size");
|
@ -4,6 +4,7 @@
|
||||
#include <SD.h>
|
||||
#include "spi_master.h"
|
||||
#include "playlist.h"
|
||||
#include "data_sources.h"
|
||||
|
||||
class Player;
|
||||
|
||||
@ -55,7 +56,6 @@ private:
|
||||
void _flush_and_cancel();
|
||||
int8_t _get_endbyte();
|
||||
void _flush(uint count, int8_t fill_byte);
|
||||
uint32_t _id3_tag_offset(File f);
|
||||
void _play_file(String filename, uint32_t offset);
|
||||
void _finish_playing();
|
||||
void _finish_stopping(bool turn_speaker_off);
|
||||
@ -72,7 +72,7 @@ private:
|
||||
SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
||||
SPISettings* _spi_settings = &_spi_settings_slow;
|
||||
|
||||
File _file;
|
||||
DataSource* _file;
|
||||
uint32_t _file_size = 0;
|
||||
uint8_t _buffer[32];
|
||||
uint32_t _current_play_position = 0;
|
||||
|
@ -2,6 +2,22 @@
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
#include <ArduinoJson.h>
|
||||
#include "main.h"
|
||||
#include "http_client_wrapper.h"
|
||||
|
||||
enum PlaylistPersistence {
|
||||
PERSIST_NONE,
|
||||
PERSIST_TEMPORARY,
|
||||
PERSIST_PERMANENTLY
|
||||
};
|
||||
|
||||
struct PlaylistEntry {
|
||||
String filename;
|
||||
String title;
|
||||
String id;
|
||||
|
||||
bool operator<(PlaylistEntry p) { return title < p.title; }
|
||||
};
|
||||
|
||||
class Playlist {
|
||||
private:
|
||||
@ -9,19 +25,31 @@ private:
|
||||
uint32_t _current_track = 0;
|
||||
bool _started = false;
|
||||
bool _shuffled = false;
|
||||
std::vector<String> _files;
|
||||
std::vector<PlaylistEntry> _files;
|
||||
String _title = "";
|
||||
String _path;
|
||||
void _add_path(String path);
|
||||
void _examine_http_url(String url);
|
||||
void _parse_rss(HTTPClientWrapper* http);
|
||||
void _parse_m3u(HTTPClientWrapper* http);
|
||||
void _parse_pls(HTTPClientWrapper* http);
|
||||
public:
|
||||
PlaylistPersistence persistence = PERSIST_TEMPORARY;
|
||||
Playlist(String path);
|
||||
void start();
|
||||
uint16_t get_file_count();
|
||||
bool has_track_next();
|
||||
bool has_track_prev();
|
||||
bool track_next();
|
||||
bool track_prev();
|
||||
void track_restart();
|
||||
bool set_track(uint8_t track);
|
||||
void set_track_by_id(String id);
|
||||
void reset();
|
||||
String path();
|
||||
bool is_empty();
|
||||
String get_current_file();
|
||||
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);
|
||||
|
@ -19,4 +19,6 @@ public:
|
||||
void scan_files();
|
||||
String json();
|
||||
bool add_mapping(String id, String folder);
|
||||
String create_mapping_txt();
|
||||
void persist(Playlist* p);
|
||||
};
|
||||
|
10
include/updater.h
Normal file
10
include/updater.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "http_client_wrapper.h"
|
||||
|
||||
class Updater {
|
||||
public:
|
||||
static void run();
|
||||
static bool do_update(int cmd, String url, String expected_md5);
|
||||
static bool read_line(String* dst, HTTPClientWrapper* http, String expected_key);
|
||||
};
|
6
partitions.csv
Normal file
6
partitions.csv
Normal file
@ -0,0 +1,6 @@
|
||||
# Custom partition table without SPIFFS.
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x220000,
|
||||
app1, app, ota_1, 0x230000,0x220000,
|
|
@ -8,15 +8,30 @@
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:esp32]
|
||||
[platformio]
|
||||
default_envs = esp32
|
||||
|
||||
[env]
|
||||
platform = espressif32
|
||||
board = esp-wrover-kit
|
||||
framework = arduino
|
||||
upload_speed = 512000
|
||||
build_flags=!./build_version.sh
|
||||
lib_deps = MFRC522
|
||||
lib_deps =
|
||||
63 ; MFRC522
|
||||
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
||||
ArduinoJSON
|
||||
upload_port = /dev/cu.SLAB_USBtoUART
|
||||
monitor_speed = 74480
|
||||
64 ; ArduinoJSON
|
||||
6691 ; TinyXML
|
||||
monitor_speed = 115200
|
||||
board_build.embed_files =
|
||||
src/webinterface/timezones.json.gz
|
||||
src/webinterface/index.html.gz
|
||||
;board_build.partitions = partitions.csv
|
||||
;monitor_port = /dev/cu.wchusbserial1420
|
||||
extra_scripts =
|
||||
post:tools/post_build.py
|
||||
|
||||
[env:esp32]
|
||||
build_flags=!./build_version.sh
|
||||
upload_port = /dev/cu.SLAB_USBtoUART
|
||||
|
||||
[env:deploy]
|
||||
|
@ -1,16 +1,18 @@
|
||||
#include "controller.h"
|
||||
#include "main.h"
|
||||
#include "spi_master.h"
|
||||
#include "config.h"
|
||||
#include "playlist.h"
|
||||
#include "http_server.h"
|
||||
#include "updater.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
Controller::Controller(Player* p, PlaylistManager* pm) {
|
||||
_player = p;
|
||||
_pm = pm;
|
||||
Controller::Controller(Player* p, PlaylistManager* playlist_manager) {
|
||||
player = p;
|
||||
pm = playlist_manager;
|
||||
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
|
||||
|
||||
_player->register_controller(this);
|
||||
player->register_controller(this);
|
||||
|
||||
BTN_NEXT_SETUP();
|
||||
BTN_PREV_SETUP();
|
||||
@ -34,7 +36,6 @@ void Controller::register_http_server(HTTPServer* h) {
|
||||
}
|
||||
|
||||
void Controller::loop() {
|
||||
TRACE("Controller::loop()...\n");
|
||||
unsigned long now = millis();
|
||||
if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) {
|
||||
_check_rfid();
|
||||
@ -50,7 +51,20 @@ void Controller::loop() {
|
||||
process_message(_cmd_queue);
|
||||
_cmd_queue = "";
|
||||
}
|
||||
TRACE("Controller::loop() done.\n");
|
||||
|
||||
#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() {
|
||||
@ -69,7 +83,7 @@ uint32_t Controller::_get_rfid_card_uid() {
|
||||
}
|
||||
|
||||
void Controller::_check_rfid() {
|
||||
TRACE("check_rfid running...\n");
|
||||
//TRACE("check_rfid running...\n");
|
||||
MFRC522::StatusCode status;
|
||||
if (_rfid_present) {
|
||||
byte buffer[2];
|
||||
@ -87,7 +101,7 @@ void Controller::_check_rfid() {
|
||||
_rfid_present = false;
|
||||
INFO("No more RFID card.\n");
|
||||
if (_state != LOCKED) {
|
||||
_player->stop();
|
||||
player->stop();
|
||||
}
|
||||
send_controller_status();
|
||||
} else {
|
||||
@ -106,7 +120,7 @@ void Controller::_check_rfid() {
|
||||
String data = _read_rfid_data();
|
||||
_last_rfid_data = data;
|
||||
|
||||
Playlist* pl = _pm->get_playlist_for_id(s_uid);
|
||||
Playlist* pl = pm->get_playlist_for_id(s_uid);
|
||||
if (data.indexOf("[lock]") != -1) {
|
||||
if (_state == LOCKED) {
|
||||
_state = NORMAL;
|
||||
@ -128,7 +142,8 @@ void Controller::_check_rfid() {
|
||||
if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december.
|
||||
pl->advent_shuffle(time.tm_mday);
|
||||
} else {
|
||||
// TODO
|
||||
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();
|
||||
@ -152,7 +167,7 @@ void Controller::_check_rfid() {
|
||||
DEBUG("ControllerState is now LOCKED.\n");
|
||||
}
|
||||
|
||||
_player->play(pl);
|
||||
player->play(pl);
|
||||
//send_playlist_manager_status();
|
||||
send_controller_status();
|
||||
}
|
||||
@ -184,7 +199,7 @@ String Controller::_read_rfid_data() {
|
||||
case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break;
|
||||
default: INFO("Unknown PICC type %s\n", String(MFRC522::PICC_GetTypeName(type)).c_str());
|
||||
}
|
||||
|
||||
sectors = 2; // Pretend we have only two sectors, so we read only sector #1.
|
||||
int good_key_index = -1;
|
||||
for (uint8_t sector=1; sector<sectors; sector++) {
|
||||
uint8_t blocks = (sector < 32) ? 4 : 16;
|
||||
@ -228,8 +243,6 @@ String Controller::_read_rfid_data() {
|
||||
}
|
||||
|
||||
void Controller::_check_serial() {
|
||||
TRACE("check_serial running...\n");
|
||||
|
||||
if (Serial.available() > 0) {
|
||||
char c = Serial.read();
|
||||
Serial.printf("%c", c);
|
||||
@ -248,38 +261,33 @@ 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("ls")) {
|
||||
// _execute_command_ls("/");
|
||||
//} else if (cmd.startsWith("ls ")) {
|
||||
// _execute_command_ls(cmd.substring(3));
|
||||
Playlist* p = pm->get_playlist_for_folder(cmd.substring(5));
|
||||
player->play(p);
|
||||
} else if (cmd.equals("play")) {
|
||||
_player->play();
|
||||
|
||||
player->play();
|
||||
} else if (cmd.equals("stop")) {
|
||||
_player->stop();
|
||||
player->stop();
|
||||
} else if (cmd.equals("help")) {
|
||||
_execute_command_help();
|
||||
} else if (cmd.equals("-")) {
|
||||
_player->vol_down();
|
||||
player->vol_down();
|
||||
} else if (cmd.equals("+")) {
|
||||
_player->vol_up();
|
||||
player->vol_up();
|
||||
} else if (cmd.startsWith("volume=")) {
|
||||
uint8_t vol = cmd.substring(7).toInt();
|
||||
_player->set_volume(vol);
|
||||
player->set_volume(vol);
|
||||
} else if (cmd.equals("track_prev")) {
|
||||
_player->track_prev();
|
||||
player->track_prev();
|
||||
} else if (cmd.equals("track_next")) {
|
||||
_player->track_next();
|
||||
player->track_next();
|
||||
} else if (cmd.startsWith("track=")) {
|
||||
uint8_t track = cmd.substring(6).toInt();
|
||||
_player->set_track(track);
|
||||
player->set_track(track);
|
||||
} else if (cmd.equals("ids")) {
|
||||
_pm->dump_ids();
|
||||
pm->dump_ids();
|
||||
} else if (cmd.equals("reset_vs1053")) {
|
||||
_player->stop();
|
||||
_player->init();
|
||||
player->stop();
|
||||
player->init();
|
||||
} else if (cmd.equals("reboot")) {
|
||||
ESP.restart();
|
||||
} else if (cmd.startsWith("add_mapping=")) {
|
||||
@ -287,8 +295,30 @@ bool Controller::process_message(String cmd) {
|
||||
uint8_t idx = rest.indexOf('=');
|
||||
String id = rest.substring(0, idx);
|
||||
String folder = rest.substring(idx + 1);
|
||||
_pm->add_mapping(id, folder);
|
||||
pm->add_mapping(id, folder);
|
||||
send_playlist_manager_status();
|
||||
#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 {
|
||||
ERROR("Unknown command: %s\n", cmd.c_str());
|
||||
return false;
|
||||
@ -299,7 +329,7 @@ bool Controller::process_message(String cmd) {
|
||||
void Controller::_execute_command_ls(String path) {
|
||||
INFO("Listing contents of %s:\n", path.c_str());
|
||||
// TODO
|
||||
//std::list<String> files = _player->ls(path);
|
||||
//std::list<String> files = player->ls(path);
|
||||
//for(std::list<String>::iterator it=files.begin(); it!=files.end(); ++it) {
|
||||
// INFO(" %s\n", (*it).c_str());
|
||||
//}
|
||||
@ -317,21 +347,19 @@ void Controller::_execute_command_help() {
|
||||
}
|
||||
|
||||
void Controller::_check_buttons() {
|
||||
TRACE("check_buttons running...\n");
|
||||
|
||||
if (BTN_PREV() && _debounce_button(0)) {
|
||||
if (_state == NORMAL) {
|
||||
_player->track_prev();
|
||||
player->track_prev();
|
||||
} else {
|
||||
DEBUG("Ignoring btn_prev because state is LOCKED.\n");
|
||||
}
|
||||
} else if (BTN_VOL_UP() && _debounce_button(1)) {
|
||||
_player->vol_up();
|
||||
player->vol_up();
|
||||
} else if (BTN_VOL_DOWN() && _debounce_button(2)) {
|
||||
_player->vol_down();
|
||||
player->vol_down();
|
||||
} else if (BTN_NEXT() && _debounce_button(3)) {
|
||||
if (_state == NORMAL) {
|
||||
_player->track_next();
|
||||
player->track_next();
|
||||
} else {
|
||||
DEBUG("Ignoring btn_next because state is LOCKED.\n");
|
||||
}
|
||||
@ -360,6 +388,24 @@ String Controller::json() {
|
||||
JsonObject rfid = json.createNestedObject("last_rfid");
|
||||
rfid["uid"] = _last_rfid_uid;
|
||||
rfid["data"] = _last_rfid_data;
|
||||
json["uptime"] = millis() / 1000;
|
||||
json["free_heap"] = ESP.getFreeHeap();
|
||||
JsonObject versions = json.createNestedObject("versions");
|
||||
versions["ota"] = OTA_VERSION;
|
||||
#ifdef VERSION
|
||||
versions["release"] = VERSION;
|
||||
#else
|
||||
versions["release"] = "unknown";
|
||||
#endif
|
||||
|
||||
JsonObject wifi = json.createNestedObject("wifi");
|
||||
if (WiFi.isConnected()) {
|
||||
wifi["connected"] = true;
|
||||
wifi["ssid"] = WiFi.SSID();
|
||||
wifi["rssi"] = WiFi.RSSI();
|
||||
} else {
|
||||
wifi["connected"] = false;
|
||||
}
|
||||
return json.as<String>();
|
||||
}
|
||||
|
||||
@ -367,22 +413,22 @@ void Controller::send_player_status() {
|
||||
TRACE("In send_player_status()...\n");
|
||||
|
||||
if (_http_server->ws->count() > 0) {
|
||||
_http_server->ws->textAll(_player->json());
|
||||
_http_server->ws->textAll(_player->position_json());
|
||||
_http_server->ws->textAll(player->json());
|
||||
_http_server->ws->textAll(player->position_json());
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::send_playlist_manager_status() {
|
||||
TRACE("In send_playlist_manager_status()...\n");
|
||||
if (_http_server->ws->count() > 0) {
|
||||
_http_server->ws->textAll(_pm->json());
|
||||
_http_server->ws->textAll(pm->json());
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::send_position() {
|
||||
TRACE("In send_position()...\n");
|
||||
if (_http_server->ws->count() > 0 && _player->is_playing()) {
|
||||
_http_server->ws->textAll(_player->position_json());
|
||||
if (_http_server->ws->count() > 0) {
|
||||
_http_server->ws->textAll(player->position_json());
|
||||
}
|
||||
_last_position_info_at = millis();
|
||||
}
|
||||
@ -396,11 +442,11 @@ void Controller::send_controller_status() {
|
||||
|
||||
void Controller::inform_new_client(AsyncWebSocketClient* client) {
|
||||
String s;
|
||||
s += _pm->json();
|
||||
s += pm->json();
|
||||
s += '\n';
|
||||
s += _player->json();
|
||||
s += player->json();
|
||||
s += '\n';
|
||||
s += _player->position_json();
|
||||
s += player->position_json();
|
||||
s += '\n';
|
||||
s += json();
|
||||
client->text(s);
|
||||
@ -412,6 +458,6 @@ void Controller::queue_command(String s) {
|
||||
}
|
||||
|
||||
void Controller::update_playlist_manager() {
|
||||
_pm->scan_files();
|
||||
pm->scan_files();
|
||||
send_playlist_manager_status();
|
||||
}
|
71
src/data_sources.cpp
Normal file
71
src/data_sources.cpp
Normal file
@ -0,0 +1,71 @@
|
||||
#include "data_sources.h"
|
||||
|
||||
void DataSource::skip_id3_tag() {
|
||||
if (peek(0)=='I' && peek(1)=='D' && peek(2)=='3') {
|
||||
DEBUG("ID3 tag found\n");
|
||||
// Skip ID3 tag marker
|
||||
read(); read(); read();
|
||||
// Skip ID3 tag version
|
||||
read(); read();
|
||||
byte tags = read();
|
||||
bool footer_present = tags & 0x10;
|
||||
DEBUG("ID3 footer found: %d\n", footer_present);
|
||||
uint32_t offset = 0;
|
||||
for (byte i=0; i<4; i++) {
|
||||
offset <<= 7;
|
||||
offset |= (0x7F & read());
|
||||
}
|
||||
offset += 10;
|
||||
if (footer_present) offset += 10;
|
||||
DEBUG("ID3 tag length is %d bytes.\n", offset);
|
||||
seek(offset);
|
||||
} else {
|
||||
DEBUG("No ID3 tag found\n");
|
||||
}
|
||||
}
|
||||
|
||||
////////////// SDDataSource //////////////
|
||||
SDDataSource::SDDataSource(String file) { _file = SD.open(file, "r"); }
|
||||
SDDataSource::~SDDataSource() { if (_file) _file.close(); }
|
||||
size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); }
|
||||
int SDDataSource::read() { return _file.read(); }
|
||||
size_t SDDataSource::position() { return _file.position(); }
|
||||
void SDDataSource::seek(size_t position) { _file.seek(position); }
|
||||
size_t SDDataSource::size() { return _file.size(); }
|
||||
void SDDataSource::close() { _file.close(); }
|
||||
bool SDDataSource::usable() { return _file; }
|
||||
int SDDataSource::peek(int offset) {
|
||||
if (offset==0) return _file.peek();
|
||||
size_t start_position = _file.position();
|
||||
_file.seek(start_position + offset);
|
||||
int result = _file.peek();
|
||||
_file.seek(start_position);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
////////////// HTTPSDataSource //////////////
|
||||
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
|
||||
_url = url;
|
||||
_init(url, offset);
|
||||
}
|
||||
|
||||
void HTTPSDataSource::_init(String url, uint32_t offset) {
|
||||
_url = url;
|
||||
_http = new HTTPClientWrapper();
|
||||
if (!_http->get(url, offset)) return;
|
||||
_position = 0;
|
||||
}
|
||||
|
||||
HTTPSDataSource::~HTTPSDataSource() {
|
||||
_http->close();
|
||||
delete _http;
|
||||
}
|
||||
bool HTTPSDataSource::usable() { return _http; }
|
||||
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; }
|
||||
int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; }
|
||||
size_t HTTPSDataSource::position() { return _position; }
|
||||
void HTTPSDataSource::seek(size_t position) { _http->close(); delete _http; _init(_url, position); }
|
||||
size_t HTTPSDataSource::size() { return _http->getSize(); }
|
||||
void HTTPSDataSource::close() { _http->close(); }
|
||||
int HTTPSDataSource::peek(int offset) { return _http->peek(offset); }
|
222
src/http_client_wrapper.cpp
Normal file
222
src/http_client_wrapper.cpp
Normal file
@ -0,0 +1,222 @@
|
||||
#include "http_client_wrapper.h"
|
||||
#include <WiFiClientSecure.h>
|
||||
|
||||
HTTPClientWrapper::HTTPClientWrapper() {
|
||||
_buffer = new uint8_t[512];
|
||||
_buffer_size = 512;
|
||||
}
|
||||
|
||||
HTTPClientWrapper::~HTTPClientWrapper() {
|
||||
if (_http) {
|
||||
_http->end();
|
||||
delete _http;
|
||||
}
|
||||
delete _buffer;
|
||||
}
|
||||
|
||||
void HTTPClientWrapper::close() {
|
||||
_http->end();
|
||||
_connected = false;
|
||||
}
|
||||
|
||||
bool HTTPClientWrapper::get(String url, uint32_t offset, uint8_t redirection_count) { return _request("GET", url, offset, redirection_count); }
|
||||
bool HTTPClientWrapper::head(String url, uint32_t offset, uint8_t redirection_count) { return _request("HEAD", url, offset, redirection_count); }
|
||||
|
||||
bool HTTPClientWrapper::_request(String method, String url, uint32_t offset, uint8_t redirection_count) {
|
||||
if (redirection_count>=5) return false;
|
||||
|
||||
//if (_http) delete _http;
|
||||
|
||||
DEBUG("%s %s\n", method.c_str(), url.c_str());
|
||||
_http = new HTTPClient();
|
||||
|
||||
|
||||
_http->setUserAgent("PodBox/0.1");
|
||||
if (offset) {
|
||||
String s = "bytes=";
|
||||
s += offset;
|
||||
s += "-";
|
||||
_http->addHeader("Range: ", s);
|
||||
}
|
||||
|
||||
const char* headers[] = {"Location", "Content-Type", "Transfer-Encoding"};
|
||||
_http->collectHeaders(headers, 3);
|
||||
bool result;
|
||||
/*if (url.startsWith("https:")) {
|
||||
BearSSL::WiFiClientSecure* client = new BearSSL::WiFiClientSecure();
|
||||
client->setInsecure();
|
||||
result = _http->begin(*client, url);
|
||||
} else {
|
||||
result = _http->begin(url);
|
||||
}*/
|
||||
result = _http->begin(url);
|
||||
TRACE("HTTP->begin result: %d\n", result);
|
||||
if (!result) return false;
|
||||
|
||||
int status = _http->sendRequest(method.c_str());
|
||||
TRACE("HTTP Status code: %d\n", status);
|
||||
if (status == HTTP_CODE_FOUND || status==HTTP_CODE_MOVED_PERMANENTLY || status==HTTP_CODE_TEMPORARY_REDIRECT) {
|
||||
if (_http->hasHeader("Location")) {
|
||||
url = _http->header("Location");
|
||||
_http->end();
|
||||
delete _http;
|
||||
_http = NULL;
|
||||
return _request(method, url, offset, redirection_count+1);
|
||||
} else {
|
||||
ERROR("Got redirection HTTP code, but no Location header.\n");
|
||||
delete _http;
|
||||
_http = NULL;
|
||||
return false;
|
||||
}
|
||||
} else if (status != HTTP_CODE_OK) {
|
||||
DEBUG("Unexpected HTTP return code %d. Cancelling.\n", status);
|
||||
return false;
|
||||
}
|
||||
|
||||
_buffer_position = 0;
|
||||
_buffer_length = 0;
|
||||
|
||||
_connected = true;
|
||||
_length = _http->getSize() + offset;
|
||||
if (_http->hasHeader("Content-Type")) {
|
||||
_content_type = _http->header("Content-Type");
|
||||
} else {
|
||||
_content_type = "";
|
||||
}
|
||||
_is_chunked = (_http->hasHeader("Transfer-Encoding")) && (_http->header("Transfer-Encoding").indexOf("chunked")!=-1);
|
||||
_stream = _http->getStreamPtr();
|
||||
if (_is_chunked) {
|
||||
_read_next_chunk_header(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HTTPClientWrapper::_read_next_chunk_header(bool first) {
|
||||
if (!_connected) {
|
||||
_chunk_length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!first) {
|
||||
// read() returns an error if no bytes is available right at this moment.
|
||||
// So we wait until 2 bytes are available or the connection times out.
|
||||
while (_stream->connected() && !_stream->available()) { delay(1); }
|
||||
int c1 = _stream->read();
|
||||
while (_stream->connected() && !_stream->available()) { delay(1); }
|
||||
int c2 = _stream->read();
|
||||
if (c1==-1 || c2==-1) {
|
||||
ERROR("Connection timeout.\n");
|
||||
DEBUG("_stream.connected() returns %d\n", _stream->connected());
|
||||
_chunk_length = 0;
|
||||
_connected = false;
|
||||
return;
|
||||
} else if (c1!='\r' || c2!='\n') {
|
||||
ERROR("Invalid chunk border found. Found: 0x%02X 0x%02X\n", c1, c2);
|
||||
_chunk_length = 0;
|
||||
_connected = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
String chunk_header = _stream->readStringUntil('\n');
|
||||
chunk_header.trim();
|
||||
_chunk_length = strtol(chunk_header.c_str(), NULL, 16);
|
||||
if (_chunk_length == 0) {
|
||||
_connected = false;
|
||||
TRACE("Empty chunk found -> EOF reached.\n");
|
||||
} else {
|
||||
TRACE("Chunk found. Length: %d\n", _chunk_length);
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t HTTPClientWrapper::_fill_buffer() {
|
||||
if (!_connected) {
|
||||
_buffer_position = 0;
|
||||
_buffer_length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t bytes_to_fill = _buffer_size;
|
||||
uint16_t bytes_filled = 0;
|
||||
while (bytes_to_fill > 0) {
|
||||
uint16_t bytes_to_request = bytes_to_fill;
|
||||
if (_is_chunked && _chunk_length < bytes_to_fill) bytes_to_request = _chunk_length;
|
||||
TRACE("fill_buffer loop. _is_chunked: %d, _chunk_length: %d, _buffer_size: %d, bytes_filled: %d, bytes_to_fill: %d, bytes_to_request: %d", _is_chunked, _chunk_length, _buffer_size, bytes_filled, bytes_to_fill, bytes_to_request);
|
||||
uint16_t result = _stream->readBytes(_buffer + bytes_filled, bytes_to_request);
|
||||
TRACE(", result: %d\n", result);
|
||||
bytes_filled += result;
|
||||
bytes_to_fill -= result;
|
||||
if (_is_chunked) {
|
||||
_chunk_length -= result;
|
||||
if (_chunk_length == 0) _read_next_chunk_header(false);
|
||||
}
|
||||
if (result == 0) {
|
||||
_connected = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_buffer_position = 0;
|
||||
_buffer_length = bytes_filled;
|
||||
TRACE("Buffer filled. _buffer_length: %d\n", _buffer_length);
|
||||
return bytes_filled;
|
||||
}
|
||||
|
||||
String HTTPClientWrapper::getContentType() {
|
||||
return _content_type;
|
||||
}
|
||||
|
||||
int HTTPClientWrapper::read() {
|
||||
if (_buffer_position >= _buffer_length) _fill_buffer();
|
||||
if (_buffer_position >= _buffer_length) return -1;
|
||||
return _buffer[_buffer_position++];
|
||||
}
|
||||
|
||||
uint32_t HTTPClientWrapper::read(uint8_t* dst, uint32_t len) {
|
||||
TRACE("Reading %d bytes...\n", len);
|
||||
uint32_t bytes_filled = 0;
|
||||
while (1) {
|
||||
if (_buffer_position >= _buffer_length) _fill_buffer();
|
||||
if (_buffer_position >= _buffer_length) break;
|
||||
|
||||
uint32_t bytes_to_fill = len;
|
||||
if (bytes_to_fill > _buffer_length - _buffer_position) bytes_to_fill = _buffer_length - _buffer_position;
|
||||
|
||||
TRACE("read_loop: _buffer_length=%d, _buffer_position=%d, len=%d, bytes_to_fill=%d\n", _buffer_length, _buffer_position, len, bytes_to_fill);
|
||||
memcpy(dst + bytes_filled, _buffer + _buffer_position, bytes_to_fill);
|
||||
_buffer_position += bytes_to_fill;
|
||||
bytes_filled += bytes_to_fill;
|
||||
len -= bytes_to_fill;
|
||||
if (bytes_to_fill==0 || len==0) break;
|
||||
}
|
||||
return bytes_filled;
|
||||
}
|
||||
|
||||
uint32_t HTTPClientWrapper::getSize() {return _length; }
|
||||
|
||||
String HTTPClientWrapper::readUntil(String sep) {
|
||||
String result = "";
|
||||
while(true) {
|
||||
int i = read();
|
||||
if (i==-1) break;
|
||||
char c = i;
|
||||
if (sep.indexOf(c)!=-1) {
|
||||
// separator
|
||||
if (result.length()>0) break;
|
||||
} else {
|
||||
result.concat(c);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String HTTPClientWrapper::readLine() {
|
||||
return readUntil("\n\r");
|
||||
}
|
||||
|
||||
int HTTPClientWrapper::peek(int offset) {
|
||||
if (_buffer_position >= _buffer_length) _fill_buffer();
|
||||
|
||||
if (_buffer_position + offset < 0 || _buffer_position + offset >= _buffer_length) {
|
||||
return -1;
|
||||
}
|
||||
return _buffer[_buffer_position + offset];
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
#include "http_server.h"
|
||||
#include "main.h"
|
||||
#include "spi_master.h"
|
||||
#include <ESPmDNS.h>
|
||||
#include <SPIFFS.h>
|
||||
|
||||
HTTPServer::HTTPServer(Player* p, Controller* c) {
|
||||
_player = p;
|
||||
@ -10,8 +10,43 @@ HTTPServer::HTTPServer(Player* p, Controller* c) {
|
||||
ws = new AsyncWebSocket("/ws");
|
||||
_server->addHandler(ws);
|
||||
ws->onEvent([&](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){this->_onEvent(server, client, type, arg, data, len);});
|
||||
_server->on("/", [&](AsyncWebServerRequest* req) {req->send(SPIFFS, "/index.html", "text/html");});
|
||||
_server->on("/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("/", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||
req->send_P(200, "text/html", file_index_html_start, file_index_html_size);
|
||||
});
|
||||
_server->on("/timezone.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||
AsyncWebServerResponse* res = req->beginResponse_P(200, "application/json", file_timezones_json_start, file_timezones_json_size);
|
||||
res->addHeader("Content-Encoding", "gzip");
|
||||
req->send(res);
|
||||
});
|
||||
_server->on("/upload", HTTP_POST, [](AsyncWebServerRequest* req) {
|
||||
req->send(200);
|
||||
}, [&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
this->_handle_upload(request, filename, index, data, len, final);
|
||||
});
|
||||
_server->on("/_mapping.txt", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||
req->send(200, "text/plain", _controller->pm->create_mapping_txt());
|
||||
});
|
||||
_server->on("/player.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||
req->send(200, "application/json", _controller->player->json());
|
||||
});
|
||||
_server->on("/playlist_manager.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||
req->send(200, "application/json", _controller->pm->json());
|
||||
});
|
||||
_server->on("/controller.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||
req->send(200, "application/json", _controller->json());
|
||||
});
|
||||
_server->on("/position.json", HTTP_GET, [&](AsyncWebServerRequest* req) {
|
||||
req->send(200, "application/json", _controller->player->position_json());
|
||||
});
|
||||
_server->on("/cmd", HTTP_POST, [&](AsyncWebServerRequest *req) {
|
||||
if (req->hasParam("cmd", true)) {
|
||||
_controller->queue_command(req->getParam("cmd", true)->value());
|
||||
req->send(200);
|
||||
} else {
|
||||
req->send(400);
|
||||
}
|
||||
});
|
||||
_server->begin();
|
||||
MDNS.addService("http", "tcp", 80);
|
||||
}
|
||||
@ -129,6 +164,7 @@ void HTTPServer::_onEvent(AsyncWebSocket * server, AsyncWebSocketClient * 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);
|
||||
}
|
||||
|
81
src/main.cpp
81
src/main.cpp
@ -2,14 +2,17 @@
|
||||
#include <SPI.h>
|
||||
#include <SD.h>
|
||||
#include <WiFi.h>
|
||||
#include <WiFiMulti.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <SPIFFS.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;
|
||||
@ -18,18 +21,67 @@ 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() {
|
||||
delay(500);
|
||||
Serial.begin(74880);
|
||||
// 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\n", VERSION);
|
||||
INFO("ESMP3 version %s (OTA_VERSION %d)\n", VERSION, OTA_VERSION);
|
||||
#else
|
||||
INFO("ESMP3, version unknown\n");
|
||||
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();
|
||||
@ -47,9 +99,6 @@ void setup() {
|
||||
}
|
||||
spi->select_sd(false);
|
||||
|
||||
DEBUG("Starting SPIFFS...\n");
|
||||
SPIFFS.begin(true);
|
||||
|
||||
DEBUG("Initializing PlaylistManager...\n");
|
||||
pm = new PlaylistManager();
|
||||
|
||||
@ -58,15 +107,7 @@ void setup() {
|
||||
controller = new Controller(player, pm);
|
||||
INFO("Player and controller initialized.\n");
|
||||
|
||||
DEBUG("Connecting to wifi \"%s\"...\n", WIFI_SSID);
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
||||
ERROR("Could not connect to Wifi. Rebooting.");
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
INFO("WiFi connected. IP address: %s\n", WiFi.localIP().toString().c_str());
|
||||
wifi_connect();
|
||||
|
||||
MDNS.begin("esmp3");
|
||||
|
||||
@ -86,6 +127,12 @@ void setup() {
|
||||
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");
|
||||
}
|
||||
|
||||
|
@ -10,8 +10,6 @@ Player::Player(SPIMaster* s) {
|
||||
_spi = s;
|
||||
PIN_VS1053_XRESET_SETUP();
|
||||
PIN_VS1053_XRESET(HIGH);
|
||||
PIN_SPEAKER_L_SETUP();
|
||||
PIN_SPEAKER_R_SETUP();
|
||||
_speaker_off();
|
||||
_spi->disable();
|
||||
PIN_VS1053_DREQ_SETUP();
|
||||
@ -463,13 +461,15 @@ void Player::set_volume(uint8_t vol, bool save) {
|
||||
}
|
||||
|
||||
void Player::vol_up() {
|
||||
uint8_t vol = _volume + VOLUME_STEP;
|
||||
if (!is_playing()) return;
|
||||
uint16_t vol = _volume + VOLUME_STEP;
|
||||
if (vol > VOLUME_MAX) vol=VOLUME_MAX;
|
||||
set_volume(vol);
|
||||
}
|
||||
|
||||
void Player::vol_down() {
|
||||
uint8_t vol = _volume - VOLUME_STEP;
|
||||
if (!is_playing()) return;
|
||||
int16_t vol = _volume - VOLUME_STEP;
|
||||
if (vol < VOLUME_MIN) vol=VOLUME_MIN;
|
||||
set_volume(vol);
|
||||
}
|
||||
@ -531,8 +531,13 @@ 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 = _current_playlist->get_current_file();
|
||||
String file;
|
||||
if (!_current_playlist->get_current_file(&file)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t position = _current_playlist->get_position();
|
||||
_state = playing;
|
||||
_play_file(file, position);
|
||||
@ -543,10 +548,16 @@ bool Player::play() {
|
||||
void Player::_play_file(String file, uint32_t file_offset) {
|
||||
INFO("play_file('%s', %d)\n", file.c_str(), file_offset);
|
||||
_spi->select_sd();
|
||||
_file = SD.open(file);
|
||||
_file_size = _file.size();
|
||||
if (file.startsWith("/")) {
|
||||
_file = new SDDataSource(file);
|
||||
} else if (file.startsWith("http")) {
|
||||
_file = new HTTPSDataSource(file);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
_file_size = _file->size();
|
||||
_spi->select_sd(false);
|
||||
if (!_file) {
|
||||
if (!_file || !_file->usable()) {
|
||||
DEBUG("Could not open file %s", file.c_str());
|
||||
return;
|
||||
}
|
||||
@ -559,10 +570,10 @@ void Player::_play_file(String file, uint32_t file_offset) {
|
||||
|
||||
_spi->select_sd();
|
||||
if (file_offset == 0) {
|
||||
_file.seek(_id3_tag_offset(_file));
|
||||
_file->skip_id3_tag();
|
||||
}
|
||||
_refills = 0;
|
||||
_current_play_position = _file.position();
|
||||
_current_play_position = _file->position();
|
||||
_spi->select_sd(false);
|
||||
_skip_to = file_offset;
|
||||
if (_skip_to>0) _mute();
|
||||
@ -571,30 +582,6 @@ void Player::_play_file(String file, uint32_t file_offset) {
|
||||
_controller->send_player_status();
|
||||
}
|
||||
|
||||
uint32_t Player::_id3_tag_offset(File f) {
|
||||
uint32_t original_position = f.position();
|
||||
uint32_t offset = 0;
|
||||
if (f.read()=='I' && f.read()=='D' && f.read()=='3') {
|
||||
DEBUG("ID3 tag found\n");
|
||||
// Skip ID3 tag version
|
||||
f.read(); f.read();
|
||||
byte tags = f.read();
|
||||
bool footer_present = tags & 0x10;
|
||||
DEBUG("ID3 footer found: %d\n", footer_present);
|
||||
for (byte i=0; i<4; i++) {
|
||||
offset <<= 7;
|
||||
offset |= (0x7F & f.read());
|
||||
}
|
||||
offset += 10;
|
||||
if (footer_present) offset += 10;
|
||||
DEBUG("ID3 tag length is %d bytes.\n", offset);
|
||||
} else {
|
||||
DEBUG("No ID3 tag found\n");
|
||||
}
|
||||
f.seek(original_position);
|
||||
return offset;
|
||||
}
|
||||
|
||||
void Player::_flush(uint count, int8_t byte) {
|
||||
_spi->select_vs1053_xdcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
@ -625,6 +612,7 @@ 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;
|
||||
@ -650,7 +638,8 @@ void Player::_finish_stopping(bool turn_speaker_off) {
|
||||
_state = idle;
|
||||
_stopped_at = millis();
|
||||
if (_file) {
|
||||
_file.close();
|
||||
_file->close();
|
||||
delete _file;
|
||||
}
|
||||
_current_play_position = 0;
|
||||
_file_size = 0;
|
||||
@ -662,7 +651,7 @@ void Player::_refill() {
|
||||
_spi->select_sd();
|
||||
_refills++;
|
||||
if (_refills % 1000 == 0) DEBUG(".");
|
||||
uint8_t result = _file.read(_buffer, sizeof(_buffer));
|
||||
uint8_t result = _file->read(_buffer, sizeof(_buffer));
|
||||
_spi->select_sd(false);
|
||||
if (result == 0) {
|
||||
// File is over.
|
||||
@ -684,13 +673,13 @@ void Player::_refill() {
|
||||
_write_data(_buffer);
|
||||
|
||||
if (_skip_to > 0) {
|
||||
if (_skip_to > _file.position()) {
|
||||
if (_skip_to > _file->position()) {
|
||||
uint16_t status = _read_control_register(SCI_STATUS);
|
||||
if ((status & SS_DO_NOT_JUMP) == 0) {
|
||||
DEBUG("Skipping to %d.\n", _skip_to);
|
||||
_flush(2048, _get_endbyte());
|
||||
_spi->select_sd();
|
||||
_file.seek(_skip_to);
|
||||
_file->seek(_skip_to);
|
||||
_spi->select_sd(false);
|
||||
_skip_to = 0;
|
||||
_unmute();
|
||||
@ -755,6 +744,7 @@ String Player::json() {
|
||||
}
|
||||
|
||||
String Player::position_json() {
|
||||
if (!is_playing()) return "null";
|
||||
DynamicJsonDocument json(200);
|
||||
json["_type"] = "position";
|
||||
json["position"] = _current_play_position;
|
||||
|
260
src/playlist.cpp
260
src/playlist.cpp
@ -4,9 +4,20 @@
|
||||
#include <SD.h>
|
||||
#include <algorithm>
|
||||
#include <ArduinoJson.h>
|
||||
#include <TinyXML.h>
|
||||
|
||||
Playlist::Playlist(String path) {
|
||||
// Add files to _files
|
||||
_path = path;
|
||||
if (path.startsWith("/")) {
|
||||
persistence = PERSIST_TEMPORARY;
|
||||
_add_path(path);
|
||||
} else if (path.startsWith("http")) {
|
||||
_examine_http_url(path);
|
||||
}
|
||||
if (_title.length()==0) _title=path;
|
||||
}
|
||||
|
||||
void Playlist::_add_path(String path) {
|
||||
SPIMaster::select_sd();
|
||||
TRACE("Examining folder %s...\n", path.c_str());
|
||||
if (!path.startsWith("/")) path = String("/") + path;
|
||||
@ -15,6 +26,11 @@ Playlist::Playlist(String path) {
|
||||
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()) {
|
||||
@ -29,7 +45,8 @@ Playlist::Playlist(String path) {
|
||||
ext.equals(".mp4") ||
|
||||
ext.equals(".mpa"))) {
|
||||
TRACE(" Adding entry %s\n", entry.name());
|
||||
_files.push_back(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);
|
||||
@ -51,6 +68,195 @@ Playlist::Playlist(String path) {
|
||||
std::sort(_files.begin(), _files.end());
|
||||
}
|
||||
|
||||
void Playlist::_examine_http_url(String url) {
|
||||
HTTPClientWrapper* http = new HTTPClientWrapper();
|
||||
if (!http->get(url)) {
|
||||
DEBUG("Could not GET %s.\n", url.c_str());
|
||||
return;
|
||||
}
|
||||
String ct = http->getContentType();
|
||||
DEBUG("Content-Type is %s.\n", ct.c_str());
|
||||
if (ct.startsWith("audio/x-mpegurl")) {
|
||||
_parse_m3u(http);
|
||||
} else if (ct.startsWith("audio/")) {
|
||||
persistence = PERSIST_NONE;
|
||||
_files.push_back({.filename=url, .title=url, .id="none"});
|
||||
} else if (ct.startsWith("application/rss+xml") || ct.startsWith("application/xml")) {
|
||||
persistence = PERSIST_PERMANENTLY;
|
||||
_parse_rss(http);
|
||||
} else if (ct.startsWith("application/pls+xml")) {
|
||||
persistence = PERSIST_PERMANENTLY;
|
||||
_parse_pls(http);
|
||||
} else {
|
||||
ERROR("Unknown content type %s.\n", ct.c_str());
|
||||
}
|
||||
http->close();
|
||||
delete http;
|
||||
}
|
||||
|
||||
std::vector<PlaylistEntry>* xml_files_ptr = NULL;
|
||||
String xml_last_tag = "";
|
||||
String xml_title = "";
|
||||
String xml_album_title = "";
|
||||
String xml_url = "";
|
||||
String xml_enclosure_url = "";
|
||||
String xml_guid = "";
|
||||
bool xml_enclosure_is_audio = false;
|
||||
|
||||
void xmlcb(uint8_t status, char* tagName, uint16_t tagLen, char* data, uint16_t dataLen) {
|
||||
String tag(tagName);
|
||||
if (status & STATUS_START_TAG) xml_last_tag = tag;
|
||||
|
||||
if (trace_enabled) {
|
||||
if (status & STATUS_START_TAG) {
|
||||
TRACE("Start of tag: %s\n", tagName);
|
||||
} else if (status & STATUS_END_TAG) {
|
||||
TRACE("End of tag: %s\n", tagName);
|
||||
}
|
||||
}
|
||||
|
||||
if (tag.equals("/rss/channel/title") && (status & STATUS_TAG_TEXT)) {
|
||||
xml_album_title = data;
|
||||
} else if (tag.endsWith("/title") && (status & STATUS_TAG_TEXT)) {
|
||||
xml_title = String(data);
|
||||
} else if (tag.endsWith("/guid") && (status & STATUS_TAG_TEXT)) {
|
||||
xml_guid = data;
|
||||
//} else if (xml_last_tag.endsWith("/item/enclosure") && (status & STATUS_ATTR_TEXT)) {
|
||||
// DEBUG("tag: %s, data: %s\n", tag.c_str(), data);
|
||||
} else if (xml_last_tag.endsWith("/enclosure") && tag.equals("type") && (status & STATUS_ATTR_TEXT) && String(data).indexOf("audio/")>=0) {
|
||||
DEBUG("enclosure is audio\n");
|
||||
xml_enclosure_is_audio = true;
|
||||
} else if (xml_last_tag.endsWith("/enclosure") && tag.equals("url") && (status & STATUS_ATTR_TEXT)) {
|
||||
DEBUG("found url\n");
|
||||
xml_enclosure_url = String(data);
|
||||
} else if (tag.endsWith("/enclosure") && (status & STATUS_END_TAG)) {
|
||||
DEBUG("end of enclosure. xml_enclosure_is_audio: %d, xml_enclosure_url: %s\n", xml_enclosure_is_audio, xml_enclosure_url.c_str());
|
||||
if (xml_enclosure_is_audio && xml_enclosure_url.length()>0) {
|
||||
xml_url = xml_enclosure_url;
|
||||
}
|
||||
xml_enclosure_is_audio = false;
|
||||
xml_enclosure_url = "";
|
||||
} else if (tag.endsWith("/item") && (status & STATUS_END_TAG || status & STATUS_START_TAG)) {
|
||||
if (xml_title.length()>0 && xml_url.length()>0) {
|
||||
if (xml_files_ptr->size() > 20) return;
|
||||
DEBUG("Adding playlist entry: '%s' => '%s'\n", xml_title.c_str(), xml_url.c_str());
|
||||
xml_files_ptr->insert(xml_files_ptr->begin(), {.filename=xml_url, .title=xml_title, .id=xml_guid});
|
||||
}
|
||||
xml_title = "";
|
||||
xml_url = "";
|
||||
xml_guid = "";
|
||||
}
|
||||
}
|
||||
|
||||
void Playlist::_parse_rss(HTTPClientWrapper* http) {
|
||||
DEBUG("RSS parser running.\n");
|
||||
// http is already initialized
|
||||
int i;
|
||||
|
||||
TinyXML xml;
|
||||
uint8_t* buffer = new uint8_t[150];
|
||||
xml.init(buffer, 150, &xmlcb);
|
||||
xml_files_ptr = &_files;
|
||||
xml_title = "";
|
||||
xml_album_title = "";
|
||||
xml_url = "";
|
||||
xml_enclosure_is_audio = false;
|
||||
xml_enclosure_url = "";
|
||||
while ((i = http->read()) >= 0) {
|
||||
xml.processChar(i);
|
||||
}
|
||||
_current_track = _files.size()-1;
|
||||
xml_files_ptr = NULL;
|
||||
if (xml_album_title.length()>0) {
|
||||
_title = xml_album_title;
|
||||
}
|
||||
xml_album_title = "";
|
||||
delete buffer;
|
||||
// don't close http at the end
|
||||
DEBUG("RSS parser finished.\n");
|
||||
}
|
||||
|
||||
void Playlist::_parse_m3u(HTTPClientWrapper* http) {
|
||||
// http is already initialized
|
||||
String line = "";
|
||||
String title = "";
|
||||
int i;
|
||||
do {
|
||||
i = http->read();
|
||||
char c = i;
|
||||
if (i>=0 && c!='\r' && c!='\n') {
|
||||
line += c;
|
||||
} else {
|
||||
if (line.equals("#EXTM3U")) {
|
||||
// Do nothing
|
||||
} else if (line.startsWith("#EXTINF")) {
|
||||
int idx = line.indexOf(",");
|
||||
if (idx>4) {
|
||||
// Get the title
|
||||
title = line.substring(idx+1);
|
||||
if (_title.length()==0) _title=title;
|
||||
}
|
||||
} else if (line.startsWith("http")) {
|
||||
if (title.length()==0) title = line;
|
||||
_files.push_back({.filename=line, .title=title, .id="none"});
|
||||
title = "";
|
||||
}
|
||||
line = "";
|
||||
}
|
||||
} while (i>=0);
|
||||
// don't close http at the end
|
||||
}
|
||||
|
||||
void Playlist::_parse_pls(HTTPClientWrapper* http) {
|
||||
// http is already initialized
|
||||
String line;
|
||||
String title = "";
|
||||
String url = "";
|
||||
int last_index = -1;
|
||||
int index;
|
||||
|
||||
while(true) {
|
||||
line = http->readLine();
|
||||
if (line.startsWith("Title")) {
|
||||
uint8_t eq_idx = line.indexOf('=');
|
||||
if (eq_idx==-1) continue;
|
||||
|
||||
index = line.substring(5, eq_idx-4).toInt();
|
||||
title = line.substring(eq_idx+1);
|
||||
if (index != last_index) {
|
||||
url = "";
|
||||
last_index = index;
|
||||
}
|
||||
} else if (line.startsWith("File")) {
|
||||
uint8_t eq_idx = line.indexOf('=');
|
||||
if (eq_idx==-1) continue;
|
||||
|
||||
index = line.substring(5, eq_idx-4).toInt();
|
||||
url = line.substring(eq_idx+1);
|
||||
if (index != last_index) {
|
||||
title = "";
|
||||
last_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
if (title.length()>0 && url.length()>0) {
|
||||
_files.push_back({.filename=url, .title=title, .id="none"});
|
||||
last_index = -1;
|
||||
title = "";
|
||||
url = "";
|
||||
}
|
||||
}
|
||||
// don't close http at the end
|
||||
}
|
||||
|
||||
String Playlist::path() {
|
||||
return _path;
|
||||
}
|
||||
|
||||
uint16_t Playlist::get_file_count() {
|
||||
return _files.size();
|
||||
}
|
||||
|
||||
void Playlist::start() {
|
||||
_started = true;
|
||||
}
|
||||
@ -90,6 +296,15 @@ bool Playlist::set_track(uint8_t track) {
|
||||
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;
|
||||
}
|
||||
@ -100,7 +315,7 @@ void Playlist::shuffle(uint8_t random_offset) {
|
||||
int j = random(random_offset, _files.size()-1);
|
||||
if (i!=j) {
|
||||
TRACE(" Swapping elements %d and %d.\n", i, j);
|
||||
String temp = _files[i];
|
||||
PlaylistEntry temp = _files[i];
|
||||
_files[i] = _files[j];
|
||||
_files[j] = temp;
|
||||
}
|
||||
@ -110,12 +325,19 @@ void Playlist::shuffle(uint8_t random_offset) {
|
||||
}
|
||||
|
||||
void Playlist::advent_shuffle(uint8_t day) {
|
||||
if (day > 24) day = 24;
|
||||
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;
|
||||
|
||||
if (day > _files.size()) return;
|
||||
|
||||
_files.insert(_files.begin(), _files[day - 1]);
|
||||
_files.erase(_files.begin() + day - 1, _files.end());
|
||||
_files.erase(_files.begin() + day, _files.end());
|
||||
}
|
||||
|
||||
void Playlist::reset() {
|
||||
@ -126,8 +348,20 @@ void Playlist::reset() {
|
||||
_started = false;
|
||||
}
|
||||
|
||||
String Playlist::get_current_file() {
|
||||
return _files[_current_track];
|
||||
String Playlist::get_current_track_id() {
|
||||
if (_current_track > _files.size()) {
|
||||
return "";
|
||||
}
|
||||
return _files[_current_track].id;
|
||||
}
|
||||
|
||||
bool Playlist::get_current_file(String* dst) {
|
||||
if (_current_track > _files.size()) {
|
||||
return false;
|
||||
} else {
|
||||
dst->concat(_files[_current_track].filename);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Playlist::get_position() {
|
||||
@ -144,15 +378,19 @@ bool Playlist::is_fresh() {
|
||||
|
||||
void Playlist::dump() {
|
||||
for (int i=0; i<_files.size(); i++) {
|
||||
DEBUG(" %02d %2s %s\n", i+1, (i==_current_track) ? "->" : "", _files[i].c_str());
|
||||
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 (String file: _files) {
|
||||
files.add(file);
|
||||
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();
|
||||
|
@ -40,15 +40,17 @@ void PlaylistManager::scan_files() {
|
||||
TRACE(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
|
||||
_map[rfid_id] = folder;
|
||||
|
||||
bool found=false;
|
||||
for (String f: folders) {
|
||||
if (f.equals(folder)) {
|
||||
found = true;
|
||||
break;
|
||||
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());
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,10 +104,40 @@ Playlist* PlaylistManager::get_playlist_for_id(String id) {
|
||||
}
|
||||
|
||||
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
|
||||
Playlist* p;
|
||||
if (!_playlists.count(folder)) {
|
||||
_playlists[folder] = new Playlist(folder);
|
||||
p = new Playlist(folder);
|
||||
_playlists[folder] = p;
|
||||
if (p->persistence == PERSIST_PERMANENTLY) {
|
||||
String search = folder;
|
||||
search += "=";
|
||||
SPIMaster::select_sd();
|
||||
if (SD.exists("/_positions.txt")) {
|
||||
File f = SD.open("/_positions.txt", "r");
|
||||
while (true) {
|
||||
String s = f.readStringUntil('\n');
|
||||
if (s.length()==0) break;
|
||||
if (s.startsWith(search)) {
|
||||
s = s.substring(search.length());
|
||||
int idx = s.indexOf(',');
|
||||
String title_index = s.substring(0, idx);
|
||||
uint32_t position = s.substring(idx+1).toInt();
|
||||
p->set_track_by_id(title_index);
|
||||
p->set_position(position);
|
||||
break;
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
SPIMaster::select_sd(false);
|
||||
}
|
||||
} else {
|
||||
p = _playlists[folder];
|
||||
if (p->persistence == PERSIST_NONE) {
|
||||
p->reset();
|
||||
}
|
||||
}
|
||||
return _playlists[folder];
|
||||
return p;
|
||||
}
|
||||
|
||||
void PlaylistManager::dump_ids() {
|
||||
@ -143,13 +175,7 @@ bool PlaylistManager::add_mapping(String id, String folder) {
|
||||
void PlaylistManager::_save_mapping() {
|
||||
SPIMaster::select_sd();
|
||||
File f = SD.open("/_mapping.txt", "w");
|
||||
String s;
|
||||
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) {
|
||||
s += it->first;
|
||||
s += "=";
|
||||
s += it->second;
|
||||
s += '\n';
|
||||
}
|
||||
String s = create_mapping_txt();
|
||||
unsigned char* buf = new unsigned char[s.length()];
|
||||
s.getBytes(buf, s.length());
|
||||
f.write(buf, s.length()-1);
|
||||
@ -157,3 +183,57 @@ void PlaylistManager::_save_mapping() {
|
||||
SPIMaster::select_sd(false);
|
||||
delete buf;
|
||||
}
|
||||
|
||||
String PlaylistManager::create_mapping_txt() {
|
||||
String s;
|
||||
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) {
|
||||
s += it->first;
|
||||
s += "=";
|
||||
s += it->second;
|
||||
s += '\n';
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
void PlaylistManager::persist(Playlist* p) {
|
||||
if (p->persistence == PERSIST_NONE) {
|
||||
_playlists.erase(p->path());
|
||||
return;
|
||||
} else if (p->persistence == PERSIST_PERMANENTLY) {
|
||||
|
||||
String search = p->path();
|
||||
search += '=';
|
||||
|
||||
bool old_file_existed = false;
|
||||
|
||||
SPIMaster::select_sd();
|
||||
if (SD.exists("_positions.txt")) {
|
||||
SD.rename("/_positions.txt", "/_positions.temp.txt");
|
||||
old_file_existed = true;
|
||||
}
|
||||
File dst = SD.open("/_positions.txt", "w");
|
||||
|
||||
if (old_file_existed) {
|
||||
File src = SD.open("/_positions.temp.txt", "r");
|
||||
|
||||
while (true) {
|
||||
String line = src.readStringUntil('\n');
|
||||
line.trim();
|
||||
if (line.startsWith(search)) continue;
|
||||
dst.println(line);
|
||||
}
|
||||
|
||||
src.close();
|
||||
SD.remove("/_positions.temp.txt");
|
||||
}
|
||||
|
||||
dst.print(search);
|
||||
dst.print(p->get_current_track_id());
|
||||
dst.print(',');
|
||||
dst.println(p->get_position());
|
||||
dst.close();
|
||||
SPIMaster::select_sd(false);
|
||||
|
||||
_playlists.erase(p->path());
|
||||
}
|
||||
}
|
||||
|
96
src/updater.cpp
Normal file
96
src/updater.cpp
Normal file
@ -0,0 +1,96 @@
|
||||
#include <Arduino.h>
|
||||
#include <Update.h>
|
||||
#include "config.h"
|
||||
#include "updater.h"
|
||||
#include "http_client_wrapper.h"
|
||||
|
||||
void Updater::run() {
|
||||
DEBUG("Updater is running...\n");
|
||||
HTTPClientWrapper* http = new HTTPClientWrapper();
|
||||
DEBUG("Requesting update info...\n");
|
||||
bool result = http->get(OTA_UPDATE_URL);
|
||||
if (!result) {
|
||||
ERROR("Updater failed requesting %s.\n", OTA_UPDATE_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
String line_str = "";
|
||||
if (!read_line(&line_str, http, "VERSION")) {
|
||||
return;
|
||||
}
|
||||
uint16_t version = line_str.toInt();
|
||||
if (version==0) {
|
||||
ERROR("Could not parse version number.\n");
|
||||
return;
|
||||
}
|
||||
DEBUG("Found version %d. My version is %d.\n", version, OTA_VERSION);
|
||||
if (version <= OTA_VERSION) {
|
||||
return;
|
||||
}
|
||||
|
||||
String image_path = "";
|
||||
if (!read_line(&image_path, http, "IMAGE_PATH")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String image_md5 = "";
|
||||
if (!read_line(&image_md5, http, "IMAGE_MD5")) {
|
||||
return;
|
||||
}
|
||||
|
||||
http->close();
|
||||
delete http;
|
||||
|
||||
if(do_update(U_FLASH, image_path, image_md5)) {
|
||||
DEBUG("Update done. Rebooting...\n");
|
||||
} else {
|
||||
DEBUG("Update failed. Rebooting...\n");
|
||||
}
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
bool Updater::read_line(String* dst, HTTPClientWrapper* http, String expected_key) {
|
||||
expected_key += "=";
|
||||
String line = http->readUntil("\n");
|
||||
if (!line.startsWith(expected_key)) {
|
||||
ERROR("Expected line start with '%s', but it started with '%s'.\n", expected_key.c_str(), line.c_str());
|
||||
return false;
|
||||
}
|
||||
line = line.substring(expected_key.length());
|
||||
line.trim();
|
||||
dst->concat(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Updater::do_update(int command, String url, String expected_md5) {
|
||||
HTTPClientWrapper* http = new HTTPClientWrapper();
|
||||
bool result = http->get(url);
|
||||
if (!result) {
|
||||
ERROR("Updater failed requesting %s.\n", url.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
result = Update.begin(http->getSize(), command);
|
||||
if (!result) {
|
||||
ERROR("Update could not be started.\n");
|
||||
return false;
|
||||
}
|
||||
Update.setMD5(expected_md5.c_str());
|
||||
uint8_t buf[512];
|
||||
uint16_t len;
|
||||
while((len = http->read(buf, 512))) {
|
||||
Update.write(buf, len);
|
||||
}
|
||||
|
||||
http->close();
|
||||
delete http;
|
||||
|
||||
result = Update.end();
|
||||
if (!result) {
|
||||
const char* error = Update.errorString();
|
||||
ERROR("Writing the update failed. The error was: %s\n", error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
@ -9,9 +9,24 @@
|
||||
<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">
|
||||
@ -94,6 +109,20 @@
|
||||
</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>
|
||||
@ -137,6 +166,7 @@
|
||||
<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">
|
||||
@ -173,7 +203,7 @@ update_playlist = function(data) {
|
||||
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].substr(data.files[i].lastIndexOf('/')+1)));
|
||||
tr.append($('<td>').html(data.files[i].title));
|
||||
$('#track_list').append(tr);
|
||||
}
|
||||
|
||||
@ -189,12 +219,10 @@ update_playlist = function(data) {
|
||||
$('#button_track_prev').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||
}
|
||||
|
||||
$('#album').html(data.title);
|
||||
var file = data.files[data.current_track];
|
||||
if (file) {
|
||||
file = file.substr(1);
|
||||
$('#album').html(file.substr(0, file.indexOf('/')));
|
||||
file = file.substr(file.indexOf('/')+1);
|
||||
$('#track').html(file.substr(0, file.lastIndexOf('.')));
|
||||
$('#track').html(file.title);
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,6 +277,7 @@ process_ws_message = function(event) {
|
||||
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;
|
||||
@ -259,10 +288,30 @@ process_ws_message = function(event) {
|
||||
}
|
||||
|
||||
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() {
|
||||
ws = new WebSocket("ws://" + location.host + "/ws");
|
||||
ws.onmessage = process_ws_message;
|
||||
start_reconnect_timer();
|
||||
|
||||
$('#volume_slider').change(function(e) { ws.send("volume=" + e.target.value); });
|
||||
$('#button_play').click(function(e) { ws.send("play"); });
|
||||
@ -275,13 +324,36 @@ $(function() {
|
||||
$('#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(); e.stopPropagation(); play_on_click=true; return 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>
|
462
src/webinterface/timezones.json
Normal file
462
src/webinterface/timezones.json
Normal file
@ -0,0 +1,462 @@
|
||||
{"timezones": {
|
||||
"Africa/Abidjan":"GMT0",
|
||||
"Africa/Accra":"GMT0",
|
||||
"Africa/Addis_Ababa":"EAT-3",
|
||||
"Africa/Algiers":"CET-1",
|
||||
"Africa/Asmara":"EAT-3",
|
||||
"Africa/Bamako":"GMT0",
|
||||
"Africa/Bangui":"WAT-1",
|
||||
"Africa/Banjul":"GMT0",
|
||||
"Africa/Bissau":"GMT0",
|
||||
"Africa/Blantyre":"CAT-2",
|
||||
"Africa/Brazzaville":"WAT-1",
|
||||
"Africa/Bujumbura":"CAT-2",
|
||||
"Africa/Cairo":"EET-2",
|
||||
"Africa/Casablanca":"<+01>-1",
|
||||
"Africa/Ceuta":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Africa/Conakry":"GMT0",
|
||||
"Africa/Dakar":"GMT0",
|
||||
"Africa/Dar_es_Salaam":"EAT-3",
|
||||
"Africa/Djibouti":"EAT-3",
|
||||
"Africa/Douala":"WAT-1",
|
||||
"Africa/El_Aaiun":"<+01>-1",
|
||||
"Africa/Freetown":"GMT0",
|
||||
"Africa/Gaborone":"CAT-2",
|
||||
"Africa/Harare":"CAT-2",
|
||||
"Africa/Johannesburg":"SAST-2",
|
||||
"Africa/Juba":"EAT-3",
|
||||
"Africa/Kampala":"EAT-3",
|
||||
"Africa/Khartoum":"CAT-2",
|
||||
"Africa/Kigali":"CAT-2",
|
||||
"Africa/Kinshasa":"WAT-1",
|
||||
"Africa/Lagos":"WAT-1",
|
||||
"Africa/Libreville":"WAT-1",
|
||||
"Africa/Lome":"GMT0",
|
||||
"Africa/Luanda":"WAT-1",
|
||||
"Africa/Lubumbashi":"CAT-2",
|
||||
"Africa/Lusaka":"CAT-2",
|
||||
"Africa/Malabo":"WAT-1",
|
||||
"Africa/Maputo":"CAT-2",
|
||||
"Africa/Maseru":"SAST-2",
|
||||
"Africa/Mbabane":"SAST-2",
|
||||
"Africa/Mogadishu":"EAT-3",
|
||||
"Africa/Monrovia":"GMT0",
|
||||
"Africa/Nairobi":"EAT-3",
|
||||
"Africa/Ndjamena":"WAT-1",
|
||||
"Africa/Niamey":"WAT-1",
|
||||
"Africa/Nouakchott":"GMT0",
|
||||
"Africa/Ouagadougou":"GMT0",
|
||||
"Africa/Porto-Novo":"WAT-1",
|
||||
"Africa/Sao_Tome":"GMT0",
|
||||
"Africa/Tripoli":"EET-2",
|
||||
"Africa/Tunis":"CET-1",
|
||||
"Africa/Windhoek":"CAT-2",
|
||||
"America/Adak":"HST10HDT,M3.2.0,M11.1.0",
|
||||
"America/Anchorage":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Anguilla":"AST4",
|
||||
"America/Antigua":"AST4",
|
||||
"America/Araguaina":"<-03>3",
|
||||
"America/Argentina/Buenos_Aires":"<-03>3",
|
||||
"America/Argentina/Catamarca":"<-03>3",
|
||||
"America/Argentina/Cordoba":"<-03>3",
|
||||
"America/Argentina/Jujuy":"<-03>3",
|
||||
"America/Argentina/La_Rioja":"<-03>3",
|
||||
"America/Argentina/Mendoza":"<-03>3",
|
||||
"America/Argentina/Rio_Gallegos":"<-03>3",
|
||||
"America/Argentina/Salta":"<-03>3",
|
||||
"America/Argentina/San_Juan":"<-03>3",
|
||||
"America/Argentina/San_Luis":"<-03>3",
|
||||
"America/Argentina/Tucuman":"<-03>3",
|
||||
"America/Argentina/Ushuaia":"<-03>3",
|
||||
"America/Aruba":"AST4",
|
||||
"America/Asuncion":"<-04>4<-03>,M10.1.0/0,M3.4.0/0",
|
||||
"America/Atikokan":"EST5",
|
||||
"America/Bahia":"<-03>3",
|
||||
"America/Bahia_Banderas":"CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Barbados":"AST4",
|
||||
"America/Belem":"<-03>3",
|
||||
"America/Belize":"CST6",
|
||||
"America/Blanc-Sablon":"AST4",
|
||||
"America/Boa_Vista":"<-04>4",
|
||||
"America/Bogota":"<-05>5",
|
||||
"America/Boise":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Cambridge_Bay":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Campo_Grande":"<-04>4",
|
||||
"America/Cancun":"EST5",
|
||||
"America/Caracas":"<-04>4",
|
||||
"America/Cayenne":"<-03>3",
|
||||
"America/Cayman":"EST5",
|
||||
"America/Chicago":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Chihuahua":"MST7MDT,M4.1.0,M10.5.0",
|
||||
"America/Costa_Rica":"CST6",
|
||||
"America/Creston":"MST7",
|
||||
"America/Cuiaba":"<-04>4",
|
||||
"America/Curacao":"AST4",
|
||||
"America/Danmarkshavn":"GMT0",
|
||||
"America/Dawson":"PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Dawson_Creek":"MST7",
|
||||
"America/Denver":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Detroit":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Dominica":"AST4",
|
||||
"America/Edmonton":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Eirunepe":"<-05>5",
|
||||
"America/El_Salvador":"CST6",
|
||||
"America/Fortaleza":"<-03>3",
|
||||
"America/Fort_Nelson":"MST7",
|
||||
"America/Glace_Bay":"AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Godthab":"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1",
|
||||
"America/Goose_Bay":"AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Grand_Turk":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Grenada":"AST4",
|
||||
"America/Guadeloupe":"AST4",
|
||||
"America/Guatemala":"CST6",
|
||||
"America/Guayaquil":"<-05>5",
|
||||
"America/Guyana":"<-04>4",
|
||||
"America/Halifax":"AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Havana":"CST5CDT,M3.2.0/0,M11.1.0/1",
|
||||
"America/Hermosillo":"MST7",
|
||||
"America/Indiana/Indianapolis":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Knox":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Marengo":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Petersburg":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Tell_City":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Vevay":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Vincennes":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Winamac":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Inuvik":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Iqaluit":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Jamaica":"EST5",
|
||||
"America/Juneau":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Kentucky/Louisville":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Kentucky/Monticello":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Kralendijk":"AST4",
|
||||
"America/La_Paz":"<-04>4",
|
||||
"America/Lima":"<-05>5",
|
||||
"America/Los_Angeles":"PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Lower_Princes":"AST4",
|
||||
"America/Maceio":"<-03>3",
|
||||
"America/Managua":"CST6",
|
||||
"America/Manaus":"<-04>4",
|
||||
"America/Marigot":"AST4",
|
||||
"America/Martinique":"AST4",
|
||||
"America/Matamoros":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Mazatlan":"MST7MDT,M4.1.0,M10.5.0",
|
||||
"America/Menominee":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Merida":"CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Metlakatla":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Mexico_City":"CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Miquelon":"<-03>3<-02>,M3.2.0,M11.1.0",
|
||||
"America/Moncton":"AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Monterrey":"CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Montevideo":"<-03>3",
|
||||
"America/Montreal":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Montserrat":"AST4",
|
||||
"America/Nassau":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/New_York":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Nipigon":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Nome":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Noronha":"<-02>2",
|
||||
"America/North_Dakota/Beulah":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/North_Dakota/Center":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/North_Dakota/New_Salem":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Ojinaga":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Panama":"EST5",
|
||||
"America/Pangnirtung":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Paramaribo":"<-03>3",
|
||||
"America/Phoenix":"MST7",
|
||||
"America/Port-au-Prince":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Port_of_Spain":"AST4",
|
||||
"America/Porto_Velho":"<-04>4",
|
||||
"America/Puerto_Rico":"AST4",
|
||||
"America/Punta_Arenas":"<-03>3",
|
||||
"America/Rainy_River":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rankin_Inlet":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Recife":"<-03>3",
|
||||
"America/Regina":"CST6",
|
||||
"America/Resolute":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rio_Branco":"<-05>5",
|
||||
"America/Santarem":"<-03>3",
|
||||
"America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24",
|
||||
"America/Santo_Domingo":"AST4",
|
||||
"America/Sao_Paulo":"<-03>3",
|
||||
"America/Scoresbysund":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
|
||||
"America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/St_Barthelemy":"AST4",
|
||||
"America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0",
|
||||
"America/St_Kitts":"AST4",
|
||||
"America/St_Lucia":"AST4",
|
||||
"America/St_Thomas":"AST4",
|
||||
"America/St_Vincent":"AST4",
|
||||
"America/Swift_Current":"CST6",
|
||||
"America/Tegucigalpa":"CST6",
|
||||
"America/Thule":"AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Thunder_Bay":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tijuana":"PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Toronto":"EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tortola":"AST4",
|
||||
"America/Vancouver":"PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Whitehorse":"PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0",
|
||||
"Antarctica/Casey":"<+08>-8",
|
||||
"Antarctica/Davis":"<+07>-7",
|
||||
"Antarctica/DumontDUrville":"<+10>-10",
|
||||
"Antarctica/Macquarie":"<+11>-11",
|
||||
"Antarctica/Mawson":"<+05>-5",
|
||||
"Antarctica/McMurdo":"NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Antarctica/Palmer":"<-03>3",
|
||||
"Antarctica/Rothera":"<-03>3",
|
||||
"Antarctica/Syowa":"<+03>-3",
|
||||
"Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
|
||||
"Antarctica/Vostok":"<+06>-6",
|
||||
"Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Asia/Aden":"<+03>-3",
|
||||
"Asia/Almaty":"<+06>-6",
|
||||
"Asia/Amman":"EET-2EEST,M3.5.4/24,M10.5.5/1",
|
||||
"Asia/Anadyr":"<+12>-12",
|
||||
"Asia/Aqtau":"<+05>-5",
|
||||
"Asia/Aqtobe":"<+05>-5",
|
||||
"Asia/Ashgabat":"<+05>-5",
|
||||
"Asia/Atyrau":"<+05>-5",
|
||||
"Asia/Baghdad":"<+03>-3",
|
||||
"Asia/Bahrain":"<+03>-3",
|
||||
"Asia/Baku":"<+04>-4",
|
||||
"Asia/Bangkok":"<+07>-7",
|
||||
"Asia/Barnaul":"<+07>-7",
|
||||
"Asia/Beirut":"EET-2EEST,M3.5.0/0,M10.5.0/0",
|
||||
"Asia/Bishkek":"<+06>-6",
|
||||
"Asia/Brunei":"<+08>-8",
|
||||
"Asia/Chita":"<+09>-9",
|
||||
"Asia/Choibalsan":"<+08>-8",
|
||||
"Asia/Colombo":"<+0530>-5:30",
|
||||
"Asia/Damascus":"EET-2EEST,M3.5.5/0,M10.5.5/0",
|
||||
"Asia/Dhaka":"<+06>-6",
|
||||
"Asia/Dili":"<+09>-9",
|
||||
"Asia/Dubai":"<+04>-4",
|
||||
"Asia/Dushanbe":"<+05>-5",
|
||||
"Asia/Famagusta":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Asia/Gaza":"EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||
"Asia/Hebron":"EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||
"Asia/Ho_Chi_Minh":"<+07>-7",
|
||||
"Asia/Hong_Kong":"HKT-8",
|
||||
"Asia/Hovd":"<+07>-7",
|
||||
"Asia/Irkutsk":"<+08>-8",
|
||||
"Asia/Jakarta":"WIB-7",
|
||||
"Asia/Jayapura":"WIT-9",
|
||||
"Asia/Jerusalem":"IST-2IDT,M3.4.4/26,M10.5.0",
|
||||
"Asia/Kabul":"<+0430>-4:30",
|
||||
"Asia/Kamchatka":"<+12>-12",
|
||||
"Asia/Karachi":"PKT-5",
|
||||
"Asia/Kathmandu":"<+0545>-5:45",
|
||||
"Asia/Khandyga":"<+09>-9",
|
||||
"Asia/Kolkata":"IST-5:30",
|
||||
"Asia/Krasnoyarsk":"<+07>-7",
|
||||
"Asia/Kuala_Lumpur":"<+08>-8",
|
||||
"Asia/Kuching":"<+08>-8",
|
||||
"Asia/Kuwait":"<+03>-3",
|
||||
"Asia/Macau":"CST-8",
|
||||
"Asia/Magadan":"<+11>-11",
|
||||
"Asia/Makassar":"WITA-8",
|
||||
"Asia/Manila":"PST-8",
|
||||
"Asia/Muscat":"<+04>-4",
|
||||
"Asia/Nicosia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Asia/Novokuznetsk":"<+07>-7",
|
||||
"Asia/Novosibirsk":"<+07>-7",
|
||||
"Asia/Omsk":"<+06>-6",
|
||||
"Asia/Oral":"<+05>-5",
|
||||
"Asia/Phnom_Penh":"<+07>-7",
|
||||
"Asia/Pontianak":"WIB-7",
|
||||
"Asia/Pyongyang":"KST-9",
|
||||
"Asia/Qatar":"<+03>-3",
|
||||
"Asia/Qyzylorda":"<+05>-5",
|
||||
"Asia/Riyadh":"<+03>-3",
|
||||
"Asia/Sakhalin":"<+11>-11",
|
||||
"Asia/Samarkand":"<+05>-5",
|
||||
"Asia/Seoul":"KST-9",
|
||||
"Asia/Shanghai":"CST-8",
|
||||
"Asia/Singapore":"<+08>-8",
|
||||
"Asia/Srednekolymsk":"<+11>-11",
|
||||
"Asia/Taipei":"CST-8",
|
||||
"Asia/Tashkent":"<+05>-5",
|
||||
"Asia/Tbilisi":"<+04>-4",
|
||||
"Asia/Tehran":"<+0330>-3:30<+0430>,J79/24,J263/24",
|
||||
"Asia/Thimphu":"<+06>-6",
|
||||
"Asia/Tokyo":"JST-9",
|
||||
"Asia/Tomsk":"<+07>-7",
|
||||
"Asia/Ulaanbaatar":"<+08>-8",
|
||||
"Asia/Urumqi":"<+06>-6",
|
||||
"Asia/Ust-Nera":"<+10>-10",
|
||||
"Asia/Vientiane":"<+07>-7",
|
||||
"Asia/Vladivostok":"<+10>-10",
|
||||
"Asia/Yakutsk":"<+09>-9",
|
||||
"Asia/Yangon":"<+0630>-6:30",
|
||||
"Asia/Yekaterinburg":"<+05>-5",
|
||||
"Asia/Yerevan":"<+04>-4",
|
||||
"Atlantic/Azores":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
|
||||
"Atlantic/Bermuda":"AST4ADT,M3.2.0,M11.1.0",
|
||||
"Atlantic/Canary":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Cape_Verde":"<-01>1",
|
||||
"Atlantic/Faroe":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Madeira":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Reykjavik":"GMT0",
|
||||
"Atlantic/South_Georgia":"<-02>2",
|
||||
"Atlantic/Stanley":"<-03>3",
|
||||
"Atlantic/St_Helena":"GMT0",
|
||||
"Australia/Adelaide":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Brisbane":"AEST-10",
|
||||
"Australia/Broken_Hill":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Currie":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Darwin":"ACST-9:30",
|
||||
"Australia/Eucla":"<+0845>-8:45",
|
||||
"Australia/Hobart":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Lindeman":"AEST-10",
|
||||
"Australia/Lord_Howe":"<+1030>-10:30<+11>-11,M10.1.0,M4.1.0",
|
||||
"Australia/Melbourne":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Perth":"AWST-8",
|
||||
"Australia/Sydney":"AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Europe/Amsterdam":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Andorra":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Astrakhan":"<+04>-4",
|
||||
"Europe/Athens":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Belgrade":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Berlin":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bratislava":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Brussels":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bucharest":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Budapest":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Busingen":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Chisinau":"EET-2EEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Copenhagen":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Dublin":"IST-1GMT0,M10.5.0,M3.5.0/1",
|
||||
"Europe/Gibraltar":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Guernsey":"GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Helsinki":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Isle_of_Man":"GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Istanbul":"<+03>-3",
|
||||
"Europe/Jersey":"GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Kaliningrad":"EET-2",
|
||||
"Europe/Kiev":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Kirov":"<+03>-3",
|
||||
"Europe/Lisbon":"WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Ljubljana":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/London":"GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Luxembourg":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Madrid":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Malta":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Mariehamn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Minsk":"<+03>-3",
|
||||
"Europe/Monaco":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Moscow":"MSK-3",
|
||||
"Europe/Oslo":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Paris":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Podgorica":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Prague":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Riga":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Rome":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Samara":"<+04>-4",
|
||||
"Europe/San_Marino":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Sarajevo":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Saratov":"<+04>-4",
|
||||
"Europe/Simferopol":"MSK-3",
|
||||
"Europe/Skopje":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Sofia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Stockholm":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Tallinn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Tirane":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Ulyanovsk":"<+04>-4",
|
||||
"Europe/Uzhgorod":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Vaduz":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vatican":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vienna":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vilnius":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Volgograd":"<+04>-4",
|
||||
"Europe/Warsaw":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Zagreb":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Zaporozhye":"EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Zurich":"CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Indian/Antananarivo":"EAT-3",
|
||||
"Indian/Chagos":"<+06>-6",
|
||||
"Indian/Christmas":"<+07>-7",
|
||||
"Indian/Cocos":"<+0630>-6:30",
|
||||
"Indian/Comoro":"EAT-3",
|
||||
"Indian/Kerguelen":"<+05>-5",
|
||||
"Indian/Mahe":"<+04>-4",
|
||||
"Indian/Maldives":"<+05>-5",
|
||||
"Indian/Mauritius":"<+04>-4",
|
||||
"Indian/Mayotte":"EAT-3",
|
||||
"Indian/Reunion":"<+04>-4",
|
||||
"Pacific/Apia":"<+13>-13<+14>,M9.5.0/3,M4.1.0/4",
|
||||
"Pacific/Auckland":"NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Pacific/Bougainville":"<+11>-11",
|
||||
"Pacific/Chatham":"<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45",
|
||||
"Pacific/Chuuk":"<+10>-10",
|
||||
"Pacific/Easter":"<-06>6<-05>,M9.1.6/22,M4.1.6/22",
|
||||
"Pacific/Efate":"<+11>-11",
|
||||
"Pacific/Enderbury":"<+13>-13",
|
||||
"Pacific/Fakaofo":"<+13>-13",
|
||||
"Pacific/Fiji":"<+12>-12<+13>,M11.2.0,M1.2.3/99",
|
||||
"Pacific/Funafuti":"<+12>-12",
|
||||
"Pacific/Galapagos":"<-06>6",
|
||||
"Pacific/Gambier":"<-09>9",
|
||||
"Pacific/Guadalcanal":"<+11>-11",
|
||||
"Pacific/Guam":"ChST-10",
|
||||
"Pacific/Honolulu":"HST10",
|
||||
"Pacific/Kiritimati":"<+14>-14",
|
||||
"Pacific/Kosrae":"<+11>-11",
|
||||
"Pacific/Kwajalein":"<+12>-12",
|
||||
"Pacific/Majuro":"<+12>-12",
|
||||
"Pacific/Marquesas":"<-0930>9:30",
|
||||
"Pacific/Midway":"SST11",
|
||||
"Pacific/Nauru":"<+12>-12",
|
||||
"Pacific/Niue":"<-11>11",
|
||||
"Pacific/Norfolk":"<+11>-11<+12>,M10.1.0,M4.1.0/3",
|
||||
"Pacific/Noumea":"<+11>-11",
|
||||
"Pacific/Pago_Pago":"SST11",
|
||||
"Pacific/Palau":"<+09>-9",
|
||||
"Pacific/Pitcairn":"<-08>8",
|
||||
"Pacific/Pohnpei":"<+11>-11",
|
||||
"Pacific/Port_Moresby":"<+10>-10",
|
||||
"Pacific/Rarotonga":"<-10>10",
|
||||
"Pacific/Saipan":"ChST-10",
|
||||
"Pacific/Tahiti":"<-10>10",
|
||||
"Pacific/Tarawa":"<+12>-12",
|
||||
"Pacific/Tongatapu":"<+13>-13",
|
||||
"Pacific/Wake":"<+12>-12",
|
||||
"Pacific/Wallis":"<+12>-12",
|
||||
"Etc/GMT":"GMT0",
|
||||
"Etc/GMT-0":"GMT0",
|
||||
"Etc/GMT-1":"<+01>-1",
|
||||
"Etc/GMT-2":"<+02>-2",
|
||||
"Etc/GMT-3":"<+03>-3",
|
||||
"Etc/GMT-4":"<+04>-4",
|
||||
"Etc/GMT-5":"<+05>-5",
|
||||
"Etc/GMT-6":"<+06>-6",
|
||||
"Etc/GMT-7":"<+07>-7",
|
||||
"Etc/GMT-8":"<+08>-8",
|
||||
"Etc/GMT-9":"<+09>-9",
|
||||
"Etc/GMT-10":"<+10>-10",
|
||||
"Etc/GMT-11":"<+11>-11",
|
||||
"Etc/GMT-12":"<+12>-12",
|
||||
"Etc/GMT-13":"<+13>-13",
|
||||
"Etc/GMT-14":"<+14>-14",
|
||||
"Etc/GMT0":"GMT0",
|
||||
"Etc/GMT+0":"GMT0",
|
||||
"Etc/GMT+1":"<-01>1",
|
||||
"Etc/GMT+2":"<-02>2",
|
||||
"Etc/GMT+3":"<-03>3",
|
||||
"Etc/GMT+4":"<-04>4",
|
||||
"Etc/GMT+5":"<-05>5",
|
||||
"Etc/GMT+6":"<-06>6",
|
||||
"Etc/GMT+7":"<-07>7",
|
||||
"Etc/GMT+8":"<-08>8",
|
||||
"Etc/GMT+9":"<-09>9",
|
||||
"Etc/GMT+10":"<-10>10",
|
||||
"Etc/GMT+11":"<-11>11",
|
||||
"Etc/GMT+12":"<-12>12",
|
||||
"Etc/UCT":"UTC0",
|
||||
"Etc/UTC":"UTC0",
|
||||
"Etc/Greenwich":"GMT0",
|
||||
"Etc/Universal":"UTC0",
|
||||
"Etc/Zulu":"UTC0"
|
||||
}}
|
14
tools/create_tz_json.sh
Normal file
14
tools/create_tz_json.sh
Normal file
@ -0,0 +1,14 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
URL="https://raw.githubusercontent.com/nayarsystems/posix_tz_db/master/zones.csv"
|
||||
(
|
||||
first=1
|
||||
echo "{\"timezones\": {"
|
||||
curl --silent "$URL" | while read line; do
|
||||
[ $first -ne 1 ] && echo ","
|
||||
first=0
|
||||
echo -n "${line/\",\"/\":\"}"
|
||||
done
|
||||
echo
|
||||
echo "}}"
|
||||
) > src/webinterface/timezones.json
|
9
tools/post_build.py
Normal file
9
tools/post_build.py
Normal file
@ -0,0 +1,9 @@
|
||||
Import("env")
|
||||
|
||||
env.Execute("gzip -9 < src/webinterface/index.html > src/webinterface/index.html.gz")
|
||||
env.Execute("gzip -9 < src/webinterface/timezones.json > src/webinterface/timezones.json.gz")
|
||||
|
||||
def build(source, target, env):
|
||||
env.Execute("rm src/webinterface/index.html.gz src/webinterface/timezones.json.gz")
|
||||
|
||||
env.AddPostAction("buildprog", build)
|
Reference in New Issue
Block a user