Files
Vision-Web/SERVER_INTEGRATION.md
T
2026-06-03 00:01:13 +08:00

572 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AimBot 上游服务器对接文档
## 整体架构
```
┌──────────────────────────────────────────────────────────┐
│ 浏览器 (Vue3 前端) │
│ - 自动模式:观看 MJPEG 视频流 │
│ - 手动模式:键盘遥控(WASD/方向键/J) + MJPEG 视频流 │
│ - 调试模式:PID 调参 + 激光开关 │
└──────┬──────────────┬──────────────┬─────────────────────┘
│ HTTP │ WebSocket │ HTTP (MJPEG)
▼ ▼ ▼
┌──────────────┐ ┌──────────┐ ┌───────────────────┐
│ Nginx │ │rosbridge │ │ web_video_server │
│ 静态文件 │ │ :9090 │ │ :8081 │
│ 反向代理 │ └────┬─────┘ └────────┬──────────┘
└──────┬───────┘ │ │
│ ▼ ▼
│ ┌────────────────────────────────┐
└─────►│ ROS2 节点 │
│ - /cmd_wheel (轮速控制) │
│ - /cmd_gimbal (云台控制) │
│ - /pid_config (PID参数) │
│ - /api/mode/switch (模式切换) │
│ - /api/laser (激光开关) │
└────────────────────────────────┘
```
## 一、数据结构详解
前端与 ROS2 之间通过 HTTP API 和 rosbridge WebSocket 两种通道通信。以下为所有接口和消息的完整数据格式。
---
### 1.1 HTTP API — 模式切换
前端发起模式切换请求,ROS2 后端需实现对应 HTTP 接口。
```
POST /api/mode/switch
Content-Type: application/json
```
**请求体:**
```json
{
"mode": "auto"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `mode` | string | 是 | 目标模式,可选值: `"auto"` \| `"manual"` \| `"debug"` |
**成功响应 (200):**
```json
{
"code": 0,
"message": "ok"
}
```
**错误响应 (4xx/5xx):**
```json
{
"code": -1,
"message": "unknown mode: xxx"
}
```
---
### 1.2 HTTP API — 激光开关
```
POST /api/laser
Content-Type: application/json
```
**请求体:**
```json
{
"action": "on"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `action` | string | 是 | `"on"` 开激光, `"off"` 关激光 |
**成功响应 (200):**
```json
{
"code": 0,
"message": "ok"
}
```
---
### 1.3 rosbridge WebSocket — 连接
```
URL: ws://<host>:9090
协议: rosbridge v2.0 (JSON)
连接后: 前端通过 roslibjs 自动完成协议握手
```
连接状态通过前端 UI 实时展示:
- `已连接` (绿色) — WebSocket 已建立,rosbridge 协议就绪
- `未连接` (红色) — 断开或错误
---
### 1.4 ROS 话题 — `/cmd_wheel` (底盘轮速)
| 属性 | 值 |
|------|-----|
| 消息类型 | `std_msgs/Float32MultiArray` |
| 方向 | 前端 → ROS2 |
| 发布时机 | 手动模式下按 WASD, 值变化时发送; 松键后自动停发 |
| 发布频率 | 按需 (值变即发), 约 30~60 Hz |
**数据格式:**
```json
{
"layout": {
"dim": [],
"data_offset": 0
},
"data": [1.0, 0.5]
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `data[0]` | float64 | 左轮线速度 (m/s), 正值前转, 负值后转 |
| `data[1]` | float64 | 右轮线速度 (m/s), 正值前转, 负值后转 |
**速度计算 (差速驱动运动学):**
```
给定:
linear = 线速度 (m/s), WASD 合成
angular = 角速度 (rad/s), WASD 合成
sep = 轮距 (m)
左轮速度 = linear - angular × (sep / 2)
右轮速度 = linear + angular × (sep / 2)
```
**典型值示例:**
| 操作 | data[0] (左) | data[1] (右) | 效果 |
|------|-------------|-------------|------|
| W (前进) | 0.5 | 0.5 | 直行前进 |
| S (后退) | -0.5 | -0.5 | 直行后退 |
| A (左转) | -0.1 | 0.1 | 原地左转 |
| D (右转) | 0.1 | -0.1 | 原地右转 |
| W+A (前左) | 0.4 | 0.6 | 前进 + 左转 |
| W+D (前右) | 0.6 | 0.4 | 前进 + 右转 |
---
### 1.5 ROS 话题 — `/cmd_gimbal` (云台角速度)
| 属性 | 值 |
|------|-----|
| 消息类型 | `geometry_msgs/Twist` |
| 方向 | 前端 → ROS2 |
| 发布时机 | 手动模式下按方向键, 值变化时发送; 松键后自动停发 |
| 发布频率 | 按需 (值变即发), 约 30~60 Hz |
**数据格式:**
```json
{
"linear": { "x": 0, "y": 0, "z": 0 },
"angular": { "x": 0, "y": 30.0, "z": -15.0 }
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `linear` | Vector3 | 不使用, 始终为 0 |
| `angular.x` | float64 | 不使用, 始终为 0 |
| `angular.y` | float64 | **云台 pitch 角度速率 (度/秒)**. 正值抬头, 负值低头 |
| `angular.z` | float64 | **云台 yaw 角度速率 (度/秒)**. 正值左转, 负值右转 |
> **注意:** 虽然底层消息类型是 `geometry_msgs/Twist`(语义上是 rad/s),但前端注入的数值单位是 **度/秒**。ROS2 侧接收后如需 rad/s,需自行转换:`rad/s = deg/s × π / 180`。
**典型值示例:**
| 操作 | angular.y (pitch) | angular.z (yaw) | 效果 |
|------|-------------------|-----------------|------|
| ↑ | 30.0 | 0 | 云台抬头 |
| ↓ | -30.0 | 0 | 云台低头 |
| ← | 0 | 30.0 | 云台左转 |
| → | 0 | -30.0 | 云台右转 |
| ↑+← | 30.0 | 30.0 | 抬头+左转 |
---
### 1.6 ROS 话题 — `/pid_config` (PID 参数)
| 属性 | 值 |
|------|-----|
| 消息类型 | `std_msgs/String` |
| 方向 | 前端 → ROS2 |
| 发布时机 | 调试页面点击「发布参数」按钮 |
**数据格式 (JSON 字符串, 放在 `data` 字段中):**
```json
{
"data": "{\"visual_yaw\":{\"kp\":1.0,\"ki\":0.1,\"kd\":0.01},\"visual_pitch\":{\"kp\":1.0,\"ki\":0.1,\"kd\":0.01},\"angle_yaw\":{\"kp\":2.0,\"ki\":0.2,\"kd\":0.02},\"angle_pitch\":{\"kp\":2.0,\"ki\":0.2,\"kd\":0.02}}"
}
```
**解析后结构:**
```json
{
"visual_yaw": { "kp": 1.0, "ki": 0.1, "kd": 0.01 },
"visual_pitch": { "kp": 1.0, "ki": 0.1, "kd": 0.01 },
"angle_yaw": { "kp": 2.0, "ki": 0.2, "kd": 0.02 },
"angle_pitch": { "kp": 2.0, "ki": 0.2, "kd": 0.02 }
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `visual_yaw.kp` | float64 | 视觉环 Yaw 电机 — 比例系数 |
| `visual_yaw.ki` | float64 | 视觉环 Yaw 电机 — 积分系数 |
| `visual_yaw.kd` | float64 | 视觉环 Yaw 电机 — 微分系数 |
| `visual_pitch.kp` | float64 | 视觉环 Pitch 电机 — 比例系数 |
| `visual_pitch.ki` | float64 | 视觉环 Pitch 电机 — 积分系数 |
| `visual_pitch.kd` | float64 | 视觉环 Pitch 电机 — 微分系数 |
| `angle_yaw.kp` | float64 | 角度环 Yaw 电机 — 比例系数 |
| `angle_yaw.ki` | float64 | 角度环 Yaw 电机 — 积分系数 |
| `angle_yaw.kd` | float64 | 角度环 Yaw 电机 — 微分系数 |
| `angle_pitch.kp` | float64 | 角度环 Pitch 电机 — 比例系数 |
| `angle_pitch.ki` | float64 | 角度环 Pitch 电机 — 积分系数 |
| `angle_pitch.kd` | float64 | 角度环 Pitch 电机 — 微分系数 |
ROS2 侧接收解析示例 (Python):
```python
import json
msg = json.loads(ros_msg.data) # ros_msg 是 std_msgs/String
visual_yaw_kp = msg["visual_yaw"]["kp"] # → 1.0
```
---
### 1.7 MJPEG 视频流
| 属性 | 值 |
|------|-----|
| 协议 | HTTP multipart/x-mixed-replace |
| 帧格式 | JPEG 图像 |
| 提供方 | `web_video_server` |
| URL 模板 | `http://<host>:8081/stream?topic=<话题名>` |
**URL 参数:**
| 参数 | 必填 | 说明 |
|------|------|------|
| `topic` | 是 | ROS 图像话题名, 如 `/camera/image_raw` |
| `mode` | 否 | 前端附加参数, 标识当前页面,服务端可忽略 |
**请求头:**
```
GET /stream?topic=/camera/image_raw HTTP/1.1
Accept: image/jpeg
```
**响应流 (multipart/x-mixed-replace):**
```
HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=--boundary
--boundary
Content-Type: image/jpeg
Content-Length: 54321
<JPEG binary data>
--boundary
Content-Type: image/jpeg
Content-Length: 54102
<JPEG binary data>
...
```
**前端消费方式:**
```html
<!-- 浏览器原生支持 MJPEG, 直接 <img src="..."> 即可 -->
<img src="http://192.168.0.11/stream?topic=/camera/image_raw&mode=auto" />
```
**注意事项:**
| 事项 | 说明 |
|------|------|
| 单连接限制 | `web_video_server` 默认每客户端仅允许一个连接。前端通过 `onBeforeRouteLeave` 在页面切换前主动断开旧连接 |
| Nginx 缓冲 | 反代 MJPEG 时必须 `proxy_buffering off`, 否则画面延迟严重 |
| 跨域 | 同域部署时使用相对路径可避免 CORS; 跨域场景需 Nginx 添加 CORS 头或使用 `Access-Control-Allow-Origin` |
---
### 1.8 键盘映射速查表
**底盘控制 (WASD), 手动模式:**
| 按键 | e.code | 动作 |
|------|--------|------|
| W | `KeyW` | 线速度 +0.5 m/s |
| S | `KeyS` | 线速度 -0.5 m/s |
| A | `KeyA` | 角速度 +1.0 rad/s |
| D | `KeyD` | 角速度 -1.0 rad/s |
**云台控制 (方向键), 手动模式:**
| 按键 | e.code | 动作 |
|------|--------|------|
| ↑ | `ArrowUp` | pitch +30 度/秒 |
| ↓ | `ArrowDown` | pitch -30 度/秒 |
| ← | `ArrowLeft` | yaw +30 度/秒 |
| → | `ArrowRight` | yaw -30 度/秒 |
**激光开关:**
| 按键 | e.code | 动作 |
|------|--------|------|
| J | `KeyJ` | 激光 开/关 切换 |
**多键策略:**
- 同轴冲突 (W+S, A+D, ↑+↓, ←+→): 相互抵消, 净值为 0
- 异轴组合 (W+A 等): 同时生效, 合成运动不冲突
- 重复按键 (`e.repeat`): 仅 J 键过滤 repeat, WASD/方向键允许 repeat (持续按住)
---
### 1.9 可配置参数一览
```js
// src/config.js — 部署时按实际情况修改
HTTP_API_BASE = 'http://192.168.0.11' // HTTP API 基础地址
ROSBRIDGE_URL = 'ws://192.168.0.11:9090' // rosbridge WebSocket
MJPEG_AUTO_URL = '.../stream?topic=/camera/image_raw&mode=auto' // 自动模式视频流
MJPEG_MANUAL_URL = '.../stream?topic=/camera/image_raw&mode=manual' // 手动模式视频流
CMD_WHEEL_TOPIC = '/cmd_wheel' // 底盘轮速话题
CMD_GIMBAL_TOPIC = '/cmd_gimbal' // 云台角速度话题
PID_CONFIG_TOPIC = '/pid_config' // PID 参数话题
MAX_LINEAR = 0.5 // 最大线速度 (m/s)
MAX_ANGULAR = 1.0 // 最大角速度 (rad/s)
WHEEL_SEPARATION = 0.2 // 轮距 (m)
GIMBAL_PITCH_DEG_S = 30 // 云台 pitch 速率 (度/秒)
GIMBAL_YAW_DEG_S = 30 // 云台 yaw 速率 (度/秒)
```
---
## 二、Nginx 配置
Nginx 负责三件事:托管 Vue 前端静态文件、反代 MJPEG 视频流、反代 HTTP API。
### 2.1 前端部署
构建产物在 `dist/` 目录,部署到服务器后由 Nginx 直接托管:
```bash
# 本地构建
cd aimbot-web
pnpm build
# 上传到服务器
scp -r dist/* user@server:/var/www/aimbot/
```
### 2.2 Nginx 配置示例
```nginx
server {
listen 80;
server_name your-domain.com;
# 前端静态文件
root /var/www/aimbot;
index index.html;
# SPA 路由回退 — 所有非文件请求回 index.html
location / {
try_files $uri $uri/ /index.html;
}
# MJPEG 视频流反代 (web_video_server)
location /stream {
proxy_pass http://127.0.0.1:8081/stream;
proxy_http_version 1.1;
proxy_buffering off;
# MJPEG multipart 边界必须保持完整
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# 长连接支持
proxy_set_header Connection "";
chunked_transfer_encoding off;
# 超时配置 — 视频流需要长连接
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# 模式切换 API (HTTP → ROS2 节点)
location /api/ {
proxy_pass http://127.0.0.1:8080/api/;
proxy_set_header Host $host;
proxy_set_header Content-Type application/json;
}
}
```
### 2.3 关键说明
| 配置项 | 说明 |
|--------|------|
| `proxy_buffering off` | MJPEG 流必须关闭缓冲,否则画面延迟严重 |
| `chunked_transfer_encoding off` | multipart 边界依赖原始编码 |
| `proxy_read_timeout 86400s` | 视频流是长连接,超时必须足够大 |
| `try_files $uri /index.html` | Vue Hash 路由支持,直接访问 /#/manual 等路径 |
---
## 三、ROS2 环境配置
### 3.1 rosbridge (WebSocket 服务)
前端通过 roslibjs 直连 rosbridge WebSocket,不走 Nginx 反代。
```bash
# 安装
sudo apt install ros-${ROS_DISTRO}-rosbridge-server
# 启动 (默认 ws://0.0.0.0:9090)
ros2 run rosbridge_server rosbridge_websocket
```
### 3.2 web_video_server (MJPEG 推流)
```bash
# 安装
sudo apt install ros-${ROS_DISTRO}-web-video-server
# 启动 (默认 http://0.0.0.0:8081)
# 发布摄像头话题后自动生成 /stream?topic=<topic_name>
ros2 run web_video_server web_video_server
```
### 3.3 话题一览
rosbridge 发布/订阅的话题需要与前端 `config.js` 一致:
| 话题 | 消息类型 | 方向 | 用途 |
|------|----------|------|------|
| `/cmd_wheel` | `std_msgs/Float32MultiArray` | 前端→ROS2 | 左右轮速 `[left, right]` |
| `/cmd_gimbal` | `geometry_msgs/Twist` | 前端→ROS2 | 云台角度速度(单位度/秒) `angular.y`(pitch) `angular.z`(yaw) |
| `/pid_config` | `std_msgs/String` | 前端→ROS2 | PID 参数 JSON |
### 3.4 模式切换与激光 API
前端 HTTP POST → Nginx 反代 → ROS2 节点(用户自行实现 HTTP 端)。
需实现两个接口,完整数据格式见 [1.1](#11-http-api--模式切换) 和 [1.2](#12-http-api--激光开关):
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/mode/switch` | POST | `{"mode": "auto"|"manual"|"debug"}` |
| `/api/laser` | POST | `{"action": "on"|"off"}` |
此接口需由 ROS2 侧 HTTP 服务(如 FastAPI / Flask / ros2-web-bridge)接收并处理。
---
## 四、MJPEG 视频流
详细格式见 [1.7 MJPEG 视频流](#17-mjpeg-视频流)。
### 启动与话题发布
```bash
# 启动 web_video_server
ros2 run web_video_server web_video_server
# 确保摄像头话题有数据
ros2 run image_transport republish raw in:=/camera/image_raw out:=/camera/compressed
```
`web_video_server` 自动识别话题类型并推送 MJPEG。
---
## 五、启动顺序
```bash
# 1. 启动 ROS2 核心节点
ros2 launch your_robot bringup.launch.py
# 2. 启动 rosbridge
ros2 run rosbridge_server rosbridge_websocket &
# 3. 启动视频推流
ros2 run web_video_server web_video_server &
# 4. 启动模式切换 HTTP 服务(自行实现)
python3 mode_api_server.py &
# 5. 启动/重载 Nginx
sudo systemctl reload nginx
```
---
## 六、前端 config.js 配置
完整配置项说明见 [1.9 可配置参数一览](#19-可配置参数一览)。
部署前修改 `src/config.js` 中的地址以匹配实际服务器环境,然后重新 `pnpm build`。
---
## 七、前端 Nginx 完整部署命令
```bash
# 构建
cd aimbot-web && pnpm build
# 部署
sudo cp -r dist/* /var/www/aimbot/
sudo cp nginx-aimbot.conf /etc/nginx/sites-available/aimbot
sudo ln -s /etc/nginx/sites-available/aimbot /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
---
## 八、故障排查
| 现象 | 检查项 |
|------|--------|
| MJPEG 不出画面 | `web_video_server` 是否运行,话题是否有数据,Nginx `proxy_buffering` 是否设为 `off` |
| rosbridge 连不上 | 防火墙是否开放 9090,`ROSBRIDGE_URL` 是否用 `ws://` 协议 |
| 按键无反应 | rosbridge 连接状态是否为"已连接",话题名是否与 ROS2 侧一致 |
| 模式切换报错 | `/api/mode/switch` 的 HTTP 服务是否实现并运行 |
| 激光开关无反应 | `/api/laser` 的 HTTP 服务是否实现,J 键或按钮是否正确触发 |
| 页面空白 | Nginx `try_files` 是否配了 `$uri /index.html` |
| 页面切换后画面不显示 | MJPEG 服务端是否单连接限制,检查 `onBeforeRouteLeave` 是否正常释放 |