From 7179b6531efa108393ba035424c962582f47fd29 Mon Sep 17 00:00:00 2001 From: pi Date: Mon, 6 Apr 2026 21:28:13 +1200 Subject: [PATCH] feat(controller): udp hello discovery and remove tcp registration Made-with: Cursor --- led-driver | 2 +- src/main.py | 67 +++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/led-driver b/led-driver index cef9e00..aaaf660 160000 --- a/led-driver +++ b/led-driver @@ -1 +1 @@ -Subproject commit cef9e00819dc1a95f088a6d2ee8538a6f83552b0 +Subproject commit aaaf660e9d1c380697e6c30046b30bcbd893e03f diff --git a/src/main.py b/src/main.py index 148bc6f..5962ceb 100644 --- a/src/main.py +++ b/src/main.py @@ -35,6 +35,7 @@ _tcp_device_lock = threading.Lock() # Wi-Fi drivers send one hello line then stay quiet; periodic outbound data makes dead peers # fail drain() within this interval (keepalive alone is often slow or ineffective). TCP_LIVENESS_PING_INTERVAL_S = 12.0 +DISCOVERY_UDP_PORT = 8766 # Keepalive or lossy Wi-Fi can still surface OSError(110) / TimeoutError on recv or wait_closed. _TCP_PEER_GONE = ( @@ -108,7 +109,7 @@ async def _tcp_liveness_ping_loop(writer, peer_ip: str) -> None: return -def _register_tcp_device_sync( +def _register_udp_device_sync( device_name: str, peer_ip: str, mac, device_type=None ) -> None: with _tcp_device_lock: @@ -119,13 +120,65 @@ def _register_tcp_device_sync( ) if did: print( - f"TCP device registered: mac={did} name={device_name!r} ip={peer_ip!r}" + f"UDP device registered: mac={did} name={device_name!r} ip={peer_ip!r}" ) except Exception as e: - print(f"TCP device registry failed: {e}") + print(f"UDP device registry failed: {e}") traceback.print_exception(type(e), e, e.__traceback__) +async def _handle_udp_discovery(sock) -> None: + while True: + try: + data, addr = await asyncio.get_running_loop().sock_recvfrom(sock, 2048) + except asyncio.CancelledError: + raise + except Exception as e: + print(f"[UDP] recv failed: {e!r}") + continue + peer_ip = addr[0] if addr else "" + line = data.split(b"\n", 1)[0].strip() + if line: + try: + parsed = json.loads(line.decode("utf-8")) + if isinstance(parsed, dict): + dns = str(parsed.get("device_name") or "").strip() + mac = parsed.get("mac") or parsed.get("device_mac") or parsed.get( + "sta_mac" + ) + device_type = parsed.get("type") or parsed.get("device_type") + if dns and normalize_mac(mac): + _register_udp_device_sync(dns, peer_ip, mac, device_type) + except (UnicodeError, ValueError, TypeError): + pass + try: + await asyncio.get_running_loop().sock_sendto(sock, data, addr) + except Exception as e: + print(f"[UDP] echo send failed: {e!r}") + + +async def _run_udp_discovery_server() -> None: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setblocking(False) + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + except (AttributeError, OSError): + pass + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + except (AttributeError, OSError): + pass + sock.bind(("0.0.0.0", DISCOVERY_UDP_PORT)) + print(f"UDP discovery listening on 0.0.0.0:{DISCOVERY_UDP_PORT}") + try: + await _handle_udp_discovery(sock) + finally: + try: + sock.close() + except Exception: + pass + + async def _handle_tcp_client(reader, writer): """Read newline-delimited JSON from Wi-Fi LED drivers; forward to serial bridge.""" peer = writer.get_extra_info("peername") @@ -173,13 +226,6 @@ async def _handle_tcp_client(reader, writer): pass continue if isinstance(parsed, dict): - dns = str(parsed.get("device_name") or "").strip() - mac = parsed.get("mac") or parsed.get("device_mac") or parsed.get("sta_mac") - device_type = parsed.get("type") or parsed.get("device_type") - if dns and normalize_mac(mac): - _register_tcp_device_sync( - dns, peer_ip, mac, device_type=device_type - ) addr = parsed.pop("to", None) payload = json.dumps(parsed) if parsed else "{}" if sender: @@ -356,6 +402,7 @@ async def main(port=80): await asyncio.gather( app.start_server(host="0.0.0.0", port=port), _run_tcp_server(settings), + _run_udp_discovery_server(), ) except OSError as e: if e.errno == errno.EADDRINUSE: