Repository navigation
RTL M1: confirmed receive state, free capture browsing and finer zoom - #5924
Conversation
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.
There was a problem hiding this comment.
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 << 8is inserted aboveCw = 1u << 7(RadioCapabilities.h:629). Values are correct and unique; the ordering just reads as a mistake. Move it belowCw.docs/architecture/radio-capabilities-map.mdis not updated. Line 186 enumeratesclientSettingsDomainsand line 256 still records RTL's row asTuning|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, andrtl_runtime_settings_test.cpp:63already 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). Onlyrtl_receiver_registry_test.cpp:679exercises it. Either wire it (theprepare()-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 afterm_preparedis latched at line 73. Unreachable today becauseRtlCaptureTransaction::submit()already clampsaudioGain/audioPanto 0..100, so this is ordering hygiene, not a live bug.- The mixer's
configure()return value is discarded inRtlReceivePipeline::process(). A rejected configure (e.g. a zerohandle.instance) leaves the mixer running against the previous slot map whilepush()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_settingsActivecheck, so a session that connects and disconnects without ever publishing a capture loses its disconnect-flush write entirely — onmainthat flush went toRadioStateMemory::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'sReservationdestructs while its reusedWdspChannels stay alive. It doesn't:WdspChannel.h:108-128documents 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 thenewCount == 0path for a null-Reservationdereference — theif (prepared->receivers[i]) continue;skip makes*prepared->reservationunreachable in that case. Both attacks failed. - Lifetime of reused receivers across the acquisition/executor boundary. The
shared_ptrcopy atRtlReceiverRegistry.cpp:~300is taken under the executor mutex while the callback holds only raw pointers, destruction is executor-only, andReceiverSpec::operator==includesepoch,handle.instanceandcapture— andsubmitImploverwritesreceivers[i].capturebefore 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()forfirstSample + sizeoverflow, fully-past blocks, partial-past blocks, ring aliasing atposition % kCapacity(stale frames fail theframe.position == m_next + iidentity check and degrade to silence, not to wrong audio), and slot deactivate/reactivate with a matching instance+epoch (!slot.activeforces invalidation). I hand-traced every assertion inrtl_audio_mixer_test.cppagainst the implementation, including the256 + kDeadlineFrames - 1/+ kDeadlineFramesboundary pair and the0.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. RtlRfExtractorlate-join and subspan underflow. I tried to makeinput.subspan(m_startInput - firstSample)underflow by feeding blocks shorter than the lattice period; the contiguity guard (firstSample != m_nextInput→ withdraw) plus them_nextInput <= m_startInputearly return makefirstSample <= m_startInputan 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 (
operationis bumped,tokenretained), that aRestoredresult is validated againstwork.beforerather thanwork.target, thatm_confirmedsurvives a successful compensation, and that a malformed-but-correctly-tokenedAppliedstill invalidates. I did not find a way to publish a superseded state. - The persistence seam.
rtl_runtime_settings_test.cppcovers 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 > 20dB) and thecallbackAllocations == 0claim inrtl_receive_pipeline_test.cppare 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 raisingkQualifiedReceiverCapacityis, correctly, not claimed as met here —kQualifiedReceiverCapacityis still1.
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
|
Review follow-up in
Validation at 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 |
|
Driver compatibility follow-up in
All seven GitHub checks passed on preceding head 73, Ozy K6OZY · GPT-6 Astra-Ultra |
|
Final validation and operator development-build delivery completed for signed, GitHub-Verified head
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
left a comment
There was a problem hiding this comment.
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)
- 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. - The new cross-family seam:
receiveControlPolicy(),configureSettingsScope()/storeOperatingState(), theRtlSlicesdomain, andclientSettingsDomainschanging at runtime.storeOperatingStatereverses 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. - Whether
d42619fships 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."d42619fis 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. - 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:1665still saysdispatchPanCenterBandwidth"advances the model only when the send actually happened." It now sends, then returnsfalse, for Confirmed backends. The profile-load flush atRadioModel.cpp:6803counts thatfalseasvoided. RTL declareshasProfiles = 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 onRadioModel.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 reachSliceModelthrough queued lambdas withMainWindowas context. TheAudioEngine,Hl2RxDspandAnanRxDsphits aresetModeon WDSP and gate objects, not onSliceModel. I found no live off-thread caller, so the guard changes nothing today; hence a nit and not a blocker. - A disengaged
confirmed()atRtlSdrBackend.cpp:945after a failed first capture.Failedis only everRestored, andcomplete()refusesRestoredunlesswork.beforeis engaged;endSession()bumps the session, so stale completions areIgnored. 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 carryscopeSampleRateexplicitly. The two remaininglegacyStereo24()consumers that could see RTL frames are both fed from Flex'sdaxPcmReady, 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.
dispatchPanCenterBandwidthreturningfalseconfusing 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
Maintainer rulings on the four open decisionsThanks, @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:
Two pieces are genuinely new: 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 (
One thing needed before merge: pin the Flex mode path
The probe from the review works as a drop-in, socket-free test. It builds the real
|
…ch. Principle XI.
Accessibility check findingsThis PR touches
See 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.
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
|
Implemented the requested Flex mode-path regression in signed commit 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 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; 73, Ozy K6OZY · GPT-6 Astra-Ultra |
CI: only
|
| 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=Releasevs Linux'sRelWithDebInfo- 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
-
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.cpppulls<algorithm> <cmath> <cstring> <limits> <numbers> <numeric>explicitly,RtlSquelchGate.handRtlViewport.hare self-contained. Two spots that lean on transitive includes and are worth a second look:RtlSdrUsbDevice.husesstd::optional,std::int64_tandstd::uint32_tbut includes onlyRtlSdrWorker.hand<rtl-sdr.h>;RtlReceivePipeline.huses the mixer/registry types without<cstdint>or<vector>of its own. -
tests/CallbackAllocationProbe.h— this replaces globaloperator new/new[]/deleteand is included byrtl_receive_pipeline_test.cppandrtl_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 itsdelete), so any over-aligned allocation inaethercore/WDSP/Qt bypasses your counter and takes libc++'s default path. That's worth closing regardless of this failure. -
third_party/wdsp/port/wdsp_port.c— the newstatic _Thread_local uint64_t g_threadAllocationSequence. I verified the four newly-declared WDSP entry points (SetRXAFMDeviation,SetRXAFMLimGain,SetRXAFMLimRun,SetRXAPanelGain1) are defined upstream infmd.candpatchpanel.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
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
|
The compiler output is now retrieved: macOS Build log reports Signed test-only commit Focused Nobara checks: I also rechecked the feedback against this exact branch:
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 |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
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>
## 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>
… 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>
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>
…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>
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>
…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>
…#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
… 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>
…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>
…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>
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
efc358a1native 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
7601593fbuild 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) compileRadioSetupDialog.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.
SquelchLevelScale'sunitandmodesExclusivefields;requestReceiveTune(view),recenterReceiveCaptureandReceiveCapturePlacement; and theBackendPanAveragingpersistence/description fields. Averaging persistence and coherent 3D presentation are part of that ratification.RadioCapabilities::squelchLevelScale, not a second record.unitcarries RTL's dBFS/bin, andmodesExclusivemarks the record as the receiver's own gate.HL2 FFT-FPS/dBm persistence coordination stays separate, with sibling ClientDisplay tables preserved.
New gui/ touchpoints (ratified)
core/backends/RadioCapabilities.hsrc/gui/ModeFilterPresets.cppReceiveFilterControl/SquelchLevelScalerecordsmodels/PanadapterModel.hsrc/gui/PanFrameGuard.hmodels/RadioModel.hsrc/gui/ReceiveCaptureAction.h,src/gui/RtlReceiverSettingsWidget.cppmodels/SliceModel.hsrc/gui/MainWindowHelpers.cpprequestSlicePanCenterresolves the slice's pan and sends confirmed backends a Center tune instead of a pan requestEffects on other radios
ModeFilters::squelchAvailableInMode). AsquelchLevelScalewithmodesExclusive(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 leavesmodesExclusivefalse, so its SQL line, Auto SQL and availability are unchanged.ModeFilters::fmFilterAdjustable. Declared FM rows are authoritative (RTL). A radio-owned filter with no FM row refuses FM edits (Flex, per FlexLibSlice.cs). HL2, ANAN, Icom and the Demo keep their existing adjustable passband.SpectrumWidget::sliceDragCancelled).SliceModelsetters are refused and logged.CI
The Linux
buildjob 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'sstartCapture()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 inrtl_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)