Skip to content

RTL M1: confirmed receive state, free capture browsing and finer zoom - #5924

Merged
ten9876 merged 40 commits into
mainfrom
codex/rtl-m1a-capture-transactions
Oct 4, 2026
Merged

ten9876 merged 40 commits into
mainfrom
codex/rtl-m1a-capture-transactions

Conversation

@Ozy311

@Ozy311 Ozy311 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

RTL receive changes now publish only after hardware readback and DSP adoption. Capture browsing preserves each slice's frequency and settings, parks receivers outside usable capture, and resumes them when capture returns. Offscreen markers reveal the receiver through a confirmed capture transaction. Zoom uses a continuous 65,536-sample FFT (36.621 Hz bins at 2.4 MS/s), independently of audio and capture bandwidth.

FM/FM-N have symmetric capability-bounded filters, 48 kHz per-receiver PCM, squelch, session/revision fences, rollback and accepted-state persistence. Requested or refused state never becomes saved live state. Spectrum averaging preserves RF history through display movement, and coherent capture coverage supports the 3D waterfall. PPM/DC settings use confirmed backend controls. Production admission at this checkpoint remains one receiver.

Implements the bounded receive runtime of RFC #5468; respects the backend seam obligations in #5554/#5262. WFM stereo, optional digital decoding, live PPM interaction and multi-RX delivery are downstream increments.

Integration and validation

Reconciled with main at a798558ac (#6118).

The reconciliation preserves main's typed tune/filter/mode/APF intents, HL2 squelch and WDSP changes. Each slice's SQL intent uses the existing common audio-intent binding once; the merge does not add a duplicate backend connection. The confirmed-pan dispatch comment now distinguishes an accepted request awaiting observation from refusal. Runtime documentation now describes the implemented 65,536-bin display FFT.

Checkpoint efc358a1 native Apple Silicon/Qt 6.12 build and CTest: 5/5 pass, no skips, in 11.29 seconds (receive_intent_routing_test, slice_apf_agc_off_seam_test, flex_slice_mode_intent_test, rtl_model_acceptance_test, slice_model_letter_test). These are socket-free model/dispatch regressions; they do not claim live-radio, GUI or hardware timing qualification. The downstream #6083 application and broader tests are separately revision-pinned there.

This requests human review and preserves the outstanding in-scope maintainer decisions; it does not mark the free-pan amendment approved. HD decoding is outside this PR and HD consent is not a merge condition here.

Upstream's single spectrum outlet and socket-free test rehoming are preserved. The real model derives paced waterfall rows from spectrum observations; retired orphan waterfall signals are not restored. The new upstream receive-contract test now expects RTL's implemented FM/FM-N filter capability. Generated seam/touchpoint manifests are regenerated.

Earlier downstream aggregate evidence (not rerun on this individual head): the aggregate builds AetherSDR with native Apple Silicon/Qt 6.12, RTL required and the optional decoder OFF. Focused downstream tests already pass for Flex mode dispatch, confirmed RTL model/capture state, FFT averaging, WFM, the PPM widget and seam consumers. This is aggregate evidence, not a claim that every platform was rerun on this individual head. Fresh hosted checks are required on this PR.

The operator reports that the delivered 7601593f build ran stably for days. That is live hardware feedback on the delivered build, not an instrumented soak of the new upstream integration. Current-head sanitizer, ARM throughput and full release-artifact qualification are not claimed.

CI integration repair

Upstream Radio Setup test targets (including #6008's peripheral_auth_dialog_test) compile RadioSetupDialog.cpp, so they also list the RTL widget and its availability registry. Both repaired tests build and pass on the native Mac decoder-OFF integration (2/2, 4.00 seconds). This follow-up changes test linkage only; earlier application/regression measurements retain their stated revisions. New-head CI is required and remains distinct from those measurements.

Maintainer decisions

All of the decisions below were made by Jeremy, the maintainer.

HL2 FFT-FPS/dBm persistence coordination stays separate, with sibling ClientDisplay tables preserved.

New gui/ touchpoints (ratified)

Header New includer Why
core/backends/RadioCapabilities.h src/gui/ModeFilterPresets.cpp FM/squelch availability reads the declared ReceiveFilterControl/SquelchLevelScale records
models/PanadapterModel.h src/gui/PanFrameGuard.h Snapshots the pan's geometry when a frame arrives, so a confirmed FFT is never painted against a newer range
models/RadioModel.h src/gui/ReceiveCaptureAction.h, src/gui/RtlReceiverSettingsWidget.cpp Capture-recenter action and the RTL device settings both go through RadioModel's capability-gated verbs
models/SliceModel.h src/gui/MainWindowHelpers.cpp requestSlicePanCenter resolves the slice's pan and sends confirmed backends a Center tune instead of a pan request

Effects on other radios

  • Squelch availability follows one rule in both the RX applet and the VFO (ModeFilters::squelchAvailableInMode). A squelchLevelScale with modesExclusive (RTL only: FM/FM-N, −120 + 1.2·level dBFS/bin, Auto SQL) lists the supported modes, keeps each receiver's manual/Auto intent, and has Auto start from the absolute threshold. Otherwise the squelch cuts signals in rtty #2504 rule disables SQL in DIGU/DIGL/NT/RTTY/CW, unless the radio declares mode-independent squelch. Every other family's scale leaves modesExclusive false, so its SQL line, Auto SQL and availability are unchanged.
  • SQL's unavailable reason stays on the button. Where SQL is unavailable in the current mode, the RX applet and the VFO no longer replace the registry's accessible reason with the Auto-SQL description.
  • FM-family passband availability follows ModeFilters::fmFilterAdjustable. Declared FM rows are authoritative (RTL). A radio-owned filter with no FM row refuses FM edits (Flex, per FlexLib Slice.cs). HL2, ANAN, Icom and the Demo keep their existing adjustable passband.
  • WFM/WBFM use carrier-centered FM edges and no longer inherit the SSB preset ladder.
  • Cancelled VFO drags release the drag hold without the final retune (SpectrumWidget::sliceDragCancelled).
  • Off-thread SliceModel setters are refused and logged.

CI

The Linux build job now deletes unused host toolchains (Android, .NET, GHC, CodeQL) before building. The full RelWithDebInfo tree, which now includes this PR's RTL tests (about 3.6 GiB of test binaries), outgrew the hosted runner's free disk. Jeremy authorized the workflow change.

Integration with #6107

#6107 gated a stranded RTL worker on the connectRadio() wiring this PR replaces. This PR's startCapture() already detaches the worker before stopping it, and gates every relay on producer identity and the capture session, so that wiring is kept. #6107's read-error guarantee is pinned in rtl_capture_worker_test's stop-timeout block. That test fails if the producer gate is removed.

Principles II, V and XIV govern confirmed state and persistence; VIII/XI govern evidence and regression tests.


73, Ozy K6OZY · Codex

Generated with OpenAI Codex (GPT-6 Astra)

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.
@Ozy311 Ozy311 self-assigned this Sep 22, 2026
@Ozy311
Ozy311 marked this pull request as ready for review September 22, 2026 20:50
@Ozy311
Ozy311 requested review from a team as code owners September 22, 2026 20:50

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1. Issue fit

No fixes/closes #NNNN reached me in the PR metadata, so I reviewed against the PR's own stated intent plus docs/rtl-m1-runtime.md (which this PR adds and which reads as the acceptance spec) and the RFC #5468 lineage already present in RtlSliceSettings.h / RadioStateMemory.h. On that basis: Partially — the M1 runtime (transactional capture owner, prepared-receiver reuse, 48 kHz FM extraction, mixer, typed PCM, accepted-state persistence) is all present, wired into CORE_SOURCES, and backed by real tests. What is not settled is the maintainer-facing surface the doc itself defers: the FM mode-transition passband resize, the new cross-family IRadioBackend virtuals, and the declared dependency on unmerged PR #5919. Those are decisions, not defects — see §3.

This is architecture work under GOVERNANCE.md, and it does cite RFC #5468 throughout; I take the RFC prerequisite as satisfied, but the new seam added here (configureSettingsScope / storeOperatingState on IRadioBackend) is broader than RTL and I did not find it named in the RFC text I can see.

2. Scope

File / group What it changes Claimed by the PR? Verdict
CMakeLists.txt registers the four new RTL sources in CORE_SOURCES yes In scope
docs/rtl-m1-runtime.md new design/admission doc yes In scope
src/core/backends/rtl/Rtl{CaptureTransaction,ReceivePipeline,AudioMixer,RfExtractor}.* the M1 runtime itself yes In scope
RtlReceiverRegistry.{h,cpp} shared-ptr receivers, cross-bank reuse, currentHandle, submitVerifiedRollback, cancelPending mostly In scope; cancelPending() has no production caller (nit)
RtlSdrBackend.{h,cpp}, RtlSdrWorker.{h,cpp}, RtlSdrDdc.{h,cpp} backend/worker integration, legacy DDC retained yes In scope
IRadioBackend.h two new cross-family virtuals yes, but understated Needs maintainer decision
RadioCapabilities.h new RtlSlices client-settings domain yes Needs maintainer decision (new capability surface)
RadioModel.cpp routes the persistence write through the backend hook yes In scope
RtlSliceSettings.h, RadioStateMemory.h comment-only ownership updates yes In scope
WdspChannel.{h,cpp} per-thread allocation guard; Config::operator== yes In scope
third_party/wdsp/** (4 files) adds wdspPortThreadAllocationSequence() on both platform arms, documented in AETHERSDR-PATCHES.md yes In scope — additive, process-wide counter retained, patch entry written. This is the right way to touch the vendored tree.
tests/** (+ tests.cmake) 7 new/extended targets, all registered yes In scope
CHANGELOG.md not touched — Correct — please keep it that way

Nothing in the diff is unexplained by the stated scope. No unrelated bundling, no removed guards, no formatting churn.

Socket-test disclosure (informational, not a finding): I grepped every added/modified test source (rtl_capture_transaction_test, rtl_capture_worker_test, rtl_receive_pipeline_test, rtl_audio_mixer_test, rtl_rf_extractor_test, rtl_runtime_settings_test, rtl_backend_test, wdsp_allocation_scope_test, RtlInjectedDevice.h, CallbackAllocationProbe.h) for QTcpServer/QTcpSocket/QUdpSocket/QLocalServer/bind/listen/connectToHost/peer processes. Zero hits. Device access is via the injected RtlSdrWorker::Device seam, settings via TestSettingsProfile.h. This PR adds no socket-owning test and no fake radio peer. Every new target is registered in tests.cmake; none of them are in CI's -R filters, which is normal here.

3. Blockers

1. createSlice() dereferences m_capture.confirmed() without checking it is engaged — src/core/backends/rtl/RtlSdrBackend.cpp:464 (inline comment). RtlCaptureTransaction::complete() resets m_confirmed on the Invalidated path, and serviceCapture() reaches emit connectionError(...) before disconnectRadio(), so there is a window where m_connected == true, !m_capture.busy(), and confirmed() is std::nullopt. Any re-entrant event-loop spin from that signal (a modal notice, a queued GUI action) can land in createSlice() and dereference a disengaged optional. Every other reader of accepted state in this file goes through m_lastPublished and null-checks it first; this one does not. I reasoned this from code — I have no build and could not reproduce it at runtime — but the guard is one line and costs nothing. Suggested fix in the inline comment.

2. Needs maintainer decision, not a code defect — the FM mode-transition passband resize. src/core/backends/rtl/RtlSdrBackend.cpp:507: selecting FM/FMN on a receiver whose inherited passband cannot describe the FM graph now silently rewrites the user's filter edges to ±8 kHz. The PR's own doc says "This mode transition needs maintainer UX review", which I read as the author correctly refusing to self-certify it. Per GOVERNANCE.md this is a user-visible behavior change to an existing control and needs a maintainer ruling before merge — it is not something a reviewer can wave through. I am not calling it smuggled preference: it is technically forced (the 48 kHz graph cannot carry a 200 kHz passband) and it is disclosed.

3. Needs maintainer decision — new cross-family public seam. IRadioBackend gains configureSettingsScope() and storeOperatingState(), and RadioCapabilities::ClientSettingsDomain gains RtlSlices (RadioCapabilities.h:629, inline comment). Both are surfaces every backend inherits and that outlive this PR. The storeOperatingState contract in particular inverts a rule the header previously stated in absolute terms ("the backend never touches the settings store") — the new comment is honest about the inversion, which I appreciate, but the inversion itself is a maintainer call. I looked for a canon sentence forbidding it and did not find one; AGENTS.md's Settings Persistence section requires a feature document with one owner and a checked write result, and this design satisfies both (std::optional<bool>, refusal never falls through — rtl_runtime_settings_test.cpp:80-85 pins exactly that). So: legitimate, but it should be ratified rather than inherited.

4. Nits (non-blocking)

  • RtlSlices = 1u << 8 is inserted above Cw = 1u << 7 (RadioCapabilities.h:629). Values are correct and unique; the ordering just reads as a mistake. Move it below Cw.
  • docs/architecture/radio-capabilities-map.md is not updated. Line 186 enumerates clientSettingsDomains and line 256 still records RTL's row as Tuning|Passband|SpanRate|RfGain|Memories, which this PR makes conditionally false. I could not find a canon sentence making the caps-map update merge-gating, so this is a nit — but the doc is now wrong for RTL, and rtl_runtime_settings_test.cpp:63 already asserts the new shape, so the table is the only thing out of date.
  • RtlReceiverRegistry::cancelPending() has no production caller (RtlReceiverRegistry.cpp:616, inline comment). Only rtl_receiver_registry_test.cpp:679 exercises it. Either wire it (the prepare()-fails-after-submit path below is a candidate) or drop it.
  • RtlReceivePipeline::prepare() validates monitor bounds after the registry has already accepted the bank (RtlReceivePipeline.cpp:76, inline comment) and after m_prepared is latched at line 73. Unreachable today because RtlCaptureTransaction::submit() already clamps audioGain/audioPan to 0..100, so this is ordering hygiene, not a live bug.
  • The mixer's configure() return value is discarded in RtlReceivePipeline::process(). A rejected configure (e.g. a zero handle.instance) leaves the mixer running against the previous slot map while push() silently rejects the mismatched instances — audible as one-sided silence with no counter a support bundle could show.
  • storeOperatingState() refuses the generic writer before the first published capture (RtlSdrBackend.cpp:692, inline comment). if (!m_lastPublished && m_settingsScope.isValid()) return false; runs before the !m_settingsActive check, so a session that connects and disconnects without ever publishing a capture loses its disconnect-flush write entirely — on main that flush went to RadioStateMemory::store. Narrow window, no test covers it, and no comment explains whether it is deliberate. Worth a one-line comment either way.
  • None of the observability counters reach diagnostics. RtlAudioMixer::lateFrames()/rejectedBlocks(), RtlReceivePipeline::droppedPackets(), RtlRfExtractor::outputFrames()/groupDelayInputFrames() are read only by tests. Given how much of this design's correctness argument rests on "drops are bounded and observable", a field report of RTL dropouts will arrive with zero evidence. A row in the RTL health/diagnostics map would close that.

5. What I tried to break

Everything below is reasoned from the head checkout at /tmp/aetherclaude/pr-5924; I have no build, no test run and no hardware, so nothing here was reproduced at runtime.

  • The WDSP reservation accounting under cross-bank receiver reuse. I expected prepared->reservation = reserveChannels(newCount) to under-reserve and let the pool over-admit once the old bank's Reservation destructs while its reused WdspChannels stay alive. It doesn't: WdspChannel.h:108-128 documents that a consumed slot belongs to the channel and only unconsumed slots return, so the reused receivers carry their own channel IDs. I also checked the newCount == 0 path for a null-Reservation dereference — the if (prepared->receivers[i]) continue; skip makes *prepared->reservation unreachable in that case. Both attacks failed.
  • Lifetime of reused receivers across the acquisition/executor boundary. The shared_ptr copy at RtlReceiverRegistry.cpp:~300 is taken under the executor mutex while the callback holds only raw pointers, destruction is executor-only, and ReceiverSpec::operator== includes epoch, handle.instance and capture — and submitImpl overwrites receivers[i].capture before comparing, so a capture change cannot alias into a reuse. I could not construct a two-banks-driving-one-receiver case.
  • Integer/position arithmetic in the mixer and extractor. I walked push() for firstSample + size overflow, fully-past blocks, partial-past blocks, ring aliasing at position % kCapacity (stale frames fail the frame.position == m_next + i identity check and degrade to silence, not to wrong audio), and slot deactivate/reactivate with a matching instance+epoch (!slot.active forces invalidation). I hand-traced every assertion in rtl_audio_mixer_test.cpp against the implementation, including the 256 + kDeadlineFrames - 1 / + kDeadlineFrames boundary pair and the 0.4/0.2/clip expectations — they compute out exactly, and they would fail if the deadline, the 1/N headroom or the identity checks were removed. That test is not self-confirming.
  • RtlRfExtractor late-join and subspan underflow. I tried to make input.subspan(m_startInput - firstSample) underflow by feeding blocks shorter than the lattice period; the contiguity guard (firstSample != m_nextInput → withdraw) plus the m_nextInput <= m_startInput early return make firstSample <= m_startInput an invariant on every pre-start call. The worst case I could produce is the documented ~1 s join delay at 225001 Hz, which the doc already states.
  • The transaction's compensation path. I checked that a compensation completion cannot be confused with a forward one (operation is bumped, token retained), that a Restored result is validated against work.before rather than work.target, that m_confirmed survives a successful compensation, and that a malformed-but-correctly-tokened Applied still invalidates. I did not find a way to publish a superseded state.
  • The persistence seam. rtl_runtime_settings_test.cpp covers the cases I would have asked for: pre-readback claim refusal, overlapping-domain removal as a set, the preserved legacy downgrade snapshot including an unknown "future" field, newer-schema refusal that stays handled, retry after refusal, anonymous-scope identity, and the USB index never becoming a persistent ID. The one gap I found is the pre-first-publish window in §4.
  • What I could not check: anything requiring execution — the four-carrier WDSP separation threshold (rejection > 20 dB) and the callbackAllocations == 0 claim in rtl_receive_pipeline_test.cpp are well-constructed and would fail against a broken implementation, but I cannot say whether they pass or how stable that dB threshold is across platforms and FFTW wisdom states. The doc's 30-minute integrated qualification gate for raising kQualifiedReceiverCapacity is, correctly, not claimed as met here — kQualifiedReceiverCapacity is still 1.

6. Recommendation

Needs maintainer decision, with one small code fix first.

Fix blocker 1 (m_capture.confirmed() null-check) — it is a one-liner. Then this needs a maintainer on three things a reviewer cannot rule on: the FM mode-transition passband resize (which the author correctly flagged rather than self-certified), the new IRadioBackend virtuals and RtlSlices capability domain, and whether to land ahead of PR #5919 given the doc's own "refresh and integrate the dependency before claiming end-to-end model acceptance".

On engineering quality this is unusually strong for its size: the vendored-WDSP change is additive and documented in AETHERSDR-PATCHES.md, admission stays at one receiver with an explicit evidence gate for raising it, the tests are socket-free and would genuinely fail against broken code, and the doc is candid about what is not qualified. Thanks for writing the "Evidence and admission gates" section the way you did — a PR that names its own unproven claims is much faster to review than one that doesn't.


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

Comment thread src/core/backends/rtl/RtlSdrBackend.cpp
Comment thread src/core/backends/rtl/RtlSdrBackend.cpp
Comment thread src/core/backends/rtl/RtlSdrBackend.cpp
Comment thread src/core/backends/RadioCapabilities.h Outdated
Comment thread src/core/backends/rtl/RtlReceiverRegistry.cpp Outdated
Comment thread src/core/backends/rtl/RtlReceivePipeline.cpp Outdated
@Ozy311

Ozy311 commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Review follow-up in 53b364fe5d4c121ce1d6f268dea320e1a18b1430:

  • Guarded reentrant creation after capture invalidation; moved preparation refusal checks before submission and removed unused cancellation API.
  • Handled mixer configuration refusal by discarding its old slot map/buffer and requesting repair. A seeded-audio regression proves stale output cannot escape; removing the reset fails it.
  • Added cumulative queue-drop, late-frame, rejected-block and configuration-failure counters to the existing RTL health snapshot. Callback code only publishes atomic mirrors; the existing owner service timer caches them. Real injected acquisition saturates the bounded queue and proves its counter reaches health. These are independently sampled counters, not callback latency or full performance qualification.
  • Preserved pre-publication persistence refusal with a mutation-proven actual model disconnect-flush test; pinned live capability ownership relay and corrected enum/docs.

Validation at 53b364fe5d4c121ce1d6f268dea320e1a18b1430: native Mac, Windows and Nobara each passed the five affected RTL-ON tests; Mac and Windows also passed the three RTL-OFF tests. Mac ASan/UBSan passed all five (the final strengthened persistence test was separately rebuilt/rerun). Instrumented-Qt TSan passed all five plus three repetitions each of pipeline, registry and worker. Five isolated mutations were caught, including a passing hardened control for the optional guard; seven static checks passed. Six production TSan objects and five ASan objects have instrumentation witnesses. The full source manifest (3545 files) and source-delta hash were verified across the native/TSan snapshots. Mac leak detection is disabled; TSan uses existing suppressions, instrumented Qt 6.8.3 and stock FFTW/librtlsdr. These checks cover the review fixes; the earlier full-app/39-test matrix remains evidence for 6a8baff9, not a rerun at this head.

Maintainer decisions remain open: FM passband UX, the cross-family settings hooks/domain and runtime ownership transition, and dependency integration with #5919. Admission remains one. #5919 was not modified or copied; no radio was opened, no drivers changed, and no merge/auto-merge is requested.

73, Ozy K6OZY · GPT-6 Astra-Ultra

@Ozy311

Ozy311 commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Driver compatibility follow-up in ea7fb980d389ea4f6f23a886f6515fb734d17c2d:

  • Confirmed and fixed an antenna-power side effect: Blog aed0ea19 toggles bias tee on R820T/R828D before returning offset-tuning error -2; the getter still reports tuning offset. Our failed request then compensated against offset=0, skipped that matching getter and could leave DC enabled. The adapter now refuses unsupported R82xx offset changes before the setter. It does not turn external bias tee off or reinterpret the offset control as power control.
  • Moved the existing private USB adapter into a header for C-API injection. The new socket-free rtl_usb_controls_test drives the actual adapter and transaction, covering preserved accepted state, no DC writes on refusal/compensation, supported offset changes, genuine I/O failure and direct-sampling refusal. It has no USB enumeration, driver load or socket peer. Registered only in the existing RTL-enabled test graph; no frozen CI gate expansion.
  • Mac/Nobara source checkouts are clean Blog aed0ea19; Windows DLL SHA256 0fa7f8bf4b7e07918a0a9e98228007cccac3109dbaac7497e116e416135d58a5 matches the vendor V1.4.0 archive (archive SHA256 verified). This is exact provenance for the audited artifacts, not a header/linkage-only inference. Upstream comparison: 797f8143.
  • Native Mac/Windows/Nobara each passed adapter, capture transaction and capture worker tests. Mac ASan/UBSan passed those three, with leak detection disabled. The isolated guard-removal mutant fails the antenna-DC regression; seven mechanical checks pass. Windows initially exposed a missing FFTW header include in the new test target; the target now consumes both imported libraries' include properties and the final run passes. No unrelated matrix rerun; prior TSan/full-app evidence remains bound to its earlier heads.
  • Isolated exact-source probes confirm Blog forced-bias EEPROM behavior and mode-0 auto-direct sampling. HF/V4 policy needs a separate device-specific decision; exact readback remains enforced. Added those prerequisites to the runtime document and saved receive runbook. No RTL device was opened, no GPIO/EEPROM/driver state changed, and Nigel's unspecified GATE issue was not reproduced.

All seven GitHub checks passed on preceding head 53b364fe; CI for this new head is pending. Ready status, one-receiver admission, maintainer architecture/UX and hardware qualification gates remain unchanged.

73, Ozy K6OZY · GPT-6 Astra-Ultra

@Ozy311

Ozy311 commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Final validation and operator development-build delivery completed for signed, GitHub-Verified head d42619f107cd9a746bd527b2c5df42f60ca192e2.

  • Native Mac, Nobara and Windows full RTL ON/OFF builds passed the affected 23 ON / 12 OFF tests on each platform. Mac ASan/UBSan passed 8 tests; instrumented-Qt TSan passed 6 plus 3 further full-model repetitions. Three isolated guard mutations were caught. Detailed coverage and sanitizer limitations are in the updated PR body.
  • A self-contained arm64 Mac ZIP was delivered locally: 87 Mach-O objects audited, no unresolved non-system dependencies, minimum macOS 26.0, ad-hoc development signed and not notarized. Deep strict signature checks and native Cocoa startup passed after fresh ZIP extraction in a clean environment through the actual dedicated shortcut. ZIP SHA256: 41caef4ab55b4c15c6ec6e1ae8c81e7f8b57aeb6bf7a4b986d60622150f4d7c5.
  • Nobara received a persistent same-revision binary, matching RTL/libusb runtime, separate settings profile and dedicated launcher. Artifact hash, runtime linkage, direct startup and actual launcher startup were verified. Binary SHA256: 719f2a341a0bb8d52942738443e34b6a6f0f793ce148ff578a5c1e62d2acaa3b.
  • Both startup probes verified authenticated bridge identity against the exact child PID, refused tokenless access, had TX automation disabled and remained disconnected/idle. Each probe stopped only its own child. These are startup/offline results, not USB/RF reception or multi-receiver qualification. Production admission remains one; ASR/RADE/DFNR are unavailable in these operator builds.

All 3,548 final source-manifest files were rechecked and the worktree is clean. The existing Mac/stable installations and active sessions were preserved. The operator will perform live FM/FM-N reception testing. Required human review, the documented maintainer decisions, and live qualification remain open; no merge or auto-merge was performed.

73, Ozy K6OZY · GPT-6 Astra

@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.

Review at d42619f1 — executed, not just read

The earlier agent review (at 6a8baff9) was reasoned from source with no build. This pass builds the PR head and runs it, attacks the newest commit (d42619f, which touches the shared SliceModel/RadioModel every backend uses), and checks the result against the RFC #5468 approval conditions.

Environment: Ubuntu 24.04, GCC 13, Qt 6.8.3 (aqtinstall), librtlsdr 2.0.1. That is the CI image's toolchain, and not a combination in the PR's own validation matrix (its Linux leg was GCC 16.2.1 / Qt 6.11.1), so these results reproduce the author's independently.

Issue fit

This implements the M1 receive runtime of RFC #5468, which was approved conditionally on 2026-09-07. On the conditions that bear on M1:

Condition At this head
1 Persistence: one RtlSlices owner, schema v1, USB index never laundered into radioId, fitting-subset restore Present. rtl_runtime_settings_test passes, and covers pre-readback refusal, newer-schema refusal, unknown-field preservation and index-never-an-ID
2 Audio at 48 kHz with no 24 kHz interim; the four 24000 literals Met, by the A-series before this PR. Three literals are gone; the fourth (AudioEngine.cpp:1900) is the CW sidetone→decoder path, not RX. This PR adds the first production 48 kHz PcmFrame producer (RtlSdrBackend.cpp:1004); git grep finds none on base. The existing audio_engine_rates_test (35 references to 48 kHz ingest) covers the consumer side. I ran it and it passes
6 Capability fields documented RtlSlices and the runtime clientSettingsDomains transfer are both in radio-capabilities-map.md
Process: "keep the existing WFM path working" WFM and non-FM modes stay on the exclusive legacy 24 kHz DDC

Conditions 3, 4, 7 and 8 are release gates, and the body says plainly that they are not met here. I agree; admission stays at kQualifiedReceiverCapacity = 1.

Scope

File / group Claimed? Verdict
src/core/backends/rtl/** (capture transaction, pipeline, extractor, mixer, registry, worker, DDC, USB adapter) Yes In scope
RtlSdrUsbDevice.h, R82xx bias-tee refusal (ea7fb980) Yes, in a follow-up comment In scope. A safety fix found during the work, disclosed with provenance
IRadioBackend.h, SliceDelta.h (ReceiveControlPolicy, configureSettingsScope, storeOperatingState), RadioCapabilities.h (RtlSlices) Yes, and flagged by the author for ratification New cross-family surface. Maintainer decision, see below
SliceModel.{h,cpp}, RadioModel.{h,cpp} (d42619f) Partly Rewires Flex's slice-mode intent path, which the body does not say. See the inline comment on RadioModel.cpp:10146
WdspChannel.*, third_party/wdsp/** + AETHERSDR-PATCHES.md Yes In scope. Additive and documented
docs/rtl-m1-runtime.md, caps map Yes In scope
tests/**, tests.cmake Yes In scope. Preflight: no added or modified test binds, listens or connects. RtlInjectedDevice.h injects at the librtlsdr C-API seam, which is the injected-transport layer AGENTS.md's Test-layer boundary prescribes
CHANGELOG.md Not touched Correct

The merge base is one commit behind main, the PR merges cleanly, and the only overlapping file is tests.cmake. CI: all 7 checks are green on this head, including Linux build.

Every finding from the previous review is resolved at this head

The createSlice() disengaged-optional dereference is guarded (RtlSdrBackend.cpp:479). RtlSlices is ordered after Cw. The caps map is updated. cancelPending() is removed (the 31 remaining hits are unrelated classes). The mixer configure() result is handled. Monitor bounds are validated before submission. The drop, late-frame and rejection counters reach the RTL health snapshot.

Blockers

None found.

Maintainer decisions still open (none are defects)

  1. The FM mode-transition passband resize to ±8 kHz (docs/rtl-m1-runtime.md). This changes an existing control's behaviour, so under GOVERNANCE.md it is a UX ruling and not a reviewer's call. The author flagged it rather than self-certifying it.
  2. The new cross-family seam: receiveControlPolicy(), configureSettingsScope() / storeOperatingState(), the RtlSlices domain, and clientSettingsDomains changing at runtime. storeOperatingState reverses the header's former rule that "the backend never touches the settings store". The reversal is legitimate and honestly commented, but it should be ratified rather than inherited.
  3. Whether d42619f ships here or separately. The RFC approval asked for exactly this split: "Keep PRs one-feature-and-its-tests rather than one-milestone — the foundation stage in particular will want splitting between the neutral slice lifecycle seam and the shared-window policy, since the former touches Flex behavior and deserves its own regression review." d42619f is that seam, and it rewires Flex's mode path. My probe below shows the rewire is equivalent, so the split is now a process choice rather than a correctness requirement. Either way, the probe (or something like it) should land as a registered test, because today nothing in the suite pins that path.
  4. Integration order with #5919, as the body states.

Nits (non-blocking)

  • The unconditional foreign-thread guard exits silently. Inline comment on SliceModel.cpp:117.
  • A stale private contract, unreachable today. RadioModel.h:1665 still says dispatchPanCenterBandwidth "advances the model only when the send actually happened." It now sends, then returns false, for Confirmed backends. The profile-load flush at RadioModel.cpp:6803 counts that false as voided. RTL declares hasProfiles = false, so the hold never engages and the flush never runs on RTL. But the first Confirmed backend that declares profiles will log real sends as voided. A sentence on the header doc would close it. Inline comment on RadioModel.cpp:6649.

Verified by execution

What Result
The PR's 11 affected targets (rtl_*, wdsp_allocation_scope_test) 11/11 pass
Shared-path regression: backend_slice_lifecycle_test (non-Flex, no duplicate intent bindings), audio_engine_rates_test (48 kHz ingest), memory_import_test 3/3 pass
Flex rewire probe. A throwaway, socket-free test that builds the real FlexBackend through rebuildBackendForTest("flex"), materialises slice 0 through handleSliceStatus, and captures only seam dispatches 8/8 pass. setMode(LSB) produces exactly one slice set 0 mode=LSB; repeated status adds no duplicate binding; Flex frequency and filter edits stay off the seam; Flex stays optimistic
M1 mutant: drop the Flex mode branch at RadioModel.cpp:10147 Probe fails (2 checks). The probe is sensitive
M2 mutant: RTL setFrequency publishes optimistically again rtl_model_acceptance_test fails ("pending sparse receiver edits do not change observed getters" and three more). Confirms the author's mutation claim
M3 mutant: remove the R820T/R828D offset guard rtl_usb_controls_test fails ("refused offset and compensation never enable antenna DC"). The bias-tee safety test genuinely guards

Running app, demo simulator only. I built the full app at this head and launched it with an isolated settings directory, AETHER_AUTOMATION_NO_TX=1, and connect local serial DEMO-0001. get radio confirmed "AetherSDR Demo", family sim; there was no live hardware. Sim takes the Optimistic path that d42619f rewired. Driving slice 0 through the GUI, each control landed in the model immediately: tune 14.2345 (asserted), mode LSB, filter -2700/-200, AF gain 73, Audio pan 20, Slice audio mute on. The app log shows zero warnings, criticals or asserts. So the "other backends retain optimistic publication" claim holds end to end in the running GUI, not only at model level.

What I tried to break, and could not

  • The foreign-thread guard as a cross-backend regression. It is unconditional, so it applies to Flex, HL2, Icom and ANAN too. I looked for any worker-thread object that calls the eight guarded setters directly. The external controllers (FlexControl, MIDI, HID, serial, dial on m_extCtrlThread) all reach SliceModel through queued lambdas with MainWindow as context. The AudioEngine, Hl2RxDsp and AnanRxDsp hits are setMode on WDSP and gate objects, not on SliceModel. I found no live off-thread caller, so the guard changes nothing today; hence a nit and not a blocker.
  • A disengaged confirmed() at RtlSdrBackend.cpp:945 after a failed first capture. Failed is only ever Restored, and complete() refuses Restored unless work.before is engaged; endSession() bumps the session, so stale completions are Ignored. It is safe by invariant, although the invariant isn't written down next to the dereference.
  • 48 kHz PCM reaching a 24 kHz-assuming consumer. feedPcmFrame → resetMainPcmState(rate) prepares every RX DSP stage per rate, and post-DSP taps carry scopeSampleRate explicitly. The two remaining legacyStereo24() consumers that could see RTL frames are both fed from Flex's daxPcmReady, which RTL never emits. legacyStereo24() refuses rather than mislabels in any case.
  • The bias-tee guard on the first apply of a session, with nothing confirmed yet and a restored setting requesting offset. The guard sits inside the per-control write and compares against the live getter, so it refuses any R82xx offset change, the first one included.
  • dispatchPanCenterBandwidth returning false confusing a GUI caller into reverting. The only caller that reads it as failure is the profile-load flush, which RTL cannot reach (see the nit).

Could not verify

Anything that needs an RTL dongle: live reception, the integrated 1/2/4-receiver workload and thermal soak, cold start, and aarch64. The demo simulator cannot reach RTL-specific behaviour. The body already lists these as open, and they are RFC release gates rather than M1 merge criteria.


Review run by Claude Code under @ten9876's account using the repo's /pr-review skill. Scratch probe and mutants were run in an isolated worktree and nothing was pushed.


Generated by Claude Code

Comment thread src/models/RadioModel.cpp Outdated
Comment thread src/models/SliceModel.cpp Outdated
Comment thread src/models/RadioModel.cpp

ten9876 commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Maintainer rulings on the four open decisions

Thanks, @Ozy311. This one was straightforward to decide because the body named its own open questions instead of burying them.

1. FM/FMN mode-transition resize to ±8 kHz: approved. A 48 kHz FM graph can't carry an inherited wide or sideband passband, so the resize is forced by the graph, not a preference. Ordinary filter edits and stored restores staying unresized is the right boundary.

2. The cross-family seam: ratified. Here's what I checked before ratifying. Most of it follows existing practice:

  • Backends already own feature documents. Hl2FreqCal::savePpb and AnanDroopCalibrator write through scope.featureExact, and Hl2Settings, AnanSettings and IcomSettings are backend-owned too. The old "the backend never touches the settings store" line only ever described the generic OperatingState write.
  • capabilitiesChanged at runtime is established. ANAN, Flex, HL2, Icom and Sim all emit it.
  • Waiting for the backend before publishing isn't new either. Non-Flex pan bandwidth already waits for the backend's report, and Icom's slaved scope re-asserts its own geometry.

Two pieces are genuinely new: storeOperatingState (a backend taking over the generic OperatingState write) and swapping clientSettingsDomains mid-session. I'm ratifying both as RTL-owned, the way the caps map already records them. If a second backend wants either, that PR should show why before we treat it as a general pattern. receiveControlPolicy() defaulting to Optimistic for every existing backend is what makes this safe to land.

3. Scope: this stays a single PR. No split. That means this PR carries the seam that rewires Flex's slice-mode path, so it also has to carry the regression test for that path (see below).

4. Merge order: this PR first, then the #5892 → #5907 → #5919 stack rebases onto it. A trial merge of #5919 against this head gives 6 content conflicts, about 365 lines, all in the shared seam (RadioModel.cpp 213, SliceModel.h 46, IRadioBackend.h 44, RtlSdrBackend.cpp 26, RadioModel.h 23, SliceModel.cpp 13). The two PRs independently built the same guards: off-thread refusal, retired-object / same-ID reuse, and a connected gate. Reasons for this order:

  • This PR is green on all 7 checks and reviewed; aetherd: consolidate slice receive-control routing #5919 is a draft waiting on two unmerged prerequisites.
  • rtl_model_acceptance_test is a proven pin for confirmed-publication semantics. Re-enabling optimistic publication makes it fail, so a rebase that drops them will be caught.
  • Your body already states the reconciliation terms: accepted-state semantics and sparse stable IDs survive, and typed dispatch is not acknowledgment.

One thing needed before merge: pin the Flex mode path

d42619f removes the dedicated Flex modeChangeRequested binding in handleSliceStatus and reroutes it through wireSliceAudioIntentsToBackend. The review showed the reroute is equivalent, but nothing in the suite pins it. #5919's rebase will rewrite exactly this code, so the pin should land here, before that happens.

The probe from the review works as a drop-in, socket-free test. It builds the real FlexBackend through rebuildBackendForTest("flex") and captures only seam dispatches. It passed 8/8 at d42619f, and with the Flex branch at RadioModel.cpp:10147 mutated to a no-op it fails two checks, so it's not self-confirming.

tests/flex_slice_mode_intent_test.cpp
// The Flex slice-mode intent reaches FlexBackend exactly once, and Flex
// frequency/filter edits do NOT also go through the neutral seam verbs (they
// ride SliceModel::commandReady). The sink sees ONLY seam dispatches
// (FlexBackend::sendSlice). Pins the binding d42619f moved from
// handleSliceStatus into wireSliceAudioIntentsToBackend (#5924).
#include "models/RadioModel.h"
#include "models/SliceModel.h"
#include "core/backends/flex/FlexBackend.h"
#include "TestSettingsProfile.h"
#include <QCoreApplication>
#include <cstdio>
using namespace AetherSDR;
static int g_fail = 0;
static void check(bool ok, const char* m) { std::printf("%s  %s\n", ok ? "PASS" : "FAIL", m); if (!ok) ++g_fail; }
int main(int argc, char** argv)
{
    QCoreApplication app(argc, argv);
    TestSettingsProfile profile(QStringLiteral("flex-slice-mode-intent"));
    RadioModel model;
    check(model.rebuildBackendForTest(QStringLiteral("flex")), "real FlexBackend built via production family switch");
    auto* flex = dynamic_cast<FlexBackend*>(model.backend());
    check(flex != nullptr, "backend() is FlexBackend");
    if (!flex) return 1;
    QStringList seam;
    flex->setSliceCommandSink([&](const QString& c) { seam << c; });

    QMap<QString, QString> kvs;
    kvs["in_use"] = "1"; kvs["RF_frequency"] = "14.200000"; kvs["mode"] = "USB"; kvs["active"] = "1";
    model.handleSliceStatusForTest(0, kvs, false);
    SliceModel* s = model.slice(0);
    check(s != nullptr, "Flex slice 0 materialised from status (handleSliceStatus path)");
    if (!s) return 1;

    seam.clear();
    s->setMode(QStringLiteral("LSB"));
    check(seam.filter(QStringLiteral("slice set 0 mode=LSB")).size() == 1, "one setMode -> exactly one Flex mode dispatch");
    check(s->mode() == QLatin1String("LSB"), "Flex stays optimistic: model mode updated immediately");

    model.handleSliceStatusForTest(0, kvs, false);   // repeated status
    seam.clear();
    s->setMode(QStringLiteral("CW"));
    check(seam.filter(QStringLiteral("mode=CW")).size() == 1, "repeated status does not add a duplicate mode binding");

    seam.clear();
    s->setFrequency(14.210);
    s->setFilterWidth(100, 2800);
    check(seam.isEmpty(), "Flex freq/filter edits do not ALSO dispatch through the neutral seam");
    check(qFuzzyCompare(s->frequency(), 14.210), "Flex frequency stays optimistic");

    return g_fail ? 1 : 0;
}
add_executable(flex_slice_mode_intent_test tests/flex_slice_mode_intent_test.cpp)
target_include_directories(flex_slice_mode_intent_test PRIVATE src tests)
target_link_libraries(flex_slice_mode_intent_test PRIVATE aethercore Qt6::Core Qt6::Test)
add_test(NAME flex_slice_mode_intent_test COMMAND flex_slice_mode_intent_test)

It sits outside the RTL block, so it runs on RTL-OFF builds too. Adjust the tests.cmake shape to whatever the settings-consumer contract needs.

With that test in, this is clear to merge from my side. The two review nits (logging the off-thread refusal, and the stale RadioModel.h:1665 doc) are welcome but won't block it.

— Jeremy (KK7GWY)

Drafted with Claude Code at my direction.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 16 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 12
Files with findings: 4
Total findings : 16

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

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 #5468.
@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 16 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 13
Files with findings: 4
Total findings : 16

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@Ozy311 Ozy311 changed the title RTL M1: transactional capture and prepared 48 kHz FM runtime RTL M1: transactional receive runtime, DC placement and independent viewport Sep 23, 2026
@Ozy311

Ozy311 commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Implemented the requested Flex mode-path regression in signed commit 800dfebd1259e3b50fa6a86a203dfd2c04d0b07f. It is unconditional (RTL ON/OFF), uses the real Flex backend through the production family switch, isolates settings before constructing the application, and asserts the exact single seam command. It passed on Nobara in both native configurations and with instrumented Qt under TSan. Removing only the Flex mode-dispatch branch in an isolated object/library made the two mode checks fail; the unchanged control passed before and after. The working source was never mutated.

The body and runtime/capability docs now reflect your ruling: the earlier publication/settings seam is ratified as RTL-owned, a second backend must justify settings takeover separately, the FM transition resize is approved, and this PR precedes the #5892 → #5907 → #5919 rebases.

This push also includes the separately requested FM/FM-N DC-placement and independent viewport work in 19e85176. Capture displacement preserves receiver RF and production NCO; pan/zoom crops real FFT bins without retuning; typed receiver/view intent waits for acceptance together. The new placement policy, neutral intents/capability and concrete UI action remain explicitly pending your ratification. Production admission remains one; WFM stereo/192 kHz is deferred.

Final head validation: full Nobara RTL-ON app + 28/28 affected tests; full RTL-OFF app + 16/16; 11 TSan targets + three additional model repetitions; three focused ASan/UBSan targets with unchanged source hashes; four demonstrated mutations. The new package and activated dedicated launcher passed isolated offscreen startup/linkage checks with the existing detach-enabled RTL runtime; 41fc9314 remains available for rollback. No live radio/RF/TX, Mac/Windows native rebuild, or merge was performed. The body also discloses an earlier intermittent failure of the unchanged FM-audio test; final runs passed, but its root cause remains unresolved.

73, Ozy K6OZY · GPT-6 Astra-Ultra

@aethersdr-agent

Copy link
Copy Markdown
Contributor

CI: only check-macos failed, at the Build step

Thanks for this one — it's a big, carefully-staged piece of work, and the failure looks narrow rather than structural.

Check runs for 800dfebd:

Job Result
build (Linux) ✅ success
check-windows ✅ success
check-macos ❌ failure — step Build
Static checks / Sanitizer configures / CodeQL ×2 ✅ success

Every step before Build passed (Configure, the GPU-spectrum assertion, Opus), and everything after it is skipped. So this is a compile/link failure in cmake --build build, not an environment or packaging problem.

Up front, so you don't chase a ghost: I could not retrieve the compiler output. The check-runs API I have access to returns step-level status only, and I'm not authenticated for raw log download — so I am not quoting an error message, because I don't have one. The fastest way to the actual line is:

gh run view 35835026792 --repo aethersdr/AetherSDR --log-failed

or the job log directly: https://github.com/aethersdr/AetherSDR/actions/runs/35835026792/job/107096297778

What I can do is narrow where to look, from your diff and the workflow files.

Why the two green jobs don't clear this

The first instinct is "RTL code only builds on macOS" — I checked, and that's not it. .github/docker/Dockerfile:37 installs librtlsdr-dev, so the Linux job configures AETHER_BACKEND_RTL on too and compiles RtlSdrBackend.cpp, RtlSdrWorker.cpp, RtlSdrDdc.cpp and the if(AETHER_BACKEND_RTL) test targets. It built them clean.

check-windows is a different story and is worth knowing about generally — ci.yml:758 builds a target subset:

cmake --build build -j ... --target AetherSDR aether-dv-waveform thumbdv_queue_test \
      dstar_model_test digital_voice_waveform_process_test cross_needle_meter_test

None of your ~20 new test TUs are in that list, so its green says nothing about them. Linux (ci.yml:274) and macOS (ci.yml:1133) are the two full-tree builds.

So the real delta between the job that failed and the job that passed the same sources is:

  • Apple clang + libc++ vs GCC + libstdc++
  • -DCMAKE_BUILD_TYPE=Release vs Linux's RelWithDebInfo
  • macOS additionally configures -DREQUIRE_KEYCHAIN=ON -DMQTT_TLS=OFF

Worth noting there's no -Werror on these targets (CMakeLists.txt ~3226 sets only -Wall -Wextra -Wpedantic), so this is a hard error, not a warning promoted to one.

Where I'd look first

  1. libc++ transitive-include differences. The usual "Linux and Windows pass, macOS doesn't" cause for new DSP code. I read the includes on all your new RTL files and they're actually disciplined — RtlRfExtractor.cpp pulls <algorithm> <cmath> <cstring> <limits> <numbers> <numeric> explicitly, RtlSquelchGate.h and RtlViewport.h are self-contained. Two spots that lean on transitive includes and are worth a second look: RtlSdrUsbDevice.h uses std::optional, std::int64_t and std::uint32_t but includes only RtlSdrWorker.h and <rtl-sdr.h>; RtlReceivePipeline.h uses the mixer/registry types without <cstdint> or <vector> of its own.

  2. tests/CallbackAllocationProbe.h — this replaces global operator new/new[]/delete and is included by rtl_receive_pipeline_test.cpp and rtl_rf_extractor_test.cpp, both of which link Qt. Replacing global allocation operators is the most platform-sensitive thing in the diff by some distance. Separately from whatever the build error is: the C++17 aligned overloads aren't replaced (operator new(std::size_t, std::align_val_t) and its delete), so any over-aligned allocation in aethercore/WDSP/Qt bypasses your counter and takes libc++'s default path. That's worth closing regardless of this failure.

  3. third_party/wdsp/port/wdsp_port.c — the new static _Thread_local uint64_t g_threadAllocationSequence. I verified the four newly-declared WDSP entry points (SetRXAFMDeviation, SetRXAFMLimGain, SetRXAFMLimRun, SetRXAPanelGain1) are defined upstream in fmd.c and patchpanel.c, so that's not an undefined-symbol link break.

Reproducing the CI compiler locally

If you're on a Mac, this matches the job's configure exactly (ci.yml:1103):

brew install ninja fftw portaudio hidapi librtlsdr autoconf automake libtool
cmake -B build -G Ninja \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_PREFIX_PATH="$QT_ROOT;$(brew --prefix)" \
  -DAETHER_GPU_SPECTRUM=ON -DREQUIRE_KEYCHAIN=ON -DMQTT_TLS=OFF
cmake --build build --target build_opus
cmake --build build

The Release build type and the full-tree (no --target) build are both load-bearing — a subset build is exactly how this class of failure stays hidden locally.

On the review comments

The aethersdr-agent blocker at RtlSdrBackend.cpp:536 (unchecked m_capture.confirmed() dereference on the Invalidated path) is a real correctness issue but is not related to this build break — it compiles fine. Same for the RadioCapabilities.h ordering nit and the RtlReceiverRegistry::cancelPending() dead-addition note. Those are worth addressing on their own merits, not as a fix for CI.

Post the failing lines here once you've pulled the log and I'll take another pass with you.


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

@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 16 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 13
Files with findings: 4
Total findings : 16

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

@Ozy311

Ozy311 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

The compiler output is now retrieved: macOS Build log reports tests/rtl_dc_audio_test.cpp:107:75: error: no member named 'cyl_bessel_j' in namespace 'std'. It is the test's ideal-carrier reference calculation, not the allocation probe or a production RTL compile failure.

Signed test-only commit 8a105f3f777bc7b1e519fce440df403da87f0e73 replaces that call with the NIST DLMF 10.2.2 J0 series, restricted to the fixture's 0 <= beta <= 2.5, evaluated once per measurement. The first omitted term is below 4.51e-36; double rounding is checked separately. A Nobara comparison with the prior libstdc++ reference passed 10,001 evenly spaced arguments over that interval with maximum observed absolute difference zero. The existing test also pins zero and its two modulation indices with fixed reference values and 1e-15 absolute tolerance. Its blind-mean negative control, all 29 generated IQ cases, and all audio criteria remain intact.

Focused Nobara checks: rtl_dc_audio_test, rtl_fm_audio_test, and rtl_capture_transaction_test passed (3/3). A deliberately truncated series failed the two numerical reference checks; the restored 29-case DC-audio control passed. No production source, test registration, CI allow-list or dependencies changed. The installed Nobara 800dfebd application, launcher and profile remain in place for the operator's test. Its earlier 28/16/11 validation counts remain tied to 800dfebd, not relabeled as this test-only head. All seven hosted CI checks passed on this exact head. The completed macOS Build log confirms rtl_dc_audio_test.cpp.o compiled and rtl_dc_audio_test linked. Linux full-tree and the existing Windows target-subset builds also passed. This is hosted compilation evidence; the new RTL test was executed on Nobara, not added to the frozen hosted macOS test gate. No local Mac/Windows build was run.

I also rechecked the feedback against this exact branch:

  • The disengaged capture optional is already guarded in createSlice; the old unchecked post-request path is gone. ClientSettingsDomain::RtlSlices follows Cw, and RtlReceiverRegistry::cancelPending is absent. Jeremy's executed review already recorded these fixes; they are not new unresolved defects at this head.
  • The allocation probe intentionally covers ordinary C++ allocation only. Its header explicitly excludes malloc/realloc, aligned new and private allocators; WDSP's separate guard covers its own C path. The runtime document also limits the claim. This does not prove zero aligned/Qt/private allocation, and I am not broadening that claim or adding allocation-operator replacements to fix an unrelated missing math function.
  • Jeremy's two nonblocking nits remain: foreign-thread setters return silently, and the private pan-dispatch header comment still describes optimistic publication without the Confirmed exception. This bounded test-only change does not resolve either.

The earlier seam/settings and FM-transition rulings remain approved; the new DC/viewport and squelch/recipe decisions still require the stated maintainer review. No merge or hardware operation was performed.

73, Ozy K6OZY · GPT-6 Astra-Ultra

@github-actions

Copy link
Copy Markdown

Accessibility check findings

This PR touches src/gui/ files with 16 accessibility issue(s).
Full details appear as inline annotations on the diff above.

Files scanned : 15
Files with findings: 4
Total findings : 16

See docs/a11y.md for the canonical
Qt patterns (setAccessibleName, QAccessibleValueChangeEvent,
QAccessibleInterface subclasses, etc.). Structural remediation
(keyboard navigation, custom QAccessibleInterface subclasses)
is tracked in #3288.

Findings are warnings only — the build is not blocked.

on8st added a commit to on8st/AetherSDR that referenced this pull request Oct 4, 2026
main gained aethersdr#5924, which adds the fftAverages table to the ClientDisplay
document. Three keep-both conflicts:

- ClientDisplaySettings.h: main's FftAverage, fftAverage, saveFftAverage
  and decodeAverage stay as they are, beside this branch's fftFps and
  dbmRanges members; one private section holds both sides' helpers. The
  class comment now names fftAverages as well.
- MainWindow.h: scheduleClientFftAverageSave beside this branch's four
  declarations.
- MainWindow_Wiring.cpp: scheduleClientFftAverageSave after this branch's
  four functions.

The pending-write keys stay distinct (main's starts with "fft:", this
branch's end in the field name). Touchpoint manifest regenerated: 238,
unchanged by the tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 4, 2026
## 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](#5919 (comment))
is satisfied. [Jeremy's Oct 3
ruling](#5924 (comment))
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](#5919 (comment))):
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 #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 #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 #6107's RTL post-disconnect gate
(`RtlSdrBackend::wireWorker()`, read-error rule-6 check) and its
stranded-worker regression in `rtl_backend_test`, which #5924's squash
`a325fb99` dropped from `main`; adapted to the token-qualified pipeline,
not extended. #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. #5924 is merged, but #5919 **stays
draft** for the [unresolved maintainer
decision](#5919 (comment))
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. #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. #5924 is still open, so
#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. #5924 remains open, so
#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.

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

## Checklist

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

Generated with OpenAI Codex (GPT-6 Astra)

---------

Co-authored-by: Ozy311 <Ozy311@users.noreply.github.com>
Co-authored-by: jensenpat <patjensen@gmail.com>
Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 4, 2026
… VIII. (#6152)

## Problem

**Full Suite on `main` is red.** Run
[37181657060](https://github.com/aethersdr/AetherSDR/actions/runs/37181657060)
on `a325fb99e` (#5924) died at link 4934/4976 with `final link failed:
No space left on device`. The previous commit's run was green, so the
tree is right at the runner's limit.

- **ci.yml's required `build` job builds the same tree.** It only
survives because #5924 gave it a step that frees 18 GB of host
toolchains (84 → 102 GB free). That's about 50 more core-linking tests
of headroom before the PR gate hits the same wall.
- **The TSan lane ran out of disk too.** With the Qt 6.12 `:tsan` image
from #6123 it got far enough to build for the first time, then filled
the runner: `overlay 145G 145G 163M 100%` at link 4438/4854 (run
[37176807431](https://github.com/aethersdr/AetherSDR/actions/runs/37176807431),
#6148).
- **The red Full Suite run filed no issue.** When Build fails, ctest is
skipped, and a skipped step's output reads as `null`. GitHub's
expression `exit != '0'` coerces `null` to 0, so every report step was
skipped.

Underneath all three: ~250 test executables each link a static
`aethercore` and carry their own copy of its debug info.

## Fix

- **`ci.yml` build job + `full-suite.yml`:** configure RelWithDebInfo at
`-g1` (line tables only), via `CMAKE_{C,CXX}_FLAGS_RELWITHDEBINFO`.
Backtraces keep file:line; only debugger-level locals and types are
dropped. Both jobs change together because Full Suite restores the build
job's ccache, so the first `main` run after merge builds cold and the
next is warm again.
- **`full-suite.yml`:** the same host-toolchain mounts and "Free runner
disk" step as ci.yml's build job, and a `df` after the build, including
when it fails.
- **`full-suite.yml`:** a "Report a suite that did not build" step runs
only when Configure or Build failed. It writes the report with a stable
per-step fingerprint, and the artifact and tracking-issue steps also run
on that path. Same pattern as #6136.
- **`sanitizers.yml`:** TSan's `shared_core` goes to `"ON"`, the change
the ASan lane proved in #6136.

## Validation

Measured locally (GCC, this tree, `aethercore` plus four core-linking
tests):

| | `-g` (today) | `-gsplit-dwarf` | **`-g1`** |
|---|---|---|---|
| core-linking test executable | 325 MB | 214 MB | **120 MB** |
| `libaethercore.a` | 496 MB | 165 MB | **114 MB** |

At ~250 core-linking executables, that's roughly 50 GB off a tree that
needs a little over 84 GB.

`actionlint` is clean on `full-suite.yml` and `sanitizers.yml`. It
reports two errors in `ci.yml` (`runner.workspace`, lines 451 and 841),
which were already on `main` and are untouched here.

**On the runner:**
- **Full Suite**, branch run
[37208804048](https://github.com/aethersdr/AetherSDR/actions/runs/37208804048)
on this head, green:
  - Free runner disk: 84 → 102 GB free.
- The build tree used **32 GB** (44 → 76 GB used), leaving 69 GB free.
Before, it needed more than 84 GB.
  - **653/653 tests passed** (13.5 min).
- The build took 46 min at a 52% ccache hit rate, because no cached
`-g1` objects exist yet. The first `main` build after merge saves them,
and later runs are warm again.
- **ci.yml `build`**, the required PR check on this head: green, so the
`-g1` configure works on the build job.
- **Sanitizers**, branch run
[37208806037](https://github.com/aethersdr/AetherSDR/actions/runs/37208806037):
still running at merge time. ASan finished building and is running
tests; TSan is still building on the shared core. The result is reported
on this PR after merge. If TSan then fails, it is a sanitizer-lane
problem, separate from the Full Suite and `build` fixes proven above.

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

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tropo1234 added a commit to tropo1234/AetherSDR that referenced this pull request Oct 4, 2026
Restores frequency, mode, span, tuning step, dBm range and the radio's own
output level across a restart, each gated on a declared capability rather
than a family name.

The radio output level is carried by a new ClientSettingsDomain,
ReceiveOutputLevel, and a typed RestoredRadioState field -- not by a flat
AppSettings key. That answers both review blockers at once: the deciding
test becomes a declared capability instead of a `family == "anan"` compare
in a core/ header, and the level becomes per-radio state, so two G2s on one
profile no longer overwrite each other's. It also removes
core/AudioOutputVolumePolicy.h entirely, and with it the new gui->engine
touchpoint this PR had asked a maintainer to rule on.

The sentinel is -1, not 0. Zero is a level the operator can deliberately
select -- a radio speaker silenced on purpose -- and it is the hardest one
to tell apart from a fault, so it is exactly the value that has to survive
a restart. "Not restored" leaves the backend's own default of 50, which is
what the operator already had.

FFT-averaging persistence is dropped from this branch. aethersdr#5924 landed the
same feature on main while this was open, under the same "fftAverages"
settings key but with the weighted flag inside each row rather than in a
second section. Keeping both would have made one build's saved averaging
unreadable to the other, and main's version is the more complete one
(debounced saves, corrupt-document refusal, wired into the pan-copy paths).
The dBm range stays here; main has no equivalent.

Also in this commit, from the review:
- dbmRange() validates in float, the type it returns. Two ordered doubles
  within one float ulp used to collapse to min == max on the way out -- the
  empty scale the guard exists to refuse, through a check that passed.
- beginRateChange() announces the new span once it is actually running, so
  a zoom followed by a kill or a crash is remembered. Previously only the
  disconnect flush wrote it, so a clean quit was captured and a crash was
  not.
- radio_output_volume_pct is gone from the support bundle. It duplicated
  radio_lineout_gain, which reads the model, and on every other family it
  could only report a 0 that looks like a muted radio to whoever is
  triaging.
- The PC-Audio toggle asks the capability instead of comparing the family
  string, and reads MasterVolume's default as 100 like every other caller
  rather than 50.
- Two whitespace-only hunks reverted.

applyOperatorTuningStep() and the new activeOutputVolumePercent() live in
MainWindow_Controllers.cpp rather than beside applyMasterVolume(), which is
the busiest insertion point in MainWindow.cpp -- four of main's commits in
one day added methods against that same anchor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 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
…#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
ten9876 added a commit that referenced this pull request Oct 7, 2026
… the RTL backend. Principle XI. (#6263)

## Summary

Fixes #6248, and the System Libraries Canary failure on `main`. Both
came from #5973 (`ca4e7cb`).

- **Full Suite: `control_receive_test`.** #5973 turned on
`RtlReceivePipeline::kQualifiedWfmEnabled`, so RTL now declares WFM as a
third adjustable filter mode along with `broadcastFmReceive`. That is
intended; #5973's description says "RTL advertises
`broadcastFmReceive`". The assertion was written by #5924 while the gate
was off and still demanded exactly FM and FMN. It now follows the gate
the way the other RTL tests do:
  - FM and FMN with their ±21.6 kHz edges
- when qualified: WFM with its own ±100 kHz range, and the broadcast-FM
controls
  - when not: no broadcast-FM controls
  - legacy, unqualified WFM is never offered
- **System Libraries Canary: link error.** `rtl_wfm_pipeline_test` is
registered on every build, but its opt-in DDC benchmark calls
`RtlSdrDdc`. That class is compiled only with the RTL backend (librtlsdr
plus fftw3f), which the canary image doesn't install, so the link failed
with 9 undefined references. The benchmark and its header now sit behind
`AETHER_BACKEND_RTL`, as `control_receive_test`'s RTL block already
does. The pipeline checks still run on every build, and asking for the
benchmark on a build without RTL prints that it was skipped.

Test-only change: no production code moves.

## Constitution principle honored

**Principle XI.** `main`'s CI should re-run green on its own merge
commits. This restores the two red workflows.

## Test plan

- [x] **RTL build:** the full `ctest` suite passes, 672/672 including
`control_receive_test`, which failed on `main` before this change.
- [x] **`-DENABLE_RTL=OFF`** (same effect as the canary, which has no
librtlsdr):
  - the entire tree builds with `ninja -k 0` and no link errors
- `rtl_wfm_pipeline_test` reports 0 failures, and with
`AETHER_WFM_BENCHMARK=1` prints "benchmark skipped, this build has no
RTL backend"
- restoring `main`'s `rtl_wfm_pipeline_test.cpp` in that tree reproduces
the canary's 9 undefined `RtlSdrDdc` references
- [x] `check_test_registration` and `check_ci_test_gate` pass.

## Checklist

- [x] Commits are signed
- [x] Test-only change, with no settings, colour or meter changes
- [x] No CHANGELOG entry

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

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
jensenpat pushed a commit that referenced this pull request Oct 10, 2026
…II. (#6254)

Refs #5942

**Hermes-Lite 2 receive audio becomes 12.04 dB quieter, at every slider
position.** The panel does not depend on the mode. The level is measured
in USB and LSB. AM is checked only for the setting surviving a rebuild,
and CW and FM are not measured. That covers the speaker (and the HL2's
codec, which the speaker mix feeds), the per-slice audio sent to TCI,
and the in-app decoders. Today the chain output sits above full scale:
WDSP's panel multiplies by 4.0 after the AGC has limited at about -0.16
dBFS. **No maintainer has ruled on this level change.** It needs a
release note: anyone who pulled the slice AF slider down to about 25 to
stop strong stations distorting can put it back at 100.

## Limits first

- **HL2 only.** ANAN and RTL behave exactly as before. ANAN in every
mode, and RTL outside narrow FM, still run WDSP's 4.0. Those backends
have their own owners, and this PR does not decide for them. RTL narrow
FM already opens the panel at 1.0 (#5924). The new field is not set on
any RTL path, wide FM (#5973) included.
- **Offline only.** The figures below come from a real `Hl2RxDsp` fed
synthetic IQ. No radio, simulator or socket was involved, and nothing
was measured on the air. Step 5 of #5942 (does the slider-25 crossover
hold across LNA gain) is still unrun. This PR fixes the gain structure,
which is provably 12 dB hot. It does not claim to cure any one
listener's distortion.
- **Unity does not promise `|out| <= 1.0`.** Speak, mpeak and the second
bandpass pass all run between the AGC and the panel, as the triage
noted. The test therefore bounds a steady tone, not every signal.
- This is #5942's step 1, plus step 4's test for the HL2 chain only.
Step 2 (the slider moving into `SetRXAPanelGain1`) is not here, because
the triage asked for it as a separate change. Step 3 (`Meter::AgcGain`)
is not here either.
- Measured on one Mac (macOS, Qt 6.12.0).

## What changes

- `WdspChannel::Config` gains `std::optional<double> rxPanelGain`. When
it is unset, `open()` leaves WDSP's `create_rxa()` value of 4.0
untouched, so every existing owner is unaffected. When it is set,
`open()` calls `SetRXAPanelGain1` after the FM block, so a set value
wins over the FM recipe's 1.0 (no caller sets both). `validateConfig()`
refuses a set value that is non-finite, negative, or on a transmit
channel.
- `Hl2RxDsp::buildChannel()` sets it to `Hl2RxDsp::kRxPanelGain = 1.0`,
so every HL2 build, rebuild and rate change opens at unity.
- Two comments in `hl2_rxdsp_unmute_staleness_test` that described the
x4 are updated (its marker is still above its own floor).
`docs/HERMES.md` §AGC notes that its recorded peaks (3.186, 2.664)
include the x4. It also notes that the stages between the AGC and the
panel mean the AGC's ceiling does not bound the output.

## The test: `hl2_rx_panel_gain_test` (socket-free, about 1 s)

| leg | this commit | HL2 without the panel line (= main) |
|---|---|---|
| AGC off, 1500 Hz tone at 0.01: chain gain | **+10.01 dB** (the 10.00
dB fixed gain alone) | +22.05 dB |
| control: the built config re-opened directly | -0.01 dB vs the HL2
chain | -0.01 dB |
| negative control: the same config, panel unset | **+12.03 dB** vs the
HL2 chain | -0.01 dB (fails, as it should) |
| AGC med (39 dB ceiling), tone at 0.1, USB peak | **0.830 (-1.62
dBFS)**, rms 0.587 | 3.318 (+10.42 dBFS) |
| the same, LSB | 0.828 (-1.64 dBFS) | 3.311 (+10.40 dBFS) |
| rebuild to AM at 96 kHz keeps unity | yes | no |

The ratio between the two AGC-med columns is 4.00 (12.04 dB), and the
AGC-off gain moves by the same amount. The negative control takes
`Hl2RxDsp::channelConfig()` and clears only `rxPanelGain`. That isolates
the panel from everything else the HL2 sets.

**Red on mutation** (scratch commits, never pushed; the full suite each
time):

| mutation | result |
|---|---|
| `Hl2RxDsp` does not set `rxPanelGain` (main's HL2 behaviour) | RED:
the gain, negative-control, both AGC-med and both config checks |
| `WdspChannel::open()` ignores `rxPanelGain` (the field is set but
never applied) | RED: the gain, negative-control and both AGC-med
checks, while the config checks pass. So the level legs check the
effect, not the field. |

Full ctest suite on macOS (Qt 6.12.0, offscreen), on `121340c0` (head
`594d96cf`): 654 of 658 pass at -j8 and 655 after the serial rerun. The
three remaining failures are known on clean main on this machine, each
with only its known check lines: `control_receive_test` (#6248),
`peripheral_auth_dialog_test` (#6008) and `unified_title_bar_test`
(#6224). `vkamp_connection_test` (#5254) failed at -j8 and passed
serially. This PR touches none of their files. The figures in the table
above were re-read from `hl2_rx_panel_gain_test` on this head and are
unchanged; the main-column figures and the two mutations are from before
the rebase. The rebase brought in #5973 (RTL wide FM, inside
`WdspChannel`) and #5975 (HL2 CW carrier level, transmit side); neither
touches the panel gain or `Hl2RxDsp`. On the second mutation
`vkamp_connection_test` also failed at -j8 and passed serially (#5254).
The level figures and the mutations below were taken at `e9a0c396`;
`594d96cf` changes comments and one doc line, no executable line.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants