Skip to content

aetherd: consolidate slice receive-control routing - #5919

Merged
ten9876 merged 61 commits into
aethersdr:mainfrom
rfoust:codex/aetherd-m4-receive-controls
Oct 4, 2026
Merged

ten9876 merged 61 commits into
aethersdr:mainfrom
rfoust:codex/aetherd-m4-receive-controls

Conversation

@rfoust

@rfoust rfoust commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Closes #5917. Continues the accepted aetherd RFC #3849, #5262 M4 and #5554 §2.3 with one consolidated desktop receive-control migration. It does not finish those parent trackers or add headless audio/control features.

#5907 and #5924 have merged. #5924 landed on 2026-10-04 as signed squash a325fb99; signed integration merge 5748e69b reconciles that current main with all newer #5919 fixes, without a force-push. It preserves the landed RTL parked/free capture pipeline, current-main squelch scale/UI and Radio Setup test dependencies, HL2 APF/squelch/AGC-off additions, per-receiver RIT/XIT and WAN receive admission. Jeremy's merge-order ruling is satisfied. Jeremy's Oct 3 ruling ratified the prerequisite RTL scope and seam/include additions. The effective remaining diff is 37 files for this receive-control migration. HD Radio decoding and HD consent remain outside this PR. The local-success/unsupported classification is decided (maintainer ruling, 2026-10-04): an operator-moved control the backend cannot apply refuses aloud through the existing once-per-session notice and log; refused compatibility-origin writes (Kiwi suppression mute, NRS profile restore) are logged without the notice.

  • Route per-slice AF gain/mute/balance, NB/NR/ANF/manual-notch/APF and existing Flex v4 DSP, squelch, RX antenna and slice lock through typed backend adapters. Remove 31 additional raw Flex command sites from SliceModel: the current-main command-plane count drops 400 → 369, without double-counting aetherd: migrate receive controls behind the backend seam #5907's seven sites.
  • Share one model-owned binding across both slice construction paths. Preserve notifications and independent observations; guard duplicate wiring, disconnected/staged/retired objects, same-ID reuse, off-thread intents, deleted objects and superseded nested edits.
  • Preserve exact Flex field encoding behind the guarded slice sink, including independent DSP fields and separate squelch writes. Keep Kiwi's local replacement controls and the existing Flex primary-audio mute handoff/status reassertion. HL2/ANAN/RTL still refuse primary-native suppression: this migration does not add a missing Kiwi handoff to those families. Preserve the explicit NRS firmware-default profile-restore exception.
  • Keep local tune lock, Flex per-slice lock and Icom global dial lock distinct. Icom, HL2, ANAN and optional RTL adapt only their existing implementations, including HL2 CW APF, mode-aware squelch and AGC-off gain; Demo NB/ANF enable still affect its real generator. Unsupported controls report through the existing notice/log path instead of silently succeeding.
  • Consume ANAN NB from landed feat(anan): run WDSP's noise blanker from the NB button. Principle IV. #5824 rather than regressing it to an unsupported request. Preserve the landed split-audio recorder with one shared typed-intent translation; radio readback, Kiwi compatibility suppression, automatic replay and non-operator writes remain outside preference learning. No new DSP implementation, settings or preference defaults are added. Unsupported operator dispatch now produces the existing user-visible notice/log instead of a silent backend no-op (Demo AF gain/mute/pan, squelch and NB level, and Icom mute/pan, have no local effect, so they refuse aloud); refused compatibility-origin writes are logged only.
  • Restore fix(rtl): gate worker spectrum and read errors after disconnect. Principle VIII. #6107's RTL post-disconnect gate (RtlSdrBackend::wireWorker(), read-error rule-6 check) and its stranded-worker regression in rtl_backend_test, which RTL M1: confirmed receive state, free capture browsing and finer zoom #5924's squash a325fb99 dropped from main; adapted to the token-qualified pipeline, not extended. IRadioBackend rule 6: spectrum has no base-class gate (RtlSdr/HL2 forwards unverified) #6096's base-class spectrum choke point stays with that issue.

The new adapters are in-process C++ contracts, not wire-protocol methods. ReceiveDispatch distinguishes dispatch, client-only lock and unsupported requests; it is not hardware acknowledgement. Existing backend hooks remain for daemon/internal callers. Desktop support does not expand daemon capabilities or bypass its authorization/admission/readback rules.

Not included: TX, RIT/XIT, FM repeater/digital offsets, diversity/session lifecycle, panadapter command conversion, DAX/PCM transport, new radio features, UI redesign, settings or dependencies. Flex manual notch remains unsupported (TNF is separate); no Demo capability flip or independent mixer is invented.

Constitution principle honored

Principle II: dispatch and observation remain distinct, including synchronous backend corrections and explicitly named compatibility origins. Principle XI: regression and mutation evidence demonstrate the nested-squelch ordering fix and retained Demo worker effect. Principle VI: no transmit authority or keying operation is added.

Current integration verification (2026-10-04)

Signed head: 5748e69b758ad57214096c1fdafccb0e1f828171; tested current main / landed #5924 squash: a325fb99e25c8f830f6fda835569abe23b7615db. Signed merge parents are 8c88612b and a325fb99; no force-push or historical contributor replacement.

  • Native macOS ARM64 / Qt 6.12 app, daemon and 49 selected test targets built at two build jobs. RADE, matching QtKeychain and optional RTL are enabled. CMake host/system processors and both binaries are ARM64; no RNNoise x86 sources are present. RTL uses an isolated scratch dependency prefix; no USB device was opened. DFNR and sherpa are unavailable; existing Qt deprecation and offline Metal compiler warnings remain.
  • 49/49 focused CTests passed serially, including the previous 41 receive-control/backend/RTL/split-audio/GUI regressions, current-main squelch_level_scale_test, and all seven Radio Setup consumers, including peripheral_auth_dialog_test. This is a focused target build and test run, not the full repository build or suite. No fake firmware peer or frozen PR-gate test was added.
  • The old head 8c88612b failed hosted macOS/Linux linking at peripheral_auth_dialog_test: its RadioSetupDialog object lacked RtlReceiverSettingsWidget. Landed main supplies this and the companion Radio Setup dependencies; the reconciled CMakeLists.txt and tests/tests.cmake are byte-identical to main. The formerly failing target and all seven consumers now build and test locally. This is not a disk-exhaustion fix.
  • Current-main squelch scale/UI and its expanded widget tests are preserved; the retired prerequisite ReceiveSquelchModel duplicate is absent. The FM widget test observes the new typed request and retains both positive dispatch-count and level-44 assertions. Current-main per-receiver RIT/XIT, HL2 digital-mode AGC-off defaults/memory, and WAN admission remain intact.
  • Production MCP/Demo passed on an authenticated, identity-matched owned instance with fresh settings and TX disabled: bidirectional linked-slice fixtures and two Demo connect/control/disconnect cycles cover mode/frequency/filter/AGC, NB/ANF, squelch, GUI AF gain/mute/pan and lock. Captures were 1,920,000 bytes / 773 chunks and 1,918,976 bytes / 690 chunks. Exported PCM at raw, post and final taps was finite and nonzero in both cycles; this is in-process audio evidence, not physical speaker output. The owned app was stopped; no physical radio was connected.
  • Demo AF/squelch/NB-level model assertions remain presentation checks, not backend-effect proof. The fresh run also observed explicit audio/squelch/DSP refusal logs. Actual Demo NB/ANF generator effects remain covered by the contract test. This does not turn unsupported controls into a genuine local mixer/DSP implementation.
  • Static gates passed: engine boundary remains 102 findings with no additions, command plane 400 → 369, capability records 70/70, strict test registration, frozen CI gate, generated touchpoint manifest, seam-probe table and whitespace. The effective diff against landed main is 37 files.
  • The Oct 3 APF/squelch/RTL guard mutation evidence below is retained historical evidence; mutations were not repeated in this refresh. Hosted CI must finish on the pushed head. RTL M1: confirmed receive state, free capture browsing and finer zoom #5924 is merged, but aetherd: consolidate slice receive-control routing #5919 stays draft for the unresolved maintainer decision and subsequent re-review. No fresh Windows/Linux VM, live-radio, GPU presentation or physical speaker-output claim is made.

Previous integration verification (historical, 2026-10-03)

Signed head: 8c88612b46c93f50ab9d282097f1c9d79c20ddf8; current main: d9de044c7e895185bac86dfbdb3dc79e8e539c2e; prerequisite #5924: 5dd50ff74dd8d397a9fdb428a4522338928a5924. No historical contributor changes were discarded; the branch is updated with signed integration merges, not a force-push.

  • Native macOS ARM64 / Qt 6.12 app, daemon and focused test build passed. RADE, matching QtKeychain and optional RTL are enabled; CMake host/system processors and both binaries are ARM64, with no RNNoise x86 sources. RTL uses an isolated scratch dependency prefix; no USB device was opened. DFNR and sherpa are unavailable. Existing Qt deprecation/offline Metal compiler warnings remain.
  • 41/41 focused CTests passed, including receive contracts/routing/admission, main's APF/AGC-off and dead-control regressions, HL2 actual worker APF/squelch/NB/AGC-off, RTL confirmed-capture/worker/model/pipeline/lifecycle tests, ANAN NB, split-audio, Kiwi/slice-letter and GUI squelch reconciliation. These are not the full repository suite and add no fake firmware peer or frozen PR-gate test.
  • Deliberately removing HL2 APF dispatch, removing HL2 squelch dispatch, and bypassing the RTL connected-frame guard each made the relevant regression test fail. All mutations were restored; final focused tests passed on the clean signed source.
  • Production MCP/Demo passed: authenticated owned instance, isolated settings, bidirectional linked-slice fixtures, and two Demo connect/control/disconnect cycles covering mode/frequency/filter/AGC, NB/ANF, squelch, GUI AF gain/mute/pan and lock. TX was disabled and no physical radio was used. Captures reported 1,921,024 bytes / 776 chunks and 1,920,000 bytes / 775 chunks; aggregate RX capture was verified, not physical speaker output or individual tap sample quality. The owned app was stopped.
  • Demo AF/squelch/NB-level model changes are presentation checks, not backend-effect proof. The MCP run also observed explicit audio/squelch/DSP refusal logs. Demo NB/ANF enable has separate actual-generator coverage in the contract test. Icom mute/balance and Demo AF/squelch/NB-level remain unsupported; changing these to genuine client-local success is left for maintainer decision, not inferred from optimistic state.
  • Static gates passed: engine boundary has the same 102 findings as current main, command plane 401 → 370, capability records 70/70, test registration, frozen CI gate, generated touchpoint manifest, seam-probe table and whitespace. RTL M1: confirmed receive state, free capture browsing and finer zoom #5924's five GUI→core include additions are prerequisite changes covered by Jeremy's ruling, not new incremental additions here.
  • Fresh hosted CI must finish after this push. RTL M1: confirmed receive state, free capture browsing and finer zoom #5924 is still open, so aetherd: consolidate slice receive-control routing #5919 stays draft. No fresh Windows/Linux VM, live-radio, GPU presentation or actual speaker-output claim is made.

Previous integration verification (historical, 2026-10-01)

Current head: 4ab5ba8e160697b4992a320e9856dfaa29adaee4; application and nine-test production build: da77a241c5797c87de1de0438b5ac16713fc427f. The later commit changes only the FM widget test to observe the typed squelch request; main 142f1d63; prerequisite #5924 f89d2577.

  • Native macOS ARM64 / Qt 6.12 incremental application build and nine test targets passed, with optional RTL enabled. DFNR, QtKeychain and sherpa were unavailable in this build.
  • Headless focused CTests: 10/10 passed across two selections (backend receive contract, receive intent routing, RTL model acceptance, Flex mode intent, slice letter/Kiwi, split-audio profile, ANAN NB readback, GUI squelch reconciliation and squelch memory, plus the separately built/run FM filter controls regression on the current test-only fix). These are offline tests; no firmware peer or USB device was opened.
  • Owned offscreen Demo bridge: NB/ANF on/off and optimistic NB-level model assertions passed (NB level was not backend-effect proof), as did model connection and AM/AGC assertions. Captured raw PCM was finite and nonzero. Two attempts captured only raw PCM; post/final output was not verified, so no end-to-end audio-output claim is made. TX was pinned off and each owned app was stopped.
  • Command-plane checker passed at baseline 375, with zero strict violations; staged whitespace check passed; the integration commit has a verified SSH signature.
  • Fresh hosted CI must complete on this head. RTL M1: confirmed receive state, free capture browsing and finer zoom #5924 remains open, so aetherd: consolidate slice receive-control routing #5919 remains draft. This evidence does not establish live radio or GPU-rendering behavior.

Earlier refresh evidence (historical)

Before integration with current main and #5924

Refreshed source head: b9e4d90344e696ee45d417cd132d3c0bf398af18; main 2700cb6d5ba08d9ee42c3a2fcbf626a07f1d915a; prerequisite #5907 04fd459e81ce8214e2762ca9ca6695e2ae19aace.

  • Fresh native Mac ARM64/Qt 6.12 app, daemon and 25 focused targets: passed; RADE enabled, both binaries and CMake host/system processors verified ARM64, no RNNoise x86 sources. Optional RTL-SDR and DFNR are absent in this local build and are not claimed tested. Existing Qt deprecation and offline Metal compiler warnings remain.
  • Focused tests: 25/25 passed after the final build; the receive contract, intent routing, ANAN NB readback and split-audio tests additionally passed three runs each. The split-audio operator-origin mutation failed three assertions and was restored; the ANAN fail-before/pass-after evidence is detailed below.
  • Production MCP/Demo, isolated settings and TX disabled: passed. Authenticated owned app with matching PID/socket/label; bidirectional linked-fixture tuning; two Demo connect/control/disconnect cycles covering mode, frequency, filter, AGC, NB/ANF, squelch, GUI AF gain/mute/pan and lock. Each cycle captured 1,920,000 RX bytes / 763 chunks across raw/post/final taps. Demo AF/squelch remain presentation checks, not proof of independent DSP/mixer support. No physical radio or TX was used; the owned app was stopped. Offscreen checks do not prove GPU presentation.
  • Static engine boundary (102 tracked findings, zero new blockers), command-plane baseline (375), capability records (71), test registration, frozen gate, generated touchpoint manifest, seam-probe table and whitespace: passed.
  • Fresh hosted CI for this head: pending. This remains a draft regardless of CI while prerequisites are open.

The existing anan_noise_blanker_readback_test failed four reconnect/model assertions against the integrated source without the ANAN adapter. Its final run additionally drives the typed request through the actual worker and checks requested versus applied state. split_audio_profile_test shares production translation helpers and now pins compatibility suppression/readback exclusion inside an operator scope, alongside existing repeat/replay/restore/non-operator cases. Removing the shared production helper's operator-origin guard caused three assertions to fail; the mutation was restored before final validation. The focused set also includes the newly landed hl2_dbref_test.

Historical implementation verification

The evidence below predates this refresh. It is retained as historical context, not fresh Windows/Linux VM or real-radio validation of the refreshed head.

Integrated source head: c68e3b4714ae9f1c6dbb9b54964021db602f12c6.

Platform Native app / daemon Focused CTests Production MCP / Demo
macOS ARM64 / Qt 6.11.2 Passed, RADE enabled; architecture verified 18/18; receive tests repeated ten times each Passed, two cycles
Debian ARM64 / Qt 6.8.2 Passed; architecture verified 18/18; receive tests repeated ten times each Passed, two cycles
Windows MSVC x64 / Qt 6.8.3 Passed; x64 architecture verified 18/18; receive tests repeated ten times each Passed, two cycles

Main subsequently advanced to 7f68dda0 (separate FFTW planner fix). An isolated
review merge with this head was conflict-free (tree db520634e4b575c98c8100f2e17ed349688bb9c9);
native Mac builds and the two receive tests plus fftw_planner_lock_test passed
on that merge result. The full three-platform matrix above uses the fixed head.

The focused set covers the two receive contract/routing tests; daemon receive/frequency admission; backend lifecycle, family switch, affinity and capability revision; Kiwi/slice-letter, squelch memory and GUI reconciliation; APF and antenna aliases; Demo, HL2 NB, ANAN and nested GUI lifetime regressions. These are not the entire repository suite. No test or workflow is added to the frozen per-PR gate; the existing unconditional targets run in main full-suite and sanitizer lanes.

All modified tests are socket-free. Icom uses the real scheduler with an unstarted transport and separately injected readback, not firmware simulation or a radio ACK. HL2 uses its actual RX worker without network/TX startup. Demo's NB is checked in the worker and ANF against generated sample energy, not merely a copied model flag. RTL is explicitly reported as not built where librtlsdr is absent; no USB hardware is opened.

Mutation evidence: removing the Demo NB worker call makes backend_receive_contract_test fail; replacing the squelch pending-enable accumulation makes the nested-edit assertion in receive_intent_routing_test fail. Both mutations were restored. Self-review found and fixed that squelch regression, cold Icom false acceptance, and an APF test namespace error. Added Kiwi handoff ordering and Icom antenna admission/readback coverage passed without further production changes.

MCP checks use the unchanged production server, fresh settings, authenticated owned instances, Qt offscreen and TX disabled. They exercise the actual MainWindow linked-slice adapter on disconnected fixtures, then two Demo connect/control/disconnect cycles, NB/ANF, gain/mute/balance, lock, squelch and bounded positive RX capture. Demo AF/squelch are presentation checks, not a claim of independent mixer/DSP support. Offscreen proof does not establish GPU presentation. Each test-owned app is stopped afterward.

Receive-only FLEX-6700 / firmware 4.2.18.41174 smoke also passed on the implementation before the mechanical #5834 integration: NB/NR/ANF, AF gain/mute/balance and slice lock, successful native replies and lock status, restoration of original controls, preserved frequency/mode, then 497 RX chunks / 1,438,720 captured bytes and clean disconnect. TX remained disabled. RX antenna was only reselected, not changed; squelch, extended DSP and other models are not claimed as live-tested. First-run slice readiness and a capture attempted while muted were harness issues; the restored-state capture passed.

Known pre-existing test issue: the 40-ms startup assertion in sim_backend_test intermittently failed during concurrent builds, reproduced on unchanged #5907 as well as this branch. Tracked separately in #5918; not claimed fixed here. The final focused test results are reported above.

Linux linked debug sections are omitted to fit the VM's 17-GiB root disk; object debug information is preserved. Windows uses its matching Qt SDK plugin path for offscreen tests and native ARM64-hosted MSVC targeting x64. The compiler switch reset nested Opus options; restoring the repository's Release/DRED/OSCE settings fixed the resulting RADE/CRT link failure without disabling RADE or changing source. During the relocation rebuild, timestamps were reused only for files whose hashes matched both complete source manifests; changed files retained fresh timestamps. Packaging resumed only after re-verifying all source hashes. No source content or validation checks were skipped.

Static gates pass: command-plane baseline, engine boundary (102 existing findings, zero new), capability-record freeze (71), test registration, frozen CI allowlist, generated touchpoint manifest and whitespace. No CHANGELOG.md edit.

  • Review and regression/mutation checks completed
  • Final native VM and MCP validation completed
  • Bounded real-radio receive check documented with coverage limits
  • Hosted CI passed on historical head c68e3b47; fresh-head status is reported above

Checklist

  • Authored commits are signed and locally verified
  • No new settings keys or persistence ownership
  • Clean-room implementation; Flex encoding checked against published FlexLib 4.2.18 source
  • No meter/UI design change
  • Architecture/test documentation updated
  • No new security-sensitive protocol or credential surface

Generated with OpenAI Codex (GPT-6 Astra)

@rfoust rfoust self-assigned this Sep 22, 2026
Route complete capture requests through bounded transactions with verified rollback, superseded-result compensation, session fencing and block-boundary receiver adoption. Retain the legacy single-receiver DDC while later M1 work adds extraction and mixing. Pin injected worker publication and delayed-reader retirement.

ten9876 commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Heads-up: merge order with #5924, and what the rebase needs to preserve

@rfoust, I've ruled that #5924 (RTL M1) merges first, and this stack (#5892 → #5907 → #5919) rebases onto it. Details are in the rulings on #5924. This comment gives you the collision map up front, so it doesn't surprise you when the prerequisites land.

The collision

A trial merge of this head (c68e3b47) against #5924's head (d42619f1) gives 6 content conflicts, about 365 lines, all in the shared receive seam:

File Conflict
src/models/RadioModel.cpp 4 hunks, 213 lines
src/models/SliceModel.h 46 lines
src/core/backends/IRadioBackend.h 44 lines
src/core/backends/rtl/RtlSdrBackend.cpp 26 lines
src/models/RadioModel.h 23 lines
src/models/SliceModel.cpp 13 lines

The two PRs independently built the same guards in different shapes. This PR has typed tune requests with revision counters, QPointer liveness, an off-thread check, retired-object / same-ID refusal, and an isConnected gate. #5924 has ReceiveControlPolicy::{Optimistic,Confirmed} on SliceModel, a canDispatch lambda in wireSliceAudioIntentsToBackend (same-thread, live-object, connected), and an unconditional same-thread guard on 8 SliceModel setters. Neither is a subset of the other.

What the rebase has to preserve

  1. RTL's confirmed publication, expressed through your typed dispatch. Your body already draws the key line: "ReceiveDispatch distinguishes dispatch… it is not hardware acknowledgement." For a Confirmed backend, the model must not publish frequency, mode, filter, audio or pan geometry until the backend reports the value. Pending, refused and superseded requests stay invisible, and stable IDs stay sparse (a sole restored ID 3 must work). rtl_model_acceptance_test is the pin. Making setFrequency optimistic for RTL makes it fail on "pending sparse receiver edits do not change observed getters" and three more checks, so CI will tell you if the reconciliation drops this.
  2. One implementation of each guard. Please don't let both the canDispatch lambda and your binding survive side by side, or two copies of the off-thread check. Keep one binding, one liveness/same-ID check and one thread check. If yours subsumes RTL M1: confirmed receive state, free capture browsing and finer zoom #5924's, remove RTL M1: confirmed receive state, free capture browsing and finer zoom #5924's.
  3. The Flex mode path. RTL M1: confirmed receive state, free capture browsing and finer zoom #5924 moves the Flex modeChangeRequested binding out of handleSliceStatus into the shared binding. I've asked for flex_slice_mode_intent_test to land with RTL M1: confirmed receive state, free capture browsing and finer zoom #5924, so that path is pinned before your rebase touches it. Keep it green: one setMode gives exactly one slice set N mode=…, and Flex frequency/filter edits never also dispatch through the neutral seam.

No action is needed until #5892 and #5907 land. This is so the refresh you already planned ("Hold for the prerequisites and refresh the diff after they merge") accounts for #5924 as well.

— Jeremy (KK7GWY)

Drafted with Claude Code at my direction.


Generated by Claude Code

Preserve absolute receiver RF while validating whole-set DC placement. Crop genuine FFT bins for independent display pan/zoom, and admit confirmed receiver plus view intent together so refusals and rollbacks preserve observations. Add capability-gated explicit placement, accepted-capture diagnostics, and synthetic DSP/model/UI regressions.

Nobara native RTL ON: full app and 27 focused tests passed. RTL OFF: full app and 15 focused tests passed. Three deliberate regressions fail and restored controls pass. Production receiver admission remains one. Scoped UX and neutral seam decisions require maintainer ratification under RFC aethersdr#5468.
Keep configured slice RF and settings fixed while a confirmed RTL capture drag moves across the tuner range. Park receivers outside the fully guarded usable capture and resume them after a bounded, verified bank transition.

Expose capture membership to the desktop, automation observations, and headless resources. Center typed tunes against the actual FFT viewport, coalesce pending zoom intent, and guard parked FM filters. Cover transaction, model, worker, restore, and real widget paths with native Nobara validation.
…notice. Principle VIII.

reportReceiveDispatch raised commandDropped for every refused typed request,
including writes the operator never made. HL2, ANAN and RTL deliberately
refuse the Kiwi suppression mute (Origin::ExternalReceiveSuppression), and
the Kiwi virtual-antenna path has no Flex gate, so picking a Kiwi antenna on
those radios showed "This radio doesn't support that control" with no
control touched and spent the once-per-session notice. Refusals are still
logged on aether.protocol, now tagged operator or compatibility origin;
only operator-origin refusals emit commandDropped.

Also give SliceSquelchRequest the valid() its siblings have and use it in
the Flex, HL2, Icom and RTL adapters instead of four copied range checks,
and log off-thread squelch writes through refuseOffThread like the other
setters rather than dropping them silently.

receive_intent_routing_test pins the notice split (fails with the origin
guard removed); backend_receive_contract_test pins the squelch range on
valid() and the Flex adapter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 4, 2026 07:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

It bundles separately tracked #6096 lifecycle work and has two explicit coding-convention violations.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

This PR migrates desktop per-slice receive controls from raw Flex commands to typed backend dispatch while preserving backend-specific behavior and readback authority.

Changes:

  • Adds typed DSP, audio, squelch, antenna, and lock requests across six backends.
  • Consolidates guarded intent routing and reentrancy handling in RadioModel and SliceModel.
  • Updates documentation, regression coverage, and the reduced command-plane baseline.
File Description
docs/​architecture/​backend-receive-contract-tests.md Documents receive-control contracts and coverage.
docs/​rtl-m1-runtime.md Documents RTL typed control behavior.
src/​core/​backends/​IRadioBackend.h Adds typed desktop adapter methods.
src/​core/​backends/​ReceiveCommand.h Defines typed receive requests and results.
src/​core/​backends/​anan/​AnanBackend.cpp Routes supported ANAN controls.
src/​core/​backends/​anan/​AnanBackend.h Declares ANAN adapters.
src/​core/​backends/​flex/​FlexBackend.cpp Encodes typed requests as Flex commands.
src/​core/​backends/​flex/​FlexBackend.h Declares Flex adapters and command sink.
src/​core/​backends/​hl2/​Hl2Backend.cpp Implements HL2 receive adapters.
src/​core/​backends/​hl2/​Hl2Backend.h Declares HL2 adapters.
src/​core/​backends/​icom/​IcomCivBackend.cpp Maps supported controls to CI-V.
src/​core/​backends/​icom/​IcomCivBackend.h Declares Icom adapters.
src/​core/​backends/​rtl/​RtlSdrBackend.cpp Adds confirmed RTL controls and lifecycle changes.
src/​core/​backends/​rtl/​RtlSdrBackend.h Declares RTL adapters and test access.
src/​core/​backends/​sim/​SimBackend.cpp Routes Demo NB and ANF controls.
src/​core/​backends/​sim/​SimBackend.h Declares Demo adapter and test hook.
src/​core/​backends/​sim/​SimSignalSource.h Exposes test-only generator access.
src/​gui/​MainWindow_Wiring.cpp Learns split audio from typed intents.
src/​gui/​SplitAudioProfile.h Translates typed audio requests for learning.
src/​models/​RadioModel.cpp Consolidates guarded backend dispatch.
src/​models/​RadioModel.h Declares receive-routing helpers.
src/​models/​SliceModel.cpp Publishes typed intents with revision guards.
src/​models/​SliceModel.h Defines intent signals and revision state.
tests/​anan_noise_blanker_readback_test.cpp Exercises typed ANAN NB dispatch.
tests/​antenna_alias_test.cpp Verifies typed antenna intent.
tests/​backend_receive_contract_test.cpp Covers backend adapter contracts.
tests/​fm_filter_controls_test.cpp Observes typed squelch requests.
tests/​phone_cw_applet_apf_test.cpp Verifies typed APF intent.
tests/​receive_intent_routing_test.cpp Covers routing, lifecycle, and reentrancy.
tests/​reroute_dead_controls_test.cpp Updates dead-control routing coverage.
tests/​rtl_backend_test.cpp Tests RTL controls and stranded-worker behavior.
tests/​rx_applet_squelch_reconciliation_test.cpp Updates squelch reconciliation assertions.
tests/​slice_apf_agc_off_seam_test.cpp Verifies APF through the typed seam.
tests/​slice_model_letter_test.cpp Updates Kiwi suppression expectations.
tests/​slice_model_squelch_memory_test.cpp Updates typed squelch-memory coverage.
tests/​split_audio_profile_test.cpp Covers intent-origin filtering.
tools/​check_command_plane.py Lowers the migrated command-plane baseline.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/rtl_backend_test.cpp
Comment thread tests/rtl_backend_test.cpp
rtl_backend_test uses QEventLoop::AllEvents and QEvent::DeferredDelete but
relied on QCoreApplication pulling both in transitively; AGENTS.md requires
direct includes because Qt's transitive set varies across supported versions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved at 701a6327c, built and tested on Linux (full tree, no target filter).

Fixed in this pass

  • 012ca37a1: refused compatibility-origin writes no longer raise the operator's "unsupported control" notice (RadioModel.cpp:9892).
    • Before: the Kiwi suppression mute that HL2, ANAN and RTL refuse showed the notice with no control touched, and used up its once-per-session slot. Found by the automated review pass.
    • Proof: receive_intent_routing_test "a refused compatibility-origin mute is logged…" fails with the guard removed and passes with it.
  • 012ca37a1: SliceSquelchRequest::valid() replaces four copied range checks (Flex, HL2, Icom, RTL). backend_receive_contract_test pins the range and the Flex refusal. Off-thread setSquelch now logs through refuseOffThread.
  • 701a6327c: direct <QEvent>/<QEventLoop> includes in rtl_backend_test (Copilot thread).

Verified on head, earlier fixes

  • HL2 squelch/APF adapters (my blocker), fixed by @rfoust in 5748e69b, with an applied-WDSP check at backend_receive_contract_test.cpp:656-670.
  • Nits 3–6: comments, enum-sized epoch array, helper rename, ANF comment.

Decided by the maintainer

  • Operator-moved controls the backend cannot apply refuse aloud.
  • Demo AF/squelch/NB level and Icom mute/pan have no local effect, so the notice is accurate.

Refuted

  • The RTL wireWorker() hunks are not new #6096 scope. They re-land merged #6107 (c7513e125), which #5924's squash a325fb99 dropped from main. The PR body now says so.
  • The bot's setLocked "redundant guard" nit: the second check runs after emit lockedChanged, which handlers can re-enter, so it is the notify-then-dispatch guard.

Evidence

  • 19 focused tests pass: receive contract/routing, split-audio, squelch memory/scale, GUI squelch/FM/APF/lifetime, Kiwi/slice-letter, ANAN NB, antenna alias, dead-controls, RTL backend/model acceptance, Flex mode intent, Icom family, HL2 dBref.
  • Static gates, main vs head:
    • Engine boundary: 102 findings, identical file by file.
    • Touchpoint manifest: up to date, unchanged.
    • Capability records: frozen at 70.
    • Command plane: 400 → 369.
  • Bridge on DEMO-0001 (offscreen, isolated settings, TX disabled): ANF accepted. AF gain and NB level log refused … (operator).
  • CI: 5/5 green on this head. The run was created at 2026-10-04T08:23Z, after main's tip a325fb99 (06:03Z).

Not verified: live Flex/HL2/Icom/ANAN hardware; RTL with a dongle; the Kiwi path on HL2 (no Kiwi server).

@ten9876
ten9876 merged commit f4e463c into aethersdr:main Oct 4, 2026
5 checks passed
@ten9876

ten9876 commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

@rfoust, thanks. This landed as squash f4e463c2. Jeremy ruled on the open classification question: operator-moved controls the backend can't apply keep refusing aloud through the existing notice. On Demo (AF/squelch/NB level) and on Icom (mute/pan) these controls have no local effect, so the notice is accurate.

I added two small commits on your branch before landing it:

  • 012ca37a1: refusals of compatibility-origin requests (the Kiwi suppression mute, the NRS profile restore) are now logged without the operator notice. Before this, choosing a Kiwi antenna on HL2/ANAN/RTL showed the notice and used up its once-per-session slot. The commit also adds SliceSquelchRequest::valid() and logs off-thread squelch writes through refuseOffThread.
  • 701a6327c: direct Qt includes in rtl_backend_test.

On Copilot's split request: the RTL wireWorker() hunks re-land #6107, which #5924's squash had dropped from main, so I refuted it in the thread. I also updated the body: the classification decision is recorded, and it notes the #6107 restoration. The evidence is in my approving review.

— Jeremy (KK7GWY)

Drafted with Claude Code at my direction.

ten9876 added a commit that referenced this pull request Oct 4, 2026
…lay, Qt 6.12 (#6159)

## Summary

Six-file release prep for **v26.10.1**, metadata and docs only. No
source, test or CI changes.

Cutoff: `origin/main` at `18c5245e5` (#6155, merged 2026-10-04).
Anything that merges after it is folded in before the tag.

**95 merged changes** (95 PRs, no direct commits) from **12 human
contributors**, plus AetherClaude (2) and Dependabot (0). There are no
first-time contributors this cycle. That was checked for each login by
looking for a merged PR before v26.9.5's tag, not from
`author_association`.

## Changelog

Built from `v26.9.5..18c5245` mapped through the commit-to-PR API
(`repos/…/commits/<sha>/pulls`), not from commit subjects. All 95 PRs
are cited in the section. The notes lead with three things:
- the sweep that makes every control on a radio without a Flex command
plane either work or refuse with a reason (#6044, #6049, #6015, #6111);
- the CTR2 relay (#6090, #6114, #6155) and authenticated 4O3A
connections with a Peripherals page (#6008, #6027);
- the Hermes-Lite 2's squelch, APF, AGC-off, RIT/XIT and FFT AVG.

The structural half covers Qt 6.12 for every binary and source build
(#5842, #6055) and the slice receive controls moving behind the seam
(#5892, #5907, #5919; raw Flex commands above the seam 413 → 369,
confirmed with `check_command_plane.py` at the cutoff). Upgrade-visible
changes are called out with **On upgrading** inline: the Ulanzi default
and removed MOX/TUNE pills (#5964), auto RF gain arming and law (#5976,
#6062), DIGU/DIGL opening with AGC off (#6119), HL2 squelch calibration
and the Auto SQL fallback (#6127), and the macOS 14.4 floor (#5842).

The previous tag v26.9.5 is an ancestor of `main`, so nothing is
excluded from the range.

## Docs refresh, not just version strings

- **README:** Highlights "Broad hardware control" gains the CTR2 relay.
Supported Hardware says direct TGXL/PGXL/Antenna Genius connections
authenticate. The Other radio families paragraphs gain the HL2's
squelch, APF, RIT/XIT, FFT averaging and CL1 reference, the ANAN-G2's
receiver audio and radio speaker, and RTL-SDR's confirmed receive state,
capture browsing and zoom FFT. In the Roadmap section, the aetherd line
gains the receive controls and a CTR2 relay line is added. No maturity
label changed. The six backends and seven NR engines were re-checked
against `src/core/backends/` and `AetherRxProfiles.cpp`.
- **ROADMAP, In flight:**
  - aetherd: the receive controls behind the seam.
- Icom: TUNE power live, no false drop notices, the one-panadapter
correction. The stale "VOX and CW break-in" leaves Remaining:
`IcomCivBackend` has implemented both since #4799/#5113.
  - ANAN-G2: receiver audio and radio speaker.
  - RTL-SDR: M1, with Remaining restated from #5924's body.
- Hermes-Lite 2: the v26.10.1 control parity, and the CW latency cost
updated to 4096 taps.
  - A new **CTR2 controller relay** entry.
  - Nothing left In flight as finished.
- **ROADMAP, Open RFCs:** diffed against `label:rfc`. #5893 leaves
(closed). #6106 and #6079 join under Radios, protocol and devices.
- **ROADMAP, Recently shipped:** 8 new `(v26.10.1)` entries. The list is
trimmed to the v26.10.x cycle at the month rollover, and its intro names
v26.10.x. Everything trimmed is still in CHANGELOG.

## Judgment calls flagged for the maintainer

- **Recently shipped trimmed to v26.10.x at the rollover:** maintainer's
call. This makes the validator's "only the four current-version lines
lose `26.9.5`" check FAIL. The lines it lists are exactly the trimmed
`(v26.9.5)` entry markers, and there is no historical mention elsewhere.
That is the one FAIL below. One side effect: the trim removes ROADMAP's
last mentions of #5630 and #5684, approved RFCs that shipped in v26.9.4.
Both are now closed as completed, and both remain in CHANGELOG.
- **A CTR2 relay In-flight entry:** maintainer's call. Wi-Fi works
today. USB's host side shipped and has carried the radio's status stream
to the firmware author's development firmware; it waits on a CTR2 USB
firmware release.
- **Intro wording with zero Dependabot updates:** "AetherClaude and no
dependency updates within that total" keeps the house shape and passes
the count check. The Contributors block has no Dependabot sentence.
- **#6125 is filed under the seam section.** It fixes a WAN/SmartLink
receive regression introduced by #5907 inside this range, so released
builds never had the regression. The section states what ships: receive
intents gate on `RadioModel::isConnected()`.
- Ready for review, not draft: maintainer's call.

## Scope

| file | change |
|---|---|
| `CMakeLists.txt` | `project(AetherSDR VERSION 26.10.1)` |
| `README.md` | Current version line, plus the prose refresh above |
| `AGENTS.md` | Current version line |
| `CHANGELOG.md` | new `[v26.10.1] — 2026-10-04` section; `[Unreleased]`
stays and stays empty; history below byte-identical |
| `packaging/linux/io.github.aethersdr.aethersdr.metainfo.xml` |
`<release version="26.10.1" date="2026-10-04"/>` first in `<releases>` |
| `ROADMAP.md` | cycle heading → post-v26.10.1, plus the prose refresh
above |

## Validation

`check_release_prep.py --mapping mapping.json`: 29 checks, **1 FAIL**
(explained above), 0 WARN.

- All six version spots say 26.10.1. The CHANGELOG heading date equals
the metainfo date, 2026-10-04.
- `appstreamcli validate` passed. The `<release>` list is
date-descending with unique versions (22 entries).
- `[Unreleased]` stays empty, `[v26.10.1]` sits directly under it, and
the CHANGELOG diff is exactly the inserted section, with history
byte-identical to `origin/main`.
- All 95 PRs in the range are cited. The only other numbers cited are
#3849 (RFC), #5262 (tracking issue), #5468 (RFC), #6091 (RFC, approved
and closed) and #6142 (open follow-up issue for #6119).
- The intro (95/12), the Contributors counts (13 entries) and
"first-time: none" are re-derived from the mapping, and they match.
- ROADMAP `(vX)` markers lead with v26.10.1. Every local link in README
and ROADMAP resolves. No removed heading is linked elsewhere. Code
fences balance, with no duplicated table headers or doubled `---`.
- **FAIL:** "only the four current-version lines lose `26.9.5`". The
lines are the trimmed Recently-shipped markers; see the judgment calls
above.
- `git diff --check` is clean. The diff touches no `src/`, `tests/`,
`third_party/` or `.github/workflows/` path.
- Configure: `cmake -S . -B <scratch>` reports
`CMAKE_PROJECT_VERSION=26.10.1`. No build or test was run, because this
is metadata only; CI builds it.
- `main` CI at the cutoff: `CI` passed on `18c5245e5`.

After merge: tag the resulting **main** commit, not this branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
on8st added a commit to on8st/AetherSDR that referenced this pull request Oct 4, 2026
Main gained the RTL M1 work (aethersdr#5924), receive-control routing (aethersdr#5919),
the int16 cast guards (aethersdr#6089), BUILD-OPTIONS.md (aethersdr#6098) and the
v26.10.1 release prep (aethersdr#6159). Seven files this branch touches merged
without conflict.

WdspChannel::Config now has a defaulted operator== (aethersdr#5924); this
branch's txAmCarrierLevel and txFmDeviationHz take part in it.
None of main's changes sits under AETHER_HL2_TX_TXA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 5, 2026
…lane, incl. two WDSP races. Principle VIII. (#6173)

## Summary

Refs #6154, #6156. This doesn't close them: the sanitizer-slowdown
timeouts are still open (see below).

The weekly Sanitizers lane recently started building the full test tree
again (#6136, #6152). Its first complete run on `main`,
[37208806037](https://github.com/aethersdr/AetherSDR/actions/runs/37208806037),
failed 6 tests under ASan + UBSan and 11 under TSan. This PR fixes every
one of those failures that was an actual sanitizer report. **In the
branch run
[37230031178](https://github.com/aethersdr/AetherSDR/actions/runs/37230031178),
neither lane reports a single sanitizer error.** What still fails there
is timeouts, plus one plain assertion in `rtl_capture_worker_test`
(below).

Two of the eight commits change shipped DSP behaviour. They are vendored
WDSP patches, documented in `third_party/wdsp/AETHERSDR-PATCHES.md`:

- **Patch 16, transmit path.** `SetTXABandpassFreqs()` was the only one
of four `TXASetupBPFilters()` callers that didn't take `csDSP`.
`TXASetupBPFilters()` clears `bp1`/`bp2 run` first and re-enables them
only after the FIR redesign. So a TX filter change while transmitting
with COMP (and CESSB) on could run blocks with the post-compressor
bandpass bypassed, which is out-of-band splatter. Fix: take the lock, as
`SetTXAMode()` does. It adds no new lock order, and costs one FIR
redesign stall per TX filter change, which upstream already accepts for
a mode change.
- **Patch 17, panadapter analyzer.**
  1. `stop` and `end_dispatcher` are now atomic.
2. `input_busy` is released *after* `stitch()`, so the next frame's FFT
workers can no longer overwrite results while the stitch is still
copying them (a torn spectrum frame).
3. `sendbuf()` now hands off `IQout_index` inside
`BufferControlSection`, closing a lost update.

Only part 2 changes timing: the next frame's FFTs start one stitch
later, tens of microseconds.

| Commit | Lane | Finding | Fix |
|---|---|---|---|
| `1f9c4ed66` | ASan | `offline_health_registry_test`:
stack-use-after-scope. Section 6 reuses a factory that captured section
5's block-scoped `RecorderState` by reference | `rec` moves to `main()`
scope (test only) |
| `cacf9fe30` + `49fa9a75a` | UBSan |
`rx_applet_squelch_reconciliation`, `vfo_dsp_launcher_width`,
`fm_filter_controls`: invalid `QPointer<QLabel>` downcast in
`VfoWidget::eventFilter` during the collapsed label's teardown | Remove
the filter before `~VfoWidget` deletes the label, **and** test the event
type before the guarded-pointer comparison. The second covers the label
dying *first*, after `beginDirectEntry()` `raise()`s the VFO (found in
review). Regression: `gui_nested_lifetime_test`
`vfoCollapsedLabelTeardownInEitherOrder`, covering both child orders |
| `01d06c6d1` | TSan | `receive_intent_routing`, `tci_automation`,
`tci_server_review`: lock-order inversion between two of Qt's own
TLS-backend mutexes, both taken on the main thread | Anchored
`deadlock:QSslSocketPrivate::tlsBackendInUse` suppression |
| `f04a9ffb2` | TSan | `tci_server_review`: unlocked global
`QStringList` log sink written from worker threads | One mutex for the
sink and its lists (test only) |
| `6f6e4a5fd` | TSan | `tx_operation_integration`: the command sink
appends on the `RadioConnection` thread to a list destroyed before that
thread is joined | Declare the list before `radio` (test only) |
| `fbbd24087` | TSan | `wdsp_channel`: WDSP patch 16 (above) | Lock in
`SetTXABandpassFreqs` |
| `af9c15224` | TSan | `anan_rxdsp_handedness`, `spectrum_sequence_gap`,
`wdsp_process_tally`: WDSP patch 17 (above) | Three analyzer hand-offs
synchronised |
| `31e249c3e` | TSan | `weather_radar_loading`: Qt's
`QHttpThreadDelegate` teardown vs a main-thread reply delete. Three map
tests were fetching live OSM tiles | Stub network in those three tests,
plus an anchored `race:QHttpThreadDelegate::~QHttpThreadDelegate`
suppression |

Each suppression in `tests/sanitizers/tsan.supp` carries its reasoning
and a caveat. Neither is anchored on a frame AetherSDR code runs inside.
The file's DO-NOT-ADD list is untouched. Note that a suppression hides
**every** stack it matches, not only the reported cycle or teardown. So
zero TSan reports with these entries means no *unsuppressed* race was
seen. It is not proof that every Qt TLS or HTTP-teardown interaction is
race-free.

**Still failing after this PR, tracked in #6154 / #6156:**
- **Timeouts under the sanitizers' slowdown, with no sanitizer report.**
ASan: `rtl_spectrum_averaging`, `weather_radar_loading`. TSan: the same
two plus `audio_engine_rates`, `nr_stereo_independence`,
`peripheral_auth_dialog`.
- **`rtl_capture_worker_test`, TSan lane only:** fails the assertion "DC
mode change discards old estimates and shows the actual first corrected
window", with no race reported. This branch touches no RTL code, and the
test passed in the same run's ASan lane. It passed in both lanes this
morning, before `main` gained #5919's RTL changes.

**Leak fixes (added after the first push, found by a full local
LeakSanitizer sweep):**

| Commit | Leak | Fix |
|---|---|---|
| `6121f6ad6` | `RadioModel::disconnectClientHandlesThen`: the `step`
lambda captured its own `shared_ptr`, a cycle that leaked on every
client-disconnect chain | Capture a `weak_ptr`; the in-flight `sendCmd`
callback holds the strong reference |
| `7577bab41` + `7f37cf045` | WDSP **patch 18**: `destroy_calcc()` never
closed the five PureSignal `SemsPSCorr` semaphores. 86 leak blocks
across 17 tests (every TX channel destroy) | Close them, and the exit
event, only once the correction thread has signalled exit; on the 500 ms
timeout they are not freed under a live thread. **This does not make the
timeout path safe:** `destroy_calcc()` still frees `a`, which a late
thread would use. That hazard predates this PR and is now #6179 |
| `192e8f09a` | `phone_tx_filter_numeric_entry_test`: `sendEvent(…, new
QFocusEvent)` leaked, since `sendEvent` never takes ownership | Stack
event (test only) |

**Why CI never reported these leaks:** the ASan lane's `ASAN_OPTIONS`
include `halt_on_error=0`, and with that, a leak doesn't change a test's
exit code. The test "passes" and `ctest --output-on-failure` throws its
output away. Verified with `phone_tx_filter_numeric_entry_test`: it
exits 0 with CI's options and 1 with plain `detect_leaks=1`. Fixing that
is a `sanitizers.yml` change and is **not** in this PR.

**Full local sweep (all 653 tests, leaks counted as failures):** 26
failed before these three commits. After them, the only leak left
anywhere is whisper.cpp's ggml Vulkan backend registry, a
process-lifetime static that only appears on a machine with a Vulkan
GPU. Not ours, and CI has no GPU. The other non-leak failures are: the
two known timeouts; `connection_panel_size` and `local_control_server`,
which fail only on this machine with no sanitizer report and pass in CI;
and `rtl_receive_pipeline` and `cw_pcm_consumer`, which fail
intermittently (1 run in 3 serially, and under parallel load,
respectively) with no sanitizer report. Those are flakes already on
`main`, out of scope here.

## Constitution principle honored

Principle VIII: every finding was reproduced before it was fixed. Each
fix that could be checked locally was shown to fail without it and pass
with it; the TSan-only fixes are proven by the branch dispatch. The two
WDSP changes follow the vendoring rules: minimal, commented in place,
and documented in `AETHERSDR-PATCHES.md` with what to keep on the next
WDSP update.

## Test plan

- [x] Local build passes: full tree, Debug, with CI's ASan + UBSan flags
and `AETHER_SHARED_CORE=ON`
- [x] **Red/green for both ASan fixes.** Reverted, all 4 tests reproduce
CI's exact reports (stack-use-after-scope at `:95`, downcast at
`qpointer.h:76`). With the fixes they pass 3/3.
- [x] 71 WDSP, ANAN, TX, HL2 DSP, map and edited tests under ASan +
UBSan. All pass apart from the 2 known 30 s timeouts.
`weather_radar_loading_test` run directly: 44 passed, 0 failed, 26 s.
- [x] **Red/green for the leak fixes.** In a full-suite LSan sweep on
the same tree, before vs after the three leak commits: 86 `create_calcc`
blocks, 5 `disconnectClientHandlesThen` blocks and the `QFocusEvent`
leak, down to none. All the affected tests pass with leak detection on,
`tx_operation_integration` with 421/421 assertions. Leaks from this
machine only (fontconfig/pango/GTK, NVIDIA `cuInit`, ggml Vulkan) were
suppressed locally, not in the repo.
- [x] Sanitizers `workflow_dispatch`
[37230031178](https://github.com/aethersdr/AetherSDR/actions/runs/37230031178)
tested **`31e249c3e`**, the head before the leak fixes and the review
fixes, **not** the final head. Zero sanitizer `SUMMARY:` reports in
either lane, so every originally targeted test is green. The run still
went red on the timeouts and on `rtl_capture_worker_test`. Its duplicate
stickies (#6165, #6170) are closed into #6154 / #6156.
- [x] Sanitizers `workflow_dispatch` on the **final head `7f37cf045`**,
[37243767521](https://github.com/aethersdr/AetherSDR/actions/runs/37243767521):
**zero** sanitizer `SUMMARY:` reports in either lane. ASan failed 2/656
and TSan 5/656, every one a known sanitizer-slowdown timeout.
`gui_nested_lifetime_test` (the new VFO regression),
`phone_tx_filter_numeric_entry_test`, `wdsp_channel_test` and
`rtl_capture_worker_test` passed in both lanes.
- [x] The same head **plus #6176**, so a leak fails its test,
[37243769261](https://github.com/aethersdr/AetherSDR/actions/runs/37243769261),
on the temporary branch `ci/integration-6173-6176` (since deleted): **no
ASan, UBSan or LeakSanitizer report**, and the same timeouts only. Its
stickies #6189 and #6191 are closed into #6154 / #6156.
- [x] **VFO teardown red/green.** The new
`vfoCollapsedLabelTeardownInEitherOrder` aborts on the `qpointer.h:76`
downcast in its "label-dies-first" row with only the destructor fix.
With the event-type-first check, all 10 cases pass, 3 of 3 runs. The 26
vfo/rx_applet/fm_filter/slice/lifetime tests pass.
- [ ] Behaviour on a real radio: patch 16 is a TX-path change. A COMP-on
filter-width change while keyed should be checked on hardware and a
dummy load before it ships.
- [ ] Existing tests pass (CI)

## Checklist

- [x] Commits are signed (`docs/COMMIT-SIGNING.md`)
- [x] No new flat-key `AppSettings` calls
- [x] Code is clean-room
- [x] No meter UI touched
- [x] Documentation updated: `third_party/wdsp/AETHERSDR-PATCHES.md`
(patches 16, 17) and the in-file rationale in
`tests/sanitizers/tsan.supp`. No `CHANGELOG.md`.
- [x] Not security-sensitive

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 pushed a commit that referenced this pull request Oct 6, 2026
…ite 2. Principle III. (#6140)

**Update, `d60f4660` (2026-10-05):** `main` `12df6943` merged in; one
conflict, in `docs/architecture/radio-capabilities-map.md`, both sides
kept. No code of this PR changed.

**Update, `dabf83e3` (2026-10-05):** one fix on top of the reviewed
head: the 3D floor resync after a zoom is no longer adopted or stored as
the operator's range. Maintainer ruling of 2026-10-05: this PR owns
`dbmRanges[pan]`, its helpers and its accepted range; #6129 rebases onto
it. Line references below are at `7a5ca748`; `MainWindow_Wiring.cpp`
gains four lines after `:4269` since.

**Update after review, `7a5ca748` (2026-10-05).** The change request is
applied, and it narrows this PR:

- **Only the Hermes-Lite 2 is covered now.** Storing and restoring FFT
FPS and the dBm range is gated on an owner the backend declares, not on
backend identity. The HL2 declares both. Icom, ANAN, RTL-SDR and the
demo backend declare none and behave as on `main`. A demo run that
showed FFT FPS restored at `295bff6e` will, by the code, not reproduce
at this head; that was not run.
- **#6129 (open) adds the same `dbmRanges[pan] = {min, max}` table**
with a different accept-set (`min < max`, -250..100), a different gate
(`!radioOwnsDbmScale`: ANAN, Icom) and a restore into the widget instead
of the pan model. With the HL2-only opt-in no radio is claimed by both
PRs any more. The two accept-sets still differ and this PR adopts
whichever the maintainer rules. A trial merge of `7a5ca748` with #6129
at `04762258` conflicts in `MainWindow_Wiring.cpp` and the two
touchpoint files; `ClientDisplaySettings.h` merges as text and then
holds two definitions of `DbmRange`, `dbmRange()` and `saveDbmRange()`.
The comparison is in the review reply.
- **A stored dBm value is validated before it is narrowed to a float**
(NaN, the infinities and ±1e308 are refused through the real store and
reader).
- The source-text pins match whole call statements.

Refs #5678 (HL2 parity; rows P16, the dBm range, and the FPS half of P7
in our status table). No issue exists for this. #5111 (the Flex side) is
not touched.

## Limits, first

- **Where it is stored, since you ruled on that for #6119:** in the
radio's settings document, not a flat key. Two optional tables,
`fftFps[pan]` and `dbmRanges[pan] = {min, max}`, in the existing
per-radio `ClientDisplay` feature document that already holds
`waterfallRates`. No schema bump.
- **Who may store and restore is declared per feature, by the backend.**
FFT FPS: a new optional record, `PanFrameRateShaping`, field
`clientPersistsFrameRate`, read through
`RadioCapabilities::clientPersistsPanFrameRate()`. The dBm range: a new
field in the existing `PanAmplitudeModel`, `clientPersistsDbmRange`,
read through `RadioCapabilities::clientPersistsDbmRange()`. Same shape
as `BackendPanAveraging::clientPersistsAveraging` (#5924). No new
top-level capability bool: the count stays at the frozen 70.
- **Two second conditions remain, and you may want them changed.** FFT
FPS also needs `RadioModel::shapesDisplayRatesLocally()`, as the
averaging path does: the restore sits where the local shaper is seeded.
The range also needs absolute bins, inside its accessor: the client
writes the range into the pan model, which is wrong on a radio whose
bins are scaled by it or whose backend publishes its own range. Neither
condition grants anything without the declaration.
- **The waterfall-rate gate on `main` is not touched.** It still reads
`shapesDisplayRatesLocally()` alone.
- **Half of what was observed.** FFT AVG and Wt Avg are not fixed on the
HL2. #5924 gave them a table in this document (`fftAverages`), kept only
for a backend that declares `clientPersistsAveraging`; the HL2 declares
it false.
- **Not seen in a running app at this head, on any radio.** The defect
was (HL2, 2026-10-01, 2 of 2 restarts: FPS 12 came back 25; a 120 dB
scale came back -40 / 90 dB).
- **It changes what the pan model holds, on an HL2 only:** an operator's
range is now written into `PanadapterModel`. That range is absolute dB
(dBFS under a dBm label).
- **Families:** the Hermes-Lite 2 only, for both settings. It is the
radio the defect was observed on, and it stores nothing itself. Flex,
Icom, ANAN, RTL-SDR and the demo backend declare no owner: nothing is
stored or restored for them, as on `main`. Widening to another family is
one declaration in that family's backend, for its owner or the
maintainer to make; #6129 claims the ANAN side of the range.
- **Yours to move:** the FPS bounds 5..60 (the slider's own) and the
plausibility bounds, moved unchanged from `MainWindow_Wiring.cpp:168` on
`main` to `core/DbmRangePlausibility.h`.

## What was wrong on `main`

A Flex stores both and reports them on connect. A radio with no display
engine reports nothing (observed on the HL2, and fixed here for the HL2
only; line references are at `7a5ca748`):

- **FFT FPS** stays at the widget default (`SpectrumWidget.h:1666`, 25)
and pan wiring seeds the shaper from the widget
(`MainWindow_Wiring.cpp:3320`).
- **The dBm range:** pan wiring primes the widget from the model
(`MainWindow_Wiring.cpp:4074`, `MainWindow_Session.cpp:1855`), the model
starts at -130 / -40 (`PanadapterModel.h:279`), and on a radio that does
not own the scale nothing ever moves it: on an HL2 the range command is
attempted and dropped at the command plane (the HL2 leaves
`radioOwnsDbmScale` at its default `true`, `RadioCapabilities.h:439`, so
the gate at `MainWindow_Wiring.cpp:3779` does not stop it), and nothing
echoes.
- Of a pan's display settings only the waterfall rate
(`core/ClientDisplaySettings.h:90-132`) and, since #5924, FFT averaging
for a backend that declares it (`:48-88`) have a client store.

## The change

- **FFT FPS**, where the backend declares the client its owner, is saved
by the Display slider (before the adaptive-throttle return), Clone to
all Pans and Reset to Defaults, and restored into the widget before the
shaper is seeded from it.
- **The dBm range**, where the backend declares the client its owner, is
saved on a scale request or a finished drag, and restored into the pan
model before the widget is primed from it, at both prime sites. FFT
Floor Auto's own moves are not stored.
- The pending-write key gains the field. It was (radio, pan), so two
fields of one pan edited inside the 250 ms window replaced each other.
- `dbm_range_plausibility_test` now includes the real predicate instead
of a hand copy. One row added to the touchpoint manifest (240 with
`main` at `12df6943` merged in).
- A Flex is unchanged, and so are Icom, ANAN, RTL-SDR and the demo
backend: none declares an owner, so every new reader returns nothing and
every new writer returns before the store. Save and restore of each
setting ask one predicate (`MainWindow::clientPersistsFftFps()`,
`MainWindow::clientOwnsPanDbmRange()`), each reading one
`RadioCapabilities` accessor.
- `docs/architecture/radio-capabilities-map.md` has the new record and
field with each family's value.

## Tests

`client_display_pan_settings_test` (new, socket-free, real store): round
trip per radio and pan; the Flex-shaped scope; ownership of both
settings read through the two accessors off real HL2, Icom, ANAN and
Flex backends (the HL2 stores and restores; the others and an undeclared
backend write nothing, read nothing and leave a held document byte for
byte); refusals on write and read; a newer schema; coexistence with an
`fftAverages` table in #5924's shape; two fields in one deferral window.

`spectrum_overlay_fft_fps_bounds_test` (new, real widget, in a
`TestSettingsProfile`): the slider runs the stored bounds.

**One source-text pin block, and its claim:** the order and presence of
the save and restore calls inside `MainWindow` methods, which link into
no test, and that the save and the restore of each setting ask the one
predicate.

Fail-before, one build and one run per row. The two ownership rows are
at `7a5ca748`; the other rows were measured at `295bff6e` and not run
again:

| mutation | failing checks |
|---|---|
| this branch | 0 |
| `main`'s wiring, and a store that neither reads nor writes the new
tables | 17 |
| pan wiring does not restore FFT FPS | 1 (source text) |
| the store ignores who owns the value on read | 1 |
| the pending-write key has no field | 2 |
| both ownership accessors return true for every backend | 33 (8 each
for Icom, ANAN, Flex and an undeclared backend, and the relative-bins
check) |
| both ownership accessors return false for every backend | 8 (7 HL2,
and the absolute-bins control) |
| a stored range is not validated on read | 1 |
| a scale request does not adopt the range | 1 (source text) |
| the writer replaces the whole document | 6, `fftAverages` and the
waterfall rate among them |
| the stored FPS maximum is 61 | 1 (the slider bounds test) |

Full ctest, macOS, Qt 6.12.0, at `d60f4660` (`main` `12df6943` merged
in, no code of this PR changed): 645 of 646 pass at `-j8`; the one
failure is `peripheral_auth_dialog_test` (#6008), which also fails
alone. The run before it, at `7a5ca748` (`main` `85f2a124` merged in):
644 of 646 pass at `-j8`. Two failures, neither from this PR:
`peripheral_auth_dialog_test` (#6008; diagnostic on #6134), which also
fails alone, and `vkamp_connection_test` (#5254), which passed when
rerun alone. `hl2_receiver_count_restart_paced_test` and
`libre_radar_test` passed at `-j8` in this run.

Static checks (`static-checks.yml`, stdin from `/dev/null`): the script
steps of the `Static checks` job pass, including the touchpoint manifest
(240, up to date) and `check_capability_records.py --strict` (70
booleans, at the frozen 70); colour ratchet +0. `check_a11y` on the
three changed GUI files: no findings. The `Sanitizer option configures`
job was not run locally.

## With the open PRs

- **#5924** (merged, and merged in here at `295bff6e`): it conflicted in
`ClientDisplaySettings.h`, `MainWindow.h` and `MainWindow_Wiring.cpp`,
all resolved by keeping both sides. The tables do not overlap and its
pending-write key has its own prefix.
- **#5919** (merged) is in this branch through `main`.
- #6117 and #6115 (both merged) are in this branch.
- **#6129** (open, head `04762258`): see the update block at the top.
- **#6139** (open, ours): both PRs add to `RadioCapabilities.h` and to
each backend's `capabilities()`, in different places. A trial merge of
`7a5ca748` with its head `98be49a1` is clean; the merged tree was not
compiled.

## Not verified

- The restore in a running app, on any radio, at this head.
- The older rows of the fail-before table at this head (see Tests).
- Linux and Windows: built and tested on macOS only.
- The 3D view in a running app. Its floor resync after a zoom is
excluded from the adopt and the store by a guard (`dabf83e3`) that is
pinned as source text only; the path was not driven with frames. Reset
to Defaults does not reset the dBm range; it did not before.

**Known red on `main`:** `peripheral_auth_dialog_test` (#6008) fails on
this Mac at this head, as it did on clean `main` at `22507580` ("Clear
code or Status is clipped at the default dialog size": a serial-port
name on this host widens the page; diagnostic on #6134). Clean `main` at
`85f2a124` was not built here. `hl2_pan_create_async_test` passed;
`vkamp_connection_test` (#5254) failed at `-j8` and passed alone.

73, ON8ST

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jensenpat pushed a commit that referenced this pull request Oct 7, 2026
…ciple IV.

## Summary

WDSP ships two impulse blankers. #5824 landed the first (ANB, `nob.c`); this
adds the second (NOB, `nobII.c`), which deskHPSDR, pihpsdr and Thetis all call
NB2. Instead of gating the blanked window to zero it reconstructs it from the
samples either side: better when the impulse lands on a wanted signal, worse
when the noise is dense enough that those samples are not clean. Every
reference client offers both and replaces neither, and so does this.

Host-side DSP on our own IQ throughout. No wire protocol, no radio firmware,
and no behaviour change for a Flex or an Icom, whose blankers are the radio's.

- **`WdspChannel` opens both stages** for the life of a receive channel and
  runs at most one: `processIq` feeds exactly one stage per block, chosen by
  the kind. Both are created even when no blanker is on, because
  `create_nobEXT` allocates and a lazily built stage would allocate on the
  real-time path the first time someone clicked NB2.
- **The transmit hold covers both**, for the reason it covered one: the
  trigger is relative to a running average, so a transmit period of zeros
  would decay that average and gate the first real receive samples.
- **Kind and fill ride #5919's typed route** (ruling 1). `SliceDspRequest`
  gains `kind` and `fill` members, carried on every `Feature::Nb` request,
  and one new field, `Field::Fill`. A change of blanker is an ordinary
  `Field::Enabled` request: `enabled` says whether one runs, `kind` says
  which, so a radio with one blanker (Flex, Sim) reads `enabled` exactly as
  before. `SliceDspRequest::requestedBlanker()` derives the three-state answer
  in one place, and a request built without a kind still means the first
  blanker. There is deliberately no `Field::Kind`: the NB button calls
  `setNbKind()` on every radio, so a new field would have reached Flex and
  Sim, which do not handle it.
- **Host backends apply it through one verb**,
  `IRadioBackend::setSliceNoiseBlanker(id, kind, level, fill)`, which the
  HL2, ANAN and Icom `requestSliceDsp` call. Kind, level and fill arrive
  together, so no ordering between them exists. Icom treats anything but
  `Off` as on. Flex refuses `Field::Fill` as `Unsupported` rather than
  encoding it; the fill is only reachable where `hasHostNoiseBlanker` is set
  (HL2, ANAN), so no operator sees that refusal.
- **Two enums, one conversion.** `core/backends/NoiseBlankerKind.h` is the
  seam's vocabulary; `WdspChannel` keeps its own, because `core/dsp` is a
  leaf. `core/backends/WdspNoiseBlanker.h` holds the conversion, with
  `static_assert`s that fail the build if an existing value is renumbered.
- **`SliceModel` gains `nbKind()`/`nbFill()`, and `nbOn()` is now derived from
  the kind**, so there is one source of truth. `setNb(bool)` keeps its
  meaning: `true` selects the first blanker (ruling 2). rigctl's `NB`,
  SmartCat's `NB`, TCI's `rx_nb_enable`, `get slice`, the MIDI mapping, the
  keyboard shortcut and the band-stack entry are untouched. A radio's bool
  echo cannot downgrade a host NB2 to NB.
- **UI: the NB button cycles Off → NB → NB2 → Off** where this host runs the
  blanker, and stays a plain toggle elsewhere (ruling 2). The label follows
  the running kind; the `objectName` does not, since the bridge addresses the
  button by it. An **NB2 fill combo** appears under the DSP grid while NB2
  runs, and leaves with the NB button in FM (ruling 4).
- **Bridge** (ruling 3): `slice dsp nb2 on <level> <fill>`, refused on a radio
  without `hasHostNoiseBlanker`, and `get slice` / `get hostnb` report the
  kind and fill beside the existing `on`.
- **`aether_wdsp.h` gained the NOB declarations.** `nobII.c` was already
  compiled; only the curated header was missing it.

## Review and rulings

| Item | Where |
|---|---|
| Blocker 1: #5919 removed the routing this was built on | Rebased onto `85f2a124` and moved onto `SliceDspRequest` in `340d00b8` |
| Ruling 1: widen `SliceDspRequest` with kind and fill | `340d00b8` |
| Ruling 2: the cycling button, and `setNb(true)` dropping NB2 to NB | Unchanged, as accepted |
| Ruling 3: the bridge surface | Unchanged, as accepted |
| Ruling 4: the fill row showing in FM | `3ef4cd36` |
| Blocker 2: the run-flag ratchet | Removed in `3ef4cd36` |
| Nits: stale line cites, the retired-test reference, the `static_assert` claim, comment length | `3ef4cd36` |

**Blocker 2.** The review's mutations showed the ratchet guarded an inert
property. A run flag left on the unchosen stage blanks nothing, because
`processIq` never feeds that stage. The one mutation that does double-blank
(both stages fed, the ANB run flag left on under NB2) passed the ratchet and
failed DISCRIMINATION 2 in `hl2_noise_blanker_test`.
`tools/check_nb_exclusivity.py`, its `Static checks` step and its
`docs/agents/backends.md` entry are gone. The test's closing comment now names
the discrimination checks as the exclusivity pin, and the run-flag comment in
`WdspChannel::setNoiseBlanker` names the dispatch as the guarantee.

**Ruling 4.** The fill row follows `m_nbModeOk`, and `refreshNbControls()` now
runs at both places that recompute it: the `modeChanged` handler, and
`syncFromSlice()`, whose earlier refresh ran before `m_nbModeOk` was set. The
shared DSP level row is unchanged.

## Rebase onto `85f2a124`

Main was 38 commits ahead and the branch was `dirty` in eight files. The move
onto #5919 is a port, not a marker resolution:

- **`SliceModel`** no longer has `noiseBlankerCommandIssued`. `setNb`,
  `setNbKind` and `setNbFill` publish through main's `notifyReceiveDspIntent`,
  and `currentDspRequest()` fills `kind` and `fill` into every Nb request. The
  per-field intent revisions grow from two fields to three.
- **`RadioModel`**: the block of direct receive-DSP `connect()`s this branch
  carried is dropped; `wireSliceReceiveIntentsToBackend()` owns that wiring now.
- **`WdspChannel.h`**: main's `FmReceive` and this branch's two enums landed at
  the same spot; both kept.
- **Tests from main, ported**: `backend_receive_contract_test` (from #5919)
  asserted `Config::noiseBlankerEnabled`, which this branch replaces with the
  `noiseBlanker` kind, so it now asserts `Impulse`.
  `anan_noise_blanker_readback_test` keeps main's typed dispatch. New rows in
  the contract test pin that a Flex reads a host kind as `nb=1` and refuses the
  fill with no wire text, and `noise_blanker_kind_model_test` asserts on the
  `SliceDspRequest` the model emits.

Main has since taken two Icom commits (to `12df6943`). They touch
`IcomCivBackend.{h,cpp}`, which this PR also changes. The merge is clean, and
neither side uses a signature the other changed. CI on `3ef4cd36` runs on the
merge with `12df6943`.

Earlier rebases, onto `b714cd3d` and `732b99a9`, resolved comment-prose
conflicts from #6072, accepted #6075's retirement of
`wdsp_nb_hold_invariant_test.cpp` (its behavioural successor
`wdsp_nb_hold_test` is ported), kept #5982's squelch API, and ported two tests
main had added since the earlier bases: `hl2_rxdsp_adaptive_taps_test.cpp` and
`reroute_dead_controls_test.cpp`.

## Constitution principle honored

Principle IV: clean-room. WDSP's NOB is in-tree. deskHPSDR was read for which
knobs exist and what its defaults are (the 3-way choice, the shared threshold,
the zero-fill default), and no code was copied.
Principle VIII: the blanking, the fill reaching WDSP, single-stage dispatch,
the model's echo handling, the request shape, the fill row's FM rule and the
applied-state readback are each pinned by a test with a mutation run behind it,
listed below.

Squashed-from: #5966

Co-authored-by: tropo1234 <299180028+tropo1234@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jensenpat pushed a commit that referenced this pull request Oct 7, 2026
…#5974)

## Rebased on current main — 2026-10-06

Current head: `ebb35cfd20892431758fd8a165b7c66357b74090`. Base stack
starts at main `5f09371e25ffc032666d36f0ea265dc506621066`; parent
PR/branch is unchanged. This is a signed replay of the existing
increment. It preserves main's unified window chrome and release
debug-symbol archives, alongside the RTL dependency packaging and
explicit Digital-OFF release flags. Generated touchpoint counts were
regenerated from the combined sources.

Fresh rebase validation on analog `ca3a36bd`: native Apple Silicon / Qt
6.12 / RTL required / Digital OFF application build succeeds. **51/52
selected CTests pass** in 226.55 seconds: all 47 RTL regressions and
four additional UI checks pass. The new upstream
`unified_title_bar_test` fails its Alt+F/menu-rounding assertions under
Mac offscreen rendering; an independent rebuild and run on clean main
`5f09371e` reproduces the identical two failures. This is an inherited
test failure, not a green full-suite claim. Static
engine/routing/capability, generated manifests,
registration/frozen-gate, theme and build-options checks pass;
package-helper tests pass 9/9 and required-RTL configuration checks pass
16 combinations. The Digital increment and vendor trees are unchanged by
replay; decoder-ON tests and release packaging were not rerun for these
heads. Hosted checks are being refreshed by the push.

All validation recorded below predates this rebase and remains evidence
for the specific revisions named there. No new live-RF, capacity, or
release-package qualification is claimed. Readiness is unchanged;
Digital #6002 remains draft.

---

## Previous checkpoint record — 2026-10-05

## Scope

Analog WFM backend and applet integration under RFC #5468, based on the
nine-file DSP foundation #5973 and main
`035a858356949a0c902e286ee5250c04f5ef86c0`.

Head: `b8aba26e677f49f07d155685e5f7bef8d2bfe425`. Parent: #5973. Digital
#6002 / RFC #6079 is not a dependency.

The selected RTL WFM receiver exposes Mono/Auto stereo, 50/75 µs
deemphasis, bandwidth and measured pilot/stereo diagnostics. Accepted
backend state drives readback and per-device persistence. The
`broadcastFmReceive` capability is documented for all six backend
families; RTL advertises the implemented controls and other families do
not.

WFM requests use the existing typed receive dispatcher through
`SliceWfmRequest` / `requestSliceWfm`, including thread, current
receiver identity, ownership, mode, value and backend-support guards.
Refusals use the shared reporting path. This carries the #5919
request-routing contract into WFM.

Speaker renegotiation prefers 48 kHz only for an accepted 48 kHz
producer. If opening the new sink fails, it makes one bounded attempt
using the previous rate policy and reports failure if neither opens.
Producer-domain buffers, DSP and rate state are preserved. This shared
AudioEngine behavior affects every backend family. The associated
recorder-rate test follows producer rate rather than inferring it from
the speaker sink.

The opt-in nonblocking WFM exchange uses eight slots: 42.667 ms total
exchange buffering, 32 ms above the legacy depth. Existing
blocking/legacy owners retain their prior depth. Vendored WDSP exchange
changes and test exports are included. Generic ContainerWidget managed
presentation is used by the WFM applet; the default WFM tile placement
applies to RTL.

## Validation and remaining decisions

Exact checkpoint, native Apple Silicon / Qt 6.12 / optional decoder OFF:
4/4 rebuilt tests pass in 17.10 seconds (`wfm_controls_test`,
`audio_engine_rates_test`, `fm_filter_controls_test`,
`rtl_model_acceptance_test`). Removing speaker recovery produces six
failures in the rate test; restored code passes. The regression injects
failed opens, checks bounded fallback and actual-rate stereo rendering,
and preserves producer/DSP/lifecycle state. Static ratchets and
generated manifests pass; independent source review found no remaining
typed-WFM routing defect.

Historical operator stability on `7601593f` is not a new-head soak. An
earlier Mac filter/stereo transition intermittently withdrew a WFM
receiver; passing diagnostic reruns did not establish a root cause, and
this remains unresolved.

Maintainer review still covers WFM applet/default presentation, the
typed public seam and vendor exchange/test surface, and the buffering
tradeoff. The approved RTL-only free-pan and confirmed-control seam from
#5924 are not reopened. Normal admission remains one receiver; 2/4/8
evaluation and capacity qualification belong to the delivery increment.
No Digital approval is requested.

Principles II, V/XIV and XI: accepted state, scoped persistence and
demonstrated behavior.

---
73, Ozy **K6OZY** · Codex
rfoust pushed a commit that referenced this pull request Oct 12, 2026
#6255)

Refs #5678 (HL2 parity rows), #5263 (the drop notice). No `Fixes` line:
I found no issue for this.

## Limits first

- **Nothing here was seen in the running app or on a radio.** The defect
is a source reading of `main`; the fix is tested on `RadioModel` with an
injected backend, and the four `MainWindow` call sites are pinned as
text only.
- **This is not an M4 conversion.** The `display panafall set` text is
not replaced by a typed intent. It moves out of `MainWindow_Wiring.cpp`
into the two wire sites `RadioModel` already has.
`tools/check_command_plane.py`: `MainWindow_Wiring.cpp` 20 to 14,
`RadioModel.cpp` unchanged at 135.
- **One design choice is yours: the Black Level gate.** It reads
`RadioCapabilities::panBinsAbsolute()`, the same proxy for the row unit
the renderer reads (`radio-capabilities-map.md` calls it a proxy). If
the row unit gets its own signal later, this gate should follow it. An
undeclared `panAmplitude` reads as not absolute, so a backend that
computes dB rows and forgets the declaration gets a false notice, not a
missing one.
- **On Icom, Black Level keeps its notice, also when it comes from Clone
to all Pans or Reset display defaults.** Reset turns auto black on, so
the value it writes is not in use at that moment; the notice is still
raised there, as it is on `main`. I did not make that case quieter.
- **ANAN and RTL-SDR are reached** (both declare `binsAbsolute`), and
the test does not run with their capabilities: only Hermes-Lite 2, Icom
and Flex.
- **Two other false notices on a Hermes-Lite 2 are not touched:** the
dBm scale (`sendDbmRangeCommand`, beside the dBm-range work of my #6140,
now merged) and `cw break_in` (left open by #6111).
- **The test ends in a source-text pin** (six checks). It names its
claim.
- The branch is rebased onto `121340c0`, with no conflict; head
`182bb99c` adds two review commits on `1935624d`. Two open PRs edit the
same files: #6241 merges with this branch without conflict; #6199
conflicts with it only on adjacent new lines in the `tests/tests.cmake`
test list (it also conflicts with `main` itself in `RadioModel.cpp`,
independently of this PR). Neither touches the four sites, the two
setters, or the gain and black text. Neither merged tree was built.

## What was wrong on main

On a backend with no Flex command plane, four Display-panel controls set
their value on the `SpectrumWidget`, which draws it, and then also sent
Flex wire text:

| control | site in `src/gui/MainWindow_Wiring.cpp` | text |
|---|---|---|
| WtrFall Gain | `wirePanadapter`,
`SpectrumOverlayMenu::wfColorGainChanged` handler | `display panafall
set <wf> color_gain=N` |
| Black Level | `wirePanadapter`,
`SpectrumOverlayMenu::wfBlackLevelChanged` handler | `display panafall
set <wf> black_level=N` |
| Clone to all Pans | `MainWindow::cloneDisplaySettingsToAllPans` |
both, per target pan |
| Reset display defaults | `wirePanadapter`,
`SpectrumOverlayMenu::displaySettingsReset` handler | `color_gain=50`,
`black_level=15` |

Each went through `RadioModel::sendCommand()` to `sendCmd()`, which has
no command plane to write to, logs the drop and emits `commandDropped`.
`MainWindow` shows that once per session as "nothing was sent to the
radio". So the notice was raised for a control that had acted, and the
one-shot was spent.

`RadioModel::setWaterfallColorGain()` and `setWaterfallBlackLevel()`
already return early without a command plane. The four sites did not use
them: those setters address the active pan, the sites address a named
one.

Manual Black Level acts on a Hermes-Lite 2 since #6115
(`WaterfallLevelMap::manualBlackThreshold` on absolute-dB rows).

## The change

- `RadioModel` gains `setWaterfallColorGainFor(panId, gain, autoBlack)`
and `setWaterfallBlackLevelFor(panId, level)`. All four setters go
through two private senders, `sendWaterfallColorGain()` and
`sendWaterfallBlackLevel()`, which hold the only wire site and the only
command-plane gate for each key.
- The four sites call those setters after the widget call, and pass the
pan's `wfAutoBlack()` with the gain. They no longer build wire text.
- **WtrFall Gain** is withheld without a command plane where the client
draws it: auto black on, or `panBinsAbsolute()`. With manual black on
any other row (Icom), every row is black whatever the gain, so it goes
to `sendCmd()` and is reported.
- **Black Level** is withheld without a command plane only where
`panBinsAbsolute()` is true (Hermes-Lite 2, ANAN, RTL-SDR). Elsewhere it
goes to `sendCmd()` and is reported. On Icom the manual black point is
still in tile units against a dB row, so every position draws black: the
slider does nothing there and says so.
- The connect-time replay (`setWaterfallBlackLevel()` from
`panadapterInfoChanged`) stays quiet on every backend with no command
plane, as on `main`. No operator moved anything.
- No capability is added and no family is named.
- `docs/architecture/radio-capabilities-map.md` records the flag's new
consumer.

**A Flex is sent what it is sent today**: same text, same pan, same
order. The sites used `sendCommand()` and the setters use `sendCmd()`;
the only thing `sendCommand()` adds is the pan-ownership gate, which
matches `display pan set ` and not `display panafall set `.

## How this sits with the #5919 review

That review kept the notice for an operator-moved control the backend
cannot apply, because nothing happened and the notice says so. It was
about receive controls; nobody has ruled on display text, and I am not
reading it as covering this.

The rule used here is narrower than "no command plane, no notice": the
send is withheld only where the control has acted in the client. Where
it has not (Black Level on Icom, and WtrFall Gain on Icom with manual
black), the notice stays and the test pins it. If you would rather keep
the notice on these four until pan commands get a typed intent, close
this and I will leave them.

## Tests

`pan_display_drop_notice_test` (new, socket-free, 41 checks). The
injected backend carries `capabilities()` read off a real `Hl2Backend`,
`IcomCivBackend` and `FlexBackend`; panes are made the way a backend
without Flex status makes them.

- **Hermes-Lite 2 capabilities, no command plane (12 checks):** the
renderer's law changes with gain (auto and manual black) and with the
manual black level for a dB row; none of the four raises
`commandDropped`; the same gain text sent raw is still reported, so the
alarm is live in the fixture.
- **Icom capabilities, no command plane (13 checks):** with auto black
gain raises nothing; with manual black no gain changes a dB row and gain
raises `commandDropped`, with the exact text; every Black Level position
draws a dB row black and the slider still raises `commandDropped`; Clone
reports the black level only with auto black, and both keys with manual
black; Reset reports the black level only; the connect-time replay
raises nothing.
- **A command plane (10 checks):** the wire text compared whole through
a command sink on an unopened `RadioConnection`, for each control, for
the target pan and not the active one, gain before black. With
absolute-dB rows and a command plane the black level is still sent.
- **Source-text pin (6 checks).** The claim: each of the four sites sets
the widget and then calls the model setter, and `MainWindow_Wiring.cpp`
builds no gain or black wire text of its own. No behavioural seam
reaches it: the sites are `MainWindow` lambdas and no test target
constructs a `MainWindow`. It shows how the sites are written, not that
they run.

**Mutations**, each as a scratch commit on `182bb99c` through the full
suite:

| mutation | failing checks of 41 | other tests |
|---|---|---|
| both gates removed, the four sites as on `main` | 15 |
`icom_family_test` |
| Black Level gated on `hasCommandPlane()` alone | 4 | none |
| colour gain ignores auto black (row unit only) | 4 | none |
| colour gain ignores the row unit (auto black only) | 4 | none |
| colour gain always withheld (the `1935624d` rule) | 2 | none |
| the connect-time replay reports its drop | 1 | `icom_family_test` |

The legs share one capture of dropped commands, so a wrong drop is also
counted by the checks after it in that leg.

Full ctest on macOS (Qt 6.12.0, Ninja, RelWithDebInfo) at `182bb99c`:
654 of 658 pass, at -j8 and on the serial rerun. The four failures are
known on clean `main` on this machine, each with only its known check
lines: `control_receive_test` ("RTL adjustable filter qualification
excludes legacy WFM", #6248), `vkamp_connection_test` ("commands after a
disconnect are dropped, not queued", #5254),
`peripheral_auth_dialog_test` (#6008, a serial-port name of this host)
and `unified_title_bar_test` (its two menu checks, #6224). This PR
changes `MainWindow_Wiring.cpp`, but only the four display sites; the
same two title-bar checks fail identically on two other branches at the
same base that do not touch it. Nothing else this PR touches is built by
those tests.

Every script step of the `Static checks` job was run locally at
`182bb99c` with stdin from `/dev/null` and is clean: colour ratchet +0,
`gen_touchpoint_manifest.py --check` up to date with no change,
`check_a11y.py` on the one changed GUI file with no findings. The
`Sanitizer option configures` job needs the CI image and was not run.

## Not verified

- Nothing in the running app; nothing on a Hermes-Lite 2, an Icom or a
Flex.
- Whether each of the four controls is enabled and reachable on a
Hermes-Lite 2 today.
- That the Icom Black Level slider is dead, and WtrFall Gain with it
under manual black, is the renderer's arithmetic plus `IcomCivBackend`
sending its scope rows through `toDbm`; I have no Icom.
- That the three sites pass the pan's own `wfAutoBlack()` is pinned by
source text only.
- ANAN and RTL-SDR, beyond their `binsAbsolute` declaration.
- Linux and Windows builds: CI will say.

73, ON8ST

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
rfoust added a commit that referenced this pull request Oct 12, 2026
…I. (#6139)

**Update after review, `ea2cf144` (2026-10-05):** ownership of the flag
is now declared, not inferred. Only the Hermes-Lite 2 declares the
client its owner (`RadioCapabilities::txWaterfallClientFlag`,
`clientPersistsFlag` true). The demo, ANAN, Icom and RTL-SDR declare
nothing and keep `main`'s behaviour: the toggle sends the wire text, the
model stays false, nothing is stored or restored. That is narrower than
this PR was. On the demo, `txwaterfall on` answers with the radio-echo
note again. Full suite at this head: 644 of 645; the one failure is
`peripheral_auth_dialog_test` (#6008), which fails on clean `main` too.
Source line numbers below are `main` at `85f2a124`.

Refs #5678 (HL2 parity; "Show TX in Waterfall", row P30 of our status
table). No issue exists for this.

## Limits, first

- **Nothing was keyed.** On an HL2 the toggle now sets the flag. What
the waterfall draws during an over on an HL2 has not been seen, and what
that radio's receive path reads while it transmits has not been
measured. A `true` model flag shows neither the colour law nor real
receive IQ while keyed; the demo cannot key at all.
- **The fix has not been seen in a running app.** The dead toggle was
(2026-10-01, bridge verb, 6 of 6 polls false). The fix is tested
socket-free on the real models and dialog.
- **It reaches the Hermes-Lite 2 only.** The request, the store and the
restore are gated on one accessor,
`RadioCapabilities::clientPersistsShowTxInWaterfall()`, which reads a
declared per-feature record. Flex, Icom, ANAN, RTL-SDR and the demo
leave the record absent. On Icom, ANAN, RTL-SDR and the demo the toggle
is as dead as on `main`. Engaging another family is one declaration, for
that family's owner or the maintainer to make.
- **Yours to move:** the keyed rows take the colour law a Flex's keyed
rows use (`dbmToWaterfallLevel`). On an HL2 they are absolute dB (dBFS
under a dBm label); whether the law suits them is unknown until someone
keys one.
- **Persistence is in the radio's settings document**, not a flat key:
one optional bool, `showTxInWaterfall`, in the existing per-radio
`ClientDisplay` feature document. No schema bump. Written and read only
where the backend declares the client its owner.

## What was wrong on `main`

On a Flex the flag is the radio's: `transmit set show_tx_in_waterfall=`
tells it, and its transmit status echoes it (`FlexBackend.cpp:1193`).
That echo is the only writer of `TransmitModel::showTxInWaterfall()`.

- The Radio Setup button sends the wire text unconditionally
(`RadioSetupDialog.cpp:2717`), and so does the bridge verb
(`AutomationServer.cpp:3599`).
- A radio with no command plane drops it (`RadioModel.cpp:9764`) and
nothing echoes, so the flag can never become true. The demo has a
command plane and no handler for the flag, so nothing echoes there
either.
- While keyed, the pane holding the TX passband draws rows only when the
flag is true (`SpectrumWidget.cpp:8488`). With it false that pane's
waterfall stands still for the whole over.

There is something to draw: the HL2 mutes its demodulator while keyed
and the spectrum keeps running on real IQ (`Hl2RxDsp::setAudioMuted`).

## The change

- `RadioModel::requestLocalShowTxInWaterfall(bool)`, the shape of the
existing `requestLocalPanWeightedAverage`: where the backend declares
the client the owner of the flag it applies the flag and returns true;
everywhere else it returns false and the caller sends the wire text,
which only a Flex answers. No raw command is added above the seam
(`check_command_plane`: 400, baseline 400).
- It writes the model through `TransmitModel::applyChanges`, the path an
echo takes.
- It stores the value per radio, on the same declaration. A disconnect
clears the transmit model (`TransmitModel.cpp:43`) and no status puts
the flag back, so `RadioModel::onConnected()` restores it, on the same
declaration.
- The Radio Setup button and the `txwaterfall` verb ask the model first.
- `RadioCapabilities::txWaterfallClientFlag`: a new optional per-feature
record with one field, `clientPersistsFlag` (no default). Engaged by the
HL2 only. No top-level capability boolean is added
(`check_capability_records`: 70 of 70).

`SpectrumWidget` is not changed. Default unchanged: off.

## Tests

`show_tx_in_waterfall_client_flag_test` (new, socket-free, real
backends): on the real `Hl2Backend`, request, store per radio, restore
on connect, and the bridge verb; on the real Icom, ANAN, RTL-SDR and
demo backends, each with a `ClientDisplay` document that already holds
the flag, nothing is restored, the request declines and the document is
untouched; the same on a real `FlexBackend` (the echo still works) and
on a backend that engages the record with the field false; the bridge
verb's radio-echo note on Flex and on the demo; the store's refusals.

`radio_setup_show_tx_waterfall_test` (new, production dialog, real
backends): on an HL2 a click sets the model and sends no wire text; on
an Icom, which declares no owner, and on a Flex the click sends the wire
text and the model is not set. No source-text pin.

Fail-before, one build and one run per row, failing checks in the two
tests:

| mutation | client flag test | dialog test |
|---|---|---|
| this branch | 0 | 0 |
| `main`'s behaviour (request declines, connect restores nothing) | 11 |
1 slot |
| the request does not write the model | 5 | 1 slot |
| the connect edge does not restore | 1 | 0 |
| the request is taken on a Flex too | 4 | 1 slot (Flex) |
| the request does not store | 3 | 0 |
| the button sends the wire text unguarded | 0 | 1 slot |
| the bridge verb skips the local route | 2 | 0 |
| the store ignores who owns the flag on read | 1 | 0 |
| the ownership accessor is true for every backend | 40 | 2 slots |
| the ownership accessor is false for every backend | 15 | 1 slot |

The two accessor rows were measured at `ea2cf144`, one build and one
full run each. The other rows were measured at earlier heads, before the
opt-out checks existed, and were not re-run.

Full ctest, macOS, Qt 6.12.0, at `ea2cf144`: 644 of 645 pass. The one
failure is not ours; see below.

Static checks (`static-checks.yml`, stdin from `/dev/null`): clean.
`check_a11y` on `RadioSetupDialog.cpp`: 9 warnings, the same 9 as
`main`, none on lines this PR touches. `check_capability_records`: 70
booleans, frozen at 70. The steps were run from this branch's
`static-checks.yml`; `main`'s newer `check_build_options` steps are not
on this branch and were not run.

## With the open PRs

- #5924, #6117 and #6115 are merged and in this branch. #5919 is merged
into `main` and is not in this branch.
- #6140 (open, `7a5ca748`) adds its own records to
`RadioCapabilities.h`; a trial merge with this head is clean and the
merged header still counts 70 booleans. A trial merge with `main`
`85f2a124` is clean. Neither merge result was built.

## Not verified

- Anything keyed; see Limits.
- Linux and Windows at this head. ANAN, Icom and RTL-SDR on a radio:
they keep `main`'s behaviour by declaration, checked on their real
backends, never connected.
- A running app at this head, the demo included.
- That the main window hands a restored flag to the spectrum widget.

**Known red on `main`:** `peripheral_auth_dialog_test` (#6008) fails on
this Mac at `ea2cf144` with the diagnostic it prints on clean `main` at
`22507580` (pasted on #6134): "Clear code or Status is clipped at the
default dialog size", `dialog 960x680, viewport 658, page minimum 784,
horizontal scroll max 126` (a serial-port name on this host widens the
page). Clean `main` was not re-run at `85f2a124`.
`hl2_pan_create_async_test` and `vkamp_connection_test` passed here.

73, ON8ST

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Robbie Foust <rfoust@gmail.com>
@rfoust
rfoust deleted the codex/aetherd-m4-receive-controls branch October 14, 2026 01:12
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.

aetherd: consolidate slice receive DSP and audio control routing

6 participants