ESP32 低功耗蓝牙广播包中自定义厂商数据的编码与解析:兼容 iOS/Android 的细节

一、BLE 广播包结构基础

BLE 广播包(Advertising Packet)由若干 AD Structure(Advertising Data Structure)组成,每个结构包含:

  • 长度字节(Length):表示后续数据的总字节数(Type + Data)。
  • 类型字节(Type):标识数据类型,例如 0x01(Flags)、0xFF(Manufacturer Specific Data)等。
  • 数据字节(Data):具体内容,长度可变。

对于厂商自定义数据,Type 固定为 0xFF。其 Data 部分必须包含:

  • Company Identifier(2 字节):由 Bluetooth SIG 分配的厂商 ID,例如 0x004C 为 Apple,0x0059 为 Nordic,0xFFFF 为测试用。
  • 自定义数据(0~28 字节):用户自由定义的内容。

注意:广播包总长度(含所有 AD Structure)不能超过 31 字节(传统广播)或 255 字节(扩展广播,但受限于实际 PHY)。ESP32 默认使用传统广播,因此自定义数据长度需严格控制。

二、iOS 与 Android 的解析差异

2.1 iOS 的 CoreBluetooth 限制

  • iOS 只能通过 CBAdvertisementDataManufacturerDataKey 获取厂商数据,但该 key 仅在设备作为 peripheral 广播时,central 端才能读取。
  • iOS 对广播包中的厂商数据有严格校验:Company ID 必须有效(非 0xFFFF),且数据长度不能超过 28 字节(因为广播包总长 31 字节,减去 3 字节头部)。
  • iOS 不支持读取广播包中的自定义服务 UUID(除非使用标准服务),因此厂商数据是主要透传渠道。

2.2 Android 的 BLE 扫描

  • Android 通过 ScanRecord.getManufacturerSpecificData() 获取厂商数据,返回一个 SparseArray,key 为 Company ID。
  • Android 对长度限制较宽松,但同样受广播包总长限制。
  • Android 允许 Company ID 为 0xFFFF,但 iOS 会忽略该数据,因此若需跨平台,建议使用真实厂商 ID(如 0x1234 自定义,但需注意 SIG 保留范围)。

关键细节:iOS 要求 Company ID 必须是大端序(Big-Endian),而 Android 在解析时也按大端序处理,但某些第三方库可能按小端序,因此编码时务必统一。

三、ESP32 端编码实现(Arduino 框架)

3.1 配置 BLE 广播数据

使用 BLEAdvertising 类设置广播数据。以下代码演示如何将传感器数据(如温度、湿度)编码为厂商数据并广播。

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

// 自定义厂商 ID(例如 0x1234,需自行申请或使用测试值)
#define COMPANY_ID 0x1234

void setup() {
  Serial.begin(115200);
  BLEDevice::init("ESP32_BLE");
  
  // 创建广播对象
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  
  // 准备厂商数据(例如:温度 25.6°C,湿度 60%)
  uint8_t temp = 256; // 实际值需转换,这里简化
  uint8_t hum = 60;
  
  // 构建厂商数据数组:Company ID (2字节) + 自定义数据
  uint8_t manData[4];
  manData[0] = (COMPANY_ID >> 8) & 0xFF; // 高字节
  manData[1] = COMPANY_ID & 0xFF;        // 低字节
  manData[2] = temp;                     // 温度整数部分
  manData[3] = hum;                      // 湿度
  
  // 创建 AD Structure 数据包
  BLEAdvertisementData advData;
  advData.setManufacturerData(std::string((char*)manData, sizeof(manData)));
  
  // 设置广播数据(可同时设置名称、服务 UUID 等)
  pAdvertising->setAdvertisementData(advData);
  
  // 启动广播
  pAdvertising->start();
  Serial.println("Broadcasting with manufacturer data");
}

void loop() {
  // 可定期更新广播数据
  delay(10000);
}

3.2 动态更新广播数据

实际项目中,传感器数据会变化,需定期更新广播内容。注意:更新广播数据前需停止广播,重新设置后再启动。

void updateBroadcast(uint8_t temp, uint8_t hum) {
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->stop();
  
  uint8_t manData[4];
  manData[0] = (COMPANY_ID >> 8) & 0xFF;
  manData[1] = COMPANY_ID & 0xFF;
  manData[2] = temp;
  manData[3] = hum;
  
  BLEAdvertisementData advData;
  advData.setManufacturerData(std::string((char*)manData, sizeof(manData)));
  pAdvertising->setAdvertisementData(advData);
  pAdvertising->start();
}

四、解析端代码示例(Android / iOS)

4.1 Android 解析(Kotlin)

val scanResult = result // ScanResult 对象
val record = scanResult.scanRecord
val manufacturerData = record?.getManufacturerSpecificData(0x1234)
if (manufacturerData != null) {
    val temp = manufacturerData[0].toInt() // 注意:数组索引从0开始,但厂商数据中前两字节是 Company ID,所以实际数据从索引2开始
    // 但 getManufacturerSpecificData 返回的是去掉 Company ID 后的数据,因此直接取索引0
    val temp = manufacturerData[0]
    val hum = manufacturerData[1]
    Log.d("BLE", "Temp: $temp, Hum: $hum")
}

注意:Android 的 getManufacturerSpecificData() 返回的字节数组已自动跳过 Company ID,因此直接读取自定义部分。

4.2 iOS 解析(Swift)

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
    if let manData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data {
        // 前两个字节是 Company ID(小端序?需注意)
        let companyID = manData.prefix(2).reduce(0) { ($0 << 8) + UInt16($1) } // 大端序
        if companyID == 0x1234 {
            let temp = manData[2] // 自定义数据从索引2开始
            let hum = manData[3]
            print("Temp: \(temp), Hum: \(hum)")
        }
    }
}

注意:iOS 返回的 Data 包含完整的厂商数据(含 Company ID),且 Company ID 为大端序,需手动转换。

五、注意事项与常见坑

  • 长度限制:传统广播包最大 31 字节,若包含设备名称(默认 9 字节)、Flags(3 字节),则厂商数据最多约 19 字节。建议精简设备名称或使用扩展广播(ESP32 支持)。
  • 字节序:Company ID 必须大端序,自定义数据建议统一大端序,避免跨平台解析混乱。
  • iOS 兼容性:避免使用 0xFFFF 作为 Company ID,否则 iOS 会忽略整个厂商数据。
  • 广播间隔:频繁更新广播数据会增加功耗,建议根据业务需求调整(如 100ms~1s)。
  • 扫描响应包:若数据量较大,可使用扫描响应包(Scan Response)扩展,但需在连接前发送,且同样受 31 字节限制。
  • 调试工具:使用 nRF Connect 或 LightBlue 工具查看广播包,验证编码是否正确。

六、总结

本文详细介绍了 ESP32 低功耗蓝牙广播包中厂商数据的编码与解析,重点强调了 iOS 与 Android 的差异。通过合理设计数据结构、严格遵循字节序和长度限制,可以轻松实现跨平台的数据透传。实际开发中,建议先使用调试工具验证广播内容,再编写解析代码,以减少调试时间。

掌握这些细节,你的嵌入式 BLE 应用将更加健壮和兼容。