From 3e4198aa7cd654ea805b588aa01c7c4ad890fb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E5=8B=A4=E6=B0=91?= Date: Mon, 20 Apr 2026 17:02:54 +0800 Subject: [PATCH] =?UTF-8?q?refactor(AsrHelper):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=AF=AD=E9=9F=B3=E8=AF=86=E5=88=AB=E8=BF=9E=E6=8E=A5=E9=87=8D?= =?UTF-8?q?=E8=BF=9E=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除未使用的 import 语句 - 添加指数退避算法计算重连延迟时间 - 引入最大重连尝试次数限制防止无限重连 - 添加重连状态管理变量控制重连行为 - 实现重连状态重置功能 - 优化网络可用时的重连逻辑 - 改进连接失败时的状态清理 - 添加详细的重连日志记录 --- .../com/nova/brain/glass/helper/AsrHelper.kt | 107 ++++++++++++++---- 1 file changed, 84 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt b/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt index 3ecaa08..a2ad0bf 100644 --- a/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt +++ b/app/src/main/java/com/nova/brain/glass/helper/AsrHelper.kt @@ -1,6 +1,5 @@ package com.nova.brain.glass.helper -import android.R.attr.action import android.app.AlertDialog import android.content.Context import android.content.Intent @@ -13,6 +12,7 @@ import android.os.Handler import android.os.Looper import android.util.Log import android.view.LayoutInflater +import androidx.core.graphics.toColorInt import com.blankj.utilcode.util.Utils import com.nova.brain.glass.BuildConfig import com.nova.brain.glass.R @@ -25,22 +25,20 @@ import com.rokid.online.speech.TtsClient import com.rokid.online.speech.open.AndroidPcmTtsStreamPlayer import com.rokid.online.speech.open.OpenSdkAudioSource import com.xuqm.base.common.AppManager -import com.xuqm.base.extensions.showMessage import kotlin.system.exitProcess -import androidx.core.graphics.toColorInt object AsrHelper : OfflineCmdListener { private const val TAG = "AsrHelper" // 配置信息来自 BuildConfig(在 app/build.gradle 的 buildConfigField 中维护) - private val DOMAIN get() = BuildConfig.SPEECH_DOMAIN - private val AK get() = BuildConfig.SPEECH_AK - private val SK get() = BuildConfig.SPEECH_SK - private val UID get() = BuildConfig.SPEECH_UID + private val DOMAIN get() = BuildConfig.SPEECH_DOMAIN + private val AK get() = BuildConfig.SPEECH_AK + private val SK get() = BuildConfig.SPEECH_SK + private val UID get() = BuildConfig.SPEECH_UID private val DEVICE_ID get() = BuildConfig.SPEECH_DEVICE_ID - private val ASR_PATH get() = BuildConfig.SPEECH_ASR_PATH - private val TTS_PATH get() = BuildConfig.SPEECH_TTS_PATH + private val ASR_PATH get() = BuildConfig.SPEECH_ASR_PATH + private val TTS_PATH get() = BuildConfig.SPEECH_TTS_PATH // 唤醒词:Nova Nova private const val WAKE_WORD = "Nova Nova" @@ -60,12 +58,18 @@ object AsrHelper : OfflineCmdListener { private var isTtsConnecting = false private var pendingStartMic = false private var isClosed = false + private var asrReconnectAttempt = 0 + private var ttsReconnectAttempt = 0 + private var asrReconnectPaused = false + private var ttsReconnectPaused = false private var connectivityManager: ConnectivityManager? = null private var networkCallbackRegistered = false private const val WAKE_RESPONSE = "在呢,您请说" private const val LISTENING_TIMEOUT_MS = 30_000L - private const val RECONNECT_DELAY_MS = 3_000L + private const val BASE_RECONNECT_DELAY_MS = 3_000L + private const val MAX_RECONNECT_DELAY_MS = 30_000L + private const val MAX_RECONNECT_ATTEMPTS = 5 private val mainHandler = Handler(Looper.getMainLooper()) private var listeningDialog: AlertDialog? = null @@ -100,6 +104,7 @@ object AsrHelper : OfflineCmdListener { private val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { Log.d(TAG, "Network available, reconnect speech channels if needed") + resetReconnectState() reconnectAllIfNeeded(immediate = true) dismissNoNetworkDialog() } @@ -161,6 +166,7 @@ object AsrHelper : OfflineCmdListener { registerNetworkCallback() // 自动建立 ASR / TTS 连接 + resetReconnectState() asrConnect() ttsConnect() @@ -197,6 +203,9 @@ object AsrHelper : OfflineCmdListener { private fun asrStartMic() { if (!isConnected) { + if (asrReconnectPaused) { + resetAsrReconnectState() + } pendingStartMic = true asrConnect() Log.w(TAG, "ASR not connected, queue startMic after reconnect") @@ -224,23 +233,38 @@ object AsrHelper : OfflineCmdListener { mainHandler.removeCallbacks(listeningTimeoutRunnable) } - private fun scheduleAsrReconnect(delayMs: Long = RECONNECT_DELAY_MS) { + private fun scheduleAsrReconnect(delayMs: Long? = null) { if (!shouldReconnectAsr()) return + if (asrReconnectAttempt >= MAX_RECONNECT_ATTEMPTS) { + asrReconnectPaused = true + pendingStartMic = false + Log.w(TAG, "ASR reconnect paused after $MAX_RECONNECT_ATTEMPTS failures") + return + } + asrReconnectAttempt += 1 + val actualDelayMs = delayMs ?: calculateReconnectDelay(asrReconnectAttempt) mainHandler.removeCallbacks(asrReconnectRunnable) - mainHandler.postDelayed(asrReconnectRunnable, delayMs) - Log.d(TAG, "ASR reconnect scheduled in ${delayMs}ms") + mainHandler.postDelayed(asrReconnectRunnable, actualDelayMs) + Log.d(TAG, "ASR reconnect scheduled in ${actualDelayMs}ms, attempt=$asrReconnectAttempt") } - private fun scheduleTtsReconnect(delayMs: Long = RECONNECT_DELAY_MS) { + private fun scheduleTtsReconnect(delayMs: Long? = null) { if (!shouldReconnectTts()) return + if (ttsReconnectAttempt >= MAX_RECONNECT_ATTEMPTS) { + ttsReconnectPaused = true + Log.w(TAG, "TTS reconnect paused after $MAX_RECONNECT_ATTEMPTS failures") + return + } + ttsReconnectAttempt += 1 + val actualDelayMs = delayMs ?: calculateReconnectDelay(ttsReconnectAttempt) mainHandler.removeCallbacks(ttsReconnectRunnable) - mainHandler.postDelayed(ttsReconnectRunnable, delayMs) - Log.d(TAG, "TTS reconnect scheduled in ${delayMs}ms") + mainHandler.postDelayed(ttsReconnectRunnable, actualDelayMs) + Log.d(TAG, "TTS reconnect scheduled in ${actualDelayMs}ms, attempt=$ttsReconnectAttempt") } private fun reconnectAllIfNeeded(immediate: Boolean = false) { if (!isNetworkAvailable()) return - val delayMs = if (immediate) 0L else RECONNECT_DELAY_MS + val delayMs = if (immediate) 0L else null if (!isConnected) { scheduleAsrReconnect(delayMs) } @@ -250,11 +274,33 @@ object AsrHelper : OfflineCmdListener { } private fun shouldReconnectAsr(): Boolean { - return !isClosed && !isConnected && !isAsrConnecting && isNetworkAvailable() + return !isClosed && !isConnected && !isAsrConnecting && !asrReconnectPaused && isNetworkAvailable() } private fun shouldReconnectTts(): Boolean { - return !isClosed && !isTtsConnected && !isTtsConnecting && isNetworkAvailable() + return !isClosed && !isTtsConnected && !isTtsConnecting && !ttsReconnectPaused && isNetworkAvailable() + } + + private fun calculateReconnectDelay(attempt: Int): Long { + val factor = 1L shl (attempt - 1).coerceAtLeast(0) + return (BASE_RECONNECT_DELAY_MS * factor).coerceAtMost(MAX_RECONNECT_DELAY_MS) + } + + private fun resetAsrReconnectState() { + asrReconnectAttempt = 0 + asrReconnectPaused = false + mainHandler.removeCallbacks(asrReconnectRunnable) + } + + private fun resetTtsReconnectState() { + ttsReconnectAttempt = 0 + ttsReconnectPaused = false + mainHandler.removeCallbacks(ttsReconnectRunnable) + } + + private fun resetReconnectState() { + resetAsrReconnectState() + resetTtsReconnectState() } private fun getContext(): Context = Utils.getApp() @@ -377,7 +423,7 @@ object AsrHelper : OfflineCmdListener { override fun onOpen() { isConnected = true isAsrConnecting = false - mainHandler.removeCallbacks(asrReconnectRunnable) + resetAsrReconnectState() Log.d(TAG, "ASR websocket open") if (pendingStartMic) { asrStartMic() @@ -421,7 +467,10 @@ object AsrHelper : OfflineCmdListener { when (action.name) { "goToDecisionCenter" -> onGoToDecisionCenter?.invoke(action) "goToTaskCenter" -> onGoToTaskCenter?.invoke(action) - "openTaskDetail", "openTaskDetailWithFilter" -> onOpenTaskDetail?.invoke(action) + "openTaskDetail", "openTaskDetailWithFilter" -> onOpenTaskDetail?.invoke( + action + ) + else -> Log.d(TAG, "unhandled action: $action") } }, @@ -438,6 +487,10 @@ object AsrHelper : OfflineCmdListener { isConnected = false isAsrConnecting = false isMicRunning = false + if (pendingStartMic) { + Log.w(TAG, "ASR pending startMic cleared after websocket failure") + } + pendingStartMic = false dismissListeningDialog() scheduleAsrReconnect() } @@ -446,6 +499,7 @@ object AsrHelper : OfflineCmdListener { isConnected = false isAsrConnecting = false isMicRunning = false + pendingStartMic = false dismissListeningDialog() Log.d(TAG, "ASR closed code=$code reason=$reason") scheduleAsrReconnect() @@ -458,7 +512,7 @@ object AsrHelper : OfflineCmdListener { override fun onOpen() { isTtsConnected = true isTtsConnecting = false - mainHandler.removeCallbacks(ttsReconnectRunnable) + resetTtsReconnectState() Log.d(TAG, "TTS websocket open") } @@ -470,7 +524,7 @@ object AsrHelper : OfflineCmdListener { override fun onError(code: Int, message: String) { isTtsConnected = false isTtsConnecting = false - Log.e(TAG, "TTS error code=$code msg=$message, fallback to mic") + Log.e(TAG, "TTS error code=$code msg=$message") scheduleTtsReconnect() asrStartMic() } @@ -497,6 +551,7 @@ object AsrHelper : OfflineCmdListener { showListeningDialog() restartListeningTimeout() pendingStartMic = true + resetReconnectState() if (isTtsConnected) { tts?.speak(WAKE_RESPONSE) } else { @@ -519,6 +574,7 @@ object AsrHelper : OfflineCmdListener { activity.finishAffinity() exitProcess(0) } + "返回", "退回" -> { activity.startActivity( Intent(activity, WelcomeActivity::class.java).apply { @@ -556,6 +612,11 @@ object AsrHelper : OfflineCmdListener { isAsrConnecting = false isTtsConnected = false isTtsConnecting = false + asrReconnectAttempt = 0 + ttsReconnectAttempt = 0 + asrReconnectPaused = false + ttsReconnectPaused = false + pendingStartMic = false Log.d(TAG, "AsrHelper closed") } }