Skip to content

Commit bac87c8

Browse files
Vlad Ilieabcminiuser
authored andcommitted
Complete support for StreamDeck Neo
1 parent 6131c16 commit bac87c8

File tree

11 files changed

+400
-3
lines changed

11 files changed

+400
-3
lines changed

src/StreamDeck/Devices/StreamDeck.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class StreamDeck(ABC):
5252
KEY_COLS = 0
5353
KEY_ROWS = 0
5454

55+
TOUCH_KEY_COUNT = 0
56+
5557
KEY_PIXEL_WIDTH = 0
5658
KEY_PIXEL_HEIGHT = 0
5759
KEY_IMAGE_FORMAT = ""
@@ -64,6 +66,12 @@ class StreamDeck(ABC):
6466
TOUCHSCREEN_FLIP = (False, False)
6567
TOUCHSCREEN_ROTATION = 0
6668

69+
SCREEN_PIXEL_WIDTH = 0
70+
SCREEN_PIXEL_HEIGHT = 0
71+
SCREEN_IMAGE_FORMAT = ""
72+
SCREEN_FLIP = (False, False)
73+
SCREEN_ROTATION = 0
74+
6775
DIAL_COUNT = 0
6876

6977
DECK_TYPE = ""
@@ -72,7 +80,7 @@ class StreamDeck(ABC):
7280

7381
def __init__(self, device):
7482
self.device = device
75-
self.last_key_states = [False] * self.KEY_COUNT
83+
self.last_key_states = [False] * (self.KEY_COUNT + self.TOUCH_KEY_COUNT)
7684
self.last_dial_states = [False] * self.DIAL_COUNT
7785
self.read_thread = None
7886
self.run_read_thread = False
@@ -292,6 +300,15 @@ def key_count(self):
292300
:return: Number of physical buttons.
293301
"""
294302
return self.KEY_COUNT
303+
304+
def touch_key_count(self):
305+
"""
306+
Retrieves number of touch buttons on the attached StreamDeck device.
307+
308+
:rtype: int
309+
:return: Number of touch buttons.
310+
"""
311+
return self.TOUCH_KEY_COUNT
295312

296313
def dial_count(self):
297314
"""
@@ -376,6 +393,26 @@ def touchscreen_image_format(self):
376393
'flip': self.TOUCHSCREEN_FLIP,
377394
'rotation': self.TOUCHSCREEN_ROTATION,
378395
}
396+
397+
def screen_image_format(self):
398+
"""
399+
Retrieves the image format accepted by the screen of the Stream
400+
Deck. Images should be given in this format when drawing on
401+
screen.
402+
403+
.. seealso:: See :func:`~StreamDeck.set_screen_image` method to
404+
draw an image on the StreamDeck screen.
405+
406+
:rtype: dict()
407+
:return: Dictionary describing the various image parameters
408+
(size, image format).
409+
"""
410+
return {
411+
'size': (self.SCREEN_PIXEL_WIDTH, self.SCREEN_PIXEL_HEIGHT),
412+
'format': self.SCREEN_IMAGE_FORMAT,
413+
'flip': self.SCREEN_FLIP,
414+
'rotation': self.SCREEN_ROTATION,
415+
}
379416

380417
def set_poll_frequency(self, hz):
381418
"""
@@ -619,3 +656,30 @@ def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0):
619656
620657
"""
621658
pass
659+
660+
@abstractmethod
661+
def set_key_color(self, key, r, g, b):
662+
"""
663+
Sets the color of the touch buttons. These buttons are indexed
664+
in order after the standard keys.
665+
666+
:param int key: Index of the button
667+
:param int r: Red value
668+
:param int g: Green value
669+
:param int b: Blue value
670+
671+
"""
672+
pass
673+
674+
@abstractmethod
675+
def set_screen_image(self, image):
676+
"""
677+
Draws an image on the touchless screen of the StreamDeck.
678+
679+
.. seealso:: See :func:`~StreamDeck.screen_image_format` method for
680+
information on the image format accepted by the device.
681+
682+
:param enumerable image: Raw data of the image to set on the button.
683+
If `None`, the screen will be cleared.
684+
"""
685+
pass

src/StreamDeck/Devices/StreamDeckMini.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,9 @@ def set_key_image(self, key, image):
121121

122122
def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0):
123123
pass
124+
125+
def set_key_color(self, key, r, g, b):
126+
pass
127+
128+
def set_screen_image(self, image):
129+
pass

src/StreamDeck/Devices/StreamDeckNeo.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class StreamDeckNeo(StreamDeck):
1717
KEY_COLS = 4
1818
KEY_ROWS = 2
1919

20+
TOUCH_KEY_COUNT = 2
21+
2022
KEY_PIXEL_WIDTH = 96
2123
KEY_PIXEL_HEIGHT = 96
2224
KEY_IMAGE_FORMAT = "JPEG"
@@ -26,6 +28,12 @@ class StreamDeckNeo(StreamDeck):
2628
DECK_TYPE = "Stream Deck Neo"
2729
DECK_VISUAL = True
2830

31+
SCREEN_PIXEL_WIDTH = 248
32+
SCREEN_PIXEL_HEIGHT = 58
33+
SCREEN_IMAGE_FORMAT = "JPEG"
34+
SCREEN_FLIP = (True, True)
35+
SCREEN_ROTATION = 0
36+
2937
IMAGE_REPORT_LENGTH = 1024
3038
IMAGE_REPORT_HEADER_LENGTH = 8
3139
IMAGE_REPORT_PAYLOAD_LENGTH = IMAGE_REPORT_LENGTH - IMAGE_REPORT_HEADER_LENGTH
@@ -75,8 +83,24 @@ class StreamDeckNeo(StreamDeck):
7583
0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x0f, 0xff, 0xd9
7684
]
7785

86+
# 248 x 58 black JPEG
87+
BLANK_SCREEN_IMAGE = [
88+
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
89+
0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08,
90+
0x0a, 0x0c, 0x14, 0x0d, 0x0c, 0x0b, 0x0b, 0x0c, 0x19, 0x12, 0x13, 0x0f, 0x14, 0x1d, 0x1a, 0x1f, 0x1e, 0x1d, 0x1a,
91+
0x1c, 0x1c, 0x20, 0x24, 0x2e, 0x27, 0x20, 0x22, 0x2c, 0x23, 0x1c, 0x1c, 0x28, 0x37, 0x29, 0x2c, 0x30, 0x31, 0x34,
92+
0x34, 0x34, 0x1f, 0x27, 0x39, 0x3d, 0x38, 0x32, 0x3c, 0x2e, 0x33, 0x34, 0x32, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00,
93+
0x3a, 0x00, 0xf8, 0x01, 0x01, 0x11, 0x00, 0xff, 0xc4, 0x00, 0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
94+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0xc4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00,
95+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xda, 0x00, 0x08, 0x01,
96+
0x01, 0x00, 0x00, 0x3f, 0x00, 0x9f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
97+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
98+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
99+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xd9
100+
]
101+
78102
def _read_control_states(self):
79-
states = self.device.read(4 + self.KEY_COUNT)
103+
states = self.device.read(4 + self.KEY_COUNT + self.TOUCH_KEY_COUNT)
80104
if states is None:
81105
return None
82106

@@ -114,7 +138,7 @@ def get_firmware_version(self):
114138
return self._extract_string(version[6:])
115139

116140
def set_key_image(self, key, image):
117-
if min(max(key, 0), self.KEY_COUNT) != key:
141+
if min(max(key, 0), self.KEY_COUNT - 1) != key:
118142
raise IndexError("Invalid key index {}.".format(key))
119143

120144
image = bytes(image or self.BLANK_KEY_IMAGE)
@@ -143,5 +167,46 @@ def set_key_image(self, key, image):
143167
bytes_remaining = bytes_remaining - this_length
144168
page_number = page_number + 1
145169

170+
def set_key_color(self, key, r, g, b):
171+
if min(max(key, 0), self.KEY_COUNT + self.TOUCH_KEY_COUNT - 1) != key:
172+
raise IndexError("Invalid touch key index {}.".format(key))
173+
174+
if r > 255 or r < 0 or g > 255 or g < 0 or b > 255 or b < 0:
175+
raise ValueError("Invalid color")
176+
177+
payload = bytearray(32)
178+
payload[0:6] = [0x03, 0x06, key, r, g, b]
179+
self.device.write_feature(payload)
180+
181+
def set_screen_image(self, image):
182+
if not image:
183+
image = self.BLANK_SCREEN_IMAGE
184+
185+
image = bytes(image)
186+
187+
page_number = 0
188+
bytes_remaining = len(image)
189+
while bytes_remaining > 0:
190+
this_length = min(bytes_remaining, self.IMAGE_REPORT_PAYLOAD_LENGTH)
191+
bytes_sent = page_number * self.IMAGE_REPORT_PAYLOAD_LENGTH
192+
193+
header = [
194+
0x02, # 0
195+
0x0b, # 1
196+
0x00, # 2
197+
0x01 if this_length == bytes_remaining else 0x00, # 3 is the last report?
198+
this_length & 0xff, # 5 bytecount high byte
199+
(this_length >> 8) & 0xff, # 4 bytecount high byte
200+
page_number & 0xff, # 7 pagenumber low byte
201+
(page_number >> 8) & 0xff # 6 pagenumber high byte
202+
]
203+
204+
payload = bytes(header) + image[bytes_sent:bytes_sent + this_length]
205+
padding = bytearray(self.IMAGE_REPORT_LENGTH - len(payload))
206+
self.device.write(payload + padding)
207+
208+
bytes_remaining = bytes_remaining - this_length
209+
page_number = page_number + 1
210+
146211
def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0):
147212
pass

src/StreamDeck/Devices/StreamDeckOriginal.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,9 @@ def set_key_image(self, key, image):
125125

126126
def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0):
127127
pass
128+
129+
def set_key_color(self, key, r, g, b):
130+
pass
131+
132+
def set_screen_image(self, image):
133+
pass

src/StreamDeck/Devices/StreamDeckOriginalV2.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,9 @@ def set_key_image(self, key, image):
143143

144144
def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0):
145145
pass
146+
147+
def set_key_color(self, key, r, g, b):
148+
pass
149+
150+
def set_screen_image(self, image):
151+
pass

src/StreamDeck/Devices/StreamDeckPedal.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,9 @@ def set_key_image(self, key, image):
5252

5353
def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0):
5454
pass
55+
56+
def set_key_color(self, key, r, g, b):
57+
pass
58+
59+
def set_screen_image(self, image):
60+
pass

src/StreamDeck/Devices/StreamDeckPlus.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,3 +490,9 @@ def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0):
490490

491491
bytes_remaining = bytes_remaining - this_length
492492
page_number = page_number + 1
493+
494+
def set_key_color(self, key, r, g, b):
495+
pass
496+
497+
def set_screen_image(self, image):
498+
pass

src/StreamDeck/Devices/StreamDeckXL.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,9 @@ def set_key_image(self, key, image):
145145

146146
def set_touchscreen_image(self, image, x_pos=0, y_pos=0, width=0, height=0):
147147
pass
148+
149+
def set_key_color(self, key, r, g, b):
150+
pass
151+
152+
def set_screen_image(self, image):
153+
pass

src/StreamDeck/ImageHelpers/PILHelper.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,24 @@ def create_touchscreen_image(deck, background='black'):
9696
return _create_image(deck.touchscreen_image_format(), background)
9797

9898

99+
def create_screen_image(deck, background='black'):
100+
"""
101+
Creates a new PIL Image with the correct image dimensions for the given
102+
StreamDeck device's creen.
103+
104+
.. seealso:: See :func:`~PILHelper.to_native_screen_format` method for converting a
105+
PIL image instance to the native screen image format of a given
106+
StreamDeck device.
107+
108+
:param StreamDeck deck: StreamDeck device to generate a compatible image for.
109+
:param str background: Background color to use, compatible with `PIL.Image.new()`.
110+
111+
:rtype: PIL.Image
112+
:return: Created PIL image
113+
"""
114+
return _create_image(deck.screen_image_format(), background)
115+
116+
99117
def create_scaled_image(deck, image, margins=[0, 0, 0, 0], background='black'):
100118
"""
101119
.. deprecated:: 0.9.5
@@ -152,6 +170,30 @@ def create_scaled_touchscreen_image(deck, image, margins=[0, 0, 0, 0], backgroun
152170
return _scale_image(image, deck.touchscreen_image_format(), margins, background)
153171

154172

173+
def create_scaled_screen_image(deck, image, margins=[0, 0, 0, 0], background='black'):
174+
"""
175+
Creates a new screen image that contains a scaled version of a given image,
176+
resized to best fit the given StreamDeck device's screen with the given
177+
margins around each side.
178+
179+
The scaled image is centered within the new screen image, offset by the given
180+
margins. The aspect ratio of the image is preserved.
181+
182+
.. seealso:: See :func:`~PILHelper.to_native_screen_format` method for converting a
183+
PIL image instance to the native key image format of a given
184+
StreamDeck device.
185+
186+
:param StreamDeck deck: StreamDeck device to generate a compatible image for.
187+
:param Image image: PIL Image object to scale
188+
:param list(int): Array of margin pixels in (top, right, bottom, left) order.
189+
:param str background: Background color to use, compatible with `PIL.Image.new()`.
190+
191+
:rtrype: PIL.Image
192+
:return: Loaded PIL image scaled and centered
193+
"""
194+
return _scale_image(image, deck.screen_image_format(), margins, background)
195+
196+
155197
def to_native_format(deck, image):
156198
"""
157199
.. deprecated:: 0.9.5
@@ -192,3 +234,19 @@ def to_native_touchscreen_format(deck, image):
192234
:return: Image converted to the given StreamDeck's native touchscreen format
193235
"""
194236
return _to_native_format(image, deck.touchscreen_image_format())
237+
238+
def to_native_screen_format(deck, image):
239+
"""
240+
Converts a given PIL image to the native screen image format for a StreamDeck,
241+
suitable for passing to :func:`~StreamDeck.set_screen_image`.
242+
243+
.. seealso:: See :func:`~PILHelper.create_screen_image` method for creating a PIL
244+
image instance for a given StreamDeck device.
245+
246+
:param StreamDeck deck: StreamDeck device to generate a compatible native image for.
247+
:param PIL.Image image: PIL Image to convert to the native StreamDeck image format
248+
249+
:rtype: enumerable()
250+
:return: Image converted to the given StreamDeck's native screen format
251+
"""
252+
return _to_native_format(image, deck.screen_image_format())

src/example_basic.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ def key_change_callback(deck, key, state):
8585
# Print new key state
8686
print("Deck {} Key {} = {}".format(deck.id(), key, state), flush=True)
8787

88+
# Don't try to draw an image on a touch button
89+
if key >= deck.key_count():
90+
return
91+
8892
# Update the key image based on the new key state.
8993
update_key_image(deck, key, state)
9094

0 commit comments

Comments
 (0)