// 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)"; } }; }