Refactor radar system

This commit is contained in:
Ayzen
2026-10-05 16:46:28 +03:00
parent 515049d2ed
commit 8f097dfa2e
179 changed files with 8212 additions and 6676 deletions
+131 -25
View File
@@ -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) - */