Skip to content

RTL: apply frequency-correction edits live with confirmed readback - #6003

Merged
Ozy311 merged 0 commit into
codex/rtl-native-hd-fmfrom
codex/rtl-live-ppm
Oct 2, 2026
Merged

Ozy311 merged 0 commit into
codex/rtl-native-hd-fmfrom
codex/rtl-live-ppm

Conversation

@Ozy311

@Ozy311 Ozy311 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review moved to #6082

During separation of Digital FM into RFC #6079, branch ancestry was changed before PR bases were retargeted. GitHub automatically recognized this head inside the Digital branch and marked this PR merged there, then deleted the branch. This was not a merge into main. I restored the branch and created #6082 against the correct analog base. That replacement is Ready for review; original review discussion and historical evidence remain here.

Current analog order: #5924 → #5973 → #5974 → #6082 → #6083. Draft Digital #6002 follows #6083 and is no longer an analog dependency. The previous full integration remains preserved at codex/rtl-digital-evaluation-preserved-20261001.

Prior PR description, retained as historical context

Summary

RTL frequency correction applies completed arrow/keyboard edits after a 200 ms debounce, allowing visual PPM adjustment while receiving. Apply submits immediately; Enter commits the field without activating the parent dialog's Close button. Requested values stay separate from confirmed hardware readback and accepted per-device persistence.

Newer edits survive older completion/refusal. Rejected values are not automatically retried. Closing or hiding the page cancels unsent edits; accepted in-flight correction may finish. Disconnect, capability loss and backend replacement cancel deferred work without replaying it into another session. No new queue, thread, direct USB path, automatic calibration value or synthetic spectrum shift is introduced.

Current head: 9c50425e533d5a3c417a8945d4419e36df53a9ff, stacked on #6002. Reconciled with main at 68cdf661. The later e4eb2ffe update changes audio-pipeline documentation only. This requests human review and preserves the maintainer decisions below; it does not authorize a merge or close RFC #5468. The optional decoder remains a separate draft dependency; this PR cannot land independently of that base as currently stacked.

Validation

The native Apple Silicon/Qt 6.12 downstream aggregate builds with the optional decoder OFF. Its real-keyboard/widget regression and injected capture-worker/model tests pass, including stale responses, refusal, coalescing, synchronous completion, accepted-state geometry and PPM preservation. The worker test follows upstream's single spectrum outlet, while model tests retain actual waterfall-row coverage. The earlier exact 7911324a Nobara checkpoint passed 63 selected tests and the automatic-apply mutation check; that historical evidence is not relabeled as a new exact-head run.

The operator explicitly confirmed live PPM adjustment, and reports days of stability on the later delivered 7601593f build. This does not measure absolute calibration or reacquisition timing on the reconciled head.

Maintainer decision

Apply-on-edit versus Apply-on-click remains an explicit UX decision under RFC #5468. Existing nonblocking review notes about no-change requests during another pending transaction, Enter followed immediately by hide, and window-system hides remain disclosed; this readiness request does not mark them resolved. No blocking defect was reported in the existing PPM review. Principles II, V/XIV and XI govern readback, persistence and verification.


73, Ozy K6OZY · Codex

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

No fixes/closes #NNNN — the governing document is RFC #5468, whose retained-work list asks to "expose PPM correction in radio settings, apply it through the existing backend transaction, and persist only accepted correction under the appropriate device scope. Preserve requested RF frequencies and handle refusal/reconnect coherently." Measured against that, the diff delivers: the entry applies through the existing ppm.set transaction, never claims applied until readback confirms, the widget-level tests pin refusal, supersession, disconnect, capability loss and backend rebind, and the two backend tests pin that adoption moves only hardware.ppm + the capture generation while receivers, RF geometry and the viewport stay fixed. No value or sign is auto-selected, as the RFC requires.

What the RFC text does not ask for is the interaction change this PR is actually about — dropping the requirement to press Apply. That is a workflow change on a control that already worked, it costs a full capture restart per applied value (RtlCaptureTransaction::submit() mints a new revision unconditionally — there is no no-change early-out), and the only authority cited for it is the operator's subjective "confirmed the PPM adjust works great" recorded in #5468. The RFC's own decision line — "Jeremy, please approve or request changes to … the scoped PPM-setting addition" — is still open. I'm not calling that a blocker, but it is a maintainer call, not a review call. See "Needs maintainer decision" below.

Mechanically this is a stacked PR: 7911324a's parent is dea570a2 (#6002, draft), which sits on #5974 → #5973 → #5924. The five-file diff above is the increment; it cannot merge until that stack does.

Scope

File / group What it changes Claimed by the title/RFC? Verdict
src/gui/RtlReceiverSettingsWidget.{h,cpp} Debounced auto-submit, RtlPpmSpinBox Return capture, apply->setAutoDefault(false), hideEvent discard, m_submittedPpm, help text Yes In scope
tests/rtl_receiver_settings_widget_test.cpp Dialog-shaped fixture, real key events, debounce/burst/hide/rebind/synchronous-completion coverage Yes In scope
tests/rtl_capture_worker_test.cpp Adds a waterfallRowReady counter and asserts PPM adoption preserves the WFM receiver + RF geometry and rejects pre-adoption frames Adjacent — backend hardening, no backend source change here In scope, tangential
tests/rtl_device_controls_test.cpp seed() fixture Fm→Fmn, filters -8000/8000→-6500/7500, adds gain/pan/mute/squelch; narrows the view; adds four preservation assertions Explained by 7911324a's message, not by the PR title In scope, see nit

No CHANGELOG.md entry (correct), no new protocol verb, config key or capability field, no settings-persistence surface, no removed guard, no deleted comment naming a symptom, no dead additions — RtlReceiverSettingsWidget is reached from RadioSetupDialog.cpp:906. No socket, QTcpServer/QUdpSocket/QLocalServer/bind/listen/connectToHost or fake-peer construct appears in any added or modified test; nothing to record under the socket-test disclosure.

CodeGuard's nine CG-PATH-001 hits are all in files this PR does not touch (MainWindow.cpp, wdsp_wbfm_test.cpp:2, two vendored third_party trees). Refuted as findings against this diff.

Blockers

None.

Needs maintainer decision

  1. Apply-on-edit replaces Apply-on-click. Arrow keys, the spin buttons, Enter and focus-out now each schedule a hardware transaction 200 ms later, and every accepted transaction restarts the capture — the help text says so ("reception briefly reacquires after a correction"). The Apply button survives, so nothing is lost; the question is whether an RX-path interruption should be reachable from an unmodified arrow-key press on a settings page. RFC #5468 scopes the control, not this interaction, and its decision on the scoped PPM addition is explicitly still requested. Worth an explicit ruling rather than arriving as part of an implementation PR.

Nits (non-blocking)

  • submitPpm() can dispatch a value-unchanged ppm.set — RtlReceiverSettingsWidget.cpp:226. Inline comment; the backend already publishes requestedPpm, which the test fake now mirrors but the widget never reads.
  • An explicit Enter can still be silently dropped — RtlReceiverSettingsWidget.cpp:110. Enter is a commit gesture but it only starts the 200 ms timer; closing the page inside that window discards it. Inline.
  • hideEvent also fires for window-system hides — RtlReceiverSettingsWidget.cpp:211. Inline.
  • Debounce-vs-pumpFor timing margins in the new widget test — tests/rtl_receiver_settings_widget_test.cpp:166. Inline.
  • tests/rtl_device_controls_test.cpp mutates a seed() fixture that pre-existing assertions in the same file depend on. I read the assertions ahead of the change: they cover device writes, the restored-PPM snapshot and displayPeak on the display path, none of which the Fm→Fmn, filter-width, or audioMute/squelch changes touch, and the added comment explains why the narrowed view retains the DC bin. Clean as far as I can tell — flagging only so the fixture change is visible as a deliberate one.
  • tests/rtl_capture_worker_test.cpp:197 adds a second test consumer of IRadioBackend::waterfallRowReady. Open PR #5978 proposes deleting that signal as "an outlet nobody connected". Purely a merge-order note — whichever lands second needs a touch-up.

What I tried to break

  • Request-identity races. Submit R1, edit again, let the debounce mint R2 while R1 is in flight: the id != m_ppmRequest guards in both the result and error lambdas drop R1's late completion and the backend's "superseded" refusal, so no stale reason reaches m_error and no stale value settles the entry. Held. m_bindingGeneration + the captured QPointer source close the same hole across a backend swap.
  • Reentrancy. submit() assigns pending before invokeBackendExtension, so a synchronous extensionResult sees the correct m_ppmRequest, zeroes it, and the reentrant queryStatus()→acceptStatus() runs under QSignalBlocker, so no valueChanged re-arms the timer. The test's synchronousPpm mode exercises exactly this. Held.
  • m_ppmEdited clobbering. Walked confirm-while-newer-edit-pending, refuse-while-newer-edit-pending, refuse-then-Apply-retry, and reverting to the confirmed value while a different value is in flight. In each case the applied label kept the confirmed value and the entry kept the operator's newer number. Held.
  • Lifecycle. Disconnect, reconnect, capability loss and restore, backend replacement and widget destruction all stop the timer (bindBackend() first line; the QTimer is parented to this). Stale m_submittedPpm is only read while m_ppmRequest != 0, which bindBackend() clears. Held.
  • The Return override. QAbstractSpinBox::keyPressEvent deliberately ignore()s Enter so QDialog::keyPressEvent can click the autoDefault button — RadioSetupDialog.cpp:1100 builds exactly the Close-only QDialogButtonBox the test mirrors, so without this override Enter would have closed the dialog. The check(dialog.isVisible(), …) assertion is mutation-sensitive (key propagation to the parent happens in QApplication::notify, so sendEvent reproduces it). Held.
  • Aggregate-init trap in the changed fixture. {…, Mode::Fmn, 37, 68, true, true, 43} maps to audioGain/audioPan/audioMute/squelchEnabled/squelchLevel, leaving wfmDeemphasisUs at its defaulted 75 — had it landed on wfmDeemphasisUs=43, submit()'s 50-or-75 validation would have rejected the whole fixture. Correct as written.
  • Could not check: anything requiring a build or a run. Both hand-checked failure modes below are reasoned from the code, not reproduced. Worth noting that CI on 7911324a has only two completed checks — "Static checks" and "Sanitizer option configures". No compile job and no ctest ran, and rtl_receiver_settings_widget_test is not in any ctest -R filter in ci.yml, so nothing in the green badges shows the new tests build, let alone pass.

Recommendation

Needs maintainer decision. The engineering is careful — the confirmed-readback discipline, request fencing and lifecycle handling all survived the attacks above, and the widget test is unusually honest about non-events. What needs a ruling is the interaction itself: auto-apply makes an RX-path capture restart reachable from a bare arrow-key press, and RFC #5468's decision on the scoped PPM addition is still outstanding with only a subjective operator report behind it. Next step: get that ruling on the auto-apply policy, fold in the requestedPpm comparison so a no-change submit cannot restart the capture, and get a real build+test run on this head before anyone reads the checks as green.

Thanks for the thorough test work here — the hidden-page, rebind and synchronous-completion cases are the ones that usually go untested, and they are all pinned.


🤖 aethersdr-agent · cost: $6.4046 · model: claude-opus-5

Comment on lines +225 to +231
if (m_ppmRequest && requested == m_submittedPpm) { renderStatus(); return; }
if (!m_ppmRequest && !m_confirmed.value(QStringLiteral("pending")).toBool()
&& requested == m_confirmed.value(QStringLiteral("ppm")).toInt()) {
m_ppmEdited = false;
renderStatus();
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This no-op guard only applies when the backend reports pending == false, so when something else is mid-transaction the widget submits a ppm.set whose value equals the applied value.

Reachable path: click Suppress IQ DC (m_dcRequest set, backend publishes pending: true), then nudge the ppm arrows up and back down. The debounce fires with m_ppmRequest == 0, pending == true, requested == confirmed → a ppm.set goes out. RtlSdrBackend::requestCapture() → RtlCaptureTransaction::submit() has no no-change early-out: it mints a new revision unconditionally, so that costs a full capture restart and the reacquire the help text warns about, for a value that did not change.

The information needed to avoid it is already on the wire — deviceSettingsStatus() publishes requestedPpm (RtlSdrBackend.cpp:2023), and this PR even adds that key to the test fake's accepted map, but the widget never reads it. Comparing against the backend's own latest-requested value would make the guard work while pending is true, and would drop the client-side m_submittedPpm shadow in favour of the radio-authoritative field (Principle II/III shape). Reasoned from code; I have no build here.

connect(m_ppm, &QSpinBox::valueChanged, this, [this] {
if (!isVisible() || !m_haveState) { return; }
m_ppmEdited = true;
m_ppmTimer->start();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enter is an explicit commit gesture — the help text added above says the correction applies "after … a completed entry (Enter or focus change)" — but it only starts the 200 ms timer. Press Enter and close the page (or switch settings pages) inside that window and hideEvent() stops the timer and reverts the entry, so the committed number is silently discarded with no indication.

The debounce exists to coalesce arrow/spin bursts; Enter and focus-out are already single, deliberate, terminal gestures. Flushing them immediately (connect QAbstractSpinBox::editingFinished to submitPpm, or call it from the RtlPpmSpinBox Return branch) would keep the coalescing where it is needed and make the help text literally true.

// Discard only unsent input. An accepted in-flight correction is not a
// preview and is still allowed to complete and persist after the page closes.
const QSignalBlocker block(m_ppm);
m_ppm->setValue(m_ppmRequest ? m_submittedPpm : m_confirmed.value(QStringLiteral("ppm")).toInt());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hideEvent is being used as "the operator navigated away from this page", but it also fires for window-system-driven hides — hiding the application (macOS ⌘H) or platform-driven hides of the dialog window send hide events down to visible children. In that case an in-progress, not-yet-debounced edit is silently reverted to the applied value while the operator believes the dialog is merely behind something.

If you want to keep the discard scoped to a real page/dialog change, event->spontaneous() distinguishes them: programmatic setVisible(false) (stacked-page switch, QDialog::close) delivers a non-spontaneous hide, while a platform hide delivers a spontaneous one. I have not run this, so worth confirming on the platforms you can drive before changing anything.

Comment on lines +166 to +169
ppm->setValue(30); pumpFor(40);
ppm->setValue(31); pumpFor(40);
ppm->setValue(32);
check(source->controlRequests == beforeBurst, "rapid PPM edits do not dispatch an intermediate value");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This asserts a non-dispatch across two pumpFor(40) gaps against a 200 ms debounce, so it depends on the process not being starved for 200 ms between the setValue calls. pumpFor busy-loops with processEvents(AllEvents, 10) + msleep(1), which is fine on an idle machine, but the full suite runs ctest in parallel and a 200 ms scheduling stall on a loaded runner turns this into a red test on correct code.

The several pumpFor(260) waits have only a 60 ms margin over the same 200 ms interval, in the other direction.

A test-only seam for the interval (or driving the timer explicitly) would make the burst-coalescing assertion deterministic rather than margin-dependent. Non-blocking — flagging it as a flake risk, not a defect in the feature.

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (9 finding(s))

  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6003/src/gui/MainWindow.cpp:9560
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6003/src/gui/MainWindow.cpp:9672
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6003/src/gui/MainWindow.cpp:9791
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6003/src/gui/MainWindow.cpp:9792
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6003/src/gui/MainWindow.cpp:9793
  • [MEDIUM] CG-PATH-001 — Potential path traversal in tests/wdsp_wbfm_test.cpp /tmp/aetherclaude/pr-6003/tests/wdsp_wbfm_test.cpp:2
  • [MEDIUM] CG-PATH-001 — Potential path traversal in third_party/faad_hdc/upstream/libfaad/common.h /tmp/aetherclaude/pr-6003/third_party/faad_hdc/upstream/libfaad/common.h:39
  • [MEDIUM] CG-PATH-001 — Potential path traversal in third_party/wdsp/upstream/wbfm.c /tmp/aetherclaude/pr-6003/third_party/wdsp/upstream/wbfm.c:28
  • [MEDIUM] CG-PATH-001 — Potential path traversal in third_party/wdsp/upstream/wdsp.h /tmp/aetherclaude/pr-6003/third_party/wdsp/upstream/wdsp.h:789

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $6.5366 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Antares Detector — candidate vulnerable file(s)

  • src/core/RtlDeviceSettings.cpp

The backend parses and validates PPM correction values from QJsonDocument; if the backend implementation copies or manipulates those values into buffers (or otherwise uses untrusted input) without bounds checks, it can lead to out-of-bounds writes or use-after-free.

Localized by Cisco Foundation AI Antares-1B running locally in the AetherClaude sandbox, seeded by the Cartographer security map. Advisory only — please verify before acting.


🤖 aethersdr-agent · cost: $6.5366 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Thanks for the careful write-up on this one — the failure is a stale generated file, not a problem with your code.

What failed

One step, in Static checks → aetherd touchpoint manifest (static-checks.yml:178):

::error::aetherd-touchpoints.md is stale — run `python tools/gen_touchpoint_manifest.py`

"Sanitizer option configures" passed. The other run on this SHA (36948659007) was cancelled by your newer push — not a second failure.

Fix

python tools/gen_touchpoint_manifest.py

One line in docs/architecture/aetherd-touchpoints.md:

-| `core/backends/RadioCapabilities.h` | 10 | universal — Backend-neutral capability descriptor …
+| `core/backends/RadioCapabilities.h` | 11 | universal — Backend-neutral capability descriptor …

I ran that at 07cd6c56 and --check passes; the diff is 1 insertion, 1 deletion and nothing else.

Why it drifted — worth a look, because it will come back

Not a missed regeneration on your part. The manifest row is a count of UI includers (gen_touchpoint_manifest.py:40 — src/gui/ + src/main.cpp), and a count merges textually without conflict even when both sides changed the underlying set. At merge 707b1b0d ("Reconcile WFM stereo and applet with current RTL stack"), each parent was self-consistent:

UI includers of RadioCapabilities.h manifest row
824b784a (#5973 head) 10 — has TxAudioPathPolicy.h, no WfmApplet.h 9
6e87be85 (your signed applet checkpoint) 10 — has WfmApplet.h, no TxAudioPathPolicy.h 10 ✅
707b1b0d (merge) 11 — the union 10 ❌

TxAudioPathPolicy.h arrived from main, WfmApplet.h from your applet work. Git had no textual conflict to show you, so nothing flagged it.

A note on bisecting this if you try it yourself: the signal is non-monotonic, so git bisect run gives a wrong answer. --check passes at 6607ce9e's descendants 94aa7656, 208bc220 and 1dfa3f3e, then fails from 707b1b0d onward — a plain bisect reports 6607ce9e instead.

The upstream branches need it too

I checked the stacked heads, and regenerating only here will re-conflict on the next merge down:

Regenerating in #5973 and #5974 as well should settle it for the whole stack.

On the inline review comments

The four bot comments on RtlReceiverSettingsWidget.cpp and rtl_receiver_settings_widget_test.cpp (the pending == true ppm no-op guard, editingFinished, hideEvent spontaneity, the 200 ms debounce margins) are independent of this failure — none of them touch includes or the manifest. The debounce-margin one is a CI-flake observation, but it isn't what broke this run. Worth their own read on the merits.

Nothing here needs a rebuild — just the regenerated manifest. Nice work on the PPM fencing.


🤖 aethersdr-agent · cost: $7.2366 · model: claude-opus-5

@Ozy311
Ozy311 merged commit 5e39790 into codex/rtl-native-hd-fm Oct 2, 2026
@Ozy311
Ozy311 force-pushed the codex/rtl-native-hd-fm branch from b6f87a9 to 2602312 Compare October 2, 2026 02:17
@Ozy311
Ozy311 deleted the codex/rtl-live-ppm branch October 2, 2026 02:17
@Ozy311
Ozy311 force-pushed the codex/rtl-live-ppm branch from 9c50425 to 5e39790 Compare October 2, 2026 02:17
@Ozy311
Ozy311 restored the codex/rtl-live-ppm branch October 2, 2026 02:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant