fizzctl 0.2.0__tar.gz → 0.2.1__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.
- {fizzctl-0.2.0 → fizzctl-0.2.1}/PKG-INFO +4 -5
- {fizzctl-0.2.0 → fizzctl-0.2.1}/README.md +3 -3
- {fizzctl-0.2.0 → fizzctl-0.2.1}/pyproject.toml +2 -5
- fizzctl-0.2.1/pyproject.toml.orig +41 -0
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/blobs.py +25 -125
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/capture.py +1 -20
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/cfg.py +0 -12
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/cli.py +26 -61
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/effects.py +69 -26
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/hid.py +18 -74
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/protocol.py +5 -18
- fizzctl-0.2.0/pyproject.toml.orig +0 -46
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/__init__.py +0 -0
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/animations.py +0 -0
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/keymap.py +0 -0
- {fizzctl-0.2.0 → fizzctl-0.2.1}/src/fizzctl/udev_rules.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fizzctl
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Control a Redragon K617 Fizz keyboard on Linux: solid colors, 22 firmware-native RGB effects, per-key painting, live animations, and Cfg.ini keymap restore — no vendor software required
|
|
5
5
|
Keywords: redragon,k617,fizz,rgb,keyboard,backlight,led,hid,linux
|
|
6
6
|
Author: Ayoub Dya
|
|
@@ -14,7 +14,6 @@ Classifier: Programming Language :: Python :: 3
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.14
|
|
15
15
|
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
16
16
|
Classifier: Topic :: System :: Hardware
|
|
17
|
-
Requires-Dist: dpkt>=1.9.8
|
|
18
17
|
Requires-Dist: hidapi>=0.15.0
|
|
19
18
|
Requires-Python: >=3.14
|
|
20
19
|
Description-Content-Type: text/markdown
|
|
@@ -91,9 +90,9 @@ fizzctl rgb 00ff00 --brightness 4 # or: rgb 00ff00 -b 4
|
|
|
91
90
|
### Firmware effects
|
|
92
91
|
|
|
93
92
|
All 22 effects from the official Redragon software are supported, with full
|
|
94
|
-
speed and brightness control. `speed` and `brightness`
|
|
95
|
-
matching the firmware); higher = faster / brighter. Run
|
|
96
|
-
with no name to list every effect:
|
|
93
|
+
speed and brightness control. `speed` is 1..5 and `brightness` is 0..4 (5
|
|
94
|
+
levels each, matching the firmware); higher = faster / brighter. Run
|
|
95
|
+
`fizzctl effect` with no name to list every effect:
|
|
97
96
|
|
|
98
97
|
```bash
|
|
99
98
|
fizzctl effect
|
|
@@ -70,9 +70,9 @@ fizzctl rgb 00ff00 --brightness 4 # or: rgb 00ff00 -b 4
|
|
|
70
70
|
### Firmware effects
|
|
71
71
|
|
|
72
72
|
All 22 effects from the official Redragon software are supported, with full
|
|
73
|
-
speed and brightness control. `speed` and `brightness`
|
|
74
|
-
matching the firmware); higher = faster / brighter. Run
|
|
75
|
-
with no name to list every effect:
|
|
73
|
+
speed and brightness control. `speed` is 1..5 and `brightness` is 0..4 (5
|
|
74
|
+
levels each, matching the firmware); higher = faster / brighter. Run
|
|
75
|
+
`fizzctl effect` with no name to list every effect:
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
fizzctl effect
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fizzctl"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "Control a Redragon K617 Fizz keyboard on Linux: solid colors, 22 firmware-native RGB effects, per-key painting, live animations, and Cfg.ini keymap restore — no vendor software required"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
keywords = [
|
|
@@ -25,10 +25,7 @@ classifiers = [
|
|
|
25
25
|
"Topic :: System :: Hardware",
|
|
26
26
|
]
|
|
27
27
|
requires-python = ">=3.14"
|
|
28
|
-
dependencies = [
|
|
29
|
-
"dpkt>=1.9.8",
|
|
30
|
-
"hidapi>=0.15.0",
|
|
31
|
-
]
|
|
28
|
+
dependencies = ["hidapi>=0.15.0"]
|
|
32
29
|
|
|
33
30
|
[project.license]
|
|
34
31
|
text = "MIT"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fizzctl"
|
|
3
|
+
version = "0.2.1"
|
|
4
|
+
description = "Control a Redragon K617 Fizz keyboard on Linux: solid colors, 22 firmware-native RGB effects, per-key painting, live animations, and Cfg.ini keymap restore — no vendor software required"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [{ name = "Ayoub Dya", email = "ayoubdya@gmail.com" }]
|
|
8
|
+
keywords = [
|
|
9
|
+
"redragon",
|
|
10
|
+
"k617",
|
|
11
|
+
"fizz",
|
|
12
|
+
"rgb",
|
|
13
|
+
"keyboard",
|
|
14
|
+
"backlight",
|
|
15
|
+
"led",
|
|
16
|
+
"hid",
|
|
17
|
+
"linux",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Environment :: Console",
|
|
22
|
+
"Intended Audience :: End Users/Desktop",
|
|
23
|
+
"Operating System :: POSIX :: Linux",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: System :: Hardware :: Hardware Drivers",
|
|
27
|
+
"Topic :: System :: Hardware",
|
|
28
|
+
]
|
|
29
|
+
requires-python = ">=3.14"
|
|
30
|
+
dependencies = ["hidapi>=0.15.0"]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
fizzctl = "fizzctl:main"
|
|
34
|
+
fizzctl-dev = "fizzctl:main_dev"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["uv_build>=0.12.13,<0.13.0"]
|
|
38
|
+
build-backend = "uv_build"
|
|
39
|
+
|
|
40
|
+
[tool.uv]
|
|
41
|
+
package = true
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
"""Baked firmware bytes, inlined as hex so the package ships no binary files.
|
|
2
2
|
|
|
3
3
|
All constants come from USB captures of the OEM software:
|
|
4
|
-
* CONST_MODE / CONST_CANVAS / CONST_ROUTING / CONST_EXEC
|
|
4
|
+
* CONST_MODE / CONST_CANVAS / CONST_ROUTING / CONST_EXEC — the
|
|
5
5
|
four constant 1032-byte frames of the Restore sequence.
|
|
6
|
-
*
|
|
6
|
+
* FW_TEMPLATE — the 5-frame effect template: shared MODE and ROUTING,
|
|
7
|
+
a distinct CANVAS, and five byte overrides of CONST_EXEC.
|
|
8
|
+
* RGB_EXEC / RGB_SEC — the whole-board solid-color variant.
|
|
7
9
|
"""
|
|
8
10
|
|
|
11
|
+
|
|
12
|
+
def _patch(base: bytes, diffs: dict[int, int]) -> bytes:
|
|
13
|
+
"""Copy `base`, overriding a handful of byte offsets."""
|
|
14
|
+
b = bytearray(base)
|
|
15
|
+
for off, val in diffs.items():
|
|
16
|
+
b[off] = val
|
|
17
|
+
return bytes(b)
|
|
18
|
+
|
|
19
|
+
|
|
9
20
|
CONST_MODE = bytes.fromhex(
|
|
10
21
|
"0608b80040000000000000000000000000000000000000000000000000ff0000"
|
|
11
22
|
"0000ff00ff00ffff00ff00ff00ffffffffffff00000000ff00ff00ffff00ff00"
|
|
@@ -150,49 +161,9 @@ CONST_EXEC = bytes.fromhex(
|
|
|
150
161
|
"0000000000000000"
|
|
151
162
|
)
|
|
152
163
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
"0583b6000000"
|
|
157
|
-
),
|
|
158
|
-
# frame 1: 1032B
|
|
159
|
-
bytes.fromhex(
|
|
160
|
-
"0608b80040000000000000000000000000000000000000000000000000ff0000"
|
|
161
|
-
"0000ff00ff00ffff00ff00ff00ffffffffffff00000000ff00ff00ffff00ff00"
|
|
162
|
-
"ff00ffffffffffff00000000ff00ff00ffff00ff00ff00ffffffffffff000000"
|
|
163
|
-
"00ff00ff00ffff00ff00ff00ffffffffffff00000000ff00ff00ffff00ff00ff"
|
|
164
|
-
"00ffffffffffff00000000ff00ff00ffff00ff00ff00ffffffffffff00000000"
|
|
165
|
-
"ff00ff00ffff00ff00ff00ffffffffffff00000000ff00ff00ffff00ff00ff00"
|
|
166
|
-
"ffffffffffff00000000ff00ff00ffff00ff00ff00ffffffffffff00000000ff"
|
|
167
|
-
"00ff00ffff00ff00ff00ffffffffffff00000000ff00ff00ffff00ff00ff00ff"
|
|
168
|
-
"ffffffffff00000000ff00ff00ffff00ff00ff00ffffffffffff00000000ff00"
|
|
169
|
-
"ff00ffff00ff00ff00ffffffffffff00000000ff00ff00ffff00ff00ff00ffff"
|
|
170
|
-
"ffffffff00000000ff00ff00ffff00ff00ff00ffffffffffff00000000ff00ff"
|
|
171
|
-
"00ffff00ff00ff00ffffffffffff00000000ff00ff00ffff00ff00ff00ffffff"
|
|
172
|
-
"ffffff00000000ff00ff00ffff00ff00ff00ffffffffffff00000000ff00ff00"
|
|
173
|
-
"ffff00ff00ff00ffffffffffff00000000ff00ff00ffff00ff00ff00ffffffff"
|
|
174
|
-
"ffff00000000ff00ff00ffff00ff00ff00ffffffffffff00000000ff00ff00ff"
|
|
175
|
-
"ff00ff00ff00ffffffffff000000000000000000000000000000000000000000"
|
|
176
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
177
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
178
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
179
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
180
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
181
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
182
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
183
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
184
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
185
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
186
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
187
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
188
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
189
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
190
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
191
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
192
|
-
"0000000000000000"
|
|
193
|
-
),
|
|
194
|
-
# frame 2: 1032B
|
|
195
|
-
bytes.fromhex(
|
|
164
|
+
INIT = bytes.fromhex("0583b6000000")
|
|
165
|
+
|
|
166
|
+
FW_CANVAS = bytes.fromhex(
|
|
196
167
|
"0609bc0040000000000000000000000000000000000000000000000000000000"
|
|
197
168
|
"00000000000000000000000000000000000000ff00ff00ff0000000000000000"
|
|
198
169
|
"0000000000000000ffffff00ff0000000000000000000000000000000000ff00"
|
|
@@ -226,89 +197,18 @@ FW_FRAMES = [
|
|
|
226
197
|
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
227
198
|
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
228
199
|
"0000000000000000"
|
|
229
|
-
|
|
230
|
-
# frame 3: 1032B
|
|
231
|
-
bytes.fromhex(
|
|
232
|
-
"0609c00040000000000000000000ffffffffffffff0000000000000000000000"
|
|
233
|
-
"000000ffffffffff00000000000000000000000000000000ffffffffff000000"
|
|
234
|
-
"000000000000000000000000ff000000ffff0000000000000000000000000000"
|
|
235
|
-
"00ff000000000000000000000000000000000000000000000000000000000000"
|
|
236
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
237
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
238
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
239
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
240
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
241
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
242
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
243
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
244
|
-
"0000000000000000ffffffffffffff0000000000000000000000000000ffffff"
|
|
245
|
-
"ff0000000000000000000000000000000000ffffffff00000000000000000000"
|
|
246
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
247
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
248
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
249
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
250
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
251
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
252
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
253
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
254
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
255
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
256
|
-
"000000000000000000000000000000000000000000000000ff00ff0000000000"
|
|
257
|
-
"000000000000000000000000ffffff0000000000000000000000000000000000"
|
|
258
|
-
"ff0000000000000000000000000000000000000000ff00ff0000000000000000"
|
|
259
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
260
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
261
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
262
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
263
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
264
|
-
"0000000000000000"
|
|
265
|
-
),
|
|
266
|
-
# frame 4: 1032B
|
|
267
|
-
bytes.fromhex(
|
|
268
|
-
"0603b600000000000000000000005aa503030000000120010000000055550100"
|
|
269
|
-
"00000000ffff0034073307330733073307330733073307330733073307330733"
|
|
270
|
-
"07330733073307330733073307335aa500100744074407440744074407440744"
|
|
271
|
-
"0404040404040404040400000000000000000000000000000000000000000000"
|
|
272
|
-
"0000000000000000005aa5030300000000000000000000000000000000000000"
|
|
273
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
274
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
275
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
276
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
277
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
278
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
279
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
280
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
281
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
282
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
283
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
284
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
285
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
286
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
287
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
288
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
289
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
290
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
291
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
292
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
293
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
294
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
295
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
296
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
297
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
298
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
299
|
-
"0000000000000000000000000000000000000000000000000000000000000000"
|
|
300
|
-
"0000000000000000"
|
|
301
|
-
),
|
|
302
|
-
]
|
|
200
|
+
)
|
|
303
201
|
|
|
202
|
+
EXEC_DIFFS = {18: 0, 21: 1, 38: 0, 39: 52, 141: 0}
|
|
304
203
|
|
|
204
|
+
FW_TEMPLATE = [
|
|
205
|
+
INIT,
|
|
206
|
+
CONST_MODE,
|
|
207
|
+
FW_CANVAS,
|
|
208
|
+
CONST_ROUTING,
|
|
209
|
+
_patch(CONST_EXEC, EXEC_DIFFS),
|
|
210
|
+
]
|
|
305
211
|
|
|
306
|
-
# --- RGB static-color protocol ---
|
|
307
|
-
RGB_INIT = bytes.fromhex("0583b6000000")
|
|
308
|
-
# P1 canvas is BUILT (header + split-plane colors + SEC at 660), not taken from
|
|
309
|
-
# a template. P2 routing is the same 06 09 c0 block used by the Restore
|
|
310
|
-
# sequence (matches RESTORE_CONSTANT_FRAMES[2] exactly). EXEC must be the
|
|
311
|
-
# static-color variant: effect_id=0x01, brightness=0x15.
|
|
312
212
|
RGB_SEC = bytes.fromhex(
|
|
313
213
|
"ffffffffff000000000000000000000000000000ffffffffff00000000000000"
|
|
314
214
|
"00000000000000000000ffffff00ff000000000000000000000000000000ff00"
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""USB capture import/diff/export for the K617 OEM software.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Input accepted:
|
|
4
4
|
* Wireshark JSON : `tshark -r cap.pcapng -T json > cap.json` (recommended)
|
|
5
|
-
* raw .pcap : Linux usbmon capture, parsed with dpkt
|
|
6
5
|
|
|
7
6
|
The job:
|
|
8
7
|
1. pull every HID feature-report payload out of the USB stream,
|
|
@@ -147,24 +146,6 @@ def _parse_usb_layer(u: dict, setup: dict | None = None) -> FrameCapture | None:
|
|
|
147
146
|
)
|
|
148
147
|
|
|
149
148
|
|
|
150
|
-
def load_pcap_usbmon(path: str) -> list[FrameCapture]:
|
|
151
|
-
"""Minimal Linux usbmon .pcap reader (dpkt)."""
|
|
152
|
-
import dpkt
|
|
153
|
-
|
|
154
|
-
out: list[FrameCapture] = []
|
|
155
|
-
with open(path, "rb") as fh:
|
|
156
|
-
pcap = dpkt.pcap.Reader(fh)
|
|
157
|
-
for _, buf in pcap:
|
|
158
|
-
try:
|
|
159
|
-
u = dpkt.usbmon.LinuxUSB(buf)
|
|
160
|
-
except Exception:
|
|
161
|
-
continue
|
|
162
|
-
data = bytes(u.transfer_buffer) if u.xfer_type & 0x80 else None
|
|
163
|
-
direction = "IN" if u.urb_type == 0x55 else "OUT"
|
|
164
|
-
out.append(FrameCapture(direction=direction, transfer="urb", request=None, report_id=None, data=data or b""))
|
|
165
|
-
return out
|
|
166
|
-
|
|
167
|
-
|
|
168
149
|
def significant(records: list[FrameCapture], direction: str = "OUT") -> list[FrameCapture]:
|
|
169
150
|
"""Keep only payload-bearing writes from the host (the interesting direction)."""
|
|
170
151
|
return [r for r in records if r.direction == direction and len(r.data) > 0]
|
|
@@ -104,15 +104,3 @@ class CfgIni:
|
|
|
104
104
|
@property
|
|
105
105
|
def key_entries(self) -> list[tuple[int, KeyEntry]]:
|
|
106
106
|
return sorted(self.keys.items())
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if __name__ == "__main__":
|
|
110
|
-
import sys
|
|
111
|
-
cfg = CfgIni(sys.argv[1] if len(sys.argv) > 1 else "Cfg.ini")
|
|
112
|
-
print(f"OPT: {len(cfg.opt)} keys")
|
|
113
|
-
print(f"[FN] {len(cfg.fn)} mappings")
|
|
114
|
-
for idx, be in cfg.fn_entries:
|
|
115
|
-
print(f" K{idx:<3} = {', '.join(f'0x{b:02X}' for b in be)}")
|
|
116
|
-
print(f"[KEY] {len(cfg.keys)} keys")
|
|
117
|
-
for idx, e in cfg.key_entries:
|
|
118
|
-
print(f" K{idx:<3} = {e.matrix} {', '.join(f'0x{b:02X}' for b in e.behavior)}")
|
|
@@ -26,7 +26,7 @@ import sys
|
|
|
26
26
|
from .animations import cmd_animate
|
|
27
27
|
from .capture import diff_captures, export_frames, load_frames, load_tshark_json, significant
|
|
28
28
|
from .cfg import CfgIni
|
|
29
|
-
from .hid import
|
|
29
|
+
from .hid import NoDeviceError, open_device, send_burst
|
|
30
30
|
from .keymap import KeymapEncoder
|
|
31
31
|
from .protocol import RESTORE_CONSTANT_FRAMES
|
|
32
32
|
|
|
@@ -91,7 +91,7 @@ def cmd_replay(args):
|
|
|
91
91
|
if dev is None:
|
|
92
92
|
return 1
|
|
93
93
|
try:
|
|
94
|
-
dev
|
|
94
|
+
send_burst(dev, frames, handshake=False, delay_ms=args.delay_ms)
|
|
95
95
|
finally:
|
|
96
96
|
dev.close()
|
|
97
97
|
return 0
|
|
@@ -123,7 +123,7 @@ def cmd_keymap(args):
|
|
|
123
123
|
if dev is None:
|
|
124
124
|
return 1
|
|
125
125
|
try:
|
|
126
|
-
dev
|
|
126
|
+
send_burst(dev, frames, handshake=False, delay_ms=args.delay_ms)
|
|
127
127
|
finally:
|
|
128
128
|
dev.close()
|
|
129
129
|
print(f"wrote keymap from {args.cfg} to flash")
|
|
@@ -151,24 +151,22 @@ def cmd_effect(args):
|
|
|
151
151
|
fizzctl effect waterfall --color ff8800
|
|
152
152
|
fizzctl effect static --brightness 1
|
|
153
153
|
"""
|
|
154
|
-
from .effects import
|
|
155
|
-
|
|
154
|
+
from .effects import (
|
|
155
|
+
EFFECT_ACCEPTS_COLOR, EFFECT_DEFAULTS, EFFECT_ID,
|
|
156
|
+
_ALIASES, encode_firmware_effect, parse_color,
|
|
157
|
+
)
|
|
156
158
|
|
|
157
|
-
name = args.name
|
|
158
|
-
if name is None:
|
|
159
|
+
name = _ALIASES.get(args.name, args.name)
|
|
160
|
+
if name is None or name not in EFFECT_ID:
|
|
161
|
+
if name is not None:
|
|
162
|
+
print(f"unknown effect {name!r}")
|
|
159
163
|
print("Firmware effects (22). Run like: fizzctl effect rainbow --speed 2 --brightness 4")
|
|
160
164
|
for n, eid in EFFECT_ID.items():
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
print(f" {n:18s} id={eid:#04x} color:{color:3s} default sb={defs[0]}.{defs[1]}")
|
|
164
|
-
return 0
|
|
165
|
-
if name not in EFFECT_ID:
|
|
166
|
-
print(f"unknown effect {name!r}. Available ({', '.join(EFFECT_ID)}):")
|
|
167
|
-
for n, eid in EFFECT_ID.items():
|
|
168
|
-
defs = EFFECT_DEFAULTS[n]
|
|
165
|
+
spd, bri = EFFECT_DEFAULTS[n]
|
|
166
|
+
spd = spd + 1 if spd else 0 # stored 0-based, displayed 1..5
|
|
169
167
|
color = "yes" if n in EFFECT_ACCEPTS_COLOR else "-"
|
|
170
|
-
print(f" {n:18s} id={eid:#04x} color:{color:3s} default
|
|
171
|
-
return 1
|
|
168
|
+
print(f" {n:18s} id={eid:#04x} color:{color:3s} default speed={spd} brightness={bri}")
|
|
169
|
+
return 0 if name is None else 1
|
|
172
170
|
|
|
173
171
|
color = None
|
|
174
172
|
if args.color:
|
|
@@ -185,7 +183,7 @@ def cmd_effect(args):
|
|
|
185
183
|
if dev is None:
|
|
186
184
|
return 1
|
|
187
185
|
try:
|
|
188
|
-
|
|
186
|
+
send_burst(dev, frames)
|
|
189
187
|
finally:
|
|
190
188
|
dev.close()
|
|
191
189
|
desc = f"effect {name}"
|
|
@@ -198,38 +196,17 @@ def cmd_effect(args):
|
|
|
198
196
|
|
|
199
197
|
|
|
200
198
|
def cmd_key(args):
|
|
201
|
-
"""Paint a single key
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
from .protocol import NAME_TO_INDEX
|
|
205
|
-
|
|
206
|
-
color = parse_color(args.color)
|
|
207
|
-
if color is None:
|
|
208
|
-
print(f"bad color {args.color!r}")
|
|
209
|
-
return 1
|
|
210
|
-
if args.key not in NAME_TO_INDEX:
|
|
211
|
-
print(f"unknown key {args.key!r}. Available: {', '.join(sorted(NAME_TO_INDEX))}")
|
|
212
|
-
return 1
|
|
213
|
-
frames = rgb_sequence({args.key: color})
|
|
214
|
-
try:
|
|
215
|
-
dev = open_device(debug=args.debug)
|
|
216
|
-
except NoDeviceError:
|
|
217
|
-
return 1
|
|
218
|
-
if dev is None:
|
|
219
|
-
return 1
|
|
220
|
-
try:
|
|
221
|
-
send_rgb(dev, frames)
|
|
222
|
-
print(f"painted {args.key} -> #{color[0]:02x}{color[1]:02x}{color[2]:02x}")
|
|
223
|
-
finally:
|
|
224
|
-
dev.close()
|
|
225
|
-
return 0
|
|
199
|
+
"""Paint a single key (`key W red` == `paint W=red`)."""
|
|
200
|
+
args.specs = [f"{args.key}={args.color}"]
|
|
201
|
+
return cmd_paint(args)
|
|
226
202
|
|
|
227
203
|
|
|
228
204
|
def cmd_paint(args):
|
|
229
205
|
"""Paint many keys via the CANVAS + 5AA5 execute path (flash write):
|
|
230
206
|
fizzctl paint W=ff0000 A=00ff00 S=0000ff D=ffffff
|
|
231
207
|
"""
|
|
232
|
-
from .
|
|
208
|
+
from .effects import parse_color
|
|
209
|
+
from .hid import rgb_sequence
|
|
233
210
|
from .protocol import NAME_TO_INDEX
|
|
234
211
|
|
|
235
212
|
colors = {}
|
|
@@ -237,7 +214,7 @@ def cmd_paint(args):
|
|
|
237
214
|
if "=" not in spec:
|
|
238
215
|
print(f"bad spec {spec!r}: expected KEY=COLOR")
|
|
239
216
|
return 1
|
|
240
|
-
key, c = spec.
|
|
217
|
+
key, c = spec.rsplit("=", 1)
|
|
241
218
|
if key not in NAME_TO_INDEX:
|
|
242
219
|
print(f"unknown key {key!r}")
|
|
243
220
|
return 1
|
|
@@ -254,25 +231,13 @@ def cmd_paint(args):
|
|
|
254
231
|
if dev is None:
|
|
255
232
|
return 1
|
|
256
233
|
try:
|
|
257
|
-
|
|
258
|
-
print(f"painted {len(colors)}
|
|
234
|
+
send_burst(dev, frames)
|
|
235
|
+
print(f"painted {len(colors)} key{'s' if len(colors) != 1 else ''}")
|
|
259
236
|
finally:
|
|
260
237
|
dev.close()
|
|
261
238
|
return 0
|
|
262
239
|
|
|
263
240
|
|
|
264
|
-
def parse_color(s: str) -> tuple[int, int, int] | None:
|
|
265
|
-
from .effects import parse_color as _pc
|
|
266
|
-
|
|
267
|
-
return _pc(s)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def _kind(frame: bytes) -> str:
|
|
271
|
-
from .protocol import frame_kind
|
|
272
|
-
|
|
273
|
-
return frame_kind(frame)
|
|
274
|
-
|
|
275
|
-
|
|
276
241
|
def _build_parser(dev: bool) -> argparse.ArgumentParser:
|
|
277
242
|
p = argparse.ArgumentParser(
|
|
278
243
|
prog="fizzctl" if not dev else "fizzctl-dev",
|
|
@@ -323,8 +288,8 @@ Examples:
|
|
|
323
288
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
324
289
|
peff.add_argument("name", nargs="?",
|
|
325
290
|
help="effect name (omit or run `effect` alone to list all)")
|
|
326
|
-
peff.add_argument("-c", "--color", help="base color (name or hex) —
|
|
327
|
-
peff.add_argument("-s", "--speed", type=int, help="
|
|
291
|
+
peff.add_argument("-c", "--color", help="base color (name or hex) — RGB/single-color mode")
|
|
292
|
+
peff.add_argument("-s", "--speed", type=int, help="1..5 (level; higher = faster)")
|
|
328
293
|
peff.add_argument("-b", "--brightness", type=int, help="0..4 (level; higher = brighter)")
|
|
329
294
|
|
|
330
295
|
pk = sub.add_parser("key",
|
|
@@ -4,12 +4,24 @@ Two independent protocols:
|
|
|
4
4
|
|
|
5
5
|
* Firmware effects — a 5-frame burst, all built from the fw-static template
|
|
6
6
|
by patching a few bytes:
|
|
7
|
-
MODE[
|
|
8
|
-
|
|
9
|
-
EXEC[
|
|
7
|
+
MODE[218..220] = base color (R,G,B) — the real color field (per-effect
|
|
8
|
+
slot; sine-wave uses MODE[281..283])
|
|
9
|
+
EXEC[38+2*(id-1)] = per-effect RGB/color toggle: 0x07 multicolor,
|
|
10
|
+
0x00 render the MODE base color
|
|
11
|
+
EXEC[21] = effect_id (selects rainbow/snake/wheel/...)
|
|
12
|
+
EXEC[39] = packed nibbles (high=speed-1 (0..4), low=brightness 0..4)
|
|
10
13
|
Sending requires the mandatory GET_REPORT(0x06, 1032) handshake after INIT
|
|
11
14
|
(without it the firmware silently ignores the burst).
|
|
12
15
|
|
|
16
|
+
Speed is stored 0-based: the firmware displays ``nibble + 1`` as speed
|
|
17
|
+
1..5, so passing ``--speed 1`` stores 0. Verified on hardware (sine-wave:
|
|
18
|
+
sending 1/2/3 showed 2/3/4 before the fix). Brightness is stored raw
|
|
19
|
+
(0..4). The color mechanism was verified from OEM captures (same effect in
|
|
20
|
+
red/green/blue differs only at the effect's MODE color slots, with that
|
|
21
|
+
effect's flag byte zeroed); all stock templates bake every flag byte to
|
|
22
|
+
0x07 (multicolor), so the base color needs the effect's own flag flipped to
|
|
23
|
+
0x00 to take effect — EXEC[56] (the old assumption) is only snake's slot.
|
|
24
|
+
|
|
13
25
|
* Per-key paint — a SINGLE 382-byte feature report ``08 0a 7a 01`` followed by
|
|
14
26
|
96 RGB triplets in a 16-col x 6-row column-major raster (pos = col*6+row).
|
|
15
27
|
No handshake, no flash commit, host-side (volatile) — bytes are re-applied
|
|
@@ -26,7 +38,6 @@ from __future__ import annotations
|
|
|
26
38
|
# Baseline is the captured fw-static template (init + mode + canvas + routing
|
|
27
39
|
# + exec). Every other effect only differs in the 3-5 bytes listed below.
|
|
28
40
|
# Indexes into base_frames(): 0=INIT, 1=MODE, 2=CANVAS, 3=ROUTING, 4=EXEC.
|
|
29
|
-
from .blobs import RGB_EXEC, RGB_INIT, RGB_SEC
|
|
30
41
|
from .protocol import base_frames
|
|
31
42
|
|
|
32
43
|
# Official Redragon software effect menu (order from K617 software).
|
|
@@ -61,6 +72,23 @@ EFFECTS = [
|
|
|
61
72
|
("off", ("off",), 0x16, True, 0x00, ""),
|
|
62
73
|
]
|
|
63
74
|
|
|
75
|
+
# The base color slot in the MODE frame is effect-specific. Verified:
|
|
76
|
+
# fixed-on (0x01) -> MODE[29..31] live-tested green on hardware
|
|
77
|
+
# snake (0x0a) -> MODE[218,219,220] live-tested blue on hardware
|
|
78
|
+
# sine-wave (0x0d) -> MODE[280..284]-ish: red/green OEM captures moved
|
|
79
|
+
# bytes [281]=R and [282]=G (byte [280]=0xff constant; B at [283]
|
|
80
|
+
# inferred since both captured colors had B=0). Everything else
|
|
81
|
+
# defaults to the [218..220] slot (snake-verified).
|
|
82
|
+
_COLOR_SLOTS: dict[str, tuple[int, int, int]] = {
|
|
83
|
+
"fixed-on": (29, 30, 31),
|
|
84
|
+
"sine-wave": (281, 282, 283),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _color_slot(name: str) -> tuple[int, int, int]:
|
|
89
|
+
return _COLOR_SLOTS.get(name, (218, 219, 220))
|
|
90
|
+
|
|
91
|
+
|
|
64
92
|
EFFECT_ID: dict[str, int] = {n: eid for n, _, eid, *_ in EFFECTS}
|
|
65
93
|
_ALIASES: dict[str, str] = {a: n for n, as_, *_ in EFFECTS for a in as_}
|
|
66
94
|
EFFECT_ACCEPTS_COLOR = {n for n, _, _, ac, *_ in EFFECTS if ac}
|
|
@@ -89,11 +117,24 @@ def encode_firmware_effect(
|
|
|
89
117
|
brightness: int | None = None,
|
|
90
118
|
) -> list[bytes]:
|
|
91
119
|
"""Encode the 5-frame burst (INIT, MODE, CANVAS, ROUTING, EXEC) for an
|
|
92
|
-
effect. Patches MODE
|
|
93
|
-
EXEC[
|
|
120
|
+
effect. Patches the effect's MODE color slot, EXEC[21] (effect_id), its
|
|
121
|
+
EXEC[38+2*(id-1)] RGB/color toggle and the speed/brightness onto the
|
|
122
|
+
fw-static baseline.
|
|
123
|
+
|
|
124
|
+
The color mechanism was established from OEM captures: applying the same
|
|
125
|
+
effect in red/green/blue differs ONLY at the MODE color slot (snake:
|
|
126
|
+
MODE[218..220]; sine-wave: MODE[281..283]). Each effect owns an
|
|
127
|
+
RGB/color toggle byte at EXEC[38+2*(id-1)]: 0x07 = multicolor animation,
|
|
128
|
+
0x00 = render the MODE base color instead. Stock templates bake every
|
|
129
|
+
toggle to 0x07 (the OEM's "RGB" checkbox is ON by default), which is why
|
|
130
|
+
colors silently never took effect before — and EXEC[56] is only snake's
|
|
131
|
+
toggle, not a global one.
|
|
94
132
|
|
|
95
|
-
speed
|
|
96
|
-
clamped
|
|
133
|
+
OEM slider ranges: speed 1..5, brightness 0..4 (5 levels each). Values
|
|
134
|
+
are clamped when the user passes them; missing flags keep the template
|
|
135
|
+
default (so ``off`` at 0x00 is preserved). A color is only applied when
|
|
136
|
+
the effect accepts one; when no color is given the baked RGB-mode flag
|
|
137
|
+
is left untouched.
|
|
97
138
|
"""
|
|
98
139
|
name = _canonical(name)
|
|
99
140
|
eid = EFFECT_ID[name]
|
|
@@ -102,28 +143,31 @@ def encode_firmware_effect(
|
|
|
102
143
|
frames = [bytearray(f) for f in base_frames()]
|
|
103
144
|
mode, canvas, routing, exec_ = frames[1], frames[2], frames[3], frames[4]
|
|
104
145
|
|
|
105
|
-
if name in EFFECT_ACCEPTS_COLOR:
|
|
106
|
-
r, g, b = color
|
|
107
|
-
|
|
146
|
+
if color is not None and name in EFFECT_ACCEPTS_COLOR:
|
|
147
|
+
r, g, b = color[0] & 0xFF, color[1] & 0xFF, color[2] & 0xFF
|
|
148
|
+
ro, go, bo = _color_slot(name)
|
|
149
|
+
mode[ro], mode[go], mode[bo] = r, g, b
|
|
150
|
+
# EXEC[38 + 2*(eid-1)] is this effect's RGB/single-color flag byte
|
|
151
|
+
# (0x07 = RGB/random, 0x00 = render MODE color). EXEC[56] is the
|
|
152
|
+
# snake (eid 10) slot -- not a global toggle, which is why sine stayed
|
|
153
|
+
# multicolor before the per-effect slot was used.
|
|
154
|
+
exec_[38 + 2 * (eid - 1)] = 0x00
|
|
108
155
|
|
|
109
156
|
exec_[21] = eid
|
|
110
157
|
|
|
111
|
-
#
|
|
112
|
-
# low nibble = brightness).
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
new_bright = max(0, min(4, round(target_bright)))
|
|
158
|
+
# EXEC[39] is the live speed×brightness value (high nibble = speed,
|
|
159
|
+
# low nibble = brightness). Evidence: an OEM solid-color capture carries
|
|
160
|
+
# 0x32 there, the fw-static template 0x34, and the OEM keeps per-effect
|
|
161
|
+
# remembered values in a 20-slot table at EXEC[39 + 2*(id-1)] (ids 1..20;
|
|
162
|
+
# the same capture shows saved 0x44 in slot 5 and 0x22 in slot 12). The
|
|
163
|
+
# value must land in both places or per-effect speed/brightness is ignored.
|
|
164
|
+
# Speed is stored 0-based: the OEM displays nibble+1 (hardware-verified).
|
|
165
|
+
new_speed = (max(1, min(5, round(speed))) - 1) if speed is not None else defaults[0]
|
|
166
|
+
new_bright = max(0, min(4, round(brightness))) if brightness is not None else defaults[1]
|
|
121
167
|
packed = ((new_speed & 0x0F) << 4) | (new_bright & 0x0F)
|
|
122
168
|
exec_[39] = packed
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if 1 <= eid <= 19:
|
|
126
|
-
exec_[39 + eid * 2] = packed
|
|
169
|
+
if 1 <= eid <= 20:
|
|
170
|
+
exec_[39 + 2 * (eid - 1)] = packed
|
|
127
171
|
|
|
128
172
|
return [bytes(frames[0])] + [bytes(f) for f in frames[1:]]
|
|
129
173
|
|
|
@@ -134,7 +178,6 @@ def encode_firmware_effect(
|
|
|
134
178
|
|
|
135
179
|
PERKEY_HEADER = bytes.fromhex("080a7a01")
|
|
136
180
|
PERKEY_PACKET_LEN = 382
|
|
137
|
-
SINODRAGON_LED_COUNT = 96
|
|
138
181
|
|
|
139
182
|
# K617 key name -> position in the 16x6 column-major raster (pos = col*6+row).
|
|
140
183
|
# Rows 1..4 in the 6-row raster hold rows 0..4 of the keyboard; raster row 0
|
|
@@ -16,7 +16,7 @@ import time
|
|
|
16
16
|
|
|
17
17
|
import hid
|
|
18
18
|
|
|
19
|
-
from .protocol import PID, VID
|
|
19
|
+
from .protocol import PID, VID, frame_kind
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class NoDeviceError(Exception):
|
|
@@ -100,17 +100,6 @@ class K617:
|
|
|
100
100
|
raw = self._dev.get_feature_report(report_id, size)
|
|
101
101
|
return bytes(raw) if raw is not None else bytes(size)
|
|
102
102
|
|
|
103
|
-
def send_sequence(self, frames: list[bytes], delay_ms: int = 30) -> None:
|
|
104
|
-
"""Send a list of frames sequentially (init -> data -> commit)."""
|
|
105
|
-
from time import sleep
|
|
106
|
-
|
|
107
|
-
for i, frame in enumerate(frames):
|
|
108
|
-
kind = _kind(frame)
|
|
109
|
-
self.send_feature(frame)
|
|
110
|
-
if self.debug:
|
|
111
|
-
print(f" [{i + 1}/{len(frames)}] {kind}")
|
|
112
|
-
sleep(delay_ms / 1000)
|
|
113
|
-
|
|
114
103
|
def close(self) -> None:
|
|
115
104
|
if self._dev is not None:
|
|
116
105
|
try:
|
|
@@ -161,12 +150,6 @@ def open_device(debug: bool = False) -> K617 | None:
|
|
|
161
150
|
return None
|
|
162
151
|
|
|
163
152
|
|
|
164
|
-
def _kind(frame: bytes) -> str:
|
|
165
|
-
from .protocol import frame_kind
|
|
166
|
-
|
|
167
|
-
return frame_kind(frame)
|
|
168
|
-
|
|
169
|
-
|
|
170
153
|
def rgb_sequence(led_colors: dict[int, tuple[int, int, int]]) -> list[bytes]:
|
|
171
154
|
"""Build the RGB-write payloads: [INIT, P1(no SEC yet), P2, EXEC].
|
|
172
155
|
|
|
@@ -175,7 +158,7 @@ def rgb_sequence(led_colors: dict[int, tuple[int, int, int]]) -> list[bytes]:
|
|
|
175
158
|
|
|
176
159
|
NOTE: the EXEC frame commits to flash (5AA5 magic). Do not loop this.
|
|
177
160
|
"""
|
|
178
|
-
from .blobs import
|
|
161
|
+
from .blobs import INIT, RGB_EXEC, RGB_SEC
|
|
179
162
|
from .protocol import NAME_TO_INDEX, RESTORE_CONSTANT_FRAMES, set_key_color
|
|
180
163
|
|
|
181
164
|
canvas = bytearray(1032)
|
|
@@ -186,68 +169,29 @@ def rgb_sequence(led_colors: dict[int, tuple[int, int, int]]) -> list[bytes]:
|
|
|
186
169
|
set_key_color(canvas, idx, rgb)
|
|
187
170
|
canvas[660:660 + len(RGB_SEC)] = RGB_SEC
|
|
188
171
|
p2 = RESTORE_CONSTANT_FRAMES[2] # routing, never modify
|
|
189
|
-
return [bytes(
|
|
190
|
-
|
|
172
|
+
return [bytes(INIT), bytes(canvas), p2, RGB_EXEC]
|
|
191
173
|
|
|
192
|
-
def rgb_all(color: tuple[int, int, int]) -> list[bytes]:
|
|
193
|
-
from .protocol import LED_INDEX
|
|
194
174
|
|
|
195
|
-
|
|
175
|
+
def send_burst(dev: K617, frames: list[bytes], handshake: bool = True,
|
|
176
|
+
delay_ms: int = 60) -> None:
|
|
177
|
+
"""Send an RGB write burst: INIT -> [mandatory GET handshake] -> blocks.
|
|
196
178
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
Protocol: INIT -> GET (handshake) -> P1 -> P2 -> EXEC.
|
|
202
|
-
Without the handshake the firmware silently ignores the writes.
|
|
203
|
-
"""
|
|
204
|
-
from time import sleep
|
|
205
|
-
|
|
206
|
-
init, canvas, p2, exec_ = frames
|
|
207
|
-
dev.send_feature(init)
|
|
208
|
-
if dev.debug:
|
|
209
|
-
print(" [1/4] INIT")
|
|
210
|
-
sleep(0.06)
|
|
211
|
-
resp = dev.get_feature(0x06, 1032) # mandatory handshake
|
|
212
|
-
if dev.debug:
|
|
213
|
-
print(f" [handshake] get_feature(0x06, 1032) -> {len(resp)}B")
|
|
214
|
-
sleep(0.06)
|
|
215
|
-
dev.send_feature(canvas)
|
|
216
|
-
if dev.debug:
|
|
217
|
-
print(" [2/4] CANVAS")
|
|
218
|
-
sleep(0.06)
|
|
219
|
-
dev.send_feature(p2)
|
|
220
|
-
if dev.debug:
|
|
221
|
-
print(" [3/4] ROUTING")
|
|
222
|
-
sleep(0.06)
|
|
223
|
-
dev.send_feature(exec_)
|
|
224
|
-
if dev.debug:
|
|
225
|
-
print(" [4/4] EXEC")
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
# firmware effects + per-key paint (see fizzctl.effects)
|
|
229
|
-
def send_firmware_effect(dev: K617, frames: list[bytes]) -> None:
|
|
230
|
-
"""Send the 5-frame firmware-effect burst WITH the mandatory handshake.
|
|
231
|
-
|
|
232
|
-
Protocol: INIT -> GET (handshake) -> MODE -> CANVAS -> ROUTING -> EXEC.
|
|
233
|
-
The EXEC block commits the effect selection to flash (5AA5 magic).
|
|
179
|
+
``handshake=True`` (rgb/effect/key/paint): the firmware ignores the burst
|
|
180
|
+
unless we poll GET_REPORT(0x06, 1032) after INIT. ``handshake=False``
|
|
181
|
+
(keymap/replay) skips it.
|
|
234
182
|
"""
|
|
235
183
|
from time import sleep
|
|
236
184
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if dev.debug:
|
|
240
|
-
print(" [1/5] INIT")
|
|
241
|
-
sleep(0.06)
|
|
242
|
-
resp = dev.get_feature(0x06, 1032) # mandatory handshake
|
|
243
|
-
if dev.debug:
|
|
244
|
-
print(f" [handshake] get_feature(0x06, 1032) -> {len(resp)}B")
|
|
245
|
-
sleep(0.06)
|
|
246
|
-
for i, block in enumerate(blocks, start=2):
|
|
247
|
-
dev.send_feature(block)
|
|
185
|
+
for i, frame in enumerate(frames, start=1):
|
|
186
|
+
dev.send_feature(frame)
|
|
248
187
|
if dev.debug:
|
|
249
|
-
print(f" [{i}/
|
|
250
|
-
sleep(
|
|
188
|
+
print(f" [{i}/{len(frames)}] {frame_kind(frame)}")
|
|
189
|
+
sleep(delay_ms / 1000)
|
|
190
|
+
if handshake and i == 1:
|
|
191
|
+
resp = dev.get_feature(0x06, 1032)
|
|
192
|
+
if dev.debug:
|
|
193
|
+
print(f" [handshake] get_feature(0x06, 1032) -> {len(resp)}B")
|
|
194
|
+
sleep(delay_ms / 1000)
|
|
251
195
|
|
|
252
196
|
|
|
253
197
|
def send_per_key(dev: K617, frame: bytes) -> None:
|
|
@@ -4,27 +4,18 @@ Nothing in this module talks to hardware; it's pure constants + helpers.
|
|
|
4
4
|
"""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
from .blobs import CONST_CANVAS, CONST_EXEC, CONST_MODE, CONST_ROUTING,
|
|
7
|
+
from .blobs import CONST_CANVAS, CONST_EXEC, CONST_MODE, CONST_ROUTING, FW_TEMPLATE
|
|
8
8
|
|
|
9
9
|
VID = 0x258A
|
|
10
10
|
PID = 0x0049
|
|
11
11
|
|
|
12
|
-
# ---- report IDs seen in HID descriptors (interface 1, vendor usage 0xFF00) ----
|
|
13
|
-
REPORT_IDS = [0x05, 0x06, 0x08]
|
|
14
|
-
|
|
15
|
-
# ---- known frame sizes ----
|
|
16
|
-
SIZE_INIT = 6
|
|
17
|
-
SIZE_BLOCK = 1032
|
|
18
|
-
SIZE_PERKEY = 382
|
|
19
|
-
|
|
20
12
|
# ---- known-good packet interiors (inlined in blobs.py from captures) ----
|
|
21
|
-
# Index in the static-effect template (
|
|
13
|
+
# Index in the static-effect template (base_frames()):
|
|
22
14
|
# 0 = INIT (05 83 b6 00 00 00)
|
|
23
15
|
# 1 = mode/config (06 08 b8 00 40 ...)
|
|
24
16
|
# 2 = RGB canvas base (06 09 bc 00 40 ...)
|
|
25
17
|
# 3 = routing (06 09 c0 00 40 ...) — same family as P2, never hand-edit
|
|
26
18
|
# 4 = EXEC/commit (06 03 b6 00 00 ...) — contains 5A A5 flash-commit magic
|
|
27
|
-
FRAME_INIT, FRAME_MODE, FRAME_CANVAS, FRAME_ROUTING, FRAME_EXEC = range(5)
|
|
28
19
|
|
|
29
20
|
# Constant frames of the Restore sequence (06 xx xx 00 40 ...), 1032 bytes each.
|
|
30
21
|
RESTORE_CONSTANT_FRAMES: list[bytes] = [
|
|
@@ -35,13 +26,9 @@ RESTORE_CONSTANT_FRAMES: list[bytes] = [
|
|
|
35
26
|
]
|
|
36
27
|
|
|
37
28
|
|
|
38
|
-
def base_frames(
|
|
39
|
-
"""Return the captured static-effect frame template.
|
|
40
|
-
|
|
41
|
-
`effect` is accepted for API compatibility; only the built-in template is
|
|
42
|
-
shipped (no data files on disk anymore).
|
|
43
|
-
"""
|
|
44
|
-
return [bytes(f) for f in FW_FRAMES]
|
|
29
|
+
def base_frames() -> list[bytes]:
|
|
30
|
+
"""Return the captured static-effect frame template."""
|
|
31
|
+
return [bytes(f) for f in FW_TEMPLATE]
|
|
45
32
|
|
|
46
33
|
|
|
47
34
|
def frame_kind(frame: bytes) -> str:
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "fizzctl"
|
|
3
|
-
version = "0.2.0"
|
|
4
|
-
description = "Control a Redragon K617 Fizz keyboard on Linux: solid colors, 22 firmware-native RGB effects, per-key painting, live animations, and Cfg.ini keymap restore — no vendor software required"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
license = { text = "MIT" }
|
|
7
|
-
authors = [
|
|
8
|
-
{ name = "Ayoub Dya", email = "ayoubdya@gmail.com" }
|
|
9
|
-
]
|
|
10
|
-
keywords = [
|
|
11
|
-
"redragon",
|
|
12
|
-
"k617",
|
|
13
|
-
"fizz",
|
|
14
|
-
"rgb",
|
|
15
|
-
"keyboard",
|
|
16
|
-
"backlight",
|
|
17
|
-
"led",
|
|
18
|
-
"hid",
|
|
19
|
-
"linux",
|
|
20
|
-
]
|
|
21
|
-
classifiers = [
|
|
22
|
-
"Development Status :: 4 - Beta",
|
|
23
|
-
"Environment :: Console",
|
|
24
|
-
"Intended Audience :: End Users/Desktop",
|
|
25
|
-
"Operating System :: POSIX :: Linux",
|
|
26
|
-
"Programming Language :: Python :: 3",
|
|
27
|
-
"Programming Language :: Python :: 3.14",
|
|
28
|
-
"Topic :: System :: Hardware :: Hardware Drivers",
|
|
29
|
-
"Topic :: System :: Hardware",
|
|
30
|
-
]
|
|
31
|
-
requires-python = ">=3.14"
|
|
32
|
-
dependencies = [
|
|
33
|
-
"dpkt>=1.9.8",
|
|
34
|
-
"hidapi>=0.15.0",
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
[project.scripts]
|
|
38
|
-
fizzctl = "fizzctl:main"
|
|
39
|
-
fizzctl-dev = "fizzctl:main_dev"
|
|
40
|
-
|
|
41
|
-
[build-system]
|
|
42
|
-
requires = ["uv_build>=0.12.13,<0.13.0"]
|
|
43
|
-
build-backend = "uv_build"
|
|
44
|
-
|
|
45
|
-
[tool.uv]
|
|
46
|
-
package = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|