makcu 0.1.2__py3-none-any.whl → 0.1.4__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/__main__.py +6 -4
- makcu/connection.py +109 -90
- makcu/controller.py +19 -24
- makcu/mouse.py +112 -69
- makcu/test_suite.py +10 -27
- {makcu-0.1.2.dist-info → makcu-0.1.4.dist-info}/METADATA +34 -70
- makcu-0.1.4.dist-info/RECORD +14 -0
- makcu-0.1.2.dist-info/RECORD +0 -14
- {makcu-0.1.2.dist-info → makcu-0.1.4.dist-info}/WHEEL +0 -0
- {makcu-0.1.2.dist-info → makcu-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {makcu-0.1.2.dist-info → makcu-0.1.4.dist-info}/top_level.txt +0 -0
makcu/__main__.py
CHANGED
@@ -33,7 +33,7 @@ def debug_console():
|
|
33
33
|
def test_port(port):
|
34
34
|
try:
|
35
35
|
print(f"Trying to connect to {port} (without init command)...")
|
36
|
-
controller = create_controller(send_init=False)
|
36
|
+
controller = create_controller(fallback_com_port=port, send_init=False)
|
37
37
|
print(f"✅ Successfully connected to {port}")
|
38
38
|
controller.disconnect()
|
39
39
|
except MakcuConnectionError as e:
|
@@ -47,14 +47,14 @@ def run_tests():
|
|
47
47
|
package_dir = Path(__file__).resolve().parent
|
48
48
|
test_file = package_dir / "test_suite.py"
|
49
49
|
|
50
|
-
|
50
|
+
result = pytest.main([
|
51
51
|
str(test_file),
|
52
52
|
"--rootdir", str(package_dir),
|
53
53
|
"-v", "--tb=short",
|
54
54
|
"--capture=tee-sys",
|
55
55
|
"--html=latest_pytest.html",
|
56
56
|
"--self-contained-html"
|
57
|
-
])
|
57
|
+
])
|
58
58
|
|
59
59
|
report_path = os.path.abspath("latest_pytest.html")
|
60
60
|
if os.path.exists(report_path):
|
@@ -63,11 +63,13 @@ def run_tests():
|
|
63
63
|
else:
|
64
64
|
print("❌ Report not found. Something went wrong.")
|
65
65
|
|
66
|
-
if result
|
66
|
+
if result != 0:
|
67
67
|
print("❌ Some tests failed.")
|
68
68
|
else:
|
69
69
|
print("✅ All tests passed.")
|
70
70
|
|
71
|
+
sys.exit(result)
|
72
|
+
|
71
73
|
def main():
|
72
74
|
args = sys.argv[1:]
|
73
75
|
|
makcu/connection.py
CHANGED
@@ -29,7 +29,12 @@ class SerialTransport:
|
|
29
29
|
self._listener_thread = None
|
30
30
|
self._button_states = {btn: False for btn in self.button_map.values()}
|
31
31
|
self._last_callback_time = {bit: 0 for bit in self.button_map}
|
32
|
-
|
32
|
+
|
33
|
+
self._response_buffer = ""
|
34
|
+
self._response_ready = threading.Event()
|
35
|
+
self._waiting_for_response = False
|
36
|
+
self._response_timeout = 0.01
|
37
|
+
self._command_lock = threading.Lock()
|
33
38
|
|
34
39
|
self._button_enum_map = {
|
35
40
|
0: MouseButton.LEFT,
|
@@ -47,28 +52,45 @@ class SerialTransport:
|
|
47
52
|
self.serial = None
|
48
53
|
self._current_baud = None
|
49
54
|
|
50
|
-
|
51
|
-
def receive_response(self, max_bytes=1024, max_lines=3, sent_command: str = "") -> str:
|
52
|
-
lines = []
|
55
|
+
def receive_response(self, sent_command: str = "") -> str:
|
53
56
|
try:
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
57
|
+
if self._response_ready.wait(timeout=self._response_timeout):
|
58
|
+
response = self._response_buffer
|
59
|
+
self._response_buffer = ""
|
60
|
+
self._response_ready.clear()
|
61
|
+
|
62
|
+
lines = response.strip().split('\n')
|
63
|
+
lines = [line.strip() for line in lines if line.strip()]
|
64
|
+
|
65
|
+
command_clean = sent_command.strip()
|
66
|
+
cleaned_lines = []
|
67
|
+
|
68
|
+
for line in lines:
|
69
|
+
if not line:
|
70
|
+
continue
|
71
|
+
if line == command_clean:
|
72
|
+
continue
|
73
|
+
if line.startswith('>>> '):
|
74
|
+
actual_response = line[4:].strip()
|
75
|
+
if actual_response and actual_response != command_clean:
|
76
|
+
cleaned_lines.append(actual_response)
|
77
|
+
continue
|
78
|
+
if line == command_clean:
|
79
|
+
continue
|
80
|
+
cleaned_lines.append(line)
|
81
|
+
|
82
|
+
result = "\n".join(cleaned_lines)
|
83
|
+
if self.debug:
|
84
|
+
self._log(f"Command: {command_clean} -> Response: '{result}'")
|
85
|
+
return result
|
86
|
+
else:
|
87
|
+
return ""
|
88
|
+
|
61
89
|
except Exception as e:
|
62
|
-
|
90
|
+
if self.debug:
|
91
|
+
self._log(f"Error in receive_response: {e}")
|
63
92
|
return ""
|
64
93
|
|
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
94
|
def set_button_callback(self, callback):
|
73
95
|
self._button_callback = callback
|
74
96
|
|
@@ -115,7 +137,6 @@ class SerialTransport:
|
|
115
137
|
return True
|
116
138
|
return False
|
117
139
|
|
118
|
-
|
119
140
|
def connect(self):
|
120
141
|
if self._is_connected:
|
121
142
|
self._log("Already connected.")
|
@@ -153,37 +174,40 @@ class SerialTransport:
|
|
153
174
|
return self._is_connected
|
154
175
|
|
155
176
|
def send_command(self, command, expect_response=False):
|
156
|
-
time.sleep(0.06)
|
157
177
|
if not self._is_connected or not self.serial or not self.serial.is_open:
|
158
178
|
raise MakcuConnectionError("Serial connection not open.")
|
159
|
-
|
179
|
+
|
180
|
+
with self._command_lock:
|
160
181
|
try:
|
161
|
-
|
162
|
-
|
182
|
+
if expect_response:
|
183
|
+
self._response_buffer = ""
|
184
|
+
self._response_ready.clear()
|
185
|
+
self._waiting_for_response = True
|
186
|
+
|
163
187
|
self.serial.write(command.encode("ascii") + b"\r\n")
|
164
188
|
self.serial.flush()
|
189
|
+
|
165
190
|
if expect_response:
|
166
191
|
response = self.receive_response(sent_command=command)
|
192
|
+
self._waiting_for_response = False
|
167
193
|
if not response:
|
168
194
|
raise MakcuTimeoutError(f"No response from device for command: {command}")
|
169
195
|
return response
|
170
|
-
|
171
|
-
|
196
|
+
|
197
|
+
except Exception as e:
|
198
|
+
self._waiting_for_response = False
|
199
|
+
raise
|
172
200
|
|
173
201
|
def get_button_states(self):
|
174
202
|
return dict(self._button_states)
|
175
203
|
|
176
204
|
def get_button_mask(self) -> int:
|
177
|
-
|
178
|
-
for i, name in self.button_map.items():
|
179
|
-
if self._button_states.get(name, False):
|
180
|
-
mask |= (1 << i)
|
181
|
-
return mask
|
205
|
+
return self._last_mask
|
182
206
|
|
183
207
|
def enable_button_monitoring(self, enable: bool = True):
|
184
208
|
self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
|
185
209
|
|
186
|
-
def catch_button(self, button:
|
210
|
+
def catch_button(self, button: MouseButton):
|
187
211
|
command = {
|
188
212
|
"LEFT": "km.catch_ml(0)",
|
189
213
|
"RIGHT": "km.catch_mr(0)",
|
@@ -196,76 +220,71 @@ class SerialTransport:
|
|
196
220
|
else:
|
197
221
|
raise ValueError(f"Unsupported button: {button}")
|
198
222
|
|
199
|
-
def
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
"MOUSE4": "km.catch_ms1()",
|
205
|
-
"MOUSE5": "km.catch_ms2()",
|
206
|
-
}.get(button.upper())
|
207
|
-
if command:
|
208
|
-
result = self.send_command(command, expect_response=True)
|
209
|
-
try:
|
210
|
-
return int(result.strip())
|
211
|
-
except Exception:
|
212
|
-
return 0
|
213
|
-
else:
|
214
|
-
raise ValueError(f"Unsupported button: {button}")
|
223
|
+
def _is_button_data(self, byte_value):
|
224
|
+
return byte_value <= 0b11111 and byte_value not in [0x0D, 0x0A]
|
225
|
+
|
226
|
+
def _is_ascii_data(self, byte_value):
|
227
|
+
return 32 <= byte_value <= 126 or byte_value in [0x0D, 0x0A] # Include CR/LF
|
215
228
|
|
216
229
|
def _listen(self, debug=False):
|
217
230
|
self._log("Started listener thread")
|
218
|
-
button_states = {i: False for i in self.button_map}
|
219
231
|
self._last_mask = 0
|
220
|
-
|
232
|
+
ascii_buffer = bytearray()
|
233
|
+
response_lines = []
|
221
234
|
|
222
235
|
while self._is_connected and not self._stop_event.is_set():
|
223
|
-
if self._pause_listener:
|
224
|
-
time.sleep(0.001)
|
225
|
-
continue
|
226
|
-
|
227
236
|
try:
|
228
|
-
|
229
|
-
if
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
if
|
236
|
-
|
237
|
-
button_states[bit] = False
|
238
|
-
|
239
|
-
elif "b'\\x" in byte_str:
|
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
|
-
|
245
|
-
for bit, name in self.button_map.items():
|
246
|
-
self._button_states[name] = button_states[bit]
|
247
|
-
|
248
|
-
if self._button_callback:
|
237
|
+
data = self.serial.read(self.serial.in_waiting or 1)
|
238
|
+
if not data:
|
239
|
+
continue
|
240
|
+
|
241
|
+
for byte_val in data:
|
242
|
+
if (self._is_button_data(byte_val) and
|
243
|
+
not self._waiting_for_response):
|
244
|
+
if byte_val != self._last_mask:
|
245
|
+
changed_bits = byte_val ^ self._last_mask
|
249
246
|
for bit, name in self.button_map.items():
|
250
|
-
|
251
|
-
|
252
|
-
|
247
|
+
if changed_bits & (1 << bit):
|
248
|
+
is_pressed = bool(byte_val & (1 << bit))
|
249
|
+
self._button_states[name] = is_pressed
|
253
250
|
button_enum = self._button_enum_map.get(bit)
|
254
|
-
if button_enum:
|
255
|
-
self._button_callback(button_enum,
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
251
|
+
if button_enum and self._button_callback:
|
252
|
+
self._button_callback(button_enum, is_pressed)
|
253
|
+
|
254
|
+
self._last_mask = byte_val
|
255
|
+
|
256
|
+
if debug:
|
257
|
+
pressed = [name for _, name in self.button_map.items() if self._button_states[name]]
|
258
|
+
button_str = ", ".join(pressed) if pressed else "No buttons pressed"
|
259
|
+
self._log(f"Mask: 0x{byte_val:02X} -> {button_str}")
|
260
|
+
elif self._is_ascii_data(byte_val):
|
261
|
+
if self._waiting_for_response:
|
262
|
+
ascii_buffer.append(byte_val)
|
263
|
+
if byte_val == 0x0A:
|
264
|
+
try:
|
265
|
+
line = ascii_buffer.decode('ascii', errors='ignore').strip()
|
266
|
+
ascii_buffer.clear()
|
267
|
+
|
268
|
+
if line:
|
269
|
+
response_lines.append(line)
|
270
|
+
|
271
|
+
if (len(response_lines) >= 2 or
|
272
|
+
(len(response_lines) == 1 and not line.startswith('>>> '))):
|
273
|
+
|
274
|
+
full_response = '\n'.join(response_lines)
|
275
|
+
self._response_buffer = full_response
|
276
|
+
self._response_ready.set()
|
277
|
+
response_lines.clear()
|
278
|
+
|
279
|
+
except Exception as e:
|
280
|
+
self._log(f"Error decoding ASCII response: {e}")
|
281
|
+
ascii_buffer.clear()
|
282
|
+
response_lines.clear()
|
263
283
|
|
264
284
|
except serial.SerialException as e:
|
265
285
|
if "ClearCommError failed" not in str(e):
|
266
286
|
self._log(f"Serial error during listening: {e}")
|
267
287
|
break
|
268
|
-
|
269
|
-
|
270
|
-
|
288
|
+
except Exception as e:
|
289
|
+
self._log(f"Unexpected error in listener: {e}")
|
271
290
|
self._log("Listener thread exiting")
|
makcu/controller.py
CHANGED
@@ -72,6 +72,14 @@ class MakcuController:
|
|
72
72
|
self._check_connection()
|
73
73
|
self.mouse.lock_side2(lock)
|
74
74
|
|
75
|
+
def lock_x(self, lock: bool):
|
76
|
+
self._check_connection()
|
77
|
+
self.mouse.lock_x(lock)
|
78
|
+
|
79
|
+
def lock_y(self, lock: bool):
|
80
|
+
self._check_connection()
|
81
|
+
self.mouse.lock_y(lock)
|
82
|
+
|
75
83
|
def spoof_serial(self, serial: str):
|
76
84
|
self._check_connection()
|
77
85
|
self.mouse.spoof_serial(serial)
|
@@ -92,33 +100,24 @@ class MakcuController:
|
|
92
100
|
self._check_connection()
|
93
101
|
return self.transport.get_button_mask()
|
94
102
|
|
95
|
-
def
|
96
|
-
self._check_connection()
|
97
|
-
return self.mouse.is_button_locked(button)
|
98
|
-
|
99
|
-
def capture(self, button: MouseButton):
|
103
|
+
def is_locked(self, button: MouseButton) -> bool:
|
100
104
|
self._check_connection()
|
101
|
-
self.mouse.
|
102
|
-
|
103
|
-
def get_captured_clicks(self, button: MouseButton) -> int:
|
104
|
-
self._check_connection()
|
105
|
-
return self.mouse.stop_capturing_clicks(button.name)
|
106
|
-
|
105
|
+
return self.mouse.is_locked(button)
|
107
106
|
|
108
107
|
def click_human_like(self, button: MouseButton, count: int = 1,
|
109
108
|
profile: str = "normal", jitter: int = 0):
|
110
109
|
self._check_connection()
|
111
110
|
|
112
111
|
timing_profiles = {
|
113
|
-
"normal":
|
114
|
-
"fast":
|
115
|
-
"slow":
|
112
|
+
"normal": (60, 120, 100, 180),
|
113
|
+
"fast": (30, 60, 50, 100),
|
114
|
+
"slow": (100, 180, 150, 300),
|
116
115
|
}
|
117
116
|
|
118
117
|
if profile not in timing_profiles:
|
119
118
|
raise ValueError(f"Invalid profile: {profile}. Choose from {list(timing_profiles.keys())}")
|
120
119
|
|
121
|
-
|
120
|
+
min_down, max_down, min_wait, max_wait = timing_profiles[profile]
|
122
121
|
|
123
122
|
for _ in range(count):
|
124
123
|
if jitter > 0:
|
@@ -127,10 +126,10 @@ class MakcuController:
|
|
127
126
|
self.mouse.move(dx, dy)
|
128
127
|
|
129
128
|
self.mouse.press(button)
|
130
|
-
time.sleep(random.uniform(
|
129
|
+
time.sleep(random.uniform(min_down, max_down) / 1000.0)
|
131
130
|
self.mouse.release(button)
|
132
|
-
time.sleep(random.uniform(
|
133
|
-
|
131
|
+
time.sleep(random.uniform(min_wait, max_wait) / 1000.0)
|
132
|
+
|
134
133
|
def enable_button_monitoring(self, enable: bool = True):
|
135
134
|
self._check_connection()
|
136
135
|
self.transport.enable_button_monitoring(enable)
|
@@ -143,10 +142,6 @@ class MakcuController:
|
|
143
142
|
self._check_connection()
|
144
143
|
return self.mouse.get_all_lock_states()
|
145
144
|
|
146
|
-
def stop_capturing_clicks(self, button: str) -> int:
|
147
|
-
self._check_connection()
|
148
|
-
return self.mouse.stop_capturing_clicks(button)
|
149
|
-
|
150
145
|
def press(self, button: MouseButton):
|
151
146
|
self._check_connection()
|
152
147
|
self.mouse.press(button)
|
@@ -154,11 +149,11 @@ class MakcuController:
|
|
154
149
|
def release(self, button: MouseButton):
|
155
150
|
self._check_connection()
|
156
151
|
self.mouse.release(button)
|
157
|
-
|
152
|
+
|
158
153
|
def get_button_states(self) -> dict:
|
159
154
|
self._check_connection()
|
160
155
|
return self.transport.get_button_states()
|
161
156
|
|
162
|
-
def
|
157
|
+
def is_pressed(self, button: MouseButton) -> bool:
|
163
158
|
self._check_connection()
|
164
159
|
return self.transport.get_button_states().get(button.name.lower(), False)
|
makcu/mouse.py
CHANGED
@@ -1,32 +1,29 @@
|
|
1
1
|
from .enums import MouseButton
|
2
2
|
from .errors import MakcuCommandError
|
3
3
|
from serial.tools import list_ports
|
4
|
+
import time
|
4
5
|
|
5
6
|
class Mouse:
|
6
7
|
def __init__(self, transport):
|
7
8
|
self.transport = transport
|
8
9
|
|
10
|
+
def _send_button_command(self, button: MouseButton, state: int):
|
11
|
+
command_map = {
|
12
|
+
MouseButton.LEFT: "left",
|
13
|
+
MouseButton.RIGHT: "right",
|
14
|
+
MouseButton.MIDDLE: "middle",
|
15
|
+
MouseButton.MOUSE4: "ms1",
|
16
|
+
MouseButton.MOUSE5: "ms2",
|
17
|
+
}
|
18
|
+
if button not in command_map:
|
19
|
+
raise MakcuCommandError(f"Unsupported button: {button}")
|
20
|
+
self.transport.send_command(f"km.{command_map[button]}({state})")
|
21
|
+
|
9
22
|
def press(self, button: MouseButton):
|
10
|
-
|
11
|
-
MouseButton.LEFT: "km.left(1)",
|
12
|
-
MouseButton.RIGHT: "km.right(1)",
|
13
|
-
MouseButton.MIDDLE: "km.middle(1)"
|
14
|
-
}.get(button)
|
15
|
-
if cmd:
|
16
|
-
self.transport.send_command(cmd)
|
17
|
-
else:
|
18
|
-
raise MakcuCommandError(f"Unsupported button for press(): {button}")
|
23
|
+
self._send_button_command(button, 1)
|
19
24
|
|
20
25
|
def release(self, button: MouseButton):
|
21
|
-
|
22
|
-
MouseButton.LEFT: "km.left(0)",
|
23
|
-
MouseButton.RIGHT: "km.right(0)",
|
24
|
-
MouseButton.MIDDLE: "km.middle(0)"
|
25
|
-
}.get(button)
|
26
|
-
if cmd:
|
27
|
-
self.transport.send_command(cmd)
|
28
|
-
else:
|
29
|
-
raise MakcuCommandError(f"Unsupported button for release(): {button}")
|
26
|
+
self._send_button_command(button, 0)
|
30
27
|
|
31
28
|
def move(self, x: int, y: int):
|
32
29
|
self.transport.send_command(f"km.move({x},{y})")
|
@@ -40,16 +37,33 @@ class Mouse:
|
|
40
37
|
def scroll(self, delta: int):
|
41
38
|
self.transport.send_command(f"km.wheel({delta})")
|
42
39
|
|
43
|
-
def lock_left(self, lock: bool):
|
44
|
-
|
45
|
-
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
def
|
40
|
+
def lock_left(self, lock: bool):
|
41
|
+
self.transport.send_command(f"km.lock_ml({int(lock)})")
|
42
|
+
|
43
|
+
def lock_middle(self, lock: bool):
|
44
|
+
self.transport.send_command(f"km.lock_mm({int(lock)})")
|
45
|
+
|
46
|
+
def lock_right(self, lock: bool):
|
47
|
+
self.transport.send_command(f"km.lock_mr({int(lock)})")
|
48
|
+
|
49
|
+
def lock_side1(self, lock: bool):
|
50
|
+
self.transport.send_command(f"km.lock_ms1({int(lock)})")
|
51
|
+
|
52
|
+
def lock_side2(self, lock: bool):
|
53
|
+
self.transport.send_command(f"km.lock_ms2({int(lock)})")
|
54
|
+
|
55
|
+
def lock_x(self, lock: bool):
|
56
|
+
self.transport.send_command(f"km.lock_mx({int(lock)})")
|
57
|
+
|
58
|
+
def lock_y(self, lock: bool):
|
59
|
+
self.transport.send_command(f"km.lock_my({int(lock)})")
|
50
60
|
|
51
|
-
|
52
|
-
def
|
61
|
+
|
62
|
+
def spoof_serial(self, serial: str):
|
63
|
+
self.transport.send_command(f"km.serial('{serial}')")
|
64
|
+
|
65
|
+
def reset_serial(self):
|
66
|
+
self.transport.send_command("km.serial(0)")
|
53
67
|
|
54
68
|
def get_device_info(self) -> dict:
|
55
69
|
port_name = self.transport.port
|
@@ -74,51 +88,80 @@ class Mouse:
|
|
74
88
|
def get_firmware_version(self) -> str:
|
75
89
|
return self.transport.send_command("km.version()", expect_response=True)
|
76
90
|
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
"
|
81
|
-
"
|
82
|
-
"LEFT": "km.lock_ml()",
|
83
|
-
"MIDDLE": "km.lock_mm()",
|
84
|
-
"RIGHT": "km.lock_mr()",
|
85
|
-
"MOUSE4": "km.lock_ms1()",
|
86
|
-
"MOUSE5": "km.lock_ms2()",
|
91
|
+
def _get_lock_command(self, target: str) -> str:
|
92
|
+
command_map = {
|
93
|
+
"X": "mx", "Y": "my",
|
94
|
+
"LEFT": "ml", "RIGHT": "mr", "MIDDLE": "mm",
|
95
|
+
"MOUSE4": "ms1", "MOUSE5": "ms2",
|
87
96
|
}
|
88
|
-
|
97
|
+
key = target.upper()
|
98
|
+
if key not in command_map:
|
89
99
|
raise ValueError(f"Unsupported lock target: {target}")
|
100
|
+
return f"km.lock_{command_map[key]}()"
|
101
|
+
|
102
|
+
def is_locked(self, button: MouseButton) -> bool:
|
90
103
|
try:
|
91
|
-
|
92
|
-
|
93
|
-
|
104
|
+
# Sleep for previous command to finish first, hoping to get rid of this soon.
|
105
|
+
time.sleep(0.03)
|
106
|
+
|
107
|
+
command = self._get_lock_command(button.name)
|
108
|
+
result = self.transport.send_command(command, expect_response=True)
|
109
|
+
if hasattr(self.transport, 'debug') and self.transport.debug:
|
110
|
+
print(f"Lock status command: {command}")
|
111
|
+
print(f"Raw result: '{result}'")
|
112
|
+
|
113
|
+
result = result.strip()
|
114
|
+
|
115
|
+
if result in ['1', '0']:
|
116
|
+
return result == '1'
|
117
|
+
|
118
|
+
lines = result.split('\n')
|
119
|
+
for line in lines:
|
120
|
+
line = line.strip()
|
121
|
+
if line in ['1', '0']:
|
122
|
+
return line == '1'
|
123
|
+
|
124
|
+
import re
|
125
|
+
numbers = re.findall(r'\b[01]\b', result)
|
126
|
+
if numbers:
|
127
|
+
return numbers[-1] == '1'
|
128
|
+
|
129
|
+
if hasattr(self.transport, 'debug') and self.transport.debug:
|
130
|
+
print(f"Could not parse lock status from: '{result}'")
|
131
|
+
|
132
|
+
return False
|
133
|
+
|
134
|
+
except Exception as e:
|
135
|
+
if hasattr(self.transport, 'debug') and self.transport.debug:
|
136
|
+
print(f"Error checking lock status: {e}")
|
94
137
|
return False
|
95
|
-
|
96
|
-
def is_button_locked(self, button: MouseButton) -> bool:
|
97
|
-
name_map = {
|
98
|
-
MouseButton.LEFT: "LEFT",
|
99
|
-
MouseButton.RIGHT: "RIGHT",
|
100
|
-
MouseButton.MIDDLE: "MIDDLE",
|
101
|
-
MouseButton.MOUSE4: "MOUSE4",
|
102
|
-
MouseButton.MOUSE5: "MOUSE5"
|
103
|
-
}
|
104
|
-
return self.is_locked(name_map[button])
|
105
|
-
|
106
|
-
def begin_capture(self, button: str):
|
107
|
-
"""
|
108
|
-
Assumes lock_<button>(1) has already been called.
|
109
|
-
Sends catch_<button>(0) to begin capturing click cycles.
|
110
|
-
"""
|
111
|
-
self.transport.catch_button(button)
|
112
|
-
|
113
|
-
def stop_capturing_clicks(self, button: str) -> int:
|
114
|
-
"""
|
115
|
-
Assumes lock_<button>(0) has already been called.
|
116
|
-
Returns the total number of clicks since begin_capture.
|
117
|
-
"""
|
118
|
-
return self.transport.read_captured_clicks(button)
|
119
138
|
|
120
139
|
def get_all_lock_states(self) -> dict:
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
140
|
+
"""Get all lock states - no timing delays needed with acknowledgment"""
|
141
|
+
targets = ["X", "Y", "LEFT", "RIGHT", "MIDDLE", "MOUSE4", "MOUSE5"]
|
142
|
+
state = {}
|
143
|
+
|
144
|
+
for target in targets:
|
145
|
+
try:
|
146
|
+
# No sleep needed - each command waits for acknowledgment
|
147
|
+
if hasattr(MouseButton, target):
|
148
|
+
state[target] = self.is_locked(MouseButton[target])
|
149
|
+
else:
|
150
|
+
# Handle X and Y axis locks
|
151
|
+
if target in ["X", "Y"]:
|
152
|
+
# Create a mock enum-like object for axis locks
|
153
|
+
class AxisButton:
|
154
|
+
def __init__(self, name):
|
155
|
+
self.name = name
|
156
|
+
state[target] = self.is_locked(AxisButton(target))
|
157
|
+
else:
|
158
|
+
state[target] = False
|
159
|
+
except Exception as e:
|
160
|
+
if hasattr(self.transport, 'debug') and self.transport.debug:
|
161
|
+
print(f"Error getting lock state for {target}: {e}")
|
162
|
+
state[target] = False
|
163
|
+
|
164
|
+
if hasattr(self.transport, 'debug') and self.transport.debug:
|
165
|
+
print(f"All lock states: {state}")
|
166
|
+
|
167
|
+
return state
|
makcu/test_suite.py
CHANGED
@@ -1,22 +1,17 @@
|
|
1
|
-
import pytest
|
1
|
+
import pytest, time
|
2
2
|
from makcu import MouseButton
|
3
3
|
|
4
|
-
def test_is_button_pressed(makcu):
|
5
|
-
assert makcu.is_button_pressed(MouseButton.LEFT) in [True, False]
|
6
|
-
|
7
4
|
def test_press_and_release(makcu):
|
8
5
|
makcu.press(MouseButton.LEFT)
|
9
6
|
makcu.release(MouseButton.LEFT)
|
10
7
|
|
11
8
|
def test_firmware_version(makcu):
|
12
|
-
print("Getting firmware version...")
|
13
9
|
version = makcu.mouse.get_firmware_version()
|
14
|
-
print(f"Firmware version: {version}")
|
15
10
|
assert version and len(version.strip()) > 0
|
16
11
|
|
17
12
|
def test_middle_click(makcu):
|
18
|
-
makcu.
|
19
|
-
makcu.
|
13
|
+
makcu.press(MouseButton.MIDDLE)
|
14
|
+
makcu.release(MouseButton.MIDDLE)
|
20
15
|
|
21
16
|
def test_device_info(makcu):
|
22
17
|
print("Fetching device info...")
|
@@ -31,16 +26,16 @@ def test_port_connection(makcu):
|
|
31
26
|
@pytest.mark.skip(reason="Capture test disabled until firmware supports tracking clicks from software input")
|
32
27
|
def test_capture_right_clicks(makcu):
|
33
28
|
makcu.mouse.lock_right(True)
|
34
|
-
assert makcu.mouse.
|
29
|
+
assert makcu.mouse.is_locked(MouseButton.RIGHT)
|
35
30
|
|
36
31
|
makcu.mouse.begin_capture("RIGHT")
|
37
|
-
makcu.
|
32
|
+
makcu.press(MouseButton.RIGHT)
|
38
33
|
makcu.mouse.release(MouseButton.RIGHT)
|
39
|
-
makcu.
|
34
|
+
makcu.press(MouseButton.RIGHT)
|
40
35
|
makcu.mouse.release(MouseButton.RIGHT)
|
41
36
|
|
42
37
|
makcu.mouse.lock_right(False)
|
43
|
-
assert not makcu.mouse.
|
38
|
+
assert not makcu.mouse.is_locked(MouseButton.RIGHT)
|
44
39
|
|
45
40
|
count = makcu.mouse.stop_capturing_clicks("RIGHT")
|
46
41
|
assert count >= 2, f"Expected >=2 captured clicks, got {count}"
|
@@ -60,22 +55,10 @@ def test_get_button_states(makcu):
|
|
60
55
|
def test_lock_state(makcu):
|
61
56
|
print("Locking LEFT button...")
|
62
57
|
makcu.lock_left(True)
|
63
|
-
|
64
58
|
print("Querying lock state while LEFT is locked...")
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
all_states = makcu.get_all_lock_states()
|
69
|
-
print(f"All lock states: {all_states}")
|
70
|
-
|
71
|
-
assert all_states["LEFT"] is True
|
72
|
-
assert isinstance(all_states["RIGHT"], bool)
|
73
|
-
|
74
|
-
print("Unlocking LEFT button...")
|
75
|
-
makcu.lock_left(False)
|
76
|
-
|
77
|
-
print("Rechecking LEFT lock state after unlock...")
|
78
|
-
assert not makcu.is_button_locked(MouseButton.LEFT)
|
59
|
+
state = makcu.is_locked(MouseButton.LEFT) # Check state AFTER ensuring it's locked
|
60
|
+
print(state)
|
61
|
+
assert state # Now assert the current state
|
79
62
|
|
80
63
|
def test_makcu_behavior(makcu):
|
81
64
|
makcu.move(25, 25)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: makcu
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.4
|
4
4
|
Summary: Python library to interact with Makcu devices.
|
5
5
|
Author: SleepyTotem
|
6
6
|
License: GPL
|
@@ -50,14 +50,6 @@ python -m makcu [command]
|
|
50
50
|
| `--testPort COM3` | Tests a specific COM port for connectivity |
|
51
51
|
| `--runtest` | Runs all automated tests and opens a test report |
|
52
52
|
|
53
|
-
### Examples
|
54
|
-
|
55
|
-
```bash
|
56
|
-
python -m makcu --debug
|
57
|
-
python -m makcu --testPort COM3
|
58
|
-
python -m makcu --runtest
|
59
|
-
```
|
60
|
-
|
61
53
|
---
|
62
54
|
|
63
55
|
## 🧠 Quickstart (Python)
|
@@ -65,7 +57,7 @@ python -m makcu --runtest
|
|
65
57
|
```python
|
66
58
|
from makcu import create_controller, MouseButton
|
67
59
|
|
68
|
-
makcu = create_controller("COM1")
|
60
|
+
makcu = create_controller("COM1")
|
69
61
|
makcu.click(MouseButton.LEFT)
|
70
62
|
makcu.move(100, 50)
|
71
63
|
makcu.scroll(-1)
|
@@ -79,20 +71,14 @@ makcu.disconnect()
|
|
79
71
|
### 🔧 Initialization
|
80
72
|
|
81
73
|
```python
|
82
|
-
makcu = create_controller(debug=True, send_init=True)
|
83
|
-
```
|
84
|
-
|
85
|
-
#### Set fallback port manually
|
86
|
-
|
87
|
-
```python
|
88
|
-
makcu = create_controller("COM4") # Optional fallback com port
|
74
|
+
makcu = create_controller(fallback_com_port="COM1", debug=True, send_init=True)
|
89
75
|
```
|
90
76
|
|
91
77
|
---
|
92
78
|
|
93
79
|
### 🎮 Mouse Control
|
94
80
|
|
95
|
-
####
|
81
|
+
#### Button Actions
|
96
82
|
|
97
83
|
```python
|
98
84
|
makcu.click(MouseButton.LEFT)
|
@@ -103,21 +89,21 @@ makcu.release(MouseButton.RIGHT)
|
|
103
89
|
#### Movement
|
104
90
|
|
105
91
|
```python
|
106
|
-
makcu.move(
|
92
|
+
makcu.move(30, 20)
|
107
93
|
makcu.move_smooth(100, 40, segments=10)
|
108
94
|
makcu.move_bezier(50, 50, 15, ctrl_x=25, ctrl_y=25)
|
109
95
|
```
|
110
96
|
|
111
|
-
####
|
97
|
+
#### Scroll Wheel
|
112
98
|
|
113
99
|
```python
|
114
|
-
makcu.scroll(-3)
|
115
|
-
makcu.scroll(3)
|
100
|
+
makcu.scroll(-3)
|
101
|
+
makcu.scroll(3)
|
116
102
|
```
|
117
103
|
|
118
104
|
---
|
119
105
|
|
120
|
-
### 🔒 Locking
|
106
|
+
### 🔒 Locking
|
121
107
|
|
122
108
|
```python
|
123
109
|
makcu.lock_left(True)
|
@@ -129,10 +115,10 @@ makcu.lock_mouse_x(True)
|
|
129
115
|
makcu.lock_mouse_y(False)
|
130
116
|
```
|
131
117
|
|
132
|
-
#### Lock
|
118
|
+
#### Lock State Query
|
133
119
|
|
134
120
|
```python
|
135
|
-
makcu.
|
121
|
+
makcu.is_locked(MouseButton.LEFT)
|
136
122
|
makcu.get_all_lock_states()
|
137
123
|
```
|
138
124
|
|
@@ -144,7 +130,7 @@ makcu.get_all_lock_states()
|
|
144
130
|
makcu.click_human_like(
|
145
131
|
button=MouseButton.LEFT,
|
146
132
|
count=5,
|
147
|
-
profile="normal", # "fast", "slow"
|
133
|
+
profile="normal", # or "fast", "slow"
|
148
134
|
jitter=3
|
149
135
|
)
|
150
136
|
```
|
@@ -155,10 +141,7 @@ makcu.click_human_like(
|
|
155
141
|
|
156
142
|
```python
|
157
143
|
info = makcu.get_device_info()
|
158
|
-
print(info)
|
159
|
-
|
160
144
|
version = makcu.get_firmware_version()
|
161
|
-
print(version)
|
162
145
|
```
|
163
146
|
|
164
147
|
---
|
@@ -172,15 +155,15 @@ makcu.reset_serial()
|
|
172
155
|
|
173
156
|
---
|
174
157
|
|
175
|
-
## 🧪 Button Monitoring
|
158
|
+
## 🧪 Button Monitoring
|
176
159
|
|
177
|
-
### Enable
|
160
|
+
### Enable Monitoring
|
178
161
|
|
179
162
|
```python
|
180
163
|
makcu.enable_button_monitoring(True)
|
181
164
|
```
|
182
165
|
|
183
|
-
### Set Callback
|
166
|
+
### Set Event Callback
|
184
167
|
|
185
168
|
```python
|
186
169
|
def on_button_event(button, pressed):
|
@@ -191,66 +174,46 @@ makcu.set_button_callback(on_button_event)
|
|
191
174
|
|
192
175
|
---
|
193
176
|
|
194
|
-
## ❌ Click Capturing (Pending Firmware
|
195
|
-
|
196
|
-
Click capturing will allow you to detect and count click events in software.
|
177
|
+
## ❌ Click Capturing (Pending Firmware Fix)
|
197
178
|
|
198
179
|
```python
|
199
180
|
makcu.mouse.lock_right(True)
|
200
|
-
makcu.
|
181
|
+
makcu.mouse.begin_capture("RIGHT")
|
201
182
|
|
202
|
-
#
|
183
|
+
# Simulated user input...
|
203
184
|
|
204
185
|
makcu.mouse.lock_right(False)
|
205
|
-
count = makcu.
|
186
|
+
count = makcu.mouse.stop_capturing_clicks("RIGHT")
|
206
187
|
print(f"Captured clicks: {count}")
|
207
188
|
```
|
208
189
|
|
209
|
-
> ⚠️
|
190
|
+
> ⚠️ Not fully supported yet — firmware must be updated to complete this feature.
|
210
191
|
|
211
192
|
---
|
212
193
|
|
213
194
|
## 🔢 Bitmask & Button States
|
214
195
|
|
215
|
-
### Get Bitmask of Active Buttons
|
216
|
-
|
217
196
|
```python
|
218
197
|
mask = makcu.get_button_mask()
|
219
|
-
print(f"Button mask: {mask}")
|
220
|
-
```
|
221
|
-
|
222
|
-
### Get Raw Button State Map
|
223
|
-
|
224
|
-
```python
|
225
198
|
states = makcu.get_button_states()
|
226
|
-
print(states) # {'left': False, 'right': True, ...}
|
227
|
-
```
|
228
|
-
|
229
|
-
### Check if a Specific Button Is Pressed
|
230
199
|
|
231
|
-
|
232
|
-
|
233
|
-
print("Right button is pressed")
|
200
|
+
if makcu.is_pressed(MouseButton.RIGHT):
|
201
|
+
print("Right button is currently pressed")
|
234
202
|
```
|
235
203
|
|
236
204
|
---
|
237
205
|
|
238
|
-
## ⚙️ Low-Level
|
239
|
-
|
240
|
-
### Send raw serial commands
|
206
|
+
## ⚙️ Low-Level Access
|
241
207
|
|
242
208
|
```python
|
243
|
-
from makcu import create_controller
|
244
|
-
makcu = create_controller()
|
245
209
|
response = makcu.transport.send_command("km.version()", expect_response=True)
|
246
|
-
print(response)
|
247
210
|
```
|
248
211
|
|
249
212
|
---
|
250
213
|
|
251
214
|
## 🧪 Test Suite
|
252
215
|
|
253
|
-
Run
|
216
|
+
Run full test suite and generate an HTML report:
|
254
217
|
|
255
218
|
```bash
|
256
219
|
python -m makcu --runtest
|
@@ -287,11 +250,11 @@ except MakcuConnectionError as e:
|
|
287
250
|
|
288
251
|
## 🛠️ Developer Notes
|
289
252
|
|
290
|
-
-
|
291
|
-
-
|
292
|
-
-
|
293
|
-
-
|
294
|
-
- Supports
|
253
|
+
- Communicates via CH343 USB serial
|
254
|
+
- Automatically finds correct port or uses fallback
|
255
|
+
- Switches baud to 4M after initial connect
|
256
|
+
- Enables `km.buttons(1)` on init if requested
|
257
|
+
- Supports full button state tracking with events
|
295
258
|
|
296
259
|
---
|
297
260
|
|
@@ -301,10 +264,11 @@ GPL License © SleepyTotem
|
|
301
264
|
|
302
265
|
---
|
303
266
|
|
304
|
-
## Support
|
305
|
-
|
267
|
+
## 🙋 Support
|
268
|
+
|
269
|
+
Open an issue on GitHub if you encounter bugs or need help.
|
306
270
|
|
307
271
|
## 🌐 Links
|
308
272
|
|
309
|
-
-
|
310
|
-
-
|
273
|
+
- [GitHub Repo](https://github.com/SleepyTotem/makcu-py-lib)
|
274
|
+
- [PyPI Package](https://pypi.org/project/makcu/)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
makcu/__init__.py,sha256=hCP6COi14T4C0V35crnbBEzJPa9hnwGb-gDPoxs_H6E,459
|
2
|
+
makcu/__main__.py,sha256=wjRtr7V6qd54w43lHmXQldlVffKMW27nkhKa4E5B9t8,2830
|
3
|
+
makcu/conftest.py,sha256=TQibb01_1OfzDrDU5u3IDlrfehXyr7E7jx3g0VySZmU,560
|
4
|
+
makcu/connection.py,sha256=NDo8cmAnsu_-Njc-CacLFTKXkALvSc7E2BXYOY5aljg,12030
|
5
|
+
makcu/controller.py,sha256=wBrGlO_mivd7YFRQJ84BVXGIuu3G7ChRos9fucTYWzM,5115
|
6
|
+
makcu/enums.py,sha256=VmvCLmpghVHuTAkvCGMfA14MgWTtFVMfsGQQNnJ58Ts,126
|
7
|
+
makcu/errors.py,sha256=4CkQ4gKa7GL5-BO3yOAJMMsy3QlUDDL42S1P1clqV4A,562
|
8
|
+
makcu/mouse.py,sha256=PkBowk--SSHo13gRwK3jnORSQVQV1YeIF4NWE_Cm4KU,6388
|
9
|
+
makcu/test_suite.py,sha256=qHYklwhVCeZbpndlwUrSGnd2a5wQtJwefjlo_ZWXD-Y,2661
|
10
|
+
makcu-0.1.4.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
11
|
+
makcu-0.1.4.dist-info/METADATA,sha256=tZCMI-5J3s4Sp_zAVlrvKiLbGCcFGDCbRkDzzlilQWs,4828
|
12
|
+
makcu-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
13
|
+
makcu-0.1.4.dist-info/top_level.txt,sha256=IRO1UVb5LK_ovjau0g4oObyXQqy00tVEE-yF5lPgw1w,6
|
14
|
+
makcu-0.1.4.dist-info/RECORD,,
|
makcu-0.1.2.dist-info/RECORD
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
makcu/__init__.py,sha256=hCP6COi14T4C0V35crnbBEzJPa9hnwGb-gDPoxs_H6E,459
|
2
|
-
makcu/__main__.py,sha256=0fC-GdjCjC-ZCwE0Zwq-EJAuD5wEOo78Di7j9UAvcy0,2790
|
3
|
-
makcu/conftest.py,sha256=TQibb01_1OfzDrDU5u3IDlrfehXyr7E7jx3g0VySZmU,560
|
4
|
-
makcu/connection.py,sha256=AL1W-5ql7Wf54O_lDH8Tf36G73Ny1MUpx05TYXQt8zk,10268
|
5
|
-
makcu/controller.py,sha256=AgulithR4NFHib-Q3mwmSlqhrodOmNFtojAnBgs3pMQ,5518
|
6
|
-
makcu/enums.py,sha256=VmvCLmpghVHuTAkvCGMfA14MgWTtFVMfsGQQNnJ58Ts,126
|
7
|
-
makcu/errors.py,sha256=4CkQ4gKa7GL5-BO3yOAJMMsy3QlUDDL42S1P1clqV4A,562
|
8
|
-
makcu/mouse.py,sha256=rjrSE9PwO0e3CCtZ9oYqoxd17gVFNpeXlyBe5aV-QFc,4912
|
9
|
-
makcu/test_suite.py,sha256=zko1SaUC-zOnEy7PH22DzJXXELVoV4JVHwagIdCTW5M,3196
|
10
|
-
makcu-0.1.2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
11
|
-
makcu-0.1.2.dist-info/METADATA,sha256=lCvjpEpQ0GnQ7bbUWiB3JWqJoNqZiVxtp9fTj_k2P88,5600
|
12
|
-
makcu-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
13
|
-
makcu-0.1.2.dist-info/top_level.txt,sha256=IRO1UVb5LK_ovjau0g4oObyXQqy00tVEE-yF5lPgw1w,6
|
14
|
-
makcu-0.1.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|