Repository navigation
fix(squelch): SQL line and Auto SQL from a backend-published scale; HL2 amsq gate referred to the LNA. Principle VIII. - #6127
Conversation
…shed scale; refer the HL2 amsq gate to the LNA. Principle VIII. The panadapter SQL line and Auto SQL assumed every radio's squelch level is Flex's -160 + level dBm. On the HL2 the amsq gate is -140 + 0.7*level in raw dBFS, fixed against a pan axis that carries the LNA offset, so the line sat up to tens of dB from the gate and Auto SQL held no margin. - WdspChannel::setSquelch takes a caller-supplied offset for the amsq map. Hl2DbReference::levelSquelchOffsetDb() refers it to the LNA, anchored at -12 dB where on8st's measured brackets fit (supported, not established), so the gate is -108 + 0.7*L - lnaOffsetDb() and tracks the LNA dB for dB. Hl2Backend re-pushes it on every LNA change. - RadioCapabilities::squelchLevelScale (optional record: offset, dB/step, modes, autoSquelch). Flex -160 + 1*L everywhere with Auto SQL; HL2 -118.98 + 0.7*L on the pan axis in AM/SAM/DSB/LSB/USB, no Auto SQL (amsq reads passband magnitude before AGC, the pan per-bin noise). Icom, demo, ANAN, RTL absent. The +6.02 dB carrier term is pinned on both sides. - SpectrumWidget draws and computes from the record; absent or unlisted mode means no line and no Auto SQL. RxApplet's cycle skips Auto where it is unavailable, a slice in Auto drops to Manual, and the reason is on the SQL button's accessible description (mirrored on the VFO). Fixes #6092. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
…TL-SDR. Principle II. Per the maintainer's ruling on #6127: a family with no measured squelch map of its own publishes legacyDbmSquelchScale() (-160 + 1*level, every mode, Auto SQL), so its SQL line and Auto SQL behave exactly as before. Only the HL2 carries its own map. squelch_level_scale_test pins each family. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
There was a problem hiding this comment.
Issue fit
Yes. #6092's three defects map cleanly onto the diff: the −160 + level line scale (SpectrumWidget::drawSquelchLine now reads thresholdDb()), the Auto SQL conversion (levelForThresholdDb() + the autoSquelchAvailable() early return), and the missing capability gate (autoSquelchShouldRunOnSpectrum + syncSquelchScaleToUi). The shape is the one the maintainer ruled on in the thread ("The plan (my decision): 1. a backend-published squelch-scale record… 2. refer the HL2 amsq map to the LNA… 3. Auto SQL stays hidden on the HL2"), with on8st's 2026-10-03 re-anchoring at −12 dB folded in. I checked the formula against his correction: −108 + 0.7L − lnaOffsetDb() and the implemented −140 + 0.7L + (G + 12) are the same function (−128 + 0.7L + G), since kReferenceLnaGainDb == kDefaultLnaGainDb == 20. The test asserts the first form and the code computes the second, which is the right way round.
Coverage is at the right layer — capabilities and DSP mapping, socket-free CTest. No socket test is added or modified; nothing in the diff touches QTcpServer/QUdpSocket/QLocalServer/bind/listen/connectToHost, and no fake radio peer appears.
Scope
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
core/backends/SquelchLevelScale.h (new) |
the record + squelchScaleForMode / autoSquelchAvailable predicates |
yes | in scope; new public surface, pre-ruled in the thread |
RadioCapabilities.h |
std::optional<SquelchLevelScale> squelchLevelScale |
yes | in scope |
flex/FlexBackend.cpp, hl2/Hl2Backend.cpp |
publish the record | yes | in scope |
hl2/Hl2DbReference.h, Hl2RxDsp.{h,cpp}, dsp/WdspChannel.{h,cpp} |
the LNA referral and its plumbing | yes | in scope |
hl2/Hl2Spectrum.h |
kToneGainDb constant |
yes | in scope — names existing behaviour, no code change |
gui/SpectrumWidget.{h,cpp}, MainWindow*.{h,cpp}, RxApplet.{h,cpp}, VfoWidget.cpp |
read the record; skip/drop Auto; a11y text | yes | in scope |
docs/architecture/* (3 files) |
caps-map row, touchpoint manifest + tags | yes | in scope; manifest is generated, 233→234 is consistent |
tests/* (5 files) + tests.cmake |
new target, signature updates | yes | in scope |
Nothing in the diff is unexplained by #6092 or its thread. No CHANGELOG.md entry (correct). No deleted guard — the only - lines of consequence are the removed kSqlMinDbm constants and the inlined SQL tooltip, both replaced. The three disclosed behaviour changes (Icom and Demo lose the line and Auto; HL2 manual squelch gates 32 dB higher at the default LNA) are all consequences of the ruled shape, not smuggled preference: the issue and the triage comment both establish that Icom's 0..255 register has no published dB mapping, so the line there was drawn on a scale that never applied.
Governance. New capability field arrives with the full trio (RadioCapabilities + radio-capabilities-map.md row + squelch_level_scale_test gating test), per AGENTS.md. New universal touchpoint header follows the NoiseFloorAutoAdjustGate.h precedent, and the justification is in the PR body. I re-ran the gates on the head checkout myself: check_capability_records.py --strict → 71 boolean(s), at the frozen 71 — ok, gen_touchpoint_manifest.py --check → manifest up to date, check_engine_boundary.py --strict → 102 findings / 0 blocking, check_test_registration.py → OK. The body's numbers reproduce.
Blockers
None.
Nits (all non-blocking)
- A non-finite offset is refused, and the refusal is retried forever.
WdspChannel::setSquelchreturnsfalsefor!std::isfinite(levelOffsetDb), butHl2RxDsp::pushSquelchToChannelcannot tell that apart from a transient control-op refusal: it setsm_squelchPending = true, andprocessIqBlock()then retries once per block for the rest of the session. A permanently-invalid argument and "busy, try later" want different answers. Inline comment onWdspChannel.cpp:602. - The unavailability reason can be wrong for a radio that does publish Auto. With no active slice yet (
activeSlice() == nullptr) butisConnected()true,activeSliceSquelchScale()returnsnullopt, and the reason selector falls through!published(false on Flex) ands && …(false) into the detector message — so a connecting Flex can briefly announce "this radio's squelch detector does not read the panadapter's noise floor". Harmless in practice:setActiveSliceInternalre-syncs beforeRxApplet::setSlicerestores intent, which is exactly what the ordering comment protects. Treating!sas permissive (like disconnected) would close it. Inline onMainWindow_Wiring.cpp:1389. - At the shipped LNA default the usable slider span is ~5 steps. From on8st's +20 row and the implemented map, level 50 gates at −73.0 dBFS with his noise top at −73.5 and the carrier above −70 — the test's own 0.5 dB window. Everything below level 50 is under the floor (always open) and above ~55 is above any signal he measured. That is what the measurement says, and it is better than the pre-change behaviour (open below ~85), but the caps-map paragraph would be more useful to an operator if it said where the live range sits at +20. Inline on
Hl2DbReference.h:105. pushEffectiveLnaGain()now issues a secondbeginControlOperation()-taking push per LNA step (including every auto-RF-gain step), andpushSquelchsends even when squelch is off. Same cost class as the adjacentsetAgcpush that was already there, so probably fine — flagging only so it is a decision rather than an accident.recordsPerBackendpins the HL2 mode list inpublishedModeStrings()order, so a harmless reorder there fails a test that only cares about set membership. Inline ontests/squelch_level_scale_test.cpp:126.- Mixed i18n on the same user-visible sentence:
MainWindowbuilds the reason withtr(),RxAppletembeds it inQStringLiteralscaffolding, so a translated reason lands inside an untranslated frame. Pre-existing for the surrounding strings.
What I tried to break
- The arithmetic. Expanded the HL2 record offset by hand:
−140 + (G + 12) + (3 − G) + 6.02→−128.98 + 6.02, LNA-free, matching the test's−125 + kToneGainDband the body's claim. Checked that a +10 dB LNA move leaves the displayed line fixed (gate moves +10 dBFS,offsetDb()moves −10) — the cancellation holds in both the gate and the pan direction, so the line does not walk with RF gain. Recomputed level 50 against all four of on8st's rows (+20 → −73.0, +8 → −85.0, −4 → −97.0, −12 → −105.0) and confirmed each sits inside his brackets, using his lower-bound −70 at +20 as the only thing that row can support. - Flex bit-for-bit equivalence. Walked
levelForThresholdDbagainst the deletedclamp(int(target + 160 + 0.5f), 1, 100)at both ends of the test's sweep, including the negative-steps case wherestatic_cast<int>truncates toward zero rather than rounding up as the comment says — both land on 1, so the comment is loose but the result is identical. - The Auto-drop as a Flex self-race (the attack that would have been a blocker): if
isConnected()could be true whilesquelchLevelScalewas absent on a Flex, every connect would silently drop a restored Auto to Manual andsaveClientSquelchIntent()would persistautoEnabled = falsewith no path back.RadioModel::backendCapabilities()is a live call-through tom_backend->capabilities(), not a cached copy, andsyncSquelchScaleToUiruns afterm_activeSliceIdis assigned and beforem_appletPanel->setSlice(s). No window I can find. - Stale string-based invokers. The
Q_INVOKABLEarity changed, which a wrong-arityinvokeMethodwould fail at runtime rather than at compile time.grep -rn '"setSquelch"' src testson the head checkout returns exactly one hit, the updatedHl2Backend::pushSquelch. Confirmedm_dbRef.setLnaGainDb()runs before the push loop inpushEffectiveLnaGain. - The Kiwi paths. Checked that the new guards sit after the Kiwi branches in
drawSquelchLineandupdateAutoSquelchFromBins, thatautoSquelchShouldRunOnSpectrumstill returns on its Kiwi early-returns before reaching the new&&, and thatRxApplet::autoSqlAvailable()ORs inusingExternalReceiveSquelch()so Kiwi keeps its own Auto. Kiwi is untouched. - The truncated diff. The
wdsp_channel_test.cpphunk is cut off above, so I read the file:runSquelchCarrierThresholdTestexists, is registered at line 3957, and does pin the ±2 dB carrier case at offsets 0 and +32 plus the non-finite refusal at:3607. The "verified on both sides" claim for the +6.02 dB term checks out.
Not verified, and it matters: everything above is reasoned from the code and from on8st's posted brackets — I ran no build, no test and no GUI. CI green means "it compiles." Every ctest step in ci.yml is -R-filtered and none of the filters names a squelch, wdsp-channel or rx-applet test, so the author's local run and the 11 mutation kills are the only evidence those tests pass. And as the body says honestly, the −12 dB anchor and the 1:1 LNA shift are supported, not established — one on-air sweep on an HL2 is what would settle the thing this PR is built on.
Recommendation
Approve with nits — with a maintainer note. The code is careful, the seam is the one that was ruled on, the gates reproduce locally, and the LNA referral is algebraically consistent in both directions. None of the nits break a user. What a maintainer should weigh is not the code: it is that HL2 manual squelch moves 32 dB at the shipped default gain on the strength of bracket fits from one afternoon, with the author saying so plainly. Worth landing and then getting an on-air confirmation of the anchor from @on8st, rather than holding the GUI fix hostage to it. Concrete next step: the isfinite-refusal nit is a two-line change and worth taking before merge; the rest can ride.
Nice work — in particular the mutation table, the honest "supported, not established" framing, and excluding Auto SQL on the HL2 instead of shipping a plausible-looking pan-derived floor.
🤖 aethersdr-agent · cost: $8.2055 · model: claude-opus-5
|
@ten9876 Checked 790ef15 against our rows by reading the source; nothing was built or run. Bad news first.
Level 50 from
Inside all four. The record includes the pan offset ( 73, ON8ST |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
…pping # Conflicts: # docs/architecture/aetherd-touchpoints.md
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Hl2RxDsp keeps the last finite level offset, so a non-finite one is not retried at the IQ block rate as if the channel were busy; the reason selector stays permissive before a slice is active; the HL2 mode-list test checks membership, not order; the SQL button's frame strings go through tr(); and the capabilities map says where the HL2's working squelch range sits at the default LNA gain. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
hl2_rxdsp_squelch_test now drives Hl2Backend: a slice squelch lands on amsq at the current LNA's offset, and setLnaAutoOffsetDb moves the gate dB for dB and back, read on the chain's own thread. Removing the pushSquelch in pushEffectiveLnaGain fails it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
|
Maintainer pass on 8bc6ebc. This covers the points in the bot's review body and in @on8st's 19:49 check that have no inline thread. All four inline threads are answered and resolved. on8st, untested re-push on an LNA change (
The values are read on the chain's own thread. If the on8st, the +20 row carries the anchor. The maintainer took −12 dB as supported, not established, and that is how the PR body and on8st, the line is exact for a steady carrier only. That is the record's stated contract ( Bot body: extra Bot body: mixed i18n. In f77795a, the SQL button's tooltip and accessible-description frames in Bot review predates 33711e2. Icom, the demo, ANAN and RTL-SDR now keep Flex's Local proof at 8bc6ebc: RelWithDebInfo build is clean. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
|
Admin-merged on the maintainer's approval (self-authored PR). Head
|
ten9876
left a comment
There was a problem hiding this comment.
PR #6127 — post-merge review of 1c574109331ed0855331d2eb3c822813533a54ea (merged while this review was running).
Issue fit: The implementation addresses #6092's HL2 scale mismatch: the backend publishes the line's scale, the DSP gate follows LNA gain, and Auto SQL is unavailable where a passband detector cannot use the per-bin pan floor. The issue thread explicitly authorizes this shape; the later maintainer PR comment accepts the new touchpoint and retaining legacy mappings on Icom/demo/ANAN/RTL. Those families' calibration is intentionally not solved here. Hardware confirmation of the −12 dB anchor remains outstanding and is disclosed.
Scope
| File/group | Change | Claimed? | Verdict |
|---|---|---|---|
RadioCapabilities.h, new SquelchLevelScale.h |
Optional record, forward/inverse mapping, mode/Auto predicates | Yes | In scope; new surface has maintainer ruling |
| Flex/Icom/sim/ANAN/RTL backend declarations | Preserve existing line and Auto behavior | Yes | In scope; accepted compatibility choice |
hl2/Hl2Backend.{h,cpp}, Hl2DbReference.h, Hl2RxDsp.{h,cpp} |
LNA referral, queued offset propagation, gain-change re-push, test access | Yes | In scope |
dsp/WdspChannel.{h,cpp}, hl2/Hl2Spectrum.h |
Offset only on level squelch; named existing carrier normalization | Yes | In scope |
MainWindow.{h,cpp}, MainWindow_Wiring.cpp, SpectrumWidget.{h,cpp} |
Publish active-mode scale to rendering and Auto algorithm | Yes | In scope |
RxApplet.{h,cpp}, VfoWidget.cpp |
Skip unavailable Auto, coerce restored Auto, explain availability | Yes | In scope; issue-authorized correction |
Four test sources and tests/tests.cmake |
Mapping, DSP, lifecycle and UI regressions | Yes | In scope |
Three docs/architecture/ files |
Capability map and generated touchpoint/tag entry | Yes | In scope; new universal include explicitly accepted |
All 31 changed files are explained by the issue and accepted follow-up decisions. Commit author dates and messages reveal no unrelated bundled work. No CHANGELOG entry or ratchet weakening. The boundary audit reproduced 102→102 findings (only line-number changes), 70→70 capability booleans and 400→400 above-seam commands; both manifests passed. The new universal touchpoint is the documented 236→237 growth, not a vendor reclassification. No new family-string branch, concrete-backend cast, wire command above the seam, thread or TX path was introduced.
Blockers
None found.
Nits — non-blocking
-
Isolate the newly settings-touching HL2 test.
tests/hl2_rxdsp_squelch_test.cpp:89startsQCoreApplicationwithoutTestSettingsProfile, and the new backend at:194callsAutomationBridgeSettings::txAllowed()during construction. That instantiatesAppSettings, whose constructor creates the normal configuration directory and runs the platform path-migration hook. The generated CTest environment only supplies the WDSP planner/wisdom overrides. WithAETHER_SETTINGS_DIRunset andXDG_CONFIG_HOMEredirected to a fresh scratch directory, I observed the head test createAetherSDR/; the identical probe against the built merge-base test did not create it. This was an isolated probe, not an experiment on the operator's profile; I did not observe settings-value modification on Linux. Add the sameTestSettingsProfileused by the new scale test beforeQCoreApplication, and checkisValid(). This follows the project test-isolation convention and keeps the backend fixture self-contained.Add
#include "TestSettingsProfile.h", then startmainwith:TestSettingsProfile settingsProfile(QStringLiteral("hl2-rxdsp-squelch-test")); if (!settingsProfile.isValid()) { return 1; } QCoreApplication app(argc, argv);
What I tried to break
- Built the exact PR head in an isolated worktree. All four focused socket-free tests passed:
squelch_level_scale_test,hl2_rxdsp_squelch_test,wdsp_channel_test,rx_applet_squelch_reconciliation_test. The GUI test initially hit the host's inherited GTK3 platform theme/display dependency; it passed with offscreen generic/Fusion styling. The preflight covered full test sources and the cold backend constructors, including Flex's inert socket allocation; no test opens/binds/connects a socket or runs a synthetic firmware peer. - Mutation-checked the actual test sources using separate compiled objects so the production build remained unchanged: dropping the line slope caused ten scale-test failures; omitting the WDSP offset failed both WDSP and HL2 tests, including reconfigure and LNA-change assertions; removing the Auto coercion failed
restoredAutoIntentLandsOnManualWhenUnavailable(Actual: 2,Expected Manual: 1). All mutants exited 1. The unmodified DSP test also exercised the ±2 dB AM carrier gate at offsets 0 and 32. - Drove the head's own offscreen simulator over
/tmp/6127-demo-head/bridge.sock, isolated settings,AETHER_AUTOMATION_NO_TX=1.get radioconfirmed"model":"AetherSDR Demo","serial":"DEMO-0001","transmitting":false. Clicking the real RxApplet button produced(squelch,level)false/20 → true/20 → true/66 → false/20; manual level stayed 20. Auto remained on at level 66 after disconnect/reconnect and USB→AM→USB. The accessible description retained all three modes. Only this owned instance was driven and it was closed. - Built exact merge base
3e4b14e2eand ran the identical demo script on its own/tmp/6127-demo-base/bridge.sock. All eight state snapshots matched the head, including manual-cache preservation, reconnect and the mode round trip. Re-launched both builds with their respective existing isolated profiles: each starts Off/20 and then produces the same sequence. Thus this demonstrates unchanged restart behavior, not cross-process Auto persistence. All four runs were explicitly compared by assertions. Both owned demo instances were closed after every run; the merge-base HL2 and RxApplet tests passed as well. - Read the failure paths and sibling surfaces: the non-finite-offset guard prevents permanent retries; the queued invoker uses the new arity; gain is updated before re-pushing; FM's detector ignores the added level offset; Kiwi retains its early-return path and availability override. The existing review's stale no-active-slice and non-finite-offset concerns are fixed in this head. No independent automated-review skill was available.
Evidence limits: The simulator cannot verify the HL2 hardware anchor or RF-gain transfer, nor its Auto-unavailable mode through a real HL2 session; the latter is covered by the injected UI and DSP tests. The screenshot request returned {"error":"grab produced an empty image for SpectrumWidget","ok":false}; no pixel-level line-placement proof is claimed. Synthetic carrier FFT arithmetic and the line mapping were tested, including all gain/level combinations in a separate header harness. No live radio was used. The head's CI and Static Checks were successful, created at 2026-10-04 01:28:12 UTC, after its included main tip's 01:13:20 UTC commit; this is not a claim that all tests were CI-gating or that the later squash has been independently certified.
Recommendation
Approve with nits, as a post-merge assessment rather than a pending merge action. No user-facing regression was found in the tested paths. Add test-profile isolation in a small follow-up and obtain the disclosed HL2 hardware confirmation of the anchor when feasible; this review does not substitute simulator evidence for RF measurements.
main gained aethersdr#6127 (squelch scale) after the first merge. No textual conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained aethersdr#6127 (squelch scale) after the first merge. No textual conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…VIII. aethersdr#6127 added a row on main and this branch has one of its own, so the merged tree again held one row more than its Totals line said. Regenerated with tools/gen_touchpoint_manifest.py: 238 headers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained aethersdr#6127 (squelch scale) after the first merge. No textual conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's #6127 added RadioCapabilities::squelchLevelScale for the SQL line and Auto SQL; this branch had ReceiveSquelchModel for the same RTL gate. Folded into one record (maintainer ruling): SquelchLevelScale gains `unit` and `modesExclusive`. RTL publishes RtlSquelchGate's -120 + 1.2*level dBFS/bin, FM/FMN, Auto SQL, exclusive; every other family leaves modesExclusive false, so the #2504 mode rule and radio-owned squelch state are unchanged. SpectrumWidget keeps one setSquelchScale and the finite-bin Auto estimator. Where the registry marks SQL unavailable in a mode, RxApplet and VfoWidget no longer overwrite its accessible reason with the Auto-SQL description. peripheral_auth_dialog_test (#6008) links RadioSetupDialog.cpp, so it now lists RtlReceiverSettingsWidget.cpp and ControlAvailabilityRegistry.cpp. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#5924) ## Summary RTL receive changes now publish only after hardware readback and DSP adoption. Capture browsing preserves each slice's frequency and settings, parks receivers outside usable capture, and resumes them when capture returns. Offscreen markers reveal the receiver through a confirmed capture transaction. Zoom uses a continuous 65,536-sample FFT (36.621 Hz bins at 2.4 MS/s), independently of audio and capture bandwidth. FM/FM-N have symmetric capability-bounded filters, 48 kHz per-receiver PCM, squelch, session/revision fences, rollback and accepted-state persistence. Requested or refused state never becomes saved live state. Spectrum averaging preserves RF history through display movement, and coherent capture coverage supports the 3D waterfall. PPM/DC settings use confirmed backend controls. Production admission at this checkpoint remains one receiver. Implements the bounded receive runtime of RFC #5468; respects the backend seam obligations in #5554/#5262. WFM stereo, optional digital decoding, live PPM interaction and multi-RX delivery are downstream increments. ## Integration and validation Reconciled with main at `a798558ac` (#6118). The reconciliation preserves main's typed tune/filter/mode/APF intents, HL2 squelch and WDSP changes. Each slice's SQL intent uses the existing common audio-intent binding once; the merge does not add a duplicate backend connection. The confirmed-pan dispatch comment now distinguishes an accepted request awaiting observation from refusal. Runtime documentation now describes the implemented 65,536-bin display FFT. Checkpoint `efc358a1` native Apple Silicon/Qt 6.12 build and CTest: **5/5 pass**, no skips, in 11.29 seconds (`receive_intent_routing_test`, `slice_apf_agc_off_seam_test`, `flex_slice_mode_intent_test`, `rtl_model_acceptance_test`, `slice_model_letter_test`). These are socket-free model/dispatch regressions; they do not claim live-radio, GUI or hardware timing qualification. The downstream #6083 application and broader tests are separately revision-pinned there. This requests human review and preserves the outstanding in-scope maintainer decisions; it does not mark the free-pan amendment approved. HD decoding is outside this PR and HD consent is not a merge condition here. Upstream's single spectrum outlet and socket-free test rehoming are preserved. The real model derives paced waterfall rows from spectrum observations; retired orphan waterfall signals are not restored. The new upstream receive-contract test now expects RTL's implemented FM/FM-N filter capability. Generated seam/touchpoint manifests are regenerated. Earlier downstream aggregate evidence (not rerun on this individual head): the aggregate builds AetherSDR with native Apple Silicon/Qt 6.12, RTL required and the optional decoder OFF. Focused downstream tests already pass for Flex mode dispatch, confirmed RTL model/capture state, FFT averaging, WFM, the PPM widget and seam consumers. This is aggregate evidence, not a claim that every platform was rerun on this individual head. Fresh hosted checks are required on this PR. The operator reports that the delivered `7601593f` build ran stably for days. That is live hardware feedback on the delivered build, not an instrumented soak of the new upstream integration. Current-head sanitizer, ARM throughput and full release-artifact qualification are not claimed. ## CI integration repair Upstream Radio Setup test targets (including #6008's `peripheral_auth_dialog_test`) compile `RadioSetupDialog.cpp`, so they also list the RTL widget and its availability registry. Both repaired tests build and pass on the native Mac decoder-OFF integration (2/2, 4.00 seconds). This follow-up changes test linkage only; earlier application/regression measurements retain their stated revisions. New-head CI is required and remains distinct from those measurements. ## Maintainer decisions All of the decisions below were made by Jeremy, the maintainer. - **2026-09-23 rulings.** The [mode-transition, seam and scope rulings](#5924 (comment)) stand. - **2026-10-03, free capture browsing.** The RFC #5468 amendment of 2026-09-25 (free capture browsing with parked slices) is **approved as RTL-only**; other backends keep their existing pan behavior. - **2026-10-03, later seam additions.** The seam pieces added after 09-23 are **ratified as RTL-owned**, on the same terms as 09-23: a second backend must justify reuse. They are `SquelchLevelScale`'s `unit` and `modesExclusive` fields; `requestReceiveTune(view)`, `recenterReceiveCapture` and `ReceiveCapturePlacement`; and the `BackendPanAveraging` persistence/description fields. Averaging persistence and coherent 3D presentation are part of that ratification. - **2026-10-04, one squelch record.** RTL's squelch is described by #6127's `RadioCapabilities::squelchLevelScale`, not a second record. `unit` carries RTL's dBFS/bin, and `modesExclusive` marks 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) | Header | New includer | Why | |---|---|---| | `core/backends/RadioCapabilities.h` | `src/gui/ModeFilterPresets.cpp` | FM/squelch availability reads the declared `ReceiveFilterControl`/`SquelchLevelScale` records | | `models/PanadapterModel.h` | `src/gui/PanFrameGuard.h` | Snapshots the pan's geometry when a frame arrives, so a confirmed FFT is never painted against a newer range | | `models/RadioModel.h` | `src/gui/ReceiveCaptureAction.h`, `src/gui/RtlReceiverSettingsWidget.cpp` | Capture-recenter action and the RTL device settings both go through RadioModel's capability-gated verbs | | `models/SliceModel.h` | `src/gui/MainWindowHelpers.cpp` | `requestSlicePanCenter` resolves the slice's pan and sends confirmed backends a Center tune instead of a pan request | ## Effects on other radios - **Squelch availability** follows one rule in both the RX applet and the VFO (`ModeFilters::squelchAvailableInMode`). A `squelchLevelScale` with `modesExclusive` (RTL only: FM/FM-N, −120 + 1.2·level dBFS/bin, Auto SQL) lists the supported modes, keeps each receiver's manual/Auto intent, and has Auto start from the absolute threshold. Otherwise the #2504 rule disables SQL in DIGU/DIGL/NT/RTTY/CW, unless the radio declares mode-independent squelch. Every other family's scale leaves `modesExclusive` false, so its SQL line, Auto SQL and availability are unchanged. - **SQL's unavailable reason** stays on the button. Where SQL is unavailable in the current mode, the RX applet and the VFO no longer replace the registry's accessible reason with the Auto-SQL description. - **FM-family passband availability** follows `ModeFilters::fmFilterAdjustable`. Declared FM rows are authoritative (RTL). A radio-owned filter with no FM row refuses FM edits (Flex, per FlexLib `Slice.cs`). HL2, ANAN, Icom and the Demo keep their existing adjustable passband. - **WFM/WBFM** use carrier-centered FM edges and no longer inherit the SSB preset ladder. - **Cancelled VFO drags** release the drag hold without the final retune (`SpectrumWidget::sliceDragCancelled`). - **Off-thread `SliceModel` setters** are refused and logged. ## CI The Linux `build` job now deletes unused host toolchains (Android, .NET, GHC, CodeQL) before building. The full RelWithDebInfo tree, which now includes this PR's RTL tests (about 3.6 GiB of test binaries), outgrew the hosted runner's free disk. Jeremy authorized the workflow change. ## Integration with #6107 #6107 gated a stranded RTL worker on the `connectRadio()` wiring this PR replaces. This PR's `startCapture()` already detaches the worker before stopping it, and gates every relay on producer identity and the capture session, so that wiring is kept. #6107's read-error guarantee is pinned in `rtl_capture_worker_test`'s stop-timeout block. That test fails if the producer gate is removed. Principles II, V and XIV govern confirmed state and persistence; VIII/XI govern evidence and regression tests. --- 73, Ozy **K6OZY** · Codex Generated with OpenAI Codex (GPT-6 Astra) --------- 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> Co-authored-by: Jeremy [KK7GWY] <105957118+ten9876@users.noreply.github.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>
…ick off. Principle II. (#6175) Fixes #6172. ## What was wrong On Icom, AetherSDR's squelch could get stuck **off**. There is no squelch enable on these radios: the `14 03` threshold is the control. `IcomCivBackend` therefore derived `squelchOn = level > 0` from every readback, and the confirmation read of our own "on, level 0" write published Off. `SliceModel` confirms receive controls from the radio, so from then on: - every `setSquelch(receiveSquelchOn(), level)` was sent as Off and wrote `0000` again. That covers TCI `sql_level`/`sql_enable`, the SQL shortcut and controller bindings; - once the remembered manual level was 0, SQL → Manual wrote `0000` and its readback returned the slice to Off. Squelch could not be turned on from AetherSDR again. This affects the IC-7300MK2, IC-705 and IC-9700, which share the write, the decode and the confirmation read. The MK2 also re-reads `14 03` every few seconds, so it re-asserted Off even without a write. Live on v26.10.1, Manual at 75 when a TCI client connected: ``` TCI rx: "sql_enable:0,true;" TCI rx: "sql_level:0,0;" TX -> ... 14 03 00 00 fd RX <- 14 03 00 00 TCI tx→all: sql_enable:0,false; # our own write read back as Off TCI rx: "sql_level:0,20;" ... "sql_level:0,100;" TX -> ... 14 03 00 00 fd (all six) # every later level written as 0 ``` ## The fix `IcomCivBackend` remembers that its last `14 03` write was **on at threshold 0** (`m_squelchOnAtZero`). - That write's 0 readback, and the MK2's periodic 0 poll, stay **on**. - A radio-side non-zero value clears the flag, so a later front-panel return to 0 still reads as off. - An explicit Off write reads as off, and disconnect forgets the flag. - The controls scrub re-asserts on-at-0 as on. ## Proof (live IC-7300MK2, proof build) **1. The trapped state is no longer a trap.** Starting from SQL Off with the remembered manual level at 0, clicking SQL → Manual wrote `0000`. The radio echoed `0000` and squelch **stayed on** through three periodic polls. Before this fix it bounced back to Off.  **2. The TCI sweep that used to stick at 0** (`sql_enable:true`, then `sql_level` 0 → 100) now writes every level as itself: `0000, 0051, 0102, 0153, 0204, 0255`. No `sql_enable:false` is broadcast back. RX audio over TCI shows the radio honouring each write: open at -17 dBFS through 0102, digital silence (-200 dBFS) from 0153 up. ## Tests (headless) `icom_control_profile_test` runs a new case per model (IC-7300MK2, IC-705, IC-9700): | Readback | Expected | |---|---| | connect-time 0 | off | | 0 after an on-at-0 write | on | | periodic 0 poll after an on-at-0 write | on | | non-zero threshold | on, at that level | | radio-side 0 after a non-zero threshold | off | | 0 after an explicit Off write | off | Run against the pre-fix backend, the two on-at-0 checks fail on all three models. All 27 Icom and squelch-related ctest targets pass. ## Not in this PR: the SQL line / Auto SQL scale on Icom Icom still publishes `legacyDbmSquelchScale()` (Flex's `−160 + level` dBm, #6127). I measured the MK2's SSB S-meter squelch through this build: `14 03` stepped while the TCI RX audio was watched for the gate, on 14.2968 USB band noise. | Front end | S-meter | Gate (`14 03` raw) | |---|---|---| | preamp off | −106.5 dBm (raw ≈ 46) | ≈ 113 | | P.AMP1 | −86.5 dBm (raw ≈ 90) | ≈ 146 | | P.AMP2 | −77 dBm (raw ≈ 111) | ≈ 166 | | 20 dB ATT | pinned at S0 | ≈ 85 | That fits roughly `gate_raw ≈ 0.82 · smeter_raw + 75`, or about `−168 + 1.4 · level` dBm below S9. The current line is therefore around 15–20 dB below where the radio gates. A correct line also needs the Icom pan's nominal dBm axis reconciled with the S-meter: the pan floor read −123 while the S-meter read −87 on the same noise. That is a calibration change for its own PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
Fixes #6092.
What was wrong
The panadapter SQL line and Auto SQL assume every radio's squelch level means
−160 + leveldBm. That is Flex's scale. On the HL2 it is wrong in three ways:−140 + 0.7·leveldBFS (WdspChannel::levelSquelchThresholdDbfsForLevel), so every dB of LNA gain moves the antenna-referred point where squelch opens by a dB. on8st's re-run on a real HL2 (HL2: panadapter SQL line and Auto SQL use the Flex −160+level scale; the HL2 amsq gate is −140+0.7·level dBFS #6092, 2026-10-03) shows the map was fitted at LNA −12 dB. At the shipped +20 dB, level 50 sits 24 dB or more below the no-signal floor: it opens below about level 85 and never closes on a carrier.offsetDb() = fullScale − LNA(−17 dB at +20). The line was drawn on Flex's scale with neither offset.xamsqcap,third_party/wdsp/upstream/RXA.c:650, afterxnbp(nbp0)at:647, beforexwcpagcat:665). It compares an averaged magnitude against10^(dB/20)(amsq.c:125-126,:221-224). Pan noise is per bin, so the two differ by an amount that depends on both the filter width and the bin width. That is on8st's point on the issue.The fix (Jeremy's ruling on #6092: C+R)
gate = −108 + 0.7·L − lnaOffsetDb()dBFS. It equals on8st's map at LNA −12 and tracks the LNA dB for dB, the same way the AGC ceiling already does.WdspChannelstays family-neutral.setSquelch(on, level, levelOffsetDb)adds a caller-supplied offset to the amsq map only, and the channel has no gain of its own to refer to.Hl2DbReference::levelSquelchOffsetDb(). That class already "owns everything relating raw dBFS to dBm, plus the AGC setpoint that must move with it" (docs/agents/backends.md: radio-facing logic stays in the family backend).Hl2Backendsends the offset with every squelch push and re-pushes on every LNA change (pushEffectiveLnaGain), including auto RF gain steps.Hl2RxDspkeeps the last finite offset if it is handed a non-finite one, so its retry loop only ever means "the channel is busy".radio-capabilities-map.mdsays so.RadioCapabilities::squelchLevelScale(core/backends/SquelchLevelScale.h). It holds offset, dB per step, modes, and whether Auto SQL may use it. Absent means no SQL line and no Auto SQL.{−160, 1, all modes, Auto}, unchanged behaviour.−140 + (G + 12) + (3 − G) + 6.02= −118.98 + 0.7·L on the pan axis, in AM/SAM/DSB/LSB/USB only. The LNA terms cancel, so the record is a constant. FM is excluded: fmsq is a noise-quieting gate with no dB place.legacyDbmSquelchScale(), i.e. Flex's{−160, 1, all modes, Auto}(maintainer decision on this PR). None of them has a measured map of its own, so their SQL line and Auto SQL behave exactly as on main. On main, ANAN and RTL-SDR had both features in the GUI too: the SQL button is live in voice modes, so the line drew and Auto SQL ran. They therefore get the same record, so nothing regresses. Kiwi keeps its own server-relative branch, untouched.Hl2Spectrumdivides by half the window sum, so a steady carrier of magnitude A reads20·log10(A) + 6.02.squelch_level_scale_testpins this against a synthetic carrier. amsq opens on the magnitude itself, whichwdsp_channel_test's new carrier case pins: ±2 dB around the gate, at two offsets. A carrier off a bin centre reads up to 1.4 dB lower (Hann scalloping).check_capability_records.py --strictstays at the frozen count (70/70 on current main): the record is an optional, and itsautoSquelchbool sits inside the nested struct.SpectrumWidgetdraws the line fromthresholdDb(level). Auto SQL computeslevelForThresholdDb(floor + margin), which is bit-for-bit the old Flex result across −175..−40 dB, and returns early when the record is absent or has no Auto SQL.MainWindow::syncSquelchScaleToUi()pushes the record for the active slice's mode. It runs on a capability change, an active-slice switch (beforeRxApplet::setSlice), a slice mode change, and when a pan is added.autoSquelchShouldRunOnSpectrumgates on it too.accessibleDescriptionand tooltip (translated), mirrored on the VFO's SQL button. Before a slice is active the gate stays permissive, as it is while disconnected.Behaviour changes to note
Validation
All of this was run locally on Arch Linux with an offscreen Qt platform. No HL2 or FLEX hardware was touched.
The unit tests, the full suite and the static gates were re-run at 1c57410, after main (3e4b14e) was merged in with a signed merge. The click table below was recorded at 33711e2. The reconnect check was run at 8bc6ebc.
Unit tests (socket-free, mutation-checked).
squelch_level_scale_test(new).{−160, 1, all modes, Auto}.Hl2Spectrumreads 20·log10(A) + 6.02, and a carrier at the amsq gate peaks exactly on the drawn line at LNA −12/+8/+20.−160 + levelarithmetic across −175..−40 dB.wdsp_channel_test.hl2_rxdsp_squelch_test.configure().Hl2Backendon its real I/O thread:setSliceSquelchlands at the current LNA's offset,setLnaAutoOffsetDb(10)moves the gate −10 dB, and 0 moves it back.rx_applet_squelch_reconciliation_test.thresholdDbsetSqlModeAuto coercion removed (this one survived at first and was killed by the added restore-path test)Hl2RxDsp::setSquelchpushSquelchinHl2Backend::pushEffectiveLnaGainFull local suite.
ctest -j8ran 563 tests: 561 passed, one skipped by design, andconnection_panel_size_testfailed, which is known on this host (#5811). Excluded: 68 tests whose sources use a socket API, or whose names match the socket-test families (automation, TCI, rigctl, CAT, discovery, bridge, DAX and so on), classified by grepping each test's sources fromtests.cmake.Agent automation bridge (demo sim, offscreen, own socket,
AETHER_AUTOMATION_NO_TX=1).serial DEMO-0001;get radioreturnsAetherSDR Demo.dumpTreeshows "Squelch mode" with the description "Cycle squelch through Off, Manual, and Auto modes".invoke RxApplet/Squelch mode clickcalls, reading the button andget sliceafter each:That is main's behaviour, unchanged.
disconnectand thenconnect … DEMO-0001again, which goes through the connected-but-no-active-slice window. Afterwards the button still readsAUTO(slice on, 66), with the Auto-available description, so it is not demoted to Manual.rx_applet_squelch_reconciliation_test: the cycle skips Auto, the reason reaches both buttons, a restored Auto intent lands on Manual, and a slice in Auto drops to Manual.Not verified:
AETHER_NO_GPU=1, so the line could not be grabbed.Static gates (merge base vs head)
Run on the merge base and on the head; findings diffed per file.
check_engine_boundary.py --strict: 102 → 102 findings. The only diffs are line shifts inMainWindow_Wiring.cppfrom one added include.gen_touchpoint_manifest.py --check: up to date after regeneration. One new row,core/backends/SquelchLevelScale.h, taggeduniversal, with 2 includers (SpectrumWidget.h,MainWindow_Wiring.cpp).core/backends/SquelchLevelScale.h,universal, 2 GUI includers. It is the record type and its pure mode/Auto predicates. It follows theNoiseFloorAutoAdjustGate.hpattern, so the widget, MainWindow and the test read one function rather than copies.check_capability_records.py --strict: 70 → 70 booleans. The record is astd::optional, and itsautoSquelchbool is nested.check_command_plane.py --strict: 400 → 400 (main's current baseline).check_test_registration,check_ci_test_gate,check_qsettings,gen_seam_probe_table --check.check_a11yreports 16 findings on the 8 touched GUI files, the same as main.Thanks to @on8st for the hardware sweeps (the original map on #5982 and the LNA-pinned re-run on #6092), and for the RXA.c/amsq.c analysis of what amsq actually reads.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g