makcu 2.2.0__tar.gz → 2.2.2__tar.gz
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-2.2.0 → makcu-2.2.2}/PKG-INFO +4 -3
- {makcu-2.2.0 → makcu-2.2.2}/makcu/README.md +406 -405
- {makcu-2.2.0 → makcu-2.2.2}/makcu/__init__.py +21 -21
- {makcu-2.2.0 → makcu-2.2.2}/makcu/__main__.py +27 -14
- makcu-2.2.2/makcu/conftest.py +8 -0
- {makcu-2.2.0 → makcu-2.2.2}/makcu/connection.py +91 -30
- {makcu-2.2.0 → makcu-2.2.2}/makcu/py.typed +1 -1
- {makcu-2.2.0 → makcu-2.2.2}/makcu/test_suite.py +20 -3
- {makcu-2.2.0 → makcu-2.2.2}/makcu.egg-info/PKG-INFO +4 -3
- {makcu-2.2.0 → makcu-2.2.2}/pyproject.toml +1 -1
- makcu-2.2.0/makcu/conftest.py +0 -34
- {makcu-2.2.0 → makcu-2.2.2}/LICENSE +0 -0
- {makcu-2.2.0 → makcu-2.2.2}/makcu/controller.py +0 -0
- {makcu-2.2.0 → makcu-2.2.2}/makcu/enums.py +0 -0
- {makcu-2.2.0 → makcu-2.2.2}/makcu/errors.py +0 -0
- {makcu-2.2.0 → makcu-2.2.2}/makcu/makcu.pyi +0 -0
- {makcu-2.2.0 → makcu-2.2.2}/makcu/mouse.py +0 -0
- {makcu-2.2.0 → makcu-2.2.2}/makcu.egg-info/SOURCES.txt +0 -0
- {makcu-2.2.0 → makcu-2.2.2}/makcu.egg-info/dependency_links.txt +0 -0
- {makcu-2.2.0 → makcu-2.2.2}/makcu.egg-info/requires.txt +0 -0
- {makcu-2.2.0 → makcu-2.2.2}/makcu.egg-info/top_level.txt +0 -0
- {makcu-2.2.0 → makcu-2.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: makcu
|
3
|
-
Version: 2.2.
|
3
|
+
Version: 2.2.2
|
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.2.
|
711
|
+
# 🖱️ Makcu Python Library v2.2.2
|
712
712
|
|
713
713
|
[](https://pypi.org/project/makcu/)
|
714
714
|
[](https://pypi.org/project/makcu/)
|
@@ -1112,4 +1112,5 @@ GPL License © SleepyTotem
|
|
1112
1112
|
|
1113
1113
|
- [GitHub Repository](https://github.com/SleepyTotem/makcu-py-lib)
|
1114
1114
|
- [PyPI Package](https://pypi.org/project/makcu/)
|
1115
|
-
- [Documentation](https://makcu.readthedocs.io/)
|
1115
|
+
- [Documentation](https://makcu-py-lib.readthedocs.io/)
|
1116
|
+
- [Changelog](https://makcu-py-lib.readthedocs.io/en/latest/changelog.html)
|
@@ -1,405 +1,406 @@
|
|
1
|
-
# 🖱️ Makcu Python Library v2.2.
|
2
|
-
|
3
|
-
[](https://pypi.org/project/makcu/)
|
4
|
-
[](https://pypi.org/project/makcu/)
|
5
|
-
[](LICENSE)
|
6
|
-
|
7
|
-
Makcu Py Lib is a high-performance Python library for controlling Makcu devices — now with **async/await support**, **zero-delay command execution**, and **automatic reconnection**!
|
8
|
-
|
9
|
-
---
|
10
|
-
|
11
|
-
## 📦 Installation
|
12
|
-
|
13
|
-
### Recommended: PyPI
|
14
|
-
|
15
|
-
```bash
|
16
|
-
pip install makcu
|
17
|
-
```
|
18
|
-
|
19
|
-
### From Source
|
20
|
-
|
21
|
-
```bash
|
22
|
-
git clone https://github.com/SleepyTotem/makcu-py-lib
|
23
|
-
cd makcu-py-lib
|
24
|
-
pip install .
|
25
|
-
```
|
26
|
-
|
27
|
-
---
|
28
|
-
|
29
|
-
## 🧠 Quick Start
|
30
|
-
|
31
|
-
### Synchronous API (Classic)
|
32
|
-
|
33
|
-
```python
|
34
|
-
from makcu import create_controller, MouseButton
|
35
|
-
|
36
|
-
# Create and connect
|
37
|
-
makcu = create_controller(debug=True, auto_reconnect=True)
|
38
|
-
|
39
|
-
# Basic operations
|
40
|
-
makcu.click(MouseButton.LEFT)
|
41
|
-
makcu.move(100, 50)
|
42
|
-
makcu.scroll(-1)
|
43
|
-
|
44
|
-
# Human-like interaction
|
45
|
-
makcu.click_human_like(MouseButton.LEFT, count=2, profile="gaming", jitter=3)
|
46
|
-
|
47
|
-
# Clean disconnect
|
48
|
-
makcu.disconnect()
|
49
|
-
```
|
50
|
-
|
51
|
-
### Asynchronous API (New!)
|
52
|
-
|
53
|
-
```python
|
54
|
-
import asyncio
|
55
|
-
from makcu import create_async_controller, MouseButton
|
56
|
-
|
57
|
-
async def main():
|
58
|
-
# Auto-connect with context manager
|
59
|
-
async with await create_async_controller(debug=True) as makcu:
|
60
|
-
# Parallel operations
|
61
|
-
await asyncio.gather(
|
62
|
-
makcu.move(100, 0),
|
63
|
-
makcu.click(MouseButton.LEFT),
|
64
|
-
makcu.scroll(-1)
|
65
|
-
)
|
66
|
-
|
67
|
-
# Human-like clicking
|
68
|
-
await makcu.click_human_like(MouseButton.RIGHT, count=3)
|
69
|
-
|
70
|
-
asyncio.run(main())
|
71
|
-
```
|
72
|
-
|
73
|
-
---
|
74
|
-
|
75
|
-
## 🎮 Core Features
|
76
|
-
|
77
|
-
### Mouse Control
|
78
|
-
|
79
|
-
```python
|
80
|
-
# Button actions
|
81
|
-
await makcu.click(MouseButton.LEFT)
|
82
|
-
await makcu.double_click(MouseButton.RIGHT)
|
83
|
-
await makcu.press(MouseButton.MIDDLE)
|
84
|
-
await makcu.release(MouseButton.MIDDLE)
|
85
|
-
|
86
|
-
# Movement
|
87
|
-
await makcu.move(100, 50) # Relative movement
|
88
|
-
await makcu.move_smooth(200, 100, segments=20) # Smooth interpolation
|
89
|
-
await makcu.move_bezier(150, 150, segments=30, ctrl_x=75, ctrl_y=200) # Bezier curve
|
90
|
-
|
91
|
-
# Scrolling
|
92
|
-
await makcu.scroll(-5) # Scroll down
|
93
|
-
await makcu.scroll(3) # Scroll up
|
94
|
-
|
95
|
-
# Dragging
|
96
|
-
await makcu.drag(0, 0, 300, 200, button=MouseButton.LEFT, duration=1.5)
|
97
|
-
```
|
98
|
-
|
99
|
-
### Button & Axis Locking
|
100
|
-
|
101
|
-
```python
|
102
|
-
# New unified locking API
|
103
|
-
await makcu.lock(MouseButton.LEFT) # Lock left button
|
104
|
-
await makcu.unlock(MouseButton.RIGHT) # Unlock right button
|
105
|
-
await makcu.lock("X") # Lock X-axis movement
|
106
|
-
await makcu.unlock("Y") # Unlock Y-axis movement
|
107
|
-
|
108
|
-
# Query lock states (no delays!)
|
109
|
-
is_locked = await makcu.is_locked(MouseButton.LEFT)
|
110
|
-
all_states = await makcu.get_all_lock_states()
|
111
|
-
# Returns: {"LEFT": True, "RIGHT": False, "X": True, ...}
|
112
|
-
```
|
113
|
-
|
114
|
-
### Human-like Interactions
|
115
|
-
|
116
|
-
```python
|
117
|
-
# Realistic clicking with timing variations
|
118
|
-
await makcu.click_human_like(
|
119
|
-
button=MouseButton.LEFT,
|
120
|
-
count=5,
|
121
|
-
profile="gaming", # "fast", "normal", "slow", "variable", "gaming"
|
122
|
-
jitter=5 # Random mouse movement between clicks
|
123
|
-
)
|
124
|
-
```
|
125
|
-
|
126
|
-
### Button Event Monitoring
|
127
|
-
|
128
|
-
```python
|
129
|
-
# Real-time button monitoring
|
130
|
-
def on_button_event(button: MouseButton, pressed: bool):
|
131
|
-
print(f"{button.name} {'pressed' if pressed else 'released'}")
|
132
|
-
|
133
|
-
makcu.set_button_callback(on_button_event)
|
134
|
-
await makcu.enable_button_monitoring(True)
|
135
|
-
|
136
|
-
# Check current button states
|
137
|
-
states = makcu.get_button_states()
|
138
|
-
if makcu.is_pressed(MouseButton.RIGHT):
|
139
|
-
print("Right button is pressed")
|
140
|
-
```
|
141
|
-
|
142
|
-
### Connection Management
|
143
|
-
|
144
|
-
```python
|
145
|
-
# Auto-reconnection on disconnect
|
146
|
-
makcu = await create_async_controller(auto_reconnect=True)
|
147
|
-
|
148
|
-
# Connection status callbacks
|
149
|
-
@makcu.on_connection_change
|
150
|
-
async def handle_connection(connected: bool):
|
151
|
-
if connected:
|
152
|
-
print("Device reconnected!")
|
153
|
-
else:
|
154
|
-
print("Device disconnected!")
|
155
|
-
|
156
|
-
# Manual reconnection
|
157
|
-
if not makcu.is_connected():
|
158
|
-
await makcu.connect()
|
159
|
-
```
|
160
|
-
|
161
|
-
---
|
162
|
-
|
163
|
-
## 🔧 Advanced Features
|
164
|
-
|
165
|
-
### Batch Operations
|
166
|
-
|
167
|
-
```python
|
168
|
-
# Execute multiple commands efficiently
|
169
|
-
async def combo_action():
|
170
|
-
await makcu.batch_execute([
|
171
|
-
lambda: makcu.move(50, 0),
|
172
|
-
lambda: makcu.click(MouseButton.LEFT),
|
173
|
-
lambda: makcu.move(-50, 0),
|
174
|
-
lambda: makcu.click(MouseButton.RIGHT)
|
175
|
-
])
|
176
|
-
```
|
177
|
-
|
178
|
-
### Device Information
|
179
|
-
|
180
|
-
```python
|
181
|
-
# Get device details
|
182
|
-
info = await makcu.get_device_info()
|
183
|
-
# {'port': 'COM3', 'vid': '0x1a86', 'pid': '0x55d3', ...}
|
184
|
-
|
185
|
-
# Firmware version
|
186
|
-
version = await makcu.get_firmware_version()
|
187
|
-
```
|
188
|
-
|
189
|
-
### Serial Spoofing
|
190
|
-
|
191
|
-
```python
|
192
|
-
# Spoof device serial
|
193
|
-
await makcu.spoof_serial("CUSTOM123456")
|
194
|
-
|
195
|
-
# Reset to default
|
196
|
-
await makcu.reset_serial()
|
197
|
-
```
|
198
|
-
|
199
|
-
### Low-Level Access
|
200
|
-
|
201
|
-
```python
|
202
|
-
# Send raw commands with tracked responses
|
203
|
-
response = await makcu.transport.async_send_command(
|
204
|
-
"km.version()",
|
205
|
-
expect_response=True,
|
206
|
-
timeout=0.1 # Optimized for gaming
|
207
|
-
)
|
208
|
-
```
|
209
|
-
|
210
|
-
---
|
211
|
-
|
212
|
-
## 🧪 Command-Line Tools
|
213
|
-
|
214
|
-
```bash
|
215
|
-
# Interactive debug console
|
216
|
-
python -m makcu --debug
|
217
|
-
|
218
|
-
# Test specific port
|
219
|
-
python -m makcu --testPort COM3
|
220
|
-
|
221
|
-
# Run automated tests
|
222
|
-
python -m makcu --runtest
|
223
|
-
```
|
224
|
-
|
225
|
-
### Tool Descriptions
|
226
|
-
|
227
|
-
- `--debug`: Launches an interactive console where you can type raw device commands and see live responses.
|
228
|
-
- `--testPort COMx`: Attempts to connect to the given COM port and reports success or failure.
|
229
|
-
- `--runtest`: Runs `test_suite.py` using `pytest` and opens a detailed HTML test report.
|
230
|
-
|
231
|
-
---
|
232
|
-
|
233
|
-
### Test Suite
|
234
|
-
|
235
|
-
- File: `test_suite.py`
|
236
|
-
- Run with: `python -m makcu --runtest`
|
237
|
-
- Output: `latest_pytest.html`
|
238
|
-
|
239
|
-
Includes tests for:
|
240
|
-
- Port connection
|
241
|
-
- Firmware version check
|
242
|
-
- Mouse movement and button control
|
243
|
-
- Button masking and locking
|
244
|
-
|
245
|
-
---
|
246
|
-
|
247
|
-
## Test Timings (v1.3 vs v1.4 vs v2.0)
|
248
|
-
|
249
|
-
| Test Name | v1.3 | v1.4 | v2.0 | Improvement (v1.3 → v2.0) |
|
250
|
-
|--------------------------|--------|-------|-------|----------------------------|
|
251
|
-
| connect_to_port | ~100ms | ~55ms | **46ms** | ~2.2x faster |
|
252
|
-
| press_and_release | ~18ms | ~9ms | **1ms** | ~18x faster |
|
253
|
-
| firmware_version | ~20ms | ~9ms | **1ms** | ~20x faster |
|
254
|
-
| middle_click | ~18ms | ~9ms | **1ms** | ~18x faster |
|
255
|
-
| device_info | ~25ms | ~13ms | **6ms** | ~4.1x faster |
|
256
|
-
| port_connection | ~20ms | ~9ms | **1ms** | ~20x faster |
|
257
|
-
| button_mask | ~17ms | ~8ms | **1ms** | ~17x faster |
|
258
|
-
| get_button_states | ~18ms | ~9ms | **1ms** | ~18x faster |
|
259
|
-
| lock_state | ~33ms | ~10ms | **1ms** | ~33x faster |
|
260
|
-
| makcu_behavior | ~20ms | ~10ms | **1ms** | ~20x faster |
|
261
|
-
| batch_commands | ~350ms | ~90ms | **3ms** | ~117x faster |
|
262
|
-
| rapid_moves | ~17ms | ~8ms | **2ms** | ~8.5x faster |
|
263
|
-
| button_performance | ~18ms | ~9ms | **2ms** | ~9x faster |
|
264
|
-
| mixed_operations | ~22ms | ~10ms | **2ms** | ~11x faster |
|
265
|
-
|
266
|
-
Based on the measured test suite, v2.0 is on average **~17× faster** than v1.3 across all core operations.
|
267
|
-
|
268
|
-
|
269
|
-
### Gaming Performance Targets (v2.0)
|
270
|
-
|
271
|
-
- **144Hz Gaming**: 7ms frame time — ✅ Easily met (avg 1–3ms per operation)
|
272
|
-
- **240Hz Gaming**: 4.2ms frame time — ✅ Consistently met (most ops ≤ 2ms)
|
273
|
-
- **360Hz Gaming**: 2.8ms frame time — ⚡ Achievable for atomic/single ops
|
274
|
-
|
275
|
-
---
|
276
|
-
|
277
|
-
## 🏎️ Performance Optimization Details
|
278
|
-
|
279
|
-
### Version History & Performance
|
280
|
-
|
281
|
-
- **v1.3 and earlier**: Original implementation with sleep delays
|
282
|
-
- **v1.4**: Initial optimizations, removed some sleep delays
|
283
|
-
- **v2.0**: Complete rewrite with zero-delay architecture
|
284
|
-
|
285
|
-
### Key Optimizations in v2.0
|
286
|
-
|
287
|
-
1. **Pre-computed Commands**: All commands are pre-formatted at initialization
|
288
|
-
2. **Bitwise Operations**: Button states use single integer with bit manipulation
|
289
|
-
3. **Zero-Copy Buffers**: Pre-allocated buffers for parsing
|
290
|
-
4. **Reduced Timeouts**: Gaming-optimized timeouts (100ms default)
|
291
|
-
5. **Cache Everything**: Connection states, lock states, and device info cached
|
292
|
-
6. **Minimal Allocations**: Reuse objects and avoid string formatting
|
293
|
-
7. **Fast Serial Settings**: 1ms read timeout, 10ms write timeout
|
294
|
-
8. **Optimized Listener**: Batch processing with minimal overhead
|
295
|
-
|
296
|
-
### Tips for Maximum Performance
|
297
|
-
|
298
|
-
```python
|
299
|
-
# Disable debug mode in production
|
300
|
-
makcu = create_controller(debug=False)
|
301
|
-
|
302
|
-
# Use cached connection checks
|
303
|
-
if makcu.is_connected(): # Cached, no serial check
|
304
|
-
makcu.click(MouseButton.LEFT)
|
305
|
-
|
306
|
-
# Batch similar operations
|
307
|
-
with makcu: # Context manager ensures connection
|
308
|
-
for _ in range(10):
|
309
|
-
makcu.move(10, 0) # No connection check per call
|
310
|
-
```
|
311
|
-
|
312
|
-
---
|
313
|
-
|
314
|
-
## 🔍 Debugging
|
315
|
-
|
316
|
-
Enable debug mode for detailed logging:
|
317
|
-
|
318
|
-
```python
|
319
|
-
makcu = await create_async_controller(debug=True)
|
320
|
-
|
321
|
-
# View command flow (optimized timestamps)
|
322
|
-
# [123.456] [INFO] Sent command #42: km.move(100,50)
|
323
|
-
# [123.458] [DEBUG] Command #42 completed in 0.002s
|
324
|
-
```
|
325
|
-
|
326
|
-
---
|
327
|
-
|
328
|
-
## 🏗️ Migration from v1.x
|
329
|
-
|
330
|
-
Most code works without changes! Key differences:
|
331
|
-
|
332
|
-
```python
|
333
|
-
# v1.x (still works)
|
334
|
-
makcu = create_controller()
|
335
|
-
makcu.move(100, 100)
|
336
|
-
|
337
|
-
# v2.0 (async)
|
338
|
-
makcu = await create_async_controller()
|
339
|
-
await makcu.move(100, 100)
|
340
|
-
|
341
|
-
# v2.0 context manager (auto cleanup)
|
342
|
-
async with await create_async_controller() as makcu:
|
343
|
-
await makcu.click(MouseButton.LEFT)
|
344
|
-
```
|
345
|
-
|
346
|
-
---
|
347
|
-
|
348
|
-
## 📚 API Reference
|
349
|
-
|
350
|
-
### Enumerations
|
351
|
-
|
352
|
-
```python
|
353
|
-
from makcu import MouseButton
|
354
|
-
|
355
|
-
MouseButton.LEFT # Left mouse button
|
356
|
-
MouseButton.RIGHT # Right mouse button
|
357
|
-
MouseButton.MIDDLE # Middle mouse button
|
358
|
-
MouseButton.MOUSE4 # Side button 1
|
359
|
-
MouseButton.MOUSE5 # Side button 2
|
360
|
-
```
|
361
|
-
|
362
|
-
### Exception Handling
|
363
|
-
|
364
|
-
```python
|
365
|
-
from makcu import MakcuError, MakcuConnectionError, MakcuTimeoutError
|
366
|
-
|
367
|
-
try:
|
368
|
-
makcu = await create_async_controller()
|
369
|
-
except MakcuConnectionError as e:
|
370
|
-
print(f"Connection failed: {e}")
|
371
|
-
except MakcuTimeoutError as e:
|
372
|
-
print(f"Command timed out: {e}")
|
373
|
-
```
|
374
|
-
|
375
|
-
---
|
376
|
-
|
377
|
-
## 🛠️ Technical Details
|
378
|
-
|
379
|
-
- **Protocol**: CH343 USB serial at 4Mbps
|
380
|
-
- **Command Format**: ASCII with optional ID tracking (`command#ID`)
|
381
|
-
- **Response Format**: `>>> #ID:response` for tracked commands
|
382
|
-
- **Threading**: High-priority listener thread with async bridge
|
383
|
-
- **Auto-Discovery**: VID:PID=1A86:55D3 detection
|
384
|
-
- **Buffer Size**: 4KB read buffer, 256B line buffer
|
385
|
-
- **Cleanup Interval**: 50ms for timed-out commands
|
386
|
-
|
387
|
-
---
|
388
|
-
|
389
|
-
## 📜 License
|
390
|
-
|
391
|
-
GPL License © SleepyTotem
|
392
|
-
|
393
|
-
---
|
394
|
-
|
395
|
-
## 🙋 Support
|
396
|
-
|
397
|
-
- **Issues**: [GitHub Issues](https://github.com/SleepyTotem/makcu-py-lib/issues)
|
398
|
-
|
399
|
-
---
|
400
|
-
|
401
|
-
## 🌐 Links
|
402
|
-
|
403
|
-
- [GitHub Repository](https://github.com/SleepyTotem/makcu-py-lib)
|
404
|
-
- [PyPI Package](https://pypi.org/project/makcu/)
|
405
|
-
- [Documentation](https://makcu.readthedocs.io/)
|
1
|
+
# 🖱️ Makcu Python Library v2.2.2
|
2
|
+
|
3
|
+
[](https://pypi.org/project/makcu/)
|
4
|
+
[](https://pypi.org/project/makcu/)
|
5
|
+
[](LICENSE)
|
6
|
+
|
7
|
+
Makcu Py Lib is a high-performance Python library for controlling Makcu devices — now with **async/await support**, **zero-delay command execution**, and **automatic reconnection**!
|
8
|
+
|
9
|
+
---
|
10
|
+
|
11
|
+
## 📦 Installation
|
12
|
+
|
13
|
+
### Recommended: PyPI
|
14
|
+
|
15
|
+
```bash
|
16
|
+
pip install makcu
|
17
|
+
```
|
18
|
+
|
19
|
+
### From Source
|
20
|
+
|
21
|
+
```bash
|
22
|
+
git clone https://github.com/SleepyTotem/makcu-py-lib
|
23
|
+
cd makcu-py-lib
|
24
|
+
pip install .
|
25
|
+
```
|
26
|
+
|
27
|
+
---
|
28
|
+
|
29
|
+
## 🧠 Quick Start
|
30
|
+
|
31
|
+
### Synchronous API (Classic)
|
32
|
+
|
33
|
+
```python
|
34
|
+
from makcu import create_controller, MouseButton
|
35
|
+
|
36
|
+
# Create and connect
|
37
|
+
makcu = create_controller(debug=True, auto_reconnect=True)
|
38
|
+
|
39
|
+
# Basic operations
|
40
|
+
makcu.click(MouseButton.LEFT)
|
41
|
+
makcu.move(100, 50)
|
42
|
+
makcu.scroll(-1)
|
43
|
+
|
44
|
+
# Human-like interaction
|
45
|
+
makcu.click_human_like(MouseButton.LEFT, count=2, profile="gaming", jitter=3)
|
46
|
+
|
47
|
+
# Clean disconnect
|
48
|
+
makcu.disconnect()
|
49
|
+
```
|
50
|
+
|
51
|
+
### Asynchronous API (New!)
|
52
|
+
|
53
|
+
```python
|
54
|
+
import asyncio
|
55
|
+
from makcu import create_async_controller, MouseButton
|
56
|
+
|
57
|
+
async def main():
|
58
|
+
# Auto-connect with context manager
|
59
|
+
async with await create_async_controller(debug=True) as makcu:
|
60
|
+
# Parallel operations
|
61
|
+
await asyncio.gather(
|
62
|
+
makcu.move(100, 0),
|
63
|
+
makcu.click(MouseButton.LEFT),
|
64
|
+
makcu.scroll(-1)
|
65
|
+
)
|
66
|
+
|
67
|
+
# Human-like clicking
|
68
|
+
await makcu.click_human_like(MouseButton.RIGHT, count=3)
|
69
|
+
|
70
|
+
asyncio.run(main())
|
71
|
+
```
|
72
|
+
|
73
|
+
---
|
74
|
+
|
75
|
+
## 🎮 Core Features
|
76
|
+
|
77
|
+
### Mouse Control
|
78
|
+
|
79
|
+
```python
|
80
|
+
# Button actions
|
81
|
+
await makcu.click(MouseButton.LEFT)
|
82
|
+
await makcu.double_click(MouseButton.RIGHT)
|
83
|
+
await makcu.press(MouseButton.MIDDLE)
|
84
|
+
await makcu.release(MouseButton.MIDDLE)
|
85
|
+
|
86
|
+
# Movement
|
87
|
+
await makcu.move(100, 50) # Relative movement
|
88
|
+
await makcu.move_smooth(200, 100, segments=20) # Smooth interpolation
|
89
|
+
await makcu.move_bezier(150, 150, segments=30, ctrl_x=75, ctrl_y=200) # Bezier curve
|
90
|
+
|
91
|
+
# Scrolling
|
92
|
+
await makcu.scroll(-5) # Scroll down
|
93
|
+
await makcu.scroll(3) # Scroll up
|
94
|
+
|
95
|
+
# Dragging
|
96
|
+
await makcu.drag(0, 0, 300, 200, button=MouseButton.LEFT, duration=1.5)
|
97
|
+
```
|
98
|
+
|
99
|
+
### Button & Axis Locking
|
100
|
+
|
101
|
+
```python
|
102
|
+
# New unified locking API
|
103
|
+
await makcu.lock(MouseButton.LEFT) # Lock left button
|
104
|
+
await makcu.unlock(MouseButton.RIGHT) # Unlock right button
|
105
|
+
await makcu.lock("X") # Lock X-axis movement
|
106
|
+
await makcu.unlock("Y") # Unlock Y-axis movement
|
107
|
+
|
108
|
+
# Query lock states (no delays!)
|
109
|
+
is_locked = await makcu.is_locked(MouseButton.LEFT)
|
110
|
+
all_states = await makcu.get_all_lock_states()
|
111
|
+
# Returns: {"LEFT": True, "RIGHT": False, "X": True, ...}
|
112
|
+
```
|
113
|
+
|
114
|
+
### Human-like Interactions
|
115
|
+
|
116
|
+
```python
|
117
|
+
# Realistic clicking with timing variations
|
118
|
+
await makcu.click_human_like(
|
119
|
+
button=MouseButton.LEFT,
|
120
|
+
count=5,
|
121
|
+
profile="gaming", # "fast", "normal", "slow", "variable", "gaming"
|
122
|
+
jitter=5 # Random mouse movement between clicks
|
123
|
+
)
|
124
|
+
```
|
125
|
+
|
126
|
+
### Button Event Monitoring
|
127
|
+
|
128
|
+
```python
|
129
|
+
# Real-time button monitoring
|
130
|
+
def on_button_event(button: MouseButton, pressed: bool):
|
131
|
+
print(f"{button.name} {'pressed' if pressed else 'released'}")
|
132
|
+
|
133
|
+
makcu.set_button_callback(on_button_event)
|
134
|
+
await makcu.enable_button_monitoring(True)
|
135
|
+
|
136
|
+
# Check current button states
|
137
|
+
states = makcu.get_button_states()
|
138
|
+
if makcu.is_pressed(MouseButton.RIGHT):
|
139
|
+
print("Right button is pressed")
|
140
|
+
```
|
141
|
+
|
142
|
+
### Connection Management
|
143
|
+
|
144
|
+
```python
|
145
|
+
# Auto-reconnection on disconnect
|
146
|
+
makcu = await create_async_controller(auto_reconnect=True)
|
147
|
+
|
148
|
+
# Connection status callbacks
|
149
|
+
@makcu.on_connection_change
|
150
|
+
async def handle_connection(connected: bool):
|
151
|
+
if connected:
|
152
|
+
print("Device reconnected!")
|
153
|
+
else:
|
154
|
+
print("Device disconnected!")
|
155
|
+
|
156
|
+
# Manual reconnection
|
157
|
+
if not makcu.is_connected():
|
158
|
+
await makcu.connect()
|
159
|
+
```
|
160
|
+
|
161
|
+
---
|
162
|
+
|
163
|
+
## 🔧 Advanced Features
|
164
|
+
|
165
|
+
### Batch Operations
|
166
|
+
|
167
|
+
```python
|
168
|
+
# Execute multiple commands efficiently
|
169
|
+
async def combo_action():
|
170
|
+
await makcu.batch_execute([
|
171
|
+
lambda: makcu.move(50, 0),
|
172
|
+
lambda: makcu.click(MouseButton.LEFT),
|
173
|
+
lambda: makcu.move(-50, 0),
|
174
|
+
lambda: makcu.click(MouseButton.RIGHT)
|
175
|
+
])
|
176
|
+
```
|
177
|
+
|
178
|
+
### Device Information
|
179
|
+
|
180
|
+
```python
|
181
|
+
# Get device details
|
182
|
+
info = await makcu.get_device_info()
|
183
|
+
# {'port': 'COM3', 'vid': '0x1a86', 'pid': '0x55d3', ...}
|
184
|
+
|
185
|
+
# Firmware version
|
186
|
+
version = await makcu.get_firmware_version()
|
187
|
+
```
|
188
|
+
|
189
|
+
### Serial Spoofing
|
190
|
+
|
191
|
+
```python
|
192
|
+
# Spoof device serial
|
193
|
+
await makcu.spoof_serial("CUSTOM123456")
|
194
|
+
|
195
|
+
# Reset to default
|
196
|
+
await makcu.reset_serial()
|
197
|
+
```
|
198
|
+
|
199
|
+
### Low-Level Access
|
200
|
+
|
201
|
+
```python
|
202
|
+
# Send raw commands with tracked responses
|
203
|
+
response = await makcu.transport.async_send_command(
|
204
|
+
"km.version()",
|
205
|
+
expect_response=True,
|
206
|
+
timeout=0.1 # Optimized for gaming
|
207
|
+
)
|
208
|
+
```
|
209
|
+
|
210
|
+
---
|
211
|
+
|
212
|
+
## 🧪 Command-Line Tools
|
213
|
+
|
214
|
+
```bash
|
215
|
+
# Interactive debug console
|
216
|
+
python -m makcu --debug
|
217
|
+
|
218
|
+
# Test specific port
|
219
|
+
python -m makcu --testPort COM3
|
220
|
+
|
221
|
+
# Run automated tests
|
222
|
+
python -m makcu --runtest
|
223
|
+
```
|
224
|
+
|
225
|
+
### Tool Descriptions
|
226
|
+
|
227
|
+
- `--debug`: Launches an interactive console where you can type raw device commands and see live responses.
|
228
|
+
- `--testPort COMx`: Attempts to connect to the given COM port and reports success or failure.
|
229
|
+
- `--runtest`: Runs `test_suite.py` using `pytest` and opens a detailed HTML test report.
|
230
|
+
|
231
|
+
---
|
232
|
+
|
233
|
+
### Test Suite
|
234
|
+
|
235
|
+
- File: `test_suite.py`
|
236
|
+
- Run with: `python -m makcu --runtest`
|
237
|
+
- Output: `latest_pytest.html`
|
238
|
+
|
239
|
+
Includes tests for:
|
240
|
+
- Port connection
|
241
|
+
- Firmware version check
|
242
|
+
- Mouse movement and button control
|
243
|
+
- Button masking and locking
|
244
|
+
|
245
|
+
---
|
246
|
+
|
247
|
+
## Test Timings (v1.3 vs v1.4 vs v2.0)
|
248
|
+
|
249
|
+
| Test Name | v1.3 | v1.4 | v2.0 | Improvement (v1.3 → v2.0) |
|
250
|
+
|--------------------------|--------|-------|-------|----------------------------|
|
251
|
+
| connect_to_port | ~100ms | ~55ms | **46ms** | ~2.2x faster |
|
252
|
+
| press_and_release | ~18ms | ~9ms | **1ms** | ~18x faster |
|
253
|
+
| firmware_version | ~20ms | ~9ms | **1ms** | ~20x faster |
|
254
|
+
| middle_click | ~18ms | ~9ms | **1ms** | ~18x faster |
|
255
|
+
| device_info | ~25ms | ~13ms | **6ms** | ~4.1x faster |
|
256
|
+
| port_connection | ~20ms | ~9ms | **1ms** | ~20x faster |
|
257
|
+
| button_mask | ~17ms | ~8ms | **1ms** | ~17x faster |
|
258
|
+
| get_button_states | ~18ms | ~9ms | **1ms** | ~18x faster |
|
259
|
+
| lock_state | ~33ms | ~10ms | **1ms** | ~33x faster |
|
260
|
+
| makcu_behavior | ~20ms | ~10ms | **1ms** | ~20x faster |
|
261
|
+
| batch_commands | ~350ms | ~90ms | **3ms** | ~117x faster |
|
262
|
+
| rapid_moves | ~17ms | ~8ms | **2ms** | ~8.5x faster |
|
263
|
+
| button_performance | ~18ms | ~9ms | **2ms** | ~9x faster |
|
264
|
+
| mixed_operations | ~22ms | ~10ms | **2ms** | ~11x faster |
|
265
|
+
|
266
|
+
Based on the measured test suite, v2.0 is on average **~17× faster** than v1.3 across all core operations.
|
267
|
+
|
268
|
+
|
269
|
+
### Gaming Performance Targets (v2.0)
|
270
|
+
|
271
|
+
- **144Hz Gaming**: 7ms frame time — ✅ Easily met (avg 1–3ms per operation)
|
272
|
+
- **240Hz Gaming**: 4.2ms frame time — ✅ Consistently met (most ops ≤ 2ms)
|
273
|
+
- **360Hz Gaming**: 2.8ms frame time — ⚡ Achievable for atomic/single ops
|
274
|
+
|
275
|
+
---
|
276
|
+
|
277
|
+
## 🏎️ Performance Optimization Details
|
278
|
+
|
279
|
+
### Version History & Performance
|
280
|
+
|
281
|
+
- **v1.3 and earlier**: Original implementation with sleep delays
|
282
|
+
- **v1.4**: Initial optimizations, removed some sleep delays
|
283
|
+
- **v2.0**: Complete rewrite with zero-delay architecture
|
284
|
+
|
285
|
+
### Key Optimizations in v2.0
|
286
|
+
|
287
|
+
1. **Pre-computed Commands**: All commands are pre-formatted at initialization
|
288
|
+
2. **Bitwise Operations**: Button states use single integer with bit manipulation
|
289
|
+
3. **Zero-Copy Buffers**: Pre-allocated buffers for parsing
|
290
|
+
4. **Reduced Timeouts**: Gaming-optimized timeouts (100ms default)
|
291
|
+
5. **Cache Everything**: Connection states, lock states, and device info cached
|
292
|
+
6. **Minimal Allocations**: Reuse objects and avoid string formatting
|
293
|
+
7. **Fast Serial Settings**: 1ms read timeout, 10ms write timeout
|
294
|
+
8. **Optimized Listener**: Batch processing with minimal overhead
|
295
|
+
|
296
|
+
### Tips for Maximum Performance
|
297
|
+
|
298
|
+
```python
|
299
|
+
# Disable debug mode in production
|
300
|
+
makcu = create_controller(debug=False)
|
301
|
+
|
302
|
+
# Use cached connection checks
|
303
|
+
if makcu.is_connected(): # Cached, no serial check
|
304
|
+
makcu.click(MouseButton.LEFT)
|
305
|
+
|
306
|
+
# Batch similar operations
|
307
|
+
with makcu: # Context manager ensures connection
|
308
|
+
for _ in range(10):
|
309
|
+
makcu.move(10, 0) # No connection check per call
|
310
|
+
```
|
311
|
+
|
312
|
+
---
|
313
|
+
|
314
|
+
## 🔍 Debugging
|
315
|
+
|
316
|
+
Enable debug mode for detailed logging:
|
317
|
+
|
318
|
+
```python
|
319
|
+
makcu = await create_async_controller(debug=True)
|
320
|
+
|
321
|
+
# View command flow (optimized timestamps)
|
322
|
+
# [123.456] [INFO] Sent command #42: km.move(100,50)
|
323
|
+
# [123.458] [DEBUG] Command #42 completed in 0.002s
|
324
|
+
```
|
325
|
+
|
326
|
+
---
|
327
|
+
|
328
|
+
## 🏗️ Migration from v1.x
|
329
|
+
|
330
|
+
Most code works without changes! Key differences:
|
331
|
+
|
332
|
+
```python
|
333
|
+
# v1.x (still works)
|
334
|
+
makcu = create_controller()
|
335
|
+
makcu.move(100, 100)
|
336
|
+
|
337
|
+
# v2.0 (async)
|
338
|
+
makcu = await create_async_controller()
|
339
|
+
await makcu.move(100, 100)
|
340
|
+
|
341
|
+
# v2.0 context manager (auto cleanup)
|
342
|
+
async with await create_async_controller() as makcu:
|
343
|
+
await makcu.click(MouseButton.LEFT)
|
344
|
+
```
|
345
|
+
|
346
|
+
---
|
347
|
+
|
348
|
+
## 📚 API Reference
|
349
|
+
|
350
|
+
### Enumerations
|
351
|
+
|
352
|
+
```python
|
353
|
+
from makcu import MouseButton
|
354
|
+
|
355
|
+
MouseButton.LEFT # Left mouse button
|
356
|
+
MouseButton.RIGHT # Right mouse button
|
357
|
+
MouseButton.MIDDLE # Middle mouse button
|
358
|
+
MouseButton.MOUSE4 # Side button 1
|
359
|
+
MouseButton.MOUSE5 # Side button 2
|
360
|
+
```
|
361
|
+
|
362
|
+
### Exception Handling
|
363
|
+
|
364
|
+
```python
|
365
|
+
from makcu import MakcuError, MakcuConnectionError, MakcuTimeoutError
|
366
|
+
|
367
|
+
try:
|
368
|
+
makcu = await create_async_controller()
|
369
|
+
except MakcuConnectionError as e:
|
370
|
+
print(f"Connection failed: {e}")
|
371
|
+
except MakcuTimeoutError as e:
|
372
|
+
print(f"Command timed out: {e}")
|
373
|
+
```
|
374
|
+
|
375
|
+
---
|
376
|
+
|
377
|
+
## 🛠️ Technical Details
|
378
|
+
|
379
|
+
- **Protocol**: CH343 USB serial at 4Mbps
|
380
|
+
- **Command Format**: ASCII with optional ID tracking (`command#ID`)
|
381
|
+
- **Response Format**: `>>> #ID:response` for tracked commands
|
382
|
+
- **Threading**: High-priority listener thread with async bridge
|
383
|
+
- **Auto-Discovery**: VID:PID=1A86:55D3 detection
|
384
|
+
- **Buffer Size**: 4KB read buffer, 256B line buffer
|
385
|
+
- **Cleanup Interval**: 50ms for timed-out commands
|
386
|
+
|
387
|
+
---
|
388
|
+
|
389
|
+
## 📜 License
|
390
|
+
|
391
|
+
GPL License © SleepyTotem
|
392
|
+
|
393
|
+
---
|
394
|
+
|
395
|
+
## 🙋 Support
|
396
|
+
|
397
|
+
- **Issues**: [GitHub Issues](https://github.com/SleepyTotem/makcu-py-lib/issues)
|
398
|
+
|
399
|
+
---
|
400
|
+
|
401
|
+
## 🌐 Links
|
402
|
+
|
403
|
+
- [GitHub Repository](https://github.com/SleepyTotem/makcu-py-lib)
|
404
|
+
- [PyPI Package](https://pypi.org/project/makcu/)
|
405
|
+
- [Documentation](https://makcu-py-lib.readthedocs.io/)
|
406
|
+
- [Changelog](https://makcu-py-lib.readthedocs.io/en/latest/changelog.html)
|
@@ -1,22 +1,22 @@
|
|
1
|
-
from .controller import (
|
2
|
-
MakcuController,
|
3
|
-
create_controller,
|
4
|
-
create_async_controller,
|
5
|
-
maybe_async
|
6
|
-
)
|
7
|
-
from .enums import MouseButton
|
8
|
-
from .errors import MakcuConnectionError
|
9
|
-
|
10
|
-
# Version info
|
11
|
-
__version__ = "2.2.
|
12
|
-
__author__ = "SleepyTotem"
|
13
|
-
|
14
|
-
# Main exports
|
15
|
-
__all__ = [
|
16
|
-
'MakcuController',
|
17
|
-
'MouseButton',
|
18
|
-
'MakcuConnectionError',
|
19
|
-
'create_controller',
|
20
|
-
'create_async_controller',
|
21
|
-
'maybe_async'
|
1
|
+
from .controller import (
|
2
|
+
MakcuController,
|
3
|
+
create_controller,
|
4
|
+
create_async_controller,
|
5
|
+
maybe_async
|
6
|
+
)
|
7
|
+
from .enums import MouseButton
|
8
|
+
from .errors import MakcuConnectionError
|
9
|
+
|
10
|
+
# Version info
|
11
|
+
__version__ = "2.2.1"
|
12
|
+
__author__ = "SleepyTotem"
|
13
|
+
|
14
|
+
# Main exports
|
15
|
+
__all__ = [
|
16
|
+
'MakcuController',
|
17
|
+
'MouseButton',
|
18
|
+
'MakcuConnectionError',
|
19
|
+
'create_controller',
|
20
|
+
'create_async_controller',
|
21
|
+
'maybe_async'
|
22
22
|
]
|
@@ -91,42 +91,48 @@ def find_writable_directory() -> Path:
|
|
91
91
|
def parse_html_results(html_file: Path) -> Tuple[List[Tuple[str, str, int]], int]:
|
92
92
|
if not html_file.exists():
|
93
93
|
raise FileNotFoundError(f"HTML report not found: {html_file}")
|
94
|
-
|
94
|
+
|
95
95
|
with open(html_file, 'r', encoding='utf-8') as f:
|
96
96
|
content = f.read()
|
97
|
-
|
97
|
+
|
98
98
|
match = re.search(r'data-jsonblob="([^"]*)"', content)
|
99
99
|
if not match:
|
100
100
|
raise ValueError("Could not find JSON data in HTML report")
|
101
|
-
|
101
|
+
|
102
102
|
json_str = match.group(1)
|
103
103
|
json_str = json_str.replace('"', '"').replace(''', "'").replace('&', '&')
|
104
|
-
|
104
|
+
|
105
105
|
try:
|
106
106
|
data = json.loads(json_str)
|
107
107
|
except json.JSONDecodeError as e:
|
108
108
|
raise ValueError(f"Failed to parse JSON data: {e}")
|
109
|
-
|
109
|
+
|
110
110
|
test_results = []
|
111
111
|
total_ms = 0
|
112
|
-
|
112
|
+
|
113
113
|
skip_tests = {'test_connect_to_port'}
|
114
|
-
|
114
|
+
|
115
115
|
for test_id, test_data_list in data.get('tests', {}).items():
|
116
116
|
test_name = test_id.split('::')[-1]
|
117
|
+
|
118
|
+
# Skip tests that are in skip_tests
|
117
119
|
if test_name in skip_tests:
|
118
120
|
continue
|
119
|
-
|
121
|
+
|
120
122
|
for test_data in test_data_list:
|
121
123
|
status = test_data.get('result', 'UNKNOWN')
|
122
124
|
duration_str = test_data.get('duration', '0 ms')
|
123
|
-
|
125
|
+
|
124
126
|
duration_match = re.search(r'(\d+)\s*ms', duration_str)
|
125
127
|
duration_ms = int(duration_match.group(1)) if duration_match else 0
|
126
|
-
total_ms += duration_ms
|
127
128
|
|
129
|
+
# Always add test to results
|
128
130
|
test_results.append((test_name, status, duration_ms))
|
129
|
-
|
131
|
+
|
132
|
+
# Only add time to total if it's not a cleanup test
|
133
|
+
if 'cleanup' not in test_name.lower():
|
134
|
+
total_ms += duration_ms
|
135
|
+
|
130
136
|
return test_results, total_ms
|
131
137
|
|
132
138
|
def run_tests() -> NoReturn:
|
@@ -244,8 +250,12 @@ def run_tests() -> NoReturn:
|
|
244
250
|
display_name = test_name.replace("test_", "").replace("_", " ").title()
|
245
251
|
|
246
252
|
if status.upper() == "PASSED":
|
247
|
-
|
248
|
-
|
253
|
+
if display_name.lower().startswith("cleanup"):
|
254
|
+
status_text = ""
|
255
|
+
passed += 1
|
256
|
+
else:
|
257
|
+
status_text = "[green]✅ PASSED[/green]"
|
258
|
+
passed += 1
|
249
259
|
elif status.upper() == "FAILED":
|
250
260
|
status_text = "[red]❌ FAILED[/red]"
|
251
261
|
failed += 1
|
@@ -263,7 +273,10 @@ def run_tests() -> NoReturn:
|
|
263
273
|
elif duration_ms <= 10:
|
264
274
|
perf = "[yellow]Good[/yellow]"
|
265
275
|
elif duration_ms > 0:
|
266
|
-
|
276
|
+
if display_name.lower().startswith("cleanup"):
|
277
|
+
perf = ""
|
278
|
+
else:
|
279
|
+
perf = "[red]🐌 Needs work[/red]"
|
267
280
|
else:
|
268
281
|
perf = "-"
|
269
282
|
|
@@ -161,31 +161,33 @@ class SerialTransport:
|
|
161
161
|
content = line.decode('ascii', 'ignore').strip()
|
162
162
|
return ParsedResponse(None, content, False)
|
163
163
|
|
164
|
+
|
164
165
|
def _handle_button_data(self, byte_val: int) -> None:
|
165
166
|
if byte_val == self._last_button_mask:
|
166
167
|
return
|
167
|
-
|
168
|
+
|
168
169
|
changed_bits = byte_val ^ self._last_button_mask
|
170
|
+
print("\n", end='')
|
169
171
|
self._log(f"Button state changed: 0x{self._last_button_mask:02X} -> 0x{byte_val:02X}")
|
170
172
|
|
171
|
-
for bit in range(
|
173
|
+
for bit in range(8):
|
172
174
|
if changed_bits & (1 << bit):
|
173
175
|
is_pressed = bool(byte_val & (1 << bit))
|
174
176
|
button_name = self.BUTTON_MAP[bit] if bit < len(self.BUTTON_MAP) else f"bit{bit}"
|
175
|
-
|
177
|
+
|
176
178
|
self._log(f"Button {button_name}: {'PRESSED' if is_pressed else 'RELEASED'}")
|
177
|
-
|
179
|
+
print(">>> ", end='', flush=True)
|
178
180
|
if is_pressed:
|
179
181
|
self._button_states |= (1 << bit)
|
180
182
|
else:
|
181
183
|
self._button_states &= ~(1 << bit)
|
182
|
-
|
184
|
+
|
183
185
|
if self._button_callback and bit < len(self.BUTTON_ENUM_MAP):
|
184
186
|
try:
|
185
187
|
self._button_callback(self.BUTTON_ENUM_MAP[bit], is_pressed)
|
186
188
|
except Exception as e:
|
187
189
|
self._log(f"Button callback failed: {e}", "ERROR")
|
188
|
-
|
190
|
+
|
189
191
|
self._last_button_mask = byte_val
|
190
192
|
|
191
193
|
def _process_pending_commands(self, content: str) -> None:
|
@@ -234,54 +236,114 @@ class SerialTransport:
|
|
234
236
|
|
235
237
|
def _listen(self) -> None:
|
236
238
|
self._log("Starting listener thread")
|
237
|
-
|
238
239
|
read_buffer = bytearray(4096)
|
239
240
|
line_buffer = bytearray(256)
|
240
241
|
line_pos = 0
|
241
|
-
|
242
242
|
|
243
243
|
serial_read = self.serial.read
|
244
244
|
serial_in_waiting = lambda: self.serial.in_waiting
|
245
245
|
is_connected = lambda: self._is_connected
|
246
246
|
stop_requested = self._stop_event.is_set
|
247
|
-
|
248
247
|
|
249
248
|
last_cleanup = time.time()
|
250
249
|
cleanup_interval = 0.05
|
251
250
|
|
251
|
+
expecting_text_mode = False
|
252
|
+
last_byte = None
|
253
|
+
|
252
254
|
while is_connected() and not stop_requested():
|
253
255
|
try:
|
254
256
|
bytes_available = serial_in_waiting()
|
255
257
|
if not bytes_available:
|
256
258
|
time.sleep(0.001)
|
257
259
|
continue
|
258
|
-
|
260
|
+
|
259
261
|
bytes_read = serial_read(min(bytes_available, 4096))
|
260
|
-
|
261
262
|
for byte_val in bytes_read:
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
if
|
263
|
+
|
264
|
+
if last_byte == 0x0D and byte_val == 0x0A:
|
265
|
+
if line_pos > 0:
|
266
|
+
line = bytes(line_buffer[:line_pos])
|
267
|
+
line_pos = 0
|
268
|
+
if line:
|
269
|
+
response = self._parse_response_line(line)
|
270
|
+
if response.content:
|
271
|
+
self._process_pending_commands(response.content)
|
272
|
+
expecting_text_mode = False
|
273
|
+
|
274
|
+
elif byte_val >= 32 or byte_val in (0x09,):
|
275
|
+
expecting_text_mode = True
|
276
|
+
if line_pos < 256:
|
277
|
+
line_buffer[line_pos] = byte_val
|
278
|
+
line_pos += 1
|
279
|
+
|
280
|
+
elif byte_val == 0x0D:
|
281
|
+
if expecting_text_mode or line_pos > 0:
|
282
|
+
expecting_text_mode = True
|
283
|
+
else:
|
284
|
+
pass
|
285
|
+
|
286
|
+
elif byte_val == 0x0A:
|
287
|
+
last_byte_str = f"0x{last_byte:02X}" if last_byte is not None else "None"
|
288
|
+
# self._log(f"LF detected: last_byte={last_byte_str}, expecting_text={expecting_text_mode}, line_pos={line_pos}", "DEBUG")
|
289
|
+
|
290
|
+
button_combination_detected = False
|
291
|
+
|
292
|
+
if (self._last_button_mask != 0 or
|
293
|
+
(last_byte is not None and last_byte < 32 and last_byte != 0x0D) or
|
294
|
+
(line_pos > 0 and not expecting_text_mode)):
|
295
|
+
|
296
|
+
# self._log("LF: Detected as button combination (right + mouse4 = 0x0A)", "DEBUG")
|
297
|
+
self._handle_button_data(byte_val)
|
298
|
+
expecting_text_mode = False
|
299
|
+
button_combination_detected = True
|
300
|
+
line_pos = 0
|
301
|
+
|
302
|
+
if not button_combination_detected:
|
303
|
+
if last_byte == 0x0D:
|
304
|
+
self._log("LF: Completing CRLF sequence", "DEBUG")
|
305
|
+
if line_pos > 0:
|
306
|
+
line = bytes(line_buffer[:line_pos])
|
307
|
+
line_pos = 0
|
308
|
+
if line:
|
309
|
+
response = self._parse_response_line(line)
|
310
|
+
if response.content:
|
311
|
+
self._process_pending_commands(response.content)
|
312
|
+
expecting_text_mode = False
|
313
|
+
elif line_pos > 0 and expecting_text_mode:
|
314
|
+
self._log("LF: Ending line with text in buffer", "DEBUG")
|
268
315
|
line = bytes(line_buffer[:line_pos])
|
269
316
|
line_pos = 0
|
270
|
-
|
271
317
|
if line:
|
272
318
|
response = self._parse_response_line(line)
|
273
319
|
if response.content:
|
274
320
|
self._process_pending_commands(response.content)
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
321
|
+
expecting_text_mode = False
|
322
|
+
elif expecting_text_mode:
|
323
|
+
self._log("LF: Empty line in text mode", "DEBUG")
|
324
|
+
expecting_text_mode = False
|
325
|
+
else:
|
326
|
+
self._log("LF: Treating as button data (no text context)", "DEBUG")
|
327
|
+
self._handle_button_data(byte_val)
|
328
|
+
expecting_text_mode = False
|
329
|
+
line_pos = 0 # Clear any accumulated data
|
330
|
+
|
331
|
+
elif byte_val < 32:
|
332
|
+
if last_byte == 0x0D:
|
333
|
+
self._log(f"Processing delayed CR as button data: 0x0D", "DEBUG")
|
334
|
+
self._handle_button_data(0x0D)
|
335
|
+
|
336
|
+
self._handle_button_data(byte_val)
|
337
|
+
expecting_text_mode = False
|
338
|
+
line_pos = 0
|
339
|
+
|
340
|
+
last_byte = byte_val
|
341
|
+
|
280
342
|
current_time = time.time()
|
281
343
|
if current_time - last_cleanup > cleanup_interval:
|
282
344
|
self._cleanup_timed_out_commands()
|
283
345
|
last_cleanup = current_time
|
284
|
-
|
346
|
+
|
285
347
|
except serial.SerialException as e:
|
286
348
|
self._log(f"Serial exception in listener: {e}", "ERROR")
|
287
349
|
if self.auto_reconnect:
|
@@ -290,7 +352,7 @@ class SerialTransport:
|
|
290
352
|
break
|
291
353
|
except Exception as e:
|
292
354
|
self._log(f"Unexpected exception in listener: {e}", "ERROR")
|
293
|
-
|
355
|
+
|
294
356
|
self._log("Listener thread ending")
|
295
357
|
|
296
358
|
def _attempt_reconnect(self) -> None:
|
@@ -449,7 +511,7 @@ class SerialTransport:
|
|
449
511
|
self.serial = None
|
450
512
|
self._log("Disconnection completed")
|
451
513
|
|
452
|
-
def send_command(self, command: str, expect_response: bool =
|
514
|
+
def send_command(self, command: str, expect_response: bool = False,
|
453
515
|
timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
|
454
516
|
command_start = time.time()
|
455
517
|
|
@@ -457,11 +519,10 @@ class SerialTransport:
|
|
457
519
|
raise MakcuConnectionError("Not connected")
|
458
520
|
|
459
521
|
if not expect_response:
|
460
|
-
|
461
|
-
self.serial.write(cmd_bytes)
|
522
|
+
self.serial.write(f"{command}\r\n".encode('ascii'))
|
462
523
|
self.serial.flush()
|
463
524
|
send_time = time.time() - command_start
|
464
|
-
self._log(f"Command '{command}' sent in {send_time:.
|
525
|
+
self._log(f"Command '{command}' sent in {send_time:.5f}s (no response expected)")
|
465
526
|
return command
|
466
527
|
|
467
528
|
cmd_id = self._generate_command_id()
|
@@ -488,7 +549,7 @@ class SerialTransport:
|
|
488
549
|
|
489
550
|
response = result.split('#')[0] if '#' in result else result
|
490
551
|
total_time = time.time() - command_start
|
491
|
-
self._log(f"Command '{command}' completed in {total_time:.
|
552
|
+
self._log(f"Command '{command}' completed in {total_time:.5f}s total")
|
492
553
|
return response
|
493
554
|
|
494
555
|
except TimeoutError:
|
@@ -1,2 +1,2 @@
|
|
1
|
-
# This file is intentionally empty.
|
1
|
+
# This file is intentionally empty.
|
2
2
|
# It serves as a marker to indicate that this package supports type hints.
|
@@ -137,6 +137,23 @@ def test_mixed_operations(makcu):
|
|
137
137
|
assert elapsed_ms < 15
|
138
138
|
|
139
139
|
|
140
|
-
|
141
|
-
|
142
|
-
|
140
|
+
def test_cleanup(makcu):
|
141
|
+
time.sleep(0.1)
|
142
|
+
|
143
|
+
makcu.lock_left(False)
|
144
|
+
makcu.lock_right(False)
|
145
|
+
makcu.lock_middle(False)
|
146
|
+
makcu.lock_side1(False)
|
147
|
+
makcu.lock_side2(False)
|
148
|
+
makcu.lock_x(False)
|
149
|
+
makcu.lock_y(False)
|
150
|
+
|
151
|
+
makcu.release(MouseButton.LEFT)
|
152
|
+
makcu.release(MouseButton.RIGHT)
|
153
|
+
makcu.release(MouseButton.MIDDLE)
|
154
|
+
makcu.release(MouseButton.MOUSE4)
|
155
|
+
makcu.release(MouseButton.MOUSE5)
|
156
|
+
|
157
|
+
makcu.enable_button_monitoring(False)
|
158
|
+
makcu.disconnect()
|
159
|
+
assert not makcu.is_connected(), "Failed to disconnect from the makcu"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: makcu
|
3
|
-
Version: 2.2.
|
3
|
+
Version: 2.2.2
|
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.2.
|
711
|
+
# 🖱️ Makcu Python Library v2.2.2
|
712
712
|
|
713
713
|
[](https://pypi.org/project/makcu/)
|
714
714
|
[](https://pypi.org/project/makcu/)
|
@@ -1112,4 +1112,5 @@ GPL License © SleepyTotem
|
|
1112
1112
|
|
1113
1113
|
- [GitHub Repository](https://github.com/SleepyTotem/makcu-py-lib)
|
1114
1114
|
- [PyPI Package](https://pypi.org/project/makcu/)
|
1115
|
-
- [Documentation](https://makcu.readthedocs.io/)
|
1115
|
+
- [Documentation](https://makcu-py-lib.readthedocs.io/)
|
1116
|
+
- [Changelog](https://makcu-py-lib.readthedocs.io/en/latest/changelog.html)
|
makcu-2.2.0/makcu/conftest.py
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
import pytest
|
2
|
-
import time
|
3
|
-
from makcu import MakcuController, MouseButton
|
4
|
-
|
5
|
-
@pytest.fixture(scope="session")
|
6
|
-
def makcu(request):
|
7
|
-
ctrl = MakcuController(fallback_com_port="COM1", debug=False)
|
8
|
-
|
9
|
-
def cleanup():
|
10
|
-
if ctrl.is_connected():
|
11
|
-
|
12
|
-
time.sleep(0.1)
|
13
|
-
|
14
|
-
ctrl.lock_left(False)
|
15
|
-
ctrl.lock_right(False)
|
16
|
-
ctrl.lock_middle(False)
|
17
|
-
ctrl.lock_side1(False)
|
18
|
-
ctrl.lock_side2(False)
|
19
|
-
ctrl.lock_x(False)
|
20
|
-
ctrl.lock_y(False)
|
21
|
-
|
22
|
-
ctrl.release(MouseButton.LEFT)
|
23
|
-
ctrl.release(MouseButton.RIGHT)
|
24
|
-
ctrl.release(MouseButton.MIDDLE)
|
25
|
-
ctrl.release(MouseButton.MOUSE4)
|
26
|
-
ctrl.release(MouseButton.MOUSE5)
|
27
|
-
|
28
|
-
ctrl.enable_button_monitoring(False)
|
29
|
-
|
30
|
-
ctrl.disconnect()
|
31
|
-
|
32
|
-
request.addfinalizer(cleanup)
|
33
|
-
|
34
|
-
return ctrl
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|