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/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
- class SerialTransport:
9
- baud_change_command = bytearray([0xDE, 0xAD, 0x05, 0x00, 0xA5, 0x00, 0x09, 0x3D, 0x00])
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
- button_map = {
12
- 0: 'left',
13
- 1: 'right',
14
- 2: 'middle',
15
- 3: 'mouse4',
16
- 4: 'mouse5'
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, send_init=True):
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._button_callback = None
25
- self._last_mask = 0
26
- self._lock = threading.Lock()
64
+ self.auto_reconnect = auto_reconnect
65
+ self.override_port = override_port
66
+
67
+ # Connection state
27
68
  self._is_connected = False
28
- self._stop_event = threading.Event()
29
- self._listener_thread = None
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}
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
- self._response_buffer = ""
34
- self._response_ready = threading.Event()
35
- self._waiting_for_response = False
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
- self._button_enum_map = {
40
- 0: MouseButton.LEFT,
41
- 1: MouseButton.RIGHT,
42
- 2: MouseButton.MIDDLE,
43
- 3: MouseButton.MOUSE4,
44
- 4: MouseButton.MOUSE5,
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
- self.port = self.find_com_port()
48
- if not self.port:
49
- raise MakcuConnectionError("Makcu device not found. Please specify a port explicitly.")
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
- self.baudrate = 115200
52
- self.serial = None
53
- self._current_baud = None
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 receive_response(self, sent_command: str = "") -> str:
56
- try:
57
- if self._response_ready.wait(timeout=self._response_timeout):
58
- response = self._response_buffer
59
- self._response_buffer = ""
60
- self._response_ready.clear()
61
-
62
- lines = response.strip().split('\n')
63
- lines = [line.strip() for line in lines if line.strip()]
64
-
65
- command_clean = sent_command.strip()
66
- cleaned_lines = []
67
-
68
- for line in lines:
69
- if not line:
70
- continue
71
- if line == command_clean:
72
- continue
73
- if line.startswith('>>> '):
74
- actual_response = line[4:].strip()
75
- if actual_response and actual_response != command_clean:
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
- result = "\n".join(cleaned_lines)
83
- if self.debug:
84
- self._log(f"Command: {command_clean} -> Response: '{result}'")
85
- return result
86
- else:
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
- except Exception as e:
90
- if self.debug:
91
- self._log(f"Error in receive_response: {e}")
92
- return ""
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 set_button_callback(self, callback):
95
- self._button_callback = callback
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
- def _log(self, message):
98
- timestamp = time.strftime("%H:%M:%S")
99
- entry = f"[{timestamp}] {message}"
100
- self._log_messages.append(entry)
101
- if len(self._log_messages) > 20:
102
- self._log_messages.pop(0)
103
- print(entry, flush=True)
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
- def find_com_port(self):
106
- self._log("Searching for CH343 device...")
185
+ if pending.future.done():
186
+ return
107
187
 
108
- for port in list_ports.comports():
109
- if "VID:PID=1A86:55D3" in port.hwid.upper():
110
- self._log(f"Device found: {port.device}")
111
- return port.device
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
- if self._fallback_com_port:
114
- self._log(f"Device not found. Falling back to specified port: {self._fallback_com_port}")
115
- return self._fallback_com_port
116
- else:
117
- self._log("Fallback port not specified or invalid.")
118
- return None
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 _open_serial_port(self, port, baud_rate):
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._log(f"Trying to open {port} at {baud_rate} baud.")
123
- return serial.Serial(port, baud_rate, timeout=0.05)
124
- except serial.SerialException:
125
- self._log(f"Failed to open {port} at {baud_rate} baud.")
126
- return None
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._log("Sending baud rate switch command to 4M.")
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
- self.serial = self._open_serial_port(self.port, 115200)
145
- if not self.serial:
146
- raise MakcuConnectionError(f"Failed to connect to {self.port} at 115200.")
147
- self._log(f"Connected to {self.port} at 115200.")
148
- if not self._change_baud_to_4M():
149
- raise MakcuConnectionError("Failed to switch to 4M baud.")
150
- self._is_connected = True
151
- if self.send_init:
152
- with self._lock:
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
- self._log("Sent init command: km.buttons(1)")
156
-
364
+
365
+ # Start high-priority listener thread
157
366
  self._stop_event.clear()
158
- self._listener_thread = threading.Thread(target=self._listen, kwargs={"debug": self.debug}, daemon=True)
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
- with self._lock:
167
- if self.serial and self.serial.is_open:
168
- self.serial.close()
169
- self.serial = None
170
- self._is_connected = False
171
- self._log("Disconnected.")
172
-
173
- def is_connected(self):
174
- return self._is_connected
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("Serial connection not open.")
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
- try:
182
- if expect_response:
183
- self._response_buffer = ""
184
- self._response_ready.clear()
185
- self._waiting_for_response = True
186
-
187
- self.serial.write(command.encode("ascii") + b"\r\n")
188
- self.serial.flush()
189
-
190
- if expect_response:
191
- response = self.receive_response(sent_command=command)
192
- self._waiting_for_response = False
193
- if not response:
194
- raise MakcuTimeoutError(f"No response from device for command: {command}")
195
- return response
196
-
197
- except Exception as e:
198
- self._waiting_for_response = False
199
- raise
200
-
201
- def get_button_states(self):
202
- return dict(self._button_states)
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 enable_button_monitoring(self, enable: bool = True):
208
- self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
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 catch_button(self, button: MouseButton):
211
- command = {
212
- "LEFT": "km.catch_ml(0)",
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 _is_button_data(self, byte_value):
224
- return byte_value <= 0b11111 and byte_value not in [0x0D, 0x0A]
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 _is_ascii_data(self, byte_value):
227
- return 32 <= byte_value <= 126 or byte_value in [0x0D, 0x0A] # Include CR/LF
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 _listen(self, debug=False):
230
- self._log("Started listener thread")
231
- self._last_mask = 0
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
- while self._is_connected and not self._stop_event.is_set():
236
- try:
237
- data = self.serial.read(self.serial.in_waiting or 1)
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
- for byte_val in data:
242
- if (self._is_button_data(byte_val) and
243
- not self._waiting_for_response):
244
- if byte_val != self._last_mask:
245
- changed_bits = byte_val ^ self._last_mask
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
- self._last_mask = byte_val
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
- if debug:
257
- pressed = [name for _, name in self.button_map.items() if self._button_states[name]]
258
- button_str = ", ".join(pressed) if pressed else "No buttons pressed"
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
- except serial.SerialException as e:
285
- if "ClearCommError failed" not in str(e):
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()