Skip to content

feat(hid): RC-28 button-mapping config dialog (#3323)#3328

Merged
jensenpat merged 3 commits into
aethersdr:mainfrom
w5jwp:feat/rc28-mapping-dialog
Jun 3, 2026
Merged

feat(hid): RC-28 button-mapping config dialog (#3323)#3328
jensenpat merged 3 commits into
aethersdr:mainfrom
w5jwp:feat/rc28-mapping-dialog

Conversation

@w5jwp

@w5jwp w5jwp commented May 31, 2026

Copy link
Copy Markdown
Contributor

Closes #3323. Extends the initial RC-28 support delivered in #3293, which shipped working encoder/LED/PTT handling but deferred the configuration panel to a follow-up — this is that follow-up.

Summary

  • RC28MappingDialog — a dedicated config panel for the Icom RC-28 (Settings → Icom RC-28 Remote Encoder...): assign F1/F2 short-press and long-press (600 ms hold) actions, choose PTT mode (Momentary vs Latched), invert encoder direction, and view live device info (VID/PID, path, serial, firmware) plus a button-event activity log. The TX bar is hardwired to PTT and is not remappable.
  • New tuning actions — Snap to nearest 100 Hz / 500 Hz / 1 kHz / 100 kHz / 500 kHz, selectable as F1/F2 actions.
  • Per-key hold detection — independent timers so F1 and F2 can be held at once; short press fires on release, long-press is a latched toggle. F1/F2 LEDs mirror their hold-action state.
  • Multi-device guard — when more than one RC-28 is enumerated, open() is deferred and the dialog surfaces a warning until only one remains.

Configuration storage (Principle V / XIV)

All RC-28 mapping persists as a single nested-JSON blob under one root AppSettings key, RC28Mapping (fields: f1Press, f1Hold, f2Press, f2Hold, pttMode), via two shared helpers (HidEncoderManager::rc28MappingField / setRc28MappingField). Writes regenerate the full object and persist atomically (single setValue + save). No new flat keys are introduced for this feature; the pre-existing HidEncoderInvertDir flat key is grandfathered. No migration shim is included because the namespaced flat keys were never shipped to users (agreed on #3323).

Namespacing

RC-28 F1/F2 configuration is fully separated from the generic StreamDeck+ HidKeyAction* settings, so the two devices can no longer clobber each other's storage or diverge in displayed labels.

Protocol verification (Principle I / VIII)

The LED output report format and button/encoder report layout were verified against the wfview source (src/usbcontroller.cpp RC28 featureLEDControl path) and the open-source FlexRC-28 Node.js reference driver. Behaviour was additionally exercised on physical Icom RC-28 hardware.

Test plan

  • Build links clean on macOS Tahoe (arm64), no new warnings
  • F1/F2 short-press fires the assigned action; long-press (>=600 ms) fires the assigned hold action as a latched toggle
  • F1/F2 LEDs track their hold-action on/off state; no LED writes while a button is held (single post-release write)
  • TX bar transmits in both Momentary (hold-to-talk) and Latched (press-to-toggle) modes; latched TX drops on radio disconnect
  • Snap-to-nearest actions round the active slice to the configured grid
  • Settings round-trip: every field persists across app restart; blob written atomically and survives the AppSettings corruption guard
  • Multi-device: a second RC-28 defers open() and raises the dialog warning; removing it restores normal operation via hotplug

CODEOWNERS

Touches maintainer-owned paths (MainWindow.{h,cpp}, CMakeLists.txt) and requires maintainer review.

🤖 Generated with Claude Code

@w5jwp w5jwp requested review from a team as code owners May 31, 2026 20:57
@jensenpat jensenpat self-assigned this May 31, 2026

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

Thanks for the thorough implementation here. I’m holding merge because I found behavior regressions that CI cannot catch.

Findings:

  1. src/gui/MainWindow.cpp:6857 uses TransmitModel::isMox() to drive the RC-28 TX LED. In this model m_mox is only updated from a mox= transmit status key, and the radio does not send that key on current firmware; local setMox() also updates m_transmitting, not m_mox. As written, bit0 stays high and the RC-28 TX LED will not track local PTT, latched PTT, or hardware TX even though the PR claims it mirrors TX. Use the authoritative transmit state, isTransmitting(), for the LED calculation.

  2. src/gui/RC28MappingDialog.cpp:216-219 changes the default F-key mappings from both the existing RC-28 behavior and #3323’s accepted defaults. Existing users with no RC28Mapping blob will now get F1 short = StepCycle instead of StepUp, F2 short = None instead of StepDown, and F2 hold = None instead of the requested ModeCycle. Default-value changes are user-visible, so please preserve the issue’s defaults unless there is an explicit maintainer decision to change them.

  3. src/core/HidEncoderManager.cpp:118 makes the multi-device warning edge-triggered only. If app startup or hotplug detects two RC-28s before RC28MappingDialog exists, later opening the dialog only calls refreshDeviceInfo(), sees isOpen()==false, and shows “Not connected” while m_multipleDetected suppresses repeated multipleDevicesDetected signals on hotplug retries. The PR says the dialog surfaces the warning until only one remains, so the blocked state needs to be queryable/replayed when the dialog opens, or otherwise refreshed while still blocked.

CI is green and a synthetic merge into current main is textually clean, so this looks close. Please address the items above and I’ll re-review.

w5jwp added a commit to w5jwp/AetherSDR that referenced this pull request May 31, 2026
- Fix TX LED to use isTransmitting() not isMox() (isMox reads m_mox
  which firmware never updates; isTransmitting reads m_transmitting
  which setTransmitting() sets and emits moxChanged on)
- Fix defaults: F1 short→StepUp, F2 short→StepDown, F2 hold→ModeCycle
  (was StepCycle/None/None — now consistent across dialog combos,
  hold timer, press dispatch, and LED read in all 4 locations)
- Add isBlockedByMultiple()/blockedDeviceName() to HidEncoderManager;
  refreshDeviceInfo() replays multi-device warning for dialogs opened
  after the initial edge-triggered emit
- Add StepUp/StepDown to kActions and ModeCycle to kHoldActions so
  the dialog combos can display and persist the new defaults
- Remove dead RC28_LEDS_LINK and RC28_LEDS_TX_LINK constants
- Fix misleading kHoldActions comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@w5jwp

w5jwp commented May 31, 2026

Copy link
Copy Markdown
Contributor Author

Hi @jensenpat — thanks for the detailed review. All three findings have been addressed in the latest push:

Finding 1 — TX LED reads wrong state
Fixed. updateRC28Leds() now calls isTransmitting() instead of isMox(). isMox() reads m_mox which firmware never updates via the socket layer; isTransmitting() reads m_transmitting which is set by setTransmitting() and also drives the moxChanged() signal we're already connected to.

Finding 2 — Dialog defaults don't match expected RC-28 behaviour
Fixed. Defaults are now F1 short-press → Step Up, F2 short-press → Step Down, F2 hold → Mode Cycle. These are applied consistently in all four locations: the dialog combos, the hold timer default, the press dispatch default, and the LED state read. Also added StepUp/StepDown to kActions and ModeCycle to kHoldActions so the entries can be displayed and persisted correctly.

Finding 3 — Multi-device warning not shown when dialog opened after initial detection
Fixed. Added isBlockedByMultiple() and blockedDeviceName() getters to HidEncoderManager (storing the device name in a new m_blockedDeviceName member). refreshDeviceInfo() now checks isBlockedByMultiple() at entry and replays the warning banner for dialogs opened after the edge-triggered multipleDevicesDetected signal has already fired.


All test cases were re-run after these changes and the build is clean (no new warnings from RC-28 code). Ready for re-review when you have a chance.

@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 @w5jwp — the follow-up reads well. Single-JSON-blob storage via rc28MappingField/setRc28MappingField matches Principle V cleanly, the per-key hold timers and the post-release LED write are a nicer model than the prior approach, and the isRC28CompatibleId lift to a static avoids the duplication that the multi-device guard would have invited. AppSettings (not QSettings), QPointers for the dialog/encoder, parented QTimers, queued cross-thread invokes for setInvertDirection/setRC28Leds, and bounded log (setMaximumBlockCount(50)) all match house conventions. The multi-device guard's serial-then-path keying with macOS usage-collection caveat in the comment is well-justified, and the m_rc28PttLatched safety drop on disconnect is the right call.

One substantive observation and a couple of small notes:

1. pttMode is read for any "PTT" action, not only the RC-28 TX bar (src/gui/MainWindow.cpp:4083-4084).

A StreamDeck+ user who maps a generic HidKeyAction* to "PTT" will silently inherit the RC-28's latched mode, because the pttMode lookup runs unconditionally for actionName == "PTT". This contradicts the dialog's framing (TX bar is "hardwired to PTT" and the latched/momentary radios live in the RC-28 section) and would surprise a user who configured latched for their RC-28 and then bound a deck key to PTT.

Suggested gate:

const bool latched = m_hidEncoder->isRC28Compatible()
    && HidEncoderManager::rc28MappingField("pttMode", "Momentary") == "Latched";

For symmetry the two m_rc28MappingDialog->appendButtonEvent(...) log lines in this branch probably also want a non-RC-28 fallback label (or to be skipped entirely for non-RC-28 sources), since m_rc28PttLatched and the "TX bar" wording are RC-28 concepts.

2. Related (very minor): the short-press/key-press dispatch logs "key N press" to the RC-28 dialog when the source isn't the RC-28 (dispatchHidAction in MainWindow.cpp:4113 → 6876–6877). If the dialog is open while a StreamDeck+ key fires, the user sees an entry that doesn't correspond to anything on their RC-28. Easy to suppress by checking m_hidEncoder->isRC28Compatible() before the appendButtonEvent inside dispatchHidAction, or by passing a "source is RC-28" flag through the gesture label.

3. Stale tune-mode flags across rebinding (minor). If the user has TuneFast latched on (m_hidFastTune == true) and then rebinds f1Hold to something else via the dialog, m_hidFastTune stays true — direct-tune mode keeps eating encoder steps even though nothing on the device toggles it anymore. A one-liner in setRc28MappingField (or in the dialog's combo handler) to clear m_hidFastTune/m_hidFineTune when their owning slot changes would close that. Not blocking — easy to recover by unplug/replug.

Nothing else jumped out. The cross-thread access of the new diagnostic fields (m_devicePath, m_serialNumber, m_releaseNumber, m_multipleDetected, m_blockedDeviceName) matches the existing const-after-open / benign-stale-read convention you explicitly called out in the header — fine.


🤖 aethersdr-agent · cost: $15.2298 · model: claude-opus-4-7

@w5jwp

w5jwp commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Hi @AetherClaude and @jensenpat — all four findings from the last review have been addressed in the latest push (55837f63):

Finding 1 — pttMode latched applies to any PTT source, not just RC-28 TX bar (substantive)
Fixed. const bool latched is now gated behind m_hidEncoder->isRC28Compatible(), so a StreamDeck+ key bound to "PTT" always behaves momentary regardless of the RC-28 latched-PTT setting. The appendButtonEvent calls inside the PTT block are gated the same way, so "TX bar" labels can't appear in the activity log for non-RC-28 sources.

Finding 2 — dispatchHidAction logs to RC-28 dialog for non-RC-28 sources (minor)
Fixed. appendButtonEvent in dispatchHidAction is now additionally gated on isRC28Compatible(), so StreamDeck+ button presses no longer appear in the RC-28 activity log if the dialog happens to be open.

Finding 3 — Stale tune-mode flags after rebinding (minor)
Fixed. Added mappingFieldChanged(field, value) signal to RC28MappingDialog, emitted from populateActionCombo's combo handler after each setRc28MappingField write. On first open of the dialog, MainWindow connects to this signal and clears m_hidFastTune/m_hidFineTune when f1Hold or f2Hold is rebound — preventing the encoder from staying in fast/fine-tune mode after the owning slot is reassigned. updateRC28Leds() is called immediately after clearing the flags so the F-key LED extinguishes at the moment of rebind rather than waiting for the next unrelated state change.

Build is clean on macOS Tahoe (arm64), no new warnings from RC-28 code.

@w5jwp w5jwp requested a review from jensenpat June 1, 2026 12:21
jensenpat and others added 3 commits June 1, 2026 20:37
Hotfix release on top of v26.6.1, bundling three already-merged fixes
and nothing else:

- fix(ci): unblock macOS DMG build broken by missing background image (aethersdr#3349)
- fix(gui): only toast "Step: …" on deliberate step changes (aethersdr#3337)
- fix(radio-setup): wrap tab pages in QScrollArea so tall tabs stay reachable (aethersdr#3347)

Bumps project version 26.6.1 -> 26.6.1.1 (CalVer hotfix sub-patch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes aethersdr#3323. Extends the initial RC-28 support delivered in aethersdr#3293,
which shipped working encoder/LED/PTT handling but deferred the
configuration panel to a follow-up — this is that follow-up.

## What this adds
- RC28MappingDialog: a dedicated config panel for the Icom RC-28.
  Assign F1/F2 short-press and long-press (600 ms hold) actions, choose
  PTT mode (Momentary vs Latched), invert encoder direction, and view
  live device info (VID/PID, path, serial, firmware) plus a button-event
  activity log. The TX bar is hardwired to PTT and is not remappable.
- New tuning actions: Snap to nearest 100 Hz / 500 Hz / 1 kHz / 100 kHz
  / 500 kHz, selectable as F1/F2 actions.
- Per-key independent hold detection (short press on release, latched
  long-press toggle), and F1/F2 LEDs that mirror their hold-action state.
- Multi-device guard: when more than one RC-28 is enumerated, open() is
  deferred and the dialog surfaces a warning until only one remains.

## Configuration storage (Principle V / XIV)
All RC-28 mapping is persisted as a single nested-JSON blob under one
root AppSettings key, "RC28Mapping" (fields: f1Press, f1Hold, f2Press,
f2Hold, pttMode), via two shared static helpers
(HidEncoderManager::rc28MappingField / setRc28MappingField). Writes
regenerate the full object and persist atomically (single setValue +
save). No new flat keys are introduced for this feature. The pre-
existing HidEncoderInvertDir flat key is grandfathered and left as-is.
Per the agreement on aethersdr#3323, no migration shim is included because the
namespaced flat keys were never shipped to users.

## Namespacing
RC-28 F1/F2 configuration is fully separated from the generic
StreamDeck+ HidKeyAction* settings, so the two devices can no longer
clobber each other's storage or diverge in displayed labels.

## Protocol verification (Principle I / VIII)
The LED output report format and button/encoder report layout were
verified against the wfview source (src/usbcontroller.cpp RC28
featureLEDControl path) and the open-source FlexRC-28 Node.js reference
driver. Behaviour was additionally exercised on physical Icom RC-28
hardware.

## Test plan (all passing)
- Build: links clean on macOS Tahoe (arm64), no new warnings.
- F1/F2 short-press fires the assigned action; long-press (>=600 ms)
  fires the assigned hold action as a latched toggle.
- F1/F2 LEDs track their hold-action on/off state; no LED writes occur
  while a button is held (single post-release write).
- TX bar transmits in both Momentary (hold-to-talk) and Latched
  (press-to-toggle) modes; latched TX drops on radio disconnect.
- Snap-to-nearest actions round the active slice to the configured grid.
- Settings round-trip: every field persists across app restart; the
  blob is written atomically and survives the AppSettings corruption
  guard.
- Multi-device: a second RC-28 defers open() and raises the dialog
  warning; removing it restores normal operation via hotplug.

CODEOWNERS note: this PR touches maintainer-owned paths
(MainWindow.{h,cpp}, CMakeLists.txt) and requires maintainer review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All findings from the post-rebase review against v26.6.1.1 resolved.

jensenpat review findings:
- TX LED now uses isTransmitting() instead of isMox(). m_mox is only
  updated by the socket mox= key that current firmware does not send;
  isTransmitting() (driven by setTransmitting()) is the authoritative
  local transmit state.
- Dialog defaults restored to aethersdr#3323 accepted values: F1 short-press →
  StepUp, F2 short-press → StepDown, F2 hold → ModeCycle, applied
  consistently in the combo defaults, hold timer defaults, press
  dispatch defaults, and LED state read.
- Multi-device warning now replayed on dialog open: isBlockedByMultiple()
  and blockedDeviceName() getters added to HidEncoderManager; the
  dialog's refreshDeviceInfo() checks the blocked state on entry so
  the warning appears even when the dialog is opened after the edge-
  triggered multipleDevicesDetected signal has already fired.

aethersdr-agent review findings:
- PTT latched mode now gated on isRC28Compatible(): a StreamDeck+ key
  bound to "PTT" always behaves momentary; appendButtonEvent in the PTT
  block is gated the same way so "TX bar" labels can't appear for non-
  RC-28 sources.
- dispatchHidAction: appendButtonEvent is now gated on isRC28Compatible()
  so non-RC-28 button events (e.g. StreamDeck+) do not appear in the
  RC-28 activity log.
- Stale tune-mode flags: RC28MappingDialog emits mappingFieldChanged
  after each setRc28MappingField write; MainWindow clears m_hidFastTune
  and m_hidFineTune when f1Hold/f2Hold is rebound and calls
  updateRC28Leds() immediately to extinguish the F-key LED.

Additional findings from post-rebase code review:
- Dialog title corrected to "Icom RC-28 Button Mapping" for consistency
  with all other RC-28 references in the application.
- AppSettings race: HidKeyAction0/1 → RC28Mapping one-time migration
  moved to the main thread (MainWindow constructor, before the ext-ctrl
  invokeMethod) to avoid touching AppSettings from two threads.
  AppSettings has no mutex.
- Migration bypass: migration block moved outside the HidEncoderEnabled
  gate so users who had HID disabled at upgrade time still get their
  old HidKeyAction0/1 mappings migrated when they later enable HID
  via Preferences. The inner !s.contains("RC28Mapping") guard keeps it
  a true one-shot.
- m_rc28PttLatched safety: latch flag cleared in
  onConnectionStateChanged(false) so a radio TCP drop doesn't leave
  a phantom latched-TX state that would re-key the radio on reconnect.
- m_multipleDetected: changed from bool to std::atomic<bool> with
  release store and acquire load; m_blockedDeviceName is written before
  the release store establishing happens-before with the acquire load in
  blockedDeviceName().
- updateRC28Leds() guard: isOpen() check added so the LED-update
  invokeMethod is not queued when the device handle is already closed.
- loadSettings() idempotency: isOpen() early-return added so repeated
  Preferences-dialog OK clicks do not close and reopen a live device.
  The stale HidEncoderAutoDetect check removed; callers gate on the
  new HidEncoderEnabled key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@w5jwp w5jwp force-pushed the feat/rc28-mapping-dialog branch from 55837f6 to 5ad10a2 Compare June 2, 2026 17:09
@w5jwp

w5jwp commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Hi @jensenpat and @AetherClaude — rebased onto v26.6.1.1 and completed a full post-rebase review. The branch now has a clean 2-commit history: the original feature commit and one consolidated fix commit covering everything below.


All review findings addressed

jensenpat (#1) — TX LED reads wrong state
Fixed. updateRC28Leds() now uses isTransmitting() (reads m_transmitting, set by setTransmitting() and the source of moxChanged) instead of isMox() (reads m_mox, which current firmware's socket layer does not update).

jensenpat (#2) — Dialog defaults don't match accepted #3323 values
Fixed. Defaults are F1 short-press → Step Up, F2 short-press → Step Down, F2 hold → Mode Cycle, applied consistently in combo initial values, hold-timer dispatch defaults, press-dispatch defaults, and LED state reads.

jensenpat (#3) — Multi-device warning not shown when dialog opened after initial detection
Fixed. isBlockedByMultiple() and blockedDeviceName() getters added to HidEncoderManager. refreshDeviceInfo() checks the blocked state at entry and replays the warning for dialogs opened after the edge-triggered multipleDevicesDetected signal has already fired.

aethersdr-agent (#1) — pttMode latched applies to any PTT source, not only RC-28 TX bar
Fixed. const bool latched is gated on m_hidEncoder->isRC28Compatible(). A StreamDeck+ key bound to "PTT" always behaves momentary. The appendButtonEvent calls in the PTT block carry the same gate so "TX bar" labels can't appear in the log for non-RC-28 sources.

aethersdr-agent (#2) — dispatchHidAction logs non-RC-28 events to the RC-28 dialog
Fixed. appendButtonEvent in dispatchHidAction is additionally gated on isRC28Compatible().

aethersdr-agent (#3) — Stale tune-mode flags after rebinding
Fixed. RC28MappingDialog emits mappingFieldChanged(field, value) after each write. MainWindow connects to it on first dialog open and clears m_hidFastTune/m_hidFineTune when f1Hold/f2Hold is rebound, then calls updateRC28Leds() immediately to extinguish the F-key LED.


Additional findings corrected during post-rebase review

  • Dialog title corrected from "RC-28 Button Mapping" to "Icom RC-28 Button Mapping" for consistency with all other RC-28 references in the UI.
  • AppSettings race: the HidKeyAction0/1 → RC28Mapping one-time migration was running on the ext-ctrl thread inside loadSettings(). Moved it to the main thread in the MainWindow constructor (before the QMetaObject::invokeMethod that starts loadSettings()). AppSettings has no mutex.
  • Migration bypass: migration was gated on HidEncoderEnabled=True at startup, so users who had HID disabled at upgrade time would silently lose their old F1/F2 assignments. Block now runs unconditionally; the inner !s.contains("RC28Mapping") sentinel keeps it a true one-shot.
  • m_rc28PttLatched safety: latch flag cleared in onConnectionStateChanged(false) to prevent a phantom latched-TX state from re-keying the radio on reconnect after a TCP drop.
  • m_multipleDetected data race: changed from bool to std::atomic<bool> with release store / acquire load; m_blockedDeviceName is written before the release store establishing the required happens-before.
  • updateRC28Leds() guard: isOpen() check added so no LED-update invoke is queued against a closed device handle.
  • loadSettings() idempotency: isOpen() early-return added so repeated Preferences OK clicks don't close and reopen a live device. Removed the stale HidEncoderAutoDetect check; callers gate on the new HidEncoderEnabled key.

Test coverage

All changes have been tested on macOS Tahoe (arm64) and Linux (x86_64). Build is clean with no new warnings from RC-28 code. Verified:

  • All six original review findings behave as described
  • F1/F2 short-press, long-press, LED state, PTT momentary/latched all function correctly end-to-end
  • Settings round-trip across app restart
  • Radio disconnect drops latched TX and clears PTT latch flag
  • Preferences OK click with HID active does not cause unnecessary device reconnect
  • Dialog opened after multi-device detection correctly shows the blocked-state warning

Requesting re-review from @jensenpat and @AetherClaude.

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

Re-reviewed at 5ad10a2. All three CHANGES_REQUESTED findings are addressed:

  1. RC-28 TX LED now derives from TransmitModel::isTransmitting() in updateRC28Leds(), so it tracks local/latched PTT and hardware TX rather than the never-sent mox= key.
  2. Default F-key mappings restored to the #3323 defaults: F1 short StepUp, F2 short StepDown, F2 hold ModeCycle — consistent across the dialog combos and the dispatch fallbacks.
  3. Multi-device blocked state is now queryable (isBlockedByMultiple()/blockedDeviceName()) and refreshDeviceInfo() replays the warning when the dialog opens while two encoders remain.

aethersdr-agent's notes are also handled: latched-PTT mode is gated on isRC28Compatible() so generic HID keys bound to PTT stay momentary; the dialog activity-log lines are RC-28-gated; and stale fast/fine-tune flags are cleared on hold-action rebind and on disconnect.

CI green on all platforms; all commits GPG-verified. Approving.

@jensenpat jensenpat merged commit d876385 into aethersdr:main Jun 3, 2026
5 checks passed
@w5jwp w5jwp deleted the feat/rc28-mapping-dialog branch June 3, 2026 22:37
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…rsdr#3328)

Closes aethersdr#3323. Extends the initial RC-28 support delivered in aethersdr#3293,
which shipped working encoder/LED/PTT handling but deferred the
configuration panel to a follow-up — this is that follow-up.

## Summary
- **RC28MappingDialog** — a dedicated config panel for the Icom RC-28
(Settings → Icom RC-28 Remote Encoder...): assign F1/F2 short-press and
long-press (600 ms hold) actions, choose PTT mode (Momentary vs
Latched), invert encoder direction, and view live device info (VID/PID,
path, serial, firmware) plus a button-event activity log. The TX bar is
hardwired to PTT and is not remappable.
- **New tuning actions** — Snap to nearest 100 Hz / 500 Hz / 1 kHz / 100
kHz / 500 kHz, selectable as F1/F2 actions.
- **Per-key hold detection** — independent timers so F1 and F2 can be
held at once; short press fires on release, long-press is a latched
toggle. F1/F2 LEDs mirror their hold-action state.
- **Multi-device guard** — when more than one RC-28 is enumerated,
`open()` is deferred and the dialog surfaces a warning until only one
remains.

## Configuration storage (Principle V / XIV)
All RC-28 mapping persists as a single nested-JSON blob under one root
AppSettings key, `RC28Mapping` (fields: `f1Press`, `f1Hold`, `f2Press`,
`f2Hold`, `pttMode`), via two shared helpers
(`HidEncoderManager::rc28MappingField` / `setRc28MappingField`). Writes
regenerate the full object and persist atomically (single `setValue` +
`save`). No new flat keys are introduced for this feature; the
pre-existing `HidEncoderInvertDir` flat key is grandfathered. No
migration shim is included because the namespaced flat keys were never
shipped to users (agreed on aethersdr#3323).

## Namespacing
RC-28 F1/F2 configuration is fully separated from the generic
StreamDeck+ `HidKeyAction*` settings, so the two devices can no longer
clobber each other's storage or diverge in displayed labels.

## Protocol verification (Principle I / VIII)
The LED output report format and button/encoder report layout were
verified against the **wfview** source (`src/usbcontroller.cpp` RC28
`featureLEDControl` path) and the open-source **FlexRC-28** Node.js
reference driver. Behaviour was additionally exercised on physical Icom
RC-28 hardware.

## Test plan
- [x] Build links clean on macOS Tahoe (arm64), no new warnings
- [x] F1/F2 short-press fires the assigned action; long-press (>=600 ms)
fires the assigned hold action as a latched toggle
- [x] F1/F2 LEDs track their hold-action on/off state; no LED writes
while a button is held (single post-release write)
- [x] TX bar transmits in both Momentary (hold-to-talk) and Latched
(press-to-toggle) modes; latched TX drops on radio disconnect
- [x] Snap-to-nearest actions round the active slice to the configured
grid
- [x] Settings round-trip: every field persists across app restart; blob
written atomically and survives the AppSettings corruption guard
- [x] Multi-device: a second RC-28 defers open() and raises the dialog
warning; removing it restores normal operation via hotplug

## CODEOWNERS
Touches maintainer-owned paths (`MainWindow.{h,cpp}`, `CMakeLists.txt`)
and requires maintainer review.

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

---------

Co-authored-by: jensenpat <patjensen@gmail.com>
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.

feat(hid): RC-28 Button Mapping dialog with press/hold, PTT mode, and device info panel

2 participants