docs: 文档整理,与前端对接文档.md 替换为 API.md + front-end_ROS.md

- 新增 docs/API.md(HTTP API 数据格式)
- 新增 docs/front-end_ROS.md(前端 ROS 话题格式)
- 新增 docs/底盘通信协议指南.md(底盘串口协议)
- 删除 docs/与前端对接文档.md
- 云台通信协议指南补充 0xA9 前馈帧说明
- 修正 README API 路径及 ROS 话题引用
- .gitignore 添加 workspace/data/
This commit is contained in:
dev01
2026-06-07 13:48:59 +08:00
parent 74c470a8fa
commit 0d40e0fabf
7 changed files with 414 additions and 573 deletions
+105
View File
@@ -0,0 +1,105 @@
# HTTP API 数据格式
Base URL: `http://192.168.0.11`
所有 POST 请求 Content-Type 为 `application/json`。
---
## 1. 云台模式切换
```
POST /api/gimbal/mode/switch
```
**请求体:**
```json
{ "mode": "auto" }
```
`mode` 取值: `"auto"` | `"manual"` | `"debug"`
---
## 2. 底盘模式切换
```
POST /api/chassis/mode/switch
```
**请求体:**
```json
{ "mode": "auto" }
```
`mode` 取值: `"auto"` | `"manual"` | `"debug"`
---
## 3. 激光控制
```
POST /api/laser
```
**请求体:**
```json
{ "action": "on" }
```
`action` 取值: `"on"` | `"off"`
---
## 4. 云台 PID 配置
```
GET /api/gimbal/pidconfig (获取)
POST /api/gimbal/pidconfig (保存)
```
**GET 响应 / POST 请求体:**
```json
{
"visual_yaw": { "kp": 0.0, "ki": 0.0, "kd": 0.0 },
"visual_pitch": { "kp": 0.0, "ki": 0.0, "kd": 0.0 },
"angle_yaw": { "kp": 0.0, "ki": 0.0, "kd": 0.0 },
"angle_pitch": { "kp": 0.0, "ki": 0.0, "kd": 0.0 }
}
```
| 字段 | 对应 PID 环 |
|------|------------|
| `visual_yaw` | 视觉环 Yaw 电机 |
| `visual_pitch` | 视觉环 Pitch 电机 |
| `angle_yaw` | 角度环 Yaw 电机 |
| `angle_pitch` | 角度环 Pitch 电机 |
---
## 5. 底盘 PID 配置
```
GET /api/chassis/pidconfig (获取)
POST /api/chassis/pidconfig (保存)
```
**GET 响应 / POST 请求体:**
```json
{
"speed": { "kp": 0.0, "ki": 0.0, "kd": 0.0 },
"angle": { "kp": 0.0, "ki": 0.0, "kd": 0.0 },
"position": { "kp": 0.0, "ki": 0.0, "kd": 0.0 }
}
```
| 字段 | 对应 PID 环 |
|------|------------|
| `speed` | 速度环 |
| `angle` | 角度环 |
| `position` | 位置环 |
+55
View File
@@ -0,0 +1,55 @@
# ROS 话题格式
通过 rosbridge WebSocket(`ws://192.168.0.11:9090`)发布,均为单向 `publish`(前端 → ROS),无订阅。
---
## 1. `/cmd_chassis` — 底盘控制
**类型:** `std_msgs/Float32MultiArray`
**格式:**
```json
{
"layout": { "dim": [], "data_offset": 0 },
"data": [0.5, 0.0]
}
```
`data` 字段:
| 索引 | 含义 | 单位 | 来源常量 |
|------|------|------|---------|
| `data[0]` | 线速度 | m/s | `BASE_SPEED` (0.5) |
| `data[1]` | 角速度(偏航) | 度/秒 | `YAW_VELOCITY` (1.0) |
---
## 2. `/cmd_gimbal` — 云台控制
**类型:** `geometry_msgs/Twist`
**格式:**
```json
{
"linear": { "x": 0, "y": 0, "z": 0 },
"angular": { "x": 0, "y": 10.0, "z": 0 }
}
```
`angular` 字段:
| 字段 | 含义 | 单位 | 来源常量 |
|------|------|------|---------|
| `angular.y` | 俯仰角速度 (pitch) | 度/秒 | `GIMBAL_PITCH_DEG_S` (10) |
| `angular.z` | 偏航角速度 (yaw) | 度/秒 | `GIMBAL_YAW_DEG_S` (10) |
`linear` 字段保留填零,云台无平移运动。
---
## 3. 发布频率
发布频率常量 `PUBLISH_RATE = 30` Hz,但在 ManualMode 中实际采用 on-demand 模式:仅当键盘按住、且速度值有变化时才发布。
-571
View File
@@ -1,571 +0,0 @@
# 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` 是否正常释放 |
+29
View File
@@ -19,6 +19,35 @@
| 0xA6 | 10 | 手动控制数据 |
| 0xA7 | 3 | 命令 |
| 0xA8 | 50 | 调试调参数据 |
| 0xA9 | 6 | 前馈输入 |
---
## 5. 前馈输入帧 0xA9 (6 字节)
**用途**: 下发前馈输入值到 yaw 电机前馈控制器,在自动和手动模式下均可生效。
```
[SOF:0xA9][feedforward:4][checksum:1]
```
| 偏移 | 长度 | 类型 | 含义 |
|---|---|---|---|
| 0 | 1 | uint8 | 帧头 0xA9 |
| 1 | 4 | float | 前馈输入值 |
| 5 | 1 | uint8 | checksum |
**Python 写入示例**:
```python
def send_feedforward(ser, feedforward: float):
data = bytearray()
data.append(0xA9)
data.extend(struct.pack('<f', feedforward))
checksum = sum(data) & 0xFF
data.append(checksum)
ser.write(data)
```
---
+222
View File
@@ -0,0 +1,222 @@
# 串口通信协议
## 概述
- **物理层**: UART2,引脚 TX=B15 / RX=B16
- **波特率**: 115200 bps
- **数据位**: 8,无校验,1 停止位(默认 UART 配置)
- **字节序**: 多字节数值均采用**小端序 (little-endian)**
- **校验方式**: 累加和校验(checksum = 帧内校验位之前所有字节之和,截断为 uint8_t)
- **软件 FIFO**: 16 字节环形缓冲区,UART RX 中断中写入,主循环 `command_task()` 中解析
## 工作模式
系统有三种工作模式,通过命令帧切换:
| 模式 | 枚举值 | 说明 | 使能的控制环 |
|---|---|---|---|
| 自动模式 | `COMMAND_MODE_AUTO` (0) | 视觉追踪 / 全闭环 | 电机速度环 + 角度环 + 位置环 |
| 手动模式 | `COMMAND_MODE_MANUAL` (1) | 手动遥控 | 电机速度环 + 角度环 |
| 调试模式 | `COMMAND_MODE_DEBUG` (2) | PID 参数整定 | 无(所有控制环关闭) |
---
## 数据帧定义
### 1. 命令帧 `0xA7`(3 字节)
切换工作模式。
```
┌────────┬───────┬──────────┐
│ SOF │ cmd │ checksum │
│ 0xA7 │ 1 B │ 1 B │
└────────┴───────┴──────────┘
```
| 偏移 | 长度 | 字段 | 类型 | 说明 |
|---|---|---|---|---|
| 0 | 1 | SOF | uint8_t | 帧头,固定值 `0xA7` |
| 1 | 1 | cmd | uint8_t | 命令码 |
| 2 | 1 | checksum | uint8_t | `(0xA7 + cmd) & 0xFF` |
**cmd 取值:**
| 值 | 功能 | 对应函数 |
|---|---|---|
| `0x01` | 切换到自动模式 | `SwitchToAutoMode()` |
| `0x02` | 切换到手动模式 | `SwitchToManualMode()` |
| `0x03` | 切换到调试模式 | `SwitchToDebugMode()` |
**示例(切换手动模式):** `A7 02 A9`
---
### 2. 手动控制帧 `0xA6`(8 字节)
仅在**手动模式**下生效,用于实时控制底盘运动。
```
┌────────┬────────────────────┬─────────────────────┬──────────┐
│ SOF │ motor_speed_base │ angle_velocity │ checksum │
│ 0xA6 │ int16_t (2 B) │ float (4 B) │ 1 B │
└────────┴────────────────────┴─────────────────────┴──────────┘
```
| 偏移 | 长度 | 字段 | 类型 | 说明 |
|---|---|---|---|---|
| 0 | 1 | SOF | uint8_t | 帧头,固定值 `0xA6` |
| 1 | 2 | motor_speed_base | int16_t (LE) | 基础直行速度,范围 -50~+50 |
| 3 | 4 | angle_velocity | float (LE) | 角度速率,单位 °/s |
| 7 | 1 | checksum | uint8_t | `(0xA6 + 前 6 字节之和) & 0xFF` |
**控制算法:**
```
motor_speed_left_ref = motor_speed_base - speed_dif
motor_speed_right_ref = motor_speed_base + speed_dif
```
其中 `speed_dif` 由角度环 PID 根据 `yaw_ref` 与当前 `imu_angle.yaw` 计算得出。手动模式下 `yaw_ref` 以 `angle_velocity` 速率持续累加:
```
yaw_ref += angle_velocity * dt (dt = 0.02s)
```
---
### 3. 调试调参帧 `0xA8`(38 字节)
仅在**调试模式**下生效,一次性更新所有 PID 参数(Kp / Ki / Kd)。
```
┌────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ SOF │ Kp_speed │ Ki_speed │ Kd_speed │ Kp_yaw │ Ki_yaw │ Kd_yaw │ Kp_pos │ Ki_pos │ Kd_pos │ checksum │
│ 0xA8 │ float(4) │ float(4) │ float(4) │ float(4) │ float(4) │ float(4) │ float(4) │ float(4) │ float(4) │ 1 B │
└────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
```
| 偏移 | 长度 | 字段 | 类型 | 说明 |
|---|---|---|---|---|
| 0 | 1 | SOF | uint8_t | 帧头,固定值 `0xA8` |
| 1 | 4 | Kp_speed | float (LE) | 电机速度环比例系数(左右共用) |
| 5 | 4 | Ki_speed | float (LE) | 电机速度环积分系数 |
| 9 | 4 | Kd_speed | float (LE) | 电机速度环微分系数 |
| 13 | 4 | Kp_yaw | float (LE) | 角度环比例系数 |
| 17 | 4 | Ki_yaw | float (LE) | 角度环积分系数 |
| 21 | 4 | Kd_yaw | float (LE) | 角度环微分系数 |
| 25 | 4 | Kp_position | float (LE) | 位置环比例系数 |
| 29 | 4 | Ki_position | float (LE) | 位置环积分系数 |
| 33 | 4 | Kd_position | float (LE) | 位置环微分系数 |
| 37 | 1 | checksum | uint8_t | `(0xA8 + 前 36 字节之和) & 0xFF` |
**各环默认参数:**
| 控制环 | Kp | Ki | Kd | 输出限幅 | 采样周期 | 优化环节 |
|---|---|---|---|---|---|---|
| 电机速度环(左/右) | 300.0 | 150.0 | 0.0 | ±3000 (MOTOR_MAX) | 10ms | — |
| 角度环 | 1.0 | 0.0 | 0.0 | ±25 (°/s) | 20ms | 偏航角误差修正 |
| 位置环 | 0.5 | 0.0 | 0.0 | ±90 (°/s) | 20ms | — |
> **注意**:调试模式下所有控制任务已关闭,电机停转。修改参数后需切换到自动或手动模式才能观察效果。修改的参数直接写入 PID 实例的 Kp/Ki/Kd 字段,不清除积分等运行时状态。
---
### 4. 角度速率前馈帧 `0xA9`(6 字节)— MCU → PC
由 MCU 经 DMA 主动发送,上报位置环计算的角度速率,供云台等设备做前馈控制。
```
┌────────┬─────────────────────┬──────────┐
│ SOF │ angle_velocity │ checksum │
│ 0xA9 │ float (4 B) │ 1 B │
└────────┴─────────────────────┴──────────┘
```
| 偏移 | 长度 | 字段 | 类型 | 说明 |
|---|---|---|---|---|
| 0 | 1 | SOF | uint8_t | 帧头,固定值 `0xA9` |
| 1 | 4 | angle_velocity | float (LE) | 角度速率,单位 °/s |
| 5 | 1 | checksum | uint8_t | `(0xA9 + 前 4 字节之和) & 0xFF` |
**发送触发条件**:位置环控制任务使能时,每 20ms(50Hz)在 `position_control_task()` 中调用 `command_send_angle_velocity()`。
---
## 通信流程
```
┌──────────┐ ┌──────────┐
│ PC │ ──── 0xA7 cmd ────────→ │ MSPM0 │
│ (上位机) │ ←─── 0xA9 feedback ──── │ (MCU) │
│ │ ──── 0xA6 manual ─────→ │ │
│ │ ──── 0xA8 debug ──────→ │ │
└──────────┘ └──────────┘
```
**典型操作流程:**
1. **上电默认**:自动模式,位置环+角度环+速度环全部使能
2. **切换到手动模式**:发送 `A7 02 A9`,然后周期发送 `0xA6` 控制帧驱动底盘
3. **PID 调参**:发送 `A7 03 AA` 进入调试模式(电机停转),发送 `0xA8` 调参帧更新参数,再发 `A7 01 A8` 或 `A7 02 A9` 切换回运行模式观察效果
## 模式切换副作用
每次切换模式时,系统会执行以下操作:
| 操作 | 自动模式 | 手动模式 | 调试模式 |
|---|---|---|---|
| 设置 `current_mode` | AUTO (0) | MANUAL (1) | DEBUG (2) |
| 清除 PID 运行时状态 | 全部 4 个 PID | 速度环+角度环 | — |
| `yaw_ref` | = 当前 `imu_angle.yaw` | = 当前 `imu_angle.yaw` | — |
| `angle_velocity` | 归零 | 归零 | — |
| `motor_speed_ref` | 归零 | 归零 | — |
| `motor_speed_base` | = 10 (AUTO_MODE_BASE_SPEED) | 归零 | — |
| 电机 PWM | 归零 | 归零 | 归零 |
| 速度环使能 | ✅ | ✅ | ❌ |
| 角度环使能 | ✅ | ✅ | ❌ |
| 位置环使能 | ✅ | ❌ | ❌ |
| 蜂鸣器 | 100ms | 200ms | 300ms |
## C 语言帧构造示例
```c
#include <stdint.h>
#include <string.h>
// --- 构造 0xA7 命令帧 ---
void build_cmd_frame(uint8_t *buf, uint8_t cmd) {
buf[0] = 0xA7;
buf[1] = cmd;
buf[2] = (uint8_t)(0xA7 + cmd);
// 发送 buf, 长度 3
}
// --- 构造 0xA6 手动控制帧 ---
void build_manual_frame(uint8_t *buf, int16_t speed, float yaw_rate) {
buf[0] = 0xA6;
memcpy(&buf[1], &speed, 2); // int16_t, 小端
memcpy(&buf[3], &yaw_rate, 4); // float, 小端
uint8_t sum = 0xA6;
for (int i = 1; i < 7; i++) sum += buf[i];
buf[7] = sum;
// 发送 buf, 长度 8
}
// --- 构造 0xA8 调试调参帧 ---
void build_debug_frame(uint8_t *buf, float kp_s, float ki_s, float kd_s,
float kp_y, float ki_y, float kd_y,
float kp_p, float ki_p, float kd_p) {
buf[0] = 0xA8;
memcpy(&buf[1], &kp_s, 4);
memcpy(&buf[5], &ki_s, 4);
memcpy(&buf[9], &kd_s, 4);
memcpy(&buf[13], &kp_y, 4);
memcpy(&buf[17], &ki_y, 4);
memcpy(&buf[21], &kd_y, 4);
memcpy(&buf[25], &kp_p, 4);
memcpy(&buf[29], &ki_p, 4);
memcpy(&buf[33], &kd_p, 4);
uint8_t sum = 0xA8;
for (int i = 1; i < 37; i++) sum += buf[i];
buf[37] = sum;
// 发送 buf, 长度 38
}
```