[cat] Fix WSJT-X startup CAT lock-mode timeout#3115
Conversation
Co-authored-by: Codex <noreply@openai.com>
There was a problem hiding this comment.
Thanks @jensenpat — really thorough write-up. The root-cause analysis with the pre/post log timestamps showing the 20s gap collapse to ~30-50ms is exactly the kind of evidence that makes this easy to evaluate.
The fix itself looks good:
RigctlProtocol.cpp:372-376— minimal, well-commented change. Extended mode behavior preserved, bare mode now returns0\nRPRT 0.tests/rigctld_test.cpp:1317-1323— regression test asserts the exact two-line response on a fresh non-extended connection, which matches the WSJT-X/fldigi connection pattern documented in the section-13 header comment. Skip counter bumped from 9 to 10 correctly.
One observation, not a blocker: the existing bare-mode handlers right above this in RigctlProtocol.cpp (chk_vfo at line 346-350, get_powerstat at 354-359, hamlib_version at 384-388) all still return "value\n" with no RPRT 0 terminator. The narrow scoping is defensible — you only reproduced the stall on get_lock_mode, and broadening it could regress simple scripts that expect just the value — but it leaves the protocol response shape inconsistent across these bare-mode getters. Worth keeping in mind if similar stalls show up against another long-form probe; the fix would be a one-liner per command.
Logs/timing validation and the explicit pre/post comparison table are exemplary. Nice work.
🤖 aethersdr-agent · cost: $5.1107 · model: claude-opus-4-7
|
Claude here — merged as Narrow scope (only 73, Jeremy KK7GWY & Claude (AI dev partner) |
…ciple I. Hamlib's NET rigctl reference appends RPRT N to every response in both bare and extended modes. AetherSDR's bare-mode long-form getters omitted the terminator on 10 commands (chk_vfo, get_powerstat, hamlib_version, get_split_freq_mode, get_vfo_list, get_modes, get_rptr_shift, get_rptr_offs, get_ctcss_tone, get_dcs_code). Clients other than WSJT-X — fldigi, JTDX, hamlib-cli, etc. — may stall waiting for the missing terminator the same way WSJT-X stalled on get_lock_mode in #3115. Mechanical sweep: each bare branch now returns the value lines plus rprt(0), mirroring the Hamlib reference exactly. Extended-mode branches already carried the terminator and are unchanged. Short-form single-character getters are left alone — they follow a different convention that omits RPRT (see cmdGetFreq and siblings). Each new bare-mode terminator gets a regression assertion in tests/rigctld_test.cpp section 13 (13.11–13.20), following the 13.10 / #3115 pattern of asserting the exact two-line {value, "RPRT 0"} response. get_split_freq_mode is 4 lines bare (freq, mode, passband, RPRT 0) and accepts the alternate "RPRT -1" shape when no TX slice exists. Principle I (FlexLib / upstream-protocol authority): Hamlib's NET rigctl backend is the reference for this on-wire protocol; the bare-mode omission was a divergence from that reference. Restoring conformance is the load-bearing principle. Blast radius: codegraph impact on processCommand reports risk_score=0.104 with 5 high-risk downstream model accessors (RadioModel::transmitModel/sendCmd/isConnected/sendCmdPublic, SliceModel::sliceId). These are read by the cmd handlers that the unchanged branches dispatch to; the edits here are purely textual additions to return values and do not touch handler bodies or any downstream model state. The PreToolUse file-level hook flagged risk_score=0.182 with MainWindow as a transitive caller via header inclusion — same conclusion: text-only return-value change, no behavioural ripple. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bare-mode terminator behavior is command-specific, verified against Hamlib 4.7.1 reference rigctld (dummy rig): get_lock_mode and hamlib_version are terminated with RPRT 0, while other getters return value-only. The branch's blanket removal of bare-mode RPRT 0 had reintroduced the 20-second WSJT-X startup stall fixed in aethersdr#3115 (Hamlib's NET backend blocks waiting for the get_lock_mode terminator). Restore RPRT 0 on those two getters only; the removal on the other 8 stays (they match reference). Tests 13.10 / 13.13 updated to assert value + RPRT 0. Resolves code-review finding aethersdr#1. Full suite: 191/191 (--ptt --cw), 2 skips. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
… mapping, CTCSS/FM tones, levels & funcs (#3619) 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 `0` → `0x3` (FREQ|MODE targetable by VFO prefix, as required by `netrigctl_open`). - **`dump_state` `ptt_type`** updated from `0` → `1` (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=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_sql` silently returned `RPRT 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 -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_mode` → `0\nRPRT 0\n`). Omitting it on `get_lock_mode` reintroduces the measured 20-second WSJT-X startup stall fixed in #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 M2** — `rigctld_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](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary Fixes a WSJT-X startup CAT stall where Hamlib would stop issuing queued frequency/mode work for roughly 20 seconds after probing `\get_lock_mode` through AetherSDR's rigctld server. The actual QSY command was not being received by AetherSDR until after Hamlib's timeout expired. This made the bug look like AetherSDR was buffering or delaying a frequency change after WSJT-X connected, especially when the active VFO started in `USB` and WSJT-X needed to move it into `PKTUSB`/Flex `DIGU`. This PR makes the bare long-form `\get_lock_mode` response include the Hamlib status terminator that the NET rigctl backend expects: ```text 0 RPRT 0 ``` instead of only: ```text 0 ``` ## Root Cause WSJT-X/Hamlib sends a long-form bare `\get_lock_mode` command during startup. AetherSDR returned the lock value (`0`) but did not terminate that response with `RPRT 0` unless the command was in extended mode. The latest pre-fix log made the timing visible: - WSJT-X startup CAT sequence began normally. - AetherSDR responded to `\get_lock_mode` with only `"0"`. - Hamlib stopped sending new CAT commands for about 20 seconds. - WSJT-X then sent `M PKTUSB -1`; AetherSDR switched the Flex slice from `USB` to `DIGU` and enabled DAX TX. - AetherSDR again responded to `\get_lock_mode` with only `"0"`. - Hamlib stopped sending new CAT commands for about another 20 seconds. - The queued frequency change finally arrived after the second timeout. The key pre-fix timing from `aethersdr-20260524-221826.log` was: | Time | Event | |---|---| | `22:18:36.914` | `F 14043100.000000 -> RPRT 0` | | `22:18:36.914` | `\get_lock_mode -> "0"` | | `22:18:56.933` | next CAT command after ~20s silence | | `22:18:56.934` | `M PKTUSB -1 -> RPRT 0` and Flex slice mode set to `DIGU` | | `22:18:56.971` | `\get_lock_mode -> "0"` | | `22:19:17.149` | queued `F 7074000.000000 -> RPRT 0` finally arrives | That made the issue protocol-level rather than DAX/audio-level: AetherSDR could not act on the QSY because Hamlib had not delivered it yet. ## Changes ### `RigctlProtocol` Bare `\get_lock_mode` now returns the value plus status terminator: ```text 0 RPRT 0 ``` Extended-mode behavior is unchanged and continues to return the labeled response plus `RPRT 0`. This is intentionally scoped to `get_lock_mode`, because that command is a long-form Hamlib startup probe and the observed stall is specifically tied to the missing terminator there. ### `rigctld_test` Adds a regression check in the bare-response section: - sends raw `\get_lock_mode` - reads two lines - verifies the exact response is `0`, `RPRT 0` The skip accounting for section 13 was updated from 9 to 10 checks. ## Source References ### AetherSDR rigctld server and protocol path The server path is straightforward and was useful for proving that the delay was upstream of AetherSDR command execution: - `src/core/RigctlServer.cpp:158` calls `cs.protocol->handleLine(line)` for each newline-delimited rigctld command. - `src/core/RigctlServer.cpp:159-162` logs the command and response, then writes the response bytes to the socket. - The CAT log line therefore represents a command AetherSDR has actually received, processed, and responded to. The absence of CAT log lines after pre-fix `\get_lock_mode` showed that Hamlib was not sending more commands during the stall. The fixed protocol branch is: - `src/core/RigctlProtocol.cpp:366` enters the `get_lock_mode` handler. - `src/core/RigctlProtocol.cpp:370-371` preserves extended-mode behavior: labeled value plus `RPRT 0`. - `src/core/RigctlProtocol.cpp:372-376` now handles the bare long-form Hamlib startup probe by returning the value and the status terminator. The regression coverage is in the bare/non-extended rigctld test path: - `tests/rigctld_test.cpp:1238-1241` documents that section 13 uses a fresh connection that never sends `+`, matching the bare protocol path used by WSJT-X/fldigi-style clients. - `tests/rigctld_test.cpp:1250-1253` updates skip accounting for the added check. - `tests/rigctld_test.cpp:1317-1323` sends raw `\get_lock_mode`, reads two lines, and verifies the exact response is `0` followed by `RPRT 0`. ### WSJT-X / Hamlib source path inspected Local WSJT-X source inspected from: ```text /Users/patj/Documents/wsjtx-improved/Transceiver/HamlibTransceiver.cpp ``` Relevant source references: - `HamlibTransceiver.cpp:377-378` maps Hamlib `RIG_MODE_PKTUSB` back to WSJT-X `DIG_U`. - `HamlibTransceiver.cpp:403-404` maps WSJT-X `DIG_U` to Hamlib `RIG_MODE_PKTUSB`; this is why AetherSDR sees `M PKTUSB -1` during the USB-to-digital startup transition. - `HamlibTransceiver.cpp:644` opens the Hamlib rig connection with `rig_open(...)`. - `HamlibTransceiver.cpp:659-660` notes that the NET rigctl backend advertises broad capability support, so WSJT-X/Hamlib does active startup probing. - `HamlibTransceiver.cpp:829-862` performs the startup frequency-resolution probe by setting and reading back small test frequency offsets, matching the observed `F ...955`, readback, then restore pattern in the AetherSDR logs. - `HamlibTransceiver.cpp:915-929` shows the RX frequency path: WSJT-X/Hamlib sets frequency, reads current mode, and conditionally sets the mapped mode. - `HamlibTransceiver.cpp:996-1009` shows the transmit/split-side path also sets frequency, reads mode, and conditionally sets the mapped mode. The specific `\get_lock_mode` command appears to be emitted by Hamlib's NET rigctl backend during its capability/startup probing rather than being a direct WSJT-X source-level call. The local WSJT-X source still explains the surrounding command sequence observed in the AetherSDR logs: connection open, frequency resolution probe, current mode read as USB, mode mapping to PKTUSB/DIG_U, then queued QSY. ## Post-Fix Validation From Logs After deploying the build, the latest three test-session logs show the timeout gone. Each session now returns `0\nRPRT 0`, and the following mode/QSY commands continue within milliseconds rather than after a 20-second timeout. Pulled with the `logpull` skill to: ```text /private/tmp/aethersdr-logpull/20260524-223649 ``` Remote logs inspected: - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223315.log` - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223353.log` - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223429.log` Post-fix startup timing: | Log | `\get_lock_mode` response | Next `M PKTUSB` | Startup QSY | |---|---:|---:|---:| | `223315` | `22:33:33.802` -> `0\nRPRT 0` | `+27ms` | `14.074` at `+127ms` | | `223353` | `22:34:03.145` -> `0\nRPRT 0` | `+51ms` | `14.074` at `+134ms` | | `223429` | `22:34:36.967` -> `0\nRPRT 0` | same timestamp | `14.074` at `+128ms` | Representative fixed sequence from `aethersdr-20260524-223429.log`: ```text 22:34:36.944 F 14082900.000000 -> RPRT 0 22:34:36.967 \get_lock_mode -> 0\nRPRT 0 22:34:36.967 m -> USB\n2700 22:34:36.967 M PKTUSB -1 -> RPRT 0 22:34:36.967 slice set 0 mode=DIGU 22:34:36.969 transmit set dax=1 22:34:37.095 F 14074000.000000 -> RPRT 0 ``` No 20-second CAT silence after `\get_lock_mode` was present in the last three sessions. ## Audio / DAX Checks The pulled post-fix logs also showed normal audio summary logging: | Log | startup | RX | TX | CW | aux | failure | category | |---|---|---|---|---|---|---|---| | `aethersdr-20260524-223315.log` | yes | yes | no | yes | yes | no | yes | | `aethersdr-20260524-223353.log` | yes | yes | no | yes | yes | no | yes | | `aethersdr-20260524-223429.log` | yes | yes | no | yes | yes | no | yes | Notes: - Missing TX source summary is expected for these sessions because TX mic capture was not started. - CW sidetone summaries were present. - Auxiliary sink summaries were present. - No audio open failure summaries were present. - RX sink used `MacBook Air Speakers`, `48000Hz`, `Float`, `resampling=yes`, `fallback=no`. - CW PortAudio matched `MacBook Air Speakers`, `fallback=no`. - Auxiliary sink had `resampling=no`, `fallback=no`. ## Build / Test ### Commands run ```bash git diff --check env -u CPLUS_INCLUDE_PATH cmake --build build --target rigctld_test -j22 env -u CPLUS_INCLUDE_PATH cmake --build build -j22 ``` Results: - `git diff --check` passed. - `rigctld_test` target built successfully. - Full `AetherSDR.app` build completed successfully. - Fresh app deployed to `patj@mac.jensencloud.net:/Users/patj/Desktop/AetherSDR.app` and quarantine was cleared. Build environment note: - The local shell had `CPLUS_INCLUDE_PATH` pointing at CommandLineTools libc++ headers, which mixed CLT C++ headers with the Xcode SDK C headers and broke unrelated C++ compilation. - Unsetting `CPLUS_INCLUDE_PATH` for the build gave a consistent Xcode SDK/toolchain view and allowed the full build to complete. ### Detailed validation matrix | Area | What was checked | Result | |---|---|---| | Diff hygiene | `git diff --check` | passed | | Focused build | `env -u CPLUS_INCLUDE_PATH cmake --build build --target rigctld_test -j22` | passed; no work after final rebuild | | Full app build | `env -u CPLUS_INCLUDE_PATH cmake --build build -j22` | passed; `AetherSDR.app` linked successfully | | Deployment | `deploy-aethersdr-test-build` skill with built `build/AetherSDR.app` | uploaded to test Mac Desktop and cleared quarantine | | Pre-fix logs | pulled latest logs before the fix with `logpull --count 5` | reproduced two CAT silences after bare `\get_lock_mode -> "0"` | | Post-fix logs | pulled latest logs after deploy with `logpull --count 3` | no CAT silence after `\get_lock_mode`; follow-on commands arrived in 0-51ms | | Audio/DAX sanity | scanned summary/failure/fallback/sample-rate indicators in pulled logs | no audio open failures; DAX/audio summaries normal for exercised paths | ### Pre-fix log pull Pulled to: ```text /private/tmp/aethersdr-logpull/20260524-222034 ``` Remote logs captured: - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-221826.log` - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-221703.log` - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-221547.log` - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-220001.log` - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-194032.log` The key failing session was `aethersdr-20260524-221826.log`. It showed: ```text 22:18:36.914 F 14043100.000000 -> RPRT 0 22:18:36.914 \get_lock_mode -> "0" 22:18:56.933 next CAT command after roughly 20 seconds 22:18:56.934 M PKTUSB -1 -> RPRT 0 22:18:56.934 slice set 0 mode=DIGU 22:18:56.965 F 14043100.000000 -> RPRT 0 22:18:56.971 \get_lock_mode -> "0" 22:19:17.021 next CAT command after roughly 20 seconds 22:19:17.149 F 7074000.000000 -> RPRT 0 ``` That sequence proved that AetherSDR was not delaying a command it had already received. The actual QSY did not appear in the rigctld logs until after Hamlib recovered from the missing terminator. ### Post-fix log pull Pulled to: ```text /private/tmp/aethersdr-logpull/20260524-223649 ``` Remote logs captured: - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223315.log` - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223353.log` - `/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223429.log` The last three sessions all showed the intended response and immediate continuation: | Log | Lock probe | Follow-on behavior | |---|---|---| | `223315` | `22:33:33.802 \get_lock_mode -> 0\nRPRT 0` | `M PKTUSB` at `22:33:33.829`, `F 14074000` at `22:33:33.929` | | `223353` | `22:34:03.145 \get_lock_mode -> 0\nRPRT 0` | `M PKTUSB` at `22:34:03.196`, `F 14074000` at `22:34:03.279` | | `223429` | `22:34:36.967 \get_lock_mode -> 0\nRPRT 0` | `M PKTUSB` at `22:34:36.967`, `F 14074000` at `22:34:37.095` | The newest session also later accepted additional QSY traffic while normal polling continued: ```text 22:34:49.786 F 7074000.000000 -> RPRT 0 22:34:50.495 F 14074000.000000 -> RPRT 0 ``` ### Audio summary validation The logpull validation table for the post-fix logs was: | Log | startup | RX | TX | CW | aux | failure | category | |---|---|---|---|---|---|---|---| | `aethersdr-20260524-223315.log` | yes | yes | no | yes | yes | no | yes | | `aethersdr-20260524-223353.log` | yes | yes | no | yes | yes | no | yes | | `aethersdr-20260524-223429.log` | yes | yes | no | yes | yes | no | yes | Audio/DAX observations: - TX source summary was absent in these logs because TX mic capture was not started during the sessions. - CW sidetone summary was present in all three post-fix sessions. - Auxiliary sink summary was present in all three post-fix sessions. - No `Audio open failure summary` appeared. - RX sink summary showed `MacBook Air Speakers`, `48000Hz`, `Float`, `resampling=yes`, `fallback=no`. - CW sidetone PortAudio matched `MacBook Air Speakers`, `fallback=no`. - Auxiliary sink summary showed `Quindar local sink`, `48000Hz`, `Float`, `resampling=no`, `fallback=no`. ## Documentation No docs were updated because this is a narrow rigctld protocol compatibility fix. It does not change AetherSDR's core audio pipeline, DAX transport behavior, or user-facing configuration. 👨🏼💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat Co-authored-by: Codex <noreply@openai.com>
Summary
Fixes a WSJT-X startup CAT stall where Hamlib would stop issuing queued frequency/mode work for roughly 20 seconds after probing
\get_lock_modethrough AetherSDR's rigctld server.The actual QSY command was not being received by AetherSDR until after Hamlib's timeout expired. This made the bug look like AetherSDR was buffering or delaying a frequency change after WSJT-X connected, especially when the active VFO started in
USBand WSJT-X needed to move it intoPKTUSB/FlexDIGU.This PR makes the bare long-form
\get_lock_moderesponse include the Hamlib status terminator that the NET rigctl backend expects:instead of only:
Root Cause
WSJT-X/Hamlib sends a long-form bare
\get_lock_modecommand during startup. AetherSDR returned the lock value (0) but did not terminate that response withRPRT 0unless the command was in extended mode.The latest pre-fix log made the timing visible:
\get_lock_modewith only"0".M PKTUSB -1; AetherSDR switched the Flex slice fromUSBtoDIGUand enabled DAX TX.\get_lock_modewith only"0".The key pre-fix timing from
aethersdr-20260524-221826.logwas:22:18:36.914F 14043100.000000 -> RPRT 022:18:36.914\get_lock_mode -> "0"22:18:56.93322:18:56.934M PKTUSB -1 -> RPRT 0and Flex slice mode set toDIGU22:18:56.971\get_lock_mode -> "0"22:19:17.149F 7074000.000000 -> RPRT 0finally arrivesThat made the issue protocol-level rather than DAX/audio-level: AetherSDR could not act on the QSY because Hamlib had not delivered it yet.
Changes
RigctlProtocolBare
\get_lock_modenow returns the value plus status terminator:Extended-mode behavior is unchanged and continues to return the labeled response plus
RPRT 0.This is intentionally scoped to
get_lock_mode, because that command is a long-form Hamlib startup probe and the observed stall is specifically tied to the missing terminator there.rigctld_testAdds a regression check in the bare-response section:
\get_lock_mode0,RPRT 0The skip accounting for section 13 was updated from 9 to 10 checks.
Source References
AetherSDR rigctld server and protocol path
The server path is straightforward and was useful for proving that the delay was upstream of AetherSDR command execution:
src/core/RigctlServer.cpp:158callscs.protocol->handleLine(line)for each newline-delimited rigctld command.src/core/RigctlServer.cpp:159-162logs the command and response, then writes the response bytes to the socket.\get_lock_modeshowed that Hamlib was not sending more commands during the stall.The fixed protocol branch is:
src/core/RigctlProtocol.cpp:366enters theget_lock_modehandler.src/core/RigctlProtocol.cpp:370-371preserves extended-mode behavior: labeled value plusRPRT 0.src/core/RigctlProtocol.cpp:372-376now handles the bare long-form Hamlib startup probe by returning the value and the status terminator.The regression coverage is in the bare/non-extended rigctld test path:
tests/rigctld_test.cpp:1238-1241documents that section 13 uses a fresh connection that never sends+, matching the bare protocol path used by WSJT-X/fldigi-style clients.tests/rigctld_test.cpp:1250-1253updates skip accounting for the added check.tests/rigctld_test.cpp:1317-1323sends raw\get_lock_mode, reads two lines, and verifies the exact response is0followed byRPRT 0.WSJT-X / Hamlib source path inspected
Local WSJT-X source inspected from:
Relevant source references:
HamlibTransceiver.cpp:377-378maps HamlibRIG_MODE_PKTUSBback to WSJT-XDIG_U.HamlibTransceiver.cpp:403-404maps WSJT-XDIG_Uto HamlibRIG_MODE_PKTUSB; this is why AetherSDR seesM PKTUSB -1during the USB-to-digital startup transition.HamlibTransceiver.cpp:644opens the Hamlib rig connection withrig_open(...).HamlibTransceiver.cpp:659-660notes that the NET rigctl backend advertises broad capability support, so WSJT-X/Hamlib does active startup probing.HamlibTransceiver.cpp:829-862performs the startup frequency-resolution probe by setting and reading back small test frequency offsets, matching the observedF ...955, readback, then restore pattern in the AetherSDR logs.HamlibTransceiver.cpp:915-929shows the RX frequency path: WSJT-X/Hamlib sets frequency, reads current mode, and conditionally sets the mapped mode.HamlibTransceiver.cpp:996-1009shows the transmit/split-side path also sets frequency, reads mode, and conditionally sets the mapped mode.The specific
\get_lock_modecommand appears to be emitted by Hamlib's NET rigctl backend during its capability/startup probing rather than being a direct WSJT-X source-level call. The local WSJT-X source still explains the surrounding command sequence observed in the AetherSDR logs: connection open, frequency resolution probe, current mode read as USB, mode mapping to PKTUSB/DIG_U, then queued QSY.Post-Fix Validation From Logs
After deploying the build, the latest three test-session logs show the timeout gone. Each session now returns
0\nRPRT 0, and the following mode/QSY commands continue within milliseconds rather than after a 20-second timeout.Pulled with the
logpullskill to:Remote logs inspected:
/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223315.log/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223353.log/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223429.logPost-fix startup timing:
\get_lock_moderesponseM PKTUSB22331522:33:33.802->0\nRPRT 0+27ms14.074at+127ms22335322:34:03.145->0\nRPRT 0+51ms14.074at+134ms22342922:34:36.967->0\nRPRT 014.074at+128msRepresentative fixed sequence from
aethersdr-20260524-223429.log:No 20-second CAT silence after
\get_lock_modewas present in the last three sessions.Audio / DAX Checks
The pulled post-fix logs also showed normal audio summary logging:
aethersdr-20260524-223315.logaethersdr-20260524-223353.logaethersdr-20260524-223429.logNotes:
MacBook Air Speakers,48000Hz,Float,resampling=yes,fallback=no.MacBook Air Speakers,fallback=no.resampling=no,fallback=no.Build / Test
Commands run
Results:
git diff --checkpassed.rigctld_testtarget built successfully.AetherSDR.appbuild completed successfully.patj@mac.jensencloud.net:/Users/patj/Desktop/AetherSDR.appand quarantine was cleared.Build environment note:
CPLUS_INCLUDE_PATHpointing at CommandLineTools libc++ headers, which mixed CLT C++ headers with the Xcode SDK C headers and broke unrelated C++ compilation.CPLUS_INCLUDE_PATHfor the build gave a consistent Xcode SDK/toolchain view and allowed the full build to complete.Detailed validation matrix
git diff --checkenv -u CPLUS_INCLUDE_PATH cmake --build build --target rigctld_test -j22env -u CPLUS_INCLUDE_PATH cmake --build build -j22AetherSDR.applinked successfullydeploy-aethersdr-test-buildskill with builtbuild/AetherSDR.applogpull --count 5\get_lock_mode -> "0"logpull --count 3\get_lock_mode; follow-on commands arrived in 0-51msPre-fix log pull
Pulled to:
Remote logs captured:
/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-221826.log/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-221703.log/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-221547.log/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-220001.log/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-194032.logThe key failing session was
aethersdr-20260524-221826.log. It showed:That sequence proved that AetherSDR was not delaying a command it had already received. The actual QSY did not appear in the rigctld logs until after Hamlib recovered from the missing terminator.
Post-fix log pull
Pulled to:
Remote logs captured:
/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223315.log/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223353.log/Users/patj/Library/Preferences/AetherSDR/aethersdr-20260524-223429.logThe last three sessions all showed the intended response and immediate continuation:
22331522:33:33.802 \get_lock_mode -> 0\nRPRT 0M PKTUSBat22:33:33.829,F 14074000at22:33:33.92922335322:34:03.145 \get_lock_mode -> 0\nRPRT 0M PKTUSBat22:34:03.196,F 14074000at22:34:03.27922342922:34:36.967 \get_lock_mode -> 0\nRPRT 0M PKTUSBat22:34:36.967,F 14074000at22:34:37.095The newest session also later accepted additional QSY traffic while normal polling continued:
Audio summary validation
The logpull validation table for the post-fix logs was:
aethersdr-20260524-223315.logaethersdr-20260524-223353.logaethersdr-20260524-223429.logAudio/DAX observations:
Audio open failure summaryappeared.MacBook Air Speakers,48000Hz,Float,resampling=yes,fallback=no.MacBook Air Speakers,fallback=no.Quindar local sink,48000Hz,Float,resampling=no,fallback=no.Documentation
No docs were updated because this is a narrow rigctld protocol compatibility fix. It does not change AetherSDR's core audio pipeline, DAX transport behavior, or user-facing configuration.
👨🏼💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat