Skip to content

fix(dax): one owner for the radio's transmit dax flag, so TCI no longer latches it and RADE sets it on Windows. Principle XI. - #6593

Merged
jensenpat merged 9 commits into
aethersdr:mainfrom
NF0T:fix/6538-tci-dax-restore
Oct 20, 2026
Merged

jensenpat merged 9 commits into
aethersdr:mainfrom
NF0T:fix/6538-tci-dax-restore

Conversation

@NF0T

@NF0T NF0T commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Ready for review. Unit-tested and proven on a FLEX-8400 against current main (live results below). The reporters of #6538 and #5524 have been invited to retest; their results are welcome but not blocking.

Summary

Fixes #6538
Fixes #5524
Fixes #6579

The radio's transmit dax flag is one bit for the whole radio, shared by every client. It decides whether the next transmission takes its audio from the DAX TX stream (1) or the microphone (0). On Windows (and Linux without PipeWire) nothing in AetherSDR managed that bit, which produced two bugs:

Why #6538 settles the "one interop report" question on #5524

#5524 #6538
Clients OpsLog voice keyer WSJT-X, plus the reporter's own minimal TCI client
Repro "use the OpsLog DVK" Bare protocol sequence: audio_start → trx:2,true,tci → audio → trx:2,false → audio_stop → close
Log TCI category only; the outgoing command isn't visible aether.connection shows C351|transmit set dax=1 sent and no dax=0 ever after. The only dax=0 comes from a different client handle (the reporter's workaround)

On Linux, turning DAX on while in SSB silences the microphone and turning it off restores it, so the flag behaves the same everywhere. Linux only escapes because the mode sync rewrites it.

The fix: one owner of the flag

Six in-process producers need the flag on while they transmit: TCI, RADE, the voice keyer, AX.25, WSPR and RX Playback. Each used to save and restore it for itself. That is only correct when they let go in the reverse order they took it. RADE holds for as long as its slice has TX, while the others hold for one over, so a TCI over on another slice overlaps a RADE hold. Whether that comes out right depended on whether the radio's slice tx= status arrived before or after the R reply to slice set tx=1. When it didn't, TCI saved RADE's 1, RADE cleared the flag under TCI's over, and TCI then restored the 1: the original latch.

RadioModel now owns the flag through a holder bitmask (acquireTransmitDax / releaseTransmitDax / transmitDaxHeldBy), the same shape as PanadapterStream's DAX RX holder table from #3305:

  • First hold: record the flag, set dax=1.
  • Last release: wait until the radio reports a settled receive state (READY / RECEIVE / NOT_READY; not PTT_REQUESTED, UNKEY_REQUESTED or a fault), so the mic is never switched in during a TX transition. Then turn the flag off only if it was off before and is still on. If the operator or another client turned it off mid-hold, theirs is the newer choice. A SmartSDR DAX2 user who had it on sees no command at all (When switching bands and running the DAX2 new version, the DAX TX Stream shows busy. #2315).
  • No grace window. A mic or footswitch key-up straight after an FT8 over has to modulate, so the restore is prompt: two commands per FT8 cycle.
  • Session retirement (disconnect or backend replacement) drops every hold, pending restore, old DAX observation and unanswered DAX request without issuing a restore. Cleanup runs before and after retirement callbacks. Reply-ticket numbers remain monotonic so a late old answer cannot consume a new session request.

All six producers use it and keep their own "does this radio have a transmit dax at all" guards. RADE holds the flag only while its slice is the TX slice, and only on builds where the mode sync is compiled out.

Commits:

  1. fix(tci): TCI hands the flag back.
  2. fix(rade): RADE takes the flag on Windows.
  3. fix(dax): both, plus the other four producers, moved onto the shared holder table.
  4. fix(dax): from review: a stale dax status can no longer overrule an unanswered request. Every dax request is tracked until the radio answers it, and the table decides from the newest request meanwhile.
  5. docs(rade): from review: platform-qualified RADE restore wording; TciServer.cpp command-plane baseline 11 → 10.
  6. fix(dax): from review: the restore waits for a settled receive interlock state (READY / RECEIVE / NOT_READY), not just "not TRANSMITTING", so UNKEY_REQUESTED, PTT_REQUESTED and fault states keep it pending.

Out of scope

User docs: tci-server.md gains the restore behaviour and drops the #5524 known issue; rade-digital-voice.md notes that RADE turns DAX on and hands it back. Log Analyzer source-line references are regenerated after integration with main. No CHANGELOG.md entry.

Constitution principle honored

Principle XI: these Fixes claims are hypotheses until demonstrated on a radio. The PR stays draft until the live steps below are done.

Integration validation

Signed merge commit 7c6684f27 integrates main 68ccf459f without rewriting contributor history. The mechanical command-plane conflict preserves both reductions: TciServer 11 → 10 and main's AutomationServer 6 → 3.

Session retirement is corrected in b06f54f9a: the DAX table, receive qualification, observations and unanswered requests are retired on both disconnect and backend replacement, including requests attempted by retirement callbacks. The new session uses its own radio-reported baseline. Log Analyzer source-line references are regenerated.

The final ARM64 focused build and 9/9 selected offscreen CTests passed, including the production backend family-switch and slice-lifecycle routing tests. All 26 holder assertions pass. Eight added lifecycle assertions failed on the prior production head; an additional callback-order probe exposed and verifies the exit cleanup. In particular, the replacement's initially-ON setting remains dax=1 writes=0, and retirement/disconnect issue no restore request. Signed merge commit 25d9afc9b integrates main 933f2fdee after #6621 landed. Both command reductions are preserved (TCI 4 → 3, AutomationServer stays 1), and moved Log Analyzer citations are regenerated. Boundary diagnostics match that main snapshot; raw command source and baseline are 171 (172 on main). Hosted CI reruns on the final head; no new live-radio or RF validation is claimed for the retirement correction.

Test plan

  • Original fix: author's MSVC/Qt 6.12.0 Windows 11 app and affected-target build. Session-retirement correction: final ARM64/Qt 6.12 focused build; platform builds run in hosted CI.
  • New transmit_dax_hold_test: 26 assertions covering both release orders, the wait for RX, a key-up before that RX, an outside turn-off mid-hold, a non-holder release, a disconnect, and (from review) a delayed dax=1 echo overtaking an unanswered dax=0 (from the table or the operator) plus the unsent-request answer. The original rules were mutation-checked by the author. Added retirement coverage checks active holds, pending restores, old requests/replies, observed state, receive qualification, disconnect ordering and retirement callbacks, with before/after failure evidence.
  • tci_server_review_test: new case for TCI handing the flag back (both starting states, a release while keyed, and a re-key before RX). Mutation-checked.
  • Existing producer tests pass: voice_keyer_tx_route_test, tx_operation_integration_test, hl2_family_transition_test, wspr_beacon_test, rx_playback_mute_owners_test, model_control_routing_test, transmit_model_test
  • voice_keyer_radio_test fails 2 VOX-during-recording cases, identically on current main (tracked by the open full-suite issues); unrelated to this change
  • docs/user build passes
  • Live, FLEX-8400 fw 4.3.16.42034, Windows 11: current main (cedd50f4e) first, then this branch. Results below.
  • Reporters of TCI TX leaves the radio’s DAX flag on after PTT off / disconnect; mic gives no modulation afterwards #6538 and TCI DVK leaves DAX enabled after TX #5524 retested on Windows 11 and confirm the fix (TCI TX leaves the radio’s DAX flag on after PTT off / disconnect; mic gives no modulation afterwards #6538: own TCI voice keyer, DAX off and on; TCI DVK leaves DAX enabled after TX #5524: OpsLog DVK). Both built the branch before the review fixes (commits 4–6).

Live results

FLEX-8400 fw 4.3.16.42034, Windows 11 MSVC builds, driven through the agent automation bridge. On air on 6 m (50.200 MHz USB, ANT1 via the ATU at SWR 1.2), RF power 10, operator monitoring. Overs of 2–2.5 s. The "mic over" is the bridge's testtone (1 kHz, −10 dB) into the PC mic path plus key ptt; the TCI over is the bridge's TCI client sending trx:N,true,tci with no audio. Each command and status line is from the aether.connection debug log.

Test main cedd50f4e This branch
Control mic over 9.4 W, dax=0 9.4 W, dax=0
Select RADE (no TX) slice → DIGU, dax_tx stream created, no transmit set dax, dax=0 transmit set dax=1 sent; radio echoes transmit dax=1
Leave RADE (back to USB) dax=0 (never set) transmit set dax=0 sent; radio echoes dax=0
DAX on first, RADE in and out — no transmit set dax sent; stays on
TCI over transmit set dax=1, never cleared dax=1 while keyed; transmit set dax=0 8 ms after interlock state=UNKEY_REQUESTED
Mic over after the TCI over 0.014 W: keyed, no modulation (#6538) 9.7 W
DAX on first, TCI over — no transmit set dax sent; stays on
RADE on slice A, TCI over on slice B — over keyed with dax=1; after it dax=0 with TX left on B; TX back to A → RADE retakes dax=1; RADE off → dax=0
Mic over after RADE — 9.6 W (#6579)

Re-check after the review fix (same radio and settings): with DAX already on (set by another client), RADE in and out and a TCI over sent zero transmit set dax commands. With DAX off: control mic over 9.4 W, TCI over keyed with dax=1, transmit set dax=0 12 ms after UNKEY_REQUESTED, next mic over 9.5 W.

Superseded timing note. The table above (and the re-check) shows transmit set dax=0 going out 8–12 ms after UNKEY_REQUESTED, which was before the radio's READY. Review flagged that as too early; commit 6 now waits for READY.

One thing the overlap test showed. When TCI moves TX from RADE's slice to its own, RADE lets go and TCI takes the flag about 6 ms apart: the wire shows transmit set dax=0 then transmit set dax=1, both before the radio reports TRANSMITTING. It's harmless (the over itself carries dax=1), but it is a visible flip. In this run the radio's slice … tx= status arrived before the R reply to slice set … tx=1; the other ordering is covered by transmit_dax_hold_test rather than the live run.

Checklist

  • Commits are signed
  • No new flat-key AppSettings calls
  • Code is clean-room
  • N/A: no meter UI touched
  • Documentation updated (docs/user/docs/tci-server.md, rade-digital-voice.md); not CHANGELOG.md
  • N/A: not security-sensitive

73,
Ryan NF0T


👨🏼‍💻 Co-authored by Claude Opus 5.5

🤖 Generated with Claude Code

Generated with OpenAI Codex (GPT-6 Astra)

rfoust
rfoust previously requested changes Oct 19, 2026

@rfoust rfoust left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The ordinary TCI and RADE paths address #6538, #5524 and #6579, and consolidating the six producers explains the scope. Requesting changes for one reproduced asynchronous-status race: an older ON echo can suppress the next producer's required ON command while an OFF command is still in flight.

Scope

File/group Claimed in the PR? Verdict
RadioModel.cpp/.h Yes Shared holder ownership; blocking race below
TciServer.cpp Yes Acquire/release lifecycle for #6538/#5524
MainWindow.h, MainWindow_DigitalModes.cpp, MainWindow_Wiring.cpp Yes RADE ownership for #6579
Voice keyer, AX.25, WSPR and RX Playback implementations/private fields Yes Relevant consolidation to preserve overlapping producers
transmit_dax_hold_test.cpp, tci_server_review_test.cpp, tests.cmake Yes Relevant regression coverage and registration
rade-digital-voice.md, tci-server.md Yes Relevant user documentation; platform qualification nit below

Everything in the diff is explained by the stated fix. No unrelated UI/default changes or CHANGELOG entry were found, and the scope-related checklist claims hold up.

Blocker

  1. [P2] An older ON status can suppress a new acquisition while OFF remains in flight. See the inline comment on RadioModel.cpp:13410. Socket-free injection through the production TransmitModel::applyChanges() seam reproduces dax=0 writes=0 holder=1: TCI still holds the flag, but acquisition sent no replacement ON command. Please distinguish a genuinely pre-existing ON baseline from stale status while a prior restore is outstanding, and cover that ordering before merging.

Non-blocking nits

  • Qualify RADE's restore guarantee by platform (inline on the user guide): the new holder path excludes macOS and PipeWire.
  • Lower src/core/TciServer.cpp's command-plane baseline from 11 to 10 in tools/check_command_plane.py. The checker reports the reduction; ratcheting the row down preserves that gain.

Verification and attacks

  • Reviewed head 657860a78de1ddc60a12b424eb6f631ac463d0ef, with a clean local merge into refreshed upstream/main f042f903fbfdd7df26818b26a2b74ae6cefcc273; resulting index tree b09b066de2edc1f85e97ecd6be492ad60ff204af. PR base reported by GitHub is cedd50f4e59055866dceaccf013218e93513d670.
  • ARM64 configure/build with the operator's toolchain passed for AetherSDR, transmit_dax_hold_test, and voice_keyer_tx_route_test, with RADE enabled. Host/system processors and the executable were ARM64; no RNNoise x86 sources appeared in the graph.
  • Original tests passed: ctest --test-dir build-codex-arm64 -R '^(transmit_dax_hold_test|voice_keyer_tx_route_test)$' --output-on-failure --no-tests=error. The nine holder cases survived both release orders, baseline already ON, wait-for-RX, reacquisition before RX, outside OFF, non-holder release, and disconnect. A separate mutant suppressing finishTransmitDaxRelease() caused five original holder cases to fail. The added delayed-echo case fails on the unmodified PR implementation; its code and output are inline.
  • Engine-boundary, touchpoint-manifest, capability-record and command-plane checks introduced no new violations compared with current main. The command-plane reduction above was the only new diagnostic.
  • Visible PR CI checks were green on the reviewed head. The CI workflow is triggered by pull_request; local verification additionally included the newer main snapshot named above.
  • Demo-only bridge smoke check: isolated settings, explicit socket and token, TX disabled; connect list, connect local serial DEMO-0001, connect wait 30000, get radio, get transmit, disconnect, then close. Confirmed model="AetherSDR Demo", serial="DEMO-0001", transmitting=false, dax=false; the review instance closed cleanly. The Flex-specific race was reproduced at the production model/status boundary, not with live hardware or RF; the receive-only demo cannot prove that positive TX path.
  • The modified tci_server_review_test contains existing WebSocket connections to AetherSDR's own server. The operator was notified before continuing; that target was not built or run in this review. Its registration names the ephemeral socket and existing bind failures return promptly. It is outside the filtered PR test gate.
  • Manual issue-fit and lifecycle/failure-mode passes completed; no separate model-invocable automated reviewer was available.

Comment thread src/models/RadioModel.cpp Outdated
Comment thread docs/user/docs/rade-digital-voice.md Outdated
NF0T added a commit to NF0T/AetherSDR that referenced this pull request Oct 19, 2026
Review of aethersdr#6593 (rfoust) reproduced it through TransmitModel's status
seam: a holder's restore sends dax=0, the radio's echo of an EARLIER
dax=1 lands before that request is answered, and the next acquire reads
daxOn() as on. It skipped its own dax=1 and took the stale 1 as the
baseline, so the producer held the flag with the radio off, and its
release would not have restored it either. The old TCI path never hit
this because it sent dax=1 unconditionally.

A status can only overtake a request until the radio answers it: on a
FLEX-8400 (fw 4.3.16) every `transmit dax=` status for our command came
before or with its R reply, and the radio sometimes sends no status at
all for a value superseded within a round trip. So TransmitModel now
tracks every dax request as outstanding until answered (answer tickets,
as the VOX hold already uses), and the holder table decides from the
newest requested value while any is outstanding, and from daxOn()
otherwise. That covers stale echoes of the operator's own clicks too, not
just the table's. A pre-existing ON with nothing in flight still sends no
command (aethersdr#2315).

RadioModel::dispatchTransmitControl() answers a dax request on every path
that never reaches the backend (disconnected, no backend, closing), so
the flag cannot read as in flight for the rest of the session.

transmit_dax_hold_test adds the reviewer's delayed-echo case as written,
its baseline consequence, the same race started by the operator, and the
unsent-request answer; each new rule was mutation-checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NF0T added a commit to NF0T/AetherSDR that referenced this pull request Oct 19, 2026
…command-plane row

From the aethersdr#6593 review. The RADE hold exists only where the slice-mode
sync is compiled out (Windows, Linux without PipeWire); on macOS and
PipeWire builds DAX follows the slice mode, so the guide now says so and
links aethersdr#6595. TciServer.cpp is down to 10 raw Flex commands from 11, so
its row in tools/check_command_plane.py drops to keep the gain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NF0T
NF0T requested a review from rfoust October 19, 2026 21:31
NF0T and others added 5 commits October 20, 2026 07:48
TciServer::prepareTxAudio() sent a raw `transmit set dax=1` on the first
TCI key-up and nothing ever cleared it. The flag is the radio's TX audio
source for every later transmission, so the next microphone PTT keyed a
carrier with no modulation. On macOS / PipeWire builds the slice-mode
sync in MainWindow_Wiring happens to clear it on the next change to a
voice mode; Windows compiles that sync out (aethersdr#2315), so there the flag
stayed on until the operator turned DAX off by hand.

Save the flag before taking it, through TransmitModel::setDax() so the
Phone/CW DAX button follows, and restore it from stopTxChrono(), which
every unkey and teardown path reaches. The restore waits for the radio
to report RX, because the abort paths stop the chrono while the carrier
may still be up, and a key-up that lands before that RX keeps the value
the first session found. It writes only a value that differs, so a
SmartSDR DAX2 user who already had DAX on sees no command. This is the
save/restore the voice keyer, AX.25 and WSPR producers already do.

Fixes aethersdr#6538
Fixes aethersdr#5524

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RADE sends its modem audio on the dax_tx stream, which the radio only
modulates while `transmit dax=1`. activateRADE() creates that stream but
never sets the flag. On macOS / PipeWire builds the slice-mode sync sets
it for the DIGU/DIGL slice RADE runs on; on Windows and non-PipeWire
Linux nothing does, so RADE keyed a silent carrier until the operator
pressed DAX, and voice then stayed silent until they turned it off.

Hold the flag while RADE's slice is the TX slice and hand back the value
found when RADE is turned off or TX moves to another slice. Builds that
already drive the flag from the slice mode are left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`transmit dax` is one bit for the whole radio, and six in-process
producers need it on while they transmit: TCI, RADE, the voice keyer,
AX.25, WSPR and RX Playback. Each saved and restored it for itself, which
is only right when they let go in the reverse order they took it. RADE
holds for as long as its slice has TX while the others hold for one over,
so a TCI over on another slice can overlap a RADE hold. Whether the
release order reverses then depends on whether the radio's `slice tx=`
status lands before or after the `R` reply to `slice set tx=1`. When it
doesn't, TCI saves RADE's 1, RADE clears the flag under TCI's over, and
TCI then puts the 1 back, which is the original latch.

RadioModel now owns the flag through a holder bitmask, the same shape as
PanadapterStream's DAX RX table (aethersdr#3305). The first hold records the flag
and sets dax=1. The last release waits for the radio to report RX and
then turns the flag off only if it was off before and is still on, so an
operator or another client who turned it off mid-hold keeps that choice,
and a DAX2 user who had it on sees no command (aethersdr#2315). There is no grace
window: a mic or footswitch key-up straight after an FT8 over must
modulate. A disconnect drops every hold without sending anything.

All six producers move to it and keep their own "does this radio have a
`transmit dax`" guards. TCI loses its private hold and RX hook, and RADE
its saved fields. transmit_dax_hold_test covers both release orders, the
wait for RX, a key-up before that RX, an outside change, a non-holder
release and a disconnect; each rule was mutation-checked.

Fixes aethersdr#6579

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of aethersdr#6593 (rfoust) reproduced it through TransmitModel's status
seam: a holder's restore sends dax=0, the radio's echo of an EARLIER
dax=1 lands before that request is answered, and the next acquire reads
daxOn() as on. It skipped its own dax=1 and took the stale 1 as the
baseline, so the producer held the flag with the radio off, and its
release would not have restored it either. The old TCI path never hit
this because it sent dax=1 unconditionally.

A status can only overtake a request until the radio answers it: on a
FLEX-8400 (fw 4.3.16) every `transmit dax=` status for our command came
before or with its R reply, and the radio sometimes sends no status at
all for a value superseded within a round trip. So TransmitModel now
tracks every dax request as outstanding until answered (answer tickets,
as the VOX hold already uses), and the holder table decides from the
newest requested value while any is outstanding, and from daxOn()
otherwise. That covers stale echoes of the operator's own clicks too, not
just the table's. A pre-existing ON with nothing in flight still sends no
command (aethersdr#2315).

RadioModel::dispatchTransmitControl() answers a dax request on every path
that never reaches the backend (disconnected, no backend, closing), so
the flag cannot read as in flight for the rest of the session.

transmit_dax_hold_test adds the reviewer's delayed-echo case as written,
its baseline consequence, the same race started by the operator, and the
unsent-request answer; each new rule was mutation-checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…command-plane row

From the aethersdr#6593 review. The RADE hold exists only where the slice-mode
sync is compiled out (Windows, Linux without PipeWire); on macOS and
PipeWire builds DAX follows the slice mode, so the guide now says so and
links aethersdr#6595. TciServer.cpp is down to 10 raw Flex commands from 11, so
its row in tools/check_command_plane.py drops to keep the gain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NF0T
NF0T force-pushed the fix/6538-tci-dax-restore branch from ad38538 to 6e4ef83 Compare October 20, 2026 12:48

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The normal TCI/RADE paths address #6538, #5524 and #6579, and the pending-request change fixes the earlier delayed-echo finding. Requesting changes for a separate, reproduced restore-timing bug: the new holder table treats transitional interlock states as proof that the radio has returned to receive.

Scope

File/group Claimed in the PR? Verdict
RadioModel.cpp/.h Yes Shared DAX holder lifecycle; restore-timing blocker below
TransmitModel.cpp/.h Yes Pending DAX answer tickets for the earlier review finding
TciServer.cpp Yes Acquire/release replaces the permanent DAX latch
MainWindow.h, MainWindow_DigitalModes.cpp, MainWindow_Wiring.cpp Yes Platform-qualified RADE hold and TX-slice synchronization
Voice keyer, AX.25, WSPR and RX Playback implementations/fields Yes Relevant consolidation for overlapping producers
Holder/TCI tests and tests.cmake Yes Relevant regression coverage; transition coverage gap below
RADE and TCI user guides Yes Relevant behavior documentation
tools/check_command_plane.py Yes Correct shrink from 11 to 10 for TciServer

Everything in the diff is explained by the stated fix. No unrelated visual/default changes, new daemon methods, dependency, or CHANGELOG entry were found. The known macOS/PipeWire mode-sync problem is explicitly scoped to #6595 rather than introduced by this PR.

Blocker

  1. [P1] Wait for a receiving state before restoring the microphone input. src/models/RadioModel.cpp:2223-2226 invokes finishTransmitDaxRelease() on every radioTransmittingChanged(false), and src/models/RadioModel.cpp:13466-13477 permits the restore whenever m_radioTransmitting is false. But the production interlock handler at src/models/RadioModel.cpp:12232-12234 defines that boolean as state == "TRANSMITTING": it is already false in UNKEY_REQUESTED and PTT_REQUESTED, not just when receiving.

    After the last holder releases while transmitting, the next UNKEY_REQUESTED report therefore requests dax=0 immediately, before any READY/RECEIVE report. Releasing during PTT_REQUESTED also requests OFF immediately. The consequence is that a stop/abort can select microphone audio while the radio is still completing its TX transition. This defeats the exact safety condition stated by the new code's comment and the PR's promise to wait for RX. The PR's live observation of OFF following UNKEY_REQUESTED does not establish that RX had completed.

    The official Flex xmit reference distinguishes a requested unkey transition from the receiving READY state. The project's existing FlexPttStopTracker.cpp:309-313 likewise advances from AwaitUnkey to AwaitReady on UNKEY_REQUESTED; it does not treat that event as terminal receiving evidence.

    I reproduced both cases without sockets, firmware simulation or RF by extending transmit_dax_hold_test with calls through the production handleStatusForTest() router, leaving all production source identical to head 6e4ef83b6ac2344efab1293a1430eacec142958f. The first probe is:

    RadioModel radio;
    TransmitModel& tx = radio.transmitModel();
    tx.setDax(false);
    radio.acquireTransmitDax(Holder::Tci);
    radio.handleStatusForTest("interlock", {{"state", "TRANSMITTING"}});
    radio.releaseTransmitDax(Holder::Tci);
    DaxWrites writes(tx);
    radio.handleStatusForTest("interlock", {{"state", "UNKEY_REQUESTED"}});
    report("unkey_requested_must_not_restore_before_ready",
           tx.daxOn() && writes.count == 0, state(tx, writes));
    radio.handleStatusForTest("interlock", {{"state", "READY"}});
    report("ready_finishes_pending_restore", !tx.daxOn(), state(tx, writes));

    Observed:

    [FAIL] unkey_requested_must_not_restore_before_ready  dax=0 writes=1
    [ OK ] ready_finishes_pending_restore               dax=0 writes=1
    [FAIL] release_during_ptt_requested_waits_for_ready   dax=0 writes=1
    

    For the second probe, acquire TCI, feed PTT_REQUESTED, start the write recorder, then release TCI before feeding READY. The existing release_waits_for_rx case only toggles the boolean directly (tests/transmit_dax_hold_test.cpp:124-135), so it cannot catch this production state-to-boolean mismatch.

    Please qualify this restore against actual receiving/stop evidence for the current session, keep requested TX/unkey transitions ineligible, and add socket-free cases through the real status router for both orderings. Preserve the existing signal's other consumers when correcting the restore gate. This is a demonstrated premature control request and a source-backed microphone-exposure risk; no RF/audio leakage was claimed or tested on hardware.

Earlier findings and non-blocking notes

  • The earlier delayed-ON-echo race is fixed at this head. Its cases pass; replacing transmitDaxEffective() with daxOn() makes all three corresponding assertions fail, including the original dax=0 writes=0 holder=1 reproduction. The mutation was restored locally and was never pushed.
  • Both prior nits are addressed: the RADE guide qualifies restore behavior by platform, and the command-plane baseline drops to the measured count. The prior review threads are still unresolved; this review does not dismiss or resolve them while the new blocker remains.
  • No additional nits.

Verification and limits

  • Reviewed exact head 6e4ef83b6ac2344efab1293a1430eacec142958f in an isolated scratch worktree, with merge base 616d3ba1e975afaa2221c2f791be1afef9a9cea5. All five commits are GitHub-verified. CI's build, Windows, macOS, Static checks and hosted Docs checks are green on the PR head. The PR workflow tests its merge snapshot; I did not infer current-main integration freshness from those checks or update the branch solely because main advanced.
  • ARM64 focused build passed with RADE enabled. 8/8 CTests passed on unmodified head: transmit_dax_hold_test, voice_keyer_tx_route_test, transmit_model_test, model_control_routing_test, tx_operation_integration_test, hl2_family_transition_test, wspr_beacon_test, rx_playback_mute_owners_test. Runs used QT_QPA_PLATFORM=offscreen, isolated settings and ctest -j22 --no-tests=error. The original holder test's 13 assertions pass; adding the two review probes exposes the blocker above.
  • Engine-boundary findings, touchpoint manifest and capability counts match the merge base. Raw command count and baseline both shrink 215 -> 214. No RNNoise x86 sources appear in the ARM64 Ninja graph. The static comparison did not rely only on exit status.
  • Inspected producer acquire/release/abort paths, both overlap release orders, already-ON baseline, reacquisition before RX, outside OFF, disconnect, delayed echoes and unanswered dispatch. The existing socket-free TX-operation and HL2-family selections passed; this is not live proof for other radio families.
  • The modified tci_server_review_test opens WebSocket connections to AetherSDR's own ephemeral server. The operator was notified before continuing; that target was not built or run. Its registration documents the bound socket and bind failures return promptly. It is outside the filtered per-PR test gate and runs in the default full-suite/sanitizer graph.
  • No app bundle was built and no bridge session or live radio was driven in this review: the new finding is at the Flex-specific model/interlock boundary, and the RX-only demo cannot establish positive TX transitions. The author's FLEX-8400 results and both reporters' successful Windows retests are supporting external evidence, not hardware validation performed in this session. Full native Windows/Linux and sanitizer execution were not run locally. A separate model-invocable automated reviewer was unavailable; the adversarial code/lifecycle pass was manual.

Recommendation: Request changes; do not merge yet. Fix the receiving-state qualification and demonstrate the transition cases before the final approval and merge pass.

Review of aethersdr#6593 (jensenpat) reproduced it through the production
interlock status router: the holder table finished a pending restore on
any radioTransmittingChanged(false), and m_radioTransmitting is only
`state == TRANSMITTING`. It is already false in UNKEY_REQUESTED (on the
way out of TX) and PTT_REQUESTED (on the way in), so a release during an
abort could switch the radio back to the microphone while it was still
completing a TX transition. The FLEX-8400 run in the PR shows the same
thing: `transmit set dax=0` went out 8 ms after UNKEY_REQUESTED and 6 ms
before READY.

RadioModel now keeps the last interlock state= the radio reported, and
the restore waits for a settled non-transmitting state: READY, RECEIVE
or NOT_READY. PTT_REQUESTED, TRANSMITTING, UNKEY_REQUESTED and the fault
states keep it pending, matching FlexPttStopTracker, which also waits for
READY after UNKEY_REQUESTED. A backend that reports no interlock, and a
Flex before its first report, keep the confirmed transmit state as
before. radioTransmittingChanged and its other consumers are unchanged;
the interlock handler already emits it on every state=, so READY
finishes the pending restore.

transmit_dax_hold_test adds the reviewer's two probes as written,
through handleStatusForTest(): UNKEY_REQUESTED then READY, and a release
during PTT_REQUESTED, plus a TX_FAULT case. Four mutants of the gate fail
them; reverting to the TX boolean reproduces the reviewer's output.

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

NF0T commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

@jensenpat thanks. Confirmed, and our own FLEX-8400 log independently shows the same thing: in the live re-check, transmit set dax=0 went out at 23.932, 8 ms after UNKEY_REQUESTED and 6 ms before READY (23.938). I had described that as "after the radio leaves TRANSMITTING", which is true but isn't the same as "receiving". The PR body now says so explicitly.

Fixed in 01c981c (on top of the rebased head; commits 1–5 are unchanged, git range-diff shows all =):

  • RadioModel keeps the last interlock state= (any owner, cleared on disconnect). The restore waits for a settled non-transmitting state: READY, RECEIVE or NOT_READY. PTT_REQUESTED, TRANSMITTING, UNKEY_REQUESTED and the fault states (TX_FAULT, TIMEOUT, STUCK_INPUT) keep it pending. That matches FlexPttStopTracker's AwaitUnkey → AwaitReady step, and FlexLib's own tuner, which gates on InterlockState.Ready.
  • A backend that reports no interlock, and a Flex before its first report, keep the confirmed transmit state as before.
  • radioTransmittingChanged and its other consumers are untouched. The interlock handler already emits it on every state=, so READY is what now finishes a pending restore.

Tests (transmit_dax_hold_test, 17 cases, all pass):

  • Your two probes as written, through handleStatusForTest(): unkey_requested_must_not_restore_before_ready (dax=1 writes=0), ready_finishes_pending_restore, and release_during_ptt_requested_waits_for_ready.
  • tx_fault_waits_for_ready, so the gate can't quietly become "anything but TRANSMITTING".
  • Four gate mutants all fail. Reverting to the TX boolean reproduces your dax=0 writes=1 output exactly.
  • The affected set is otherwise unchanged: tci_server_review_test, tx_operation_integration_test, hl2_family_transition_test, wspr_beacon_test, rx_playback_mute_owners_test, model_control_routing_test, transmit_model_test and voice_keyer_tx_route_test pass. voice_keyer_radio_test still has only the same two pre-existing VOX-recording failures as main.

Not yet done: a live FLEX-8400 run to show the wire order UNKEY_REQUESTED → READY → transmit set dax=0. I'll add it here once I've run it.

73,
Ryan NF0T


👨🏼‍💻 Co-authored by Claude Opus 5.5

@NF0T
NF0T requested a review from jensenpat October 20, 2026 16:29
@NF0T

NF0T commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

Live check of 01c981c on the FLEX-8400 (fw 4.3.16.42034, Windows 11 MSVC build), as promised above. 6 m, 50.200 MHz USB, ANT1, RF power 10, operator monitoring; the bridge's TCI client keyed trx:0,true,tci for 2.5 s with no audio (about 0.01 W); aether.connection debug log:

Over UNKEY_REQUESTED READY transmit set dax=0 radio echo
1 05.968 05.968 (same read) 05.976 dax=0 06.038
2 20.446 20.458 (+12 ms) 20.472 dax=0 20.525
3 27.086 27.096 (+10 ms) 27.113 dax=0 27.167

In overs 2 and 3 the radio left a real gap between UNKEY_REQUESTED and READY, and the restore waited it out every time. Before this commit, the same setup sent dax=0 inside that gap (yesterday's log: 8 ms after UNKEY_REQUESTED, 6 ms before READY). A microphone over afterwards modulated normally: 9.7 W, SWR 1.14. The PTT_REQUESTED and fault orderings weren't forced on air; they're covered by the unit cases.

73,
Ryan NF0T


👨🏼‍💻 Co-authored by Claude Opus 5.5

@jensenpat jensenpat self-assigned this Oct 20, 2026
@jensenpat
jensenpat dismissed stale reviews from rfoust and themself October 20, 2026 16:42

Verified final head 01c981c in an isolated ARM64 worktree: focused build and 7/7 selected offscreen CTests passed, including all 17 transmit_dax_hold_test assertions. ef00abe fixes the delayed-ON-echo race with answer tickets and latest-request decisions (TransmitModel.cpp:821; RadioModel.cpp:13418); original reproduction now reports dax=0 writes=1 holder=1 and the baseline, operator-click and unsent-answer cases pass. 6e4ef83 qualifies RADE platform wording and shrinks the measured command-plane baseline. Both threads carry verification replies. No concern from this review remains unaddressed.

jensenpat
jensenpat previously approved these changes Oct 20, 2026

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

Reviewed exact head 01c981c093b613924694fd1d9f17df792d59b97b. No remaining correctness blockers found. This fixes the TCI DAX latch in #6538/#5524 and the Windows/non-PipeWire RADE input selection in #6579 while preserving a pre-existing ON setting and overlapping producers.

Scope audit

File/group Claimed? Verdict
RadioModel.cpp/.h, TransmitModel.cpp/.h Yes Shared flag ownership, pending answers and receive-state qualification directly support the fix
TciServer.cpp Yes Matching acquire/release lifecycle for TCI
MainWindow.h, MainWindow_DigitalModes.cpp, MainWindow_Wiring.cpp Yes Platform-qualified RADE hold and TX-slice tracking
Voice keyer, AX.25, WSPR and RX Playback code/fields Yes Relevant consolidation prevents out-of-order restoration by overlapping producers
Holder/TCI tests and tests.cmake Yes Relevant coverage and registration
RADE/TCI user guides and command-plane baseline Yes Documents behavior and retains the measured command-count reduction

No unrelated visual/default change, dependency, daemon method, new vendor include above the seam, expanded ratchet, or CHANGELOG entry was introduced. The macOS/PipeWire mode-sync interaction remains the explicitly disclosed pre-existing #6595 limitation.

Earlier findings verified as fixed

  • Premature restore during TX transition: commit 01c981c09, src/models/RadioModel.cpp:13463 and :13487, retains the actual interlock state and accepts only settled receive states after an interlock report. Production-router tests now retain dax=1 writes=0 on UNKEY_REQUESTED and retain DAX through PTT_REQUESTED and TX_FAULT until READY. The existing radioTransmittingChanged semantics remain intact. A local mutation replacing this gate with !m_radioTransmitting reproduced three failures, each dax=0 writes=1; the mutation was restored and not committed.
  • Delayed ON status suppressing a new acquisition: commit ef00abe5f, src/models/TransmitModel.cpp:821 and src/models/RadioModel.cpp:13418, tracks outstanding answer tickets and uses the latest requested value while unanswered. The original injected delayed-echo case passes (dax=0 writes=1 holder=1), its release restores OFF, and the operator-originated variant and unsent-request answer pass. Status still updates the observed model, and disconnect resets pending tickets.
  • RADE documentation/baseline nits: commit 6e4ef83b6, docs/user/docs/rade-digital-voice.md:40, explicitly qualifies platform behavior and links #6595. TciServer's raw-command baseline drops 11 -> 10, matching the source.

Verification and limits

  • Fresh isolated ARM64 worktree; Qt 6.12, RADE enabled. Focused build passed using cmake --build build -j22 --target .... Final source tree is clean at the reviewed SHA. No app bundle was built.
  • 7/7 selected CTests passed with QT_QPA_PLATFORM=offscreen, isolated settings, -j22 --no-tests=error: transmit_dax_hold_test, voice_keyer_tx_route_test, transmit_model_test, model_control_routing_test, tx_operation_integration_test, wspr_beacon_test, rx_playback_mute_owners_test. All 17 holder assertions pass. Attacks include both overlap release orders, baseline already ON/no writes, wait for RX, reacquisition before RX, outside OFF, non-holder release, disconnect, delayed echoes, unanswered dispatch, and real interlock-router transitions.
  • Engine-boundary diagnostics match merge base 616d3ba1e975afaa2221c2f791be1afef9a9cea5; touchpoint manifest is current and unchanged; capability count stays 70. Raw command count and baseline both shrink 215 -> 214. Test registration passes; ARM64 graph contains no RNNoise x86 sources. These comparisons inspect diagnostics, not only exit codes.
  • The modified tci_server_review_test owns WebSocket connections to AetherSDR's ephemeral server. The operator was notified before continuing; this target was not built or run. Its registration identifies the socket and bind failures return promptly. It is outside the filtered per-PR gate. The existing hl2_family_transition_test was built but not run, because inspection found actual connection attempts; no hardware or synthetic firmware peer was contacted by this review's selected tests.
  • Inspected all six producers' acquisition, completion and abort paths, existing backend guards, RADE platform guards, TX-slice changes, and answer-dispatch lifetime handling. Shared model regressions passed; other radio families have no new live qualification from this review.
  • The RX-only demo cannot establish this Flex-specific positive TX/interlock path, so no app/bridge session or live radio was driven. The author's latest FLEX-8400 Windows recheck reports READY before transmit set dax=0 in three overs and subsequent microphone modulation; reporters of both TCI issues confirmed the earlier fix on Windows. Those are external hardware evidence, separate from this session's socket-free reproduction and mutation evidence. Native Windows/Linux and sanitizer runs were not performed locally. A separate model-invocable automated reviewer was unavailable; the adversarial pass was manual.

Approval applies only to the exact head above. Merge remains conditional on the platform CI jobs completing successfully; hosted Docs and Static checks have passed. No post-merge results are claimed.

@jensenpat
jensenpat enabled auto-merge (squash) October 20, 2026 16:43
@jensenpat
jensenpat disabled auto-merge October 20, 2026 16:51

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

Reviewed exact head 7c6684f27004f57a5ac1e1bd1a0ba6309ff2af77. No remaining correctness blockers found. This fixes the TCI DAX latch in #6538/#5524 and the Windows/non-PipeWire RADE input selection in #6579 while preserving a pre-existing ON setting and overlapping producers.

Scope audit

File/group Claimed? Verdict
RadioModel.cpp/.h, TransmitModel.cpp/.h Yes Shared flag ownership, pending answers and receive-state qualification directly support the fix
TciServer.cpp Yes Matching acquire/release lifecycle for TCI
MainWindow.h, MainWindow_DigitalModes.cpp, MainWindow_Wiring.cpp Yes Platform-qualified RADE hold and TX-slice tracking
Voice keyer, AX.25, WSPR and RX Playback code/fields Yes Relevant consolidation prevents out-of-order restoration by overlapping producers
Holder/TCI tests and tests.cmake Yes Relevant coverage and registration
RADE/TCI user guides, regenerated Log Analyzer citations and command-plane baseline Yes Documents behavior, refreshes moved source lines and retains both measured command-count reductions

No unrelated visual/default change, dependency, daemon method, new vendor include above the seam, expanded ratchet, or CHANGELOG entry was introduced. The macOS/PipeWire mode-sync interaction remains the explicitly disclosed pre-existing #6595 limitation.

Earlier findings verified as fixed

  • Premature restore during TX transition: commit 01c981c09, RadioModel::radioReceivingForTransmitDax() and finishTransmitDaxRelease(), retains the actual interlock state and accepts only settled receive states after an interlock report. Production-router tests now retain dax=1 writes=0 on UNKEY_REQUESTED and retain DAX through PTT_REQUESTED and TX_FAULT until READY. The existing radioTransmittingChanged semantics remain intact. A local mutation replacing this gate with !m_radioTransmitting reproduced three failures, each dax=0 writes=1; the mutation was restored and not committed.
  • Delayed ON status suppressing a new acquisition: commit ef00abe5f, src/models/TransmitModel.cpp:821 and RadioModel::transmitDaxEffective(), tracks outstanding answer tickets and uses the latest requested value while unanswered. The original injected delayed-echo case passes (dax=0 writes=1 holder=1), its release restores OFF, and the operator-originated variant and unsent-request answer pass. Status still updates the observed model, and disconnect resets pending tickets.
  • RADE documentation/baseline nits: commit 6e4ef83b6, docs/user/docs/rade-digital-voice.md:40, explicitly qualifies platform behavior and links #6595. TciServer's raw-command baseline drops 11 -> 10, matching the source.

Verification and limits

  • Fresh isolated ARM64 worktree; Qt 6.12, RADE enabled. Focused build passed using cmake --build build -j22 --target .... Final source tree is clean at the reviewed SHA. No app bundle was built.
  • 7/7 selected CTests passed with QT_QPA_PLATFORM=offscreen, isolated settings, -j22 --no-tests=error: transmit_dax_hold_test, voice_keyer_tx_route_test, transmit_model_test, model_control_routing_test, tx_operation_integration_test, wspr_beacon_test, rx_playback_mute_owners_test. All 17 holder assertions pass. Attacks include both overlap release orders, baseline already ON/no writes, wait for RX, reacquisition before RX, outside OFF, non-holder release, disconnect, delayed echoes, unanswered dispatch, and real interlock-router transitions.
  • Engine-boundary diagnostics match integrated main/merge base 68ccf459f; touchpoint manifest is current and unchanged; capability count stays 70. Raw command count and baseline both shrink 192 -> 191 against integrated main. Log Analyzer validation passes 707 checks for 45 rules, generated references are current, and only source-line citations were regenerated. Test registration passes; ARM64 graph contains no RNNoise x86 sources. These comparisons inspect diagnostics, not only exit codes.
  • The modified tci_server_review_test owns WebSocket connections to AetherSDR's ephemeral server. The operator was notified before continuing; this target was not built or run. Its registration identifies the socket and bind failures return promptly. It is outside the filtered per-PR gate. The existing hl2_family_transition_test was built but not run, because inspection found actual connection attempts; no hardware or synthetic firmware peer was contacted by this review's selected tests.
  • Inspected all six producers' acquisition, completion and abort paths, existing backend guards, RADE platform guards, TX-slice changes, and answer-dispatch lifetime handling. Shared model regressions passed; other radio families have no new live qualification from this review.
  • The RX-only demo cannot establish this Flex-specific positive TX/interlock path, so no app/bridge session or live radio was driven. The author's latest FLEX-8400 Windows recheck reports READY before transmit set dax=0 in three overs and subsequent microphone modulation; reporters of both TCI issues confirmed the earlier fix on Windows. Those are external hardware evidence, separate from this session's socket-free reproduction and mutation evidence. Native Windows/Linux and sanitizer runs were not performed locally. A separate model-invocable automated reviewer was unavailable; the adversarial pass was manual.

The previous approval covered 01c981c09. A concurrent main landing then caused a conflict in adjacent command-plane baseline entries. Signed merge commit 7c6684f27 integrates main 68ccf459f without rewriting contributor history, preserves TciServer=10 and AutomationServer=3, and regenerates moved Log Analyzer source-line citations. Re-reviewed the resulting main-relative diff, rebuilt the seven focused targets and reran the seven selections successfully on this clean integrated head. The holder implementation and mutation-tested gate retain their reviewed behavior.

Approval applies only to the exact integrated head above. Hosted CI must complete successfully on this new head before merge; earlier-head green checks are not substituted. No post-merge results are claimed.

@jensenpat
jensenpat enabled auto-merge (squash) October 20, 2026 16:53
@jensenpat
jensenpat disabled auto-merge October 20, 2026 17:04

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

Request changes; do not merge head 7c6684f27004f57a5ac1e1bd1a0ba6309ff2af77 yet. The final backend-replacement probes exposed a lifecycle blocker missed in my earlier approving pass. I disabled auto-merge before posting this review. The earlier delayed-echo and receive-transition fixes remain verified.

Scope audit

File/group Claimed? Verdict
Shared DAX table/answer tracking and six producer integrations Yes Relevant to #6538/#5524/#6579; session-retirement blocker below
Holder/TCI tests and registration Yes Relevant; missing socket-free backend-replacement case
User guides, command-plane baseline, generated Log Analyzer citations Yes Relevant documentation and mechanical integration maintenance

The main-relative diff remains in scope. Signed merge commit 7c6684f27 preserves both command-plane reductions and only regenerates moved Log Analyzer source-line references beyond the original fix.

Blocker

  1. [P2] Retire the DAX borrowing state when replacing the backend, including replacements without onDisconnected(). src/models/RadioModel.cpp:8479 clears the holder mask/baseline/pending restore only in onDisconnected(), while rebuildBackendForFamily() at src/models/RadioModel.cpp:10793 runs dropAllSessionModelsForFamilySwitch() -> teardownBackend() -> setupBackend() without that handler. teardownBackend() already documents that distinction for other session-owned state. The new DAX mask, pending restore, baseline and interlock state survive that replacement.

    This is more than a stale bookkeeping bit: at src/models/RadioModel.cpp:13485, a pending restore suppresses capture of the new radio's baseline. After replacement, a new TCI hold can therefore borrow against the old radio's OFF baseline and turn DAX OFF on release even when the new radio reported it ON before TCI began. A lingering holder from the retired session can also prevent the new session's last producer from restoring at all.

    I reproduced both with a temporary extension of transmit_dax_hold_test, leaving all production source identical to this head. rebuildBackendForTest() delegates to the same rebuildBackendForFamily() used by connectToRadio(); it constructs the backends without connecting to any radio, listener or synthetic peer.

    RadioModel radio;
    TransmitModel& tx = radio.transmitModel();
    tx.setDax(false);
    radio.acquireTransmitDax(Holder::Rade);
    radio.handleStatusForTest("interlock", {{"state", "TRANSMITTING"}});
    radio.releaseTransmitDax(Holder::Rade); // restore belongs to old session
    radio.rebuildBackendForTest("sim");
    radio.rebuildBackendForTest("flex");
    QCoreApplication::processEvents();
    TransmitDelta newRadioOn;
    newRadioOn.dax = true;
    tx.applyChanges(newRadioOn);           // new radio already has DAX ON
    DaxWrites writes(tx);
    radio.acquireTransmitDax(Holder::Tci);
    radio.releaseTransmitDax(Holder::Tci);
    radio.handleStatusForTest("interlock", {{"state", "READY"}});
    report("new_backend_on_baseline_survives_old_pending_restore",
           tx.daxOn() && writes.count == 0, state(tx, writes));

    Observed:

    [FAIL] backend_replacement_drops_old_holders
    [FAIL] new_backend_on_baseline_survives_old_pending_restore  dax=0 writes=1
    

    The first assertion is a separate short probe: acquire a holder, call rebuildBackendForTest("sim"), then assert that transmitDaxHeldBy() no longer reports that old holder. It still does.

    Please retire the borrowing table and its receive-state qualification on every session/backend retirement path, without restoring the old radio's flag onto a replacement. Add socket-free replacement cases for both an active hold and a last release pending RX, and verify that the new session's initially-ON baseline remains ON with zero writes. Include any pending-answer/session state needed to prevent old requests from affecting the new borrowing decision. This is a demonstrated premature/wrong control request across session replacement, not a claim of RF behavior.

Verification and limits

  • ARM64 focused build passed at clean head 7c6684f27 with RADE enabled, and the original 7/7 selected offscreen CTests passed, including all 17 existing holder assertions. The additional replacement-probe build passed; its two extra assertions failed as above. Temporary test edits were saved outside the worktree and restored; no probe or production fix was committed or pushed.
  • The prior receive-state mutation still provides valid evidence for that separate fix: replacing the gate with !m_radioTransmitting reproduced the UNKEY_REQUESTED, PTT_REQUESTED and fault failures; restored source and the seven-test selection passed afterward.
  • Boundary diagnostics match integrated main 68ccf459f; capability count is 70; command source and baseline shrink 192 -> 191; generated references are current and Log Analyzer validation passes 707 checks. Hosted Docs and Static checks passed on the new head; platform CI was still running when this new blocker was found.
  • The modified WebSocket-owning tci_server_review_test was disclosed to the operator and was not built or run. The existing connection-owning HL2 family-transition target was built on the earlier head but not run. No app/bridge session or live hardware was driven: the RX-only demo cannot prove the Flex TX path. The author's live FLEX-8400 results and reporters' Windows confirmations do not exercise this backend-replacement ordering. A separate model-invocable automated reviewer was unavailable; this was a manual adversarial pass with socket-free production-path probes.

Recommendation: Request changes. The pending restore and holders must belong to the session that created them before this can merge.

@jensenpat jensenpat assigned jensenpat and unassigned jensenpat Oct 20, 2026
@jensenpat
jensenpat dismissed their stale review October 20, 2026 17:15

Fixed in b06f54f. RadioModel retires the DAX table and receive qualification before and after teardown/disconnect callbacks; TransmitModel drops old observations and pending DAX answers while retaining monotonic tickets. All 26 holder assertions and 8/8 selected offscreen CTests pass on the clean final head, including production backend family switching. Eight new assertions failed on 7c6684f; the additional callback-order probe failed without exit cleanup. The original new-session ON reproduction now reports dax=1 writes=0, and retirement/disconnect issue zero restore requests. TCI user docs and generated Log Analyzer line references are updated. No live hardware or RF was used.

jensenpat
jensenpat previously approved these changes Oct 20, 2026

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

Approved exact head b06f54f9a206a87917b103db1ce84d7af69abbf0 after fixing and demonstrating the backend-retirement P2 blocker. The session that borrowed DAX now owns its holders, saved baseline, receive qualification, observed DAX value and pending answers through retirement; none is carried into a replacement session.

Resolution and evidence

  • RadioModel.cpp:1758, :8391, and resetTransmitDaxSessionState() retire the table and receive state at entry and exit of backend teardown/disconnect. Entry cleanup prevents release/false-TX notifications from flushing an obsolete restore. Exit cleanup discards requests attempted by retirement callbacks. Both normal replacement and the no-backend return run the cleanup.
  • TransmitModel::resetDaxSessionState() discards old DAX observations and unanswered requests without emitting control intents or model notifications. The same reset is reused by resetState(). Answer-ticket numbers remain monotonic, so an old reply cannot consume a new session's request. VOX answer tracking remains separate.
  • transmit_dax_hold_test now has 26 passing assertions. Eight added assertions failed against production head 7c6684f27. A ninth callback-order probe exposed the need for exit cleanup on the first implementation and now passes. The original review reproduction now reports new_backend_on_baseline_survives_old_pending_restore: dax=1 writes=0; old pending OFF requests and retirement-callback edits likewise leave the new ON baseline at dax=1 writes=0. Backend retirement and disconnect both issue zero obsolete restore requests. Active holders, old observations, receive qualification and late-answer isolation are covered through the production backend replacement path without dialing a radio or opening a peer.
  • The prior delayed-echo and premature-restore findings remain fixed. Their cases pass, including both overlap release orders, a pre-existing ON baseline, reacquisition before RX, operator OFF, UNKEY_REQUESTED, PTT_REQUESTED and TX_FAULT. The earlier local mutation of the receive gate reproduced the transition failures; that mutant was restored and never pushed.

Scope

File/group Purpose Verdict
RadioModel/TransmitModel implementation and private declarations Retire session-owned DAX state on every backend teardown/disconnect exit Within the P2 fix
transmit_dax_hold_test.cpp Behavioral replacement, disconnect, answer and callback regressions Within the P2 fix
TCI user guide and generated Log Analyzer citations Describe retirement behavior and refresh moved source locations Required documentation maintenance
Original producer integrations, RADE/TCI docs, test registration and shrinking baseline Address #6538/#5524/#6579 and preserve overlapping DAX producers Within the original accepted scope

No new dependency, thread, daemon method, vendor include above the seam, changed default, workflow edit, expanded ratchet or CHANGELOG entry was added. The explicitly disclosed pre-existing macOS/PipeWire mode-sync issue remains #6595.

Validation and boundaries

  • Clean source tree at the approved SHA. ARM64/Qt 6.12 build with RADE enabled passed for all eight focused targets using cmake --build build -j22 --target ....
  • 8/8 selected CTests passed on this exact head, with QT_QPA_PLATFORM=offscreen, isolated settings, TX disabled and ctest -j22 --no-tests=error: transmit_dax_hold_test, voice_keyer_tx_route_test, transmit_model_test, model_control_routing_test, tx_operation_integration_test, wspr_beacon_test, rx_playback_mute_owners_test, backend_family_switch_test. The family-switch test uses the in-process demo and cold backends; this is not live-device qualification.
  • Engine-boundary diagnostics match integrated main/merge base 68ccf459f; touchpoint manifest is unchanged and current; capability count stays 70; command-plane source and baseline stay 191 (192 on the integrated main). Test registration and generated-reference checks pass; Log Analyzer validation passes 707 checks for 45 rules and source citations were regenerated. No RNNoise x86 sources are in the ARM64 build graph.
  • The modified WebSocket-owning tci_server_review_test was previously disclosed and remains not built or run. No real-radio session, RF or full app bundle was used for this fix. The RX-only demo bridge cannot establish the Flex TX/interlock path; the corrected retirement behavior is demonstrated at the socket-free production model/teardown seam instead. The author's FLEX-8400 evidence and reporters' Windows retests concern the earlier fixes, not new hardware proof for this retirement change. Native Windows/Linux and sanitizer execution were not run locally. A separate model-invocable automated reviewer was unavailable; source/lifecycle review was manual.

The blocking review is satisfied by the committed fix and empirical before/after evidence above. Merge is conditional on the required hosted checks for this exact head; no earlier-head or post-merge results are claimed.

@jensenpat
jensenpat enabled auto-merge (squash) October 20, 2026 17:16
@jensenpat
jensenpat disabled auto-merge October 20, 2026 17:29

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

Approved exact head 25d9afc9bce0fc84aad10948f2afb6c318a78f64 after fixing and demonstrating the backend-retirement P2 blocker. The session that borrowed DAX now owns its holders, saved baseline, receive qualification, observed DAX value and pending answers through retirement; none is carried into a replacement session.

Resolution and evidence

  • RadioModel::teardownBackend(), onDisconnected(), and resetTransmitDaxSessionState() retire the table and receive state at entry and exit of backend teardown/disconnect. Entry cleanup prevents release/false-TX notifications from flushing an obsolete restore. Exit cleanup discards requests attempted by retirement callbacks. Both normal replacement and the no-backend return run the cleanup.
  • TransmitModel::resetDaxSessionState() discards old DAX observations and unanswered requests without emitting control intents or model notifications. The same reset is reused by resetState(). Answer-ticket numbers remain monotonic, so an old reply cannot consume a new session's request. VOX answer tracking remains separate.
  • transmit_dax_hold_test now has 26 passing assertions. Eight added assertions failed against production head 7c6684f27. A ninth callback-order probe exposed the need for exit cleanup on the first implementation and now passes. The original review reproduction now reports new_backend_on_baseline_survives_old_pending_restore: dax=1 writes=0; old pending OFF requests and retirement-callback edits likewise leave the new ON baseline at dax=1 writes=0. Backend retirement and disconnect both issue zero obsolete restore requests. Active holders, old observations, receive qualification and late-answer isolation are covered through the production backend replacement path without dialing a radio or opening a peer.
  • The prior delayed-echo and premature-restore findings remain fixed. Their cases pass, including both overlap release orders, a pre-existing ON baseline, reacquisition before RX, operator OFF, UNKEY_REQUESTED, PTT_REQUESTED and TX_FAULT. The earlier local mutation of the receive gate reproduced the transition failures; that mutant was restored and never pushed.

Scope

File/group Purpose Verdict
RadioModel/TransmitModel implementation and private declarations Retire session-owned DAX state on every backend teardown/disconnect exit Within the P2 fix
transmit_dax_hold_test.cpp Behavioral replacement, disconnect, answer and callback regressions Within the P2 fix
TCI user guide and generated Log Analyzer citations Describe retirement behavior and refresh moved source locations Required documentation maintenance
Original producer integrations, RADE/TCI docs, test registration and shrinking baseline Address #6538/#5524/#6579 and preserve overlapping DAX producers Within the original accepted scope

No new dependency, thread, daemon method, vendor include above the seam, changed default, workflow edit, expanded ratchet or CHANGELOG entry was added. The explicitly disclosed pre-existing macOS/PipeWire mode-sync issue remains #6595.

Validation and boundaries

  • Clean source tree at the approved SHA. ARM64/Qt 6.12 build with RADE enabled passed for all nine focused targets using cmake --build build -j22 --target ....
  • 9/9 selected CTests passed on this exact head, with QT_QPA_PLATFORM=offscreen, isolated settings, TX disabled and ctest -j22 --no-tests=error: transmit_dax_hold_test, voice_keyer_tx_route_test, transmit_model_test, model_control_routing_test, tx_operation_integration_test, wspr_beacon_test, rx_playback_mute_owners_test, backend_family_switch_test, slice_lifecycle_routing_test. The family-switch test uses the in-process demo and cold backends; this is not live-device qualification.
  • Engine-boundary diagnostics match integrated main/merge base 933f2fdee; touchpoint manifest is unchanged and current; capability count stays 70; command-plane source and baseline are 171 (172 on the integrated main). Test registration and generated-reference checks pass; Log Analyzer validation passes 707 checks for 45 rules and source citations were regenerated. No RNNoise x86 sources are in the ARM64 build graph.
  • The modified WebSocket-owning tci_server_review_test was previously disclosed and remains not built or run. No real-radio session, RF or full app bundle was used for this fix. The RX-only demo bridge cannot establish the Flex TX/interlock path; the corrected retirement behavior is demonstrated at the socket-free production model/teardown seam instead. The author's FLEX-8400 evidence and reporters' Windows retests concern the earlier fixes, not new hardware proof for this retirement change. Native Windows/Linux and sanitizer execution were not run locally. A separate model-invocable automated reviewer was unavailable; source/lifecycle review was manual.

The blocking review is satisfied by the committed fix and empirical before/after evidence above. Merge is conditional on the required hosted checks for this exact head; no earlier-head or post-merge results are claimed.

A concurrent main landing (#6621) caused mechanical conflicts after the earlier approval. Signed merge commit 25d9afc9b preserves the P2 fix in b06f54f9a, integrates main 933f2fdee, preserves both shrinking command baselines and regenerates moved Log Analyzer source citations. The resulting main-relative diff was re-reviewed; all nine targets rebuilt and all nine selected tests passed on this clean integrated head. TCI acquire/release and all 26 holder assertions remain intact.

@jensenpat
jensenpat enabled auto-merge (squash) October 20, 2026 17:33
@jensenpat
jensenpat merged commit 8394342 into aethersdr:main Oct 20, 2026
9 checks passed
@NF0T
NF0T deleted the fix/6538-tci-dax-restore branch October 20, 2026 18:28
wa2n-code pushed a commit to wa2n-code/AetherSDR that referenced this pull request Oct 21, 2026
…thersdr#6628)

## Summary

Closes aethersdr#6623. Refs aethersdr#3849 and aethersdr#5262.

Moves desktop panadapter/waterfall lifecycle and existing display
controls behind typed backend requests. This is a standalone M4 cleanup
based on `main`, including the merged aethersdr#6619 utilities, aethersdr#6621 slice/split
routes and aethersdr#6593 DAX fix. It does not complete M4 or add daemon protocol
methods, grants, transmit authority, dependencies, or new rendering/UI
features.

- Covers creation, paired pan/waterfall removal, inventory replay,
startup/layout creation, geometry/band, dBm range, pixel/analyzer
dimensions, averaging/FPS, RF gain, WNB, loops, noise-floor controls,
and waterfall rate/levels/auto-black.
- Flex owns bounded command encoding; native backends declare only the
controls they implement. Native synchronous creation/refusal and
asynchronous Demo publication are preserved.
- Answered creation correlates the original session's actual returned ID
with its new owned model. Unknown outcomes retain bounded reservations
without automatic retry or orphan deletion. Waterfall status can arrive
later.
- Original model/session/ownership/pairing and per-field intent fences
protect delayed initialization, compatibility fallback, removal and
profile-load batches. Newer operator intent wins, including edits to
individual waterfall fields after an aggregate defaults request.
- Bridge lifecycle receipts describe admission, not observed completion.
Ordinary close preserves the last pan; explicit `all` still respects
foreign ownership and native refusal.
- Removes 46 raw Flex command sites above the seam (171 → 125). DAX/IQ
binding remains separate; the merged utility and slice/split/FDX routes
are preserved.
- Fixes the noise-floor setter keys: `display pan set` now sends
`pan_position=` / `pan_position_enable=` as FlexLib v4.3.16 does
(`Panadapter.cs:530/544`); `noise_floor_position*` remains the status
key.
- Normalizes a pan's `waterfall=` value to the canonical `0x%08x` ID
used by the inventory maps. Unprefixed digits parse as decimal, matching
FlexLib `StringHelper.TryParseInteger`.
- On a backend that declares its pan display controls without span
(today the Demo, whose span is fixed), zoom is left alone: the −/+ pair
is dimmed on every pane with the reason in its tooltip and accessible
description, and keyboard, MIDI, wheel and scale-drag zoom send no span
request the backend would refuse. A drag still moves the center. A pane
showing KiwiSDR keeps its live zoom; disconnect restores the pair.

Protocol encoding was checked against the official published [FlexLib
v4.3.16
archive](https://edge.flexradio.com/www/offload/20261012095725/FlexLib_API_v4.3.16.zip),
including `pan_position` / `pan_position_enable`, creation and paired
teardown. No proprietary-binary decompilation was used.

## Constitution principle honored

Principle II: radio-authoritative observation, not request admission,
establishes live resources. Principle VII: typed values, IDs and replies
are validated at ingress. Principles IV and VIII: clean-room protocol
authority and behavior-backed evidence.

## Test plan

Three-platform qualification revision:
`a85749d26f27a89c68810c841f1ce7450fbdb230`, based on
`933f2fdeeb95ef838345afb421d1dda03b8b0197`.

VM payload manifests verify that complete source tree. Their dedicated
checkouts use the recorded base plus the applied patch, so their runtime
build identities correctly report the base as dirty.

| Platform | Application + aetherd | Focused CTests | Production
Demo/MCP checks |
|---|---|---|---|
| macOS ARM64 | Pass; RADE enabled, architecture verified | 20/20 |
39/39 |
| Linux ARM64 | Pass | 20/20 | 39/39 |
| Windows MSVC x64 | Pass | 20/20 | 39/39 |

Publication head: `87501ac87210c8325f315dd72f81b3b409021029`,
incorporating `main` at `839434225e973edba35d0c5757a2d67bac5f595c`
(aethersdr#6593). The only merge conflict was generated log source line numbers.
Comparing the normalized patch before/after integration, excluding those
anchors, gives identical code, tests and documentation changes.
Post-integration Mac application/daemon, 21 focused tests (including the
upstream DAX-holder regression) and 39 MCP checks all pass; ARM64 and
RADE checks also pass. Linux/Windows were not rerun at the publication
head; their results above are pinned to the three-platform revision, not
represented as a final-head or RF test.

The local Windows deployment helper's archive timestamp handling was
repaired and regression-tested separately; that tooling repair is not
part of this product PR.

The 20 tests cover pan control/dimension/drop-notice routing,
radio-owned and client display settings, inventory/status policy, pan
decode/dBm/frame guards, backend affinity/family replacement,
noise-floor gating, HL2 creation, Demo, read-only metadata, desktop
utility controls and both merged slice lifecycle targets. New
regressions are socket-free and exercise the production Flex encoder,
reply/publication order, timeout reservations, requester death, ID
reuse, ownership changes, history bounds, newer/reentrant intent and
partial/reverse-ACK teardown.

Mutation check: disabling creation identity protection and restoring the
old noise-floor key made five regression assertions fail; restored
production code returned them to green. Full changed-code review and
incremental security review found no remaining findings. Formal security
passes were pinned to immutable pre-rebase implementation/correction
revisions; integration resolutions were reviewed separately, without
claiming a new formal scan of the final head.

MCP checks use the unmodified production stdio server, authenticated
task-owned sockets, fresh isolated settings, `DEMO-0001`,
`AETHER_AUTOMATION_NO_TX=1` and auto-connect disabled. They exercise
repeated create/close, last-pan/native refusals, display controls and
readback, reflow, pop-out/redock, production layout-driven
creation/settling/teardown, slice 0 tuning, blocked MOX and disconnect.
Mac uses Cocoa; VM runs use Qt offscreen. These are Demo/control-path
proofs, not physical-radio, RF, GPU-rendering, or full-suite/CI
acceptance.

Engine-boundary, capability-record, command-plane and touchpoint
ratchets, test registration, generated references, 707 Log Analyzer
checks, whitespace checks and the documentation build pass. Updated
user/bridge/design docs and generated source citations; no visual
redesign or screenshot change.

## Checklist

- [x] Local/native application and daemon builds and focused tests pass.
- [ ] Physical-radio behavior tested — not performed in this RX-only
Demo qualification.
- [ ] Existing tests pass in CI — pending the PR run; full suite not run
locally.
- [x] Commits are SSH-signed.
- [x] No new flat-key AppSettings persistence or credentials.
- [x] Clean-room implementation; no meter rendering changes.
- [x] User documentation updated in this PR; CHANGELOG.md untouched.
- [x] No reportable security finding requiring an advisory.

Generated with OpenAI Codex.

---------

Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
Co-authored-by: Claude Opus 5.5 (1M context) <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

3 participants