Rokid AI 眼镜远程协作应用"一线互联"开发实践:SDK 轮询兜底与可靠性保障

image

连接 Rokid AI 眼镜的时候,CXR SDK 会在连接成功后回调 onConnected()。大多数时候这个回调是准的,但少数情况下它不来——SDK 内部状态已经 Connected 了,但回调没触发。

这个问题第一次出现的时候,我们花了一下午才定位到:不是没连上,是连上了但没人告诉你连上了。

不能只靠回调

SDK 的 isBluetoothConnected 属性是可信的——它是 SDK 内部状态的一个直接查询,返回 true 就说明链路已经建立。所以解决办法很简单:在发起连接后,启动一个轮询去检查 isBluetoothConnected,回调不来就轮询补上

private fun scheduleConnectedStatePoll(generation: Int, attempt: Int = 0) {
    val delayMs = CONNECTED_STATE_POLL_DELAYS_MS.getOrNull(attempt) ?: return
    mainHandler.postDelayed({
        // generation 变了说明连接已经结束或重试了
        if (connectGeneration != generation) return@postDelayed

        val currentState = _state.value
        if (currentState != RokidGlassesBtState.ResolvingEndpoint &&
            currentState != RokidGlassesBtState.Connecting
        ) return@postDelayed  // 已经进入终态

        if (syncConnectedStateFromSdk()) return@postDelayed  // 确认已连接

        scheduleConnectedStatePoll(generation, attempt + 1)  // 下一轮
    }, delayMs)
}

private fun syncConnectedStateFromSdk(): Boolean {
    if (!isConnected()) return false

    val endpoint = pendingEndpoint ?: endpointStore.load()
    if (endpoint != null && RokidGlassesBtEndpointValidator.validate(endpoint) == null) {
        endpointStore.save(endpoint)
    }
    cancelConnectTimeout()
    pendingEndpoint = null
    activeConnectStage = null
    _state.value = RokidGlassesBtStateReducer.connected()
    return true
}

轮询间隔为什么是指数递增的

轮询频率的选择是个权衡。太快了浪费 CPU,太慢了用户感知到延迟。我们用了一组指数递增的延迟:

val CONNECTED_STATE_POLL_DELAYS_MS = longArrayOf(
    500L,       // 第 1 次:0.5 秒
    1_000L,     // 第 2 次:1 秒
    2_000L,     // 第 3 次:2 秒
    4_000L,     // 第 4 次:4 秒
    8_000L,     // 第 5 次:8 秒
    16_000L,    // 第 6 次:16 秒
    32_000L     // 第 7 次:32 秒
)

前几次间隔短,因为连接通常在几百毫秒到一两秒内完成。后面的间隔拉开,避免长时间空转。七次轮询覆盖了大约 63 秒的时间窗口,超过这个时间还连不上,大概率是真的有问题,让超时逻辑去接管。

轮询怎么知道该停了

三个条件:

  1. generation 变了——说明用户发起了新的连接请求或 disconnect,当前轮询作废
  2. 状态已经不是 ResolvingEndpoint 或 Connecting——说明已经进入终态(Connected/Failed/Disconnected),没必要再查
  3. syncConnectedStateFromSdk 返回 true——已经确认连接,停止轮询

这里 generation 的判断很重要。没有它的话,上一轮连接的轮询任务可能在新连接发起后仍然在执行,把状态搞乱:

if (connectGeneration != generation) return@postDelayed

在 connectBluetooth 发起后就立刻启动轮询

轮询的启动时机在 connectBluetoothInternal 方法里,紧跟在 cxrClient.connectBluetooth 调用之后:

private fun connectBluetoothInternal(
    endpoint: RokidGlassesBtEndpoint,
    stage: RokidGlassesBtStage
): Boolean {
    // ... 校验 endpoint、鉴权、设置状态为 Connecting ...

    return runCatching {
        cxrClient.connectBluetooth(
            appContext, endpoint.socketUuid, endpoint.macAddress,
            config.bluetoothClientName, bluetoothCallback,
            config.authBlob, config.clientSecretForSdk
        )
    }.onSuccess {
        scheduleConnectedStatePoll(generation)  // 启动轮询
    }.onFailure { error ->
        cancelConnectTimeout()
        endpointStore.clear()
        _state.value = RokidGlassesBtStateReducer.failed(
            stage = stage, code = RokidGlassesBtErrorCode.SDK_EXCEPTION,
            detail = error.message
        )
    }.isSuccess
}

这其实是一个通用模式

"异步回调 + 定时轮询兜底"不是我们发明的,但在蓝牙 SDK 对接这个场景里特别好用。第三方 SDK 的回调可靠性你是控制不了的——它可能因为内部线程调度问题、生命周期问题、版本兼容问题不回调。加一层兜底轮询,代价很小(几行代码、微乎其微的 CPU 开销),收益是消灭了一整类"假性连接失败"。


相关仓库:github.com/jumuxyz/jlink-ai-rokid-glasses-bt · Apache 2.0

遇到类似问题的时候可以参考这个思路:主路径用回调,兜底用轮询,轮询间隔用指数递增,用 generation 计数器防止状态污染

posted @ 2026-06-03 11:04  byte_2  阅读(2)  评论(0)    收藏  举报