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.
Files changed (42) hide show
  1. g13_linux/__init__.py +35 -0
  2. g13_linux/cli.py +24 -0
  3. g13_linux/device.py +253 -0
  4. g13_linux/gui/__init__.py +7 -0
  5. g13_linux/gui/controllers/__init__.py +7 -0
  6. g13_linux/gui/controllers/app_controller.py +399 -0
  7. g13_linux/gui/controllers/device_event_controller.py +44 -0
  8. g13_linux/gui/main.py +85 -0
  9. g13_linux/gui/models/__init__.py +7 -0
  10. g13_linux/gui/models/event_decoder.py +321 -0
  11. g13_linux/gui/models/g13_device.py +140 -0
  12. g13_linux/gui/models/global_hotkeys.py +284 -0
  13. g13_linux/gui/models/hardware_controller.py +87 -0
  14. g13_linux/gui/models/macro_manager.py +162 -0
  15. g13_linux/gui/models/macro_player.py +290 -0
  16. g13_linux/gui/models/macro_recorder.py +305 -0
  17. g13_linux/gui/models/macro_types.py +167 -0
  18. g13_linux/gui/models/profile_manager.py +153 -0
  19. g13_linux/gui/resources/__init__.py +7 -0
  20. g13_linux/gui/resources/g13_layout.py +59 -0
  21. g13_linux/gui/views/__init__.py +7 -0
  22. g13_linux/gui/views/button_mapper.py +246 -0
  23. g13_linux/gui/views/hardware_control.py +98 -0
  24. g13_linux/gui/views/live_monitor.py +97 -0
  25. g13_linux/gui/views/macro_editor.py +489 -0
  26. g13_linux/gui/views/main_window.py +72 -0
  27. g13_linux/gui/views/profile_manager.py +116 -0
  28. g13_linux/gui/widgets/__init__.py +7 -0
  29. g13_linux/gui/widgets/color_picker.py +72 -0
  30. g13_linux/gui/widgets/g13_button.py +139 -0
  31. g13_linux/gui/widgets/key_selector.py +130 -0
  32. g13_linux/gui/widgets/macro_record_dialog.py +272 -0
  33. g13_linux/hardware/__init__.py +7 -0
  34. g13_linux/hardware/backlight.py +107 -0
  35. g13_linux/hardware/lcd.py +327 -0
  36. g13_linux/mapper.py +109 -0
  37. g13_linux-1.1.3.dist-info/METADATA +426 -0
  38. g13_linux-1.1.3.dist-info/RECORD +42 -0
  39. g13_linux-1.1.3.dist-info/WHEEL +5 -0
  40. g13_linux-1.1.3.dist-info/entry_points.txt +3 -0
  41. g13_linux-1.1.3.dist-info/licenses/LICENSE +21 -0
  42. 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
@@ -0,0 +1,7 @@
1
+ """
2
+ G13LogitechOPS GUI Module
3
+
4
+ PyQt6-based graphical interface for configuring the Logitech G13 Gaming Keyboard.
5
+ """
6
+
7
+ __all__ = []
@@ -0,0 +1,7 @@
1
+ """
2
+ GUI Controllers Module
3
+
4
+ Business logic coordinators connecting models to views.
5
+ """
6
+
7
+ __all__ = []