Skip to content

feat(rigctld): full Hamlib NET rigctl implementation with 149-test suite#2975

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
K5PTB:feat/rigctld-extended-commands
May 23, 2026
Merged

feat(rigctld): full Hamlib NET rigctl implementation with 149-test suite#2975
ten9876 merged 2 commits into
aethersdr:mainfrom
K5PTB:feat/rigctld-extended-commands

Conversation

@K5PTB

@K5PTB K5PTB commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements complete Hamlib NET rigctl (rigctld) emulation in AetherSDR, enabling bidirectional rig control from MacLoggerDX, WSJT-X, fldigi, and any other Hamlib-compatible logging or digital-mode software. Closes #2379.

Command coverage was verified against the Hamlib source (tests/rigctl_parse.c and src/rig.c) to ensure no gaps. All short-form character mappings were audited and corrected against the canonical Hamlib table.

What's implemented

  • Extended-mode (+) and bare-mode responses; short-form single-character commands — TCP and PTY paths share the same RigctlProtocol handler
  • Frequency, mode, VFO, split VFO (two-slice and single-slice deferred-promotion paths), PTT, RIT/XIT, antenna, tuning step, levels (AF/RF/SQL/RFPOWER/STRENGTH/KEYSPD), functions (NB/NR/ANF/VOX/MUTE/FBKIN)
  • CW/Morse: send_morse (inline + two-line Not1MM form), stop_morse, set_level KEYSPD; wait_morse returns RPRT -4 — CWX queue depth is not queryable on a FlexRadio, so an honest error is better than a false "done"
  • dump_state (positional Hamlib v1 format), get_info, get_rig_info, chk_vfo, get/set_trn, get_dcd, power2mW/mW2power
  • FM/repeater stubs: get/set_rptr_shift/offs, get/set_ctcss_tone/dcs_code
  • Hamlib init probes: get/set_powerstat, get/set_lock_mode, set_vfo_opt, hamlib_version, get_vfo_list, get_modes
  • set_powerstat 0 and set_lock_mode 1 return RPRT -1 — honest about what a network-connected FlexRadio cannot do

Bug fixes found during integration testing

Bug Symptom Fix
set_mode ? missing guard MacLoggerDX capability probe clobbered mode to USB on every connect Return capability list early before mode conversion
Split race (two-slice) findTxSlice() returned stale slice A after setTxSlice(true) was queued async Set m_pendingTxSlice synchronously so findTxSlice() is correct before event loop fires
Split race (single-slice) set_split_freq/mode arrived before new slice existed Stash in m_pendingSplitFreqMHz/m_pendingSplitMode, apply in tryPromoteTxSlice()
get_lock_modeRPRT -4 WSJT-X 3.0 interprets -4 as lock active, suppresses its own mode-set commands Return Lock Mode: 0
get_powerstatRPRT -4 WSJT-X init probe failed Return Power Status: 1
set_powerstat 0 / set_lock_mode 1 Silently returned RPRT 0 for operations the radio cannot perform Now return RPRT -1
case 'k' short form Ignored m_extended flag, always returned bare format Check m_extended and return labeled extended response

Integration testing results

Software Freq sync Mode sync Split Morse
MacLoggerDX ✅ bidirectional ✅ bidirectional ✅ single-slice + two-slice ✅ keyer works
WSJT-X 3.0.0 ✅ bidirectional ✅ bidirectional N/A N/A
fldigi ✅ bidirectional ✅ follows radio N/A N/A

fldigi note: fldigi does not send set_mode via CAT — it follows the radio rather than leading it. This is a known fldigi design choice, not an AetherSDR bug. The preferred fldigi integration path is fldigi → flrig → AetherSDR via TCI, which provides full bidirectional mode control. Direct rigctld support is provided as a fallback for users who prefer it.

Test suite

tests/rigctld_test.cpp — 149 checks across 15 sections + edge cases (C++ only, no Python dependency). Run with:

cmake --build build --target rigctld_test
./build/rigctld_test                    # protocol tests only
./build/rigctld_test --ptt              # add PTT tests (dummy load required)
./build/rigctld_test --ptt --cw         # add CW/morse tests (dummy load required)

Every set_* command is followed by a companion get_* round-trip verification. Section 14 is a dedicated regression suite for the short-form character mapping audit; section 15 covers FM/repeater stubs.

Test plan

  • Build clean on macOS (darwin 25.4.0)
  • rigctld_test 148/148 passed (1 skip: set_ts read-back — radio snaps to band-valid step) with --ptt --cw against FLEX-6500 fw 4.2.18
  • MacLoggerDX bidirectional freq/mode sync, split, morse keyer — confirmed
  • WSJT-X 3.0.0 — connects cleanly, freq/mode sync confirmed
  • fldigi — freq sync confirmed; mode-follow confirmed; direct set_mode limitation documented
  • Rebased cleanly onto current upstream/main (conflict was a comment-only merge in cmdSendMorse, resolved by keeping upstream comment from CW sidetone is missing when transmitting via CAT (rigctrld) #2909)
  • CI

🤖 Generated with Claude Code

…ite (aethersdr#2379)

Implements complete Hamlib NET rigctl (rigctld) emulation in AetherSDR,
enabling bidirectional rig control from MacLoggerDX, WSJT-X, fldigi,
and any other Hamlib-compatible logging or digital-mode software.

Command coverage was verified against the Hamlib source (tests/rigctl_parse.c
and src/rig.c) to ensure no gaps. All short-form character mappings were
audited and corrected against the canonical Hamlib table.

- Extended-mode (+) and bare-mode responses, short-form single-character
  commands — both TCP and PTY/virtual-serial paths share the same handler
- Frequency: get/set_freq, VFO ops (UP/DOWN/BAND-UP/BAND-DOWN)
- Mode: get/set_mode with all SmartSDR modes; passband 0 defers to radio
- VFO: get/set_vfo, get_vfo_info (Freq/Mode/Width/Split/SatMode per VFO)
- Split VFO: get/set_split_vfo, get/set_split_freq, get/set_split_mode,
  get/set_split_freq_mode — two-slice and single-slice deferred-promotion
  paths both tested
- PTT: get/set_ptt via interlock (not mox)
- Levels: AF, RF, SQL, RFPOWER, STRENGTH, KEYSPD with set+get round-trips
- Functions: NB, NR, ANF, VOX, MUTE, FBKIN
- RIT/XIT: get/set/clear with read-back confirmation
- Antenna: get/set with capability probe (set_ant ?)
- CW/Morse: send_morse (inline + two-line Not1MM form), stop_morse,
  set_level KEYSPD; wait_morse returns RPRT -4 (CWX queue not queryable)
- Tuning step: get/set_ts with capability probe (set_ts ?)
- Power conversion: power2mW / mW2power
- dump_state (positional Hamlib v1 format), get_info, get_rig_info,
  chk_vfo, get_trn/set_trn, get_dcd
- FM/repeater stubs: get/set_rptr_shift/offs, get/set_ctcss_tone/dcs_code
- Hamlib init probes: get/set_powerstat, get/set_lock_mode, set_vfo_opt,
  hamlib_version, get_vfo_list, get_modes, client_version
- Short-form character table corrected against Hamlib tests/rigctl_parse.c
  (a/A=trn, y/Y=ant, n/N=ts, G=vfo_op, k/K=split_freq_mode, r/R/o/O/c/C/d/D=FM)

- set_mode ?: missing guard was calling hamlibToSmartSDR("?") → "USB" and
  clobbering mode on every MacLoggerDX connection
- Split race (two-slice): setTxSlice(true) queued async; findTxSlice()
  returned stale slice A. Fixed with m_pendingTxSlice set synchronously.
- Split race (single-slice): set_split_freq/mode arrived before new slice
  existed. Fixed with m_pendingSplitFreqMHz/m_pendingSplitMode stash,
  applied in tryPromoteTxSlice() when the new slice appears.
- get_lock_mode: was RPRT -4; WSJT-X 3.0 interprets -4 as lock active
  and suppresses its own mode-set commands. Now returns Lock Mode: 0.
- get_powerstat: was RPRT -4; now returns Power Status: 1.
- set_powerstat 0 / set_lock_mode 1: now return RPRT -1 rather than
  silently accepting — honest about what the radio cannot do.
- case 'k' (get_split_freq_mode short form): ignored m_extended flag,
  always returned bare format even in extended mode.

- MacLoggerDX: frequency sync bidirectional, mode sync bidirectional,
  split (single-slice creation path and two-slice burst), morse keyer —
  all confirmed working.
- WSJT-X 3.0.0: connects cleanly, frequency and mode sync bidirectional.
- fldigi: frequency sync bidirectional, mode changes on radio are
  reflected in fldigi. fldigi does not send set_mode via CAT (known
  fldigi design choice — it follows the radio, does not lead it).
  Preferred fldigi path is fldigi→flrig→AetherSDR via TCI (full
  bidirectional mode control); direct rigctld provided as fallback.

149 checks across 15 sections + edge cases (C++ integration test,
no Python dependency):
- Sections 1–13: connection/init, frequency, mode, VFO, split, PTT,
  levels, functions, RIT/XIT, antenna, CW/morse, power conversion,
  bare-protocol (non-extended) mode
- Section 1b: Hamlib init probe command responses (WSJT-X compat)
- Section 14: short-form character mapping regression
- Section 15: FM/repeater stub responses
- Every set_* command followed by companion get_* round-trip verification
- Optional --ptt and --cw flags for TX-active tests (dummy load required)

Closes aethersdr#2379

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@K5PTB

K5PTB commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

I did some additional integration testing: built this on Linux (Raspberry Pi 5), and tested Not1MM for proper operation. Band switching, frequency sync, and send_morse commands work as expected.

@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 — the rigctld work itself is impressive: thorough Hamlib audit, honest RPRT -1 for unsupported ops, and a 1591-line round-trip test harness is a real gift to future contributors. A few things worth addressing before merge.

Blocking

src/gui/SpectrumWidget.cpp changes are out of scope and revert PR #2826

The PR description says the only rebase conflict was a comment-only merge in cmdSendMorse, but the SpectrumWidget changes look like an accidental revert of commit 4d5b4d7a ("Honor FFT Line slider on QPainter (software) render path (#2722). Principle III. (#2826)"). Specifically:

  • setFftLineWidth() loses its trailing update() — slider changes no longer trigger immediate repaint.
  • drawSpectrum() drops the drawLine = m_fftLineWidth > 0.0f "off" gate and the cosmetic pen built from m_fftLineWidth.
  • The heat-map line path is replaced with a hardcoded QPen(heatColor(avgT), 1.5) — the user's slider value is silently ignored on the QPainter render path.

This regresses the fix for #2722 on the software render path. Please drop these hunks from the PR (or rebase again against the current main and keep #2826's logic).

Worth a look

Dangling-pointer risk on m_pendingTxSlice

RigctlProtocol::findTxSlice() returns m_pendingTxSlice (a raw SliceModel*) before the model has confirmed the slice exists, and the pointer can persist across multiple command bursts (and even across split disable→enable cycles if cmdSetSplitVfo clears it but a later op repopulates from a removed slice). If the user closes the slice via the GUI between two rigctld commands, m_pendingTxSlice becomes dangling and the next access is UB.

Two reasonable fixes:

  • Hold it as QPointer<SliceModel> and null-check on use, or
  • Connect to whatever slice-removed signal RadioModel emits and clear m_pendingTxSlice there.

dump_state extended-mode terminator

if (m_extended)
    dump += rprt(0);

Good catch. Worth confirming that chk_vfo in extended mode (chk_vfo:\nVFO Mode: 0\n + RPRT 0) matches what real Hamlib 4.x clients parse — most other extended responses in this PR follow the cmd: arg\nKey: value\n shape, but chk_vfo has no arg, so chk_vfo: (trailing colon, nothing after) is a bit unusual. Not a bug if you tested it with WSJT-X/MacLoggerDX and it works (the integration table says yes), just worth a sanity check against the Hamlib source if you haven't already.

Extended-mode get_level format change

"get_level:\nLevel: %1\nLevel Value: %2\n"   // old
"get_level: %1\n%2: %3\n"                    // new

This now matches canonical Hamlib better, but it's a breaking change for anything that was parsing the old field names. Worth a one-line note in the PR description so future readers don't get confused.

Nice work

  • The split-VFO race writeup in findTxSlice() and tryPromoteTxSlice() is exactly the kind of comment that earns its keep.
  • The RPRT -1 vs RPRT 0 distinction for set_powerstat 0 / set_lock_mode 1 is the right call — silent lies here would be much worse than honest errors.
  • The case 'k' short-form fix and the set_mode ? early-return guard are good bug catches.

🤖 aethersdr-agent · cost: $3.6467 · model: claude-opus-4-7

…inter path

The rebase accidentally reverted PR aethersdr#2826 (commit 4d5b4d7). Restoring
src/gui/SpectrumWidget.cpp from current main brings back:
- setFftLineWidth() update() trigger so slider repaints immediately
- drawLine = m_fftLineWidth > 0.0f "off" gate
- cosmetic pen built from m_fftLineWidth so slider value is honored
- and the unrelated improvements that landed after PR base (panadapter
  center clamp aethersdr#2867, waterfall stale-echo animation guard)

All rigctld work in this PR is unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 merged commit e442469 into aethersdr:main May 23, 2026
5 checks passed
@ten9876

ten9876 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Merged — thanks @K5PTB! Complete Hamlib NET rigctl support has been on the wishlist since #2379; the audit against tests/rigctl_parse.c + src/rig.c to find gaps, the honest RPRT -1 for ops a network-FlexRadio cannot perform, and the 149-test C++ harness with set/get round-trips together make this drop-in for MacLoggerDX / WSJT-X 3.0 / fldigi / Not1MM users in a way the stock dummy-net path never was.

The split-VFO race writeup (two-slice m_pendingTxSlice synchronous-set + single-slice deferred-promotion via m_pendingSplitFreqMHz/m_pendingSplitMode) is exactly the kind of in-code comment that earns its keep — saved me from chasing the same race myself.

Restored an accidental revert of #2826's FFT-line-width slider gating in SpectrumWidget.cpp (stale-base on top of PR #2722's fix) so the slider keeps working on the QPainter render path. The rigctld work itself was untouched.

Shipping in the next release. 73 de KK7GWY — and welcome to the AetherSDR maintainer rotation if you're ever interested; this is real maintainer-grade work.

73,
Jeremy KK7GWY & Claude (AI dev partner)

@K5PTB

K5PTB commented May 23, 2026

Copy link
Copy Markdown
Contributor Author

@ten9876 Jeremy, thank you for the kind words. I haven't done anything like this for a long time (former OS developer), and working with Claude has been an interesting experience. I appreciate the offer to join the team, and someday I may take you up on it. But, we travel for extended periods of time, and I don't think I could give this the attention it deserves.

I'm not sure how the accidental revert of the FFT-line-width slider gating happened.. I had Claude rebase and build/test just before I submitted the PR. 🤷🏼‍♂️

I am winding up integration testing of a full CAT server that will be able to replace SmartCAT altogether, so external software like N1MM that doesn't support rigctld will work without a bridge.

73's, Paul

ten9876 added a commit to pepefrog1234/AetherSDR that referenced this pull request May 23, 2026
…dr#2995

Reconciles three concurrent improvements to cmdSetSplitVfo:
- pepefrog1234's edge-detection (m_lastSplitEnable tri-state) so the
  Hamlib-logger poll doesn't ping-pong TX away from the user's choice
- K5PTB's deferred-promotion state (m_pendingSplitEnable / m_pendingTxSlice /
  m_pendingSplitFreqMHz / m_pendingSplitMode) from aethersdr#2975
- The m_pendingTxSliceChange same-pass race flag from aethersdr#2975

The combined logic: reclaim TX on the RX slice only when we observe a real
1→0 split transition OR the same-pass race (split-1 then split-0 in one
onClientData() pass).  Steady-state poll-zeros and the first-report-after-
connect leave the user's TX badge alone.  The race path stays distinct so
the queued setTxSlice() race from aethersdr#2975 is still covered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
K5PTB added a commit to K5PTB/AetherSDR that referenced this pull request May 25, 2026
…ethersdr#2962)

Implements a native SmartCAT-compatible TCP CAT server, enabling band,
mode, and frequency sync with N1MM, fldigi, WSJT-X, MacLoggerDX, and
other logging/digital-mode software via the industry-standard CAT
protocol.

## Why CAT instead of rigctld

N1MM does not speak the Hamlib NET rigctl protocol; it requires a CAT
port (TS-2000 or FlexCAT dialect). This made a full CAT implementation
necessary for N1MM compatibility. Not1MM (a separate Linux-only product)
uses rigctld — that is covered by aethersdr#2975.

## Protocol coverage

- Full Kenwood TS-2000 command set per the manual appendix
- Full FlexCAT command set per SmartCAT User Guide v4.1.5, except
  commands not applicable to a Flex radio (see PR body for gap list)
- Two TCP ports; each port has a companion PTY that accepts the same
  commands — operators with existing SmartCAT/SmartDAX configurations
  can keep them unchanged

## Integration testing

Tested against real hardware (FlexRadio 6500, firmware 4.2.18):

| App          | Connection | Result |
|--------------|-----------|--------|
| fldigi       | TCP + PTY | ✓      |
| WSJT-X       | TCP + PTY | ✓      |
| MacLoggerDX  | TCP       | ✓      |
| N1MM+        | TCP       | ✓ (CAT sync; CW keying not possible — see below) |

Primary platform: macOS. Also tested on Raspberry Pi 5 / Raspberry Pi
OS Trixie.

### N1MM CW keying — known limitation

N1MM keys CW by toggling RTS/DTR on a real serial COM port. Virtual
serial ports (PTYs) are not exposed as COM ports in Windows, so RTS/DTR
keying cannot reach AetherSDR. No workaround exists without a
WinKeyer-compatible keyer emulator; documented here so operators don't
waste time chasing it.

## Test suites

- `CAT_TS-2000_test`: 15 sections, 103 checks (92 always-on + 11
  PTT/CW gated)
- `CAT_Flex_test`: 16 sections, 129 checks (105 always-on + 24
  PTT/CW gated)

PTT and CW keyer tests are **skipped by default** to avoid unintended
transmissions. Enable with `--ptt` and `--cw` when a radio is connected
and the operator is ready to transmit.

Firmware TX race noted during CW testing: the radio briefly reports
TX=0 via CAT while the PA is still hot after CW completes. Safety
watches added to the test suite; documented as known hardware behaviour.

## CAT applet redesign

Configuration moved to a pop-out window (port number, dialect, VFO A/B
slice assignment per port) rather than the right panel. Inspired by
SmartSDR for macOS but more intuitive. The pattern — space-consuming
config in a pop-out, status summary in the applet — is worth adopting
for other applets.

UI details: dynamic PTY-path column sizing; "Flex" dialect label (was
"FlexCAT"); VFO column widths; column stretch.

## Security

- Applied the PTY symlink fix (same pattern as GHSA-qxhr-cwrc-pvrm,
  aethersdr#3027) to CatPort: per-user symlink directories, atomic rename
  instead of unlink+symlink.

## Code quality

- UAF audit: SmartCatProtocol and the new RigctlProtocol additions are
  both clean — no `[this]` captures in queued lambdas; all dispatch is
  synchronous on the main thread.
- ThemeManager: `CatControlApplet` was included in the Phase 2 mass
  migration (aethersdr#3102). Because this branch replaces the entire applet,
  that migration did not carry forward. We applied
  `ThemeManager::applyStyleSheet()` throughout — both the docked panel
  and the pop-out window — so all widgets auto-restyle on `themeChanged`,
  consistent with the rest of the codebase. One intentional hardcode
  remains: the `:checked` background `#006040` (CAT active state); there
  is no `color.background.success` token analogous to
  `color.background.tx`. Adding one would complete the conversion and is
  worth proposing as a follow-up.

Closes aethersdr#2962

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
K5PTB added a commit to K5PTB/AetherSDR that referenced this pull request May 25, 2026
…ethersdr#2962)

Implements a native SmartCAT-compatible TCP CAT server, enabling band,
mode, and frequency sync with N1MM, fldigi, WSJT-X, MacLoggerDX, and
other logging/digital-mode software via the industry-standard CAT
protocol.

## Why CAT instead of rigctld

N1MM does not speak the Hamlib NET rigctl protocol; it requires a CAT
port (TS-2000 or FlexCAT dialect). This made a full CAT implementation
necessary for N1MM compatibility. Not1MM (a separate Linux-only product)
uses rigctld — that is covered by aethersdr#2975.

## Protocol coverage

- Full Kenwood TS-2000 command set per the manual appendix
- Full FlexCAT command set per SmartCAT User Guide v4.1.5, with the
  exception of ZZPA/ZZPE (panadapter data streaming — out of scope)
- Two TCP ports by default: 4532 (rigctld) and 5001 (FlexCAT); up to
  6 more ports can be configured. Each TCP port has a companion PTY
  that accepts the same commands and responds identically. PTY paths
  are shown in the pop-out applet and can be copied to the clipboard
  with a right-click.

## Integration testing

Tested against real hardware (FlexRadio 6500, firmware 4.2.18):

| App          | Connection | Result |
|--------------|-----------|--------|
| fldigi       | TCP + PTY | ✓      |
| WSJT-X       | TCP + PTY | ✓      |
| MacLoggerDX  | PTY (TS-2000 only) | ✓ |
| N1MM+        | TCP       | ✓ (CAT sync; CW keying not possible — see below) |

Primary platform: macOS. Also tested on Raspberry Pi 5 / Raspberry Pi
OS Trixie.

### N1MM CW keying — known limitation

N1MM keys CW by toggling RTS/DTR on a real serial COM port. Virtual
serial ports (PTYs) are not exposed as COM ports in Windows, so RTS/DTR
keying cannot reach AetherSDR. No workaround exists without a
WinKeyer-compatible keyer emulator; documented so operators don't waste
time chasing it.

## Test suites

- `CAT_TS-2000_test`: 15 sections, 103 checks (92 always-on + 11
  PTT/CW gated)
- `CAT_Flex_test`: 16 sections, 129 checks (105 always-on + 24
  PTT/CW gated)

PTT and CW keyer tests are **skipped by default** to avoid unintended
transmissions. Enable with `--ptt` and `--cw` when a radio is connected
and the operator is ready to transmit.

Firmware TX race noted during CW testing: the radio briefly reports
TX=0 via CAT while the PA is still hot after CW completes. Safety
watches added to the test suite; documented as known hardware behaviour.

## CAT applet redesign

Inspired by SmartSDR for macOS. The space-consuming configuration
belongs in a pop-out, not the applet panel. PTY paths for running ports
are shown in the table and can be copied to the clipboard with a
right-click — handy when configuring external apps.

## Security

- Applied the PTY symlink fix (same pattern as GHSA-qxhr-cwrc-pvrm,
  aethersdr#3027) to CatPort: per-user symlink directories, atomic rename
  instead of unlink+symlink.

## Code quality

- UAF audit: SmartCatProtocol and new RigctlProtocol additions both
  clean — no [this] captures in queued lambdas; all dispatch
  synchronous on main thread
- ThemeManager: CatControlApplet was included in the Phase 2 mass
  migration (aethersdr#3102). Because this branch replaces the entire applet,
  that migration did not carry forward. Applied
  ThemeManager::applyStyleSheet() throughout — both the docked panel
  and the pop-out window — so all widgets auto-restyle on themeChanged,
  consistent with the rest of the codebase. One intentional hardcode
  remains: the :checked background #006040 (CAT active state); there
  is no color.background.success token analogous to color.background.tx.
  Adding one would complete the conversion and is worth proposing as
  a follow-up.

Closes aethersdr#2962

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@K5PTB K5PTB deleted the feat/rigctld-extended-commands branch May 25, 2026 17:11
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…ite (aethersdr#2975)

## Summary

Implements complete Hamlib NET rigctl (rigctld) emulation in AetherSDR,
enabling bidirectional rig control from MacLoggerDX, WSJT-X, fldigi, and
any other Hamlib-compatible logging or digital-mode software. Closes
aethersdr#2379.

Command coverage was verified against the Hamlib source
(`tests/rigctl_parse.c` and `src/rig.c`) to ensure no gaps. All
short-form character mappings were audited and corrected against the
canonical Hamlib table.

## What's implemented

- **Extended-mode** (`+`) and **bare-mode** responses; **short-form**
single-character commands — TCP and PTY paths share the same
`RigctlProtocol` handler
- Frequency, mode, VFO, split VFO (two-slice and single-slice
deferred-promotion paths), PTT, RIT/XIT, antenna, tuning step, levels
(AF/RF/SQL/RFPOWER/STRENGTH/KEYSPD), functions
(NB/NR/ANF/VOX/MUTE/FBKIN)
- CW/Morse: `send_morse` (inline + two-line Not1MM form), `stop_morse`,
`set_level KEYSPD`; `wait_morse` returns `RPRT -4` — CWX queue depth is
not queryable on a FlexRadio, so an honest error is better than a false
"done"
- `dump_state` (positional Hamlib v1 format), `get_info`,
`get_rig_info`, `chk_vfo`, `get/set_trn`, `get_dcd`,
`power2mW`/`mW2power`
- FM/repeater stubs: `get/set_rptr_shift/offs`,
`get/set_ctcss_tone/dcs_code`
- Hamlib init probes: `get/set_powerstat`, `get/set_lock_mode`,
`set_vfo_opt`, `hamlib_version`, `get_vfo_list`, `get_modes`
- `set_powerstat 0` and `set_lock_mode 1` return `RPRT -1` — honest
about what a network-connected FlexRadio cannot do

## Bug fixes found during integration testing

| Bug | Symptom | Fix |
|-----|---------|-----|
| `set_mode ?` missing guard | MacLoggerDX capability probe clobbered
mode to USB on every connect | Return capability list early before mode
conversion |
| Split race (two-slice) | `findTxSlice()` returned stale slice A after
`setTxSlice(true)` was queued async | Set `m_pendingTxSlice`
synchronously so `findTxSlice()` is correct before event loop fires |
| Split race (single-slice) | `set_split_freq/mode` arrived before new
slice existed | Stash in `m_pendingSplitFreqMHz`/`m_pendingSplitMode`,
apply in `tryPromoteTxSlice()` |
| `get_lock_mode` → `RPRT -4` | WSJT-X 3.0 interprets `-4` as lock
active, suppresses its own mode-set commands | Return `Lock Mode: 0` |
| `get_powerstat` → `RPRT -4` | WSJT-X init probe failed | Return `Power
Status: 1` |
| `set_powerstat 0` / `set_lock_mode 1` | Silently returned `RPRT 0` for
operations the radio cannot perform | Now return `RPRT -1` |
| `case 'k'` short form | Ignored `m_extended` flag, always returned
bare format | Check `m_extended` and return labeled extended response |

## Integration testing results

| Software | Freq sync | Mode sync | Split | Morse |
|----------|-----------|-----------|-------|-------|
| **MacLoggerDX** | ✅ bidirectional | ✅ bidirectional | ✅ single-slice +
two-slice | ✅ keyer works |
| **WSJT-X 3.0.0** | ✅ bidirectional | ✅ bidirectional | N/A | N/A |
| **fldigi** | ✅ bidirectional | ✅ follows radio | N/A | N/A |

**fldigi note:** fldigi does not send `set_mode` via CAT — it follows
the radio rather than leading it. This is a known fldigi design choice,
not an AetherSDR bug. The preferred fldigi integration path is `fldigi →
flrig → AetherSDR via TCI`, which provides full bidirectional mode
control. Direct rigctld support is provided as a fallback for users who
prefer it.

## Test suite

`tests/rigctld_test.cpp` — 149 checks across 15 sections + edge cases
(C++ only, no Python dependency). Run with:

```bash
cmake --build build --target rigctld_test
./build/rigctld_test                    # protocol tests only
./build/rigctld_test --ptt              # add PTT tests (dummy load required)
./build/rigctld_test --ptt --cw         # add CW/morse tests (dummy load required)
```

Every `set_*` command is followed by a companion `get_*` round-trip
verification. Section 14 is a dedicated regression suite for the
short-form character mapping audit; section 15 covers FM/repeater stubs.

## Test plan

- [x] Build clean on macOS (darwin 25.4.0)
- [x] `rigctld_test` 148/148 passed (1 skip: `set_ts` read-back — radio
snaps to band-valid step) with `--ptt --cw` against FLEX-6500 fw 4.2.18
- [x] MacLoggerDX bidirectional freq/mode sync, split, morse keyer —
confirmed
- [x] WSJT-X 3.0.0 — connects cleanly, freq/mode sync confirmed
- [x] fldigi — freq sync confirmed; mode-follow confirmed; direct
set_mode limitation documented
- [x] Rebased cleanly onto current upstream/main (conflict was a
comment-only merge in `cmdSendMorse`, resolved by keeping upstream
comment from aethersdr#2909)
- [ ] CI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.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.

Implement full rigctld protocol

2 participants