"use client"; import { useEffect, useState } from "react"; import { c2api } from "@/lib/c2api"; import type { NodeRecord, SdrDevice } from "@/lib/types"; // node-26#9 / #11. OP25 always has exactly one SDR (pinned by serial, or the // first one found). Every other SDR runs the next enabled service, top first; // a service can be pinned to the SDR that carries its antenna. const MODES: { mode: string; name: string; hint: string }[] = [ { mode: "adsb", name: "ADS-B", hint: "Aircraft · 1090 MHz" }, { mode: "ais", name: "AIS", hint: "Vessels · 162 MHz" }, ]; type Row = { mode: string; enabled: boolean; pin: string }; function rowsFrom(priority: string[], pins: Record): Row[] { const known = priority.filter((m) => MODES.some((x) => x.mode === m)); return [ ...known.map((mode) => ({ mode, enabled: true, pin: pins[mode] ?? "" })), ...MODES.filter((x) => !known.includes(x.mode)).map((x) => ({ mode: x.mode, enabled: false, pin: pins[x.mode] ?? "" })), ]; } const selectClass = "bg-gray-800 border border-gray-700 rounded px-2 py-1 text-gray-200 text-xs focus:outline-none focus:border-indigo-500 disabled:opacity-60 max-w-[13rem]"; function DeviceSelect({ value, devices, devicesKnown, autoLabel, label, disabled, onChange, }: { value: string; devices: SdrDevice[]; devicesKnown: boolean; autoLabel: string; label: string; disabled: boolean; onChange: (serial: string) => void; }) { const known = devices.some((d) => d.serial === value); return ( ); } export function SdrSettings({ node, canEdit }: { node: NodeRecord; canEdit: boolean }) { const priority = node.secondary_sdr_priority ?? []; const pins = node.sdr_pins ?? {}; // null/absent = the node has never reported (older firmware, container down // at checkin): unknown, not "nothing running" (server-26#187). const reported = node.secondary_sdr_running != null; const running = node.secondary_sdr_running ?? []; const devices = node.sdr_devices ?? []; const devicesKnown = reported && node.sdr_devices != null; const [rows, setRows] = useState(() => rowsFrom(priority, pins)); const [op25Pin, setOp25Pin] = useState(pins.op25 ?? ""); const [dirty, setDirty] = useState(false); const [saving, setSaving] = useState(false); const [message, setMessage] = useState(null); // Follow the node's live checkin unless there are unsaved edits. const serverKey = JSON.stringify([priority, pins]); useEffect(() => { if (dirty) return; const [p, pn] = JSON.parse(serverKey) as [string[], Record]; setRows(rowsFrom(p, pn)); setOp25Pin(pn.op25 ?? ""); }, [serverKey, dirty]); function edit(next: Row[]) { setRows(next); setDirty(true); setMessage(null); } function move(i: number, delta: number) { const next = [...rows]; [next[i], next[i + delta]] = [next[i + delta], next[i]]; edit(next); } const pinned = [op25Pin, ...rows.map((r) => r.pin)].filter(Boolean); const clash = new Set(pinned).size !== pinned.length; const op25Moving = op25Pin !== (pins.op25 ?? ""); async function save() { setSaving(true); setMessage(null); try { const nextPins: Record = { op25: op25Pin || null }; rows.forEach((r) => { nextPins[r.mode] = r.pin || null; }); await c2api.updateNode(node.node_id, { secondary_sdr_priority: rows.filter((r) => r.enabled).map((r) => r.mode), sdr_pins: nextPins, }); setDirty(false); setMessage("Sent to the node. Status updates when it checks in."); } catch (err) { setMessage(err instanceof Error ? err.message : "Save failed."); } finally { setSaving(false); } } let rank = 0; return (

SDRs

{devicesKnown ? `This node reports ${devices.length} SDR${devices.length === 1 ? "" : "s"}.` : "This node hasn't reported its SDRs yet."}

OP25 SDR
{op25Moving ? "Saving restarts OP25 on the selected SDR." : node.op25_sdr_serial ? `Using serial ${node.op25_sdr_serial}` : "P25 / analog scanning"}
{ setOp25Pin(v); setDirty(true); setMessage(null); }} />
{rows.map((row, i) => { const meta = MODES.find((x) => x.mode === row.mode)!; const pinMissing = devicesKnown && !!row.pin && !devices.some((d) => d.serial === row.pin); const state = !row.enabled ? "Off" : dirty ? "Unsaved" : !reported ? "Not reported" : running.includes(row.mode) ? "Running" : pinMissing ? "Pinned SDR missing" : "Waiting for SDR"; return (
{row.enabled ? ++rank : ""} edit(rows.map((r, j) => (j === i ? { ...r, enabled: e.target.checked } : r)))} className="rounded bg-gray-800 border-gray-700 text-indigo-600 focus:ring-indigo-500 focus:ring-offset-gray-900" />
{meta.name}
{meta.hint}
edit(rows.map((r, j) => (j === i ? { ...r, pin: v } : r)))} /> {canEdit && (
)} {state}
); })}

Every SDR besides OP25's runs the next enabled service, top first. Pin a service to the SDR with its antenna, or leave it on "Any spare SDR".

{devices.some((d) => d.duplicate_serial) && (

Two SDRs share a serial number, so they can't be told apart. Give each a unique serial (rtl_eeprom -s) before pinning.

)} {clash &&

Two services are pinned to the same SDR.

} {canEdit && (
{message && {message}}
)}
); }