Skip to content

Fix DAX IQ silently broken on Windows / non-PipeWire Linux#2524

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
nigelfenton:fix/dax-iq-windows-registration
May 10, 2026
Merged

Fix DAX IQ silently broken on Windows / non-PipeWire Linux#2524
ten9876 merged 2 commits into
aethersdr:mainfrom
nigelfenton:fix/dax-iq-windows-registration

Conversation

@nigelfenton

Copy link
Copy Markdown
Contributor

Symptom

DAX IQ has never delivered samples to consumer code on Windows or Linux-without-PipeWire — neither the GUI's DaxIqApplet level meter nor any TCI IQ client receives any data, even though stream creation succeeds and the radio sends UDP packets that PanadapterStream observably receives.

Confirmed on Flex 6700 / Windows 11 / main@dfd47a8:

  • Toggle DAX IQ channel 1 "On" in DaxIqApplet → no level on the meter
  • Send iq_start:0; from a TCI client → 0 bytes / 0 frames received

Yet AE's own log (with DAX, Protocol/Status, Connection/Commands, VITA-49 modules enabled in Support & Diagnostics) clearly shows:

TX: "C140|stream create type=dax_iq daxiq_channel=1"
DBG aether.vita49: PanadapterStream: new stream 4128 bytes streamId=0x20000000
RX: "stream 0x20000000 type=dax_iq daxiq_channel=1 pan=0x40000000 ... active=1"
DBG aether.dax: RadioModel: DAX stream status stream=0x20000000 type=dax_iq ours=1

So the radio creates the stream and PanadapterStream receives the VITA-49 packets. But iqDataReady never fires — there is no PanadapterStream: registered IQ stream 0xNNNNNNNN log line, only registered pan stream and registered wf stream for the panadapter and waterfall respectively.

Root cause

Same class of bug as #1820 (RADE RX on Windows).

The DAX IQ stream-status registration handler — the code that calls panStream->registerIqStream(streamId, ch) when the radio confirms a stream — lives inside MainWindow::startDax(), which is gated:

#if defined(Q_OS_MAC) || defined(HAVE_PIPEWIRE)
bool MainWindow::startDax() { ... }
#endif

On Windows / non-PipeWire Linux, startDax() is compiled out entirely. Without registerIqStream, PanadapterStream receives the inbound VITA-49 IQ packets but has no channel→streamId mapping for them. iqDataReady doesn't fire. The GUI level meter stays silent (no DaxIqModel::feedRawIqPacket), and TciServer::onIqDataReady (which is connected unconditionally) never sees a single frame.

Fix

Mirror just the IQ-side wiring outside the audio-bridge gate. The DAX RX (audio) wiring genuinely needs the bridge so it stays gated as-is. The DAX IQ wiring is independent of the audio bridge — pure connect() calls between RadioModel, PanadapterStream, DaxIqModel, and DaxIqApplet, all of which exist on every platform.

The new block is conditional on #if !defined(Q_OS_MAC) && !defined(HAVE_PIPEWIRE) so we don't double-connect on platforms where startDax() still does the wiring lazily when DAX audio is toggled.

Test plan

Verified across all three build targets:

Platform Build Code path Result
Windows 11 (Qt 6.10.3, MSVC) ✅ clean Fix active Real-world: TCI IQ client captured 2813 frames in 30s after iq_start:0;
macOS Apple Silicon (Qt 6.11.0) ✅ clean (529 targets) Fix gated out, existing startDax() unchanged No behavior change
Ubuntu 22.04 / R620 (Qt 6.2.4, PulseAudio) ✅ clean (520 targets) Fix active — same bug applies Should fix Linux/PulseAudio users alongside Windows

The Linux box's CMake configure prints Linux DAX bridge enabled (PulseAudio pipe modules) confirming HAVE_PIPEWIRE is undefined there too, so this PR closes the registration gap on two platforms at once, not just Windows.

Separate observation, not part of this PR

After this fix, IQ frames flow end-to-end (verified via TCI client receiving binary frames matching TciAudioHeader with type=0). However, the contents of those frames during my testing were zero-padded — 11 MB WAV file with all-zero samples even though frame headers and counts are correct.

The radio's stream status reports endpoint_type=Display and client_gui_handle == client_handle, suggesting the radio distinguishes between Display-endpoint streams (panadapter spectrum data, what AE-as-a-GUI-client gets) and DAX-endpoint streams (raw IQ samples, what SmartSDR's separate DAX-IQ utility gets via a different client identity). If that's the case, AE may need a different code path to negotiate a DAX endpoint — but that's out of scope for this PR and probably worth a separate discussion.

This PR fixes the registration gap so that whatever AE eventually does deliver gets routed to consumers (GUI applet + TCI clients). The empty-payload issue, if it's real, is downstream.

Diff summary

73 de G0JKN & Claude (AI dev partner)

DAX IQ has never delivered samples on Windows or non-PipeWire Linux:
neither the GUI's DaxIqApplet level meter nor any TCI IQ client receives
any data, even though stream creation succeeds and the radio sends UDP
packets.

Same class of bug as aethersdr#1820 (RADE RX on Windows):

  startDax() is gated by #if defined(Q_OS_MAC) || defined(HAVE_PIPEWIRE)
  and contains the DAX IQ stream-status registration handler. On
  platforms without an audio bridge it's compiled out entirely. As a
  result, when iq_start arrives or the user toggles a DAX IQ channel
  'On' in the applet:

    - 'stream create type=dax_iq daxiq_channel=N' is sent OK
    - The radio creates the stream and sends VITA-49 IQ packets
    - PanadapterStream receives them but never registers them to a
      channel (registerIqStream() lives inside startDax())
    - PanadapterStream::iqDataReady never fires
    - DaxIqModel::feedRawIqPacket never runs → GUI meter silent
    - TciServer::onIqDataReady never runs → TCI clients get nothing

Mirror the DAX IQ wiring (status handler + registration + level meter
+ enable/disable hooks) outside the audio-bridge gate, conditional on
the inverse so we don't double-connect on Mac/PipeWire builds where
startDax() still does its own wiring lazily when DAX audio is toggled.

Verified on Windows (Flex 6700) by capturing 30s of frames into a
TCI IQ client.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nigelfenton nigelfenton requested a review from ten9876 as a code owner May 9, 2026 22:25

@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 @nigelfenton — clear write-up, the diagnosis matches the code. The new block is a faithful mirror of the IQ-side wiring inside startDax() (MainWindow.cpp:13231–13281), with the audio-bridge-dependent pieces correctly left out. The compile-time gates are inverse (!Q_OS_MAC && !HAVE_PIPEWIRE vs Q_OS_MAC || HAVE_PIPEWIRE), so no double-connect risk on Mac/PipeWire builds. The added null guards (m_appletPanel && m_appletPanel->daxIqApplet() && m_radioModel.panStream()) are a small improvement over the original. Looks correct.

A few non-blocking notes:

  1. Drift risk from duplication. The fix introduces a second copy of ~50 lines of stream-status / connect wiring. Any future change to the IQ-side wiring (an extra connect, a log line, a refactor of streamStatusBelongsToUs, etc.) now has to be applied in both places, and a desync would re-create exactly this class of bug — silent on one platform, working on another. A small follow-up that extracts a private wireDaxIqRouting() helper called from both MainWindow() and startDax() would collapse the duplication. Not necessary for this PR, but worth filing as a cleanup.

  2. Missing qCDebug(lcDax) lines. The original IQ block in startDax() logs the stream status, the "ignoring stream for another client" case, and removals. The new block drops those. Given that this very PR was diagnosed by reading those exact log lines, keeping the diagnostics on the Windows/non-PipeWire path would help the next person debugging a related issue. Cheap to add.

  3. Lifetime of the lambda. Using this as the receiver context is correct — RadioModel, PanadapterStream, DaxIqModel, and DaxIqApplet are all owned (transitively) by MainWindow, so when this dies the connection is dropped before the captured pointers go stale. Matches the pattern in the existing block.

  4. Out-of-scope empty-payload observation. The note about Display- vs DAX-endpoint streams in the radio's stream-status is interesting — worth opening as a separate issue once this lands so it doesn't get lost. Agreed it's not part of this PR.

Build and platform matrix in the test plan look thorough. Nice fix.

Mirror the three diagnostic logs the original startDax() block has —
removed, ignoring-non-owner, and per-status — so the new ctor-time
path on Windows / non-PipeWire Linux produces the same trail when
debugging future stream-routing issues.  These were exactly the lines
that diagnosed this bug in the first place; keeping them on the new
path matches the pattern the next debugger will expect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 merged commit 6f16e35 into aethersdr:main May 10, 2026
4 checks passed
@aethersdr-agent aethersdr-agent Bot mentioned this pull request May 10, 2026
2 tasks
@nigelfenton nigelfenton deleted the fix/dax-iq-windows-registration branch May 25, 2026 19:18
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