-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathvkb_led_init.py
232 lines (190 loc) · 7.48 KB
/
vkb_led_init.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import os
import struct
from enum import IntEnum, auto
from textwrap import wrap
import bitstruct as bs
from pywinusb.hid import HidDevice
LED_CONFIG_COUNT = 16
LED_REPORT_ID = 0x59
LED_REPORT_LEN = 129
LED_SET_OP_CODE = bytes.fromhex("59a50a")
# TODO: it really looks like the 4 bytes after the op code are random, but using random numbers doesn't work
# this... however... always works, so <shrugs>
LED_SET_UKN_CODE = bytes.fromhex("6e4d349a")
class ColorMode(IntEnum):
COLOR1 = 0
COLOR2 = auto()
COLOR1_d_2 = auto()
COLOR2_d_1 = auto()
COLOR1_p_2 = auto()
class LEDMode(IntEnum):
OFF = 0
CONSTANT = auto()
SLOW_BLINK = auto()
FAST_BLINK = auto()
ULTRA_BLINK = auto()
class LEDConfig:
"""
An configuration for an LED in a VKB device.
Setting the LEDs consists of a possible 30 LED configs each of which is a 4 byte structure as follows:
byte 0: LED ID
bytes 2-4: a 24 bit color config as follows:
000 001 010 011 100 101 110 111
clm lem b2 g2 r2 b1 g1 r1
color mode (clm):
0 - color1
1 - color2
2 - color1/2
3 - color2/1
4 - color1+2
led mode (lem):
0 - off
1 - constant
2 - slow blink
3 - fast blink
4 - ultra fast
Colors
------
VKB uses a simple RGB color configuration for all LEDs. Non-rgb LEDs will not light if you set their primary color
to 0 in the color config. The LEDs have a very reduced color range due to VKB using 0-7 to determine the brightness
of R, G, and B. `LEDConfig` takes in a standard hex color code and converts it into this smaller range, so color
reproduction will not be accurate.
Bytes
-----
Convert the LEDConfig to an appropriate binary representation by using `bytes`::
>>> led1 = LEDConfig(led=1, led_mode=LEDMode.CONSTANT)
>>> bytes(led1)
b'\x01\x07p\x04'
:param led: `int` ID of the LED to control
:param color_mode: `int` color mode, see :class:`ColorMode`
:param led_mode: `int` LED mode, see :class:`LEDMode`
:param color1: `str` hex color code for use as `color1`
:param color2: `str` hex color code for use as `color2`
"""
def __init__(
self,
led: int,
color_mode: int = 0,
led_mode: int = 0,
color1: str = None,
color2: str = None,
):
self.led = int(led)
self.color_mode = ColorMode(color_mode)
self.led_mode = LEDMode(led_mode)
self.color1 = color1 or "#000"
self.color2 = color2 or "#000"
def __repr__(self):
return (
f"<LEDConfig {self.led} clm:{ColorMode(self.color_mode).name} lem:{LEDMode(self.led_mode).name} "
f"color1:{self.color1} color2:{self.color2}>"
)
def __bytes__(self):
return struct.pack(">B", self.led) + bs.byteswap(
"3",
bs.pack(
"u3" * 8,
self.color_mode,
self.led_mode,
*(hex_color_to_vkb_color(self.color2)[::-1]),
*(hex_color_to_vkb_color(self.color1)[::-1]),
),
)
@classmethod
def frombytes(cls, buf):
""" Creates an :class:`LEDConfig` from a bytes object """
assert len(buf) == 4
led_id = int(buf[0])
buf = bs.byteswap("3", buf[1:])
clm, lem, b2, g2, r2, b1, g1, r1 = bs.unpack("u3" * 8, buf)
return cls(
led=led_id,
color_mode=clm,
led_mode=lem,
color1=vkb_color_to_hex_color([r1, g1, b1]),
color2=vkb_color_to_hex_color([r2, g2, b2]),
)
def get_led_configs(dev: HidDevice):
"""
Returns a list of :class:`LEDConfig`s that have currently been set on the given `dev` :class:`HidDevice`.
.. warning::
This will only return LED configs that have been set into the device. Either with this library or with the
test LED tool in VKBDevCfg. LED profiles that are configured normally are not retrieved in this manor and not
configured the same way.
"""
led_report = [
_ for _ in dev.find_feature_reports() if _.report_id == LED_REPORT_ID
][0]
data = bytes(led_report.get(False))
assert len(data) >= 8
if data[:3] != LED_SET_OP_CODE:
return (
[]
) # it wont be returned until something is set, so default to showing no configs
num_led_configs = int(data[7])
data = data[8:]
leds = []
while num_led_configs > 0:
leds.append(LEDConfig.frombytes(data[:4]))
data = data[4:]
num_led_configs -= 1
return leds
def _led_conf_checksum(num_configs, buf):
"""
It's better not to ask too many questions about this. Why didn't they just use a CRC?
Why do we only check (num_configs+1)*3 bytes instead of the whole buffer?
We may never know... it works...
"""
def conf_checksum_bit(chk, b):
chk ^= b
for i in range(8):
_ = chk & 1
chk >>= 1
if _ != 0:
chk ^= 0xA001
return chk
chk = 0xFFFF
for i in range((num_configs + 1) * 3):
chk = conf_checksum_bit(chk, buf[i])
return struct.pack("<H", chk)
def set_leds(dev: HidDevice, led_configs: [LEDConfig]):
f"""
Set the :class:`LEDConfig`s to a `dev` HidDevice.
A maximum of {LED_CONFIG_COUNT} configs can be set at once.
.. warning::
This will overwrite the state for each given LED until the device is turned off/on again. Only the specified
LEDs are affected, and all other LEDs will continue to behave as configured in the device profile.
:param dev: :class:`usb.core.HidDevice` for a connected VKB device
:param led_configs: List of :class:`LEDConfigs` to set.
"""
if len(led_configs) > LED_CONFIG_COUNT:
raise ValueError(f"Can only set a maximum of {LED_CONFIG_COUNT} LED configs")
num_configs = len(led_configs)
led_configs = b"".join(bytes(_) for _ in led_configs)
# see note about trying random above
# LED_SET_OP_CODE + struct.pack('>4B', *[random.randint(1, 255) for _ in range(4)]) + led_configs
led_configs = os.urandom(2) + struct.pack(">B", num_configs) + led_configs
chksum = _led_conf_checksum(num_configs, led_configs)
cmd = LED_SET_OP_CODE + chksum + led_configs
cmd = cmd + b"\x00" * (LED_REPORT_LEN - len(cmd))
led_report = [
_ for _ in dev.find_feature_reports() if _.report_id == LED_REPORT_ID
][0]
led_report.send(cmd)
def hex_color_to_vkb_color(hex_code: str) -> [int]:
""" Takes a hex formatted color and converts it to VKB color format.
>>> hex_color_to_led("#FF3300")
[7, 1, 0]
:param hex_code: Hex color code string to be converted to a VKB color code
:return: List of integers in a VKB LED color code format, `[R, G, B]`
"""
hex_code = hex_code.lstrip("#")
if len(hex_code) != 3 and len(hex_code) != 6:
return ValueError("Invalid hex color code")
hex_code = [_ + _ for _ in hex_code] if len(hex_code) == 3 else wrap(hex_code, 2)
# VKB uses a 7 as the max brightness and 0 as off - so convert the 255 range to this
hex_code = [round(min(int(_, 16), 255) / 255.0 * 7) for _ in hex_code]
return hex_code
def vkb_color_to_hex_color(vkb_color_code: [int]) -> str:
""" Takes a VKB color code `[R, G, B]` and converts it into a hex code string"""
return f'#{"".join("%02x" % round((min(int(i), 7)/7.0) * 255) for i in vkb_color_code)}'