"""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, }