Repository navigation
fix(hl2): open DIGU/DIGL with AGC off, correct §18. Principle VIII. - #6119
Conversation
Entering DIGU or DIGL on a Hermes-Lite 2 now turns the receiver's AGC off, and leaving them restores the AGC mode held before. aethersdr#5629 measured the reason offline against WSJT-X's FT8 decoder: medium AGC raises the noise between transmissions, inflates the decoder's noise reference and costs 32 of 82 weak decodes beside a +30 dB signal; AGC off recovers them. The default is adopted on a mode change only, like the per-mode passband. An AGC mode the operator sets inside a data mode is theirs and is kept on leaving it. The data mode's own "off" is never written to the remembered AGC, so it cannot follow the operator into SSB on the next launch. With AGC off the level comes from the AGC-off level (default 10 dB of fixed gain) instead of up to 39 dB of AGC gain, so DIGU/DIGL are quieter on the speaker and on the TCI level meter. Docs and comments named in the aethersdr#5629 triage: HERMES.md §13 rows 26, 28 and 30, §18.5 and §18.7 now state the measured mechanism and that the Wideband tap is withdrawn for HL2; the blockForOutput comments in Hl2RxDsp.h and WdspChannel.h no longer claim a deterministic burst feed. Refs aethersdr#5629 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
1 · Issue fit
Partially. #5629 asked for exactly this (agcMode = "off" on entering DIGU/DIGL, plus the §13/§18 doc corrections), and the triage comment invited it explicitly — "Do the DIGU/DIGL default AGC, subject to the level question above… send the XS PR" — so the shape is already ruled on and this is not an unratified default change. The doc rewrite is faithful to the measurement: §18.5 now states the real mechanism (noise reference inflated 18.25 dB, signal window within 0.042 dB) rather than the hand-wave it replaces, and withdrawing the Wideband tap rather than deferring it matches the recommendation.
What it does not cover is the path an FT8 operator actually takes most often: the AGC default is adopted only on a mode change through setSliceMode(), and connect-time mode restore does not go through setSliceMode(). Detail in Blocker 1. The title's claim "open DIGU/DIGL with AGC off" is true for a mode click and false for relaunching into DIGU.
The triage's one open question — the ~29 dB level step from a 39 dB AGC ceiling to a 10 dB fixed gain — is answered in the commit message ("DIGU/DIGL are quieter on the speaker and on the TCI level meter"), which satisfies the "be explicit" half. It is not in HERMES.md, where the next reader will look; see nits.
2 · Scope
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
Hl2Backend.cpp / .h |
isDigitalDataMode(), the AGC-off adoption in setSliceMode, the agcModeBeforeDigital member, the capture/restore guards in setSliceAgc, seedReceiverAgc, currentOperatingState |
Yes — the fix | In scope |
tests/hl2_tci_signaling_test.cpp |
testDigitalModeDefaultAgc(), registered in main() |
Yes | In scope |
docs/HERMES.md |
§13 rows 26/28/30, §18.5, §18.7 item 5, the capability table row | Yes | In scope — all four named in the #5629 triage |
Hl2RxDsp.h, WdspChannel.h |
blockForOutput comments corrected: the flag blocks but does not make a burst feed reproducible |
Not in the title; in the commit body and in #5629 §"One harness fault" | In scope — comment-only, two lines each, traceable to a named triage item |
No new public surface, no settings keys, no capability fields, no CHANGELOG.md entry (correct). No removed guards — the only - lines of substance are the if/else brace conversion in setSliceAgc, which preserves the original branch logic exactly. The §18 edits delete no claim that was true: §18.5's old "it decodes, but a modem on AGC'd audio is a known-marginal arrangement" is replaced by the measurement that supersedes it. Nothing in the diff is unexplained.
3 · Blockers
1 · Restoring into DIGU/DIGL leaves medium AGC — the primary WSJT-X workflow doesn't get the fix. (inline on Hl2Backend.cpp:2906)
Reasoned from code, not reproduced at runtime:
pushInitialState()writes the restored mode directly:txRx->mode = m_restoredState.mode;(Hl2Backend.cpp:7101-7102), then derives the passband from it and pushessetAgc(wdspAgcMode(r.agcMode), …)at:7155. It never callssetSliceMode(), so the new block never runs.seedReceiverAgc()has already setr.agcModefrom the remembered flat pair and clearedagcModeBeforeDigital(:6530-6533).- The capture side of this PR deliberately never stores the data mode's
off, so the remembered pair is always the non-digital one — by design.
Net: operator runs FT8 in DIGU, quits, relaunches, reconnects → DIGU with slow/med AGC, agcModeBeforeDigital empty, and nothing pending. It self-heals only after a mode round-trip out of and back into a data mode. DIGU → DIGL doesn't help either (wasDigital && isDigital takes neither branch).
The PR's own analogy is what makes this a gap rather than a judgement call: the comment says "Adopted on entering them, like the passband", and the passband is handled at that exact restore site (defaultPassbandForMode(txRx->mode), :7104-7106). The AGC has no counterpart there. Fix looks like a few lines in the same restore block: if isDigitalDataMode(txRx->mode), stash agcModeBeforeDigital = agcMode and set off before the setAgc push at :7155, under the existing m_passbandDerivedThisConnect guard so an EP6 re-linkUp can't replay it over a live edit.
I'd call this a blocker because the PR reads as "fixed" and the most common entry into DIGU is the one left on medium.
4 · Nits (non-blocking)
createPanadapter()'s seed can strandoff. (inline onHl2Backend.h:511) The seed atHl2Backend.cpp:962-972copiesfirst.modeandfirst.agcModebut notagcModeBeforeDigital(correctly — it's per-receiver). So a pan created while receiver 0 sits in DIGU starts atmode=DIGU, agcMode=off, before=""; moving that receiver to USB hitswasDigital && !isDigital && before.isEmpty()and leaves it onofffor the rest of the session. Smaller blast radius than Blocker 1 and the same root shape (a path that setsmodewithout going throughsetSliceMode); worth one line either way.- The
currentOperatingState()hunk is unreachable in practice. (inline onHl2Backend.cpp:6565)m_agcModeis assigned unconditionally inapplyRestoredState()(:6503, which runs before every connect) and again inseedReceiverAgc()(:6539), and is never cleared — so theelse iffallback only fires pre-connect, whenrx(m_txDdc)is null anyway. The test'scurrentOperatingState().agcMode == "slow"checks pass through them_agcModebranch, not this one. The change is correct and defensive; it just isn't load-bearing and isn't covered. - The level step isn't in the docs. The triage flagged the ~29 dB drop (39 dB AGC ceiling →
agcFixedGainDb10.0) as "a plausible next issue". The commit message says it;HERMES.md§13 row 30 and §18.5 don't. One sentence next to the row would stop the next reader filing it. Related question, not a finding: did #5629's AGC-off arm run at the sameagcOffLeveldefault (10) this PR will produce, or at unity? !previous.isEmpty()is dead in both the passband and the new AGC block —Receiver::modedefaults to"USB"(Hl2Backend.h:498), so it's never empty. Harmless, and consistent with the sibling above it.
5 · What I tried to break
- Mode-transition matrix.
DIGU→DIGL(no change, correct),DIGL→DIGU, re-selecting the same data mode, a refused mode string (early return beforer->modeis written, sopreviouscan't be corrupted), andUSB→CWUwith a pendingbefore— all behave.agcModeBeforeDigitalcan only be non-empty while the mode is digital, so there's no path where the restore branch fires outside a data mode. - The capture seam. Walked
setSliceAgc→m_agcMode→currentOperatingState→applyRestoredState→seedReceiverAgcfor leakage of the data mode'soffinto the persisted document. It doesn't leak: entering DIGU never touchesm_agcMode, and the threshold-only call (mempty) keeps thebeforevalue. One edge the PR accepts rather than handles: an operator who explicitly re-selectsoffwhile in DIGU getsm == r->agcMode, sobeforeis not cleared and their choice is overwritten on leaving — the test pins this deliberately, and I agree it's the lesser surprise. - Multi-receiver.
agcModeBeforeDigitalis per-Receiverand resolved throughddcForSlice/rx(), every access null-checked, and the flatm_agcModecapture keeps the "last receiver the operator touched" rule from #4909 intact. Receiver 1 in DIGU does not drag receiver 0's AGC.buildReceivers()carries state wholesale;seedReceiverAgc()clears the member. The only hole found is thecreatePanadapterseed above. - Threading and lifetime. The new
setAgcpush isQt::QueuedConnectiontor->dspbehind anr->dspnull check — byte-identical shape to thesetAgccall insetSliceAgcfour hundred lines down, and on the same (GUI) thread. No new ordering: the AGC push lands before thesetMode/setFilterpair, andapplyRxAgcis mode-independent, so theSetRXAModerebuild below it can't discard the AGC the way it discards filter edges. - The test. It would fail on main — without the new block the first
check(agc == "off")sees"slow". Mutatingif (m != r->agcMode)to clear unconditionally reddens the last assertion; dropping thewasDigitalterm reddens the DIGU→DIGL check. It runs withr->dsp == nullptr, so it covers the state machine and the published delta but not the WDSP push — an honest layer boundary, not a gap. Socket-free, no fake peer, registered attests/tests.cmake:5660; one stickyagccapture variable means "a non-data mode change leaves AGC alone" would also pass ifemitSliceStateomittedagcMode, which is weak but not wrong. - Could not check: anything at runtime — no build, no HL2, no WSJT-X. Every finding above is read from the head checkout at
540e149. CI is green on all five checks (build,check-macos,check-windows, Static checks, Sanitizer configures) against base732b99a9, which is currentmain; note the per-PRctestis-R-filtered, sohl2_tci_signaling_testpassing in CI is not something I can confirm from the check list.
6 · Recommendation
Request changes, on Blocker 1 only. The measurement behind this is unusually well done and the doc rewrite is the kind that saves the next person a week — but the fix is wired into the one mode path that the connect-time restore bypasses, and the operator who benefits most from AGC-off in DIGU is precisely the one who left the radio there overnight. Concrete next step: extend the restore block in pushInitialState() (Hl2Backend.cpp:7099-7113) to adopt the data-mode AGC default alongside the passband it already derives, add the mirror-image case to testDigitalModeDefaultAgc() (restore into DIGU, check off, leave to USB, check the remembered mode returns), and decide the createPanadapter seed either way. The nits can ride along or follow.
🤖 aethersdr-agent · cost: $8.1571 · model: claude-opus-5
Review of aethersdr#6119: the data modes' AGC default was adopted only in setSliceMode(), so three other ways a receiver comes to be in DIGU or DIGL kept the remembered AGC. followModeAgc() now holds the rule and is called wherever a receiver's mode is written or its AGC is seeded: - setSliceMode(): mode click, TCI, band-stack recall (unchanged behaviour, the code moved into the helper). - pushInitialState(): the connect-time restore writes the restored mode directly. Under the once-per-connect guard, so a link-up after EP6 silence does not replay it over an operator's AGC choice. - seedReceiverAgc(): a receiver already in a data mode when the remembered AGC is seeded keeps AGC off and holds the seeded mode. - createPanadapter(): the seed copies agcModeBeforeDigital with the mode and AGC it already copied, so the new receiver can leave "off". The remembered AGC stays the operator's. seedReceiverAgc() primes the capture member from the held mode, never from a data mode's "off", and setSliceAgc() and the pre-connect fallback in currentOperatingState() already did. The dead "!previous.isEmpty()" term is dropped from the AGC rule. docs/HERMES.md section 18.5 and section 13 item 30 state the level step: 10 dB fixed gain at the default AGC-off level against a 39 dB AGC ceiling, up to 29 dB quieter, arithmetic and not a measurement. Tests: hl2_state_restore_test (restore into DIGU, later link-up, seed in DIGL), hl2_pan_create_async_test (receiver seeded from a DIGU one), hl2_tci_signaling_test (capture fallback before any AGC is set, and the non-data mode check no longer passes on an earlier publish). Refs aethersdr#5629 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Bad news first: Blocker 1 stands. I had it in the body as limit 3 and called it out of scope; that was the wrong call. Fixed in One limit remains. A band-stack bookmark applies its own stored AGC after the mode (
Fail-before (the new tests on Suite at Not verified: anything on a radio, and that WDSP receives the pushed AGC. 73, ON8ST |
ten9876
left a comment
There was a problem hiding this comment.
Issue fit
The mechanism is correct, and the head now covers every way into DIGU/DIGL. Whether HL2 should change this default is still a maintainer ruling, and nobody has made it yet. #5629 measured offline that WDSP's AGC costs FT8 decodes beside a +30/+40 dB neighbour, and that AGC off on the slice recovers them. The triage comment on #5629 said "send the XS PR". That comment came from aethersdr-agent, though, and the issue still carries maintainer-review. Under Principle XIII peer-agent suggestions "are hints", and GOVERNANCE.md lists "default values affecting all users" as something an agent "may not autonomously change". It also says "any change to default UX behavior" needs an RFC. This PR turns AGC off for every HL2 operator who selects DIGU/DIGL, and on a quiet band the receiver gets up to 29 dB quieter on the speaker and on the TCI meter. Limit 1 of the PR body already says this is the maintainer's call, which is right. The review agrees: the code is ready for that decision, and the decision itself is not made here.
Fix or preference (the skill's step 6): it is not a bare preference. It cites a measurement with a stated denominator and registered falsifiers. That measurement is offline only, though: no radio, no band, no static crashes. The HL2 gateware has nothing to say here, because AGC is host-side WDSP. The authority is #5629's harness, and nothing else backs it. FlexLib (reference/FlexLib_API_v4.1.5.39794/FlexLib/Slice.cs) couples DIGU/DIGL to nothing on the client side: AGC is a per-slice radio property, set and echoed independently of the mode. So no reference behaviour either supports or contradicts the default. With this PR, HL2 DIGU will open with AGC off and a Flex DIGU slice will keep whatever AGC the radio holds.
Scope
| File / group | Change | Claimed? | Verdict |
|---|---|---|---|
src/core/backends/hl2/Hl2Backend.cpp / .h |
isDigitalDataMode(), followModeAgc() and its 3 callers, the pan-seed copy, capture/seed/fallback read the held mode, the setSliceAgc clear |
Yes | In scope (the default change itself needs the maintainer, see above) |
docs/HERMES.md §13 rows 26/28/30, §18.5, capability table, §18.7 item 5 |
Measured mechanism, Wideband withdrawn, level step stated | Yes, as the #5629 triage asked | In scope. §18.5 rewritten in place, limit 5 flags it for the maintainer |
Hl2RxDsp.h, WdspChannel.h |
blockForOutput comments corrected (comments only) |
Yes | In scope (triage item) |
tests/hl2_tci_signaling_test.cpp, hl2_state_restore_test.cpp, hl2_pan_create_async_test.cpp |
New cases | Yes | In scope. See the socket note below |
Commits are 540e149e and 6d09a779, both authored 2026-10-03, and both about the issue. The diff adds no new settings key, verb, capability field or CHANGELOG.md entry. Nothing on the - side removes a guard: the only removed logic is the brace conversion in setSliceAgc, and it keeps the same branches. Everything in the diff is explained by the issue.
Blockers
None in the code. The merge depends on the maintainer ruling on the default (above), which is not a defect the author can fix.
Nits / maintainer-decision inputs (non-blocking)
- The PR's answer to the level drop does not survive a restart. Limit 2 says "the operator can raise the AGC-off level". On HL2 that level lives only in
Receiver::agcOffLevel(Hl2Backend.cpp:3090, pushed at:3288).RestoredRadioStatehas no field for it (src/core/backends/RestoredRadioState.h:22-31persists onlyagcMode/agcThreshold), and nothing insrc/writes it to settings. So an FT8 operator who raises it to get the 29 dB back has to raise it again every launch, on every receiver. Before this PR, AGC off was an explicit operator choice, so this was a corner. After it, the AGC-off level is the operating gain of every HL2 DIGU session. Not this PR's bug, but the maintainer should weigh it with the default. Either persist the off level next to the AGC pair, or rule on a data-mode fixed gain, which was the other option the triage offered. - Siblings left behind, disclosed in limit 1. ANAN-G2 (
AnanBackend::setSliceAgc,AnanBackend.cpp:818) and RTL-SDR (RtlSdrBackend.cpp:572) run the same WDSPwcpAGC. The mechanism #5629 measured is a WDSP property, not an HL2 one. After this PR, the same mode on two host-DSP radios in one app behaves differently. That is fine as an HL2-first step, but it should be on record (an issue or a §13 row) so it does not look like a deliberate divergence. - Pre-PR band-stack bookmarks re-impose medium (limit 3). I read
RadioModel::recallBandStackReceiveDsp(RadioModel.cpp:4183-4198). Mode,sliceChangedand AGC are allDirectConnection, so the backend'soffreachesSliceModelbefore the bookmark comparison runs. A post-PR bookmark (storedoff) therefore round-trips cleanly. A pre-PR DIGU bookmark (storedmed) sendssetSliceAgc("med")afterslice->setMode(MainWindow.cpp:5317and then:5351).Hl2Backend.cpp:3006treats that as an operator choice, clears the held mode, and leaves medium AGC on in DIGU. Saving the entry again storesmedagain, so the old value never ages out unless the operator picksoffby hand. The PR body discloses this as limit 3. The automated pass flagged the same path independently. If the maintainer accepts the default, a follow-up could ignore a stored AGC on DIGU/DIGL recall, or migrate those entries once. - One claim cannot be checked from the issue. "#5629's AGC-off arm ran at that same 10 dB" (
docs/HERMES.md§18.5) is not in #5629's body. Only the author's PR comment says it. If the level matters to the decode result, put the figure in the issue thread, where the measurement lives. - Stale CI. The earliest run on
6d09a779was created at 18:38Z, andmainhas since moved 7 commits (19:25Z). Three of those touch HL2 (#6104, #6113, #6126), includingHl2Backend.cppandhl2_pan_create_async_test.cpp.git merge-treeagainst current main is clean. I checked main'sHl2Backend.cpphunks: they are in TX keying/tune,openReceiverDsp, capabilities and the TX reset at the top ofpushInitialState(), and none touchesReceiver::mode/agcMode, the pan seed,seedReceiverAgcor the restore block. So I found no functional overlap, but I did not build the merged tree. A CI re-run on current main would close this.
Socket-test notice (step 0)
tests/hl2_state_restore_test.cpp gains three connectRadio() cases, and that path starts MetisClient. MetisClient binds a QUdpSocket on AnyIPv4:0 (MetisClient.cpp:330-331, via Hl2Backend.cpp:2658) and sends to TEST-NET-1. The existing cases in that test already do the same, so this is not new socket ownership. It does make the test a modified socket-owning one, so I did not run it, and its three checks are reasoned from code, not executed. The other two targets are socket-free: hl2_pan_create_async_test asserts transportIdle, and the new hl2_tci_signaling_test case never connects.
What was verified vs. read, and what held
- Built the head
6d09a779(Linux, Qt 6, RelWithDebInfo) and ranhl2_tci_signaling_test("all checks passed") andhl2_pan_create_async_test("0 failure(s)"). - Mutations, both caught. Dropping
seed.agcModeBeforeDigital = first.agcModeBeforeDigital;(Hl2Backend.cpp:971) gives[FAIL] and returns to the first receiver's held AGC on leaving DIGU. Short-circuiting thefollowModeAgccall insetSliceMode(:2926) gives 5 FAILs ("entering DIGU turns AGC off", "DIGU to DIGL keeps AGC off", "entering DIGL turns AGC off", "a threshold change in a data mode does not capture off", "and the held AGC still returns afterwards"). - GUI echo loop. If the GUI re-sent a stale AGC after the mode change, the default would be undone. The GUI does not:
SliceModel::setAgcThresholdsendsm_agcMode, whichsliceChangedhas already set tooffsynchronously, and an equal mode keeps the held mode (Hl2Backend.cpp:3004-3008).SliceModel::applyDelta's AGC update is equality-guarded and emits no command. - Restore/seed matrix, read. Fresh connect, restore into DIGU, then seed: off, held = remembered. Carried receiver already in DIGU, then a different-radio seed: off over the seeded mode. Carried DIGU, then restore USB: the held mode returns. Same-radio reconnect: no reseed, and the restore block runs once per connect (
m_passbandDerivedThisConnect). Every write toReceiver::mode(:967,:2914,:7116) either callsfollowModeAgcor copies the held mode. - Capture. The data mode's
offnever reachesm_agcMode/currentOperatingState(), except when the operator picks a different AGC and thenoffinside DIGU. That is an explicit choice, so it being kept is consistent with the table. - Doc numbers checked against #5629. 32/82 at +30 dB, 58/76 at +40 dB, 0.042 dB, 25–29 dB, 18.25 dB, and 39 dB = 65 × 0.6 (
Hl2DbReference::agcCeilingDb).agcFixedGainDbForOffLevel(10)= 10 dB. All match. - Automated pass (
/code-review, medium): one low-severity finding, the pre-PR bookmark path, folded into nit 3 after I checked it againstMainWindow.cpp:5310-5351. It found nothing else. - Engine boundary.
tools/check_engine_boundary.pygives identical per-file findings on the merge base732b99a9and the head. - Not driven in the app. The demo is
SimBackend, which never runsHl2Backend, so a bridge session would exercise none of the changed lines. Nothing was run on an HL2. Speaker and TCI levels and the on-air decode result are unverified, and the PR says so itself.
Recommendation: Needs maintainer decision. If the default is accepted, the code is mergeable as is, after a CI re-run on current main. Nit 1 (the off level is not persisted) is the input most likely to change how the default lands.
Brings the branch onto main at d9de044 so CI runs against it. The merge is textually clean. Main's HL2 changes since the branch point (aethersdr#6104, aethersdr#6113, aethersdr#6126) are in TX keying and tune, openReceiverDsp, capabilities and the TX reset in pushInitialState(); none writes Receiver::mode, agcMode or the held AGC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…VIII. Section 18.5 now says the DIGU/DIGL AGC default is HL2-only while the mechanism aethersdr#5629 measured is wcpAGC's: ANAN-G2 runs the same AGC and keeps the operator's mode in the data modes, and RTL-SDR runs it at WDSP's medium with no data mode and no AGC command. Review nit 2 asked for the divergence to be on record. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Bad news first, two items.
Pushed
Inputs to your ruling, not built:
If you accept the default I build both as follow-ups, the persisted off level first: it touches every DIGU session on every launch, the bookmark recall only entries saved earlier. On those two failures: the HL2 one fails here on clean main every time (60 of 60), and its cause is in the test, not in the guard; I posted it on #6113. The other one I have not traced beyond a layout that needs 680 px in a 640 px viewport on this Mac. 73, ON8ST |
|
Maintainer ruling on #6119:
73, Jeremy KK7GWY |
Maintainer ruling on aethersdr#6119: the AGC-off level is the operating gain of DIGU/DIGL once they open with AGC off, so it has to survive a restart. RestoredRadioState gains agcOffLevels, one 0..100 entry per receiver index (-1 = none). RadioStateMemory stores it as the agcOffLevels array of the radio's OperatingState document, and reads or writes it only for a backend that declares the Agc domain and hasAgcThreshold. A document without the array restores the default of 10. No schema bump: the field is optional, as the AGC pair was when it was added. Hl2Backend captures a change in requestSliceAgc(), seeds each receiver with its own entry in seedReceiverAgc(), gives a receiver opened later the entry for its index, and drops an out-of-range entry at the restore boundary. No other family reads or writes the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HERMES.md §5 (AGC) and §18.5, and the clientSettingsDomains row of the capabilities map, describe where the level is stored, which declaration gates it, and that ANAN and RTL-SDR ignore it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Bad news first: the head is not green here. 612 of 615 on macOS; the three failures are upstream's and are named in the body. Merged with main at Pushed
Tests: two levels, capture, store, new backend, load, restore; each WDSP channel reports the gain. Ten checks red on the old source. Dropping the restore: 4 red. Dropping the capture: 5 red. Not verified: a radio, a real app restart, more than two receivers. The bookmark follow-up is not started. 73, ON8ST |
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
|
We're updating this branch against main and landing it (maintainer pass); please don't push to it meanwhile. New commits go on top, with no rebase or force-push. |
…VIII.
testAgcOffLevelCapture restores {20, 61}, sets receiver 0 to 37 and
expects the capture {37, 61} plus one capture request. It runs without
connectRadio(), so the capture half of the per-receiver persistence has
a check that runs where hl2_state_restore_test (which binds UDP) cannot.
Red when requestSliceAgc() stops remembering the level, when
currentOperatingState() stops reporting it, or when applyRestoredState()
stops loading it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
|
Maintainer pass: answers to my review of The rulings are as posted on 2026-10-03: the default is accepted, the AGC-off level persists per receiver in this PR, and stale bookmarks are a follow-up. Point by point:
Socket notice. Static gates. |
ten9876
left a comment
There was a problem hiding this comment.
Approved at f48369a7b: maintainer pass (/pr-land).
Maintainer decisions (Jeremy, KK7GWY)
- Default accepted. On HL2, DIGU/DIGL open with AGC off, on the basis of #5629's offline FT8 measurement.
- The AGC-off level persists per receiver in this PR. on8st did it in
7ce11806/71225eee:RestoredRadioState::agcOffLevels, theagcOffLevelsarray in the radio'sOperatingStatedocument. I verified it is capability-shaped (ownsAgcOffLevel=Agcdomain ∧hasAgcThreshold; only HL2 declares the domain). There is no family check and no flat key. - No schema bump, as with #4911: an additive array that older builds ignore.
- Stale DIGU/DIGL band-stack bookmarks that restore
medare follow-up #6142, not this PR.
Findings, each closed at this head
- Blocker, the connect-time restore bypassed the default. Fixed by on8st in
6d09a779.followModeAgc()(Hl2Backend.cpp:2922) is called fromsetSliceMode():2972,seedReceiverAgc():6699, and thepushInitialState()restore block:7288, inside them_passbandDerivedThisConnectguard (:7268–:7318). ThesetAgcpush at:7341follows it. - Nit, the pan seed stranded
off. Fixed in6d09a779(:992). Proved byhl2_pan_create_async_test, which goes red when the copy is removed. - Nit, the capture fallback looked unreachable. Refuted: it is reachable before a connect, and it is covered by
testDigitalModeDefaultAgc, which goes red when the fallback is removed. - Nit, the mirror-image restore test. Present in
hl2_state_restore_test. - My review's points 1–5 are answered in the top-level comment. Main is merged in at
a8cfd9901(signed mergec201755ec).f48369a7baddstestAgcOffLevelCapture, a socket-free capture check.
Verified empirically (Linux, Qt 6, RelWithDebInfo, the merged tree)
hl2_tci_signaling_test,hl2_pan_create_async_testandradio_state_memory_testpass.- Full socket-free ctest: 507 of 508. The one failure is
connection_panel_size_test, the known local failure (#5811). The 123 tests whose sources open or drive a socket were excluded. - Ten mutations, each red, each restored and sha256-checked: RadioStateMemory load, store and gate; capture in
requestSliceAgc(); report incurrentOperatingState(); load inapplyRestoredState(); pan-seed level; pan held AGC; capture fallback; the mode-clickfollowModeAgccall. - Static gates (engine boundary, touchpoint manifest, capability records, command plane) give identical output on main
a8cfd9901and on the head. No ratchet moved. All commits are verified.
Reviewed from code only, not run
hl2_state_restore_test: the restore-into-DIGU case, the second link-up, the seed in DIGL, and the off-level store→load→connect round trip. It binds a UDP socket throughMetisClientand was not authorised to run.- Nothing ran on an HL2 radio, and there was no app session: the demo's
SimBackendnever runsHl2Backend.
Freshness. The CI run on this head was created at 2026-10-04T02:25:37Z. Main's tip is a8cfd9901 (01:53:44Z), which the head already contains, so the run builds against current main. Auto-merge waits for that run.
Conflict: src/core/dsp/WdspChannel.h only. This branch added the transmit Config fields txAmCarrierLevel and txFmDeviationHz where main (aethersdr#6119) added a comment on blockForOutput. Both are kept, main's comment as written. AETHER_HL2_TX_TXA and the phasing-modulator arm are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, AGENTS.md line count. Principle VIII. - CTR2: the intro, README highlight and Roadmap line, and ROADMAP entries say Wi-Fi works today and USB's host side is ready, has carried the radio's status stream to development firmware, and waits on a CTR2 USB firmware release; the CHANGELOG bullet notes USB is the default and nothing persists, so current firmware needs Wi-Fi chosen each launch. - #6119's upgrade note carries the open #6142 bookmark exception. - AGENTS.md is 300 lines, not 301. - The 413 -> 369 command-plane drop credits #6049's and #6124's reroutes alongside the seam migration. Co-Authored-By: Claude Opus 5.5 (1M context) <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>
…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>
Fixes #6142 On a Hermes-Lite 2, recalling a band-stack bookmark into DIGU or DIGL no longer puts the bookmark's stored AGC mode over the data-mode AGC off. The bookmark's threshold still applies. The AGC held before entering the data mode still returns on leaving it. An AGC the operator selects while already in DIGU/DIGL stays until they leave. A BandStackEntry stores only the mode string, so a stale pre-#6119 value and an AGC the operator chose inside DIGU and then bookmarked cannot be told apart. Both recall to AGC off. The operator can select that mode again afterward. Stored entries are not rewritten; a stale one ages out on the next save, which reads the slice. SliceAgcRequest carries Origin { Operator, Recall }, default Operator. RadioModel::recallBandStackReceiveDsp is the one caller of SliceModel::recallAgcMode. Hl2Backend::requestSliceAgc drops a Recall mode while the receiver is in DIGU/DIGL and echoes the receiver's state synchronously, so the threshold recall that follows does not clear the held AGC. No other backend reads the origin. Flex still sends the stored agc_mode= text. Squashed-from: #6150 Co-authored-by: on8st <258096273+on8st@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Patrick Jensen <patjensen@gmail.com>
Maintainer ruling (2026-10-03): the default is accepted; the AGC-off level persists per receiver in this PR, capability-shaped, in the radio's settings document, with no schema bump; stale DIGU/DIGL band-stack bookmarks are follow-up #6142.
Refs #5629
DIGU and DIGL open with AGC off on the Hermes-Lite 2, and the AGC-off level that then sets their gain is remembered per receiver.
Limits first.
hl2family is touched. ANAN-G2 runs the same AGC and keeps the operator's mode; RTL-SDR offers no data mode. Neither was measured or changed.kSchemaVersionstays 2, as it did when the AGC pair was added (fix(hl2): persist the operator's AGC mode and threshold across restarts — Principle III. #4911). An older build that captures over the document drops the array, and the level is 10 again after a downgrade.hasAgcThresholdis documented as "threshold/off level". The field is read and written when the backend declares theAgcsettings domain andhasAgcThreshold. No new boolean (the capability-record count is frozen at 70).RadioModel::recallBandStackReceiveDsp). A bookmark saved after it stores off. Read from the source, not run.What changed
The default (
src/core/backends/hl2/Hl2Backend.cpp):One function,
Hl2Backend::followModeAgc, holds the rule; every site that writes a receiver's mode or seeds its AGC calls it.The remembered AGC pair stays the operator's. It is one flat setting for the radio. The data mode's "off" never reaches it:
setSliceAgc(), the seed and the capture all record the held mode instead.The AGC-off level, per receiver (the ruling):
OperatingStatedocument inradio_settings, scope(hl2, <MAC>), as one array:"agcOffLevels": [37, 52], one 0..100 entry per receiver index,-1for a receiver without one. No flatAppSettingskey.docs/agents/settings.mdnames one pipeline for state the radio cannot hold: a typed field inRestoredRadioState, written and read only byRadioStateMemory, gated by the backend's declaration. The AGC pair uses it, so the level sits beside the pair asRestoredRadioState::agcOffLevels. The other shape in that document, the per-domainextsub-objects, holds the per-band maps ofRfGainandTxSetpointsonly.RadioStateMemoryloads and stores the array only for a backend that declares theAgcdomain andhasAgcThreshold. There is no family check.Hl2Backend::requestSliceAgc()records the level for that receiver and asks for a capture;currentOperatingState()reports the list.applyRestoredState()validates each entry (outside 0..100 is dropped, not clamped) andseedReceiverAgc()gives each receiver its own entry at connect, where the AGC pair is seeded.pushInitialState()andfinishReceiverDspBuild()already push the receiver's level to WDSP (feat(hl2): route APF and the AGC-off level to WDSP. Principle VIII. #6050).createPanadapter()) takes the entry for its index, else the default. A receiver that is closed keeps its entry.settings.mdasks for one when a key is renamed or restructured; this is a new optional field.hasAgcThresholdbut not theAgcdomain, and RTL-SDR declares neither, soRadioStateMemoryhands them an empty list and writes no array for them. No backend other thanHl2Backendreads the field.Docs and comments:
docs/HERMES.md§13: item 30 is marked done with the measured result; item 26 no longer lists theWidebandtap as open; item 28 records that the HL2's tap-kinds answer isDemodonly.docs/HERMES.md§18.5: thetapparagraph states the measured mechanism, the level step of limit 2, that the level is remembered per receiver, and that the default is HL2-only.docs/HERMES.md§5 (AGC): a paragraph on the per-receiver AGC-off level: field, document, gate, seed, validation.docs/architecture/radio-capabilities-map.md: theclientSettingsDomainsrow names the field, its gate and which families ignore it.src/core/backends/hl2/Hl2RxDsp.handsrc/core/dsp/WdspChannel.h: theblockForOutputcomments no longer claim a burst feed is deterministic.Tests
All without a radio.
hl2_tci_signaling_test,testDigitalModeDefaultAgchl2_tci_signaling_test,testAgcOffLevelCapture{20, 61}, receiver 0 set to 37 → one capture request and the capture{37, 61}hl2_state_restore_test(default)hl2_state_restore_test(off level)currentOperatingState()→RadioStateMemory::store→ a new backend →RadioStateMemory::load→applyRestoredState()→ connect: each receiver publishes its own level, each WDSP channel reports the matching fixed gain (dspChains(), what the channel accepted), and the next capture is unchanged. Also: a change asks for a capture; a radio swap does not carry the levels across; a document without the array gives 10 on both receivers and 10 dB in the channel; an out-of-range entry falls to the default and its neighbour holdsradio_state_memory_testOperatingStatedocument; a backend withouthasAgcThresholdis handed none and still gets the AGC pair; a backend without theAgcdomain is handed none; a store without the capability writes no array; a document without the field loads; a state carrying only levels is not "nothing stored"hl2_pan_create_async_testFail-before, the off level. The new tests on
bbddff97's source, plus the one struct field they need to compile (a throwaway commit, full suite): ten checks red.Two mutations of the head, each in a full-suite run:
seedReceiverAgc()does not write the receiver's level (the restore)hl2_state_restore_test: the restore, the WDSP gain, the second capture, the out-of-range casecurrentOperatingState()does not report the levels (the capture)hl2_state_restore_test: the capture, the capture before connect, and the three restore checks that depend on itFail-before, the default (earlier heads): the mode-change checks were red on main when the PR was opened (5 of 12); restore into DIGU, the seed in DIGL and the pan seed were red on
540e149e's source.Full suite at the head (macOS, Apple Silicon, Qt 6.12.0, RelWithDebInfo): 612 of 615 at
-j8, the same three on the serial rerun. None is from this PR:hl2_pan_create_async_test, one check, "the reuser has its own chain" (fix(hl2): drop spectrum and meter output from a closed receiver's DSP. Principle VIII. #6113). It fails on clean main at this branch's merge based9de044c, and passes in the tree merged with main at5fae2f86.peripheral_auth_dialog_test(feat(peripherals): authenticate direct TGXL, PGXL, and Antenna Genius connections #6008). It fails on clean main atd9de044cand in the merged tree.vkamp_connection_test, one check, "out-of-range antenna ports never reach the wire": the Replace retained socket fixtures with injected and opt-in coverage #5254 race in the test. It passed in the run of the same sources one commit earlier (the last commit is docs only).The tree merged with main at
5fae2f86(a throwaway merge, not pushed): 616 of 617,peripheral_auth_dialog_testonly.git merge-treeagainst main at52b6f7adis clean; that tree was not built.Static checks, every step of the "Static checks" job of
static-checks.ymlrun locally at the head: all pass, including the capability-record convention (70 booleans, unchanged) and the QSettings ratchet. Nosrc/guifile is touched, so the a11y step has nothing to scan. The "Sanitizer option configures" job was not re-run for this head; no CMake file changed.Not verified
RadioStateMemoryand the settings store in one process, with a new backend object.RadioModel's debounced capture and its flush on disconnect and on quit are the existing path for every field and were not driven for this one.a8cfd9901: the socket-free suite, 507 of 508, the one failure the known localconnection_panel_size_test, Four tests fail deterministically on clean main locally: the a11y announcement fixture never receives focus (offscreen) #5811). Windows only through CI.73, ON8ST
🤖 Generated with Claude Code