From 97e9742c75a648ee9b265f93a3f3e4725f1a6a35 Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Sun, 7 Dec 2025 13:06:14 +0100 Subject: [PATCH 1/4] feat: Add basic watchdog timer Signed-off-by: Matthias Blankertz --- software/src/main.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/software/src/main.py b/software/src/main.py index 768ead1..613ff97 100644 --- a/software/src/main.py +++ b/software/src/main.py @@ -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() From e23f8bd34cfa4020a2be31c0ac40c754f6743ae8 Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Sun, 7 Dec 2025 13:07:38 +0100 Subject: [PATCH 2/4] fix: exceptions in callback should not terminate caller Signed-off-by: Matthias Blankertz --- software/src/nfc/nfc.py | 3 ++- software/src/utils/__init__.py | 3 ++- software/src/utils/buttons.py | 3 ++- software/src/utils/helpers.py | 12 ++++++++++++ software/src/utils/timer.py | 5 ++++- 5 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 software/src/utils/helpers.py diff --git a/software/src/nfc/nfc.py b/software/src/nfc/nfc.py index 01fd8b8..650c80c 100644 --- a/software/src/nfc/nfc.py +++ b/software/src/nfc/nfc.py @@ -6,6 +6,7 @@ Copyright (c) 2025 Matthias Blankertz 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) diff --git a/software/src/utils/__init__.py b/software/src/utils/__init__.py index 3f5d65d..cdb1ec8 100644 --- a/software/src/utils/__init__.py +++ b/software/src/utils/__init__.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2025 Matthias Blankertz +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"] diff --git a/software/src/utils/buttons.py b/software/src/utils/buttons.py index a1f1c6e..b6b5a24 100644 --- a/software/src/utils/buttons.py +++ b/software/src/utils/buttons.py @@ -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") diff --git a/software/src/utils/helpers.py b/software/src/utils/helpers.py new file mode 100644 index 0000000..afb91a6 --- /dev/null +++ b/software/src/utils/helpers.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2025 Matthias Blankertz + +import sys + + +def safe_callback(func, name="callback"): + try: + func() + except Exception as ex: + print(f"Uncaught exception in {name}") + sys.print_exception(ex) diff --git a/software/src/utils/timer.py b/software/src/utils/timer.py index e389ee5..301a7e6 100644 --- a/software/src/utils/timer.py +++ b/software/src/utils/timer.py @@ -4,6 +4,7 @@ import asyncio import heapq import time +from utils import safe_callback TIMER_DEBUG = True @@ -72,5 +73,7 @@ class TimerManager(object): continue except asyncio.TimeoutError: pass + if len(self.timers) == 0: + continue _, callback = heapq.heappop(self.timers) - callback() + safe_callback(callback, "timer callback") From c0b9ef29618ec445869b52ca8930027a1d329d4c Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Sun, 7 Dec 2025 13:25:35 +0100 Subject: [PATCH 3/4] fix: timer: handle modifications to timers when _timer_worker is already scheduled correctly Signed-off-by: Matthias Blankertz --- software/src/utils/timer.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/software/src/utils/timer.py b/software/src/utils/timer.py index 301a7e6..99d7552 100644 --- a/software/src/utils/timer.py +++ b/software/src/utils/timer.py @@ -73,7 +73,6 @@ class TimerManager(object): continue except asyncio.TimeoutError: pass - if len(self.timers) == 0: - continue - _, callback = heapq.heappop(self.timers) - safe_callback(callback, "timer callback") + else: + _, callback = heapq.heappop(self.timers) + safe_callback(callback, "timer callback") From e0ff9c54bc5b891133a9607cf385605f17f36b05 Mon Sep 17 00:00:00 2001 From: Matthias Blankertz Date: Tue, 16 Dec 2025 20:19:30 +0100 Subject: [PATCH 4/4] refactor: timer: split out two helpers from _timer_worker Signed-off-by: Matthias Blankertz --- software/src/utils/timer.py | 49 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/software/src/utils/timer.py b/software/src/utils/timer.py index 99d7552..ae43f60 100644 --- a/software/src/utils/timer.py +++ b/software/src/utils/timer.py @@ -50,29 +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 + 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")