Files
m20_gamepad/AGENTS.md
T
hexone2086 df35ff3b05 feat: 状态栏优化 — 背景吸附+连接按钮合并+隐藏系统状态栏
- StatusBar 重构为 Box + Row 双层结构,背景延伸到屏幕顶端
- 连接按钮合并到顶部 ConnectionIndicator,删除底部独立连接按钮
- ConnectionIndicator 改为按钮形态(Surface + 圆角边框)
- 隐藏系统状态栏(WindowInsetsControllerCompat),压缩 StatusBar 高度

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

11 KiB
Raw Blame History

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 + SurfaceViewExoPlayer 拉取 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 或终端中执行。

# 在项目根目录(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,循环回绕
  • 断线判定: 3 秒无任何 UDP 包 → 标记离线

已完成 / 待办

已完成

  • Gradle 配置:版本目录、摇杆本地模块、全部依赖
  • 协议层:Header.kt(16 字节小端编解码)
  • 协议层:ApduMessage.ktHeader + ASDU 封装)
  • 协议层:ControlCommands.kt(全部指令构建函数 + 常量)
  • 协议层:StatusReports.kt(全部状态上报数据类 + JSON 解析器)
  • 协议层:PacketEncoder.ktMsgId 管理 + APDU 编码)
  • 协议层:PacketDecoder.ktAPDU 解码 + 缓冲区搜索)
  • 网络层:ProtocolClient.ktUDP socket、心跳 1Hz、指令发送通道、接收循环、断线检测 3s、订阅触发)
  • 摇杆映射:JoystickController.ktangle/power -> X/Y/Yaw
  • 状态管理:MainViewModel.ktProtocolClient 生命周期、20Hz 轴指令循环、状态收集)
  • 视频播放:RtspVideoPlayer.ktMedia3 ExoPlayer RTSPComposable 包装)
  • 主界面:MainScreen.kt(视频背景 + 双摇杆 + 状态栏 + 控制按钮)
  • 入口:MainActivity.kt + INTERNET 权限
  • 持久化层:AppSettings.kt(配置数据类 + 默认值 + 校验)、SettingsRepository.ktDataStore Preferences 封装,settings: Flow<AppSettings> + 单项 setter + setAll())、M20App.ktApplication 子类,进程级 settingsRepository 单例)
  • 设置界面 UISettingsScreen.kt(主机/端口/RTSP 输入 + 编码器 ExposedDropdownMenu,保存按钮调用 setAll 后回调 onSaved
  • 导航框架:NavRoutes.ktMAIN/SETTINGS 路由常量)、MainActivity.kt 改写为 AppNavGraph()NavHoststartDestination=MAIN,主界面齿轮按钮跳转设置,返回/保存后 popBackStack 到 MAIN
  • 连接参数从设置读取:MainViewModel 构造注入 SettingsRepositorysettings: StateFlow<AppSettings>stateIn(Eagerly)),connect() 读取 settings.value 的 host/portinit {} 监听 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_H265toMediaCodecSelector() 映射到对应 MediaCodecSelectorDefaultLoadControl 最小缓冲(100ms起播,总缓冲<500ms,无回退缓存,时间优先于大小阈值);FORCE_HW 模式对所有 MIME 仅保留硬件加速解码器
  • 摇杆尺寸增大:左右摇杆从 140dp 增大到 180dp,提升操控精度
  • 视频缩放模式:VideoResizeMode 枚举(FIT/ZOOM/FILL),设置界面新增"视频缩放模式"下拉选项,持久化保存,实时生效
  • 状态机修复:趴下状态下允许切换站立状态(机器人站立后自动进入 RL 控制,无需 App 下发)
  • UI 布局:增加 bg1.png 作为底图,无视频流时显示(视频流播放时覆盖底图)
  • UI 布局:开关灯合并为前灯/后灯两个按键,通过颜色(琥珀色=开/暗色=关)表示置位状态,配合机器人回报状态同步
  • 状态栏:ICMP 延迟测量 + WiFi 信号强度图标显示,放置于连接状态文字右侧,延迟独立于连接生命周期
  • UI 布局:StatusBar 背景延伸到屏幕顶端,视觉吸附优化(Box + Row 双层结构,背景填满状态栏区域)
  • UI 布局:连接按钮合并到顶部 StatusBar 的 ConnectionIndicator,删除底部独立连接按钮
  • UI 布局:ConnectionIndicator 改为按钮形态(Surface + 圆角边框),仅指示器区域可点击切换连接
  • UI 布局:隐藏系统状态栏(WindowInsetsControllerCompat),压缩自定义 StatusBar 高度,扩展可视区域

待办

  • UI 布局:功能按钮均布在四周,充分利用屏幕空间,优化单手操作体验
  • 视频进一步延迟优化:探索更激进的低延迟策略(如降低帧率、缩小分辨率、调整 ExoPlayer 缓冲策略等)