fix(cat): match missing Hamlib rigctld functionality — VFO mode, mode mapping, CTCSS/FM tones, levels & funcs#3619
Conversation
Close the gaps between AetherSDR's rigctld emulation and what real Hamlib
clients (WSJT-X, fldigi, pat/Winlink, MacLoggerDX) expect. Triggered by pat
refusing to load AetherSDR ("rigctl is not running in VFO mode"), which prompted
a full audit against Hamlib's tests/rigctl_parse.c and rigs/network/netrigctl.c.
VFO mode:
- chk_vfo=1; set_vfo_opt no-op; dump_state targetable_vfo=0x3 (FREQ|MODE),
ptt_type=1 (RIG_PTT_RIG). Dynamic get_vfo_list (VFOB only when split active).
- Single VFO-prefix resolver takeVfoPrefix() used by every VFO-sensitive command
(freq/mode/level/func/ptt/ctcss/ts/ant/vfo_op/split), replacing divergent
inline strippers. VFOB/SUB → TX slice or RPRT -8 (never silently redirected to
VFOA); VFOMEM → -8. Slice-specific levels act on the resolved slice.
Split via a targetable TX VFO (WSJT-X "Rig" split):
- set_freq/set_mode VFOB (and set_split_freq/mode/freq_mode) now strip the VFO
prefix and enable split on demand via ensureSplitTxSlice() — addressing the TX
VFO directly works without a preceding set_split_vfo, honouring targetable_vfo.
- All split-path SliceModel writes queued onto the GUI thread (QueuedConnection);
read-only VFOB resolution has no side effects.
Mode mapping:
- Hamlib CWR → Flex CW (Flex has no per-slice CW-reverse; sideband is global).
set_split_mode uses the canonical smartsdrToHamlib/hamlibToSmartSDR tables.
CTCSS / FM / funcs / levels:
- CTCSS TX tone wired to fmToneValue/fmToneMode; CTCSS RX squelch and DCS return
RPRT -8 (unsupported) instead of lying. AGC level read/write wired to agcMode.
func TONE/TSQL handling; get/set func and level masks corrected.
Response format:
- Bare-mode getters match Hamlib 4.7.1 reference rigctld command-by-command:
get_lock_mode and hamlib_version keep the RPRT 0 terminator (omitting it
reintroduced the aethersdr#3115 WSJT-X 20s startup stall); other getters are value-only.
- quit/halt/Q acknowledge with RPRT 0 for a clean Hamlib session close.
Tests: rigctld_test extended to 208 cases (--ptt --cw); includes regression
tests for every VFO-prefix path, the CWR mapping, and targetable VFOB split.
Validated live on a FlexRadio (Mac), cross-checked on RPi 5 and Win 11, and
smoke-tested with WSJT-X (FT8 + Rig split) and fldigi (bare-mode).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks for this, @K5PTB — this is an exceptionally thorough and well-documented PR. The decision to audit Hamlib's rigctl_parse.c/netrigctl.c source rather than docs shows, and the per-area writeup made review tractable for a change this size. All 6 CI checks are green (build, macOS, Windows, CodeQL, accessibility, analyze). I traced the major paths and found no blocking issues.
Things I verified and liked:
- The single
takeVfoPrefix()→sliceForVfo()resolver replacing five drifted inline strippers is a real correctness win, and the "never silently redirect VFOB→VFOA" rule (nullptr →-8) is applied consistently in the freq/mode/level/func/ant/ts paths. - AGC round-trips correctly:
getmaps fast→2/slow→3/med→5/off→0 andsetinverts those exact values; the N:1 collapse for Hamlib's superfast/user/auto is documented, not accidental. - Thread-safety fix is sound — the four previously-direct split-path writes (
tryPromoteTxSlice,cmdSetSplitFreq,cmdSetSplitMode, the disable path) are nowQt::QueuedConnection, and recordingm_pendingTxSlicesynchronously sofindTxSlice()resolves across the async gap is the right pattern.findTxSlice(promote=false)keeping reads side-effect-free is a nice touch. quit/halt/Qreturningrprt(0): confirmedCatPort::onClientDatanever closes the socket itself on quit (the client drives the disconnect viaonRigctlDisconnected), so the added ack is harmless and strictly better — no behavior depended on the old empty return.
Minor (non-blocking):
get_ctcss_toneignores the VFO prefix whileset_ctcss_tonehonors it. The dispatcher callscmdGetCtcssTone()with no args (RigctlProtocol.cpp:547), and that handler readscurrentSlice()directly — soget_ctcss_tone VFOBreturns VFOA's tone instead of-8, whereasset_ctcss_tone VFOB …correctly routes throughtakeVfoPrefix(). This is the same silent-redirect-to-VFOA case the PR is otherwise careful to reject. Low impact (CTCSS-on-VFOB is niche), but for symmetry you may wantcmdGetCtcssTone(const QString& vfo = {})mirroringcmdGetFreq/cmdGetMode.
One thing to keep an eye on (not a change request):
- The bare-mode terminator narrowing is the highest-risk part of the change since it's pinned to empirical behavior of Hamlib 4.7.1's reference
rigctld. Keeping theRPRT 0onget_lock_modeandhamlib_versioncorrectly guards the #3115 WSJT-X startup stall, and your test 13.x coverage locks the framing in — so if a future Hamlib version diverges, the suite should catch it. Just flagging that this is the surface most sensitive to upstream variance.
Nothing here blocks merge from my view — the asymmetry above is the only concrete code nit, and it's cosmetic. Nice work, and thanks for the detailed test matrix.
🤖 aethersdr-agent · cost: $5.4679 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Approving — a careful, source-audited correctness pass on the rigctld emulation. Verified the high-risk pieces:
- VFO resolver (
sliceForVfo/takeVfoPrefix) correctly returnsnullptrfor VFOB-no-split and VFOMEM (never silently redirecting to VFOA), every caller mapsnullptr → RPRT -8, and it cleanly replaces the five drifted inline strippers. - Thread-safety is right: the previously-direct split-path writes are now queued (
QMetaObject::invokeMethod, CAT thread → GUI-thread SliceModel), withm_pendingTxSlicerecorded synchronously to bridge the async gap sofindTxSlice()resolves before the queuedsetTxSlicelands.ensureSplitTxSlice()is a clean idempotent factor. - Targetable-VFO split auto-enable computes
wantsTxVfobefore stripping the token and validates before routing — correct for WSJT-X "Rig" split. CWR → CW(wasCWR → CWL→ coerced to PKTUSB) — documented as deliberately lossy on read-back since Flex has no per-slice CW-reverse.- Bare-mode terminators: the standout — narrows #3120's over-generalization back to reference behavior while keeping
get_lock_mode/hamlib_versionterminated to preserve the #3115 20s-stall fix. Exactly the regression a careless cleanup would have re-broken; it's explicitly guarded. - Honest
-8for unsupported DCS / RX-CTCSS instead of faking success.
208/208 rigctld_test + live WSJT-X/fldigi/pat + reference-verified against Hamlib 4.7.1, cross-platform, CI green across all six. Excellent work @K5PTB.
…ethersdr#3619) Reconciles the split-VFO work onto upstream's reworked rigctld (aethersdr#3619), which landed independently after this work was branched. aethersdr#3619 added the rigctld create/promote machinery (ensureSplitTxSlice, findTxSlice(promote), VFO-prefix front door) but no slice cleanup at all — a created split slice was left hanging on every disable and on client disconnect. This layers our full cleanup + the SmartCat split behavior on top. SmartCatProtocol (Flex/TS-2000 dialects): - Two-mechanism split mirroring SmartSDR-for-Windows: reuse a configured VFO B slice, else create a dedicated TX slice when there's room, else XIT-offset fallback on slice A. RX frequency never moves. - Config-driven (not OS-driven): single-VFO port auto-creates; dual-VFO port reuses the configured VFO B, and returns NOT_ENABLED ("?;") when that VFO B is configured but its slice is absent (matches SmartSDR-Mac). - FR/ZZFR per the SmartSDR CAT spec (ZZFR unsupported; FR gated on a real VFO B; no invented VFO A/B swap). - Disconnect cleanup + idempotent enable/disable polls. RigctlProtocol (on top of aethersdr#3619): - Close a split TX slice we created: on split-disable and in a new destructor (disconnect). aethersdr#3619 created it but never removed it. - Pointer-snapshot promotion (m_preSplitSlices) so a reused slice id on a rapid disable→enable isn't mistaken for pre-existing → no id-reuse orphan. - Forced TX reclaim onto the RX slice when removing the created slice, so a TX slice always exists after teardown (get_split_freq_mode never -1). - enable→disable race fix (m_removeCreatedSliceWhenItAppears): a disable arriving before the async addSlice() materializes no longer abandons the create — the slice is removed when it appears. GUI: - CAT config: VFO B selector forced to "-" and disabled for the rigctld dialect (single-VFO per port; no VFO B concept). - Guard the per-session DAX-restore 300 ms timer with QPointer (UAF: a CAT split slice created then closed inside the window dangled a raw pointer). Tests: - --sections N[,N...] filter on CAT_Flex_test / CAT_TS-2000_test (default: all) for per-section slice-leak isolation — the tooling that pinned the enable→disable orphan. - Single-vs-dual VFO auto-detection; "?;" error-response handling. Verified on MacBook Air M2 against a running AetherSDR (FLEX-6500): rigctld_test (aethersdr#3619 suite) 180/180; CAT_Flex_test 121/111; CAT_TS-2000_test 99/98; full-batch slice balance 4 create/4 remove, no orphan (was 4/3 before the cleanup). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aethersdr#3619's rigctld_test verifies split disables (Split: 0) but not the teardown outcome. 5.9c asserts that after set_split_vfo 0 the TX slice we created is removed and TX is reclaimed onto the RX slice — so get_split_freq_mode still succeeds (RPRT 0, never -1) and reports the RX frequency (TX == RX). Guards both fixes layered on the rigctld rework: the stale-cache reclaim skip that left no TX slice, and the created split slice orphaned at disable. Polls for the reclaim to settle (setTxSlice + slice-remove are queued/async), matching the surrounding split assertions, so it isn't timing-flaky. The physical orphan itself is not CAT-observable; this asserts the visible contract that proves the reclaim + removal ran in the right order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves #3620
Summary
This PR closes the gaps between AetherSDR's
rigctldemulation and what realHamlib clients (WSJT-X, fldigi, pat/Winlink, MacLoggerDX, …) actually expect.
The immediate trigger was pat refusing to load AetherSDR as a rig:
AetherSDR was reporting
chk_vfo → 0; pat and WSJT-X requirechk_vfo=1(VFO-prefix mode). That one symptom prompted a full audit of Hamlib's
tests/rigctl_parse.candrigs/network/netrigctl.c(source read, not docs) tofind every command gap, wrong response, or mode that silently mis-mapped. The
result is a broad correctness pass across the rigctld surface — not just VFO
mode but mode mapping, CTCSS/FM tones, AGC, PTT, func masks,
dump_state,bare-mode response framing, and thread-safety of slice mutations.
Validated by the in-tree
rigctld_testsuite (208/208 with--ptt --cw,live against a radio on Mac; cross-checked on RPi 5 and Win 11) plus live smoke
tests with WSJT-X (FT8, extended mode, and "Rig" split — confirmed creating a
slice on demand) and fldigi (connect + freq/mode sync, bare mode). Bare-mode
terminator behaviour was verified command-by-command against Hamlib 4.7.1's
reference
rigctld(dummy rig).Changes by area
VFO mode — core fix
chk_vfonow returns1(was0); extended-mode response updated toVFO Mode: 1.set_vfo_optaccepted as no-op — VFO-prefix mode is always active, no switching needed.dump_statetargetable_vfoupdated from0→0x3(FREQ|MODE targetable by VFO prefix, as required bynetrigctl_open).dump_stateptt_typeupdated from0→1(RIG_PTT_RIG — CAT PTT is functional via theTcommand).VFO-prefix command routing — single resolver
A single front door,
takeVfoPrefix(parts) → SliceModel*, handles the VFO tokenfor every VFO-sensitive command. It strips a leading VFO name if present and
resolves the slice it addresses; with no prefix it returns the port's bound
slice. It is built on
sliceForVfo(vfo):VFOA/MAIN→ current RX slice;VFOB/SUB→ TX slice when split is active, elsenullptr;VFOMEM→nullptr(no per-slice memory VFO on Flex).nullptrmaps toRPRT -8(RIG_ENAVAIL) — an absent VFOB or VFOMEM is never silently redirected to
VFOA, which would return wrong data or tune the wrong slice.
Routed through the resolver (replacing five divergent inline strippers that had
drifted — some handled
MAIN/SUB, some didn't):get_freq/set_freq,get_mode/set_mode,get_level/set_level,get_func/set_func,set_ptt,set_ctcss_tone,set_ts,set_ant,vfo_op,set_split_freq/set_split_mode/set_split_freq_mode(and their short forms). Slice-specificlevels (AGC/AF/RF/SQL/APF/NR/NB/STRENGTH) now act on the resolved slice, so
get_level VFOB AGCreads the TX slice instead of silently falling back to VFOA.Split via a targetable TX VFO (WSJT-X "Rig" split)
We advertise
targetable_vfo = FREQ|MODE, which tells Hamlib it may set a VFO'sfreq/mode by addressing it directly. WSJT-X "Rig" split relies on this — it sets
the TX VFO with
set_freq VFOB <hz>/set_mode VFOB <mode>(and theset_split_*forms) without a precedingset_split_vfo. AetherSDR's slicemodel only has a second slice when split is active, so those calls used to fail
(
RPRT -8/-1, surfaced as "Protocol error" / "Invalid parameter settingsplit TX frequency and mode"). Now a write that targets
VFOB/SUBenablessplit on demand via
ensureSplitTxSlice()(promote an existing second slice,or create one with deferred promotion), honouring the advertised capability.
Reads (
get_freq/get_mode VFOB) are unchanged — still-8with no split, andwith no side effects. Verified live: WSJT-X Rig split works (creating a slice
when needed) and fldigi is unaffected.
Mode-name mapping — CW-reverse fixed
FlexRadio has a single CW slice mode (
CW); CW sideband is a global radiopreference, not a per-slice mode — there is no
CWL/CWRslice mode. The modetable previously mapped Hamlib
CWR ↔ Flex CWL, soset_mode CWRsent theradio an invalid
CWLwhich it silently coerced (observed:→ PKTUSB). Thetable now maps Hamlib
CWR → Flex CW(and the defensiveCWL → CW). This islossy on read-back — a client that sets
CWRreads backCW— but correct,since Flex cannot represent per-slice CW-reverse.
RTTYR → RTTYandWFM → FMwere already valid. Applies to plain
set_modeandset_split_modealike (bothnow go through the canonical
smartsdrToHamlib/hamlibToSmartSDRtables ratherthan a hand-rolled subset).
Thread safety — slice mutations off the CAT thread
The CAT protocol runs on the socket thread while
SliceModellives on the GUIthread, so all model mutations must go through
QMetaObject::invokeMethod(..., Qt::QueuedConnection). Four split-path writes were calling setters directly(
tryPromoteTxSlice,cmdSetSplitFreq,cmdSetSplitMode, and thecmdSetSplitVfodisable path) — now all queued. The disable path also recordsm_pendingTxSlicesynchronously sofindTxSlice()resolves correctly across theasync gap. Read-only resolution (
get_freq/get_mode VFOB) no longer drives thedeferred split-promotion state machine: a query has no side effects.
Dynamic
get_vfo_listPreviously hardcoded
"VFOA VFOB". Now returns"VFOA VFOB"only when a distinct TX slice exists (split active), otherwise"VFOA". Applies to both extended and bare modes.CTCSS TX encode — wired to hardware
get_ctcss_tone/set_ctcss_tonenow read and writeSliceModel::fmToneValue().1000= 100.0 Hz)"100.0")hamlib = round(flex_hz × 10)and inverseget_func TONE/set_func TONEwired toSliceModel::fmToneMode():TONE=1→setFmToneMode("ctcss_tx");TONE=0→setFmToneMode("off")CTCSS RX squelch — honest error
Flex radios have no RX CTCSS squelch (TX-only hardware). Previously
set_ctcss_sqlsilently returnedRPRT 0(success lie).get_ctcss_sql→0(truthful: no RX CTCSS active)set_ctcss_sql→RPRT -8(RIG_ENAVAIL: not supported)get_func TSQL→0(always off — truthful)set_func TSQL→RPRT -8TSQL is included in
get_func ?(readable — always 0) but excluded fromset_func ?(not settable). The masks and token lists are split accordingly.DCS — rejected at protocol layer
DCS is entirely unsupported on Flex. Previously
get_dcs_codereturned0(silent stub) andset_dcs_codereturnedRPRT 0(success lie).get_dcs_code,set_dcs_code,get_dcs_sql,set_dcs_sql→ all returnRPRT -8d/Dupdated to matchAGC level — wired to hardware
get_level AGC/set_level AGCnow read and writeSliceModel::agcMode().Hamlib
RIG_AGC_*↔ FlexagcModemapping:"off""fast""slow""med"kRigLevelAgc(bit 6) added to bothkRigGetLevelMaskandkRigSetLevelMask."AGC"added to bothrigGetLevelTokens()andrigSetLevelTokens().Protocol response format fixes — bare-mode terminators (reference-verified)
Bare-mode getter framing was aligned to match Hamlib's reference
rigctldexactly. This was verified empirically against Hamlib 4.7.1 reference
rigctld(dummy rig, model 1) command-by-command — the behavior is command-specific,
not uniform:
RPRT 0(
RPRTis for setter success and errors). The spuriousrprt(0)is removedfrom the bare-mode path of:
chk_vfo,get_powerstat,get_vfo_list,get_modes,get_rptr_shift,get_rptr_offs,get_ctcss_tone,get_split_freq_mode. (Extended-mode path unchanged throughout.)get_lock_modeandhamlib_versionkeep the trailingRPRT 0— thereference daemon terminates these two with
RPRT 0in bare mode(
\get_lock_mode→0\nRPRT 0\n). Omitting it onget_lock_modereintroduces the measured 20-second WSJT-X startup stall fixed in [cat] Fix WSJT-X startup CAT lock-mode timeout #3115
(Hamlib's NET backend blocks waiting for the terminator after that probe).
This supersedes #3120, which had over-generalized #3115's
get_lock_modefixby adding the terminator to every bare getter — a divergence from reference
rigctld. The audit narrows it back to the two commands the reference daemonactually terminates.
Quit / halt handling
quitnow returnsrprt(0)before disconnecting so Hamlib gets a clean acknowledgement. Previously returned empty string, causing some clients to log a read error on disconnect.haltsame treatment — returnsrprt(0)(was empty string).Q(uppercase quit, short-form) added to the short-form switch — was previously returningRPRT -4(invalid command).Files changed
src/core/RigctlProtocol.htakeVfoPrefix(),ensureSplitTxSlice(),findTxSlice(bool promote),cmdGetCtcssTone()/cmdSetCtcssTone();sliceForVfo()src/core/RigctlProtocol.cpptests/rigctld_test.cppNew and updated tests
1b.5chk_vforeturnsVFO Mode: 11b.5bset_vfo_optreturnsRPRT 01b.7get_vfo_list(no split) contains VFOA and not VFOB2.6–2.8get_freq VFOA/set_freq VFOAround-trip2.8bget_freq VFOBwith no split returnsRPRT -82.8c–2.8dget_freq/set_freq VFOMEMreturnRPRT -8(no memory-VFO slice)3.11–3.13get_mode VFOA/set_mode VFOAround-trip3.14set_mode CWRreads backCW(valid Flex mode, not coerced PKTUSB)4.3–4.4vfo_op UP/DOWNstep move (tolerant of radio step quantization)4.5get_vfo_info VFOA— 5 fields present4.6get_vfo_info VFOB(no split) returnsRPRT -86.4c–6.4dget_ptt/set_ptt5.7e–5.7gset_split_freq/set_split_mode/set_split_freq_mode5.11set_freq/set_mode VFOBwith split off auto-enables split (RPRT 0, not-8)7.11–7.12get_level VFOA STRENGTH/set_level VFOA AF7.13–7.14get_level AGC/set_level AGC 3round-trip8.11–8.15get_func TONE/set_func TONEround-trip;get_func TSQL= 0;set_func TSQL= -8; VFO-prefixedget_func/set_func VFOA TONE15.6bset_ctcss_tone VFOA 1000round-trip13.10,13.13get_lock_mode/hamlib_versionreturn value +RPRT 0(matches reference rigctld; guards the #3115 WSJT-X stall)13.11–13.12,13.14–13.20chk_vfo,get_powerstat,get_vfo_list,get_modes,get_rptr_shift,get_rptr_offs,get_ctcss_tone,get_dcs_code(→ -8),get_split_freq_mode15.5–15.6get_ctcss_tone/set_ctcss_tone 1000real round-trip (was stub check for 0)15.9–15.12get_ctcss_sql= 0,set_ctcss_sql= -8;get_dcs_sql= -8,set_dcs_sql= -8What is not changed
No previously-working command is regressed. All changes are either:
return rprt(0)) with honest errors (return rprt(-8)) for genuinely unsupported hardwarerigctldexactly (see above), keeping theRPRT 0terminator on the two getters the reference daemon terminatesPREAMP,ATT, andNOTCHFlevels remain unimplemented — Flex doesn't expose them over the TCP API.Built and smoke tested on
rigctld_test208/208 (--ptt --cw, 1 skip: band-edge step clamp), live against a FlexRadio on 20 m HF; WSJT-X (FT8 + Rig split) and fldigi (bare mode) confirmed working.--ptt --cwexercised; PTY tests skip — not available on Windows).🤖 Generated with Claude Code