3 Commits

Author SHA1 Message Date
2971df7b68 feat: Enable dualstack IPv4/IPv6 for microdot
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m40s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 7s
Run pytests / Check-Pytest (push) Successful in 10s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-23 12:00:05 +01:00
18a58992f3 feat: Visual feedback on LEDs during startup
Set the LEDs to a fixed color during bootup to show the different modes:
- Orange when the device is booting
- Red when button 0 is held and the device goes to a shell
- Blue when button 1 is held and the device goes to AP mode instead of
  joining the configured WiFi network
- Red blinking when the run() method throws an exception for any reason

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-23 12:00:05 +01:00
4e0af8e3fc feat: Join an existing WiFi network
Add ssid and passphrase to config to configure the network to join. If
empty SSID is configured, it will create the "TonberryPicoAP..." network
in AP mode as before.

Holding the button 1 during startup will revert to AP mode regardless of
the current configuration.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-23 12:00:05 +01:00
15 changed files with 148 additions and 347 deletions

View File

@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>TonBERRY pico</title> <title>Device Admin</title>
<style> <style>
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
@@ -141,7 +141,7 @@
</head> </head>
<body> <body>
<h1>TonBERRY pico</h1> <h1>Device Admin</h1>
<nav> <nav>
<button onclick="showScreen('menu')">🏠 Main Menu</button> <button onclick="showScreen('menu')">🏠 Main Menu</button>
@@ -167,7 +167,7 @@
<div id="screen-config" class="screen"> <div id="screen-config" class="screen">
<h2>Configuration Editor</h2> <h2>Configuration Editor</h2>
<div id="config-container">Loading…</div> <div id="config-container">Loading…</div>
<button id="config-save-btn" disabled>Save &amp; Reboot</button> <button id="config-save-btn" disabled>Save</button>
</div> </div>
<!-- PLAYLIST EDITOR SCREEN 1: list of playlists --> <!-- PLAYLIST EDITOR SCREEN 1: list of playlists -->
@@ -213,10 +213,6 @@
<option value="audioplay">Audioplay (no shuffle, start at previous track)</option> <option value="audioplay">Audioplay (no shuffle, start at previous track)</option>
</select> </select>
</div> </div>
<div>
<label>Playlist name</label>
<input type="text" placeholder="Playlist name" id="playlist-edit-name" />
</div>
<div class="flex-horizontal"> <div class="flex-horizontal">
<div style="flex-grow: 1"> <div style="flex-grow: 1">
<label>Tracks</label> <label>Tracks</label>
@@ -316,9 +312,7 @@
return; return;
} }
alert("Configuration saved successfully, device will now reboot/shutdown! " + alert("Configuration saved successfully!");
"On battery, press Power button after shutdown to restart.");
await fetch('/api/v1/reboot/application', {'method': 'POST'});
} catch (err) { } catch (err) {
alert("Error saving configuration: " + err); alert("Error saving configuration: " + err);
} }
@@ -356,13 +350,7 @@
'root.BUTTON_MAP.PLAY_PAUSE': 'Play/Pause', 'root.BUTTON_MAP.PLAY_PAUSE': 'Play/Pause',
'root.TAG_TIMEOUT_SECS': 'Tag removal timeout (seconds)', 'root.TAG_TIMEOUT_SECS': 'Tag removal timeout (seconds)',
'root.TAGMODE': 'Tag mode', 'root.TAGMODE': 'Tag mode',
'root.LED_COUNT': 'Length of WS2182 (Neopixel) LED chain', 'root.LED_COUNT': 'Length of WS2182 (Neopixel) LED chain'
'root.VOLUME_MAX': 'Maximum volume (0-255)',
'root.VOLUME_BOOT': 'Volume at startup (0-255)',
'root.LED_MAX': 'Maximum LED brightness (0-255)',
'root.WIFI.SSID': 'Network name (SSID) (leave empty for AP mode)',
'root.WIFI.PASSPHRASE': 'Password',
'root.WIFI.SECURITY': 'Security mode'
}; };
const config_input_override = { const config_input_override = {
'root.TAGMODE': { 'root.TAGMODE': {
@@ -396,29 +384,11 @@
'root.LED_COUNT': { 'root.LED_COUNT': {
'input-type': 'number' 'input-type': 'number'
}, },
'root.WIFI.SSID': { 'root.WLAN.SSID': {
'input-type': 'text' 'input-type': 'text'
}, },
'root.WIFI.PASSPHRASE': { 'root.WLAN.PASSPHRASE': {
'input-type': 'text' 'input-type': 'text'
},
'root.WIFI.SECURITY': {
'element': 'select',
'values': {
'open': 'Open',
'wpa_wpa2': 'WPA/WPA2 (PSK Mixed)',
'wpa3': 'WPA3 (SAE AES)',
'wpa2_wpa3': 'WPA2/WPA3 (PSK AES)'
}
},
'root.VOLUME_MAX': {
'input-type': 'number'
},
'root.VOLUME_BOOT': {
'input-type': 'number'
},
'root.LED_MAX': {
'input-type': 'number'
} }
}; };
@@ -523,7 +493,7 @@
document.getElementById('playlist-exist-button-delete') document.getElementById('playlist-exist-button-delete')
.addEventListener('click', (e) => { .addEventListener('click', (e) => {
if (lastSelected === null) return; if (lastSelected === null) return;
const tagid = lastSelected.getAttribute('data-tag'); const tagid = lastSelected.innerText;
if(confirm(`Really delete playlist ${tagid}?`)) { if(confirm(`Really delete playlist ${tagid}?`)) {
fetch(`/api/v1/playlist/${tagid}`, { fetch(`/api/v1/playlist/${tagid}`, {
method: 'DELETE'}) method: 'DELETE'})
@@ -534,7 +504,7 @@
.addEventListener('click', selectLastTag); .addEventListener('click', selectLastTag);
document.getElementById('playlist-exist-button-edit').addEventListener("click", (e) => { document.getElementById('playlist-exist-button-edit').addEventListener("click", (e) => {
if (lastSelected !== null) if (lastSelected !== null)
showScreen("playlist_edit", {load: lastSelected.getAttribute('data-tag')}); showScreen("playlist_edit", {load: lastSelected.innerText});
}); });
document.getElementById('playlist-exist-list').addEventListener("click", (e) => { document.getElementById('playlist-exist-list').addEventListener("click", (e) => {
const node = e.target.closest("li"); const node = e.target.closest("li");
@@ -589,9 +559,8 @@
container.innerHTML = ""; container.innerHTML = "";
for (const playlist of playlists) { for (const playlist of playlists) {
const li = document.createElement("li"); const li = document.createElement("li");
li.innerHTML = `${playlist.name} (${playlist.tag})`; li.innerHTML = playlist;
li.className = "node" li.className = "node"
li.setAttribute('data-tag', playlist.tag);
container.appendChild(li) container.appendChild(li)
} }
} }
@@ -616,7 +585,7 @@
document.getElementById('playlist-exist-list') document.getElementById('playlist-exist-list')
.querySelectorAll("li") .querySelectorAll("li")
.forEach(n => { .forEach(n => {
if (n.getAttribute('data-tag') == tagtext) { if (n.innerText == tagtext) {
n.classList.add("selected"); n.classList.add("selected");
lastSelected = n; lastSelected = n;
} else { } else {
@@ -718,8 +687,6 @@
} else if (playlist.persist === "track" && playlist.shuffle === "no") { } else if (playlist.persist === "track" && playlist.shuffle === "no") {
playlisttype.value = "audioplay"; playlisttype.value = "audioplay";
} }
const playlistname = document.getElementById('playlist-edit-name');
playlistname.value = playlist.name;
} }
async function save() { async function save() {
@@ -742,8 +709,6 @@
playlistData.shuffle = "no"; playlistData.shuffle = "no";
break; break;
} }
const playlistname = document.getElementById('playlist-edit-name');
playlistData.name = playlistname.value;
const container = document.getElementById('playlist-edit-list'); const container = document.getElementById('playlist-edit-list');
playlistData.paths = [...container.querySelectorAll("li")].map((node) => node.innerText); playlistData.paths = [...container.querySelectorAll("li")].map((node) => node.innerText);
const saveRes = await fetch(`/api/v1/playlist/${playlistId}`, const saveRes = await fetch(`/api/v1/playlist/${playlistId}`,
@@ -808,10 +773,6 @@
async function onShow(intent) { async function onShow(intent) {
document.getElementById('playlist-filebrowser-addtrack').disabled = true; document.getElementById('playlist-filebrowser-addtrack').disabled = true;
if (intent !== 'refresh') {
document.getElementById('playlist-filebrowser-upload-progress').value = 0;
document.getElementById("playlist-filebrowser-upload-files").value = "";
}
tree = document.getElementById("playlist-filebrowser-tree"); tree = document.getElementById("playlist-filebrowser-tree");
tree.innerHTML = "Loading..."; tree.innerHTML = "Loading...";
fetch('/api/v1/audiofiles') fetch('/api/v1/audiofiles')
@@ -846,6 +807,7 @@
if (type === 'directory') { if (type === 'directory') {
const nested = document.createElement('ul'); const nested = document.createElement('ul');
node.appendChild(nested); node.appendChild(nested);
node.classList.add('expanded');
parent.appendChild(node); parent.appendChild(node);
return nested; return nested;
} }
@@ -932,7 +894,7 @@
} }
if (donecount + 1 === totalcount) { if (donecount + 1 === totalcount) {
// Reload file list from device // Reload file list from device
onShow('refresh'); onShow();
} else { } else {
uploadFileHelper(totalcount, donecount + 1, destdir, files.slice(1)); uploadFileHelper(totalcount, donecount + 1, destdir, files.slice(1));
} }
@@ -959,7 +921,7 @@
const saveRes = await fetch(`/api/v1/audiofiles?type=directory&location=${location}`, const saveRes = await fetch(`/api/v1/audiofiles?type=directory&location=${location}`,
{method: 'POST'}); {method: 'POST'});
// Reload file list from device // Reload file list from device
onShow('refresh'); onShow();
} }
async function deleteItems() { async function deleteItems() {
@@ -980,7 +942,7 @@
} }
} }
// Reload file list from device // Reload file list from device
onShow('refresh'); onShow();
} }
let tree = (() => { let tree = (() => {

View File

@@ -1,14 +1,11 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
// Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org> // Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
#include <stdarg.h>
#include "py/obj.h" #include "py/obj.h"
#include "sd.h" #include "sd.h"
// Include MicroPython API. // Include MicroPython API.
#include "py/mperrno.h" #include "py/mperrno.h"
#include "py/mpprint.h"
#include "py/runtime.h" #include "py/runtime.h"
// This module is RP2 specific // This module is RP2 specific
@@ -16,14 +13,6 @@
#include <string.h> #include <string.h>
void sd_printf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
mp_vprintf(&mp_sys_stdout_print, fmt, ap);
va_end(ap);
}
const mp_obj_type_t sdcard_type; const mp_obj_type_t sdcard_type;
struct sdcard_obj { struct sdcard_obj {
mp_obj_base_t base; mp_obj_base_t base;
@@ -100,14 +89,11 @@ static mp_obj_t sdcard_writeblocks(mp_obj_t self_obj, mp_obj_t block_obj, mp_obj
if (bufinfo.len % SD_SECTOR_SIZE != 0) if (bufinfo.len % SD_SECTOR_SIZE != 0)
mp_raise_ValueError(MP_ERROR_TEXT("Buffer length is invalid")); mp_raise_ValueError(MP_ERROR_TEXT("Buffer length is invalid"));
const int nblocks = bufinfo.len / SD_SECTOR_SIZE; const int nblocks = bufinfo.len / SD_SECTOR_SIZE;
bool ret; for (int block = 0; block < nblocks; block++) {
if (nblocks > 1) { // TODO: Implement CMD25 write multiple blocks
ret = sd_writeblocks(&self->sd_context, start_block, nblocks, bufinfo.buf); if (!sd_writeblock(&self->sd_context, start_block + block, bufinfo.buf + block * SD_SECTOR_SIZE))
} else { mp_raise_OSError(MP_EIO);
ret = sd_writeblock(&self->sd_context, start_block, bufinfo.buf);
} }
if (!ret)
mp_raise_OSError(MP_EIO);
return mp_const_none; return mp_const_none;
} }
static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_writeblocks_obj, sdcard_writeblocks); static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_writeblocks_obj, sdcard_writeblocks);

View File

@@ -6,7 +6,7 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
extern void sd_printf(const char *fmt, ...); // #define SD_DEBUG
#define SD_R1_ILLEGAL_COMMAND (1 << 2) #define SD_R1_ILLEGAL_COMMAND (1 << 2)
@@ -26,14 +26,14 @@ static bool sd_early_init(void)
for (int i = 0; i < 500; ++i) { for (int i = 0; i < 500; ++i) {
if (sd_cmd(0, 0, 1, &buf)) { if (sd_cmd(0, 0, 1, &buf)) {
#ifdef SD_DEBUG #ifdef SD_DEBUG
sd_printf("CMD0 resp %02x\n", buf); printf("CMD0 resp %02hhx\n", buf);
#endif #endif
if (buf == 0x01) { if (buf == 0x01) {
return true; return true;
} }
} }
#ifdef SD_DEBUG #ifdef SD_DEBUG
sd_printf("CMD0 timeout, try again...\n"); printf("CMD0 timeout, try again...\n");
#endif #endif
} }
return false; return false;
@@ -44,14 +44,14 @@ static bool sd_check_interface_condition(void)
uint8_t buf[5]; uint8_t buf[5];
if (sd_cmd(8, 0x000001AA, 5, buf)) { if (sd_cmd(8, 0x000001AA, 5, buf)) {
if ((buf[3] & 0xf) != 0x1 || buf[4] != 0xAA) { if ((buf[3] & 0xf) != 0x1 || buf[4] != 0xAA) {
sd_printf("sd_init: check interface condition failed\n"); printf("sd_init: check interface condition failed\n");
return false; return false;
} }
} else { } else {
if (buf[0] & SD_R1_ILLEGAL_COMMAND) { if (buf[0] & SD_R1_ILLEGAL_COMMAND) {
sd_printf("sd_init: check interface condition returned illegal command - old card?\n"); printf("sd_init: check interface condition returned illegal command - old card?\n");
} else { } else {
sd_printf("sd_init: check interface condition failed\n"); printf("sd_init: check interface condition failed\n");
return false; return false;
} }
} }
@@ -72,12 +72,12 @@ static bool sd_send_op_cond(void)
if (!result) { if (!result) {
if (use_acmd && buf & SD_R1_ILLEGAL_COMMAND) { if (use_acmd && buf & SD_R1_ILLEGAL_COMMAND) {
#ifdef SD_DEBUG #ifdef SD_DEBUG
sd_printf("sd_init: card does not understand ACMD41, try CMD1...\n"); printf("sd_init: card does not understand ACMD41, try CMD1...\n");
#endif #endif
use_acmd = false; use_acmd = false;
continue; continue;
} else if (buf != 0x01) { } else if (buf != 0x01) {
sd_printf("sd_init: send_op_cond failed\n"); printf("sd_init: send_op_cond failed\n");
return false; return false;
} else { } else {
continue; continue;
@@ -87,7 +87,7 @@ static bool sd_send_op_cond(void)
return true; return true;
} }
} }
sd_printf("sd_init: send_op_cond: timeout waiting for !idle\n"); printf("sd_init: send_op_cond: timeout waiting for !idle\n");
return false; return false;
} }
@@ -107,7 +107,7 @@ static void sd_dump_cid [[maybe_unused]] (void)
const uint8_t crc = sd_crc7(15, buf); const uint8_t crc = sd_crc7(15, buf);
const uint8_t card_crc = buf[15] >> 1; const uint8_t card_crc = buf[15] >> 1;
if (card_crc != crc) { if (card_crc != crc) {
sd_printf("CRC mismatch: Got %02x, expected %02x\n", card_crc, crc); printf("CRC mismatch: Got %02hhx, expected %02hhx\n", card_crc, crc);
// Some cheap SD cards always report CRC=0, don't fail in that case // Some cheap SD cards always report CRC=0, don't fail in that case
if (card_crc != 0) { if (card_crc != 0) {
return; return;
@@ -122,8 +122,8 @@ static void sd_dump_cid [[maybe_unused]] (void)
uint32_t psn = buf[9] << 24 | buf[10] << 16 | buf[11] << 8 | buf[12]; uint32_t psn = buf[9] << 24 | buf[10] << 16 | buf[11] << 8 | buf[12];
int mdt_year = 2000 + ((buf[13] & 0xf) << 4 | (buf[14] & 0xf0) >> 4); int mdt_year = 2000 + ((buf[13] & 0xf) << 4 | (buf[14] & 0xf0) >> 4);
int mdt_month = buf[14] & 0x0f; int mdt_month = buf[14] & 0x0f;
sd_printf("CID: mid: %02x, oid: %.2s, pnm: %.5s, prv: %02x, psn: %08" PRIx32 ", mdt_year: %d, mdt_month: %d\n", printf("CID: mid: %02hhx, oid: %.2s, pnm: %.5s, prv: %02hhx, psn: %08" PRIx32 ", mdt_year: %d, mdt_month: %d\n",
mid, oid, pnm, prv, psn, mdt_year, mdt_month); mid, oid, pnm, prv, psn, mdt_year, mdt_month);
} }
} }
@@ -131,13 +131,13 @@ static bool sd_read_csd(struct sd_context *sd_context)
{ {
uint8_t buf[16]; uint8_t buf[16];
if (!sd_cmd_read(9, 0, 16, buf)) { if (!sd_cmd_read(9, 0, 16, buf)) {
sd_printf("Failed to read CSD\n"); printf("Failed to read CSD\n");
return false; return false;
} }
const uint8_t crc = sd_crc7(15, buf); const uint8_t crc = sd_crc7(15, buf);
const uint8_t card_crc = buf[15] >> 1; const uint8_t card_crc = buf[15] >> 1;
if (card_crc != crc) { if (card_crc != crc) {
sd_printf("CRC mismatch: Got %02x, expected %02x\n", card_crc, crc); printf("CRC mismatch: Got %02hhx, expected %02hhx\n", card_crc, crc);
// Some cheap SD cards always report CRC=0, don't fail in that case // Some cheap SD cards always report CRC=0, don't fail in that case
if (card_crc != 0) { if (card_crc != 0) {
return false; return false;
@@ -151,12 +151,12 @@ static bool sd_read_csd(struct sd_context *sd_context)
switch (csd_ver) { switch (csd_ver) {
case 0: { case 0: {
if (sd_context->sdhc_sdxc) { if (sd_context->sdhc_sdxc) {
sd_printf("sd_init: Got CSD v1.0 but card is SDHC/SDXC?\n"); printf("sd_init: Got CSD v1.0 but card is SDHC/SDXC?\n");
return false; return false;
} }
const unsigned read_bl_len = buf[5] & 0xf; const unsigned read_bl_len = buf[5] & 0xf;
if (read_bl_len < 9 || read_bl_len > 11) { if (read_bl_len < 9 || read_bl_len > 11) {
sd_printf("Invalid read_bl_len in CSD 1.0\n"); printf("Invalid read_bl_len in CSD 1.0\n");
return false; return false;
} }
blocksize = 1 << (buf[5] & 0xf); blocksize = 1 << (buf[5] & 0xf);
@@ -176,15 +176,15 @@ static bool sd_read_csd(struct sd_context *sd_context)
break; break;
} }
case 2: { case 2: {
sd_printf("sd_init: Got CSD v3.0, but SDUC does not support SPI.\n"); printf("sd_init: Got CSD v3.0, but SDUC does not support SPI.\n");
return false; return false;
} }
} }
sd_context->blocks = blocks; sd_context->blocks = blocks;
sd_context->blocksize = blocksize; sd_context->blocksize = blocksize;
#ifdef SD_DEBUG #ifdef SD_DEBUG
sd_printf("CSD version %u.0, blocksize %u, blocks %u, capacity %u MiB, max speed %u\n", version, blocksize, blocks, printf("CSD version %u.0, blocksize %u, blocks %u, capacity %llu MiB, max speed %u\n", version, blocksize, blocks,
(uint32_t)(((uint64_t)blocksize * blocks) / (1024 * 1024)), max_speed); ((uint64_t)blocksize * blocks) / (1024 * 1024), max_speed);
#endif #endif
return true; return true;
} }
@@ -205,11 +205,11 @@ bool sd_init(struct sd_context *sd_context, int mosi, int miso, int sck, int ss,
uint32_t ocr; uint32_t ocr;
if (!sd_read_ocr(&ocr)) { if (!sd_read_ocr(&ocr)) {
sd_printf("sd_init: read OCR failed\n"); printf("sd_init: read OCR failed\n");
goto out_spi; goto out_spi;
} }
if ((ocr & 0x00380000) != 0x00380000) { if ((ocr & 0x00380000) != 0x00380000) {
sd_printf("sd_init: unsupported card voltage range\n"); printf("sd_init: unsupported card voltage range\n");
goto out_spi; goto out_spi;
} }
@@ -219,11 +219,11 @@ bool sd_init(struct sd_context *sd_context, int mosi, int miso, int sck, int ss,
sd_spi_set_bitrate(rate); sd_spi_set_bitrate(rate);
if (!sd_read_ocr(&ocr)) { if (!sd_read_ocr(&ocr)) {
sd_printf("sd_init: read OCR failed\n"); printf("sd_init: read OCR failed\n");
goto out_spi; goto out_spi;
} }
if (!(ocr & (1 << 31))) { if (!(ocr & (1 << 31))) {
sd_printf("sd_init: card not powered up but !idle?\n"); printf("sd_init: card not powered up but !idle?\n");
goto out_spi; goto out_spi;
} }
sd_context->sdhc_sdxc = (ocr & (1 << 30)); sd_context->sdhc_sdxc = (ocr & (1 << 30));
@@ -234,20 +234,20 @@ bool sd_init(struct sd_context *sd_context, int mosi, int miso, int sck, int ss,
if (sd_context->blocksize != SD_SECTOR_SIZE) { if (sd_context->blocksize != SD_SECTOR_SIZE) {
if (sd_context->blocksize != 1024 && sd_context->blocksize != 2048) { if (sd_context->blocksize != 1024 && sd_context->blocksize != 2048) {
sd_printf("sd_init: Unsupported block size %u\n", sd_context->blocksize); printf("sd_init: Unsupported block size %u\n", sd_context->blocksize);
goto out_spi; goto out_spi;
} }
// Attempt SET_BLOCKLEN command // Attempt SET_BLOCKLEN command
uint8_t resp[1]; uint8_t resp[1];
if (!sd_cmd(16, SD_SECTOR_SIZE, 1, resp)) { if (!sd_cmd(16, SD_SECTOR_SIZE, 1, resp)) {
sd_printf("sd_init: SET_BLOCKLEN failed\n"); printf("sd_init: SET_BLOCKLEN failed\n");
goto out_spi; goto out_spi;
} }
// Successfully set blocksize to SD_SECTOR_SIZE, adjust context // Successfully set blocksize to SD_SECTOR_SIZE, adjust context
sd_context->blocks *= sd_context->blocksize / SD_SECTOR_SIZE; sd_context->blocks *= sd_context->blocksize / SD_SECTOR_SIZE;
#ifdef SD_DEBUG #ifdef SD_DEBUG
sd_printf("Adjusted blocksize from %u to 512, card now has %u blocks\n", sd_context->blocksize, printf("Adjusted blocksize from %u to 512, card now has %u blocks\n", sd_context->blocksize,
sd_context->blocks); sd_context->blocks);
#endif #endif
sd_context->blocksize = SD_SECTOR_SIZE; sd_context->blocksize = SD_SECTOR_SIZE;
} }
@@ -307,7 +307,7 @@ bool sd_readblock_complete(struct sd_context *sd_context)
bool sd_readblock_is_complete(struct sd_context *sd_context) { return sd_cmd_read_is_complete(); } bool sd_readblock_is_complete(struct sd_context *sd_context) { return sd_cmd_read_is_complete(); }
bool sd_writeblock(struct sd_context *sd_context, const size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE]) bool sd_writeblock(struct sd_context *sd_context, size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE])
{ {
if (!sd_context->initialized || sector_num >= sd_context->blocks) if (!sd_context->initialized || sector_num >= sd_context->blocks)
return false; return false;
@@ -319,20 +319,3 @@ bool sd_writeblock(struct sd_context *sd_context, const size_t sector_num, uint8
} }
return sd_cmd_write(24, addr, SD_SECTOR_SIZE, buffer); return sd_cmd_write(24, addr, SD_SECTOR_SIZE, buffer);
} }
bool sd_writeblocks(struct sd_context *sd_context, const size_t sector_num, const size_t sectors, uint8_t *const buffer)
{
if (!sd_context->initialized || sector_num + sectors >= sd_context->blocks)
return false;
if (!sd_context->sdhc_sdxc) {
// Don't use multi-block writes for SDSC for now
// Need to configure WRITE_BL_LEN correctly
for (size_t sector = 0; sector < sectors; ++sector) {
if (!sd_writeblock(sd_context, sector_num + sector, buffer + sector * SD_SECTOR_SIZE))
return false;
}
return true;
}
return sd_cmd_write_multiple(25, sector_num, sectors, SD_SECTOR_SIZE, buffer);
}

View File

@@ -24,4 +24,3 @@ bool sd_readblock_complete(struct sd_context *context);
bool sd_readblock_is_complete(struct sd_context *context); bool sd_readblock_is_complete(struct sd_context *context);
bool sd_writeblock(struct sd_context *context, size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE]); bool sd_writeblock(struct sd_context *context, size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE]);
bool sd_writeblocks(struct sd_context *sd_context, const size_t sector_num, const size_t sectors, uint8_t *buffer);

View File

@@ -12,8 +12,6 @@
#include <pico/time.h> #include <pico/time.h>
#include <string.h> #include <string.h>
extern void sd_printf(const char *fmt, ...);
typedef enum { DMA_READ_TOKEN, DMA_READ, DMA_IDLE } sd_dma_state; typedef enum { DMA_READ_TOKEN, DMA_READ, DMA_IDLE } sd_dma_state;
struct sd_dma_context { struct sd_dma_context {
@@ -224,7 +222,7 @@ bool sd_cmd_read_complete(void)
sd_spi_read_blocking(0xff, &buf, 1); sd_spi_read_blocking(0xff, &buf, 1);
if (sd_spi_context.sd_dma_context.read_token_buf != 0xfe) { if (sd_spi_context.sd_dma_context.read_token_buf != 0xfe) {
#ifdef SD_DEBUG #ifdef SD_DEBUG
sd_printf("read failed: invalid read token %02x\n", sd_spi_context.sd_dma_context.read_token_buf); printf("read failed: invalid read token %02hhx\n", sd_spi_context.sd_dma_context.read_token_buf);
#endif #endif
return false; return false;
} }
@@ -233,7 +231,7 @@ bool sd_cmd_read_complete(void)
const uint16_t act_crc = sd_spi_context.sd_dma_context.crc_buf[0] << 8 | sd_spi_context.sd_dma_context.crc_buf[1]; const uint16_t act_crc = sd_spi_context.sd_dma_context.crc_buf[0] << 8 | sd_spi_context.sd_dma_context.crc_buf[1];
if (act_crc != expect_crc) { if (act_crc != expect_crc) {
#ifdef SD_DEBUG #ifdef SD_DEBUG
sd_printf("read CRC fail: got %04x, expected %04x\n", act_crc, expect_crc); printf("read CRC fail: got %04hx, expected %04hx\n", act_crc, expect_crc);
#endif #endif
return false; return false;
} }
@@ -241,9 +239,10 @@ bool sd_cmd_read_complete(void)
return true; return true;
} }
static bool sd_cmd_write_begin(uint8_t cmd, uint32_t arg) bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen])
{ {
uint8_t buf[1]; uint8_t buf[2];
const uint16_t crc = sd_crc16(datalen, data);
sd_spi_cmd_send(cmd, arg); sd_spi_cmd_send(cmd, arg);
// Read up to 8 garbage bytes (0xff), followed by R1 (MSB is zero) // Read up to 8 garbage bytes (0xff), followed by R1 (MSB is zero)
bool got_r1 = false; bool got_r1 = false;
@@ -254,20 +253,9 @@ static bool sd_cmd_write_begin(uint8_t cmd, uint32_t arg)
break; break;
} }
} }
if (!got_r1 || buf[0] != 0x00) { if (!got_r1 || buf[0] != 0x00)
#ifdef SD_DEBUG goto abort;
sd_printf("write cmd fail: %02x\n", buf[0]); buf[0] = 0xfe;
#endif
return false;
}
return true;
}
static bool sd_cmd_write_block(uint8_t token, unsigned datalen, uint8_t data[const static datalen])
{
uint8_t buf[2];
const uint16_t crc = sd_crc16(datalen, data);
buf[0] = token;
sd_spi_write_blocking(buf, 1); sd_spi_write_blocking(buf, 1);
sd_spi_write_blocking(data, datalen); sd_spi_write_blocking(data, datalen);
buf[0] = crc >> 8; buf[0] = crc >> 8;
@@ -276,16 +264,11 @@ static bool sd_cmd_write_block(uint8_t token, unsigned datalen, uint8_t data[con
sd_spi_read_blocking(0xff, buf, 1); sd_spi_read_blocking(0xff, buf, 1);
if ((buf[0] & 0x1f) != 0x5) { if ((buf[0] & 0x1f) != 0x5) {
#ifdef SD_DEBUG #ifdef SD_DEBUG
sd_printf("Write fail: %2x\n", buf[0]); printf("Write fail: %2hhx\n", buf[0]);
#endif #endif
return false; goto abort;
} }
return true;
}
static bool sd_cmd_write_wait_nbusy(void)
{
uint8_t buf[1];
int timeout = 0; int timeout = 0;
bool got_done = false; bool got_done = false;
for (timeout = 0; timeout < 131072; ++timeout) { for (timeout = 0; timeout < 131072; ++timeout) {
@@ -296,73 +279,18 @@ static bool sd_cmd_write_wait_nbusy(void)
} }
} }
#ifdef SD_DEBUG #ifdef SD_DEBUG
sd_printf("dbg write end: %d, %2x\n", timeout, buf[0]); printf("dbg write end: %d, %2hhx\n", timeout, buf[0]);
#endif #endif
return got_done; if (!got_done)
}
bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen])
{
#ifdef SD_DEBUG
sd_printf("write 1 block at %u\n", arg);
#endif
uint8_t buf[2];
if (!sd_cmd_write_begin(cmd, arg))
goto abort;
if (!sd_cmd_write_block(0xfe, datalen, data))
goto abort;
if (!sd_cmd_write_wait_nbusy())
goto abort; goto abort;
gpio_put(sd_spi_context.ss, true); gpio_put(sd_spi_context.ss, true);
sd_spi_read_blocking(0xff, buf, 1); sd_spi_read_blocking(0xff, buf, 1);
#ifdef SD_DEBUG
sd_printf("write ok\n");
#endif
return true; return true;
abort: abort:
gpio_put(sd_spi_context.ss, true); gpio_put(sd_spi_context.ss, true);
sd_spi_read_blocking(0xff, buf, 1); sd_spi_read_blocking(0xff, buf, 1);
#ifdef SD_DEBUG
sd_printf("write fail\n");
#endif
return false;
}
bool sd_cmd_write_multiple(uint8_t cmd, uint32_t arg, unsigned blocks, unsigned datalen, uint8_t *const data)
{
#ifdef SD_DEBUG
sd_printf("write %u blocks at %u\n", blocks, arg);
#endif
uint8_t buf[2];
if (!sd_cmd_write_begin(cmd, arg))
goto abort;
for (unsigned i = 0; i < blocks; ++i) {
if (!sd_cmd_write_block(0b11111100, datalen, data + datalen * i))
goto abort;
if (!sd_cmd_write_wait_nbusy())
goto abort;
}
buf[0] = 0b11111101;
buf[1] = 0xff;
sd_spi_write_blocking(buf, 2);
if (!sd_cmd_write_wait_nbusy())
goto abort;
gpio_put(sd_spi_context.ss, true);
sd_spi_read_blocking(0xff, buf, 1);
#ifdef SD_DEBUG
sd_printf("write ok\n");
#endif
return true;
abort:
gpio_put(sd_spi_context.ss, true);
sd_spi_read_blocking(0xff, buf, 1);
#ifdef SD_DEBUG
sd_printf("write fail\n");
#endif
return false; return false;
} }

View File

@@ -27,4 +27,3 @@ bool sd_cmd_read_complete(void);
bool sd_cmd_read_is_complete(void); bool sd_cmd_read_is_complete(void);
bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen]); bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen]);
bool sd_cmd_write_multiple(uint8_t cmd, uint32_t arg, unsigned blocks, unsigned datalen, uint8_t *const data);

View File

@@ -9,8 +9,8 @@ from utils import TimerManager
Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb', 'hwconfig', 'leds', Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb', 'hwconfig', 'leds',
'config')) 'config'))
# Should be ~ 3dB steps # Should be ~ 6dB steps
VOLUME_CURVE = [1, 2, 3, 4, 6, 8, 11, 16, 23, 32, 45, 64, 91, 128, 181, 255] VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251]
class PlayerApp: class PlayerApp:
@@ -52,19 +52,11 @@ class PlayerApp:
self.hwconfig = deps.hwconfig(self) self.hwconfig = deps.hwconfig(self)
self.leds = deps.leds(self) self.leds = deps.leds(self)
self.tag_mode = self.config.get_tagmode() self.tag_mode = self.config.get_tagmode()
self.volume_max = self.config.get_volume_max()
self.volume_pos = 3 # fallback if config.get_volume_boot is nonsense
try:
for idx, val in enumerate(VOLUME_CURVE):
if val >= self.config.get_volume_boot():
self.volume_pos = idx
break
except (TypeError, ValueError):
pass
self.playing_tag = None self.playing_tag = None
self.playlist = None self.playlist = None
self.buttons = deps.buttons(self) if deps.buttons is not None else None self.buttons = deps.buttons(self) if deps.buttons is not None else None
self.mp3file = None self.mp3file = None
self.volume_pos = 3
self.paused = False self.paused = False
self.playing = False self.playing = False
self.player.set_volume(VOLUME_CURVE[self.volume_pos]) self.player.set_volume(VOLUME_CURVE[self.volume_pos])
@@ -82,7 +74,7 @@ class PlayerApp:
uid_str = b''.join('{:02x}'.format(x).encode() for x in new_tag) uid_str = b''.join('{:02x}'.format(x).encode() for x in new_tag)
if self.tag_mode == 'tagremains' or (self.tag_mode == 'tagstartstop' and new_tag != self.playing_tag): if self.tag_mode == 'tagremains' or (self.tag_mode == 'tagstartstop' and new_tag != self.playing_tag):
self._set_playlist(uid_str) self._set_playlist(uid_str)
self.playing_tag = new_tag if self.playlist is not None else None self.playing_tag = new_tag
elif self.tag_mode == 'tagstartstop': elif self.tag_mode == 'tagstartstop':
print('Tag presented again, stopping playback') print('Tag presented again, stopping playback')
self._unset_playlist() self._unset_playlist()
@@ -99,10 +91,8 @@ class PlayerApp:
def onButtonPressed(self, what): def onButtonPressed(self, what):
assert self.buttons is not None assert self.buttons is not None
if what == self.buttons.VOLUP: if what == self.buttons.VOLUP:
new_volume = min(self.volume_pos + 1, len(VOLUME_CURVE) - 1) self.volume_pos = min(self.volume_pos + 1, len(VOLUME_CURVE) - 1)
if VOLUME_CURVE[new_volume] <= self.volume_max: self.player.set_volume(VOLUME_CURVE[self.volume_pos])
self.volume_pos = new_volume
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
elif what == self.buttons.VOLDOWN: elif what == self.buttons.VOLDOWN:
self.volume_pos = max(self.volume_pos - 1, 0) self.volume_pos = max(self.volume_pos - 1, 0)
self.player.set_volume(VOLUME_CURVE[self.volume_pos]) self.player.set_volume(VOLUME_CURVE[self.volume_pos])

View File

@@ -3,9 +3,11 @@
import aiorepl # type: ignore import aiorepl # type: ignore
import asyncio import asyncio
from errno import ENOENT
import machine import machine
import micropython import micropython
import network import network
import os
import time import time
import ubinascii import ubinascii
import sys import sys
@@ -36,19 +38,19 @@ hwconfig.board_init()
machine.mem32[0x40030000 + 0x00] = 0x10 machine.mem32[0x40030000 + 0x00] = 0x10
def setup_wifi(ssid='', passphrase='', security=network.WLAN.SEC_WPA_WPA2): def setup_wifi(ssid='', passphrase='', sec=network.WLAN.SEC_WPA2_WPA3):
network.hostname("TonberryPico") network.hostname("TonberryPico")
if ssid is None or ssid == '': if ssid == '':
apname = f"TonberryPicoAP_{machine.unique_id().hex()}" apname = f"TonberryPicoAP_{machine.unique_id().hex()}"
print(f"Create AP {apname}") print(f"Create AP {apname}")
wlan = network.WLAN(network.WLAN.IF_AP) wlan = network.WLAN(network.WLAN.IF_AP)
wlan.config(ssid=apname, password=passphrase if passphrase is not None else '', security=security) wlan.config(ssid=apname, security=wlan.SEC_OPEN)
wlan.active(True) wlan.active(True)
else: else:
print(f"Connect to SSID {ssid} with passphrase {passphrase}...") print(f"Connect to SSID {ssid} with passphrase {passphrase}...")
wlan = network.WLAN() wlan = network.WLAN()
wlan.active(True) wlan.active(True)
wlan.connect(ssid, passphrase if passphrase is not None else '', security=security) wlan.connect(ssid, passphrase, security=sec)
# configure power management # configure power management
wlan.config(pm=network.WLAN.PM_PERFORMANCE) wlan.config(pm=network.WLAN.PM_PERFORMANCE)
@@ -74,8 +76,7 @@ config = Configuration()
# Setup LEDs # Setup LEDs
np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1) np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1)
led_max = config.get_led_max() np.fill((32, 32, 0))
np.fill((led_max, led_max, 0))
np.write() np.write()
@@ -83,26 +84,25 @@ def run():
asyncio.new_event_loop() asyncio.new_event_loop()
if machine.Pin(hwconfig.BUTTONS[1], machine.Pin.IN, machine.Pin.PULL_UP).value() == 0: if machine.Pin(hwconfig.BUTTONS[1], machine.Pin.IN, machine.Pin.PULL_UP).value() == 0:
np.fill((0, 0, led_max)) np.fill((0, 0, 32))
np.write() np.write()
# Force default access point # Force default access point
setup_wifi('', '', network.WLAN.SEC_OPEN) setup_wifi('', '')
else: else:
secstring = config.get_wifi_security() setup_wifi(config.get_wifi_ssid(), config.get_wifi_passphrase())
security = network.WLAN.SEC_WPA_WPA2
if secstring == 'open':
security = network.WLAN.SEC_OPEN
elif secstring == 'wpa_wpa2':
security = network.WLAN.SEC_WPA_WPA2
elif secstring == 'wpa3':
security = network.WLAN.SEC_WPA3
elif secstring == 'wpa2_wpa3':
security = network.WLAN.SEC_WPA2_WPA3
setup_wifi(config.get_wifi_ssid(), config.get_wifi_passphrase(), security)
# Setup MP3 player # Setup MP3 player
with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS, with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS,
baudrate=hwconfig.SD_CLOCKRATE): baudrate=hwconfig.SD_CLOCKRATE):
# Temporary hack: build database from folders if no database exists
# Can be removed once playlists can be created via API
try:
_ = os.stat(DB_PATH)
except OSError as ex:
if ex.errno == ENOENT:
print("No playlist DB found, trying to build DB from tag dirs")
builddb()
with BTreeFileManager(DB_PATH) as playlistdb, \ with BTreeFileManager(DB_PATH) as playlistdb, \
AudioContext(hwconfig.I2S_DIN, hwconfig.I2S_DCLK, hwconfig.I2S_LRCLK) as audioctx: AudioContext(hwconfig.I2S_DIN, hwconfig.I2S_DCLK, hwconfig.I2S_LRCLK) as audioctx:
@@ -116,7 +116,7 @@ def run():
buttons=lambda the_app: Buttons(the_app, config, hwconfig), buttons=lambda the_app: Buttons(the_app, config, hwconfig),
playlistdb=lambda _: playlistdb, playlistdb=lambda _: playlistdb,
hwconfig=lambda _: hwconfig, hwconfig=lambda _: hwconfig,
leds=lambda _: LedManager(np, config), leds=lambda _: LedManager(np),
config=lambda _: config) config=lambda _: config)
the_app = app.PlayerApp(deps) the_app = app.PlayerApp(deps)
@@ -129,11 +129,27 @@ def run():
asyncio.get_event_loop().run_forever() asyncio.get_event_loop().run_forever()
def builddb():
"""
For testing, build a playlist db based on the previous tag directory format.
Can be removed once uploading files / playlist via the web api is possible.
"""
try:
os.unlink(DB_PATH)
except OSError:
pass
with BTreeFileManager(DB_PATH) as db:
for name, type_, _, _ in os.ilistdir(b'/sd'):
if type_ != 0x4000:
continue
fl = [b'/sd/' + name + b'/' + x for x in os.listdir(b'/sd/' + name) if x.endswith(b'.mp3')]
db.createPlaylistForTag(name, fl)
os.sync()
def error_blink(): def error_blink():
while True: while True:
if machine.Pin(hwconfig.BUTTONS[0], machine.Pin.IN, machine.Pin.PULL_UP).value() == 0: np.fill((32, 0, 0))
machine.reset()
np.fill((led_max, 0, 0))
np.write() np.write()
time.sleep_ms(500) time.sleep_ms(500)
np.fill((0, 0, 0)) np.fill((0, 0, 0))
@@ -150,5 +166,5 @@ if __name__ == '__main__':
sys.print_exception(ex) sys.print_exception(ex)
error_blink() error_blink()
else: else:
np.fill((led_max, 0, 0)) np.fill((32, 0, 0))
np.write() np.write()

View File

@@ -26,11 +26,7 @@ class Configuration:
'WIFI': { 'WIFI': {
'SSID': '', 'SSID': '',
'PASSPHRASE': '', 'PASSPHRASE': '',
'SECURITY': 'wpa_wpa2', }
},
'VOLUME_MAX': 255,
'VOLUME_BOOT': 16,
'LED_MAX': 255,
} }
def __init__(self, config_path='/config.json'): def __init__(self, config_path='/config.json'):
@@ -101,18 +97,6 @@ class Configuration:
def get_wifi_passphrase(self) -> str: def get_wifi_passphrase(self) -> str:
return self._get('WIFI')['PASSPHRASE'] return self._get('WIFI')['PASSPHRASE']
def get_wifi_security(self) -> str:
return self._get('WIFI')['SECURITY']
def get_volume_max(self) -> int:
return self._get('VOLUME_MAX')
def get_led_max(self) -> int:
return self._get('LED_MAX')
def get_volume_boot(self) -> int:
return self._get('VOLUME_BOOT')
# For the web API # For the web API
def get_config(self) -> Mapping[str, Any]: def get_config(self) -> Mapping[str, Any]:
return self.config return self.config
@@ -130,9 +114,6 @@ class Configuration:
self._validate(self.DEFAULT_CONFIG, config) self._validate(self.DEFAULT_CONFIG, config)
if 'TAGMODE' in config and config['TAGMODE'] not in ['tagremains', 'tagstartstop']: if 'TAGMODE' in config and config['TAGMODE'] not in ['tagremains', 'tagstartstop']:
raise ValueError("Invalid TAGMODE: Must be 'tagremains' or 'tagstartstop'") raise ValueError("Invalid TAGMODE: Must be 'tagremains' or 'tagstartstop'")
if 'WLAN' in config and 'SECURITY' in config['WLAN'] and \
config['WLAN']['SECURITY'] not in ['open', 'wpa_wpa2', 'wpa3', 'wpa2_wpa3']:
raise ValueError("Invalid WLAN SECURITY: Must be 'open', 'wpa_wpa2', 'wpa3' or 'wpa2_wpa3'")
self._merge_configs(self.config, config) self._merge_configs(self.config, config)
self.config = config self.config = config
self._save() self._save()

View File

@@ -12,10 +12,10 @@ class LedManager:
PLAYING = const(1) PLAYING = const(1)
REBOOTING = const(2) REBOOTING = const(2)
def __init__(self, np, config): def __init__(self, np):
self.led_state = LedManager.IDLE self.led_state = LedManager.IDLE
self.brightness = config.get_led_max() / 255
self.np = np self.np = np
self.brightness = 0.1
self.leds = len(self.np) self.leds = len(self.np)
asyncio.create_task(self.run()) asyncio.create_task(self.run())

View File

@@ -33,13 +33,12 @@ class BTreeDB(IPlaylistDB):
PERSIST_OFFSET = b'offset' PERSIST_OFFSET = b'offset'
class Playlist(IPlaylist): class Playlist(IPlaylist):
def __init__(self, parent: "BTreeDB", tag: bytes, pos: int, persist, shuffle, name): def __init__(self, parent: "BTreeDB", tag: bytes, pos: int, persist, shuffle):
self.parent = parent self.parent = parent
self.tag = tag self.tag = tag
self.pos = pos self.pos = pos
self.persist = persist self.persist = persist
self.shuffle = shuffle self.shuffle = shuffle
self.name = name
self.length = self.parent._getPlaylistLength(self.tag) self.length = self.parent._getPlaylistLength(self.tag)
self._shuffle() self._shuffle()
@@ -169,10 +168,6 @@ class BTreeDB(IPlaylistDB):
return (b''.join([tag, b'/playlist/']), return (b''.join([tag, b'/playlist/']),
b''.join([tag, b'/playlist0'])) b''.join([tag, b'/playlist0']))
@staticmethod
def _keyPlaylistName(tag):
return b''.join([tag, b'/playlistname'])
def _flush(self): def _flush(self):
""" """
Flush the database and call the flush_func if it was provided. Flush the database and call the flush_func if it was provided.
@@ -227,13 +222,12 @@ class BTreeDB(IPlaylistDB):
raise RuntimeError("Malformed playlist key") raise RuntimeError("Malformed playlist key")
return int(elements[2])+1 return int(elements[2])+1
def _savePlaylist(self, tag, entries, persist, shuffle, name, flush=True): def _savePlaylist(self, tag, entries, persist, shuffle, flush=True):
self._deletePlaylist(tag, False) self._deletePlaylist(tag, False)
for idx, entry in enumerate(entries): for idx, entry in enumerate(entries):
self.db[self._keyPlaylistEntry(tag, idx)] = entry self.db[self._keyPlaylistEntry(tag, idx)] = entry
self.db[self._keyPlaylistPersist(tag)] = persist self.db[self._keyPlaylistPersist(tag)] = persist
self.db[self._keyPlaylistShuffle(tag)] = shuffle self.db[self._keyPlaylistShuffle(tag)] = shuffle
self.db[self._keyPlaylistName(tag)] = name.encode()
if flush: if flush:
self._flush() self._flush()
@@ -246,7 +240,7 @@ class BTreeDB(IPlaylistDB):
pass pass
for k in (self._keyPlaylistPos(tag), self._keyPlaylistPosOffset(tag), for k in (self._keyPlaylistPos(tag), self._keyPlaylistPosOffset(tag),
self._keyPlaylistPersist(tag), self._keyPlaylistShuffle(tag), self._keyPlaylistPersist(tag), self._keyPlaylistShuffle(tag),
self._keyPlaylistShuffleSeed(tag), self._keyPlaylistName(tag)): self._keyPlaylistShuffleSeed(tag)):
try: try:
del self.db[k] del self.db[k]
except KeyError: except KeyError:
@@ -254,18 +248,15 @@ class BTreeDB(IPlaylistDB):
if flush: if flush:
self._flush() self._flush()
def getPlaylists(self): def getPlaylistTags(self):
""" """
Get a list of all defined playlists with their tag and names. Get a keys-only dict of all defined playlists. Playlists currently do not have names, but are identified by
their tag.
""" """
playlist_tags = set() playlist_tags = set()
for item in self.db: for item in self.db:
playlist_tags.add(item.split(b'/')[0]) playlist_tags.add(item.split(b'/')[0])
playlists = [] return playlist_tags
for tag in playlist_tags:
name = self.db.get(self._keyPlaylistName(tag), b'').decode()
playlists.append({'tag': tag, 'name': name})
return playlists
def getPlaylistForTag(self, tag: bytes): def getPlaylistForTag(self, tag: bytes):
""" """
@@ -284,19 +275,18 @@ class BTreeDB(IPlaylistDB):
return None return None
if self._keyPlaylistEntry(tag, pos) not in self.db: if self._keyPlaylistEntry(tag, pos) not in self.db:
pos = 0 pos = 0
name = self.db.get(self._keyPlaylistName(tag), b'').decode()
shuffle = self.db.get(self._keyPlaylistShuffle(tag), self.SHUFFLE_NO) shuffle = self.db.get(self._keyPlaylistShuffle(tag), self.SHUFFLE_NO)
return self.Playlist(self, tag, pos, persist, shuffle, name) return self.Playlist(self, tag, pos, persist, shuffle)
def createPlaylistForTag(self, tag: bytes, entries: typing.Iterable[bytes], persist=PERSIST_TRACK, def createPlaylistForTag(self, tag: bytes, entries: typing.Iterable[bytes], persist=PERSIST_TRACK,
shuffle=SHUFFLE_NO, name: str = ''): shuffle=SHUFFLE_NO):
""" """
Create and save a playlist for 'tag' and return the Playlist object. If a playlist already existed for 'tag' it Create and save a playlist for 'tag' and return the Playlist object. If a playlist already existed for 'tag' it
is overwritten. is overwritten.
""" """
assert persist in (self.PERSIST_NO, self.PERSIST_TRACK, self.PERSIST_OFFSET) assert persist in (self.PERSIST_NO, self.PERSIST_TRACK, self.PERSIST_OFFSET)
assert shuffle in (self.SHUFFLE_NO, self.SHUFFLE_YES) assert shuffle in (self.SHUFFLE_NO, self.SHUFFLE_YES)
self._savePlaylist(tag, entries, persist, shuffle, name) self._savePlaylist(tag, entries, persist, shuffle)
return self.getPlaylistForTag(tag) return self.getPlaylistForTag(tag)
def deletePlaylistForTag(self, tag: bytes): def deletePlaylistForTag(self, tag: bytes):
@@ -380,14 +370,6 @@ class BTreeDB(IPlaylistDB):
_ = int(val) _ = int(val)
except ValueError: except ValueError:
fail(f' Bad playlistposoffset value for {last_tag}: {val!r}') fail(f' Bad playlistposoffset value for {last_tag}: {val!r}')
elif fields[1] == b'playlistname':
val = self.db[k]
try:
name = val.decode()
if dump:
print(f'\tName: {name}')
except UnicodeError:
fail(f' Bad playlistname for {last_tag}: Not valid unicode')
else: else:
fail(f'Unknown key {k!r}') fail(f'Unknown key {k!r}')
return result return result

View File

@@ -5,7 +5,6 @@ Copyright (c) 2024-2025 Stefan Kratochwil <Kratochwil-LA@gmx.de>
import asyncio import asyncio
import board import board
import errno
import hwconfig import hwconfig
import json import json
import machine import machine
@@ -113,7 +112,7 @@ async def static(request, path):
@webapp.route('/api/v1/playlists', methods=['GET']) @webapp.route('/api/v1/playlists', methods=['GET'])
async def playlists_get(request): async def playlists_get(request):
return playlist_db.getPlaylists() return sorted(playlist_db.getPlaylistTags())
def is_hex(s): def is_hex(s):
@@ -134,11 +133,10 @@ async def playlist_get(request, tag):
return None, 404 return None, 404
return { return {
'shuffle': playlist.shuffle, 'shuffle': playlist.__dict__.get('shuffle'),
'persist': playlist.persist, 'persist': playlist.__dict__.get('persist'),
'paths': [(p[len(fsroot):] if p.startswith(fsroot) else p).decode() 'paths': [(p[len(fsroot):] if p.startswith(fsroot) else p).decode()
for p in playlist.getPaths()], for p in playlist.getPaths()],
'name': playlist.name
} }
@@ -158,8 +156,7 @@ async def playlist_put(request, tag):
playlist_db.createPlaylistForTag(tag.encode(), playlist_db.createPlaylistForTag(tag.encode(),
(fsroot + path.encode() for path in playlist.get('paths', [])), (fsroot + path.encode() for path in playlist.get('paths', [])),
playlist.get('persist', 'track').encode(), playlist.get('persist', 'track').encode(),
playlist.get('shuffle', 'no').encode(), playlist.get('shuffle', 'no').encode())
playlist.get('name', ''))
return '', 204 return '', 204
@@ -203,25 +200,6 @@ async def audiofiles_get(request):
return directory_iterator(), {'Content-Type': 'application/json; charset=UTF-8'} return directory_iterator(), {'Content-Type': 'application/json; charset=UTF-8'}
async def stream_to_file(stream, file_, length):
data = array('b', range(4096))
bytes_copied = 0
while True:
bytes_read = await stream.readinto(data)
if bytes_read == 0:
# End of body
break
bytes_written = file_.write(data[:bytes_read])
if bytes_written != bytes_read:
# short writes shouldn't happen
raise OSError(errno.EIO, 'unexpected short write')
bytes_copied += bytes_written
if bytes_copied == length:
break
app.reset_idle_timeout()
return bytes_copied
@webapp.route('/api/v1/audiofiles', methods=['POST']) @webapp.route('/api/v1/audiofiles', methods=['POST'])
async def audiofile_upload(request): async def audiofile_upload(request):
if 'type' not in request.args or request.args['type'] not in ['file', 'directory']: if 'type' not in request.args or request.args['type'] not in ['file', 'directory']:
@@ -238,13 +216,26 @@ async def audiofile_upload(request):
os.mkdir(path) os.mkdir(path)
return '', 204 return '', 204
with open(path, 'wb') as newfile: with open(path, 'wb') as newfile:
try: data = array('b', range(4096))
if length > Request.max_body_length: bytes_copied = 0
bytes_copied = await stream_to_file(request.stream, newfile, length) while True:
else: try:
bytes_copied = newfile.write(request.body) bytes_read = await request.stream.readinto(data)
except OSError as ex: except OSError as ex:
return f'error writing data to file: {ex}', 500 return f'read error: {ex}', 500
if bytes_read == 0:
# End of body
break
try:
bytes_written = newfile.write(data[:bytes_read])
except OSError as ex:
return f'write error: {ex}', 500
if bytes_written != bytes_read:
# short writes shouldn't happen
return 'write failure', 500
bytes_copied += bytes_written
if bytes_copied == length:
break
if bytes_copied == length: if bytes_copied == length:
return '', 204 return '', 204
else: else:
@@ -276,9 +267,10 @@ async def audiofile_delete(request):
@webapp.route('/api/v1/reboot/<method>', methods=['POST']) @webapp.route('/api/v1/reboot/<method>', methods=['POST'])
async def reboot(request, method): async def reboot(request, method):
if hwconfig.get_on_battery():
return 'not allowed: usb not connected', 403
if method == 'bootloader': if method == 'bootloader':
if hwconfig.get_on_battery():
return 'not possible: connect USB first', 403
leds.set_state(LedManager.REBOOTING) leds.set_state(LedManager.REBOOTING)
timer_manager.schedule(time.ticks_ms() + 1500, machine.bootloader) timer_manager.schedule(time.ticks_ms() + 1500, machine.bootloader)
elif method == 'application': elif method == 'application':

View File

@@ -139,12 +139,6 @@ class FakeConfig:
def get_tagmode(self): def get_tagmode(self):
return 'tagremains' return 'tagremains'
def get_volume_max(self):
return 255
def get_volume_boot(self):
return 16
def fake_open(filename, mode): def fake_open(filename, mode):
return FakeFile(filename, mode) return FakeFile(filename, mode)
@@ -173,7 +167,7 @@ def test_construct_app(micropythonify, faketimermanager):
deps = _makedeps(mp3player=fake_mp3) deps = _makedeps(mp3player=fake_mp3)
dut = app.PlayerApp(deps) dut = app.PlayerApp(deps)
fake_mp3 = dut.player fake_mp3 = dut.player
assert fake_mp3.volume is not None and fake_mp3.volume >= 16 assert fake_mp3.volume is not None
def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch): def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch):

View File

@@ -15,8 +15,6 @@ option(ENABLE_PLAY_TEST "Enable mp3 playback test" OFF)
option(ENABLE_SD_READ_CRC "Enable crc check when reading from sd card" OFF) option(ENABLE_SD_READ_CRC "Enable crc check when reading from sd card" OFF)
option(ENABLE_SD_DEBUG "Enable debug output for sd card driver" OFF) option(ENABLE_SD_DEBUG "Enable debug output for sd card driver" OFF)
set(PICO_USE_FASTEST_SUPPORTED_CLOCK 1)
# initialize the Raspberry Pi Pico SDK # initialize the Raspberry Pi Pico SDK
pico_sdk_init() pico_sdk_init()
@@ -64,7 +62,7 @@ add_subdirectory(../../lib/helix_mp3 helix_mp3)
target_link_libraries(standalone_mp3 PRIVATE pico_stdlib hardware_dma hardware_spi hardware_sync hardware_pio helix_mp3) target_link_libraries(standalone_mp3 PRIVATE pico_stdlib hardware_dma hardware_spi hardware_sync hardware_pio helix_mp3)
target_include_directories(standalone_mp3 PRIVATE ${SD_LIB_DIR}) target_include_directories(standalone_mp3 PRIVATE ${SD_LIB_DIR})
target_compile_options(standalone_mp3 PRIVATE -Og) target_compile_options(standalone_mp3 PRIVATE -Og -DSD_DEBUG)
pico_add_extra_outputs(standalone_mp3) pico_add_extra_outputs(standalone_mp3)

View File

@@ -2,7 +2,6 @@
#include "sd.h" #include "sd.h"
#include <math.h> #include <math.h>
#include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@@ -21,14 +20,6 @@ extern void sd_spi_dbg_clk(const int div, const int frac);
extern void sd_spi_dbg_loop(void); extern void sd_spi_dbg_loop(void);
void sd_printf(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vprintf(fmt, ap);
va_end(ap);
}
#define MAX_VOLUME 0x8000u #define MAX_VOLUME 0x8000u
void __time_critical_func(volume_adjust)(int16_t *restrict buf, size_t samples, uint16_t scalef) void __time_critical_func(volume_adjust)(int16_t *restrict buf, size_t samples, uint16_t scalef)
@@ -166,7 +157,7 @@ static void write_test(struct sd_context *sd_context)
data_buffer[i] ^= 0xff; data_buffer[i] ^= 0xff;
} }
if (!sd_writeblocks(sd_context, 0, sizeof(data_buffer) / SD_SECTOR_SIZE, data_buffer)) { if (!sd_writeblock(sd_context, 0, data_buffer)) {
printf("sd_writeblock failed\n"); printf("sd_writeblock failed\n");
return; return;
} }