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/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
- def __init__(self, transport):
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
- 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:
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
- self.transport.send_command(f"km.{command_map[button]}({state})")
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._send_button_command(button, 1)
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._send_button_command(button, 0)
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 move_smooth(self, x: int, y: int, segments: int):
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
- def lock_left(self, lock: bool):
41
- self.transport.send_command(f"km.lock_ml({int(lock)})")
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
- def lock_middle(self, lock: bool):
44
- self.transport.send_command(f"km.lock_mm({int(lock)})")
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 lock_right(self, lock: bool):
47
- self.transport.send_command(f"km.lock_mr({int(lock)})")
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 lock_side1(self, lock: bool):
50
- self.transport.send_command(f"km.lock_ms1({int(lock)})")
123
+ def lock_right(self, lock: bool) -> None:
124
+ self._set_lock("RIGHT", lock)
51
125
 
52
- def lock_side2(self, lock: bool):
53
- self.transport.send_command(f"km.lock_ms2({int(lock)})")
126
+ def lock_side1(self, lock: bool) -> None:
127
+ self._set_lock("MOUSE4", lock)
54
128
 
55
- def lock_x(self, lock: bool):
56
- self.transport.send_command(f"km.lock_mx({int(lock)})")
129
+ def lock_side2(self, lock: bool) -> None:
130
+ self._set_lock("MOUSE5", lock)
57
131
 
58
- def lock_y(self, lock: bool):
59
- self.transport.send_command(f"km.lock_my({int(lock)})")
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) -> dict:
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": "Unknown",
74
- "vid": "Unknown",
159
+ "description": "Connected Device",
160
+ "vid": "Unknown",
75
161
  "pid": "Unknown",
76
- "isConnected": is_connected
162
+ "isConnected": True
77
163
  }
78
- for port in list_ports.comports():
79
- if port.device == port_name:
80
- info.update({
81
- "description": port.description,
82
- "vid": hex(port.vid) if port.vid is not None else "Unknown",
83
- "pid": hex(port.pid) if port.pid is not None else "Unknown"
84
- })
85
- break
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
- return self.transport.send_command("km.version()", expect_response=True)
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
- 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",
96
- }
97
- key = target.upper()
98
- if key not in command_map:
99
- raise ValueError(f"Unsupported lock target: {target}")
100
- return f"km.lock_{command_map[key]}()"
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
- # 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}'")
226
+ target_name = button.name if hasattr(button, 'name') else str(button)
112
227
 
113
- result = result.strip()
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
- if result in ['1', '0']:
116
- return result == '1'
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
- lines = result.split('\n')
119
- for line in lines:
120
- line = line.strip()
121
- if line in ['1', '0']:
122
- return line == '1'
237
+ if not result:
238
+ return False
123
239
 
124
- import re
125
- numbers = re.findall(r'\b[01]\b', result)
126
- if numbers:
127
- return numbers[-1] == '1'
240
+ result = result.strip()
241
+ is_locked = result == '1'
128
242
 
129
- if hasattr(self.transport, 'debug') and self.transport.debug:
130
- print(f"Could not parse lock status from: '{result}'")
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 False
249
+ return is_locked
133
250
 
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
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
@@ -0,0 +1,2 @@
1
+ # This file is intentionally empty.
2
+ # It serves as a marker to indicate that this package supports type hints.
makcu/test_suite.py CHANGED
@@ -1,6 +1,17 @@
1
- import pytest, time
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 ['left', 'right', 'middle', 'mouse4', 'mouse5']:
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) # Check state AFTER ensuring it's locked
53
+ state = makcu.is_locked(MouseButton.LEFT)
60
54
  print(state)
61
- assert state # Now assert the current 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 test_reset_all(makcu):
69
- makcu.mouse.lock_left(False)
70
- makcu.mouse.lock_right(False)
71
- makcu.mouse.lock_middle(False)
72
- makcu.mouse.lock_side1(False)
73
- makcu.mouse.lock_side2(False)
74
- makcu.mouse.lock_x(False)
75
- makcu.mouse.lock_y(False)
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
- makcu.enable_button_monitoring(False)
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