razer-analog 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- razer_analog-0.1.0/PKG-INFO +15 -0
- razer_analog-0.1.0/pyproject.toml +32 -0
- razer_analog-0.1.0/razer_analog/__init__.py +0 -0
- razer_analog-0.1.0/razer_analog/driver.py +399 -0
- razer_analog-0.1.0/razer_analog/hidraw.py +138 -0
- razer_analog-0.1.0/razer_analog/hidrawtests.py +86 -0
- razer_analog-0.1.0/razer_analog/layout.py +56 -0
- razer_analog-0.1.0/razer_analog/mouse.py +419 -0
- razer_analog-0.1.0/razer_analog/razer_huntsman_mini_analog.json +19 -0
- razer_analog-0.1.0/razer_analog/razerctl.py +165 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: razer-analog
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
License: GPL-2
|
|
6
|
+
Author: Dick Marinus
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: License :: Other/Proprietary License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Dist: evdev (>=1.6.0,<2.0.0)
|
|
14
|
+
Requires-Dist: ioctl-opt (>=1.2.2,<2.0.0)
|
|
15
|
+
Requires-Dist: pyudev (>=0.24.0,<0.25.0)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "razer-analog"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["Dick Marinus"]
|
|
6
|
+
license = "GPL-2"
|
|
7
|
+
#readme = "README.md"
|
|
8
|
+
#packages = [{include = "razer_analog"}]
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = "^3.10"
|
|
12
|
+
ioctl-opt = "^1.2.2"
|
|
13
|
+
evdev = "^1.6.0"
|
|
14
|
+
pyudev = "^0.24.0"
|
|
15
|
+
|
|
16
|
+
[tool.poetry.group.dev.dependencies]
|
|
17
|
+
pylint = "^2.15.10"
|
|
18
|
+
mypy = "^0.991"
|
|
19
|
+
coverage = "^7.0.5"
|
|
20
|
+
pytest = "^7.2.1"
|
|
21
|
+
pytest-cov = "^4.0.0"
|
|
22
|
+
black = "^23.1.0"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["poetry-core"]
|
|
26
|
+
build-backend = "poetry.core.masonry.api"
|
|
27
|
+
|
|
28
|
+
[tool.poetry.scripts]
|
|
29
|
+
razer-analog = 'razer_analog.driver:main'
|
|
30
|
+
razer-analog-mouse = 'razer_analog.mouse:main'
|
|
31
|
+
razerctl = 'razer_analog.razerctl:main'
|
|
32
|
+
hidrawtests = 'razer_analog.hidrawtests:main'
|
|
File without changes
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Razer Analog driver
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
import struct
|
|
7
|
+
import atexit
|
|
8
|
+
import signal
|
|
9
|
+
import sys
|
|
10
|
+
import typing
|
|
11
|
+
import io
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
import evdev # type: ignore
|
|
15
|
+
import pyudev # type: ignore
|
|
16
|
+
|
|
17
|
+
import razer_analog.layout
|
|
18
|
+
import razer_analog.hidraw
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HuntsmanMiniAnalog:
|
|
22
|
+
"""
|
|
23
|
+
Huntsman Mini Analog class
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self, pressed_queue: asyncio.Queue[typing.Dict[int, int]], parent: str
|
|
28
|
+
):
|
|
29
|
+
self.report_queue: asyncio.Queue[bytes] = asyncio.Queue()
|
|
30
|
+
self.pressed_queue: asyncio.Queue[typing.Dict[int, int]] = pressed_queue
|
|
31
|
+
self.devices: typing.List[io.BufferedReader] = []
|
|
32
|
+
self.context = pyudev.Context()
|
|
33
|
+
self.parent = pyudev.Devices.from_path(self.context, parent)
|
|
34
|
+
|
|
35
|
+
self.open()
|
|
36
|
+
|
|
37
|
+
self.monitor = pyudev.Monitor.from_netlink(self.context)
|
|
38
|
+
self.monitor.filter_by(subsystem="hidraw")
|
|
39
|
+
self.monitor.start()
|
|
40
|
+
|
|
41
|
+
def open(self) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Open device
|
|
44
|
+
"""
|
|
45
|
+
self.devices = []
|
|
46
|
+
loop = asyncio.get_event_loop()
|
|
47
|
+
for udev in self.context.list_devices(parent=self.parent, subsystem="hidraw"):
|
|
48
|
+
# pylint: disable=consider-using-with
|
|
49
|
+
device_handle = open(udev.device_node, "rb")
|
|
50
|
+
os.set_blocking(device_handle.fileno(), False)
|
|
51
|
+
hidraw = razer_analog.hidraw.HIDRaw(device_handle)
|
|
52
|
+
i = hidraw.getInfo()
|
|
53
|
+
if i.vendor == 0x1532 and i.product == 0x0282:
|
|
54
|
+
self.devices.append(device_handle)
|
|
55
|
+
loop.add_reader(device_handle.fileno(), self.read, device_handle)
|
|
56
|
+
desc = hidraw.getRawReportDescriptor()
|
|
57
|
+
# I'd like to parse these using python-hid-parser but that needs to be
|
|
58
|
+
# fixed in: https://github.com/usb-tools/python-hid-parser/pull/17
|
|
59
|
+
# fmt: off
|
|
60
|
+
if list(desc) == [5, 12, 9, 1, 161, 1, 6, 0, 255, 9, 2, 21, 0, 37, 1,
|
|
61
|
+
117, 8, 149, 90, 177, 1, 192,
|
|
62
|
+
]:
|
|
63
|
+
# put control interface first
|
|
64
|
+
self.devices[0], self.devices[-1] = self.devices[-1], self.devices[0]
|
|
65
|
+
elif list(desc) == [ 5, 1, 9, 6, 161, 1, 133, 1, 5, 7, 5, 7, 25, 224, 41,
|
|
66
|
+
231, 21, 0, 37, 1, 117, 1, 149, 8, 129, 2, 25, 0, 41, 160, 21,
|
|
67
|
+
0, 37, 1, 117, 1, 149, 160, 129, 2, 117, 8, 149, 2, 129, 1, 5,
|
|
68
|
+
8, 25, 1, 41, 3, 21, 0, 37, 1, 117, 1, 149, 3, 145, 2, 149, 5,
|
|
69
|
+
145, 1, 192, 5, 12, 9, 1, 161, 1, 133, 2, 25, 0, 42, 60, 2, 21,
|
|
70
|
+
0, 38, 60, 2, 149, 1, 117, 16, 129, 0, 117, 8, 149, 21, 129, 1,
|
|
71
|
+
192, 5, 1, 9, 128, 161, 1, 133, 3, 25, 129, 41, 131, 21, 0, 37,
|
|
72
|
+
1, 117, 1, 149, 3, 129, 2, 149, 5, 129, 1, 117, 8, 149, 22, 129,
|
|
73
|
+
1, 192, 5, 1, 9, 0, 161, 1, 133, 4, 9, 3, 21, 0, 38, 255, 0, 53,
|
|
74
|
+
0, 70, 255, 0, 117, 8, 149, 23, 129, 0, 192, 5, 1, 9, 0, 161, 1,
|
|
75
|
+
133, 5, 9, 3, 21, 0, 38, 255, 0, 53, 0, 70, 255, 0, 117, 8, 149,
|
|
76
|
+
23, 129, 0, 192, 5, 1, 9, 0, 161, 1, 133, 7, 9, 3, 21, 0, 38,
|
|
77
|
+
255, 0, 53, 0, 70, 255, 0, 117, 8, 149, 23, 129, 0, 192,
|
|
78
|
+
]:
|
|
79
|
+
pass
|
|
80
|
+
# fmt: on
|
|
81
|
+
else:
|
|
82
|
+
device_handle.close()
|
|
83
|
+
|
|
84
|
+
def close(self) -> None:
|
|
85
|
+
"""
|
|
86
|
+
close device
|
|
87
|
+
"""
|
|
88
|
+
loop = asyncio.get_event_loop()
|
|
89
|
+
for device in self.devices:
|
|
90
|
+
loop.remove_reader(device.fileno())
|
|
91
|
+
device.close()
|
|
92
|
+
self.devices = []
|
|
93
|
+
|
|
94
|
+
def read(self, device_handle: io.BufferedReader) -> None:
|
|
95
|
+
"""
|
|
96
|
+
read from device
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
buf = device_handle.read(2048)
|
|
100
|
+
except OSError:
|
|
101
|
+
print("failed to read, exiting")
|
|
102
|
+
self.close()
|
|
103
|
+
sys.exit()
|
|
104
|
+
while buf:
|
|
105
|
+
if buf[0] == 0x04:
|
|
106
|
+
pass # For some reason presses of Fn are reported here
|
|
107
|
+
elif buf[0] == 0x07:
|
|
108
|
+
self.report_queue.put_nowait(buf[1:23])
|
|
109
|
+
else:
|
|
110
|
+
print([buf])
|
|
111
|
+
buf = buf[24:]
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def crc(buf: bytes) -> int:
|
|
115
|
+
"""
|
|
116
|
+
Calculate Razer CRC
|
|
117
|
+
"""
|
|
118
|
+
result = 0
|
|
119
|
+
for i in range(2, 86):
|
|
120
|
+
result ^= buf[i]
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
def razer_command(self, data: bytes) -> bytes:
|
|
124
|
+
"""
|
|
125
|
+
Send a razer command
|
|
126
|
+
"""
|
|
127
|
+
if not self.devices:
|
|
128
|
+
print("cannot send command, no devices attached")
|
|
129
|
+
return b""
|
|
130
|
+
|
|
131
|
+
hidraw = razer_analog.hidraw.HIDRaw(self.devices[0])
|
|
132
|
+
send_report = struct.pack(
|
|
133
|
+
">BBHBB",
|
|
134
|
+
0x00, # status
|
|
135
|
+
0x1F, # transaction_id
|
|
136
|
+
0x00, # remaining_packets
|
|
137
|
+
0x00, # protocol_type
|
|
138
|
+
len(data) - 2, # size
|
|
139
|
+
)
|
|
140
|
+
send_report += data
|
|
141
|
+
send_report += b"\x00" * (82 - len(data))
|
|
142
|
+
send_report += bytes([self.crc(send_report)])
|
|
143
|
+
send_report += b"\x00"
|
|
144
|
+
hidraw.sendFeatureReport(send_report)
|
|
145
|
+
received_report = hidraw.getFeatureReport(0, 90)
|
|
146
|
+
|
|
147
|
+
if received_report[1] != 0x02: # SUCCESS
|
|
148
|
+
print(
|
|
149
|
+
f"Failed razer_command ({received_report[1]}). received_report, send_report"
|
|
150
|
+
)
|
|
151
|
+
print(received_report)
|
|
152
|
+
print(send_report)
|
|
153
|
+
|
|
154
|
+
return received_report
|
|
155
|
+
|
|
156
|
+
def set_device_mode(self, mode: int) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Set device mode
|
|
159
|
+
"""
|
|
160
|
+
data = b"\x00\x04" + bytes((mode, 0x00))
|
|
161
|
+
received_report = self.razer_command(data)[7:11]
|
|
162
|
+
if received_report != data:
|
|
163
|
+
print("set_device_mode mismatch", data, received_report)
|
|
164
|
+
|
|
165
|
+
async def run(self) -> None:
|
|
166
|
+
"""
|
|
167
|
+
Main loop
|
|
168
|
+
"""
|
|
169
|
+
previous_pressed_keys: typing.Set[int] = set([])
|
|
170
|
+
while True:
|
|
171
|
+
report = await self.report_queue.get()
|
|
172
|
+
pressed = {}
|
|
173
|
+
for i in range(0, len(report), 2):
|
|
174
|
+
if report[i] == 0 and report[i + 1] == 0:
|
|
175
|
+
break
|
|
176
|
+
key, value = report[i : i + 2]
|
|
177
|
+
pressed[key] = value
|
|
178
|
+
# add key_up = 0 when pressed key "disappear" to simplify
|
|
179
|
+
# handling of events.
|
|
180
|
+
for key_up in previous_pressed_keys - set(pressed.keys()):
|
|
181
|
+
pressed[key_up] = 0
|
|
182
|
+
await self.pressed_queue.put(pressed)
|
|
183
|
+
previous_pressed_keys = set(pressed.keys())
|
|
184
|
+
|
|
185
|
+
async def client_connected(
|
|
186
|
+
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
|
187
|
+
) -> None:
|
|
188
|
+
"""
|
|
189
|
+
Call back when a client connects to the control socket
|
|
190
|
+
"""
|
|
191
|
+
while True:
|
|
192
|
+
pktlen = await reader.read(1)
|
|
193
|
+
if pktlen == b"\x00":
|
|
194
|
+
return
|
|
195
|
+
try:
|
|
196
|
+
data = await reader.readexactly(pktlen[0])
|
|
197
|
+
except asyncio.IncompleteReadError:
|
|
198
|
+
break
|
|
199
|
+
if len(data) > 2:
|
|
200
|
+
received_report = self.razer_command(data)
|
|
201
|
+
if received_report:
|
|
202
|
+
writer.write(bytes((len(received_report),)) + received_report)
|
|
203
|
+
try:
|
|
204
|
+
await writer.drain()
|
|
205
|
+
except ConnectionResetError:
|
|
206
|
+
print("disconnect")
|
|
207
|
+
return
|
|
208
|
+
else:
|
|
209
|
+
print("empty reply")
|
|
210
|
+
else:
|
|
211
|
+
print("received insufficient data")
|
|
212
|
+
|
|
213
|
+
async def unix_server(self) -> None:
|
|
214
|
+
"""
|
|
215
|
+
Start control socket
|
|
216
|
+
"""
|
|
217
|
+
server = await asyncio.start_unix_server(
|
|
218
|
+
self.client_connected, path=f"/var/run/razer-analog-{os.getpid()}"
|
|
219
|
+
)
|
|
220
|
+
async with server:
|
|
221
|
+
await server.serve_forever()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def user_input_write(
|
|
225
|
+
user_input: evdev.UInput,
|
|
226
|
+
user_input_queue: asyncio.Queue[typing.Tuple[int, int, int]],
|
|
227
|
+
) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Write user_input_queue to UInput device
|
|
230
|
+
"""
|
|
231
|
+
while True:
|
|
232
|
+
etype, code, value = await user_input_queue.get()
|
|
233
|
+
user_input.write(etype, code, value)
|
|
234
|
+
user_input.syn()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def released_modifier_keys(
|
|
238
|
+
leftmeta_released: bool,
|
|
239
|
+
leftmeta_in_repeat: bool,
|
|
240
|
+
fn_released: bool,
|
|
241
|
+
fn_in_repeat: bool,
|
|
242
|
+
) -> bool:
|
|
243
|
+
"""
|
|
244
|
+
Modifier keys are released and previously pressed
|
|
245
|
+
"""
|
|
246
|
+
return (
|
|
247
|
+
(leftmeta_released and leftmeta_in_repeat)
|
|
248
|
+
or (fn_released and leftmeta_in_repeat)
|
|
249
|
+
or (fn_released and leftmeta_released and fn_in_repeat)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def key_triggered(triggered: typing.Dict[int, bool], key: int, value: int) -> bool:
|
|
254
|
+
"""
|
|
255
|
+
Check if key should trigger
|
|
256
|
+
"""
|
|
257
|
+
if value < 96:
|
|
258
|
+
triggered[key] = False
|
|
259
|
+
return False
|
|
260
|
+
if value > 128:
|
|
261
|
+
triggered[key] = True
|
|
262
|
+
return True
|
|
263
|
+
return triggered.get(key, False)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def razer_key_pressed(
|
|
267
|
+
pressed_keys: typing.Dict[int, int],
|
|
268
|
+
triggered: typing.Dict[int, bool],
|
|
269
|
+
razer_key: str,
|
|
270
|
+
) -> bool:
|
|
271
|
+
"""
|
|
272
|
+
Check if key is pressed
|
|
273
|
+
"""
|
|
274
|
+
razer_keys = {
|
|
275
|
+
"FN": 59,
|
|
276
|
+
"LEFTMETA": 127,
|
|
277
|
+
}
|
|
278
|
+
key = razer_keys[razer_key]
|
|
279
|
+
value = pressed_keys.get(key, 0)
|
|
280
|
+
return key_triggered(triggered, key, value)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
async def virtual_keyboard(
|
|
284
|
+
pressed_queue: asyncio.Queue[typing.Dict[int, int]],
|
|
285
|
+
user_input_queue: asyncio.Queue[typing.Tuple[int, int, int]],
|
|
286
|
+
) -> None:
|
|
287
|
+
"""
|
|
288
|
+
Handle Fn layout, left meta as Fn, FnFn as left meta and keyboard repeat
|
|
289
|
+
"""
|
|
290
|
+
repeat: typing.Dict[int, float] = {}
|
|
291
|
+
triggered: typing.Dict[int, bool] = {}
|
|
292
|
+
while True:
|
|
293
|
+
try:
|
|
294
|
+
pressed_keys: typing.Dict[int, int] = pressed_queue.get_nowait()
|
|
295
|
+
except asyncio.QueueEmpty:
|
|
296
|
+
await asyncio.sleep(0.03)
|
|
297
|
+
for pressed_key, repeat_until in repeat.items():
|
|
298
|
+
if time.monotonic() > repeat_until:
|
|
299
|
+
await user_input_queue.put(
|
|
300
|
+
(evdev.ecodes.ecodes["EV_KEY"], pressed_key, 2)
|
|
301
|
+
)
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
layout = razer_analog.layout.get_layout(
|
|
305
|
+
razer_key_pressed(pressed_keys, triggered, "FN"),
|
|
306
|
+
razer_key_pressed(pressed_keys, triggered, "LEFTMETA"),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if released_modifier_keys(
|
|
310
|
+
leftmeta_released=not razer_key_pressed(
|
|
311
|
+
pressed_keys, triggered, "LEFTMETA"
|
|
312
|
+
),
|
|
313
|
+
leftmeta_in_repeat=evdev.ecodes.ecodes["KEY_LEFTMETA"] in repeat,
|
|
314
|
+
fn_released=not razer_key_pressed(pressed_keys, triggered, "FN"),
|
|
315
|
+
fn_in_repeat=evdev.ecodes.ecodes["KEY_FN"] in repeat,
|
|
316
|
+
):
|
|
317
|
+
for code in repeat:
|
|
318
|
+
await user_input_queue.put((evdev.ecodes.ecodes["EV_KEY"], code, 0))
|
|
319
|
+
repeat = {}
|
|
320
|
+
|
|
321
|
+
for pressed_key, value in pressed_keys.items():
|
|
322
|
+
if pressed_key in razer_analog.layout.layout_analog:
|
|
323
|
+
await user_input_queue.put(
|
|
324
|
+
(
|
|
325
|
+
evdev.ecodes.ecodes["EV_ABS"],
|
|
326
|
+
razer_analog.layout.layout_analog[pressed_key][0],
|
|
327
|
+
value,
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if not pressed_key in layout:
|
|
332
|
+
# ignore pressed_keys not in layout (ie. released keys)
|
|
333
|
+
continue
|
|
334
|
+
|
|
335
|
+
code = layout[pressed_key]
|
|
336
|
+
|
|
337
|
+
if key_triggered(triggered, pressed_key, value):
|
|
338
|
+
if not code in repeat:
|
|
339
|
+
repeat[code] = time.monotonic() + 0.2
|
|
340
|
+
await user_input_queue.put((evdev.ecodes.ecodes["EV_KEY"], code, 1))
|
|
341
|
+
elif code != evdev.ecodes.ecodes["KEY_FN"] and code in repeat:
|
|
342
|
+
# Handle untrigger of key
|
|
343
|
+
# Fn key release is handled above
|
|
344
|
+
del repeat[code]
|
|
345
|
+
await user_input_queue.put((evdev.ecodes.ecodes["EV_KEY"], code, 0))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def main() -> None:
|
|
349
|
+
"""
|
|
350
|
+
Setup queues and await:
|
|
351
|
+
- unix_service (for control messages)
|
|
352
|
+
- receive pressed keys
|
|
353
|
+
- handle virtual keyboard
|
|
354
|
+
- write to user input queue
|
|
355
|
+
"""
|
|
356
|
+
loop = asyncio.get_event_loop()
|
|
357
|
+
|
|
358
|
+
pressed_queue: asyncio.Queue[typing.Dict[int, int]] = asyncio.Queue()
|
|
359
|
+
user_input_queue: asyncio.Queue[typing.Tuple[int, int, int]] = asyncio.Queue()
|
|
360
|
+
|
|
361
|
+
user_input_keyb = evdev.UInput(
|
|
362
|
+
{
|
|
363
|
+
evdev.ecodes.ecodes["EV_KEY"]: list(
|
|
364
|
+
razer_analog.layout.layout["plain"].values()
|
|
365
|
+
)
|
|
366
|
+
+ list(razer_analog.layout.layout["fn"].values())
|
|
367
|
+
+ list(razer_analog.layout.layout["fn_fn"].values()),
|
|
368
|
+
evdev.ecodes.ecodes["EV_ABS"]: list(
|
|
369
|
+
razer_analog.layout.layout_analog.values()
|
|
370
|
+
),
|
|
371
|
+
},
|
|
372
|
+
name=f"razer-analog-keyboard-{os.getpid()}",
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Run: udevadm info -t
|
|
376
|
+
# Search for: Razer_Huntsman_Mini_Analog
|
|
377
|
+
# Check for "T: usb_device", use (P:):
|
|
378
|
+
# /sys/devices/pci0000:00/0000:00:14.0/usb3/3-1
|
|
379
|
+
parent = sys.argv[1]
|
|
380
|
+
razer_huntsman_mini = HuntsmanMiniAnalog(pressed_queue, parent)
|
|
381
|
+
|
|
382
|
+
def shutdown() -> None:
|
|
383
|
+
razer_huntsman_mini.set_device_mode(0)
|
|
384
|
+
razer_huntsman_mini.close()
|
|
385
|
+
os.unlink(f"/var/run/razer-analog-{os.getpid()}")
|
|
386
|
+
|
|
387
|
+
atexit.register(shutdown)
|
|
388
|
+
|
|
389
|
+
razer_huntsman_mini.set_device_mode(3)
|
|
390
|
+
|
|
391
|
+
tasks = asyncio.gather(
|
|
392
|
+
razer_huntsman_mini.unix_server(),
|
|
393
|
+
razer_huntsman_mini.run(),
|
|
394
|
+
virtual_keyboard(pressed_queue, user_input_queue),
|
|
395
|
+
user_input_write(user_input_keyb, user_input_queue),
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
loop.add_signal_handler(signal.SIGTERM, tasks.cancel)
|
|
399
|
+
loop.run_until_complete(tasks)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import collections
|
|
3
|
+
import fcntl
|
|
4
|
+
import ioctl_opt
|
|
5
|
+
|
|
6
|
+
# input.h
|
|
7
|
+
BUS_USB = 0x03
|
|
8
|
+
BUS_HIL = 0x04
|
|
9
|
+
BUS_BLUETOOTH = 0x05
|
|
10
|
+
BUS_VIRTUAL = 0x06
|
|
11
|
+
|
|
12
|
+
# hid.h
|
|
13
|
+
_HID_MAX_DESCRIPTOR_SIZE = 4096
|
|
14
|
+
|
|
15
|
+
# hidraw.h
|
|
16
|
+
class _hidraw_report_descriptor(ctypes.Structure):
|
|
17
|
+
_fields_ = [
|
|
18
|
+
("size", ctypes.c_uint),
|
|
19
|
+
("value", ctypes.c_ubyte * _HID_MAX_DESCRIPTOR_SIZE),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _hidraw_devinfo(ctypes.Structure):
|
|
24
|
+
_fields_ = [
|
|
25
|
+
("bustype", ctypes.c_uint),
|
|
26
|
+
("vendor", ctypes.c_short),
|
|
27
|
+
("product", ctypes.c_short),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_HIDIOCGRDESCSIZE = ioctl_opt.IOR(ord("H"), 0x01, ctypes.c_int)
|
|
32
|
+
_HIDIOCGRDESC = ioctl_opt.IOR(ord("H"), 0x02, _hidraw_report_descriptor)
|
|
33
|
+
_HIDIOCGRAWINFO = ioctl_opt.IOR(ord("H"), 0x03, _hidraw_devinfo)
|
|
34
|
+
_HIDIOCGRAWNAME = lambda len: ioctl_opt.IOC(ioctl_opt.IOC_READ, ord("H"), 0x04, len)
|
|
35
|
+
_HIDIOCGRAWPHYS = lambda len: ioctl_opt.IOC(ioctl_opt.IOC_READ, ord("H"), 0x05, len)
|
|
36
|
+
_HIDIOCSFEATURE = lambda len: ioctl_opt.IOC(
|
|
37
|
+
ioctl_opt.IOC_WRITE | ioctl_opt.IOC_READ, ord("H"), 0x06, len
|
|
38
|
+
)
|
|
39
|
+
_HIDIOCGFEATURE = lambda len: ioctl_opt.IOC(
|
|
40
|
+
ioctl_opt.IOC_WRITE | ioctl_opt.IOC_READ, ord("H"), 0x07, len
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
HIDRAW_FIRST_MINOR = 0
|
|
44
|
+
HIDRAW_MAX_DEVICES = 64
|
|
45
|
+
HIDRAW_BUFFER_SIZE = 64
|
|
46
|
+
|
|
47
|
+
DevInfo = collections.namedtuple("DevInfo", ["bustype", "vendor", "product"])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class HIDRaw(object):
|
|
51
|
+
"""
|
|
52
|
+
Provides methods to access hidraw device's ioctls.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, device):
|
|
56
|
+
"""
|
|
57
|
+
device (file, fileno)
|
|
58
|
+
A file object or a fileno of an open hidraw device node.
|
|
59
|
+
"""
|
|
60
|
+
self._device = device
|
|
61
|
+
|
|
62
|
+
def _ioctl(self, func, arg, mutate_flag=False):
|
|
63
|
+
result = fcntl.ioctl(self._device, func, arg, mutate_flag)
|
|
64
|
+
if result < 0:
|
|
65
|
+
raise IOError(result)
|
|
66
|
+
|
|
67
|
+
def getRawReportDescriptor(self):
|
|
68
|
+
"""
|
|
69
|
+
Return a binary string containing the raw HID report descriptor.
|
|
70
|
+
"""
|
|
71
|
+
descriptor = _hidraw_report_descriptor()
|
|
72
|
+
size = ctypes.c_uint()
|
|
73
|
+
self._ioctl(_HIDIOCGRDESCSIZE, size, True)
|
|
74
|
+
descriptor.size = size
|
|
75
|
+
self._ioctl(_HIDIOCGRDESC, descriptor, True)
|
|
76
|
+
return descriptor.value[: size.value]
|
|
77
|
+
|
|
78
|
+
# TODO: decode descriptor into a python object
|
|
79
|
+
# def getReportDescriptor(self):
|
|
80
|
+
|
|
81
|
+
def getInfo(self):
|
|
82
|
+
"""
|
|
83
|
+
Returns a DevInfo instance, a named tuple with the following items:
|
|
84
|
+
- bustype: one of BUS_USB, BUS_HIL, BUS_BLUETOOTH or BUS_VIRTUAL
|
|
85
|
+
- vendor: device's vendor number
|
|
86
|
+
- product: device's product number
|
|
87
|
+
"""
|
|
88
|
+
devinfo = _hidraw_devinfo()
|
|
89
|
+
self._ioctl(_HIDIOCGRAWINFO, devinfo, True)
|
|
90
|
+
return DevInfo(devinfo.bustype, devinfo.vendor, devinfo.product)
|
|
91
|
+
|
|
92
|
+
def getName(self, length=512):
|
|
93
|
+
"""
|
|
94
|
+
Returns device name as an unicode object.
|
|
95
|
+
"""
|
|
96
|
+
name = ctypes.create_string_buffer(length)
|
|
97
|
+
self._ioctl(_HIDIOCGRAWNAME(length), name, True)
|
|
98
|
+
return name.value.decode("UTF-8")
|
|
99
|
+
|
|
100
|
+
def getPhysicalAddress(self, length=512):
|
|
101
|
+
"""
|
|
102
|
+
Returns device physical address as a string.
|
|
103
|
+
See hidraw documentation for value signification, as it depends on
|
|
104
|
+
device's bus type.
|
|
105
|
+
"""
|
|
106
|
+
name = ctypes.create_string_buffer(length)
|
|
107
|
+
self._ioctl(_HIDIOCGRAWPHYS(length), name, True)
|
|
108
|
+
return name.value
|
|
109
|
+
|
|
110
|
+
def sendFeatureReport(self, report, report_num=0):
|
|
111
|
+
"""
|
|
112
|
+
Send a feature report.
|
|
113
|
+
"""
|
|
114
|
+
length = len(report) + 1
|
|
115
|
+
buf = bytearray(length)
|
|
116
|
+
buf[0] = report_num
|
|
117
|
+
buf[1:] = report
|
|
118
|
+
self._ioctl(
|
|
119
|
+
_HIDIOCSFEATURE(length),
|
|
120
|
+
(ctypes.c_char * length).from_buffer(buf),
|
|
121
|
+
True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def getFeatureReport(self, report_num=0, length=63):
|
|
125
|
+
"""
|
|
126
|
+
Receive a feature report.
|
|
127
|
+
Blocks, unless you configured provided file (descriptor) to be
|
|
128
|
+
non-blocking.
|
|
129
|
+
"""
|
|
130
|
+
length += 1
|
|
131
|
+
buf = bytearray(length)
|
|
132
|
+
buf[0] = report_num
|
|
133
|
+
self._ioctl(
|
|
134
|
+
_HIDIOCGFEATURE(length),
|
|
135
|
+
(ctypes.c_char * length).from_buffer(buf),
|
|
136
|
+
True,
|
|
137
|
+
)
|
|
138
|
+
return buf
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import razer_analog.hidraw
|
|
4
|
+
import pyudev
|
|
5
|
+
import struct
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def crc(buf: bytes) -> int:
|
|
9
|
+
"""
|
|
10
|
+
Calculate Razer CRC
|
|
11
|
+
"""
|
|
12
|
+
result = 0
|
|
13
|
+
for i in range(2, 86):
|
|
14
|
+
result ^= buf[i]
|
|
15
|
+
return result
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def razer_command(hidraw, data: bytes) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Send a razer command
|
|
21
|
+
"""
|
|
22
|
+
send_report = struct.pack(
|
|
23
|
+
">BBHBB",
|
|
24
|
+
0x00, # status
|
|
25
|
+
0xFF, # transaction_id
|
|
26
|
+
0x00, # remaining_packets
|
|
27
|
+
0x00, # protocol_type
|
|
28
|
+
len(data) - 2, # size
|
|
29
|
+
)
|
|
30
|
+
send_report += data
|
|
31
|
+
send_report += b"\x00" * (82 - len(data))
|
|
32
|
+
send_report += bytes([crc(send_report)])
|
|
33
|
+
send_report += b"\x00"
|
|
34
|
+
|
|
35
|
+
hidraw.sendFeatureReport(send_report)
|
|
36
|
+
received_report = hidraw.getFeatureReport(0, 90)
|
|
37
|
+
|
|
38
|
+
if received_report[1] == 0x02: # SUCCESS
|
|
39
|
+
if received_report[2:] != send_report[1:]:
|
|
40
|
+
print("mismatched received_report != send_report")
|
|
41
|
+
print(received_report)
|
|
42
|
+
print(send_report)
|
|
43
|
+
else:
|
|
44
|
+
print("failed to send_report")
|
|
45
|
+
print(send_report)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
context = pyudev.Context()
|
|
50
|
+
parent = pyudev.Devices.from_path(context, sys.argv[1])
|
|
51
|
+
for udev in context.list_devices(parent=parent, subsystem="hidraw"):
|
|
52
|
+
device_handle = open(udev.device_node, "rb")
|
|
53
|
+
os.set_blocking(device_handle.fileno(), False)
|
|
54
|
+
hidraw = razer_analog.hidraw.HIDRaw(device_handle)
|
|
55
|
+
i = hidraw.getInfo()
|
|
56
|
+
if i.vendor == 0x1532 and i.product == 0x0282:
|
|
57
|
+
desc = hidraw.getRawReportDescriptor()
|
|
58
|
+
print(desc)
|
|
59
|
+
if list(desc) == [
|
|
60
|
+
5,
|
|
61
|
+
12,
|
|
62
|
+
9,
|
|
63
|
+
1,
|
|
64
|
+
161,
|
|
65
|
+
1,
|
|
66
|
+
6,
|
|
67
|
+
0,
|
|
68
|
+
255,
|
|
69
|
+
9,
|
|
70
|
+
2,
|
|
71
|
+
21,
|
|
72
|
+
0,
|
|
73
|
+
37,
|
|
74
|
+
1,
|
|
75
|
+
117,
|
|
76
|
+
8,
|
|
77
|
+
149,
|
|
78
|
+
90,
|
|
79
|
+
177,
|
|
80
|
+
1,
|
|
81
|
+
192,
|
|
82
|
+
]:
|
|
83
|
+
print("found")
|
|
84
|
+
break
|
|
85
|
+
device_handle.close()
|
|
86
|
+
razer_command(hidraw, bytes((3, 0x00)))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Layout for keyboard
|
|
3
|
+
"""
|
|
4
|
+
import typing
|
|
5
|
+
import json
|
|
6
|
+
import pkg_resources # type: ignore
|
|
7
|
+
import evdev # type: ignore
|
|
8
|
+
|
|
9
|
+
absinfo = evdev.device.AbsInfo(value=0, min=0, max=255, fuzz=0, flat=0, resolution=1)
|
|
10
|
+
layout_analog = {
|
|
11
|
+
36: (evdev.ecodes.ecodes["ABS_HAT0X"], absinfo),
|
|
12
|
+
37: (evdev.ecodes.ecodes["ABS_HAT0Y"], absinfo),
|
|
13
|
+
38: (evdev.ecodes.ecodes["ABS_HAT1X"], absinfo),
|
|
14
|
+
39: (evdev.ecodes.ecodes["ABS_HAT1Y"], absinfo),
|
|
15
|
+
18: (evdev.ecodes.ecodes["ABS_HAT2X"], absinfo),
|
|
16
|
+
47: (evdev.ecodes.ecodes["ABS_HAT2Y"], absinfo),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_layout() -> typing.Dict[str, typing.Dict[int, int]]:
|
|
21
|
+
"""
|
|
22
|
+
Load layout from json file
|
|
23
|
+
"""
|
|
24
|
+
with open(
|
|
25
|
+
pkg_resources.resource_filename(
|
|
26
|
+
"razer_analog", "razer_huntsman_mini_analog.json"
|
|
27
|
+
),
|
|
28
|
+
encoding="utf-8",
|
|
29
|
+
) as razer_huntsman_mini_analog_file:
|
|
30
|
+
result: typing.Dict[str, typing.Dict[int, int]] = {
|
|
31
|
+
"plain": {},
|
|
32
|
+
"fn": {},
|
|
33
|
+
"fn_fn": {},
|
|
34
|
+
}
|
|
35
|
+
for layout_key, layout_value in json.load(
|
|
36
|
+
razer_huntsman_mini_analog_file
|
|
37
|
+
).items():
|
|
38
|
+
for razer_key, evdev_key in layout_value.items():
|
|
39
|
+
result[layout_key][int(razer_key)] = evdev.ecodes.ecodes[
|
|
40
|
+
"KEY_" + evdev_key
|
|
41
|
+
]
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
layout = load_layout()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_layout(fn_pressed: bool, meta_pressed: bool) -> typing.Dict[int, int]:
|
|
49
|
+
"""
|
|
50
|
+
Get layout from pressed keys
|
|
51
|
+
"""
|
|
52
|
+
if fn_pressed and meta_pressed:
|
|
53
|
+
return layout["fn_fn"]
|
|
54
|
+
if fn_pressed or meta_pressed:
|
|
55
|
+
return layout["fn"]
|
|
56
|
+
return layout["plain"]
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Razer Ananlog virtual mouse
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
5
|
+
import collections
|
|
6
|
+
import atexit
|
|
7
|
+
import time
|
|
8
|
+
import colorsys
|
|
9
|
+
import socket
|
|
10
|
+
import signal
|
|
11
|
+
import sys
|
|
12
|
+
import typing
|
|
13
|
+
import os.path
|
|
14
|
+
|
|
15
|
+
import evdev # type: ignore
|
|
16
|
+
import pyudev # type: ignore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Mouse:
|
|
20
|
+
"""
|
|
21
|
+
Virtual Mouse
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, state: typing.Dict[int, collections.defaultdict[int, int]]):
|
|
25
|
+
self.ui_mouse = evdev.UInput(
|
|
26
|
+
evdev.util.find_ecodes_by_regex(
|
|
27
|
+
r"(REL_X|REL_Y|REL_WHEEL|REL_WHEEL_HI_RES|"
|
|
28
|
+
r"BTN_RIGHT|BTN_MIDDLE|BTN_LEFT|KEY_CAPSLOCK)$"
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
self.enabled: bool = False
|
|
32
|
+
self.state: typing.Dict[int, collections.defaultdict[int, int]] = state
|
|
33
|
+
|
|
34
|
+
def enable(self) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Enable mouse
|
|
37
|
+
"""
|
|
38
|
+
self.enabled = True
|
|
39
|
+
|
|
40
|
+
def disable(self) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Disable mouse
|
|
43
|
+
"""
|
|
44
|
+
self.enabled = False
|
|
45
|
+
|
|
46
|
+
def handle(self, from_event: int, to_event: int, direction: int) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Handle event
|
|
49
|
+
"""
|
|
50
|
+
value = self.state[evdev.ecodes.ecodes["EV_ABS"]][from_event]
|
|
51
|
+
|
|
52
|
+
if value:
|
|
53
|
+
value = 0.000000000749 * pow(value, 4.5) + 1
|
|
54
|
+
self.ui_mouse.write(
|
|
55
|
+
evdev.ecodes.ecodes["EV_REL"], to_event, int(value) * direction
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def write(self, typ: int, code: int, value: int) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Write event
|
|
61
|
+
"""
|
|
62
|
+
self.ui_mouse.write(typ, code, value)
|
|
63
|
+
|
|
64
|
+
async def run(self) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Main loop
|
|
67
|
+
"""
|
|
68
|
+
while True:
|
|
69
|
+
if self.enabled:
|
|
70
|
+
await asyncio.sleep(0.05)
|
|
71
|
+
self.handle(evdev.ecodes.ABS_HAT0X, evdev.ecodes.REL_X, -1)
|
|
72
|
+
self.handle(evdev.ecodes.ABS_HAT0Y, evdev.ecodes.REL_Y, 1)
|
|
73
|
+
self.handle(evdev.ecodes.ABS_HAT1X, evdev.ecodes.REL_Y, -1)
|
|
74
|
+
self.handle(evdev.ecodes.ABS_HAT1Y, evdev.ecodes.REL_X, 1)
|
|
75
|
+
value = self.state[evdev.ecodes.EV_ABS][evdev.ecodes.ABS_HAT2Y]
|
|
76
|
+
if value:
|
|
77
|
+
self.ui_mouse.write(
|
|
78
|
+
evdev.ecodes.EV_REL,
|
|
79
|
+
evdev.ecodes.REL_WHEEL_HI_RES,
|
|
80
|
+
int(value) * -1,
|
|
81
|
+
)
|
|
82
|
+
self.ui_mouse.write(evdev.ecodes.EV_REL, evdev.ecodes.REL_WHEEL, -1)
|
|
83
|
+
value = self.state[evdev.ecodes.EV_ABS][evdev.ecodes.ABS_HAT2X]
|
|
84
|
+
if value:
|
|
85
|
+
self.ui_mouse.write(
|
|
86
|
+
evdev.ecodes.EV_REL,
|
|
87
|
+
evdev.ecodes.REL_WHEEL_HI_RES,
|
|
88
|
+
int(value) * 1,
|
|
89
|
+
)
|
|
90
|
+
self.ui_mouse.write(evdev.ecodes.EV_REL, evdev.ecodes.REL_WHEEL, 1)
|
|
91
|
+
|
|
92
|
+
self.ui_mouse.syn()
|
|
93
|
+
else:
|
|
94
|
+
await asyncio.sleep(0.1)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Chroma:
|
|
98
|
+
"""
|
|
99
|
+
Chroma lighting
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, razer_analog_pid: int):
|
|
103
|
+
sock_file = f"/var/run/razer-analog-{razer_analog_pid}"
|
|
104
|
+
for _ in range(10):
|
|
105
|
+
if os.path.exists(sock_file):
|
|
106
|
+
break
|
|
107
|
+
time.sleep(0.1)
|
|
108
|
+
|
|
109
|
+
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
110
|
+
self.sock.connect(sock_file)
|
|
111
|
+
self.sock.settimeout(1)
|
|
112
|
+
# custom
|
|
113
|
+
self.do_time_of_day = True
|
|
114
|
+
self.brightness = 0xFF
|
|
115
|
+
self.time_of_day()
|
|
116
|
+
|
|
117
|
+
def custom_frame(
|
|
118
|
+
self, row: int, start: int, data: typing.Tuple[typing.Tuple[int, int, int], ...]
|
|
119
|
+
) -> bytes:
|
|
120
|
+
"""
|
|
121
|
+
Enable custom frame mode (so we can manually enable LEDs)
|
|
122
|
+
"""
|
|
123
|
+
result = b"\x0f\x03\x00\x00"
|
|
124
|
+
result += bytes((row, start, start + len(data) - 1))
|
|
125
|
+
for rgb in data:
|
|
126
|
+
result += bytes(rgb)
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
def send(self, data: bytes) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Send data using simple protocol (len + data)
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
self.sock.sendall(bytes((len(data),)) + data)
|
|
135
|
+
except BrokenPipeError as exception:
|
|
136
|
+
print("failed to send", exception)
|
|
137
|
+
|
|
138
|
+
if not data:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
pktlen = self.sock.recv(1)
|
|
142
|
+
|
|
143
|
+
received_data = self.sock.recv(pktlen[0])[7 : 7 + len(data)]
|
|
144
|
+
if received_data != data:
|
|
145
|
+
print("received_data != data", received_data, data)
|
|
146
|
+
|
|
147
|
+
def times255(
|
|
148
|
+
self, rgb: typing.Tuple[float, float, float]
|
|
149
|
+
) -> typing.Tuple[int, int, int]:
|
|
150
|
+
"""
|
|
151
|
+
Multiply RGB set by 255
|
|
152
|
+
"""
|
|
153
|
+
return int(rgb[0] * 0xFF), int(rgb[1] * 0xFF), int(rgb[2] * 0xFF)
|
|
154
|
+
|
|
155
|
+
def mouse(self) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Enable custom frame for mouse mode
|
|
158
|
+
"""
|
|
159
|
+
self.do_time_of_day = False
|
|
160
|
+
self.send(self.custom_frame(0, 0, ((0xFF, 0xFF, 0xFF),) * 1))
|
|
161
|
+
self.send(self.custom_frame(0, 1, ((0, 0, 0),) * 14))
|
|
162
|
+
|
|
163
|
+
self.send(self.custom_frame(1, 0, ((0, 0, 0),) * 2))
|
|
164
|
+
self.send(self.custom_frame(1, 2, ((0xFF, 0xFF, 0xFF),) * 1))
|
|
165
|
+
self.send(self.custom_frame(1, 3, ((0, 0, 0),) * 12))
|
|
166
|
+
|
|
167
|
+
self.send(self.custom_frame(2, 0, ((0xFF, 0xFF, 0xFF),) * 4))
|
|
168
|
+
self.send(self.custom_frame(2, 4, ((0, 0, 0),) * 2))
|
|
169
|
+
self.send(self.custom_frame(2, 6, ((0xFF, 0xFF, 0xFF),) * 4))
|
|
170
|
+
self.send(self.custom_frame(2, 10, ((0, 0, 0),) * 5))
|
|
171
|
+
|
|
172
|
+
self.send(self.custom_frame(3, 0, ((0, 0, 0),) * 3))
|
|
173
|
+
self.send(self.custom_frame(3, 3, ((0xFF, 0xFF, 0xFF),) * 1))
|
|
174
|
+
self.send(self.custom_frame(3, 4, ((0, 0, 0),) * 11))
|
|
175
|
+
|
|
176
|
+
self.send(self.custom_frame(4, 0, ((0, 0, 0),) * 15))
|
|
177
|
+
|
|
178
|
+
def time_of_day(self) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Enable custom frame for time of day mode
|
|
181
|
+
"""
|
|
182
|
+
self.do_time_of_day = True
|
|
183
|
+
self.send(b"\x0f\x02\x00\x05\x08")
|
|
184
|
+
localtime = time.localtime()
|
|
185
|
+
hue = (localtime.tm_hour * 60 + localtime.tm_min) / (24 * 60)
|
|
186
|
+
for row in range(5):
|
|
187
|
+
self.send(
|
|
188
|
+
self.custom_frame(
|
|
189
|
+
row, 0, (self.times255(colorsys.hls_to_rgb(hue, 0.5, 1)),) * 15
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def highlight_fn(self) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Highlight buttons that can be used with fn
|
|
196
|
+
"""
|
|
197
|
+
self.send(self.custom_frame(0, 0, ((0xFF, 0xFF, 0xFF),) * 15))
|
|
198
|
+
|
|
199
|
+
self.send(self.custom_frame(1, 0, ((0xFF, 0xFF, 0xFF),) * 13))
|
|
200
|
+
self.send(self.custom_frame(1, 13, ((0x00, 0x00, 0x00),) * 2))
|
|
201
|
+
|
|
202
|
+
self.send(self.custom_frame(2, 0, ((0x00, 0x00, 0x00),) * 5))
|
|
203
|
+
self.send(self.custom_frame(2, 5, ((0xFF, 0xFF, 0xFF),) * 7))
|
|
204
|
+
self.send(self.custom_frame(2, 12, ((0x00, 0x00, 0x00),) * 3))
|
|
205
|
+
|
|
206
|
+
self.send(self.custom_frame(3, 0, ((0x00, 0x00, 0x00),) * 9))
|
|
207
|
+
self.send(self.custom_frame(3, 9, ((0xFF, 0xFF, 0xFF),) * 3))
|
|
208
|
+
self.send(self.custom_frame(3, 12, ((0x00, 0x00, 0x00),) * 3))
|
|
209
|
+
|
|
210
|
+
self.send(self.custom_frame(4, 0, ((0x00, 0x00, 0x00),) * 15))
|
|
211
|
+
|
|
212
|
+
def highlight_shift(self) -> None:
|
|
213
|
+
"""
|
|
214
|
+
Highlight buttons that can be used with shift
|
|
215
|
+
"""
|
|
216
|
+
self.send(self.custom_frame(0, 0, ((0x00, 0x00, 0x00),) * 1))
|
|
217
|
+
self.send(self.custom_frame(0, 1, ((0xFF, 0xFF, 0xFF),) * 13))
|
|
218
|
+
self.send(self.custom_frame(0, 14, ((0x00, 0x00, 0x00),) * 1))
|
|
219
|
+
|
|
220
|
+
self.send(self.custom_frame(1, 0, ((0xFF, 0xFF, 0xFF),) * 15))
|
|
221
|
+
|
|
222
|
+
self.send(self.custom_frame(2, 0, ((0x00, 0x00, 0x00),) * 1))
|
|
223
|
+
self.send(self.custom_frame(2, 1, ((0xFF, 0xFF, 0xFF),) * 14))
|
|
224
|
+
|
|
225
|
+
self.send(self.custom_frame(3, 0, ((0x00, 0x00, 0x00),) * 1))
|
|
226
|
+
self.send(self.custom_frame(3, 1, ((0xFF, 0xFF, 0xFF),) * 11))
|
|
227
|
+
self.send(self.custom_frame(3, 12, ((0x00, 0x00, 0x00),) * 3))
|
|
228
|
+
|
|
229
|
+
self.send(self.custom_frame(4, 0, ((0x00, 0x00, 0x00),) * 15))
|
|
230
|
+
|
|
231
|
+
def highlight_shift_fn(self) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Highlight buttons that can be used with shift-fn
|
|
234
|
+
"""
|
|
235
|
+
self.send(self.custom_frame(0, 0, ((0xFF, 0xFF, 0xFF),) * 15))
|
|
236
|
+
|
|
237
|
+
self.send(self.custom_frame(1, 0, ((0x00, 0x00, 0x00),) * 8))
|
|
238
|
+
self.send(self.custom_frame(1, 8, ((0xFF, 0xFF, 0xFF),) * 5))
|
|
239
|
+
self.send(self.custom_frame(1, 13, ((0x00, 0x00, 0x00),) * 2))
|
|
240
|
+
|
|
241
|
+
self.send(self.custom_frame(2, 0, ((0x00, 0x00, 0x00),) * 7))
|
|
242
|
+
self.send(self.custom_frame(2, 7, ((0xFF, 0xFF, 0xFF),) * 5))
|
|
243
|
+
self.send(self.custom_frame(2, 12, ((0x00, 0x00, 0x00),) * 3))
|
|
244
|
+
|
|
245
|
+
self.send(self.custom_frame(3, 0, ((0x00, 0x00, 0x00),) * 11))
|
|
246
|
+
self.send(self.custom_frame(3, 11, ((0xFF, 0xFF, 0xFF),) * 1))
|
|
247
|
+
self.send(self.custom_frame(3, 12, ((0x00, 0x00, 0x00),) * 3))
|
|
248
|
+
|
|
249
|
+
self.send(self.custom_frame(4, 0, ((0x00, 0x00, 0x00),) * 15))
|
|
250
|
+
|
|
251
|
+
def adjust_brightness(self, offset: int) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Adjust brightness of LEDs
|
|
254
|
+
"""
|
|
255
|
+
self.brightness += offset
|
|
256
|
+
self.brightness = min(max(self.brightness, 0), 0xFF)
|
|
257
|
+
self.send(b"\x0f\x04\x00\x00" + bytes((self.brightness,)))
|
|
258
|
+
|
|
259
|
+
async def run(self) -> None:
|
|
260
|
+
"""
|
|
261
|
+
Main loop
|
|
262
|
+
"""
|
|
263
|
+
while 1:
|
|
264
|
+
await asyncio.sleep(60)
|
|
265
|
+
if self.do_time_of_day:
|
|
266
|
+
self.time_of_day()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
async def keyboard(
|
|
270
|
+
razer_analog_keyboard: evdev.InputDevice,
|
|
271
|
+
state: typing.Dict[int, collections.defaultdict[int, int]],
|
|
272
|
+
mouse: Mouse,
|
|
273
|
+
chroma: Chroma,
|
|
274
|
+
) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Watch keyboard events
|
|
277
|
+
"""
|
|
278
|
+
shift_pressed = False
|
|
279
|
+
fn_pressed = False
|
|
280
|
+
try:
|
|
281
|
+
async for event in razer_analog_keyboard.async_read_loop():
|
|
282
|
+
state[event.type][event.code] = event.value
|
|
283
|
+
|
|
284
|
+
if mouse.enabled:
|
|
285
|
+
if (
|
|
286
|
+
event.type == evdev.ecodes.ecodes["EV_KEY"]
|
|
287
|
+
and event.code == evdev.ecodes.ecodes["KEY_CAPSLOCK"]
|
|
288
|
+
):
|
|
289
|
+
mouse.write(
|
|
290
|
+
evdev.ecodes.ecodes["EV_KEY"],
|
|
291
|
+
evdev.ecodes.ecodes["KEY_CAPSLOCK"],
|
|
292
|
+
event.value,
|
|
293
|
+
)
|
|
294
|
+
if (
|
|
295
|
+
event.type == evdev.ecodes.ecodes["EV_KEY"]
|
|
296
|
+
and event.code == evdev.ecodes.ecodes["KEY_A"]
|
|
297
|
+
):
|
|
298
|
+
mouse.write(
|
|
299
|
+
evdev.ecodes.ecodes["EV_KEY"],
|
|
300
|
+
evdev.ecodes.ecodes["BTN_LEFT"],
|
|
301
|
+
event.value,
|
|
302
|
+
)
|
|
303
|
+
if (
|
|
304
|
+
event.type == evdev.ecodes.ecodes["EV_KEY"]
|
|
305
|
+
and event.code == evdev.ecodes.ecodes["KEY_S"]
|
|
306
|
+
):
|
|
307
|
+
mouse.write(
|
|
308
|
+
evdev.ecodes.ecodes["EV_KEY"],
|
|
309
|
+
evdev.ecodes.ecodes["BTN_MIDDLE"],
|
|
310
|
+
event.value,
|
|
311
|
+
)
|
|
312
|
+
if (
|
|
313
|
+
event.type == evdev.ecodes.ecodes["EV_KEY"]
|
|
314
|
+
and event.code == evdev.ecodes.ecodes["KEY_D"]
|
|
315
|
+
):
|
|
316
|
+
mouse.write(
|
|
317
|
+
evdev.ecodes.ecodes["EV_KEY"],
|
|
318
|
+
evdev.ecodes.ecodes["BTN_RIGHT"],
|
|
319
|
+
event.value,
|
|
320
|
+
)
|
|
321
|
+
if (
|
|
322
|
+
event.type == evdev.ecodes.ecodes["EV_KEY"]
|
|
323
|
+
and event.code == evdev.ecodes.ecodes["KEY_ESC"]
|
|
324
|
+
):
|
|
325
|
+
mouse.disable()
|
|
326
|
+
chroma.time_of_day()
|
|
327
|
+
razer_analog_keyboard.ungrab()
|
|
328
|
+
else:
|
|
329
|
+
if (
|
|
330
|
+
event.type == evdev.ecodes.ecodes["EV_KEY"]
|
|
331
|
+
and event.code == evdev.ecodes.ecodes["KEY_KEYBOARD"]
|
|
332
|
+
and event.value
|
|
333
|
+
):
|
|
334
|
+
chroma.mouse()
|
|
335
|
+
while razer_analog_keyboard.active_keys():
|
|
336
|
+
await asyncio.sleep(0.1)
|
|
337
|
+
# fingers crossed no keys are pressed..
|
|
338
|
+
razer_analog_keyboard.grab()
|
|
339
|
+
|
|
340
|
+
mouse.enable()
|
|
341
|
+
elif (
|
|
342
|
+
event.type == evdev.ecodes.ecodes["EV_KEY"]
|
|
343
|
+
and event.code == evdev.ecodes.ecodes["KEY_FN"]
|
|
344
|
+
):
|
|
345
|
+
if event.value:
|
|
346
|
+
fn_pressed = True
|
|
347
|
+
if shift_pressed:
|
|
348
|
+
chroma.highlight_shift_fn()
|
|
349
|
+
else:
|
|
350
|
+
chroma.highlight_fn()
|
|
351
|
+
else:
|
|
352
|
+
fn_pressed = False
|
|
353
|
+
chroma.time_of_day()
|
|
354
|
+
|
|
355
|
+
elif event.type == evdev.ecodes.ecodes["EV_KEY"] and event.code in (
|
|
356
|
+
evdev.ecodes.ecodes["KEY_LEFTSHIFT"],
|
|
357
|
+
evdev.ecodes.ecodes["KEY_RIGHTSHIFT"],
|
|
358
|
+
):
|
|
359
|
+
if event.value:
|
|
360
|
+
if fn_pressed:
|
|
361
|
+
chroma.highlight_shift_fn()
|
|
362
|
+
else:
|
|
363
|
+
chroma.highlight_shift()
|
|
364
|
+
|
|
365
|
+
shift_pressed = True
|
|
366
|
+
else:
|
|
367
|
+
chroma.time_of_day()
|
|
368
|
+
shift_pressed = False
|
|
369
|
+
elif (
|
|
370
|
+
event.type == evdev.ecodes.ecodes["EV_KEY"]
|
|
371
|
+
and event.code == evdev.ecodes.ecodes["KEY_KBDILLUMDOWN"]
|
|
372
|
+
):
|
|
373
|
+
chroma.adjust_brightness(-0x0F)
|
|
374
|
+
elif (
|
|
375
|
+
event.type == evdev.ecodes.ecodes["EV_KEY"]
|
|
376
|
+
and event.code == evdev.ecodes.ecodes["KEY_KBDILLUMUP"]
|
|
377
|
+
):
|
|
378
|
+
chroma.adjust_brightness(0x0F)
|
|
379
|
+
|
|
380
|
+
except OSError as exception:
|
|
381
|
+
print(exception, "exiting")
|
|
382
|
+
sys.exit()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def main() -> None:
|
|
386
|
+
"""
|
|
387
|
+
Main loop
|
|
388
|
+
"""
|
|
389
|
+
context = pyudev.Context()
|
|
390
|
+
# run: udevadm info -t
|
|
391
|
+
# search for "razer-analog-keyboard-"
|
|
392
|
+
# Use the child of this device (event*) (P:):
|
|
393
|
+
# /sys/devices/virtual/input/input39/event23
|
|
394
|
+
udev = pyudev.Devices.from_path(context, sys.argv[1])
|
|
395
|
+
razer_analog_keyboard = evdev.InputDevice(udev.device_node)
|
|
396
|
+
|
|
397
|
+
razer_analog_pid = razer_analog_keyboard.name.split("-")[-1]
|
|
398
|
+
print(razer_analog_keyboard)
|
|
399
|
+
|
|
400
|
+
state: typing.Dict[int, collections.defaultdict[int, int]] = {
|
|
401
|
+
evdev.ecodes.ecodes["EV_SYN"]: collections.defaultdict(int),
|
|
402
|
+
evdev.ecodes.ecodes["EV_ABS"]: collections.defaultdict(int),
|
|
403
|
+
evdev.ecodes.ecodes["EV_KEY"]: collections.defaultdict(int),
|
|
404
|
+
}
|
|
405
|
+
mouse = Mouse(state)
|
|
406
|
+
chroma = Chroma(razer_analog_pid)
|
|
407
|
+
|
|
408
|
+
tasks = asyncio.gather(
|
|
409
|
+
mouse.run(), keyboard(razer_analog_keyboard, state, mouse, chroma), chroma.run()
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
loop = asyncio.get_event_loop()
|
|
413
|
+
loop.add_signal_handler(signal.SIGTERM, tasks.cancel)
|
|
414
|
+
|
|
415
|
+
def shutdown() -> None:
|
|
416
|
+
chroma.send(b"")
|
|
417
|
+
|
|
418
|
+
atexit.register(shutdown)
|
|
419
|
+
loop.run_until_complete(tasks)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"plain": {
|
|
3
|
+
"110": "ESC", "2": "1", "3": "2", "4": "3", "5": "4", "6": "5", "7": "6", "8": "7", "9": "8", "10": "9", "11": "0", "12": "MINUS", "13": "EQUAL", "15": "BACKSPACE",
|
|
4
|
+
"16": "TAB", "17": "Q", "18": "W", "19": "E", "20": "R", "21": "T", "22": "Y", "23": "U", "24": "I", "25": "O", "26": "P", "27": "LEFTBRACE", "28": "RIGHTBRACE", "29": "BACKSLASH",
|
|
5
|
+
"30": "CAPSLOCK", "31": "A", "32": "S", "33": "D", "34": "F", "35": "G", "36": "H", "37": "J", "38": "K", "39": "L", "40": "SEMICOLON", "41": "APOSTROPHE", "43": "ENTER",
|
|
6
|
+
"44": "LEFTSHIFT", "46": "Z", "47": "X", "48": "C", "49": "V", "50": "B", "51": "N", "52": "M", "53": "COMMA", "54": "DOT", "55": "SLASH", "57": "RIGHTSHIFT",
|
|
7
|
+
"58": "LEFTCTRL", "127": "LEFTMETA", "60": "LEFTALT", "61": "SPACE", "62": "RIGHTALT", "59": "FN", "129": "COMPOSE", "64": "RIGHTCTRL"
|
|
8
|
+
},
|
|
9
|
+
"fn": {
|
|
10
|
+
"110": "GRAVE", "2": "F1", "3": "F2", "4": "F3", "5": "F4", "6": "F5", "7": "F6", "8": "F7", "9": "F8", "10": "F9", "11": "F10", "12": "F11", "13": "F12", "15": "DELETE",
|
|
11
|
+
"16": "MUTE", "17": "VOLUMEDOWN", "18": "VOLUMEUP", "19": "REWIND", "20": "PLAYPAUSE", "21": "FASTFORWARD", "24": "UP", "25": "SCROLLLOCK", "26": "SYSRQ", "27": "PAGEUP", "28": "HOME",
|
|
12
|
+
"30": "CAPSLOCK", "35": "KBDILLUMDOWN", "36": "KBDILLUMUP","37": "LEFT", "38": "DOWN", "39": "RIGHT", "40": "PAGEDOWN", "41": "END", "43": "KEYBOARD",
|
|
13
|
+
"44": "LEFTSHIFT", "53": "SLEEP", "54": "PAUSE", "55": "INSERT", "57": "RIGHTSHIFT",
|
|
14
|
+
"58": "LEFTCTRL", "127": "FN", "60": "LEFTALT", "62": "RIGHTALT", "59": "FN", "64": "RIGHTCTRL"
|
|
15
|
+
},
|
|
16
|
+
"fn_fn": {
|
|
17
|
+
"127": "LEFTMETA", "59": "FN"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import time
|
|
3
|
+
import colorsys
|
|
4
|
+
import glob
|
|
5
|
+
import random
|
|
6
|
+
import typing
|
|
7
|
+
import atexit
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def times255(
|
|
11
|
+
self, rgb: typing.Tuple[float, float, float]
|
|
12
|
+
) -> typing.Tuple[int, int, int]:
|
|
13
|
+
"""
|
|
14
|
+
Multiply RGB set by 255
|
|
15
|
+
"""
|
|
16
|
+
return int(rgb[0] * 0xFF), int(rgb[1] * 0xFF), int(rgb[2] * 0xFF)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def custom_frame(row, start, data):
|
|
20
|
+
result = b"\x0f\x03\x00\x00"
|
|
21
|
+
result += bytes((row, start, start + len(data) - 1))
|
|
22
|
+
for rgb in data:
|
|
23
|
+
result += bytes(rgb)
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def times255(rgb):
|
|
28
|
+
return int(rgb[0] * 0xFF), int(rgb[1] * 0xFF), int(rgb[2] * 0xFF)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RazerCtl:
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
34
|
+
self.sock.connect(glob.glob("/var/run/razer-analog-*")[0])
|
|
35
|
+
|
|
36
|
+
def send(self, data):
|
|
37
|
+
self.sock.sendall(bytes((len(data),)) + data)
|
|
38
|
+
|
|
39
|
+
if not data:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
pktlen = self.sock.recv(1)
|
|
43
|
+
received_data = self.sock.recv(pktlen[0])[7 : 7 + len(data)]
|
|
44
|
+
|
|
45
|
+
print("received_data, data", received_data, data)
|
|
46
|
+
|
|
47
|
+
return received_data
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main():
|
|
51
|
+
razerctl = RazerCtl()
|
|
52
|
+
|
|
53
|
+
# wave
|
|
54
|
+
# razerctl.send("\x0f\x02\x01\x05\x04\x01\x20")
|
|
55
|
+
|
|
56
|
+
# spectrum cycle
|
|
57
|
+
# razerctl.send("\x0f\x02\x01\x05\x03")
|
|
58
|
+
|
|
59
|
+
# custom
|
|
60
|
+
if False:
|
|
61
|
+
razerctl.send(b"\x0f\x02\x00\x00\x08")
|
|
62
|
+
|
|
63
|
+
# custom frame
|
|
64
|
+
if False:
|
|
65
|
+
t = time.localtime()
|
|
66
|
+
y = (t.tm_hour * 60 + t.tm_min) / (24 * 60)
|
|
67
|
+
razerctl.send(
|
|
68
|
+
custom_frame(0, 0, (times255(colorsys.hls_to_rgb(y, 0.5, 1)),) * 15)
|
|
69
|
+
)
|
|
70
|
+
razerctl.send(
|
|
71
|
+
custom_frame(1, 0, (times255(colorsys.hls_to_rgb(y, 0.5, 1)),) * 15)
|
|
72
|
+
)
|
|
73
|
+
razerctl.send(
|
|
74
|
+
custom_frame(2, 0, (times255(colorsys.hls_to_rgb(y, 0.5, 1)),) * 15)
|
|
75
|
+
)
|
|
76
|
+
razerctl.send(
|
|
77
|
+
custom_frame(3, 0, (times255(colorsys.hls_to_rgb(y, 0.5, 1)),) * 15)
|
|
78
|
+
)
|
|
79
|
+
razerctl.send(
|
|
80
|
+
custom_frame(4, 0, (times255(colorsys.hls_to_rgb(y, 0.5, 1)),) * 15)
|
|
81
|
+
)
|
|
82
|
+
if False:
|
|
83
|
+
razerctl.send(custom_frame(0, 0, ((0xFF, 0xFF, 0xFF),) * 1))
|
|
84
|
+
razerctl.send(custom_frame(0, 1, ((0, 0, 0),) * 14))
|
|
85
|
+
|
|
86
|
+
razerctl.send(custom_frame(1, 0, ((0, 0, 0),) * 2))
|
|
87
|
+
razerctl.send(custom_frame(1, 2, ((0xFF, 0xFF, 0xFF),) * 1))
|
|
88
|
+
razerctl.send(custom_frame(1, 3, ((0, 0, 0),) * 12))
|
|
89
|
+
|
|
90
|
+
razerctl.send(custom_frame(2, 0, ((0, 0, 0),) * 1))
|
|
91
|
+
razerctl.send(custom_frame(2, 1, ((0xFF, 0xFF, 0xFF),) * 3))
|
|
92
|
+
razerctl.send(custom_frame(2, 4, ((0, 0, 0),) * 2))
|
|
93
|
+
razerctl.send(custom_frame(2, 6, ((0xFF, 0xFF, 0xFF),) * 4))
|
|
94
|
+
|
|
95
|
+
razerctl.send(custom_frame(3, 0, ((0, 0, 0),) * 3))
|
|
96
|
+
razerctl.send(custom_frame(3, 3, ((0xFF, 0xFF, 0xFF),) * 1))
|
|
97
|
+
razerctl.send(custom_frame(3, 4, ((0, 0, 0),) * 12))
|
|
98
|
+
|
|
99
|
+
razerctl.send(custom_frame(4, 0, ((0, 0, 0),) * 15))
|
|
100
|
+
|
|
101
|
+
if False:
|
|
102
|
+
# brightness
|
|
103
|
+
razerctl.send(b"\x0f\x04\x00\x00\xFF")
|
|
104
|
+
|
|
105
|
+
if False:
|
|
106
|
+
# Get layout / ping
|
|
107
|
+
razerctl.send(b"\x00\x86\x00\x00")
|
|
108
|
+
|
|
109
|
+
if False:
|
|
110
|
+
# Device mode
|
|
111
|
+
razerctl.send(b"\x00\x04\x03\x00")
|
|
112
|
+
|
|
113
|
+
if False:
|
|
114
|
+
razerctl.send(b"\x05\x8A\x00")
|
|
115
|
+
razerctl.send(b"\x05\x03\x1d")
|
|
116
|
+
razerctl.send(b"\x06\x80\x00\x00")
|
|
117
|
+
# memory stats
|
|
118
|
+
# razerctl.send(b"\x06\x8e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
|
119
|
+
|
|
120
|
+
# razerctl.send(b"\x05\x80\x00")
|
|
121
|
+
# razerctl.send(b"\x00\x87\x00")
|
|
122
|
+
# razerctl.send(b"\x05\x88\x00\x00\x00\x00\xfa\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
|
123
|
+
# razerctl.send(b"\x05\x81\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
|
124
|
+
# razerctl.send(b"\x0f\x82\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
|
|
125
|
+
# razerctl.send(b"\x02\x12\x01\x11\x00\x00\x00\x02\x02\x00\x17\x00\x00\x00")
|
|
126
|
+
# razerctl.send(b"\x02\x12\x05\x11\x00\x00\x00\x02\x02\x00\x05\x00\x00\x00")
|
|
127
|
+
# razerctl.send(b"\x02\x12\x01\x11\x00\x00\x00\x02\x02\x00\x14\x00\x00\x00")
|
|
128
|
+
pass
|
|
129
|
+
if True:
|
|
130
|
+
# left meta = fn
|
|
131
|
+
razerctl.send(b"\x02\x12\x01\x7f\x00\x00\x00\x0c\x01\x01\x00\x00\x00\x00")
|
|
132
|
+
razerctl.send(b"\x02\x12\x01\x7f\x01\x00\x00\x0c\x01\x01\x00\x00\x00\x00")
|
|
133
|
+
|
|
134
|
+
razerctl.send(b"\x02\x12\x02\x7f\x00\x00\x00\x0c\x01\x01\x00\x00\x00\x00")
|
|
135
|
+
razerctl.send(b"\x02\x12\x02\x7f\x01\x00\x00\x0c\x01\x01\x00\x00\x00\x00")
|
|
136
|
+
if False:
|
|
137
|
+
# Read presets
|
|
138
|
+
razerctl.send(
|
|
139
|
+
b"\x05\x88\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
140
|
+
)
|
|
141
|
+
razerctl.send(
|
|
142
|
+
b"\x05\x88\x02\x00\x40\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
143
|
+
)
|
|
144
|
+
razerctl.send(
|
|
145
|
+
b"\x05\x88\x02\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
146
|
+
)
|
|
147
|
+
razerctl.send(
|
|
148
|
+
b"\x05\x88\x02\x00\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
while False:
|
|
152
|
+
for row in range(5):
|
|
153
|
+
randoms = []
|
|
154
|
+
for i in range(15):
|
|
155
|
+
randoms.append(
|
|
156
|
+
times255(colorsys.hls_to_rgb(random.uniform(0, 1), 0.5, 1))
|
|
157
|
+
)
|
|
158
|
+
razerctl.send(custom_frame(row, 0, randoms))
|
|
159
|
+
time.sleep(0.1)
|
|
160
|
+
|
|
161
|
+
def shutdown() -> None:
|
|
162
|
+
razerctl.send(b"")
|
|
163
|
+
|
|
164
|
+
atexit.register(shutdown)
|
|
165
|
+
# sock.sendall(b"\x0f\x02\x01\x05\x03")
|