# 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，循环回绕
- **断线判定**: 3 秒无任何 UDP 包 → 标记离线

---

## 已完成 / 待办

### 已完成

- [x] Gradle 配置：版本目录、摇杆本地模块、全部依赖
- [x] 协议层：Header.kt（16 字节小端编解码）
- [x] 协议层：ApduMessage.kt（Header + ASDU 封装）
- [x] 协议层：ControlCommands.kt（全部指令构建函数 + 常量）
- [x] 协议层：StatusReports.kt（全部状态上报数据类 + JSON 解析器）
- [x] 协议层：PacketEncoder.kt（MsgId 管理 + APDU 编码）
- [x] 协议层：PacketDecoder.kt（APDU 解码 + 缓冲区搜索）
- [x] 网络层：ProtocolClient.kt（UDP socket、心跳 1Hz、指令发送通道、接收循环、断线检测 3s、订阅触发）
- [x] 摇杆映射：JoystickController.kt（angle/power -> X/Y/Yaw）
- [x] 状态管理：MainViewModel.kt（ProtocolClient 生命周期、20Hz 轴指令循环、状态收集）
- [x] 视频播放：RtspVideoPlayer.kt（Media3 ExoPlayer RTSP，Composable 包装）
- [x] 主界面：MainScreen.kt（视频背景 + 双摇杆 + 状态栏 + 控制按钮）
- [x] 入口：MainActivity.kt + INTERNET 权限
- [x] 持久化层：AppSettings.kt（配置数据类 + 默认值 + 校验）、SettingsRepository.kt（DataStore Preferences 封装，`settings: Flow<AppSettings>` + 单项 setter + `setAll()`）、M20App.kt（Application 子类，进程级 `settingsRepository` 单例）
- [x] 设置界面 UI：SettingsScreen.kt（主机/端口/RTSP 输入 + 编码器 ExposedDropdownMenu，保存按钮调用 `setAll` 后回调 `onSaved`）
- [x] 导航框架：NavRoutes.kt（`MAIN`/`SETTINGS` 路由常量）、MainActivity.kt 改写为 `AppNavGraph()`（NavHost，startDestination=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 布局：所有控制按钮在未连接时灰色不可点击

### 待办

- [ ] 视频进一步延迟优化：探索更激进的低延迟策略（如降低帧率、缩小分辨率、调整 ExoPlayer 缓冲策略等）
