Skip to content

cat: implement Not1MM interoperability (#2048, #2108)#2438

Merged
ten9876 merged 5 commits into
mainfrom
rigctld-pipe-fixes
May 7, 2026
Merged

cat: implement Not1MM interoperability (#2048, #2108)#2438
ten9876 merged 5 commits into
mainfrom
rigctld-pipe-fixes

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Summary

Four-commit sweep against the rigctld protocol dispatcher. Closes #2048 and #2108; also lands two adjacent fixes discovered while tracing Not1MM's wire bytes against the dispatcher.

After this PR, Not1MM, MacLoggerDX, fldigi, and any other Hamlib NET-rigctl client work end-to-end against AetherSDR's rigctld — frequency, mode, RF power, PTT, and CW are all live, not just answered with RPRT 0.

Both transports inherit every fix: TCP server (RigctlServer, all platforms) and the Unix PTY (RigctlPty, /tmp/AetherSDR-CAT) share one RigctlProtocol dispatcher.

Commits

1. Pipe separator (#2048) — ec4a30e

The rigctld wire protocol uses | as a command separator (|f = "get freq with extended output"). AetherSDR only recognised ;, so every pipe-prefixed command returned RPRT -4. This blocked Not1MM, MacLoggerDX, and standard rigctld -m 23005 tooling completely.

Cherry-picked from the orphaned PR #2051 (commit 15ea129) which was authored, verified by the original reporter (@ct1drb), and never merged.

2. Multi-line bare b send_morse (#2108) — 517f9d6

Hamlib's b command allows the morse text on the next line:

b
CQ TEST DE...

RigctlServer processes lines independently, so a bare b returned RPRT -1 and the morse text on the next line was dispatched as an unknown command (RPRT -4).

Adds a one-shot m_pendingMorseLine flag set by cmdSendMorse(""). The next handleLine() call consumes that line verbatim as the morse text and clears the flag. Inline-arg form (b CQ TEST) is unchanged. Per-connection state — RigctlServer constructs a fresh RigctlProtocol per client, RigctlPty owns one per slice.

3. RFPOWER get_level/set_level — bec6738

cmdGetLevel("RFPOWER") and cmdSetLevel("RFPOWER 0.0..1.0") previously returned RPRT -11 (RIG_ENAVAIL), so any Hamlib client trying to read or set TX power via the standard token appeared to succeed but did nothing. Not1MM and other contest loggers use this for per-band power profiles — silent failure is worse than RPRT -4.

Wires RFPOWER through to TransmitModel::setRfPower / rfPower():

  • get: returns rfPower / 100 as a 0.0–1.0 ratio (Hamlib spec)
  • set: clamps the ratio to 0.0–1.0, scales to 0–100, calls setRfPower which emits transmit set rfpower=N to the radio

Adds RIG_LEVEL_RFPOWER (bit 13) to kRigGetLevelMask and a new kRigSetLevelMask, advertises RFPOWER in both token lists, and updates dump_state to emit the correct set_level mask (was previously hardcoded to KEYSPD only).

4. Per-mode filter passband placement — b8f7250

cmdSetMode applied the SSB upper-sideband convention (lo=95, hi=width) to every non-LSB mode, so |M CW 250 produced a 155 Hz filter centered at 172 Hz instead of a 250 Hz filter centered on 0 (where CW operators expect it given the radio's BFO pitch offset). On-air smoke test caught this — tuning to a CW signal heard a noticeably narrower-than-requested filter at the wrong center.

Mirrors the canonical mode→filter-edge mapping from VfoWidget::applyFilterWidthForMode so the rigctld path produces the same filter the GUI does:

  • CW / CWL / AM / SAM / DSB / FM / NFM / DFM → lo = -W/2, hi = +W/2 (symmetric around 0)
  • LSB → lo = -W, hi = -95
  • USB / DIGU / RTTY / FDV / others → lo = 95, hi = W

DIGU/DIGL offset and RTTY shift are intentionally not modelled here — Hamlib clients rarely set digital filter widths, and the symmetric placement is a safe default. The full GUI logic in VfoWidget covers the digital-mode offset cases for users tuning via the on-screen filter buttons.

Not1MM compatibility — verified end-to-end

Walked Not1MM's cat_interface.py against the patched dispatcher. Every commonly-issued command now reaches the radio, not just returns RPRT 0:

Not1MM wire bytes Before After
|f (get freq, pipe mode) RPRT -4 get_freq:|Frequency: NNNN|RPRT 0
|m (get mode) RPRT -4 extended response ✅
|l RFPOWER RPRT -4 0.0–1.0 ratio ✅
|F 14025000 (set freq) RPRT -4 tunes radio (slice tune) ✅
|M CW 250 (set mode + filter) RPRT -4 mode + correct 250 Hz centered filter ✅
|L RFPOWER 0.5 (set power) RPRT -11 radio TX power → 50 ✅
|T 1 / |T 0 (PTT) RPRT -4 radio enters/exits TX ✅
L KEYSPD 25 (CW speed) already worked (PR #2111) unchanged ✅
b CQ TEST (inline morse) already worked unchanged ✅
bare b + next-line morse text (#2108 case) RPRT -1 then RPRT -4 morse keyed correctly ✅
\stop_morse already worked unchanged ✅

Diff size

  • 2 files changed
  • 110 insertions, 8 deletions
  • All in src/core/RigctlProtocol.{h,cpp} — no API changes, no new dependencies

Test plan

  • Build clean on macOS arm64 (RelWithDebInfo, no warnings on RigctlProtocol)
  • Manual smoke test via telnet 127.0.0.1 4532 confirmed pipe mode, set_freq, set_mode, set_level RFPOWER, send_morse all work
  • On-air filter regression caught and fixed during testing
  • Not1MM end-to-end contest CW workflow
  • MacLoggerDX SmartLink workflow (@K5PTB volunteered as tester per Implement full rigctld protocol #2379)

Follow-up not in this PR

  • Unit tests for RigctlProtocol — currently no test infrastructure mocks RadioModel. Worth a follow-up issue.
  • Full rigctld coverage: \get_rig_info, \get_vfo_info, RIT/XIT, atomic set_split_freq_mode — tracked in Implement full rigctld protocol #2379.
  • TUNE function (\set_func TUNE 1) — would let Hamlib clients verify TX RF without an audio source. Not in this PR; easy add if useful.

🤖 Generated with Claude Code

AetherClaude and others added 4 commits May 6, 2026 18:48
The rigctld wire protocol uses '|' as a command separator (e.g. "|f"
means "get frequency with extended output"). AetherSDR only recognised
';' as a batch separator, so pipe-prefixed commands like "|f" returned
RPRT -4 (invalid command).

Split on '|' in handleLine(), enable extended response mode for
pipe-separated commands, and join responses with '|' — matching
standard rigctld behaviour that tools like Not1MM expect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Hamlib's `b` (send_morse) command allows the morse text to arrive on
the line after the command character:

    b\n
    CQ TEST DE...\n

AetherSDR's RigctlProtocol handled each line independently, so a bare
`b` returned RPRT -1 (empty text) and the morse text on the next line
was dispatched as an unknown command (RPRT -4).

Add a one-shot pending flag (m_pendingMorseLine) that is set when
cmdSendMorse() receives empty text. The next handleLine() call consumes
that line verbatim as the morse argument and clears the flag.
Inline-arg form (`b CQ TEST`) is unchanged. Per-connection state is
safe — RigctlServer constructs a fresh RigctlProtocol per client and
RigctlPty owns one per slice.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cmdGetLevel("RFPOWER") and cmdSetLevel("RFPOWER 0.0..1.0") previously
returned RPRT -11 (RIG_ENAVAIL), so any Hamlib client trying to read
or set TX power via the standard token did nothing. Not1MM and other
contest loggers use this to apply per-band power profiles, so the
result was a "successful" response with no actual power change.

Wire RFPOWER through to TransmitModel:
- get: return rfPower / 100 as a 0.0–1.0 ratio (Hamlib spec)
- set: convert ratio to 0–100 and call setRfPower() which emits
  `transmit set rfpower=N` to the radio

Add RIG_LEVEL_RFPOWER (bit 13) to both kRigGetLevelMask and a new
kRigSetLevelMask, advertise RFPOWER in rigGetLevelTokens() and
rigSetLevelTokens(), and update dump_state to emit the correct
set_level mask (was previously hardcoded to KEYSPD only).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
cmdSetMode applied the SSB upper-sideband convention (lo=95, hi=width)
to every non-LSB mode, so `|M CW 250` produced lo=95, hi=250 — a 155 Hz
filter centered at 172 Hz instead of a 250 Hz filter centered on 0
(where CW operators expect it given the radio's BFO pitch offset).

Mirror the canonical mode→filter-edge mapping from VfoWidget so the
rigctld path produces the same filter the GUI does:

  CW/CWL, AM/SAM/DSB, FM/NFM/DFM → lo = -W/2, hi = +W/2 (symmetric)
  LSB                            → lo = -W,   hi = -95
  USB/DIGU/RTTY/FDV/etc.         → lo =  95,  hi =  W

DIGU/DIGL offset and RTTY shift are not modelled here — Hamlib clients
rarely set digital filter widths, and the simple symmetric placement is
a safe default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jensenpat jensenpat requested a review from ten9876 as a code owner May 7, 2026 02:14
@jensenpat jensenpat changed the title rigctld: pipe separator + multi-line send_morse + RFPOWER + per-mode filter placement (#2048, #2108) rigctld: implement Not1MM interoperability (#2048, #2108) May 7, 2026
@jensenpat jensenpat changed the title rigctld: implement Not1MM interoperability (#2048, #2108) cat: implement Not1MM interoperability (#2048, #2108) May 7, 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 nice piece of work. The PR is well-scoped, the per-commit story is easy to follow, and the wire-protocol research (especially the Not1MM table) makes review fast. Pipe mode, RFPOWER plumbing, and the bare-b two-line morse handling all look correct.

A few things from reading the diff against the rest of RigctlProtocol.cpp and VfoWidget:

One real regression: DIGL mapping in cmdSetMode

Pre-PR, DIGL was grouped with LSB/CWL into setFilterWidth(-passband, -95). The new else branch catches DIGL and gives it the upper-sideband layout (95, passband). That's a sideband flip vs. both the old behavior and VfoWidget::applyFilterPreset (src/gui/VfoWidget.cpp:3578-3593), which mirrors DIGU into the negative half-plane for DIGL:

} else if (mode == "DIGL") {
    if (widthHz < 3000) { ... centered on -digl_offset, hi clamped to -95 ... }
    else { lo = -widthHz; hi = -95; }
}

The PR text says "DIGU/DIGL offset and RTTY shift are intentionally not modelled here", which I read as "ignore the offset" — but the sideband selection should still happen. Suggested fix is to add DIGL alongside LSB:

} else if (m == "LSB" || m == "DIGL") {
    lo = -passband;
    hi = -95;
}

Same caveat for RTTY: pre-PR it landed in the USB-side branch and that's preserved here, but the GUI mapping is symmetric around -shift/2. Probably fine to leave as a follow-up since you already note RTTY shift isn't modelled — just calling it out so the asymmetric scoping is intentional rather than accidental.

Minor: bare b followed by empty next line re-arms m_pendingMorseLine

In handleLine, if m_pendingMorseLine is set and the next line trims to empty, the flag is cleared and cmdSendMorse("") runs — which immediately sets m_pendingMorseLine = true again. So a client that sends b\n\n<anything> will key the third line as morse instead of the second. Not a security issue and not what compliant clients do, but a single-line guard inside cmdSendMorse (only arm pending if not already in the middle of the two-line form) would close the loop. Optional.

Nits (take or leave)

  • cmdSetLevel for RFPOWER returns rprt(-1) on a non-numeric value but rprt(-8) would be slightly more consistent with the model-missing branch immediately below. Truly cosmetic.
  • The pipe-mode block correctly saves/restores m_extended so the sticky-+ state isn't clobbered — nice catch on that.

Overall this is a really clear net win for Hamlib-NET clients. The DIGL fix is the only thing I'd want to see addressed before merge; everything else is optional polish.

The new per-mode filter passband placement in cmdSetMode dropped DIGL
from the explicit LSB-style branch — DIGL fell through to the default
USB-style (lo=95, hi=W) placement, which is on the wrong sideband for
a lower-sideband digital mode. A Hamlib `M DIGL 250` would produce a
filter that completely missed the DIGL audio.

Add DIGL to the LSB branch alongside LSB itself so the wide-fallback
geometry matches VfoWidget::applyFilterPreset (lo=-W, hi=-95). The
narrow-width offset-aware placement that the GUI applies via
diglOffset is intentionally not modelled in the rigctld path; Hamlib
clients rarely set narrow digital filter widths, and the LSB-edge
fallback is correct for the wide case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Claude here — pushed a one-line fix on top of your branch (ba6360e) to add DIGL alongside LSB in the lower-sideband edge-geometry branch. The new cmdSetMode mapping had dropped DIGL from the explicit LSB list (the old code had it; your symmetric/AM/FM additions covered CW/CWL/AM/FM correctly but DIGL fell through to the default USB-style branch, putting the filter on the wrong sideband). Wide-width fallback only — the offset-aware narrow-width placement stays GUI-only as you noted. CI re-running.

The other three threads (pipe separator, multi-line morse, RFPOWER) are clean. Once CI is green this is good to merge.

73, Jeremy KK7GWY & Claude (AI dev partner)

@ten9876 ten9876 merged commit 2b81104 into main May 7, 2026
4 checks passed
@ten9876 ten9876 deleted the rigctld-pipe-fixes branch May 7, 2026 03:30
@ten9876

ten9876 commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Claude here — really comprehensive Not1MM compat work, jensenpat. The wire-trace evidence and the cherry of @ct1drb's orphaned PR #2051 with credit are both excellent practice. Unblocks Not1MM, MacLoggerDX, and any other Hamlib NET-rigctl client end-to-end — frequency, mode, RF power, PTT, and CW all working, not just RPRT 0. The dump_state set_level mask correctness fix bundled in is a quiet but real bonus. Merged with the DIGL touch-up on top.

73, Jeremy KK7GWY & Claude (AI dev partner)

ten9876 added a commit that referenced this pull request May 7, 2026
… sweep (#2441)

Bumps version 0.9.7 → 0.9.8 in CMakeLists.txt, README.md, and
CLAUDE.md. Adds the v0.9.8 CHANGELOG entry covering 16 PRs since
v0.9.7, grouped Features / Bug fixes / Infrastructure with prominent
contributor credits to @chibondking, @jensenpat, @rfoust, and @s53zo,
plus @ct1drb for the cherry-picked rigctld pipe-separator fix.
WhatsNewData.cpp regenerated from CHANGELOG.md.

Theme: Aetherial Audio Channel Strip RX side (#2425) lands as the
headline feature, paired with Hamlib NET-rigctl / Not1MM interop
(#2438), an editable CW value-fields UX upgrade (#2429), and an
AetherDSP Settings dialog chrome refit. Reliability sweep covers two
silent Windows footguns (UI Scale completely broken on Windows;
spot clients crashed on disconnect cascade), hardware-PTT mic_selection
combinations, MIDI VFO jog-wheel stabilization, and a thrice-reported
filter-width-indicator drift fix.

Tag held until after this lands and CI passes.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@K5PTB

K5PTB commented May 7, 2026

Copy link
Copy Markdown
Contributor

After this PR, Not1MM, MacLoggerDX, fldigi, and any other Hamlib NET-rigctl client work end-to-end against AetherSDR's rigctld — frequency, mode, RF power, PTT, and CW are all live, not just answered with RPRT 0.

I haven't checked the other clients, but there are still unimplemented functions used by MacLoggerDX. Here's the debug log from AetherSDR with CAT/rigctld enabled:

[07:26:53.959] DBG aether.cat: rigctld cmd: "+\chk_vfo" -> resp: "0"
[07:26:53.959] DBG aether.cat: rigctld cmd: "+\get_rig_info" -> resp: "RPRT -4"
[07:26:53.959] DBG aether.cat: rigctld cmd: "+\set_mode ?" -> resp: "RPRT 0"
[07:26:53.959] DBG aether.cat: rigctld cmd: "+\get_level ?" -> resp: "get_level:\nLevels: RFPOWER KEYSPD SWR RFPOWER_METER RFPOWER_"
[07:26:53.959] DBG aether.cat: rigctld cmd: "+\set_trn ?" -> resp: "RPRT -4"
[07:26:53.959] DBG aether.cat: rigctld cmd: "+\vfo_op ?" -> resp: "RPRT -4"
[07:26:53.959] DBG aether.cat: rigctld cmd: "+\set_func ?" -> resp: "RPRT -4"
[07:26:53.959] DBG aether.cat: rigctld cmd: "+\get_info" -> resp: "FLEX-6500 v3.9.18.36988"
[07:26:53.959] DBG aether.cat: rigctld cmd: "+\get_vfo" -> resp: "get_vfo:\nVFO: VFOA\nRPRT 0"
[07:26:53.959] DBG aether.cat: rigctld cmd: "+\get_trn" -> resp: "RPRT -4"
[07:26:54.799] DBG aether.cat: rigctld cmd: "+\get_vfo_info VFOA" -> resp: "RPRT -4"
[07:26:54.799] DBG aether.cat: rigctld cmd: "+\get_vfo_info VFOB" -> resp: "RPRT -4"

The last two lines repeat on a 1 second interval.

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.

Problem with CAT/rigctld.

4 participants