makcu 0.1.4__py3-none-any.whl → 0.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/__init__.py +71 -9
- makcu/__main__.py +254 -37
- makcu/conftest.py +27 -17
- makcu/connection.py +427 -231
- makcu/controller.py +348 -86
- makcu/makcu.pyi +13 -0
- makcu/mouse.py +219 -116
- makcu/py.typed +2 -0
- makcu/test_suite.py +112 -34
- makcu-0.2.0.dist-info/METADATA +1141 -0
- makcu-0.2.0.dist-info/RECORD +16 -0
- makcu-0.1.4.dist-info/METADATA +0 -274
- makcu-0.1.4.dist-info/RECORD +0 -14
- {makcu-0.1.4.dist-info → makcu-0.2.0.dist-info}/WHEEL +0 -0
- {makcu-0.1.4.dist-info → makcu-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {makcu-0.1.4.dist-info → makcu-0.2.0.dist-info}/top_level.txt +0 -0
makcu/__init__.py
CHANGED
@@ -1,16 +1,78 @@
|
|
1
|
-
|
1
|
+
"""
|
2
|
+
Makcu Python Library v2.0
|
3
|
+
|
4
|
+
High-performance library for controlling Makcu devices with async support,
|
5
|
+
zero-delay command execution, and automatic reconnection.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import List
|
9
|
+
|
10
|
+
# Import main components
|
11
|
+
from .controller import MakcuController, create_controller, create_async_controller
|
2
12
|
from .enums import MouseButton
|
3
|
-
from .errors import
|
13
|
+
from .errors import (
|
14
|
+
MakcuError,
|
15
|
+
MakcuConnectionError,
|
16
|
+
MakcuCommandError,
|
17
|
+
MakcuTimeoutError,
|
18
|
+
MakcuResponseError
|
19
|
+
)
|
4
20
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
21
|
+
# Version info
|
22
|
+
__version__: str = "2.0.0"
|
23
|
+
__author__: str = "SleepyTotem"
|
24
|
+
__license__: str = "GPL"
|
9
25
|
|
10
|
-
|
26
|
+
# Public API
|
27
|
+
__all__: List[str] = [
|
28
|
+
# Main controller
|
11
29
|
"MakcuController",
|
30
|
+
"create_controller",
|
31
|
+
"create_async_controller",
|
32
|
+
|
33
|
+
# Enums
|
12
34
|
"MouseButton",
|
35
|
+
|
36
|
+
# Errors
|
13
37
|
"MakcuError",
|
14
38
|
"MakcuConnectionError",
|
15
|
-
"
|
16
|
-
|
39
|
+
"MakcuCommandError",
|
40
|
+
"MakcuTimeoutError",
|
41
|
+
"MakcuResponseError",
|
42
|
+
]
|
43
|
+
|
44
|
+
# Convenience imports for backward compatibility
|
45
|
+
from .controller import MakcuController as Controller
|
46
|
+
|
47
|
+
# Package metadata
|
48
|
+
__doc__ = """
|
49
|
+
Makcu Python Library provides a high-performance interface for controlling
|
50
|
+
Makcu USB devices. Features include:
|
51
|
+
|
52
|
+
- Full async/await support for modern Python applications
|
53
|
+
- Zero-delay command execution with intelligent tracking
|
54
|
+
- Automatic device reconnection on disconnect
|
55
|
+
- Human-like mouse movement and clicking patterns
|
56
|
+
- Comprehensive button and axis locking
|
57
|
+
- Real-time button event monitoring
|
58
|
+
|
59
|
+
Quick Start:
|
60
|
+
>>> from makcu import create_controller, MouseButton
|
61
|
+
>>> makcu = create_controller()
|
62
|
+
>>> makcu.click(MouseButton.LEFT)
|
63
|
+
>>> makcu.move(100, 50)
|
64
|
+
>>> makcu.disconnect()
|
65
|
+
|
66
|
+
Async Usage:
|
67
|
+
>>> import asyncio
|
68
|
+
>>> from makcu import create_async_controller, MouseButton
|
69
|
+
>>>
|
70
|
+
>>> async def main():
|
71
|
+
... async with await create_async_controller() as makcu:
|
72
|
+
... await makcu.click(MouseButton.LEFT)
|
73
|
+
... await makcu.move(100, 50)
|
74
|
+
>>>
|
75
|
+
>>> asyncio.run(main())
|
76
|
+
|
77
|
+
For more information, visit: https://github.com/SleepyTotem/makcu-py-lib
|
78
|
+
"""
|
makcu/__main__.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
import sys
|
2
|
-
import webbrowser
|
3
2
|
import os
|
4
3
|
from pathlib import Path
|
4
|
+
from typing import List, NoReturn
|
5
5
|
import pytest
|
6
|
-
|
6
|
+
import time
|
7
|
+
from makcu import create_controller, MakcuConnectionError, MakcuController
|
8
|
+
import json
|
9
|
+
import re
|
7
10
|
|
8
11
|
def debug_console():
|
9
12
|
controller = create_controller()
|
@@ -13,6 +16,8 @@ def debug_console():
|
|
13
16
|
print("Type a raw command (e.g., km.version()) and press Enter.")
|
14
17
|
print("Type 'exit' or 'quit' to leave.")
|
15
18
|
|
19
|
+
command_counter = 0
|
20
|
+
|
16
21
|
while True:
|
17
22
|
try:
|
18
23
|
cmd = input(">>> ").strip()
|
@@ -21,8 +26,21 @@ def debug_console():
|
|
21
26
|
if not cmd:
|
22
27
|
continue
|
23
28
|
|
29
|
+
command_counter += 1
|
30
|
+
|
31
|
+
# Send command and expect response for most commands
|
24
32
|
response = transport.send_command(cmd, expect_response=True)
|
25
|
-
|
33
|
+
|
34
|
+
# Handle the response properly
|
35
|
+
if response and response.strip():
|
36
|
+
# If response is just the command echoed back, that means success
|
37
|
+
if response.strip() == cmd:
|
38
|
+
print(f"{cmd}")
|
39
|
+
else:
|
40
|
+
# This is actual response data (like "km.MAKCU" for version)
|
41
|
+
print(f"{response}")
|
42
|
+
else:
|
43
|
+
print("(no response)")
|
26
44
|
|
27
45
|
except Exception as e:
|
28
46
|
print(f"⚠️ Error: {e}")
|
@@ -30,48 +48,247 @@ def debug_console():
|
|
30
48
|
controller.disconnect()
|
31
49
|
print("Disconnected.")
|
32
50
|
|
33
|
-
def test_port(port):
|
51
|
+
def test_port(port: str) -> None:
|
34
52
|
try:
|
35
|
-
print(f"Trying to connect to {port}
|
36
|
-
|
37
|
-
|
38
|
-
|
53
|
+
print(f"Trying to connect to {port}...")
|
54
|
+
makcu = MakcuController(fallback_com_port=port, send_init=False, override_port=True)
|
55
|
+
makcu.connect()
|
56
|
+
if makcu.is_connected:
|
57
|
+
print(f"✅ Successfully connected to {port}.")
|
58
|
+
makcu.disconnect()
|
39
59
|
except MakcuConnectionError as e:
|
40
|
-
|
60
|
+
if "FileNotFoundError" in str(e):
|
61
|
+
print(f"❌ Port {port} does not exist. Please check the port name.")
|
62
|
+
else:
|
63
|
+
print(f"❌ Failed to connect to {port}: ")
|
41
64
|
except Exception as e:
|
42
65
|
print(f"❌ Unexpected error: {e}")
|
43
66
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
"
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
67
|
+
def parse_html_results(html_file: Path):
|
68
|
+
"""Parse test results from the pytest HTML report"""
|
69
|
+
if not html_file.exists():
|
70
|
+
raise FileNotFoundError(f"HTML report not found: {html_file}")
|
71
|
+
|
72
|
+
with open(html_file, 'r', encoding='utf-8') as f:
|
73
|
+
content = f.read()
|
74
|
+
|
75
|
+
# Extract the JSON data from the HTML file
|
76
|
+
match = re.search(r'data-jsonblob="([^"]*)"', content)
|
77
|
+
if not match:
|
78
|
+
raise ValueError("Could not find JSON data in HTML report")
|
79
|
+
|
80
|
+
# Decode HTML entities in the JSON string
|
81
|
+
json_str = match.group(1)
|
82
|
+
json_str = json_str.replace('"', '"').replace(''', "'").replace('&', '&')
|
83
|
+
|
84
|
+
try:
|
85
|
+
data = json.loads(json_str)
|
86
|
+
except json.JSONDecodeError as e:
|
87
|
+
raise ValueError(f"Failed to parse JSON data: {e}")
|
88
|
+
|
89
|
+
test_results = []
|
90
|
+
total_ms = 0
|
91
|
+
|
92
|
+
# Filter out the connect_to_port test from display
|
93
|
+
skip_tests = {'test_connect_to_port'}
|
94
|
+
|
95
|
+
for test_id, test_data_list in data.get('tests', {}).items():
|
96
|
+
test_name = test_id.split('::')[-1] # Get just the test function name
|
97
|
+
|
98
|
+
# Skip connection test from display
|
99
|
+
if test_name in skip_tests:
|
100
|
+
continue
|
101
|
+
|
102
|
+
for test_data in test_data_list:
|
103
|
+
status = test_data.get('result', 'UNKNOWN')
|
104
|
+
duration_str = test_data.get('duration', '0 ms')
|
105
|
+
|
106
|
+
# Parse duration (format: "X ms")
|
107
|
+
duration_match = re.search(r'(\d+)\s*ms', duration_str)
|
108
|
+
duration_ms = int(duration_match.group(1)) if duration_match else 0
|
109
|
+
total_ms += duration_ms
|
110
|
+
|
111
|
+
test_results.append((test_name, status, duration_ms))
|
112
|
+
|
113
|
+
return test_results, total_ms
|
65
114
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
115
|
+
def run_tests() -> NoReturn:
|
116
|
+
"""Run tests with beautiful console output"""
|
117
|
+
try:
|
118
|
+
from rich.console import Console
|
119
|
+
from rich.table import Table
|
120
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
|
121
|
+
from rich.panel import Panel
|
122
|
+
from rich.align import Align
|
123
|
+
from rich import print as rprint
|
124
|
+
from rich.text import Text
|
125
|
+
import subprocess
|
126
|
+
|
127
|
+
console = Console()
|
128
|
+
|
129
|
+
header = Panel.fit(
|
130
|
+
"[bold cyan]🧪 Makcu Test Suite v2.0[/bold cyan]\n[dim]High-Performance Python Library[/dim]",
|
131
|
+
border_style="bright_blue"
|
132
|
+
)
|
133
|
+
console.print(Align.center(header))
|
134
|
+
console.print()
|
135
|
+
|
136
|
+
package_dir: Path = Path(__file__).resolve().parent
|
137
|
+
test_file: Path = package_dir / "test_suite.py"
|
138
|
+
html_file: Path = package_dir.parent / "latest_pytest.html"
|
139
|
+
|
140
|
+
start_time = time.time()
|
141
|
+
|
142
|
+
with Progress(
|
143
|
+
SpinnerColumn(),
|
144
|
+
TextColumn("[progress.description]{task.description}"),
|
145
|
+
BarColumn(),
|
146
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
147
|
+
TimeElapsedColumn(),
|
148
|
+
console=console,
|
149
|
+
transient=True
|
150
|
+
) as progress:
|
151
|
+
task = progress.add_task("[cyan]Running tests...", total=100)
|
152
|
+
|
153
|
+
# Run pytest with minimal output, generating HTML report
|
154
|
+
result = subprocess.run(
|
155
|
+
[
|
156
|
+
sys.executable, "-m", "pytest",
|
157
|
+
str(test_file),
|
158
|
+
"--rootdir", str(package_dir),
|
159
|
+
"-q", # Quiet mode
|
160
|
+
"--tb=no", # No traceback
|
161
|
+
"--html", str(html_file),
|
162
|
+
"--self-contained-html"
|
163
|
+
],
|
164
|
+
stdout=subprocess.DEVNULL, # Hide stdout completely
|
165
|
+
stderr=subprocess.DEVNULL, # Hide stderr completely
|
166
|
+
text=True
|
167
|
+
)
|
168
|
+
|
169
|
+
progress.update(task, completed=100)
|
170
|
+
|
171
|
+
# Parse results from HTML file
|
172
|
+
try:
|
173
|
+
test_results, total_ms = parse_html_results(html_file)
|
174
|
+
except (FileNotFoundError, ValueError) as e:
|
175
|
+
console.print(f"[red]❌ Failed to parse test results: {e}[/red]")
|
176
|
+
console.print(f"[yellow]⚠️ pytest exit code: {result.returncode}[/yellow]")
|
177
|
+
sys.exit(1)
|
178
|
+
|
179
|
+
elapsed_time = time.time() - start_time
|
180
|
+
|
181
|
+
# Table rendering
|
182
|
+
table = Table(title="[bold]Test Results[/bold]", show_header=True, header_style="bold magenta")
|
183
|
+
table.add_column("Test", style="cyan", no_wrap=True)
|
184
|
+
table.add_column("Status", justify="center")
|
185
|
+
table.add_column("Time", justify="right", style="yellow")
|
186
|
+
table.add_column("Performance", justify="center")
|
187
|
+
|
188
|
+
passed = failed = skipped = 0
|
189
|
+
|
190
|
+
for test_name, status, duration_ms in test_results:
|
191
|
+
display_name = test_name.replace("test_", "").replace("_", " ").title()
|
192
|
+
|
193
|
+
if status.upper() == "PASSED":
|
194
|
+
status_text = "[green]✅ PASSED[/green]"
|
195
|
+
passed += 1
|
196
|
+
elif status.upper() == "FAILED":
|
197
|
+
status_text = "[red]❌ FAILED[/red]"
|
198
|
+
failed += 1
|
199
|
+
elif status.upper() == "SKIPPED":
|
200
|
+
status_text = "[yellow]⏭️ SKIPPED[/yellow]"
|
201
|
+
skipped += 1
|
202
|
+
else:
|
203
|
+
status_text = status
|
204
|
+
|
205
|
+
time_str = f"{duration_ms}ms" if duration_ms else "-"
|
206
|
+
if duration_ms < 3:
|
207
|
+
perf = "[green]⚡ Excellent[/green]"
|
208
|
+
elif duration_ms < 5:
|
209
|
+
perf = "[cyan]🚀 Great[/cyan]"
|
210
|
+
elif duration_ms < 10:
|
211
|
+
perf = "[yellow]👍 Good[/yellow]"
|
212
|
+
elif duration_ms > 0:
|
213
|
+
perf = "[red]🐌 Needs work[/red]"
|
214
|
+
else:
|
215
|
+
perf = "-"
|
216
|
+
|
217
|
+
table.add_row(display_name, status_text, time_str, perf)
|
218
|
+
|
219
|
+
console.print("\n")
|
220
|
+
console.print(table)
|
221
|
+
console.print()
|
222
|
+
|
223
|
+
summary = Table.grid(padding=1)
|
224
|
+
summary.add_column(style="bold cyan", justify="right")
|
225
|
+
summary.add_column(justify="left")
|
226
|
+
summary.add_row("Total Tests:", str(len(test_results)))
|
227
|
+
summary.add_row("Passed:", f"[green]{passed}[/green]")
|
228
|
+
summary.add_row("Failed:", f"[red]{failed}[/red]" if failed else str(failed))
|
229
|
+
summary.add_row("Skipped:", f"[yellow]{skipped}[/yellow]" if skipped else str(skipped))
|
230
|
+
summary.add_row("Total Time:", f"{elapsed_time:.2f}s")
|
231
|
+
summary.add_row("Avg Time/Test:", f"{total_ms/len(test_results):.1f}ms" if test_results else "0ms")
|
232
|
+
|
233
|
+
console.print(Align.center(Panel(summary, title="[bold]Summary[/bold]", border_style="blue", expand=False)))
|
234
|
+
console.print()
|
235
|
+
|
236
|
+
if test_results:
|
237
|
+
avg_time = total_ms / len(test_results)
|
238
|
+
if avg_time < 3:
|
239
|
+
perf_text = Text("Performance: ELITE - Ready for 360Hz+ gaming!", style="bold bright_green")
|
240
|
+
elif avg_time < 5:
|
241
|
+
perf_text = Text("Performance: EXCELLENT - Ready for 240Hz+ gaming!", style="bold green")
|
242
|
+
elif avg_time < 10:
|
243
|
+
perf_text = Text("Performance: GREAT - Ready for 144Hz gaming!", style="bold cyan")
|
244
|
+
else:
|
245
|
+
perf_text = Text("Performance: GOOD - Suitable for standard gaming", style="bold yellow")
|
246
|
+
else:
|
247
|
+
perf_text = Text("⚠️ No test results parsed. Check your test suite.", style="bold red")
|
248
|
+
|
249
|
+
console.print(Align.center(Panel(perf_text, border_style="green")))
|
250
|
+
sys.exit(0 if failed == 0 else 1)
|
251
|
+
|
252
|
+
except ImportError:
|
253
|
+
print("📦 Rich not installed. Install it via `pip install rich` for enhanced output.")
|
254
|
+
print("\nFallback to raw pytest output...\n")
|
255
|
+
|
256
|
+
package_dir: Path = Path(__file__).resolve().parent
|
257
|
+
test_file: Path = package_dir / "test_suite.py"
|
258
|
+
html_file: Path = package_dir.parent / "latest_pytest.html"
|
259
|
+
|
260
|
+
result = pytest.main([
|
261
|
+
str(test_file),
|
262
|
+
"--rootdir", str(package_dir),
|
263
|
+
"-q",
|
264
|
+
"--tb=no",
|
265
|
+
"--html", str(html_file),
|
266
|
+
"--self-contained-html"
|
267
|
+
])
|
268
|
+
|
269
|
+
# Try to parse HTML results even in fallback mode
|
270
|
+
try:
|
271
|
+
test_results, total_ms = parse_html_results(html_file)
|
272
|
+
passed = sum(1 for _, status, _ in test_results if status.upper() == "PASSED")
|
273
|
+
failed = sum(1 for _, status, _ in test_results if status.upper() == "FAILED")
|
274
|
+
skipped = sum(1 for _, status, _ in test_results if status.upper() == "SKIPPED")
|
275
|
+
|
276
|
+
print(f"\n📊 Results: {passed} passed, {failed} failed, {skipped} skipped")
|
277
|
+
if test_results:
|
278
|
+
avg_time = total_ms / len(test_results)
|
279
|
+
print(f"⏱️ Average time per test: {avg_time:.1f}ms")
|
280
|
+
except (FileNotFoundError, ValueError):
|
281
|
+
print("\n⚠️ Could not parse HTML results for summary")
|
282
|
+
|
283
|
+
if result != 0:
|
284
|
+
print("\n❌ Some tests failed.")
|
285
|
+
else:
|
286
|
+
print("\n✅ All tests passed.")
|
70
287
|
|
71
|
-
|
288
|
+
sys.exit(result)
|
72
289
|
|
73
|
-
def main():
|
74
|
-
args = sys.argv[1:]
|
290
|
+
def main() -> None:
|
291
|
+
args: List[str] = sys.argv[1:]
|
75
292
|
|
76
293
|
if not args:
|
77
294
|
print("Usage:")
|
makcu/conftest.py
CHANGED
@@ -1,22 +1,32 @@
|
|
1
1
|
import pytest
|
2
|
-
from makcu import create_controller
|
3
2
|
import time
|
3
|
+
from makcu import MakcuController, MouseButton
|
4
4
|
|
5
5
|
@pytest.fixture(scope="session")
|
6
|
-
def makcu():
|
7
|
-
|
8
|
-
|
9
|
-
ctrl.disconnect()
|
10
|
-
time.sleep(0.2)
|
6
|
+
def makcu(request):
|
7
|
+
"""Session-scoped fixture with final cleanup at end of all tests"""
|
8
|
+
ctrl = MakcuController(fallback_com_port="COM1", debug=False)
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
10
|
+
def cleanup():
|
11
|
+
if ctrl.is_connected():
|
12
|
+
ctrl.lock_left(False)
|
13
|
+
ctrl.lock_right(False)
|
14
|
+
ctrl.lock_middle(False)
|
15
|
+
ctrl.lock_side1(False)
|
16
|
+
ctrl.lock_side2(False)
|
17
|
+
ctrl.lock_x(False)
|
18
|
+
ctrl.lock_y(False)
|
19
|
+
|
20
|
+
ctrl.release(MouseButton.LEFT)
|
21
|
+
ctrl.release(MouseButton.RIGHT)
|
22
|
+
ctrl.release(MouseButton.MIDDLE)
|
23
|
+
ctrl.release(MouseButton.MOUSE4)
|
24
|
+
ctrl.release(MouseButton.MOUSE5)
|
25
|
+
|
26
|
+
ctrl.enable_button_monitoring(False)
|
27
|
+
|
28
|
+
ctrl.disconnect()
|
29
|
+
|
30
|
+
request.addfinalizer(cleanup)
|
31
|
+
|
32
|
+
return ctrl
|