from __future__ import annotations import asyncio from fastapi import APIRouter, WebSocket, WebSocketDisconnect import config from log_tail import LogTailer from routers.system import build_status router = APIRouter() STATUS_PUSH_HZ = 1.0 # slowed from 2.0 — paired with health.py's wider RATE_WINDOW, # the operator asked for a calmer/more trustworthy number over a # snappier one (2026-08-06). LOG_POLL_HZ = 4.0 @router.websocket("/ws/status") async def ws_status(ws: WebSocket): await ws.accept() period = 1.0 / STATUS_PUSH_HZ try: while True: await ws.send_json(build_status(ws.app)) await asyncio.sleep(period) except WebSocketDisconnect: pass @router.websocket("/ws/pointcloud") async def ws_pointcloud(ws: WebSocket, profile: str = "desktop"): """M3 live scan view — binary-framed decimated point cloud, see pointcloud_codec.py for the wire format. `profile` picks desktop vs. phone point/Hz budgets rather than trusting client-supplied numbers directly (a client could otherwise ask for an unbounded frame size).""" await ws.accept() prof = config.POINTCLOUD_PROFILES.get(profile, config.POINTCLOUD_PROFILES["desktop"]) period = 1.0 / prof["hz"] ros = ws.app.state.ros try: while True: await ws.send_bytes(ros.pointcloud.encode_frame(max_points=prof["max_points"])) await asyncio.sleep(period) except WebSocketDisconnect: pass @router.websocket("/ws/logs") async def ws_logs(ws: WebSocket): """Unified log tail — mirrors the old PyQt GUIs' single scrollback pane. Sends recent backlog once on connect, then only newly-appended lines.""" await ws.accept() tailer = LogTailer({ "lidar": config.LIDAR_LOG_PATH, "camera": config.CAMERA_LOG_PATH, "recording": config.RECORDING_LOG_PATH, "fastlio": config.FASTLIO_LOG_PATH, }) backlog = [] for tag in ("lidar", "camera", "recording", "fastlio"): backlog += [{"tag": tag, "line": ln} for ln in tailer.read_tail(tag, max_lines=80)] if backlog: await ws.send_json({"backlog": backlog}) period = 1.0 / LOG_POLL_HZ try: while True: new_lines = [{"tag": tag, "line": line} for tag, line in tailer.read_new()] if new_lines: await ws.send_json({"lines": new_lines}) await asyncio.sleep(period) except WebSocketDisconnect: pass