feat(controller): migrate wifi drivers from tcp to websocket clients

This commit is contained in:
2026-04-14 23:13:26 +12:00
parent f5a7b42e7c
commit 96712dda88
19 changed files with 1195 additions and 673 deletions

View File

@@ -1,10 +1,10 @@
"""Deliver driver JSON messages over serial (ESP-NOW) and/or TCP (Wi-Fi clients)."""
"""Deliver driver JSON messages over serial (ESP-NOW) and/or WebSocket (Wi-Fi drivers)."""
import asyncio
import json
from models.device import normalize_mac
from models.tcp_clients import send_json_line_to_ip
from models.wifi_ws_clients import send_json_line_to_ip
# Serial bridge (ESP32): broadcast MAC + this envelope → firmware unicasts ``body`` to each peer.
_SPLIT_MODE = "split"
@@ -20,7 +20,7 @@ def _split_serial_envelope(inner_json_str, peer_hex_list):
def _wifi_message_for_device(msg, device_name):
"""
For Wi-Fi TCP fanout, narrow a v1 select map to a single device name.
For Wi-Fi WebSocket fanout, narrow a v1 select map to a single device name.
Returns the original message when no narrowing applies.
"""
if not device_name:
@@ -40,6 +40,33 @@ def _wifi_message_for_device(msg, device_name):
return json.dumps(body, separators=(",", ":"))
def _combine_preset_chunks_for_wifi(chunk_messages):
"""Merge chunked v1 preset messages into one v1 JSON string for Wi-Fi."""
merged_presets = {}
save_flag = False
default_id = None
for msg in chunk_messages:
try:
body = json.loads(msg)
except Exception:
continue
if not isinstance(body, dict):
continue
presets = body.get("presets")
if isinstance(presets, dict):
merged_presets.update(presets)
if body.get("save"):
save_flag = True
if body.get("default") is not None:
default_id = body.get("default")
out = {"v": "1", "presets": merged_presets}
if save_flag:
out["save"] = True
if default_id is not None:
out["default"] = default_id
return json.dumps(out, separators=(",", ":"))
async def deliver_preset_broadcast_then_per_device(
sender,
chunk_messages,
@@ -50,8 +77,8 @@ async def deliver_preset_broadcast_then_per_device(
):
"""
Send preset definition chunks: ESP-NOW broadcast once per chunk; same chunk to each
Wi-Fi driver over TCP. If default_id is set, send a per-target default message
(unicast serial or TCP) with targets=[device name] for each registry entry.
Wi-Fi driver over WebSocket. If default_id is set, send a per-target default message
(unicast serial or WebSocket) with targets=[device name] for each registry entry.
"""
if not chunk_messages:
return 0
@@ -72,17 +99,22 @@ async def deliver_preset_broadcast_then_per_device(
wifi_ips.append(str(doc["address"]).strip())
deliveries = 0
wifi_combined_msg = _combine_preset_chunks_for_wifi(chunk_messages)
for msg in chunk_messages:
tasks = [sender.send(msg, addr=_BROADCAST_MAC_HEX)]
for ip in wifi_ips:
if ip:
tasks.append(send_json_line_to_ip(ip, msg))
results = await asyncio.gather(*tasks, return_exceptions=True)
if results and results[0] is True:
deliveries += 1
for r in results[1:]:
if r is True:
await asyncio.sleep(delay_s)
for ip in wifi_ips:
if not ip:
continue
try:
if await send_json_line_to_ip(ip, wifi_combined_msg):
deliveries += 1
except Exception as e:
print(f"[driver_delivery] Wi-Fi preset send failed: {e!r}")
await asyncio.sleep(delay_s)
if default_id:
@@ -98,7 +130,7 @@ async def deliver_preset_broadcast_then_per_device(
if await send_json_line_to_ip(ip, out):
deliveries += 1
except Exception as e:
print(f"[driver_delivery] default TCP failed: {e!r}")
print(f"[driver_delivery] default Wi-Fi send failed: {e!r}")
else:
try:
await sender.send(out, addr=mac)
@@ -112,10 +144,10 @@ async def deliver_preset_broadcast_then_per_device(
async def deliver_json_messages(sender, messages, target_macs, devices_model, delay_s=0.1):
"""
Send each message string to the bridge and/or TCP clients.
Send each message string to the bridge and/or Wi-Fi WebSocket clients.
If target_macs is None or empty: one serial send per message (default/broadcast address).
Otherwise: Wi-Fi uses TCP in parallel. Multiple ESP-NOW peers are sent in **one** serial
Otherwise: Wi-Fi uses WebSocket in parallel. Multiple ESP-NOW peers are sent in **one** serial
write to the ESP32 (broadcast + split envelope); the bridge unicasts ``body`` to each
peer. A single ESP-NOW peer still uses one unicast serial frame. Wi-Fi and serial
tasks run together in one asyncio.gather.