fix(gui): route keyboard/space PTT through the Quindar coordinator (#3610)#3798
Conversation
…ethersdr#3610) Space-bar PTT and the keyboard MOX/TX toggle (T) keyed the radio via RadioModel::setTransmit() -> TransmitModel::setTransmitting(), the raw state setter that emits an `xmit` command but never touches the Quindar intro/outro state machine. Only the GUI MOX button and TCI hardware PTT went through TransmitModel::requestPttOn/Off(), the single coordinator that sequences the tone around the MOX edge. So Quindar was silent for keyboard PTT even with QUIN enabled on a phone mode, while the MOX button worked — exactly as reported. Point the space-bar event-filter path and the mox_toggle shortcut at requestPttOn/Off(PttSource::Mox) instead, so the keyboard path now runs the same coordinator the working MOX button uses. requestPttOn/Off still terminate in an `xmit 0/1` command, which RadioModel's xmit handler gates identically (interlock arm, m_txRequested, txAudioGate teardown), and the coordinator's preflight applies the same local interlock check — so no gating is lost; the only added behaviour is the Quindar sequence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Nice fix — clean, minimal, and the root-cause analysis is correct. I traced both PTT paths and the equivalence claim holds:
The redirect is safe. The new path emits commandReady("xmit 1/0") from TransmitModel::setMox(), and RadioModel's commandReady handler (RadioModel.cpp:494-511) applies the same armInterlockNotification / m_txRequested / txAudioGate-teardown gating that setTransmit() did inline. More to the point: the change makes keyboard/space PTT call the identical requestPttOn/Off(PttSource::Mox) entrypoint the already-working GUI MOX button uses (TxApplet.cpp:374-376), so it inherits exactly that button's interlock and gating behavior — there's no new code path to get wrong.
It also matches documented intent. TransmitModel.h:148-150 states "All UI and TCI hardware-PTT callers should use this path so Quindar tones happen consistently regardless of source." The space-bar filter and mox_toggle shortcut were the outliers; this aligns them.
Edge cases I checked, all fine:
- Interlock block on key-down:
m_spacePttActiveis settruebefore the (now-blocked)requestPttOn; the matching key-up still callsrequestPttOff, which short-circuits (phase == Idle) to a harmlessxmit 0. Same net effect as before. - Re-key during the outro window: a fast space re-press lands in
requestPttOn'scoalesceReEngagepath (phaseDisengaging), cancelling the pending unkey — the intended coalescing, not a stuck state. - Non-phone / CW:
isPhoneModeForQuindar()gates the tone, so CW PTT via space is unchanged — no deferred unkey, no regression. - RADE EOO: unkey now routes through
dispatchMoxOff()'spttOffHook, same as the MOX button — a consistency improvement, and a path the button already exercises.
One minor semantic note (non-blocking): both space-bar (momentary, footswitch-like) and T (toggle, MOX-like) are tagged PttSource::Mox. That's the pragmatic choice to match the working button, and you've already called out PttSource::Footswitch wiring as deliberate follow-up in the scope note — just flagging that the source label on interlock notifications for space PTT will read as MOX. Fine to leave as-is.
Thanks for the thorough writeup and the bridge-gap reporting — the deferred-unkey timing table is a clever way to prove the coordinator path runs end-to-end. LGTM.
🤖 aethersdr-agent · cost: $4.2290 · model: claude-opus-4-8
There was a problem hiding this comment.
Reviewed the change against the surrounding TX path — this is correct, minimal, and well-scoped. Thanks for the careful writeup, @jensenpat.
Correctness — verified against the codebase
The strongest argument for this fix is that the keyboard paths now call byte-identical code to the already-shipping GUI MOX button (TxApplet.cpp:374-376 → requestPttOn/Off(PttSource::Mox)). I traced the gating claims and they hold:
- Preflight interlock preserved — the coordinator's preflight is wired to
localPttInterlockMessage(RadioModel.cpp:457), the same check the old inlinesetTransmit()ran (RadioModel.cpp:1445). - xmit gating preserved —
requestPttOn/Offterminate insetMox()/dispatchMoxOff()→commandReady("xmit 0/1"), and thecommandReadyhandler (RadioModel.cpp:492-511) applies the samearmInterlockNotification/m_txRequested/txAudioGate-teardown gating. It additionally runstransmitStartBlockedByInhibit, which the old inline keyboard path skipped — so this is a small consistency gain with the MOX button, not a regression. - Toggle logic equivalent —
mox_toggle'sisTransmitting() ? Off : Onmatches the priorsetTransmit(!isTransmitting()). - No null-pointer / RAII / settings concerns;
m_quindarToneis null-checked inside the coordinator, andtransmitModel()returns a member reference.
Conventions / scope
Single file, signal-routing only, no FlexLib/protocol change — matches the stated scope. Comments are clear and reference the issue.
One behavioral note (not blocking)
With QUIN enabled on a phone mode, requestPttOff now defers the unkey by the outro window (~0.5 s) for space-bar PTT too. That's intended and matches the MOX button, but it does mean releasing Space no longer unkeys instantly in that one configuration. Given the deferred-unkey is exactly the Quindar behavior the issue asks for — and is consistent across all GUI PTT sources now — I think that's the right call; just flagging it explicitly for the record.
The deliberate follow-up scoping (MIDI/RC-28/FlexControl/footswitch, and keeping PttSource::Dax off the deferred unkey) is the right boundary. LGTM.
🤖 aethersdr-agent · cost: $2.2069 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Traced the full TX-keying path and verified the redirect is safe by equivalence — space-bar/mox_toggle now call the exact requestPttOn/Off(PttSource::Mox) the production MOX button uses.
Confirmed no gating is lost: requestPttOn → setMox → emit commandReady("xmit") → RadioModel's commandReady handler (RadioModel.cpp:472), which applies the same interlock-arm / m_txRequested / txAudioGate-teardown / sendCmd as the old inline setTransmit, plus an extra transmitStartBlockedByInhibit gate, plus the local interlock preflight (runPttPreflight). With QUIN off, requestPttOff falls straight through to an immediate dispatchMoxOff() — zero behavior change when Quindar is disabled; the tones/deferral only run with QUIN on, which is the fix.
Nice empirical proof of the coordinator over real hardware (deferred unkey 23ms QUIN-off vs 535ms QUIN-on via the MOX button, since the bridge can't inject a raw Space key event). Good scoping: other client PTT sources deferred (need hardware), and PttSource::Dax correctly excluded from the deferred unkey. Thanks @jensenpat.
Issue
#3610 — Quindar tones fire for the GUI MOX button but not for space-bar PTT or the keyboard MOX/TX shortcut (
T). The radio keys and mic audio is sent; only the Quindar intro/outro is missing. Confirmed by the reporter on a real station (QUIN enabled, USB/phone mode).Root cause
There were two divergent PTT paths and only one drove the Quindar state machine:
TxApplet.cpp) and TCI hardware PTT (TciServer.cpp) callTransmitModel::requestPttOn/Off()— the single coordinator that sequences the tone around the MOX edge (startIntro()before key-up;startOutro()with the unkey deferred by the outro duration).mox_toggleshortcut (MainWindow_Shortcuts.cpp) calledRadioModel::setTransmit()→TransmitModel::setTransmitting(), the raw state setter that emitsxmit 0/1andmoxChangedbut never touches the Quindar coordinator.So with QUIN enabled on a phone mode, keyboard PTT keyed the radio with no tone, while the MOX button worked — exactly the reported behaviour. (
PttSource::Footswitchwas defined but never wired, consistent with the reporter's read that this routing was intended but unfinished.)Fix
Point the space-bar event-filter path and the
mox_toggleshortcut atrequestPttOn/Off(PttSource::Mox)— the same coordinator the working MOX button uses. Minimal, signal-routing only; no FlexLib protocol change.No gating is lost by the redirect:
requestPttOn/Offstill terminate in anxmit 0/1command, andRadioModel'sxmithandler applies the identical interlock-arm /m_txRequested/txAudioGate-teardown gating thatsetTransmit()did inline. The coordinator's preflight runs the same local interlock check (localPttInterlockMessage). The only added behaviour is the Quindar intro/outro.How the agent automation bridge proved it
The fix makes the keyboard path call the identical
requestPttOff(PttSource::Mox)the MOX button calls. The MOX button is reachable from the bridge, so I exercised that exact coordinator path on live hardware (FLEX-8400M, TX slice on LSB, TX antenna ANT1 dummy load — re-confirmed from live state before keying; force-unkeyed and verifiedtransmitting == falseafter each cycle).The coordinator's observable, bridge-readable effect is the deferred unkey:
requestPttOffholds the radio keyed for the outro window before sendingxmit 0, soget transmit → transmittingstaystrueafter the unkey command. Drivinginvoke MOX transmit setCheckedwhile pollingtransmitting:transmitting:falseThe ~0.5 s deferral with QUIN on vs. immediate with QUIN off confirms the Quindar outro sequencing runs on this build over real hardware. Since the patched space-bar /
mox_togglepaths now call that samerequestPttOff(PttSource::Mox), they inherit that behaviour — the previously-silent keyboard PTT now drives the full intro/outro.Agent automation bridge — gaps found
A direct empirical "press the physical Space bar and watch Quindar" check was not possible through the bridge. Surfacing these so we can close them:
invoketargets named widgets / visible menu actions; the space-bar PTT is an application-levelQKeyEventfilter andmox_toggleis a globalQShortcut— neither is reachable, so the bridge can't drive the exact code path this fix changes. (Drove the equivalentrequestPttOffvia the MOX button instead.)quindarphase/active field in anygetsnapshot andClientQuindarTonehas no log category. The only proxy is the deferred-unkey timing; the tone itself and thequindarActiveChangedchip flash can't be read. Proposed: addquindarPhase/quindarActiveto thetransmitsnapshot, or anlcQuindarlog line on intro/outro start.dumpTree. The chip is an unnamed checkableQPushButton, so the bridge can't read or toggle Quindar state. Proposed: give it anaccessibleName.AETHER_AUTOMATIONinstances share one fixedQLocalServername + one discovery file (<temp>/aethersdr-automation.json) andremoveServer()each other's socket; they also share the settings file and log dir. Worked around withAETHER_AUTOMATION_SOCKET=<unique>and a direct socket connect. Proposed: make the discovery file / socket name per-PID, or index the discovery file by pid.Scope note
Other client-handled PTT sources share this root cause (MIDI, RC-28, FlexControl MOX, the serial footswitch —
PttSource::Footswitch's intended home). They're left as a deliberate follow-up: they need their respective hardware to verify, and CAT/rigctl/TCI-digital paths (PttSource::Dax) should not get the deferred unkey (programmatic, timing-sensitive). As the reporter noted, PTT commanded directly to the radio over FlexLib with no client involvement (external footswitch box) can't get a true client-generated outro — worth a line in the QUIN editor dialog.💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat