From 176fc66c178241e4122be5372fb5014681787bcc Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Sun, 23 Nov 2025 14:50:11 +0100 Subject: [PATCH 1/7] feat: config store Load a config.json file from the root of the littlefs flash filesystem. Signed-off-by: Matthias Blankertz --- software/src/utils/__init__.py | 5 ++-- software/src/utils/config.py | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 software/src/utils/config.py diff --git a/software/src/utils/__init__.py b/software/src/utils/__init__.py index abf0eae..3f5d65d 100644 --- a/software/src/utils/__init__.py +++ b/software/src/utils/__init__.py @@ -2,6 +2,7 @@ # Copyright (c) 2025 Matthias Blankertz from utils.buttons import Buttons +from utils.config import Configuration from utils.leds import LedManager from utils.mbrpartition import MBRPartition from utils.pinindex import get_pin_index @@ -9,5 +10,5 @@ from utils.playlistdb import BTreeDB, BTreeFileManager from utils.sdcontext import SDContext from utils.timer import TimerManager -__all__ = ["BTreeDB", "BTreeFileManager", "Buttons", "get_pin_index", "LedManager", "MBRPartition", "SDContext", - "TimerManager"] +__all__ = ["BTreeDB", "BTreeFileManager", "Buttons", "Configuration", "get_pin_index", "LedManager", "MBRPartition", + "SDContext", "TimerManager"] diff --git a/software/src/utils/config.py b/software/src/utils/config.py new file mode 100644 index 0000000..ea9b050 --- /dev/null +++ b/software/src/utils/config.py @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2025 Matthias Blankertz + +from errno import ENOENT +import json +import os + + +class Configuration: + DEFAULT_CONFIG = { + } + + def __init__(self, config_path='/config.json'): + self.config_path = config_path + try: + with open(self.config_path, 'r') as conf_file: + self.config = json.load(conf_file) + except OSError as ex: + if ex.errno == ENOENT: + self.config = Configuration.DEFAULT_CONFIG + self._save() + else: + raise + except ValueError as ex: + print(f"Warning: Could not load configuration {self.config_path}:\n{ex}") + self._move_config_to_backup() + self.config = Configuration.DEFAULT_CONFIG + + def _move_config_to_backup(self): + # Remove old backup + try: + os.remove(self.config_path + '.bup') + os.rename(self.config_path, self.config_path + '.bup') + except OSError as ex: + if ex.errno != ENOENT: + raise + os.sync() + + def _save(self): + with open(self.config_path + '.new', 'w') as conf_file: + json.dump(self.config, conf_file) + self._move_config_to_backup() + os.rename(self.config_path + '.new', self.config_path) + os.sync() From 22259066643f7cb7e8e57c92df23415fd102953c Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Fri, 28 Nov 2025 18:19:04 +0100 Subject: [PATCH 2/7] feat: Move LED_COUNT from hwconfig to config.json Signed-off-by: Matthias Blankertz --- software/src/hwconfig_Rev1/hwconfig.py | 1 - software/src/hwconfig_breadboard/hwconfig.py | 1 - software/src/main.py | 6 ++++-- software/src/utils/config.py | 4 ++++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/software/src/hwconfig_Rev1/hwconfig.py b/software/src/hwconfig_Rev1/hwconfig.py index c9240a7..6ad9afc 100644 --- a/software/src/hwconfig_Rev1/hwconfig.py +++ b/software/src/hwconfig_Rev1/hwconfig.py @@ -28,7 +28,6 @@ RC522_SS = Pin.board.GP13 # WS2812 LED_DIN = Pin.board.GP16 -LED_COUNT = 1 # Buttons BUTTON_VOLUP = Pin.board.GP17 diff --git a/software/src/hwconfig_breadboard/hwconfig.py b/software/src/hwconfig_breadboard/hwconfig.py index 9243682..12df6bb 100644 --- a/software/src/hwconfig_breadboard/hwconfig.py +++ b/software/src/hwconfig_breadboard/hwconfig.py @@ -27,7 +27,6 @@ RC522_SS = Pin.board.GP13 # WS2812 LED_DIN = Pin.board.GP16 -LED_COUNT = 1 # Buttons BUTTON_VOLUP = Pin.board.GP17 diff --git a/software/src/main.py b/software/src/main.py index 813b036..5db7f15 100644 --- a/software/src/main.py +++ b/software/src/main.py @@ -18,7 +18,7 @@ from mfrc522 import MFRC522 from mp3player import MP3Player from nfc import Nfc from rp2_neopixel import NeoPixel -from utils import BTreeFileManager, Buttons, SDContext, TimerManager, LedManager +from utils import BTreeFileManager, Buttons, SDContext, TimerManager, LedManager, Configuration from webserver import start_webserver try: @@ -55,11 +55,13 @@ def setup_wifi(): DB_PATH = '/sd/tonberry.db' +config = Configuration() + def run(): asyncio.new_event_loop() # Setup LEDs - np = NeoPixel(hwconfig.LED_DIN, hwconfig.LED_COUNT, sm=1) + np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1) # Wifi with default config setup_wifi() diff --git a/software/src/utils/config.py b/software/src/utils/config.py index ea9b050..b2fd95f 100644 --- a/software/src/utils/config.py +++ b/software/src/utils/config.py @@ -8,6 +8,7 @@ import os class Configuration: DEFAULT_CONFIG = { + 'LED_COUNT': 1, } def __init__(self, config_path='/config.json'): @@ -42,3 +43,6 @@ class Configuration: self._move_config_to_backup() os.rename(self.config_path + '.new', self.config_path) os.sync() + + def get_led_count(self): + return self.config.get('LED_COUNT', self.DEFAULT_CONFIG['LED_COUNT']) From 83deb1b4c2664e453e0a39462ab4535b56d58d69 Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Fri, 28 Nov 2025 19:41:44 +0100 Subject: [PATCH 3/7] feat: Replace hardcoded timeouts in app with configurable Signed-off-by: Matthias Blankertz --- software/src/app.py | 17 +++++++++++------ software/src/main.py | 3 ++- software/src/utils/config.py | 13 ++++++++++++- software/tests/test_playerapp.py | 18 ++++++++++++++++-- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/software/src/app.py b/software/src/app.py index 382deb3..cd07a37 100644 --- a/software/src/app.py +++ b/software/src/app.py @@ -6,7 +6,8 @@ import time from utils import TimerManager -Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb', 'hwconfig', 'leds')) +Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb', 'hwconfig', 'leds', + 'config')) # Should be ~ 6dB steps VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251] @@ -14,11 +15,12 @@ VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251] class PlayerApp: class TagStateMachine: - def __init__(self, parent, timer_manager): + def __init__(self, parent, timer_manager, timeout=5000): self.parent = parent self.timer_manager = timer_manager self.current_tag = None self.current_tag_time = time.ticks_ms() + self.timeout = timeout def onTagChange(self, new_tag): if new_tag is not None: @@ -31,7 +33,7 @@ class PlayerApp: self.current_tag = new_tag self.parent.onNewTag(new_tag) else: - self.timer_manager.schedule(time.ticks_ms() + 5000, self.onTagRemoveDelay) + self.timer_manager.schedule(time.ticks_ms() + self.timeout, self.onTagRemoveDelay) def onTagRemoveDelay(self): if self.current_tag is not None: @@ -40,7 +42,10 @@ class PlayerApp: def __init__(self, deps: Dependencies): self.timer_manager = TimerManager() - self.tag_state_machine = self.TagStateMachine(self, self.timer_manager) + self.config = deps.config(self) + self.tag_timeout_ms = self.config.get_tag_timeout() * 1000 + self.idle_timeout_ms = self.config.get_idle_timeout() * 1000 + self.tag_state_machine = self.TagStateMachine(self, self.timer_manager, self.tag_timeout_ms) self.player = deps.mp3player(self) self.nfc = deps.nfcreader(self.tag_state_machine) self.playlist_db = deps.playlistdb(self) @@ -103,7 +108,7 @@ class PlayerApp: self.hwconfig.power_off() else: # Check again in a minute - self.timer_manager.schedule(time.ticks_ms() + 60*1000, self.onIdleTimeout) + self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout) def _set_playlist(self, tag: bytes): if self.playlist is not None: @@ -144,7 +149,7 @@ class PlayerApp: self._onActive() def _onIdle(self): - self.timer_manager.schedule(time.ticks_ms() + 60*1000, self.onIdleTimeout) + self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout) self.leds.set_state(self.leds.IDLE) def _onActive(self): diff --git a/software/src/main.py b/software/src/main.py index 5db7f15..17f0881 100644 --- a/software/src/main.py +++ b/software/src/main.py @@ -94,7 +94,8 @@ def run(): pin_next=hwconfig.BUTTON_NEXT), playlistdb=lambda _: playlistdb, hwconfig=lambda _: hwconfig, - leds=lambda _: LedManager(np)) + leds=lambda _: LedManager(np), + config=lambda _: config) the_app = app.PlayerApp(deps) # Start diff --git a/software/src/utils/config.py b/software/src/utils/config.py index b2fd95f..56a52af 100644 --- a/software/src/utils/config.py +++ b/software/src/utils/config.py @@ -9,6 +9,8 @@ import os class Configuration: DEFAULT_CONFIG = { 'LED_COUNT': 1, + 'IDLE_TIMEOUT_SECS': 60, + 'TAG_TIMEOUT_SECS': 5, } def __init__(self, config_path='/config.json'): @@ -44,5 +46,14 @@ class Configuration: os.rename(self.config_path + '.new', self.config_path) os.sync() + def _get(self, key): + return self.config.get(key, self.DEFAULT_CONFIG[key]) + def get_led_count(self): - return self.config.get('LED_COUNT', self.DEFAULT_CONFIG['LED_COUNT']) + return self._get('LED_COUNT') + + def get_idle_timeout(self): + return self._get('IDLE_TIMEOUT_SECS') + + def get_tag_timeout(self): + return self._get('TAG_TIMEOUT_SECS') diff --git a/software/tests/test_playerapp.py b/software/tests/test_playerapp.py index fd839dc..9288bd9 100644 --- a/software/tests/test_playerapp.py +++ b/software/tests/test_playerapp.py @@ -124,6 +124,19 @@ class FakeLeds: self.state = state +class FakeConfig: + def __init__(self): pass + + def get_led_count(self): + return 1 + + def get_idle_timeout(self): + return 60 + + def get_tag_timeout(self): + return 5 + + def fake_open(filename, mode): return FakeFile(filename, mode) @@ -136,13 +149,14 @@ def faketimermanager(monkeypatch): def _makedeps(mp3player=FakeMp3Player, nfcreader=FakeNfcReader, buttons=FakeButtons, - playlistdb=FakePlaylistDb, hwconfig=FakeHwconfig, leds=FakeLeds): + playlistdb=FakePlaylistDb, hwconfig=FakeHwconfig, leds=FakeLeds, config=FakeConfig): 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) + leds=lambda _: leds() if callable(leds) else leds, + config=lambda _: config() if callable(config) else config) def test_construct_app(micropythonify, faketimermanager): From 856bf34161e082983e43f18760bd7010e6e29bf5 Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Sun, 30 Nov 2025 12:31:50 +0100 Subject: [PATCH 4/7] feat: Make button mapping configurable - Remove Pin to button mapping from hwconfig, replace with a simple list of Pins to which buttons are attached - Add BUTTON_MAP to config.json which maps indices in the HW button list to button names - Update utils.Buttons to use the button map to connect the correct pins to the matching key codes Signed-off-by: Matthias Blankertz --- software/src/hwconfig_Rev1/hwconfig.py | 10 +++--- software/src/hwconfig_breadboard/hwconfig.py | 8 ++--- software/src/main.py | 6 ++-- software/src/utils/buttons.py | 38 ++++++++++++++++---- software/src/utils/config.py | 20 +++++++++-- 5 files changed, 60 insertions(+), 22 deletions(-) diff --git a/software/src/hwconfig_Rev1/hwconfig.py b/software/src/hwconfig_Rev1/hwconfig.py index 6ad9afc..5cffeed 100644 --- a/software/src/hwconfig_Rev1/hwconfig.py +++ b/software/src/hwconfig_Rev1/hwconfig.py @@ -30,10 +30,12 @@ RC522_SS = Pin.board.GP13 LED_DIN = Pin.board.GP16 # Buttons -BUTTON_VOLUP = Pin.board.GP17 -BUTTON_VOLDOWN = Pin.board.GP19 -BUTTON_NEXT = Pin.board.GP18 -BUTTON_POWER = Pin.board.GP21 +BUTTONS = [Pin.board.GP17, + Pin.board.GP18, + Pin.board.GP19, + Pin.board.GP20, + Pin.board.GP21, + ] # Power POWER_EN = Pin.board.GP22 diff --git a/software/src/hwconfig_breadboard/hwconfig.py b/software/src/hwconfig_breadboard/hwconfig.py index 12df6bb..4fc026d 100644 --- a/software/src/hwconfig_breadboard/hwconfig.py +++ b/software/src/hwconfig_breadboard/hwconfig.py @@ -29,10 +29,10 @@ RC522_SS = Pin.board.GP13 LED_DIN = Pin.board.GP16 # Buttons -BUTTON_VOLUP = Pin.board.GP17 -BUTTON_VOLDOWN = Pin.board.GP19 -BUTTON_NEXT = Pin.board.GP18 -BUTTON_POWER = None +BUTTONS = [Pin.board.GP17, + Pin.board.GP18, + Pin.board.GP19, + ] # Power POWER_EN = None diff --git a/software/src/main.py b/software/src/main.py index 17f0881..a02b9a1 100644 --- a/software/src/main.py +++ b/software/src/main.py @@ -89,9 +89,7 @@ def run(): # 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), + buttons=lambda the_app: Buttons(the_app, config, hwconfig), playlistdb=lambda _: playlistdb, hwconfig=lambda _: hwconfig, leds=lambda _: LedManager(np), @@ -124,5 +122,5 @@ def builddb(): if __name__ == '__main__': time.sleep(1) - if machine.Pin(hwconfig.BUTTON_VOLUP, machine.Pin.IN, machine.Pin.PULL_UP).value() != 0: + if machine.Pin(hwconfig.BUTTONS[0], machine.Pin.IN, machine.Pin.PULL_UP).value() != 0: run() diff --git a/software/src/utils/buttons.py b/software/src/utils/buttons.py index 140ffc6..a1f1c6e 100644 --- a/software/src/utils/buttons.py +++ b/software/src/utils/buttons.py @@ -17,14 +17,27 @@ if TYPE_CHECKING: class Buttons: - def __init__(self, cb: "ButtonCallback", pin_volup=17, pin_voldown=19, pin_next=18): - self.VOLUP = micropython.const(1) - self.VOLDOWN = micropython.const(2) - self.NEXT = micropython.const(3) + VOLUP = micropython.const(1) + VOLDOWN = micropython.const(2) + NEXT = micropython.const(3) + PREV = micropython.const(4) + PLAY_PAUSE = micropython.const(5) + KEYMAP = {VOLUP: 'VOLUP', + VOLDOWN: 'VOLDOWN', + NEXT: 'NEXT', + PREV: 'PREV', + PLAY_PAUSE: 'PLAY_PAUSE'} + + def __init__(self, cb: "ButtonCallback", config, hwconfig): + self.button_map = config.get_button_map() + self.hw_buttons = hwconfig.BUTTONS self.cb = cb - self.buttons = {machine.Pin(pin_volup, machine.Pin.IN, machine.Pin.PULL_UP): self.VOLUP, - machine.Pin(pin_voldown, machine.Pin.IN, machine.Pin.PULL_UP): self.VOLDOWN, - machine.Pin(pin_next, machine.Pin.IN, machine.Pin.PULL_UP): self.NEXT} + self.buttons = dict() + for key_id, key_name in self.KEYMAP.items(): + pin = self._get_pin(key_name) + if pin is None: + continue + self.buttons[pin] = key_id self.int_flag = asyncio.ThreadSafeFlag() self.pressed: list[int] = [] self.last: dict[int, int] = {} @@ -32,6 +45,17 @@ class Buttons: button.irq(handler=self._interrupt, trigger=machine.Pin.IRQ_FALLING | machine.Pin.IRQ_RISING) asyncio.create_task(self.task()) + def _get_pin(self, key): + key_id = self.button_map.get(key, None) + if key_id is None: + return None + if key_id < 0 or key_id >= len(self.hw_buttons): + return None + pin = self.hw_buttons[key_id] + if pin is not None: + pin.init(machine.Pin.IN, machine.Pin.PULL_UP) + return pin + def _interrupt(self, button): keycode = self.buttons[button] last = self.last.get(keycode, 0) diff --git a/software/src/utils/config.py b/software/src/utils/config.py index 56a52af..432c2b8 100644 --- a/software/src/utils/config.py +++ b/software/src/utils/config.py @@ -4,6 +4,10 @@ from errno import ENOENT import json import os +try: + from typing import TYPE_CHECKING, Mapping +except ImportError: + TYPE_CHECKING = False class Configuration: @@ -11,6 +15,13 @@ class Configuration: 'LED_COUNT': 1, 'IDLE_TIMEOUT_SECS': 60, 'TAG_TIMEOUT_SECS': 5, + 'BUTTON_MAP': { + 'PLAY_PAUSE': 4, + 'VOLUP': 0, + 'VOLDOWN': 2, + 'PREV': None, + 'NEXT': 1, + } } def __init__(self, config_path='/config.json'): @@ -49,11 +60,14 @@ class Configuration: def _get(self, key): return self.config.get(key, self.DEFAULT_CONFIG[key]) - def get_led_count(self): + def get_led_count(self) -> int: return self._get('LED_COUNT') - def get_idle_timeout(self): + def get_idle_timeout(self) -> int: return self._get('IDLE_TIMEOUT_SECS') - def get_tag_timeout(self): + def get_tag_timeout(self) -> int: return self._get('TAG_TIMEOUT_SECS') + + def get_button_map(self) -> Mapping[str, int | None]: + return self._get('BUTTON_MAP') From fa0e23ee8793bd66f1f7d2516e65ae1025d33f15 Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Sun, 30 Nov 2025 13:38:53 +0100 Subject: [PATCH 5/7] feat: Add play/pause and prev track function Signed-off-by: Matthias Blankertz --- software/src/app.py | 25 +++++++++++++++++++++++++ software/src/utils/playlistdb.py | 11 +++++++++++ 2 files changed, 36 insertions(+) diff --git a/software/src/app.py b/software/src/app.py index cd07a37..15f486c 100644 --- a/software/src/app.py +++ b/software/src/app.py @@ -57,6 +57,7 @@ class PlayerApp: self.buttons = deps.buttons(self) if deps.buttons is not None else None self.mp3file = None self.volume_pos = 3 + self.paused = False self.player.set_volume(VOLUME_CURVE[self.volume_pos]) self._onIdle() @@ -96,6 +97,10 @@ class PlayerApp: self.player.set_volume(VOLUME_CURVE[self.volume_pos]) elif what == self.buttons.NEXT: self._play_next() + elif what == self.buttons.PREV: + self._play_prev() + elif what == self.buttons.PLAY_PAUSE: + self._pause_toggle() def onPlaybackDone(self): assert self.mp3file is not None @@ -136,6 +141,15 @@ class PlayerApp: self.playlist = None self.playing_tag = None + def _play_prev(self): + if self.playlist is None: + return + filename = self.playlist.getPrevPath() + 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: self.player.stop() @@ -146,8 +160,19 @@ class PlayerApp: print(f'Playing {filename!r}') self.mp3file = open(filename, 'rb') self.player.play(self.mp3file, offset) + self.paused = False self._onActive() + def _pause_toggle(self): + if self.playlist is None: + return + if self.paused: + self._play(self.playlist.getCurrentPath(), self.pause_offset) + else: + self.pause_offset = self.player.stop() + self.paused = True + self._onIdle() + def _onIdle(self): self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout) self.leds.set_state(self.leds.IDLE) diff --git a/software/src/utils/playlistdb.py b/software/src/utils/playlistdb.py index 01ecd60..a8c866c 100644 --- a/software/src/utils/playlistdb.py +++ b/software/src/utils/playlistdb.py @@ -107,6 +107,17 @@ class BTreeDB(IPlaylistDB): self.setPlaybackOffset(0) return self.getCurrentPath() + def getPrevPath(self): + """ + Select prev track and return path. + """ + if self.pos > 0: + self.pos -= 1 + if self.persist != BTreeDB.PERSIST_NO: + self.parent._setPlaylistPos(self.tag, self.pos) + self.setPlaybackOffset(0) + return self.getCurrentPath() + def setPlaybackOffset(self, offset): """ Store the current position in the track for PERSIST_OFFSET mode From 2e1bc7782bac999601199457463915002c3d3de3 Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Sun, 30 Nov 2025 13:39:36 +0100 Subject: [PATCH 6/7] fix: Ensure each timer can only be scheduled once If a timer is scheduled with the timer manager while it is already scheduled, the new exipry time should override the prev. expiry time instead of adding a second instance of the same timer function. Signed-off-by: Matthias Blankertz --- software/src/utils/timer.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/software/src/utils/timer.py b/software/src/utils/timer.py index 853416b..e389ee5 100644 --- a/software/src/utils/timer.py +++ b/software/src/utils/timer.py @@ -22,6 +22,7 @@ class TimerManager(object): def schedule(self, when, what): cur_nearest = self.timers[0][0] if len(self.timers) > 0 else None + self._remove_timer(what) # Ensure timer is not already scheduled heapq.heappush(self.timers, (when, what)) if cur_nearest is None or cur_nearest > self.timers[0][0]: # New timer is closer than previous closest timer @@ -31,18 +32,22 @@ class TimerManager(object): self.worker_event.set() def cancel(self, what): + remove_idx = self._remove_timer(what) + if remove_idx == 0: + # Cancel timer was closest timer + if self.timer_debug: + print("cancel: wake") + self.worker_event.set() + return True + + def _remove_timer(self, what): try: (when, _), i = next(filter(lambda item: item[0][1] == what, zip(self.timers, range(len(self.timers))))) except StopIteration: return False del self.timers[i] heapq.heapify(self.timers) - if i == 0: - # Cancel timer was closest timer - if self.timer_debug: - print("cancel: wake") - self.worker_event.set() - return True + return i async def _timer_worker(self): while True: From a7e58853bb7d86e8faf5234c7b75386cb9cc9e48 Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Wed, 3 Dec 2025 20:12:29 +0100 Subject: [PATCH 7/7] feat: Add api/v1/config to get, put config Signed-off-by: Matthias Blankertz --- software/src/main.py | 2 +- software/src/utils/config.py | 20 +++++++++++++++++++- software/src/webserver.py | 20 ++++++++++++++++++-- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/software/src/main.py b/software/src/main.py index a02b9a1..62282ad 100644 --- a/software/src/main.py +++ b/software/src/main.py @@ -65,7 +65,7 @@ def run(): # Wifi with default config setup_wifi() - start_webserver() + start_webserver(config) # Setup MP3 player with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS, diff --git a/software/src/utils/config.py b/software/src/utils/config.py index 432c2b8..b0e0104 100644 --- a/software/src/utils/config.py +++ b/software/src/utils/config.py @@ -5,7 +5,7 @@ from errno import ENOENT import json import os try: - from typing import TYPE_CHECKING, Mapping + from typing import TYPE_CHECKING, Mapping, Any except ImportError: TYPE_CHECKING = False @@ -71,3 +71,21 @@ class Configuration: def get_button_map(self) -> Mapping[str, int | None]: return self._get('BUTTON_MAP') + + # For the web API + def get_config(self) -> Mapping[str, Any]: + return self.config + + def _validate(self, default, config, path=''): + for k in config.keys(): + if k not in default: + raise ValueError(f'Invalid config key {path}/{k}') + if isinstance(default[k], dict): + if not isinstance(config[k], dict): + raise ValueError(f'Invalid config: Value of {path}/{k} must be mapping') + self._validate(default[k], config[k], f'{path}/{k}') + + def set_config(self, config): + self._validate(self.DEFAULT_CONFIG, config) + self.config = config + self._save() diff --git a/software/src/webserver.py b/software/src/webserver.py index eeb2a95..6f1aaf6 100644 --- a/software/src/webserver.py +++ b/software/src/webserver.py @@ -9,11 +9,13 @@ from microdot import Microdot webapp = Microdot() server = None +config = None -def start_webserver(): - global server +def start_webserver(config_): + global server, config server = asyncio.create_task(webapp.start_server(port=80)) + config = config_ @webapp.route('/') @@ -39,3 +41,17 @@ async def filesystem_post(request): async def playlist_post(request): print(request) return {'success': False} + + +@webapp.route('/api/v1/config', methods=['GET']) +async def config_get(request): + return config.get_config() + + +@webapp.route('/api/v1/config', methods=['PUT']) +async def config_put(request): + try: + config.set_config(request.json) + except ValueError as ex: + return str(ex), 400 + return '', 204