Compare commits
9 Commits
3e888790e4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 49197c8ca4 | |||
| e447902001 | |||
| 768b630722 | |||
| e0ff9c54bc | |||
| c0b9ef2961 | |||
| e23f8bd34c | |||
| 97e9742c75 | |||
| c687e3a977 | |||
| aa1a02ce54 |
@@ -17,6 +17,7 @@ set -eu
|
||||
)
|
||||
|
||||
BUILDDIR=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/
|
||||
BUILDDIR_UNIX=lib/micropython/ports/unix/build-tonberry_unix/
|
||||
OUTDIR=$(pwd)/build
|
||||
mkdir -p "$OUTDIR"
|
||||
FS_STAGE_DIR=$(mktemp -d)
|
||||
@@ -51,5 +52,9 @@ for hwconfig in boards/RPI_PICO_W/manifest-*.py; do
|
||||
$PICOTOOL uf2 convert $BUILDDIR/firmware-filesystem.bin "$OUTDIR"/firmware-filesystem-"$hwname".uf2
|
||||
done
|
||||
|
||||
cp "$BUILDDIR_UNIX"/micropython "$OUTDIR"/micropython-tonberry_unix
|
||||
chmod u+x "$OUTDIR"/micropython-tonberry_unix
|
||||
|
||||
echo "Output in" "${OUTDIR}"/firmware-*.uf2
|
||||
echo "Images with filesystem in" "${OUTDIR}"/firmware-filesystem-*.uf2
|
||||
echo "Unix build in" "${OUTDIR}"/micropython-tonberry_unix
|
||||
|
||||
@@ -51,7 +51,7 @@ class PlayerApp:
|
||||
self.playlist_db = deps.playlistdb(self)
|
||||
self.hwconfig = deps.hwconfig(self)
|
||||
self.leds = deps.leds(self)
|
||||
self.tag_mode = self.playlist_db.getSetting('tagmode')
|
||||
self.tag_mode = self.config.get_tagmode()
|
||||
self.playing_tag = None
|
||||
self.playlist = None
|
||||
self.buttons = deps.buttons(self) if deps.buttons is not None else None
|
||||
|
||||
@@ -53,6 +53,13 @@ def setup_wifi():
|
||||
print(f"ifconfig: {wlan.ifconfig()}")
|
||||
|
||||
|
||||
async def wdt_task(wdt):
|
||||
# TODO: more checking of app health
|
||||
# Right now this only protects against the asyncio executor crashing completely
|
||||
while True:
|
||||
await asyncio.sleep_ms(500)
|
||||
wdt.feed()
|
||||
|
||||
DB_PATH = '/sd/tonberry.db'
|
||||
|
||||
config = Configuration()
|
||||
@@ -97,8 +104,10 @@ def run():
|
||||
|
||||
start_webserver(config, the_app)
|
||||
# Start
|
||||
wdt = machine.WDT(timeout=1000)
|
||||
asyncio.create_task(aiorepl.task({'timer_manager': TimerManager(),
|
||||
'app': the_app}))
|
||||
asyncio.create_task(wdt_task(wdt))
|
||||
asyncio.get_event_loop().run_forever()
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from utils import safe_callback
|
||||
|
||||
from mfrc522 import MFRC522
|
||||
try:
|
||||
@@ -74,7 +75,7 @@ class Nfc:
|
||||
self.last_uid = uid
|
||||
self.last_uid_timestamp = time.ticks_us()
|
||||
if self.cb is not None and last_callback_uid != uid:
|
||||
self.cb.onTagChange(uid)
|
||||
safe_callback(lambda: self.cb.onTagChange(uid), "nfc tag change")
|
||||
last_callback_uid = uid
|
||||
|
||||
await asyncio.sleep_ms(poll_interval_ms)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
from utils.helpers import safe_callback
|
||||
from utils.buttons import Buttons
|
||||
from utils.config import Configuration
|
||||
from utils.leds import LedManager
|
||||
@@ -11,4 +12,4 @@ from utils.sdcontext import SDContext
|
||||
from utils.timer import TimerManager
|
||||
|
||||
__all__ = ["BTreeDB", "BTreeFileManager", "Buttons", "Configuration", "get_pin_index", "LedManager", "MBRPartition",
|
||||
"SDContext", "TimerManager"]
|
||||
"safe_callback", "SDContext", "TimerManager"]
|
||||
|
||||
@@ -5,6 +5,7 @@ import asyncio
|
||||
import machine
|
||||
import micropython
|
||||
import time
|
||||
from utils import safe_callback
|
||||
try:
|
||||
from typing import TYPE_CHECKING # type: ignore
|
||||
except ImportError:
|
||||
@@ -74,4 +75,4 @@ class Buttons:
|
||||
await self.int_flag.wait()
|
||||
while len(self.pressed) > 0:
|
||||
what = self.pressed.pop()
|
||||
self.cb.onButtonPressed(what)
|
||||
safe_callback(lambda: self.cb.onButtonPressed(what), "button callback")
|
||||
|
||||
@@ -21,7 +21,8 @@ class Configuration:
|
||||
'VOLDOWN': 2,
|
||||
'PREV': None,
|
||||
'NEXT': 1,
|
||||
}
|
||||
},
|
||||
'TAGMODE': 'tagremains'
|
||||
}
|
||||
|
||||
def __init__(self, config_path='/config.json'):
|
||||
@@ -72,6 +73,9 @@ class Configuration:
|
||||
def get_button_map(self) -> Mapping[str, int | None]:
|
||||
return self._get('BUTTON_MAP')
|
||||
|
||||
def get_tagmode(self) -> str:
|
||||
return self._get('TAGMODE')
|
||||
|
||||
# For the web API
|
||||
def get_config(self) -> Mapping[str, Any]:
|
||||
return self.config
|
||||
@@ -87,5 +91,7 @@ class Configuration:
|
||||
|
||||
def set_config(self, config):
|
||||
self._validate(self.DEFAULT_CONFIG, config)
|
||||
if 'TAGMODE' in config and config['TAGMODE'] not in ['tagremains', 'tagstartstop']:
|
||||
raise ValueError("Invalid TAGMODE: Must be 'tagremains' or 'tagstartstop'")
|
||||
self.config = config
|
||||
self._save()
|
||||
|
||||
12
software/src/utils/helpers.py
Normal file
12
software/src/utils/helpers.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def safe_callback(func, name="callback"):
|
||||
try:
|
||||
func()
|
||||
except Exception as ex:
|
||||
print(f"Uncaught exception in {name}")
|
||||
sys.print_exception(ex)
|
||||
@@ -31,9 +31,6 @@ class BTreeDB(IPlaylistDB):
|
||||
PERSIST_NO = b'no'
|
||||
PERSIST_TRACK = b'track'
|
||||
PERSIST_OFFSET = b'offset'
|
||||
DEFAULT_SETTINGS = {
|
||||
b'tagmode': b'tagremains'
|
||||
}
|
||||
|
||||
class Playlist(IPlaylist):
|
||||
def __init__(self, parent: "BTreeDB", tag: bytes, pos: int, persist, shuffle):
|
||||
@@ -282,11 +279,6 @@ class BTreeDB(IPlaylistDB):
|
||||
self._savePlaylist(tag, entries, persist, shuffle)
|
||||
return self.getPlaylistForTag(tag)
|
||||
|
||||
def getSetting(self, key: bytes | str) -> str:
|
||||
if type(key) is str:
|
||||
key = key.encode()
|
||||
return self.db.get(b'settings/' + key, self.DEFAULT_SETTINGS[key]).decode()
|
||||
|
||||
def validate(self, dump=False):
|
||||
"""
|
||||
Validate the structure of the playlist database.
|
||||
@@ -306,8 +298,7 @@ class BTreeDB(IPlaylistDB):
|
||||
fail(f'Malformed key {k!r}')
|
||||
continue
|
||||
if fields[0] == b'settings':
|
||||
val = self.db[k].decode()
|
||||
print(f'Setting {fields[1].decode()} = {val}')
|
||||
# Legacy, not used any more
|
||||
continue
|
||||
if last_tag != fields[0]:
|
||||
last_tag = fields[0]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import asyncio
|
||||
import heapq
|
||||
import time
|
||||
from utils import safe_callback
|
||||
|
||||
TIMER_DEBUG = True
|
||||
|
||||
@@ -49,28 +50,36 @@ class TimerManager(object):
|
||||
heapq.heapify(self.timers)
|
||||
return i
|
||||
|
||||
def _next_timeout(self):
|
||||
if len(self.timers) == 0:
|
||||
if self.timer_debug:
|
||||
print("timer: worker: queue empty")
|
||||
return None
|
||||
cur_nearest = self.timers[0][0]
|
||||
next_timeout = cur_nearest - time.ticks_ms()
|
||||
if self.timer_debug:
|
||||
if next_timeout > 0:
|
||||
print(f"timer: worker: next is {self.timers[0]}, sleep {next_timeout} ms")
|
||||
else:
|
||||
print(f"timer: worker: {self.timers[0]} elapsed @{cur_nearest}, delay {-next_timeout} ms")
|
||||
return next_timeout
|
||||
|
||||
async def _wait(self, timeout):
|
||||
try:
|
||||
await asyncio.wait_for_ms(self.worker_event.wait(), timeout)
|
||||
if self.timer_debug:
|
||||
print("timer: worker: event")
|
||||
# got woken up due to event
|
||||
self.worker_event.clear()
|
||||
return True
|
||||
except asyncio.TimeoutError:
|
||||
return False
|
||||
|
||||
async def _timer_worker(self):
|
||||
while True:
|
||||
if len(self.timers) == 0:
|
||||
# Nothing to do
|
||||
await self.worker_event.wait()
|
||||
if self.timer_debug:
|
||||
print("_timer_worker: event 0")
|
||||
self.worker_event.clear()
|
||||
continue
|
||||
cur_nearest = self.timers[0][0]
|
||||
wait_time = cur_nearest - time.ticks_ms()
|
||||
if wait_time > 0:
|
||||
if self.timer_debug:
|
||||
print(f"_timer_worker: next is {self.timers[0]}, sleep {wait_time} ms")
|
||||
try:
|
||||
await asyncio.wait_for_ms(self.worker_event.wait(), wait_time)
|
||||
if self.timer_debug:
|
||||
print("_timer_worker: event 1")
|
||||
# got woken up due to event
|
||||
self.worker_event.clear()
|
||||
continue
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
_, callback = heapq.heappop(self.timers)
|
||||
callback()
|
||||
next_timeout = self._next_timeout()
|
||||
if next_timeout is None or next_timeout > 0:
|
||||
await self._wait(next_timeout)
|
||||
else:
|
||||
_, callback = heapq.heappop(self.timers)
|
||||
safe_callback(callback, "timer callback")
|
||||
|
||||
@@ -136,6 +136,9 @@ class FakeConfig:
|
||||
def get_tag_timeout(self):
|
||||
return 5
|
||||
|
||||
def get_tagmode(self):
|
||||
return 'tagremains'
|
||||
|
||||
|
||||
def fake_open(filename, mode):
|
||||
return FakeFile(filename, mode)
|
||||
@@ -226,18 +229,16 @@ def test_playlist_unknown_tag(micropythonify, faketimermanager, monkeypatch):
|
||||
|
||||
|
||||
def test_tagmode_startstop(micropythonify, faketimermanager, monkeypatch):
|
||||
class MyFakePlaylistDb(FakePlaylistDb):
|
||||
def __init__(self, tracklist=[b'test/path.mp3']):
|
||||
super().__init__(tracklist)
|
||||
class FakeStartStopConfig(FakeConfig):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def getSetting(self, key: bytes | str):
|
||||
if key == 'tagmode':
|
||||
return 'tagstartstop'
|
||||
return None
|
||||
def get_tagmode(self):
|
||||
return 'tagstartstop'
|
||||
|
||||
fake_db = MyFakePlaylistDb()
|
||||
fake_db = FakePlaylistDb([b'test/path.mp3'])
|
||||
fake_mp3 = FakeMp3Player()
|
||||
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
||||
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db, config=FakeStartStopConfig)
|
||||
app.PlayerApp(deps)
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(builtins, 'open', fake_open)
|
||||
@@ -264,16 +265,7 @@ def test_tagmode_startstop(micropythonify, faketimermanager, monkeypatch):
|
||||
|
||||
|
||||
def test_tagmode_remains(micropythonify, faketimermanager, monkeypatch):
|
||||
class MyFakePlaylistDb(FakePlaylistDb):
|
||||
def __init__(self, tracklist=[b'test/path.mp3']):
|
||||
super().__init__(tracklist)
|
||||
|
||||
def getSetting(self, key: bytes | str):
|
||||
if key == 'tagmode':
|
||||
return 'tagremains'
|
||||
return None
|
||||
|
||||
fake_db = MyFakePlaylistDb()
|
||||
fake_db = FakePlaylistDb([b'test/path.mp3'])
|
||||
fake_mp3 = FakeMp3Player()
|
||||
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
||||
app.PlayerApp(deps)
|
||||
|
||||
Reference in New Issue
Block a user