Repository navigation
fix(gui): FFT FPS and the dBm range survive a restart. Principle VIII. - #6140
Conversation
On a Hermes-Lite 2, FFT FPS 12 came back as 25 after a clean quit and relaunch, and a dBm scale of -2.19 top / 120 dB came back as -40 / 90. A Flex stores these per pan and reports them on connect, so the client deliberately keeps no copy (aethersdr#2465, aethersdr#4126). A radio whose display the engine shapes stores nothing and reports nothing: the widget kept its default, and pan wiring primed the scale from the PanadapterModel's built-in -130..-40. The waterfall rate already had the answer: the per-radio ClientDisplay feature document, written only where shapesDisplayRatesLocally() holds. The two values join it there, keyed by pan slot, with no schema bump (each table is optional, every writer is read-modify-write). - FFT FPS: saved by the Display slider, Clone to all Pans and Reset to Defaults; restored into the widget before pan wiring seeds the shaper. - dBm range: only where clientOwnsDbmRange() holds, which adds panBinsAbsolute(), so an Icom, whose backend publishes its range, stays out. An operator's range goes into the pan model as well as the store, and the stored one goes into the model before the first prime, so every site that re-seeds from the model agrees. FFT Floor Auto's own moves are not stored. - One DeferredSettingsWrites key per field: two fields of one pan edited inside 250 ms would have shared a key. FFT AVG and Wt Avg were lost in the same observation and are not covered: the open RTL work already stores them under `fftAverages` with a capability of its own, and a second shape under that key would break both. The test pins that these writers leave that table alone. dbmRangeLooksPlausible() moves from a file-static to core/DbmRangePlausibility.h, since the store validates with it; dbm_range_plausibility_test drops its hand copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: docs/architecture/aetherd-touchpoints.md totals, regenerated with tools/gen_touchpoint_manifest.py (234 headers, --check clean). The branch's long comments are cut to the five-line rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: docs/architecture/aetherd-touchpoints.md, the totals line. Regenerated with tools/gen_touchpoint_manifest.py: main's 235 headers plus core/DbmRangePlausibility.h, 236. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le VIII. The slider-bounds check read SpectrumOverlayMenu.cpp as text. The menu links into tests, so spectrum_overlay_fft_fps_bounds_test reads the real slider's range against ClientDisplaySettings::kFftFpsMin/Max. The remaining source-text block covers only call order inside MainWindow methods, which no test can construct, and says so. Comments and the HERMES.md section the branch adds state what is true now; history stays in the PR body. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for this one — the limits-first write-up made it quick to confirm nothing in your change is at fault here. What failedOnly Static checks, and only one step inside it: This is merge skew, not your codeYour PR body says the manifest is clean locally, and that is accurate for your tree. I reproduced it: at The two sides each added one row and each bumped the same Totals line to the same text, so git merged that line conflict-free while the table gained both rows:
The fix, and the order mattersRegenerating at your current head will just write 236 again, because git merge origin/main
python tools/gen_touchpoint_manifest.py
git commit -s docs/architecture/aetherd-touchpoints.mdExpected result: I ran exactly that in a throwaway worktree: the merge is conflict-free, Small bookkeeping nit while you're in there: the PR body's "One row added to the touchpoint manifest (236)" becomes 237 after the merge. No Copilot or reviewer comments on the PR, so nothing flagged there that bears on this. Heads-up for your other open PRs: anything that adds a new engine header under 🤖 Generated with Claude Code 🤖 aethersdr-agent · cost: $5.9324 · model: claude-opus-5 |
main gained aethersdr#6138, aethersdr#6115 and aethersdr#6027 since this branch was cut. No textual conflict; the touchpoint manifest is regenerated in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…VIII. This branch and main each added one row and each set Totals to 236, so the merge held 237 rows under a Totals of 236 and the manifest check failed. Regenerated with tools/gen_touchpoint_manifest.py on the merged tree: 237 headers (198 core, 39 models). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained aethersdr#6127 (squelch scale) after the first merge. No textual conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…VIII. aethersdr#6127 added a row on main and this branch has one of its own, so the merged tree again held one row more than its Totals line said. Regenerated with tools/gen_touchpoint_manifest.py: 238 headers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
73, ON8ST |
There was a problem hiding this comment.
1. Issue fit
No linked issue, and the body says so plainly ("Refs #5678 ... No issue exists for this"). GOVERNANCE.md does not require one for a bug fix with a clear root cause, and the root cause here is stated precisely and checks out: on a non-Flex backend the FFT FPS stays at SpectrumWidget's built-in 25 and the pan model stays at its built-in -130 / -40 (PanadapterModel.h), because nothing reports either and only waterfallRates had a client store. Both halves of the stated defect are addressed, and the scope limits (FFT AVG / Wt Avg left to #5924, FFT Floor Auto's own moves not stored, Reset-to-Defaults still not resetting the range) are disclosed rather than glossed.
The design choice that carries the PR is the right one: the remembered range goes into the pan model, not the widget. I checked all eight setDbmRange(pan->minDbm(), pan->maxDbm()) sites in src/gui — MainWindow_Wiring.cpp:2715/4011/4164/4173, MainWindow_Session.cpp:1836, MainWindow_KiwiSdr.cpp:449, MainWindow.cpp:8732/10765 — and every one reads the model. So the two restore calls cover the six re-seed sites the PR doesn't touch, including createPansSequentially's layout-change prime. That is why there is no "sibling site left behind" finding here.
2. Scope
| File / group | What it changes | Claimed in body? | Verdict |
|---|---|---|---|
src/core/ClientDisplaySettings.h |
fftFps + dbmRanges tables, clientOwnsDbmRange, pendingWriteKey |
yes | in scope |
src/core/DbmRangePlausibility.h (new) |
predicate moved out of MainWindow_Wiring.cpp, unchanged |
yes ("yours to move") | in scope |
src/gui/MainWindow.h, _Wiring.cpp, _Session.cpp |
save/restore wiring; the predicate's old home deleted | yes | in scope |
tests/ ×3 + tests.cmake |
two new targets, dbm_range_plausibility_test now includes the real predicate |
yes | in scope |
docs/HERMES.md, aetherd-icom-civ-backend-design.md |
prose for the two new tables | yes | in scope |
docs/architecture/aetherd-touchpoint* |
one row for the new header, 237→238 | yes | in scope |
Everything in the diff is explained by the stated fix. No CHANGELOG.md entry (correct — it must not have one). No new protocol verb, wire message, CLI flag or capability bool; the two new JSON fields are optional members of an existing schema-1 document, and panBinsAbsolute() is read, not added. The - lines are a clean removal: dbmRangeLooksPlausible moves to a header byte-for-byte (I diffed the bodies) and the deleted test comment is replaced by prose that is less flattering to the test, not more. No guard was dropped.
I re-ran python3 tools/gen_touchpoint_manifest.py --check on the head: "manifest up to date" — so the 238/199 count is real, not a stale post-merge number.
Socket-test entry (§2 disclosure). The PR adds no socket test. client_display_pan_settings_test constructs Hl2Backend, FlexBackend and IcomCivBackend purely to read capabilities(); grep for QTcpServer|QTcpSocket|QUdpSocket|QLocalServer|listen(|bind(|connectToHost|Fake over both new test sources returns nothing. Transitively, FlexBackend's constructor starts the PanadapterStream thread and PanadapterStream::init() does new QUdpSocket(this) — but the three bind() calls live at PanadapterStream.cpp:212/292/345 in start paths the test never reaches, so that is an inert socket object, not an opened one. The cited precedent noise_floor_auto_adjust_gate_test constructs the same three backends and the new test correctly copies its TestSettingsProfile-before-QCoreApplication guard. Nothing to flag.
3. Blockers
None. Two items below are routed to you rather than to the author, because neither is the author's to decide.
Needs maintainer decision
1. #6129 already adds dbmRanges to this same document, with an incompatible accept-set. (inline on ClientDisplaySettings.h:137) The author's "With the open PRs" section checks #5924 and #5919 but not #6129, which is open and persists the ANAN pan dBm range in the same ClientDisplay doc, same dbmRanges table, same min/max keys, same DbmRange struct and same dbmRange() / saveDbmRange() method names — with different bounds and different signatures:
| #6129 | this PR | |
|---|---|---|
| accepts | min < max, min ≥ -250, max ≤ 100 |
min ≥ -180, max ≤ 80, 10 ≤ max-min ≤ 180 |
| signature | dbmRange(scope, panIndex) |
dbmRange(scope, panIndex, clientOwned) |
On an ANAN both are live: AnanBackend.cpp:392 sets binsAbsolute = true, so this PR's clientOwnsDbmRange() is engaged on exactly the radio #6129 targets. A range of -200 / -100 is valid for #6129's writer and silently rejected by this reader (min < -180 → nullopt → fall back to the built-in -130 / -40), i.e. the operator's scale "doesn't stick" — the bug both PRs exist to fix. The same-named members with different arities also mean the author's stated conflict strategy ("both add members at one place; keep both") cannot be applied here. Neither PR is wrong alone; the two accept-sets and one of the two signatures need reconciling before both land, and you may want to pick the sequencing.
2. The FFT FPS predicate, which the author already flagged for you. shapesDisplayRatesLocally() is m_backend != nullptr && m_flexBackend == nullptr (RadioModel.h:1333) — a family check, and it includes the Sim/demo backend, which the body's family list ("HL2, ANAN, Icom, RTL-SDR") omits. Worth weighing against docs/agents/settings.md:97-98 ("capability-shaped, never a family-name check") and the line just above it: "Flex and Sim declare explicitly empty — a CI test guards this". That sentence is scoped to RadioStateMemory / the OperatingState document, so it does not bind the ClientDisplay doc and this is not a violation — but the project's own sanctioned client-memory gate deliberately excludes Sim, while this gate includes it, and the PR widens that pre-existing waterfallRates seam to two more fields. The dBm range half is already capability-shaped (panBinsAbsolute()), which is why an Icom stays out. The author offered to move FPS behind a declared flag; your call.
4. Nits (non-blocking)
- The adopt comment over-claims the echo. (inline,
MainWindow_Wiring.cpp:4203)PanadapterModel::setRangeemitslevelChangedonly when a bound actually changed (PanadapterModel.cpp:197-207), so when the operator re-requests the range the model already holds, no synthetic echo fires and the armed handshake sits tokDbmRangeHandshakeTimeoutMs. Not a regression — onmainthe HL2 handshake always times out, so this strictly improves it — but the comment reads as unconditional. double→floatnarrowing happens before the finiteness check. (inline,ClientDisplaySettings.h:129) Credit: security-audit (CodeGuard), which I confirmed against the code. A stored{"min": -1e300}is narrowed out offloatrange (formally UB, [conv.double]) beforedbmRangeLooksPlausibleruns; in practice every target yields±infandstd::isfiniterejects it, so nothing misbehaves today. One-line fix in the inline comment. Sanitizer-class, not functional.- The body's
:3610citation points at the wrong gate.sendDbmRangeCommandreturns early on!radioOwnsDbmScale(MainWindow_Wiring.cpp:3716), and HL2 never declares that field so it defaults true (RadioCapabilities.h:390). The command is therefore attempted on an HL2 and dropped at the command plane, not suppressed by that gate. Effect is the same; the sentence isn't. - The source-text pin is brittle in two ways. (inline,
client_display_pan_settings_test.cpp:242)count("scheduleClientFftFpsSave(") == 4counts the definition alongside the three call sites, so a future comment that merely mentions the name turns the test red; and the adjacent ordering assertion compares twoindexOfresults, which passes vacuously if the first literal is absent (the neighbouringcontainscheck covers that today, so it is fragility, not a hole). - Reset to Defaults saves FPS
25beside the existingsw->setFftFps(25)and does not touch the dBm range — consistent with the waterfall-rate line next to it, and the asymmetry is disclosed.
5. What I tried to break, and could not
- The stale-head trap. The provided checkout was at
b1cdeab7while the task listed613f9c3on top, so I fetchedpull/6140/headfromaethersdr/AetherSDR(the checkout'soriginis a fork) and pinned the SHA: the real head isb1cdeab7, andgit diffagainst the merge-base reproduces the posted diff exactly. Reviewed against that. - "The restore is defeated by a later re-seed."
wirePanDisplayStatusrestores FPS into the widget and then, after theshapesDisplayRatesLocally()block, unconditionally doessw->setFftFps(pan->fps()). I expected that to clobber the restore. It does not:RadioModel::requestPanDisplayRatescallspan->setDisplayRates(fps, wfRate)synchronously in the locally-shaped branch (RadioModel.cpp:6147), sopan->fps()already equals the restored value and the later line is a no-op. - Re-entrancy in the restore's
flush().DeferredSettingsWrites::flush()doesstd::exchange(m_pending, {})before iterating, so a write scheduled from inside a flush lands in a fresh map. Safe. It does flush other pans'/fields' pending writes too, but each lambda captures its ownscope, so they land on the right radio. - Radio-swap and disconnect inside the 250 ms window. Every scheduled lambda captures
scopeby value and the key is length-prefixed on bothfamilyandradioId(size()+':'+ value), so the encoding is injective and a swap cannot cross-write or collide. The three field names are distinct, which is the bug the key change fixes. - Document destruction. Both writers are read-modify-write on one table row, refuse
version > 1/Corrupt/Unavailable, and checksetFeature's bool. I confirmed the test's fixture 6 proves an unknownfftAveragestable (#6129/#5924's shape) survives both new writers and that the untouched waterfall writer preserves the new tables. - Tests that pass against the unfixed code. The refusal block would fail with the validators emptied (it asserts
nulloptfor a fractional FPS, an out-of-range FPS, a string FPS, an implausible range, a half range and a non-object), the ownership block readspanBinsAbsolute()off three real backends rather than restating constants, and the mutation table gives a distinct failing count per row — including17for "main's wiring", which is the right shape for a fail-before. - CI. All 5 checks are green on this exact head (
build,check-macos,check-windows,Static checks,Sanitizer option configures), which also settles my one link concern: the newspectrum_overlay_fft_fps_bounds_testis not inAETHER_SETTINGS_CONSUMERSwhile the sibling it copies (spectrum_overlay_dax_availability_test) is — but that list only addsaether_sqlite3for targets compiling the settings sources directly, and the new target linksaethercore, so it builds on all three platforms. Noting it only because the author built locally on macOS alone. Standard caveat:ctestinci.ymlis-R-filtered, so green is not "the suite passed" — the 622/623 figure is the author's local run, which I cannot reproduce.
Everything above is reasoned from reading the code at b1cdeab7, not observed at runtime. I have no build, no test run and no way to drive the GUI, so the restore-on-restart behaviour the PR is named for is exactly the thing I could not verify — and the author says the same ("Not verified: the restore in a running app, on any radio").
6. Recommendation
Needs maintainer decision — on the #6129 collision above all, and secondarily on the FPS predicate shape the author explicitly put to you. The code itself is in good order: the model-as-single-source choice is what makes the six untouched re-seed sites correct for free, the store honours every bullet in docs/agents/settings.md "Radio-Scoped Feature Documents" (exact read, newer-schema refusal, checked write, empty-radio_id guard), validation runs on both the way in and the way out, and the test suite pins the refusals and the multi-table coexistence rather than restating the implementation. I found no blocker and no regression.
Concrete next step: rule on whether dbmRanges is #6129's or this PR's, since both cannot define dbmRange()/saveDbmRange() and both are live on an ANAN. Whichever lands second should take the first's accept-set rather than adding a second one. The four nits are all one-line changes and none of them need to gate a merge.
Nice work on the limits section — "not seen in a running app", the per-mutation failing counts, and flagging your own predicate as the thing you might have wrong are what made this reviewable.
🤖 aethersdr-agent · cost: $20.5682 · model: claude-opus-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ple VIII. PanadapterModel::setRange emits levelChanged only on a change, so a request for the range the model already holds leaves the handshake to its timeout, as on main. The comment said the echo always completes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iple VIII. A stored double outside float's range was narrowed before the finiteness test ran. dbmRange() now refuses it as a double. Results are unchanged for every value a float holds, including one that rounds onto a bound. The source-text pins match whole call statements instead of counting a prefix that also matched the definition, and both order helpers require both literals to be present. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The body missed #6129, and two of its sentences were wrong (the #6129, for your ruling. Read at
In 2D no drag emits a range this reader refuses: each passes Options: this PR takes #6129's set; #6129 takes this one's; one shared reader and writer; the second to land adapts. I would do the last. Reading the wider set here is the same choice, so nothing is changed. The FPS predicate waits for your ruling too. Nits, at Full ctest 621 of 623: 73, ON8ST |
main gained aethersdr#5924, which adds the fftAverages table to the ClientDisplay document. Three keep-both conflicts: - ClientDisplaySettings.h: main's FftAverage, fftAverage, saveFftAverage and decodeAverage stay as they are, beside this branch's fftFps and dbmRanges members; one private section holds both sides' helpers. The class comment now names fftAverages as well. - MainWindow.h: scheduleClientFftAverageSave beside this branch's four declarations. - MainWindow_Wiring.cpp: scheduleClientFftAverageSave after this branch's four functions. The pending-write keys stay distinct (main's starts with "fft:", this branch's end in the field name). Touchpoint manifest regenerated: 238, unchanged by the tool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The full ctest is not clean on this Mac: at 73, ON8ST |
ten9876
left a comment
There was a problem hiding this comment.
Issue fit: The proposed save/restore path addresses the reported FPS/range loss, with an ownership-contract blocker. At 295bff6eb54609512b04ea1bd0244acd3cd45f22, FPS is saved on slider, clone and reset intents and restored before rate shaping; the dBm range is restored into the pan model before widget priming. The PR references #5678 rather than closing a dedicated issue. Its overlap with #6129 needs the disclosed maintainer decision before both designs can land.
Scope
| File/group | Claimed? | Verdict |
|---|---|---|
ClientDisplaySettings.h |
Yes: optional fftFps/dbmRanges tables and distinct pending-write keys |
In scope; eligibility issue below |
DbmRangePlausibility.h |
Yes: share the existing production predicate | Relevant extraction; limits unchanged |
MainWindow.h, _Wiring.cpp, _Session.cpp |
Yes: save/restore and model adoption | Relevant operator-intent and reconnect wiring |
Three test sources and tests/tests.cmake |
Yes: settings, slider bounds and real plausibility helper | Relevant coverage; isolation gap below |
| HERMES/Icom documentation and touchpoint tags/manifest | Yes: ownership, tables and new helper include | Explained support; manifest regenerated consistently |
No unrelated wire verb, default change, dependency or changelog entry was found. The authored commits are implementation/tests plus integration and manifest maintenance. The scope checklist is accurate. The two persistent JSON fields are a new storage contract even though they extend an existing schema-1 document.
Blocker
-
The new persistent fields must use declared ownership.
src/gui/MainWindow_Wiring.cpp:3138enables FPS persistence throughshapesDisplayRatesLocally(), and:3153-3156adds only absolute-bin semantics for the dBm range. The underlying predicate atsrc/models/RadioModel.h:1341ism_backend != nullptr && m_flexBackend == nullptr. Neither backend identity nor bin units declare whether this radio stores these settings itself.The precise requirement in
docs/agents/settings.md, Settings Authority Policy, is: “The deciding test is whether THIS radio can save and restore the value — a declared capability, not a family assumption.” Reusing the older waterfall-rate exception expands that exception to two more fields; disclosing it as “you may want it changed” does not satisfy the rule. The adjacent FFT-averaging path already requires the declaredclientPersistsAveragingfield in addition to local shaping (MainWindow_Wiring.cpp:3201-3205).Keep the
ClientDisplaydocument and its atomic whole-document writes, but introduce/use declared per-feature ownership for FPS and range, apply it consistently to save and restore, and test both opt-in and opt-out. Follow the capability-record contract instead of adding a frozen boolean. This finding is about the new contract, not an observed clobber of a currently supported radio.
Non-blocking nits / maintainer decisions
- Jeremy: reconcile #6129 before landing both storage implementations.
src/core/ClientDisplaySettings.h:171reads the samedbmRanges[pan] = {min,max}namespace with different ownership and validity rules than #6129. The body now states that conflict honestly. Choose one accept-set/ownership contract and preserve valid existing documents when integrating; “keep both” is not a coherent resolution. tests/spectrum_overlay_fft_fps_bounds_test.cpp:18-20constructsSpectrumOverlayMenuwithoutTestSettingsProfile. Its constructor reachesThemeManager, thenAppSettings, which creates the normal configuration directory and invokes legacy-path migration. This is a widget test with settings side effects, not an inert header-only test. Add the same profile guard used by the neighboring settings test beforeQApplication, addteststo its include directories, and add the target toAETHER_SETTINGS_CONSUMERSasdocs/agents/tests-ci.mdrequires. I did not run it against the operator's profile or establish that it overwrites the SQLite store.Also includeTestSettingsProfile profile(QStringLiteral("spectrum-overlay-fft-fps-bounds")); if (!profile.isValid()) { return 1; } QApplication app(argc, argv);
"TestSettingsProfile.h".docs/HERMES.md:2196's new “FFT AVG and Wt Avg are not remembered” sentence is stale after the merged #5924 code present in this head:scheduleClientFftAverageSaveuses its declared persistence flag. Describe which settings this PR adds without claiming the averaging feature is still absent.
Verification and attacks
- Built the exact PR head's full
AetherSDRapplication and all three new CMake test targets with GCC 16.2 / Qt 6.12.0.client_display_pan_settings_test,spectrum_overlay_fft_fps_bounds_test, anddbm_range_plausibility_testall returned 0. The widget target ran only under an external private settings/config profile because of the isolation nit above. - Attacked the actual slider contract by changing only
kFftFpsMaxfrom 60 to 61 in the scratch header: the new widget test returned 1 withFAIL: slider maximum 60 is kFftFpsMax. Restored the header, rebuilt, and the test returned 0. Separately, the settings test's store/coalescing portion, linked to the production settings implementation under native Qt 6.11.2 (omitting only the backend-construction capability block), failed twice when the pending-write key lost its field discriminator; it passed before and after restoration. The final exact CMake test above includes the omitted backend block. - Drove exact-head demo processes 2208948 and 2209640, offscreen with isolated settings and
AETHER_AUTOMATION_NO_TX=1. OpenedpanMenuDisplayBtn, set the realdisplayFftFpsSliderto 12, andget panreturned"fps":12. Quit, launched a new process using the same private profile, explicitly connectedDEMO-0001, and again received"fps":12.get radioconfirmed"model":"AetherSDR Demo","serial":"DEMO-0001","transmitting":false. Both review instances were closed; no live radio was connected. - A separate writer/reader process probe using the exact production settings implementation retained
{"dbmRanges":{"0":{"max":-2.190000057220459,"min":-122.19000244140625}},"fftFps":{"0":12}}across process exit. The new settings test also exercises invalid values, distinct field writes, and preservation of unreadable/newer documents. Demo does not advertise absolute pan bins, so I could not drive its dBm restore path; that backend-specific GUI wiring was inspected, not reproduced. No exact merge-base GUI run or hardware/cross-platform run is claimed.
The four architecture gates pass at both merge base and head. Comparing per-file findings shows the same 102 tracked engine warnings; the new helper changes only the scanned-file count, and its touchpoint row is explained in the body. No new socket-owning test was found; backend objects in the settings test are only constructed to read capabilities. No model-invocable code-review skill was available; this was a manual audit plus local validation.
Required CI checks succeeded, but earliest head-run creation 2026-10-04 07:01:23 UTC is older than current main 85f2a124 (22:40:53 UTC). Those pull_request jobs tested an older merge result; protection is strict: false. No complete-current-main-suite claim is made.
Recommendation: Request changes for declared persistence ownership, followed by a maintainer ruling on the overlap with #6129. The focused tests support the store/coalescing implementation; they do not authorize widening persistence by backend identity.
— Jeremy KK7GWY + Codex
The touchpoint manifest conflicted (both sides added rows and wrote the Totals line); it is regenerated with tools/gen_touchpoint_manifest.py after the merge, not edited by hand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpectrumOverlayMenu reaches ThemeManager and then AppSettings from its constructor, so the widget test opened the normal configuration directory. It now takes a TestSettingsProfile before QApplication, has tests/ on its include path and is listed in AETHER_SETTINGS_CONSUMERS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Storing and restoring a pan FFT FPS and dBm range was gated on RadioModel::shapesDisplayRatesLocally(), which is backend identity, and for the range on absolute bins. Both now ask a declared owner in a per-feature capability record, as BackendPanAveraging does for averaging: - PanFrameRateShaping (new record), clientPersistsFrameRate, read through RadioCapabilities::clientPersistsPanFrameRate(); - PanAmplitudeModel::clientPersistsDbmRange, read through RadioCapabilities::clientPersistsDbmRange(), which also requires absolute bins because the client writes the range into the pan model. Only the Hermes-Lite 2 declares either. Flex, Icom, ANAN, RTL-SDR and the demo backend keep the behaviour of main: nothing stored, nothing restored. That is narrower than before, when every non-Flex backend was covered. Save and restore of each field go through one MainWindow predicate. The ClientDisplay document, its schema and its whole-document writes are unchanged, and the waterfall-rate gate is not touched. Tests read the accessors off real HL2, Icom, ANAN and Flex capabilities against the real store. docs/HERMES.md no longer says FFT AVG and Wt Avg have no store: they have one behind clientPersistsAveraging, which the HL2 declares false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Three things first, none of them good news:
The blocker: declared ownershipDone in
No record described the frame rate, so FFT FPS gets one small record of its own. The range's owner sits in
All six backends state the frame-rate record in their own In Two things you may want changed:
The #6129With the opt-in on the HL2 only, this PR no longer acts on an ANAN or an Icom. #6129 at
A range such as -200 / -100, or a 5 dB span, is valid there and refused here. A trial merge of the two heads conflicts in Nits
MergeThe PR conflicted with Tests at
|
| mutation | failing checks |
|---|---|
| both accessors return true for every backend | 33 (8 each for Icom, ANAN, Flex and the undeclared backend, and the relative-bins check) |
| both accessors return false for every backend | 8 (7 HL2, and the absolute-bins control) |
Static checks: every script step passes with stdin from /dev/null; colour ratchet +0; check_a11y.py on the three changed GUI files, no findings.
Not verified
- The restore in a running app, on any radio, at this head.
- Linux and Windows: built and tested on macOS only.
- The
Sanitizer option configuresjob was not run locally. - The older rows of the body's mutation table were measured at
295bff6eand not run again.
73, ON8ST
ten9876
left a comment
There was a problem hiding this comment.
Reviewed at head 7a5ca748, which arrived while my review of 295bff6e was in progress. Everything below is against 7a5ca748.
Issue fit: yes, against its own stated intent (Refs #5678 rows P16 and the FPS half of P7; no issue is linked). On a Hermes-Lite 2, FFT FPS and the dBm range are now stored in, and restored from, the per-radio ClientDisplay document. Each sits behind a declared owner in a per-feature capability record. Every other family keeps main's behaviour: nothing stored, nothing restored.
Scope
| File / group | Change | Claimed? | Verdict |
|---|---|---|---|
src/core/backends/RadioCapabilities.h:100-108, 150-154, 263, 284-298 |
new optional PanFrameRateShaping{clientPersistsFrameRate} record; PanAmplitudeModel::clientPersistsDbmRange (default false); two accessors |
Yes (the reply to the previous review) | In scope. New capability surface. No new top-level bool: check_capability_records.py --strict → "70 boolean(s), at the frozen 70" |
Six backends' capabilities() |
HL2 engages both; Flex/Icom/ANAN/RTL/Sim state absent or false explicitly | Yes | In scope, per the ADDING A FIELD contract (RadioCapabilities.h:228-231) |
docs/architecture/radio-capabilities-map.md:189, 559, 593 |
both fields recorded | Yes | In scope |
src/core/ClientDisplaySettings.h |
fftFps[pan], dbmRanges[pan], per-field pendingWriteKey |
Yes | In scope |
src/core/DbmRangePlausibility.h (new) |
predicate moved unchanged out of MainWindow_Wiring.cpp |
Yes | In scope |
src/gui/MainWindow.h, _Wiring.cpp, _Session.cpp |
save, restore and adopt, each behind one predicate | Yes | In scope |
3 tests + tests/tests.cmake |
store and ownership, slider bounds, the real predicate | Yes | In scope |
docs/HERMES.md, Icom design doc, touchpoint tags and manifest |
prose plus one manifest row | Yes | In scope |
8 merge: origin/main commits, latest 57a6f01f (85f2a124) |
keep-both resolutions; manifest regenerated | Yes | Fine |
No CHANGELOG.md entry. Socket preflight: client_display_pan_settings_test constructs the HL2, Icom, ANAN and Flex backends only to read capabilities(). Their sockets are never bound.
Blockers
None. The previous review's blocker, ownership inferred from "not a Flex", is resolved in 7a5ca748, and I verified the fix rather than taking it from the reply (see below).
Needs maintainer decision (@ten9876)
-
#6129 and this PR still both define
dbmRanges[pan] = {min, max}in the same document, with the samedbmRange()/saveDbmRange()names, different arities and different accept-sets:Accepts this PR −180..80, span 10..180 #6129 min < max, −250..100They no longer claim the same radio: this PR covers HL2 only, and #6129 covers
!radioOwnsDbmScale(ANAN, Icom). The author reports, without compiling it, that a textual merge leaves both definitions inClientDisplaySettings.h. Whichever lands second will not build until the two are reconciled. Rule on one accept-set and one owner of the helpers; the author has said this PR will adopt whichever you choose. -
The new capability surface. Two persistence-owner declarations (
panFrameRateShaping,PanAmplitudeModel::clientPersistsDbmRange) follow theBackendPanAveraging::clientPersistsAveragingshape. Because they are third-party-visible capability fields, they need your sign-off, though the shape is the one canon prescribes.shapesDisplayRatesLocally()remains a second condition for FFT FPS, as in the averaging path, and no longer grants anything on its own.
Nits (non-blocking)
- 3D emit not marked as auto-floor.
src/gui/SpectrumWidget.cpp:3781: the 3D DSS zoom-floor emit ofdbmRangeChangeRequestedis not flagged as an auto-floor change (pendingAutoNoiseFloorDbmRange()). On an HL2 in 3D it would therefore be adopted and stored like an operator edit (MainWindow_Wiring.cpp, theadoptClientOwnedDbmRangecall aftersendDbmRangeCommand). The 2D auto-floor path is correctly excluded:autoFloorChange→localOnly→ return before adopt. The author calls the 3D path "unchecked", so either a guard or a sentence in Limits. - Untested sibling. The waterfall-rate save/restore on
mainstill uses the bareshapesDisplayRatesLocally()gate. That is out of scope here, and correctly untouched, but it is now the odd one out: worth a follow-up issue.
What I tried to break / verified
-
Built this head's app and all three targets (Linux, Qt 6.12.0;
main85f2a124is in the branch).client_display_pan_settings_test,dbm_range_plausibility_testandspectrum_overlay_fft_fps_bounds_testall exit 0. -
Static gates at this head:
check_capability_records.py --strict→ 70 booleans, at the frozen 70;gen_touchpoint_manifest.py --check→ "manifest up to date";check_command_plane.py→ 369, baseline 369;check_engine_boundary.py→ 102 findings, 0 would block;git merge-treeagainstmain→ clean (the earlier manifest conflict is gone).
-
Mutation: HL2
PanFrameRateShaping{true}→{false}makesclient_display_pan_settings_testexit 1 with "FAIL: hl2: FFT FPS owner as declared", "…stored and restored only by its owner", "…read only by its owner". The file was restored (sha256matchesgit show HEAD:) and touched. -
Settings isolation fixed: with a scratch
HOME/XDG_CONFIG_HOME,spectrum_overlay_fft_fps_bounds_testnow creates 0 files there. At295bff6e, the same run created$HOME/.config/AetherSDR. -
Bridge, demo only, opt-out proven across a relaunch: isolated settings,
AETHER_AUTOMATION_NO_TX=1, socket/tmp/aethersdr-pr6140.connect local serial DEMO-0001→"model": "AetherSDR Demo","serial": "DEMO-0001".Step get pan/ storeinvoke displayFftFpsSlider setValue 12"fps": 12SQLite store after kill -TERMno ClientDisplayrowrelaunch, reconnect DEMO-0001"fps": 25At
295bff6ethe identical sequence storedsim|DEMO-0001|ClientDisplay|1|{"fftFps":{"0":12}}and restored 12. The declaration is doing what the author says, and the demo now does whatmaindoes. -
Could not drive the opt-in (HL2) restore: the demo cannot reach it and there is no HL2 here. That half rests on the unit tests and on reading the code; the author says the same.
-
Earlier attacks that still hold at this head:
- 2D auto-floor moves are never adopted;
- the restore is not overwritten by the later
sw->setFftFps(pan->fps()), becauserequestPanDisplayRatessets the model synchronously on the local path; - all eight
setDbmRange(pan->minDbm(), pan->maxDbm())re-seed sites read the restored model.
Recommendation: no code blocker remains. Needs your ruling on the #6129 dbmRanges overlap and your sign-off on the two capability fields.
— Jeremy KK7GWY + Claude
|
Maintainer ruling: this PR owns — Jeremy KK7GWY + Claude |
The 3D (DSS) floor resync after a bandwidth zoom requests a dBm range computed from the first trusted post-zoom frame. It is not an operator edit of the scale, so MainWindow no longer adopts it into the pan model or stores it in ClientDisplay on a backend that declares clientPersistsDbmRange (the Hermes-Lite 2). SpectrumWidget marks that one request while it emits it; MainWindow reads the mark only at the adopt. The handshake, the stream decoder range and the display pan set command are unchanged, so a Flex sends the same wire text. The 2D auto-floor mark is not reused: it routes a request to the local-only branch, which sends nothing. client_display_pan_settings_test pins the mark, its single reader and the 2D return-before-adopt as source text: SpectrumWidget and MainWindow link into no test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The nit holds, and it is a guard, not a Limits sentence ( 73, ON8ST |
One conflict, docs/architecture/radio-capabilities-map.md, in the dBm-axis table: main rewrote the Icom cell of the squelchLevelScale row for the IC-7300MK2's measured map (aethersdr#6180), and this branch added the clientPersistsDbmRange() row directly above it. Both kept: this branch's row unchanged, then main's squelchLevelScale row as main has it. Main's IC-7300MK2 paragraph merged as text. IcomCivBackend.cpp merged without conflict: main changed the squelchLevelScale assignment and the preamp/ATT reads, this branch one panFrameRateShaping line earlier in capabilities(). Main did not touch RadioCapabilities.h or PanAmplitudeModel. Touchpoint manifest regenerated after the merge: 240 headers, unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The PR conflicted when the two Icom fixes landed ( 73, ON8ST |
NF0T
left a comment
There was a problem hiding this comment.
Reviewed at head d60f4660 (main 12df6943 merged in), and again as a merge with current main 5f09371e. Verdict: APPROVE. I found no blocker. Merge is gated by something other than code, see "What happens next".
Thank you for the narrowing. The change I would have asked for after reading the two maintainer reviews is the one you made: the backend declares the owner, the save and the restore of each setting read the one accessor, and every family that declares nothing keeps main's behaviour. The "Limits, first" section is accurate, and the "Not verified" list matched what I could and could not reproduce.
Scope
| File / group | Claimed? | Verdict |
|---|---|---|
core/ClientDisplaySettings.h (fftFps, dbmRanges, per-field pendingWriteKey) |
yes | in scope |
core/DbmRangePlausibility.h (predicate moved out of MainWindow_Wiring.cpp, plus dbmNarrowsToFloat) |
yes | in scope |
RadioCapabilities.h + six capabilities() |
yes | in scope. New third-party-visible fields, same shape as clientPersistsAveraging; no new top-level bool |
MainWindow.h, _Wiring.cpp, _Session.cpp, SpectrumWidget.{h,cpp} |
yes | in scope. The SpectrumWidget change is one flag set around one existing emit |
3 tests + tests.cmake |
yes | in scope. tests/ is Tier 3 |
| docs and touchpoint manifest | yes | in scope |
No CHANGELOG.md entry, no wire verb, no default changed. Deleted lines: only the predicate that moved to the header (bodies identical) and the hand copy in the test. Highest CODEOWNERS tier touched is Tier 3 (src/, tests/, docs/ outside docs/agents/).
Fix, not preference. It restores a defect observed on an HL2 (FPS 12 came back 25; a 120 dB scale came back -40 / 90 dB). Nothing about a default, a layout or a workflow changes, and a Flex is unchanged by construction: clientPersistsPanFrameRate() and clientPersistsDbmRange() are false for it, and every new reader returns nothing and every new writer returns before the store.
#5554 backend gate: none of the six patterns is added: no new family-string or command-plane branch, no raw Flex wire text, no dynamic_cast to a backend, no capability without behaviour behind it, no HL2 scaffolding copy, no keying verb.
What I tried to break
Build: MinGW, Qt 6.12.0, RelWithDebInfo, the PR merged with main 5f09371e (clean git merge-tree).
- Static gates on that merge result:
gen_touchpoint_manifest.py --checkup to date;check_capability_records.py --strict70, at the frozen 70;check_command_plane.py369 = baseline;check_test_registration.py,check_ci_test_gate.py,check_qsettings.py,check_network_timeouts.pyok;check_engine_boundary.py102 findings, 0 would block. Required CI ran on the merge ref at 2026-10-05 02:52Z;mainhas moved 9 commits since, so I ran the above rather than rely on it. - Tests:
dbm_range_plausibility_testandspectrum_overlay_fft_fps_bounds_testpass.client_display_pan_settings_testfails 6 checks on this machine, all multi-line source-text pins (see the first nit). With the four source files converted to LF it passes, so the cause is line endings, not behaviour. - Mutations, one build each: the pending-write key without its field: +2 failures over the 6 line-ending ones, matching your table. Owner ignored in the store's read and write: +26. The tests notice.
- Driven through the real
MainWindow, demo only. The demo declares no owner, so I patchedSimBackend::capabilities()locally and uncommitted to declare what the HL2 declares (PanFrameRateShaping{true}, and aPanAmplitudeModelwithbinsAbsoluteandclientPersistsDbmRange). IsolatedAETHER_SETTINGS_DIR,AETHER_AUTOMATION_NO_TX=1,connect local serial DEMO-0001→"model": "AetherSDR Demo","transmitting": false.- Baseline
get pan:fps 25,-130 / -40. invoke displayFftFpsSlider setValue 12, then twoclickAt SpectrumWidget 1110 5on the dBm scale's up arrow:fps 12,minDbm -130,maxDbm -20.- Closed through
titlebar close. The store heldsim|DEMO-0001|ClientDisplay|1|{"dbmRanges":{"0":{"max":-20,"min":-130}},"fftFps":{"0":12}}. - Relaunched on the same store, reconnected
DEMO-0001:fps 12,minDbm -130,maxDbm -20, and the Display panel'sdisplayFftFpsSliderreads12. - What this proves: the save sites, the flush on quit, and both restores work through the real wiring, across a process boundary, on Windows. What it does not: it is the demo with an HL2-shaped declaration, not an HL2. The demo vends a
RadioConnectionand may echo a range itself, so the model values alone prove little; the store row, which only this PR writes, and the restore after relaunch are the evidence.
- Baseline
- Read hostilely, and it held:
- The slider's programmatic syncs (
SpectrumOverlayMenu.cpp) useQSignalBlocker, so a restore cannot re-emitfftFpsChangedand re-save. - The slider save sits before the adaptive-throttle return.
restoreClientOwnedDbmRangeflushes pending writes first, so a deferred edit is not read back stale.- The 3D floor resync mark is set only around the one emit (
SpectrumWidget.cpp:3781) and the adopt skips it; the 2D auto-floor returns atlocalOnlybefore the adopt. - Out-of-range, fractional, non-finite and wrong-typed stored values are all refused through the real store.
- The slider's programmatic syncs (
- Not done: an HL2 on the air; Linux; 3D with live frames; Reset to Defaults and Clone to all Pans (pinned as source text only); the older rows of your fail-before table.
Collateral: the maintainer's blocker (ownership by backend identity) is resolved at 7a5ca748, and I checked it myself against real backend objects and the mutation above, not from the reply. His 3D-emit nit is resolved at dabf83e3. The bot's nits were fixed in 39cf03bb and e60d1daf. The #6129 overlap is settled by his 2026-10-05 ruling that this PR owns dbmRanges[pan] and its helpers.
Non-blocking, for the future
- Windows checkouts with
core.autocrlf=true:readSource()inclient_display_pan_settings_test.cppdoes not normalise\r\n, so the multi-line pins fail there. CI checks out LF and is unaffected.QString::fromUtf8(file.readAll()).remove(QLatin1Char('\r'))fixes it; other source-text tests share the exposure. - Squash citation: the change implements Principle III (the client persists only state the radio does not store, decided by what this radio can save and restore), and the review turned on exactly that. I suggest III on the squash subject over VIII.
docs/agents/settings.md, Settings Authority Policy: it says an HL2's client memory is persisted "ONLY throughRadioStateMemory'sOperatingStatedocument". The waterfall rate, FFT AVG and now this live inClientDisplay, which the maintainer asked you to keep. That is a docs follow-up for whoever ownsdocs/agents/, not something for this PR.- No issue is filed for the use case;
Refs #5678and the observed HL2 repro carry it, which is enough here.
What happens next
No code blocker remains. I am approving the code. I am deliberately not merging, and not trying to clear the standing review. That is our reasoning, and these are the three parts of it:
- The
CHANGES_REQUESTEDis his, and it is still active. It was filed 2026-10-04 against295bff6e;reviewDecisionis stillCHANGES_REQUESTED. Its one blocker (ownership by backend identity) was fixed in7a5ca748, and his own 2026-10-05 review at that head says "Blockers: None". Its three non-blocking items are also done (theTestSettingsProfileisolation, the staleHERMES.mdsentence, the #6129 ruling). But he followed it with aCOMMENTEDreview, not an approval or a dismissal, so whether it still stands is his to say. Our approval does not clear it and we should not route around it. - His 2026-10-05 review names him for a decision I cannot find on the PR. Under "Needs maintainer decision (@ten9876)", item 2: the two new third-party-visible capability fields (
PanFrameRateShaping::clientPersistsFrameRate,PanAmplitudeModel::clientPersistsDbmRange) "need your sign-off". His 2026-10-05 00:30 ruling coversdbmRangesownership only. I found no ruling on the capability fields. - The HL2 restore has not been seen on an HL2, by anyone. My drive was the demo with an HL2-shaped declaration.
@ten9876, two explicit questions for you, since you asked for them:
- Do you sign off on the two capability fields? I read them against the
BackendPanAveraging::clientPersistsAveragingshape and the frozen count of 70, and they match. - Does your 2026-10-05 review at
7a5ca748stand in place of your 2026-10-04CHANGES_REQUESTED? If so, please dismiss it or re-review so the PR can merge. If you want anything further first, please say what.
Until you answer, this stays unmerged.
Suggested squash subject: fix(gui): FFT FPS and the dBm range survive a restart on the Hermes-Lite 2. Principle III. (#6140)
73,
👨🏼💻 Co-authored by Claude Sonnet 5.5
ten9876
left a comment
There was a problem hiding this comment.
Line references are at the PR head. Reviewed at head d60f4660, and as a merge of that head with current main 5f09371e. The merge is clean, and it brings in the 52 px title bar and the -g1/test-source build changes, both of which touch MainWindow. Everything I ran, I ran on that merge.
Issue fit: yes, against the PR's own stated intent. No issue is linked; it Refs #5678 (rows P16 and the FPS half of P7). On a Hermes-Lite 2, FFT FPS and the dBm range are stored in the per-radio ClientDisplay document and restored from it. Each sits behind an owner the backend declares, and each declaration has one accessor that both the save and the restore read. Every other family keeps main's behaviour. The earlier blocker (ownership inferred from "not a Flex") is resolved; I checked the six capabilities() declarations myself rather than taking the reply's word for it.
Scope
| File / group | Claimed? | Verdict |
|---|---|---|
src/core/ClientDisplaySettings.h: fftFps, dbmRanges, per-field pendingWriteKey |
yes | in scope |
src/core/DbmRangePlausibility.h (new): predicate moved verbatim from MainWindow_Wiring.cpp, plus dbmNarrowsToFloat |
yes | in scope; the - side of the move is byte-identical |
src/core/backends/RadioCapabilities.h + six backends' capabilities() |
yes | in scope; new third-party-visible capability surface (see below) |
src/gui/MainWindow.h, MainWindow_Wiring.cpp, MainWindow_Session.cpp |
yes | in scope |
src/gui/SpectrumWidget.{h,cpp}: one flag set around the 3D zoom-floor emit |
yes (update block, dabf83e3) |
in scope |
3 tests + tests/tests.cmake (both new targets in AETHER_SETTINGS_CONSUMERS) |
yes | in scope |
docs/HERMES.md, the Icom design doc, the caps map, touchpoint tags and manifest |
yes | in scope |
13 merge: origin/main / manifest-regen commits |
yes | fine |
Everything in the diff is explained by the body. There is no CHANGELOG.md entry, no wire verb, and no changed default. The only removed code is the predicate that moved to the header, plus the hand copy in dbm_range_plausibility_test. No guard was removed.
Blockers
None.
Needs maintainer decision (@ten9876)
These two items were raised in your 2026-10-05 review and in @NF0T's approval. I can't find either answered on the PR, so I'm leaving them with you rather than restating a verdict:
- Sign-off on the two capability fields.
PanFrameRateShaping::clientPersistsFrameRateis a new optional record (RadioCapabilities.h:100-108);PanAmplitudeModel::clientPersistsDbmRangeis a new field (:150-154). They follow the shape ofBackendPanAveraging::clientPersistsAveraging, and the boolean count stays at the frozen 70. - Whether your 2026-10-04
CHANGES_REQUESTEDat295bff6estill stands. Its blocker is fixed at7a5ca748, and your own review at that head says "Blockers: None".reviewDecisionis stillCHANGES_REQUESTED, though.
Nits (non-blocking)
- Coverage boundary: neither model write is pinned.
src/gui/MainWindow_Wiring.cpp:3175is thepan->setRange(minDbm, maxDbm)insideadoptClientOwnedDbmRange, and:3201is thepan->setRange(...)insiderestoreClientOwnedDbmRange. Neither is checked by any test. I deleted the adopt's model write and kept its store write:client_display_pan_settings_teststill exits 0, and nothing in the test mentionssetRange. "Restore into the pan model, not the widget" is the design point that separates this PR from #6129, and it is now proven only by @NF0T's patched-demo drive and by reading the code. I am not asking for another source-text pin, becausedocs/agents/tests-ci.md:82-88prefers behavioural seams andMainWindowhas none. I'm naming the gap so it isn't read as covered. If a seam turns up later (for example, the adopt/restore pair moving into a small helper that takes aPanadapterModel*), that is the place for the test. - Known sibling gap, already disclosed by the author. The band-stack restore at
src/gui/MainWindow.cpp:11084-11087still sendsdisplay pan set … min_dbm=as a raw command. An HL2 drops it, so a band-stack recall neither moves nor stores the range there. This is unchanged frommainand worth its own issue.
What I verified, and what I tried to break
-
Built
AetherSDRand the three new test targets on the merge with5f09371e(Linux, GCC, Qt 6.12.0, shared core, RelWithDebInfo -g1).client_display_pan_settings_test,spectrum_overlay_fft_fps_bounds_testanddbm_range_plausibility_testall pass. -
Ran the static gates on that merge, because required CI ran at 2026-10-05 02:52Z and
mainhas moved since:gen_touchpoint_manifest.py --check: up to date.check_capability_records.py --strict: 70, at the frozen 70.check_command_plane.py: 369 = baseline.check_engine_boundary.py: 102 findings, 0 would block.
-
Socket preflight: no added or modified test opens, binds, listens or connects. The backends in
client_display_pan_settings_testare constructed only to readcapabilities(). -
Mutation: the adopt without its model write survives (nit above). Afterwards I restored the file; its sha256 matches
git show HEAD:, and I touched it. -
Bridge drive, demo only, on the merged head. Settings were isolated (
AETHER_SETTINGS_DIR, scratchHOME), withAETHER_AUTOMATION_NO_TX=1and socket/tmp/aethersdr-pr6140. I connected with raw-JSONconnect local serial DEMO-0001and confirmed"model":"AetherSDR Demo","serial":"DEMO-0001","transmitting":false.step result get pan active"fps":25,-130 / -40panMenuDisplayBtn click,displayFftFpsSlider setValue 12"fps":12kill -TERM, then read the storeradio_settingsholds no rows: nothing was written for a backend that declares no ownerrelaunch on the same store, reconnect DEMO-0001"fps":25,-130 / -40So the opt-out holds on Linux at current
main. Both instances were mine and both are closed; no live radio was touched. -
Read hostilely, and it held:
- All three
dbmRangeChangeRequestedemit sites:- the 2D auto-floor (
SpectrumWidget.cpp:4132) returns atlocalOnlybefore the adopt; - the scale arrows (
:9879) are an operator gesture; - the 3D zoom-floor resync (
:3782) is excluded by the flag.
- the 2D auto-floor (
- Both
dbmRangeDragFinishedsites (:112883D floor drag,:11332scale drag) are operator gestures. - The flag is sound by construction: a same-thread
connectis a direct call, the flag brackets the singleemit, and the slot reads it synchronously. That is reasoned from code; I did not drive 3D with frames. - Profile-load holds and
!ownedByClientboth return before the adopt. - Both prime sites (
MainWindow_Wiring.cpp:4073,MainWindow_Session.cpp:1854) restore beforesw->setDbmRange(pan->minDbm(), …). TheisConnected()term inclientOwnsPanDbmRange()makes the pre-connect wirePanadapter call a no-op, and the session prime covers it. - The FPS slider save sits before the adaptive-throttle return.
fpsReported(:3262-3270) does not save, so neither the cap nor a radio report reaches the store.
- All three
-
Not done: an HL2 on the air (none here), the opt-in path on Linux (@NF0T drove it on Windows with a patched demo), 3D with live frames, Windows/macOS builds.
Recommendation: needs maintainer decision. The code is clean, with no blocker at this head or on the merge with current main. What remains is the two rulings above, which are @ten9876's to make.
— Jeremy KK7GWY + Claude
ten9876
left a comment
There was a problem hiding this comment.
Maintainer sign-off at d60f4660. I accept the two capability fields, PanFrameRateShaping::clientPersistsFrameRate and PanAmplitudeModel::clientPersistsDbmRange, in the shape of clientPersistsAveraging. This approval supersedes my 2026-10-04 CHANGES_REQUESTED at 295bff6e, whose blocker was fixed in 7a5ca748. The non-blocking coverage nit in my review today stands as a follow-up.
— Jeremy KK7GWY + Claude
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>
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 #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. Squashed-from: #6129 Co-authored-by: Sotiris <299180028+tropo1234@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… and headroom recovery writes only for Auto Floor on an owned pan. Principle II. - Both dBm-range gestures adopt the operator's range again on a backend with no command plane (HL2, aethersdr#6140): a dropped dispatch no longer returns before adoptClientOwnedDbmRange(), which is a no-op on a radio-owned range. - An accepted range reply keeps a radio status received after the write (Principle II; FlexLib's handlers ignore an accepted reply). A rejection falls back to the range the radio last confirmed, not an earlier request still in flight. - Headroom recovery, floor and ceiling, rewrites min_dbm/max_dbm only with Auto Floor on and only on a pan this session owns (maintainer ruling). A clipped floor holds baseline acquisition only while a recovery can still come, so a foreign pan or a floor at -180 dBm no longer stalls auto floor. Recovery helpers never move an endpoint inward. - The rewire epoch is a SpectrumWidget member, not a dynamic property, and one ownership predicate serves the widget and the dispatch path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Flex FFT headroom recovery can lower the peak ceiling while trying to unclip the noise floor. Ordinary radio status also leaves the GUI encoder cache stale, and accepted reply-only range writes do not converge the canonical pan model. This repair preserves the opposite encoder endpoint, validates before decoder mutation, and completes range writes on the current radio reply. Refs #6238. **This is a partial repair: complete band/Go To/preamp trace-bounce removal is not claimed, and the issue remains open.** ## Linked display reports The six open KN7K panadapter display reports are linked below, together with the related Auto Floor OFF report #5525. These are reference links, not closing claims; the PR's implementation and validation scope remain the partial range/controller repair described below. | Issue | Relationship to this PR | |---|---| | [#5685 — FFT peaks clipping](#5685) | Primary clipping report addressed by the bounded headroom/range repair; KN7K's exact Windows/8600 environment remains unverified. | | [#6219 — FFT peaks clipping #5685 — reminder](#6219) | KN7K confirms the same clipping report persists in 26.10.1; linked with #5685. | | [#5204 — Inconsistent CW signal on waterfall](#5204) | Its RX flat-topped FFT screenshot is relevant to clipping. Its CW TX/waterfall-width symptom is not tested by this PR. | | [#4221 — Waterfall a little jumpy/not smooth](#4221) | Related waterfall cadence report, linked for tracking; not reproduced or claimed fixed by this range repair. | | [#3970 — Keep S and Noise Floor readings in minimized Panadapter applet](#3970) | Related display request, linked for tracking; minimized-readout behavior is unchanged and untested here. | | [#2371 — Two receivers do not center when switching between them](#2371) | Related panadapter centering report, linked for tracking; centering behavior is unchanged and untested here. | | [#5525 — Reference level rises on frequency changes with Auto Floor OFF](#5525) | Related report by @aaldocampana-art. The current auto-floor-ON proof does not establish a fix for the OFF case. | Earlier KN7K reports [#3483 — band-to-band floor positioning](#3483), [#3731 — FFT artifacts](#3731), and [#3804 — post-TX floor recovery](#3804) remain historical context. Those closed reports are not reopened or reclassified as fixed by this PR. ## Change - Expand only a clipped encoder endpoint, bounded by FlexLib's -180/+20 dBm endpoints and the client's 180 dB aperture. Detect sparse ceiling clipping using the actual decoder row scale. Saturated ranges become no-ops, and a recovery helper never moves an endpoint inward. - **Headroom recovery (floor and ceiling) rewrites `min_dbm`/`max_dbm` only while Auto Floor is on, and only on a pan this session owns.** Flex stores the range per band, so with Auto Floor off the client never changes the operator's range. This was a maintainer ruling. It also changes the floor recovery `main` ran with Auto Floor off. - Refresh the per-pan encoder cache from ordinary status. Rejected, superseded, rewired or foreign-pan callbacks cannot complete a newer request. **A radio status received after the write wins over the request, accepted or not** (Principle II; FlexLib's `SetLowDbmReply`/`SetHighDbmReply` ignore an accepted reply). With no such status, an accepted reply-only write converges the model, and a rejection falls back to the range the radio last confirmed. - Validate and confirm dispatch before committing decoder/handshake state. Both existing RadioModel command entry points retain confirmed foreign-pan refusal. On a backend with no command plane (HL2), both dBm gestures still adopt and persist the operator's client-owned range (#6140). - Keep the native auto-floor presentation axis separate from encoder headroom. Reacquire once after the existing 750 ms settling interval plus three fresh floors within 1 dB over at least 100 ms; restore ordinary transient rejection afterward. Hold headroom requests during settling. A clipped floor blocks baseline acquisition only while a recovery can still come: on a foreign pan, with Auto Floor off, or with the floor at -180 dBm, the baseline acquires instead of stalling. - Extend existing trace diagnostics with decoder metadata, pre-EMA input statistics and the last native 2D draw's floor/axis. These local counters do not identify VITA wire encoding generations. The generated touchpoint change records reuse of the existing core dBm validator by the GUI range helper. No new engine dependency, raw command-plane site, capability or bridge verb is added; the frozen raw-site count remains 369. Host absolute-bin and Kiwi paths retain their existing acquisition behavior. ## Proof Agent automation bridge, native Cocoa/Qt 6.12/Metal, RX-only FLEX-8400M, firmware 4.2.18.41174. MW broadcast signals, native band buttons, keyboard Go To, local band-stack bookmarks, and RF-gain controls were exercised. These are maintainer 8400M screenshots, **not KN7K's**. They are separate ambient RX captures, not a controlled RF amplitude comparison. | Regression | Observed repair / remaining result | |---|---| | R1: encoder ceiling | Starting at `-178/-88`, automatic upper recovery reached `-178/-40`, keeping the minimum fixed. The after image shows intact AM peaks around `-42 dBm` instead of the old `-88` plateau. | | R2: floor recovery | `-100/-16` recovered to `-124/-16`; the maximum stayed `-16`. Presentation span stayed 84 dB while the encoder aperture expanded. | | R3: status cache / reply-only completion | After ordinary status `-80/-10`, recovery requested `-104/-10`, then `-128/-10`, with successful replies and no matching self-status range echo in the captured bracket. The model and decoder metadata both converged to `-128/-10`. | | R4: invalid / refused writes | Tests reject an invalid `-202/-112` range, NaN, dispatch refusal and a confirmed foreign pan before state commit. Invalid ranges were not sent to hardware. | | R5: band / keyboard / bookmarks | The controller guards are demonstrated, but trace transients persist: 40m→80m reached 35.26% before settling near 25%; a keyboard transition reached 63.31%; bookmarks reached 29.52%. | | R6: preamp | 0→32 dB retained encoder `-124/-16`, preventing the intermediate candidate's false expansion to `-148/-16`. The rendered trace still moved (24.77–36.30%, ending 25.49% at an 84 dB presentation span). Reverse and 86% target cases also retained transients. | Before: earlier unfixed maintainer 8400M reproduction, `abc5b26`, with encoder ceiling `-88 dBm`.  After: current production changes, automatic ceiling recovery and intact MW signal peaks.  Native captures were made before committing as `30882701-dirty`; only a source comment and stronger test assertions changed before signed commit `7725ebe2` was rebuilt and tested. Rendered telemetry describes draw submission, not scanout; the after image was inspected separately. Raw logs and the full evidence archive remain local at the operator's request. ## Validation and limits - Full build with `cmake --build <build-dir> -j22` passed at signed commit `7725ebe2`. - `QT_QPA_PLATFORM=offscreen ctest --test-dir <build-dir> --no-tests=error`: **7/7 selected tests passed** at `793ab77c` on Linux/Qt 6.12: `panadapter_dbm_range_test`, `spectrum_preview_logic_test`, `dbm_range_plausibility_test`, `noise_floor_auto_adjust_gate_test`, `pan_frame_guard_test`, `client_display_pan_settings_test` (the #6140 HL2 pin), and opt-in `spectrum_confirmed_geometry_test`. The last one needs `-DAETHER_BUILD_SPECTRUM_GESTURE_TEST=ON` at configure. Offscreen widget checks are logic/gesture proof, not GPU rendering proof. - Each landing-pass fix goes red when reverted: the HL2 adopt (`client_display_pan_settings_test`), radio-status-wins (`panadapter_dbm_range_test`), the Auto Floor gate, the ownership gate, and the -180 dBm limit (`spectrum_confirmed_geometry_test`). - Demo bridge (Linux, `DEMO-0001`, `transmitting: false`): clicking the dBm strip's up arrow moved the pan model and encoder to -130/-50 and then -130/-60, with the display axis following. This held before and after a disconnect and reconnect, which exercises the rewire epoch. - Scratch mutation builds fail when validation, endpoint preservation, accepted-reply completion, settle timing or clipped-baseline protection is removed. The widget mutant fails three relevant assertions; production binaries were not replaced. - Strict engine boundary, generated touchpoints, strict capability records, strict command plane, strict test registration, frozen CI test gate and generated bridge docs passed. - RX session cleanup confirmed TX/tune idle, owned app stopped, shared RF gain and line-out mute restored, front-panel client retained, and radio reservation released. - CI, KN7K's Windows/FLEX-8600/firmware-4.1.5 environment, resize/y_pixels stress, a second owned pan and post-TX behavior remain unverified. Auto Floor OFF report #5525 is outside this proof. The remaining bounce needs raw VITA frame/pixel correlation with decoding range and TCP status timing. The agent automation bridge has no raw VITA capture/replay surface; the noninteractive packet-capture privilege check required a password, and no pcap was captured. A trace hold/blanking policy or automatic display-span change would require maintainer UX direction. This PR introduces neither and should not close the complete bounce investigation. Generated with OpenAI Codex (GPT-6 Astra) --------- Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#6255) Refs #5678 (HL2 parity rows), #5263 (the drop notice). No `Fixes` line: I found no issue for this. ## Limits first - **Nothing here was seen in the running app or on a radio.** The defect is a source reading of `main`; the fix is tested on `RadioModel` with an injected backend, and the four `MainWindow` call sites are pinned as text only. - **This is not an M4 conversion.** The `display panafall set` text is not replaced by a typed intent. It moves out of `MainWindow_Wiring.cpp` into the two wire sites `RadioModel` already has. `tools/check_command_plane.py`: `MainWindow_Wiring.cpp` 20 to 14, `RadioModel.cpp` unchanged at 135. - **One design choice is yours: the Black Level gate.** It reads `RadioCapabilities::panBinsAbsolute()`, the same proxy for the row unit the renderer reads (`radio-capabilities-map.md` calls it a proxy). If the row unit gets its own signal later, this gate should follow it. An undeclared `panAmplitude` reads as not absolute, so a backend that computes dB rows and forgets the declaration gets a false notice, not a missing one. - **On Icom, Black Level keeps its notice, also when it comes from Clone to all Pans or Reset display defaults.** Reset turns auto black on, so the value it writes is not in use at that moment; the notice is still raised there, as it is on `main`. I did not make that case quieter. - **ANAN and RTL-SDR are reached** (both declare `binsAbsolute`), and the test does not run with their capabilities: only Hermes-Lite 2, Icom and Flex. - **Two other false notices on a Hermes-Lite 2 are not touched:** the dBm scale (`sendDbmRangeCommand`, beside the dBm-range work of my #6140, now merged) and `cw break_in` (left open by #6111). - **The test ends in a source-text pin** (six checks). It names its claim. - The branch is rebased onto `121340c0`, with no conflict; head `182bb99c` adds two review commits on `1935624d`. Two open PRs edit the same files: #6241 merges with this branch without conflict; #6199 conflicts with it only on adjacent new lines in the `tests/tests.cmake` test list (it also conflicts with `main` itself in `RadioModel.cpp`, independently of this PR). Neither touches the four sites, the two setters, or the gain and black text. Neither merged tree was built. ## What was wrong on main On a backend with no Flex command plane, four Display-panel controls set their value on the `SpectrumWidget`, which draws it, and then also sent Flex wire text: | control | site in `src/gui/MainWindow_Wiring.cpp` | text | |---|---|---| | WtrFall Gain | `wirePanadapter`, `SpectrumOverlayMenu::wfColorGainChanged` handler | `display panafall set <wf> color_gain=N` | | Black Level | `wirePanadapter`, `SpectrumOverlayMenu::wfBlackLevelChanged` handler | `display panafall set <wf> black_level=N` | | Clone to all Pans | `MainWindow::cloneDisplaySettingsToAllPans` | both, per target pan | | Reset display defaults | `wirePanadapter`, `SpectrumOverlayMenu::displaySettingsReset` handler | `color_gain=50`, `black_level=15` | Each went through `RadioModel::sendCommand()` to `sendCmd()`, which has no command plane to write to, logs the drop and emits `commandDropped`. `MainWindow` shows that once per session as "nothing was sent to the radio". So the notice was raised for a control that had acted, and the one-shot was spent. `RadioModel::setWaterfallColorGain()` and `setWaterfallBlackLevel()` already return early without a command plane. The four sites did not use them: those setters address the active pan, the sites address a named one. Manual Black Level acts on a Hermes-Lite 2 since #6115 (`WaterfallLevelMap::manualBlackThreshold` on absolute-dB rows). ## The change - `RadioModel` gains `setWaterfallColorGainFor(panId, gain, autoBlack)` and `setWaterfallBlackLevelFor(panId, level)`. All four setters go through two private senders, `sendWaterfallColorGain()` and `sendWaterfallBlackLevel()`, which hold the only wire site and the only command-plane gate for each key. - The four sites call those setters after the widget call, and pass the pan's `wfAutoBlack()` with the gain. They no longer build wire text. - **WtrFall Gain** is withheld without a command plane where the client draws it: auto black on, or `panBinsAbsolute()`. With manual black on any other row (Icom), every row is black whatever the gain, so it goes to `sendCmd()` and is reported. - **Black Level** is withheld without a command plane only where `panBinsAbsolute()` is true (Hermes-Lite 2, ANAN, RTL-SDR). Elsewhere it goes to `sendCmd()` and is reported. On Icom the manual black point is still in tile units against a dB row, so every position draws black: the slider does nothing there and says so. - The connect-time replay (`setWaterfallBlackLevel()` from `panadapterInfoChanged`) stays quiet on every backend with no command plane, as on `main`. No operator moved anything. - No capability is added and no family is named. - `docs/architecture/radio-capabilities-map.md` records the flag's new consumer. **A Flex is sent what it is sent today**: same text, same pan, same order. The sites used `sendCommand()` and the setters use `sendCmd()`; the only thing `sendCommand()` adds is the pan-ownership gate, which matches `display pan set ` and not `display panafall set `. ## How this sits with the #5919 review That review kept the notice for an operator-moved control the backend cannot apply, because nothing happened and the notice says so. It was about receive controls; nobody has ruled on display text, and I am not reading it as covering this. The rule used here is narrower than "no command plane, no notice": the send is withheld only where the control has acted in the client. Where it has not (Black Level on Icom, and WtrFall Gain on Icom with manual black), the notice stays and the test pins it. If you would rather keep the notice on these four until pan commands get a typed intent, close this and I will leave them. ## Tests `pan_display_drop_notice_test` (new, socket-free, 41 checks). The injected backend carries `capabilities()` read off a real `Hl2Backend`, `IcomCivBackend` and `FlexBackend`; panes are made the way a backend without Flex status makes them. - **Hermes-Lite 2 capabilities, no command plane (12 checks):** the renderer's law changes with gain (auto and manual black) and with the manual black level for a dB row; none of the four raises `commandDropped`; the same gain text sent raw is still reported, so the alarm is live in the fixture. - **Icom capabilities, no command plane (13 checks):** with auto black gain raises nothing; with manual black no gain changes a dB row and gain raises `commandDropped`, with the exact text; every Black Level position draws a dB row black and the slider still raises `commandDropped`; Clone reports the black level only with auto black, and both keys with manual black; Reset reports the black level only; the connect-time replay raises nothing. - **A command plane (10 checks):** the wire text compared whole through a command sink on an unopened `RadioConnection`, for each control, for the target pan and not the active one, gain before black. With absolute-dB rows and a command plane the black level is still sent. - **Source-text pin (6 checks).** The claim: each of the four sites sets the widget and then calls the model setter, and `MainWindow_Wiring.cpp` builds no gain or black wire text of its own. No behavioural seam reaches it: the sites are `MainWindow` lambdas and no test target constructs a `MainWindow`. It shows how the sites are written, not that they run. **Mutations**, each as a scratch commit on `182bb99c` through the full suite: | mutation | failing checks of 41 | other tests | |---|---|---| | both gates removed, the four sites as on `main` | 15 | `icom_family_test` | | Black Level gated on `hasCommandPlane()` alone | 4 | none | | colour gain ignores auto black (row unit only) | 4 | none | | colour gain ignores the row unit (auto black only) | 4 | none | | colour gain always withheld (the `1935624d` rule) | 2 | none | | the connect-time replay reports its drop | 1 | `icom_family_test` | The legs share one capture of dropped commands, so a wrong drop is also counted by the checks after it in that leg. Full ctest on macOS (Qt 6.12.0, Ninja, RelWithDebInfo) at `182bb99c`: 654 of 658 pass, at -j8 and on the serial rerun. The four failures are known on clean `main` on this machine, each with only its known check lines: `control_receive_test` ("RTL adjustable filter qualification excludes legacy WFM", #6248), `vkamp_connection_test` ("commands after a disconnect are dropped, not queued", #5254), `peripheral_auth_dialog_test` (#6008, a serial-port name of this host) and `unified_title_bar_test` (its two menu checks, #6224). This PR changes `MainWindow_Wiring.cpp`, but only the four display sites; the same two title-bar checks fail identically on two other branches at the same base that do not touch it. Nothing else this PR touches is built by those tests. Every script step of the `Static checks` job was run locally at `182bb99c` with stdin from `/dev/null` and is clean: colour ratchet +0, `gen_touchpoint_manifest.py --check` up to date with no change, `check_a11y.py` on the one changed GUI file with no findings. The `Sanitizer option configures` job needs the CI image and was not run. ## Not verified - Nothing in the running app; nothing on a Hermes-Lite 2, an Icom or a Flex. - Whether each of the four controls is enabled and reachable on a Hermes-Lite 2 today. - That the Icom Black Level slider is dead, and WtrFall Gain with it under manual black, is the renderer's arithmetic plus `IcomCivBackend` sending its scope rows through `toDbm`; I have no Icom. - That the three sites pass the pan's own `wfAutoBlack()` is pinned by source text only. - ANAN and RTL-SDR, beyond their `binsAbsolute` declaration. - Linux and Windows builds: CI will say. 73, ON8ST 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…I. (#6139) **Update after review, `ea2cf144` (2026-10-05):** ownership of the flag is now declared, not inferred. Only the Hermes-Lite 2 declares the client its owner (`RadioCapabilities::txWaterfallClientFlag`, `clientPersistsFlag` true). The demo, ANAN, Icom and RTL-SDR declare nothing and keep `main`'s behaviour: the toggle sends the wire text, the model stays false, nothing is stored or restored. That is narrower than this PR was. On the demo, `txwaterfall on` answers with the radio-echo note again. Full suite at this head: 644 of 645; the one failure is `peripheral_auth_dialog_test` (#6008), which fails on clean `main` too. Source line numbers below are `main` at `85f2a124`. Refs #5678 (HL2 parity; "Show TX in Waterfall", row P30 of our status table). No issue exists for this. ## Limits, first - **Nothing was keyed.** On an HL2 the toggle now sets the flag. What the waterfall draws during an over on an HL2 has not been seen, and what that radio's receive path reads while it transmits has not been measured. A `true` model flag shows neither the colour law nor real receive IQ while keyed; the demo cannot key at all. - **The fix has not been seen in a running app.** The dead toggle was (2026-10-01, bridge verb, 6 of 6 polls false). The fix is tested socket-free on the real models and dialog. - **It reaches the Hermes-Lite 2 only.** The request, the store and the restore are gated on one accessor, `RadioCapabilities::clientPersistsShowTxInWaterfall()`, which reads a declared per-feature record. Flex, Icom, ANAN, RTL-SDR and the demo leave the record absent. On Icom, ANAN, RTL-SDR and the demo the toggle is as dead as on `main`. Engaging another family is one declaration, for that family's owner or the maintainer to make. - **Yours to move:** the keyed rows take the colour law a Flex's keyed rows use (`dbmToWaterfallLevel`). On an HL2 they are absolute dB (dBFS under a dBm label); whether the law suits them is unknown until someone keys one. - **Persistence is in the radio's settings document**, not a flat key: one optional bool, `showTxInWaterfall`, in the existing per-radio `ClientDisplay` feature document. No schema bump. Written and read only where the backend declares the client its owner. ## What was wrong on `main` On a Flex the flag is the radio's: `transmit set show_tx_in_waterfall=` tells it, and its transmit status echoes it (`FlexBackend.cpp:1193`). That echo is the only writer of `TransmitModel::showTxInWaterfall()`. - The Radio Setup button sends the wire text unconditionally (`RadioSetupDialog.cpp:2717`), and so does the bridge verb (`AutomationServer.cpp:3599`). - A radio with no command plane drops it (`RadioModel.cpp:9764`) and nothing echoes, so the flag can never become true. The demo has a command plane and no handler for the flag, so nothing echoes there either. - While keyed, the pane holding the TX passband draws rows only when the flag is true (`SpectrumWidget.cpp:8488`). With it false that pane's waterfall stands still for the whole over. There is something to draw: the HL2 mutes its demodulator while keyed and the spectrum keeps running on real IQ (`Hl2RxDsp::setAudioMuted`). ## The change - `RadioModel::requestLocalShowTxInWaterfall(bool)`, the shape of the existing `requestLocalPanWeightedAverage`: where the backend declares the client the owner of the flag it applies the flag and returns true; everywhere else it returns false and the caller sends the wire text, which only a Flex answers. No raw command is added above the seam (`check_command_plane`: 400, baseline 400). - It writes the model through `TransmitModel::applyChanges`, the path an echo takes. - It stores the value per radio, on the same declaration. A disconnect clears the transmit model (`TransmitModel.cpp:43`) and no status puts the flag back, so `RadioModel::onConnected()` restores it, on the same declaration. - The Radio Setup button and the `txwaterfall` verb ask the model first. - `RadioCapabilities::txWaterfallClientFlag`: a new optional per-feature record with one field, `clientPersistsFlag` (no default). Engaged by the HL2 only. No top-level capability boolean is added (`check_capability_records`: 70 of 70). `SpectrumWidget` is not changed. Default unchanged: off. ## Tests `show_tx_in_waterfall_client_flag_test` (new, socket-free, real backends): on the real `Hl2Backend`, request, store per radio, restore on connect, and the bridge verb; on the real Icom, ANAN, RTL-SDR and demo backends, each with a `ClientDisplay` document that already holds the flag, nothing is restored, the request declines and the document is untouched; the same on a real `FlexBackend` (the echo still works) and on a backend that engages the record with the field false; the bridge verb's radio-echo note on Flex and on the demo; the store's refusals. `radio_setup_show_tx_waterfall_test` (new, production dialog, real backends): on an HL2 a click sets the model and sends no wire text; on an Icom, which declares no owner, and on a Flex the click sends the wire text and the model is not set. No source-text pin. Fail-before, one build and one run per row, failing checks in the two tests: | mutation | client flag test | dialog test | |---|---|---| | this branch | 0 | 0 | | `main`'s behaviour (request declines, connect restores nothing) | 11 | 1 slot | | the request does not write the model | 5 | 1 slot | | the connect edge does not restore | 1 | 0 | | the request is taken on a Flex too | 4 | 1 slot (Flex) | | the request does not store | 3 | 0 | | the button sends the wire text unguarded | 0 | 1 slot | | the bridge verb skips the local route | 2 | 0 | | the store ignores who owns the flag on read | 1 | 0 | | the ownership accessor is true for every backend | 40 | 2 slots | | the ownership accessor is false for every backend | 15 | 1 slot | The two accessor rows were measured at `ea2cf144`, one build and one full run each. The other rows were measured at earlier heads, before the opt-out checks existed, and were not re-run. Full ctest, macOS, Qt 6.12.0, at `ea2cf144`: 644 of 645 pass. The one failure is not ours; see below. Static checks (`static-checks.yml`, stdin from `/dev/null`): clean. `check_a11y` on `RadioSetupDialog.cpp`: 9 warnings, the same 9 as `main`, none on lines this PR touches. `check_capability_records`: 70 booleans, frozen at 70. The steps were run from this branch's `static-checks.yml`; `main`'s newer `check_build_options` steps are not on this branch and were not run. ## With the open PRs - #5924, #6117 and #6115 are merged and in this branch. #5919 is merged into `main` and is not in this branch. - #6140 (open, `7a5ca748`) adds its own records to `RadioCapabilities.h`; a trial merge with this head is clean and the merged header still counts 70 booleans. A trial merge with `main` `85f2a124` is clean. Neither merge result was built. ## Not verified - Anything keyed; see Limits. - Linux and Windows at this head. ANAN, Icom and RTL-SDR on a radio: they keep `main`'s behaviour by declaration, checked on their real backends, never connected. - A running app at this head, the demo included. - That the main window hands a restored flag to the spectrum widget. **Known red on `main`:** `peripheral_auth_dialog_test` (#6008) fails on this Mac at `ea2cf144` with the diagnostic it prints on clean `main` at `22507580` (pasted on #6134): "Clear code or Status is clipped at the default dialog size", `dialog 960x680, viewport 658, page minimum 784, horizontal scroll max 126` (a serial-port name on this host widens the page). Clean `main` was not re-run at `85f2a124`. `hl2_pan_create_async_test` and `vkamp_connection_test` passed here. 73, ON8ST 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Robbie Foust <rfoust@gmail.com>
Update,
d60f4660(2026-10-05):main12df6943merged in; one conflict, indocs/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 ownsdbmRanges[pan], its helpers and its accepted range; #6129 rebases onto it. Line references below are at7a5ca748;MainWindow_Wiring.cppgains four lines after:4269since.Update after review,
7a5ca748(2026-10-05). The change request is applied, and it narrows this PR:main. A demo run that showed FFT FPS restored at295bff6ewill, by the code, not reproduce at this head; that was not run.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 of7a5ca748with feat(anan): remember where the operator left the radio. Principle III. #6129 at04762258conflicts inMainWindow_Wiring.cppand the two touchpoint files;ClientDisplaySettings.hmerges as text and then holds two definitions ofDbmRange,dbmRange()andsaveDbmRange(). The comparison is in the review reply.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
fftFps[pan]anddbmRanges[pan] = {min, max}, in the existing per-radioClientDisplayfeature document that already holdswaterfallRates. No schema bump.PanFrameRateShaping, fieldclientPersistsFrameRate, read throughRadioCapabilities::clientPersistsPanFrameRate(). The dBm range: a new field in the existingPanAmplitudeModel,clientPersistsDbmRange, read throughRadioCapabilities::clientPersistsDbmRange(). Same shape asBackendPanAveraging::clientPersistsAveraging(RTL M1: confirmed receive state, free capture browsing and finer zoom #5924). No new top-level capability bool: the count stays at the frozen 70.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.mainis not touched. It still readsshapesDisplayRatesLocally()alone.fftAverages), kept only for a backend that declaresclientPersistsAveraging; the HL2 declares it false.PanadapterModel. That range is absolute dB (dBFS under a dBm label).main. Widening to another family is one declaration in that family's backend, for its owner or the maintainer to make; feat(anan): remember where the operator left the radio. Principle III. #6129 claims the ANAN side of the range.MainWindow_Wiring.cpp:168onmaintocore/DbmRangePlausibility.h.What was wrong on
mainA 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):SpectrumWidget.h:1666, 25) and pan wiring seeds the shaper from the widget (MainWindow_Wiring.cpp:3320).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 leavesradioOwnsDbmScaleat its defaulttrue,RadioCapabilities.h:439, so the gate atMainWindow_Wiring.cpp:3779does not stop it), and nothing echoes.core/ClientDisplaySettings.h:90-132) and, since RTL M1: confirmed receive state, free capture browsing and finer zoom #5924, FFT averaging for a backend that declares it (:48-88) have a client store.The change
dbm_range_plausibility_testnow includes the real predicate instead of a hand copy. One row added to the touchpoint manifest (240 withmainat12df6943merged in).MainWindow::clientPersistsFftFps(),MainWindow::clientOwnsPanDbmRange()), each reading oneRadioCapabilitiesaccessor.docs/architecture/radio-capabilities-map.mdhas 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 anfftAveragestable in #5924's shape; two fields in one deferral window.spectrum_overlay_fft_fps_bounds_test(new, real widget, in aTestSettingsProfile): 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
MainWindowmethods, 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 at295bff6eand not run again:main's wiring, and a store that neither reads nor writes the new tablesfftAveragesand the waterfall rate among themFull ctest, macOS, Qt 6.12.0, at
d60f4660(main12df6943merged in, no code of this PR changed): 645 of 646 pass at-j8; the one failure isperipheral_auth_dialog_test(#6008), which also fails alone. The run before it, at7a5ca748(main85f2a124merged 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, andvkamp_connection_test(#5254), which passed when rerun alone.hl2_receiver_count_restart_paced_testandlibre_radar_testpassed at-j8in this run.Static checks (
static-checks.yml, stdin from/dev/null): the script steps of theStatic checksjob pass, including the touchpoint manifest (240, up to date) andcheck_capability_records.py --strict(70 booleans, at the frozen 70); colour ratchet +0.check_a11yon the three changed GUI files: no findings. TheSanitizer option configuresjob was not run locally.With the open PRs
295bff6e): it conflicted inClientDisplaySettings.h,MainWindow.handMainWindow_Wiring.cpp, all resolved by keeping both sides. The tables do not overlap and its pending-write key has its own prefix.main.04762258): see the update block at the top.RadioCapabilities.hand to each backend'scapabilities(), in different places. A trial merge of7a5ca748with its head98be49a1is clean; the merged tree was not compiled.Not verified
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 cleanmainat22507580("Clear code or Status is clipped at the default dialog size": a serial-port name on this host widens the page; diagnostic on #6134). Cleanmainat85f2a124was not built here.hl2_pan_create_async_testpassed;vkamp_connection_test(#5254) failed at-j8and passed alone.73, ON8ST
🤖 Generated with Claude Code