Skip to content

feat(hid): initial Icom RC-28 encoder support#3293

Merged
jensenpat merged 2 commits into
aethersdr:mainfrom
w5jwp:fix/rc28-hid-encoder
May 31, 2026
Merged

feat(hid): initial Icom RC-28 encoder support#3293
jensenpat merged 2 commits into
aethersdr:mainfrom
w5jwp:fix/rc28-hid-encoder

Conversation

@w5jwp

@w5jwp w5jwp commented May 30, 2026

Copy link
Copy Markdown
Contributor

Closes #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 #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

@w5jwp w5jwp requested a review from a team as a code owner May 30, 2026 16:54
@NF0T NF0T self-assigned this May 30, 2026
NF0T
NF0T previously requested changes May 30, 2026

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

  • 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; 0x02 is the firmware query response type, 0x01 is normal input — wfview uses this exact same discriminator.
  • if (len < 5)if (len < 6): correct for the new layout (needs up to buf[5]).
  • Counter deduplication (counter != m_prevCounter): more robust than seq == 0x01; handles any counter behaviour without depending on a specific alternation pattern.
  • 4-element positional init into the 5-field HidEvent struct: valid C++20 aggregate init; encoderIndex zero-initialises correctly. Existing callers using {HidEvent::Button, 0, button, action} are unaffected.

HidEncoderManager.hisRC28Compatible() + 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 before hid_close() in close(): ordering correct — device is still open when the LED-off command is sent.
  • hid_write through std::atomic<hid_device*>: the implicit conversion to hid_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 a supportedDevices() scan, preventing reconnection after a disconnect. The fix is correct.
  • Open question — LED output report format: report[0]=0x00, report[1]=0x01, report[2]=ledByte looks 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.
  • moxChanged LED mirroring: TransmitModel::moxChanged(bool) signal exists, setRC28Leds() called via QMetaObject::invokeMethod to 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 ⚠️ Needs a deliberate decision

- 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 the tuneSteps signal signature and HidEvent struct. ✅
  • CODEOWNERS: src/gui/MainWindow.{h,cpp} is in the maintainer-only tier. This PR requires approval from @ten9876 before merge regardless of other reviews.

Recommended Path

  1. Resolve the migration default question before merge — opt-in ("False") vs opt-out ("True") for users with no encoder device is a product decision.
  2. Verify the LED output report format against a primary source (wfview's LED path or the FlexRC-28 driver).
  3. Open a separate minimal PR for the hotplugCheck() scan fix and the Clang #ifdef HAVE_HIDAPI guard fix — both bugs exist in main today and neither needs to wait on the layout debate.
  4. Once (1) and (2) are addressed, ready for @ten9876 review and approval.

@w5jwp w5jwp requested a review from NF0T May 30, 2026 23:47
w5jwp and others added 2 commits May 30, 2026 21:59
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 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 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 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.

Feedback addressed in latest push; re-reviewed & approved

@jensenpat jensenpat dismissed NF0T’s stale review May 31, 2026 05:04

Feedback addressed in latest push; re-reviewed & approved

@jensenpat jensenpat merged commit bb2308e into aethersdr:main May 31, 2026
5 checks passed
w5jwp added a commit to w5jwp/AetherSDR that referenced this pull request May 31, 2026
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>
w5jwp added a commit to w5jwp/AetherSDR that referenced this pull request Jun 2, 2026
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>
jensenpat added a commit that referenced this pull request Jun 3, 2026
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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
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>
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.

ICOM RC-28 remote encoder (USB VFO)

3 participants