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

连接 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 秒的时间窗口,超过这个时间还连不上,大概率是真的有问题,让超时逻辑去接管。
轮询怎么知道该停了
三个条件:
- generation 变了——说明用户发起了新的连接请求或 disconnect,当前轮询作废
- 状态已经不是 ResolvingEndpoint 或 Connecting——说明已经进入终态(Connected/Failed/Disconnected),没必要再查
- 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 计数器防止状态污染。
浙公网安备 33010602011771号