makcu 2.1.2__py3-none-any.whl → 2.1.3__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 CHANGED
@@ -1,60 +1,378 @@
1
- from typing import List
2
- from .controller import MakcuController, create_controller, create_async_controller
3
- from .enums import MouseButton
4
- from .errors import (
5
- MakcuError,
6
- MakcuConnectionError,
7
- MakcuCommandError,
8
- MakcuTimeoutError,
9
- MakcuResponseError
10
- )
11
-
12
- __version__: str = "2.0.0"
13
- __author__: str = "SleepyTotem"
14
- __license__: str = "GPL"
15
-
16
- __all__: List[str] = [
17
- "MakcuController",
18
- "create_controller",
19
- "create_async_controller",
20
- "MouseButton",
21
- "MakcuError",
22
- "MakcuConnectionError",
23
- "MakcuCommandError",
24
- "MakcuTimeoutError",
25
- "MakcuResponseError",
26
- ]
27
-
28
- from .controller import MakcuController as Controller
29
-
30
- __doc__ = """
31
- Makcu Python Library provides a high-performance interface for controlling
32
- Makcu USB devices. Features include:
33
-
34
- - Full async/await support for modern Python applications
35
- - Zero-delay command execution with intelligent tracking
36
- - Automatic device reconnection on disconnect
37
- - Human-like mouse movement and clicking patterns
38
- - Comprehensive button and axis locking
39
- - Real-time button event monitoring
40
-
41
- Quick Start:
42
- >>> from makcu import create_controller, MouseButton
43
- >>> makcu = create_controller()
44
- >>> makcu.click(MouseButton.LEFT)
45
- >>> makcu.move(100, 50)
46
- >>> makcu.disconnect()
47
-
48
- Async Usage:
49
- >>> import asyncio
50
- >>> from makcu import create_async_controller, MouseButton
51
- >>>
52
- >>> async def main():
53
- ... async with await create_async_controller() as makcu:
54
- ... await makcu.click(MouseButton.LEFT)
55
- ... await makcu.move(100, 50)
56
- >>>
57
- >>> asyncio.run(main())
58
-
59
- For more information, visit: https://github.com/SleepyTotem/makcu-py-lib
60
- """
1
+ import asyncio
2
+ import random
3
+ import time
4
+ from typing import Optional, Dict, Callable, Union, List
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ from .mouse import Mouse
7
+ from .connection import SerialTransport
8
+ from .errors import MakcuConnectionError
9
+ from .enums import MouseButton
10
+ from .controller import maybe_async
11
+
12
+ class MakcuController:
13
+ _BUTTON_LOCK_MAP = {
14
+ MouseButton.LEFT: 'lock_left',
15
+ MouseButton.RIGHT: 'lock_right',
16
+ MouseButton.MIDDLE: 'lock_middle',
17
+ MouseButton.MOUSE4: 'lock_side1',
18
+ MouseButton.MOUSE5: 'lock_side2',
19
+ }
20
+
21
+ def __init__(self, fallback_com_port: str = "", debug: bool = False,
22
+ send_init: bool = True, auto_reconnect: bool = True,
23
+ override_port: bool = False) -> None:
24
+ self.transport = SerialTransport(
25
+ fallback_com_port,
26
+ debug=debug,
27
+ send_init=send_init,
28
+ auto_reconnect=auto_reconnect,
29
+ override_port=override_port
30
+ )
31
+ self.mouse = Mouse(self.transport)
32
+ self._executor = ThreadPoolExecutor(max_workers=1)
33
+ self._connection_callbacks: List[Callable[[bool], None]] = []
34
+ self._connected = False
35
+
36
+ async def _run_async(self, func, *args, **kwargs):
37
+ """Run a function in the thread pool executor"""
38
+ loop = asyncio.get_running_loop()
39
+ return await loop.run_in_executor(self._executor, func, self, *args, **kwargs)
40
+
41
+ def _check_connection(self) -> None:
42
+ if not self._connected:
43
+ raise MakcuConnectionError("Not connected")
44
+
45
+ def _notify_connection_change(self, connected: bool) -> None:
46
+ for callback in self._connection_callbacks:
47
+ try:
48
+ callback(connected)
49
+ except Exception:
50
+ pass
51
+
52
+ @maybe_async
53
+ def connect(self) -> None:
54
+ self.transport.connect()
55
+ self._connected = True
56
+ self._notify_connection_change(True)
57
+
58
+ @maybe_async
59
+ def disconnect(self) -> None:
60
+ self.transport.disconnect()
61
+ self._connected = False
62
+ self._notify_connection_change(False)
63
+ self._executor.shutdown(wait=False)
64
+
65
+ @maybe_async
66
+ def is_connected(self) -> bool:
67
+ return self._connected and self.transport.is_connected()
68
+
69
+ @maybe_async
70
+ def click(self, button: MouseButton) -> None:
71
+ self._check_connection()
72
+ self.mouse.press(button)
73
+ self.mouse.release(button)
74
+
75
+ @maybe_async
76
+ def double_click(self, button: MouseButton) -> None:
77
+ self._check_connection()
78
+ self.mouse.press(button)
79
+ self.mouse.release(button)
80
+ time.sleep(0.001)
81
+ self.mouse.press(button)
82
+ self.mouse.release(button)
83
+
84
+ @maybe_async
85
+ def move(self, dx: int, dy: int) -> None:
86
+ self._check_connection()
87
+ self.mouse.move(dx, dy)
88
+
89
+ @maybe_async
90
+ def scroll(self, delta: int) -> None:
91
+ self._check_connection()
92
+ self.mouse.scroll(delta)
93
+
94
+ @maybe_async
95
+ def press(self, button: MouseButton) -> None:
96
+ self._check_connection()
97
+ self.mouse.press(button)
98
+
99
+ @maybe_async
100
+ def release(self, button: MouseButton) -> None:
101
+ self._check_connection()
102
+ self.mouse.release(button)
103
+
104
+ @maybe_async
105
+ def move_smooth(self, dx: int, dy: int, segments: int = 10) -> None:
106
+ self._check_connection()
107
+ self.mouse.move_smooth(dx, dy, segments)
108
+
109
+ @maybe_async
110
+ def move_bezier(self, dx: int, dy: int, segments: int = 20,
111
+ ctrl_x: Optional[int] = None, ctrl_y: Optional[int] = None) -> None:
112
+ self._check_connection()
113
+ if ctrl_x is None:
114
+ ctrl_x = dx // 2
115
+ if ctrl_y is None:
116
+ ctrl_y = dy // 2
117
+ self.mouse.move_bezier(dx, dy, segments, ctrl_x, ctrl_y)
118
+
119
+ @maybe_async
120
+ def lock(self, target: Union[MouseButton, str]) -> None:
121
+ self._check_connection()
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
+ @maybe_async
137
+ def unlock(self, target: Union[MouseButton, str]) -> None:
138
+ self._check_connection()
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
+ @maybe_async
154
+ def lock_left(self, lock: bool) -> None:
155
+ self._check_connection()
156
+ self.mouse.lock_left(lock)
157
+
158
+ @maybe_async
159
+ def lock_middle(self, lock: bool) -> None:
160
+ self._check_connection()
161
+ self.mouse.lock_middle(lock)
162
+
163
+ @maybe_async
164
+ def lock_right(self, lock: bool) -> None:
165
+ self._check_connection()
166
+ self.mouse.lock_right(lock)
167
+
168
+ @maybe_async
169
+ def lock_side1(self, lock: bool) -> None:
170
+ self._check_connection()
171
+ self.mouse.lock_side1(lock)
172
+
173
+ @maybe_async
174
+ def lock_side2(self, lock: bool) -> None:
175
+ self._check_connection()
176
+ self.mouse.lock_side2(lock)
177
+
178
+ @maybe_async
179
+ def lock_x(self, lock: bool) -> None:
180
+ self._check_connection()
181
+ self.mouse.lock_x(lock)
182
+
183
+ @maybe_async
184
+ def lock_y(self, lock: bool) -> None:
185
+ self._check_connection()
186
+ self.mouse.lock_y(lock)
187
+
188
+ @maybe_async
189
+ def lock_mouse_x(self, lock: bool) -> None:
190
+ self.lock_x(lock)
191
+
192
+ @maybe_async
193
+ def lock_mouse_y(self, lock: bool) -> None:
194
+ self.lock_y(lock)
195
+
196
+ @maybe_async
197
+ def is_locked(self, button: MouseButton) -> bool:
198
+ self._check_connection()
199
+ return self.mouse.is_locked(button)
200
+
201
+ @maybe_async
202
+ def get_all_lock_states(self) -> Dict[str, bool]:
203
+ self._check_connection()
204
+ return self.mouse.get_all_lock_states()
205
+
206
+ @maybe_async
207
+ def spoof_serial(self, serial: str) -> None:
208
+ self._check_connection()
209
+ self.mouse.spoof_serial(serial)
210
+
211
+ @maybe_async
212
+ def reset_serial(self) -> None:
213
+ self._check_connection()
214
+ self.mouse.reset_serial()
215
+
216
+ @maybe_async
217
+ def get_device_info(self) -> Dict[str, str]:
218
+ self._check_connection()
219
+ return self.mouse.get_device_info()
220
+
221
+ @maybe_async
222
+ def get_firmware_version(self) -> str:
223
+ self._check_connection()
224
+ return self.mouse.get_firmware_version()
225
+
226
+ @maybe_async
227
+ def get_button_mask(self) -> int:
228
+ self._check_connection()
229
+ return self.transport.get_button_mask()
230
+
231
+ @maybe_async
232
+ def get_button_states(self) -> Dict[str, bool]:
233
+ self._check_connection()
234
+ return self.transport.get_button_states()
235
+
236
+ @maybe_async
237
+ def is_pressed(self, button: MouseButton) -> bool:
238
+ self._check_connection()
239
+ return self.transport.get_button_states().get(button.name.lower(), False)
240
+
241
+ @maybe_async
242
+ def enable_button_monitoring(self, enable: bool = True) -> None:
243
+ self._check_connection()
244
+ self.transport.enable_button_monitoring(enable)
245
+
246
+ @maybe_async
247
+ def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
248
+ self._check_connection()
249
+ self.transport.set_button_callback(callback)
250
+
251
+ @maybe_async
252
+ def on_connection_change(self, callback: Callable[[bool], None]) -> None:
253
+ self._connection_callbacks.append(callback)
254
+
255
+ @maybe_async
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
+
260
+ @maybe_async
261
+ def click_human_like(self, button: MouseButton, count: int = 1,
262
+ profile: str = "normal", jitter: int = 0) -> None:
263
+ self._check_connection()
264
+
265
+ timing_profiles = {
266
+ "normal": (60, 120, 100, 180),
267
+ "fast": (30, 60, 50, 100),
268
+ "slow": (100, 180, 150, 300),
269
+ "variable": (40, 200, 80, 250),
270
+ "gaming": (20, 40, 30, 60),
271
+ }
272
+
273
+ if profile not in timing_profiles:
274
+ raise ValueError(f"Invalid profile: {profile}")
275
+
276
+ min_down, max_down, min_wait, max_wait = timing_profiles[profile]
277
+
278
+ for i in range(count):
279
+ if jitter > 0:
280
+ dx = random.randint(-jitter, jitter)
281
+ dy = random.randint(-jitter, jitter)
282
+ self.mouse.move(dx, dy)
283
+
284
+ self.mouse.press(button)
285
+ time.sleep(random.uniform(min_down, max_down) / 1000.0)
286
+ self.mouse.release(button)
287
+
288
+ if i < count - 1:
289
+ time.sleep(random.uniform(min_wait, max_wait) / 1000.0)
290
+
291
+ @maybe_async
292
+ def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
293
+ button: MouseButton = MouseButton.LEFT, duration: float = 1.0) -> None:
294
+ self._check_connection()
295
+
296
+ # Move to start position
297
+ self.move(start_x, start_y)
298
+ time.sleep(0.02)
299
+
300
+ # Press button
301
+ self.press(button)
302
+ time.sleep(0.02)
303
+
304
+ # Smooth move to end position
305
+ segments = max(10, int(duration * 30))
306
+ self.move_smooth(end_x - start_x, end_y - start_y, segments)
307
+
308
+ # Release button
309
+ time.sleep(0.02)
310
+ self.release(button)
311
+
312
+ # Context managers for both sync and async
313
+ def __enter__(self):
314
+ if not self.is_connected():
315
+ self.connect()
316
+ return self
317
+
318
+ def __exit__(self, exc_type, exc_val, exc_tb):
319
+ self.disconnect()
320
+
321
+ async def __aenter__(self):
322
+ if not await self.is_connected():
323
+ await self.connect()
324
+ return self
325
+
326
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
327
+ await self.disconnect()
328
+
329
+ # Legacy async methods for backward compatibility
330
+ async def async_connect(self) -> None:
331
+ """Legacy method - use connect() instead"""
332
+ await self.connect()
333
+
334
+ async def async_disconnect(self) -> None:
335
+ """Legacy method - use disconnect() instead"""
336
+ await self.disconnect()
337
+
338
+ async def async_click(self, button: MouseButton) -> None:
339
+ """Legacy method - use click() instead"""
340
+ await self.click(button)
341
+
342
+ async def async_move(self, dx: int, dy: int) -> None:
343
+ """Legacy method - use move() instead"""
344
+ await self.move(dx, dy)
345
+
346
+ async def async_scroll(self, delta: int) -> None:
347
+ """Legacy method - use scroll() instead"""
348
+ await self.scroll(delta)
349
+
350
+
351
+ def create_controller(fallback_com_port: str = "", debug: bool = False,
352
+ send_init: bool = True, auto_reconnect: bool = True,
353
+ override_port: bool = False) -> MakcuController:
354
+ """Create and connect a controller synchronously"""
355
+ makcu = MakcuController(
356
+ fallback_com_port,
357
+ debug=debug,
358
+ send_init=send_init,
359
+ auto_reconnect=auto_reconnect,
360
+ override_port=override_port
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
+ """Create and connect a controller asynchronously"""
370
+ makcu = MakcuController(
371
+ fallback_com_port,
372
+ debug=debug,
373
+ send_init=send_init,
374
+ auto_reconnect=auto_reconnect,
375
+ override_port=override_port
376
+ )
377
+ await makcu.connect()
378
+ return makcu