3 Commits

Author SHA1 Message Date
32bf6a8d68 always return content type application/json
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m36s
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 10s
2025-12-16 23:13:04 +01:00
e2f9287ebd feat: last connected tag uid available at /api/v1/last_tag_uid
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m39s
Check code formatting / Check-C-Format (push) Successful in 8s
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 9s
Run pytests / Check-Pytest (push) Successful in 11s
2025-12-16 22:41:55 +01:00
19dff763bd feat: webserver: keep alive; move playback write prot to handler
- Reset the app idle timer when interacting with the webapp, so that the
  device does not turn off while the web ui is used.

- Handle denying put/post while playback is active centrally in the
  before_request handler, so that it does not need to be copy/pasted
  into every request handler.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-16 22:34:06 +01:00
5 changed files with 54 additions and 23 deletions

View File

@@ -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.config.get_tagmode()
self.tag_mode = self.playlist_db.getSetting('tagmode')
self.playing_tag = None
self.playlist = None
self.buttons = deps.buttons(self) if deps.buttons is not None else None
@@ -116,6 +116,10 @@ class PlayerApp:
# Check again in a minute
self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout)
def reset_idle_timeout(self):
if not self.playing:
self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout)
def is_playing(self) -> bool:
return self.playing
@@ -186,3 +190,6 @@ class PlayerApp:
self.timer_manager.cancel(self.onIdleTimeout)
self.leds.set_state(self.leds.PLAYING)
self.playing = True
def get_nfc(self):
return self.nfc

View File

@@ -21,8 +21,7 @@ class Configuration:
'VOLDOWN': 2,
'PREV': None,
'NEXT': 1,
},
'TAGMODE': 'tagremains'
}
}
def __init__(self, config_path='/config.json'):
@@ -73,9 +72,6 @@ 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
@@ -91,7 +87,5 @@ 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()

View File

@@ -31,6 +31,9 @@ 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):
@@ -279,6 +282,11 @@ 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.
@@ -298,7 +306,8 @@ class BTreeDB(IPlaylistDB):
fail(f'Malformed key {k!r}')
continue
if fields[0] == b'settings':
# Legacy, not used any more
val = self.db[k].decode()
print(f'Setting {fields[1].decode()} = {val}')
continue
if last_tag != fields[0]:
last_tag = fields[0]

View File

@@ -11,13 +11,22 @@ webapp = Microdot()
server = None
config = None
app = None
nfc = None
def start_webserver(config_, app_):
global server, config, app
global server, config, app, nfc
server = asyncio.create_task(webapp.start_server(port=80))
config = config_
app = app_
nfc = app.get_nfc()
@webapp.before_request
async def before_request_handler(request):
if request.method in ['PUT', 'POST'] and app.is_playing():
return "Cannot write to device while playback is active", 503
app.reset_idle_timeout()
@webapp.route('/')
@@ -52,10 +61,14 @@ async def config_get(request):
@webapp.route('/api/v1/config', methods=['PUT'])
async def config_put(request):
if app.is_playing():
return 503
try:
config.set_config(request.json)
except ValueError as ex:
return str(ex), 400
return '', 204
@webapp.route('/api/v1/last_tag_uid', methods=['GET'])
async def last_tag_uid_get(request):
tag, _ = nfc.get_last_uid()
return {'tag': tag}

View File

@@ -136,9 +136,6 @@ class FakeConfig:
def get_tag_timeout(self):
return 5
def get_tagmode(self):
return 'tagremains'
def fake_open(filename, mode):
return FakeFile(filename, mode)
@@ -229,16 +226,18 @@ def test_playlist_unknown_tag(micropythonify, faketimermanager, monkeypatch):
def test_tagmode_startstop(micropythonify, faketimermanager, monkeypatch):
class FakeStartStopConfig(FakeConfig):
def __init__(self):
super().__init__()
class MyFakePlaylistDb(FakePlaylistDb):
def __init__(self, tracklist=[b'test/path.mp3']):
super().__init__(tracklist)
def get_tagmode(self):
return 'tagstartstop'
def getSetting(self, key: bytes | str):
if key == 'tagmode':
return 'tagstartstop'
return None
fake_db = FakePlaylistDb([b'test/path.mp3'])
fake_db = MyFakePlaylistDb()
fake_mp3 = FakeMp3Player()
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db, config=FakeStartStopConfig)
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
app.PlayerApp(deps)
with monkeypatch.context() as m:
m.setattr(builtins, 'open', fake_open)
@@ -265,7 +264,16 @@ def test_tagmode_startstop(micropythonify, faketimermanager, monkeypatch):
def test_tagmode_remains(micropythonify, faketimermanager, monkeypatch):
fake_db = FakePlaylistDb([b'test/path.mp3'])
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_mp3 = FakeMp3Player()
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
app.PlayerApp(deps)