makcu 0.1.4__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- makcu/__init__.py +53 -9
- makcu/__main__.py +238 -37
- makcu/conftest.py +26 -17
- makcu/connection.py +399 -227
- makcu/controller.py +295 -77
- makcu/errors.py +0 -5
- makcu/makcu.pyi +13 -0
- makcu/mouse.py +201 -116
- makcu/py.typed +2 -0
- makcu/test_suite.py +97 -33
- makcu-0.2.1.dist-info/METADATA +1141 -0
- makcu-0.2.1.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.1.dist-info}/WHEEL +0 -0
- {makcu-0.1.4.dist-info → makcu-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {makcu-0.1.4.dist-info → makcu-0.2.1.dist-info}/top_level.txt +0 -0
makcu/mouse.py
CHANGED
@@ -1,167 +1,252 @@
|
|
1
|
+
from typing import Dict, Optional, List, Union, Any
|
1
2
|
from .enums import MouseButton
|
3
|
+
from .connection import SerialTransport
|
2
4
|
from .errors import MakcuCommandError
|
3
5
|
from serial.tools import list_ports
|
4
6
|
import time
|
5
7
|
|
8
|
+
|
9
|
+
class AxisButton:
|
10
|
+
def __init__(self, name: str) -> None:
|
11
|
+
self.name = name
|
12
|
+
|
6
13
|
class Mouse:
|
7
|
-
|
14
|
+
|
15
|
+
|
16
|
+
_BUTTON_COMMANDS = {
|
17
|
+
MouseButton.LEFT: "left",
|
18
|
+
MouseButton.RIGHT: "right",
|
19
|
+
MouseButton.MIDDLE: "middle",
|
20
|
+
MouseButton.MOUSE4: "ms1",
|
21
|
+
MouseButton.MOUSE5: "ms2",
|
22
|
+
}
|
23
|
+
|
24
|
+
|
25
|
+
_PRESS_COMMANDS = {}
|
26
|
+
_RELEASE_COMMANDS = {}
|
27
|
+
_LOCK_COMMANDS = {}
|
28
|
+
_UNLOCK_COMMANDS = {}
|
29
|
+
_LOCK_QUERY_COMMANDS = {}
|
30
|
+
|
31
|
+
def __init__(self, transport: SerialTransport) -> None:
|
8
32
|
self.transport = transport
|
33
|
+
self._lock_states_cache: int = 0
|
34
|
+
self._cache_valid = False
|
35
|
+
|
9
36
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
37
|
+
self._init_command_cache()
|
38
|
+
|
39
|
+
def _init_command_cache(self) -> None:
|
40
|
+
|
41
|
+
for button, cmd in self._BUTTON_COMMANDS.items():
|
42
|
+
self._PRESS_COMMANDS[button] = f"km.{cmd}(1)"
|
43
|
+
self._RELEASE_COMMANDS[button] = f"km.{cmd}(0)"
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
lock_targets = [
|
48
|
+
("LEFT", "ml", 0),
|
49
|
+
("RIGHT", "mr", 1),
|
50
|
+
("MIDDLE", "mm", 2),
|
51
|
+
("MOUSE4", "ms1", 3),
|
52
|
+
("MOUSE5", "ms2", 4),
|
53
|
+
("X", "mx", 5),
|
54
|
+
("Y", "my", 6),
|
55
|
+
]
|
56
|
+
|
57
|
+
for name, cmd, bit in lock_targets:
|
58
|
+
self._LOCK_COMMANDS[name] = (f"km.lock_{cmd}(1)", bit)
|
59
|
+
self._UNLOCK_COMMANDS[name] = (f"km.lock_{cmd}(0)", bit)
|
60
|
+
self._LOCK_QUERY_COMMANDS[name] = (f"km.lock_{cmd}()", bit)
|
61
|
+
|
62
|
+
def _send_button_command(self, button: MouseButton, state: int) -> None:
|
63
|
+
if button not in self._BUTTON_COMMANDS:
|
19
64
|
raise MakcuCommandError(f"Unsupported button: {button}")
|
20
|
-
|
65
|
+
|
66
|
+
|
67
|
+
cmd = self._PRESS_COMMANDS[button] if state else self._RELEASE_COMMANDS[button]
|
68
|
+
self.transport.send_command(cmd)
|
21
69
|
|
22
|
-
def press(self, button: MouseButton):
|
23
|
-
self.
|
70
|
+
def press(self, button: MouseButton) -> None:
|
71
|
+
self.transport.send_command(self._PRESS_COMMANDS[button])
|
24
72
|
|
25
|
-
def release(self, button: MouseButton):
|
26
|
-
self.
|
73
|
+
def release(self, button: MouseButton) -> None:
|
74
|
+
self.transport.send_command(self._RELEASE_COMMANDS[button])
|
27
75
|
|
28
|
-
def move(self, x: int, y: int):
|
76
|
+
def move(self, x: int, y: int) -> None:
|
29
77
|
self.transport.send_command(f"km.move({x},{y})")
|
30
78
|
|
31
|
-
def
|
79
|
+
def click(self, button: MouseButton) -> None:
|
80
|
+
if button not in self._BUTTON_COMMANDS:
|
81
|
+
raise MakcuCommandError(f"Unsupported button: {button}")
|
82
|
+
|
83
|
+
|
84
|
+
press_cmd = self._PRESS_COMMANDS[button]
|
85
|
+
release_cmd = self._RELEASE_COMMANDS[button]
|
86
|
+
|
87
|
+
|
88
|
+
transport = self.transport
|
89
|
+
transport.send_command(press_cmd)
|
90
|
+
transport.send_command(release_cmd)
|
91
|
+
|
92
|
+
def move_smooth(self, x: int, y: int, segments: int) -> None:
|
32
93
|
self.transport.send_command(f"km.move({x},{y},{segments})")
|
33
94
|
|
34
|
-
def move_bezier(self, x: int, y: int, segments: int, ctrl_x: int, ctrl_y: int):
|
95
|
+
def move_bezier(self, x: int, y: int, segments: int, ctrl_x: int, ctrl_y: int) -> None:
|
35
96
|
self.transport.send_command(f"km.move({x},{y},{segments},{ctrl_x},{ctrl_y})")
|
36
97
|
|
37
|
-
def scroll(self, delta: int):
|
98
|
+
def scroll(self, delta: int) -> None:
|
38
99
|
self.transport.send_command(f"km.wheel({delta})")
|
39
100
|
|
40
|
-
|
41
|
-
|
101
|
+
|
102
|
+
def _set_lock(self, name: str, lock: bool) -> None:
|
103
|
+
if lock:
|
104
|
+
cmd, bit = self._LOCK_COMMANDS[name]
|
105
|
+
else:
|
106
|
+
cmd, bit = self._UNLOCK_COMMANDS[name]
|
42
107
|
|
43
|
-
|
44
|
-
|
108
|
+
self.transport.send_command(cmd)
|
109
|
+
|
110
|
+
|
111
|
+
if lock:
|
112
|
+
self._lock_states_cache |= (1 << bit)
|
113
|
+
else:
|
114
|
+
self._lock_states_cache &= ~(1 << bit)
|
115
|
+
self._cache_valid = True
|
45
116
|
|
46
|
-
def
|
47
|
-
self.
|
117
|
+
def lock_left(self, lock: bool) -> None:
|
118
|
+
self._set_lock("LEFT", lock)
|
119
|
+
|
120
|
+
def lock_middle(self, lock: bool) -> None:
|
121
|
+
self._set_lock("MIDDLE", lock)
|
48
122
|
|
49
|
-
def
|
50
|
-
self.
|
123
|
+
def lock_right(self, lock: bool) -> None:
|
124
|
+
self._set_lock("RIGHT", lock)
|
51
125
|
|
52
|
-
def
|
53
|
-
self.
|
126
|
+
def lock_side1(self, lock: bool) -> None:
|
127
|
+
self._set_lock("MOUSE4", lock)
|
54
128
|
|
55
|
-
def
|
56
|
-
self.
|
129
|
+
def lock_side2(self, lock: bool) -> None:
|
130
|
+
self._set_lock("MOUSE5", lock)
|
57
131
|
|
58
|
-
def
|
59
|
-
self.
|
132
|
+
def lock_x(self, lock: bool) -> None:
|
133
|
+
self._set_lock("X", lock)
|
60
134
|
|
135
|
+
def lock_y(self, lock: bool) -> None:
|
136
|
+
self._set_lock("Y", lock)
|
61
137
|
|
62
|
-
def spoof_serial(self, serial: str):
|
138
|
+
def spoof_serial(self, serial: str) -> None:
|
63
139
|
self.transport.send_command(f"km.serial('{serial}')")
|
64
140
|
|
65
|
-
def reset_serial(self):
|
141
|
+
def reset_serial(self) -> None:
|
66
142
|
self.transport.send_command("km.serial(0)")
|
67
143
|
|
68
|
-
def get_device_info(self) ->
|
144
|
+
def get_device_info(self) -> Dict[str, Union[str, bool]]:
|
69
145
|
port_name = self.transport.port
|
70
146
|
is_connected = self.transport.is_connected()
|
147
|
+
|
148
|
+
if not is_connected or not port_name:
|
149
|
+
return {
|
150
|
+
"port": port_name or "Unknown",
|
151
|
+
"description": "Disconnected",
|
152
|
+
"vid": "Unknown",
|
153
|
+
"pid": "Unknown",
|
154
|
+
"isConnected": False
|
155
|
+
}
|
156
|
+
|
71
157
|
info = {
|
72
158
|
"port": port_name,
|
73
|
-
"description": "
|
74
|
-
"vid": "Unknown",
|
159
|
+
"description": "Connected Device",
|
160
|
+
"vid": "Unknown",
|
75
161
|
"pid": "Unknown",
|
76
|
-
"isConnected":
|
162
|
+
"isConnected": True
|
77
163
|
}
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
"
|
83
|
-
|
84
|
-
|
85
|
-
|
164
|
+
|
165
|
+
try:
|
166
|
+
for port in list_ports.comports():
|
167
|
+
if port.device == port_name:
|
168
|
+
info["description"] = port.description or "Connected Device"
|
169
|
+
if port.vid is not None:
|
170
|
+
info["vid"] = f"0x{port.vid:04x}"
|
171
|
+
if port.pid is not None:
|
172
|
+
info["pid"] = f"0x{port.pid:04x}"
|
173
|
+
break
|
174
|
+
except Exception:
|
175
|
+
pass
|
176
|
+
|
86
177
|
return info
|
87
178
|
|
88
179
|
def get_firmware_version(self) -> str:
|
89
|
-
|
180
|
+
response = self.transport.send_command("km.version()", expect_response=True, timeout=0.1)
|
181
|
+
return response or ""
|
182
|
+
|
183
|
+
def _invalidate_cache(self) -> None:
|
184
|
+
self._cache_valid = False
|
185
|
+
|
186
|
+
def get_all_lock_states(self) -> Dict[str, bool]:
|
187
|
+
|
188
|
+
if self._cache_valid:
|
189
|
+
return {
|
190
|
+
"X": bool(self._lock_states_cache & (1 << 5)),
|
191
|
+
"Y": bool(self._lock_states_cache & (1 << 6)),
|
192
|
+
"LEFT": bool(self._lock_states_cache & (1 << 0)),
|
193
|
+
"RIGHT": bool(self._lock_states_cache & (1 << 1)),
|
194
|
+
"MIDDLE": bool(self._lock_states_cache & (1 << 2)),
|
195
|
+
"MOUSE4": bool(self._lock_states_cache & (1 << 3)),
|
196
|
+
"MOUSE5": bool(self._lock_states_cache & (1 << 4)),
|
197
|
+
}
|
198
|
+
|
90
199
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
200
|
+
states = {}
|
201
|
+
targets = ["X", "Y", "LEFT", "RIGHT", "MIDDLE", "MOUSE4", "MOUSE5"]
|
202
|
+
|
203
|
+
for target in targets:
|
204
|
+
cmd, bit = self._LOCK_QUERY_COMMANDS[target]
|
205
|
+
try:
|
206
|
+
result = self.transport.send_command(cmd, expect_response=True, timeout=0.05)
|
207
|
+
if result and result.strip() in ['0', '1']:
|
208
|
+
is_locked = result.strip() == '1'
|
209
|
+
states[target] = is_locked
|
210
|
+
|
211
|
+
|
212
|
+
if is_locked:
|
213
|
+
self._lock_states_cache |= (1 << bit)
|
214
|
+
else:
|
215
|
+
self._lock_states_cache &= ~(1 << bit)
|
216
|
+
else:
|
217
|
+
states[target] = False
|
218
|
+
except Exception:
|
219
|
+
states[target] = False
|
220
|
+
|
221
|
+
self._cache_valid = True
|
222
|
+
return states
|
101
223
|
|
102
|
-
def is_locked(self, button: MouseButton) -> bool:
|
224
|
+
def is_locked(self, button: Union[MouseButton, AxisButton]) -> bool:
|
103
225
|
try:
|
104
|
-
|
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}'")
|
226
|
+
target_name = button.name if hasattr(button, 'name') else str(button)
|
112
227
|
|
113
|
-
|
228
|
+
|
229
|
+
if self._cache_valid and target_name in self._LOCK_QUERY_COMMANDS:
|
230
|
+
_, bit = self._LOCK_QUERY_COMMANDS[target_name]
|
231
|
+
return bool(self._lock_states_cache & (1 << bit))
|
114
232
|
|
115
|
-
|
116
|
-
|
233
|
+
|
234
|
+
cmd, bit = self._LOCK_QUERY_COMMANDS[target_name]
|
235
|
+
result = self.transport.send_command(cmd, expect_response=True, timeout=0.05)
|
117
236
|
|
118
|
-
|
119
|
-
|
120
|
-
line = line.strip()
|
121
|
-
if line in ['1', '0']:
|
122
|
-
return line == '1'
|
237
|
+
if not result:
|
238
|
+
return False
|
123
239
|
|
124
|
-
|
125
|
-
|
126
|
-
if numbers:
|
127
|
-
return numbers[-1] == '1'
|
240
|
+
result = result.strip()
|
241
|
+
is_locked = result == '1'
|
128
242
|
|
129
|
-
|
130
|
-
|
243
|
+
|
244
|
+
if is_locked:
|
245
|
+
self._lock_states_cache |= (1 << bit)
|
246
|
+
else:
|
247
|
+
self._lock_states_cache &= ~(1 << bit)
|
131
248
|
|
132
|
-
return
|
249
|
+
return is_locked
|
133
250
|
|
134
|
-
except Exception
|
135
|
-
|
136
|
-
print(f"Error checking lock status: {e}")
|
137
|
-
return False
|
138
|
-
|
139
|
-
def get_all_lock_states(self) -> dict:
|
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
|
251
|
+
except Exception:
|
252
|
+
return False
|
makcu/py.typed
ADDED
makcu/test_suite.py
CHANGED
@@ -1,6 +1,17 @@
|
|
1
|
-
import pytest
|
1
|
+
import pytest
|
2
|
+
import time
|
2
3
|
from makcu import MouseButton
|
3
4
|
|
5
|
+
|
6
|
+
TEST_BUTTONS = (MouseButton.LEFT, MouseButton.RIGHT, MouseButton.MIDDLE)
|
7
|
+
BUTTON_STATE_KEYS = ('left', 'right', 'middle', 'mouse4', 'mouse5')
|
8
|
+
MOVE_COORDS = ((10, 0), (0, 10), (-10, 0), (0, -10))
|
9
|
+
|
10
|
+
def test_connect_to_port(makcu):
|
11
|
+
print("Connecting to port...")
|
12
|
+
makcu.connect()
|
13
|
+
assert makcu.is_connected(), "Failed to connect to the makcu"
|
14
|
+
|
4
15
|
def test_press_and_release(makcu):
|
5
16
|
makcu.press(MouseButton.LEFT)
|
6
17
|
makcu.release(MouseButton.LEFT)
|
@@ -23,23 +34,6 @@ def test_device_info(makcu):
|
|
23
34
|
def test_port_connection(makcu):
|
24
35
|
assert makcu.is_connected()
|
25
36
|
|
26
|
-
@pytest.mark.skip(reason="Capture test disabled until firmware supports tracking clicks from software input")
|
27
|
-
def test_capture_right_clicks(makcu):
|
28
|
-
makcu.mouse.lock_right(True)
|
29
|
-
assert makcu.mouse.is_locked(MouseButton.RIGHT)
|
30
|
-
|
31
|
-
makcu.mouse.begin_capture("RIGHT")
|
32
|
-
makcu.press(MouseButton.RIGHT)
|
33
|
-
makcu.mouse.release(MouseButton.RIGHT)
|
34
|
-
makcu.press(MouseButton.RIGHT)
|
35
|
-
makcu.mouse.release(MouseButton.RIGHT)
|
36
|
-
|
37
|
-
makcu.mouse.lock_right(False)
|
38
|
-
assert not makcu.mouse.is_locked(MouseButton.RIGHT)
|
39
|
-
|
40
|
-
count = makcu.mouse.stop_capturing_clicks("RIGHT")
|
41
|
-
assert count >= 2, f"Expected >=2 captured clicks, got {count}"
|
42
|
-
|
43
37
|
def test_button_mask(makcu):
|
44
38
|
print("Getting button mask...")
|
45
39
|
mask = makcu.get_button_mask()
|
@@ -49,33 +43,103 @@ def test_button_mask(makcu):
|
|
49
43
|
def test_get_button_states(makcu):
|
50
44
|
states = makcu.get_button_states()
|
51
45
|
assert isinstance(states, dict)
|
52
|
-
for key in
|
46
|
+
for key in BUTTON_STATE_KEYS:
|
53
47
|
assert key in states
|
54
48
|
|
55
49
|
def test_lock_state(makcu):
|
56
50
|
print("Locking LEFT button...")
|
57
51
|
makcu.lock_left(True)
|
58
52
|
print("Querying lock state while LEFT is locked...")
|
59
|
-
state = makcu.is_locked(MouseButton.LEFT)
|
53
|
+
state = makcu.is_locked(MouseButton.LEFT)
|
60
54
|
print(state)
|
61
|
-
assert state
|
55
|
+
assert state
|
62
56
|
|
63
57
|
def test_makcu_behavior(makcu):
|
64
58
|
makcu.move(25, 25)
|
65
59
|
makcu.click(MouseButton.LEFT)
|
66
60
|
makcu.scroll(-2)
|
67
61
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
makcu.
|
75
|
-
makcu.
|
62
|
+
def test_batch_commands(makcu):
|
63
|
+
print("Testing batch command execution (10 commands)...")
|
64
|
+
|
65
|
+
start_time = time.perf_counter()
|
66
|
+
|
67
|
+
|
68
|
+
makcu.move(10, 0)
|
69
|
+
makcu.click(MouseButton.LEFT)
|
70
|
+
makcu.move(0, 10)
|
71
|
+
makcu.press(MouseButton.RIGHT)
|
72
|
+
makcu.release(MouseButton.RIGHT)
|
73
|
+
makcu.scroll(-1)
|
74
|
+
makcu.move(-10, 0)
|
75
|
+
makcu.click(MouseButton.MIDDLE)
|
76
|
+
makcu.move(0, -10)
|
77
|
+
makcu.scroll(1)
|
78
|
+
|
79
|
+
end_time = time.perf_counter()
|
80
|
+
elapsed_ms = (end_time - start_time) * 1000
|
81
|
+
|
82
|
+
print(f"Batch execution time: {elapsed_ms:.2f}ms")
|
83
|
+
print(f"Average per command: {elapsed_ms/10:.2f}ms")
|
84
|
+
|
85
|
+
|
86
|
+
assert elapsed_ms < 50, f"Batch commands took {elapsed_ms:.2f}ms, expected < 50ms"
|
87
|
+
|
88
|
+
|
89
|
+
start_time = time.perf_counter()
|
90
|
+
for _ in range(10):
|
91
|
+
makcu.move(5, 5)
|
92
|
+
end_time = time.perf_counter()
|
93
|
+
|
94
|
+
move_only_ms = (end_time - start_time) * 1000
|
95
|
+
print(f"10 move commands: {move_only_ms:.2f}ms ({move_only_ms/10:.2f}ms per move)")
|
96
|
+
|
97
|
+
def test_rapid_moves(makcu):
|
98
|
+
start = time.perf_counter_ns()
|
99
|
+
|
100
|
+
|
101
|
+
makcu.move(5, 5)
|
102
|
+
makcu.move(5, 5)
|
103
|
+
makcu.move(5, 5)
|
104
|
+
makcu.move(5, 5)
|
105
|
+
makcu.move(5, 5)
|
106
|
+
makcu.move(5, 5)
|
107
|
+
makcu.move(5, 5)
|
108
|
+
makcu.move(5, 5)
|
109
|
+
makcu.move(5, 5)
|
110
|
+
makcu.move(5, 5)
|
111
|
+
|
112
|
+
elapsed_ms = (time.perf_counter_ns() - start) / 1_000_000
|
113
|
+
print(f"10 rapid moves: {elapsed_ms:.2f}ms")
|
114
|
+
assert elapsed_ms < 30
|
115
|
+
|
116
|
+
def test_button_performance(makcu):
|
117
|
+
start = time.perf_counter_ns()
|
118
|
+
|
119
|
+
|
120
|
+
for button in TEST_BUTTONS:
|
121
|
+
makcu.press(button)
|
122
|
+
makcu.release(button)
|
123
|
+
|
124
|
+
elapsed_ms = (time.perf_counter_ns() - start) / 1_000_000
|
125
|
+
print(f"Button operations: {elapsed_ms:.2f}ms")
|
126
|
+
assert elapsed_ms < 20
|
127
|
+
|
128
|
+
def test_mixed_operations(makcu):
|
129
|
+
start = time.perf_counter_ns()
|
130
|
+
|
131
|
+
|
132
|
+
makcu.move(20, 20)
|
133
|
+
makcu.press(MouseButton.LEFT)
|
134
|
+
makcu.move(-20, -20)
|
135
|
+
makcu.release(MouseButton.LEFT)
|
136
|
+
makcu.scroll(1)
|
137
|
+
|
138
|
+
elapsed_ms = (time.perf_counter_ns() - start) / 1_000_000
|
139
|
+
print(f"Mixed operations: {elapsed_ms:.2f}ms")
|
140
|
+
assert elapsed_ms < 15
|
76
141
|
|
77
|
-
states = makcu.mouse.get_all_lock_states()
|
78
|
-
assert all(state is False for state in states.values() if state is not None), \
|
79
|
-
f"Expected all unlocked, got: {states}"
|
80
142
|
|
81
|
-
|
143
|
+
@pytest.mark.skip(reason="Capture test disabled until firmware supports tracking clicks from software input")
|
144
|
+
def test_capture_right_clicks(makcu):
|
145
|
+
pass
|