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