SDR tuning (PPM / preset) is coupled to system assignment and silently wiped on reassign #111

Open
opened 2026-09-07 00:01:15 -04:00 by logan · 0 comments
Owner

Two related defects in the node SDR-config path. Expands DEFERRED.md row 53 ("SDR hardware config … clobbered on system change").

1. PPM is bundled into the system-assignment call

drb-frontend/components/NodeConfigModal.tsx:41 — the only way to set PPM/preset from the UI:

await c2api.assignSystem(node.node_id, systemId, preset, ppmOverride);

There is no "update SDR tuning only" path. So:

  • Re-assigning or changing a node's radio system re-sends whatever the modal currently holds for preset / ppm. If the operator changes the system without re-typing the PPM they dialed in earlier, it is overwritten (with the modal's default, or undefined).
  • Server side, routers/nodes.py POST /nodes/{node_id}/config/{system_id} takes hardware_preset + ppm_override as part of the same request; there is no separate endpoint.

Fix shape: a dedicated PATCH /nodes/{id}/sdr (preset, ppm, gain, device index, sample rate, antenna) that is independent of config/{system_id}, persisted on the node doc and merged — never reset by an assignment. Frontend: a separate "SDR" section/modal that calls it.

2. The modal cannot set PPM to 0, and its default is a silent no-op

NodeConfigModal.tsx:28-29:

const ppmVal = parseFloat(ppm);
const ppmOverride = !isNaN(ppmVal) && ppmVal !== 0 ? ppmVal : undefined;
  • Entering 0 sends undefined — you cannot explicitly pin PPM to 0, and a dongle that genuinely needs 0 offset just falls through to the preset default (also 0, so harmless today, but wrong if a preset ever ships a non-zero default).
  • The field's default value is the string "0" (line 22, when node.ppm_override is falsy), so opening the modal and hitting save on an untouched form sends ppm_override: undefined — which, combined with defect 1, is one of the ways a previously-set PPM gets wiped.

Fix shape: send ppm_override whenever the field parses to a finite number (including 0); only omit it when the field is blank. Distinguish "unset" from "0".

Context

  • op25-container: app/models.py has SDR presets each with ppm: 0.0 and a GeneratorConfig.ppm_override; routers/op25_controller.py:77 picks ppm_override over the preset. The edge-node (drb-edge-node/app/main.py:225,242) already stores ppm_override per node — the plumbing exists, only the UI/API shape forces the coupling.
  • Calibration workflow for reference: stop op25, rtl_test -p (~10 min) or kalibrate-rtl for a rough figure, then iterate against the live frequency error in the OP25 terminal (:8081). RTL-SDR v3 / NESDR are TCXO (±1–2 ppm) with cqpsk tracking already disabled in the presets, so the PPM value has to be right.

Link back to DEFERRED.md row 53 so neither is the sole record.

Two related defects in the node SDR-config path. Expands DEFERRED.md row 53 ("SDR hardware config … clobbered on system change"). ## 1. PPM is bundled into the system-assignment call `drb-frontend/components/NodeConfigModal.tsx:41` — the only way to set PPM/preset from the UI: ``` await c2api.assignSystem(node.node_id, systemId, preset, ppmOverride); ``` There is no "update SDR tuning only" path. So: - Re-assigning or changing a node's radio system re-sends whatever the modal currently holds for `preset` / `ppm`. If the operator changes the system without re-typing the PPM they dialed in earlier, it is overwritten (with the modal's default, or `undefined`). - Server side, `routers/nodes.py` `POST /nodes/{node_id}/config/{system_id}` takes `hardware_preset` + `ppm_override` as part of the same request; there is no separate endpoint. **Fix shape:** a dedicated `PATCH /nodes/{id}/sdr` (preset, ppm, gain, device index, sample rate, antenna) that is independent of `config/{system_id}`, persisted on the node doc and merged — never reset by an assignment. Frontend: a separate "SDR" section/modal that calls it. ## 2. The modal cannot set PPM to 0, and its default is a silent no-op `NodeConfigModal.tsx:28-29`: ``` const ppmVal = parseFloat(ppm); const ppmOverride = !isNaN(ppmVal) && ppmVal !== 0 ? ppmVal : undefined; ``` - Entering `0` sends `undefined` — you cannot explicitly pin PPM to 0, and a dongle that genuinely needs 0 offset just falls through to the preset default (also 0, so harmless today, but wrong if a preset ever ships a non-zero default). - The field's default value is the string `"0"` (line 22, when `node.ppm_override` is falsy), so opening the modal and hitting save on an untouched form sends `ppm_override: undefined` — which, combined with defect 1, is one of the ways a previously-set PPM gets wiped. **Fix shape:** send `ppm_override` whenever the field parses to a finite number (including 0); only omit it when the field is blank. Distinguish "unset" from "0". ## Context - op25-container: `app/models.py` has SDR presets each with `ppm: 0.0` and a `GeneratorConfig.ppm_override`; `routers/op25_controller.py:77` picks `ppm_override` over the preset. The edge-node (`drb-edge-node/app/main.py:225,242`) already stores `ppm_override` per node — the plumbing exists, only the UI/API shape forces the coupling. - Calibration workflow for reference: stop op25, `rtl_test -p` (~10 min) or `kalibrate-rtl` for a rough figure, then iterate against the live frequency error in the OP25 terminal (:8081). RTL-SDR v3 / NESDR are TCXO (±1–2 ppm) with cqpsk tracking already disabled in the presets, so the PPM value has to be right. Link back to DEFERRED.md row 53 so neither is the sole record.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logan/server-26#111