平台差异
Android 与 iOS 平台的 BLE 广播实现差异
概述
由于 iOS CoreBluetooth 框架的限制,Android 和 iOS 平台使用截然不同的数据编码方式发送 BLE 广播指令。
| 平台 | 编码方式 | 数据格式 | 限制 |
|---|---|---|---|
| Android | Manufacturer Specific Data | 原始字节数组 | 无特殊限制 |
| iOS | Service UUID 列表 | 13 个 16-bit UUID 编码 | 不支持自定义 Manufacturer Data |
Android 编码
Android 直接使用 BluetoothLeAdvertiser 的 Manufacturer Specific Data 携带命令字节,编码简单直接。
编码流程
Hex 命令字符串
│
▼
hexStringToBytes() 转为字节数组
│
▼
写入 Manufacturer Data (ID=255)
│
▼
BLE Advertisement 发送代码实现
// Android 原生层实现
AdvertiseData data = new AdvertiseData.Builder()
.addManufacturerSpecificData(255, commandBytes)
.addServiceUuid(new ParcelUuid(SERVICE_UUID_FADE))
.build();广播参数
| 参数 | 值 | 说明 |
|---|---|---|
| Manufacturer ID | 255 (0x00FF) | 命令数据载荷标识 |
| Connectable | true | 可连接广播 |
| Timeout | 0 | 无限广播,由 App 控制停止 |
| Mode | LOW_LATENCY | 高功率低延迟 |
| TX Power | HIGH | 最大发射功率 |
| Service UUID | 0000FADE-... | 广播来源标识 |
iOS 编码
iOS 的 CBPeripheralManager 不支持自定义 Manufacturer Data 广播。系统使用算法编码方案,将命令数据映射到 13 个 16-bit Service UUID 中。
为什么需要不同的方案
iOS CoreBluetooth 框架对 Peripheral 广播有严格限制:
- 不支持 Manufacturer Data —
CBPeripheralManager的startAdvertising方法仅支持CBAdvertisementDataLocalNameKey和CBAdvertisementDataServiceUUIDsKey - 设备名称长度受限 — 不适合编码命令数据
- Service UUID 列表可用 — 可以携带多个 16-bit UUID,用于编码命令
因此,iOS 端将命令数据通过算法编码到 Service UUID 列表中,设备端同时支持解析 Manufacturer Data(Android)和 Service UUID 列表(iOS)。
UUID 列表结构
总计 13 个 16-bit UUID:
┌────────────────────────────┐
│ 固定前缀(4 个 UUID) │ 08F9, 2349, CBAE, D1C1
├────────────────────────────┤
│ 可变 UUID 1 (uuid5) │ 编码后的数据
├────────────────────────────┤
│ 可变 UUID 2 (uuid6) │ 编码后的数据
├────────────────────────────┤
│ 固定后缀(7 个 UUID) │ 0D0C, 0F0E, 1110, 1312,
│ │ 1514, 1716, 1918
└────────────────────────────┘- 固定前缀(4 个):用于设备端识别广播来源
- 可变 UUID(2 个):编码了实际的命令数据
- 固定后缀(7 个):填充结构,保持一致性
Default 设备编码算法
从命令字节数组的最后 3 个字节(A, B, C)计算两个可变 UUID。
输入
命令 Hex 字符串,例如: 6db643ce97fe427ce49c6c
最后 3 字节: A = 0xE4, B = 0x9C, C = 0x6C可变 UUID 2 (uuid6)
uuid6 = 0x0B00 | (C XOR 0x40)可变 UUID 1 (uuid5)
C = 命令最后一个字节
B = 命令倒数第二个字节
A = 命令倒数第三个字节
Hn = C >> 4 // C 的高 4 位
L = C & 0x0F // C 的低 4 位
A_high = A >> 4 // A 的高 4 位
// 模式判断
if A_high == 0xE → E-mode
if A_high == 0xF → F-mode
// E-mode 查找表
d_e = {
0x0: 0, 0x1: 0, 0x2: 1, 0x3: 15,
0x4: 14, 0x5: 14, 0x6: 3, 0x7: 1,
0xE: 7, 0xF: 5
}
// F-mode 查找表
d_f = {
0x1: 0, 0x3: 1, 0x5: 2,
0xE: 5, 0xF: 7
}
// 计算 UUID 字节
highByte = B XOR 0x89
lowByte = base | ((L + d(Hn)) % 16)
// E-mode: base = 0x60
// F-mode: base = 0x70
uuid5 = (highByte << 8) | lowByte完整示例
发送 Default 设备模式 1 指令 6db643ce97fe427ce49c6c:
最后 3 字节: A = 0xE4, B = 0x9C, C = 0x6C
--- uuid6 ---
uuid6 = 0x0B00 | (0x6C XOR 0x40)
= 0x0B00 | 0x2C
= 0x0B2C
--- uuid5 ---
C = 0x6C
Hn = 0x6C >> 4 = 0x6
L = 0x6C & 0x0F = 0xC
A_high = 0xE4 >> 4 = 0xE → E-mode
d_e[0x6] = 3
highByte = 0x9C XOR 0x89 = 0x15
lowByte = 0x60 | ((0xC + 3) % 16)
= 0x60 | 0xF
= 0x6F
uuid5 = (0x15 << 8) | 0x6F = 0x156F
--- 最终 UUID 列表 ---
08F9, 2349, CBAE, D1C1,
156F, ← 可变 UUID 1
0B2C, ← 可变 UUID 2
0D0C, 0F0E, 1110, 1312, 1514, 1716, 1918NNG 设备编码算法
NNG 设备使用不同的固定 UUID 前缀和简化的可变计算:
固定前缀 (4 个): 08F9, C949, A8C1, D1C1
固定后缀 (7 个): 0D0C, 0F0E, 1110, 1312, 1514, 1716, 1918
可变 UUID(从命令末尾字节 b1, b2, b3 计算):
uuid5 = ((b2 XOR 0x89) << 8) | (b1 XOR 0x8B)
uuid6 = 0x0B00 | (b3 XOR 0x40)权限要求
Android (API 31+)
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />Android 12(API 31)及以上版本需要在运行时动态请求上述权限。低于 API 31 的版本使用传统的 BLUETOOTH 和 BLUETOOTH_ADMIN 权限。
iOS
<!-- Info.plist -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限以控制设备</string>使用 CoreBluetooth 框架的 CBPeripheralManager 进行广播。iOS 系统在首次使用蓝牙时会弹出权限请求弹窗。
平台差异总结
| 维度 | Android | iOS |
|---|---|---|
| 编码方式 | Manufacturer Data (ID=255) | Service UUID 列表 (13 个) |
| 数据容量 | 直接携带原始字节 | 编码到 2 个可变 UUID 中 |
| 广播模式 | LOW_LATENCY(可配置) | 系统控制(不可配置) |
| TX 功率 | HIGH(可配置) | 系统控制(不可配置) |
| Connectable | true | true |
| 超时控制 | 由 App 控制(设为 0) | 由 App 控制(设为 0) |
| 权限 | BLUETOOTH_ADVERTISE (运行时) | NSBluetoothAlwaysUsageDescription |
| 原生 API | BluetoothLeAdvertiser | CBPeripheralManager |
| 设备端解析 | 读取 Manufacturer Data 字段 | 读取 Service UUID 列表并反向解码 |