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