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:
@@ -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
|
||||
Reference in New Issue
Block a user