g13-linux 1.1.3__py3-none-any.whl
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.
- g13_linux/__init__.py +35 -0
- g13_linux/cli.py +24 -0
- g13_linux/device.py +253 -0
- g13_linux/gui/__init__.py +7 -0
- g13_linux/gui/controllers/__init__.py +7 -0
- g13_linux/gui/controllers/app_controller.py +399 -0
- g13_linux/gui/controllers/device_event_controller.py +44 -0
- g13_linux/gui/main.py +85 -0
- g13_linux/gui/models/__init__.py +7 -0
- g13_linux/gui/models/event_decoder.py +321 -0
- g13_linux/gui/models/g13_device.py +140 -0
- g13_linux/gui/models/global_hotkeys.py +284 -0
- g13_linux/gui/models/hardware_controller.py +87 -0
- g13_linux/gui/models/macro_manager.py +162 -0
- g13_linux/gui/models/macro_player.py +290 -0
- g13_linux/gui/models/macro_recorder.py +305 -0
- g13_linux/gui/models/macro_types.py +167 -0
- g13_linux/gui/models/profile_manager.py +153 -0
- g13_linux/gui/resources/__init__.py +7 -0
- g13_linux/gui/resources/g13_layout.py +59 -0
- g13_linux/gui/views/__init__.py +7 -0
- g13_linux/gui/views/button_mapper.py +246 -0
- g13_linux/gui/views/hardware_control.py +98 -0
- g13_linux/gui/views/live_monitor.py +97 -0
- g13_linux/gui/views/macro_editor.py +489 -0
- g13_linux/gui/views/main_window.py +72 -0
- g13_linux/gui/views/profile_manager.py +116 -0
- g13_linux/gui/widgets/__init__.py +7 -0
- g13_linux/gui/widgets/color_picker.py +72 -0
- g13_linux/gui/widgets/g13_button.py +139 -0
- g13_linux/gui/widgets/key_selector.py +130 -0
- g13_linux/gui/widgets/macro_record_dialog.py +272 -0
- g13_linux/hardware/__init__.py +7 -0
- g13_linux/hardware/backlight.py +107 -0
- g13_linux/hardware/lcd.py +327 -0
- g13_linux/mapper.py +109 -0
- g13_linux-1.1.3.dist-info/METADATA +426 -0
- g13_linux-1.1.3.dist-info/RECORD +42 -0
- g13_linux-1.1.3.dist-info/WHEEL +5 -0
- g13_linux-1.1.3.dist-info/entry_points.txt +3 -0
- g13_linux-1.1.3.dist-info/licenses/LICENSE +21 -0
- g13_linux-1.1.3.dist-info/top_level.txt +1 -0
g13_linux/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
G13 Linux Driver
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
A Python userspace driver for the Logitech G13 Gaming Keyboard on Linux.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Full key mapping and macro support
|
|
9
|
+
- RGB LED control
|
|
10
|
+
- LCD display management (160x43 pixels)
|
|
11
|
+
- Profile-based configuration
|
|
12
|
+
- PyQt6 GUI application
|
|
13
|
+
|
|
14
|
+
Basic Usage:
|
|
15
|
+
>>> from g13_linux import open_g13, G13Mapper
|
|
16
|
+
>>> device = open_g13()
|
|
17
|
+
>>> mapper = G13Mapper(device)
|
|
18
|
+
|
|
19
|
+
For more information, see: https://github.com/AreteDriver/G13_Linux
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
__version__ = "1.0.0"
|
|
23
|
+
__author__ = "AreteDriver"
|
|
24
|
+
__license__ = "MIT"
|
|
25
|
+
|
|
26
|
+
from .device import open_g13, read_event, G13_VENDOR_ID, G13_PRODUCT_ID
|
|
27
|
+
from .mapper import G13Mapper
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"open_g13",
|
|
31
|
+
"read_event",
|
|
32
|
+
"G13Mapper",
|
|
33
|
+
"G13_VENDOR_ID",
|
|
34
|
+
"G13_PRODUCT_ID",
|
|
35
|
+
]
|
g13_linux/cli.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .device import open_g13, read_event
|
|
2
|
+
from .mapper import G13Mapper
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
print("Opening Logitech G13…")
|
|
7
|
+
h = open_g13()
|
|
8
|
+
mapper = G13Mapper()
|
|
9
|
+
print("G13 opened. Press keys; Ctrl+C to exit.")
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
while True:
|
|
13
|
+
data = read_event(h)
|
|
14
|
+
if data:
|
|
15
|
+
mapper.handle_raw_report(data)
|
|
16
|
+
except KeyboardInterrupt:
|
|
17
|
+
print("\nExiting.")
|
|
18
|
+
finally:
|
|
19
|
+
h.close()
|
|
20
|
+
mapper.close()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
g13_linux/device.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import glob
|
|
3
|
+
import fcntl
|
|
4
|
+
|
|
5
|
+
G13_VENDOR_ID = 0x046D
|
|
6
|
+
G13_PRODUCT_ID = 0xC21C
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _hidiocsfeature(length):
|
|
10
|
+
"""HIDIOCSFEATURE ioctl for setting feature reports."""
|
|
11
|
+
return 0xC0004806 | (length << 16)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _hidiocgfeature(length):
|
|
15
|
+
"""HIDIOCGFEATURE ioctl for getting feature reports."""
|
|
16
|
+
return 0xC0004807 | (length << 16)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HidrawDevice:
|
|
20
|
+
"""Wrapper for hidraw device file to provide consistent interface."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, path):
|
|
23
|
+
self.path = path
|
|
24
|
+
self._fd = None
|
|
25
|
+
self._file = None
|
|
26
|
+
|
|
27
|
+
def open(self):
|
|
28
|
+
self._file = open(self.path, "rb+", buffering=0)
|
|
29
|
+
self._fd = self._file.fileno()
|
|
30
|
+
os.set_blocking(self._fd, False)
|
|
31
|
+
|
|
32
|
+
def read(self, size):
|
|
33
|
+
try:
|
|
34
|
+
data = self._file.read(size)
|
|
35
|
+
return list(data) if data else None
|
|
36
|
+
except BlockingIOError:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def write(self, data):
|
|
40
|
+
"""Write an output report to the device."""
|
|
41
|
+
return self._file.write(bytes(data))
|
|
42
|
+
|
|
43
|
+
def send_feature_report(self, data):
|
|
44
|
+
"""
|
|
45
|
+
Send a HID feature report to the device.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
data: Report data (first byte should be report ID)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Number of bytes written
|
|
52
|
+
"""
|
|
53
|
+
if self._fd is None:
|
|
54
|
+
raise RuntimeError("Device not open")
|
|
55
|
+
|
|
56
|
+
buf = bytes(data)
|
|
57
|
+
return fcntl.ioctl(self._fd, _hidiocsfeature(len(buf)), buf)
|
|
58
|
+
|
|
59
|
+
def get_feature_report(self, report_id, size):
|
|
60
|
+
"""
|
|
61
|
+
Get a HID feature report from the device.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
report_id: Report ID to request
|
|
65
|
+
size: Expected report size
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Report data as bytes
|
|
69
|
+
"""
|
|
70
|
+
if self._fd is None:
|
|
71
|
+
raise RuntimeError("Device not open")
|
|
72
|
+
|
|
73
|
+
buf = bytearray(size)
|
|
74
|
+
buf[0] = report_id
|
|
75
|
+
fcntl.ioctl(self._fd, _hidiocgfeature(size), buf)
|
|
76
|
+
return bytes(buf)
|
|
77
|
+
|
|
78
|
+
def close(self):
|
|
79
|
+
if self._file:
|
|
80
|
+
self._file.close()
|
|
81
|
+
self._file = None
|
|
82
|
+
self._fd = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def find_g13_hidraw():
|
|
86
|
+
"""Find the hidraw device path for the G13."""
|
|
87
|
+
for hidraw in glob.glob("/sys/class/hidraw/hidraw*"):
|
|
88
|
+
uevent_path = os.path.join(hidraw, "device", "uevent")
|
|
89
|
+
try:
|
|
90
|
+
with open(uevent_path, "r") as f:
|
|
91
|
+
content = f.read()
|
|
92
|
+
# Check for G13 HID_ID (format: 0003:0000046D:0000C21C)
|
|
93
|
+
if "0000046D" in content.upper() and "0000C21C" in content.upper():
|
|
94
|
+
device_name = os.path.basename(hidraw)
|
|
95
|
+
return f"/dev/{device_name}"
|
|
96
|
+
except (IOError, OSError):
|
|
97
|
+
continue
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def open_g13():
|
|
102
|
+
"""Open the G13 device and return a handle."""
|
|
103
|
+
hidraw_path = find_g13_hidraw()
|
|
104
|
+
if not hidraw_path:
|
|
105
|
+
raise RuntimeError("Logitech G13 not found")
|
|
106
|
+
|
|
107
|
+
device = HidrawDevice(hidraw_path)
|
|
108
|
+
device.open()
|
|
109
|
+
return device
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def read_event(handle):
|
|
113
|
+
"""Read a HID report from the device."""
|
|
114
|
+
data = handle.read(64)
|
|
115
|
+
return data if data else None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class LibUSBDevice:
|
|
119
|
+
"""
|
|
120
|
+
Direct libusb access for G13 input reading.
|
|
121
|
+
|
|
122
|
+
Required because hid-generic kernel driver consumes input reports
|
|
123
|
+
and doesn't pass them to hidraw. This requires root/sudo to detach
|
|
124
|
+
the kernel driver.
|
|
125
|
+
|
|
126
|
+
Note: Linux kernel 6.19+ will have proper hid-lg-g15 support for G13.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
ENDPOINT_IN = 0x81 # EP 1 IN for button/joystick data
|
|
130
|
+
ENDPOINT_OUT = 0x02 # EP 2 OUT for LCD data
|
|
131
|
+
REPORT_SIZE = 8 # 7 bytes data + 1 byte report ID
|
|
132
|
+
|
|
133
|
+
def __init__(self):
|
|
134
|
+
self._dev = None
|
|
135
|
+
self._reattach = False
|
|
136
|
+
|
|
137
|
+
def open(self):
|
|
138
|
+
"""Open G13 via libusb, detaching kernel driver."""
|
|
139
|
+
try:
|
|
140
|
+
import usb.core
|
|
141
|
+
import usb.util
|
|
142
|
+
except ImportError:
|
|
143
|
+
raise RuntimeError("pyusb not installed. Run: pip install pyusb")
|
|
144
|
+
|
|
145
|
+
self._dev = usb.core.find(idVendor=G13_VENDOR_ID, idProduct=G13_PRODUCT_ID)
|
|
146
|
+
if self._dev is None:
|
|
147
|
+
raise RuntimeError("G13 not found")
|
|
148
|
+
|
|
149
|
+
# Detach kernel driver from all interfaces
|
|
150
|
+
for intf_num in range(2):
|
|
151
|
+
try:
|
|
152
|
+
if self._dev.is_kernel_driver_active(intf_num):
|
|
153
|
+
self._dev.detach_kernel_driver(intf_num)
|
|
154
|
+
self._reattach = True
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
# Set configuration
|
|
159
|
+
try:
|
|
160
|
+
self._dev.set_configuration()
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Claim both interfaces
|
|
165
|
+
import usb.util
|
|
166
|
+
for intf_num in range(2):
|
|
167
|
+
try:
|
|
168
|
+
usb.util.claim_interface(self._dev, intf_num)
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
# Get endpoints from interface 0
|
|
173
|
+
cfg = self._dev.get_active_configuration()
|
|
174
|
+
intf = cfg[(0, 0)]
|
|
175
|
+
|
|
176
|
+
self._ep_in = usb.util.find_descriptor(
|
|
177
|
+
intf,
|
|
178
|
+
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress)
|
|
179
|
+
== usb.util.ENDPOINT_IN,
|
|
180
|
+
)
|
|
181
|
+
self._ep_out = usb.util.find_descriptor(
|
|
182
|
+
intf,
|
|
183
|
+
custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress)
|
|
184
|
+
== usb.util.ENDPOINT_OUT,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def read(self, timeout_ms=100):
|
|
188
|
+
"""
|
|
189
|
+
Read button/joystick report.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
List of bytes or None on timeout
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
data = self._ep_in.read(64, timeout=timeout_ms)
|
|
196
|
+
return list(data) if data else None
|
|
197
|
+
except Exception:
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
def write(self, data):
|
|
201
|
+
"""
|
|
202
|
+
Write output data (for LCD) via interrupt transfer.
|
|
203
|
+
|
|
204
|
+
Uses endpoint 0x02 OUT which is the LCD data endpoint.
|
|
205
|
+
"""
|
|
206
|
+
# Use direct interrupt write to endpoint 0x02
|
|
207
|
+
return self._dev.write(self.ENDPOINT_OUT, bytes(data), timeout=1000)
|
|
208
|
+
|
|
209
|
+
def send_feature_report(self, data):
|
|
210
|
+
"""Send feature report via control transfer."""
|
|
211
|
+
report_id = data[0]
|
|
212
|
+
return self._dev.ctrl_transfer(
|
|
213
|
+
0x21, # bmRequestType: Host-to-device, Class, Interface
|
|
214
|
+
0x09, # bRequest: SET_REPORT
|
|
215
|
+
0x0300 | report_id, # wValue: Feature report + report ID
|
|
216
|
+
0, # wIndex: Interface 0
|
|
217
|
+
bytes(data),
|
|
218
|
+
1000, # timeout
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def close(self):
|
|
222
|
+
"""Close device and reattach kernel driver."""
|
|
223
|
+
if self._dev:
|
|
224
|
+
import usb.util
|
|
225
|
+
|
|
226
|
+
# Release both interfaces
|
|
227
|
+
for intf_num in range(2):
|
|
228
|
+
try:
|
|
229
|
+
usb.util.release_interface(self._dev, intf_num)
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
# Reattach kernel drivers
|
|
234
|
+
if self._reattach:
|
|
235
|
+
for intf_num in range(2):
|
|
236
|
+
try:
|
|
237
|
+
self._dev.attach_kernel_driver(intf_num)
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
self._dev = None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def open_g13_libusb():
|
|
245
|
+
"""
|
|
246
|
+
Open G13 using libusb for input reading.
|
|
247
|
+
|
|
248
|
+
Requires root/sudo to detach kernel driver.
|
|
249
|
+
Use this when you need button/joystick input.
|
|
250
|
+
"""
|
|
251
|
+
device = LibUSBDevice()
|
|
252
|
+
device.open()
|
|
253
|
+
return device
|