airmouse-core 2.0.0__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.
- airmouse_core-2.0.0/LICENSE +21 -0
- airmouse_core-2.0.0/PKG-INFO +247 -0
- airmouse_core-2.0.0/README.md +204 -0
- airmouse_core-2.0.0/airmouse/__init__.py +21 -0
- airmouse_core-2.0.0/airmouse/cli.py +201 -0
- airmouse_core-2.0.0/airmouse/py.typed +1 -0
- airmouse_core-2.0.0/airmouse/server.py +442 -0
- airmouse_core-2.0.0/airmouse_core.egg-info/PKG-INFO +247 -0
- airmouse_core-2.0.0/airmouse_core.egg-info/SOURCES.txt +36 -0
- airmouse_core-2.0.0/airmouse_core.egg-info/dependency_links.txt +1 -0
- airmouse_core-2.0.0/airmouse_core.egg-info/entry_points.txt +2 -0
- airmouse_core-2.0.0/airmouse_core.egg-info/requires.txt +13 -0
- airmouse_core-2.0.0/airmouse_core.egg-info/top_level.txt +5 -0
- airmouse_core-2.0.0/core/actuator.py +64 -0
- airmouse_core-2.0.0/core/jitter_buffer.py +91 -0
- airmouse_core-2.0.0/core/mouse_controller.py +69 -0
- airmouse_core-2.0.0/core/one_euro.py +67 -0
- airmouse_core-2.0.0/network/protocol.py +286 -0
- airmouse_core-2.0.0/network/tcp_server.py +214 -0
- airmouse_core-2.0.0/network/udp_discovery.py +102 -0
- airmouse_core-2.0.0/network/ws_server.py +128 -0
- airmouse_core-2.0.0/pyproject.toml +80 -0
- airmouse_core-2.0.0/security/auth.py +132 -0
- airmouse_core-2.0.0/security/tls_manager.py +145 -0
- airmouse_core-2.0.0/setup.cfg +4 -0
- airmouse_core-2.0.0/tests/test_actuator_and_buffer.py +67 -0
- airmouse_core-2.0.0/tests/test_airmouse_server.py +173 -0
- airmouse_core-2.0.0/tests/test_auth.py +23 -0
- airmouse_core-2.0.0/tests/test_cli.py +59 -0
- airmouse_core-2.0.0/tests/test_filters.py +29 -0
- airmouse_core-2.0.0/tests/test_integration.py +537 -0
- airmouse_core-2.0.0/tests/test_protocol.py +96 -0
- airmouse_core-2.0.0/tests/test_protocol_edge_cases.py +160 -0
- airmouse_core-2.0.0/tests/test_tls_manager.py +47 -0
- airmouse_core-2.0.0/tests/test_udp_discovery.py +94 -0
- airmouse_core-2.0.0/tests/test_ws_server.py +162 -0
- airmouse_core-2.0.0/ui/app.py +210 -0
- airmouse_core-2.0.0/ui/tray_manager.py +50 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AirMouse Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: airmouse-core
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: High-performance, secure Python server and protocol engine for AirMouse remote input devices
|
|
5
|
+
Author-email: AirMouse Team <dev@airmouse.io>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/AirMouse/AirMouse
|
|
8
|
+
Project-URL: Documentation, https://github.com/AirMouse/AirMouse/tree/main/pc/airmouse_py
|
|
9
|
+
Project-URL: Repository, https://github.com/AirMouse/AirMouse.git
|
|
10
|
+
Project-URL: Issues, https://github.com/AirMouse/AirMouse/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/AirMouse/AirMouse/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: airmouse,remote-mouse,input-devices,gyroscope,accelerometer,websocket,tcp,udp-discovery,one-euro-filter,jitter-buffer,zero-latency
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: pydantic>=2.0.0
|
|
31
|
+
Requires-Dist: pynput>=1.7.6
|
|
32
|
+
Requires-Dist: cryptography>=42.0.0
|
|
33
|
+
Requires-Dist: websockets>=12.0
|
|
34
|
+
Requires-Dist: qrcode[pil]>=7.4.2
|
|
35
|
+
Requires-Dist: pillow>=10.0.0
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
40
|
+
Requires-Dist: build>=1.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
<div align="center">
|
|
45
|
+
|
|
46
|
+
# 🖱️ AirMouse Core (`airmouse-core`)
|
|
47
|
+
|
|
48
|
+
[](https://pypi.org/project/airmouse-core/)
|
|
49
|
+
[](https://pypi.org/project/airmouse-core/)
|
|
50
|
+
[](https://opensource.org/licenses/MIT)
|
|
51
|
+
[](https://peps.python.org/pep-0561/)
|
|
52
|
+
[]()
|
|
53
|
+
|
|
54
|
+
**High-performance, ultra-low-latency remote mouse and input server engine for Python.**
|
|
55
|
+
Seamlessly turns Android, iOS, or web client devices into responsive air mice with hardware-grade cursor smoothing, robust replay-attack mitigation, and TLS 1.2+ encryption.
|
|
56
|
+
|
|
57
|
+
[Key Features](#-key-features) • [Installation](#-installation) • [Quick Start CLI](#-quick-start-cli) • [Python API](#-python-api) • [Architecture](#-architecture) • [Protocol V2](#-protocol-v2-specification) • [Testing](#-test-suite)
|
|
58
|
+
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## ⚡ Key Features
|
|
64
|
+
|
|
65
|
+
- 🔒 **Enterprise-Grade TLS Security**:
|
|
66
|
+
- Full TLS 1.2+ encryption on both TCP and WebSocket connections.
|
|
67
|
+
- Automatic self-signed X.509 certificate provisioning with persistent key storage (`0600` permissions) to prevent TLS pinning breaks on server reboot.
|
|
68
|
+
- 📱 **Zero-Friction Terminal QR Pairing**:
|
|
69
|
+
- Out-of-the-box CLI displays ANSI QR codes right in your terminal for 1-second camera scan pairing from mobile devices.
|
|
70
|
+
- 🎯 **1€ (One Euro) Adaptive Smoothing**:
|
|
71
|
+
- Implements Casiez et al. adaptive low-pass filter algorithm to eliminate micro-jitter during slow precision movements while eliminating lag during fast sweeps.
|
|
72
|
+
- 🌐 **LAN Auto-Discovery**:
|
|
73
|
+
- Multithreaded UDP broadcast responder with cryptographic challenge-response nonces for instantaneous zero-configuration LAN discovery.
|
|
74
|
+
- 🧵 **Thread-Safe Actuation Decoupling**:
|
|
75
|
+
- Completely decouples high-frequency network I/O (`asyncio`) from OS windowing/mouse controller drivers (`pynput`) via an internal thread-safe queue.
|
|
76
|
+
- 🛡️ **Anti-Replay Attack Protection**:
|
|
77
|
+
- Enforces sliding timestamp freshness windows and per-session monotonic sequence numbering.
|
|
78
|
+
- 📦 **PEP 561 Compliant**:
|
|
79
|
+
- Full typing support with `py.typed` marker for static analysis with mypy, pyright, and modern IDE auto-complete.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 📥 Installation
|
|
84
|
+
|
|
85
|
+
Install the stable release directly from PyPI:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install airmouse-core
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or install with development dependencies:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install airmouse-core[dev]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 🚀 Quick Start CLI
|
|
100
|
+
|
|
101
|
+
Run the production server directly from the command line:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
airmouse-server --port 8080 --ws-port 8081 --udp-port 8083 --qr
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### CLI Options:
|
|
108
|
+
|
|
109
|
+
| Flag | Description | Default |
|
|
110
|
+
|------|-------------|---------|
|
|
111
|
+
| `--host` | Bind interface address | `0.0.0.0` |
|
|
112
|
+
| `--port` | TCP listening port | `8080` |
|
|
113
|
+
| `--ws-port` | WebSocket listening port | `8081` |
|
|
114
|
+
| `--udp-port` | UDP LAN discovery broadcast port | `8083` |
|
|
115
|
+
| `--qr` | Render terminal ANSI QR code for mobile pairing | `False` |
|
|
116
|
+
| `--no-tls` | Disable TLS encryption | `False` (TLS enabled) |
|
|
117
|
+
| `--no-smoothing` | Disable 1€ adaptive filter | `False` (Smoothing enabled) |
|
|
118
|
+
| `--sensitivity` | Mouse cursor speed multiplier | `1.0` |
|
|
119
|
+
| `--name` | Friendly server display name | `AirMouse Server` |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 💻 Python API
|
|
124
|
+
|
|
125
|
+
`airmouse-core` exposes a clean, intuitive public API:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import asyncio
|
|
129
|
+
from airmouse import (
|
|
130
|
+
AirMouseServer,
|
|
131
|
+
OneEuroFilter,
|
|
132
|
+
UDPJitterBuffer,
|
|
133
|
+
AuthManager,
|
|
134
|
+
ProtocolParser,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
async def main():
|
|
138
|
+
# 1. Initialize server
|
|
139
|
+
server = AirMouseServer(
|
|
140
|
+
host="0.0.0.0",
|
|
141
|
+
tcp_port=8080,
|
|
142
|
+
ws_port=8081,
|
|
143
|
+
udp_port=8083,
|
|
144
|
+
enable_tls=True,
|
|
145
|
+
sensitivity=1.2,
|
|
146
|
+
smoothing=True,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# 2. Register lifecycle and input event callbacks
|
|
150
|
+
server.on_connect(lambda session_id: print(f"Client connected: {session_id}"))
|
|
151
|
+
server.on_disconnect(lambda session_id: print(f"Client disconnected: {session_id}"))
|
|
152
|
+
server.on_move(lambda dx, dy: print(f"Motion: dx={dx:.2f}, dy={dy:.2f}"))
|
|
153
|
+
server.on_click(lambda button: print(f"Click: {button}"))
|
|
154
|
+
|
|
155
|
+
# 3. Start server
|
|
156
|
+
await server.start()
|
|
157
|
+
print("AirMouse Server is running. Press Ctrl+C to exit.")
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
await asyncio.Event().wait()
|
|
161
|
+
finally:
|
|
162
|
+
await server.stop()
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
asyncio.run(main())
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 🏗️ Architecture
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
Mobile / Web Client (Android / iOS / Browser)
|
|
174
|
+
│
|
|
175
|
+
┌───────────┴───────────┐
|
|
176
|
+
▼ ▼
|
|
177
|
+
[TCP TLS 1.2+] [WebSocket TLS]
|
|
178
|
+
│ │
|
|
179
|
+
└───────────┬───────────┘
|
|
180
|
+
│ (Asyncio Network Loop)
|
|
181
|
+
▼
|
|
182
|
+
ProtocolParser (V2)
|
|
183
|
+
[Replay & Bounds Validation]
|
|
184
|
+
│
|
|
185
|
+
▼
|
|
186
|
+
Thread-Safe Command Queue
|
|
187
|
+
│
|
|
188
|
+
▼
|
|
189
|
+
MouseActuatorThread
|
|
190
|
+
[OneEuroFilter Smoothing]
|
|
191
|
+
│
|
|
192
|
+
▼
|
|
193
|
+
OS Cursor Actuation (pynput)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 📡 Protocol V2 Specification
|
|
199
|
+
|
|
200
|
+
Every wire packet follows the strict canonical V2 envelope:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"v": 2,
|
|
205
|
+
"type": "move",
|
|
206
|
+
"seq": 1024,
|
|
207
|
+
"ts": 1729771200000,
|
|
208
|
+
"id": "optional-request-id",
|
|
209
|
+
"payload": {
|
|
210
|
+
"dx": 12.5,
|
|
211
|
+
"dy": -3.2,
|
|
212
|
+
"dt": 0.016
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Supported Message Types:
|
|
218
|
+
- **`hello`**: Initial handshake pairing request carrying HMAC token.
|
|
219
|
+
- **`welcome`**: Server approval response.
|
|
220
|
+
- **`move`**: Relative mouse coordinates (`dx`, `dy`, `dt`).
|
|
221
|
+
- **`click`**: Mouse button clicks (`left`, `right`, `middle`, `double`).
|
|
222
|
+
- **`scroll`**: Wheel scroll offset (`delta`).
|
|
223
|
+
- **`key`**: Keyboard input events (`key`, `modifiers`).
|
|
224
|
+
- **`discover` / `offer`**: UDP LAN pairing handshake.
|
|
225
|
+
- **`ping` / `pong`**: Heartbeat and connection keep-alive.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 🧪 Test Suite
|
|
230
|
+
|
|
231
|
+
The test suite covers 100% of the core functionality with high test coverage:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
cd pc/airmouse_py
|
|
235
|
+
pytest --cov=. --cov-report=term-missing tests
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Results:**
|
|
239
|
+
- **46 passing unit and integration tests**
|
|
240
|
+
- **88% code coverage**
|
|
241
|
+
- Automated testing for replay protection, crypto HMAC validation, TLS X.509 generation, and socket concurrency.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## 📄 License
|
|
246
|
+
|
|
247
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🖱️ AirMouse Core (`airmouse-core`)
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/airmouse-core/)
|
|
6
|
+
[](https://pypi.org/project/airmouse-core/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://peps.python.org/pep-0561/)
|
|
9
|
+
[]()
|
|
10
|
+
|
|
11
|
+
**High-performance, ultra-low-latency remote mouse and input server engine for Python.**
|
|
12
|
+
Seamlessly turns Android, iOS, or web client devices into responsive air mice with hardware-grade cursor smoothing, robust replay-attack mitigation, and TLS 1.2+ encryption.
|
|
13
|
+
|
|
14
|
+
[Key Features](#-key-features) • [Installation](#-installation) • [Quick Start CLI](#-quick-start-cli) • [Python API](#-python-api) • [Architecture](#-architecture) • [Protocol V2](#-protocol-v2-specification) • [Testing](#-test-suite)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## ⚡ Key Features
|
|
21
|
+
|
|
22
|
+
- 🔒 **Enterprise-Grade TLS Security**:
|
|
23
|
+
- Full TLS 1.2+ encryption on both TCP and WebSocket connections.
|
|
24
|
+
- Automatic self-signed X.509 certificate provisioning with persistent key storage (`0600` permissions) to prevent TLS pinning breaks on server reboot.
|
|
25
|
+
- 📱 **Zero-Friction Terminal QR Pairing**:
|
|
26
|
+
- Out-of-the-box CLI displays ANSI QR codes right in your terminal for 1-second camera scan pairing from mobile devices.
|
|
27
|
+
- 🎯 **1€ (One Euro) Adaptive Smoothing**:
|
|
28
|
+
- Implements Casiez et al. adaptive low-pass filter algorithm to eliminate micro-jitter during slow precision movements while eliminating lag during fast sweeps.
|
|
29
|
+
- 🌐 **LAN Auto-Discovery**:
|
|
30
|
+
- Multithreaded UDP broadcast responder with cryptographic challenge-response nonces for instantaneous zero-configuration LAN discovery.
|
|
31
|
+
- 🧵 **Thread-Safe Actuation Decoupling**:
|
|
32
|
+
- Completely decouples high-frequency network I/O (`asyncio`) from OS windowing/mouse controller drivers (`pynput`) via an internal thread-safe queue.
|
|
33
|
+
- 🛡️ **Anti-Replay Attack Protection**:
|
|
34
|
+
- Enforces sliding timestamp freshness windows and per-session monotonic sequence numbering.
|
|
35
|
+
- 📦 **PEP 561 Compliant**:
|
|
36
|
+
- Full typing support with `py.typed` marker for static analysis with mypy, pyright, and modern IDE auto-complete.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 📥 Installation
|
|
41
|
+
|
|
42
|
+
Install the stable release directly from PyPI:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install airmouse-core
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or install with development dependencies:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install airmouse-core[dev]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🚀 Quick Start CLI
|
|
57
|
+
|
|
58
|
+
Run the production server directly from the command line:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
airmouse-server --port 8080 --ws-port 8081 --udp-port 8083 --qr
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### CLI Options:
|
|
65
|
+
|
|
66
|
+
| Flag | Description | Default |
|
|
67
|
+
|------|-------------|---------|
|
|
68
|
+
| `--host` | Bind interface address | `0.0.0.0` |
|
|
69
|
+
| `--port` | TCP listening port | `8080` |
|
|
70
|
+
| `--ws-port` | WebSocket listening port | `8081` |
|
|
71
|
+
| `--udp-port` | UDP LAN discovery broadcast port | `8083` |
|
|
72
|
+
| `--qr` | Render terminal ANSI QR code for mobile pairing | `False` |
|
|
73
|
+
| `--no-tls` | Disable TLS encryption | `False` (TLS enabled) |
|
|
74
|
+
| `--no-smoothing` | Disable 1€ adaptive filter | `False` (Smoothing enabled) |
|
|
75
|
+
| `--sensitivity` | Mouse cursor speed multiplier | `1.0` |
|
|
76
|
+
| `--name` | Friendly server display name | `AirMouse Server` |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 💻 Python API
|
|
81
|
+
|
|
82
|
+
`airmouse-core` exposes a clean, intuitive public API:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import asyncio
|
|
86
|
+
from airmouse import (
|
|
87
|
+
AirMouseServer,
|
|
88
|
+
OneEuroFilter,
|
|
89
|
+
UDPJitterBuffer,
|
|
90
|
+
AuthManager,
|
|
91
|
+
ProtocolParser,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
async def main():
|
|
95
|
+
# 1. Initialize server
|
|
96
|
+
server = AirMouseServer(
|
|
97
|
+
host="0.0.0.0",
|
|
98
|
+
tcp_port=8080,
|
|
99
|
+
ws_port=8081,
|
|
100
|
+
udp_port=8083,
|
|
101
|
+
enable_tls=True,
|
|
102
|
+
sensitivity=1.2,
|
|
103
|
+
smoothing=True,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# 2. Register lifecycle and input event callbacks
|
|
107
|
+
server.on_connect(lambda session_id: print(f"Client connected: {session_id}"))
|
|
108
|
+
server.on_disconnect(lambda session_id: print(f"Client disconnected: {session_id}"))
|
|
109
|
+
server.on_move(lambda dx, dy: print(f"Motion: dx={dx:.2f}, dy={dy:.2f}"))
|
|
110
|
+
server.on_click(lambda button: print(f"Click: {button}"))
|
|
111
|
+
|
|
112
|
+
# 3. Start server
|
|
113
|
+
await server.start()
|
|
114
|
+
print("AirMouse Server is running. Press Ctrl+C to exit.")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
await asyncio.Event().wait()
|
|
118
|
+
finally:
|
|
119
|
+
await server.stop()
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
asyncio.run(main())
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 🏗️ Architecture
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Mobile / Web Client (Android / iOS / Browser)
|
|
131
|
+
│
|
|
132
|
+
┌───────────┴───────────┐
|
|
133
|
+
▼ ▼
|
|
134
|
+
[TCP TLS 1.2+] [WebSocket TLS]
|
|
135
|
+
│ │
|
|
136
|
+
└───────────┬───────────┘
|
|
137
|
+
│ (Asyncio Network Loop)
|
|
138
|
+
▼
|
|
139
|
+
ProtocolParser (V2)
|
|
140
|
+
[Replay & Bounds Validation]
|
|
141
|
+
│
|
|
142
|
+
▼
|
|
143
|
+
Thread-Safe Command Queue
|
|
144
|
+
│
|
|
145
|
+
▼
|
|
146
|
+
MouseActuatorThread
|
|
147
|
+
[OneEuroFilter Smoothing]
|
|
148
|
+
│
|
|
149
|
+
▼
|
|
150
|
+
OS Cursor Actuation (pynput)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 📡 Protocol V2 Specification
|
|
156
|
+
|
|
157
|
+
Every wire packet follows the strict canonical V2 envelope:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"v": 2,
|
|
162
|
+
"type": "move",
|
|
163
|
+
"seq": 1024,
|
|
164
|
+
"ts": 1729771200000,
|
|
165
|
+
"id": "optional-request-id",
|
|
166
|
+
"payload": {
|
|
167
|
+
"dx": 12.5,
|
|
168
|
+
"dy": -3.2,
|
|
169
|
+
"dt": 0.016
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Supported Message Types:
|
|
175
|
+
- **`hello`**: Initial handshake pairing request carrying HMAC token.
|
|
176
|
+
- **`welcome`**: Server approval response.
|
|
177
|
+
- **`move`**: Relative mouse coordinates (`dx`, `dy`, `dt`).
|
|
178
|
+
- **`click`**: Mouse button clicks (`left`, `right`, `middle`, `double`).
|
|
179
|
+
- **`scroll`**: Wheel scroll offset (`delta`).
|
|
180
|
+
- **`key`**: Keyboard input events (`key`, `modifiers`).
|
|
181
|
+
- **`discover` / `offer`**: UDP LAN pairing handshake.
|
|
182
|
+
- **`ping` / `pong`**: Heartbeat and connection keep-alive.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 🧪 Test Suite
|
|
187
|
+
|
|
188
|
+
The test suite covers 100% of the core functionality with high test coverage:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
cd pc/airmouse_py
|
|
192
|
+
pytest --cov=. --cov-report=term-missing tests
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Results:**
|
|
196
|
+
- **46 passing unit and integration tests**
|
|
197
|
+
- **88% code coverage**
|
|
198
|
+
- Automated testing for replay protection, crypto HMAC validation, TLS X.509 generation, and socket concurrency.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 📄 License
|
|
203
|
+
|
|
204
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AirMouse Core Library
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Production-grade Python package for AirMouse servers and input processing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from core.jitter_buffer import UDPJitterBuffer
|
|
8
|
+
from core.one_euro import OneEuroFilter
|
|
9
|
+
from network.protocol import ProtocolParser
|
|
10
|
+
from security.auth import AuthManager
|
|
11
|
+
from airmouse.server import AirMouseServer
|
|
12
|
+
|
|
13
|
+
__version__ = "2.0.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AirMouseServer",
|
|
17
|
+
"OneEuroFilter",
|
|
18
|
+
"UDPJitterBuffer",
|
|
19
|
+
"AuthManager",
|
|
20
|
+
"ProtocolParser",
|
|
21
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
airmouse.cli
|
|
3
|
+
------------
|
|
4
|
+
Command Line Interface for AirMouse server.
|
|
5
|
+
Generates terminal QR code for zero-friction client pairing and runs the server out-of-the-box.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import asyncio
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import signal
|
|
15
|
+
import socket
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
import qrcode
|
|
20
|
+
|
|
21
|
+
from airmouse.server import AirMouseServer
|
|
22
|
+
from security.auth import AuthManager
|
|
23
|
+
|
|
24
|
+
logging.basicConfig(
|
|
25
|
+
level=logging.INFO,
|
|
26
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
27
|
+
)
|
|
28
|
+
logger = logging.getLogger("AirMouseCLI")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_local_ip() -> str:
|
|
32
|
+
"""Best-effort discovery of non-loopback LAN IP address."""
|
|
33
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
34
|
+
try:
|
|
35
|
+
# Does not actually connect but picks the outgoing route interface
|
|
36
|
+
s.connect(("8.8.8.8", 80))
|
|
37
|
+
ip = s.getsockname()[0]
|
|
38
|
+
except Exception:
|
|
39
|
+
ip = "127.0.0.1"
|
|
40
|
+
finally:
|
|
41
|
+
s.close()
|
|
42
|
+
return ip
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def display_terminal_qr(data: str) -> None:
|
|
46
|
+
"""Prints an ASCII/ANSI QR code directly in the terminal."""
|
|
47
|
+
qr = qrcode.QRCode(
|
|
48
|
+
version=1,
|
|
49
|
+
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
50
|
+
box_size=1,
|
|
51
|
+
border=1,
|
|
52
|
+
)
|
|
53
|
+
qr.add_data(data)
|
|
54
|
+
qr.make(fit=True)
|
|
55
|
+
|
|
56
|
+
f = io.StringIO()
|
|
57
|
+
qr.print_ascii(out=f, invert=True)
|
|
58
|
+
f.seek(0)
|
|
59
|
+
print(f.read())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_args(argv=None) -> argparse.Namespace:
|
|
63
|
+
parser = argparse.ArgumentParser(
|
|
64
|
+
prog="airmouse-server",
|
|
65
|
+
description="AirMouse Server - Zero-latency, TLS-secured remote input server",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--host",
|
|
69
|
+
type=str,
|
|
70
|
+
default="0.0.0.0",
|
|
71
|
+
help="IP address to bind to (default: 0.0.0.0)",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--port",
|
|
75
|
+
type=int,
|
|
76
|
+
default=8080,
|
|
77
|
+
help="TCP listening port (default: 8080)",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--ws-port",
|
|
81
|
+
type=int,
|
|
82
|
+
default=8081,
|
|
83
|
+
help="WebSocket listening port (default: 8081)",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--udp-port",
|
|
87
|
+
type=int,
|
|
88
|
+
default=8083,
|
|
89
|
+
help="UDP discovery broadcast listening port (default: 8083)",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--no-tls",
|
|
93
|
+
action="store_true",
|
|
94
|
+
help="Disable TLS encryption (TLS 1.2+ is enabled by default)",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--no-smoothing",
|
|
98
|
+
action="store_true",
|
|
99
|
+
help="Disable 1€ adaptive low-pass filter smoothing",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"--sensitivity",
|
|
103
|
+
type=float,
|
|
104
|
+
default=1.0,
|
|
105
|
+
help="Mouse speed multiplier (default: 1.0)",
|
|
106
|
+
)
|
|
107
|
+
parser.add_argument(
|
|
108
|
+
"--qr",
|
|
109
|
+
action="store_true",
|
|
110
|
+
help="Display terminal QR code for instant Android client pairing",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--name",
|
|
114
|
+
type=str,
|
|
115
|
+
default="AirMouse Server",
|
|
116
|
+
help="Friendly server display name",
|
|
117
|
+
)
|
|
118
|
+
return parser.parse_args(argv)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def run_server(args: argparse.Namespace) -> None:
|
|
122
|
+
auth_mgr = AuthManager()
|
|
123
|
+
token = auth_mgr.generate_token("cli_client")
|
|
124
|
+
lan_ip = get_local_ip()
|
|
125
|
+
|
|
126
|
+
enable_tls = not args.no_tls
|
|
127
|
+
|
|
128
|
+
pairing_payload = {
|
|
129
|
+
"host": lan_ip,
|
|
130
|
+
"tcp_port": args.port,
|
|
131
|
+
"ws_port": args.ws_port,
|
|
132
|
+
"token": token,
|
|
133
|
+
"tls": enable_tls,
|
|
134
|
+
"name": args.name,
|
|
135
|
+
}
|
|
136
|
+
pairing_json = json.dumps(pairing_payload)
|
|
137
|
+
|
|
138
|
+
print("=" * 60)
|
|
139
|
+
print(f" {args.name}")
|
|
140
|
+
print(f" LAN Address: {lan_ip}")
|
|
141
|
+
print(f" TCP Port : {args.port} (TLS: {enable_tls})")
|
|
142
|
+
print(f" WS Port : {args.ws_port} (TLS: {enable_tls})")
|
|
143
|
+
print(f" UDP Port : {args.udp_port}")
|
|
144
|
+
print(f" Pairing Token: {token}")
|
|
145
|
+
print("=" * 60)
|
|
146
|
+
|
|
147
|
+
if args.qr:
|
|
148
|
+
print("\nScan this QR code with the AirMouse Android App to pair:\n")
|
|
149
|
+
try:
|
|
150
|
+
display_terminal_qr(pairing_json)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.warning(f"Could not print QR code to terminal: {e}")
|
|
153
|
+
|
|
154
|
+
server = AirMouseServer(
|
|
155
|
+
host=args.host,
|
|
156
|
+
tcp_port=args.port,
|
|
157
|
+
ws_port=args.ws_port,
|
|
158
|
+
udp_port=args.udp_port,
|
|
159
|
+
enable_tls=enable_tls,
|
|
160
|
+
auth_manager=auth_mgr,
|
|
161
|
+
sensitivity=args.sensitivity,
|
|
162
|
+
smoothing=not args.no_smoothing,
|
|
163
|
+
server_name=args.name,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
server.on_connect(lambda s_id: logger.info(f"[Event] Client Connected: {s_id}"))
|
|
167
|
+
server.on_disconnect(lambda s_id: logger.info(f"[Event] Client Disconnected: {s_id}"))
|
|
168
|
+
server.on_click(lambda btn: logger.debug(f"[Event] Mouse Click: {btn}"))
|
|
169
|
+
|
|
170
|
+
await server.start()
|
|
171
|
+
logger.info("AirMouse server started successfully and ready for connections.")
|
|
172
|
+
|
|
173
|
+
stop_event = asyncio.Event()
|
|
174
|
+
|
|
175
|
+
def _shutdown():
|
|
176
|
+
logger.info("Shutdown signal received, shutting down gracefully...")
|
|
177
|
+
stop_event.set()
|
|
178
|
+
|
|
179
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
180
|
+
try:
|
|
181
|
+
asyncio.get_running_loop().add_signal_handler(sig, _shutdown)
|
|
182
|
+
except NotImplementedError:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
await stop_event.wait()
|
|
187
|
+
finally:
|
|
188
|
+
await server.stop()
|
|
189
|
+
logger.info("AirMouse server shut down cleanly.")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main(argv=None) -> None:
|
|
193
|
+
args = parse_args(argv)
|
|
194
|
+
try:
|
|
195
|
+
asyncio.run(run_server(args))
|
|
196
|
+
except KeyboardInterrupt:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Marker file for PEP 561 type checking.
|