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.
@@ -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")