Skip to content

[dax] [panadapter] Fix multi-client startup panadapter creation#2222

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:aether/issue-2194-dax-panadapter
May 1, 2026
Merged

[dax] [panadapter] Fix multi-client startup panadapter creation#2222
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:aether/issue-2194-dax-panadapter

Conversation

@jensenpat

@jensenpat jensenpat commented May 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #2194 by making AetherSDR's startup panadapter creation resilient when SmartSDR, DAX, or another GUI/audio client is already connected to the radio.

The primary startup path now matches FlexLib v4.2.18 and creates a display pair with:

display panafall create x=100 y=100

If older firmware rejects that command, AetherSDR falls back to the legacy:

panadapter create

The fix also hardens status handling so AetherSDR can recover when panadapter or waterfall ownership status arrives out of order during multi-client startup.

Fixes #2194, #2210, #2203, #2221, #2223, #2224

Root Cause

When SmartSDR DAX is already running, the radio is not starting from a quiet single-client state. On connect, AetherSDR receives status for all current radio objects, including slices, pans, waterfalls, DAX streams, and TX/audio streams associated with other client handles.

AetherSDR correctly filters these objects by client_handle so it does not accidentally adopt SmartSDR/DAX-owned state. The failure came from the interaction between that filtering and the startup fallback:

  1. AetherSDR connects and registers as a GUI client.
  2. The radio replays status for multiple clients.
  3. AetherSDR rejects non-owned slices and panadapters, as intended.
  4. slice list can still return non-empty because slices exist globally on the radio.
  5. AetherSDR may still have no owned slice after filtering.
  6. The deferred startup fallback creates a default pan and slice for AetherSDR.
  7. That fallback used the older bare panadapter create command and waited too heavily on later pan ownership status.
  8. If display pan status was delayed, interleaved, or first arrived without client_handle, AetherSDR could fail to instantiate a PanadapterModel.

Without a PanadapterModel, FFT and waterfall stream IDs are not registered with PanadapterStream, so VITA-49 data never reaches the UI and the startup "getting ready" path can remain stuck.

Why DAX Exposed This

The SmartSDR DAX app creates exactly the kind of multi-client environment that stresses this path:

  • the radio reports globally existing slices even when they belong to another client
  • DAX and SmartSDR stream status includes separate client_handle ownership
  • pan/waterfall/slice status can be interleaved with AetherSDR's own startup sequence
  • AetherSDR must reject other clients' objects, then create or claim its own objects

So the issue is broader than firmware 4.2.18. The newer firmware and FlexLib API made it more visible, but the underlying bug was that AetherSDR's owned-pan startup path was too dependent on later, perfectly ordered ownership status.

Implementation

This PR changes RadioModel in a few coordinated ways:

  • Adds normalized panadapter ID parsing for command responses that may return either pan=0x..., id=0x..., or a bare stream ID.
  • Adds ensureOwnedPanadapter() to create and register a PanadapterModel as soon as AetherSDR has an owned pan ID.
  • Updates manual and startup pan creation to try display panafall create x=100 y=100 first, matching FlexLib v4.2.18.
  • Keeps compatibility with older firmware by retrying panadapter create if display panafall create fails.
  • Defers early display pan status that lacks client_handle instead of dropping it permanently.
  • Replays deferred pan status once ownership is confirmed.
  • Uses owned waterfall status with panadapter=... to recover the parent pan/waterfall association if waterfall status arrives before a complete pan status.
  • Emits the existing status-bar capacity signals when pan or slice creation ultimately fails, so true radio resource exhaustion is visible instead of looking like a startup hang.

Compatibility

Older firmware should continue to work. The new command is tried first because it is what current FlexLib uses, but any rejection falls back to the legacy panadapter create command.

This is intentionally capability-based rather than a hard firmware-version gate. The radio/API behavior varies enough across model and firmware combinations that "try current command, fall back to legacy command" is safer than guessing from version strings.

Panadapter and Waterfall Sizing

display panafall create x=100 y=100 is only the creation request for the paired display object. It does not set the final UI stream dimensions.

AetherSDR still pushes real dimensions after the PanadapterModel exists with:

display pan set <pan_id> xpixels=<widget_width> ypixels=<widget_height>

That matches FlexLib's model: panadapter width/height drive display pan set ... xpixels/ypixels, while waterfall width/height do not send separate pixel sizing commands. Final FFT/waterfall sizing is therefore still controlled by AetherSDR's widget-dimension path, not by the initial x=100 y=100 create command.

User Impact

Users should be able to start AetherSDR while SmartSDR DAX or another client is already connected, as long as the radio still has available pan/slice capacity.

If the radio is genuinely out of resources, AetherSDR now routes that failure through the existing status-bar messages instead of silently failing to get a panadapter.

Validation

cmake --build build -j10
  • Ran CTest discovery:
ctest --test-dir build --output-on-failure -j10

CTest completed successfully but reported no registered tests in this build tree.

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

@jensenpat jensenpat changed the title Fix multi-client startup panadapter creation [panadapter] [dax] Fix multi-client startup panadapter creation May 1, 2026
@jensenpat jensenpat marked this pull request as ready for review May 1, 2026 00:54
@jensenpat jensenpat requested a review from ten9876 as a code owner May 1, 2026 00:54
@jensenpat jensenpat changed the title [panadapter] [dax] Fix multi-client startup panadapter creation [P0] [panadapter] Fix multi-client startup panadapter creation May 1, 2026
@jensenpat jensenpat added the priority: high High priority label May 1, 2026
@jensenpat jensenpat changed the title [P0] [panadapter] Fix multi-client startup panadapter creation [dax] [panadapter] Fix multi-client startup panadapter creation May 1, 2026

@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.

PR #2222 Review — Multi-client startup panadapter creation

Nice work on this one, @jensenpat. The PR description is thorough and the root cause analysis is excellent — understanding how the client_handle filtering interacts with the startup fallback path was the hard part, and you've nailed it.

The overall approach is sound: ensureOwnedPanadapter() centralizes pan creation logic, the display panafall createpanadapter create fallback is the right pattern, and deferring/replaying pan status without client_handle is a clean solution to the ordering problem.

A few things I'd like to flag:

1. m_pendingPanStatuses not cleared on disconnect

onDisconnected() clears m_panadapters, m_activePanId, m_ownedSliceIds, etc., but does not clear m_pendingPanStatuses. If a disconnect occurs while deferred statuses are pending, those stale entries survive into the next connection. On reconnect (possibly to a different radio), ensureOwnedPanadapter() could replay status from the previous session if a pan ID happens to collide.

Suggested fix — add to onDisconnected() alongside the existing cleanup:

m_pendingPanStatuses.clear();

2. Waterfall status path — double setWaterfallId risk

In the waterfall status handler, the new code does:

if (ownerPan && ownerPan->waterfallId().isEmpty())
    ownerPan->setWaterfallId(wfId);

Then a few lines later:

if (activeWfId().isEmpty() && ownerPan == activePanadapter())
    ownerPan->setWaterfallId(wfId);

The second call is redundant if the first already set it (since activeWfId() delegates to activePanadapter()->waterfallId()). Not a bug — setWaterfallId should be idempotent — but the second block appears to be a leftover from the original setActiveWfId() pattern. Consider removing it to avoid confusion, or adding a comment if there's a subtle ordering reason it needs to stay.

3. handleCreatedPan lambda captures this — consider lifecycle

In createDefaultSlice(), the handleCreatedPan lambda captures this, freqMhz, mode, and antenna by value, then gets copied into the inner legacy-fallback lambda. This is fine as long as the RadioModel outlives the command response, which should be guaranteed by the disconnect cleanup. Just noting it for awareness — the same pattern exists in the current code.

4. parsePanadapterCreateId falls through to bare body

parsePanadapterCreateId() falls back to normalizePanadapterId(body) when neither pan nor id keys are present. If the radio returns an unexpected response body (e.g. an error string that happens to not be caught by the code != 0 check), this could produce a garbage pan ID. The normalizePanadapterId hex parse will return the original text if toUInt fails, which then flows into ensureOwnedPanadapter. The isEmpty() guard catches empty strings but not invalid ones. Low risk in practice — just something to be aware of.

Summary

This is a well-scoped fix that addresses a real multi-client pain point. The deferred-status replay and fallback command pattern are both solid. The one actionable item is clearing m_pendingPanStatuses on disconnect (#1 above) — the rest are minor observations.

Thanks for the contribution and the detailed write-up!

The deferred pan-status queue was added in this PR to hold
display pan status that arrives without client_handle until
ownership can be confirmed.  However, onDisconnected() didn't
clear it, which leaks state across reconnects: if AetherSDR
disconnects before ownership confirms and reconnects to a
different radio, the new radio's pan IDs (typically the same
0x40000000+N range) would replay stale center/bandwidth/min-max
values from the previous session through ensureOwnedPanadapter.

One-line fix: clear the queue alongside m_panadapters in the
disconnect cleanup block.

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

ten9876 commented May 1, 2026

Copy link
Copy Markdown
Collaborator

Hi @jensenpat — Claude here. Pushed a one-line follow-up to your branch directly (3a908961):

m_pendingPanStatuses.clear();

added alongside the existing m_panadapters.clear() in onDisconnected(). Without it, the new deferred-status queue you added leaks state across disconnects: if AetherSDR queues some pre-client_handle pan status, then disconnects and reconnects to a different radio, ensureOwnedPanadapter() would replay the previous radio's pan center/bandwidth/etc onto the new radio's matching pan ID (typical 0x40000000+N range collides cleanly). Reproduced by inspection only, not in the wild — but worth fixing now since the queue is brand-new in this PR.

There's a separate, lower-severity concern with that queue that I filed as a follow-up: #2228 — entries for pans whose owner never emits a client_handle-bearing frame can sit in the map forever. In practice every Multi-Flex client emits one quickly, so the leak is bounded in size and short-lived; not blocking this PR.

Excellent work overall. Capability-based fallback (try-new, retry-legacy) is the right shape, the ensureOwnedPanadapter() factory cleans up the duplicated PanadapterModel-creation logic, and routing failure cases through panadapterLimitReached / sliceCreateFailed so the status bar surfaces real radio resource exhaustion is a nice UX touch.

73, Jeremy KK7GWY & Claude (AI dev partner)

@ten9876 ten9876 merged commit 96acbba into aethersdr:main May 1, 2026
4 checks passed
ten9876 added a commit that referenced this pull request May 1, 2026
Heavy community-contribution release:
- AetherSweep — in-panadapter SWR analyzer (jensenpat, 3 PRs)
- ShackSwitch antenna switch integration (nigelfenton, 2 PRs)
- Multi-client startup hardening: panadapter creation (#2222) +
  PC audio remote stream ownership (#2226), both jensenpat
- macOS DMG dark-theme fix (Chaosuk97)
- Windows-without-Qt6::SerialPort build fix (NF0T)
- NRL gating correctness on 6000-series (#2219)

- CMakeLists.txt / README.md / CLAUDE.md: 0.9.3 → 0.9.4
- CHANGELOG: v0.9.4 entry covers 10 PRs since v0.9.3
- WhatsNewData.cpp: regenerated from CHANGELOG

Acknowledgements:
- jensenpat — AetherSweep, multi-client startup, PC audio ownership
- nigelfenton — ShackSwitch integration
- NF0T — Windows build fix
- Chaosuk97 — macOS DMG dark theme fix

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: high High priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failure to start AetherSDR properly when either Flex CAT or DAX is already connected to radio

2 participants