Skip to content

feat(hid): recognize aether-pad RC-28 emulator (Arduino VID/PID alias)#3171

Merged
jensenpat merged 1 commit into
aethersdr:mainfrom
nigelfenton:feat/aetherpad-rc28-alias
May 26, 2026
Merged

feat(hid): recognize aether-pad RC-28 emulator (Arduino VID/PID alias)#3171
jensenpat merged 1 commit into
aethersdr:mainfrom
nigelfenton:feat/aetherpad-rc28-alias

Conversation

@nigelfenton

Copy link
Copy Markdown
Contributor

Summary

Adds the Arduino composite VID/PID 0x2341:0x0266 to HidDeviceParser's supported-devices table, routing it to IcomRC28Parser. Twelve lines, no behaviour change for any existing device.

Motivation

The aether-pad reference implementation (github.com/nigelfenton/aether-pad) is a small Arduino Giga R1-based RC-28 emulator I've been using as a hardware-in-the-loop test bench for the RC-28 wire protocol. End-to-end validation against PR #2870's report-layout fix was done with this bench on 2026-05-20 (parse-side DIAG trace + frequency display tracking each detent).

The bench has been carrying this alias as a local-only patch since then. Filing it upstream so the bench works against unmodified main going forward, and so other folks who want to build community RC-28-compatible hardware on Arduino have a path in.

Why an alias rather than spoofing Icom's VID/PID

The Giga R1's mbed PluggableUSB stack hardcodes the device-level VID/PID at the core level — the emulator firmware can't claim 0x0C26:0x001E even if it wanted to. And spoofing FlexRadio-class hardware identifiers in community firmware is something I'd rather not do for the obvious driver-INF / ethical reasons. Routing the Arduino-default composite VID/PID to the same parser is the clean way in.

The wire protocol is identical to the real RC-28 — same 32-byte report layout, same seq/dir/button encoding documented above IcomRC28Parser. The alias just gives community hardware a supported route into the existing parser.

Test plan

  • Compiles clean (incremental Windows build, 698/698 targets)
  • Binary launches + stays alive, closes cleanly
  • Live wire validation already on file from the 2026-05-20 bench (with this exact patch on a feature branch); aether-pad firmware on nigelfenton/aether-pad@feat/rc28-test-mode drives the protocol bytes

Out of scope

The aether-pad also has an AetherControl-style FlexControl-protocol persona in flight — that needs separate plumbing on the AE side (FlexControlManager::detectPort()'s VID/PID filter is the blocker there). I'll file that as its own PR once the firmware-side persona work lands. This PR is just the HID alias.

🤖 Generated with Claude Code

Adds the Arduino composite VID/PID 0x2341:0x0266 to HidDeviceParser's
supported-devices table, routing it to IcomRC28Parser. This lets the
aether-pad reference implementation (github.com/nigelfenton/aether-pad)
plug in as a hardware-in-the-loop test bench for the RC-28 wire
protocol — most notably PR aethersdr#2870's report-layout fix, which was
validated end-to-end against this bench on 2026-05-20.

Why an alias rather than spoofing Icom's VID/PID:

The Giga R1's mbed PluggableUSB stack hardcodes the device-level
VID/PID at the core level, so the emulator firmware can't claim
0x0C26:0x001E even if it wanted to (and we wouldn't want to anyway —
driver INF pain on Windows + the obvious ethical issue). Routing the
Arduino-default composite VID/PID to the same parser is the clean way
in.

The wire protocol is identical to the real RC-28 — same 32-byte report
layout, same seq/dir/button encoding documented above IcomRC28Parser.
The alias just gives community hardware a supported route into the
existing parser.

Test plan:
  - Builds clean (incremental, 698/698 targets).
  - Binary launches + stays alive 6 s, closes cleanly.
  - Live RC-28 wire validation already on file from the 2026-05-20
    bench session (with the alias on a feature branch); see
    aethersdr-20260520-220037.log in that bench's evidence trail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nigelfenton nigelfenton requested a review from a team as a code owner May 25, 2026 23:20

@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 Nigel — this is a clean, well-scoped change.

What I checked:

  • Scope matches description. Two entries in kSupportedDevices and a matching factory branch in HidDeviceParser::create(). Existing devices route identically (the new branch is below all the originals); no behavior change for FlexControl, Griffin, or Contour paths.
  • Parser reuse is sound. IcomRC28Parser::parse does its own length/dispatch handling (the len >= 33 Windows/Linux vs macOS branch and the len < 5 early return at HidDeviceParser.cpp:41), so any device misclaimed as RC-28 would safely produce no events rather than crash. Length-based report-ID detection means the emulator just needs to match the wire layout, which the PR body confirms.
  • No RAII / null-pointer / resource concernsstd::make_unique matches the surrounding pattern.
  • Out-of-scope work correctly held back. The FlexControl persona path is appropriately deferred to #3111; this PR is HID-only.

The comment block is verbose but it captures the why (Arduino VID/PID being hardcoded by mbed PluggableUSB, and the deliberate decision not to spoof FlexRadio identifiers), which is the right call for a community-hardware alias that future maintainers will need to understand.

Nothing blocking from me. Nice work on the upstream PR after carrying the local patch — and good documentation in the body of the bench setup and the ethical reasoning around VID/PID spoofing.


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

@ten9876

ten9876 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Claude here — really nice work on this, Nigel. Solid PR shape: pattern-consistent with the existing 4 entries, well-commented, hardware-validated against #2870, and a thoughtful explanation of why you're aliasing rather than spoofing Icom's VID/PID.

One safety addition before we land it, since the collision risk is more concrete than I'd want to ship:

The concern: 0x2341:0x0266 is Arduino LLC's official VID + the composite-device PID used by the Giga R1 (and other boards with the same composite descriptor). ANY Giga running ANY HID firmware — Arduino's own MIDI examples, third-party game-controller projects, a hobbyist macro pad — would get routed to IcomRC28Parser the moment a user plugs both that board AND AetherSDR into the same machine. Best-case the parser sees non-32-byte reports and silently no-ops; worst-case the user gets phantom RC-28 events from arbitrary HID reports that happen to look like the right shape.

Proposed fix — match on a USB string descriptor too:

We'd extend HidDeviceId in src/core/HidDeviceParser.h:18-22 to carry an optional product-string filter:

struct HidDeviceId {
    uint16_t vid;
    uint16_t pid;
    const char* name;
    const wchar_t* productStringMatch = nullptr;  // nullptr = match VID/PID only
};

Then in HidEncoderManager::detectDevice() the iteration would check the product_string field hidapi already exposes via hid_device_info:

auto* info = hid_enumerate(devices[i].vid, devices[i].pid);
for (auto* cur = info; cur; cur = cur->next) {
    if (devices[i].productStringMatch &&
        wcscmp(cur->product_string, devices[i].productStringMatch) != 0)
        continue;  // VID/PID hit but product string doesn't match — skip
    // ...match...
}

Existing four entries (real RC-28, PowerMate, ShuttleXpress, ShuttlePro v2) leave productStringMatch as the default nullptr — zero behaviour change. Only the new aether-pad entry would set it:

{0x2341, 0x0266, "AetherPad RC-28 emulator", L"AetherPad RC-28"},

Firmware side — set the iProduct string:

On the Giga's mbed PluggableUSB stack, override USBDevice::stringIproductDesc() to return a UTF-16LE descriptor with your chosen product name. Suggestion: "AetherPad RC-28" (matches what we'd hardcode on the AE side). Roughly:

const uint8_t* AetherPadUSB::stringIproductDesc() {
    static const uint8_t desc[] = {
        2 + 2*15,                       // bLength: 2 header + 15 chars × 2 bytes
        STRING_DESCRIPTOR,
        'A',0,'e',0,'t',0,'h',0,'e',0,'r',0,'P',0,'a',0,'d',0,' ',0,
        'R',0,'C',0,'-',0,'2',0,'8',0
    };
    return desc;
}

(Or whatever the equivalent shape is in your mbed version — you know that stack better than I do.)

Happy to do the AetherSDR-side patch myself if you want to focus on the firmware string-descriptor update — easier split than having you flip back and forth between repos. Just let me know the exact iProduct string you end up shipping and we'll hardcode that on the AE side, then merge both PRs in lockstep.

73, Jeremy KK7GWY & Claude (AI dev partner)

@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 Nigel, dedicated VID/PID is the right approach for a new class of devices.

@jensenpat jensenpat merged commit 4ee99f7 into aethersdr:main May 26, 2026
5 checks passed
@nigelfenton nigelfenton deleted the feat/aetherpad-rc28-alias branch May 27, 2026 17:55
ten9876 added a commit that referenced this pull request May 28, 2026
…urator (#3238)

First-platform delivery of #3232 (cross-platform BT-HID encoder
support).
Linux side complete; Windows/macOS land in follow-up PRs.

Backend (EvdevEncoderManager, Linux-only):
- Scans /dev/input/event* for "Ulanzi Dial Keyboard" sub-device
- Opens with EVIOCGRAB to claim it exclusively so the dial's media-key
  emissions (KEY_PREVIOUSSONG/NEXTSONG/PLAYPAUSE/MUTE/Ctrl+chords) don't
  leak to the focused window
- Event-driven via QSocketNotifier; no polling
- Chord assembly state machine decodes Ctrl+key + compound chord events
- Hot-plug via QFileSystemWatcher on /dev/input/

UI (UlanziDialMapperDialog, fixed 640x640):
- Product image of the dial in the centre, action-combo "pills"
  positioned around it at each physical control
- Each pill is a QComboBox listing every registered ShortcutManager
  action + every discrete-type MIDI param (shortcut:<id> / midi:<id>
  tagged so MainWindow dispatcher can route correctly)
- Signature-to-pill mapping is firmware-property (hardcoded in
  kPillSpecs); the function assigned to each pill is user-configurable
  via the combo and persists to AppSettings under
  UlanziDial/action/<pillId>

MainWindow integration:
- Instantiates the manager on the existing ExtControllers thread
- Rotary tuneSteps coalesces into the active slice tune step
  (same pattern as HidEncoderManager from #3171)
- buttonEvent dispatcher parses the prefix and invokes the matching
  ShortcutManager handler or MIDI param setter; falls back to the
  per-pill defaultAction so first-launch bindings work without the
  user ever opening the mapper

Files:
- src/core/EvdevEncoderManager.{h,cpp}   - new, Linux-only
- src/gui/UlanziDialMapperDialog.{h,cpp} - new, Linux-only
- resources/images/ulanzi-dial.png       - product image
- src/gui/MainWindow.{h,cpp}             - wiring + menu entry
- CMakeLists.txt - gate new sources on UNIX AND NOT APPLE

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
aethersdr#3171)

## Summary

Adds the Arduino composite VID/PID `0x2341:0x0266` to
`HidDeviceParser`'s supported-devices table, routing it to
`IcomRC28Parser`. Twelve lines, no behaviour change for any existing
device.

## Motivation

The aether-pad reference implementation
([github.com/nigelfenton/aether-pad](https://github.com/nigelfenton/aether-pad))
is a small Arduino Giga R1-based RC-28 emulator I've been using as a
hardware-in-the-loop test bench for the RC-28 wire protocol. End-to-end
validation against PR aethersdr#2870's report-layout fix was done with this bench
on 2026-05-20 (parse-side DIAG trace + frequency display tracking each
detent).

The bench has been carrying this alias as a local-only patch since then.
Filing it upstream so the bench works against unmodified `main` going
forward, and so other folks who want to build community RC-28-compatible
hardware on Arduino have a path in.

## Why an alias rather than spoofing Icom's VID/PID

The Giga R1's mbed PluggableUSB stack hardcodes the device-level VID/PID
at the core level — the emulator firmware can't claim `0x0C26:0x001E`
even if it wanted to. And spoofing FlexRadio-class hardware identifiers
in community firmware is something I'd rather not do for the obvious
driver-INF / ethical reasons. Routing the Arduino-default composite
VID/PID to the same parser is the clean way in.

The wire protocol is identical to the real RC-28 — same 32-byte report
layout, same seq/dir/button encoding documented above `IcomRC28Parser`.
The alias just gives community hardware a supported route into the
existing parser.

## Test plan
- [x] Compiles clean (incremental Windows build, 698/698 targets)
- [x] Binary launches + stays alive, closes cleanly
- [x] Live wire validation already on file from the 2026-05-20 bench
(with this exact patch on a feature branch); aether-pad firmware on
[nigelfenton/aether-pad@feat/rc28-test-mode](https://github.com/nigelfenton/aether-pad/tree/feat/rc28-test-mode)
drives the protocol bytes

## Out of scope

The aether-pad also has an AetherControl-style FlexControl-protocol
persona in flight — that needs separate plumbing on the AE side
(`FlexControlManager::detectPort()`'s VID/PID filter is the blocker
there). I'll file that as its own PR once the firmware-side persona work
lands. This PR is just the HID alias.

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…urator (aethersdr#3238)

First-platform delivery of aethersdr#3232 (cross-platform BT-HID encoder
support).
Linux side complete; Windows/macOS land in follow-up PRs.

Backend (EvdevEncoderManager, Linux-only):
- Scans /dev/input/event* for "Ulanzi Dial Keyboard" sub-device
- Opens with EVIOCGRAB to claim it exclusively so the dial's media-key
  emissions (KEY_PREVIOUSSONG/NEXTSONG/PLAYPAUSE/MUTE/Ctrl+chords) don't
  leak to the focused window
- Event-driven via QSocketNotifier; no polling
- Chord assembly state machine decodes Ctrl+key + compound chord events
- Hot-plug via QFileSystemWatcher on /dev/input/

UI (UlanziDialMapperDialog, fixed 640x640):
- Product image of the dial in the centre, action-combo "pills"
  positioned around it at each physical control
- Each pill is a QComboBox listing every registered ShortcutManager
  action + every discrete-type MIDI param (shortcut:<id> / midi:<id>
  tagged so MainWindow dispatcher can route correctly)
- Signature-to-pill mapping is firmware-property (hardcoded in
  kPillSpecs); the function assigned to each pill is user-configurable
  via the combo and persists to AppSettings under
  UlanziDial/action/<pillId>

MainWindow integration:
- Instantiates the manager on the existing ExtControllers thread
- Rotary tuneSteps coalesces into the active slice tune step
  (same pattern as HidEncoderManager from aethersdr#3171)
- buttonEvent dispatcher parses the prefix and invokes the matching
  ShortcutManager handler or MIDI param setter; falls back to the
  per-pill defaultAction so first-launch bindings work without the
  user ever opening the mapper

Files:
- src/core/EvdevEncoderManager.{h,cpp}   - new, Linux-only
- src/gui/UlanziDialMapperDialog.{h,cpp} - new, Linux-only
- resources/images/ulanzi-dial.png       - product image
- src/gui/MainWindow.{h,cpp}             - wiring + menu entry
- CMakeLists.txt - gate new sources on UNIX AND NOT APPLE

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <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.

3 participants