refactor(api): complete fastapi migration and related features
Finish native FastAPI controllers, drop vendored microdot, and add Wi-Fi driver runtime, beat SSE, simulated BPM, sequence playback improvements, bridge ESP-NOW sources, UI updates, and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,10 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from http_responses import J, html_response, plain, read_json, send_file
|
||||
from http_session import with_session
|
||||
|
||||
|
||||
import json
|
||||
import secrets
|
||||
|
||||
from microdot import Microdot
|
||||
|
||||
from settings import get_settings
|
||||
from util.bridge_profiles import find_bridge_profile, normalise_bridges
|
||||
@@ -20,7 +24,7 @@ from util.bridge_runtime import (
|
||||
)
|
||||
from util.pi_wifi import list_wifi_interfaces, nmcli_available, scan_wifi
|
||||
|
||||
controller = Microdot()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _bridge_transport(settings) -> str:
|
||||
@@ -44,55 +48,39 @@ def _bridges_payload(settings) -> dict:
|
||||
}
|
||||
|
||||
|
||||
@controller.get("/interfaces")
|
||||
async def wifi_interfaces(request):
|
||||
@router.get("/interfaces")
|
||||
async def wifi_interfaces(request: Request):
|
||||
_ = request
|
||||
if not nmcli_available():
|
||||
return (
|
||||
json.dumps({"ok": False, "error": "nmcli not found (install NetworkManager)"}),
|
||||
503,
|
||||
{"Content-Type": "application/json"},
|
||||
)
|
||||
return (
|
||||
json.dumps({"ok": True, "interfaces": list_wifi_interfaces()}),
|
||||
200,
|
||||
{"Content-Type": "application/json"},
|
||||
)
|
||||
return J({"ok": False, "error": "nmcli not found (install NetworkManager)"}, 503)
|
||||
return J({"ok": True, "interfaces": list_wifi_interfaces()}, 200)
|
||||
|
||||
|
||||
@controller.get("/scan")
|
||||
async def wifi_scan(request):
|
||||
device = (request.args.get("device") or "").strip()
|
||||
@router.get("/scan")
|
||||
async def wifi_scan(request: Request):
|
||||
device = (request.query_params.get("device") or "").strip()
|
||||
if not device:
|
||||
return json.dumps({"error": "device query param required"}), 400, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"error": "device query param required"}, 400)
|
||||
if not nmcli_available():
|
||||
return json.dumps({"ok": False, "error": "nmcli not found"}), 503, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": False, "error": "nmcli not found"}, 503)
|
||||
try:
|
||||
networks = await scan_wifi(device)
|
||||
return json.dumps({"ok": True, "device": device, "networks": networks}), 200, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": True, "device": device, "networks": networks}, 200)
|
||||
except Exception as e:
|
||||
return json.dumps({"ok": False, "error": str(e)}), 500, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": False, "error": str(e)}, 500)
|
||||
|
||||
|
||||
@controller.get("/bridges")
|
||||
async def get_bridges(request):
|
||||
@router.get("/bridges")
|
||||
async def get_bridges(request: Request):
|
||||
_ = request
|
||||
settings = get_settings()
|
||||
return json.dumps(_bridges_payload(settings)), 200, {"Content-Type": "application/json"}
|
||||
return J(_bridges_payload(settings), 200)
|
||||
|
||||
|
||||
@controller.put("/bridges")
|
||||
async def put_bridges(request):
|
||||
@router.put("/bridges")
|
||||
async def put_bridges(request: Request):
|
||||
try:
|
||||
data = request.json or {}
|
||||
data = await read_json(request)
|
||||
settings = get_settings()
|
||||
if "wifi_interface" in data:
|
||||
settings["wifi_interface"] = str(data.get("wifi_interface") or "").strip()
|
||||
@@ -109,62 +97,50 @@ async def put_bridges(request):
|
||||
if "bridges" in data:
|
||||
settings["bridges"] = normalise_bridges(data.get("bridges"))
|
||||
settings.save()
|
||||
return json.dumps({"ok": True, "message": "Bridge profiles saved"}), 200, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": True, "message": "Bridge profiles saved"}, 200)
|
||||
except Exception as e:
|
||||
return json.dumps({"ok": False, "error": str(e)}), 400, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": False, "error": str(e)}, 400)
|
||||
|
||||
|
||||
@controller.delete("/bridges/<bridge_id>")
|
||||
async def delete_bridge_profile(request, bridge_id):
|
||||
@router.delete("/bridges/{bridge_id}")
|
||||
async def delete_bridge_profile(request: Request, bridge_id):
|
||||
_ = request
|
||||
settings = get_settings()
|
||||
bid = str(bridge_id or "").strip()
|
||||
bridges = normalise_bridges(settings.get("bridges"))
|
||||
kept = [b for b in bridges if str(b.get("id") or "") != bid]
|
||||
if len(kept) == len(bridges):
|
||||
return json.dumps({"ok": False, "error": "Bridge profile not found"}), 404, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": False, "error": "Bridge profile not found"}, 404)
|
||||
settings["bridges"] = kept
|
||||
settings.save()
|
||||
payload = _bridges_payload(settings)
|
||||
payload["message"] = "Bridge profile deleted"
|
||||
return json.dumps(payload), 200, {"Content-Type": "application/json"}
|
||||
return J(payload, 200)
|
||||
|
||||
|
||||
@controller.post("/bridges/<bridge_id>/connect")
|
||||
async def connect_saved_bridge(request, bridge_id):
|
||||
@router.post("/bridges/{bridge_id}/connect")
|
||||
async def connect_saved_bridge(request: Request, bridge_id):
|
||||
_ = request
|
||||
settings = get_settings()
|
||||
profile = find_bridge_profile(settings, bridge_id)
|
||||
if not profile:
|
||||
return json.dumps({"error": "Bridge profile not found"}), 404, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"error": "Bridge profile not found"}, 404)
|
||||
try:
|
||||
ok, err = await connect_bridge_profile(profile, settings)
|
||||
if not ok:
|
||||
return json.dumps({"ok": False, "error": err or "Connect failed"}), 400, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": False, "error": err or "Connect failed"}, 400)
|
||||
payload = _bridges_payload(settings)
|
||||
payload["message"] = f"Connected to {profile.get('label')}"
|
||||
return json.dumps(payload), 200, {"Content-Type": "application/json"}
|
||||
return J(payload, 200)
|
||||
except Exception as e:
|
||||
return json.dumps({"ok": False, "error": str(e)}), 500, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": False, "error": str(e)}, 500)
|
||||
|
||||
|
||||
@controller.post("/connect")
|
||||
async def wifi_connect_bridge(request):
|
||||
@router.post("/connect")
|
||||
async def wifi_connect_bridge(request: Request):
|
||||
"""Join a bridge AP and open its WebSocket."""
|
||||
try:
|
||||
data = request.json or {}
|
||||
data = await read_json(request)
|
||||
settings = get_settings()
|
||||
device = str(data.get("device") or settings.get("wifi_interface") or "").strip()
|
||||
ssid = str(data.get("ssid") or "").strip()
|
||||
@@ -177,13 +153,9 @@ async def wifi_connect_bridge(request):
|
||||
label = str(data.get("label") or ssid).strip() or ssid
|
||||
save_profile = bool(data.get("save_profile", True))
|
||||
if not device:
|
||||
return json.dumps({"error": "Wi‑Fi interface (device) is required"}), 400, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"error": "Wi‑Fi interface (device) is required"}, 400)
|
||||
if not ssid:
|
||||
return json.dumps({"error": "ssid is required"}), 400, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"error": "ssid is required"}, 400)
|
||||
settings["wifi_interface"] = device
|
||||
bridges = normalise_bridges(settings.get("bridges"))
|
||||
profile_id = None
|
||||
@@ -217,23 +189,19 @@ async def wifi_connect_bridge(request):
|
||||
}
|
||||
ok, err = await connect_bridge_wifi(profile, settings)
|
||||
if not ok:
|
||||
return json.dumps({"ok": False, "error": err or "Connect failed"}), 400, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": False, "error": err or "Connect failed"}, 400)
|
||||
payload = _bridges_payload(settings)
|
||||
payload["profile_id"] = profile_id
|
||||
payload["message"] = f"Connected to {ssid}"
|
||||
return json.dumps(payload), 200, {"Content-Type": "application/json"}
|
||||
return J(payload, 200)
|
||||
except Exception as e:
|
||||
return json.dumps({"ok": False, "error": str(e)}), 500, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": False, "error": str(e)}, 500)
|
||||
|
||||
|
||||
@controller.post("/serial/connect")
|
||||
async def serial_connect_bridge(request):
|
||||
@router.post("/serial/connect")
|
||||
async def serial_connect_bridge(request: Request):
|
||||
try:
|
||||
data = request.json or {}
|
||||
data = await read_json(request)
|
||||
port = str(data.get("port") or data.get("serial_port") or "").strip()
|
||||
save_profile = bool(data.get("save_profile", True))
|
||||
label = str(data.get("label") or port).strip() or port
|
||||
@@ -242,9 +210,7 @@ async def serial_connect_bridge(request):
|
||||
except (TypeError, ValueError):
|
||||
baud = 921600
|
||||
if not port:
|
||||
return json.dumps({"error": "port is required"}), 400, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"error": "port is required"}, 400)
|
||||
settings = get_settings()
|
||||
bridges = normalise_bridges(settings.get("bridges"))
|
||||
profile_id = None
|
||||
@@ -269,14 +235,10 @@ async def serial_connect_bridge(request):
|
||||
profile = {"transport": "serial", "serial_port": port, "serial_baudrate": baud}
|
||||
ok, err = await connect_bridge_serial(profile, settings)
|
||||
if not ok:
|
||||
return json.dumps({"ok": False, "error": err}), 500, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": False, "error": err}, 500)
|
||||
payload = _bridges_payload(settings)
|
||||
payload["profile_id"] = profile_id
|
||||
payload["message"] = f"Connected on {port}"
|
||||
return json.dumps(payload), 200, {"Content-Type": "application/json"}
|
||||
return J(payload, 200)
|
||||
except Exception as e:
|
||||
return json.dumps({"ok": False, "error": str(e)}), 500, {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
return J({"ok": False, "error": str(e)}, 500)
|
||||
|
||||
Reference in New Issue
Block a user