makcu 0.1.4__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 +427 -231
- makcu/controller.py +348 -86
- makcu/makcu.pyi +13 -0
- makcu/mouse.py +219 -116
- makcu/py.typed +2 -0
- makcu/test_suite.py +112 -34
- makcu-0.2.0.dist-info/METADATA +1141 -0
- makcu-0.2.0.dist-info/RECORD +16 -0
- makcu-0.1.4.dist-info/METADATA +0 -274
- makcu-0.1.4.dist-info/RECORD +0 -14
- {makcu-0.1.4.dist-info → makcu-0.2.0.dist-info}/WHEEL +0 -0
- {makcu-0.1.4.dist-info → makcu-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {makcu-0.1.4.dist-info → makcu-0.2.0.dist-info}/top_level.txt +0 -0
makcu/connection.py
CHANGED
@@ -1,290 +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
|
-
|
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
|
10
27
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
35
|
+
|
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
|
+
)
|
18
56
|
|
19
|
-
def __init__(self, fallback, debug=False,
|
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.
|
31
|
-
self.
|
69
|
+
self._reconnect_attempts = 0
|
70
|
+
self.port: Optional[str] = None
|
71
|
+
self.baudrate = 115200
|
72
|
+
self.serial: Optional[serial.Serial] = None
|
73
|
+
self._current_baud: Optional[int] = None
|
32
74
|
|
33
|
-
|
34
|
-
self.
|
35
|
-
self.
|
36
|
-
self._response_timeout = 0.01
|
75
|
+
# Command tracking with pre-allocated buffer
|
76
|
+
self._command_counter = 0
|
77
|
+
self._pending_commands: Dict[int, PendingCommand] = {}
|
37
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
|
38
99
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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)
|
46
111
|
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
50
116
|
|
51
|
-
|
52
|
-
|
53
|
-
self.
|
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
|
+
|
125
|
+
for port in list_ports.comports():
|
126
|
+
if target_hwid in port.hwid.upper():
|
127
|
+
self._log(f"Device found: {port.device}")
|
128
|
+
return port.device
|
129
|
+
|
130
|
+
if self._fallback_com_port:
|
131
|
+
self._log(f"Using fallback: {self._fallback_com_port}")
|
132
|
+
return self._fallback_com_port
|
133
|
+
|
134
|
+
return None
|
54
135
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
cleaned_lines.append(actual_response)
|
77
|
-
continue
|
78
|
-
if line == command_clean:
|
79
|
-
continue
|
80
|
-
cleaned_lines.append(line)
|
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))
|
81
157
|
|
82
|
-
|
83
|
-
if
|
84
|
-
self.
|
85
|
-
|
86
|
-
|
87
|
-
return ""
|
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)
|
88
163
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
93
171
|
|
94
|
-
def
|
95
|
-
|
172
|
+
def _process_pending_commands(self, content: str) -> None:
|
173
|
+
"""Optimized command processing"""
|
174
|
+
if not content or not self._pending_commands:
|
175
|
+
return
|
96
176
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
self.
|
103
|
-
|
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]
|
104
184
|
|
105
|
-
|
106
|
-
|
185
|
+
if pending.future.done():
|
186
|
+
return
|
107
187
|
|
108
|
-
|
109
|
-
if
|
110
|
-
|
111
|
-
|
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]
|
112
196
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
+
)
|
119
218
|
|
120
|
-
def
|
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
|
282
|
+
|
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
|
+
|
121
291
|
try:
|
122
|
-
self.
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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)
|
127
318
|
|
128
|
-
def _change_baud_to_4M(self):
|
319
|
+
def _change_baud_to_4M(self) -> bool:
|
320
|
+
"""Optimized baud rate change"""
|
129
321
|
if self.serial and self.serial.is_open:
|
130
|
-
self.
|
131
|
-
self.serial.write(self.baud_change_command)
|
322
|
+
self.serial.write(self.BAUD_CHANGE_COMMAND)
|
132
323
|
self.serial.flush()
|
133
|
-
time.sleep(0.05
|
324
|
+
time.sleep(0.02) # Reduced from 0.05
|
134
325
|
self.serial.baudrate = 4000000
|
135
326
|
self._current_baud = 4000000
|
136
|
-
self._log("Switched to 4M baud successfully.")
|
137
327
|
return True
|
138
328
|
return False
|
139
329
|
|
140
|
-
def connect(self):
|
330
|
+
def connect(self) -> None:
|
331
|
+
"""Optimized connection with minimal overhead"""
|
141
332
|
if self._is_connected:
|
142
|
-
self._log("Already connected.")
|
143
333
|
return
|
144
|
-
|
145
|
-
if not self.
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
self.
|
151
|
-
|
152
|
-
|
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:
|
153
362
|
self.serial.write(b"km.buttons(1)\r")
|
154
363
|
self.serial.flush()
|
155
|
-
|
156
|
-
|
364
|
+
|
365
|
+
# Start high-priority listener thread
|
157
366
|
self._stop_event.clear()
|
158
|
-
self._listener_thread = threading.Thread(
|
367
|
+
self._listener_thread = threading.Thread(
|
368
|
+
target=self._listen,
|
369
|
+
daemon=True,
|
370
|
+
name="MakcuListener"
|
371
|
+
)
|
159
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}")
|
160
378
|
|
161
|
-
def disconnect(self):
|
379
|
+
def disconnect(self) -> None:
|
380
|
+
"""Fast disconnect"""
|
381
|
+
self._is_connected = False
|
382
|
+
|
162
383
|
if self.send_init:
|
163
384
|
self._stop_event.set()
|
164
|
-
if self._listener_thread:
|
165
|
-
self._listener_thread.join()
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
self.
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
175
397
|
|
176
|
-
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"""
|
177
401
|
if not self._is_connected or not self.serial or not self.serial.is_open:
|
178
|
-
raise MakcuConnectionError("
|
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()
|
179
415
|
|
180
416
|
with self._command_lock:
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
def get_button_mask(self) -> int:
|
205
|
-
return self._last_mask
|
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
|
206
439
|
|
207
|
-
def
|
208
|
-
|
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
|
+
)
|
209
447
|
|
210
|
-
def
|
211
|
-
|
212
|
-
|
213
|
-
"RIGHT": "km.catch_mr(0)",
|
214
|
-
"MIDDLE": "km.catch_mm(0)",
|
215
|
-
"MOUSE4": "km.catch_ms1(0)",
|
216
|
-
"MOUSE5": "km.catch_ms2(0)",
|
217
|
-
}.get(button.upper())
|
218
|
-
if command:
|
219
|
-
self.send_command(command)
|
220
|
-
else:
|
221
|
-
raise ValueError(f"Unsupported button: {button}")
|
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
|
222
451
|
|
223
|
-
def
|
224
|
-
|
452
|
+
def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
|
453
|
+
"""Set button callback"""
|
454
|
+
self._button_callback = callback
|
225
455
|
|
226
|
-
def
|
227
|
-
|
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
|
+
}
|
228
462
|
|
229
|
-
def
|
230
|
-
|
231
|
-
self.
|
232
|
-
ascii_buffer = bytearray()
|
233
|
-
response_lines = []
|
463
|
+
def get_button_mask(self) -> int:
|
464
|
+
"""Direct mask access"""
|
465
|
+
return self._last_button_mask
|
234
466
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
if not data:
|
239
|
-
continue
|
467
|
+
def enable_button_monitoring(self, enable: bool = True) -> None:
|
468
|
+
"""Fast button monitoring toggle"""
|
469
|
+
self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
|
240
470
|
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
for bit, name in self.button_map.items():
|
247
|
-
if changed_bits & (1 << bit):
|
248
|
-
is_pressed = bool(byte_val & (1 << bit))
|
249
|
-
self._button_states[name] = is_pressed
|
250
|
-
button_enum = self._button_enum_map.get(bit)
|
251
|
-
if button_enum and self._button_callback:
|
252
|
-
self._button_callback(button_enum, is_pressed)
|
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
|
253
476
|
|
254
|
-
|
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)
|
255
480
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
self._log(f"Mask: 0x{byte_val:02X} -> {button_str}")
|
260
|
-
elif self._is_ascii_data(byte_val):
|
261
|
-
if self._waiting_for_response:
|
262
|
-
ascii_buffer.append(byte_val)
|
263
|
-
if byte_val == 0x0A:
|
264
|
-
try:
|
265
|
-
line = ascii_buffer.decode('ascii', errors='ignore').strip()
|
266
|
-
ascii_buffer.clear()
|
267
|
-
|
268
|
-
if line:
|
269
|
-
response_lines.append(line)
|
270
|
-
|
271
|
-
if (len(response_lines) >= 2 or
|
272
|
-
(len(response_lines) == 1 and not line.startswith('>>> '))):
|
273
|
-
|
274
|
-
full_response = '\n'.join(response_lines)
|
275
|
-
self._response_buffer = full_response
|
276
|
-
self._response_ready.set()
|
277
|
-
response_lines.clear()
|
278
|
-
|
279
|
-
except Exception as e:
|
280
|
-
self._log(f"Error decoding ASCII response: {e}")
|
281
|
-
ascii_buffer.clear()
|
282
|
-
response_lines.clear()
|
481
|
+
def __enter__(self):
|
482
|
+
self.connect()
|
483
|
+
return self
|
283
484
|
|
284
|
-
|
285
|
-
|
286
|
-
self._log(f"Serial error during listening: {e}")
|
287
|
-
break
|
288
|
-
except Exception as e:
|
289
|
-
self._log(f"Unexpected error in listener: {e}")
|
290
|
-
self._log("Listener thread exiting")
|
485
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
486
|
+
self.disconnect()
|