makcu 0.1.3__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/connection.py +106 -87
- makcu/controller.py +10 -27
- makcu/mouse.py +103 -52
- makcu/test_suite.py +5 -24
- {makcu-0.1.3.dist-info → makcu-0.1.4.dist-info}/METADATA +34 -70
- makcu-0.1.4.dist-info/RECORD +14 -0
- makcu-0.1.3.dist-info/RECORD +0 -14
- {makcu-0.1.3.dist-info → makcu-0.1.4.dist-info}/WHEEL +0 -0
- {makcu-0.1.3.dist-info → makcu-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {makcu-0.1.3.dist-info → makcu-0.1.4.dist-info}/top_level.txt +0 -0
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.")
|
@@ -155,20 +176,27 @@ class SerialTransport:
|
|
155
176
|
def send_command(self, command, expect_response=False):
|
156
177
|
if not self._is_connected or not self.serial or not self.serial.is_open:
|
157
178
|
raise MakcuConnectionError("Serial connection not open.")
|
158
|
-
|
179
|
+
|
180
|
+
with self._command_lock:
|
159
181
|
try:
|
160
|
-
|
161
|
-
|
182
|
+
if expect_response:
|
183
|
+
self._response_buffer = ""
|
184
|
+
self._response_ready.clear()
|
185
|
+
self._waiting_for_response = True
|
186
|
+
|
162
187
|
self.serial.write(command.encode("ascii") + b"\r\n")
|
163
188
|
self.serial.flush()
|
189
|
+
|
164
190
|
if expect_response:
|
165
191
|
response = self.receive_response(sent_command=command)
|
192
|
+
self._waiting_for_response = False
|
166
193
|
if not response:
|
167
194
|
raise MakcuTimeoutError(f"No response from device for command: {command}")
|
168
195
|
return response
|
169
|
-
|
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)
|
@@ -176,11 +204,10 @@ class SerialTransport:
|
|
176
204
|
def get_button_mask(self) -> int:
|
177
205
|
return self._last_mask
|
178
206
|
|
179
|
-
|
180
207
|
def enable_button_monitoring(self, enable: bool = True):
|
181
208
|
self.send_command("km.buttons(1)" if enable else "km.buttons(0)")
|
182
209
|
|
183
|
-
def catch_button(self, button:
|
210
|
+
def catch_button(self, button: MouseButton):
|
184
211
|
command = {
|
185
212
|
"LEFT": "km.catch_ml(0)",
|
186
213
|
"RIGHT": "km.catch_mr(0)",
|
@@ -193,79 +220,71 @@ class SerialTransport:
|
|
193
220
|
else:
|
194
221
|
raise ValueError(f"Unsupported button: {button}")
|
195
222
|
|
196
|
-
def
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
"MOUSE4": "km.catch_ms1()",
|
202
|
-
"MOUSE5": "km.catch_ms2()",
|
203
|
-
}.get(button.upper())
|
204
|
-
if command:
|
205
|
-
result = self.send_command(command, expect_response=True)
|
206
|
-
try:
|
207
|
-
return int(result.strip())
|
208
|
-
except Exception:
|
209
|
-
return 0
|
210
|
-
else:
|
211
|
-
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
|
212
228
|
|
213
229
|
def _listen(self, debug=False):
|
214
230
|
self._log("Started listener thread")
|
215
|
-
button_states = {i: False for i in self.button_map}
|
216
231
|
self._last_mask = 0
|
217
|
-
|
232
|
+
ascii_buffer = bytearray()
|
233
|
+
response_lines = []
|
218
234
|
|
219
235
|
while self._is_connected and not self._stop_event.is_set():
|
220
|
-
if self._pause_listener:
|
221
|
-
time.sleep(0.001)
|
222
|
-
continue
|
223
|
-
|
224
236
|
try:
|
225
|
-
|
226
|
-
if not
|
237
|
+
data = self.serial.read(self.serial.in_waiting or 1)
|
238
|
+
if not data:
|
227
239
|
continue
|
228
240
|
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
246
|
+
for bit, name in self.button_map.items():
|
247
|
+
if changed_bits & (1 << bit):
|
248
|
+
is_pressed = bool(byte_val & (1 << bit))
|
249
|
+
self._button_states[name] = is_pressed
|
250
|
+
button_enum = self._button_enum_map.get(bit)
|
251
|
+
if button_enum and self._button_callback:
|
252
|
+
self._button_callback(button_enum, is_pressed)
|
253
|
+
|
254
|
+
self._last_mask = byte_val
|
234
255
|
|
235
|
-
if value != self._last_mask:
|
236
|
-
if byte_str.startswith("b'\\x00"):
|
237
|
-
for bit, name in self.button_map.items():
|
238
|
-
button_states[bit] = False
|
239
|
-
self._button_states[name] = False
|
240
|
-
if debug:
|
241
|
-
print(f"{name} -> False")
|
242
|
-
else:
|
243
|
-
for bit, name in self.button_map.items():
|
244
|
-
is_pressed = bool(value & (1 << bit))
|
245
|
-
button_states[bit] = is_pressed
|
246
|
-
self._button_states[name] = is_pressed
|
247
256
|
if debug:
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
if
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
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()
|
265
283
|
|
266
284
|
except serial.SerialException as e:
|
267
285
|
if "ClearCommError failed" not in str(e):
|
268
286
|
self._log(f"Serial error during listening: {e}")
|
269
287
|
break
|
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
@@ -25,8 +25,8 @@ class MakcuController:
|
|
25
25
|
|
26
26
|
def click(self, button: MouseButton):
|
27
27
|
self._check_connection()
|
28
|
-
self.
|
29
|
-
self.
|
28
|
+
self.mouse.press(button)
|
29
|
+
self.mouse.release(button)
|
30
30
|
|
31
31
|
def move(self, dx: int, dy: int):
|
32
32
|
self._check_connection()
|
@@ -100,23 +100,9 @@ class MakcuController:
|
|
100
100
|
self._check_connection()
|
101
101
|
return self.transport.get_button_mask()
|
102
102
|
|
103
|
-
def
|
103
|
+
def is_locked(self, button: MouseButton) -> bool:
|
104
104
|
self._check_connection()
|
105
|
-
return self.mouse.
|
106
|
-
|
107
|
-
#def capture(self, button: MouseButton):
|
108
|
-
# self._check_connection()
|
109
|
-
# self.mouse.begin_capture(button.name)
|
110
|
-
|
111
|
-
|
112
|
-
#def stop_capturing_clicks(self, button: str) -> int:
|
113
|
-
# self._check_connection()
|
114
|
-
# return self.mouse.stop_capturing_clicks(button)
|
115
|
-
|
116
|
-
#def get_captured_clicks(self, button: MouseButton) -> int:
|
117
|
-
# self._check_connection()
|
118
|
-
# return self.mouse.stop_capturing_clicks(button.name)
|
119
|
-
|
105
|
+
return self.mouse.is_locked(button)
|
120
106
|
|
121
107
|
def click_human_like(self, button: MouseButton, count: int = 1,
|
122
108
|
profile: str = "normal", jitter: int = 0):
|
@@ -139,11 +125,11 @@ class MakcuController:
|
|
139
125
|
dy = random.randint(-jitter, jitter)
|
140
126
|
self.mouse.move(dx, dy)
|
141
127
|
|
142
|
-
self.press(button)
|
128
|
+
self.mouse.press(button)
|
143
129
|
time.sleep(random.uniform(min_down, max_down) / 1000.0)
|
144
130
|
self.mouse.release(button)
|
145
131
|
time.sleep(random.uniform(min_wait, max_wait) / 1000.0)
|
146
|
-
|
132
|
+
|
147
133
|
def enable_button_monitoring(self, enable: bool = True):
|
148
134
|
self._check_connection()
|
149
135
|
self.transport.enable_button_monitoring(enable)
|
@@ -155,22 +141,19 @@ class MakcuController:
|
|
155
141
|
def get_all_lock_states(self) -> dict:
|
156
142
|
self._check_connection()
|
157
143
|
return self.mouse.get_all_lock_states()
|
158
|
-
|
159
|
-
def _send_button_command(self, button: MouseButton, state: int):
|
160
|
-
self.mouse._send_button_command(button, state)
|
161
144
|
|
162
145
|
def press(self, button: MouseButton):
|
163
146
|
self._check_connection()
|
164
|
-
self.
|
147
|
+
self.mouse.press(button)
|
165
148
|
|
166
149
|
def release(self, button: MouseButton):
|
167
150
|
self._check_connection()
|
168
|
-
self.
|
169
|
-
|
151
|
+
self.mouse.release(button)
|
152
|
+
|
170
153
|
def get_button_states(self) -> dict:
|
171
154
|
self._check_connection()
|
172
155
|
return self.transport.get_button_states()
|
173
156
|
|
174
|
-
def
|
157
|
+
def is_pressed(self, button: MouseButton) -> bool:
|
175
158
|
self._check_connection()
|
176
159
|
return self.transport.get_button_states().get(button.name.lower(), False)
|
makcu/mouse.py
CHANGED
@@ -1,6 +1,7 @@
|
|
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):
|
@@ -18,6 +19,12 @@ class Mouse:
|
|
18
19
|
raise MakcuCommandError(f"Unsupported button: {button}")
|
19
20
|
self.transport.send_command(f"km.{command_map[button]}({state})")
|
20
21
|
|
22
|
+
def press(self, button: MouseButton):
|
23
|
+
self._send_button_command(button, 1)
|
24
|
+
|
25
|
+
def release(self, button: MouseButton):
|
26
|
+
self._send_button_command(button, 0)
|
27
|
+
|
21
28
|
def move(self, x: int, y: int):
|
22
29
|
self.transport.send_command(f"km.move({x},{y})")
|
23
30
|
|
@@ -30,18 +37,33 @@ class Mouse:
|
|
30
37
|
def scroll(self, delta: int):
|
31
38
|
self.transport.send_command(f"km.wheel({delta})")
|
32
39
|
|
33
|
-
def lock_left(self, lock: bool):
|
40
|
+
def lock_left(self, lock: bool):
|
34
41
|
self.transport.send_command(f"km.lock_ml({int(lock)})")
|
35
|
-
|
36
|
-
def lock_middle(self, lock: bool):
|
37
|
-
|
38
|
-
|
39
|
-
def
|
40
|
-
|
41
|
-
|
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)})")
|
60
|
+
|
42
61
|
|
43
|
-
def spoof_serial(self, serial: str):
|
44
|
-
|
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)")
|
45
67
|
|
46
68
|
def get_device_info(self) -> dict:
|
47
69
|
port_name = self.transport.port
|
@@ -66,51 +88,80 @@ class Mouse:
|
|
66
88
|
def get_firmware_version(self) -> str:
|
67
89
|
return self.transport.send_command("km.version()", expect_response=True)
|
68
90
|
|
69
|
-
def
|
70
|
-
|
71
|
-
|
72
|
-
"
|
73
|
-
"
|
74
|
-
"LEFT": "km.lock_ml()",
|
75
|
-
"MIDDLE": "km.lock_mm()",
|
76
|
-
"RIGHT": "km.lock_mr()",
|
77
|
-
"MOUSE4": "km.lock_ms1()",
|
78
|
-
"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",
|
79
96
|
}
|
80
|
-
|
97
|
+
key = target.upper()
|
98
|
+
if key not in command_map:
|
81
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:
|
82
103
|
try:
|
83
|
-
|
84
|
-
|
85
|
-
except Exception:
|
86
|
-
return False
|
104
|
+
# Sleep for previous command to finish first, hoping to get rid of this soon.
|
105
|
+
time.sleep(0.03)
|
87
106
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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}")
|
137
|
+
return False
|
111
138
|
|
112
139
|
def get_all_lock_states(self) -> dict:
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
@@ -26,7 +26,7 @@ def test_port_connection(makcu):
|
|
26
26
|
@pytest.mark.skip(reason="Capture test disabled until firmware supports tracking clicks from software input")
|
27
27
|
def test_capture_right_clicks(makcu):
|
28
28
|
makcu.mouse.lock_right(True)
|
29
|
-
assert makcu.mouse.
|
29
|
+
assert makcu.mouse.is_locked(MouseButton.RIGHT)
|
30
30
|
|
31
31
|
makcu.mouse.begin_capture("RIGHT")
|
32
32
|
makcu.press(MouseButton.RIGHT)
|
@@ -35,7 +35,7 @@ def test_capture_right_clicks(makcu):
|
|
35
35
|
makcu.mouse.release(MouseButton.RIGHT)
|
36
36
|
|
37
37
|
makcu.mouse.lock_right(False)
|
38
|
-
assert not makcu.mouse.
|
38
|
+
assert not makcu.mouse.is_locked(MouseButton.RIGHT)
|
39
39
|
|
40
40
|
count = makcu.mouse.stop_capturing_clicks("RIGHT")
|
41
41
|
assert count >= 2, f"Expected >=2 captured clicks, got {count}"
|
@@ -55,29 +55,10 @@ def test_get_button_states(makcu):
|
|
55
55
|
def test_lock_state(makcu):
|
56
56
|
print("Locking LEFT button...")
|
57
57
|
makcu.lock_left(True)
|
58
|
-
|
59
|
-
time.sleep(0.1)
|
60
|
-
|
61
58
|
print("Querying lock state while LEFT is locked...")
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
all_states = makcu.get_all_lock_states()
|
66
|
-
print(f"All lock states: {all_states}")
|
67
|
-
|
68
|
-
assert all_states["LEFT"] is True
|
69
|
-
assert isinstance(all_states["RIGHT"], bool)
|
70
|
-
|
71
|
-
makcu.press(MouseButton.LEFT)
|
72
|
-
makcu.release(MouseButton.LEFT)
|
73
|
-
|
74
|
-
time.sleep(0.1)
|
75
|
-
|
76
|
-
print("Unlocking LEFT button...")
|
77
|
-
makcu.lock_left(False)
|
78
|
-
|
79
|
-
print("Rechecking LEFT lock state after unlock...")
|
80
|
-
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
|
81
62
|
|
82
63
|
def test_makcu_behavior(makcu):
|
83
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.3.dist-info/RECORD
DELETED
@@ -1,14 +0,0 @@
|
|
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=QObUZ-iNH-LaC7hamgXDH5p2wH_VfJ6OEAv5qJ1RwVE,10208
|
5
|
-
makcu/controller.py,sha256=u2BeScKqQI19nSXQzRZqNL9rIj7_AyGDRcawdHvQtxM,5776
|
6
|
-
makcu/enums.py,sha256=VmvCLmpghVHuTAkvCGMfA14MgWTtFVMfsGQQNnJ58Ts,126
|
7
|
-
makcu/errors.py,sha256=4CkQ4gKa7GL5-BO3yOAJMMsy3QlUDDL42S1P1clqV4A,562
|
8
|
-
makcu/mouse.py,sha256=vvJ88r9tOLaGT6evHHx_K45wwTa_bxc9c0S6wj7gX6o,4686
|
9
|
-
makcu/test_suite.py,sha256=kmsLRv00mWLu3cUW5iAWL3QAhdqOL-rUwAWn6Rs1_ME,3104
|
10
|
-
makcu-0.1.3.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
11
|
-
makcu-0.1.3.dist-info/METADATA,sha256=hIYmeM8Qd9IWNCEfE_tbZ4DHqe-ttU9uvE8247ih2w0,5588
|
12
|
-
makcu-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
13
|
-
makcu-0.1.3.dist-info/top_level.txt,sha256=IRO1UVb5LK_ovjau0g4oObyXQqy00tVEE-yF5lPgw1w,6
|
14
|
-
makcu-0.1.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|