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