makcu 0.1.3__py3-none-any.whl → 0.2.0__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.
- makcu/__init__.py +71 -9
- makcu/__main__.py +254 -37
- makcu/conftest.py +27 -17
- makcu/connection.py +439 -224
- makcu/controller.py +345 -100
- makcu/makcu.pyi +13 -0
- makcu/mouse.py +240 -86
- makcu/py.typed +2 -0
- makcu/test_suite.py +113 -54
- makcu-0.2.0.dist-info/METADATA +1141 -0
- makcu-0.2.0.dist-info/RECORD +16 -0
- makcu-0.1.3.dist-info/METADATA +0 -310
- makcu-0.1.3.dist-info/RECORD +0 -14
- {makcu-0.1.3.dist-info → makcu-0.2.0.dist-info}/WHEEL +0 -0
- {makcu-0.1.3.dist-info → makcu-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {makcu-0.1.3.dist-info → makcu-0.2.0.dist-info}/top_level.txt +0 -0
makcu/connection.py
CHANGED
@@ -1,271 +1,486 @@
|
|
1
1
|
import serial
|
2
2
|
import threading
|
3
3
|
import time
|
4
|
+
import struct
|
5
|
+
from typing import Optional, Dict, Callable, List, Any, Tuple, Union
|
4
6
|
from serial.tools import list_ports
|
7
|
+
from dataclasses import dataclass, field
|
8
|
+
from collections import deque
|
9
|
+
from concurrent.futures import Future
|
10
|
+
import logging
|
11
|
+
import asyncio
|
12
|
+
import re
|
5
13
|
from .errors import MakcuConnectionError, MakcuTimeoutError
|
6
14
|
from .enums import MouseButton
|
7
15
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class PendingCommand:
|
20
|
+
"""Tracks a command waiting for response"""
|
21
|
+
command_id: int
|
22
|
+
command: str
|
23
|
+
future: Future
|
24
|
+
timestamp: float
|
25
|
+
expect_response: bool = True
|
26
|
+
timeout: float = 0.1 # Reduced from 1.0
|
27
|
+
|
28
|
+
@dataclass
|
29
|
+
class ParsedResponse:
|
30
|
+
"""Parsed response from device"""
|
31
|
+
command_id: Optional[int]
|
32
|
+
content: str
|
33
|
+
is_button_data: bool = False
|
34
|
+
button_mask: Optional[int] = None
|
18
35
|
|
19
|
-
|
36
|
+
class SerialTransport:
|
37
|
+
"""Ultra-optimized serial transport for gaming performance"""
|
38
|
+
|
39
|
+
BAUD_CHANGE_COMMAND = bytearray([0xDE, 0xAD, 0x05, 0x00, 0xA5, 0x00, 0x09, 0x3D, 0x00])
|
40
|
+
DEFAULT_TIMEOUT = 0.1 # Reduced from 1.0 for gaming
|
41
|
+
MAX_RECONNECT_ATTEMPTS = 3 # Reduced from 5
|
42
|
+
RECONNECT_DELAY = 0.1 # Reduced from 0.5
|
43
|
+
|
44
|
+
# Pre-computed button maps for faster lookups
|
45
|
+
BUTTON_MAP = (
|
46
|
+
'left', 'right', 'middle', 'mouse4', 'mouse5'
|
47
|
+
)
|
48
|
+
|
49
|
+
BUTTON_ENUM_MAP = (
|
50
|
+
MouseButton.LEFT,
|
51
|
+
MouseButton.RIGHT,
|
52
|
+
MouseButton.MIDDLE,
|
53
|
+
MouseButton.MOUSE4,
|
54
|
+
MouseButton.MOUSE5,
|
55
|
+
)
|
56
|
+
|
57
|
+
def __init__(self, fallback: str = "", debug: bool = False,
|
58
|
+
send_init: bool = True, auto_reconnect: bool = True,
|
59
|
+
override_port: bool = False) -> None:
|
60
|
+
# Basic config
|
20
61
|
self._fallback_com_port = fallback
|
21
|
-
self._log_messages = []
|
22
62
|
self.debug = debug
|
23
63
|
self.send_init = send_init
|
24
|
-
self.
|
25
|
-
self.
|
26
|
-
|
64
|
+
self.auto_reconnect = auto_reconnect
|
65
|
+
self.override_port = override_port
|
66
|
+
|
67
|
+
# Connection state
|
27
68
|
self._is_connected = False
|
28
|
-
self.
|
29
|
-
self.
|
30
|
-
self._button_states = {btn: False for btn in self.button_map.values()}
|
31
|
-
self._last_callback_time = {bit: 0 for bit in self.button_map}
|
32
|
-
self._pause_listener = False
|
33
|
-
|
34
|
-
self._button_enum_map = {
|
35
|
-
0: MouseButton.LEFT,
|
36
|
-
1: MouseButton.RIGHT,
|
37
|
-
2: MouseButton.MIDDLE,
|
38
|
-
3: MouseButton.MOUSE4,
|
39
|
-
4: MouseButton.MOUSE5,
|
40
|
-
}
|
41
|
-
|
42
|
-
self.port = self.find_com_port()
|
43
|
-
if not self.port:
|
44
|
-
raise MakcuConnectionError("Makcu device not found. Please specify a port explicitly.")
|
45
|
-
|
69
|
+
self._reconnect_attempts = 0
|
70
|
+
self.port: Optional[str] = None
|
46
71
|
self.baudrate = 115200
|
47
|
-
self.serial = None
|
48
|
-
self._current_baud = None
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
self.
|
74
|
-
|
75
|
-
def _log(self, message):
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
72
|
+
self.serial: Optional[serial.Serial] = None
|
73
|
+
self._current_baud: Optional[int] = None
|
74
|
+
|
75
|
+
# Command tracking with pre-allocated buffer
|
76
|
+
self._command_counter = 0
|
77
|
+
self._pending_commands: Dict[int, PendingCommand] = {}
|
78
|
+
self._command_lock = threading.Lock()
|
79
|
+
|
80
|
+
# Response parsing with optimized buffer
|
81
|
+
self._parse_buffer = bytearray(1024) # Pre-allocate
|
82
|
+
self._buffer_pos = 0
|
83
|
+
self._response_queue = deque(maxlen=100) # Limit queue size
|
84
|
+
|
85
|
+
# Button state with bitwise operations
|
86
|
+
self._button_callback: Optional[Callable[[MouseButton, bool], None]] = None
|
87
|
+
self._last_button_mask = 0
|
88
|
+
self._button_states = 0 # Use single int instead of dict
|
89
|
+
|
90
|
+
# Threading
|
91
|
+
self._stop_event = threading.Event()
|
92
|
+
self._listener_thread: Optional[threading.Thread] = None
|
93
|
+
|
94
|
+
# Logging optimization
|
95
|
+
self._log_messages: deque = deque(maxlen=100)
|
96
|
+
|
97
|
+
# Cache for frequently used data
|
98
|
+
self._ascii_decode_table = bytes(range(128)) # ASCII lookup table
|
99
|
+
|
100
|
+
def _log(self, message: str, level: str = "INFO") -> None:
|
101
|
+
"""Optimized logging - only format when needed"""
|
102
|
+
if not self.debug and level == "DEBUG":
|
103
|
+
return
|
104
|
+
|
105
|
+
if self.debug:
|
106
|
+
# Use faster time formatting
|
107
|
+
timestamp = f"{time.time():.3f}"
|
108
|
+
entry = f"[{timestamp}] [{level}] {message}"
|
109
|
+
self._log_messages.append(entry)
|
110
|
+
print(entry, flush=True)
|
111
|
+
|
112
|
+
def _generate_command_id(self) -> int:
|
113
|
+
"""Generate unique command ID - optimized with no lock for single-threaded access"""
|
114
|
+
self._command_counter = (self._command_counter + 1) & 0x2710 # Faster than % 10000
|
115
|
+
return self._command_counter
|
116
|
+
|
117
|
+
def find_com_port(self) -> Optional[str]:
|
118
|
+
"""Optimized port finding with caching"""
|
119
|
+
if self.override_port:
|
120
|
+
return self._fallback_com_port
|
121
|
+
|
122
|
+
# Cache the VID:PID string
|
123
|
+
target_hwid = "VID:PID=1A86:55D3"
|
124
|
+
|
86
125
|
for port in list_ports.comports():
|
87
|
-
if
|
126
|
+
if target_hwid in port.hwid.upper():
|
88
127
|
self._log(f"Device found: {port.device}")
|
89
128
|
return port.device
|
90
|
-
|
129
|
+
|
91
130
|
if self._fallback_com_port:
|
92
|
-
self._log(f"
|
131
|
+
self._log(f"Using fallback: {self._fallback_com_port}")
|
93
132
|
return self._fallback_com_port
|
94
|
-
|
95
|
-
|
96
|
-
|
133
|
+
|
134
|
+
return None
|
135
|
+
|
136
|
+
def _parse_response_line(self, line: bytes) -> ParsedResponse:
|
137
|
+
"""Optimized parsing - work with bytes directly"""
|
138
|
+
# Skip decode for simple checks
|
139
|
+
if line.startswith(b'>>> '):
|
140
|
+
content = line[4:].decode('ascii', 'ignore').strip()
|
141
|
+
return ParsedResponse(None, content, False)
|
142
|
+
|
143
|
+
content = line.decode('ascii', 'ignore').strip()
|
144
|
+
return ParsedResponse(None, content, False)
|
145
|
+
|
146
|
+
def _handle_button_data(self, byte_val: int) -> None:
|
147
|
+
"""Optimized button handling with bitwise operations"""
|
148
|
+
if byte_val == self._last_button_mask:
|
149
|
+
return
|
150
|
+
|
151
|
+
changed_bits = byte_val ^ self._last_button_mask
|
152
|
+
|
153
|
+
# Use bitwise operations instead of dict lookups
|
154
|
+
for bit in range(5): # Only check 5 buttons
|
155
|
+
if changed_bits & (1 << bit):
|
156
|
+
is_pressed = bool(byte_val & (1 << bit))
|
157
|
+
|
158
|
+
# Update button state in single int
|
159
|
+
if is_pressed:
|
160
|
+
self._button_states |= (1 << bit)
|
161
|
+
else:
|
162
|
+
self._button_states &= ~(1 << bit)
|
163
|
+
|
164
|
+
if self._button_callback and bit < len(self.BUTTON_ENUM_MAP):
|
165
|
+
try:
|
166
|
+
self._button_callback(self.BUTTON_ENUM_MAP[bit], is_pressed)
|
167
|
+
except Exception:
|
168
|
+
pass # Silently ignore callback errors for speed
|
169
|
+
|
170
|
+
self._last_button_mask = byte_val
|
171
|
+
|
172
|
+
def _process_pending_commands(self, content: str) -> None:
|
173
|
+
"""Optimized command processing"""
|
174
|
+
if not content or not self._pending_commands:
|
175
|
+
return
|
97
176
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
177
|
+
with self._command_lock:
|
178
|
+
if not self._pending_commands:
|
179
|
+
return
|
180
|
+
|
181
|
+
# Get oldest command without min() call
|
182
|
+
oldest_id = next(iter(self._pending_commands))
|
183
|
+
pending = self._pending_commands[oldest_id]
|
184
|
+
|
185
|
+
if pending.future.done():
|
186
|
+
return
|
187
|
+
|
188
|
+
# Fast string comparison
|
189
|
+
if content == pending.command:
|
190
|
+
if not pending.expect_response:
|
191
|
+
pending.future.set_result(pending.command)
|
192
|
+
del self._pending_commands[oldest_id]
|
193
|
+
else:
|
194
|
+
pending.future.set_result(content)
|
195
|
+
del self._pending_commands[oldest_id]
|
196
|
+
|
197
|
+
def _cleanup_timed_out_commands(self) -> None:
|
198
|
+
"""Optimized cleanup - batch operations"""
|
199
|
+
if not self._pending_commands:
|
200
|
+
return
|
201
|
+
|
202
|
+
current_time = time.time()
|
203
|
+
with self._command_lock:
|
204
|
+
# Collect timed out commands
|
205
|
+
timed_out = [
|
206
|
+
(cmd_id, pending)
|
207
|
+
for cmd_id, pending in self._pending_commands.items()
|
208
|
+
if current_time - pending.timestamp > pending.timeout
|
209
|
+
]
|
210
|
+
|
211
|
+
# Batch remove
|
212
|
+
for cmd_id, pending in timed_out:
|
213
|
+
del self._pending_commands[cmd_id]
|
214
|
+
if not pending.future.done():
|
215
|
+
pending.future.set_exception(
|
216
|
+
MakcuTimeoutError(f"Command #{cmd_id} timed out")
|
217
|
+
)
|
218
|
+
|
219
|
+
def _listen(self) -> None:
|
220
|
+
"""Ultra-optimized listener for gaming performance"""
|
221
|
+
# Pre-allocate buffers
|
222
|
+
read_buffer = bytearray(4096)
|
223
|
+
line_buffer = bytearray(256)
|
224
|
+
line_pos = 0
|
225
|
+
|
226
|
+
# Cache frequently accessed attributes
|
227
|
+
serial_read = self.serial.read
|
228
|
+
serial_in_waiting = lambda: self.serial.in_waiting
|
229
|
+
is_connected = lambda: self._is_connected
|
230
|
+
stop_requested = self._stop_event.is_set
|
231
|
+
|
232
|
+
# Timing for cleanup
|
233
|
+
last_cleanup = time.time()
|
234
|
+
cleanup_interval = 0.05 # 50ms cleanup interval
|
235
|
+
|
236
|
+
while is_connected() and not stop_requested():
|
237
|
+
try:
|
238
|
+
# Check bytes available
|
239
|
+
bytes_available = serial_in_waiting()
|
240
|
+
if not bytes_available:
|
241
|
+
time.sleep(0.001) # 1ms sleep to prevent CPU spinning
|
242
|
+
continue
|
243
|
+
|
244
|
+
# Read into pre-allocated buffer
|
245
|
+
bytes_read = serial_read(min(bytes_available, 4096))
|
246
|
+
|
247
|
+
# Process bytes
|
248
|
+
for byte_val in bytes_read:
|
249
|
+
# Fast button data check
|
250
|
+
if byte_val < 32 and byte_val not in (0x0D, 0x0A):
|
251
|
+
self._handle_button_data(byte_val)
|
252
|
+
else:
|
253
|
+
# Build line
|
254
|
+
if byte_val == 0x0A: # LF
|
255
|
+
if line_pos > 0:
|
256
|
+
# Process line without allocation
|
257
|
+
line = bytes(line_buffer[:line_pos])
|
258
|
+
line_pos = 0
|
259
|
+
|
260
|
+
if line:
|
261
|
+
response = self._parse_response_line(line)
|
262
|
+
if response.content:
|
263
|
+
self._process_pending_commands(response.content)
|
264
|
+
elif byte_val != 0x0D: # Ignore CR
|
265
|
+
if line_pos < 256: # Prevent overflow
|
266
|
+
line_buffer[line_pos] = byte_val
|
267
|
+
line_pos += 1
|
268
|
+
|
269
|
+
# Periodic cleanup with reduced frequency
|
270
|
+
current_time = time.time()
|
271
|
+
if current_time - last_cleanup > cleanup_interval:
|
272
|
+
self._cleanup_timed_out_commands()
|
273
|
+
last_cleanup = current_time
|
274
|
+
|
275
|
+
except serial.SerialException:
|
276
|
+
if self.auto_reconnect:
|
277
|
+
self._attempt_reconnect()
|
278
|
+
else:
|
279
|
+
break
|
280
|
+
except Exception:
|
281
|
+
pass # Silently continue for maximum performance
|
105
282
|
|
106
|
-
def
|
283
|
+
def _attempt_reconnect(self) -> None:
|
284
|
+
"""Fast reconnection attempt"""
|
285
|
+
if self._reconnect_attempts >= self.MAX_RECONNECT_ATTEMPTS:
|
286
|
+
self._is_connected = False
|
287
|
+
return
|
288
|
+
|
289
|
+
self._reconnect_attempts += 1
|
290
|
+
|
291
|
+
try:
|
292
|
+
if self.serial and self.serial.is_open:
|
293
|
+
self.serial.close()
|
294
|
+
|
295
|
+
time.sleep(self.RECONNECT_DELAY)
|
296
|
+
|
297
|
+
self.port = self.find_com_port()
|
298
|
+
if not self.port:
|
299
|
+
raise MakcuConnectionError("Device not found")
|
300
|
+
|
301
|
+
# Use write_timeout for faster failure detection
|
302
|
+
self.serial = serial.Serial(
|
303
|
+
self.port,
|
304
|
+
self.baudrate,
|
305
|
+
timeout=0.001, # 1ms read timeout
|
306
|
+
write_timeout=0.01 # 10ms write timeout
|
307
|
+
)
|
308
|
+
self._change_baud_to_4M()
|
309
|
+
|
310
|
+
if self.send_init:
|
311
|
+
self.serial.write(b"km.buttons(1)\r")
|
312
|
+
self.serial.flush()
|
313
|
+
|
314
|
+
self._reconnect_attempts = 0
|
315
|
+
|
316
|
+
except Exception:
|
317
|
+
time.sleep(self.RECONNECT_DELAY)
|
318
|
+
|
319
|
+
def _change_baud_to_4M(self) -> bool:
|
320
|
+
"""Optimized baud rate change"""
|
107
321
|
if self.serial and self.serial.is_open:
|
108
|
-
self.
|
109
|
-
self.serial.write(self.baud_change_command)
|
322
|
+
self.serial.write(self.BAUD_CHANGE_COMMAND)
|
110
323
|
self.serial.flush()
|
111
|
-
time.sleep(0.05
|
324
|
+
time.sleep(0.02) # Reduced from 0.05
|
112
325
|
self.serial.baudrate = 4000000
|
113
326
|
self._current_baud = 4000000
|
114
|
-
self._log("Switched to 4M baud successfully.")
|
115
327
|
return True
|
116
328
|
return False
|
117
329
|
|
118
|
-
|
119
|
-
|
330
|
+
def connect(self) -> None:
|
331
|
+
"""Optimized connection with minimal overhead"""
|
120
332
|
if self._is_connected:
|
121
|
-
self._log("Already connected.")
|
122
333
|
return
|
123
|
-
|
124
|
-
if not self.
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
self.
|
130
|
-
|
131
|
-
|
334
|
+
|
335
|
+
if not self.override_port:
|
336
|
+
self.port = self.find_com_port()
|
337
|
+
else:
|
338
|
+
self.port = self._fallback_com_port
|
339
|
+
|
340
|
+
if not self.port:
|
341
|
+
raise MakcuConnectionError("Makcu device not found")
|
342
|
+
|
343
|
+
try:
|
344
|
+
# Optimized serial settings for gaming
|
345
|
+
self.serial = serial.Serial(
|
346
|
+
self.port,
|
347
|
+
115200,
|
348
|
+
timeout=0.001, # 1ms timeout
|
349
|
+
write_timeout=0.01, # 10ms write timeout
|
350
|
+
xonxoff=False,
|
351
|
+
rtscts=False,
|
352
|
+
dsrdtr=False
|
353
|
+
)
|
354
|
+
|
355
|
+
if not self._change_baud_to_4M():
|
356
|
+
raise MakcuConnectionError("Failed to switch to 4M baud")
|
357
|
+
|
358
|
+
self._is_connected = True
|
359
|
+
self._reconnect_attempts = 0
|
360
|
+
|
361
|
+
if self.send_init:
|
132
362
|
self.serial.write(b"km.buttons(1)\r")
|
133
363
|
self.serial.flush()
|
134
|
-
|
135
|
-
|
364
|
+
|
365
|
+
# Start high-priority listener thread
|
136
366
|
self._stop_event.clear()
|
137
|
-
self._listener_thread = threading.Thread(
|
367
|
+
self._listener_thread = threading.Thread(
|
368
|
+
target=self._listen,
|
369
|
+
daemon=True,
|
370
|
+
name="MakcuListener"
|
371
|
+
)
|
138
372
|
self._listener_thread.start()
|
373
|
+
|
374
|
+
except Exception as e:
|
375
|
+
if self.serial:
|
376
|
+
self.serial.close()
|
377
|
+
raise MakcuConnectionError(f"Failed to connect: {e}")
|
139
378
|
|
140
|
-
def disconnect(self):
|
379
|
+
def disconnect(self) -> None:
|
380
|
+
"""Fast disconnect"""
|
381
|
+
self._is_connected = False
|
382
|
+
|
141
383
|
if self.send_init:
|
142
384
|
self._stop_event.set()
|
143
|
-
if self._listener_thread:
|
144
|
-
self._listener_thread.join()
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
self.
|
151
|
-
|
152
|
-
|
153
|
-
|
385
|
+
if self._listener_thread and self._listener_thread.is_alive():
|
386
|
+
self._listener_thread.join(timeout=0.1) # Reduced timeout
|
387
|
+
|
388
|
+
with self._command_lock:
|
389
|
+
for pending in self._pending_commands.values():
|
390
|
+
if not pending.future.done():
|
391
|
+
pending.future.cancel()
|
392
|
+
self._pending_commands.clear()
|
393
|
+
|
394
|
+
if self.serial and self.serial.is_open:
|
395
|
+
self.serial.close()
|
396
|
+
self.serial = None
|
154
397
|
|
155
|
-
def send_command(self, command, expect_response=False
|
398
|
+
def send_command(self, command: str, expect_response: bool = False,
|
399
|
+
timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
|
400
|
+
"""Optimized command sending for minimal latency"""
|
156
401
|
if not self._is_connected or not self.serial or not self.serial.is_open:
|
157
|
-
raise MakcuConnectionError("
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
402
|
+
raise MakcuConnectionError("Not connected")
|
403
|
+
|
404
|
+
# For commands without response, send and return immediately
|
405
|
+
if not expect_response:
|
406
|
+
self.serial.write(f"{command}\r\n".encode('ascii'))
|
407
|
+
self.serial.flush()
|
408
|
+
return command
|
409
|
+
|
410
|
+
# Commands with response need tracking
|
411
|
+
cmd_id = self._generate_command_id()
|
412
|
+
tagged_command = f"{command}#{cmd_id}"
|
413
|
+
|
414
|
+
future = Future()
|
415
|
+
|
416
|
+
with self._command_lock:
|
417
|
+
self._pending_commands[cmd_id] = PendingCommand(
|
418
|
+
command_id=cmd_id,
|
419
|
+
command=command,
|
420
|
+
future=future,
|
421
|
+
timestamp=time.time(),
|
422
|
+
expect_response=expect_response,
|
423
|
+
timeout=timeout
|
424
|
+
)
|
425
|
+
|
426
|
+
try:
|
427
|
+
self.serial.write(f"{tagged_command}\r\n".encode('ascii'))
|
428
|
+
self.serial.flush()
|
429
|
+
|
430
|
+
result = future.result(timeout=timeout)
|
431
|
+
return result.split('#')[0] if '#' in result else result
|
432
|
+
|
433
|
+
except TimeoutError:
|
434
|
+
raise MakcuTimeoutError(f"Command timed out: {command}")
|
435
|
+
except Exception as e:
|
436
|
+
with self._command_lock:
|
437
|
+
self._pending_commands.pop(cmd_id, None)
|
438
|
+
raise
|
439
|
+
|
440
|
+
async def async_send_command(self, command: str, expect_response: bool = False,
|
441
|
+
timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
|
442
|
+
"""Async command optimized for gaming"""
|
443
|
+
loop = asyncio.get_running_loop()
|
444
|
+
return await loop.run_in_executor(
|
445
|
+
None, self.send_command, command, expect_response, timeout
|
446
|
+
)
|
447
|
+
|
448
|
+
def is_connected(self) -> bool:
|
449
|
+
"""Fast connection check"""
|
450
|
+
return self._is_connected and self.serial is not None and self.serial.is_open
|
451
|
+
|
452
|
+
def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
|
453
|
+
"""Set button callback"""
|
454
|
+
self._button_callback = callback
|
172
455
|
|
173
|
-
def get_button_states(self):
|
174
|
-
|
456
|
+
def get_button_states(self) -> Dict[str, bool]:
|
457
|
+
"""Get button states with optimized lookup"""
|
458
|
+
return {
|
459
|
+
self.BUTTON_MAP[i]: bool(self._button_states & (1 << i))
|
460
|
+
for i in range(5)
|
461
|
+
}
|
175
462
|
|
176
463
|
def get_button_mask(self) -> int:
|
177
|
-
|
178
|
-
|
464
|
+
"""Direct mask access"""
|
465
|
+
return self._last_button_mask
|
179
466
|
|
180
|
-
def enable_button_monitoring(self, enable: bool = True):
|
467
|
+
def enable_button_monitoring(self, enable: bool = True) -> None:
|
468
|
+
"""Fast button monitoring toggle"""
|
181
469
|
self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
|
182
470
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
"MOUSE4": "km.catch_ms1(0)",
|
189
|
-
"MOUSE5": "km.catch_ms2(0)",
|
190
|
-
}.get(button.upper())
|
191
|
-
if command:
|
192
|
-
self.send_command(command)
|
193
|
-
else:
|
194
|
-
raise ValueError(f"Unsupported button: {button}")
|
471
|
+
# Context managers unchanged but included for completeness
|
472
|
+
async def __aenter__(self):
|
473
|
+
loop = asyncio.get_running_loop()
|
474
|
+
await loop.run_in_executor(None, self.connect)
|
475
|
+
return self
|
195
476
|
|
196
|
-
def
|
197
|
-
|
198
|
-
|
199
|
-
"RIGHT": "km.catch_mr()",
|
200
|
-
"MIDDLE": "km.catch_mm()",
|
201
|
-
"MOUSE4": "km.catch_ms1()",
|
202
|
-
"MOUSE5": "km.catch_ms2()",
|
203
|
-
}.get(button.upper())
|
204
|
-
if command:
|
205
|
-
result = self.send_command(command, expect_response=True)
|
206
|
-
try:
|
207
|
-
return int(result.strip())
|
208
|
-
except Exception:
|
209
|
-
return 0
|
210
|
-
else:
|
211
|
-
raise ValueError(f"Unsupported button: {button}")
|
212
|
-
|
213
|
-
def _listen(self, debug=False):
|
214
|
-
self._log("Started listener thread")
|
215
|
-
button_states = {i: False for i in self.button_map}
|
216
|
-
self._last_mask = 0
|
217
|
-
self._last_callback_time = {bit: 0 for bit in self.button_map}
|
218
|
-
|
219
|
-
while self._is_connected and not self._stop_event.is_set():
|
220
|
-
if self._pause_listener:
|
221
|
-
time.sleep(0.001)
|
222
|
-
continue
|
477
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
478
|
+
loop = asyncio.get_running_loop()
|
479
|
+
await loop.run_in_executor(None, self.disconnect)
|
223
480
|
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
continue
|
228
|
-
|
229
|
-
value = byte[0]
|
230
|
-
byte_str = str(byte)
|
231
|
-
|
232
|
-
if not byte_str.startswith("b'\\x"):
|
233
|
-
continue
|
234
|
-
|
235
|
-
if value != self._last_mask:
|
236
|
-
if byte_str.startswith("b'\\x00"):
|
237
|
-
for bit, name in self.button_map.items():
|
238
|
-
button_states[bit] = False
|
239
|
-
self._button_states[name] = False
|
240
|
-
if debug:
|
241
|
-
print(f"{name} -> False")
|
242
|
-
else:
|
243
|
-
for bit, name in self.button_map.items():
|
244
|
-
is_pressed = bool(value & (1 << bit))
|
245
|
-
button_states[bit] = is_pressed
|
246
|
-
self._button_states[name] = is_pressed
|
247
|
-
if debug:
|
248
|
-
print(f"{name} -> {is_pressed}")
|
249
|
-
|
250
|
-
if self._button_callback:
|
251
|
-
for bit, name in self.button_map.items():
|
252
|
-
previous = bool(self._last_mask & (1 << bit))
|
253
|
-
current = bool(value & (1 << bit))
|
254
|
-
if previous != current:
|
255
|
-
button_enum = self._button_enum_map.get(bit)
|
256
|
-
if button_enum:
|
257
|
-
self._button_callback(button_enum, current)
|
258
|
-
|
259
|
-
self._last_mask = value
|
260
|
-
|
261
|
-
if debug:
|
262
|
-
pressed = [name for bit, name in self.button_map.items() if button_states[bit]]
|
263
|
-
button_str = ", ".join(pressed) if pressed else "No buttons pressed"
|
264
|
-
self._log(f"Byte: {value} (0x{value:02X}) -> {button_str}")
|
265
|
-
|
266
|
-
except serial.SerialException as e:
|
267
|
-
if "ClearCommError failed" not in str(e):
|
268
|
-
self._log(f"Serial error during listening: {e}")
|
269
|
-
break
|
481
|
+
def __enter__(self):
|
482
|
+
self.connect()
|
483
|
+
return self
|
270
484
|
|
271
|
-
|
485
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
486
|
+
self.disconnect()
|