feat(automation): observability fields + close/drag/showMenu/pan verbs, grab pan, token-anchored TX guard (#3646)#3842
Conversation
|
Filed the production-side bug as #3843 — the GUI pan-close path never sends |
There was a problem hiding this comment.
Thorough, well-documented batch — thanks @jensenpat. I traced every new path against the codebase and it holds up: all RadioModel/PanadapterModel APIs used by doPan exist (maxPanadapters, panadapters, createPanadapter, activePanadapter, panadapter(pid), sendCommand; panId/waterfallId), the panIndex/panId getters the new Q_PROPERTYs wrap are already present, and all required includes (QMouseEvent, QPointer, QPushButton, QToolButton, QComboBox, QMetaType) are in place. The core→gui decoupling via meta-object property() reads is clean — core/ stays GUI-include-free. CI is green across all 6 checks.
A few highlights I checked specifically:
- Token tokenizer is correct. I hand-traced
aprsSvcWXBOT→["aprs","svc","wxbot"](the acronym-hump rule keepsWXBOTwhole), socwxno longer false-matches, andstatus/temperaturestay single tokens that don't equalatu.moxButton/Auto-Tunestill match. The fix does what it claims. - Crash-safe deferral on
close/showMenu(singleShot(0)+ unconditionalraise()/activateWindow()beforepopup) matches the macOS hazard described in #3828's findings. Good. - Drag globals computed once from the press point — correct call for
QSizeGrip, where re-mapping mid-resize would compound.
Two small things worth a note (neither blocking):
-
TX-guard narrowing is strictly more permissive than the old
contains(). Whole-token anchoring is the right fix for the false positives, but it also drops fallback coverage for compound names where a deny-word is a substring of a larger token — e.g.Tuner/tunerno longer matchestune(an ATU "Tuner" button briefly keys TX), andtransmitter/transmittingno longer matchestransmit. SincemarkTxKeying()is the authoritative path this is an acceptable trade, but it does mean any keyer relying solely on a compound-name fallback now slips through unguarded. Worth a quick sanity-check that no current keying control depends on substring-only matching. -
pan close <0x…>is format-sensitive.radio->panadapter(pid)is a direct string lookup, so if the caller's hex formatting differs from the stored id, the lookup returns null and thedisplay panafall remove(waterfall teardown) is silently skipped — you'd getresolved:falseand a half-closed pan. Index/active/allselectors avoid this since they source the id from the model; just a caveat for the explicit-id path.
Overall this is a high-quality, faithfully-verified change. Nice work.
🤖 aethersdr-agent · cost: $6.3968 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Review — strong PR, one blocker (rebase needed)
Reviewed the full diff with a focus on the TX-safety guard (Principle VI). Verdict below.
⚠️ Blocker: mergeable=CONFLICTING
This now conflicts with main as a direct result of #3832 landing earlier today — both PRs edit the same verb-dispatch region of AutomationServer.cpp (and SpectrumWidget.h). #3832 added doCwx/floors/antenna+pan verbs; this PR adds close/drag/showMenu/pan add+close + grab-pan into the same dispatch, so git can't auto-merge.
Because the conflict lands squarely in the safety-critical verb dispatch + TX-guard, and you already live-verified these changes against a real radio, the safest path is you rebase onto current main and re-run the live TX-guard verification rather than have a conflict resolved blind in keying code. (Happy to do the rebase myself if you'd rather — just say the word.)
✅ TX-safety (Principle VI) — PASS
- Authoritative guard unchanged.
kTxKeyingProperty(themarkTxKeying()marker) is still checked first inisTransmitControl/isTransmitAction. Every marked keyer is blocked regardless of name. The change touches only the name-match fallback. - Fallback is more precise, not weaker for real keyers. Substring
contains()→ whole-token match viaidentifierTokens()(camelCase humps + separator split) +matchesTxDenyToken()(tokens.contains(d)),kDeny = {mox, ptt, transmit, vox, cwx, atu}.- Genuine keyers still match:
moxButton→[mox,button],pttSend→[ptt,send], ATU tuner→[atu], VOX→[vox]. ✅ - False-positives correctly freed:
status/temperature(noatutoken),aprsSvcWXBOT→[aprs,svc,wxbot] (nocwxtoken). ✅
- Genuine keyers still match:
- Residual risk (low, acceptable): a stricter fallback would miss an unmarked keyer with a glued all-lowercase name (e.g.
cwxsend). But real keyers carry the marker, the primary fail-safe is intact, and the fallback still logs aqCWarningtelling the dev to addmarkTxKeying(). Right trade.
✅ Other changes
- Verbs (
close/drag/showMenu): deferred / GUI-loop-posted with window raise+activate — handles the macOS popup re-entrancy crash class cited. - Observability (
toolTip,QComboBox items[]/currentIndex,panIndex/panIdvia meta-object): no core→gui include leak. Closes most of #3845 as intended.
CI is green on all 6 checks; only the merge conflict + a fresh live-verify stand between this and merge. Rebase and I'll merge.
…s, grab pan, token-anchored TX guard (aethersdr#3646) Next batch of agent-automation-bridge verbs and fixes, each driven against a live FLEX-8400M and checked for the macOS "popup while window unfocused" segfault class. dumpTree observability - Serialize widget toolTip (Bug 5: the two "Clear" buttons now distinguishable by "Clear all bookmarks" vs "Clear the displayed SWR sweep trace."). - QComboBox nodes carry items[] + currentIndex, so option lists are assertable without stepping (and applying) each selection. - SpectrumWidget nodes carry panIndex so a driver can discover which index to grab/close. Exposed via new Q_PROPERTYs on SpectrumWidget (panIndex) and PanadapterApplet (panId), read through the meta-object so core/ keeps no GUI include. TX-guard deny-words anchored to whole tokens - isTransmitControl/isTransmitAction tokenize identifiers on camelCase humps and separators, then match a deny-word against a WHOLE token. Fixes the unanchored contains() that flagged the RX-only APRS weather entry (aprsSvcWXBOT → "svc"+ "wxbot" contained the trigram "cwx"), and likewise "stATUs"/"temperATUre". Genuine keyers (moxButton, pttSend, Auto-Tune, the VOX control) still match. New verbs - close <target>: close the target's top-level window (deferred; reaches the custom frameless title-bar close that invoke-click can't, works for any window). - drag <target> <dx> <dy> (alias mouse): synthesize press→move→release so resize grips and slider handles are provable end-to-end. Globals are computed once from the press point — a QSizeGrip moves as the window resizes, so re-mapping mid-drag would overshoot. Verified: grip drag resized the window by exactly the requested delta. - showMenu <target> (alias openMenu): pop a QToolButton/QPushButton drop-down, posted onto the GUI loop with the owning window raised+activated first — showing the native popup from inside the socket-read callback (or while backgrounded) re-enters Cocoa and segfaults. Survived 6 backgrounded rounds, no crash. - pan add / pan close <panId|index|active|all>: create/tear down a panadapter regardless of how it was opened. Close sends the FlexLib-correct pair `display pan remove` AND `display panafall remove` (panId + waterfallId), so a panafall-created pan closes without the slice-removal workaround. RadioModel::removePanadapter (production) still sends only `display pan close` — left untouched here; flagged for a separate fix. - grab pan <index> [path]: capture a specific pan's SpectrumWidget in a multi-pan layout (plain `grab SpectrumWidget` only ever returns the first). 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat
5824379 to
a9dccd8
Compare
Re-verified live on the rebased build (FLEX-8400M)After rebasing onto current
Also re-confirmed on the rebased build: widget
|
ten9876
left a comment
There was a problem hiding this comment.
Approved. Re-reviewed after the rebase onto current main (bb5e575a):
- Clean rebase — both #3832's verbs (
cwx/floors/antenna) and this PR's (showMenu/drag/close/grab) survive the merge; none dropped or duplicated. - TX-safety (Principle VI) PASS —
kTxKeyingPropertymarker checked first inisTransmitControl, then the whole-token fallback;kDeny = {mox, ptt, tune, transmit, vox, cwx, atu}covers every genuine keyer (moxButton,pttSend,Auto-Tune,VOX, ATU,cwx send) while freeing the prior false-positives (status/temperature/WXBot). - Crash-safe verbs —
close/drag/showMenuare GUI-loop-deferred with window raise+activate (handles the macOS popup re-entrancy class). - Observability —
toolTip, comboitems[]/currentIndex,panIndex/panIdvia meta-object; no core→gui include leak. - Builds clean on current main locally; CI green on all 6.
Closes most of #3845. Thanks @jensenpat — clean work, and the rebase resolved the conflict cleanly.
…) (#3855) ## What Closing a **panafall-created** panadapter never freed its **waterfall stream on the radio**. The FlexLib-correct teardown is the pair `Panadapter.Close()` + `Waterfall.Close()` — i.e. **both** `display pan remove 0x<panId>` **and** `display panafall remove 0x<wfStreamId>` (FlexLib v4.2.18) — but the close path sent only the first. Two defects from the issue, both addressed: 1. **Dead + bogus public API.** `RadioModel::removePanadapter` issued `display pan close`, which is **not a FlexLib command at all**, and had **zero callers** — a latent trap. 2. **Live close path omitted the waterfall removal.** The GUI X-button and the layout-shrink path each sent an inline `display pan remove` only, so the panafall's waterfall stream was left allocated on the radio. The client-side cleanup was already complete (the `display pan <id> removed` echo unregisters *both* streams), so `get pans` always read clean — which is precisely why this radio-side leak went unnoticed. Fixes #3843. ## The fix - **`RadioModel::removePanadapter`** now captures the waterfall id **before** sending (the removal echo deletes the `PanadapterModel`, which would otherwise race the read) and sends `display pan remove` **plus** `display panafall remove` when a waterfall is present. - **Single source of truth:** the X-button handler (`MainWindow_Wiring`) and the layout-shrink path (`MainWindow::applyPanLayout`) both route through `removePanadapter` instead of inlining `display pan remove`. - The automation bridge **`pan close`** verb now also drives `removePanadapter`, so it exercises the exact production teardown (and DRYs up the duplicated command pair). Tightly scoped to the close-path bug. A radio-side **stream-inventory diagnostic** to make this class of leak directly observable (and to catch *resource-level* lingering / duplicate pans) is split out into its own feature request so this fix stays minimal and low-risk. ## How the agent automation bridge proved it Driven against a **live FLEX-8400M (firmware 4.2.18)**. A leak-repro build (panafall-remove omitted) and the fixed build were each driven through the **production** close path (`pan close` → `removePanadapter`), capturing the radio command stream: | Build | `display pan remove` | `display panafall remove` | |---|---|---| | **Broken baseline** | ✅ `display pan remove 0x40000001` | ❌ **absent** | | **Fixed** | ✅ `display pan remove 0x40000001` | ✅ **`display panafall remove 0x42000001`** | Broken-build capture: ``` RadioModel::removePanadapter: "0x40000001" waterfall: "0x42000001" RadioModel::sendCommand: "display pan remove 0x40000001" (no display panafall remove) ``` Fixed-build capture: ``` RadioModel::removePanadapter: "0x40000001" waterfall: "0x42000001" RadioModel::sendCommand: "display pan remove 0x40000001" RadioModel::sendCommand: "display panafall remove 0x42000001" ``` **Firmware note:** on this radio the waterfall UDP stops on pan-removal regardless, so the leak here is the waterfall *object* left allocated rather than continued UDP — the command-level capture above is the decisive evidence that the radio is now correctly *told* to free it. Continued-UDP waterfall leaks do occur on older (#268-class) firmware. ## Test plan - [x] Builds clean (Ninja, RelWithDebInfo, arm64). - [x] Live FLEX-8400M: production close path sends the FlexLib-correct command pair (before/after capture above). - [ ] Maintainer review (label present on the issue). ## Related - #268 — distinct glitch-corruption Multi-Flex case (Pan 2 FFT blank, stuck); different root cause. - #269 — restored broken pan state on restart. - #3212 — duplicate-panadapter on reconnect (fixed via `SliceRecreatePolicy`). - PR #3842 — automation-bridge `pan close` verb that first proved the correct teardown. - Follow-up: radio-side stream-inventory / leak-detection feature request (filed separately). 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Release prep for v26.6.5 Docs + version-string updates for the **50 merges** since v26.6.4. No code change — safe to merge without a rebuild. ### Changes - **CHANGELOG.md** — new `[v26.6.5] — 2026-06-28` section, headlined by **KiwiSDR receive sync** (#3872), **SmartMTR TX meters** (#3776), the **PROF profile-switcher applet** (#3829), and a large **agent automation bridge** expansion (#3851/#3856/#3883/#3842/#3832/#3819); categorized Added/Changed/Fixed/Performance/Removed compiled from the full merge list. Folds in the prior `[Unreleased]` entries and resets `[Unreleased]` to empty. - **CMakeLists.txt / README.md / AGENTS.md** — `26.6.4` → `26.6.5`. - **ROADMAP.md** — current cycle → `post-v26.6.5`; four v26.6.5 highlights added atop *Recently shipped*. ### Not included - Tagging `v26.6.5` is a separate, explicit step (not done here). - The untracked `docs/aetherd-headless-engine-design.md` RFC and `prototypes/` are intentionally left out. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Next batch of agent-automation-bridge verbs and fixes (issue #3646), addressing the gaps surfaced during the last dogfooding pass. Every change was driven against a live FLEX-8400M and explicitly checked for the macOS "show a native popup while the window isn't in focus" segfault class.
dumpTree observability (assert without stepping a control)
toolTipon every widget node — Bug 5 is now provable: the two "Clear" buttons are distinguishable byClear all bookmarksvsClear the displayed SWR sweep trace.items[]+currentIndexonQComboBoxnodes — the full option list is verifiable without stepping (and applying) each selection.panIndexonSpectrumWidgetnodes — so a driver can discover which index tograb/close. Exposed via newQ_PROPERTYs onSpectrumWidget(panIndex) andPanadapterApplet(panId), read through the meta-object socore/keeps no GUI include.TX-guard deny-words anchored to whole tokens
isTransmitControl/isTransmitActionnow tokenize an identifier on camelCase humps and separators, then match a deny-word against a complete token. This fixes the unanchoredcontains()that flagged the RX-only APRS weather entry (aprsSvcWXBOT→svc+wxbotcontained the trigramcwx), and the same class of bug instATUs/temperATUre. Genuine keyers (moxButton,pttSend,Auto-Tune, the VOX control) still match.Live-verified:
multiFLEX statusandAmplifier temperature→ no longer blocked;VOX voice-operated transmit(whole-token fallback) andMOX transmit(keying marker) → still blocked.New verbs
close <target>QLabelthatinvoke … clickcan't, and works for any window.drag <target> <dx> <dy>(aliasmouse)QSizeGripmoves as the window resizes, so re-mapping mid-drag would overshoot.showMenu <target>(aliasopenMenu)QToolButton/QPushButtondrop-down, posted onto the GUI loop with the owning window raised+activated first (the crash-safe path for a backgrounded macOS instance).pan add/pan close <panId|index|active|all>display pan removeanddisplay panafall remove(panId + waterfallId).grab pan <index> [path]SpectrumWidgetin a multi-pan layout — plaingrab SpectrumWidgetonly ever returns the first.Live test evidence (FLEX-8400M)
dumpTreeconfirms it's gone; app alive.QSizeGripgrew the window by exactly the requested 140×90 (no overshoot); slider-handle drag movedAF gain.display pan remove 0x40000001+display panafall remove 0x42000001— closing without the slice-removal workaround the olddisplay pan closerequired. Also verified close by explicit panId, and graceful errors for bad index / bad selector.tools/automation_validate.pyregression sweep drove dozens of existing controls cleanly (its 8 "issues" are pre-existing radio/control states, not bridge regressions).Follow-up (not in this PR)
RadioModel::removePanadapter(the production close-pan path) still sends onlydisplay pan close, which is what leaves a panafall's waterfall behind. This PR scopes the fix to automation (per the request); the production path warrants a separate change with proper command-flow tracing.Files
src/core/AutomationServer.{h,cpp}— verbs, dumpTree fields, token-anchored guardsrc/gui/SpectrumWidget.h,src/gui/PanadapterApplet.h—Q_PROPERTYexposuredocs/automation-bridge.md— documented the new fields/verbs💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat