Compare commits
No commits in common. "master" and "feature-webstreams" have entirely different histories.
master
...
feature-we
151
README.md
151
README.md
@ -1,8 +1,147 @@
|
||||
# ESMP3
|
||||
|
||||
## Audio files
|
||||
System messages are created using:
|
||||
* https://ttsmp3.com/
|
||||
* German / Vicki
|
||||
* "Dies ist ein Text.<break time="1s"/>"
|
||||
* Download as MP3.
|
||||
## 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.
|
||||
|
@ -1,3 +0,0 @@
|
||||
VERSION=1
|
||||
IMAGE_PATH=https://files.schle.nz/esmp3/firmware.bin
|
||||
IMAGE_MD5=00000000000000000000000000000000
|
322
data/index.html
Normal file
322
data/index.html
Normal 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>×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h6>Open URL</h6>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<i class="fa fa-link"></i>
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" class="form-control" id="input_url" />
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" id="button_url_open">Go</button>
|
||||
<button class="btn btn-danger" id="button_url_add_mapping" style="display: none;"><i class="fa fa-arrows-alt-h"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div id="albums_without_id_area">
|
||||
<h6>Albums without RFID card</h6>
|
||||
<table class="table table-hover table-sm">
|
||||
<tbody id="albums_without_id">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h6>Albums with RFID</h6>
|
||||
<table class="table table-hover table-sm">
|
||||
<tbody id="albums_with_id">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="settingsModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Settings</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<h6>Last RFID id:</h6>
|
||||
<span id="last_rfid_id"></span> <button class="btn btn-warning" id="button_add_mapping"><i class="fa fa-arrows-alt-h"></i></button>
|
||||
|
||||
<h6>Last RFID data:</h6>
|
||||
<span id="last_rfid_data"></span>
|
||||
|
||||
<h6>Actions</h6>
|
||||
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reset_vs1053">Reset VS1053 chip</button>
|
||||
<button type="button" class="btn btn-danger btn-lg btn-block" id="button_reboot">Reboot ESMP3</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
update_player = function(data) {
|
||||
$('#play_state_icon i').removeClass('fa-stop', 'fa-play').addClass(data.playing ? 'fa-play' : 'fa-stop');
|
||||
|
||||
if (data.playing) {
|
||||
$('#button_play').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||
$('#button_stop').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
|
||||
} else if (data.playlist) {
|
||||
$('#button_play').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
|
||||
$('#button_stop').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||
} else {
|
||||
$('#button_play').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||
$('#button_stop').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||
}
|
||||
|
||||
$('#volume_slider').attr('min', data.volume.min).attr('max', data.volume.max).val(data.volume.current);
|
||||
|
||||
if (data.playlist) update_playlist(data.playlist);
|
||||
}
|
||||
|
||||
update_playlist = function(data) {
|
||||
$('#track_list tr').remove();
|
||||
for (var i=0; i<data.files.length; i++) {
|
||||
tr = $('<tr>').data('track', i);
|
||||
tr.append($('<td>').html(i + 1));
|
||||
tr.append($('<td>').html(data.current_track==i ? '<i class="fa fa-play"></i>' : ''));
|
||||
tr.append($('<td>').html(data.files[i].substr(data.files[i].title)));
|
||||
$('#track_list').append(tr);
|
||||
}
|
||||
|
||||
if (data.has_track_next) {
|
||||
$('#button_track_next').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
|
||||
} else {
|
||||
$('#button_track_next').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||
}
|
||||
|
||||
if (data.has_track_prev) {
|
||||
$('#button_track_prev').removeClass('btn-secondary', 'btn-disabled').addClass('btn-primary');
|
||||
} else {
|
||||
$('#button_track_prev').removeClass('btn-primary').addClass('btn-secondary', 'btn-disabled');
|
||||
}
|
||||
|
||||
$('#album').html(data.title);
|
||||
var file = data.files[data.current_track];
|
||||
if (file) {
|
||||
$('#track').html(file.title);
|
||||
}
|
||||
}
|
||||
|
||||
update_controller = function(data) {
|
||||
if (data.lock_state == "locked") {
|
||||
$('#button_lock').removeClass('btn-primary', 'btn-warning').addClass('btn-danger');
|
||||
$('#button_lock i').removeClass('fa-lock-open').addClass('fa-lock');
|
||||
} else if (data.lock_state == "locking") {
|
||||
$('#button_lock').removeClass('btn-primary', 'btn-danger').addClass('btn-warning');
|
||||
$('#button_lock i').removeClass('fa-lock-open').addClass('fa-lock');
|
||||
} else {
|
||||
$('#button_lock').removeClass('btn-danger', 'btn-warning').addClass('btn-primary');
|
||||
$('#button_lock i').removeClass('fa-lock').addClass('fa-lock-open');
|
||||
}
|
||||
|
||||
$('#button_add_mapping').toggle(data.last_rfid.uid.length>0);
|
||||
$('#last_rfid_id').html(data.last_rfid.uid);
|
||||
$('#last_rfid_data').html(data.last_rfid.data);
|
||||
}
|
||||
|
||||
update_playlist_manager = function(data) {
|
||||
if (data.unmapped.length > 0) {
|
||||
$('#albums_without_id_area').show();
|
||||
$('#albums_without_id tr').remove();
|
||||
data.unmapped = data.unmapped.sort();
|
||||
for (var i=0; i<data.unmapped.length; i++) {
|
||||
var tr = $('<tr>').attr('data-folder', data.unmapped[i]);
|
||||
tr.append($('<td>').html(data.unmapped[i].substr(1)));
|
||||
tr.append($('<td>').append($('<button>').addClass('button btn-warning add_mapping_button').hide().append($('<i>').addClass('fa fa-arrows-alt-h'))));
|
||||
$('#albums_without_id').append(tr);
|
||||
}
|
||||
} else {
|
||||
$('#albums_without_id_area').hide();
|
||||
}
|
||||
|
||||
var folders = Object.keys(data.folders).sort();
|
||||
for (var i in folders) {
|
||||
var folder = folders[i];
|
||||
var tr = $('<tr>').attr('data-folder', folder);
|
||||
tr.append($('<td>').html(folder.substr(1)));
|
||||
tr.append($('<td>').append($('<button>').addClass('button btn-danger add_mapping_button').hide().append($('<i>').addClass('fa fa-arrows-alt-h'))));
|
||||
$('#albums_with_id').append(tr);
|
||||
}
|
||||
}
|
||||
|
||||
update_position = function(data) {
|
||||
$('#position_slider').attr('max', data.file_size).val(data.position);
|
||||
}
|
||||
|
||||
process_ws_message = function(event) {
|
||||
var data = event.data.split("\n");;
|
||||
for (var i=0; i<data.length; i++) {
|
||||
var json = JSON.parse(data[i]);
|
||||
console.log(json);
|
||||
if (json === null) continue;
|
||||
switch(json["_type"]) {
|
||||
case "position": update_position(json); break;
|
||||
case "player": update_player(json); break;
|
||||
case "playlist_manager": update_playlist_manager(json); break;
|
||||
case "controller": update_controller(json); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var play_on_click = true;
|
||||
|
||||
$(function() {
|
||||
ws = new WebSocket("ws://" + location.host + "/ws");
|
||||
ws.onmessage = process_ws_message;
|
||||
|
||||
$('#volume_slider').change(function(e) { ws.send("volume=" + e.target.value); });
|
||||
$('#button_play').click(function(e) { ws.send("play"); });
|
||||
$('#button_stop').click(function(e) { ws.send("stop"); });
|
||||
$('#button_track_next').click(function(e) { ws.send("track_next"); });
|
||||
$('#button_track_prev').click(function(e) { ws.send("track_prev"); });
|
||||
$('#button_open').click(function(e) { $('#openModal').modal('show'); });
|
||||
$('#track_list').on('click', 'tr', function(e) { ws.send("track=" + $(e.target).parent().data('track')); });
|
||||
$('#albums_without_id, #albums_with_id').on('click', 'tr', function(e) { if (play_on_click) {ws.send("play " + $(e.target).parents('tr').data('folder')); $('#openModal').modal('hide');} });
|
||||
$('#button_settings').click(function(e) { $('#settingsModal').modal('show'); });
|
||||
$('#button_reset_vs1053').click(function(e) { ws.send("reset_vs1053"); $('#settingsModal').modal('hide'); });
|
||||
$('#button_reboot').click(function(e) { ws.send("reboot"); $('#settingsModal').modal('hide'); });
|
||||
$('#button_url_open').click(function(e) { ws.send("play " + $('#input_url').val()); $('#openModal').modal('hide');});
|
||||
$('#button_add_mapping').click(function(e) {
|
||||
$('#settingsModal').modal('hide');
|
||||
$('#openModal').modal('show');
|
||||
$('.add_mapping_button').show();
|
||||
$('#button_url_open').hide();
|
||||
$('#button_url_add_mapping').show();
|
||||
play_on_click = false;
|
||||
});
|
||||
$('#openModal').on('click', '.add_mapping_button', function(e) {
|
||||
ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $(e.target).parents('tr').data('folder'));
|
||||
$('#openModal').modal('hide');
|
||||
$('.add_mapping_button').hide();
|
||||
$('#button_url_open').hide();
|
||||
$('#button_url_add_mapping').show();
|
||||
e.stopPropagation();
|
||||
play_on_click=true;
|
||||
return false;
|
||||
});
|
||||
$('#button_url_add_mapping').click(function(e) {
|
||||
ws.send("add_mapping=" + $('#last_rfid_id').html() + "=" + $('#input_url').val());
|
||||
$('#openModal').modal('hide');
|
||||
$('.add_mapping_button').hide();
|
||||
$('#button_url_open').hide();
|
||||
$('#button_url_add_mapping').show();
|
||||
e.stopPropagation();
|
||||
play_on_click=true;
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</html>
|
53
deploy.sh
53
deploy.sh
@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#set -x
|
||||
set -e
|
||||
|
||||
if ! git diff-index --quiet HEAD ; then
|
||||
echo "Git isn't clean. Cant deploy."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
branch_name=$(git symbolic-ref -q HEAD)
|
||||
branch_name=${branch_name##refs/heads/}
|
||||
branch_name=${branch_name:-HEAD}
|
||||
|
||||
if [ "$branch_name" != "master" ]; then
|
||||
echo "We are not on master branch. Can't deploy."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo
|
||||
echo
|
||||
echo "Last tags:"
|
||||
vers=`git tag --sort=-version:refname | head -n 5`
|
||||
while read version; do
|
||||
echo " $version"
|
||||
done <<< "$vers"
|
||||
|
||||
read -p "Version to generate: " VERSION
|
||||
|
||||
OTA_VERSION=`grep "VERSION=" bin/update.manifest | cut -d"=" -f2`
|
||||
OTA_VERSION=$(( $OTA_VERSION + 1 ))
|
||||
|
||||
sed -i.bak "s/#define OTA_VERSION .*/#define OTA_VERSION $OTA_VERSION/" include/config.h include/config.sample.h
|
||||
rm include/config.h.bak include/config.sample.h.bak
|
||||
|
||||
PLATFORMIO_BUILD_FLAGS='-DVERSION=\"$VERSION\"' pio run -e deploy -t buildprog || exit 1
|
||||
|
||||
cp .pio/build/deploy/firmware.bin bin/firmware.bin || exit 1
|
||||
|
||||
sed -i.bak "s/VERSION=.*/VERSION=$OTA_VERSION/" bin/update.manifest
|
||||
MD5=`md5sum --binary bin/firmware.bin | cut -d" " -f1`
|
||||
sed -i.bak "s/IMAGE_MD5=.*/IMAGE_MD5=$MD5/" bin/update.manifest
|
||||
rm bin/update.manifest.bak
|
||||
|
||||
echo; echo; echo; echo; echo
|
||||
echo "Please check the git diff, if everything looks okay:"
|
||||
git diff
|
||||
|
||||
read -p "Press ENTER to continue, Ctrl-C to abort. " foo
|
||||
|
||||
git add bin/firmware.bin bin/update.manifest
|
||||
git commit -m "Deploying version $VERSION."
|
||||
git tag -a -m "Deploying version $VERSION" $VERSION
|
||||
git push --follow-tags
|
76
include/config.sample.h
Normal file
76
include/config.sample.h
Normal 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
|
@ -1,32 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <MFRC522v2.h>
|
||||
#include <MFRC522Debug.h>
|
||||
#include "esmp3.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>
|
||||
|
||||
enum ControllerState { NORMAL, LOCKING, LOCKED };
|
||||
|
||||
class Controller {
|
||||
private:
|
||||
void handle_buttons();
|
||||
void handle_rfid();
|
||||
bool is_button_pressed(uint8_t pin);
|
||||
Playlist current_playlist;
|
||||
bool is_rfid_present = false;
|
||||
unsigned long last_rfid_check = 0;
|
||||
unsigned long last_button_check = 0;
|
||||
unsigned long last_position_save = 0;
|
||||
uint8_t button_pressed = 0;
|
||||
unsigned long button_pressed_since = 0;
|
||||
bool button_already_processed = false;
|
||||
String read_rfid_data();
|
||||
private:
|
||||
MFRC522* _rfid;
|
||||
HTTPServer* _http_server;
|
||||
ControllerState _state = NORMAL;
|
||||
bool _rfid_enabled = true;
|
||||
void _check_rfid();
|
||||
void _check_serial();
|
||||
void _check_buttons();
|
||||
bool _debounce_button(uint8_t index);
|
||||
uint32_t _get_rfid_card_uid();
|
||||
String _read_rfid_data();
|
||||
bool _rfid_present = false;
|
||||
String _last_rfid_uid = "";
|
||||
String _last_rfid_data = "";
|
||||
|
||||
public:
|
||||
void handle();
|
||||
void next_track();
|
||||
void prev_track();
|
||||
void play();
|
||||
void play(String rfid_id, bool shuffle=false);
|
||||
void stop();
|
||||
void eof_mp3(String info);
|
||||
unsigned long _last_rfid_scan_at = 0;
|
||||
unsigned long _last_position_info_at = 0;
|
||||
String _serial_buffer = String();
|
||||
String _cmd_queue = "";
|
||||
void _execute_command_ls(String path);
|
||||
void _execute_command_ids();
|
||||
void _execute_command_help();
|
||||
unsigned long _button_last_pressed_at[NUM_BUTTONS];
|
||||
bool _check_button(uint8_t btn);
|
||||
public:
|
||||
Controller(Player* p, PlaylistManager* pm);
|
||||
PlaylistManager* pm;
|
||||
Player* player;
|
||||
void register_http_server(HTTPServer* h);
|
||||
void loop();
|
||||
void send_controller_status();
|
||||
void send_player_status();
|
||||
void send_playlist_manager_status();
|
||||
void send_position();
|
||||
void inform_new_client(AsyncWebSocketClient* client);
|
||||
String json();
|
||||
bool process_message(String m);
|
||||
void queue_command(String cmd);
|
||||
void update_playlist_manager();
|
||||
};
|
54
include/data_sources.h
Normal file
54
include/data_sources.h
Normal file
@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include "config.h"
|
||||
#include "http_client_wrapper.h"
|
||||
|
||||
class DataSource {
|
||||
private:
|
||||
public:
|
||||
DataSource() {};
|
||||
virtual ~DataSource() {};
|
||||
virtual size_t read(uint8_t* buf, size_t len) = 0;
|
||||
virtual int read() = 0;
|
||||
virtual size_t position() = 0;
|
||||
virtual void seek(size_t position) = 0;
|
||||
virtual size_t size() = 0;
|
||||
virtual void close() = 0;
|
||||
virtual void skip_id3_tag() {};
|
||||
virtual bool usable() = 0;
|
||||
};
|
||||
|
||||
class SDDataSource : public DataSource {
|
||||
private:
|
||||
File _file;
|
||||
public:
|
||||
SDDataSource(String file);
|
||||
~SDDataSource();
|
||||
size_t read(uint8_t* buf, size_t len);
|
||||
int read();
|
||||
size_t position();
|
||||
void seek(size_t position);
|
||||
size_t size();
|
||||
void close();
|
||||
void skip_id3_tag();
|
||||
bool usable();
|
||||
};
|
||||
|
||||
class HTTPSDataSource : public DataSource {
|
||||
private:
|
||||
WiFiClient* _stream = NULL;
|
||||
HTTPClientWrapper* _http = NULL;
|
||||
uint32_t _position;
|
||||
public:
|
||||
HTTPSDataSource(String url, uint32_t offset=0);
|
||||
~HTTPSDataSource();
|
||||
size_t read(uint8_t* buf, size_t len);
|
||||
int read();
|
||||
size_t position();
|
||||
void seek(size_t position);
|
||||
size_t size();
|
||||
void close();
|
||||
bool usable();
|
||||
};
|
@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "controller.h"
|
||||
#include "playlist_manager.h"
|
||||
#include <Audio.h>
|
||||
|
||||
#define PIN_CS_SD 22
|
||||
#define PIN_CS_RFID 21
|
||||
|
||||
#define PIN_BTN_VOL_UP 32
|
||||
#define PIN_BTN_VOL_DOWN 33
|
||||
#define PIN_BTN_TRACK_NEXT 17
|
||||
#define PIN_BTN_TRACK_PREV 16
|
||||
|
||||
#define I2S_DOUT 25
|
||||
#define I2S_BCLK 26
|
||||
#define I2S_LRC 27
|
||||
|
||||
class Controller;
|
||||
|
||||
extern Controller controller;
|
||||
extern Audio audio;
|
||||
extern PlaylistManager* pm;
|
||||
extern MFRC522* rfid;
|
||||
|
||||
void save_audio_current_time();
|
37
include/http_client_wrapper.h
Normal file
37
include/http_client_wrapper.h
Normal file
@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <HTTPClient.h>
|
||||
#include "config.h"
|
||||
|
||||
class HTTPClientWrapper {
|
||||
private:
|
||||
HTTPClient* _http;
|
||||
uint8_t* _buffer;
|
||||
uint16_t _buffer_size;
|
||||
uint16_t _buffer_length;
|
||||
uint16_t _buffer_position;
|
||||
uint32_t _chunk_length;
|
||||
|
||||
bool _connected = false;
|
||||
String _content_type;
|
||||
uint32_t _length;
|
||||
bool _request(String method, String url, uint32_t offset=0, uint8_t redirection_count=0);
|
||||
WiFiClient* _stream;
|
||||
bool _is_chunked;
|
||||
void _read_next_chunk_header(bool first);
|
||||
uint16_t _fill_buffer();
|
||||
|
||||
public:
|
||||
HTTPClientWrapper();
|
||||
~HTTPClientWrapper();
|
||||
bool get(String url, uint32_t offset=0, uint8_t redirection_count=0);
|
||||
bool head(String url, uint32_t offset=0, uint8_t redirection_count=0);
|
||||
String getContentType();
|
||||
String getString();
|
||||
int read();
|
||||
uint32_t read(uint8_t* dst, uint32_t len);
|
||||
void close();
|
||||
uint32_t getSize();
|
||||
String readUntil(String sep);
|
||||
String readLine();
|
||||
};
|
29
include/http_server.h
Normal file
29
include/http_server.h
Normal file
@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
class HTTPServer;
|
||||
|
||||
#include "player.h"
|
||||
#include "controller.h"
|
||||
#include <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
class HTTPServer {
|
||||
private:
|
||||
AsyncWebServer* _server;
|
||||
|
||||
Player* _player;
|
||||
Controller* _controller;
|
||||
void _handle_upload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
|
||||
uint16_t _chunk_length;
|
||||
uint8_t* _chunk;
|
||||
File _upload_file;
|
||||
uint32_t _file_size;
|
||||
uint32_t _file_size_done;
|
||||
bool _need_header;
|
||||
uint32_t _upload_position;
|
||||
void _onEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len);
|
||||
void _handle_index(AsyncWebServerRequest* req);
|
||||
public:
|
||||
HTTPServer(Player* p, Controller* c);
|
||||
AsyncWebSocket* ws;
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
struct PersistedPlaylist {
|
||||
String dir;
|
||||
uint16_t file = 0;
|
||||
uint32_t position = 0;
|
||||
PersistedPlaylist(String s="") : dir(s) {}
|
||||
};
|
104
include/player.h
Normal file
104
include/player.h
Normal file
@ -0,0 +1,104 @@
|
||||
#pragma once
|
||||
#include "config.h"
|
||||
#include <SPI.h>
|
||||
#include <SD.h>
|
||||
#include "spi_master.h"
|
||||
#include "playlist.h"
|
||||
#include "data_sources.h"
|
||||
|
||||
class Player;
|
||||
|
||||
#include "controller.h"
|
||||
|
||||
#define SCI_MODE 0x00
|
||||
#define SCI_STATUS 0x01
|
||||
#define SCI_BASS 0x02
|
||||
#define SCI_CLOCKF 0x03
|
||||
#define SCI_DECODE_TIME 0x04
|
||||
#define SCI_AUDATA 0x05
|
||||
#define SCI_VOL 0x0B
|
||||
#define SCI_WRAMADDR 0x07
|
||||
#define SCI_WRAM 0x06
|
||||
#define SCI_HDAT0 0x08
|
||||
#define SCI_HDAT1 0x09
|
||||
#define SCI_AIADDR 0x0A
|
||||
#define SCI_AICTRL0 0x0C
|
||||
#define SCI_AICTRL1 0x0D
|
||||
#define SCI_AICTRL2 0x0E
|
||||
#define SCI_AICTRL3 0x0F
|
||||
|
||||
#define CMD_WRITE 0x02
|
||||
#define CMD_READ 0x03
|
||||
|
||||
#define ADDR_ENDBYTE 0x1E06
|
||||
|
||||
#define SM_LAYER12 0x0001
|
||||
#define SM_RESET 0x0004
|
||||
#define SM_CANCEL 0x0008
|
||||
#define SM_SDINEW 0x0800
|
||||
#define SM_ADPCM 0x1000
|
||||
#define SS_DO_NOT_JUMP 0x8000
|
||||
|
||||
class Player {
|
||||
private:
|
||||
enum state { uninitialized, idle, playing, stopping,
|
||||
sleeping, recording };
|
||||
void _reset();
|
||||
void _wait();
|
||||
uint16_t _read_control_register(uint8_t address, bool do_wait=true);
|
||||
void _write_control_register(uint8_t address, uint16_t value, bool do_wait=true);
|
||||
void _write_direct(uint8_t address, uint16_t value);
|
||||
void _write_data(uint8_t* data);
|
||||
uint16_t _read_wram(uint16_t address);
|
||||
state _state = state::uninitialized;
|
||||
void _refill();
|
||||
bool _refill_needed();
|
||||
void _flush_and_cancel();
|
||||
int8_t _get_endbyte();
|
||||
void _flush(uint count, int8_t fill_byte);
|
||||
void _play_file(String filename, uint32_t offset);
|
||||
void _finish_playing();
|
||||
void _finish_stopping(bool turn_speaker_off);
|
||||
void _mute();
|
||||
void _unmute();
|
||||
void _sleep();
|
||||
void _wakeup();
|
||||
void _record();
|
||||
void _patch_adpcm();
|
||||
void _speaker_off();
|
||||
void _speaker_on();
|
||||
|
||||
SPISettings _spi_settings_slow = SPISettings(250000, MSBFIRST, SPI_MODE0);
|
||||
SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
||||
SPISettings* _spi_settings = &_spi_settings_slow;
|
||||
|
||||
DataSource* _file;
|
||||
uint32_t _file_size = 0;
|
||||
uint8_t _buffer[32];
|
||||
uint32_t _current_play_position = 0;
|
||||
Playlist* _current_playlist = NULL;
|
||||
uint _refills;
|
||||
uint8_t _volume;
|
||||
uint16_t _stop_delay;
|
||||
uint32_t _skip_to;
|
||||
SPIMaster* _spi;
|
||||
Controller* _controller;
|
||||
unsigned long _stopped_at;
|
||||
public:
|
||||
Player(SPIMaster* s);
|
||||
void init();
|
||||
void register_controller(Controller* c);
|
||||
void vol_up();
|
||||
void vol_down();
|
||||
void track_next();
|
||||
void track_prev();
|
||||
void set_track(uint8_t track);
|
||||
bool is_playing();
|
||||
bool play();
|
||||
bool play(Playlist* p);
|
||||
void stop(bool turn_speaker_off=true);
|
||||
bool loop();
|
||||
void set_volume(uint8_t vol, bool save = true);
|
||||
String position_json();
|
||||
String json();
|
||||
};
|
@ -1,30 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <Arduino.h>
|
||||
#include "persisted_playlist.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:
|
||||
std::vector<String> files;
|
||||
uint8_t current_file = 0;
|
||||
uint32_t current_time = 0;
|
||||
String rfid_id;
|
||||
PersistedPlaylist* pp;
|
||||
|
||||
public:
|
||||
Playlist();
|
||||
Playlist(String rfid_id, PersistedPlaylist* p);
|
||||
void add_file(String filename);
|
||||
void sort();
|
||||
String get_rfid_id();
|
||||
String get_current_file_name();
|
||||
bool next_track();
|
||||
bool prev_track();
|
||||
void restart();
|
||||
void set_current_time(uint32_t time);
|
||||
uint32_t get_current_time();
|
||||
void shuffle();
|
||||
void set_current_position(uint8_t file, uint32_t position=0);
|
||||
void save_current_position(uint32_t position=0);
|
||||
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);
|
||||
};
|
||||
|
@ -1,25 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
#include "playlist.h"
|
||||
#include "persisted_playlist.h"
|
||||
|
||||
class Playlist;
|
||||
|
||||
class PlaylistManager {
|
||||
private:
|
||||
Playlist get_playlist_for_tag_id(String id);
|
||||
String current_rfid_tag_id;
|
||||
uint32_t audio_current_time = 0;
|
||||
|
||||
public:
|
||||
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();
|
||||
std::map<String, PersistedPlaylist> map;
|
||||
Playlist get_playlist(String rfid_id);
|
||||
bool has_playlist(String rfid_id);
|
||||
Playlist current_playlist;
|
||||
void set_audio_current_time(uint32_t time);
|
||||
String pp_to_String();
|
||||
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();
|
||||
};
|
@ -1,9 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
#include "config.h"
|
||||
|
||||
class SPIMaster {
|
||||
public:
|
||||
static void enable_sd();
|
||||
static void enable_rfid();
|
||||
static void disable_all();
|
||||
static void initialize();
|
||||
public:
|
||||
static uint8_t state;
|
||||
|
||||
static void init() {
|
||||
PIN_SD_CS_SETUP();
|
||||
PIN_VS1053_XCS_SETUP();
|
||||
PIN_VS1053_XDCS_SETUP();
|
||||
PIN_RC522_CS_SETUP();
|
||||
disable();
|
||||
}
|
||||
|
||||
static void select_sd(bool enabled=true) {
|
||||
PIN_SD_CS(enabled ? LOW : HIGH);
|
||||
if (enabled) {
|
||||
state |= 1;
|
||||
} else {
|
||||
state &= ~1;
|
||||
}
|
||||
}
|
||||
|
||||
static void select_vs1053_xcs(bool enabled=true) {
|
||||
PIN_VS1053_XCS(enabled ? LOW : HIGH);
|
||||
if (enabled) {
|
||||
state |= 2;
|
||||
} else {
|
||||
state &= ~2;
|
||||
}
|
||||
}
|
||||
|
||||
static void select_vs1053_xdcs(bool enabled=true) {
|
||||
PIN_VS1053_XDCS(enabled ? LOW : HIGH);
|
||||
if (enabled) {
|
||||
state |= 4;
|
||||
} else {
|
||||
state &= ~4;
|
||||
}
|
||||
}
|
||||
|
||||
static void select_rc522(bool enabled=true) {
|
||||
PIN_RC522_CS(enabled ? LOW : HIGH);
|
||||
if (enabled) {
|
||||
state |= 8;
|
||||
} else {
|
||||
state &= ~8;
|
||||
}
|
||||
}
|
||||
|
||||
static void set_state(uint8_t s) {
|
||||
disable();
|
||||
if (s & 1) select_sd();
|
||||
if (s & 2) select_vs1053_xcs();
|
||||
if (s & 4) select_vs1053_xdcs();
|
||||
if (s & 8) select_rc522();
|
||||
}
|
||||
|
||||
static void disable() {
|
||||
PIN_SD_CS(HIGH);
|
||||
PIN_VS1053_XCS(HIGH);
|
||||
PIN_VS1053_XDCS(HIGH);
|
||||
PIN_RC522_CS(HIGH);
|
||||
state = 0;
|
||||
}
|
||||
};
|
@ -1,6 +0,0 @@
|
||||
# Custom partition table without SPIFFS.
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x220000,
|
||||
app1, app, ota_1, 0x230000,0x220000,
|
|
@ -8,35 +8,16 @@
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
default_envs = esp32
|
||||
|
||||
[extra]
|
||||
lib_deps =
|
||||
|
||||
[env:esp32]
|
||||
platform = espressif32
|
||||
board = esp-wrover-kit
|
||||
framework = arduino
|
||||
upload_speed = 921600
|
||||
build_flags = -DCORE_DEBUG_LEVEL=5 -DCONFIG_ARDUHAL_LOG_COLORS=1 ; !./build_version.sh
|
||||
lib_deps =
|
||||
${extra.lib_deps}
|
||||
esphome/ESP32-audioI2S@^2.1.0
|
||||
computer991/Arduino_MFRC522v2@^2.0.1
|
||||
https://github.com/dplasa/FTPClientServer
|
||||
;upload_port = 10.10.2.108
|
||||
monitor_speed = 115200
|
||||
monitor_port = /dev/cu.usbserial-0001
|
||||
monitor_filters = esp32_exception_decoder
|
||||
|
||||
[env:deploy]
|
||||
platform = espressif32
|
||||
board = esp-wrover-kit
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
${extra.lib_deps}
|
||||
esphome/ESP32-audioI2S@^2.1.0
|
||||
computer991/Arduino_MFRC522v2@^2.0.1
|
||||
board_build.embed_txtfiles = src/index.html
|
||||
board_build.partitions = partitions.csv
|
||||
upload_speed = 512000
|
||||
build_flags=!./build_version.sh
|
||||
lib_deps = MFRC522
|
||||
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
||||
ArduinoJSON
|
||||
6691 ; TinyXML
|
||||
upload_port = /dev/cu.SLAB_USBtoUART
|
||||
monitor_speed = 74480
|
||||
;monitor_port = /dev/cu.wchusbserial1420
|
||||
|
@ -1,67 +1,97 @@
|
||||
#include "controller.h"
|
||||
#include "esmp3.h"
|
||||
#include "spi_master.h"
|
||||
#include "config.h"
|
||||
#include "playlist.h"
|
||||
#include "http_server.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
void Controller::handle() {
|
||||
if (last_rfid_check + 500 < millis() || last_rfid_check > millis()) {
|
||||
handle_rfid();
|
||||
last_rfid_check = millis();
|
||||
}
|
||||
if (last_button_check + 10 < millis() || last_button_check > millis()) {
|
||||
handle_buttons();
|
||||
last_button_check = millis();
|
||||
}
|
||||
if (last_position_save + 10000 < millis() || last_position_save > millis()) {
|
||||
current_playlist.save_current_position(audio.getFilePos());
|
||||
last_position_save = millis();
|
||||
//Serial.println(pm->pp_to_String().c_str());
|
||||
}
|
||||
Controller::Controller(Player* p, PlaylistManager* playlist_manager) {
|
||||
player = p;
|
||||
pm = playlist_manager;
|
||||
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
|
||||
|
||||
player->register_controller(this);
|
||||
|
||||
BTN_NEXT_SETUP();
|
||||
BTN_PREV_SETUP();
|
||||
BTN_VOL_UP_SETUP();
|
||||
BTN_VOL_DOWN_SETUP();
|
||||
|
||||
SPIMaster::select_rc522();
|
||||
DEBUG("Initializing RC522...\n");
|
||||
_rfid->PCD_Init();
|
||||
#ifdef SHOW_DEBUG
|
||||
_rfid->PCD_DumpVersionToSerial();
|
||||
#endif
|
||||
SPIMaster::select_rc522(false);
|
||||
INFO("RC522 initialized.\n");
|
||||
|
||||
for (uint8_t i=0; i<NUM_BUTTONS; i++) _button_last_pressed_at[i]=0;
|
||||
}
|
||||
|
||||
void Controller::handle_buttons() {
|
||||
if (is_button_pressed(PIN_BTN_VOL_UP)) {
|
||||
log_i("BTN_VOL_UP pressed");
|
||||
uint8_t vol = min(audio.getVolume()+2, 21);
|
||||
log_d("Setting new volume %d", vol);
|
||||
audio.setVolume(vol);
|
||||
} else if (is_button_pressed(PIN_BTN_VOL_DOWN)) {
|
||||
log_i("BTN_VOL_DOWN pressed");
|
||||
uint8_t vol;
|
||||
if ((vol = audio.getVolume()) >= 3) {
|
||||
vol -= 2;
|
||||
} else {
|
||||
vol = 1;
|
||||
}
|
||||
log_d("Setting new volume %d", vol);
|
||||
audio.setVolume(vol);
|
||||
} else if (is_button_pressed(PIN_BTN_TRACK_NEXT)) {
|
||||
log_i("BTN_TRACK_NEXT pressed");
|
||||
next_track();
|
||||
} else if (is_button_pressed(PIN_BTN_TRACK_PREV)) {
|
||||
log_i("BTN_TRACK_PREV pressed");
|
||||
prev_track();
|
||||
}
|
||||
void Controller::register_http_server(HTTPServer* h) {
|
||||
_http_server = h;
|
||||
}
|
||||
|
||||
void Controller::handle_rfid() {
|
||||
if (is_rfid_present) {
|
||||
void Controller::loop() {
|
||||
TRACE("Controller::loop()...\n");
|
||||
unsigned long now = millis();
|
||||
if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) {
|
||||
_check_rfid();
|
||||
_last_rfid_scan_at = now;
|
||||
}
|
||||
if ((_last_position_info_at < now - POSITION_SEND_INTERVAL) || (now < _last_position_info_at)) {
|
||||
send_position();
|
||||
_last_position_info_at = now;
|
||||
}
|
||||
_check_serial();
|
||||
_check_buttons();
|
||||
if (_cmd_queue.length() > 0) {
|
||||
process_message(_cmd_queue);
|
||||
_cmd_queue = "";
|
||||
}
|
||||
TRACE("Controller::loop() done.\n");
|
||||
}
|
||||
|
||||
uint32_t Controller::_get_rfid_card_uid() {
|
||||
SPIMaster::select_rc522();
|
||||
if (!_rfid->PICC_ReadCardSerial()) {
|
||||
if (!_rfid->PICC_IsNewCardPresent()) {
|
||||
return 0;
|
||||
}
|
||||
if (!_rfid->PICC_ReadCardSerial()) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
SPIMaster::select_rc522(false);
|
||||
uint32_t uid = _rfid->uid.uidByte[0]<<24 | _rfid->uid.uidByte[1]<<16 | _rfid->uid.uidByte[2]<<8 | _rfid->uid.uidByte[3];
|
||||
return uid;
|
||||
}
|
||||
|
||||
void Controller::_check_rfid() {
|
||||
TRACE("check_rfid running...\n");
|
||||
MFRC522::StatusCode status;
|
||||
if (_rfid_present) {
|
||||
byte buffer[2];
|
||||
byte buffer_size = 2;
|
||||
MFRC522Constants::StatusCode status = rfid->PICC_WakeupA(buffer, &buffer_size);
|
||||
if (status == MFRC522Constants::STATUS_OK) {
|
||||
SPIMaster::select_rc522();
|
||||
status = _rfid->PICC_WakeupA(buffer, &buffer_size);
|
||||
if (status == MFRC522::STATUS_OK) {
|
||||
// Card is still present.
|
||||
rfid->PICC_HaltA();
|
||||
} else {
|
||||
Serial.printf("RFID status is %s\n", MFRC522Debug::GetStatusCodeName(status));
|
||||
is_rfid_present = false;
|
||||
Serial.println("No more RFID card.\n");
|
||||
stop();
|
||||
_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 {
|
||||
if (rfid->PICC_IsNewCardPresent()) {
|
||||
if (rfid->PICC_ReadCardSerial()) {
|
||||
uint32_t uid = rfid->uid.uidByte[0]<<24 | rfid->uid.uidByte[1]<<16 | rfid->uid.uidByte[2]<<8 | rfid->uid.uidByte[3];
|
||||
Serial.printf("Found new rfid card with uid %x\n", uid);
|
||||
is_rfid_present = true;
|
||||
uint32_t uid = _get_rfid_card_uid();
|
||||
if (uid > 0) {
|
||||
String temp = String(uid, HEX);
|
||||
String s_uid = "";
|
||||
@ -69,158 +99,321 @@ void Controller::handle_rfid() {
|
||||
s_uid.concat("0");
|
||||
}
|
||||
s_uid.concat(temp);
|
||||
INFO("New RFID card uid: %s\n", s_uid.c_str());
|
||||
_last_rfid_uid = s_uid;
|
||||
_rfid_present = true;
|
||||
|
||||
String data = read_rfid_data();
|
||||
String data = _read_rfid_data();
|
||||
_last_rfid_data = data;
|
||||
|
||||
play(s_uid, data.indexOf("[random]")>=0);
|
||||
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;
|
||||
}
|
||||
rfid->PICC_HaltA();
|
||||
}
|
||||
if (pl==NULL) {
|
||||
INFO("Could not find album for id '%s'.\n", s_uid.c_str());
|
||||
send_controller_status();
|
||||
return;
|
||||
}
|
||||
int index;
|
||||
if (data.indexOf("[advent]") != -1 && pl->is_fresh()) {
|
||||
struct tm time;
|
||||
getLocalTime(&time);
|
||||
if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december.
|
||||
pl->advent_shuffle(time.tm_mday);
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
} else if (data.indexOf("[random]") != -1 && pl->is_fresh()) {
|
||||
pl->shuffle();
|
||||
} else if ((index=data.indexOf("[random:")) != -1 && pl->is_fresh()) {
|
||||
String temp = data.substring(index + 8);
|
||||
index = temp.indexOf("]");
|
||||
TRACE("temp: %s, temp.substring(0, %d): %s\n", temp.c_str(), index, temp.substring(0, index).c_str());
|
||||
if (index>0) {
|
||||
uint8_t random_offset = temp.substring(0, index).toInt();
|
||||
pl->shuffle(random_offset);
|
||||
}
|
||||
}
|
||||
|
||||
if (_state == LOCKED) {
|
||||
DEBUG("ControllerState is LOCKED, ignoring card.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_state == LOCKING) {
|
||||
_state = LOCKED;
|
||||
DEBUG("ControllerState is now LOCKED.\n");
|
||||
}
|
||||
|
||||
player->play(pl);
|
||||
//send_playlist_manager_status();
|
||||
send_controller_status();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String Controller::read_rfid_data() {
|
||||
log_v("read_rfid_data() running...");
|
||||
MFRC522::StatusCode status;
|
||||
MFRC522::PICC_Type type = rfid->PICC_GetType(rfid->uid.sak);
|
||||
uint16_t pageStart = 0;
|
||||
uint16_t pages = 4;
|
||||
uint16_t pageSize = 1;
|
||||
switch(type) {
|
||||
case MFRC522Constants::PICC_TYPE_MIFARE_MINI:
|
||||
case MFRC522Constants::PICC_TYPE_MIFARE_1K:
|
||||
case MFRC522Constants::PICC_TYPE_MIFARE_4K: {
|
||||
log_v("Trying to authenticate Mifare card.");
|
||||
MFRC522::MIFARE_Key key = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
|
||||
status = rfid->PCD_Authenticate(MFRC522Constants::PICC_CMD_MF_AUTH_KEY_A, 4, &key, &rfid->uid);
|
||||
if (status == MFRC522Constants::STATUS_OK) {
|
||||
log_v("Authentication succeeded.");
|
||||
} else {
|
||||
log_v("Authentication failed. Trying to read anyway.");
|
||||
}
|
||||
pageStart = 4;
|
||||
break;
|
||||
}
|
||||
case MFRC522Constants::PICC_TYPE_MIFARE_UL:
|
||||
log_v("PICC type is Mifare Ultralight. No authentication necessary.");
|
||||
pages = 16;
|
||||
pageSize = 4;
|
||||
break;
|
||||
default:
|
||||
log_v("Unexpected rfid card type %s. Trying to read anyway.", MFRC522Debug::PICC_GetTypeName(type));
|
||||
}
|
||||
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 = "";
|
||||
for (uint8_t block=pageStart; block<pages+pageStart; block+=pageSize) {
|
||||
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, buffer, &byte_count);
|
||||
if (status != MFRC522Constants::STATUS_OK) {
|
||||
log_d("MIFARE_Read() failed: %s\n", String(MFRC522Debug::GetStatusCodeName(status)).c_str());
|
||||
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->PCD_StopCrypto1();
|
||||
log_v("Read rfid data: '%s'", data.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
_rfid->PICC_HaltA();
|
||||
_rfid->PCD_StopCrypto1();
|
||||
DEBUG("Data from RFID: %s\n", data.c_str());
|
||||
SPIMaster::select_rc522(false);
|
||||
return data;
|
||||
}
|
||||
|
||||
void Controller::play(String rfid_id, bool shuffle) {
|
||||
if (!rfid_id.equals(current_playlist.get_rfid_id())) {
|
||||
if (pm->has_playlist(rfid_id)) {
|
||||
current_playlist = pm->get_playlist(rfid_id);
|
||||
if (shuffle) {
|
||||
log_i("Shuffling the playlist.");
|
||||
current_playlist.shuffle();
|
||||
}
|
||||
play();
|
||||
} else {
|
||||
Serial.printf("There is no playlist for rfid_id %s\n", rfid_id.c_str());
|
||||
// This is working more or less, but downloading files is really, REALLY slow. (About 4 minutes for 10 MBytes).
|
||||
//download_album(rfid_id);
|
||||
audio.connecttoFS(SD, "/system/sys_unknown_card.mp3");
|
||||
void Controller::_check_serial() {
|
||||
TRACE("check_serial running...\n");
|
||||
|
||||
if (Serial.available() > 0) {
|
||||
char c = Serial.read();
|
||||
Serial.printf("%c", c);
|
||||
if (c==10 || c==13) {
|
||||
if (_serial_buffer.length()>0) {
|
||||
process_message(_serial_buffer);
|
||||
_serial_buffer = String();
|
||||
}
|
||||
} else {
|
||||
if (!audio.isRunning()) {
|
||||
play();
|
||||
_serial_buffer.concat(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::play() {
|
||||
String file = current_playlist.get_current_file_name();
|
||||
bool Controller::process_message(String cmd) {
|
||||
DEBUG("Executing command: %s\n", cmd.c_str());
|
||||
|
||||
if (file.startsWith("/")) {
|
||||
log_i("Playing file %s via connecttoFS", file.c_str());
|
||||
audio.connecttoFS(SD, file.c_str(), current_playlist.get_current_time());
|
||||
} else if (file.startsWith("http")) {
|
||||
log_i("Playing URL %s via connecttohost", file.c_str());
|
||||
audio.connecttoFS(SD, "/system/sys_connecting.mp3");
|
||||
while (audio.isRunning()) {
|
||||
yield();
|
||||
audio.loop();
|
||||
}
|
||||
audio.connecttohost(file.c_str());
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
void Controller::next_track() {
|
||||
if (current_playlist.next_track()) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::prev_track() {
|
||||
uint32_t time = audio.getAudioCurrentTime();
|
||||
log_d("prev_track() called. getAudioCurrentTime() returns %d", time);
|
||||
if (time >= 5) {
|
||||
log_d("Restarting current track.");
|
||||
current_playlist.restart();
|
||||
play();
|
||||
} else if (cmd.equals("stop")) {
|
||||
player->stop();
|
||||
} else if (cmd.equals("help")) {
|
||||
_execute_command_help();
|
||||
} else if (cmd.equals("-")) {
|
||||
player->vol_down();
|
||||
} else if (cmd.equals("+")) {
|
||||
player->vol_up();
|
||||
} else if (cmd.startsWith("volume=")) {
|
||||
uint8_t vol = cmd.substring(7).toInt();
|
||||
player->set_volume(vol);
|
||||
} else if (cmd.equals("track_prev")) {
|
||||
player->track_prev();
|
||||
} else if (cmd.equals("track_next")) {
|
||||
player->track_next();
|
||||
} else if (cmd.startsWith("track=")) {
|
||||
uint8_t track = cmd.substring(6).toInt();
|
||||
player->set_track(track);
|
||||
} else if (cmd.equals("ids")) {
|
||||
pm->dump_ids();
|
||||
} else if (cmd.equals("reset_vs1053")) {
|
||||
player->stop();
|
||||
player->init();
|
||||
} else if (cmd.equals("reboot")) {
|
||||
ESP.restart();
|
||||
} else if (cmd.startsWith("add_mapping=")) {
|
||||
String rest = cmd.substring(12);
|
||||
uint8_t idx = rest.indexOf('=');
|
||||
String id = rest.substring(0, idx);
|
||||
String folder = rest.substring(idx + 1);
|
||||
pm->add_mapping(id, folder);
|
||||
send_playlist_manager_status();
|
||||
} else {
|
||||
if (current_playlist.prev_track()) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::stop() {
|
||||
if (audio.isRunning()) {
|
||||
current_playlist.set_current_time(audio.stopSong());
|
||||
}
|
||||
}
|
||||
|
||||
bool Controller::is_button_pressed(uint8_t pin) {
|
||||
//log_d("Button %d state is %d", pin, digitalRead(pin));
|
||||
if (!digitalRead(pin)) {
|
||||
// Button is pressed - let's debounce it.
|
||||
if (button_pressed == pin) {
|
||||
if (button_pressed_since + 150 < millis() && !button_already_processed) {
|
||||
button_already_processed = true;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
button_pressed = pin;
|
||||
button_pressed_since = millis();
|
||||
button_already_processed = false;
|
||||
}
|
||||
} else {
|
||||
if (button_pressed == pin) {
|
||||
button_pressed = 0;
|
||||
}
|
||||
}
|
||||
ERROR("Unknown command: %s\n", cmd.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Controller::eof_mp3(String info) {
|
||||
log_d("Handling eof. Keep playing until the file is finished.");
|
||||
while(audio.isRunning()) { audio.loop(); yield; }
|
||||
if (info.startsWith("sys_")) {
|
||||
log_d("File ending was a system audio file. Not running next_track.");
|
||||
void Controller::_execute_command_ls(String path) {
|
||||
INFO("Listing contents of %s:\n", path.c_str());
|
||||
// TODO
|
||||
//std::list<String> files = player->ls(path);
|
||||
//for(std::list<String>::iterator it=files.begin(); it!=files.end(); ++it) {
|
||||
// INFO(" %s\n", (*it).c_str());
|
||||
//}
|
||||
}
|
||||
|
||||
void Controller::_execute_command_help() {
|
||||
INFO("Valid commands are:");
|
||||
INFO(" help - Displays this help\n");
|
||||
//INFO(" ls [dir] - Lists the contents of [dir] or, if not given, of /\n");
|
||||
INFO(" ids - Lists all known ID-to-folder mappings\n");
|
||||
INFO(" play [id] - Plays the album with the given id\n");
|
||||
INFO(" stop - Stops playback\n");
|
||||
INFO(" - / + - Decrease or increase the volume\n");
|
||||
INFO(" p / n - Previous or next track\n");
|
||||
}
|
||||
|
||||
void Controller::_check_buttons() {
|
||||
TRACE("check_buttons running...\n");
|
||||
|
||||
if (BTN_PREV() && _debounce_button(0)) {
|
||||
if (_state == NORMAL) {
|
||||
player->track_prev();
|
||||
} else {
|
||||
next_track();
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Controller::_debounce_button(uint8_t index) {
|
||||
bool ret = false;
|
||||
if (_button_last_pressed_at[index] + DEBOUNCE_MILLIS < millis()) {
|
||||
DEBUG("Button %d pressed.\n", index);
|
||||
ret = true;
|
||||
}
|
||||
_button_last_pressed_at[index] = millis();
|
||||
return ret;
|
||||
}
|
||||
|
||||
String Controller::json() {
|
||||
DynamicJsonDocument json(1024);
|
||||
json["_type"] = "controller";
|
||||
switch(_state) {
|
||||
case LOCKED: json["state"] = "locked"; break;
|
||||
case LOCKING: json["state"] = "locking"; break;
|
||||
case NORMAL: json["state"] = "normal"; break;
|
||||
}
|
||||
json["is_rfid_present"] = _rfid_present;
|
||||
JsonObject rfid = json.createNestedObject("last_rfid");
|
||||
rfid["uid"] = _last_rfid_uid;
|
||||
rfid["data"] = _last_rfid_data;
|
||||
json["uptime"] = millis() / 1000;
|
||||
json["free_heap"] = ESP.getFreeHeap();
|
||||
return json.as<String>();
|
||||
}
|
||||
|
||||
void Controller::send_player_status() {
|
||||
TRACE("In send_player_status()...\n");
|
||||
|
||||
if (_http_server->ws->count() > 0) {
|
||||
_http_server->ws->textAll(player->json());
|
||||
_http_server->ws->textAll(player->position_json());
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::send_playlist_manager_status() {
|
||||
TRACE("In send_playlist_manager_status()...\n");
|
||||
if (_http_server->ws->count() > 0) {
|
||||
_http_server->ws->textAll(pm->json());
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::send_position() {
|
||||
TRACE("In send_position()...\n");
|
||||
if (_http_server->ws->count() > 0) {
|
||||
_http_server->ws->textAll(player->position_json());
|
||||
}
|
||||
_last_position_info_at = millis();
|
||||
}
|
||||
|
||||
void Controller::send_controller_status() {
|
||||
TRACE("In send_controller_status()...\n");
|
||||
if (_http_server->ws->count() > 0) {
|
||||
_http_server->ws->textAll(json());
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::inform_new_client(AsyncWebSocketClient* client) {
|
||||
String s;
|
||||
s += pm->json();
|
||||
s += '\n';
|
||||
s += player->json();
|
||||
s += '\n';
|
||||
s += player->position_json();
|
||||
s += '\n';
|
||||
s += json();
|
||||
client->text(s);
|
||||
}
|
||||
|
||||
void Controller::queue_command(String s) {
|
||||
DEBUG("Enqeueing command '%s'.\n", s.c_str());
|
||||
_cmd_queue = s;
|
||||
}
|
||||
|
||||
void Controller::update_playlist_manager() {
|
||||
pm->scan_files();
|
||||
send_playlist_manager_status();
|
||||
}
|
||||
|
57
src/data_sources.cpp
Normal file
57
src/data_sources.cpp
Normal file
@ -0,0 +1,57 @@
|
||||
#include "data_sources.h"
|
||||
|
||||
////////////// SDDataSource //////////////
|
||||
SDDataSource::SDDataSource(String file) { _file = SD.open(file, "r"); }
|
||||
SDDataSource::~SDDataSource() { if (_file) _file.close(); }
|
||||
size_t SDDataSource::read(uint8_t* buf, size_t len) { return _file.read(buf, len); }
|
||||
int SDDataSource::read() { return _file.read(); }
|
||||
size_t SDDataSource::position() { return _file.position(); }
|
||||
void SDDataSource::seek(size_t position) { _file.seek(position); }
|
||||
size_t SDDataSource::size() { return _file.size(); }
|
||||
void SDDataSource::close() { _file.close(); }
|
||||
bool SDDataSource::usable() { return _file; }
|
||||
|
||||
void SDDataSource::skip_id3_tag() {
|
||||
uint32_t original_position = _file.position();
|
||||
uint32_t offset = 0;
|
||||
if (_file.read()=='I' && _file.read()=='D' && _file.read()=='3') {
|
||||
DEBUG("ID3 tag found\n");
|
||||
// Skip ID3 tag version
|
||||
_file.read(); _file.read();
|
||||
byte tags = _file.read();
|
||||
bool footer_present = tags & 0x10;
|
||||
DEBUG("ID3 footer found: %d\n", footer_present);
|
||||
for (byte i=0; i<4; i++) {
|
||||
offset <<= 7;
|
||||
offset |= (0x7F & _file.read());
|
||||
}
|
||||
offset += 10;
|
||||
if (footer_present) offset += 10;
|
||||
DEBUG("ID3 tag length is %d bytes.\n", offset);
|
||||
_file.seek(offset);
|
||||
} else {
|
||||
DEBUG("No ID3 tag found\n");
|
||||
_file.seek(original_position);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////// HTTPSDataSource //////////////
|
||||
HTTPSDataSource::HTTPSDataSource(String url, uint32_t offset) {
|
||||
_http = new HTTPClientWrapper();
|
||||
if (!_http->get(url, offset)) return;
|
||||
_position = 0;
|
||||
}
|
||||
|
||||
HTTPSDataSource::~HTTPSDataSource() {
|
||||
_http->close();
|
||||
delete _http;
|
||||
}
|
||||
bool HTTPSDataSource::usable() { return _http; }
|
||||
size_t HTTPSDataSource::read(uint8_t* buf, size_t len) { size_t result = _http->read(buf, len); _position += result; return result; }
|
||||
int HTTPSDataSource::read() { int b = _http->read(); if (b>=0) _position++; return b; }
|
||||
size_t HTTPSDataSource::position() { return _position; }
|
||||
void HTTPSDataSource::seek(size_t position) { return; /* TODO */ }
|
||||
size_t HTTPSDataSource::size() { return _http->getSize(); }
|
||||
void HTTPSDataSource::close() { _http->close(); }
|
139
src/esmp3.cpp
139
src/esmp3.cpp
@ -1,139 +0,0 @@
|
||||
#include <WiFi.h>
|
||||
#include <ArduinoOTA.h>
|
||||
#include <SD.h>
|
||||
#include "spi_master.h"
|
||||
#include "playlist_manager.h"
|
||||
#include "controller.h"
|
||||
#include <Audio.h>
|
||||
#include "esmp3.h"
|
||||
#include <Ticker.h>
|
||||
#include <MFRC522v2.h>
|
||||
#include <MFRC522DriverSPI.h>
|
||||
#include <MFRC522DriverPinSimple.h>
|
||||
#include <MFRC522Debug.h>
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <FTPServer.h>
|
||||
|
||||
Controller controller;
|
||||
Audio audio;
|
||||
PlaylistManager* pm;
|
||||
MFRC522* rfid;
|
||||
FTPServer ftp(SD);
|
||||
|
||||
void setup() {
|
||||
pinMode(PIN_CS_SD, OUTPUT); digitalWrite(PIN_CS_SD, HIGH);
|
||||
pinMode(PIN_CS_RFID, OUTPUT); digitalWrite(PIN_CS_RFID, HIGH);
|
||||
|
||||
Serial.begin(115200);
|
||||
WiFi.begin("Schlenz", "1410WischlingenPanda");
|
||||
log_i("Connecting to WiFi...");
|
||||
uint8_t i=9;
|
||||
while(WiFi.status() != WL_CONNECTED) {
|
||||
Serial.print(i);
|
||||
Serial.print("... ");
|
||||
delay(1000);
|
||||
i--;
|
||||
if (i==0) {
|
||||
Serial.println("Could not connect to WiFi. Restarting in 1s.");
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
Serial.println();
|
||||
Serial.print("Connected to WiFi. IP address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
ArduinoOTA.begin();
|
||||
log_i("Waiting for OTA...");
|
||||
for(int i=0; i<20; i++) {
|
||||
ArduinoOTA.handle();
|
||||
delay(100);
|
||||
}
|
||||
|
||||
Serial.println("Setting up audio...");
|
||||
audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
|
||||
audio.setVolume(1);
|
||||
audio.forceMono(true);
|
||||
audio.setBufsize(30000, -1);
|
||||
audio.setConnectionTimeout(1000, 1000);
|
||||
|
||||
Serial.println("Initializing SPI...");
|
||||
SPI.begin();
|
||||
//SPI.setHwCs(false);
|
||||
//SPIMaster::initialize();
|
||||
|
||||
Serial.print("Initializing SD card...");
|
||||
//SPIMaster::enable_sd();
|
||||
while(!SD.begin(PIN_CS_SD, SPI, 25000000)) {
|
||||
for(int i=0; i<10; i++) {
|
||||
if(SPI.transfer(0xFF)==0xFF) break;
|
||||
delay(10);
|
||||
}
|
||||
Serial.print(".");
|
||||
delay(100);
|
||||
}
|
||||
Serial.println();
|
||||
|
||||
Serial.println("Initializing PlaylistManager...");
|
||||
pm = new PlaylistManager();
|
||||
|
||||
Serial.println("Setting up rfid reader...");
|
||||
pinMode(PIN_CS_RFID, OUTPUT);
|
||||
MFRC522DriverPin* pin = new MFRC522DriverPinSimple(PIN_CS_RFID);
|
||||
MFRC522Driver* spi = new MFRC522DriverSPI(*pin);
|
||||
rfid = new MFRC522(*spi);
|
||||
rfid->PCD_Init();
|
||||
MFRC522Debug::PCD_DumpVersionToSerial(*rfid, Serial);
|
||||
|
||||
Serial.println("Setting up buttons...");
|
||||
pinMode(PIN_BTN_VOL_UP, INPUT_PULLUP);
|
||||
pinMode(PIN_BTN_VOL_DOWN, INPUT_PULLUP);
|
||||
pinMode(PIN_BTN_TRACK_NEXT, INPUT_PULLUP);
|
||||
pinMode(PIN_BTN_TRACK_PREV, INPUT_PULLUP);
|
||||
|
||||
Serial.println("Setup finished.");
|
||||
|
||||
audio.setVolume(12);
|
||||
audio.connecttoFS(SD, "/system/sys_ready.mp3");
|
||||
|
||||
ftp.begin("", "");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
ArduinoOTA.handle();
|
||||
controller.handle();
|
||||
audio.loop();
|
||||
ftp.handleFTP();
|
||||
}
|
||||
|
||||
void audio_info(const char *info){
|
||||
Serial.print("info "); Serial.println(info);
|
||||
}
|
||||
void audio_id3data(const char *info){ //id3 metadata
|
||||
Serial.print("id3data ");Serial.println(info);
|
||||
}
|
||||
void audio_eof_mp3(const char *info){ //end of file
|
||||
Serial.print("eof_mp3 ");Serial.println(info);
|
||||
controller.eof_mp3(info);
|
||||
}
|
||||
void audio_showstation(const char *info){
|
||||
Serial.print("station ");Serial.println(info);
|
||||
}
|
||||
void audio_showstreamtitle(const char *info){
|
||||
Serial.print("streamtitle ");Serial.println(info);
|
||||
}
|
||||
void audio_bitrate(const char *info){
|
||||
Serial.print("bitrate ");Serial.println(info);
|
||||
}
|
||||
void audio_commercial(const char *info){ //duration in sec
|
||||
Serial.print("commercial ");Serial.println(info);
|
||||
}
|
||||
void audio_icyurl(const char *info){ //homepage
|
||||
Serial.print("icyurl ");Serial.println(info);
|
||||
}
|
||||
void audio_lasthost(const char *info){ //stream URL played
|
||||
Serial.print("lasthost ");Serial.println(info);
|
||||
}
|
||||
void audio_eof_speech(const char *info){
|
||||
Serial.print("eof_speech ");Serial.println(info);
|
||||
}
|
210
src/http_client_wrapper.cpp
Normal file
210
src/http_client_wrapper.cpp
Normal 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
144
src/http_server.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
130
src/main.cpp
Normal file
130
src/main.cpp
Normal file
@ -0,0 +1,130 @@
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
#include <SD.h>
|
||||
#include <WiFi.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <SPIFFS.h>
|
||||
#include "config.h"
|
||||
#include "controller.h"
|
||||
#include "player.h"
|
||||
#include "spi_master.h"
|
||||
#include "http_server.h"
|
||||
#include "playlist_manager.h"
|
||||
|
||||
Controller* controller;
|
||||
Player* player;
|
||||
PlaylistManager* pm;
|
||||
HTTPServer* http_server;
|
||||
|
||||
uint8_t SPIMaster::state = 0;
|
||||
|
||||
bool connect_to_wifi(String ssid, String pass) {
|
||||
TRACE("Connecting to wifi \"%s\"...\n", ssid.c_str());
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
WiFi.begin(ssid.c_str(), pass.c_str());
|
||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
||||
DEBUG("Could not connect to wifi \"%s\".\n", ssid.c_str());
|
||||
return false;
|
||||
} else {
|
||||
INFO("Connected to \"%s\". IP address: %s\n", ssid.c_str(), WiFi.localIP().toString().c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void setup() {
|
||||
delay(500);
|
||||
Serial.begin(74880);
|
||||
Serial.println("Starting...");
|
||||
Serial.println("Started.");
|
||||
INFO("Starting.\n");
|
||||
#ifdef VERSION
|
||||
INFO("ESMP3 version %s\n", VERSION);
|
||||
#else
|
||||
INFO("ESMP3, version unknown\n");
|
||||
#endif
|
||||
INFO("Initializing...\n");
|
||||
|
||||
DEBUG("Setting up SPI...\n");
|
||||
SPI.begin();
|
||||
SPI.setHwCs(false);
|
||||
SPIMaster::init();
|
||||
SPIMaster* spi = new SPIMaster();
|
||||
INFO("SPI initialized.\n");
|
||||
|
||||
DEBUG("Setting up SD card...\n");
|
||||
spi->select_sd();
|
||||
if (SD.begin(42, SPI, 25000000)) {
|
||||
INFO("SD card initialized.\n");
|
||||
} else {
|
||||
ERROR("Could not initialize SD card.\n");
|
||||
}
|
||||
spi->select_sd(false);
|
||||
|
||||
DEBUG("Starting SPIFFS...\n");
|
||||
SPIFFS.begin(true);
|
||||
|
||||
DEBUG("Initializing PlaylistManager...\n");
|
||||
pm = new PlaylistManager();
|
||||
|
||||
DEBUG("Initializing Player and Controller...\n");
|
||||
player = new Player(spi);
|
||||
controller = new Controller(player, pm);
|
||||
INFO("Player and controller initialized.\n");
|
||||
|
||||
bool connected = false;
|
||||
INFO("Connecting to WiFi...\n");
|
||||
SPIMaster::select_sd();
|
||||
if (SD.exists("/_wifis.txt")) {
|
||||
DEBUG("Reading /_wifis.txt\n");
|
||||
File f = SD.open("/_wifis.txt", "r");
|
||||
while (String line = f.readStringUntil('\n')) {
|
||||
if (line.length()==0 || line.startsWith("#") || line.indexOf('=')==-1) {
|
||||
continue;
|
||||
}
|
||||
String ssid = line.substring(0, line.indexOf('='));
|
||||
String pass = line.substring(line.indexOf('=')+1);
|
||||
connected = connect_to_wifi(ssid, pass);
|
||||
if (connected) break;
|
||||
}
|
||||
f.close();
|
||||
} else {
|
||||
File f = SD.open("/_wifis.txt", "w");
|
||||
f.print("# WiFi definitions. Syntax: <SSID>=<PASS>. Lines starting with # are ignored. Example:\n# My WiFi=VerySecretPassword\n");
|
||||
f.close();
|
||||
}
|
||||
SPIMaster::select_sd(false);
|
||||
if (!connected) {
|
||||
DEBUG("Trying hardcoded WiFi data...\n");
|
||||
connected = connect_to_wifi(WIFI_SSID, WIFI_PASS);
|
||||
}
|
||||
if (!connected) {
|
||||
INFO("No WiFi connection!\n");
|
||||
}
|
||||
|
||||
MDNS.begin("esmp3");
|
||||
|
||||
DEBUG("Setting up HTTP server...\n");
|
||||
http_server = new HTTPServer(player, controller);
|
||||
controller->register_http_server(http_server);
|
||||
|
||||
DEBUG("Starting NTP client...\n");
|
||||
// Taken from https://github.com/esp8266/Arduino/blob/master/cores/esp8266/TZ.h
|
||||
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "europe.pool.ntp.org");
|
||||
struct tm time;
|
||||
if (getLocalTime(&time, 10000)) {
|
||||
char buffer[100];
|
||||
strftime(buffer, 100, "%Y-%m-%d %H:%M:%S", &time);
|
||||
DEBUG("Got time: %s\n", buffer);
|
||||
} else {
|
||||
INFO("Could not fetch current time via NTP.\n");
|
||||
}
|
||||
|
||||
INFO("Initialization completed.\n");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
bool more_data_needed = player->loop();
|
||||
if (more_data_needed) return;
|
||||
|
||||
controller->loop();
|
||||
}
|
750
src/player.cpp
Normal file
750
src/player.cpp
Normal file
@ -0,0 +1,750 @@
|
||||
// Based on https://github.com/mpflaga/Arduino_Library-vs1053_for_SdFat/blob/master/src/vs1053_SdFat.cpp
|
||||
|
||||
#include "player.h"
|
||||
#include "spi_master.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
//Player::_spi_settings
|
||||
|
||||
Player::Player(SPIMaster* s) {
|
||||
_spi = s;
|
||||
PIN_VS1053_XRESET_SETUP();
|
||||
PIN_VS1053_XRESET(HIGH);
|
||||
PIN_SPEAKER_L_SETUP();
|
||||
PIN_SPEAKER_R_SETUP();
|
||||
_speaker_off();
|
||||
_spi->disable();
|
||||
PIN_VS1053_DREQ_SETUP();
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
void Player::register_controller(Controller* c) {
|
||||
_controller = c;
|
||||
}
|
||||
|
||||
void Player::_reset() {
|
||||
PIN_VS1053_XRESET(LOW);
|
||||
delay(100);
|
||||
PIN_VS1053_XRESET(HIGH);
|
||||
delay(100);
|
||||
_state = uninitialized;
|
||||
_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow
|
||||
}
|
||||
|
||||
void Player::init() {
|
||||
DEBUG("Resetting VS1053...\n");
|
||||
_reset();
|
||||
|
||||
uint16_t result = _read_control_register(SCI_MODE);
|
||||
DEBUG("SCI_MODE: 0x%04X\n", result);
|
||||
if (result != 0x4800) {
|
||||
ERROR("SCI_MODE was 0x%04X, expected was 0x4800. Rebooting.\n", result);
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
}
|
||||
result = _read_control_register(SCI_STATUS);
|
||||
DEBUG("SCI_STATUS: 0x%04X\n", result);
|
||||
if (result != 0x0040 && result != 0x0048) {
|
||||
ERROR("SCI_STATUS was 0x%04X, expected was 0x0040 or 0x0048. Rebooting.\n", result);
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
}
|
||||
result = _read_control_register(SCI_CLOCKF);
|
||||
DEBUG("SCI_CLOCKF: 0x%04X\n", result);
|
||||
|
||||
DEBUG("VS1053 Init looking good.\n");
|
||||
DEBUG("Upping VS1053 multiplier...\n");
|
||||
|
||||
_write_control_register(SCI_CLOCKF, 0xC000);
|
||||
delay(10);
|
||||
|
||||
_spi_settings = &_spi_settings_fast;
|
||||
|
||||
result = _read_control_register(SCI_CLOCKF);
|
||||
DEBUG("SCI_CLOCKF: 0x%04X\n", result);
|
||||
if (result != 0xC000) {
|
||||
ERROR("Error: SCI_CLOCKF was 0x%04X, expected was 0xC000. Rebooting.\n", result);
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
set_volume(VOLUME_DEFAULT);
|
||||
|
||||
INFO("VS1053 initialization completed.\n");
|
||||
|
||||
_state = idle;
|
||||
}
|
||||
|
||||
void Player::_speaker_off() {
|
||||
DEBUG("Speaker off\n");
|
||||
PIN_SPEAKER_L(LOW);
|
||||
PIN_SPEAKER_R(LOW);
|
||||
}
|
||||
|
||||
void Player::_speaker_on() {
|
||||
DEBUG("Speaker on\n");
|
||||
PIN_SPEAKER_L(HIGH);
|
||||
PIN_SPEAKER_R(HIGH);
|
||||
}
|
||||
|
||||
void Player::_sleep() {
|
||||
DEBUG("VS1053 going to sleep.\n");
|
||||
_speaker_off();
|
||||
_write_control_register(SCI_CLOCKF, 0x0000);
|
||||
_spi_settings = &_spi_settings_slow;
|
||||
_write_control_register(SCI_AUDATA, 0x0010);
|
||||
set_volume(0, false);
|
||||
_state = sleeping;
|
||||
TRACE("VS1053 is sleeping now.\n");
|
||||
}
|
||||
|
||||
void Player::_wakeup() {
|
||||
if (_state != sleeping && _state != recording) return;
|
||||
_stopped_at = millis();
|
||||
DEBUG("Waking VS1053...\n");
|
||||
set_volume(_volume, false);
|
||||
_write_control_register(SCI_AUDATA, 0x0000);
|
||||
_write_control_register(SCI_CLOCKF, 0x6000);
|
||||
_write_control_register(SCI_MODE, 0x4800 | SM_RESET);
|
||||
delay(10);
|
||||
//_speaker_on();
|
||||
_spi_settings = &_spi_settings_fast;
|
||||
_state = idle;
|
||||
}
|
||||
|
||||
void Player::_record() {
|
||||
// http://www.vlsi.fi/fileadmin/software/VS10XX/VS1053_VS1063_PcmRecorder.pdf
|
||||
DEBUG("Starting recording.\n");
|
||||
set_volume(1, false);
|
||||
|
||||
// Disable SCI_BASS
|
||||
_write_control_register(SCI_BASS, 0);
|
||||
|
||||
// Disable user applications
|
||||
_write_control_register(SCI_AIADDR, 0);
|
||||
|
||||
// Disable interrupts
|
||||
_write_control_register(SCI_WRAMADDR, 0xC01A);
|
||||
_write_control_register(SCI_WRAM, 0x0002);
|
||||
|
||||
_patch_adpcm();
|
||||
|
||||
_write_control_register(SCI_MODE, SM_ADPCM);
|
||||
|
||||
_write_control_register(SCI_AICTRL0, 0x8000); // Mono VU meter
|
||||
_write_control_register(SCI_AICTRL1, 1024); // Manual gain, 1x
|
||||
_write_control_register(SCI_AICTRL2, 0); // Maximum gain for autogain - ignored
|
||||
_write_control_register(SCI_AICTRL3, 0); // status: record
|
||||
|
||||
_write_control_register(SCI_AIADDR, 0x0034, false);
|
||||
delay(1);
|
||||
|
||||
DEBUG("Recording.\n");
|
||||
delay(10);
|
||||
_state = recording;
|
||||
}
|
||||
|
||||
inline void Player::_wait() {
|
||||
while(!PIN_VS1053_DREQ());
|
||||
}
|
||||
|
||||
uint16_t Player::_read_control_register(uint8_t address, bool do_wait) {
|
||||
if (do_wait) _wait();
|
||||
_spi->select_vs1053_xcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
SPI.transfer(CMD_READ);
|
||||
SPI.transfer(address);
|
||||
uint8_t b1 = SPI.transfer(0xFF);
|
||||
_wait();
|
||||
uint8_t b2 = SPI.transfer(0xFF);
|
||||
_wait();
|
||||
SPI.endTransaction();
|
||||
_spi->select_vs1053_xcs(false);
|
||||
|
||||
return (b1 << 8) | b2;
|
||||
}
|
||||
|
||||
void Player::_write_control_register(uint8_t address, uint16_t value, bool do_wait) {
|
||||
_wait();
|
||||
_spi->select_vs1053_xcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
SPI.transfer(CMD_WRITE);
|
||||
SPI.transfer(address);
|
||||
SPI.transfer(value >> 8);
|
||||
SPI.transfer(value & 0xFF);
|
||||
SPI.endTransaction();
|
||||
_spi->select_vs1053_xcs(false);
|
||||
if (do_wait) _wait();
|
||||
}
|
||||
|
||||
void Player::_patch_adpcm() {
|
||||
static const uint16_t patch_data[] = {
|
||||
0x0007, 0x0001, 0xc01a, 0x0006, 0x0001, 0x0002, 0x0007, 0x0001, /* 0 */
|
||||
0x0008, 0x0006, 0x8002, 0x0000, 0x0007, 0x0001, 0x000c, 0x0006, /* 8 */
|
||||
0x0002, 0x7000, 0x0017, 0x0007, 0x0001, 0x8034, 0x0006, 0x0022, /* 10 */
|
||||
0x0030, 0x0490, 0xb080, 0x0024, 0x3800, 0x0024, 0x0000, 0x1090, /* 18 */
|
||||
0xf400, 0x5404, 0x0000, 0x0851, 0xf400, 0x5648, 0xf400, 0x5404, /* 20 */
|
||||
0xf400, 0x5658, 0xf400, 0x5404, 0xf400, 0x5640, 0x0000, 0x800a, /* 28 */
|
||||
0x2900, 0x9180, 0x0006, 0x2016, 0x2a00, 0x1bce, 0x2a00, 0x114e, /* 30 */
|
||||
0x2a00, 0x168e, 0x0007, 0x0001, 0x1800, 0x0006, 0x8006, 0x0000, /* 38 */
|
||||
0x0007, 0x0001, 0x8045, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, /* 40 */
|
||||
0x7808, 0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, /* 48 */
|
||||
0x7800, 0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x3e80, 0x3e10, /* 50 */
|
||||
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, /* 58 */
|
||||
0x4024, 0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, /* 60 */
|
||||
0x8024, 0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, /* 68 */
|
||||
0x0001, 0x805a, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, 0x7808, /* 70 */
|
||||
0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, 0x7800, /* 78 */
|
||||
0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x5e40, 0x3e10, 0x7800, /* 80 */
|
||||
0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, 0x4024, /* 88 */
|
||||
0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, /* 90 */
|
||||
0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, /* 98 */
|
||||
0x806f, 0x0006, 0x0030, 0x3e12, 0xb817, 0x3e12, 0x7808, 0x3e18, /* a0 */
|
||||
0x3821, 0x3e18, 0xb823, 0x3e10, 0x7800, 0xb880, 0x3855, 0x0030, /* a8 */
|
||||
0x0497, 0x48b2, 0x3c00, 0x0000, 0x800a, 0x2900, 0x7300, 0x3e10, /* b0 */
|
||||
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x6890, 0x1bd5, 0x0030, /* b8 */
|
||||
0x0497, 0x3f00, 0x0024, 0x36f0, 0x5800, 0x36f8, 0x9823, 0x36f8, /* c0 */
|
||||
0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, 0x0030, 0x0717, 0x2100, /* c8 */
|
||||
0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, 0x8010, 0x0006, 0x000e, /* d0 */
|
||||
0x3e02, 0x8024, 0x0001, 0x000a, 0x6012, 0x0024, 0xfea2, 0x0024, /* d8 */
|
||||
0x48b2, 0x1bca, 0x2000, 0x0000, 0x4180, 0x0024, 0x0007, 0x0001, /* e0 */
|
||||
0x8087, 0x0006, 0x00e6, 0x3e00, 0x7843, 0x3e01, 0x3845, 0x3e04, /* e8 */
|
||||
0x3812, 0x0006, 0x08d0, 0x3000, 0x4024, 0x6182, 0x0024, 0x0030, /* f0 */
|
||||
0x06d0, 0x2800, 0x2655, 0xb882, 0x0024, 0x0000, 0x0201, 0x0000, /* f8 */
|
||||
0x0005, 0x0030, 0x0210, 0xa016, 0x4004, 0x1fff, 0xfe01, 0xae1a, /* 100 */
|
||||
0x0024, 0xc342, 0x0024, 0xb882, 0x2001, 0x0030, 0x06d0, 0x3800, /* 108 */
|
||||
0x4024, 0x0006, 0x0890, 0x3004, 0x0024, 0x3000, 0x4024, 0x0006, /* 110 */
|
||||
0x12d0, 0x6182, 0x0024, 0x3000, 0x4024, 0x2800, 0x3e05, 0xf400, /* 118 */
|
||||
0x4050, 0x3009, 0x2000, 0x0006, 0x08d0, 0x0006, 0x0892, 0x3000, /* 120 */
|
||||
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x0030, 0x0250, 0xb882, /* 128 */
|
||||
0x2001, 0x0030, 0x0710, 0x3800, 0x4024, 0x0006, 0x12d0, 0x3000, /* 130 */
|
||||
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x3204, 0x0024, 0x3023, /* 138 */
|
||||
0x0024, 0x30e0, 0xc024, 0x6312, 0x0024, 0x0000, 0x00c3, 0x2800, /* 140 */
|
||||
0x3141, 0x0000, 0x0024, 0x3033, 0x0024, 0x3a04, 0x0024, 0x3000, /* 148 */
|
||||
0x4024, 0x6182, 0x0024, 0x0006, 0x0890, 0x2800, 0x2fd8, 0x0006, /* 150 */
|
||||
0x0301, 0x3a00, 0x4024, 0x0000, 0x00c3, 0x3004, 0x0024, 0x3013, /* 158 */
|
||||
0x0024, 0x3000, 0x4024, 0x0006, 0x12d0, 0x3800, 0x4024, 0x0030, /* 160 */
|
||||
0x0310, 0xf000, 0x0001, 0x6236, 0x0024, 0x001f, 0xffc3, 0x2800, /* 168 */
|
||||
0x3395, 0x0000, 0x0024, 0x0000, 0x0203, 0xa132, 0x0024, 0x001f, /* 170 */
|
||||
0xffc3, 0xb136, 0x0024, 0x6306, 0x0024, 0x0000, 0x0024, 0x2800, /* 178 */
|
||||
0x3611, 0x0000, 0x0024, 0x0020, 0x0003, 0xb132, 0x0024, 0x0000, /* 180 */
|
||||
0x0024, 0x2800, 0x3a85, 0x0000, 0x0024, 0x0000, 0x0081, 0xb212, /* 188 */
|
||||
0x0024, 0x0000, 0x0024, 0x2800, 0x3a05, 0x0000, 0x0024, 0x6892, /* 190 */
|
||||
0x0024, 0xb212, 0x0024, 0x0000, 0x0005, 0x2800, 0x3c55, 0x0030, /* 198 */
|
||||
0x0310, 0x0000, 0x3fc1, 0x3000, 0x8024, 0xb214, 0x0024, 0x003f, /* 1a0 */
|
||||
0xc001, 0xb010, 0x0024, 0xc200, 0x0024, 0x0030, 0x0310, 0x3800, /* 1a8 */
|
||||
0x0024, 0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x5803, 0x2000, /* 1b0 */
|
||||
0x0000, 0x0000, 0x0024, 0x0030, 0x0310, 0x0000, 0x0005, 0x003f, /* 1b8 */
|
||||
0xc001, 0x4088, 0x0002, 0xb214, 0x0024, 0x1fff, 0xfe01, 0xae12, /* 1c0 */
|
||||
0x0024, 0x2800, 0x3a00, 0xc200, 0x0024, 0x2800, 0x28c0, 0x3800, /* 1c8 */
|
||||
0x0024, 0x0007, 0x0001, 0x80fa, 0x0006, 0x00fe, 0x3e12, 0x0024, /* 1d0 */
|
||||
0x3e05, 0xb814, 0x3615, 0x0024, 0x3e00, 0x3841, 0x3e00, 0xb843, /* 1d8 */
|
||||
0x3e01, 0x3845, 0x3e04, 0x3851, 0x0030, 0x10d0, 0x3e04, 0x8024, /* 1e0 */
|
||||
0x3010, 0x0024, 0x3000, 0x8024, 0x0006, 0x1190, 0x3000, 0x4024, /* 1e8 */
|
||||
0x6182, 0x0024, 0x0000, 0x0024, 0x2800, 0x5dd5, 0x0000, 0x0024, /* 1f0 */
|
||||
0x0030, 0x03d0, 0x0000, 0x00c1, 0x3000, 0xc024, 0xb318, 0x0024, /* 1f8 */
|
||||
0x6896, 0x0024, 0x6436, 0x0024, 0x0020, 0x0003, 0x2800, 0x59c5, /* 200 */
|
||||
0x0000, 0x0024, 0x0006, 0x1150, 0x3000, 0x4024, 0x6136, 0x0024, /* 208 */
|
||||
0x0000, 0x0024, 0x2800, 0x4741, 0x0000, 0x0024, 0x0000, 0x0803, /* 210 */
|
||||
0x4132, 0x0024, 0x3800, 0x4024, 0x0006, 0x0190, 0x0006, 0xf011, /* 218 */
|
||||
0x2900, 0xb500, 0x3613, 0x0024, 0x0006, 0xf011, 0x0006, 0x1152, /* 220 */
|
||||
0x0006, 0x0250, 0x4082, 0x0800, 0xfe82, 0x184c, 0x1fff, 0xfc41, /* 228 */
|
||||
0x48ba, 0x0024, 0xae1a, 0x0024, 0x2900, 0xb500, 0x4280, 0x4103, /* 230 */
|
||||
0x0006, 0x1110, 0x4084, 0x0800, 0xfe84, 0x0002, 0x48ba, 0x0024, /* 238 */
|
||||
0xae12, 0x0024, 0xf400, 0x4001, 0x0000, 0x0180, 0x6200, 0x0024, /* 240 */
|
||||
0x0000, 0x0080, 0x2800, 0x5241, 0x4200, 0x0024, 0x3800, 0x0024, /* 248 */
|
||||
0x0006, 0x1090, 0x3004, 0x8024, 0xf400, 0x4491, 0x3113, 0x0024, /* 250 */
|
||||
0x3804, 0x4024, 0x3a00, 0xc024, 0x3004, 0x8024, 0xf400, 0x4491, /* 258 */
|
||||
0x3113, 0x0024, 0x3804, 0x4024, 0x3a00, 0x4024, 0x0006, 0x1081, /* 260 */
|
||||
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x0f00, 0x2800, 0x5248, /* 268 */
|
||||
0x0000, 0x0024, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0080, /* 270 */
|
||||
0x3000, 0x4024, 0x0030, 0x0710, 0xb104, 0x0024, 0x0000, 0x0001, /* 278 */
|
||||
0x3800, 0x4024, 0x0006, 0x08d0, 0x3001, 0x0024, 0x0006, 0x0910, /* 280 */
|
||||
0x3000, 0x4024, 0x6100, 0x0024, 0x6042, 0x0024, 0x0030, 0x06d0, /* 288 */
|
||||
0x2800, 0x5711, 0xb880, 0x0024, 0x2900, 0x21c0, 0x4380, 0x184c, /* 290 */
|
||||
0xb880, 0x0024, 0x3800, 0x0024, 0x36f4, 0x8024, 0x36f4, 0x1811, /* 298 */
|
||||
0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, /* 2a0 */
|
||||
0x36f3, 0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, /* 2a8 */
|
||||
0x0006, 0x1152, 0x0000, 0x0804, 0x3200, 0xc024, 0x6346, 0x0024, /* 2b0 */
|
||||
0x6386, 0x2803, 0x0000, 0x0024, 0x2800, 0x4755, 0x0000, 0x0024, /* 2b8 */
|
||||
0x3800, 0x4024, 0x0030, 0x0690, 0x0000, 0x0081, 0xb882, 0x22c1, /* 2c0 */
|
||||
0x3800, 0x4024, 0x0030, 0x0590, 0x2800, 0x4740, 0x3800, 0x4024, /* 2c8 */
|
||||
0x2800, 0x5700, 0x4190, 0x0024, 0x0007, 0x0001, 0x8179, 0x0006, /* 2d0 */
|
||||
0x00a6, 0x3e12, 0x0024, 0x3e05, 0xb814, 0x3625, 0x0024, 0x3e00, /* 2d8 */
|
||||
0x3841, 0x3e00, 0xb843, 0x3e04, 0x3851, 0x0006, 0x1110, 0x3e04, /* 2e0 */
|
||||
0xb813, 0x3000, 0x0024, 0x6080, 0x0024, 0x0006, 0x11d2, 0x2800, /* 2e8 */
|
||||
0x70c5, 0x0000, 0x0081, 0x6010, 0x984c, 0x3800, 0x0024, 0x0006, /* 2f0 */
|
||||
0x10d0, 0x3200, 0x0024, 0xf100, 0x0011, 0xf100, 0x0024, 0xf102, /* 2f8 */
|
||||
0x0400, 0x0006, 0x1311, 0x2900, 0x0400, 0x3100, 0x8024, 0x0030, /* 300 */
|
||||
0x1293, 0x3413, 0x184c, 0x3c04, 0x4024, 0x3b00, 0x0024, 0x3004, /* 308 */
|
||||
0xc024, 0xf400, 0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3310, /* 310 */
|
||||
0x0024, 0x3a00, 0x0024, 0x0006, 0x1212, 0x3200, 0x0024, 0xf100, /* 318 */
|
||||
0x13d1, 0xf100, 0x0402, 0x2900, 0x0400, 0xf102, 0x0c00, 0x0030, /* 320 */
|
||||
0x12d1, 0x0006, 0x1081, 0x3900, 0x0024, 0x3004, 0xc024, 0xf400, /* 328 */
|
||||
0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3300, 0x0024, 0x3a00, /* 330 */
|
||||
0x0024, 0xf400, 0x4440, 0x6010, 0x0024, 0x1fee, 0xe002, 0x2800, /* 338 */
|
||||
0x6bc8, 0x0006, 0x0f00, 0x3800, 0x0024, 0x0006, 0x0010, 0xb886, /* 340 */
|
||||
0x0040, 0x30f0, 0x4024, 0x6c92, 0x40c3, 0x3810, 0x0024, 0xb182, /* 348 */
|
||||
0x23c1, 0x0006, 0x0950, 0x3000, 0x0024, 0x6090, 0x0024, 0x6cd2, /* 350 */
|
||||
0x2000, 0x0000, 0x0000, 0x2800, 0x70c8, 0x0000, 0x0024, 0x3800, /* 358 */
|
||||
0x0024, 0x0000, 0x0210, 0x3010, 0x0024, 0x30f0, 0x4024, 0x6c92, /* 360 */
|
||||
0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, 0x36f4, 0x9813, 0x36f4, /* 368 */
|
||||
0x1811, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, 0x36f3, /* 370 */
|
||||
0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, 0x0007, /* 378 */
|
||||
0x0001, 0x81cc, 0x0006, 0x00f4, 0x3e00, 0x3841, 0x0000, 0x0201, /* 380 */
|
||||
0x3e00, 0xb843, 0x3e01, 0x3845, 0x3e04, 0x3812, 0x0030, 0x0410, /* 388 */
|
||||
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x08d0, 0x2800, 0x8045, /* 390 */
|
||||
0x0000, 0x0181, 0x6012, 0x0024, 0x0006, 0x1250, 0x2800, 0x7e45, /* 398 */
|
||||
0x0000, 0x05c1, 0x6012, 0x0024, 0x0030, 0x01d0, 0x2800, 0x7c45, /* 3a0 */
|
||||
0x0000, 0x0581, 0x6010, 0x03cc, 0x0000, 0x0024, 0x2800, 0x7a95, /* 3a8 */
|
||||
0x0000, 0x0024, 0x3000, 0x8024, 0x0006, 0x1250, 0x3000, 0x0024, /* 3b0 */
|
||||
0x6092, 0x0024, 0x3800, 0x4024, 0xf400, 0x4010, 0x3800, 0x8024, /* 3b8 */
|
||||
0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, /* 3c0 */
|
||||
0x2000, 0x0000, 0x0000, 0x0024, 0x0030, 0x01d0, 0x3000, 0x0024, /* 3c8 */
|
||||
0x0006, 0x1250, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3d0 */
|
||||
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3d8 */
|
||||
0x6090, 0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3e0 */
|
||||
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3e8 */
|
||||
0x6080, 0x0024, 0x0000, 0x0024, 0x2800, 0x8515, 0x0000, 0x0024, /* 3f0 */
|
||||
0x0006, 0x1350, 0x0000, 0x0082, 0x0030, 0x0352, 0xb886, 0x0040, /* 3f8 */
|
||||
0x30f0, 0x4024, 0x4cd2, 0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, /* 400 */
|
||||
0x3a00, 0x0024, 0x3010, 0x0024, 0x30f0, 0x4024, 0x0030, 0x0390, /* 408 */
|
||||
0x2800, 0x7a80, 0x4180, 0x2001, 0x4090, 0x0024, 0x3800, 0x0024, /* 410 */
|
||||
0x0030, 0x0250, 0x3800, 0x0024, 0x0006, 0x1290, 0x3000, 0x0024, /* 418 */
|
||||
0x6090, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x8024, /* 420 */
|
||||
0x3223, 0x0024, 0x32e0, 0x4024, 0x6100, 0x0024, 0x0000, 0x0024, /* 428 */
|
||||
0x2800, 0x8c81, 0x0000, 0x0024, 0x3233, 0x0024, 0x3804, 0x8024, /* 430 */
|
||||
0x3200, 0x0024, 0x6080, 0x0024, 0x0006, 0x0300, 0x2800, 0x8b18, /* 438 */
|
||||
0x0000, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x0024, /* 440 */
|
||||
0x3013, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, 0x3800, 0x0024, /* 448 */
|
||||
0x0006, 0x0850, 0x3004, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, /* 450 */
|
||||
0x6080, 0x0024, 0x3000, 0x0024, 0x2800, 0x9115, 0xf400, 0x4010, /* 458 */
|
||||
0x3000, 0x0024, 0x0000, 0x0201, 0x0000, 0x0005, 0x0030, 0x0210, /* 460 */
|
||||
0xa014, 0x4004, 0x1fff, 0xfe01, 0xae12, 0x0024, 0xc200, 0x0024, /* 468 */
|
||||
0x2800, 0x8180, 0x3800, 0x0024, 0x2800, 0x8ec0, 0x3009, 0x0000, /* 470 */
|
||||
0x0007, 0x0001, 0x8246, 0x0006, 0x0104, 0x0030, 0x1092, 0x0007, /* 478 */
|
||||
0x9250, 0x003f, 0xfc42, 0xb880, 0x184c, 0x3e12, 0x0024, 0x3800, /* 480 */
|
||||
0x0024, 0x0030, 0x0290, 0x38f0, 0x0024, 0x3800, 0x0024, 0x0030, /* 488 */
|
||||
0x0050, 0x3000, 0x4024, 0xb122, 0x0024, 0x6894, 0x2001, 0x0000, /* 490 */
|
||||
0x0141, 0x3a70, 0x4024, 0x0004, 0x1fc1, 0x3a00, 0x4024, 0x0030, /* 498 */
|
||||
0x00d2, 0x0030, 0x0001, 0x3a00, 0x4024, 0x0030, 0x0552, 0x3a10, /* 4a0 */
|
||||
0x0024, 0x3a00, 0x0024, 0x3000, 0x4024, 0xc122, 0x0024, 0x3800, /* 4a8 */
|
||||
0x4024, 0x0030, 0x05d0, 0x0000, 0x03c1, 0x3820, 0x4024, 0x3800, /* 4b0 */
|
||||
0x0024, 0x0000, 0x0310, 0x3010, 0x0024, 0x30f0, 0x4024, 0xf2c2, /* 4b8 */
|
||||
0x0024, 0x3810, 0x0024, 0x0000, 0x3fc0, 0x38f0, 0x4024, 0x0030, /* 4c0 */
|
||||
0x02d0, 0x3000, 0x4024, 0x2912, 0x1400, 0xb104, 0x0024, 0x0006, /* 4c8 */
|
||||
0x1312, 0x6802, 0x0024, 0x000d, 0xac00, 0x6012, 0x2801, 0x0000, /* 4d0 */
|
||||
0x0024, 0x2800, 0x9dc1, 0x0000, 0x0024, 0x3a00, 0x0024, 0x2909, /* 4d8 */
|
||||
0x1b40, 0x3613, 0x0024, 0x0000, 0x0084, 0x0000, 0x1905, 0x2908, /* 4e0 */
|
||||
0xbe80, 0x3613, 0x0024, 0x0000, 0x0000, 0x0006, 0x0302, 0x4002, /* 4e8 */
|
||||
0x0024, 0x4012, 0x0024, 0x4212, 0x0024, 0xf400, 0x4050, 0x3000, /* 4f0 */
|
||||
0x4024, 0x6182, 0x0024, 0x0006, 0x0350, 0x2800, 0xa6c8, 0x0000, /* 4f8 */
|
||||
0x0024, 0x4002, 0x0024, 0x4014, 0x0024, 0x0006, 0x0301, 0x4124, /* 500 */
|
||||
0x0024, 0x0000, 0x0081, 0x4212, 0x0024, 0x4002, 0x4050, 0x4014, /* 508 */
|
||||
0x0003, 0x0006, 0x0301, 0x4122, 0x0024, 0x6192, 0x0024, 0x6090, /* 510 */
|
||||
0x4050, 0x3000, 0x4024, 0x0006, 0x0910, 0x6312, 0x0024, 0x6194, /* 518 */
|
||||
0x0001, 0x4122, 0x0024, 0x2800, 0x9f80, 0x3800, 0x4024, 0x0006, /* 520 */
|
||||
0x12d2, 0x0006, 0x0991, 0x3000, 0x0024, 0x0006, 0x1290, 0x3a00, /* 528 */
|
||||
0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x2900, 0xb200, 0x0000, /* 530 */
|
||||
0x0580, 0x0030, 0x0210, 0x0014, 0x9240, 0x003f, 0xf502, 0x003f, /* 538 */
|
||||
0xffc3, 0x3800, 0x0024, 0x0000, 0x0580, 0x0006, 0x1350, 0x3200, /* 540 */
|
||||
0x4024, 0x4102, 0x0024, 0x3a00, 0x4024, 0x3810, 0x8024, 0x38f0, /* 548 */
|
||||
0xc024, 0x0006, 0x08d0, 0x3800, 0x0024, 0x0030, 0x0690, 0x0000, /* 550 */
|
||||
0x8280, 0xb880, 0x2080, 0x3800, 0x0024, 0x6890, 0x2000, 0x0030, /* 558 */
|
||||
0x0490, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0100, 0x3000, /* 560 */
|
||||
0x584c, 0xb100, 0x0024, 0x0000, 0x0024, 0x2800, 0xb185, 0x0000, /* 568 */
|
||||
0x0024, 0x003f, 0xfec1, 0x3000, 0x1bcc, 0xb010, 0x0024, 0x2908, /* 570 */
|
||||
0x0b80, 0x3800, 0x0024, 0x3613, 0x0024, 0x2910, 0x0180, 0x0000, /* 578 */
|
||||
0xae48, 0x0007, 0x0001, 0x1806, 0x0006, 0x8007, 0x0000, 0x0006, /* 580 */
|
||||
0x002f, 0x0010, 0x17ff, 0x0000, 0x1a00, 0x1dff, 0x0000, 0x1f00, /* 588 */
|
||||
0x3fff, 0x0001, 0x0000, 0x17ff, 0x0001, 0x1c00, 0x3fff, 0x0001, /* 590 */
|
||||
0xe000, 0xfffd, 0xffff, 0x0000, 0x0000, 0x180c, 0x180c, 0x0000, /* 598 */
|
||||
0x0000, 0x0000, 0x4952, 0x4646, 0xffff, 0xffff, 0x4157, 0x4556, /* 5a0 */
|
||||
0x6d66, 0x2074, 0x0010, 0x0000, 0x0001, 0x0001, 0xbb80, 0x0000, /* 5a8 */
|
||||
0x7700, 0x0001, 0x0002, 0x0010, 0x6164, 0x6174, 0xffff, 0xffff, /* 5b0 */
|
||||
0x0006, 0x8006, 0x0000, 0x0006, 0x0005, 0x183c, 0x183c, 0x0000, /* 5b8 */
|
||||
0x0020, 0x0040, 0x0006, 0x8003, 0x0000, 0x0007, 0x0001, 0x5bc0, /* 5c0 */
|
||||
0x0006, 0x0009, 0x801c, 0x7fe4, 0x8039, 0x804e, 0x7fb2, 0x809d, /* 5c8 */
|
||||
0x809c, 0x7f64, 0x8139, 0x0007, 0x0001, 0x82c8, 0x0006, 0x0018, /* 5d0 */
|
||||
0x4080, 0x184c, 0x3e13, 0x780f, 0x2800, 0xb405, 0x4090, 0x380e, /* 5d8 */
|
||||
0x2400, 0xb3c0, 0xf400, 0x4417, 0x3110, 0x0024, 0x3f10, 0x0024, /* 5e0 */
|
||||
0x36f3, 0x8024, 0x36f3, 0x580f, 0x2000, 0x0000, 0x0000, 0x0024, /* 5e8 */
|
||||
0x0007, 0x0001, 0x82d4, 0x0006, 0x002a, 0x3e11, 0xb807, 0x3009, /* 5f0 */
|
||||
0x384a, 0x3e11, 0x3805, 0x3e10, 0xb803, 0x3e00, 0x4442, 0x0001, /* 5f8 */
|
||||
0x800a, 0xbf8e, 0x8443, 0xfe06, 0x0045, 0x3011, 0x0401, 0x545e, /* 600 */
|
||||
0x0385, 0x525e, 0x2040, 0x72ce, 0x1bc1, 0x48ba, 0x9803, 0x4588, /* 608 */
|
||||
0x4885, 0x6fee, 0x1bc2, 0x4ffe, 0x9805, 0xf6fe, 0x1bc4, 0xf7f0, /* 610 */
|
||||
0x2046, 0x3801, 0xdbca, 0x2000, 0x0000, 0x36f1, 0x9807,
|
||||
};
|
||||
const uint16_t patch_size = 1567;
|
||||
DEBUG("Patching...\n");
|
||||
|
||||
_spi->select_vs1053_xcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
|
||||
for (int i=0; i<patch_size; i++) {
|
||||
DEBUG(" %d\n", i);
|
||||
unsigned short addr, n, val;
|
||||
addr = patch_data[i++];
|
||||
n = patch_data[i++];
|
||||
SPI.transfer(CMD_WRITE);
|
||||
SPI.transfer(addr & 0XFF);
|
||||
if (n & 0x8000U) { /* RLE run, replicate n samples */
|
||||
n &= 0x7FFF;
|
||||
val = patch_data[i++];
|
||||
while (n--) {
|
||||
SPI.transfer(val >> 8);
|
||||
SPI.transfer(val & 0xFF);
|
||||
_wait();
|
||||
}
|
||||
} else { /* Copy run, copy n samples */
|
||||
while (n--) {
|
||||
val = patch_data[i++];
|
||||
SPI.transfer(val >> 8);
|
||||
SPI.transfer(val & 0xFF);
|
||||
_wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SPI.endTransaction();
|
||||
_spi->select_vs1053_xcs(false);
|
||||
|
||||
DEBUG("Patch sent.\n");
|
||||
}
|
||||
|
||||
void Player::_write_data(uint8_t* buffer) {
|
||||
_spi->select_vs1053_xdcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
for (uint i=0; i<sizeof(_buffer); i++) {
|
||||
SPI.transfer(_buffer[i]);
|
||||
}
|
||||
SPI.endTransaction();
|
||||
_spi->select_vs1053_xdcs(false);
|
||||
}
|
||||
|
||||
uint16_t Player::_read_wram(uint16_t address) {
|
||||
DEBUG("Reading WRAM address 0x%04X...\n", address);
|
||||
_write_control_register(SCI_WRAMADDR, address);
|
||||
uint16_t r1 = _read_control_register(SCI_WRAM);
|
||||
_write_control_register(SCI_WRAMADDR, address);
|
||||
uint16_t r2 = _read_control_register(SCI_WRAM);
|
||||
if (r1 == r2) return r1;
|
||||
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
|
||||
_write_control_register(SCI_WRAMADDR, address);
|
||||
r1 = _read_control_register(SCI_WRAM);
|
||||
if (r1 == r2) return r1;
|
||||
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
|
||||
_write_control_register(SCI_WRAMADDR, address);
|
||||
r2 = _read_control_register(SCI_WRAM);
|
||||
if (r1 == r2) return r1;
|
||||
DEBUG("Reading WRAM resulted in different values: 0x%04X and 0x%04X.\n", r1, r2);
|
||||
DEBUG("Returning last value (0x%04X)...\n", r2);
|
||||
return r2;
|
||||
}
|
||||
|
||||
int8_t Player::_get_endbyte() {
|
||||
int8_t endbyte = _read_wram(ADDR_ENDBYTE) & 0xFF;
|
||||
DEBUG("Endbyte is 0x%02X.\n", endbyte);
|
||||
return endbyte;
|
||||
}
|
||||
|
||||
|
||||
void Player::set_volume(uint8_t vol, bool save) {
|
||||
if (save) {
|
||||
_volume = vol;
|
||||
}
|
||||
INFO("Setting volume to %d\n", vol);
|
||||
vol = 0xFF - vol;
|
||||
uint16_t value = (vol<<8)|vol;
|
||||
DEBUG("Setting volume register to 0x%04X\n", value);
|
||||
_write_control_register(SCI_VOL, value);
|
||||
}
|
||||
|
||||
void Player::vol_up() {
|
||||
if (!is_playing()) return;
|
||||
uint8_t vol = _volume + VOLUME_STEP;
|
||||
if (vol > VOLUME_MAX) vol=VOLUME_MAX;
|
||||
set_volume(vol);
|
||||
}
|
||||
|
||||
void Player::vol_down() {
|
||||
if (!is_playing()) return;
|
||||
uint8_t vol = _volume - VOLUME_STEP;
|
||||
if (vol < VOLUME_MIN) vol=VOLUME_MIN;
|
||||
set_volume(vol);
|
||||
}
|
||||
|
||||
void Player::_mute() {
|
||||
INFO("Muting.\n");
|
||||
_speaker_off();
|
||||
set_volume(1, false);
|
||||
}
|
||||
|
||||
void Player::_unmute() {
|
||||
INFO("Unmuting.\n");
|
||||
set_volume(_volume, false);
|
||||
_speaker_on();
|
||||
}
|
||||
|
||||
void Player::track_next() {
|
||||
if (_state != playing) return;
|
||||
if (!_current_playlist->has_track_next()) {
|
||||
return;
|
||||
}
|
||||
stop();
|
||||
_current_playlist->track_next();
|
||||
play();
|
||||
}
|
||||
|
||||
void Player::track_prev() {
|
||||
if (_state != playing) return;
|
||||
if (_current_play_position > 100000) {
|
||||
stop();
|
||||
_current_playlist->track_restart();
|
||||
play();
|
||||
} else {
|
||||
if (!_current_playlist->has_track_prev()) {
|
||||
return;
|
||||
}
|
||||
stop();
|
||||
_current_playlist->track_prev();
|
||||
play();
|
||||
}
|
||||
}
|
||||
|
||||
void Player::set_track(uint8_t id) {
|
||||
stop();
|
||||
_current_playlist->set_track(id);
|
||||
play();
|
||||
}
|
||||
|
||||
bool Player::is_playing() {
|
||||
return _state == playing;
|
||||
}
|
||||
|
||||
bool Player::play(Playlist* p) {
|
||||
_current_playlist = p;
|
||||
return play();
|
||||
}
|
||||
|
||||
bool Player::play() {
|
||||
if (_state == sleeping || _state == recording) _wakeup();
|
||||
if (_state != idle) return false;
|
||||
if (_current_playlist == NULL) return false;
|
||||
if (_current_playlist->get_file_count()==0) return false;
|
||||
_current_playlist->start();
|
||||
String file = _current_playlist->get_current_file();
|
||||
uint32_t position = _current_playlist->get_position();
|
||||
_state = playing;
|
||||
_play_file(file, position);
|
||||
_controller->send_player_status();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Player::_play_file(String file, uint32_t file_offset) {
|
||||
INFO("play_file('%s', %d)\n", file.c_str(), file_offset);
|
||||
_spi->select_sd();
|
||||
if (file.startsWith("/")) {
|
||||
_file = new SDDataSource(file);
|
||||
} else if (file.startsWith("http")) {
|
||||
_file = new HTTPSDataSource(file);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
_file_size = _file->size();
|
||||
_spi->select_sd(false);
|
||||
if (!_file || !_file->usable()) {
|
||||
DEBUG("Could not open file %s", file.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG("Resetting SCI_DECODE_TIME...\n");
|
||||
_write_control_register(SCI_DECODE_TIME, 0);
|
||||
DEBUG("Resetting SS_DO_NOT_JUMP...\n");
|
||||
_write_control_register(SCI_STATUS, _read_control_register(SCI_STATUS) & ~SS_DO_NOT_JUMP);
|
||||
delay(100);
|
||||
|
||||
_spi->select_sd();
|
||||
if (file_offset == 0) {
|
||||
_file->skip_id3_tag();
|
||||
}
|
||||
_refills = 0;
|
||||
_current_play_position = _file->position();
|
||||
_spi->select_sd(false);
|
||||
_skip_to = file_offset;
|
||||
if (_skip_to>0) _mute();
|
||||
else _speaker_on();
|
||||
INFO("Now playing.\n");
|
||||
_controller->send_player_status();
|
||||
}
|
||||
|
||||
void Player::_flush(uint count, int8_t byte) {
|
||||
_spi->select_vs1053_xdcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
for(uint i=0; i<count; i++) {
|
||||
_wait();
|
||||
SPI.transfer(byte);
|
||||
}
|
||||
SPI.endTransaction();
|
||||
_spi->select_vs1053_xdcs(false);
|
||||
}
|
||||
|
||||
void Player::_finish_playing() {
|
||||
uint8_t endbyte = _get_endbyte();
|
||||
_flush(2052, endbyte);
|
||||
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
|
||||
for (int i=0; i<64; i++) {
|
||||
_flush(32, endbyte);
|
||||
uint16_t mode = _read_control_register(SCI_MODE);
|
||||
if ((mode & SM_CANCEL) == 0) return;
|
||||
}
|
||||
// If we reached this, the Chip didn't stop. That should not happen.
|
||||
// (That's written in the manual.)
|
||||
// Reset the chip.
|
||||
init();
|
||||
}
|
||||
|
||||
void Player::stop(bool turn_speaker_off) {
|
||||
if (_state != playing) return;
|
||||
INFO("Stopping...\n");
|
||||
_current_playlist->set_position(_current_play_position);
|
||||
|
||||
_state = stopping;
|
||||
_stop_delay = 0;
|
||||
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
|
||||
uint8_t endbyte = _get_endbyte();
|
||||
while (true) {
|
||||
_refill();
|
||||
uint16_t mode = _read_control_register(SCI_MODE);
|
||||
if ((mode & SM_CANCEL) == 0) {
|
||||
_flush(2052, endbyte);
|
||||
_finish_stopping(turn_speaker_off);
|
||||
break;
|
||||
} else if (_stop_delay > 2048) {
|
||||
init();
|
||||
break;
|
||||
}
|
||||
_stop_delay++;
|
||||
}
|
||||
}
|
||||
|
||||
void Player::_finish_stopping(bool turn_speaker_off) {
|
||||
if (turn_speaker_off) _speaker_off();
|
||||
_state = idle;
|
||||
_stopped_at = millis();
|
||||
if (_file) {
|
||||
_file->close();
|
||||
delete _file;
|
||||
}
|
||||
_current_play_position = 0;
|
||||
_file_size = 0;
|
||||
INFO("Stopped.\n");
|
||||
_controller->send_player_status();
|
||||
}
|
||||
|
||||
void Player::_refill() {
|
||||
_spi->select_sd();
|
||||
_refills++;
|
||||
if (_refills % 1000 == 0) DEBUG(".");
|
||||
uint8_t result = _file->read(_buffer, sizeof(_buffer));
|
||||
_spi->select_sd(false);
|
||||
if (result == 0) {
|
||||
// File is over.
|
||||
DEBUG("EOF reached.\n");
|
||||
_skip_to = 0;
|
||||
_finish_playing();
|
||||
_finish_stopping(false);
|
||||
if (_current_playlist->has_track_next()) {
|
||||
_current_playlist->track_next();
|
||||
play();
|
||||
} else {
|
||||
_current_playlist->reset();
|
||||
_controller->send_player_status();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
_current_play_position+=result;
|
||||
_write_data(_buffer);
|
||||
|
||||
if (_skip_to > 0) {
|
||||
if (_skip_to > _file->position()) {
|
||||
uint16_t status = _read_control_register(SCI_STATUS);
|
||||
if ((status & SS_DO_NOT_JUMP) == 0) {
|
||||
DEBUG("Skipping to %d.\n", _skip_to);
|
||||
_flush(2048, _get_endbyte());
|
||||
_spi->select_sd();
|
||||
_file->seek(_skip_to);
|
||||
_spi->select_sd(false);
|
||||
_skip_to = 0;
|
||||
_unmute();
|
||||
_controller->send_position();
|
||||
}
|
||||
} else {
|
||||
_skip_to = 0;
|
||||
_unmute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Player::_refill_needed() {
|
||||
return _state==playing || _state==stopping;
|
||||
}
|
||||
|
||||
bool Player::loop() {
|
||||
if (PIN_VS1053_DREQ() && _refill_needed()) {
|
||||
_refill();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_state == recording) {
|
||||
DEBUG("r");
|
||||
uint16_t samples_available = _read_control_register(SCI_HDAT1, false);
|
||||
uint16_t vu_value = _read_control_register(SCI_AICTRL0, false);
|
||||
DEBUG("Samples available: %4d, VU meter: 0x%04X\n", samples_available, vu_value);
|
||||
if (samples_available >= 500) {
|
||||
unsigned long sum = 0;
|
||||
for (int i=0; i<500; i++) {
|
||||
uint16_t sample = _read_control_register(SCI_HDAT0, false);
|
||||
sum += sample * sample;
|
||||
}
|
||||
double result = sqrt(sum / 500);
|
||||
DEBUG("Loudness: %f", result);
|
||||
}
|
||||
}
|
||||
|
||||
if (_state == idle && _stopped_at < millis() - VS1053_SLEEP_DELAY) {
|
||||
_sleep();
|
||||
//_record();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String Player::json() {
|
||||
DynamicJsonDocument json(10240);
|
||||
json["_type"] = "player";
|
||||
json["playing"] = is_playing();
|
||||
if (_current_playlist) {
|
||||
JsonObject playlist = json.createNestedObject("playlist");
|
||||
_current_playlist->json(playlist);
|
||||
} else {
|
||||
json["playlist"] = nullptr;
|
||||
}
|
||||
JsonObject volume = json.createNestedObject("volume");
|
||||
volume["current"] = _volume;
|
||||
volume["min"] = VOLUME_MIN;
|
||||
volume["max"] = VOLUME_MAX;
|
||||
volume["step"] = VOLUME_STEP;
|
||||
return json.as<String>();
|
||||
}
|
||||
|
||||
String Player::position_json() {
|
||||
if (!is_playing()) return "null";
|
||||
DynamicJsonDocument json(200);
|
||||
json["_type"] = "position";
|
||||
json["position"] = _current_play_position;
|
||||
json["file_size"] = _file_size;
|
||||
return json.as<String>();
|
||||
}
|
395
src/playlist.cpp
395
src/playlist.cpp
@ -1,82 +1,347 @@
|
||||
#include "playlist.h"
|
||||
#include <playlist.h>
|
||||
#include "spi_master.h"
|
||||
#include "config.h"
|
||||
#include <SD.h>
|
||||
#include <algorithm>
|
||||
#include <ArduinoJson.h>
|
||||
#include <TinyXML.h>
|
||||
|
||||
Playlist::Playlist() {}
|
||||
|
||||
Playlist::Playlist(String id, PersistedPlaylist* p) {
|
||||
rfid_id = id;
|
||||
pp = p;
|
||||
}
|
||||
|
||||
String Playlist::get_rfid_id() {
|
||||
return rfid_id;
|
||||
}
|
||||
|
||||
void Playlist::add_file(String filename) {
|
||||
files.push_back(filename);
|
||||
}
|
||||
|
||||
void Playlist::sort() {
|
||||
std::sort(files.begin(), files.end());
|
||||
}
|
||||
|
||||
void Playlist::set_current_position(uint8_t file, uint32_t bytes) {
|
||||
log_d("Setting position: File %d, bytes %d.", file, bytes);
|
||||
current_file = file;
|
||||
current_time = bytes;
|
||||
save_current_position();
|
||||
}
|
||||
|
||||
void Playlist::save_current_position(uint32_t position) {
|
||||
if (position==0) {
|
||||
position = current_time;
|
||||
}
|
||||
log_d("Saving current position: File %d, bytes %d.", current_file, position);
|
||||
if (pp != NULL) {
|
||||
pp->file = current_file;
|
||||
pp->position = position;
|
||||
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;
|
||||
}
|
||||
|
||||
String Playlist::get_current_file_name() {
|
||||
if (current_file >= files.size()) {
|
||||
Serial.printf("Requested a file number %d, which is not available in this playlist. Starting over.\n", current_file);
|
||||
set_current_position(0);
|
||||
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;
|
||||
}
|
||||
return files[current_file];
|
||||
}
|
||||
|
||||
bool Playlist::next_track() {
|
||||
if (files.size() <= current_file + 1) {
|
||||
Serial.println("next_track does not exist. Resetting playlist and returning false.");
|
||||
set_current_position(0, 0);
|
||||
return false;
|
||||
_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());
|
||||
}
|
||||
set_current_position(current_file + 1, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Playlist::prev_track() {
|
||||
log_d("Playlist::prev_track called. current_file is %d", current_file);
|
||||
if (current_file == 0) {
|
||||
set_current_position(0, 0);
|
||||
} else {
|
||||
set_current_position(current_file - 1, 0);
|
||||
TRACE(" Ignoring entry %s\n", filename.c_str());
|
||||
}
|
||||
return files.size()>0;
|
||||
entry.close();
|
||||
}
|
||||
dir.close();
|
||||
SPIMaster::select_sd(false);
|
||||
std::sort(_files.begin(), _files.end());
|
||||
}
|
||||
|
||||
void Playlist::restart() {
|
||||
current_time = 0;
|
||||
void Playlist::_examine_http_url(String url) {
|
||||
HTTPClientWrapper* http = new HTTPClientWrapper();
|
||||
if (!http->get(url)) {
|
||||
DEBUG("Could not GET %s.\n", url.c_str());
|
||||
return;
|
||||
}
|
||||
String ct = http->getContentType();
|
||||
DEBUG("Content-Type is %s.\n", ct.c_str());
|
||||
if (ct.startsWith("audio/x-mpegurl")) {
|
||||
_parse_m3u(http);
|
||||
} else if (ct.startsWith("audio/")) {
|
||||
_files.push_back({.filename=url, .title=url});
|
||||
} else if (ct.startsWith("application/rss+xml")) {
|
||||
_parse_rss(http);
|
||||
} else if (ct.startsWith("application/pls+xml")) {
|
||||
_parse_pls(http);
|
||||
} else {
|
||||
ERROR("Unknown content type %s.\n", ct.c_str());
|
||||
}
|
||||
http->close();
|
||||
delete http;
|
||||
}
|
||||
|
||||
void Playlist::set_current_time(uint32_t pos) {
|
||||
set_current_position(current_file, pos);
|
||||
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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Playlist::get_current_time() {
|
||||
return current_time;
|
||||
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::shuffle() {
|
||||
std::random_shuffle(files.begin(), files.end());
|
||||
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();
|
||||
}
|
@ -1,17 +1,32 @@
|
||||
#include "playlist_manager.h"
|
||||
#include "spi_master.h"
|
||||
#include <SD.h>
|
||||
#include "spi_master.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
PlaylistManager::PlaylistManager() {
|
||||
SPIMaster::enable_sd();
|
||||
current_rfid_tag_id = String("");
|
||||
scan_files();
|
||||
}
|
||||
|
||||
if (!SD.exists("/_mapping.txt")) {
|
||||
Serial.println("WARNING: /_mapping.txt not found!");
|
||||
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 {
|
||||
map.clear();
|
||||
File f = SD.open("/_mapping.txt");
|
||||
Serial.println(" Reading /_mapping.txt...");
|
||||
DEBUG("Reading /_mapping.txt...\n");
|
||||
while (f.available()) {
|
||||
char buffer[512];
|
||||
size_t pos = f.readBytesUntil('\n', buffer, 511);
|
||||
@ -22,64 +37,130 @@ PlaylistManager::PlaylistManager() {
|
||||
if (eq>0 && eq<data.length()-1) {
|
||||
String rfid_id = data.substring(0, eq);
|
||||
String folder = data.substring(eq + 1);
|
||||
Serial.printf(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
|
||||
map[rfid_id] = PersistedPlaylist(folder);
|
||||
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();
|
||||
|
||||
Playlist PlaylistManager::get_playlist(String rfid_id) {
|
||||
if (rfid_id.equals(current_rfid_tag_id)) {
|
||||
return current_playlist;
|
||||
} else {
|
||||
if (map.count(rfid_id)==0) {
|
||||
Serial.printf("No known playlist for id %s.\n", rfid_id);
|
||||
return current_playlist;
|
||||
} else {
|
||||
PersistedPlaylist* ap = &(map[rfid_id]);
|
||||
log_d("PP status is: File %d, bytes %d.", ap->file, ap->position);
|
||||
current_playlist = Playlist(rfid_id, ap);
|
||||
String path = ap->dir;
|
||||
if (path.startsWith("/")) {
|
||||
File dir = SD.open(path);
|
||||
while(File entry = dir.openNextFile()) {
|
||||
_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();
|
||||
String ext = filename.substring(filename.length()-4);
|
||||
if (!entry.isDirectory() &&
|
||||
!filename.startsWith(".") &&
|
||||
ext.equals(".mp3")) {
|
||||
Serial.printf("Adding %s to the list of files\n", (path + "/" + filename).c_str());
|
||||
current_playlist.add_file(path + "/" + filename);
|
||||
filename = filename.substring(folder.length() + 1);
|
||||
if (!filename.startsWith(".") && filename.endsWith(".mp3")) {
|
||||
found = true;
|
||||
}
|
||||
entry.close();
|
||||
if (found) break;
|
||||
}
|
||||
dir.close();
|
||||
current_playlist.set_current_position(ap->file, ap->position);
|
||||
} else if (path.startsWith("http")) {
|
||||
Serial.printf("Adding URL %s to the list of files\n", path.c_str());
|
||||
current_playlist.add_file(path);
|
||||
if (found) {
|
||||
_unmapped_folders.push_back(folder);
|
||||
}
|
||||
current_playlist.sort();
|
||||
current_rfid_tag_id = rfid_id;
|
||||
return current_playlist;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PlaylistManager::set_audio_current_time(uint32_t time) {
|
||||
audio_current_time = time;
|
||||
Playlist* PlaylistManager::get_playlist_for_id(String id) {
|
||||
if (!_map.count(id)) return NULL;
|
||||
String folder = _map[id];
|
||||
return get_playlist_for_folder(folder);
|
||||
}
|
||||
|
||||
bool PlaylistManager::has_playlist(String rfid_id) {
|
||||
return map.count(rfid_id) == 1;
|
||||
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
|
||||
if (!_playlists.count(folder)) {
|
||||
_playlists[folder] = new Playlist(folder);
|
||||
}
|
||||
return _playlists[folder];
|
||||
}
|
||||
|
||||
String PlaylistManager::pp_to_String() {
|
||||
String s = "";
|
||||
for(const auto& kv : map) {
|
||||
s += kv.first + "=" + kv.second.file + "," + kv.second.position + '\n';
|
||||
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;
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
#include "spi_master.h"
|
||||
#include <Arduino.h>
|
||||
#include "esmp3.h"
|
||||
|
||||
void SPIMaster::initialize() {
|
||||
pinMode(PIN_CS_SD, OUTPUT);
|
||||
pinMode(PIN_CS_RFID, OUTPUT);
|
||||
disable_all();
|
||||
}
|
||||
|
||||
void SPIMaster::disable_all() {
|
||||
digitalWrite(PIN_CS_SD, HIGH);
|
||||
digitalWrite(PIN_CS_RFID, HIGH);
|
||||
}
|
||||
|
||||
void SPIMaster::enable_rfid() {
|
||||
disable_all();
|
||||
digitalWrite(PIN_CS_RFID, LOW);
|
||||
delay(5);
|
||||
}
|
||||
|
||||
void SPIMaster::enable_sd() {
|
||||
disable_all();
|
||||
digitalWrite(PIN_CS_SD, LOW);
|
||||
delay(5);
|
||||
}
|
Loading…
Reference in New Issue
Block a user