第三章 扫描设备实战:从入门到避坑

好,咱们直接进入正题。扫描设备,这是 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_SCANBLUETOOTH_CONNECT。我曾经在一个项目里忘了处理权限回调,结果扫描回调死活不触发,排查了半天才发现是权限被拒绝了。

⚠️ 权限警告: 从 Android 12(API 31)开始,蓝牙权限发生了重大变化。如果你还在用旧的 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 过滤后,回调频率从每秒几十次降到了几次,效果立竿见影。

💡 小技巧: 如果你不确定设备的 Service UUID,可以用 nRF Connect 这类工具先扫描一下,看看广播包里都带了什么。我每次对接新硬件都会先这么干。

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 本章知识体系

下面这张图总结了扫描设备的核心流程和关键点:

BLE 扫描设备核心流程 前置条件 ✅ 蓝牙已开启 ✅ 权限已授予 ✅ Adapter 非空 设置扫描参数 ScanSettings 扫描模式 ScanFilter 过滤条件 ScanCallback 回调 startScan() 处理 ScanResult RSSI 信号强度 估算距离 ScanRecord 广播数据 解析厂商数据

这张图把整个扫描流程串起来了。从前置条件开始,到参数设置,再到扫描启动和结果处理,每一步都有坑。你按这个流程走,基本不会出大问题。

📌 我的经验: 扫描这块最容易出问题的不是代码本身,而是对硬件行为的不了解。比如有些设备广播间隔是 100ms,有些是 1s,如果你用 LOW_POWER 模式(扫描窗口 100ms),很可能错过那些广播间隔长的设备。所以,拿到新硬件后,先用 nRF Connect 摸清它的广播规律,再写代码。

好了,扫描设备这块就讲到这里。代码不多,但细节不少。你回去把上面的代码跑一遍,遇到问题再回来翻翻这张图,应该能解决大部分问题。


公众号:蓝海资料掘金营,微信deep3321