Skip to content

[cat] Fix WSJT-X startup CAT lock-mode timeout#3115

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:aether/wsjtx-startup-frequency-delay
May 25, 2026
Merged

[cat] Fix WSJT-X startup CAT lock-mode timeout#3115
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:aether/wsjtx-startup-frequency-delay

Conversation

@jensenpat

@jensenpat jensenpat commented May 25, 2026

Copy link
Copy Markdown
Collaborator

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:

0
RPRT 0

instead of only:

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:

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:

/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:

/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:

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

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:

/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:

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:

/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:

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>
@jensenpat jensenpat marked this pull request as ready for review May 25, 2026 05:43
@jensenpat jensenpat requested review from a team as code owners May 25, 2026 05:43
@jensenpat jensenpat changed the title Fix WSJT-X startup CAT lock-mode timeout [cat] Fix WSJT-X startup CAT lock-mode timeout May 25, 2026

@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 @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 returns 0\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

@ten9876 ten9876 merged commit b3eaded into aethersdr:main May 25, 2026
5 checks passed
@ten9876

ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Claude here — merged as b3eaded5. Thank you Pat, this is a model bug report: the dual pre/post-fix logpull capture with millisecond timestamps showed unambiguously that the 20-second silence was on Hamlib's side waiting for RPRT, not AetherSDR queueing the QSY. Matching it against HamlibTransceiver.cpp:377-378 to confirm the RIG_MODE_PKTUSB ↔ DIG_U mapping closed the loop on root cause.

Narrow scope (only \get_lock_mode) is the right call — fix the observed stall, don't expand the protocol-compatibility surface speculatively. We've filed a follow-up to audit the other bare-mode getters (chk_vfo, get_powerstat, hamlib_version, etc.) for Hamlib-spec compliance: #3120. If you see similar startup stalls on other commands from fldigi / JTDX / Log4OM testing, that's the issue to attach traces to.

73, Jeremy KK7GWY & Claude (AI dev partner)

aethersdr-agent Bot added a commit that referenced this pull request May 25, 2026
…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>
K5PTB added a commit to K5PTB/AetherSDR that referenced this pull request Jun 15, 2026
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>
K5PTB added a commit to K5PTB/AetherSDR that referenced this pull request Jun 15, 2026
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>
ten9876 pushed a commit that referenced this pull request Jun 19, 2026
… 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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
## 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>
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.

2 participants