feat(app): Implement tag handling modes according to #28.
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m24s
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 6s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m24s
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 6s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
If the tagmode setting is 'tagremains' (the default): Play as long as the tag is on the reader, with a 5 second timeout (i.e. playback stops when tag is gone for more than 5 seconds). If the tagmode setting is 'tagstartstop': Playback starts when a tag is seen. Presenting a different tag causes the playback to stop and playback for the new tag to start. Re-presenting the same tag used to start with a no-tag-time of 5 seconds or more in between stops playback. Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
This commit is contained in:
@@ -44,6 +44,9 @@ class PlayerApp:
|
||||
self.player = deps.mp3player(self)
|
||||
self.nfc = deps.nfcreader(self.tag_state_machine)
|
||||
self.playlist_db = deps.playlistdb(self)
|
||||
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
|
||||
self.mp3file = None
|
||||
self.volume_pos = 3
|
||||
@@ -59,17 +62,21 @@ class PlayerApp:
|
||||
Callback (typically called by TagStateMachine) to signal that a new tag has been presented.
|
||||
"""
|
||||
uid_str = b''.join('{:02x}'.format(x).encode() for x in new_tag)
|
||||
self._set_playlist(uid_str)
|
||||
if self.tag_mode == 'tagremains' or (self.tag_mode == 'tagstartstop' and new_tag != self.playing_tag):
|
||||
self._set_playlist(uid_str)
|
||||
self.playing_tag = new_tag
|
||||
elif self.tag_mode == 'tagstartstop':
|
||||
print('Tag presented again, stopping playback')
|
||||
self._unset_playlist()
|
||||
self.playing_tag = None
|
||||
|
||||
def onTagRemoved(self):
|
||||
"""
|
||||
Callback (typically called by TagStateMachine) to signal that a tag has been removed.
|
||||
"""
|
||||
print('Tag gone, stopping playback')
|
||||
if self.playlist is not None:
|
||||
pos = self.player.stop()
|
||||
if pos is not None:
|
||||
self.playlist.setPlaybackOffset(pos)
|
||||
if self.tag_mode == 'tagremains':
|
||||
print('Tag gone, stopping playback')
|
||||
self._unset_playlist()
|
||||
|
||||
def onButtonPressed(self, what):
|
||||
assert self.buttons is not None
|
||||
@@ -89,10 +96,18 @@ class PlayerApp:
|
||||
self._play_next()
|
||||
|
||||
def _set_playlist(self, tag: bytes):
|
||||
self._unset_playlist()
|
||||
self.playlist = self.playlist_db.getPlaylistForTag(tag)
|
||||
self._play(self.playlist.getCurrentPath() if self.playlist is not None else None,
|
||||
self.playlist.getPlaybackOffset() if self.playlist is not None else 0)
|
||||
|
||||
def _unset_playlist(self):
|
||||
if self.playlist is not None:
|
||||
pos = self.player.stop()
|
||||
if pos is not None:
|
||||
self.playlist.setPlaybackOffset(pos)
|
||||
self.playlist = None
|
||||
|
||||
def _play_next(self):
|
||||
if self.playlist is None:
|
||||
return
|
||||
@@ -100,6 +115,7 @@ class PlayerApp:
|
||||
self._play(filename)
|
||||
if filename is None:
|
||||
self.playlist = None
|
||||
self.playing_tag = None
|
||||
|
||||
def _play(self, filename: bytes | None, offset=0):
|
||||
if self.mp3file is not None:
|
||||
|
||||
@@ -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):
|
||||
@@ -269,6 +272,11 @@ class BTreeDB(IPlaylistDB):
|
||||
self._savePlaylist(tag, entries, persist, shuffle)
|
||||
return self.getPlaylistForTag(tag)
|
||||
|
||||
def getSetting(self, key: bytes | str) -> str:
|
||||
if 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.
|
||||
@@ -286,6 +294,11 @@ class BTreeDB(IPlaylistDB):
|
||||
fields = k.split(b'/')
|
||||
if len(fields) <= 1:
|
||||
fail(f'Malformed key {k!r}')
|
||||
continue
|
||||
if fields[0] == b'settings':
|
||||
val = self.db[k].decode()
|
||||
print(f'Setting {fields[1].decode()} = {val}')
|
||||
continue
|
||||
if last_tag != fields[0]:
|
||||
last_tag = fields[0]
|
||||
last_pos = None
|
||||
|
||||
@@ -38,11 +38,17 @@ class FakeMp3Player:
|
||||
def play(self, track: FakeFile, offset: int):
|
||||
self.track = track
|
||||
|
||||
def stop(self):
|
||||
self.track = None
|
||||
|
||||
|
||||
class FakeTimerManager:
|
||||
def __init__(self): pass
|
||||
def cancel(self, timer): pass
|
||||
|
||||
def schedule(self, when, what):
|
||||
what()
|
||||
|
||||
|
||||
class FakeNfcReader:
|
||||
tag_callback = None
|
||||
@@ -79,6 +85,11 @@ class FakePlaylistDb:
|
||||
def getPlaylistForTag(self, tag: bytes):
|
||||
return self.FakePlaylist(self)
|
||||
|
||||
def getSetting(self, key: bytes | str):
|
||||
if key == 'tagmode':
|
||||
return 'tagremains'
|
||||
return None
|
||||
|
||||
|
||||
def fake_open(filename, mode):
|
||||
return FakeFile(filename, mode)
|
||||
@@ -106,7 +117,7 @@ def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch):
|
||||
nfcreader=lambda x: FakeNfcReader(x),
|
||||
buttons=lambda _: FakeButtons(),
|
||||
playlistdb=lambda _: fake_db)
|
||||
dut = app.PlayerApp(deps)
|
||||
app.PlayerApp(deps)
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(builtins, 'open', fake_open)
|
||||
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||
@@ -150,14 +161,88 @@ def test_playlist_unknown_tag(micropythonify, faketimermanager, monkeypatch):
|
||||
def getPlaylistForTag(self, tag):
|
||||
return None
|
||||
|
||||
def getSetting(self, key: bytes | str):
|
||||
return None
|
||||
|
||||
fake_db = FakeNoPlaylistDb()
|
||||
fake_mp3 = FakeMp3Player()
|
||||
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
|
||||
nfcreader=lambda x: FakeNfcReader(x),
|
||||
buttons=lambda _: FakeButtons(),
|
||||
playlistdb=lambda _: fake_db)
|
||||
dut = app.PlayerApp(deps)
|
||||
app.PlayerApp(deps)
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(builtins, 'open', fake_open)
|
||||
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||
assert fake_mp3.track is None
|
||||
|
||||
|
||||
def test_tagmode_startstop(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 'tagstartstop'
|
||||
return None
|
||||
|
||||
fake_db = MyFakePlaylistDb()
|
||||
fake_mp3 = FakeMp3Player()
|
||||
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
|
||||
nfcreader=lambda x: FakeNfcReader(x),
|
||||
buttons=lambda _: FakeButtons(),
|
||||
playlistdb=lambda _: fake_db)
|
||||
app.PlayerApp(deps)
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(builtins, 'open', fake_open)
|
||||
# Present tag to start playback
|
||||
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||
assert fake_mp3.track is not None
|
||||
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||
# Removing tag should not stop playback
|
||||
FakeNfcReader.tag_callback.onTagChange(None)
|
||||
assert fake_mp3.track is not None
|
||||
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||
# Presenting tag should stop playback
|
||||
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||
assert fake_mp3.track is None
|
||||
# Nothing should change here
|
||||
FakeNfcReader.tag_callback.onTagChange(None)
|
||||
assert fake_mp3.track is None
|
||||
# Presenting tag again should start playback again
|
||||
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||
assert fake_mp3.track is not None
|
||||
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||
|
||||
|
||||
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_mp3 = FakeMp3Player()
|
||||
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
|
||||
nfcreader=lambda x: FakeNfcReader(x),
|
||||
buttons=lambda _: FakeButtons(),
|
||||
playlistdb=lambda _: fake_db)
|
||||
app.PlayerApp(deps)
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(builtins, 'open', fake_open)
|
||||
# Present tag to start playback
|
||||
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||
assert fake_mp3.track is not None
|
||||
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||
# Remove tag to stop playback
|
||||
FakeNfcReader.tag_callback.onTagChange(None)
|
||||
assert fake_mp3.track is None
|
||||
# Presenting tag again should start playback again
|
||||
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||
assert fake_mp3.track is not None
|
||||
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||
|
||||
Reference in New Issue
Block a user