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.
149 lines
5.9 KiB
JavaScript
149 lines
5.9 KiB
JavaScript
// 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)";
|
|
}
|
|
};
|
|
}
|