diff --git a/AGENTS.md b/AGENTS.md index 619e1cd..5d4bf6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` @@ -213,6 +214,9 @@ 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 控制时禁用 ### 待办 diff --git a/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt b/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt index c049325..af90e91 100644 --- a/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt +++ b/app/src/main/java/com/example/m20_gamepad/ui/MainScreen.kt @@ -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()}%") } diff --git a/app/src/main/java/com/example/m20_gamepad/ui/MainViewModel.kt b/app/src/main/java/com/example/m20_gamepad/ui/MainViewModel.kt index d451080..0ed9205 100644 --- a/app/src/main/java/com/example/m20_gamepad/ui/MainViewModel.kt +++ b/app/src/main/java/com/example/m20_gamepad/ui/MainViewModel.kt @@ -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 }