Skip to content

fix(panadapter): prime spectrum widget dBm range on wire to prevent flat secondary pans on reconnect#3034

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
chibondking:fix/flat-secondary-pans-on-reconnect
May 24, 2026
Merged

fix(panadapter): prime spectrum widget dBm range on wire to prevent flat secondary pans on reconnect#3034
ten9876 merged 1 commit into
aethersdr:mainfrom
chibondking:fix/flat-secondary-pans-on-reconnect

Conversation

@chibondking

Copy link
Copy Markdown
Collaborator

Summary

  • Bug: Secondary panadapters (Slices B–H, up to 8 total) occasionally go completely flat after reconnecting to the radio — showing no signal while Slice A works normally. Switching bands restores them, and the issue is intermittent.
  • Root cause: Two compounding issues:
    1. SpectrumWidget initialises with refLevel = -50 dBm, dynamicRange = 100 dB but PanadapterModel already carries the correct saved range (default −130 → −40 dBm). Neither wirePanadapter() nor the panadapterAdded reconnect path primed the widget from the model, so every non-primary pan started with a stale widget state.
    2. If DisplayNoiseFloorEnable is on for any secondary pan, the noise-floor auto-adjust begins animating refLevel toward its target from the wrong starting point (−50 instead of −40 dBm) before the radio echoes back its saved min_dbm/max_dbm. This produces bogus dbmRangeChangeRequested values (e.g. −84.59 → +5.41 dBm). The pendingDbm guard in wirePanadapter then poisons PanadapterStream::m_dbmRanges with those values and sends them to the radio, causing the VITA-49 decoder to normalise FFT bins to a window far above the actual HF noise floor. All real signals fall off the bottom of the display. The guard re-asserts the bogus range on every subsequent levelChanged echo, creating a self-reinforcing lock-out.
  • Slice A immunity: Slice A is wired first; by the time its noise-floor auto-adjust fires, a dBm command from a separate code path has already arrived and set the correct range.

Fix

Prime sw->setDbmRange(pan->minDbm(), pan->maxDbm()) immediately after wiring levelChanged in both affected code paths (MainWindow.cpp):

  • wirePanadapter() — every new applet at initial connection
  • panadapterAdded() "existing applet" branch — every pan on reconnect

Both sites apply to all panadapters (1–8) unconditionally.

Test plan

  • Connect to radio with two or more panadapters and noise-floor auto-adjust enabled on all pans — confirm all pans display spectrum data after connect
  • Disconnect and reconnect 5–10 times — confirm no secondary pan goes flat
  • Switch bands while connected — confirm dBm range adjusts correctly on all pans
  • Connect with a single panadapter — confirm no regression
  • Verify noise-floor auto-adjust still tracks and adjusts the display correctly on all pans after connect

🤖 Generated with Claude Code

…lat secondary pans on reconnect (aethersdr#3029)

Root cause (two compounding issues):

1. SpectrumWidget default / radio model mismatch at wire time
   SpectrumWidget initialises with refLevel = -50 dBm, dynamicRange = 100 dB
   (i.e. display window −150 → −50 dBm). PanadapterModel already carries the
   correct saved range (default −130 → −40 dBm, or whatever the radio last
   reported). wirePanadapter() and the panadapterAdded reconnect path both
   skipped priming the widget from the model, so every non-primary pan (Slice
   B through H — up to 8 slices total) started in the wrong state.

2. Noise-floor auto-adjust racing the first levelChanged echo
   If DisplayNoiseFloorEnable is on for any non-primary pan, the auto-adjust
   begins animating refLevel toward its target as soon as the first FFT frames
   arrive — before the radio has had time to echo back its saved min_dbm /
   max_dbm. Because it starts from −50 dBm instead of the saved top-of-range
   (e.g. −40 dBm), the computed target is wildly wrong (e.g. +5.41 dBm with
   noise floor at −80 dBm and position 95%). sendNoiseFloorRangeCommand()
   emits dbmRangeChangeRequested with those bogus values.

   wirePanadapter()'s dbmRangeChangeRequested handler sets
   pendingDbm->active = true, calls setStreamDbmRange(bogus, waitForEcho=true)
   — poisoning PanadapterStream::m_dbmRanges so the VITA-49 decoder
   normalises incoming FFT bins to the wrong window — and fires
   "display pan set 0xXXXXXXXX min_dbm=<bogus> max_dbm=<bogus>" to the radio.
   Once the radio starts encoding FFT data for that window, all real HF signals
   fall far below the display floor, producing the flat/dead panadapter. The
   pendingDbm guard then re-asserts the bogus range whenever the radio tries to
   echo the correct values, creating a self-reinforcing lock-out.

   The primary pan (Slice A) was not affected because it is wired first and
   its dBm command arrives from a different code path before the auto-adjust
   fires. Every subsequent pan — Slices B through H — had no such early
   command and fell into the race whenever noise-floor tracking was enabled.

   The symptom was intermittent ("hard to reproduce consistently") because
   the race window is narrow: it only manifests if the noise-floor auto-adjust
   fires before the radio's pan-status echo populates levelChanged. Switching
   bands fixed it by sending a fresh display pan set min_dbm/max_dbm command,
   breaking the stale pendingDbm lock.

Fix: call sw->setDbmRange(pan->minDbm(), pan->maxDbm()) immediately after
wiring the levelChanged signal in both code paths:
  • wirePanadapter() — covers every new applet at initial connection
  • panadapterAdded() "existing applet" branch — covers every pan on reconnect

Both paths apply universally to all panadapters regardless of count (1–8).
The prime aligns the widget's refLevel/dynamicRange with the radio model's
values before any spectrum data arrives, so the noise-floor auto-adjust starts
from the correct baseline and produces sensible targets rather than values
derived from the stale constructor defaults.

If a stale m_pendingDbmRangeEcho survives from a dropped session (edge case:
connection lost mid-command), SpectrumWidget::setDbmRange's 2-second timeout
check at line 2149 clears it and applies the prime on the first call, so no
additional reset is required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chibondking

Copy link
Copy Markdown
Collaborator Author

@AetherClaude your thoughts on this?

@aethersdr-agent

Copy link
Copy Markdown
Contributor

The diagnosis looks correct after walking the code. Confirmed:

  • MainWindow::wirePanadapter() at src/gui/MainWindow.cpp:11337 connects PanadapterModel::levelChanged → sw->setDbmRange() but never seeds the widget with the model's current minDbm()/maxDbm() — so the widget keeps its constructor defaults until the radio emits the next confirmed levelChanged.
  • The panadapterAdded "existing applet" branch at src/gui/MainWindow.cpp:2816-2822 has the exact same gap.
  • PanadapterModel::minDbm()/maxDbm() accessors exist (src/models/PanadapterModel.h:31-32), so priming is trivial.

The pendingDbm guard interaction you described is the nasty part: once noise-floor auto-adjust crafts a request off the stale −50 starting point, the guard re-asserts that bogus range on each echo from the radio. Priming the widget before the noise-floor adjust path can fire short-circuits the whole cascade.

Proposed fix — add one line in each of the two sites, right after the levelChanged connect:

// src/gui/MainWindow.cpp — wirePanadapter(), after the connect at ~11337-11359
connect(pan, &PanadapterModel::levelChanged,
        sw, [sw, pendingDbm, dbmMatches, setStreamDbmRange](float minDbm, float maxDbm) {
    // ...existing guard body...
    sw->setDbmRange(minDbm, maxDbm);
});

// Prime widget from the model so noise-floor auto-adjust starts from the
// correct refLevel/dynamicRange instead of SpectrumWidget's -50/100 defaults
// (#3034). Without this, secondary pans can lock into a bogus dBm range
// via the pendingDbm guard on reconnect.
sw->setDbmRange(pan->minDbm(), pan->maxDbm());
// src/gui/MainWindow.cpp — panadapterAdded existing-applet branch, after ~2816-2822
connect(pan, &PanadapterModel::levelChanged,
        sw, [sw](float minDbm, float maxDbm) {
    if (sw->isDraggingDbmScale()) {
        return;
    }
    sw->setDbmRange(minDbm, maxDbm);
});
// Prime widget from the model on reconnect — see #3034.
sw->setDbmRange(pan->minDbm(), pan->maxDbm());

Two small notes worth checking when implementing:

  1. The prime must happen after the levelChanged connect but before the noise-floor auto-adjust path can run (i.e. before any DisplayNoiseFloorEnable wiring later in wirePanadapter()). Worth tracing the order to be sure.
  2. The prime is a direct setDbmRange on the widget, which does not round-trip through pendingDbm/setStreamDbmRange — it just fixes the widget's starting refLevel. That's the intended behavior (the radio already has the saved range), but worth eyeballing to confirm it doesn't trip dbmRangeChangeRequested or isDraggingDbmScale in some surprising way.

A maintainer can authorize the orchestrator to land this fix by adding the aetherclaude-eligible label.

73, Jeremy KK7GWY & Claude (AI dev partner)


🤖 aethersdr-agent · cost: $1.6706 · model: claude-opus-4-7

@chibondking

chibondking commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

@AetherClaude Both notes verified against the implementation:

Note 1 — ordering: The prime at line 11372 lands 121 lines before the dbmRangeChangeRequested connect at line 11493, so the guard handler isn't wired yet when the prime runs. More importantly, noise-floor auto-adjust has no signal connections in wirePanadapter at all — it's driven purely by the per-frame FFT pipeline in SpectrumWidget, which can't fire until UDP packets arrive on the VITA-49 socket. wirePanadapter runs synchronously, so there's no event-loop interleaving. Order is safe.

Note 2 — no surprising side-effects: SpectrumWidget::setDbmRange (lines 2126–2164) takes exactly two paths: early-return if m_pendingDbmRangeEcho is set and within the 2-second timeout, or fall through to applyDbmRangeImmediate. applyDbmRangeImmediate updates m_refLevel/m_dynamicRange, calls resetNoiseFloorBaseline(), and markOverlayDirty(). None of those emit dbmRangeChangeRequested or check isDraggingDbmScale. The prime is purely a state write to the widget — no round-trip, no signals fired outward.

@ten9876 ten9876 merged commit c44f1c5 into aethersdr:main May 24, 2026
5 checks passed
@ten9876

ten9876 commented May 24, 2026

Copy link
Copy Markdown
Collaborator

Merged at 06:22 UTC — thanks @chibondking, great diagnosis and fix. Filed #3047 as a small follow-up for the (#3029) issue references in the new comments — they should point at #3034 (this PR) instead. No action needed from you; tagged aetherclaude-eligible so the orchestrator can pick it up. 73, Jeremy KK7GWY & Claude (AI dev partner)

aethersdr-agent Bot added a commit that referenced this pull request May 24, 2026
… comments (#3047). Principle XI.

PR #3034 introduced two comment blocks in src/gui/MainWindow.cpp tagged
(#3029). That number refers to an unrelated FlexControl wheel-tuning PR
that merged the same night; the meaningful reference for the panadapter
dBm-range prime fix is #3034 itself. Correct both citations so code
archaeology lands on the actual fix PR.

Comment-only change; no behavior, no symbols touched.
ten9876 pushed a commit that referenced this pull request May 24, 2026
… comments (#3047). Principle XI. (#3048)

## Summary

Fixes #3047

### What was changed

cleanup(panadapter): fix stale #3029 references to #3034 in dBm-prime
comments (#3047). Principle XI.

### Files modified

- `src/gui/MainWindow.cpp`

```
 src/gui/MainWindow.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
```

---
Generated by AetherClaude (automated agent for AetherSDR)

---
<sub>🤖 aethersdr-agent · cost: $1.7186 · model: claude-opus-4-7</sub>

Co-authored-by: aethersdr-agent[bot] <273844287+aethersdr-agent[bot]@users.noreply.github.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…lat secondary pans on reconnect (aethersdr#3034)

## Summary

- **Bug**: Secondary panadapters (Slices B–H, up to 8 total)
occasionally go completely flat after reconnecting to the radio —
showing no signal while Slice A works normally. Switching bands restores
them, and the issue is intermittent.
- **Root cause**: Two compounding issues:
1. `SpectrumWidget` initialises with `refLevel = -50 dBm, dynamicRange =
100 dB` but `PanadapterModel` already carries the correct saved range
(default −130 → −40 dBm). Neither `wirePanadapter()` nor the
`panadapterAdded` reconnect path primed the widget from the model, so
every non-primary pan started with a stale widget state.
2. If `DisplayNoiseFloorEnable` is on for any secondary pan, the
noise-floor auto-adjust begins animating `refLevel` toward its target
from the wrong starting point (−50 instead of −40 dBm) before the radio
echoes back its saved `min_dbm`/`max_dbm`. This produces bogus
`dbmRangeChangeRequested` values (e.g. −84.59 → +5.41 dBm). The
`pendingDbm` guard in `wirePanadapter` then poisons
`PanadapterStream::m_dbmRanges` with those values and sends them to the
radio, causing the VITA-49 decoder to normalise FFT bins to a window far
above the actual HF noise floor. All real signals fall off the bottom of
the display. The guard re-asserts the bogus range on every subsequent
`levelChanged` echo, creating a self-reinforcing lock-out.
- **Slice A immunity**: Slice A is wired first; by the time its
noise-floor auto-adjust fires, a dBm command from a separate code path
has already arrived and set the correct range.

## Fix

Prime `sw->setDbmRange(pan->minDbm(), pan->maxDbm())` immediately after
wiring `levelChanged` in both affected code paths (`MainWindow.cpp`):

- `wirePanadapter()` — every new applet at initial connection
- `panadapterAdded()` "existing applet" branch — every pan on reconnect

Both sites apply to all panadapters (1–8) unconditionally.

## Test plan

- [ ] Connect to radio with two or more panadapters and noise-floor
auto-adjust enabled on all pans — confirm all pans display spectrum data
after connect
- [ ] Disconnect and reconnect 5–10 times — confirm no secondary pan
goes flat
- [ ] Switch bands while connected — confirm dBm range adjusts correctly
on all pans
- [ ] Connect with a single panadapter — confirm no regression
- [ ] Verify noise-floor auto-adjust still tracks and adjusts the
display correctly on all pans after connect

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…#3034 in dBm-prime comments (aethersdr#3047). Principle XI. (aethersdr#3048)

## Summary

Fixes aethersdr#3047

### What was changed

cleanup(panadapter): fix stale aethersdr#3029 references to aethersdr#3034 in dBm-prime
comments (aethersdr#3047). Principle XI.

### Files modified

- `src/gui/MainWindow.cpp`

```
 src/gui/MainWindow.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
```

---
Generated by AetherClaude (automated agent for AetherSDR)

---
<sub>🤖 aethersdr-agent · cost: $1.7186 · model: claude-opus-4-7</sub>

Co-authored-by: aethersdr-agent[bot] <273844287+aethersdr-agent[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants