[codex] Fix panadapter restore across reconnects#3429
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes stale/dead panadapter behavior after reconnect by reconciling prior-session UI models with the radio-restored GUI client state at connect time, so restored panadapters/slices reuse existing UI wiring while truly missing ones are pruned.
Changes:
- Stage existing slice/panadapter models at connect time, reclaiming them when matching IDs reappear in status, and pruning only those that never return.
- Adjust ownership handling so staged panadapters are treated as “known” while still removing explicitly foreign panadapters when a different
client_handleappears. - Prevent the panadapter “connecting” overlay from being cleared by queued frame callbacks while disconnected, and configure reclaimed pans by ID (not just the active pan).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/models/RadioModel.h | Adds stale-session model storage and generation tracking, and updates configurePan to target a specific pan ID. |
| src/models/RadioModel.cpp | Implements connect-time staging/reclaim/prune for slices and panadapters; updates status/ownership handling and per-pan configuration. |
| src/gui/MainWindow.cpp | Keeps the panadapter connection animation from being cleared while disconnected. |
There was a problem hiding this comment.
Thanks @rfoust — the stage-on-connect / reclaim-by-ID / prune-what-didn't-return shape is a clean way to fix this, and the surgical scope (only MainWindow.cpp + RadioModel.{h,cpp}) looks appropriate.
A few things worth a closer look before merge:
1. Hard-coded 2 s prune window in registerAsGuiClient (RadioModel.cpp:2291)
sendCmd("slice list",
[this](int code3, const QString& body) {
const quint64 restoreGeneration = m_sessionModelGeneration;
QTimer::singleShot(2000, this, [this, restoreGeneration]() {
pruneStaleSessionModels(restoreGeneration);
});The timer is armed as soon as the slice list callback fires — i.e. when the slice list response comes back, not when all the per-slice/per-pan status messages have actually been replayed. On a healthy LAN that's plenty; on a routed/SmartLink reconnect with several slices + pans (status burst can stretch out past a second), or if the radio is rebooting and a second reconnect race-stacks, there's a real risk of pruning a panadapter that would have been reclaimed by a status message arriving at ~2.1 s. The symptom would be the same dead pan this PR is fixing, just rarer.
Two options that seem better than the fixed window:
- Trigger the prune when some explicit completion signal arrives (e.g. an empty-pending-deferred-status sweep, or the activity meter going idle for N ms), rather than a fixed delay from the
slice listreply. - At minimum, bump it to something safer (~5 s) and add a debug log when prune actually deletes something so this is observable in field logs.
The generation counter correctly guards the disconnect-during-window race — good — but it doesn't help with this "slow status replay" case since the generation hasn't moved.
2. Stream IDs across a radio reboot when reclaiming PanadapterModel
ensureOwnedPanadapter reclaims by panId, preserving the PanadapterModel instance and its existing signal/slot connections. After a radio reboot (vs. a TCP drop), the underlying pan_stream_id / wf_stream_id will be re-issued by the radio, and the old IDs are now stale w.r.t. m_panStream. I think this works out (incoming status will call applyStatus and re-emit waterfallIdChanged, the existing updateStreamFilters connection still fires, etc.), but it's worth double-checking that:
m_panStreamdoesn't end up with a registration for the old stream ID lingering from the prior session, andsetWaterfallConfigured(false)(which you do instageSessionModelsForReconnect) is enough to force re-configureWaterfall()for the new IDs.
A git diff --check + clean build is fine but won't catch this — would be good to verify on real hardware with a reboot, not just a disconnect/reconnect.
3. Stale-pan cleanup in onStatusReceived Ignore branch (RadioModel.cpp:~4113)
if (auto it = m_stalePanadapters.find(panId); ...) {
rejectedPan = it.value();
m_stalePanadapters.erase(it);
} else if (kvs.contains(QStringLiteral("client_handle"))) {
rejectedPan = m_panadapters.take(panId);
}This now removes an owned (non-stale) pan whenever an Ignore decision is reached with an explicit client_handle kv — that's new behavior, not just a stale-map cleanup. If classifyOwnedStatus ever returns Ignore for an already-claimed pan because a transient/partial status kv arrived (e.g. a status fragment without our handle), we'd silently rip down the user's panadapter. Worth a sanity check that Ignore + kvs.contains("client_handle") is only reached for genuinely-foreign claims.
Smaller notes
configurePansignature change to takepanIdand thepan->panId()plumbing inhandlePanadapterStatusis the right fix for the multi-pan case — nice catch.MainWindow.cppfinishPanadapterConnectionAnimationearly-return when disconnected is the right shape; the overlay stays put for reconnect.- Memory ownership looks correct — the stage maps hold the same
this-parented pointers, anddeleteLater()on prune/foreign-takeover is the Qt-idiomatic path.
Otherwise looks good. Mostly the prune-window timing is what I'd want to harden before this lands.
🤖 aethersdr-agent · cost: $12.7698 · model: claude-opus-4-7
|
Pushed a follow-up commit addressing the review feedback: Changes made:
I also rechecked the stream-ID concern: disconnect still calls Validation:
The local build completed and linked I still recommend real-radio reboot testing before merge because the status replay timing is hardware/runtime behavior rather than something the build can prove. |
|
Ran a real-radio reboot test of this branch — the validation noted as the outstanding item. Happy to report it passes. Build: Method: Connected with 1 panadapter + 2 slices (A 21.074 DIGU, B 21.172 DIGU), spectrum and waterfall both live. Triggered a genuine radio reboot via Radio Setup → Reboot Radio (so the radio re-issues fresh Result:
Caveat: this was a single-panadapter configuration (two slices on one pan), so it exercises the reclaim-by-ID + stream re-registration path but not the per-pan-vs-active-pan waterfall fix ( Screenshots captured if useful. Nice work @rfoust — the reboot path behaves exactly as intended. 73 |
|
Follow-up: re-ran with two panadapters to close the caveat from my previous comment, and it passes cleanly. Setup: Top pan with Slices A (21.074 DIGU) + B (21.172 DIGU); a second pan with Slice C (21.200 USB). Both spectrum + waterfall live before the reboot. Reboot method this time: physical power-button reboot on the FLEX-6600 (not the in-app button — see note below), with AetherSDR left running so the in-memory session staging/reclaim path is exercised. Result after auto-reconnect (~40s):
So the multi-pan reboot path looks solid on real hardware. 👍 Unrelated observation while testing (flagging, not blocking): the in-app Reboot Radio button in Radio Setup was present on first launch, but after one reboot/reconnect cycle it was gone from the dialog (Radio Information section) and didn't return on close+reopen. Had to use the physical button for the second test. Since the remote-reboot button (#3334) and the reconnect/session work both live on this branch, it might be a side effect of the session-restore changes — but it could equally be a pre-existing persisted-dialog quirk; I haven't confirmed root cause. Worth a quick look. 73 |
|
Re the reboot-button observation in my earlier comment — I dug into it, and good news: it's not a side effect of this PR's session-restore work. Root cause: the "Reboot Radio" button (#3334) is correctly added on every open, but Fixed separately as a styling tweak in #3441 — nothing for this PR to address. Apologies for the earlier red herring! |
Move reconnect cleanup to the post-connect restore path so transient disconnects keep existing panadapter UI alive while the radio reports the new session state. Reclaim matching slice and panadapter models, prune only models that are not restored, and keep the reconnect overlay visible until a live frame arrives after reconnection. Co-authored-by: Codex <noreply@openai.com>
Address PR review feedback by widening the reconnect restore prune grace window, limiting foreign-owner cleanup to staged stale panadapters, and configuring restored waterfalls by their owning panadapter rather than the active pan only. Co-authored-by: Codex <noreply@openai.com>
…ross-radio Review follow-ups on the reconnect staging/reclaim/prune design: 1. Reclaim staged pans only on a confirmed client_handle match. Staged pans are no longer treated as "known" by classifyOwnedStatus, so a handle-less status for a stale ID defers into m_pendingPanStatuses (replayed on claim) instead of Apply-reclaiming an ID that may have been reassigned to another client (SmartSDR) after a radio reboot. The foreign-owner-on-claimed-pan log is now a qCWarning. 2. Re-wire per-pan FPS / waterfall-line-duration reconcilers for reclaimed pans. Disconnect tears these connections down and only wirePanadapter() (via panadapterAdded) re-created them; reclaimed pans never re-emit panadapterAdded. New panadapterReclaimed signal + MainWindow::wirePanReconcilers() extracted from wirePanadapter(). 3. Re-assert TX on reclaimed TX slices (aethersdr#145 semantics): after a reboot the radio recreates the slice with tx=1 but a dead tx_client_handle; fresh slices get "slice set N tx=1" from onSliceAdded, reclaimed slices now send it from the reclaim path. 4. Cross-radio guard: record chassis_serial at disconnect and drop (not stage) previous-session models when connecting to a different radio — slice indexes and stream IDs collide across radios, and a reclaimed SliceModel would drain queued commands at the wrong radio. LAN checks the discovery serial at stage time; WAN re-checks when the "info" reply delivers chassis_serial. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
658c46d to
824762a
Compare
|
Did a deep follow-up review of this PR focused on three questions: do we reclaim the right pan/slices on reconnect, can we ever capture IDs that belong to SmartSDR (or another client), and do orphaned slices get cleaned up. The stage-at-connect / reclaim-by-ID / prune-what-didn't-return architecture is right, and it correctly formalizes what 1. Stale pans could be reclaimed without ownership confirmation (the SmartSDR-capture risk) 2. Reclaimed pans lost their FPS / waterfall-line-duration reconcilers 3. Reclaimed TX slices skipped the #145 TX re-claim 4. Staging was keyed only by ID — switching radios would reclaim across radios Also verified during review: no textual conflicts with Remaining known limitation (unchanged from the earlier review round): the prune window is still a fixed 5 s armed from the Thanks @rfoust for the clean architecture here, and @M7HNF-Ian for the hardware validation runs — a TX-after-reboot check on real hardware would be a welcome confirmation of fix 3. |
jensenpat
left a comment
There was a problem hiding this comment.
Approved after extensive review (see detailed findings comment above). Rebased onto main and hardened in 824762a: handle-confirmed pan reclaim (no SmartSDR ID capture), reclaimed-pan reconciler re-wiring, TX re-claim on reclaimed slices, and a cross-radio chassis_serial guard. Build verified locally; hardware reboot tests by @M7HNF-Ian.
|
Additional hardware validation on the rebased + hardened head (824762a): ~40 forced reconnect collisions with 1 and 2 panadapters open — 0 empty/dead panadapters after reconnect. Also confirmed a second, non-patched client connected to the same radio no longer ends up with orphaned pans, since the patched client no longer creates duplicate panafalls during its reconnect window. Merging once CodeQL completes. |
…ts (aethersdr#3429) ## Summary This fixes the dead/stale panadapter behavior seen after reconnect by reconciling previous-session UI models with the radio-restored GUI client state at connect time. When a connection comes back, AetherSDR now stages the previous slice and panadapter models, reclaims the ones whose IDs reappear in radio status, and prunes only models that do not return. This preserves existing UI wiring for restored panadapters while still removing genuinely stale session data. ## Context The original symptom was an extra non-functional panadapter/waterfall after reconnect (aethersdr#3212 family). An earlier local cleanup attempt moved too much cleanup to disconnect time, which caused existing panadapters to disappear during radio reboot while the reconnect dialog was active. This PR replaces that approach with connect-time reconciliation. ## Details - Preserve current slice and panadapter models through disconnect. - Stage prior session models at connect time, then reclaim models whose IDs return in slice/pan status. - Remove only staged models that do not reappear after the reconnect restore window (5 s, generation-guarded). - Reclaim staged panadapters only on a confirmed client_handle match: handle-less statuses for staged IDs defer into m_pendingPanStatuses (replayed on claim) so an ID reassigned to another client (SmartSDR) after a reboot can never be captured; explicitly foreign statuses remove the stale local model. - Re-wire per-pan FPS / waterfall line-duration reconcilers for reclaimed pans via a new panadapterReclaimed signal (disconnect tears them down; reclaimed pans never re-emit panadapterAdded). - Re-assert TX (slice set N tx=1, aethersdr#145 semantics) on reclaimed TX slices, since they skip onSliceAdded. - Cross-radio guard: record chassis_serial at disconnect and drop, rather than stage, previous-session models when connecting to a different radio (LAN checks discovery serial at stage time; WAN re-checks on the info reply). - Keep the panadapter connecting overlay visible during disconnect so stale queued frame callbacks do not clear it prematurely. - Re-push dimensions and dBm setup for the specific reclaimed panadapter, not just the active panadapter, so multi-panadapter restores do not leave non-active panes at radio defaults. ## Validation - Real-hardware reboot tests by @M7HNF-Ian (FLEX-6600, single- and two-pan, in-app reboot and physical power-button): pans/slices restored, waterfalls live, overlay behavior correct. - Rebased onto main (clean); local RelWithDebInfo build links with only pre-existing warnings. Squashed-from: aethersdr#3429 Co-authored-by: Robbie Foust <2991296+rfoust@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pat Jensen <patjensen@gmail.com>
…n (streams verb) (#3856) (#3857) ## What Implements the radio-side display-stream **inventory + leak detection** feature requested in #3856. It makes leaked / duplicate / lingering panadapter & waterfall streams *on the radio* directly observable — a class of bug `get pans` can never surface, because the client always cleans up its own view on the radio's `removed` echo. Two independent radio-authoritative layers behind one `streams` bridge verb: ### Layer A — `streams` (VITA-49 UDP truth) `PanadapterStream` records any FFT/waterfall packet for a stream id that was **ever registered this session but is no longer registered** — a stream we once owned and let go of that the radio keeps transmitting. Keyed off *ever-registered ∧ not-now-registered* (not "live set non-empty"), so it stays detectable after `pan close all` and never mis-flags a freshly-created stream's registration-lag window. Catches **continued-stream** leaks (the #268 class). `streams reset` re-baselines. ### Layer B — `streams radio` (status-bookkeeping truth) `RadioModel` now accumulates the radio-authoritative display-object set (`m_radioDisplayPans` / `m_radioDisplayWaterfalls`) from **all** `display pan` / `display waterfall` status — ours, foreign, and orphan — pruned on the matching `removed`, independent of `m_panadapters` (which only holds objects *we* own). The pure `DisplayInventory::classify()` labels each object `ours` / `foreign` / `orphan` and flags **leaked waterfalls** (parent panadapter gone) — the #3843 fingerprint. This catches **resource-level lingering that emits no UDP**, which Layer A cannot see. `DisplayInventoryPolicy` is a pure, header-only function (mirroring `RadioStatusOwnership` / `SliceRecreatePolicy`), with a unit test (`display_inventory_policy_test`, 12 assertions) covering clean, leaked-parent-gone, foreign-vs-orphan, no-parent-reported, and unknown-own-handle cases. Closes #3856. ## Bridge surface | action | layer | effect | |---|---|---| | `streams` | A | `registeredPanStreams`, `registeredWfStreams`, `orphanStreams` (`streamId`, `kind`, `packets`, `age_ms`) | | `streams radio` (alias `inventory`) | B | radio display-object set classified ours/foreign/orphan + `leakedWaterfalls` | | `streams reset` | A | clear the orphan tally | All `streams` actions are RX/observe only — none sends a radio command or keys TX. Docs in `docs/automation-bridge.md`. ## Proven on a live FLEX-8400M (firmware 4.2.18) `streams radio` tracked the inventory **exactly** across an add/close cycle, every object `ownership: ours`, no false-positive leaks: ``` baseline: radioPanCount=1 radioWaterfallCount=1 leakCount=0 after add: radioPanCount=2 radioWaterfallCount=2 leakCount=0 (both ours, parentMissing=false) after close:radioPanCount=1 radioWaterfallCount=1 leakCount=0 ``` **Key firmware finding (answers the open #3843 question):** on this radio, `display pan remove` **alone** makes the radio *also* remove the waterfall — it echoes `display waterfall <id> removed`. A leak-repro build that deliberately omitted `display panafall remove` produced the **same clean result** (`leakedWaterfalls=[]`, waterfall count 2→1, removal echo observed). So **neither #3843 symptom — continued UDP nor lingering resource — reproduces on 8000-series / 4.2.18**: the radio auto-cascades. The #3855 close-path fix remains correct per FlexLib and matters for firmware that does **not** cascade (the #268 class). Because this firmware never leaves an orphan, **Layer B's positive leak detection is covered by the unit test** (synthetic parent-gone waterfall → flagged); live, it correctly reads clean with no false positives. Layer A likewise reads `orphanCount:0` here (this firmware stops the UDP), and is the guard for firmware that keeps streaming. ## Test plan - [x] `display_inventory_policy_test` — 12/12 pass (pure classification logic, incl. the leaked-waterfall case). - [x] Full app builds clean (Ninja, RelWithDebInfo, arm64). - [x] Live FLEX-8400M: `streams` + `streams radio` inventory accurate across add/close; no false-positive leaks; firmware cascade behavior characterized. - [ ] Maintainer review. ## Notes / follow-ups - The empirical cascade finding means Layer B's live positive-catch can only be demonstrated on #268-class firmware (which leaves the orphan). The detection logic is unit-tested. - Layer B is positioned to plug into reconnect/duplicate-pan reconciliation (#3429) — surfacing a radio-side pan the client doesn't own — as a future step. ## Related - #3856 — this feature request. - #3855 / #3843 — the panafall close-path fix this observability complements (the *send* side; this is the *observe* side). - #268 — continued-UDP waterfall leak on older firmware (the case Layer A guards). - #3429 — reconnect reconciliation (future Layer B integration point). 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Jeremy [KK7GWY] <kk7gwy@aethersdr.com>
Summary
This fixes the dead/stale panadapter behavior seen after reconnect by reconciling previous-session UI models with the radio-restored GUI client state at connect time.
When a connection comes back, AetherSDR now stages the previous slice and panadapter models, reclaims the ones whose IDs reappear in radio status, and prunes only models that do not return. This preserves existing UI wiring for restored panadapters while still removing genuinely stale session data.
Context
The original symptom was an extra non-functional panadapter/waterfall after reconnect. An earlier local cleanup attempt moved too much cleanup to disconnect time, which caused existing panadapters to disappear during radio reboot while the reconnect dialog was active. This PR replaces that approach with connect-time reconciliation.
Details
client_handlearrives.Validation
git diff --checkcmake --build build --parallelThe local build completed and linked
AetherSDR.app/Contents/MacOS/AetherSDR. The build still reports unrelated existing warnings in other files.