Skip to content

fix(gui): no drop notice for a pan control that acted. Principle VIII. - #6255

Merged
rfoust merged 3 commits into
aethersdr:mainfrom
on8st:hl2/no-false-pan-notice
Oct 12, 2026
Merged

rfoust merged 3 commits into
aethersdr:mainfrom
on8st:hl2/no-false-pan-notice

Conversation

@on8st

@on8st on8st commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

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 fix(gui): FFT FPS and the dBm range survive a restart. Principle VIII. #6140, now merged) and cw break_in (left open by fix(radio): route VOX, monitor, speech processor, CW speed and break-in through the seam gate. Principle VIII. #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: Fix Flex FFT headroom and range reply convergence #6241 merges with this branch without conflict; refactor(aetherd): route remaining slice and transmit model controls #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

On a backend with no Flex command plane, WtrFall Gain, Black Level,
Clone to all Pans and Reset display defaults set their value on the
SpectrumWidget and then sent `display panafall set ... color_gain=` /
`black_level=` through RadioModel::sendCommand(). sendCmd() dropped the
text and emitted commandDropped, so the once-per-session "nothing was
sent to the radio" notice was raised for a control that had just acted.

The four sites in MainWindow_Wiring.cpp now call two per-pan setters,
RadioModel::setWaterfallColorGainFor() and setWaterfallBlackLevelFor().
The existing active-pan setters delegate to them, so RadioModel keeps
its two wire sites and MainWindow_Wiring.cpp loses six (command-plane
baseline 20 -> 14).

Colour gain is withheld on every backend with no command plane: the
client draws it on every row it renders. The black level is withheld
only where the client draws the manual black point in the row's unit,
RadioCapabilities::panBinsAbsolute() (HL2, ANAN, RTL-SDR). On Icom the
manual black point draws every row black, so the slider keeps its drop
notice. The connect-time replay through setWaterfallBlackLevel() stays
quiet on every backend with no command plane, as before.

A radio with a command plane is sent the same text for the same pan.
`display panafall set` is not covered by sendCommand()'s pan-ownership
gate, so going through sendCmd() directly changes nothing on the wire.

pan_display_drop_notice_test: the setters by execution on an injected
backend carrying the real Hl2Backend, IcomCivBackend and FlexBackend
capabilities, the wire text through a command sink on an unopened
RadioConnection, and the four MainWindow sites by source text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st
on8st requested a review from a team as a code owner October 7, 2026 17:50

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue fit

There is no linked issue, by the author's own statement ("No Fixes line: I found no issue for this"), and GOVERNANCE.md does not require one for a bug fix with a clear root cause. Reviewed against the PR's own stated intent, it does what it says: the four Display-panel sites in src/gui/MainWindow_Wiring.cpp no longer build display panafall set text, they call the two new named-pan setters, and the gate withholds the send exactly where the client has already drawn the value. I confirmed the defect is real on main rather than vacuous — a non-Flex pan does get a synthetic waterfall id at src/models/RadioModel.cpp:1522 (pan->setWaterfallId(neutralWfIdString(panIdx))), so the old !pan->waterfallId().isEmpty() guard did not save those sites from sendCmd()'s drop path at src/models/RadioModel.cpp:9840.

Scope

Everything in the diff is explained by the stated fix. Seven rows, no unbundling needed:

File What it changes Claimed in body Verdict
src/models/RadioModel.h:1019-1024 Declares the two …For(panId, …) setters Yes In scope
src/models/RadioModel.cpp:6707-6739 The two new setters; the two active-pan setters delegate Yes In scope
src/gui/MainWindow_Wiring.cpp (4 sites) Raw wire text → the model setters Yes In scope
tests/pan_display_drop_notice_test.cpp New socket-free test, 36 checks Yes In scope
tests/tests.cmake:6010-6022, 7865 Registers the target; joins AETHER_SETTINGS_CONSUMERS Yes In scope — satisfies the CMake settings contract
tools/check_command_plane.py:88 MainWindow_Wiring.cpp 20 → 14 Yes In scope — required by the ratchet, and exactly the six removed sends
docs/architecture/radio-capabilities-map.md:637-642 Records panBinsAbsolute()'s new consumer Yes In scope — AGENTS.md wants the caps-map doc to track consumers

No new public surface: no capability field, no protocol verb, no settings key, no family check. No CHANGELOG.md edit. No removed guard — I read the - lines; the only things deleted are the six sendCommand() calls and their pan && !pan->waterfallId().isEmpty() guards, and both guards are reproduced inside the new setters.

Socket tests: the PR adds none. tests/pan_display_drop_notice_test.cpp constructs real Hl2Backend, IcomCivBackend and FlexBackend objects, and I checked each constructor for bind(/listen(/connectToHost(/QUdpSocket — none are present, and startDiscovery is not called from the HL2 constructor. RadioConnection's constructor starts no thread, so the m_commandSinkForTest writes land synchronously, matching the established pattern in tests/tx_operation_integration_test.cpp:128. The "socket-free" claim holds. Worth knowing, not a finding: the HL2 and Flex constructors do start worker threads and timers (Hl2Backend.cpp ctor: m_ioThread->start(), m_dspBuildThread->start(), pollStateTimer->start()), so hl2Capabilities()/flexCapabilities() spin a thread set up and down per call. The test never runs the event loop, so no timer fires.

Blockers

None.

Nits (none blocking)

  • src/models/RadioModel.cpp:6713-6716 — the "colour gain always acts" claim holds only with auto-black on. The comment says "The client draws colour gain on every row it renders", and the body generalises it to "Icom included". That is true on the auto-black branch, but not on the manual one. With autoBlack = false and rowsAreAbsoluteDb = false (Icom), WaterfallLevelMap::manualBlackThreshold returns 160 - blackLevel, i.e. 60…160 in tile units, while every dB sample is negative — so (value - blackThresh) is ≤ −80 and level() pins to 0.0 for every colorGain. On Icom with Black Level in manual, WtrFall Gain draws nothing and the notice is withheld anyway, which is the inverse of the rule the PR states. The test's clientDrawsColourGain() only probes params.autoBlack = true, so it cannot see this. I'd keep the behaviour (the pan is already fully black there — that is the row-unit defect, not this one) and just narrow the claim: a word in the comment and the body, plus an Icom autoBlack = false row in the test as a documented boundary.

  • src/models/RadioModel.cpp:6736 — the gate reads "undeclared" as "relative rows". RadioCapabilities::panBinsAbsolute() is panAmplitude && panAmplitude->binsAbsolute, and the header comment above PanAmplitudeModel (src/core/backends/RadioCapabilities.h:135-137) says absence means "no backend has been read". IcomCivBackend never assigns the field, so today the false answer is correct there by accident as much as by declaration. No current backend is misrouted (ANAN and RTL declare it; Sim has a command plane so the gate is moot; the Kiwi pans early-return in the handlers), but a future backend that computes dB rows and forgets the field gets a false notice — the opposite error from the one being fixed. This reinforces rather than contradicts the author's own disclosure that the gate should follow a real row-unit signal when one exists; worth a sentence in the comment so the next reader knows which way it fails.

  • src/models/RadioModel.cpp:6725-6727 vs 6707-6710 — asymmetric guard. setWaterfallBlackLevel() keeps its own if (!hasCommandPlane()) return; while setWaterfallColorGain() relies entirely on the …For() variant's gate. Net behaviour is identical to main today for both, but the Black Level intent now lives in two places, so a later move of the gate has two edit sites instead of one. The comment explains why the early return is there, which is the right kind of comment; it is the duplication I'd flatten, not the note.

  • tests/tests.cmake:6016 — the new test is not in CI's gate. ctest -R in .github/workflows/ci.yml names a fixed handful of targets and pan_display_drop_notice_test is not among them (zero matches). All five checks on 1935624d are green, which proves it compiles on macOS, Linux and Windows but not that it ran anywhere except the author's machine. That is the frozen-gate policy working as designed, not a defect in the PR — I note it so the 36 checks are not read as CI-protected.

What I tried to break

  • The sendCommand() → sendCmd() swap on Flex. The body claims identical text, pan and order. Confirmed: sendCommand() (RadioModel.cpp:9430) adds only the #3977 ownership gate, which matches cmd.startsWith("display pan set ") — a string "display panafall set …" cannot start with that, so the gate was always a no-op for these six sends. All four sites ignored the bool return. The only loss is one qCDebug(lcProtocol) line. No behaviour change on Flex.
  • The active-pan delegation. setWaterfallColorGain/setWaterfallBlackLevel moved from activeWfId() to panadapter(m_activePanId)->waterfallId(). I expected a fallback to diverge; activePanadapter() is literally m_panadapters.value(m_activePanId, nullptr) and panadapter(panId) is m_panadapters.value(panId, nullptr), so the connect-time replay addresses the same pan it always did. The test pins this on Flex with an explicit setActivePanId(f.pan1).
  • Null and unknown inputs. backendCapabilities() is m_backend ? m_backend->capabilities() : RadioCapabilities{}, so the new gate cannot dereference a missing backend; an unknown or empty pan id early-returns on !pan, and the test asserts that writes nothing and drops nothing.
  • Siblings left behind. Grepped the whole tree for color_gain= / black_level=. Outside RadioModel.cpp only the Sim/RadioConnection demo echo strings remain, which are inbound status text, not sends. Inside it, configureWaterfall() (RadioModel.cpp:12341) still sends black_level=15 color_gain=50 through an ungated sendCmd() with a retry callback that would drop twice — but its only caller (RadioModel.cpp:11328) sits inside Flex waterfall-status parsing, so it is unreachable without a command plane. Not a finding.
  • The rest of the Reset handler. If requestPanAverage / requestLocalPanWeightedAverage / requestPanDisplayRates fell through to wire text on a non-Flex backend, Reset would still burn the one-shot notice and the fix would be cosmetic. All three take a shapesDisplayRatesLocally() arm first (RadioModel.h:1350 — m_backend && !m_flexBackend) and send nothing, so on an HL2 the whole Reset path is now silent. The PR correctly keys its own gate on hasCommandPlane() rather than that predicate, which matters because Sim is non-Flex but does have a command plane.
  • Whether the test would pass against the unfixed code. The Icom legs assert the exact dropped text, the HL2 legs assert dropped.isEmpty(), and the fixture proves the alarm is live by sending the same gain text raw through sendCommand() first and checking it is reported. The source-text pin fails on main by construction. I also re-derived clientDrawsManualBlackLevel() by hand for both unit cases and it is driven by the real unit mismatch, not by an unset Params field — HL2 (atZero = -60) straddles the dB row and returns true; Icom (atZero = 160) cannot and returns false.
  • The static gate. Ran tools/check_command_plane.py on the head: 363 raw commands across 32 files against a baseline of 363, zero strict violations. 20 → 14 is exactly the six removed sends.

Everything above is reasoned from the code in /tmp/aetherclaude/pr-6255 and from the arithmetic in src/gui/WaterfallLevelMap.h. I have no build and no radio, so nothing here was reproduced at runtime, and I cannot confirm any of the author's local test or full-suite figures.

Recommendation

Needs maintainer decision — on the rule, not on the code. The implementation is clean and the hard part (the gate keying on a capability rather than a family, the one-wire-site-per-key invariant, the honest Icom negative) is done right. What needs your ruling is the carve-out itself, which the author explicitly hands over: the #5919 ruling you made on 2026-10-04 is phrased generally — "an operator-moved control the backend cannot apply refuses aloud through the existing once-per-session notice" — and although its examples are all receive controls, nothing in it is scoped to them. This PR adds a refinement rather than a contradiction (here the control is applied, by the client, so there is nothing unapplied to refuse), and the refinement is narrow and testable: withhold only where the client demonstrably drew the value. If you accept it, the four nits above are all comment-and-body wording plus one test row, and this can merge after those. If you would rather the notice stay on all four until pan commands get a typed intent, the author has offered to close it.

Thank you for the "Limits first" section and for pinning the Icom negative rather than only the quiet cases — the test asserting where the notice must still fire is what made the rest of it checkable.


🤖 aethersdr-agent · cost: $10.4281 · model: claude-opus-5

on8st and others added 2 commits October 7, 2026 21:24
Review of aethersdr#6255: setWaterfallBlackLevel() kept its own hasCommandPlane()
early return while setWaterfallColorGain() relied on the gate in its
...For() variant, so the black-level gate lived in two places.

Each key now has one private sender, sendWaterfallColorGain() and
sendWaterfallBlackLevel(), which holds the wire text and the only
command-plane gate. The four public setters say whether a drop is
reported: never for the connect-time replay, never for colour gain, and
for the black level where the rows are not absolute dB.

No behaviour change: pan_display_drop_notice_test and icom_family_test
pin every path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iple VIII.

Review of aethersdr#6255: without a command plane, setWaterfallColorGainFor()
withheld the colour-gain text on every backend, on the claim that the
client draws the gain on every row. With manual black on rows that are
not absolute dB (Icom), WaterfallLevelMap::manualBlackThreshold() puts
the black point at 160 - blackLevel, 60 to 160, against rows that are
negative dB, so level() is 0 for every colour gain: the control did
nothing and its drop notice was withheld anyway.

setWaterfallColorGainFor() now takes the pan's auto-black state from the
widget and withholds the text only where the client draws the gain:
auto black on, or absolute-dB rows. The three sites pass the widget's
wfAutoBlack(), read after it is set (the clone target's, and true after
Reset). The connect-time replay stays quiet, as before.

Test: Icom manual black now reports the gain (WtrFall Gain and Clone),
Icom auto black does not, the Hermes-Lite 2 does not either way; each
leg is checked first on the renderer's law. 41 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st

on8st commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Bad news first: your first nit is right, and it was a wrong rule, not only a wrong word. On Icom with Black Level in manual, WaterfallLevelMap::manualBlackThreshold puts the black point at 160 − blackLevel (60 to 160) while toDbm rows are negative dB (−140 to −60 at the default calibration). So level() is 0 for every colour gain. WtrFall Gain drew nothing there, and on 1935624d its notice was withheld anyway. That is the case this PR says must keep its notice, so I changed the gate rather than the comment.

Nit 1, colour gain with manual black on rows that are not absolute dB. Fixed in 182bb99c. setWaterfallColorGainFor(panId, gain, autoBlack) now takes the pan's auto-black state from its widget. Without a command plane it withholds the text only where level() draws the gain: auto black on (the black point is estimated from the row, in its own unit), or panBinsAbsolute(). Everywhere else it goes to sendCmd() and is reported. The three sites pass wfAutoBlack(), read after it is set: the clone target's own value, and true after Reset. The connect-time replay stays quiet, as before. New test legs, each checked first against WaterfallLevelMap::level: Icom manual black reports the gain (WtrFall Gain, and both keys from Clone); Icom auto black does not; Hermes-Lite 2 does not with either. The capabilities-map paragraph says the same.

Nit 3, the asymmetric guard. Done in 3984c5a2, with no behaviour change. Each key now has one private sender, sendWaterfallColorGain() / sendWaterfallBlackLevel(). It holds the wire text and the only command-plane gate. The four public setters only say whether a drop is reported: never for the connect-time replay, and for an operator's change where the client did not draw it. The existing legs and icom_family_test's replay check pin it (mutation 6 below).

Nit 2, undeclared panAmplitude read as relative rows. No behaviour change. One sentence at the gate says that a mistake there is a false notice, not a missing one. The body says it too.

Nit 4, the test is not in CI's gate. Agreed, nothing to change. The 41 checks have run only here.

Tests. Full ctest on macOS (Qt 6.12.0, Ninja, RelWithDebInfo) at 182bb99c: 654 of 658 pass at -j8 and after the serial rerun. pan_display_drop_notice_test (41 checks) and icom_family_test pass. The four failures fail on clean main on this machine too, 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, red serially as well this time), peripheral_auth_dialog_test (#6008, its clipped-at-default-size line) and unified_title_bar_test (its two menu checks, #6224). Our harness prints RESULT: RED pr6255-review 182bb99c because #6248 is on no waiver list and the title-bar waiver does not apply to a branch that edits MainWindow_Wiring.cpp. I checked each failing line by hand. tools/check_command_plane.py is unchanged: 363 of 363, RadioModel.cpp 135, MainWindow_Wiring.cpp 14. Every script step of Static checks is clean with stdin from /dev/null. The colour ratchet is +0, and check_a11y.py on MainWindow_Wiring.cpp finds nothing.

Red on mutation (scratch commits on 182bb99c, never pushed, full suite each):

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 next checks in that leg. That carries one check in the first and third rows, and three of the four in the auto-black-only row.

Not verified: nothing in the running app, and nothing on a Hermes-Lite 2, an Icom or a Flex. That the Icom gain is dead under manual black is the renderer's arithmetic on toDbm rows; I have no Icom. The three sites pass wfAutoBlack(), and that is pinned by source text only. ANAN and RTL-SDR were not run. Linux and Windows: CI will say.

73, ON8ST

@Ozy311 Ozy311 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of the two commits since the aethersdr-agent review at 1935624d (head 182bb99c).

The WtrFall Gain nit is fixed, not just reworded. The review found that on a backend whose rows are not absolute dB (Icom), with Black Level in manual, every row draws black whatever the gain, yet the gain-change notice was withheld. setWaterfallColorGainFor() now takes that pan's autoBlack and treats the client as drawing gain only when autoBlack || panBinsAbsolute(). Otherwise it reports the drop (RadioModel.cpp:6714-6722). All three callers pass the widget's state:

  • The overlay slider.
  • Reset, which passes the default 50.
  • Clone-to-all-pans. There dst->setWfAutoBlack(src->wfAutoBlack()) runs before the gain call, so the target's new state is the one read.

One gate per waterfall key. sendWaterfallColorGain() and sendWaterfallBlackLevel() are now the only places that send these two keys, each taking a reportDrop flag. That flattens the asymmetric early return the review pointed out. I checked the behaviour against the previous head:

  • Black Level: unchanged. The replay sends nothing without a command plane, and an operator move reports a drop unless the rows are absolute.
  • Colour gain: the replay is unchanged. An operator move changes only in the manual-black case on non-absolute rows.

On a Flex, hasCommandPlane() is true, so both keys still reach sendCmd() exactly as before.

The comment on setWaterfallBlackLevelFor() now also says which way an undeclared panAmplitude fails: as a false notice, never a missing one. That covers the review's second nit. The branch merges cleanly with current main. Not run: pan_display_drop_notice_test, which needs the core build.

What is left is the maintainer's call on the carve-out itself: withhold the notice where the client demonstrably drew the value. The code now implements that rule more exactly than before. Nothing new from me.

73, K6OZY


Generated by Claude Code

@rfoust rfoust self-assigned this Oct 12, 2026

@rfoust rfoust left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR implements its stated fix: client-effective waterfall display changes no longer spend the unsupported-control notice, while ineffective Icom manual-black changes retain it. No closing issue is linked. The operator, a maintainer, accepted this notice behavior in this review and explicitly requested approval and squash merge.

Scope

File/group Claimed Verdict
RadioModel.cpp/.h Yes Named-pan setters and shared command-plane gates are in scope
MainWindow_Wiring.cpp Yes Four display call-site conversions are in scope
pan_display_drop_notice_test.cpp and tests.cmake Yes Socket-free regression coverage and registration are in scope
Capability-map documentation and command-plane checker Yes Consumer documentation and six-send baseline reduction are in scope

No unrelated changes, new wire verbs, capability fields, settings keys, defaults, or visual changes. No code correctness blockers found.

Verification and attacks

Reviewed head 182bb99. A fresh local merge with main c2e543c produced tree 025547a5c8634a0f5bf286949dbfa73cdf240133. Native macOS ARM64 application and focused test built with RADE enabled; processors/executable were ARM64 and RNNoise x86 sources absent. All 41 focused test checks passed. Removing the absolute-row condition caused four failures; restoring it returned the test to green. Boundary/capability/manifest checks matched main except for the intended six-send reduction; registration and frozen CI-gate checks passed.

Attacked target-pan routing, command order, active-pan replay, invalid IDs, and Icom negative cases. Owned offscreen DEMO-0001 instance, TX disabled: actual gain and manual-black controls accepted both range limits, Reset returned wfColorGain=50 / wfBlackLevel=15 / wfAutoBlack=true, and reconnect preserved those defaults. The owned instance was stopped. No physical radio or TX was used.

All five hosted checks are green on this head. They are from October 7; the fresh local merge supplies later-main integration evidence. The new test is outside the frozen per-PR gate by policy. No unresolved review threads. Automated review invocation was unavailable; the independent second pass was manual.

Remaining limitations / non-blocking notes

ANAN/RTL coverage is declaration-only, and multi-pan Clone remains model-test/source-backed. Demo owns a command plane, so the no-command-plane notice defect is proven by the injected model test, not by Demo GUI behavior. Offscreen checks do not establish rendered pixel correctness. A matching Qt 6.12 QtKeychain was used for runtime testing after detecting a local Homebrew Qt mismatch.

The user guide has not been updated, despite AGENTS.md's same-PR documentation instruction. This gap was disclosed to the operator before their explicit approval-and-merge instruction; it remains a documentation follow-up rather than an assertion that documentation is complete.

Approved at the operator's explicit direction, with the notice-behavior decision settled and the above limitations recorded.

@rfoust
rfoust merged commit f6cd464 into aethersdr:main Oct 12, 2026
5 checks passed
rfoust pushed a commit that referenced this pull request Oct 12, 2026
…6402)

Refs #6185.

## Limits, first

- **Behaviour-preserving, "wide".** Every backend that stores the
waterfall rate on `main` today (HL2, ANAN, Icom, RTL, the demo radio)
declares the client its owner. Flex declares none. The same radios store
and restore the same value as before; only the shape of the gate
changes.
- **The wide/narrow ruling is not made here.** The triage on #6185
recommends wide and leaves it to the maintainer. If a family should stop
storing the rate, that is one edit in that backend's `capabilities()`
(`true` to `false`, or `std::nullopt`), and its row in
`client_display_pan_settings_test` flips with it.
- **The identity predicate is not touched** (ruling (b) in the triage).
The waterfall rate still gates on `scope.hasRadioIdentity()`, so a
serial-less RTL dongle still writes the family-wide `rtl` row, where FFT
averaging refuses. That is why this is `Refs` and not `Fixes`.
- **The `shapedLocally` parameter of
`ClientDisplaySettings::waterfallRate` / `saveWaterfallRate` keeps its
name.** The triage suggests renaming it to match the averaging pair.
#6139 inserts code one line from it, so here only the call sites change
what they pass. The rename is a follow-up once #6139 has landed.
- **#6139:** a trial merge of this branch with its head (`cf0006c5`) has
no conflict (`git merge-tree --write-tree`). That merged tree was not
built. #6255, which touched the same files, is in `main` and under this
branch.
- **#5996** (IC-9700 waterfall rate resets after a restart) is not
changed by this PR in either direction: the Icom backend declares the
client the owner, so the store and restore that `main` already has for
Icom keep running.
- No radio was used. Nothing here was run against hardware.

## What was wrong

Read on `main` at `16cfa80b`, before this change; the branch now sits on
`aa0dd0e4`:

- Save: `MainWindow::scheduleClientWaterfallRateSave`,
`src/gui/MainWindow_Wiring.cpp:3145-3157`. The only owner gate is
`m_radioModel.shapesDisplayRatesLocally()` (`:3148`).
- Restore: `src/gui/MainWindow_Wiring.cpp:3333-3336`, under the `if
(m_radioModel.shapesDisplayRatesLocally())` at `:3321`, with a literal
`true` passed to the store.
- The gate, `src/models/RadioModel.h:1366-1368`, is `m_backend !=
nullptr && m_flexBackend == nullptr`: "a backend exists and it is not a
Flex".

So ownership of the waterfall rate was inferred from the family, where
`docs/agents/settings.md` asks for a declared capability. FFT averaging,
FFT FPS and the dBm range each already have a declared owner.

## The rule now

- `PanWaterfallRateShaping { bool clientPersistsWaterfallRate; }` in
`RadioCapabilities.h`, an optional record `panWaterfallRateShaping`, and
the accessor `RadioCapabilities::clientPersistsPanWaterfallRate()`.
Absent means not declared, and an undeclared backend owns nothing.
- `MainWindow::clientPersistsWaterfallRate()` is
`shapesDisplayRatesLocally() && clientPersistsPanWaterfallRate()`, the
same shape as `clientPersistsFftFps()`. The outer term stays, as the
triage asked.
- The save and the restore both read that one predicate.

A separate record, not a second field in `PanFrameRateShaping`: four
backends leave that record `std::nullopt` for FFT FPS, and engaging it
to carry the waterfall rate would turn their "not declared" into
"declared false".

No new top-level bool in `RadioCapabilities`:
`tools/check_capability_records.py --strict` stays at 70.

## Per family

| family | `panWaterfallRateShaping` | stores and restores the rate,
before | after |
|---|---|---|---|
| Flex | absent | no (the radio stores and reports it) | no |
| HL2 | engaged, `true` | yes | yes |
| ANAN | engaged, `true` | yes | yes |
| Icom | engaged, `true` | yes | yes |
| RTL | engaged, `true` | yes | yes |
| Sim (demo) | engaged, `true` | yes | yes |

## Tests

`client_display_pan_settings_test` is extended; no new test target.

- Each real backend (`Hl2Backend`, `IcomCivBackend`, `AnanBackend`,
`SimBackend`, `RtlSdrBackend` where the build has it, `FlexBackend`) is
constructed, and its declaration is read through
`clientPersistsPanWaterfallRate()` and run against the real settings
store: an owner stores 63 and reads it back, and reads and rewrites a
rate already in the document; a non-owner does neither and leaves a held
document byte for byte.
- A record engaged with `false` stores and restores nothing. An absent
record does not either. A record engaged with `true` on otherwise empty
capabilities does (control).
- Owning the waterfall rate leaves the FFT FPS record absent (Icom).
- Source text, because `MainWindow` links into no test: the predicate
reads the outer term and the accessor; the save asks the predicate and
hands its answer to the store; the restore asks the predicate; there is
exactly one stored read and one stored write of the rate in the wiring.
The RTL declaration is also pinned as text, for builds without
librtlsdr.

**Mutation evidence.** One scratch commit with these mutations was built
and run through the full suite. `client_display_pan_settings_test`
reported 29 failed checks, and every check added here is among them:

| mutation | checks that turned red |
|---|---|
| restore reads `shapesDisplayRatesLocally()` alone | "and so does the
waterfall rate restore" |
| save reads `shapesDisplayRatesLocally()` alone | "the waterfall rate
save asks that predicate" |
| save passes a literal `true` to the store | "and hands the store that
answer" |
| predicate drops the capability term | "the waterfall rate predicate
reads the backend's declaration" |
| a second direct `ClientDisplaySettings::waterfallRate(` read in the
wiring | "the waterfall rate has one stored read and one stored write" |
| accessor ignores the bool (`has_value()` only) | all six
`rate-declined:` rows |
| Icom leaves the record absent | five `icom:` rows, and "the waterfall
rate owner does not engage the FFT FPS record" |
| Icom engages `PanFrameRateShaping{false}` | the same "does not engage
the FFT FPS record" check; "icom: FFT FPS owner as declared" stayed
green, which is the distinction the separate record protects |
| RTL leaves the record absent | five `rtl:` rows, and "the RTL backend
declares the client the waterfall rate owner" |
| Flex engages the record with `true` | six `flex:` rows |

Two limits of that evidence. The "does not engage the FFT FPS record"
check is one conjunction and two of the mutations each break one half of
it, so the run does not separate them. And one mutation was not run: an
accessor that treats an absent record as an owner, which is what the
`undeclared:` rows guard.

## Results

macOS, Qt 6.12.0, full `ctest`:

- At `40398b89`: 771 tests, 770 pass, 1 fails, at `-j8` and again on a
serial rerun. It also fails on clean `main` on this machine:
- `peripheral_auth_dialog_interaction_test`: "Clear code or Status is
clipped at the default dialog size" (#6008, a long USB serial-port name
on this host widens the page). No dialog file is touched.
- `client_display_pan_settings_test`, `client_display_settings_test` and
`pan_dimension_routing_test` pass. The RTL backend is built in this
configuration, so the `rtl:` rows ran.

Static checks, all `--strict`: `check_capability_records` (70 of the
frozen 70), `check_command_plane`, `check_engine_boundary`,
`check_ci_test_gate`, `check_test_registration`,
`check_wdsp_test_links`, `check_qsettings`, `check_network_timeouts`,
`check_build_options`, `check_qt_pin`, `check_shader_dialects`,
`check_theme_seed` pass. `gen_touchpoint_manifest.py --check` reports
the manifest up to date.

Docs updated to the new rule: `docs/HERMES.md` §15.2.2,
`docs/architecture/radio-capabilities-map.md` (a
`panWaterfallRateShaping` row),
`docs/architecture/aetherd-icom-civ-backend-design.md`.

73, ON8ST

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants