55 Commits

Author SHA1 Message Date
8f19b990ff PlaylistManager: Only search for folders, don't try it with webstreams. 2019-11-28 06:29:51 +01:00
519ac0e3bd Playlist: Album title now gets handled better. 2019-11-28 06:20:57 +01:00
651843fb06 Webinterface: Fixed usage of filenames as titles. 2019-11-28 06:20:07 +01:00
fcbbdce118 Playlist: Initialization of PlaylistEntries now uses designators. 2019-11-28 06:19:11 +01:00
6f8683ba9d WIP: Lots of streaming stuff 2019-11-27 06:51:20 +01:00
710b8a2cdc Add UserAgent, remove superfluous form of location mapping. 2019-11-20 06:17:18 +01:00
b989784fb9 You can now also play MP3s streamed from the internet. (Very rough & wonky code. More or less proof-of-concept right now.) 2019-11-20 06:13:15 +01:00
94489618ca Moved reading of SD card data into a dedicated class DataSource. 2019-11-20 05:04:27 +01:00
82d8f07eea Player: Only change volume and report a position if we are actually playing something. 2019-11-19 20:48:43 +01:00
20041dd483 Extended http_server to provide new endpoints:
/_mapping.txt, /player.json, /playlist_manager.json, /controller.json and /position.json to get the matching data as well as /cmd to send commands to.
2019-11-19 20:48:11 +01:00
4f9174d362 PlaylistManager: Extracted create_mapping_txt from _save_mapping. 2019-11-19 20:46:54 +01:00
68ecc05712 Made player and playlist_manager pubilc members of Controller. 2019-11-19 20:46:04 +01:00
5fad39ee0e Added File System Upload step to installation instructions. 2019-11-19 20:44:03 +01:00
01f513c97b More typographic fixes. 2019-11-17 19:34:38 +01:00
3bfbea92d8 Typo. -_- 2019-11-17 19:33:25 +01:00
d818624287 Added much more info to README.md 2019-11-17 19:31:59 +01:00
d92388d11f Add config.h to .gitignore. 2019-11-17 17:39:50 +01:00
37df309127 Removed MQTT client. Was more or less unused, anyways. 2019-11-17 17:38:41 +01:00
be8a124803 Prepared config.sample.h for making it public. 2019-11-17 17:31:52 +01:00
104236dd0c Remove old files from .vscode. 2019-11-17 14:27:43 +01:00
e1dd004cf5 Reload folder mappings after modifying a mapping. 2019-11-17 14:26:23 +01:00
b5ec78ab41 When chaging to another track, play it from the beginning. 2019-11-17 14:25:47 +01:00
fff9d9bc61 Small fix. 2019-11-17 14:25:22 +01:00
ef47c771ef You can now get and set the state of all CS pins at once. 2019-11-17 14:24:01 +01:00
9f442259e9 Worked on the upload stuff. It works, but the SD card seems to die after a few MB... :-/ 2019-11-17 14:23:17 +01:00
8e5a3195b9 Scrollable modals, working add_mapping feature. 2019-11-17 14:22:22 +01:00
cc4729eb6b Ignore .vscode folder. 2019-11-17 14:21:39 +01:00
f7c4b0d70a Removed timestamp from build_version.sh cause it forced a complete recompile of everything, all the time. 2019-11-17 14:20:55 +01:00
566068f7cd Increased the SD card speed from 4 MHz to 25 MHz for faster directory reading. 2019-11-17 00:35:55 +01:00
5c15a7d4cb index.html is now served from SPIFFS. You can add rfid tag -> folder mappings via the webinterface. And I've added the missing controller json data messages. 2019-11-17 00:35:23 +01:00
b9a4770ff2 Massive changes. Added a quite nice webinterface with live data using WebSockets. Removed the FTP server (wasn't that useful anyways). JSON creating using ArduinoJson instead of String concatenation. Ans, and, and. 2019-11-16 23:03:13 +01:00
e471a57578 Changed the playing code to use Playlists managed by a PlaylistManager. This allows you to have randomized playlists and stuff. Also, you can now access special functions via the contents of RFID tags. See the README for a list of available modes. 2019-11-14 20:42:02 +01:00
6e05900b5a More changes for the recording... It's still not working. -_- 2019-11-14 06:50:13 +01:00
15f6d78128 RFID reading now includes RFID data, enabling you to save additional data (still to come...) to the rfid card. 2019-11-14 06:48:21 +01:00
b32f7d1228 Experimental code to read RFID card data. 2019-11-13 20:14:09 +01:00
45fef23bad Another try in getting the recording stuff to work... 2019-11-13 20:13:52 +01:00
e20e6b7d3e Added serial command ids; id-to-folder mapping now works. 2019-11-13 06:51:38 +01:00
0531b599fe Internal changes. 2019-11-13 06:50:29 +01:00
a5751eec79 Merge branch 'master' of https://git.schle.nz/fabian/esmp3 2019-11-12 19:31:31 +01:00
e02b8571f6 Tries to get the recording functionality to work. As of yet it's not working. :-/ 2019-11-12 19:31:26 +01:00
303a8d3877 Removed old MCP23017 pin definitions. 2019-11-12 19:30:42 +01:00
6d00474315 ID of album to play now comes from a file ids.txt within the folder. This is as of yet untested. 2019-11-12 19:29:55 +01:00
46fb4c7615 Modified build_version.sh to also include date and time of the build. 2019-11-12 19:29:03 +01:00
48c93ed043 Added recording mode to detect volumes. 2019-11-11 17:40:47 +01:00
2d1f049444 Changed from ESP8266 to ESP32. Works much better.
HTTP server is disabled (for now).
2019-11-11 05:32:41 +01:00
c313f6eb72 Changed from MCP23S17 to MCP23017. Lots of changes.
Doesn't really work because of timing stuff.
2019-11-10 14:45:33 +01:00
cccdc9cedb Merge branch 'master' of https://git.schle.nz/fabian/esmp3 2019-09-03 06:07:07 +02:00
429979c6d1 Start Wifi after initializing VS1053 - faster bootup when multiple tries setting up VS1053 are needed. 2019-09-03 06:06:51 +02:00
e28a541fe9 Better default volumes. 2019-09-03 06:05:26 +02:00
0f2b8c6564 Fixed LIST command of FTP server. It now returns something akin to 'ls -la'. 2019-08-21 20:50:27 +02:00
5f682c303f Added MQTT client, better speaker handling, fixed bug in FTP server, ... 2019-08-14 21:01:01 +02:00
dcbb42f5ef Add code for switching the speaker respective their amps on or off. 2019-08-14 06:36:26 +02:00
235ef8c39d Added FTP server and automatic version generation. 2019-08-13 19:39:03 +02:00
4d59c66354 Format changes. 2019-08-13 06:16:08 +02:00
25fa963752 Added sleep mode for VS1053, HTTP server, tar upload, JSON status, RFID card removal debouncing, ... 2019-08-12 20:15:00 +02:00
22 changed files with 2720 additions and 388 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
.pio
.pioenvs
.piolibdeps
.vscode
include/config.h

147
README.md Normal file
View File

@ -0,0 +1,147 @@
# 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.

2
build_version.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
echo -n "-DVERSION=\\\"`git describe --tags --dirty`\\\""

322
data/index.html Normal file
View File

@ -0,0 +1,322 @@
<!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>

76
include/config.sample.h Normal file
View File

@ -0,0 +1,76 @@
#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,30 +1,57 @@
#pragma once
#include <Arduino.h>
#include <ESPAsyncWebServer.h>
#include "config.h"
class Controller;
#include "player.h"
#include "playlist.h"
#include "playlist_manager.h"
#include "http_server.h"
#include <MFRC522.h>
#include <MCP23S17/MCP23S17.h>
enum ControllerState { NORMAL, LOCKING, LOCKED };
class Controller {
private:
MFRC522* _rfid;
MCP* _mcp;
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();
uint32_t _last_rfid_card_uid = 0;
Player* _player;
String _read_rfid_data();
bool _rfid_present = false;
String _last_rfid_uid = "";
String _last_rfid_data = "";
unsigned long _last_rfid_scan_at = 0;
unsigned long _last_position_info_at = 0;
String _serial_buffer = String();
void _execute_serial_command(String cmd);
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, MCP* m);
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();
};

54
include/data_sources.h Normal file
View File

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

View File

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

29
include/http_server.h Normal file
View File

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

View File

@ -2,45 +2,52 @@
#include "config.h"
#include <SPI.h>
#include <SD.h>
#include <list>
#include <map>
#include <MCP23S17/MCP23S17.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
#define XRESET PIN_VS1053_XRESET
#define DREQ PIN_VS1053_DREQ
#define XCS PIN_VS1053_XCS
#define XDCS PIN_VS1053_XDCS
class Player {
private:
enum state { uninitialized, idle, playing, stopping,
system_sound_while_playing, system_sound_while_stopped };
struct album_state {
uint8_t index;
uint32_t position;
};
void _check_system_sound(String filename);
sleeping, recording };
void _reset();
void _init();
void _wait();
uint16_t _read_control_register(uint8_t address);
void _write_control_register(uint8_t address, uint16_t value);
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;
@ -49,44 +56,49 @@ private:
void _flush_and_cancel();
int8_t _get_endbyte();
void _flush(uint count, int8_t fill_byte);
void _set_last_track(const char* album, uint8_t track, uint32_t position);
std::map<String, album_state> _last_tracks;
void _play_file(String filename, uint32_t offset);
uint32_t _id3_tag_offset(File f);
void _finish_playing();
void _finish_stopping();
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;
std::list<String> _files_in_dir(String dir);
String _find_album_dir(String album);
File _file;
DataSource* _file;
uint32_t _file_size = 0;
uint8_t _buffer[32];
String _playing_album;
uint8_t _playing_index;
uint8_t _playing_album_songs;
uint32_t _current_play_position;
uint32_t _current_play_position = 0;
Playlist* _current_playlist = NULL;
uint _refills;
uint8_t _volume;
uint16_t _stop_delay;
uint32_t _skip_to;
MCP* _mcp;
SPIMaster* _spi;
Controller* _controller;
unsigned long _stopped_at;
public:
Player(MCP* m);
Player(SPIMaster* s);
void init();
void register_controller(Controller* c);
void vol_up();
void vol_down();
void track_next();
void track_prev();
bool play_album(String album);
bool play_song(String album, uint8_t song_index, uint32_t offset=0);
void play_system_sound(String filename);
void stop();
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);
std::list<String> ls(String path);
String position_json();
String json();
};

47
include/playlist.h Normal file
View File

@ -0,0 +1,47 @@
#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; }
};
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);
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);
};

View File

@ -0,0 +1,23 @@
#pragma once
#include <map>
#include <vector>
#include "playlist.h"
class PlaylistManager {
private:
std::map<String, String> _map;
std::map<String, Playlist*> _playlists;
std::vector<String> _unmapped_folders;
void _check_for_special_chars(String s);
void _save_mapping();
public:
PlaylistManager();
Playlist* get_playlist_for_id(String id);
Playlist* get_playlist_for_folder(String folder);
void dump_ids();
void scan_files();
String json();
bool add_mapping(String id, String folder);
String create_mapping_txt();
};

View File

@ -6,30 +6,65 @@
class SPIMaster {
public:
static uint8_t state;
static void init() {
SPI.setHwCs(false);
pinMode(PIN_SD_CS, OUTPUT);
pinMode(PIN_VS1053_XCS, OUTPUT);
pinMode(PIN_VS1053_XDCS, OUTPUT);
pinMode(PIN_MCP, OUTPUT);
}
static void enable(uint8_t pin) {
digitalWrite(PIN_SD_CS, pin==PIN_SD_CS ? LOW : HIGH);
digitalWrite(PIN_VS1053_XCS, pin==PIN_VS1053_XCS ? LOW : HIGH);
digitalWrite(PIN_VS1053_XDCS, pin==PIN_VS1053_XDCS ? LOW : HIGH);
digitalWrite(PIN_MCP, pin==PIN_MCP ? LOW : HIGH);
PIN_SD_CS_SETUP();
PIN_VS1053_XCS_SETUP();
PIN_VS1053_XDCS_SETUP();
PIN_RC522_CS_SETUP();
disable();
}
static void printStatus() {
Serial.printf("CS state: SD:%d, VS1053_XCS:%d, VS1053_XDCS:%d, MCP:%d\n",
digitalRead(PIN_SD_CS),
digitalRead(PIN_VS1053_XCS),
digitalRead(PIN_VS1053_XDCS),
digitalRead(PIN_MCP));
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() {
enable(142);
PIN_SD_CS(HIGH);
PIN_VS1053_XCS(HIGH);
PIN_VS1053_XDCS(HIGH);
PIN_RC522_CS(HIGH);
state = 0;
}
};
};

View File

@ -8,11 +8,16 @@
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp12e]
platform = espressif8266
board = esp12e
[env:esp32]
platform = espressif32
board = esp-wrover-kit
framework = arduino
upload_speed = 512000
lib_deps = 63
https://github.com/n0mjs710/MCP23S17.git
upload_port = /dev/cu.wchusbserial1420
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

View File

@ -1,41 +1,60 @@
#include "controller.h"
#include "spi_master.h"
#include "config.h"
#include "playlist.h"
#include "http_server.h"
#include <ArduinoJson.h>
Controller::Controller(Player* p, MCP* m) {
_player = p;
_mcp = m;
_rfid = new MFRC522(PIN_RC522_CS, MFRC522::UNUSED_PIN);
Controller::Controller(Player* p, PlaylistManager* playlist_manager) {
player = p;
pm = playlist_manager;
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
player->register_controller(this);
SPIMaster::enable(PIN_MCP);
_mcp->pinMode(1, INPUT); _mcp->pullupMode(1, HIGH);
_mcp->pinMode(2, INPUT); _mcp->pullupMode(2, HIGH);
_mcp->pinMode(3, INPUT); _mcp->pullupMode(3, HIGH);
_mcp->pinMode(4, INPUT); _mcp->pullupMode(4, HIGH);
BTN_NEXT_SETUP();
BTN_PREV_SETUP();
BTN_VOL_UP_SETUP();
BTN_VOL_DOWN_SETUP();
SPIMaster::enable(PIN_RC522_CS);
DEBUG("Initializing RC522...");
SPIMaster::select_rc522();
DEBUG("Initializing RC522...\n");
_rfid->PCD_Init();
#ifdef SHOW_DEBUG
_rfid->PCD_DumpVersionToSerial();
#endif
SPIMaster::disable();
SPIMaster::select_rc522(false);
INFO("RC522 initialized.\n");
for (uint8_t i=0; i<NUM_BUTTONS; i++) _button_last_pressed_at[i]=0;
}
void Controller::register_http_server(HTTPServer* h) {
_http_server = h;
}
void Controller::loop() {
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::enable(PIN_RC522_CS);
SPIMaster::select_rc522();
if (!_rfid->PICC_ReadCardSerial()) {
if (!_rfid->PICC_IsNewCardPresent()) {
return 0;
@ -44,33 +63,179 @@ uint32_t Controller::_get_rfid_card_uid() {
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];
SPIMaster::disable();
return uid;
}
void Controller::_check_rfid() {
uint32_t uid = _get_rfid_card_uid();
if (uid != _last_rfid_card_uid) {
if (uid > 0) {
INFO("New RFID card uid: %08x\n", uid);
String s_uid = String(uid, HEX);
_player->play_album(s_uid);
} else {
INFO("No more RFID card.");
_player->stop();
TRACE("check_rfid running...\n");
MFRC522::StatusCode status;
if (_rfid_present) {
byte buffer[2];
byte buffer_size = 2;
SPIMaster::select_rc522();
status = _rfid->PICC_WakeupA(buffer, &buffer_size);
if (status == MFRC522::STATUS_OK) {
// Card is still present.
_rfid->PICC_HaltA();
SPIMaster::select_rc522(false);
return;
}
SPIMaster::select_rc522(false);
// Card is now gone
_rfid_present = false;
INFO("No more RFID card.\n");
if (_state != LOCKED) {
player->stop();
}
send_controller_status();
} else {
uint32_t uid = _get_rfid_card_uid();
if (uid > 0) {
String temp = String(uid, HEX);
String s_uid = "";
for (int i=0; i<(8-temp.length()); i++) {
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;
Playlist* pl = pm->get_playlist_for_id(s_uid);
if (data.indexOf("[lock]") != -1) {
if (_state == LOCKED) {
_state = NORMAL;
DEBUG("ControllerState is now UNLOCKED\n");
} else {
DEBUG("ControllerState is now LOCKING\n");
_state = LOCKING;
}
}
if (pl==NULL) {
INFO("Could not find album for id '%s'.\n", s_uid.c_str());
send_controller_status();
return;
}
int index;
if (data.indexOf("[advent]") != -1 && pl->is_fresh()) {
struct tm time;
getLocalTime(&time);
if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december.
pl->advent_shuffle(time.tm_mday);
} else {
// 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();
}
_last_rfid_card_uid = uid;
}
}
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;
MFRC522::StatusCode status;
for (int i=0; i<8; i++) {
MFRC522::MIFARE_Key *k = &keys[i];
TRACE("Trying MIFARE key %02X %02X %02X %02X %02X %02X...\n", k->keyByte[0], k->keyByte[1], k->keyByte[2], k->keyByte[3], k->keyByte[4], k->keyByte[5]);
status = _rfid->PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, block_offset, k, &_rfid->uid);
if (status == MFRC522::STATUS_OK) {
TRACE("Authentication succeeded with key #%d\n", i);
good_key_index = i;
break;
}
}
if (good_key_index == -1) {
TRACE("Could not find a valid MIFARE key.\n");
} else {
for (uint8_t block=0; block<blocks-1; block++) {
byte buffer[18];
uint8_t byte_count = 18;
status = _rfid->MIFARE_Read(block_offset + block, buffer, &byte_count);
if (status != MFRC522::STATUS_OK) {
DEBUG("MIFARE_Read() failed: %s\n", String(_rfid->GetStatusCodeName(status)).c_str());
continue;
}
for (int i=0; i<16; i++) {
if (buffer[i]>=0x20 && buffer[i]<0x7F) data.concat((char)buffer[i]);
}
}
}
}
_rfid->PICC_HaltA();
_rfid->PCD_StopCrypto1();
DEBUG("Data from RFID: %s\n", data.c_str());
SPIMaster::select_rc522(false);
return data;
}
void Controller::_check_serial() {
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) {
_execute_serial_command(_serial_buffer);
process_message(_serial_buffer);
_serial_buffer = String();
}
} else {
@ -79,90 +244,176 @@ void Controller::_check_serial() {
}
}
void Controller::_execute_serial_command(String cmd) {
DEBUG("Executing command: %s", cmd.c_str());
bool Controller::process_message(String cmd) {
DEBUG("Executing command: %s\n", cmd.c_str());
if (cmd.equals("ls")) {
_execute_command_ls("/");
} else if (cmd.startsWith("ls ")) {
_execute_command_ls(cmd.substring(3));
} else if (cmd.startsWith("play ")) {
_player->play_album(cmd.substring(5));
} else if (cmd.startsWith("sys ")) {
_player->play_system_sound(cmd.substring(4));
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();
} else if (cmd.equals("stop")) {
_player->stop();
player->stop();
} else if (cmd.equals("help")) {
_execute_command_help();
} else if (cmd.equals("-")) {
_player->vol_down();
player->vol_down();
} else if (cmd.equals("+")) {
_player->vol_up();
} else if (cmd.equals("p")) {
_player->track_prev();
} else if (cmd.equals("n")) {
_player->track_next();
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();
} else {
ERROR("Unknown command: %s\n", cmd.c_str());
return false;
}
return;
return true;
}
void Controller::_execute_command_ls(String path) {
INFO("Listing contents of %s:\n", path.c_str());
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());
}
// 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(" 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(" sys [file]- Plays the file as system sound\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() {
SPIMaster::enable(PIN_MCP);
SPI.beginTransaction(SPISettings(250000, MSBFIRST, SPI_MODE0));
/*if (millis()%100==0) {
Serial.printf("Buttons: %d %d %d %d\n", _mcp->digitalRead(1), _mcp->digitalRead(2), _mcp->digitalRead(3), _mcp->digitalRead(4));
}*/
if (_check_button(0)) {
_player->track_prev();
} else if (_check_button(1)) {
_player->vol_up();
} else if (_check_button(2)) {
_player->vol_down();
} else if (_check_button(3)) {
_player->track_next();
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");
}
} 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");
}
}
SPI.endTransaction();
SPIMaster::disable();
}
bool Controller::_check_button(uint8_t index) {
if (index >= NUM_BUTTONS) return false;
bool Controller::_debounce_button(uint8_t index) {
bool ret = false;
uint8_t sum = 0;
while (1) {
sum = 0;
for (int i=0; i<8; i++) {
sum += _mcp->digitalRead(index + 1) == HIGH ? 1 : 0;
}
if (sum==0 || sum==8) break;
}
if (sum == 0) {
if (_button_last_pressed_at[index] + DEBOUNCE_MILLIS < millis()) {
DEBUG("Button %d pressed.\n", index);
ret = true;
}
_button_last_pressed_at[index] = millis();
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::send_playlist_manager_status() {
TRACE("In send_playlist_manager_status()...\n");
if (_http_server->ws->count() > 0) {
_http_server->ws->textAll(pm->json());
}
}
void Controller::send_position() {
TRACE("In send_position()...\n");
if (_http_server->ws->count() > 0) {
_http_server->ws->textAll(player->position_json());
}
_last_position_info_at = millis();
}
void Controller::send_controller_status() {
TRACE("In send_controller_status()...\n");
if (_http_server->ws->count() > 0) {
_http_server->ws->textAll(json());
}
}
void Controller::inform_new_client(AsyncWebSocketClient* client) {
String s;
s += pm->json();
s += '\n';
s += player->json();
s += '\n';
s += player->position_json();
s += '\n';
s += json();
client->text(s);
}
void Controller::queue_command(String s) {
DEBUG("Enqeueing command '%s'.\n", s.c_str());
_cmd_queue = s;
}
void Controller::update_playlist_manager() {
pm->scan_files();
send_playlist_manager_status();
}

57
src/data_sources.cpp Normal file
View File

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

210
src/http_client_wrapper.cpp Normal file
View File

@ -0,0 +1,210 @@
#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");
}

144
src/http_server.cpp Normal file
View File

@ -0,0 +1,144 @@
#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,42 +1,123 @@
#include <Arduino.h>
#include <SPI.h>
#include <SD.h>
#include <MCP23S17/MCP23S17.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;
MCP* mcp;
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 MCP...\n");
SPIMaster::enable(PIN_MCP);
mcp = new MCP(0, PIN_MCP);
INFO("MCP initialized.");
DEBUG("Setting up SD card...\n");
SPIMaster::enable(PIN_SD_CS);
if (SD.begin(PIN_SD_CS)) {
spi->select_sd();
if (SD.begin(42, SPI, 25000000)) {
INFO("SD card initialized.\n");
} else {
ERROR("Could not initialize SD card. Halting.\n");
while(1);
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");
}
player = new Player(mcp);
controller = new Controller(player, mcp);
INFO("Initialization completed.\n");
}

View File

@ -2,43 +2,53 @@
#include "player.h"
#include "spi_master.h"
#include <ArduinoJson.h>
//Player::_spi_settings
Player::Player(MCP* m) {
_mcp = m;
_mcp->pinMode(XRESET, OUTPUT);
_mcp->digitalWrite(XRESET, HIGH);
pinMode(DREQ, INPUT);
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();
}
_init();
void Player::register_controller(Controller* c) {
_controller = c;
}
void Player::_reset() {
_mcp->digitalWrite(XRESET, LOW);
PIN_VS1053_XRESET(LOW);
delay(100);
_mcp->digitalWrite(XRESET, HIGH);
PIN_VS1053_XRESET(HIGH);
delay(100);
_state = uninitialized;
_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow
}
void Player::_init() {
SPIMaster::disable();
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.\n", result);
return;
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.\n", result);
return;
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);
@ -46,48 +56,102 @@ void Player::_init() {
DEBUG("VS1053 Init looking good.\n");
DEBUG("Upping VS1053 multiplier...\n");
_write_control_register(SCI_CLOCKF, 0x6000);
_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 != 0x6000) {
ERROR("Error: SCI_CLOCKF was 0x%04X, expected was 0x6000.\n", result);
return;
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");
INFO("Checking system sounds...\n");
SPIMaster::enable(PIN_SD_CS);
_check_system_sound("no_prev_song.mp3");
_check_system_sound("no_next_song.mp3");
_check_system_sound("volume_max.mp3");
_check_system_sound("volume_min.mp3");
_state = idle;
}
void Player::_check_system_sound(String filename) {
String path = String("/system/") + filename;
if (!SD.exists(path)) {
ERROR("System sound %s is missing on the sd card!\n", path.c_str());
} else {
DEBUG("%s found.\n", path.c_str());
}
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(!digitalRead(DREQ));
while(!PIN_VS1053_DREQ());
}
uint16_t Player::_read_control_register(uint8_t address) {
_wait();
SPIMaster::enable(XCS);
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);
@ -96,34 +160,268 @@ uint16_t Player::_read_control_register(uint8_t address) {
uint8_t b2 = SPI.transfer(0xFF);
_wait();
SPI.endTransaction();
SPIMaster::disable();
_spi->select_vs1053_xcs(false);
return (b1 << 8) | b2;
}
void Player::_write_control_register(uint8_t address, uint16_t value) {
uint8_t b1 = value >> 8;
uint8_t b2 = value & 0xFF;
void Player::_write_control_register(uint8_t address, uint16_t value, bool do_wait) {
_wait();
SPIMaster::enable(XCS);
_spi->select_vs1053_xcs();
SPI.beginTransaction(*_spi_settings);
SPI.transfer(CMD_WRITE);
SPI.transfer(address);
SPI.transfer(b1);
SPI.transfer(b2);
_wait();
SPI.transfer(value >> 8);
SPI.transfer(value & 0xFF);
SPI.endTransaction();
SPIMaster::disable();
_spi->select_vs1053_xcs(false);
if (do_wait) _wait();
}
void Player::_patch_adpcm() {
static const uint16_t patch_data[] = {
0x0007, 0x0001, 0xc01a, 0x0006, 0x0001, 0x0002, 0x0007, 0x0001, /* 0 */
0x0008, 0x0006, 0x8002, 0x0000, 0x0007, 0x0001, 0x000c, 0x0006, /* 8 */
0x0002, 0x7000, 0x0017, 0x0007, 0x0001, 0x8034, 0x0006, 0x0022, /* 10 */
0x0030, 0x0490, 0xb080, 0x0024, 0x3800, 0x0024, 0x0000, 0x1090, /* 18 */
0xf400, 0x5404, 0x0000, 0x0851, 0xf400, 0x5648, 0xf400, 0x5404, /* 20 */
0xf400, 0x5658, 0xf400, 0x5404, 0xf400, 0x5640, 0x0000, 0x800a, /* 28 */
0x2900, 0x9180, 0x0006, 0x2016, 0x2a00, 0x1bce, 0x2a00, 0x114e, /* 30 */
0x2a00, 0x168e, 0x0007, 0x0001, 0x1800, 0x0006, 0x8006, 0x0000, /* 38 */
0x0007, 0x0001, 0x8045, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, /* 40 */
0x7808, 0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, /* 48 */
0x7800, 0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x3e80, 0x3e10, /* 50 */
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, /* 58 */
0x4024, 0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, /* 60 */
0x8024, 0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, /* 68 */
0x0001, 0x805a, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, 0x7808, /* 70 */
0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, 0x7800, /* 78 */
0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x5e40, 0x3e10, 0x7800, /* 80 */
0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, 0x4024, /* 88 */
0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, /* 90 */
0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, /* 98 */
0x806f, 0x0006, 0x0030, 0x3e12, 0xb817, 0x3e12, 0x7808, 0x3e18, /* a0 */
0x3821, 0x3e18, 0xb823, 0x3e10, 0x7800, 0xb880, 0x3855, 0x0030, /* a8 */
0x0497, 0x48b2, 0x3c00, 0x0000, 0x800a, 0x2900, 0x7300, 0x3e10, /* b0 */
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x6890, 0x1bd5, 0x0030, /* b8 */
0x0497, 0x3f00, 0x0024, 0x36f0, 0x5800, 0x36f8, 0x9823, 0x36f8, /* c0 */
0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, 0x0030, 0x0717, 0x2100, /* c8 */
0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, 0x8010, 0x0006, 0x000e, /* d0 */
0x3e02, 0x8024, 0x0001, 0x000a, 0x6012, 0x0024, 0xfea2, 0x0024, /* d8 */
0x48b2, 0x1bca, 0x2000, 0x0000, 0x4180, 0x0024, 0x0007, 0x0001, /* e0 */
0x8087, 0x0006, 0x00e6, 0x3e00, 0x7843, 0x3e01, 0x3845, 0x3e04, /* e8 */
0x3812, 0x0006, 0x08d0, 0x3000, 0x4024, 0x6182, 0x0024, 0x0030, /* f0 */
0x06d0, 0x2800, 0x2655, 0xb882, 0x0024, 0x0000, 0x0201, 0x0000, /* f8 */
0x0005, 0x0030, 0x0210, 0xa016, 0x4004, 0x1fff, 0xfe01, 0xae1a, /* 100 */
0x0024, 0xc342, 0x0024, 0xb882, 0x2001, 0x0030, 0x06d0, 0x3800, /* 108 */
0x4024, 0x0006, 0x0890, 0x3004, 0x0024, 0x3000, 0x4024, 0x0006, /* 110 */
0x12d0, 0x6182, 0x0024, 0x3000, 0x4024, 0x2800, 0x3e05, 0xf400, /* 118 */
0x4050, 0x3009, 0x2000, 0x0006, 0x08d0, 0x0006, 0x0892, 0x3000, /* 120 */
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x0030, 0x0250, 0xb882, /* 128 */
0x2001, 0x0030, 0x0710, 0x3800, 0x4024, 0x0006, 0x12d0, 0x3000, /* 130 */
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x3204, 0x0024, 0x3023, /* 138 */
0x0024, 0x30e0, 0xc024, 0x6312, 0x0024, 0x0000, 0x00c3, 0x2800, /* 140 */
0x3141, 0x0000, 0x0024, 0x3033, 0x0024, 0x3a04, 0x0024, 0x3000, /* 148 */
0x4024, 0x6182, 0x0024, 0x0006, 0x0890, 0x2800, 0x2fd8, 0x0006, /* 150 */
0x0301, 0x3a00, 0x4024, 0x0000, 0x00c3, 0x3004, 0x0024, 0x3013, /* 158 */
0x0024, 0x3000, 0x4024, 0x0006, 0x12d0, 0x3800, 0x4024, 0x0030, /* 160 */
0x0310, 0xf000, 0x0001, 0x6236, 0x0024, 0x001f, 0xffc3, 0x2800, /* 168 */
0x3395, 0x0000, 0x0024, 0x0000, 0x0203, 0xa132, 0x0024, 0x001f, /* 170 */
0xffc3, 0xb136, 0x0024, 0x6306, 0x0024, 0x0000, 0x0024, 0x2800, /* 178 */
0x3611, 0x0000, 0x0024, 0x0020, 0x0003, 0xb132, 0x0024, 0x0000, /* 180 */
0x0024, 0x2800, 0x3a85, 0x0000, 0x0024, 0x0000, 0x0081, 0xb212, /* 188 */
0x0024, 0x0000, 0x0024, 0x2800, 0x3a05, 0x0000, 0x0024, 0x6892, /* 190 */
0x0024, 0xb212, 0x0024, 0x0000, 0x0005, 0x2800, 0x3c55, 0x0030, /* 198 */
0x0310, 0x0000, 0x3fc1, 0x3000, 0x8024, 0xb214, 0x0024, 0x003f, /* 1a0 */
0xc001, 0xb010, 0x0024, 0xc200, 0x0024, 0x0030, 0x0310, 0x3800, /* 1a8 */
0x0024, 0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x5803, 0x2000, /* 1b0 */
0x0000, 0x0000, 0x0024, 0x0030, 0x0310, 0x0000, 0x0005, 0x003f, /* 1b8 */
0xc001, 0x4088, 0x0002, 0xb214, 0x0024, 0x1fff, 0xfe01, 0xae12, /* 1c0 */
0x0024, 0x2800, 0x3a00, 0xc200, 0x0024, 0x2800, 0x28c0, 0x3800, /* 1c8 */
0x0024, 0x0007, 0x0001, 0x80fa, 0x0006, 0x00fe, 0x3e12, 0x0024, /* 1d0 */
0x3e05, 0xb814, 0x3615, 0x0024, 0x3e00, 0x3841, 0x3e00, 0xb843, /* 1d8 */
0x3e01, 0x3845, 0x3e04, 0x3851, 0x0030, 0x10d0, 0x3e04, 0x8024, /* 1e0 */
0x3010, 0x0024, 0x3000, 0x8024, 0x0006, 0x1190, 0x3000, 0x4024, /* 1e8 */
0x6182, 0x0024, 0x0000, 0x0024, 0x2800, 0x5dd5, 0x0000, 0x0024, /* 1f0 */
0x0030, 0x03d0, 0x0000, 0x00c1, 0x3000, 0xc024, 0xb318, 0x0024, /* 1f8 */
0x6896, 0x0024, 0x6436, 0x0024, 0x0020, 0x0003, 0x2800, 0x59c5, /* 200 */
0x0000, 0x0024, 0x0006, 0x1150, 0x3000, 0x4024, 0x6136, 0x0024, /* 208 */
0x0000, 0x0024, 0x2800, 0x4741, 0x0000, 0x0024, 0x0000, 0x0803, /* 210 */
0x4132, 0x0024, 0x3800, 0x4024, 0x0006, 0x0190, 0x0006, 0xf011, /* 218 */
0x2900, 0xb500, 0x3613, 0x0024, 0x0006, 0xf011, 0x0006, 0x1152, /* 220 */
0x0006, 0x0250, 0x4082, 0x0800, 0xfe82, 0x184c, 0x1fff, 0xfc41, /* 228 */
0x48ba, 0x0024, 0xae1a, 0x0024, 0x2900, 0xb500, 0x4280, 0x4103, /* 230 */
0x0006, 0x1110, 0x4084, 0x0800, 0xfe84, 0x0002, 0x48ba, 0x0024, /* 238 */
0xae12, 0x0024, 0xf400, 0x4001, 0x0000, 0x0180, 0x6200, 0x0024, /* 240 */
0x0000, 0x0080, 0x2800, 0x5241, 0x4200, 0x0024, 0x3800, 0x0024, /* 248 */
0x0006, 0x1090, 0x3004, 0x8024, 0xf400, 0x4491, 0x3113, 0x0024, /* 250 */
0x3804, 0x4024, 0x3a00, 0xc024, 0x3004, 0x8024, 0xf400, 0x4491, /* 258 */
0x3113, 0x0024, 0x3804, 0x4024, 0x3a00, 0x4024, 0x0006, 0x1081, /* 260 */
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x0f00, 0x2800, 0x5248, /* 268 */
0x0000, 0x0024, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0080, /* 270 */
0x3000, 0x4024, 0x0030, 0x0710, 0xb104, 0x0024, 0x0000, 0x0001, /* 278 */
0x3800, 0x4024, 0x0006, 0x08d0, 0x3001, 0x0024, 0x0006, 0x0910, /* 280 */
0x3000, 0x4024, 0x6100, 0x0024, 0x6042, 0x0024, 0x0030, 0x06d0, /* 288 */
0x2800, 0x5711, 0xb880, 0x0024, 0x2900, 0x21c0, 0x4380, 0x184c, /* 290 */
0xb880, 0x0024, 0x3800, 0x0024, 0x36f4, 0x8024, 0x36f4, 0x1811, /* 298 */
0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, /* 2a0 */
0x36f3, 0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, /* 2a8 */
0x0006, 0x1152, 0x0000, 0x0804, 0x3200, 0xc024, 0x6346, 0x0024, /* 2b0 */
0x6386, 0x2803, 0x0000, 0x0024, 0x2800, 0x4755, 0x0000, 0x0024, /* 2b8 */
0x3800, 0x4024, 0x0030, 0x0690, 0x0000, 0x0081, 0xb882, 0x22c1, /* 2c0 */
0x3800, 0x4024, 0x0030, 0x0590, 0x2800, 0x4740, 0x3800, 0x4024, /* 2c8 */
0x2800, 0x5700, 0x4190, 0x0024, 0x0007, 0x0001, 0x8179, 0x0006, /* 2d0 */
0x00a6, 0x3e12, 0x0024, 0x3e05, 0xb814, 0x3625, 0x0024, 0x3e00, /* 2d8 */
0x3841, 0x3e00, 0xb843, 0x3e04, 0x3851, 0x0006, 0x1110, 0x3e04, /* 2e0 */
0xb813, 0x3000, 0x0024, 0x6080, 0x0024, 0x0006, 0x11d2, 0x2800, /* 2e8 */
0x70c5, 0x0000, 0x0081, 0x6010, 0x984c, 0x3800, 0x0024, 0x0006, /* 2f0 */
0x10d0, 0x3200, 0x0024, 0xf100, 0x0011, 0xf100, 0x0024, 0xf102, /* 2f8 */
0x0400, 0x0006, 0x1311, 0x2900, 0x0400, 0x3100, 0x8024, 0x0030, /* 300 */
0x1293, 0x3413, 0x184c, 0x3c04, 0x4024, 0x3b00, 0x0024, 0x3004, /* 308 */
0xc024, 0xf400, 0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3310, /* 310 */
0x0024, 0x3a00, 0x0024, 0x0006, 0x1212, 0x3200, 0x0024, 0xf100, /* 318 */
0x13d1, 0xf100, 0x0402, 0x2900, 0x0400, 0xf102, 0x0c00, 0x0030, /* 320 */
0x12d1, 0x0006, 0x1081, 0x3900, 0x0024, 0x3004, 0xc024, 0xf400, /* 328 */
0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3300, 0x0024, 0x3a00, /* 330 */
0x0024, 0xf400, 0x4440, 0x6010, 0x0024, 0x1fee, 0xe002, 0x2800, /* 338 */
0x6bc8, 0x0006, 0x0f00, 0x3800, 0x0024, 0x0006, 0x0010, 0xb886, /* 340 */
0x0040, 0x30f0, 0x4024, 0x6c92, 0x40c3, 0x3810, 0x0024, 0xb182, /* 348 */
0x23c1, 0x0006, 0x0950, 0x3000, 0x0024, 0x6090, 0x0024, 0x6cd2, /* 350 */
0x2000, 0x0000, 0x0000, 0x2800, 0x70c8, 0x0000, 0x0024, 0x3800, /* 358 */
0x0024, 0x0000, 0x0210, 0x3010, 0x0024, 0x30f0, 0x4024, 0x6c92, /* 360 */
0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, 0x36f4, 0x9813, 0x36f4, /* 368 */
0x1811, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, 0x36f3, /* 370 */
0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, 0x0007, /* 378 */
0x0001, 0x81cc, 0x0006, 0x00f4, 0x3e00, 0x3841, 0x0000, 0x0201, /* 380 */
0x3e00, 0xb843, 0x3e01, 0x3845, 0x3e04, 0x3812, 0x0030, 0x0410, /* 388 */
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x08d0, 0x2800, 0x8045, /* 390 */
0x0000, 0x0181, 0x6012, 0x0024, 0x0006, 0x1250, 0x2800, 0x7e45, /* 398 */
0x0000, 0x05c1, 0x6012, 0x0024, 0x0030, 0x01d0, 0x2800, 0x7c45, /* 3a0 */
0x0000, 0x0581, 0x6010, 0x03cc, 0x0000, 0x0024, 0x2800, 0x7a95, /* 3a8 */
0x0000, 0x0024, 0x3000, 0x8024, 0x0006, 0x1250, 0x3000, 0x0024, /* 3b0 */
0x6092, 0x0024, 0x3800, 0x4024, 0xf400, 0x4010, 0x3800, 0x8024, /* 3b8 */
0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, /* 3c0 */
0x2000, 0x0000, 0x0000, 0x0024, 0x0030, 0x01d0, 0x3000, 0x0024, /* 3c8 */
0x0006, 0x1250, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3d0 */
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3d8 */
0x6090, 0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3e0 */
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3e8 */
0x6080, 0x0024, 0x0000, 0x0024, 0x2800, 0x8515, 0x0000, 0x0024, /* 3f0 */
0x0006, 0x1350, 0x0000, 0x0082, 0x0030, 0x0352, 0xb886, 0x0040, /* 3f8 */
0x30f0, 0x4024, 0x4cd2, 0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, /* 400 */
0x3a00, 0x0024, 0x3010, 0x0024, 0x30f0, 0x4024, 0x0030, 0x0390, /* 408 */
0x2800, 0x7a80, 0x4180, 0x2001, 0x4090, 0x0024, 0x3800, 0x0024, /* 410 */
0x0030, 0x0250, 0x3800, 0x0024, 0x0006, 0x1290, 0x3000, 0x0024, /* 418 */
0x6090, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x8024, /* 420 */
0x3223, 0x0024, 0x32e0, 0x4024, 0x6100, 0x0024, 0x0000, 0x0024, /* 428 */
0x2800, 0x8c81, 0x0000, 0x0024, 0x3233, 0x0024, 0x3804, 0x8024, /* 430 */
0x3200, 0x0024, 0x6080, 0x0024, 0x0006, 0x0300, 0x2800, 0x8b18, /* 438 */
0x0000, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x0024, /* 440 */
0x3013, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, 0x3800, 0x0024, /* 448 */
0x0006, 0x0850, 0x3004, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, /* 450 */
0x6080, 0x0024, 0x3000, 0x0024, 0x2800, 0x9115, 0xf400, 0x4010, /* 458 */
0x3000, 0x0024, 0x0000, 0x0201, 0x0000, 0x0005, 0x0030, 0x0210, /* 460 */
0xa014, 0x4004, 0x1fff, 0xfe01, 0xae12, 0x0024, 0xc200, 0x0024, /* 468 */
0x2800, 0x8180, 0x3800, 0x0024, 0x2800, 0x8ec0, 0x3009, 0x0000, /* 470 */
0x0007, 0x0001, 0x8246, 0x0006, 0x0104, 0x0030, 0x1092, 0x0007, /* 478 */
0x9250, 0x003f, 0xfc42, 0xb880, 0x184c, 0x3e12, 0x0024, 0x3800, /* 480 */
0x0024, 0x0030, 0x0290, 0x38f0, 0x0024, 0x3800, 0x0024, 0x0030, /* 488 */
0x0050, 0x3000, 0x4024, 0xb122, 0x0024, 0x6894, 0x2001, 0x0000, /* 490 */
0x0141, 0x3a70, 0x4024, 0x0004, 0x1fc1, 0x3a00, 0x4024, 0x0030, /* 498 */
0x00d2, 0x0030, 0x0001, 0x3a00, 0x4024, 0x0030, 0x0552, 0x3a10, /* 4a0 */
0x0024, 0x3a00, 0x0024, 0x3000, 0x4024, 0xc122, 0x0024, 0x3800, /* 4a8 */
0x4024, 0x0030, 0x05d0, 0x0000, 0x03c1, 0x3820, 0x4024, 0x3800, /* 4b0 */
0x0024, 0x0000, 0x0310, 0x3010, 0x0024, 0x30f0, 0x4024, 0xf2c2, /* 4b8 */
0x0024, 0x3810, 0x0024, 0x0000, 0x3fc0, 0x38f0, 0x4024, 0x0030, /* 4c0 */
0x02d0, 0x3000, 0x4024, 0x2912, 0x1400, 0xb104, 0x0024, 0x0006, /* 4c8 */
0x1312, 0x6802, 0x0024, 0x000d, 0xac00, 0x6012, 0x2801, 0x0000, /* 4d0 */
0x0024, 0x2800, 0x9dc1, 0x0000, 0x0024, 0x3a00, 0x0024, 0x2909, /* 4d8 */
0x1b40, 0x3613, 0x0024, 0x0000, 0x0084, 0x0000, 0x1905, 0x2908, /* 4e0 */
0xbe80, 0x3613, 0x0024, 0x0000, 0x0000, 0x0006, 0x0302, 0x4002, /* 4e8 */
0x0024, 0x4012, 0x0024, 0x4212, 0x0024, 0xf400, 0x4050, 0x3000, /* 4f0 */
0x4024, 0x6182, 0x0024, 0x0006, 0x0350, 0x2800, 0xa6c8, 0x0000, /* 4f8 */
0x0024, 0x4002, 0x0024, 0x4014, 0x0024, 0x0006, 0x0301, 0x4124, /* 500 */
0x0024, 0x0000, 0x0081, 0x4212, 0x0024, 0x4002, 0x4050, 0x4014, /* 508 */
0x0003, 0x0006, 0x0301, 0x4122, 0x0024, 0x6192, 0x0024, 0x6090, /* 510 */
0x4050, 0x3000, 0x4024, 0x0006, 0x0910, 0x6312, 0x0024, 0x6194, /* 518 */
0x0001, 0x4122, 0x0024, 0x2800, 0x9f80, 0x3800, 0x4024, 0x0006, /* 520 */
0x12d2, 0x0006, 0x0991, 0x3000, 0x0024, 0x0006, 0x1290, 0x3a00, /* 528 */
0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x2900, 0xb200, 0x0000, /* 530 */
0x0580, 0x0030, 0x0210, 0x0014, 0x9240, 0x003f, 0xf502, 0x003f, /* 538 */
0xffc3, 0x3800, 0x0024, 0x0000, 0x0580, 0x0006, 0x1350, 0x3200, /* 540 */
0x4024, 0x4102, 0x0024, 0x3a00, 0x4024, 0x3810, 0x8024, 0x38f0, /* 548 */
0xc024, 0x0006, 0x08d0, 0x3800, 0x0024, 0x0030, 0x0690, 0x0000, /* 550 */
0x8280, 0xb880, 0x2080, 0x3800, 0x0024, 0x6890, 0x2000, 0x0030, /* 558 */
0x0490, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0100, 0x3000, /* 560 */
0x584c, 0xb100, 0x0024, 0x0000, 0x0024, 0x2800, 0xb185, 0x0000, /* 568 */
0x0024, 0x003f, 0xfec1, 0x3000, 0x1bcc, 0xb010, 0x0024, 0x2908, /* 570 */
0x0b80, 0x3800, 0x0024, 0x3613, 0x0024, 0x2910, 0x0180, 0x0000, /* 578 */
0xae48, 0x0007, 0x0001, 0x1806, 0x0006, 0x8007, 0x0000, 0x0006, /* 580 */
0x002f, 0x0010, 0x17ff, 0x0000, 0x1a00, 0x1dff, 0x0000, 0x1f00, /* 588 */
0x3fff, 0x0001, 0x0000, 0x17ff, 0x0001, 0x1c00, 0x3fff, 0x0001, /* 590 */
0xe000, 0xfffd, 0xffff, 0x0000, 0x0000, 0x180c, 0x180c, 0x0000, /* 598 */
0x0000, 0x0000, 0x4952, 0x4646, 0xffff, 0xffff, 0x4157, 0x4556, /* 5a0 */
0x6d66, 0x2074, 0x0010, 0x0000, 0x0001, 0x0001, 0xbb80, 0x0000, /* 5a8 */
0x7700, 0x0001, 0x0002, 0x0010, 0x6164, 0x6174, 0xffff, 0xffff, /* 5b0 */
0x0006, 0x8006, 0x0000, 0x0006, 0x0005, 0x183c, 0x183c, 0x0000, /* 5b8 */
0x0020, 0x0040, 0x0006, 0x8003, 0x0000, 0x0007, 0x0001, 0x5bc0, /* 5c0 */
0x0006, 0x0009, 0x801c, 0x7fe4, 0x8039, 0x804e, 0x7fb2, 0x809d, /* 5c8 */
0x809c, 0x7f64, 0x8139, 0x0007, 0x0001, 0x82c8, 0x0006, 0x0018, /* 5d0 */
0x4080, 0x184c, 0x3e13, 0x780f, 0x2800, 0xb405, 0x4090, 0x380e, /* 5d8 */
0x2400, 0xb3c0, 0xf400, 0x4417, 0x3110, 0x0024, 0x3f10, 0x0024, /* 5e0 */
0x36f3, 0x8024, 0x36f3, 0x580f, 0x2000, 0x0000, 0x0000, 0x0024, /* 5e8 */
0x0007, 0x0001, 0x82d4, 0x0006, 0x002a, 0x3e11, 0xb807, 0x3009, /* 5f0 */
0x384a, 0x3e11, 0x3805, 0x3e10, 0xb803, 0x3e00, 0x4442, 0x0001, /* 5f8 */
0x800a, 0xbf8e, 0x8443, 0xfe06, 0x0045, 0x3011, 0x0401, 0x545e, /* 600 */
0x0385, 0x525e, 0x2040, 0x72ce, 0x1bc1, 0x48ba, 0x9803, 0x4588, /* 608 */
0x4885, 0x6fee, 0x1bc2, 0x4ffe, 0x9805, 0xf6fe, 0x1bc4, 0xf7f0, /* 610 */
0x2046, 0x3801, 0xdbca, 0x2000, 0x0000, 0x36f1, 0x9807,
};
const uint16_t patch_size = 1567;
DEBUG("Patching...\n");
_spi->select_vs1053_xcs();
SPI.beginTransaction(*_spi_settings);
for (int i=0; i<patch_size; i++) {
DEBUG(" %d\n", i);
unsigned short addr, n, val;
addr = patch_data[i++];
n = patch_data[i++];
SPI.transfer(CMD_WRITE);
SPI.transfer(addr & 0XFF);
if (n & 0x8000U) { /* RLE run, replicate n samples */
n &= 0x7FFF;
val = patch_data[i++];
while (n--) {
SPI.transfer(val >> 8);
SPI.transfer(val & 0xFF);
_wait();
}
} else { /* Copy run, copy n samples */
while (n--) {
val = patch_data[i++];
SPI.transfer(val >> 8);
SPI.transfer(val & 0xFF);
_wait();
}
}
}
SPI.endTransaction();
_spi->select_vs1053_xcs(false);
DEBUG("Patch sent.\n");
}
void Player::_write_data(uint8_t* buffer) {
SPIMaster::enable(XDCS);
_spi->select_vs1053_xdcs();
SPI.beginTransaction(*_spi_settings);
for (uint i=0; i<sizeof(_buffer); i++) {
SPI.transfer(_buffer[i]);
}
SPI.endTransaction();
SPIMaster::disable();
_spi->select_vs1053_xdcs(false);
}
uint16_t Player::_read_wram(uint16_t address) {
@ -159,174 +457,108 @@ void Player::set_volume(uint8_t vol, bool save) {
}
INFO("Setting volume to %d\n", vol);
vol = 0xFF - vol;
if (vol==0xFF) vol=0xFE;
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 (_volume == VOLUME_MAX) play_system_sound("volume_max.mp3");
else if (_volume + VOLUME_STEP > VOLUME_MAX) set_volume(VOLUME_MAX);
else set_volume(_volume + VOLUME_STEP);
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 (_volume >= VOLUME_MIN + VOLUME_STEP) set_volume(_volume - VOLUME_STEP);
else if (_volume == VOLUME_MIN) play_system_sound("volume_min.mp3");
else set_volume(VOLUME_MIN);
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.");
set_volume(0, false);
INFO("Muting.\n");
_speaker_off();
set_volume(1, false);
}
void Player::_unmute() {
INFO("Unmuting.");
INFO("Unmuting.\n");
set_volume(_volume, false);
_speaker_on();
}
void Player::track_next() {
if (_state != playing) return;
if (_playing_index + 1 >= _playing_album_songs) {
play_system_sound("no_next_song.mp3");
if (!_current_playlist->has_track_next()) {
return;
}
stop();
play_song(_playing_album, _playing_index + 1);
_current_playlist->track_next();
play();
}
void Player::track_prev() {
if (_state != playing) return;
if (_current_play_position > 100000) {
stop();
play_song(_playing_album, _playing_index);
_current_playlist->track_restart();
play();
} else {
if (_playing_index == 0) {
play_system_sound("no_prev_song.mp3");
if (!_current_playlist->has_track_prev()) {
return;
}
stop();
play_song(_playing_album, _playing_index - 1);
_current_playlist->track_prev();
play();
}
}
std::list<String> Player::ls(String path) {
SPIMaster::enable(PIN_SD_CS);
std::list<String> result;
if (!SD.exists(path)) return result;
File dir = SD.open(path);
File entry;
while (entry = dir.openNextFile()) {
String filename = entry.name();
if (entry.isDirectory()) filename.concat("/");
result.push_back(filename);
}
return result;
void Player::set_track(uint8_t id) {
stop();
_current_playlist->set_track(id);
play();
}
String Player::_find_album_dir(String id) {
String id_with_divider = id + " - ";
File root = SD.open("/");
File entry;
String result = String("");
while ((result.length()==0) && (entry = root.openNextFile())) {
String name = entry.name();
if (entry.isDirectory() && (name.startsWith(id_with_divider) || name.equals(id))) {
result = name;
}
entry.close();
}
root.close();
return result;
bool Player::is_playing() {
return _state == playing;
}
std::list<String> Player::_files_in_dir(String path) {
DEBUG("Examining folder %s...\n", path.c_str());
if (!path.startsWith("/")) path = String("/") + path;
if (!path.endsWith("/")) path.concat("/");
std::list<String> result;
if (!SD.exists(path)) return result;
File dir = SD.open(path);
File entry;
while (entry = dir.openNextFile()) {
String filename = entry.name();
if (!entry.isDirectory() &&
!filename.startsWith(".") &&
( filename.endsWith(".mp3") ||
filename.endsWith(".ogg") ||
filename.endsWith(".wma") ||
filename.endsWith(".mp4") ||
filename.endsWith(".mpa"))) {
DEBUG(" Adding entry %s\n", filename.c_str());
result.push_back(path + filename);
} else {
DEBUG(" Ignoring entry %s\n", filename.c_str());
}
entry.close();
}
dir.close();
result.sort();
return result;
bool Player::play(Playlist* p) {
_current_playlist = p;
return play();
}
bool Player::play_album(String album) {
//if (_state==playing) stop();
album_state s = _last_tracks[album.c_str()];
DEBUG("Last index for album %s was %d,%d\n", album.c_str(), s.index, s.position);
return play_song(album, s.index, s.position);
}
bool Player::play_song(String album, uint8_t index, uint32_t skip_to) {
bool Player::play() {
if (_state == sleeping || _state == recording) _wakeup();
if (_state != idle) return false;
DEBUG("Trying to play song at index %d, offset %d of album %s\n", index, skip_to, album.c_str());
String path = _find_album_dir(album);
if (path.length()==0) {
ERROR("Could not find album.\n");
return false;
} else {
INFO("Found album in dir '%s'.\n", path.c_str());
}
std::list<String> files = _files_in_dir(path);
_playing_album_songs = files.size();
DEBUG("Found %d songs in album\n", files.size());
if (index >= files.size()) {
ERROR("No matching file found - not playing.\n");
return false;
}
//std::list<String>::iterator it = files.begin();
//std::advance(it, index);
String file = *(std::next(files.begin(), index));
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;
_playing_album = album;
_playing_index = index;
_set_last_track(album.c_str(), index, skip_to);
_play_file(file, skip_to);
_play_file(file, position);
_controller->send_player_status();
return true;
}
void Player::play_system_sound(String filename) {
//String file = String("/system/") + filename;
String file = filename;
if (!SD.exists(file)) {
ERROR("File %s does not exist!\n", file.c_str());
return;
}
if (_state == playing) {
stop();
_state = system_sound_while_playing;
} else {
_state = system_sound_while_stopped;
}
_play_file(file, 0);
}
void Player::_play_file(String file, uint32_t file_offset) {
INFO("play_file('%s', %d)\n", file.c_str(), file_offset);
_file = SD.open(file);
if (!_file) return;
_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);
@ -334,48 +566,29 @@ void Player::_play_file(String file, uint32_t file_offset) {
_write_control_register(SCI_STATUS, _read_control_register(SCI_STATUS) & ~SS_DO_NOT_JUMP);
delay(100);
_spi->select_sd();
if (file_offset == 0) {
_file.seek(_id3_tag_offset(_file));
_file->skip_id3_tag();
}
_refills = 0;
_current_play_position = _file.position();
_current_play_position = _file->position();
_spi->select_sd(false);
_skip_to = file_offset;
if (_skip_to>0) _mute();
else _speaker_on();
INFO("Now playing.\n");
}
uint32_t Player::_id3_tag_offset(File f) {
uint32_t original_position = f.position();
uint32_t offset = 0;
if (f.read()=='I' && f.read()=='D' && f.read()=='3') {
DEBUG("ID3 tag found\n");
// Skip ID3 tag version
f.read(); f.read();
byte tags = f.read();
bool footer_present = tags & 0x10;
DEBUG("ID3 footer found: %d\n", footer_present);
for (byte i=0; i<4; i++) {
offset <<= 7;
offset |= (0x7F & f.read());
}
offset += 10;
if (footer_present) offset += 10;
DEBUG("ID3 tag length is %d bytes.\n", offset);
} else {
DEBUG("No ID3 tag found\n");
}
f.seek(original_position);
return offset;
_controller->send_player_status();
}
void Player::_flush(uint count, int8_t byte) {
SPIMaster::enable(XDCS);
_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() {
@ -390,15 +603,14 @@ void Player::_finish_playing() {
// If we reached this, the Chip didn't stop. That should not happen.
// (That's written in the manual.)
// Reset the chip.
_init();
init();
}
void Player::stop() {
if (_state != playing /* && _state != system_sound_while_playing && _state != system_sound_while_stopped*/) return;
void Player::stop(bool turn_speaker_off) {
if (_state != playing) return;
INFO("Stopping...\n");
if (_state == playing) {
_set_last_track(_playing_album.c_str(), _playing_index, (uint32_t)_file.position());
}
_current_playlist->set_position(_current_play_position);
_state = stopping;
_stop_delay = 0;
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
@ -408,7 +620,7 @@ void Player::stop() {
uint16_t mode = _read_control_register(SCI_MODE);
if ((mode & SM_CANCEL) == 0) {
_flush(2052, endbyte);
_finish_stopping();
_finish_stopping(turn_speaker_off);
break;
} else if (_stop_delay > 2048) {
init();
@ -418,51 +630,57 @@ void Player::stop() {
}
}
void Player::_finish_stopping() {
void Player::_finish_stopping(bool turn_speaker_off) {
if (turn_speaker_off) _speaker_off();
_state = idle;
_stopped_at = millis();
if (_file) {
_file.close();
_file->close();
delete _file;
}
_current_play_position = 0;
_file_size = 0;
INFO("Stopped.\n");
_controller->send_player_status();
}
void Player::_refill() {
SPIMaster::enable(PIN_SD_CS);
_spi->select_sd();
_refills++;
if (_refills % 1000 == 0) DEBUG(".");
uint8_t result = _file.read(_buffer, sizeof(_buffer));
uint8_t result = _file->read(_buffer, sizeof(_buffer));
_spi->select_sd(false);
if (result == 0) {
// File is over.
DEBUG("EOF reached.\n");
_skip_to = 0;
_finish_playing();
if (_state == system_sound_while_playing) {
_finish_stopping();
play_album(_playing_album);
return;
} else if (_state == system_sound_while_stopped) {
_finish_stopping();
return;
}
_finish_stopping();
bool result = play_song(_playing_album, _playing_index + 1);
if (!result) {
_set_last_track(_playing_album.c_str(), 0, 0);
_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()) {
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());
_file.seek(_skip_to);
_spi->select_sd();
_file->seek(_skip_to);
_spi->select_sd(false);
_skip_to = 0;
_unmute();
_controller->send_position();
}
} else {
_skip_to = 0;
@ -472,21 +690,61 @@ void Player::_refill() {
}
bool Player::_refill_needed() {
return _state==playing ||
_state==stopping ||
_state==system_sound_while_playing ||
_state==system_sound_while_stopped;
return _state==playing || _state==stopping;
}
bool Player::loop() {
if (digitalRead(DREQ) && _refill_needed()) {
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;
}
void Player::_set_last_track(const char* album, uint8_t index, uint32_t position) {
DEBUG("Setting _last_track[%s]=%d,%d.\n", album, index, position);
_last_tracks[album] = {index, position};
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>();
}

347
src/playlist.cpp Normal file
View File

@ -0,0 +1,347 @@
#include <playlist.h>
#include "spi_master.h"
#include "config.h"
#include <SD.h>
#include <algorithm>
#include <ArduinoJson.h>
#include <TinyXML.h>
Playlist::Playlist(String path) {
if (path.startsWith("/")) {
_add_path(path);
} else if (path.startsWith("http")) {
_examine_http_url(path);
}
if (_title.length()==0) _title=path;
}
void Playlist::_add_path(String path) {
SPIMaster::select_sd();
TRACE("Examining folder %s...\n", path.c_str());
if (!path.startsWith("/")) path = String("/") + path;
if (!SD.exists(path)) {
DEBUG("Could not open path '%s'.\n", path.c_str());
SPIMaster::select_sd(false);
return;
}
_title = path.substring(1);
int idx = _title.indexOf('/');
if (idx>0) {
_title.remove(idx);
}
File dir = SD.open(path);
File entry;
while (entry = dir.openNextFile()) {
String filename = entry.name();
filename = filename.substring(path.length() + 1);
String ext = filename.substring(filename.length() - 4);
if (!entry.isDirectory() &&
!filename.startsWith(".") &&
( ext.equals(".mp3") ||
ext.equals(".ogg") ||
ext.equals(".wma") ||
ext.equals(".mp4") ||
ext.equals(".mpa"))) {
TRACE(" Adding entry %s\n", entry.name());
String title = filename.substring(0, filename.length() - 4);
_files.push_back({.filename=entry.name(), .title=title});
bool non_ascii_chars = false;
for(int i=0; i<filename.length(); i++) {
char c = filename.charAt(i);
if (c < 0x20 || c >= 0x7F) {
non_ascii_chars = true;
break;
}
}
if (non_ascii_chars) {
ERROR("WARNING: File '%s' contains non-ascii chars!\n", filename.c_str());
}
} else {
TRACE(" Ignoring entry %s\n", filename.c_str());
}
entry.close();
}
dir.close();
SPIMaster::select_sd(false);
std::sort(_files.begin(), _files.end());
}
void Playlist::_examine_http_url(String url) {
HTTPClientWrapper* http = new HTTPClientWrapper();
if (!http->get(url)) {
DEBUG("Could not GET %s.\n", url.c_str());
return;
}
String ct = http->getContentType();
DEBUG("Content-Type is %s.\n", ct.c_str());
if (ct.startsWith("audio/x-mpegurl")) {
_parse_m3u(http);
} else if (ct.startsWith("audio/")) {
_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;
}
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;
}
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::_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);
}
xml_files_ptr = NULL;
if (xml_album_title.length()>0) {
_title = xml_album_title;
}
xml_album_title = "";
delete buffer;
// don't close http at the end
DEBUG("RSS parser finished.\n");
}
void Playlist::_parse_m3u(HTTPClientWrapper* http) {
// http is already initialized
String line = "";
String title = "";
int i;
do {
i = http->read();
char c = i;
if (i>=-1 && c!='\r' && c!='\n') {
line += c;
} else {
if (line.equals("#EXTM3U")) {
// Do nothing
} else if (line.startsWith("#EXTINF")) {
int idx = line.indexOf(",");
if (idx>4) {
// Get the title
title = line.substring(idx+1);
if (_title.length()==0) _title=title;
}
} else if (line.startsWith("http")) {
if (title.length()==0) title = line;
_files.push_back({.filename=line, .title=title});
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;
}
return false;
}
bool Playlist::track_next() {
if (_current_track < _files.size()-1) {
_current_track++;
_position = 0;
return true;
}
return false;
}
bool Playlist::set_track(uint8_t track) {
if (track < _files.size()) {
_current_track = track;
_position = 0;
return true;
}
return false;
}
void Playlist::track_restart() {
_position = 0;
}
void Playlist::shuffle(uint8_t random_offset) {
DEBUG("Shuffling the playlist with an offset of %d...\n", random_offset);
for (int i=random_offset; i<_files.size(); i++) {
int j = random(random_offset, _files.size()-1);
if (i!=j) {
TRACE(" Swapping elements %d and %d.\n", i, j);
PlaylistEntry temp = _files[i];
_files[i] = _files[j];
_files[j] = temp;
}
}
_shuffled = true;
TRACE("Done.\n");
}
void Playlist::advent_shuffle(uint8_t day) {
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();
}

166
src/playlist_manager.cpp Normal file
View File

@ -0,0 +1,166 @@
#include "playlist_manager.h"
#include <SD.h>
#include "spi_master.h"
#include <ArduinoJson.h>
PlaylistManager::PlaylistManager() {
scan_files();
}
void PlaylistManager::scan_files() {
SPIMaster::select_sd();
std::vector<String> folders;
File root = SD.open("/");
File entry;
while (entry = root.openNextFile()) {
String foldername = entry.name();
if (foldername.startsWith("/.")) continue;
foldername.remove(foldername.length());
folders.push_back(foldername);
_check_for_special_chars(foldername);
entry.close();
}
_map.clear();
if (!SD.exists("/_mapping.txt\n")) {
ERROR("WARNING: File /_mapping.txt not found.\n");
} else {
File f = SD.open("/_mapping.txt");
DEBUG("Reading /_mapping.txt...\n");
while (f.available()) {
char buffer[512];
size_t pos = f.readBytesUntil('\n', buffer, 511);
buffer[pos] = '\0';
String data = buffer;
uint8_t eq = data.indexOf('=');
if (eq>0 && eq<data.length()-1) {
String rfid_id = data.substring(0, eq);
String folder = data.substring(eq + 1);
TRACE(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
_map[rfid_id] = folder;
if (folder.charAt(0)=='/') {
bool found=false;
for (String f: folders) {
if (f.equals(folder)) {
found = true;
break;
}
}
if (!found) {
INFO("WARNING: Found mapping for RFID id %s which maps to non-existing folder %s!\n", rfid_id.c_str(), folder.c_str());
}
}
}
}
f.close();
}
root.close();
_unmapped_folders.clear();
for (String folder: folders) {
bool found = false;
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) {
if (it->second.equals(folder)) {
found = true;
break;
}
}
if (!found) {
// Checking folder for media files
File dir = SD.open(folder);
while (entry = dir.openNextFile()) {
String filename = entry.name();
filename = filename.substring(folder.length() + 1);
if (!filename.startsWith(".") && filename.endsWith(".mp3")) {
found = true;
}
entry.close();
if (found) break;
}
if (found) {
_unmapped_folders.push_back(folder);
}
}
}
SPIMaster::select_sd(false);
}
void PlaylistManager::_check_for_special_chars(String s) {
for(int i=0; i<s.length(); i++) {
char c = s.charAt(i);
if (c < 0x20 || c >= 0x7F) {
ERROR("WARNING: Folder / file '%s' contains non-ascii chars!\n", s.c_str());
return;
}
}
}
Playlist* PlaylistManager::get_playlist_for_id(String id) {
if (!_map.count(id)) return NULL;
String folder = _map[id];
return get_playlist_for_folder(folder);
}
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
if (!_playlists.count(folder)) {
_playlists[folder] = new Playlist(folder);
}
return _playlists[folder];
}
void PlaylistManager::dump_ids() {
for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) {
INFO(" %s -> %s\n", it->first.c_str(), it->second.c_str());
}
}
String PlaylistManager::json() {
DynamicJsonDocument json(10240);
json["_type"] = "playlist_manager";
JsonObject folders = json.createNestedObject("folders");
for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) {
folders[it->second] = it->first;
}
JsonArray started = json.createNestedArray("started");
for (std::map<String, Playlist*>::iterator it = _playlists.begin(); it!=_playlists.end(); it++) {
if (it->second->is_fresh()) continue;
started.add(it->first);
}
JsonArray unmapped = json.createNestedArray("unmapped");
for(String folder : _unmapped_folders) {
unmapped.add(folder);
}
return json.as<String>();
}
bool PlaylistManager::add_mapping(String id, String folder) {
DEBUG("Adding mapping: %s=>%s\n", id.c_str(), folder.c_str());
_map[id] = folder;
_save_mapping();
return true;
}
void PlaylistManager::_save_mapping() {
SPIMaster::select_sd();
File f = SD.open("/_mapping.txt", "w");
String s = create_mapping_txt();
unsigned char* buf = new unsigned char[s.length()];
s.getBytes(buf, s.length());
f.write(buf, s.length()-1);
f.close();
SPIMaster::select_sd(false);
delete buf;
}
String PlaylistManager::create_mapping_txt() {
String s;
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) {
s += it->first;
s += "=";
s += it->second;
s += '\n';
}
return s;
}