makcu 2.1.1__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/controller.py CHANGED
@@ -1,377 +1,389 @@
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 scroll(self, delta: int) -> None:
102
+ self._check_connection()
103
+ self.mouse.scroll(delta)
104
+
105
+ @maybe_async
106
+ def press(self, button: MouseButton) -> None:
107
+ self._check_connection()
108
+ self.mouse.press(button)
109
+
110
+ @maybe_async
111
+ def release(self, button: MouseButton) -> None:
112
+ self._check_connection()
113
+ self.mouse.release(button)
114
+
115
+ @maybe_async
116
+ def move_smooth(self, dx: int, dy: int, segments: int = 10) -> None:
117
+ self._check_connection()
118
+ self.mouse.move_smooth(dx, dy, segments)
119
+
120
+ @maybe_async
121
+ def move_bezier(self, dx: int, dy: int, segments: int = 20,
122
+ ctrl_x: Optional[int] = None, ctrl_y: Optional[int] = None) -> None:
123
+ self._check_connection()
124
+ if ctrl_x is None:
125
+ ctrl_x = dx // 2
126
+ if ctrl_y is None:
127
+ ctrl_y = dy // 2
128
+ self.mouse.move_bezier(dx, dy, segments, ctrl_x, ctrl_y)
129
+
130
+ @maybe_async
131
+ def lock(self, target: Union[MouseButton, str]) -> None:
132
+ self._check_connection()
133
+
134
+ if isinstance(target, MouseButton):
135
+ if target in self._BUTTON_LOCK_MAP:
136
+ getattr(self.mouse, self._BUTTON_LOCK_MAP[target])(True)
137
+ else:
138
+ raise ValueError(f"Unsupported button: {target}")
139
+ elif target.upper() in ['X', 'Y']:
140
+ if target.upper() == 'X':
141
+ self.mouse.lock_x(True)
142
+ else:
143
+ self.mouse.lock_y(True)
144
+ else:
145
+ raise ValueError(f"Invalid lock target: {target}")
146
+
147
+ @maybe_async
148
+ def unlock(self, target: Union[MouseButton, str]) -> None:
149
+ self._check_connection()
150
+
151
+ if isinstance(target, MouseButton):
152
+ if target in self._BUTTON_LOCK_MAP:
153
+ getattr(self.mouse, self._BUTTON_LOCK_MAP[target])(False)
154
+ else:
155
+ raise ValueError(f"Unsupported button: {target}")
156
+ elif target.upper() in ['X', 'Y']:
157
+ if target.upper() == 'X':
158
+ self.mouse.lock_x(False)
159
+ else:
160
+ self.mouse.lock_y(False)
161
+ else:
162
+ raise ValueError(f"Invalid unlock target: {target}")
163
+
164
+ @maybe_async
165
+ def lock_left(self, lock: bool) -> None:
166
+ self._check_connection()
167
+ self.mouse.lock_left(lock)
168
+
169
+ @maybe_async
170
+ def lock_middle(self, lock: bool) -> None:
171
+ self._check_connection()
172
+ self.mouse.lock_middle(lock)
173
+
174
+ @maybe_async
175
+ def lock_right(self, lock: bool) -> None:
176
+ self._check_connection()
177
+ self.mouse.lock_right(lock)
178
+
179
+ @maybe_async
180
+ def lock_side1(self, lock: bool) -> None:
181
+ self._check_connection()
182
+ self.mouse.lock_side1(lock)
183
+
184
+ @maybe_async
185
+ def lock_side2(self, lock: bool) -> None:
186
+ self._check_connection()
187
+ self.mouse.lock_side2(lock)
188
+
189
+ @maybe_async
190
+ def lock_x(self, lock: bool) -> None:
191
+ self._check_connection()
192
+ self.mouse.lock_x(lock)
193
+
194
+ @maybe_async
195
+ def lock_y(self, lock: bool) -> None:
196
+ self._check_connection()
197
+ self.mouse.lock_y(lock)
198
+
199
+ @maybe_async
200
+ def lock_mouse_x(self, lock: bool) -> None:
201
+ self.lock_x(lock)
202
+
203
+ @maybe_async
204
+ def lock_mouse_y(self, lock: bool) -> None:
205
+ self.lock_y(lock)
206
+
207
+ @maybe_async
208
+ def is_locked(self, button: MouseButton) -> bool:
209
+ self._check_connection()
210
+ return self.mouse.is_locked(button)
211
+
212
+ @maybe_async
213
+ def get_all_lock_states(self) -> Dict[str, bool]:
214
+ self._check_connection()
215
+ return self.mouse.get_all_lock_states()
216
+
217
+ @maybe_async
218
+ def spoof_serial(self, serial: str) -> None:
219
+ self._check_connection()
220
+ self.mouse.spoof_serial(serial)
221
+
222
+ @maybe_async
223
+ def reset_serial(self) -> None:
224
+ self._check_connection()
225
+ self.mouse.reset_serial()
226
+
227
+ @maybe_async
228
+ def get_device_info(self) -> Dict[str, str]:
229
+ self._check_connection()
230
+ return self.mouse.get_device_info()
231
+
232
+ @maybe_async
233
+ def get_firmware_version(self) -> str:
234
+ self._check_connection()
235
+ return self.mouse.get_firmware_version()
236
+
237
+ @maybe_async
238
+ def get_button_mask(self) -> int:
239
+ self._check_connection()
240
+ return self.transport.get_button_mask()
241
+
242
+ @maybe_async
243
+ def get_button_states(self) -> Dict[str, bool]:
244
+ self._check_connection()
245
+ return self.transport.get_button_states()
246
+
247
+ @maybe_async
248
+ def is_pressed(self, button: MouseButton) -> bool:
249
+ self._check_connection()
250
+ return self.transport.get_button_states().get(button.name.lower(), False)
251
+
252
+ @maybe_async
253
+ def enable_button_monitoring(self, enable: bool = True) -> None:
254
+ self._check_connection()
255
+ self.transport.enable_button_monitoring(enable)
256
+
257
+ @maybe_async
258
+ def set_button_callback(self, callback: Optional[Callable[[MouseButton, bool], None]]) -> None:
259
+ self._check_connection()
260
+ self.transport.set_button_callback(callback)
261
+
262
+ @maybe_async
263
+ def on_connection_change(self, callback: Callable[[bool], None]) -> None:
264
+ self._connection_callbacks.append(callback)
265
+
266
+ @maybe_async
267
+ def remove_connection_callback(self, callback: Callable[[bool], None]) -> None:
268
+ if callback in self._connection_callbacks:
269
+ self._connection_callbacks.remove(callback)
270
+
271
+ @maybe_async
272
+ def click_human_like(self, button: MouseButton, count: int = 1,
273
+ profile: str = "normal", jitter: int = 0) -> None:
274
+ self._check_connection()
275
+
276
+ timing_profiles = {
277
+ "normal": (60, 120, 100, 180),
278
+ "fast": (30, 60, 50, 100),
279
+ "slow": (100, 180, 150, 300),
280
+ "variable": (40, 200, 80, 250),
281
+ "gaming": (20, 40, 30, 60),
282
+ }
283
+
284
+ if profile not in timing_profiles:
285
+ raise ValueError(f"Invalid profile: {profile}")
286
+
287
+ min_down, max_down, min_wait, max_wait = timing_profiles[profile]
288
+
289
+ for i in range(count):
290
+ if jitter > 0:
291
+ dx = random.randint(-jitter, jitter)
292
+ dy = random.randint(-jitter, jitter)
293
+ self.mouse.move(dx, dy)
294
+
295
+ self.mouse.press(button)
296
+ time.sleep(random.uniform(min_down, max_down) / 1000.0)
297
+ self.mouse.release(button)
298
+
299
+ if i < count - 1:
300
+ time.sleep(random.uniform(min_wait, max_wait) / 1000.0)
301
+
302
+ @maybe_async
303
+ def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
304
+ button: MouseButton = MouseButton.LEFT, duration: float = 1.0) -> None:
305
+ self._check_connection()
306
+
307
+ # Move to start position
308
+ self.move(start_x, start_y)
309
+ time.sleep(0.02)
310
+
311
+ # Press button
312
+ self.press(button)
313
+ time.sleep(0.02)
314
+
315
+ # Smooth move to end position
316
+ segments = max(10, int(duration * 30))
317
+ self.move_smooth(end_x - start_x, end_y - start_y, segments)
318
+
319
+ # Release button
320
+ time.sleep(0.02)
321
+ self.release(button)
322
+
323
+ # Context managers for both sync and async
324
+ def __enter__(self):
325
+ if not self.is_connected():
326
+ self.connect()
327
+ return self
328
+
329
+ def __exit__(self, exc_type, exc_val, exc_tb):
330
+ self.disconnect()
331
+
332
+ async def __aenter__(self):
333
+ if not await self.is_connected():
334
+ await self.connect()
335
+ return self
336
+
337
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
338
+ await self.disconnect()
339
+
340
+ # Legacy async methods for backward compatibility
341
+ async def async_connect(self) -> None:
342
+ """Legacy method - use connect() instead"""
343
+ await self.connect()
344
+
345
+ async def async_disconnect(self) -> None:
346
+ """Legacy method - use disconnect() instead"""
347
+ await self.disconnect()
348
+
349
+ async def async_click(self, button: MouseButton) -> None:
350
+ """Legacy method - use click() instead"""
351
+ await self.click(button)
352
+
353
+ async def async_move(self, dx: int, dy: int) -> None:
354
+ """Legacy method - use move() instead"""
355
+ await self.move(dx, dy)
356
+
357
+ async def async_scroll(self, delta: int) -> None:
358
+ """Legacy method - use scroll() instead"""
359
+ await self.scroll(delta)
360
+
361
+
362
+ def create_controller(fallback_com_port: str = "", debug: bool = False,
363
+ send_init: bool = True, auto_reconnect: bool = True,
364
+ override_port: bool = False) -> MakcuController:
365
+ """Create and connect a controller synchronously"""
366
+ makcu = MakcuController(
367
+ fallback_com_port,
368
+ debug=debug,
369
+ send_init=send_init,
370
+ auto_reconnect=auto_reconnect,
371
+ override_port=override_port
372
+ )
373
+ makcu.connect()
374
+ return makcu
375
+
376
+
377
+ async def create_async_controller(fallback_com_port: str = "", debug: bool = False,
378
+ send_init: bool = True, auto_reconnect: bool = True,
379
+ override_port: bool = False) -> MakcuController:
380
+ """Create and connect a controller asynchronously"""
381
+ makcu = MakcuController(
382
+ fallback_com_port,
383
+ debug=debug,
384
+ send_init=send_init,
385
+ auto_reconnect=auto_reconnect,
386
+ override_port=override_port
387
+ )
388
+ await makcu.connect()
377
389
  return makcu