fix(dax-iq): deliver IQ samples end-to-end (#2529)#3521
Conversation
The IQ pipe was only (re)built when a daxiq_rate CHANGE arrived. A fresh
enable at the default rate (daxiq_rate=48000 == IqStream::sampleRate{48000})
produced no rate delta, so the pipe was never created. Track isNew on the
new-stream branch and merge the guard to fire createPipe on isNew OR a rate
change, hoisted outside the daxiq_rate-present check.
Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
Rewrite createPipe/destroyPipe: mkfifo the owner-only FIFO ourselves first (0600 under $XDG_RUNTIME_DIR/aethersdr, not world-writable /tmp), then load module-pipe-source SYNCHRONOUSLY and capture the module index so we know it succeeded and can unload it cleanly by index. Open the write end O_RDWR| O_NONBLOCK (never ENXIOs before the read side attaches). Single-quote source_properties so the spaced device.description "AetherSDR DAX IQ N" survives the module-arg parser and drives the label shown in qpwgraph/JACK. Adds anon-namespace helpers (iqFifoDir/iqPipePath/makeOwnedFifo/runPactlSync) and an m_pipeModuleIdx[] member to track the loaded module per channel. Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
dax_iq float32 samples arrive little-endian (payload_endian=little), unlike the big-endian pan/wf/meter/audio streams. Reading them big-endian reversed each float into a denormal ≈ 0, flat-lining the RMS meter and feeding garbage to the format=float32le pipe. Decode little-endian (a no-op on LE hosts). Co-authored-by: Don @ cloaked.agency <don@cloaked.agency> & K6OZY
There was a problem hiding this comment.
Thanks @Ozy311 — this is an excellent root-cause PR. I verified all three claimed defects against the base code and they're real:
- Rate-change-only guard — confirmed:
createPipewas only reachable vianewRate != s.sampleRate, andIqStream::sampleRate{48000}equals the radio's defaultdaxiq_rate, so a fresh enable never built the pipe. The mergedisNew || rate-changeguard (kept outside thedaxiq_rate-present check) is the right fix, anddestroyPipeis idempotent so destroy-then-create on a fresh stream is safe. - Open race — confirmed: the old path fire-and-forgot
pactlviabash -c, slept 200 ms, and openedO_WRONLY. mkfifo-first + synchronous load +O_RDWR|O_NONBLOCKis strictly better, and error paths roll back cleanly (unload module, unlink FIFO). - Endianness — can't verify against hardware, but the live A/B evidence (data only resolving as LE, BE reads as denormals ≈ 0) is convincing and consistent with the #2529 symptom.
I also confirmed the security hardening genuinely mirrors PipeWireAudioBridge (daxFifoDir/makeOwnedFifo/runPactl, the GHSA-x8xf-4g5v-ppf9 pattern, single-quoted source_properties, unload-by-index), and that cleanupStaleModules() in PipeWireAudioBridge matches the aethersdr- prefix, so stale aethersdr-iq-N modules from a crashed session still get reaped on next launch. Scope is exactly the two stated files. No conventions issues.
Two minor, non-blocking suggestions:
- Misleading warning when
pactlis absent (DaxIqModel.cpp,runPactlSync): if the binary doesn't exist,QProcess::startfails andwaitForFinishedreturns false immediately, so the log says "pactl timed out" — and macOS hits this path on every IQ enable. Consider checkingproc.error() == QProcess::FailedToStartand logging "pactl not found" instead. (The existingPipeWireAudioBridge::runPactlhas the same shape, so fine to defer.) - Helper duplication:
iqFifoDir/makeOwnedFifo/runPactlSyncare near-verbatim copies of the anonymous-namespace helpers inPipeWireAudioBridge.cpp. Worth a follow-up to hoist them into a shared utility so a future hardening fix doesn't have to land twice — not something to block this fix on.
One pre-existing behavior worth noting (not a regression): on normal app exit with an active IQ stream, the worker destructor closes the fds but the module + FIFO survive until the next session's stale-module cleanup. Now that the module index is tracked, unloading in ~DaxIqWorker would be a cheap follow-up.
Also noting the visible node label changes from "AetherSDR IQ N" to "AetherSDR DAX IQ N" — looks intentional and consistent with the DAX naming work, just flagging it since patchbay users will see the rename.
Great write-up and verification rigor. 73!
🤖 aethersdr-agent · cost: $8.6511 · model: claude-fable-5
There was a problem hiding this comment.
Thanks @Ozy311 — this is a high-quality root-cause fix with an unusually clear writeup. I verified each of the three defects against the current code and they all check out:
isNewpipe creation — correct.IqStream::sampleRate{48000}matches the radio's defaultdaxiq_rate, so the old rate-delta guard genuinely never fired on a fresh enable. Keeping the trigger outside thekvs.contains("daxiq_rate")check is the right call, and sincehandleStreamRemovedresets the slot toIqStream{}, re-enable also goes through theisNewpath again. 👍- mkfifo-first + synchronous
pactl+O_RDWR|O_NONBLOCK— this faithfully mirrors the hardenedPipeWireAudioBridgepattern (daxFifoDir/makeOwnedFifo/runPactl, the single-quotedsource_propertiesfor pipewire-pulse's arg parser, and the unload+unlink cleanup on both failure paths). Good consistency with the GHSA-x8xf-4g5v-ppf9 hardening. - Little-endian decode — the dax_iq LE convention matches FlexLib behavior, and the denormal-≈-0 explanation is consistent with the #2529 symptom. Live A/B on real hardware is convincing.
Two non-blocking notes:
Minor (inherited): the /tmp fallback in iqFifoDir() can't actually work. When XDG_RUNTIME_DIR is unset and /run/user/<uid> doesn't exist, base becomes /tmp/aethersdr-<uid>, so the ::mkdir of base + "/aethersdr" fails with ENOENT — the parent is never created — and the function returns empty. This exact quirk exists in the PipeWireAudioBridge::daxFifoDir you mirrored, so it's pre-existing and essentially unreachable on a normal desktop session (XDG_RUNTIME_DIR is always set under systemd). Fine to leave as-is here for fidelity; a one-line QDir().mkpath(dir) (or creating base first) in both copies would make a good tiny follow-up.
Suggestion: clean up on app exit. ~DaxIqWorker closes the fds but doesn't unload the pipe-source modules or unlink the FIFOs, so an enabled IQ channel at quit leaves a stale aethersdr-iq-N node behind, and the next session's load-module can collide with the leftover source_name. Now that you track m_pipeModuleIdx, the destructor could simply call destroyPipe() for each channel — the old grep-based code couldn't do this cleanly, but your index tracking makes it trivial. Happy to take this as a follow-up if you'd rather keep this PR's scope tight.
Convention/scope checks: file scope matches the PR description (the two DaxIqModel files only), the new plumbing is correctly #ifndef Q_OS_WIN, error handling at every system boundary (mkdir, mkfifo, pactl, open) logs and bails cleanly, and the raw-fd handling matches the existing house style. No Copilot findings to address.
Nice work — and thanks for the cross-platform runtime verification matrix; it made this easy to review.
🤖 aethersdr-agent · cost: $4.5088 · model: claude-fable-5
NF0T
left a comment
There was a problem hiding this comment.
Claimed; read full diff, all three commits, PR body, and bot review.
Evidence basis — Principle VIII satisfied. parec capture off the aethersdr-iq-1 PipeWire source: 768 KB @ ~384 KB/s (float32 × 2ch × 48 kHz), 98% non-zero. The big-endian-decoded equivalent reads as denormals — the #2529 "zero-byte payload" failure mode is demonstrated, not asserted.
All three root causes independently verified as real:
Bug 1 — pipe never created on fresh enable. createPipe was gated on newRate != s.sampleRate; a fresh enable at daxiq_rate=48000 matches IqStream::sampleRate{48000} — zero delta, gate never fired. The isNew || rate-change guard hoisted outside the kvs.contains("daxiq_rate") check is the correct fix. destroyPipe is idempotent so destroy-then-create composes safely.
Bug 2 — race on FIFO open. Fire-and-forget startDetached + 200ms sleep + O_WRONLY|O_NONBLOCK → ENXIO if the module's read side hadn't attached. Fix is correct on all three axes: create the FIFO before loading the module, load synchronously to capture the module index for a clean unload, open O_RDWR|O_NONBLOCK which never ENXIOs regardless of read-side state. The makeOwnedFifo pattern (TOCTOU-safe: mkfifo(0600) → on EEXIST, lstat verifies uid + S_ISFIFO before unlinking and retrying) directly mirrors PipeWireAudioBridge::makeOwnedFifo and closes the same world-writable /tmp FIFO injection vector from GHSA-x8xf-4g5v-ppf9. iqFifoDir() correctly mirrors daxFifoDir() — $XDG_RUNTIME_DIR → /run/user/<uid> → /tmp/aethersdr-<uid> fallback chain with 0700 directory permissions.
Bug 3 — wrong endian. dax_iq is payload_endian=little; qFromBigEndian byte-reversed every float32 into a denormal ≈ 0. qFromLittleEndian is a correct no-op on LE hosts.
Scope discipline. DaxIqModel.{cpp,h} only. No MainWindow touch — zero #3351 refactor exposure.
Governance note (non-blocking). All three commits are signed and verified. AI co-authorship is disclosed as Co-authored-by: Don @ cloaked.agency <don@cloaked.agency>. The spirit of disclosure is met; the project convention is Co-Authored-By: Claude [version] <noreply@anthropic.com> / Codex <noreply@openai.com>. No explicit format requirement exists in CODEOWNERS or AGENTS.md, so this is informational — @Ozy311, please be aware of the convention for future PRs.
Approved. Three independently real, hardware-validated bugs fixed in the correct layer with security-hardened plumbing that mirrors existing repo patterns. Closes #2529.
…3522) ## Summary **Stacked on #3521** (the core fix that makes DAX-IQ actually deliver samples). Because both PRs target `main`, this one currently shows **all 8 commits** — the first 3 belong to #3521; the **5 commits unique to this PR** are the UX/state fixes listed below. Once #3521 merges, this diff auto-reduces to just those 5. **Please review/merge #3521 first.** With IQ finally flowing, the DAX-IQ applet's UX/state handling — never exercised because the feature never worked — needed fixing: the level meter, sample-rate switching, and enable/disable/startup state. Four focused commits. > Review note for the meter: DAX-IQ is the radio's **native int16-scale `float32`** (raw amplitude, ~32768 full-scale), not normalized `[-1, 1]` — so the meter is scaled accordingly (see commit 1). This is a radio data convention, not a wire-type guarantee. ## Fixes **1. `fix(dax-iq): scale the level meter in dBFS for raw int16 IQ`** — the meter did `rms * 200` assuming normalized `[0, 0.5]` IQ, so it **pegged at 100% on the noise floor**. It now maps RMS to **dBFS over a `[-70, -10]` window** (a level/overload meter: noise sits low, the bar fills toward digital overload) with attack/decay ballistics matching the DAX-audio meter. *Before (pegged `×200`) vs after (responsive dBFS):* | Before — `rms × 200` pegs on the noise floor | After — dBFS, tracking signal across rates | |---|---| |  |  | **2. `fix(dax-iq): apply and persist the selected sample rate across enable`** — picking 24/96/192 k reverted to 48 k: `createStream` sent no rate (radio defaults 48 k) and `setSampleRate` dropped the selection when no stream existed yet. The chosen rate is now remembered and re-applied (`stream set … daxiq_rate=`) once the stream exists; the combo retains the user's choice across disable; and the transient 48k UI flip on enable is suppressed. **3. `fix(dax-iq): zero the level meter when a channel is disabled or unbound`** — a level update queued just before a channel was switched off (or unbound from its panadapter, `pan=0x0`) could re-freeze the bar at its last value. The meter now drops to 0 whenever a channel isn't actively bound and receiving. **4. `fix(dax-iq): restore persisted-enabled IQ channels and rates on startup`** — enabled channels showed "On" at launch but had no stream (dead). On Linux/macOS the IQ↔model wiring is established lazily inside `MainWindow::startDax()`, so the applet's connect-time restore fired before the wiring existed and was dropped. The restore is now triggered from `startDax()` (idempotent), and saved **rates** restore too. **5. `fix(dax-iq): auto-start DAX so a DAX-IQ channel works without first enabling DAX audio`** — on PipeWire/macOS the DAX-IQ `enable → stream create` wiring is established lazily inside `startDax()` (Windows wires it at construction). So enabling a DAX-IQ channel *before* DAX audio was started silently dropped the request — nothing reached the radio and the meter never moved. Enabling a DAX-IQ channel now auto-starts DAX and creates the triggering channel directly; once the bridge is up the guard is inert and `startDax()`'s own connection handles later enables (no double-create). *(Open design note for the maintainer: this auto-starts DAX **audio** as a side effect — the small, additive fix. The alternative is to un-lazy the DAX-IQ connections so they need no DAX-audio bridge at all; happy to take that route if preferred.)* ## Verified Live on a **FLEX-6700**: - All four rates (24/48/96/192 k) switch and persist across enable → disable → enable. - Meter tracks signal across the real dynamic range: noise floor ~9%, strong AM carrier ~19%, super-strong broadcast AM ~74% — no pegging. (Calibration: −66 dBFS noise, −25.6 dBFS broadcast, confirming int16 full-scale.) - Mixed on/off state (e.g. ch1+ch3 on at 96k/24k, ch2+ch4 off) restores **exactly** on boot. ## Scope - 4 commits, `src/gui/DaxIqApplet.{cpp,h}` + one call added to `src/gui/MainWindow.cpp::startDax()`. All portable Qt/STL — no platform guards needed. ## Test plan - [x] Each commit builds standalone; full build clean (Linux). - [x] **Live A/B on a FLEX-6700:** rate switching (24/48/96/192 k persist across enable), dBFS meter response (noise ~9% → strong AM ~19% → super-strong broadcast ~74%, no peg), freeze-on-disable/unbind, startup restore of mixed on/off state. - [x] **CI green on all three platforms:** Linux build · Windows (MSVC) · macOS (clang) · CodeQL · accessibility. - [x] **Runtime-verified on Linux, Windows, and macOS** native builds against the same FLEX-6700: meter, rate switching, and startup state all work. The auto-start fix (commit 5) was confirmed live on macOS — a DAX-IQ channel now brings the meter alive on the first enable with no prior DAX-audio step. --- 73, Ozy **K6OZY** AI compute partnership: [cloaked.agency](https://cloaked.agency) — drafted with Don, our VP of Engineering agent, on Linux dev stack (`nobara-dell`), live-verified against a FLEX-6700.
…r#3521) ## Summary DAX-IQ has **never delivered samples in-app** on Linux/macOS. The CHANGELOG advertises "DAX IQ pipe output is Linux/macOS only" as a working feature, and **aethersdr#2529** reports it producing zero/empty IQ payloads. This is the root-cause fix: **three independent defects in the data path** that, together, prevented any IQ from reaching the pipe. ## Root cause (3 defects, all in `DaxIqModel`) **1. The pipe was never created on a normal enable.** `createPipe()` was only called inside a rate-*change* guard (`if (newRate != s.sampleRate)`), but a fresh stream is born at `daxiq_rate=48000`, which equals the model's default sample rate — so enabling a channel produced no rate delta and the FIFO + its PipeWire source node were never built. → fire `createPipe` on first appearance (`isNew`) as well. **2. Even when triggered, the FIFO open raced and failed.** The old path fire-and-forgot `pactl load-module`, slept 200 ms, then `::open(O_WRONLY|O_NONBLOCK)` — which returns `ENXIO` if the module hasn't attached its read side yet (the "cannot open IQ pipe" warning). → create the FIFO ourselves first (`mkfifo`), load the pipe-source module **synchronously** (so we know it succeeded and can unload it by index), and open `O_RDWR|O_NONBLOCK` (which never `ENXIO`s even with no reader yet). The FIFO also moves off world-writable `/tmp` to `$XDG_RUNTIME_DIR/aethersdr/iq-N.pipe` mode `0600`, mirroring the existing DAX-audio hardening. **3. The payload was decoded with the wrong endianness.** `dax_iq` is the one stream type the radio sends as `payload_endian=little`; the client force-swapped it as big-endian (`qFromBigEndian`), byte-reversing every `float32` into a denormal ≈ 0 — a flat meter and a pipe full of near-zero. **This is the likely mechanism behind aethersdr#2529's "zero-byte payload."** → honor little-endian (`qFromLittleEndian`); a correct no-op on an LE host. ## Verified Live on a **FLEX-6700 (RX-only)** from a Linux/PipeWire host: - The `aethersdr-iq-1` PipeWire source appears on enable (was absent before); FIFO `/run/user/<uid>/aethersdr/iq-1.pipe` present, mode `0600`. - `parec` off that source captured **768 KB @ ~384 KB/s** (= `float32 × 2ch × 48 kHz`), **98% non-zero**, clean small-integer IQ that only resolves correctly decoded little-endian (big-endian reads as denormals). - Note: the pipe exposes the radio's **native int16-scale `float32`** (raw baseband amplitude, magnitudes up to ~32768), matching DAX-audio convention — **not** normalized `[-1, 1]`. This is a data convention, not a wire-type guarantee. ## Scope - 3 commits, `src/models/DaxIqModel.{cpp,h}` only. - New FIFO/`pactl` plumbing is entirely `#ifndef Q_OS_WIN` (Windows uses its own DAX path); the endian decode is platform-independent. ## Test plan - [x] Each commit builds standalone; full build clean (Linux, gcc 15.2.1, Qt 6.10.3). - [x] **Live A/B on a FLEX-6700 (RX-only):** before → no `aethersdr-iq-*` node, flat meter; after → named source + real IQ. `parec` off the source captured **768 KB @ ~384 KB/s** (`float32 ×2ch ×48 kHz`), **98% non-zero**, resolving correctly only as little-endian (big-endian reads as denormals ≈ 0 — the aethersdr#2529 "zero payload"). - [x] **CI green on all three platforms:** Linux build · **Windows (MSVC)** · **macOS (clang)** · CodeQL · accessibility. - [x] **Runtime-confirmed on Windows and macOS** native builds against the same FLEX-6700: DAX-IQ works — the level meter tracks live signal, confirming the (platform-independent) little-endian decode. The full end-to-end **pipe** is proven on Linux via `parec` above; the FIFO/`pactl` plumbing is non-Windows (`#ifndef Q_OS_WIN`) and, since `pactl` is a PipeWire/PulseAudio tool, the OS pipe node materializes on Linux specifically — so the macOS confirmation here is of the decode (via the meter), not a macOS pipe node. Closes aethersdr#2529 --- 73, Ozy **K6OZY** AI compute partnership: [cloaked.agency](https://cloaked.agency) — drafted with Don, our VP of Engineering agent, on Linux dev stack (`nobara-dell`), live-verified against a FLEX-6700.
…ethersdr#3522) ## Summary **Stacked on aethersdr#3521** (the core fix that makes DAX-IQ actually deliver samples). Because both PRs target `main`, this one currently shows **all 8 commits** — the first 3 belong to aethersdr#3521; the **5 commits unique to this PR** are the UX/state fixes listed below. Once aethersdr#3521 merges, this diff auto-reduces to just those 5. **Please review/merge aethersdr#3521 first.** With IQ finally flowing, the DAX-IQ applet's UX/state handling — never exercised because the feature never worked — needed fixing: the level meter, sample-rate switching, and enable/disable/startup state. Four focused commits. > Review note for the meter: DAX-IQ is the radio's **native int16-scale `float32`** (raw amplitude, ~32768 full-scale), not normalized `[-1, 1]` — so the meter is scaled accordingly (see commit 1). This is a radio data convention, not a wire-type guarantee. ## Fixes **1. `fix(dax-iq): scale the level meter in dBFS for raw int16 IQ`** — the meter did `rms * 200` assuming normalized `[0, 0.5]` IQ, so it **pegged at 100% on the noise floor**. It now maps RMS to **dBFS over a `[-70, -10]` window** (a level/overload meter: noise sits low, the bar fills toward digital overload) with attack/decay ballistics matching the DAX-audio meter. *Before (pegged `×200`) vs after (responsive dBFS):* | Before — `rms × 200` pegs on the noise floor | After — dBFS, tracking signal across rates | |---|---| |  |  | **2. `fix(dax-iq): apply and persist the selected sample rate across enable`** — picking 24/96/192 k reverted to 48 k: `createStream` sent no rate (radio defaults 48 k) and `setSampleRate` dropped the selection when no stream existed yet. The chosen rate is now remembered and re-applied (`stream set … daxiq_rate=`) once the stream exists; the combo retains the user's choice across disable; and the transient 48k UI flip on enable is suppressed. **3. `fix(dax-iq): zero the level meter when a channel is disabled or unbound`** — a level update queued just before a channel was switched off (or unbound from its panadapter, `pan=0x0`) could re-freeze the bar at its last value. The meter now drops to 0 whenever a channel isn't actively bound and receiving. **4. `fix(dax-iq): restore persisted-enabled IQ channels and rates on startup`** — enabled channels showed "On" at launch but had no stream (dead). On Linux/macOS the IQ↔model wiring is established lazily inside `MainWindow::startDax()`, so the applet's connect-time restore fired before the wiring existed and was dropped. The restore is now triggered from `startDax()` (idempotent), and saved **rates** restore too. **5. `fix(dax-iq): auto-start DAX so a DAX-IQ channel works without first enabling DAX audio`** — on PipeWire/macOS the DAX-IQ `enable → stream create` wiring is established lazily inside `startDax()` (Windows wires it at construction). So enabling a DAX-IQ channel *before* DAX audio was started silently dropped the request — nothing reached the radio and the meter never moved. Enabling a DAX-IQ channel now auto-starts DAX and creates the triggering channel directly; once the bridge is up the guard is inert and `startDax()`'s own connection handles later enables (no double-create). *(Open design note for the maintainer: this auto-starts DAX **audio** as a side effect — the small, additive fix. The alternative is to un-lazy the DAX-IQ connections so they need no DAX-audio bridge at all; happy to take that route if preferred.)* ## Verified Live on a **FLEX-6700**: - All four rates (24/48/96/192 k) switch and persist across enable → disable → enable. - Meter tracks signal across the real dynamic range: noise floor ~9%, strong AM carrier ~19%, super-strong broadcast AM ~74% — no pegging. (Calibration: −66 dBFS noise, −25.6 dBFS broadcast, confirming int16 full-scale.) - Mixed on/off state (e.g. ch1+ch3 on at 96k/24k, ch2+ch4 off) restores **exactly** on boot. ## Scope - 4 commits, `src/gui/DaxIqApplet.{cpp,h}` + one call added to `src/gui/MainWindow.cpp::startDax()`. All portable Qt/STL — no platform guards needed. ## Test plan - [x] Each commit builds standalone; full build clean (Linux). - [x] **Live A/B on a FLEX-6700:** rate switching (24/48/96/192 k persist across enable), dBFS meter response (noise ~9% → strong AM ~19% → super-strong broadcast ~74%, no peg), freeze-on-disable/unbind, startup restore of mixed on/off state. - [x] **CI green on all three platforms:** Linux build · Windows (MSVC) · macOS (clang) · CodeQL · accessibility. - [x] **Runtime-verified on Linux, Windows, and macOS** native builds against the same FLEX-6700: meter, rate switching, and startup state all work. The auto-start fix (commit 5) was confirmed live on macOS — a DAX-IQ channel now brings the meter alive on the first enable with no prior DAX-audio step. --- 73, Ozy **K6OZY** AI compute partnership: [cloaked.agency](https://cloaked.agency) — drafted with Don, our VP of Engineering agent, on Linux dev stack (`nobara-dell`), live-verified against a FLEX-6700.
Summary
DAX-IQ has never delivered samples in-app on Linux/macOS. The CHANGELOG advertises "DAX IQ pipe output is Linux/macOS only" as a working feature, and #2529 reports it producing zero/empty IQ payloads. This is the root-cause fix: three independent defects in the data path that, together, prevented any IQ from reaching the pipe.
Root cause (3 defects, all in
DaxIqModel)1. The pipe was never created on a normal enable.
createPipe()was only called inside a rate-change guard (if (newRate != s.sampleRate)), but a fresh stream is born atdaxiq_rate=48000, which equals the model's default sample rate — so enabling a channel produced no rate delta and the FIFO + its PipeWire source node were never built. → firecreatePipeon first appearance (isNew) as well.2. Even when triggered, the FIFO open raced and failed. The old path fire-and-forgot
pactl load-module, slept 200 ms, then::open(O_WRONLY|O_NONBLOCK)— which returnsENXIOif the module hasn't attached its read side yet (the "cannot open IQ pipe" warning). → create the FIFO ourselves first (mkfifo), load the pipe-source module synchronously (so we know it succeeded and can unload it by index), and openO_RDWR|O_NONBLOCK(which neverENXIOs even with no reader yet). The FIFO also moves off world-writable/tmpto$XDG_RUNTIME_DIR/aethersdr/iq-N.pipemode0600, mirroring the existing DAX-audio hardening.3. The payload was decoded with the wrong endianness.
dax_iqis the one stream type the radio sends aspayload_endian=little; the client force-swapped it as big-endian (qFromBigEndian), byte-reversing everyfloat32into a denormal ≈ 0 — a flat meter and a pipe full of near-zero. This is the likely mechanism behind #2529's "zero-byte payload." → honor little-endian (qFromLittleEndian); a correct no-op on an LE host.Verified
Live on a FLEX-6700 (RX-only) from a Linux/PipeWire host:
aethersdr-iq-1PipeWire source appears on enable (was absent before); FIFO/run/user/<uid>/aethersdr/iq-1.pipepresent, mode0600.parecoff that source captured 768 KB @ ~384 KB/s (=float32 × 2ch × 48 kHz), 98% non-zero, clean small-integer IQ that only resolves correctly decoded little-endian (big-endian reads as denormals).float32(raw baseband amplitude, magnitudes up to ~32768), matching DAX-audio convention — not normalized[-1, 1]. This is a data convention, not a wire-type guarantee.Scope
src/models/DaxIqModel.{cpp,h}only.pactlplumbing is entirely#ifndef Q_OS_WIN(Windows uses its own DAX path); the endian decode is platform-independent.Test plan
aethersdr-iq-*node, flat meter; after → named source + real IQ.parecoff the source captured 768 KB @ ~384 KB/s (float32 ×2ch ×48 kHz), 98% non-zero, resolving correctly only as little-endian (big-endian reads as denormals ≈ 0 — the DAX IQ streams report endpoint_type=Display with zero-byte payload — should AE be requesting a DAX endpoint? #2529 "zero payload").parecabove; the FIFO/pactlplumbing is non-Windows (#ifndef Q_OS_WIN) and, sincepactlis a PipeWire/PulseAudio tool, the OS pipe node materializes on Linux specifically — so the macOS confirmation here is of the decode (via the meter), not a macOS pipe node.Closes #2529
73, Ozy K6OZY
AI compute partnership: cloaked.agency — drafted with Don, our VP of Engineering agent, on Linux dev stack (
nobara-dell), live-verified against a FLEX-6700.