Compare commits
	
		
			84 Commits
		
	
	
		
			0.1
			...
			bcf7625285
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| bcf7625285 | |||
| 4a3e79f02e | |||
| 68e1073858 | |||
| f73d45404f | |||
| ecc7c46b8d | |||
| 0dd5937707 | |||
| 547080acf5 | |||
| d3c699aefa | |||
| a8d19cd6e1 | |||
| 38d48ab0e4 | |||
| 51bef05465 | |||
| 4eef69516e | |||
| 9175193b67 | |||
| 65118fbc42 | |||
| 076f0e9dfd | |||
| 571e969bc4 | |||
| 8e15f87cd3 | |||
| dd9e1538c8 | |||
| 001e275131 | |||
| 196021bef5 | |||
| 63b9616677 | |||
| d4c9a6d582 | |||
| 5fe66fdaef | |||
| 6445dc0fb8 | |||
| 7a20cf4b04 | |||
| bbf77c6b1e | |||
| b805d1b183 | |||
| 07b1ea3a5c | |||
| 3b0410f560 | |||
| 8f19b990ff | |||
| 519ac0e3bd | |||
| 651843fb06 | |||
| fcbbdce118 | |||
| 6f8683ba9d | |||
| 710b8a2cdc | |||
| b989784fb9 | |||
| 94489618ca | |||
| 82d8f07eea | |||
| 20041dd483 | |||
| 4f9174d362 | |||
| 68ecc05712 | |||
| 5fad39ee0e | |||
| 01f513c97b | |||
| 3bfbea92d8 | |||
| d818624287 | |||
| d92388d11f | |||
| 37df309127 | |||
| be8a124803 | |||
| 104236dd0c | |||
| e1dd004cf5 | |||
| b5ec78ab41 | |||
| fff9d9bc61 | |||
| ef47c771ef | |||
| 9f442259e9 | |||
| 8e5a3195b9 | |||
| cc4729eb6b | |||
| f7c4b0d70a | |||
| 566068f7cd | |||
| 5c15a7d4cb | |||
| b9a4770ff2 | |||
| e471a57578 | |||
| 6e05900b5a | |||
| 15f6d78128 | |||
| b32f7d1228 | |||
| 45fef23bad | |||
| e20e6b7d3e | |||
| 0531b599fe | |||
| a5751eec79 | |||
| e02b8571f6 | |||
| 303a8d3877 | |||
| 6d00474315 | |||
| 46fb4c7615 | |||
| 48c93ed043 | |||
| 2d1f049444 | |||
| c313f6eb72 | |||
| cccdc9cedb | |||
| 429979c6d1 | |||
| e28a541fe9 | |||
| 0f2b8c6564 | |||
| 5f682c303f | |||
| dcbb42f5ef | |||
| 235ef8c39d | |||
| 4d59c66354 | |||
| 25fa963752 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,5 @@ | |||||||
| .pio | .pio | ||||||
| .pioenvs | .pioenvs | ||||||
| .piolibdeps | .piolibdeps | ||||||
|  | .vscode | ||||||
|  | include/config.h | ||||||
							
								
								
									
										172
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | |||||||
|  | # ESMP3 | ||||||
|  |  | ||||||
|  | ## What you need | ||||||
|  | Please note: This list is a "things I used", neither | ||||||
|  | "these are the best things for this stuff" nor "you | ||||||
|  | can only use these things". But please be aware that | ||||||
|  | using other stuff may lead to you having to make | ||||||
|  | more or less easy modifications. | ||||||
|  |  | ||||||
|  | Prizes are more or less the cheapest I could find on | ||||||
|  | Aliexpress. | ||||||
|  |  | ||||||
|  | | What? | For what? | Price (approx) | | ||||||
|  | |-------|-----------|----------------| | ||||||
|  | | ESP-32-WROOM-32D | Controlling everything |   4€ | | ||||||
|  | | WS1053B on a PCB with SD card slot | Play the MP3 files; provide an SD card slot |   5€ | | ||||||
|  | | MFRC522 | RFID reader |   1€ | | ||||||
|  | | 5V Amplifier(s) (e.g. 2x PAM-8302) | Single-channel Amplifier |   2€ | | ||||||
|  | | Speaker(s) matching your amp (e.g. 2pcs 4 Ohm 5W) | Enabling you to hear the sounds |   4€ | | ||||||
|  | | RFID tags (ISO14443A) - e.g. 10 cards | You can also get Keyfobs or stickers |   4€ | | ||||||
|  | | 4 buttons | Prev/Next track, Volume up/down |   1€ | | ||||||
|  |  | ||||||
|  | You'all also need an SD card, some breadboard(s), jumper cables and a soldering iron. | ||||||
|  | Also, some kind of box for the finished player. | ||||||
|  |  | ||||||
|  | ## How to connect | ||||||
|  |  | ||||||
|  | Schematics coming soon...ish... | ||||||
|  |  | ||||||
|  | ## How to install | ||||||
|  |  | ||||||
|  | Format your SD card with FAT32 and put files on it: Every album has | ||||||
|  | to go into its own folder in the root of the SD card. Folders and files | ||||||
|  | should not contain special characters (meaning stuff like äöüß). Spaces | ||||||
|  | and dashes an alike are okay. Put the SD card into the SD card slot. | ||||||
|  |  | ||||||
|  | Copy `include/config.sample.h` to `include/config.h`. Modify it to at | ||||||
|  | least contain the correct login details for your WiFi. | ||||||
|  |  | ||||||
|  | The code then should compile in PlatformIO without errors. Upload it | ||||||
|  | to your ESP32. After that, upload static files using PlatformIO's task | ||||||
|  | "Upload file system image". | ||||||
|  |  | ||||||
|  | The serial console in PlatformIO should give you more or less useful | ||||||
|  | messages about what's going on. There will also be a line saying | ||||||
|  | "WiFi connected. IP address: xxx.xxx.xxx.xxx" when the connection to | ||||||
|  | your WiFi succeeded. | ||||||
|  |  | ||||||
|  | In your browser, enter "http://xxx.xxx.xxx.xxx/" (using the IP address) | ||||||
|  | from above. From there you can define mappings between RFID tag IDs and | ||||||
|  | folders on the SD card. | ||||||
|  |  | ||||||
|  | ## RFID-folder-mappings | ||||||
|  |  | ||||||
|  | ### Via webinterface | ||||||
|  |  | ||||||
|  | To create a new mapping between an RFID tag and an folder, you can use | ||||||
|  | the web interface. Click the button with the cogs icon. After putting | ||||||
|  | your rfid tag on the reader (and possibly removing it again), its ID | ||||||
|  | will be shown in the dialog. Click the button with the arrows behind | ||||||
|  | the ID to start the mapping mode. | ||||||
|  |  | ||||||
|  | The dialog showing all folders with media files will be shown. Click the | ||||||
|  | button with the arrows behind the correct folder, to create the mapping. | ||||||
|  |  | ||||||
|  | ### Manually | ||||||
|  |  | ||||||
|  | Mapping are stored on the SD card in the file `/_mapping.txt`. Every | ||||||
|  | mapping goes on its own line. Lines should be separated by \n (Unix- | ||||||
|  | style line endings); the last line should also end with a newline. | ||||||
|  |  | ||||||
|  | Format of a line is `<RFID id>=<folder>`. RFID id is the UID of an | ||||||
|  | RFID tag, expressed as 8 lowercase characters with leading 0 (if | ||||||
|  | necessary). Folder is the foldername to play; starting with a slash and | ||||||
|  | ending without one. | ||||||
|  |  | ||||||
|  | A valid `_mapping.txt` could look like this: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | 1a2b3c4d=/Christmas Music Vol. 17 | ||||||
|  | 003aab7f=/Let it go | ||||||
|  | b691a22c=/Frozen Audiobook | ||||||
|  | 22cb6ae9=/Let it go | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | (Yes, more than one tag can map to a folder.) | ||||||
|  |  | ||||||
|  | ## Technical details | ||||||
|  |  | ||||||
|  | ### Ports | ||||||
|  |  | ||||||
|  | | Device | Port | Connected to | | ||||||
|  | | ------ | ---- | ------------ | | ||||||
|  | | VS1053 | CS | 16 | | ||||||
|  | | VS1053 | MISO | 19 | | ||||||
|  | | VS1053 | MOSI | 23 | | ||||||
|  | | VS1053 | SCK | 18 | | ||||||
|  | | VS1053 | XCS | 4 | | ||||||
|  | | VS1053 | XRESET | 0 | | ||||||
|  | | VS1053 | XDCS | 2 | | ||||||
|  | | VS1053 | DREQ | 15 | | ||||||
|  | | RC522 | SDA | 17 | | ||||||
|  | | RC522 | SCK | 18 | | ||||||
|  | | RC522 | MOSI | 23 | | ||||||
|  | | RC522 | MISO | 19 | | ||||||
|  | | AMP_L | SD | 27 | | ||||||
|  | | AMP_R | SD | 26 | | ||||||
|  | | BTN_PREV | | 22 | | ||||||
|  | | BTN_NEXT | | 33 | | ||||||
|  | | BTN_VOL_UP | | 21 | | ||||||
|  | | BTN_VOL_DOWN | | 32 | | ||||||
|  |  | ||||||
|  | Buttons pull to GND if pushed -> Internal Pull-Up needed! | ||||||
|  |  | ||||||
|  | ### RFID tags | ||||||
|  | The mapping of rfid tags to files uses the ID of the | ||||||
|  | tag. A file called `_mapping.txt` in the root folder of | ||||||
|  | the SD card defines the mappings between RFID tag ids and | ||||||
|  | folders to play. | ||||||
|  |  | ||||||
|  | The easiest way to create this file is to use the mapping | ||||||
|  | functionality of the webinterface. | ||||||
|  |  | ||||||
|  | #### Special modes | ||||||
|  | You can also save data on the tags to further manipulate | ||||||
|  | the system. Position of the data is irrelevant, the whole | ||||||
|  | tag will be searched. | ||||||
|  |  | ||||||
|  | Using `[random]` will play the files in a random order. | ||||||
|  | `[random:2]` will randomize everything except the first 2 | ||||||
|  | files. This can be useful for having the favorite song of | ||||||
|  | your kids playing, but after that getting a bit of randomness. | ||||||
|  |  | ||||||
|  | Using `[lock]` will turn this key into a key for the locking | ||||||
|  | mode. Scanning the tag enables locking mode. The next album | ||||||
|  | started will keep running until the end. Removing the tag | ||||||
|  | will be deactivated, as are the buttons for prev and next | ||||||
|  | track. You can disable locking mode by again scanning the | ||||||
|  | lock tag again. | ||||||
|  |  | ||||||
|  | `[advent]` is used for christmas time. An album with this tag | ||||||
|  | will only play in December. On December 1st, only track 1 | ||||||
|  | will play. On December 2nd, track 2 followed by track 1. On | ||||||
|  | December 3rd, tracks 3, 1 and 2. From December 24th on, track | ||||||
|  | 24 followed by tracks 1-23. So your kid will get the "daily track" | ||||||
|  | first, followed by all previous tags in the right order. | ||||||
|  |  | ||||||
|  | #### API | ||||||
|  | You can send commands to ESMP3 using three different ways: | ||||||
|  | * Through a websocket connection to `ws://<IP>/ws`. | ||||||
|  | * Through the serial console using an USB cable. | ||||||
|  | * Via HTTP POST request to `http://<IP>/cmd`, having the | ||||||
|  |   command in the variable `cmd`. | ||||||
|  |  | ||||||
|  | Supported commands are: | ||||||
|  | | Command | Action | | ||||||
|  | |---------|--------| | ||||||
|  | | `play <PATH>` | Starts playing the given path. Path may be a path on the | ||||||
|  | sd card or a http(s) URL of a webstream (direct links to mp3/4/ogg streams, | ||||||
|  | PLS files, M3U files or podcast XML feeds are supported). | | ||||||
|  | | `play` | Continues playing the previously played thing. | | ||||||
|  | | `stop` | Stops playing. | | ||||||
|  | | `volume=<X>` | Sets the volume to X (0-255). | | ||||||
|  | | `track_prev` | Starts the previous track, if available. | | ||||||
|  | | `track_next` | Starts the next track, if available. | | ||||||
|  | | `track=<X>` | Starts playing track no. X of the currently playing album. | | ||||||
|  | | `reset_vs1053` | Resets the VS1053 audio chip. | | ||||||
|  | | `reboot` | Reboots ESMP3. | | ||||||
|  | | `add_mapping=<ID>=<PATH>` | Adds a mapping between RFID card <ID> and path | ||||||
|  | <PATH>. See `play` for valid path formats. | | ||||||
|  | | `update` | Runs an update check. | | ||||||
							
								
								
									
										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 | ||||||
							
								
								
									
										2
									
								
								build_version.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								build_version.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | echo -n "-DVERSION=\\\"`git describe --tags --dirty`\\\"" | ||||||
							
								
								
									
										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 | ||||||
							
								
								
									
										84
									
								
								include/config.sample.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								include/config.sample.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | #pragma once | ||||||
|  | #include <Arduino.h> | ||||||
|  |  | ||||||
|  | // This is a simple number indicating the version for the HTTP Updater. | ||||||
|  | #define OTA_VERSION 1 | ||||||
|  | // Comment out to prevent automatic updates. | ||||||
|  | #define OTA_UPDATE_URL "https://files.schle.nz/esmp3/update.manifest" | ||||||
|  | #define OTA_CHECK_INTERVAL 12*60*60*1000 // 12 hours | ||||||
|  |  | ||||||
|  | #define SHOW_DEBUG | ||||||
|  | //#define SHOW_TRACE | ||||||
|  | #define FTP_DEBUG | ||||||
|  | #define DELAY_AFTER_DEBUG_AND_TRACE 0 | ||||||
|  |  | ||||||
|  | // Here you can define WiFi data to use. But actually, the better way to do | ||||||
|  | // this is by using /_wifi.txt on the sd card. | ||||||
|  | //#define WIFI_SSID "---CHANGEME---" | ||||||
|  | //#define WIFI_PASS "---CHANGEME---" | ||||||
|  |  | ||||||
|  | #define VS1053_SLEEP_DELAY 5000 | ||||||
|  | #define POSITION_SEND_INTERVAL 5000 | ||||||
|  |  | ||||||
|  | #define DEBOUNCE_MILLIS      200 | ||||||
|  | #define VOLUME_DEFAULT 230 | ||||||
|  | #define VOLUME_MIN 190 | ||||||
|  | #define VOLUME_MAX 255 | ||||||
|  | #define VOLUME_STEP 0x08 | ||||||
|  |  | ||||||
|  | #define RFID_SCAN_INTERVAL 100 | ||||||
|  |  | ||||||
|  | #define NUM_BUTTONS 4 | ||||||
|  |  | ||||||
|  | #define PIN_SD_CS(x) (digitalWrite(16,  x)) | ||||||
|  | #define PIN_SD_CS_SETUP() (pinMode(16, OUTPUT)) | ||||||
|  |  | ||||||
|  | #define PIN_VS1053_XCS(x) (digitalWrite(4, x)) | ||||||
|  | #define PIN_VS1053_XCS_SETUP() (pinMode(4, OUTPUT)) | ||||||
|  |  | ||||||
|  | #define PIN_VS1053_XRESET(x) (digitalWrite(0, x)) | ||||||
|  | #define PIN_VS1053_XRESET_SETUP() (pinMode(0, OUTPUT)) | ||||||
|  |  | ||||||
|  | #define PIN_VS1053_XDCS(x) (digitalWrite(2, x)) | ||||||
|  | #define PIN_VS1053_XDCS_SETUP() (pinMode(2, OUTPUT)) | ||||||
|  |  | ||||||
|  | #define PIN_VS1053_DREQ() (digitalRead(15)) | ||||||
|  | #define PIN_VS1053_DREQ_SETUP() (pinMode(15, INPUT)) | ||||||
|  |  | ||||||
|  | #define PIN_RC522_CS(x) (digitalWrite(17, x)) | ||||||
|  | #define PIN_RC522_CS_SETUP() (pinMode(17, OUTPUT)) | ||||||
|  |  | ||||||
|  | #define PIN_SPEAKER_L(x) (digitalWrite(27, x)) | ||||||
|  | #define PIN_SPEAKER_L_SETUP() (pinMode(27, OUTPUT)) | ||||||
|  |  | ||||||
|  | #define PIN_SPEAKER_R(x) (digitalWrite(26, x)) | ||||||
|  | #define PIN_SPEAKER_R_SETUP() (pinMode(26, OUTPUT)) | ||||||
|  |  | ||||||
|  | #define BTN_PREV() ( ! digitalRead(22)) | ||||||
|  | #define BTN_PREV_SETUP() (pinMode(22, INPUT_PULLUP)) | ||||||
|  |  | ||||||
|  | #define BTN_VOL_UP() ( ! digitalRead(21)) | ||||||
|  | #define BTN_VOL_UP_SETUP() (pinMode(21, INPUT_PULLUP)) | ||||||
|  |  | ||||||
|  | #define BTN_VOL_DOWN() ( ! digitalRead(32)) | ||||||
|  | #define BTN_VOL_DOWN_SETUP() (pinMode(32, INPUT_PULLUP)) | ||||||
|  |  | ||||||
|  | #define BTN_NEXT() ( ! digitalRead(33)) | ||||||
|  | #define BTN_NEXT_SETUP() (pinMode(33, INPUT_PULLUP)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // Other definitions | ||||||
|  | #define INFO(x, ...) Serial.printf(x, ##__VA_ARGS__) | ||||||
|  | #define ERROR(x, ...) Serial.printf(x, ##__VA_ARGS__) | ||||||
|  |  | ||||||
|  | #ifdef SHOW_DEBUG | ||||||
|  | 	#define DEBUG(x, ...) {Serial.printf(x, ##__VA_ARGS__); delay(DELAY_AFTER_DEBUG_AND_TRACE);} | ||||||
|  | #else | ||||||
|  | 	#define DEBUG(x, ...) while(0) {} | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef SHOW_TRACE | ||||||
|  | 	#define TRACE(x, ...) {Serial.printf(x, ##__VA_ARGS__); delay(DELAY_AFTER_DEBUG_AND_TRACE);} | ||||||
|  | #else | ||||||
|  | 	#define TRACE(x, ...) while(0) {} | ||||||
|  | #endif | ||||||
| @@ -1,30 +1,61 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include <Arduino.h> | #include <Arduino.h> | ||||||
|  | #include <ESPAsyncWebServer.h> | ||||||
| #include "config.h" | #include "config.h" | ||||||
|  |  | ||||||
|  | class Controller; | ||||||
|  |  | ||||||
| #include "player.h" | #include "player.h" | ||||||
|  | #include "playlist.h" | ||||||
|  | #include "playlist_manager.h" | ||||||
|  | #include "http_server.h" | ||||||
|  |  | ||||||
|  | #undef DEPRECATED | ||||||
| #include <MFRC522.h> | #include <MFRC522.h> | ||||||
| #include <MCP23S17/MCP23S17.h> |  | ||||||
|  | enum ControllerState { NORMAL, LOCKING, LOCKED }; | ||||||
|  |  | ||||||
| class Controller { | class Controller { | ||||||
| private: | private: | ||||||
| 	MFRC522* _rfid; | 	MFRC522* _rfid; | ||||||
| 	MCP* _mcp; | 	HTTPServer* _http_server; | ||||||
|  | 	ControllerState _state = NORMAL; | ||||||
| 	bool _rfid_enabled = true; | 	bool _rfid_enabled = true; | ||||||
| 	void _check_rfid(); | 	void _check_rfid(); | ||||||
| 	void _check_serial(); | 	void _check_serial(); | ||||||
| 	void _check_buttons(); | 	void _check_buttons(); | ||||||
|  | 	bool _debounce_button(uint8_t index); | ||||||
| 	uint32_t _get_rfid_card_uid(); | 	uint32_t _get_rfid_card_uid(); | ||||||
| 	uint32_t _last_rfid_card_uid = 0; | 	String _read_rfid_data(); | ||||||
| 	Player* _player; | 	bool _rfid_present = false; | ||||||
|  | 	String _last_rfid_uid = ""; | ||||||
|  | 	String _last_rfid_data = ""; | ||||||
|  | 	 | ||||||
| 	unsigned long _last_rfid_scan_at = 0; | 	unsigned long _last_rfid_scan_at = 0; | ||||||
|  | 	unsigned long _last_position_info_at = 0; | ||||||
|  | 	unsigned long _last_update_check_at = 0; | ||||||
|  | 	unsigned long _last_wifi_try_at = 0; | ||||||
| 	String _serial_buffer = String(); | 	String _serial_buffer = String(); | ||||||
| 	void _execute_serial_command(String cmd); | 	String _cmd_queue = ""; | ||||||
| 	void _execute_command_ls(String path); | 	void _execute_command_ls(String path); | ||||||
|  | 	void _execute_command_ids(); | ||||||
| 	void _execute_command_help(); | 	void _execute_command_help(); | ||||||
| 	unsigned long _button_last_pressed_at[NUM_BUTTONS]; | 	unsigned long _button_last_pressed_at[NUM_BUTTONS]; | ||||||
| 	bool _check_button(uint8_t btn); | 	bool _check_button(uint8_t btn); | ||||||
| public: | public: | ||||||
| 	Controller(Player* p, MCP* m); | 	Controller(Player* p, PlaylistManager* pm); | ||||||
|  | 	PlaylistManager* pm; | ||||||
|  | 	Player* player; | ||||||
|  | 	void register_http_server(HTTPServer* h); | ||||||
| 	void loop(); | 	void loop(); | ||||||
|  | 	void send_controller_status(); | ||||||
|  | 	void send_player_status(); | ||||||
|  | 	void send_playlist_manager_status(); | ||||||
|  | 	void send_position(); | ||||||
|  | 	void inform_new_client(AsyncWebSocketClient* client); | ||||||
|  | 	String json(); | ||||||
|  | 	bool process_message(String m); | ||||||
|  | 	void queue_command(String cmd); | ||||||
|  | 	void update_playlist_manager(); | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								include/data_sources.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								include/data_sources.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <Arduino.h> | ||||||
|  | #include <SD.h> | ||||||
|  | #include "config.h" | ||||||
|  | #include "http_client_wrapper.h" | ||||||
|  |  | ||||||
|  | class DataSource { | ||||||
|  | private: | ||||||
|  | public: | ||||||
|  | 	DataSource() {}; | ||||||
|  | 	virtual ~DataSource() {}; | ||||||
|  | 	virtual size_t read(uint8_t* buf, size_t len) = 0; | ||||||
|  | 	virtual int read() = 0; | ||||||
|  | 	virtual size_t position() = 0; | ||||||
|  | 	virtual void seek(size_t position) = 0; | ||||||
|  | 	virtual size_t size() = 0; | ||||||
|  | 	virtual void close() = 0; | ||||||
|  | 	virtual void skip_id3_tag() {}; | ||||||
|  | 	virtual bool usable() = 0; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class SDDataSource : public DataSource { | ||||||
|  | private: | ||||||
|  | 	File _file; | ||||||
|  | public: | ||||||
|  | 	SDDataSource(String file); | ||||||
|  | 	~SDDataSource(); | ||||||
|  | 	size_t read(uint8_t* buf, size_t len); | ||||||
|  | 	int read(); | ||||||
|  | 	size_t position(); | ||||||
|  | 	void seek(size_t position); | ||||||
|  | 	size_t size(); | ||||||
|  | 	void close(); | ||||||
|  | 	void skip_id3_tag(); | ||||||
|  | 	bool usable(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class HTTPSDataSource : public DataSource { | ||||||
|  | private: | ||||||
|  | 	WiFiClient* _stream = NULL; | ||||||
|  | 	HTTPClientWrapper* _http = NULL; | ||||||
|  | 	uint32_t _position; | ||||||
|  | public: | ||||||
|  | 	HTTPSDataSource(String url, uint32_t offset=0); | ||||||
|  | 	~HTTPSDataSource(); | ||||||
|  | 	size_t read(uint8_t* buf, size_t len); | ||||||
|  | 	int read(); | ||||||
|  | 	size_t position(); | ||||||
|  | 	void seek(size_t position); | ||||||
|  | 	size_t size(); | ||||||
|  | 	void close(); | ||||||
|  | 	bool usable(); | ||||||
|  | }; | ||||||
							
								
								
									
										37
									
								
								include/http_client_wrapper.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								include/http_client_wrapper.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | #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(); | ||||||
|  | }; | ||||||
							
								
								
									
										29
									
								
								include/http_server.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								include/http_server.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | class HTTPServer; | ||||||
|  |  | ||||||
|  | #include "player.h" | ||||||
|  | #include "controller.h" | ||||||
|  | #include <AsyncTCP.h> | ||||||
|  | #include <ESPAsyncWebServer.h> | ||||||
|  |  | ||||||
|  | class HTTPServer { | ||||||
|  | private: | ||||||
|  | 	AsyncWebServer* _server; | ||||||
|  | 	 | ||||||
|  | 	Player* _player; | ||||||
|  | 	Controller* _controller; | ||||||
|  | 	void _handle_upload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final); | ||||||
|  | 	uint16_t _chunk_length; | ||||||
|  | 	uint8_t* _chunk; | ||||||
|  | 	File _upload_file; | ||||||
|  | 	uint32_t _file_size; | ||||||
|  | 	uint32_t _file_size_done; | ||||||
|  | 	bool _need_header; | ||||||
|  | 	uint32_t _upload_position; | ||||||
|  | 	void _onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len); | ||||||
|  | 	void _handle_index(AsyncWebServerRequest* req); | ||||||
|  | public: | ||||||
|  | 	HTTPServer(Player* p, Controller* c); | ||||||
|  | 	AsyncWebSocket* ws; | ||||||
|  | }; | ||||||
							
								
								
									
										5
									
								
								include/main.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								include/main.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | void wifi_connect(); | ||||||
|  |  | ||||||
|  | extern const uint8_t file_index_html_start[] asm("_binary_src_index_html_start"); | ||||||
| @@ -2,45 +2,52 @@ | |||||||
| #include "config.h" | #include "config.h" | ||||||
| #include <SPI.h> | #include <SPI.h> | ||||||
| #include <SD.h> | #include <SD.h> | ||||||
| #include <list> | #include "spi_master.h" | ||||||
| #include <map> | #include "playlist.h" | ||||||
| #include <MCP23S17/MCP23S17.h> | #include "data_sources.h" | ||||||
|  |  | ||||||
|  | class Player; | ||||||
|  |  | ||||||
|  | #include "controller.h" | ||||||
|  |  | ||||||
| #define SCI_MODE 0x00 | #define SCI_MODE 0x00 | ||||||
| #define SCI_STATUS 0x01 | #define SCI_STATUS 0x01 | ||||||
|  | #define SCI_BASS 0x02 | ||||||
| #define SCI_CLOCKF 0x03 | #define SCI_CLOCKF 0x03 | ||||||
| #define SCI_DECODE_TIME 0x04 | #define SCI_DECODE_TIME 0x04 | ||||||
|  | #define SCI_AUDATA 0x05 | ||||||
| #define SCI_VOL 0x0B | #define SCI_VOL 0x0B | ||||||
| #define SCI_WRAMADDR 0x07 | #define SCI_WRAMADDR 0x07 | ||||||
| #define SCI_WRAM 0x06 | #define SCI_WRAM 0x06 | ||||||
|  | #define SCI_HDAT0 0x08 | ||||||
|  | #define SCI_HDAT1 0x09 | ||||||
|  | #define SCI_AIADDR 0x0A | ||||||
|  | #define SCI_AICTRL0 0x0C | ||||||
|  | #define SCI_AICTRL1 0x0D | ||||||
|  | #define SCI_AICTRL2 0x0E | ||||||
|  | #define SCI_AICTRL3 0x0F | ||||||
|  |  | ||||||
| #define CMD_WRITE 0x02 | #define CMD_WRITE 0x02 | ||||||
| #define CMD_READ 0x03 | #define CMD_READ 0x03 | ||||||
|  |  | ||||||
| #define ADDR_ENDBYTE 0x1E06 | #define ADDR_ENDBYTE 0x1E06 | ||||||
|  |  | ||||||
|  | #define SM_LAYER12 0x0001 | ||||||
|  | #define SM_RESET 0x0004 | ||||||
| #define SM_CANCEL 0x0008 | #define SM_CANCEL 0x0008 | ||||||
|  | #define SM_SDINEW 0x0800 | ||||||
|  | #define SM_ADPCM 0x1000 | ||||||
| #define SS_DO_NOT_JUMP 0x8000 | #define SS_DO_NOT_JUMP 0x8000 | ||||||
|  |  | ||||||
| #define XRESET PIN_VS1053_XRESET |  | ||||||
| #define DREQ PIN_VS1053_DREQ |  | ||||||
| #define XCS PIN_VS1053_XCS |  | ||||||
| #define XDCS PIN_VS1053_XDCS |  | ||||||
|  |  | ||||||
| class Player { | class Player { | ||||||
| private: | private: | ||||||
| 	enum state { uninitialized, idle, playing, stopping, | 	enum state { uninitialized, idle, playing, stopping, | ||||||
| 		system_sound_while_playing, system_sound_while_stopped }; | 		sleeping, recording }; | ||||||
| 	struct album_state { |  | ||||||
| 		uint8_t index; |  | ||||||
| 		uint32_t position; |  | ||||||
| 	}; |  | ||||||
| 	void _check_system_sound(String filename); |  | ||||||
| 	void _reset(); | 	void _reset(); | ||||||
| 	void _init(); |  | ||||||
| 	void _wait(); | 	void _wait(); | ||||||
| 	uint16_t _read_control_register(uint8_t address); | 	uint16_t _read_control_register(uint8_t address, bool do_wait=true); | ||||||
| 	void _write_control_register(uint8_t address, uint16_t value); | 	void _write_control_register(uint8_t address, uint16_t value, bool do_wait=true); | ||||||
|  | 	void _write_direct(uint8_t address, uint16_t value); | ||||||
| 	void _write_data(uint8_t* data); | 	void _write_data(uint8_t* data); | ||||||
| 	uint16_t _read_wram(uint16_t address); | 	uint16_t _read_wram(uint16_t address); | ||||||
| 	state _state = state::uninitialized; | 	state _state = state::uninitialized; | ||||||
| @@ -49,44 +56,49 @@ private: | |||||||
| 	void _flush_and_cancel(); | 	void _flush_and_cancel(); | ||||||
| 	int8_t _get_endbyte(); | 	int8_t _get_endbyte(); | ||||||
| 	void _flush(uint count, int8_t fill_byte); | 	void _flush(uint count, int8_t fill_byte); | ||||||
| 	void _set_last_track(const char* album, uint8_t track, uint32_t position); |  | ||||||
| 	std::map<String, album_state> _last_tracks; |  | ||||||
| 	void _play_file(String filename, uint32_t offset); | 	void _play_file(String filename, uint32_t offset); | ||||||
| 	uint32_t _id3_tag_offset(File f); |  | ||||||
| 	void _finish_playing(); | 	void _finish_playing(); | ||||||
| 	void _finish_stopping(); | 	void _finish_stopping(bool turn_speaker_off); | ||||||
| 	void _mute(); | 	void _mute(); | ||||||
| 	void _unmute(); | 	void _unmute(); | ||||||
|  | 	void _sleep(); | ||||||
|  | 	void _wakeup(); | ||||||
|  | 	void _record(); | ||||||
|  | 	void _patch_adpcm(); | ||||||
|  | 	void _speaker_off(); | ||||||
|  | 	void _speaker_on(); | ||||||
|  |  | ||||||
| 	SPISettings _spi_settings_slow = SPISettings(250000, MSBFIRST, SPI_MODE0); | 	SPISettings _spi_settings_slow = SPISettings(250000, MSBFIRST, SPI_MODE0); | ||||||
| 	SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0); | 	SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0); | ||||||
| 	SPISettings* _spi_settings = &_spi_settings_slow; | 	SPISettings* _spi_settings = &_spi_settings_slow; | ||||||
|  |  | ||||||
| 	std::list<String> _files_in_dir(String dir); | 	DataSource* _file; | ||||||
| 	String _find_album_dir(String album); | 	uint32_t _file_size = 0; | ||||||
| 	File _file; |  | ||||||
| 	uint8_t _buffer[32]; | 	uint8_t _buffer[32]; | ||||||
| 	String _playing_album; | 	uint32_t _current_play_position = 0; | ||||||
| 	uint8_t _playing_index; | 	Playlist* _current_playlist = NULL; | ||||||
| 	uint8_t _playing_album_songs; |  | ||||||
| 	uint32_t _current_play_position; |  | ||||||
| 	uint _refills; | 	uint _refills; | ||||||
| 	uint8_t _volume; | 	uint8_t _volume; | ||||||
| 	uint16_t _stop_delay; | 	uint16_t _stop_delay; | ||||||
| 	uint32_t _skip_to; | 	uint32_t _skip_to; | ||||||
| 	MCP* _mcp; | 	SPIMaster* _spi; | ||||||
|  | 	Controller* _controller; | ||||||
|  | 	unsigned long _stopped_at; | ||||||
| public: | public: | ||||||
| 	Player(MCP* m); | 	Player(SPIMaster* s); | ||||||
|  | 	void init(); | ||||||
|  | 	void register_controller(Controller* c); | ||||||
| 	void vol_up(); | 	void vol_up(); | ||||||
| 	void vol_down(); | 	void vol_down(); | ||||||
| 	void track_next(); | 	void track_next(); | ||||||
| 	void track_prev(); | 	void track_prev(); | ||||||
|  | 	void set_track(uint8_t track); | ||||||
| 	bool play_album(String album); | 	bool is_playing(); | ||||||
| 	bool play_song(String album, uint8_t song_index, uint32_t offset=0); | 	bool play(); | ||||||
| 	void play_system_sound(String filename); | 	bool play(Playlist* p); | ||||||
| 	void stop(); | 	void stop(bool turn_speaker_off=true); | ||||||
| 	bool loop(); | 	bool loop(); | ||||||
| 	void set_volume(uint8_t vol, bool save = true); | 	void set_volume(uint8_t vol, bool save = true); | ||||||
| 	std::list<String> ls(String path); | 	String position_json(); | ||||||
|  | 	String json(); | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										59
									
								
								include/playlist.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								include/playlist.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | #pragma once | ||||||
|  | #include <Arduino.h> | ||||||
|  | #include <vector> | ||||||
|  | #include <ArduinoJson.h> | ||||||
|  | #include "http_client_wrapper.h" | ||||||
|  |  | ||||||
|  | enum PlaylistPersistence { | ||||||
|  | 	PERSIST_NONE, | ||||||
|  | 	PERSIST_TEMPORARY, | ||||||
|  | 	PERSIST_PERMANENTLY | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | struct PlaylistEntry { | ||||||
|  | 	String filename; | ||||||
|  | 	String title; | ||||||
|  | 	String id; | ||||||
|  | 	 | ||||||
|  | 	bool operator<(PlaylistEntry p) { return title < p.title; } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class Playlist { | ||||||
|  | private: | ||||||
|  | 	uint32_t _position = 0; | ||||||
|  | 	uint32_t _current_track = 0; | ||||||
|  | 	bool _started = false; | ||||||
|  | 	bool _shuffled = false; | ||||||
|  | 	std::vector<PlaylistEntry> _files; | ||||||
|  | 	String _title = ""; | ||||||
|  | 	String _path; | ||||||
|  | 	void _add_path(String path); | ||||||
|  | 	void _examine_http_url(String url); | ||||||
|  | 	void _parse_rss(HTTPClientWrapper* http); | ||||||
|  | 	void _parse_m3u(HTTPClientWrapper* http); | ||||||
|  | 	void _parse_pls(HTTPClientWrapper* http); | ||||||
|  | public: | ||||||
|  | 	PlaylistPersistence persistence = PERSIST_TEMPORARY; | ||||||
|  | 	Playlist(String path); | ||||||
|  | 	void start(); | ||||||
|  | 	uint16_t get_file_count(); | ||||||
|  | 	bool has_track_next(); | ||||||
|  | 	bool has_track_prev(); | ||||||
|  | 	bool track_next(); | ||||||
|  | 	bool track_prev(); | ||||||
|  | 	void track_restart(); | ||||||
|  | 	bool set_track(uint8_t track); | ||||||
|  | 	void set_track_by_id(String id); | ||||||
|  | 	void reset(); | ||||||
|  | 	String path(); | ||||||
|  | 	bool is_empty(); | ||||||
|  | 	bool get_current_file(String* dst); | ||||||
|  | 	String get_current_track_id(); | ||||||
|  | 	uint32_t get_position(); | ||||||
|  | 	void set_position(uint32_t p); | ||||||
|  | 	void shuffle(uint8_t random_offset=0); | ||||||
|  | 	void advent_shuffle(uint8_t day); | ||||||
|  | 	bool is_fresh(); | ||||||
|  | 	void dump(); | ||||||
|  | 	void json(JsonObject json); | ||||||
|  | }; | ||||||
							
								
								
									
										24
									
								
								include/playlist_manager.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								include/playlist_manager.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <map> | ||||||
|  | #include <vector> | ||||||
|  | #include "playlist.h" | ||||||
|  |  | ||||||
|  | class PlaylistManager { | ||||||
|  | private: | ||||||
|  | 	std::map<String, String> _map; | ||||||
|  | 	std::map<String, Playlist*> _playlists; | ||||||
|  | 	std::vector<String> _unmapped_folders; | ||||||
|  | 	void _check_for_special_chars(String s); | ||||||
|  | 	void _save_mapping(); | ||||||
|  | public: | ||||||
|  | 	PlaylistManager(); | ||||||
|  | 	Playlist* get_playlist_for_id(String id); | ||||||
|  | 	Playlist* get_playlist_for_folder(String folder); | ||||||
|  | 	void dump_ids(); | ||||||
|  | 	void scan_files(); | ||||||
|  | 	String json(); | ||||||
|  | 	bool add_mapping(String id, String folder); | ||||||
|  | 	String create_mapping_txt(); | ||||||
|  | 	void persist(Playlist* p); | ||||||
|  | }; | ||||||
| @@ -6,30 +6,65 @@ | |||||||
|  |  | ||||||
| class SPIMaster { | class SPIMaster { | ||||||
| public: | public: | ||||||
|  | 	static uint8_t state; | ||||||
|  |  | ||||||
| 	static void init() { | 	static void init() { | ||||||
| 		SPI.setHwCs(false); | 		PIN_SD_CS_SETUP(); | ||||||
| 		pinMode(PIN_SD_CS, OUTPUT); | 		PIN_VS1053_XCS_SETUP(); | ||||||
| 		pinMode(PIN_VS1053_XCS, OUTPUT); | 		PIN_VS1053_XDCS_SETUP(); | ||||||
| 		pinMode(PIN_VS1053_XDCS, OUTPUT); | 		PIN_RC522_CS_SETUP(); | ||||||
| 		pinMode(PIN_MCP, OUTPUT); | 		disable(); | ||||||
| 	} |  | ||||||
| 	static void enable(uint8_t pin) { |  | ||||||
| 		digitalWrite(PIN_SD_CS, pin==PIN_SD_CS ? LOW : HIGH); |  | ||||||
| 		digitalWrite(PIN_VS1053_XCS, pin==PIN_VS1053_XCS ? LOW : HIGH); |  | ||||||
| 		digitalWrite(PIN_VS1053_XDCS, pin==PIN_VS1053_XDCS ? LOW : HIGH); |  | ||||||
| 		digitalWrite(PIN_MCP, pin==PIN_MCP ? LOW : HIGH); |  | ||||||
|  |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static void printStatus() { | 	static void select_sd(bool enabled=true) { | ||||||
| 		Serial.printf("CS state: SD:%d, VS1053_XCS:%d, VS1053_XDCS:%d, MCP:%d\n", | 		PIN_SD_CS(enabled ? LOW : HIGH); | ||||||
| 		digitalRead(PIN_SD_CS), | 		if (enabled) { | ||||||
| 		digitalRead(PIN_VS1053_XCS), | 			state |= 1; | ||||||
| 		digitalRead(PIN_VS1053_XDCS), | 		} else { | ||||||
| 		digitalRead(PIN_MCP)); | 			state &= ~1; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static void select_vs1053_xcs(bool enabled=true) { | ||||||
|  | 		PIN_VS1053_XCS(enabled ? LOW : HIGH); | ||||||
|  | 		if (enabled) { | ||||||
|  | 			state |= 2; | ||||||
|  | 		} else { | ||||||
|  | 			state &= ~2; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static void select_vs1053_xdcs(bool enabled=true) { | ||||||
|  | 		PIN_VS1053_XDCS(enabled ? LOW : HIGH); | ||||||
|  | 		if (enabled) { | ||||||
|  | 			state |= 4; | ||||||
|  | 		} else { | ||||||
|  | 			state &= ~4; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static void select_rc522(bool enabled=true) { | ||||||
|  | 		PIN_RC522_CS(enabled ? LOW : HIGH); | ||||||
|  | 		if (enabled) { | ||||||
|  | 			state |= 8; | ||||||
|  | 		} else { | ||||||
|  | 			state &= ~8; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	static void set_state(uint8_t s) { | ||||||
|  | 		disable(); | ||||||
|  | 		if (s & 1) select_sd(); | ||||||
|  | 		if (s & 2) select_vs1053_xcs(); | ||||||
|  | 		if (s & 4) select_vs1053_xdcs(); | ||||||
|  | 		if (s & 8) select_rc522(); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	static void disable() { | 	static void disable() { | ||||||
| 		enable(142); | 		PIN_SD_CS(HIGH); | ||||||
|  | 		PIN_VS1053_XCS(HIGH); | ||||||
|  | 		PIN_VS1053_XDCS(HIGH); | ||||||
|  | 		PIN_RC522_CS(HIGH); | ||||||
|  | 		state = 0; | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
							
								
								
									
										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,11 +8,33 @@ | |||||||
| ; 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:esp12e] | [platformio] | ||||||
| platform = espressif8266 | default_envs = esp32 | ||||||
| board = esp12e |  | ||||||
|  | [extra] | ||||||
|  | lib_deps = | ||||||
|  | 	63 ; MFRC522 | ||||||
|  | 	https://github.com/me-no-dev/ESPAsyncWebServer.git | ||||||
|  | 	ArduinoJSON | ||||||
|  | 	6691 ; TinyXML | ||||||
|  |  | ||||||
|  | [env:esp32] | ||||||
|  | platform = espressif32 | ||||||
|  | board = esp-wrover-kit | ||||||
| framework = arduino | framework = arduino | ||||||
| upload_speed = 512000 | upload_speed = 512000 | ||||||
| lib_deps = 63 | build_flags=!./build_version.sh | ||||||
|     https://github.com/n0mjs710/MCP23S17.git | lib_deps = ${extra.lib_deps} | ||||||
| upload_port = /dev/cu.wchusbserial1420 | upload_port = /dev/cu.SLAB_USBtoUART | ||||||
|  | monitor_speed = 115200 | ||||||
|  | board_build.embed_txtfiles = src/index.html | ||||||
|  | ;board_build.partitions = partitions.csv | ||||||
|  | ;monitor_port = /dev/cu.wchusbserial1420 | ||||||
|  |  | ||||||
|  | [env:deploy] | ||||||
|  | platform = espressif32 | ||||||
|  | board = esp-wrover-kit | ||||||
|  | framework = arduino | ||||||
|  | lib_deps = ${extra.lib_deps} | ||||||
|  | board_build.embed_txtfiles = src/index.html | ||||||
|  | board_build.partitions = partitions.csv | ||||||
|   | |||||||
| @@ -1,41 +1,74 @@ | |||||||
| #include "controller.h" | #include "controller.h" | ||||||
|  | #include "main.h" | ||||||
| #include "spi_master.h" | #include "spi_master.h" | ||||||
|  | #include "config.h" | ||||||
|  | #include "playlist.h" | ||||||
|  | #include "http_server.h" | ||||||
|  | #include "updater.h" | ||||||
|  | #include <ArduinoJson.h> | ||||||
|  |  | ||||||
| Controller::Controller(Player* p, MCP* m) { | Controller::Controller(Player* p, PlaylistManager* playlist_manager) { | ||||||
| 	_player = p; | 	player = p; | ||||||
| 	_mcp = m; | 	pm = playlist_manager; | ||||||
| 	_rfid = new MFRC522(PIN_RC522_CS, MFRC522::UNUSED_PIN); | 	_rfid = new MFRC522(17, MFRC522::UNUSED_PIN); | ||||||
| 	 | 	 | ||||||
| 	SPIMaster::enable(PIN_MCP); | 	player->register_controller(this); | ||||||
| 	_mcp->pinMode(1, INPUT); _mcp->pullupMode(1, HIGH); |  | ||||||
| 	_mcp->pinMode(2, INPUT); _mcp->pullupMode(2, HIGH); |  | ||||||
| 	_mcp->pinMode(3, INPUT); _mcp->pullupMode(3, HIGH); |  | ||||||
| 	_mcp->pinMode(4, INPUT); _mcp->pullupMode(4, HIGH); |  | ||||||
|  |  | ||||||
| 	SPIMaster::enable(PIN_RC522_CS); | 	BTN_NEXT_SETUP(); | ||||||
| 	DEBUG("Initializing RC522..."); | 	BTN_PREV_SETUP(); | ||||||
|  | 	BTN_VOL_UP_SETUP(); | ||||||
|  | 	BTN_VOL_DOWN_SETUP(); | ||||||
|  |  | ||||||
|  | 	SPIMaster::select_rc522(); | ||||||
|  | 	DEBUG("Initializing RC522...\n"); | ||||||
| 	_rfid->PCD_Init(); | 	_rfid->PCD_Init(); | ||||||
| 	#ifdef SHOW_DEBUG | 	#ifdef SHOW_DEBUG | ||||||
| 		_rfid->PCD_DumpVersionToSerial(); | 		_rfid->PCD_DumpVersionToSerial(); | ||||||
| 	#endif | 	#endif | ||||||
| 	SPIMaster::disable(); | 	SPIMaster::select_rc522(false); | ||||||
| 	INFO("RC522 initialized.\n"); | 	INFO("RC522 initialized.\n"); | ||||||
|  |  | ||||||
| 	for (uint8_t i=0; i<NUM_BUTTONS; i++) _button_last_pressed_at[i]=0; | 	for (uint8_t i=0; i<NUM_BUTTONS; i++) _button_last_pressed_at[i]=0; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void Controller::register_http_server(HTTPServer* h) { | ||||||
|  | 	_http_server = h; | ||||||
|  | } | ||||||
|  |  | ||||||
| void Controller::loop() { | void Controller::loop() { | ||||||
| 	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(); | ||||||
| 		_last_rfid_scan_at = now; | 		_last_rfid_scan_at = now; | ||||||
| 	} | 	} | ||||||
|  | 	if ((_last_position_info_at < now - POSITION_SEND_INTERVAL) || (now < _last_position_info_at)) { | ||||||
|  | 		send_position(); | ||||||
|  | 		_last_position_info_at = now; | ||||||
|  | 	} | ||||||
| 	_check_serial(); | 	_check_serial(); | ||||||
| 	_check_buttons(); | 	_check_buttons(); | ||||||
|  | 	if (_cmd_queue.length() > 0) { | ||||||
|  | 		process_message(_cmd_queue); | ||||||
|  | 		_cmd_queue = ""; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	#ifdef OTA_UPDATE_URL | ||||||
|  | 	if (!player->is_playing() && _last_update_check_at < now && _last_update_check_at + OTA_CHECK_INTERVAL < now) { | ||||||
|  | 		Updater::run(); | ||||||
|  | 	} else { | ||||||
|  | 		_last_update_check_at = now; | ||||||
|  | 	} | ||||||
|  | 	#endif | ||||||
|  | 	 | ||||||
|  | 	if (!player->is_playing() && !WiFi.isConnected() && _last_wifi_try_at < now && _last_wifi_try_at + 5*60*1000 < now) { | ||||||
|  | 		wifi_connect(); | ||||||
|  | 	} else { | ||||||
|  | 		_last_wifi_try_at = now; | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| uint32_t Controller::_get_rfid_card_uid() { | uint32_t Controller::_get_rfid_card_uid() { | ||||||
| 	SPIMaster::enable(PIN_RC522_CS); | 	SPIMaster::select_rc522(); | ||||||
| 	if (!_rfid->PICC_ReadCardSerial()) { | 	if (!_rfid->PICC_ReadCardSerial()) { | ||||||
| 		if (!_rfid->PICC_IsNewCardPresent()) { | 		if (!_rfid->PICC_IsNewCardPresent()) { | ||||||
| 			return 0; | 			return 0; | ||||||
| @@ -44,25 +77,170 @@ uint32_t Controller::_get_rfid_card_uid() { | |||||||
| 			return 0; | 			return 0; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	SPIMaster::select_rc522(false); | ||||||
| 	uint32_t uid = _rfid->uid.uidByte[0]<<24 | _rfid->uid.uidByte[1]<<16 | _rfid->uid.uidByte[2]<<8 | _rfid->uid.uidByte[3]; | 	uint32_t uid = _rfid->uid.uidByte[0]<<24 | _rfid->uid.uidByte[1]<<16 | _rfid->uid.uidByte[2]<<8 | _rfid->uid.uidByte[3]; | ||||||
| 	SPIMaster::disable(); |  | ||||||
| 	return uid; | 	return uid; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Controller::_check_rfid() { | void Controller::_check_rfid() { | ||||||
| 	uint32_t uid = _get_rfid_card_uid(); | 	//TRACE("check_rfid running...\n"); | ||||||
| 	if (uid != _last_rfid_card_uid) { | 	MFRC522::StatusCode status; | ||||||
| 		if (uid > 0) { | 	if (_rfid_present) { | ||||||
| 			INFO("New RFID card uid: %08x\n", uid); | 		byte buffer[2]; | ||||||
| 			String s_uid = String(uid, HEX); | 		byte buffer_size = 2; | ||||||
| 			_player->play_album(s_uid); | 		SPIMaster::select_rc522(); | ||||||
|  | 		status = _rfid->PICC_WakeupA(buffer, &buffer_size); | ||||||
|  | 		if (status == MFRC522::STATUS_OK) { | ||||||
|  | 			// Card is still present. | ||||||
|  | 			_rfid->PICC_HaltA(); | ||||||
|  | 			SPIMaster::select_rc522(false); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		SPIMaster::select_rc522(false); | ||||||
|  | 		// Card is now gone | ||||||
|  | 		_rfid_present = false; | ||||||
|  | 		INFO("No more RFID card.\n"); | ||||||
|  | 		if (_state != LOCKED) { | ||||||
|  | 			player->stop(); | ||||||
|  | 		} | ||||||
|  | 		send_controller_status(); | ||||||
| 	} else { | 	} else { | ||||||
| 			INFO("No more RFID card."); | 		uint32_t uid = _get_rfid_card_uid(); | ||||||
| 			_player->stop(); | 		if (uid > 0) { | ||||||
|  | 			String temp = String(uid, HEX); | ||||||
|  | 			String s_uid = ""; | ||||||
|  | 			for (int i=0; i<(8-temp.length()); i++) { | ||||||
|  | 				s_uid.concat("0"); | ||||||
| 			} | 			} | ||||||
| 		_last_rfid_card_uid = uid; | 			s_uid.concat(temp); | ||||||
|  | 			INFO("New RFID card uid: %s\n", s_uid.c_str()); | ||||||
|  | 			_last_rfid_uid = s_uid; | ||||||
|  | 			_rfid_present = true; | ||||||
|  | 			 | ||||||
|  | 			String data = _read_rfid_data(); | ||||||
|  | 			_last_rfid_data = data; | ||||||
|  |  | ||||||
|  | 			Playlist* pl = pm->get_playlist_for_id(s_uid); | ||||||
|  | 			if (data.indexOf("[lock]") != -1) { | ||||||
|  | 				if (_state == LOCKED) { | ||||||
|  | 					_state = NORMAL; | ||||||
|  | 					DEBUG("ControllerState is now UNLOCKED\n"); | ||||||
|  | 				} else { | ||||||
|  | 					DEBUG("ControllerState is now LOCKING\n"); | ||||||
|  | 					_state = LOCKING; | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | 			if (pl==NULL) { | ||||||
|  | 				INFO("Could not find album for id '%s'.\n", s_uid.c_str()); | ||||||
|  | 				send_controller_status(); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			int index; | ||||||
|  | 			if (data.indexOf("[advent]") != -1 && pl->is_fresh()) { | ||||||
|  | 				struct tm time; | ||||||
|  | 				getLocalTime(&time); | ||||||
|  | 				if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december. | ||||||
|  | 					pl->advent_shuffle(time.tm_mday); | ||||||
|  | 				} else { | ||||||
|  | 					DEBUG("Album is in advent mode, but it isn't december (yet). Not playing.\n"); | ||||||
|  | 					return; | ||||||
|  | 				} | ||||||
|  | 			} else if (data.indexOf("[random]") != -1 && pl->is_fresh()) { | ||||||
|  | 				pl->shuffle(); | ||||||
|  | 			} else if ((index=data.indexOf("[random:")) != -1 && pl->is_fresh()) { | ||||||
|  | 				String temp = data.substring(index + 8); | ||||||
|  | 				index = temp.indexOf("]"); | ||||||
|  | 				TRACE("temp: %s, temp.substring(0, %d): %s\n", temp.c_str(), index, temp.substring(0, index).c_str()); | ||||||
|  | 				if (index>0) { | ||||||
|  | 					uint8_t random_offset = temp.substring(0, index).toInt(); | ||||||
|  | 					pl->shuffle(random_offset); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			if (_state == LOCKED) { | ||||||
|  | 				DEBUG("ControllerState is LOCKED, ignoring card.\n"); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			if (_state == LOCKING) { | ||||||
|  | 				_state = LOCKED; | ||||||
|  | 				DEBUG("ControllerState is now LOCKED.\n"); | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			player->play(pl); | ||||||
|  | 			//send_playlist_manager_status(); | ||||||
|  | 			send_controller_status(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | String Controller::_read_rfid_data() { | ||||||
|  | 	TRACE("_read_rfid_data() running...\n"); | ||||||
|  | 	static MFRC522::MIFARE_Key keys[8] = { | ||||||
|  | 		{{0xd3, 0xf7, 0xd3, 0xf7, 0xd3, 0xf7}}, // D3 F7 D3 F7 D3 F7 | ||||||
|  | 		{{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}, // FF FF FF FF FF FF = factory default | ||||||
|  | 		{{0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5}}, // A0 A1 A2 A3 A4 A5 | ||||||
|  | 		{{0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5}}, // B0 B1 B2 B3 B4 B5 | ||||||
|  | 		{{0x4d, 0x3a, 0x99, 0xc3, 0x51, 0xdd}}, // 4D 3A 99 C3 51 DD | ||||||
|  | 		{{0x1a, 0x98, 0x2c, 0x7e, 0x45, 0x9a}}, // 1A 98 2C 7E 45 9A | ||||||
|  | 		{{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}}, // AA BB CC DD EE FF | ||||||
|  | 		{{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}  // 00 00 00 00 00 00 | ||||||
|  | 	}; | ||||||
|  | 	SPIMaster::select_rc522(); | ||||||
|  | 	DEBUG("Trying to read RFID data...\n"); | ||||||
|  | 	 | ||||||
|  | 	String data = ""; | ||||||
|  | 	MFRC522::PICC_Type type = _rfid->PICC_GetType(_rfid->uid.sak); | ||||||
|  | 	 | ||||||
|  | 	uint8_t sectors = 0; | ||||||
|  | 	switch(type) { | ||||||
|  | 		case MFRC522::PICC_TYPE_MIFARE_MINI: sectors = 5; break; | ||||||
|  | 		case MFRC522::PICC_TYPE_MIFARE_1K: sectors = 16; break; | ||||||
|  | 		case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break; | ||||||
|  | 		default: INFO("Unknown PICC type %s\n", String(MFRC522::PICC_GetTypeName(type)).c_str()); | ||||||
|  | 	} | ||||||
|  | 	sectors = 2; // Pretend we have only two sectors, so we read only sector #1. | ||||||
|  | 	int good_key_index = -1; | ||||||
|  | 	for (uint8_t sector=1; sector<sectors; sector++) { | ||||||
|  | 		uint8_t blocks = (sector < 32) ? 4 : 16; | ||||||
|  | 		uint8_t block_offset = (sector < 32) ? sector * 4 : 128 + (sector - 32) * 16; | ||||||
|  | 		 | ||||||
|  | 		MFRC522::StatusCode status; | ||||||
|  |  | ||||||
|  | 		for (int i=0; i<8; i++) { | ||||||
|  | 			MFRC522::MIFARE_Key *k = &keys[i]; | ||||||
|  | 			TRACE("Trying MIFARE key %02X %02X %02X %02X %02X %02X...\n", k->keyByte[0], k->keyByte[1], k->keyByte[2], k->keyByte[3], k->keyByte[4], k->keyByte[5]); | ||||||
|  | 			status = _rfid->PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, block_offset, k, &_rfid->uid); | ||||||
|  | 			if (status == MFRC522::STATUS_OK) { | ||||||
|  | 				TRACE("Authentication succeeded with key #%d\n", i); | ||||||
|  | 				good_key_index = i; | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if (good_key_index == -1) { | ||||||
|  | 			TRACE("Could not find a valid MIFARE key.\n"); | ||||||
|  | 		} else {		 | ||||||
|  | 			for (uint8_t block=0; block<blocks-1; block++) { | ||||||
|  | 				byte buffer[18]; | ||||||
|  | 				uint8_t byte_count = 18; | ||||||
|  | 				status = _rfid->MIFARE_Read(block_offset + block, buffer, &byte_count); | ||||||
|  | 				if (status != MFRC522::STATUS_OK) { | ||||||
|  | 					DEBUG("MIFARE_Read() failed: %s\n", String(_rfid->GetStatusCodeName(status)).c_str()); | ||||||
|  | 					continue; | ||||||
|  | 				} | ||||||
|  | 				for (int i=0; i<16; i++) { | ||||||
|  | 					if (buffer[i]>=0x20 && buffer[i]<0x7F) data.concat((char)buffer[i]); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	_rfid->PICC_HaltA(); | ||||||
|  | 	_rfid->PCD_StopCrypto1(); | ||||||
|  | 	DEBUG("Data from RFID: %s\n", data.c_str()); | ||||||
|  | 	SPIMaster::select_rc522(false); | ||||||
|  | 	return data; | ||||||
|  | } | ||||||
|  |  | ||||||
| void Controller::_check_serial() { | void Controller::_check_serial() { | ||||||
| 	if (Serial.available() > 0) { | 	if (Serial.available() > 0) { | ||||||
| @@ -70,7 +248,7 @@ void Controller::_check_serial() { | |||||||
| 		Serial.printf("%c", c); | 		Serial.printf("%c", c); | ||||||
| 		if (c==10 || c==13) { | 		if (c==10 || c==13) { | ||||||
| 			if (_serial_buffer.length()>0) { | 			if (_serial_buffer.length()>0) { | ||||||
| 				_execute_serial_command(_serial_buffer); | 				process_message(_serial_buffer); | ||||||
| 				_serial_buffer = String(); | 				_serial_buffer = String(); | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| @@ -79,90 +257,189 @@ void Controller::_check_serial() { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| void Controller::_execute_serial_command(String cmd) { | bool Controller::process_message(String cmd) { | ||||||
| 	DEBUG("Executing command: %s", cmd.c_str()); | 	DEBUG("Executing command: %s\n", cmd.c_str()); | ||||||
|  |  | ||||||
| 	if (cmd.equals("ls")) { | 	if (cmd.startsWith("play ")) { | ||||||
| 		_execute_command_ls("/"); | 		Playlist* p = pm->get_playlist_for_folder(cmd.substring(5)); | ||||||
| 	} else if (cmd.startsWith("ls ")) { | 		player->play(p); | ||||||
| 		_execute_command_ls(cmd.substring(3)); | 	} else if (cmd.equals("play")) { | ||||||
| 	} else if (cmd.startsWith("play ")) { | 		player->play(); | ||||||
| 		_player->play_album(cmd.substring(5)); |  | ||||||
| 	} else if (cmd.startsWith("sys ")) { |  | ||||||
| 		_player->play_system_sound(cmd.substring(4)); |  | ||||||
| 	} else if (cmd.equals("stop")) { | 	} else if (cmd.equals("stop")) { | ||||||
| 		_player->stop(); | 		player->stop(); | ||||||
| 	} else if (cmd.equals("help")) { | 	} else if (cmd.equals("help")) { | ||||||
| 		_execute_command_help(); | 		_execute_command_help(); | ||||||
| 	} else if (cmd.equals("-")) { | 	} else if (cmd.equals("-")) { | ||||||
| 		_player->vol_down(); | 		player->vol_down(); | ||||||
| 	} else if (cmd.equals("+")) { | 	} else if (cmd.equals("+")) { | ||||||
| 		_player->vol_up(); | 		player->vol_up(); | ||||||
| 	} else if (cmd.equals("p")) { | 	} else if (cmd.startsWith("volume=")) { | ||||||
| 		_player->track_prev(); | 		uint8_t vol = cmd.substring(7).toInt(); | ||||||
| 	} else if (cmd.equals("n")) { | 		player->set_volume(vol); | ||||||
| 		_player->track_next(); | 	} else if (cmd.equals("track_prev")) { | ||||||
|  | 		player->track_prev(); | ||||||
|  | 	} else if (cmd.equals("track_next")) { | ||||||
|  | 		player->track_next(); | ||||||
|  | 	} else if (cmd.startsWith("track=")) { | ||||||
|  | 		uint8_t track = cmd.substring(6).toInt(); | ||||||
|  | 		player->set_track(track); | ||||||
|  | 	} else if (cmd.equals("ids")) { | ||||||
|  | 		pm->dump_ids(); | ||||||
|  | 	} else if (cmd.equals("reset_vs1053")) { | ||||||
|  | 		player->stop(); | ||||||
|  | 		player->init(); | ||||||
|  | 	} else if (cmd.equals("reboot")) { | ||||||
|  | 		ESP.restart(); | ||||||
|  | 	} else if (cmd.startsWith("add_mapping=")) { | ||||||
|  | 		String rest = cmd.substring(12); | ||||||
|  | 		uint8_t idx = rest.indexOf('='); | ||||||
|  | 		String id = rest.substring(0, idx); | ||||||
|  | 		String folder = rest.substring(idx + 1); | ||||||
|  | 		pm->add_mapping(id, folder); | ||||||
|  | 		send_playlist_manager_status(); | ||||||
|  | 	#ifdef OTA_UPDATE_URL | ||||||
|  | 	} else if (cmd.equals("update")) { | ||||||
|  | 		Updater::run(); | ||||||
|  | 	#endif | ||||||
| 	} else { | 	} else { | ||||||
| 		ERROR("Unknown command: %s\n", cmd.c_str()); | 		ERROR("Unknown command: %s\n", cmd.c_str()); | ||||||
|  | 		return false; | ||||||
| 	} | 	} | ||||||
| 	return; | 	return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Controller::_execute_command_ls(String path) { | void Controller::_execute_command_ls(String path) { | ||||||
| 	INFO("Listing contents of %s:\n", path.c_str()); | 	INFO("Listing contents of %s:\n", path.c_str()); | ||||||
| 	std::list<String> files = _player->ls(path); | 	// TODO | ||||||
| 	for(std::list<String>::iterator it=files.begin(); it!=files.end(); it++) { | 	//std::list<String> files = player->ls(path); | ||||||
| 		INFO("  %s\n", (*it).c_str()); | 	//for(std::list<String>::iterator it=files.begin(); it!=files.end(); ++it) { | ||||||
| 	} | 	//	INFO("  %s\n", (*it).c_str()); | ||||||
|  | 	//} | ||||||
| } | } | ||||||
|  |  | ||||||
| void Controller::_execute_command_help() { | void Controller::_execute_command_help() { | ||||||
| 	INFO("Valid commands are:"); | 	INFO("Valid commands are:"); | ||||||
| 	INFO("  help      - Displays this help\n"); | 	INFO("  help      - Displays this help\n"); | ||||||
| 	INFO("  ls [dir]  - Lists the contents of [dir] or, if not given, of /\n"); | 	//INFO("  ls [dir]  - Lists the contents of [dir] or, if not given, of /\n"); | ||||||
|  | 	INFO("  ids       - Lists all known ID-to-folder mappings\n"); | ||||||
| 	INFO("  play [id] - Plays the album with the given id\n"); | 	INFO("  play [id] - Plays the album with the given id\n"); | ||||||
| 	INFO("  sys [file]- Plays the file as system sound\n"); |  | ||||||
| 	INFO("  stop      - Stops playback\n"); | 	INFO("  stop      - Stops playback\n"); | ||||||
| 	INFO("  - / +     - Decrease or increase the volume\n"); | 	INFO("  - / +     - Decrease or increase the volume\n"); | ||||||
| 	INFO("  p / n     - Previous or next track\n"); | 	INFO("  p / n     - Previous or next track\n"); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Controller::_check_buttons() { | void Controller::_check_buttons() { | ||||||
| 	SPIMaster::enable(PIN_MCP); | 	if (BTN_PREV() && _debounce_button(0)) { | ||||||
| 	SPI.beginTransaction(SPISettings(250000, MSBFIRST, SPI_MODE0)); | 		if (_state == NORMAL) { | ||||||
| 	/*if (millis()%100==0) { | 			player->track_prev(); | ||||||
| 		Serial.printf("Buttons: %d %d %d %d\n", _mcp->digitalRead(1), _mcp->digitalRead(2), _mcp->digitalRead(3), _mcp->digitalRead(4)); | 		} else { | ||||||
| 	}*/ | 			DEBUG("Ignoring btn_prev because state is LOCKED.\n"); | ||||||
| 	if (_check_button(0)) { | 		} | ||||||
| 		_player->track_prev(); | 	} else if (BTN_VOL_UP() && _debounce_button(1)) { | ||||||
| 	} else if (_check_button(1)) { | 		player->vol_up(); | ||||||
| 		_player->vol_up(); | 	} else if (BTN_VOL_DOWN() && _debounce_button(2)) { | ||||||
| 	} else if (_check_button(2)) { | 		player->vol_down(); | ||||||
| 		_player->vol_down(); | 	} else if (BTN_NEXT() && _debounce_button(3)) { | ||||||
| 	} else if (_check_button(3)) { | 		if (_state == NORMAL) { | ||||||
| 		_player->track_next(); | 			player->track_next(); | ||||||
|  | 		} else { | ||||||
|  | 			DEBUG("Ignoring btn_next because state is LOCKED.\n"); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 	SPI.endTransaction(); |  | ||||||
| 	SPIMaster::disable(); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| bool Controller::_check_button(uint8_t index) { | bool Controller::_debounce_button(uint8_t index) { | ||||||
| 	if (index >= NUM_BUTTONS) return false; |  | ||||||
| 	bool ret = false; | 	bool ret = false; | ||||||
| 	uint8_t sum = 0; |  | ||||||
| 	while (1) { |  | ||||||
| 		sum = 0; |  | ||||||
| 		for (int i=0; i<8; i++) { |  | ||||||
| 			sum += _mcp->digitalRead(index + 1) == HIGH ? 1 : 0; |  | ||||||
| 		} |  | ||||||
| 		if (sum==0 || sum==8) break; |  | ||||||
| 	} |  | ||||||
| 	if (sum == 0) { |  | ||||||
| 	if (_button_last_pressed_at[index] + DEBOUNCE_MILLIS < millis()) { | 	if (_button_last_pressed_at[index] + DEBOUNCE_MILLIS < millis()) { | ||||||
| 		DEBUG("Button %d pressed.\n", index); | 		DEBUG("Button %d pressed.\n", index); | ||||||
| 		ret = true; | 		ret = true; | ||||||
| 	} | 	} | ||||||
| 	_button_last_pressed_at[index] = millis(); | 	_button_last_pressed_at[index] = millis(); | ||||||
| 	} |  | ||||||
| 	return ret; | 	return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | String Controller::json() { | ||||||
|  | 	DynamicJsonDocument json(1024); | ||||||
|  | 	json["_type"] = "controller"; | ||||||
|  | 	switch(_state) { | ||||||
|  | 		case LOCKED: json["state"] = "locked"; break; | ||||||
|  | 		case LOCKING: json["state"] = "locking"; break; | ||||||
|  | 		case NORMAL: json["state"] = "normal"; break; | ||||||
|  | 	} | ||||||
|  | 	json["is_rfid_present"] = _rfid_present; | ||||||
|  | 	JsonObject rfid = json.createNestedObject("last_rfid"); | ||||||
|  | 	rfid["uid"] = _last_rfid_uid; | ||||||
|  | 	rfid["data"] = _last_rfid_data; | ||||||
|  | 	json["uptime"] = millis() / 1000; | ||||||
|  | 	json["free_heap"] = ESP.getFreeHeap(); | ||||||
|  | 	JsonObject versions = json.createNestedObject("versions"); | ||||||
|  | 	versions["ota"] = OTA_VERSION; | ||||||
|  | 	#ifdef VERSION | ||||||
|  | 	versions["release"] = VERSION; | ||||||
|  | 	#else | ||||||
|  | 	versions["release"] = "unknown"; | ||||||
|  | 	#endif | ||||||
|  | 	 | ||||||
|  | 	JsonObject wifi = json.createNestedObject("wifi"); | ||||||
|  | 	if (WiFi.isConnected()) { | ||||||
|  | 		wifi["connected"] = true; | ||||||
|  | 		wifi["ssid"] = WiFi.SSID(); | ||||||
|  | 		wifi["rssi"] = WiFi.RSSI(); | ||||||
|  | 	} else { | ||||||
|  | 		wifi["connected"] = false; | ||||||
|  | 	} | ||||||
|  | 	return json.as<String>(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Controller::send_player_status() { | ||||||
|  | 	TRACE("In send_player_status()...\n"); | ||||||
|  |  | ||||||
|  | 	if (_http_server->ws->count() > 0) { | ||||||
|  | 		_http_server->ws->textAll(player->json()); | ||||||
|  | 		_http_server->ws->textAll(player->position_json()); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Controller::send_playlist_manager_status() { | ||||||
|  | 	TRACE("In send_playlist_manager_status()...\n"); | ||||||
|  | 	if (_http_server->ws->count() > 0) { | ||||||
|  | 		_http_server->ws->textAll(pm->json()); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Controller::send_position() { | ||||||
|  | 	TRACE("In send_position()...\n"); | ||||||
|  | 	if (_http_server->ws->count() > 0) { | ||||||
|  | 		_http_server->ws->textAll(player->position_json()); | ||||||
|  | 	} | ||||||
|  | 	_last_position_info_at = millis(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Controller::send_controller_status() { | ||||||
|  | 	TRACE("In send_controller_status()...\n"); | ||||||
|  | 	if (_http_server->ws->count() > 0) { | ||||||
|  | 		_http_server->ws->textAll(json()); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Controller::inform_new_client(AsyncWebSocketClient* client) { | ||||||
|  | 	String s; | ||||||
|  | 	s += pm->json(); | ||||||
|  | 	s += '\n'; | ||||||
|  | 	s += player->json(); | ||||||
|  | 	s += '\n'; | ||||||
|  | 	s += player->position_json(); | ||||||
|  | 	s += '\n'; | ||||||
|  | 	s += json(); | ||||||
|  | 	client->text(s); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Controller::queue_command(String s) { | ||||||
|  | 	DEBUG("Enqeueing command '%s'.\n", s.c_str()); | ||||||
|  | 	_cmd_queue = s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Controller::update_playlist_manager() { | ||||||
|  | 	pm->scan_files(); | ||||||
|  | 	send_playlist_manager_status(); | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										57
									
								
								src/data_sources.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/data_sources.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | #include "data_sources.h" | ||||||
|  |  | ||||||
|  | ////////////// SDDataSource ////////////// | ||||||
|  | SDDataSource::SDDataSource(String file) { _file = SD.open(file, "r"); } | ||||||
|  | SDDataSource::~SDDataSource() { if (_file) _file.close(); } | ||||||
|  | size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); } | ||||||
|  | int SDDataSource::read() { return _file.read(); } | ||||||
|  | size_t SDDataSource::position() { return _file.position(); } | ||||||
|  | void SDDataSource::seek(size_t position) { _file.seek(position); } | ||||||
|  | size_t SDDataSource::size() { return _file.size(); } | ||||||
|  | void SDDataSource::close() { _file.close(); } | ||||||
|  | bool SDDataSource::usable() { return _file; } | ||||||
|  |  | ||||||
|  | void SDDataSource::skip_id3_tag() { | ||||||
|  | 	uint32_t original_position = _file.position(); | ||||||
|  | 	uint32_t offset = 0; | ||||||
|  | 	if (_file.read()=='I' && _file.read()=='D' && _file.read()=='3') { | ||||||
|  | 		DEBUG("ID3 tag found\n"); | ||||||
|  | 		// Skip ID3 tag version | ||||||
|  | 		_file.read(); _file.read(); | ||||||
|  | 		byte tags = _file.read(); | ||||||
|  | 		bool footer_present = tags & 0x10; | ||||||
|  | 		DEBUG("ID3 footer found: %d\n", footer_present); | ||||||
|  | 		for (byte i=0; i<4; i++) { | ||||||
|  | 			offset <<= 7; | ||||||
|  | 			offset |= (0x7F & _file.read()); | ||||||
|  | 		} | ||||||
|  | 		offset += 10; | ||||||
|  | 		if (footer_present) offset += 10; | ||||||
|  | 		DEBUG("ID3 tag length is %d bytes.\n", offset); | ||||||
|  | 		_file.seek(offset); | ||||||
|  | 	} else { | ||||||
|  | 		DEBUG("No ID3 tag found\n"); | ||||||
|  | 		_file.seek(original_position); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ////////////// HTTPSDataSource ////////////// | ||||||
|  | HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) { | ||||||
|  | 	_http = new HTTPClientWrapper(); | ||||||
|  | 	if (!_http->get(url, offset)) return; | ||||||
|  | 	_position = 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | HTTPSDataSource::~HTTPSDataSource() { | ||||||
|  | 	_http->close(); | ||||||
|  | 	delete _http; | ||||||
|  | } | ||||||
|  | bool HTTPSDataSource::usable() { return _http; } | ||||||
|  | size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; } | ||||||
|  | int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; } | ||||||
|  | size_t HTTPSDataSource::position() { return _position; } | ||||||
|  | void HTTPSDataSource::seek(size_t position) { return; /* TODO */ } | ||||||
|  | size_t HTTPSDataSource::size() { return _http->getSize(); } | ||||||
|  | void HTTPSDataSource::close() { _http->close(); } | ||||||
							
								
								
									
										213
									
								
								src/http_client_wrapper.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								src/http_client_wrapper.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,213 @@ | |||||||
|  | #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"); | ||||||
|  | } | ||||||
							
								
								
									
										167
									
								
								src/http_server.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								src/http_server.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | |||||||
|  | #include "http_server.h" | ||||||
|  | #include "main.h" | ||||||
|  | #include "spi_master.h" | ||||||
|  | #include <ESPmDNS.h> | ||||||
|  |  | ||||||
|  | HTTPServer::HTTPServer(Player* p, Controller* c) { | ||||||
|  | 	_player = p; | ||||||
|  | 	_controller = c; | ||||||
|  | 	_server = new AsyncWebServer(80); | ||||||
|  | 	ws = new AsyncWebSocket("/ws"); | ||||||
|  | 	_server->addHandler(ws); | ||||||
|  | 	ws->onEvent([&](AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){this->_onEvent(server, client, type, arg, data, len);}); | ||||||
|  | 	 | ||||||
|  | 	_server->on("/", HTTP_GET, [&](AsyncWebServerRequest* req) { | ||||||
|  | 		req->send(200, "text/html", (const char*)file_index_html_start); | ||||||
|  | 	}); | ||||||
|  | 	_server->on("/upload", HTTP_POST, [](AsyncWebServerRequest* req) { | ||||||
|  | 		req->send(200);  | ||||||
|  | 	}, [&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { | ||||||
|  | 		this->_handle_upload(request, filename, index, data, len, final); | ||||||
|  | 	}); | ||||||
|  | 	_server->on("/_mapping.txt", HTTP_GET, [&](AsyncWebServerRequest* req) { | ||||||
|  | 		req->send(200, "text/plain", _controller->pm->create_mapping_txt()); | ||||||
|  | 	}); | ||||||
|  | 	_server->on("/player.json", HTTP_GET, [&](AsyncWebServerRequest* req) { | ||||||
|  | 		req->send(200, "application/json", _controller->player->json()); | ||||||
|  | 	}); | ||||||
|  | 	_server->on("/playlist_manager.json", HTTP_GET, [&](AsyncWebServerRequest* req) { | ||||||
|  | 		req->send(200, "application/json", _controller->pm->json()); | ||||||
|  | 	}); | ||||||
|  | 	_server->on("/controller.json", HTTP_GET, [&](AsyncWebServerRequest* req) { | ||||||
|  | 		req->send(200, "application/json", _controller->json()); | ||||||
|  | 	}); | ||||||
|  | 	_server->on("/position.json", HTTP_GET, [&](AsyncWebServerRequest* req) { | ||||||
|  | 		req->send(200, "application/json", _controller->player->position_json()); | ||||||
|  | 	}); | ||||||
|  | 	_server->on("/cmd", HTTP_POST, [&](AsyncWebServerRequest *req) { | ||||||
|  | 		if (req->hasParam("cmd", true)) { | ||||||
|  | 			_controller->queue_command(req->getParam("cmd", true)->value()); | ||||||
|  | 			req->send(200); | ||||||
|  | 		} else { | ||||||
|  | 			req->send(400); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 	_server->begin(); | ||||||
|  | 	MDNS.addService("http", "tcp", 80); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HTTPServer::_handle_upload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) { | ||||||
|  | 	// https://www.gnu.org/software/tar/manual/html_node/Standard.html | ||||||
|  | 	// https://www.mkssoftware.com/docs/man4/tar.4.asp | ||||||
|  | 	 | ||||||
|  | 	if (index == 0) { // Starting upload | ||||||
|  | 		_chunk = new uint8_t[512]; | ||||||
|  | 		_chunk_length = 0; | ||||||
|  | 		_upload_position = 0; | ||||||
|  | 		_file_size = 0; | ||||||
|  | 		_file_size_done = 0; | ||||||
|  | 		_need_header = true; | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	uint32_t upload_offset = 0; | ||||||
|  | 	while (upload_offset < len) { | ||||||
|  | 		// Load a chunk | ||||||
|  | 		if (_chunk_length < 512 && len > upload_offset) { | ||||||
|  | 			uint16_t needed = 512 - _chunk_length; | ||||||
|  | 			if (needed > len - upload_offset) needed = len - upload_offset; | ||||||
|  | 			memcpy(_chunk + _chunk_length, data + upload_offset, needed); | ||||||
|  | 			_chunk_length += needed; | ||||||
|  | 			upload_offset += needed; | ||||||
|  | 			_upload_position += needed; | ||||||
|  | 		 | ||||||
|  | 			if (_chunk_length == 512) { | ||||||
|  | 				// Process chunk | ||||||
|  | 				DEBUG("."); | ||||||
|  | 				if (_need_header) { | ||||||
|  | 					if (_chunk[257]=='u'&&_chunk[258]=='s'&&_chunk[259]=='t'&&_chunk[260]=='a'&&_chunk[261]=='r') { | ||||||
|  | 						DEBUG("It is a valid header, starting at 0x%X!\n", _upload_position-512); | ||||||
|  | 						char filename[200]; | ||||||
|  | 						strncpy(filename, (char*)_chunk, 100); | ||||||
|  | 						DEBUG("filename: %s\n", filename); | ||||||
|  | 						_file_size = 0; | ||||||
|  | 						_file_size_done = 0; | ||||||
|  | 						for (int i=0; i<11; i++) { | ||||||
|  | 							//Serial.print(_header_buffer[124 + i]); | ||||||
|  | 							_file_size = (_file_size<<3) + (_chunk[124 + i] - '0'); | ||||||
|  | 						} | ||||||
|  | 						DEBUG("filesize: %d\n", _file_size); | ||||||
|  | 						uint8_t type = _chunk[156] - '0'; | ||||||
|  | 						if (type==0) { | ||||||
|  | 							String path = "/"; | ||||||
|  | 							path += filename; | ||||||
|  | 							DEBUG("Opening file %s\n", path.c_str()); | ||||||
|  | 							uint8_t state = SPIMaster::state; | ||||||
|  | 							SPIMaster::disable(); | ||||||
|  | 							SPIMaster::select_sd(); | ||||||
|  | 							// Better safe than sorry. ;-) | ||||||
|  | 							_upload_file.close(); | ||||||
|  | 							_upload_file = SD.open(path, "w"); | ||||||
|  | 							SPIMaster::set_state(state); | ||||||
|  | 						} else if (type==5) { | ||||||
|  | 							String dirname = "/"; | ||||||
|  | 							dirname += filename; | ||||||
|  | 							dirname.remove(dirname.length()-1); | ||||||
|  | 							uint8_t state = SPIMaster::state; | ||||||
|  | 							SPIMaster::disable(); | ||||||
|  | 							SPIMaster::select_sd(); | ||||||
|  | 							bool res = SD.mkdir(dirname); | ||||||
|  | 							SPIMaster::set_state(state); | ||||||
|  | 							DEBUG("Creating folder '%s' returned %d.\n", dirname.c_str(), res); | ||||||
|  | 						} else { | ||||||
|  | 							ERROR("Unknown file type %d\n", type); | ||||||
|  | 						} | ||||||
|  | 						_need_header = (type==5 || _file_size==0); // No chunks needed for directories. | ||||||
|  | 					} else { | ||||||
|  | 						bool byte_found = false; | ||||||
|  | 						for (int i=0; i<512; i++) byte_found = byte_found || _chunk[i]>0; | ||||||
|  | 						if (!byte_found) { | ||||||
|  | 							DEBUG("Empty chunk while looking for header -> ignoring.\n"); | ||||||
|  | 						} else { | ||||||
|  | 							ERROR("Invalid tar header: %c %c %c %c %c. Looking at header start offset 0x%X.\n", _chunk[257], _chunk[258], _chunk[259], _chunk[260], _chunk[261], _upload_position-512); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} else { | ||||||
|  | 					uint32_t bytes_to_write = _file_size - _file_size_done; | ||||||
|  | 					if (bytes_to_write > 512) bytes_to_write=512; | ||||||
|  | 					uint8_t state = SPIMaster::state; | ||||||
|  | 					SPIMaster::disable(); | ||||||
|  | 					SPIMaster::select_sd(); | ||||||
|  | 					_upload_file.write(_chunk, bytes_to_write); | ||||||
|  | 					_file_size_done += bytes_to_write; | ||||||
|  | 					if (_file_size_done >= _file_size) { | ||||||
|  | 						_upload_file.close(); | ||||||
|  | 						_need_header = true; | ||||||
|  | 					} | ||||||
|  | 					SPIMaster::set_state(state); | ||||||
|  | 				} | ||||||
|  | 				 | ||||||
|  | 				_chunk_length = 0; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (final == true) { | ||||||
|  | 		uint8_t state = SPIMaster::state; | ||||||
|  | 		SPIMaster::disable(); | ||||||
|  | 		SPIMaster::select_sd(); | ||||||
|  | 		_upload_file.close(); | ||||||
|  | 		SPIMaster::set_state(state); | ||||||
|  | 		delete _chunk; | ||||||
|  | 		_controller->update_playlist_manager(); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void HTTPServer::_onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) { | ||||||
|  | 	if (type==WS_EVT_CONNECT) { | ||||||
|  | 		_controller->inform_new_client(client); | ||||||
|  | 	} else if (type==WS_EVT_DATA) { | ||||||
|  | 		AwsFrameInfo* info = (AwsFrameInfo*) arg; | ||||||
|  | 		if (info->final && info->index==0 && info->len==len && info->opcode==WS_TEXT) { | ||||||
|  | 			data[len]='\0'; | ||||||
|  | 			DEBUG("Received ws message: %s\n", (char*)data); | ||||||
|  | 			_controller->queue_command((char*)data); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										324
									
								
								src/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								src/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,324 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  | 	<meta charset="utf-8"> | ||||||
|  | 	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||||
|  | 	<title>ESMP3</title> | ||||||
|  | 	<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8=" crossorigin="anonymous"></script> | ||||||
|  | 	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> | ||||||
|  | 	<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script> | ||||||
|  | 	<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script> | ||||||
|  | 	<script src="https://kit.fontawesome.com/272149490a.js" crossorigin="anonymous"></script> | ||||||
|  | </head> | ||||||
|  |  | ||||||
|  | <body> | ||||||
|  | 	<div class="container bg-dark text-light"> | ||||||
|  | 		<div class="row"> | ||||||
|  | 			<div class="col-sm-1"> | ||||||
|  | 				<h1 id="play_state_icon"><i class="fa fa-stop"></i></h1> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="col-sm-11"> | ||||||
|  | 				<h2><i class="fa fa-compact-disc"></i> <span id="album"></span></h2> | ||||||
|  | 				<h2><i class="fa fa-scroll"></i> <span id="track"></span></h2> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | 	<div class="container"> | ||||||
|  | 		<div class="row"> | ||||||
|  | 			<div class="col"> | ||||||
|  | 				<input type="range" class="custom-range" id="position_slider" disabled> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="row"> | ||||||
|  | 			<div class="col-sm-6"> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="col-sm-1"> | ||||||
|  | 				<h3><i class="fa fa-volume-down"></i></h3> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="col-sm-4"> | ||||||
|  | 				<input type="range" class="custom-range" id="volume_slider"> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="col-sm-1"> | ||||||
|  | 				<h3><i class="fa fa-volume-up"></i></h3> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 		<div class="row"> | ||||||
|  | 			<div class="col"> | ||||||
|  | 				<button type="button" class="btn btn-primary btn-lg btn-block" id="button_track_prev"><i class="fa fa-step-backward"></i></button> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="col"> | ||||||
|  | 				<button type="button" class="btn btn-primary btn-lg btn-block" id="button_stop"><i class="fa fa-stop"></i></button> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="col"> | ||||||
|  | 				<button type="button" class="btn btn-primary btn-lg btn-block" id="button_play"><i class="fa fa-play"></i></button> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="col"> | ||||||
|  | 				<button type="button" class="btn btn-primary btn-lg btn-block" id="button_track_next"><i class="fa fa-step-forward"></i></button> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="col"> | ||||||
|  | 				<button type="button" class="btn btn-primary btn-lg btn-block" id="button_lock"><i class="fa fa-lock-open"></i></button> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="col"> | ||||||
|  | 				<button type="button" class="btn btn-primary btn-lg btn-block" id="button_open"><i class="fa fa-eject"></i></button> | ||||||
|  | 			</div> | ||||||
|  | 			<div class="col"> | ||||||
|  | 				<button type="button" class="btn btn-primary btn-lg btn-block" id="button_settings"><i class="fa fa-cog"></i></button> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  | 	 | ||||||
|  | 	<div class="container"> | ||||||
|  | 		<table class="table table-hover table-sm" id="track_list_table"> | ||||||
|  | 			<thead class="thead-light"> | ||||||
|  | 				<tr> | ||||||
|  | 					<th>Nr.</th> | ||||||
|  | 					<th>Status</th> | ||||||
|  | 					<th>Track</th> | ||||||
|  | 				</tr> | ||||||
|  | 			</thead> | ||||||
|  | 			<tbody class="" id="track_list"> | ||||||
|  | 			 | ||||||
|  | 			</tbody> | ||||||
|  | 		</table> | ||||||
|  | 	</div> | ||||||
|  | 	 | ||||||
|  | 	 | ||||||
|  | 	 | ||||||
|  | 	<div class="modal fade" id="openModal" tabindex="-1" role="dialog"> | ||||||
|  | 		<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document"> | ||||||
|  | 			<div class="modal-content"> | ||||||
|  | 				<div class="modal-header"> | ||||||
|  | 					<h5 class="modal-title">Album öffnen</h5> | ||||||
|  | 					<button type="button" class="close" data-dismiss="modal"> | ||||||
|  | 						<span>×</span> | ||||||
|  | 					</button> | ||||||
|  | 				</div> | ||||||
|  |  | ||||||
|  | 				<h6>Open URL</h6> | ||||||
|  | 				<div class="input-group"> | ||||||
|  | 					<div class="input-group-prepend"> | ||||||
|  | 						<span class="input-group-text"> | ||||||
|  | 							<i class="fa fa-link"></i> | ||||||
|  | 						</span> | ||||||
|  | 					</div> | ||||||
|  | 					<input type="text" class="form-control" id="input_url" /> | ||||||
|  | 					<div class="input-group-append"> | ||||||
|  | 						<button class="btn btn-primary" id="button_url_open">Go</button> | ||||||
|  | 						<button class="btn btn-danger" id="button_url_add_mapping" style="display: none;"><i class="fa fa-arrows-alt-h"></i></button> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 				 | ||||||
|  | 				<div class="modal-body"> | ||||||
|  | 					<div id="albums_without_id_area"> | ||||||
|  | 						<h6>Albums without RFID card</h6> | ||||||
|  | 						<table class="table table-hover table-sm"> | ||||||
|  | 							<tbody id="albums_without_id"> | ||||||
|  | 							</tbody> | ||||||
|  | 						</table> | ||||||
|  | 					</div> | ||||||
|  | 					 | ||||||
|  | 					<h6>Albums with RFID</h6> | ||||||
|  | 					<table class="table table-hover table-sm"> | ||||||
|  | 						<tbody id="albums_with_id"> | ||||||
|  | 						</tbody> | ||||||
|  | 					</table> | ||||||
|  | 				</div> | ||||||
|  | 				 | ||||||
|  | 				<div class="modal-footer"> | ||||||
|  | 					<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
|  | 	<div class="modal fade" id="settingsModal" tabindex="-1" role="dialog"> | ||||||
|  | 			<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document"> | ||||||
|  | 				<div class="modal-content"> | ||||||
|  | 					<div class="modal-header"> | ||||||
|  | 						<h5 class="modal-title">Settings</h5> | ||||||
|  | 						<button type="button" class="close" data-dismiss="modal"> | ||||||
|  | 							<span>×</span> | ||||||
|  | 						</button> | ||||||
|  | 					</div> | ||||||
|  | 					 | ||||||
|  | 					<div class="modal-body"> | ||||||
|  | 						<h6>Last RFID id:</h6> | ||||||
|  | 						<span id="last_rfid_id"></span> <button class="btn btn-warning" id="button_add_mapping"><i class="fa fa-arrows-alt-h"></i></button> | ||||||
|  | 						 | ||||||
|  | 						<h6>Last RFID data:</h6> | ||||||
|  | 						<span id="last_rfid_data"></span> | ||||||
|  |  | ||||||
|  | 						<h6>Actions</h6> | ||||||
|  | 						<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reset_vs1053">Reset VS1053 chip</button> | ||||||
|  | 						<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reboot">Reboot ESMP3</button> | ||||||
|  | 						<button type="button" class="btn btn-danger btn-lg btn-block" id="button_update">Check for and install update</button> | ||||||
|  | 					</div> | ||||||
|  | 					 | ||||||
|  | 					<div class="modal-footer"> | ||||||
|  | 						<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | </body> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | update_player = function(data) { | ||||||
|  | 	$('#play_state_icon i').removeClass('fa-stop', 'fa-play').addClass(data.playing ? 'fa-play' : 'fa-stop'); | ||||||
|  | 	 | ||||||
|  | 	if (data.playing) { | ||||||
|  | 		$('#button_play').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled'); | ||||||
|  | 		$('#button_stop').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary'); | ||||||
|  | 	} else if (data.playlist) { | ||||||
|  | 		$('#button_play').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary'); | ||||||
|  | 		$('#button_stop').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled'); | ||||||
|  | 	} else { | ||||||
|  | 		$('#button_play').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled'); | ||||||
|  | 		$('#button_stop').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled'); | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	$('#volume_slider').attr('min', data.volume.min).attr('max', data.volume.max).val(data.volume.current); | ||||||
|  | 	 | ||||||
|  | 	if (data.playlist) update_playlist(data.playlist); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | update_playlist = function(data) { | ||||||
|  | 	$('#track_list tr').remove(); | ||||||
|  | 	for (var i=0; i<data.files.length; i++) { | ||||||
|  | 		tr = $('<tr>').data('track', i); | ||||||
|  | 		tr.append($('<td>').html(i + 1)); | ||||||
|  | 		tr.append($('<td>').html(data.current_track==i ? '<i class="fa fa-play"></i>' : '')); | ||||||
|  | 		tr.append($('<td>').html(data.files[i].title)); | ||||||
|  | 		$('#track_list').append(tr); | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	if (data.has_track_next) { | ||||||
|  | 		$('#button_track_next').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary'); | ||||||
|  | 	} else { | ||||||
|  | 		$('#button_track_next').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled'); | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	if (data.has_track_prev) { | ||||||
|  | 		$('#button_track_prev').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary'); | ||||||
|  | 	} else { | ||||||
|  | 		$('#button_track_prev').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled'); | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	$('#album').html(data.title); | ||||||
|  | 	var file = data.files[data.current_track]; | ||||||
|  | 	if (file) { | ||||||
|  | 		$('#track').html(file.title);  | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | update_controller = function(data) { | ||||||
|  | 	if (data.lock_state == "locked") { | ||||||
|  | 		$('#button_lock').removeClass('btn-primary', 'btn-warning').addClass('btn-danger'); | ||||||
|  | 		$('#button_lock i').removeClass('fa-lock-open').addClass('fa-lock'); | ||||||
|  | 	} else if (data.lock_state == "locking") { | ||||||
|  | 		$('#button_lock').removeClass('btn-primary', 'btn-danger').addClass('btn-warning'); | ||||||
|  | 		$('#button_lock i').removeClass('fa-lock-open').addClass('fa-lock'); | ||||||
|  | 	} else { | ||||||
|  | 		$('#button_lock').removeClass('btn-danger', 'btn-warning').addClass('btn-primary'); | ||||||
|  | 		$('#button_lock i').removeClass('fa-lock').addClass('fa-lock-open'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	$('#button_add_mapping').toggle(data.last_rfid.uid.length>0); | ||||||
|  | 	$('#last_rfid_id').html(data.last_rfid.uid); | ||||||
|  | 	$('#last_rfid_data').html(data.last_rfid.data); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | update_playlist_manager = function(data) { | ||||||
|  | 	if (data.unmapped.length > 0) { | ||||||
|  | 		$('#albums_without_id_area').show(); | ||||||
|  | 		$('#albums_without_id tr').remove(); | ||||||
|  | 		data.unmapped = data.unmapped.sort(); | ||||||
|  | 		for (var i=0; i<data.unmapped.length; i++) { | ||||||
|  | 			var tr = $('<tr>').attr('data-folder', data.unmapped[i]); | ||||||
|  | 			tr.append($('<td>').html(data.unmapped[i].substr(1))); | ||||||
|  | 			tr.append($('<td>').append($('<button>').addClass('button btn-warning add_mapping_button').hide().append($('<i>').addClass('fa fa-arrows-alt-h')))); | ||||||
|  | 			$('#albums_without_id').append(tr); | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		$('#albums_without_id_area').hide(); | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	var folders = Object.keys(data.folders).sort(); | ||||||
|  | 	for (var i in folders) { | ||||||
|  | 		var folder = folders[i]; | ||||||
|  | 		var tr = $('<tr>').attr('data-folder', folder); | ||||||
|  | 		tr.append($('<td>').html(folder.substr(1))); | ||||||
|  | 		tr.append($('<td>').append($('<button>').addClass('button btn-danger add_mapping_button').hide().append($('<i>').addClass('fa fa-arrows-alt-h')))); | ||||||
|  | 		$('#albums_with_id').append(tr); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | update_position = function(data) { | ||||||
|  | 	$('#position_slider').attr('max', data.file_size).val(data.position); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | process_ws_message = function(event) { | ||||||
|  | 	var data = event.data.split("\n");; | ||||||
|  | 	for (var i=0; i<data.length; i++) { | ||||||
|  | 		var json = JSON.parse(data[i]); | ||||||
|  | 		console.log(json); | ||||||
|  | 		if (json === null) continue; | ||||||
|  | 		switch(json["_type"]) { | ||||||
|  | 			case "position": update_position(json); break; | ||||||
|  | 			case "player": update_player(json); break; | ||||||
|  | 			case "playlist_manager": update_playlist_manager(json); break; | ||||||
|  | 			case "controller": update_controller(json); break; | ||||||
|  | 		}	 | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var play_on_click = true; | ||||||
|  |  | ||||||
|  | $(function() { | ||||||
|  | 	ws = new WebSocket("ws://" + location.host + "/ws"); | ||||||
|  | 	ws.onmessage = process_ws_message; | ||||||
|  | 	 | ||||||
|  | 	$('#volume_slider').change(function(e) { ws.send("volume=" + e.target.value); }); | ||||||
|  | 	$('#button_play').click(function(e) { ws.send("play"); }); | ||||||
|  | 	$('#button_stop').click(function(e) { ws.send("stop"); }); | ||||||
|  | 	$('#button_track_next').click(function(e) { ws.send("track_next"); }); | ||||||
|  | 	$('#button_track_prev').click(function(e) { ws.send("track_prev"); }); | ||||||
|  | 	$('#button_open').click(function(e) { $('#openModal').modal('show'); }); | ||||||
|  | 	$('#track_list').on('click', 'tr', function(e) { ws.send("track=" + $(e.target).parent().data('track')); }); | ||||||
|  | 	$('#albums_without_id, #albums_with_id').on('click', 'tr', function(e) { if (play_on_click) {ws.send("play " + $(e.target).parents('tr').data('folder')); $('#openModal').modal('hide');} }); | ||||||
|  | 	$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); }); | ||||||
|  | 	$('#button_reset_vs1053').click(function(e) { ws.send("reset_vs1053"); $('#settingsModal').modal('hide'); }); | ||||||
|  | 	$('#button_reboot').click(function(e) { ws.send("reboot"); $('#settingsModal').modal('hide'); }); | ||||||
|  | 	$('#button_update').click(function(e) { ws.send("update"); $('#settingsModal').modal('hide'); }); | ||||||
|  | 	$('#button_url_open').click(function(e) { ws.send("play " + $('#input_url').val()); $('#openModal').modal('hide');}); | ||||||
|  | 	$('#button_add_mapping').click(function(e) { | ||||||
|  | 		$('#settingsModal').modal('hide'); | ||||||
|  | 		$('#openModal').modal('show'); | ||||||
|  | 		$('.add_mapping_button').show(); | ||||||
|  | 		$('#button_url_open').hide(); | ||||||
|  | 		$('#button_url_add_mapping').show(); | ||||||
|  | 		play_on_click = false; | ||||||
|  | 	}); | ||||||
|  | 	$('#openModal').on('click', '.add_mapping_button', function(e) { | ||||||
|  | 		ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $(e.target).parents('tr').data('folder')); | ||||||
|  | 		$('#openModal').modal('hide'); | ||||||
|  | 		$('.add_mapping_button').hide(); | ||||||
|  | 		$('#button_url_open').hide(); | ||||||
|  | 		$('#button_url_add_mapping').show(); | ||||||
|  | 		e.stopPropagation(); | ||||||
|  | 		play_on_click=true; | ||||||
|  | 		return false; | ||||||
|  | 	}); | ||||||
|  | 	$('#button_url_add_mapping').click(function(e) { | ||||||
|  | 		ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $('#input_url').val()); | ||||||
|  | 		$('#openModal').modal('hide'); | ||||||
|  | 		$('.add_mapping_button').hide(); | ||||||
|  | 		$('#button_url_open').hide(); | ||||||
|  | 		$('#button_url_add_mapping').show(); | ||||||
|  | 		e.stopPropagation(); | ||||||
|  | 		play_on_click=true; | ||||||
|  | 		return false; | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | </html> | ||||||
							
								
								
									
										112
									
								
								src/main.cpp
									
									
									
									
									
								
							
							
						
						
									
										112
									
								
								src/main.cpp
									
									
									
									
									
								
							| @@ -1,42 +1,124 @@ | |||||||
| #include <Arduino.h> | #include <Arduino.h> | ||||||
| #include <SPI.h> | #include <SPI.h> | ||||||
| #include <SD.h> | #include <SD.h> | ||||||
| #include <MCP23S17/MCP23S17.h> | #include <WiFi.h> | ||||||
|  | #include <WiFiMulti.h> | ||||||
|  | #include <ESPmDNS.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 "playlist_manager.h" | ||||||
|  | #include "updater.h" | ||||||
|  |  | ||||||
| Controller* controller; | Controller* controller; | ||||||
| Player* player; | Player* player; | ||||||
| MCP* mcp; | PlaylistManager* pm; | ||||||
|  | HTTPServer* http_server; | ||||||
|  |  | ||||||
|  | uint8_t SPIMaster::state = 0; | ||||||
|  |  | ||||||
|  | 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("Started."); | ||||||
| 	INFO("Starting.\n"); | 	INFO("Starting.\n"); | ||||||
|  | 	#ifdef VERSION | ||||||
|  | 		INFO("ESMP3 version %s (OTA_VERSION %d)\n", VERSION, OTA_VERSION); | ||||||
|  | 	#else | ||||||
|  | 		INFO("ESMP3, version unknown (OTA_VERSION %d)\n", OTA_VERSION); | ||||||
|  | 	#endif | ||||||
| 	INFO("Initializing...\n"); | 	INFO("Initializing...\n"); | ||||||
|  |  | ||||||
| 	DEBUG("Setting up SPI...\n"); | 	DEBUG("Setting up SPI...\n"); | ||||||
| 	SPI.begin(); | 	SPI.begin(); | ||||||
|  | 	SPI.setHwCs(false); | ||||||
| 	SPIMaster::init(); | 	SPIMaster::init(); | ||||||
|  | 	SPIMaster* spi = new SPIMaster(); | ||||||
| 	INFO("SPI initialized.\n"); | 	INFO("SPI initialized.\n"); | ||||||
|  |  | ||||||
| 	DEBUG("Setting up MCP...\n"); |  | ||||||
| 	SPIMaster::enable(PIN_MCP); |  | ||||||
| 	mcp = new MCP(0, PIN_MCP); |  | ||||||
| 	INFO("MCP initialized."); |  | ||||||
|  |  | ||||||
| 	DEBUG("Setting up SD card...\n"); | 	DEBUG("Setting up SD card...\n"); | ||||||
| 	SPIMaster::enable(PIN_SD_CS); | 	spi->select_sd(); | ||||||
| 	if (SD.begin(PIN_SD_CS)) { | 	if (SD.begin(42, SPI, 25000000)) { | ||||||
| 		INFO("SD card initialized.\n"); | 		INFO("SD card initialized.\n"); | ||||||
| 	} else { | 	} else { | ||||||
| 		ERROR("Could not initialize SD card. Halting.\n"); | 		ERROR("Could not initialize SD card.\n"); | ||||||
| 		while(1); |  | ||||||
| 	} | 	} | ||||||
| 	player = new Player(mcp); | 	spi->select_sd(false); | ||||||
|     controller = new Controller(player, mcp); |  | ||||||
|  | 	DEBUG("Initializing PlaylistManager...\n"); | ||||||
|  | 	pm = new PlaylistManager(); | ||||||
|  | 	 | ||||||
|  | 	DEBUG("Initializing Player and Controller...\n"); | ||||||
|  | 	player = new Player(spi); | ||||||
|  |     controller = new Controller(player, pm); | ||||||
|  |     INFO("Player and controller initialized.\n"); | ||||||
|  |  | ||||||
|  |     wifi_connect(); | ||||||
|  | 	 | ||||||
|  | 	MDNS.begin("esmp3"); | ||||||
|  |  | ||||||
|  |     DEBUG("Setting up HTTP server...\n"); | ||||||
|  | 	http_server = new HTTPServer(player, controller); | ||||||
|  | 	controller->register_http_server(http_server); | ||||||
|  | 	 | ||||||
|  | 	DEBUG("Starting NTP client...\n"); | ||||||
|  | 	// Taken from https://github.com/esp8266/Arduino/blob/master/cores/esp8266/TZ.h | ||||||
|  | 	configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "europe.pool.ntp.org"); | ||||||
|  | 	struct tm time; | ||||||
|  | 	if (getLocalTime(&time, 10000)) { | ||||||
|  | 		char buffer[100]; | ||||||
|  | 		strftime(buffer, 100, "%Y-%m-%d %H:%M:%S", &time); | ||||||
|  | 		DEBUG("Got time: %s\n", buffer); | ||||||
|  | 	} else { | ||||||
|  | 		INFO("Could not fetch current time via NTP.\n"); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	#ifdef VERSION | ||||||
|  | 		INFO("ESMP3 version %s (OTA_VERSION %d)\n", VERSION, OTA_VERSION); | ||||||
|  | 	#else | ||||||
|  | 		INFO("ESMP3, version unknown (OTA_VERSION %d)\n", OTA_VERSION); | ||||||
|  | 	#endif | ||||||
| 	 | 	 | ||||||
|     INFO("Initialization completed.\n"); |     INFO("Initialization completed.\n"); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										708
									
								
								src/player.cpp
									
									
									
									
									
								
							
							
						
						
									
										708
									
								
								src/player.cpp
									
									
									
									
									
								
							| @@ -2,43 +2,53 @@ | |||||||
|  |  | ||||||
| #include "player.h" | #include "player.h" | ||||||
| #include "spi_master.h" | #include "spi_master.h" | ||||||
|  | #include <ArduinoJson.h> | ||||||
|  |  | ||||||
| //Player::_spi_settings | //Player::_spi_settings | ||||||
|  |  | ||||||
| Player::Player(MCP* m) { | Player::Player(SPIMaster* s) { | ||||||
| 	_mcp = m; | 	_spi = s; | ||||||
| 	_mcp->pinMode(XRESET, OUTPUT); | 	PIN_VS1053_XRESET_SETUP(); | ||||||
| 	_mcp->digitalWrite(XRESET, HIGH); | 	PIN_VS1053_XRESET(HIGH); | ||||||
| 	pinMode(DREQ, INPUT); | 	PIN_SPEAKER_L_SETUP(); | ||||||
|  | 	PIN_SPEAKER_R_SETUP(); | ||||||
|  | 	_speaker_off(); | ||||||
|  | 	_spi->disable(); | ||||||
|  | 	PIN_VS1053_DREQ_SETUP(); | ||||||
| 	 | 	 | ||||||
| 	_init(); | 	init(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Player::register_controller(Controller* c) { | ||||||
|  | 	_controller = c; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_reset() { | void Player::_reset() { | ||||||
| 	_mcp->digitalWrite(XRESET, LOW); | 	PIN_VS1053_XRESET(LOW); | ||||||
| 	delay(100); | 	delay(100); | ||||||
| 	_mcp->digitalWrite(XRESET, HIGH); | 	PIN_VS1053_XRESET(HIGH); | ||||||
| 	delay(100); | 	delay(100); | ||||||
| 	_state = uninitialized; | 	_state = uninitialized; | ||||||
| 	_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow | 	_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_init() { | void Player::init() { | ||||||
| 	SPIMaster::disable(); |  | ||||||
| 	DEBUG("Resetting VS1053...\n"); | 	DEBUG("Resetting VS1053...\n"); | ||||||
| 	_reset(); | 	_reset(); | ||||||
|  |  | ||||||
| 	uint16_t result = _read_control_register(SCI_MODE); | 	uint16_t result = _read_control_register(SCI_MODE); | ||||||
| 	DEBUG("SCI_MODE: 0x%04X\n", result); | 	DEBUG("SCI_MODE: 0x%04X\n", result); | ||||||
| 	if (result != 0x4800) { | 	if (result != 0x4800) { | ||||||
| 		ERROR("SCI_MODE was 0x%04X, expected was 0x4800.\n", result); | 		ERROR("SCI_MODE was 0x%04X, expected was 0x4800. Rebooting.\n", result); | ||||||
| 		return; | 		delay(500); | ||||||
|  | 		ESP.restart(); | ||||||
| 	} | 	} | ||||||
| 	result = _read_control_register(SCI_STATUS); | 	result = _read_control_register(SCI_STATUS); | ||||||
| 	DEBUG("SCI_STATUS: 0x%04X\n", result); | 	DEBUG("SCI_STATUS: 0x%04X\n", result); | ||||||
| 	if (result != 0x0040 && result != 0x0048) { | 	if (result != 0x0040 && result != 0x0048) { | ||||||
| 		ERROR("SCI_STATUS was 0x%04X, expected was 0x0040 or 0x0048.\n", result); | 		ERROR("SCI_STATUS was 0x%04X, expected was 0x0040 or 0x0048. Rebooting.\n", result); | ||||||
| 		return; | 		delay(500); | ||||||
|  | 		ESP.restart(); | ||||||
| 	} | 	} | ||||||
| 	result = _read_control_register(SCI_CLOCKF); | 	result = _read_control_register(SCI_CLOCKF); | ||||||
| 	DEBUG("SCI_CLOCKF: 0x%04X\n", result); | 	DEBUG("SCI_CLOCKF: 0x%04X\n", result); | ||||||
| @@ -46,48 +56,102 @@ void Player::_init() { | |||||||
| 	DEBUG("VS1053 Init looking good.\n"); | 	DEBUG("VS1053 Init looking good.\n"); | ||||||
| 	DEBUG("Upping VS1053 multiplier...\n"); | 	DEBUG("Upping VS1053 multiplier...\n"); | ||||||
|  |  | ||||||
| 	_write_control_register(SCI_CLOCKF, 0x6000); | 	_write_control_register(SCI_CLOCKF, 0xC000); | ||||||
| 	delay(10); | 	delay(10); | ||||||
|  |  | ||||||
| 	_spi_settings = &_spi_settings_fast; | 	_spi_settings = &_spi_settings_fast; | ||||||
|  |  | ||||||
| 	result = _read_control_register(SCI_CLOCKF); | 	result = _read_control_register(SCI_CLOCKF); | ||||||
| 	DEBUG("SCI_CLOCKF: 0x%04X\n", result); | 	DEBUG("SCI_CLOCKF: 0x%04X\n", result); | ||||||
| 	if (result != 0x6000) { | 	if (result != 0xC000) { | ||||||
| 		ERROR("Error: SCI_CLOCKF was 0x%04X, expected was 0x6000.\n", result); | 		ERROR("Error: SCI_CLOCKF was 0x%04X, expected was 0xC000. Rebooting.\n", result); | ||||||
| 		return; | 		delay(500); | ||||||
|  | 		ESP.restart(); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	set_volume(VOLUME_DEFAULT); | 	set_volume(VOLUME_DEFAULT); | ||||||
|  |  | ||||||
| 	INFO("VS1053 initialization completed.\n"); | 	INFO("VS1053 initialization completed.\n"); | ||||||
|  |  | ||||||
| 	INFO("Checking system sounds...\n"); |  | ||||||
| 	SPIMaster::enable(PIN_SD_CS); |  | ||||||
| 	_check_system_sound("no_prev_song.mp3"); |  | ||||||
| 	_check_system_sound("no_next_song.mp3"); |  | ||||||
| 	_check_system_sound("volume_max.mp3"); |  | ||||||
| 	_check_system_sound("volume_min.mp3"); |  | ||||||
|  |  | ||||||
| 	_state = idle; | 	_state = idle; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_check_system_sound(String filename) { | void Player::_speaker_off() { | ||||||
| 	String path = String("/system/") + filename; | 	DEBUG("Speaker off\n"); | ||||||
| 	if (!SD.exists(path)) { | 	PIN_SPEAKER_L(LOW); | ||||||
| 		ERROR("System sound %s is missing on the sd card!\n", path.c_str()); | 	PIN_SPEAKER_R(LOW); | ||||||
| 	} else { |  | ||||||
| 		DEBUG("%s found.\n", path.c_str()); |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void Player::_speaker_on() { | ||||||
|  | 	DEBUG("Speaker on\n"); | ||||||
|  | 	PIN_SPEAKER_L(HIGH); | ||||||
|  | 	PIN_SPEAKER_R(HIGH); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Player::_sleep() { | ||||||
|  | 	DEBUG("VS1053 going to sleep.\n"); | ||||||
|  | 	_speaker_off(); | ||||||
|  | 	_write_control_register(SCI_CLOCKF, 0x0000); | ||||||
|  | 	_spi_settings = &_spi_settings_slow; | ||||||
|  | 	_write_control_register(SCI_AUDATA, 0x0010); | ||||||
|  | 	set_volume(0, false); | ||||||
|  | 	_state = sleeping; | ||||||
|  | 	TRACE("VS1053 is sleeping now.\n"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Player::_wakeup() { | ||||||
|  | 	if (_state != sleeping && _state != recording) return; | ||||||
|  | 	_stopped_at = millis(); | ||||||
|  | 	DEBUG("Waking VS1053...\n"); | ||||||
|  | 	set_volume(_volume, false); | ||||||
|  | 	_write_control_register(SCI_AUDATA, 0x0000); | ||||||
|  | 	_write_control_register(SCI_CLOCKF, 0x6000); | ||||||
|  | 	_write_control_register(SCI_MODE, 0x4800 | SM_RESET); | ||||||
|  | 	delay(10); | ||||||
|  | 	//_speaker_on(); | ||||||
|  | 	_spi_settings = &_spi_settings_fast; | ||||||
|  | 	_state = idle; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Player::_record() { | ||||||
|  | 	// http://www.vlsi.fi/fileadmin/software/VS10XX/VS1053_VS1063_PcmRecorder.pdf | ||||||
|  | 	DEBUG("Starting recording.\n"); | ||||||
|  | 	set_volume(1, false); | ||||||
|  | 	 | ||||||
|  | 	// Disable SCI_BASS | ||||||
|  | 	_write_control_register(SCI_BASS, 0); | ||||||
|  | 	 | ||||||
|  | 	// Disable user applications | ||||||
|  | 	_write_control_register(SCI_AIADDR, 0); | ||||||
|  | 	 | ||||||
|  | 	// Disable interrupts | ||||||
|  | 	_write_control_register(SCI_WRAMADDR, 0xC01A); | ||||||
|  | 	_write_control_register(SCI_WRAM, 0x0002); | ||||||
|  | 	 | ||||||
|  | 	_patch_adpcm(); | ||||||
|  | 	 | ||||||
|  | 	_write_control_register(SCI_MODE, SM_ADPCM); | ||||||
|  | 	 | ||||||
|  | 	_write_control_register(SCI_AICTRL0, 0x8000); // Mono VU meter | ||||||
|  | 	_write_control_register(SCI_AICTRL1, 1024); // Manual gain, 1x | ||||||
|  | 	_write_control_register(SCI_AICTRL2, 0); // Maximum gain for autogain - ignored | ||||||
|  | 	_write_control_register(SCI_AICTRL3, 0); // status: record | ||||||
|  | 	 | ||||||
|  | 	_write_control_register(SCI_AIADDR, 0x0034, false); | ||||||
|  | 	delay(1); | ||||||
|  | 	 | ||||||
|  | 	DEBUG("Recording.\n"); | ||||||
|  | 	delay(10); | ||||||
|  | 	_state = recording; | ||||||
| } | } | ||||||
|  |  | ||||||
| inline void Player::_wait() { | inline void Player::_wait() { | ||||||
| 	while(!digitalRead(DREQ)); | 	while(!PIN_VS1053_DREQ()); | ||||||
| } | } | ||||||
|  |  | ||||||
| uint16_t Player::_read_control_register(uint8_t address) { | uint16_t Player::_read_control_register(uint8_t address, bool do_wait) { | ||||||
| 	_wait(); | 	if (do_wait) _wait(); | ||||||
| 	SPIMaster::enable(XCS); | 	_spi->select_vs1053_xcs(); | ||||||
| 	SPI.beginTransaction(*_spi_settings); | 	SPI.beginTransaction(*_spi_settings); | ||||||
| 	SPI.transfer(CMD_READ); | 	SPI.transfer(CMD_READ); | ||||||
| 	SPI.transfer(address); | 	SPI.transfer(address); | ||||||
| @@ -96,34 +160,268 @@ uint16_t Player::_read_control_register(uint8_t address) { | |||||||
| 	uint8_t b2 = SPI.transfer(0xFF); | 	uint8_t b2 = SPI.transfer(0xFF); | ||||||
| 	_wait(); | 	_wait(); | ||||||
| 	SPI.endTransaction(); | 	SPI.endTransaction(); | ||||||
| 	SPIMaster::disable(); | 	_spi->select_vs1053_xcs(false); | ||||||
|  |  | ||||||
| 	return (b1 << 8) | b2; | 	return (b1 << 8) | b2; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_write_control_register(uint8_t address, uint16_t value) { | void Player::_write_control_register(uint8_t address, uint16_t value, bool do_wait) { | ||||||
| 	uint8_t b1 = value >> 8; |  | ||||||
| 	uint8_t b2 = value & 0xFF; |  | ||||||
| 	_wait(); | 	_wait(); | ||||||
| 	SPIMaster::enable(XCS); | 	_spi->select_vs1053_xcs(); | ||||||
| 	SPI.beginTransaction(*_spi_settings); | 	SPI.beginTransaction(*_spi_settings); | ||||||
| 	SPI.transfer(CMD_WRITE); | 	SPI.transfer(CMD_WRITE); | ||||||
| 	SPI.transfer(address); | 	SPI.transfer(address); | ||||||
| 	SPI.transfer(b1); | 	SPI.transfer(value >> 8); | ||||||
| 	SPI.transfer(b2); | 	SPI.transfer(value & 0xFF); | ||||||
| 	_wait(); |  | ||||||
| 	SPI.endTransaction(); | 	SPI.endTransaction(); | ||||||
| 	SPIMaster::disable(); | 	_spi->select_vs1053_xcs(false); | ||||||
|  | 	if (do_wait) _wait(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Player::_patch_adpcm() { | ||||||
|  | 	static const uint16_t patch_data[] = { | ||||||
|  | 		0x0007, 0x0001, 0xc01a, 0x0006, 0x0001, 0x0002, 0x0007, 0x0001, /*    0 */ | ||||||
|  | 		0x0008, 0x0006, 0x8002, 0x0000, 0x0007, 0x0001, 0x000c, 0x0006, /*    8 */ | ||||||
|  | 		0x0002, 0x7000, 0x0017, 0x0007, 0x0001, 0x8034, 0x0006, 0x0022, /*   10 */ | ||||||
|  | 		0x0030, 0x0490, 0xb080, 0x0024, 0x3800, 0x0024, 0x0000, 0x1090, /*   18 */ | ||||||
|  | 		0xf400, 0x5404, 0x0000, 0x0851, 0xf400, 0x5648, 0xf400, 0x5404, /*   20 */ | ||||||
|  | 		0xf400, 0x5658, 0xf400, 0x5404, 0xf400, 0x5640, 0x0000, 0x800a, /*   28 */ | ||||||
|  | 		0x2900, 0x9180, 0x0006, 0x2016, 0x2a00, 0x1bce, 0x2a00, 0x114e, /*   30 */ | ||||||
|  | 		0x2a00, 0x168e, 0x0007, 0x0001, 0x1800, 0x0006, 0x8006, 0x0000, /*   38 */ | ||||||
|  | 		0x0007, 0x0001, 0x8045, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, /*   40 */ | ||||||
|  | 		0x7808, 0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, /*   48 */ | ||||||
|  | 		0x7800, 0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x3e80, 0x3e10, /*   50 */ | ||||||
|  | 		0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, /*   58 */ | ||||||
|  | 		0x4024, 0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, /*   60 */ | ||||||
|  | 		0x8024, 0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, /*   68 */ | ||||||
|  | 		0x0001, 0x805a, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, 0x7808, /*   70 */ | ||||||
|  | 		0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, 0x7800, /*   78 */ | ||||||
|  | 		0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x5e40, 0x3e10, 0x7800, /*   80 */ | ||||||
|  | 		0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, 0x4024, /*   88 */ | ||||||
|  | 		0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, /*   90 */ | ||||||
|  | 		0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, /*   98 */ | ||||||
|  | 		0x806f, 0x0006, 0x0030, 0x3e12, 0xb817, 0x3e12, 0x7808, 0x3e18, /*   a0 */ | ||||||
|  | 		0x3821, 0x3e18, 0xb823, 0x3e10, 0x7800, 0xb880, 0x3855, 0x0030, /*   a8 */ | ||||||
|  | 		0x0497, 0x48b2, 0x3c00, 0x0000, 0x800a, 0x2900, 0x7300, 0x3e10, /*   b0 */ | ||||||
|  | 		0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x6890, 0x1bd5, 0x0030, /*   b8 */ | ||||||
|  | 		0x0497, 0x3f00, 0x0024, 0x36f0, 0x5800, 0x36f8, 0x9823, 0x36f8, /*   c0 */ | ||||||
|  | 		0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, 0x0030, 0x0717, 0x2100, /*   c8 */ | ||||||
|  | 		0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, 0x8010, 0x0006, 0x000e, /*   d0 */ | ||||||
|  | 		0x3e02, 0x8024, 0x0001, 0x000a, 0x6012, 0x0024, 0xfea2, 0x0024, /*   d8 */ | ||||||
|  | 		0x48b2, 0x1bca, 0x2000, 0x0000, 0x4180, 0x0024, 0x0007, 0x0001, /*   e0 */ | ||||||
|  | 		0x8087, 0x0006, 0x00e6, 0x3e00, 0x7843, 0x3e01, 0x3845, 0x3e04, /*   e8 */ | ||||||
|  | 		0x3812, 0x0006, 0x08d0, 0x3000, 0x4024, 0x6182, 0x0024, 0x0030, /*   f0 */ | ||||||
|  | 		0x06d0, 0x2800, 0x2655, 0xb882, 0x0024, 0x0000, 0x0201, 0x0000, /*   f8 */ | ||||||
|  | 		0x0005, 0x0030, 0x0210, 0xa016, 0x4004, 0x1fff, 0xfe01, 0xae1a, /*  100 */ | ||||||
|  | 		0x0024, 0xc342, 0x0024, 0xb882, 0x2001, 0x0030, 0x06d0, 0x3800, /*  108 */ | ||||||
|  | 		0x4024, 0x0006, 0x0890, 0x3004, 0x0024, 0x3000, 0x4024, 0x0006, /*  110 */ | ||||||
|  | 		0x12d0, 0x6182, 0x0024, 0x3000, 0x4024, 0x2800, 0x3e05, 0xf400, /*  118 */ | ||||||
|  | 		0x4050, 0x3009, 0x2000, 0x0006, 0x08d0, 0x0006, 0x0892, 0x3000, /*  120 */ | ||||||
|  | 		0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x0030, 0x0250, 0xb882, /*  128 */ | ||||||
|  | 		0x2001, 0x0030, 0x0710, 0x3800, 0x4024, 0x0006, 0x12d0, 0x3000, /*  130 */ | ||||||
|  | 		0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x3204, 0x0024, 0x3023, /*  138 */ | ||||||
|  | 		0x0024, 0x30e0, 0xc024, 0x6312, 0x0024, 0x0000, 0x00c3, 0x2800, /*  140 */ | ||||||
|  | 		0x3141, 0x0000, 0x0024, 0x3033, 0x0024, 0x3a04, 0x0024, 0x3000, /*  148 */ | ||||||
|  | 		0x4024, 0x6182, 0x0024, 0x0006, 0x0890, 0x2800, 0x2fd8, 0x0006, /*  150 */ | ||||||
|  | 		0x0301, 0x3a00, 0x4024, 0x0000, 0x00c3, 0x3004, 0x0024, 0x3013, /*  158 */ | ||||||
|  | 		0x0024, 0x3000, 0x4024, 0x0006, 0x12d0, 0x3800, 0x4024, 0x0030, /*  160 */ | ||||||
|  | 		0x0310, 0xf000, 0x0001, 0x6236, 0x0024, 0x001f, 0xffc3, 0x2800, /*  168 */ | ||||||
|  | 		0x3395, 0x0000, 0x0024, 0x0000, 0x0203, 0xa132, 0x0024, 0x001f, /*  170 */ | ||||||
|  | 		0xffc3, 0xb136, 0x0024, 0x6306, 0x0024, 0x0000, 0x0024, 0x2800, /*  178 */ | ||||||
|  | 		0x3611, 0x0000, 0x0024, 0x0020, 0x0003, 0xb132, 0x0024, 0x0000, /*  180 */ | ||||||
|  | 		0x0024, 0x2800, 0x3a85, 0x0000, 0x0024, 0x0000, 0x0081, 0xb212, /*  188 */ | ||||||
|  | 		0x0024, 0x0000, 0x0024, 0x2800, 0x3a05, 0x0000, 0x0024, 0x6892, /*  190 */ | ||||||
|  | 		0x0024, 0xb212, 0x0024, 0x0000, 0x0005, 0x2800, 0x3c55, 0x0030, /*  198 */ | ||||||
|  | 		0x0310, 0x0000, 0x3fc1, 0x3000, 0x8024, 0xb214, 0x0024, 0x003f, /*  1a0 */ | ||||||
|  | 		0xc001, 0xb010, 0x0024, 0xc200, 0x0024, 0x0030, 0x0310, 0x3800, /*  1a8 */ | ||||||
|  | 		0x0024, 0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x5803, 0x2000, /*  1b0 */ | ||||||
|  | 		0x0000, 0x0000, 0x0024, 0x0030, 0x0310, 0x0000, 0x0005, 0x003f, /*  1b8 */ | ||||||
|  | 		0xc001, 0x4088, 0x0002, 0xb214, 0x0024, 0x1fff, 0xfe01, 0xae12, /*  1c0 */ | ||||||
|  | 		0x0024, 0x2800, 0x3a00, 0xc200, 0x0024, 0x2800, 0x28c0, 0x3800, /*  1c8 */ | ||||||
|  | 		0x0024, 0x0007, 0x0001, 0x80fa, 0x0006, 0x00fe, 0x3e12, 0x0024, /*  1d0 */ | ||||||
|  | 		0x3e05, 0xb814, 0x3615, 0x0024, 0x3e00, 0x3841, 0x3e00, 0xb843, /*  1d8 */ | ||||||
|  | 		0x3e01, 0x3845, 0x3e04, 0x3851, 0x0030, 0x10d0, 0x3e04, 0x8024, /*  1e0 */ | ||||||
|  | 		0x3010, 0x0024, 0x3000, 0x8024, 0x0006, 0x1190, 0x3000, 0x4024, /*  1e8 */ | ||||||
|  | 		0x6182, 0x0024, 0x0000, 0x0024, 0x2800, 0x5dd5, 0x0000, 0x0024, /*  1f0 */ | ||||||
|  | 		0x0030, 0x03d0, 0x0000, 0x00c1, 0x3000, 0xc024, 0xb318, 0x0024, /*  1f8 */ | ||||||
|  | 		0x6896, 0x0024, 0x6436, 0x0024, 0x0020, 0x0003, 0x2800, 0x59c5, /*  200 */ | ||||||
|  | 		0x0000, 0x0024, 0x0006, 0x1150, 0x3000, 0x4024, 0x6136, 0x0024, /*  208 */ | ||||||
|  | 		0x0000, 0x0024, 0x2800, 0x4741, 0x0000, 0x0024, 0x0000, 0x0803, /*  210 */ | ||||||
|  | 		0x4132, 0x0024, 0x3800, 0x4024, 0x0006, 0x0190, 0x0006, 0xf011, /*  218 */ | ||||||
|  | 		0x2900, 0xb500, 0x3613, 0x0024, 0x0006, 0xf011, 0x0006, 0x1152, /*  220 */ | ||||||
|  | 		0x0006, 0x0250, 0x4082, 0x0800, 0xfe82, 0x184c, 0x1fff, 0xfc41, /*  228 */ | ||||||
|  | 		0x48ba, 0x0024, 0xae1a, 0x0024, 0x2900, 0xb500, 0x4280, 0x4103, /*  230 */ | ||||||
|  | 		0x0006, 0x1110, 0x4084, 0x0800, 0xfe84, 0x0002, 0x48ba, 0x0024, /*  238 */ | ||||||
|  | 		0xae12, 0x0024, 0xf400, 0x4001, 0x0000, 0x0180, 0x6200, 0x0024, /*  240 */ | ||||||
|  | 		0x0000, 0x0080, 0x2800, 0x5241, 0x4200, 0x0024, 0x3800, 0x0024, /*  248 */ | ||||||
|  | 		0x0006, 0x1090, 0x3004, 0x8024, 0xf400, 0x4491, 0x3113, 0x0024, /*  250 */ | ||||||
|  | 		0x3804, 0x4024, 0x3a00, 0xc024, 0x3004, 0x8024, 0xf400, 0x4491, /*  258 */ | ||||||
|  | 		0x3113, 0x0024, 0x3804, 0x4024, 0x3a00, 0x4024, 0x0006, 0x1081, /*  260 */ | ||||||
|  | 		0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x0f00, 0x2800, 0x5248, /*  268 */ | ||||||
|  | 		0x0000, 0x0024, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0080, /*  270 */ | ||||||
|  | 		0x3000, 0x4024, 0x0030, 0x0710, 0xb104, 0x0024, 0x0000, 0x0001, /*  278 */ | ||||||
|  | 		0x3800, 0x4024, 0x0006, 0x08d0, 0x3001, 0x0024, 0x0006, 0x0910, /*  280 */ | ||||||
|  | 		0x3000, 0x4024, 0x6100, 0x0024, 0x6042, 0x0024, 0x0030, 0x06d0, /*  288 */ | ||||||
|  | 		0x2800, 0x5711, 0xb880, 0x0024, 0x2900, 0x21c0, 0x4380, 0x184c, /*  290 */ | ||||||
|  | 		0xb880, 0x0024, 0x3800, 0x0024, 0x36f4, 0x8024, 0x36f4, 0x1811, /*  298 */ | ||||||
|  | 		0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, /*  2a0 */ | ||||||
|  | 		0x36f3, 0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, /*  2a8 */ | ||||||
|  | 		0x0006, 0x1152, 0x0000, 0x0804, 0x3200, 0xc024, 0x6346, 0x0024, /*  2b0 */ | ||||||
|  | 		0x6386, 0x2803, 0x0000, 0x0024, 0x2800, 0x4755, 0x0000, 0x0024, /*  2b8 */ | ||||||
|  | 		0x3800, 0x4024, 0x0030, 0x0690, 0x0000, 0x0081, 0xb882, 0x22c1, /*  2c0 */ | ||||||
|  | 		0x3800, 0x4024, 0x0030, 0x0590, 0x2800, 0x4740, 0x3800, 0x4024, /*  2c8 */ | ||||||
|  | 		0x2800, 0x5700, 0x4190, 0x0024, 0x0007, 0x0001, 0x8179, 0x0006, /*  2d0 */ | ||||||
|  | 		0x00a6, 0x3e12, 0x0024, 0x3e05, 0xb814, 0x3625, 0x0024, 0x3e00, /*  2d8 */ | ||||||
|  | 		0x3841, 0x3e00, 0xb843, 0x3e04, 0x3851, 0x0006, 0x1110, 0x3e04, /*  2e0 */ | ||||||
|  | 		0xb813, 0x3000, 0x0024, 0x6080, 0x0024, 0x0006, 0x11d2, 0x2800, /*  2e8 */ | ||||||
|  | 		0x70c5, 0x0000, 0x0081, 0x6010, 0x984c, 0x3800, 0x0024, 0x0006, /*  2f0 */ | ||||||
|  | 		0x10d0, 0x3200, 0x0024, 0xf100, 0x0011, 0xf100, 0x0024, 0xf102, /*  2f8 */ | ||||||
|  | 		0x0400, 0x0006, 0x1311, 0x2900, 0x0400, 0x3100, 0x8024, 0x0030, /*  300 */ | ||||||
|  | 		0x1293, 0x3413, 0x184c, 0x3c04, 0x4024, 0x3b00, 0x0024, 0x3004, /*  308 */ | ||||||
|  | 		0xc024, 0xf400, 0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3310, /*  310 */ | ||||||
|  | 		0x0024, 0x3a00, 0x0024, 0x0006, 0x1212, 0x3200, 0x0024, 0xf100, /*  318 */ | ||||||
|  | 		0x13d1, 0xf100, 0x0402, 0x2900, 0x0400, 0xf102, 0x0c00, 0x0030, /*  320 */ | ||||||
|  | 		0x12d1, 0x0006, 0x1081, 0x3900, 0x0024, 0x3004, 0xc024, 0xf400, /*  328 */ | ||||||
|  | 		0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3300, 0x0024, 0x3a00, /*  330 */ | ||||||
|  | 		0x0024, 0xf400, 0x4440, 0x6010, 0x0024, 0x1fee, 0xe002, 0x2800, /*  338 */ | ||||||
|  | 		0x6bc8, 0x0006, 0x0f00, 0x3800, 0x0024, 0x0006, 0x0010, 0xb886, /*  340 */ | ||||||
|  | 		0x0040, 0x30f0, 0x4024, 0x6c92, 0x40c3, 0x3810, 0x0024, 0xb182, /*  348 */ | ||||||
|  | 		0x23c1, 0x0006, 0x0950, 0x3000, 0x0024, 0x6090, 0x0024, 0x6cd2, /*  350 */ | ||||||
|  | 		0x2000, 0x0000, 0x0000, 0x2800, 0x70c8, 0x0000, 0x0024, 0x3800, /*  358 */ | ||||||
|  | 		0x0024, 0x0000, 0x0210, 0x3010, 0x0024, 0x30f0, 0x4024, 0x6c92, /*  360 */ | ||||||
|  | 		0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, 0x36f4, 0x9813, 0x36f4, /*  368 */ | ||||||
|  | 		0x1811, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, 0x36f3, /*  370 */ | ||||||
|  | 		0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, 0x0007, /*  378 */ | ||||||
|  | 		0x0001, 0x81cc, 0x0006, 0x00f4, 0x3e00, 0x3841, 0x0000, 0x0201, /*  380 */ | ||||||
|  | 		0x3e00, 0xb843, 0x3e01, 0x3845, 0x3e04, 0x3812, 0x0030, 0x0410, /*  388 */ | ||||||
|  | 		0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x08d0, 0x2800, 0x8045, /*  390 */ | ||||||
|  | 		0x0000, 0x0181, 0x6012, 0x0024, 0x0006, 0x1250, 0x2800, 0x7e45, /*  398 */ | ||||||
|  | 		0x0000, 0x05c1, 0x6012, 0x0024, 0x0030, 0x01d0, 0x2800, 0x7c45, /*  3a0 */ | ||||||
|  | 		0x0000, 0x0581, 0x6010, 0x03cc, 0x0000, 0x0024, 0x2800, 0x7a95, /*  3a8 */ | ||||||
|  | 		0x0000, 0x0024, 0x3000, 0x8024, 0x0006, 0x1250, 0x3000, 0x0024, /*  3b0 */ | ||||||
|  | 		0x6092, 0x0024, 0x3800, 0x4024, 0xf400, 0x4010, 0x3800, 0x8024, /*  3b8 */ | ||||||
|  | 		0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, /*  3c0 */ | ||||||
|  | 		0x2000, 0x0000, 0x0000, 0x0024, 0x0030, 0x01d0, 0x3000, 0x0024, /*  3c8 */ | ||||||
|  | 		0x0006, 0x1250, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /*  3d0 */ | ||||||
|  | 		0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /*  3d8 */ | ||||||
|  | 		0x6090, 0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /*  3e0 */ | ||||||
|  | 		0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /*  3e8 */ | ||||||
|  | 		0x6080, 0x0024, 0x0000, 0x0024, 0x2800, 0x8515, 0x0000, 0x0024, /*  3f0 */ | ||||||
|  | 		0x0006, 0x1350, 0x0000, 0x0082, 0x0030, 0x0352, 0xb886, 0x0040, /*  3f8 */ | ||||||
|  | 		0x30f0, 0x4024, 0x4cd2, 0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, /*  400 */ | ||||||
|  | 		0x3a00, 0x0024, 0x3010, 0x0024, 0x30f0, 0x4024, 0x0030, 0x0390, /*  408 */ | ||||||
|  | 		0x2800, 0x7a80, 0x4180, 0x2001, 0x4090, 0x0024, 0x3800, 0x0024, /*  410 */ | ||||||
|  | 		0x0030, 0x0250, 0x3800, 0x0024, 0x0006, 0x1290, 0x3000, 0x0024, /*  418 */ | ||||||
|  | 		0x6090, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x8024, /*  420 */ | ||||||
|  | 		0x3223, 0x0024, 0x32e0, 0x4024, 0x6100, 0x0024, 0x0000, 0x0024, /*  428 */ | ||||||
|  | 		0x2800, 0x8c81, 0x0000, 0x0024, 0x3233, 0x0024, 0x3804, 0x8024, /*  430 */ | ||||||
|  | 		0x3200, 0x0024, 0x6080, 0x0024, 0x0006, 0x0300, 0x2800, 0x8b18, /*  438 */ | ||||||
|  | 		0x0000, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x0024, /*  440 */ | ||||||
|  | 		0x3013, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, 0x3800, 0x0024, /*  448 */ | ||||||
|  | 		0x0006, 0x0850, 0x3004, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, /*  450 */ | ||||||
|  | 		0x6080, 0x0024, 0x3000, 0x0024, 0x2800, 0x9115, 0xf400, 0x4010, /*  458 */ | ||||||
|  | 		0x3000, 0x0024, 0x0000, 0x0201, 0x0000, 0x0005, 0x0030, 0x0210, /*  460 */ | ||||||
|  | 		0xa014, 0x4004, 0x1fff, 0xfe01, 0xae12, 0x0024, 0xc200, 0x0024, /*  468 */ | ||||||
|  | 		0x2800, 0x8180, 0x3800, 0x0024, 0x2800, 0x8ec0, 0x3009, 0x0000, /*  470 */ | ||||||
|  | 		0x0007, 0x0001, 0x8246, 0x0006, 0x0104, 0x0030, 0x1092, 0x0007, /*  478 */ | ||||||
|  | 		0x9250, 0x003f, 0xfc42, 0xb880, 0x184c, 0x3e12, 0x0024, 0x3800, /*  480 */ | ||||||
|  | 		0x0024, 0x0030, 0x0290, 0x38f0, 0x0024, 0x3800, 0x0024, 0x0030, /*  488 */ | ||||||
|  | 		0x0050, 0x3000, 0x4024, 0xb122, 0x0024, 0x6894, 0x2001, 0x0000, /*  490 */ | ||||||
|  | 		0x0141, 0x3a70, 0x4024, 0x0004, 0x1fc1, 0x3a00, 0x4024, 0x0030, /*  498 */ | ||||||
|  | 		0x00d2, 0x0030, 0x0001, 0x3a00, 0x4024, 0x0030, 0x0552, 0x3a10, /*  4a0 */ | ||||||
|  | 		0x0024, 0x3a00, 0x0024, 0x3000, 0x4024, 0xc122, 0x0024, 0x3800, /*  4a8 */ | ||||||
|  | 		0x4024, 0x0030, 0x05d0, 0x0000, 0x03c1, 0x3820, 0x4024, 0x3800, /*  4b0 */ | ||||||
|  | 		0x0024, 0x0000, 0x0310, 0x3010, 0x0024, 0x30f0, 0x4024, 0xf2c2, /*  4b8 */ | ||||||
|  | 		0x0024, 0x3810, 0x0024, 0x0000, 0x3fc0, 0x38f0, 0x4024, 0x0030, /*  4c0 */ | ||||||
|  | 		0x02d0, 0x3000, 0x4024, 0x2912, 0x1400, 0xb104, 0x0024, 0x0006, /*  4c8 */ | ||||||
|  | 		0x1312, 0x6802, 0x0024, 0x000d, 0xac00, 0x6012, 0x2801, 0x0000, /*  4d0 */ | ||||||
|  | 		0x0024, 0x2800, 0x9dc1, 0x0000, 0x0024, 0x3a00, 0x0024, 0x2909, /*  4d8 */ | ||||||
|  | 		0x1b40, 0x3613, 0x0024, 0x0000, 0x0084, 0x0000, 0x1905, 0x2908, /*  4e0 */ | ||||||
|  | 		0xbe80, 0x3613, 0x0024, 0x0000, 0x0000, 0x0006, 0x0302, 0x4002, /*  4e8 */ | ||||||
|  | 		0x0024, 0x4012, 0x0024, 0x4212, 0x0024, 0xf400, 0x4050, 0x3000, /*  4f0 */ | ||||||
|  | 		0x4024, 0x6182, 0x0024, 0x0006, 0x0350, 0x2800, 0xa6c8, 0x0000, /*  4f8 */ | ||||||
|  | 		0x0024, 0x4002, 0x0024, 0x4014, 0x0024, 0x0006, 0x0301, 0x4124, /*  500 */ | ||||||
|  | 		0x0024, 0x0000, 0x0081, 0x4212, 0x0024, 0x4002, 0x4050, 0x4014, /*  508 */ | ||||||
|  | 		0x0003, 0x0006, 0x0301, 0x4122, 0x0024, 0x6192, 0x0024, 0x6090, /*  510 */ | ||||||
|  | 		0x4050, 0x3000, 0x4024, 0x0006, 0x0910, 0x6312, 0x0024, 0x6194, /*  518 */ | ||||||
|  | 		0x0001, 0x4122, 0x0024, 0x2800, 0x9f80, 0x3800, 0x4024, 0x0006, /*  520 */ | ||||||
|  | 		0x12d2, 0x0006, 0x0991, 0x3000, 0x0024, 0x0006, 0x1290, 0x3a00, /*  528 */ | ||||||
|  | 		0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x2900, 0xb200, 0x0000, /*  530 */ | ||||||
|  | 		0x0580, 0x0030, 0x0210, 0x0014, 0x9240, 0x003f, 0xf502, 0x003f, /*  538 */ | ||||||
|  | 		0xffc3, 0x3800, 0x0024, 0x0000, 0x0580, 0x0006, 0x1350, 0x3200, /*  540 */ | ||||||
|  | 		0x4024, 0x4102, 0x0024, 0x3a00, 0x4024, 0x3810, 0x8024, 0x38f0, /*  548 */ | ||||||
|  | 		0xc024, 0x0006, 0x08d0, 0x3800, 0x0024, 0x0030, 0x0690, 0x0000, /*  550 */ | ||||||
|  | 		0x8280, 0xb880, 0x2080, 0x3800, 0x0024, 0x6890, 0x2000, 0x0030, /*  558 */ | ||||||
|  | 		0x0490, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0100, 0x3000, /*  560 */ | ||||||
|  | 		0x584c, 0xb100, 0x0024, 0x0000, 0x0024, 0x2800, 0xb185, 0x0000, /*  568 */ | ||||||
|  | 		0x0024, 0x003f, 0xfec1, 0x3000, 0x1bcc, 0xb010, 0x0024, 0x2908, /*  570 */ | ||||||
|  | 		0x0b80, 0x3800, 0x0024, 0x3613, 0x0024, 0x2910, 0x0180, 0x0000, /*  578 */ | ||||||
|  | 		0xae48, 0x0007, 0x0001, 0x1806, 0x0006, 0x8007, 0x0000, 0x0006, /*  580 */ | ||||||
|  | 		0x002f, 0x0010, 0x17ff, 0x0000, 0x1a00, 0x1dff, 0x0000, 0x1f00, /*  588 */ | ||||||
|  | 		0x3fff, 0x0001, 0x0000, 0x17ff, 0x0001, 0x1c00, 0x3fff, 0x0001, /*  590 */ | ||||||
|  | 		0xe000, 0xfffd, 0xffff, 0x0000, 0x0000, 0x180c, 0x180c, 0x0000, /*  598 */ | ||||||
|  | 		0x0000, 0x0000, 0x4952, 0x4646, 0xffff, 0xffff, 0x4157, 0x4556, /*  5a0 */ | ||||||
|  | 		0x6d66, 0x2074, 0x0010, 0x0000, 0x0001, 0x0001, 0xbb80, 0x0000, /*  5a8 */ | ||||||
|  | 		0x7700, 0x0001, 0x0002, 0x0010, 0x6164, 0x6174, 0xffff, 0xffff, /*  5b0 */ | ||||||
|  | 		0x0006, 0x8006, 0x0000, 0x0006, 0x0005, 0x183c, 0x183c, 0x0000, /*  5b8 */ | ||||||
|  | 		0x0020, 0x0040, 0x0006, 0x8003, 0x0000, 0x0007, 0x0001, 0x5bc0, /*  5c0 */ | ||||||
|  | 		0x0006, 0x0009, 0x801c, 0x7fe4, 0x8039, 0x804e, 0x7fb2, 0x809d, /*  5c8 */ | ||||||
|  | 		0x809c, 0x7f64, 0x8139, 0x0007, 0x0001, 0x82c8, 0x0006, 0x0018, /*  5d0 */ | ||||||
|  | 		0x4080, 0x184c, 0x3e13, 0x780f, 0x2800, 0xb405, 0x4090, 0x380e, /*  5d8 */ | ||||||
|  | 		0x2400, 0xb3c0, 0xf400, 0x4417, 0x3110, 0x0024, 0x3f10, 0x0024, /*  5e0 */ | ||||||
|  | 		0x36f3, 0x8024, 0x36f3, 0x580f, 0x2000, 0x0000, 0x0000, 0x0024, /*  5e8 */ | ||||||
|  | 		0x0007, 0x0001, 0x82d4, 0x0006, 0x002a, 0x3e11, 0xb807, 0x3009, /*  5f0 */ | ||||||
|  | 		0x384a, 0x3e11, 0x3805, 0x3e10, 0xb803, 0x3e00, 0x4442, 0x0001, /*  5f8 */ | ||||||
|  | 		0x800a, 0xbf8e, 0x8443, 0xfe06, 0x0045, 0x3011, 0x0401, 0x545e, /*  600 */ | ||||||
|  | 		0x0385, 0x525e, 0x2040, 0x72ce, 0x1bc1, 0x48ba, 0x9803, 0x4588, /*  608 */ | ||||||
|  | 		0x4885, 0x6fee, 0x1bc2, 0x4ffe, 0x9805, 0xf6fe, 0x1bc4, 0xf7f0, /*  610 */ | ||||||
|  | 		0x2046, 0x3801, 0xdbca, 0x2000, 0x0000, 0x36f1, 0x9807, | ||||||
|  | 	}; | ||||||
|  | 	const uint16_t patch_size = 1567; | ||||||
|  | 	DEBUG("Patching...\n"); | ||||||
|  |  | ||||||
|  | 	_spi->select_vs1053_xcs(); | ||||||
|  | 	SPI.beginTransaction(*_spi_settings); | ||||||
|  | 	 | ||||||
|  | 	for (int i=0; i<patch_size; i++) { | ||||||
|  | 		DEBUG("  %d\n", i); | ||||||
|  | 		unsigned short addr, n, val; | ||||||
|  | 		addr = patch_data[i++]; | ||||||
|  | 		n = patch_data[i++]; | ||||||
|  | 		SPI.transfer(CMD_WRITE); | ||||||
|  | 		SPI.transfer(addr & 0XFF); | ||||||
|  | 		if (n & 0x8000U) { /* RLE run, replicate n samples */ | ||||||
|  | 			n &= 0x7FFF; | ||||||
|  | 			val = patch_data[i++]; | ||||||
|  | 			while (n--) { | ||||||
|  | 				SPI.transfer(val >> 8); | ||||||
|  | 				SPI.transfer(val & 0xFF); | ||||||
|  | 				_wait(); | ||||||
|  | 			} | ||||||
|  | 		} else {           /* Copy run, copy n samples */ | ||||||
|  | 			while (n--) { | ||||||
|  | 				val = patch_data[i++]; | ||||||
|  | 				SPI.transfer(val >> 8); | ||||||
|  | 				SPI.transfer(val & 0xFF); | ||||||
|  | 				_wait(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	SPI.endTransaction(); | ||||||
|  | 	_spi->select_vs1053_xcs(false); | ||||||
|  |  | ||||||
|  | 	DEBUG("Patch sent.\n"); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_write_data(uint8_t* buffer) { | void Player::_write_data(uint8_t* buffer) { | ||||||
| 	SPIMaster::enable(XDCS); | 	_spi->select_vs1053_xdcs(); | ||||||
| 	SPI.beginTransaction(*_spi_settings); | 	SPI.beginTransaction(*_spi_settings); | ||||||
| 	for (uint i=0; i<sizeof(_buffer); i++) { | 	for (uint i=0; i<sizeof(_buffer); i++) { | ||||||
| 		SPI.transfer(_buffer[i]); | 		SPI.transfer(_buffer[i]); | ||||||
| 	} | 	} | ||||||
| 	SPI.endTransaction(); | 	SPI.endTransaction(); | ||||||
| 	SPIMaster::disable(); | 	_spi->select_vs1053_xdcs(false); | ||||||
| } | } | ||||||
|  |  | ||||||
| uint16_t Player::_read_wram(uint16_t address) { | uint16_t Player::_read_wram(uint16_t address) { | ||||||
| @@ -159,174 +457,111 @@ void Player::set_volume(uint8_t vol, bool save) { | |||||||
| 	} | 	} | ||||||
| 	INFO("Setting volume to %d\n", vol); | 	INFO("Setting volume to %d\n", vol); | ||||||
| 	vol = 0xFF - vol; | 	vol = 0xFF - vol; | ||||||
| 	if (vol==0xFF) vol=0xFE; |  | ||||||
| 	uint16_t value = (vol<<8)|vol; | 	uint16_t value = (vol<<8)|vol; | ||||||
| 	DEBUG("Setting volume register to 0x%04X\n", value); | 	DEBUG("Setting volume register to 0x%04X\n", value); | ||||||
| 	_write_control_register(SCI_VOL, value); | 	_write_control_register(SCI_VOL, value); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::vol_up() { | void Player::vol_up() { | ||||||
| 	if (_volume == VOLUME_MAX) play_system_sound("volume_max.mp3"); | 	if (!is_playing()) return; | ||||||
| 	else if (_volume + VOLUME_STEP > VOLUME_MAX) set_volume(VOLUME_MAX); | 	uint16_t vol = _volume + VOLUME_STEP; | ||||||
| 	else set_volume(_volume + VOLUME_STEP); | 	if (vol > VOLUME_MAX) vol=VOLUME_MAX; | ||||||
|  | 	set_volume(vol); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::vol_down() { | void Player::vol_down() { | ||||||
| 	if (_volume >= VOLUME_MIN + VOLUME_STEP) set_volume(_volume - VOLUME_STEP); | 	if (!is_playing()) return; | ||||||
| 	else if (_volume == VOLUME_MIN) play_system_sound("volume_min.mp3"); | 	int16_t vol = _volume - VOLUME_STEP; | ||||||
| 	else set_volume(VOLUME_MIN); | 	if (vol < VOLUME_MIN) vol=VOLUME_MIN; | ||||||
|  | 	set_volume(vol); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_mute() { | void Player::_mute() { | ||||||
| 	INFO("Muting."); | 	INFO("Muting.\n"); | ||||||
| 	set_volume(0, false); | 	_speaker_off(); | ||||||
|  | 	set_volume(1, false); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_unmute() { | void Player::_unmute() { | ||||||
| 	INFO("Unmuting."); | 	INFO("Unmuting.\n"); | ||||||
| 	set_volume(_volume, false); | 	set_volume(_volume, false); | ||||||
|  | 	_speaker_on(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::track_next() { | void Player::track_next() { | ||||||
| 	if (_state != playing) return; | 	if (_state != playing) return; | ||||||
| 	if (_playing_index + 1 >= _playing_album_songs) { | 	if (!_current_playlist->has_track_next()) { | ||||||
| 		play_system_sound("no_next_song.mp3"); |  | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
| 	stop(); | 	stop(); | ||||||
| 	play_song(_playing_album, _playing_index + 1); | 	_current_playlist->track_next(); | ||||||
|  | 	play(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::track_prev() { | void Player::track_prev() { | ||||||
| 	if (_state != playing) return; | 	if (_state != playing) return; | ||||||
| 	if (_current_play_position > 100000) { | 	if (_current_play_position > 100000) { | ||||||
| 		stop(); | 		stop(); | ||||||
| 		play_song(_playing_album, _playing_index); | 		_current_playlist->track_restart(); | ||||||
|  | 		play(); | ||||||
| 	} else { | 	} else { | ||||||
| 		if (_playing_index == 0) { | 		if (!_current_playlist->has_track_prev()) { | ||||||
| 			play_system_sound("no_prev_song.mp3"); |  | ||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
| 		stop(); | 		stop(); | ||||||
| 		play_song(_playing_album, _playing_index - 1); | 		_current_playlist->track_prev(); | ||||||
|  | 		play(); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| std::list<String> Player::ls(String path) { | void Player::set_track(uint8_t id) { | ||||||
| 	SPIMaster::enable(PIN_SD_CS); | 	stop(); | ||||||
| 	std::list<String> result; | 	_current_playlist->set_track(id); | ||||||
| 	if (!SD.exists(path)) return result; | 	play(); | ||||||
| 	File dir = SD.open(path); |  | ||||||
| 	File entry; |  | ||||||
| 	while (entry = dir.openNextFile()) { |  | ||||||
| 		String filename = entry.name(); |  | ||||||
| 		if (entry.isDirectory()) filename.concat("/"); |  | ||||||
| 		result.push_back(filename); |  | ||||||
| 	} |  | ||||||
| 	return result; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| String Player::_find_album_dir(String id) { | bool Player::is_playing() { | ||||||
| 	String id_with_divider = id + " - "; | 	return _state == playing; | ||||||
| 	File root = SD.open("/"); |  | ||||||
| 	File entry; |  | ||||||
| 	String result = String(""); |  | ||||||
| 	while ((result.length()==0) && (entry = root.openNextFile())) { |  | ||||||
| 		String name = entry.name(); |  | ||||||
| 		if (entry.isDirectory() && (name.startsWith(id_with_divider) || name.equals(id))) { |  | ||||||
| 			result = name; |  | ||||||
| 		} |  | ||||||
| 		entry.close(); |  | ||||||
| 	} |  | ||||||
| 	root.close(); |  | ||||||
| 	return result; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| std::list<String> Player::_files_in_dir(String path) { | bool Player::play(Playlist* p) { | ||||||
| 	DEBUG("Examining folder %s...\n", path.c_str()); | 	_current_playlist = p; | ||||||
| 	if (!path.startsWith("/")) path = String("/") + path; | 	return play(); | ||||||
| 	if (!path.endsWith("/")) path.concat("/"); |  | ||||||
| 	std::list<String> result; |  | ||||||
| 	if (!SD.exists(path)) return result; |  | ||||||
| 	File dir = SD.open(path); |  | ||||||
| 	File entry; |  | ||||||
| 	while (entry = dir.openNextFile()) { |  | ||||||
| 		String filename = entry.name(); |  | ||||||
| 		if (!entry.isDirectory() && |  | ||||||
| 		    !filename.startsWith(".") && |  | ||||||
| 		    ( filename.endsWith(".mp3") || |  | ||||||
| 		      filename.endsWith(".ogg") || |  | ||||||
| 		      filename.endsWith(".wma") || |  | ||||||
| 		      filename.endsWith(".mp4") || |  | ||||||
| 		      filename.endsWith(".mpa"))) { |  | ||||||
| 			DEBUG("    Adding entry %s\n", filename.c_str()); |  | ||||||
| 			result.push_back(path + filename); |  | ||||||
| 		} else { |  | ||||||
| 			DEBUG("  Ignoring entry %s\n", filename.c_str()); |  | ||||||
| 		} |  | ||||||
| 		entry.close(); |  | ||||||
| 	} |  | ||||||
| 	dir.close(); |  | ||||||
| 	result.sort(); |  | ||||||
|  |  | ||||||
| 	return result; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| bool Player::play_album(String album) { | bool Player::play() { | ||||||
| 	//if (_state==playing) stop(); | 	if (_state == sleeping || _state == recording) _wakeup(); | ||||||
| 	album_state s = _last_tracks[album.c_str()]; |  | ||||||
| 	DEBUG("Last index for album %s was %d,%d\n", album.c_str(), s.index, s.position); |  | ||||||
| 	return play_song(album, s.index, s.position); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool Player::play_song(String album, uint8_t index, uint32_t skip_to) { |  | ||||||
| 	if (_state != idle) return false; | 	if (_state != idle) return false; | ||||||
| 	DEBUG("Trying to play song at index %d, offset %d of album %s\n", index, skip_to, album.c_str()); | 	if (_current_playlist == NULL) return false; | ||||||
| 	String path = _find_album_dir(album); | 	if (_current_playlist->get_file_count()==0) return false; | ||||||
| 	if (path.length()==0) { | 	_current_playlist->start(); | ||||||
| 		ERROR("Could not find album.\n"); | 	String file; | ||||||
| 		return false; | 	if (!_current_playlist->get_current_file(&file)) { | ||||||
| 	} else { |  | ||||||
| 		INFO("Found album in dir '%s'.\n", path.c_str()); |  | ||||||
| 	} |  | ||||||
| 	std::list<String> files = _files_in_dir(path); |  | ||||||
| 	_playing_album_songs = files.size(); |  | ||||||
| 	DEBUG("Found %d songs in album\n", files.size()); |  | ||||||
| 	if (index >= files.size()) { |  | ||||||
| 		ERROR("No matching file found - not playing.\n"); |  | ||||||
| 		return false; | 		return false; | ||||||
| 	} | 	} | ||||||
| 	//std::list<String>::iterator it = files.begin(); | 	uint32_t position = _current_playlist->get_position(); | ||||||
| 	//std::advance(it, index); |  | ||||||
| 	String file = *(std::next(files.begin(), index)); |  | ||||||
| 	_state = playing; | 	_state = playing; | ||||||
| 	_playing_album = album; | 	_play_file(file, position); | ||||||
| 	_playing_index = index; | 	_controller->send_player_status(); | ||||||
| 	_set_last_track(album.c_str(), index, skip_to); |  | ||||||
| 	_play_file(file, skip_to); |  | ||||||
| 	return true; | 	return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::play_system_sound(String filename) { |  | ||||||
| 	//String file = String("/system/") + filename; |  | ||||||
| 	String file = filename; |  | ||||||
| 	if (!SD.exists(file)) { |  | ||||||
| 		ERROR("File %s does not exist!\n", file.c_str()); |  | ||||||
| 		return; |  | ||||||
| 	} |  | ||||||
| 	if (_state == playing) { |  | ||||||
| 		stop(); |  | ||||||
| 		_state = system_sound_while_playing; |  | ||||||
| 	} else { |  | ||||||
| 		_state = system_sound_while_stopped; |  | ||||||
| 	} |  | ||||||
| 	_play_file(file, 0); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void Player::_play_file(String file, uint32_t file_offset) { | void Player::_play_file(String file, uint32_t file_offset) { | ||||||
| 	INFO("play_file('%s', %d)\n", file.c_str(), file_offset); | 	INFO("play_file('%s', %d)\n", file.c_str(), file_offset); | ||||||
| 	_file = SD.open(file); | 	_spi->select_sd(); | ||||||
| 	if (!_file) return; | 	if (file.startsWith("/")) { | ||||||
|  | 		_file = new SDDataSource(file); | ||||||
|  | 	} else if (file.startsWith("http")) { | ||||||
|  | 		_file = new HTTPSDataSource(file); | ||||||
|  | 	} else { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 	_file_size = _file->size(); | ||||||
|  | 	_spi->select_sd(false); | ||||||
|  | 	if (!_file || !_file->usable()) { | ||||||
|  | 		DEBUG("Could not open file %s", file.c_str()); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	DEBUG("Resetting SCI_DECODE_TIME...\n"); | 	DEBUG("Resetting SCI_DECODE_TIME...\n"); | ||||||
| 	_write_control_register(SCI_DECODE_TIME, 0); | 	_write_control_register(SCI_DECODE_TIME, 0); | ||||||
| @@ -334,48 +569,29 @@ void Player::_play_file(String file, uint32_t file_offset) { | |||||||
| 	_write_control_register(SCI_STATUS, _read_control_register(SCI_STATUS) & ~SS_DO_NOT_JUMP); | 	_write_control_register(SCI_STATUS, _read_control_register(SCI_STATUS) & ~SS_DO_NOT_JUMP); | ||||||
| 	delay(100); | 	delay(100); | ||||||
|  |  | ||||||
|  | 	_spi->select_sd(); | ||||||
| 	if (file_offset == 0) { | 	if (file_offset == 0) { | ||||||
| 		_file.seek(_id3_tag_offset(_file)); | 		_file->skip_id3_tag(); | ||||||
| 	} | 	} | ||||||
| 	_refills = 0; | 	_refills = 0; | ||||||
| 	_current_play_position = _file.position(); | 	_current_play_position = _file->position(); | ||||||
|  | 	_spi->select_sd(false); | ||||||
| 	_skip_to = file_offset; | 	_skip_to = file_offset; | ||||||
| 	if (_skip_to>0) _mute(); | 	if (_skip_to>0) _mute(); | ||||||
|  | 	else _speaker_on(); | ||||||
| 	INFO("Now playing.\n"); | 	INFO("Now playing.\n"); | ||||||
| } | 	_controller->send_player_status(); | ||||||
|  |  | ||||||
| uint32_t Player::_id3_tag_offset(File f) { |  | ||||||
| 	uint32_t original_position = f.position(); |  | ||||||
| 	uint32_t offset = 0; |  | ||||||
| 	if (f.read()=='I' && f.read()=='D' && f.read()=='3') { |  | ||||||
| 		DEBUG("ID3 tag found\n"); |  | ||||||
| 		// Skip ID3 tag version |  | ||||||
| 		f.read(); f.read(); |  | ||||||
| 		byte tags = f.read(); |  | ||||||
| 		bool footer_present = tags & 0x10; |  | ||||||
| 		DEBUG("ID3 footer found: %d\n", footer_present); |  | ||||||
| 		for (byte i=0; i<4; i++) { |  | ||||||
| 			offset <<= 7; |  | ||||||
| 			offset |= (0x7F & f.read()); |  | ||||||
| 		} |  | ||||||
| 		offset += 10; |  | ||||||
| 		if (footer_present) offset += 10; |  | ||||||
| 		DEBUG("ID3 tag length is %d bytes.\n", offset); |  | ||||||
| 	} else { |  | ||||||
| 		DEBUG("No ID3 tag found\n"); |  | ||||||
| 	} |  | ||||||
| 	f.seek(original_position); |  | ||||||
| 	return offset; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_flush(uint count, int8_t byte) { | void Player::_flush(uint count, int8_t byte) { | ||||||
| 	SPIMaster::enable(XDCS); | 	_spi->select_vs1053_xdcs(); | ||||||
| 	SPI.beginTransaction(*_spi_settings); | 	SPI.beginTransaction(*_spi_settings); | ||||||
| 	for(uint i=0; i<count; i++) { | 	for(uint i=0; i<count; i++) { | ||||||
| 		_wait(); | 		_wait(); | ||||||
| 		SPI.transfer(byte); | 		SPI.transfer(byte); | ||||||
| 	} | 	} | ||||||
| 	SPI.endTransaction(); | 	SPI.endTransaction(); | ||||||
|  | 	_spi->select_vs1053_xdcs(false); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_finish_playing() { | void Player::_finish_playing() { | ||||||
| @@ -390,15 +606,15 @@ void Player::_finish_playing() { | |||||||
| 	// If we reached this, the Chip didn't stop. That should not happen. | 	// If we reached this, the Chip didn't stop. That should not happen. | ||||||
| 	// (That's written in the manual.) | 	// (That's written in the manual.) | ||||||
| 	// Reset the chip. | 	// Reset the chip. | ||||||
| 	_init(); | 	init(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::stop() { | void Player::stop(bool turn_speaker_off) { | ||||||
| 	if (_state != playing /* && _state != system_sound_while_playing && _state != system_sound_while_stopped*/) return; | 	if (_state != playing) return; | ||||||
| 	INFO("Stopping...\n"); | 	INFO("Stopping...\n"); | ||||||
| 	if (_state == playing) { | 	_current_playlist->set_position(_current_play_position); | ||||||
| 		_set_last_track(_playing_album.c_str(), _playing_index, (uint32_t)_file.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); | ||||||
| @@ -408,7 +624,7 @@ void Player::stop() { | |||||||
| 		uint16_t mode = _read_control_register(SCI_MODE); | 		uint16_t mode = _read_control_register(SCI_MODE); | ||||||
| 		if ((mode & SM_CANCEL) == 0) { | 		if ((mode & SM_CANCEL) == 0) { | ||||||
| 			_flush(2052, endbyte); | 			_flush(2052, endbyte); | ||||||
| 			_finish_stopping(); | 			_finish_stopping(turn_speaker_off); | ||||||
| 			break; | 			break; | ||||||
| 		} else if (_stop_delay > 2048) { | 		} else if (_stop_delay > 2048) { | ||||||
| 			init(); | 			init(); | ||||||
| @@ -418,51 +634,57 @@ void Player::stop() { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_finish_stopping() { | void Player::_finish_stopping(bool turn_speaker_off) { | ||||||
|  | 	if (turn_speaker_off) _speaker_off(); | ||||||
| 	_state = idle; | 	_state = idle; | ||||||
|  | 	_stopped_at = millis(); | ||||||
| 	if (_file) { | 	if (_file) { | ||||||
| 		_file.close(); | 		_file->close(); | ||||||
|  | 		delete _file; | ||||||
| 	} | 	} | ||||||
|  | 	_current_play_position = 0; | ||||||
|  | 	_file_size = 0; | ||||||
| 	INFO("Stopped.\n"); | 	INFO("Stopped.\n"); | ||||||
|  | 	_controller->send_player_status(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_refill() { | void Player::_refill() { | ||||||
| 	SPIMaster::enable(PIN_SD_CS); | 	_spi->select_sd(); | ||||||
| 	_refills++; | 	_refills++; | ||||||
| 	if (_refills % 1000 == 0) DEBUG("."); | 	if (_refills % 1000 == 0) DEBUG("."); | ||||||
| 	uint8_t result = _file.read(_buffer, sizeof(_buffer)); | 	uint8_t result = _file->read(_buffer, sizeof(_buffer)); | ||||||
|  | 	_spi->select_sd(false); | ||||||
| 	if (result == 0) { | 	if (result == 0) { | ||||||
| 		// File is over. | 		// File is over. | ||||||
| 		DEBUG("EOF reached.\n"); | 		DEBUG("EOF reached.\n"); | ||||||
|  | 		_skip_to = 0; | ||||||
| 		_finish_playing(); | 		_finish_playing(); | ||||||
| 		if (_state == system_sound_while_playing) { | 		_finish_stopping(false); | ||||||
| 			_finish_stopping(); | 		if (_current_playlist->has_track_next()) { | ||||||
| 			play_album(_playing_album); | 			_current_playlist->track_next(); | ||||||
| 			return; | 			play(); | ||||||
| 		} else if (_state == system_sound_while_stopped) { | 		} else { | ||||||
| 			_finish_stopping(); | 			_current_playlist->reset(); | ||||||
| 			return; | 			_controller->send_player_status(); | ||||||
| 		} | 		} | ||||||
| 		 | 		 | ||||||
| 		_finish_stopping(); |  | ||||||
| 		bool result = play_song(_playing_album, _playing_index + 1); |  | ||||||
| 		if (!result) { |  | ||||||
| 			_set_last_track(_playing_album.c_str(), 0, 0); |  | ||||||
| 		} |  | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
| 	_current_play_position+=result; | 	_current_play_position+=result; | ||||||
| 	_write_data(_buffer); | 	_write_data(_buffer); | ||||||
|  |  | ||||||
| 	if (_skip_to > 0) { | 	if (_skip_to > 0) { | ||||||
| 		if (_skip_to > _file.position()) { | 		if (_skip_to > _file->position()) { | ||||||
| 			uint16_t status = _read_control_register(SCI_STATUS); | 			uint16_t status = _read_control_register(SCI_STATUS); | ||||||
| 			if ((status & SS_DO_NOT_JUMP) == 0) { | 			if ((status & SS_DO_NOT_JUMP) == 0) { | ||||||
| 				DEBUG("Skipping to %d.\n", _skip_to); | 				DEBUG("Skipping to %d.\n", _skip_to); | ||||||
| 				_flush(2048, _get_endbyte()); | 				_flush(2048, _get_endbyte()); | ||||||
| 				_file.seek(_skip_to); | 				_spi->select_sd(); | ||||||
|  | 				_file->seek(_skip_to); | ||||||
|  | 				_spi->select_sd(false); | ||||||
| 				_skip_to = 0; | 				_skip_to = 0; | ||||||
| 				_unmute(); | 				_unmute(); | ||||||
|  | 				_controller->send_position(); | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			_skip_to = 0; | 			_skip_to = 0; | ||||||
| @@ -472,21 +694,61 @@ void Player::_refill() { | |||||||
| } | } | ||||||
|  |  | ||||||
| bool Player::_refill_needed() { | bool Player::_refill_needed() { | ||||||
| 	return _state==playing || | 	return _state==playing || _state==stopping; | ||||||
| 		_state==stopping || |  | ||||||
| 		_state==system_sound_while_playing || |  | ||||||
| 		_state==system_sound_while_stopped; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| bool Player::loop() { | bool Player::loop() { | ||||||
| 	if (digitalRead(DREQ) && _refill_needed()) { | 	if (PIN_VS1053_DREQ() && _refill_needed()) { | ||||||
| 		_refill(); | 		_refill(); | ||||||
| 		return true; | 		return true; | ||||||
| 	} | 	} | ||||||
|  | 	 | ||||||
|  | 	if (_state == recording) { | ||||||
|  | 		DEBUG("r"); | ||||||
|  | 		uint16_t samples_available = _read_control_register(SCI_HDAT1, false); | ||||||
|  | 		uint16_t vu_value = _read_control_register(SCI_AICTRL0, false); | ||||||
|  | 		DEBUG("Samples available: %4d, VU meter: 0x%04X\n", samples_available, vu_value); | ||||||
|  | 		if (samples_available >= 500) { | ||||||
|  | 			unsigned long sum = 0; | ||||||
|  | 			for (int i=0; i<500; i++) { | ||||||
|  | 				uint16_t sample = _read_control_register(SCI_HDAT0, false); | ||||||
|  | 				sum += sample * sample; | ||||||
|  | 			} | ||||||
|  | 			double result = sqrt(sum / 500); | ||||||
|  | 			DEBUG("Loudness: %f", result); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (_state == idle && _stopped_at < millis() - VS1053_SLEEP_DELAY) { | ||||||
|  | 		_sleep(); | ||||||
|  | 		//_record(); | ||||||
|  | 	} | ||||||
| 	return false; | 	return false; | ||||||
| } | } | ||||||
|  |  | ||||||
| void Player::_set_last_track(const char* album, uint8_t index, uint32_t position) { | String Player::json() { | ||||||
| 	DEBUG("Setting _last_track[%s]=%d,%d.\n", album, index, position); | 	DynamicJsonDocument json(10240); | ||||||
| 	_last_tracks[album] = {index, position}; | 	json["_type"] = "player"; | ||||||
|  | 	json["playing"] = is_playing(); | ||||||
|  | 	if (_current_playlist) { | ||||||
|  | 		JsonObject playlist = json.createNestedObject("playlist"); | ||||||
|  | 		_current_playlist->json(playlist); | ||||||
|  | 	} else { | ||||||
|  | 		json["playlist"] = nullptr; | ||||||
|  | 	} | ||||||
|  | 	JsonObject volume = json.createNestedObject("volume"); | ||||||
|  | 	volume["current"] = _volume; | ||||||
|  | 	volume["min"] = VOLUME_MIN; | ||||||
|  | 	volume["max"] = VOLUME_MAX; | ||||||
|  | 	volume["step"] = VOLUME_STEP; | ||||||
|  | 	return json.as<String>(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | String Player::position_json() { | ||||||
|  | 	if (!is_playing()) return "null"; | ||||||
|  | 	DynamicJsonDocument json(200); | ||||||
|  | 	json["_type"] = "position"; | ||||||
|  | 	json["position"] = _current_play_position; | ||||||
|  | 	json["file_size"] = _file_size; | ||||||
|  | 	return json.as<String>(); | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										392
									
								
								src/playlist.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										392
									
								
								src/playlist.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,392 @@ | |||||||
|  | #include <playlist.h> | ||||||
|  | #include "spi_master.h" | ||||||
|  | #include "config.h" | ||||||
|  | #include <SD.h> | ||||||
|  | #include <algorithm> | ||||||
|  | #include <ArduinoJson.h> | ||||||
|  | #include <TinyXML.h> | ||||||
|  |  | ||||||
|  | Playlist::Playlist(String path) { | ||||||
|  | 	_path = path; | ||||||
|  | 	if (path.startsWith("/")) { | ||||||
|  | 		persistence = PERSIST_TEMPORARY; | ||||||
|  | 		_add_path(path); | ||||||
|  | 	} else if (path.startsWith("http")) { | ||||||
|  | 		_examine_http_url(path); | ||||||
|  | 	} | ||||||
|  | 	if (_title.length()==0) _title=path; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::_add_path(String path) { | ||||||
|  | 	SPIMaster::select_sd(); | ||||||
|  | 	TRACE("Examining folder %s...\n", path.c_str()); | ||||||
|  | 	if (!path.startsWith("/")) path = String("/") + path; | ||||||
|  | 	if (!SD.exists(path)) { | ||||||
|  | 		DEBUG("Could not open path '%s'.\n", path.c_str()); | ||||||
|  | 		SPIMaster::select_sd(false); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 	_title = path.substring(1); | ||||||
|  | 	int idx = _title.indexOf('/'); | ||||||
|  | 	if (idx>0) { | ||||||
|  | 		_title.remove(idx); | ||||||
|  | 	} | ||||||
|  | 	File dir = SD.open(path); | ||||||
|  | 	File entry; | ||||||
|  | 	while (entry = dir.openNextFile()) { | ||||||
|  | 		String filename = entry.name(); | ||||||
|  | 		filename = filename.substring(path.length() + 1); | ||||||
|  | 		String ext = filename.substring(filename.length() - 4); | ||||||
|  | 		if (!entry.isDirectory() && | ||||||
|  | 			!filename.startsWith(".") && | ||||||
|  | 			( ext.equals(".mp3") || | ||||||
|  | 			  ext.equals(".ogg") || | ||||||
|  | 			  ext.equals(".wma") || | ||||||
|  | 			  ext.equals(".mp4") || | ||||||
|  | 			  ext.equals(".mpa"))) { | ||||||
|  | 			TRACE("    Adding entry %s\n", entry.name()); | ||||||
|  | 			String title = filename.substring(0, filename.length() - 4); | ||||||
|  | 			_files.push_back({.filename=entry.name(), .title=title, .id=String(_files.size())}); | ||||||
|  | 			bool non_ascii_chars = false; | ||||||
|  | 			for(int i=0; i<filename.length(); i++) { | ||||||
|  | 				char c = filename.charAt(i); | ||||||
|  | 				if (c < 0x20 || c >= 0x7F) { | ||||||
|  | 					non_ascii_chars = true; | ||||||
|  | 					break; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if (non_ascii_chars) { | ||||||
|  | 				ERROR("WARNING: File '%s' contains non-ascii chars!\n", filename.c_str()); | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			TRACE("  Ignoring entry %s\n", filename.c_str()); | ||||||
|  | 		} | ||||||
|  | 		entry.close(); | ||||||
|  | 	} | ||||||
|  | 	dir.close(); | ||||||
|  | 	SPIMaster::select_sd(false); | ||||||
|  | 	std::sort(_files.begin(), _files.end()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::_examine_http_url(String url) { | ||||||
|  | 	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")) { | ||||||
|  | 		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 (tag.equals("/rss/channel/title") && (status & STATUS_TAG_TEXT)) { | ||||||
|  | 		xml_album_title = data; | ||||||
|  | 	} else if (tag.endsWith("/item") && (status & STATUS_START_TAG)) { | ||||||
|  | 		xml_title = ""; | ||||||
|  | 		xml_url = ""; | ||||||
|  | 		xml_guid = ""; | ||||||
|  | 	} else if (tag.endsWith("/item/title") && (status & STATUS_TAG_TEXT)) { | ||||||
|  | 		xml_title = String(data); | ||||||
|  | 	} else if (tag.endsWith("/item/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("/item/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("/item/enclosure") && tag.equals("url") && (status & STATUS_ATTR_TEXT)) { | ||||||
|  | 		DEBUG("found url\n"); | ||||||
|  | 		xml_enclosure_url = String(data); | ||||||
|  | 	} else if (tag.endsWith("/item/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)) { | ||||||
|  | 		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}); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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>=-1 && c!='\r' && c!='\n') { | ||||||
|  | 			line += c; | ||||||
|  | 		} else { | ||||||
|  | 			if (line.equals("#EXTM3U")) { | ||||||
|  | 				// Do nothing | ||||||
|  | 			} else if (line.startsWith("#EXTINF")) { | ||||||
|  | 				int idx = line.indexOf(","); | ||||||
|  | 				if (idx>4) { | ||||||
|  | 					// Get the title | ||||||
|  | 					title = line.substring(idx+1); | ||||||
|  | 					if (_title.length()==0) _title=title; | ||||||
|  | 				} | ||||||
|  | 			} else if (line.startsWith("http")) { | ||||||
|  | 				if (title.length()==0) title = line; | ||||||
|  | 				_files.push_back({.filename=line, .title=title, .id="none"}); | ||||||
|  | 				title = ""; | ||||||
|  | 			} | ||||||
|  | 			line = ""; | ||||||
|  | 		} | ||||||
|  | 	} while (i>=0); | ||||||
|  | 	// don't close http at the end | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::_parse_pls(HTTPClientWrapper* http) { | ||||||
|  | 	// http is already initialized | ||||||
|  | 	String line; | ||||||
|  | 	String title = ""; | ||||||
|  | 	String url = ""; | ||||||
|  | 	int last_index = -1; | ||||||
|  | 	int index; | ||||||
|  |  | ||||||
|  | 	while(true) { | ||||||
|  | 		line = http->readLine(); | ||||||
|  | 		if (line.startsWith("Title")) { | ||||||
|  | 			uint8_t eq_idx = line.indexOf('='); | ||||||
|  | 			if (eq_idx==-1) continue; | ||||||
|  |  | ||||||
|  | 			index = line.substring(5, eq_idx-4).toInt(); | ||||||
|  | 			title = line.substring(eq_idx+1); | ||||||
|  | 			if (index != last_index) { | ||||||
|  | 				url = ""; | ||||||
|  | 				last_index = index; | ||||||
|  | 			} | ||||||
|  | 		} else if (line.startsWith("File")) { | ||||||
|  | 			uint8_t eq_idx = line.indexOf('='); | ||||||
|  | 			if (eq_idx==-1) continue; | ||||||
|  |  | ||||||
|  | 			index = line.substring(5, eq_idx-4).toInt(); | ||||||
|  | 			url = line.substring(eq_idx+1); | ||||||
|  | 			if (index != last_index) { | ||||||
|  | 				title = ""; | ||||||
|  | 				last_index = index; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (title.length()>0 && url.length()>0) { | ||||||
|  | 			_files.push_back({.filename=url, .title=title, .id="none"}); | ||||||
|  | 			last_index = -1; | ||||||
|  | 			title = ""; | ||||||
|  | 			url = ""; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	// don't close http at the end | ||||||
|  | } | ||||||
|  |  | ||||||
|  | String Playlist::path() { | ||||||
|  | 	return _path; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint16_t Playlist::get_file_count() { | ||||||
|  | 	return _files.size(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::start() { | ||||||
|  | 	_started = true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Playlist::has_track_prev() { | ||||||
|  | 	return _current_track > 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Playlist::has_track_next() { | ||||||
|  | 	return _current_track < _files.size()-1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Playlist::track_prev() { | ||||||
|  | 	if (_current_track > 0) { | ||||||
|  | 		_current_track--; | ||||||
|  | 		_position = 0; | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 	return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Playlist::track_next() { | ||||||
|  | 	if (_current_track < _files.size()-1) { | ||||||
|  | 		_current_track++; | ||||||
|  | 		_position = 0; | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 	return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Playlist::set_track(uint8_t track) { | ||||||
|  | 	if (track < _files.size()) { | ||||||
|  | 		_current_track = track; | ||||||
|  | 		_position = 0; | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 	return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::set_track_by_id(String id) { | ||||||
|  | 	for (int i=0; i<_files.size(); i++) { | ||||||
|  | 		if (id.equals(_files[i].id)) { | ||||||
|  | 			set_track(i); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::track_restart() { | ||||||
|  | 	_position = 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::shuffle(uint8_t random_offset) { | ||||||
|  | 	DEBUG("Shuffling the playlist with an offset of %d...\n", random_offset); | ||||||
|  | 	for (int i=random_offset; i<_files.size(); i++) { | ||||||
|  | 		int j = random(random_offset, _files.size()-1); | ||||||
|  | 		if (i!=j) { | ||||||
|  | 			TRACE("  Swapping elements %d and %d.\n", i, j); | ||||||
|  | 			PlaylistEntry temp = _files[i]; | ||||||
|  | 			_files[i] = _files[j]; | ||||||
|  | 			_files[j] = temp; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	_shuffled = true; | ||||||
|  | 	TRACE("Done.\n"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::advent_shuffle(uint8_t day) { | ||||||
|  | 	TRACE("advent_shuffle running...\n"); | ||||||
|  | 	 | ||||||
|  | 	// Not enough songs till the current day? Play all songs in the default order. | ||||||
|  | 	if (day > _files.size()) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// We are in the "different playlist every day" mode. So we don't persist it in order to not miss changes. | ||||||
|  | 	persistence = PERSIST_NONE; | ||||||
|  |  | ||||||
|  | 	 | ||||||
|  | 	_files.insert(_files.begin(), _files[day - 1]); | ||||||
|  | 	_files.erase(_files.begin() + day, _files.end()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::reset() { | ||||||
|  | 	std::sort(_files.begin(), _files.end()); | ||||||
|  | 	_current_track = 0; | ||||||
|  | 	_position = 0; | ||||||
|  | 	_shuffled = false; | ||||||
|  | 	_started = false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | String Playlist::get_current_track_id() { | ||||||
|  | 	if (_current_track > _files.size()) { | ||||||
|  | 		return ""; | ||||||
|  | 	} | ||||||
|  | 	return _files[_current_track].id; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Playlist::get_current_file(String* dst) { | ||||||
|  | 	if (_current_track > _files.size()) { | ||||||
|  | 		return false; | ||||||
|  | 	} else { | ||||||
|  | 		dst->concat(_files[_current_track].filename); | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint32_t Playlist::get_position() { | ||||||
|  | 	return _position; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::set_position(uint32_t p) { | ||||||
|  | 	_position = p; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool Playlist::is_fresh() { | ||||||
|  | 	return !_shuffled && !_started && _position==0 && _current_track==0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::dump() { | ||||||
|  | 	for (int i=0; i<_files.size(); i++) { | ||||||
|  | 		DEBUG("  %02d %2s %s\n", i+1, (i==_current_track) ? "->" : "", _files[i].filename.c_str()); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Playlist::json(JsonObject json) { | ||||||
|  | 	json["_type"] = "playlist"; | ||||||
|  | 	json["title"] = _title; | ||||||
|  | 	JsonArray files = json.createNestedArray("files"); | ||||||
|  | 	for (PlaylistEntry entry: _files) { | ||||||
|  | 		JsonObject o = files.createNestedObject(); | ||||||
|  | 		o["filename"] = entry.filename; | ||||||
|  | 		o["title"] = entry.title; | ||||||
|  | 		o["id"] = entry.id; | ||||||
|  | 	} | ||||||
|  | 	json["current_track"] = _current_track; | ||||||
|  | 	json["has_track_next"] = has_track_next(); | ||||||
|  | 	json["has_track_prev"] = has_track_prev(); | ||||||
|  | } | ||||||
							
								
								
									
										239
									
								
								src/playlist_manager.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								src/playlist_manager.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,239 @@ | |||||||
|  | #include "playlist_manager.h" | ||||||
|  | #include <SD.h> | ||||||
|  | #include "spi_master.h" | ||||||
|  | #include <ArduinoJson.h> | ||||||
|  |  | ||||||
|  | PlaylistManager::PlaylistManager() { | ||||||
|  | 	scan_files(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void PlaylistManager::scan_files() { | ||||||
|  | 	SPIMaster::select_sd(); | ||||||
|  | 	std::vector<String> folders; | ||||||
|  | 	File root = SD.open("/"); | ||||||
|  | 	File entry; | ||||||
|  | 	while (entry = root.openNextFile()) { | ||||||
|  | 		String foldername = entry.name(); | ||||||
|  | 		if (foldername.startsWith("/.")) continue; | ||||||
|  | 		foldername.remove(foldername.length()); | ||||||
|  | 		folders.push_back(foldername); | ||||||
|  | 		_check_for_special_chars(foldername); | ||||||
|  | 		entry.close(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_map.clear(); | ||||||
|  | 	if (!SD.exists("/_mapping.txt\n")) { | ||||||
|  | 		ERROR("WARNING: File /_mapping.txt not found.\n"); | ||||||
|  | 	} else { | ||||||
|  | 		File f = SD.open("/_mapping.txt"); | ||||||
|  | 		DEBUG("Reading /_mapping.txt...\n"); | ||||||
|  | 		while (f.available()) { | ||||||
|  | 			char buffer[512]; | ||||||
|  | 			size_t pos = f.readBytesUntil('\n', buffer, 511); | ||||||
|  | 			buffer[pos] = '\0'; | ||||||
|  |  | ||||||
|  | 			String data = buffer; | ||||||
|  | 			uint8_t eq = data.indexOf('='); | ||||||
|  | 			if (eq>0 && eq<data.length()-1) { | ||||||
|  | 				String rfid_id = data.substring(0, eq); | ||||||
|  | 				String folder = data.substring(eq + 1); | ||||||
|  | 				TRACE("  Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str()); | ||||||
|  | 				_map[rfid_id] = folder; | ||||||
|  | 				 | ||||||
|  | 				if (folder.charAt(0)=='/') { | ||||||
|  | 					bool found=false; | ||||||
|  | 					for (String f: folders) { | ||||||
|  | 						if (f.equals(folder)) { | ||||||
|  | 							found = true; | ||||||
|  | 							break; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 					if (!found) { | ||||||
|  | 						INFO("WARNING: Found mapping for RFID id %s which maps to non-existing folder %s!\n", rfid_id.c_str(), folder.c_str()); | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		f.close(); | ||||||
|  | 	} | ||||||
|  | 	root.close(); | ||||||
|  |  | ||||||
|  | 	_unmapped_folders.clear(); | ||||||
|  | 	for (String folder: folders) { | ||||||
|  | 		bool found = false; | ||||||
|  | 		for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) { | ||||||
|  | 			if (it->second.equals(folder)) { | ||||||
|  | 				found = true; | ||||||
|  | 				break; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if (!found) { | ||||||
|  | 			// Checking folder for media files | ||||||
|  | 			File dir = SD.open(folder); | ||||||
|  | 			while (entry = dir.openNextFile()) { | ||||||
|  | 				String filename = entry.name(); | ||||||
|  | 				filename = filename.substring(folder.length() + 1); | ||||||
|  | 				if (!filename.startsWith(".") && filename.endsWith(".mp3")) { | ||||||
|  | 					found = true; | ||||||
|  | 				} | ||||||
|  | 				entry.close(); | ||||||
|  | 				if (found) break; | ||||||
|  | 			} | ||||||
|  | 			if (found) { | ||||||
|  | 				_unmapped_folders.push_back(folder); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	SPIMaster::select_sd(false); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void PlaylistManager::_check_for_special_chars(String s) { | ||||||
|  | 	for(int i=0; i<s.length(); i++) { | ||||||
|  | 		char c = s.charAt(i); | ||||||
|  | 		if (c < 0x20 || c >= 0x7F) { | ||||||
|  | 			ERROR("WARNING: Folder / file '%s' contains non-ascii chars!\n", s.c_str()); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Playlist* PlaylistManager::get_playlist_for_id(String id) { | ||||||
|  | 	if (!_map.count(id)) return NULL; | ||||||
|  | 	String folder = _map[id]; | ||||||
|  | 	return get_playlist_for_folder(folder); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Playlist* PlaylistManager::get_playlist_for_folder(String folder) { | ||||||
|  | 	Playlist* p; | ||||||
|  | 	if (!_playlists.count(folder)) { | ||||||
|  | 		p = new Playlist(folder); | ||||||
|  | 		_playlists[folder] = p; | ||||||
|  | 		if (p->persistence == PERSIST_PERMANENTLY) { | ||||||
|  | 			String search = folder; | ||||||
|  | 			search += "="; | ||||||
|  | 			SPIMaster::select_sd(); | ||||||
|  | 			if (SD.exists("/_positions.txt")) { | ||||||
|  | 				File f = SD.open("/_positions.txt", "r"); | ||||||
|  | 				while (true) { | ||||||
|  | 					String s = f.readStringUntil('\n'); | ||||||
|  | 					if (s.length()==0) break; | ||||||
|  | 					if (s.startsWith(search)) { | ||||||
|  | 						s = s.substring(search.length()); | ||||||
|  | 						int idx = s.indexOf(','); | ||||||
|  | 						String title_index = s.substring(0, idx); | ||||||
|  | 						uint32_t position = s.substring(idx+1).toInt(); | ||||||
|  | 						p->set_track_by_id(title_index); | ||||||
|  | 						p->set_position(position); | ||||||
|  | 						break; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 				f.close(); | ||||||
|  | 			} | ||||||
|  | 			SPIMaster::select_sd(false); | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		p = _playlists[folder]; | ||||||
|  | 		if (p->persistence == PERSIST_NONE) { | ||||||
|  | 			p->reset(); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return p; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void PlaylistManager::dump_ids() { | ||||||
|  | 	for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) { | ||||||
|  | 		INFO("  %s -> %s\n", it->first.c_str(), it->second.c_str()); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | String PlaylistManager::json() { | ||||||
|  | 	DynamicJsonDocument json(10240); | ||||||
|  | 	json["_type"] = "playlist_manager"; | ||||||
|  | 	JsonObject folders = json.createNestedObject("folders"); | ||||||
|  | 	for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) { | ||||||
|  | 		folders[it->second] = it->first; | ||||||
|  | 	} | ||||||
|  | 	JsonArray started = json.createNestedArray("started"); | ||||||
|  | 	for (std::map<String, Playlist*>::iterator it = _playlists.begin(); it!=_playlists.end(); it++) { | ||||||
|  | 		if (it->second->is_fresh()) continue; | ||||||
|  | 		started.add(it->first); | ||||||
|  | 	} | ||||||
|  | 	JsonArray unmapped = json.createNestedArray("unmapped"); | ||||||
|  | 	for(String folder : _unmapped_folders) { | ||||||
|  | 		unmapped.add(folder); | ||||||
|  | 	} | ||||||
|  | 	return json.as<String>(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool PlaylistManager::add_mapping(String id, String folder) { | ||||||
|  | 	DEBUG("Adding mapping: %s=>%s\n", id.c_str(), folder.c_str()); | ||||||
|  | 	_map[id] = folder; | ||||||
|  | 	_save_mapping(); | ||||||
|  | 	return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void PlaylistManager::_save_mapping() { | ||||||
|  | 	SPIMaster::select_sd(); | ||||||
|  | 	File f = SD.open("/_mapping.txt", "w"); | ||||||
|  | 	String s = create_mapping_txt(); | ||||||
|  | 	unsigned char* buf = new unsigned char[s.length()]; | ||||||
|  | 	s.getBytes(buf, s.length()); | ||||||
|  | 	f.write(buf, s.length()-1); | ||||||
|  | 	f.close(); | ||||||
|  | 	SPIMaster::select_sd(false); | ||||||
|  | 	delete buf; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | String PlaylistManager::create_mapping_txt() { | ||||||
|  | 	String s; | ||||||
|  | 	for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) { | ||||||
|  | 		s += it->first; | ||||||
|  | 		s += "="; | ||||||
|  | 		s += it->second; | ||||||
|  | 		s += '\n'; | ||||||
|  | 	} | ||||||
|  | 	return s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void PlaylistManager::persist(Playlist* p) { | ||||||
|  | 	if (p->persistence == PERSIST_NONE) { | ||||||
|  | 		_playlists.erase(p->path()); | ||||||
|  | 		return; | ||||||
|  | 	} else if (p->persistence == PERSIST_PERMANENTLY) { | ||||||
|  | 	 | ||||||
|  | 		String search = p->path(); | ||||||
|  | 		search += '='; | ||||||
|  | 		 | ||||||
|  | 		bool old_file_existed = false; | ||||||
|  | 		 | ||||||
|  | 		SPIMaster::select_sd(); | ||||||
|  | 		if (SD.exists("_positions.txt")) { | ||||||
|  | 			SD.rename("/_positions.txt", "/_positions.temp.txt"); | ||||||
|  | 			old_file_existed = true; | ||||||
|  | 		} | ||||||
|  | 		File dst = SD.open("/_positions.txt", "w"); | ||||||
|  | 		 | ||||||
|  | 		if (old_file_existed) { | ||||||
|  | 			File src = SD.open("/_positions.temp.txt", "r"); | ||||||
|  | 		 | ||||||
|  | 			while (true) { | ||||||
|  | 				String line = src.readStringUntil('\n'); | ||||||
|  | 				line.trim(); | ||||||
|  | 				if (line.startsWith(search)) continue; | ||||||
|  | 				dst.println(line); | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			src.close(); | ||||||
|  | 			SD.remove("/_positions.temp.txt"); | ||||||
|  | 		} | ||||||
|  | 		 | ||||||
|  | 		dst.print(search); | ||||||
|  | 		dst.print(p->get_current_track_id()); | ||||||
|  | 		dst.print(','); | ||||||
|  | 		dst.println(p->get_position()); | ||||||
|  | 		dst.close(); | ||||||
|  | 		SPIMaster::select_sd(false); | ||||||
|  |  | ||||||
|  | 		_playlists.erase(p->path()); | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										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; | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user