Skip to content

Add DAX virtual audio, Hamlib CAT control, and persistent settings#11

Closed
pepefrog1234 wants to merge 4 commits into
aethersdr:mainfrom
pepefrog1234:feature/dax-cat-rebase
Closed

Add DAX virtual audio, Hamlib CAT control, and persistent settings#11
pepefrog1234 wants to merge 4 commits into
aethersdr:mainfrom
pepefrog1234:feature/dax-cat-rebase

Conversation

@pepefrog1234

@pepefrog1234 pepefrog1234 commented Mar 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix stuck TX: Immediately gate MOX audio on unkey instead of waiting for radio's interlock state transition (7-15s delay on fw v1.4.0.0)
  • Hamlib rigctld CAT control: TCP server (port 4532) + PTY virtual serial port (/tmp/AetherSDR-CAT) for WSJT-X, fldigi, N1MM integration. Supports get/set freq, mode, VFO, PTT, split, dump_state, batch commands
  • 4-channel DAX audio: DaxStreamManager creates DAX RX streams, VirtualAudioBridge provides POSIX shared memory IPC with lock-free ring buffers at native 24 kHz stereo
  • macOS HAL plugin: libASPL-based Core Audio driver creating 4 RX + 1 TX virtual audio devices ("AetherSDR DAX 1-4" + "AetherSDR DAX TX") at 24 kHz stereo
  • CatApplet GUI: Settings panel for rigctld TCP, virtual serial port, and DAX enable/disable
  • Persistent settings: Applet toggle states, frequency/mode, connection panel state saved via QSettings and restored on startup

Test plan

  • echo "f" | nc localhost 4532 returns frequency in Hz
  • WSJT-X → Settings → Radio → Hamlib NET rigctl → 127.0.0.1:4532 → Test CAT passes
  • macOS Audio MIDI Setup shows 4 "AetherSDR DAX" + 1 TX device at 24 kHz
  • WSJT-X audio input set to "AetherSDR DAX 1" receives FT8 waterfall and decodes
  • DAX TX audio from WSJT-X transmits correctly via MOX
  • Applet toggle states persist across app restart
  • Frequency and mode restore on reconnect
  • No compiler warnings with -Wall -Wextra -Wpedantic

🤖 Generated with Claude Code

pepefrog1234 and others added 4 commits March 16, 2026 18:01
When turning off MOX, immediately set m_transmitting=false and emit
moxChanged(false) before sending "xmit 0" to the radio. This prevents
TX audio from continuing to stream during the 7-15 second interlock
state transition (UNKEY_REQUESTED → READY) on fw v1.4.0.0.

Also gate TX in RadioModel::setTransmit() for the same reason.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement a rigctld protocol emulator that allows WSJT-X, fldigi, and
other Hamlib-compatible software to control the radio. Includes:

- RigctlProtocol: pure protocol parser/responder (no I/O), supports
  get/set freq, mode, VFO, PTT, split, dump_state, batch commands
- RigctlServer: QTcpServer wrapper, multi-client, per-client state
- RigctlPty: openpty() virtual serial port with /tmp/AetherSDR-CAT symlink

Mode mapping verified against FLEX-8600 fw v1.4.0.0. Thread-safe writes
via QMetaObject::invokeMethod with Qt::QueuedConnection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement DAX RX/TX audio path for digital mode software integration:

- DaxStreamManager: creates 4 DAX RX streams via SmartSDR protocol
  (fw v1.4.0.0), routes decoded audio by stream ID
- VirtualAudioBridge: POSIX shared memory IPC with lock-free ring
  buffers, 4 RX + 1 TX channels at native 24 kHz stereo
- PanadapterStream: DAX stream ID routing, decodes PCC 0x0123 (int16)
  and PCC 0x03E3 (float32) audio, sendToRadio() for TX packets
- AudioEngine: DAX TX audio path with VITA-49 framing, MOX gating,
  silence flush on unkey, accumulator for packet-sized chunks
- HAL plugin (macOS): libASPL-based Core Audio driver creating 4 RX +
  1 TX virtual audio devices at 24 kHz stereo, reads/writes shared
  memory ring buffers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CatApplet: settings panel for rigctld TCP, virtual serial port, and
  DAX audio enable/disable with port configuration
- AppletPanel: persistent applet toggle states via QSettings, add CAT
  button; refactor addApplet() to restore saved visibility on startup
- MainWindow: wire up RigctlServer, RigctlPty, DaxStreamManager,
  VirtualAudioBridge; auto-start CAT/DAX based on saved settings;
  save/restore frequency, mode, connection panel collapsed state
- RadioModel: restore last frequency/mode from QSettings on reconnect
- CMakeLists.txt: add all new source files and HAL plugin subdirectory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ten9876 ten9876 self-assigned this Mar 16, 2026
ten9876 added a commit that referenced this pull request Mar 16, 2026
- Migrate QSettings → AppSettings for connection panel collapsed state
- Add ConnectionPanel::isCollapsed() public getter
- Remove deprecated band memory restore (issue #9)
- All settings now use AppSettings XML format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Mar 16, 2026
- Migrate QSettings → AppSettings for connection panel collapsed state
- Add ConnectionPanel::isCollapsed() public getter
- Remove deprecated band memory restore (issue #9)
- All settings now use AppSettings XML format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request Mar 16, 2026
* Fix stuck TX by immediately gating MOX on unkey

When turning off MOX, immediately set m_transmitting=false and emit
moxChanged(false) before sending "xmit 0" to the radio. This prevents
TX audio from continuing to stream during the 7-15 second interlock
state transition (UNKEY_REQUESTED → READY) on fw v1.4.0.0.

Also gate TX in RadioModel::setTransmit() for the same reason.

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

* Add Hamlib rigctld-compatible CAT control (TCP + PTY)

Implement a rigctld protocol emulator that allows WSJT-X, fldigi, and
other Hamlib-compatible software to control the radio. Includes:

- RigctlProtocol: pure protocol parser/responder (no I/O), supports
  get/set freq, mode, VFO, PTT, split, dump_state, batch commands
- RigctlServer: QTcpServer wrapper, multi-client, per-client state
- RigctlPty: openpty() virtual serial port with /tmp/AetherSDR-CAT symlink

Mode mapping verified against FLEX-8600 fw v1.4.0.0. Thread-safe writes
via QMetaObject::invokeMethod with Qt::QueuedConnection.

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

* Add CatApplet GUI, persistent settings, and MainWindow wiring

- CatApplet: settings panel for rigctld TCP, virtual serial port, and
  DAX audio enable/disable with port configuration
- AppletPanel: persistent applet toggle states via QSettings, add CAT
  button; refactor addApplet() to restore saved visibility on startup
- MainWindow: wire up RigctlServer, RigctlPty, DaxStreamManager,
  VirtualAudioBridge; auto-start CAT/DAX based on saved settings;
  save/restore frequency, mode, connection panel collapsed state
- RadioModel: restore last frequency/mode from QSettings on reconnect
- CMakeLists.txt: add all new source files and HAL plugin subdirectory

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

* Fixup: resolve rebase conflicts for PR #11

- Migrate QSettings → AppSettings for connection panel collapsed state
- Add ConnectionPanel::isCollapsed() public getter
- Remove deprecated band memory restore (issue #9)
- All settings now use AppSettings XML format

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

* Style fixes for CatApplet: title bars and toggle buttons

- Replace sectionHeader() with appletTitleBar() for gradient title bars
- Replace QCheckBox enables with QPushButton toggles (green active style)
- Matches STYLEGUIDE.md conventions

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

* Remove DAX shared memory implementation, keep rigctld and TX fix

DAX virtual audio needs PipeWire virtual devices to work with existing
ham radio software (WSJT-X, fldigi, FreeDV). The shared memory IPC
approach has no compatibility with standard audio apps.

Removed:
- DaxStreamManager, VirtualAudioBridge (shared memory IPC)
- macOS HAL plugin (hal-plugin/)
- DAX gain/TX gain sliders and enable button from CatApplet
- All DAX wiring from MainWindow

Kept:
- Hamlib rigctld TCP server (port 4532) + PTY virtual serial port
- CatApplet GUI with rigctld/PTY controls
- TX stuck fix (MOX gating in TransmitModel)
- DAX title bar with "coming soon" placeholder

DAX tracked in issue #15.

Co-Authored-By: pepeforg1234 <pty.11111@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Guard SpectrumWidget::paintEvent against zero-size painting

SEGV occurred in QPainter::drawLine during paintEvent when rigctld
was stopped while WSJT-X was actively polling. The crash was in
operator new (allocation failure), likely triggered by painting with
invalid geometry during a state transition.

Added early return guard when width/height are too small to render.

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

* Autostart CAT: rename menu items, save state, start on connect

- Renamed "Autostart CAT/DAX with SmartSDR" to "with AetherSDR"
- Menu items are now checkable toggles, persisted via AppSettings
- When AutoStartCAT is enabled and radio connects, rigctld TCP server
  and PTY virtual serial port start automatically
- Fixes first-launch WSJT-X connection failure (server wasn't running)

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

* Fix CAT server auto-starting: migrate QSettings to AppSettings

CatApplet was reading cat/tcpEnable and cat/ptyEnable from old QSettings
on construction, causing the toggle buttons to fire and auto-start the
server even when Autostart CAT wasn't enabled.

- Removed per-button state persistence (tcpEnable/ptyEnable) — CAT
  auto-start is now solely controlled by the "Autostart CAT with
  AetherSDR" menu item
- Migrated port setting from QSettings cat/tcpPort to AppSettings CatTcpPort
- Removed all QSettings usage from CatApplet

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

* Fix CAT autostart and remove false "not implemented" message

- Exclude wired actions (network, memory, spots, autostart CAT/DAX)
  from the catch-all "not implemented" handler
- Add debug logging to AutoStartCAT to verify it fires on connect
- Use saved CatTcpPort for autostart server port

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

* Split CAT autostart into rigctld and PTY, sync applet buttons

- New "Autostart rigctld with AetherSDR" menu item (TCP server)
- Existing "Autostart CAT with AetherSDR" now controls PTY only
- Both persist independently in AppSettings
- On radio connect, auto-started services sync their CatApplet
  Enable buttons to green via setTcpEnabled()/setPtyEnabled()
- Removed stale auto-start logic from CatApplet::setRigctlPty()

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

* Fix rigctld slow response: flush TCP socket after each write

WSJT-X "Test CAT" took 46 seconds because socket->write() buffered
the dump_state response without flushing. WSJT-X waited for data
that was sitting in Qt's write buffer. Added socket->flush() after
each response write.

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

* Fix rigctld delayed commands: use TCP_NODELAY instead of flush

socket->flush() may block if the client isn't reading. Replace with
TCP_NODELAY (LowDelayOption) on client accept to disable Nagle's
algorithm. Responses are sent immediately without buffering delays.

This fixes WSJT-X commands (band change, PTT) being queued for 42+
seconds before executing all at once.

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

* Add rigctld command-level debug logging

Log every command received and response sent for diagnosing
WSJT-X connection delays. dump_state response format may need
adjustment for faster NET rigctl handshake.

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

---------

Co-authored-by: pepeforg1234 <pty.11111@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@ten9876

ten9876 commented Mar 16, 2026

Copy link
Copy Markdown
Collaborator

Thank you @pepefrog1234 for this excellent contribution! We've merged the following from your PR:

Accepted and merged (PR #62):

  • Hamlib rigctld TCP server + PTY virtual serial port
  • CatApplet GUI with settings panel
  • TX stuck fix (immediate MOX gating on unkey)

Deferred:

Adaptations we made:

  • Migrated QSettings to our AppSettings XML system
  • Added autostart menu items for rigctld and CAT independently
  • Styled CatApplet to match STYLEGUIDE.md (gradient title bars, toggle buttons)
  • Added TCP_NODELAY for faster response delivery
  • Added paintEvent safety guard

Known issue: dump_state handshake slow with WSJT-X (#63)

73 de KK7GWY

@ten9876 ten9876 closed this Mar 16, 2026
ten9876 added a commit that referenced this pull request Mar 18, 2026
- Rule #11: Use AppSettings, NEVER QSettings (prevents future regressions)
- AI-to-AI Debugging section: describes how contributor AIs can open
  issues for the maintainer's AI to analyze and respond to

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
K5PTB added a commit to K5PTB/AetherSDR that referenced this pull request Jun 10, 2026
…/shim

- Delete default constructor (= delete) to prevent null-vector deref (aethersdr#1)
- Remove noexcept from processBlock overrides, buildPrefilter, buildRrcLowpass
  — allocating ops inside noexcept terminate on OOM (aethersdr#2)
- Unconditionally call lane.demod->reset() on gate-close so DPLL/AGC
  are not left stale across silence gaps (aethersdr#3)
- Add lastHdlcFrameStartSampleIndex with 10-sample dedup window to
  prevent 9x inflation of totalHdlcFrameStarts in A+ mode (aethersdr#4)
- Record demod diagnostics from lanes.size()/2 (middle slicer) instead
  of lane 0 (weakest slicer in A+ mode) (aethersdr#6)
- Fix int*int overflow in duplicateWindowSamples: cast first operand
  to quint64 before multiply (aethersdr#8)
- Extract vhfModeLayout() free function; replace two switch copies
  in configure() and ax25DemodLaneCount() (aethersdr#10)
- AfskDemodWrapper<Demod,Result> template replaces two identical structs;
  using aliases preserve LibmodemAfskDemod / DirewolfAfskDemod names (aethersdr#11)
- Delete dead try_demodulate(double, uint8_t&) overload; all callers
  use the demod_result overload (aethersdr#12)
- Add pushLP() helper for LP ring-buffer writes; replaces 4 open-coded
  stores with manual bump in try_demodulate (aethersdr#13)

Not fixed (intentional):
  aethersdr#5 — phaseOffsetSamples=0 on all A+ lanes: DPLL pull-in makes
       phase stagger redundant (kPllSearchingInertia=0.50 locks in 4-5
       symbols from any starting phase, matching Direwolf behaviour)
  aethersdr#7 — Normal polarity hard-coded in setModemProfile: pre-existing in
       main since PR aethersdr#3279; no polarity UI or AppSettings key exists
  aethersdr#9 — DC-sum normalisation in buildRrcLowpass: correct for envelope
       detector on IQ baseband; energy norm only valid for matched filter

Principle VIII.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
K5PTB added a commit to K5PTB/AetherSDR that referenced this pull request Jun 11, 2026
…/shim

- Delete default constructor (= delete) to prevent null-vector deref (aethersdr#1)
- Remove noexcept from processBlock overrides, buildPrefilter, buildRrcLowpass
  — allocating ops inside noexcept terminate on OOM (aethersdr#2)
- Unconditionally call lane.demod->reset() on gate-close so DPLL/AGC
  are not left stale across silence gaps (aethersdr#3)
- Add lastHdlcFrameStartSampleIndex with 10-sample dedup window to
  prevent 9x inflation of totalHdlcFrameStarts in A+ mode (aethersdr#4)
- Record demod diagnostics from lanes.size()/2 (middle slicer) instead
  of lane 0 (weakest slicer in A+ mode) (aethersdr#6)
- Fix int*int overflow in duplicateWindowSamples: cast first operand
  to quint64 before multiply (aethersdr#8)
- Extract vhfModeLayout() free function; replace two switch copies
  in configure() and ax25DemodLaneCount() (aethersdr#10)
- AfskDemodWrapper<Demod,Result> template replaces two identical structs;
  using aliases preserve LibmodemAfskDemod / DirewolfAfskDemod names (aethersdr#11)
- Delete dead try_demodulate(double, uint8_t&) overload; all callers
  use the demod_result overload (aethersdr#12)
- Add pushLP() helper for LP ring-buffer writes; replaces 4 open-coded
  stores with manual bump in try_demodulate (aethersdr#13)

Not fixed (intentional):
  aethersdr#5 — phaseOffsetSamples=0 on all A+ lanes: DPLL pull-in makes
       phase stagger redundant (kPllSearchingInertia=0.50 locks in 4-5
       symbols from any starting phase, matching Direwolf behaviour)
  aethersdr#7 — Normal polarity hard-coded in setModemProfile: pre-existing in
       main since PR aethersdr#3279; no polarity UI or AppSettings key exists
  aethersdr#9 — DC-sum normalisation in buildRrcLowpass: correct for envelope
       detector on IQ baseband; energy norm only valid for matched filter

Principle VIII.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ten9876 pushed a commit that referenced this pull request Jun 26, 2026
) (#3799)

## Summary
Fixes the weekly **ASan/UBSan** job failure in
`cwx_local_keyer_drift_test` (#3740): a heap-use-after-free at `Fixture`
teardown.

This is a **test-only** lifetime bug — no production code change.

## Root cause
`Fixture` members destruct in reverse declaration order. With `keyer`
declared **before** `states`, `states` is freed **first**:

```cpp
struct Fixture {
    TestKeyer keyer;          // declared first  -> destroyed LAST
    std::vector<int> states;  // declared last   -> destroyed FIRST  (freed)
    Fixture() { keyer.setOnKeyDownChange([this](bool d){ states.push_back(d?1:0); }); }
};
```

When a test leaves the keyer **key-down** at teardown, the keyer's
destructor still fires the callback into the already-freed vector:

`~CwxLocalKeyer()` (`CwxLocalKeyer.cpp:48`) → `keyUpIfDown()` (`:235`) →
`emitKeyDown(false)` (`:229`) → installed callback →
`states.push_back(0)` **into freed memory**.

`emitKeyDown` early-returns when the requested state already matches
`m_lastEmittedKeyDown`, so only a test that ends **key-down** trips it.
`testLateTicksShortenNextWait` starts `"S"`, ticks twice, and never
drains or stops — leaving the key down — which is exactly the case the
ASan trace points at.

## Fix
Reorder the two members so `states` is declared first (destroyed last)
and outlives `keyer`. The destructor callback now writes into a live
vector. Robust against any future test that leaves the keyer keyed-down.

## Why production is unaffected
- `MainWindow` resets the keyer **before** the audio sink it calls into.
- The sidetone callback is null-guarded.

So no runtime UAF is reachable — this only broke the sanitizer job
(`priority: low`).

## Proof — agent automation bridge N/A; this is a sanitizer/unit-test
bug
The automation bridge drives the running app and can't observe a
unit-test teardown UAF, so the proof here is a direct **before → after**
ASan run. The keyer's worker thread makes the live test's
key-up/key-down state at teardown timing-dependent (hence a *weekly*
flake); to make the proof deterministic I replicated the exact
synchronous `TestKeyer(spawnWorker=false)` path and the
`testLateTicksShortenNextWait` sequence (which leaves `states` at
`size=3, cap=4` — matching the CI trace's "12 bytes inside of 16-byte
region"):

**Before (buggy `states`-freed-first order)** — reproduces the CI stack
frame-for-frame:
```
==ERROR: AddressSanitizer: heap-use-after-free ... WRITE of size 4
  #9  AetherSDR::CwxLocalKeyer::emitKeyDown(bool)  CwxLocalKeyer.cpp:229
  #10 AetherSDR::CwxLocalKeyer::keyUpIfDown()      CwxLocalKeyer.cpp:235
  #11 AetherSDR::CwxLocalKeyer::~CwxLocalKeyer()   CwxLocalKeyer.cpp:48
SUMMARY: AddressSanitizer: heap-use-after-free in std::vector<int>::emplace_back
exit=134
```

**After (fixed `states`-outlives-`keyer` order)** — clean:
```
states=3 key DOWN; teardown fires keyUpIfDown into LIVE states
OK: clean teardown
exit=0
```

**Real test, fixed, built with `-fsanitize=address`:** `54 OK, 0 FAIL`,
no ASan report, `All tests passed.`

## Related
- #3740 (this issue) — `good first issue`, `sanitizer`, `asan-ubsan`,
`CW`.

Fixes #3740

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.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