makcu 0.1.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/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ from .controller import MakcuController
2
+ from .enums import MouseButton
3
+ from .errors import MakcuError, MakcuConnectionError
4
+
5
+ def create_controller(debug=False, send_init=True):
6
+ makcu = MakcuController(debug=debug, send_init=send_init)
7
+ makcu.connect()
8
+ return makcu
9
+
10
+ __all__ = [
11
+ "MakcuController",
12
+ "MouseButton",
13
+ "MakcuError",
14
+ "MakcuConnectionError",
15
+ "create_controller",
16
+ ]
makcu/__main__.py ADDED
@@ -0,0 +1,80 @@
1
+ # makcu/__main__.py
2
+
3
+ import sys
4
+ import subprocess
5
+ import webbrowser
6
+ import os
7
+ from makcu import create_controller, MakcuConnectionError
8
+
9
+ def debug_console():
10
+ controller = create_controller()
11
+ transport = controller.transport
12
+
13
+ print("๐Ÿ”ง Makcu Debug Console")
14
+ print("Type a raw command (e.g., km.version()) and press Enter.")
15
+ print("Type 'exit' or 'quit' to leave.")
16
+
17
+ while True:
18
+ try:
19
+ cmd = input(">>> ").strip()
20
+ if cmd.lower() in {"exit", "quit"}:
21
+ break
22
+ if not cmd:
23
+ continue
24
+
25
+ response = transport.send_command(cmd, expect_response=True)
26
+ print(f"{response or '(no response)'}")
27
+
28
+ except Exception as e:
29
+ print(f"โš ๏ธ Error: {e}")
30
+
31
+ controller.disconnect()
32
+ print("Disconnected.")
33
+
34
+ def test_port(port):
35
+ try:
36
+ print(f"Trying to connect to {port} (without init command)...")
37
+ controller = create_controller(send_init=False)
38
+ print(f"โœ… Successfully connected to {port}")
39
+ controller.disconnect()
40
+ except MakcuConnectionError as e:
41
+ print(f"โŒ Failed to connect to {port}: {e}")
42
+ except Exception as e:
43
+ print(f"โŒ Unexpected error: {e}")
44
+
45
+ def run_tests():
46
+ print("๐Ÿงช Running Pytest Suite...")
47
+ subprocess.run([
48
+ sys.executable, "-m", "pytest",
49
+ "--html=latest_pytest.html", "--self-contained-html"
50
+ ])
51
+
52
+ report_path = os.path.abspath("latest_pytest.html")
53
+ print(f"๐Ÿ“„ Opening test report: {report_path}")
54
+ webbrowser.open(f"file://{report_path}")
55
+
56
+ def main():
57
+ args = sys.argv[1:]
58
+
59
+ if not args:
60
+ print("Usage:")
61
+ print(" python -m makcu --debug")
62
+ print(" python -m makcu --testPort COM3")
63
+ print(" python -m makcu --runtest")
64
+ return
65
+
66
+ if args[0] == "--debug":
67
+ debug_console()
68
+ elif args[0] == "--testPort" and len(args) == 2:
69
+ test_port(args[1])
70
+ elif args[0] == "--runtest":
71
+ run_tests()
72
+ else:
73
+ print(f"Unknown command: {' '.join(args)}")
74
+ print("Usage:")
75
+ print(" python -m makcu --debug")
76
+ print(" python -m makcu --testPort COM3")
77
+ print(" python -m makcu --runtest")
78
+
79
+ if __name__ == "__main__":
80
+ main()
makcu/connection.py ADDED
@@ -0,0 +1,277 @@
1
+ import serial
2
+ import threading
3
+ import time
4
+ from serial.tools import list_ports
5
+ from .errors import MakcuConnectionError, MakcuTimeoutError
6
+ from .enums import MouseButton
7
+
8
+ class SerialTransport:
9
+ global fallback_com_port
10
+ baud_change_command = bytearray([0xDE, 0xAD, 0x05, 0x00, 0xA5, 0x00, 0x09, 0x3D, 0x00])
11
+
12
+ button_map = {
13
+ 0: 'left',
14
+ 1: 'right',
15
+ 2: 'middle',
16
+ 3: 'mouse4',
17
+ 4: 'mouse5'
18
+ }
19
+
20
+ def __init__(self, debug=False, send_init=True):
21
+ self._log_messages = []
22
+ self.debug = debug
23
+ self.send_init = send_init
24
+ self._button_callback = None
25
+ self._last_mask = 0
26
+ self._lock = threading.Lock()
27
+ self._is_connected = False
28
+ self._stop_event = threading.Event()
29
+ self._listener_thread = None
30
+ self._button_states = {btn: False for btn in self.button_map.values()}
31
+ self._debounce_time_ms = 50
32
+ self._last_callback_time = {bit: 0 for bit in self.button_map}
33
+ self._pause_listener = False
34
+
35
+ self._button_enum_map = {
36
+ 0: MouseButton.LEFT,
37
+ 1: MouseButton.RIGHT,
38
+ 2: MouseButton.MIDDLE,
39
+ 3: MouseButton.MOUSE4,
40
+ 4: MouseButton.MOUSE5,
41
+ }
42
+
43
+ self.port = self.find_com_port()
44
+ if not self.port:
45
+ raise MakcuConnectionError("Makcu device not found. Please specify a port explicitly.")
46
+
47
+ self.baudrate = 115200
48
+ self.serial = None
49
+ self._current_baud = None
50
+
51
+ def receive_response(self, max_bytes=1024, max_lines=3, sent_command: str = "") -> str:
52
+ lines = []
53
+ try:
54
+ for _ in range(max_lines):
55
+ line = self.serial.readline(max_bytes)
56
+ if not line:
57
+ break
58
+ decoded = line.decode(errors="ignore").strip()
59
+ if decoded:
60
+ lines.append(decoded)
61
+ except Exception as e:
62
+ print(f"[RECV ERROR] {e}")
63
+ return ""
64
+
65
+ command_clean = sent_command.strip()
66
+ if lines:
67
+ lines.pop(-1)
68
+ if command_clean in lines and len(lines) > 1:
69
+ lines.remove(command_clean)
70
+ return "\n".join(lines)
71
+
72
+ def set_debounce_time(self, ms: int):
73
+ self._debounce_time_ms = ms
74
+
75
+ def set_button_callback(self, callback):
76
+ self._button_callback = callback
77
+
78
+ def _log(self, message):
79
+ timestamp = time.strftime("%H:%M:%S")
80
+ entry = f"[{timestamp}] {message}"
81
+ self._log_messages.append(entry)
82
+ if len(self._log_messages) > 20:
83
+ self._log_messages.pop(0)
84
+ print(entry, flush=True)
85
+
86
+ def find_com_port(self):
87
+ global fallback_com_port
88
+ self._log("Searching for CH343 device...")
89
+
90
+ for port in list_ports.comports():
91
+ if "USB-Enhanced-SERIAL CH343" in port.description:
92
+ self._log(f"Device found: {port.device}")
93
+ return port.device
94
+
95
+ if fallback_com_port and "COM" in fallback_com_port:
96
+ self._log(f"CH343 not found. Falling back to specified port: {fallback_com_port}")
97
+ return fallback_com_port
98
+ else:
99
+ self._log("Fallback port is not valid.")
100
+ return None
101
+
102
+
103
+ def _open_serial_port(self, port, baud_rate):
104
+ try:
105
+ self._log(f"Trying to open {port} at {baud_rate} baud.")
106
+ return serial.Serial(port, baud_rate, timeout=0.05)
107
+ except serial.SerialException:
108
+ self._log(f"Failed to open {port} at {baud_rate} baud.")
109
+ return None
110
+
111
+ def _change_baud_to_4M(self):
112
+ if self.serial and self.serial.is_open:
113
+ self._log("Sending baud rate switch command to 4M.")
114
+ self.serial.write(self.baud_change_command)
115
+ self.serial.flush()
116
+ port = self.serial.name
117
+ self.serial.close()
118
+ time.sleep(0.1)
119
+ self.serial = self._open_serial_port(port, 4000000)
120
+ if self.serial:
121
+ self._current_baud = 4000000
122
+ self._log("Switched to 4M baud successfully.")
123
+ return True
124
+ else:
125
+ self._log("Failed to reopen port at 4M baud.")
126
+ return False
127
+
128
+ def connect(self):
129
+ if self._is_connected:
130
+ self._log("Already connected.")
131
+ return
132
+ self.serial = self._open_serial_port(self.port, 115200)
133
+ if not self.serial:
134
+ raise MakcuConnectionError(f"Failed to connect to {self.port} at 115200.")
135
+ self._log(f"Connected to {self.port} at 115200.")
136
+ if not self._change_baud_to_4M():
137
+ raise MakcuConnectionError("Failed to switch to 4M baud.")
138
+ self._is_connected = True
139
+ if self.send_init:
140
+ with self._lock:
141
+ self.serial.write(b"km.buttons(1)\r")
142
+ self.serial.flush()
143
+ self._log("Sent init command: km.buttons(1)")
144
+
145
+ self._stop_event.clear()
146
+ self._listener_thread = threading.Thread(target=self._listen, kwargs={"debug": self.debug}, daemon=True)
147
+ self._listener_thread.start()
148
+
149
+ def disconnect(self):
150
+ if self.send_init:
151
+ self._stop_event.set()
152
+ if self._listener_thread:
153
+ self._listener_thread.join()
154
+ with self._lock:
155
+ if self.serial and self.serial.is_open:
156
+ self.serial.close()
157
+ self.serial = None
158
+ self._is_connected = False
159
+ self._log("Disconnected.")
160
+
161
+ def is_connected(self):
162
+ return self._is_connected
163
+
164
+ def send_command(self, command, expect_response=False):
165
+ time.sleep(0.06)
166
+ if not self._is_connected or not self.serial or not self.serial.is_open:
167
+ raise MakcuConnectionError("Serial connection not open.")
168
+ with self._lock:
169
+ try:
170
+ self._pause_listener = True
171
+ self.serial.reset_input_buffer()
172
+ self.serial.write(command.encode("ascii") + b"\r\n")
173
+ self.serial.flush()
174
+ if expect_response:
175
+ response = self.receive_response(sent_command=command)
176
+ if not response:
177
+ raise MakcuTimeoutError(f"No response from device for command: {command}")
178
+ return response
179
+ finally:
180
+ self._pause_listener = False
181
+
182
+ def get_button_states(self):
183
+ return dict(self._button_states)
184
+
185
+ def get_button_mask(self) -> int:
186
+ mask = 0
187
+ for i, name in self.button_map.items():
188
+ if self._button_states.get(name, False):
189
+ mask |= (1 << i)
190
+ return mask
191
+
192
+ def enable_button_monitoring(self, enable: bool = True):
193
+ self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
194
+
195
+ def catch_button(self, button: str):
196
+ command = {
197
+ "LEFT": "km.catch_ml(0)",
198
+ "RIGHT": "km.catch_mr(0)",
199
+ "MIDDLE": "km.catch_mm(0)",
200
+ "MOUSE4": "km.catch_ms1(0)",
201
+ "MOUSE5": "km.catch_ms2(0)",
202
+ }.get(button.upper())
203
+ if command:
204
+ self.send_command(command)
205
+ else:
206
+ raise ValueError(f"Unsupported button: {button}")
207
+
208
+ def read_captured_clicks(self, button: str) -> int:
209
+ command = {
210
+ "LEFT": "km.catch_ml()",
211
+ "RIGHT": "km.catch_mr()",
212
+ "MIDDLE": "km.catch_mm()",
213
+ "MOUSE4": "km.catch_ms1()",
214
+ "MOUSE5": "km.catch_ms2()",
215
+ }.get(button.upper())
216
+ if command:
217
+ result = self.send_command(command, expect_response=True)
218
+ try:
219
+ return int(result.strip())
220
+ except Exception:
221
+ return 0
222
+ else:
223
+ raise ValueError(f"Unsupported button: {button}")
224
+
225
+ def _listen(self, debug=False):
226
+ self._log("Started listener thread")
227
+ last_value = None
228
+ button_states = {i: False for i in self.button_map}
229
+ self._last_mask = 0
230
+
231
+ while self._is_connected and not self._stop_event.is_set():
232
+ if self._pause_listener:
233
+ time.sleep(0.001)
234
+ continue
235
+ try:
236
+ byte = self.serial.read(1)
237
+ if byte:
238
+ value = byte[0]
239
+ mask = 0
240
+ for bit, name in self.button_map.items():
241
+ is_pressed = bool(value & (1 << bit))
242
+ if is_pressed != button_states[bit]:
243
+ button_states[bit] = is_pressed
244
+ if is_pressed:
245
+ mask |= (1 << bit)
246
+
247
+ for bit, name in self.button_map.items():
248
+ self._button_states[name] = button_states[bit]
249
+
250
+ now = time.time() * 1000
251
+ if self._button_callback and mask != self._last_mask:
252
+ for bit, name in self.button_map.items():
253
+ previous = bool(self._last_mask & (1 << bit))
254
+ current = bool(mask & (1 << bit))
255
+ if previous != current:
256
+ last_time = self._last_callback_time.get(bit, 0)
257
+ if now - last_time >= self._debounce_time_ms:
258
+ button_enum = self._button_enum_map.get(bit)
259
+ if button_enum:
260
+ self._button_callback(button_enum, current)
261
+ self._last_callback_time[bit] = now
262
+
263
+ self._last_mask = mask
264
+
265
+ if debug:
266
+ pressed = [name for bit, name in self.button_map.items() if button_states[bit]]
267
+ button_str = ", ".join(pressed) if pressed else "No buttons pressed"
268
+ self._log(f"Byte: {value} (0x{value:02X}) -> {button_str}")
269
+
270
+ last_value = value
271
+ except serial.SerialException as e:
272
+ if "ClearCommError failed" not in str(e):
273
+ self._log(f"Serial error during listening: {e}")
274
+ break
275
+ time.sleep(0.0001)
276
+
277
+ self._log("Listener thread exiting")
makcu/controller.py ADDED
@@ -0,0 +1,197 @@
1
+ import random
2
+ import time
3
+ from .mouse import Mouse
4
+ from .connection import SerialTransport
5
+ from .errors import MakcuConnectionError
6
+ from .enums import MouseButton
7
+
8
+ class MakcuController:
9
+ def __init__(self, debug=False, send_init=True):
10
+ self.transport = SerialTransport(debug=debug, send_init=send_init)
11
+ self.mouse = Mouse(self.transport)
12
+
13
+ def connect(self):
14
+ self.transport.connect()
15
+
16
+ def disconnect(self):
17
+ self.transport.disconnect()
18
+
19
+ def is_connected(self):
20
+ return self.transport.is_connected()
21
+
22
+ def _check_connection(self):
23
+ if not self.transport.serial or not self.transport.serial.is_open:
24
+ raise MakcuConnectionError("Not connected")
25
+
26
+ def click(self, button: MouseButton):
27
+ self._check_connection()
28
+ self.mouse.press(button)
29
+ self.mouse.release(button)
30
+
31
+ def move(self, dx: int, dy: int):
32
+ self._check_connection()
33
+ self.mouse.move(dx, dy)
34
+
35
+ def scroll(self, delta: int):
36
+ self._check_connection()
37
+ self.mouse.scroll(delta)
38
+
39
+ def move_smooth(self, dx: int, dy: int, segments: int):
40
+ self._check_connection()
41
+ self.mouse.move_smooth(dx, dy, segments)
42
+
43
+ def move_bezier(self, dx: int, dy: int, segments: int, ctrl_x: int, ctrl_y: int):
44
+ self._check_connection()
45
+ self.mouse.move_bezier(dx, dy, segments, ctrl_x, ctrl_y)
46
+
47
+ def lock_mouse_x(self, lock: bool):
48
+ self._check_connection()
49
+ self.mouse.lock_x(lock)
50
+
51
+ def lock_mouse_y(self, lock: bool):
52
+ self._check_connection()
53
+ self.mouse.lock_y(lock)
54
+
55
+ def lock_left(self, lock: bool):
56
+ self._check_connection()
57
+ self.mouse.lock_left(lock)
58
+
59
+ def lock_middle(self, lock: bool):
60
+ self._check_connection()
61
+ self.mouse.lock_middle(lock)
62
+
63
+ def lock_right(self, lock: bool):
64
+ self._check_connection()
65
+ self.mouse.lock_right(lock)
66
+
67
+ def lock_side1(self, lock: bool):
68
+ self._check_connection()
69
+ self.mouse.lock_side1(lock)
70
+
71
+ def lock_side2(self, lock: bool):
72
+ self._check_connection()
73
+ self.mouse.lock_side2(lock)
74
+
75
+ def spoof_serial(self, serial: str):
76
+ self._check_connection()
77
+ self.mouse.spoof_serial(serial)
78
+
79
+ def reset_serial(self):
80
+ self._check_connection()
81
+ self.mouse.reset_serial()
82
+
83
+ def get_device_info(self):
84
+ self._check_connection()
85
+ return self.mouse.get_device_info()
86
+
87
+ def get_firmware_version(self):
88
+ self._check_connection()
89
+ return self.mouse.get_firmware_version()
90
+
91
+ def get_button_mask(self) -> int:
92
+ self._check_connection()
93
+ return self.transport.get_button_mask()
94
+
95
+ def is_locked(self, target: str) -> bool:
96
+ self._check_connection()
97
+ return self.mouse.is_locked(target)
98
+
99
+ def is_button_locked(self, button: MouseButton) -> bool:
100
+ self._check_connection()
101
+ return self.mouse.is_button_locked(button)
102
+
103
+ def capture(self, button: MouseButton):
104
+ self._check_connection()
105
+ self.mouse.begin_capture(button.name)
106
+
107
+ def get_captured_clicks(self, button: MouseButton) -> int:
108
+ self._check_connection()
109
+ return self.mouse.stop_capturing_clicks(button.name)
110
+
111
+
112
+ def click_human_like(self, button: MouseButton, count: int = 1,
113
+ profile: str = "normal", jitter: int = 0):
114
+ self._check_connection()
115
+
116
+ timing_profiles = {
117
+ "normal": {"min_down": 60, "max_down": 120, "min_wait": 100, "max_wait": 180},
118
+ "fast": {"min_down": 30, "max_down": 60, "min_wait": 50, "max_wait": 100},
119
+ "slow": {"min_down": 100, "max_down": 180, "min_wait": 150, "max_wait": 300},
120
+ }
121
+
122
+ if profile not in timing_profiles:
123
+ raise ValueError(f"Invalid profile: {profile}. Choose from {list(timing_profiles.keys())}")
124
+
125
+ t = timing_profiles[profile]
126
+
127
+ for _ in range(count):
128
+ if jitter > 0:
129
+ dx = random.randint(-jitter, jitter)
130
+ dy = random.randint(-jitter, jitter)
131
+ self.mouse.move(dx, dy)
132
+
133
+ self.mouse.press(button)
134
+ time.sleep(random.uniform(t["min_down"], t["max_down"]) / 1000.0)
135
+ self.mouse.release(button)
136
+ time.sleep(random.uniform(t["min_wait"], t["max_wait"]) / 1000.0)
137
+
138
+ def enable_button_monitoring(self, enable: bool = True):
139
+ self._check_connection()
140
+ self.transport.enable_button_monitoring(enable)
141
+
142
+ def set_button_callback(self, callback):
143
+ self._check_connection()
144
+ self.transport.set_button_callback(callback)
145
+
146
+ def get_all_lock_states(self) -> dict:
147
+ self._check_connection()
148
+ return self.mouse.get_all_lock_states()
149
+
150
+ def set_callback_debounce_time(self, ms: int):
151
+ self._check_connection()
152
+ self.transport.set_debounce_time(ms)
153
+
154
+ def start_capturing_clicks(self, button: str):
155
+ self._check_connection()
156
+ self.transport._capture_counts[button] = 0
157
+ self.transport._capture_active[button] = True
158
+ self.transport._capture_last_state[button] = None
159
+ self.mouse.begin_capture(button)
160
+
161
+ def stop_capturing_clicks(self, button: str) -> int:
162
+ self._check_connection()
163
+ self.transport._capture_active[button] = False
164
+ return self.mouse.stop_capturing_clicks(button)
165
+
166
+ def press(self, button: MouseButton):
167
+ self._check_connection()
168
+ self.mouse.press(button)
169
+
170
+ def release(self, button: MouseButton):
171
+ self._check_connection()
172
+ self.mouse.release(button)
173
+
174
+ def set_port(self, port: str):
175
+ import makcu.connection
176
+ makcu.connection.fallback_com_port = port
177
+
178
+ def get_button_states(self) -> dict:
179
+ self._check_connection()
180
+ return self.transport.get_button_states()
181
+
182
+ def is_button_pressed(self, button: MouseButton) -> bool:
183
+ self._check_connection()
184
+ return self.transport.get_button_states().get(button.name.lower(), False)
185
+
186
+ def get_raw_mask(self) -> int:
187
+ self._check_connection()
188
+ return self.transport.get_button_mask()
189
+
190
+ def get_virtual_bit_mapping(self) -> dict:
191
+ return {
192
+ "LEFT": 0,
193
+ "RIGHT": 1,
194
+ "MIDDLE": 2,
195
+ "MOUSE4": 3,
196
+ "MOUSE5": 4
197
+ }
makcu/enums.py ADDED
@@ -0,0 +1,8 @@
1
+ from enum import Enum
2
+
3
+ class MouseButton(Enum):
4
+ LEFT = 0
5
+ RIGHT = 1
6
+ MIDDLE = 2
7
+ MOUSE4 = 3
8
+ MOUSE5 = 4
makcu/errors.py ADDED
@@ -0,0 +1,19 @@
1
+ class MakcuError(Exception):
2
+ """Base exception for all Makcu-related errors."""
3
+ pass
4
+
5
+ class MakcuConnectionError(MakcuError):
6
+ """Raised when the device connection fails."""
7
+ pass
8
+
9
+ class MakcuCommandError(MakcuError):
10
+ """Raised when a device command is invalid, rejected, or fails."""
11
+ pass
12
+
13
+ class MakcuTimeoutError(MakcuError):
14
+ """Raised when the device does not respond in time."""
15
+ pass
16
+
17
+ class MakcuResponseError(MakcuError):
18
+ """Raised when the response from the device is malformed or unexpected."""
19
+ pass