From 3ee89ce3b4abf9c2873eb1132fbc4292fa0129eb Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sun, 3 May 2026 14:54:12 +1200 Subject: [PATCH] feat(driver): add HTTP routes, startup split, and binary envelope support Wire controller messages through new modules (background tasks, runtime state, startup) and add binary envelope handling. Co-authored-by: Cursor --- src/background_tasks.py | 42 ++++++++ src/binary_envelope.py | 209 +++++++++++++++++++++++++++++++++++++ src/controller_messages.py | 22 ++-- src/http_routes.py | 125 ++++++++++++++++++++++ src/runtime_state.py | 12 +++ src/startup.py | 53 ++++++++++ 6 files changed, 457 insertions(+), 6 deletions(-) create mode 100644 src/background_tasks.py create mode 100644 src/binary_envelope.py create mode 100644 src/http_routes.py create mode 100644 src/runtime_state.py create mode 100644 src/startup.py diff --git a/src/background_tasks.py b/src/background_tasks.py new file mode 100644 index 0000000..32638e6 --- /dev/null +++ b/src/background_tasks.py @@ -0,0 +1,42 @@ +import asyncio +import gc +import utime + +from hello import broadcast_hello_udp + + +async def presets_loop(presets, wdt): + last_mem_log = utime.ticks_ms() + while True: + presets.tick() + wdt.feed() + if bool(getattr(presets, "debug", False)): + now = utime.ticks_ms() + if utime.ticks_diff(now, last_mem_log) >= 5000: + gc.collect() + print("mem runtime:", {"free": gc.mem_free(), "alloc": gc.mem_alloc()}) + last_mem_log = now + # tick() does not await; yield so UDP hello and HTTP/WebSocket can run. + await asyncio.sleep(0) + + +async def udp_hello_loop_after_http_ready(sta_if, settings, wdt, runtime_state): + """Broadcast hello at startup-fast cadence, then slower cadence.""" + await asyncio.sleep(1) + started_ms = utime.ticks_ms() + while True: + if runtime_state.hello: + print("UDP hello: broadcasting...") + try: + broadcast_hello_udp( + sta_if, + settings.get("name", ""), + wait_reply=False, + wdt=wdt, + dual_destinations=True, + ) + except Exception as ex: + print("UDP hello broadcast failed:", ex) + elapsed_ms = utime.ticks_diff(utime.ticks_ms(), started_ms) + interval_s = 5 if elapsed_ms < 60000 else 60 + await asyncio.sleep(interval_s) diff --git a/src/binary_envelope.py b/src/binary_envelope.py new file mode 100644 index 0000000..555be9b --- /dev/null +++ b/src/binary_envelope.py @@ -0,0 +1,209 @@ +"""Decode compact binary controller envelopes — v2 native binary, v1 legacy JSON blobs.""" + +import json +import struct + +BINARY_ENVELOPE_VERSION_1 = 1 +BINARY_ENVELOPE_VERSION_2 = 2 +HEADER_LEN = 5 + + +def _brightness_0_255_from_wire(wire): + w = max(0, min(127, int(wire))) + return min(255, (w * 255) // 127) + + +def _decode_preset_record(buf, off): + nl = buf[off] + off += 1 + name = buf[off : off + nl].decode("utf-8") + off += nl + pl = buf[off] + off += 1 + pattern = buf[off : off + pl].decode("utf-8") + off += pl + nc = buf[off] + off += 1 + colors = [] + for _ in range(nc): + r, g, b = buf[off], buf[off + 1], buf[off + 2] + off += 3 + colors.append("#%02x%02x%02x" % (r, g, b)) + if off + 16 > len(buf): + raise ValueError("truncated") + delay, br, auto, n1, n2, n3, n4, n5, n6 = struct.unpack_from( + "