Skip to content

fix(radio): reroute controls a non-Flex radio dropped. Principle VIII. - #6049

Merged
ten9876 merged 12 commits into
aethersdr:mainfrom
on8st:hl2/reroute-step-recall-meters
Oct 3, 2026
Merged

ten9876 merged 12 commits into
aethersdr:mainfrom
on8st:hl2/reroute-step-recall-meters

Conversation

@on8st

@on8st on8st commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On a radio with no Flex command plane (HL2, ANAN, Icom, RTL), several controls wrote Flex wire text by hand. RadioModel::sendCmd() dropped the text, and the operator was told the control is unsupported. In each case the radio can already do what was asked, through a path another control uses. This PR routes those controls to that path. Where no path exists, the control now refuses visibly instead of setting a model flag that nothing acts on.

Routed to a working path:

control before, on a radio with no command plane now
CAT set_ts, the RX applet STEP control, a net's Tune Now step slice set <n> step= was dropped. CAT still answered RPRT 0, and get_ts read the old step. RadioModel::applyClientOwnedSliceStep() applies the step on the client, which is the same write the host memory bank already makes (SliceModel::applyRecalledStepHz).
Band-stack bookmark recall: AGC mode and threshold, NB on and level, NR on and level Hand-written slice set text was dropped. The recall restored frequency and mode and nothing else. Goes through the SliceModel setters, which reach setSliceAgc / setSliceNoiseBlanker. NR is recalled only where radio-side NR exists.
A net's Tune Now filter A hand-written filt was dropped. SliceModel::setFilterWidth, which reaches setSliceFilter.
Graphic EQ (applet and the ten MIDI eq.* params) Already served by ClientEq through applyGraphicEqToClientEq(), but the eq text still went into the drop, and the drop notice called a working control unsupported. The EqualizerModel sink skips the wire text when there is no command plane.
S-meter TX "Level" face Reads the MIC meter. An HL2 publishes TX:MICPEAK and no MIC, so the needle stayed at the -50 floor with a live mic. MeterModel::transmitLevelFaceValue() shows MICPEAK where MIC is not defined.

Now refuse visibly, through showUnsupportedControlNotice(), the one-shot notice #5262 M3 ruled a refusing gate must raise:

  • MIDI rx.nrEnable, MIDI rx.anfEnable and the anf_toggle shortcut turning NR/ANF on, where the radio declares no radio-side DSP (hasRadioSideDsp). The VFO already hides the NR and ANF buttons on the same field. OFF is already true there and is accepted, so a momentary MIDI button's release raises no second notice.
  • MIDI phone.amCarrier, where the radio declares no AM carrier control (hasAmCarrierLevel). The Phone applet already dims the slider on the same field.
  • nr_cycle skips its radio-NR step on such a radio and goes straight to NR2. Before this, its first press did nothing audible.

Flex is unchanged, and the test pins it. On a radio with a command plane, applyClientOwnedSliceStep() declines and the caller sends slice set <n> step= as before. The setters used by the band-stack and net paths produce the same text the hand-written code sent, in the same order among themselves (the AGC pair now goes out after the audio gain). Since #5904, the AGC and filter setters reach the Flex as receive intents that FlexBackend encodes into that same text on its guarded slice sink; NB and NR still go out as commandReady text. While KiwiSDR external receive replaces the slice, the AGC is not recalled through the setters, which would address the KiwiSDR AGC: MainWindow sends the original two slice set … agc_mode=/agc_threshold= lines instead (02dd0cc8). The test compares that text byte for byte, KiwiSDR case included, reading both the slice's commandReady and a real FlexBackend's slice sink. There is one deliberate exception, listed below.

#5262 M4. This lowers the command-plane baseline: MainWindow.cpp goes from 21 to 17 and MainWindow_Nets.cpp from 3 to 2 (406 to 401 in total on current main; the two KiwiSDR AGC lines stay in MainWindow.cpp). It removes hand-written wire text from GUI callers. It does not convert any of SliceModel's own wire sites. It calls the existing setters, whatever those do today.

#5904 (@rfoust's receive-control conversion, landed on main) routes setAgcMode, setAgcThreshold and setFilterWidth through typed backend intents. These callers use those routes with no code change; on a seam backend the band-stack AGC now reaches requestSliceAgc, whose default calls setSliceAgc.

#5919 (@rfoust, still open) converts the rest of the setters this PR calls (setNb, setNbLevel, setNr, setNrLevel, setAnf). @rfoust said to go ahead; the NB/NR commandReady assertions in reroute_dead_controls_test move with #5919's rebase, as the AGC/filter ones moved here for #5904.

Maintainer decisions (2026-10-02)

  • set_ts 0 returns RPRT -1 on every radio, Flex included. Third-party CAT clients see this change on a Flex: before, they got RPRT 0 and the radio was sent step=0. FlexLib never sends a step below 1 (Slice.cs:1553-1572, TuneStep clamps to 1), so the Flex is no longer sent a value FlexLib never sends.
  • The Flex wire text is pinned through FlexBackend's receive intents (c88e3fa1), since aetherd: migrate the first receive-command group behind the backend seam #5904 moved AGC and filter dispatch there.
  • Accepted: a net preset with positive LSB edges is mirrored before filt reaches a Flex, and the Demo radio refuses NR (nr_cycle goes straight to NR2 there).

Choices a reviewer should see

  • set_ts 0 is refused on every radio, Flex included, with RPRT -1 (RIG_EINVAL). Before, a Flex was sent step=0. The 0 that set_ts ? lists is Hamlib's RIG_TS_ANY marker, not a step (50a7477c).

  • Remote NR/ANF ON is refused where the radio has no radio-side NR/ANF, on rigctld (RPRT -11), SmartSDR-CAT (?;), the automation bridge (error) and TCI (broadcasts false). OFF is accepted as before (9e81be62).

  • setFilterWidth changes the text for one case. The raw filt sent a net preset's edges as stored. setFilterWidth first normalises polarity (Modes and width do not recover to profile settings when switching back from using FreeDV Flex on RASPberry PI5 #3434), as it does for every other caller. A net stored with positive lower-sideband edges, such as LSB 100..2900, now goes out as filt <n> -2900 -100, not filt <n> 100 2900. Canonical edges go out exactly as stored. Both cases are pinned in the test.

  • The Demo radio now refuses MIDI NR and AM carrier. The Sim declares neither hasRadioSideDsp nor hasAmCarrierLevel, and its synthetic connection has no answer for nr= or am_carrier=. ANF is still accepted wherever a command plane exists, because the Demo connection turns anf= into the generator's audible notch (SimBackend::setDemoAnf). Only a radio with neither a command plane nor radio-side DSP refuses ANF.

  • Pre-connect behaviour is kept. radioSideNoiseReductionAvailable() fails open with no backend attached, and the AM-carrier gate only refuses once a backend is attached.

  • A test-only friend. RadioModel gains friend class RerouteDeadControlsTestAccess, next to the two existing test friends. It installs an unopened RadioConnection, so the test can exercise the command-plane branch with no socket.

Localization (docs/HERMES.md, "keep bring-up inside the family backend")

The pre-PR grep reports these hits. None of them is a family test: there is no family == check and no usesFlexCommandPlane() branch. Every gate uses hasCommandPlane() or a declared RadioCapabilities field.

  • src/gui/MainWindow.cpp, MainWindow_Controllers.cpp, MainWindow_Nets.cpp, MainWindow_Shortcuts.cpp are the callers that wrote Flex text by hand. They now call a model method or a setter. That call site is what this PR changes.
  • src/gui/MainWindow_Wiring.cpp is the S-meter micMetersChanged connection, which now passes transmitLevelFaceValue().
  • src/models/RadioModel.cpp / .h hold the routing helpers. They sit next to hasCommandPlane() because the question each one answers is whether this radio has a command plane, or a declared capability. Also the EQ sink guard and the test friend.
  • src/models/SliceModel.h is a comment only: applyRecalledStepHz() gains its second caller.

No backend changes, and no new seam verb.

Constitution principle honored

Principle VIII. A control either acts or says that it cannot. None of them fakes success. Principle II. Where a command plane exists, the step stays radio-owned: the client does not assert it, and it is set when the radio's status comes back.

Test plan

Maintainer landing pass, Linux (Arch, Qt 6, RelWithDebInfo), on this branch merged with current main:

  • Full ctest, socket-owning tests excluded: all pass except connection_panel_size_test, a known local failure (Four tests fail deterministically on clean main locally: the a11y announcement fixture never receives focus (offscreen) #5811).
  • reroute_dead_controls_test goes red under each of nine mutations of the no-command-plane path (step, NR gate, ANF carve-out, band-stack NR and KiwiSDR guards, level face, set_ts 0, TCI gate, NR OFF).
  • Static gates --strict (engine boundary, capability records, command plane 401/401) and the touchpoint manifest check.
  • Demo simulator driven offscreen: RX applet STEP sends slice set 0 step=250 through the command plane as before; anf_toggle and slice dsp anf on accepted; slice dsp nr on refused.

Author's run:

  • Local build passes: Ninja, Qt 6.8.3, RelWithDebInfo, macOS arm64
  • Full ctest at 0a88bdf4 (rebased on current main): 554/555 passed, reroute_dead_controls_test included. The one failure is vkamp_connection_test, which also fails on a clean main on this machine; its timing sensitivity is tracked in Replace retained socket fixtures with injected and opt-in coverage #5254. This PR touches no VKAmp file.
  • New reroute_dead_controls_test (socket-free: a stub backend with no command plane, plus an unopened RadioConnection where one has to exist). It pins:
    • the client-owned step, and declining when a command plane exists
    • CAT set_ts/get_ts round trip
    • NR/ANF refusal with no phantom model state, and routing where radio-side DSP exists
    • Demo ANF still accepted, Demo NR still refused
    • AM carrier following its capability
    • EQ not dropped
    • the S-meter Level face on HL2, Flex and no-meter layouts
    • band-stack recall reaching the seam, plus the Flex wire text byte for byte
    • the net filter on the seam and on the wire, including the LSB mirroring
  • The static-checks gates pass locally at the PR head: check_command_plane.py, check_engine_boundary.py, check_capability_records.py, check_test_registration.py, check_ci_test_gate.py, check_network_timeouts.py, check_theme_seed.py, check_shader_dialects.py (all --strict), and gen_touchpoint_manifest.py --check, gen_seam_probe_table.py --check
  • Behaviour on a real radio: not claimed. Checked on the model and seam only.
  • Tested on macOS only. Not built on Linux or Windows.

Checklist

  • Commits are signed
  • No new AppSettings keys
  • Clean-room
  • No meter UI change beyond which value the existing face receives
  • No CHANGELOG.md edit

73, ON8ST

🤖 Generated with Claude Code

on8st and others added 2 commits September 30, 2026 15:56
On a radio with no Flex command plane (HL2, ANAN, Icom, RTL) these
controls wrote Flex wire text by hand, sendCmd() dropped it, and the
operator was told the control is unsupported -- although the radio
serves what they ask through a path another control already uses.

- CAT set_ts, the RX applet STEP control and a net's Tune Now step:
  RadioModel::applyClientOwnedSliceStep() applies the step to the
  slice when there is no command plane (the same client-side write the
  host memory bank already makes), and declines when there is one, so
  a Flex still gets `slice set <n> step=` and sets the step by status.
- Band-stack bookmark recall: AGC mode/threshold, NB on/level and NR
  on/level go through the SliceModel setters. On a Flex they write the
  same `slice set` text; elsewhere they reach setSliceAgc and
  setSliceNoiseBlanker, which the hand-written text bypassed. NR is
  recalled only where radio-side NR exists. Antenna and WNB lines are
  unchanged.
- Net Tune Now filter: SliceModel::setFilterWidth instead of `filt`.
  The setter mirrors lower-sideband edges stored positive (aethersdr#3434) before
  sending, as it does for every other caller; that is the one Flex wire
  difference, and canonical edges go out exactly as stored.
- MIDI rx.nrEnable / rx.anfEnable, the anf_toggle shortcut and
  phone.amCarrier: the radio's own NR/ANF exist only with
  hasRadioSideDsp (the VFO hides those buttons otherwise) and AM
  carrier only with hasAmCarrierLevel (the Phone applet dims it
  otherwise). With nothing to reroute to, they refuse through
  showUnsupportedControlNotice() instead of planting a phantom model
  state. ANF is still accepted wherever a command plane exists, because
  the Demo radio's synthetic connection turns `anf=` into an audible
  notch. nr_cycle skips its NR step on such a radio and goes straight
  to NR2.

MainWindow.cpp loses six raw commands and MainWindow_Nets.cpp one; the
command-plane baseline rows are lowered to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two more controls that looked dead on a radio with no Flex command
plane, each with a path the radio already serves:

- S-meter TX "Level": the face reads the MIC meter; an HL2 publishes
  TX:MICPEAK and no MIC, so the needle sat at the -50 floor with a live
  mic. MeterModel::transmitLevelFaceValue() gives the face MICPEAK where
  MIC is not defined. A Flex defines MIC and is unchanged.
- Graphic EQ (applet and the ten MIDI eq.* params): already served by
  ClientEq on these radios through applyGraphicEqToClientEq(), but the
  `eq` wire text still went into sendCmd's drop, which told the
  operator a working control was unsupported. The sink now skips it
  without a command plane.

reroute_dead_controls_test pins every reroute and refusal of this
branch at the model/seam level against a stub backend with no command
plane, and compares the Flex wire text of the setters the band-stack
and net paths now use with what was sent before -- byte for byte,
except the pinned mirroring of positive lower-sideband net edges. A
test-only friend gives it an unopened RadioConnection where a command
plane has to exist.

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.

1. Issue fit

No Fixes/Closes line is present in the PR body, so I reviewed against the PR's own stated intent: controls that a radio without a Flex command plane silently dropped, rerouted to a path that works or refused where none exists. It delivers that for the seven surfaces it names, and I could verify the load-bearing claims by reading the tree rather than taking them on trust:

  • FlexBackend.cpp:337 declares hasRadioSideDsp = true and :314 hasAmCarrierLevel = true, so neither new gate refuses anything on a Flex — that was my first suspicion and it does not hold.
  • RadioConnection.cpp:452 really does echo anf= and nb= but not nr=, so the ANF-on-Demo carve-out and the NR-still-refused asymmetry in requestRadioAutoNotch are both accurate rather than convenient.
  • Hl2Backend.h:122/128 overrides setSliceAgc and setSliceNoiseBlanker, so the band-stack reroute genuinely lands AGC and NB on an HL2 — this is the strongest part of the PR. setSliceAutoNotch/setSliceNoiseReduction are not overridden there (base no-ops at IRadioBackend.h:770/778), so refusing rather than rerouting NR/ANF is the right call and not an over-gate.
  • tools/check_command_plane.py: −6 in MainWindow.cpp (six deleted sendCommand calls) and −1 in MainWindow_Nets.cpp (the filt). The counts reconcile exactly; no send was quietly added elsewhere.

CI: all 5 checks green on 0a88bdf. The new test is not in any ctest -R gate in ci.yml, which is the repo's normal frozen-gate posture (#5405), not a finding.

2. Scope

File / group What it changes Claimed? Verdict
RadioModel.{h,cpp} 5 new query/route helpers + recallBandStackReceiveDsp; EQ commandReady early-return Yes In scope
RigctlProtocol.cpp set_ts routes through the client-owned step Yes In scope
MainWindow.cpp RX-applet step; band-stack DSP recall via the helper Yes In scope
MainWindow_Controllers.cpp MIDI NR/ANF/AM-carrier gated with notice Yes In scope
MainWindow_Shortcuts.cpp nr_cycle skip, anf_toggle gate Yes In scope
MainWindow_Nets.cpp net filter via setFilterWidth; step via the helper Yes In scope
MainWindow_Wiring.cpp S-meter TX Level face reads MICPEAK Yes In scope
MeterModel.h hasMicLevelMeter() / transmitLevelFaceValue() Yes In scope
SliceModel.h comment only (second caller of applyRecalledStepHz) Yes In scope
tests/*, tools/check_command_plane.py new test + ratchet counts Yes In scope

Everything in the diff is explained by the stated fix. No new capability field, settings key, protocol verb or CLI surface; CHANGELOG.md correctly untouched; no removed guard whose symptom can recur (the deleted sendCommand blocks are replaced by setters that emit the same text on a Flex, checked below). Nothing here reads as smuggled preference — every gate keys off an existing declaration (hasRadioSideDsp, hasAmCarrierLevel, hasCommandPlane()), not a family string.

Socket-test register: tests/reroute_dead_controls_test.cpp adds no socket. RadioConnection is constructed unopened and used only as a non-null pointer for hasCommandPlane(); there is no listen/bind/connectToHost/QTcpServer in the file. Nothing to notify.

CodeGuard: the six CG-PATH-001 hits are at RigctlProtocol.cpp:538 and MainWindow.cpp:9531/9643/9762-9764. None of those lines are in this diff (the hunks are at ~2072 and ~2050/5461). Pre-existing, not this PR's.

3. Blockers

None.

4. Nits (non-blocking)

  • The S-meter fix has two siblings in the same signal, one of them in the same file. MainWindow_Wiring.cpp:1569 feeds the VFO SmartMTR scale and MainWindow_DspApplets.cpp:243 feeds the Phone/CW Level gauge (PhoneCwApplet.cpp:1377, m_levelGauge->setValue(micLevel)); both still pass the raw micLevel and so still sit on the −50 floor on an HL2. transmitLevelFaceValue() is exactly the helper they want. Worth noting that SMeterWidget.cpp:307's own comment says the S-meter Level face "matches Phone/CW Level gauge" — after this PR, on an HL2, it no longer does. See inline.
  • The NR/ANF gate covers MIDI and the shortcut, not the four remote-control surfaces. RigctlProtocol.cpp:1949/1953, TciProtocol.cpp:1461/1485, AutomationServer.cpp:8166/8171 and SmartCatProtocol.cpp:1091/1241/1278 still call setNr/setAnf unguarded, so the phantom "NR on" the new test asserts against is still plantable on the same radio from rigctld or WSJT-X. The asymmetry is visible because the PR did reach into RigctlProtocol for the step. See inline.
  • "Byte-for-byte on a Flex" has one more exception than the test covers. SliceModel::setAgcMode/setAgcThreshold divert to the KiwiSDR external-receive branch (m_externalReceiveAudioReplacement) and emit no wire text; the hand-written slice set … agc_mode= did. The test fixture never sets that flag. See inline.
  • cmdSetTs accepts hz == 0 (only hz < 0 is rejected, and the ? list advertises "0 = any step accepted"). Without a command plane it now returns RPRT 0 while applyRecalledStepHz's hz <= 0 guard makes it a no-op and nothing is sent. Previously step=0 went to the wire. Trivial, but a silent success for an input the command documents.
  • In buildUI the AGC pair used to be sent before slice->setAudioGain(); it now lands after it, since recallBandStackReceiveDsp is called below the Volume block. The set of commands and their order among themselves is unchanged (which is what the new test pins), but "the same relative order as the hand-written wire text" is slightly stronger than what the diff does.

5. What I tried to break

  • "A Flex is unchanged." Attacked it four ways: (a) grepped every hasRadioSideDsp/hasAmCarrierLevel declaration to see whether a gate would refuse on Flex — FlexBackend.cpp:337/314 set both true; (b) read all six replaced setters for de-dup/clamp/early-return that would swallow a command the raw text sent — setNb/setNr have no equality guard, the level setters clamp to 0..100 and the recall already guards on difference, so the first-call output is identical; (c) checked the KiwiSDR branch, which is the one real divergence (reported above); (d) reconciled the check_command_plane.py ratchet against the deleted sends. Only (c) survived.
  • The tuneToNet ordering hazard. setFilterWidth's LSB mirroring depends on the slice's current mode, and setMode runs two lines earlier — if the model waited for the radio echo, a preset switching USB→LSB would mirror against the stale mode. SliceModel.cpp:185 sets m_mode synchronously before returning, so the mirror sees the new mode. Not a bug.
  • nr_cycle reachability. Walked all five branches: the new else if (!radioSideNoiseReductionAvailable()) sits after s->nrOn() and before the final else, so it is reachable exactly when NR is off and the radio has no radio-side NR, and it cannot shadow the NR2/NR4/DFNR steps above it.
  • Lifetime and receiver context on the rewritten connect. The new lambda captures this but keeps m_appletPanel->sMeterWidget() as the context object, so it is still disconnected with the widget; and setMicMeters only stores micPeak for a not-yet-implemented peak tick (SMeterWidget.cpp:308), so passing it twice is inert rather than double-counting.
  • Whether HL2/ANAN actually have an engine-side ANF the gate would wrongly refuse. The Hl2Backend.cpp:2493 comment about a WDSP notch looked like exactly that, but it is the manual/TNF notch (maxNotchFilters), and neither backend overrides setSliceAutoNotch/setSliceNoiseReduction. The refusal is correct.
  • Test quality. The Flex halves compare SliceModel::commandReady output against literal expected strings rather than re-deriving them, and the LSB divergence is pinned as a deliberate exception rather than hidden — this is a test that would go red if the setters stopped emitting. What I could not check: nothing here was built or executed. In particular I could not confirm that RadioConnection's default constructor is genuinely inert when slice->setAnf() reaches sendCmd through an unopened connection in testAnfStillReachesTheDemoCommandPlane, nor that the new test actually fails against unfixed code. Every finding above is reasoned from the head checkout, not reproduced at runtime.

6. Recommendation

Approve with nits. The core reroute is well-targeted and, unusually, its central claims survived direct checking against the capability declarations and backend overrides rather than resting on the PR body. Nothing here breaks a Flex or a user on current main. The two worth acting on before merge are the S-meter siblings (one line each, and the PR's own helper already exists for them) and a sentence in the body acknowledging that the NR/ANF gate is GUI-only so the remote-control surfaces are a known follow-up rather than an oversight. Neither needs to hold the merge if the maintainer would rather take them as a separate PR.


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

Comment thread src/gui/MainWindow_Wiring.cpp
Comment thread src/models/RadioModel.cpp
Comment thread src/models/RadioModel.cpp Outdated
Comment thread src/core/RigctlProtocol.cpp
@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (6 finding(s))

  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/core/RigctlProtocol.cpp /tmp/aetherclaude/pr-6049/src/core/RigctlProtocol.cpp:538
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6049/src/gui/MainWindow.cpp:9531
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6049/src/gui/MainWindow.cpp:9643
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6049/src/gui/MainWindow.cpp:9762
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6049/src/gui/MainWindow.cpp:9763
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6049/src/gui/MainWindow.cpp:9764

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


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

on8st and others added 4 commits September 30, 2026 17:48
The S-meter's Level face was moved to MeterModel::transmitLevelFaceValue()
in this PR, but three other consumers of micMetersChanged still passed the
raw MIC value, which never leaves its -50 floor on a radio that publishes
only TX:MICPEAK (HL2): the VFO SmartMTR TX scale, the Phone/CW Level gauge
(whose value the S-meter face is documented to match), and TCI tx_sensors'
mic field. All four now pass through the one helper, so they agree again.
A radio that defines MIC (Flex) is unchanged.

The test reads every micMetersChanged connection in src/ as text and
requires the helper in each, since three of them are MainWindow lambdas the
test cannot construct; a new consumer that forgets it fails too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MIDI and shortcut paths were gated in this PR, but CAT (rigctld
set_func NR/ANF, SmartSDR-CAT ZZNR/NR/NT), TCI rx_nr_enable/rx_anf_enable
and the automation bridge's `slice dsp` still reached SliceModel::setNr and
setAnf directly. On a radio with no radio-side NR/ANF (HL2, ANAN) that only
marked the model on: a phantom "NR on" that get_func, TCI and the VFO read
back while nothing ran.

Each surface now refuses ON in its own protocol's terms: rigctld answers
RIG_ENAVAIL (-11), SmartSDR-CAT `?;`, the bridge an error, and TCI, which
has no error reply, broadcasts the true state (false). OFF is already the
truth on such a radio and is answered as before, so a client that syncs
state by sending OFF sees no new error. RadioModel gains
radioSideAutoNotchAvailable(), the ANF half of the existing predicate
(the Demo radio keeps ANF through its command plane). A radio with
radio-side DSP, and a Flex, are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While KiwiSDR external receive audio replaces a slice, SliceModel's
setAgcMode/setAgcThreshold write the KiwiSDR AGC and send nothing. The
bookmark holds the RADIO's AGC (it is saved from agcMode() and
agcThreshold()), so routing the recall through those setters stopped the
radio AGC reaching a Flex and wrote a 0..100 radio threshold into the
KiwiSDR AGC's dB range.

In that state the recall now leaves the AGC alone and MainWindow sends the
same two `slice set` lines the hand-written recall always sent, so a Flex
receives byte-for-byte what it did before this PR. MainWindow.cpp's
command-plane baseline is 17 rather than 15; it was 21 before this PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hamlib's tuning step is a positive number of Hz. The trailing 0 that
`set_ts ?` lists is RIG_TS_ANY, the capability marker for "any step",
not a step, but set_ts accepted 0: without a command plane it answered
RPRT 0 and changed nothing, and with one it sent step=0 to the radio.
It now answers RIG_EINVAL (-1) on every radio, and the comment on the
list says what the 0 means.

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

on8st commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All four inline points were valid. Two of them showed that the PR body claimed more than the branch did. Each is fixed in its own commit on top of 0a88bdf4. No rebase and no force-push.

Corrections first.

  • "A Flex receives the same commands" was false for KiwiSDR external receive, and the new routing had a second defect there. You found the first half: setAgcMode/setAgcThreshold take the KiwiSDR branch and send nothing. The second half is that the bookmark stores the radio's AGC (it is saved from agcMode()/agcThreshold()). Under KiwiSDR the recall therefore wrote a 0..100 radio threshold into the KiwiSDR AGC, which uses a dB range. That is fixed below, and a Flex is now byte-identical in that state too.
  • Two numbers in the body are now wrong. The MainWindow.cpp command-plane baseline is 17, not 15. Total is 408, not 406; it was 413 before this PR. The two lines kept for the KiwiSDR case are the cause.
  • One Flex-visible change is deliberate: set_ts 0 now answers RPRT -1 on every radio. Before, a Flex was sent step=0.
  • Your ordering nit is correct, and I left the code as it is. Among the recalled commands the order is unchanged, but the AGC pair now goes out after setAudioGain(). So "the same relative order as the hand-written wire text" was stronger than the diff. The code comment now says "among themselves". I don't know of a Flex behaviour that depends on the order of AGC and audio gain. If you know of one, I'll move it.

MainWindow_Wiring.cpp:6393, the Level-face siblings (41313ab8). All micMetersChanged consumers now go through MeterModel::transmitLevelFaceValue(): the VFO SmartMTR (MainWindow_Wiring.cpp:1575) and the Phone/CW Level gauge (MainWindow_DspApplets.cpp:251). There was also a third one you didn't list: TCI tx_sensors' mic field (TciServer.cpp:255). So the SMeterWidget.cpp:307 comment is true again. Three of these consumers are MainWindow lambdas, so the test reads every &MeterModel::micMetersChanged connection under src/ as text and requires the helper in each. A future consumer that skips it will also fail.

RadioModel.cpp:4464, NR/ANF on the remote surfaces (9e81be62). All four surfaces now refuse ON where the radio has no radio-side NR/ANF, each in its own protocol's terms:

  • rigctld set_func NR|ANF 1 answers RPRT -11 (RIG_ENAVAIL) (RigctlProtocol.cpp:1955/1961)
  • SmartSDR-CAT ZZNR1/NR1|2/NT1 answer ?; (SmartCatProtocol.cpp:1092/1244/1283)
  • the bridge's slice dsp nr|anf on returns an error (AutomationServer.cpp:8168)
  • TCI has no error reply, so it broadcasts the true state, rx_nr_enable:<trx>,false; (TciProtocol.cpp:1464/1493)

OFF is already true on such a radio, so it is answered as before. That way a client that syncs state by sending OFF gets no new error. RadioModel::radioSideAutoNotchAvailable() (RadioModel.cpp:4454) is the ANF half of the existing predicate, so the Demo radio keeps ANF. The test covers each surface: the refusal, no phantom "on", no ON reaching the seam, OFF still accepted. It also checks that an Icom-shaped radio still reaches setSliceNoiseReduction/setSliceAutoNotch.

RadioModel.cpp:4507, Flex byte-for-byte under KiwiSDR (02dd0cc8). With external receive active, recallBandStackReceiveDsp() leaves the AGC alone (RadioModel.cpp:4518). MainWindow sends the same two slice set … agc_mode= / agc_threshold= lines the hand-written recall always sent (MainWindow.cpp:5474). They go before the Volume block, as they did before this PR. I kept the lines in MainWindow.cpp rather than moving them into RadioModel.cpp, so that no file's baseline grows. The test adds your third case: KiwiSDR active on a Flex-shaped slice. It checks that the KiwiSDR AGC is untouched, that no AGC intent is sent, that the NB/NR text is byte-for-byte as before, and that MainWindow still carries the two lines. On a radio with no command plane, this KiwiSDR case behaves as on main (the text is dropped).

RigctlProtocol.cpp:2079, set_ts 0 (50a7477c). In Hamlib a tuning step is a positive number of Hz. The trailing 0 that set_ts ? lists is RIG_TS_ANY, the capability marker meaning "any step", and is not itself a step. set_ts 0 now answers RPRT -1 (RIG_EINVAL) and leaves the step unchanged (RigctlProtocol.cpp:2086). The ? output is unchanged, and its comment now explains what the 0 means.

CodeGuard. I agree with your reading. The six CG-PATH-001 hits (RigctlProtocol.cpp:538, MainWindow.cpp:9531/9643/9762-9764 at the old head) are all outside this PR's hunks. They are a comment, two UI string literals containing \n\n, and three QDir::cdUp() calls on the application's own bundle path. None of them takes a path from input. They are pre-existing and none is this PR's.

Issue fit. There is no Fixes line because there is no single issue this closes. It is a slice of #5262 M4, the tracking issue, which must stay open. The work comes from a per-radio inventory of the UI: we traced 1,010 controls, and 118 of them are silent on an HL2. This PR is one group from that inventory: controls the radio can already serve through another path. Part B of the same group is #6021. On #5919, @rfoust said to go ahead (link), and whichever PR lands second adjusts the wire-text assertions. I'd welcome a Refs #5262 on the PR if a maintainer wants it linked.

Test plan update. Before the fix, the new checks fail against the unfixed source (a throwaway local commit with only the tests applied): reroute_dead_controls_test fails, with each thread's checks red: set_ts 0, all four remote surfaces, the level-face consumers, and the KiwiSDR AGC. Full ctest at 50a7477c: 555/555 passed (Ninja, Qt 6.8.3, RelWithDebInfo, macOS arm64). The static checks pass at 50a7477c: engine boundary, touchpoint manifest, seam probe table, test registration, capability records, command plane (408/408), CI test gate, network timeouts, theme seed and shader dialects.

73, ON8ST

@ten9876

ten9876 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

We're updating this branch against main and landing it (maintainer pass); please don't push to it meanwhile. New commits go on top, with no rebase or force-push.

@ten9876 ten9876 self-assigned this Oct 3, 2026
ten9876 and others added 6 commits October 2, 2026 18:42
Conflicts: RadioModel.h (adjacent declarations, both kept) and
tests/tests.cmake (adjacent test blocks; main retired
wdsp_nb_hold_invariant_test in aethersdr#6075, so its block stays out; both
AETHER_SETTINGS_CONSUMERS entries kept).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
…le VIII.

Main's aethersdr#5904 moved AGC and filter dispatch from SliceModel::commandReady
to backend receive intents, which FlexBackend encodes as the same text
through its slice sink. The Flex cases of reroute_dead_controls_test now
read that sink as well as commandReady, so the byte-for-byte comparison
still covers every recalled command.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
Comments state the current invariant only; the history they carried is in
the PR body and the earlier commits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
A momentary MIDI button sends 0 on release; refusing it announced the
control as unsupported a second time. OFF is already true there, as the
CAT, TCI and bridge surfaces already answer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
Conflict: tests/tests.cmake, adjacent test blocks (aethersdr#6013's two tests and
reroute_dead_controls_test); all kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g

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

Maintainer landing pass at 10079f4a7 (on8st's six commits plus main merged in). Approved.

Review findings, each fixed by @on8st and re-proved here

  • Level-face siblings: 41313ab87
  • Remote NR/ANF surfaces: 9e81be623
  • KiwiSDR AGC on a Flex: 02dd0cc87
  • set_ts 0: 50a7477c1

All four threads are answered with evidence and resolved.

Landing-pass commits

  • bfe826ddd / 94118f1a4 / 10079f4a7: merges of main. Every conflict was adjacent: RadioModel.h declarations (both kept) and tests.cmake blocks (#6075 retired wdsp_nb_hold_invariant_test, so its block stays out; #6013's two tests kept). #6013 also touches RigctlProtocol.cpp and MainWindow.cpp, in different functions (RF level), and merged cleanly.
  • c88e3fa19: main's #5904 moved AGC and filter dispatch onto backend receive intents. The test's Flex cases now read a real FlexBackend slice sink plus commandReady, so the byte-for-byte Flex wire text is still pinned.
  • c489c8a10: comments condensed to AGENTS.md § Comments.
  • 86a8f0f80: radio NR/ANF OFF is accepted where the radio has none. Found by the automated review pass: a momentary MIDI button's release raised a second notice.

Maintainer decisions (Jeremy, 2026-10-02)

  • set_ts 0 returns RPRT -1 on every radio, Flex included. Third-party CAT clients see this change on a Flex, which was sent step=0 before. FlexLib Slice.cs:1553-1572 never sends a step below 1.
  • The FlexBackend-sink test re-pin is the accepted shape.
  • Accepted as well: the LSB net-preset mirroring before filt on a Flex, and the Demo NR refusal (nr_cycle goes straight to NR2 there).

Verified empirically (Linux, RelWithDebInfo)

  • Full ctest, socket-owning tests excluded: 551 of 552 pass, including #6013's rigctl_rf_gain_pan_test and receiver_slot_count_test. The one failure is connection_panel_size_test, a known local failure (#5811).
  • reroute_dead_controls_test goes red under nine mutations of the no-command-plane path.
  • Static gates --strict pass. Command-plane baseline lowered only (406→401), with no new gui→engine include.
  • Demo simulator, driven offscreen: the RX applet STEP sends slice set 0 step=250 through the command plane as before. ANF is accepted and NR is refused.

Reasoned, not run

  • A Flex is unchanged on the NR/ANF/AM gates because FlexBackend declares hasRadioSideDsp and hasAmCarrierLevel; the unit test covers this with a Flex-shaped fixture.
  • Not tested on live hardware.

Green freshness: CI created 2026-10-03T02:37:25Z on this head. Main tip ad46155dc (02:23:24Z) is this head's merge parent, so there is no drift.

@ten9876
ten9876 merged commit 5ef09b0 into aethersdr:main Oct 3, 2026
5 checks passed
ten9876 added a commit to on8st/AetherSDR that referenced this pull request Oct 3, 2026
Brings in aethersdr#6049 (RadioModel/SliceModel reroutes and an adjacent
tests/tests.cmake registration); no conflict. Its band-stack AGC recall
reaches requestSliceAgc, which Hl2Backend passes to the base for every
field but OffLevel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
ten9876 added a commit that referenced this pull request Oct 3, 2026
…6050)

## Summary

On a Hermes-Lite 2, two receive controls had no route to the DSP that
runs on this host:

- **The AGC-T slider with AGC Off** moved `SliceModel::agcOffLevel`, and
the HL2 backend ignored the request, so every HL2 ran AGC Off at WDSP's
channel default of 10 dB (`WdspChannel::Config::agcFixedGainDb`)
whatever the slider said.
- **The CW audio peaking filter (APF)** was Flex wire text only (`apf=`
/ `apf_level=`). That is why HL2 declared `hasAudioPeakingFilter =
false` and the APF row stayed hidden, although WDSP builds a
peaking-filter stage into every receive channel.

## Seam

- **AGC-off level: main's typed route.** `SliceModel::setAgcOffLevel`
emits `receiveAgcRequested(SliceAgcRequest::Field::OffLevel)` →
`IRadioBackend::requestSliceAgc` (#5907). `Hl2Backend` now overrides
`requestSliceAgc`: it handles `Field::OffLevel` itself and passes every
other field to `IRadioBackend::requestSliceAgc` (the paired
`setSliceAgc`). No new verb.
- **APF: one new seam verb, interim.**
`IRadioBackend::setSliceApf(sliceId, on, level)` is a defaulted no-op.
`SliceModel` emits `apfCommandIssued(on, level)` from the operator
setters only, never from status application (Principle II), and
`RadioModel::wireBackendReceiverState()` routes it next to the squelch
route. **It folds into #5919's `SliceDspRequest::Feature::Apf` when
#5919 lands.**
- **Flex is unchanged.** It overrides neither verb, its
`apf=`/`apf_level=` text from SliceModel is byte-identical, and its
`agc_off_level=` text comes from `FlexBackend::requestSliceAgc` as on
main. Icom, RTL, Sim and ANAN keep the base no-op and still declare
`hasAudioPeakingFilter = false`.
- The `agcOffLevel` branch in `SliceModel::applyChanges()` gets the same
unchanged-value guard as the threshold beside it. HL2 now echoes the off
level on every slice-state publish, and `AgcCalibrationDialog` wires
`agcOffLevelChanged` to the calibrator.

**Maintainer decisions (KK7GWY, 2026-10-02):** (1) the AGC-off level
uses main's `requestSliceAgc` route, not a parallel verb; (2)
`setSliceApf` is accepted as an interim verb until #5919's request type
replaces it.

## HL2

- **AGC-off level → `SetRXAAGCFixed`**, via a new
`WdspChannel::setAgcFixedGain()`. The mapping is `gain_dB = 10 +
0.6·(level − 10)`, so the range is 4..64 dB. **The default level 10
gives exactly 10.0 dB**, so AGC Off sounds the same until the operator
moves the slider. The slope matches the AGC-T threshold's 0.6 dB/unit.
WDSP applies the fixed gain only in AGC mode 0, so the value is pushed
in every mode and takes effect when AGC is switched off.
- **APF → WDSP SPCW** (`apfshadow`:
`SetRXASPCWSelection/Run/Freq/Bandwidth/Gain`, declared in
`aether_wdsp.h`), via `WdspChannel::setApf()`:
- The filter is centred on the CW pitch in audio Hz, and it follows a
pitch change on every receiver, whether or not it is in CW at the time.
- Level 0..100 sets the bandwidth, because the control is labelled "APF
bandwidth" / "Higher values narrow the peak". The map is 200 Hz → 50 Hz,
geometric. The default of 50 gives **100 Hz at gain 2.0, which is
RXA.c's own construction design**.
- The stage runs only in CWL/CWU. The request is kept through a trip to
SSB and comes back with CW.
- `open()` selects the double-pole explicitly. An unchanged `setApf()`
returns before the control handshake and the FFTW planner lock.
- While it runs, both audio outputs derive from I (WDSP's mode-2
double-pole copies I into Q).
- `hasAudioPeakingFilter` is now `true` for HL2. The caps map row says
the host runs it.
- Both settings are held per receiver outside `Config` and applied again
on channel rebuild, reconnect and new panadapter, the same way the noise
blanker is.
- Both appear in `dsp.chains` as what WDSP last **accepted** (`apfRun`,
`apfCenterHz`, `apfBandwidthHz`, `apfGain`, `agcFixedGainDb`).
- **Not persisted, on purpose:** like the host noise blanker. After a
reconnect the off level is 10 again and the APF is off.

## Tests

Both new tests are socket-free and registered in `tests/tests.cmake`.

- **`slice_apf_agc_off_seam_test`** (a recording backend installed
through `RadioModel`'s production receiver bindings):
  - Flex's APF wire text is byte-identical.
- The operator setters emit the APF intent with enable and level
together.
- `RadioModel` routes the APF to `setSliceApf` and the off level to
`requestSliceAgc(Field::OffLevel)` for the right slice, including
through the AGC-T knob and the AGC-T calibrator's sweep and restore.
- Status application never re-emits a command, and an echo at an
unchanged off level notifies nothing.
- The base verbs emit no signal, and the base drops `OffLevel` rather
than calling `setSliceAgc`.
- **`hl2_apf_agc_off_test`** (a real `Hl2RxDsp` and WDSP channel,
measured audio):
  1. the level maps;
2. refusals, and an unchanged `setApf()` that does not wait on the
planner lock;
3. AGC Off: 50 units is 30 dB of audio, and with AGC on the off level
changes nothing;
4. APF in CWU lifts the pitch over a tone 200 Hz away by more than 10
dB, outside CW it is held and inert, and CWL is centred on the pitch;
  5. both survive a rebuild;
6. through `Hl2Backend`: `setSliceApf` / `requestSliceAgc(OffLevel)` →
WDSP → published slice state and `dsp.chains`. A pitch set outside CW is
the centre on entering CW, and a `Mode` request still reaches the
chain's AGC and leaves the off level alone.
- **Mutation-checked** (each goes red, and green again when restored):
  - sending the APF only to receivers already in CW;
  - `Hl2Backend::requestSliceAgc` not handling `OffLevel`;
  - not delegating other fields to the base.

**Results at the head (Linux, RelWithDebInfo):**
- Full build. Non-socket ctest 549/550 (including #6049's
`reroute_dead_controls_test`); the one failure is
`connection_panel_size_test`, which fails locally (#5811).
- Static gates: engine boundary, touchpoint manifest, capability records
(frozen count unchanged), command plane, test registration and the CI
test gate all pass, with findings identical to main's.
- on8st's earlier run: 556/556 on macOS.

### Hardware evidence (receive only)

Hermes-Lite 2 (gateware 74) with the antenna switch on the dummy load,
read through the automation bridge with TX blocked. This run was at an
earlier head, when the AGC-off level still used its own verb. The
HL2/WDSP half is the same code, and the route change is covered by the
tests above.

| # | Condition | Expected | Observed | Result |
|---|---|---|---|---|
| A1 | AGC Off, off level 10 | fixed gain 10 dB | 10 dB | PASS |
| A2 | AGC Off, off level 40 | 28 dB | 28 dB | PASS |
| A3 | AGC Off, off level 100 | 64 dB | 64 dB | PASS |
| P1–P3 | APF not requested (USB / CW / CWL) | not running | `apfRun`
false | PASS |
| P4 | APF on, CW | running at CW pitch | `apfRun` true, 600 Hz (pitch
600), 100 Hz, gain 2 | PASS |
| P5 | APF on, CWL | running at CW pitch | `apfRun` true, 600 Hz, 100
Hz, gain 2 | PASS |
| P6 | APF requested, USB | held, not running | `apfRun` false, model
still on | PASS |
| P7 | back to CW | running again | `apfRun` true, 600 Hz | PASS |

## Not covered

- No listening check of the APF on hardware. The audible effect is
covered by the offline measurements (sections 3 and 4).
- The APF level slider was not swept on hardware.
- No transmit path is touched, and nothing was transmitted.

## Update against main

Merged `main` into this branch (no rebase). The conflicts were adjacent
additions next to #5982's squelch, #5980's pan averaging and #5981's
filter length in `Hl2Backend`, `Hl2RxDsp` and `WdspChannel`, plus test
registrations; both sides were kept. `SliceModel::setAgcOffLevel` took
main's version, and the following commit moved the HL2 route onto it
(decision 1 above). Two later merges brought in #6013 and #6049 cleanly;
#6049's band-stack AGC recall reaches `requestSliceAgc`, which
`Hl2Backend` passes to the base for every field but `OffLevel`.

Refs #5262, #5917.

73, ON8ST

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
tropo1234 added a commit to tropo1234/AetherSDR that referenced this pull request Oct 3, 2026
…ures.

Neither file existed at this branch's previous base, so neither conflicted and
neither appeared in the rebase: hl2_rxdsp_adaptive_taps_test arrived with aethersdr#5981
and reroute_dead_controls_test with aethersdr#6049, and both call signatures this branch
widens.

hl2_rxdsp_adaptive_taps_test: four buildChannel(cfg, false, 0) calls become
(cfg, NoiseBlanker::Off, 0, NoiseBlankerFill::Zero). These cases assert filter
tap lengths across a rebuild and never engage a blanker, so off with a zero
fill is what they already had.

reroute_dead_controls_test: the fake backend's setSliceNoiseBlanker override
takes the kind and fill, and records kind != Off as the bool its rows already
assert — the same reading a radio with one blanker gives the request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 3, 2026
…ciple VI. (#6104)

## Summary

On HL2 and Icom, a TUNE power change made while TUNE is keyed now
reaches the carrier. Before this, the value waited for the next
key-down, and the operator saw a "dropped" notice for a control that
looks live. This PR also fixes two TX-power defects in the same paths:

- **HL2 transmit tail.** At TUNE release the RF drive was restored right
after MOX-off, but the HL2 keeps radiating for its FIFO drain plus PTT
hang after the MOX-off frame. The restore landed under the
still-draining full-scale tune tone. The drive restore and the receive
filter switch now wait out that tail. (Found by ON8ST.)
- **Icom restore.** An RF power change made during TUNE was written over
the tune drive and then lost at the unkey.

## What changed

**Seam and capability**
- `IRadioBackend::setTunePower(int)` is a new verb with a default no-op,
next to `setTxPower`. It never keys. A backend that generates its own
carrier re-applies drive to a carrier in progress and otherwise does
nothing; the next `setTune()` carries the value. Owner-thread contract
as for `setTxPower`.
- `RadioCapabilities::TransmitDriveControl::tunePowerAppliesLive`
defaults to `false`. It is a field inside the existing record, not a
top-level bool, so `FROZEN_BOOL_COUNT` stays at 71
(`check_capability_records.py --strict` reports 71 on both base and
head). The contract text in `IRadioBackend.h` and
`docs/architecture/radio-capabilities-map.md` are updated to match.

**Models**
- New signal `TransmitModel::tunePowerCommandIssued(int)`. Only
`setTunePower()` emits it, after `commandReady`, mirroring
`rfPowerCommandIssued`. `applyChanges()` (status decode) never emits it.
- `RadioModel` connects to it in the constructor's model-lifetime block,
next to `rfPowerCommandIssued` (#4599). It forwards a change only when
all of these hold:
  - `isTuning()` is true
  - a live `TxActivity::Tune` contribution exists
  - the backend declares `tunePowerAppliesLive`
- The connect checks `tunePowerAppliesLive` before calling
`setTunePower`. The behaviour is the same for every real backend,
because the base verb is a no-op. The check makes the contract explicit:
the field promises the verb is real, and a backend that doesn't declare
it is never handed the call.
- Both the forward and the drop-notice gate use one predicate,
`RadioModel::tuneCarrierLive()`: `isTuning()` AND a live
`TxActivity::Tune`. The gate is `routed = canTransmit &&
transmitDriveControl && (!isTuning() || (tuneCarrierLive() &&
tunePowerAppliesLive))`. While tuning, a value that is not forwarded
live always keeps its notice: a backend without the field, or a tune
state decoded off the radio with no TUNE this client admitted.
- Every `TransmitDriveControl` is built with the field stated
explicitly, as the ADDING A FIELD rule requires: `true` on HL2 and Icom,
`false` on Flex (whose tune power travels as command-plane text) and
ANAN. Sim and RTL declare no record.

**HL2**
- `Hl2Backend::setTunePower` writes only the drive register, through
`applyDrive()`, so the clamp and the transmit gate still apply. It
doesn't touch `m_rfPowerPercent`, band memory or
`notifyOperatingStateChanged`, and it doesn't route through `setTxPower`
or re-call `setTune(true)`.
- It acts only while `m_tuning` is true. It also acts only while the
TUNE operation, stored as `m_tuneOperation` in `setTune(true)`, still
permits dispatch. That optional defence is included: a TUNE whose
admission was revoked or emergency-stopped takes no drive change. Every
unkey and the connection reset clear `m_tuneOperation`, so it never
outlives the carrier.
- `tunePowerAppliesLive = true` in the HL2 capabilities.

**Icom**
- `IcomCivBackend::setTunePower` writes CI-V `14 0A` (the tune drive)
while tuning and does nothing otherwise. It also requires the same
admission `setTune(true)` checked: the TUNE operation stored there, and
the backend's transmit context. The operation is cleared at every unkey
and session reset. A refusal is logged on `aether.icom.tx`, because the
verb returns nothing to `RadioModel`. `tunePowerAppliesLive = true` in
the Icom capabilities.
- **Restore fix.** `setTxPower` during TUNE now records the value in
`m_preTuneTxPowerPercent`, which the unkey (and the disconnect emergency
path) writes back. It no longer writes over the tune drive.
- The raw write is factored into `writeTxPowerLevel()`. The TUNE
re-request branch in `setTune(true)` and the `tx.power` controls scrub
both use it, so neither can stamp the tune drive into the restore slot.
- **Readback during TUNE.** The CI-V read that confirms the tune-drive
write (`14 0A`) is no longer published as `TransmitDelta::rfPower` while
tuning. The periodic poll already skipped that read during TUNE. Without
this, `TransmitModel::rfPower()` showed the tune level. A relative
control (the power wheel, or a CAT/TCI read-modify-write) would then
step from that level, and the result would become the value the unkey
restores. Example: 100% RF and 10% tune, then one wheel step, left the
radio at 11%. *Visible effect:* on Icom the RF power slider no longer
jumps to the tune level while TUNE is keyed. It keeps the operator's RF
power, matching Flex. (Found by code review of this branch.)

**Evidence for the readback leak, and its limits.** This comes from the
code path and a scripted-reply unit test. It is **not
hardware-verified**: no Icom was on the bench.
- Icom has no tune-carrier command, so TUNE writes the tune level into
`14 0A`.
- `sendUserCommand()` queues a confirming read of that level 60 ms later
(`queueRead(..., now + 60)`).
- The `kRfPower` reply handler published it as `TransmitDelta::rfPower`.
- `icom_backend_seam_test` scripts that CI-V reply. It does not observe
a real radio's answer.

**Still wanted:** a CI-V capture from a real Icom of TUNE plus one
RF-power wheel step. It should show the confirming `14 0A` read during
TUNE and confirm that the restore after the unkey is the operator's RF
power.

## The HL2 transmit tail (found by ON8ST)

ON8ST showed from the gateware RTL that the HL2 keeps transmitting after
the MOX-off frame:
- MOX travels with the samples, not as a register.
`dsopenhpsdr1.v:351-352` tags each sample with `ds_cmd_ptt`.
- In `PTTTX`, `radio.v` drains the keyed samples already queued in the
FIFO (`tx_buffer_latency`, 20 ms default, `radio.v:934`). AetherSDR
never writes 0x17, so the default applies.
- It then holds the last sample for `ptt_hang_time` (12 ms default,
`radio.v:935`, 1036-1047). `tx_on` stays high until the state machine
reaches `NOTX` (`radio.v:1002`).
- Drive is not buffered. `ad9866ctrl.v:134-140` loads the TX gain as
soon as the command arrives.

So restoring RF drive one frame after MOX-off still landed under roughly
20 + 12 ms of full-scale tune carrier. ON8ST's unit (gateware 74.2)
unkeyed itself 40-54 ms after the host stopped sending keyed frames. The
same applies to the receive filter switch at unkey.

**Why a timer, not `tx_on`.** `tx_on` is reported only in the discovery
/ alternate-port reply: `resp_control` (`control.v:899`) feeds
`usopenhpsdr1.v:292` (`6'h20`). The EP6 stream's responses carry
`ptt_resp = cw_on | ext_ptt` instead (`control.v:456`, 472-475), which a
host MOX never raises. `MetisProtocol.cpp:546` is in
`parseDiscoveryReply()`. Reading `tx_on` mid-stream would mean polling
the discovery port during TX, which the stream-free telemetry design
avoids because that reply preempts EP6 (`Hl2TelemetryCadence.h`).
Gateware without `EXTENDED_RESP` also sends zeros there, so a "`tx_on`
low" trigger would fail open.

**Fix (`Hl2Backend`).**
- Every key-up arms a transmit tail of `kTxTailHoldMs` = 70 ms, after
the MOX-off is queued.
- 70 ms is never below the existing RX unkey hold (a `static_assert`
pins it). It covers the gateware's 20 + 12 ms default tail and ON8ST's
40-54 ms measurement. The RX-hold measurement of the same tail on one
HL2 was 51.66-66.15 ms, n = 11.
- While the tail is pending, `applyBandFilter()` keeps the transmit
filter byte, and `setTxPower()` records an owed RF drive without writing
it.
- When the tail ends, the receive filter and the owed RF drive are
applied.
- A re-key cancels the tail, so a restore is never applied over a new
carrier, and the drive stays owed to the next unkey's tail.
- A disconnect or connection reset drops the tail.

Icom is unchanged: its PTT-off is queued before the power restore, and
the radio owns its own timing.

**Maintainer decisions (Jeremy):**
1. A fixed 70 ms timer ends the tail. There is no `tx_on` poll, and 0x17
is not written.
2. A voice key inside the tail of a TUNE release keeps the tune drive
for that transmission. The restore is cancelled, never applied over the
new carrier, and RF drive returns at the next unkey's tail.
3. The transmit filter byte is held for the tail on every unkey,
including CW break-in elements.

## Out of scope (unchanged)

- No changes to TCI `tune_drive` or MIDI power limits. Note that TCI
`tune_drive` and the automation `transmit tunepower` verb go through
`TransmitModel::setTunePower`, so on HL2/Icom they now also apply live
while TUNE is keyed, as they already did on Flex.
- No slider dimming or tooltip UX for backends that don't declare the
field.
- No tune-power ceiling tied to RF power. Flex keeps them independent,
and so does this PR.
- Drag flood is accepted as is, matching RF power: no coalescing and no
debounce.

## Tests (all socket-free; nothing keys real hardware)

- `transmit_seam_drop_notice_test`:
- Flag on: `setTunePower(30)` arrives exactly once, with no re-key and
no drop notice.
  - Flag off: no call, and the notice stays.
  - Not tuning: no call, and the next `startTune` carries the value.
- A decoded tune power while keyed (`applyChanges`, a different value)
sends no call.
  - After `stopTune`, no call.
- A tune state decoded off the radio (`applyChanges`, `tune=true`) with
no live Tune activity: no call, and the drop notice stands. This pins
the Tune-activity check on its own, in both the forward and the gate.
- `hl2_tx_gate_test` reads the 0x09 bank bytes off the real EP2 packet
builder.

  | Step | 0x09 bank result |
  |---|---|
  | RF power 40% | drive 102 |
  | TUNE at 10% | drive 25 |
  | live 60% | drive 153, PA on |
  | 150% | 255 |
  | −5% | 0, PA off |
  | release | 102 restored |
  | 80% after release | no bank, no keyed frame |
  | gate closed while tuning | 0, PA off |
  | TUNE operation emergency-stopped | no bank |
  | release | stored TUNE operation dropped |
  | stopped client | no bank |

The same file has the transmit-tail tests, with the hold parked so each
test ends the tail itself:
- The unkey's queued calls post no RF-drive restore; walked packet by
packet with an event filter on `MetisClient`.
  - The tail ending restores RF drive.
- A re-key inside the tail cancels it; no restore under the new carrier,
and the next unkey's tail pays it.
  - TUNE re-keyed inside the tail keeps its tune drive.
  - RF power set during the tail waits for it.
  - A live tune drive with no key-time tune power is still restored.
  - A disconnect drops the tail.
- On a TX-only filter board, the TX byte holds through the tail, and
through a retune inside it, until the tail ends.
  - The real timer ends a 20 ms tail.
- `icom_backend_seam_test` checks the CI-V `14 0A` writes:
  - The capability is declared.
  - Not tuning: no write.
  - While tuning: the live 25% is written, with no re-key.
  - RF 50% during TUNE: no write, and it is held for restore.
  - A TUNE re-request writes 15% and keeps 50% held.
- A `tx.power` scrub during TUNE re-asserts the live 15% and keeps 50%
held.
  - Release writes 50%.
  - After release: no write, and the stored TUNE operation is dropped.
  - A TUNE whose operation was emergency-stopped: no write.
- A lapsed transmit context with the operation still live: no write, and
one warning logged.
- An RF power readback of the tune level during TUNE publishes no
`rfPower`. The premise is checked first: the same readback outside TUNE
does publish.

### Mutation results (each mutation made its test fail, then the file
was restored and verified by sha256 against HEAD)

| Mutation | Result |
|---|---|
| RadioModel connect deleted | RED |
| gate reverted to `!tuneKeyed` | RED |
| flag ignored (gate + connect) | RED |
| wired from `tunePowerChanged` instead | RED |
| `isTuning`/Tune-activity check removed | RED |
| Tune-activity check alone removed | RED |
| HL2 `m_tuning` guard removed | RED |
| HL2 implemented via `setTxPower` | RED |
| HL2 `applyDrive` bypassed | RED |
| HL2 tune-operation check removed | RED |
| HL2 flag not declared | RED |
| HL2 tail deferral removed (restore at unkey) | RED |
| HL2 re-key cancel removed | RED |
| HL2 `setTxPower` tail check removed | RED |
| HL2 filter predicate ignores the tail | RED |
| HL2 disconnect cancel removed | RED |
| HL2 `setTunePower` not marking drive owed | RED |
| Icom restore fix reverted | RED |
| Icom `setTunePower` guard removed | RED |
| Icom `setTunePower` no-op | RED |
| Icom TUNE re-request via `setTxPower` | RED |
| Icom scrub via `setTxPower` | RED |
| Icom flag not declared | RED |
| Icom tune-drive readback published during TUNE | RED |
| Icom tune-operation check removed | RED |
| Icom tune operation not stored at `setTune(true)` | RED |
| Icom tune operation not cleared at unkey | RED |
| Icom transmit-context check removed | RED |
| Icom refusal warning removed | RED |
| Drop-notice gate ignores `tuneCarrierLive` | RED |
| Forward uses `isTuning()` alone | RED |
| HL2 tune operation not cleared at unkey | RED |

## Merge with main

The branch was cut from `2d5712946`. It now carries signed merges of
`origin/main` up to `c7513e125` (27 commits, including #6062, #6005,
#5979, #5982, #5976, #6011, #5980, #6019, #5981, #6012 and #6050 on HL2,
and #6049 in `RadioModel`). Every merge was clean. The files both sides
touched (`Hl2Backend.cpp/.h`, `IRadioBackend.h`, `RadioModel.cpp`,
`radio-capabilities-map.md`) needed no manual resolution, and none of
main's changes in them touch keying, drive or TUNE. Everything below was
re-run on the current head.

## Static gates (merge base `c7513e125` vs merged head, findings diffed
per file)

`check_engine_boundary.py --strict`, `gen_touchpoint_manifest.py
--check`, `check_capability_records.py --strict` and
`check_command_plane.py --strict` all produce identical findings on base
and head once line numbers are normalised:

- Engine boundary: 102 tracked findings, 0 blocking.
- Touchpoint manifest: up to date.
- Capability records: 71 bools.
- Command plane: 401 commands across 32 files.

No ratchet moved, and no new wire text was added.
`gen_seam_probe_table.py --check` is clean (51 signals): this PR adds a
seam verb but no seam signal.

## Not verified

- No live-radio test on HL2 or Icom; per the TX rules, nothing was
keyed. The Icom path, including the readback leak and its guard, is
proven only against the backend's CI-V output and scripted CI-V replies.
A real-Icom CI-V capture of TUNE plus one RF-power wheel step is still
wanted.
- Icom's RF-power readback is suppressed during TUNE. CI-V cannot tell
the confirming read of the tune drive from a front-panel change, so an
RF-power knob turned on the radio during TUNE is not seen and the unkey
restores the pre-TUNE value (the code comment says so).
- The HL2 transmit-tail fix has no hardware test. Its 70 ms rests on the
gateware source defaults and on bench measurements made before it
existed: ON8ST's 40-54 ms unkey, and the RX unkey-hold measurement of
the same tail. Nothing here observed `tx_on` or RF output with this
change.
- Full default ctest at -j8 on the merged head, with every test whose
source uses a socket API excluded (523 of 620 run; the pty-backed
`thumbdv_queue_test` and four new socket tests from main are excluded
too): all pass except `connection_panel_size_test`, which is a known
local failure (#5811). `transmit_seam_drop_notice_test`,
`hl2_tx_gate_test` and `icom_backend_seam_test` pass.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 3, 2026
…in through the seam gate. Principle VIII. (#6111)

## Summary

On a radio with no Flex command plane, six transmit controls reach the
radio through the backend seam and still raise the one-shot **"dropping
transmit set …"** notice: **VOX** (enable, level), the **speech
processor**, the **SSB monitor**, **CW speed** and **CW break-in**. On
an Icom all of them apply. The drop-notice gate from #6015,
`transmitCommandDeliveredThroughSeam()`, did not route them, because no
capability said their setters do anything.

Fixes #6086.

## What changes

**CW speed and break-in** (first commit). `cw wpm N` and `cw break_in N`
are bare `cw …` text, not `transmit set` keys, so they get prefix
branches next to `cw pitch`. They are routed under
`hasRadioSideCwKeyer`, the same capability their seam connections
already use. `cw break_in_delay` has no seam setter and keeps its
notice.

**VOX, monitor, speech processor** (second commit). Three new optional
capability records:

| record | replaces | present means | Flex | Icom (transmitting) | HL2 /
ANAN / Sim / RTL |
|---|---|---|---|---|---|
| `voxControl { hasDelay }` | `hasVoxDelay` | the radio has its own VOX.
Without a command plane, `setVox` applies enable and level; `hasDelay`
means the hang time is applied too | ✅ `hasDelay` | ✅ no delay | absent
|
| `speechProcessorControl { levelMaximum, label }` |
`speechProcessorLevelMaximum`, `speechProcessorLabel` | the radio has
its own processor (`setSpeechProcessor`) | ✅ PROC 0..2 | ✅ from the
profile: IC-9700 COMP 0..100, others PROC 0..2 | absent |
| `txMonitorControl {}` | — | the radio has its own SSB monitor
(`setTxMonitor`: MON and its gain) | ✅ | ✅ | absent |

When `speechProcessorControl` is absent, the P/CW face shows PROC 0..2,
the same as when disconnected. The UI is unchanged on every backend. The
unidentified receive-only Icom fallback declares none of the three.

How the gate treats each key without a command plane:

| key | treated as delivered when |
|---|---|
| `vox_enable`, `vox_level` | `voxControl` is present |
| `vox_delay` | `voxControl->hasDelay`. False on Icom (`setVox` ignores
the delay), so the notice there is correct and stays |
| `mon`, `mon_gain_sb` | `txMonitorControl` is present |
| `speech_processor_enable`, `speech_processor_level` |
`speechProcessorControl` is present, **or** `hostModulates &&
canTransmit` (the HL2's ClientComp,
`MainWindow::applySpeechProcessorToClientComp`). ANAN sets
`hostModulates` but not `canTransmit`, so nothing serves its PROC and
the notice stands |
| `mon_gain_cw`, `mon_pan_cw`, `cw break_in_delay` | never: no seam
setter |

The three seam connections are now gated on the same records. They used
to check `m_backend` (VOX), `!usesFlexCommandPlane()` (monitor) and
`!m_flexBackend` (PROC).

**PROC on the HL2:** @on8st, your PROC-on-HL2 work should use this gate.
A host-side processor is covered by the `hostModulates && canTransmit`
arm. `speechProcessorControl` is only for a radio's own processor behind
`setSpeechProcessor`.

**Ratchet:** `hasVoxDelay` moves into the VOX record, so the bool count
drops from 71 to 70. `FROZEN_BOOL_COUNT` is lowered to 70 in the same
commit. The checker prints `capability-records: 70 boolean(s), at the
frozen 70 — ok (shrink only)`. No ratchet grows.

## Flex is unchanged

Flex has a command plane, so the gate is never consulted for it, and
`TransmitModel`'s wire text is untouched. Flex declares all three
records. That means its `setVox`, `setTxMonitor` and
`setSpeechProcessor` are now called; the last two were not called
before. `FlexBackend` doesn't override them, so they are the base
no-ops. `flexSeamSettersWriteNothing` pins two things. First, with all
three of FlexBackend's command sinks recording, those calls write
nothing. Second, the `transmit set vox_enable=1 / vox_delay=30 / mon=1 /
speech_processor_level=1` text is byte for byte what it was.

## Choices a reviewer should see

- On the unidentified receive-only Icom fallback, VOX, monitor and PROC
no longer cross the seam. Before, `setVox` was called there
unconditionally. That model has no transmitter.
- The Sim's `setVox`/`setSpeechProcessor` (base no-ops) are no longer
called. Its VOX delay control stays dimmed, as before.

## Known limits

- **HL2 CW break-in and break-in delay keep a false notice.** The HL2
has no radio-side keyer (`hasRadioSideCwKeyer` is false), so `cw
break_in` and `cw break_in_delay` stay unrouted. Both act there anyway:
`RadioModel` passes `cwBreakIn()` and `cwDelay()` to `setCwKeying()` at
key time, and `Hl2Backend::setCwKeying` keys MOX on the first and clamps
the second into its hang timer. Routing them needs a capability that
says a backend implements `setCwKeying()`; `hostModulates` is the wrong
proxy, because ANAN sets it and does not override that setter. Found by
@on8st on hardware.
- CW speed, break-in and pitch reach only the IC-705 and IC-7300MK2
among Icoms (#6110).

## Validation

**Unit tests** (Icom path; there is no Icom on this bench):
- `transmit_seam_drop_notice_test`: 57 PASS, 0 FAIL. For each newly
routed key there is a positive case (record present: setter called, no
notice) and a negative control (record absent: setter not called, notice
stands). Also covered: `vox_delay` with and without `hasDelay`; PROC on
a host-modulating transmitter and on a non-transmitting one; `cw
break_in_delay`; `mon_gain_cw`; and the Flex no-write case.
- **Mutation check:** 17 mutants, each caught by the case meant to catch
it. In the gate:
  - each key's routing forced to false, and forced to true;
  - `vox_delay` ignoring `hasDelay`;
  - each arm of the PROC disjunction removed;
  - the host arm without `canTransmit`.
  
  In the code around it:
  - each of the three connections with its gate removed;
  - the CW prefixes and the trailing space;
  - Flex's `txMonitorControl` declaration removed.
- Updated: `icom_control_profile_test` (Flex, IC-7300MK2, IC-9700 COMP,
HL2 and the unidentified Icom records),
`phone_cw_mic_gain_authority_test` (the MainWindow presentation
fan-out). `icom_family_test` is unchanged and passes.
- `ctest`: 568/568 pass, excluding the tests that own sockets
(`QUdpSocket`/`QTcp*`/`QLocal*`/`bind(`), the automation tests and
`connection_panel_size_test` (#5811).

**Static gates:** engine boundary `--strict` 0 blocking, touchpoint
manifest up to date, capability records 70 at frozen 70, command plane
401 = baseline.

**The agent automation bridge, Demo sim (Flex path only).** The binary
was built from the tree in the second commit; the build stamp reads
`ddc9aa4b5-dirty` because that commit was made right after the build.
Steps:
1. Connected to `DEMO-0001`, then set the slice to CW.
2. `invoke "CW speed" setValue` and `invoke "CW break-in" click`: `get
transmit` read cwSpeed 20→24 and cwBreakIn false→true.
3. Set the slice to USB, then `invoke` on VOX level (65), Speech
processor, Processor level (2), TX monitor and Monitor volume (35): `get
transmit` read voxLevel 50→65, speechProc false→true, speechProcLevel
0→2, monitor false→true, monGainSb 50→35.
4. A marked log window with `aether.protocol` on contains no `dropping`
or seam lines, and the app log has none either.

The VOX enable click was refused by the bridge's TX-safety guard, as it
should be. The Demo has a command plane, so this run shows the Flex path
is unchanged. The Icom path is covered by the unit tests only.

## Related

- #6110 (filed from this work). `hasRadioSideCwKeyer` means "the radio
buffers CW text", but it also gates
`setCwSpeed`/`setCwBreakIn`/`setCwPitch`. So those controls do nothing
on the IC-9700/7610/785x/7300/905, and this PR's CW routing reaches only
the IC-705 and IC-7300MK2.
- #6015 (added the gate), #5637, #6049 (the don't-emit approach for the
EQ), #5262 M2 (records over bools).

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

https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 4, 2026
…, AGENTS.md line count. Principle VIII.

- CTR2: the intro, README highlight and Roadmap line, and ROADMAP entries
  say Wi-Fi works today and USB's host side is ready, has carried the
  radio's status stream to development firmware, and waits on a CTR2 USB
  firmware release; the CHANGELOG bullet notes USB is the default and
  nothing persists, so current firmware needs Wi-Fi chosen each launch.
- #6119's upgrade note carries the open #6142 bookmark exception.
- AGENTS.md is 300 lines, not 301.
- The 413 -> 369 command-plane drop credits #6049's and #6124's reroutes
  alongside the seam migration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Oct 4, 2026
…lay, Qt 6.12 (#6159)

## Summary

Six-file release prep for **v26.10.1**, metadata and docs only. No
source, test or CI changes.

Cutoff: `origin/main` at `18c5245e5` (#6155, merged 2026-10-04).
Anything that merges after it is folded in before the tag.

**95 merged changes** (95 PRs, no direct commits) from **12 human
contributors**, plus AetherClaude (2) and Dependabot (0). There are no
first-time contributors this cycle. That was checked for each login by
looking for a merged PR before v26.9.5's tag, not from
`author_association`.

## Changelog

Built from `v26.9.5..18c5245` mapped through the commit-to-PR API
(`repos/…/commits/<sha>/pulls`), not from commit subjects. All 95 PRs
are cited in the section. The notes lead with three things:
- the sweep that makes every control on a radio without a Flex command
plane either work or refuse with a reason (#6044, #6049, #6015, #6111);
- the CTR2 relay (#6090, #6114, #6155) and authenticated 4O3A
connections with a Peripherals page (#6008, #6027);
- the Hermes-Lite 2's squelch, APF, AGC-off, RIT/XIT and FFT AVG.

The structural half covers Qt 6.12 for every binary and source build
(#5842, #6055) and the slice receive controls moving behind the seam
(#5892, #5907, #5919; raw Flex commands above the seam 413 → 369,
confirmed with `check_command_plane.py` at the cutoff). Upgrade-visible
changes are called out with **On upgrading** inline: the Ulanzi default
and removed MOX/TUNE pills (#5964), auto RF gain arming and law (#5976,
#6062), DIGU/DIGL opening with AGC off (#6119), HL2 squelch calibration
and the Auto SQL fallback (#6127), and the macOS 14.4 floor (#5842).

The previous tag v26.9.5 is an ancestor of `main`, so nothing is
excluded from the range.

## Docs refresh, not just version strings

- **README:** Highlights "Broad hardware control" gains the CTR2 relay.
Supported Hardware says direct TGXL/PGXL/Antenna Genius connections
authenticate. The Other radio families paragraphs gain the HL2's
squelch, APF, RIT/XIT, FFT averaging and CL1 reference, the ANAN-G2's
receiver audio and radio speaker, and RTL-SDR's confirmed receive state,
capture browsing and zoom FFT. In the Roadmap section, the aetherd line
gains the receive controls and a CTR2 relay line is added. No maturity
label changed. The six backends and seven NR engines were re-checked
against `src/core/backends/` and `AetherRxProfiles.cpp`.
- **ROADMAP, In flight:**
  - aetherd: the receive controls behind the seam.
- Icom: TUNE power live, no false drop notices, the one-panadapter
correction. The stale "VOX and CW break-in" leaves Remaining:
`IcomCivBackend` has implemented both since #4799/#5113.
  - ANAN-G2: receiver audio and radio speaker.
  - RTL-SDR: M1, with Remaining restated from #5924's body.
- Hermes-Lite 2: the v26.10.1 control parity, and the CW latency cost
updated to 4096 taps.
  - A new **CTR2 controller relay** entry.
  - Nothing left In flight as finished.
- **ROADMAP, Open RFCs:** diffed against `label:rfc`. #5893 leaves
(closed). #6106 and #6079 join under Radios, protocol and devices.
- **ROADMAP, Recently shipped:** 8 new `(v26.10.1)` entries. The list is
trimmed to the v26.10.x cycle at the month rollover, and its intro names
v26.10.x. Everything trimmed is still in CHANGELOG.

## Judgment calls flagged for the maintainer

- **Recently shipped trimmed to v26.10.x at the rollover:** maintainer's
call. This makes the validator's "only the four current-version lines
lose `26.9.5`" check FAIL. The lines it lists are exactly the trimmed
`(v26.9.5)` entry markers, and there is no historical mention elsewhere.
That is the one FAIL below. One side effect: the trim removes ROADMAP's
last mentions of #5630 and #5684, approved RFCs that shipped in v26.9.4.
Both are now closed as completed, and both remain in CHANGELOG.
- **A CTR2 relay In-flight entry:** maintainer's call. Wi-Fi works
today. USB's host side shipped and has carried the radio's status stream
to the firmware author's development firmware; it waits on a CTR2 USB
firmware release.
- **Intro wording with zero Dependabot updates:** "AetherClaude and no
dependency updates within that total" keeps the house shape and passes
the count check. The Contributors block has no Dependabot sentence.
- **#6125 is filed under the seam section.** It fixes a WAN/SmartLink
receive regression introduced by #5907 inside this range, so released
builds never had the regression. The section states what ships: receive
intents gate on `RadioModel::isConnected()`.
- Ready for review, not draft: maintainer's call.

## Scope

| file | change |
|---|---|
| `CMakeLists.txt` | `project(AetherSDR VERSION 26.10.1)` |
| `README.md` | Current version line, plus the prose refresh above |
| `AGENTS.md` | Current version line |
| `CHANGELOG.md` | new `[v26.10.1] — 2026-10-04` section; `[Unreleased]`
stays and stays empty; history below byte-identical |
| `packaging/linux/io.github.aethersdr.aethersdr.metainfo.xml` |
`<release version="26.10.1" date="2026-10-04"/>` first in `<releases>` |
| `ROADMAP.md` | cycle heading → post-v26.10.1, plus the prose refresh
above |

## Validation

`check_release_prep.py --mapping mapping.json`: 29 checks, **1 FAIL**
(explained above), 0 WARN.

- All six version spots say 26.10.1. The CHANGELOG heading date equals
the metainfo date, 2026-10-04.
- `appstreamcli validate` passed. The `<release>` list is
date-descending with unique versions (22 entries).
- `[Unreleased]` stays empty, `[v26.10.1]` sits directly under it, and
the CHANGELOG diff is exactly the inserted section, with history
byte-identical to `origin/main`.
- All 95 PRs in the range are cited. The only other numbers cited are
#3849 (RFC), #5262 (tracking issue), #5468 (RFC), #6091 (RFC, approved
and closed) and #6142 (open follow-up issue for #6119).
- The intro (95/12), the Contributors counts (13 entries) and
"first-time: none" are re-derived from the mapping, and they match.
- ROADMAP `(vX)` markers lead with v26.10.1. Every local link in README
and ROADMAP resolves. No removed heading is linked elsewhere. Code
fences balance, with no duplicated table headers or doubled `---`.
- **FAIL:** "only the four current-version lines lose `26.9.5`". The
lines are the trimmed Recently-shipped markers; see the judgment calls
above.
- `git diff --check` is clean. The diff touches no `src/`, `tests/`,
`third_party/` or `.github/workflows/` path.
- Configure: `cmake -S . -B <scratch>` reports
`CMAKE_PROJECT_VERSION=26.10.1`. No build or test was run, because this
is metadata only; CI builds it.
- `main` CI at the cutoff: `CI` passed on `18c5245e5`.

After merge: tag the resulting **main** commit, not this branch.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tropo1234 added a commit to tropo1234/AetherSDR that referenced this pull request Oct 5, 2026
…ures.

Neither file existed at this branch's previous base, so neither conflicted and
neither appeared in the rebase: hl2_rxdsp_adaptive_taps_test arrived with aethersdr#5981
and reroute_dead_controls_test with aethersdr#6049, and both call signatures this branch
widens.

hl2_rxdsp_adaptive_taps_test: four buildChannel(cfg, false, 0) calls become
(cfg, NoiseBlanker::Off, 0, NoiseBlankerFill::Zero). These cases assert filter
tap lengths across a rebuild and never engage a blanker, so off with a zero
fill is what they already had.

reroute_dead_controls_test: the fake backend's setSliceNoiseBlanker override
takes the kind and fill, and records kind != Off as the bool its rows already
assert — the same reading a radio with one blanker gives the request.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants