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,138 @@
|
||||
// Renders the top-bar health badges + startup/recording state lines from /ws/status.
|
||||
import { formatBytes, formatElapsed } from "/js/format.js";
|
||||
|
||||
function badgeClass(health) {
|
||||
if (health === "OK") return "badge badge-ok";
|
||||
if (health === "STALE") return "badge badge-stale";
|
||||
return "badge badge-down";
|
||||
}
|
||||
|
||||
// NBSP padding, not CSS min-width: regular spaces collapse in rendered
|
||||
// text, so a guessed ch-width can still drift. Padding to a fixed character
|
||||
// count (monospace font) makes each badge's pixel width identical on every
|
||||
// tick regardless of health word or Hz digit count — nothing to reflow the
|
||||
// row on mobile.
|
||||
function padHealth(health) {
|
||||
return health.padEnd(5, " ");
|
||||
}
|
||||
|
||||
function formatHz(hz) {
|
||||
return hz.toFixed(1).padStart(5, " ");
|
||||
}
|
||||
|
||||
function makeBadge(label, health, hz) {
|
||||
const b = document.createElement("span");
|
||||
// badge-sensor: fixed CSS width (see style.css) — the NBSP padding above
|
||||
// keeps internal jitter down, but the box's own size being fixed is what
|
||||
// actually guarantees the position never moves, regardless of icon glyph
|
||||
// width or font-metric rounding.
|
||||
b.className = `${badgeClass(health)} badge-sensor`;
|
||||
b.textContent = `${label} ${padHealth(health)}`;
|
||||
const hzSpan = document.createElement("span");
|
||||
hzSpan.className = "badge-hz";
|
||||
hzSpan.textContent = health !== "DOWN" && hz > 0
|
||||
? `${formatHz(hz)}Hz`
|
||||
: `${" ".repeat(5)}Hz`;
|
||||
b.appendChild(hzSpan);
|
||||
return b;
|
||||
}
|
||||
|
||||
export function startStatusSocket(onStatus) {
|
||||
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
let ws;
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(`${proto}//${location.host}/ws/status`);
|
||||
ws.onmessage = (ev) => {
|
||||
const status = JSON.parse(ev.data);
|
||||
render(status);
|
||||
if (onStatus) onStatus(status);
|
||||
};
|
||||
ws.onclose = () => setTimeout(connect, 1500);
|
||||
ws.onerror = () => ws.close();
|
||||
}
|
||||
connect();
|
||||
}
|
||||
|
||||
function render(status) {
|
||||
const badges = document.getElementById("status-badges");
|
||||
badges.innerHTML = "";
|
||||
|
||||
badges.appendChild(makeBadge("LiDAR", status.lidar.health, status.lidar.hz));
|
||||
badges.appendChild(makeBadge("IMU", status.lidar.imu_health, status.lidar.imu_hz));
|
||||
badges.appendChild(makeBadge("MAP", status.map.health, status.map.hz));
|
||||
for (const cam of status.cameras) {
|
||||
badges.appendChild(makeBadge(cam.id, cam.health, cam.hz));
|
||||
}
|
||||
|
||||
if (status.recording.active) {
|
||||
const b = document.createElement("span");
|
||||
b.className = "badge badge-rec";
|
||||
b.textContent = "REC";
|
||||
badges.appendChild(b);
|
||||
}
|
||||
|
||||
const lidarState = document.getElementById("lidar-state");
|
||||
if (lidarState) {
|
||||
lidarState.textContent = status.lidar.running ? "● 실행 중" : "● 정지";
|
||||
lidarState.className = "state-line " + (status.lidar.running ? "on" : "off");
|
||||
}
|
||||
|
||||
const recPath = document.getElementById("rec-path");
|
||||
if (recPath) {
|
||||
recPath.textContent = status.recording.path ? `저장: ${status.recording.path}` : "";
|
||||
}
|
||||
const recMeter = document.getElementById("rec-meter");
|
||||
if (recMeter) {
|
||||
if (status.recording.active) {
|
||||
recMeter.textContent = `${formatElapsed(status.recording.elapsed_s)} · ${formatBytes(status.recording.size_bytes)}`;
|
||||
recMeter.classList.remove("hidden");
|
||||
} else {
|
||||
recMeter.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Always-on compact readout in the HUD — the backend already resolves the
|
||||
// right path (DEFAULT_SAVE_DIR when idle, the live recording target while
|
||||
// active), so this can just mirror status.disk unconditionally.
|
||||
const diskEl = document.getElementById("disk-free");
|
||||
if (diskEl && status.disk) {
|
||||
const freeGb = status.disk.free_gb;
|
||||
diskEl.textContent = `여유 ${freeGb.toFixed(1)}GB`;
|
||||
diskEl.classList.remove("warning", "danger");
|
||||
if (freeGb < status.disk.low_danger_gb) diskEl.classList.add("danger");
|
||||
else if (freeGb < status.disk.low_warning_gb) diskEl.classList.add("warning");
|
||||
}
|
||||
|
||||
const mapPointsEl = document.getElementById("map-points");
|
||||
if (mapPointsEl && status.map) {
|
||||
mapPointsEl.textContent = status.map.points > 0 ? `${status.map.points.toLocaleString()}pt` : "";
|
||||
mapPointsEl.classList.toggle("hidden", status.map.points === 0);
|
||||
}
|
||||
|
||||
document.getElementById("btn-lidar-start").disabled = status.lidar.running;
|
||||
document.getElementById("btn-lidar-stop").disabled = !status.lidar.running;
|
||||
|
||||
// The record button swaps for a stop button once recording is live,
|
||||
// rather than just disabling both — HUD real estate is tight on a phone.
|
||||
const recOpenBtn = document.getElementById("btn-rec-open");
|
||||
const recStopBtn = document.getElementById("btn-rec-stop");
|
||||
recOpenBtn.classList.toggle("hidden", status.recording.active);
|
||||
recStopBtn.classList.toggle("hidden", !status.recording.active);
|
||||
recOpenBtn.disabled = !status.lidar.running;
|
||||
|
||||
const fastlioTestState = document.getElementById("fastlio-test-state");
|
||||
const fastlioTestStart = document.getElementById("btn-fastlio-test-start");
|
||||
const fastlioTestStop = document.getElementById("btn-fastlio-test-stop");
|
||||
if (fastlioTestState && status.map) {
|
||||
const running = !!(status.process_status && status.process_status.fastlio && status.process_status.fastlio.running);
|
||||
fastlioTestState.textContent = running
|
||||
? `● 실행 중 (${status.map.health} · ${status.map.hz.toFixed(1)}Hz · ${status.map.points.toLocaleString()}pt)`
|
||||
: "● 정지";
|
||||
fastlioTestState.className = "state-line " + (running ? "on" : "off");
|
||||
fastlioTestStart.disabled = !status.lidar.running || running;
|
||||
// Stopping while a real recording owns fastlio is rejected server-side
|
||||
// (409) — disable client-side too so the test button can't fire that.
|
||||
fastlioTestStop.disabled = !running || status.recording.active;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user