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/controller.py
CHANGED
@@ -1,125 +1,283 @@
|
|
1
|
+
import asyncio
|
1
2
|
import random
|
2
3
|
import time
|
4
|
+
from typing import Optional, Dict, Callable, Union, List, Any
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
3
6
|
from .mouse import Mouse
|
4
7
|
from .connection import SerialTransport
|
5
8
|
from .errors import MakcuConnectionError
|
6
9
|
from .enums import MouseButton
|
7
10
|
|
8
11
|
class MakcuController:
|
9
|
-
|
10
|
-
|
12
|
+
|
13
|
+
|
14
|
+
_BUTTON_LOCK_MAP = {
|
15
|
+
MouseButton.LEFT: 'lock_left',
|
16
|
+
MouseButton.RIGHT: 'lock_right',
|
17
|
+
MouseButton.MIDDLE: 'lock_middle',
|
18
|
+
MouseButton.MOUSE4: 'lock_side1',
|
19
|
+
MouseButton.MOUSE5: 'lock_side2',
|
20
|
+
}
|
21
|
+
|
22
|
+
def __init__(self, fallback_com_port: str = "", debug: bool = False,
|
23
|
+
send_init: bool = True, auto_reconnect: bool = True,
|
24
|
+
override_port: bool = False) -> None:
|
25
|
+
self.transport = SerialTransport(
|
26
|
+
fallback_com_port,
|
27
|
+
debug=debug,
|
28
|
+
send_init=send_init,
|
29
|
+
auto_reconnect=auto_reconnect,
|
30
|
+
override_port=override_port
|
31
|
+
)
|
11
32
|
self.mouse = Mouse(self.transport)
|
33
|
+
self._executor = ThreadPoolExecutor(max_workers=1)
|
34
|
+
self._connection_callbacks: List[Callable[[bool], None]] = []
|
35
|
+
|
12
36
|
|
13
|
-
|
37
|
+
self._connected = False
|
38
|
+
|
39
|
+
|
40
|
+
def connect(self) -> None:
|
14
41
|
self.transport.connect()
|
42
|
+
self._connected = True
|
43
|
+
self._notify_connection_change(True)
|
15
44
|
|
16
|
-
def disconnect(self):
|
45
|
+
def disconnect(self) -> None:
|
17
46
|
self.transport.disconnect()
|
47
|
+
self._connected = False
|
48
|
+
self._notify_connection_change(False)
|
49
|
+
self._executor.shutdown(wait=False)
|
50
|
+
|
51
|
+
def is_connected(self) -> bool:
|
52
|
+
return self._connected and self.transport.is_connected()
|
53
|
+
|
54
|
+
def _check_connection(self) -> None:
|
55
|
+
if not self._connected:
|
56
|
+
raise MakcuConnectionError("Not connected")
|
57
|
+
|
58
|
+
def _notify_connection_change(self, connected: bool) -> None:
|
59
|
+
for callback in self._connection_callbacks:
|
60
|
+
try:
|
61
|
+
callback(connected)
|
62
|
+
except Exception:
|
63
|
+
pass
|
18
64
|
|
19
|
-
def is_connected(self):
|
20
|
-
return self.transport.is_connected()
|
21
65
|
|
22
|
-
def
|
23
|
-
if not self.
|
66
|
+
def click(self, button: MouseButton) -> None:
|
67
|
+
if not self._connected:
|
24
68
|
raise MakcuConnectionError("Not connected")
|
69
|
+
self.mouse.press(button)
|
70
|
+
self.mouse.release(button)
|
71
|
+
|
72
|
+
def double_click(self, button: MouseButton) -> None:
|
73
|
+
if not self._connected:
|
74
|
+
raise MakcuConnectionError("Not connected")
|
75
|
+
self.mouse.press(button)
|
76
|
+
self.mouse.release(button)
|
25
77
|
|
26
|
-
|
27
|
-
self._check_connection()
|
78
|
+
time.sleep(0.001)
|
28
79
|
self.mouse.press(button)
|
29
80
|
self.mouse.release(button)
|
30
81
|
|
31
|
-
def move(self, dx: int, dy: int):
|
32
|
-
self.
|
82
|
+
def move(self, dx: int, dy: int) -> None:
|
83
|
+
if not self._connected:
|
84
|
+
raise MakcuConnectionError("Not connected")
|
33
85
|
self.mouse.move(dx, dy)
|
34
86
|
|
35
|
-
def scroll(self, delta: int):
|
36
|
-
self.
|
87
|
+
def scroll(self, delta: int) -> None:
|
88
|
+
if not self._connected:
|
89
|
+
raise MakcuConnectionError("Not connected")
|
37
90
|
self.mouse.scroll(delta)
|
38
91
|
|
39
|
-
def
|
40
|
-
self.
|
92
|
+
def press(self, button: MouseButton) -> None:
|
93
|
+
if not self._connected:
|
94
|
+
raise MakcuConnectionError("Not connected")
|
95
|
+
self.mouse.press(button)
|
96
|
+
|
97
|
+
def release(self, button: MouseButton) -> None:
|
98
|
+
if not self._connected:
|
99
|
+
raise MakcuConnectionError("Not connected")
|
100
|
+
self.mouse.release(button)
|
101
|
+
|
102
|
+
|
103
|
+
def move_smooth(self, dx: int, dy: int, segments: int = 10) -> None:
|
104
|
+
if not self._connected:
|
105
|
+
raise MakcuConnectionError("Not connected")
|
41
106
|
self.mouse.move_smooth(dx, dy, segments)
|
42
107
|
|
43
|
-
def move_bezier(self, dx: int, dy: int, segments: int
|
44
|
-
|
108
|
+
def move_bezier(self, dx: int, dy: int, segments: int = 20,
|
109
|
+
ctrl_x: Optional[int] = None, ctrl_y: Optional[int] = None) -> None:
|
110
|
+
if not self._connected:
|
111
|
+
raise MakcuConnectionError("Not connected")
|
112
|
+
if ctrl_x is None:
|
113
|
+
ctrl_x = dx // 2
|
114
|
+
if ctrl_y is None:
|
115
|
+
ctrl_y = dy // 2
|
45
116
|
self.mouse.move_bezier(dx, dy, segments, ctrl_x, ctrl_y)
|
46
117
|
|
47
|
-
def lock_mouse_x(self, lock: bool):
|
48
|
-
self._check_connection()
|
49
|
-
self.mouse.lock_x(lock)
|
50
118
|
|
51
|
-
def
|
52
|
-
self.
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
119
|
+
def lock(self, target: Union[MouseButton, str]) -> None:
|
120
|
+
if not self._connected:
|
121
|
+
raise MakcuConnectionError("Not connected")
|
122
|
+
|
123
|
+
if isinstance(target, MouseButton):
|
124
|
+
if target in self._BUTTON_LOCK_MAP:
|
125
|
+
getattr(self.mouse, self._BUTTON_LOCK_MAP[target])(True)
|
126
|
+
else:
|
127
|
+
raise ValueError(f"Unsupported button: {target}")
|
128
|
+
elif target.upper() in ['X', 'Y']:
|
129
|
+
if target.upper() == 'X':
|
130
|
+
self.mouse.lock_x(True)
|
131
|
+
else:
|
132
|
+
self.mouse.lock_y(True)
|
133
|
+
else:
|
134
|
+
raise ValueError(f"Invalid lock target: {target}")
|
135
|
+
|
136
|
+
def unlock(self, target: Union[MouseButton, str]) -> None:
|
137
|
+
if not self._connected:
|
138
|
+
raise MakcuConnectionError("Not connected")
|
139
|
+
|
140
|
+
if isinstance(target, MouseButton):
|
141
|
+
if target in self._BUTTON_LOCK_MAP:
|
142
|
+
getattr(self.mouse, self._BUTTON_LOCK_MAP[target])(False)
|
143
|
+
else:
|
144
|
+
raise ValueError(f"Unsupported button: {target}")
|
145
|
+
elif target.upper() in ['X', 'Y']:
|
146
|
+
if target.upper() == 'X':
|
147
|
+
self.mouse.lock_x(False)
|
148
|
+
else:
|
149
|
+
self.mouse.lock_y(False)
|
150
|
+
else:
|
151
|
+
raise ValueError(f"Invalid unlock target: {target}")
|
152
|
+
|
153
|
+
|
154
|
+
def lock_left(self, lock: bool) -> None:
|
155
|
+
if not self._connected:
|
156
|
+
raise MakcuConnectionError("Not connected")
|
57
157
|
self.mouse.lock_left(lock)
|
58
158
|
|
59
|
-
def lock_middle(self, lock: bool):
|
60
|
-
self.
|
159
|
+
def lock_middle(self, lock: bool) -> None:
|
160
|
+
if not self._connected:
|
161
|
+
raise MakcuConnectionError("Not connected")
|
61
162
|
self.mouse.lock_middle(lock)
|
62
163
|
|
63
|
-
def lock_right(self, lock: bool):
|
64
|
-
self.
|
164
|
+
def lock_right(self, lock: bool) -> None:
|
165
|
+
if not self._connected:
|
166
|
+
raise MakcuConnectionError("Not connected")
|
65
167
|
self.mouse.lock_right(lock)
|
66
168
|
|
67
|
-
def lock_side1(self, lock: bool):
|
68
|
-
self.
|
169
|
+
def lock_side1(self, lock: bool) -> None:
|
170
|
+
if not self._connected:
|
171
|
+
raise MakcuConnectionError("Not connected")
|
69
172
|
self.mouse.lock_side1(lock)
|
70
173
|
|
71
|
-
def lock_side2(self, lock: bool):
|
72
|
-
self.
|
174
|
+
def lock_side2(self, lock: bool) -> None:
|
175
|
+
if not self._connected:
|
176
|
+
raise MakcuConnectionError("Not connected")
|
73
177
|
self.mouse.lock_side2(lock)
|
74
178
|
|
75
|
-
def lock_x(self, lock: bool):
|
76
|
-
self.
|
179
|
+
def lock_x(self, lock: bool) -> None:
|
180
|
+
if not self._connected:
|
181
|
+
raise MakcuConnectionError("Not connected")
|
77
182
|
self.mouse.lock_x(lock)
|
78
183
|
|
79
|
-
def lock_y(self, lock: bool):
|
80
|
-
self.
|
184
|
+
def lock_y(self, lock: bool) -> None:
|
185
|
+
if not self._connected:
|
186
|
+
raise MakcuConnectionError("Not connected")
|
81
187
|
self.mouse.lock_y(lock)
|
82
188
|
|
83
|
-
def
|
84
|
-
self.
|
189
|
+
def lock_mouse_x(self, lock: bool) -> None:
|
190
|
+
self.lock_x(lock)
|
191
|
+
|
192
|
+
def lock_mouse_y(self, lock: bool) -> None:
|
193
|
+
self.lock_y(lock)
|
194
|
+
|
195
|
+
def is_locked(self, button: MouseButton) -> bool:
|
196
|
+
if not self._connected:
|
197
|
+
raise MakcuConnectionError("Not connected")
|
198
|
+
return self.mouse.is_locked(button)
|
199
|
+
|
200
|
+
def get_all_lock_states(self) -> Dict[str, bool]:
|
201
|
+
if not self._connected:
|
202
|
+
raise MakcuConnectionError("Not connected")
|
203
|
+
return self.mouse.get_all_lock_states()
|
204
|
+
|
205
|
+
|
206
|
+
def spoof_serial(self, serial: str) -> None:
|
207
|
+
if not self._connected:
|
208
|
+
raise MakcuConnectionError("Not connected")
|
85
209
|
self.mouse.spoof_serial(serial)
|
86
210
|
|
87
|
-
def reset_serial(self):
|
88
|
-
self.
|
211
|
+
def reset_serial(self) -> None:
|
212
|
+
if not self._connected:
|
213
|
+
raise MakcuConnectionError("Not connected")
|
89
214
|
self.mouse.reset_serial()
|
90
215
|
|
91
|
-
def get_device_info(self):
|
92
|
-
self.
|
216
|
+
def get_device_info(self) -> Dict[str, str]:
|
217
|
+
if not self._connected:
|
218
|
+
raise MakcuConnectionError("Not connected")
|
93
219
|
return self.mouse.get_device_info()
|
94
220
|
|
95
|
-
def get_firmware_version(self):
|
96
|
-
self.
|
221
|
+
def get_firmware_version(self) -> str:
|
222
|
+
if not self._connected:
|
223
|
+
raise MakcuConnectionError("Not connected")
|
97
224
|
return self.mouse.get_firmware_version()
|
98
225
|
|
226
|
+
|
99
227
|
def get_button_mask(self) -> int:
|
100
|
-
self.
|
228
|
+
if not self._connected:
|
229
|
+
raise MakcuConnectionError("Not connected")
|
101
230
|
return self.transport.get_button_mask()
|
102
231
|
|
103
|
-
def
|
104
|
-
self.
|
105
|
-
|
232
|
+
def get_button_states(self) -> Dict[str, bool]:
|
233
|
+
if not self._connected:
|
234
|
+
raise MakcuConnectionError("Not connected")
|
235
|
+
return self.transport.get_button_states()
|
236
|
+
|
237
|
+
def is_pressed(self, button: MouseButton) -> bool:
|
238
|
+
if not self._connected:
|
239
|
+
raise MakcuConnectionError("Not connected")
|
240
|
+
return self.transport.get_button_states().get(button.name.lower(), False)
|
241
|
+
|
242
|
+
def enable_button_monitoring(self, enable: bool = True) -> None:
|
243
|
+
if not self._connected:
|
244
|
+
raise MakcuConnectionError("Not connected")
|
245
|
+
self.transport.enable_button_monitoring(enable)
|
246
|
+
|
247
|
+
def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
|
248
|
+
if not self._connected:
|
249
|
+
raise MakcuConnectionError("Not connected")
|
250
|
+
self.transport.set_button_callback(callback)
|
251
|
+
|
252
|
+
|
253
|
+
def on_connection_change(self, callback: Callable[[bool], None]) -> None:
|
254
|
+
self._connection_callbacks.append(callback)
|
255
|
+
|
256
|
+
def remove_connection_callback(self, callback: Callable[[bool], None]) -> None:
|
257
|
+
if callback in self._connection_callbacks:
|
258
|
+
self._connection_callbacks.remove(callback)
|
259
|
+
|
106
260
|
|
107
261
|
def click_human_like(self, button: MouseButton, count: int = 1,
|
108
|
-
|
109
|
-
self.
|
262
|
+
profile: str = "normal", jitter: int = 0) -> None:
|
263
|
+
if not self._connected:
|
264
|
+
raise MakcuConnectionError("Not connected")
|
265
|
+
|
110
266
|
|
111
267
|
timing_profiles = {
|
112
268
|
"normal": (60, 120, 100, 180),
|
113
269
|
"fast": (30, 60, 50, 100),
|
114
270
|
"slow": (100, 180, 150, 300),
|
271
|
+
"variable": (40, 200, 80, 250),
|
272
|
+
"gaming": (20, 40, 30, 60),
|
115
273
|
}
|
116
274
|
|
117
275
|
if profile not in timing_profiles:
|
118
|
-
raise ValueError(f"Invalid profile: {profile}
|
276
|
+
raise ValueError(f"Invalid profile: {profile}")
|
119
277
|
|
120
278
|
min_down, max_down, min_wait, max_wait = timing_profiles[profile]
|
121
279
|
|
122
|
-
for
|
280
|
+
for i in range(count):
|
123
281
|
if jitter > 0:
|
124
282
|
dx = random.randint(-jitter, jitter)
|
125
283
|
dy = random.randint(-jitter, jitter)
|
@@ -128,32 +286,92 @@ class MakcuController:
|
|
128
286
|
self.mouse.press(button)
|
129
287
|
time.sleep(random.uniform(min_down, max_down) / 1000.0)
|
130
288
|
self.mouse.release(button)
|
131
|
-
|
289
|
+
|
290
|
+
if i < count - 1:
|
291
|
+
time.sleep(random.uniform(min_wait, max_wait) / 1000.0)
|
132
292
|
|
133
|
-
def
|
134
|
-
|
135
|
-
self.
|
293
|
+
def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
|
294
|
+
button: MouseButton = MouseButton.LEFT, duration: float = 1.0) -> None:
|
295
|
+
if not self._connected:
|
296
|
+
raise MakcuConnectionError("Not connected")
|
297
|
+
|
136
298
|
|
137
|
-
|
138
|
-
|
139
|
-
|
299
|
+
self.move(start_x, start_y)
|
300
|
+
time.sleep(0.02)
|
301
|
+
|
140
302
|
|
141
|
-
|
142
|
-
|
143
|
-
|
303
|
+
self.press(button)
|
304
|
+
time.sleep(0.02)
|
305
|
+
|
144
306
|
|
145
|
-
|
146
|
-
self.
|
147
|
-
|
307
|
+
segments = max(10, int(duration * 30))
|
308
|
+
self.move_smooth(end_x - start_x, end_y - start_y, segments)
|
309
|
+
|
148
310
|
|
149
|
-
|
150
|
-
self.
|
151
|
-
self.mouse.release(button)
|
311
|
+
time.sleep(0.02)
|
312
|
+
self.release(button)
|
152
313
|
|
153
|
-
def get_button_states(self) -> dict:
|
154
|
-
self._check_connection()
|
155
|
-
return self.transport.get_button_states()
|
156
314
|
|
157
|
-
def
|
158
|
-
self.
|
159
|
-
|
315
|
+
def __enter__(self):
|
316
|
+
if not self.is_connected():
|
317
|
+
self.connect()
|
318
|
+
return self
|
319
|
+
|
320
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
321
|
+
self.disconnect()
|
322
|
+
|
323
|
+
|
324
|
+
async def async_connect(self) -> None:
|
325
|
+
loop = asyncio.get_running_loop()
|
326
|
+
await loop.run_in_executor(self._executor, self.connect)
|
327
|
+
|
328
|
+
async def async_disconnect(self) -> None:
|
329
|
+
loop = asyncio.get_running_loop()
|
330
|
+
await loop.run_in_executor(self._executor, self.disconnect)
|
331
|
+
|
332
|
+
async def async_click(self, button: MouseButton) -> None:
|
333
|
+
loop = asyncio.get_running_loop()
|
334
|
+
await loop.run_in_executor(self._executor, self.click, button)
|
335
|
+
|
336
|
+
async def async_move(self, dx: int, dy: int) -> None:
|
337
|
+
loop = asyncio.get_running_loop()
|
338
|
+
await loop.run_in_executor(self._executor, self.move, dx, dy)
|
339
|
+
|
340
|
+
async def async_scroll(self, delta: int) -> None:
|
341
|
+
loop = asyncio.get_running_loop()
|
342
|
+
await loop.run_in_executor(self._executor, self.scroll, delta)
|
343
|
+
|
344
|
+
|
345
|
+
async def __aenter__(self):
|
346
|
+
await self.async_connect()
|
347
|
+
return self
|
348
|
+
|
349
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
350
|
+
await self.async_disconnect()
|
351
|
+
|
352
|
+
|
353
|
+
|
354
|
+
def create_controller(fallback_com_port: str = "", debug: bool = False,
|
355
|
+
send_init: bool = True, auto_reconnect: bool = True) -> MakcuController:
|
356
|
+
makcu = MakcuController(
|
357
|
+
fallback_com_port,
|
358
|
+
debug=debug,
|
359
|
+
send_init=send_init,
|
360
|
+
auto_reconnect=auto_reconnect
|
361
|
+
)
|
362
|
+
makcu.connect()
|
363
|
+
return makcu
|
364
|
+
|
365
|
+
|
366
|
+
async def create_async_controller(fallback_com_port: str = "", debug: bool = False,
|
367
|
+
send_init: bool = True, auto_reconnect: bool = True,
|
368
|
+
override_port: bool = False) -> MakcuController:
|
369
|
+
makcu = MakcuController(
|
370
|
+
fallback_com_port,
|
371
|
+
debug=debug,
|
372
|
+
send_init=send_init,
|
373
|
+
auto_reconnect=auto_reconnect,
|
374
|
+
override_port=override_port
|
375
|
+
)
|
376
|
+
await makcu.async_connect()
|
377
|
+
return makcu
|
makcu/errors.py
CHANGED
@@ -1,19 +1,14 @@
|
|
1
1
|
class MakcuError(Exception):
|
2
|
-
"""Base exception for all Makcu-related errors."""
|
3
2
|
pass
|
4
3
|
|
5
4
|
class MakcuConnectionError(MakcuError):
|
6
|
-
"""Raised when the device connection fails."""
|
7
5
|
pass
|
8
6
|
|
9
7
|
class MakcuCommandError(MakcuError):
|
10
|
-
"""Raised when a device command is invalid, rejected, or fails."""
|
11
8
|
pass
|
12
9
|
|
13
10
|
class MakcuTimeoutError(MakcuError):
|
14
|
-
"""Raised when the device does not respond in time."""
|
15
11
|
pass
|
16
12
|
|
17
13
|
class MakcuResponseError(MakcuError):
|
18
|
-
"""Raised when the response from the device is malformed or unexpected."""
|
19
14
|
pass
|
makcu/makcu.pyi
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
from typing import Optional, Dict, Callable, List, Union
|
2
|
+
from .controller import MakcuController
|
3
|
+
from .enums import MouseButton
|
4
|
+
from .errors import MakcuError, MakcuConnectionError, MakcuCommandError, MakcuTimeoutError, MakcuResponseError
|
5
|
+
|
6
|
+
__version__: str
|
7
|
+
__all__: List[str]
|
8
|
+
|
9
|
+
def create_controller(
|
10
|
+
fallback_com_port: str = "",
|
11
|
+
debug: bool = False,
|
12
|
+
send_init: bool = True
|
13
|
+
) -> MakcuController: ...
|