Pi port: serial transport, addressed ESP-NOW bridge, port 80

- Run app on Raspberry Pi: serial to ESP32 bridge at 912000 baud, /dev/ttyS0
- Remove ESP-NOW/MicroPython-only code from src (espnow, p2p, wifi, machine/Pin)
- Transport: always send 6-byte MAC + payload; optional to/destination_mac in API and WebSocket
- Settings and model DB use project paths (no root); fix sys.print_exception for CPython
- Preset/settings controllers use get_current_sender(); template paths for cwd=src
- Pipfile: run from src, PORT from env; scripts for port 80 (setcap) and test
- ESP32 bridge: receive 6-byte addr + payload, LRU peer management (20 max), handle ESP_ERR_ESPNOW_EXIST
- Add esp32/main.py, esp32/benchmark_peers.py, scripts/setup-port80.sh, scripts/test-port80.sh

Made-with: Cursor
This commit is contained in:
2026-03-15 17:16:07 +13:00
parent 0fdc11c0b0
commit ac9fca8d4b
19 changed files with 656 additions and 500 deletions

View File

@@ -1,14 +1,11 @@
import asyncio
import gc
import json
import machine
from machine import Pin
import os
from microdot import Microdot, send_file
from microdot.websocket import with_websocket
from microdot.session import Session
from settings import Settings
import aioespnow
import controllers.preset as preset
import controllers.profile as profile
import controllers.group as group
@@ -18,7 +15,7 @@ import controllers.palette as palette
import controllers.scene as scene
import controllers.pattern as pattern
import controllers.settings as settings_controller
from models.espnow import ESPNow
from models.transport import get_sender, set_sender
async def main(port=80):
@@ -26,8 +23,9 @@ async def main(port=80):
print(settings)
print("Starting")
# Initialize ESPNow singleton (config + peers)
esp = ESPNow()
# Initialize transport (serial to ESP32 bridge)
sender = get_sender(settings)
set_sender(sender)
app = Microdot()
@@ -58,7 +56,7 @@ async def main(port=80):
app.mount(pattern.controller, '/patterns')
app.mount(settings_controller.controller, '/settings')
# Serve index.html at root
# Serve index.html at root (cwd is src/ when run via pipenv run run)
@app.route('/')
def index(request):
"""Serve the main web UI."""
@@ -91,19 +89,25 @@ async def main(port=80):
data = await ws.receive()
print(data)
if data:
# Debug: log incoming WebSocket data
try:
parsed = json.loads(data)
print("WS received JSON:", parsed)
except Exception:
print("WS received raw:", data)
# Forward raw JSON payload over ESPNow to configured peers
try:
await esp.send(data)
# Optional "to": 12-char hex MAC; rest is payload (sent with that address).
addr = parsed.pop("to", None)
payload = json.dumps(parsed) if parsed else data
await sender.send(payload, addr=addr)
except json.JSONDecodeError:
# Not JSON: send raw with default address
try:
await sender.send(data)
except Exception:
try:
await ws.send(json.dumps({"error": "Send failed"}))
except Exception:
pass
except Exception:
try:
await ws.send(json.dumps({"error": "ESP-NOW send failed"}))
await ws.send(json.dumps({"error": "Send failed"}))
except Exception:
pass
else:
@@ -113,25 +117,11 @@ async def main(port=80):
server = asyncio.create_task(app.start_server(host="0.0.0.0", port=port))
#wdt = machine.WDT(timeout=10000)
#wdt.feed()
# Initialize heartbeat LED (XIAO ESP32S3 built-in LED on GPIO 21)
led = Pin(15, Pin.OUT)
led_state = False
while True:
gc.collect()
for i in range(60):
#wdt.feed()
# Heartbeat: toggle LED every 500 ms
led.value(not led.value())
await asyncio.sleep_ms(500)
await asyncio.sleep(30)
# cleanup before ending the application
if __name__ == "__main__":
asyncio.run(main())
import os
port = int(os.environ.get("PORT", 80))
asyncio.run(main(port=port))