feat(hid): initial Icom RC-28 encoder support#3293
Conversation
NF0T
left a comment
There was a problem hiding this comment.
Thanks for putting this together, @w5jwp — the RC-28 is a popular device and this addresses a real gap. I've done a thorough pass on all five changed files and cross-referenced the byte layout claim against wfview and the underlying hidapi platform behavior. Here's the full write-up.
Review: feat(hid): initial Icom RC-28 encoder support
Summary: The core byte layout claim in this PR is correct and independently verified by three sources. The current main is non-functional for real RC-28 hardware. This PR should be the path forward, with two items needing resolution before merge and two orthogonal fixes that should land in a separate PR immediately.
Byte Layout — Dispute Resolved
This PR and the merged #2870 describe incompatible byte offsets for the RC-28 input report. Three independent sources now converge on this PR's layout being correct.
Hardware captures (bobbyrmanson, macOS, Python hidapi):
buf[0] = 0x01 (constant — packet type, not report ID)
buf[1] = detent magnitude/counter
buf[3] = direction: 0x01 = CW, 0x02 = CCW
buf[5] = button state: 0x07 = idle, 0x06 = TX bar, 0x05 = F1, 0x03 = F2
wfview src/usbcontroller.cpp (independent project, real RC-28 hardware):
case RC28:
if ((quint8)data[0] == 0x02) {
// firmware version response — normal reports always have data[0] == 0x01
} else {
tempButtons |= !((quint8)data[5] ^ 0x06) << 0; // TX bar
tempButtons |= !((quint8)data[5] ^ 0x05) << 1; // F1
tempButtons |= !((quint8)data[5] ^ 0x03) << 2; // F2
if ((quint8)data[5] == 0x07) {
if ((quint8)data[3] == 0x01) value += data[1]; // CW
else if ((quint8)data[3] == 0x02) value -= data[1]; // CCW
}
}wfview reads with hid_read(handle, buf, 64) and uses the raw buffer directly — no platform branch, no length discriminator, no report ID strip — on Windows, Linux, and macOS alike. This is the tell: the Icom RC-28 does not declare HID Report IDs in its USB descriptor. hidapi only prepends a report ID byte when a device has multiple named reports; the RC-28 has a single unnamed input report, and hid_read returns the same 32 raw bytes on all platforms.
Why #2870 appeared to work: It was validated against an Arduino-based HIL emulator (0x2341:0x0266, now in kSupportedDevices). That emulator evidently does use HID Report IDs (it reported 33 bytes on all platforms in the bench), which is consistent with the Arduino mbed PluggableUSB stack. The len-based discriminator worked correctly against that emulator. The emulator's wire format is not the real device's wire format.
Impact on current main: On a real RC-28, IcomRC28Parser::parse() reads data[2] (direction) and data[4] (buttons) after optionally stripping a report ID that never exists. Both fields are always 0x00 padding — no rotation event ever fires, and the initial button state comparison produces a spurious event on the first report. Issue #1896 is not actually closed for real hardware.
Change-by-Change Review
HidDeviceParser.cpp / HidDeviceParser.h — IcomRC28Parser ✅
reportSize()33→32: correct; the 33 was sized for a report ID that the real device never sends.if (buf[0] != 0x01) return {}guard: correct;0x02is the firmware query response type,0x01is normal input — wfview uses this exact same discriminator.if (len < 5)→if (len < 6): correct for the new layout (needs up tobuf[5]).- Counter deduplication (
counter != m_prevCounter): more robust thanseq == 0x01; handles any counter behaviour without depending on a specific alternation pattern. - 4-element positional init into the 5-field
HidEventstruct: valid C++20 aggregate init;encoderIndexzero-initialises correctly. Existing callers using{HidEvent::Button, 0, button, action}are unaffected.
HidEncoderManager.h — isRC28Compatible() + LED constants ✅
- Operator precedence: correct —
(vid == 0x0C26 && pid == 0x001E) || (vid == 0x2341 && pid == 0x0266)is explicitly parenthesised. - Explicit
.load(std::memory_order_relaxed)on both atomic members: correct cross-thread access, consistent with the existing pattern established by the StreamDeck+ work. - Active-low LED constants (
RC28_LEDS_OFF=0x0F,RC28_LEDS_LINK=0x07,RC28_LEDS_TX_LINK=0x06) are internally consistent with the input report button encoding (0x07= all bits set = all released).
HidEncoderManager.cpp — LED control, hotplug fix ✅ with one open question
setRC28Leds()called beforehid_close()inclose(): ordering correct — device is still open when the LED-off command is sent.hid_writethroughstd::atomic<hid_device*>: the implicit conversion tohid_device*is well-defined; same pattern used throughout the file.hotplugCheck()scan fix: the current code has a bug where the else-branch (no device open) was missing asupportedDevices()scan, preventing reconnection after a disconnect. The fix is correct.- Open question — LED output report format:
report[0]=0x00, report[1]=0x01, report[2]=ledBytelooks like a plausible hidapi output report structure, but the actual RC-28 output report command format needs a primary source. Cross-referencing wfview's LED control path or the CerberusSolutions FlexRC-28 driver would be useful here.
MainWindow.cpp — buttonPressed handler ✅
- PTT hold-to-talk: the PTT action is checked before
if (action != 0) return;, so both press (setTransmit(true)) and release (setTransmit(false)) are delivered correctly. - Defaults gated on
isRC28Compatible(): correct — non-RC-28 devices keep their existing"None"default and custom bindings are unaffected. moxChangedLED mirroring:TransmitModel::moxChanged(bool)signal exists,setRC28Leds()called viaQMetaObject::invokeMethodto the worker thread — correct cross-thread dispatch.- Step-change toast: clean; Hz/kHz/MHz formatting handles edge cases correctly.
MainWindow.cpp — Clang guard fix ✅ (should be extracted to a separate PR)
hidEncoderDefaultAction() and hidEncoderDefaultPushAction() are defined at the bottom of MainWindow.cpp outside any #ifdef HAVE_HIDAPI guard, while their declarations in MainWindow.h:551-554 are inside the guard. This is a hard compile error on no-HIDAPI builds that CI silently ignores because the CI environment has hidapi installed. The two-line fix is correct and should not wait on the rest of this PR — it's a real build bug affecting any downstream packager who builds without hidapi.
MainWindow.cpp — migration default change
- s.value("HidEncoderAutoDetect", "False").toString() == "True";
+ s.value("HidEncoderAutoDetect", "True").toString() == "True";This one-time migration runs for any user who has never set HidEncoderAutoDetect. Changing the default from "False" to "True" means those users — including anyone running AetherSDR without an encoder device — will have HidEncoderEnabled set to True on their next launch and will incur a USB hotplug scan every 3 seconds indefinitely. If the intent is that RC-28 support should be opt-out for new users this is a reasonable product decision, but it should be made explicitly rather than as a side effect of this PR.
Constitution and Contributing Compliance
- Principle VIII (Evidence Over Assertion): The byte layout claim is now supported by hardware captures, an independent codebase (wfview), and platform analysis. The Windows concern is analytically resolved — no report ID prepend occurs because the RC-28 does not use HID Report IDs. ✅
- Principle XI (Fixes Are Demonstrated): macOS and Debian demonstrated; Windows analytically covered by the HID Report ID analysis. The remaining open item (LED output report format) is low-risk but should be confirmed. ✅
- Principle X (Atomic Commits): PR is correctly rebased against current
main, including the StreamDeck+ changes from #3236 that modified thetuneStepssignal signature andHidEventstruct. ✅ - CODEOWNERS:
src/gui/MainWindow.{h,cpp}is in the maintainer-only tier. This PR requires approval from@ten9876before merge regardless of other reviews.
Recommended Path
- Resolve the migration default question before merge — opt-in (
"False") vs opt-out ("True") for users with no encoder device is a product decision. - Verify the LED output report format against a primary source (wfview's LED path or the FlexRC-28 driver).
- Open a separate minimal PR for the
hotplugCheck()scan fix and the Clang#ifdef HAVE_HIDAPIguard fix — both bugs exist inmaintoday and neither needs to wait on the layout debate. - Once (1) and (2) are addressed, ready for
@ten9876review and approval.
Fixes the RC-28 rotary encoder which was silently producing no events
due to wrong byte offsets in IcomRC28Parser. The parser was reading
direction from buf[2] (always 0x00) instead of the correct buf[3], and
button state from buf[4] instead of buf[5]. Layout verified against
hardware captures and cross-referenced with the open-source FlexRC-28
driver.
Changes:
- Fix IcomRC28Parser byte offsets (direction buf[3], buttons buf[5])
- Add counter-based deduplication on buf[1] to emit exactly one step
per physical detent (device sends bursts of identical reports)
- Fix reportSize 33→32 (no report ID prefix on any platform)
- Add isRC28Compatible() covering Icom (0x0C26:0x001E) and AetherPad
emulator (0x2341:0x0266) — both run the same wire protocol
- Add setRC28Leds() with RC28_LEDS_* constants for LED control
- LINK LED lights when AetherSDR opens the device; extinguishes on close
- TX LED mirrors MOX state; both LEDs sync correctly on hot-plug
- RC-28 button defaults: F1=StepUp, F2=StepDown, TX bar=PTT (hold)
- PTT fires setTransmit on both press and release (hold-to-talk)
- Step-change toast on status bar when F1/F2 pressed
- Fix missing #ifdef HAVE_HIDAPI guards around hidEncoderDefault*
definitions (clang on macOS correctly flagged the mismatch)
- Fix missing return in hotplugCheck() and add full device scan for
encoders connected after AetherSDR starts
Limitations / known issues (follow-up work):
- Button action customization (F1, F2, TX bar) is not yet exposed in
the UI; actions can be overridden via HidKeyAction{0-2} settings keys
- Only supports RC-28 connected directly to the computer running
AetherSDR via USB; connecting the RC-28 directly to the FlexRadio
hardware is not supported by this implementation
Tested on: macOS Tahoe (arm64), Debian 13 (arm64)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove hotplugCheck scan loop and #ifdef HAVE_HIDAPI guard fixes; extracted to standalone PR aethersdr#3298 per reviewer request. - Revert migration default (HidEncoderAutoDetect fallback) to "False" (opt-in baseline); the opt-in vs opt-out product decision does not need to block this PR — if opt-out is chosen it can land as a one-line follow-up after merge. - Add source-citation comment to setRC28Leds(): format verified against FlexRC-28 Node.js driver (_sendLED) and wfview usbcontroller.cpp. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
jensenpat
left a comment
There was a problem hiding this comment.
Re-reviewed at latest push. All three items from the prior review are resolved: the HidEncoderAutoDetect default change was dropped (opt-in preserved), the LED report format is now cited against FlexRC-28 + wfview, and the hotplug/Clang-guard fixes were extracted and merged as #3298. Parser layout matches the wfview cross-reference and hardware captures; CI green. Two non-blocking follow-ups noted (TX LED tracks isMox() rather than interlock transmit state; per-toggle LED writes) — filing a follow-up issue. Approving.
jensenpat
left a comment
There was a problem hiding this comment.
Feedback addressed in latest push; re-reviewed & approved
Feedback addressed in latest push; re-reviewed & approved
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>
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>
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 - [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>
Closes aethersdr#616 ## Summary - **Fixes silent no-op**: `IcomRC28Parser` had wrong byte offsets — direction was being read from `buf[2]` (always `0x00`) instead of `buf[3]`, and buttons from `buf[4]` instead of `buf[5]`. All encoder events were silently discarded. Layout verified against hardware captures and cross-referenced with the open-source FlexRC-28 reference driver. - **Counter-based deduplication**: the RC-28 sends bursts of identical reports per detent; dedup on `buf[1]` emits exactly one step per physical click. - **LED control**: LINK LED lights when AetherSDR opens the device and extinguishes on close/disconnect. TX LED mirrors MOX state. Both sync correctly on hot-plug. Output report format (`[0x00, 0x01, ledByte, zeros…]`, 33 bytes, active-low bitmask) verified against the FlexRC-28 Node.js driver (`_sendLED`) and wfview `src/usbcontroller.cpp` (RC28 `featureLEDControl` path). - **Button defaults for RC-28**: F1=StepUp, F2=StepDown, TX bar=PTT (hold-to-talk — fires `setTransmit` on both press and release). - **Step-change toast**: status bar briefly shows the new step size when F1/F2 is pressed. - **`isRC28Compatible()`**: covers both Icom VID/PID (`0x0C26:0x001E`) and the AetherPad emulator (`0x2341:0x0266`), which runs the same wire protocol. > **Note:** The `hotplugCheck()` scan fix and `#ifdef HAVE_HIDAPI` guard fix have been extracted to PR aethersdr#3298 per reviewer request — both were bugs in `main` independent of this work. > **Migration default (open question):** The one-time migration fallback for `HidEncoderAutoDetect` has been left at `"False"` (opt-in) — matching the pre-PR baseline. This means clean installs will not have `HidEncoderEnabled` set automatically. Whether RC-28 support should be opt-in or opt-out for new users is a product decision left open for the merge review. ## Limitations / follow-up work - **Button customization UI is not included in this PR.** F1, F2, and TX bar actions can be overridden via the `HidKeyAction{0-2}` settings keys, but there is no UI for this yet. A follow-up PR will add a configuration panel. - **Direct USB connection only.** This implementation supports the RC-28 connected via USB directly to the computer running AetherSDR. Connecting the RC-28 directly to the FlexRadio hardware is not addressed by this change. ## Test plan - [ ] RC-28 LINK LED lights on app launch; goes out when AetherSDR quits - [ ] Rotating encoder CW/CCW moves VFO frequency - [ ] F1 steps tuning rate up; status bar shows new step size - [ ] F2 steps tuning rate down - [ ] Holding TX bar activates MOX and lights TX LED; releasing drops both - [ ] Hot-plug: unplug and re-plug RC-28 while app is running — LINK LED returns and encoder/buttons resume without restart ## Platform testing | Platform | Result | |---|---| | macOS Tahoe (arm64) | ✅ Tested | | Debian 13 (arm64) | ✅ Tested | 🤖 Generated with [Claude Code](https://claude.ai/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…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>
Closes #616
Summary
IcomRC28Parserhad wrong byte offsets — direction was being read frombuf[2](always0x00) instead ofbuf[3], and buttons frombuf[4]instead ofbuf[5]. All encoder events were silently discarded. Layout verified against hardware captures and cross-referenced with the open-source FlexRC-28 reference driver.buf[1]emits exactly one step per physical click.[0x00, 0x01, ledByte, zeros…], 33 bytes, active-low bitmask) verified against the FlexRC-28 Node.js driver (_sendLED) and wfviewsrc/usbcontroller.cpp(RC28featureLEDControlpath).setTransmiton both press and release).isRC28Compatible(): covers both Icom VID/PID (0x0C26:0x001E) and the AetherPad emulator (0x2341:0x0266), which runs the same wire protocol.Limitations / follow-up work
HidKeyAction{0-2}settings keys, but there is no UI for this yet. A follow-up PR will add a configuration panel.Test plan
Platform testing
🤖 Generated with Claude Code