Fix DAX IQ silently broken on Windows / non-PipeWire Linux#2524
Conversation
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>
There was a problem hiding this comment.
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:
-
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 privatewireDaxIqRouting()helper called from bothMainWindow()andstartDax()would collapse the duplication. Not necessary for this PR, but worth filing as a cleanup. -
Missing
qCDebug(lcDax)lines. The original IQ block instartDax()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. -
Lifetime of the lambda. Using
thisas the receiver context is correct —RadioModel,PanadapterStream,DaxIqModel, andDaxIqAppletare all owned (transitively) byMainWindow, so whenthisdies the connection is dropped before the captured pointers go stale. Matches the pattern in the existing block. -
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>
Symptom
DAX IQ has never delivered samples to consumer code on Windows or Linux-without-PipeWire — neither the GUI's
DaxIqAppletlevel meter nor any TCI IQ client receives any data, even though stream creation succeeds and the radio sends UDP packets thatPanadapterStreamobservably receives.Confirmed on Flex 6700 / Windows 11 /
main@dfd47a8:iq_start:0;from a TCI client → 0 bytes / 0 frames receivedYet AE's own log (with
DAX,Protocol/Status,Connection/Commands,VITA-49modules enabled in Support & Diagnostics) clearly shows:So the radio creates the stream and
PanadapterStreamreceives the VITA-49 packets. ButiqDataReadynever fires — there is noPanadapterStream: registered IQ stream 0xNNNNNNNNlog line, onlyregistered pan streamandregistered wf streamfor 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 insideMainWindow::startDax(), which is gated:On Windows / non-PipeWire Linux,
startDax()is compiled out entirely. WithoutregisterIqStream,PanadapterStreamreceives the inbound VITA-49 IQ packets but has no channel→streamId mapping for them.iqDataReadydoesn't fire. The GUI level meter stays silent (noDaxIqModel::feedRawIqPacket), andTciServer::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 betweenRadioModel,PanadapterStream,DaxIqModel, andDaxIqApplet, 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 wherestartDax()still does the wiring lazily when DAX audio is toggled.Test plan
Verified across all three build targets:
iq_start:0;startDax()unchangedThe Linux box's CMake configure prints
Linux DAX bridge enabled (PulseAudio pipe modules)confirmingHAVE_PIPEWIREis 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
TciAudioHeaderwithtype=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=Displayandclient_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
src/gui/MainWindow.cpp)73 de G0JKN & Claude (AI dev partner)