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,148 @@
|
||||
// Per-camera exposure/gain settings tabs — mirrors the QTabWidget layout in
|
||||
// scan_gui_triple.py (auto-exposure toggle, brightness/exp_min/exp_max vs.
|
||||
// exp_time mutually exclusive groups, gain; Apply = live ros2 param set,
|
||||
// Save = persist into the base YAML).
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
function field(labelText, id, type, step) {
|
||||
const wrap = document.createDocumentFragment();
|
||||
const label = document.createElement("label");
|
||||
label.textContent = labelText;
|
||||
label.htmlFor = id;
|
||||
const input = document.createElement("input");
|
||||
input.type = type;
|
||||
input.id = id;
|
||||
if (step) input.step = step;
|
||||
wrap.appendChild(label);
|
||||
wrap.appendChild(input);
|
||||
return { wrap, input };
|
||||
}
|
||||
|
||||
export async function buildCameraTabs(cameras) {
|
||||
const tabBar = document.getElementById("camera-tabs");
|
||||
const panelsRoot = document.getElementById("camera-tab-panels");
|
||||
tabBar.innerHTML = "";
|
||||
panelsRoot.innerHTML = "";
|
||||
|
||||
for (let i = 0; i < cameras.length; i++) {
|
||||
const cam = cameras[i];
|
||||
const tabBtn = document.createElement("button");
|
||||
tabBtn.className = "tab-btn" + (i === 0 ? " active" : "");
|
||||
tabBtn.textContent = cam.id;
|
||||
tabBtn.onclick = () => {
|
||||
document.querySelectorAll(".tab-btn").forEach((b) => b.classList.remove("active"));
|
||||
document.querySelectorAll(".tab-panel").forEach((p) => p.classList.remove("active"));
|
||||
tabBtn.classList.add("active");
|
||||
document.getElementById(`cam-panel-${cam.id}`).classList.add("active");
|
||||
};
|
||||
tabBar.appendChild(tabBtn);
|
||||
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "tab-panel" + (i === 0 ? " active" : "");
|
||||
panel.id = `cam-panel-${cam.id}`;
|
||||
panelsRoot.appendChild(panel);
|
||||
|
||||
await buildCameraPanel(panel, cam.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildCameraPanel(panel, camId) {
|
||||
const values = await getJSON(`/api/camera/${camId}/params`);
|
||||
|
||||
const autoLabel = document.createElement("label");
|
||||
const autoChk = document.createElement("input");
|
||||
autoChk.type = "checkbox";
|
||||
autoChk.checked = values.exposure_auto;
|
||||
autoLabel.appendChild(autoChk);
|
||||
autoLabel.appendChild(document.createTextNode(" 자동 노출"));
|
||||
panel.appendChild(autoLabel);
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "cam-field-grid";
|
||||
panel.appendChild(grid);
|
||||
|
||||
const fBrightness = field("목표 밝기:", `${camId}-brightness`, "number");
|
||||
fBrightness.input.min = 0; fBrightness.input.max = 255; fBrightness.input.value = values.exposure_auto_target_brightness;
|
||||
const fExpMin = field("노출 하한 (us):", `${camId}-exp-min`, "number");
|
||||
fExpMin.input.min = 10; fExpMin.input.max = 1000000; fExpMin.input.step = 100; fExpMin.input.value = values.exposure_auto_min;
|
||||
const fExpMax = field("노출 상한 (us):", `${camId}-exp-max`, "number");
|
||||
fExpMax.input.min = 10; fExpMax.input.max = 1000000; fExpMax.input.step = 1000; fExpMax.input.value = values.exposure_auto_max;
|
||||
const fExpTime = field("노출 시간 (us):", `${camId}-exp-time`, "number");
|
||||
fExpTime.input.min = 10; fExpTime.input.max = 1000000; fExpTime.input.value = values.exposure_time;
|
||||
const fGain = field("게인 (dB):", `${camId}-gain`, "number");
|
||||
fGain.input.min = 0; fGain.input.max = 16.9; fGain.input.step = 0.5; fGain.input.value = values.gain;
|
||||
|
||||
for (const f of [fBrightness, fExpMin, fExpMax, fExpTime, fGain]) grid.appendChild(f.wrap);
|
||||
|
||||
const autoWidgets = [fBrightness.input, fExpMin.input, fExpMax.input];
|
||||
const manualWidgets = [fExpTime.input];
|
||||
|
||||
function applyToggleState(checked) {
|
||||
autoWidgets.forEach((w) => (w.disabled = !checked));
|
||||
manualWidgets.forEach((w) => (w.disabled = checked));
|
||||
}
|
||||
autoChk.onchange = () => applyToggleState(autoChk.checked);
|
||||
applyToggleState(values.exposure_auto);
|
||||
|
||||
const btnRow = document.createElement("div");
|
||||
btnRow.className = "btn-row";
|
||||
const btnApply = document.createElement("button");
|
||||
btnApply.className = "btn btn-blue";
|
||||
btnApply.textContent = "적용";
|
||||
const btnSave = document.createElement("button");
|
||||
btnSave.className = "btn btn-green";
|
||||
btnSave.textContent = "저장";
|
||||
btnRow.appendChild(btnApply);
|
||||
btnRow.appendChild(btnSave);
|
||||
panel.appendChild(btnRow);
|
||||
|
||||
const result = document.createElement("div");
|
||||
result.className = "cam-result";
|
||||
panel.appendChild(result);
|
||||
|
||||
function collect() {
|
||||
return {
|
||||
exposure_auto: autoChk.checked,
|
||||
exposure_auto_target_brightness: parseInt(fBrightness.input.value, 10),
|
||||
exposure_auto_min: parseFloat(fExpMin.input.value),
|
||||
exposure_auto_max: parseFloat(fExpMax.input.value),
|
||||
exposure_time: parseInt(fExpTime.input.value, 10),
|
||||
gain: parseFloat(fGain.input.value),
|
||||
};
|
||||
}
|
||||
|
||||
btnApply.onclick = async () => {
|
||||
result.textContent = "적용 중…";
|
||||
try {
|
||||
await getJSON(`/api/camera/${camId}/params`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(collect()),
|
||||
});
|
||||
const r = await getJSON(`/api/camera/${camId}/params/apply`, { method: "POST" });
|
||||
result.textContent = r.ok ? "적용 완료" : r.results.filter(x => !x.ok).map(x => `${x.name}: ${x.message}`).join(" / ");
|
||||
result.style.color = r.ok ? "var(--color-success)" : "var(--color-danger)";
|
||||
} catch (e) {
|
||||
result.textContent = String(e);
|
||||
result.style.color = "var(--color-danger)";
|
||||
}
|
||||
};
|
||||
|
||||
btnSave.onclick = async () => {
|
||||
result.textContent = "저장 중…";
|
||||
try {
|
||||
await getJSON(`/api/camera/${camId}/params`, {
|
||||
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(collect()),
|
||||
});
|
||||
const r = await getJSON(`/api/camera/${camId}/params/save`, { method: "POST" });
|
||||
result.textContent = `저장됨 → ${r.path}`;
|
||||
result.style.color = "var(--color-success)";
|
||||
} catch (e) {
|
||||
result.textContent = String(e);
|
||||
result.style.color = "var(--color-danger)";
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user