Compare commits
42 Commits
0.1
...
01f513c97b
Author | SHA1 | Date | |
---|---|---|---|
01f513c97b | |||
3bfbea92d8 | |||
d818624287 | |||
d92388d11f | |||
37df309127 | |||
be8a124803 | |||
104236dd0c | |||
e1dd004cf5 | |||
b5ec78ab41 | |||
fff9d9bc61 | |||
ef47c771ef | |||
9f442259e9 | |||
8e5a3195b9 | |||
cc4729eb6b | |||
f7c4b0d70a | |||
566068f7cd | |||
5c15a7d4cb | |||
b9a4770ff2 | |||
e471a57578 | |||
6e05900b5a | |||
15f6d78128 | |||
b32f7d1228 | |||
45fef23bad | |||
e20e6b7d3e | |||
0531b599fe | |||
a5751eec79 | |||
e02b8571f6 | |||
303a8d3877 | |||
6d00474315 | |||
46fb4c7615 | |||
48c93ed043 | |||
2d1f049444 | |||
c313f6eb72 | |||
cccdc9cedb | |||
429979c6d1 | |||
e28a541fe9 | |||
0f2b8c6564 | |||
5f682c303f | |||
dcbb42f5ef | |||
235ef8c39d | |||
4d59c66354 | |||
25fa963752 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
.pio
|
||||
.pioenvs
|
||||
.piolibdeps
|
||||
.vscode
|
||||
include/config.h
|
145
README.md
Normal file
145
README.md
Normal file
@ -0,0 +1,145 @@
|
||||
# ESMP3
|
||||
|
||||
## What you need
|
||||
Please note: This list is a "things I used", neither
|
||||
"these are the best things for this stuff" nor "you
|
||||
can only use these things". But please be aware that
|
||||
using other stuff may lead to you having to make
|
||||
more or less easy modifications.
|
||||
|
||||
Prizes are more or less the cheapest I could find on
|
||||
Aliexpress.
|
||||
|
||||
| What? | For what? | Price (approx) |
|
||||
|-------|-----------|----------------|
|
||||
| ESP-32-WROOM-32D | Controlling everything | 4€ |
|
||||
| WS1053B on a PCB with SD card slot | Play the MP3 files; provide an SD card slot | 5€ |
|
||||
| MFRC522 | RFID reader | 1€ |
|
||||
| 5V Amplifier(s) (e.g. 2x PAM-8302) | Single-channel Amplifier | 2€ |
|
||||
| Speaker(s) matching your amp (e.g. 2pcs 4 Ohm 5W) | Enabling you to hear the sounds | 4€ |
|
||||
| RFID tags (ISO14443A) - e.g. 10 cards | You can also get Keyfobs or stickers | 4€ |
|
||||
| 4 buttons | Prev/Next track, Volume up/down | 1€ |
|
||||
|
||||
You'all also need an SD card, some breadboard(s), jumper cables and a soldering iron.
|
||||
Also, some kind of box for the finished player.
|
||||
|
||||
## How to connect
|
||||
|
||||
Schematics coming soon...ish...
|
||||
|
||||
## How to install
|
||||
|
||||
Format your SD card with FAT32 and put files on it: Every album has
|
||||
to go into its own folder in the root of the SD card. Folders and files
|
||||
should not contain special characters (meaning stuff like äöüß). Spaces
|
||||
and dashes an alike are okay. Put the SD card into the SD card slot.
|
||||
|
||||
Copy `include/config.sample.h` to `include/config.h`. Modify it to at
|
||||
least contain the correct login details for your WiFi.
|
||||
|
||||
The code then should compile in PlatformIO without errors.
|
||||
|
||||
The serial console in PlatformIO should give you more or less useful
|
||||
messages about what's going on. There will also be a line saying
|
||||
"WiFi connected. IP address: xxx.xxx.xxx.xxx" when the connection to
|
||||
your WiFi succeeded.
|
||||
|
||||
In your browser, enter "http://xxx.xxx.xxx.xxx/" (using the IP address)
|
||||
from above. From there you can define mappings between RFID tag IDs and
|
||||
folders on the SD card.
|
||||
|
||||
## RFID-folder-mappings
|
||||
|
||||
### Via webinterface
|
||||
|
||||
To create a new mapping between an RFID tag and an folder, you can use
|
||||
the web interface. Click the button with the cogs icon. After putting
|
||||
your rfid tag on the reader (and possibly removing it again), its ID
|
||||
will be shown in the dialog. Click the button with the arrows behind
|
||||
the ID to start the mapping mode.
|
||||
|
||||
The dialog showing all folders with media files will be shown. Click the
|
||||
button with the arrows behind the correct folder, to create the mapping.
|
||||
|
||||
### Manually
|
||||
|
||||
Mapping are stored on the SD card in the file `/_mapping.txt`. Every
|
||||
mapping goes on its own line. Lines should be separated by \n (Unix-
|
||||
style line endings); the last line should also end with a newline.
|
||||
|
||||
Format of a line is `<RFID id>=<folder>`. RFID id is the UID of an
|
||||
RFID tag, expressed as 8 lowercase characters with leading 0 (if
|
||||
necessary). Folder is the foldername to play; starting with a slash and
|
||||
ending without one.
|
||||
|
||||
A valid `_mapping.txt` could look like this:
|
||||
|
||||
```
|
||||
1a2b3c4d=/Christmas Music Vol. 17
|
||||
003aab7f=/Let it go
|
||||
b691a22c=/Frozen Audiobook
|
||||
22cb6ae9=/Let it go
|
||||
|
||||
```
|
||||
|
||||
(Yes, more than one tag can map to a folder.)
|
||||
|
||||
## Technical details
|
||||
|
||||
### Ports
|
||||
|
||||
| Device | Port | Connected to |
|
||||
| ------ | ---- | ------------ |
|
||||
| VS1053 | CS | 16 |
|
||||
| VS1053 | MISO | 19 |
|
||||
| VS1053 | MOSI | 23 |
|
||||
| VS1053 | SCK | 18 |
|
||||
| VS1053 | XCS | 4 |
|
||||
| VS1053 | XRESET | 0 |
|
||||
| VS1053 | XDCS | 2 |
|
||||
| VS1053 | DREQ | 15 |
|
||||
| RC522 | SDA | 17 |
|
||||
| RC522 | SCK | 18 |
|
||||
| RC522 | MOSI | 23 |
|
||||
| RC522 | MISO | 19 |
|
||||
| AMP_L | SD | 27 |
|
||||
| AMP_R | SD | 26 |
|
||||
| BTN_PREV | | 22 |
|
||||
| BTN_NEXT | | 33 |
|
||||
| BTN_VOL_UP | | 21 |
|
||||
| BTN_VOL_DOWN | | 32 |
|
||||
|
||||
Buttons pull to GND if pushed -> Internal Pull-Up needed!
|
||||
|
||||
### RFID tags
|
||||
The mapping of rfid tags to files uses the ID of the
|
||||
tag. A file called `_mapping.txt` in the root folder of
|
||||
the SD card defines the mappings between RFID tag ids and
|
||||
folders to play.
|
||||
|
||||
The easiest way to create this file is to use the mapping
|
||||
functionality of the webinterface.
|
||||
|
||||
#### Special modes
|
||||
You can also save data on the tags to further manipulate
|
||||
the system. Position of the data is irrelevant, the whole
|
||||
tag will be searched.
|
||||
|
||||
Using `[random]` will play the files in a random order.
|
||||
`[random:2]` will randomize everything except the first 2
|
||||
files. This can be useful for having the favorite song of
|
||||
your kids playing, but after that getting a bit of randomness.
|
||||
|
||||
Using `[lock]` will turn this key into a key for the locking
|
||||
mode. Scanning the tag enables locking mode. The next album
|
||||
started will keep running until the end. Removing the tag
|
||||
will be deactivated, as are the buttons for prev and next
|
||||
track. You can disable locking mode by again scanning the
|
||||
lock tag again.
|
||||
|
||||
`[advent]` is used for christmas time. An album with this tag
|
||||
will only play in December. On December 1st, only track 1
|
||||
will play. On December 2nd, track 2 followed by track 1. On
|
||||
December 3rd, tracks 3, 1 and 2. From December 24th on, track
|
||||
24 followed by tracks 1-23. So your kid will get the "daily track"
|
||||
first, followed by all previous tags in the right order.
|
2
build_version.sh
Executable file
2
build_version.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
echo -n "-DVERSION=\\\"`git describe --tags --dirty`\\\""
|
287
data/index.html
Normal file
287
data/index.html
Normal file
@ -0,0 +1,287 @@
|
||||
<!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>
|
||||
|
||||
<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].lastIndexOf('/')+1)));
|
||||
$('#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');
|
||||
}
|
||||
|
||||
var file = data.files[data.current_track];
|
||||
if (file) {
|
||||
file = file.substr(1);
|
||||
$('#album').html(file.substr(0, file.indexOf('/')));
|
||||
file = file.substr(file.indexOf('/')+1);
|
||||
$('#track').html(file.substr(0, file.lastIndexOf('.')));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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_add_mapping').click(function(e) {
|
||||
$('#settingsModal').modal('hide');
|
||||
$('#openModal').modal('show');
|
||||
$('.add_mapping_button').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(); e.stopPropagation(); play_on_click=true; return false;});
|
||||
});
|
||||
</script>
|
||||
</html>
|
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,30 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include "config.h"
|
||||
|
||||
class Controller;
|
||||
|
||||
#include "player.h"
|
||||
#include "playlist.h"
|
||||
#include "playlist_manager.h"
|
||||
#include "http_server.h"
|
||||
#include <MFRC522.h>
|
||||
#include <MCP23S17/MCP23S17.h>
|
||||
|
||||
enum ControllerState { NORMAL, LOCKING, LOCKED };
|
||||
|
||||
class Controller {
|
||||
private:
|
||||
MFRC522* _rfid;
|
||||
MCP* _mcp;
|
||||
HTTPServer* _http_server;
|
||||
PlaylistManager* _pm;
|
||||
ControllerState _state = NORMAL;
|
||||
bool _rfid_enabled = true;
|
||||
void _check_rfid();
|
||||
void _check_serial();
|
||||
void _check_buttons();
|
||||
bool _debounce_button(uint8_t index);
|
||||
uint32_t _get_rfid_card_uid();
|
||||
uint32_t _last_rfid_card_uid = 0;
|
||||
String _read_rfid_data();
|
||||
bool _rfid_present = false;
|
||||
String _last_rfid_uid = "";
|
||||
String _last_rfid_data = "";
|
||||
Player* _player;
|
||||
unsigned long _last_rfid_scan_at = 0;
|
||||
unsigned long _last_position_info_at = 0;
|
||||
String _serial_buffer = String();
|
||||
void _execute_serial_command(String cmd);
|
||||
String _cmd_queue = "";
|
||||
void _execute_command_ls(String path);
|
||||
void _execute_command_ids();
|
||||
void _execute_command_help();
|
||||
unsigned long _button_last_pressed_at[NUM_BUTTONS];
|
||||
bool _check_button(uint8_t btn);
|
||||
public:
|
||||
Controller(Player* p, MCP* m);
|
||||
Controller(Player* p, PlaylistManager* pm);
|
||||
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();
|
||||
};
|
||||
|
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;
|
||||
};
|
@ -2,45 +2,51 @@
|
||||
#include "config.h"
|
||||
#include <SPI.h>
|
||||
#include <SD.h>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <MCP23S17/MCP23S17.h>
|
||||
#include "spi_master.h"
|
||||
#include "playlist.h"
|
||||
|
||||
class Player;
|
||||
|
||||
#include "controller.h"
|
||||
|
||||
#define SCI_MODE 0x00
|
||||
#define SCI_STATUS 0x01
|
||||
#define SCI_BASS 0x02
|
||||
#define SCI_CLOCKF 0x03
|
||||
#define SCI_DECODE_TIME 0x04
|
||||
#define SCI_AUDATA 0x05
|
||||
#define SCI_VOL 0x0B
|
||||
#define SCI_WRAMADDR 0x07
|
||||
#define SCI_WRAM 0x06
|
||||
#define SCI_HDAT0 0x08
|
||||
#define SCI_HDAT1 0x09
|
||||
#define SCI_AIADDR 0x0A
|
||||
#define SCI_AICTRL0 0x0C
|
||||
#define SCI_AICTRL1 0x0D
|
||||
#define SCI_AICTRL2 0x0E
|
||||
#define SCI_AICTRL3 0x0F
|
||||
|
||||
#define CMD_WRITE 0x02
|
||||
#define CMD_READ 0x03
|
||||
|
||||
#define ADDR_ENDBYTE 0x1E06
|
||||
|
||||
#define SM_LAYER12 0x0001
|
||||
#define SM_RESET 0x0004
|
||||
#define SM_CANCEL 0x0008
|
||||
#define SM_SDINEW 0x0800
|
||||
#define SM_ADPCM 0x1000
|
||||
#define SS_DO_NOT_JUMP 0x8000
|
||||
|
||||
#define XRESET PIN_VS1053_XRESET
|
||||
#define DREQ PIN_VS1053_DREQ
|
||||
#define XCS PIN_VS1053_XCS
|
||||
#define XDCS PIN_VS1053_XDCS
|
||||
|
||||
class Player {
|
||||
private:
|
||||
enum state { uninitialized, idle, playing, stopping,
|
||||
system_sound_while_playing, system_sound_while_stopped };
|
||||
struct album_state {
|
||||
uint8_t index;
|
||||
uint32_t position;
|
||||
};
|
||||
void _check_system_sound(String filename);
|
||||
sleeping, recording };
|
||||
void _reset();
|
||||
void _init();
|
||||
void _wait();
|
||||
uint16_t _read_control_register(uint8_t address);
|
||||
void _write_control_register(uint8_t address, uint16_t value);
|
||||
uint16_t _read_control_register(uint8_t address, bool do_wait=true);
|
||||
void _write_control_register(uint8_t address, uint16_t value, bool do_wait=true);
|
||||
void _write_direct(uint8_t address, uint16_t value);
|
||||
void _write_data(uint8_t* data);
|
||||
uint16_t _read_wram(uint16_t address);
|
||||
state _state = state::uninitialized;
|
||||
@ -49,44 +55,50 @@ private:
|
||||
void _flush_and_cancel();
|
||||
int8_t _get_endbyte();
|
||||
void _flush(uint count, int8_t fill_byte);
|
||||
void _set_last_track(const char* album, uint8_t track, uint32_t position);
|
||||
std::map<String, album_state> _last_tracks;
|
||||
void _play_file(String filename, uint32_t offset);
|
||||
uint32_t _id3_tag_offset(File f);
|
||||
void _play_file(String filename, uint32_t offset);
|
||||
void _finish_playing();
|
||||
void _finish_stopping();
|
||||
void _finish_stopping(bool turn_speaker_off);
|
||||
void _mute();
|
||||
void _unmute();
|
||||
void _sleep();
|
||||
void _wakeup();
|
||||
void _record();
|
||||
void _patch_adpcm();
|
||||
void _speaker_off();
|
||||
void _speaker_on();
|
||||
|
||||
SPISettings _spi_settings_slow = SPISettings(250000, MSBFIRST, SPI_MODE0);
|
||||
SPISettings _spi_settings_fast = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
||||
SPISettings* _spi_settings = &_spi_settings_slow;
|
||||
|
||||
std::list<String> _files_in_dir(String dir);
|
||||
String _find_album_dir(String album);
|
||||
File _file;
|
||||
uint32_t _file_size = 0;
|
||||
uint8_t _buffer[32];
|
||||
String _playing_album;
|
||||
uint8_t _playing_index;
|
||||
uint8_t _playing_album_songs;
|
||||
uint32_t _current_play_position;
|
||||
uint32_t _current_play_position = 0;
|
||||
Playlist* _current_playlist = NULL;
|
||||
uint _refills;
|
||||
uint8_t _volume;
|
||||
uint16_t _stop_delay;
|
||||
uint32_t _skip_to;
|
||||
MCP* _mcp;
|
||||
SPIMaster* _spi;
|
||||
Controller* _controller;
|
||||
unsigned long _stopped_at;
|
||||
public:
|
||||
Player(MCP* m);
|
||||
Player(SPIMaster* s);
|
||||
void init();
|
||||
void register_controller(Controller* c);
|
||||
void vol_up();
|
||||
void vol_down();
|
||||
void track_next();
|
||||
void track_prev();
|
||||
|
||||
bool play_album(String album);
|
||||
bool play_song(String album, uint8_t song_index, uint32_t offset=0);
|
||||
void play_system_sound(String filename);
|
||||
void stop();
|
||||
void set_track(uint8_t track);
|
||||
bool is_playing();
|
||||
bool play();
|
||||
bool play(Playlist* p);
|
||||
void stop(bool turn_speaker_off=true);
|
||||
bool loop();
|
||||
void set_volume(uint8_t vol, bool save = true);
|
||||
std::list<String> ls(String path);
|
||||
String position_json();
|
||||
String json();
|
||||
};
|
||||
|
32
include/playlist.h
Normal file
32
include/playlist.h
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
class Playlist {
|
||||
private:
|
||||
uint32_t _position = 0;
|
||||
uint32_t _current_track = 0;
|
||||
bool _started = false;
|
||||
bool _shuffled = false;
|
||||
std::vector<String> _files;
|
||||
public:
|
||||
Playlist(String path);
|
||||
void start();
|
||||
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);
|
||||
};
|
22
include/playlist_manager.h
Normal file
22
include/playlist_manager.h
Normal file
@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include "playlist.h"
|
||||
|
||||
class PlaylistManager {
|
||||
private:
|
||||
std::map<String, String> _map;
|
||||
std::map<String, Playlist*> _playlists;
|
||||
std::vector<String> _unmapped_folders;
|
||||
void _check_for_special_chars(String s);
|
||||
void _save_mapping();
|
||||
public:
|
||||
PlaylistManager();
|
||||
Playlist* get_playlist_for_id(String id);
|
||||
Playlist* get_playlist_for_folder(String folder);
|
||||
void dump_ids();
|
||||
void scan_files();
|
||||
String json();
|
||||
bool add_mapping(String id, String folder);
|
||||
};
|
@ -6,30 +6,65 @@
|
||||
|
||||
class SPIMaster {
|
||||
public:
|
||||
static uint8_t state;
|
||||
|
||||
static void init() {
|
||||
SPI.setHwCs(false);
|
||||
pinMode(PIN_SD_CS, OUTPUT);
|
||||
pinMode(PIN_VS1053_XCS, OUTPUT);
|
||||
pinMode(PIN_VS1053_XDCS, OUTPUT);
|
||||
pinMode(PIN_MCP, OUTPUT);
|
||||
}
|
||||
static void enable(uint8_t pin) {
|
||||
digitalWrite(PIN_SD_CS, pin==PIN_SD_CS ? LOW : HIGH);
|
||||
digitalWrite(PIN_VS1053_XCS, pin==PIN_VS1053_XCS ? LOW : HIGH);
|
||||
digitalWrite(PIN_VS1053_XDCS, pin==PIN_VS1053_XDCS ? LOW : HIGH);
|
||||
digitalWrite(PIN_MCP, pin==PIN_MCP ? LOW : HIGH);
|
||||
|
||||
PIN_SD_CS_SETUP();
|
||||
PIN_VS1053_XCS_SETUP();
|
||||
PIN_VS1053_XDCS_SETUP();
|
||||
PIN_RC522_CS_SETUP();
|
||||
disable();
|
||||
}
|
||||
|
||||
static void printStatus() {
|
||||
Serial.printf("CS state: SD:%d, VS1053_XCS:%d, VS1053_XDCS:%d, MCP:%d\n",
|
||||
digitalRead(PIN_SD_CS),
|
||||
digitalRead(PIN_VS1053_XCS),
|
||||
digitalRead(PIN_VS1053_XDCS),
|
||||
digitalRead(PIN_MCP));
|
||||
static void select_sd(bool enabled=true) {
|
||||
PIN_SD_CS(enabled ? LOW : HIGH);
|
||||
if (enabled) {
|
||||
state |= 1;
|
||||
} else {
|
||||
state &= ~1;
|
||||
}
|
||||
}
|
||||
|
||||
static void select_vs1053_xcs(bool enabled=true) {
|
||||
PIN_VS1053_XCS(enabled ? LOW : HIGH);
|
||||
if (enabled) {
|
||||
state |= 2;
|
||||
} else {
|
||||
state &= ~2;
|
||||
}
|
||||
}
|
||||
|
||||
static void select_vs1053_xdcs(bool enabled=true) {
|
||||
PIN_VS1053_XDCS(enabled ? LOW : HIGH);
|
||||
if (enabled) {
|
||||
state |= 4;
|
||||
} else {
|
||||
state &= ~4;
|
||||
}
|
||||
}
|
||||
|
||||
static void select_rc522(bool enabled=true) {
|
||||
PIN_RC522_CS(enabled ? LOW : HIGH);
|
||||
if (enabled) {
|
||||
state |= 8;
|
||||
} else {
|
||||
state &= ~8;
|
||||
}
|
||||
}
|
||||
|
||||
static void set_state(uint8_t s) {
|
||||
disable();
|
||||
if (s & 1) select_sd();
|
||||
if (s & 2) select_vs1053_xcs();
|
||||
if (s & 4) select_vs1053_xdcs();
|
||||
if (s & 8) select_rc522();
|
||||
}
|
||||
|
||||
static void disable() {
|
||||
enable(142);
|
||||
PIN_SD_CS(HIGH);
|
||||
PIN_VS1053_XCS(HIGH);
|
||||
PIN_VS1053_XDCS(HIGH);
|
||||
PIN_RC522_CS(HIGH);
|
||||
state = 0;
|
||||
}
|
||||
};
|
@ -8,11 +8,15 @@
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:esp12e]
|
||||
platform = espressif8266
|
||||
board = esp12e
|
||||
[env:esp32]
|
||||
platform = espressif32
|
||||
board = esp-wrover-kit
|
||||
framework = arduino
|
||||
upload_speed = 512000
|
||||
lib_deps = 63
|
||||
https://github.com/n0mjs710/MCP23S17.git
|
||||
upload_port = /dev/cu.wchusbserial1420
|
||||
build_flags=!./build_version.sh
|
||||
lib_deps = MFRC522
|
||||
https://github.com/me-no-dev/ESPAsyncWebServer.git
|
||||
ArduinoJSON
|
||||
upload_port = /dev/cu.SLAB_USBtoUART
|
||||
monitor_speed = 74480
|
||||
;monitor_port = /dev/cu.wchusbserial1420
|
@ -1,41 +1,60 @@
|
||||
#include "controller.h"
|
||||
#include "spi_master.h"
|
||||
#include "config.h"
|
||||
#include "playlist.h"
|
||||
#include "http_server.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
Controller::Controller(Player* p, MCP* m) {
|
||||
Controller::Controller(Player* p, PlaylistManager* pm) {
|
||||
_player = p;
|
||||
_mcp = m;
|
||||
_rfid = new MFRC522(PIN_RC522_CS, MFRC522::UNUSED_PIN);
|
||||
_pm = pm;
|
||||
_rfid = new MFRC522(17, MFRC522::UNUSED_PIN);
|
||||
|
||||
SPIMaster::enable(PIN_MCP);
|
||||
_mcp->pinMode(1, INPUT); _mcp->pullupMode(1, HIGH);
|
||||
_mcp->pinMode(2, INPUT); _mcp->pullupMode(2, HIGH);
|
||||
_mcp->pinMode(3, INPUT); _mcp->pullupMode(3, HIGH);
|
||||
_mcp->pinMode(4, INPUT); _mcp->pullupMode(4, HIGH);
|
||||
_player->register_controller(this);
|
||||
|
||||
SPIMaster::enable(PIN_RC522_CS);
|
||||
DEBUG("Initializing RC522...");
|
||||
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::disable();
|
||||
SPIMaster::select_rc522(false);
|
||||
INFO("RC522 initialized.\n");
|
||||
|
||||
for (uint8_t i=0; i<NUM_BUTTONS; i++) _button_last_pressed_at[i]=0;
|
||||
}
|
||||
|
||||
void Controller::register_http_server(HTTPServer* h) {
|
||||
_http_server = h;
|
||||
}
|
||||
|
||||
void Controller::loop() {
|
||||
TRACE("Controller::loop()...\n");
|
||||
unsigned long now = millis();
|
||||
if ((_last_rfid_scan_at < now - RFID_SCAN_INTERVAL) || (now < _last_rfid_scan_at)) {
|
||||
_check_rfid();
|
||||
_last_rfid_scan_at = now;
|
||||
}
|
||||
if ((_last_position_info_at < now - POSITION_SEND_INTERVAL) || (now < _last_position_info_at)) {
|
||||
send_position();
|
||||
_last_position_info_at = now;
|
||||
}
|
||||
_check_serial();
|
||||
_check_buttons();
|
||||
if (_cmd_queue.length() > 0) {
|
||||
process_message(_cmd_queue);
|
||||
_cmd_queue = "";
|
||||
}
|
||||
TRACE("Controller::loop() done.\n");
|
||||
}
|
||||
|
||||
uint32_t Controller::_get_rfid_card_uid() {
|
||||
SPIMaster::enable(PIN_RC522_CS);
|
||||
SPIMaster::select_rc522();
|
||||
if (!_rfid->PICC_ReadCardSerial()) {
|
||||
if (!_rfid->PICC_IsNewCardPresent()) {
|
||||
return 0;
|
||||
@ -44,33 +63,179 @@ uint32_t Controller::_get_rfid_card_uid() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
SPIMaster::select_rc522(false);
|
||||
uint32_t uid = _rfid->uid.uidByte[0]<<24 | _rfid->uid.uidByte[1]<<16 | _rfid->uid.uidByte[2]<<8 | _rfid->uid.uidByte[3];
|
||||
SPIMaster::disable();
|
||||
return uid;
|
||||
}
|
||||
|
||||
void Controller::_check_rfid() {
|
||||
uint32_t uid = _get_rfid_card_uid();
|
||||
if (uid != _last_rfid_card_uid) {
|
||||
if (uid > 0) {
|
||||
INFO("New RFID card uid: %08x\n", uid);
|
||||
String s_uid = String(uid, HEX);
|
||||
_player->play_album(s_uid);
|
||||
} else {
|
||||
INFO("No more RFID card.");
|
||||
TRACE("check_rfid running...\n");
|
||||
MFRC522::StatusCode status;
|
||||
if (_rfid_present) {
|
||||
byte buffer[2];
|
||||
byte buffer_size = 2;
|
||||
SPIMaster::select_rc522();
|
||||
status = _rfid->PICC_WakeupA(buffer, &buffer_size);
|
||||
if (status == MFRC522::STATUS_OK) {
|
||||
// Card is still present.
|
||||
_rfid->PICC_HaltA();
|
||||
SPIMaster::select_rc522(false);
|
||||
return;
|
||||
}
|
||||
SPIMaster::select_rc522(false);
|
||||
// Card is now gone
|
||||
_rfid_present = false;
|
||||
INFO("No more RFID card.\n");
|
||||
if (_state != LOCKED) {
|
||||
_player->stop();
|
||||
}
|
||||
_last_rfid_card_uid = uid;
|
||||
send_controller_status();
|
||||
} else {
|
||||
uint32_t uid = _get_rfid_card_uid();
|
||||
if (uid > 0) {
|
||||
String temp = String(uid, HEX);
|
||||
String s_uid = "";
|
||||
for (int i=0; i<(8-temp.length()); i++) {
|
||||
s_uid.concat("0");
|
||||
}
|
||||
s_uid.concat(temp);
|
||||
INFO("New RFID card uid: %s\n", s_uid.c_str());
|
||||
_last_rfid_uid = s_uid;
|
||||
_rfid_present = true;
|
||||
|
||||
String data = _read_rfid_data();
|
||||
_last_rfid_data = data;
|
||||
|
||||
Playlist* pl = _pm->get_playlist_for_id(s_uid);
|
||||
if (data.indexOf("[lock]") != -1) {
|
||||
if (_state == LOCKED) {
|
||||
_state = NORMAL;
|
||||
DEBUG("ControllerState is now UNLOCKED\n");
|
||||
} else {
|
||||
DEBUG("ControllerState is now LOCKING\n");
|
||||
_state = LOCKING;
|
||||
}
|
||||
}
|
||||
if (pl==NULL) {
|
||||
INFO("Could not find album for id '%s'.\n", s_uid.c_str());
|
||||
send_controller_status();
|
||||
return;
|
||||
}
|
||||
int index;
|
||||
if (data.indexOf("[advent]") != -1 && pl->is_fresh()) {
|
||||
struct tm time;
|
||||
getLocalTime(&time);
|
||||
if (time.tm_mon == 11) { // tm_mon is "months since january", so 11 means december.
|
||||
pl->advent_shuffle(time.tm_mday);
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
} else if (data.indexOf("[random]") != -1 && pl->is_fresh()) {
|
||||
pl->shuffle();
|
||||
} else if ((index=data.indexOf("[random:")) != -1 && pl->is_fresh()) {
|
||||
String temp = data.substring(index + 8);
|
||||
index = temp.indexOf("]");
|
||||
TRACE("temp: %s, temp.substring(0, %d): %s\n", temp.c_str(), index, temp.substring(0, index).c_str());
|
||||
if (index>0) {
|
||||
uint8_t random_offset = temp.substring(0, index).toInt();
|
||||
pl->shuffle(random_offset);
|
||||
}
|
||||
}
|
||||
|
||||
if (_state == LOCKED) {
|
||||
DEBUG("ControllerState is LOCKED, ignoring card.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_state == LOCKING) {
|
||||
_state = LOCKED;
|
||||
DEBUG("ControllerState is now LOCKED.\n");
|
||||
}
|
||||
|
||||
_player->play(pl);
|
||||
//send_playlist_manager_status();
|
||||
send_controller_status();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String Controller::_read_rfid_data() {
|
||||
TRACE("_read_rfid_data() running...\n");
|
||||
static MFRC522::MIFARE_Key keys[8] = {
|
||||
{{0xd3, 0xf7, 0xd3, 0xf7, 0xd3, 0xf7}}, // D3 F7 D3 F7 D3 F7
|
||||
{{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}}, // FF FF FF FF FF FF = factory default
|
||||
{{0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5}}, // A0 A1 A2 A3 A4 A5
|
||||
{{0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5}}, // B0 B1 B2 B3 B4 B5
|
||||
{{0x4d, 0x3a, 0x99, 0xc3, 0x51, 0xdd}}, // 4D 3A 99 C3 51 DD
|
||||
{{0x1a, 0x98, 0x2c, 0x7e, 0x45, 0x9a}}, // 1A 98 2C 7E 45 9A
|
||||
{{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}}, // AA BB CC DD EE FF
|
||||
{{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}} // 00 00 00 00 00 00
|
||||
};
|
||||
SPIMaster::select_rc522();
|
||||
DEBUG("Trying to read RFID data...\n");
|
||||
|
||||
String data = "";
|
||||
MFRC522::PICC_Type type = _rfid->PICC_GetType(_rfid->uid.sak);
|
||||
|
||||
uint8_t sectors = 0;
|
||||
switch(type) {
|
||||
case MFRC522::PICC_TYPE_MIFARE_MINI: sectors = 5; break;
|
||||
case MFRC522::PICC_TYPE_MIFARE_1K: sectors = 16; break;
|
||||
case MFRC522::PICC_TYPE_MIFARE_4K: sectors = 40; break;
|
||||
default: INFO("Unknown PICC type %s\n", String(MFRC522::PICC_GetTypeName(type)).c_str());
|
||||
}
|
||||
|
||||
int good_key_index = -1;
|
||||
for (uint8_t sector=1; sector<sectors; sector++) {
|
||||
uint8_t blocks = (sector < 32) ? 4 : 16;
|
||||
uint8_t block_offset = (sector < 32) ? sector * 4 : 128 + (sector - 32) * 16;
|
||||
|
||||
MFRC522::StatusCode status;
|
||||
|
||||
for (int i=0; i<8; i++) {
|
||||
MFRC522::MIFARE_Key *k = &keys[i];
|
||||
TRACE("Trying MIFARE key %02X %02X %02X %02X %02X %02X...\n", k->keyByte[0], k->keyByte[1], k->keyByte[2], k->keyByte[3], k->keyByte[4], k->keyByte[5]);
|
||||
status = _rfid->PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, block_offset, k, &_rfid->uid);
|
||||
if (status == MFRC522::STATUS_OK) {
|
||||
TRACE("Authentication succeeded with key #%d\n", i);
|
||||
good_key_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (good_key_index == -1) {
|
||||
TRACE("Could not find a valid MIFARE key.\n");
|
||||
} else {
|
||||
for (uint8_t block=0; block<blocks-1; block++) {
|
||||
byte buffer[18];
|
||||
uint8_t byte_count = 18;
|
||||
status = _rfid->MIFARE_Read(block_offset + block, buffer, &byte_count);
|
||||
if (status != MFRC522::STATUS_OK) {
|
||||
DEBUG("MIFARE_Read() failed: %s\n", String(_rfid->GetStatusCodeName(status)).c_str());
|
||||
continue;
|
||||
}
|
||||
for (int i=0; i<16; i++) {
|
||||
if (buffer[i]>=0x20 && buffer[i]<0x7F) data.concat((char)buffer[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_rfid->PICC_HaltA();
|
||||
_rfid->PCD_StopCrypto1();
|
||||
DEBUG("Data from RFID: %s\n", data.c_str());
|
||||
SPIMaster::select_rc522(false);
|
||||
return data;
|
||||
}
|
||||
|
||||
void Controller::_check_serial() {
|
||||
TRACE("check_serial running...\n");
|
||||
|
||||
if (Serial.available() > 0) {
|
||||
char c = Serial.read();
|
||||
Serial.printf("%c", c);
|
||||
if (c==10 || c==13) {
|
||||
if (_serial_buffer.length()>0) {
|
||||
_execute_serial_command(_serial_buffer);
|
||||
process_message(_serial_buffer);
|
||||
_serial_buffer = String();
|
||||
}
|
||||
} else {
|
||||
@ -79,17 +244,19 @@ void Controller::_check_serial() {
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::_execute_serial_command(String cmd) {
|
||||
DEBUG("Executing command: %s", cmd.c_str());
|
||||
bool Controller::process_message(String cmd) {
|
||||
DEBUG("Executing command: %s\n", cmd.c_str());
|
||||
|
||||
if (cmd.startsWith("play ")) {
|
||||
Playlist* p = _pm->get_playlist_for_folder(cmd.substring(5));
|
||||
_player->play(p);
|
||||
//} else if (cmd.equals("ls")) {
|
||||
// _execute_command_ls("/");
|
||||
//} else if (cmd.startsWith("ls ")) {
|
||||
// _execute_command_ls(cmd.substring(3));
|
||||
} else if (cmd.equals("play")) {
|
||||
_player->play();
|
||||
|
||||
if (cmd.equals("ls")) {
|
||||
_execute_command_ls("/");
|
||||
} else if (cmd.startsWith("ls ")) {
|
||||
_execute_command_ls(cmd.substring(3));
|
||||
} else if (cmd.startsWith("play ")) {
|
||||
_player->play_album(cmd.substring(5));
|
||||
} else if (cmd.startsWith("sys ")) {
|
||||
_player->play_system_sound(cmd.substring(4));
|
||||
} else if (cmd.equals("stop")) {
|
||||
_player->stop();
|
||||
} else if (cmd.equals("help")) {
|
||||
@ -98,71 +265,153 @@ void Controller::_execute_serial_command(String cmd) {
|
||||
_player->vol_down();
|
||||
} else if (cmd.equals("+")) {
|
||||
_player->vol_up();
|
||||
} else if (cmd.equals("p")) {
|
||||
} 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("n")) {
|
||||
} else if (cmd.equals("track_next")) {
|
||||
_player->track_next();
|
||||
} else if (cmd.startsWith("track=")) {
|
||||
uint8_t track = cmd.substring(6).toInt();
|
||||
_player->set_track(track);
|
||||
} else if (cmd.equals("ids")) {
|
||||
_pm->dump_ids();
|
||||
} else if (cmd.equals("reset_vs1053")) {
|
||||
_player->stop();
|
||||
_player->init();
|
||||
} else if (cmd.equals("reboot")) {
|
||||
ESP.restart();
|
||||
} else if (cmd.startsWith("add_mapping=")) {
|
||||
String rest = cmd.substring(12);
|
||||
uint8_t idx = rest.indexOf('=');
|
||||
String id = rest.substring(0, idx);
|
||||
String folder = rest.substring(idx + 1);
|
||||
_pm->add_mapping(id, folder);
|
||||
send_playlist_manager_status();
|
||||
} else {
|
||||
ERROR("Unknown command: %s\n", cmd.c_str());
|
||||
return false;
|
||||
}
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Controller::_execute_command_ls(String path) {
|
||||
INFO("Listing contents of %s:\n", path.c_str());
|
||||
std::list<String> files = _player->ls(path);
|
||||
for(std::list<String>::iterator it=files.begin(); it!=files.end(); it++) {
|
||||
INFO(" %s\n", (*it).c_str());
|
||||
}
|
||||
// TODO
|
||||
//std::list<String> files = _player->ls(path);
|
||||
//for(std::list<String>::iterator it=files.begin(); it!=files.end(); ++it) {
|
||||
// INFO(" %s\n", (*it).c_str());
|
||||
//}
|
||||
}
|
||||
|
||||
void Controller::_execute_command_help() {
|
||||
INFO("Valid commands are:");
|
||||
INFO(" help - Displays this help\n");
|
||||
INFO(" ls [dir] - Lists the contents of [dir] or, if not given, of /\n");
|
||||
//INFO(" ls [dir] - Lists the contents of [dir] or, if not given, of /\n");
|
||||
INFO(" ids - Lists all known ID-to-folder mappings\n");
|
||||
INFO(" play [id] - Plays the album with the given id\n");
|
||||
INFO(" sys [file]- Plays the file as system sound\n");
|
||||
INFO(" stop - Stops playback\n");
|
||||
INFO(" - / + - Decrease or increase the volume\n");
|
||||
INFO(" p / n - Previous or next track\n");
|
||||
}
|
||||
|
||||
void Controller::_check_buttons() {
|
||||
SPIMaster::enable(PIN_MCP);
|
||||
SPI.beginTransaction(SPISettings(250000, MSBFIRST, SPI_MODE0));
|
||||
/*if (millis()%100==0) {
|
||||
Serial.printf("Buttons: %d %d %d %d\n", _mcp->digitalRead(1), _mcp->digitalRead(2), _mcp->digitalRead(3), _mcp->digitalRead(4));
|
||||
}*/
|
||||
if (_check_button(0)) {
|
||||
TRACE("check_buttons running...\n");
|
||||
|
||||
if (BTN_PREV() && _debounce_button(0)) {
|
||||
if (_state == NORMAL) {
|
||||
_player->track_prev();
|
||||
} else if (_check_button(1)) {
|
||||
_player->vol_up();
|
||||
} else if (_check_button(2)) {
|
||||
_player->vol_down();
|
||||
} else if (_check_button(3)) {
|
||||
_player->track_next();
|
||||
} else {
|
||||
DEBUG("Ignoring btn_prev because state is LOCKED.\n");
|
||||
}
|
||||
} else if (BTN_VOL_UP() && _debounce_button(1)) {
|
||||
_player->vol_up();
|
||||
} else if (BTN_VOL_DOWN() && _debounce_button(2)) {
|
||||
_player->vol_down();
|
||||
} else if (BTN_NEXT() && _debounce_button(3)) {
|
||||
if (_state == NORMAL) {
|
||||
_player->track_next();
|
||||
} else {
|
||||
DEBUG("Ignoring btn_next because state is LOCKED.\n");
|
||||
}
|
||||
}
|
||||
SPI.endTransaction();
|
||||
SPIMaster::disable();
|
||||
}
|
||||
|
||||
bool Controller::_check_button(uint8_t index) {
|
||||
if (index >= NUM_BUTTONS) return false;
|
||||
bool Controller::_debounce_button(uint8_t index) {
|
||||
bool ret = false;
|
||||
uint8_t sum = 0;
|
||||
while (1) {
|
||||
sum = 0;
|
||||
for (int i=0; i<8; i++) {
|
||||
sum += _mcp->digitalRead(index + 1) == HIGH ? 1 : 0;
|
||||
}
|
||||
if (sum==0 || sum==8) break;
|
||||
}
|
||||
if (sum == 0) {
|
||||
if (_button_last_pressed_at[index] + DEBOUNCE_MILLIS < millis()) {
|
||||
DEBUG("Button %d pressed.\n", index);
|
||||
ret = true;
|
||||
}
|
||||
_button_last_pressed_at[index] = millis();
|
||||
}
|
||||
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;
|
||||
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 && _player->is_playing()) {
|
||||
_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();
|
||||
}
|
136
src/http_server.cpp
Normal file
136
src/http_server.cpp
Normal file
@ -0,0 +1,136 @@
|
||||
#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("/", [&](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->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) {
|
||||
DEBUG("Received ws message: %s\n", (char*)data);
|
||||
_controller->queue_command((char*)data);
|
||||
}
|
||||
}
|
||||
}
|
74
src/main.cpp
74
src/main.cpp
@ -1,42 +1,90 @@
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
#include <SD.h>
|
||||
#include <MCP23S17/MCP23S17.h>
|
||||
#include <WiFi.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <SPIFFS.h>
|
||||
#include "config.h"
|
||||
#include "controller.h"
|
||||
#include "player.h"
|
||||
#include "spi_master.h"
|
||||
#include "http_server.h"
|
||||
#include "playlist_manager.h"
|
||||
|
||||
Controller* controller;
|
||||
Player* player;
|
||||
MCP* mcp;
|
||||
PlaylistManager* pm;
|
||||
HTTPServer* http_server;
|
||||
|
||||
uint8_t SPIMaster::state = 0;
|
||||
|
||||
void setup() {
|
||||
delay(500);
|
||||
Serial.begin(74880);
|
||||
Serial.println("Starting...");
|
||||
Serial.println("Started.");
|
||||
INFO("Starting.\n");
|
||||
#ifdef VERSION
|
||||
INFO("ESMP3 version %s\n", VERSION);
|
||||
#else
|
||||
INFO("ESMP3, version unknown\n");
|
||||
#endif
|
||||
INFO("Initializing...\n");
|
||||
|
||||
DEBUG("Setting up SPI...\n");
|
||||
SPI.begin();
|
||||
SPI.setHwCs(false);
|
||||
SPIMaster::init();
|
||||
SPIMaster* spi = new SPIMaster();
|
||||
INFO("SPI initialized.\n");
|
||||
|
||||
DEBUG("Setting up MCP...\n");
|
||||
SPIMaster::enable(PIN_MCP);
|
||||
mcp = new MCP(0, PIN_MCP);
|
||||
INFO("MCP initialized.");
|
||||
|
||||
DEBUG("Setting up SD card...\n");
|
||||
SPIMaster::enable(PIN_SD_CS);
|
||||
if (SD.begin(PIN_SD_CS)) {
|
||||
spi->select_sd();
|
||||
if (SD.begin(42, SPI, 25000000)) {
|
||||
INFO("SD card initialized.\n");
|
||||
} else {
|
||||
ERROR("Could not initialize SD card. Halting.\n");
|
||||
while(1);
|
||||
ERROR("Could not initialize SD card.\n");
|
||||
}
|
||||
spi->select_sd(false);
|
||||
|
||||
DEBUG("Starting SPIFFS...\n");
|
||||
SPIFFS.begin(true);
|
||||
|
||||
DEBUG("Initializing PlaylistManager...\n");
|
||||
pm = new PlaylistManager();
|
||||
|
||||
DEBUG("Initializing Player and Controller...\n");
|
||||
player = new Player(spi);
|
||||
controller = new Controller(player, pm);
|
||||
INFO("Player and controller initialized.\n");
|
||||
|
||||
DEBUG("Connecting to wifi \"%s\"...\n", WIFI_SSID);
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
|
||||
ERROR("Could not connect to Wifi. Rebooting.");
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
INFO("WiFi connected. IP address: %s\n", WiFi.localIP().toString().c_str());
|
||||
|
||||
MDNS.begin("esmp3");
|
||||
|
||||
DEBUG("Setting up HTTP server...\n");
|
||||
http_server = new HTTPServer(player, controller);
|
||||
controller->register_http_server(http_server);
|
||||
|
||||
DEBUG("Starting NTP client...\n");
|
||||
// Taken from https://github.com/esp8266/Arduino/blob/master/cores/esp8266/TZ.h
|
||||
configTzTime("CET-1CEST,M3.5.0,M10.5.0/3", "europe.pool.ntp.org");
|
||||
struct tm time;
|
||||
if (getLocalTime(&time, 10000)) {
|
||||
char buffer[100];
|
||||
strftime(buffer, 100, "%Y-%m-%d %H:%M:%S", &time);
|
||||
DEBUG("Got time: %s\n", buffer);
|
||||
} else {
|
||||
INFO("Could not fetch current time via NTP.\n");
|
||||
}
|
||||
player = new Player(mcp);
|
||||
controller = new Controller(player, mcp);
|
||||
|
||||
INFO("Initialization completed.\n");
|
||||
}
|
||||
|
661
src/player.cpp
661
src/player.cpp
@ -2,43 +2,53 @@
|
||||
|
||||
#include "player.h"
|
||||
#include "spi_master.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
//Player::_spi_settings
|
||||
|
||||
Player::Player(MCP* m) {
|
||||
_mcp = m;
|
||||
_mcp->pinMode(XRESET, OUTPUT);
|
||||
_mcp->digitalWrite(XRESET, HIGH);
|
||||
pinMode(DREQ, INPUT);
|
||||
Player::Player(SPIMaster* s) {
|
||||
_spi = s;
|
||||
PIN_VS1053_XRESET_SETUP();
|
||||
PIN_VS1053_XRESET(HIGH);
|
||||
PIN_SPEAKER_L_SETUP();
|
||||
PIN_SPEAKER_R_SETUP();
|
||||
_speaker_off();
|
||||
_spi->disable();
|
||||
PIN_VS1053_DREQ_SETUP();
|
||||
|
||||
_init();
|
||||
init();
|
||||
}
|
||||
|
||||
void Player::register_controller(Controller* c) {
|
||||
_controller = c;
|
||||
}
|
||||
|
||||
void Player::_reset() {
|
||||
_mcp->digitalWrite(XRESET, LOW);
|
||||
PIN_VS1053_XRESET(LOW);
|
||||
delay(100);
|
||||
_mcp->digitalWrite(XRESET, HIGH);
|
||||
PIN_VS1053_XRESET(HIGH);
|
||||
delay(100);
|
||||
_state = uninitialized;
|
||||
_spi_settings = &_spi_settings_slow; // After reset, communication has to be slow
|
||||
}
|
||||
|
||||
void Player::_init() {
|
||||
SPIMaster::disable();
|
||||
void Player::init() {
|
||||
DEBUG("Resetting VS1053...\n");
|
||||
_reset();
|
||||
|
||||
uint16_t result = _read_control_register(SCI_MODE);
|
||||
DEBUG("SCI_MODE: 0x%04X\n", result);
|
||||
if (result != 0x4800) {
|
||||
ERROR("SCI_MODE was 0x%04X, expected was 0x4800.\n", result);
|
||||
return;
|
||||
ERROR("SCI_MODE was 0x%04X, expected was 0x4800. Rebooting.\n", result);
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
}
|
||||
result = _read_control_register(SCI_STATUS);
|
||||
DEBUG("SCI_STATUS: 0x%04X\n", result);
|
||||
if (result != 0x0040 && result != 0x0048) {
|
||||
ERROR("SCI_STATUS was 0x%04X, expected was 0x0040 or 0x0048.\n", result);
|
||||
return;
|
||||
ERROR("SCI_STATUS was 0x%04X, expected was 0x0040 or 0x0048. Rebooting.\n", result);
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
}
|
||||
result = _read_control_register(SCI_CLOCKF);
|
||||
DEBUG("SCI_CLOCKF: 0x%04X\n", result);
|
||||
@ -46,48 +56,102 @@ void Player::_init() {
|
||||
DEBUG("VS1053 Init looking good.\n");
|
||||
DEBUG("Upping VS1053 multiplier...\n");
|
||||
|
||||
_write_control_register(SCI_CLOCKF, 0x6000);
|
||||
_write_control_register(SCI_CLOCKF, 0xC000);
|
||||
delay(10);
|
||||
|
||||
_spi_settings = &_spi_settings_fast;
|
||||
|
||||
result = _read_control_register(SCI_CLOCKF);
|
||||
DEBUG("SCI_CLOCKF: 0x%04X\n", result);
|
||||
if (result != 0x6000) {
|
||||
ERROR("Error: SCI_CLOCKF was 0x%04X, expected was 0x6000.\n", result);
|
||||
return;
|
||||
if (result != 0xC000) {
|
||||
ERROR("Error: SCI_CLOCKF was 0x%04X, expected was 0xC000. Rebooting.\n", result);
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
set_volume(VOLUME_DEFAULT);
|
||||
|
||||
INFO("VS1053 initialization completed.\n");
|
||||
|
||||
INFO("Checking system sounds...\n");
|
||||
SPIMaster::enable(PIN_SD_CS);
|
||||
_check_system_sound("no_prev_song.mp3");
|
||||
_check_system_sound("no_next_song.mp3");
|
||||
_check_system_sound("volume_max.mp3");
|
||||
_check_system_sound("volume_min.mp3");
|
||||
|
||||
_state = idle;
|
||||
}
|
||||
|
||||
void Player::_check_system_sound(String filename) {
|
||||
String path = String("/system/") + filename;
|
||||
if (!SD.exists(path)) {
|
||||
ERROR("System sound %s is missing on the sd card!\n", path.c_str());
|
||||
} else {
|
||||
DEBUG("%s found.\n", path.c_str());
|
||||
}
|
||||
void Player::_speaker_off() {
|
||||
DEBUG("Speaker off\n");
|
||||
PIN_SPEAKER_L(LOW);
|
||||
PIN_SPEAKER_R(LOW);
|
||||
}
|
||||
|
||||
void Player::_speaker_on() {
|
||||
DEBUG("Speaker on\n");
|
||||
PIN_SPEAKER_L(HIGH);
|
||||
PIN_SPEAKER_R(HIGH);
|
||||
}
|
||||
|
||||
void Player::_sleep() {
|
||||
DEBUG("VS1053 going to sleep.\n");
|
||||
_speaker_off();
|
||||
_write_control_register(SCI_CLOCKF, 0x0000);
|
||||
_spi_settings = &_spi_settings_slow;
|
||||
_write_control_register(SCI_AUDATA, 0x0010);
|
||||
set_volume(0, false);
|
||||
_state = sleeping;
|
||||
TRACE("VS1053 is sleeping now.\n");
|
||||
}
|
||||
|
||||
void Player::_wakeup() {
|
||||
if (_state != sleeping && _state != recording) return;
|
||||
_stopped_at = millis();
|
||||
DEBUG("Waking VS1053...\n");
|
||||
set_volume(_volume, false);
|
||||
_write_control_register(SCI_AUDATA, 0x0000);
|
||||
_write_control_register(SCI_CLOCKF, 0x6000);
|
||||
_write_control_register(SCI_MODE, 0x4800 | SM_RESET);
|
||||
delay(10);
|
||||
//_speaker_on();
|
||||
_spi_settings = &_spi_settings_fast;
|
||||
_state = idle;
|
||||
}
|
||||
|
||||
void Player::_record() {
|
||||
// http://www.vlsi.fi/fileadmin/software/VS10XX/VS1053_VS1063_PcmRecorder.pdf
|
||||
DEBUG("Starting recording.\n");
|
||||
set_volume(1, false);
|
||||
|
||||
// Disable SCI_BASS
|
||||
_write_control_register(SCI_BASS, 0);
|
||||
|
||||
// Disable user applications
|
||||
_write_control_register(SCI_AIADDR, 0);
|
||||
|
||||
// Disable interrupts
|
||||
_write_control_register(SCI_WRAMADDR, 0xC01A);
|
||||
_write_control_register(SCI_WRAM, 0x0002);
|
||||
|
||||
_patch_adpcm();
|
||||
|
||||
_write_control_register(SCI_MODE, SM_ADPCM);
|
||||
|
||||
_write_control_register(SCI_AICTRL0, 0x8000); // Mono VU meter
|
||||
_write_control_register(SCI_AICTRL1, 1024); // Manual gain, 1x
|
||||
_write_control_register(SCI_AICTRL2, 0); // Maximum gain for autogain - ignored
|
||||
_write_control_register(SCI_AICTRL3, 0); // status: record
|
||||
|
||||
_write_control_register(SCI_AIADDR, 0x0034, false);
|
||||
delay(1);
|
||||
|
||||
DEBUG("Recording.\n");
|
||||
delay(10);
|
||||
_state = recording;
|
||||
}
|
||||
|
||||
inline void Player::_wait() {
|
||||
while(!digitalRead(DREQ));
|
||||
while(!PIN_VS1053_DREQ());
|
||||
}
|
||||
|
||||
uint16_t Player::_read_control_register(uint8_t address) {
|
||||
_wait();
|
||||
SPIMaster::enable(XCS);
|
||||
uint16_t Player::_read_control_register(uint8_t address, bool do_wait) {
|
||||
if (do_wait) _wait();
|
||||
_spi->select_vs1053_xcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
SPI.transfer(CMD_READ);
|
||||
SPI.transfer(address);
|
||||
@ -96,34 +160,268 @@ uint16_t Player::_read_control_register(uint8_t address) {
|
||||
uint8_t b2 = SPI.transfer(0xFF);
|
||||
_wait();
|
||||
SPI.endTransaction();
|
||||
SPIMaster::disable();
|
||||
_spi->select_vs1053_xcs(false);
|
||||
|
||||
return (b1 << 8) | b2;
|
||||
}
|
||||
|
||||
void Player::_write_control_register(uint8_t address, uint16_t value) {
|
||||
uint8_t b1 = value >> 8;
|
||||
uint8_t b2 = value & 0xFF;
|
||||
void Player::_write_control_register(uint8_t address, uint16_t value, bool do_wait) {
|
||||
_wait();
|
||||
SPIMaster::enable(XCS);
|
||||
_spi->select_vs1053_xcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
SPI.transfer(CMD_WRITE);
|
||||
SPI.transfer(address);
|
||||
SPI.transfer(b1);
|
||||
SPI.transfer(b2);
|
||||
_wait();
|
||||
SPI.transfer(value >> 8);
|
||||
SPI.transfer(value & 0xFF);
|
||||
SPI.endTransaction();
|
||||
SPIMaster::disable();
|
||||
_spi->select_vs1053_xcs(false);
|
||||
if (do_wait) _wait();
|
||||
}
|
||||
|
||||
void Player::_patch_adpcm() {
|
||||
static const uint16_t patch_data[] = {
|
||||
0x0007, 0x0001, 0xc01a, 0x0006, 0x0001, 0x0002, 0x0007, 0x0001, /* 0 */
|
||||
0x0008, 0x0006, 0x8002, 0x0000, 0x0007, 0x0001, 0x000c, 0x0006, /* 8 */
|
||||
0x0002, 0x7000, 0x0017, 0x0007, 0x0001, 0x8034, 0x0006, 0x0022, /* 10 */
|
||||
0x0030, 0x0490, 0xb080, 0x0024, 0x3800, 0x0024, 0x0000, 0x1090, /* 18 */
|
||||
0xf400, 0x5404, 0x0000, 0x0851, 0xf400, 0x5648, 0xf400, 0x5404, /* 20 */
|
||||
0xf400, 0x5658, 0xf400, 0x5404, 0xf400, 0x5640, 0x0000, 0x800a, /* 28 */
|
||||
0x2900, 0x9180, 0x0006, 0x2016, 0x2a00, 0x1bce, 0x2a00, 0x114e, /* 30 */
|
||||
0x2a00, 0x168e, 0x0007, 0x0001, 0x1800, 0x0006, 0x8006, 0x0000, /* 38 */
|
||||
0x0007, 0x0001, 0x8045, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, /* 40 */
|
||||
0x7808, 0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, /* 48 */
|
||||
0x7800, 0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x3e80, 0x3e10, /* 50 */
|
||||
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, /* 58 */
|
||||
0x4024, 0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, /* 60 */
|
||||
0x8024, 0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, /* 68 */
|
||||
0x0001, 0x805a, 0x0006, 0x002a, 0x3e12, 0xb817, 0x3e12, 0x7808, /* 70 */
|
||||
0x3e18, 0x3821, 0x3e18, 0xb823, 0x3e15, 0x4024, 0x3e10, 0x7800, /* 78 */
|
||||
0x48b2, 0x0024, 0x0000, 0x800a, 0x2900, 0x5e40, 0x3e10, 0x7800, /* 80 */
|
||||
0x36f0, 0x5800, 0x2210, 0x0000, 0x36f0, 0x5800, 0x36f5, 0x4024, /* 88 */
|
||||
0x36f8, 0x9823, 0x36f8, 0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, /* 90 */
|
||||
0x0030, 0x0717, 0x2100, 0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, /* 98 */
|
||||
0x806f, 0x0006, 0x0030, 0x3e12, 0xb817, 0x3e12, 0x7808, 0x3e18, /* a0 */
|
||||
0x3821, 0x3e18, 0xb823, 0x3e10, 0x7800, 0xb880, 0x3855, 0x0030, /* a8 */
|
||||
0x0497, 0x48b2, 0x3c00, 0x0000, 0x800a, 0x2900, 0x7300, 0x3e10, /* b0 */
|
||||
0x7800, 0x36f0, 0x5800, 0x2210, 0x0000, 0x6890, 0x1bd5, 0x0030, /* b8 */
|
||||
0x0497, 0x3f00, 0x0024, 0x36f0, 0x5800, 0x36f8, 0x9823, 0x36f8, /* c0 */
|
||||
0x1821, 0x36f2, 0x5808, 0x3602, 0x8024, 0x0030, 0x0717, 0x2100, /* c8 */
|
||||
0x0000, 0x3f05, 0xdbd7, 0x0007, 0x0001, 0x8010, 0x0006, 0x000e, /* d0 */
|
||||
0x3e02, 0x8024, 0x0001, 0x000a, 0x6012, 0x0024, 0xfea2, 0x0024, /* d8 */
|
||||
0x48b2, 0x1bca, 0x2000, 0x0000, 0x4180, 0x0024, 0x0007, 0x0001, /* e0 */
|
||||
0x8087, 0x0006, 0x00e6, 0x3e00, 0x7843, 0x3e01, 0x3845, 0x3e04, /* e8 */
|
||||
0x3812, 0x0006, 0x08d0, 0x3000, 0x4024, 0x6182, 0x0024, 0x0030, /* f0 */
|
||||
0x06d0, 0x2800, 0x2655, 0xb882, 0x0024, 0x0000, 0x0201, 0x0000, /* f8 */
|
||||
0x0005, 0x0030, 0x0210, 0xa016, 0x4004, 0x1fff, 0xfe01, 0xae1a, /* 100 */
|
||||
0x0024, 0xc342, 0x0024, 0xb882, 0x2001, 0x0030, 0x06d0, 0x3800, /* 108 */
|
||||
0x4024, 0x0006, 0x0890, 0x3004, 0x0024, 0x3000, 0x4024, 0x0006, /* 110 */
|
||||
0x12d0, 0x6182, 0x0024, 0x3000, 0x4024, 0x2800, 0x3e05, 0xf400, /* 118 */
|
||||
0x4050, 0x3009, 0x2000, 0x0006, 0x08d0, 0x0006, 0x0892, 0x3000, /* 120 */
|
||||
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x0030, 0x0250, 0xb882, /* 128 */
|
||||
0x2001, 0x0030, 0x0710, 0x3800, 0x4024, 0x0006, 0x12d0, 0x3000, /* 130 */
|
||||
0x4024, 0x6192, 0x0024, 0x3800, 0x4024, 0x3204, 0x0024, 0x3023, /* 138 */
|
||||
0x0024, 0x30e0, 0xc024, 0x6312, 0x0024, 0x0000, 0x00c3, 0x2800, /* 140 */
|
||||
0x3141, 0x0000, 0x0024, 0x3033, 0x0024, 0x3a04, 0x0024, 0x3000, /* 148 */
|
||||
0x4024, 0x6182, 0x0024, 0x0006, 0x0890, 0x2800, 0x2fd8, 0x0006, /* 150 */
|
||||
0x0301, 0x3a00, 0x4024, 0x0000, 0x00c3, 0x3004, 0x0024, 0x3013, /* 158 */
|
||||
0x0024, 0x3000, 0x4024, 0x0006, 0x12d0, 0x3800, 0x4024, 0x0030, /* 160 */
|
||||
0x0310, 0xf000, 0x0001, 0x6236, 0x0024, 0x001f, 0xffc3, 0x2800, /* 168 */
|
||||
0x3395, 0x0000, 0x0024, 0x0000, 0x0203, 0xa132, 0x0024, 0x001f, /* 170 */
|
||||
0xffc3, 0xb136, 0x0024, 0x6306, 0x0024, 0x0000, 0x0024, 0x2800, /* 178 */
|
||||
0x3611, 0x0000, 0x0024, 0x0020, 0x0003, 0xb132, 0x0024, 0x0000, /* 180 */
|
||||
0x0024, 0x2800, 0x3a85, 0x0000, 0x0024, 0x0000, 0x0081, 0xb212, /* 188 */
|
||||
0x0024, 0x0000, 0x0024, 0x2800, 0x3a05, 0x0000, 0x0024, 0x6892, /* 190 */
|
||||
0x0024, 0xb212, 0x0024, 0x0000, 0x0005, 0x2800, 0x3c55, 0x0030, /* 198 */
|
||||
0x0310, 0x0000, 0x3fc1, 0x3000, 0x8024, 0xb214, 0x0024, 0x003f, /* 1a0 */
|
||||
0xc001, 0xb010, 0x0024, 0xc200, 0x0024, 0x0030, 0x0310, 0x3800, /* 1a8 */
|
||||
0x0024, 0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x5803, 0x2000, /* 1b0 */
|
||||
0x0000, 0x0000, 0x0024, 0x0030, 0x0310, 0x0000, 0x0005, 0x003f, /* 1b8 */
|
||||
0xc001, 0x4088, 0x0002, 0xb214, 0x0024, 0x1fff, 0xfe01, 0xae12, /* 1c0 */
|
||||
0x0024, 0x2800, 0x3a00, 0xc200, 0x0024, 0x2800, 0x28c0, 0x3800, /* 1c8 */
|
||||
0x0024, 0x0007, 0x0001, 0x80fa, 0x0006, 0x00fe, 0x3e12, 0x0024, /* 1d0 */
|
||||
0x3e05, 0xb814, 0x3615, 0x0024, 0x3e00, 0x3841, 0x3e00, 0xb843, /* 1d8 */
|
||||
0x3e01, 0x3845, 0x3e04, 0x3851, 0x0030, 0x10d0, 0x3e04, 0x8024, /* 1e0 */
|
||||
0x3010, 0x0024, 0x3000, 0x8024, 0x0006, 0x1190, 0x3000, 0x4024, /* 1e8 */
|
||||
0x6182, 0x0024, 0x0000, 0x0024, 0x2800, 0x5dd5, 0x0000, 0x0024, /* 1f0 */
|
||||
0x0030, 0x03d0, 0x0000, 0x00c1, 0x3000, 0xc024, 0xb318, 0x0024, /* 1f8 */
|
||||
0x6896, 0x0024, 0x6436, 0x0024, 0x0020, 0x0003, 0x2800, 0x59c5, /* 200 */
|
||||
0x0000, 0x0024, 0x0006, 0x1150, 0x3000, 0x4024, 0x6136, 0x0024, /* 208 */
|
||||
0x0000, 0x0024, 0x2800, 0x4741, 0x0000, 0x0024, 0x0000, 0x0803, /* 210 */
|
||||
0x4132, 0x0024, 0x3800, 0x4024, 0x0006, 0x0190, 0x0006, 0xf011, /* 218 */
|
||||
0x2900, 0xb500, 0x3613, 0x0024, 0x0006, 0xf011, 0x0006, 0x1152, /* 220 */
|
||||
0x0006, 0x0250, 0x4082, 0x0800, 0xfe82, 0x184c, 0x1fff, 0xfc41, /* 228 */
|
||||
0x48ba, 0x0024, 0xae1a, 0x0024, 0x2900, 0xb500, 0x4280, 0x4103, /* 230 */
|
||||
0x0006, 0x1110, 0x4084, 0x0800, 0xfe84, 0x0002, 0x48ba, 0x0024, /* 238 */
|
||||
0xae12, 0x0024, 0xf400, 0x4001, 0x0000, 0x0180, 0x6200, 0x0024, /* 240 */
|
||||
0x0000, 0x0080, 0x2800, 0x5241, 0x4200, 0x0024, 0x3800, 0x0024, /* 248 */
|
||||
0x0006, 0x1090, 0x3004, 0x8024, 0xf400, 0x4491, 0x3113, 0x0024, /* 250 */
|
||||
0x3804, 0x4024, 0x3a00, 0xc024, 0x3004, 0x8024, 0xf400, 0x4491, /* 258 */
|
||||
0x3113, 0x0024, 0x3804, 0x4024, 0x3a00, 0x4024, 0x0006, 0x1081, /* 260 */
|
||||
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x0f00, 0x2800, 0x5248, /* 268 */
|
||||
0x0000, 0x0024, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0080, /* 270 */
|
||||
0x3000, 0x4024, 0x0030, 0x0710, 0xb104, 0x0024, 0x0000, 0x0001, /* 278 */
|
||||
0x3800, 0x4024, 0x0006, 0x08d0, 0x3001, 0x0024, 0x0006, 0x0910, /* 280 */
|
||||
0x3000, 0x4024, 0x6100, 0x0024, 0x6042, 0x0024, 0x0030, 0x06d0, /* 288 */
|
||||
0x2800, 0x5711, 0xb880, 0x0024, 0x2900, 0x21c0, 0x4380, 0x184c, /* 290 */
|
||||
0xb880, 0x0024, 0x3800, 0x0024, 0x36f4, 0x8024, 0x36f4, 0x1811, /* 298 */
|
||||
0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, /* 2a0 */
|
||||
0x36f3, 0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, /* 2a8 */
|
||||
0x0006, 0x1152, 0x0000, 0x0804, 0x3200, 0xc024, 0x6346, 0x0024, /* 2b0 */
|
||||
0x6386, 0x2803, 0x0000, 0x0024, 0x2800, 0x4755, 0x0000, 0x0024, /* 2b8 */
|
||||
0x3800, 0x4024, 0x0030, 0x0690, 0x0000, 0x0081, 0xb882, 0x22c1, /* 2c0 */
|
||||
0x3800, 0x4024, 0x0030, 0x0590, 0x2800, 0x4740, 0x3800, 0x4024, /* 2c8 */
|
||||
0x2800, 0x5700, 0x4190, 0x0024, 0x0007, 0x0001, 0x8179, 0x0006, /* 2d0 */
|
||||
0x00a6, 0x3e12, 0x0024, 0x3e05, 0xb814, 0x3625, 0x0024, 0x3e00, /* 2d8 */
|
||||
0x3841, 0x3e00, 0xb843, 0x3e04, 0x3851, 0x0006, 0x1110, 0x3e04, /* 2e0 */
|
||||
0xb813, 0x3000, 0x0024, 0x6080, 0x0024, 0x0006, 0x11d2, 0x2800, /* 2e8 */
|
||||
0x70c5, 0x0000, 0x0081, 0x6010, 0x984c, 0x3800, 0x0024, 0x0006, /* 2f0 */
|
||||
0x10d0, 0x3200, 0x0024, 0xf100, 0x0011, 0xf100, 0x0024, 0xf102, /* 2f8 */
|
||||
0x0400, 0x0006, 0x1311, 0x2900, 0x0400, 0x3100, 0x8024, 0x0030, /* 300 */
|
||||
0x1293, 0x3413, 0x184c, 0x3c04, 0x4024, 0x3b00, 0x0024, 0x3004, /* 308 */
|
||||
0xc024, 0xf400, 0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3310, /* 310 */
|
||||
0x0024, 0x3a00, 0x0024, 0x0006, 0x1212, 0x3200, 0x0024, 0xf100, /* 318 */
|
||||
0x13d1, 0xf100, 0x0402, 0x2900, 0x0400, 0xf102, 0x0c00, 0x0030, /* 320 */
|
||||
0x12d1, 0x0006, 0x1081, 0x3900, 0x0024, 0x3004, 0xc024, 0xf400, /* 328 */
|
||||
0x44d1, 0x3113, 0x0024, 0x3804, 0x4024, 0x3300, 0x0024, 0x3a00, /* 330 */
|
||||
0x0024, 0xf400, 0x4440, 0x6010, 0x0024, 0x1fee, 0xe002, 0x2800, /* 338 */
|
||||
0x6bc8, 0x0006, 0x0f00, 0x3800, 0x0024, 0x0006, 0x0010, 0xb886, /* 340 */
|
||||
0x0040, 0x30f0, 0x4024, 0x6c92, 0x40c3, 0x3810, 0x0024, 0xb182, /* 348 */
|
||||
0x23c1, 0x0006, 0x0950, 0x3000, 0x0024, 0x6090, 0x0024, 0x6cd2, /* 350 */
|
||||
0x2000, 0x0000, 0x0000, 0x2800, 0x70c8, 0x0000, 0x0024, 0x3800, /* 358 */
|
||||
0x0024, 0x0000, 0x0210, 0x3010, 0x0024, 0x30f0, 0x4024, 0x6c92, /* 360 */
|
||||
0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, 0x36f4, 0x9813, 0x36f4, /* 368 */
|
||||
0x1811, 0x36f0, 0x9803, 0x36f0, 0x1801, 0x3405, 0x9014, 0x36f3, /* 370 */
|
||||
0x0024, 0x36f2, 0x0024, 0x2000, 0x0000, 0x0000, 0x0024, 0x0007, /* 378 */
|
||||
0x0001, 0x81cc, 0x0006, 0x00f4, 0x3e00, 0x3841, 0x0000, 0x0201, /* 380 */
|
||||
0x3e00, 0xb843, 0x3e01, 0x3845, 0x3e04, 0x3812, 0x0030, 0x0410, /* 388 */
|
||||
0x3000, 0x0024, 0x6012, 0x0024, 0x0006, 0x08d0, 0x2800, 0x8045, /* 390 */
|
||||
0x0000, 0x0181, 0x6012, 0x0024, 0x0006, 0x1250, 0x2800, 0x7e45, /* 398 */
|
||||
0x0000, 0x05c1, 0x6012, 0x0024, 0x0030, 0x01d0, 0x2800, 0x7c45, /* 3a0 */
|
||||
0x0000, 0x0581, 0x6010, 0x03cc, 0x0000, 0x0024, 0x2800, 0x7a95, /* 3a8 */
|
||||
0x0000, 0x0024, 0x3000, 0x8024, 0x0006, 0x1250, 0x3000, 0x0024, /* 3b0 */
|
||||
0x6092, 0x0024, 0x3800, 0x4024, 0xf400, 0x4010, 0x3800, 0x8024, /* 3b8 */
|
||||
0x36f4, 0x1812, 0x36f1, 0x1805, 0x36f0, 0x9803, 0x36f0, 0x1801, /* 3c0 */
|
||||
0x2000, 0x0000, 0x0000, 0x0024, 0x0030, 0x01d0, 0x3000, 0x0024, /* 3c8 */
|
||||
0x0006, 0x1250, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3d0 */
|
||||
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3d8 */
|
||||
0x6090, 0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x3000, 0x0024, /* 3e0 */
|
||||
0x0030, 0x0190, 0x2800, 0x7a80, 0x3800, 0x0024, 0x3000, 0x0024, /* 3e8 */
|
||||
0x6080, 0x0024, 0x0000, 0x0024, 0x2800, 0x8515, 0x0000, 0x0024, /* 3f0 */
|
||||
0x0006, 0x1350, 0x0000, 0x0082, 0x0030, 0x0352, 0xb886, 0x0040, /* 3f8 */
|
||||
0x30f0, 0x4024, 0x4cd2, 0x0024, 0x3810, 0x0024, 0x38f0, 0x4024, /* 400 */
|
||||
0x3a00, 0x0024, 0x3010, 0x0024, 0x30f0, 0x4024, 0x0030, 0x0390, /* 408 */
|
||||
0x2800, 0x7a80, 0x4180, 0x2001, 0x4090, 0x0024, 0x3800, 0x0024, /* 410 */
|
||||
0x0030, 0x0250, 0x3800, 0x0024, 0x0006, 0x1290, 0x3000, 0x0024, /* 418 */
|
||||
0x6090, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x8024, /* 420 */
|
||||
0x3223, 0x0024, 0x32e0, 0x4024, 0x6100, 0x0024, 0x0000, 0x0024, /* 428 */
|
||||
0x2800, 0x8c81, 0x0000, 0x0024, 0x3233, 0x0024, 0x3804, 0x8024, /* 430 */
|
||||
0x3200, 0x0024, 0x6080, 0x0024, 0x0006, 0x0300, 0x2800, 0x8b18, /* 438 */
|
||||
0x0000, 0x0024, 0x3800, 0x0024, 0x0006, 0x0850, 0x3004, 0x0024, /* 440 */
|
||||
0x3013, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, 0x3800, 0x0024, /* 448 */
|
||||
0x0006, 0x0850, 0x3004, 0x0024, 0x3000, 0x0024, 0x0006, 0x1290, /* 450 */
|
||||
0x6080, 0x0024, 0x3000, 0x0024, 0x2800, 0x9115, 0xf400, 0x4010, /* 458 */
|
||||
0x3000, 0x0024, 0x0000, 0x0201, 0x0000, 0x0005, 0x0030, 0x0210, /* 460 */
|
||||
0xa014, 0x4004, 0x1fff, 0xfe01, 0xae12, 0x0024, 0xc200, 0x0024, /* 468 */
|
||||
0x2800, 0x8180, 0x3800, 0x0024, 0x2800, 0x8ec0, 0x3009, 0x0000, /* 470 */
|
||||
0x0007, 0x0001, 0x8246, 0x0006, 0x0104, 0x0030, 0x1092, 0x0007, /* 478 */
|
||||
0x9250, 0x003f, 0xfc42, 0xb880, 0x184c, 0x3e12, 0x0024, 0x3800, /* 480 */
|
||||
0x0024, 0x0030, 0x0290, 0x38f0, 0x0024, 0x3800, 0x0024, 0x0030, /* 488 */
|
||||
0x0050, 0x3000, 0x4024, 0xb122, 0x0024, 0x6894, 0x2001, 0x0000, /* 490 */
|
||||
0x0141, 0x3a70, 0x4024, 0x0004, 0x1fc1, 0x3a00, 0x4024, 0x0030, /* 498 */
|
||||
0x00d2, 0x0030, 0x0001, 0x3a00, 0x4024, 0x0030, 0x0552, 0x3a10, /* 4a0 */
|
||||
0x0024, 0x3a00, 0x0024, 0x3000, 0x4024, 0xc122, 0x0024, 0x3800, /* 4a8 */
|
||||
0x4024, 0x0030, 0x05d0, 0x0000, 0x03c1, 0x3820, 0x4024, 0x3800, /* 4b0 */
|
||||
0x0024, 0x0000, 0x0310, 0x3010, 0x0024, 0x30f0, 0x4024, 0xf2c2, /* 4b8 */
|
||||
0x0024, 0x3810, 0x0024, 0x0000, 0x3fc0, 0x38f0, 0x4024, 0x0030, /* 4c0 */
|
||||
0x02d0, 0x3000, 0x4024, 0x2912, 0x1400, 0xb104, 0x0024, 0x0006, /* 4c8 */
|
||||
0x1312, 0x6802, 0x0024, 0x000d, 0xac00, 0x6012, 0x2801, 0x0000, /* 4d0 */
|
||||
0x0024, 0x2800, 0x9dc1, 0x0000, 0x0024, 0x3a00, 0x0024, 0x2909, /* 4d8 */
|
||||
0x1b40, 0x3613, 0x0024, 0x0000, 0x0084, 0x0000, 0x1905, 0x2908, /* 4e0 */
|
||||
0xbe80, 0x3613, 0x0024, 0x0000, 0x0000, 0x0006, 0x0302, 0x4002, /* 4e8 */
|
||||
0x0024, 0x4012, 0x0024, 0x4212, 0x0024, 0xf400, 0x4050, 0x3000, /* 4f0 */
|
||||
0x4024, 0x6182, 0x0024, 0x0006, 0x0350, 0x2800, 0xa6c8, 0x0000, /* 4f8 */
|
||||
0x0024, 0x4002, 0x0024, 0x4014, 0x0024, 0x0006, 0x0301, 0x4124, /* 500 */
|
||||
0x0024, 0x0000, 0x0081, 0x4212, 0x0024, 0x4002, 0x4050, 0x4014, /* 508 */
|
||||
0x0003, 0x0006, 0x0301, 0x4122, 0x0024, 0x6192, 0x0024, 0x6090, /* 510 */
|
||||
0x4050, 0x3000, 0x4024, 0x0006, 0x0910, 0x6312, 0x0024, 0x6194, /* 518 */
|
||||
0x0001, 0x4122, 0x0024, 0x2800, 0x9f80, 0x3800, 0x4024, 0x0006, /* 520 */
|
||||
0x12d2, 0x0006, 0x0991, 0x3000, 0x0024, 0x0006, 0x1290, 0x3a00, /* 528 */
|
||||
0x0024, 0x3800, 0x0024, 0xf400, 0x4010, 0x2900, 0xb200, 0x0000, /* 530 */
|
||||
0x0580, 0x0030, 0x0210, 0x0014, 0x9240, 0x003f, 0xf502, 0x003f, /* 538 */
|
||||
0xffc3, 0x3800, 0x0024, 0x0000, 0x0580, 0x0006, 0x1350, 0x3200, /* 540 */
|
||||
0x4024, 0x4102, 0x0024, 0x3a00, 0x4024, 0x3810, 0x8024, 0x38f0, /* 548 */
|
||||
0xc024, 0x0006, 0x08d0, 0x3800, 0x0024, 0x0030, 0x0690, 0x0000, /* 550 */
|
||||
0x8280, 0xb880, 0x2080, 0x3800, 0x0024, 0x6890, 0x2000, 0x0030, /* 558 */
|
||||
0x0490, 0x3800, 0x0024, 0x0030, 0x0010, 0x0000, 0x0100, 0x3000, /* 560 */
|
||||
0x584c, 0xb100, 0x0024, 0x0000, 0x0024, 0x2800, 0xb185, 0x0000, /* 568 */
|
||||
0x0024, 0x003f, 0xfec1, 0x3000, 0x1bcc, 0xb010, 0x0024, 0x2908, /* 570 */
|
||||
0x0b80, 0x3800, 0x0024, 0x3613, 0x0024, 0x2910, 0x0180, 0x0000, /* 578 */
|
||||
0xae48, 0x0007, 0x0001, 0x1806, 0x0006, 0x8007, 0x0000, 0x0006, /* 580 */
|
||||
0x002f, 0x0010, 0x17ff, 0x0000, 0x1a00, 0x1dff, 0x0000, 0x1f00, /* 588 */
|
||||
0x3fff, 0x0001, 0x0000, 0x17ff, 0x0001, 0x1c00, 0x3fff, 0x0001, /* 590 */
|
||||
0xe000, 0xfffd, 0xffff, 0x0000, 0x0000, 0x180c, 0x180c, 0x0000, /* 598 */
|
||||
0x0000, 0x0000, 0x4952, 0x4646, 0xffff, 0xffff, 0x4157, 0x4556, /* 5a0 */
|
||||
0x6d66, 0x2074, 0x0010, 0x0000, 0x0001, 0x0001, 0xbb80, 0x0000, /* 5a8 */
|
||||
0x7700, 0x0001, 0x0002, 0x0010, 0x6164, 0x6174, 0xffff, 0xffff, /* 5b0 */
|
||||
0x0006, 0x8006, 0x0000, 0x0006, 0x0005, 0x183c, 0x183c, 0x0000, /* 5b8 */
|
||||
0x0020, 0x0040, 0x0006, 0x8003, 0x0000, 0x0007, 0x0001, 0x5bc0, /* 5c0 */
|
||||
0x0006, 0x0009, 0x801c, 0x7fe4, 0x8039, 0x804e, 0x7fb2, 0x809d, /* 5c8 */
|
||||
0x809c, 0x7f64, 0x8139, 0x0007, 0x0001, 0x82c8, 0x0006, 0x0018, /* 5d0 */
|
||||
0x4080, 0x184c, 0x3e13, 0x780f, 0x2800, 0xb405, 0x4090, 0x380e, /* 5d8 */
|
||||
0x2400, 0xb3c0, 0xf400, 0x4417, 0x3110, 0x0024, 0x3f10, 0x0024, /* 5e0 */
|
||||
0x36f3, 0x8024, 0x36f3, 0x580f, 0x2000, 0x0000, 0x0000, 0x0024, /* 5e8 */
|
||||
0x0007, 0x0001, 0x82d4, 0x0006, 0x002a, 0x3e11, 0xb807, 0x3009, /* 5f0 */
|
||||
0x384a, 0x3e11, 0x3805, 0x3e10, 0xb803, 0x3e00, 0x4442, 0x0001, /* 5f8 */
|
||||
0x800a, 0xbf8e, 0x8443, 0xfe06, 0x0045, 0x3011, 0x0401, 0x545e, /* 600 */
|
||||
0x0385, 0x525e, 0x2040, 0x72ce, 0x1bc1, 0x48ba, 0x9803, 0x4588, /* 608 */
|
||||
0x4885, 0x6fee, 0x1bc2, 0x4ffe, 0x9805, 0xf6fe, 0x1bc4, 0xf7f0, /* 610 */
|
||||
0x2046, 0x3801, 0xdbca, 0x2000, 0x0000, 0x36f1, 0x9807,
|
||||
};
|
||||
const uint16_t patch_size = 1567;
|
||||
DEBUG("Patching...\n");
|
||||
|
||||
_spi->select_vs1053_xcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
|
||||
for (int i=0; i<patch_size; i++) {
|
||||
DEBUG(" %d\n", i);
|
||||
unsigned short addr, n, val;
|
||||
addr = patch_data[i++];
|
||||
n = patch_data[i++];
|
||||
SPI.transfer(CMD_WRITE);
|
||||
SPI.transfer(addr & 0XFF);
|
||||
if (n & 0x8000U) { /* RLE run, replicate n samples */
|
||||
n &= 0x7FFF;
|
||||
val = patch_data[i++];
|
||||
while (n--) {
|
||||
SPI.transfer(val >> 8);
|
||||
SPI.transfer(val & 0xFF);
|
||||
_wait();
|
||||
}
|
||||
} else { /* Copy run, copy n samples */
|
||||
while (n--) {
|
||||
val = patch_data[i++];
|
||||
SPI.transfer(val >> 8);
|
||||
SPI.transfer(val & 0xFF);
|
||||
_wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SPI.endTransaction();
|
||||
_spi->select_vs1053_xcs(false);
|
||||
|
||||
DEBUG("Patch sent.\n");
|
||||
}
|
||||
|
||||
void Player::_write_data(uint8_t* buffer) {
|
||||
SPIMaster::enable(XDCS);
|
||||
_spi->select_vs1053_xdcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
for (uint i=0; i<sizeof(_buffer); i++) {
|
||||
SPI.transfer(_buffer[i]);
|
||||
}
|
||||
SPI.endTransaction();
|
||||
SPIMaster::disable();
|
||||
_spi->select_vs1053_xdcs(false);
|
||||
}
|
||||
|
||||
uint16_t Player::_read_wram(uint16_t address) {
|
||||
@ -159,174 +457,99 @@ void Player::set_volume(uint8_t vol, bool save) {
|
||||
}
|
||||
INFO("Setting volume to %d\n", vol);
|
||||
vol = 0xFF - vol;
|
||||
if (vol==0xFF) vol=0xFE;
|
||||
uint16_t value = (vol<<8)|vol;
|
||||
DEBUG("Setting volume register to 0x%04X\n", value);
|
||||
_write_control_register(SCI_VOL, value);
|
||||
}
|
||||
|
||||
void Player::vol_up() {
|
||||
if (_volume == VOLUME_MAX) play_system_sound("volume_max.mp3");
|
||||
else if (_volume + VOLUME_STEP > VOLUME_MAX) set_volume(VOLUME_MAX);
|
||||
else set_volume(_volume + VOLUME_STEP);
|
||||
uint8_t vol = _volume + VOLUME_STEP;
|
||||
if (vol > VOLUME_MAX) vol=VOLUME_MAX;
|
||||
set_volume(vol);
|
||||
}
|
||||
|
||||
void Player::vol_down() {
|
||||
if (_volume >= VOLUME_MIN + VOLUME_STEP) set_volume(_volume - VOLUME_STEP);
|
||||
else if (_volume == VOLUME_MIN) play_system_sound("volume_min.mp3");
|
||||
else set_volume(VOLUME_MIN);
|
||||
uint8_t vol = _volume - VOLUME_STEP;
|
||||
if (vol < VOLUME_MIN) vol=VOLUME_MIN;
|
||||
set_volume(vol);
|
||||
}
|
||||
|
||||
void Player::_mute() {
|
||||
INFO("Muting.");
|
||||
set_volume(0, false);
|
||||
INFO("Muting.\n");
|
||||
_speaker_off();
|
||||
set_volume(1, false);
|
||||
}
|
||||
|
||||
void Player::_unmute() {
|
||||
INFO("Unmuting.");
|
||||
INFO("Unmuting.\n");
|
||||
set_volume(_volume, false);
|
||||
_speaker_on();
|
||||
}
|
||||
|
||||
void Player::track_next() {
|
||||
if (_state != playing) return;
|
||||
if (_playing_index + 1 >= _playing_album_songs) {
|
||||
play_system_sound("no_next_song.mp3");
|
||||
if (!_current_playlist->has_track_next()) {
|
||||
return;
|
||||
}
|
||||
stop();
|
||||
play_song(_playing_album, _playing_index + 1);
|
||||
_current_playlist->track_next();
|
||||
play();
|
||||
}
|
||||
|
||||
void Player::track_prev() {
|
||||
if (_state != playing) return;
|
||||
if (_current_play_position > 100000) {
|
||||
stop();
|
||||
play_song(_playing_album, _playing_index);
|
||||
_current_playlist->track_restart();
|
||||
play();
|
||||
} else {
|
||||
if (_playing_index == 0) {
|
||||
play_system_sound("no_prev_song.mp3");
|
||||
if (!_current_playlist->has_track_prev()) {
|
||||
return;
|
||||
}
|
||||
stop();
|
||||
play_song(_playing_album, _playing_index - 1);
|
||||
_current_playlist->track_prev();
|
||||
play();
|
||||
}
|
||||
}
|
||||
|
||||
std::list<String> Player::ls(String path) {
|
||||
SPIMaster::enable(PIN_SD_CS);
|
||||
std::list<String> result;
|
||||
if (!SD.exists(path)) return result;
|
||||
File dir = SD.open(path);
|
||||
File entry;
|
||||
while (entry = dir.openNextFile()) {
|
||||
String filename = entry.name();
|
||||
if (entry.isDirectory()) filename.concat("/");
|
||||
result.push_back(filename);
|
||||
}
|
||||
return result;
|
||||
void Player::set_track(uint8_t id) {
|
||||
stop();
|
||||
_current_playlist->set_track(id);
|
||||
play();
|
||||
}
|
||||
|
||||
String Player::_find_album_dir(String id) {
|
||||
String id_with_divider = id + " - ";
|
||||
File root = SD.open("/");
|
||||
File entry;
|
||||
String result = String("");
|
||||
while ((result.length()==0) && (entry = root.openNextFile())) {
|
||||
String name = entry.name();
|
||||
if (entry.isDirectory() && (name.startsWith(id_with_divider) || name.equals(id))) {
|
||||
result = name;
|
||||
}
|
||||
entry.close();
|
||||
}
|
||||
root.close();
|
||||
return result;
|
||||
bool Player::is_playing() {
|
||||
return _state == playing;
|
||||
}
|
||||
|
||||
std::list<String> Player::_files_in_dir(String path) {
|
||||
DEBUG("Examining folder %s...\n", path.c_str());
|
||||
if (!path.startsWith("/")) path = String("/") + path;
|
||||
if (!path.endsWith("/")) path.concat("/");
|
||||
std::list<String> result;
|
||||
if (!SD.exists(path)) return result;
|
||||
File dir = SD.open(path);
|
||||
File entry;
|
||||
while (entry = dir.openNextFile()) {
|
||||
String filename = entry.name();
|
||||
if (!entry.isDirectory() &&
|
||||
!filename.startsWith(".") &&
|
||||
( filename.endsWith(".mp3") ||
|
||||
filename.endsWith(".ogg") ||
|
||||
filename.endsWith(".wma") ||
|
||||
filename.endsWith(".mp4") ||
|
||||
filename.endsWith(".mpa"))) {
|
||||
DEBUG(" Adding entry %s\n", filename.c_str());
|
||||
result.push_back(path + filename);
|
||||
} else {
|
||||
DEBUG(" Ignoring entry %s\n", filename.c_str());
|
||||
}
|
||||
entry.close();
|
||||
}
|
||||
dir.close();
|
||||
result.sort();
|
||||
|
||||
return result;
|
||||
bool Player::play(Playlist* p) {
|
||||
_current_playlist = p;
|
||||
return play();
|
||||
}
|
||||
|
||||
bool Player::play_album(String album) {
|
||||
//if (_state==playing) stop();
|
||||
album_state s = _last_tracks[album.c_str()];
|
||||
DEBUG("Last index for album %s was %d,%d\n", album.c_str(), s.index, s.position);
|
||||
return play_song(album, s.index, s.position);
|
||||
}
|
||||
|
||||
bool Player::play_song(String album, uint8_t index, uint32_t skip_to) {
|
||||
bool Player::play() {
|
||||
if (_state == sleeping || _state == recording) _wakeup();
|
||||
if (_state != idle) return false;
|
||||
DEBUG("Trying to play song at index %d, offset %d of album %s\n", index, skip_to, album.c_str());
|
||||
String path = _find_album_dir(album);
|
||||
if (path.length()==0) {
|
||||
ERROR("Could not find album.\n");
|
||||
return false;
|
||||
} else {
|
||||
INFO("Found album in dir '%s'.\n", path.c_str());
|
||||
}
|
||||
std::list<String> files = _files_in_dir(path);
|
||||
_playing_album_songs = files.size();
|
||||
DEBUG("Found %d songs in album\n", files.size());
|
||||
if (index >= files.size()) {
|
||||
ERROR("No matching file found - not playing.\n");
|
||||
return false;
|
||||
}
|
||||
//std::list<String>::iterator it = files.begin();
|
||||
//std::advance(it, index);
|
||||
String file = *(std::next(files.begin(), index));
|
||||
if (_current_playlist == NULL) return false;
|
||||
_current_playlist->start();
|
||||
String file = _current_playlist->get_current_file();
|
||||
uint32_t position = _current_playlist->get_position();
|
||||
_state = playing;
|
||||
_playing_album = album;
|
||||
_playing_index = index;
|
||||
_set_last_track(album.c_str(), index, skip_to);
|
||||
_play_file(file, skip_to);
|
||||
_play_file(file, position);
|
||||
_controller->send_player_status();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Player::play_system_sound(String filename) {
|
||||
//String file = String("/system/") + filename;
|
||||
String file = filename;
|
||||
if (!SD.exists(file)) {
|
||||
ERROR("File %s does not exist!\n", file.c_str());
|
||||
return;
|
||||
}
|
||||
if (_state == playing) {
|
||||
stop();
|
||||
_state = system_sound_while_playing;
|
||||
} else {
|
||||
_state = system_sound_while_stopped;
|
||||
}
|
||||
_play_file(file, 0);
|
||||
}
|
||||
|
||||
void Player::_play_file(String file, uint32_t file_offset) {
|
||||
INFO("play_file('%s', %d)\n", file.c_str(), file_offset);
|
||||
_spi->select_sd();
|
||||
_file = SD.open(file);
|
||||
if (!_file) return;
|
||||
_file_size = _file.size();
|
||||
_spi->select_sd(false);
|
||||
if (!_file) {
|
||||
DEBUG("Could not open file %s", file.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG("Resetting SCI_DECODE_TIME...\n");
|
||||
_write_control_register(SCI_DECODE_TIME, 0);
|
||||
@ -334,14 +557,18 @@ void Player::_play_file(String file, uint32_t file_offset) {
|
||||
_write_control_register(SCI_STATUS, _read_control_register(SCI_STATUS) & ~SS_DO_NOT_JUMP);
|
||||
delay(100);
|
||||
|
||||
_spi->select_sd();
|
||||
if (file_offset == 0) {
|
||||
_file.seek(_id3_tag_offset(_file));
|
||||
}
|
||||
_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();
|
||||
}
|
||||
|
||||
uint32_t Player::_id3_tag_offset(File f) {
|
||||
@ -369,13 +596,14 @@ uint32_t Player::_id3_tag_offset(File f) {
|
||||
}
|
||||
|
||||
void Player::_flush(uint count, int8_t byte) {
|
||||
SPIMaster::enable(XDCS);
|
||||
_spi->select_vs1053_xdcs();
|
||||
SPI.beginTransaction(*_spi_settings);
|
||||
for(uint i=0; i<count; i++) {
|
||||
_wait();
|
||||
SPI.transfer(byte);
|
||||
}
|
||||
SPI.endTransaction();
|
||||
_spi->select_vs1053_xdcs(false);
|
||||
}
|
||||
|
||||
void Player::_finish_playing() {
|
||||
@ -390,15 +618,14 @@ void Player::_finish_playing() {
|
||||
// If we reached this, the Chip didn't stop. That should not happen.
|
||||
// (That's written in the manual.)
|
||||
// Reset the chip.
|
||||
_init();
|
||||
init();
|
||||
}
|
||||
|
||||
void Player::stop() {
|
||||
if (_state != playing /* && _state != system_sound_while_playing && _state != system_sound_while_stopped*/) return;
|
||||
void Player::stop(bool turn_speaker_off) {
|
||||
if (_state != playing) return;
|
||||
INFO("Stopping...\n");
|
||||
if (_state == playing) {
|
||||
_set_last_track(_playing_album.c_str(), _playing_index, (uint32_t)_file.position());
|
||||
}
|
||||
_current_playlist->set_position(_current_play_position);
|
||||
|
||||
_state = stopping;
|
||||
_stop_delay = 0;
|
||||
_write_control_register(SCI_MODE, _read_control_register(SCI_MODE) | SM_CANCEL);
|
||||
@ -408,7 +635,7 @@ void Player::stop() {
|
||||
uint16_t mode = _read_control_register(SCI_MODE);
|
||||
if ((mode & SM_CANCEL) == 0) {
|
||||
_flush(2052, endbyte);
|
||||
_finish_stopping();
|
||||
_finish_stopping(turn_speaker_off);
|
||||
break;
|
||||
} else if (_stop_delay > 2048) {
|
||||
init();
|
||||
@ -418,37 +645,39 @@ void Player::stop() {
|
||||
}
|
||||
}
|
||||
|
||||
void Player::_finish_stopping() {
|
||||
void Player::_finish_stopping(bool turn_speaker_off) {
|
||||
if (turn_speaker_off) _speaker_off();
|
||||
_state = idle;
|
||||
_stopped_at = millis();
|
||||
if (_file) {
|
||||
_file.close();
|
||||
}
|
||||
_current_play_position = 0;
|
||||
_file_size = 0;
|
||||
INFO("Stopped.\n");
|
||||
_controller->send_player_status();
|
||||
}
|
||||
|
||||
void Player::_refill() {
|
||||
SPIMaster::enable(PIN_SD_CS);
|
||||
_spi->select_sd();
|
||||
_refills++;
|
||||
if (_refills % 1000 == 0) DEBUG(".");
|
||||
uint8_t result = _file.read(_buffer, sizeof(_buffer));
|
||||
_spi->select_sd(false);
|
||||
if (result == 0) {
|
||||
// File is over.
|
||||
DEBUG("EOF reached.\n");
|
||||
_skip_to = 0;
|
||||
_finish_playing();
|
||||
if (_state == system_sound_while_playing) {
|
||||
_finish_stopping();
|
||||
play_album(_playing_album);
|
||||
return;
|
||||
} else if (_state == system_sound_while_stopped) {
|
||||
_finish_stopping();
|
||||
return;
|
||||
_finish_stopping(false);
|
||||
if (_current_playlist->has_track_next()) {
|
||||
_current_playlist->track_next();
|
||||
play();
|
||||
} else {
|
||||
_current_playlist->reset();
|
||||
_controller->send_player_status();
|
||||
}
|
||||
|
||||
_finish_stopping();
|
||||
bool result = play_song(_playing_album, _playing_index + 1);
|
||||
if (!result) {
|
||||
_set_last_track(_playing_album.c_str(), 0, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_current_play_position+=result;
|
||||
@ -460,9 +689,12 @@ void Player::_refill() {
|
||||
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;
|
||||
@ -472,21 +704,60 @@ void Player::_refill() {
|
||||
}
|
||||
|
||||
bool Player::_refill_needed() {
|
||||
return _state==playing ||
|
||||
_state==stopping ||
|
||||
_state==system_sound_while_playing ||
|
||||
_state==system_sound_while_stopped;
|
||||
return _state==playing || _state==stopping;
|
||||
}
|
||||
|
||||
bool Player::loop() {
|
||||
if (digitalRead(DREQ) && _refill_needed()) {
|
||||
if (PIN_VS1053_DREQ() && _refill_needed()) {
|
||||
_refill();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_state == recording) {
|
||||
DEBUG("r");
|
||||
uint16_t samples_available = _read_control_register(SCI_HDAT1, false);
|
||||
uint16_t vu_value = _read_control_register(SCI_AICTRL0, false);
|
||||
DEBUG("Samples available: %4d, VU meter: 0x%04X\n", samples_available, vu_value);
|
||||
if (samples_available >= 500) {
|
||||
unsigned long sum = 0;
|
||||
for (int i=0; i<500; i++) {
|
||||
uint16_t sample = _read_control_register(SCI_HDAT0, false);
|
||||
sum += sample * sample;
|
||||
}
|
||||
double result = sqrt(sum / 500);
|
||||
DEBUG("Loudness: %f", result);
|
||||
}
|
||||
}
|
||||
|
||||
if (_state == idle && _stopped_at < millis() - VS1053_SLEEP_DELAY) {
|
||||
_sleep();
|
||||
//_record();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Player::_set_last_track(const char* album, uint8_t index, uint32_t position) {
|
||||
DEBUG("Setting _last_track[%s]=%d,%d.\n", album, index, position);
|
||||
_last_tracks[album] = {index, position};
|
||||
String Player::json() {
|
||||
DynamicJsonDocument json(10240);
|
||||
json["_type"] = "player";
|
||||
json["playing"] = is_playing();
|
||||
if (_current_playlist) {
|
||||
JsonObject playlist = json.createNestedObject("playlist");
|
||||
_current_playlist->json(playlist);
|
||||
} else {
|
||||
json["playlist"] = nullptr;
|
||||
}
|
||||
JsonObject volume = json.createNestedObject("volume");
|
||||
volume["current"] = _volume;
|
||||
volume["min"] = VOLUME_MIN;
|
||||
volume["max"] = VOLUME_MAX;
|
||||
volume["step"] = VOLUME_STEP;
|
||||
return json.as<String>();
|
||||
}
|
||||
|
||||
String Player::position_json() {
|
||||
DynamicJsonDocument json(200);
|
||||
json["_type"] = "position";
|
||||
json["position"] = _current_play_position;
|
||||
json["file_size"] = _file_size;
|
||||
return json.as<String>();
|
||||
}
|
||||
|
160
src/playlist.cpp
Normal file
160
src/playlist.cpp
Normal file
@ -0,0 +1,160 @@
|
||||
#include <playlist.h>
|
||||
#include "spi_master.h"
|
||||
#include "config.h"
|
||||
#include <SD.h>
|
||||
#include <algorithm>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
Playlist::Playlist(String path) {
|
||||
// Add files to _files
|
||||
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;
|
||||
}
|
||||
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());
|
||||
_files.push_back(entry.name());
|
||||
bool non_ascii_chars = false;
|
||||
for(int i=0; i<filename.length(); i++) {
|
||||
char c = filename.charAt(i);
|
||||
if (c < 0x20 || c >= 0x7F) {
|
||||
non_ascii_chars = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (non_ascii_chars) {
|
||||
ERROR("WARNING: File '%s' contains non-ascii chars!\n", filename.c_str());
|
||||
}
|
||||
} else {
|
||||
TRACE(" Ignoring entry %s\n", filename.c_str());
|
||||
}
|
||||
entry.close();
|
||||
}
|
||||
dir.close();
|
||||
SPIMaster::select_sd(false);
|
||||
std::sort(_files.begin(), _files.end());
|
||||
}
|
||||
|
||||
void Playlist::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);
|
||||
String 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];
|
||||
}
|
||||
|
||||
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].c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Playlist::json(JsonObject json) {
|
||||
json["_type"] = "playlist";
|
||||
JsonArray files = json.createNestedArray("files");
|
||||
for (String file: _files) {
|
||||
files.add(file);
|
||||
}
|
||||
json["current_track"] = _current_track;
|
||||
json["has_track_next"] = has_track_next();
|
||||
json["has_track_prev"] = has_track_prev();
|
||||
}
|
159
src/playlist_manager.cpp
Normal file
159
src/playlist_manager.cpp
Normal file
@ -0,0 +1,159 @@
|
||||
#include "playlist_manager.h"
|
||||
#include <SD.h>
|
||||
#include "spi_master.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
PlaylistManager::PlaylistManager() {
|
||||
scan_files();
|
||||
}
|
||||
|
||||
void PlaylistManager::scan_files() {
|
||||
SPIMaster::select_sd();
|
||||
std::vector<String> folders;
|
||||
File root = SD.open("/");
|
||||
File entry;
|
||||
while (entry = root.openNextFile()) {
|
||||
String foldername = entry.name();
|
||||
if (foldername.startsWith("/.")) continue;
|
||||
foldername.remove(foldername.length());
|
||||
folders.push_back(foldername);
|
||||
_check_for_special_chars(foldername);
|
||||
entry.close();
|
||||
}
|
||||
|
||||
_map.clear();
|
||||
if (!SD.exists("/_mapping.txt\n")) {
|
||||
ERROR("WARNING: File /_mapping.txt not found.\n");
|
||||
} else {
|
||||
File f = SD.open("/_mapping.txt");
|
||||
DEBUG("Reading /_mapping.txt...\n");
|
||||
while (f.available()) {
|
||||
char buffer[512];
|
||||
size_t pos = f.readBytesUntil('\n', buffer, 511);
|
||||
buffer[pos] = '\0';
|
||||
|
||||
String data = buffer;
|
||||
uint8_t eq = data.indexOf('=');
|
||||
if (eq>0 && eq<data.length()-1) {
|
||||
String rfid_id = data.substring(0, eq);
|
||||
String folder = data.substring(eq + 1);
|
||||
TRACE(" Adding mapping: %s=>%s\n", rfid_id.c_str(), folder.c_str());
|
||||
_map[rfid_id] = folder;
|
||||
|
||||
bool found=false;
|
||||
for (String f: folders) {
|
||||
if (f.equals(folder)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
INFO("WARNING: Found mapping for RFID id %s which maps to non-existing folder %s!\n", rfid_id.c_str(), folder.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
root.close();
|
||||
|
||||
_unmapped_folders.clear();
|
||||
for (String folder: folders) {
|
||||
bool found = false;
|
||||
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) {
|
||||
if (it->second.equals(folder)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// Checking folder for media files
|
||||
File dir = SD.open(folder);
|
||||
while (entry = dir.openNextFile()) {
|
||||
String filename = entry.name();
|
||||
filename = filename.substring(folder.length() + 1);
|
||||
if (!filename.startsWith(".") && filename.endsWith(".mp3")) {
|
||||
found = true;
|
||||
}
|
||||
entry.close();
|
||||
if (found) break;
|
||||
}
|
||||
if (found) {
|
||||
_unmapped_folders.push_back(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
SPIMaster::select_sd(false);
|
||||
}
|
||||
|
||||
void PlaylistManager::_check_for_special_chars(String s) {
|
||||
for(int i=0; i<s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (c < 0x20 || c >= 0x7F) {
|
||||
ERROR("WARNING: Folder / file '%s' contains non-ascii chars!\n", s.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Playlist* PlaylistManager::get_playlist_for_id(String id) {
|
||||
if (!_map.count(id)) return NULL;
|
||||
String folder = _map[id];
|
||||
return get_playlist_for_folder(folder);
|
||||
}
|
||||
|
||||
Playlist* PlaylistManager::get_playlist_for_folder(String folder) {
|
||||
if (!_playlists.count(folder)) {
|
||||
_playlists[folder] = new Playlist(folder);
|
||||
}
|
||||
return _playlists[folder];
|
||||
}
|
||||
|
||||
void PlaylistManager::dump_ids() {
|
||||
for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) {
|
||||
INFO(" %s -> %s\n", it->first.c_str(), it->second.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
String PlaylistManager::json() {
|
||||
DynamicJsonDocument json(10240);
|
||||
json["_type"] = "playlist_manager";
|
||||
JsonObject folders = json.createNestedObject("folders");
|
||||
for (std::map<String, String>::iterator it = _map.begin(); it!=_map.end(); it++) {
|
||||
folders[it->second] = it->first;
|
||||
}
|
||||
JsonArray started = json.createNestedArray("started");
|
||||
for (std::map<String, Playlist*>::iterator it = _playlists.begin(); it!=_playlists.end(); it++) {
|
||||
if (it->second->is_fresh()) continue;
|
||||
started.add(it->first);
|
||||
}
|
||||
JsonArray unmapped = json.createNestedArray("unmapped");
|
||||
for(String folder : _unmapped_folders) {
|
||||
unmapped.add(folder);
|
||||
}
|
||||
return json.as<String>();
|
||||
}
|
||||
|
||||
bool PlaylistManager::add_mapping(String id, String folder) {
|
||||
DEBUG("Adding mapping: %s=>%s\n", id.c_str(), folder.c_str());
|
||||
_map[id] = folder;
|
||||
_save_mapping();
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlaylistManager::_save_mapping() {
|
||||
SPIMaster::select_sd();
|
||||
File f = SD.open("/_mapping.txt", "w");
|
||||
String s;
|
||||
for(std::map<String, String>::iterator it = _map.begin(); it != _map.end(); it++) {
|
||||
s += it->first;
|
||||
s += "=";
|
||||
s += it->second;
|
||||
s += '\n';
|
||||
}
|
||||
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;
|
||||
}
|
Reference in New Issue
Block a user