feat(bridge): add wifi/serial bridge runtime and UI

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-28 00:38:21 +12:00
parent 2cf019079e
commit 78dc8ffc77
92 changed files with 5679 additions and 1790 deletions

View File

@@ -2,7 +2,7 @@
import asyncio
import json
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Set, Union
from util.bridge_envelope import (
BROADCAST_MAC,
@@ -12,15 +12,9 @@ from util.bridge_envelope import (
split_v1_body_for_espnow,
)
from util.espnow_message import build_message
from util.espnow_wire import WIRE_MAGIC, pack_group_cmd
_MAX_JSON_ESPNOW = 240
def v1_message_bytes(body: Dict[str, Any]) -> bytes:
return json.dumps(body, separators=(",", ":")).encode("utf-8")
def _body_from_message(msg: Union[str, bytes, bytearray, Dict[str, Any]]) -> Optional[Dict[str, Any]]:
if isinstance(msg, dict):
if msg.get("v") == "1" and "devices" not in msg:
@@ -44,17 +38,7 @@ def _body_from_message(msg: Union[str, bytes, bytearray, Dict[str, Any]]) -> Opt
return None
async def deliver_envelope(sender, envelope: Dict[str, Any], delay_s: float = 0.1) -> int:
if not envelope or not isinstance(envelope.get("devices"), dict):
return 0
if await sender.send(envelope):
if delay_s > 0:
await asyncio.sleep(delay_s)
return 1
return 0
async def _deliver_v1_body(sender, mac_key: str, body: Dict[str, Any], delay_s: float) -> int:
async def _deliver_v1_body(bridge, mac_key: str, body: Dict[str, Any], delay_s: float) -> int:
deliveries = 0
try:
chunks = split_v1_body_for_espnow(body)
@@ -62,76 +46,13 @@ async def _deliver_v1_body(sender, mac_key: str, body: Dict[str, Any], delay_s:
return 0
for chunk in chunks:
env = build_devices_envelope({mac_key: chunk})
if await sender.send(env):
if await bridge.send(env):
deliveries += 1
if delay_s > 0:
await asyncio.sleep(delay_s)
return deliveries
async def deliver_packets(
sender,
packets: List[bytes],
*,
delay_s: float = 0.1,
target_macs: Optional[List[str]] = None,
unicast: bool = False,
) -> int:
if not packets:
return 0
deliveries = 0
mac_keys = _unicast_mac_keys(target_macs) if unicast and target_macs else [BROADCAST_MAC]
for mac_key in mac_keys:
for pkt in packets:
body = _body_from_message(pkt)
if body:
deliveries += await _deliver_v1_body(sender, mac_key, body, delay_s)
else:
if await sender.send(pkt):
deliveries += 1
if delay_s > 0:
await asyncio.sleep(delay_s)
return deliveries
async def deliver_binary_packets(
sender,
packets: List[bytes],
target_macs: Optional[List[str]] = None,
*,
delay_s: float = 0.1,
unicast: bool = False,
) -> int:
return await deliver_packets(
sender, packets, delay_s=delay_s, target_macs=target_macs, unicast=unicast
)
async def deliver_group_binary_packets(
sender,
group_id: str,
packets: List[bytes],
*,
delay_s: float = 0.1,
) -> int:
"""Broadcast GROUP_CMD wire packets (legacy binary passthrough on bridge)."""
from util.espnow_wire import parse_cmd
deliveries = 0
for pkt in packets:
env, save = parse_cmd(pkt)
if env is None:
continue
try:
g_pkt = pack_group_cmd(str(group_id), env, save=save)
except ValueError:
continue
if await sender.send(g_pkt):
deliveries += 1
await asyncio.sleep(delay_s)
return deliveries
def build_preset_json_chunks(
presets_by_name: Dict[str, Any],
*,
@@ -174,29 +95,6 @@ def build_preset_json_chunks(
return [c for c in chunks if c]
async def deliver_preset_broadcast_then_per_device(
sender,
chunk_messages,
target_macs,
devices_model,
default_id,
delay_s=0.1,
):
del devices_model, target_macs
deliveries = 0
for msg in chunk_messages:
body = _body_from_message(msg)
if not body:
continue
deliveries += await _deliver_v1_body(sender, BROADCAST_MAC, body, delay_s)
if default_id:
body = {"default": str(default_id), "save": True}
deliveries += await _deliver_v1_body(sender, BROADCAST_MAC, body, delay_s)
return deliveries
def _unicast_mac_keys(target_macs: Optional[List[str]]) -> List[str]:
"""One formatted MAC per target; empty list means broadcast."""
if not target_macs:
@@ -212,7 +110,7 @@ def _unicast_mac_keys(target_macs: Optional[List[str]]) -> List[str]:
async def deliver_json_messages(
sender,
bridge,
messages,
target_macs,
devices_model,
@@ -224,17 +122,27 @@ async def deliver_json_messages(
Deliver v1 JSON to drivers. Default: ESP-NOW broadcast (``ff:ff:…``); drivers
filter on ``groups`` in the body. Set ``unicast=True`` only for per-device settings
or single-device identify.
Uses the current bridge connection only (per-group bridge assignment is disabled).
"""
del devices_model
deliveries = 0
from models.transport import get_current_bridge
active = get_current_bridge() or bridge
if active is None:
raise RuntimeError("Transport not configured")
if unicast and target_macs:
mac_keys = _unicast_mac_keys(target_macs)
else:
mac_keys = [BROADCAST_MAC]
deliveries = 0
for mac_key in mac_keys:
for msg in messages:
body = _body_from_message(msg)
if not body:
continue
deliveries += await _deliver_v1_body(sender, mac_key, body, delay_s)
deliveries += await _deliver_v1_body(active, mac_key, body, delay_s)
return deliveries, len(messages)