Compare commits
4 Commits
9b8cd09929
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 9883eb4b44 | |||
| 28438d05bb | |||
| dae40cc73c | |||
| 3225ab0922 |
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -37,7 +37,7 @@ com.example.m20_gamepad/
|
||||
│ ├── RobotConnection.kt # 连接状态机管理(连接/订阅/心跳/断线判定)
|
||||
│ └── JoystickController.kt # 摇杆输入 → 协议指令映射
|
||||
├── video/
|
||||
│ └── RtspSurfaceView.kt # RTSP 视频流播放(SurfaceView 层)
|
||||
│ └── RtspVideoPlayer.kt # RTSP 视频流播放(ExoPlayer + Composable 包装)
|
||||
├── data/
|
||||
│ ├── SettingsRepository.kt # 设置项读写(DataStore 封装)
|
||||
│ └── AppSettings.kt # 配置数据类定义 + 默认值
|
||||
@@ -52,12 +52,12 @@ com.example.m20_gamepad/
|
||||
- **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}
|
||||
- 心跳: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)
|
||||
|
||||
---
|
||||
@@ -159,6 +159,7 @@ gradlew installDebug
|
||||
- **协程**: 网络 I/O 使用 `kotlinx.coroutines`,避免裸线程
|
||||
- **UDP**: 使用 `java.net.DatagramSocket`,绑定本地随机端口
|
||||
- **状态管理**: 使用 `StateFlow` 暴露连接状态、机器人状态
|
||||
- **状态上报合并策略**: `handleStatusReport` 对 `_latestStatus` 采用合并(`prev.copy`)而非替换,避免 4 个独立订阅(Basic/Error/Motion/Device)的局部报告覆盖整个状态导致 UI 闪烁。各分项(`batteryStatus`、`motionStatus` 等)同时维护独立 StateFlow,供 UI 直接订阅
|
||||
- **生命周期**: 网络连接生命周期绑定到 Activity/Service,前台时活跃
|
||||
- **MsgId**: 从 0 递增 u16,循环回绕
|
||||
- **断线判定**: 收到首个有效数据包前为 `CONNECTING`(5 秒无数据 → `TIMEOUT`);`CONNECTED` 后 3 秒无任何 UDP 包 → `TIMEOUT`
|
||||
@@ -187,9 +188,9 @@ gradlew installDebug
|
||||
- [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] 状态上报 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] 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 下发)
|
||||
@@ -200,10 +201,10 @@ gradlew installDebug
|
||||
- [x] UI 布局:连接按钮合并到顶部 StatusBar 的 ConnectionIndicator,删除底部独立连接按钮
|
||||
- [x] UI 布局:ConnectionIndicator 改为按钮形态(Surface + 圆角边框),仅指示器区域可点击切换连接
|
||||
- [x] UI 布局:隐藏系统状态栏(WindowInsetsControllerCompat),压缩自定义 StatusBar 高度,扩展可视区域
|
||||
- [x] UI 布局:功能按钮均布在四周——左上角模式选择+步态切换+起立/趴下,右上角灯光+休眠+充电,充分利用屏幕空间,优化单手操作体验
|
||||
- [x] UI 布局:功能按钮均布在四周——左上角模式选择 + 步态切换 + 起立/趴下,右上角灯光 + 休眠 + 充电,充分利用屏幕空间,优化单手操作体验
|
||||
- [x] UI 布局:起立/趴下合并为切换按钮(琥珀色=站立时显示"趴下",暗色=非站立时显示"起立"),移至左上角 StatusBar 下方
|
||||
- [x] UI 布局:模式选择器(常规/导航/辅助,3个互斥按钮,无缝隙,底色显示当前模式)移至左上角,位于起立/趴上方
|
||||
- [x] UI 布局:步态切换(基础/楼梯/平地敏捷/楼梯敏捷,4个互斥按钮,无缝隙)移至模式选择器下方,始终显示,仅普通模式+RL控制时可点击
|
||||
- [x] UI 布局:模式选择器(常规/导航/辅助,3 个互斥按钮,无缝隙,底色显示当前模式)移至左上角,位于起立/趴上方
|
||||
- [x] UI 布局:步态切换(基础/楼梯/平地敏捷/楼梯敏捷,4 个互斥按钮,无缝隙)移至模式选择器下方,始终显示,仅普通模式+RL 控制时可点击
|
||||
- [x] UI 布局:照明/休眠/充电移至右上角,紧凑排列(前灯后灯无缝隙一行,休眠/唤醒切换,充电/充电中切换)
|
||||
- [x] UI 布局:底部控制区移除"更多"展开区域,仅保留左右摇杆
|
||||
- [x] UI 布局:所有功能按钮统一使用平行四边形形状(左侧按钮斜边左下→右上,右侧按钮斜边左上→右下)
|
||||
@@ -213,7 +214,12 @@ gradlew installDebug
|
||||
- [x] 状态机补充:`RobotConnection` 允许 `CONNECTING → TIMEOUT` 合法转换
|
||||
- [x] 心跳启动时机修复:`ProtocolClient.connect()` 立即启动心跳线程,不再等待首次收到数据包后才启动;心跳在 `CONNECTING` 和 `CONNECTED` 状态下均正常运行
|
||||
- [x] 软急停滑块:右上角新增平行四边形水平滑块,滑到右侧触发软急停(`MOTION_DAMPING`),跟随机器人状态上报自动归位
|
||||
- [x] 状态上报闪烁修复:`_latestStatus` 改用合并策略(`prev.copy`),保留上次报告的非 null 字段,避免 4 个独立订阅交替覆盖导致状态栏/步态按钮闪烁
|
||||
- [x] 电量显示稳定性优化:`StatusBar` 电量改用独立 `batteryStatus` StateFlow,不依赖 `status?.batteryStatus`
|
||||
- [x] 休眠按钮状态约束:仅非站立状态(空闲/趴下/阻尼等)可切换休眠,站立/RL 控制时禁用
|
||||
- [x] RTSP 自动重连:`RtspVideoPlayer` 播放失败后自动重试,指数退避(初始 1s,最大 30s,倍率 2),URL/解码器变化时重置退避状态;覆盖层显示重试次数和倒计时
|
||||
- [x] 订阅请求清理:删除 `ProtocolClient.sendSubscriptionRequests()` 方法及 `connect()` 中的注释调用;同步删除 `ControlCommands.subscribeStatus()` 和 `SUB_*` 常量,彻底清除死代码
|
||||
|
||||
### 待办
|
||||
|
||||
- [ ] 视频进一步延迟优化:探索更激进的低延迟策略(如降低帧率、缩小分辨率、调整 ExoPlayer 缓冲策略等)
|
||||
- [ ] 视频进一步延迟优化:探索更激进的低延迟策略(跳过 Media3 ExoPlayer,直接使用 MediaCodec + SurfaceView)
|
||||
|
||||
@@ -171,6 +171,7 @@ fun MainScreen(
|
||||
StatusBar(
|
||||
connectionState = connectionState,
|
||||
status = status,
|
||||
batteryStatus = batteryStatus,
|
||||
pingLatencyMs = pingLatencyMs,
|
||||
wifiSignalLevel = wifiSignalLevel,
|
||||
onStatusClick = { statusPanelVisible = true },
|
||||
@@ -255,11 +256,13 @@ fun MainScreen(
|
||||
viewModel.sendLightControl(ledStatus?.front ?: 0, if (backOn) 0 else 1)
|
||||
}
|
||||
}
|
||||
// 休眠/唤醒
|
||||
// 休眠/唤醒 — 仅趴下/空闲等非站立状态可切换
|
||||
val isSleeping = status?.basicStatus?.sleep == 1
|
||||
val canSleep = motionState != ControlCommands.MOTION_STAND &&
|
||||
motionState != ControlCommands.MOTION_RL_CONTROL
|
||||
SleepToggleButton(
|
||||
isSleeping = isSleeping,
|
||||
connected = connectionState == ProtocolClient.State.CONNECTED,
|
||||
connected = connectionState == ProtocolClient.State.CONNECTED && canSleep,
|
||||
onToggle = { viewModel.sendSleepSettings(!isSleeping) }
|
||||
)
|
||||
// 充电/结束充电
|
||||
@@ -307,6 +310,7 @@ fun MainScreen(
|
||||
private fun StatusBar(
|
||||
connectionState: ProtocolClient.State,
|
||||
status: com.example.m20_gamepad.network.models.StatusReport?,
|
||||
batteryStatus: com.example.m20_gamepad.network.models.BatteryStatus?,
|
||||
pingLatencyMs: Long?,
|
||||
wifiSignalLevel: Int,
|
||||
onStatusClick: () -> Unit,
|
||||
@@ -340,7 +344,7 @@ private fun StatusBar(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
status?.batteryStatus?.let {
|
||||
batteryStatus?.let {
|
||||
val avgLevel = (it.batteryLevelLeft + it.batteryLevelRight) / 2
|
||||
StatusChip("电量 ${avgLevel.toInt()}%")
|
||||
}
|
||||
|
||||
@@ -229,7 +229,30 @@ class MainViewModel(
|
||||
|
||||
/** 状态上报分发:更新机器人运动状态机 + 各分项缓存。 */
|
||||
private fun handleStatusReport(report: StatusReport) {
|
||||
_latestStatus.value = report
|
||||
// 合并:保留上次报告中非 null 的字段,避免部分报告覆盖导致 UI 闪烁
|
||||
val prev = _latestStatus.value
|
||||
_latestStatus.value = if (prev != null) {
|
||||
prev.copy(
|
||||
type = report.type,
|
||||
command = report.command,
|
||||
time = report.time,
|
||||
errorCode = report.errorCode ?: prev.errorCode,
|
||||
errorMessage = report.errorMessage ?: prev.errorMessage,
|
||||
basicStatus = report.basicStatus ?: prev.basicStatus,
|
||||
motionStatus = report.motionStatus ?: prev.motionStatus,
|
||||
motorStatus = report.motorStatus ?: prev.motorStatus,
|
||||
batteryList = report.batteryList ?: prev.batteryList,
|
||||
batteryStatus = report.batteryStatus ?: prev.batteryStatus,
|
||||
deviceTemperature = report.deviceTemperature ?: prev.deviceTemperature,
|
||||
led = report.led ?: prev.led,
|
||||
gps = report.gps ?: prev.gps,
|
||||
devEnable = report.devEnable ?: prev.devEnable,
|
||||
cpu = report.cpu ?: prev.cpu,
|
||||
errorList = report.errorList ?: prev.errorList
|
||||
)
|
||||
} else {
|
||||
report
|
||||
}
|
||||
_lastUpdate.value = report.time
|
||||
robot.updateFromReport(report)
|
||||
report.errorList?.let { _errorList.value = it }
|
||||
|
||||
Reference in New Issue
Block a user