- 在android,依照协议,虽然定时包可以最多31字节,但app其实最多只能28字节。扫描回复包则是31字节。
- Bluetooth SIG关于16-bit UUID的已分配列表:Assigned Numbers下的“16-bit UUIDS“(pdf文件)。

图1来自“蓝牙GATT协议介绍[1] ”,描述了广播阶段各数据包流向。外围设备(peripheral)会设定一个广播间隔(Advertising interval),每个广播间隔中,它会重新发送自己的定时包(Advertising data)。广播间隔越长,越省电,同时也不太容易扫描到。中心设备(center)扫描到一个定时包后,可以向peripheral请求扫描回复,方法是向peripherl发扫描回复请求包(Scan response request)。peripherl收到这个扫描回复请求包后,向该center发扫描回复包(Scan response data)。
测试下来,在center端,不论android还是ios,扫描到定时包、发出扫描回复请求包、接收到扫描回复包,这一系列过程,对app代码都是透明的。app代码看到的已是收到“定时包+扫描回复包”后的数据。换回话说,center扫描到peripheral,调用app设置的回调函数,onScanResult(andorid)或didDiscoverPeripheral(ios),在这两函数得到的总是“定时包+扫描回复包”后数据。所以某种内容是放在定时包还是扫描回复包,对center app编程没影响。
- 参数包。peripherl启动广播阶段设置到ble硬件的三个包之一。用于告知ble硬件如何发送,像数据包发送间隔、用哪些信道。center不会收到这个包。
- 广播数据包(定时包)。peripherl启动广播阶段设置到ble硬件的三个包之一。ble硬件依着参数包设置的发送间隔主动、定时发这个包。为方便叙述,此文把它称为定时包。
- 扫描回复数据包。peripherl启动广播阶段设置到ble硬件的三个包之一。peripherl收到center发来扫描回复请求包后,向该center发这种包。
有人会问,都是在广播阶段peripherl发向center的数据,为什么要拆分为定时包和扫描回复包。以下是我的理解。
- 蓝牙要保证低功耗,意味着定时包中数据不能太多,于是限定了最多31字节。
- 一些应用需要超过31字节,再增加种扫描回复包,它是收到center发来的扫描回复请求包才发的,发送频率大大低于定时包。
- 拆分成两个包后,间接实现了用更低功耗(相比只有定时包)实现了center能收到62字节的数据。
- 代码上,不论peripherl还是center,都用同一种数据结构封装定时包和扫描回复包,像android center用的是AdvertiseData。对要发送给center某种内容,像名称(name),ManufacturerData、ServiceUUID等,可以按自个需要,既可放在定时包,也可放在扫描回复包。
- 基于定时包、扫描回复包的发送频率,为降低功耗,理论上数据应该少放定时包,多放到扫描回复包。
- 为让center扫描到peripherl,peripherl必须发送定时包,所以定时包是必须的。如果该peripherl广播数据少,一个定时包就够了,那可以不用扫描回复包,所以扫描回复包是可选的。
本文用一个实例描述BLE广播中的数据包,包括参数包、定时包和扫描回复包。
一、ble center代码
private BluetoothManager mBluetoothManager = (BluetoothManager)mActivity.getSystemService(Context.BLUETOOTH_SERVICE); private BluetoothAdapter mBluetoothAdapter = mBluetoothManager.getAdapter(); private UUID UUID_SERVER = UUID.fromString(UuidUtils.uuid16To128("5356")); public void startAdvertiser(String name, int manufacturerId, byte[] manufacturerData) { // 严格来说,在android-12,应该使用startAdvertisingSet代替startAdvertising, // startAdvertising内部其实是调用startAdvertisingSet if (name != null && !name.isEmpty()) { mBluetoothAdapter.setName(name); } else { Log.d(TAG,"you don't want change ble peripheral name"); } // advertiseMode roc-rk3588s-pc // ADVERTISE_MODE_LOW_LATENCY ==> (160ms, 210ms) // ADVERTISE_MODE_BALANCED ==> (400ms, 450ms) // ADVERTISE_MODE_LOW_POWER ==> (1600ms--1650ms) AdvertiseSettings settings = new AdvertiseSettings.Builder() .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) // 设置最大广播间隔(最低功耗), .setConnectable(true) .setTimeout(0) .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) .build(); AdvertiseData.Builder builder = new AdvertiseData.Builder() .setIncludeDeviceName(true) .setIncludeTxPowerLevel(true); if (manufacturerData != null) { // manufacturerId: 65520(0xfff0) // manufacturerData: 6b:6f:73:2d:64:65:76:69:63:65 builder.addManufacturerData(manufacturerId, manufacturerData); } // advertiseData: 定时包 AdvertiseData advertiseData = builder.build(); // scanResponseData: 扫描回复包。可以看到,封装它和定时包都是AdvertiseData。 AdvertiseData scanResponseData = new AdvertiseData.Builder() .addServiceUuid(new ParcelUuid(UUID_SERVER)) .setIncludeTxPowerLevel(true) .build(); BluetoothLeAdvertiser bluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser(); if (bluetoothLeAdvertiser == null){ Log.d(TAG,"设备不支持广播蓝牙"); return; } // startAdvertising将导致bt模块开始下文要说的四个步骤。 bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, scanResponseData, callback); }
mBluetoothAdapter.setName(name)用于修改主板上蓝牙设备名称(peripheral name),个人不建议app修改这名称。android任何时刻似乎只能有一个peripheral name。也就是说,一旦一个app改了,会影响其它app,谁都争抢着改, 那这个改名还有啥作用。
封装定时包和扫描回复包的都是AdvertiseData。示例中两种都调用“setIncludeTxPowerLevel(true)”,即都包含TxPowerLevel,为省功耗,使用时应该只设一个就行。
包类型 | 操作 | 值 | 功能 |
参数包 | setAdvertiseMode | ADVERTISE_MODE_LOW_POWER | 广播包发送播间隔 |
参数包 | setConnectable | true | center可连接该peripheral |
参数包 | setTimeout | 0 | |
参数包 | setTxPowerLevel | ADVERTISE_TX_POWER_HIGH | |
定时包 | setIncludeDeviceName | true | 定时包包含名称:rk3588 |
定时包 | setIncludeTxPowerLevel | true | 定时包包含TxPowerLevel |
定时包 | addManufacturerData | manufacturerId, manufacturerData | 定时包包含ManufacturerData |
扫描回复包 | addServiceUuid | 5356 | 扫描回复包包含16位的UUID |
扫描回复包 | setIncludeTxPowerLeve | true | 扫描回复包包含TxPowerLevel |
二、peripheral启动广播阶段
“低功耗蓝牙BLE传统广播总结[2] ”,这篇文章结合BLE协议,很好总结了如何启动蓝牙广播,在阅读下文前,建议先看它。它把启动广播流程分为四个步骤:设置广播参数、设置广播数据、设置扫描回复数据、使能广播。本文依照这分法,看各个步骤发给HCI的包数据。在执行时机上,app执行上面的“bluetoothLeAdvertiser.startAdvertising(...)”,便会促使aosp的bt模块执行这四个步骤。
编程时,三种包只需要在启动广播时发送一次。也就是说,虽然蓝牙硬件每隔advertising interval会发一次定时包,但只要这里向硬件设置一次,之后硬件会按这里设置的值一次次广播。
2.1 步骤1:设置广播参数
此步骤生成参数包,并发向HCI。
<aosp>/system/bt/stack/btm/ble_advertiser_hci_interface.cc ------ class BleAdvertiserLegacyHciInterfaceImpl : public BleAdvertiserHciInterface { void SetParameters(uint8_t handle, uint16_t properties, uint32_t adv_int_min, uint32_t adv_int_max, uint8_t channel_map, uint8_t own_address_type, const RawAddress& /* own_address */, uint8_t peer_address_type, const RawAddress& peer_address, uint8_t filter_policy, int8_t tx_power, uint8_t primary_phy, uint8_t secondary_max_skip, uint8_t secondary_phy, uint8_t advertising_sid, uint8_t scan_request_notify_enable, parameters_cb command_complete) override };
- 命令:HCI_BLE_WRITE_ADV_PARAMS (OCF:0x0006)
- 字节数:HCIC_PARAM_SIZE_BLE_WRITE_ADV_PARAMS(15)
针对示例,发向HCI的参数包(15字节) 40:06:72:06:00:00:00:00:00:00:00:00:00:07:00
变量 | 字节数 | 数值 |
adv_int_min | 2 | 1600(0x640) |
adv_int_max | 2 | 1650(0x672) |
properties | 1 | 0x13 --> (type:0x0) |
own_address_type | 1 | 0x0 |
peer_address_type | 1 | 0x0 |
peer_address | 6(BD_ADDR_LEN) | 00:00:00:00:00:00 |
channel_map | 1 | 0x7 |
filter_policy | 1 | 0x0 |
在Firfly ROC-RK3588S-PC主板,三种表示间隔的宏对应的时间间隔。三种间隔的最大值都比最小值大50ms。
ADVERTISE_MODE_LOW_LATENCY | (160, 210) |
ADVERTISE_MODE_BALANCED | (400, 450) |
ADVERTISE_MODE_LOW_POWER | (1600--1650) |
2.2 步骤2:设置广播数据
此步骤生成定时包,并发向HCI。
class BleAdvertiserLegacyHciInterfaceImpl : public BleAdvertiserHciInterface { void SetAdvertisingData(uint8_t handle, uint8_t operation, uint8_t fragment_preference, uint8_t data_length, uint8_t* data, status_cb command_complete) override { // 参数[data, data_length]是下文“阶段2:在advDataBytes前补上3字节”后的数据 uint8_t param[HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA + 1]; if (data_length > HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA) { // 一旦之前生成的data_length超过31字节,那接下只发送前面的31字节 data_length = HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA; } // 对应下面“阶段3:补足32字节”。 uint8_t* pp = param; memset(pp, 0, HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA + 1); UINT8_TO_STREAM(pp, data_length); ARRAY_TO_STREAM(pp, data, data_length); SendAdvCmd(FROM_HERE, HCI_BLE_WRITE_ADV_DATA, param, HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA + 1, command_complete); } };
- 命令:HCI_BLE_WRITE_ADV_PARAMS (OCF: 0x0008)
- 字节数:HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA(31) + 1
针对示例,发向HCI的定时包(32字节)f0:ff:6b:6f:73:2d:64:65:76:69:63:65是ManufacturerData 1c:02:01:02:07:09:72:6b:33:35:38:38:0d:ff:f0:ff:6b:6f:73:2d:64:65:76:69:63:65:02:0a:00:00:00:00
如何生成这32个字节,可分为3个阶段。先列下三个阶段的字节如何变化,再分阶段具体说。
(25) 07:09:72:6b:33:35:38:38:0d:ff:f0:ff:6b:6f:73:2d:64:65:76:69:63:65:02:0a:00 <-(java)advDataBytes (28) 02:01:02:07:09:72:6b:33:35:38:38:0d:ff:f0:ff:6b:6f:73:2d:64:65:76:69:63:65:02:0a:00 <-经过SetData(...)后 02是固定值,表示后面是2个字节。 01:表示类型HCI_EIR_FLAGS_TYPE。值等于HCI_EIR_FLAGS_TYPE = 1。 02:表示flags_val。值BTM_GENERAL_DISCOVERABLE(2),如果p_inst->duration不是0,值变成p_inst->duration(1)。 (32)1c:02:01:02:07:09:72:6b:33:35:38:38:0d:ff:f0:ff:6b:6f:73:2d:64:65:76:69:63:65:02:0a:00:00:00:00 补的方法是前面补一个表示长度的字节,值就是28(0x1c)。后面用0补剩余字节,31-28=3,要补3个0。
阶段1:生成25字节的advDataBytes
<aosp>/ackages/apps/Bluetooth/src/com/android/bluetooth/gatt/AdvertiseManager.java ------ void startAdvertisingSet(...) { ...... byte[] advDataBytes = AdvertiseHelper.advertiseDataToBytes(advertiseData, deviceName); ...... }
25字节的advDataBytes
07:09:72:6b:33:35:38:38: 0d:ff:f0:ff:6b:6f:73:2d:64:65:76:69:63:65: 02:0a:00
advDataBytes包含三种内容:名称、manufacturerData、TxPowerLevel。每种内容的第一个字节用于表示该内容的字节数。
内容1:名称
07:(1字节)1+6
09:(1字节)COMPLETE_LOCAL_NAME
72:6b:33:35:38:38:(6字节)rk3588
内容2:manufacturerData
0d:(1字节)1+12
ff:(1字节)MANUFACTURER_SPECIFIC_DATA
f0:ff:6b:6f:73:2d:64:65:76:69:63:65:(12字节)manufacturerData
内容3:TxPowerLevel
02:(1字节)1+1
0a:(1字节)TX_POWER_LEVEL=10
00:(1字节)lower layers will fill this value.
阶段2:在advDataBytes前补上3字节
<aosp>/system/bt/stack/btm/btm_ble_multi_adv.cc ------ void SetData(uint8_t inst_id, bool is_scan_rsp, std::vector<uint8_t> data, MultiAdvCb cb) override { ...... if (!is_scan_rsp && is_connectable(p_inst->advertising_event_properties)) { // 定时包以及后面的扫描回复包都会调用SetData,is_scan_rsp指示此时是否是扫描回复包。 // 定时包时is_scan_rsp是false,会进这入口。 uint8_t flags_val = BTM_GENERAL_DISCOVERABLE; if (p_inst->duration) flags_val = BTM_LIMITED_DISCOVERABLE; std::vector<uint8_t> flags; flags.push_back(2); // length flags.push_back(HCI_EIR_FLAGS_TYPE); flags.push_back(flags_val); data.insert(data.begin(), flags.begin(), flags.end()); } ...... }
定时包以及后面的扫描回复包都会调用SetData,is_scan_rsp指示此时是否是扫描回复包。在生成定时包时,is_scan_rsp是false,会进入if入口,在advDataBytes前补上3字节。
阶段3:补足32字节
经过阶段2,内容就是我们通常意义上的广播包,不能超过31字节(HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA)。在第3阶段,要补足到32字节(HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA + 1)。补的方法是前面补一个表示的长度的字节,后面用0补剩余字节。前面一个字节的语义是广播包的字节数。示例是0x1c,表示广播包中有效的字节数是28字节。后面补的都是0,31-28=3,要补3个0。
2.3 步骤4:设置扫描回复数据
此步骤生成扫描回复包,并发向HCI。
class BleAdvertiserLegacyHciInterfaceImpl : public BleAdvertiserHciInterface { void SetScanResponseData(uint8_t handle, uint8_t operation, uint8_t fragment_preference, uint8_t scan_response_data_length, uint8_t* scan_response_data, status_cb command_complete) override };
SetScanResponseData逻辑类似SetAdvertisingData。
- 命令:HCI_BLE_WRITE_SCAN_RSP_DATA (OCF: 0x0009)
- 字节数:HCIC_PARAM_SIZE_BLE_WRITE_ADV_DATA(31) + 1
针对示例,发向HCI的扫描回复包(32字节) 07:02:0a:00:03:03:56:53:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
在aosp的bt模块,是以和定时包一样的流程生成扫描回复包,只是这时的is_scan_rsp是true。于是也可以将生成过程分为三个阶段。
阶段1:生成7字节的scanResponseBytes
<aosp>/ackages/apps/Bluetooth/src/com/android/bluetooth/gatt/AdvertiseManager.java ------ void startAdvertisingSet(...) { ...... byte[] scanResponseBytes = AdvertiseHelper.advertiseDataToBytes(scanResponse, deviceName); ...... }
7字节的scanResponseBytes
02:0a:00: 03:03:56:53
scanResponseBytes包含两种内容:TxPowerLevel、uuid。每种内容的第一个字节用于表示该内容的字节数。
内容1:TxPowerLevel
02:(1字节)1+1
0a:(1字节)TX_POWER_LEVEL=10
00:(1字节)lower layers will fill this value.
内容2:uuid
03:07:(1字节)1+2
03:COMPLETE_LIST_16_BIT_SERVICE_UUIDS(3)
00:fd:uuid值:fd00
阶段2:不在scanResponseBytes补字节
由于is_scan_rsp是true,不在scanResponseBytes补字节
阶段3:补足32字节
补的方法是前面补一个表示长度的字节,值就是31。后面用0补剩余字节。
2.4 步骤4:使能广播
步骤4把启动广播命令发向HCI,收到它后,蓝牙硬件开始广播。
class BleAdvertiserLegacyHciInterfaceImpl : public BleAdvertiserHciInterface { void Enable(uint8_t enable, std::vector<SetEnableData> sets, status_cb command_complete) }
- 命令:HCIC_PARAM_SIZE_WRITE_ADV_ENABLE (OCF: 0x000A)
- 字节数:HCIC_PARAM_SIZE_WRITE_ADV_ENABLE(1)
0:结束广播
1:开始广播
三、重新设置定时包、扫描回复包
上面在设置定时包、扫描回复包存在几个缺陷。
- 定时包快到满额31字节,而扫描回复包较空,导致功耗相对来说较大。
- 在定时包,ManufacturerData要求的字节数是固定的,一旦遇到个较长name,就超过了31字节(android实际最大是28字节)。
- 重复发送TxPowerLevel。
既然知道缺陷,优化下定时包、扫描回复包构造代码。
AdvertiseData advertiseData = new AdvertiseData.Builder() .setIncludeDeviceName(true) .setIncludeTxPowerLevel(true).build(); AdvertiseData.Builder builder = new AdvertiseData.Builder() .addServiceUuid(new ParcelUuid(UUID_SERVER)); if (manufacturerData != null) { builder.addManufacturerData(manufacturerId, manufacturerData); } AdvertiseData scanResponseData = builder.build();
相对于之前代码,ManufacturerData放到了扫描回复包,TxPowerLevel只发送一次。以下是此时发向HCI的包数据。
参数包(15)
40:06:72:06:00:00:00:00:00:00:00:00:00:07:00
定时包(32)
(11) 07:09:72:6b:33:35:38:38:02:0a:00 <-(java)advDataBytes (14) 02:01:02:07:09:72:6b:33:35:38:38:02:0a:00 <-经过SetData(...)后 (32)0e:02:01:02:07:09:72:6b:33:35:38:38:02:0a:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
扫描回复包(32)
(18) 0d:ff:f0:ff:6b:6f:73:2d:64:65:76:69:63:65:03:03:56:53 <-(java)scanResponseBytes (18) 0d:ff:f0:ff:6b:6f:73:2d:64:65:76:69:63:65:03:03:56:53 <-经过SetData(...)后 (32)12:0d:ff:f0:ff:6b:6f:73:2d:64:65:76:69:63:65:03:03:56:53:00:00:00:00:00:00:00:00:00:00:00:00:00
修改后,定时包减少到14字节,扫描回复包增加到18字节。即使遇到十多字节长的name,定时包也不会溢出。而对于center来说,它看到的结果和之前是一样的。