feat(espnow): add wire transport and simplify broadcast main
Binary espnow_wire/espnow_transport modules plus a minimal main that broadcasts a JSON hello and polls ESP-NOW while running presets. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
171
src/main.py
171
src/main.py
@@ -1,33 +1,24 @@
|
||||
import print_timestamp # noqa: F401 — prefixes every print with [ticks_ms]
|
||||
import print_timestamp # noqa: F401
|
||||
from settings import Settings
|
||||
import machine
|
||||
import utime
|
||||
import asyncio
|
||||
import json
|
||||
import gc
|
||||
from microdot import Microdot
|
||||
from microdot.websocket import WebSocketError, with_websocket
|
||||
import network
|
||||
import espnow
|
||||
from presets import Presets
|
||||
from controller_messages import apply_startup_pattern, process_data
|
||||
from runtime_state import RuntimeState
|
||||
from background_tasks import presets_loop, udp_hello_loop_after_http_ready
|
||||
from controller_messages import apply_startup_pattern
|
||||
from background_tasks import presets_loop
|
||||
from espnow_transport import espnow_receive_loop, init_espnow, send_boot_announce
|
||||
from mem_stats import print_mem
|
||||
from wifi_sta import boot_sta
|
||||
try:
|
||||
import uos as os
|
||||
except ImportError:
|
||||
import os
|
||||
import json
|
||||
|
||||
wdt = machine.WDT(timeout=10000)
|
||||
wdt.feed()
|
||||
|
||||
machine.freq(160000000)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
gc.collect()
|
||||
sta_if = boot_sta(settings, wdt)
|
||||
|
||||
presets = Presets(settings["led_pin"], settings["num_leds"])
|
||||
presets.load(settings)
|
||||
@@ -37,130 +28,32 @@ gc.collect()
|
||||
|
||||
apply_startup_pattern(settings, presets)
|
||||
|
||||
sta_if = network.WLAN(network.STA_IF)
|
||||
sta_if.active(True)
|
||||
print(sta_if.ifconfig())
|
||||
print(sta_if.config("channel"))
|
||||
|
||||
def _print_network_ips(controller_ip=None):
|
||||
"""Always log STA address and led-controller (WS client) address when known."""
|
||||
try:
|
||||
led_ip = sta_if.ifconfig()[0]
|
||||
except Exception:
|
||||
led_ip = "?"
|
||||
ctrl = controller_ip if controller_ip else "(not connected)"
|
||||
print("led-driver IP:", led_ip, " led-controller IP:", ctrl)
|
||||
|
||||
|
||||
_print_network_ips()
|
||||
print_mem("startup")
|
||||
|
||||
runtime_state = RuntimeState()
|
||||
|
||||
app = Microdot()
|
||||
|
||||
|
||||
def _safe_pattern_filename(name):
|
||||
if not isinstance(name, str):
|
||||
return False
|
||||
if not name.endswith(".py"):
|
||||
return False
|
||||
if "/" in name or "\\" in name or ".." in name:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@app.route("/ws")
|
||||
@with_websocket
|
||||
async def ws_handler(request, ws):
|
||||
runtime_state.ws_connected()
|
||||
controller_ip = None
|
||||
try:
|
||||
client_addr = getattr(request, "client_addr", None)
|
||||
if isinstance(client_addr, (tuple, list)) and client_addr:
|
||||
controller_ip = client_addr[0]
|
||||
elif isinstance(client_addr, str):
|
||||
controller_ip = client_addr
|
||||
except Exception:
|
||||
controller_ip = None
|
||||
_print_network_ips(controller_ip)
|
||||
print_mem("ws connect")
|
||||
try:
|
||||
while True:
|
||||
data = await ws.receive()
|
||||
if not data:
|
||||
break
|
||||
process_data(data, settings, presets, controller_ip=controller_ip)
|
||||
except WebSocketError as e:
|
||||
print("WS client disconnected:", e)
|
||||
except OSError as e:
|
||||
print("WS client dropped (OSError):", e)
|
||||
finally:
|
||||
runtime_state.ws_disconnected()
|
||||
|
||||
|
||||
@app.post("/patterns/upload")
|
||||
async def upload_pattern(request):
|
||||
"""Receive one pattern file body from led-controller and reload patterns."""
|
||||
raw_name = request.args.get("name")
|
||||
reload_raw = request.args.get("reload", "1")
|
||||
reload_patterns = str(reload_raw).strip().lower() not in ("0", "false", "no", "off")
|
||||
|
||||
if not isinstance(raw_name, str) or not raw_name.strip():
|
||||
return json.dumps({"error": "name is required"}), 400, {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
body = request.body
|
||||
if not isinstance(body, (bytes, bytearray)) or not body:
|
||||
return json.dumps({"error": "code is required"}), 400, {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
try:
|
||||
code = body.decode("utf-8")
|
||||
except UnicodeError:
|
||||
return json.dumps({"error": "body must be utf-8 text"}), 400, {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
if not code.strip():
|
||||
return json.dumps({"error": "code is required"}), 400, {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
name = raw_name.strip()
|
||||
if not name.endswith(".py"):
|
||||
name += ".py"
|
||||
if not _safe_pattern_filename(name) or name in ("__init__.py", "main.py"):
|
||||
return json.dumps({"error": "invalid pattern filename"}), 400, {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
try:
|
||||
os.mkdir("patterns")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
path = "patterns/" + name
|
||||
try:
|
||||
with open(path, "w") as f:
|
||||
f.write(code)
|
||||
if reload_patterns:
|
||||
presets.reload_patterns()
|
||||
except OSError as e:
|
||||
print("patterns/upload failed:", e)
|
||||
return json.dumps({"error": str(e)}), 500, {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
return json.dumps({
|
||||
"message": "pattern uploaded",
|
||||
"name": name,
|
||||
"reloaded": reload_patterns,
|
||||
}), 201, {"Content-Type": "application/json"}
|
||||
|
||||
|
||||
async def main(port=80):
|
||||
asyncio.create_task(presets_loop(presets, wdt))
|
||||
asyncio.create_task(
|
||||
udp_hello_loop_after_http_ready(sta_if, settings, wdt, runtime_state)
|
||||
)
|
||||
await app.start_server(host="0.0.0.0", port=port)
|
||||
esp = espnow.ESPNow()
|
||||
esp.active(True)
|
||||
esp.add_peer(b"\xff\xff\xff\xff\xff\xff")
|
||||
|
||||
hello = json.dumps({
|
||||
"v": "1",
|
||||
"settings": settings,
|
||||
"type": "led",
|
||||
})
|
||||
esp.send(b"\xff\xff\xff\xff\xff\xff", hello)
|
||||
print(hello)
|
||||
|
||||
|
||||
async def main():
|
||||
while True:
|
||||
presets.tick()
|
||||
wdt.feed()
|
||||
if esp.any():
|
||||
host, msg = esp.recv(0)
|
||||
print(host, msg)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main(port=80))
|
||||
asyncio.run(main())
|
||||
|
||||
Reference in New Issue
Block a user