fix(nets): route cross-band net tune through canonical tune policy (#3918)#3921
Conversation
…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>
There was a problem hiding this comment.
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):
-
Dropped
clearSwrSweepForBandChange/reassertUnmutedSliceAudioForPan. These were both there specifically to counteract thedisplay 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 canonicalAbsoluteJumppaths don't call them either. Reads correct to me; flagging only so it's a deliberate record. -
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. -
Mode ordering (minor/optional). The cited reference path (bandstack-recall) sets
slice->setMode()beforeapplyTuneRequestwith the comment "Mode first (affects filter ranges)"; this PR sets it after. Because the explicitfiltcommand 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
left a comment
There was a problem hiding this comment.
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.
#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>
…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>
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 withdisplay 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 sliceRF_frequencystatus for the subsequentslice 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 withslice 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
tuneRequestedhandler and the traymessageClickedhandler calledshowNormal()on theMainWindow, which clears a Maximized/FullScreen window (the reporter's "un-maximizes to a smaller window" symptom). Both now guard behindisMinimized(), 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.
display pan set band=40+ synchronousslice tune 7.1750net echoesslice tune 0 7.175 autopan=0+display pan set center=…(noband=)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, andget slicereads the net frequency.Agent automation bridge — gaps found
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 buttonkDeny, or mark genuinely-keying buttons explicitly.invokecan't select a row in aQTableWidget/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 aselectRow/setCurrentRowaction.dumpTreeexposes nowindowState, andresizeonly sets explicit geometry — so a Maximized→Normal regression (Bug 2) can't be set up or observed through the bridge. Fix: exposeisMaximizedindumpTreeand amaximizeverb.💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat