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 +0 -18
- makcu/__main__.py +13 -29
- makcu/conftest.py +0 -2
- makcu/connection.py +57 -83
- makcu/controller.py +27 -71
- makcu/errors.py +0 -5
- makcu/makcu.pyi +1 -3
- makcu/mouse.py +21 -41
- makcu/test_suite.py +8 -22
- {makcu-0.2.0.dist-info → makcu-2.1.1.dist-info}/METADATA +10 -10
- makcu-2.1.1.dist-info/RECORD +16 -0
- makcu-0.2.0.dist-info/RECORD +0 -16
- {makcu-0.2.0.dist-info → makcu-2.1.1.dist-info}/WHEEL +0 -0
- {makcu-0.2.0.dist-info → makcu-2.1.1.dist-info}/licenses/LICENSE +0 -0
- {makcu-0.2.0.dist-info → makcu-2.1.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.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",
|
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:
|
@@ -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 =
|
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
|
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
|
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
|
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
|
41
|
-
MAX_RECONNECT_ATTEMPTS = 3
|
42
|
-
RECONNECT_DELAY = 0.1
|
35
|
+
DEFAULT_TIMEOUT = 0.1
|
36
|
+
MAX_RECONNECT_ATTEMPTS = 3
|
37
|
+
RECONNECT_DELAY = 0.1
|
43
38
|
|
44
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
81
|
-
self._parse_buffer = bytearray(1024)
|
75
|
+
|
76
|
+
self._parse_buffer = bytearray(1024)
|
82
77
|
self._buffer_pos = 0
|
83
|
-
self._response_queue = deque(maxlen=100)
|
78
|
+
self._response_queue = deque(maxlen=100)
|
84
79
|
|
85
|
-
|
80
|
+
|
86
81
|
self._button_callback: Optional[Callable[[MouseButton, bool], None]] = None
|
87
82
|
self._last_button_mask = 0
|
88
|
-
self._button_states = 0
|
83
|
+
self._button_states = 0
|
89
84
|
|
90
|
-
|
85
|
+
|
91
86
|
self._stop_event = threading.Event()
|
92
87
|
self._listener_thread: Optional[threading.Thread] = None
|
93
88
|
|
94
|
-
|
89
|
+
|
95
90
|
self._log_messages: deque = deque(maxlen=100)
|
96
91
|
|
97
|
-
|
98
|
-
self._ascii_decode_table = bytes(range(128))
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
154
|
-
for bit in range(5):
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
220
|
+
|
233
221
|
last_cleanup = time.time()
|
234
|
-
cleanup_interval = 0.05
|
222
|
+
cleanup_interval = 0.05
|
235
223
|
|
236
224
|
while is_connected() and not stop_requested():
|
237
225
|
try:
|
238
|
-
|
226
|
+
|
239
227
|
bytes_available = serial_in_waiting()
|
240
228
|
if not bytes_available:
|
241
|
-
time.sleep(0.001)
|
229
|
+
time.sleep(0.001)
|
242
230
|
continue
|
243
231
|
|
244
|
-
|
232
|
+
|
245
233
|
bytes_read = serial_read(min(bytes_available, 4096))
|
246
234
|
|
247
|
-
|
235
|
+
|
248
236
|
for byte_val in bytes_read:
|
249
|
-
|
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
|
-
|
254
|
-
if byte_val == 0x0A:
|
241
|
+
|
242
|
+
if byte_val == 0x0A:
|
255
243
|
if line_pos > 0:
|
256
|
-
|
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:
|
265
|
-
if line_pos < 256:
|
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
|
-
|
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
|
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
|
-
|
288
|
+
|
302
289
|
self.serial = serial.Serial(
|
303
290
|
self.port,
|
304
291
|
self.baudrate,
|
305
|
-
timeout=0.001,
|
306
|
-
write_timeout=0.01
|
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)
|
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
|
-
|
329
|
+
|
345
330
|
self.serial = serial.Serial(
|
346
331
|
self.port,
|
347
332
|
115200,
|
348
|
-
timeout=0.001,
|
349
|
-
write_timeout=0.01,
|
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
|
-
|
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)
|
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)
|