Skip to content

fix(tx): Show TX in Waterfall works with no radio echo. Principle VIII. - #6139

Merged
rfoust merged 18 commits into
aethersdr:mainfrom
on8st:hl2/show-tx-in-waterfall
Oct 12, 2026
Merged

rfoust merged 18 commits into
aethersdr:mainfrom
on8st:hl2/show-tx-in-waterfall

Conversation

@on8st

@on8st on8st commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Update after review, ea2cf144 (2026-10-05): ownership of the flag is now declared, not inferred. Only the Hermes-Lite 2 declares the client its owner (RadioCapabilities::txWaterfallClientFlag, clientPersistsFlag true). The demo, ANAN, Icom and RTL-SDR declare nothing and keep main's behaviour: the toggle sends the wire text, the model stays false, nothing is stored or restored. That is narrower than this PR was. On the demo, txwaterfall on answers with the radio-echo note again. Full suite at this head: 644 of 645; the one failure is peripheral_auth_dialog_test (#6008), which fails on clean main too. Source line numbers below are main at 85f2a124.

Refs #5678 (HL2 parity; "Show TX in Waterfall", row P30 of our status table). No issue exists for this.

Limits, first

  • Nothing was keyed. On an HL2 the toggle now sets the flag. What the waterfall draws during an over on an HL2 has not been seen, and what that radio's receive path reads while it transmits has not been measured. A true model flag shows neither the colour law nor real receive IQ while keyed; the demo cannot key at all.
  • The fix has not been seen in a running app. The dead toggle was (2026-10-01, bridge verb, 6 of 6 polls false). The fix is tested socket-free on the real models and dialog.
  • It reaches the Hermes-Lite 2 only. The request, the store and the restore are gated on one accessor, RadioCapabilities::clientPersistsShowTxInWaterfall(), which reads a declared per-feature record. Flex, Icom, ANAN, RTL-SDR and the demo leave the record absent. On Icom, ANAN, RTL-SDR and the demo the toggle is as dead as on main. Engaging another family is one declaration, for that family's owner or the maintainer to make.
  • Yours to move: the keyed rows take the colour law a Flex's keyed rows use (dbmToWaterfallLevel). On an HL2 they are absolute dB (dBFS under a dBm label); whether the law suits them is unknown until someone keys one.
  • Persistence is in the radio's settings document, not a flat key: one optional bool, showTxInWaterfall, in the existing per-radio ClientDisplay feature document. No schema bump. Written and read only where the backend declares the client its owner.

What was wrong on main

On a Flex the flag is the radio's: transmit set show_tx_in_waterfall= tells it, and its transmit status echoes it (FlexBackend.cpp:1193). That echo is the only writer of TransmitModel::showTxInWaterfall().

  • The Radio Setup button sends the wire text unconditionally (RadioSetupDialog.cpp:2717), and so does the bridge verb (AutomationServer.cpp:3599).
  • A radio with no command plane drops it (RadioModel.cpp:9764) and nothing echoes, so the flag can never become true. The demo has a command plane and no handler for the flag, so nothing echoes there either.
  • While keyed, the pane holding the TX passband draws rows only when the flag is true (SpectrumWidget.cpp:8488). With it false that pane's waterfall stands still for the whole over.

There is something to draw: the HL2 mutes its demodulator while keyed and the spectrum keeps running on real IQ (Hl2RxDsp::setAudioMuted).

The change

  • RadioModel::requestLocalShowTxInWaterfall(bool), the shape of the existing requestLocalPanWeightedAverage: where the backend declares the client the owner of the flag it applies the flag and returns true; everywhere else it returns false and the caller sends the wire text, which only a Flex answers. No raw command is added above the seam (check_command_plane: 400, baseline 400).
  • It writes the model through TransmitModel::applyChanges, the path an echo takes.
  • It stores the value per radio, on the same declaration. A disconnect clears the transmit model (TransmitModel.cpp:43) and no status puts the flag back, so RadioModel::onConnected() restores it, on the same declaration.
  • The Radio Setup button and the txwaterfall verb ask the model first.
  • RadioCapabilities::txWaterfallClientFlag: a new optional per-feature record with one field, clientPersistsFlag (no default). Engaged by the HL2 only. No top-level capability boolean is added (check_capability_records: 70 of 70).

SpectrumWidget is not changed. Default unchanged: off.

Tests

show_tx_in_waterfall_client_flag_test (new, socket-free, real backends): on the real Hl2Backend, request, store per radio, restore on connect, and the bridge verb; on the real Icom, ANAN, RTL-SDR and demo backends, each with a ClientDisplay document that already holds the flag, nothing is restored, the request declines and the document is untouched; the same on a real FlexBackend (the echo still works) and on a backend that engages the record with the field false; the bridge verb's radio-echo note on Flex and on the demo; the store's refusals.

radio_setup_show_tx_waterfall_test (new, production dialog, real backends): on an HL2 a click sets the model and sends no wire text; on an Icom, which declares no owner, and on a Flex the click sends the wire text and the model is not set. No source-text pin.

Fail-before, one build and one run per row, failing checks in the two tests:

mutation client flag test dialog test
this branch 0 0
main's behaviour (request declines, connect restores nothing) 11 1 slot
the request does not write the model 5 1 slot
the connect edge does not restore 1 0
the request is taken on a Flex too 4 1 slot (Flex)
the request does not store 3 0
the button sends the wire text unguarded 0 1 slot
the bridge verb skips the local route 2 0
the store ignores who owns the flag on read 1 0
the ownership accessor is true for every backend 40 2 slots
the ownership accessor is false for every backend 15 1 slot

The two accessor rows were measured at ea2cf144, one build and one full run each. The other rows were measured at earlier heads, before the opt-out checks existed, and were not re-run.

Full ctest, macOS, Qt 6.12.0, at ea2cf144: 644 of 645 pass. The one failure is not ours; see below.

Static checks (static-checks.yml, stdin from /dev/null): clean. check_a11y on RadioSetupDialog.cpp: 9 warnings, the same 9 as main, none on lines this PR touches. check_capability_records: 70 booleans, frozen at 70. The steps were run from this branch's static-checks.yml; main's newer check_build_options steps are not on this branch and were not run.

With the open PRs

Not verified

  • Anything keyed; see Limits.
  • Linux and Windows at this head. ANAN, Icom and RTL-SDR on a radio: they keep main's behaviour by declaration, checked on their real backends, never connected.
  • A running app at this head, the demo included.
  • That the main window hands a restored flag to the spectrum widget.

Known red on main: peripheral_auth_dialog_test (#6008) fails on this Mac at ea2cf144 with the diagnostic it prints on clean main at 22507580 (pasted on #6134): "Clear code or Status is clipped at the default dialog size", dialog 960x680, viewport 658, page minimum 784, horizontal scroll max 126 (a serial-port name on this host widens the page). Clean main was not re-run at 85f2a124. hl2_pan_create_async_test and vkamp_connection_test passed here.

73, ON8ST

🤖 Generated with Claude Code

on8st and others added 4 commits October 1, 2026 12:30
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>
@on8st
on8st requested a review from a team as a code owner October 4, 2026 01:19
@aethersdr-agent

Copy link
Copy Markdown
Contributor

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 gh token is expired (401) and the log tool returned step names only, so everything below is from the tree at 0ff1da30 plus what the three jobs tell us. I'd rather say that than paraphrase a log I didn't see.

What the jobs say

job scope result
check-windows subset only — --target AetherSDR aether-dv-waveform thumbdv_queue_test dstar_model_test digital_voice_waveform_process_test cross_needle_meter_test (ci.yml) ✅ pass
check-macos whole tree (cmake --build build -j…) ❌ Build
build (Linux) whole tree ❌

Windows passing is the useful signal: it builds every production file you touched and never builds either new test target. So RadioModel.cpp, RadioSetupDialog.cpp, AutomationServer.cpp and ClientDisplaySettings.h are fine as written — MSVC compiled all of them. The only things macOS and Linux build that Windows doesn't are your two new test targets.

I also ran clang++ -fsyntax-only over both new test sources and all three changed production sources locally (Homebrew Qt 6.11, not CI's 6.12): clean, with the dialog test stopping only at its .moc include, which is expected without a build tree. So this isn't a compile error.

The cause

show_tx_in_waterfall_client_flag_test constructs the production bridge — AutomationServer bridge; at lines 178 and 196 — but its link line is:

target_link_libraries(show_tx_in_waterfall_client_flag_test PRIVATE
    aethercore Qt6::Core Qt6::Network Qt6::Widgets)

AutomationServer.cpp is not in aethercore. It lives in aetherdesktop_support (CMakeLists.txt:1591-1598), and handleLine is out-of-line (AutomationServer.h:309), as are the ctor/dtor and the moc'd metaobject. So expect the log to end in Undefined symbols for architecture arm64: "AetherSDR::AutomationServer::AutomationServer()" (and handleLine, staticMetaObject) on macOS, and the undefined reference to equivalent on Linux.

The tell is in your own cmake comment: "Qt6::Widgets for AutomationServer." The dependency was spotted — it's just that Qt6::Widgets isn't what carries the bridge; aetherdesktop_support is, and it already brings Qt6::Widgets and aethercore with it (PUBLIC).

The fix

tests/tests.cmake keeps a registry for exactly this, with a comment that reads like it was written for this failure:

AutomationServer is desktop support, not engine code. Keep every test that instantiates the production bridge linked through the same desktop-only library as AetherSDR so moving QtWidgets out of aethercore cannot silently leave these harnesses with unresolved bridge symbols.

Add one line to AETHER_AUTOMATION_SERVER_TESTS:

 set(AETHER_AUTOMATION_SERVER_TESTS
     ...
     reroute_dead_controls_test
+    show_tx_in_waterfall_client_flag_test
 )

That's how automation_persist_diagnostics_test and automation_boundary_core_test work — they also declare only aethercore Qt6::Core Qt6::Network on their own line and pick up aetherdesktop_support from the foreach at the bottom. Appending aetherdesktop_support to your target's own link line also works, but the registry is the convention here and is the thing that survives the next library reshuffle.

radio_setup_show_tx_waterfall_test is already correct — it links aetherdesktop_support directly, and its source list matches radio_setup_recording_mode_dim_test exactly.

To confirm in ten seconds:

cmake --build build --target show_tx_in_waterfall_client_flag_test

One 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

on8st and others added 4 commits October 4, 2026 03:45
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>

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Issue fit

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.md entry — correct; it must not be added in an ordinary PR.
  • No new public surface. txwaterfall already existed; only the response note differs on the new branch, and the response keys (ok, txwaterfall, note) are unchanged in shape.
  • New persisted key, no new document. showTxInWaterfall goes inside the existing versioned ClientDisplay feature document under RadioModel::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/#3804 refs 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 joined AETHER_SETTINGS_CONSUMERS; the dialog block is shape-identical to its sibling radio_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 drives handleLine() 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.

  1. The enumeration omits the demo simulator. docs/automation-bridge.md:3005 and the PR body both list "Hermes-Lite 2, ANAN, Icom, RTL-SDR", but the gate is shapesDisplayRatesLocally() = m_backend != nullptr && m_flexBackend == nullptr, and SimBackend : public IRadioBackend with m_flexBackend = nullptr; // no Flex alias in demo mode (RadioModel.cpp:858). So sim takes the local route too. That is almost certainly the behaviour you want — I found no show_tx_in_waterfall handling anywhere in src/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 and radiocert drive most, and it is the one family in scope with a live command plane. Worth adding to the sentence. Inline on the doc line.

  2. The restored flag's path to the renderer is the one link nothing covered. Two paths seed SpectrumWidget: onSliceAdded (MainWindow_Wiring.cpp:1932-1935) and syncShowTxWf on TransmitModel::stateChanged (MainWindow_Session.cpp:1428-1437). On a fresh connect the restore fires stateChanged while m_radioModel.panadapters() is still empty, so onSliceAdded is the actual carrier — which assumes slice-add follows pan-add, and panadapterAdded (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 own m_showTxInWaterfall survives a disconnect, so the realistic reconnect case is covered by inertia. I raise it only because your onConnected() 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.

  3. 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 a show_tx_in_waterfall register at all. The clientSettingsDomains enum 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::waterfallRate already 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.

  4. tools/aether_mcp.py:759 groups txwaterfall under "gated by AETHER_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::sendCommand logs the full command text at lcProtocol debug 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 calls setupBackend("flex") (RadioModel.cpp:2272), so "a bare model is on the Flex backend" is accurate, and the other test additionally exercises a real FlexBackend via rebuildBackendForTest.
  • 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-1437 connects TransmitModel::stateChanged to a lambda that pushes to every pan's SpectrumWidget, 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 make get transmit showTxInWaterfall and the troubleshooting dump report a client value as radio-confirmed. It doesn't: the m_haveTransmitStatus / m_rfPowerFromRadio latches fire only on d.rfPower / d.maxPowerLevel (TransmitModel.cpp:112-120), so a showTxInWaterfall-only delta flips no provenance flag and emits exactly one stateChanged, as the test asserts.
  • Walked the connect/disconnect/radio-swap lifecycle. resetState() is called from exactly one place, onDisconnected() (RadioModel.cpp:7957), so nothing after onConnected() wipes the restore — and the !hasCommandPlane() early return sits after it. Identity is available at that moment: serial() returns m_lastInfo.serial (RadioModel.cpp:11573) and onConnected() already reads m_lastInfo.serial a 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. saveShowTxInWaterfall refuses on version > 1, Corrupt and Unavailable and checks the setFeature result, matching saveWaterfallRate exactly; 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 dbmToWaterfallLevel suits dBFS-under-a-dBm-label rows, remain open by your own admission. Also unchecked: Linux/Windows, and whether the dialog test's global qInstallMessageHandler sink is safe under the sanitizer lane with a QApplication running (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 is full-suite at 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

Comment thread docs/automation-bridge.md Outdated
Comment thread src/models/RadioModel.cpp
Comment thread src/core/AutomationServer.cpp
@on8st

on8st commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

The CI log's first error, both jobs: undefined reference to RadioSetupDialog::buildPeripheralsTab() linking radio_setup_show_tx_waterfall_test. Merge skew: #6027 moved that tab to RadioSetupDialog_Peripherals.cpp after this branch was cut; the new test lacked the file. The bridge test did link (the retrofit adds aetherdesktop_support), so the bot's cause was not it; registered anyway. Head ded1f7d3, main a8cfd990 merged: 622 of 623, only peripheral_auth_dialog_test (#6008) fails.

73, ON8ST

on8st and others added 3 commits October 4, 2026 05:46
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>
@on8st

on8st commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Not closed: the GUI half of nit 2. Nothing shows MainWindow handing a restored flag to a SpectrumWidget. The panadapterAdded seed is not added: no socket-free test can reach it. It is under "Not verified", and yours to ask for.

Head 3c491216, main a798558a merged. #6136 landed since (CI and CMake only) and is not merged in.

  • Nit 1: sim is in the doc and the body. By source, the demo, ANAN and RTL-SDR cannot key (canTransmit false), so the flag is stored there and no keyed row is drawn.
  • Nit 2, model half: comment reworded. 00740ca3 pins it on production HL2 wiring: the flag is in the model at connectionStateChanged(true), at the first pan and at the first slice. Moving the restore below the emit fails 1 check.
  • Nit 3: unchanged; the maintainer's call.
  • Nit 4: docs/automation-bridge.md:171 says the same. Not touched.
  • Sanitizer lane, message-handler sink: not checked.

622 of 623 pass; peripheral_auth_dialog_test (#6008) fails.

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>
@on8st

on8st commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Merged main into this branch after RTL M1 made it conflict (merge commit, no force-push). One test is red on this Mac: peripheral_auth_dialog_test, with the same serial-port diagnostic it prints on clean main at 22507580 (pasted on #6134); I did not re-run it on clean a325fb99. Head 98be49a1, main a325fb99 merged: 644 of 645 pass, both tests this PR adds among them.

One conflict, tests/tests.cmake: the RTL M1 merge and this branch added test targets at the same place, and both are kept. radio_setup_show_tx_waterfall_test now also lists RtlReceiverSettingsWidget.cpp and ControlAvailabilityRegistry.cpp, as main's own dialog tests do, since RadioSetupDialog.cpp builds the RTL Receiver page. The touchpoint manifest regenerates unchanged and the static checks pass.

73, ON8ST

ten9876
ten9876 previously requested changes Oct 4, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

  1. Declare persistence ownership rather than inferring it from the concrete backend. src/models/RadioModel.cpp:6157-6190 uses shapesDisplayRatesLocally() to authorize both writing and restoring the new setting. Its implementation in src/models/RadioModel.h:1346 is m_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 in docs/agents/settings.md, Settings Authority Policy, is: “The deciding test is whether THIS radio can save and restore the value — a declared capability, not a family assumption.” 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 ClientDisplay owner/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:1947 and src/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 and AETHER_AUTOMATION_NO_TX=1, connected only DEMO-0001. The bridge returned:
    {"ok":true,"txwaterfall":true,"note":"client-side flag on this radio, applied now; get transmit showTxInWaterfall reads it"}
    get transmit then showed showTxInWaterfall: true and transmitting: false. Opened Radio Setup → Transmit; the actual visible button read Enabled and checked: true. Coordinate clicks on that confirmed button produced model false, then true.
  • Quit/relaunched the app against the same private profile and reconnected the demo: showTxInWaterfall restored to true across the process boundary. Invalid txwaterfall banana returned ok: 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 on returned 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
ten9876 previously requested changes Oct 5, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 txwaterfall verb write TransmitModel through RadioModel::requestLocalShowTxInWaterfall() (via applyChanges, the echo path);
  • the value is stored per radio in ClientDisplay and restored in onConnected() before connectionStateChanged(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

  1. Ownership is inferred from "not a Flex", not declared.
    • Where: RadioModel.cpp:6157-6190. Both the request (the write and the store) and restoreClientShowTxInWaterfall() gate on shapesDisplayRatesLocally(), which is m_backend != nullptr && m_flexBackend == nullptr.
    • Canon: docs/agents/settings.md:143 says "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 on while connected to DEMO-0001 stores sim|DEMO-0001|ClientDisplay|1|{"showTxInWaterfall":true}. After kill -TERM, a relaunch and a reconnect, get transmit reads "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.

Nits (non-blocking)

  • txwaterfall while disconnected reports success for a no-op. AutomationServer.cpp:3598-3610: with no radio connected, txwaterfall on returns {"ok": true, "note": "radio echoes status…"}, and get transmit stays showTxInWaterfall: 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 an ok: false when !isConnected().
  • The restored flag's last hop to SpectrumWidget is untested (onSliceAdded at MainWindow_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_test exits 0, and radio_setup_show_tx_waterfall_test passes 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 (sha256 matches git 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 and HOME, AETHER_AUTOMATION_NO_TX=1. Connected with connect local serial DEMO-0001, which reported "serial": "DEMO-0001", "transmitting": false.

    Step get transmit
    before "showTxInWaterfall": false
    txwaterfall on → "note": "client-side flag on this radio, applied now…" true
    disconnect 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 in TransmitModel fire only on rfPower/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>
@on8st

on8st commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Bad news first: this is narrower than what you reviewed. The demo, ANAN, Icom and RTL-SDR are back to main's dead toggle. On DEMO-0001, txwaterfall on now answers with the radio-echo note, get transmit stays false, and nothing is stored or restored: the case both reviews drove gives a different answer at this head. And one test is red on this Mac: peripheral_auth_dialog_test (#6008), with the diagnostic it prints on clean main.

Head ea2cf144, one new commit on 98be49a1. main is not merged in; a trial merge with 85f2a124 is clean and was not built.

Blocker (both reviews): ownership is declared.

  • New optional record RadioCapabilities::txWaterfallClientFlag, field clientPersistsFlag with no default. No new top-level bool: check_capability_records reads 70 of 70.
  • One accessor, RadioCapabilities::clientPersistsShowTxInWaterfall(), gates the request, its store, and the connect-time restore. shapesDisplayRatesLocally() no longer decides anything for this flag.
  • I read "request/store/restore" as including the local apply, so an undeclared backend takes the wire route as on main.
  • Only the Hermes-Lite 2 engages it. Flex, Icom, ANAN, RTL-SDR and the demo declare it absent. Another family is one declaration, for its owner or you to make.
  • The ClientDisplay document and its schema are unchanged. Flex is unchanged on the wire.

Tests, socket-free, on the real backends.

  • Opt-in: real Hl2Backend stores, restores, and answers the bridge verb locally.
  • Opt-out: real Icom, ANAN, RTL-SDR and demo backends, each with a ClientDisplay document that already holds the flag: nothing restored, the request declines, the document is untouched. Same on Flex, and on a backend that engages the record with the field false.
  • Dialog test: a new slot on a real Icom backend (the click sends the wire text, the model is not set).
  • Accessor forced true for every backend: 40 checks fail in the flag test, 2 slots in the dialog test. Forced false: 15 checks and 1 slot.
  • Full suite at ea2cf144, macOS, Qt 6.12.0: 644 of 645 pass. Static checks pass (stdin from /dev/null); a11y 9 warnings, the same 9 as main.

Notes.

  • Docs: docs/HERMES.md, docs/automation-bridge.md and the capabilities map now name the HL2 as the owner. The bridge doc's demo and receive-only cases describe the wire route.
  • The hardware limit stays in the body: nothing was keyed, and a true model flag shows neither the colour law nor real receive IQ while keyed.
  • txwaterfall answering ok for a no-op while disconnected is not changed. The same note is now what the demo, ANAN, Icom and RTL-SDR return while connected. Refusing there is a change to the verb's contract: yours to ask for.
  • If you want the flag applied for the session on other host-rendering families without storing it, that is a second field in this record: say so.
  • With fix(gui): FFT FPS and the dBm range survive a restart. Principle VIII. #6140 at 7a5ca748: a trial merge is clean in either order, and the merged capability header still counts 70.

Not verified: anything keyed; a running app at this head (the demo included); Linux and Windows; the merge result with main; that the main window hands a restored flag to the spectrum widget.

73, ON8ST

@ten9876

ten9876 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

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 shapesDisplayRatesLocally(). The blocker in the review above stands. Flex behaviour is unchanged either way: the radio stores and echoes the flag.

— Jeremy KK7GWY + Claude

@on8st

on8st commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

The red check-windows on ea2cf144 is not from this change: the job stopped in "Setup sccache", where the download of sccache-v0.18.0-x86_64-pc-windows-msvc.tar.gz returned HTTP 500 twice, before anything was compiled. build, check-macos, Static checks and Sanitizer option configures pass on this head. I cannot re-run the job; a re-run should clear it.

73, ON8ST

ten9876 pushed a commit that referenced this pull request Oct 6, 2026
…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 Ozy311 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-review of 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 accessor clientPersistsShowTxInWaterfall() (RadioCapabilities.h:343-361). It has the same shape as PanFrameRateShaping::clientPersistsFrameRate on main.
  • requestLocalShowTxInWaterfall(), saveShowTxInWaterfall() and restoreClientShowTxInWaterfall() all read that one accessor (RadioModel.cpp:6157-6185). shapesDisplayRatesLocally() is no longer consulted for this field.
  • Only Hl2Backend opts in, with true. Flex, Icom, ANAN, RTL and Sim each state std::nullopt explicitly in their capabilities().
  • No new top-level bool: check_capability_records is 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_registration and gen_touchpoint_manifest.py --check: OK.

Not run: the two C++ tests, which need the core build, and the app.

Nits (non-blocking)

  1. 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 in AnanBackend::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.
  2. 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

jensenpat pushed a commit that referenced this pull request Oct 10, 2026
…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>
@on8st

on8st commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

Merged main (b3e0302e) into the branch as 9badd2de, because the PR had gone CONFLICTING. No rebase; the reviewed commits are unchanged.

Seven files conflicted, all for one reason: #6260 added a lineoutControl line exactly where this PR adds txWaterfallClientFlag (the six capabilities() functions and the capabilities map). Both lines are kept in each. Nothing else in the diff changed: it is still 18 files.

After the merge: check_capability_records at 70 of 70, check_command_plane 368 of 368, the touchpoint manifest up to date. Full ctest: 758 of 760; both tests of this PR pass. The two that fail are the clean-main failures on this Mac, #6008 and #6224, each with only its known lines.

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>
@on8st

on8st commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Merged main (46943eb3) into the branch as cf0006c5, because the PR had gone CONFLICTING again. No rebase; the reviewed commits are unchanged.

One file conflicted, tests/tests.cmake, in two places: #6353 registered its Radio Setup test at the spot this PR uses. Both registrations are kept. Main also added a capability record with #6083 (receiveAudioExport); all six backends state it and this PR's txWaterfallClientFlag.

After the merge: check_capability_records at 70 of 70, the touchpoint manifest up to date. Full ctest on macOS: 771 of 772; both tests of this PR pass. The one that fails is the clean-main failure #6008 on this Mac, with the dialog page minimum still 784.

73, ON8ST

rfoust pushed a commit that referenced this pull request Oct 12, 2026
…6402)

Refs #6185.

## Limits, first

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

## What was wrong

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

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

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

## The rule now

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

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

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

## Per family

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

## Tests

`client_display_pan_settings_test` is extended; no new test target.

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

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

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

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

## Results

macOS, Qt 6.12.0, full `ctest`:

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

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

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

73, ON8ST

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@rfoust rfoust self-assigned this Oct 12, 2026
rfoust
rfoust previously approved these changes Oct 12, 2026

@rfoust rfoust left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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
rfoust dismissed stale reviews from ten9876 and ten9876 October 12, 2026 19:18

Outdated review of 98be49a: the requested explicit txWaterfallClientFlag capability and one ownership accessor landed in ea2cf14; the current head also includes the user documentation and has a fresh approving review.

@rfoust
rfoust enabled auto-merge (squash) October 12, 2026 19:19

@rfoust rfoust left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

@rfoust
rfoust merged commit 0cce730 into aethersdr:main Oct 12, 2026
7 checks passed
@rfoust rfoust removed their assignment Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants