From ddce4e274efa74e7788189369fc8bf5d2b0c7ca1 Mon Sep 17 00:00:00 2001 From: Joshua Einstein-Curtis Date: Sun, 18 Jan 2026 23:09:09 +0100 Subject: [PATCH 1/2] Adding support for AJAZZ AKP03E, Mira protocol 3 --- src/StreamDeck/DeviceManager.py | 9 +- src/StreamDeck/Devices/AjazzAKP03E.py | 331 ++++++++++++++++++++++++++ src/StreamDeck/Devices/Mirabox293S.py | 10 +- src/StreamDeck/ProductIDs.py | 4 + 4 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 src/StreamDeck/Devices/AjazzAKP03E.py diff --git a/src/StreamDeck/DeviceManager.py b/src/StreamDeck/DeviceManager.py index e54e47a9..7639fea1 100644 --- a/src/StreamDeck/DeviceManager.py +++ b/src/StreamDeck/DeviceManager.py @@ -15,6 +15,7 @@ from .Devices.StreamDeckPlus import StreamDeckPlus from .Transport import Transport from .Devices.Mirabox293S import Mirabox293S +from .Devices.AjazzAKP03E import AjazzAKP03E, SoomfonCN002 from .Transport.Dummy import Dummy from .Transport.LibUSBHIDAPI import LibUSBHIDAPI from .ProductIDs import USBVendorIDs, USBProductIDs @@ -112,13 +113,19 @@ def enumerate(self) -> list[StreamDeck]: (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_XL_V2, StreamDeckXL), (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_XL_V2_MODULE, StreamDeckXL), (USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_PLUS, StreamDeckPlus), - (USBVendorIDs.USB_VID_MIRABOX, USBProductIDs.USB_PID_MIRABOX_STREAMDOCK_293S, Mirabox293S) + (USBVendorIDs.USB_VID_MIRABOX, USBProductIDs.USB_PID_MIRABOX_STREAMDOCK_293S, Mirabox293S), + (USBVendorIDs.USB_VID_AJAZZ, USBProductIDs.USB_PID_AJAZZ_AKP03E, AjazzAKP03E), + (USBVendorIDs.USB_VID_SOOMFON, USBProductIDs.USB_PID_SOOMFON_CN002, SoomfonCN002) ] streamdecks = list() for vid, pid, class_type in products: found_devices = self.transport.enumerate(vid=vid, pid=pid) + + # This device has a second HID interface as a keyboard + if class_type == AjazzAKP03E: + found_devices = found_devices[::2] streamdecks.extend([class_type(d) for d in found_devices]) return streamdecks diff --git a/src/StreamDeck/Devices/AjazzAKP03E.py b/src/StreamDeck/Devices/AjazzAKP03E.py new file mode 100644 index 00000000..68bbf6a3 --- /dev/null +++ b/src/StreamDeck/Devices/AjazzAKP03E.py @@ -0,0 +1,331 @@ +# 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, DialEventType + + +class AjazzAKP03E(StreamDeck): + """ + Represents a physically attached Ajazz AKP03E or soomfon N3 device. + + Basically a Mirabox N3, but with their own quirks. + + Two major related projects helped, along with raw device captures: + + soomfon-plugin: https://github.com/PRL-Digital/soomfon-plugin/ + mirajazz: https://github.com/4ndv/mirajazz + opendeck plugin: https://github.com/4ndv/opendeck-akp03 + """ + + KEY_COUNT = 9 + KEY_COLS = 3 + KEY_ROWS = 3 + + DIAL_COUNT = 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 = "AJAZZ AKP03E" + DECK_VISUAL = True + DECK_TOUCH = False # kind of... it could be used for the side display. + + PACKET_LENGTH = 1024 + + # the side display uses key ids 0x10, 0x11, 0x12 with 80x80 images. + # match input { + # (0..=6) | 0x25 | 0x30 | 0x31 => read_button_press(input, state), + # 0x90 | 0x91 | 0x50 | 0x51 | 0x60 | 0x61 => read_encoder_value(input), + # 0x33..=0x35 => read_encoder_press(input, state), + # _ => Err(MirajazzError::BadData), + + NO_IMG_KEYS = [0x25, 0x30, 0x31] + + KEY_NUM_TO_DEVICE_KEY_ID = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06] + NO_IMG_KEYS + KEY_DEVICE_KEY_ID_TO_NUM = {value: index for index, value in enumerate(KEY_NUM_TO_DEVICE_KEY_ID)} + + # center, left, right + DIAL_MAPPING = { + "cw": [0x51, 0x91, 0x61], + "ccw": [0x50, 0x90, 0x60], + "push": [0x35, 0x33, 0x34] + } + + DIAL_TURN_CW_LOOKUP = {value: index for index, value in enumerate(DIAL_MAPPING["cw"])} + DIAL_TURN_CCW_LOOKUP = {value: index for index, value in enumerate(DIAL_MAPPING["ccw"])} + DIAL_PUSH_LOOKUP = {value: index for index, value in enumerate(DIAL_MAPPING["push"])} + + # see note in _read_control_states() method. + _key_triggered_last_read = False + + # 85 x 85 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, 0x55, 0x00, 0x55, 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, 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_LENGTH+1) + payload[0] = report_id + payload[1:len(payload_data)+1] = payload_data + return payload + + def _send_keep_alive(self): + # # 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) + + def _read_control_states(self): + """Requires ~10 second poll for a keep-alive of device + + Given the default polling rate of 1kHz, this would be 10k ticks""" + + device_input_data = self.device.read(self.PACKET_LENGTH) # Only read maximum of possible given device + + self.counter += 1 + + if self.counter >= 10000: + # print("Sending keep alive") + self._send_keep_alive() + self.counter = 0 + + if device_input_data is None: + return None + + # Check that this is a valid message + # ACK\0\0OK\0 + if(device_input_data.startswith(bytes([0x41, 0x43, 0x4b, 0x00, 0x00, 0x4f, 0x4b, 0x00]))): + read_event = device_input_data[9] + read_value = device_input_data[10] + else: + # we don't know how to handle the response + return None + + # print(f"Received event from 0x{read_event:0x} : 0x{read_value:0x}") + + if read_event in self.KEY_NUM_TO_DEVICE_KEY_ID: # Key Event + + # Save last state to be able to build output array + self._int_key_states[self.KEY_DEVICE_KEY_ID_TO_NUM[read_event]] = bool(read_value) + + return { + ControlType.KEY: self._int_key_states + } + elif read_event in self.DIAL_MAPPING["push"]: # Dial push + + self._int_dial_key_states[self.DIAL_PUSH_LOOKUP[read_event]] = read_value + + return { + ControlType.DIAL: { + DialEventType.PUSH: self._int_dial_key_states, + } + } + elif read_event in self.DIAL_MAPPING["cw"]: # Dial turn + # print(f"Handling CW turn") + states = [0] * self.DIAL_COUNT + states[self.DIAL_TURN_CW_LOOKUP[read_event]] = -1 + return { + ControlType.DIAL: { + DialEventType.TURN: states, + } + } + elif read_event in self.DIAL_MAPPING["ccw"]: # Dial turn + # print(f"Handling CCW turn") + states = [0] * self.DIAL_COUNT + states[self.DIAL_TURN_CCW_LOOKUP[read_event]] = 1 + return { + ControlType.DIAL: { + DialEventType.TURN: states, + } + } + else: + print(f"Unknown event for key id 0x{read_event:0x} received") + return None + + def _reset_key_stream(self): + self.reset() + + def _send_disconnect(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) + + def initialize(self): + + self.counter = 0 + self._int_key_states = [False] * self.KEY_COUNT + self._int_dial_key_states = [False] * self.DIAL_COUNT + + self._send_disconnect() + self.set_brightness(0x19) # Based on raw captures + + # Quad CMD # CRT\0\0QUCMD + # From packet capture 43 52 54 00 00 51 55 43 4d 44 11 11 00 11 00 11 + payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x51, 0x55, 0x42, 0x4d, 0x44, 0x11, 0x11, 0x00, 0x11, 0x00, 0x11]) + self.device.write(payload) + + self._send_keep_alive() + + self.clear_all_button_images() + self.clear_button_states() + + + def clear_all_button_images(self): + self.clear_button_image(0xff) + + def clear_button_image(self, key): + + cval = 0xff if key == 0xff else key + 1 + + # 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, cval]) + self.device.write(payload) + + def clear_lcd_displays(self): + + # clear contents # CRT\0\0CLE\0DC + payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x0, 0x0, 0x43, 0x4c, 0x45, 0x0, 0x44, 0x43]) + self.device.write(payload) + + def clear_button_states(self): + + self._int_key_states = [False] * self.KEY_COUNT + self._int_dial_key_states = [False] * self.DIAL_COUNT + + # clear contents # CRT\0\0CLB\0DC + payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x0, 0x0, 0x43, 0x4c, 0x42, 0x0, 0x44, 0x43]) + self.device.write(payload) + + def halt_device(self): + # clear contents # CRT\0\0HAH + payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x48, 0x41, 0x48]) + self.device.write(payload) + + def reset(self): + self.initialize() + + # Set brightness to full + self.set_brightness(100) + + # Clear all button images + self.clear_all_button_images() + + 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_LENGTH + 1) + return self._extract_string(version[1:]) + + def set_key_image(self, key:int, 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_LENGTH + + if key in self.NO_IMG_KEYS: + raise RuntimeWarning("Requested key index {} has no image.".format(key)) + # TODO determine proper error/warning behavior + return + + 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 + + def set_key_color(self, key, r, g, b): + pass + + def set_screen_image(self, image): + pass + +class SoomfonCN002(AjazzAKP03E): + DECK_TYPE = "Soomfon CN002" diff --git a/src/StreamDeck/Devices/Mirabox293S.py b/src/StreamDeck/Devices/Mirabox293S.py index e82dc22e..b7e63b10 100644 --- a/src/StreamDeck/Devices/Mirabox293S.py +++ b/src/StreamDeck/Devices/Mirabox293S.py @@ -29,7 +29,7 @@ class Mirabox293S(StreamDeck): DECK_VISUAL = True DECK_TOUCH = False # kind of... it could be used for the side display. - PACKET_LENGHT = 512 + PACKET_LENGTH = 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] @@ -89,7 +89,7 @@ def _convert_device_key_id_to_key_num(self, key): def _make_payload_for_report_id(self, report_id, payload_data): - payload = bytearray(self.PACKET_LENGHT + 1) + payload = bytearray(self.PACKET_LENGTH + 1) payload[0] = report_id payload[1:len(payload_data)] = payload_data return payload @@ -102,7 +102,7 @@ def _read_control_states(self): # 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) + device_input_data = self.device.read(self.PACKET_LENGTH) if device_input_data is None: return None @@ -152,7 +152,7 @@ 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) + version = self.device.read_input(0x00, self.PACKET_LENGTH + 1) return self._extract_string(version[1:]) def set_key_image(self, key, image): @@ -160,7 +160,7 @@ def set_key_image(self, key, image): raise IndexError("Invalid key index {}.".format(key)) image = bytes(image or self.BLANK_KEY_IMAGE) - image_payload_page_length = self.PACKET_LENGHT + image_payload_page_length = self.PACKET_LENGTH key = self._convert_key_num_to_device_key_id(key) diff --git a/src/StreamDeck/ProductIDs.py b/src/StreamDeck/ProductIDs.py index f07f672f..13372c96 100644 --- a/src/StreamDeck/ProductIDs.py +++ b/src/StreamDeck/ProductIDs.py @@ -13,6 +13,8 @@ class USBVendorIDs: USB_VID_ELGATO = 0x0fd9 USB_VID_MIRABOX = 0x5548 + USB_VID_AJAZZ = 0x0300 + USB_VID_SOOMFON = 0x1500 class USBProductIDs: @@ -38,3 +40,5 @@ class USBProductIDs: USB_PID_STREAMDECK_PLUS = 0x0084 USB_PID_STREAMDECK_XL_V2_MODULE = 0x00ba USB_PID_MIRABOX_STREAMDOCK_293S = 0x6670 + USB_PID_AJAZZ_AKP03E = 0x3002 + USB_PID_SOOMFON_CN002 = 0x3001 # Stream Controller SE From 8440ebd97e5ce7dcfe7f547e53be65d9d1c0aa80 Mon Sep 17 00:00:00 2001 From: Joshua Einstein-Curtis Date: Sun, 18 Jan 2026 23:14:37 +0100 Subject: [PATCH 2/2] Fix for better ignoring second HID devices --- src/StreamDeck/DeviceManager.py | 2 +- src/StreamDeck/Devices/AjazzAKP03E.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/StreamDeck/DeviceManager.py b/src/StreamDeck/DeviceManager.py index 7639fea1..734df903 100644 --- a/src/StreamDeck/DeviceManager.py +++ b/src/StreamDeck/DeviceManager.py @@ -124,7 +124,7 @@ def enumerate(self) -> list[StreamDeck]: found_devices = self.transport.enumerate(vid=vid, pid=pid) # This device has a second HID interface as a keyboard - if class_type == AjazzAKP03E: + if getattr(class_type, "IGNORE_SECOND_HID_DEVICE", False): found_devices = found_devices[::2] streamdecks.extend([class_type(d) for d in found_devices]) diff --git a/src/StreamDeck/Devices/AjazzAKP03E.py b/src/StreamDeck/Devices/AjazzAKP03E.py index 68bbf6a3..ee1debe0 100644 --- a/src/StreamDeck/Devices/AjazzAKP03E.py +++ b/src/StreamDeck/Devices/AjazzAKP03E.py @@ -23,6 +23,8 @@ class AjazzAKP03E(StreamDeck): opendeck plugin: https://github.com/4ndv/opendeck-akp03 """ + IGNORE_SECOND_HID_DEVICE = True + KEY_COUNT = 9 KEY_COLS = 3 KEY_ROWS = 3