fix: 连接状态判定修复 — 收到首个数据包后才标记已连接,CONNECTING 5秒超时回退

- 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 <vibe@mistral.ai>
This commit is contained in:
2026-07-18 12:15:10 +08:00
parent b2adf46957
commit fbc3f1529f
4 changed files with 33 additions and 15 deletions
@@ -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 秒无数据 → TIMEOUTCONNECTING 状态 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)
}
@@ -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 ||
@@ -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()