Compare commits
12 Commits
b69f4bd87d
...
webapi-las
| Author | SHA1 | Date | |
|---|---|---|---|
| 32bf6a8d68 | |||
| e2f9287ebd | |||
| 19dff763bd | |||
| 768b630722 | |||
| e0ff9c54bc | |||
| c0b9ef2961 | |||
| e23f8bd34c | |||
| 97e9742c75 | |||
| c687e3a977 | |||
| aa1a02ce54 | |||
| 3e888790e4 | |||
| 96759c999c |
@@ -17,6 +17,7 @@ set -eu
|
||||
)
|
||||
|
||||
BUILDDIR=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/
|
||||
BUILDDIR_UNIX=lib/micropython/ports/unix/build-tonberry_unix/
|
||||
OUTDIR=$(pwd)/build
|
||||
mkdir -p "$OUTDIR"
|
||||
FS_STAGE_DIR=$(mktemp -d)
|
||||
@@ -51,5 +52,9 @@ for hwconfig in boards/RPI_PICO_W/manifest-*.py; do
|
||||
$PICOTOOL uf2 convert $BUILDDIR/firmware-filesystem.bin "$OUTDIR"/firmware-filesystem-"$hwname".uf2
|
||||
done
|
||||
|
||||
cp "$BUILDDIR_UNIX"/micropython "$OUTDIR"/micropython-tonberry_unix
|
||||
chmod u+x "$OUTDIR"/micropython-tonberry_unix
|
||||
|
||||
echo "Output in" "${OUTDIR}"/firmware-*.uf2
|
||||
echo "Images with filesystem in" "${OUTDIR}"/firmware-filesystem-*.uf2
|
||||
echo "Unix build in" "${OUTDIR}"/micropython-tonberry_unix
|
||||
|
||||
@@ -58,6 +58,7 @@ class PlayerApp:
|
||||
self.mp3file = None
|
||||
self.volume_pos = 3
|
||||
self.paused = False
|
||||
self.playing = False
|
||||
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
|
||||
self._onIdle()
|
||||
|
||||
@@ -115,6 +116,13 @@ 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
|
||||
|
||||
def _set_playlist(self, tag: bytes):
|
||||
if self.playlist is not None:
|
||||
pos = self.player.stop()
|
||||
@@ -176,7 +184,12 @@ class PlayerApp:
|
||||
def _onIdle(self):
|
||||
self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout)
|
||||
self.leds.set_state(self.leds.IDLE)
|
||||
self.playing = False
|
||||
|
||||
def _onActive(self):
|
||||
self.timer_manager.cancel(self.onIdleTimeout)
|
||||
self.leds.set_state(self.leds.PLAYING)
|
||||
self.playing = True
|
||||
|
||||
def get_nfc(self):
|
||||
return self.nfc
|
||||
|
||||
@@ -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()
|
||||
@@ -65,7 +72,6 @@ def run():
|
||||
|
||||
# Wifi with default config
|
||||
setup_wifi()
|
||||
start_webserver(config)
|
||||
|
||||
# Setup MP3 player
|
||||
with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS,
|
||||
@@ -96,9 +102,12 @@ def run():
|
||||
config=lambda _: config)
|
||||
the_app = app.PlayerApp(deps)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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")
|
||||
|
||||
12
software/src/utils/helpers.py
Normal file
12
software/src/utils/helpers.py
Normal 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)
|
||||
@@ -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")
|
||||
|
||||
@@ -10,12 +10,23 @@ from microdot import Microdot
|
||||
webapp = Microdot()
|
||||
server = None
|
||||
config = None
|
||||
app = None
|
||||
nfc = None
|
||||
|
||||
|
||||
def start_webserver(config_):
|
||||
global server, config
|
||||
def start_webserver(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('/')
|
||||
@@ -55,3 +66,9 @@ async def config_put(request):
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user