AnalogSensePy 1.0.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.
- analogsensepy-1.0.0/AnalogSensePy.egg-info/PKG-INFO +7 -0
- analogsensepy-1.0.0/AnalogSensePy.egg-info/SOURCES.txt +14 -0
- analogsensepy-1.0.0/AnalogSensePy.egg-info/dependency_links.txt +1 -0
- analogsensepy-1.0.0/AnalogSensePy.egg-info/requires.txt +1 -0
- analogsensepy-1.0.0/AnalogSensePy.egg-info/top_level.txt +1 -0
- analogsensepy-1.0.0/LICENSE +22 -0
- analogsensepy-1.0.0/PKG-INFO +7 -0
- analogsensepy-1.0.0/README.md +80 -0
- analogsensepy-1.0.0/analogsense/__init__.py +4 -0
- analogsensepy-1.0.0/analogsense/analogsense.py +100 -0
- analogsensepy-1.0.0/analogsense/keymaps.py +261 -0
- analogsensepy-1.0.0/analogsense/layouts.py +104 -0
- analogsensepy-1.0.0/analogsense/providers.py +501 -0
- analogsensepy-1.0.0/analogsense/setup.py +10 -0
- analogsensepy-1.0.0/pyproject.toml +9 -0
- analogsensepy-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
AnalogSensePy.egg-info/PKG-INFO
|
|
5
|
+
AnalogSensePy.egg-info/SOURCES.txt
|
|
6
|
+
AnalogSensePy.egg-info/dependency_links.txt
|
|
7
|
+
AnalogSensePy.egg-info/requires.txt
|
|
8
|
+
AnalogSensePy.egg-info/top_level.txt
|
|
9
|
+
analogsense/__init__.py
|
|
10
|
+
analogsense/analogsense.py
|
|
11
|
+
analogsense/keymaps.py
|
|
12
|
+
analogsense/layouts.py
|
|
13
|
+
analogsense/providers.py
|
|
14
|
+
analogsense/setup.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hid
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
analogsense
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Calamity, Inc.
|
|
4
|
+
Copyright (c) 2025 Deana Brcka
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# AnalogSense Python SDK
|
|
2
|
+
Python port of [AnalogSense.js](https://github.com/AnalogSense/JavaScript-SDK/) for analog keyboard input.
|
|
3
|
+
## Supported Keyboards/Devices
|
|
4
|
+
- Everything by Wooting
|
|
5
|
+
- Everything by NuPhy
|
|
6
|
+
- Everything by DrunkDeer
|
|
7
|
+
- Razer Huntsman V2 Analog<sup>R</sup>
|
|
8
|
+
- Razer Huntsman Mini Analog<sup>R</sup>
|
|
9
|
+
- Razer Huntsman V3 Pro<sup>R</sup>
|
|
10
|
+
- Razer Huntsman V3 Pro Mini<sup>R</sup>
|
|
11
|
+
- Razer Huntsman V3 Pro Tenkeyless<sup>R</sup>
|
|
12
|
+
- Keychron Q1 HE<sup>P, F</sup>
|
|
13
|
+
- Keychron Q3 HE<sup>P, F</sup>
|
|
14
|
+
- Keychron Q5 HE<sup>P, F</sup>
|
|
15
|
+
- Keychron K2 HE<sup>P, F</sup>
|
|
16
|
+
- Lemokey P1 HE<sup>P, F</sup>
|
|
17
|
+
- Madlions MAD60HE<sup>P</sup>
|
|
18
|
+
- Madlions MAD68HE<sup>P</sup>
|
|
19
|
+
- Madlions MAD68R<sup>P</sup>
|
|
20
|
+
- Redragon K709HE<sup>P</sup>
|
|
21
|
+
|
|
22
|
+
<sup>R</sup> Razer Synapse needs to be installed and running for analogue inputs to be received from this keyboard.
|
|
23
|
+
<sup>P</sup> The official firmware only supports polling, which can lead to lag and missed inputs.
|
|
24
|
+
<sup>F</sup> [Custom firmware with full analog report functionality is available](https://analogsense.org/firmware/).
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
```bash
|
|
28
|
+
pip install hid
|
|
29
|
+
pip install
|
|
30
|
+
```
|
|
31
|
+
On Linux you may need udev rules or `sudo` for hid
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
```python
|
|
35
|
+
from analogsense import AnalogSense
|
|
36
|
+
|
|
37
|
+
as_ = AnalogSense()
|
|
38
|
+
devices = as_.get_devices()
|
|
39
|
+
dev = devices[0]
|
|
40
|
+
|
|
41
|
+
def on_keys(active_keys):
|
|
42
|
+
for k in active_keys:
|
|
43
|
+
print(as_.scancode_to_string(k["scancode"]), f"{k['value']:.2f}")
|
|
44
|
+
|
|
45
|
+
dev.start_listening(on_keys)
|
|
46
|
+
input("enter to stop...\n")
|
|
47
|
+
dev.stop_listening()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The following functions are available on `AnalogSense`:
|
|
51
|
+
- `get_devices() -> list[Device]`
|
|
52
|
+
- `open_device(vendor_id, product_id) -> Device | None`
|
|
53
|
+
- `scancode_to_string(scancode: int) -> str`
|
|
54
|
+
|
|
55
|
+
A device instance has the following members:
|
|
56
|
+
- `start_listening(handler: Callable[[list[{"scancode": int, "value": float}]], None])`
|
|
57
|
+
- `stop_listening()`
|
|
58
|
+
- `product_name: str`
|
|
59
|
+
- `forget()`
|
|
60
|
+
- `dev: DeviceHandle`
|
|
61
|
+
|
|
62
|
+
### Scancodes
|
|
63
|
+
The scancodes provided by this library are primarily HID scancodes; most keys are mapped as seen on usage page 0x07 (A = 0x04, B = 0x05, ...).
|
|
64
|
+
|
|
65
|
+
Control keys (usage page 0x0C) are mapped in the `0x3__` range, modulo 0x100:
|
|
66
|
+
- `0x3B5` = Next Track
|
|
67
|
+
- `0x3B6` = Previous Track
|
|
68
|
+
- `0x3B7` = Stop Media
|
|
69
|
+
- `0x3CD` = Play/Pause
|
|
70
|
+
- `0x394` = Open File Explorer
|
|
71
|
+
- `0x323` = Open Browser Home Page
|
|
72
|
+
|
|
73
|
+
OEM-specific keys are mapped in the `0x4__` range:
|
|
74
|
+
- `0x401` = Brightness Up
|
|
75
|
+
- `0x402` = Brightness Down
|
|
76
|
+
- `0x403` = Profile 1
|
|
77
|
+
- `0x404` = Profile 2
|
|
78
|
+
- `0x405` = Profile 3
|
|
79
|
+
- `0x408` = Profile Switch
|
|
80
|
+
- `0x409` = Function Key (Fn)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
from .providers import ALL_PROVIDERS, AsProvider
|
|
5
|
+
from .keymaps import wooting_to_name, razer_to_wooting, nuphy_to_wooting, bytech_to_wooting
|
|
6
|
+
from .layouts import drunkdeer_index_to_hid_scancode
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import hid as _hid
|
|
10
|
+
_HID_AVAILABLE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
_HID_AVAILABLE = False
|
|
13
|
+
warnings.warn("'hid' is not installed. you can install it with: pip install hid")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DeviceHandle:
|
|
17
|
+
def __init__(self, hid_dev, info: dict):
|
|
18
|
+
self._dev = hid_dev
|
|
19
|
+
self.vendor_id = info["vendor_id"]
|
|
20
|
+
self.product_id = info["product_id"]
|
|
21
|
+
self.product_string = info.get("product_string") or f"{info['vendor_id']:#06x}:{info['product_id']:#06x}"
|
|
22
|
+
|
|
23
|
+
def __getattr__(self, name):
|
|
24
|
+
return getattr(self._dev, name)
|
|
25
|
+
|
|
26
|
+
def read(self, size, timeout_ms=100):
|
|
27
|
+
return self._dev.read(size, timeout_ms)
|
|
28
|
+
|
|
29
|
+
def write(self, data):
|
|
30
|
+
return self._dev.write(data)
|
|
31
|
+
|
|
32
|
+
def close(self):
|
|
33
|
+
return self._dev.close()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AnalogSense:
|
|
37
|
+
def __init__(self, extra_providers=None):
|
|
38
|
+
self.providers = list(ALL_PROVIDERS)
|
|
39
|
+
if extra_providers:
|
|
40
|
+
self.providers.extend(extra_providers)
|
|
41
|
+
|
|
42
|
+
def _collect_candidates(self):
|
|
43
|
+
candidates = []
|
|
44
|
+
seen = set()
|
|
45
|
+
for provider_cls in self.providers:
|
|
46
|
+
for f in provider_cls.FILTERS:
|
|
47
|
+
kwargs = {}
|
|
48
|
+
if "vendor_id" in f: kwargs["vendor_id"] = f["vendor_id"]
|
|
49
|
+
if "product_id" in f: kwargs["product_id"] = f["product_id"]
|
|
50
|
+
try:
|
|
51
|
+
for info in _hid.enumerate(**kwargs):
|
|
52
|
+
dedup_key = (info["vendor_id"], info["product_id"], info.get("usage_page", 0), info.get("usage", 0))
|
|
53
|
+
if dedup_key in seen: continue
|
|
54
|
+
if "usage_page" in f and info.get("usage_page") != f["usage_page"]: continue
|
|
55
|
+
seen.add(dedup_key)
|
|
56
|
+
candidates.append((info, provider_cls))
|
|
57
|
+
except Exception as e:
|
|
58
|
+
warnings.warn(f"Error enumerating HID devices: {e}")
|
|
59
|
+
return candidates
|
|
60
|
+
|
|
61
|
+
def _open(self, info, provider_cls):
|
|
62
|
+
dev = _hid.device()
|
|
63
|
+
dev.open_path(info["path"])
|
|
64
|
+
return provider_cls(DeviceHandle(dev, info))
|
|
65
|
+
|
|
66
|
+
def get_devices(self):
|
|
67
|
+
if not _HID_AVAILABLE: return []
|
|
68
|
+
return [self._open(info, cls) for info, cls in self._collect_candidates()]
|
|
69
|
+
|
|
70
|
+
def open_device(self, vendor_id, product_id):
|
|
71
|
+
if not _HID_AVAILABLE: return None
|
|
72
|
+
for info, provider_cls in self._collect_candidates():
|
|
73
|
+
if info["vendor_id"] == vendor_id and info["product_id"] == product_id:
|
|
74
|
+
return self._open(info, provider_cls)
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def scancode_to_string(self, scancode):
|
|
78
|
+
return wooting_to_name.get(scancode, str(scancode))
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def razer_scancode_to_hid(scancode):
|
|
82
|
+
result = razer_to_wooting.get(scancode, 0)
|
|
83
|
+
if result == 0: warnings.warn(f"Failed to map Razer scancode to HID: {scancode:#x}")
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def nuphy_scancode_to_hid(scancode):
|
|
88
|
+
result = nuphy_to_wooting.get(scancode, 0)
|
|
89
|
+
if result == 0: warnings.warn(f"Failed to map NuPhy scancode to HID: {scancode:#x}")
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def bytech_scancode_to_hid(scancode):
|
|
94
|
+
result = bytech_to_wooting.get(scancode, 0)
|
|
95
|
+
if result == 0: warnings.warn(f"Failed to map Bytech scancode to HID: {scancode:#x}")
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def drunkdeer_index_to_hid(index):
|
|
100
|
+
return drunkdeer_index_to_hid_scancode(index)
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
KEYS = [
|
|
2
|
+
{"name": "Escape", "wooting": 0x29, "razer": 0x6E, "bytech": 1},
|
|
3
|
+
{"name": "F1", "wooting": 0x3A, "razer": 0x70, "bytech": 2},
|
|
4
|
+
{"name": "F2", "wooting": 0x3B, "razer": 0x71, "bytech": 3},
|
|
5
|
+
{"name": "F3", "wooting": 0x3C, "razer": 0x72, "bytech": 4},
|
|
6
|
+
{"name": "F4", "wooting": 0x3D, "razer": 0x73, "bytech": 5},
|
|
7
|
+
{"name": "F5", "wooting": 0x3E, "razer": 0x74, "bytech": 6},
|
|
8
|
+
{"name": "F6", "wooting": 0x3F, "razer": 0x75, "bytech": 7},
|
|
9
|
+
{"name": "F7", "wooting": 0x40, "razer": 0x76, "bytech": 8},
|
|
10
|
+
{"name": "F8", "wooting": 0x41, "razer": 0x77, "bytech": 9},
|
|
11
|
+
{"name": "F9", "wooting": 0x42, "razer": 0x78, "bytech": 10},
|
|
12
|
+
{"name": "F10", "wooting": 0x43, "razer": 0x79, "bytech": 11},
|
|
13
|
+
{"name": "F11", "wooting": 0x44, "razer": 0x7A, "bytech": 12},
|
|
14
|
+
{"name": "F12", "wooting": 0x45, "razer": 0x7B, "bytech": 13},
|
|
15
|
+
{"name": "`", "wooting": 0x35, "razer": 0x01, "bytech": 14},
|
|
16
|
+
{"name": "1", "wooting": 0x1E, "razer": 0x02, "bytech": 15},
|
|
17
|
+
{"name": "2", "wooting": 0x1F, "razer": 0x03, "bytech": 16},
|
|
18
|
+
{"name": "3", "wooting": 0x20, "razer": 0x04, "bytech": 17},
|
|
19
|
+
{"name": "4", "wooting": 0x21, "razer": 0x05, "bytech": 18},
|
|
20
|
+
{"name": "5", "wooting": 0x22, "razer": 0x06, "bytech": 19},
|
|
21
|
+
{"name": "6", "wooting": 0x23, "razer": 0x07, "bytech": 20},
|
|
22
|
+
{"name": "7", "wooting": 0x24, "razer": 0x08, "bytech": 21},
|
|
23
|
+
{"name": "8", "wooting": 0x25, "razer": 0x09, "bytech": 22},
|
|
24
|
+
{"name": "9", "wooting": 0x26, "razer": 0x0A, "bytech": 23},
|
|
25
|
+
{"name": "0", "wooting": 0x27, "razer": 0x0B, "bytech": 24},
|
|
26
|
+
{"name": "-", "wooting": 0x2D, "razer": 0x0C, "bytech": 25},
|
|
27
|
+
{"name": "=", "wooting": 0x2E, "razer": 0x0D, "bytech": 26},
|
|
28
|
+
{"name": "Backspace", "wooting": 0x2A, "razer": 0x0F, "bytech": 27},
|
|
29
|
+
{"name": "Tab", "wooting": 0x2B, "razer": 0x10, "bytech": 28},
|
|
30
|
+
{"name": "Q", "wooting": 0x14, "razer": 0x11, "bytech": 29},
|
|
31
|
+
{"name": "W", "wooting": 0x1A, "razer": 0x12, "bytech": 30},
|
|
32
|
+
{"name": "E", "wooting": 0x08, "razer": 0x13, "bytech": 31},
|
|
33
|
+
{"name": "R", "wooting": 0x15, "razer": 0x14, "bytech": 32},
|
|
34
|
+
{"name": "T", "wooting": 0x17, "razer": 0x15, "bytech": 33},
|
|
35
|
+
{"name": "Y", "wooting": 0x1C, "razer": 0x16, "bytech": 34},
|
|
36
|
+
{"name": "U", "wooting": 0x18, "razer": 0x17, "bytech": 35},
|
|
37
|
+
{"name": "I", "wooting": 0x0C, "razer": 0x18, "bytech": 36},
|
|
38
|
+
{"name": "O", "wooting": 0x12, "razer": 0x19, "bytech": 37},
|
|
39
|
+
{"name": "P", "wooting": 0x13, "razer": 0x1A, "bytech": 38},
|
|
40
|
+
{"name": "[", "wooting": 0x2F, "razer": 0x1B, "bytech": 39},
|
|
41
|
+
{"name": "]", "wooting": 0x30, "razer": 0x1C, "bytech": 40},
|
|
42
|
+
{"name": "Enter", "wooting": 0x28, "razer": 0x2B, "bytech": 54},
|
|
43
|
+
{"name": "Caps Lock", "wooting": 0x39, "razer": 0x1E, "bytech": 42},
|
|
44
|
+
{"name": "A", "wooting": 0x04, "razer": 0x1F, "bytech": 43},
|
|
45
|
+
{"name": "S", "wooting": 0x16, "razer": 0x20, "bytech": 44},
|
|
46
|
+
{"name": "D", "wooting": 0x07, "razer": 0x21, "bytech": 45},
|
|
47
|
+
{"name": "F", "wooting": 0x09, "razer": 0x22, "bytech": 46},
|
|
48
|
+
{"name": "G", "wooting": 0x0A, "razer": 0x23, "bytech": 47},
|
|
49
|
+
{"name": "H", "wooting": 0x0B, "razer": 0x24, "bytech": 48},
|
|
50
|
+
{"name": "J", "wooting": 0x0D, "razer": 0x25, "bytech": 49},
|
|
51
|
+
{"name": "K", "wooting": 0x0E, "razer": 0x26, "bytech": 50},
|
|
52
|
+
{"name": "L", "wooting": 0x0F, "razer": 0x27, "bytech": 51},
|
|
53
|
+
{"name": ";", "wooting": 0x33, "razer": 0x28, "bytech": 52},
|
|
54
|
+
{"name": "'", "wooting": 0x34, "razer": 0x29, "bytech": 53},
|
|
55
|
+
{"name": "Backslash", "wooting": 0x31, "razer": 0x2A, "bytech": 41},
|
|
56
|
+
{"name": "Left Shift", "wooting": 0xE1, "razer": 0x2C, "nuphy": 0x200, "bytech": 55},
|
|
57
|
+
{"name": "Intl Backslash", "wooting": 0x64, "razer": 0x2D},
|
|
58
|
+
{"name": "Z", "wooting": 0x1D, "razer": 0x2E, "bytech": 56},
|
|
59
|
+
{"name": "X", "wooting": 0x1B, "razer": 0x2F, "bytech": 57},
|
|
60
|
+
{"name": "C", "wooting": 0x06, "razer": 0x30, "bytech": 58},
|
|
61
|
+
{"name": "V", "wooting": 0x19, "razer": 0x31, "bytech": 59},
|
|
62
|
+
{"name": "B", "wooting": 0x05, "razer": 0x32, "bytech": 60},
|
|
63
|
+
{"name": "N", "wooting": 0x11, "razer": 0x33, "bytech": 61},
|
|
64
|
+
{"name": "M", "wooting": 0x10, "razer": 0x34, "bytech": 62},
|
|
65
|
+
{"name": ",", "wooting": 0x36, "razer": 0x35, "bytech": 63},
|
|
66
|
+
{"name": ".", "wooting": 0x37, "razer": 0x36, "bytech": 64},
|
|
67
|
+
{"name": "/", "wooting": 0x38, "razer": 0x37, "bytech": 65},
|
|
68
|
+
{"name": "Right Shift", "wooting": 0xE5, "razer": 0x39, "nuphy": 0x2000, "bytech": 66},
|
|
69
|
+
{"name": "Left Ctrl", "wooting": 0xE0, "razer": 0x3A, "nuphy": 0x100, "bytech": 67},
|
|
70
|
+
{"name": "Left Meta", "wooting": 0xE3, "razer": 0x7F, "nuphy": 0x800, "bytech": 68},
|
|
71
|
+
{"name": "Left Alt", "wooting": 0xE2, "razer": 0x3C, "nuphy": 0x400, "bytech": 69},
|
|
72
|
+
{"name": "Space", "wooting": 0x2C, "razer": 0x3D, "bytech": 70},
|
|
73
|
+
{"name": "Right Alt", "wooting": 0xE6, "razer": 0x3E, "nuphy": 0x4000, "bytech": 71},
|
|
74
|
+
{"name": "Right Meta", "wooting": 0xE7, "nuphy": 0x8000},
|
|
75
|
+
{"name": "Fn", "wooting": 0x409, "razer": 0x3B, "nuphy": 0xFF05, "bytech": 72},
|
|
76
|
+
{"name": "Context Menu", "wooting": 0x65, "razer": 0x81},
|
|
77
|
+
{"name": "Right Ctrl", "wooting": 0xE4, "razer": 0x40, "nuphy": 0x1000, "bytech": 73},
|
|
78
|
+
{"name": "Print Screen", "wooting": 0x46, "razer": 0x7C},
|
|
79
|
+
{"name": "Pause", "wooting": 0x48, "razer": 0x7D},
|
|
80
|
+
{"name": "Scroll Lock", "wooting": 0x47, "razer": 0x7E},
|
|
81
|
+
{"name": "Insert", "wooting": 0x49, "razer": 0x4B},
|
|
82
|
+
{"name": "Home", "wooting": 0x4A, "razer": 0x50, "bytech": 100},
|
|
83
|
+
{"name": "Page Up", "wooting": 0x4B, "razer": 0x55, "bytech": 102},
|
|
84
|
+
{"name": "Delete", "wooting": 0x4C, "razer": 0x4C, "bytech": 99},
|
|
85
|
+
{"name": "End", "wooting": 0x4D, "razer": 0x51},
|
|
86
|
+
{"name": "Page Down", "wooting": 0x4E, "razer": 0x56, "bytech": 103},
|
|
87
|
+
{"name": "Up Arrow", "wooting": 0x52, "razer": 0x53, "bytech": 74},
|
|
88
|
+
{"name": "Left Arrow", "wooting": 0x50, "razer": 0x4F, "bytech": 76},
|
|
89
|
+
{"name": "Down Arrow", "wooting": 0x51, "razer": 0x54, "bytech": 75},
|
|
90
|
+
{"name": "Right Arrow", "wooting": 0x4F, "razer": 0x59, "bytech": 77},
|
|
91
|
+
{"name": "Num Lock", "wooting": 0x53, "razer": 0x5A},
|
|
92
|
+
{"name": "Numpad /", "wooting": 0x54, "razer": 0x5F},
|
|
93
|
+
{"name": "Numpad *", "wooting": 0x55, "razer": 0x64},
|
|
94
|
+
{"name": "Numpad -", "wooting": 0x56, "razer": 0x69},
|
|
95
|
+
{"name": "Numpad 7", "wooting": 0x5F, "razer": 0x5B},
|
|
96
|
+
{"name": "Numpad 8", "wooting": 0x60, "razer": 0x60},
|
|
97
|
+
{"name": "Numpad 9", "wooting": 0x61, "razer": 0x65},
|
|
98
|
+
{"name": "Numpad +", "wooting": 0x57, "razer": 0x6A},
|
|
99
|
+
{"name": "Numpad 4", "wooting": 0x5C, "razer": 0x5C},
|
|
100
|
+
{"name": "Numpad 5", "wooting": 0x5D, "razer": 0x61},
|
|
101
|
+
{"name": "Numpad 6", "wooting": 0x5E, "razer": 0x66},
|
|
102
|
+
{"name": "Numpad 1", "wooting": 0x59, "razer": 0x5D},
|
|
103
|
+
{"name": "Numpad 2", "wooting": 0x5A, "razer": 0x62},
|
|
104
|
+
{"name": "Numpad 3", "wooting": 0x5B, "razer": 0x67},
|
|
105
|
+
{"name": "Numpad Enter", "wooting": 0x58, "razer": 0x6C},
|
|
106
|
+
{"name": "Numpad 0", "wooting": 0x62, "razer": 0x63},
|
|
107
|
+
{"name": "Numpad .", "wooting": 0x63, "razer": 0x68},
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
wooting_to_name = {k["wooting"]: k["name"] for k in KEYS}
|
|
111
|
+
razer_to_wooting = {k["razer"]: k["wooting"] for k in KEYS if "razer" in k}
|
|
112
|
+
nuphy_to_wooting = {k.get("nuphy", k["wooting"]): k["wooting"] for k in KEYS}
|
|
113
|
+
bytech_to_wooting = {k["bytech"]: k["wooting"] for k in KEYS if "bytech" in k}
|
|
114
|
+
|
|
115
|
+
KEY_NONE = 0x00
|
|
116
|
+
KEY_A = 0x04
|
|
117
|
+
KEY_B = 0x05
|
|
118
|
+
KEY_C = 0x06
|
|
119
|
+
KEY_D = 0x07
|
|
120
|
+
KEY_E = 0x08
|
|
121
|
+
KEY_F = 0x09
|
|
122
|
+
KEY_G = 0x0A
|
|
123
|
+
KEY_H = 0x0B
|
|
124
|
+
KEY_I = 0x0C
|
|
125
|
+
KEY_J = 0x0D
|
|
126
|
+
KEY_K = 0x0E
|
|
127
|
+
KEY_L = 0x0F
|
|
128
|
+
KEY_M = 0x10
|
|
129
|
+
KEY_N = 0x11
|
|
130
|
+
KEY_O = 0x12
|
|
131
|
+
KEY_P = 0x13
|
|
132
|
+
KEY_Q = 0x14
|
|
133
|
+
KEY_R = 0x15
|
|
134
|
+
KEY_S = 0x16
|
|
135
|
+
KEY_T = 0x17
|
|
136
|
+
KEY_U = 0x18
|
|
137
|
+
KEY_V = 0x19
|
|
138
|
+
KEY_W = 0x1A
|
|
139
|
+
KEY_X = 0x1B
|
|
140
|
+
KEY_Y = 0x1C
|
|
141
|
+
KEY_Z = 0x1D
|
|
142
|
+
KEY_1 = 0x1E
|
|
143
|
+
KEY_2 = 0x1F
|
|
144
|
+
KEY_3 = 0x20
|
|
145
|
+
KEY_4 = 0x21
|
|
146
|
+
KEY_5 = 0x22
|
|
147
|
+
KEY_6 = 0x23
|
|
148
|
+
KEY_7 = 0x24
|
|
149
|
+
KEY_8 = 0x25
|
|
150
|
+
KEY_9 = 0x26
|
|
151
|
+
KEY_0 = 0x27
|
|
152
|
+
KEY_ENTER = 0x28
|
|
153
|
+
KEY_ESCAPE = 0x29
|
|
154
|
+
KEY_BACKSPACE = 0x2A
|
|
155
|
+
KEY_TAB = 0x2B
|
|
156
|
+
KEY_SPACE = 0x2C
|
|
157
|
+
KEY_MINUS = 0x2D
|
|
158
|
+
KEY_EQUALS = 0x2E
|
|
159
|
+
KEY_BRACKET_LEFT = 0x2F
|
|
160
|
+
KEY_BRACKET_RIGHT = 0x30
|
|
161
|
+
KEY_BACKSLASH = 0x31
|
|
162
|
+
KEY_INTL_HASH = 0x32
|
|
163
|
+
KEY_SEMICOLON = 0x33
|
|
164
|
+
KEY_QUOTE = 0x34
|
|
165
|
+
KEY_BACKQUOTE = 0x35
|
|
166
|
+
KEY_COMMA = 0x36
|
|
167
|
+
KEY_PERIOD = 0x37
|
|
168
|
+
KEY_SLASH = 0x38
|
|
169
|
+
KEY_CAPS_LOCK = 0x39
|
|
170
|
+
KEY_F1 = 0x3A
|
|
171
|
+
KEY_F2 = 0x3B
|
|
172
|
+
KEY_F3 = 0x3C
|
|
173
|
+
KEY_F4 = 0x3D
|
|
174
|
+
KEY_F5 = 0x3E
|
|
175
|
+
KEY_F6 = 0x3F
|
|
176
|
+
KEY_F7 = 0x40
|
|
177
|
+
KEY_F8 = 0x41
|
|
178
|
+
KEY_F9 = 0x42
|
|
179
|
+
KEY_F10 = 0x43
|
|
180
|
+
KEY_F11 = 0x44
|
|
181
|
+
KEY_F12 = 0x45
|
|
182
|
+
KEY_PRINT_SCREEN = 0x46
|
|
183
|
+
KEY_SCROLL_LOCK = 0x47
|
|
184
|
+
KEY_PAUSE = 0x48
|
|
185
|
+
KEY_INSERT = 0x49
|
|
186
|
+
KEY_HOME = 0x4A
|
|
187
|
+
KEY_PAGE_UP = 0x4B
|
|
188
|
+
KEY_DEL = 0x4C
|
|
189
|
+
KEY_END = 0x4D
|
|
190
|
+
KEY_PAGE_DOWN = 0x4E
|
|
191
|
+
KEY_ARROW_RIGHT = 0x4F
|
|
192
|
+
KEY_ARROW_LEFT = 0x50
|
|
193
|
+
KEY_ARROW_DOWN = 0x51
|
|
194
|
+
KEY_ARROW_UP = 0x52
|
|
195
|
+
KEY_NUM_LOCK = 0x53
|
|
196
|
+
KEY_NUMPAD_DIVIDE = 0x54
|
|
197
|
+
KEY_NUMPAD_MULTIPLY = 0x55
|
|
198
|
+
KEY_NUMPAD_SUBTRACT = 0x56
|
|
199
|
+
KEY_NUMPAD_ADD = 0x57
|
|
200
|
+
KEY_NUMPAD_ENTER = 0x58
|
|
201
|
+
KEY_NUMPAD1 = 0x59
|
|
202
|
+
KEY_NUMPAD2 = 0x5A
|
|
203
|
+
KEY_NUMPAD3 = 0x5B
|
|
204
|
+
KEY_NUMPAD4 = 0x5C
|
|
205
|
+
KEY_NUMPAD5 = 0x5D
|
|
206
|
+
KEY_NUMPAD6 = 0x5E
|
|
207
|
+
KEY_NUMPAD7 = 0x5F
|
|
208
|
+
KEY_NUMPAD8 = 0x60
|
|
209
|
+
KEY_NUMPAD9 = 0x61
|
|
210
|
+
KEY_NUMPAD0 = 0x62
|
|
211
|
+
KEY_NUMPAD_DECIMAL = 0x63
|
|
212
|
+
KEY_INTL_BACKSLASH = 0x64
|
|
213
|
+
KEY_CTX = 0x65
|
|
214
|
+
KEY_POWER = 0x66
|
|
215
|
+
KEY_NUMPAD_EQUAL = 0x67
|
|
216
|
+
KEY_F13 = 0x68
|
|
217
|
+
KEY_F14 = 0x69
|
|
218
|
+
KEY_F15 = 0x6A
|
|
219
|
+
KEY_F16 = 0x6B
|
|
220
|
+
KEY_F17 = 0x6C
|
|
221
|
+
KEY_F18 = 0x6D
|
|
222
|
+
KEY_F19 = 0x6E
|
|
223
|
+
KEY_F20 = 0x6F
|
|
224
|
+
KEY_F21 = 0x70
|
|
225
|
+
KEY_F22 = 0x71
|
|
226
|
+
KEY_F23 = 0x72
|
|
227
|
+
KEY_F24 = 0x73
|
|
228
|
+
KEY_OPEN = 0x74
|
|
229
|
+
KEY_HELP = 0x75
|
|
230
|
+
KEY_SELECT = 0x77
|
|
231
|
+
KEY_AGAIN = 0x79
|
|
232
|
+
KEY_UNDO = 0x7A
|
|
233
|
+
KEY_CUT = 0x7B
|
|
234
|
+
KEY_COPY = 0x7C
|
|
235
|
+
KEY_PASTE = 0x7D
|
|
236
|
+
KEY_FIND = 0x7E
|
|
237
|
+
KEY_VOLUME_MUTE = 0x7F
|
|
238
|
+
KEY_VOLUME_UP = 0x80
|
|
239
|
+
KEY_VOLUME_DOWN = 0x81
|
|
240
|
+
KEY_NUMPAD_COMMA = 0x85
|
|
241
|
+
KEY_INTL_RO = 0x87
|
|
242
|
+
KEY_KANA_MODE = 0x88
|
|
243
|
+
KEY_INTL_YEN = 0x89
|
|
244
|
+
KEY_CONVERT = 0x8A
|
|
245
|
+
KEY_NON_CONVERT = 0x8B
|
|
246
|
+
KEY_LANG1 = 0x90
|
|
247
|
+
KEY_LANG2 = 0x91
|
|
248
|
+
KEY_LANG3 = 0x92
|
|
249
|
+
KEY_LANG4 = 0x93
|
|
250
|
+
KEY_LCTRL = 0xE0
|
|
251
|
+
KEY_LSHIFT = 0xE1
|
|
252
|
+
KEY_LALT = 0xE2
|
|
253
|
+
KEY_LMETA = 0xE3
|
|
254
|
+
KEY_RCTRL = 0xE4
|
|
255
|
+
KEY_RSHIFT = 0xE5
|
|
256
|
+
KEY_RALT = 0xE6
|
|
257
|
+
KEY_RMETA = 0xE7
|
|
258
|
+
KEY_OEM_1 = 0x403
|
|
259
|
+
KEY_OEM_2 = 0x404
|
|
260
|
+
KEY_OEM_3 = 0x405
|
|
261
|
+
KEY_FN = 0x409
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from .keymaps import *
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def layout_rows(layout): return layout[0]
|
|
5
|
+
def layout_cols(layout): return layout[1]
|
|
6
|
+
def layout_size(layout): return layout[0] * layout[1]
|
|
7
|
+
def layout_key(layout, i): return layout[2 + i]
|
|
8
|
+
def layout_index_to_row(layout, i): return i // layout_cols(layout)
|
|
9
|
+
def layout_index_to_col(layout, i): return i % layout_cols(layout)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
LAYOUT_KEYCHRON_Q1_HE = (6, 15,
|
|
13
|
+
KEY_ESCAPE, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, KEY_DEL, KEY_NONE,
|
|
14
|
+
KEY_BACKQUOTE, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0, KEY_MINUS, KEY_EQUALS, KEY_BACKSPACE, KEY_PAGE_UP,
|
|
15
|
+
KEY_TAB, KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P, KEY_BRACKET_LEFT, KEY_BRACKET_RIGHT, KEY_BACKSLASH, KEY_PAGE_DOWN,
|
|
16
|
+
KEY_CAPS_LOCK, KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_SEMICOLON, KEY_QUOTE, KEY_ENTER, KEY_HOME, KEY_NONE,
|
|
17
|
+
KEY_LSHIFT, KEY_NONE, KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_COMMA, KEY_PERIOD, KEY_NONE, KEY_SLASH, KEY_RSHIFT, KEY_ARROW_UP,
|
|
18
|
+
KEY_LCTRL, KEY_LMETA, KEY_LALT, KEY_NONE, KEY_NONE, KEY_NONE, KEY_SPACE, KEY_NONE, KEY_NONE, KEY_RMETA, KEY_FN, KEY_RCTRL, KEY_ARROW_LEFT, KEY_ARROW_DOWN, KEY_ARROW_RIGHT,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
LAYOUT_KEYCHRON_Q3_HE = (6, 16,
|
|
22
|
+
KEY_ESCAPE, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, KEY_PRINT_SCREEN, KEY_OEM_1, KEY_OEM_2,
|
|
23
|
+
KEY_BACKQUOTE, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0, KEY_MINUS, KEY_EQUALS, KEY_BACKSPACE, KEY_INSERT, KEY_HOME,
|
|
24
|
+
KEY_TAB, KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P, KEY_BRACKET_LEFT, KEY_BRACKET_RIGHT, KEY_BACKSLASH, KEY_DEL, KEY_END,
|
|
25
|
+
KEY_CAPS_LOCK, KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_SEMICOLON, KEY_QUOTE, KEY_NONE, KEY_ENTER, KEY_PAGE_UP, KEY_PAGE_DOWN,
|
|
26
|
+
KEY_LSHIFT, KEY_NONE, KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_COMMA, KEY_PERIOD, KEY_NONE, KEY_SLASH, KEY_RSHIFT, KEY_NONE, KEY_ARROW_UP,
|
|
27
|
+
KEY_LCTRL, KEY_LMETA, KEY_LALT, KEY_NONE, KEY_NONE, KEY_NONE, KEY_SPACE, KEY_NONE, KEY_NONE, KEY_RALT, KEY_RMETA, KEY_FN, KEY_RCTRL, KEY_ARROW_LEFT, KEY_ARROW_DOWN, KEY_ARROW_RIGHT,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
LAYOUT_KEYCHRON_Q5_HE = (6, 19,
|
|
31
|
+
KEY_ESCAPE, KEY_NONE, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, KEY_DEL, KEY_OEM_1, KEY_OEM_2, KEY_OEM_3, KEY_NONE,
|
|
32
|
+
KEY_BACKQUOTE, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0, KEY_MINUS, KEY_EQUALS, KEY_BACKSPACE, KEY_PAGE_UP, KEY_NUM_LOCK, KEY_NUMPAD_DIVIDE, KEY_NUMPAD_MULTIPLY, KEY_NUMPAD_SUBTRACT,
|
|
33
|
+
KEY_TAB, KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P, KEY_BRACKET_LEFT, KEY_BRACKET_RIGHT, KEY_BACKSLASH, KEY_PAGE_DOWN, KEY_NUMPAD7, KEY_NUMPAD8, KEY_NUMPAD9, KEY_NUMPAD_ADD,
|
|
34
|
+
KEY_CAPS_LOCK, KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_SEMICOLON, KEY_QUOTE, KEY_ENTER, KEY_HOME, KEY_NONE, KEY_NUMPAD4, KEY_NUMPAD5, KEY_NUMPAD6, KEY_NONE,
|
|
35
|
+
KEY_LSHIFT, KEY_NONE, KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_COMMA, KEY_PERIOD, KEY_NONE, KEY_SLASH, KEY_RSHIFT, KEY_ARROW_UP, KEY_NUMPAD1, KEY_NUMPAD2, KEY_NUMPAD3, KEY_NUMPAD_ENTER,
|
|
36
|
+
KEY_LCTRL, KEY_LMETA, KEY_LALT, KEY_NONE, KEY_NONE, KEY_NONE, KEY_SPACE, KEY_NONE, KEY_NONE, KEY_RMETA, KEY_FN, KEY_RCTRL, KEY_ARROW_LEFT, KEY_ARROW_DOWN, KEY_ARROW_RIGHT, KEY_NONE, KEY_NUMPAD0, KEY_NUMPAD_DECIMAL, KEY_NONE,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
LAYOUT_KEYCHRON_K2_HE = (6, 16,
|
|
40
|
+
KEY_ESCAPE, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, KEY_PRINT_SCREEN, KEY_DEL, KEY_OEM_2,
|
|
41
|
+
KEY_BACKQUOTE, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0, KEY_MINUS, KEY_EQUALS, KEY_BACKSPACE, KEY_PAGE_UP, KEY_NONE,
|
|
42
|
+
KEY_TAB, KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P, KEY_BRACKET_LEFT, KEY_BRACKET_RIGHT, KEY_BACKSLASH, KEY_PAGE_DOWN, KEY_NONE,
|
|
43
|
+
KEY_CAPS_LOCK, KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_SEMICOLON, KEY_QUOTE, KEY_ENTER, KEY_HOME, KEY_NONE, KEY_NONE,
|
|
44
|
+
KEY_LSHIFT, KEY_NONE, KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_COMMA, KEY_PERIOD, KEY_SLASH, KEY_RSHIFT, KEY_ARROW_UP, KEY_END, KEY_NONE,
|
|
45
|
+
KEY_LCTRL, KEY_LMETA, KEY_LALT, KEY_NONE, KEY_NONE, KEY_NONE, KEY_SPACE, KEY_NONE, KEY_NONE, KEY_RALT, KEY_FN, KEY_RCTRL, KEY_ARROW_LEFT, KEY_ARROW_DOWN, KEY_ARROW_RIGHT, KEY_NONE,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
LAYOUT_LEMOKEY_P1_HE = (6, 15,
|
|
49
|
+
KEY_ESCAPE, KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10, KEY_F11, KEY_F12, KEY_DEL, KEY_NONE,
|
|
50
|
+
KEY_BACKQUOTE, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0, KEY_MINUS, KEY_EQUALS, KEY_BACKSPACE, KEY_HOME,
|
|
51
|
+
KEY_TAB, KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P, KEY_BRACKET_LEFT, KEY_BRACKET_RIGHT, KEY_BACKSLASH, KEY_PAGE_UP,
|
|
52
|
+
KEY_CAPS_LOCK, KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_SEMICOLON, KEY_QUOTE, KEY_ENTER, KEY_PAGE_DOWN, KEY_NONE,
|
|
53
|
+
KEY_LSHIFT, KEY_NONE, KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_COMMA, KEY_PERIOD, KEY_NONE, KEY_SLASH, KEY_RSHIFT, KEY_ARROW_UP,
|
|
54
|
+
KEY_LCTRL, KEY_LMETA, KEY_LALT, KEY_NONE, KEY_NONE, KEY_NONE, KEY_SPACE, KEY_NONE, KEY_NONE, KEY_RMETA, KEY_FN, KEY_RCTRL, KEY_ARROW_LEFT, KEY_ARROW_DOWN, KEY_ARROW_RIGHT,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
LAYOUT_MADLIONS_MAD60HE = [
|
|
58
|
+
KEY_ESCAPE, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0, KEY_MINUS, KEY_EQUALS, KEY_BACKSPACE,
|
|
59
|
+
KEY_TAB, KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P, KEY_BRACKET_LEFT, KEY_BRACKET_RIGHT, KEY_BACKSLASH,
|
|
60
|
+
KEY_CAPS_LOCK, KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_SEMICOLON, KEY_QUOTE, KEY_NONE, KEY_ENTER,
|
|
61
|
+
KEY_LSHIFT, KEY_NONE, KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_COMMA, KEY_PERIOD, KEY_SLASH, KEY_NONE, KEY_RSHIFT,
|
|
62
|
+
KEY_LCTRL, KEY_LMETA, KEY_LALT, KEY_NONE, KEY_NONE, KEY_NONE, KEY_SPACE, KEY_NONE, KEY_NONE, KEY_RMETA, KEY_RALT, KEY_CTX, KEY_RCTRL, KEY_FN,
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
LAYOUT_MADLIONS_MAD68HE = [
|
|
66
|
+
KEY_ESCAPE, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0, KEY_MINUS, KEY_EQUALS, KEY_BACKSPACE, KEY_INSERT,
|
|
67
|
+
KEY_TAB, KEY_Q, KEY_W, KEY_E, KEY_R, KEY_T, KEY_Y, KEY_U, KEY_I, KEY_O, KEY_P, KEY_BRACKET_LEFT, KEY_BRACKET_RIGHT, KEY_BACKSLASH, KEY_DEL,
|
|
68
|
+
KEY_CAPS_LOCK, KEY_A, KEY_S, KEY_D, KEY_F, KEY_G, KEY_H, KEY_J, KEY_K, KEY_L, KEY_SEMICOLON, KEY_QUOTE, KEY_NONE, KEY_ENTER, KEY_PAGE_UP,
|
|
69
|
+
KEY_LSHIFT, KEY_NONE, KEY_Z, KEY_X, KEY_C, KEY_V, KEY_B, KEY_N, KEY_M, KEY_COMMA, KEY_PERIOD, KEY_SLASH, KEY_RSHIFT, KEY_ARROW_UP, KEY_PAGE_DOWN,
|
|
70
|
+
KEY_LCTRL, KEY_LMETA, KEY_LALT, KEY_NONE, KEY_NONE, KEY_NONE, KEY_SPACE, KEY_NONE, KEY_NONE, KEY_RALT, KEY_FN, KEY_RCTRL, KEY_ARROW_LEFT, KEY_ARROW_DOWN, KEY_ARROW_RIGHT,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
_DRUNKDEER_MAP = {
|
|
74
|
+
(0*21)+0: 0x29, (0*21)+2: 0x3A, (0*21)+3: 0x3B, (0*21)+4: 0x3C,
|
|
75
|
+
(0*21)+5: 0x3D, (0*21)+6: 0x3E, (0*21)+7: 0x3F, (0*21)+8: 0x40,
|
|
76
|
+
(0*21)+9: 0x41, (0*21)+10: 0x42, (0*21)+11: 0x43, (0*21)+12: 0x44,
|
|
77
|
+
(0*21)+13: 0x45, (0*21)+14: 0x4C,
|
|
78
|
+
(1*21)+0: 0x35, (1*21)+1: 0x1E, (1*21)+2: 0x1F, (1*21)+3: 0x20,
|
|
79
|
+
(1*21)+4: 0x21, (1*21)+5: 0x22, (1*21)+6: 0x23, (1*21)+7: 0x24,
|
|
80
|
+
(1*21)+8: 0x25, (1*21)+9: 0x26, (1*21)+10: 0x27, (1*21)+11: 0x2D,
|
|
81
|
+
(1*21)+12: 0x2E, (1*21)+13: 0x2A, (1*21)+15: 0x4A,
|
|
82
|
+
(2*21)+0: 0x2B, (2*21)+1: 0x14, (2*21)+2: 0x1A, (2*21)+3: 0x08,
|
|
83
|
+
(2*21)+4: 0x15, (2*21)+5: 0x17, (2*21)+6: 0x1C, (2*21)+7: 0x18,
|
|
84
|
+
(2*21)+8: 0x0C, (2*21)+9: 0x12, (2*21)+10: 0x13, (2*21)+11: 0x2F,
|
|
85
|
+
(2*21)+12: 0x30, (2*21)+13: 0x31, (2*21)+15: 0x4B,
|
|
86
|
+
(3*21)+0: 0x39, (3*21)+1: 0x04, (3*21)+2: 0x16, (3*21)+3: 0x07,
|
|
87
|
+
(3*21)+4: 0x09, (3*21)+5: 0x0A, (3*21)+6: 0x0B, (3*21)+7: 0x0D,
|
|
88
|
+
(3*21)+8: 0x0E, (3*21)+9: 0x0F, (3*21)+10: 0x33, (3*21)+11: 0x34,
|
|
89
|
+
(3*21)+13: 0x28, (3*21)+15: 0x4E,
|
|
90
|
+
(4*21)+0: 0xE1, (4*21)+2: 0x1D, (4*21)+3: 0x1B, (4*21)+4: 0x06,
|
|
91
|
+
(4*21)+5: 0x19, (4*21)+6: 0x05, (4*21)+7: 0x11, (4*21)+8: 0x10,
|
|
92
|
+
(4*21)+9: 0x36, (4*21)+10: 0x37, (4*21)+11: 0x38, (4*21)+13: 0xE5,
|
|
93
|
+
(4*21)+14: 0x52, (4*21)+15: 0x4D,
|
|
94
|
+
(5*21)+0: 0xE0, (5*21)+1: 0xE3, (5*21)+2: 0xE2, (5*21)+6: 0x2C,
|
|
95
|
+
(5*21)+10: 0xE6, (5*21)+11: 0x409,(5*21)+12: 0x65,
|
|
96
|
+
(5*21)+14: 0x50, (5*21)+15: 0x51, (5*21)+16: 0x4F,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def drunkdeer_index_to_hid_scancode(i):
|
|
100
|
+
import warnings
|
|
101
|
+
sc = _DRUNKDEER_MAP.get(i, 0)
|
|
102
|
+
if sc == 0:
|
|
103
|
+
warnings.warn(f"Failed to map DrunkDeer key to HID scancode: {i}")
|
|
104
|
+
return sc
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from .keymaps import razer_to_wooting, nuphy_to_wooting, bytech_to_wooting, KEY_NONE
|
|
8
|
+
from .layouts import (
|
|
9
|
+
LAYOUT_KEYCHRON_Q1_HE, LAYOUT_KEYCHRON_Q3_HE, LAYOUT_KEYCHRON_Q5_HE,
|
|
10
|
+
LAYOUT_KEYCHRON_K2_HE, LAYOUT_LEMOKEY_P1_HE,
|
|
11
|
+
LAYOUT_MADLIONS_MAD60HE, LAYOUT_MADLIONS_MAD68HE,
|
|
12
|
+
layout_key, layout_size, layout_index_to_row, layout_index_to_col,
|
|
13
|
+
drunkdeer_index_to_hid_scancode,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
ActiveKey = dict
|
|
17
|
+
Handler = Callable[[list[ActiveKey]], None]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsProvider:
|
|
21
|
+
FILTERS = []
|
|
22
|
+
|
|
23
|
+
def __init__(self, dev):
|
|
24
|
+
self.dev = dev
|
|
25
|
+
self._buffer: dict[int, float] = {}
|
|
26
|
+
self._thread: threading.Thread | None = None
|
|
27
|
+
self._running = False
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def product_name(self):
|
|
31
|
+
return getattr(self.dev, "product_string", "Unknown")
|
|
32
|
+
|
|
33
|
+
def forget(self):
|
|
34
|
+
self.stop_listening()
|
|
35
|
+
try:
|
|
36
|
+
self.dev.close()
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
def _buffer_to_active_keys(self):
|
|
41
|
+
return [{"scancode": sc, "value": v} for sc, v in self._buffer.items()]
|
|
42
|
+
|
|
43
|
+
def start_listening(self, handler: Handler):
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
def stop_listening(self):
|
|
47
|
+
self._running = False
|
|
48
|
+
if self._thread:
|
|
49
|
+
self._thread.join(timeout=2)
|
|
50
|
+
self._thread = None
|
|
51
|
+
|
|
52
|
+
def _read_loop(self, handler: Handler):
|
|
53
|
+
while self._running:
|
|
54
|
+
try:
|
|
55
|
+
data = self.dev.read(64, timeout_ms=100)
|
|
56
|
+
if data:
|
|
57
|
+
self._handle_report(bytes(data), handler)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
warnings.warn(f"HID read error: {e}")
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
def _handle_report(self, data: bytes, handler: Handler):
|
|
63
|
+
raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AsProviderWootingV1(AsProvider):
|
|
67
|
+
FILTERS = [
|
|
68
|
+
{"usage_page": 0xFF54, "vendor_id": 0x31E3},
|
|
69
|
+
{"usage_page": 0xFF54, "vendor_id": 0x03EB, "product_id": 0xFF01},
|
|
70
|
+
{"usage_page": 0xFF54, "vendor_id": 0x03EB, "product_id": 0xFF02},
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
def start_listening(self, handler: Handler):
|
|
74
|
+
self._running = True
|
|
75
|
+
self._prev_scancodes: set[int] = set()
|
|
76
|
+
self._thread = threading.Thread(target=self._read_loop, args=(handler,), daemon=True)
|
|
77
|
+
self._thread.start()
|
|
78
|
+
|
|
79
|
+
def _handle_report(self, data: bytes, handler: Handler):
|
|
80
|
+
active_keys = []
|
|
81
|
+
current_scancodes = set()
|
|
82
|
+
i = 0
|
|
83
|
+
while i < len(data):
|
|
84
|
+
if i + 2 > len(data): break
|
|
85
|
+
scancode = (data[i] << 8) | data[i + 1]
|
|
86
|
+
i += 2
|
|
87
|
+
if scancode == 0: break
|
|
88
|
+
if i >= len(data): break
|
|
89
|
+
value = data[i]
|
|
90
|
+
i += 1
|
|
91
|
+
active_keys.append({"scancode": scancode, "value": value / 255})
|
|
92
|
+
current_scancodes.add(scancode)
|
|
93
|
+
for released in self._prev_scancodes - current_scancodes:
|
|
94
|
+
active_keys.append({"scancode": released, "value": 0.0})
|
|
95
|
+
self._prev_scancodes = current_scancodes
|
|
96
|
+
handler(active_keys)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class AsProviderWootingV2(AsProvider):
|
|
100
|
+
FILTERS = [
|
|
101
|
+
{"usage_page": 0xFF53, "vendor_id": 0x31E3},
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
def start_listening(self, handler: Handler):
|
|
105
|
+
self._running = True
|
|
106
|
+
self._prev_scancodes: set[int] = set()
|
|
107
|
+
self._thread = threading.Thread(target=self._read_loop, args=(handler,), daemon=True)
|
|
108
|
+
self._thread.start()
|
|
109
|
+
|
|
110
|
+
def _handle_report(self, data: bytes, handler: Handler):
|
|
111
|
+
active_keys = []
|
|
112
|
+
current_scancodes = set()
|
|
113
|
+
i = 0
|
|
114
|
+
while i + 4 <= len(data):
|
|
115
|
+
keycode = data[i + 1]
|
|
116
|
+
packed = data[i + 2]
|
|
117
|
+
value_hi = data[i + 3]
|
|
118
|
+
key_ns = (packed >> 2) & 0xF
|
|
119
|
+
value_lo = (packed >> 6) & 0x3
|
|
120
|
+
scancode = (key_ns << 8) | keycode
|
|
121
|
+
value = (value_hi << 2) | value_lo
|
|
122
|
+
i += 4
|
|
123
|
+
if scancode == 0: break
|
|
124
|
+
if value == 0: continue
|
|
125
|
+
active_keys.append({"scancode": scancode, "value": value / 1023})
|
|
126
|
+
current_scancodes.add(scancode)
|
|
127
|
+
for released in self._prev_scancodes - current_scancodes:
|
|
128
|
+
active_keys.append({"scancode": released, "value": 0.0})
|
|
129
|
+
self._prev_scancodes = current_scancodes
|
|
130
|
+
handler(active_keys)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class AsProviderRazerHuntsman(AsProvider):
|
|
134
|
+
FILTERS = [
|
|
135
|
+
{"vendor_id": 0x1532, "product_id": 0x0266},
|
|
136
|
+
{"vendor_id": 0x1532, "product_id": 0x0282},
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
def start_listening(self, handler: Handler):
|
|
140
|
+
self._running = True
|
|
141
|
+
self._prev_scancodes: set[int] = set()
|
|
142
|
+
self._thread = threading.Thread(target=self._read_loop, args=(handler,), daemon=True)
|
|
143
|
+
self._thread.start()
|
|
144
|
+
|
|
145
|
+
def _handle_report(self, data: bytes, handler: Handler):
|
|
146
|
+
if data[0] != 7: return
|
|
147
|
+
active_keys = []
|
|
148
|
+
current_scancodes = set()
|
|
149
|
+
i = 1
|
|
150
|
+
while i < len(data):
|
|
151
|
+
scancode = data[i]; i += 1
|
|
152
|
+
if scancode == 0: break
|
|
153
|
+
value = data[i]; i += 1
|
|
154
|
+
hid_sc = _razer_to_hid(scancode)
|
|
155
|
+
if hid_sc:
|
|
156
|
+
active_keys.append({"scancode": hid_sc, "value": value / 255})
|
|
157
|
+
current_scancodes.add(hid_sc)
|
|
158
|
+
for released in self._prev_scancodes - current_scancodes:
|
|
159
|
+
active_keys.append({"scancode": released, "value": 0.0})
|
|
160
|
+
self._prev_scancodes = current_scancodes
|
|
161
|
+
handler(active_keys)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class AsProviderRazerHuntsmanV3(AsProvider):
|
|
165
|
+
FILTERS = [
|
|
166
|
+
{"vendor_id": 0x1532, "product_id": 0x02A6},
|
|
167
|
+
{"vendor_id": 0x1532, "product_id": 0x02A7},
|
|
168
|
+
{"vendor_id": 0x1532, "product_id": 0x02B0},
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
def start_listening(self, handler: Handler):
|
|
172
|
+
self._running = True
|
|
173
|
+
self._prev_scancodes: set[int] = set()
|
|
174
|
+
self._thread = threading.Thread(target=self._read_loop, args=(handler,), daemon=True)
|
|
175
|
+
self._thread.start()
|
|
176
|
+
|
|
177
|
+
def _handle_report(self, data: bytes, handler: Handler):
|
|
178
|
+
if data[0] != 11: return
|
|
179
|
+
active_keys = []
|
|
180
|
+
current_scancodes = set()
|
|
181
|
+
i = 1
|
|
182
|
+
while i + 2 < len(data):
|
|
183
|
+
scancode = data[i]; i += 1
|
|
184
|
+
if scancode == 0: break
|
|
185
|
+
value = data[i]; i += 1
|
|
186
|
+
_unused = data[i]; i += 1
|
|
187
|
+
hid_sc = _razer_to_hid(scancode)
|
|
188
|
+
if hid_sc:
|
|
189
|
+
active_keys.append({"scancode": hid_sc, "value": value / 255})
|
|
190
|
+
current_scancodes.add(hid_sc)
|
|
191
|
+
for released in self._prev_scancodes - current_scancodes:
|
|
192
|
+
active_keys.append({"scancode": released, "value": 0.0})
|
|
193
|
+
self._prev_scancodes = current_scancodes
|
|
194
|
+
handler(active_keys)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class AsProviderNuphy(AsProvider):
|
|
198
|
+
FILTERS = [
|
|
199
|
+
{"vendor_id": 0x19F5},
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
def start_listening(self, handler: Handler):
|
|
203
|
+
self._running = True
|
|
204
|
+
self._thread = threading.Thread(target=self._read_loop, args=(handler,), daemon=True)
|
|
205
|
+
self._thread.start()
|
|
206
|
+
|
|
207
|
+
def _handle_report(self, data: bytes, handler: Handler):
|
|
208
|
+
if data[0] != 0xA0: return
|
|
209
|
+
raw_sc = (data[2] << 8) | data[3]
|
|
210
|
+
hid_sc = _nuphy_to_hid(raw_sc)
|
|
211
|
+
if hid_sc == 0: return
|
|
212
|
+
if data[7] == 0:
|
|
213
|
+
self._buffer.pop(hid_sc, None)
|
|
214
|
+
else:
|
|
215
|
+
self._buffer[hid_sc] = data[7] / 200
|
|
216
|
+
handler(self._buffer_to_active_keys())
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class AsProviderDrunkdeer(AsProvider):
|
|
220
|
+
FILTERS = [
|
|
221
|
+
{"usage_page": 0xFF00, "vendor_id": 0x352D},
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
_POLL_PAYLOAD = bytes([
|
|
225
|
+
0xB6, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00,
|
|
226
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
227
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
228
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
229
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
230
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
231
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
232
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
233
|
+
])
|
|
234
|
+
|
|
235
|
+
def start_listening(self, handler: Handler):
|
|
236
|
+
self._running = True
|
|
237
|
+
self._active_keys = []
|
|
238
|
+
self._thread = threading.Thread(target=self._poll_loop, args=(handler,), daemon=True)
|
|
239
|
+
self._thread.start()
|
|
240
|
+
|
|
241
|
+
def _poll_loop(self, handler: Handler):
|
|
242
|
+
self.dev.write(b"\x04" + self._POLL_PAYLOAD)
|
|
243
|
+
while self._running:
|
|
244
|
+
try:
|
|
245
|
+
data = self.dev.read(64, timeout_ms=100)
|
|
246
|
+
if not data: continue
|
|
247
|
+
data = bytes(data)
|
|
248
|
+
n = data[3]
|
|
249
|
+
if n == 0:
|
|
250
|
+
self._active_keys = []
|
|
251
|
+
for i in range(4, len(data)):
|
|
252
|
+
value = data[i]
|
|
253
|
+
if value != 0:
|
|
254
|
+
sc = drunkdeer_index_to_hid_scancode(n * (64 - 5) + (i - 4))
|
|
255
|
+
self._active_keys.append({"scancode": sc, "value": value / 40})
|
|
256
|
+
if n == 2:
|
|
257
|
+
handler(list(self._active_keys))
|
|
258
|
+
self._active_keys = []
|
|
259
|
+
except Exception as e:
|
|
260
|
+
warnings.warn(f"DrunkDeer read error: {e}")
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class AsProviderKeychron(AsProvider):
|
|
265
|
+
FILTERS = [
|
|
266
|
+
{"vendor_id": 0x3434, "product_id": 0x0B10},
|
|
267
|
+
{"vendor_id": 0x3434, "product_id": 0x0B11},
|
|
268
|
+
{"vendor_id": 0x3434, "product_id": 0x0B12},
|
|
269
|
+
{"vendor_id": 0x3434, "product_id": 0x0B30},
|
|
270
|
+
{"vendor_id": 0x3434, "product_id": 0x0B50},
|
|
271
|
+
{"vendor_id": 0x3434, "product_id": 0x0E20},
|
|
272
|
+
{"vendor_id": 0x3434, "product_id": 0x0E21},
|
|
273
|
+
{"vendor_id": 0x3434, "product_id": 0x0E22},
|
|
274
|
+
{"vendor_id": 0x362D, "product_id": 0x0610},
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
_INIT_PAYLOAD = bytes([
|
|
278
|
+
0xA9, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
279
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
280
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
281
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
282
|
+
])
|
|
283
|
+
|
|
284
|
+
def _pick_layout(self):
|
|
285
|
+
pid = self.dev.product_id
|
|
286
|
+
if pid in (0x0B10, 0x0B11, 0x0B12): return LAYOUT_KEYCHRON_Q1_HE
|
|
287
|
+
if pid == 0x0B30: return LAYOUT_KEYCHRON_Q3_HE
|
|
288
|
+
if pid == 0x0B50: return LAYOUT_KEYCHRON_Q5_HE
|
|
289
|
+
if pid == 0x0610: return LAYOUT_LEMOKEY_P1_HE
|
|
290
|
+
return LAYOUT_KEYCHRON_K2_HE
|
|
291
|
+
|
|
292
|
+
def start_listening(self, handler: Handler):
|
|
293
|
+
self._running = True
|
|
294
|
+
self.layout = self._pick_layout()
|
|
295
|
+
self._thread = threading.Thread(target=self._keychron_loop, args=(handler,), daemon=True)
|
|
296
|
+
self._thread.start()
|
|
297
|
+
|
|
298
|
+
def _build_single_key_request(self, index):
|
|
299
|
+
key = layout_key(self.layout, index)
|
|
300
|
+
if key == KEY_NONE: return None
|
|
301
|
+
row = layout_index_to_row(self.layout, index)
|
|
302
|
+
col = layout_index_to_col(self.layout, index)
|
|
303
|
+
buf = bytearray(32)
|
|
304
|
+
buf[0] = 0xA9; buf[1] = 0x30; buf[2] = row; buf[3] = col
|
|
305
|
+
return bytes(buf)
|
|
306
|
+
|
|
307
|
+
def _build_all_keys_request(self):
|
|
308
|
+
buf = bytearray(32)
|
|
309
|
+
buf[0] = 0xA9; buf[1] = 0x31
|
|
310
|
+
return bytes(buf)
|
|
311
|
+
|
|
312
|
+
def _keychron_loop(self, handler: Handler):
|
|
313
|
+
self.dev.write(b"\x00" + self._INIT_PAYLOAD)
|
|
314
|
+
data = bytes(self.dev.read(64, timeout_ms=500) or [])
|
|
315
|
+
if not data: return
|
|
316
|
+
am_version = data[2] if len(data) > 2 else 0
|
|
317
|
+
has_full_report = len(data) > 31 and data[31] == 0x45
|
|
318
|
+
self._buffer = {}
|
|
319
|
+
if has_full_report:
|
|
320
|
+
self._run_full_mode(handler)
|
|
321
|
+
else:
|
|
322
|
+
self._run_single_mode(handler, am_version)
|
|
323
|
+
|
|
324
|
+
def _run_full_mode(self, handler: Handler):
|
|
325
|
+
chunk_index = 0
|
|
326
|
+
self.dev.write(b"\x00" + self._build_all_keys_request())
|
|
327
|
+
while self._running:
|
|
328
|
+
data = bytes(self.dev.read(64, timeout_ms=200) or [])
|
|
329
|
+
if not data: continue
|
|
330
|
+
for i in range(30):
|
|
331
|
+
li = chunk_index * 30 + i
|
|
332
|
+
if li < layout_size(self.layout):
|
|
333
|
+
key = layout_key(self.layout, li)
|
|
334
|
+
if key != KEY_NONE:
|
|
335
|
+
travel = data[2 + i] if 2 + i < len(data) else 0
|
|
336
|
+
if travel >= 5:
|
|
337
|
+
self._buffer[key] = min(travel / 235, 1.0)
|
|
338
|
+
else:
|
|
339
|
+
self._buffer.pop(key, None)
|
|
340
|
+
chunk_index += 1
|
|
341
|
+
if chunk_index == 4:
|
|
342
|
+
handler(self._buffer_to_active_keys())
|
|
343
|
+
chunk_index = 0
|
|
344
|
+
self.dev.write(b"\x00" + self._build_all_keys_request())
|
|
345
|
+
|
|
346
|
+
def _run_single_mode(self, handler: Handler, am_version: int):
|
|
347
|
+
value_offset = 6 if am_version >= 4 else 3
|
|
348
|
+
index = 0
|
|
349
|
+
while True:
|
|
350
|
+
req = self._build_single_key_request(index)
|
|
351
|
+
if req:
|
|
352
|
+
self.dev.write(b"\x00" + req)
|
|
353
|
+
break
|
|
354
|
+
index = (index + 1) % layout_size(self.layout)
|
|
355
|
+
while self._running:
|
|
356
|
+
data = bytes(self.dev.read(64, timeout_ms=200) or [])
|
|
357
|
+
if not data: continue
|
|
358
|
+
key = layout_key(self.layout, index)
|
|
359
|
+
travel = data[value_offset] if value_offset < len(data) else 0
|
|
360
|
+
if travel >= 5:
|
|
361
|
+
self._buffer[key] = min(travel / 235, 1.0)
|
|
362
|
+
else:
|
|
363
|
+
self._buffer.pop(key, None)
|
|
364
|
+
handler(self._buffer_to_active_keys())
|
|
365
|
+
while True:
|
|
366
|
+
index = (index + 1) % layout_size(self.layout)
|
|
367
|
+
req = self._build_single_key_request(index)
|
|
368
|
+
if req:
|
|
369
|
+
self.dev.write(b"\x00" + req)
|
|
370
|
+
break
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class AsProviderMadlions(AsProvider):
|
|
374
|
+
FILTERS = [
|
|
375
|
+
{"vendor_id": 0x373B, "product_id": 0x1053},
|
|
376
|
+
{"vendor_id": 0x373B, "product_id": 0x1055},
|
|
377
|
+
{"vendor_id": 0x373B, "product_id": 0x1056},
|
|
378
|
+
{"vendor_id": 0x373B, "product_id": 0x105D},
|
|
379
|
+
{"vendor_id": 0x373B, "product_id": 0x1058},
|
|
380
|
+
{"vendor_id": 0x373B, "product_id": 0x1059},
|
|
381
|
+
{"vendor_id": 0x373B, "product_id": 0x105A},
|
|
382
|
+
{"vendor_id": 0x373B, "product_id": 0x105C},
|
|
383
|
+
{"vendor_id": 0x373B, "product_id": 0x10A7},
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
_INIT_PAYLOAD = bytes([
|
|
387
|
+
0x02, 0x96, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x04,
|
|
388
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
389
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
390
|
+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
391
|
+
])
|
|
392
|
+
|
|
393
|
+
_MAD60HE_PIDS = {0x1053, 0x1055, 0x1056, 0x105D}
|
|
394
|
+
|
|
395
|
+
def start_listening(self, handler: Handler):
|
|
396
|
+
pid = self.dev.product_id
|
|
397
|
+
self.layout = LAYOUT_MADLIONS_MAD60HE if pid in self._MAD60HE_PIDS else LAYOUT_MADLIONS_MAD68HE
|
|
398
|
+
self._running = True
|
|
399
|
+
self._thread = threading.Thread(target=self._madlions_loop, args=(handler,), daemon=True)
|
|
400
|
+
self._thread.start()
|
|
401
|
+
|
|
402
|
+
def _madlions_loop(self, handler: Handler):
|
|
403
|
+
payload = bytearray(self._INIT_PAYLOAD)
|
|
404
|
+
self.dev.write(b"\x00" + bytes(payload))
|
|
405
|
+
offset = 0
|
|
406
|
+
self._buffer = {}
|
|
407
|
+
while self._running:
|
|
408
|
+
data = bytes(self.dev.read(64, timeout_ms=200) or [])
|
|
409
|
+
if not data: continue
|
|
410
|
+
for i in range(4):
|
|
411
|
+
idx = offset + i
|
|
412
|
+
if idx < len(self.layout):
|
|
413
|
+
key = self.layout[idx]
|
|
414
|
+
pos = 7 + i * 5 + 3
|
|
415
|
+
travel = (data[pos] << 8) | data[pos + 1] if pos + 1 < len(data) else 0
|
|
416
|
+
if travel == 0:
|
|
417
|
+
self._buffer.pop(key, None)
|
|
418
|
+
else:
|
|
419
|
+
self._buffer[key] = travel / 350
|
|
420
|
+
handler(self._buffer_to_active_keys())
|
|
421
|
+
offset = (offset + 4) % len(self.layout)
|
|
422
|
+
payload[6] = offset
|
|
423
|
+
self.dev.write(b"\x00" + bytes(payload))
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class AsProviderBytech(AsProvider):
|
|
427
|
+
FILTERS = [
|
|
428
|
+
{"vendor_id": 0x372E, "product_id": 0x105B},
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
def _build_payload(self, cmd, sub):
|
|
432
|
+
buf = bytearray(63)
|
|
433
|
+
buf[0] = cmd
|
|
434
|
+
buf[1] = sub
|
|
435
|
+
total = 9 + sum(buf[:-1])
|
|
436
|
+
buf[-1] = 255 - (total % 256)
|
|
437
|
+
return bytes(buf)
|
|
438
|
+
|
|
439
|
+
def start_listening(self, handler: Handler):
|
|
440
|
+
self._running = True
|
|
441
|
+
self._thread = threading.Thread(target=self._bytech_loop, args=(handler,), daemon=True)
|
|
442
|
+
self._thread.start()
|
|
443
|
+
|
|
444
|
+
def _bytech_loop(self, handler: Handler):
|
|
445
|
+
payload = self._build_payload(0x97, 0x00)
|
|
446
|
+
self._buffer = {}
|
|
447
|
+
self.dev.write(b"\x09" + payload)
|
|
448
|
+
last_poll = time.monotonic()
|
|
449
|
+
while self._running:
|
|
450
|
+
now = time.monotonic()
|
|
451
|
+
if now - last_poll >= 1.0:
|
|
452
|
+
self.dev.write(b"\x09" + payload)
|
|
453
|
+
last_poll = now
|
|
454
|
+
data = bytes(self.dev.read(64, timeout_ms=100) or [])
|
|
455
|
+
if not data: continue
|
|
456
|
+
if data[0] == 0x97 and data[1] == 0x01:
|
|
457
|
+
self.dev.write(b"\x09" + payload)
|
|
458
|
+
last_poll = time.monotonic()
|
|
459
|
+
self._buffer = {}
|
|
460
|
+
count = data[5]
|
|
461
|
+
for i in range(0, count, 4):
|
|
462
|
+
base = 6 + i
|
|
463
|
+
if base + 3 >= len(data): break
|
|
464
|
+
pos = (data[base] << 8) | data[base + 1]
|
|
465
|
+
distance = (data[base + 2] << 8) | data[base + 3]
|
|
466
|
+
sc = _bytech_to_hid(pos)
|
|
467
|
+
if sc and distance > 10:
|
|
468
|
+
self._buffer[sc] = min(distance / 355, 1.0)
|
|
469
|
+
handler(self._buffer_to_active_keys())
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _razer_to_hid(sc):
|
|
473
|
+
result = razer_to_wooting.get(sc, 0)
|
|
474
|
+
if result == 0:
|
|
475
|
+
warnings.warn(f"Failed to map Razer scancode to HID scancode: {sc:#x}")
|
|
476
|
+
return result
|
|
477
|
+
|
|
478
|
+
def _nuphy_to_hid(sc):
|
|
479
|
+
result = nuphy_to_wooting.get(sc, 0)
|
|
480
|
+
if result == 0:
|
|
481
|
+
warnings.warn(f"Failed to map NuPhy scancode to HID scancode: {sc:#x}")
|
|
482
|
+
return result
|
|
483
|
+
|
|
484
|
+
def _bytech_to_hid(sc):
|
|
485
|
+
result = bytech_to_wooting.get(sc, 0)
|
|
486
|
+
if result == 0:
|
|
487
|
+
warnings.warn(f"Failed to map Bytech scancode to HID scancode: {sc:#x}")
|
|
488
|
+
return result
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
ALL_PROVIDERS = [
|
|
492
|
+
AsProviderWootingV1,
|
|
493
|
+
AsProviderWootingV2,
|
|
494
|
+
AsProviderRazerHuntsman,
|
|
495
|
+
AsProviderRazerHuntsmanV3,
|
|
496
|
+
AsProviderNuphy,
|
|
497
|
+
AsProviderDrunkdeer,
|
|
498
|
+
AsProviderKeychron,
|
|
499
|
+
AsProviderMadlions,
|
|
500
|
+
AsProviderBytech,
|
|
501
|
+
]
|