Repository navigation
feat(anan): remember where the operator left the radio. Principle III. - #6129
Conversation
|
Thanks for this one, @tropo1234 — six real settings-persistence bugs with a mutation per fix and a quit-and-relaunch on actual G2 hardware is more rigour than most PRs get, and the note that two of them were only visible across a process restart is a genuinely useful observation about the limits of the suite. Only one check is red, and it's a small bookkeeping step rather than anything wrong with the code. ( What failedStatic checks → I reproduced it at The new — so How to fix itNote the order —
With a tag added I ran exactly that locally and The tag itself is your call, not a mechanical one
which fits the header well. The thing that makes it a judgment call is One thing for the PR body
No Copilot or reviewer comments on the PR yet, and nothing here is CI infrastructure — the check is behaving correctly, it just hasn't been fed the new header's tag. 🤖 aethersdr-agent · cost: $4.3810 · model: claude-opus-5 |
8e6e449 to
bb45902
Compare
There was a problem hiding this comment.
1. Issue fit
There is no linked issue — no Fixes/Closes #NNNN in the body — so I reviewed against the PR's own stated intent. GOVERNANCE.md wants an RFC for architectural changes and explicitly does not require an issue for a bug fix with a clear root cause; this is a feat( PR bundling five client-persistence fixes plus a new core/ header and a new gui→engine touchpoint, which sits closer to the RFC side of that line. The author pre-flags the touchpoint as a maintainer call, which is the right instinct — see §5 below.
Against its own spec the PR delivers all six rows of its table. The five restore paths are real, the validation is refuse-not-clamp as claimed, and the "restore before the push" ordering in wirePanDisplayStatus() is genuinely placed where the comment says. But two of them are implemented against a family string rather than a declared capability, and canon has a sentence about exactly that. Details in Blockers.
2. Scope
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
core/AudioOutputVolumePolicy.h (new) |
PC-vs-radio volume key/default selectors + family predicate | Yes, incl. self-flagged touchpoint | New gui→engine touchpoint + new flat settings key → maintainer decision; see Blockers 1–2 |
core/ClientDisplaySettings.h |
FFT averaging + dBm range accessors, shared readInt/writeInt/loadForWrite/store |
Yes | In scope |
core/DeviceDiagnostics.cpp |
Two snapshots gain radio_output_volume_pct |
Implied | In scope (see nit) |
core/TciProtocol.cpp |
Init burst + volume GET read the active sink |
Implied | In scope |
backends/anan/* |
Tuning + SpanRate domains, accept-lists, first-connect defaults | Yes | In scope; the 10 MHz → 14.175 MHz default change is disclosed → maintainer decision |
gui/MainWindow*.cpp/.h |
Volume key routing, connect-time lineout push, applyOperatorTuningStep() extraction |
Yes | In scope |
gui/MainWindow_Wiring.cpp |
Step seeding on onSliceAdded, display restore, dBm/avg saves |
Yes | In scope |
gui/SpectrumOverlayMenu.*, gui/TitleBar.cpp |
Signal-blocked setters, PC-key read at construction | Yes | In scope |
gui/MainWindow_Session.cpp:1763, MainWindow_Wiring.cpp:3015 |
Blank line only | No | Nit — unrelated whitespace churn |
tests/*, tests.cmake, docs/architecture/* |
New targets + manifest rows | Yes | In scope; registration verified |
Everything else in the diff is explained by the PR's stated intent. No CHANGELOG.md entry was added — correct. No socket tests added or removed; anan_backend_test's new --restore-check arm re-executes the test binary as a child process to prove disk persistence, which is a process boundary, not a socket.
3. Blockers
1. separatesRadioOutputLevel() is a family check deciding persistence, where canon requires a declared capability. (inline: AudioOutputVolumePolicy.h:52)
docs/agents/settings.md:143-145, under Settings Authority Policy:
The deciding test is whether THIS radio can save and restore the value — a declared capability, not a family assumption
return family.compare(QLatin1String("anan"), Qt::CaseInsensitive) == 0; is a family assumption, in a core/ header, selecting which key a setting persists under. The header comment argues the condition is "who owns the level" rather than "which vendor" — I agree that is the right question, and that is exactly why it should be a declared answer. This same PR gets it right three lines of diff away: clientSettingsDomains |= Tuning | SpanRate in AnanBackend::capabilities(). The dBm-range and averaging restores also gate correctly on radioOwnsDbmScale / backendPanAveraging. Only the volume split reaches for the string.
Fix: a RadioCapabilities field — clientOwnsReceiveOutputLevel, or an std::optional<ReceiveOutputLevelControl> record if you want the default value to travel with it. Note for the ratchet: an std::optional<…Control> record needs no FROZEN_BOOL_COUNT ruling, while a bare bool grows the count. The pre-existing family compare in MainWindow.cpp's PC-Audio toggle handler doesn't license a new one in core/.
2. RadioSpeakerVolume is a flat, app-global key holding per-radio state. (inline: AudioOutputVolumePolicy.h:33)
Same section of docs/agents/settings.md:150-154:
On a radio that persists nothing and declares so (HL2 today): the client IS the radio's memory — for exactly the declared domains, persisted ONLY through
RadioStateMemory'sOperatingStatedocument. Never in flatAppSettingskeys, never via ad-hoc code paths.
That is precisely the case the PR body makes for this value ("this client originates that audio and the radio has no output-level register"). MasterVolume is legitimately flat because the PC sink is app-global; RadioSpeakerVolume is not — it is one radio's output level. Concrete consequence, from the code rather than a bench run: two ANAN G2s on one profile share a single output level, and the first one connected writes the other's. Every other value in this PR is scoped per radio through RadioSettingsScope / RadioStateMemory, which makes this one the odd member of its own change.
Fix: carry it in the OperatingState document under its own declared domain, the way RfGain already rides in state.extension. That also dissolves Blocker 1 — the domain flag is the declared capability.
4. Nits (non-blocking)
- The weighted-average save and restore use different gates (inline,
MainWindow_Wiring.cpp:4564, with a suggestion). Save fires wheneverrequestLocalPanWeightedAverage()returns true, which is any non-Flex backend (RadioModel.cpp:6102→shapesDisplayRatesLocally()→m_flexBackend == nullptr). Restore gates onbackendPanAveraging.has_value(), which is ANAN and HL2 only. On Icom, RTL-SDR and the demo the flag is written to the per-radio document and never read back — a dead write. The siblingsaveFftAverage()eight lines up already uses the capability gate. radio_output_volume_pctis emitted unconditionally (DeviceDiagnostics.cpp:331,:430) but reads a key nothing writes on Flex, so every Flex support bundle will report0— which reads as a muted radio to whoever is triaging it. Gate it on the same predicate, or omit the field when it doesn't apply.- A zoom alone never arms the SpanRate capture. The body calls the live span "the whole point", and
beginRateChange()(AnanBackend.cpp:1080) is the function a zoom moves it through — but the threeemit operatingStateChanged()sites aresetSliceMode, the attenuation setter, andapplyTuneToRadioAndPan. A zoom followed by a clean quit is still captured by the disconnect flush, so the bench run would not show this; a zoom followed by a kill or crash loses the span. One line inbeginRateChange(). RadioSetupDialog.cpp:4759is a second writer to the same radio output level (m_model->setLineoutGain(v)) and knows nothing aboutRadioSpeakerVolume. After this change, a level set there is silently replaced at the next connect by the stored value — default0. Not a new loss (it previously reverted to the backend's 100), but the new destination is silent, and the two surfaces now disagree about who owns the number. Folding the level into the capability/domain from Blockers 1–2 would give both surfaces one writer.- With PC Audio on — the shipped default — the master slider cannot reach the radio output at all (
applyMasterVolume()is strictly either/or), while the connect handler now pushes it to0. Radio Setup is the only route. Worth a sentence in the body so the first G2 operator who files "my radio speaker went silent after updating" gets the answer from the release note. dbmRange()validates ordering indoublebut returnsfloat(ClientDisplaySettings.h:132-137) — two ordered doubles within float epsilon passlo < hiand collapse tomin == max, the empty scale the comment at:130exists to prevent. AetherSDR's own writer compares floats so it cannot produce such a pair; only a hand-edited document can. Correctness note, not a security issue. (Surfaced by security-audit (CodeGuard) and confirmed against the file.)- The dBm-range and averaging persistence is not ANAN-only. Both gate on capabilities (
radioOwnsDbmScale,backendPanAveraging), so HL2, RTL-SDR and the demo get them too. That is the correct shape per Principle III — but the body frames the whole change as a G2 fix, and the bench evidence is G2-only. Worth stating which other families this lands on. - Two whitespace-only hunks (
MainWindow_Session.cpp:1763,MainWindow_Wiring.cpp:3015).
Needs maintainer decision
- New gui→engine touchpoint (
core/AudioOutputVolumePolicy.h, 5 GUI includers, manifest 235 → 236). Self-flagged by the author with both alternatives spelled out. Resolving Blockers 1–2 likely removes the header entirely, so this may be moot. - First-connect default 10 MHz → 14.175 MHz (inline,
AnanBackend.h:120). Disclosed in both the body and the commit message, and the WWV-as-diagnostic-default argument is a reasonable one — but it is a user-visible default change with no issue citing the old default as a defect, which per GOVERNANCE.md is your call rather than the contributor's.
5. What I tried to break (and could not)
Findings below are reasoned from the head checkout at /tmp/aetherclaude/pr-6129; I have no build and ran nothing. The ${PR_DIFF} strings in the task's diff where & belongs are a harness artifact, not source corruption — the real file is fine.
- "A rejected document leaks the previous radio's dial onto the next radio." No:
RadioModel.cpp:660callsapplyRestoredState()unconditionally, with the comment saying why, andAnanBackend::applyRestoredState()assigns all three sentinels unconditionally. Radio A's dial cannot survive a swap to radio B. - "The restore is overwritten by the reclaimed-pan block 15 lines later." No:
PanadapterModelinitialisesm_average{-1}andm_weightedAverageKnown{false}, sopan->average() >= 0is false on a fresh pan, and on a reclaimed one the live value legitimately wins. - "The restore seeds the widget but not the pan stream" — the drag handler calls both
sw->setDbmRange()andsetStreamDbmRange(), the restore only the first. Harmless:m_panStreamis assigned only fromflex->panStream()(RadioModel.cpp:832), and the restore is gated on!radioOwnsDbmScale, so on every family that reaches it the stream is null. - "TCI
volumeGET and SET now read and write different keys." No: the SET branch setsm_pendingMasterVolume→TciServer:1559masterVolumeRequested→MainWindowapplyMasterVolume(), which writes through the samevolumeKey(). GET and SET stay symmetric, including the dB round-trip echo. - "
family()goes empty on disconnect, so a slider nudge while disconnected writes the radio level into the PC key." No:m_familyis assigned at connect and at backend injection and is never cleared, so the key stays correct across a disconnect. - Missing transitive includes —
std::isfinite,std::find,std::numeric_limitsare all covered:<cmath>,<algorithm>and<limits>are present in bothAnanBackend.cppand the test. No Linux-only compile break of the usual shape. - The restore tests are not self-confirming.
isRestorableFrequencyHzis pinned at0.0, negative, exactly Nyquist, 200 MHz, NaN and infinity, andisKnownModeStringhas a negative case; emptying either guard turns those red.audio_output_volume_policy_testdoes assertseparatesRadioOutputLevel("anan")true and("flex")false, so the "disable the output-level split" mutation claim is credible. - Where I could not test: the "found on the bench, not by the suite" claims — the dBm restore-ordering race and the step-seeding overwrite — are the author's; I confirmed the code now sits where the comments say, not that the bench run happened. The
MainWindowcall sites have no test target at all, which is why Blockers 1–2 matter more than they otherwise would: nothing would catch a regression there. CI is green on all 5 checks atbb459020, but perci.ymlthat is an-R-filtered subset, not the 621-test suite the body cites.
6. Recommendation
Request changes — on Blockers 1 and 2 only, and they are one fix. Moving the radio output level into a declared capability + the OperatingState document resolves both canon citations, dissolves the two-G2 collision, gives RadioSetupDialog and the master slider a single writer, and very likely removes the new touchpoint that the author asked you to rule on. Everything else in this PR is solid work: the refuse-don't-clamp validation is exactly what Principle VII asks for, the restore-before-push ordering is correctly placed and correctly explained, and the limits section is honest about what the bench did and did not prove.
Next step for the contributor: add the capability (an std::optional<…Control> record keeps the ratchet flat), route the level through RestoredRadioState, then re-run the nine mutations — the "disable the output-level split" one will need rewriting against the new gate. The nits can ride along in the same push.
Thanks for the unusually clear write-up — flagging the touchpoint yourself rather than burying it, and separating "measured" from "reasoned", made this much faster to review.
🤖 aethersdr-agent · cost: $16.5334 · model: claude-opus-5
bb45902 to
0476225
Compare
ten9876
left a comment
There was a problem hiding this comment.
Review of 04762258
Issue fit. No linked issue, so I reviewed this against the PR's own intent: ANAN-G2 state the client has to remember because the radio can't. The capability-shaped part is well built. Tuning, SpanRate and the new ReceiveOutputLevel domain all go through RadioStateMemory's OperatingState document. That resolves both blockers from the earlier automated review on bb459020 (AudioOutputVolumePolicy.h and the flat RadioSpeakerVolume key are both gone). The refuse-don't-clamp validation holds up. Four things stop it merging: a registered test fails at this head, Static checks is red, the volume rework breaks hotkey/controller volume with PC Audio off on every radio that doesn't declare the new domain (reproduced below), and the PR body describes an earlier version of the code.
Scope
| File / group | What it changes | Claimed? | Verdict |
|---|---|---|---|
backends/anan/AnanBackend.{h,cpp} |
Tuning + SpanRate + ReceiveOutputLevel domains, accept-lists, capture emits, first-connect default | Yes | In scope. The 10 MHz → 14.175 MHz USB default needs a maintainer decision (below) |
RadioCapabilities.h, RestoredRadioState.h, RadioStateMemory.{h,cpp} |
New ReceiveOutputLevel = 1u << 9 domain, schema 2 → 3 |
No. The body still describes a flat key plus a family predicate | In scope for the code, but undisclosed. See Blocker 4 |
models/RadioModel.cpp |
Seeds m_lineoutGain from the restored level |
No | In scope, no test (nit) |
gui/MainWindow*.cpp/.h, TitleBar.cpp, core/TciProtocol.cpp |
activeOutputVolumePercent(); MasterVolume written only with PC Audio on |
Partly | Regression, Blocker 3 |
gui/MainWindow.cpp → MainWindow_Controllers.cpp |
Step handler extracted to applyOperatorTuningStep(), moved to another TU |
Yes | Trips the command-plane ratchet, Blocker 2 |
gui/MainWindow_Shortcuts.cpp |
step_up/step_down now push to the slice, persist and toast |
Yes | Also lands on Flex (now sends slice set N step=). Probably a fix, but the body frames everything as ANAN-only (nit) |
gui/MainWindow_Wiring.cpp |
Step seeding on onSliceAdded; dBm-range restore/save |
Yes | In scope. The dBm range also lands on Icom (radioOwnsDbmScale=false), and step seeding on every radio with no command plane. Both undisclosed (nit). Step seeding needs a maintainer decision (below) |
core/ClientDisplaySettings.h + test |
dbmRange/saveDbmRange, loadForWrite/store |
Yes | In scope |
gui/SpectrumOverlayMenu.{h,cpp} |
setFftAverage() / setFftWeightedAverage() |
Implied by the "FFT averaging" row | Dead code. Nothing calls either (nit) |
core/DeviceDiagnostics.cpp:325-334, 427-432 |
Comment plus reformat, no behaviour change | No | Churn (nit) |
docs/architecture/aetherd-touchpoint* |
Note text for ClientDisplaySettings.h |
Yes | Fine. --check reports "manifest up to date" |
The body's "FFT averaging: reset to off every launch → where you left it" row is not this PR. ClientDisplaySettings::fftAverage()/saveFftAverage() were already on main from #5924 (a325fb99e), and this diff adds no averaging persistence.
Blockers
1. anan_backend_test fails at this head. See tests/anan_backend_test.cpp:184-189. AnanBackend::capabilities() (AnanBackend.cpp:487-491) now declares RfGain | Tuning | SpanRate | ReceiveOutputLevel, but the test asserts exact equality with the first three:
$ ctest -R '^anan_backend_test$' --output-on-failure
FAIL: RF gain, tuning AND span restore are declared -- this radio reports no frequency, mode or span, so the client is its only memory
CI doesn't run this target (ci.yml is -R-filtered), so the green build doesn't cover it. The body's "621/621" was measured on c7513e12, not this head. Fix:
check(c.clientSettingsDomains
== (RadioCapabilities::ClientSettingsDomain::RfGain
| RadioCapabilities::ClientSettingsDomain::Tuning
| RadioCapabilities::ClientSettingsDomain::SpanRate
| RadioCapabilities::ClientSettingsDomain::ReceiveOutputLevel),
"RF gain, tuning, span AND radio output level restore are declared -- "
"this radio reports none of them, so the client is its only memory");2. Static checks is red on the command-plane ratchet. See src/gui/MainWindow_Controllers.cpp:3341. Moving the step handler out of MainWindow.cpp carries its slice set %1 step= wire string to another file:
::error file=src/gui/MainWindow_Controllers.cpp,title=command-plane-ratchet::src/gui/MainWindow_Controllers.cpp grew from 4 to 5 raw Flex command(s) above the seam. This baseline may only shrink (#5262 M4).
::notice ... src/gui/MainWindow.cpp is down to 15 from 16
I reproduced this locally with python3 tools/check_command_plane.py. The baseline is per file, so a lateral move counts as growth. Simplest fix: define applyOperatorTuningStep() in MainWindow.cpp, where the code came from. The comment at :3336-3340 gives merge-conflict avoidance as the reason for the move. That reason belongs in the commit message, not in the source. (The other red step in that job, gh pr comment … Resource not accessible by integration, is the fork token's permissions, not this PR.)
3. Regression: with PC Audio off, hotkey, FlexControl, HID and wheel volume jump to, and stick at, the PC level on every radio that doesn't declare ReceiveOutputLevel (Flex, Demo, HL2, Icom, RTL).
applyMasterVolume()(MainWindow.cpp:7471) now writesMasterVolumeonly when PC Audio is on.activeOutputVolumePercent()(MainWindow_Controllers.cpp:3366-3376) reads the radio level only for aReceiveOutputLevelradio. Everywhere else it falls back toMasterVolume, which no longer tracks the slider in that mode.- So every +/-5 starts from the stale PC level and pushes the result to
mixer lineout gain.
Reproduced through the automation bridge against the built-in demo (DEMO-0001, isolated settings, offscreen). The same steps on the merge base 5fae7003 and on this head:
step merge base 5fae7003 PR head 04762258
invoke "Master volume" setValue 60 vol 60 (PC on) vol 60 (PC on)
invoke "PC Audio" click vol 50 (PC off) vol 50 (PC off)
invoke "Master volume" setValue 30 vol 30 vol 30
shortcut master_volume_up vol 35 vol 65
shortcut master_volume_up vol 40 vol 65 <- stuck at MasterVolume+5
On a Flex that is a +35 jump on the radio's line out from one key press, and "up" can never go past 65. The same read sits in handleFlexControlButton (:563, :569), dispatchHidAction (:1106, :1111), applyFlexControlWheelAction (:1523) and the master_volume_up/down shortcuts (MainWindow_Shortcuts.cpp:1304, :1311). syncTitleBarOutput() (MainWindow.cpp:7442-7454) and your own TCI copy (TciProtocol.cpp:34-45) already use the right rule, with no capability gate. Make this one match:
int MainWindow::activeOutputVolumePercent() const
{
const bool pcAudio = AppSettings::instance()
.value("PcAudioEnabled", "True").toString() == "True";
if (!pcAudio)
return m_radioModel.lineoutGain();
return AppSettings::instance()
.value(QStringLiteral("MasterVolume"), QStringLiteral("100")).toInt();
}That matches what applyMasterVolume() actually drives (setLineoutGain() whenever PC Audio is off, on any family). Please also add a socket-free test of the selector, so this can't drift a third time.
4. The PR body describes code that is no longer in the PR. All of these are false at 04762258:
- "defaulting to 0" / "silent until deliberately raised". The code restores
kDefaultLineoutGainPercent = 50(AnanBackend.cpp:546-554), and its own comment argues against 0. - "Scoped to this family", "no migration from the shared key", and the whole "new gui→engine touchpoint
AudioOutputVolumePolicy.h" section, including 233 → 236 and the tag discussion. The header doesn't exist and the touchpoint count is unchanged. - "Nine mutations". At least two ("disable the output-level split", "restore the 100% default") target removed code.
- The FFT-averaging row (see Scope).
- The actual design, a new
ClientSettingsDomain::ReceiveOutputLevelbit plus anOperatingStateschema bump 2 → 3, isn't mentioned at all.
The body is what the squash commit and later archaeology will read, and the maintainer is being asked to rule on "two deliberate behaviour changes" that aren't what ships. Please rewrite it against the current diff and re-run the mutations against it.
Needs maintainer decision (@ten9876)
- First-connect default 10 MHz WWV → 14.175 MHz USB (
AnanBackend.h:122). This changes a user-visible default. The comment cites "the operator's choice", and no issue calls the old default a defect. A preference, per GOVERNANCE.md. It doesn't block the persistence work and can be split out. - The stored span beats the connect dialog's
anan.ddc0RateKspspicker (AnanBackend.cpp:648). Disclosed and reasoned in the body. Flagging only because the body asks for a ruling. - Seeding the slice step from the flat
TuningStepSizekey (MainWindow_Wiring.cpp:1854-1866) on every radio with no command plane.docs/agents/settings.md:150-154: on a radio that persists nothing, client-owned radio state is "persisted ONLY throughRadioStateMemory'sOperatingStatedocument. Never in flatAppSettingskeys, never via ad-hoc code paths." The key predates this PR as a widget preference. What's new is reading it back intoSliceModel. It's either a UI preference (fine) or per-radio slice state (should ride the Tuning domain).
Nits (non-blocking)
SpectrumOverlayMenu.cpp:3098-3114/.h:164-168:setFftAverage()andsetFftWeightedAverage()have no callers (grep -rnfinds onlySpectrumWidgetmethods of the same name). Drop them or wire them up.- Comments that narrate history (AGENTS.md §"Comments"):
RadioCapabilities.h:453-458("added on main while this branch was open"),MainWindow_Controllers.cpp:3336-3340,MainWindow.cpp:2028("This was a family compare until…"),MainWindow.cpp:6530-6536("an earlier version of this handler"), andDeviceDiagnostics.cpp:325-331, which explains a field that isn't there. Many new comments also run well past ~5 lines. TitleBar.cpp:332-334says the MainWindow handler is "the only side that knows the radio family". It now gates on a capability.RadioCapabilities.h:216(ADDING A FIELD) asks for the map to be updated.docs/architecture/radio-capabilities-map.md:214doesn't listReceiveOutputLevel(it's already missingCwtoo).RadioModel.cpp:660-677(seedingm_lineoutGainso the slider shows the restored level with PC Audio off) has no test. This is the #6124 surface.- The
step_up/step_downchange now sendsslice set N step=to a Flex, persists and toasts. Probably right, but please say so in the body.
What was verified vs. read
- Built and ran
anan_backend_test,radio_state_memory_testandclient_display_settings_testat the head: one failure, Blocker 1. Mutation: makingRadioStateMemory::load()dropreceiveOutputLevelPctturns bothradio_state_memory_testandanan_backend_testred ("radio A's output level comes back as 35…"), so the new per-radio level tests pin real behaviour. - Ratchets:
check_command_plane.pyreproduces the CI error.check_engine_boundary.pygives identical findings on the merge base and the head (only a line number shifts).gen_touchpoint_manifest.py --checkis clean. - Bridge, demo only: the volume regression above on base vs. head.
step_uppersistsTuningStepSizeacross a relaunch. On the demo the slice'sstepHzstays 100 because the demo takes the command-plane branch, so theonSliceAddedseeding path isn't reachable there. - Could not drive: every ANAN path (restore ordering, the dBm range before
wirePanDisplayStatus()'s push, span precedence, first-connect defaults). The demo declaresradioOwnsDbmScale=trueand no client domains, and there's no G2 here. Those parts are reviewed from the code. - Attacks that held: a same-family backend reuse can't carry radio A's dial, span or output level onto radio B (
applyRestoredState()resets all four unconditionally, and the test pins it). A level of 0 survives as 0, not as "not restored". A bad mode doesn't drop a good frequency. A stored span that isn't one of the six rates is refused, not snapped. The float-collapse dBm pair is refused. The span capture moved tofinishRateChange(), so a rolled-back rate is never captured.setLineoutGain()emits only on an actual change. - CI: all runs on this head were created at 18:16Z, before
mainmoved to8df6db2f(18:26Z), so the green jobs describe a slightly older merge.
|
Maintainer rulings on #6129 (following the review above)
|
…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>
An ANAN-G2 reports no frequency, mode, tuning step or output level of its own, so every connect started from defaults. The client now keeps each of them per radio, each behind a declared capability: - Tuning: AnanBackend declares the Tuning domain. Frequency and mode are restored at connect and captured once per settled tune and on each mode change. A stored value is refused, not clamped: a mode modeFromString() cannot map, or a frequency at or above the DDS Nyquist. A radio with no document starts at 14.175 MHz USB instead of 10 MHz WWV (maintainer ruling). - Tuning step: rides the Tuning domain as tuningStepHz wherever no command plane carries `slice set step=`. RadioModel restores it at connect, seeds it into each new slice and captures any slice's step change, as it does for the CW surface. ANAN, HL2 and legacy RTL get it; Flex, Icom, settings-active RTL and the demo are unchanged. The flat TuningStepSize key stays a widget preference and is no longer read back into a slice. - Radio output level: a new ClientSettingsDomain, ReceiveOutputLevel (bit 9), and RestoredRadioState::receiveOutputLevelPct, whose sentinel is -1 so a deliberately silenced speaker survives as 0. Seeded into both the backend and RadioModel::lineoutGain(). OperatingState schema 2 -> 3. - dBm range: ANAN declares PanAmplitudeModel::clientPersistsDbmRange, so aethersdr#6140's store and restore cover it. Master volume: RadioModel::activeOutputVolumePercent() is the one rule for the level the operator hears -- MasterVolume with PC Audio on, the line-out gain with it off, on every family. The hotkeys, FlexControl, HID, wheel, MIDI and TCI all read it. applyMasterVolume() writes MasterVolume only with PC Audio on, so moving the radio's level no longer overwrites the PC level. The PC-Audio toggle re-applies the level where the radio declares ReceiveOutputLevel, instead of comparing the family string. The step_up/step_down shortcuts now go through applyOperatorTuningStep(), so the chosen step reaches the slice and the RX applet; on a Flex that sends `slice set N step=`, as the STEP buttons already did. The span is not remembered: the connect dialog's DDC0 rate always wins (maintainer ruling), so a G2 zoom is lost on reconnect, as on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0476225 to
a6299ad
Compare
|
@ten9876 the head is now Blockers
Rulings
Nits
Bot threads on
|
jensenpat
left a comment
There was a problem hiding this comment.
Review
Reviewed a6299adbd2decbea1792a03580659c3aa96183c6.
Verdict: Approve. The change matches the persistence intent and the maintainer rulings already recorded on this PR: the tuning step rides the Tuning domain, the first-connect default of 14.175 MHz USB is the accepted default, the connect dialog's DDC0 rate wins, and #6140 owns the dBm range. No blockers.
No linked issue. The review is against the PR body plus those rulings.
Scope
| Area | Change |
|---|---|
| ANAN restore | Declares Tuning and ReceiveOutputLevel. Frequency and mode are refused, not clamped, and judged separately. A stored level of 0 is kept. Nothing stored starts at 14.175 MHz USB and output level 50. |
| RadioModel | Restores, seeds, and captures the client-owned step where there is no command plane. Clears that step on a radio swap. Seeds a stored line-out level into the model. activeOutputVolumePercent() is the level the operator hears, with no capability gate. |
| Volume surfaces | Hotkeys, FlexControl, HID, the wheel, MIDI, and TCI GET read that helper. applyMasterVolume() writes MasterVolume only while PC Audio is on. The PC Audio toggle re-applies the destination level only when ReceiveOutputLevel is declared. |
| Schema | OperatingState schema 2 → 3 (tuningStepHz, receiveOutputLevelPct). A newer document stays read-only. |
| Display | ANAN sets clientPersistsDbmRange. No second dBm store. |
| Step shortcuts | step_up / step_down send slice set N step= on a command-plane radio and update the RX applet. |
| Tests | anan_backend_test, radio_state_memory_test, client_display_pan_settings_test, new radio_model_client_state_test. |
Out of scope, as ruled: saved span, FFT averaging, and a second dBm implementation.
Prior request for changes
The review on 04762258 is addressed on this head. Persistence is a declared domain, not a family check or a flat key. The volume selector has no capability gate. The step is per radio on Tuning, and TuningStepSize is not read back into the slice. Span restore is gone. dBm accessors are gone. applyOperatorTuningStep() is back in MainWindow.cpp. The body matches this diff.
Verification
CI on this SHA succeeded: build, check-macos, check-windows, Static checks, and Sanitizer option configures. A merge with current main (25de97c35a3493e01839e5a436e128817346dffa, the style-guide commit) is clean. No local rebuild.
radio_model_client_state_test constructs AnanBackend, whose P2Client binds a QUdpSocket only in start(). This test never connects, so it does not bind. It is registered with add_test and no extra socket name. Not run locally; the green CI suite on this SHA is the execution evidence.
Non-blocking
handRestoredStateToBackend() clears m_clientStepHz before loading, and sets m_lineoutGain only when a level was stored. Disconnect already resets the model gain to 50, and that is the path a radio change takes. A same-family connectToRadio() that does not disconnect first would leave the previous radio's level on the slider while the backend falls back to 50.
Follow-up, not a merge prerequisite
The new test pins the restored model level and the step swap. It does not pin the model level across a radio with no stored document. A socket-free case: restore radio A at 35, then radio B with no document, and expect lineoutGain() == 50 without relying on onDisconnected().
Dismissing the request for changes on 0476225. Head a6299ad addresses those blockers and the later maintainer rulings (per-radio Tuning step, span picker wins, dBm range owned by #6140). Approving review is #6129 (review).
… 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>
What this fixes
An ANAN-G2 reports no frequency, mode, tuning step or output level of its own, so every connect started from defaults. The client now remembers each one per radio:
How
Each value rides a capability the backend declares. None of it is a family check.
ClientSettingsDomain::Tuning, already used by HL2 and RTL).AnanBackendrestores frequency and mode before connect. It captures them once per settled tune (applyTuneToRadioAndPan()) and on each mode change. A stored value is refused, not clamped: a modemodeFromString()cannot map, or a frequency at or above the DDS Nyquist (tuningMinHz/MaxHzare not reported, so there is no band edge to clamp to). The two fields are judged separately, so a bad mode does not drop a good frequency.RestoredRadioState::tuningStepHz, but only where no command plane carriesslice set step=.RadioModelfollows its CW capture/restore. It restores the step at connect, seeds it into each new slice beforesliceAdded, and captures any slice's step change. A radio swap clears it first, so radio A's step never reaches radio B. Steps outside 1 Hz–1 MHz are refused. The flatTuningStepSizekey stays as the widget preference it was, and is no longer read back into a slice.ClientSettingsDomain::ReceiveOutputLevel(bit 9) plusRestoredRadioState::receiveOutputLevelPct. The sentinel is −1, so a speaker silenced on purpose comes back as 0. With nothing stored, the backend keeps its default of 50. The level is seeded into both the backend andRadioModel::lineoutGain(), which is what the slider shows with PC Audio off. Two G2s on one profile keep separate levels.OperatingStateschema goes 2 → 3.PanAmplitudeModel::clientPersistsDbmRange. fix(gui): FFT FPS and the dBm range survive a restart. Principle VIII. #6140's accessors, accept-set and restore do the rest, and this PR adds no dBm code of its own.Master volume
RadioModel::activeOutputVolumePercent()is now the one rule for the level the operator hears. With PC Audio on it isMasterVolume. With it off it is the radio's line-out gain, on every family, because that is whatapplyMasterVolume()drives. The volume hotkeys, FlexControl, HID, the wheel, MIDI and TCI all read it. MainWindow and TCI keep no copy of the rule.applyMasterVolume()now writesMasterVolumeonly with PC Audio on. A level set for the radio's speaker therefore no longer comes back as the PC level.Behaviour changes outside ANAN
MasterVolume. TCI'svolumereports the line-out level.step_up/step_downshortcuts now sendslice set N step=, as the STEP buttons already did, and update the RX applet and the toast.ReceiveOutputLevel(ANAN today). It no longer compares the family string, and the slider now shows the level for the new destination.Testing
On
a6299adb, rebased onto2e3d7ae7(#6140); merge-tree clean against30882701.Serial
ctest: 657 of 659 pass, with the one deliberate skip.pgxl_panel_testfails here on clean main too. The newradio_model_client_state_testfailed on that run because the test never opened its settings store (AppSettings::load()); it was fixed before the push and passes.Static checks: all 17 Python gates from
static-checks.ymlpass with--strict. Command plane is 369, at baseline. Capability booleans are 70, at the frozen 70. The touchpoint manifest is up to date and unchanged by this PR. No colour lines were added.12 mutations, 12 killed, each run against a target that was green beforehand:
ReceiveOutputLevelanan_backend_testradio_state_memory_testradio_state_memory_testanan_backend_testsetLineoutGain()stops announcing the changeanan_backend_testradio_model_client_state_testradio_model_client_state_testradio_model_client_state_testradio_model_client_state_testradio_model_client_state_testradio_state_memory_testclient_display_pan_settings_testANAN-G2, quit and relaunch: frequency, mode, a 2.5 kHz step, the radio output level with PC Audio off, and the dBm range (the first run of fix(gui): FFT FPS and the dBm range survive a restart. Principle VIII. #6140's restore in the app) all came back. A different DDC0 rate picked in the connect dialog applied. With PC Audio off and the slider at 30, Master Volume Up went 35, then 40.
Limits
clientPersistsDbmRange()declaration term (the synthetic case below is its only control now) and the two newisEmpty()terms. Each mutation would rebuild most ofaethercoretwice. The test rows read those fields directly.RadioStateMemory's newer-schema rule). That applies to HL2 and RTL operating state as well as ANAN.RadioModelcode the new test drives with an ANAN backend.Not in this PR
🤖 Generated with Claude Code