-
Notifications
You must be signed in to change notification settings - Fork 2
/
lenovolight.py
251 lines (207 loc) · 7.15 KB
/
lenovolight.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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
#!/usr/bin/env python
#
# Lenovo Ideapad / Legion Gaming 2021 keyboard light controller
# INS, 2021, MIT
#
# Add udev rule as "/etc/udev/rules.d/10-kblight.rules" if you want control light as user
# SUBSYSTEM=="usb", ATTR{idVendor}=="048d", ATTR{idProduct}=="c965", MODE="0666"
#
# Payload description
#
# HEADER ........... cc
# HEADER ........... 16
# EFFECT ........... 01 - static / 03 - breath / 04 - wave / 06 - hue
# SPEED ............ 01 / 02 / 03 / 04
# BRIGHTNESS ....... 01 / 02
# RED SECTION 1 .... 00-ff
# GREEN SECTION 1 .. 00-ff
# BLUE SECTION 1 ... 00-ff
# RED SECTION 2 .... 00-ff
# GREEN SECTION 2 .. 00-ff
# BLUE SECTION 2 ... 00-ff
# RED SECTION 3 .... 00-ff
# GREEN SECTION 3 .. 00-ff
# BLUE SECTION 3 ... 00-ff
# RED SECTION 4 .... 00-ff
# GREEN SECTION 4 .. 00-ff
# BLUE SECTION 4 ... 00-ff
# UNUSED ........... 00
# WAVE MODE RTL .... 00 / 01
# WAVE MODE LTR .... 00 / 01
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
# UNUSED ........... 00
#
import re
import usb.core
class LedController:
# Keyboard light device
# Integrated Technology Express, Inc. ITE Device(8295)
#WARNING
#replacing with your VENDOR and PRODUCT might be needed!
VENDOR = 0x048D
PRODUCT = 0xC963
EFFECT = {"static": 1, "breath": 3, "wave": 4, "hue": 6}
def __init__(self):
device = usb.core.find(idVendor=self.VENDOR, idProduct=self.PRODUCT)
if device is None:
raise ValueError("Light device not found")
#error if vendor and product not correct or not supported ( mostly not correct )
# Prevent usb.core.USBError: [Errno 16] Resource busy
if device.is_kernel_driver_active(0):
device.detach_kernel_driver(0)
self.device = device
# Build light device control string
def build_control_string(
self,
effect,
colors=None,
speed=1,
brightness=1,
wave_direction=None,
):
data = [204, 22]
if effect == "off":
data.append(self.EFFECT["static"])
data += [0] * 30
return data
data.append(self.EFFECT[effect])
data.append(speed)
data.append(brightness)
if effect not in ["static", "breath"]:
data += [0] * 12
else:
chunk = None
for section in range(0, 4):
if section < len(colors):
color = colors[section].lower()
model = None
# Detect color model
if re.match(r"^[0-9a-f]{6}$", color):
# HEX model
chunk = [
int(color[i : i + 2], 16) for i in range(0, len(color), 2)
]
else:
components = color.split(",")
if components[0].isdigit():
# RGB model
components = list(map(lambda c: int(c), components))
# Validate RGB input
for component in components:
if not 0 <= component <= 255:
raise ValueError(
f"Invalid RGB color model: {color}"
)
chunk = list(components)
elif re.match(r"^\d+\.\d+$", components[0]):
# HSV model
components = list(map(lambda c: float(c), components))
# Validate HSV input
for component in components:
if not 0 <= component <= 1:
raise ValueError(
f"Invalid HSV color model: {color}"
)
from colorsys import hsv_to_rgb
chunk = list(
map(lambda c: int(c * 255), hsv_to_rgb(*components))
)
else:
raise ValueError(f"Invalid color model: {color}")
data += chunk
# Unused
data += [0]
# Wave direction
if wave_direction == "rtl":
data += [1, 0]
elif wave_direction == "ltr":
data += [0, 1]
else:
data += [0, 0]
# Unused
data += [0] * 13
return data
# Send command to device
def send_control_string(self, data):
self.device.ctrl_transfer(
bmRequestType=0x21,
bRequest=0x9,
wValue=0x03CC,
wIndex=0x00,
data_or_wLength=data,
)
# CLI Stuff
if __name__ == "__main__":
import argparse
# Parse arguments
argparser = argparse.ArgumentParser(
description="Lenovo Legion 5 Pro 2021 keyboard light controller"
)
effect_subparsers = argparser.add_subparsers(help="Light effect", dest="effect")
# Global options
global_parser = argparse.ArgumentParser(add_help=False)
global_parser.add_argument(
"--brightness",
type=int,
choices=range(1, 3),
default=1,
help="Light brightness",
)
# Options for custom color settings only
custom_parser = argparse.ArgumentParser(add_help=False)
custom_parser.add_argument("colors", nargs="+", help="Colors of sections")
# Options for wave effect
wave_parser = argparse.ArgumentParser(add_help=False)
wave_parser.add_argument(
"direction",
type=str,
choices=["ltr", "rtl"],
help="Direction of wave",
)
# Options for animated effects
animated_parser = argparse.ArgumentParser(add_help=False)
animated_parser.add_argument(
"--speed", type=int, choices=range(1, 5), default=1, help="Animation speed"
)
# Effects
effect_subparsers.add_parser(
"static", help="Static color", parents=[global_parser, custom_parser]
)
effect_subparsers.add_parser(
"breath",
help="Fade light in and out",
parents=[global_parser, custom_parser, animated_parser],
)
effect_subparsers.add_parser(
"hue",
help="Transition across hue circle",
parents=[global_parser, animated_parser],
)
effect_subparsers.add_parser(
"wave",
help="Rainbow wawe",
parents=[global_parser, animated_parser, wave_parser],
)
effect_subparsers.add_parser("off", help="Turn light off")
args = argparser.parse_args()
# Use controller
controller = LedController()
data = controller.build_control_string(
effect=args.effect,
colors=getattr(args, "colors", None),
speed=getattr(args, "speed", 1),
brightness=getattr(args, "brightness", 1),
wave_direction=getattr(args, "direction", None),
)
controller.send_control_string(data)