Skip to content

fix(cat): match missing Hamlib rigctld functionality — VFO mode, mode mapping, CTCSS/FM tones, levels & funcs#3619

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
K5PTB:fix/match-hamlib-functions
Jun 19, 2026
Merged

fix(cat): match missing Hamlib rigctld functionality — VFO mode, mode mapping, CTCSS/FM tones, levels & funcs#3619
ten9876 merged 1 commit into
aethersdr:mainfrom
K5PTB:fix/match-hamlib-functions

Conversation

@K5PTB

@K5PTB K5PTB commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Resolves #3620

Summary

This PR closes the gaps between AetherSDR's rigctld emulation and what real
Hamlib clients (WSJT-X, fldigi, pat/Winlink, MacLoggerDX, …) actually expect.
The immediate trigger was pat refusing to load AetherSDR as a rig:

Cannot load rig 'AetherSDR': Unable to select VFO: rigctl is not running in VFO mode

AetherSDR was reporting chk_vfo → 0; pat and WSJT-X require chk_vfo=1
(VFO-prefix mode). That one symptom prompted a full audit of Hamlib's
tests/rigctl_parse.c and rigs/network/netrigctl.c (source read, not docs) to
find 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_test suite (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_vfo now returns 1 (was 0); extended-mode response updated to VFO Mode: 1.
  • set_vfo_opt accepted as no-op — VFO-prefix mode is always active, no switching needed.
  • dump_state targetable_vfo updated from 00x3 (FREQ|MODE targetable by VFO prefix, as required by netrigctl_open).
  • dump_state ptt_type updated from 01 (RIG_PTT_RIG — CAT PTT is functional via the T command).

VFO-prefix command routing — single resolver

A single front door, takeVfoPrefix(parts) → SliceModel*, handles the VFO token
for 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, else nullptr; VFOMEM
nullptr (no per-slice memory VFO on Flex). nullptr maps to RPRT -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-specific
levels (AGC/AF/RF/SQL/APF/NR/NB/STRENGTH) now act on the resolved slice, so
get_level VFOB AGC reads 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's
freq/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 the
set_split_* forms) without a preceding set_split_vfo. AetherSDR's slice
model only has a second slice when split is active, so those calls used to fail
(RPRT -8 / -1, surfaced as "Protocol error" / "Invalid parameter setting
split TX frequency and mode"). Now a write that targets VFOB/SUB enables
split 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 -8 with no split, and
with 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 radio
preference, not a per-slice mode — there is no CWL/CWR slice mode. The mode
table previously mapped Hamlib CWR ↔ Flex CWL, so set_mode CWR sent the
radio an invalid CWL which it silently coerced (observed: → PKTUSB). The
table now maps Hamlib CWR → Flex CW (and the defensive CWL → CW). This is
lossy on read-back — a client that sets CWR reads back CW — but correct,
since Flex cannot represent per-slice CW-reverse. RTTYR → RTTY and WFM → FM
were already valid. Applies to plain set_mode and set_split_mode alike (both
now go through the canonical smartsdrToHamlib/hamlibToSmartSDR tables rather
than a hand-rolled subset).

Thread safety — slice mutations off the CAT thread

The CAT protocol runs on the socket thread while SliceModel lives on the GUI
thread, so all model mutations must go through QMetaObject::invokeMethod(..., Qt::QueuedConnection). Four split-path writes were calling setters directly
(tryPromoteTxSlice, cmdSetSplitFreq, cmdSetSplitMode, and the
cmdSetSplitVfo disable path) — now all queued. The disable path also records
m_pendingTxSlice synchronously so findTxSlice() resolves correctly across the
async gap. Read-only resolution (get_freq/get_mode VFOB) no longer drives the
deferred split-promotion state machine: a query has no side effects.

Dynamic get_vfo_list

Previously 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_tone now read and write SliceModel::fmToneValue().

  • Hamlib unit: tenths-of-Hz integer (e.g. 1000 = 100.0 Hz)
  • Flex unit: float Hz string ("100.0")
  • Conversion: hamlib = round(flex_hz × 10) and inverse

get_func TONE / set_func TONE wired to SliceModel::fmToneMode():

  • TONE=1setFmToneMode("ctcss_tx") ; TONE=0setFmToneMode("off")

CTCSS RX squelch — honest error

Flex radios have no RX CTCSS squelch (TX-only hardware). Previously set_ctcss_sql silently returned RPRT 0 (success lie).

  • get_ctcss_sql0 (truthful: no RX CTCSS active)
  • set_ctcss_sqlRPRT -8 (RIG_ENAVAIL: not supported)
  • get_func TSQL0 (always off — truthful)
  • set_func TSQLRPRT -8

TSQL is included in get_func ? (readable — always 0) but excluded from set_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_code returned 0 (silent stub) and set_dcs_code returned RPRT 0 (success lie).

  • get_dcs_code, set_dcs_code, get_dcs_sql, set_dcs_sql → all return RPRT -8
  • Short-form d/D updated to match

AGC level — wired to hardware

get_level AGC / set_level AGC now read and write SliceModel::agcMode().

Hamlib RIG_AGC_* ↔ Flex agcMode mapping:

Hamlib value Flex mode
0 "off"
1–2 "fast"
3 "slow"
4–6 "med"

kRigLevelAgc (bit 6) added to both kRigGetLevelMask and kRigSetLevelMask. "AGC" added to both rigGetLevelTokens() and rigSetLevelTokens().

Protocol response format fixes — bare-mode terminators (reference-verified)

Bare-mode getter framing was aligned to match Hamlib's reference rigctld
exactly. 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
:

  • Most getters return the value line(s) only, with no trailing RPRT 0
    (RPRT is for setter success and errors). The spurious rprt(0) is removed
    from 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_mode and hamlib_version keep the trailing RPRT 0 — the
    reference daemon terminates these two with RPRT 0 in bare mode
    (\get_lock_mode0\nRPRT 0\n). Omitting it on get_lock_mode
    reintroduces 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_mode fix
by adding the terminator to every bare getter — a divergence from reference
rigctld. The audit narrows it back to the two commands the reference daemon
actually terminates.

Quit / halt handling

  • quit now returns rprt(0) before disconnecting so Hamlib gets a clean acknowledgement. Previously returned empty string, causing some clients to log a read error on disconnect.
  • halt same treatment — returns rprt(0) (was empty string).
  • Q (uppercase quit, short-form) added to the short-form switch — was previously returning RPRT -4 (invalid command).

Files changed

File What changed
src/core/RigctlProtocol.h Declarations for takeVfoPrefix(), ensureSplitTxSlice(), findTxSlice(bool promote), cmdGetCtcssTone()/cmdSetCtcssTone(); sliceForVfo()
src/core/RigctlProtocol.cpp All protocol changes above
tests/rigctld_test.cpp New/updated live tests — see below

New and updated tests

Test What it verifies
1b.5 chk_vfo returns VFO Mode: 1
1b.5b set_vfo_opt returns RPRT 0
1b.7 get_vfo_list (no split) contains VFOA and not VFOB
2.6–2.8 VFO-prefixed get_freq VFOA / set_freq VFOA round-trip
2.8b get_freq VFOB with no split returns RPRT -8
2.8c–2.8d get_freq/set_freq VFOMEM return RPRT -8 (no memory-VFO slice)
3.11–3.13 VFO-prefixed get_mode VFOA / set_mode VFOA round-trip
3.14 set_mode CWR reads back CW (valid Flex mode, not coerced PKTUSB)
4.3–4.4 vfo_op UP/DOWN step move (tolerant of radio step quantization)
4.5 get_vfo_info VFOA — 5 fields present
4.6 get_vfo_info VFOB (no split) returns RPRT -8
6.4c–6.4d VFO-prefixed get_ptt / set_ptt
5.7e–5.7g split-mode round-trip via canonical table; VFOB/VFOA levels resolve to independent slices; VFO-prefixed set_split_freq/set_split_mode/set_split_freq_mode
5.11 set_freq/set_mode VFOB with split off auto-enables split (RPRT 0, not -8)
7.11–7.12 VFO-prefixed get_level VFOA STRENGTH / set_level VFOA AF
7.13–7.14 get_level AGC / set_level AGC 3 round-trip
8.11–8.15 get_func TONE/set_func TONE round-trip; get_func TSQL = 0; set_func TSQL = -8; VFO-prefixed get_func/set_func VFOA TONE
15.6b VFO-prefixed set_ctcss_tone VFOA 1000 round-trip
13.10, 13.13 Bare-mode get_lock_mode / hamlib_version return value + RPRT 0 (matches reference rigctld; guards the #3115 WSJT-X stall)
13.11–13.12, 13.14–13.20 Bare-mode value-only (no terminator) for chk_vfo, get_powerstat, get_vfo_list, get_modes, get_rptr_shift, get_rptr_offs, get_ctcss_tone, get_dcs_code (→ -8), get_split_freq_mode
15.5–15.6 get_ctcss_tone / set_ctcss_tone 1000 real round-trip (was stub check for 0)
15.9–15.12 get_ctcss_sql = 0, set_ctcss_sql = -8; get_dcs_sql = -8, set_dcs_sql = -8

What is not changed

No previously-working command is regressed. All changes are either:

  • Adding VFO-prefix handling to commands that ignored it
  • Replacing silent stubs (return rprt(0)) with honest errors (return rprt(-8)) for genuinely unsupported hardware
  • Aligning bare-mode getter framing to match Hamlib reference rigctld exactly (see above), keeping the RPRT 0 terminator on the two getters the reference daemon terminates

PREAMP, ATT, and NOTCHF levels remain unimplemented — Flex doesn't expose them over the TCP API.


Built and smoke tested on

  • MacBook Air M2rigctld_test 208/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.
  • RPi 5 (Debian trixie) — release + debug builds, suite green.
  • Win 11 (i3) — release build, suite green (--ptt --cw exercised; PTY tests skip — not available on Windows).

🤖 Generated with Claude Code

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>

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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: get maps fast→2/slow→3/med→5/off→0 and set inverts 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 now Qt::QueuedConnection, and recording m_pendingTxSlice synchronously so findTxSlice() resolves across the async gap is the right pattern. findTxSlice(promote=false) keeping reads side-effect-free is a nice touch.
  • quit/halt/Q returning rprt(0): confirmed CatPort::onClientData never closes the socket itself on quit (the client drives the disconnect via onRigctlDisconnected), so the added ack is harmless and strictly better — no behavior depended on the old empty return.

Minor (non-blocking):

  • get_ctcss_tone ignores the VFO prefix while set_ctcss_tone honors it. The dispatcher calls cmdGetCtcssTone() with no args (RigctlProtocol.cpp:547), and that handler reads currentSlice() directly — so get_ctcss_tone VFOB returns VFOA's tone instead of -8, whereas set_ctcss_tone VFOB … correctly routes through takeVfoPrefix(). 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 want cmdGetCtcssTone(const QString& vfo = {}) mirroring cmdGetFreq/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 the RPRT 0 on get_lock_mode and hamlib_version correctly 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 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving — a careful, source-audited correctness pass on the rigctld emulation. Verified the high-risk pieces:

  • VFO resolver (sliceForVfo/takeVfoPrefix) correctly returns nullptr for VFOB-no-split and VFOMEM (never silently redirecting to VFOA), every caller maps nullptr → 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), with m_pendingTxSlice recorded synchronously to bridge the async gap so findTxSlice() resolves before the queued setTxSlice lands. ensureSplitTxSlice() is a clean idempotent factor.
  • Targetable-VFO split auto-enable computes wantsTxVfo before stripping the token and validates before routing — correct for WSJT-X "Rig" split.
  • CWR → CW (was CWR → 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_version terminated to preserve the #3115 20s-stall fix. Exactly the regression a careless cleanup would have re-broken; it's explicitly guarded.
  • Honest -8 for 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.

@ten9876 ten9876 merged commit 36c62ea into aethersdr:main Jun 19, 2026
6 checks passed
K5PTB added a commit to K5PTB/AetherSDR that referenced this pull request Jun 20, 2026
…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>
K5PTB added a commit to K5PTB/AetherSDR that referenced this pull request Jun 20, 2026
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>
@K5PTB K5PTB deleted the fix/match-hamlib-functions branch June 22, 2026 11:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rigctld: emulation diverges from Hamlib client expectations — VFO mode, split, CW-reverse, CTCSS/AGC, bare-mode framing

2 participants