makcu 0.2.0__py3-none-any.whl → 2.1.1__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 CHANGED
@@ -1,13 +1,4 @@
1
- """
2
- Makcu Python Library v2.0
3
-
4
- High-performance library for controlling Makcu devices with async support,
5
- zero-delay command execution, and automatic reconnection.
6
- """
7
-
8
1
  from typing import List
9
-
10
- # Import main components
11
2
  from .controller import MakcuController, create_controller, create_async_controller
12
3
  from .enums import MouseButton
13
4
  from .errors import (
@@ -18,22 +9,15 @@ from .errors import (
18
9
  MakcuResponseError
19
10
  )
20
11
 
21
- # Version info
22
12
  __version__: str = "2.0.0"
23
13
  __author__: str = "SleepyTotem"
24
14
  __license__: str = "GPL"
25
15
 
26
- # Public API
27
16
  __all__: List[str] = [
28
- # Main controller
29
17
  "MakcuController",
30
18
  "create_controller",
31
19
  "create_async_controller",
32
-
33
- # Enums
34
20
  "MouseButton",
35
-
36
- # Errors
37
21
  "MakcuError",
38
22
  "MakcuConnectionError",
39
23
  "MakcuCommandError",
@@ -41,10 +25,8 @@ __all__: List[str] = [
41
25
  "MakcuResponseError",
42
26
  ]
43
27
 
44
- # Convenience imports for backward compatibility
45
28
  from .controller import MakcuController as Controller
46
29
 
47
- # Package metadata
48
30
  __doc__ = """
49
31
  Makcu Python Library provides a high-performance interface for controlling
50
32
  Makcu USB devices. Features include:
makcu/__main__.py CHANGED
@@ -28,16 +28,12 @@ def debug_console():
28
28
 
29
29
  command_counter += 1
30
30
 
31
- # Send command and expect response for most commands
32
31
  response = transport.send_command(cmd, expect_response=True)
33
32
 
34
- # Handle the response properly
35
33
  if response and response.strip():
36
- # If response is just the command echoed back, that means success
37
34
  if response.strip() == cmd:
38
35
  print(f"{cmd}")
39
36
  else:
40
- # This is actual response data (like "km.MAKCU" for version)
41
37
  print(f"{response}")
42
38
  else:
43
39
  print("(no response)")
@@ -65,19 +61,16 @@ def test_port(port: str) -> None:
65
61
  print(f"❌ Unexpected error: {e}")
66
62
 
67
63
  def parse_html_results(html_file: Path):
68
- """Parse test results from the pytest HTML report"""
69
64
  if not html_file.exists():
70
65
  raise FileNotFoundError(f"HTML report not found: {html_file}")
71
66
 
72
67
  with open(html_file, 'r', encoding='utf-8') as f:
73
68
  content = f.read()
74
69
 
75
- # Extract the JSON data from the HTML file
76
70
  match = re.search(r'data-jsonblob="([^"]*)"', content)
77
71
  if not match:
78
72
  raise ValueError("Could not find JSON data in HTML report")
79
73
 
80
- # Decode HTML entities in the JSON string
81
74
  json_str = match.group(1)
82
75
  json_str = json_str.replace('"', '"').replace(''', "'").replace('&', '&')
83
76
 
@@ -89,13 +82,10 @@ def parse_html_results(html_file: Path):
89
82
  test_results = []
90
83
  total_ms = 0
91
84
 
92
- # Filter out the connect_to_port test from display
93
85
  skip_tests = {'test_connect_to_port'}
94
86
 
95
87
  for test_id, test_data_list in data.get('tests', {}).items():
96
- test_name = test_id.split('::')[-1] # Get just the test function name
97
-
98
- # Skip connection test from display
88
+ test_name = test_id.split('::')[-1]
99
89
  if test_name in skip_tests:
100
90
  continue
101
91
 
@@ -103,7 +93,6 @@ def parse_html_results(html_file: Path):
103
93
  status = test_data.get('result', 'UNKNOWN')
104
94
  duration_str = test_data.get('duration', '0 ms')
105
95
 
106
- # Parse duration (format: "X ms")
107
96
  duration_match = re.search(r'(\d+)\s*ms', duration_str)
108
97
  duration_ms = int(duration_match.group(1)) if duration_match else 0
109
98
  total_ms += duration_ms
@@ -113,7 +102,6 @@ def parse_html_results(html_file: Path):
113
102
  return test_results, total_ms
114
103
 
115
104
  def run_tests() -> NoReturn:
116
- """Run tests with beautiful console output"""
117
105
  try:
118
106
  from rich.console import Console
119
107
  from rich.table import Table
@@ -127,7 +115,7 @@ def run_tests() -> NoReturn:
127
115
  console = Console()
128
116
 
129
117
  header = Panel.fit(
130
- "[bold cyan]🧪 Makcu Test Suite v2.0[/bold cyan]\n[dim]High-Performance Python Library[/dim]",
118
+ "[bold cyan]Makcu Test Suite v2.1.1[/bold cyan]\n[dim]High-Performance Python Library[/dim]",
131
119
  border_style="bright_blue"
132
120
  )
133
121
  console.print(Align.center(header))
@@ -150,25 +138,23 @@ def run_tests() -> NoReturn:
150
138
  ) as progress:
151
139
  task = progress.add_task("[cyan]Running tests...", total=100)
152
140
 
153
- # Run pytest with minimal output, generating HTML report
154
141
  result = subprocess.run(
155
142
  [
156
143
  sys.executable, "-m", "pytest",
157
144
  str(test_file),
158
145
  "--rootdir", str(package_dir),
159
- "-q", # Quiet mode
160
- "--tb=no", # No traceback
146
+ "-q",
147
+ "--tb=no",
161
148
  "--html", str(html_file),
162
149
  "--self-contained-html"
163
150
  ],
164
- stdout=subprocess.DEVNULL, # Hide stdout completely
165
- stderr=subprocess.DEVNULL, # Hide stderr completely
151
+ stdout=subprocess.DEVNULL,
152
+ stderr=subprocess.DEVNULL,
166
153
  text=True
167
154
  )
168
155
 
169
156
  progress.update(task, completed=100)
170
157
 
171
- # Parse results from HTML file
172
158
  try:
173
159
  test_results, total_ms = parse_html_results(html_file)
174
160
  except (FileNotFoundError, ValueError) as e:
@@ -178,7 +164,6 @@ def run_tests() -> NoReturn:
178
164
 
179
165
  elapsed_time = time.time() - start_time
180
166
 
181
- # Table rendering
182
167
  table = Table(title="[bold]Test Results[/bold]", show_header=True, header_style="bold magenta")
183
168
  table.add_column("Test", style="cyan", no_wrap=True)
184
169
  table.add_column("Status", justify="center")
@@ -203,12 +188,12 @@ def run_tests() -> NoReturn:
203
188
  status_text = status
204
189
 
205
190
  time_str = f"{duration_ms}ms" if duration_ms else "-"
206
- if duration_ms < 3:
207
- perf = "[green]Excellent[/green]"
208
- elif duration_ms < 5:
209
- perf = "[cyan]🚀 Great[/cyan]"
210
- elif duration_ms < 10:
211
- perf = "[yellow]👍 Good[/yellow]"
191
+ if duration_ms <= 3:
192
+ perf = "[green]Excellent[/green]"
193
+ elif duration_ms <= 5:
194
+ perf = "[cyan]Great[/cyan]"
195
+ elif duration_ms <= 10:
196
+ perf = "[yellow]Good[/yellow]"
212
197
  elif duration_ms > 0:
213
198
  perf = "[red]🐌 Needs work[/red]"
214
199
  else:
@@ -255,7 +240,7 @@ def run_tests() -> NoReturn:
255
240
 
256
241
  package_dir: Path = Path(__file__).resolve().parent
257
242
  test_file: Path = package_dir / "test_suite.py"
258
- html_file: Path = package_dir.parent / "latest_pytest.html"
243
+ html_file: Path = Path.cwd() / "latest_pytest.html"
259
244
 
260
245
  result = pytest.main([
261
246
  str(test_file),
@@ -266,7 +251,6 @@ def run_tests() -> NoReturn:
266
251
  "--self-contained-html"
267
252
  ])
268
253
 
269
- # Try to parse HTML results even in fallback mode
270
254
  try:
271
255
  test_results, total_ms = parse_html_results(html_file)
272
256
  passed = sum(1 for _, status, _ in test_results if status.upper() == "PASSED")
makcu/conftest.py CHANGED
@@ -1,10 +1,8 @@
1
1
  import pytest
2
- import time
3
2
  from makcu import MakcuController, MouseButton
4
3
 
5
4
  @pytest.fixture(scope="session")
6
5
  def makcu(request):
7
- """Session-scoped fixture with final cleanup at end of all tests"""
8
6
  ctrl = MakcuController(fallback_com_port="COM1", debug=False)
9
7
 
10
8
  def cleanup():
makcu/connection.py CHANGED
@@ -1,15 +1,13 @@
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
+ from typing import Optional, Dict, Callable
6
5
  from serial.tools import list_ports
7
- from dataclasses import dataclass, field
6
+ from dataclasses import dataclass
8
7
  from collections import deque
9
8
  from concurrent.futures import Future
10
9
  import logging
11
10
  import asyncio
12
- import re
13
11
  from .errors import MakcuConnectionError, MakcuTimeoutError
14
12
  from .enums import MouseButton
15
13
 
@@ -17,31 +15,28 @@ logger = logging.getLogger(__name__)
17
15
 
18
16
  @dataclass
19
17
  class PendingCommand:
20
- """Tracks a command waiting for response"""
21
18
  command_id: int
22
19
  command: str
23
20
  future: Future
24
21
  timestamp: float
25
22
  expect_response: bool = True
26
- timeout: float = 0.1 # Reduced from 1.0
23
+ timeout: float = 0.1
27
24
 
28
25
  @dataclass
29
26
  class ParsedResponse:
30
- """Parsed response from device"""
31
27
  command_id: Optional[int]
32
28
  content: str
33
29
  is_button_data: bool = False
34
30
  button_mask: Optional[int] = None
35
31
 
36
32
  class SerialTransport:
37
- """Ultra-optimized serial transport for gaming performance"""
38
33
 
39
34
  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
35
+ DEFAULT_TIMEOUT = 0.1
36
+ MAX_RECONNECT_ATTEMPTS = 3
37
+ RECONNECT_DELAY = 0.1
43
38
 
44
- # Pre-computed button maps for faster lookups
39
+
45
40
  BUTTON_MAP = (
46
41
  'left', 'right', 'middle', 'mouse4', 'mouse5'
47
42
  )
@@ -57,14 +52,14 @@ class SerialTransport:
57
52
  def __init__(self, fallback: str = "", debug: bool = False,
58
53
  send_init: bool = True, auto_reconnect: bool = True,
59
54
  override_port: bool = False) -> None:
60
- # Basic config
55
+
61
56
  self._fallback_com_port = fallback
62
57
  self.debug = debug
63
58
  self.send_init = send_init
64
59
  self.auto_reconnect = auto_reconnect
65
60
  self.override_port = override_port
66
61
 
67
- # Connection state
62
+
68
63
  self._is_connected = False
69
64
  self._reconnect_attempts = 0
70
65
  self.port: Optional[str] = None
@@ -72,54 +67,51 @@ class SerialTransport:
72
67
  self.serial: Optional[serial.Serial] = None
73
68
  self._current_baud: Optional[int] = None
74
69
 
75
- # Command tracking with pre-allocated buffer
70
+
76
71
  self._command_counter = 0
77
72
  self._pending_commands: Dict[int, PendingCommand] = {}
78
73
  self._command_lock = threading.Lock()
79
74
 
80
- # Response parsing with optimized buffer
81
- self._parse_buffer = bytearray(1024) # Pre-allocate
75
+
76
+ self._parse_buffer = bytearray(1024)
82
77
  self._buffer_pos = 0
83
- self._response_queue = deque(maxlen=100) # Limit queue size
78
+ self._response_queue = deque(maxlen=100)
84
79
 
85
- # Button state with bitwise operations
80
+
86
81
  self._button_callback: Optional[Callable[[MouseButton, bool], None]] = None
87
82
  self._last_button_mask = 0
88
- self._button_states = 0 # Use single int instead of dict
83
+ self._button_states = 0
89
84
 
90
- # Threading
85
+
91
86
  self._stop_event = threading.Event()
92
87
  self._listener_thread: Optional[threading.Thread] = None
93
88
 
94
- # Logging optimization
89
+
95
90
  self._log_messages: deque = deque(maxlen=100)
96
91
 
97
- # Cache for frequently used data
98
- self._ascii_decode_table = bytes(range(128)) # ASCII lookup table
92
+
93
+ self._ascii_decode_table = bytes(range(128))
99
94
 
100
95
  def _log(self, message: str, level: str = "INFO") -> None:
101
- """Optimized logging - only format when needed"""
102
96
  if not self.debug and level == "DEBUG":
103
97
  return
104
98
 
105
99
  if self.debug:
106
- # Use faster time formatting
100
+
107
101
  timestamp = f"{time.time():.3f}"
108
102
  entry = f"[{timestamp}] [{level}] {message}"
109
103
  self._log_messages.append(entry)
110
104
  print(entry, flush=True)
111
105
 
112
106
  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
107
+ self._command_counter = (self._command_counter + 1) & 0x2710
115
108
  return self._command_counter
116
109
 
117
110
  def find_com_port(self) -> Optional[str]:
118
- """Optimized port finding with caching"""
119
111
  if self.override_port:
120
112
  return self._fallback_com_port
121
113
 
122
- # Cache the VID:PID string
114
+
123
115
  target_hwid = "VID:PID=1A86:55D3"
124
116
 
125
117
  for port in list_ports.comports():
@@ -134,8 +126,7 @@ class SerialTransport:
134
126
  return None
135
127
 
136
128
  def _parse_response_line(self, line: bytes) -> ParsedResponse:
137
- """Optimized parsing - work with bytes directly"""
138
- # Skip decode for simple checks
129
+
139
130
  if line.startswith(b'>>> '):
140
131
  content = line[4:].decode('ascii', 'ignore').strip()
141
132
  return ParsedResponse(None, content, False)
@@ -144,18 +135,17 @@ class SerialTransport:
144
135
  return ParsedResponse(None, content, False)
145
136
 
146
137
  def _handle_button_data(self, byte_val: int) -> None:
147
- """Optimized button handling with bitwise operations"""
148
138
  if byte_val == self._last_button_mask:
149
139
  return
150
140
 
151
141
  changed_bits = byte_val ^ self._last_button_mask
152
142
 
153
- # Use bitwise operations instead of dict lookups
154
- for bit in range(5): # Only check 5 buttons
143
+
144
+ for bit in range(5):
155
145
  if changed_bits & (1 << bit):
156
146
  is_pressed = bool(byte_val & (1 << bit))
157
147
 
158
- # Update button state in single int
148
+
159
149
  if is_pressed:
160
150
  self._button_states |= (1 << bit)
161
151
  else:
@@ -165,12 +155,11 @@ class SerialTransport:
165
155
  try:
166
156
  self._button_callback(self.BUTTON_ENUM_MAP[bit], is_pressed)
167
157
  except Exception:
168
- pass # Silently ignore callback errors for speed
158
+ pass
169
159
 
170
160
  self._last_button_mask = byte_val
171
161
 
172
162
  def _process_pending_commands(self, content: str) -> None:
173
- """Optimized command processing"""
174
163
  if not content or not self._pending_commands:
175
164
  return
176
165
 
@@ -178,14 +167,14 @@ class SerialTransport:
178
167
  if not self._pending_commands:
179
168
  return
180
169
 
181
- # Get oldest command without min() call
170
+
182
171
  oldest_id = next(iter(self._pending_commands))
183
172
  pending = self._pending_commands[oldest_id]
184
173
 
185
174
  if pending.future.done():
186
175
  return
187
176
 
188
- # Fast string comparison
177
+
189
178
  if content == pending.command:
190
179
  if not pending.expect_response:
191
180
  pending.future.set_result(pending.command)
@@ -195,20 +184,19 @@ class SerialTransport:
195
184
  del self._pending_commands[oldest_id]
196
185
 
197
186
  def _cleanup_timed_out_commands(self) -> None:
198
- """Optimized cleanup - batch operations"""
199
187
  if not self._pending_commands:
200
188
  return
201
189
 
202
190
  current_time = time.time()
203
191
  with self._command_lock:
204
- # Collect timed out commands
192
+
205
193
  timed_out = [
206
194
  (cmd_id, pending)
207
195
  for cmd_id, pending in self._pending_commands.items()
208
196
  if current_time - pending.timestamp > pending.timeout
209
197
  ]
210
198
 
211
- # Batch remove
199
+
212
200
  for cmd_id, pending in timed_out:
213
201
  del self._pending_commands[cmd_id]
214
202
  if not pending.future.done():
@@ -216,44 +204,44 @@ class SerialTransport:
216
204
  MakcuTimeoutError(f"Command #{cmd_id} timed out")
217
205
  )
218
206
 
207
+
219
208
  def _listen(self) -> None:
220
- """Ultra-optimized listener for gaming performance"""
221
- # Pre-allocate buffers
209
+
222
210
  read_buffer = bytearray(4096)
223
211
  line_buffer = bytearray(256)
224
212
  line_pos = 0
225
213
 
226
- # Cache frequently accessed attributes
214
+
227
215
  serial_read = self.serial.read
228
216
  serial_in_waiting = lambda: self.serial.in_waiting
229
217
  is_connected = lambda: self._is_connected
230
218
  stop_requested = self._stop_event.is_set
231
219
 
232
- # Timing for cleanup
220
+
233
221
  last_cleanup = time.time()
234
- cleanup_interval = 0.05 # 50ms cleanup interval
222
+ cleanup_interval = 0.05
235
223
 
236
224
  while is_connected() and not stop_requested():
237
225
  try:
238
- # Check bytes available
226
+
239
227
  bytes_available = serial_in_waiting()
240
228
  if not bytes_available:
241
- time.sleep(0.001) # 1ms sleep to prevent CPU spinning
229
+ time.sleep(0.001)
242
230
  continue
243
231
 
244
- # Read into pre-allocated buffer
232
+
245
233
  bytes_read = serial_read(min(bytes_available, 4096))
246
234
 
247
- # Process bytes
235
+
248
236
  for byte_val in bytes_read:
249
- # Fast button data check
237
+
250
238
  if byte_val < 32 and byte_val not in (0x0D, 0x0A):
251
239
  self._handle_button_data(byte_val)
252
240
  else:
253
- # Build line
254
- if byte_val == 0x0A: # LF
241
+
242
+ if byte_val == 0x0A:
255
243
  if line_pos > 0:
256
- # Process line without allocation
244
+
257
245
  line = bytes(line_buffer[:line_pos])
258
246
  line_pos = 0
259
247
 
@@ -261,12 +249,12 @@ class SerialTransport:
261
249
  response = self._parse_response_line(line)
262
250
  if response.content:
263
251
  self._process_pending_commands(response.content)
264
- elif byte_val != 0x0D: # Ignore CR
265
- if line_pos < 256: # Prevent overflow
252
+ elif byte_val != 0x0D:
253
+ if line_pos < 256:
266
254
  line_buffer[line_pos] = byte_val
267
255
  line_pos += 1
268
256
 
269
- # Periodic cleanup with reduced frequency
257
+
270
258
  current_time = time.time()
271
259
  if current_time - last_cleanup > cleanup_interval:
272
260
  self._cleanup_timed_out_commands()
@@ -278,10 +266,9 @@ class SerialTransport:
278
266
  else:
279
267
  break
280
268
  except Exception:
281
- pass # Silently continue for maximum performance
269
+ pass
282
270
 
283
271
  def _attempt_reconnect(self) -> None:
284
- """Fast reconnection attempt"""
285
272
  if self._reconnect_attempts >= self.MAX_RECONNECT_ATTEMPTS:
286
273
  self._is_connected = False
287
274
  return
@@ -298,12 +285,12 @@ class SerialTransport:
298
285
  if not self.port:
299
286
  raise MakcuConnectionError("Device not found")
300
287
 
301
- # Use write_timeout for faster failure detection
288
+
302
289
  self.serial = serial.Serial(
303
290
  self.port,
304
291
  self.baudrate,
305
- timeout=0.001, # 1ms read timeout
306
- write_timeout=0.01 # 10ms write timeout
292
+ timeout=0.001,
293
+ write_timeout=0.01
307
294
  )
308
295
  self._change_baud_to_4M()
309
296
 
@@ -317,18 +304,16 @@ class SerialTransport:
317
304
  time.sleep(self.RECONNECT_DELAY)
318
305
 
319
306
  def _change_baud_to_4M(self) -> bool:
320
- """Optimized baud rate change"""
321
307
  if self.serial and self.serial.is_open:
322
308
  self.serial.write(self.BAUD_CHANGE_COMMAND)
323
309
  self.serial.flush()
324
- time.sleep(0.02) # Reduced from 0.05
310
+ time.sleep(0.02)
325
311
  self.serial.baudrate = 4000000
326
312
  self._current_baud = 4000000
327
313
  return True
328
314
  return False
329
315
 
330
316
  def connect(self) -> None:
331
- """Optimized connection with minimal overhead"""
332
317
  if self._is_connected:
333
318
  return
334
319
 
@@ -341,12 +326,12 @@ class SerialTransport:
341
326
  raise MakcuConnectionError("Makcu device not found")
342
327
 
343
328
  try:
344
- # Optimized serial settings for gaming
329
+
345
330
  self.serial = serial.Serial(
346
331
  self.port,
347
332
  115200,
348
- timeout=0.001, # 1ms timeout
349
- write_timeout=0.01, # 10ms write timeout
333
+ timeout=0.001,
334
+ write_timeout=0.01,
350
335
  xonxoff=False,
351
336
  rtscts=False,
352
337
  dsrdtr=False
@@ -362,7 +347,7 @@ class SerialTransport:
362
347
  self.serial.write(b"km.buttons(1)\r")
363
348
  self.serial.flush()
364
349
 
365
- # Start high-priority listener thread
350
+
366
351
  self._stop_event.clear()
367
352
  self._listener_thread = threading.Thread(
368
353
  target=self._listen,
@@ -377,13 +362,12 @@ class SerialTransport:
377
362
  raise MakcuConnectionError(f"Failed to connect: {e}")
378
363
 
379
364
  def disconnect(self) -> None:
380
- """Fast disconnect"""
381
365
  self._is_connected = False
382
366
 
383
367
  if self.send_init:
384
368
  self._stop_event.set()
385
369
  if self._listener_thread and self._listener_thread.is_alive():
386
- self._listener_thread.join(timeout=0.1) # Reduced timeout
370
+ self._listener_thread.join(timeout=0.1)
387
371
 
388
372
  with self._command_lock:
389
373
  for pending in self._pending_commands.values():
@@ -397,17 +381,14 @@ class SerialTransport:
397
381
 
398
382
  def send_command(self, command: str, expect_response: bool = False,
399
383
  timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
400
- """Optimized command sending for minimal latency"""
401
384
  if not self._is_connected or not self.serial or not self.serial.is_open:
402
385
  raise MakcuConnectionError("Not connected")
403
386
 
404
- # For commands without response, send and return immediately
405
387
  if not expect_response:
406
388
  self.serial.write(f"{command}\r\n".encode('ascii'))
407
389
  self.serial.flush()
408
390
  return command
409
391
 
410
- # Commands with response need tracking
411
392
  cmd_id = self._generate_command_id()
412
393
  tagged_command = f"{command}#{cmd_id}"
413
394
 
@@ -439,36 +420,29 @@ class SerialTransport:
439
420
 
440
421
  async def async_send_command(self, command: str, expect_response: bool = False,
441
422
  timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
442
- """Async command optimized for gaming"""
443
423
  loop = asyncio.get_running_loop()
444
424
  return await loop.run_in_executor(
445
425
  None, self.send_command, command, expect_response, timeout
446
426
  )
447
427
 
448
428
  def is_connected(self) -> bool:
449
- """Fast connection check"""
450
429
  return self._is_connected and self.serial is not None and self.serial.is_open
451
430
 
452
431
  def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
453
- """Set button callback"""
454
432
  self._button_callback = callback
455
433
 
456
434
  def get_button_states(self) -> Dict[str, bool]:
457
- """Get button states with optimized lookup"""
458
435
  return {
459
436
  self.BUTTON_MAP[i]: bool(self._button_states & (1 << i))
460
437
  for i in range(5)
461
438
  }
462
439
 
463
440
  def get_button_mask(self) -> int:
464
- """Direct mask access"""
465
441
  return self._last_button_mask
466
442
 
467
443
  def enable_button_monitoring(self, enable: bool = True) -> None:
468
- """Fast button monitoring toggle"""
469
444
  self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
470
445
 
471
- # Context managers unchanged but included for completeness
472
446
  async def __aenter__(self):
473
447
  loop = asyncio.get_running_loop()
474
448
  await loop.run_in_executor(None, self.connect)