Skip to content

Add support for Ulanzi Stream Controller D200 - #14

Open
bksenhuck wants to merge 1 commit into
StreamController:masterfrom
bksenhuck:add-ulanzi-d200-support
Open

Add support for Ulanzi Stream Controller D200#14
bksenhuck wants to merge 1 commit into
StreamController:masterfrom
bksenhuck:add-ulanzi-d200-support

Conversation

@bksenhuck

Copy link
Copy Markdown

Summary

Adds native support for the Ulanzi Stream Controller D200 (USB VID:PID 2207:0019), following the same integration pattern already used for the Mirabox Stream Dock 293S (Mirabox293S.py).

⚠️ Tested on Linux only. Everything in this PR was built and verified on Linux (Arch-based), against real D200 hardware, through StreamController's LibUSBHIDAPI transport.

The device's protocol isn't documented anywhere by the vendor. It was reverse-engineered by redphx in strmdck and homedeck (both MIT-licensed); this PR adapts that protocol knowledge into a StreamDeck subclass for this library, plus a few hardware-specific workarounds discovered while integrating it with StreamController (full details in the UlanziD200 class docstring).

What changed

  • Devices/UlanziD200.py (new): the device implementation. Handles the D200's 14-key grid, button-press reporting, brightness, and multi-key image uploads. Documents (in the class docstring) several non-obvious hardware quirks this works around:
    • the device only accepts a button-image upload for a short window after connecting, so a background thread periodically "primes" the connection until a real upload arrives;
    • icons appear to be cached by filename rather than content on the device itself, so every upload uses a fresh filename;
    • open() is made idempotent, since StreamController (the primary consumer of this library) calls it multiple times during its own startup with no guard of its own.
  • DeviceManager.py: registers the D200 in _default_factory(), alongside Elgato and Mirabox. Also extends the products list format to optionally filter by USB interface number, since the D200 exposes two HID interfaces under the same VID/PID (a keyboard-emulation interface, unrelated to the deck protocol) and only one may be wrapped as a StreamDeck.
  • ProductIDs.py: adds USB_VID_ULANZI / USB_PID_ULANZI_D200.
  • Transport/LibUSBHIDAPI.py: exposes hidapi's interface_number field (previously read but discarded), which the interface filtering above depends on.

Compatibility

Status
Hardware tested Ulanzi Stream Controller D200 only
OS tested Linux (Arch-based)
Transport tested LibUSBHIDAPI (the libusb backend)
Consumer app tested StreamController, real hardware, extended session
Other Ulanzi/D200 variants (e.g. D200H) Not tested - unknown whether the protocol matches
Existing devices (Elgato, Mirabox) Unaffected - changes are additive; the only shared code path (DeviceManager._default_factory) preserves prior behavior for entries without an interface-number filter

Tested against real hardware with StreamController, on Linux.

Adds a UlanziD200 Device class (USB VID:PID 2207:0019) alongside the
existing Elgato and Mirabox support, following the same pattern as the
Mirabox293S integration.

Protocol notes (see docstring in UlanziD200.py for details):
- 14 physical keys in a 5x3 grid; the 15th grid slot has no LCD (it's
  the small stats/clock window), modeled as an inert phantom key so the
  library's KEY_ROWS x KEY_COLS grid math stays rectangular.
- The device only accepts a button image upload for a short window
  after opening; a periodic "priming" upload keeps that window open
  until the caller's first real upload arrives.
- Button image uploads for multiple keys are combined into a single
  multi-packet upload, matching how other D200 clients (e.g. homedeck)
  operate; the device merges an upload into what's currently displayed
  rather than replacing it wholesale.
- open() is made idempotent per connection, since callers such as
  StreamController call it more than once during their own startup
  with no is_open() guard, and doing that repeatedly within
  milliseconds left the device unable to render images for the rest of
  the session.

Also exposes hidapi's interface_number on LibUSBHIDAPI enumeration
results (previously unused) and lets DeviceManager._default_factory()
filter by it, since the D200 exposes a second, unrelated
keyboard-emulation HID interface under the same VID:PID that must not
be wrapped as a StreamDeck.

Protocol reverse-engineered by redphx:
https://github.com/redphx/strmdck
https://github.com/redphx/homedeck
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.

1 participant