feat: initial ROS2 vision pipeline with gimbal control and nginx config
- camera_node (driver_pkg): Hikrobot MvCamera SDK image capture - vision_node (algorithm_pkg): Otsu threshold + contour detection + target tracking - web_node (application_pkg): HTTP server with MJPEG preview and mode switch API - gimbal_node (gimbal_control_pkg): UART serial communication per gimbal protocol - bringup_pkg: launch file for full pipeline - config: nginx reverse-proxy for MJPEG streaming and API routing
This commit is contained in:
+250
@@ -0,0 +1,250 @@
|
||||
# 与前端对接文档
|
||||
|
||||
## 整体架构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ 浏览器 (Vue3 前端) │
|
||||
│ - 自动模式:观看 MJPEG 视频流 │
|
||||
│ - 手动模式:键盘遥控 + MJPEG 视频流 │
|
||||
│ - 调试模式:PID 调参 │
|
||||
└──────┬──────────────┬──────────────┬─────────────────────┘
|
||||
│ HTTP │ WebSocket │ HTTP (MJPEG)
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────┐ ┌───────────────────┐
|
||||
│ Nginx │ │rosbridge │ │ web_video_server │
|
||||
│ 静态文件 │ │ :9090 │ │ :8081 │
|
||||
│ 反向代理 │ └────┬─────┘ └────────┬──────────┘
|
||||
└──────┬───────┘ │ │
|
||||
│ ▼ ▼
|
||||
│ ┌────────────────────────────────┐
|
||||
└─────►│ ROS2 节点 │
|
||||
│ - /cmd_wheel (轮速控制) │
|
||||
│ - /cmd_gimbal (云台控制) │
|
||||
│ - /pid_config (PID参数) │
|
||||
│ - /api/mode/switch (模式切换) │
|
||||
└────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 一、Nginx 配置
|
||||
|
||||
Nginx 负责三件事:托管 Vue 前端静态文件、反代 MJPEG 视频流、反代 HTTP API。
|
||||
|
||||
### 1.1 前端部署
|
||||
|
||||
构建产物在 `dist/` 目录,部署到服务器后由 Nginx 直接托管:
|
||||
|
||||
```bash
|
||||
# 本地构建
|
||||
cd aimbot-web
|
||||
pnpm build
|
||||
|
||||
# 上传到服务器
|
||||
scp -r dist/* user@server:/var/www/aimbot/
|
||||
```
|
||||
|
||||
### 1.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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 关键说明
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|--------|------|
|
||||
| `proxy_buffering off` | MJPEG 流必须关闭缓冲,否则画面延迟严重 |
|
||||
| `chunked_transfer_encoding off` | multipart 边界依赖原始编码 |
|
||||
| `proxy_read_timeout 86400s` | 视频流是长连接,超时必须足够大 |
|
||||
| `try_files $uri /index.html` | Vue Hash 路由支持,直接访问 /#/manual 等路径 |
|
||||
|
||||
---
|
||||
|
||||
## 二、ROS2 环境配置
|
||||
|
||||
### 2.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
|
||||
```
|
||||
|
||||
### 2.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
|
||||
```
|
||||
|
||||
### 2.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 |
|
||||
|
||||
### 2.4 模式切换 API
|
||||
|
||||
前端 HTTP POST → Nginx 反代 → ROS2 节点(用户自行实现 HTTP 端):
|
||||
|
||||
```
|
||||
POST /api/mode/switch
|
||||
Content-Type: application/json
|
||||
|
||||
{ "mode": "auto" | "manual" | "debug" }
|
||||
```
|
||||
|
||||
此接口需由 ROS2 侧 HTTP 服务(如 FastAPI / Flask / ros2-web-bridge)接收并处理模式切换逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 三、MJPEG 视频流地址
|
||||
|
||||
`web_video_server` 的 URL 规则:`http://<host>:8081/stream?topic=<话题名>`
|
||||
|
||||
前端 `config.js` 中配置对应 URL:
|
||||
|
||||
```js
|
||||
// src/config.js
|
||||
export const MJPEG_AUTO_URL = '/stream?topic=/camera_auto'
|
||||
export const MJPEG_MANUAL_URL = '/stream?topic=/camera_manual'
|
||||
```
|
||||
|
||||
使用 `/stream` 相对路径时由 Nginx 反代到 `web_video_server`,避免跨域问题。
|
||||
|
||||
### 摄像头话题发布示例
|
||||
|
||||
```bash
|
||||
# 确保摄像头话题有数据
|
||||
ros2 run image_transport republish raw in:=/camera_auto/image_raw out:=/camera_auto/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 配置
|
||||
|
||||
部署前修改 `src/config.js` 中的地址以匹配实际服务器环境:
|
||||
|
||||
```js
|
||||
// 后端 HTTP API(模式切换)
|
||||
export const HTTP_API_BASE = '' // 同域代理可留空或填 '/api'
|
||||
|
||||
// rosbridge WebSocket
|
||||
export const ROSBRIDGE_URL = 'ws://192.168.1.100:9090' // 改成 ROS2 主机 IP
|
||||
|
||||
// MJPEG 视频流 — 经 Nginx 代理
|
||||
export const MJPEG_AUTO_URL = '/stream?topic=/camera_auto'
|
||||
export const MJPEG_MANUAL_URL = '/stream?topic=/camera_manual'
|
||||
|
||||
// ROS 话题 — 必须与 ROS2 侧一致
|
||||
export const CMD_WHEEL_TOPIC = '/cmd_wheel'
|
||||
export const CMD_GIMBAL_TOPIC = '/cmd_gimbal'
|
||||
export const PID_CONFIG_TOPIC = '/pid_config'
|
||||
|
||||
// 键盘控制参数 — 按实际机器人调整
|
||||
export const MAX_LINEAR = 0.5 // 最大线速度 m/s
|
||||
export const MAX_ANGULAR = 1.0 // 最大角速度 rad/s
|
||||
export const WHEEL_SEPARATION = 0.2 // 轮距 m
|
||||
export const GIMBAL_PITCH_SPEED = 0.5 // 云台 pitch 角速度 rad/s
|
||||
export const GIMBAL_YAW_SPEED = 0.5 // 云台 yaw 角速度 rad/s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、前端 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 服务是否实现并运行 |
|
||||
| 页面空白 | Nginx `try_files` 是否配了 `$uri /index.html` |
|
||||
@@ -0,0 +1,190 @@
|
||||
# 云台通信协议指南
|
||||
|
||||
## 串口配置
|
||||
|
||||
| 参数 | 值 |
|
||||
|---|---|
|
||||
| 波特率 | 115200 |
|
||||
| 数据位 | 8 |
|
||||
| 停止位 | 1 |
|
||||
| 校验位 | None |
|
||||
|
||||
## 帧格式总览
|
||||
|
||||
所有帧通过帧头 (SOF) 区分类型,checksum = 帧头到数据末尾前一字节的所有字节累加和取低 8 位。
|
||||
|
||||
| 帧头 | 长度 | 用途 |
|
||||
|---|---|---|
|
||||
| 0xA5 | 10 | 自动/视觉追踪数据 |
|
||||
| 0xA6 | 11 | 手动控制数据 |
|
||||
| 0xA7 | 3 | 命令 |
|
||||
| 0xA8 | 50 | 调试调参数据 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 自动/视觉追踪数据帧 0xA5 (10 字节)
|
||||
|
||||
**用途**: 视觉识别完成后持续发送跟踪误差。
|
||||
|
||||
```
|
||||
[SOF:0xA5][error_x:4][error_y:4][checksum:1]
|
||||
```
|
||||
|
||||
| 偏移 | 长度 | 类型 | 含义 |
|
||||
|---|---|---|---|
|
||||
| 0 | 1 | uint8 | 帧头 0xA5 |
|
||||
| 1 | 4 | float | 归一化 X 轴误差 |
|
||||
| 5 | 4 | float | 归一化 Y 轴误差 |
|
||||
| 9 | 1 | uint8 | checksum |
|
||||
|
||||
**Python 写入示例**:
|
||||
|
||||
```python
|
||||
import struct
|
||||
|
||||
def send_auto_tracking(ser, error_x: float, error_y: float):
|
||||
data = bytearray()
|
||||
data.append(0xA5)
|
||||
data.extend(struct.pack('<ff', error_x, error_y))
|
||||
checksum = sum(data) & 0xFF
|
||||
data.append(checksum)
|
||||
ser.write(data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 手动控制数据帧 0xA6 (11 字节)
|
||||
|
||||
**用途**: 手动模式下发送角度目标值。
|
||||
|
||||
```
|
||||
[SOF:0xA6][zero_force:1][yaw_ref:4][pitch_ref:4][checksum:1]
|
||||
```
|
||||
|
||||
| 偏移 | 长度 | 类型 | 含义 |
|
||||
|---|---|---|---|
|
||||
| 0 | 1 | uint8 | 帧头 0xA6 |
|
||||
| 1 | 1 | uint8 | 零力标志 (0=正常, 1=零力) |
|
||||
| 2 | 4 | float | Yaw 角度参考 (degrees) |
|
||||
| 6 | 4 | float | Pitch 角度参考 (degrees) |
|
||||
| 10 | 1 | uint8 | checksum |
|
||||
|
||||
**Python 写入示例**:
|
||||
|
||||
```python
|
||||
def send_manual_control(ser, yaw_deg: float, pitch_deg: float, zero_force: int = 0):
|
||||
data = bytearray()
|
||||
data.append(0xA6)
|
||||
data.append(0 if zero_force else 0)
|
||||
data.extend(struct.pack('<ff', yaw_deg, pitch_deg))
|
||||
checksum = sum(data) & 0xFF
|
||||
data.append(checksum)
|
||||
ser.write(data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 命令帧 0xA7 (3 字节)
|
||||
|
||||
**用途**: 切换模式和控制激光。
|
||||
|
||||
```
|
||||
[SOF:0xA7][cmd:1][checksum:1]
|
||||
```
|
||||
|
||||
| cmd 值 | 含义 |
|
||||
|---|---|
|
||||
| 0x01 | 切换到自动/视觉追踪模式 |
|
||||
| 0x02 | 切换到手动模式 |
|
||||
| 0x05 | 切换到调试模式 (失能所有电机) |
|
||||
| 0x03 | 激光开 |
|
||||
| 0x04 | 激光关 |
|
||||
|
||||
**Python 写入示例**:
|
||||
|
||||
```python
|
||||
CMD_AUTO = 0x01
|
||||
CMD_MANUAL = 0x02
|
||||
CMD_LASER_ON = 0x03
|
||||
CMD_LASER_OFF = 0x04
|
||||
CMD_DEBUG = 0x05
|
||||
|
||||
def send_command(ser, cmd: int):
|
||||
data = bytearray([0xA7, cmd])
|
||||
checksum = sum(data) & 0xFF
|
||||
data.append(checksum)
|
||||
ser.write(data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 调试调参帧 0xA8 (50 字节)
|
||||
|
||||
**用途**: 在调试模式下发送 PID 参数进行调参。发送后固件会立即生效。
|
||||
|
||||
```
|
||||
[SOF:0xA8]
|
||||
[vision_yaw_Kp:4][vision_yaw_Ki:4][vision_yaw_Kd:4]
|
||||
[vision_pitch_Kp:4][vision_pitch_Ki:4][vision_pitch_Kd:4]
|
||||
[motor_yaw_Kp:4][motor_yaw_Ki:4][motor_yaw_Kd:4]
|
||||
[motor_pitch_Kp:4][motor_pitch_Ki:4][motor_pitch_Kd:4]
|
||||
[checksum:1]
|
||||
```
|
||||
|
||||
| 偏移 | 长度 | 类型 | 含义 |
|
||||
|---|---|---|---|
|
||||
| 0 | 1 | uint8 | 帧头 0xA8 |
|
||||
| 1 | 4 | float | 视觉环 Yaw Kp |
|
||||
| 5 | 4 | float | 视觉环 Yaw Ki |
|
||||
| 9 | 4 | float | 视觉环 Yaw Kd |
|
||||
| 13 | 4 | float | 视觉环 Pitch Kp |
|
||||
| 17 | 4 | float | 视觉环 Pitch Ki |
|
||||
| 21 | 4 | float | 视觉环 Pitch Kd |
|
||||
| 25 | 4 | float | 电机角度环 Yaw Kp |
|
||||
| 29 | 4 | float | 电机角度环 Yaw Ki |
|
||||
| 33 | 4 | float | 电机角度环 Yaw Kd |
|
||||
| 37 | 4 | float | 电机角度环 Pitch Kp |
|
||||
| 41 | 4 | float | 电机角度环 Pitch Ki |
|
||||
| 45 | 4 | float | 电机角度环 Pitch Kd |
|
||||
| 49 | 1 | uint8 | checksum |
|
||||
|
||||
**Python 写入示例**:
|
||||
|
||||
```python
|
||||
def send_debug_tune(ser,
|
||||
v_yaw_kp, v_yaw_ki, v_yaw_kd,
|
||||
v_pitch_kp, v_pitch_ki, v_pitch_kd,
|
||||
m_yaw_kp, m_yaw_ki, m_yaw_kd,
|
||||
m_pitch_kp, m_pitch_ki, m_pitch_kd):
|
||||
data = bytearray()
|
||||
data.append(0xA8)
|
||||
data.extend(struct.pack('<fff', v_yaw_kp, v_yaw_ki, v_yaw_kd))
|
||||
data.extend(struct.pack('<fff', v_pitch_kp, v_pitch_ki, v_pitch_kd))
|
||||
data.extend(struct.pack('<fff', m_yaw_kp, m_yaw_ki, m_yaw_kd))
|
||||
data.extend(struct.pack('<fff', m_pitch_kp, m_pitch_ki, m_pitch_kd))
|
||||
checksum = sum(data) & 0xFF
|
||||
data.append(checksum)
|
||||
ser.write(data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 调试流程
|
||||
|
||||
1. 上位机发送命令帧 (0xA7, cmd=0x05) 切换到调试模式
|
||||
2. 固件收到后失能 yaw 和 pitch 电机,日志指示进入调试模式
|
||||
3. 通过 0xA8 帧写入 PID 参数,参数会**实时生效**:
|
||||
- 视觉环 PID → 直接写入选中的视觉 PID 实例
|
||||
- 电机角度环 PID → 通过话题推送到 gimbal 更新电机角度 PID
|
||||
4. 调试完毕后,发送命令帧 (0xA7, cmd=0x02 手动 或 0x01 自动) 切出调试模式,电机会自动使能
|
||||
|
||||
---
|
||||
|
||||
## 校验和计算
|
||||
|
||||
```python
|
||||
def calc_checksum(data: bytes) -> int:
|
||||
return sum(data) & 0xFF # 所有字节累加取低 8 位
|
||||
```
|
||||
|
||||
校验和覆盖范围:**帧头到数据末尾前一字节**的全部字节。接收端在收到数据后也应计算并比对校验和。
|
||||
Reference in New Issue
Block a user