From 2080e4462c3f1edc08b6a4520d8d6b3cde3191a1 Mon Sep 17 00:00:00 2001 From: Renato Schmidt Date: Thu, 8 Aug 2024 14:04:30 -0300 Subject: [PATCH 1/5] initial basic support for Mirabox Stream Deck 293S --- src/StreamDeck/DeviceManager.py | 2 + src/StreamDeck/Devices/Mirabox293S.py | 181 +++++++++++++++++++++++ src/StreamDeck/ProductIDs.py | 3 + src/StreamDeck/Transport/LibUSBHIDAPI.py | 50 +++++++ src/StreamDeck/Transport/Transport.py | 15 ++ 5 files changed, 251 insertions(+) create mode 100644 src/StreamDeck/Devices/Mirabox293S.py diff --git a/src/StreamDeck/DeviceManager.py b/src/StreamDeck/DeviceManager.py index 640e3bac..5c33eb77 100644 --- a/src/StreamDeck/DeviceManager.py +++ b/src/StreamDeck/DeviceManager.py @@ -11,6 +11,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 @@ -111,6 +112,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() diff --git a/src/StreamDeck/Devices/Mirabox293S.py b/src/StreamDeck/Devices/Mirabox293S.py new file mode 100644 index 00000000..42cbbcd5 --- /dev/null +++ b/src/StreamDeck/Devices/Mirabox293S.py @@ -0,0 +1,181 @@ +# Python Stream Deck Library +# Released under the MIT license +# +# dean [at] fourwalledcubicle [dot] com +# www.fourwalledcubicle.com +# +# Mirabox Stream Dock 293S support by rescbr + +from .StreamDeck import StreamDeck, ControlType + + +class Mirabox293S(StreamDeck): + """ + Represents a physically attached Mirabox Stream Dock 293S device. + """ + + KEY_COUNT = 15 + KEY_COLS = 5 + 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... + + PACKET_LENGHT = 512 + IMAGE_REPORT_HEADER_LENGTH = 16 + + # the side display uses key ids 0x10, 0x11, 0x12 with 80x80 images. + KEY_NUM_TO_DEVICE_KEY_ID = [0x0d, 0x0a, 0x07, 0x04, 0x01, 0xe, 0xb, 0x08, 0x05, 0x02, 0x0f, 0x0c, 0x09, 0x06, 0x03] + KEY_DEVICE_KEY_ID_TO_NUM = {value: index for index, value in enumerate(KEY_NUM_TO_DEVICE_KEY_ID)} + + # 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): + 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 + + return { + ControlType.KEY: states + } + + def _reset_key_stream(self): + return + + 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 + 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 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 + 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 diff --git a/src/StreamDeck/ProductIDs.py b/src/StreamDeck/ProductIDs.py index 6ce76e74..49cc9979 100644 --- a/src/StreamDeck/ProductIDs.py +++ b/src/StreamDeck/ProductIDs.py @@ -12,6 +12,7 @@ class USBVendorIDs: """ USB_VID_ELGATO = 0x0fd9 + USB_VID_MIRABOX = 0x5548 class USBProductIDs: @@ -28,3 +29,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 + diff --git a/src/StreamDeck/Transport/LibUSBHIDAPI.py b/src/StreamDeck/Transport/LibUSBHIDAPI.py index 1f884f37..c81a16a1 100644 --- a/src/StreamDeck/Transport/LibUSBHIDAPI.py +++ b/src/StreamDeck/Transport/LibUSBHIDAPI.py @@ -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 @@ -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 @@ -300,6 +304,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. @@ -391,6 +434,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'] @@ -402,6 +448,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: diff --git a/src/StreamDeck/Transport/Transport.py b/src/StreamDeck/Transport/Transport.py index da161a46..52b9c213 100644 --- a/src/StreamDeck/Transport/Transport.py +++ b/src/StreamDeck/Transport/Transport.py @@ -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): """ From 67dbecaa7d531ab79dd675e75ce8206f1dc1c046 Mon Sep 17 00:00:00 2001 From: Renato Schmidt Date: Thu, 8 Aug 2024 19:36:36 -0300 Subject: [PATCH 2/5] implements key up/down emulation --- src/StreamDeck/Devices/Mirabox293S.py | 40 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/StreamDeck/Devices/Mirabox293S.py b/src/StreamDeck/Devices/Mirabox293S.py index 42cbbcd5..34277d3f 100644 --- a/src/StreamDeck/Devices/Mirabox293S.py +++ b/src/StreamDeck/Devices/Mirabox293S.py @@ -26,15 +26,17 @@ class Mirabox293S(StreamDeck): DECK_TYPE = "Mirabox Stream Dock 293S" DECK_VISUAL = True - DECK_TOUCH = False # kind of... + DECK_TOUCH = False # kind of... it could be used for the side display. PACKET_LENGHT = 512 - IMAGE_REPORT_HEADER_LENGTH = 16 # the side display uses key ids 0x10, 0x11, 0x12 with 80x80 images. KEY_NUM_TO_DEVICE_KEY_ID = [0x0d, 0x0a, 0x07, 0x04, 0x01, 0xe, 0xb, 0x08, 0x05, 0x02, 0x0f, 0x0c, 0x09, 0x06, 0x03] 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, @@ -92,25 +94,35 @@ def _make_payload_for_report_id(self, report_id, payload_data): return payload def _read_control_states(self): - 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 + + # _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): - return + self.reset() def reset(self): # disconnect # CRT\0\0DIS From 00bf64e171bb9088a0bf993e97a569d08f013a90 Mon Sep 17 00:00:00 2001 From: Renato Schmidt Date: Thu, 8 Aug 2024 19:46:00 -0300 Subject: [PATCH 3/5] improve contributor message :) --- src/StreamDeck/Devices/Mirabox293S.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StreamDeck/Devices/Mirabox293S.py b/src/StreamDeck/Devices/Mirabox293S.py index 34277d3f..b5c4fb9e 100644 --- a/src/StreamDeck/Devices/Mirabox293S.py +++ b/src/StreamDeck/Devices/Mirabox293S.py @@ -4,7 +4,8 @@ # dean [at] fourwalledcubicle [dot] com # www.fourwalledcubicle.com # -# Mirabox Stream Dock 293S support by rescbr +# Mirabox Stream Dock 293S non-official support +# by Renato Schmidt (github.com/rescbr) from .StreamDeck import StreamDeck, ControlType From 671d8746b550a9e36ba59c15ab0140bd54cd2081 Mon Sep 17 00:00:00 2001 From: fightforlife <5511687+fightforlife@users.noreply.github.com> Date: Sun, 12 Jan 2025 11:02:47 +0100 Subject: [PATCH 4/5] add dummy functions, needed by streamcontroller --- src/StreamDeck/Devices/Mirabox293S.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/StreamDeck/Devices/Mirabox293S.py b/src/StreamDeck/Devices/Mirabox293S.py index b5c4fb9e..27520635 100644 --- a/src/StreamDeck/Devices/Mirabox293S.py +++ b/src/StreamDeck/Devices/Mirabox293S.py @@ -192,3 +192,9 @@ def set_key_image(self, key, image): 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 From 78e7e45d0cde6ee995ada123f918acd97d74f924 Mon Sep 17 00:00:00 2001 From: fightforlife <5511687+fightforlife@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:35:24 +0100 Subject: [PATCH 5/5] add side display and small fix in Devicemanager --- src/StreamDeck/DeviceManager.py | 2 ++ src/StreamDeck/Devices/Mirabox293S.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/StreamDeck/DeviceManager.py b/src/StreamDeck/DeviceManager.py index 853db8ff..61ad2e57 100644 --- a/src/StreamDeck/DeviceManager.py +++ b/src/StreamDeck/DeviceManager.py @@ -43,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): diff --git a/src/StreamDeck/Devices/Mirabox293S.py b/src/StreamDeck/Devices/Mirabox293S.py index 27520635..e82dc22e 100644 --- a/src/StreamDeck/Devices/Mirabox293S.py +++ b/src/StreamDeck/Devices/Mirabox293S.py @@ -15,8 +15,8 @@ class Mirabox293S(StreamDeck): Represents a physically attached Mirabox Stream Dock 293S device. """ - KEY_COUNT = 15 - KEY_COLS = 5 + KEY_COUNT = 18 + KEY_COLS = 6 KEY_ROWS = 3 KEY_PIXEL_WIDTH = 85 # TODO: check if this is the correct value @@ -32,7 +32,7 @@ class Mirabox293S(StreamDeck): 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, 0xe, 0xb, 0x08, 0x05, 0x02, 0x0f, 0x0c, 0x09, 0x06, 0x03] + 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.