Initial commit: scan_web integrated recording UI

FastAPI backend + vanilla JS frontend for LiDAR/camera startup, camera
settings, and rosbag recording, unifying the previous scan_gui/scan_gui_dual/
scan_gui_triple desktop tools into one web app.
This commit is contained in:
gardentech
2026-08-07 14:26:00 +09:00
commit f34817d5b6
38 changed files with 5430 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
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