Repository navigation
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
Conversation
rfoust
left a comment
There was a problem hiding this comment.
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
- [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 productionTransmitModel::applyChanges()seam reproducesdax=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 intools/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/mainf042f903fbfdd7df26818b26a2b74ae6cefcc273; resulting index treeb09b066de2edc1f85e97ecd6be492ad60ff204af. PR base reported by GitHub iscedd50f4e59055866dceaccf013218e93513d670. - ARM64 configure/build with the operator's toolchain passed for
AetherSDR,transmit_dax_hold_test, andvoice_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 suppressingfinishTransmitDaxRelease()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. Confirmedmodel="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_testcontains 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.
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>
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>
ad38538 to
6e4ef83
Compare
jensenpat
left a comment
There was a problem hiding this comment.
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
-
[P1] Wait for a receiving state before restoring the microphone input.
src/models/RadioModel.cpp:2223-2226invokesfinishTransmitDaxRelease()on everyradioTransmittingChanged(false), andsrc/models/RadioModel.cpp:13466-13477permits the restore wheneverm_radioTransmittingis false. But the production interlock handler atsrc/models/RadioModel.cpp:12232-12234defines that boolean asstate == "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_REQUESTEDreport therefore requestsdax=0immediately, before any READY/RECEIVE report. Releasing duringPTT_REQUESTEDalso 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-313likewise 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_testwith calls through the productionhandleStatusForTest()router, leaving all production source identical to head6e4ef83b6ac2344efab1293a1430eacec142958f. 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=1For the second probe, acquire TCI, feed PTT_REQUESTED, start the write recorder, then release TCI before feeding READY. The existing
release_waits_for_rxcase 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()withdaxOn()makes all three corresponding assertions fail, including the originaldax=0 writes=0 holder=1reproduction. 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
6e4ef83b6ac2344efab1293a1430eacec142958fin an isolated scratch worktree, with merge base616d3ba1e975afaa2221c2f791be1afef9a9cea5. 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 usedQT_QPA_PLATFORM=offscreen, isolated settings andctest -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_testopens 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>
|
@jensenpat thanks. Confirmed, and our own FLEX-8400 log independently shows the same thing: in the live re-check, Fixed in 01c981c (on top of the rebased head; commits 1–5 are unchanged,
Tests (
Not yet done: a live FLEX-8400 run to show the wire order 73, 👨🏼💻 Co-authored by Claude Opus 5.5 |
|
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
In overs 2 and 3 the radio left a real gap between 73, 👨🏼💻 Co-authored by Claude Opus 5.5 |
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
left a comment
There was a problem hiding this comment.
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:13463and:13487, retains the actual interlock state and accepts only settled receive states after an interlock report. Production-router tests now retaindax=1 writes=0on UNKEY_REQUESTED and retain DAX through PTT_REQUESTED and TX_FAULT until READY. The existingradioTransmittingChangedsemantics remain intact. A local mutation replacing this gate with!m_radioTransmittingreproduced three failures, eachdax=0 writes=1; the mutation was restored and not committed. - Delayed ON status suppressing a new acquisition: commit
ef00abe5f,src/models/TransmitModel.cpp:821andsrc/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_testowns 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 existinghl2_family_transition_testwas 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=0in 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
left a comment
There was a problem hiding this comment.
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()andfinishTransmitDaxRelease(), retains the actual interlock state and accepts only settled receive states after an interlock report. Production-router tests now retaindax=1 writes=0on UNKEY_REQUESTED and retain DAX through PTT_REQUESTED and TX_FAULT until READY. The existingradioTransmittingChangedsemantics remain intact. A local mutation replacing this gate with!m_radioTransmittingreproduced three failures, eachdax=0 writes=1; the mutation was restored and not committed. - Delayed ON status suppressing a new acquisition: commit
ef00abe5f,src/models/TransmitModel.cpp:821andRadioModel::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_testowns 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 existinghl2_family_transition_testwas 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=0in 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
left a comment
There was a problem hiding this comment.
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
-
[P2] Retire the DAX borrowing state when replacing the backend, including replacements without
onDisconnected().src/models/RadioModel.cpp:8479clears the holder mask/baseline/pending restore only inonDisconnected(), whilerebuildBackendForFamily()atsrc/models/RadioModel.cpp:10793runsdropAllSessionModelsForFamilySwitch()->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 samerebuildBackendForFamily()used byconnectToRadio(); 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=1The first assertion is a separate short probe: acquire a holder, call
rebuildBackendForTest("sim"), then assert thattransmitDaxHeldBy()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
7c6684f27with 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_radioTransmittingreproduced 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_testwas 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.
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
left a comment
There was a problem hiding this comment.
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, andresetTransmitDaxSessionState()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 byresetState(). Answer-ticket numbers remain monotonic, so an old reply cannot consume a new session's request. VOX answer tracking remains separate.transmit_dax_hold_testnow has 26 passing assertions. Eight added assertions failed against production head7c6684f27. A ninth callback-order probe exposed the need for exit cleanup on the first implementation and now passes. The original review reproduction now reportsnew_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 atdax=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 andctest -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_testwas 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
left a comment
There was a problem hiding this comment.
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(), andresetTransmitDaxSessionState()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 byresetState(). Answer-ticket numbers remain monotonic, so an old reply cannot consume a new session's request. VOX answer tracking remains separate.transmit_dax_hold_testnow has 26 passing assertions. Eight added assertions failed against production head7c6684f27. A ninth callback-order probe exposed the need for exit cleanup on the first implementation and now passes. The original review reproduction now reportsnew_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 atdax=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 andctest -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_testwas 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.
…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>
Summary
Fixes #6538
Fixes #5524
Fixes #6579
The radio's
transmit daxflag 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:TciServer::prepareTxAudio()sent a rawtransmit set dax=1on the first TCI key-up and nothing ever sent it back, so the next microphone transmission keyed a carrier with no modulation. The macOS/PipeWire slice-mode sync happens to clear it on the next change to a voice mode, and fix(dax): don't toggle radio dax flag on Windows from slice-mode changes (#2315). Principle I. #2670 compiled that sync out on Windows (When switching bands and running the DAX2 new version, the DAX TX Stream shows busy. #2315).activateRADE()creates itsdax_txstream, but RADE had been gettingdax=1from that same mode sync. Since fix(dax): don't toggle radio dax flag on Windows from slice-mode changes (#2315). Principle I. #2670 (v26.5.2), Windows users have had to press DAX for RADE to modulate, and then SSB is silent until they turn it off again.Why #6538 settles the "one interop report" question on #5524
audio_start→trx:2,true,tci→ audio →trx:2,false→audio_stop→ closeaether.connectionshowsC351|transmit set dax=1sent and nodax=0ever after. The onlydax=0comes 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 theRreply toslice set tx=1. When it didn't, TCI saved RADE's1, RADE cleared the flag under TCI's over, and TCI then restored the1: the original latch.RadioModelnow owns the flag through a holder bitmask (acquireTransmitDax/releaseTransmitDax/transmitDaxHeldBy), the same shape asPanadapterStream's DAX RX holder table from #3305:dax=1.READY/RECEIVE/NOT_READY; notPTT_REQUESTED,UNKEY_REQUESTEDor 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).All six producers use it and keep their own "does this radio have a
transmit daxat 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:
fix(tci): TCI hands the flag back.fix(rade): RADE takes the flag on Windows.fix(dax): both, plus the other four producers, moved onto the shared holder table.fix(dax): from review: a staledaxstatus can no longer overrule an unanswered request. Everydaxrequest is tracked until the radio answers it, and the table decides from the newest request meanwhile.docs(rade): from review: platform-qualified RADE restore wording;TciServer.cppcommand-plane baseline 11 → 10.fix(dax): from review: the restore waits for a settled receive interlock state (READY/RECEIVE/NOT_READY), not just "not TRANSMITTING", soUNKEY_REQUESTED,PTT_REQUESTEDand fault states keep it pending.Out of scope
daxunder a TCI over on a USB slice. It's a pre-existing hole on those platforms, filed as macOS/PipeWire: the slice-mode sync overwrites the transmit dax flag mid-transmission (follow-up to #6593) #6595 rather than changing their behaviour here.awaiting-response) may be this bug. I'll ask the reporter to retest once this merges rather than claim it.User docs:
tci-server.mdgains the restore behaviour and drops the #5524 known issue;rade-digital-voice.mdnotes that RADE turns DAX on and hands it back. Log Analyzer source-line references are regenerated after integration with main. NoCHANGELOG.mdentry.Constitution principle honored
Principle XI: these
Fixesclaims are hypotheses until demonstrated on a radio. The PR stays draft until the live steps below are done.Integration validation
Signed merge commit
7c6684f27integrates main68ccf459fwithout 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 commit25d9afc9bintegrates main933f2fdeeafter #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
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 delayeddax=1echo overtaking an unanswereddax=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.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_testvoice_keyer_radio_testfails 2 VOX-during-recording cases, identically on currentmain(tracked by the open full-suite issues); unrelated to this changedocs/userbuild passesmain(cedd50f4e) first, then this branch. Results below.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 pluskey ptt; the TCI over is the bridge's TCI client sendingtrx:N,true,tciwith no audio. Each command and status line is from theaether.connectiondebug log.maincedd50f4edax=0dax=0dax_txstream created, notransmit set dax,dax=0transmit set dax=1sent; radio echoestransmit dax=1dax=0(never set)transmit set dax=0sent; radio echoesdax=0transmit set daxsent; stays ontransmit set dax=1, never cleareddax=1while keyed;transmit set dax=08 ms afterinterlock state=UNKEY_REQUESTEDtransmit set daxsent; stays ondax=1; after itdax=0with TX left on B; TX back to A → RADE retakesdax=1; RADE off →dax=0Re-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 daxcommands. With DAX off: control mic over 9.4 W, TCI over keyed withdax=1,transmit set dax=012 ms afterUNKEY_REQUESTED, next mic over 9.5 W.Superseded timing note. The table above (and the re-check) shows
transmit set dax=0going out 8–12 ms afterUNKEY_REQUESTED, which was before the radio'sREADY. Review flagged that as too early; commit 6 now waits forREADY.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=0thentransmit set dax=1, both before the radio reports TRANSMITTING. It's harmless (the over itself carriesdax=1), but it is a visible flip. In this run the radio'sslice … tx=status arrived before theRreply toslice set … tx=1; the other ordering is covered bytransmit_dax_hold_testrather than the live run.Checklist
AppSettingscallsdocs/user/docs/tci-server.md,rade-digital-voice.md); notCHANGELOG.md73,
Ryan NF0T
👨🏼💻 Co-authored by Claude Opus 5.5
🤖 Generated with Claude Code
Generated with OpenAI Codex (GPT-6 Astra)