Skip to content

Support for Mirabox Stream Dock 293S #148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/StreamDeck/DeviceManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .Devices.StreamDeckXL import StreamDeckXL
from .Devices.StreamDeckPedal import StreamDeckPedal
from .Devices.StreamDeckPlus import StreamDeckPlus
from .Devices.Mirabox293S import Mirabox293S
from .Transport.Dummy import Dummy
from .Transport.LibUSBHIDAPI import LibUSBHIDAPI
from .ProductIDs import USBVendorIDs, USBProductIDs
Expand Down Expand Up @@ -42,6 +43,8 @@ class DeviceManager:
USB_PID_STREAMDECK_MK2 = 0x0080
USB_PID_STREAMDECK_PEDAL = 0x0086
USB_PID_STREAMDECK_PLUS = 0x0084
USB_VID_MIRABOX = 0x5548
USB_PID_MIRABOX_STREAMDOCK_293S = 0x6670

@staticmethod
def _get_transport(transport):
Expand Down Expand Up @@ -114,6 +117,7 @@ def enumerate(self):
(USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_MINI_MK2, StreamDeckMini),
(USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_XL_V2, StreamDeckXL),
(USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_PLUS, StreamDeckPlus),
(USBVendorIDs.USB_VID_MIRABOX, USBProductIDs.USB_PID_MIRABOX_STREAMDOCK_293S, Mirabox293S)
]

streamdecks = list()
Expand Down
200 changes: 200 additions & 0 deletions src/StreamDeck/Devices/Mirabox293S.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Python Stream Deck Library
# Released under the MIT license
#
# dean [at] fourwalledcubicle [dot] com
# www.fourwalledcubicle.com
#
# Mirabox Stream Dock 293S non-official support
# by Renato Schmidt (github.com/rescbr)

from .StreamDeck import StreamDeck, ControlType


class Mirabox293S(StreamDeck):
"""
Represents a physically attached Mirabox Stream Dock 293S device.
"""

KEY_COUNT = 18
KEY_COLS = 6
KEY_ROWS = 3

KEY_PIXEL_WIDTH = 85 # TODO: check if this is the correct value
KEY_PIXEL_HEIGHT = 85 # TODO: check if this is the correct value
KEY_IMAGE_FORMAT = "JPEG"
KEY_FLIP = (False, False)
KEY_ROTATION = 90

DECK_TYPE = "Mirabox Stream Dock 293S"
DECK_VISUAL = True
DECK_TOUCH = False # kind of... it could be used for the side display.

PACKET_LENGHT = 512

# the side display uses key ids 0x10, 0x11, 0x12 with 80x80 images.
KEY_NUM_TO_DEVICE_KEY_ID = [0x0d, 0x0a, 0x07, 0x04, 0x01, 0x10, 0xe, 0xb, 0x08, 0x05, 0x02, 0x11, 0x0f, 0x0c, 0x09, 0x06, 0x03, 0x12]
KEY_DEVICE_KEY_ID_TO_NUM = {value: index for index, value in enumerate(KEY_NUM_TO_DEVICE_KEY_ID)}

# see note in _read_control_states() method.
_key_triggered_last_read = False

# 72 x 72 black JPEG
BLANK_KEY_IMAGE = [
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08,
0x0a, 0x0c, 0x14, 0x0d, 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12, 0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d, 0x1a,
0x1c, 0x1c, 0x20, 0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, 0x1c, 0x28, 0x37, 0x29, 0x2c, 0x30, 0x31, 0x34,
0x34, 0x34, 0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32, 0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x09,
0x09, 0x09, 0x0c, 0x0b, 0x0c, 0x18, 0x0d, 0x0d, 0x18, 0x32, 0x21, 0x1c, 0x21, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x48, 0x00, 0x48, 0x03, 0x01, 0x22, 0x00,
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,
0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62,
0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85,
0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,
0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7,
0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00,
0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04,
0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,
0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09,
0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a,
0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75,
0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8,
0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,
0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9, 0xfe, 0x8a, 0x28,
0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a,
0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02,
0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0,
0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28,
0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a,
0x28, 0xa0, 0x0f, 0xff, 0xd9
]

def _convert_key_num_to_device_key_id(self, key):
return self.KEY_NUM_TO_DEVICE_KEY_ID[key]

def _convert_device_key_id_to_key_num(self, key):
return self.KEY_DEVICE_KEY_ID_TO_NUM[key]


def _make_payload_for_report_id(self, report_id, payload_data):
payload = bytearray(self.PACKET_LENGHT + 1)
payload[0] = report_id
payload[1:len(payload_data)] = payload_data
return payload

def _read_control_states(self):
states = [False] * self.KEY_COUNT

# _key_triggered_last_read exists since 293S only triggers an HID event when a button is released.
# there are no key down and key up events, so we have to simulate the key being pressed and released.
# if a firmware upgrade that supports key down/up events is released, this variable can be removed from the code.

if not self._key_triggered_last_read:
device_input_data = self.device.read(self.PACKET_LENGHT)
if device_input_data is None:
return None

if(device_input_data.startswith(bytes([0x41, 0x43, 0x4b, 0x00, 0x00, 0x4f, 0x4b, 0x00]))): # ACK\0\0OK\0
triggered_key = self._convert_device_key_id_to_key_num(int.from_bytes(device_input_data[9:10], 'big', signed=False))
else:
# we don't know how to handle the response
return None

states = [False] * self.KEY_COUNT
states[triggered_key] = True
self._key_triggered_last_read = True
else:
self._key_triggered_last_read = False

return {
ControlType.KEY: states
}

def _reset_key_stream(self):
self.reset()

def reset(self):
# disconnect # CRT\0\0DIS
payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x44, 0x49, 0x53])
self.device.write(payload)

# connect/ping # CRT\0\0CONNECT
payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54])
self.device.write(payload)

# clear contents # CRT\0\0CLE #0x00 0x00 0x00 <KEY ID | 0xff for all>
payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x43, 0x4c, 0x45, 0x00, 0x00, 0x00, 0xff])
self.device.write(payload)

def set_brightness(self, percent):
if isinstance(percent, float):
percent = int(100.0 * percent)

percent = min(max(percent, 0), 100)

# set brightness # CRT\0\0LIG #0x00 0x00 <PERCENT> 0x00
payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x4c, 0x49, 0x47, 0x00, 0x00, percent, 0x00])
self.device.write(payload)

def get_serial_number(self):
return self.device.serial_number()

def get_firmware_version(self):
version = self.device.read_input(0x00, self.PACKET_LENGHT + 1)
return self._extract_string(version[1:])

def set_key_image(self, key, image):
if min(max(key, 0), self.KEY_COUNT) != key:
raise IndexError("Invalid key index {}.".format(key))

image = bytes(image or self.BLANK_KEY_IMAGE)
image_payload_page_length = self.PACKET_LENGHT

key = self._convert_key_num_to_device_key_id(key)

image_size_uint16_be = int.to_bytes(len(image), 2, 'big', signed=False)

# start batch # CRT\0\0BAT #0x00 0x00 <image size uint16_be> <key id>
command = bytes([0x43, 0x52, 0x54, 0x00, 0x00, 0x42, 0x41, 0x54, 0x00, 0x00]) + image_size_uint16_be + bytes([key])
payload = self._make_payload_for_report_id(0x00, command)
self.device.write(payload)

page_number = 0
bytes_remaining = len(image)
while bytes_remaining > 0:
this_length = min(bytes_remaining, image_payload_page_length)
bytes_sent = page_number * image_payload_page_length

#send data
payload = self._make_payload_for_report_id(0x00, image[bytes_sent:bytes_sent + this_length])
self.device.write(payload)

bytes_remaining = bytes_remaining - this_length
page_number = page_number + 1

# stop batch # CRT\0\0STP
payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x53, 0x54, 0x50])
self.device.write(payload)



def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0):
pass

def set_key_color(self, key, r, g, b):
pass

def set_screen_image(self, image):
pass
3 changes: 3 additions & 0 deletions src/StreamDeck/ProductIDs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class USBVendorIDs:
"""

USB_VID_ELGATO = 0x0fd9
USB_VID_MIRABOX = 0x5548


class USBProductIDs:
Expand All @@ -29,3 +30,5 @@ class USBProductIDs:
USB_PID_STREAMDECK_PEDAL = 0x0086
USB_PID_STREAMDECK_MINI_MK2 = 0x0090
USB_PID_STREAMDECK_PLUS = 0x0084
USB_PID_MIRABOX_STREAMDOCK_293S = 0x6670

50 changes: 50 additions & 0 deletions src/StreamDeck/Transport/LibUSBHIDAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ class hid_device_info(ctypes.Structure):
self.HIDAPI_INSTANCE.hid_get_feature_report.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t]
self.HIDAPI_INSTANCE.hid_get_feature_report.restype = ctypes.c_int

self.HIDAPI_INSTANCE.hid_get_input_report.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t]
self.HIDAPI_INSTANCE.hid_get_input_report.restype = ctypes.c_int

self.HIDAPI_INSTANCE.hid_write.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t]
self.HIDAPI_INSTANCE.hid_write.restype = ctypes.c_int

Expand Down Expand Up @@ -198,6 +201,7 @@ def enumerate(self, vendor_id=None, product_id=None):
'path': current_device.contents.path.decode('utf-8'),
'vendor_id': current_device.contents.vendor_id,
'product_id': current_device.contents.product_id,
'serial_number': current_device.contents.serial_number
})

current_device = current_device.contents.next
Expand Down Expand Up @@ -298,6 +302,45 @@ def get_feature_report(self, handle, report_id, length):
# We read an extra byte (as expected). Just return the first length requested bytes.
return data.raw[:length]

def get_input_report(self, handle, report_id, length):
"""
Retrieves a HID Input report from an open HID device.

:param Handle handle: Device handle to access.
:param int report_id: Report ID of the report being read.
:param int length: Maximum length of the Input report to read.

:rtype: bytearray()
:return: Array of bytes containing the read Input report. The
first byte of the report will be the Report ID of the
report that was read.
"""

# We may need to oversize our read due a bug in some versions of
# HIDAPI. Only applied on Mac systems, as this will cause other
# issues on other platforms.
read_length = (length + 1) if self.platform_name == 'Darwin' else length

data = ctypes.create_string_buffer(read_length)
data[0] = report_id

with self.mutex:
if not handle:
raise TransportError("No HID device.")

result = self.hidapi.hid_get_input_report(handle, data, len(data))

if result < 0:
raise TransportError("Failed to read input report (%d)" % result)

if length < read_length and result == read_length:
# Mac HIDAPI 0.9.0 bug, we read one less than we expected (not including report ID).
# We requested an over-sized report, so we actually got the amount we wanted.
return data.raw

# We read an extra byte (as expected). Just return the first length requested bytes.
return data.raw[:length]

def write(self, handle, data):
"""
Writes a HID Out report to an open HID device.
Expand Down Expand Up @@ -388,6 +431,9 @@ def vendor_id(self):

def product_id(self):
return self.device_info['product_id']

def serial_number(self):
return self.device_info['serial_number']

def path(self):
return self.device_info['path']
Expand All @@ -399,6 +445,10 @@ def write_feature(self, payload):
def read_feature(self, report_id, length):
with self.mutex:
return self.hidapi.get_feature_report(self.device_handle, report_id, length)

def read_input(self, report_id, length):
with self.mutex:
return self.hidapi.get_input_report(self.device_handle, report_id, length)

def write(self, payload):
with self.mutex:
Expand Down
15 changes: 15 additions & 0 deletions src/StreamDeck/Transport/Transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ def read_feature(self, report_id, length):
"""
pass

@abstractmethod
def read_input(self, report_id, length):
"""
Reads a HID Input report from the open HID device.

:param int report_id: Report ID of the report being read.
:param int length: Maximum length of the Input report to read.

:rtype: list(byte)
:return: List of bytes containing the read Feature report. The
first byte of the report will be the Input ID of the
report that was read.
"""
pass

@abstractmethod
def write(self, payload):
"""
Expand Down