feat: impl From<StreamDeck> for AsyncStreamDeck#59
Open
kingb wants to merge 1 commit into
Open
Conversation
Member
|
This should be implemented using Rust's |
Allows opening a device via the synchronous API on a caller-chosen thread and then handing it to the async wrapper. This matters on macOS, where hidapi's IOHIDManager is bound to the run loop of the thread that created the HidApi: enumerate/open must happen on that thread, but AsyncStreamDeck::connect runs them via block_in_place on whatever tokio worker is current, which can crash (EXC_BREAKPOINT in CoreFoundation) when the device set changes, e.g. after sleep/wake. With this, an application can own the HidApi on a dedicated thread, open devices there with StreamDeck::connect, and still use the async API for everything else.
e14dfd0 to
132b7b7
Compare
kingb
added a commit
to kingb/OpenDeck
that referenced
this pull request
Jun 11, 2026
On macOS, hidapi's IOHIDManager is implicitly bound to the run loop of the thread that created the HidApi. Enumerating or opening devices from any other thread schedules IOKit run-loop sources on a foreign run loop, which traps in CoreFoundation (__CFCheckCFInfoPACSignature, EXC_BREAKPOINT). It fires most reliably ~right after sleep/wake, when the HID device set changes and the next 10s poll takes the IOHIDDeviceScheduleWithRunLoop -> CFRunLoopAddSource path. initialise_devices() shared one HidApi across arbitrary tokio worker threads, and elgato-streamdeck's async wrappers use block_in_place, which does NOT pin to a single OS thread (verified: it migrates workers even on a 1-worker runtime). So enumerate/connect ran on whatever worker the task landed on. Fix: create the HidApi once on a dedicated "opendeck-hid" OS thread and run every enumerate/open there via the synchronous elgato API (which never calls block_in_place). Opened StreamDecks are Send, so they're handed back over a channel and wrapped via From<StreamDeck> for AsyncStreamDeck for async use. Already-open devices' reads/writes are unchanged (they use per-device handles). The From impl is proposed upstream in OpenActionAPI/rust-elgato-streamdeck#59; until that lands in a release, [patch.crates-io] points elgato-streamdeck at a fork branch carrying that one impl on top of 0.12.1 (its AsyncStreamDeck fields are private, so it cannot be constructed from outside the crate).
kingb
added a commit
to kingb/OpenDeck
that referenced
this pull request
Jun 11, 2026
On macOS, hidapi's IOHIDManager is implicitly bound to the run loop of the thread that created the HidApi. Enumerating or opening devices from any other thread schedules IOKit run-loop sources on a foreign run loop, which traps in CoreFoundation (__CFCheckCFInfoPACSignature, EXC_BREAKPOINT). It fires most reliably ~right after sleep/wake, when the HID device set changes and the next 10s poll takes the IOHIDDeviceScheduleWithRunLoop -> CFRunLoopAddSource path. initialise_devices() shared one HidApi across arbitrary tokio worker threads, and elgato-streamdeck's async wrappers use block_in_place, which does NOT pin to a single OS thread (verified: it migrates workers even on a 1-worker runtime). So enumerate/connect ran on whatever worker the task landed on. Fix: create the HidApi once on a dedicated "opendeck-hid" OS thread and run every enumerate/open there via the synchronous elgato API (which never calls block_in_place). Opened StreamDecks are Send, so they're handed back over a channel and wrapped via From<StreamDeck> for AsyncStreamDeck for async use. Already-open devices' reads/writes are unchanged (they use per-device handles). The From impl is proposed upstream in OpenActionAPI/rust-elgato-streamdeck#59; until that lands in a release, [patch.crates-io] points elgato-streamdeck at a fork branch carrying that one impl on top of 0.12.1 (its AsyncStreamDeck fields are private, so it cannot be constructed from outside the crate).
Author
|
Done — reworked as |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Implements
From<StreamDeck> for AsyncStreamDeck, so an already-opened synchronousStreamDeckcan be wrapped into the async wrapper without going throughAsyncStreamDeck::connect.Why
On macOS, hidapi's
IOHIDManageris implicitly bound to the run loop of the thread that created theHidApi. Enumerating/opening devices from any other thread schedules IOKit sources on a foreign run loop, which can trap in CoreFoundation (EXC_BREAKPOINTin__CFCheckCFInfoPACSignatureviaCFRunLoopAddSource←IOHIDDeviceScheduleWithRunLoop) — most reliably right after sleep/wake when the device set changes.AsyncStreamDeck::connectrunsStreamDeck::connectviablock_in_placeon whichever tokio worker thread is current, so an async application has no way to control which OS thread performs the HID open. Empirically,block_in_placedoes not pin to a single OS thread even on a single-worker runtime.With this impl, an application can own the
HidApion one dedicated thread, enumerate and open devices there using the synchronous API, and hand the opened device (which isSend) to the async runtime for everything else.Context
Found while debugging a reproducible sleep/wake crash in OpenDeck on macOS 15 (Apple Silicon). Restructuring OpenDeck's device polling around a dedicated HID thread + this impl eliminated the crash.
🤖 Generated with Claude Code