第三章 扫描设备实战:从入门到避坑
好,咱们直接进入正题。扫描设备,这是 BLE 开发的第一步,也是最容易出幺蛾子的地方。我记得刚入行那会儿,光一个扫描就折腾了两天——不是扫不到设备,就是扫到一堆不该扫的。今天我把这些经验都摊开讲,你照着做,能少走不少弯路。
3.1 基础扫描:三行代码搞定?没那么简单
先看最基础的扫描实现。Kotlin 配合 Android 的 BLE API,代码确实不多:
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
val bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
bluetoothLeScanner.startScan(object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let {
Log.d("BLE_SCAN", "发现设备: ${it.device.name ?: "未知"} - ${it.device.address}")
}
}
})
嗯,这里要注意。很多人以为拿到 BluetoothAdapter 就能直接扫,其实有个坑——你得先确保蓝牙已开启。我个人习惯在扫描前加个检查:
if (!bluetoothAdapter.isEnabled) {
// 请求用户开启蓝牙
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
return
}
另外,别忘了权限。Android 6.0 以上需要 ACCESS_FINE_LOCATION,Android 12 以上还要 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT。我曾经在一个项目里忘了处理权限回调,结果扫描回调死活不触发,排查了半天才发现是权限被拒绝了。
ACCESS_COARSE_LOCATION,扫描可能直接失败。建议使用 BLUETOOTH_SCAN + BLUETOOTH_CONNECT 组合。
3.2 过滤特定设备:别让垃圾数据淹了你
实际开发中,你周围可能有几十个蓝牙设备在广播。不加过滤的话,日志刷得飞起,根本找不到你要的设备。我一般用两种方式过滤:
3.2.1 基于名称过滤
最简单粗暴的方式,在回调里判断设备名:
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let {
val deviceName = it.device.name ?: ""
if (deviceName.startsWith("MyDevice_")) {
// 这是我们关心的设备
handleDevice(it)
}
}
}
但说实话,这种方式不太靠谱。有些设备广播时不带名字,或者名字会变。你想想看,如果设备名是空的,你拿什么过滤?
3.2.2 基于 Service UUID 过滤(推荐)
更专业的方式是用 ScanFilter。每个 BLE 设备广播时都会携带 Service UUID,这是标准做法:
val scanFilter = ScanFilter.Builder()
.setServiceUuid(ParcelUuid(YOUR_SERVICE_UUID))
.build()
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
bluetoothLeScanner.startScan(
listOf(scanFilter),
scanSettings,
scanCallback
)
这样做的好处是,系统底层就会帮你过滤,回调里只会收到匹配的设备。省电又高效。我在做智能家居项目时,周围有几十个蓝牙传感器,用 UUID 过滤后,回调频率从每秒几十次降到了几次,效果立竿见影。
3.3 设置扫描参数:省电还是快?这是个问题
扫描参数的核心就三个:扫描模式、回调类型、匹配模式。咱们一个个说。
| 参数 | 选项 | 说明 | 我的建议 |
|---|---|---|---|
| 扫描模式 | SCAN_MODE_LOW_POWER SCAN_MODE_BALANCED SCAN_MODE_LOW_LATENCY |
功耗与速度的权衡 | 调试用 LOW_LATENCY,发布用 BALANCED |
| 回调类型 | CALLBACK_TYPE_ALL_MATCHES CALLBACK_TYPE_FIRST_MATCH CALLBACK_TYPE_MATCH_LOST |
何时触发回调 | 大部分场景用 ALL_MATCHES |
| 匹配模式 | MATCH_MODE_AGGRESSIVE MATCH_MODE_STICKY |
匹配的严格程度 | AGGRESSIVE 更灵敏,但可能重复 |
举个例子,如果你在做门禁系统,希望设备一靠近就立刻发现,那就用 LOW_LATENCY + AGGRESSIVE。但如果你在做环境监测,设备每隔几秒上报一次数据就行,用 LOW_POWER 能省不少电。
我曾经在一个穿戴设备项目里,因为扫描模式设成了 LOW_LATENCY,导致手环半天就没电了。后来改成 BALANCED,续航直接翻倍。嗯,这里要记住:扫描模式不是越快越好,得看实际场景。
3.4 处理扫描结果:别只拿个地址就完事
ScanResult 里其实藏着不少信息。很多人只取 device.address,其实浪费了:
data class ScanResult(
val device: BluetoothDevice, // 设备对象
val rssi: Int, // 信号强度(dBm)
val scanRecord: ScanRecord?, // 广播数据
val timestampNanos: Long // 时间戳
)
我常用的几个字段:
- rssi:判断设备距离。一般 -50dBm 以上算近,-90dBm 以下基本不可用。
- scanRecord:广播数据里可能包含厂商自定义数据、设备名称、Tx Power 等。
- timestampNanos:用于去重。同一个设备可能在短时间内被多次扫描到,用时间戳可以过滤掉重复的。
这里有个实用技巧:用 RSSI 估算距离。虽然不精确,但够用:
fun estimateDistance(rssi: Int, txPower: Int = -59): Double {
if (rssi == 0) return -1.0
val ratio = rssi.toDouble() / txPower
return if (ratio < 1.0) {
Math.pow(ratio, 10.0)
} else {
(0.89976 * Math.pow(ratio, 7.7095)) + 0.111
}
}
这个公式是我从 Apple 的 iBeacon 规范里扒的,实测在 1-10 米范围内还算靠谱。当然,别指望它精确到厘米,毕竟信号受环境影响太大了。
HashMap<String, ScanResult> 按地址缓存,每次更新 RSSI 和时间戳,这样既能去重,又能拿到最新的信号强度。
3.5 本章知识体系
下面这张图总结了扫描设备的核心流程和关键点:
这张图把整个扫描流程串起来了。从前置条件开始,到参数设置,再到扫描启动和结果处理,每一步都有坑。你按这个流程走,基本不会出大问题。
好了,扫描设备这块就讲到这里。代码不多,但细节不少。你回去把上面的代码跑一遍,遇到问题再回来翻翻这张图,应该能解决大部分问题。