cat: implement Not1MM interoperability (#2048, #2108)#2438
Conversation
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>
There was a problem hiding this comment.
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)
cmdSetLevelfor RFPOWER returnsrprt(-1)on a non-numeric value butrprt(-8)would be slightly more consistent with the model-missing branch immediately below. Truly cosmetic.- The pipe-mode block correctly saves/restores
m_extendedso 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>
|
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) |
|
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) |
… 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>
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" The last two lines repeat on a 1 second interval. |
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 oneRigctlProtocoldispatcher.Commits
1. Pipe separator (#2048) —
ec4a30eThe rigctld wire protocol uses
|as a command separator (|f= "get freq with extended output"). AetherSDR only recognised;, so every pipe-prefixed command returnedRPRT -4. This blocked Not1MM, MacLoggerDX, and standardrigctld -m 23005tooling 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
bsend_morse (#2108) —517f9d6Hamlib's
bcommand allows the morse text on the next line:RigctlServerprocesses lines independently, so a barebreturnedRPRT -1and the morse text on the next line was dispatched as an unknown command (RPRT -4).Adds a one-shot
m_pendingMorseLineflag set bycmdSendMorse(""). The nexthandleLine()call consumes that line verbatim as the morse text and clears the flag. Inline-arg form (b CQ TEST) is unchanged. Per-connection state —RigctlServerconstructs a freshRigctlProtocolper client,RigctlPtyowns one per slice.3. RFPOWER get_level/set_level —
bec6738cmdGetLevel("RFPOWER")andcmdSetLevel("RFPOWER 0.0..1.0")previously returnedRPRT -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 thanRPRT -4.Wires RFPOWER through to
TransmitModel::setRfPower/rfPower():rfPower / 100as a 0.0–1.0 ratio (Hamlib spec)setRfPowerwhich emitstransmit set rfpower=Nto the radioAdds
RIG_LEVEL_RFPOWER(bit 13) tokRigGetLevelMaskand a newkRigSetLevelMask, advertises RFPOWER in both token lists, and updatesdump_stateto emit the correct set_level mask (was previously hardcoded to KEYSPD only).4. Per-mode filter passband placement —
b8f7250cmdSetModeapplied the SSB upper-sideband convention (lo=95, hi=width) to every non-LSB mode, so|M CW 250produced 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::applyFilterWidthForModeso the rigctld path produces the same filter the GUI does:lo = -W/2, hi = +W/2(symmetric around 0)lo = -W, hi = -95lo = 95, hi = WDIGU/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
VfoWidgetcovers 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.pyagainst the patched dispatcher. Every commonly-issued command now reaches the radio, not just returns RPRT 0:|f(get freq, pipe mode)RPRT -4get_freq:|Frequency: NNNN|RPRT 0✅|m(get mode)RPRT -4|l RFPOWERRPRT -4|F 14025000(set freq)RPRT -4slice tune) ✅|M CW 250(set mode + filter)RPRT -4|L RFPOWER 0.5(set power)RPRT -11|T 1/|T 0(PTT)RPRT -4L KEYSPD 25(CW speed)b CQ TEST(inline morse)b+ next-line morse text (#2108 case)RPRT -1thenRPRT -4\stop_morseDiff size
src/core/RigctlProtocol.{h,cpp}— no API changes, no new dependenciesTest plan
telnet 127.0.0.1 4532confirmed pipe mode, set_freq, set_mode, set_level RFPOWER, send_morse all workFollow-up not in this PR
RigctlProtocol— currently no test infrastructure mocksRadioModel. Worth a follow-up issue.\get_rig_info,\get_vfo_info, RIT/XIT, atomicset_split_freq_mode— tracked in Implement full rigctld protocol #2379.\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