Files
2026-05-03 14:56:35 +12:00

121 lines
2.7 KiB
Python

import asyncio
import json
from microdot import Microdot
from microdot.websocket import WebSocketError, with_websocket
import espnow
import network
from util import format_mac, parse_mac
app = Microdot()
_esp = None
_known_peers = set()
_ws_clients = set()
def _init_espnow():
global _esp
sta = network.WLAN(network.STA_IF)
sta.active(True)
_esp = espnow.ESPNow()
_esp.active(True)
def _validate_envelope(obj):
if obj.get("v") != "1":
raise ValueError("message.v must be '1'")
devices = obj["devices"]
for address in devices.keys():
parse_mac(address)
return obj
def _send_espnow(address, payload):
if _esp is None:
raise ValueError("espnow is not initialized")
mac = parse_mac(address)
msg = json.dumps(payload, separators=(",", ":")).encode("utf-8")
if mac not in _known_peers:
_esp.add_peer(mac)
_known_peers.add(mac)
_esp.send(mac, msg)
return mac, len(msg)
async def _broadcast_ws(obj):
text = json.dumps(obj)
dead = []
for client in list(_ws_clients):
try:
await client.send(text)
except Exception:
dead.append(client)
for client in dead:
_ws_clients.discard(client)
async def _espnow_receive_loop():
while True:
host, msg = _esp.recv(0)
if not host:
await asyncio.sleep(0.01)
continue
await _broadcast_ws(
{
"from": format_mac(host),
"payload": msg.decode("utf-8"),
}
)
@app.route("/ws")
@with_websocket
async def ws(request, ws):
_ws_clients.add(ws)
while True:
try:
raw = await ws.receive()
except WebSocketError:
break
if not raw:
break
try:
parsed = json.loads(raw)
env = _validate_envelope(parsed)
sent = []
for address, payload in env["devices"].items():
mac, payload_size = _send_espnow(address, payload)
sent.append(
{
"address": format_mac(mac),
"bytes": payload_size,
}
)
except (ValueError, TypeError) as e:
await ws.send(json.dumps({"ok": False, "error": str(e)}))
continue
await ws.send(
json.dumps(
{
"ok": True,
"sent": sent,
}
)
)
_ws_clients.discard(ws)
async def main(port=80):
_init_espnow()
asyncio.create_task(_espnow_receive_loop())
await app.start_server(host="0.0.0.0", port=port)
if __name__ == "__main__":
asyncio.run(main(port=80))