Compare commits
	
		
			48 Commits
		
	
	
		
			710b8a2cdc
			...
			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 | 
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								README.md
									
									
									
									
									
								
							| @@ -145,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 | 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" | 24 followed by tracks 1-23. So your kid will get the "daily track" | ||||||
| first, followed by all previous tags in the right order. | 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 | #pragma once | ||||||
| #include <Arduino.h> | #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_DEBUG | ||||||
| //#define SHOW_TRACE | //#define SHOW_TRACE | ||||||
| #define FTP_DEBUG | #define FTP_DEBUG | ||||||
| #define DELAY_AFTER_DEBUG_AND_TRACE 0 | #define DELAY_AFTER_DEBUG_AND_TRACE 0 | ||||||
|  |  | ||||||
| #define WIFI_SSID "---CHANGEME---" | // Here you can define WiFi data to use. But actually, the better way to do | ||||||
| #define WIFI_PASS "---CHANGEME---" | // this is by using /_wifi.txt on the sd card. | ||||||
|  | //#define WIFI_SSID "---CHANGEME---" | ||||||
|  | //#define WIFI_PASS "---CHANGEME---" | ||||||
|  |  | ||||||
| #define VS1053_SLEEP_DELAY 5000 | #define VS1053_SLEEP_DELAY 5000 | ||||||
| #define POSITION_SEND_INTERVAL 5000 | #define POSITION_SEND_INTERVAL 5000 | ||||||
|   | |||||||
| @@ -10,6 +10,8 @@ class Controller; | |||||||
| #include "playlist.h" | #include "playlist.h" | ||||||
| #include "playlist_manager.h" | #include "playlist_manager.h" | ||||||
| #include "http_server.h" | #include "http_server.h" | ||||||
|  |  | ||||||
|  | #undef DEPRECATED | ||||||
| #include <MFRC522.h> | #include <MFRC522.h> | ||||||
|  |  | ||||||
| enum ControllerState { NORMAL, LOCKING, LOCKED }; | enum ControllerState { NORMAL, LOCKING, LOCKED }; | ||||||
| @@ -32,6 +34,8 @@ private: | |||||||
| 	 | 	 | ||||||
| 	unsigned long _last_rfid_scan_at = 0; | 	unsigned long _last_rfid_scan_at = 0; | ||||||
| 	unsigned long _last_position_info_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 _serial_buffer = String(); | ||||||
| 	String _cmd_queue = ""; | 	String _cmd_queue = ""; | ||||||
| 	void _execute_command_ls(String path); | 	void _execute_command_ls(String path); | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| #include <Arduino.h> | #include <Arduino.h> | ||||||
| #include <SD.h> | #include <SD.h> | ||||||
| #include "config.h" | #include "config.h" | ||||||
| #include <HTTPClient.h> | #include "http_client_wrapper.h" | ||||||
|  |  | ||||||
| class DataSource { | class DataSource { | ||||||
| private: | private: | ||||||
| @@ -11,13 +11,14 @@ public: | |||||||
| 	DataSource() {}; | 	DataSource() {}; | ||||||
| 	virtual ~DataSource() {}; | 	virtual ~DataSource() {}; | ||||||
| 	virtual size_t read(uint8_t* buf, size_t len) = 0; | 	virtual size_t read(uint8_t* buf, size_t len) = 0; | ||||||
| 	virtual uint8_t read() = 0; | 	virtual int read() = 0; | ||||||
| 	virtual size_t position() = 0; | 	virtual size_t position() = 0; | ||||||
| 	virtual void seek(size_t position) = 0; | 	virtual void seek(size_t position) = 0; | ||||||
| 	virtual size_t size() = 0; | 	virtual size_t size() = 0; | ||||||
| 	virtual void close() = 0; | 	virtual void close() = 0; | ||||||
| 	virtual void skip_id3_tag() {}; |  | ||||||
| 	virtual bool usable() = 0; | 	virtual bool usable() = 0; | ||||||
|  | 	virtual int peek(int offset) = 0; | ||||||
|  | 	void skip_id3_tag(); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class SDDataSource : public DataSource { | class SDDataSource : public DataSource { | ||||||
| @@ -27,29 +28,31 @@ public: | |||||||
| 	SDDataSource(String file); | 	SDDataSource(String file); | ||||||
| 	~SDDataSource(); | 	~SDDataSource(); | ||||||
| 	size_t read(uint8_t* buf, size_t len); | 	size_t read(uint8_t* buf, size_t len); | ||||||
| 	uint8_t read(); | 	int read(); | ||||||
| 	size_t position(); | 	size_t position(); | ||||||
| 	void seek(size_t position); | 	void seek(size_t position); | ||||||
| 	size_t size(); | 	size_t size(); | ||||||
| 	void close(); | 	void close(); | ||||||
| 	void skip_id3_tag(); |  | ||||||
| 	bool usable(); | 	bool usable(); | ||||||
|  | 	int peek(int offset=0); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class HTTPSDataSource : public DataSource { | class HTTPSDataSource : public DataSource { | ||||||
| private: | private: | ||||||
| 	WiFiClient* _stream = NULL; | 	WiFiClient* _stream = NULL; | ||||||
| 	HTTPClient* _http = NULL; | 	HTTPClientWrapper* _http = NULL; | ||||||
| 	uint32_t _length; |  | ||||||
| 	uint32_t _position; | 	uint32_t _position; | ||||||
|  | 	String _url; | ||||||
|  | 	void _init(String url, uint32_t offset); | ||||||
| public: | public: | ||||||
| 	HTTPSDataSource(String url, uint32_t offset=0); | 	HTTPSDataSource(String url, uint32_t offset=0); | ||||||
| 	~HTTPSDataSource(); | 	~HTTPSDataSource(); | ||||||
| 	size_t read(uint8_t* buf, size_t len); | 	size_t read(uint8_t* buf, size_t len); | ||||||
| 	uint8_t read(); | 	int read(); | ||||||
| 	size_t position(); | 	size_t position(); | ||||||
| 	void seek(size_t position); | 	void seek(size_t position); | ||||||
| 	size_t size(); | 	size_t size(); | ||||||
| 	void close(); | 	void close(); | ||||||
| 	bool usable(); | 	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"); | ||||||
| @@ -2,6 +2,22 @@ | |||||||
| #include <Arduino.h> | #include <Arduino.h> | ||||||
| #include <vector> | #include <vector> | ||||||
| #include <ArduinoJson.h> | #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 { | class Playlist { | ||||||
| private: | private: | ||||||
| @@ -9,19 +25,31 @@ private: | |||||||
| 	uint32_t _current_track = 0; | 	uint32_t _current_track = 0; | ||||||
| 	bool _started = false; | 	bool _started = false; | ||||||
| 	bool _shuffled = 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: | public: | ||||||
| 	Playlist(String path, bool is_url=false); | 	PlaylistPersistence persistence = PERSIST_TEMPORARY; | ||||||
|  | 	Playlist(String path); | ||||||
| 	void start(); | 	void start(); | ||||||
|  | 	uint16_t get_file_count(); | ||||||
| 	bool has_track_next(); | 	bool has_track_next(); | ||||||
| 	bool has_track_prev(); | 	bool has_track_prev(); | ||||||
| 	bool track_next(); | 	bool track_next(); | ||||||
| 	bool track_prev(); | 	bool track_prev(); | ||||||
| 	void track_restart(); | 	void track_restart(); | ||||||
| 	bool set_track(uint8_t track); | 	bool set_track(uint8_t track); | ||||||
|  | 	void set_track_by_id(String id); | ||||||
| 	void reset(); | 	void reset(); | ||||||
|  | 	String path(); | ||||||
| 	bool is_empty(); | 	bool is_empty(); | ||||||
| 	String get_current_file(); | 	bool get_current_file(String* dst); | ||||||
|  | 	String get_current_track_id(); | ||||||
| 	uint32_t get_position(); | 	uint32_t get_position(); | ||||||
| 	void set_position(uint32_t p); | 	void set_position(uint32_t p); | ||||||
| 	void shuffle(uint8_t random_offset=0); | 	void shuffle(uint8_t random_offset=0); | ||||||
|   | |||||||
| @@ -20,4 +20,5 @@ public: | |||||||
| 	String json(); | 	String json(); | ||||||
| 	bool add_mapping(String id, String folder); | 	bool add_mapping(String id, String folder); | ||||||
| 	String create_mapping_txt(); | 	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 | ; Please visit documentation for the other options and examples | ||||||
| ; https://docs.platformio.org/page/projectconf.html | ; https://docs.platformio.org/page/projectconf.html | ||||||
|  |  | ||||||
| [env:esp32] | [platformio] | ||||||
|  | default_envs = esp32 | ||||||
|  |  | ||||||
|  | [env] | ||||||
| platform = espressif32 | platform = espressif32 | ||||||
| board = esp-wrover-kit | board = esp-wrover-kit | ||||||
| framework = arduino | framework = arduino | ||||||
| upload_speed = 512000 | upload_speed = 512000 | ||||||
| build_flags=!./build_version.sh | lib_deps = | ||||||
| lib_deps = MFRC522 | 	63 ; MFRC522 | ||||||
| 	https://github.com/me-no-dev/ESPAsyncWebServer.git | 	https://github.com/me-no-dev/ESPAsyncWebServer.git | ||||||
| 	ArduinoJSON | 	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 | upload_port = /dev/cu.SLAB_USBtoUART | ||||||
| monitor_speed = 74480 |  | ||||||
| ;monitor_port = /dev/cu.wchusbserial1420 | [env:deploy] | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| #include "controller.h" | #include "controller.h" | ||||||
|  | #include "main.h" | ||||||
| #include "spi_master.h" | #include "spi_master.h" | ||||||
| #include "config.h" | #include "config.h" | ||||||
| #include "playlist.h" | #include "playlist.h" | ||||||
| #include "http_server.h" | #include "http_server.h" | ||||||
|  | #include "updater.h" | ||||||
| #include <ArduinoJson.h> | #include <ArduinoJson.h> | ||||||
|  |  | ||||||
| Controller::Controller(Player* p, PlaylistManager* playlist_manager) { | Controller::Controller(Player* p, PlaylistManager* playlist_manager) { | ||||||
| @@ -34,7 +36,6 @@ void Controller::register_http_server(HTTPServer* h) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void Controller::loop() { | void Controller::loop() { | ||||||
| 	TRACE("Controller::loop()...\n"); |  | ||||||
| 	unsigned long now = millis(); | 	unsigned long now = millis(); | ||||||
| 	if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) { | 	if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) { | ||||||
| 		_check_rfid(); | 		_check_rfid(); | ||||||
| @@ -50,7 +51,20 @@ void Controller::loop() { | |||||||
| 		process_message(_cmd_queue); | 		process_message(_cmd_queue); | ||||||
| 		_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() { | uint32_t Controller::_get_rfid_card_uid() { | ||||||
| @@ -69,7 +83,7 @@ uint32_t Controller::_get_rfid_card_uid() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void Controller::_check_rfid() { | void Controller::_check_rfid() { | ||||||
| 	TRACE("check_rfid running...\n"); | 	//TRACE("check_rfid running...\n"); | ||||||
| 	MFRC522::StatusCode status; | 	MFRC522::StatusCode status; | ||||||
| 	if (_rfid_present) { | 	if (_rfid_present) { | ||||||
| 		byte buffer[2]; | 		byte buffer[2]; | ||||||
| @@ -128,7 +142,8 @@ void Controller::_check_rfid() { | |||||||
| 				if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december. | 				if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december. | ||||||
| 					pl->advent_shuffle(time.tm_mday); | 					pl->advent_shuffle(time.tm_mday); | ||||||
| 				} else { | 				} 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()) { | 			} else if (data.indexOf("[random]") != -1 && pl->is_fresh()) { | ||||||
| 				pl->shuffle(); | 				pl->shuffle(); | ||||||
| @@ -184,7 +199,7 @@ String Controller::_read_rfid_data() { | |||||||
| 		case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break; | 		case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break; | ||||||
| 		default: INFO("Unknown PICC type %s\n", String(MFRC522::PICC_GetTypeName(type)).c_str()); | 		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; | 	int good_key_index = -1; | ||||||
| 	for (uint8_t sector=1; sector<sectors; sector++) { | 	for (uint8_t sector=1; sector<sectors; sector++) { | ||||||
| 		uint8_t blocks = (sector < 32) ? 4 : 16; | 		uint8_t blocks = (sector < 32) ? 4 : 16; | ||||||
| @@ -228,8 +243,6 @@ String Controller::_read_rfid_data() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void Controller::_check_serial() { | void Controller::_check_serial() { | ||||||
| 	TRACE("check_serial running...\n"); |  | ||||||
|  |  | ||||||
| 	if (Serial.available() > 0) { | 	if (Serial.available() > 0) { | ||||||
| 		char c = Serial.read(); | 		char c = Serial.read(); | ||||||
| 		Serial.printf("%c", c); | 		Serial.printf("%c", c); | ||||||
| @@ -250,13 +263,8 @@ bool Controller::process_message(String cmd) { | |||||||
| 	if (cmd.startsWith("play ")) { | 	if (cmd.startsWith("play ")) { | ||||||
| 		Playlist* p = pm->get_playlist_for_folder(cmd.substring(5)); | 		Playlist* p = pm->get_playlist_for_folder(cmd.substring(5)); | ||||||
| 		player->play(p); | 		player->play(p); | ||||||
| 	//} else if (cmd.equals("ls")) { |  | ||||||
| 	//	_execute_command_ls("/"); |  | ||||||
| 	//} else if (cmd.startsWith("ls ")) { |  | ||||||
| 	//	_execute_command_ls(cmd.substring(3)); |  | ||||||
| 	} else if (cmd.equals("play")) { | 	} else if (cmd.equals("play")) { | ||||||
| 		player->play(); | 		player->play(); | ||||||
| 	 |  | ||||||
| 	} else if (cmd.equals("stop")) { | 	} else if (cmd.equals("stop")) { | ||||||
| 		player->stop(); | 		player->stop(); | ||||||
| 	} else if (cmd.equals("help")) { | 	} else if (cmd.equals("help")) { | ||||||
| @@ -289,6 +297,28 @@ bool Controller::process_message(String cmd) { | |||||||
| 		String folder = rest.substring(idx + 1); | 		String folder = rest.substring(idx + 1); | ||||||
| 		pm->add_mapping(id, folder); | 		pm->add_mapping(id, folder); | ||||||
| 		send_playlist_manager_status(); | 		send_playlist_manager_status(); | ||||||
|  | 	#ifdef OTA_UPDATE_URL | ||||||
|  | 	} else if (cmd.equals("update")) { | ||||||
|  | 		Updater::run(); | ||||||
|  | 	#endif | ||||||
|  | 	} else if (cmd.startsWith("trace=")) { | ||||||
|  | 		int val = cmd.substring(6).toInt(); | ||||||
|  | 		if (val==0) { | ||||||
|  | 			trace_enabled = false; | ||||||
|  | 			prefs.putBool("trace_enabled", false); | ||||||
|  | 		} else if (val==1) { | ||||||
|  | 			trace_enabled = true; | ||||||
|  | 			prefs.putBool("trace_enabled", true); | ||||||
|  | 		} | ||||||
|  | 	} else if (cmd.startsWith("debug=")) { | ||||||
|  | 		int val = cmd.substring(6).toInt(); | ||||||
|  | 		if (val==0) { | ||||||
|  | 			debug_enabled = false; | ||||||
|  | 			prefs.putBool("debug_enabled", false); | ||||||
|  | 		} else if (val==1) { | ||||||
|  | 			debug_enabled = true; | ||||||
|  | 			prefs.putBool("debug_enabled", true); | ||||||
|  | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		ERROR("Unknown command: %s\n", cmd.c_str()); | 		ERROR("Unknown command: %s\n", cmd.c_str()); | ||||||
| 		return false; | 		return false; | ||||||
| @@ -317,8 +347,6 @@ void Controller::_execute_command_help() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void Controller::_check_buttons() { | void Controller::_check_buttons() { | ||||||
| 	TRACE("check_buttons running...\n"); |  | ||||||
|  |  | ||||||
| 	if (BTN_PREV() && _debounce_button(0)) { | 	if (BTN_PREV() && _debounce_button(0)) { | ||||||
| 		if (_state == NORMAL) { | 		if (_state == NORMAL) { | ||||||
| 			player->track_prev(); | 			player->track_prev(); | ||||||
| @@ -362,6 +390,22 @@ String Controller::json() { | |||||||
| 	rfid["data"] = _last_rfid_data; | 	rfid["data"] = _last_rfid_data; | ||||||
| 	json["uptime"] = millis() / 1000; | 	json["uptime"] = millis() / 1000; | ||||||
| 	json["free_heap"] = ESP.getFreeHeap(); | 	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>(); | 	return json.as<String>(); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,93 +1,71 @@ | |||||||
| #include "data_sources.h" | #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::SDDataSource(String file) { _file = SD.open(file, "r"); } | SDDataSource::SDDataSource(String file) { _file = SD.open(file, "r"); } | ||||||
| SDDataSource::~SDDataSource() { if (_file) _file.close(); } | SDDataSource::~SDDataSource() { if (_file) _file.close(); } | ||||||
| size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); } | size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); } | ||||||
| uint8_t SDDataSource::read() { return _file.read(); } | int SDDataSource::read() { return _file.read(); } | ||||||
| size_t SDDataSource::position() { return _file.position(); } | size_t SDDataSource::position() { return _file.position(); } | ||||||
| void SDDataSource::seek(size_t position) { _file.seek(position); } | void SDDataSource::seek(size_t position) { _file.seek(position); } | ||||||
| size_t SDDataSource::size() { return _file.size(); } | size_t SDDataSource::size() { return _file.size(); } | ||||||
| void SDDataSource::close() { _file.close(); } | void SDDataSource::close() { _file.close(); } | ||||||
| bool SDDataSource::usable() { return _file; } | bool SDDataSource::usable() { return _file; } | ||||||
|  | int SDDataSource::peek(int offset) { | ||||||
| void SDDataSource::skip_id3_tag() { | 	if (offset==0) return _file.peek(); | ||||||
| 	uint32_t original_position = _file.position(); | 	size_t start_position = _file.position(); | ||||||
| 	uint32_t offset = 0; | 	_file.seek(start_position + offset); | ||||||
| 	if (_file.read()=='I' && _file.read()=='D' && _file.read()=='3') { | 	int result = _file.peek(); | ||||||
| 		DEBUG("ID3 tag found\n"); | 	_file.seek(start_position); | ||||||
| 		// Skip ID3 tag version | 	return result; | ||||||
| 		_file.read(); _file.read(); |  | ||||||
| 		byte tags = _file.read(); |  | ||||||
| 		bool footer_present = tags & 0x10; |  | ||||||
| 		DEBUG("ID3 footer found: %d\n", footer_present); |  | ||||||
| 		for (byte i=0; i<4; i++) { |  | ||||||
| 			offset <<= 7; |  | ||||||
| 			offset |= (0x7F & _file.read()); |  | ||||||
| 		} |  | ||||||
| 		offset += 10; |  | ||||||
| 		if (footer_present) offset += 10; |  | ||||||
| 		DEBUG("ID3 tag length is %d bytes.\n", offset); |  | ||||||
| 		_file.seek(offset); |  | ||||||
| 	} else { |  | ||||||
| 		DEBUG("No ID3 tag found\n"); |  | ||||||
| 		_file.seek(original_position); |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ////////////// HTTPSDataSource ////////////// | ////////////// HTTPSDataSource ////////////// | ||||||
| HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) { | HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) { | ||||||
| 	uint8_t tries_left = 5; | 	_url = url; | ||||||
| 	int status; | 	_init(url, offset); | ||||||
| 	do { | } | ||||||
| 		if (tries_left == 0) { |  | ||||||
| 			ERROR("Redirection loop? Cancelling!\n"); | void HTTPSDataSource::_init(String url, uint32_t offset) { | ||||||
| 			return; | 	_url = url; | ||||||
| 		} | 	_http = new HTTPClientWrapper(); | ||||||
| 		tries_left--; | 	if (!_http->get(url, offset)) return; | ||||||
| 		DEBUG("Connecting to %s...\n", url.c_str()); | 	_position = 0; | ||||||
| 		if (_http) delete _http; |  | ||||||
| 		_http = new HTTPClient(); |  | ||||||
| 		_http->setUserAgent("PodBox 0.1"); |  | ||||||
| 		const char* headers[] = {"Location"}; |  | ||||||
| 		_http->collectHeaders(headers, 1); |  | ||||||
| 		bool result = _http->begin(url); |  | ||||||
| 		DEBUG("HTTP->begin result: %d\n", result); |  | ||||||
| 		if (!result) return; |  | ||||||
| 		status = _http->GET(); |  | ||||||
| 		DEBUG("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"); |  | ||||||
| 			} else { |  | ||||||
| 				ERROR("Got redirection HTTP code, but could not find Location header.\n"); |  | ||||||
| 				for(int i=0; i<_http->headers(); i++) { |  | ||||||
| 					DEBUG("  Header: %s=%s\n", _http->headerName(i).c_str(), _http->header(i).c_str()); |  | ||||||
| 				} |  | ||||||
| 				return; |  | ||||||
| 			} |  | ||||||
| 		} else if (status != HTTP_CODE_OK) { |  | ||||||
| 			DEBUG("Unexpected HTTP return code. Cancelling.\n"); |  | ||||||
| 			return; |  | ||||||
| 		} |  | ||||||
| 	} while (status != HTTP_CODE_OK); |  | ||||||
| 	_length = _http->getSize(); |  | ||||||
| 	DEBUG("Content-Length: %d\n", _length); |  | ||||||
| 	_stream = _http->getStreamPtr(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| HTTPSDataSource::~HTTPSDataSource() { | HTTPSDataSource::~HTTPSDataSource() { | ||||||
| 	if (_stream) _stream->stop(); | 	_http->close(); | ||||||
| 	_http->end(); |  | ||||||
| 	delete _stream; |  | ||||||
| 	delete _http; | 	delete _http; | ||||||
| } | } | ||||||
| bool HTTPSDataSource::usable() { return _http && _stream; } | bool HTTPSDataSource::usable() { return _http; } | ||||||
| size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _stream->read(buf, len); _position += result; return result; } | size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; } | ||||||
| uint8_t HTTPSDataSource::read() { _position++; return _stream->read(); } | int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; } | ||||||
| size_t HTTPSDataSource::position() { return _position; } | size_t HTTPSDataSource::position() { return _position; } | ||||||
| void HTTPSDataSource::seek(size_t position) { return; /* TODO */ } | void HTTPSDataSource::seek(size_t position) { _http->close(); delete _http; _init(_url, position); } | ||||||
| size_t HTTPSDataSource::size() { return _length; } | size_t HTTPSDataSource::size() { return _http->getSize(); } | ||||||
| void HTTPSDataSource::close() { _stream->stop(); } | 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 "http_server.h" | ||||||
|  | #include "main.h" | ||||||
| #include "spi_master.h" | #include "spi_master.h" | ||||||
| #include <ESPmDNS.h> | #include <ESPmDNS.h> | ||||||
| #include <SPIFFS.h> |  | ||||||
|  |  | ||||||
| HTTPServer::HTTPServer(Player* p, Controller* c) { | HTTPServer::HTTPServer(Player* p, Controller* c) { | ||||||
| 	_player = p; | 	_player = p; | ||||||
| @@ -11,14 +11,42 @@ HTTPServer::HTTPServer(Player* p, Controller* c) { | |||||||
| 	_server->addHandler(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);}); | 	ws->onEvent([&](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){this->_onEvent(server, client, type, arg, data, len);}); | ||||||
| 	 | 	 | ||||||
| 	_server->on("/", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(SPIFFS, "/index.html", "text/html");}); | 	_server->on("/", HTTP_GET, [&](AsyncWebServerRequest* req) { | ||||||
| 	_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);})); | 		req->send_P(200, "text/html", file_index_html_start, file_index_html_size); | ||||||
| 	_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("/timezone.json", HTTP_GET, [&](AsyncWebServerRequest* req) { | ||||||
| 	_server->on("/playlist_manager.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->pm->json());}); | 		AsyncWebServerResponse* res = req->beginResponse_P(200, "application/json", file_timezones_json_start, file_timezones_json_size); | ||||||
| 	_server->on("/controller.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->json());}); | 		res->addHeader("Content-Encoding", "gzip"); | ||||||
| 	_server->on("/position.json", HTTP_GET, [&](AsyncWebServerRequest* req) {req->send(200, "application/json", _controller->player->position_json());}); | 		req->send(res); | ||||||
| 	_server->on("/cmd", HTTP_POST, [&](AsyncWebServerRequest *req) {req->send(200); }, NULL, [&](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {_controller->queue_command((char*)data);}); | 	}); | ||||||
|  | 	_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(); | 	_server->begin(); | ||||||
| 	MDNS.addService("http", "tcp", 80); | 	MDNS.addService("http", "tcp", 80); | ||||||
| } | } | ||||||
| @@ -136,6 +164,7 @@ void HTTPServer::_onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client | |||||||
| 	} else if (type==WS_EVT_DATA) { | 	} else if (type==WS_EVT_DATA) { | ||||||
| 		AwsFrameInfo* info = (AwsFrameInfo*) arg; | 		AwsFrameInfo* info = (AwsFrameInfo*) arg; | ||||||
| 		if (info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT) { | 		if (info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT) { | ||||||
|  | 			data[len]='\0'; | ||||||
| 			DEBUG("Received ws message: %s\n", (char*)data); | 			DEBUG("Received ws message: %s\n", (char*)data); | ||||||
| 			_controller->queue_command((char*)data); | 			_controller->queue_command((char*)data); | ||||||
| 		} | 		} | ||||||
|   | |||||||
							
								
								
									
										81
									
								
								src/main.cpp
									
									
									
									
									
								
							
							
						
						
									
										81
									
								
								src/main.cpp
									
									
									
									
									
								
							| @@ -2,14 +2,17 @@ | |||||||
| #include <SPI.h> | #include <SPI.h> | ||||||
| #include <SD.h> | #include <SD.h> | ||||||
| #include <WiFi.h> | #include <WiFi.h> | ||||||
|  | #include <WiFiMulti.h> | ||||||
| #include <ESPmDNS.h> | #include <ESPmDNS.h> | ||||||
| #include <SPIFFS.h> | #include <Preferences.h> | ||||||
|  | #include "main.h" | ||||||
| #include "config.h" | #include "config.h" | ||||||
| #include "controller.h" | #include "controller.h" | ||||||
| #include "player.h" | #include "player.h" | ||||||
| #include "spi_master.h" | #include "spi_master.h" | ||||||
| #include "http_server.h" | #include "http_server.h" | ||||||
| #include "playlist_manager.h" | #include "playlist_manager.h" | ||||||
|  | #include "updater.h" | ||||||
|  |  | ||||||
| Controller* controller; | Controller* controller; | ||||||
| Player* player; | Player* player; | ||||||
| @@ -18,18 +21,67 @@ HTTPServer* http_server; | |||||||
|  |  | ||||||
| uint8_t SPIMaster::state = 0; | 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() { | void setup() { | ||||||
| 	delay(500); | 	// Small delay to give the Serial console a bit of time to connect. | ||||||
| 	Serial.begin(74880); | 	delay(1000); | ||||||
|  | 	Serial.begin(115200); | ||||||
| 	Serial.println("Starting..."); | 	Serial.println("Starting..."); | ||||||
| 	Serial.println("Started."); | 	Serial.println("Started."); | ||||||
| 	INFO("Starting.\n"); | 	INFO("Starting.\n"); | ||||||
| 	#ifdef VERSION | 	#ifdef VERSION | ||||||
| 		INFO("ESMP3 version %s\n", VERSION); | 		INFO("ESMP3 version %s (OTA_VERSION %d)\n", VERSION, OTA_VERSION); | ||||||
| 	#else | 	#else | ||||||
| 		INFO("ESMP3, version unknown\n"); | 		INFO("ESMP3, version unknown (OTA_VERSION %d)\n", OTA_VERSION); | ||||||
| 	#endif | 	#endif | ||||||
| 	INFO("Initializing...\n"); | 	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"); | 	DEBUG("Setting up SPI...\n"); | ||||||
| 	SPI.begin(); | 	SPI.begin(); | ||||||
| @@ -47,9 +99,6 @@ void setup() { | |||||||
| 	} | 	} | ||||||
| 	spi->select_sd(false); | 	spi->select_sd(false); | ||||||
|  |  | ||||||
| 	DEBUG("Starting SPIFFS...\n"); |  | ||||||
| 	SPIFFS.begin(true); |  | ||||||
|  |  | ||||||
| 	DEBUG("Initializing PlaylistManager...\n"); | 	DEBUG("Initializing PlaylistManager...\n"); | ||||||
| 	pm = new PlaylistManager(); | 	pm = new PlaylistManager(); | ||||||
| 	 | 	 | ||||||
| @@ -58,15 +107,7 @@ void setup() { | |||||||
|     controller = new Controller(player, pm); |     controller = new Controller(player, pm); | ||||||
|     INFO("Player and controller initialized.\n"); |     INFO("Player and controller initialized.\n"); | ||||||
|  |  | ||||||
| 	DEBUG("Connecting to wifi \"%s\"...\n", WIFI_SSID); |     wifi_connect(); | ||||||
| 	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()); |  | ||||||
| 	 | 	 | ||||||
| 	MDNS.begin("esmp3"); | 	MDNS.begin("esmp3"); | ||||||
|  |  | ||||||
| @@ -86,6 +127,12 @@ void setup() { | |||||||
| 		INFO("Could not fetch current time via NTP.\n"); | 		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"); |     INFO("Initialization completed.\n"); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,8 +10,6 @@ Player::Player(SPIMaster* s) { | |||||||
| 	_spi = s; | 	_spi = s; | ||||||
| 	PIN_VS1053_XRESET_SETUP(); | 	PIN_VS1053_XRESET_SETUP(); | ||||||
| 	PIN_VS1053_XRESET(HIGH); | 	PIN_VS1053_XRESET(HIGH); | ||||||
| 	PIN_SPEAKER_L_SETUP(); |  | ||||||
| 	PIN_SPEAKER_R_SETUP(); |  | ||||||
| 	_speaker_off(); | 	_speaker_off(); | ||||||
| 	_spi->disable(); | 	_spi->disable(); | ||||||
| 	PIN_VS1053_DREQ_SETUP(); | 	PIN_VS1053_DREQ_SETUP(); | ||||||
| @@ -464,14 +462,14 @@ void Player::set_volume(uint8_t vol, bool save) { | |||||||
|  |  | ||||||
| void Player::vol_up() { | void Player::vol_up() { | ||||||
| 	if (!is_playing()) return; | 	if (!is_playing()) return; | ||||||
| 	uint8_t vol = _volume + VOLUME_STEP; | 	uint16_t vol = _volume + VOLUME_STEP; | ||||||
| 	if (vol > VOLUME_MAX) vol=VOLUME_MAX; | 	if (vol > VOLUME_MAX) vol=VOLUME_MAX; | ||||||
| 	set_volume(vol); | 	set_volume(vol); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::vol_down() { | void Player::vol_down() { | ||||||
| 	if (!is_playing()) return; | 	if (!is_playing()) return; | ||||||
| 	uint8_t vol = _volume - VOLUME_STEP; | 	int16_t vol = _volume - VOLUME_STEP; | ||||||
| 	if (vol < VOLUME_MIN) vol=VOLUME_MIN; | 	if (vol < VOLUME_MIN) vol=VOLUME_MIN; | ||||||
| 	set_volume(vol); | 	set_volume(vol); | ||||||
| } | } | ||||||
| @@ -533,8 +531,13 @@ bool Player::play() { | |||||||
| 	if (_state == sleeping || _state == recording) _wakeup(); | 	if (_state == sleeping || _state == recording) _wakeup(); | ||||||
| 	if (_state != idle) return false; | 	if (_state != idle) return false; | ||||||
| 	if (_current_playlist == NULL) return false; | 	if (_current_playlist == NULL) return false; | ||||||
|  | 	if (_current_playlist->get_file_count()==0) return false; | ||||||
|  | 	_speaker_on(); | ||||||
| 	_current_playlist->start(); | 	_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(); | 	uint32_t position = _current_playlist->get_position(); | ||||||
| 	_state = playing; | 	_state = playing; | ||||||
| 	_play_file(file, position); | 	_play_file(file, position); | ||||||
| @@ -547,7 +550,7 @@ void Player::_play_file(String file, uint32_t file_offset) { | |||||||
| 	_spi->select_sd(); | 	_spi->select_sd(); | ||||||
| 	if (file.startsWith("/")) { | 	if (file.startsWith("/")) { | ||||||
| 		_file = new SDDataSource(file); | 		_file = new SDDataSource(file); | ||||||
| 	} else if (file.startsWith("https://")) { | 	} else if (file.startsWith("http")) { | ||||||
| 		_file = new HTTPSDataSource(file); | 		_file = new HTTPSDataSource(file); | ||||||
| 	} else { | 	} else { | ||||||
| 		return; | 		return; | ||||||
| @@ -609,7 +612,8 @@ void Player::stop(bool turn_speaker_off) { | |||||||
| 	if (_state != playing) return; | 	if (_state != playing) return; | ||||||
| 	INFO("Stopping...\n"); | 	INFO("Stopping...\n"); | ||||||
| 	_current_playlist->set_position(_current_play_position); | 	_current_playlist->set_position(_current_play_position); | ||||||
|  | 	_controller->pm->persist(_current_playlist); | ||||||
|  | 	 | ||||||
| 	_state = stopping; | 	_state = stopping; | ||||||
| 	_stop_delay = 0; | 	_stop_delay = 0; | ||||||
| 	_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL); | 	_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL); | ||||||
|   | |||||||
							
								
								
									
										264
									
								
								src/playlist.cpp
									
									
									
									
									
								
							
							
						
						
									
										264
									
								
								src/playlist.cpp
									
									
									
									
									
								
							| @@ -4,13 +4,20 @@ | |||||||
| #include <SD.h> | #include <SD.h> | ||||||
| #include <algorithm> | #include <algorithm> | ||||||
| #include <ArduinoJson.h> | #include <ArduinoJson.h> | ||||||
|  | #include <TinyXML.h> | ||||||
|  |  | ||||||
| Playlist::Playlist(String path, bool is_url) { | Playlist::Playlist(String path) { | ||||||
| 	if (is_url) { | 	_path = path; | ||||||
| 		_files.push_back(path); | 	if (path.startsWith("/")) { | ||||||
| 		return; | 		persistence = PERSIST_TEMPORARY; | ||||||
|  | 		_add_path(path); | ||||||
|  | 	} else if (path.startsWith("http")) { | ||||||
|  | 		_examine_http_url(path); | ||||||
| 	} | 	} | ||||||
| 	// Add files to _files | 	if (_title.length()==0) _title=path; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::_add_path(String path) { | ||||||
| 	SPIMaster::select_sd(); | 	SPIMaster::select_sd(); | ||||||
| 	TRACE("Examining folder %s...\n", path.c_str()); | 	TRACE("Examining folder %s...\n", path.c_str()); | ||||||
| 	if (!path.startsWith("/")) path = String("/") + path; | 	if (!path.startsWith("/")) path = String("/") + path; | ||||||
| @@ -19,6 +26,11 @@ Playlist::Playlist(String path, bool is_url) { | |||||||
| 		SPIMaster::select_sd(false); | 		SPIMaster::select_sd(false); | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
|  | 	_title = path.substring(1); | ||||||
|  | 	int idx = _title.indexOf('/'); | ||||||
|  | 	if (idx>0) { | ||||||
|  | 		_title.remove(idx); | ||||||
|  | 	} | ||||||
| 	File dir = SD.open(path); | 	File dir = SD.open(path); | ||||||
| 	File entry; | 	File entry; | ||||||
| 	while (entry = dir.openNextFile()) { | 	while (entry = dir.openNextFile()) { | ||||||
| @@ -33,7 +45,8 @@ Playlist::Playlist(String path, bool is_url) { | |||||||
| 			  ext.equals(".mp4") || | 			  ext.equals(".mp4") || | ||||||
| 			  ext.equals(".mpa"))) { | 			  ext.equals(".mpa"))) { | ||||||
| 			TRACE("    Adding entry %s\n", entry.name()); | 			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; | 			bool non_ascii_chars = false; | ||||||
| 			for(int i=0; i<filename.length(); i++) { | 			for(int i=0; i<filename.length(); i++) { | ||||||
| 				char c = filename.charAt(i); | 				char c = filename.charAt(i); | ||||||
| @@ -55,6 +68,195 @@ Playlist::Playlist(String path, bool is_url) { | |||||||
| 	std::sort(_files.begin(), _files.end()); | 	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() { | void Playlist::start() { | ||||||
| 	_started = true; | 	_started = true; | ||||||
| } | } | ||||||
| @@ -94,6 +296,15 @@ bool Playlist::set_track(uint8_t track) { | |||||||
| 	return false; | 	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() { | void Playlist::track_restart() { | ||||||
| 	_position = 0; | 	_position = 0; | ||||||
| } | } | ||||||
| @@ -104,7 +315,7 @@ void Playlist::shuffle(uint8_t random_offset) { | |||||||
| 		int j = random(random_offset, _files.size()-1); | 		int j = random(random_offset, _files.size()-1); | ||||||
| 		if (i!=j) { | 		if (i!=j) { | ||||||
| 			TRACE("  Swapping elements %d and %d.\n", i, j); | 			TRACE("  Swapping elements %d and %d.\n", i, j); | ||||||
| 			String temp = _files[i]; | 			PlaylistEntry temp = _files[i]; | ||||||
| 			_files[i] = _files[j]; | 			_files[i] = _files[j]; | ||||||
| 			_files[j] = temp; | 			_files[j] = temp; | ||||||
| 		} | 		} | ||||||
| @@ -114,12 +325,19 @@ void Playlist::shuffle(uint8_t random_offset) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void Playlist::advent_shuffle(uint8_t day) { | void Playlist::advent_shuffle(uint8_t day) { | ||||||
| 	if (day > 24) day = 24; | 	TRACE("advent_shuffle running...\n"); | ||||||
| 	 | 	 | ||||||
| 	if (day > _files.size()) return; | 	// Not enough songs till the current day? Play all songs in the default order. | ||||||
|  | 	if (day > _files.size()) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// We are in the "different playlist every day" mode. So we don't persist it in order to not miss changes. | ||||||
|  | 	persistence = PERSIST_NONE; | ||||||
|  |  | ||||||
| 	 | 	 | ||||||
| 	_files.insert(_files.begin(), _files[day - 1]); | 	_files.insert(_files.begin(), _files[day - 1]); | ||||||
| 	_files.erase(_files.begin() + day - 1, _files.end()); | 	_files.erase(_files.begin() + day, _files.end()); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Playlist::reset() { | void Playlist::reset() { | ||||||
| @@ -130,8 +348,20 @@ void Playlist::reset() { | |||||||
| 	_started = false; | 	_started = false; | ||||||
| } | } | ||||||
|  |  | ||||||
| String Playlist::get_current_file() { | String Playlist::get_current_track_id() { | ||||||
| 	return _files[_current_track]; | 	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() { | uint32_t Playlist::get_position() { | ||||||
| @@ -148,15 +378,19 @@ bool Playlist::is_fresh() { | |||||||
|  |  | ||||||
| void Playlist::dump() { | void Playlist::dump() { | ||||||
| 	for (int i=0; i<_files.size(); i++) { | 	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) { | void Playlist::json(JsonObject json) { | ||||||
| 	json["_type"] = "playlist"; | 	json["_type"] = "playlist"; | ||||||
|  | 	json["title"] = _title; | ||||||
| 	JsonArray files = json.createNestedArray("files"); | 	JsonArray files = json.createNestedArray("files"); | ||||||
| 	for (String file: _files) { | 	for (PlaylistEntry entry: _files) { | ||||||
| 		files.add(file); | 		JsonObject o = files.createNestedObject(); | ||||||
|  | 		o["filename"] = entry.filename; | ||||||
|  | 		o["title"] = entry.title; | ||||||
|  | 		o["id"] = entry.id; | ||||||
| 	} | 	} | ||||||
| 	json["current_track"] = _current_track; | 	json["current_track"] = _current_track; | ||||||
| 	json["has_track_next"] = has_track_next(); | 	json["has_track_next"] = has_track_next(); | ||||||
|   | |||||||
| @@ -39,16 +39,18 @@ void PlaylistManager::scan_files() { | |||||||
| 				String folder = data.substring(eq + 1); | 				String folder = data.substring(eq + 1); | ||||||
| 				TRACE("  Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str()); | 				TRACE("  Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str()); | ||||||
| 				_map[rfid_id] = folder; | 				_map[rfid_id] = folder; | ||||||
|  | 				 | ||||||
| 				bool found=false; | 				if (folder.charAt(0)=='/') { | ||||||
| 				for (String f: folders) { | 					bool found=false; | ||||||
| 					if (f.equals(folder)) { | 					for (String f: folders) { | ||||||
| 						found = true; | 						if (f.equals(folder)) { | ||||||
| 						break; | 							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* PlaylistManager::get_playlist_for_folder(String folder) { | ||||||
|  | 	Playlist* p; | ||||||
| 	if (!_playlists.count(folder)) { | 	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() { | void PlaylistManager::dump_ids() { | ||||||
| @@ -162,3 +194,46 @@ String PlaylistManager::create_mapping_txt() { | |||||||
| 	} | 	} | ||||||
| 	return s; | 	return s; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void PlaylistManager::persist(Playlist* p) { | ||||||
|  | 	if (p->persistence == PERSIST_NONE) { | ||||||
|  | 		_playlists.erase(p->path()); | ||||||
|  | 		return; | ||||||
|  | 	} else if (p->persistence == PERSIST_PERMANENTLY) { | ||||||
|  | 	 | ||||||
|  | 		String search = p->path(); | ||||||
|  | 		search += '='; | ||||||
|  | 		 | ||||||
|  | 		bool old_file_existed = false; | ||||||
|  | 		 | ||||||
|  | 		SPIMaster::select_sd(); | ||||||
|  | 		if (SD.exists("_positions.txt")) { | ||||||
|  | 			SD.rename("/_positions.txt", "/_positions.temp.txt"); | ||||||
|  | 			old_file_existed = true; | ||||||
|  | 		} | ||||||
|  | 		File dst = SD.open("/_positions.txt", "w"); | ||||||
|  | 		 | ||||||
|  | 		if (old_file_existed) { | ||||||
|  | 			File src = SD.open("/_positions.temp.txt", "r"); | ||||||
|  | 		 | ||||||
|  | 			while (true) { | ||||||
|  | 				String line = src.readStringUntil('\n'); | ||||||
|  | 				line.trim(); | ||||||
|  | 				if (line.startsWith(search)) continue; | ||||||
|  | 				dst.println(line); | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			src.close(); | ||||||
|  | 			SD.remove("/_positions.temp.txt"); | ||||||
|  | 		} | ||||||
|  | 		 | ||||||
|  | 		dst.print(search); | ||||||
|  | 		dst.print(p->get_current_track_id()); | ||||||
|  | 		dst.print(','); | ||||||
|  | 		dst.println(p->get_position()); | ||||||
|  | 		dst.close(); | ||||||
|  | 		SPIMaster::select_sd(false); | ||||||
|  |  | ||||||
|  | 		_playlists.erase(p->path()); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										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://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://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> | 	<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> | </head> | ||||||
| 
 | 
 | ||||||
| <body> | <body> | ||||||
|  | 	<div id="overlay" class="overlay">Not connected...</div> | ||||||
| 	<div class="container bg-dark text-light"> | 	<div class="container bg-dark text-light"> | ||||||
| 		<div class="row"> | 		<div class="row"> | ||||||
| 			<div class="col-sm-1"> | 			<div class="col-sm-1"> | ||||||
| @@ -93,6 +108,20 @@ | |||||||
| 						<span>×</span> | 						<span>×</span> | ||||||
| 					</button> | 					</button> | ||||||
| 				</div> | 				</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 class="modal-body"> | ||||||
| 					<div id="albums_without_id_area"> | 					<div id="albums_without_id_area"> | ||||||
| @@ -137,6 +166,7 @@ | |||||||
| 						<h6>Actions</h6> | 						<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_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_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> | ||||||
| 					 | 					 | ||||||
| 					<div class="modal-footer"> | 					<div class="modal-footer"> | ||||||
| @@ -173,7 +203,7 @@ update_playlist = function(data) { | |||||||
| 		tr = $('<tr>').data('track', i); | 		tr = $('<tr>').data('track', i); | ||||||
| 		tr.append($('<td>').html(i + 1)); | 		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.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); | 		$('#track_list').append(tr); | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| @@ -189,12 +219,10 @@ update_playlist = function(data) { | |||||||
| 		$('#button_track_prev').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled'); | 		$('#button_track_prev').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled'); | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
|  | 	$('#album').html(data.title); | ||||||
| 	var file = data.files[data.current_track]; | 	var file = data.files[data.current_track]; | ||||||
| 	if (file) { | 	if (file) { | ||||||
| 		file = file.substr(1); | 		$('#track').html(file.title);  | ||||||
| 		$('#album').html(file.substr(0, file.indexOf('/'))); |  | ||||||
| 		file = file.substr(file.indexOf('/')+1); |  | ||||||
| 		$('#track').html(file.substr(0, file.lastIndexOf('.')));  |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @@ -249,6 +277,7 @@ process_ws_message = function(event) { | |||||||
| 	for (var i=0; i<data.length; i++) { | 	for (var i=0; i<data.length; i++) { | ||||||
| 		var json = JSON.parse(data[i]); | 		var json = JSON.parse(data[i]); | ||||||
| 		console.log(json); | 		console.log(json); | ||||||
|  | 		if (json === null) continue; | ||||||
| 		switch(json["_type"]) { | 		switch(json["_type"]) { | ||||||
| 			case "position": update_position(json); break; | 			case "position": update_position(json); break; | ||||||
| 			case "player": update_player(json); break; | 			case "player": update_player(json); break; | ||||||
| @@ -259,10 +288,30 @@ process_ws_message = function(event) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| var play_on_click = true; | 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() { | $(function() { | ||||||
| 	ws = new WebSocket("ws://" + location.host + "/ws"); | 	start_reconnect_timer(); | ||||||
| 	ws.onmessage = process_ws_message; |  | ||||||
| 	 | 	 | ||||||
| 	$('#volume_slider').change(function(e) { ws.send("volume=" + e.target.value); }); | 	$('#volume_slider').change(function(e) { ws.send("volume=" + e.target.value); }); | ||||||
| 	$('#button_play').click(function(e) { ws.send("play"); }); | 	$('#button_play').click(function(e) { ws.send("play"); }); | ||||||
| @@ -275,13 +324,36 @@ $(function() { | |||||||
| 	$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); }); | 	$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); }); | ||||||
| 	$('#button_reset_vs1053').click(function(e) { ws.send("reset_vs1053"); $('#settingsModal').modal('hide'); }); | 	$('#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_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) { | 	$('#button_add_mapping').click(function(e) { | ||||||
| 		$('#settingsModal').modal('hide'); | 		$('#settingsModal').modal('hide'); | ||||||
| 		$('#openModal').modal('show'); | 		$('#openModal').modal('show'); | ||||||
| 		$('.add_mapping_button').show(); | 		$('.add_mapping_button').show(); | ||||||
|  | 		$('#button_url_open').hide(); | ||||||
|  | 		$('#button_url_add_mapping').show(); | ||||||
| 		play_on_click = false; | 		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> | </script> | ||||||
| </html> | </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