Repository navigation
fix(hl2): remember TUNE power, FFT AVG, the receiver set and every receiver's setpoints. Principle III. - #6259
Conversation
There was a problem hiding this comment.
Reviewed at 4653edf against the PR head checkout. All five checks are green on that SHA (build, check-windows, check-macos, Static checks, Sanitizer option configures) — noting as always that every ctest call in ci.yml is -R-filtered, so green is not "the suite passed".
1. Issue fit
There is no Fixes line. The body references #5777 (receivers B/C/D not persisted) and implements the answer on8st proposed there; the TUNE power, FFT AVG, notch and auto-gain-law parts have no linked issue and stand on Principle III ("a radio that stores nothing makes the client its memory"). I read #5777 and its full thread: the triage agent and on8st both asked for a ruling (09-30, re-asked 10-04), the issue still carries maintainer-review, and no maintainer has answered. The body is honest about this. So: the non-Receivers work is reviewable on its merits and solves what it claims; the Receivers domain is a correct implementation of an unratified decision — see the recommendation.
Each claim I could test from source held. One I checked specifically because it is the headline of the FFT AVG row: flipping clientPersistsAveraging really does engage an existing, working path — MainWindow_Wiring.cpp:3206-3221 (save) and :3296-3306 (restore) are both gated on shapesDisplayRatesLocally() && clientPersistsAveraging, which is true for the HL2, and Hl2Backend::emitPanState (:8446-8459) emits only centre/bandwidth, so pan->average() stays -1 and the disk branch is actually reached rather than losing to a live model value. Single save site, single restore site, one key — no second owner, so Constitution V holds.
2. Scope
| File(s) | What it changes | Claimed in the body? | Verdict |
|---|---|---|---|
src/core/backends/hl2/Hl2Backend.{h,cpp} |
TUNE-power band memory, Receivers/Notches capture + replay, createPanadapter()→addReceiver(seed), auto-gain law/floor persistence, nbEchoPending |
yes | in scope |
src/core/backends/RadioCapabilities.h |
two new ClientSettingsDomain bits (Receivers, Notches) |
yes | in scope — new maintainer-visible surface (see below) |
src/core/RadioStateMemory.cpp |
gateExtension() table shared by load()/store() |
yes | in scope; a genuine de-duplication of the PR #4614 gate |
src/core/backends/IRadioBackend.h |
setTunePower() contract comment only |
yes | in scope |
src/models/RadioModel.cpp |
widened tunePowerCommandIssued gate + comment |
yes | in scope |
src/gui/MainWindow_SwrSweep.cpp |
restore TUNE power before the frequency | yes | in scope, and necessary — see §5 |
docs/HERMES.md, docs/architecture/radio-capabilities-map.md |
the two caps rows + the receiver-count paragraph | yes | in scope; the caps-map row is the required doc half of a capability change |
tests/hl2_session_memory_test.cpp, tests/tests.cmake |
new 13-case test, registered at tests.cmake:1584-1587 |
yes | in scope |
tests/transmit_seam_drop_notice_test.cpp |
widened for the new forwarding | yes | in scope |
Nothing in the diff is unexplained by the body. No CHANGELOG.md entry (correct). No socket, no fake peer, no Fake* radio class added — I grepped the PR's own test source for QTcpServer/QUdpSocket/bind(/listen(/connectToHost/Fake and it is empty, so there is nothing to disclose on that axis.
For the maintainer to rule on, not a violation: two new ClientSettingsDomain flags, and six new keys inside the HL2-owned ext sub-objects (txSetpoints.tuneByBand, tuneDefaultPercent, rfGain.autoLaw, autoFloorDb, plus the receivers and notches arrays) with no extensionSchemaVersion bump. The no-bump reasoning holds on inspection: store() rebuilds ext from scratch, so an older build simply drops keys it does not write rather than leaving a half-migrated document, and an absent array restores today's behaviour. These are not protocol surface and not third-party-visible. The capability ratchet is unaffected — it counts bools, and these are enum bits.
3. Blockers
1. src/core/backends/hl2/Hl2Backend.cpp:7139 — a truncated replay erases the receivers that did not come back, permanently.
if (!addReceiver(seed)) {
qCInfo(lcHl2) << "HL2 restore:" << (m_restoredReceivers.size() - i)
<< "remembered receiver(s) not reopened";
break;
}replayRestoredSession() clears the guard flag on its first line (:7116, m_sessionReplayPending = false;) and only then opens receivers. Each successful addReceiver() ends with notifyOperatingStateChanged() (:1077), which reaches RadioModel::scheduleOperatingStateSave() — and isConnected() is already true, because emit connected() ran at :501 before the replay. So the debounced store lands a moment after the loop with m_sessionReplayPending == false and m_rx holding only the receivers that fit. In currentOperatingState() the protective branch
// Same for the receivers while none are running or B.. are not yet back.
if (m_rx.empty() || m_sessionReplayPending) {(:7276-7284) cannot fire — the flag it tests was cleared before the first add — so receivers is rebuilt from the short live list and the remainder is gone from disk. The next connect, even one with room, has nothing to restore.
Failure scenario, reasoned from code rather than run: a skimmer board (m_boardMaxRx 9-12) with six receivers stored. The operator next connects by IP, the unicast discovery probe at :2331-2342 is dropped or answers short, and :2350 falls back to m_boardMaxRx = kAssumedBoardMaxRx (4). The replay reopens four, logs 2 remembered receiver(s) not reopened — and the capture it just triggered rewrites receivers with four entries. The two skimmer receivers are not recoverable. The same erasure follows any openReceiverDsp() failure mid-replay (:1052, WDSP channel pool), which also returns false into this break.
On a shipping 4-RX board with a self-consistent span this will not fire: I checked that applyPanBandwidth refuses a zoom the running receivers cannot carry (:3790-3800), so a stored (count, span) pair is normally reachable again. That is what keeps this out of the common path — not the code.
The fix is a judgment call rather than a one-liner, because the tail must not be kept after an operator close (removePanadapter) or the capture would resurrect a receiver they deliberately shut. The shape I would suggest: have the replay record what it could not open — e.g. a std::vector<Receiver> m_replayUnreopened filled from m_restoredReceivers.begin() + i at the break — append it to receivers in currentOperatingState(), and clear it in removePanadapter() and in addReceiver() once the operator adds one back. The doc sentence this PR adds ("the receiver ceiling at the current span still decides how many return") then becomes true across sessions, which is what it currently implies and does not deliver.
The test that would have caught it is already most of the way there — tests/hl2_session_memory_test.cpp:355-367 asserts the surviving count equals Access::ceiling(backend) but never re-reads currentOperatingState() afterwards. One extra check on the stored array size in that case pins the fix.
4. Nits (non-blocking)
src/core/backends/hl2/Hl2Backend.cpp:5226— TUNE power goes stale after a band change under the carrier, and the next key-down overwrites the new band's memory.applyTuneMemoryFor()correctly refuses whilem_tuning(if (m_tuning || tune < 0 || tune == m_tunePowerPercent) return;), and the test pins that refusal — but nothing re-applies on unkey. Keyed TUNE on 40 m at 30 → QSY to 20 m (remembered 55) → unkey: the slider still reads 30, and the next TUNE records 30 over 20 m's 55 viarecordTunePower()at:5171. Drop-in, in the unkey branch at:5181-5183:
} else {
setKeying(false, operation, completion); // clears m_tuning and restores the operator's RF power
setTxTestTone(0.0, 0.0, operation);
// A band change under the carrier left the new band's own TUNE power
// unapplied (applyTuneMemoryFor refuses while m_tuning). The carrier is
// down now, so bring it back — otherwise the next key-down runs, and
// records, the previous band's value on this band.
applyTuneMemoryFor(m_currentBandKey);
}-
src/core/backends/hl2/Hl2Backend.cpp:7137— anumRx > 1connect leaves the pre-opened receivers without their remembered setpoints. The replay loop starts atm_rx.size(), which is the documented intent, but only receiver A getsseedFirstReceiverFromMemory()(:2407). WithnumRx=2from automation, receiver B comes up on A's inherited state and its remembered blanker/APF/squelch/RIT/audio are silently skipped — then captured over. Automation-only, so low severity; worth a sentence in the comment at minimum. -
src/core/backends/hl2/Hl2Backend.cpp:7268-7275(notches) /:7284(receivers) — asymmetric emptiness.notchesis always inserted (an empty array is written);receiversis inserted only when non-empty. Both are harmless givengateExtension(), but the asymmetry will read as accidental to the next person. -
tests/hl2_session_memory_test.cpp:191— receiver A's extras are exercised by calling the private seed directly (Access::seedFirstReceiver), not throughconnectRadio(). The seed function itself is well covered; what is not covered anywhere is the gate that decides whether it runs (request.serial != m_agcSeededSerial || !m_rxCarriedState,:2404), i.e. the "plain reconnect keeps the live values" half of the design. Hardware run 1 covered the positive case.
5. What I tried to break (and could not)
- Sibling
setTunePower()implementations. The widenedRadioModelgate (RadioModel.cpp:2216-2225) now forwards unkeyed to any backend declaringtunePowerAppliesLive. Only two declare it — HL2 (Hl2Backend.cpp:1910) and Icom (IcomCivBackend.cpp:376); ANAN and Flex are explicitly false, andIcomCivBackend::setTunePower(:5117) returns early when not tuning. So no backend gains an unkeyed drive write, and the decoded-TUNE refusal case intransmit_seam_drop_notice_testis correctly left unmodified (isTuning() && !tuneCarrierLive()still returns). No Principle VI exposure here. - The
m_rx[i] == DDC iassumption that both thetransmitflag (:7259,i == m_txDdc) and the replay'sm_ids.byDdc(m_restoredTxReceiver)rest on.rx(ddc)is&m_rx[ddc](:765), andremovePanadapter()compacts:m_rx.erase(begin()+ddc)plusm_ids.remove(ddc)renumbers, withm_txDdcandm_activeDdcmoved explicitly (:1313-1329). A middle close does not strand the TX flag. TheA+B-only hardware run did not cover this; the code does. - An echo loop on TUNE power.
applyTuneMemoryFor()assignsm_tunePowerPercentbefore emitting, so aTransmitModelround trip back throughsetTunePower()hitsrecordTunePower()'s equality guard and dies there. No oscillation and no spurious band write. - The SWR sweep reorder. My first read was that it moves the record onto the wrong band, so I checked what the sweep spans:
MainWindow_SwrSweep.cpp:312-344confines a sweep to the current band and:162aborts it on a band change, so swept band == original band and restoring the power before the frequency is both safe and the only ordering that records the operator's value on the right band. The aborted-by-disconnect leak is real and the body already discloses it. - A stale overwrite on disconnect.
flushPendingOperatingStateonly writes when a timer is pending, andemit disconnected()fires from the linkDown handler (:495-501) withm_rxintact —disconnectRadio()routes through the same place (:2842) — so the flush captures the live receivers, not the connect-timem_restoredReceivers. Them_rx.empty()fallback incurrentOperatingState()is genuinely for the torn-down case. - The gating negative.
hl2_session_memory_test.cpp:491-496clears theReceiversbit and asserts the list is withheld on load, so the new domains have the gating test the capability contract asks for, not just the declaration and the doc row. - A second owner for FFT AVG. Checked after seeing #5980's "#5754's stored FFT AVG" line:
ClientDisplaySettings::saveFftAverage/fftAveragehave exactly one call site each and the HL2 keeps nothing of its own, so the flip adds an owner where there was none rather than a competing one. #5782 ruled on where the averaging happens, not on persistence;falsefor HL2 was the shipped default from the RTL work that introduced the field (a325fb99e), not a ruling. I do not read the flip as a preference change needing its own issue — it matchespanFrameRateShaping, which the HL2 already owns for the same reason. - Tests attacked rather than counted. The new test drives real
Hl2Backendobjects through an injected link edge, and the mutation table's rows map to assertions I can find in the file — it is not asserting the implementation back to itself. Everything above is reasoned from the source in the head checkout; I have no build and ran nothing.
6. Recommendation
Needs maintainer decision, on two separable questions, plus one blocker to fix either way.
The blocker (§3) is independent of the ruling and should be fixed before merge — it silently destroys exactly the state this PR exists to keep, on the path the code itself writes a log line for. The TUNE power, FFT AVG, notch and auto-gain-law work is, in my reading, clean, well-tested, correctly capability-shaped, and solid Principle III; I would take it on its own.
The Receivers domain still has no answer to the question #5777 has been asking since 09-30. The body's claim that it can come out alone holds — m_restoredReceivers, m_restoredTxReceiver, seedFirstReceiverFromMemory(), the addReceiver(seed) refactor, the transmit flag and the receiver-A flat-field override are all on that side of the line, with Notches independent — so if the ruling is no, the surgery is real but bounded. Concrete next step: fix the truncated-replay erasure and the unkey nit, and ping #5777 for the ruling; the rest of the PR does not have to wait on it.
Thanks for the unusually legible write-up — the mutation table and the two hardware runs made this far faster to review than a PR this size normally is, and the honest "not verified" list pointed me straight at the one place the design actually leaks.
🤖 aethersdr-agent · cost: $15.9290 · model: claude-opus-5
…ceiver's setpoints. Principle III. A Hermes-Lite 2 stores nothing, so the client is its memory. These HL2 settings were lost on every restart: - TUNE power: per band, with a first-set baseline like drive (txSetpoints.tuneByBand / tuneDefaultPercent). RadioModel now forwards an unkeyed TUNE power change to a live-declaring backend so it can be recorded; a TUNE decoded off the radio still forwards nothing. - FFT AVG and Wt Avg: the HL2 declares clientPersistsAveraging, so the existing per-radio ClientDisplay.fftAverages table covers it. - Receivers B.. (#5777): a new Receivers domain. Each receiver's frequency, pan centre, mode, passband, blanker, APF, squelch, RIT/XIT and audio gain/pan, receiver A first, plus which one transmits. After link-up replayRestoredSession() reopens B.. through the operator's add path (receiver ceiling still applies) and moves transmit back. - Notches: a new Notches domain, recreated through createNotch() so each comes back with a marker. - Auto RF gain law and floor, when the operator chose them. The SWR sweep restores TUNE power before the frequency, so a per-band radio records it on the swept band. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…wer at unkey. Principle III. Review of #6259: - A replay cut short by the receiver ceiling (or a failed open) no longer erases the rest: m_replayUnreopened is captured after the live receivers and dropped once the operator adds or closes one. - A band changed under a TUNE carrier applies its own TUNE power at unkey. - Receivers a numRx connect param opened take their own remembered entries (seedOpenReceiversFromMemory), not receiver A's. - `receivers` is always written, like `notches`. - Tests: the stored list after a short replay and after an operator close, the numRx seed, the unkey, and the connectRadio() gate that keeps a same- radio reconnect's live values. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
4653edf to
646e256
Compare
|
Thanks for the careful pass. The blocker was real. Addressed in
Each new guard was mutated and goes red; the table in the body has the rows. The other nine HL2/RadioStateMemory tests are still green locally. On #5777: the |
jensenpat
left a comment
There was a problem hiding this comment.
Verdict
Approve. Reviewed 646e2564528a4a524ed24a1bc51ac4e3ef6f7e8f. Issue fit is good: this is the HL2 client-memory gap in #5777, plus the TUNE-power, FFT AVG, notch, and auto-RF-gain law/floor setpoints the body lists. Scope stays inside that. Merging accepts #5777's proposed answer (restore each receiver's state, not a persisted count). The issue is not in closingIssuesReferences, so it will stay open.
RadioModel seam: this does not cross it. The only RadioModel edit is the tunePowerCommandIssued forward. It still calls IRadioBackend::setTunePower only when transmitDriveControl->tunePowerAppliesLive is set, and it still returns before that call when TUNE is on but the carrier is not this client's (isTuning() && !tuneCarrierLive()). There is no new family-string branch, no new vendor include, no Flex wire text, and no key. The HL2 sentence in the comment is motivation; the gate is the capability.
Who the widened unkeyed call reaches:
| Family | tunePowerAppliesLive |
Unkeyed setTunePower |
|---|---|---|
| HL2 | true | records the band; drive is applied only while this client's carrier is admitted |
| Icom | true | IcomCivBackend::setTunePower returns immediately when !m_tuning, so no CI-V write |
| Flex, ANAN | false | not called |
| Sim, RTL | record absent | not called |
Receivers and Notches are new ClientSettingsDomain bits. RadioStateMemory::gateExtension() withholds them unless declared. Flex, Sim, and Icom still declare an empty domain set. ANAN and RTL do not declare either new bit. The extension contents stay inside Hl2Backend.
setTxSlice() during replay moves ownership and band memory. It does not call setKeying.
Scope
| Group | Files | What it does |
|---|---|---|
| Seam contract | IRadioBackend.h, src/models/RadioModel.cpp |
Unkeyed TUNE power is forwarded only to a live-declaring backend |
| Domain gate | RadioCapabilities.h, RadioStateMemory.cpp |
Receivers / Notches sub-objects, same gate on load and store |
| HL2 memory | Hl2Backend.cpp, Hl2Backend.h |
Per-band TUNE power, chosen auto-gain law/floor, receiver and notch replay |
| Sweep order | MainWindow_SwrSweep.cpp |
Restores TUNE power before the frequency, while still on the swept band |
| Docs | docs/HERMES.md, docs/architecture/radio-capabilities-map.md |
Matches the new declarations |
| Tests | tests/hl2_session_memory_test.cpp, tests/transmit_seam_drop_notice_test.cpp, tests/tests.cmake |
Socket-free; registered |
No CHANGELOG.md edit. The prior review's truncated-replay erasure and the unkey TUNE-power gap are fixed in this commit (m_replayUnreopened, applyTuneMemoryFor on unkey).
Verification
Required checks on this SHA are green: build, check-windows, check-macos, Static checks. Those jobs do not execute this new test (the per-PR ctest gate is frozen). Local, Qt 6.12.0, QT_QPA_PLATFORM=offscreen, worktree /tmp/aether-grokreview-6259 at 646e25645:
transmit_seam_drop_notice_testpassedhl2_session_memory_testpassed
The new test injects MetisClient::linkUp. It does not construct or bind a socket (start() is what binds). Icom's unkeyed no-op is from reading IcomCivBackend::setTunePower, not from an Icom test run.
Non-blocking
- A ceiling-truncated replay keeps the receivers it could not open, but
currentOperatingState()stampstransmitonly on the livem_txDdc. If transmit was on a receiver past the ceiling, the next capture moves that flag onto receiver A. See the inline. - An SWR sweep aborted by a band change (
preserveBandSwitchOnFinish) now records the original TUNE power onto the destination band, because unkeyedsetTunePowerreaches HL2. Same-band finish is the path the reorder fixes. The body already discloses the disconnect/preserve case for sweep power.
Non-blocking test follow-up
The transmit-flag-on-an-unreopened-tail case above is not asserted. A useful addition on the existing ceiling case: mark transmit on the last remembered receiver, capture after the short replay, and check the flag is still on that entry. HL2 only. Not a merge prerequisite.
… Principle VIII. An audit of every page against the 106 PRs in v26.10.2, each fact checked against the source at the release commit: - Menu paths: AetherMap, AetherModem, Callsign Lookup and FreeDV Reporter are on View, and Waveforms is on Tools. - New coverage: Windows Firewall rules and the Network Diagnostics page (#6289), live CW contacts (#6268), NB2 on the HL2 and ANAN-G2 (#5966), ANAN-G2 AGC-T and per-radio memory (#6282, #6129), HL2 state restore and its level changes (#6259, #6254, #5975), DVK import (#6247, #6262, #6251), Copy Assist from unprocessed audio (#6216), keyboard-operable status-bar indicators (#6267), the KPA1500 (#5905), CTR2 extensions and the AetherKnob (#6338), live PPM (#6082), and Icom COMP and the squelch scale (#6174, #6184). - Known issues whose GitHub issues are closed are removed. - The style guide now asks for US spelling, and every page follows it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… and re-take the Stable snapshot. Principle VIII. (#6396) ## Summary This brings the user guide (docs.aethersdr.com) up to date with everything in v26.10.2 and re-takes the Stable snapshot so the default site shows it. Docs only, apart from the TCI page prose in `tools/docs/gen_reference.py`; no application source, test or CI changes. Every page was audited against the 106 PRs in the release. Every fact that was added or kept was checked against the source at `f753dbfc4`, the v26.10.2 release commit, not taken from the PR bodies alone. ## What changed - **Menu paths.** - AetherModem, Callsign Lookup, AetherMap and FreeDV Reporter are on **Tools**. They are created on View and then moved to Tools (`MainWindow_Menus.cpp:1519-1520`, `:1569-1575`); the style guide's example says **Tools → AetherMap...**. - **Show live CW contacts** is on **View**. - Waveforms is on **Tools**, not File. - The VUDU applet button was called TXDSP. - Labels now match the source's `...` vs `…`. - **Gaps from this cycle.** - **Windows and Network Diagnostics:** Windows Firewall rules and the Network Diagnostics Windows Firewall page (#6289); the `aether.network` and `aether.nvafx` log categories (#6289, #6308). - **Remote access:** Tailscale shim revocation semantics (#6325). - **Sleep, App Nap and power throttling (#6195):** per-OS notes. - **Hermes-Lite 2:** state restore, RX audio at unity, CW at TUNE level, multi-interface discovery and a discovery troubleshooting entry (#6259, #6254, #5975, #6009, #6293). - **ANAN-G2:** AGC-T range, a "What is remembered" section, and supply voltage and link status (#6282, #6129, #6137, #6211). - **NB2** on the HL2 and ANAN-G2 (#5966). - **DVK:** WAV import requirements, the slot menu without Delete, status text and licensing (#6247, #6262, #6251). - **Copy Assist** from unprocessed audio (#6216). - **Live CW contacts** replace the decoder's contact card (#6268). - **Status-bar indicators** work from the keyboard (#6267). - **Volume:** controllers, hotkeys and TCI `volume` follow the active output (#6129, #6260). - **Peripherals:** the KPA1500 (#5905), CTR2 link extensions and the AetherKnob (#6338), TMate 2 udev (#6250), MIDI RADE Modem and FDV modes (#6031, #6038). - **RTL-SDR:** live PPM (#6082). - **Icom:** COMP and the MK2 squelch scale (#6174, #6184). - **Known issues.** Entries whose GitHub issues are closed are removed (each checked with `gh issue view`). As decided, this includes three closed today without a demonstrated fix: #5107, #4824 and #5665. - **US spelling.** `docs-style-guide.md` asked for British spelling; it now asks for US, by the maintainer's decision, and every page is converted. The generated TCI pages are converted at their source, the prose strings in `tools/docs/gen_reference.py`. Code spans, links and URLs were left alone. UI labels were checked against the source strings, and none of them are British. Two in-page anchors and two Log Analyzer rule anchors (`#bnr-is-grayed-out-…`) follow the renamed headings. - **Stable snapshot.** `snapshot_stable.py 26.10.2` was re-run on the release tree with these corrections, before `main` was merged in, so Stable carries no post-release docs. `docs/` also has `main`'s post-release pages, in US spelling. Stable alone keeps the IC-9700 waterfall-rate entry (#5996): #6402 fixed it after the 26.10.2 tag, so 26.10.2 still has it. The PDF manual already attached to the v26.10.2 release keeps the tag's docs. ## Validation - `npm ci && npm run build` in `docs/user`: success. This is the broken-link and broken-anchor check. - `node tools/docs/test_log_rules.mjs`: 707 checks passed for 45 rules. - `python3 tools/docs/gen_reference.py --check`: up to date (8 pages). - `git diff --check`: clean. - Menu paths on a build of this branch (offscreen, isolated settings, no radio), `menu list`: `Tools → AetherModem...`, `Tools → Callsign Lookup...`, `Tools → AetherMap...`, `Tools → FreeDV Reporter...`, `Tools → Waveforms...`, `View → Show live CW contacts`. ## Screenshots now out of date (not recaptured) These need a recapture pass on the radio: - **Old panadapter title strip (#6386):** `panadapter`, `main-window`, `first-session-*`, `multi-slice-two-slices`, `split-pair`, `tnf-markers`, `offscreen-vfo-indicator`, `pan-3d-stacked-trace`, `default-light-theme`. - **Pre-canon windows (#6334, #6339, #6341, #6372):** `aetherrx*`, `aethertx*`, `aethermodem-aprs`, `bnr-panel`, `connect-dialog`, `connect-by-ip-card`, `connect-icom-rows`, `connect-smartlink-card`, `rx-agc-calibration`. - **Other:** - `title-bar` (lowercase tab states, #6281); - `menu-help` (#6294); - `display-panel` (#6371); - `support-diagnostics-dialog`; - `waveforms-dialog` and `tailnet-remote-access` (show shim 0.4.0); - `about-window` (v26.10.1); - `radio-setup-qrz`. ## Found during the audit, not fixed here - **App string bug:** `TailnetShimDialog.cpp:628` tells the user to "Connect remotely with Connect → Manual"; the real path is **File → Connect to Radio... → Connect by IP**. - **ANAN AGC-T tooltips:** the RX applet and VFO tooltips show the slider position as dB, which is wrong on ANAN. #6282 lists this as a follow-up. - **MIDI Master Volume on non-Flex radios:** it is refused while PC Audio is off, whereas hotkeys, FlexControl and HID drive the line out. The docs describe the code as it is. - **RTL mode names:** `rtl-sdr.md` writes "FM-N" and "CW-R", but the source strings are `FMN` and `CWR`. Left as is until someone confirms what the mode menu displays. - **IC-705 COMP readback:** inferred from the MK2 (#6174) and not verified on an IC-705. - **Not yet documented:** the #6230 Radio Health row "Forward responses skipped", and the Store/MSIX package. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
A Hermes-Lite 2 stores no settings at all, so the client has to be its memory. These HL2 settings were lost on every restart:
OperatingState.ext.txSetpoints.tuneByBand, per band, with a first-set baselinetuneDefaultPercent(same rule as drive)ClientDisplay.fftAveragestable. The HL2 now declaresBackendPanAveraging::clientPersistsAveragingReceiversdomain:ext.receivers, receiver A first, with the transmit receiver markedreceiversentriesNotchesdomain:ext.notches(centre, width, active)ext.rfGain.autoLaw/autoFloorDb, written only when the operator chose themBefore this PR, RF drive, LNA, frequency, mode, passband, AGC, mic level and CW were already kept. FFT FPS and the dBm range are kept since #6140.
#5777 needs a ruling first. on8st asked the maintainers there whether an HL2 should restore its receiver set, and nobody has answered yet. This PR implements the answer proposed in that thread: no persisted count, because #4545's reason still holds. Each receiver's own state is stored instead, and after link-up those receivers are reopened through
createPanadapter()'s own add path, soreceiverCeiling()at the current span still decides how many come back. If the ruling is no, theReceiverspart can come out on its own. The rest of the PR does not depend on it.Design
Seam:
setTunePower().RadioModelnow also forwards a TUNE power change while unkeyed, to a backend that declarestunePowerAppliesLive. HL2 records it per band. Icom already returns early when!m_tuning, so Icom behaviour is unchanged. A TUNE decoded off the radio, i.e. not keyed by this client, still forwards nothing, and the drop-notice logic is untouched. Contract comment updated inIRadioBackend.h.Replay.
connectRadio()armsm_sessionReplayPending. The linkUp handler runsreplayRestoredSession()once, after receiver A is published:createNotch()(fresh ids, markers reachTnfModel);addReceiver(seed).createPanadapter()is nowaddReceiver()with a seed copied from A, so the operator's path and the replay share one code path;setTxSlice(). That is ownership only, never a key.m_replayUnreopened) and captured after the live ones, so a later connect with room still has them. They are dropped once the operator adds or closes a receiver.A linkUp after EP6 silence replays nothing. If an explicit
numRxparam already opened receivers, the replay starts after them.Receivers opened at connect take their own entries (
seedOpenReceiversFromMemory): receiver A its extras, and any receiver an explicitnumRxparam opened its whole entry.Receiver A takes its extras (blanker, APF, squelch, RIT/XIT, audio, pan centre) in
connectRadio(), under the same "new radio or rebuilt receivers" condition asseedReceiverAgc(). A plain reconnect keeps the live values. A's frequency, mode and passband come from its own entry rather than the flat fields, because the flat fields follow the TX receiver.AGC stays flat, as
seedReceiverAgc()documents. It is not stored per receiver.Validation (Principle VII). Each field is checked on its own and a bad value falls to its default, never clamped. An entry with no usable frequency or mode ends the list, so C never moves into B's slot.
restorableRfHz()andrestorablePassband()are now the one rule for the flat fields and the entries alike.Gating.
RadioStateMemorygates the extension through one table,gateExtension(), shared byload()andstore(). A backend that does not declareReceiversorNotchesnever sees those keys. No schema bump: the contents of the extension sub-objects belong to the backend, and an older build simply drops the new keys on its next write.Capture before the replay keeps the remembered lists.
store()rebuilds the whole document, so otherwise they would be erased.NB echo.
emitSliceState()publishes the blanker once after a restore (nbEchoPending), not on every echo, becauseSliceModel::applyChanges()re-emits all three NB signals unconditionally.TUNE power under a carrier. A band change under a TUNE carrier does not apply band memory; the carrier keeps running at the value the slider shows, and the new band's own value applies at unkey.
SWR sweep now restores TUNE power before the frequency, so a per-band radio records it on the swept band.
Evidence
Unit tests (local, macOS 26, Qt 6.12.0). New:
hl2_session_memory_test, 14 cases, injected link edge, no socket. Updated:transmit_seam_drop_notice_testfor the widened forwarding. All green:hl2_session_memory_test,transmit_seam_drop_notice_test,hl2_state_restore_test,hl2_pan_create_async_test,hl2_backend_session_scope_test,hl2_auto_gain_law_test,radio_state_memory_test,hl2_backend_seam_test,hl2_tx_gate_test,hl2_gain_restore_test. Static checks run locally:check_capability_records,check_ci_test_gate,check_command_plane,check_engine_boundary,check_network_timeouts,check_qsettings,check_test_registration,gen_seam_probe_table --check,gen_touchpoint_manifest --check. All green.Mutations. Each guard was broken, the test run, then restored:
replayRestoredSession()at link-upnbEchoPendingnever clearedclientPersistsAveragingfalseReceiversmissing from the extension gatesetTunePower()does not recordRadioModelforwards only to a live carrier (old gate)RadioModelforwards under a decoded TUNEconnectRadio()twice, same serial)Hardware: a Hermes-Lite 2, gateware reporting 4 receivers, CL1 on an external 10 MHz. Driven through the automation bridge, with a normal quit and relaunch, and checked with
get slices,get transmit,notch list,get display,healthand theOperatingStaterow:ramp, floor 12. All of it came back after relaunch, and B came back with AGC off in DIGU. Log:HL2 restore: … tune bands 1 receivers 2 notches 2,added receiver — DDC 1 … running 2 of 4.transmit moves from DDC 0 to 1) with TUNE power 23.Not verified:
preserveBandSwitchOnFinish, or is aborted by a disconnect. The sweep power can then still be recorded for the swept band, because the seam cannot tell a sweep's TUNE power from the operator's.Not kept, on purpose: the global notch bypass (session state;
TnfModel::clear()resets it on disconnect and no seam signal echoes it back); per-receiver AGC (flat by design). Auto SQL mode is the applet's, and only the backend squelch pair is stored.CHANGELOG.mduntouched.rtl_wfm_pipeline_testfails to link locally withRtlSdrDdcsymbols missing. This PR touches nothing on that path.