Skip to content

Device

device

Generic, self-describing Stream Deck HID device.

A single :class:HidDevice class handles all supported Stream Deck models. Hardware capabilities are read at open time via the Get Unit Information feature report rather than being hardcoded per model.

HidDevice

A connected Stream Deck HID device.

This class wraps a single HID device handle and provides typed methods for all supported operations. Hardware capabilities are queried from the device itself at open time via Get Unit Information.

Parameters:

Name Type Description Default
info HidDeviceInfo

Enumeration info for the device.

required

Attributes:

Name Type Description
path bytes

OS-specific HID device path.

vendor_id int

USB vendor ID (always 0x0FD9 for Elgato).

product_id int

USB product ID.

Source code in src/deux/runtime/hid/device.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
class HidDevice:
    """A connected Stream Deck HID device.

    This class wraps a single HID device handle and provides typed methods
    for all supported operations.  Hardware capabilities are queried from
    the device itself at open time via ``Get Unit Information``.

    Parameters
    ----------
    info : HidDeviceInfo
        Enumeration info for the device.

    Attributes
    ----------
    path : bytes
        OS-specific HID device path.
    vendor_id : int
        USB vendor ID (always 0x0FD9 for Elgato).
    product_id : int
        USB product ID.
    """

    __slots__ = (
        "path",
        "vendor_id",
        "product_id",
        "_handle",
        "_unit_info",
        "_serial",
        "_firmware_version",
        "_rotation",
        "_family",
    )

    def __init__(self, info: HidDeviceInfo) -> None:
        self.path: bytes = info.path
        self.vendor_id: int = info.vendor_id
        self.product_id: int = info.product_id
        self._handle: int | None = None
        self._unit_info: UnitInfo | None = None
        self._serial: str = info.serial_number
        self._firmware_version: str = ""
        self._rotation: ImageRotation = PID_ROTATION.get(
            info.product_id, ImageRotation.NONE
        )
        self._family: str = _PID_FAMILY.get(info.product_id, "Unknown")

    # -- lifecycle ----------------------------------------------------------

    def open(self) -> None:
        """Open the HID device and read hardware capabilities.

        Raises
        ------
        HidApiError
            If the device cannot be opened.
        """
        self._handle = hid_open(self.path)
        self._read_device_info()

    def close(self) -> None:
        """Close the HID device handle."""
        if self._handle is not None:
            hid_close(self._handle)
            self._handle = None

    @property
    def is_open(self) -> bool:
        """Whether the device handle is open."""
        return self._handle is not None

    def _ensure_open(self) -> int:
        """Return the handle, raising if not open.

        Returns
        -------
        int
            The HID device handle.

        Raises
        ------
        HidApiError
            If the device is not open.
        """
        if self._handle is None:
            raise HidApiError("Device is not open")
        return self._handle

    # -- device info --------------------------------------------------------

    def _read_device_info(self) -> None:
        """Read unit info, serial, and firmware version from the device."""
        handle = self._ensure_open()

        # Unit information
        try:
            data = hid_get_feature_report(handle, ReportId.UNIT_INFO, FEATURE_REPORT_SIZE)
            self._unit_info = parse_unit_info(data)
        except (HidApiError, ValueError):
            self._unit_info = None

        # Serial number (may already be populated from enumeration)
        if not self._serial:
            try:
                data = hid_get_feature_report(
                    handle, ReportId.SERIAL_NUMBER, FEATURE_REPORT_SIZE
                )
                self._serial = parse_serial_number(data)
            except HidApiError:
                self._serial = ""

        # Firmware version (AP2 = primary)
        try:
            data = hid_get_feature_report(
                handle, ReportId.FW_VERSION_AP2, FEATURE_REPORT_SIZE
            )
            self._firmware_version = parse_firmware_version(data)
        except HidApiError:
            self._firmware_version = ""

    @property
    def unit_info(self) -> UnitInfo | None:
        """Hardware information from ``Get Unit Information``, or ``None``."""
        return self._unit_info

    @property
    def serial_number(self) -> str:
        """Device serial number."""
        return self._serial

    @property
    def firmware_version(self) -> str:
        """Primary firmware version string."""
        return self._firmware_version

    @property
    def family(self) -> str:
        """Device family name (e.g. ``'Stream Deck +'``)."""
        return self._family

    @property
    def rotation(self) -> ImageRotation:
        """Image rotation to apply before upload."""
        return self._rotation

    @property
    def key_count(self) -> int:
        """Number of keys (buttons)."""
        if self._unit_info:
            return self._unit_info.rows * self._unit_info.cols
        return 0

    @property
    def key_layout(self) -> tuple[int, int]:
        """Key matrix as ``(columns, rows)``."""
        if self._unit_info:
            return (self._unit_info.cols, self._unit_info.rows)
        return (0, 0)

    @property
    def key_size(self) -> tuple[int, int]:
        """Key image dimensions as ``(width, height)``."""
        if self._unit_info:
            return (self._unit_info.key_width, self._unit_info.key_height)
        return (0, 0)

    @property
    def lcd_size(self) -> tuple[int, int]:
        """Full LCD dimensions as ``(width, height)``."""
        if self._unit_info:
            return (self._unit_info.lcd_width, self._unit_info.lcd_height)
        return (0, 0)

    @property
    def logical_lcd_size(self) -> tuple[int, int]:
        """Logical full-LCD dimensions as ``(width, height)``.

        The size of the entire back-panel LCD in the orientation the
        user sees it upright on the device (i.e. *before* the device's
        pre-upload :attr:`rotation` is applied).  Use this when
        preparing a full-screen image for HID command ``0x08``.

        Unlike :attr:`lcd_size` (which depends on the ``Get Unit
        Information`` feature report and reflects the transmit
        orientation), ``logical_lcd_size`` is a hardcoded per-PID
        constant taken from the Stream Deck hardware specification.

        Returns
        -------
        tuple[int, int]
            ``(width, height)`` in pixels, or ``(0, 0)`` if the PID
            is not in the logical LCD-size table.
        """
        return _LCD_SIZES.get(self.product_id, (0, 0))

    @property
    def has_window(self) -> bool:
        """Whether the device has a window strip (touchscreen or info)."""
        return self.product_id in _WINDOW_PIDS

    @property
    def window_size(self) -> tuple[int, int]:
        """Window strip dimensions as ``(width, height)``, or ``(0, 0)``."""
        return _WINDOW_SIZES.get(self.product_id, (0, 0))

    @property
    def has_touch(self) -> bool:
        """Whether the device has a touchscreen (not just info strip)."""
        return self.product_id in _TOUCH_PIDS

    @property
    def has_encoders(self) -> bool:
        """Whether the device has rotary encoders."""
        return self.product_id in _ENCODER_PIDS

    @property
    def encoder_count(self) -> int:
        """Number of rotary encoders."""
        return _ENCODER_COUNTS.get(self.product_id, 0)

    @property
    def sensor_count(self) -> int:
        """Number of capacitive touch sensors (Neo only)."""
        return _NEO_SENSOR_COUNT.get(self.product_id, 0)

    # -- input --------------------------------------------------------------

    def read_input(self, timeout_ms: int = 50) -> InputEvent | None:
        """Poll for an input event.

        Parameters
        ----------
        timeout_ms : int, default=50
            Read timeout in milliseconds.

        Returns
        -------
        InputEvent or None
            A parsed input event, or ``None`` on timeout.

        Raises
        ------
        HidApiError
            If the read fails (device disconnected, etc.).
        """
        handle = self._ensure_open()
        data = hid_read_timeout(handle, INPUT_REPORT_SIZE, timeout_ms)
        if data is None:
            return None
        return parse_input_report(data)

    # -- output: images -----------------------------------------------------

    def set_key_image(self, key_index: int, jpeg_data: bytes) -> None:
        """Upload a JPEG image for a single key.

        Parameters
        ----------
        key_index : int
            Zero-based key index.
        jpeg_data : bytes
            JPEG-encoded image data (must already be rotated).
        """
        handle = self._ensure_open()
        for report in build_key_image_reports(key_index, jpeg_data):
            hid_write(handle, report)

    def set_full_screen_image(self, jpeg_data: bytes) -> None:
        """Upload a JPEG image covering the entire LCD.

        Parameters
        ----------
        jpeg_data : bytes
            JPEG-encoded full-screen image (must already be rotated).
        """
        handle = self._ensure_open()
        for report in build_full_screen_reports(jpeg_data):
            hid_write(handle, report)

    def set_window_image(self, jpeg_data: bytes) -> None:
        """Upload a JPEG image for the full window strip.

        Parameters
        ----------
        jpeg_data : bytes
            JPEG-encoded window image (must already be rotated).
        """
        handle = self._ensure_open()
        for report in build_window_reports(jpeg_data):
            hid_write(handle, report)

    def set_partial_window_image(
        self, x: int, y: int, width: int, height: int, jpeg_data: bytes
    ) -> None:
        """Upload a JPEG image to a rectangular region of the window.

        Parameters
        ----------
        x : int
            X-coordinate (logical, no rotation accounting).
        y : int
            Y-coordinate.
        width : int
            Region width in pixels.
        height : int
            Region height in pixels.
        jpeg_data : bytes
            JPEG-encoded image (must already be rotated).
        """
        handle = self._ensure_open()
        for report in build_partial_window_reports(x, y, width, height, jpeg_data):
            hid_write(handle, report)

    # -- output: feature reports --------------------------------------------

    def set_brightness(self, percent: int) -> None:
        """Set the LCD backlight brightness.

        Parameters
        ----------
        percent : int
            Brightness level from 0 to 100.
        """
        handle = self._ensure_open()
        hid_send_feature_report(handle, build_set_brightness(percent))

    def show_logo(self) -> None:
        """Display the boot logo on the device."""
        handle = self._ensure_open()
        hid_send_feature_report(handle, build_show_logo())

    def fill_lcd_color(self, r: int, g: int, b: int) -> None:
        """Fill the entire LCD with an RGB color.

        Parameters
        ----------
        r : int
            Red component (0-255).
        g : int
            Green component (0-255).
        b : int
            Blue component (0-255).
        """
        handle = self._ensure_open()
        hid_send_feature_report(handle, build_fill_lcd_color(r, g, b))

    def fill_key_color(self, key_index: int, r: int, g: int, b: int) -> None:
        """Fill a single key with an RGB color.

        Parameters
        ----------
        key_index : int
            Zero-based key index.
        r : int
            Red component (0-255).
        g : int
            Green component (0-255).
        b : int
            Blue component (0-255).
        """
        handle = self._ensure_open()
        hid_send_feature_report(handle, build_fill_key_color(key_index, r, g, b))

    def set_sleep_duration(self, seconds: int) -> None:
        """Set the idle duration before the device enters sleep mode.

        Parameters
        ----------
        seconds : int
            Duration in seconds (0 = disabled).
        """
        handle = self._ensure_open()
        hid_send_feature_report(handle, build_set_sleep_duration(seconds))

    def __repr__(self) -> str:
        state = "open" if self.is_open else "closed"
        return (
            f"HidDevice({self._family}, "
            f"pid=0x{self.product_id:04X}, "
            f"serial={self._serial!r}, "
            f"{state})"
        )

is_open property

is_open: bool

Whether the device handle is open.

unit_info property

unit_info: UnitInfo | None

Hardware information from Get Unit Information, or None.

serial_number property

serial_number: str

Device serial number.

firmware_version property

firmware_version: str

Primary firmware version string.

family property

family: str

Device family name (e.g. 'Stream Deck +').

rotation property

rotation: ImageRotation

Image rotation to apply before upload.

key_count property

key_count: int

Number of keys (buttons).

key_layout property

key_layout: tuple[int, int]

Key matrix as (columns, rows).

key_size property

key_size: tuple[int, int]

Key image dimensions as (width, height).

lcd_size property

lcd_size: tuple[int, int]

Full LCD dimensions as (width, height).

logical_lcd_size property

logical_lcd_size: tuple[int, int]

Logical full-LCD dimensions as (width, height).

The size of the entire back-panel LCD in the orientation the user sees it upright on the device (i.e. before the device's pre-upload :attr:rotation is applied). Use this when preparing a full-screen image for HID command 0x08.

Unlike :attr:lcd_size (which depends on the Get Unit Information feature report and reflects the transmit orientation), logical_lcd_size is a hardcoded per-PID constant taken from the Stream Deck hardware specification.

Returns:

Type Description
tuple[int, int]

(width, height) in pixels, or (0, 0) if the PID is not in the logical LCD-size table.

has_window property

has_window: bool

Whether the device has a window strip (touchscreen or info).

window_size property

window_size: tuple[int, int]

Window strip dimensions as (width, height), or (0, 0).

has_touch property

has_touch: bool

Whether the device has a touchscreen (not just info strip).

has_encoders property

has_encoders: bool

Whether the device has rotary encoders.

encoder_count property

encoder_count: int

Number of rotary encoders.

sensor_count property

sensor_count: int

Number of capacitive touch sensors (Neo only).

open

open() -> None

Open the HID device and read hardware capabilities.

Raises:

Type Description
HidApiError

If the device cannot be opened.

Source code in src/deux/runtime/hid/device.py
def open(self) -> None:
    """Open the HID device and read hardware capabilities.

    Raises
    ------
    HidApiError
        If the device cannot be opened.
    """
    self._handle = hid_open(self.path)
    self._read_device_info()

close

close() -> None

Close the HID device handle.

Source code in src/deux/runtime/hid/device.py
def close(self) -> None:
    """Close the HID device handle."""
    if self._handle is not None:
        hid_close(self._handle)
        self._handle = None

read_input

read_input(timeout_ms: int = 50) -> InputEvent | None

Poll for an input event.

Parameters:

Name Type Description Default
timeout_ms int

Read timeout in milliseconds.

50

Returns:

Type Description
InputEvent or None

A parsed input event, or None on timeout.

Raises:

Type Description
HidApiError

If the read fails (device disconnected, etc.).

Source code in src/deux/runtime/hid/device.py
def read_input(self, timeout_ms: int = 50) -> InputEvent | None:
    """Poll for an input event.

    Parameters
    ----------
    timeout_ms : int, default=50
        Read timeout in milliseconds.

    Returns
    -------
    InputEvent or None
        A parsed input event, or ``None`` on timeout.

    Raises
    ------
    HidApiError
        If the read fails (device disconnected, etc.).
    """
    handle = self._ensure_open()
    data = hid_read_timeout(handle, INPUT_REPORT_SIZE, timeout_ms)
    if data is None:
        return None
    return parse_input_report(data)

set_key_image

set_key_image(key_index: int, jpeg_data: bytes) -> None

Upload a JPEG image for a single key.

Parameters:

Name Type Description Default
key_index int

Zero-based key index.

required
jpeg_data bytes

JPEG-encoded image data (must already be rotated).

required
Source code in src/deux/runtime/hid/device.py
def set_key_image(self, key_index: int, jpeg_data: bytes) -> None:
    """Upload a JPEG image for a single key.

    Parameters
    ----------
    key_index : int
        Zero-based key index.
    jpeg_data : bytes
        JPEG-encoded image data (must already be rotated).
    """
    handle = self._ensure_open()
    for report in build_key_image_reports(key_index, jpeg_data):
        hid_write(handle, report)

set_full_screen_image

set_full_screen_image(jpeg_data: bytes) -> None

Upload a JPEG image covering the entire LCD.

Parameters:

Name Type Description Default
jpeg_data bytes

JPEG-encoded full-screen image (must already be rotated).

required
Source code in src/deux/runtime/hid/device.py
def set_full_screen_image(self, jpeg_data: bytes) -> None:
    """Upload a JPEG image covering the entire LCD.

    Parameters
    ----------
    jpeg_data : bytes
        JPEG-encoded full-screen image (must already be rotated).
    """
    handle = self._ensure_open()
    for report in build_full_screen_reports(jpeg_data):
        hid_write(handle, report)

set_window_image

set_window_image(jpeg_data: bytes) -> None

Upload a JPEG image for the full window strip.

Parameters:

Name Type Description Default
jpeg_data bytes

JPEG-encoded window image (must already be rotated).

required
Source code in src/deux/runtime/hid/device.py
def set_window_image(self, jpeg_data: bytes) -> None:
    """Upload a JPEG image for the full window strip.

    Parameters
    ----------
    jpeg_data : bytes
        JPEG-encoded window image (must already be rotated).
    """
    handle = self._ensure_open()
    for report in build_window_reports(jpeg_data):
        hid_write(handle, report)

set_partial_window_image

set_partial_window_image(x: int, y: int, width: int, height: int, jpeg_data: bytes) -> None

Upload a JPEG image to a rectangular region of the window.

Parameters:

Name Type Description Default
x int

X-coordinate (logical, no rotation accounting).

required
y int

Y-coordinate.

required
width int

Region width in pixels.

required
height int

Region height in pixels.

required
jpeg_data bytes

JPEG-encoded image (must already be rotated).

required
Source code in src/deux/runtime/hid/device.py
def set_partial_window_image(
    self, x: int, y: int, width: int, height: int, jpeg_data: bytes
) -> None:
    """Upload a JPEG image to a rectangular region of the window.

    Parameters
    ----------
    x : int
        X-coordinate (logical, no rotation accounting).
    y : int
        Y-coordinate.
    width : int
        Region width in pixels.
    height : int
        Region height in pixels.
    jpeg_data : bytes
        JPEG-encoded image (must already be rotated).
    """
    handle = self._ensure_open()
    for report in build_partial_window_reports(x, y, width, height, jpeg_data):
        hid_write(handle, report)

set_brightness

set_brightness(percent: int) -> None

Set the LCD backlight brightness.

Parameters:

Name Type Description Default
percent int

Brightness level from 0 to 100.

required
Source code in src/deux/runtime/hid/device.py
def set_brightness(self, percent: int) -> None:
    """Set the LCD backlight brightness.

    Parameters
    ----------
    percent : int
        Brightness level from 0 to 100.
    """
    handle = self._ensure_open()
    hid_send_feature_report(handle, build_set_brightness(percent))
show_logo() -> None

Display the boot logo on the device.

Source code in src/deux/runtime/hid/device.py
def show_logo(self) -> None:
    """Display the boot logo on the device."""
    handle = self._ensure_open()
    hid_send_feature_report(handle, build_show_logo())

fill_lcd_color

fill_lcd_color(r: int, g: int, b: int) -> None

Fill the entire LCD with an RGB color.

Parameters:

Name Type Description Default
r int

Red component (0-255).

required
g int

Green component (0-255).

required
b int

Blue component (0-255).

required
Source code in src/deux/runtime/hid/device.py
def fill_lcd_color(self, r: int, g: int, b: int) -> None:
    """Fill the entire LCD with an RGB color.

    Parameters
    ----------
    r : int
        Red component (0-255).
    g : int
        Green component (0-255).
    b : int
        Blue component (0-255).
    """
    handle = self._ensure_open()
    hid_send_feature_report(handle, build_fill_lcd_color(r, g, b))

fill_key_color

fill_key_color(key_index: int, r: int, g: int, b: int) -> None

Fill a single key with an RGB color.

Parameters:

Name Type Description Default
key_index int

Zero-based key index.

required
r int

Red component (0-255).

required
g int

Green component (0-255).

required
b int

Blue component (0-255).

required
Source code in src/deux/runtime/hid/device.py
def fill_key_color(self, key_index: int, r: int, g: int, b: int) -> None:
    """Fill a single key with an RGB color.

    Parameters
    ----------
    key_index : int
        Zero-based key index.
    r : int
        Red component (0-255).
    g : int
        Green component (0-255).
    b : int
        Blue component (0-255).
    """
    handle = self._ensure_open()
    hid_send_feature_report(handle, build_fill_key_color(key_index, r, g, b))

set_sleep_duration

set_sleep_duration(seconds: int) -> None

Set the idle duration before the device enters sleep mode.

Parameters:

Name Type Description Default
seconds int

Duration in seconds (0 = disabled).

required
Source code in src/deux/runtime/hid/device.py
def set_sleep_duration(self, seconds: int) -> None:
    """Set the idle duration before the device enters sleep mode.

    Parameters
    ----------
    seconds : int
        Duration in seconds (0 = disabled).
    """
    handle = self._ensure_open()
    hid_send_feature_report(handle, build_set_sleep_duration(seconds))