From fbc3f1529f55c218e29344dc8fec9e43a0815477 Mon Sep 17 00:00:00 2001 From: CaoWangrenbo Date: Sat, 18 Jul 2026 12:15:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=9E=E6=8E=A5=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E4=BF=AE=E5=A4=8D=20=E2=80=94=20=E6=94=B6?= =?UTF-8?q?=E5=88=B0=E9=A6=96=E4=B8=AA=E6=95=B0=E6=8D=AE=E5=8C=85=E5=90=8E?= =?UTF-8?q?=E6=89=8D=E6=A0=87=E8=AE=B0=E5=B7=B2=E8=BF=9E=E6=8E=A5=EF=BC=8C?= =?UTF-8?q?CONNECTING=205=E7=A7=92=E8=B6=85=E6=97=B6=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProtocolClient: 仅收到首个有效数据包后设置 CONNECTED,不再 socket 创建后立即标记 - ProtocolClient: timeoutCheckLoop 在 connect() 中立即启动,覆盖 CONNECTING(5s) 和 CONNECTED(3s) - RobotConnection: 允许 CONNECTING → TIMEOUT 合法转换 - MainScreen: CONNECTING 时点击连接按钮仅断开回到 DISCONNECTED,不再自动重连 Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- AGENTS.md | 5 ++- .../m20_gamepad/network/ProtocolClient.kt | 38 ++++++++++++------- .../m20_gamepad/service/RobotConnection.kt | 1 + .../com/example/m20_gamepad/ui/MainScreen.kt | 4 +- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 02f7831..adc174e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -161,7 +161,7 @@ gradlew installDebug - **状态管理**: 使用 `StateFlow` 暴露连接状态、机器人状态 - **生命周期**: 网络连接生命周期绑定到 Activity/Service,前台时活跃 - **MsgId**: 从 0 递增 u16,循环回绕 -- **断线判定**: 3 秒无任何 UDP 包 → 标记离线 +- **断线判定**: 收到首个有效数据包前为 `CONNECTING`(5 秒无数据 → `TIMEOUT`);`CONNECTED` 后 3 秒无任何 UDP 包 → `TIMEOUT` --- @@ -208,6 +208,9 @@ gradlew installDebug - [x] UI 布局:底部控制区移除"更多"展开区域,仅保留左右摇杆 - [x] UI 布局:所有功能按钮统一使用平行四边形形状(左侧按钮斜边左下→右上,右侧按钮斜边左上→右下) - [x] UI 布局:所有控制按钮在未连接时灰色不可点击 +- [x] 连接状态准确性修复:`ProtocolClient` 收到首个有效数据包后才标记 `CONNECTED`,不再 socket 创建后立即标记;`CONNECTING` 状态 5 秒无响应自动超时回退 `TIMEOUT` +- [x] 连接按钮交互优化:`CONNECTING` 状态下点击连接按钮仅断开回到 `DISCONNECTED`,不再自动重连,由用户手动再次点击发起重试 +- [x] 状态机补充:`RobotConnection` 允许 `CONNECTING → TIMEOUT` 合法转换 ### 待办 diff --git a/app/src/main/java/com/example/m20_gamepad/network/ProtocolClient.kt b/app/src/main/java/com/example/m20_gamepad/network/ProtocolClient.kt index 889f8fd..a116ec2 100644 --- a/app/src/main/java/com/example/m20_gamepad/network/ProtocolClient.kt +++ b/app/src/main/java/com/example/m20_gamepad/network/ProtocolClient.kt @@ -89,9 +89,10 @@ class ProtocolClient( /** * 建立连接: * 1. 创建 UDP socket - * 2. 启动接收协程 + * 2. 启动接收/发送协程 * 3. 发送订阅请求 - * 4. 启动心跳和断线检测 + * 4. 启动断线检测(CONNECTING 5s / CONNECTED 3s 无数据 → TIMEOUT) + * 5. 首次收到有效数据包后标记已连接,并启动心跳 1Hz(在接收循环中处理) */ fun connect() { if (_state.value != State.DISCONNECTED && _state.value != State.ERROR) return @@ -131,14 +132,7 @@ class ProtocolClient( // 发送订阅请求(触发服务端 UDP 推送) sendSubscriptionRequests() - _state.value = State.CONNECTED - - // 启动心跳 - heartbeatJob = scope.launch(Dispatchers.IO) { - heartbeatLoop(sock, address) - } - - // 启动断线检测 + // 启动断线检测(立即开始,覆盖 CONNECTING 和 CONNECTED 状态) timeoutJob = scope.launch { timeoutCheckLoop() } @@ -232,6 +226,14 @@ class ProtocolClient( if (report != null) { _statusReport.tryEmit(report) } + + // 首次收到有效数据包:标记已连接,启动心跳 + if (_state.value == State.CONNECTING) { + _state.value = State.CONNECTED + heartbeatJob = scope.launch(Dispatchers.IO) { + heartbeatLoop(sock, remoteAddress!!) + } + } } catch (_: SocketTimeoutException) { // 超时正常,继续循环 continue @@ -258,12 +260,22 @@ class ProtocolClient( } } - /** 断线检测:3 秒无数据 → 标记 TIMEOUT */ + /** 断线检测:CONNECTED 状态 3 秒无数据 → TIMEOUT;CONNECTING 状态 5 秒无数据 → TIMEOUT */ private suspend fun CoroutineScope.timeoutCheckLoop() { while (isActive && _state.value != State.DISCONNECTED) { val elapsed = System.currentTimeMillis() - lastPacketTimeMs - if (elapsed > 3000 && _state.value == State.CONNECTED) { - _state.value = State.TIMEOUT + when (_state.value) { + State.CONNECTED -> { + if (elapsed > 3000) { + _state.value = State.TIMEOUT + } + } + State.CONNECTING -> { + if (elapsed > 5000) { + _state.value = State.TIMEOUT + } + } + else -> { /* DISCONNECTED/TIMEOUT/ERROR: 无需超时处理 */ } } delay(500) } diff --git a/app/src/main/java/com/example/m20_gamepad/service/RobotConnection.kt b/app/src/main/java/com/example/m20_gamepad/service/RobotConnection.kt index 1899544..0632365 100644 --- a/app/src/main/java/com/example/m20_gamepad/service/RobotConnection.kt +++ b/app/src/main/java/com/example/m20_gamepad/service/RobotConnection.kt @@ -50,6 +50,7 @@ class RobotConnection { ProtocolClient.State.CONNECTING -> to == ProtocolClient.State.CONNECTED || to == ProtocolClient.State.ERROR || + to == ProtocolClient.State.TIMEOUT || to == ProtocolClient.State.DISCONNECTED ProtocolClient.State.CONNECTED -> to == ProtocolClient.State.TIMEOUT || 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 1c52044..6c58d81 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 @@ -172,7 +172,9 @@ fun MainScreen( onStatusClick = { statusPanelVisible = true }, onSettingsClick = onSettingsClick, onConnectToggle = { - if (connectionState == ProtocolClient.State.CONNECTED) { + if (connectionState == ProtocolClient.State.CONNECTED || + connectionState == ProtocolClient.State.CONNECTING + ) { viewModel.disconnect() } else { viewModel.connect()