fix(cat): bare ZZTX; is a read, not a key — uncommanded TX in Flex dialect (#3625)#3629
Conversation
…lect) In the Flex (ZZ-extension) dialect, the ZZTX handler routed every form except ZZTX0 to cmdTX(), so a parameter-less "ZZTX;" — which is a READ of the transmit state — fell through to setTransmit(true) and keyed the radio. External apps poll TX status with bare ZZTX; once per second, so each poll caused an uncommanded transmit (logs: 30 ZZTX polls -> 30 xmit 1, 1:1). TS-2000 dialect is unaffected because it does not implement ZZTX (returns ?;). Handle the bare/"?" read in the ZZTX dispatch, returning ZZTX0;/ZZTX1; from the same authoritative state IF/ZZIF report (isRadioTransmitting()); only ZZTX0/1/2 reach cmdTX() to change state. Plain Kenwood TX; keying is untouched. Reworks CAT_Flex_test section 10: adds regression checks that bare ZZTX; reads without keying (10.0r/10.0r2) and reflects the keyed state (10.1r); keying now uses the explicit ZZTX1. Verified live: 116/116 with --ptt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit of every SmartCatProtocol handler for the same defect class as the ZZTX bug — a bare/parameter-less READ falling through to a state change — turned up two more, neither exercised by the reporting app but reachable by any client that polls them: - ZZFR: bare "ZZFR;" unconditionally swapped VFO A<->B every call. - ZZFT: bare "ZZFT;" unconditionally toggled split every call. Both now follow the Kenwood/Flex GET/SET convention used by their twins (cmdFT, cmdZZSW): a bare or "?" arg reads current state; an explicit 0/1 sets it. ZZFR gains an m_rxVfoB selector so RX-VFO select is idempotent and the read is truthful (swaps the slice mapping only when the selection changes). All other handlers were verified to already guard the read path. Reworks CAT_Flex_test: 13.34r (bare ZZFR; reads without swapping) + reworked 13.34 (explicit ZZFR1/ZZFR0 select); 8.7-8.9 (bare ZZFT; reads without toggling; ZZFT1/ZZFT0 set, consistent with ZZSW). Verified live: 120/120 --ptt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ten9876
left a comment
There was a problem hiding this comment.
Approving — this is a correct, well-scoped, and genuinely well-tested fix for a real and serious bug. Thanks for the thorough writeup and especially the audit. Notes below are minor / non-blocking.
The bug and fix check out
Confirmed cmdTX() keys on any argument except "0" — including an empty one — so a bare ZZTX; poll really did setTransmit(true), exactly as your logs show. Intercepting the empty/? read before it reaches cmdTX() is the right fix, and returning state from isRadioTransmitting() keeps the ZZTX0/1 reply consistent with the same source the IF/ZZIF report uses. ✅
The part I most wanted to verify — and it's safe
The substantive change is cmdZZFR adding m_rxVfoB next to the existing std::swap(m_vfoA, m_vfoB). New parallel state beside existing state is the classic place a desync bug hides, so I checked: m_vfoA/m_vfoB are swapped in exactly one place in the file — cmdZZFR itself — and read-only everywhere else; cmdZZSW doesn't touch the mapping. Because m_rxVfoB is written in the same function as the only swap, the two can't drift out of sync, and the if (wantB != m_rxVfoB) idempotency guard is correct. 👍
Minor notes (non-blocking)
ZZFTset-path doesn't validate —m_splitEnabled = (arg == "1")silently treats any non-1value (e.g.ZZFT2) as "off", while your newZZFRis stricter and returns?;for invalid input. It's defensible as-is because it matches the twinscmdFTandcmdZZSW, so this is really just an observation about the asymmetry withZZFRrather than a request to change anything.- Built on macOS only — noted in the PR, and it's pure-
QStringCAT parsing with no platform specifics (I rebuiltmainon Linux clean), so CI covers the rest. Just flagging since the project bar is cross-platform.
What's especially good
- The audit commit is the right instinct done well: finding
ZZTXand immediately sweeping both dialects' dispatch tables for the same defect class, catchingZZFRandZZFT, and keeping it as a droppable separate commit. - The regression tests assert both the read result and the absence of the side effect (10.0r2 re-checks
ZZIF TX='0'after a delay to prove no key; 13.34r confirmsZZFAis unchanged after a bareZZFR). That's exactly how you pin a "read must not mutate" contract. - Correctly leaves plain Kenwood
TX;keying untouched.
High-severity fix (uncommanded emissions), correctly resolved. 🚀
Resolves #3625
Summary
In the Flex (ZZ-extension) CAT dialect, a parameter-less
ZZTX;— which is a read of the transmit state — was parsed as a set and keyed the transmitter. External applications poll TX status withZZTX;roughly once per second, so every poll caused an uncommanded transmission.Root cause
ZZTXdispatched to the sharedcmdTX(), which keys on any argument except0. A bareZZTX;arrives with an empty argument, doesn't match"0", and falls through tosetTransmit(true)→xmit 1.Per the Kenwood/Flex GET/SET convention,
ZZTX;(no parameter) is a read that must returnZZTX0;/ZZTX1;; onlyZZTX1/ZZTX2should key.Evidence (from reporter debug logs, Flex vs TS-2000)
ZZTXpolls → 30xmit 1, exact 1:1.ZZTX34 times → answered?;(the dialect doesn't implement the ZZ extension) → 0xmit. The bug is specific to the Flex dialect'sZZTXhandler.Severity: high — any Flex-dialect client that polls TX status continuously keys the transmitter (unintended emissions; possible interference / PA stress).
Fix (commit 1)
Handle the bare/
?read in theZZTXdispatch, returning the same authoritative stateIF/ZZIFreport (isRadioTransmitting()); onlyZZTX0/1/2reachcmdTX(). Plain KenwoodTX;keying is deliberately untouched (it is a set command in TS-2000).Audit (commit 2 — kept separate)
Finding a defect like this, the responsible step is to check for siblings. I audited every handler in
SmartCatProtocol(both the Flex/ZZ and TS-2000/base dispatch tables) for the same class — a bare/parameter-less read falling through to a state change:Found two more, both in the Flex (ZZ-extension) dialect:
ZZFR;— unconditionally swapped VFO A↔B on every call.ZZFT;— unconditionally toggled split on every call.Both now follow the GET/SET convention used by their twins (
cmdFT,cmdZZSW): bare/?reads, explicit0/1sets.ZZFRgains an idempotent RX-VFO selector.TS-2000 dialect: none. Read guards live inside the shared handlers, so they protect both dialects; the only intentional divergence is
TX;keying, which is correct Kenwood behavior.This is the second commit so a reviewer who prefers to scope the PR to the reported bug can drop the audit commit independently.
Testing
Verified live against a FlexRadio with
CAT_Flex_test --ptt(keys the rig into a dummy load):10.0r/10.0r2/10.1r(ZZTX read vs key),13.34r/13.34(ZZFR read vs select),8.7–8.9(ZZFT read vs set).Built and tested on MacBook Air M2 (macOS) only. Not built on Linux/Windows for this change — it's a small, platform-agnostic CAT parsing fix.
🤖 Generated with Claude Code