Files
m20_gamepad/AGENTS.md
T
hexone2086 9b8cd09929 feat: 右上角添加软急停滑块(平行四边形风格)
新增 EmergencyStopSlider 组件,水平滑到右侧触发软急停指令,
滑到左侧仅 UI 复位,等待机器人状态上报自动归位。非拖动时
跟随 motionState 状态同步滑块位置。

ControlCommands 新增 softEmergencyStop() 便捷函数。

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-07-18 14:59:22 +08:00

220 lines
13 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.
# AGENTS.md — m20_gamepad
## 项目概述
山猫 M20 四足机器人手柄控制 Android App。功能:
- 通过 UDP + JSON 协议(见 `proto.md`)控制机器人运动
- 使用虚拟摇杆(集成自 `controlwear/virtual-joystick-android`
- 拉取 RTSP 视频流作为底层显示
- 设置界面:配置机器人 IP/端口、RTSP 地址、视频编码器选项,持久化保存
---
## 架构与包结构
```
com.example.m20_gamepad/
├── MainActivity.kt # 入口 Activity,管理导航(主界面 ↔ 设置界面)
├── M20App.kt # Application 子类,初始化 DataStore
├── navigation/
│ └── NavRoutes.kt # 导航路由定义
├── ui/
│ ├── theme/ # Compose 主题(已有)
│ ├── MainScreen.kt # 主界面:视频层 + 摇杆覆盖层
│ ├── SettingsScreen.kt # 设置界面:配置机器人 IP/端口、RTSP 地址、编码器
│ ├── JoystickSurface.kt # Compose 对 JoystickView 的包装
│ └── components/ # 其他 UI 组件(状态指示器、按钮等)
├── network/
│ ├── ProtocolClient.kt # UDP 客户端:收发、心跳、线程管理
│ ├── PacketEncoder.kt # 16 字节 Header 封包 + JSON 序列化
│ ├── PacketDecoder.kt # 16 字节 Header 解包 + JSON 反序列化
│ └── models/
│ ├── Header.kt # 16 字节报文头(小端 LE)
│ ├── ApduMessage.kt # 完整 APDU = Header + ASDU(JSON)
│ ├── ControlCommands.kt # 上行指令结构(心跳、轴指令、步态切换等)
│ └── StatusReports.kt # 下行状态结构(基础状态、运控状态、设备状态等)
├── service/
│ ├── RobotConnection.kt # 连接状态机管理(连接/订阅/心跳/断线判定)
│ └── JoystickController.kt # 摇杆输入 → 协议指令映射
├── video/
│ └── RtspSurfaceView.kt # RTSP 视频流播放(SurfaceView 层)
├── data/
│ ├── SettingsRepository.kt # 设置项读写(DataStore 封装)
│ └── AppSettings.kt # 配置数据类定义 + 默认值
```
---
## 协议要点(详见 proto.md
- **传输**: UDP, 目标 `10.21.31.103:30000`
- **报文**: 16 字节 Header(小端)+ JSON ASDU
- **Header 结构**: `Sync(0xEB 91 EB 90) | Length(u16 LE) | MsgId(u16 LE) | Format(0x01) | Reserved(7字节)`
- **ASDU 固定外层**: `{"PatrolDevice": {"Type": int, "Command": int, "Time": "YYYY-MM-DD HH:mm:ss", "Items": {}}}`
- **关键指令**:
- 心跳: Type=100, Cmd=100, ≥1 Hz
- 轴指令: Type=2, Cmd=21, Items: {X,Y,Z,Roll,Pitch,Yaw}, [-1,1], ≥20 Hz
- 速度指令: Type=2, Cmd=25, Items: {X,Y,Z,Roll,Pitch,Yaw}, 物理单位, ≥10 Hz
- 运动状态转换: Type=2, Cmd=22, Items: {MotionParam}
- 步态切换: Type=2, Cmd=23, Items: {GaitParam}
- 使用模式切换: Type=1101, Cmd=5, Items: {Mode}
- **状态上报**: 上行响应含 ErrorCode/ErrorMessage; 主动订阅制(Type=1002, Cmd=3/4/5/6
---
## 虚拟摇杆集成
使用 Compose 原生摇杆库 `com.erz.joysticklibrary`(来自 [erz05/JoyStick](https://github.com/erz05/JoyStick)),以本地模块方式引入(`joysticklibrary/`)。
### 关键 API
`Joystick` Compose 可组合项:
```kotlin
Joystick(
type = JoystickType.EIGHT_AXIS,
onMove = { angle: Double, power: Double, direction: JoystickDirection ->
// angle: 弧度,atan2 范围 [-PI, PI]
// power: 0.0 ~ 100.0
// direction: 方向枚举
},
onTap = { /* 单击 */ },
onDoubleTap = { /* 双击 */ }
)
```
### 角度 → 轴映射
摇杆角度/强度映射到机器人 X/Y/Yaw 轴指令:
- 角度 0° = 正右 → Yaw 负方向
- 角度 90° = 正上 → X 正方向(前进)
- 角度 180° = 正左 → Yaw 正方向
- 角度 270° = 正下 → X 负方向(后退)
- 强度百分比 → [-1, 1] 比例值
**映射公式(从左摇杆角度/强度到 X/Y)**:
```
val x = power / 100.0 * cos(angle) // 垂直分量
val y = power / 100.0 * sin(angle) // 水平分量
```
右摇杆控制 Yaw(偏航角速度)。
---
## RTSP 视频流
使用 `MediaPlayer` + `SurfaceView``ExoPlayer` 拉取 RTSP 流。视频层作为 UI 最底层背景,摇杆等控件覆盖其上(半透明)。
---
## 设置界面
### 配置项
| 配置项 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `robot_host` | String | `10.21.31.103` | 机器人 IP 地址 |
| `robot_port` | Int | `30000` | 机器人 UDP 端口 |
| `rtsp_url` | String | `rtsp://10.21.31.103:554/stream` | RTSP 视频流地址 |
| `video_codec` | Enum | `AUTO` | `AUTO` / `FORCE_HW` / `FORCE_SW_H264` / `FORCE_SW_H265` |
| `video_resize_mode` | Enum | `ZOOM` | `FIT` / `ZOOM` / `FILL` |
### 持久化
使用 Jetpack DataStore Preferences 存储,通过 `SettingsRepository` 封装:
- 写入后立即生效,下次启动自动恢复
- 暴露 `Flow<AppSettings>` 供 UI 层观察
- 修改设置后断开当前连接,下次连接时使用新参数
### 导航
主界面设设置按钮(齿轮图标),点击跳转设置界面。设置界面返回主界面时,若连接参数变更则自动断开旧连接。
---
## 构建与运行
> **注意**:项目源码位于 Windows 目录下,通过 WSL 访问和编辑。WSL 环境中没有 Android SDK / Gradle,无法直接运行构建命令。如需构建或安装,请在 Windows 端的 Android Studio 或终端中执行。
```bash
# 在项目根目录(Windows 端)
gradlew assembleDebug
# 安装到设备(Windows 端)
gradlew installDebug
```
### 依赖
- 虚拟摇杆库:本地模块 `joysticklibrary/`Compose 原生,Kotlin DSL
- JSON 序列化:`kotlinx-serialization-json:1.7.3`
- RTSP 播放:`androidx.media3:media3-exoplayer-rtsp:1.5.1`
- 持久化:`androidx.datastore:datastore-preferences:1.1.3`
- 导航:`androidx.navigation:navigation-compose:2.8.6`
---
## 编码规范
- **语言**: Kotlin,使用 Jetpack Compose 构建 UI
- **协程**: 网络 I/O 使用 `kotlinx.coroutines`,避免裸线程
- **UDP**: 使用 `java.net.DatagramSocket`,绑定本地随机端口
- **状态管理**: 使用 `StateFlow` 暴露连接状态、机器人状态
- **生命周期**: 网络连接生命周期绑定到 Activity/Service,前台时活跃
- **MsgId**: 从 0 递增 u16,循环回绕
- **断线判定**: 收到首个有效数据包前为 `CONNECTING`5 秒无数据 → `TIMEOUT`);`CONNECTED` 后 3 秒无任何 UDP 包 → `TIMEOUT`
---
## 已完成 / 待办
### 已完成
- [x] Gradle 配置:版本目录、摇杆本地模块、全部依赖
- [x] 协议层:Header.kt(16 字节小端编解码)
- [x] 协议层:ApduMessage.ktHeader + ASDU 封装)
- [x] 协议层:ControlCommands.kt(全部指令构建函数 + 常量)
- [x] 协议层:StatusReports.kt(全部状态上报数据类 + JSON 解析器)
- [x] 协议层:PacketEncoder.ktMsgId 管理 + APDU 编码)
- [x] 协议层:PacketDecoder.ktAPDU 解码 + 缓冲区搜索)
- [x] 网络层:ProtocolClient.ktUDP socket、心跳 1Hz、指令发送通道、接收循环、断线检测 3s、订阅触发)
- [x] 摇杆映射:JoystickController.ktangle/power -> X/Y/Yaw
- [x] 状态管理:MainViewModel.ktProtocolClient 生命周期、20Hz 轴指令循环、状态收集)
- [x] 视频播放:RtspVideoPlayer.ktMedia3 ExoPlayer RTSPComposable 包装)
- [x] 主界面:MainScreen.kt(视频背景 + 双摇杆 + 状态栏 + 控制按钮)
- [x] 入口:MainActivity.kt + INTERNET 权限
- [x] 持久化层:AppSettings.kt(配置数据类 + 默认值 + 校验)、SettingsRepository.ktDataStore Preferences 封装,`settings: Flow<AppSettings>` + 单项 setter + `setAll()`)、M20App.ktApplication 子类,进程级 `settingsRepository` 单例)
- [x] 设置界面 UISettingsScreen.kt(主机/端口/RTSP 输入 + 编码器 ExposedDropdownMenu,保存按钮调用 `setAll` 后回调 `onSaved`
- [x] 导航框架:NavRoutes.kt`MAIN`/`SETTINGS` 路由常量)、MainActivity.kt 改写为 `AppNavGraph()`NavHoststartDestination=MAIN,主界面齿轮按钮跳转设置,返回/保存后 popBackStack 到 MAIN
- [x] 连接参数从设置读取:MainViewModel 构造注入 `SettingsRepository``settings: StateFlow<AppSettings>``stateIn(Eagerly)`),`connect()` 读取 `settings.value` 的 host/port`init {}` 监听 host/port 变化(`distinctUntilChanged`)后断开当前连接,下次连接使用新参数(符合“修改设置后断开当前连接”约束)。移除 DEFAULT_HOST/DEFAULT_PORT/DEFAULT_RTSP_URL 硬编码
- [x] 连接状态机:RobotConnection.kt(连接状态流转校验 `isValidConnectionTransition`,运动状态机 `requestMotionTransition` 按 proto.md 2.3 正向流程 `空闲->站立->RL控制` 校验;`canSwitchGait`/`canSendAxisCommand` 仅 RL 控制下放行;`MotionTransitionResult` 枚举反馈)。MainViewModel 经状态机校验后下发指令,轴指令循环非 RL 控制时发零速度
- [x] 状态上报 UI 展示:StatusPanel.kt(异常列表+错误码映射 ErrorCodes.kt、运控状态 Roll/Pitch/Yaw/速度/高度、设备温度电机/驱动器最高温、电池左右电压/电量/温度),顶部状态栏 Info 按钮触发底部弹出面板;指令失败/状态机拒绝通过 Snackbar 反馈
- [x] 更多控制按钮:Mode(常规/导航/辅助)、步态(基础/楼梯/平地敏捷/楼梯敏捷)、照明(前/后灯开/关)、充电(开始/结束)、休眠(休眠/唤醒)。底部控制区可纵向滚动,点击"更多"展开全部控制选项
- [x] RTSP 解码器策略实现:`VideoCodec` 枚举含 `AUTO`/`FORCE_HW`/`FORCE_SW_H264`/`FORCE_SW_H265``toMediaCodecSelector()` 映射到对应 `MediaCodecSelector``DefaultLoadControl` 最小缓冲(100ms起播,总缓冲<500ms,无回退缓存,时间优先于大小阈值);`FORCE_HW` 模式对所有 MIME 仅保留硬件加速解码器
- [x] 摇杆尺寸增大:左右摇杆从 140dp 增大到 180dp,提升操控精度
- [x] 视频缩放模式:`VideoResizeMode` 枚举(FIT/ZOOM/FILL),设置界面新增"视频缩放模式"下拉选项,持久化保存,实时生效
- [x] 状态机修复:趴下状态下允许切换站立状态(机器人站立后自动进入 RL 控制,无需 App 下发)
- [x] UI 布局:增加 bg1.png 作为底图,无视频流时显示(视频流播放时覆盖底图)
- [x] UI 布局:开关灯合并为前灯/后灯两个按键,通过颜色(琥珀色=开/暗色=关)表示置位状态,配合机器人回报状态同步
- [x] 状态栏:ICMP 延迟测量 + WiFi 信号强度图标显示,放置于连接状态文字右侧,延迟独立于连接生命周期
- [x] UI 布局:StatusBar 背景延伸到屏幕顶端,视觉吸附优化(Box + Row 双层结构,背景填满状态栏区域)
- [x] UI 布局:连接按钮合并到顶部 StatusBar 的 ConnectionIndicator,删除底部独立连接按钮
- [x] UI 布局:ConnectionIndicator 改为按钮形态(Surface + 圆角边框),仅指示器区域可点击切换连接
- [x] UI 布局:隐藏系统状态栏(WindowInsetsControllerCompat),压缩自定义 StatusBar 高度,扩展可视区域
- [x] UI 布局:功能按钮均布在四周——左上角模式选择+步态切换+起立/趴下,右上角灯光+休眠+充电,充分利用屏幕空间,优化单手操作体验
- [x] UI 布局:起立/趴下合并为切换按钮(琥珀色=站立时显示"趴下",暗色=非站立时显示"起立"),移至左上角 StatusBar 下方
- [x] UI 布局:模式选择器(常规/导航/辅助,3个互斥按钮,无缝隙,底色显示当前模式)移至左上角,位于起立/趴上方
- [x] UI 布局:步态切换(基础/楼梯/平地敏捷/楼梯敏捷,4个互斥按钮,无缝隙)移至模式选择器下方,始终显示,仅普通模式+RL控制时可点击
- [x] UI 布局:照明/休眠/充电移至右上角,紧凑排列(前灯后灯无缝隙一行,休眠/唤醒切换,充电/充电中切换)
- [x] UI 布局:底部控制区移除"更多"展开区域,仅保留左右摇杆
- [x] UI 布局:所有功能按钮统一使用平行四边形形状(左侧按钮斜边左下→右上,右侧按钮斜边左上→右下)
- [x] UI 布局:所有控制按钮在未连接时灰色不可点击
- [x] 连接状态准确性修复:`ProtocolClient` 收到首个有效数据包后才标记 `CONNECTED`,不再 socket 创建后立即标记;`CONNECTING` 状态 5 秒无响应自动超时回退 `TIMEOUT`
- [x] 连接按钮交互优化:`CONNECTING` 状态下点击连接按钮仅断开回到 `DISCONNECTED`,不再自动重连,由用户手动再次点击发起重试
- [x] 状态机补充:`RobotConnection` 允许 `CONNECTING → TIMEOUT` 合法转换
- [x] 心跳启动时机修复:`ProtocolClient.connect()` 立即启动心跳线程,不再等待首次收到数据包后才启动;心跳在 `CONNECTING``CONNECTED` 状态下均正常运行
- [x] 软急停滑块:右上角新增平行四边形水平滑块,滑到右侧触发软急停(`MOTION_DAMPING`),跟随机器人状态上报自动归位
### 待办
- [ ] 视频进一步延迟优化:探索更激进的低延迟策略(如降低帧率、缩小分辨率、调整 ExoPlayer 缓冲策略等)