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
+54
View File
@@ -0,0 +1,54 @@
"""Tails the lidar/camera/recording log files for the browser log panel.
The original PyQt5 scan GUIs had a single unified QPlainTextEdit showing all
subprocess stdout with a [tag] prefix — this is the web equivalent. Each
ManagedProcess re-opens its log file in "w" mode on every start (see
process_manager.py), so a tailer must detect truncation (file shrank since
last read) and reset to the top rather than seeking past EOF forever.
"""
from __future__ import annotations
from pathlib import Path
from typing import Iterable
class LogTailer:
def __init__(self, sources: dict[str, Path]):
self._sources = sources
self._offsets: dict[str, int] = {tag: 0 for tag in sources}
def read_tail(self, tag: str, max_lines: int = 200) -> list[str]:
"""Backlog for a fresh websocket connection — last N lines, and
advances this tag's offset to end-of-file so read_new() only
reports genuinely new lines afterwards."""
path = self._sources.get(tag)
if path is None or not path.exists():
return []
try:
with open(path, "r", errors="replace") as f:
lines = f.readlines()
self._offsets[tag] = f.tell()
except OSError:
return []
return [ln.rstrip("\n") for ln in lines[-max_lines:]]
def read_new(self) -> Iterable[tuple[str, str]]:
"""Yields (tag, line) for every line appended since the last call."""
for tag, path in self._sources.items():
if not path.exists():
continue
try:
size = path.stat().st_size
if size < self._offsets.get(tag, 0):
self._offsets[tag] = 0 # file was truncated (process restarted)
with open(path, "r", errors="replace") as f:
f.seek(self._offsets[tag])
new_text = f.read()
self._offsets[tag] = f.tell()
except OSError:
continue
if not new_text:
continue
for line in new_text.splitlines():
if line:
yield tag, line