Skip to content

feat(hid): Elgato StreamDeck+ support — encoders, LCD buttons, touchscreen labels (#1510)#3236

Merged
ten9876 merged 11 commits into
aethersdr:mainfrom
motoham88:feat/add-streamdeck-rit-xit
May 28, 2026
Merged

feat(hid): Elgato StreamDeck+ support — encoders, LCD buttons, touchscreen labels (#1510)#3236
ten9876 merged 11 commits into
aethersdr:mainfrom
motoham88:feat/add-streamdeck-rit-xit

Conversation

@motoham88

Copy link
Copy Markdown
Contributor

Summary

Implements full Elgato StreamDeck+ (VID 0x0FD9, PID 0x0084) support, closing #1510.

  • 4 encoder dials — each independently configurable to any wheel action (Tune, RIT, XIT, Volume, AGC-T, APF, CW Speed, RF Power, Headphone Volume)
  • Encoder push buttons — each independently configurable; defaults: Enc 1 = Cycle Tuning Step, Enc 2 = Toggle RIT, Enc 3 = Toggle XIT
  • 8 LCD buttons — each configurable to 19 actions (MOX, Tune, RIT/XIT toggle+clear, Step Up/Down, Mute, Lock, APF, AGC, Band/Segment Zoom, Next/Prev Slice, Volume ±5, Split); button images update in real-time to show the assigned label
  • Touchscreen strip — 800×100 JPEG above the dials showing turn action (top) and push action + live ON/OFF state (bottom); RIT/XIT sections go green when active
  • Settings UI — three group boxes in Settings → Radio → Serial tab ordered to match the physical device layout (LCD buttons → dial turn → dial push); all changes push to device immediately

udev rule required for non-root access

SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0084", MODE="0666"

Protocol notes

  • Input: 14-byte reports, report ID 0x01 at buf[0], event type at buf[1]; dial turns type 0x03/sub 0x01, pushes 0x03/0x00, LCD keys 0x00
  • Key image output: 1024-byte hid_write, command 0x07, 8-byte header, 120×120 JPEG
  • Touchscreen output: 1024-byte hid_write, command 0x0c, 16-byte header with x/y/w/h, 800×100 JPEG
  • Protocol verified against python-elgato-streamdeck v0.9.8

Compatibility

Existing single-encoder devices (Icom RC-28, AetherPad emulator, Griffin PowerMate, Contour ShuttleXpress/Pro v2) are unaffected — encoderIndex was added as the last field of HidEvent to preserve all existing 4-field aggregate initializers.

Test plan

  • StreamDeck+ dials tune frequency, RIT, XIT, volume by default
  • Encoder 1 push cycles tuning step; Enc 2/3 push toggles RIT/XIT
  • Touchscreen strip shows correct labels; RIT/XIT sections change color with state
  • LCD buttons execute configured actions and display matching labels
  • Settings combos in Serial tab update device labels immediately on change
  • RC-28 / PowerMate / ShuttleXpress still work normally (no regression)
  • Build succeeds with and without HAVE_HIDAPI

🤖 Generated with Claude Code

motoham88 and others added 10 commits May 27, 2026 20:00
…rsdr#1510)

Add Elgato StreamDeck+ (VID 0x0FD9 / PID 0x0084) as a supported HID
encoder device with 4 independently configurable encoder dials.

- HidDeviceParser: add encoderIndex field to HidEvent (placed last to
  preserve existing 4-field aggregate inits); add virtual encoderCount();
  add StreamDeckPlusParser handling rotation (type 0x02), encoder button
  (type 0x03), and LCD key (type 0x00) reports; uses len to discriminate
  65-byte (Linux/Windows, report ID prepended) from 64-byte (macOS) reads,
  same pattern as IcomRC28Parser
- HidEncoderManager: tuneSteps(int) → tuneSteps(int encoderIndex, int steps)
- MainWindow: remove per-device HID coalesce timer; route each encoder
  through AppSettings("HidEncoderAction<N>") → applyFlexControlWheelAction;
  defaults: enc0=Frequency, enc1=RIT, enc2=XIT, enc3=Volume
- RadioSetupDialog: add "HID Encoder / StreamDeck+ Encoders" group to
  Serial tab with 4 action combos (WheelFrequency/RIT/XIT/Volume/etc.)

Single-encoder devices (RC-28, PowerMate, ShuttleXpress) continue to
work unchanged — they emit encoderIndex=0 and read HidEncoderAction0.

Principle VII.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Initial implementation assumed 64-byte reports with type byte at buf[0],
matching generic vendor HID guesses. Verified against python-elgato-streamdeck
v0.9.8 source against the actual device:

- reportSize: 65 → 14 (device advertises 512-byte descriptor but only 14
  bytes carry live event data; the Python reference library reads exactly 14)
- Report ID 0x01 is always at buf[0] on all platforms — no len-based
  discrimination needed
- Event type is at buf[1] (not buf[0])
- Dial events are type 0x03 with sub-type at buf[4]: 0x01=turn, 0x00=push
  (not separate type codes 0x02/0x03 as originally guessed)
- Dial values at buf[5..8], key states at buf[4..11]

Tested on Elgato StreamDeck+ firmware j2.0.3.7 on Fedora 44.

Principle XI (Evidence Over Assertion).

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

Encoder 0 push cycles the tuning step size (StepCycle), encoder 1 push
toggles RIT, encoder 2 push toggles XIT by default. All four are
user-configurable via a new "StreamDeck+ Encoder Push Actions" group box
in Settings → Radio → Serial tab. Uses HidEncoderPushAction{0-3} keys
in AppSettings; release events are suppressed so each press fires exactly
once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds real-time label rendering on all 8 StreamDeck+ LCD keys.
Keys 1-4 (top row) show each encoder's turn action (TUNE/RIT/XIT/VOL).
Keys 5-8 (bottom row) show each encoder's push action; RIT and XIT keys
change background color (green=ON, dark=OFF) and show ON/OFF text as the
active slice state changes.

Images are rendered via QPainter to 120x120 QImage, flipped 180° per
StreamDeck+ KEY_FLIP spec, JPEG-encoded, and written over HID feature
reports on the ext-ctrl thread via hid_send_feature_report.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e call

hid_send_feature_report uses a HIDIOCSFEATURE ioctl on Linux which the
StreamDeck+ firmware doesn't support for image writes, corrupting device
state and breaking hid_read. Switch to hid_write matching the Python
reference library (transport.write → hid_write).

Also batches all 8 key image writes into a single QueuedConnection
invocation (setKeyImages) instead of 8 separate queued calls to avoid
starving the 5ms poll timer on the ext-ctrl thread.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
StreamDeck+ firmware displays the image as-sent; the KEY_FLIP=(True,True)
in the Python library applies at the PIL render stage, not on the wire.
Removing mirrored(true,true) corrects the upside-down labels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the per-LCD-key labels with a single 800x100 JPEG written to
the touchscreen strip. Four 200x100 sections, one per encoder: top half
shows the turn action (TUNE/RIT/XIT/VOL), bottom half shows the push
action label and ON/OFF state. RIT/XIT sections go green when active.
LCD keys are blanked. Protocol uses command 0x0c with a 16-byte header,
verified against python-elgato-streamdeck StreamDeckPlus.set_touchscreen_image().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "StreamDeck+ LCD Button Actions" group to Settings → Radio →
Serial tab with a dropdown for each of the 8 LCD keys. Supports 19
actions (MOX, Tune, RIT/XIT toggle+clear, Step Up/Down, Mute, Lock,
APF, AGC, Band/Segment Zoom, Next/Prev Slice, Volume ±5, Split).
Settings stored as HidKeyAction0-7 in AppSettings.

Key images are rendered with the action label and a color-coded
background (red=TX, green=RIT/XIT, purple=volume, etc.) so buttons are
self-labeling on the device. Labels update whenever refreshStreamDeckLabels
is called (on connect and active-slice change).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
serialSettingsChanged was not wired to refreshStreamDeckLabels, so LCD
key labels only updated on device connect or slice change. Now any combo
change in the Serial tab pushes the new images to the device immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move StreamDeck+ LCD Button Actions above the encoder dial/push groups
so the UI order (LCD buttons → dial turn → dial push) matches the
physical device layout top to bottom.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@motoham88 motoham88 requested a review from a team as a code owner May 28, 2026 03:09
@aethersdr-agent

Copy link
Copy Markdown
Contributor

Hi @motoham88 — thanks for putting this together, the StreamDeck+ work is a great addition and the HID parser comments are exceptionally clear. 👏

The CI failure is Windows-only (check-windows → Build step). check-macos and check-paths both went green on the same commit, so it's an MSVC-specific compile issue in the new code rather than anything wrong with the feature itself or with CI infrastructure.

I can't pull the raw build log from here, but I can see two highly likely culprits in src/core/HidEncoderManager.cpp based on the diff:

1. std::min without <algorithm> (and without NOMINMAX guard) — most likely root cause

The new setKeyImage body uses std::min(PAYLOAD_SIZE, totalBytes - offset), but the file only adds #include <cstring> — not #include <algorithm>. macOS/libc++ happens to pull <algorithm> in transitively through Qt headers, but MSVC's STL does not.

There's also a second, more insidious MSVC trap: if any Qt header on the include chain ends up pulling <windows.h> (which defines min/max as macros), the min macro will eat std::min and produce a stream of misleading errors like '(': illegal token on right side of '::'. This codebase already defends against this elsewhere — e.g. src/core/SerialPortController.cpp:12-14:

#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>

Fix: add at the top of HidEncoderManager.cpp:

#include <algorithm>   // std::min

If you still hit min/max macro errors after that, wrap the call as (std::min)(PAYLOAD_SIZE, totalBytes - offset) — the extra parens disable macro expansion — or #define NOMINMAX before any Qt include.

2. Possibly: setTouchscreenImage declared but not defined

The header declares setTouchscreenImage(...) (HidEncoderManager.h), but I don't see its definition in the visible portion of the .cpp patch. If it's truly missing it would normally fail link on both platforms — so most likely it's just below the patch fold and this is a non-issue, but worth a sanity check on your end (grep -n setTouchscreenImage src/core/HidEncoderManager.cpp).

Verifying

The Build log on the failed job will name the file and column of the first error:
https://github.com/aethersdr/AetherSDR/actions/runs/26552233088/job/78241568970

If the first error sits in HidEncoderManager.cpp and mentions std::min, 'min': not a member of 'std', or illegal token on right side of '::', #1 above is your fix.

Unrelated nice-to-have

The signal change tuneSteps(int)tuneSteps(int encoderIndex, int steps) is correctly handled in MainWindow.cpp (the only HidEncoderManager::tuneSteps connection in the tree). Other tuneSteps(int) signals on FlexControlManager, EvdevEncoderManager, UlanziDialBackend, etc. are independent and unaffected, so no further fan-out needed.

Genuinely lovely PR — the protocol citations against python-elgato-streamdeck and the contiguous kSupportedDevices comment block make this very easy to review. Once Windows is green this should be in good shape. 🎛️


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

MSVC's STL does not pull <algorithm> transitively through Qt headers,
causing std::min to be unresolved in setKeyImage and setTouchscreenImage
on Windows builds.

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

@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 for the very thorough work here, @motoham88 — the device-protocol notes in the header are great, and the additive encoderIndex placement preserving the existing 4-field aggregate inits for RC-28/PowerMate/ShuttleXpress is a nice touch. Files all fall inside the stated scope, AppSettings is used correctly throughout, and routing through applyFlexControlWheelAction cleanly inherits the existing coalescer (the old m_hidCoalesceTimer removal in onSliceAdded checks out — m_flexCoalesceTimer now covers HID-driven frequency tuning too).

A few things worth a look before merge:

1. StreamDeckPlusParser::parse drops simultaneous events (src/core/HidDeviceParser.cpp:209 and :241). In both the encoder-push path and the LCD-key path, m_prevEncBtns/m_prevKeys is overwritten with newState before iterating to find the first changed bit, then only the first changed bit is emitted. If two keys or two encoder pushes change in the same HID report, the second event is permanently lost — the next poll will see no change. In practice this is rare, but it's silently lossy. Easiest fix is to clear only the bit you're consuming from newState (i.e. mask the just-reported bit out of m_prevKeys/m_prevEncBtns) so the next poll() iteration re-enters and picks up the next bit, or have parse() return a small queue.

2. hid_write return values are ignored in setKeyImage and setTouchscreenImage (src/core/HidEncoderManager.cpp:182,232). If the device is unplugged mid-write, the loop will spin through all 1024-byte packets writing into a dead handle without any signal back. Even a qWarning on a negative return inside the loop (and breaking out) would help operators correlate "deck went blank" with logs, and could let HidEncoderManager close the handle so hotplugCheck reopens cleanly.

3. Minor thread-safety nit: refreshStreamDeckLabels() runs on the main thread and reads m_hidEncoder->isOpen() / isStreamDeckPlus(), which look at m_device / m_openVid / m_openPid mutated on m_extCtrlThread in poll()/hotplugCheck(). The actual image writes are correctly marshalled via QMetaObject::invokeMethod(..., Qt::QueuedConnection) (good), but the gate read isn't synchronized. Benign on common platforms but technically a data race — std::atomic<hid_device*> or making the gate check inside the queued lambda would tighten it.

Otherwise this looks solid. Test plan in the PR description is the right shape; the RC-28/PowerMate regression check is the one I'd most want to see confirmed before merge given the tuneSteps(int) → tuneSteps(int,int) signal-signature change.


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

@ten9876 ten9876 merged commit d863608 into aethersdr:main May 28, 2026
6 checks passed
@ten9876

ten9876 commented May 28, 2026

Copy link
Copy Markdown
Collaborator

🎉 Welcome to the AetherSDR contributor list, @motoham88 — and congratulations on landing your first PR! Closing out a long-standing tracking issue (#1510 has been open since the early days) with full StreamDeck+ support is a fantastic way to start.

The device-protocol documentation in your header is especially nice work — future contributors touching the HID parser will thank you for the layout notes and command-code references.

We've filed #3248 to track three robustness follow-ups surfaced during review (lost-events on simultaneous press, hid_write hot-unplug handling, and a minor thread-safety nit on the gate read). None are blockers, all are good-first-issue territory if you want to keep going. Otherwise we'll get to them in a polish pass.

Thanks for the great contribution. Looking forward to seeing what you ship next.

73, Jeremy KK7GWY & Claude (AI dev partner)

ten9876 added a commit that referenced this pull request May 28, 2026
…#3250)

Closes #3248.

Implements the three small robustness fixes flagged during PR #3236's
review and tracked in #3248. Each is independent and scoped to one file.

## 1. \`StreamDeckPlusParser::parse\` — don't drop simultaneous events

Both the encoder-push and LCD-key paths overwrote \`m_prevEncBtns\` /
\`m_prevKeys\` with \`newState\` *before* the loop that finds the first
changed bit, so any second-changed-bit got silently dropped (the next
\`poll()\` saw no remaining change). Switch to one-bit-at-a-time commit:

\`\`\`cpp
m_prevKeys ^= (1u << i);  // consume just this bit
return {.button = i + 1, ...};
\`\`\`

Now if two keys change in the same HID report, the first poll iteration
emits one and the next poll iteration emits the other. Verified by
hand-trace: press both → emit press 1 → emit press 2 → release both →
emit release 1 → emit release 2. ✅

## 2. \`HidEncoderManager\` — check \`hid_write\` return on image writes

\`setKeyImage\` and \`setTouchscreenImage\` looped through 1024-byte
feature reports ignoring the return. If the device unplugs mid-stream
the loop spins through the rest writing into a dead handle with no log
signal. Now:

\`\`\`cpp
const int written = hid_write(m_device, pkt, PACKET_SIZE);
if (written < 0) {
qCWarning(lcDevices) << \"HidEncoderManager::setKeyImage: hid_write
failed\"
                         << \"key=\" << key << \"page=\" << pageNumber
<< \"— device disconnected? Will retry on hotplug.\";
    return;
}
\`\`\`

The next \`poll()\`'s \`hid_read() < 0\` triggers \`close()\` + hotplug
reopen, which is now correlatable in logs with the user-visible \"deck
went blank\".

## 3. \`HidEncoderManager\` — atomic gate-check members

\`m_device\` / \`m_openVid\` / \`m_openPid\` are read from the main
thread (refreshStreamDeckLabels at MainWindow.cpp:6778, status snapshot
at :7061+) and written from \`m_extCtrlThread\` (open / close /
hotplugCheck). Wrap in \`std::atomic\` so cross-thread reads are
well-defined. Relaxed memory order is sufficient because callers treat
the result as a hint and the real gate is re-checked inside the queued
slot.

\`std::atomic<hid_device*>\` and \`std::atomic<uint16_t>\` have implicit
conversion / assignment operators, so no .cpp call-site needs explicit
\`.load()\` / \`.store()\` — the source diff is purely the header change
plus the existing reads continuing to compile under the new types.

## Test plan

- [x] Build clean on Linux (627/627 targets, no new warnings)
- [ ] Confirm check-windows and check-macos pass (now always run per
#3244)
- [ ] Hand-trace the lost-events fix once more
- [ ] Optional: hotplug a StreamDeck+ during a label refresh and watch
for the new \`hid_write failed\` warning

Diff stats: +61 / -25 across 3 files.

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@motoham88 motoham88 deleted the feat/add-streamdeck-rit-xit branch May 28, 2026 23:55
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…creen labels (aethersdr#1510) (aethersdr#3236)

## Summary

Implements full Elgato StreamDeck+ (VID 0x0FD9, PID 0x0084) support,
closing aethersdr#1510.

- **4 encoder dials** — each independently configurable to any wheel
action (Tune, RIT, XIT, Volume, AGC-T, APF, CW Speed, RF Power,
Headphone Volume)
- **Encoder push buttons** — each independently configurable; defaults:
Enc 1 = Cycle Tuning Step, Enc 2 = Toggle RIT, Enc 3 = Toggle XIT
- **8 LCD buttons** — each configurable to 19 actions (MOX, Tune,
RIT/XIT toggle+clear, Step Up/Down, Mute, Lock, APF, AGC, Band/Segment
Zoom, Next/Prev Slice, Volume ±5, Split); button images update in
real-time to show the assigned label
- **Touchscreen strip** — 800×100 JPEG above the dials showing turn
action (top) and push action + live ON/OFF state (bottom); RIT/XIT
sections go green when active
- **Settings UI** — three group boxes in Settings → Radio → Serial tab
ordered to match the physical device layout (LCD buttons → dial turn →
dial push); all changes push to device immediately

### udev rule required for non-root access
```
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0fd9", ATTRS{idProduct}=="0084", MODE="0666"
```

### Protocol notes
- Input: 14-byte reports, report ID `0x01` at `buf[0]`, event type at
`buf[1]`; dial turns type `0x03`/sub `0x01`, pushes `0x03`/`0x00`, LCD
keys `0x00`
- Key image output: 1024-byte `hid_write`, command `0x07`, 8-byte
header, 120×120 JPEG
- Touchscreen output: 1024-byte `hid_write`, command `0x0c`, 16-byte
header with x/y/w/h, 800×100 JPEG
- Protocol verified against python-elgato-streamdeck v0.9.8

### Compatibility
Existing single-encoder devices (Icom RC-28, AetherPad emulator, Griffin
PowerMate, Contour ShuttleXpress/Pro v2) are unaffected — `encoderIndex`
was added as the last field of `HidEvent` to preserve all existing
4-field aggregate initializers.

## Test plan
- [ ] StreamDeck+ dials tune frequency, RIT, XIT, volume by default
- [ ] Encoder 1 push cycles tuning step; Enc 2/3 push toggles RIT/XIT
- [ ] Touchscreen strip shows correct labels; RIT/XIT sections change
color with state
- [ ] LCD buttons execute configured actions and display matching labels
- [ ] Settings combos in Serial tab update device labels immediately on
change
- [ ] RC-28 / PowerMate / ShuttleXpress still work normally (no
regression)
- [ ] Build succeeds with and without `HAVE_HIDAPI`

🤖 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
aethersdr#3248) (aethersdr#3250)

Closes aethersdr#3248.

Implements the three small robustness fixes flagged during PR aethersdr#3236's
review and tracked in aethersdr#3248. Each is independent and scoped to one file.

## 1. \`StreamDeckPlusParser::parse\` — don't drop simultaneous events

Both the encoder-push and LCD-key paths overwrote \`m_prevEncBtns\` /
\`m_prevKeys\` with \`newState\` *before* the loop that finds the first
changed bit, so any second-changed-bit got silently dropped (the next
\`poll()\` saw no remaining change). Switch to one-bit-at-a-time commit:

\`\`\`cpp
m_prevKeys ^= (1u << i);  // consume just this bit
return {.button = i + 1, ...};
\`\`\`

Now if two keys change in the same HID report, the first poll iteration
emits one and the next poll iteration emits the other. Verified by
hand-trace: press both → emit press 1 → emit press 2 → release both →
emit release 1 → emit release 2. ✅

## 2. \`HidEncoderManager\` — check \`hid_write\` return on image writes

\`setKeyImage\` and \`setTouchscreenImage\` looped through 1024-byte
feature reports ignoring the return. If the device unplugs mid-stream
the loop spins through the rest writing into a dead handle with no log
signal. Now:

\`\`\`cpp
const int written = hid_write(m_device, pkt, PACKET_SIZE);
if (written < 0) {
qCWarning(lcDevices) << \"HidEncoderManager::setKeyImage: hid_write
failed\"
                         << \"key=\" << key << \"page=\" << pageNumber
<< \"— device disconnected? Will retry on hotplug.\";
    return;
}
\`\`\`

The next \`poll()\`'s \`hid_read() < 0\` triggers \`close()\` + hotplug
reopen, which is now correlatable in logs with the user-visible \"deck
went blank\".

## 3. \`HidEncoderManager\` — atomic gate-check members

\`m_device\` / \`m_openVid\` / \`m_openPid\` are read from the main
thread (refreshStreamDeckLabels at MainWindow.cpp:6778, status snapshot
at :7061+) and written from \`m_extCtrlThread\` (open / close /
hotplugCheck). Wrap in \`std::atomic\` so cross-thread reads are
well-defined. Relaxed memory order is sufficient because callers treat
the result as a hint and the real gate is re-checked inside the queued
slot.

\`std::atomic<hid_device*>\` and \`std::atomic<uint16_t>\` have implicit
conversion / assignment operators, so no .cpp call-site needs explicit
\`.load()\` / \`.store()\` — the source diff is purely the header change
plus the existing reads continuing to compile under the new types.

## Test plan

- [x] Build clean on Linux (627/627 targets, no new warnings)
- [ ] Confirm check-windows and check-macos pass (now always run per
aethersdr#3244)
- [ ] Hand-trace the lost-events fix once more
- [ ] Optional: hotplug a StreamDeck+ during a label refresh and watch
for the new \`hid_write failed\` warning

Diff stats: +61 / -25 across 3 files.

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

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.

2 participants