Initial commit: M20 Gamepad control app

山猫 M20 四足机器人手柄控制 Android App,使用 UDP + JSON 协议控制机器人运动。

- 协议层:16 字节小端 Header + JSON ASDU 编解码
- 网络层:UDP 客户端,心跳 1Hz,断线检测 3s
- 连接状态机:空闲/连接中/已连接/断线 + 运动状态机:空闲/站立/RL 控制
- 虚拟摇杆:Compose 原生摇杆库,angle/power -> X/Y/Yaw 映射
- 视频播放:Media3 ExoPlayer RTSP,支持 AUTO/FORCE_HW/FORCE_SW_H264/FORCE_SW_H265
- 设置界面:机器人 IP/端口、RTSP 地址、编码器选择,DataStore 持久化
- 状态面板:异常列表、运控状态、设备温度、电池信息
- 控制按钮:模式/步态/照明/充电/休眠

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-07-17 19:24:39 +08:00
commit 04afb3331f
77 changed files with 5659 additions and 0 deletions
+612
View File
@@ -0,0 +1,612 @@
# 山猫 M20 basic_server 通信协议(UDP + JSON
本协议基于 TCP/UDP 传输层,本项目使用 **UDP + JSON** 模式。机器人本体为服务端,上位机(Android App)为客户端。
- **协议版本**: V1.2.0
- **更新日期**: 2026-05-18
- **服务端地址**: `10.21.31.103:30000` (UDP)
- **载荷编码**: JSON(协议头 `ASDU格式` 字段填 `0x01`
---
## 1. APDU 报文结构
```
+---------------------+------------------+
| Header (16 bytes) | ASDU (JSON 文本) |
+---------------------+------------------+
```
### 1.1 Header(固定 16 字节,小端 LE
| 偏移 | 长度 | 字段 | 值 | 说明 |
| ---: | ---: | --- | --- | --- |
| 0 | 1 | Sync0 | `0xEB` | 固定 |
| 1 | 1 | Sync1 | `0x91` | 固定 |
| 2 | 1 | Sync2 | `0xEB` | 固定 |
| 3 | 1 | Sync3 | `0x90` | 固定 |
| 4 | 2 | Length | u16 LE | ASDU 字节长度,最大 65535 |
| 6 | 2 | MsgId | u16 LE | 请求/响应配对;请求方从 0 递增,回绕 65535→0 |
| 8 | 1 | Format | `0x01` | JSONXML 为 `0x00` |
| 9 | 7 | Reserved | `0x00`×7 | 预留 |
### 1.2 ASDUJSON 格式)
固定外层包裹 `PatrolDevice`,包含 4 个通用字段:
| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `Type` | int | 消息类型(十进制) |
| `Command` | int | 命令码(十进制) |
| `Time` | string | 本地时区,格式 `YYYY-MM-DD HH:MM:SS` |
| `Items` | object | 参数项,视 Type/Command 而定 |
```json
{
"PatrolDevice": {
"Type": 100,
"Command": 100,
"Time": "2026-07-17 12:00:00",
"Items": {}
}
}
```
---
## 2. 控制类指令(上位机 → 机器人)
### 2.1 心跳指令
| Type | Command | 频率 |
| ---: | ---: | --- |
| 100 | 100 | ≥1 Hz |
`Items` 为空。服务端记录来源 IP/端口,并按需推送状态上报(见第 3 节)。
---
### 2.2 使用模式切换
| Type | Command | 说明 |
| ---: | ---: | --- |
| 1101 | 5 | 切换机器人使用模式 |
**Items**:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `Mode` | int | `0`=常规模式 · `1`=导航模式 · `2`=辅助模式 |
```json
{
"PatrolDevice": {
"Type": 1101, "Command": 5, "Time": "2026-07-17 12:00:00",
"Items": { "Mode": 0 }
}
}
```
- **常规模式**:支持轴指令(Type=2, Cmd=21
- **导航模式**:支持速度指令(Type=2, Cmd=25)和导航任务
---
### 2.3 运动状态转换
| Type | Command | 说明 |
| ---: | ---: | --- |
| 2 | 22 | 机器人运动状态转换 |
**Items**:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `MotionParam` | int | `0`=空闲 · `1`=站立 · `2`=关节阻尼/软急停 · `3`=开机阻尼 · `4`=趴下 · `17`=RL控制 |
```json
{
"PatrolDevice": {
"Type": 2, "Command": 22, "Time": "2026-07-17 12:00:00",
"Items": { "MotionParam": 0 }
}
}
```
状态流转:`空闲 → 站立 → RL控制` 为正向流程。RL 控制状态下才能下发步态切换和运动指令。
---
### 2.4 步态切换
| Type | Command | 说明 |
| ---: | ---: | --- |
| 2 | 23 | 切换步态(需在 RL 控制下) |
**Items**:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `GaitParam` | int | 见下表 |
| 值 | 步态 | 适用模式 |
| ---: | --- | --- |
| `0x1001` | 基础(标准运动模式) | 常规/辅助/导航 |
| `0x1003` | 楼梯(标准运动模式) | 常规/辅助/导航 |
| `0x3002` | 平地(敏捷运动模式) | 辅助/导航 |
| `0x3003` | 楼梯(敏捷运动模式) | 辅助/导航 |
```json
{
"PatrolDevice": {
"Type": 2, "Command": 23, "Time": "2026-07-17 12:00:00",
"Items": { "GaitParam": 4097 }
}
}
```
- 敏捷运动模式速度响应更好,适合自主算法开发
- 每次起立后或切换使用模式时,步态重置为基础步态 `0x1001`
- 步态切换仅支持在 **RL 控制** 状态下执行
---
### 2.5 运动控制(轴指令 — 常规模式)
| Type | Command | 说明 |
| ---: | ---: | --- |
| 2 | 21 | 速度比例控制,仅常规模式 |
**Items** (全部 float, 范围 [-1, 1]):
| 参数 | 含义 | 说明 |
| --- | --- | --- |
| `X` | 前后方向速度比例 | 正=前进,负=后退 |
| `Y` | 左右方向速度比例 | 正=左移,负=右移 |
| `Z` | 高度方向速度比例 | 正=向上,负=向下 |
| `Roll` | 翻滚角速度比例 | |
| `Pitch` | 俯仰角速度比例 | |
| `Yaw` | 偏航角速度比例 | 正=逆时针,负=顺时针 |
```json
{
"PatrolDevice": {
"Type": 2, "Command": 21, "Time": "2026-07-17 12:00:00",
"Items": { "X": 0.5, "Y": 0.0, "Z": 0.0, "Roll": 0.0, "Pitch": 0.0, "Yaw": 0.0 }
}
}
```
- 建议发送频率 ≥20 Hz
- 常规模式的基础/楼梯步态仅 `X`, `Y`, `Yaw` 生效
---
### 2.6 运动控制(速度指令 — 导航模式)
| Type | Command | 说明 |
| ---: | ---: | --- |
| 2 | 25 | 速度绝对值控制,仅导航模式 |
**Items** (全部 float, 带物理单位):
| 参数 | 含义 | 单位 |
| --- | --- | --- |
| `X` | 前后方向速度 | m/s |
| `Y` | 左右方向速度 | m/s |
| `Z` | 高度方向速度 | m/s |
| `Roll` | 翻滚角速度 | rad/s |
| `Pitch` | 俯仰角速度 | rad/s |
| `Yaw` | 偏航角速度 | rad/s |
```json
{
"PatrolDevice": {
"Type": 2, "Command": 25, "Time": "2026-07-17 12:00:00",
"Items": { "X": 0.5, "Y": 0.0, "Z": 0.0, "Roll": 0.0, "Pitch": 0.0, "Yaw": 0.0 }
}
}
```
---
### 2.7 照明灯控制
| Type | Command | 说明 |
| ---: | ---: | --- |
| 1101 | 2 | 照明灯开关 |
**Items**:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `Front` | int | `0`=关闭 · `1`=开启 |
| `Back` | int | `0`=关闭 · `1`=开启 |
```json
{
"PatrolDevice": {
"Type": 1101, "Command": 2, "Time": "2026-07-17 12:00:00",
"Items": { "Front": 1, "Back": 0 }
}
}
```
---
### 2.8 自主充电
| Type | Command | 说明 |
| ---: | ---: | --- |
| 2 | 24 | 自主充电控制 |
**Items**:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `Charge` | int | `0`=结束充电 · `1`=开始充电 · `2`=清除充电状态(强制归零,谨慎使用) |
```json
{
"PatrolDevice": {
"Type": 2, "Command": 24, "Time": "2026-07-17 12:00:00",
"Items": { "Charge": 1 }
}
}
```
---
### 2.9 休眠模式设置
| Type | Command | 说明 |
| ---: | ---: | --- |
| 1101 | 6 | 设置休眠模式 |
**Items**:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `Sleep` | bool | `true`=进入休眠 · `false`=唤醒 |
| `Auto` | bool | `true`=启用自动休眠 · `false`=关闭 |
| `Time` | uint | 自动休眠等待时间(分钟),范围 [5, 30] |
```json
{
"PatrolDevice": {
"Type": 1101, "Command": 6, "Time": "2026-07-17 12:00:00",
"Items": { "Sleep": false, "Auto": true, "Time": 5 }
}
}
```
---
### 2.10 休眠状态查询
| Type | Command | 说明 |
| ---: | ---: | --- |
| 1101 | 7 | 查询休眠状态及设置 |
`Items` 为空。响应同 2.9 的 Items 结构(`Sleep`, `Auto`, `Time`)。
---
## 3. 状态上报(机器人 → 上位机)
UDP 下需先发一次对应查询请求触发订阅,服务端记录来源 IP/端口后持续推送。
### 3.1 基础状态上报 — Type=1002, Cmd=6
**频率**: 2 Hz
```json
{
"PatrolDevice": {
"Type": 1002, "Command": 6, "Time": "2026-07-17 12:00:00",
"Items": {
"BasicStatus": {
"MotionState": 0,
"Gait": 0,
"Charge": 0,
"HES": 0,
"ControlUsageMode": 0,
"Direction": 0,
"OOA": 0,
"PowerManagement": 0,
"Sleep": 0,
"Version": "STD"
}
}
}
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `MotionState` | int | `0`=空闲 · `1`=站立 · `2`=软急停 · `3`=开机阻尼 · `4`=趴下 · `17`=RL控制 |
| `Gait` | int | `0x1001`=基础 · `0x1003`=楼梯 · `0x3002`=平地敏捷 · `0x3003`=楼梯敏捷 |
| `Charge` | int | `0`=空闲 · `1`=前往充电桩 · `2`=充电中 · `3`=退出充电桩 · `4`=异常 · `5`=在桩上未充电 |
| `HES` | int | `0`=未触发 · `1`=已触发(硬急停) |
| `ControlUsageMode` | int | `0`=常规 · `1`=导航 · `2`=辅助 |
| `Direction` | int | `0`=正向为前进正方向 · `1`=后向为前进正方向 |
| `OOA` | int | `0`=未启动 · `1`=空闲中 · `2`=未触发避障 · `3`=主动避障中 |
| `PowerManagement` | int | `0`=常规 · `1`=单电池模式 |
| `Sleep` | int | `0`=未休眠 · `1`=已休眠 · `2`=进入休眠中 |
| `Version` | string | `STD`=山猫M20 · `PRO`=山猫M20 Pro |
---
### 3.2 运控状态上报 — Type=1002, Cmd=4
**频率**: 10 Hz
```json
{
"PatrolDevice": {
"Type": 1002, "Command": 4, "Time": "2026-07-17 12:00:00",
"Items": {
"MotionStatus": {
"Roll": 0.0, "Pitch": 0.0, "Yaw": 0.0,
"OmegaZ": 0.0, "LinearX": 0.0, "LinearY": 0.0,
"Height": 0.0, "Payload": 0.0, "RemainMile": 0.0
},
"MotorStatus": {
"Joint": [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],
"LeftFrontHipX": 0.0, "LeftFrontHipY": 0.0, "LeftFrontKnee": 0.0, "LeftFrontWheel": 0.0,
"RightFrontHipX": 0.0, "RightFrontHipY": 0.0, "RightFrontKnee": 0.0, "RightFrontWheel": 0.0,
"LeftBackHipX": 0.0, "LeftBackHipY": 0.0, "LeftBackKnee": 0.0, "LeftBackWheel": 0.0,
"RightBackHipX": 0.0, "RightBackHipY": 0.0, "RightBackKnee": 0.0, "RightBackWheel": 0.0
}
}
}
}
```
**MotionStatus**:
| 字段 | 单位 | 说明 |
| --- | --- | --- |
| `Roll/Pitch/Yaw` | rad | 机身姿态角 |
| `OmegaZ` | rad/s | Z 方向角速度 |
| `LinearX/LinearY` | m/s | X/Y 方向线速度 |
| `Height` | m | 机身高度 |
| `RemainMile` | km | 预计剩余续航里程 |
**MotorStatus** — 16 关节顺序:LeftFrontHipX, HipY, Knee, Wheel → RightFrontHipX, HipY, Knee, Wheel → LeftBack... → RightBack...
| 字段 | 单位 | 说明 |
| --- | --- | --- |
| `Joint` | rad / rad/s | 16 元素数组,关节角度/转速 |
| `*HipX` | rad | 侧摆关节角度 |
| `*HipY` | rad | 髋关节角度 |
| `*Knee` | rad | 膝关节角度 |
| `*Wheel` | rad/s | 足轮转速 |
---
### 3.3 设备状态上报 — Type=1002, Cmd=5
**频率**: 2 Hz。包含 BatteryList, BatteryStatus, DeviceTemperature, Led, GPS, DevEnable, CPU 七组参数。
**BatteryList** (2 块电池):
```json
"BatteryList": [
{ "Voltage": 0.0, "BatteryLevel": 100.0, "battery_temperature": 0.0, "charge": false, "serial": "xxx" },
{ "Voltage": 0.0, "BatteryLevel": 100.0, "battery_temperature": 0.0, "charge": false, "serial": "xxx" }
]
```
**BatteryStatus**:
```json
"BatteryStatus": {
"VoltageLeft": 0.0, "VoltageRight": 0.0,
"BatteryLevelLeft": 0.0, "BatteryLevelRight": 0.0,
"battery_temperatureLeft": 0.0, "battery_temperatureRight": 0.0,
"chargeLeft": false, "chargeRight": false
}
```
**DeviceTemperature** — 16 电机 + 16 驱动器温度:
```json
"DeviceTemperature": {
"Motor": [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],
"Driver": [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],
"LeftFrontHipXMotor": 0.0, "LeftFrontHipXDriver": 0.0, ...
}
```
**Led**:
```json
"Led": { "Fill": { "Front": 1, "Back": 1 } }
```
**GPS**:
```json
"GPS": {
"Latitude": 0.0, "Longitude": 0.0, "Speed": 0.0, "Course": 0.0,
"FixQuality": 0, "NumSatellites": 0, "Altitude": 0.0,
"HDOP": 0.0, "VDOP": 0.0, "PDOP": 0.0, "VisibleSatellites": 0
}
```
**DevEnable**:
```json
"DevEnable": {
"FanSpeed": 100, "LoadPower": 1, "LedHost": 1, "LedExt": 1, "FP": 1,
"Lidar": { "Front": 1, "Back": 1 },
"GPS": 1,
"Video": { "Front": 1, "Back": 1 }
}
```
**CPU**:
```json
"CPU": {
"AOS": { "Temperature": 0.0, "FrequencyInt": 0.0, "FrequencyApp": 0.0 },
"NOS": { "Temperature": 0.0, "FrequencyInt": 0.0, "FrequencyApp": 0.0 },
"GOS": { "Temperature": 0.0, "FrequencyInt": 0.0, "FrequencyApp": 0.0 }
}
```
---
### 3.4 异常状态上报 — Type=1002, Cmd=3
**频率**: 2 Hz,状态变更时即时上报。
```json
{
"PatrolDevice": {
"Type": 1002, "Command": 3, "Time": "2026-07-17 12:00:00",
"Items": {
"ErrorList": [
{ "errorCode": 32769, "component": 1 },
{ "errorCode": 32770, "component": 4 }
]
}
}
}
```
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `errorCode` | int | 错误码,见下文 |
| `component` | int | bit 位标识部件编号(关节/电池) |
`component` 关节位编号(自低位起):FL HipX(0) → FL HipY(1) → FL Knee(2) → FL Wheel(3) → FR HipX(4) → ... → RB Wheel(15)。电池位编号:bit0=后侧电池,bit1=前侧电池。
常见错误码(部分):
| errorCode | 含义 | errorCode | 含义 |
| ---: | --- | ---: | --- |
| `0x8001` | 电机温度预警 | `0x8002` | 电机温度过高保护 |
| `0x8003` | 电机温度致命截止 | `0x8007` | 关节驱动器过温 |
| `0x8008` | 驱动器欠压保护 | `0x8009` | 驱动器过压保护 |
| `0x8012` | 与关节驱动器通讯超时 | `0x8016` | 编码器无值 |
| `0x8020` | 驱动器过流保护 | `0x8102` | 低电量预警 |
| `0x8103` | 保护电量 | `0x8107` | 电池放电过温保护 |
| `0x8115` | 电池充电过温保护 | `0x8117` | 电池单体过压保护 |
| `0x8201` | CPU占用率过高预警 | `0x8202` | CPU温度过高预警 |
| `0x8211` | CPU占用率过高保护 | `0x8212` | CPU温度过高保护 |
| `0x8501` | 自主充电定位超时 | `0x8503` | 自主充电定位异常 |
| `0x8506` | 充电桩无电流 | `0x8507` | 充电停障错误 |
| `0x8510` | 退桩超时 | `0x8022` | 关节角超限 |
| `0x8027` | 机身姿态错误 | `0x8029` | 运动姿态错误 |
---
## 4. 通用响应与错误码
每一条请求都会收到同 Type/Command 的响应,Items 至少包含 ErrorCode 和 ErrorMessage
```json
{
"PatrolDevice": {
"Type": 100, "Command": 100, "Time": "2026-07-17 12:00:00",
"Items": { "ErrorCode": 0, "ErrorMessage": "Success" }
}
}
```
| ErrorCode (hex) | dec | ErrorMessage | 含义 |
| --- | ---: | --- | --- |
| `0x0000` | 0 | Success | 命令已接收并触发(不代表业务成功) |
| `0xE001` | 57345 | Unsupported data format | 帧头 Format 非 JSON/XML |
| `0xE002` | 57346 | Data parsing failed | 缺少 PatrolDevice/Items/Type/Command |
| `0xE003` | 57347 | Unsupported protocol type | Type/Command 组合不存在 |
| `0xE004` | 57348 | Missing necessary fields | Items 缺字段 |
| `0xE005` | 57349 | Mismatched data type | 字段类型不匹配 |
| `0xE006` | 57350 | Mismatched request client | 2s 内轴指令须来自同一客户端 |
| `0xE007` | 57351 | No operation permission | 无操作权限 |
| `0xE008` | 57352 | Not allowed operation | 状态机不允许 |
| `0xE009` | 57353 | Operation failure | 操作失败,查 Cmd=3 错误详情 |
| `0xE00A` | 57354 | Unsupported function | 固件不支持(如导航需 Pro) |
| `0xE00B` | 57355 | Internal error | 内部错误,查日志 |
---
## 5. 导航任务(Pro 版本)
以下指令仅山猫 M20 Pro 支持。
### 5.1 下发单点导航 — Type=1003, Cmd=1
**Items**:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `Value` | int | 目标点编号(默认 0) |
| `MapID` | int | 地图编号(默认 0) |
| `PosX/Y/Z` | float | 目标点在地图坐标系中的位置 (m) |
| `AngleYaw` | float | 目标点朝向 (rad) |
| `PointInfo` | int | `0`=过渡点 · `1`=任务点 · `3`=充电点 |
| `Gait` | int | `0x3002`=平地敏捷 · `0x3003`=楼梯敏捷 |
| `Speed` | int | `0`=正常 · `1`=低速 · `2`=高速 |
| `Manner` | int | `0`=前进行走 · `1`=倒退行走 |
| `ObsMode` | int | `0`=开启停避障 · `1`=关闭 |
| `NavMode` | int | `0`=直线导航 · `1`=自主导航 |
### 5.2 取消导航 — Type=1004, Cmd=1
`Items` 为空。响应 `ErrorCode`: `0`=成功, `1`=失败。
### 5.3 查询导航任务状态 — Type=1007, Cmd=1
`Items` 为空。响应包含 `Value`, `Status`, `ErrorCode`
`Status`: `0`=空闲 · `1`=退出充电桩中 · `2`=导航预处理 · `3`=导航中 · `4`=导航完成 · `5`=进入充电桩中 · `0xFF`=暂停中
### 5.4 获取地图位置 — Type=1007, Cmd=2
**响应 Items**:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `Location` | int | `0`=定位正常 · `1`=定位丢失 |
| `PosX/Y/Z` | float | 地图坐标 (m) |
| `Roll/Pitch/Yaw` | float | 姿态角 (rad) |
### 5.5 初始化定位 — Type=2101, Cmd=1
**Items**:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `PosX/Y/Z` | float | 重新定位坐标 (m) |
| `Yaw` | float | 绕 Z 轴姿态角 (rad) |
---
## 6. 手柄 App 接口清单
| 用途 | Type | Cmd | 方向 | 频率 |
| --- | ---: | ---: | --- | --- |
| 心跳保活 | 100 | 100 | 上位机→机器人 | ≥1 Hz |
| 使用模式切换 | 1101 | 5 | 上位机→机器人 | 事件 |
| 运动状态转换 | 2 | 22 | 上位机→机器人 | 事件 |
| 步态切换 | 2 | 23 | 上位机→机器人 | 事件 |
| 轴指令(常规模式) | 2 | 21 | 上位机→机器人 | ≥20 Hz |
| 速度指令(导航模式) | 2 | 25 | 上位机→机器人 | ≥10 Hz |
| 照明灯控制 | 1101 | 2 | 上位机→机器人 | 事件 |
| 自主充电 | 2 | 24 | 上位机→机器人 | 事件 |
| 休眠设置 | 1101 | 6 | 上位机→机器人 | 事件 |
| 订阅基础状态 | 1002 | 6 | 双向 | 2 Hz 推送 |
| 订阅运控状态 | 1002 | 4 | 双向 | 10 Hz 推送 |
| 订阅设备状态 | 1002 | 5 | 双向 | 2 Hz 推送 |
| 订阅异常状态 | 1002 | 3 | 双向 | 2 Hz / 即时 |
---
## 7. 实现要点
1. UDP socket 绑定本地随机端口(源端口即为上报回执地址)。
2. 心跳线程 1 Hz,发 `Type=100, Cmd=100`
3. 速度/轴指令线程 ≥20 Hz(无输入时发零速度保持连接)。
4. 接收线程:解析 16 字节 header → 按 `Length` 读 JSON 字节 → 分发 handler。
5. 请求 MsgId 从 0 递增,用于匹配响应帧。
6. 首次连接时主动发 `1002/4`, `1002/5`, `1002/6`, `1002/3` 触发 UDP 订阅推送。
7. 断线判定:>3 s 无任何 UDP 包 → 标记离线并 UI 提示。
8. 轴指令 `X/Y/Yaw` 比例 [-1,1] 需映射到手柄摇杆范围。
9. 步态切换仅 RL 控制状态下有效,需先发 `MotionParam=17`
10. 导航模式切换后需等待 `BasicStatus.ControlUsageMode` 确认。