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.pyPOST /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
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:There is no "update SDR tuning only" path. So:
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, orundefined).routers/nodes.pyPOST /nodes/{node_id}/config/{system_id}takeshardware_preset+ppm_overrideas 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 ofconfig/{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:0sendsundefined— 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)."0"(line 22, whennode.ppm_overrideis falsy), so opening the modal and hitting save on an untouched form sendsppm_override: undefined— which, combined with defect 1, is one of the ways a previously-set PPM gets wiped.Fix shape: send
ppm_overridewhenever the field parses to a finite number (including 0); only omit it when the field is blank. Distinguish "unset" from "0".Context
app/models.pyhas SDR presets each withppm: 0.0and aGeneratorConfig.ppm_override;routers/op25_controller.py:77picksppm_overrideover the preset. The edge-node (drb-edge-node/app/main.py:225,242) already storesppm_overrideper node — the plumbing exists, only the UI/API shape forces the coupling.rtl_test -p(~10 min) orkalibrate-rtlfor 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.