Files
hexone2086 04afb3331f 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>
2026-07-17 19:24:39 +08:00

612 lines
18 KiB
Markdown
Raw Permalink 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.
# 山猫 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` 确认。