上周在工厂部署一线互联的时候,一个工程师问我:"不就是蓝牙扫描嘛,搜到了点连接不就行了?"
行是行,但在工业现场,"搜到了"这三个字没那么简单。

你周围不止一副蓝牙设备
掏出手机扫一下蓝牙,列表里几十个设备。耳机、手环、车间里的蓝牙网关、隔壁工位的手机------Rokid AI 眼镜就混在里面。问题是,眼镜的 BLE 广播名有时候是空的,有时候显示为乱码,你不能只靠名字来认。
我们把扫描过滤做成了两级:
第一级是 Android 系统层的 BLE 扫描配置。用的是低延迟模式,牺牲一点功耗换扫得更快------工厂讲究戴上就干活,不能让人等着蓝牙慢慢扫:
kotlin
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
scanner.startScan(null, settings, scanCallback)
第二级是应用层的过滤。每个扫描结果进来,我们会检查设备名和 Service UUID 是否匹配 Rokid 眼镜的特征。如果两个都不匹配,直接丢掉:
kotlin
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device ?: return
val serviceUuids = result.scanRecord?.serviceUuids
?.map { it.uuid.toString() } ?: emptyList()
val name = result.scanRecord?.deviceName ?: runCatching { device.name }.getOrNull()
if (!RokidGlassesBtScanFilter.matches(
name = name,
serviceUuids = serviceUuids,
targetServiceUuid = config.serviceUuid,
allowNameFallback = config.allowNameFallback
)
) return
// 只保留匹配的设备
val displayName = name ?: "Glass_${device.address.takeLast(5).replace(":", "")}"
upsertDevice(RokidGlassesBtDevice(
name = displayName,
address = device.address,
type = device.type,
rssi = result.rssi,
serviceUuids = serviceUuids
))
}
这里有个小细节:如果广播里拿不到设备名,我们会用 MAC 地址后五位生成一个"Glass_XXXX"的临时名称。在车间里可能同时有好几副眼镜,这个名字至少能让用户区分。
还有一个实操经验值得提:扫描到的设备按 RSSI 降序排列。信号强的排上面,大概率就是离你最近的那副。上层的代码通过 StateFlow 拿到设备列表后直接绑定到 UI,每次新设备进来自动排序:
kotlin
private fun upsertDevice(device: RokidGlassesBtDevice) {
val next = _devices.value
.filterNot { it.address.equals(device.address, ignoreCase = true) }
.plus(device)
.sortedByDescending { it.rssi }
_devices.value = next
}
为什么不用传统蓝牙配对
这是另一个经常被问到的问题。普通蓝牙耳机、音箱用的是经典蓝牙 SPP/A2DP profile,配对完系统就记住了。但 Rokid 眼镜的视频流和控制指令走的是 CXR 私有协议通道,底层虽然也是蓝牙传输,但连接建立逻辑完全不一样。
打个比方:普通蓝牙设备像是标准快递,手机操作系统认识这个快递单,帮你签收了。但 CXR 通道像是专用物流,快递单和签收流程都是定制的,你得通过 CXR SDK 去初始化、协商 endpoint、建立 socket 连接。
这也是为什么一线互联在封装连接层的时候,要把 CXR SDK 的调用全部隔离在一个内部接口后面:
kotlin
internal object AndroidRokidCxrClient : RokidCxrClient {
override fun initBluetooth(
context: Context,
device: BluetoothDevice,
callback: BluetoothStatusCallback
) {
CxrApi.getInstance().initBluetooth(context, device, callback)
}
override fun connectBluetooth(
context: Context,
socketUuid: String,
macAddress: String,
bluetoothClientName: String,
callback: BluetoothStatusCallback,
authBlob: ByteArray,
clientSecret: String
) {
CxrApi.getInstance().connectBluetooth(
context, socketUuid, macAddress, bluetoothClientName,
callback, authBlob, clientSecret
)
}
override fun deinitBluetooth() {
CxrApi.getInstance().deinitBluetooth()
}
override fun isBluetoothConnected(): Boolean {
return CxrApi.getInstance().isBluetoothConnected
}
}
对上层业务来说,它只知道调用 manager.startScan() 和 manager.connect(device),完全不用关心底层是 CXR 还是别的什么协议。如果未来 Rokid SDK 升级了 API 签名,改动的范围也只在这个文件里。
这篇文章其实就想说一件事:蓝牙扫描不是搜一下就行,设备过滤、排序、连接建立,每个环节的细节决定了工业现场的体验。把 BLE 扫描的五秒变成一秒,把"连不上"变成"三秒重连",就是这些细节堆出来的。
相关仓库:github.com/jlink-ai/ro... · Apache 2.0
下一篇聊聊连接状态机和失败模型------也就是那个"SOCKET_CONNECT_FAILED"到底是怎么来的,以及为什么每一种错误都应该对应一个明确的操作指引。