Skip to content

fix(nets): route cross-band net tune through canonical tune policy (#3918)#3921

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:fix/3918-net-tune
Jun 30, 2026
Merged

fix(nets): route cross-band net tune through canonical tune policy (#3918)#3921
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:fix/3918-net-tune

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Fixes #3918.

Summary

Two defects in the Net Scheduler's cross-band "Tune Now" path (MainWindow_Nets.cpp), both reproduced and verified against a live FLEX radio with the agent automation bridge.

Bug 1 — slice display desync (safety)

A cross-band net tune left the VFO display stuck on the band's last-used frequency while RX/TX ran on the net frequency. The GUI misreports the actual TX QRG — an operating hazard.

Root cause (deeper than the original triage). tuneToNet() preselected the band stack with display pan set <pan> band=<key> before retuning. That band-stack reload retuned the slice to the band's last-used frequency and echoed it, but the radio then emitted no slice RF_frequency status for the subsequent slice tune <netFreq> — so the display stuck on the band frequency.

The triage's "echo-ordering race" theory was wrong: there is no net-frequency echo to be overwritten. The bridge made this provable — on the fixed-with-a-500ms-defer build the display was still wrong, which ruled the ordering theory out.

Fix. Route the net's frequency change through the existing applyTuneRequest(slice, freq, TuneIntent::AbsoluteJump, "net-tune") policy — the same path a DX-cluster spot uses to jump to an arbitrary cross-band frequency. It moves the slice with slice tune <freq> (echoed back as a slice status, so the display stays radio-authoritative) and recenters the panadapter, with no band-stack reload. Net-specific mode/filter/step and repeater/tone fixup are applied around it. Net change: −21 lines (reuses policy instead of a bespoke path).

Bug 2 — window drops out of Maximized

The reminder banner's tuneRequested handler and the tray messageClicked handler called showNormal() on the MainWindow, which clears a Maximized/FullScreen window (the reporter's "un-maximizes to a smaller window" symptom). Both now guard behind isMinimized(), so raising the window never un-maximizes it.

How the agent automation bridge proved it

A one-time net was seeded to fire a reminder, then "Tune Now" was clicked on the in-app banner (a real click — the bridge's TX-safety guard currently false-positives on the word "tune", see gaps below). Slice 0 was parked on 20m beforehand so the 40m net (7.175 MHz) is cross-band.

Build Commands issued Slice display ends on
Broken (unfixed) display pan set band=40 + synchronous slice tune 7.175 7.225 (40m band stack) ✗ — 0 net echoes
Fixed (this PR) slice tune 0 7.175 autopan=0 + display pan set center=… (no band=) 7.175 (net) ✓

The broken reading was captured live on this radio (FLEX-8400M) and matches the reporter's attached FLEX-6400 log (display stuck on 7.150). After the fix, the radio echoes (transmit band 5 band_name=40, transmit freq=7.175000, apd slice=0 freq=7.175000) confirm the band context and op-freq are correct, and get slice reads the net frequency.

Bug 2 could not be proved through the bridge (it has no verb to enter the Maximized state or read window state — see gaps), so that half is fix-by-inspection: showNormal() documented to clear WindowMaximized.

Agent automation bridge — gaps found

  • TX-safety guard false-positive. isTransmitControl()'s button-name fallback denies any button whose name contains the token "tune", so it blocks the Net Scheduler "Tune Now" button (RX frequency tuning, not TX keying). The QAction path was already narrowed to drop "tune" for exactly this reason; the QWidget-button path wasn't. Forced a manual click for the proof. Fix: drop "tune"/"atu" from the button kDeny, or mark genuinely-keying buttons explicitly.
  • No item-view row-select verb. invoke can't select a row in a QTableWidget/QListWidget, so the Net Scheduler dialog's row-scoped actions (Tune Now / Edit / Remove / Disable) can't be driven. Worked around via the reminder banner. Fix: add a selectRow/setCurrentRow action.
  • No window-state read or maximize verb. dumpTree exposes no windowState, and resize only sets explicit geometry — so a Maximized→Normal regression (Bug 2) can't be set up or observed through the bridge. Fix: expose isMaximized in dumpTree and a maximize verb.

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

…ethersdr#3918)

Cross-band "Tune Now" from a net reminder left the VFO display stuck on
the band's last-used frequency while RX/TX ran on the net frequency — an
operating hazard, since the GUI misreports the actual TX QRG.

Root cause (deeper than the original triage): tuneToNet() preselected the
band stack with `display pan set <pan> band=<key>` before retuning. The
band-stack reload retuned the slice to that band's last-used frequency AND
echoed it, but the radio then emitted no slice RF_frequency status for the
subsequent `slice tune <netFreq>` — so the display stuck on the band
frequency. The triage's "echo-ordering race" hypothesis was wrong: there
is no net-frequency echo to be overwritten. Proven live on a FLEX-8400M
via the agent automation bridge (commanded 7.175 MHz, display stuck on the
40m band-stack 7.225, zero 7.175 slice echoes), consistent with the
reporter's FLEX-6400 log (stuck on 7.150).

Fix: route the net's frequency change through the existing
applyTuneRequest(slice, freq, TuneIntent::AbsoluteJump, "net-tune") policy
— the same path a DX-cluster spot uses to jump to an arbitrary cross-band
frequency. It moves the slice with `slice tune <freq>` (echoed back as a
slice status, so the display stays radio-authoritative) and recenters the
panadapter, with no band-stack reload. Net-specific mode/filter/step and
repeater/tone fixup are applied around it. Net change: -21 lines.

Also fixes the reporter's "drops out of Maximized" symptom: the reminder
banner's tuneRequested handler and the tray messageClicked handler called
showNormal() on the MainWindow, which clears a Maximized/FullScreen state.
Guard both behind isMinimized() so raising the window never un-maximizes it.

Proven via the agent automation bridge: a one-time net seeded to fire a
reminder, then a real "Tune Now" click on the in-app banner. Before
(unfixed build): slice display ended on 7.225 (band stack). After (this
build): slice display ends on 7.175 (net), and no `display pan set band=`
command is issued.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jensenpat jensenpat marked this pull request as ready for review June 30, 2026 07:18
@jensenpat jensenpat requested a review from a team as a code owner June 30, 2026 07:18

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

Thanks for this, @jensenpat — clean, well-reasoned fix, and the diagnosis is more convincing than the original triage. The before/after bridge table makes the band-stack-reload mechanism concrete rather than hand-wavy.

I verified the central claim against the tree: routing through applyTuneRequest(slice, freq, TuneIntent::AbsoluteJump, …) is exactly the canonical cross-band jump path already used by dx-cluster (MainWindow_Menus.cpp:477), spectrum-click (MainWindow_Wiring.cpp:2543), slice-move-here (MainWindow_Wiring.cpp:2807), and bandstack-recall (MainWindow.cpp:3910). So this brings net-tune into line with every other arbitrary-frequency tune instead of carrying a bespoke path — net win, and the −21 lines are real reuse. For AbsoluteJump, applyTuneRequest skips the band-stack preselect entirely and relies on revealFrequencyIfNeeded to recenter the pan, which is the proven behavior of those other call sites.

A couple of things worth confirming/noting (none blocking):

  1. Dropped clearSwrSweepForBandChange / reassertUnmutedSliceAudioForPan. These were both there specifically to counteract the display pan set band= stack reload — clearing a stale SWR sweep and re-asserting un-muted audio after the reload re-muted the slice. Since the reload is gone, dropping them is logically consistent, and the canonical AbsoluteJump paths don't call them either. Reads correct to me; flagging only so it's a deliberate record.

  2. Lost the "X isn't available on this radio" guard. The old path checked XvtrPolicy::resolveBandStackKey(...).isSupported() and showed a friendly message (without tuning) for an unconfigured transverter band. Now a net scheduled on such a band will attempt the jump like a DX spot would. For a plain HF rig this never triggers, and it matches dx-cluster behavior, so I'd accept it — but if you want to preserve the nicer message for xvtr-band nets, that's the one user-visible regression here.

  3. Mode ordering (minor/optional). The cited reference path (bandstack-recall) sets slice->setMode() before applyTuneRequest with the comment "Mode first (affects filter ranges)"; this PR sets it after. Because the explicit filt command still runs last, the end state is the same, so this is purely cosmetic consistency — not a correctness issue.

Bug 2: the if (isMinimized()) showNormal(); guard is the right call — showNormal() does clear WindowMaximized/FullScreen. I checked that MainWindow has no hide-to-tray path (the tray icon exists only for net-reminder notifications), so there's no case where the window is genuinely hidden and the guard would leave it un-shown. Good.

CI is green across all six checks. Nice work, and the bridge-gap notes are useful for #3920.


🤖 aethersdr-agent · cost: $6.2904 · model: claude-opus-4-8

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. Focused review of the net-tune routing change:

Routing net-tune through applyTuneRequest(AbsoluteJump) is correct. It avoids the display pan set band=<key> band-stack reload that retuned the slice to the band's last-used frequency and emitted no echo for the net retune — the #3918 "VFO display stuck on band freq while RX/TX runs on the net's" bug. AbsoluteJump does slice tune (radio-echoed → VFO tracks) + revealFrequencyIfNeeded recenter, the same path DX-cluster spots use to jump across bands, so the cross-band case works by parity. slice->setMode() sends the identical slice set mode= command.

The showNormal()if (isMinimized()) showNormal() guards are correct — they stop the net reminder from clobbering a Maximized/FullScreen window (same as #3920), and closeEvent doesn't hide-to-tray so there's no hidden-window regression.

One minor follow-up (non-blocking, filed as an issue): the AbsoluteJump path skips band-stack preselect, so the old "Can't tune — isn't available on this radio" feedback for unconfigured transverter bands is gone. Matches the DX-spot path (also unguarded), so acceptable, but the actionable message is lost for XVTR edge cases.

CI green.

@ten9876 ten9876 merged commit 34e3f3a into aethersdr:main Jun 30, 2026
6 checks passed
ten9876 added a commit that referenced this pull request Jul 2, 2026
#3961)

## Release documentation prep for **v26.7.1** (2026-07-02)

29 commits since v26.6.5. Docs-only + version bump — no code behavior
change.

### Version bump 26.6.5 → 26.7.1
The three canonical locations (per `AGENTS.md`):
- `CMakeLists.txt:2` — `project(AetherSDR VERSION 26.7.1)` →
`AETHERSDR_VERSION` (the app's version string)
- `README.md` — Current version line
- `AGENTS.md` — Current version line

### `CHANGELOG.md`
Promoted `[Unreleased]` → **`## [v26.7.1] — 2026-07-02`** in house style
(v-prefix, em-dash, "N commits since…" opener + headline), authored from
all merged PRs and categorized:
- **Added:** 3D stacked-trace spectrum (#3899), 3D dBm scale (#3937),
in-process NVIDIA BNR + AFX download-on-demand (#3902), TX meter
mouse-over readouts (#3936), SWR manual sweep range (#3885), CW sidetone
capture (#3895), KiwiSDR metadata scaffolding (#3898), bridge verbs
(#3920)
- **Changed:** BNR container/NIM backend removed, 60 fps + per-pixel GPU
FFT trace (#3958), FlexLib-sourced model capabilities (#3954/#2177), RX
speaker-latency cut (#3897), Display pane sections (#3935)
- **Fixed:** #3922, #3926, #3941, #3940, #3942, #3921, #3903, #3892,
#3924, #3891, #3890, #3900, #3947
- Fresh empty `[Unreleased]` restored above the release block.
Internal/CI/docs/self-fix PRs (#3931/#3916/#3934/#3929) omitted per
house style.

### `ROADMAP.md`
- Cycle header → "post-v26.7.1"
- NVIDIA BNR removed from **In flight** (shipped this cycle)
- Five v26.7.1 highlights prepended to **Recently shipped**

### `README.md`
- Highlights: GPU spectrum bullet now notes the **per-pixel FFT trace at
up to 60 fps** and the **3D stacked-trace** spectrum mode

### Reviewer notes
- Touches protected paths (`*.md` + `AGENTS.md` Tier-1 +
`CMakeLists.txt`) — needs `@aethersdr/infrastructure` sign-off.
- Tagging `v26.7.1` and cutting the release build are **not** part of
this PR (docs prep only).

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

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
jensenpat pushed a commit that referenced this pull request Jul 3, 2026
…nconfigured transverter band (#3930) (#3992)

## Summary

PR #3921 correctly routed `tuneToNet()` through `applyTuneRequest(...,
TuneIntent::AbsoluteJump, "net-tune")` to fix the VFO-display desync —
but `applyTuneRequest()` only runs `preselectBandStackForTune()` (home
of the `XvtrPolicy` support check and its "band isn't available"
status-bar feedback) for `CommandedTargetCenter`. A net scheduled on a
band the radio can't reach (no native coverage, no matching XVTR)
therefore silently issued the `slice tune` + recenter with **no
feedback**. This implements the issue's **Option 1**: a scoped pre-check
in `tuneToNet()` that mirrors the `preselectBandStackForTune()` guard
exactly and refuses — with the status-bar message — *before* touching
the radio.

## Why

- Restores the pre-#3921 "Can't tune … — Band …" feedback family for net
tunes without re-introducing the band-stack-reload desync that #3921
fixed.
- The guard is a faithful mirror of
`MainWindow::preselectBandStackForTune()`: same HF↔HF and same-band
early-outs, same `XvtrPolicy::resolveBandStackKey()` decision
(capabilities-aware, so 2 m on a FLEX-6700 stays native and is never
over-blocked), same no-XVTR wording, same `lcProtocol` warning shape.
- Deliberately scoped to the net path (Option 1). The DX-cluster spot
path (`MainWindow_Menus.cpp`, same `AbsoluteJump`, same gap) and
typed-VFO consolidation into the canonical tune policy (the issue's
Option 2) are a natural follow-up.

## Scope

- +36 lines in `src/gui/MainWindow_Nets.cpp` (pre-check +
`core/LogManager.h` include for `lcProtocol`)
- +29 lines in `tests/xvtr_policy_test.cpp` (new
`testNetTunePrecheckBandSupport()` pinning the decision matrix)
- No protocol / persistence / UX-layout change; one new user-visible
status-bar message on the refusal path (pre-#3921 wording family)

## Constitution principle honored

**Principle XI — Fixes Are Demonstrated.** Bug and fix verified A/B on a
live FLEX-6700 (via the automation bridge, RX-only), plus new unit tests
pinning the decision core.

## Test plan

- [x] `xvtr_policy_test` green (37/37 incl. 4 new cases: 440-no-XVTR
unsupported, 440-with-XVTR resolves, 2m native on `has2Meters` models,
HF control)
- [x] Local incremental `ninja -C build` clean on Linux (Nobara 43, Qt
6.10.3, gcc 15.2.1)
- [x] Live A/B on a FLEX-6700 (SmartSDR v1.4.0.0), nets seeded at
432.100 FM / 7.200 LSB / 144.200 USB, slice parked on 20 m:

| Net (Tune Now) | Baseline v26.7.1 tip | This PR |
|---|---|---|
| 432.100 (no usable XVTR mapping) | **silently tunes to 432.1** — no
message | **refuses**, slice unchanged, status bar: "Can't tune 70cm
Test Net — Band 440 has no Flex display pan band= mapping" +
`lcProtocol` warning |
| 7.200 (HF control) | tunes | tunes (early-out, check skipped) |
| 144.200 (native 2 m on 6700) | tunes | tunes (native-band branch — no
over-blocking) |

- [x] Manual GUI repro against a real FlexRadio: verified on Linux
(Nobara 43) against the FLEX-6700 — baseline v26.7.1 reproduces the
silent 432.1 jump; this branch refuses with the status-bar message; the
7.200 and 144.200 nets tune normally on both builds


**Observed while verifying, pre-existing and unchanged by this PR:**
`resolveBandStackKey()` matches XVTR entries by *name* against the
`BandSettings` band name — the lab 6700 reports an XVTR entry named
`70CM`, which does not match band `440`, so the tune refuses on the
canonical preselect path too (typed VFO, spot recall). Whether
name-matching should become frequency-range matching is a separate
policy question worth its own issue.

## Checklist

- [x] Commits are signed (docs/COMMIT-SIGNING.md)
- [x] No new flat-key AppSettings calls (Principle V)
- [x] All meter UI uses MeterSmoother (Principle II) — n/a, no meter
code touched

Closes #3930

---

73, Ozy **K6OZY**
AI compute partnership: [cloaked.agency](https://cloaked.agency) —
(model: claude-fable-5)

Co-authored-by: Don @ cloaked.agency <don@cloaked.agency>
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.

Scheduled Event Doesn't Show Correct Slice Matching Tuned Frequency

2 participants