makcu 2.1.1__py3-none-any.whl → 2.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.
makcu/connection.py CHANGED
@@ -1,460 +1,460 @@
1
- import serial
2
- import threading
3
- import time
4
- from typing import Optional, Dict, Callable
5
- from serial.tools import list_ports
6
- from dataclasses import dataclass
7
- from collections import deque
8
- from concurrent.futures import Future
9
- import logging
10
- import asyncio
11
- from .errors import MakcuConnectionError, MakcuTimeoutError
12
- from .enums import MouseButton
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
- @dataclass
17
- class PendingCommand:
18
- command_id: int
19
- command: str
20
- future: Future
21
- timestamp: float
22
- expect_response: bool = True
23
- timeout: float = 0.1
24
-
25
- @dataclass
26
- class ParsedResponse:
27
- command_id: Optional[int]
28
- content: str
29
- is_button_data: bool = False
30
- button_mask: Optional[int] = None
31
-
32
- class SerialTransport:
33
-
34
- BAUD_CHANGE_COMMAND = bytearray([0xDE, 0xAD, 0x05, 0x00, 0xA5, 0x00, 0x09, 0x3D, 0x00])
35
- DEFAULT_TIMEOUT = 0.1
36
- MAX_RECONNECT_ATTEMPTS = 3
37
- RECONNECT_DELAY = 0.1
38
-
39
-
40
- BUTTON_MAP = (
41
- 'left', 'right', 'middle', 'mouse4', 'mouse5'
42
- )
43
-
44
- BUTTON_ENUM_MAP = (
45
- MouseButton.LEFT,
46
- MouseButton.RIGHT,
47
- MouseButton.MIDDLE,
48
- MouseButton.MOUSE4,
49
- MouseButton.MOUSE5,
50
- )
51
-
52
- def __init__(self, fallback: str = "", debug: bool = False,
53
- send_init: bool = True, auto_reconnect: bool = True,
54
- override_port: bool = False) -> None:
55
-
56
- self._fallback_com_port = fallback
57
- self.debug = debug
58
- self.send_init = send_init
59
- self.auto_reconnect = auto_reconnect
60
- self.override_port = override_port
61
-
62
-
63
- self._is_connected = False
64
- self._reconnect_attempts = 0
65
- self.port: Optional[str] = None
66
- self.baudrate = 115200
67
- self.serial: Optional[serial.Serial] = None
68
- self._current_baud: Optional[int] = None
69
-
70
-
71
- self._command_counter = 0
72
- self._pending_commands: Dict[int, PendingCommand] = {}
73
- self._command_lock = threading.Lock()
74
-
75
-
76
- self._parse_buffer = bytearray(1024)
77
- self._buffer_pos = 0
78
- self._response_queue = deque(maxlen=100)
79
-
80
-
81
- self._button_callback: Optional[Callable[[MouseButton, bool], None]] = None
82
- self._last_button_mask = 0
83
- self._button_states = 0
84
-
85
-
86
- self._stop_event = threading.Event()
87
- self._listener_thread: Optional[threading.Thread] = None
88
-
89
-
90
- self._log_messages: deque = deque(maxlen=100)
91
-
92
-
93
- self._ascii_decode_table = bytes(range(128))
94
-
95
- def _log(self, message: str, level: str = "INFO") -> None:
96
- if not self.debug and level == "DEBUG":
97
- return
98
-
99
- if self.debug:
100
-
101
- timestamp = f"{time.time():.3f}"
102
- entry = f"[{timestamp}] [{level}] {message}"
103
- self._log_messages.append(entry)
104
- print(entry, flush=True)
105
-
106
- def _generate_command_id(self) -> int:
107
- self._command_counter = (self._command_counter + 1) & 0x2710
108
- return self._command_counter
109
-
110
- def find_com_port(self) -> Optional[str]:
111
- if self.override_port:
112
- return self._fallback_com_port
113
-
114
-
115
- target_hwid = "VID:PID=1A86:55D3"
116
-
117
- for port in list_ports.comports():
118
- if target_hwid in port.hwid.upper():
119
- self._log(f"Device found: {port.device}")
120
- return port.device
121
-
122
- if self._fallback_com_port:
123
- self._log(f"Using fallback: {self._fallback_com_port}")
124
- return self._fallback_com_port
125
-
126
- return None
127
-
128
- def _parse_response_line(self, line: bytes) -> ParsedResponse:
129
-
130
- if line.startswith(b'>>> '):
131
- content = line[4:].decode('ascii', 'ignore').strip()
132
- return ParsedResponse(None, content, False)
133
-
134
- content = line.decode('ascii', 'ignore').strip()
135
- return ParsedResponse(None, content, False)
136
-
137
- def _handle_button_data(self, byte_val: int) -> None:
138
- if byte_val == self._last_button_mask:
139
- return
140
-
141
- changed_bits = byte_val ^ self._last_button_mask
142
-
143
-
144
- for bit in range(5):
145
- if changed_bits & (1 << bit):
146
- is_pressed = bool(byte_val & (1 << bit))
147
-
148
-
149
- if is_pressed:
150
- self._button_states |= (1 << bit)
151
- else:
152
- self._button_states &= ~(1 << bit)
153
-
154
- if self._button_callback and bit < len(self.BUTTON_ENUM_MAP):
155
- try:
156
- self._button_callback(self.BUTTON_ENUM_MAP[bit], is_pressed)
157
- except Exception:
158
- pass
159
-
160
- self._last_button_mask = byte_val
161
-
162
- def _process_pending_commands(self, content: str) -> None:
163
- if not content or not self._pending_commands:
164
- return
165
-
166
- with self._command_lock:
167
- if not self._pending_commands:
168
- return
169
-
170
-
171
- oldest_id = next(iter(self._pending_commands))
172
- pending = self._pending_commands[oldest_id]
173
-
174
- if pending.future.done():
175
- return
176
-
177
-
178
- if content == pending.command:
179
- if not pending.expect_response:
180
- pending.future.set_result(pending.command)
181
- del self._pending_commands[oldest_id]
182
- else:
183
- pending.future.set_result(content)
184
- del self._pending_commands[oldest_id]
185
-
186
- def _cleanup_timed_out_commands(self) -> None:
187
- if not self._pending_commands:
188
- return
189
-
190
- current_time = time.time()
191
- with self._command_lock:
192
-
193
- timed_out = [
194
- (cmd_id, pending)
195
- for cmd_id, pending in self._pending_commands.items()
196
- if current_time - pending.timestamp > pending.timeout
197
- ]
198
-
199
-
200
- for cmd_id, pending in timed_out:
201
- del self._pending_commands[cmd_id]
202
- if not pending.future.done():
203
- pending.future.set_exception(
204
- MakcuTimeoutError(f"Command #{cmd_id} timed out")
205
- )
206
-
207
-
208
- def _listen(self) -> None:
209
-
210
- read_buffer = bytearray(4096)
211
- line_buffer = bytearray(256)
212
- line_pos = 0
213
-
214
-
215
- serial_read = self.serial.read
216
- serial_in_waiting = lambda: self.serial.in_waiting
217
- is_connected = lambda: self._is_connected
218
- stop_requested = self._stop_event.is_set
219
-
220
-
221
- last_cleanup = time.time()
222
- cleanup_interval = 0.05
223
-
224
- while is_connected() and not stop_requested():
225
- try:
226
-
227
- bytes_available = serial_in_waiting()
228
- if not bytes_available:
229
- time.sleep(0.001)
230
- continue
231
-
232
-
233
- bytes_read = serial_read(min(bytes_available, 4096))
234
-
235
-
236
- for byte_val in bytes_read:
237
-
238
- if byte_val < 32 and byte_val not in (0x0D, 0x0A):
239
- self._handle_button_data(byte_val)
240
- else:
241
-
242
- if byte_val == 0x0A:
243
- if line_pos > 0:
244
-
245
- line = bytes(line_buffer[:line_pos])
246
- line_pos = 0
247
-
248
- if line:
249
- response = self._parse_response_line(line)
250
- if response.content:
251
- self._process_pending_commands(response.content)
252
- elif byte_val != 0x0D:
253
- if line_pos < 256:
254
- line_buffer[line_pos] = byte_val
255
- line_pos += 1
256
-
257
-
258
- current_time = time.time()
259
- if current_time - last_cleanup > cleanup_interval:
260
- self._cleanup_timed_out_commands()
261
- last_cleanup = current_time
262
-
263
- except serial.SerialException:
264
- if self.auto_reconnect:
265
- self._attempt_reconnect()
266
- else:
267
- break
268
- except Exception:
269
- pass
270
-
271
- def _attempt_reconnect(self) -> None:
272
- if self._reconnect_attempts >= self.MAX_RECONNECT_ATTEMPTS:
273
- self._is_connected = False
274
- return
275
-
276
- self._reconnect_attempts += 1
277
-
278
- try:
279
- if self.serial and self.serial.is_open:
280
- self.serial.close()
281
-
282
- time.sleep(self.RECONNECT_DELAY)
283
-
284
- self.port = self.find_com_port()
285
- if not self.port:
286
- raise MakcuConnectionError("Device not found")
287
-
288
-
289
- self.serial = serial.Serial(
290
- self.port,
291
- self.baudrate,
292
- timeout=0.001,
293
- write_timeout=0.01
294
- )
295
- self._change_baud_to_4M()
296
-
297
- if self.send_init:
298
- self.serial.write(b"km.buttons(1)\r")
299
- self.serial.flush()
300
-
301
- self._reconnect_attempts = 0
302
-
303
- except Exception:
304
- time.sleep(self.RECONNECT_DELAY)
305
-
306
- def _change_baud_to_4M(self) -> bool:
307
- if self.serial and self.serial.is_open:
308
- self.serial.write(self.BAUD_CHANGE_COMMAND)
309
- self.serial.flush()
310
- time.sleep(0.02)
311
- self.serial.baudrate = 4000000
312
- self._current_baud = 4000000
313
- return True
314
- return False
315
-
316
- def connect(self) -> None:
317
- if self._is_connected:
318
- return
319
-
320
- if not self.override_port:
321
- self.port = self.find_com_port()
322
- else:
323
- self.port = self._fallback_com_port
324
-
325
- if not self.port:
326
- raise MakcuConnectionError("Makcu device not found")
327
-
328
- try:
329
-
330
- self.serial = serial.Serial(
331
- self.port,
332
- 115200,
333
- timeout=0.001,
334
- write_timeout=0.01,
335
- xonxoff=False,
336
- rtscts=False,
337
- dsrdtr=False
338
- )
339
-
340
- if not self._change_baud_to_4M():
341
- raise MakcuConnectionError("Failed to switch to 4M baud")
342
-
343
- self._is_connected = True
344
- self._reconnect_attempts = 0
345
-
346
- if self.send_init:
347
- self.serial.write(b"km.buttons(1)\r")
348
- self.serial.flush()
349
-
350
-
351
- self._stop_event.clear()
352
- self._listener_thread = threading.Thread(
353
- target=self._listen,
354
- daemon=True,
355
- name="MakcuListener"
356
- )
357
- self._listener_thread.start()
358
-
359
- except Exception as e:
360
- if self.serial:
361
- self.serial.close()
362
- raise MakcuConnectionError(f"Failed to connect: {e}")
363
-
364
- def disconnect(self) -> None:
365
- self._is_connected = False
366
-
367
- if self.send_init:
368
- self._stop_event.set()
369
- if self._listener_thread and self._listener_thread.is_alive():
370
- self._listener_thread.join(timeout=0.1)
371
-
372
- with self._command_lock:
373
- for pending in self._pending_commands.values():
374
- if not pending.future.done():
375
- pending.future.cancel()
376
- self._pending_commands.clear()
377
-
378
- if self.serial and self.serial.is_open:
379
- self.serial.close()
380
- self.serial = None
381
-
382
- def send_command(self, command: str, expect_response: bool = False,
383
- timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
384
- if not self._is_connected or not self.serial or not self.serial.is_open:
385
- raise MakcuConnectionError("Not connected")
386
-
387
- if not expect_response:
388
- self.serial.write(f"{command}\r\n".encode('ascii'))
389
- self.serial.flush()
390
- return command
391
-
392
- cmd_id = self._generate_command_id()
393
- tagged_command = f"{command}#{cmd_id}"
394
-
395
- future = Future()
396
-
397
- with self._command_lock:
398
- self._pending_commands[cmd_id] = PendingCommand(
399
- command_id=cmd_id,
400
- command=command,
401
- future=future,
402
- timestamp=time.time(),
403
- expect_response=expect_response,
404
- timeout=timeout
405
- )
406
-
407
- try:
408
- self.serial.write(f"{tagged_command}\r\n".encode('ascii'))
409
- self.serial.flush()
410
-
411
- result = future.result(timeout=timeout)
412
- return result.split('#')[0] if '#' in result else result
413
-
414
- except TimeoutError:
415
- raise MakcuTimeoutError(f"Command timed out: {command}")
416
- except Exception as e:
417
- with self._command_lock:
418
- self._pending_commands.pop(cmd_id, None)
419
- raise
420
-
421
- async def async_send_command(self, command: str, expect_response: bool = False,
422
- timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
423
- loop = asyncio.get_running_loop()
424
- return await loop.run_in_executor(
425
- None, self.send_command, command, expect_response, timeout
426
- )
427
-
428
- def is_connected(self) -> bool:
429
- return self._is_connected and self.serial is not None and self.serial.is_open
430
-
431
- def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
432
- self._button_callback = callback
433
-
434
- def get_button_states(self) -> Dict[str, bool]:
435
- return {
436
- self.BUTTON_MAP[i]: bool(self._button_states & (1 << i))
437
- for i in range(5)
438
- }
439
-
440
- def get_button_mask(self) -> int:
441
- return self._last_button_mask
442
-
443
- def enable_button_monitoring(self, enable: bool = True) -> None:
444
- self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
445
-
446
- async def __aenter__(self):
447
- loop = asyncio.get_running_loop()
448
- await loop.run_in_executor(None, self.connect)
449
- return self
450
-
451
- async def __aexit__(self, exc_type, exc_val, exc_tb):
452
- loop = asyncio.get_running_loop()
453
- await loop.run_in_executor(None, self.disconnect)
454
-
455
- def __enter__(self):
456
- self.connect()
457
- return self
458
-
459
- def __exit__(self, exc_type, exc_val, exc_tb):
1
+ import serial
2
+ import threading
3
+ import time
4
+ from typing import Optional, Dict, Callable
5
+ from serial.tools import list_ports
6
+ from dataclasses import dataclass
7
+ from collections import deque
8
+ from concurrent.futures import Future
9
+ import logging
10
+ import asyncio
11
+ from .errors import MakcuConnectionError, MakcuTimeoutError
12
+ from .enums import MouseButton
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ @dataclass
17
+ class PendingCommand:
18
+ command_id: int
19
+ command: str
20
+ future: Future
21
+ timestamp: float
22
+ expect_response: bool = True
23
+ timeout: float = 0.1
24
+
25
+ @dataclass
26
+ class ParsedResponse:
27
+ command_id: Optional[int]
28
+ content: str
29
+ is_button_data: bool = False
30
+ button_mask: Optional[int] = None
31
+
32
+ class SerialTransport:
33
+
34
+ BAUD_CHANGE_COMMAND = bytearray([0xDE, 0xAD, 0x05, 0x00, 0xA5, 0x00, 0x09, 0x3D, 0x00])
35
+ DEFAULT_TIMEOUT = 0.1
36
+ MAX_RECONNECT_ATTEMPTS = 3
37
+ RECONNECT_DELAY = 0.1
38
+
39
+
40
+ BUTTON_MAP = (
41
+ 'left', 'right', 'middle', 'mouse4', 'mouse5'
42
+ )
43
+
44
+ BUTTON_ENUM_MAP = (
45
+ MouseButton.LEFT,
46
+ MouseButton.RIGHT,
47
+ MouseButton.MIDDLE,
48
+ MouseButton.MOUSE4,
49
+ MouseButton.MOUSE5,
50
+ )
51
+
52
+ def __init__(self, fallback: str = "", debug: bool = False,
53
+ send_init: bool = True, auto_reconnect: bool = True,
54
+ override_port: bool = False) -> None:
55
+
56
+ self._fallback_com_port = fallback
57
+ self.debug = debug
58
+ self.send_init = send_init
59
+ self.auto_reconnect = auto_reconnect
60
+ self.override_port = override_port
61
+
62
+
63
+ self._is_connected = False
64
+ self._reconnect_attempts = 0
65
+ self.port: Optional[str] = None
66
+ self.baudrate = 115200
67
+ self.serial: Optional[serial.Serial] = None
68
+ self._current_baud: Optional[int] = None
69
+
70
+
71
+ self._command_counter = 0
72
+ self._pending_commands: Dict[int, PendingCommand] = {}
73
+ self._command_lock = threading.Lock()
74
+
75
+
76
+ self._parse_buffer = bytearray(1024)
77
+ self._buffer_pos = 0
78
+ self._response_queue = deque(maxlen=100)
79
+
80
+
81
+ self._button_callback: Optional[Callable[[MouseButton, bool], None]] = None
82
+ self._last_button_mask = 0
83
+ self._button_states = 0
84
+
85
+
86
+ self._stop_event = threading.Event()
87
+ self._listener_thread: Optional[threading.Thread] = None
88
+
89
+
90
+ self._log_messages: deque = deque(maxlen=100)
91
+
92
+
93
+ self._ascii_decode_table = bytes(range(128))
94
+
95
+ def _log(self, message: str, level: str = "INFO") -> None:
96
+ if not self.debug and level == "DEBUG":
97
+ return
98
+
99
+ if self.debug:
100
+
101
+ timestamp = f"{time.time():.3f}"
102
+ entry = f"[{timestamp}] [{level}] {message}"
103
+ self._log_messages.append(entry)
104
+ print(entry, flush=True)
105
+
106
+ def _generate_command_id(self) -> int:
107
+ self._command_counter = (self._command_counter + 1) & 0x2710
108
+ return self._command_counter
109
+
110
+ def find_com_port(self) -> Optional[str]:
111
+ if self.override_port:
112
+ return self._fallback_com_port
113
+
114
+
115
+ target_hwid = "VID:PID=1A86:55D3"
116
+
117
+ for port in list_ports.comports():
118
+ if target_hwid in port.hwid.upper():
119
+ self._log(f"Device found: {port.device}")
120
+ return port.device
121
+
122
+ if self._fallback_com_port:
123
+ self._log(f"Using fallback: {self._fallback_com_port}")
124
+ return self._fallback_com_port
125
+
126
+ return None
127
+
128
+ def _parse_response_line(self, line: bytes) -> ParsedResponse:
129
+
130
+ if line.startswith(b'>>> '):
131
+ content = line[4:].decode('ascii', 'ignore').strip()
132
+ return ParsedResponse(None, content, False)
133
+
134
+ content = line.decode('ascii', 'ignore').strip()
135
+ return ParsedResponse(None, content, False)
136
+
137
+ def _handle_button_data(self, byte_val: int) -> None:
138
+ if byte_val == self._last_button_mask:
139
+ return
140
+
141
+ changed_bits = byte_val ^ self._last_button_mask
142
+
143
+
144
+ for bit in range(5):
145
+ if changed_bits & (1 << bit):
146
+ is_pressed = bool(byte_val & (1 << bit))
147
+
148
+
149
+ if is_pressed:
150
+ self._button_states |= (1 << bit)
151
+ else:
152
+ self._button_states &= ~(1 << bit)
153
+
154
+ if self._button_callback and bit < len(self.BUTTON_ENUM_MAP):
155
+ try:
156
+ self._button_callback(self.BUTTON_ENUM_MAP[bit], is_pressed)
157
+ except Exception:
158
+ pass
159
+
160
+ self._last_button_mask = byte_val
161
+
162
+ def _process_pending_commands(self, content: str) -> None:
163
+ if not content or not self._pending_commands:
164
+ return
165
+
166
+ with self._command_lock:
167
+ if not self._pending_commands:
168
+ return
169
+
170
+
171
+ oldest_id = next(iter(self._pending_commands))
172
+ pending = self._pending_commands[oldest_id]
173
+
174
+ if pending.future.done():
175
+ return
176
+
177
+
178
+ if content == pending.command:
179
+ if not pending.expect_response:
180
+ pending.future.set_result(pending.command)
181
+ del self._pending_commands[oldest_id]
182
+ else:
183
+ pending.future.set_result(content)
184
+ del self._pending_commands[oldest_id]
185
+
186
+ def _cleanup_timed_out_commands(self) -> None:
187
+ if not self._pending_commands:
188
+ return
189
+
190
+ current_time = time.time()
191
+ with self._command_lock:
192
+
193
+ timed_out = [
194
+ (cmd_id, pending)
195
+ for cmd_id, pending in self._pending_commands.items()
196
+ if current_time - pending.timestamp > pending.timeout
197
+ ]
198
+
199
+
200
+ for cmd_id, pending in timed_out:
201
+ del self._pending_commands[cmd_id]
202
+ if not pending.future.done():
203
+ pending.future.set_exception(
204
+ MakcuTimeoutError(f"Command #{cmd_id} timed out")
205
+ )
206
+
207
+
208
+ def _listen(self) -> None:
209
+
210
+ read_buffer = bytearray(4096)
211
+ line_buffer = bytearray(256)
212
+ line_pos = 0
213
+
214
+
215
+ serial_read = self.serial.read
216
+ serial_in_waiting = lambda: self.serial.in_waiting
217
+ is_connected = lambda: self._is_connected
218
+ stop_requested = self._stop_event.is_set
219
+
220
+
221
+ last_cleanup = time.time()
222
+ cleanup_interval = 0.05
223
+
224
+ while is_connected() and not stop_requested():
225
+ try:
226
+
227
+ bytes_available = serial_in_waiting()
228
+ if not bytes_available:
229
+ time.sleep(0.001)
230
+ continue
231
+
232
+
233
+ bytes_read = serial_read(min(bytes_available, 4096))
234
+
235
+
236
+ for byte_val in bytes_read:
237
+
238
+ if byte_val < 32 and byte_val not in (0x0D, 0x0A):
239
+ self._handle_button_data(byte_val)
240
+ else:
241
+
242
+ if byte_val == 0x0A:
243
+ if line_pos > 0:
244
+
245
+ line = bytes(line_buffer[:line_pos])
246
+ line_pos = 0
247
+
248
+ if line:
249
+ response = self._parse_response_line(line)
250
+ if response.content:
251
+ self._process_pending_commands(response.content)
252
+ elif byte_val != 0x0D:
253
+ if line_pos < 256:
254
+ line_buffer[line_pos] = byte_val
255
+ line_pos += 1
256
+
257
+
258
+ current_time = time.time()
259
+ if current_time - last_cleanup > cleanup_interval:
260
+ self._cleanup_timed_out_commands()
261
+ last_cleanup = current_time
262
+
263
+ except serial.SerialException:
264
+ if self.auto_reconnect:
265
+ self._attempt_reconnect()
266
+ else:
267
+ break
268
+ except Exception:
269
+ pass
270
+
271
+ def _attempt_reconnect(self) -> None:
272
+ if self._reconnect_attempts >= self.MAX_RECONNECT_ATTEMPTS:
273
+ self._is_connected = False
274
+ return
275
+
276
+ self._reconnect_attempts += 1
277
+
278
+ try:
279
+ if self.serial and self.serial.is_open:
280
+ self.serial.close()
281
+
282
+ time.sleep(self.RECONNECT_DELAY)
283
+
284
+ self.port = self.find_com_port()
285
+ if not self.port:
286
+ raise MakcuConnectionError("Device not found")
287
+
288
+
289
+ self.serial = serial.Serial(
290
+ self.port,
291
+ self.baudrate,
292
+ timeout=0.001,
293
+ write_timeout=0.01
294
+ )
295
+ self._change_baud_to_4M()
296
+
297
+ if self.send_init:
298
+ self.serial.write(b"km.buttons(1)\r")
299
+ self.serial.flush()
300
+
301
+ self._reconnect_attempts = 0
302
+
303
+ except Exception:
304
+ time.sleep(self.RECONNECT_DELAY)
305
+
306
+ def _change_baud_to_4M(self) -> bool:
307
+ if self.serial and self.serial.is_open:
308
+ self.serial.write(self.BAUD_CHANGE_COMMAND)
309
+ self.serial.flush()
310
+ time.sleep(0.02)
311
+ self.serial.baudrate = 4000000
312
+ self._current_baud = 4000000
313
+ return True
314
+ return False
315
+
316
+ def connect(self) -> None:
317
+ if self._is_connected:
318
+ return
319
+
320
+ if not self.override_port:
321
+ self.port = self.find_com_port()
322
+ else:
323
+ self.port = self._fallback_com_port
324
+
325
+ if not self.port:
326
+ raise MakcuConnectionError("Makcu device not found")
327
+
328
+ try:
329
+
330
+ self.serial = serial.Serial(
331
+ self.port,
332
+ 115200,
333
+ timeout=0.001,
334
+ write_timeout=0.01,
335
+ xonxoff=False,
336
+ rtscts=False,
337
+ dsrdtr=False
338
+ )
339
+
340
+ if not self._change_baud_to_4M():
341
+ raise MakcuConnectionError("Failed to switch to 4M baud")
342
+
343
+ self._is_connected = True
344
+ self._reconnect_attempts = 0
345
+
346
+ if self.send_init:
347
+ self.serial.write(b"km.buttons(1)\r")
348
+ self.serial.flush()
349
+
350
+
351
+ self._stop_event.clear()
352
+ self._listener_thread = threading.Thread(
353
+ target=self._listen,
354
+ daemon=True,
355
+ name="MakcuListener"
356
+ )
357
+ self._listener_thread.start()
358
+
359
+ except Exception as e:
360
+ if self.serial:
361
+ self.serial.close()
362
+ raise MakcuConnectionError(f"Failed to connect: {e}")
363
+
364
+ def disconnect(self) -> None:
365
+ self._is_connected = False
366
+
367
+ if self.send_init:
368
+ self._stop_event.set()
369
+ if self._listener_thread and self._listener_thread.is_alive():
370
+ self._listener_thread.join(timeout=0.1)
371
+
372
+ with self._command_lock:
373
+ for pending in self._pending_commands.values():
374
+ if not pending.future.done():
375
+ pending.future.cancel()
376
+ self._pending_commands.clear()
377
+
378
+ if self.serial and self.serial.is_open:
379
+ self.serial.close()
380
+ self.serial = None
381
+
382
+ def send_command(self, command: str, expect_response: bool = False,
383
+ timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
384
+ if not self._is_connected or not self.serial or not self.serial.is_open:
385
+ raise MakcuConnectionError("Not connected")
386
+
387
+ if not expect_response:
388
+ self.serial.write(f"{command}\r\n".encode('ascii'))
389
+ self.serial.flush()
390
+ return command
391
+
392
+ cmd_id = self._generate_command_id()
393
+ tagged_command = f"{command}#{cmd_id}"
394
+
395
+ future = Future()
396
+
397
+ with self._command_lock:
398
+ self._pending_commands[cmd_id] = PendingCommand(
399
+ command_id=cmd_id,
400
+ command=command,
401
+ future=future,
402
+ timestamp=time.time(),
403
+ expect_response=expect_response,
404
+ timeout=timeout
405
+ )
406
+
407
+ try:
408
+ self.serial.write(f"{tagged_command}\r\n".encode('ascii'))
409
+ self.serial.flush()
410
+
411
+ result = future.result(timeout=timeout)
412
+ return result.split('#')[0] if '#' in result else result
413
+
414
+ except TimeoutError:
415
+ raise MakcuTimeoutError(f"Command timed out: {command}")
416
+ except Exception as e:
417
+ with self._command_lock:
418
+ self._pending_commands.pop(cmd_id, None)
419
+ raise
420
+
421
+ async def async_send_command(self, command: str, expect_response: bool = False,
422
+ timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
423
+ loop = asyncio.get_running_loop()
424
+ return await loop.run_in_executor(
425
+ None, self.send_command, command, expect_response, timeout
426
+ )
427
+
428
+ def is_connected(self) -> bool:
429
+ return self._is_connected and self.serial is not None and self.serial.is_open
430
+
431
+ def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
432
+ self._button_callback = callback
433
+
434
+ def get_button_states(self) -> Dict[str, bool]:
435
+ return {
436
+ self.BUTTON_MAP[i]: bool(self._button_states & (1 << i))
437
+ for i in range(5)
438
+ }
439
+
440
+ def get_button_mask(self) -> int:
441
+ return self._last_button_mask
442
+
443
+ def enable_button_monitoring(self, enable: bool = True) -> None:
444
+ self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
445
+
446
+ async def __aenter__(self):
447
+ loop = asyncio.get_running_loop()
448
+ await loop.run_in_executor(None, self.connect)
449
+ return self
450
+
451
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
452
+ loop = asyncio.get_running_loop()
453
+ await loop.run_in_executor(None, self.disconnect)
454
+
455
+ def __enter__(self):
456
+ self.connect()
457
+ return self
458
+
459
+ def __exit__(self, exc_type, exc_val, exc_tb):
460
460
  self.disconnect()