BLE广播中的数据包

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

图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的数据,为什么要拆分为定时包和扫描回复包。以下是我的理解。

  1. 蓝牙要保证低功耗,意味着定时包中数据不能太多,于是限定了最多31字节。
  2. 一些应用需要超过31字节,再增加种扫描回复包,它是收到center发来的扫描回复请求包才发的,发送频率大大低于定时包。
  3. 拆分成两个包后,间接实现了用更低功耗(相比只有定时包)实现了center能收到62字节的数据。
  4. 代码上,不论peripherl还是center,都用同一种数据结构封装定时包和扫描回复包,像android center用的是AdvertiseData。对要发送给center某种内容,像名称(name),ManufacturerData、ServiceUUID等,可以按自个需要,既可放在定时包,也可放在扫描回复包。
  5. 基于定时包、扫描回复包的发送频率,为降低功耗,理论上数据应该少放定时包,多放到扫描回复包。
  6. 为让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,为省功耗,使用时应该只设一个就行。

包类型操作功能
参数包setAdvertiseModeADVERTISE_MODE_LOW_POWER 广播包发送播间隔
参数包setConnectabletruecenter可连接该peripheral
参数包setTimeout0 
参数包setTxPowerLevelADVERTISE_TX_POWER_HIGH 
定时包setIncludeDeviceNametrue定时包包含名称:rk3588
定时包setIncludeTxPowerLeveltrue定时包包含TxPowerLevel
定时包addManufacturerDatamanufacturerId, manufacturerData定时包包含ManufacturerData
扫描回复包addServiceUuid5356扫描回复包包含16位的UUID
扫描回复包setIncludeTxPowerLevetrue扫描回复包包含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_min21600(0x640)
adv_int_max21650(0x672)
properties10x13 --> (type:0x0)
own_address_type10x0
peer_address_type10x0
peer_address6(BD_ADDR_LEN)00:00:00:00:00:00
channel_map10x7
filter_policy10x0

在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:开始广播

 

三、重新设置定时包、扫描回复包

上面在设置定时包、扫描回复包存在几个缺陷。

  1. 定时包快到满额31字节,而扫描回复包较空,导致功耗相对来说较大。
  2. 在定时包,ManufacturerData要求的字节数是固定的,一旦遇到个较长name,就超过了31字节(android实际最大是28字节)。
  3. 重复发送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来说,它看到的结果和之前是一样的。

全部评论: 0

    写评论: