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值
| 类型值 | 含义 | 说明 |
|---|---|---|
| 0x01 | Flags | 广播模式标志,必填项 |
| 0x02/0x03 | 16位Service UUID | 完整/不完整列表 |
| 0x06/0x07 | 128位Service UUID | 完整/不完整列表 |
| 0x08/0x09 | Local Name | 短名称/完整名称 |
| 0xFF | Manufacturer 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广播开发的内容就讲到这里。记住,广播是BLE通信的起点,也是最容易出问题的地方。多动手写代码,多在实际设备上测试,慢慢就会找到感觉。