04afb3331f
山猫 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>
9.6 KiB
9.6 KiB
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),以本地模块方式引入(joysticklibrary/)。
关键 API
Joystick Compose 可组合项:
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_SW_H264 / FORCE_SW_H265 |
持久化
使用 Jetpack DataStore Preferences 存储,通过 SettingsRepository 封装:
- 写入后立即生效,下次启动自动恢复
- 暴露
Flow<AppSettings>供 UI 层观察 - 修改设置后断开当前连接,下次连接时使用新参数
导航
主界面设设置按钮(齿轮图标),点击跳转设置界面。设置界面返回主界面时,若连接参数变更则自动断开旧连接。
构建与运行
# 在项目根目录
./gradlew assembleDebug
# 安装到设备
./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,循环回绕
- 断线判定: 3 秒无任何 UDP 包 → 标记离线
已完成 / 待办
已完成
- Gradle 配置:版本目录、摇杆本地模块、全部依赖
- 协议层:Header.kt(16 字节小端编解码)
- 协议层:ApduMessage.kt(Header + ASDU 封装)
- 协议层:ControlCommands.kt(全部指令构建函数 + 常量)
- 协议层:StatusReports.kt(全部状态上报数据类 + JSON 解析器)
- 协议层:PacketEncoder.kt(MsgId 管理 + APDU 编码)
- 协议层:PacketDecoder.kt(APDU 解码 + 缓冲区搜索)
- 网络层:ProtocolClient.kt(UDP socket、心跳 1Hz、指令发送通道、接收循环、断线检测 3s、订阅触发)
- 摇杆映射:JoystickController.kt(angle/power -> X/Y/Yaw)
- 状态管理:MainViewModel.kt(ProtocolClient 生命周期、20Hz 轴指令循环、状态收集)
- 视频播放:RtspVideoPlayer.kt(Media3 ExoPlayer RTSP,Composable 包装)
- 主界面:MainScreen.kt(视频背景 + 双摇杆 + 状态栏 + 控制按钮)
- 入口:MainActivity.kt + INTERNET 权限
- 持久化层:AppSettings.kt(配置数据类 + 默认值 + 校验)、SettingsRepository.kt(DataStore Preferences 封装,
settings: Flow<AppSettings>+ 单项 setter +setAll())、M20App.kt(Application 子类,进程级settingsRepository单例) - 设置界面 UI:SettingsScreen.kt(主机/端口/RTSP 输入 + 编码器 ExposedDropdownMenu,保存按钮调用
setAll后回调onSaved) - 导航框架:NavRoutes.kt(
MAIN/SETTINGS路由常量)、MainActivity.kt 改写为AppNavGraph()(NavHost,startDestination=MAIN,主界面齿轮按钮跳转设置,返回/保存后 popBackStack 到 MAIN) - 连接参数从设置读取:MainViewModel 构造注入
SettingsRepository,settings: StateFlow<AppSettings>(stateIn(Eagerly)),connect()读取settings.value的 host/port;init {}监听 host/port 变化(distinctUntilChanged)后断开当前连接,下次连接使用新参数(符合“修改设置后断开当前连接”约束)。移除 DEFAULT_HOST/DEFAULT_PORT/DEFAULT_RTSP_URL 硬编码 - 连接状态机:RobotConnection.kt(连接状态流转校验
isValidConnectionTransition,运动状态机requestMotionTransition按 proto.md 2.3 正向流程空闲->站立->RL控制校验;canSwitchGait/canSendAxisCommand仅 RL 控制下放行;MotionTransitionResult枚举反馈)。MainViewModel 经状态机校验后下发指令,轴指令循环非 RL 控制时发零速度 - 状态上报 UI 展示:StatusPanel.kt(异常列表+错误码映射 ErrorCodes.kt、运控状态 Roll/Pitch/Yaw/速度/高度、设备温度电机/驱动器最高温、电池左右电压/电量/温度),顶部状态栏 Info 按钮触发底部弹出面板;指令失败/状态机拒绝通过 Snackbar 反馈
- 更多控制按钮:Mode(常规/导航/辅助)、步态(基础/楼梯/平地敏捷/楼梯敏捷)、照明(前/后灯开/关)、充电(开始/结束)、休眠(休眠/唤醒)。底部控制区可纵向滚动,点击"更多"展开全部控制选项
- RTSP 解码器策略实现:
VideoCodec枚举含AUTO/FORCE_HW/FORCE_SW_H264/FORCE_SW_H265;toMediaCodecSelector()映射到对应MediaCodecSelector;DefaultLoadControl最小缓冲(100ms起播,总缓冲<500ms,无回退缓存,时间优先于大小阈值);FORCE_HW模式对所有 MIME 仅保留硬件加速解码器
待办
(无 — 所有功能已实现)