From b5e3df105433513840d445da38cb10456bdc2a7c Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Fri, 31 Oct 2025 16:59:33 +0100 Subject: [PATCH] refactor(app): Refactor tag handling Split the tag handling in PlayerApp into two parts: The low level handling of changed/removed tags from the Nfc reader, including the timeout processing is moved into the nested TagStateMachine class. The main PlayerApp has two new (internal) callbacks "onNewTag" and "onTagRemoved" which are called when a new tag is presented or a tag was actually removed. This will hopefully make the implementation of #28 "Configurable Tag Handling" cleaner. Signed-off-by: Matthias Blankertz --- software/src/app.py | 66 ++++++++++++++++++++------------ software/tests/test_playerapp.py | 17 ++++---- 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/software/src/app.py b/software/src/app.py index 3b42619..a8f046d 100644 --- a/software/src/app.py +++ b/software/src/app.py @@ -13,12 +13,36 @@ VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251] class PlayerApp: + class TagStateMachine: + def __init__(self, parent, timer_manager): + self.parent = parent + self.timer_manager = timer_manager + self.current_tag = None + self.current_tag_time = time.ticks_ms() + + def onTagChange(self, new_tag): + if new_tag is not None: + self.timer_manager.cancel(self.onTagRemoveDelay) + if new_tag == self.current_tag: + return + # Change playlist on new tag + if new_tag is not None: + self.current_tag_time = time.ticks_ms() + self.current_tag = new_tag + self.parent.onNewTag(new_tag) + else: + self.timer_manager.schedule(time.ticks_ms() + 5000, self.onTagRemoveDelay) + + def onTagRemoveDelay(self): + if self.current_tag is not None: + self.current_tag = None + self.parent.onTagRemoved() + def __init__(self, deps: Dependencies): - self.current_tag = None - self.current_tag_time = time.ticks_ms() self.timer_manager = TimerManager() + self.tag_state_machine = self.TagStateMachine(self, self.timer_manager) self.player = deps.mp3player(self) - self.nfc = deps.nfcreader(self) + self.nfc = deps.nfcreader(self.tag_state_machine) self.playlist_db = deps.playlistdb(self) self.buttons = deps.buttons(self) if deps.buttons is not None else None self.mp3file = None @@ -30,28 +54,22 @@ class PlayerApp: self.mp3file.close() self.mp3file = None - def onTagChange(self, new_tag): - if new_tag is not None: - self.timer_manager.cancel(self.onTagRemoveDelay) - if new_tag == self.current_tag: - return - # Change playlist on new tag - if new_tag is not None: - self.current_tag_time = time.ticks_ms() - self.current_tag = new_tag - uid_str = b''.join('{:02x}'.format(x).encode() for x in new_tag) - self._set_playlist(uid_str) - else: - self.timer_manager.schedule(time.ticks_ms() + 5000, self.onTagRemoveDelay) + def onNewTag(self, new_tag): + """ + 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) - def onTagRemoveDelay(self): - if self.current_tag is not None: - print('Tag gone, stopping playback') - self.current_tag = None - if self.playlist is not None: - pos = self.player.stop() - if pos is not None: - self.playlist.setPlaybackOffset(pos) + 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) def onButtonPressed(self, what): assert self.buttons is not None diff --git a/software/tests/test_playerapp.py b/software/tests/test_playerapp.py index ecacf35..b71590a 100644 --- a/software/tests/test_playerapp.py +++ b/software/tests/test_playerapp.py @@ -45,7 +45,10 @@ class FakeTimerManager: class FakeNfcReader: - def __init__(self): pass + tag_callback = None + + def __init__(self, tag_callback=None): + FakeNfcReader.tag_callback = tag_callback class FakeButtons: @@ -100,13 +103,13 @@ def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch): fake_db = FakePlaylistDb() fake_mp3 = FakeMp3Player() deps = app.Dependencies(mp3player=lambda _: fake_mp3, - nfcreader=lambda _: FakeNfcReader(), + 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) - dut.onTagChange([23, 42, 1, 2, 3]) + 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' assert "r" in fake_mp3.track.mode @@ -117,13 +120,13 @@ def test_playlist_seq(micropythonify, faketimermanager, monkeypatch): fake_db = FakePlaylistDb([b'track1.mp3', b'track2.mp3', b'track3.mp3']) fake_mp3 = FakeMp3Player() deps = app.Dependencies(mp3player=lambda _: fake_mp3, - nfcreader=lambda _: FakeNfcReader(), + 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) - dut.onTagChange([23, 42, 1, 2, 3]) + FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3]) assert fake_mp3.track is not None assert fake_mp3.track.filename == b'track1.mp3' @@ -150,11 +153,11 @@ def test_playlist_unknown_tag(micropythonify, faketimermanager, monkeypatch): fake_db = FakeNoPlaylistDb() fake_mp3 = FakeMp3Player() deps = app.Dependencies(mp3player=lambda _: fake_mp3, - nfcreader=lambda _: FakeNfcReader(), + 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) - dut.onTagChange([23, 42, 1, 2, 3]) + FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3]) assert fake_mp3.track is None