feat(gui): PROF (Profile Switcher) applet — live Global/TX/Mic profile selection (#3376)#3829
Conversation
…profiles (aethersdr#3376) Adds a compact, dedicated "PROF" applet to the right-hand applet panel that gives one-click, always-available switching between the radio's Global, TX, and Mic profiles — the persistent quick-access selector requested in aethersdr#3376, without opening the full Profile Manager dialog. The applet is three tight rows: a small Global / TX / Mic label paired with a dropdown that enumerates every profile the radio currently reports. Each dropdown's current item tracks the radio's active profile, and selecting an entry applies it live: - Global → RadioModel::loadGlobalProfile() ("profile global load") - TX → "profile transmit load" - Mic → "profile mic load" The command strings mirror the proven ProfileManagerDialog paths (the radio status verb is "tx"/"mic" but the load command is "transmit"/"mic"). The lists and active selections refresh automatically: globalProfilesChanged() rebuilds the Global row; TransmitModel::profileListChanged()/micProfileListChanged() rebuild TX/Mic; stateChanged()/micStateChanged() keep the active highlight in sync. So when a new profile is saved (e.g. via the Profile Manager) the applet picks it up with no manual refresh. Selection is driven off currentTextChanged(); every internal repopulation (rebuildCombo / syncCombo) is wrapped in a QSignalBlocker, so there is no select→load→refresh→load feedback loop while still letting both a human pick and the agent automation bridge's invoke verb trigger a live load. Registration mirrors MeterApplet: id "PROF", label "Profile Switcher", off by default, added to kDefaultOrder and the button picker's drawer. Combos carry objectName (profGlobalCombo/profTxCombo/profMicCombo) and accessibleName for the automation bridge and screen readers. Proven on a live FLEX-8400M via the agent automation bridge: dumpTree showed all three combos populated and enabled with the active profile as the current value (Global=ssb, TX=Default, Mic=Default); the radio reported the full enumerated lists (5 global / 11 tx / many mic), and driving the dropdowns emitted the expected profile-load commands and switched live. 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat
There was a problem hiding this comment.
Nice, self-contained applet — the registration mirrors MeterApplet cleanly, the theme::setContainer/objectName/accessibleName wiring matches convention, and the QSignalBlocker-guarded repopulation is the right call: I traced the select→load→*Changed→refresh path and it doesn't form a feedback loop, since every internal rebuildCombo/syncCombo mutation is blocked and only genuine selections reach the load slots. Connections are bound to this, m_model is null-guarded on every entry point, and all six changed files are in scope. One real concern and a couple of small notes.
TX load command deviates from the proven path
The TX row issues:
m_model->sendCommand(QStringLiteral("profile transmit load \"%1\"").arg(name));but the rest of the codebase consistently uses profile tx load, not profile transmit load:
ProfileManagerDialog.cpp:168,181→profile tx load "%1"TransmitModel::loadProfile()(TransmitModel.cpp:480) →profile tx load "%1"
The PR description says "the load command is transmit/mic," but the shipped, working dialog and model both use tx. Even if the radio accepts transmit as an alias (which would explain the live-test passing), this is the one spot in the tree that diverges, and that's a maintenance hazard.
Recommend routing through the existing model helpers instead of hand-rolling command strings — transmitModel() returns a non-const ref, so:
connect(m_txCombo, &QComboBox::currentTextChanged, this, [this](const QString& name) {
if (!name.isEmpty())
m_model->transmitModel().loadProfile(name); // proven "profile tx load"
});
connect(m_micCombo, &QComboBox::currentTextChanged, this, [this](const QString& name) {
if (!name.isEmpty())
m_model->transmitModel().loadMicProfile(name); // proven "profile mic load"
});This gives you a single source of truth for the command strings (the mic lambda currently duplicates loadMicProfile() verbatim), and the global row already delegates correctly via loadGlobalProfile(), so all three rows would be symmetric.
Minor
- When
activeis empty/not-found but the list is non-empty,rebuildCombofalls back to index 0, so the combo may briefly show a profile that isn't the radio's actual active one. It's harmless (the set is signal-blocked, so no spurious load), just a small display caveat worth a comment.
Not blocking beyond the TX verb — switching the two sendCommand calls to the model helpers resolves both the discrepancy and the duplication. Thanks for the thorough write-up and the automation-bridge gap notes (combo item enumeration is a genuinely useful follow-up). 👍
🤖 aethersdr-agent · cost: $5.1031 · model: claude-opus-4-8
…#3829 review) Address review feedback: the TX/Mic rows hand-rolled `profile transmit load` command strings, but the proven path across the tree is `profile tx load` (ProfileManagerDialog.cpp, TransmitModel::loadProfile). Delegate to the model helpers instead — m_model->transmitModel().loadProfile(name) / loadMicProfile(name) — so the radio command verbs live in exactly one place (the models). The commandReady signal is wired to RadioModel::sendCmd (RadioModel.cpp:520), and profile loads pass straight through the interlock lambda untouched, so behavior is identical with no string divergence and no duplication. All three rows now delegate symmetrically (global already did). Also note the cosmetic index-0 fallback in rebuildCombo (active not yet reported) with a comment — harmless, signal-blocked, self-corrects. 💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat
|
Thanks — good catch on the TX verb. Fixed in 6d84f17. All three rows now delegate to the model load helpers instead of hand-rolling command strings: m_globalCombo → m_model->loadGlobalProfile(name);
m_txCombo → m_model->transmitModel().loadProfile(name); // "profile tx load"
m_micCombo → m_model->transmitModel().loadMicProfile(name); // "profile mic load"You were right that origin/main's On the minor note: added a comment at the Corrected the PR description accordingly. |
ten9876
left a comment
There was a problem hiding this comment.
Clean, correct new applet. Feedback-loop prevention is airtight — both rebuildCombo (clear+addItems+setCurrentIndex) and syncCombo (setCurrentIndex) are QSignalBlocker-wrapped, so external profile changes update the combo silently and only genuine user/bridge selections reach the load slots; using currentTextChanged (not textActivated) lets the bridge drive a live load, which the blockers make safe. All three rows delegate to the existing model load helpers (loadGlobalProfile/loadProfile/loadMicProfile) — same source of truth as ProfileManagerDialog — with a !name.isEmpty() guard. Signals connected with this as receiver; setRadioModel called exactly once with the singleton model (no double-connect). Standard PROF registration, objectName/accessibleName for bridge + a11y, no TX-keying surface.
Non-blocking nit: setRadioModel connects unconditionally — fine since one-time-wired, but a disconnect-first guard would be defensive if re-wired. The dumpTree combo-items enumeration gap you found is being added to the #3845 bridge-observability follow-up. Thanks @jensenpat.
## 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>
What
Adds a new compact, dedicated PROF (Profile Switcher) applet to the right-hand applet panel — the persistent, always-available quick-access profile selector requested in #3376. No more opening the full Profile Manager dialog just to switch profiles.
The applet is three tight rows — Global / TX / Mic — each a small label paired with a dropdown that enumerates every profile the radio currently reports. The current item tracks the radio's active profile, and selecting an entry applies it live.
Why
The protocol plumbing for all three profile families already existed (
RadioModel::globalProfiles()/loadGlobalProfile(),TransmitModel::profileList()/micProfileList()+ their*Changedsignals), and the full list is exposed in the Profiles menu andProfileManagerDialog. What was missing — per the issue — is a persistent, low-training surface for fast switching. This delivers exactly that as a panel tile the user can enable and dock wherever they like.How
New widget
src/gui/ProfileSwitcherApplet.{h,cpp}— aQGridLayoutof three label+QComboBoxrows, tight margins,Fixedvertical size policy so it stays small.Live apply on
currentTextChanged():RadioModel::loadGlobalProfile()(profile global load)transmitModel().loadProfile()(profile tx load)transmitModel().loadMicProfile()(profile mic load)All three rows delegate to the model load helpers, so the radio command verbs live in one place (the models) — the same source of truth
ProfileManagerDialoguses (profile tx load/profile mic load).Auto-refresh — the lists and active selections rebuild on
globalProfilesChanged(),profileListChanged(),micProfileListChanged(), and the active highlight stays in sync viastateChanged()/micStateChanged(). So a newly saved profile appears with no manual refresh.No feedback loop — every internal repopulation (
rebuildCombo/syncCombo) is wrapped in aQSignalBlocker, so only genuine selections reach the load slots. UsingcurrentTextChanged()(rather than the user-onlytextActivated()) means the agent automation bridge'sinvoke setCurrentTextcan also drive a live load.Registration mirrors
MeterApplet: idPROF, labelProfile Switcher, off by default, added tokDefaultOrderand the button picker's drawer. Combos carryobjectName(profGlobalCombo/profTxCombo/profMicCombo) andaccessibleNamefor the automation bridge and screen readers.Proof (agent automation bridge, live FLEX-8400M)
dumpTreeshowed all three combos populated and enabled, eachvaluereflecting the radio's active profile: Global=ssb, TX=Default, Mic=Default.ft8, iOS_default_Profile, macOS_default_Profile, SO2RDefault, ssb), 11 tx, and many mic profiles.profile global load,profile tx load,profile mic load) and switched the active profile live — confirmed working end-to-end.Agent automation bridge — gap found
QComboBoxitems —dumpTreereports a combo'scurrentTextasvaluebut not its option list, so the bridge can prove which profile is active but not the full set of choices without stepping the selection (which would apply each one). Proposed: havedumpTreeinclude anitems: [...]array forQComboBoxnodes (and/or aget combo <target> itemsaccessor) so option lists can be verified non-destructively.Closes #3376
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat