Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 # 连接状态机管理(连接/订阅/心跳/断线判定)
|
│ ├── RobotConnection.kt # 连接状态机管理(连接/订阅/心跳/断线判定)
|
||||||
│ └── JoystickController.kt # 摇杆输入 → 协议指令映射
|
│ └── JoystickController.kt # 摇杆输入 → 协议指令映射
|
||||||
├── video/
|
├── video/
|
||||||
│ └── RtspSurfaceView.kt # RTSP 视频流播放(SurfaceView 层)
|
│ └── RtspVideoPlayer.kt # RTSP 视频流播放(ExoPlayer + Composable 包装)
|
||||||
├── data/
|
├── data/
|
||||||
│ ├── SettingsRepository.kt # 设置项读写(DataStore 封装)
|
│ ├── SettingsRepository.kt # 设置项读写(DataStore 封装)
|
||||||
│ └── AppSettings.kt # 配置数据类定义 + 默认值
|
│ └── AppSettings.kt # 配置数据类定义 + 默认值
|
||||||
@@ -159,6 +159,7 @@ gradlew installDebug
|
|||||||
- **协程**: 网络 I/O 使用 `kotlinx.coroutines`,避免裸线程
|
- **协程**: 网络 I/O 使用 `kotlinx.coroutines`,避免裸线程
|
||||||
- **UDP**: 使用 `java.net.DatagramSocket`,绑定本地随机端口
|
- **UDP**: 使用 `java.net.DatagramSocket`,绑定本地随机端口
|
||||||
- **状态管理**: 使用 `StateFlow` 暴露连接状态、机器人状态
|
- **状态管理**: 使用 `StateFlow` 暴露连接状态、机器人状态
|
||||||
|
- **状态上报合并策略**: `handleStatusReport` 对 `_latestStatus` 采用合并(`prev.copy`)而非替换,避免 4 个独立订阅(Basic/Error/Motion/Device)的局部报告覆盖整个状态导致 UI 闪烁。各分项(`batteryStatus`、`motionStatus` 等)同时维护独立 StateFlow,供 UI 直接订阅
|
||||||
- **生命周期**: 网络连接生命周期绑定到 Activity/Service,前台时活跃
|
- **生命周期**: 网络连接生命周期绑定到 Activity/Service,前台时活跃
|
||||||
- **MsgId**: 从 0 递增 u16,循环回绕
|
- **MsgId**: 从 0 递增 u16,循环回绕
|
||||||
- **断线判定**: 收到首个有效数据包前为 `CONNECTING`(5 秒无数据 → `TIMEOUT`);`CONNECTED` 后 3 秒无任何 UDP 包 → `TIMEOUT`
|
- **断线判定**: 收到首个有效数据包前为 `CONNECTING`(5 秒无数据 → `TIMEOUT`);`CONNECTED` 后 3 秒无任何 UDP 包 → `TIMEOUT`
|
||||||
@@ -213,6 +214,11 @@ gradlew installDebug
|
|||||||
- [x] 状态机补充:`RobotConnection` 允许 `CONNECTING → TIMEOUT` 合法转换
|
- [x] 状态机补充:`RobotConnection` 允许 `CONNECTING → TIMEOUT` 合法转换
|
||||||
- [x] 心跳启动时机修复:`ProtocolClient.connect()` 立即启动心跳线程,不再等待首次收到数据包后才启动;心跳在 `CONNECTING` 和 `CONNECTED` 状态下均正常运行
|
- [x] 心跳启动时机修复:`ProtocolClient.connect()` 立即启动心跳线程,不再等待首次收到数据包后才启动;心跳在 `CONNECTING` 和 `CONNECTED` 状态下均正常运行
|
||||||
- [x] 软急停滑块:右上角新增平行四边形水平滑块,滑到右侧触发软急停(`MOTION_DAMPING`),跟随机器人状态上报自动归位
|
- [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_*` 常量,彻底清除死代码
|
||||||
|
|
||||||
### 待办
|
### 待办
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ fun MainScreen(
|
|||||||
StatusBar(
|
StatusBar(
|
||||||
connectionState = connectionState,
|
connectionState = connectionState,
|
||||||
status = status,
|
status = status,
|
||||||
|
batteryStatus = batteryStatus,
|
||||||
pingLatencyMs = pingLatencyMs,
|
pingLatencyMs = pingLatencyMs,
|
||||||
wifiSignalLevel = wifiSignalLevel,
|
wifiSignalLevel = wifiSignalLevel,
|
||||||
onStatusClick = { statusPanelVisible = true },
|
onStatusClick = { statusPanelVisible = true },
|
||||||
@@ -255,11 +256,13 @@ fun MainScreen(
|
|||||||
viewModel.sendLightControl(ledStatus?.front ?: 0, if (backOn) 0 else 1)
|
viewModel.sendLightControl(ledStatus?.front ?: 0, if (backOn) 0 else 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 休眠/唤醒
|
// 休眠/唤醒 — 仅趴下/空闲等非站立状态可切换
|
||||||
val isSleeping = status?.basicStatus?.sleep == 1
|
val isSleeping = status?.basicStatus?.sleep == 1
|
||||||
|
val canSleep = motionState != ControlCommands.MOTION_STAND &&
|
||||||
|
motionState != ControlCommands.MOTION_RL_CONTROL
|
||||||
SleepToggleButton(
|
SleepToggleButton(
|
||||||
isSleeping = isSleeping,
|
isSleeping = isSleeping,
|
||||||
connected = connectionState == ProtocolClient.State.CONNECTED,
|
connected = connectionState == ProtocolClient.State.CONNECTED && canSleep,
|
||||||
onToggle = { viewModel.sendSleepSettings(!isSleeping) }
|
onToggle = { viewModel.sendSleepSettings(!isSleeping) }
|
||||||
)
|
)
|
||||||
// 充电/结束充电
|
// 充电/结束充电
|
||||||
@@ -307,6 +310,7 @@ fun MainScreen(
|
|||||||
private fun StatusBar(
|
private fun StatusBar(
|
||||||
connectionState: ProtocolClient.State,
|
connectionState: ProtocolClient.State,
|
||||||
status: com.example.m20_gamepad.network.models.StatusReport?,
|
status: com.example.m20_gamepad.network.models.StatusReport?,
|
||||||
|
batteryStatus: com.example.m20_gamepad.network.models.BatteryStatus?,
|
||||||
pingLatencyMs: Long?,
|
pingLatencyMs: Long?,
|
||||||
wifiSignalLevel: Int,
|
wifiSignalLevel: Int,
|
||||||
onStatusClick: () -> Unit,
|
onStatusClick: () -> Unit,
|
||||||
@@ -340,7 +344,7 @@ private fun StatusBar(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
status?.batteryStatus?.let {
|
batteryStatus?.let {
|
||||||
val avgLevel = (it.batteryLevelLeft + it.batteryLevelRight) / 2
|
val avgLevel = (it.batteryLevelLeft + it.batteryLevelRight) / 2
|
||||||
StatusChip("电量 ${avgLevel.toInt()}%")
|
StatusChip("电量 ${avgLevel.toInt()}%")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,7 +229,30 @@ class MainViewModel(
|
|||||||
|
|
||||||
/** 状态上报分发:更新机器人运动状态机 + 各分项缓存。 */
|
/** 状态上报分发:更新机器人运动状态机 + 各分项缓存。 */
|
||||||
private fun handleStatusReport(report: StatusReport) {
|
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
|
_lastUpdate.value = report.time
|
||||||
robot.updateFromReport(report)
|
robot.updateFromReport(report)
|
||||||
report.errorList?.let { _errorList.value = it }
|
report.errorList?.let { _errorList.value = it }
|
||||||
|
|||||||
Reference in New Issue
Block a user