Skip to content

Fix DAX coexistence stream ownership#2266

Closed
jensenpat wants to merge 2 commits intoten9876:mainfrom
jensenpat:codex/coexistence-dax-fix
Closed

Fix DAX coexistence stream ownership#2266
jensenpat wants to merge 2 commits intoten9876:mainfrom
jensenpat:codex/coexistence-dax-fix

Conversation

@jensenpat
Copy link
Copy Markdown
Collaborator

@jensenpat jensenpat commented May 1, 2026

Summary

Fixes #2194. Follows up on #2222 and #2226, and covers the DAX/TCI coexistence cluster around #2203, #2210, #2223, and #2224. #2221 is intentionally out of scope because it is packaging/runtime related rather than radio/DAX flow behavior.

This PR stabilizes AetherSDR coexistence with SmartSDR DAX2 and FlexRadio API/firmware 4.2.18 ownership semantics. AetherSDR no longer claims DAX TX at GUI attach time, keeps Windows external-DAX2 paths route-only, preserves hosted DAX on macOS/PipeWire, keeps TCI TX explicit, and makes LAN VITA UDP startup resilient when another Flex client is already using a radio-side IP/port tuple.

Architecture Changes

Explicit DAX TX Policy

DAX TX stream creation is now reason-gated through a small policy helper instead of being an implicit side effect of generic audio setup.

Reasons are explicit:

  • HostedDaxBridge for AetherSDR-owned hosted DAX on macOS/PipeWire.
  • TciTxAudio for TCI TX paths that truly need AetherSDR to own a dax_tx stream.
  • ExternalDaxRouteOnly for Windows/SmartSDR DAX2 routing where AetherSDR may set transmit dax=1/0 but must not create or steal the stream.
  • GenericAudioRecreate for PC audio recreation, which must not create dax_tx.

RadioModel::ensureDaxTxStream() now requires a reason, and there are no argless call sites left. Generic createAudioStream() no longer creates DAX TX. GUI attach still does not create DAX TX.

Windows External DAX2 Coexistence

On Windows, AetherSDR is treated as external-DAX2 route-only. It keeps the radio-side TX DAX flag aligned with the active TX slice mode so SmartSDR DAX2 can route digital TX audio, but it does not create an AetherSDR-owned dax_tx stream for GUI attach, PC Audio, generic audio recreation, or route-only mode.

Foreign SmartSDR-owned DAX TX/RX streams are logged and ignored rather than adopted into AetherSDR state.

Hosted DAX and TCI

Hosted DAX platforms still create an AetherSDR-owned dax_tx stream when the hosted bridge actually needs one. TCI TX now requests DAX TX explicitly as TciTxAudio, so the dependency is visible in code and logs instead of being hidden behind startup or generic audio paths.

A follow-up bug from local testing was fixed here too: bare Flex stream-create responses such as 84000000 are parsed as hexadecimal for DAX TX create responses. Without that, AetherSDR could store the wrong TX stream ID and send outbound DAX/TCI packets to a non-existent stream.

TCI TX slice handoff now also preserves the current DAX TX route while the radio status stream is between tx=0 on the old slice and tx=1 on the new slice. This avoids briefly sending transmit set dax=0 during a TCI PTT edge.

LAN VITA UDP Registration

LAN VITA UDP sockets now bind to an OS-assigned port on the selected local IPv4 address using DontShareAddress. The selected address order remains:

  • explicit bind address
  • session/probe address
  • TCP local address
  • AnyIPv4

After binding, AetherSDR primes radio:4992, sends client udpport <localPort>, and if the radio returns 0x500000A9 or Port/IP pair already in use, the LAN path closes, rebinds to a fresh ephemeral port, primes again, and retries client udpport.

The retry policy is LAN-only. SmartLink/WAN startWan, routed UDP behavior, client udp_register, and user port-forwarding assumptions were not changed.

Diagnostics

The patch adds concise logs for:

  • DAX TX policy decisions, including reason, platform, mode, allowed/denied state, stream, and owner.
  • skipped DAX TX creation with the policy note.
  • DAX TX create request/success/failure.
  • external DAX TX/RX streams observed with owner details.
  • dead/orphan DAX RX status ignored once for client_handle=0 ip=0.0.0.0.
  • LAN VITA UDP bind endpoint and bind flags.
  • client udpport request, registration, collision, retry registration, and retry failure.
  • first UDP packet timing and local port.

Compatibility Notes

Older radio/API behavior is preserved where ownership data is missing. Missing client_handle remains legacy-compatible where intended. client_handle=0 remains compatible unless paired with the explicit dead endpoint ip=0.0.0.0.

AetherSDR-hosted DAX still owns its streams on hosted-DAX platforms. Windows SmartSDR DAX2 is allowed to own the actual DAX streams while AetherSDR only maintains radio-side route state.

Validation

Local macOS validation:

  • git diff --check passes.
  • cmake --build build --target AetherSDR -- -j10 passes.
  • ctest --test-dir build --output-on-failure -j10 passes.
  • 2 successful FT8 QSOs via TCI.
  • 2 successful FT8 QSOs via AetherSDR-hosted DAX.

Test coverage added/updated:

  • DAX TX policy decisions for Windows external-DAX2, hosted DAX, generic audio recreation, and TCI.
  • DAX TX ownership adoption/ignore behavior.
  • dead/orphan DAX RX classification.
  • UDP port collision detection and WAN non-retry policy.
  • existing stream-create response parsing regressions, including bare hex stream IDs.

Windows validation is next: this branch will be pulled on Windows, built there, and tested with SmartSDR DAX2 coexistence.

👨🏼‍💻 Generated with OpenAI Codex (GPT-5.5 Pro 4/23) and tested by @jensenpat

@jensenpat jensenpat force-pushed the codex/coexistence-dax-fix branch from a3e9728 to 1ff6716 Compare May 2, 2026 02:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant