ESP32 低功耗蓝牙广播包中自定义厂商数据的编码与解析:兼容 iOS/Android 的细节
👁 2 阅读 · 2026-08-27 · 嵌入式
在物联网开发中,ESP32 作为低功耗蓝牙(BLE)外设时,经常需要在广播包中携带自定义厂商数据,以实现设备识别、传感器数据透传等功能。然而,iOS 和 Android 对广播包的解析规则存在差异,若编码不当,会导致数据无法被正确读取。本文深入剖析 BLE 广播包结构,重点讲解厂商数据(Manufacturer Specific Data)的编码格式、长度限制、以及兼容 iOS/Android 的细节,并提供 ESP32 端(Arduino 框架)的完整代码示例与解析注意事项,帮助开发者避免常见坑点。
# 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` 类设置广播数据。以下代码演示如何将传感器数据(如温度、湿度)编码为厂商数据并广播。
```c
#include
#include
#include
// 自定义厂商 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 动态更新广播数据
实际项目中,传感器数据会变化,需定期更新广播内容。注意:更新广播数据前需停止广播,重新设置后再启动。
```c
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)
```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)
```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 应用将更加健壮和兼容。