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 应用将更加健壮和兼容。