Skip to content

Commit 2505714

Browse files
Core447rescbrfightforlife
authored
Support for Mirabox Stream Dock 293S (#1)
* initial basic support for Mirabox Stream Deck 293S * implements key up/down emulation * improve contributor message :) * add dummy functions, needed by streamcontroller * add side display and small fix in Devicemanager --------- Co-authored-by: Renato Schmidt <[email protected]> Co-authored-by: Frederik <[email protected]>
1 parent 57132b7 commit 2505714

File tree

5 files changed

+272
-0
lines changed

5 files changed

+272
-0
lines changed

src/StreamDeck/DeviceManager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .Devices.StreamDeckXL import StreamDeckXL
1414
from .Devices.StreamDeckPedal import StreamDeckPedal
1515
from .Devices.StreamDeckPlus import StreamDeckPlus
16+
from .Devices.Mirabox293S import Mirabox293S
1617
from .Transport.Dummy import Dummy
1718
from .Transport.LibUSBHIDAPI import LibUSBHIDAPI
1819
from .ProductIDs import USBVendorIDs, USBProductIDs
@@ -43,6 +44,8 @@ class DeviceManager:
4344
USB_PID_STREAMDECK_MK2 = 0x0080
4445
USB_PID_STREAMDECK_PEDAL = 0x0086
4546
USB_PID_STREAMDECK_PLUS = 0x0084
47+
USB_VID_MIRABOX = 0x5548
48+
USB_PID_MIRABOX_STREAMDOCK_293S = 0x6670
4649

4750
@staticmethod
4851
def _get_transport(transport):
@@ -115,6 +118,7 @@ def enumerate(self) -> list[StreamDeck]:
115118
(USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_MINI_MK2, StreamDeckMini),
116119
(USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_XL_V2, StreamDeckXL),
117120
(USBVendorIDs.USB_VID_ELGATO, USBProductIDs.USB_PID_STREAMDECK_PLUS, StreamDeckPlus),
121+
(USBVendorIDs.USB_VID_MIRABOX, USBProductIDs.USB_PID_MIRABOX_STREAMDOCK_293S, Mirabox293S)
118122
]
119123

120124
streamdecks = list()

src/StreamDeck/Devices/Mirabox293S.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Python Stream Deck Library
2+
# Released under the MIT license
3+
#
4+
# dean [at] fourwalledcubicle [dot] com
5+
# www.fourwalledcubicle.com
6+
#
7+
# Mirabox Stream Dock 293S non-official support
8+
# by Renato Schmidt (github.com/rescbr)
9+
10+
from .StreamDeck import StreamDeck, ControlType
11+
12+
13+
class Mirabox293S(StreamDeck):
14+
"""
15+
Represents a physically attached Mirabox Stream Dock 293S device.
16+
"""
17+
18+
KEY_COUNT = 18
19+
KEY_COLS = 6
20+
KEY_ROWS = 3
21+
22+
KEY_PIXEL_WIDTH = 85 # TODO: check if this is the correct value
23+
KEY_PIXEL_HEIGHT = 85 # TODO: check if this is the correct value
24+
KEY_IMAGE_FORMAT = "JPEG"
25+
KEY_FLIP = (False, False)
26+
KEY_ROTATION = 90
27+
28+
DECK_TYPE = "Mirabox Stream Dock 293S"
29+
DECK_VISUAL = True
30+
DECK_TOUCH = False # kind of... it could be used for the side display.
31+
32+
PACKET_LENGHT = 512
33+
34+
# the side display uses key ids 0x10, 0x11, 0x12 with 80x80 images.
35+
KEY_NUM_TO_DEVICE_KEY_ID = [0x0d, 0x0a, 0x07, 0x04, 0x01, 0x10, 0xe, 0xb, 0x08, 0x05, 0x02, 0x11, 0x0f, 0x0c, 0x09, 0x06, 0x03, 0x12]
36+
KEY_DEVICE_KEY_ID_TO_NUM = {value: index for index, value in enumerate(KEY_NUM_TO_DEVICE_KEY_ID)}
37+
38+
# see note in _read_control_states() method.
39+
_key_triggered_last_read = False
40+
41+
# 72 x 72 black JPEG
42+
BLANK_KEY_IMAGE = [
43+
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
44+
0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08,
45+
0x0a, 0x0c, 0x14, 0x0d, 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12, 0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d, 0x1a,
46+
0x1c, 0x1c, 0x20, 0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, 0x1c, 0x28, 0x37, 0x29, 0x2c, 0x30, 0x31, 0x34,
47+
0x34, 0x34, 0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32, 0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x09,
48+
0x09, 0x09, 0x0c, 0x0b, 0x0c, 0x18, 0x0d, 0x0d, 0x18, 0x32, 0x21, 0x1c, 0x21, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
49+
0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
50+
0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32,
51+
0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x48, 0x00, 0x48, 0x03, 0x01, 0x22, 0x00,
52+
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01,
53+
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a,
54+
0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,
55+
0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
56+
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62,
57+
0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37,
58+
0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,
59+
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85,
60+
0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,
61+
0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
62+
0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7,
63+
0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00,
64+
0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03,
65+
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04,
66+
0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,
67+
0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09,
68+
0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a,
69+
0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
70+
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75,
71+
0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
72+
0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
73+
0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8,
74+
0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9,
75+
0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xf9, 0xfe, 0x8a, 0x28,
76+
0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a,
77+
0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02,
78+
0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0,
79+
0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28,
80+
0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a,
81+
0x28, 0xa0, 0x0f, 0xff, 0xd9
82+
]
83+
84+
def _convert_key_num_to_device_key_id(self, key):
85+
return self.KEY_NUM_TO_DEVICE_KEY_ID[key]
86+
87+
def _convert_device_key_id_to_key_num(self, key):
88+
return self.KEY_DEVICE_KEY_ID_TO_NUM[key]
89+
90+
91+
def _make_payload_for_report_id(self, report_id, payload_data):
92+
payload = bytearray(self.PACKET_LENGHT + 1)
93+
payload[0] = report_id
94+
payload[1:len(payload_data)] = payload_data
95+
return payload
96+
97+
def _read_control_states(self):
98+
states = [False] * self.KEY_COUNT
99+
100+
# _key_triggered_last_read exists since 293S only triggers an HID event when a button is released.
101+
# there are no key down and key up events, so we have to simulate the key being pressed and released.
102+
# if a firmware upgrade that supports key down/up events is released, this variable can be removed from the code.
103+
104+
if not self._key_triggered_last_read:
105+
device_input_data = self.device.read(self.PACKET_LENGHT)
106+
if device_input_data is None:
107+
return None
108+
109+
if(device_input_data.startswith(bytes([0x41, 0x43, 0x4b, 0x00, 0x00, 0x4f, 0x4b, 0x00]))): # ACK\0\0OK\0
110+
triggered_key = self._convert_device_key_id_to_key_num(int.from_bytes(device_input_data[9:10], 'big', signed=False))
111+
else:
112+
# we don't know how to handle the response
113+
return None
114+
115+
states = [False] * self.KEY_COUNT
116+
states[triggered_key] = True
117+
self._key_triggered_last_read = True
118+
else:
119+
self._key_triggered_last_read = False
120+
121+
return {
122+
ControlType.KEY: states
123+
}
124+
125+
def _reset_key_stream(self):
126+
self.reset()
127+
128+
def reset(self):
129+
# disconnect # CRT\0\0DIS
130+
payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x44, 0x49, 0x53])
131+
self.device.write(payload)
132+
133+
# connect/ping # CRT\0\0CONNECT
134+
payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54])
135+
self.device.write(payload)
136+
137+
# clear contents # CRT\0\0CLE #0x00 0x00 0x00 <KEY ID | 0xff for all>
138+
payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x43, 0x4c, 0x45, 0x00, 0x00, 0x00, 0xff])
139+
self.device.write(payload)
140+
141+
def set_brightness(self, percent):
142+
if isinstance(percent, float):
143+
percent = int(100.0 * percent)
144+
145+
percent = min(max(percent, 0), 100)
146+
147+
# set brightness # CRT\0\0LIG #0x00 0x00 <PERCENT> 0x00
148+
payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x4c, 0x49, 0x47, 0x00, 0x00, percent, 0x00])
149+
self.device.write(payload)
150+
151+
def get_serial_number(self):
152+
return self.device.serial_number()
153+
154+
def get_firmware_version(self):
155+
version = self.device.read_input(0x00, self.PACKET_LENGHT + 1)
156+
return self._extract_string(version[1:])
157+
158+
def set_key_image(self, key, image):
159+
if min(max(key, 0), self.KEY_COUNT) != key:
160+
raise IndexError("Invalid key index {}.".format(key))
161+
162+
image = bytes(image or self.BLANK_KEY_IMAGE)
163+
image_payload_page_length = self.PACKET_LENGHT
164+
165+
key = self._convert_key_num_to_device_key_id(key)
166+
167+
image_size_uint16_be = int.to_bytes(len(image), 2, 'big', signed=False)
168+
169+
# start batch # CRT\0\0BAT #0x00 0x00 <image size uint16_be> <key id>
170+
command = bytes([0x43, 0x52, 0x54, 0x00, 0x00, 0x42, 0x41, 0x54, 0x00, 0x00]) + image_size_uint16_be + bytes([key])
171+
payload = self._make_payload_for_report_id(0x00, command)
172+
self.device.write(payload)
173+
174+
page_number = 0
175+
bytes_remaining = len(image)
176+
while bytes_remaining > 0:
177+
this_length = min(bytes_remaining, image_payload_page_length)
178+
bytes_sent = page_number * image_payload_page_length
179+
180+
#send data
181+
payload = self._make_payload_for_report_id(0x00, image[bytes_sent:bytes_sent + this_length])
182+
self.device.write(payload)
183+
184+
bytes_remaining = bytes_remaining - this_length
185+
page_number = page_number + 1
186+
187+
# stop batch # CRT\0\0STP
188+
payload = self._make_payload_for_report_id(0x00, [0x43, 0x52, 0x54, 0x00, 0x00, 0x53, 0x54, 0x50])
189+
self.device.write(payload)
190+
191+
192+
193+
def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0):
194+
pass
195+
196+
def set_key_color(self, key, r, g, b):
197+
pass
198+
199+
def set_screen_image(self, image):
200+
pass

src/StreamDeck/ProductIDs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class USBVendorIDs:
1212
"""
1313

1414
USB_VID_ELGATO = 0x0fd9
15+
USB_VID_MIRABOX = 0x5548
1516

1617

1718
class USBProductIDs:
@@ -29,3 +30,5 @@ class USBProductIDs:
2930
USB_PID_STREAMDECK_PEDAL = 0x0086
3031
USB_PID_STREAMDECK_MINI_MK2 = 0x0090
3132
USB_PID_STREAMDECK_PLUS = 0x0084
33+
USB_PID_MIRABOX_STREAMDOCK_293S = 0x6670
34+

src/StreamDeck/Transport/LibUSBHIDAPI.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ class hid_device_info(ctypes.Structure):
133133
self.HIDAPI_INSTANCE.hid_get_feature_report.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t]
134134
self.HIDAPI_INSTANCE.hid_get_feature_report.restype = ctypes.c_int
135135

136+
self.HIDAPI_INSTANCE.hid_get_input_report.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t]
137+
self.HIDAPI_INSTANCE.hid_get_input_report.restype = ctypes.c_int
138+
136139
self.HIDAPI_INSTANCE.hid_write.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char), ctypes.c_size_t]
137140
self.HIDAPI_INSTANCE.hid_write.restype = ctypes.c_int
138141

@@ -198,6 +201,7 @@ def enumerate(self, vendor_id=None, product_id=None):
198201
'path': current_device.contents.path.decode('utf-8'),
199202
'vendor_id': current_device.contents.vendor_id,
200203
'product_id': current_device.contents.product_id,
204+
'serial_number': current_device.contents.serial_number
201205
})
202206

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

305+
def get_input_report(self, handle, report_id, length):
306+
"""
307+
Retrieves a HID Input report from an open HID device.
308+
309+
:param Handle handle: Device handle to access.
310+
:param int report_id: Report ID of the report being read.
311+
:param int length: Maximum length of the Input report to read.
312+
313+
:rtype: bytearray()
314+
:return: Array of bytes containing the read Input report. The
315+
first byte of the report will be the Report ID of the
316+
report that was read.
317+
"""
318+
319+
# We may need to oversize our read due a bug in some versions of
320+
# HIDAPI. Only applied on Mac systems, as this will cause other
321+
# issues on other platforms.
322+
read_length = (length + 1) if self.platform_name == 'Darwin' else length
323+
324+
data = ctypes.create_string_buffer(read_length)
325+
data[0] = report_id
326+
327+
with self.mutex:
328+
if not handle:
329+
raise TransportError("No HID device.")
330+
331+
result = self.hidapi.hid_get_input_report(handle, data, len(data))
332+
333+
if result < 0:
334+
raise TransportError("Failed to read input report (%d)" % result)
335+
336+
if length < read_length and result == read_length:
337+
# Mac HIDAPI 0.9.0 bug, we read one less than we expected (not including report ID).
338+
# We requested an over-sized report, so we actually got the amount we wanted.
339+
return data.raw
340+
341+
# We read an extra byte (as expected). Just return the first length requested bytes.
342+
return data.raw[:length]
343+
301344
def write(self, handle, data):
302345
"""
303346
Writes a HID Out report to an open HID device.
@@ -388,6 +431,9 @@ def vendor_id(self):
388431

389432
def product_id(self):
390433
return self.device_info['product_id']
434+
435+
def serial_number(self):
436+
return self.device_info['serial_number']
391437

392438
def path(self):
393439
return self.device_info['path']
@@ -399,6 +445,10 @@ def write_feature(self, payload):
399445
def read_feature(self, report_id, length):
400446
with self.mutex:
401447
return self.hidapi.get_feature_report(self.device_handle, report_id, length)
448+
449+
def read_input(self, report_id, length):
450+
with self.mutex:
451+
return self.hidapi.get_input_report(self.device_handle, report_id, length)
402452

403453
def write(self, payload):
404454
with self.mutex:

src/StreamDeck/Transport/Transport.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,21 @@ def read_feature(self, report_id, length):
135135
"""
136136
pass
137137

138+
@abstractmethod
139+
def read_input(self, report_id, length):
140+
"""
141+
Reads a HID Input report from the open HID device.
142+
143+
:param int report_id: Report ID of the report being read.
144+
:param int length: Maximum length of the Input report to read.
145+
146+
:rtype: list(byte)
147+
:return: List of bytes containing the read Feature report. The
148+
first byte of the report will be the Input ID of the
149+
report that was read.
150+
"""
151+
pass
152+
138153
@abstractmethod
139154
def write(self, payload):
140155
"""

0 commit comments

Comments
 (0)