makcu 2.1.3__py3-none-any.whl → 2.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/README.md +406 -0
- makcu/__init__.py +22 -378
- makcu/__main__.py +30 -16
- makcu/conftest.py +1 -27
- makcu/connection.py +141 -48
- makcu/controller.py +23 -1
- makcu/py.typed +1 -1
- makcu/test_suite.py +29 -15
- makcu-2.2.1.dist-info/METADATA +1116 -0
- makcu-2.2.1.dist-info/RECORD +17 -0
- makcu-2.2.1.dist-info/licenses/LICENSE +674 -0
- makcu-2.1.3.dist-info/METADATA +0 -32
- makcu-2.1.3.dist-info/RECORD +0 -15
- {makcu-2.1.3.dist-info → makcu-2.2.1.dist-info}/WHEEL +0 -0
- {makcu-2.1.3.dist-info → makcu-2.2.1.dist-info}/top_level.txt +0 -0
makcu/__init__.py
CHANGED
@@ -1,378 +1,22 @@
|
|
1
|
-
import
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
from .
|
8
|
-
from .errors import MakcuConnectionError
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
1
|
+
from .controller import (
|
2
|
+
MakcuController,
|
3
|
+
create_controller,
|
4
|
+
create_async_controller,
|
5
|
+
maybe_async
|
6
|
+
)
|
7
|
+
from .enums import MouseButton
|
8
|
+
from .errors import MakcuConnectionError
|
9
|
+
|
10
|
+
# Version info
|
11
|
+
__version__ = "2.2.1"
|
12
|
+
__author__ = "SleepyTotem"
|
13
|
+
|
14
|
+
# Main exports
|
15
|
+
__all__ = [
|
16
|
+
'MakcuController',
|
17
|
+
'MouseButton',
|
18
|
+
'MakcuConnectionError',
|
19
|
+
'create_controller',
|
20
|
+
'create_async_controller',
|
21
|
+
'maybe_async'
|
22
|
+
]
|
makcu/__main__.py
CHANGED
@@ -8,11 +8,12 @@ from makcu import create_controller, MakcuConnectionError, MakcuController
|
|
8
8
|
import json
|
9
9
|
import re
|
10
10
|
import subprocess
|
11
|
+
import makcu
|
11
12
|
|
12
|
-
makcu_version =
|
13
|
+
makcu_version = makcu.__version__
|
13
14
|
|
14
15
|
def debug_console():
|
15
|
-
controller = create_controller()
|
16
|
+
controller = create_controller(debug=True)
|
16
17
|
transport = controller.transport
|
17
18
|
|
18
19
|
print("🔧 Makcu Debug Console")
|
@@ -90,42 +91,48 @@ def find_writable_directory() -> Path:
|
|
90
91
|
def parse_html_results(html_file: Path) -> Tuple[List[Tuple[str, str, int]], int]:
|
91
92
|
if not html_file.exists():
|
92
93
|
raise FileNotFoundError(f"HTML report not found: {html_file}")
|
93
|
-
|
94
|
+
|
94
95
|
with open(html_file, 'r', encoding='utf-8') as f:
|
95
96
|
content = f.read()
|
96
|
-
|
97
|
+
|
97
98
|
match = re.search(r'data-jsonblob="([^"]*)"', content)
|
98
99
|
if not match:
|
99
100
|
raise ValueError("Could not find JSON data in HTML report")
|
100
|
-
|
101
|
+
|
101
102
|
json_str = match.group(1)
|
102
103
|
json_str = json_str.replace('"', '"').replace('&#x27;', "'").replace('&', '&')
|
103
|
-
|
104
|
+
|
104
105
|
try:
|
105
106
|
data = json.loads(json_str)
|
106
107
|
except json.JSONDecodeError as e:
|
107
108
|
raise ValueError(f"Failed to parse JSON data: {e}")
|
108
|
-
|
109
|
+
|
109
110
|
test_results = []
|
110
111
|
total_ms = 0
|
111
|
-
|
112
|
+
|
112
113
|
skip_tests = {'test_connect_to_port'}
|
113
|
-
|
114
|
+
|
114
115
|
for test_id, test_data_list in data.get('tests', {}).items():
|
115
116
|
test_name = test_id.split('::')[-1]
|
117
|
+
|
118
|
+
# Skip tests that are in skip_tests
|
116
119
|
if test_name in skip_tests:
|
117
120
|
continue
|
118
|
-
|
121
|
+
|
119
122
|
for test_data in test_data_list:
|
120
123
|
status = test_data.get('result', 'UNKNOWN')
|
121
124
|
duration_str = test_data.get('duration', '0 ms')
|
122
|
-
|
125
|
+
|
123
126
|
duration_match = re.search(r'(\d+)\s*ms', duration_str)
|
124
127
|
duration_ms = int(duration_match.group(1)) if duration_match else 0
|
125
|
-
total_ms += duration_ms
|
126
128
|
|
129
|
+
# Always add test to results
|
127
130
|
test_results.append((test_name, status, duration_ms))
|
128
|
-
|
131
|
+
|
132
|
+
# Only add time to total if it's not a cleanup test
|
133
|
+
if 'cleanup' not in test_name.lower():
|
134
|
+
total_ms += duration_ms
|
135
|
+
|
129
136
|
return test_results, total_ms
|
130
137
|
|
131
138
|
def run_tests() -> NoReturn:
|
@@ -243,8 +250,12 @@ def run_tests() -> NoReturn:
|
|
243
250
|
display_name = test_name.replace("test_", "").replace("_", " ").title()
|
244
251
|
|
245
252
|
if status.upper() == "PASSED":
|
246
|
-
|
247
|
-
|
253
|
+
if display_name.lower().startswith("cleanup"):
|
254
|
+
status_text = ""
|
255
|
+
passed += 1
|
256
|
+
else:
|
257
|
+
status_text = "[green]✅ PASSED[/green]"
|
258
|
+
passed += 1
|
248
259
|
elif status.upper() == "FAILED":
|
249
260
|
status_text = "[red]❌ FAILED[/red]"
|
250
261
|
failed += 1
|
@@ -262,7 +273,10 @@ def run_tests() -> NoReturn:
|
|
262
273
|
elif duration_ms <= 10:
|
263
274
|
perf = "[yellow]Good[/yellow]"
|
264
275
|
elif duration_ms > 0:
|
265
|
-
|
276
|
+
if display_name.lower().startswith("cleanup"):
|
277
|
+
perf = ""
|
278
|
+
else:
|
279
|
+
perf = "[red]🐌 Needs work[/red]"
|
266
280
|
else:
|
267
281
|
perf = "-"
|
268
282
|
|
makcu/conftest.py
CHANGED
@@ -3,32 +3,6 @@ import time
|
|
3
3
|
from makcu import MakcuController, MouseButton
|
4
4
|
|
5
5
|
@pytest.fixture(scope="session")
|
6
|
-
def makcu(
|
6
|
+
def makcu():
|
7
7
|
ctrl = MakcuController(fallback_com_port="COM1", debug=False)
|
8
|
-
|
9
|
-
def cleanup():
|
10
|
-
if ctrl.is_connected():
|
11
|
-
|
12
|
-
time.sleep(0.1)
|
13
|
-
|
14
|
-
ctrl.lock_left(False)
|
15
|
-
ctrl.lock_right(False)
|
16
|
-
ctrl.lock_middle(False)
|
17
|
-
ctrl.lock_side1(False)
|
18
|
-
ctrl.lock_side2(False)
|
19
|
-
ctrl.lock_x(False)
|
20
|
-
ctrl.lock_y(False)
|
21
|
-
|
22
|
-
ctrl.release(MouseButton.LEFT)
|
23
|
-
ctrl.release(MouseButton.RIGHT)
|
24
|
-
ctrl.release(MouseButton.MIDDLE)
|
25
|
-
ctrl.release(MouseButton.MOUSE4)
|
26
|
-
ctrl.release(MouseButton.MOUSE5)
|
27
|
-
|
28
|
-
ctrl.enable_button_monitoring(False)
|
29
|
-
|
30
|
-
ctrl.disconnect()
|
31
|
-
|
32
|
-
request.addfinalizer(cleanup)
|
33
|
-
|
34
8
|
return ctrl
|