2 Commits
Author SHA1 Message Date
Logan CusanoandClaude Opus 5.5 6bf2c9dd93 Merge feat/sdr-pins: pin each SDR service to a dongle by serial; OP25 always by serial (closes #11)
CI / lint (push) Successful in 10s
CI / test (push) Successful in 51s
Build edge-node / build (push) Successful in 58s
Build secondary-sdr / build (push) Successful in 2m39s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:41:28 -04:00
Logan CusanoandClaude Opus 5.5 575a99feb5 checkin: report an unreachable secondary-sdr as null, never keep stale 'Running'
QA blocker: when the secondary-sdr container was down, the checkin
omitted secondary_sdr_running / sdr_devices / op25_sdr_serial, and C2
only overwrites keys that are present, so the dashboard kept the last
'Running' forever. Send explicit nulls. The local card also says 'not
reported' rather than 'not plugged in' for a pin it can't check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:41:27 -04:00
2 changed files with 9 additions and 2 deletions
@@ -236,6 +236,12 @@ class MQTTManager:
} }
payload["sdr_pins"] = config.sdr_pins payload["sdr_pins"] = config.sdr_pins
secondary = await secondary_sdr_client.status() secondary = await secondary_sdr_client.status()
# Unreachable secondary-sdr: say so with explicit nulls. C2 only
# overwrites keys present in the checkin, so omitting them would leave
# a dead decoder showing "Running" forever.
payload["secondary_sdr_running"] = None
payload["sdr_devices"] = None
payload["op25_sdr_serial"] = None
if secondary is not None: if secondary is not None:
payload["secondary_sdr_running"] = [r["mode"] for r in secondary.get("running", [])] payload["secondary_sdr_running"] = [r["mode"] for r in secondary.get("running", [])]
if secondary.get("devices") is not None: if secondary.get("devices") is not None:
+3 -2
View File
@@ -568,7 +568,8 @@
return `<option value="">${esc(autoLabel)}</option>` + return `<option value="">${esc(autoLabel)}</option>` +
devs.map(d => `<option value="${esc(d.serial)}" ${d.serial === selected ? 'selected' : ''}>` + devs.map(d => `<option value="${esc(d.serial)}" ${d.serial === selected ? 'selected' : ''}>` +
`SDR ${d.index + 1} · serial ${esc(d.serial)}${d.duplicate_serial ? ' (shared serial!)' : ''}</option>`).join('') + `SDR ${d.index + 1} · serial ${esc(d.serial)}${d.duplicate_serial ? ' (shared serial!)' : ''}</option>`).join('') +
(selected && !known ? `<option value="${esc(selected)}" selected>serial ${esc(selected)} (not plugged in)</option>` : ''); (selected && !known ? `<option value="${esc(selected)}" selected>serial ${esc(selected)} ` +
`(${sdr.devices === null ? 'not reported' : 'not plugged in'})</option>` : '');
} }
function renderSdr() { function renderSdr() {
@@ -585,7 +586,7 @@
const running = sdr.running || []; const running = sdr.running || [];
sdrRows.forEach((row, i) => { sdrRows.forEach((row, i) => {
const [name, hint] = SDR_LABELS[row.mode] || [row.mode, '']; const [name, hint] = SDR_LABELS[row.mode] || [row.mode, ''];
const pinMissing = row.pin && !(sdr.devices || []).some(d => d.serial === row.pin); const pinMissing = row.pin && sdr.devices !== null && !sdr.devices.some(d => d.serial === row.pin);
const state = !row.enabled ? 'Off' const state = !row.enabled ? 'Off'
: sdrDirty ? 'Unsaved' : sdrDirty ? 'Unsaved'
: sdr.running === null ? 'Unknown' : sdr.running === null ? 'Unknown'