Files
led-controller/src/util/espnow_ping.py
2026-05-28 00:38:21 +12:00

87 lines
2.5 KiB
Python

"""ESP-NOW broadcast ping: collect PING_RSP from drivers."""
from __future__ import annotations
import asyncio
import secrets
import time
from typing import Any, Dict, Optional
from models.device import Device
from models.transport import get_current_bridge
from util.espnow_wire import pack_ping_req, parse_ping_rsp
_active: Dict[int, Dict[str, Any]] = {}
def register_device_from_ping(peer_mac: bytes, name: str) -> bool:
"""Add or update registry entry from a PING_RSP (drivers may not have sent ANNOUNCE yet)."""
if not peer_mac or len(peer_mac) != 6:
return False
mac_hex = peer_mac.hex()
label = (name or "").strip() or f"led-{mac_hex}"
did, persisted = Device().upsert_espnow_announced(mac_hex, label)
if did and persisted:
print(f"[espnow] registered mac={did} name={label!r} (ping)")
return bool(persisted)
def record_ping_rsp(peer_mac: bytes, packet: bytes) -> None:
info = parse_ping_rsp(packet)
if info is None:
return
session = _active.get(info["ping_id"])
if session is None:
return
mac_hex = peer_mac.hex()
session["responses"][mac_hex] = {
"mac": mac_hex,
"name": info["name"],
"rtt_ms": int((time.monotonic() - session["sent_at"]) * 1000),
}
if register_device_from_ping(peer_mac, info["name"]):
session["registered"] = int(session.get("registered", 0)) + 1
async def run_ping(*, timeout_s: float = 3.0) -> Dict[str, Any]:
"""
Broadcast PING_REQ and collect PING_RSP until ``timeout_s``.
Returns ``{ok, ping_id, timeout_s, responses}``; ``responses`` maps MAC hex to
``{mac, name, rtt_ms}``.
"""
bridge = get_current_bridge()
if bridge is None:
return {"ok": False, "error": "Transport not configured", "responses": {}}
ping_id = secrets.randbits(32) or 1
session: Dict[str, Any] = {
"responses": {},
"sent_at": time.monotonic(),
"registered": 0,
}
_active[ping_id] = session
pkt = pack_ping_req(ping_id)
ok = await bridge.send(pkt)
if not ok:
_active.pop(ping_id, None)
return {
"ok": False,
"error": "Send failed",
"ping_id": ping_id,
"responses": {},
}
await asyncio.sleep(timeout_s)
responses = dict(session["responses"])
registered = int(session.get("registered", 0))
_active.pop(ping_id, None)
return {
"ok": True,
"ping_id": ping_id,
"timeout_s": timeout_s,
"responses": responses,
"registered": registered,
}