4、BLE扫描基础:扫描参数配置、扫描结果回调、扫描过滤策略、扫描功耗优化

BLE扫描,说白了就是设备在找周围谁在“喊话”。

我刚开始做蓝牙项目时,觉得扫描嘛,不就是打开接口等数据?结果一跑起来,功耗高得吓人,回调乱成一锅粥。后来才明白,扫描这件事,参数配不好,后面全是坑。

4.1 扫描参数配置

鸿蒙的BLE扫描接口,核心就几个参数。但每个参数背后,都有讲究。

// 鸿蒙BLE扫描参数结构体
struct BleScanParams {
    uint16_t scanInterval;    // 扫描间隔,单位0.625ms
    uint16_t scanWindow;      // 扫描窗口,单位0.625ms
    uint8_t scanType;         // 0:被动扫描 1:主动扫描
    uint8_t scanPhy;          // 1M、2M、Coded
    uint8_t scanFilterPolicy; // 过滤策略
};

scanInterval 和 scanWindow,这两个是功耗的关键。

scanWindow 必须 ≤ scanInterval。我见过有人把窗口设得比间隔还大,结果扫描直接失败。嗯,这其实是协议栈的硬性要求。

场景 scanInterval scanWindow 说明
高实时性 30 (18.75ms) 30 (18.75ms) 连续扫描,功耗高
平衡模式 80 (50ms) 40 (25ms) 我项目中最常用的配置
低功耗 320 (200ms) 32 (20ms) 适合后台扫描

scanType,主动扫描 vs 被动扫描。

主动扫描会发扫描请求,让对方回复扫描响应。被动扫描只监听广播包。你想想看,主动扫描能拿到更多数据,但功耗也更高。我建议:如果只需要设备名和广播数据,被动扫描就够了。

scanPhy,物理层选择。

1M PHY最通用,2M PHY速率高但距离短,Coded PHY距离远但速率低。我在做室内定位项目时,就吃过Coded PHY的亏——距离是远了,但扫描周期变长,功耗反而没降下来。

4.2 扫描结果回调

扫描结果回调,是BLE扫描的“出口”。所有找到的设备,都会从这里冒出来。

// 注册扫描回调
BleCentralManager.registerBleScanCallback(new BleScanCallback() {
    @Override
    public void onScanResult(BleScanResult result) {
        // 每个设备扫描到都会回调
        String deviceAddr = result.getDevice().getDeviceAddr();
        byte[] scanRecord = result.getScanRecord();
        int rssi = result.getRssi();
        
        // 我个人习惯在这里做初步过滤
        if (rssi < -90) {
            return; // 信号太弱,直接丢弃
        }
    }

    @Override
    public void onScanFailed(int errorCode) {
        // 扫描失败回调
        Log.e("BLE", "扫描失败,错误码:" + errorCode);
    }
});

这里有个细节:onScanResult 回调频率很高。如果周围有几十个设备,每秒可能回调上百次。我曾经在回调里做数据库写入,结果手机直接卡死。后来改成先缓存到队列,再批量处理。

注意:不要在 onScanResult 回调中做耗时操作。建议用 Handler 或 WorkQueue 异步处理。

scanRecord 解析,这是另一个坑。

广播数据包是 TLV 格式(Type-Length-Value)。鸿蒙 SDK 提供了解析工具类,但有些厂商的广播包不标准。我曾经遇到一个设备,把设备名放在了自定义字段里,用标准解析拿不到。最后只能手动解析原始字节流。

// 手动解析广播包示例
private void parseScanRecord(byte[] scanRecord) {
    int index = 0;
    while (index < scanRecord.length) {
        int length = scanRecord[index] & 0xFF;
        if (length == 0) break;
        
        int type = scanRecord[index + 1] & 0xFF;
        byte[] data = Arrays.copyOfRange(scanRecord, index + 2, index + length + 1);
        
        switch (type) {
            case 0x09: // 完整设备名
            case 0x08: // 简写设备名
                String name = new String(data, StandardCharsets.UTF_8);
                break;
            // 其他类型...
        }
        index += length + 1;
    }
}

4.3 扫描过滤策略

不设过滤,回调会多到让你怀疑人生。鸿蒙提供了几种过滤方式。

UUID 过滤,最常用。

只扫描包含特定 Service UUID 的设备。比如你只关心心率设备,就过滤 0x180D。

// 设置UUID过滤
BleScanFilter filter = new BleScanFilter.Builder()
    .setServiceUuid(new UUID(0x180D, 0x00001000800000805F9B34FB))
    .build();

List<BleScanFilter> filters = new ArrayList<>();
filters.add(filter);
centralManager.startScan(filters, scanParams, callback);

设备地址过滤,适合连接已知设备。

我做过一个项目,需要同时连接多个同型号设备。用地址过滤,每个设备单独扫描,互不干扰。

RSSI 阈值过滤,简单粗暴。

信号太弱的设备,连上了也容易断。我一般设 -80dBm 为阈值,低于这个值直接忽略。

小技巧:过滤策略可以组合使用。比如先按UUID过滤,再按RSSI排序,只保留信号最强的3个设备。这样既省功耗,又提高连接成功率。

4.4 扫描功耗优化

功耗优化,是BLE开发的重头戏。我见过不少产品,功能都实现了,但电池撑不过一天。嗯,问题多半出在扫描上。

策略一:间歇扫描

不要一直扫。扫几秒,停几秒。比如扫3秒,停10秒。这样平均功耗能降70%以上。

// 间歇扫描示例
private void startPeriodicScan() {
    // 扫描3秒
    centralManager.startScan(callback);
    new Handler().postDelayed(() -> {
        // 停止扫描
        centralManager.stopScan(callback);
        // 等待10秒后再次扫描
        new Handler().postDelayed(this::startPeriodicScan, 10000);
    }, 3000);
}

策略二:动态调整扫描窗口

刚启动时用大窗口快速发现设备,发现后用最小窗口维持连接。我曾经在智能门锁项目里用这个策略,待机功耗从 2mA 降到了 0.3mA。

策略三:利用扫描过滤减少回调

回调越少,CPU唤醒次数越少,功耗越低。把过滤条件设严一点,只关心你需要的设备。

核心原则:能不扫就不扫,能少扫就少扫,能过滤就过滤。

策略四:选择合适的 PHY

Coded PHY 虽然距离远,但扫描时间更长。如果设备距离不远,用 1M PHY 就够了。我测试过,同样场景下,1M PHY 比 Coded PHY 省电约 40%。

最后说一句:功耗优化没有银弹。每个项目都要根据实际场景调参。我的习惯是:先跑一周日志,分析扫描行为,再针对性优化。你想想看,连数据都没有,怎么优化?

避坑指南:我曾经在低功耗模式下忘了关闭扫描,结果设备一晚上耗光了电。记得在进入休眠前,一定要调用 stopScan()。

好了,扫描基础就这些。下一章我们聊聊连接参数和MTU协商,那又是另一片天地。