wip: led patterns, auto power off
Some checks failed
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m22s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
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) Failing after 11s

This commit is contained in:
2025-11-01 10:47:20 +01:00
parent 64716ea2d8
commit a93a0016a0
8 changed files with 117 additions and 44 deletions

View File

@@ -31,7 +31,7 @@ for hwconfig in src/hwconfig_*.py; do
hwconfig_base=$(basename "$hwconfig")
hwname=${hwconfig_base##hwconfig_}
hwname=${hwname%%.py}
find src/ -iname '*.py' \! -iname 'hwconfig_*.py' | cpio -pdm "$FS_STAGE_DIR"
find src/ -iname '*.py' \! -iname 'hwconfig*.py' | cpio -pdm "$FS_STAGE_DIR"
cp "$hwconfig" "$FS_STAGE_DIR"/src/hwconfig.py
tools/mklittlefs/mklittlefs -p 256 -s 868352 -c "$FS_STAGE_DIR"/src $BUILDDIR/filesystem.bin
truncate -s 2M $BUILDDIR/firmware-filesystem.bin

View File

@@ -6,7 +6,7 @@ import time
from utils import TimerManager
Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb'))
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,8 @@ 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
self.playlist = None
@@ -51,6 +53,7 @@ class PlayerApp:
self.mp3file = None
self.volume_pos = 3
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
self._onIdle()
def __del__(self):
if self.mp3file is not None:
@@ -95,8 +98,15 @@ class PlayerApp:
self.mp3file = None
self._play_next()
def onIdleTimeout(self):
print('IdleTimeout')
self.hwconfig.power_off()
def _set_playlist(self, tag: bytes):
self._unset_playlist()
if self.playlist is not None:
pos = self.player.stop()
if pos is not None:
self.playlist.setPlaybackOffset(pos)
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)
@@ -104,6 +114,7 @@ class PlayerApp:
def _unset_playlist(self):
if self.playlist is not None:
pos = self.player.stop()
self._onIdle()
if pos is not None:
self.playlist.setPlaybackOffset(pos)
self.playlist = None
@@ -122,7 +133,19 @@ class PlayerApp:
self.player.stop()
self.mp3file.close()
self.mp3file = None
self._onIdle()
if filename is not None:
print(f'Playing {filename!r}')
self.mp3file = open(filename, 'rb')
self.player.play(self.mp3file, offset)
self._onActive()
def _onIdle(self):
print('Idle')
self.timer_manager.schedule(time.ticks_ms() + 60*1000, self.onIdleTimeout)
self.leds.set_state('idle')
def _onActive(self):
print('Active')
self.timer_manager.cancel(self.onIdleTimeout)
self.leds.set_state('playing')

View File

@@ -61,3 +61,8 @@ 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)

View File

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

View File

@@ -31,25 +31,54 @@ micropython.alloc_emergency_exception_buf(100)
hwconfig.board_init()
async def rainbow(np, period=10):
def gamma(value, X=2.2):
return min(max(int(brightness * pow(value / 255.0, X) * 255.0 + 0.5), 0), 255)
class LedManager:
def __init__(self, np):
self.led_state = 'boot'
self.np = np
self.brightness = 0.05
self.leds = len(self.np)
asyncio.create_task(self.run())
brightness = 0.05
count = 0.0
leds = len(np)
while True:
for i in range(leds):
ofs = (count + i) % leds
np[i] = (gamma((sin(ofs / leds * 2 * pi) + 1) * 127),
gamma((sin(ofs / leds * 2 * pi + 2/3*pi) + 1) * 127),
gamma((sin(ofs / leds * 2 * pi + 4/3*pi) + 1) * 127))
count += 0.02 * leds
before = time.ticks_ms()
await np.async_write()
now = time.ticks_ms()
if before + 20 > now:
await asyncio.sleep_ms(20 - (now - before))
def set_state(self, state):
assert state in ['boot', 'idle', 'playing']
self.led_state = state
def _gamma(self, value, X=2.2):
result = min(max(int(self.brightness * pow(value / 255.0, X) * 255.0 + 0.5), 0), 255)
if value > 0:
result = max(1, result)
return result
def _rainbow(self, time):
for i in range(self.leds):
ofs = (time * self.leds + i) % self.leds
self.np[i] = (self._gamma((sin(ofs / self.leds * 2 * pi) + 1) * 127),
self._gamma((sin(ofs / self.leds * 2 * pi + 2/3*pi) + 1) * 127),
self._gamma((sin(ofs / self.leds * 2 * pi + 4/3*pi) + 1) * 127))
def _pulse(self, time, color, speed):
scaled_sin = max(1, (sin(time / speed * 2 * pi) + 1) * 127)
val = (self._gamma(color[0]*scaled_sin),
self._gamma(color[1]*scaled_sin),
self._gamma(color[2]*scaled_sin))
for i in range(self.leds):
self.np[i] = val
async def run(self):
time_ = 0.0
while True:
if self.led_state == 'idle':
self._pulse(time_, (0, 1, 0), 5)
elif self.led_state == 'boot':
self._pulse(time_, (1, 1, 0), 1)
elif self.led_state == 'playing':
self._rainbow(time_)
time_ += 0.02
before = time.ticks_ms()
await self.np.async_write()
now = time.ticks_ms()
if before + 20 > now:
await asyncio.sleep_ms(20 - (now - before))
# high prio for proc 1
@@ -63,7 +92,6 @@ def run():
asyncio.new_event_loop()
# Setup LEDs
np = NeoPixel(hwconfig.LED_DIN, hwconfig.LED_COUNT, sm=1)
asyncio.create_task(rainbow(np))
# Setup MP3 player
with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS,
@@ -83,14 +111,15 @@ def run():
# Setup NFC
reader = MFRC522(spi_id=hwconfig.RC522_SPIID, sck=hwconfig.RC522_SCK, miso=hwconfig.RC522_MISO,
mosi=hwconfig.RC522_MOSI, cs=hwconfig.RC522_SS, rst=hwconfig.RC522_RST, tocard_retries=20)
# Setup app
deps = app.Dependencies(mp3player=lambda the_app: MP3Player(audioctx, the_app),
nfcreader=lambda the_app: Nfc(reader, the_app),
buttons=lambda the_app: Buttons(the_app, pin_volup=hwconfig.BUTTON_VOLUP,
pin_voldown=hwconfig.BUTTON_VOLDOWN,
pin_next=hwconfig.BUTTON_NEXT),
playlistdb=lambda _: playlistdb)
playlistdb=lambda _: playlistdb,
hwconfig=lambda _: hwconfig,
leds=lambda _: LedManager(np))
the_app = app.PlayerApp(deps)
# Start

View File

@@ -273,7 +273,7 @@ class BTreeDB(IPlaylistDB):
return self.getPlaylistForTag(tag)
def getSetting(self, key: bytes | str) -> str:
if key is str:
if type(key) is str:
key = key.encode()
return self.db.get(b'settings/' + key, self.DEFAULT_SETTINGS[key]).decode()

View File

@@ -91,6 +91,14 @@ class FakePlaylistDb:
return None
class FakeHwconfig:
def power_off(self): pass
class FakeLeds:
def set_state(self, state): pass
def fake_open(filename, mode):
return FakeFile(filename, mode)
@@ -100,23 +108,28 @@ def faketimermanager(monkeypatch):
monkeypatch.setattr(utils.timer.TimerManager, '_instance', FakeTimerManager())
def _makedeps(mp3player=FakeMp3Player, nfcreader=FakeNfcReader, buttons=FakeButtons,
playlistdb=FakePlaylistDb, hwconfig=FakeHwconfig, leds=FakeLeds):
return app.Dependencies(mp3player=lambda _: mp3player(),
nfcreader=lambda x: nfcreader(x),
buttons=lambda _: buttons(),
playlistdb=lambda _: playlistdb(),
hwconfig=lambda _: hwconfig(),
leds=lambda _: leds())
def test_construct_app(micropythonify, faketimermanager):
fake_mp3 = FakeMp3Player()
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
nfcreader=lambda _: FakeNfcReader(),
buttons=lambda _: FakeButtons(),
playlistdb=lambda _: FakePlaylistDb())
deps = _makedeps(mp3player=fake_mp3)
_ = app.PlayerApp(deps)
fake_mp3 = app.mp3player
assert fake_mp3.volume is not None
def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch):
fake_db = FakePlaylistDb()
fake_mp3 = FakeMp3Player()
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
nfcreader=lambda x: FakeNfcReader(x),
buttons=lambda _: FakeButtons(),
playlistdb=lambda _: fake_db)
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
app.PlayerApp(deps)
with monkeypatch.context() as m:
m.setattr(builtins, 'open', fake_open)
@@ -130,10 +143,7 @@ def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch):
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 x: FakeNfcReader(x),
buttons=lambda _: FakeButtons(),
playlistdb=lambda _: fake_db)
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
dut = app.PlayerApp(deps)
with monkeypatch.context() as m:
m.setattr(builtins, 'open', fake_open)
@@ -166,10 +176,7 @@ def test_playlist_unknown_tag(micropythonify, faketimermanager, monkeypatch):
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)
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
app.PlayerApp(deps)
with monkeypatch.context() as m:
m.setattr(builtins, 'open', fake_open)
@@ -192,7 +199,9 @@ def test_tagmode_startstop(micropythonify, faketimermanager, monkeypatch):
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
nfcreader=lambda x: FakeNfcReader(x),
buttons=lambda _: FakeButtons(),
playlistdb=lambda _: fake_db)
playlistdb=lambda _: fake_db,
hwconfig=lambda _: FakeHwconfig(),
leds=lambda _: FakeLeds())
app.PlayerApp(deps)
with monkeypatch.context() as m:
m.setattr(builtins, 'open', fake_open)
@@ -231,7 +240,9 @@ def test_tagmode_remains(micropythonify, faketimermanager, monkeypatch):
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
nfcreader=lambda x: FakeNfcReader(x),
buttons=lambda _: FakeButtons(),
playlistdb=lambda _: fake_db)
playlistdb=lambda _: fake_db,
hwconfig=lambda _: FakeHwconfig(),
leds=lambda _: FakeLeds())
app.PlayerApp(deps)
with monkeypatch.context() as m:
m.setattr(builtins, 'open', fake_open)