Skip to content

Commit 5d3894b

Browse files
authored
Merge pull request #2 from rust17/feat/rate-monitoring
rate monitoring
2 parents 0b46a99 + 6b76524 commit 5d3894b

14 files changed

Lines changed: 457 additions & 107 deletions

File tree

AGENTS.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Remote Mouse
2+
3+
本文件为 AI 助手及开发者提供项目概览、技术细节和工作流程参考。
4+
5+
## 1. 项目概览
6+
Remote Mouse 是一款轻量级、低延迟的远程控制工具,可将移动设备(iOS/Android)通过浏览器(PWA)变为电脑的无线触控板和键盘。支持 Windows, macOS, 和 Linux。
7+
8+
## 2. 技术栈
9+
- **服务端 (Server)**:
10+
- 语言: Python 3.13+
11+
- 核心库: FastAPI (Web 服务), PyAutoGUI (模拟输入), Zeroconf (mDNS 自动发现), pystray (系统托盘), Pillow (图标处理), Loguru (日志)。
12+
- 包管理: [uv](https://github.com/astral-sh/uv)
13+
- **客户端 (Web Client)**:
14+
- 语言: TypeScript
15+
- 框架/工具: Vite, 原生 CSS, Vite-PWA (离线支持)
16+
- 测试: Vitest
17+
18+
## 3. 架构概览
19+
项目采用经典的 **Client-Server** 架构:
20+
- **服务端**: 运行多线程服务。`ServiceManager` 管理 Uvicorn (HTTP/WebSocket) 和 mDNS 响应器的生命周期。`TrayIcon` 在主线程运行,提供 UI 交互。
21+
- **客户端**: 响应式 Web 应用。`transport.ts` 处理与服务端的二进制通信,`touchpad.ts``keyboard.ts` 捕获用户输入并封装为协议指令。
22+
23+
### 服务端进程/线程模型
24+
服务端采用 **单进程、多线程** 模型:
25+
- **主线程 (Main Thread)**: 运行 `pystray` 托盘 UI 循环。
26+
- **服务线程 (Service Thread)**: 由 `ServiceManager` 启动的守护线程,运行 `uvicorn` (FastAPI) 实例。负责处理 WebSocket 通信、静态文件托管以及调用 `PyAutoGUI` 模拟输入。
27+
- **监控线程 (Monitor Thread)**: 可选线程。当开启“速率显示”时,定时计算每秒包数 (PPS) 和比特率 (BPS),并动态生成图标更新主线程的托盘显示。
28+
- **并发处理**: 网络 IO 部分基于 Python 的 `asyncio`,而 UI 与背景任务则通过多线程实现物理隔离。
29+
30+
## 4. 目录地图
31+
```text
32+
.
33+
├── server/ # Python 服务端
34+
│ ├── src/server/
35+
│ │ ├── core/ # 核心逻辑 (协议定义、指标监控)
36+
│ │ ├── services/ # 后台服务 (Web, mDNS, Manager)
37+
│ │ ├── ui/ # 系统托盘 UI
38+
│ │ └── main.py # 运行入口
39+
│ └── tests/ # 服务端单元测试
40+
├── web-client/ # TypeScript 客户端
41+
│ ├── src/
42+
│ │ ├── core/ # 协议与传输层
43+
│ │ ├── input/ # 输入捕获 (Touchpad, Scroll, Keyboard)
44+
│ │ └── ui/ # UI 组件 (Settings, Status Bar)
45+
│ └── tests/ # 客户端单元测试
46+
└── requirement/ # 需求文档 (v1~v3)
47+
```
48+
49+
## 5. 核心接口 (二进制协议)
50+
客户端通过 WebSocket 发送二进制指令。指令格式:`[OpCode (1B)] [Payload]`
51+
52+
| OpCode | 指令 | Payload 格式 | 说明 |
53+
| :--- | :--- | :--- | :--- |
54+
| `0x01` | Move | `dx(2B), dy(2B)` | 相对位移 (Big-endian signed short) |
55+
| `0x02` | Click | `button(1B), mask(1B)` | button: 1-左, 2-右; mask: 修饰键位掩码 |
56+
| `0x03` | Scroll | `sx(2B), sy(2B)` | 滚动量 |
57+
| `0x04` | Drag | `state(1B)` | 0x01-按下, 0x00-释放 |
58+
| `0x05` | Text | `UTF8 String` | 文本输入 (通过剪贴板中转以支持多语言) |
59+
| `0x06` | Key | `mask(1B), key_name(UTF8)` | 特殊键 (Enter, Backspace 等) |
60+
61+
**修饰键位掩码 (Modifier Mask):**
62+
- Bit 0: Ctrl, Bit 1: Shift, Bit 2: Alt, Bit 3: Win/Cmd
63+
64+
## 6. 常用快速命令
65+
### 服务端 (server/)
66+
- **同步依赖**: `uv sync`
67+
- **开发运行**: `uv run python -m server.main` (可带 `--port`, `--log`)
68+
- **静态检查**: `uv run ruff check .`
69+
- **代码 lint**: `uv run ruff format`
70+
- **运行测试**: `uv run pytest`
71+
72+
### 客户端 (web-client/)
73+
- **安装依赖**: `npm install`
74+
- **开发模式**: `npm run dev`
75+
- **构建打包**: `npm run build` (产物由 Python 服务端自动托管)
76+
- **运行测试**: `npm run test`
77+
78+
## 7. 开发规约与技巧
79+
80+
### 如何添加新功能(扩展协议)
81+
若需添加新的控制指令(如多媒体控制):
82+
1. **定义 OpCode**: 在 `server/src/server/core/protocol.py``web-client/src/core/protocol.ts` 中同步定义新的 `OP_XXX` 常量。
83+
2. **客户端实现**: 在 `web-client/src/input/` 下相关组件中捕获输入,调用 `transport.send()` 发送二进制数据。
84+
3. **服务端实现**: 在 `server/src/server/core/protocol.py``process_binary_command` 函数中添加对应的 `elif opcode == OP_XXX` 分支逻辑。
85+
86+
### 静态文件托管
87+
- **生产/常规模式**: 服务端会自动寻找 `web-client/dist` 目录并托管。因此,修改客户端代码后需运行 `npm run build` 才能在 `uv run python -m server.main` 中看到变化。
88+
- **开发模式**: 建议分别启动 `npm run dev`(前端热更新)和服务端。
89+
90+
### 日志
91+
- 服务端使用 `loguru`。开发时建议开启 `--log` 参数以启用详细日志记录。
92+
- 关键逻辑(如输入模拟失败)应使用 `logger.error``logger.warning`
93+
94+
## 8. Git Commit 规范
95+
遵循 **Conventional Commits** 风格:
96+
- `feat`: 新功能
97+
- `fix`: 修复 bug
98+
- `docs`: 文档变更
99+
- `style`: 代码格式 (不影响逻辑)
100+
- `refactor`: 重构
101+
- `test`: 增加测试
102+
- `chore`: 构建过程或辅助工具的变动
103+
104+
示例: `feat: add real-time rate monitoring`

README.md

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Remote Mouse is a lightweight, low-latency remote control tool that transforms y
2525
- **Modern UI**: Dark mode interface with a sleek, translucent design.
2626
- **Cross-Platform**: Server runs on Python, client works in any modern mobile browser.
2727

28-
### Download & Run (For Users)
28+
### Download & Run
2929

3030
Get the latest version from the [Releases](https://github.com/rust17/remote-mouse/releases) page.
3131

@@ -45,30 +45,6 @@ Get the latest version from the [Releases](https://github.com/rust17/remote-mous
4545

4646
---
4747

48-
### Development
49-
50-
#### Tech Stack
51-
- **Server**: Python 3.12+, FastAPI, PyAutoGUI, Zeroconf, uv
52-
- **Web Client**: TypeScript, Vite, PWA
53-
54-
#### Setup
55-
**1. Server**
56-
```bash
57-
cd server
58-
uv sync
59-
uv run python -m server.main
60-
```
61-
62-
**2. Web Client**
63-
```bash
64-
cd web-client
65-
npm install
66-
npm run build
67-
# The client is served by the Python server automatically.
68-
# For dev mode with hot-reload:
69-
npm run dev
70-
```
71-
7248
### Usage
7349
1. Start the server on your computer.
7450
2. Ensure your phone and computer are on the **same local network (Wi-Fi)**.

docs/README_ZH.md

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Remote Mouse 是一款轻量级、低延迟的远程控制工具,可将您的
2525
- **现代 UI**: 采用深色模式和精致的半透明毛玻璃视觉设计。
2626
- **跨平台**: 服务端基于 Python,客户端可在任何现代移动浏览器中运行。
2727

28-
### 下载与运行 (普通用户)
28+
### 下载与运行
2929

3030
请前往 [Releases](https://github.com/rust17/remote-mouse/releases) 页面下载最新版本。
3131

@@ -45,33 +45,6 @@ Remote Mouse 是一款轻量级、低延迟的远程控制工具,可将您的
4545

4646
---
4747

48-
### 开发指南
49-
50-
#### 技术栈
51-
- **服务端**: Python 3.12+, FastAPI, PyAutoGUI, Zeroconf, uv
52-
- **Web 客户端**: TypeScript, Vite, PWA
53-
54-
#### 环境设置
55-
56-
**1. 服务端**
57-
```bash
58-
cd server
59-
# 使用 uv 安装依赖
60-
uv sync
61-
# 运行服务端
62-
uv run python -m server.main
63-
```
64-
65-
**2. Web 客户端**
66-
```bash
67-
cd web-client
68-
npm install
69-
npm run build
70-
# 客户端文件会自动由 Python 服务端托管,
71-
# 您也可以在开发模式下运行:
72-
npm run dev
73-
```
74-
7548
### 使用方法
7649
1. 在电脑上启动服务端。
7750
2. 确保您的手机和电脑处于 **同一局域网(Wi-Fi)** 下。

server/src/server/config.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
TRAY_ICON_FG_COLOR = "white"
1818

1919

20+
def is_dev() -> bool:
21+
"""判断是否为开发环境。"""
22+
return not getattr(sys, "frozen", False)
23+
24+
2025
def get_share_dir() -> Path:
2126
share_dir = Path.home() / APP_DIR_NAME
2227
share_dir.mkdir(parents=True, exist_ok=True)
@@ -25,13 +30,13 @@ def get_share_dir() -> Path:
2530

2631
def get_static_dir() -> Path:
2732
# 1. PyInstaller environment
28-
if getattr(sys, "frozen", False):
33+
if not is_dev():
2934
bundle_dir = Path(sys._MEIPASS)
3035
source_static = bundle_dir / "web_dist"
31-
36+
3237
# Target: ~/.remote-mouse/web_dist (Persistent storage to avoid /tmp cleanup)
3338
target_static = get_share_dir() / "web_dist"
34-
39+
3540
try:
3641
# Sync files to persistent directory
3742
if target_static.exists():
@@ -53,7 +58,7 @@ def get_static_dir() -> Path:
5358

5459

5560
def get_asset_path(filename: str) -> Path:
56-
if getattr(sys, "frozen", False):
61+
if not is_dev():
5762
bundle_dir = Path(sys._MEIPASS)
5863
asset_path = bundle_dir / "assets" / filename
5964
else:

server/src/server/core/metrics.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import time
2+
from threading import Lock
3+
4+
5+
class Metrics:
6+
def __init__(self):
7+
self.packets_count = 0
8+
self.bytes_count = 0
9+
self.last_reset = time.monotonic()
10+
self._lock = Lock()
11+
self._current_pps = 0
12+
self._current_bps = 0
13+
14+
def add(self, num_bytes: int):
15+
with self._lock:
16+
# Check and reset window BEFORE adding new data
17+
self._update_if_needed()
18+
self.packets_count += 1
19+
self.bytes_count += num_bytes
20+
21+
def _update_if_needed(self):
22+
# Assumes lock is held
23+
now = time.monotonic()
24+
dt = now - self.last_reset
25+
if dt >= 1.0:
26+
self._current_pps = int(self.packets_count / dt)
27+
self._current_bps = int(self.bytes_count / dt)
28+
self.packets_count = 0
29+
self.bytes_count = 0
30+
self.last_reset = now
31+
return True
32+
return False
33+
34+
def get_current(self):
35+
with self._lock:
36+
self._update_if_needed()
37+
return self._current_pps, self._current_bps
38+
39+
40+
metrics = Metrics()

server/src/server/services/mdns.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from zeroconf import IPVersion, ServiceInfo, Zeroconf
33
from loguru import logger
44

5-
from server.config import APP_NAME, DEFAULT_PORT, MDNS_HOSTNAME
5+
from server.config import APP_NAME, DEFAULT_PORT, MDNS_HOSTNAME, is_dev
66

77

88
class MDNSResponder:
@@ -12,8 +12,19 @@ class MDNSResponder:
1212

1313
def __init__(self, service_name=APP_NAME, port=DEFAULT_PORT, hostname=MDNS_HOSTNAME):
1414
self.zeroconf = Zeroconf(ip_version=IPVersion.V4Only)
15-
self.service_name_base = service_name
16-
self.hostname = hostname
15+
16+
if is_dev():
17+
# Get only the first part of the hostname (e.g. "my-mac" from "my-mac.local")
18+
machine_name = socket.gethostname().split(".")[0]
19+
self.service_name_base = f"{service_name} ({machine_name})"
20+
21+
# Strip ".local." or ".local" from the base hostname and append machine name
22+
base = hostname.replace(".local.", "").replace(".local", "").strip(".")
23+
self.hostname = f"{base}-{machine_name}.local."
24+
else:
25+
self.service_name_base = service_name
26+
self.hostname = hostname
27+
1728
self.port = port
1829
self.service_info = None
1930

@@ -27,8 +38,6 @@ def get_local_ip(self):
2738

2839
def register(self):
2940
# mDNS 规范要求主机名以 .local. 结尾
30-
# 用户直接输入 remote-mouse.local 需要该域名解析到 IP
31-
3241
local_ip = self.get_local_ip()
3342
logger.info(f"Detected Local IP: {local_ip}")
3443

server/src/server/services/web.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from server.core.protocol import process_binary_command
66
from server.config import get_static_dir
7+
from server.core.metrics import metrics
8+
from server.ui.tray_icon import TrayIcon
79

810

911
def create_app() -> FastAPI:
@@ -13,13 +15,20 @@ def create_app() -> FastAPI:
1315
if not static_dir.exists():
1416
logger.warning(f"Static directory {static_dir} does not exist!")
1517

18+
@app.post("/api/settings/tray/rate")
19+
async def toggle_server_rate(enabled: bool):
20+
if TrayIcon.instance:
21+
TrayIcon.instance.set_show_rate(enabled)
22+
return {"status": "ok"}
23+
1624
@app.websocket("/ws")
1725
async def websocket_endpoint(websocket: WebSocket):
1826
await websocket.accept()
1927
logger.info(f"WebSocket client connected: {websocket.client}")
2028
try:
2129
while True:
2230
data = await websocket.receive_bytes()
31+
metrics.add(len(data))
2332
process_binary_command(data)
2433
except WebSocketDisconnect:
2534
logger.info("WebSocket client disconnected")

0 commit comments

Comments
 (0)