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