feat(smartcat): SmartCAT TCP server — TS-2000 and FlexCAT dialects#3131
Conversation
There was a problem hiding this comment.
Thanks for this — protocol coverage and test depth are impressive, the GHSA‑qxhr‑cwrc‑pvrm pattern is correctly applied to CatPort, and the AppSettings/RAII conventions are followed throughout the new GUI surface. A few findings worth addressing before merge.
Dead, broken code in tree
src/core/SmartCatServer.{h,cpp} are present but not added to CMakeLists.txt (only CatPort.cpp, SmartCatProtocol.cpp, SmartCatSession.cpp are). CatPort.h:25 explicitly says "Replaces the separate RigctlServer / SmartCatServer / RigctlPty hierarchy" and the CMake hunk removes RigctlServer.cpp — looks like SmartCatServer.{h,cpp} were meant to be deleted in the same pass.
It's also genuinely broken: SmartCatServer.cpp:74 calls new SmartCatSession(socket, m_model, m_vfoAIndex, m_vfoBIndex, this) — a 5‑arg ctor that no longer exists (current sig is socket, model, vfoA, vfoB, flexExtensions, parent). If anything in the future picks this up, it won't compile.
Suggested fix: git rm src/core/SmartCatServer.{h,cpp}.
TX safety: PTT can be left keyed on disconnect
If a client sends TX;/ZZTX; and then drops the TCP connection (crash, network blip, app SIGKILL) without sending RX;, SmartCatSession::~SmartCatSession() only calls setAI(false) — the radio stays keyed. Rigctld has the same gotcha, but here SmartCatProtocol::cmdTX() deliberately uses PttSource::Dax to bypass voice‑mode interlocks, so it's worth handling explicitly.
One option: track a m_pttAssertedByMe bit in the session and call m_model->setTransmit(false, PttSource::Dax) from onDisconnected() / dtor if set. Don't unconditionally force RX (would clobber legitimate PTT from another source — front panel, footswitch, another CAT client).
Minor: PTY write return ignored
CatPort::onPtyData() (CatPort.cpp:347, :363) issues ::write(m_ptyMasterFd, …) and discards the return. The master is O_NONBLOCK, so on a full kernel buffer a short or EAGAIN write will silently drop response bytes. In practice CAT responses are tiny and PTY buffers large, so unlikely to bite — but at minimum a qCWarning on n < 0 would make it diagnosable if it does.
Out of scope but nice
The "Save As… → color.background.success" point you raised in the description is a clean follow‑up — happy to see it filed as an issue separately rather than expanding this PR.
Otherwise the rest looks solid: dialect dispatch is clean, the atomic symlink rename is correct, GUI uses AppSettings, slice‑lookup paths are null‑guarded. Nicely structured contribution.
🤖 aethersdr-agent · cost: $9.7990 · model: claude-opus-4-7
|
@ten9876 @AetherClaude I have the fixes ready for the issues that were identified. Do you want me to apply them to the PR? |
|
@K5PTB Yes, please go ahead and push the fixes directly to this PR branch. Smaller, well-scoped follow-up commits are easier to review than a squashed force-push, so feel free to land them as individual commits with descriptive messages — happy to rebase/clean up at merge time if needed. If any of the fixes touch the items called out in the PR body (the PTY symlink hardening, the UAF audit surface, or the ThemeManager migration), a one-line note in the commit message tying it back is appreciated. And if you'd like a follow-up issue opened for the 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 aethersdr-agent · cost: $0.7206 · model: claude-opus-4-7 |
Files were superseded by CatPort (per CatPort.h:25) and were never added to CMakeLists.txt. SmartCatServer.cpp:74 also called a 5-arg SmartCatSession ctor that no longer exists (current sig has a flexExtensions param between vfoB and parent), so the files would not compile if anything linked them. Flagged by aethersdr-agent review of aethersdr#3131. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Track m_pttAssertedByMe in SmartCatProtocol — set in cmdTX(), cleared in cmdRX(). SmartCatSession::onDisconnected() and ~SmartCatSession() both call releasePtt(), which forces setTransmit(false, PttSource::Dax) only if this protocol instance was the one that keyed the radio. Guards against leaving the radio transmitting after an abrupt client drop (crash, network blip, SIGKILL). Does not unconditionally force RX, so PTT from the front panel, footswitch, or another CAT client is unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CatPort::onPtyData() calls ::write() on a non-blocking fd. Previously the return value was silently discarded; now a qCWarning is emitted on negative return so a full kernel buffer is diagnosable in the log. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ethersdr#2962) CAT protocol coverage ───────────────────── Full Kenwood TS-2000 command set, referencing the manual appendix, except commands not applicable to a FlexRadio. FlexCAT (ZZFA/ZZFB/ZZMD/ZZFI/…) extensions on a second port. Commands implemented across both dialects: • Frequency: FA/FB, ZZFA/ZZFB, IF/ZZIF, OI • Mode: MD/ZZMD, ZZME (extended mode IDs) • Filter: SL/SH, FW, ZZFI/ZZFJ • AGC: GT/ZZGT, ZZAR/ZZAS (per-slice fast/slow threshold) • Audio: AG/ZZAG, ZZLE/ZZLB/ZZLF (level/balance/AF), ZZMA/ZZAB (mute) • RF/Mic: PC/ZZPC, MG/ZZMC, RA/PA (attenuator/preamp) • Squelch: SQ • RIT: RC/ZZRC, RD/ZZRD, RU/ZZRU, RG/ZZRG, RT/ZZRT, ZZRW/ZZRY • XIT: XT/ZZXG, ZZXC/ZZXS • PTT / TX: TX (TX0 = PTT off per spec), RX, FT, ZZFT, FR, ZZSW • CW: KS/KY, PT (sidetone) • Noise: NB/NL, NR/NT, RL, ZZNL/ZZNR • Meter: SM/ZZSM • VFO step: UP/DN • Status: PS, ID, TY, BY, LK, RM • AI (async push): AI/ZZAI — frequency and mode push on change • FlexCAT-only: ZZBI, ZZDE, ZZFR Why CAT in addition to rigctld ─────────────────────────────── Some external apps do not support rigctld, but expect to send TS-2000 CAT commands, with extensions by FlexRadio. setups that currently use SmartCAT can work directly with AetherSDR without change. TCI is preferred, but this supports both legacy apps and legacy hams. CAT applet redesign ──────────────────── The space-consuming configuration app only appears when popped out, avoiding clutter of the right panel. The right-panel applet shows a compact per-port status table (enabled, dialect, port, PTY path, client count). The pop-out window adds full controls: enable toggle, dialect selector, port number, symlink path. Right-click on the PTY path label → "Copy PTY Name". Two TCP ports by default: 4532 (rigctld) and 5001 (Flex CAT). Up to 6 more ports can be configured/enabled. Each port gets a dedicated PTY symlink in the per-user runtime directory (cat-A … cat-H). Theme: all widget styles use ThemeManager token templates so the applet tracks live theme changes (phase-5 switcher not yet built, but the plumbing is in place per aethersdr#3028). Test suites ──────────── Three C++ integration test executables (cmake targets): • rigctld_test — 16 sections, TCP + PTY round-trip • CAT_TS-2000_test — 14 sections (TS-2000 dialect), TCP + PTY • CAT_Flex_test — 15 sections (FlexCAT dialect), TCP + PTY PTT (--ptt) and CW keyer (--cw) tests are disabled by default; enable only with a dummy load connected. PTY defaults use QStandardPaths to resolve the per-user symlink directory, matching CatPort::defaultSymlinkPath. Integration testing ──────────────────── Tested end-to-end against: | App | Dialect | Transport | |----------------|----------|-----------| | N1MM Logger+ | TS-2000 | TCP | | Not1MM | rigctld | TCP | | WSJT-X | rigctld | TCP / PTY | | fldigi | TS-2000 | TCP / PTY | | flrig | TS-2000 | TCP | | MacLoggerDX | TS-2000 | PTY | Safety and security ──────────────────── • TX safety: SmartCatProtocol tracks whether it asserted PTT (m_pttAssertedByMe); SmartCatSession::onDisconnected and ~dtor both call releasePtt() so an abrupt client drop cannot leave the radio keyed. • PTY symlinks: per-user directory (QStandardPaths::RuntimeLocation / CacheLocation), chmod 0700, atomic rename — no /tmp cross-user collisions, no TOCTOU window (GHSA-qxhr-cwrc-pvrm). • Dead code removed: SmartCatServer.{h,cpp} were unreachable (not in CMakeLists, broken 6-arg ctor); deleted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dc0d18b to
b801b63
Compare
|
@AetherClaude The fixes were added to this PR, along with some adjustments to the unit tests that I didn't catch earlier. However, it looks like my Claude squashed all of it together into the one feature, rather than a separate commit. |
|
@K5PTB No worries at all — a single squashed feature commit is honestly fine here. Most projects (this one included) squash-merge PRs anyway, so the individual-commit granularity gets collapsed at merge time regardless. The only thing that's slightly harder is reviewing the fixes in isolation from the original work, but since you flagged what's in there (issue fixes + unit test adjustments), that gives me enough to navigate the diff. Thanks for catching the test adjustments too — that's exactly the kind of thing that's easy to miss the first pass. I'll take a look through the updated diff and the CAT_TS-2000_test / CAT_Flex_test changes. If anything looks off I'll leave inline review comments rather than asking you to re-split history. If you'd still like the 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 aethersdr-agent · cost: $0.4704 · model: claude-opus-4-7 |
I'll defer to whoever is doing the light/dark theme work to decide on that. It looks like that is still in progress. I just didn't want to impede them by replacing their rewrite of the Cat control applet. |
|
@K5PTB Totally fair — and that's a considerate read of the situation. The theme work is mid-flight: Phase 4 (Default Light) just shipped in #3129, and Phase 5 PR 1 (Theme Editor dialog) is queued behind it, so new token proposals are best held until that work quiesces. I'll leave the Thanks again for the fixes and the thoughtful coordination on this — the CAT applet replacement carrying forward the ThemeManager migration was exactly the right move. 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 aethersdr-agent · cost: $0.5661 · model: claude-opus-4-7 |
Anticipated by the CAT-server PR (#3131): K5PTB had to hardcode `#006040` for the CAT-active :checked button background because the canonical taxonomy had `color.background.tx` (warm orange — TX indicator) but no green sibling for the "success / active" state. Adds the missing token in both bundled themes and the seedBuiltinDefaults fallback so #3131 (and any future widget that needs a green-active background tint) can use `{{color.background.success}}` instead of hardcoding hex. Values picked to match the existing accent-tier conventions: default-dark.json → #006040 (matches K5PTB's hardcode + the existing dim color.accent.success entry) default-light.json → #c8e8d0 (pale green, light enough that dark text reads on it — mirrors how background.tx is a warm light cream in the light theme) seedBuiltinDefaults → #006040 (matches the dark theme so the seed fallback path doesn't regress active widgets if the JSON load somehow fails) A follow-up tool-pass (tools/migrate_colours.py) can sweep CatPopoutWindow.cpp and CatControlApplet.cpp once #3131 lands to canonicalize the rest of the hardcoded styles in that PR (~8 more hex literals, all already-canonical token matches). 73, Jeremy KK7GWY & Claude (AI dev partner) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary - Removes `src/core/RigctlServer.cpp/.h` and `src/core/RigctlPty.cpp/.h` — all four were unwired by PR #3131 but not deleted - Drops `src/core/RigctlPty.cpp` from `CORE_SOURCES` in `CMakeLists.txt` (the only remaining build reference) - Live-code grep confirms zero includes or symbol references remain; GHSA comment citations retained as-is ## Test plan - [x] `cmake --build` clean with `--target all` (RelWithDebInfo, macOS M2) - [x] Smoke test: AetherSDR launches, rigctld CAT control functional via CatPort Closes #3142 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ethersdr#3131) ## CAT Protocol Coverage - Full Kenwood TS-2000 command set, referencing the manual appendix, except commands not applicable to a FlexRadio - Full FlexCAT command set per SmartCAT User Guide v4.1.5, with the exception of ZZPA/ZZPE (panadapter data streaming — out of scope for a CAT server) - Two TCP ports by default: 4532 (rigctld) and 5001 (FlexCAT). Up to 6 more ports can be configured/enabled; each 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 — handy when configuring external apps. ## Why CAT in addition to rigctld Some external apps do not support rigctld, but expect to send TS-2000 CAT commands, with extensions by FlexRadio. AetherSDR now speaks both protocols, giving operators the flexibility to use whichever their software requires — setups that currently use SmartCAT can work directly with AetherSDR without change. TCI is preferred, but this supports both legacy apps and legacy hams. ## CAT applet redesign Inspired by SmartSDR for macOS. The space-consuming configuration only appears when popped out, avoiding clutter of the right panel. ## 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. A firmware TX race was noted during CW testing: the radio briefly reports TX=0 via CAT while the PA is still hot after CW completes. Safety watches are added to the test suite; documented as known hardware behaviour. ## Integration testing Tested against real hardware (FlexRadio 6500, firmware 4.2.18). Primary platform: macOS. Also tested on Raspberry Pi 5 / Raspberry Pi OS Trixie. | 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) | ### 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. ## 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 — 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…sdr#3380) ## Summary - Removes `src/core/RigctlServer.cpp/.h` and `src/core/RigctlPty.cpp/.h` — all four were unwired by PR aethersdr#3131 but not deleted - Drops `src/core/RigctlPty.cpp` from `CORE_SOURCES` in `CMakeLists.txt` (the only remaining build reference) - Live-code grep confirms zero includes or symbol references remain; GHSA comment citations retained as-is ## Test plan - [x] `cmake --build` clean with `--target all` (RelWithDebInfo, macOS M2) - [x] Smoke test: AetherSDR launches, rigctld CAT control functional via CatPort Closes aethersdr#3142 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
CAT Protocol Coverage
Why CAT in addition to rigctld
Some external apps do not support rigctld, but expect to send TS-2000 CAT commands, with extensions by FlexRadio. AetherSDR now speaks both protocols, giving operators the flexibility to use whichever their software requires — setups that currently use SmartCAT can work directly with AetherSDR without change. TCI is preferred, but this supports both legacy apps and legacy hams.
CAT applet redesign
Inspired by SmartSDR for macOS. The space-consuming configuration only appears when popped out, avoiding clutter of the right panel.
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
--pttand--cwwhen a radio is connected and the operator is ready to transmit.A firmware TX race was noted during CW testing: the radio briefly reports TX=0 via CAT while the PA is still hot after CW completes. Safety watches are added to the test suite; documented as known hardware behaviour.
Integration testing
Tested against real hardware (FlexRadio 6500, firmware 4.2.18). 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.
Security
Applied the PTY symlink fix (same pattern as GHSA-qxhr-cwrc-pvrm, #3027) to
CatPort: per-user symlink directories, atomic rename instead of unlink+symlink.Code quality
UAF audit:
SmartCatProtocoland the newRigctlProtocoladditions are both clean — no[this]captures in queued lambdas; all dispatch is synchronous on the main thread.ThemeManager:
CatControlAppletwas included in the Phase 2 mass migration (#3102). Because this branch replaces the entire applet, that migration did not carry forward. We appliedThemeManager::applyStyleSheet()throughout — both the docked panel and the pop-out — so all widgets auto-restyle onthemeChanged, consistent with the rest of the codebase. One intentional hardcode remains: the:checkedbackground#006040(CAT active state). There is nocolor.background.successtoken analogous tocolor.background.tx; adding one would complete the conversion and is worth proposing as a follow-up.Closes #2962
🤖 Generated with Claude Code