Skip to content

feat(gui): PROF (Profile Switcher) applet — live Global/TX/Mic profile selection (#3376)#3829

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:feat/3376-profile-switcher-applet
Jun 26, 2026
Merged

feat(gui): PROF (Profile Switcher) applet — live Global/TX/Mic profile selection (#3376)#3829
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:feat/3376-profile-switcher-applet

Conversation

@jensenpat

@jensenpat jensenpat commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

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 *Changed signals), and the full list is exposed in the Profiles menu and ProfileManagerDialog. 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} — a QGridLayout of three label+QComboBox rows, tight margins, Fixed vertical size policy so it stays small.

  • Live apply on currentTextChanged():

    • Global → RadioModel::loadGlobalProfile() (profile global load)
    • TX → transmitModel().loadProfile() (profile tx load)
    • Mic → 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 ProfileManagerDialog uses (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 via stateChanged()/micStateChanged(). So a newly saved profile appears with no manual refresh.

  • No feedback loop — every internal repopulation (rebuildCombo/syncCombo) is wrapped in a QSignalBlocker, so only genuine selections reach the load slots. Using currentTextChanged() (rather than the user-only textActivated()) means the agent automation bridge's invoke setCurrentText can also drive 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.

Proof (agent automation bridge, live FLEX-8400M)

  • dumpTree showed all three combos populated and enabled, each value reflecting the radio's active profile: Global=ssb, TX=Default, Mic=Default.
  • The radio reported the full enumerated lists the applet was built from: 5 global (ft8, iOS_default_Profile, macOS_default_Profile, SO2RDefault, ssb), 11 tx, and many mic profiles.
  • Driving the dropdowns emitted the expected commands (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

  • Can't enumerate QComboBox itemsdumpTree reports a combo's currentText as value but 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: have dumpTree include an items: [...] array for QComboBox nodes (and/or a get combo <target> items accessor) so option lists can be verified non-destructively.

Closes #3376

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

…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
@jensenpat jensenpat marked this pull request as ready for review June 26, 2026 04:45
@jensenpat jensenpat requested review from a team as code owners June 26, 2026 04:45

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

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,181profile 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 active is empty/not-found but the list is non-empty, rebuildCombo falls 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
@jensenpat

Copy link
Copy Markdown
Collaborator Author

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 ProfileManagerDialog/TransmitModel both use profile tx load — my PR description ("the load command is transmit/mic") was stale, carried over from an older checkout; the radio does accept transmit as an alias which is why the live test passed, but tx is now the single source of truth. Verified the helper route is wired end-to-end: TransmitModel::commandReadyRadioModel's connect at RadioModel.cpp:472sendCmd(cmd) at line 520, and profile loads aren't special-cased by the interlock lambda so they pass straight through. This also removes the mic-string duplication you flagged.

On the minor note: added a comment at the rebuildCombo index-0 fallback explaining it's a cosmetic "active not yet reported" case — signal-blocked so no spurious load, and the next *Changed/state signal corrects the highlight.

Corrected the PR description accordingly.

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

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.

@ten9876 ten9876 merged commit 88685ec into aethersdr:main Jun 26, 2026
6 checks passed
ten9876 added a commit that referenced this pull request Jun 28, 2026
## 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>
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.

Add a persistent, quick-access global profile selector

2 participants