makcu 2.1.3__py3-none-any.whl → 2.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/connection.py CHANGED
@@ -59,6 +59,19 @@ class SerialTransport:
59
59
  self.auto_reconnect = auto_reconnect
60
60
  self.override_port = override_port
61
61
 
62
+ if not hasattr(SerialTransport, '_thread_counter'):
63
+ SerialTransport._thread_counter = 0
64
+ SerialTransport._thread_map = {}
65
+
66
+ # Log version info during initialization
67
+ try:
68
+ from makcu import __version__
69
+ version = __version__
70
+ self._log(f"Makcu version: {version}")
71
+ except ImportError:
72
+ self._log("Makcu version info not available")
73
+
74
+ self._log(f"Initializing SerialTransport with params: fallback='{fallback}', debug={debug}, send_init={send_init}, auto_reconnect={auto_reconnect}, override_port={override_port}")
62
75
 
63
76
  self._is_connected = False
64
77
  self._reconnect_attempts = 0
@@ -85,48 +98,62 @@ class SerialTransport:
85
98
 
86
99
  self._stop_event = threading.Event()
87
100
  self._listener_thread: Optional[threading.Thread] = None
88
-
101
+
89
102
 
90
- self._log_messages: deque = deque(maxlen=100)
103
+ self._ascii_decode_table = bytes(range(128))
91
104
 
105
+ self._log("SerialTransport initialization completed")
92
106
 
93
- self._ascii_decode_table = bytes(range(128))
94
107
 
95
108
  def _log(self, message: str, level: str = "INFO") -> None:
96
- if not self.debug and level == "DEBUG":
109
+ if not self.debug:
97
110
  return
98
111
 
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)
112
+ timestamp = time.strftime("%H:%M:%S", time.localtime())
113
+ thread_id = threading.get_ident()
114
+
115
+ # Map thread ID to a simple number
116
+ if thread_id not in SerialTransport._thread_map:
117
+ SerialTransport._thread_counter += 1
118
+ SerialTransport._thread_map[thread_id] = SerialTransport._thread_counter
119
+
120
+ thread_num = SerialTransport._thread_map[thread_id]
121
+ entry = f"[{timestamp}] [T:{thread_num}] [{level}] {message}"
122
+ print(entry, flush=True)
105
123
 
106
124
  def _generate_command_id(self) -> int:
125
+ old_counter = self._command_counter
107
126
  self._command_counter = (self._command_counter + 1) & 0x2710
108
127
  return self._command_counter
109
128
 
110
129
  def find_com_port(self) -> Optional[str]:
130
+ self._log("Starting COM port discovery")
131
+
111
132
  if self.override_port:
133
+ self._log(f"Override port enabled, using: {self._fallback_com_port}")
112
134
  return self._fallback_com_port
113
135
 
136
+ all_ports = list_ports.comports()
137
+ self._log(f"Found {len(all_ports)} COM ports total")
114
138
 
115
139
  target_hwid = "VID:PID=1A86:55D3"
116
140
 
117
- for port in list_ports.comports():
141
+ for i, port in enumerate(all_ports):
142
+ self._log(f"Port {i}: {port.device} - HWID: {port.hwid}")
118
143
  if target_hwid in port.hwid.upper():
119
- self._log(f"Device found: {port.device}")
144
+ self._log(f"Target device found on port: {port.device}")
120
145
  return port.device
121
146
 
147
+ self._log("Target device not found in COM port scan")
148
+
122
149
  if self._fallback_com_port:
123
- self._log(f"Using fallback: {self._fallback_com_port}")
150
+ self._log(f"Using fallback COM port: {self._fallback_com_port}")
124
151
  return self._fallback_com_port
125
152
 
153
+ self._log("No fallback port specified, returning None")
126
154
  return None
127
155
 
128
156
  def _parse_response_line(self, line: bytes) -> ParsedResponse:
129
-
130
157
  if line.startswith(b'>>> '):
131
158
  content = line[4:].decode('ascii', 'ignore').strip()
132
159
  return ParsedResponse(None, content, False)
@@ -139,12 +166,14 @@ class SerialTransport:
139
166
  return
140
167
 
141
168
  changed_bits = byte_val ^ self._last_button_mask
142
-
169
+ self._log(f"Button state changed: 0x{self._last_button_mask:02X} -> 0x{byte_val:02X}")
143
170
 
144
171
  for bit in range(5):
145
172
  if changed_bits & (1 << bit):
146
173
  is_pressed = bool(byte_val & (1 << bit))
174
+ button_name = self.BUTTON_MAP[bit] if bit < len(self.BUTTON_MAP) else f"bit{bit}"
147
175
 
176
+ self._log(f"Button {button_name}: {'PRESSED' if is_pressed else 'RELEASED'}")
148
177
 
149
178
  if is_pressed:
150
179
  self._button_states |= (1 << bit)
@@ -154,8 +183,8 @@ class SerialTransport:
154
183
  if self._button_callback and bit < len(self.BUTTON_ENUM_MAP):
155
184
  try:
156
185
  self._button_callback(self.BUTTON_ENUM_MAP[bit], is_pressed)
157
- except Exception:
158
- pass
186
+ except Exception as e:
187
+ self._log(f"Button callback failed: {e}", "ERROR")
159
188
 
160
189
  self._last_button_mask = byte_val
161
190
 
@@ -166,7 +195,6 @@ class SerialTransport:
166
195
  with self._command_lock:
167
196
  if not self._pending_commands:
168
197
  return
169
-
170
198
 
171
199
  oldest_id = next(iter(self._pending_commands))
172
200
  pending = self._pending_commands[oldest_id]
@@ -174,7 +202,6 @@ class SerialTransport:
174
202
  if pending.future.done():
175
203
  return
176
204
 
177
-
178
205
  if content == pending.command:
179
206
  if not pending.expect_response:
180
207
  pending.future.set_result(pending.command)
@@ -188,24 +215,25 @@ class SerialTransport:
188
215
  return
189
216
 
190
217
  current_time = time.time()
218
+
191
219
  with self._command_lock:
192
-
193
220
  timed_out = [
194
221
  (cmd_id, pending)
195
222
  for cmd_id, pending in self._pending_commands.items()
196
223
  if current_time - pending.timestamp > pending.timeout
197
224
  ]
198
-
199
225
 
200
226
  for cmd_id, pending in timed_out:
227
+ age = current_time - pending.timestamp
228
+ self._log(f"Command '{pending.command}' timed out after {age:.3f}s", "ERROR")
201
229
  del self._pending_commands[cmd_id]
202
230
  if not pending.future.done():
203
231
  pending.future.set_exception(
204
232
  MakcuTimeoutError(f"Command #{cmd_id} timed out")
205
233
  )
206
234
 
207
-
208
235
  def _listen(self) -> None:
236
+ self._log("Starting listener thread")
209
237
 
210
238
  read_buffer = bytearray(4096)
211
239
  line_buffer = bytearray(256)
@@ -223,25 +251,20 @@ class SerialTransport:
223
251
 
224
252
  while is_connected() and not stop_requested():
225
253
  try:
226
-
227
254
  bytes_available = serial_in_waiting()
228
255
  if not bytes_available:
229
256
  time.sleep(0.001)
230
257
  continue
231
258
 
232
-
233
259
  bytes_read = serial_read(min(bytes_available, 4096))
234
-
235
260
 
236
261
  for byte_val in bytes_read:
237
-
238
262
  if byte_val < 32 and byte_val not in (0x0D, 0x0A):
263
+ # Button data
239
264
  self._handle_button_data(byte_val)
240
265
  else:
241
-
242
- if byte_val == 0x0A:
266
+ if byte_val == 0x0A: # LF
243
267
  if line_pos > 0:
244
-
245
268
  line = bytes(line_buffer[:line_pos])
246
269
  line_pos = 0
247
270
 
@@ -249,27 +272,32 @@ class SerialTransport:
249
272
  response = self._parse_response_line(line)
250
273
  if response.content:
251
274
  self._process_pending_commands(response.content)
252
- elif byte_val != 0x0D:
275
+ elif byte_val != 0x0D: # Not CR
253
276
  if line_pos < 256:
254
277
  line_buffer[line_pos] = byte_val
255
278
  line_pos += 1
256
-
257
279
 
258
280
  current_time = time.time()
259
281
  if current_time - last_cleanup > cleanup_interval:
260
282
  self._cleanup_timed_out_commands()
261
283
  last_cleanup = current_time
262
284
 
263
- except serial.SerialException:
285
+ except serial.SerialException as e:
286
+ self._log(f"Serial exception in listener: {e}", "ERROR")
264
287
  if self.auto_reconnect:
265
288
  self._attempt_reconnect()
266
289
  else:
267
290
  break
268
- except Exception:
269
- pass
291
+ except Exception as e:
292
+ self._log(f"Unexpected exception in listener: {e}", "ERROR")
293
+
294
+ self._log("Listener thread ending")
270
295
 
271
296
  def _attempt_reconnect(self) -> None:
297
+ self._log(f"Attempting reconnect #{self._reconnect_attempts + 1}/{self.MAX_RECONNECT_ATTEMPTS}")
298
+
272
299
  if self._reconnect_attempts >= self.MAX_RECONNECT_ATTEMPTS:
300
+ self._log("Max reconnect attempts reached, giving up", "ERROR")
273
301
  self._is_connected = False
274
302
  return
275
303
 
@@ -277,44 +305,63 @@ class SerialTransport:
277
305
 
278
306
  try:
279
307
  if self.serial and self.serial.is_open:
308
+ self._log("Closing existing serial connection for reconnect")
280
309
  self.serial.close()
281
310
 
282
311
  time.sleep(self.RECONNECT_DELAY)
283
312
 
284
313
  self.port = self.find_com_port()
285
314
  if not self.port:
286
- raise MakcuConnectionError("Device not found")
315
+ raise MakcuConnectionError("Device not found during reconnect")
287
316
 
288
-
317
+ self._log(f"Reconnecting to {self.port} at {self.baudrate} baud")
289
318
  self.serial = serial.Serial(
290
319
  self.port,
291
320
  self.baudrate,
292
321
  timeout=0.001,
293
322
  write_timeout=0.01
294
323
  )
295
- self._change_baud_to_4M()
324
+
325
+ if not self._change_baud_to_4M():
326
+ raise MakcuConnectionError("Failed to change baud during reconnect")
296
327
 
297
328
  if self.send_init:
329
+ self._log("Sending init command during reconnect")
298
330
  self.serial.write(b"km.buttons(1)\r")
299
331
  self.serial.flush()
300
332
 
301
333
  self._reconnect_attempts = 0
334
+ self._log("Reconnect successful")
302
335
 
303
- except Exception:
336
+ except Exception as e:
337
+ self._log(f"Reconnect attempt failed: {e}", "ERROR")
304
338
  time.sleep(self.RECONNECT_DELAY)
305
339
 
306
340
  def _change_baud_to_4M(self) -> bool:
341
+ self._log("Changing baud rate to 4M")
342
+
307
343
  if self.serial and self.serial.is_open:
308
344
  self.serial.write(self.BAUD_CHANGE_COMMAND)
309
345
  self.serial.flush()
346
+
310
347
  time.sleep(0.02)
348
+
349
+ old_baud = self.serial.baudrate
311
350
  self.serial.baudrate = 4000000
312
351
  self._current_baud = 4000000
352
+
353
+ self._log(f"Baud rate changed: {old_baud} -> {self.serial.baudrate}")
313
354
  return True
355
+
356
+ self._log("Cannot change baud - serial not open", "ERROR")
314
357
  return False
315
358
 
316
359
  def connect(self) -> None:
360
+ connection_start = time.time()
361
+ self._log("Starting connection process")
362
+
317
363
  if self._is_connected:
364
+ self._log("Already connected")
318
365
  return
319
366
 
320
367
  if not self.override_port:
@@ -325,8 +372,9 @@ class SerialTransport:
325
372
  if not self.port:
326
373
  raise MakcuConnectionError("Makcu device not found")
327
374
 
375
+ self._log(f"Connecting to {self.port}")
376
+
328
377
  try:
329
-
330
378
  self.serial = serial.Serial(
331
379
  self.port,
332
380
  115200,
@@ -343,10 +391,14 @@ class SerialTransport:
343
391
  self._is_connected = True
344
392
  self._reconnect_attempts = 0
345
393
 
394
+ connection_time = time.time() - connection_start
395
+ self._log(f"Connection established in {connection_time:.3f}s")
396
+
346
397
  if self.send_init:
347
- self.serial.write(b"km.buttons(1)\r")
398
+ self._log("Sending initialization command")
399
+ init_cmd = b"km.buttons(1)\r"
400
+ self.serial.write(init_cmd)
348
401
  self.serial.flush()
349
-
350
402
 
351
403
  self._stop_event.clear()
352
404
  self._listener_thread = threading.Thread(
@@ -355,38 +407,61 @@ class SerialTransport:
355
407
  name="MakcuListener"
356
408
  )
357
409
  self._listener_thread.start()
410
+ self._log(f"Listener thread started: {self._listener_thread.name}")
358
411
 
359
412
  except Exception as e:
413
+ self._log(f"Connection failed: {e}", "ERROR")
360
414
  if self.serial:
361
- self.serial.close()
415
+ try:
416
+ self.serial.close()
417
+ except:
418
+ pass
362
419
  raise MakcuConnectionError(f"Failed to connect: {e}")
363
420
 
364
421
  def disconnect(self) -> None:
422
+ self._log("Starting disconnection process")
423
+
365
424
  self._is_connected = False
366
425
 
367
426
  if self.send_init:
368
427
  self._stop_event.set()
369
428
  if self._listener_thread and self._listener_thread.is_alive():
370
429
  self._listener_thread.join(timeout=0.1)
430
+ if self._listener_thread.is_alive():
431
+ self._log("Listener thread did not join within timeout")
432
+ else:
433
+ self._log("Listener thread stopped")
434
+
435
+ pending_count = len(self._pending_commands)
436
+ if pending_count > 0:
437
+ self._log(f"Cancelling {pending_count} pending commands")
371
438
 
372
439
  with self._command_lock:
373
- for pending in self._pending_commands.values():
440
+ for cmd_id, pending in self._pending_commands.items():
374
441
  if not pending.future.done():
375
442
  pending.future.cancel()
376
443
  self._pending_commands.clear()
377
444
 
378
445
  if self.serial and self.serial.is_open:
446
+ self._log(f"Closing serial port: {self.serial.port}")
379
447
  self.serial.close()
448
+
380
449
  self.serial = None
450
+ self._log("Disconnection completed")
381
451
 
382
- def send_command(self, command: str, expect_response: bool = False,
452
+ def send_command(self, command: str, expect_response: bool = True,
383
453
  timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
454
+ command_start = time.time()
455
+
384
456
  if not self._is_connected or not self.serial or not self.serial.is_open:
385
457
  raise MakcuConnectionError("Not connected")
386
458
 
387
459
  if not expect_response:
388
- self.serial.write(f"{command}\r\n".encode('ascii'))
460
+ cmd_bytes = f"{command}\r\n".encode('ascii')
461
+ self.serial.write(cmd_bytes)
389
462
  self.serial.flush()
463
+ send_time = time.time() - command_start
464
+ self._log(f"Command '{command}' sent in {send_time:.3f}s (no response expected)")
390
465
  return command
391
466
 
392
467
  cmd_id = self._generate_command_id()
@@ -405,56 +480,75 @@ class SerialTransport:
405
480
  )
406
481
 
407
482
  try:
408
- self.serial.write(f"{tagged_command}\r\n".encode('ascii'))
483
+ cmd_bytes = f"{tagged_command}\r\n".encode('ascii')
484
+ self.serial.write(cmd_bytes)
409
485
  self.serial.flush()
410
486
 
411
487
  result = future.result(timeout=timeout)
412
- return result.split('#')[0] if '#' in result else result
488
+
489
+ response = result.split('#')[0] if '#' in result else result
490
+ total_time = time.time() - command_start
491
+ self._log(f"Command '{command}' completed in {total_time:.3f}s total")
492
+ return response
413
493
 
414
494
  except TimeoutError:
495
+ total_time = time.time() - command_start
496
+ self._log(f"Command '{command}' timed out after {total_time:.3f}s", "ERROR")
415
497
  raise MakcuTimeoutError(f"Command timed out: {command}")
416
498
  except Exception as e:
499
+ total_time = time.time() - command_start
500
+ self._log(f"Command '{command}' failed after {total_time:.3f}s: {e}", "ERROR")
417
501
  with self._command_lock:
418
502
  self._pending_commands.pop(cmd_id, None)
419
503
  raise
420
504
 
421
505
  async def async_send_command(self, command: str, expect_response: bool = False,
422
506
  timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
507
+ self._log(f"Async sending command: '{command}'")
423
508
  loop = asyncio.get_running_loop()
424
509
  return await loop.run_in_executor(
425
510
  None, self.send_command, command, expect_response, timeout
426
511
  )
427
512
 
428
513
  def is_connected(self) -> bool:
429
- return self._is_connected and self.serial is not None and self.serial.is_open
514
+ connected = self._is_connected and self.serial is not None and self.serial.is_open
515
+ return connected
430
516
 
431
517
  def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
518
+ self._log(f"Setting button callback: {callback is not None}")
432
519
  self._button_callback = callback
433
520
 
434
521
  def get_button_states(self) -> Dict[str, bool]:
435
- return {
522
+ states = {
436
523
  self.BUTTON_MAP[i]: bool(self._button_states & (1 << i))
437
524
  for i in range(5)
438
525
  }
526
+ return states
439
527
 
440
528
  def get_button_mask(self) -> int:
441
529
  return self._last_button_mask
442
530
 
443
531
  def enable_button_monitoring(self, enable: bool = True) -> None:
444
- self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
532
+ cmd = "km.buttons(1)" if enable else "km.buttons(0)"
533
+ self._log(f"{'Enabling' if enable else 'Disabling'} button monitoring")
534
+ self.send_command(cmd)
445
535
 
446
536
  async def __aenter__(self):
537
+ self._log("Async context manager enter")
447
538
  loop = asyncio.get_running_loop()
448
539
  await loop.run_in_executor(None, self.connect)
449
540
  return self
450
541
 
451
542
  async def __aexit__(self, exc_type, exc_val, exc_tb):
543
+ self._log("Async context manager exit")
452
544
  loop = asyncio.get_running_loop()
453
545
  await loop.run_in_executor(None, self.disconnect)
454
546
 
455
547
  def __enter__(self):
548
+ self._log("Sync context manager enter")
456
549
  self.connect()
457
550
  return self
458
551
 
459
552
  def __exit__(self, exc_type, exc_val, exc_tb):
553
+ self._log("Sync context manager exit")
460
554
  self.disconnect()
makcu/controller.py CHANGED
@@ -97,6 +97,29 @@ class MakcuController:
97
97
  self._check_connection()
98
98
  self.mouse.move(dx, dy)
99
99
 
100
+ @maybe_async
101
+ def batch_execute(self, actions: List[Callable[[], None]]) -> None:
102
+ """Execute a batch of actions in sequence.
103
+
104
+ Args:
105
+ actions: List of callable functions to execute in order
106
+
107
+ Example:
108
+ makcu.batch_execute([
109
+ lambda: makcu.move(50, 0),
110
+ lambda: makcu.click(MouseButton.LEFT),
111
+ lambda: makcu.move(-50, 0),
112
+ lambda: makcu.click(MouseButton.RIGHT)
113
+ ])
114
+ """
115
+ self._check_connection()
116
+
117
+ for action in actions:
118
+ try:
119
+ action()
120
+ except Exception as e:
121
+ raise RuntimeError(f"Batch execution failed at action: {e}")
122
+
100
123
  @maybe_async
101
124
  def scroll(self, delta: int) -> None:
102
125
  self._check_connection()
@@ -358,7 +381,6 @@ class MakcuController:
358
381
  """Legacy method - use scroll() instead"""
359
382
  await self.scroll(delta)
360
383
 
361
-
362
384
  def create_controller(fallback_com_port: str = "", debug: bool = False,
363
385
  send_init: bool = True, auto_reconnect: bool = True,
364
386
  override_port: bool = False) -> MakcuController:
makcu/test_suite.py CHANGED
@@ -17,7 +17,7 @@ def test_press_and_release(makcu):
17
17
  makcu.release(MouseButton.LEFT)
18
18
 
19
19
  def test_firmware_version(makcu):
20
- version = makcu.mouse.get_firmware_version()
20
+ version = makcu.get_firmware_version()
21
21
  assert version and len(version.strip()) > 0
22
22
 
23
23
  def test_middle_click(makcu):
@@ -63,18 +63,15 @@ def test_batch_commands(makcu):
63
63
  print("Testing batch command execution (10 commands)...")
64
64
 
65
65
  start_time = time.perf_counter()
66
-
67
66
 
68
- makcu.move(10, 0)
69
- makcu.click(MouseButton.LEFT)
70
- makcu.move(0, 10)
71
- makcu.press(MouseButton.RIGHT)
72
- makcu.release(MouseButton.RIGHT)
73
- makcu.scroll(-1)
74
- makcu.move(-10, 0)
75
- makcu.click(MouseButton.MIDDLE)
76
- makcu.move(0, -10)
77
- makcu.scroll(1)
67
+ async def combo_actions():
68
+ await makcu.batch_execute([
69
+ lambda: makcu.move(5, 5),
70
+ lambda: makcu.click(MouseButton.LEFT),
71
+ lambda: makcu.scroll(-1)
72
+ ])
73
+
74
+ combo_actions()
78
75
 
79
76
  end_time = time.perf_counter()
80
77
  elapsed_ms = (end_time - start_time) * 1000