158 lines
5.2 KiB
Python
158 lines
5.2 KiB
Python
"""HTTP and WebSocket routes for the embedded radar web UI.
|
|
|
|
Every handler is a thin shell over the :class:`WebController` (which forwards to
|
|
the AppWindow's existing buttons) and the :class:`RingBroadcaster` (the single
|
|
frame source). There is no ownership gating: the web UI lives inside the process
|
|
that already owns the hardware, so its controls are simply that process's buttons.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Body, HTTPException, Request, WebSocket, WebSocketDisconnect
|
|
|
|
from python_app.webui.controller import WebActionError, WebController
|
|
from python_app.webui.streaming import RingBroadcaster
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _controller(request: Request) -> WebController:
|
|
return request.app.state.controller
|
|
|
|
|
|
async def _run_action(func, *args) -> None:
|
|
"""Run a (blocking) controller control call off the event loop, mapping failures to 400.
|
|
|
|
The control methods run the desktop action synchronously and raise ``ValueError``
|
|
(rejected input) or ``WebActionError`` (the action itself failed) — both become a
|
|
400 the browser shows, instead of the old silent "ok". Running in a worker thread
|
|
keeps the async event loop free while the GUI thread does the work.
|
|
"""
|
|
try:
|
|
await asyncio.to_thread(func, *args)
|
|
except (ValueError, WebActionError) as exc:
|
|
logger.warning("Web UI action failed: %s", exc)
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@router.get("/api/status")
|
|
async def get_status(request: Request) -> dict:
|
|
return _controller(request).status()
|
|
|
|
|
|
@router.post("/api/start")
|
|
async def post_start(request: Request) -> dict:
|
|
logger.info("Web UI request: start")
|
|
controller = _controller(request)
|
|
await _run_action(controller.start)
|
|
return controller.status()
|
|
|
|
|
|
@router.post("/api/single_capture")
|
|
async def post_single_capture(request: Request) -> dict:
|
|
logger.info("Web UI request: single capture")
|
|
controller = _controller(request)
|
|
await _run_action(controller.single_capture)
|
|
return controller.status()
|
|
|
|
|
|
@router.post("/api/stop")
|
|
async def post_stop(request: Request) -> dict:
|
|
logger.info("Web UI request: stop")
|
|
controller = _controller(request)
|
|
await _run_action(controller.stop)
|
|
return controller.status()
|
|
|
|
|
|
@router.post("/api/start_recording")
|
|
async def post_start_recording(
|
|
request: Request,
|
|
path: str = Body("", embed=True),
|
|
name: str = Body("", embed=True),
|
|
count: int = Body(..., embed=True),
|
|
) -> dict:
|
|
logger.info("Web UI request: start with disk recording (count=%s)", count)
|
|
controller = _controller(request)
|
|
await _run_action(controller.start_recording, path, name, count)
|
|
return controller.status()
|
|
|
|
|
|
@router.post("/api/tmp_reference")
|
|
async def post_tmp_reference(request: Request) -> dict:
|
|
logger.info("Web UI request: capture temporary reference")
|
|
controller = _controller(request)
|
|
await _run_action(controller.capture_tmp_reference)
|
|
return controller.status()
|
|
|
|
|
|
@router.post("/api/remove_last")
|
|
async def post_remove_last(request: Request) -> dict:
|
|
logger.info("Web UI request: remove last measurement")
|
|
controller = _controller(request)
|
|
await _run_action(controller.remove_last_measurement)
|
|
return controller.status()
|
|
|
|
|
|
@router.get("/api/configs")
|
|
async def get_configs(request: Request) -> dict:
|
|
return {"names": _controller(request).list_configs()}
|
|
|
|
|
|
@router.post("/api/load_config")
|
|
async def post_load_config(request: Request, name: str = Body(..., embed=True)) -> dict:
|
|
logger.info("Web UI request: load config %r", name)
|
|
controller = _controller(request)
|
|
await _run_action(controller.load_config, name)
|
|
return controller.status()
|
|
|
|
|
|
@router.post("/api/save_dataset")
|
|
async def post_save_dataset(
|
|
request: Request,
|
|
path: str = Body("", embed=True),
|
|
name: str = Body("", embed=True),
|
|
) -> dict:
|
|
logger.info("Web UI request: save dataset")
|
|
controller = _controller(request)
|
|
await _run_action(controller.save_dataset, path, name)
|
|
return controller.status()
|
|
|
|
|
|
@router.get("/api/live_settings")
|
|
async def get_live_settings(request: Request) -> list:
|
|
return _controller(request).current_live_settings()
|
|
|
|
|
|
@router.post("/api/live_settings")
|
|
async def post_live_settings(request: Request, fields: dict = Body(default={})) -> list:
|
|
try:
|
|
return _controller(request).apply_live_settings(fields)
|
|
except ValueError as exc:
|
|
logger.warning("Web UI rejected live settings update: %s", exc)
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
|
|
@router.websocket("/ws")
|
|
async def ws(websocket: WebSocket) -> None:
|
|
"""Stream frames and status to one client until it disconnects."""
|
|
await websocket.accept()
|
|
broadcaster: RingBroadcaster = websocket.app.state.broadcaster
|
|
queue: asyncio.Queue[dict] = asyncio.Queue(maxsize=1)
|
|
broadcaster.register(queue)
|
|
logger.info("Web UI client connected")
|
|
try:
|
|
while True:
|
|
await websocket.send_json(await queue.get())
|
|
except WebSocketDisconnect:
|
|
logger.info("Web UI client disconnected")
|
|
finally:
|
|
broadcaster.unregister(queue)
|
|
with contextlib.suppress(Exception):
|
|
await websocket.close()
|