diff --git a/software/src/app.py b/software/src/app.py index a8f046d..95007b7 100644 --- a/software/src/app.py +++ b/software/src/app.py @@ -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: diff --git a/software/src/utils/playlistdb.py b/software/src/utils/playlistdb.py index 483cc83..3f7914a 100644 --- a/software/src/utils/playlistdb.py +++ b/software/src/utils/playlistdb.py @@ -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 diff --git a/software/tests/test_playerapp.py b/software/tests/test_playerapp.py index b71590a..4809fb3 100644 --- a/software/tests/test_playerapp.py +++ b/software/tests/test_playerapp.py @@ -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) @@ -150,6 +161,9 @@ 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, @@ -161,3 +175,74 @@ def test_playlist_unknown_tag(micropythonify, faketimermanager, monkeypatch): 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) + dut = 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) + dut = 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'