Repository navigation
fix(tx): Show TX in Waterfall works with no radio echo. Principle VIII. - #6139
Conversation
On a Hermes-Lite 2 the Radio Setup toggle could not be switched on: `transmit set show_tx_in_waterfall=1` was dropped for want of a command plane, nothing echoed, and TransmitModel::showTxInWaterfall() stayed false. With it false SpectrumWidget drops every waterfall row of the pane holding the TX passband for the whole over. On a Flex the flag is the radio's: stored there, echoed in transmit status, and the echo is the only writer of the model. A radio whose waterfall rows are made on this host has no display engine to hold it, and it does have rows to draw: Hl2RxDsp keeps the spectrum running on real IQ while the demodulator is muted for transmit. RadioModel::requestLocalShowTxInWaterfall() takes the flag wherever shapesDisplayRatesLocally() holds. It writes the transmit model through applyChanges(), the path an echo takes, and stores the value per radio in the ClientDisplay document, because a disconnect clears the model and nothing reports it again; onConnected() puts it back. On a Flex it declines and the caller sends the wire text as before, so no raw command is added above the seam. The Radio Setup button and the bridge verb `txwaterfall` both ask it first. Not keyed, so not observed: what the keyed rows look like on an HL2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: tests/tests.cmake, both sides' tests kept (replayed from a recorded resolution and checked). The branch's long comments are cut to the five-line rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No conflicts. Main at 52b6f7a (aethersdr#6117). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Show TX in Waterfall button was checked as source text. Radio Setup links into tests, so radio_setup_show_tx_waterfall_test clicks the button on the production dialog instead: on an injected backend with no command plane the model takes the flag and no wire text is sent; on a Flex the wire text is sent and the model waits for the echo. The source-text block and its AETHER_SOURCE_DIR define are retired. Comments the branch adds are cut to 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 write-up is unusually easy to review against, and the limits section up front saved me a lot of guessing. The red check is a one-line CMake omission, not a problem with the change itself. Up front: I could not read the raw build log. My What the jobs say
Windows passing is the useful signal: it builds every production file you touched and never builds either new test target. So I also ran The cause
target_link_libraries(show_tx_in_waterfall_client_flag_test PRIVATE
aethercore Qt6::Core Qt6::Network Qt6::Widgets)
The tell is in your own cmake comment: "Qt6::Widgets for AutomationServer." The dependency was spotted — it's just that The fix
Add one line to set(AETHER_AUTOMATION_SERVER_TESTS
...
reroute_dead_controls_test
+ show_tx_in_waterfall_client_flag_test
)That's how
To confirm in ten seconds: cmake --build build --target show_tx_in_waterfall_client_flag_testOne thing I can't reconcile: the PR body reports a full ctest at this head and a mutation table with "one build and one run per row." If that target linked on your Mac I'd be interested in the link line, because by the CMake above it shouldn't. The likeliest story is that the bridge-verb section of the test landed after the last full configure+build, and a stale build dir carried it. Nothing else in the diff looks CI-relevant: Static checks and the sanitizer configures are both green, and the command-plane ratchet held at 400. No Copilot or reviewer comments on the PR yet, so nothing from that side feeding into this. 🤖 aethersdr-agent · cost: $11.1168 · model: claude-opus-5 |
main gained aethersdr#6138, aethersdr#6115 and aethersdr#6027 since this branch was cut. No textual conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aethersdr#6027 moved RadioSetupDialog::buildPeripheralsTab() into RadioSetupDialog_Peripherals.cpp and added that file to every test that compiles the dialog. radio_setup_show_tx_waterfall_test is new on this branch, so the merge with main left it without the file and it failed to link: undefined RadioSetupDialog::buildPeripheralsTab(). 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>
…III. show_tx_in_waterfall_client_flag_test builds an AutomationServer. It linked already, because the deferred retrofit adds aetherdesktop_support to any test that links aethercore and Qt6::Widgets, but tests that instantiate the bridge are listed in AETHER_AUTOMATION_SERVER_TESTS so the link does not rest on that rule. Listed there now. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Issue fit
Yes. No issue is linked — the body says so plainly ("Refs #5678 … No issue exists for this"), and GOVERNANCE.md explicitly exempts a bug fix with a clear root cause from needing one first. The root cause is stated and checkable: on a radio with no command plane the wire text is dropped (RadioModel.cpp:9735) and the only writer of TransmitModel::showTxInWaterfall() is the Flex transmit-status echo (FlexBackend.cpp:1098), so the flag could never become true and the gate at SpectrumWidget.cpp:8416 (txAffectsPan && m_showTxInWaterfall) could never open. All three surfaces that set the flag — the Radio Setup button, the txwaterfall verb, and now the connect edge — are fixed together; the two that only read it (AutomationServer.cpp:1903 get transmit, RadioModel.cpp:12845 troubleshooting dump) read the model and need no change. No sibling surface is left behind.
Test layer is right: this is model/state-machine and refusal behaviour, so socket-free CTests are the correct layer and that is what was added. No fake radio peer anywhere.
Scope
| File(s) | What it changes | Claimed? | Verdict |
|---|---|---|---|
src/models/RadioModel.{h,cpp} |
requestLocalShowTxInWaterfall, restoreClientShowTxInWaterfall, the onConnected() call |
yes | in scope |
src/core/ClientDisplaySettings.h |
read/write of one optional bool in the existing v1 ClientDisplay document |
yes | in scope |
src/gui/RadioSetupDialog.cpp |
the button asks the model first | yes | in scope |
src/core/AutomationServer.cpp |
the verb asks the model first; comment rewritten | yes | in scope |
tests/*_test.cpp, tests/tests.cmake |
two new registered targets | yes | in scope |
docs/HERMES.md §15.3.1, docs/automation-bridge.md |
documents the new client-flag behaviour | yes | in scope |
Everything in the diff is explained by the stated fix. Specifically checked and clean:
- No
CHANGELOG.mdentry — correct; it must not be added in an ordinary PR. - No new public surface.
txwaterfallalready existed; only the responsenotediffers on the new branch, and the response keys (ok,txwaterfall,note) are unchanged in shape. - New persisted key, no new document.
showTxInWaterfallgoes inside the existing versionedClientDisplayfeature document underRadioModel::settingsScope(), one owner (ClientDisplaySettings), no schema bump — Constitution V satisfied, and the body discloses it. No flat key. - No deleted guard. The one removed comment (
AutomationServer.cpp) named a use for the flag, not a symptom; the#3646/#3804refs survive. - Default unchanged (off), and no UI restyle or reorder rides along — this is a fix for a dead control, not a preference. The dead toggle was observed (6 of 6 bridge polls false, 2026-10-01).
- No dead additions. Both targets are
add_test-registered and joinedAETHER_SETTINGS_CONSUMERS; the dialog block is shape-identical to its siblingradio_setup_recording_mode_dim_test. - No socket test added or removed. Neither new test touches
QTcpSocket/QUdpSocket/QLocalServer/bind/listen/connectToHost;AutomationServer's constructor does not listen (AutomationServer.cpp:2269-2272) and the test driveshandleLine()directly. Nothing to surface here.
Security audit (CodeGuard): no findings. I re-checked the two leads that mattered — the Flex fallback formats a bool (.arg(on ? 1 : 0)), so no operator text reaches sendCommand; and the verb is absent from kSafe, so the observe-only gate refuses it by default-deny, which is the right direction for a verb that writes persistent settings.
Blockers
None.
Nits
All non-blocking.
-
The enumeration omits the demo simulator.
docs/automation-bridge.md:3005and the PR body both list "Hermes-Lite 2, ANAN, Icom, RTL-SDR", but the gate isshapesDisplayRatesLocally()=m_backend != nullptr && m_flexBackend == nullptr, andSimBackend : public IRadioBackendwithm_flexBackend = nullptr; // no Flex alias in demo mode(RadioModel.cpp:858). Sosimtakes the local route too. That is almost certainly the behaviour you want — I found noshow_tx_in_waterfallhandling anywhere insrc/core/backends/sim/or the synthetic connect path, so the toggle was equally dead there and this fixes it — but the demo radio is the surface reviewers andradiocertdrive most, and it is the one family in scope with a live command plane. Worth adding to the sentence. Inline on the doc line. -
The restored flag's path to the renderer is the one link nothing covered. Two paths seed
SpectrumWidget:onSliceAdded(MainWindow_Wiring.cpp:1932-1935) andsyncShowTxWfonTransmitModel::stateChanged(MainWindow_Session.cpp:1428-1437). On a fresh connect the restore firesstateChangedwhilem_radioModel.panadapters()is still empty, soonSliceAddedis the actual carrier — which assumes slice-add follows pan-add, andpanadapterAdded(MainWindow_Session.cpp:1770) does not seed the flag. This is pre-existing wiring (a Flex echo lands at the same point in the handshake), and the widget's ownm_showTxInWaterfallsurvives a disconnect, so the realistic reconnect case is covered by inertia. I raise it only because youronConnected()comment asserts "before the GUI wires its pans" is the right moment, and that ordering is exactly what neither test nor a running app exercised. Inline on the call. -
The
shapesDisplayRatesLocally()gate has precedent — I'd call the question you raised settled. You offer "a declared capability instead is yours to ask for". For what it's worth from the code: Constitution III's test is whether the radio can save and restore the value, and no local-shaping family has ashow_tx_in_waterfallregister at all. TheclientSettingsDomainsenum is scoped to operating-state domains ("RF frequency + demod mode", "filter edges", …) and the Icom's "must never be pushed a restored state" comment (RadioModel.cpp:744-746) is about those pushes, not a client display flag — and nothing here is pushed to any radio.ClientDisplaySettings::waterfallRatealready uses the identical predicate on the identical document for the identical kind of state. I don't think a new capability is needed; a maintainer may still prefer one. -
tools/aether_mcp.py:759groupstxwaterfallunder "gated byAETHER_AUTOMATION_ALLOW_TX". It isn't — correctly, since it never keys. Pre-existing and outside this diff.
What I tried to break
Findings and non-findings alike are reasoned from the head checkout at ded1f7d3 (which I confirmed is the PR head — a c29bb2f2 with your commit subject exists in the repo but is off an unrelated base and not in this branch's history). I ran no build and no test; nothing below was observed at runtime.
- Attacked the tests' strength first. The dialog test's negative assertion (
!log.contains("transmit set show_tx_in_waterfall=")) looked like it could pass vacuously — it doesn't:RadioModel::sendCommandlogs the full command text atlcProtocoldebug before every drop path (RadioModel.cpp:9379), so a wire send is visible whether or not a session exists. The Flex positive control is genuine too, not an artifact of a backend-less model: the constructor callssetupBackend("flex")(RadioModel.cpp:2272), so "a bare model is on the Flex backend" is accurate, and the other test additionally exercises a realFlexBackendviarebuildBackendForTest. - Looked for the fix being a no-op at the renderer — the PR's load-bearing claim that
stateChanged"reaches the same consumers". It holds:MainWindow_Session.cpp:1436-1437connectsTransmitModel::stateChangedto a lambda that pushes to every pan'sSpectrumWidget, so an operator toggle on a connected radio with pans present does reach the gate. The gap I did find is only on the connect edge (nit 2). - Checked for provenance laundering — a client write entering through
applyChanges(), the echo path, could makeget transmit showTxInWaterfalland the troubleshooting dump report a client value as radio-confirmed. It doesn't: them_haveTransmitStatus/m_rfPowerFromRadiolatches fire only ond.rfPower/d.maxPowerLevel(TransmitModel.cpp:112-120), so ashowTxInWaterfall-only delta flips no provenance flag and emits exactly onestateChanged, as the test asserts. - Walked the connect/disconnect/radio-swap lifecycle.
resetState()is called from exactly one place,onDisconnected()(RadioModel.cpp:7957), so nothing afteronConnected()wipes the restore — and the!hasCommandPlane()early return sits after it. Identity is available at that moment:serial()returnsm_lastInfo.serial(RadioModel.cpp:11573) andonConnected()already readsm_lastInfo.seriala few lines earlier. Second-radio isolation is real (hasRadioIdentity()keys the scope) and the test covers serial A → B. - Attacked the store's failure paths.
saveShowTxInWaterfallrefuses onversion > 1,CorruptandUnavailableand checks thesetFeatureresult, matchingsaveWaterfallRateexactly; the reader rejects a non-bool rather than coercing; a model with no radio identity writes nothing (so no family-wide row is poisoned). The two writers do non-atomic read-modify-write on the same document, which I chased — all three call sites are main-thread, so there is no interleaving, and the test pins that neither key disturbs the other. - Could not check: anything keyed, which is the limit you state yourself — so what the HL2 draws during an over, and whether
dbmToWaterfallLevelsuits dBFS-under-a-dBm-label rows, remain open by your own admission. Also unchecked: Linux/Windows, and whether the dialog test's globalqInstallMessageHandlersink is safe under the sanitizer lane with aQApplicationrunning (the same pattern has produced a reported race elsewhere in the suite). - CI on this head is 5/5 green, but that gate is
-R-filtered and does not run either new target; the real signal isfull-suiteat merge.
Recommendation
Approve with nits. The root cause is correct and specific, the seam matches the existing requestLocalPanWeightedAverage precedent, no raw command is added above the seam, persistence lands in the right document with the right owner, and the tests survived the two ways I tried to make them vacuous. The limits section is unusually honest and I found nothing it misstates. Next step: add sim to the family list in docs/automation-bridge.md (and the body), and — if a maintainer wants nit 2 closed rather than noted — seed the flag from panadapterAdded as well, so a restored true does not depend on slice-add ordering.
Thanks for the fail-before mutation table; it made checking which assertions actually bite much faster.
🤖 aethersdr-agent · cost: $16.0720 · model: claude-opus-5
|
The CI log's first error, both jobs: 73, ON8ST |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of aethersdr#6139 (aethersdr-agent, 2026-10-04). The connect-edge comment said the restore lands before the GUI wires its pans. Nothing showed the GUI half. The comment now claims the model half only, and the test pins it through the production HL2 wiring and the backend's own connected(): the flag is in the model at connectionStateChanged(true), at the first panadapterAdded and at the first sliceAdded, in the order Hl2Backend publishes them. MainWindow seeding a SpectrumWidget from it is not tested. docs/automation-bridge.md: the demo simulator takes the local route too (SimBackend has no Flex alias), and the demo, ANAN and RTL-SDR cannot key, so the flag is stored there and draws nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Not closed: the GUI half of nit 2. Nothing shows Head
622 of 623 pass; 73, ON8ST |
One conflict, tests/tests.cmake: both sides added test targets at the same place. Both kept: main's three RTL targets, then this branch's show_tx_in_waterfall_client_flag_test. radio_setup_show_tx_waterfall_test gains RtlReceiverSettingsWidget.cpp and ControlAvailabilityRegistry.cpp: RadioSetupDialog.cpp constructs the RTL Receiver page since the RTL M1 merge, and main's own dialog tests list both. Touchpoint manifest regenerated: unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged One conflict, 73, ON8ST |
ten9876
left a comment
There was a problem hiding this comment.
Issue fit: The dead-toggle mechanism is addressed; the persistence-ownership contract still needs correction. At 98be49a116fd25f04aed4a484faab92b5c650da5, both Radio Setup and txwaterfall reach the transmit model locally, and reconnect restores the value before pan/slice announcements. The #5678 parity reference supplies context; there is no dedicated closing issue. No transmit energy or keyed waterfall appearance is claimed as verified here.
Scope
| File/group | Claimed? | Verdict |
|---|---|---|
RadioModel.{h,cpp} |
Yes: local request and connection-edge restore | Addresses the stated mechanism; ownership issue below |
ClientDisplaySettings.h |
Yes: per-radio showTxInWaterfall boolean in schema 1 |
Appropriate disclosed feature document; eligibility must be declared |
RadioSetupDialog.cpp, AutomationServer.cpp |
Yes: local route before existing Flex fallback | Both writers covered; bridge response semantics are documented |
Two new tests and tests/tests.cmake |
Yes: model/bridge injection and real dialog | Relevant registered, socket-free coverage |
HERMES.md, automation-bridge.md |
Yes: behavior, persistence, receive-only families and limits | In scope |
Authored commits concern implementation, testing, integration and documentation of this change. No unrelated restyle, new default, transmit operation or changelog entry was found. The new persistent field is disclosed; the existing bridge verb gains immediate local-success semantics, not a new verb.
Blocker
-
Declare persistence ownership rather than inferring it from the concrete backend.
src/models/RadioModel.cpp:6157-6190usesshapesDisplayRatesLocally()to authorize both writing and restoring the new setting. Its implementation insrc/models/RadioModel.h:1346ism_backend != nullptr && m_flexBackend == nullptr: every non-Flex backend is implicitly opted in. That establishes implementation identity, not whether this setting belongs to the client. The exact rule indocs/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.” The existing waterfall-rate exception does not authorize extending it to another persistent field; the body acknowledges the gap but does not cite a maintainer waiver.Keep the disclosed
ClientDisplayowner/document, but gate the request/store/restore using explicitly declared ownership for this field, following the per-feature capability-record contract rather than adding a new frozen boolean. Pin declared opt-in and opt-out backends with socket-free tests. This is a source-verified contract defect, not a claim that I observed today's HL2/Icom/ANAN/RTL state being overwritten.
Non-blocking notes
src/gui/MainWindow_Wiring.cpp:1947andsrc/gui/MainWindow_Session.cpp:1429: the existing first-slice initialization and state-change fanout do carry the flag into spectrum widgets. The simulator cannot establish the color law or actual receive IQ while an HL2 is keyed. Retain that hardware limitation in the body; don't infer it from a true model flag.docs/automation-bridge.md:3005: the documented receive-only/demo cases help distinguish successful storage from actual keyed rendering. Jeremy should explicitly accept the field's intended ownership across those backends when selecting the capability declaration.
Verification and attacks
- Built the exact-head app and both new targets on Linux / Qt 6.12.0. The model/bridge test exited 0 and the production-dialog test passed 4/4. Both passed again after restoring the mutation.
- Mutation: made
requestLocalShowTxInWaterfall()immediately decline. The model test exited 1 with 11 failed checks (including request, notification, store, restore and bridge result); the dialog test failed its host-made-rows case (3 passed / 1 failed). This is meaningful regression coverage, not a restatement of the implementation. - In the exact-head app, explicit socket
/tmp/aethersdr-codex-batch-6139, isolated settings andAETHER_AUTOMATION_NO_TX=1, connected onlyDEMO-0001. The bridge returned:{"ok":true,"txwaterfall":true,"note":"client-side flag on this radio, applied now; get transmit showTxInWaterfall reads it"}get transmitthen showedshowTxInWaterfall: trueandtransmitting: false. Opened Radio Setup → Transmit; the actual visible button readEnabledandchecked: true. Coordinate clicks on that confirmed button produced modelfalse, thentrue. - Quit/relaunched the app against the same private profile and reconnected the demo:
showTxInWaterfallrestored to true across the process boundary. Invalidtxwaterfall bananareturnedok: false/txwaterfall requires on|off; switching off remained functional. Both owned processes (2199043, 2200956) exited after disconnect/Quit. - A separate earlier control build (
51bad5ff, not this PR's merge base) retained the pre-change route:txwaterfall onreturned success but the demo model remained false. Treat that as corroborating old-behavior evidence, not an exact merge-base build. No radio hardware or operator instance was driven. Rendering actual keyed rows cannot be tested on the RX-only demo; the model-to-widget assignment was source-inspected.
All four architecture gates passed on the merge base and the head; per-file findings were unchanged apart from line movement. Reviewed schema/version refusal, unknown identity, same-family radio swap, false-value restore, preservation of other document fields, Flex echo ownership, and the existing spectrum consumers. New tests instantiate transports without opening them and inject bridge requests directly; no socket-owning changed test was executed. No model-invocable code-review skill was available; findings were checked manually and with local probes.
Required CI checks succeeded, but earliest head-run creation 2026-10-04 06:54:01 UTC predates current main 85f2a124 (2026-10-04T22:40:53Z). These pull_request checks describe an older merge result and branch protection is strict: false; they do not prove today's integration or the complete suite.
Recommendation: Request changes for the explicit persistence-ownership declaration. The local flag route is otherwise a focused fix; live keyed behavior remains outside this review and does not require a synthetic firmware peer.
— Jeremy KK7GWY + Codex
ten9876
left a comment
There was a problem hiding this comment.
Issue fit: the dead toggle is fixed; persistence ownership is not yet declared. No issue is linked (Refs #5678, row P30). At 98be49a1, on a radio whose waterfall rows are made on this host:
- Radio Setup's Show TX in Waterfall button and the
txwaterfallverb writeTransmitModelthroughRadioModel::requestLocalShowTxInWaterfall()(viaapplyChanges, the echo path); - the value is stored per radio in
ClientDisplayand restored inonConnected()beforeconnectionStateChanged(true); - a Flex still gets the
transmit set show_tx_in_waterfall=wire text.
I reproduced the fix and its persistence on the demo (below).
Scope
| File / group | Change | Claimed? | Verdict |
|---|---|---|---|
src/models/RadioModel.{h,cpp}:6157-6190, 6945-6950 |
local request, connect-edge restore | Yes | In scope; ownership gate, Blocker 1 |
src/core/ClientDisplaySettings.h:84-123 |
showTxInWaterfall bool in schema-1 ClientDisplay |
Yes | In scope; new persisted field |
src/gui/RadioSetupDialog.cpp:2714-2720, src/core/AutomationServer.cpp:3585-3605 |
ask the model first, Flex fallback unchanged | Yes | In scope |
2 new tests, tests/tests.cmake |
model/bridge injection; production dialog | Yes | In scope |
docs/HERMES.md, docs/automation-bridge.md |
behaviour, persistence, families | Yes | In scope |
8 merge: origin/main commits |
conflict resolutions | Yes (comments) | Fine |
No CHANGELOG.md, no new verb (only a different note on the local branch), no default change (off). Socket preflight: neither test opens a socket; the bridge test calls handleLine() without listening. The PR merges cleanly against main (85f2a124) and, textually, against #6140's current head.
Blockers
- Ownership is inferred from "not a Flex", not declared.
- Where:
RadioModel.cpp:6157-6190. Both the request (the write and the store) andrestoreClientShowTxInWaterfall()gate onshapesDisplayRatesLocally(), which ism_backend != nullptr && m_flexBackend == nullptr. - Canon:
docs/agents/settings.md:143says "The deciding test is whether THIS radio can save and restore the value — a declared capability, not a family assumption." - Bridge evidence, demo only:
txwaterfall onwhile connected toDEMO-0001storessim|DEMO-0001|ClientDisplay|1|{"showTxInWaterfall":true}. Afterkill -TERM, a relaunch and a reconnect,get transmitreads"showTxInWaterfall": true. So every non-Flex family, the demo included, persists this field without declaring anything. - What changed since the earlier review: #6140 (same author, same document) resolved this exact finding an hour ago in
7a5ca748. FFT FPS and the dBm range now sit behind declared owners in capability records (PanFrameRateShaping::clientPersistsFrameRate,PanAmplitudeModel::clientPersistsDbmRange), and only the HL2 opts in. - Why that matters here: if both land as they stand, the demo, ANAN and RTL keep a persisted Show-TX flag but not their FFT FPS. Two fields in one document would follow two different ownership rules.
- Fix: declare an owner for this field the same way (a record field, not a new frozen bool), opt in the families that should have it, and gate the request, store and restore on that one accessor. Pin an opt-in and an opt-out backend in
show_tx_in_waterfall_client_flag_test. @ten9876, if you would rather waive this on precedent, one ruling now covers both PRs differently, so please say which.
- Where:
Nits (non-blocking)
txwaterfallwhile disconnected reports success for a no-op.AutomationServer.cpp:3598-3610: with no radio connected,txwaterfall onreturns{"ok": true, "note": "radio echoes status…"}, andget transmitstaysshowTxInWaterfall: false, because the default backend is Flex and the wire text is dropped. This predates the PR, but the verb now has two success notes and this one is the misleading one. Worth anok: falsewhen!isConnected().- The restored flag's last hop to
SpectrumWidgetis untested (onSliceAddedatMainWindow_Wiring.cpp:1947). It is already in Limits; it stays a Limit.
What I tried to break / verified
-
Built and ran this head's tests (Linux, Qt 6.12.0, offscreen):
show_tx_in_waterfall_client_flag_testexits 0, andradio_setup_show_tx_waterfall_testpasses 4/4. -
Mutation, connect-edge restore removed from
onConnected(): the flag test exits 1 with "FAIL: the next connect puts the stored value back", "restored before connectionStateChanged(true) is emitted", "and held when the first pan is announced". The file was restored (sha256matchesgit show HEAD:) and touched. The earlier review's "request declines" mutation (11 failures) covers the other direction. -
Bridge, demo only: socket
/tmp/aethersdr-pr6139, isolated settings andHOME,AETHER_AUTOMATION_NO_TX=1. Connected withconnect local serial DEMO-0001, which reported"serial": "DEMO-0001","transmitting": false.Step get transmitbefore "showTxInWaterfall": falsetxwaterfall on→"note": "client-side flag on this radio, applied now…"truedisconnect false(resetState)relaunch, reconnect true(the persistence in Blocker 1)Both instances were closed.
-
Provenance: checked whether a client write through
applyChanges()gets reported as radio-confirmed. It doesn't: the provenance latches inTransmitModelfire only onrfPower/maxPowerLevel. -
Not verified: anything keyed, which is impossible on the demo (it cannot transmit) and needs an HL2 on a dummy load, as the body says. Also unverified: the colour law for dBFS rows, and Windows/macOS.
Recommendation: Request changes for Blocker 1. It is the same change #6140 has just made, so the template is in-tree. Everything else is ready.
— Jeremy KK7GWY + Claude
The local route and its persistence were authorised by RadioModel::shapesDisplayRatesLocally(), which is backend identity: every non-Flex backend opted in by implication. Review of aethersdr#6139 asked for the request, the store and the restore to be gated on ownership declared for this field, in a per-feature capability record, through one accessor. RadioCapabilities::txWaterfallClientFlag is that record. Its field clientPersistsFlag (no default) names the client the owner of the flag. RadioCapabilities::clientPersistsShowTxInWaterfall() is the one accessor: requestLocalShowTxInWaterfall() reads it to take the flag and to store it, restoreClientShowTxInWaterfall() reads it at the connect edge. Only the Hermes-Lite 2 engages the record, with clientPersistsFlag true. Flex, Icom, ANAN, RTL-SDR and the demo simulator leave it absent and keep main's behaviour: the wire text is sent, the client sets and stores nothing. That is narrower than before this commit, when every non-Flex backend took the local route: the bridge's txwaterfall on the demo answers with the radio-echo note again and the model stays false. The ClientDisplay document and its schema are unchanged. No top-level capability boolean is added (check_capability_records: 70 of 70). Tests: the opt-in checks run on the real Hl2Backend instead of an injected backend with empty capabilities; new opt-out checks run on the real Icom, ANAN, RTL-SDR and demo backends with a document that already holds the flag, on Flex, and on a backend that engages the record with the field false; the dialog test gains an Icom slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Bad news first: this is narrower than what you reviewed. The demo, ANAN, Icom and RTL-SDR are back to Head Blocker (both reviews): ownership is declared.
Tests, socket-free, on the real backends.
Notes.
Not verified: anything keyed; a running app at this head (the demo included); Linux and Windows; the merge result with 73, ON8ST |
|
Maintainer ruling: persisting Show TX in Waterfall must be gated by a capability each backend declares, following the pattern #6140 adopted for FFT FPS and the dBm range, not by — Jeremy KK7GWY + Claude |
|
The red 73, ON8ST |
…ite 2. Principle III. (#6140) **Update, `d60f4660` (2026-10-05):** `main` `12df6943` merged in; one conflict, in `docs/architecture/radio-capabilities-map.md`, both sides kept. No code of this PR changed. **Update, `dabf83e3` (2026-10-05):** one fix on top of the reviewed head: the 3D floor resync after a zoom is no longer adopted or stored as the operator's range. Maintainer ruling of 2026-10-05: this PR owns `dbmRanges[pan]`, its helpers and its accepted range; #6129 rebases onto it. Line references below are at `7a5ca748`; `MainWindow_Wiring.cpp` gains four lines after `:4269` since. **Update after review, `7a5ca748` (2026-10-05).** The change request is applied, and it narrows this PR: - **Only the Hermes-Lite 2 is covered now.** Storing and restoring FFT FPS and the dBm range is gated on an owner the backend declares, not on backend identity. The HL2 declares both. Icom, ANAN, RTL-SDR and the demo backend declare none and behave as on `main`. A demo run that showed FFT FPS restored at `295bff6e` will, by the code, not reproduce at this head; that was not run. - **#6129 (open) adds the same `dbmRanges[pan] = {min, max}` table** with a different accept-set (`min < max`, -250..100), a different gate (`!radioOwnsDbmScale`: ANAN, Icom) and a restore into the widget instead of the pan model. With the HL2-only opt-in no radio is claimed by both PRs any more. The two accept-sets still differ and this PR adopts whichever the maintainer rules. A trial merge of `7a5ca748` with #6129 at `04762258` conflicts in `MainWindow_Wiring.cpp` and the two touchpoint files; `ClientDisplaySettings.h` merges as text and then holds two definitions of `DbmRange`, `dbmRange()` and `saveDbmRange()`. The comparison is in the review reply. - **A stored dBm value is validated before it is narrowed to a float** (NaN, the infinities and ±1e308 are refused through the real store and reader). - The source-text pins match whole call statements. Refs #5678 (HL2 parity; rows P16, the dBm range, and the FPS half of P7 in our status table). No issue exists for this. #5111 (the Flex side) is not touched. ## Limits, first - **Where it is stored, since you ruled on that for #6119:** in the radio's settings document, not a flat key. Two optional tables, `fftFps[pan]` and `dbmRanges[pan] = {min, max}`, in the existing per-radio `ClientDisplay` feature document that already holds `waterfallRates`. No schema bump. - **Who may store and restore is declared per feature, by the backend.** FFT FPS: a new optional record, `PanFrameRateShaping`, field `clientPersistsFrameRate`, read through `RadioCapabilities::clientPersistsPanFrameRate()`. The dBm range: a new field in the existing `PanAmplitudeModel`, `clientPersistsDbmRange`, read through `RadioCapabilities::clientPersistsDbmRange()`. Same shape as `BackendPanAveraging::clientPersistsAveraging` (#5924). No new top-level capability bool: the count stays at the frozen 70. - **Two second conditions remain, and you may want them changed.** FFT FPS also needs `RadioModel::shapesDisplayRatesLocally()`, as the averaging path does: the restore sits where the local shaper is seeded. The range also needs absolute bins, inside its accessor: the client writes the range into the pan model, which is wrong on a radio whose bins are scaled by it or whose backend publishes its own range. Neither condition grants anything without the declaration. - **The waterfall-rate gate on `main` is not touched.** It still reads `shapesDisplayRatesLocally()` alone. - **Half of what was observed.** FFT AVG and Wt Avg are not fixed on the HL2. #5924 gave them a table in this document (`fftAverages`), kept only for a backend that declares `clientPersistsAveraging`; the HL2 declares it false. - **Not seen in a running app at this head, on any radio.** The defect was (HL2, 2026-10-01, 2 of 2 restarts: FPS 12 came back 25; a 120 dB scale came back -40 / 90 dB). - **It changes what the pan model holds, on an HL2 only:** an operator's range is now written into `PanadapterModel`. That range is absolute dB (dBFS under a dBm label). - **Families:** the Hermes-Lite 2 only, for both settings. It is the radio the defect was observed on, and it stores nothing itself. Flex, Icom, ANAN, RTL-SDR and the demo backend declare no owner: nothing is stored or restored for them, as on `main`. Widening to another family is one declaration in that family's backend, for its owner or the maintainer to make; #6129 claims the ANAN side of the range. - **Yours to move:** the FPS bounds 5..60 (the slider's own) and the plausibility bounds, moved unchanged from `MainWindow_Wiring.cpp:168` on `main` to `core/DbmRangePlausibility.h`. ## What was wrong on `main` A Flex stores both and reports them on connect. A radio with no display engine reports nothing (observed on the HL2, and fixed here for the HL2 only; line references are at `7a5ca748`): - **FFT FPS** stays at the widget default (`SpectrumWidget.h:1666`, 25) and pan wiring seeds the shaper from the widget (`MainWindow_Wiring.cpp:3320`). - **The dBm range:** pan wiring primes the widget from the model (`MainWindow_Wiring.cpp:4074`, `MainWindow_Session.cpp:1855`), the model starts at -130 / -40 (`PanadapterModel.h:279`), and on a radio that does not own the scale nothing ever moves it: on an HL2 the range command is attempted and dropped at the command plane (the HL2 leaves `radioOwnsDbmScale` at its default `true`, `RadioCapabilities.h:439`, so the gate at `MainWindow_Wiring.cpp:3779` does not stop it), and nothing echoes. - Of a pan's display settings only the waterfall rate (`core/ClientDisplaySettings.h:90-132`) and, since #5924, FFT averaging for a backend that declares it (`:48-88`) have a client store. ## The change - **FFT FPS**, where the backend declares the client its owner, is saved by the Display slider (before the adaptive-throttle return), Clone to all Pans and Reset to Defaults, and restored into the widget before the shaper is seeded from it. - **The dBm range**, where the backend declares the client its owner, is saved on a scale request or a finished drag, and restored into the pan model before the widget is primed from it, at both prime sites. FFT Floor Auto's own moves are not stored. - The pending-write key gains the field. It was (radio, pan), so two fields of one pan edited inside the 250 ms window replaced each other. - `dbm_range_plausibility_test` now includes the real predicate instead of a hand copy. One row added to the touchpoint manifest (240 with `main` at `12df6943` merged in). - A Flex is unchanged, and so are Icom, ANAN, RTL-SDR and the demo backend: none declares an owner, so every new reader returns nothing and every new writer returns before the store. Save and restore of each setting ask one predicate (`MainWindow::clientPersistsFftFps()`, `MainWindow::clientOwnsPanDbmRange()`), each reading one `RadioCapabilities` accessor. - `docs/architecture/radio-capabilities-map.md` has the new record and field with each family's value. ## Tests `client_display_pan_settings_test` (new, socket-free, real store): round trip per radio and pan; the Flex-shaped scope; ownership of both settings read through the two accessors off real HL2, Icom, ANAN and Flex backends (the HL2 stores and restores; the others and an undeclared backend write nothing, read nothing and leave a held document byte for byte); refusals on write and read; a newer schema; coexistence with an `fftAverages` table in #5924's shape; two fields in one deferral window. `spectrum_overlay_fft_fps_bounds_test` (new, real widget, in a `TestSettingsProfile`): the slider runs the stored bounds. **One source-text pin block, and its claim:** the order and presence of the save and restore calls inside `MainWindow` methods, which link into no test, and that the save and the restore of each setting ask the one predicate. Fail-before, one build and one run per row. The two ownership rows are at `7a5ca748`; the other rows were measured at `295bff6e` and not run again: | mutation | failing checks | |---|---| | this branch | 0 | | `main`'s wiring, and a store that neither reads nor writes the new tables | 17 | | pan wiring does not restore FFT FPS | 1 (source text) | | the store ignores who owns the value on read | 1 | | the pending-write key has no field | 2 | | both ownership accessors return true for every backend | 33 (8 each for Icom, ANAN, Flex and an undeclared backend, and the relative-bins check) | | both ownership accessors return false for every backend | 8 (7 HL2, and the absolute-bins control) | | a stored range is not validated on read | 1 | | a scale request does not adopt the range | 1 (source text) | | the writer replaces the whole document | 6, `fftAverages` and the waterfall rate among them | | the stored FPS maximum is 61 | 1 (the slider bounds test) | Full ctest, macOS, Qt 6.12.0, at `d60f4660` (`main` `12df6943` merged in, no code of this PR changed): 645 of 646 pass at `-j8`; the one failure is `peripheral_auth_dialog_test` (#6008), which also fails alone. The run before it, at `7a5ca748` (`main` `85f2a124` merged in): 644 of 646 pass at `-j8`. Two failures, neither from this PR: `peripheral_auth_dialog_test` (#6008; diagnostic on #6134), which also fails alone, and `vkamp_connection_test` (#5254), which passed when rerun alone. `hl2_receiver_count_restart_paced_test` and `libre_radar_test` passed at `-j8` in this run. Static checks (`static-checks.yml`, stdin from `/dev/null`): the script steps of the `Static checks` job pass, including the touchpoint manifest (240, up to date) and `check_capability_records.py --strict` (70 booleans, at the frozen 70); colour ratchet +0. `check_a11y` on the three changed GUI files: no findings. The `Sanitizer option configures` job was not run locally. ## With the open PRs - **#5924** (merged, and merged in here at `295bff6e`): it conflicted in `ClientDisplaySettings.h`, `MainWindow.h` and `MainWindow_Wiring.cpp`, all resolved by keeping both sides. The tables do not overlap and its pending-write key has its own prefix. - **#5919** (merged) is in this branch through `main`. - #6117 and #6115 (both merged) are in this branch. - **#6129** (open, head `04762258`): see the update block at the top. - **#6139** (open, ours): both PRs add to `RadioCapabilities.h` and to each backend's `capabilities()`, in different places. A trial merge of `7a5ca748` with its head `98be49a1` is clean; the merged tree was not compiled. ## Not verified - The restore in a running app, on any radio, at this head. - The older rows of the fail-before table at this head (see Tests). - Linux and Windows: built and tested on macOS only. - The 3D view in a running app. Its floor resync after a zoom is excluded from the adopt and the store by a guard (`dabf83e3`) that is pinned as source text only; the path was not driven with frames. Reset to Defaults does not reset the dBm range; it did not before. **Known red on `main`:** `peripheral_auth_dialog_test` (#6008) fails on this Mac at this head, as it did on clean `main` at `22507580` ("Clear code or Status is clipped at the default dialog size": a serial-port name on this host widens the page; diagnostic on #6134). Clean `main` at `85f2a124` was not built here. `hl2_pan_create_async_test` passed; `vkamp_connection_test` (#5254) failed at `-j8` and passed alone. 73, ON8ST 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Ozy311
left a comment
There was a problem hiding this comment.
Re-review of ea2cf144, the one commit since @ten9876's two request-changes reviews at 98be49a1.
The blocker is resolved the way it was asked. Both reviews asked for the request, the store and the restore to be gated on a declared owner in a per-feature capability record, through one accessor, in the shape #6140 used. That is what this commit does:
RadioCapabilities::TxWaterfallClientFlag { bool clientPersistsFlag; }(no default, so an engaging backend must state its value), with a single accessorclientPersistsShowTxInWaterfall()(RadioCapabilities.h:343-361). It has the same shape asPanFrameRateShaping::clientPersistsFrameRateonmain.requestLocalShowTxInWaterfall(),saveShowTxInWaterfall()andrestoreClientShowTxInWaterfall()all read that one accessor (RadioModel.cpp:6157-6185).shapesDisplayRatesLocally()is no longer consulted for this field.- Only
Hl2Backendopts in, withtrue. Flex, Icom, ANAN, RTL and Sim each statestd::nulloptexplicitly in theircapabilities(). - No new top-level bool:
check_capability_recordsis at 70 of 70.
The tests moved with it. The opt-in checks now run on a real Hl2Backend, and the new opt-out checks use the real Icom, ANAN, RTL and demo backends with a document that already holds the flag, plus a backend that engages the record with the field false. That last case is the one that pins "declared false" apart from "absent".
Verified
On the PR merged into current main (0156894f; the branch is 84 commits behind, and the merge is clean):
check_capability_records: 70 booleans, at the frozen 70.check_command_plane: 368 against a baseline of 368, 0 strict.check_engine_boundary: 0 would block under--strict.check_test_registrationandgen_touchpoint_manifest.py --check: OK.
Not run: the two C++ tests, which need the core build, and the app.
Nits (non-blocking)
- ANAN goes back to a dead toggle. Before this commit ANAN took the local route, as every non-Flex backend did. Now it states no owner, so the button sends
transmit set show_tx_in_waterfall=, and that is dropped because ANAN has no command plane. ANAN can transmit and makes its waterfall rows on this host, the same position as the HL2. The commit message says this is "main's behaviour", which is true, and it is the conservative reading of the review. But it means the defect this PR fixes is still there on ANAN. Opting ANAN in is one line inAnanBackend::capabilities()if a maintainer wants it, here or in a follow-up. The same applies to Icom only if its waterfall is client-made and it can transmit. RTL and the demo are receive-only, so leaving them out is clearly right. - CI is from 2026-10-05. The merge is clean, and the static gates above pass on the merged tree, but the green checks describe an older merge base.
Nothing else from either earlier review is outstanding as far as I can see. The txwaterfall-while-disconnected nit and the untested onSliceAdded hop were both left as Limits, which is where the reviews put them.
73, K6OZY
Generated by Claude Code
…6166) Refs #5370 **Limits first.** 1. **The ruling we asked for on #5370 (2026-09-30) was not answered.** This PR implements option (1) of that request: gate the Timings fields on a radio with no command plane, dimmed with the reason. It is filed so the choice is concrete, not to pre-empt it. If you prefer hiding the group, or want (3) first, say so and this changes or closes. 2. **Option (3), a host-side PTT-to-RF delay for radios with no command plane, is not here** and is still an open question. This PR says `Refs`, not `Fixes`, so #5370 stays open for that choice: it is not ours to close. 3. **No field holds a value on such a radio, on purpose.** Option (2), a stored value that delays nothing, is what the reporter and the triage both advised against. 4. **Nothing was run in the app and nothing on a radio.** Everything below is the production dialog in a socket-free test, on macOS. 5. **Only the six fields wired through `connectTimingField` change.** Three neighbours on the same tab have their own shape and are left alone, listed under "Deliberately not covered". 6. **The Demo radio is not covered.** `RadioModel::hasCommandPlane()` is true for the Sim backend, and nothing under `src/core/backends/sim/` parses `interlock`, so on the Demo the six fields stay live and still take no effect. Whether the Demo should dim is your call; the predicate here is the one #6044 uses. ## What was wrong `RadioSetupDialog::buildTxTab()` wires six fields through the `connectTimingField` helper (`src/gui/RadioSetupDialog.cpp`). Each sends `interlock set <key>=<value>` through `RadioModel::sendCommand` unconditionally and keeps no local value. On a radio with no command plane `RadioModel::sendCmd` drops the text (`"no command plane for this backend, dropping"`, `commandDropped`), so on a Hermes-Lite 2 a typed TX Delay did nothing, the operator saw at most the once-per-session notice, and the field read `0` on the next open. Meanwhile the box looked like amplifier sequencing had been set. Nothing else consumes these values on such a radio: `accTxDelay`, `txDelay`, `tx1Delay`, `tx2Delay`, `tx3Delay` and `interlockTimeout` are carried into `TransmitModel` only by `FlexBackend` (`TransmitDelta`), and no file under `src/core/backends/hl2/` reads or writes them. The HL2's own TX timing (`tx_buffer_latency`, `ptt_hang_time`) is gateware state that these fields do not reach. ## The change On a **connected radio with no command plane** the six fields are unavailable: disabled, dimmed with `color.control.unavailable`, never hidden, with the same reason on the tooltip and `accessibleDescription`: > Unavailable: this radio does not accept transmit timings from here. No delay or timeout set here is applied. An edit that arrives anyway (automation, a queued signal) returns before any send, so there is no drop and no notice. The gate follows `RadioModel::connectionStateChanged` and lets go when disconnected. - **Predicate:** `!isConnected() || hasCommandPlane()`. It is the predicate #6044 uses for the title-bar headphone pair (`headphoneControlsAvailable()` in `MixerControlAvailability.h`) and the one the Record Mode pair in this same dialog uses (`radioSideRecordingReachable()`). No family name, no `usesFlexCommandPlane()`, no new capability field. - **Mechanism:** `setEnabled` + tooltip + `accessibleDescription`, by hand, as #6044 and the Record Mode pair do. Not `ControlAvailabilityRegistry`: command-plane reachability is not in `RadioCapabilities`. - **With a command plane nothing changes on the wire:** each field sends the same `interlock set` text, same clamp, Timeout still in ms. - **Two things do change on every radio:** each field gains an accessible name from its caption (it had none), and its stylesheet gains a `QLineEdit:disabled` rule, which applies only while disabled. ## Fields | Field | Wire text | Now, connected and no command plane | |---|---|---| | ACC TX | `interlock set acc_tx_delay=` | dimmed, with the reason | | TX Delay | `interlock set tx_delay=` | dimmed, with the reason | | RCA TX1 | `interlock set tx1_delay=` | dimmed, with the reason | | RCA TX2 | `interlock set tx2_delay=` | dimmed, with the reason | | RCA TX3 | `interlock set tx3_delay=` | dimmed, with the reason | | Timeout (sec) | `interlock set timeout=` (ms) | dimmed, with the reason | **Deliberately not covered:** - **TX profile combo** (same group). It calls `TransmitModel::loadProfile`, a model verb that emits `profile tx load`, not a dialog `sendCommand`. A different shape, not named in #5370. - **TX Band Settings button.** It triggers MainWindow's action, which MainWindow already disables with a reason where there is no command plane. The button itself stays live and then does nothing; a separate, smaller defect. - **Interlocks - TX REQ, RCA and Accessory polarity.** Neither combo is connected to anything, on any radio, Flex included. Not the dropped-write shape. - **Max Power** is already read-only watts there (#6015). **Show TX in Waterfall** is #6139. **Slice/TX Follow** is client state and works everywhere. ## Tests New `radio_setup_tx_timing_fields_test`: the production `RadioSetupDialog`, an injected backend, no socket. 3 cases, all six fields in each: - **no command plane, connected:** each field is visible, disabled, its `accessibleDescription` starts `Unavailable:` and equals the tooltip, it has its accessible name; a typed value plus `editingFinished` logs no `interlock set` and raises no `commandDropped`. Disconnect: enabled, no reason. Reconnect: dimmed again. - **command plane (bare Flex model):** each field is enabled with no reason and sends its exact text once (`interlock set acc_tx_delay=11`, `tx_delay=30`, `tx1_delay=12`, `timeout=45000`, `tx2_delay=13`, `tx3_delay=14`); a negative entry is still sent and shown as `0`. - **command plane, connected (Demo):** enabled, same text, no `commandDropped`. **Mutations, each built and run through the full suite, never pushed:** - availability predicate replaced by `true`: the test fails at `!edit->isEnabled()` on ACC TX (4 passed, 1 failed); - only the send guard in the `editingFinished` handler removed: the test fails with `interlock set` logged 2 times where 0 is expected (4 passed, 1 failed). **Full suite** (macOS arm64, Qt 6.12.0, `-j8`), at `51bad5ff`: 642 of 644 pass; at `2c2853c1` (the review nits): 643 of 644, `vkamp_connection_test` passing that time. Both failures are known on `main` on this Mac and both also fail when rerun alone: - `peripheral_auth_dialog_test` (#6008). This PR touches `RadioSetupDialog.cpp`, so the comparison is exact: at this head and at `main` the output is the same line for line. `Clear code or Status is clipped at the default dialog size`; `dialog 960x680, viewport 658, page minimum 784, horizontal scroll max 126`; three `serial combo (hidden): minimum 344` lines naming this Mac's "LG Monitor Controls" serial port. The page minimum did not move, so this change did not widen the dialog. - `vkamp_connection_test` (#5254): one of its two known checks each time (`commands after a disconnect are dropped, not queued` at `-j8`, `out-of-range antenna ports never reach the wire` alone). No VKAMP file is touched, and it passed in the next full run on this machine. **Static checks:** every script step of the "Static checks" job passes locally, the colour ratchet at +0. `tools/check_a11y.py` on `RadioSetupDialog.cpp`: 9 warnings at this head, the same 9 at the base, none on the lines changed here. The touchpoint manifest regenerates unchanged. ## Overlap with #6139 #6139 (Show TX in Waterfall) edits the same function, `buildTxTab()`, and the same region of `tests/tests.cmake`. A trial merge of this head with #6139's head is clean: no conflict in either file. Either order works. ## Not verified - Nothing in the running app: no screenshot, no screen reader, not the look of the dimmed fields in either theme. - Nothing on a radio, HL2 or Flex. The Flex side rests on the bare-model and Demo cases above. - Linux and Windows locally; CI only. - ANAN, Icom and RTL individually. They have no command plane, so the same gate applies to them; only the injected no-command-plane backend was run. 73, ON8ST 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Seven conflicts, all one cause: aethersdr#6260 (eb9a1fb) added the lineoutControl record on the line where this branch adds txWaterfallClientFlag. Both sides are kept in every file, main's line first, this branch's after it. - src/core/backends/anan/AnanBackend.cpp: lineoutControl engaged (main), then txWaterfallClientFlag absent. - src/core/backends/flex/FlexBackend.cpp: lineoutControl engaged (main), then txWaterfallClientFlag absent. - src/core/backends/hl2/Hl2Backend.cpp: lineoutControl absent (main), then txWaterfallClientFlag engaged with clientPersistsFlag true. - src/core/backends/icom/IcomCivBackend.cpp: lineoutControl absent (main), then txWaterfallClientFlag absent. - src/core/backends/rtl/RtlSdrBackend.cpp: same. - src/core/backends/sim/SimBackend.cpp: same. - docs/architecture/radio-capabilities-map.md: main's lineoutControl row, then this branch's txWaterfallClientFlag row. Everything else merged without conflict. aetherd-touchpoints.md was regenerated and did not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged Seven files conflicted, all for one reason: #6260 added a After the merge: 73, ON8ST |
Conflicted file: tests/tests.cmake, in two places. 1. The registration block after radio_setup_recording_mode_dim_test. Main added radio_setup_headphone_output_gate_test there (aethersdr#6353, 9fabcbf); this branch has radio_setup_show_tx_waterfall_test in the same spot. Both blocks are kept whole, main's first, ours after it. The two source lists are identical, so ours links what main's sibling links. 2. AETHER_SETTINGS_CONSUMERS: both names kept, main's first. The merged file is origin/main plus exactly this branch's 40 lines; nothing from either side is dropped. No other file conflicted. RtlSdrBackend.cpp, SimBackend.cpp, RadioSetupDialog.cpp and RadioModel.cpp merged as text; each still states txWaterfallClientFlag next to main's new receiveAudioExport. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged One file conflicted, After the merge: 73, ON8ST |
…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>
rfoust
left a comment
There was a problem hiding this comment.
Issue fit
This fixes the dead Show TX in Waterfall toggle for the Hermes-Lite 2. The HL2 declares client ownership, so Radio Setup and txwaterfall update the transmit model without waiting for an echo, and the choice is saved per radio and restored on connect. Flex retains its radio-owned echo path. The prior maintainer request for an explicit capability declaration is addressed at this head.
Scope
| Files | Purpose | Verdict |
|---|---|---|
RadioModel, ClientDisplaySettings, capabilities and backends |
Local apply, exact per-radio storage, connect restore, and explicit owner | In scope |
| Radio Setup dialog and automation bridge | Route both controls through the model | In scope |
Two registered tests and tests.cmake |
Cover opt-in, opt-out, persistence, bridge and dialog | In scope; socket-free |
docs/HERMES.md, capability map, bridge guide, user guide and MCP description |
Explain the behavior and correct the display-only classification | In scope |
No unrelated default, UI restyle, new keying route, or CHANGELOG entry is included.
Verification
I built the PR merged with then-current main on macOS ARM64 and ran both new focused tests: 2/2 passed. Removing the connect-time restore made the model test fail on four specific assertions; restoring it returned both tests to passing. The full ARM64 app built. The new user-guide pages passed npm ci and npm run build; the MCP description passed python3 -m py_compile, and the final diff passes git diff --check. Both follow-up commits have valid SSH signatures. CI for this final head is still running, so branch protection should hold the merge until it passes.
Remaining verification boundary
The demo cannot key, and the keyed HL2 waterfall image and receive IQ have not been measured on hardware. The PR states this limit. The model test covers the restore before the production HL2's connect, pan and slice announcements; the final GUI handoff is not shown in a running HL2 session. The optional ANAN/Icom support question stays outside this HL2-only fix.
I approve this head after the documented ownership fix and the user documentation update.
rfoust
left a comment
There was a problem hiding this comment.
Reapproving head 93fea1c after the signed CI fix. Linux and macOS failures on the previous head both came from the radio_setup_show_tx_waterfall_test link omitting CanonWindow.cpp after the current main dialog change. The test target now compiles and links that source; local ARM64 CMake generation confirms it is on the target link line. The earlier review covered the HL2 client-owned flag, UI path, documentation, and focused tests. Required CI is rerunning on this head; squash auto-merge remains enabled.
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,clientPersistsFlagtrue). The demo, ANAN, Icom and RTL-SDR declare nothing and keepmain'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 onanswers with the radio-echo note again. Full suite at this head: 644 of 645; the one failure isperipheral_auth_dialog_test(#6008), which fails on cleanmaintoo. Source line numbers below aremainat85f2a124.Refs #5678 (HL2 parity; "Show TX in Waterfall", row P30 of our status table). No issue exists for this.
Limits, first
truemodel flag shows neither the colour law nor real receive IQ while keyed; the demo cannot key at all.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 onmain. Engaging another family is one declaration, for that family's owner or the maintainer to make.dbmToWaterfallLevel). On an HL2 they are absolute dB (dBFS under a dBm label); whether the law suits them is unknown until someone keys one.showTxInWaterfall, in the existing per-radioClientDisplayfeature document. No schema bump. Written and read only where the backend declares the client its owner.What was wrong on
mainOn 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 ofTransmitModel::showTxInWaterfall().RadioSetupDialog.cpp:2717), and so does the bridge verb (AutomationServer.cpp:3599).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.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 existingrequestLocalPanWeightedAverage: 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).TransmitModel::applyChanges, the path an echo takes.TransmitModel.cpp:43) and no status puts the flag back, soRadioModel::onConnected()restores it, on the same declaration.txwaterfallverb 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).SpectrumWidgetis not changed. Default unchanged: off.Tests
show_tx_in_waterfall_client_flag_test(new, socket-free, real backends): on the realHl2Backend, request, store per radio, restore on connect, and the bridge verb; on the real Icom, ANAN, RTL-SDR and demo backends, each with aClientDisplaydocument that already holds the flag, nothing is restored, the request declines and the document is untouched; the same on a realFlexBackend(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:
main's behaviour (request declines, connect restores nothing)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_a11yonRadioSetupDialog.cpp: 9 warnings, the same 9 asmain, none on lines this PR touches.check_capability_records: 70 booleans, frozen at 70. The steps were run from this branch'sstatic-checks.yml;main's newercheck_build_optionssteps are not on this branch and were not run.With the open PRs
mainand is not in this branch.7a5ca748) adds its own records toRadioCapabilities.h; a trial merge with this head is clean and the merged header still counts 70 booleans. A trial merge withmain85f2a124is clean. Neither merge result was built.Not verified
main's behaviour by declaration, checked on their real backends, never connected.Known red on
main:peripheral_auth_dialog_test(#6008) fails on this Mac atea2cf144with the diagnostic it prints on cleanmainat22507580(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). Cleanmainwas not re-run at85f2a124.hl2_pan_create_async_testandvkamp_connection_testpassed here.73, ON8ST
🤖 Generated with Claude Code