Repository navigation
fix(gui): no drop notice for a pan control that acted. Principle VIII. - #6255
Conversation
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>
There was a problem hiding this comment.
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. WithautoBlack = falseandrowsAreAbsoluteDb = false(Icom),WaterfallLevelMap::manualBlackThresholdreturns160 - blackLevel, i.e. 60…160 in tile units, while every dB sample is negative — so(value - blackThresh)is ≤ −80 andlevel()pins to0.0for everycolorGain. 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'sclientDrawsColourGain()only probesparams.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 IcomautoBlack = falserow in the test as a documented boundary. -
src/models/RadioModel.cpp:6736— the gate reads "undeclared" as "relative rows".RadioCapabilities::panBinsAbsolute()ispanAmplitude && panAmplitude->binsAbsolute, and the header comment abovePanAmplitudeModel(src/core/backends/RadioCapabilities.h:135-137) says absence means "no backend has been read".IcomCivBackendnever 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-6727vs6707-6710— asymmetric guard.setWaterfallBlackLevel()keeps its ownif (!hasCommandPlane()) return;whilesetWaterfallColorGain()relies entirely on the…For()variant's gate. Net behaviour is identical tomaintoday 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 -Rin.github/workflows/ci.ymlnames a fixed handful of targets andpan_display_drop_notice_testis not among them (zero matches). All five checks on1935624dare 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 matchescmd.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 theboolreturn. The only loss is oneqCDebug(lcProtocol)line. No behaviour change on Flex. - The active-pan delegation.
setWaterfallColorGain/setWaterfallBlackLevelmoved fromactiveWfId()topanadapter(m_activePanId)->waterfallId(). I expected a fallback to diverge;activePanadapter()is literallym_panadapters.value(m_activePanId, nullptr)andpanadapter(panId)ism_panadapters.value(panId, nullptr), so the connect-time replay addresses the same pan it always did. The test pins this on Flex with an explicitsetActivePanId(f.pan1). - Null and unknown inputs.
backendCapabilities()ism_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=. OutsideRadioModel.cpponly the Sim/RadioConnectiondemo echo strings remain, which are inbound status text, not sends. Inside it,configureWaterfall()(RadioModel.cpp:12341) still sendsblack_level=15 color_gain=50through an ungatedsendCmd()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/requestPanDisplayRatesfell 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 ashapesDisplayRatesLocally()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 onhasCommandPlane()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 throughsendCommand()first and checking it is reported. The source-text pin fails onmainby construction. I also re-derivedclientDrawsManualBlackLevel()by hand for both unit cases and it is driven by the real unit mismatch, not by an unsetParamsfield — 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.pyon 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
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>
|
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, Nit 1, colour gain with manual black on rows that are not absolute dB. Fixed in Nit 3, the asymmetric guard. Done in Nit 2, undeclared 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 Red on mutation (scratch commits on
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 73, ON8ST |
Ozy311
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
…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>
Refs #5678 (HL2 parity rows), #5263 (the drop notice). No
Fixesline: I found no issue for this.Limits first
main; the fix is tested onRadioModelwith an injected backend, and the fourMainWindowcall sites are pinned as text only.display panafall settext is not replaced by a typed intent. It moves out ofMainWindow_Wiring.cppinto the two wire sitesRadioModelalready has.tools/check_command_plane.py:MainWindow_Wiring.cpp20 to 14,RadioModel.cppunchanged at 135.RadioCapabilities::panBinsAbsolute(), the same proxy for the row unit the renderer reads (radio-capabilities-map.mdcalls it a proxy). If the row unit gets its own signal later, this gate should follow it. An undeclaredpanAmplitudereads as not absolute, so a backend that computes dB rows and forgets the declaration gets a false notice, not a missing one.main. I did not make that case quieter.binsAbsolute), and the test does not run with their capabilities: only Hermes-Lite 2, Icom and Flex.sendDbmRangeCommand, beside the dBm-range work of my fix(gui): FFT FPS and the dBm range survive a restart. Principle VIII. #6140, now merged) andcw break_in(left open by fix(radio): route VOX, monitor, speech processor, CW speed and break-in through the seam gate. Principle VIII. #6111).121340c0, with no conflict; head182bb99cadds two review commits on1935624d. 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 thetests/tests.cmaketest list (it also conflicts withmainitself inRadioModel.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:src/gui/MainWindow_Wiring.cppwirePanadapter,SpectrumOverlayMenu::wfColorGainChangedhandlerdisplay panafall set <wf> color_gain=NwirePanadapter,SpectrumOverlayMenu::wfBlackLevelChangedhandlerdisplay panafall set <wf> black_level=NMainWindow::cloneDisplaySettingsToAllPanswirePanadapter,SpectrumOverlayMenu::displaySettingsResethandlercolor_gain=50,black_level=15Each went through
RadioModel::sendCommand()tosendCmd(), which has no command plane to write to, logs the drop and emitscommandDropped.MainWindowshows 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()andsetWaterfallBlackLevel()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::manualBlackThresholdon absolute-dB rows).The change
RadioModelgainssetWaterfallColorGainFor(panId, gain, autoBlack)andsetWaterfallBlackLevelFor(panId, level). All four setters go through two private senders,sendWaterfallColorGain()andsendWaterfallBlackLevel(), which hold the only wire site and the only command-plane gate for each key.wfAutoBlack()with the gain. They no longer build wire text.panBinsAbsolute(). With manual black on any other row (Icom), every row is black whatever the gain, so it goes tosendCmd()and is reported.panBinsAbsolute()is true (Hermes-Lite 2, ANAN, RTL-SDR). Elsewhere it goes tosendCmd()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.setWaterfallBlackLevel()frompanadapterInfoChanged) stays quiet on every backend with no command plane, as onmain. No operator moved anything.docs/architecture/radio-capabilities-map.mdrecords 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 usesendCmd(); the only thingsendCommand()adds is the pan-ownership gate, which matchesdisplay pan setand notdisplay 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 carriescapabilities()read off a realHl2Backend,IcomCivBackendandFlexBackend; panes are made the way a backend without Flex status makes them.commandDropped; the same gain text sent raw is still reported, so the alarm is live in the fixture.commandDropped, with the exact text; every Black Level position draws a dB row black and the slider still raisescommandDropped; 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.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.MainWindow_Wiring.cppbuilds no gain or black wire text of its own. No behavioural seam reaches it: the sites areMainWindowlambdas and no test target constructs aMainWindow. It shows how the sites are written, not that they run.Mutations, each as a scratch commit on
182bb99cthrough the full suite:mainicom_family_testhasCommandPlane()alone1935624drule)icom_family_testThe 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 cleanmainon 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) andunified_title_bar_test(its two menu checks, #6224). This PR changesMainWindow_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 checksjob was run locally at182bb99cwith stdin from/dev/nulland is clean: colour ratchet +0,gen_touchpoint_manifest.py --checkup to date with no change,check_a11y.pyon the one changed GUI file with no findings. TheSanitizer option configuresjob needs the CI image and was not run.Not verified
IcomCivBackendsending its scope rows throughtoDbm; I have no Icom.wfAutoBlack()is pinned by source text only.binsAbsolutedeclaration.73, ON8ST
🤖 Generated with Claude Code