12、BLE广播开发:广播数据格式、广播类型、广播设置与回调

各位同学,今天我们来聊聊BLE广播开发。说实话,广播是BLE最基础也最容易踩坑的环节。我刚开始做蓝牙项目时,就因为在广播数据格式上少填了一个字节,导致设备死活搜不到——后来才发现是AD Structure的长度字段算错了。嗯,这种低级错误,我希望你们能一次避开。

12.1 广播数据格式(AD Structure)

BLE广播包的核心,说白了就是一段精心编排的字节数组。它的结构非常清晰:

  • 长度(Length):1字节,表示后续数据长度(不包含自身)
  • 类型(AD Type):1字节,标识数据类型
  • 数据(Data):N字节,具体内容

每个广播包可以包含多个这样的AD Structure,它们首尾相连。我习惯把这种结构叫做「TLV」——Type、Length、Value。你想想看,是不是很形象?

常见AD Type值

类型值含义说明
0x01Flags广播模式标志,必填项
0x02/0x0316位Service UUID完整/不完整列表
0x06/0x07128位Service UUID完整/不完整列表
0x08/0x09Local Name短名称/完整名称
0xFFManufacturer Specific厂商自定义数据

举个例子,一个最简单的广播数据构造:

// 构造广播数据
byte[] scanRecord = new byte[]{
    (byte) 0x02,  // 长度:2字节
    (byte) 0x01,  // 类型:Flags
    (byte) 0x06,  // 数据:LE General Discoverable + BR/EDR Not Supported

    (byte) 0x05,  // 长度:5字节
    (byte) 0x08,  // 类型:Shortened Local Name
    (byte) 0x42,  // 'B'
    (byte) 0x4C,  // 'L'
    (byte) 0x45,  // 'E'
    (byte) 0x00   // 结束符(可选)
};

我在项目中遇到过一个问题:有些手机扫描不到设备,就是因为Flags字段没填对。记住,Flags字段建议放在第一个AD Structure,而且一定要包含「LE General Discoverable Mode」标志位。

12.2 广播类型:可连接 vs 不可连接

广播类型决定了你的设备能不能被其他设备连接。这个选择其实挺关键的——选错了,要么连不上,要么白白浪费电量。

  • 可连接广播(Connectable):扫描设备可以发起连接。适用于需要双向通信的场景,比如智能手环、传感器。
  • 不可连接广播(Non-Connectable):只发不收,扫描设备只能接收数据。适用于纯广播场景,比如信标(Beacon)、广告推送。

我个人的经验是:如果你不确定,优先用可连接广播。因为不可连接广播一旦发出去,想改就来不及了——除非你重新启动广播。我曾经在一个项目中为了省电用了不可连接广播,结果客户要求增加数据回传功能,只能大改代码……

小技巧:Android的BluetoothLeAdvertiser支持两种广播模式:ADVERTISE_MODE_LOW_LATENCY(高频率,适合需要快速被发现)、ADVERTISE_MODE_BALANCED(平衡模式)、ADVERTISE_MODE_LOW_POWER(省电模式)。我一般调试时用LOW_LATENCY,上线后切到BALANCED。

12.3 广播设置(AdvertiseSettings)

AdvertiseSettings是配置广播行为的核心类。它控制着广播的方方面面,从发射功率到广播间隔。

AdvertiseSettings settings = new AdvertiseSettings.Builder()
    .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
    .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
    .setConnectable(true)  // 可连接
    .setTimeout(0)         // 0表示持续广播
    .build();

这里有几个参数值得注意:

  • setAdvertiseMode:广播间隔。LOW_LATENCY约100ms,BALANCED约250ms,LOW_POWER约1s。间隔越短,发现越快,但耗电也越大。
  • setTxPowerLevel:发射功率。越高信号越强,但耗电也大。我建议室内用MEDIUM,室外用HIGH。
  • setTimeout:广播超时时间(毫秒)。0表示一直广播,直到手动停止。非0值会在指定时间后自动停止。

注意:Android 8.0以上,后台应用启动广播有严格限制。如果你发现广播启动后几秒就停了,八成是被系统限制了。解决方案是使用前台服务,或者申请「android.permission.BLUETOOTH_ADVERTISE」权限(Android 12+)。

12.4 广播回调(AdvertiseCallback)

广播启动后,系统会通过回调告诉你状态变化。这个回调虽然简单,但很重要——尤其是失败时,它能帮你定位问题。

private AdvertiseCallback mCallback = new AdvertiseCallback() {
    @Override
    public void onStartSuccess(AdvertiseSettings settingsInEffect) {
        // 广播启动成功
        Log.d(TAG, "广播启动成功,模式:" + settingsInEffect.getMode());
    }

    @Override
    public void onStartFailure(int errorCode) {
        // 广播启动失败
        String errorMsg;
        switch (errorCode) {
            case AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED:
                errorMsg = "广播已经启动";
                break;
            case AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE:
                errorMsg = "广播数据太大(最大31字节)";
                break;
            case AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED:
                errorMsg = "设备不支持BLE广播";
                break;
            case AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR:
                errorMsg = "内部错误";
                break;
            case AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS:
                errorMsg = "广播者数量超限";
                break;
            default:
                errorMsg = "未知错误:" + errorCode;
        }
        Log.e(TAG, "广播启动失败:" + errorMsg);
    }
};

我曾经遇到一个坑:onStartFailure返回ADVERTISE_FAILED_DATA_TOO_LARGE。当时我明明只放了20字节数据,怎么会超?后来才发现,Android系统会在广播数据中自动添加一些标志位,实际占用的空间比我想象的多。所以,我建议你预留3-5字节的余量。

12.5 厂商自定义广播包

厂商自定义广播包(Manufacturer Specific Data)是BLE广播中最灵活的部分。你可以把任意数据塞进去,只要不超过31字节的总限制。

它的AD Type固定为0xFF,数据格式如下:

  • Company ID:2字节,由蓝牙SIG分配的厂商ID。比如Apple是0x004C,Google是0x00E0。
  • 自定义数据:N字节,你想放什么就放什么。
// 构造厂商自定义广播数据
byte[] manufacturerData = new byte[]{
    (byte) 0x4C,  // Company ID 低字节 (Apple)
    (byte) 0x00,  // Company ID 高字节
    (byte) 0x01,  // 自定义数据:设备类型
    (byte) 0x02,  // 自定义数据:固件版本
    (byte) 0x03   // 自定义数据:状态标志
};

// 封装成AD Structure
byte[] adStructure = new byte[manufacturerData.length + 2];
adStructure[0] = (byte) (manufacturerData.length + 1);  // 长度
adStructure[1] = (byte) 0xFF;  // AD Type
System.arraycopy(manufacturerData, 0, adStructure, 2, manufacturerData.length);

我个人的习惯是:自定义数据的前2个字节放一个魔数(比如0xCA 0xFE),用来快速识别是不是自家设备。这样扫描端收到广播后,先检查魔数,不是就直接跳过,效率高很多。

避坑指南:如果你没有申请蓝牙SIG的Company ID,可以用0xFFFF作为测试用厂商ID。但正式产品一定要申请正式ID,否则可能被蓝牙认证拒绝。我曾经因为偷懒用了0xFFFF,结果送检时被打了回来……

12.6 完整广播流程示例

把上面所有知识点串起来,一个完整的广播启动流程是这样的:

private void startAdvertising() {
    BluetoothLeAdvertiser advertiser = BluetoothAdapter.getDefaultAdapter()
        .getBluetoothLeAdvertiser();
    if (advertiser == null) {
        Log.e(TAG, "设备不支持BLE广播");
        return;
    }

    // 1. 构造广播数据
    AdvertiseData data = new AdvertiseData.Builder()
        .addServiceUuid(ParcelUuid.fromString("0000180F-0000-1000-8000-00805F9B34FB"))
        .setIncludeDeviceName(true)
        .addManufacturerData(0x00E0, new byte[]{0x01, 0x02, 0x03})
        .build();

    // 2. 配置广播设置
    AdvertiseSettings settings = new AdvertiseSettings.Builder()
        .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
        .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
        .setConnectable(true)
        .build();

    // 3. 启动广播
    advertiser.startAdvertising(settings, data, mCallback);
}

嗯,这里要注意:addServiceUuid和addManufacturerData不能同时放太多数据,因为广播包总大小限制是31字节。如果数据超了,系统会报ADVERTISE_FAILED_DATA_TOO_LARGE错误。

下面我用一张图来总结本章的核心知识体系:

BLE广播开发知识体系 BLE广播 广播数据格式 Length + Type + Data 广播类型 可连接 / 不可连接 广播设置 模式 / 功率 / 超时 广播回调 onStartSuccess / onStartFailure 厂商自定义广播包 Company ID + 自定义数据 核心限制:广播包总大小 ≤ 31字节 Android 8.0+ 后台广播限制 | Android 12+ 需 BLUETOOTH_ADVERTISE 权限

这张图把本章的核心知识点都串起来了。你想想看,从广播数据格式到厂商自定义,其实就是一个从「通用」到「专用」的过程。掌握了这些,你就能应对绝大多数BLE广播场景了。

好了,关于BLE广播开发的内容就讲到这里。记住,广播是BLE通信的起点,也是最容易出问题的地方。多动手写代码,多在实际设备上测试,慢慢就会找到感觉。


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