makcu 2.2.2__py3-none-any.whl → 2.3.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/README.md +405 -405
- makcu/__init__.py +1 -1
- makcu/controller.py +425 -410
- makcu/mouse.py +275 -250
- {makcu-2.2.2.dist-info → makcu-2.3.1.dist-info}/METADATA +2 -2
- {makcu-2.2.2.dist-info → makcu-2.3.1.dist-info}/RECORD +9 -9
- {makcu-2.2.2.dist-info → makcu-2.3.1.dist-info}/WHEEL +0 -0
- {makcu-2.2.2.dist-info → makcu-2.3.1.dist-info}/licenses/LICENSE +0 -0
- {makcu-2.2.2.dist-info → makcu-2.3.1.dist-info}/top_level.txt +0 -0
makcu/mouse.py
CHANGED
@@ -1,250 +1,275 @@
|
|
1
|
-
from typing import Dict, Union
|
2
|
-
from .enums import MouseButton
|
3
|
-
from .connection import SerialTransport
|
4
|
-
from .errors import MakcuCommandError
|
5
|
-
from serial.tools import list_ports
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
_BUTTON_COMMANDS = {
|
15
|
-
MouseButton.LEFT: "left",
|
16
|
-
MouseButton.RIGHT: "right",
|
17
|
-
MouseButton.MIDDLE: "middle",
|
18
|
-
MouseButton.MOUSE4: "ms1",
|
19
|
-
MouseButton.MOUSE5: "ms2",
|
20
|
-
}
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
self.
|
31
|
-
self.
|
32
|
-
self.
|
33
|
-
|
34
|
-
|
35
|
-
self.
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
("
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
("
|
51
|
-
("
|
52
|
-
("
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
def
|
122
|
-
self.
|
123
|
-
|
124
|
-
def
|
125
|
-
self.
|
126
|
-
|
127
|
-
def
|
128
|
-
self.
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
self.transport.send_command(
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
1
|
+
from typing import Dict, Union
|
2
|
+
from .enums import MouseButton
|
3
|
+
from .connection import SerialTransport
|
4
|
+
from .errors import MakcuCommandError
|
5
|
+
from serial.tools import list_ports
|
6
|
+
import ctypes
|
7
|
+
import time
|
8
|
+
|
9
|
+
class AxisButton:
|
10
|
+
def __init__(self, name: str) -> None:
|
11
|
+
self.name = name
|
12
|
+
|
13
|
+
class Mouse:
|
14
|
+
_BUTTON_COMMANDS = {
|
15
|
+
MouseButton.LEFT: "left",
|
16
|
+
MouseButton.RIGHT: "right",
|
17
|
+
MouseButton.MIDDLE: "middle",
|
18
|
+
MouseButton.MOUSE4: "ms1",
|
19
|
+
MouseButton.MOUSE5: "ms2",
|
20
|
+
}
|
21
|
+
|
22
|
+
_PRESS_COMMANDS = {}
|
23
|
+
_RELEASE_COMMANDS = {}
|
24
|
+
_LOCK_COMMANDS = {}
|
25
|
+
_UNLOCK_COMMANDS = {}
|
26
|
+
_LOCK_QUERY_COMMANDS = {}
|
27
|
+
|
28
|
+
def __init__(self, transport: SerialTransport) -> None:
|
29
|
+
self.transport = transport
|
30
|
+
self._lock_states_cache: int = 0
|
31
|
+
self._cache_valid = False
|
32
|
+
self._init_command_cache()
|
33
|
+
|
34
|
+
def _init_command_cache(self) -> None:
|
35
|
+
for button, cmd in self._BUTTON_COMMANDS.items():
|
36
|
+
self._PRESS_COMMANDS[button] = f"km.{cmd}(1)"
|
37
|
+
self._RELEASE_COMMANDS[button] = f"km.{cmd}(0)"
|
38
|
+
|
39
|
+
lock_targets = [
|
40
|
+
("LEFT", "ml", 0),
|
41
|
+
("RIGHT", "mr", 1),
|
42
|
+
("MIDDLE", "mm", 2),
|
43
|
+
("MOUSE4", "ms1", 3),
|
44
|
+
("MOUSE5", "ms2", 4),
|
45
|
+
("X", "mx", 5),
|
46
|
+
("Y", "my", 6),
|
47
|
+
]
|
48
|
+
|
49
|
+
for name, cmd, bit in lock_targets:
|
50
|
+
self._LOCK_COMMANDS[name] = (f"km.lock_{cmd}(1)", bit)
|
51
|
+
self._UNLOCK_COMMANDS[name] = (f"km.lock_{cmd}(0)", bit)
|
52
|
+
self._LOCK_QUERY_COMMANDS[name] = (f"km.lock_{cmd}()", bit)
|
53
|
+
|
54
|
+
def _send_button_command(self, button: MouseButton, state: int) -> None:
|
55
|
+
if button not in self._BUTTON_COMMANDS:
|
56
|
+
raise MakcuCommandError(f"Unsupported button: {button}")
|
57
|
+
|
58
|
+
cmd = self._PRESS_COMMANDS[button] if state else self._RELEASE_COMMANDS[button]
|
59
|
+
self.transport.send_command(cmd)
|
60
|
+
|
61
|
+
def press(self, button: MouseButton) -> None:
|
62
|
+
self.transport.send_command(self._PRESS_COMMANDS[button])
|
63
|
+
|
64
|
+
def release(self, button: MouseButton) -> None:
|
65
|
+
self.transport.send_command(self._RELEASE_COMMANDS[button])
|
66
|
+
|
67
|
+
def move(self, x: int, y: int) -> None:
|
68
|
+
self.transport.send_command(f"km.move({x},{y})")
|
69
|
+
|
70
|
+
def move_abs(self,
|
71
|
+
target: tuple[int, int],
|
72
|
+
speed: int = 1,
|
73
|
+
wait_ms: int = 2) -> None:
|
74
|
+
|
75
|
+
def get_mouse_speed_multiplier() -> float:
|
76
|
+
"""Return multiplier to convert pixels to mickeys based on Windows pointer speed."""
|
77
|
+
SPI_GETMOUSESPEED = 0x0070
|
78
|
+
speed = ctypes.c_uint()
|
79
|
+
ctypes.windll.user32.SystemParametersInfoW(
|
80
|
+
SPI_GETMOUSESPEED, 0, ctypes.byref(speed), 0
|
81
|
+
)
|
82
|
+
return speed.value / 10.0
|
83
|
+
|
84
|
+
def get_cursor_pos():
|
85
|
+
class POINT(ctypes.Structure):
|
86
|
+
_fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)]
|
87
|
+
pt = POINT()
|
88
|
+
ctypes.windll.user32.GetCursorPos(ctypes.byref(pt))
|
89
|
+
return pt.x, pt.y
|
90
|
+
|
91
|
+
multiplier = get_mouse_speed_multiplier()
|
92
|
+
end_x, end_y = target
|
93
|
+
|
94
|
+
# clamp speed to be between 1-14. >15 creates an infinite overflick loop issue.
|
95
|
+
speed = max(1, min(speed, 14))
|
96
|
+
|
97
|
+
while True:
|
98
|
+
cx, cy = get_cursor_pos()
|
99
|
+
dx, dy = end_x - cx, end_y - cy
|
100
|
+
|
101
|
+
if abs(dx) <= 1 and abs(dy) <= 1:
|
102
|
+
break
|
103
|
+
|
104
|
+
move_x = max(-speed, min(speed, int(dx / multiplier)))
|
105
|
+
move_y = max(-speed, min(speed, int(dy / multiplier)))
|
106
|
+
|
107
|
+
self.transport.send_command(f"km.move({move_x},{move_y})")
|
108
|
+
time.sleep(wait_ms / 1000)
|
109
|
+
|
110
|
+
def click(self, button: MouseButton) -> None:
|
111
|
+
if button not in self._BUTTON_COMMANDS:
|
112
|
+
raise MakcuCommandError(f"Unsupported button: {button}")
|
113
|
+
|
114
|
+
press_cmd = self._PRESS_COMMANDS[button]
|
115
|
+
release_cmd = self._RELEASE_COMMANDS[button]
|
116
|
+
|
117
|
+
transport = self.transport
|
118
|
+
transport.send_command(press_cmd)
|
119
|
+
transport.send_command(release_cmd)
|
120
|
+
|
121
|
+
def move_smooth(self, x: int, y: int, segments: int) -> None:
|
122
|
+
self.transport.send_command(f"km.move({x},{y},{segments})")
|
123
|
+
|
124
|
+
def move_bezier(self, x: int, y: int, segments: int, ctrl_x: int, ctrl_y: int) -> None:
|
125
|
+
self.transport.send_command(f"km.move({x},{y},{segments},{ctrl_x},{ctrl_y})")
|
126
|
+
|
127
|
+
def scroll(self, delta: int) -> None:
|
128
|
+
self.transport.send_command(f"km.wheel({delta})")
|
129
|
+
|
130
|
+
|
131
|
+
def _set_lock(self, name: str, lock: bool) -> None:
|
132
|
+
if lock:
|
133
|
+
cmd, bit = self._LOCK_COMMANDS[name]
|
134
|
+
else:
|
135
|
+
cmd, bit = self._UNLOCK_COMMANDS[name]
|
136
|
+
|
137
|
+
self.transport.send_command(cmd)
|
138
|
+
|
139
|
+
if lock:
|
140
|
+
self._lock_states_cache |= (1 << bit)
|
141
|
+
else:
|
142
|
+
self._lock_states_cache &= ~(1 << bit)
|
143
|
+
self._cache_valid = True
|
144
|
+
|
145
|
+
def lock_left(self, lock: bool) -> None:
|
146
|
+
self._set_lock("LEFT", lock)
|
147
|
+
|
148
|
+
def lock_middle(self, lock: bool) -> None:
|
149
|
+
self._set_lock("MIDDLE", lock)
|
150
|
+
|
151
|
+
def lock_right(self, lock: bool) -> None:
|
152
|
+
self._set_lock("RIGHT", lock)
|
153
|
+
|
154
|
+
def lock_side1(self, lock: bool) -> None:
|
155
|
+
self._set_lock("MOUSE4", lock)
|
156
|
+
|
157
|
+
def lock_side2(self, lock: bool) -> None:
|
158
|
+
self._set_lock("MOUSE5", lock)
|
159
|
+
|
160
|
+
def lock_x(self, lock: bool) -> None:
|
161
|
+
self._set_lock("X", lock)
|
162
|
+
|
163
|
+
def lock_y(self, lock: bool) -> None:
|
164
|
+
self._set_lock("Y", lock)
|
165
|
+
|
166
|
+
def spoof_serial(self, serial: str) -> None:
|
167
|
+
self.transport.send_command(f"km.serial('{serial}')")
|
168
|
+
|
169
|
+
def reset_serial(self) -> None:
|
170
|
+
self.transport.send_command("km.serial(0)")
|
171
|
+
|
172
|
+
def get_device_info(self) -> Dict[str, Union[str, bool]]:
|
173
|
+
port_name = self.transport.port
|
174
|
+
is_connected = self.transport.is_connected()
|
175
|
+
|
176
|
+
if not is_connected or not port_name:
|
177
|
+
return {
|
178
|
+
"port": port_name or "Unknown",
|
179
|
+
"description": "Disconnected",
|
180
|
+
"vid": "Unknown",
|
181
|
+
"pid": "Unknown",
|
182
|
+
"isConnected": False
|
183
|
+
}
|
184
|
+
|
185
|
+
info = {
|
186
|
+
"port": port_name,
|
187
|
+
"description": "Connected Device",
|
188
|
+
"vid": "Unknown",
|
189
|
+
"pid": "Unknown",
|
190
|
+
"isConnected": True
|
191
|
+
}
|
192
|
+
|
193
|
+
try:
|
194
|
+
for port in list_ports.comports():
|
195
|
+
if port.device == port_name:
|
196
|
+
info["description"] = port.description or "Connected Device"
|
197
|
+
if port.vid is not None:
|
198
|
+
info["vid"] = f"0x{port.vid:04x}"
|
199
|
+
if port.pid is not None:
|
200
|
+
info["pid"] = f"0x{port.pid:04x}"
|
201
|
+
break
|
202
|
+
except Exception:
|
203
|
+
pass
|
204
|
+
|
205
|
+
return info
|
206
|
+
|
207
|
+
def get_firmware_version(self) -> str:
|
208
|
+
response = self.transport.send_command("km.version()", expect_response=True, timeout=0.1)
|
209
|
+
return response or ""
|
210
|
+
|
211
|
+
def _invalidate_cache(self) -> None:
|
212
|
+
self._cache_valid = False
|
213
|
+
|
214
|
+
def get_all_lock_states(self) -> Dict[str, bool]:
|
215
|
+
|
216
|
+
if self._cache_valid:
|
217
|
+
return {
|
218
|
+
"X": bool(self._lock_states_cache & (1 << 5)),
|
219
|
+
"Y": bool(self._lock_states_cache & (1 << 6)),
|
220
|
+
"LEFT": bool(self._lock_states_cache & (1 << 0)),
|
221
|
+
"RIGHT": bool(self._lock_states_cache & (1 << 1)),
|
222
|
+
"MIDDLE": bool(self._lock_states_cache & (1 << 2)),
|
223
|
+
"MOUSE4": bool(self._lock_states_cache & (1 << 3)),
|
224
|
+
"MOUSE5": bool(self._lock_states_cache & (1 << 4)),
|
225
|
+
}
|
226
|
+
|
227
|
+
states = {}
|
228
|
+
targets = ["X", "Y", "LEFT", "RIGHT", "MIDDLE", "MOUSE4", "MOUSE5"]
|
229
|
+
|
230
|
+
for target in targets:
|
231
|
+
cmd, bit = self._LOCK_QUERY_COMMANDS[target]
|
232
|
+
try:
|
233
|
+
result = self.transport.send_command(cmd, expect_response=True, timeout=0.05)
|
234
|
+
if result and result.strip() in ['0', '1']:
|
235
|
+
is_locked = result.strip() == '1'
|
236
|
+
states[target] = is_locked
|
237
|
+
|
238
|
+
if is_locked:
|
239
|
+
self._lock_states_cache |= (1 << bit)
|
240
|
+
else:
|
241
|
+
self._lock_states_cache &= ~(1 << bit)
|
242
|
+
else:
|
243
|
+
states[target] = False
|
244
|
+
except Exception:
|
245
|
+
states[target] = False
|
246
|
+
|
247
|
+
self._cache_valid = True
|
248
|
+
return states
|
249
|
+
|
250
|
+
def is_locked(self, button: Union[MouseButton, AxisButton]) -> bool:
|
251
|
+
try:
|
252
|
+
target_name = button.name if hasattr(button, 'name') else str(button)
|
253
|
+
|
254
|
+
if self._cache_valid and target_name in self._LOCK_QUERY_COMMANDS:
|
255
|
+
_, bit = self._LOCK_QUERY_COMMANDS[target_name]
|
256
|
+
return bool(self._lock_states_cache & (1 << bit))
|
257
|
+
|
258
|
+
cmd, bit = self._LOCK_QUERY_COMMANDS[target_name]
|
259
|
+
result = self.transport.send_command(cmd, expect_response=True, timeout=0.05)
|
260
|
+
|
261
|
+
if not result:
|
262
|
+
return False
|
263
|
+
|
264
|
+
result = result.strip()
|
265
|
+
is_locked = result == '1'
|
266
|
+
|
267
|
+
if is_locked:
|
268
|
+
self._lock_states_cache |= (1 << bit)
|
269
|
+
else:
|
270
|
+
self._lock_states_cache &= ~(1 << bit)
|
271
|
+
|
272
|
+
return is_locked
|
273
|
+
|
274
|
+
except Exception:
|
275
|
+
return False
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: makcu
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.3.1
|
4
4
|
Summary: Python library for Makcu hardware device control
|
5
5
|
Author: SleepyTotem
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
@@ -708,7 +708,7 @@ Requires-Dist: twine>=4.0
|
|
708
708
|
Requires-Dist: rich>=14.0
|
709
709
|
Dynamic: license-file
|
710
710
|
|
711
|
-
# 🖱️ Makcu Python Library v2.
|
711
|
+
# 🖱️ Makcu Python Library v2.3.0
|
712
712
|
|
713
713
|
[](https://pypi.org/project/makcu/)
|
714
714
|
[](https://pypi.org/project/makcu/)
|
@@ -1,17 +1,17 @@
|
|
1
|
-
makcu/README.md,sha256=
|
2
|
-
makcu/__init__.py,sha256=
|
1
|
+
makcu/README.md,sha256=inbc1krsBQErNI1RNaIfjKXX4ZjaqGKWG3ETFMqVwfw,11193
|
2
|
+
makcu/__init__.py,sha256=wLK0OX4t587Gt7Z4H5pfFaFlgTm8HGzMtlKGT7w0a98,448
|
3
3
|
makcu/__main__.py,sha256=duRmMpsNqCKZQfQ-Wj57tIUkZ6hbxLV8MBxWnhyKarY,14527
|
4
4
|
makcu/conftest.py,sha256=EoX2T6dnUvWVI_VvJ0KTes6W82KR4Z69kMQVNtsxV7I,201
|
5
5
|
makcu/connection.py,sha256=xvgFUPz9JBVHhatlVFtj6zhnfTamT7_o8M3msxOjVMs,23767
|
6
|
-
makcu/controller.py,sha256=
|
6
|
+
makcu/controller.py,sha256=DRSS9qySJjdKKlUYXf90GemF_HXMTK3ewdqEBJRe06U,14069
|
7
7
|
makcu/enums.py,sha256=1SvIv5IoMIfXyoMAq8I5r_aKMVKjYR-L_hXkFLJ_5mw,119
|
8
8
|
makcu/errors.py,sha256=hXdeUCHvbzfqWX3uotG12Xv8EPwWs-5aHQ12xpgwx3Y,229
|
9
9
|
makcu/makcu.pyi,sha256=UfbX7774u_a9oyIa1rweWqIRoQxVAjhhHUuukDp3jiA,237
|
10
|
-
makcu/mouse.py,sha256=
|
10
|
+
makcu/mouse.py,sha256=dawpgWUs6V-CeMlsiPrELSbJmmgYl7QjlqK15Y0bXGI,9819
|
11
11
|
makcu/py.typed,sha256=ks2XNwsNldow4qVp9kuCuGBuHTmngmrzgvRnkzWIGMY,110
|
12
12
|
makcu/test_suite.py,sha256=mfzr7L63Eim1z4-l7WHtWs7szGr6bNk2U13vXVlWeM4,4295
|
13
|
-
makcu-2.
|
14
|
-
makcu-2.
|
15
|
-
makcu-2.
|
16
|
-
makcu-2.
|
17
|
-
makcu-2.
|
13
|
+
makcu-2.3.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
14
|
+
makcu-2.3.1.dist-info/METADATA,sha256=Y-ATRFlY_DLYQf3MKz5WpqI6dHhkWp2nhHFrn83u_RM,53904
|
15
|
+
makcu-2.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
16
|
+
makcu-2.3.1.dist-info/top_level.txt,sha256=IRO1UVb5LK_ovjau0g4oObyXQqy00tVEE-yF5lPgw1w,6
|
17
|
+
makcu-2.3.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|