feat: Implement shutdown on idle when on battery
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 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 11s

Allow the PlayerApp to turn off the device if it is idle for longer then
the timeout. The timeout is currently hardcoded to 1 minute, this will be
made configurable in the future.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
This commit is contained in:
2025-11-09 16:31:08 +01:00
parent 696f7b956c
commit 869a92d998
5 changed files with 75 additions and 2 deletions

View File

@@ -6,7 +6,7 @@ import time
from utils import TimerManager
Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb', 'leds'))
Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb', 'hwconfig', 'leds'))
# Should be ~ 6dB steps
VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251]
@@ -44,6 +44,7 @@ class PlayerApp:
self.player = deps.mp3player(self)
self.nfc = deps.nfcreader(self.tag_state_machine)
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.playing_tag = None
@@ -97,6 +98,13 @@ class PlayerApp:
self.mp3file = None
self._play_next()
def onIdleTimeout(self):
if self.hwconfig.get_on_battery():
self.hwconfig.power_off()
else:
# Check again in a minute
self.timer_manager.schedule(time.ticks_ms() + 60*1000, self.onIdleTimeout)
def _set_playlist(self, tag: bytes):
if self.playlist is not None:
pos = self.player.stop()
@@ -136,7 +144,9 @@ class PlayerApp:
self._onActive()
def _onIdle(self):
self.timer_manager.schedule(time.ticks_ms() + 60*1000, self.onIdleTimeout)
self.leds.set_state(self.leds.IDLE)
def _onActive(self):
self.timer_manager.cancel(self.onIdleTimeout)
self.leds.set_state(self.leds.PLAYING)

View File

@@ -39,6 +39,7 @@ BUTTON_POWER = Pin.board.GP21
# Power
POWER_EN = Pin.board.GP22
VBAT_ADC = Pin.board.GP26
VBUS_DET = Pin.board.WL_GPIO2
def board_init():
@@ -61,3 +62,13 @@ def get_battery_voltage():
adc = machine.ADC(VBAT_ADC) # create ADC object on ADC pin
battv = adc.read_u16()/65535.0*3.3*2
return battv
def power_off():
POWER_EN.init(mode=Pin.OUT)
POWER_EN.value(0)
def get_on_battery():
vbus = VBUS_DET.value()
return not vbus

View File

@@ -47,3 +47,12 @@ def board_init():
def get_battery_voltage():
# Not supported on breadboard
return None
def power_off():
# Not supported on breadboard
pass
def get_on_battery():
return False

View File

@@ -78,6 +78,7 @@ def run():
pin_voldown=hwconfig.BUTTON_VOLDOWN,
pin_next=hwconfig.BUTTON_NEXT),
playlistdb=lambda _: playlistdb,
hwconfig=lambda _: hwconfig,
leds=lambda _: LedManager(np))
the_app = app.PlayerApp(deps)

View File

@@ -101,6 +101,18 @@ class FakePlaylistDb:
return None
class FakeHwconfig:
def __init__(self):
self.powered = True
self.on_battery = False
def power_off(self):
self.powered = False
def get_on_battery(self):
return self.on_battery
class FakeLeds:
IDLE = 0
PLAYING = 1
@@ -124,11 +136,12 @@ def faketimermanager(monkeypatch):
def _makedeps(mp3player=FakeMp3Player, nfcreader=FakeNfcReader, buttons=FakeButtons,
playlistdb=FakePlaylistDb, leds=FakeLeds):
playlistdb=FakePlaylistDb, hwconfig=FakeHwconfig, leds=FakeLeds):
return app.Dependencies(mp3player=lambda _: mp3player() if callable(mp3player) else mp3player,
nfcreader=lambda x: nfcreader(x) if callable(nfcreader) else nfcreader,
buttons=lambda _: buttons() if callable(buttons) else buttons,
playlistdb=lambda _: playlistdb() if callable(playlistdb) else playlistdb,
hwconfig=lambda _: hwconfig() if callable(hwconfig) else hwconfig,
leds=lambda _: leds() if callable(leds) else leds)
@@ -278,3 +291,32 @@ def test_led_state(micropythonify, faketimermanager, monkeypatch):
FakeNfcReader.tag_callback.onTagChange(None)
faketimermanager.testing_run_queued()
assert fake_leds.state == FakeLeds.IDLE
def test_idle_shutdown_after_start(micropythonify, faketimermanager, monkeypatch):
fake_hwconfig = FakeHwconfig()
fake_hwconfig.on_battery = True
deps = _makedeps(hwconfig=fake_hwconfig)
app.PlayerApp(deps)
assert fake_hwconfig.powered
faketimermanager.testing_run_queued()
assert not fake_hwconfig.powered
def test_idle_shutdown_after_playback(micropythonify, faketimermanager, monkeypatch):
fake_hwconfig = FakeHwconfig()
fake_hwconfig.on_battery = True
deps = _makedeps(hwconfig=fake_hwconfig)
app.PlayerApp(deps)
assert fake_hwconfig.powered
with monkeypatch.context() as m:
m.setattr(builtins, 'open', fake_open)
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
faketimermanager.testing_run_queued()
assert fake_hwconfig.powered
# Stop playback
FakeNfcReader.tag_callback.onTagChange(None)
faketimermanager.testing_run_queued()
# Elapse idle timer
faketimermanager.testing_run_queued()
assert not fake_hwconfig.powered