Refactor radar system
This commit is contained in:
+131
-25
@@ -8,6 +8,10 @@
|
||||
|
||||
/* ---- DOM handles ------------------------------------------------- */
|
||||
const plotImg = document.getElementById("plot");
|
||||
const sar3dStatusEl = document.getElementById("sar3d-status");
|
||||
const displayComboControls = document.getElementById("display-combo-controls");
|
||||
const displayComboSelect = document.getElementById("display-combo");
|
||||
const btnNextCombo = document.getElementById("btn-next-combo");
|
||||
|
||||
const btnStart = document.getElementById("btn-start");
|
||||
const btnSingle = document.getElementById("btn-single");
|
||||
@@ -89,6 +93,25 @@ bindControl(btnStop, "/api/stop", {});
|
||||
bindControl(btnTmpRef, "/api/tmp_reference", {});
|
||||
bindControl(btnRemoveLast, "/api/remove_last", {});
|
||||
|
||||
let comboOptionsSignature = "";
|
||||
|
||||
async function changeDisplayCombo(path, body) {
|
||||
displayComboSelect.disabled = btnNextCombo.disabled = true;
|
||||
try {
|
||||
applyStatus(await api(path, body));
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
} finally {
|
||||
displayComboSelect.disabled = btnNextCombo.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
displayComboSelect.addEventListener("change", () => {
|
||||
const [input_pos, output_pos] = displayComboSelect.value.split(":").map(Number);
|
||||
changeDisplayCombo("/api/display_combo", { input_pos, output_pos });
|
||||
});
|
||||
btnNextCombo.addEventListener("click", () => changeDisplayCombo("/api/next_combo", {}));
|
||||
|
||||
/* ---- settings panel --------------------------------------------- */
|
||||
settingsToggle.addEventListener("click", () => sidePanel.classList.toggle("collapsed"));
|
||||
|
||||
@@ -98,24 +121,88 @@ settingsToggle.addEventListener("click", () => sidePanel.classList.toggle("colla
|
||||
const fieldInputs = {}; // field name -> { el, kind, dirty }
|
||||
let formSignature = ""; // field names currently in the form (detect mode/structure change)
|
||||
|
||||
function settingsFormSignature(schema) {
|
||||
return JSON.stringify(schema.map((entry) => [entry.name, entry.kind, entry.position_count]));
|
||||
}
|
||||
|
||||
function makeGeometryEditor(entry) {
|
||||
const el = document.createElement("div");
|
||||
el.className = "geometry-editor";
|
||||
const table = document.createElement("table");
|
||||
const head = table.createTHead().insertRow();
|
||||
const keys = [entry.position_field, "x_m", "y_m", "z_m"];
|
||||
for (const title of [entry.position_field === "output_pos" ? "Output" : "Input", "X (m)", "Y (m)", "Z (m)", ""]) {
|
||||
const cell = document.createElement("th");
|
||||
cell.textContent = title;
|
||||
head.appendChild(cell);
|
||||
}
|
||||
const body = table.createTBody();
|
||||
const add = document.createElement("button");
|
||||
const appendRow = (record) => {
|
||||
const row = body.insertRow();
|
||||
for (const [index, key] of keys.entries()) {
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.step = index === 0 ? "1" : "any";
|
||||
input.required = true;
|
||||
input.setAttribute("aria-label", `${entry.name} ${key}`);
|
||||
if (index === 0) { input.min = "0"; input.max = entry.position_count - 1; }
|
||||
input.value = record[key] ?? 0;
|
||||
input.disabled = entry.enabled === false;
|
||||
row.insertCell().appendChild(input);
|
||||
}
|
||||
const remove = document.createElement("button");
|
||||
remove.type = "button";
|
||||
remove.textContent = "−";
|
||||
remove.title = "Remove antenna";
|
||||
remove.disabled = entry.enabled === false;
|
||||
remove.addEventListener("click", () => {
|
||||
row.remove();
|
||||
add.disabled = entry.enabled === false;
|
||||
el.dispatchEvent(new Event("input"));
|
||||
});
|
||||
row.insertCell().appendChild(remove);
|
||||
};
|
||||
el.getRecords = () => Array.from(body.rows, (row) => Object.fromEntries(
|
||||
Array.from(row.querySelectorAll("input"), (input, index) => {
|
||||
if (!input.reportValidity()) throw new Error("Enter valid antenna coordinates");
|
||||
return [keys[index], Number(input.value)];
|
||||
})
|
||||
));
|
||||
el.setRecords = (records) => {
|
||||
body.replaceChildren();
|
||||
for (const record of records) appendRow(record);
|
||||
add.disabled = entry.enabled === false || records.length >= entry.position_count;
|
||||
};
|
||||
el.setRecords(entry.value);
|
||||
add.type = "button";
|
||||
add.textContent = "Add antenna";
|
||||
add.addEventListener("click", () => {
|
||||
if (body.rows.length >= entry.position_count) return;
|
||||
const positions = new Set(Array.from(body.rows, (row) => Number(row.querySelector("input").value)));
|
||||
let position = 0;
|
||||
while (positions.has(position)) position++;
|
||||
appendRow({ [entry.position_field]: position });
|
||||
add.disabled = body.rows.length >= entry.position_count;
|
||||
el.dispatchEvent(new Event("input"));
|
||||
});
|
||||
el.append(table, add);
|
||||
return el;
|
||||
}
|
||||
|
||||
function makeFieldRow(entry) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "field";
|
||||
if (entry.kind === "geometry") row.classList.add("geometry-field");
|
||||
const label = document.createElement("label");
|
||||
label.textContent = entry.name;
|
||||
label.textContent = entry.kind === "geometry" ? (entry.name === "tx_geometry" ? "Tx antenna positions" : "Rx antenna positions") : entry.name;
|
||||
label.htmlFor = "f_" + entry.name;
|
||||
if (entry.applies_on_start) {
|
||||
// Stable run_config field: editing it here takes effect on the next pipeline start.
|
||||
const hint = document.createElement("span");
|
||||
hint.className = "on-start-hint";
|
||||
hint.textContent = " (on Start)";
|
||||
hint.title = "Applied when the pipeline next starts";
|
||||
label.appendChild(hint);
|
||||
}
|
||||
row.appendChild(label);
|
||||
|
||||
let el;
|
||||
if (entry.kind === "bool") {
|
||||
if (entry.kind === "geometry") {
|
||||
el = makeGeometryEditor(entry);
|
||||
} else if (entry.kind === "bool") {
|
||||
el = document.createElement("input");
|
||||
el.type = "checkbox";
|
||||
el.checked = !!entry.value;
|
||||
@@ -173,23 +260,23 @@ function buildSettingsForm(schema) {
|
||||
settingsFields.appendChild(heading);
|
||||
}
|
||||
settingsFields.appendChild(makeFieldRow(entry));
|
||||
if (entry.name === "rx_geometry") {
|
||||
const note = document.createElement("div");
|
||||
note.className = "config-active";
|
||||
note.textContent = "To apply Tx/Rx geometry or permittivity changes: Save Config and restart the app";
|
||||
settingsFields.appendChild(note);
|
||||
}
|
||||
}
|
||||
formSignature = schema.map((e) => e.name).join(",");
|
||||
formSignature = settingsFormSignature(schema);
|
||||
}
|
||||
|
||||
function collectFields() {
|
||||
const out = {};
|
||||
for (const key in fieldInputs) {
|
||||
const { el, kind } = fieldInputs[key];
|
||||
if (kind === "bool") out[key] = el.checked;
|
||||
else if (kind === "int") out[key] = parseInt(el.value, 10);
|
||||
else if (kind === "float") out[key] = parseFloat(el.value);
|
||||
if (kind === "geometry") out[key] = el.getRecords();
|
||||
else if (kind === "bool") out[key] = el.checked;
|
||||
else if (kind === "int" || kind === "float") {
|
||||
const value = Number(el.value);
|
||||
if (!el.value.trim() || !Number.isFinite(value) || (kind === "int" && !Number.isSafeInteger(value))) {
|
||||
throw new Error("Enter valid numeric settings");
|
||||
}
|
||||
out[key] = value;
|
||||
}
|
||||
else out[key] = el.value; // select + text (positions are sent as CSV text)
|
||||
}
|
||||
return out;
|
||||
@@ -197,7 +284,8 @@ function collectFields() {
|
||||
|
||||
function setFieldValue(input, value) {
|
||||
const { el, kind } = input;
|
||||
if (kind === "bool") el.checked = !!value;
|
||||
if (kind === "geometry") el.setRecords(value);
|
||||
else if (kind === "bool") el.checked = !!value;
|
||||
else if (kind === "select") el.value = String(value);
|
||||
else el.value = value == null ? "" : value;
|
||||
}
|
||||
@@ -218,13 +306,13 @@ async function applyOne(field, value) {
|
||||
// changed (e.g. the mode switched); otherwise update values in place without
|
||||
// clobbering a field the operator is editing here.
|
||||
function applySettings(schema) {
|
||||
if (schema.map((e) => e.name).join(",") !== formSignature) {
|
||||
if (settingsFormSignature(schema) !== formSignature) {
|
||||
buildSettingsForm(schema);
|
||||
return;
|
||||
}
|
||||
for (const entry of schema) {
|
||||
const input = fieldInputs[entry.name];
|
||||
if (!input || input.el === document.activeElement || input.dirty) continue;
|
||||
if (!input || input.el.contains(document.activeElement) || input.dirty) continue;
|
||||
setFieldValue(input, entry.value);
|
||||
if (entry.enabled !== undefined) input.el.disabled = entry.enabled === false;
|
||||
}
|
||||
@@ -333,8 +421,8 @@ function updateRecordButtonState() {
|
||||
}
|
||||
|
||||
btnStartRecording.addEventListener("click", async () => {
|
||||
const count = parseInt(recordCountInput.value, 10);
|
||||
if (!Number.isFinite(count) || count < 1) {
|
||||
const count = Number(recordCountInput.value);
|
||||
if (!Number.isSafeInteger(count) || count < 1) {
|
||||
toast("Enter how many sweeps to record (>= 1)", true);
|
||||
return;
|
||||
}
|
||||
@@ -361,6 +449,22 @@ function setStat(el, label, value, cls) {
|
||||
}
|
||||
|
||||
function applyStatus(s) {
|
||||
if (s.display_combo) {
|
||||
const combo = s.display_combo;
|
||||
displayComboControls.hidden = !combo.enabled;
|
||||
const options = combo.options || [];
|
||||
const signature = JSON.stringify(options);
|
||||
if (signature !== comboOptionsSignature) {
|
||||
displayComboSelect.replaceChildren(...options.map(([inp, out]) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = `${inp}:${out}`;
|
||||
option.textContent = `Input ${inp} / Output ${out}`;
|
||||
return option;
|
||||
}));
|
||||
comboOptionsSignature = signature;
|
||||
}
|
||||
displayComboSelect.value = `${combo.input}:${combo.output}`;
|
||||
}
|
||||
if ("running" in s) {
|
||||
setStat(runningEl, "running", s.running ? "yes" : "no", s.running ? "ok" : "off");
|
||||
pipelineRunning = !!s.running;
|
||||
@@ -393,6 +497,8 @@ function applyStatus(s) {
|
||||
if ("raw_count" in s)
|
||||
setStat(scansEl, "scans",
|
||||
`raw ${s.raw_count} / preprocessed ${s.preprocessed_count} / results ${s.result_count}`);
|
||||
sar3dStatusEl.textContent = s.sar3d_status || "";
|
||||
sar3dStatusEl.hidden = !s.sar3d_status;
|
||||
}
|
||||
|
||||
/* ---- frame rendering (latest-wins; the frame IS the Qt plot image) - */
|
||||
|
||||
@@ -22,7 +22,12 @@
|
||||
<section class="plot-panel">
|
||||
<div class="plot-head">
|
||||
<span class="plot-title">Processor output</span>
|
||||
<div id="display-combo-controls" class="config-row" hidden>
|
||||
<select id="display-combo" class="config-control" aria-label="B-scan channel combination"></select>
|
||||
<button id="btn-next-combo" class="btn">Next combination</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sar3d-status" class="sar3d-status" role="status" hidden></div>
|
||||
<div class="canvas-wrap">
|
||||
<img id="plot" class="plot-img" alt="Live processor plot" />
|
||||
</div>
|
||||
@@ -45,7 +50,7 @@
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<input id="save-name" class="config-control" type="text" placeholder="Name (optional)" aria-label="Save name" />
|
||||
<button id="btn-save-dataset" class="btn">Save</button>
|
||||
<button id="btn-save-dataset" class="btn" title="Save up to 1000 most recent measurements">Save Last 1000</button>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<input id="record-count" class="config-control" type="number" min="1" step="1"
|
||||
|
||||
@@ -107,11 +107,14 @@ body {
|
||||
}
|
||||
.plot-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.plot-title { font-weight: 600; color: var(--status); }
|
||||
.sar3d-status { margin-bottom: 8px; overflow-wrap: anywhere; }
|
||||
.axes-label { color: var(--muted); font-family: var(--mono); font-size: 12px; }
|
||||
.canvas-wrap {
|
||||
flex: 1;
|
||||
@@ -141,6 +144,7 @@ body {
|
||||
}
|
||||
.config-title { font-weight: 600; color: var(--status); margin-bottom: 8px; }
|
||||
.config-row { display: flex; gap: 8px; }
|
||||
.config-row[hidden] { display: none; }
|
||||
.config-row + .config-row { margin-top: 8px; }
|
||||
.config-control {
|
||||
flex: 1;
|
||||
@@ -251,7 +255,6 @@ body {
|
||||
font-size: 12px;
|
||||
}
|
||||
.field textarea:focus { outline: none; border-color: var(--accent); }
|
||||
.on-start-hint { color: var(--muted); font-size: 11px; font-style: italic; }
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
@@ -352,3 +355,12 @@ body {
|
||||
.field select { width: 130px; }
|
||||
.statusbar { gap: 12px; }
|
||||
}
|
||||
.geometry-field { display: block; }
|
||||
.geometry-field label { display: block; margin-bottom: 5px; }
|
||||
.geometry-editor { width: 100%; min-width: 0; overflow-x: auto; }
|
||||
.geometry-editor table { width: 100%; min-width: 340px; table-layout: fixed; border-collapse: collapse; }
|
||||
.geometry-editor th { font-size: 0.7rem; text-align: left; }
|
||||
.geometry-editor td { padding: 2px; }
|
||||
.geometry-editor input[type="number"] { width: 100%; min-width: 0; }
|
||||
.geometry-editor button { background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; }
|
||||
.geometry-editor button:disabled { opacity: 0.45; }
|
||||
|
||||
Reference in New Issue
Block a user