f34817d5b6
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.
124 lines
4.2 KiB
JavaScript
124 lines
4.2 KiB
JavaScript
import { formatBytes, formatDuration } from "/js/format.js";
|
|
|
|
async function getJSON(url, opts) {
|
|
const res = await fetch(url, opts);
|
|
if (!res.ok) throw new Error(`${url}: ${res.status} ${await res.text()}`);
|
|
return res.json();
|
|
}
|
|
|
|
export function initRecordingPanel(defaultSaveDir) {
|
|
const saveDirInput = document.getElementById("rec-save-dir");
|
|
saveDirInput.value = defaultSaveDir;
|
|
|
|
const browseBtn = document.getElementById("btn-browse");
|
|
const browsePanel = document.getElementById("browse-panel");
|
|
const filenameInput = document.getElementById("rec-filename");
|
|
const confirmBtn = document.getElementById("btn-rec-confirm");
|
|
const stopBtn = document.getElementById("btn-rec-stop");
|
|
const bagList = document.getElementById("bag-list");
|
|
|
|
browseBtn.onclick = async () => {
|
|
if (!browsePanel.classList.contains("hidden")) {
|
|
browsePanel.classList.add("hidden");
|
|
return;
|
|
}
|
|
await renderBrowse(saveDirInput.value || defaultSaveDir);
|
|
browsePanel.classList.remove("hidden");
|
|
};
|
|
|
|
async function renderBrowse(path) {
|
|
const data = await getJSON(`/api/fs/browse?path=${encodeURIComponent(path)}`);
|
|
browsePanel.innerHTML = "";
|
|
const cur = document.createElement("div");
|
|
cur.className = "dir-entry";
|
|
cur.style.fontWeight = "bold";
|
|
cur.textContent = `✓ 선택: ${data.path}`;
|
|
cur.onclick = () => {
|
|
saveDirInput.value = data.path;
|
|
browsePanel.classList.add("hidden");
|
|
refreshBags();
|
|
};
|
|
browsePanel.appendChild(cur);
|
|
if (data.parent) {
|
|
const up = document.createElement("div");
|
|
up.className = "dir-entry";
|
|
up.textContent = "..";
|
|
up.onclick = () => renderBrowse(data.parent);
|
|
browsePanel.appendChild(up);
|
|
}
|
|
for (const d of data.dirs) {
|
|
const el = document.createElement("div");
|
|
el.className = "dir-entry";
|
|
el.textContent = d.name;
|
|
el.onclick = () => renderBrowse(d.path);
|
|
browsePanel.appendChild(el);
|
|
}
|
|
}
|
|
|
|
saveDirInput.addEventListener("change", refreshBags);
|
|
|
|
confirmBtn.onclick = async () => {
|
|
try {
|
|
await getJSON("/api/recording/start", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ filename: filenameInput.value, save_dir: saveDirInput.value }),
|
|
});
|
|
refreshBags();
|
|
document.getElementById("overlay-recording").classList.add("hidden");
|
|
} catch (e) {
|
|
alert(String(e));
|
|
}
|
|
};
|
|
|
|
stopBtn.onclick = async () => {
|
|
await getJSON("/api/recording/stop", { method: "POST" });
|
|
// rosbag2 flushes metadata.yaml on SIGINT — give it a moment before
|
|
// reading the bag back, otherwise it still shows as "incomplete".
|
|
setTimeout(refreshBags, 1200);
|
|
};
|
|
|
|
function renderBagItem(bag) {
|
|
const li = document.createElement("li");
|
|
li.className = "bag-item";
|
|
|
|
const nameRow = document.createElement("div");
|
|
nameRow.className = "bag-item-name";
|
|
nameRow.textContent = bag.name;
|
|
if (!bag.complete) {
|
|
const badge = document.createElement("span");
|
|
badge.className = "bag-item-incomplete";
|
|
badge.textContent = "(녹화 중 / 미완료)";
|
|
nameRow.appendChild(badge);
|
|
}
|
|
li.appendChild(nameRow);
|
|
|
|
const meta = document.createElement("div");
|
|
meta.className = "bag-item-meta";
|
|
const parts = [formatDuration(bag.duration_s), formatBytes(bag.size_bytes)];
|
|
if (bag.message_count != null) parts.push(`msg ${bag.message_count.toLocaleString()}`);
|
|
if (bag.topics && bag.topics.length) parts.push(`topic ${bag.topics.length}개`);
|
|
meta.textContent = parts.filter(Boolean).join(" · ");
|
|
li.appendChild(meta);
|
|
|
|
return li;
|
|
}
|
|
|
|
// Disk-free is owned entirely by status-panel.js now (HUD strip, driven by
|
|
// /ws/status) — this panel only needs the bag list.
|
|
async function refreshBags() {
|
|
const dir = saveDirInput.value || defaultSaveDir;
|
|
try {
|
|
const bagsData = await getJSON(`/api/bags?save_dir=${encodeURIComponent(dir)}`);
|
|
bagList.innerHTML = "";
|
|
for (const bag of bagsData.bags) {
|
|
bagList.appendChild(renderBagItem(bag));
|
|
}
|
|
} catch (e) {
|
|
// Directory may not exist yet (new save path) — not an error worth alarming over.
|
|
bagList.innerHTML = "";
|
|
}
|
|
}
|
|
refreshBags();
|
|
setInterval(refreshBags, 15000);
|
|
}
|