8 Commits

Author SHA1 Message Date
32bf6a8d68 always return content type application/json
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m36s
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 10s
2025-12-16 23:13:04 +01:00
e2f9287ebd feat: last connected tag uid available at /api/v1/last_tag_uid
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m39s
Check code formatting / Check-C-Format (push) Successful in 8s
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 9s
Run pytests / Check-Pytest (push) Successful in 11s
2025-12-16 22:41:55 +01:00
19dff763bd feat: webserver: keep alive; move playback write prot to handler
- Reset the app idle timer when interacting with the webapp, so that the
  device does not turn off while the web ui is used.

- Handle denying put/post while playback is active centrally in the
  before_request handler, so that it does not need to be copy/pasted
  into every request handler.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-16 22:34:06 +01:00
768b630722 Merge pull request 'misc-stability-improvements' (#56) from misc-stability-improvements into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m36s
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 7s
Run pytests / Check-Pytest (push) Successful in 10s
Reviewed-on: #56
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-12-16 19:25:44 +00:00
e0ff9c54bc refactor: timer: split out two helpers from _timer_worker
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m35s
Check code formatting / Check-C-Format (push) Successful in 8s
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
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-16 20:19:30 +01:00
c0b9ef2961 fix: timer: handle modifications to timers when _timer_worker is already scheduled correctly
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m45s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 11s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 6s
Run unit tests on host / Run-Unit-Tests (push) Successful in 10s
Run pytests / Check-Pytest (push) Successful in 12s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-07 13:25:35 +01:00
e23f8bd34c fix: exceptions in callback should not terminate caller
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m46s
Check code formatting / Check-C-Format (push) Successful in 9s
Check code formatting / Check-Python-Flake8 (push) Successful in 11s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 6s
Run unit tests on host / Run-Unit-Tests (push) Successful in 10s
Run pytests / Check-Pytest (push) Successful in 13s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-07 13:09:34 +01:00
97e9742c75 feat: Add basic watchdog timer
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-12-07 13:09:34 +01:00
8 changed files with 82 additions and 29 deletions

View File

@@ -116,6 +116,10 @@ class PlayerApp:
# Check again in a minute
self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout)
def reset_idle_timeout(self):
if not self.playing:
self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout)
def is_playing(self) -> bool:
return self.playing
@@ -186,3 +190,6 @@ class PlayerApp:
self.timer_manager.cancel(self.onIdleTimeout)
self.leds.set_state(self.leds.PLAYING)
self.playing = True
def get_nfc(self):
return self.nfc

View File

@@ -53,6 +53,13 @@ def setup_wifi():
print(f"ifconfig: {wlan.ifconfig()}")
async def wdt_task(wdt):
# TODO: more checking of app health
# Right now this only protects against the asyncio executor crashing completely
while True:
await asyncio.sleep_ms(500)
wdt.feed()
DB_PATH = '/sd/tonberry.db'
config = Configuration()
@@ -97,8 +104,10 @@ def run():
start_webserver(config, the_app)
# Start
wdt = machine.WDT(timeout=1000)
asyncio.create_task(aiorepl.task({'timer_manager': TimerManager(),
'app': the_app}))
asyncio.create_task(wdt_task(wdt))
asyncio.get_event_loop().run_forever()

View File

@@ -6,6 +6,7 @@ Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
import asyncio
import time
from utils import safe_callback
from mfrc522 import MFRC522
try:
@@ -74,7 +75,7 @@ class Nfc:
self.last_uid = uid
self.last_uid_timestamp = time.ticks_us()
if self.cb is not None and last_callback_uid != uid:
self.cb.onTagChange(uid)
safe_callback(lambda: self.cb.onTagChange(uid), "nfc tag change")
last_callback_uid = uid
await asyncio.sleep_ms(poll_interval_ms)

View File

@@ -1,6 +1,7 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
from utils.helpers import safe_callback
from utils.buttons import Buttons
from utils.config import Configuration
from utils.leds import LedManager
@@ -11,4 +12,4 @@ from utils.sdcontext import SDContext
from utils.timer import TimerManager
__all__ = ["BTreeDB", "BTreeFileManager", "Buttons", "Configuration", "get_pin_index", "LedManager", "MBRPartition",
"SDContext", "TimerManager"]
"safe_callback", "SDContext", "TimerManager"]

View File

@@ -5,6 +5,7 @@ import asyncio
import machine
import micropython
import time
from utils import safe_callback
try:
from typing import TYPE_CHECKING # type: ignore
except ImportError:
@@ -74,4 +75,4 @@ class Buttons:
await self.int_flag.wait()
while len(self.pressed) > 0:
what = self.pressed.pop()
self.cb.onButtonPressed(what)
safe_callback(lambda: self.cb.onButtonPressed(what), "button callback")

View File

@@ -0,0 +1,12 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
import sys
def safe_callback(func, name="callback"):
try:
func()
except Exception as ex:
print(f"Uncaught exception in {name}")
sys.print_exception(ex)

View File

@@ -4,6 +4,7 @@
import asyncio
import heapq
import time
from utils import safe_callback
TIMER_DEBUG = True
@@ -49,28 +50,36 @@ class TimerManager(object):
heapq.heapify(self.timers)
return i
def _next_timeout(self):
if len(self.timers) == 0:
if self.timer_debug:
print("timer: worker: queue empty")
return None
cur_nearest = self.timers[0][0]
next_timeout = cur_nearest - time.ticks_ms()
if self.timer_debug:
if next_timeout > 0:
print(f"timer: worker: next is {self.timers[0]}, sleep {next_timeout} ms")
else:
print(f"timer: worker: {self.timers[0]} elapsed @{cur_nearest}, delay {-next_timeout} ms")
return next_timeout
async def _wait(self, timeout):
try:
await asyncio.wait_for_ms(self.worker_event.wait(), timeout)
if self.timer_debug:
print("timer: worker: event")
# got woken up due to event
self.worker_event.clear()
return True
except asyncio.TimeoutError:
return False
async def _timer_worker(self):
while True:
if len(self.timers) == 0:
# Nothing to do
await self.worker_event.wait()
if self.timer_debug:
print("_timer_worker: event 0")
self.worker_event.clear()
continue
cur_nearest = self.timers[0][0]
wait_time = cur_nearest - time.ticks_ms()
if wait_time > 0:
if self.timer_debug:
print(f"_timer_worker: next is {self.timers[0]}, sleep {wait_time} ms")
try:
await asyncio.wait_for_ms(self.worker_event.wait(), wait_time)
if self.timer_debug:
print("_timer_worker: event 1")
# got woken up due to event
self.worker_event.clear()
continue
except asyncio.TimeoutError:
pass
_, callback = heapq.heappop(self.timers)
callback()
next_timeout = self._next_timeout()
if next_timeout is None or next_timeout > 0:
await self._wait(next_timeout)
else:
_, callback = heapq.heappop(self.timers)
safe_callback(callback, "timer callback")

View File

@@ -11,13 +11,22 @@ webapp = Microdot()
server = None
config = None
app = None
nfc = None
def start_webserver(config_, app_):
global server, config, app
global server, config, app, nfc
server = asyncio.create_task(webapp.start_server(port=80))
config = config_
app = app_
nfc = app.get_nfc()
@webapp.before_request
async def before_request_handler(request):
if request.method in ['PUT', 'POST'] and app.is_playing():
return "Cannot write to device while playback is active", 503
app.reset_idle_timeout()
@webapp.route('/')
@@ -52,10 +61,14 @@ async def config_get(request):
@webapp.route('/api/v1/config', methods=['PUT'])
async def config_put(request):
if app.is_playing():
return 503
try:
config.set_config(request.json)
except ValueError as ex:
return str(ex), 400
return '', 204
@webapp.route('/api/v1/last_tag_uid', methods=['GET'])
async def last_tag_uid_get(request):
tag, _ = nfc.get_last_uid()
return {'tag': tag}