1 Commits

Author SHA1 Message Date
93e9aea368 wip: async flush
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m43s
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 8s
Run pytests / Check-Pytest (push) Successful in 11s
2025-12-27 15:46:07 +01:00
14 changed files with 106 additions and 262 deletions

View File

@@ -1,29 +0,0 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
#include "py/obj.h"
#include "py/runtime.h"
#include "py/objstr.h"
#ifndef TONBERRY_GIT_REVISION
#define TONBERRY_GIT_REVISION "unknown"
#endif
#ifndef TONBERRY_VERSION
#define TONBERRY_VERSION "unknown"
#endif
static const MP_DEFINE_STR_OBJ(tonberry_git_revision_obj, TONBERRY_GIT_REVISION);
static const MP_DEFINE_STR_OBJ(tonberry_version_obj, TONBERRY_VERSION);
static const mp_rom_map_elem_t board_module_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_board)},
{MP_ROM_QSTR(MP_QSTR_revision), MP_ROM_PTR(&tonberry_git_revision_obj)},
{MP_ROM_QSTR(MP_QSTR_version), MP_ROM_PTR(&tonberry_version_obj)},
};
static MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table);
const mp_obj_module_t board_cmodule = {
.base = {&mp_type_module},
.globals = (mp_obj_dict_t *)&board_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_board, board_cmodule);

View File

@@ -20,22 +20,3 @@ set(GEN_PINS_CSV_ARG --board-csv "${GEN_PINS_BOARD_CSV}")
add_link_options("-Wl,--print-memory-usage")
set(PICO_USE_FASTEST_SUPPORTED_CLOCK 1)
find_program(GIT git)
execute_process(COMMAND ${GIT} rev-parse HEAD
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
OUTPUT_VARIABLE TONBERRY_GIT_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
execute_process(COMMAND ${GIT} describe --match 'v*' --always --dirty
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
OUTPUT_VARIABLE TONBERRY_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(MICROPY_SOURCE_BOARD "${CMAKE_CURRENT_LIST_DIR}/board.c")
set(MICROPY_DEF_BOARD
TONBERRY_GIT_REVISION="${TONBERRY_GIT_REVISION}"
TONBERRY_VERSION="${TONBERRY_VERSION}")

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>TonBERRY pico</title>
<title>Device Admin</title>
<style>
body {
font-family: Arial, sans-serif;
@@ -131,17 +131,11 @@
list-style: none;
}
.footer {
font-size: x-small;
color: gray;
margin-top: 20px;
}
</style>
</head>
<body>
<h1>TonBERRY pico</h1>
<h1>Device Admin</h1>
<nav>
<button onclick="showScreen('menu')">🏠 Main Menu</button>
@@ -167,7 +161,7 @@
<div id="screen-config" class="screen">
<h2>Configuration Editor</h2>
<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>
<!-- PLAYLIST EDITOR SCREEN 1: list of playlists -->
@@ -213,10 +207,6 @@
<option value="audioplay">Audioplay (no shuffle, start at previous track)</option>
</select>
</div>
<div>
<label>Playlist name</label>
<input type="text" placeholder="Playlist name" id="playlist-edit-name" />
</div>
<div class="flex-horizontal">
<div style="flex-grow: 1">
<label>Tracks</label>
@@ -267,17 +257,7 @@
</div>
</div>
</div>
<div class="footer">
<hr>
<div class="flex-horizontal">
<div>
<a href="https://git.ka.blankertz.org/TonBERRY/tonberry-pico">TonBERRY pico</a>
</div>
<div>
Version: <span id="footer-version">unknown</span>
</div>
</div>
</div>
<script>
const Screens = {};
let activeScreen = null;
@@ -316,9 +296,7 @@
return;
}
alert("Configuration saved successfully, device will now reboot/shutdown! " +
"On battery, press Power button after shutdown to restart.");
await fetch('/api/v1/reboot/application', {'method': 'POST'});
alert("Configuration saved successfully!");
} catch (err) {
alert("Error saving configuration: " + err);
}
@@ -356,8 +334,7 @@
'root.BUTTON_MAP.PLAY_PAUSE': 'Play/Pause',
'root.TAG_TIMEOUT_SECS': 'Tag removal timeout (seconds)',
'root.TAGMODE': 'Tag mode',
'root.LED_COUNT': 'Length of WS2182 (Neopixel) LED chain',
'root.VOLUME_MAX': 'Maximum volume (0-255)'
'root.LED_COUNT': 'Length of WS2182 (Neopixel) LED chain'
};
const config_input_override = {
'root.TAGMODE': {
@@ -367,21 +344,6 @@
'tagstartstop': 'Present tag once to start, present again to stop playback'
}
},
'root.BUTTON_MAP.NEXT': {
'input-type': 'number'
},
'root.BUTTON_MAP.PREV': {
'input-type': 'number'
},
'root.BUTTON_MAP.VOLUP': {
'input-type': 'number'
},
'root.BUTTON_MAP.VOLDOWN': {
'input-type': 'number'
},
'root.BUTTON_MAP.PLAY_PAUSE': {
'input-type': 'number'
},
'root.IDLE_TIMEOUT_SECS': {
'input-type': 'number'
},
@@ -391,15 +353,6 @@
'root.LED_COUNT': {
'input-type': 'number'
},
'root.WLAN.SSID': {
'input-type': 'text'
},
'root.WLAN.PASSPHRASE': {
'input-type': 'text'
},
'root.VOLUME_MAX': {
'input-type': 'number'
}
};
function renderObject(obj, path) {
@@ -470,7 +423,7 @@
let val = input.value.trim();
if (val === "") val = null;
else if (!isNaN(val) && input.type != 'text') val = Number(val);
else if (!isNaN(val)) val = Number(val);
current[path[path.length - 1]] = val;
});
@@ -503,7 +456,7 @@
document.getElementById('playlist-exist-button-delete')
.addEventListener('click', (e) => {
if (lastSelected === null) return;
const tagid = lastSelected.getAttribute('data-tag');
const tagid = lastSelected.innerText;
if(confirm(`Really delete playlist ${tagid}?`)) {
fetch(`/api/v1/playlist/${tagid}`, {
method: 'DELETE'})
@@ -514,7 +467,7 @@
.addEventListener('click', selectLastTag);
document.getElementById('playlist-exist-button-edit').addEventListener("click", (e) => {
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) => {
const node = e.target.closest("li");
@@ -569,9 +522,8 @@
container.innerHTML = "";
for (const playlist of playlists) {
const li = document.createElement("li");
li.innerHTML = `${playlist.name} (${playlist.tag})`;
li.innerHTML = playlist;
li.className = "node"
li.setAttribute('data-tag', playlist.tag);
container.appendChild(li)
}
}
@@ -596,7 +548,7 @@
document.getElementById('playlist-exist-list')
.querySelectorAll("li")
.forEach(n => {
if (n.getAttribute('data-tag') == tagtext) {
if (n.innerText == tagtext) {
n.classList.add("selected");
lastSelected = n;
} else {
@@ -698,8 +650,6 @@
} else if (playlist.persist === "track" && playlist.shuffle === "no") {
playlisttype.value = "audioplay";
}
const playlistname = document.getElementById('playlist-edit-name');
playlistname.value = playlist.name;
}
async function save() {
@@ -722,8 +672,6 @@
playlistData.shuffle = "no";
break;
}
const playlistname = document.getElementById('playlist-edit-name');
playlistData.name = playlistname.value;
const container = document.getElementById('playlist-edit-list');
playlistData.paths = [...container.querySelectorAll("li")].map((node) => node.innerText);
const saveRes = await fetch(`/api/v1/playlist/${playlistId}`,
@@ -1033,13 +981,6 @@
});
showScreen("menu");
fetch('/api/v1/info')
.then((resp) => resp.json())
.then((info) => {
const version = document.getElementById('footer-version');
version.innerText = info.version;
});
</script>
</body>

View File

@@ -2,7 +2,8 @@
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
import _audiocore
from asyncio import ThreadSafeFlag
import asyncio
from asyncio import Lock, ThreadSafeFlag
from utils import get_pin_index
@@ -11,19 +12,34 @@ class Audiocore:
# PIO requires sideset pins to be adjacent
assert get_pin_index(lrclk) == get_pin_index(dclk)+1 or get_pin_index(lrclk) == get_pin_index(dclk)-1
self.notify = ThreadSafeFlag()
self.audiocore_lock = Lock()
self._audiocore = _audiocore.Audiocore(din, dclk, lrclk, self._interrupt)
def deinit(self):
assert not self.audiocore_lock.locked()
self._audiocore.deinit()
def _interrupt(self, _):
self.notify.set()
def flush(self):
assert not self.audiocore_lock.locked()
self._audiocore.flush()
async def async_flush(self):
async with self.audiocore_lock:
self._audiocore.flush(False)
while True:
if self._audiocore.get_async_result() is not None:
return
await self.notify.wait()
async def async_set_volume(self, volume):
async with self.audiocore_lock:
self._audiocore.set_volume(volume)
def set_volume(self, volume):
self._audiocore.set_volume(volume)
asyncio.create_task(self.async_set_volume(volume))
def put(self, buffer, blocking=False):
pos = 0

View File

@@ -5,6 +5,8 @@
// Include MicroPython API.
#include "py/mperrno.h"
#include "py/obj.h"
#include "py/persistentcode.h"
#include "py/runtime.h"
#include "shared/runtime/mpirq.h"
@@ -61,6 +63,20 @@ static uint32_t get_fifo_read_value_blocking(struct audiocore_obj *obj)
}
}
static bool get_fifo_read_value(struct audiocore_obj *obj, uint32_t *result)
{
const long flags = save_and_disable_interrupts();
const uint32_t value = obj->fifo_read_value;
obj->fifo_read_value = 0;
if (value & AUDIOCORE_FIFO_DATA_FLAG) {
restore_interrupts(flags);
*result = value & ~AUDIOCORE_FIFO_DATA_FLAG;
return true;
}
restore_interrupts(flags);
return false;
}
/*
* audiocore.Context.deinit(self)
*
@@ -125,15 +141,31 @@ static MP_DEFINE_CONST_FUN_OBJ_2(audiocore_put_obj, audiocore_put);
* Tells the audiocore to stop playback as soon as all MP3 frames still in the buffer have been decoded.
* This function blocks until playback has ended.
*/
static mp_obj_t audiocore_flush(mp_obj_t self_in)
static mp_obj_t audiocore_flush(size_t n_args, const mp_obj_t *args)
{
struct audiocore_obj *self = MP_OBJ_TO_PTR(self_in);
struct audiocore_obj *self = MP_OBJ_TO_PTR(args[0]);
mp_obj_t blocking = MP_ROM_TRUE;
if (n_args == 2) {
blocking = args[1];
}
multicore_fifo_push_blocking(AUDIOCORE_CMD_FLUSH);
wake_core1();
get_fifo_read_value_blocking(self);
if (mp_obj_is_true(blocking))
get_fifo_read_value_blocking(self);
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_1(audiocore_flush_obj, audiocore_flush);
static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(audiocore_flush_obj, 1, 2, audiocore_flush);
static mp_obj_t audiocore_get_async_result(mp_obj_t self_in)
{
uint32_t result;
struct audiocore_obj *self = MP_OBJ_TO_PTR(self_in);
if (get_fifo_read_value(self, &result)) {
return mp_obj_new_int(result);
}
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_1(audiocore_get_async_result_obj, audiocore_get_async_result);
/*
* audiocore.set_volume(self, volume)
@@ -240,6 +272,7 @@ static const mp_rom_map_elem_t audiocore_locals_dict_table[] = {
{MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiocore_deinit_obj)},
{MP_ROM_QSTR(MP_QSTR_put), MP_ROM_PTR(&audiocore_put_obj)},
{MP_ROM_QSTR(MP_QSTR_flush), MP_ROM_PTR(&audiocore_flush_obj)},
{MP_ROM_QSTR(MP_QSTR_get_async_result), MP_ROM_PTR(&audiocore_get_async_result_obj)},
{MP_ROM_QSTR(MP_QSTR_set_volume), MP_ROM_PTR(&audiocore_set_volume_obj)},
};
static MP_DEFINE_CONST_DICT(audiocore_locals_dict, audiocore_locals_dict_table);

View File

@@ -9,8 +9,8 @@ from utils import TimerManager
Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb', 'hwconfig', 'leds',
'config'))
# Should be ~ 3dB steps
VOLUME_CURVE = [1, 2, 3, 4, 6, 8, 11, 16, 23, 32, 45, 64, 91, 128, 181, 255]
# Should be ~ 6dB steps
VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251]
class PlayerApp:
@@ -52,7 +52,6 @@ class PlayerApp:
self.hwconfig = deps.hwconfig(self)
self.leds = deps.leds(self)
self.tag_mode = self.config.get_tagmode()
self.volume_max = self.config.get_volume_max()
self.playing_tag = None
self.playlist = None
self.buttons = deps.buttons(self) if deps.buttons is not None else None
@@ -92,10 +91,8 @@ class PlayerApp:
def onButtonPressed(self, what):
assert self.buttons is not None
if what == self.buttons.VOLUP:
new_volume = min(self.volume_pos + 1, len(VOLUME_CURVE) - 1)
if VOLUME_CURVE[new_volume] <= self.volume_max:
self.volume_pos = new_volume
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
self.volume_pos = min(self.volume_pos + 1, len(VOLUME_CURVE) - 1)
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
elif what == self.buttons.VOLDOWN:
self.volume_pos = max(self.volume_pos - 1, 0)
self.player.set_volume(VOLUME_CURVE[self.volume_pos])

View File

@@ -10,7 +10,6 @@ import network
import os
import time
import ubinascii
import sys
# Own modules
import app
@@ -38,22 +37,14 @@ hwconfig.board_init()
machine.mem32[0x40030000 + 0x00] = 0x10
def setup_wifi(ssid='', passphrase='', sec=network.WLAN.SEC_WPA2_WPA3):
def setup_wifi():
network.hostname("TonberryPico")
if ssid == '':
apname = f"TonberryPicoAP_{machine.unique_id().hex()}"
print(f"Create AP {apname}")
wlan = network.WLAN(network.WLAN.IF_AP)
wlan.config(ssid=apname, security=wlan.SEC_OPEN)
wlan.active(True)
else:
print(f"Connect to SSID {ssid} with passphrase {passphrase}...")
wlan = network.WLAN()
wlan.active(True)
wlan.connect(ssid, passphrase, security=sec)
wlan = network.WLAN(network.WLAN.IF_AP)
wlan.config(ssid=f"TonberryPicoAP_{machine.unique_id().hex()}", security=wlan.SEC_OPEN)
wlan.active(True)
# configure power management
wlan.config(pm=network.WLAN.PM_PERFORMANCE)
# disable power management
wlan.config(pm=network.WLAN.PM_NONE)
mac = ubinascii.hexlify(network.WLAN().config('mac'), ':').decode()
print(f" mac: {mac}")
@@ -74,22 +65,14 @@ DB_PATH = '/sd/tonberry.db'
config = Configuration()
# Setup LEDs
np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1)
np.fill((32, 32, 0))
np.write()
def run():
asyncio.new_event_loop()
# Setup LEDs
np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1)
if machine.Pin(hwconfig.BUTTONS[1], machine.Pin.IN, machine.Pin.PULL_UP).value() == 0:
np.fill((0, 0, 32))
np.write()
# Force default access point
setup_wifi('', '')
else:
setup_wifi(config.get_wifi_ssid(), config.get_wifi_passphrase())
# Wifi with default config
setup_wifi()
# Setup MP3 player
with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS,
@@ -147,24 +130,7 @@ def builddb():
os.sync()
def error_blink():
while True:
np.fill((32, 0, 0))
np.write()
time.sleep_ms(500)
np.fill((0, 0, 0))
np.write()
time.sleep_ms(500)
if __name__ == '__main__':
time.sleep(1)
if machine.Pin(hwconfig.BUTTONS[0], machine.Pin.IN, machine.Pin.PULL_UP).value() != 0:
try:
run()
except Exception as ex:
sys.print_exception(ex)
error_blink()
else:
np.fill((32, 0, 0))
np.write()
run()

View File

@@ -74,7 +74,7 @@ class MP3Player:
# Call onPlaybackDone after flush
send_done = True
finally:
self.audiocore.flush()
await self.audiocore.async_flush()
if send_done:
# Only call onPlaybackDone if exit due to end of stream
# Use timer with time 0 to call callback "immediately" but from a different task

View File

@@ -2,7 +2,6 @@
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
from utils.helpers import safe_callback
from utils.timer import TimerManager
from utils.buttons import Buttons
from utils.config import Configuration
from utils.leds import LedManager
@@ -10,6 +9,7 @@ from utils.mbrpartition import MBRPartition
from utils.pinindex import get_pin_index
from utils.playlistdb import BTreeDB, BTreeFileManager
from utils.sdcontext import SDContext
from utils.timer import TimerManager
__all__ = ["BTreeDB", "BTreeFileManager", "Buttons", "Configuration", "get_pin_index", "LedManager", "MBRPartition",
"safe_callback", "SDContext", "TimerManager"]

View File

@@ -5,7 +5,7 @@ import asyncio
import machine
import micropython
import time
from utils import safe_callback, TimerManager
from utils import safe_callback
try:
from typing import TYPE_CHECKING # type: ignore
except ImportError:
@@ -32,7 +32,6 @@ class Buttons:
def __init__(self, cb: "ButtonCallback", config, hwconfig):
self.button_map = config.get_button_map()
self.hw_buttons = hwconfig.BUTTONS
self.hwconfig = hwconfig
self.cb = cb
self.buttons = dict()
for key_id, key_name in self.KEYMAP.items():
@@ -43,7 +42,6 @@ class Buttons:
self.int_flag = asyncio.ThreadSafeFlag()
self.pressed: list[int] = []
self.last: dict[int, int] = {}
self.timer_manager = TimerManager()
for button in self.buttons.keys():
button.irq(handler=self._interrupt, trigger=machine.Pin.IRQ_FALLING | machine.Pin.IRQ_RISING)
asyncio.create_task(self.task())
@@ -71,10 +69,6 @@ class Buttons:
# print(f'B{keycode} {now}')
self.pressed.append(keycode)
self.int_flag.set()
if keycode == self.VOLDOWN:
self.timer_manager.schedule(time.ticks_ms() + 5000, self.long_press_shutdown)
if button.value() == 1 and keycode == self.VOLDOWN:
self.timer_manager.cancel(self.long_press_shutdown)
async def task(self):
while True:
@@ -82,9 +76,3 @@ class Buttons:
while len(self.pressed) > 0:
what = self.pressed.pop()
safe_callback(lambda: self.cb.onButtonPressed(what), "button callback")
def long_press_shutdown(self):
if self.hwconfig.get_on_battery():
self.hwconfig.power_off()
else:
machine.reset()

View File

@@ -22,12 +22,7 @@ class Configuration:
'PREV': None,
'NEXT': 1,
},
'TAGMODE': 'tagremains',
'WIFI': {
'SSID': '',
'PASSPHRASE': '',
},
'VOLUME_MAX': 255
'TAGMODE': 'tagremains'
}
def __init__(self, config_path='/config.json'):
@@ -92,15 +87,6 @@ class Configuration:
def get_tagmode(self) -> str:
return self._get('TAGMODE')
def get_wifi_ssid(self) -> str:
return self._get('WIFI')['SSID']
def get_wifi_passphrase(self) -> str:
return self._get('WIFI')['PASSPHRASE']
def get_volume_max(self) -> int:
return self._get('VOLUME_MAX')
# For the web API
def get_config(self) -> Mapping[str, Any]:
return self.config

View File

@@ -33,13 +33,12 @@ class BTreeDB(IPlaylistDB):
PERSIST_OFFSET = b'offset'
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.tag = tag
self.pos = pos
self.persist = persist
self.shuffle = shuffle
self.name = name
self.length = self.parent._getPlaylistLength(self.tag)
self._shuffle()
@@ -169,10 +168,6 @@ class BTreeDB(IPlaylistDB):
return (b''.join([tag, b'/playlist/']),
b''.join([tag, b'/playlist0']))
@staticmethod
def _keyPlaylistName(tag):
return b''.join([tag, b'/playlistname'])
def _flush(self):
"""
Flush the database and call the flush_func if it was provided.
@@ -227,13 +222,12 @@ class BTreeDB(IPlaylistDB):
raise RuntimeError("Malformed playlist key")
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)
for idx, entry in enumerate(entries):
self.db[self._keyPlaylistEntry(tag, idx)] = entry
self.db[self._keyPlaylistPersist(tag)] = persist
self.db[self._keyPlaylistShuffle(tag)] = shuffle
self.db[self._keyPlaylistName(tag)] = name.encode()
if flush:
self._flush()
@@ -246,7 +240,7 @@ class BTreeDB(IPlaylistDB):
pass
for k in (self._keyPlaylistPos(tag), self._keyPlaylistPosOffset(tag),
self._keyPlaylistPersist(tag), self._keyPlaylistShuffle(tag),
self._keyPlaylistShuffleSeed(tag), self._keyPlaylistName(tag)):
self._keyPlaylistShuffleSeed(tag)):
try:
del self.db[k]
except KeyError:
@@ -254,18 +248,15 @@ class BTreeDB(IPlaylistDB):
if 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()
for item in self.db:
playlist_tags.add(item.split(b'/')[0])
playlists = []
for tag in playlist_tags:
name = self.db.get(self._keyPlaylistName(tag), b'').decode()
playlists.append({'tag': tag, 'name': name})
return playlists
return playlist_tags
def getPlaylistForTag(self, tag: bytes):
"""
@@ -284,19 +275,18 @@ class BTreeDB(IPlaylistDB):
return None
if self._keyPlaylistEntry(tag, pos) not in self.db:
pos = 0
name = self.db.get(self._keyPlaylistName(tag), b'').decode()
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,
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
is overwritten.
"""
assert persist in (self.PERSIST_NO, self.PERSIST_TRACK, self.PERSIST_OFFSET)
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)
def deletePlaylistForTag(self, tag: bytes):
@@ -380,14 +370,6 @@ class BTreeDB(IPlaylistDB):
_ = int(val)
except ValueError:
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:
fail(f'Unknown key {k!r}')
return result

View File

@@ -4,14 +4,11 @@ Copyright (c) 2024-2025 Stefan Kratochwil <Kratochwil-LA@gmx.de>
'''
import asyncio
import board
import hwconfig
import json
import machine
import network
import os
import time
import ubinascii
from array import array
from microdot import Microdot, redirect, send_file, Request
@@ -31,7 +28,7 @@ Request.max_content_length = 128 * 1024 * 1024 # 128MB requests allowed
def start_webserver(config_, app_):
global server, config, app, nfc, playlist_db, leds, timer_manager
server = asyncio.create_task(webapp.start_server(host='::0', port=80))
server = asyncio.create_task(webapp.start_server(port=80))
config = config_
app = app_
nfc = app.get_nfc()
@@ -112,7 +109,7 @@ async def static(request, path):
@webapp.route('/api/v1/playlists', methods=['GET'])
async def playlists_get(request):
return playlist_db.getPlaylists()
return sorted(playlist_db.getPlaylistTags())
def is_hex(s):
@@ -133,11 +130,10 @@ async def playlist_get(request, tag):
return None, 404
return {
'shuffle': playlist.shuffle,
'persist': playlist.persist,
'shuffle': playlist.__dict__.get('shuffle'),
'persist': playlist.__dict__.get('persist'),
'paths': [(p[len(fsroot):] if p.startswith(fsroot) else p).decode()
for p in playlist.getPaths()],
'name': playlist.name
}
@@ -157,8 +153,7 @@ async def playlist_put(request, tag):
playlist_db.createPlaylistForTag(tag.encode(),
(fsroot + path.encode() for path in playlist.get('paths', [])),
playlist.get('persist', 'track').encode(),
playlist.get('shuffle', 'no').encode(),
playlist.get('name', ''))
playlist.get('shuffle', 'no').encode())
return '', 204
@@ -221,17 +216,11 @@ async def audiofile_upload(request):
data = array('b', range(4096))
bytes_copied = 0
while True:
try:
bytes_read = await request.stream.readinto(data)
except OSError as ex:
return f'read error: {ex}', 500
bytes_read = await request.stream.readinto(data)
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
bytes_written = newfile.write(data[:bytes_read])
if bytes_written != bytes_read:
# short writes shouldn't happen
return 'write failure', 500
@@ -269,9 +258,10 @@ async def audiofile_delete(request):
@webapp.route('/api/v1/reboot/<method>', methods=['POST'])
async def reboot(request, method):
if hwconfig.get_on_battery():
return 'not allowed: usb not connected', 403
if method == 'bootloader':
if hwconfig.get_on_battery():
return 'not possible: connect USB first', 403
leds.set_state(LedManager.REBOOTING)
timer_manager.schedule(time.ticks_ms() + 1500, machine.bootloader)
elif method == 'application':
@@ -280,10 +270,3 @@ async def reboot(request, method):
else:
return 'method not supported', 400
return '', 204
@webapp.route('/api/v1/info', methods=['GET'])
async def get_info(request):
mac = ubinascii.hexlify(network.WLAN().config('mac'), ':').decode()
return {'version': board.version,
'mac': mac}