Compare commits

..

57 Commits

Author SHA1 Message Date
ab51af637e Merge branch 'feature/rewrite-i2s' 2022-08-25 14:53:43 +02:00
913a64d465 Die Position in einer Playlist beilbt beibehalten, solange die Papabox nicht neu gestartet wird. 2022-08-25 14:52:12 +02:00
b2cf9d6277 Systeminfos werden via mp3-Dateien in /system abgespielt. 2022-08-25 14:51:18 +02:00
076a6993c7 FTP-Server for managing the contents of the sd card. 2022-08-25 14:48:25 +02:00
13e62fea19 Shuffle the playlist before playing if "[random]" is in the rfid data. 2022-08-22 13:59:00 +02:00
15a65f7391 Added reaing of RFID contents. 2022-08-22 13:58:22 +02:00
b9df55012f Better handling of eof_mp3 events: Keep playing as long as there is data remaining. 2022-08-22 13:57:46 +02:00
fb6b5bced6 Test for button press every 50ms. That's more than enough. 2022-08-22 13:56:43 +02:00
3718f45983 Loglevel up to full debugging. ;-) 2022-08-22 13:55:57 +02:00
7dcb0cb673 Allocate a bigger (30KByte) Buffer for Audio. 2022-08-22 08:52:09 +02:00
6989248970 Less debugging messages. 2022-08-22 08:51:29 +02:00
cf433a48b2 Support for http(s) links in _mapping.txt 2022-08-22 08:51:19 +02:00
9a39b00a65 Lautstärke-Änderungen in 2er-Schritten. 2022-08-21 17:42:58 +02:00
aed9c416bf Standard-Lautstärke 12 statt 3. 2022-08-21 17:40:50 +02:00
2908d23e60 Alten Code gelöscht. 2022-08-21 17:40:32 +02:00
3272921db2 Daten von RFID-Karten (versuchen zu) lesen. 2022-08-21 17:40:22 +02:00
6ddf1efd62 Prevent playing prev_track if there are no tracks. 2022-08-21 15:27:00 +02:00
45dfe0cfe0 Don't read all dirs at startup. 2022-08-21 15:26:29 +02:00
4840c150c2 Button handling implemented. 2022-08-21 14:00:52 +02:00
9c31f70c57 It's working more or less... 2022-08-21 11:37:42 +02:00
978b25c34d Started rewrite for using an I2S amplifier with less unnecessary features. 2022-08-19 10:37:03 +02:00
dcca828197 Kleine Verbesserungen. 2022-08-18 16:38:07 +02:00
fa208858d9 Webinterface: Added an overlay to display when the websocket isn't connected. 2019-12-04 05:59:52 +01:00
6d452ecbc0 Added tracing stuff to RSSParser. 2019-12-04 05:58:25 +01:00
23fbddb055 TinyXML is broken, but I couldn't find an alternative nor fix it. So the code now is pretty hack-y to work around the bugs. 2019-12-04 05:57:58 +01:00
fe2a209e44 Debug and Trace modes can now be (de)activated via API commands and are persisted across reboots. 2019-11-30 13:53:50 +01:00
82905a8cdd Fixed M3U parser for last lines ending without a newline. 2019-11-30 13:38:34 +01:00
3751904cb4 Start with speakers off. 2019-11-30 13:37:35 +01:00
bcf7625285 deploy.sh: Fix calculation error. 2019-11-30 00:14:02 +01:00
4a3e79f02e Added empty (for now) update.manifest. 2019-11-30 00:12:02 +01:00
68e1073858 Deploy.sh: Don't show the commands being executed. 2019-11-30 00:09:36 +01:00
f73d45404f Merge branch 'develop' 2019-11-30 00:06:56 +01:00
ecc7c46b8d Playlist: Restrict RSS feeds to the newest 20 entries. Keep them in the correct order, starting with the last (newest) one. 2019-11-29 21:25:17 +01:00
0dd5937707 Player: Prevent overflows in vol_up() and vol_down(). 2019-11-29 21:24:41 +01:00
547080acf5 Main: Fix WiFi connection stuff for multiple WiFis. 2019-11-29 21:24:10 +01:00
d3c699aefa Webinterface: Typo fix. 2019-11-29 21:23:38 +01:00
a8d19cd6e1 HTTPClientWrapper: Initialize buffer variables. D'Oh. 2019-11-29 21:23:01 +01:00
38d48ab0e4 Controller: Only read sector 1 from RFID cards. That's big enough and much faster. 2019-11-29 21:22:35 +01:00
51bef05465 Fixed advent mode and persistence stuff. 2019-11-29 21:20:19 +01:00
4eef69516e Made TRACE a bit less talkative. 2019-11-29 21:18:28 +01:00
9175193b67 Disabled the custom partitioning stuff because the ESP32 entered a boot loop after flashing it. So we are back at ~85% memory used... 2019-11-29 21:16:10 +01:00
65118fbc42 Play position in stuff like podcasts can now be permanently persisted. 2019-11-29 17:41:16 +01:00
076f0e9dfd Changed the baud rate of the serial port to 115200 to match the ESP32 boot loader. 2019-11-29 06:14:45 +01:00
571e969bc4 deploy.sh: Show last tags. 2019-11-29 06:13:16 +01:00
8e15f87cd3 Moved index.html from SPIFFS to program memory. Removed dependencies to SPIFFS. Also, we can use a different partition scheme with much more program space instead of reserving some of it for SPIFFS. 2019-11-29 06:10:17 +01:00
dd9e1538c8 Playlist: More defensive proramming for when trying to play an empty playlist. 2019-11-29 05:52:00 +01:00
001e275131 Include information about versions and WiFi connection to controller's json. 2019-11-29 05:46:26 +01:00
196021bef5 Try to (re)connect to WiFi every 5 minutes. (Only when not playing at the moment.) 2019-11-29 05:45:21 +01:00
63b9616677 Removed "redefined macro" warnings from MFRC522 library. 2019-11-29 05:36:59 +01:00
d4c9a6d582 Documented API commands. 2019-11-29 05:36:22 +01:00
5fe66fdaef Added a command update to run the update check. 2019-11-29 05:35:47 +01:00
6445dc0fb8 Unly call Updater on startup if the SPIFFS needs an update. 2019-11-29 05:32:22 +01:00
7a20cf4b04 Better deploy script. 2019-11-29 05:29:01 +01:00
bbf77c6b1e Added deploy script. 2019-11-28 19:26:35 +01:00
b805d1b183 Updater: Better flow for performing updates; added MD5 validation. 2019-11-28 06:48:16 +01:00
07b1ea3a5c Added Updater to automatically perform OTA updates without user interaction. 2019-11-28 06:42:30 +01:00
3b0410f560 Merge branch 'feature-webstreams' 2019-11-28 06:30:16 +01:00
28 changed files with 679 additions and 3087 deletions

3
DEPLOY.md Normal file
View File

@ -0,0 +1,3 @@
# Deploying a new version
* Use `deploy.sh`.

151
README.md
View File

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

3
bin/update.manifest Normal file
View File

@ -0,0 +1,3 @@
VERSION=1
IMAGE_PATH=https://files.schle.nz/esmp3/firmware.bin
IMAGE_MD5=00000000000000000000000000000000

View File

@ -1,322 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>ESMP3</title>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<script src="https://kit.fontawesome.com/272149490a.js" crossorigin="anonymous"></script>
</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>&times;</span>
</button>
</div>
<h6>Open URL</h6>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="fa fa-link"></i>
</span>
</div>
<input type="text" class="form-control" id="input_url" />
<div class="input-group-append">
<button class="btn btn-primary" id="button_url_open">Go</button>
<button class="btn btn-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>&times;</span>
</button>
</div>
<div class="modal-body">
<h6>Last RFID id:</h6>
<span id="last_rfid_id"></span> <button class="btn btn-warning" id="button_add_mapping"><i class="fa fa-arrows-alt-h"></i></button>
<h6>Last RFID data:</h6>
<span id="last_rfid_data"></span>
<h6>Actions</h6>
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reset_vs1053">Reset VS1053 chip</button>
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reboot">Reboot ESMP3</button>
</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].substr(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_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>

53
deploy.sh Executable file
View 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

View File

@ -1,76 +0,0 @@
#pragma once
#include <Arduino.h>
#define SHOW_DEBUG
//#define SHOW_TRACE
#define FTP_DEBUG
#define DELAY_AFTER_DEBUG_AND_TRACE 0
#define WIFI_SSID "---CHANGEME---"
#define WIFI_PASS "---CHANGEME---"
#define VS1053_SLEEP_DELAY 5000
#define POSITION_SEND_INTERVAL 5000
#define DEBOUNCE_MILLIS 200
#define VOLUME_DEFAULT 230
#define VOLUME_MIN 190
#define VOLUME_MAX 255
#define VOLUME_STEP 0x08
#define RFID_SCAN_INTERVAL 100
#define NUM_BUTTONS 4
#define PIN_SD_CS(x) (digitalWrite(16, x))
#define PIN_SD_CS_SETUP() (pinMode(16, OUTPUT))
#define PIN_VS1053_XCS(x) (digitalWrite(4, x))
#define PIN_VS1053_XCS_SETUP() (pinMode(4, OUTPUT))
#define PIN_VS1053_XRESET(x) (digitalWrite(0, x))
#define PIN_VS1053_XRESET_SETUP() (pinMode(0, OUTPUT))
#define PIN_VS1053_XDCS(x) (digitalWrite(2, x))
#define PIN_VS1053_XDCS_SETUP() (pinMode(2, OUTPUT))
#define PIN_VS1053_DREQ() (digitalRead(15))
#define PIN_VS1053_DREQ_SETUP() (pinMode(15, INPUT))
#define PIN_RC522_CS(x) (digitalWrite(17, x))
#define PIN_RC522_CS_SETUP() (pinMode(17, OUTPUT))
#define PIN_SPEAKER_L(x) (digitalWrite(27, x))
#define PIN_SPEAKER_L_SETUP() (pinMode(27, OUTPUT))
#define PIN_SPEAKER_R(x) (digitalWrite(26, x))
#define PIN_SPEAKER_R_SETUP() (pinMode(26, OUTPUT))
#define BTN_PREV() ( ! digitalRead(22))
#define BTN_PREV_SETUP() (pinMode(22, INPUT_PULLUP))
#define BTN_VOL_UP() ( ! digitalRead(21))
#define BTN_VOL_UP_SETUP() (pinMode(21, INPUT_PULLUP))
#define BTN_VOL_DOWN() ( ! digitalRead(32))
#define BTN_VOL_DOWN_SETUP() (pinMode(32, INPUT_PULLUP))
#define BTN_NEXT() ( ! digitalRead(33))
#define BTN_NEXT_SETUP() (pinMode(33, INPUT_PULLUP))
// Other definitions
#define INFO(x, ...) Serial.printf(x, ##__VA_ARGS__)
#define ERROR(x, ...) Serial.printf(x, ##__VA_ARGS__)
#ifdef SHOW_DEBUG
#define DEBUG(x, ...) {Serial.printf(x, ##__VA_ARGS__); delay(DELAY_AFTER_DEBUG_AND_TRACE);}
#else
#define DEBUG(x, ...) while(0) {}
#endif
#ifdef SHOW_TRACE
#define TRACE(x, ...) {Serial.printf(x, ##__VA_ARGS__); delay(DELAY_AFTER_DEBUG_AND_TRACE);}
#else
#define TRACE(x, ...) while(0) {}
#endif

View File

@ -1,57 +1,32 @@
#pragma once
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include "config.h"
class Controller;
#include "player.h"
#include <MFRC522v2.h>
#include <MFRC522Debug.h>
#include "esmp3.h"
#include "playlist.h"
#include "playlist_manager.h"
#include "http_server.h"
#include <MFRC522.h>
enum ControllerState { NORMAL, LOCKING, LOCKED };
class Controller {
private:
MFRC522* _rfid;
HTTPServer* _http_server;
ControllerState _state = NORMAL;
bool _rfid_enabled = true;
void _check_rfid();
void _check_serial();
void _check_buttons();
bool _debounce_button(uint8_t index);
uint32_t _get_rfid_card_uid();
String _read_rfid_data();
bool _rfid_present = false;
String _last_rfid_uid = "";
String _last_rfid_data = "";
void handle_buttons();
void handle_rfid();
bool is_button_pressed(uint8_t pin);
Playlist current_playlist;
bool is_rfid_present = false;
unsigned long last_rfid_check = 0;
unsigned long last_button_check = 0;
unsigned long last_position_save = 0;
uint8_t button_pressed = 0;
unsigned long button_pressed_since = 0;
bool button_already_processed = false;
String read_rfid_data();
unsigned long _last_rfid_scan_at = 0;
unsigned long _last_position_info_at = 0;
String _serial_buffer = String();
String _cmd_queue = "";
void _execute_command_ls(String path);
void _execute_command_ids();
void _execute_command_help();
unsigned long _button_last_pressed_at[NUM_BUTTONS];
bool _check_button(uint8_t btn);
public:
Controller(Player* p, PlaylistManager* pm);
PlaylistManager* pm;
Player* player;
void register_http_server(HTTPServer* h);
void loop();
void send_controller_status();
void send_player_status();
void send_playlist_manager_status();
void send_position();
void inform_new_client(AsyncWebSocketClient* client);
String json();
bool process_message(String m);
void queue_command(String cmd);
void update_playlist_manager();
void handle();
void next_track();
void prev_track();
void play();
void play(String rfid_id, bool shuffle=false);
void stop();
void eof_mp3(String info);
};

View File

@ -1,54 +0,0 @@
#pragma once
#include <Arduino.h>
#include <SD.h>
#include "config.h"
#include "http_client_wrapper.h"
class DataSource {
private:
public:
DataSource() {};
virtual ~DataSource() {};
virtual size_t read(uint8_t* buf, size_t len) = 0;
virtual int read() = 0;
virtual size_t position() = 0;
virtual void seek(size_t position) = 0;
virtual size_t size() = 0;
virtual void close() = 0;
virtual void skip_id3_tag() {};
virtual bool usable() = 0;
};
class SDDataSource : public DataSource {
private:
File _file;
public:
SDDataSource(String file);
~SDDataSource();
size_t read(uint8_t* buf, size_t len);
int read();
size_t position();
void seek(size_t position);
size_t size();
void close();
void skip_id3_tag();
bool usable();
};
class HTTPSDataSource : public DataSource {
private:
WiFiClient* _stream = NULL;
HTTPClientWrapper* _http = NULL;
uint32_t _position;
public:
HTTPSDataSource(String url, uint32_t offset=0);
~HTTPSDataSource();
size_t read(uint8_t* buf, size_t len);
int read();
size_t position();
void seek(size_t position);
size_t size();
void close();
bool usable();
};

26
include/esmp3.h Normal file
View File

@ -0,0 +1,26 @@
#pragma once
#include "controller.h"
#include "playlist_manager.h"
#include <Audio.h>
#define PIN_CS_SD 22
#define PIN_CS_RFID 21
#define PIN_BTN_VOL_UP 32
#define PIN_BTN_VOL_DOWN 33
#define PIN_BTN_TRACK_NEXT 17
#define PIN_BTN_TRACK_PREV 16
#define I2S_DOUT 25
#define I2S_BCLK 26
#define I2S_LRC 27
class Controller;
extern Controller controller;
extern Audio audio;
extern PlaylistManager* pm;
extern MFRC522* rfid;
void save_audio_current_time();

View File

@ -1,37 +0,0 @@
#pragma once
#include <HTTPClient.h>
#include "config.h"
class HTTPClientWrapper {
private:
HTTPClient* _http;
uint8_t* _buffer;
uint16_t _buffer_size;
uint16_t _buffer_length;
uint16_t _buffer_position;
uint32_t _chunk_length;
bool _connected = false;
String _content_type;
uint32_t _length;
bool _request(String method, String url, uint32_t offset=0, uint8_t redirection_count=0);
WiFiClient* _stream;
bool _is_chunked;
void _read_next_chunk_header(bool first);
uint16_t _fill_buffer();
public:
HTTPClientWrapper();
~HTTPClientWrapper();
bool get(String url, uint32_t offset=0, uint8_t redirection_count=0);
bool head(String url, uint32_t offset=0, uint8_t redirection_count=0);
String getContentType();
String getString();
int read();
uint32_t read(uint8_t* dst, uint32_t len);
void close();
uint32_t getSize();
String readUntil(String sep);
String readLine();
};

View File

@ -1,29 +0,0 @@
#pragma once
class HTTPServer;
#include "player.h"
#include "controller.h"
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
class HTTPServer {
private:
AsyncWebServer* _server;
Player* _player;
Controller* _controller;
void _handle_upload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
uint16_t _chunk_length;
uint8_t* _chunk;
File _upload_file;
uint32_t _file_size;
uint32_t _file_size_done;
bool _need_header;
uint32_t _upload_position;
void _onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len);
void _handle_index(AsyncWebServerRequest* req);
public:
HTTPServer(Player* p, Controller* c);
AsyncWebSocket* ws;
};

View File

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

View File

@ -1,104 +0,0 @@
#pragma once
#include "config.h"
#include <SPI.h>
#include <SD.h>
#include "spi_master.h"
#include "playlist.h"
#include "data_sources.h"
class Player;
#include "controller.h"
#define SCI_MODE 0x00
#define SCI_STATUS 0x01
#define SCI_BASS 0x02
#define SCI_CLOCKF 0x03
#define SCI_DECODE_TIME 0x04
#define SCI_AUDATA 0x05
#define SCI_VOL 0x0B
#define SCI_WRAMADDR 0x07
#define SCI_WRAM 0x06
#define SCI_HDAT0 0x08
#define SCI_HDAT1 0x09
#define SCI_AIADDR 0x0A
#define SCI_AICTRL0 0x0C
#define SCI_AICTRL1 0x0D
#define SCI_AICTRL2 0x0E
#define SCI_AICTRL3 0x0F
#define CMD_WRITE 0x02
#define CMD_READ 0x03
#define ADDR_ENDBYTE 0x1E06
#define SM_LAYER12 0x0001
#define SM_RESET 0x0004
#define SM_CANCEL 0x0008
#define SM_SDINEW 0x0800
#define SM_ADPCM 0x1000
#define SS_DO_NOT_JUMP 0x8000
class Player {
private:
enum state { uninitialized, idle, playing, stopping,
sleeping, recording };
void _reset();
void _wait();
uint16_t _read_control_register(uint8_t address, bool do_wait=true);
void _write_control_register(uint8_t address, uint16_t value, bool do_wait=true);
void _write_direct(uint8_t address, uint16_t value);
void _write_data(uint8_t* data);
uint16_t _read_wram(uint16_t address);
state _state = state::uninitialized;
void _refill();
bool _refill_needed();
void _flush_and_cancel();
int8_t _get_endbyte();
void _flush(uint count, int8_t fill_byte);
void _play_file(String filename, uint32_t offset);
void _finish_playing();
void _finish_stopping(bool turn_speaker_off);
void _mute();
void _unmute();
void _sleep();
void _wakeup();
void _record();
void _patch_adpcm();
void _speaker_off();
void _speaker_on();
SPISettings _spi_settings_slow = SPISettings(250000, MSBFIRST, SPI_MODE0);
SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0);
SPISettings* _spi_settings = &_spi_settings_slow;
DataSource* _file;
uint32_t _file_size = 0;
uint8_t _buffer[32];
uint32_t _current_play_position = 0;
Playlist* _current_playlist = NULL;
uint _refills;
uint8_t _volume;
uint16_t _stop_delay;
uint32_t _skip_to;
SPIMaster* _spi;
Controller* _controller;
unsigned long _stopped_at;
public:
Player(SPIMaster* s);
void init();
void register_controller(Controller* c);
void vol_up();
void vol_down();
void track_next();
void track_prev();
void set_track(uint8_t track);
bool is_playing();
bool play();
bool play(Playlist* p);
void stop(bool turn_speaker_off=true);
bool loop();
void set_volume(uint8_t vol, bool save = true);
String position_json();
String json();
};

View File

@ -1,47 +1,30 @@
#pragma once
#include <Arduino.h>
#include <vector>
#include <ArduinoJson.h>
#include "http_client_wrapper.h"
struct PlaylistEntry {
String filename;
String title;
bool operator<(PlaylistEntry p) { return title < p.title; }
};
#include <Arduino.h>
#include "persisted_playlist.h"
class Playlist {
private:
uint32_t _position = 0;
uint32_t _current_track = 0;
bool _started = false;
bool _shuffled = false;
std::vector<PlaylistEntry> _files;
String _title = "";
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);
std::vector<String> files;
uint8_t current_file = 0;
uint32_t current_time = 0;
String rfid_id;
PersistedPlaylist* pp;
public:
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 reset();
bool is_empty();
String get_current_file();
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);
Playlist();
Playlist(String rfid_id, PersistedPlaylist* p);
void add_file(String filename);
void sort();
String get_rfid_id();
String get_current_file_name();
bool next_track();
bool prev_track();
void restart();
void set_current_time(uint32_t time);
uint32_t get_current_time();
void shuffle();
void set_current_position(uint8_t file, uint32_t position=0);
void save_current_position(uint32_t position=0);
};

View File

@ -1,23 +1,25 @@
#pragma once
#include <map>
#include <vector>
#include <map>
#include <Arduino.h>
#include "playlist.h"
#include "persisted_playlist.h"
class Playlist;
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();
Playlist get_playlist_for_tag_id(String id);
String current_rfid_tag_id;
uint32_t audio_current_time = 0;
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();
std::map<String, PersistedPlaylist> map;
Playlist get_playlist(String rfid_id);
bool has_playlist(String rfid_id);
Playlist current_playlist;
void set_audio_current_time(uint32_t time);
String pp_to_String();
};

View File

@ -1,70 +1,9 @@
#pragma once
#include <Arduino.h>
#include <SPI.h>
#include "config.h"
class SPIMaster {
public:
static uint8_t state;
static void init() {
PIN_SD_CS_SETUP();
PIN_VS1053_XCS_SETUP();
PIN_VS1053_XDCS_SETUP();
PIN_RC522_CS_SETUP();
disable();
}
static void select_sd(bool enabled=true) {
PIN_SD_CS(enabled ? LOW : HIGH);
if (enabled) {
state |= 1;
} else {
state &= ~1;
}
}
static void select_vs1053_xcs(bool enabled=true) {
PIN_VS1053_XCS(enabled ? LOW : HIGH);
if (enabled) {
state |= 2;
} else {
state &= ~2;
}
}
static void select_vs1053_xdcs(bool enabled=true) {
PIN_VS1053_XDCS(enabled ? LOW : HIGH);
if (enabled) {
state |= 4;
} else {
state &= ~4;
}
}
static void select_rc522(bool enabled=true) {
PIN_RC522_CS(enabled ? LOW : HIGH);
if (enabled) {
state |= 8;
} else {
state &= ~8;
}
}
static void set_state(uint8_t s) {
disable();
if (s & 1) select_sd();
if (s & 2) select_vs1053_xcs();
if (s & 4) select_vs1053_xdcs();
if (s & 8) select_rc522();
}
static void disable() {
PIN_SD_CS(HIGH);
PIN_VS1053_XCS(HIGH);
PIN_VS1053_XDCS(HIGH);
PIN_RC522_CS(HIGH);
state = 0;
}
static void enable_sd();
static void enable_rfid();
static void disable_all();
static void initialize();
};

6
partitions.csv Normal file
View 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,
1 # Custom partition table without SPIFFS.
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x5000,
4 otadata, data, ota, 0xe000, 0x2000,
5 app0, app, ota_0, 0x10000, 0x220000,
6 app1, app, ota_1, 0x230000,0x220000,

View File

@ -8,16 +8,35 @@
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = esp32
[extra]
lib_deps =
[env:esp32]
platform = espressif32
board = esp-wrover-kit
framework = arduino
upload_speed = 512000
build_flags=!./build_version.sh
lib_deps = MFRC522
https://github.com/me-no-dev/ESPAsyncWebServer.git
ArduinoJSON
6691 ; TinyXML
upload_port = /dev/cu.SLAB_USBtoUART
monitor_speed = 74480
;monitor_port = /dev/cu.wchusbserial1420
upload_speed = 921600
build_flags = -DCORE_DEBUG_LEVEL=5 -DCONFIG_ARDUHAL_LOG_COLORS=1 ; !./build_version.sh
lib_deps =
${extra.lib_deps}
esphome/ESP32-audioI2S@^2.1.0
computer991/Arduino_MFRC522v2@^2.0.1
https://github.com/dplasa/FTPClientServer
;upload_port = 10.10.2.108
monitor_speed = 115200
monitor_port = /dev/cu.usbserial-0001
monitor_filters = esp32_exception_decoder
[env:deploy]
platform = espressif32
board = esp-wrover-kit
framework = arduino
lib_deps =
${extra.lib_deps}
esphome/ESP32-audioI2S@^2.1.0
computer991/Arduino_MFRC522v2@^2.0.1
board_build.embed_txtfiles = src/index.html
board_build.partitions = partitions.csv

View File

@ -1,97 +1,67 @@
#include "controller.h"
#include "spi_master.h"
#include "config.h"
#include "playlist.h"
#include "http_server.h"
#include <ArduinoJson.h>
#include "esmp3.h"
Controller::Controller(Player* p, PlaylistManager* playlist_manager) {
player = p;
pm = playlist_manager;
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
player->register_controller(this);
BTN_NEXT_SETUP();
BTN_PREV_SETUP();
BTN_VOL_UP_SETUP();
BTN_VOL_DOWN_SETUP();
SPIMaster::select_rc522();
DEBUG("Initializing RC522...\n");
_rfid->PCD_Init();
#ifdef SHOW_DEBUG
_rfid->PCD_DumpVersionToSerial();
#endif
SPIMaster::select_rc522(false);
INFO("RC522 initialized.\n");
for (uint8_t i=0; i<NUM_BUTTONS; i++) _button_last_pressed_at[i]=0;
void Controller::handle() {
if (last_rfid_check + 500 < millis() || last_rfid_check > millis()) {
handle_rfid();
last_rfid_check = millis();
}
if (last_button_check + 10 < millis() || last_button_check > millis()) {
handle_buttons();
last_button_check = millis();
}
if (last_position_save + 10000 < millis() || last_position_save > millis()) {
current_playlist.save_current_position(audio.getFilePos());
last_position_save = millis();
//Serial.println(pm->pp_to_String().c_str());
}
}
void Controller::register_http_server(HTTPServer* h) {
_http_server = h;
void Controller::handle_buttons() {
if (is_button_pressed(PIN_BTN_VOL_UP)) {
log_i("BTN_VOL_UP pressed");
uint8_t vol = min(audio.getVolume()+2, 21);
log_d("Setting new volume %d", vol);
audio.setVolume(vol);
} else if (is_button_pressed(PIN_BTN_VOL_DOWN)) {
log_i("BTN_VOL_DOWN pressed");
uint8_t vol;
if ((vol = audio.getVolume()) >= 3) {
vol -= 2;
} else {
vol = 1;
}
log_d("Setting new volume %d", vol);
audio.setVolume(vol);
} else if (is_button_pressed(PIN_BTN_TRACK_NEXT)) {
log_i("BTN_TRACK_NEXT pressed");
next_track();
} else if (is_button_pressed(PIN_BTN_TRACK_PREV)) {
log_i("BTN_TRACK_PREV pressed");
prev_track();
}
}
void Controller::loop() {
TRACE("Controller::loop()...\n");
unsigned long now = millis();
if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) {
_check_rfid();
_last_rfid_scan_at = now;
}
if ((_last_position_info_at < now - POSITION_SEND_INTERVAL) || (now < _last_position_info_at)) {
send_position();
_last_position_info_at = now;
}
_check_serial();
_check_buttons();
if (_cmd_queue.length() > 0) {
process_message(_cmd_queue);
_cmd_queue = "";
}
TRACE("Controller::loop() done.\n");
}
uint32_t Controller::_get_rfid_card_uid() {
SPIMaster::select_rc522();
if (!_rfid->PICC_ReadCardSerial()) {
if (!_rfid->PICC_IsNewCardPresent()) {
return 0;
}
if (!_rfid->PICC_ReadCardSerial()) {
return 0;
}
}
SPIMaster::select_rc522(false);
uint32_t uid = _rfid->uid.uidByte[0]<<24 | _rfid->uid.uidByte[1]<<16 | _rfid->uid.uidByte[2]<<8 | _rfid->uid.uidByte[3];
return uid;
}
void Controller::_check_rfid() {
TRACE("check_rfid running...\n");
MFRC522::StatusCode status;
if (_rfid_present) {
void Controller::handle_rfid() {
if (is_rfid_present) {
byte buffer[2];
byte buffer_size = 2;
SPIMaster::select_rc522();
status = _rfid->PICC_WakeupA(buffer, &buffer_size);
if (status == MFRC522::STATUS_OK) {
MFRC522Constants::StatusCode status = rfid->PICC_WakeupA(buffer, &buffer_size);
if (status == MFRC522Constants::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();
rfid->PICC_HaltA();
} else {
uint32_t uid = _get_rfid_card_uid();
Serial.printf("RFID status is %s\n", MFRC522Debug::GetStatusCodeName(status));
is_rfid_present = false;
Serial.println("No more RFID card.\n");
stop();
}
} else {
if (rfid->PICC_IsNewCardPresent()) {
if (rfid->PICC_ReadCardSerial()) {
uint32_t uid = rfid->uid.uidByte[0]<<24 | rfid->uid.uidByte[1]<<16 | rfid->uid.uidByte[2]<<8 | rfid->uid.uidByte[3];
Serial.printf("Found new rfid card with uid %x\n", uid);
is_rfid_present = true;
if (uid > 0) {
String temp = String(uid, HEX);
String s_uid = "";
@ -99,321 +69,158 @@ void Controller::_check_rfid() {
s_uid.concat("0");
}
s_uid.concat(temp);
INFO("New RFID card uid: %s\n", s_uid.c_str());
_last_rfid_uid = s_uid;
_rfid_present = true;
String data = _read_rfid_data();
_last_rfid_data = data;
String data = read_rfid_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;
play(s_uid, data.indexOf("[random]")>=0);
}
rfid->PICC_HaltA();
}
if (pl==NULL) {
INFO("Could not find album for id '%s'.\n", s_uid.c_str());
send_controller_status();
return;
}
int index;
if (data.indexOf("[advent]") != -1 && pl->is_fresh()) {
struct tm time;
getLocalTime(&time);
if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december.
pl->advent_shuffle(time.tm_mday);
} else {
// TODO
}
} 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());
}
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;
String Controller::read_rfid_data() {
log_v("read_rfid_data() running...");
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;
MFRC522::PICC_Type type = rfid->PICC_GetType(rfid->uid.sak);
uint16_t pageStart = 0;
uint16_t pages = 4;
uint16_t pageSize = 1;
switch(type) {
case MFRC522Constants::PICC_TYPE_MIFARE_MINI:
case MFRC522Constants::PICC_TYPE_MIFARE_1K:
case MFRC522Constants::PICC_TYPE_MIFARE_4K: {
log_v("Trying to authenticate Mifare card.");
MFRC522::MIFARE_Key key = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
status = rfid->PCD_Authenticate(MFRC522Constants::PICC_CMD_MF_AUTH_KEY_A, 4, &key, &rfid->uid);
if (status == MFRC522Constants::STATUS_OK) {
log_v("Authentication succeeded.");
} else {
log_v("Authentication failed. Trying to read anyway.");
}
pageStart = 4;
break;
}
case MFRC522Constants::PICC_TYPE_MIFARE_UL:
log_v("PICC type is Mifare Ultralight. No authentication necessary.");
pages = 16;
pageSize = 4;
break;
default:
log_v("Unexpected rfid card type %s. Trying to read anyway.", MFRC522Debug::PICC_GetTypeName(type));
}
if (good_key_index == -1) {
TRACE("Could not find a valid MIFARE key.\n");
} else {
for (uint8_t block=0; block<blocks-1; block++) {
String data = "";
for (uint8_t block=pageStart; block<pages+pageStart; block+=pageSize) {
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());
status = rfid->MIFARE_Read(block, buffer, &byte_count);
if (status != MFRC522Constants::STATUS_OK) {
log_d("MIFARE_Read() failed: %s\n", String(MFRC522Debug::GetStatusCodeName(status)).c_str());
continue;
}
for (int i=0; i<16; i++) {
if (buffer[i]>=0x20 && buffer[i]<0x7F) data.concat((char)buffer[i]);
}
}
}
}
_rfid->PICC_HaltA();
_rfid->PCD_StopCrypto1();
DEBUG("Data from RFID: %s\n", data.c_str());
SPIMaster::select_rc522(false);
rfid->PCD_StopCrypto1();
log_v("Read rfid data: '%s'", data.c_str());
return data;
}
void Controller::_check_serial() {
TRACE("check_serial running...\n");
if (Serial.available() > 0) {
char c = Serial.read();
Serial.printf("%c", c);
if (c==10 || c==13) {
if (_serial_buffer.length()>0) {
process_message(_serial_buffer);
_serial_buffer = String();
void Controller::play(String rfid_id, bool shuffle) {
if (!rfid_id.equals(current_playlist.get_rfid_id())) {
if (pm->has_playlist(rfid_id)) {
current_playlist = pm->get_playlist(rfid_id);
if (shuffle) {
log_i("Shuffling the playlist.");
current_playlist.shuffle();
}
play();
} else {
Serial.printf("There is no playlist for rfid_id %s\n", rfid_id.c_str());
// This is working more or less, but downloading files is really, REALLY slow. (About 4 minutes for 10 MBytes).
//download_album(rfid_id);
audio.connecttoFS(SD, "/system/sys_unknown_card.mp3");
}
} else {
_serial_buffer.concat(c);
if (!audio.isRunning()) {
play();
}
}
}
bool Controller::process_message(String cmd) {
DEBUG("Executing command: %s\n", cmd.c_str());
void Controller::play() {
String file = current_playlist.get_current_file_name();
if (cmd.startsWith("play ")) {
Playlist* p = pm->get_playlist_for_folder(cmd.substring(5));
player->play(p);
//} else if (cmd.equals("ls")) {
// _execute_command_ls("/");
//} else if (cmd.startsWith("ls ")) {
// _execute_command_ls(cmd.substring(3));
} else if (cmd.equals("play")) {
player->play();
if (file.startsWith("/")) {
log_i("Playing file %s via connecttoFS", file.c_str());
audio.connecttoFS(SD, file.c_str(), current_playlist.get_current_time());
} else if (file.startsWith("http")) {
log_i("Playing URL %s via connecttohost", file.c_str());
audio.connecttoFS(SD, "/system/sys_connecting.mp3");
while (audio.isRunning()) {
yield();
audio.loop();
}
audio.connecttohost(file.c_str());
}
}
} else if (cmd.equals("stop")) {
player->stop();
} else if (cmd.equals("help")) {
_execute_command_help();
} else if (cmd.equals("-")) {
player->vol_down();
} else if (cmd.equals("+")) {
player->vol_up();
} else if (cmd.startsWith("volume=")) {
uint8_t vol = cmd.substring(7).toInt();
player->set_volume(vol);
} else if (cmd.equals("track_prev")) {
player->track_prev();
} else if (cmd.equals("track_next")) {
player->track_next();
} else if (cmd.startsWith("track=")) {
uint8_t track = cmd.substring(6).toInt();
player->set_track(track);
} else if (cmd.equals("ids")) {
pm->dump_ids();
} else if (cmd.equals("reset_vs1053")) {
player->stop();
player->init();
} else if (cmd.equals("reboot")) {
ESP.restart();
} else if (cmd.startsWith("add_mapping=")) {
String rest = cmd.substring(12);
uint8_t idx = rest.indexOf('=');
String id = rest.substring(0, idx);
String folder = rest.substring(idx + 1);
pm->add_mapping(id, folder);
send_playlist_manager_status();
void Controller::next_track() {
if (current_playlist.next_track()) {
play();
}
}
void Controller::prev_track() {
uint32_t time = audio.getAudioCurrentTime();
log_d("prev_track() called. getAudioCurrentTime() returns %d", time);
if (time >= 5) {
log_d("Restarting current track.");
current_playlist.restart();
play();
} else {
ERROR("Unknown command: %s\n", cmd.c_str());
return false;
if (current_playlist.prev_track()) {
play();
}
}
}
void Controller::stop() {
if (audio.isRunning()) {
current_playlist.set_current_time(audio.stopSong());
}
}
bool Controller::is_button_pressed(uint8_t pin) {
//log_d("Button %d state is %d", pin, digitalRead(pin));
if (!digitalRead(pin)) {
// Button is pressed - let's debounce it.
if (button_pressed == pin) {
if (button_pressed_since + 150 < millis() && !button_already_processed) {
button_already_processed = true;
return true;
}
void Controller::_execute_command_ls(String path) {
INFO("Listing contents of %s:\n", path.c_str());
// TODO
//std::list<String> files = player->ls(path);
//for(std::list<String>::iterator it=files.begin(); it!=files.end(); ++it) {
// INFO(" %s\n", (*it).c_str());
//}
}
void Controller::_execute_command_help() {
INFO("Valid commands are:");
INFO(" help - Displays this help\n");
//INFO(" ls [dir] - Lists the contents of [dir] or, if not given, of /\n");
INFO(" ids - Lists all known ID-to-folder mappings\n");
INFO(" play [id] - Plays the album with the given id\n");
INFO(" stop - Stops playback\n");
INFO(" - / + - Decrease or increase the volume\n");
INFO(" p / n - Previous or next track\n");
}
void Controller::_check_buttons() {
TRACE("check_buttons running...\n");
if (BTN_PREV() && _debounce_button(0)) {
if (_state == NORMAL) {
player->track_prev();
} else {
DEBUG("Ignoring btn_prev because state is LOCKED.\n");
button_pressed = pin;
button_pressed_since = millis();
button_already_processed = false;
}
} else if (BTN_VOL_UP() && _debounce_button(1)) {
player->vol_up();
} else if (BTN_VOL_DOWN() && _debounce_button(2)) {
player->vol_down();
} else if (BTN_NEXT() && _debounce_button(3)) {
if (_state == NORMAL) {
player->track_next();
} else {
DEBUG("Ignoring btn_next because state is LOCKED.\n");
if (button_pressed == pin) {
button_pressed = 0;
}
}
return false;
}
bool Controller::_debounce_button(uint8_t index) {
bool ret = false;
if (_button_last_pressed_at[index] + DEBOUNCE_MILLIS < millis()) {
DEBUG("Button %d pressed.\n", index);
ret = true;
}
_button_last_pressed_at[index] = millis();
return ret;
}
String Controller::json() {
DynamicJsonDocument json(1024);
json["_type"] = "controller";
switch(_state) {
case LOCKED: json["state"] = "locked"; break;
case LOCKING: json["state"] = "locking"; break;
case NORMAL: json["state"] = "normal"; break;
}
json["is_rfid_present"] = _rfid_present;
JsonObject rfid = json.createNestedObject("last_rfid");
rfid["uid"] = _last_rfid_uid;
rfid["data"] = _last_rfid_data;
json["uptime"] = millis() / 1000;
json["free_heap"] = ESP.getFreeHeap();
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::eof_mp3(String info) {
log_d("Handling eof. Keep playing until the file is finished.");
while(audio.isRunning()) { audio.loop(); yield; }
if (info.startsWith("sys_")) {
log_d("File ending was a system audio file. Not running next_track.");
} else {
next_track();
}
}
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();
}

View File

@ -1,57 +0,0 @@
#include "data_sources.h"
////////////// SDDataSource //////////////
SDDataSource::SDDataSource(String file) { _file = SD.open(file, "r"); }
SDDataSource::~SDDataSource() { if (_file) _file.close(); }
size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); }
int SDDataSource::read() { return _file.read(); }
size_t SDDataSource::position() { return _file.position(); }
void SDDataSource::seek(size_t position) { _file.seek(position); }
size_t SDDataSource::size() { return _file.size(); }
void SDDataSource::close() { _file.close(); }
bool SDDataSource::usable() { return _file; }
void SDDataSource::skip_id3_tag() {
uint32_t original_position = _file.position();
uint32_t offset = 0;
if (_file.read()=='I' && _file.read()=='D' && _file.read()=='3') {
DEBUG("ID3 tag found\n");
// Skip ID3 tag version
_file.read(); _file.read();
byte tags = _file.read();
bool footer_present = tags & 0x10;
DEBUG("ID3 footer found: %d\n", footer_present);
for (byte i=0; i<4; i++) {
offset <<= 7;
offset |= (0x7F & _file.read());
}
offset += 10;
if (footer_present) offset += 10;
DEBUG("ID3 tag length is %d bytes.\n", offset);
_file.seek(offset);
} else {
DEBUG("No ID3 tag found\n");
_file.seek(original_position);
}
}
////////////// HTTPSDataSource //////////////
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
_http = new HTTPClientWrapper();
if (!_http->get(url, offset)) return;
_position = 0;
}
HTTPSDataSource::~HTTPSDataSource() {
_http->close();
delete _http;
}
bool HTTPSDataSource::usable() { return _http; }
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; }
int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; }
size_t HTTPSDataSource::position() { return _position; }
void HTTPSDataSource::seek(size_t position) { return; /* TODO */ }
size_t HTTPSDataSource::size() { return _http->getSize(); }
void HTTPSDataSource::close() { _http->close(); }

139
src/esmp3.cpp Normal file
View File

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

View File

@ -1,210 +0,0 @@
#include "http_client_wrapper.h"
#include <WiFiClientSecure.h>
HTTPClientWrapper::HTTPClientWrapper() {
_buffer = new uint8_t[512];
_buffer_size = 512;
}
HTTPClientWrapper::~HTTPClientWrapper() {
if (_http) {
_http->end();
delete _http;
}
delete _buffer;
}
void HTTPClientWrapper::close() {
_http->end();
_connected = false;
}
bool HTTPClientWrapper::get(String url, uint32_t offset, uint8_t redirection_count) { return _request("GET", url, offset, redirection_count); }
bool HTTPClientWrapper::head(String url, uint32_t offset, uint8_t redirection_count) { return _request("HEAD", url, offset, redirection_count); }
bool HTTPClientWrapper::_request(String method, String url, uint32_t offset, uint8_t redirection_count) {
if (redirection_count>=5) return false;
//if (_http) delete _http;
DEBUG("%s %s\n", method.c_str(), url.c_str());
_http = new HTTPClient();
_http->setUserAgent("PodBox/0.1");
if (offset) {
String s = "bytes=";
s += offset;
s += "-";
_http->addHeader("Range: ", s);
}
const char* headers[] = {"Location", "Content-Type", "Transfer-Encoding"};
_http->collectHeaders(headers, 3);
bool result;
/*if (url.startsWith("https:")) {
BearSSL::WiFiClientSecure* client = new BearSSL::WiFiClientSecure();
client->setInsecure();
result = _http->begin(*client, url);
} else {
result = _http->begin(url);
}*/
result = _http->begin(url);
TRACE("HTTP->begin result: %d\n", result);
if (!result) return false;
int status = _http->sendRequest(method.c_str());
TRACE("HTTP Status code: %d\n", status);
if (status == HTTP_CODE_FOUND || status==HTTP_CODE_MOVED_PERMANENTLY || status==HTTP_CODE_TEMPORARY_REDIRECT) {
if (_http->hasHeader("Location")) {
url = _http->header("Location");
_http->end();
delete _http;
_http = NULL;
return _request(method, url, offset, redirection_count+1);
} else {
ERROR("Got redirection HTTP code, but no Location header.\n");
delete _http;
_http = NULL;
return false;
}
} else if (status != HTTP_CODE_OK) {
DEBUG("Unexpected HTTP return code %d. Cancelling.\n", status);
return false;
}
_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");
}

View File

@ -1,144 +0,0 @@
#include "http_server.h"
#include "spi_master.h"
#include <ESPmDNS.h>
#include <SPIFFS.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(SPIFFS, "/index.html", "text/html");});
_server->on("/upload", HTTP_POST, [](AsyncWebServerRequest* req) {req->send(200); }, ([&](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final){this->_handle_upload(request, filename, index, data, len, final);}));
_server->on("/_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) {req->send(200); }, NULL, [&](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {_controller->queue_command((char*)data);});
_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);
}
}
}

View File

@ -1,130 +0,0 @@
#include <Arduino.h>
#include <SPI.h>
#include <SD.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <SPIFFS.h>
#include "config.h"
#include "controller.h"
#include "player.h"
#include "spi_master.h"
#include "http_server.h"
#include "playlist_manager.h"
Controller* controller;
Player* player;
PlaylistManager* pm;
HTTPServer* http_server;
uint8_t SPIMaster::state = 0;
bool connect_to_wifi(String ssid, String pass) {
TRACE("Connecting to wifi \"%s\"...\n", ssid.c_str());
WiFi.mode(WIFI_AP_STA);
WiFi.begin(ssid.c_str(), pass.c_str());
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
DEBUG("Could not connect to wifi \"%s\".\n", ssid.c_str());
return false;
} else {
INFO("Connected to \"%s\". IP address: %s\n", ssid.c_str(), WiFi.localIP().toString().c_str());
}
return true;
}
void setup() {
delay(500);
Serial.begin(74880);
Serial.println("Starting...");
Serial.println("Started.");
INFO("Starting.\n");
#ifdef VERSION
INFO("ESMP3 version %s\n", VERSION);
#else
INFO("ESMP3, version unknown\n");
#endif
INFO("Initializing...\n");
DEBUG("Setting up SPI...\n");
SPI.begin();
SPI.setHwCs(false);
SPIMaster::init();
SPIMaster* spi = new SPIMaster();
INFO("SPI initialized.\n");
DEBUG("Setting up SD card...\n");
spi->select_sd();
if (SD.begin(42, SPI, 25000000)) {
INFO("SD card initialized.\n");
} else {
ERROR("Could not initialize SD card.\n");
}
spi->select_sd(false);
DEBUG("Starting SPIFFS...\n");
SPIFFS.begin(true);
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");
bool connected = false;
INFO("Connecting to WiFi...\n");
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 || line.startsWith("#") || line.indexOf('=')==-1) {
continue;
}
String ssid = line.substring(0, line.indexOf('='));
String pass = line.substring(line.indexOf('=')+1);
connected = connect_to_wifi(ssid, pass);
if (connected) break;
}
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 (!connected) {
DEBUG("Trying hardcoded WiFi data...\n");
connected = connect_to_wifi(WIFI_SSID, WIFI_PASS);
}
if (!connected) {
INFO("No WiFi connection!\n");
}
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");
}
INFO("Initialization completed.\n");
}
void loop() {
bool more_data_needed = player->loop();
if (more_data_needed) return;
controller->loop();
}

View File

@ -1,750 +0,0 @@
// Based on https://github.com/mpflaga/Arduino_Library-vs1053_for_SdFat/blob/master/src/vs1053_SdFat.cpp
#include "player.h"
#include "spi_master.h"
#include <ArduinoJson.h>
//Player::_spi_settings
Player::Player(SPIMaster* s) {
_spi = s;
PIN_VS1053_XRESET_SETUP();
PIN_VS1053_XRESET(HIGH);
PIN_SPEAKER_L_SETUP();
PIN_SPEAKER_R_SETUP();
_speaker_off();
_spi->disable();
PIN_VS1053_DREQ_SETUP();
init();
}
void Player::register_controller(Controller* c) {
_controller = c;
}
void Player::_reset() {
PIN_VS1053_XRESET(LOW);
delay(100);
PIN_VS1053_XRESET(HIGH);
delay(100);
_state = uninitialized;
_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow
}
void Player::init() {
DEBUG("Resetting VS1053...\n");
_reset();
uint16_t result = _read_control_register(SCI_MODE);
DEBUG("SCI_MODE: 0x%04X\n", result);
if (result != 0x4800) {
ERROR("SCI_MODE was 0x%04X, expected was 0x4800. Rebooting.\n", result);
delay(500);
ESP.restart();
}
result = _read_control_register(SCI_STATUS);
DEBUG("SCI_STATUS: 0x%04X\n", result);
if (result != 0x0040 && result != 0x0048) {
ERROR("SCI_STATUS was 0x%04X, expected was 0x0040 or 0x0048. Rebooting.\n", result);
delay(500);
ESP.restart();
}
result = _read_control_register(SCI_CLOCKF);
DEBUG("SCI_CLOCKF: 0x%04X\n", result);
DEBUG("VS1053 Init looking good.\n");
DEBUG("Upping VS1053 multiplier...\n");
_write_control_register(SCI_CLOCKF, 0xC000);
delay(10);
_spi_settings = &_spi_settings_fast;
result = _read_control_register(SCI_CLOCKF);
DEBUG("SCI_CLOCKF: 0x%04X\n", result);
if (result != 0xC000) {
ERROR("Error: SCI_CLOCKF was 0x%04X, expected was 0xC000. Rebooting.\n", result);
delay(500);
ESP.restart();
}
set_volume(VOLUME_DEFAULT);
INFO("VS1053 initialization completed.\n");
_state = idle;
}
void Player::_speaker_off() {
DEBUG("Speaker off\n");
PIN_SPEAKER_L(LOW);
PIN_SPEAKER_R(LOW);
}
void Player::_speaker_on() {
DEBUG("Speaker on\n");
PIN_SPEAKER_L(HIGH);
PIN_SPEAKER_R(HIGH);
}
void Player::_sleep() {
DEBUG("VS1053 going to sleep.\n");
_speaker_off();
_write_control_register(SCI_CLOCKF, 0x0000);
_spi_settings = &_spi_settings_slow;
_write_control_register(SCI_AUDATA, 0x0010);
set_volume(0, false);
_state = sleeping;
TRACE("VS1053 is sleeping now.\n");
}
void Player::_wakeup() {
if (_state != sleeping && _state != recording) return;
_stopped_at = millis();
DEBUG("Waking VS1053...\n");
set_volume(_volume, false);
_write_control_register(SCI_AUDATA, 0x0000);
_write_control_register(SCI_CLOCKF, 0x6000);
_write_control_register(SCI_MODE, 0x4800 | SM_RESET);
delay(10);
//_speaker_on();
_spi_settings = &_spi_settings_fast;
_state = idle;
}
void Player::_record() {
// http://www.vlsi.fi/fileadmin/software/VS10XX/VS1053_VS1063_PcmRecorder.pdf
DEBUG("Starting recording.\n");
set_volume(1, false);
// Disable SCI_BASS
_write_control_register(SCI_BASS, 0);
// Disable user applications
_write_control_register(SCI_AIADDR, 0);
// Disable interrupts
_write_control_register(SCI_WRAMADDR, 0xC01A);
_write_control_register(SCI_WRAM, 0x0002);
_patch_adpcm();
_write_control_register(SCI_MODE, SM_ADPCM);
_write_control_register(SCI_AICTRL0, 0x8000); // Mono VU meter
_write_control_register(SCI_AICTRL1, 1024); // Manual gain, 1x
_write_control_register(SCI_AICTRL2, 0); // Maximum gain for autogain - ignored
_write_control_register(SCI_AICTRL3, 0); // status: record
_write_control_register(SCI_AIADDR, 0x0034, false);
delay(1);
DEBUG("Recording.\n");
delay(10);
_state = recording;
}
inline void Player::_wait() {
while(!PIN_VS1053_DREQ());
}
uint16_t Player::_read_control_register(uint8_t address, bool do_wait) {
if (do_wait) _wait();
_spi->select_vs1053_xcs();
SPI.beginTransaction(*_spi_settings);
SPI.transfer(CMD_READ);
SPI.transfer(address);
uint8_t b1 = SPI.transfer(0xFF);
_wait();
uint8_t b2 = SPI.transfer(0xFF);
_wait();
SPI.endTransaction();
_spi->select_vs1053_xcs(false);
return (b1 << 8) | b2;
}
void Player::_write_control_register(uint8_t address, uint16_t value, bool do_wait) {
_wait();
_spi->select_vs1053_xcs();
SPI.beginTransaction(*_spi_settings);
SPI.transfer(CMD_WRITE);
SPI.transfer(address);
SPI.transfer(value >> 8);
SPI.transfer(value & 0xFF);
SPI.endTransaction();
_spi->select_vs1053_xcs(false);
if (do_wait) _wait();
}
void Player::_patch_adpcm() {
static const uint16_t patch_data[] = {
0x0007, 0x0001, 0xc01a, 0x0006, 0x0001, 0x0002, 0x0007, 0x0001, /* 0 */
0x0008, 0x0006, 0x8002, 0x0000, 0x0007, 0x0001, 0x000c, 0x0006, /* 8 */
0x0002, 0x7000, 0x0017, 0x0007, 0x0001, 0x8034, 0x0006, 0x0022, /* 10 */
0x0030, 0x0490, 0xb080, 0x0024, 0x3800, 0x0024, 0x0000, 0x1090, /* 18 */
0xf400, 0x5404, 0x0000, 0x0851, 0xf400, 0x5648, 0xf400, 0x5404, /* 20 */
0xf400, 0x5658, 0xf400, 0x5404, 0xf400, 0x5640, 0x0000, 0x800a, /* 28 */
0x2900, 0x9180, 0x0006, 0x2016, 0x2a00, 0x1bce, 0x2a00, 0x114e, /* 30 */
0x2a00, 0x168e, 0x0007, 0x0001, 0x1800, 0x0006, 0x8006, 0x0000, /* 38 */
0x0007, 0x0001, 0x8045, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, /* 40 */
0x7808, 0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, /* 48 */
0x7800, 0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x3e80, 0x3e10, /* 50 */
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, /* 58 */
0x4024, 0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, /* 60 */
0x8024, 0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, /* 68 */
0x0001, 0x805a, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, 0x7808, /* 70 */
0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, 0x7800, /* 78 */
0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x5e40, 0x3e10, 0x7800, /* 80 */
0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, 0x4024, /* 88 */
0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, /* 90 */
0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, /* 98 */
0x806f, 0x0006, 0x0030, 0x3e12, 0xb817, 0x3e12, 0x7808, 0x3e18, /* a0 */
0x3821, 0x3e18, 0xb823, 0x3e10, 0x7800, 0xb880, 0x3855, 0x0030, /* a8 */
0x0497, 0x48b2, 0x3c00, 0x0000, 0x800a, 0x2900, 0x7300, 0x3e10, /* b0 */
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x6890, 0x1bd5, 0x0030, /* b8 */
0x0497, 0x3f00, 0x0024, 0x36f0, 0x5800, 0x36f8, 0x9823, 0x36f8, /* c0 */
0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, 0x0030, 0x0717, 0x2100, /* c8 */
0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, 0x8010, 0x0006, 0x000e, /* d0 */
0x3e02, 0x8024, 0x0001, 0x000a, 0x6012, 0x0024, 0xfea2, 0x0024, /* d8 */
0x48b2, 0x1bca, 0x2000, 0x0000, 0x4180, 0x0024, 0x0007, 0x0001, /* e0 */
0x8087, 0x0006, 0x00e6, 0x3e00, 0x7843, 0x3e01, 0x3845, 0x3e04, /* e8 */
0x3812, 0x0006, 0x08d0, 0x3000, 0x4024, 0x6182, 0x0024, 0x0030, /* f0 */
0x06d0, 0x2800, 0x2655, 0xb882, 0x0024, 0x0000, 0x0201, 0x0000, /* f8 */
0x0005, 0x0030, 0x0210, 0xa016, 0x4004, 0x1fff, 0xfe01, 0xae1a, /* 100 */
0x0024, 0xc342, 0x0024, 0xb882, 0x2001, 0x0030, 0x06d0, 0x3800, /* 108 */
0x4024, 0x0006, 0x0890, 0x3004, 0x0024, 0x3000, 0x4024, 0x0006, /* 110 */
0x12d0, 0x6182, 0x0024, 0x3000, 0x4024, 0x2800, 0x3e05, 0xf400, /* 118 */
0x4050, 0x3009, 0x2000, 0x0006, 0x08d0, 0x0006, 0x0892, 0x3000, /* 120 */
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x0030, 0x0250, 0xb882, /* 128 */
0x2001, 0x0030, 0x0710, 0x3800, 0x4024, 0x0006, 0x12d0, 0x3000, /* 130 */
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x3204, 0x0024, 0x3023, /* 138 */
0x0024, 0x30e0, 0xc024, 0x6312, 0x0024, 0x0000, 0x00c3, 0x2800, /* 140 */
0x3141, 0x0000, 0x0024, 0x3033, 0x0024, 0x3a04, 0x0024, 0x3000, /* 148 */
0x4024, 0x6182, 0x0024, 0x0006, 0x0890, 0x2800, 0x2fd8, 0x0006, /* 150 */
0x0301, 0x3a00, 0x4024, 0x0000, 0x00c3, 0x3004, 0x0024, 0x3013, /* 158 */
0x0024, 0x3000, 0x4024, 0x0006, 0x12d0, 0x3800, 0x4024, 0x0030, /* 160 */
0x0310, 0xf000, 0x0001, 0x6236, 0x0024, 0x001f, 0xffc3, 0x2800, /* 168 */
0x3395, 0x0000, 0x0024, 0x0000, 0x0203, 0xa132, 0x0024, 0x001f, /* 170 */
0xffc3, 0xb136, 0x0024, 0x6306, 0x0024, 0x0000, 0x0024, 0x2800, /* 178 */
0x3611, 0x0000, 0x0024, 0x0020, 0x0003, 0xb132, 0x0024, 0x0000, /* 180 */
0x0024, 0x2800, 0x3a85, 0x0000, 0x0024, 0x0000, 0x0081, 0xb212, /* 188 */
0x0024, 0x0000, 0x0024, 0x2800, 0x3a05, 0x0000, 0x0024, 0x6892, /* 190 */
0x0024, 0xb212, 0x0024, 0x0000, 0x0005, 0x2800, 0x3c55, 0x0030, /* 198 */
0x0310, 0x0000, 0x3fc1, 0x3000, 0x8024, 0xb214, 0x0024, 0x003f, /* 1a0 */
0xc001, 0xb010, 0x0024, 0xc200, 0x0024, 0x0030, 0x0310, 0x3800, /* 1a8 */
0x0024, 0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x5803, 0x2000, /* 1b0 */
0x0000, 0x0000, 0x0024, 0x0030, 0x0310, 0x0000, 0x0005, 0x003f, /* 1b8 */
0xc001, 0x4088, 0x0002, 0xb214, 0x0024, 0x1fff, 0xfe01, 0xae12, /* 1c0 */
0x0024, 0x2800, 0x3a00, 0xc200, 0x0024, 0x2800, 0x28c0, 0x3800, /* 1c8 */
0x0024, 0x0007, 0x0001, 0x80fa, 0x0006, 0x00fe, 0x3e12, 0x0024, /* 1d0 */
0x3e05, 0xb814, 0x3615, 0x0024, 0x3e00, 0x3841, 0x3e00, 0xb843, /* 1d8 */
0x3e01, 0x3845, 0x3e04, 0x3851, 0x0030, 0x10d0, 0x3e04, 0x8024, /* 1e0 */
0x3010, 0x0024, 0x3000, 0x8024, 0x0006, 0x1190, 0x3000, 0x4024, /* 1e8 */
0x6182, 0x0024, 0x0000, 0x0024, 0x2800, 0x5dd5, 0x0000, 0x0024, /* 1f0 */
0x0030, 0x03d0, 0x0000, 0x00c1, 0x3000, 0xc024, 0xb318, 0x0024, /* 1f8 */
0x6896, 0x0024, 0x6436, 0x0024, 0x0020, 0x0003, 0x2800, 0x59c5, /* 200 */
0x0000, 0x0024, 0x0006, 0x1150, 0x3000, 0x4024, 0x6136, 0x0024, /* 208 */
0x0000, 0x0024, 0x2800, 0x4741, 0x0000, 0x0024, 0x0000, 0x0803, /* 210 */
0x4132, 0x0024, 0x3800, 0x4024, 0x0006, 0x0190, 0x0006, 0xf011, /* 218 */
0x2900, 0xb500, 0x3613, 0x0024, 0x0006, 0xf011, 0x0006, 0x1152, /* 220 */
0x0006, 0x0250, 0x4082, 0x0800, 0xfe82, 0x184c, 0x1fff, 0xfc41, /* 228 */
0x48ba, 0x0024, 0xae1a, 0x0024, 0x2900, 0xb500, 0x4280, 0x4103, /* 230 */
0x0006, 0x1110, 0x4084, 0x0800, 0xfe84, 0x0002, 0x48ba, 0x0024, /* 238 */
0xae12, 0x0024, 0xf400, 0x4001, 0x0000, 0x0180, 0x6200, 0x0024, /* 240 */
0x0000, 0x0080, 0x2800, 0x5241, 0x4200, 0x0024, 0x3800, 0x0024, /* 248 */
0x0006, 0x1090, 0x3004, 0x8024, 0xf400, 0x4491, 0x3113, 0x0024, /* 250 */
0x3804, 0x4024, 0x3a00, 0xc024, 0x3004, 0x8024, 0xf400, 0x4491, /* 258 */
0x3113, 0x0024, 0x3804, 0x4024, 0x3a00, 0x4024, 0x0006, 0x1081, /* 260 */
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x0f00, 0x2800, 0x5248, /* 268 */
0x0000, 0x0024, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0080, /* 270 */
0x3000, 0x4024, 0x0030, 0x0710, 0xb104, 0x0024, 0x0000, 0x0001, /* 278 */
0x3800, 0x4024, 0x0006, 0x08d0, 0x3001, 0x0024, 0x0006, 0x0910, /* 280 */
0x3000, 0x4024, 0x6100, 0x0024, 0x6042, 0x0024, 0x0030, 0x06d0, /* 288 */
0x2800, 0x5711, 0xb880, 0x0024, 0x2900, 0x21c0, 0x4380, 0x184c, /* 290 */
0xb880, 0x0024, 0x3800, 0x0024, 0x36f4, 0x8024, 0x36f4, 0x1811, /* 298 */
0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, /* 2a0 */
0x36f3, 0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, /* 2a8 */
0x0006, 0x1152, 0x0000, 0x0804, 0x3200, 0xc024, 0x6346, 0x0024, /* 2b0 */
0x6386, 0x2803, 0x0000, 0x0024, 0x2800, 0x4755, 0x0000, 0x0024, /* 2b8 */
0x3800, 0x4024, 0x0030, 0x0690, 0x0000, 0x0081, 0xb882, 0x22c1, /* 2c0 */
0x3800, 0x4024, 0x0030, 0x0590, 0x2800, 0x4740, 0x3800, 0x4024, /* 2c8 */
0x2800, 0x5700, 0x4190, 0x0024, 0x0007, 0x0001, 0x8179, 0x0006, /* 2d0 */
0x00a6, 0x3e12, 0x0024, 0x3e05, 0xb814, 0x3625, 0x0024, 0x3e00, /* 2d8 */
0x3841, 0x3e00, 0xb843, 0x3e04, 0x3851, 0x0006, 0x1110, 0x3e04, /* 2e0 */
0xb813, 0x3000, 0x0024, 0x6080, 0x0024, 0x0006, 0x11d2, 0x2800, /* 2e8 */
0x70c5, 0x0000, 0x0081, 0x6010, 0x984c, 0x3800, 0x0024, 0x0006, /* 2f0 */
0x10d0, 0x3200, 0x0024, 0xf100, 0x0011, 0xf100, 0x0024, 0xf102, /* 2f8 */
0x0400, 0x0006, 0x1311, 0x2900, 0x0400, 0x3100, 0x8024, 0x0030, /* 300 */
0x1293, 0x3413, 0x184c, 0x3c04, 0x4024, 0x3b00, 0x0024, 0x3004, /* 308 */
0xc024, 0xf400, 0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3310, /* 310 */
0x0024, 0x3a00, 0x0024, 0x0006, 0x1212, 0x3200, 0x0024, 0xf100, /* 318 */
0x13d1, 0xf100, 0x0402, 0x2900, 0x0400, 0xf102, 0x0c00, 0x0030, /* 320 */
0x12d1, 0x0006, 0x1081, 0x3900, 0x0024, 0x3004, 0xc024, 0xf400, /* 328 */
0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3300, 0x0024, 0x3a00, /* 330 */
0x0024, 0xf400, 0x4440, 0x6010, 0x0024, 0x1fee, 0xe002, 0x2800, /* 338 */
0x6bc8, 0x0006, 0x0f00, 0x3800, 0x0024, 0x0006, 0x0010, 0xb886, /* 340 */
0x0040, 0x30f0, 0x4024, 0x6c92, 0x40c3, 0x3810, 0x0024, 0xb182, /* 348 */
0x23c1, 0x0006, 0x0950, 0x3000, 0x0024, 0x6090, 0x0024, 0x6cd2, /* 350 */
0x2000, 0x0000, 0x0000, 0x2800, 0x70c8, 0x0000, 0x0024, 0x3800, /* 358 */
0x0024, 0x0000, 0x0210, 0x3010, 0x0024, 0x30f0, 0x4024, 0x6c92, /* 360 */
0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, 0x36f4, 0x9813, 0x36f4, /* 368 */
0x1811, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, 0x36f3, /* 370 */
0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, 0x0007, /* 378 */
0x0001, 0x81cc, 0x0006, 0x00f4, 0x3e00, 0x3841, 0x0000, 0x0201, /* 380 */
0x3e00, 0xb843, 0x3e01, 0x3845, 0x3e04, 0x3812, 0x0030, 0x0410, /* 388 */
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x08d0, 0x2800, 0x8045, /* 390 */
0x0000, 0x0181, 0x6012, 0x0024, 0x0006, 0x1250, 0x2800, 0x7e45, /* 398 */
0x0000, 0x05c1, 0x6012, 0x0024, 0x0030, 0x01d0, 0x2800, 0x7c45, /* 3a0 */
0x0000, 0x0581, 0x6010, 0x03cc, 0x0000, 0x0024, 0x2800, 0x7a95, /* 3a8 */
0x0000, 0x0024, 0x3000, 0x8024, 0x0006, 0x1250, 0x3000, 0x0024, /* 3b0 */
0x6092, 0x0024, 0x3800, 0x4024, 0xf400, 0x4010, 0x3800, 0x8024, /* 3b8 */
0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, /* 3c0 */
0x2000, 0x0000, 0x0000, 0x0024, 0x0030, 0x01d0, 0x3000, 0x0024, /* 3c8 */
0x0006, 0x1250, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3d0 */
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3d8 */
0x6090, 0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3e0 */
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3e8 */
0x6080, 0x0024, 0x0000, 0x0024, 0x2800, 0x8515, 0x0000, 0x0024, /* 3f0 */
0x0006, 0x1350, 0x0000, 0x0082, 0x0030, 0x0352, 0xb886, 0x0040, /* 3f8 */
0x30f0, 0x4024, 0x4cd2, 0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, /* 400 */
0x3a00, 0x0024, 0x3010, 0x0024, 0x30f0, 0x4024, 0x0030, 0x0390, /* 408 */
0x2800, 0x7a80, 0x4180, 0x2001, 0x4090, 0x0024, 0x3800, 0x0024, /* 410 */
0x0030, 0x0250, 0x3800, 0x0024, 0x0006, 0x1290, 0x3000, 0x0024, /* 418 */
0x6090, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x8024, /* 420 */
0x3223, 0x0024, 0x32e0, 0x4024, 0x6100, 0x0024, 0x0000, 0x0024, /* 428 */
0x2800, 0x8c81, 0x0000, 0x0024, 0x3233, 0x0024, 0x3804, 0x8024, /* 430 */
0x3200, 0x0024, 0x6080, 0x0024, 0x0006, 0x0300, 0x2800, 0x8b18, /* 438 */
0x0000, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x0024, /* 440 */
0x3013, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, 0x3800, 0x0024, /* 448 */
0x0006, 0x0850, 0x3004, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, /* 450 */
0x6080, 0x0024, 0x3000, 0x0024, 0x2800, 0x9115, 0xf400, 0x4010, /* 458 */
0x3000, 0x0024, 0x0000, 0x0201, 0x0000, 0x0005, 0x0030, 0x0210, /* 460 */
0xa014, 0x4004, 0x1fff, 0xfe01, 0xae12, 0x0024, 0xc200, 0x0024, /* 468 */
0x2800, 0x8180, 0x3800, 0x0024, 0x2800, 0x8ec0, 0x3009, 0x0000, /* 470 */
0x0007, 0x0001, 0x8246, 0x0006, 0x0104, 0x0030, 0x1092, 0x0007, /* 478 */
0x9250, 0x003f, 0xfc42, 0xb880, 0x184c, 0x3e12, 0x0024, 0x3800, /* 480 */
0x0024, 0x0030, 0x0290, 0x38f0, 0x0024, 0x3800, 0x0024, 0x0030, /* 488 */
0x0050, 0x3000, 0x4024, 0xb122, 0x0024, 0x6894, 0x2001, 0x0000, /* 490 */
0x0141, 0x3a70, 0x4024, 0x0004, 0x1fc1, 0x3a00, 0x4024, 0x0030, /* 498 */
0x00d2, 0x0030, 0x0001, 0x3a00, 0x4024, 0x0030, 0x0552, 0x3a10, /* 4a0 */
0x0024, 0x3a00, 0x0024, 0x3000, 0x4024, 0xc122, 0x0024, 0x3800, /* 4a8 */
0x4024, 0x0030, 0x05d0, 0x0000, 0x03c1, 0x3820, 0x4024, 0x3800, /* 4b0 */
0x0024, 0x0000, 0x0310, 0x3010, 0x0024, 0x30f0, 0x4024, 0xf2c2, /* 4b8 */
0x0024, 0x3810, 0x0024, 0x0000, 0x3fc0, 0x38f0, 0x4024, 0x0030, /* 4c0 */
0x02d0, 0x3000, 0x4024, 0x2912, 0x1400, 0xb104, 0x0024, 0x0006, /* 4c8 */
0x1312, 0x6802, 0x0024, 0x000d, 0xac00, 0x6012, 0x2801, 0x0000, /* 4d0 */
0x0024, 0x2800, 0x9dc1, 0x0000, 0x0024, 0x3a00, 0x0024, 0x2909, /* 4d8 */
0x1b40, 0x3613, 0x0024, 0x0000, 0x0084, 0x0000, 0x1905, 0x2908, /* 4e0 */
0xbe80, 0x3613, 0x0024, 0x0000, 0x0000, 0x0006, 0x0302, 0x4002, /* 4e8 */
0x0024, 0x4012, 0x0024, 0x4212, 0x0024, 0xf400, 0x4050, 0x3000, /* 4f0 */
0x4024, 0x6182, 0x0024, 0x0006, 0x0350, 0x2800, 0xa6c8, 0x0000, /* 4f8 */
0x0024, 0x4002, 0x0024, 0x4014, 0x0024, 0x0006, 0x0301, 0x4124, /* 500 */
0x0024, 0x0000, 0x0081, 0x4212, 0x0024, 0x4002, 0x4050, 0x4014, /* 508 */
0x0003, 0x0006, 0x0301, 0x4122, 0x0024, 0x6192, 0x0024, 0x6090, /* 510 */
0x4050, 0x3000, 0x4024, 0x0006, 0x0910, 0x6312, 0x0024, 0x6194, /* 518 */
0x0001, 0x4122, 0x0024, 0x2800, 0x9f80, 0x3800, 0x4024, 0x0006, /* 520 */
0x12d2, 0x0006, 0x0991, 0x3000, 0x0024, 0x0006, 0x1290, 0x3a00, /* 528 */
0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x2900, 0xb200, 0x0000, /* 530 */
0x0580, 0x0030, 0x0210, 0x0014, 0x9240, 0x003f, 0xf502, 0x003f, /* 538 */
0xffc3, 0x3800, 0x0024, 0x0000, 0x0580, 0x0006, 0x1350, 0x3200, /* 540 */
0x4024, 0x4102, 0x0024, 0x3a00, 0x4024, 0x3810, 0x8024, 0x38f0, /* 548 */
0xc024, 0x0006, 0x08d0, 0x3800, 0x0024, 0x0030, 0x0690, 0x0000, /* 550 */
0x8280, 0xb880, 0x2080, 0x3800, 0x0024, 0x6890, 0x2000, 0x0030, /* 558 */
0x0490, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0100, 0x3000, /* 560 */
0x584c, 0xb100, 0x0024, 0x0000, 0x0024, 0x2800, 0xb185, 0x0000, /* 568 */
0x0024, 0x003f, 0xfec1, 0x3000, 0x1bcc, 0xb010, 0x0024, 0x2908, /* 570 */
0x0b80, 0x3800, 0x0024, 0x3613, 0x0024, 0x2910, 0x0180, 0x0000, /* 578 */
0xae48, 0x0007, 0x0001, 0x1806, 0x0006, 0x8007, 0x0000, 0x0006, /* 580 */
0x002f, 0x0010, 0x17ff, 0x0000, 0x1a00, 0x1dff, 0x0000, 0x1f00, /* 588 */
0x3fff, 0x0001, 0x0000, 0x17ff, 0x0001, 0x1c00, 0x3fff, 0x0001, /* 590 */
0xe000, 0xfffd, 0xffff, 0x0000, 0x0000, 0x180c, 0x180c, 0x0000, /* 598 */
0x0000, 0x0000, 0x4952, 0x4646, 0xffff, 0xffff, 0x4157, 0x4556, /* 5a0 */
0x6d66, 0x2074, 0x0010, 0x0000, 0x0001, 0x0001, 0xbb80, 0x0000, /* 5a8 */
0x7700, 0x0001, 0x0002, 0x0010, 0x6164, 0x6174, 0xffff, 0xffff, /* 5b0 */
0x0006, 0x8006, 0x0000, 0x0006, 0x0005, 0x183c, 0x183c, 0x0000, /* 5b8 */
0x0020, 0x0040, 0x0006, 0x8003, 0x0000, 0x0007, 0x0001, 0x5bc0, /* 5c0 */
0x0006, 0x0009, 0x801c, 0x7fe4, 0x8039, 0x804e, 0x7fb2, 0x809d, /* 5c8 */
0x809c, 0x7f64, 0x8139, 0x0007, 0x0001, 0x82c8, 0x0006, 0x0018, /* 5d0 */
0x4080, 0x184c, 0x3e13, 0x780f, 0x2800, 0xb405, 0x4090, 0x380e, /* 5d8 */
0x2400, 0xb3c0, 0xf400, 0x4417, 0x3110, 0x0024, 0x3f10, 0x0024, /* 5e0 */
0x36f3, 0x8024, 0x36f3, 0x580f, 0x2000, 0x0000, 0x0000, 0x0024, /* 5e8 */
0x0007, 0x0001, 0x82d4, 0x0006, 0x002a, 0x3e11, 0xb807, 0x3009, /* 5f0 */
0x384a, 0x3e11, 0x3805, 0x3e10, 0xb803, 0x3e00, 0x4442, 0x0001, /* 5f8 */
0x800a, 0xbf8e, 0x8443, 0xfe06, 0x0045, 0x3011, 0x0401, 0x545e, /* 600 */
0x0385, 0x525e, 0x2040, 0x72ce, 0x1bc1, 0x48ba, 0x9803, 0x4588, /* 608 */
0x4885, 0x6fee, 0x1bc2, 0x4ffe, 0x9805, 0xf6fe, 0x1bc4, 0xf7f0, /* 610 */
0x2046, 0x3801, 0xdbca, 0x2000, 0x0000, 0x36f1, 0x9807,
};
const uint16_t patch_size = 1567;
DEBUG("Patching...\n");
_spi->select_vs1053_xcs();
SPI.beginTransaction(*_spi_settings);
for (int i=0; i<patch_size; i++) {
DEBUG(" %d\n", i);
unsigned short addr, n, val;
addr = patch_data[i++];
n = patch_data[i++];
SPI.transfer(CMD_WRITE);
SPI.transfer(addr & 0XFF);
if (n & 0x8000U) { /* RLE run, replicate n samples */
n &= 0x7FFF;
val = patch_data[i++];
while (n--) {
SPI.transfer(val >> 8);
SPI.transfer(val & 0xFF);
_wait();
}
} else { /* Copy run, copy n samples */
while (n--) {
val = patch_data[i++];
SPI.transfer(val >> 8);
SPI.transfer(val & 0xFF);
_wait();
}
}
}
SPI.endTransaction();
_spi->select_vs1053_xcs(false);
DEBUG("Patch sent.\n");
}
void Player::_write_data(uint8_t* buffer) {
_spi->select_vs1053_xdcs();
SPI.beginTransaction(*_spi_settings);
for (uint i=0; i<sizeof(_buffer); i++) {
SPI.transfer(_buffer[i]);
}
SPI.endTransaction();
_spi->select_vs1053_xdcs(false);
}
uint16_t Player::_read_wram(uint16_t address) {
DEBUG("Reading WRAM address 0x%04X...\n", address);
_write_control_register(SCI_WRAMADDR, address);
uint16_t r1 = _read_control_register(SCI_WRAM);
_write_control_register(SCI_WRAMADDR, address);
uint16_t r2 = _read_control_register(SCI_WRAM);
if (r1 == r2) return r1;
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
_write_control_register(SCI_WRAMADDR, address);
r1 = _read_control_register(SCI_WRAM);
if (r1 == r2) return r1;
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
_write_control_register(SCI_WRAMADDR, address);
r2 = _read_control_register(SCI_WRAM);
if (r1 == r2) return r1;
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
DEBUG("Returning last value (0x%04X)...\n", r2);
return r2;
}
int8_t Player::_get_endbyte() {
int8_t endbyte = _read_wram(ADDR_ENDBYTE) & 0xFF;
DEBUG("Endbyte is 0x%02X.\n", endbyte);
return endbyte;
}
void Player::set_volume(uint8_t vol, bool save) {
if (save) {
_volume = vol;
}
INFO("Setting volume to %d\n", vol);
vol = 0xFF - vol;
uint16_t value = (vol<<8)|vol;
DEBUG("Setting volume register to 0x%04X\n", value);
_write_control_register(SCI_VOL, value);
}
void Player::vol_up() {
if (!is_playing()) return;
uint8_t vol = _volume + VOLUME_STEP;
if (vol > VOLUME_MAX) vol=VOLUME_MAX;
set_volume(vol);
}
void Player::vol_down() {
if (!is_playing()) return;
uint8_t vol = _volume - VOLUME_STEP;
if (vol < VOLUME_MIN) vol=VOLUME_MIN;
set_volume(vol);
}
void Player::_mute() {
INFO("Muting.\n");
_speaker_off();
set_volume(1, false);
}
void Player::_unmute() {
INFO("Unmuting.\n");
set_volume(_volume, false);
_speaker_on();
}
void Player::track_next() {
if (_state != playing) return;
if (!_current_playlist->has_track_next()) {
return;
}
stop();
_current_playlist->track_next();
play();
}
void Player::track_prev() {
if (_state != playing) return;
if (_current_play_position > 100000) {
stop();
_current_playlist->track_restart();
play();
} else {
if (!_current_playlist->has_track_prev()) {
return;
}
stop();
_current_playlist->track_prev();
play();
}
}
void Player::set_track(uint8_t id) {
stop();
_current_playlist->set_track(id);
play();
}
bool Player::is_playing() {
return _state == playing;
}
bool Player::play(Playlist* p) {
_current_playlist = p;
return play();
}
bool Player::play() {
if (_state == sleeping || _state == recording) _wakeup();
if (_state != idle) return false;
if (_current_playlist == NULL) return false;
if (_current_playlist->get_file_count()==0) return false;
_current_playlist->start();
String file = _current_playlist->get_current_file();
uint32_t position = _current_playlist->get_position();
_state = playing;
_play_file(file, position);
_controller->send_player_status();
return true;
}
void Player::_play_file(String file, uint32_t file_offset) {
INFO("play_file('%s', %d)\n", file.c_str(), file_offset);
_spi->select_sd();
if (file.startsWith("/")) {
_file = new SDDataSource(file);
} else if (file.startsWith("http")) {
_file = new HTTPSDataSource(file);
} else {
return;
}
_file_size = _file->size();
_spi->select_sd(false);
if (!_file || !_file->usable()) {
DEBUG("Could not open file %s", file.c_str());
return;
}
DEBUG("Resetting SCI_DECODE_TIME...\n");
_write_control_register(SCI_DECODE_TIME, 0);
DEBUG("Resetting SS_DO_NOT_JUMP...\n");
_write_control_register(SCI_STATUS, _read_control_register(SCI_STATUS) & ~SS_DO_NOT_JUMP);
delay(100);
_spi->select_sd();
if (file_offset == 0) {
_file->skip_id3_tag();
}
_refills = 0;
_current_play_position = _file->position();
_spi->select_sd(false);
_skip_to = file_offset;
if (_skip_to>0) _mute();
else _speaker_on();
INFO("Now playing.\n");
_controller->send_player_status();
}
void Player::_flush(uint count, int8_t byte) {
_spi->select_vs1053_xdcs();
SPI.beginTransaction(*_spi_settings);
for(uint i=0; i<count; i++) {
_wait();
SPI.transfer(byte);
}
SPI.endTransaction();
_spi->select_vs1053_xdcs(false);
}
void Player::_finish_playing() {
uint8_t endbyte = _get_endbyte();
_flush(2052, endbyte);
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
for (int i=0; i<64; i++) {
_flush(32, endbyte);
uint16_t mode = _read_control_register(SCI_MODE);
if ((mode & SM_CANCEL) == 0) return;
}
// If we reached this, the Chip didn't stop. That should not happen.
// (That's written in the manual.)
// Reset the chip.
init();
}
void Player::stop(bool turn_speaker_off) {
if (_state != playing) return;
INFO("Stopping...\n");
_current_playlist->set_position(_current_play_position);
_state = stopping;
_stop_delay = 0;
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
uint8_t endbyte = _get_endbyte();
while (true) {
_refill();
uint16_t mode = _read_control_register(SCI_MODE);
if ((mode & SM_CANCEL) == 0) {
_flush(2052, endbyte);
_finish_stopping(turn_speaker_off);
break;
} else if (_stop_delay > 2048) {
init();
break;
}
_stop_delay++;
}
}
void Player::_finish_stopping(bool turn_speaker_off) {
if (turn_speaker_off) _speaker_off();
_state = idle;
_stopped_at = millis();
if (_file) {
_file->close();
delete _file;
}
_current_play_position = 0;
_file_size = 0;
INFO("Stopped.\n");
_controller->send_player_status();
}
void Player::_refill() {
_spi->select_sd();
_refills++;
if (_refills % 1000 == 0) DEBUG(".");
uint8_t result = _file->read(_buffer, sizeof(_buffer));
_spi->select_sd(false);
if (result == 0) {
// File is over.
DEBUG("EOF reached.\n");
_skip_to = 0;
_finish_playing();
_finish_stopping(false);
if (_current_playlist->has_track_next()) {
_current_playlist->track_next();
play();
} else {
_current_playlist->reset();
_controller->send_player_status();
}
return;
}
_current_play_position+=result;
_write_data(_buffer);
if (_skip_to > 0) {
if (_skip_to > _file->position()) {
uint16_t status = _read_control_register(SCI_STATUS);
if ((status & SS_DO_NOT_JUMP) == 0) {
DEBUG("Skipping to %d.\n", _skip_to);
_flush(2048, _get_endbyte());
_spi->select_sd();
_file->seek(_skip_to);
_spi->select_sd(false);
_skip_to = 0;
_unmute();
_controller->send_position();
}
} else {
_skip_to = 0;
_unmute();
}
}
}
bool Player::_refill_needed() {
return _state==playing || _state==stopping;
}
bool Player::loop() {
if (PIN_VS1053_DREQ() && _refill_needed()) {
_refill();
return true;
}
if (_state == recording) {
DEBUG("r");
uint16_t samples_available = _read_control_register(SCI_HDAT1, false);
uint16_t vu_value = _read_control_register(SCI_AICTRL0, false);
DEBUG("Samples available: %4d, VU meter: 0x%04X\n", samples_available, vu_value);
if (samples_available >= 500) {
unsigned long sum = 0;
for (int i=0; i<500; i++) {
uint16_t sample = _read_control_register(SCI_HDAT0, false);
sum += sample * sample;
}
double result = sqrt(sum / 500);
DEBUG("Loudness: %f", result);
}
}
if (_state == idle && _stopped_at < millis() - VS1053_SLEEP_DELAY) {
_sleep();
//_record();
}
return false;
}
String Player::json() {
DynamicJsonDocument json(10240);
json["_type"] = "player";
json["playing"] = is_playing();
if (_current_playlist) {
JsonObject playlist = json.createNestedObject("playlist");
_current_playlist->json(playlist);
} else {
json["playlist"] = nullptr;
}
JsonObject volume = json.createNestedObject("volume");
volume["current"] = _volume;
volume["min"] = VOLUME_MIN;
volume["max"] = VOLUME_MAX;
volume["step"] = VOLUME_STEP;
return json.as<String>();
}
String Player::position_json() {
if (!is_playing()) return "null";
DynamicJsonDocument json(200);
json["_type"] = "position";
json["position"] = _current_play_position;
json["file_size"] = _file_size;
return json.as<String>();
}

View File

@ -1,347 +1,82 @@
#include <playlist.h>
#include "spi_master.h"
#include "config.h"
#include <SD.h>
#include <algorithm>
#include <ArduinoJson.h>
#include <TinyXML.h>
#include "playlist.h"
Playlist::Playlist(String path) {
if (path.startsWith("/")) {
_add_path(path);
} else if (path.startsWith("http")) {
_examine_http_url(path);
}
if (_title.length()==0) _title=path;
Playlist::Playlist() {}
Playlist::Playlist(String id, PersistedPlaylist* p) {
rfid_id = id;
pp = p;
}
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});
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());
String Playlist::get_rfid_id() {
return rfid_id;
}
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/")) {
_files.push_back({.filename=url, .title=url});
} else if (ct.startsWith("application/rss+xml")) {
_parse_rss(http);
} else if (ct.startsWith("application/pls+xml")) {
_parse_pls(http);
} else {
ERROR("Unknown content type %s.\n", ct.c_str());
}
http->close();
delete http;
void Playlist::add_file(String filename) {
files.push_back(filename);
}
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 = "";
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 = "";
} else if (tag.endsWith("/item/title") && (status & STATUS_TAG_TEXT)) {
xml_title = String(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;
void Playlist::sort() {
std::sort(files.begin(), files.end());
}
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) {
DEBUG("Adding playlist entry: '%s' => '%s'\n", xml_title.c_str(), xml_url.c_str());
xml_files_ptr->push_back({xml_url, xml_title});
void Playlist::set_current_position(uint8_t file, uint32_t bytes) {
log_d("Setting position: File %d, bytes %d.", file, bytes);
current_file = file;
current_time = bytes;
save_current_position();
}
void Playlist::save_current_position(uint32_t position) {
if (position==0) {
position = current_time;
}
log_d("Saving current position: File %d, bytes %d.", current_file, position);
if (pp != NULL) {
pp->file = current_file;
pp->position = position;
}
}
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);
String Playlist::get_current_file_name() {
if (current_file >= files.size()) {
Serial.printf("Requested a file number %d, which is not available in this playlist. Starting over.\n", current_file);
set_current_position(0);
}
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");
return files[current_file];
}
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});
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});
last_index = -1;
title = "";
url = "";
}
}
// don't close http at the end
}
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;
}
bool Playlist::next_track() {
if (files.size() <= current_file + 1) {
Serial.println("next_track does not exist. Resetting playlist and returning false.");
set_current_position(0, 0);
return false;
}
bool Playlist::track_next() {
if (_current_track < _files.size()-1) {
_current_track++;
_position = 0;
set_current_position(current_file + 1, 0);
return true;
}
return false;
bool Playlist::prev_track() {
log_d("Playlist::prev_track called. current_file is %d", current_file);
if (current_file == 0) {
set_current_position(0, 0);
} else {
set_current_position(current_file - 1, 0);
}
return files.size()>0;
}
bool Playlist::set_track(uint8_t track) {
if (track < _files.size()) {
_current_track = track;
_position = 0;
return true;
}
return false;
void Playlist::restart() {
current_time = 0;
}
void Playlist::track_restart() {
_position = 0;
void Playlist::set_current_time(uint32_t pos) {
set_current_position(current_file, pos);
}
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");
uint32_t Playlist::get_current_time() {
return current_time;
}
void Playlist::advent_shuffle(uint8_t day) {
if (day > 24) day = 24;
if (day > _files.size()) return;
_files.insert(_files.begin(), _files[day - 1]);
_files.erase(_files.begin() + day - 1, _files.end());
}
void Playlist::reset() {
std::sort(_files.begin(), _files.end());
_current_track = 0;
_position = 0;
_shuffled = false;
_started = false;
}
String Playlist::get_current_file() {
return _files[_current_track].filename;
}
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;
}
json["current_track"] = _current_track;
json["has_track_next"] = has_track_next();
json["has_track_prev"] = has_track_prev();
void Playlist::shuffle() {
std::random_shuffle(files.begin(), files.end());
}

View File

@ -1,32 +1,17 @@
#include "playlist_manager.h"
#include <SD.h>
#include "spi_master.h"
#include <ArduinoJson.h>
#include <SD.h>
PlaylistManager::PlaylistManager() {
scan_files();
}
SPIMaster::enable_sd();
current_rfid_tag_id = String("");
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");
if (!SD.exists("/_mapping.txt")) {
Serial.println("WARNING: /_mapping.txt not found!");
} else {
map.clear();
File f = SD.open("/_mapping.txt");
DEBUG("Reading /_mapping.txt...\n");
Serial.println(" Reading /_mapping.txt...");
while (f.available()) {
char buffer[512];
size_t pos = f.readBytesUntil('\n', buffer, 511);
@ -37,130 +22,64 @@ void PlaylistManager::scan_files() {
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());
}
}
Serial.printf(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
map[rfid_id] = PersistedPlaylist(folder);
}
}
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()) {
Playlist PlaylistManager::get_playlist(String rfid_id) {
if (rfid_id.equals(current_rfid_tag_id)) {
return current_playlist;
} else {
if (map.count(rfid_id)==0) {
Serial.printf("No known playlist for id %s.\n", rfid_id);
return current_playlist;
} else {
PersistedPlaylist* ap = &(map[rfid_id]);
log_d("PP status is: File %d, bytes %d.", ap->file, ap->position);
current_playlist = Playlist(rfid_id, ap);
String path = ap->dir;
if (path.startsWith("/")) {
File dir = SD.open(path);
while(File entry = dir.openNextFile()) {
String filename = entry.name();
filename = filename.substring(folder.length() + 1);
if (!filename.startsWith(".") && filename.endsWith(".mp3")) {
found = true;
String ext = filename.substring(filename.length()-4);
if (!entry.isDirectory() &&
!filename.startsWith(".") &&
ext.equals(".mp3")) {
Serial.printf("Adding %s to the list of files\n", (path + "/" + filename).c_str());
current_playlist.add_file(path + "/" + filename);
}
entry.close();
if (found) break;
}
if (found) {
_unmapped_folders.push_back(folder);
dir.close();
current_playlist.set_current_position(ap->file, ap->position);
} else if (path.startsWith("http")) {
Serial.printf("Adding URL %s to the list of files\n", path.c_str());
current_playlist.add_file(path);
}
}
}
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;
current_playlist.sort();
current_rfid_tag_id = rfid_id;
return current_playlist;
}
}
}
Playlist* PlaylistManager::get_playlist_for_id(String id) {
if (!_map.count(id)) return NULL;
String folder = _map[id];
return get_playlist_for_folder(folder);
void PlaylistManager::set_audio_current_time(uint32_t time) {
audio_current_time = time;
}
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
if (!_playlists.count(folder)) {
_playlists[folder] = new Playlist(folder);
}
return _playlists[folder];
bool PlaylistManager::has_playlist(String rfid_id) {
return map.count(rfid_id) == 1;
}
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';
String PlaylistManager::pp_to_String() {
String s = "";
for(const auto& kv : map) {
s += kv.first + "=" + kv.second.file + "," + kv.second.position + '\n';
}
return s;
}

26
src/spi_master.cpp Normal file
View File

@ -0,0 +1,26 @@
#include "spi_master.h"
#include <Arduino.h>
#include "esmp3.h"
void SPIMaster::initialize() {
pinMode(PIN_CS_SD, OUTPUT);
pinMode(PIN_CS_RFID, OUTPUT);
disable_all();
}
void SPIMaster::disable_all() {
digitalWrite(PIN_CS_SD, HIGH);
digitalWrite(PIN_CS_RFID, HIGH);
}
void SPIMaster::enable_rfid() {
disable_all();
digitalWrite(PIN_CS_RFID, LOW);
delay(5);
}
void SPIMaster::enable_sd() {
disable_all();
digitalWrite(PIN_CS_SD, LOW);
delay(5);
}