导语:Beacon 是 BLE 里一类"只发不收"的设备——它不建立连接,只是周期性广播一段特定格式的数据。最常见的两种标准是 Apple 的 iBeacon 和 Google 的 Eddystone。本文拆解 iBeacon 的完整字节布局、接收端解析方法、与 Eddystone 的差异,以及工程落地时最容易踩的坑。
注意:Beacon 是"广播(Advertising)"层面的东西,不是 GATT Service。它不出现在 GATT 数据库里,不能用
gattc_read去读——这点常被初学者搞混。
Beacon(信标)泛指一类"以固定周期广播唯一标识"的 BLE 设备。它的核心特征是:
iBeacon 和 Eddystone 都只是"广播包里那段数据的格式定义",不是新协议、不是新物理层。
iBeacon 把标识塞进 Manufacturer Specific Data(AD Type 0xFF),并打上 Apple 的 Company ID(0x004C)。一个典型完整包(hex):
02 01 06 1A FF 4C 00 02 15
FB 0B 57 A2 82 28 44 CD 91 3A 94 A1 22 BA 12 06 00
01 00 02 D1 00
逐字节拆解(LTV = Length-Type-Value):
| 偏移 | 字节 | 含义 |
|---|---|---|
| 0 | 02 |
AD 结构长度(本结构 2 字节:类型+值) |
| 1 | 01 |
AD Type:Flags |
| 2 | 06 |
LE General Discoverable + BR/EDR Not Supported |
| 3 | 1A |
AD 结构长度(26 字节,从下一字节起到结尾) |
| 4 | FF |
AD Type:Manufacturer Specific Data |
| 5–6 | 4C 00 |
Company ID:Apple(0x004C),小端序 |
| 7 | 02 |
iBeacon 子类型(Apple 分配) |
| 8 | 15 |
iBeacon 数据长度(0x15 = 21 字节,即后面 UUID+Major+Minor+TxPower 总长) |
| 9–24 | 16 字节 | Proximity UUID(大端序,128-bit) |
| 25–26 | 2 字节 | Major(大端序,uint16) |
| 27–28 | 2 字节 | Minor(大端序,uint16) |
| 29 | 1 字节 | Tx Power(有符号 int8,1 米处的参考 RSSI,单位 dBm) |
关键点:
0x4C 00 是 Apple 在 SIG 注册的 Company ID,写成小端(低字节在前)。其它厂商做 iBeacon 兼容信标时必须 用这个值,否则 iOS 不认。02 15 = 子类型 02 + 数据长度 15,合起来是 Apple iBeacon 的固定前缀。0xC5)。接收端用它和实测 RSSI 做差,估算距离。这个值是设备出厂时校准写死的,不是动态测的。距离估算不是 iBeacon 协议规定的,而是 App 端基于 RSSI 的路径损耗模型(如
d = 10^((TxPower − RSSI)/(10×n)),n 为环境衰减系数,室内约 2~4)算出来的。iBeacon 只负责把 Tx Power 广播出去。
扫描拿到原始 advertisement_data,过滤出 0xFF 段,验证 Company ID == 0x004C、子类型 == 0x02,再按偏移拆字段。下面是一段可直接用的 Python 解析函数:
import struct
def parse_ibeacon(mfr_data: bytes):
"""mfr_data 是 AD Type 0xFF 的 value(不含前面的 length/type 字节)。"""
if len(mfr_data) < 25:
return None
company = mfr_data[0] | (mfr_data[1] << 8) # 小端
if company != 0x004C:
return None
subtype = mfr_data[2]
if subtype != 0x02:
return None
# 0x15 = 21 字节数据长度;校验总长
data = mfr_data[4:4 + 21]
if len(data) < 21:
return None
raw = data[0:16].hex()
uuid = f"{raw[0:8]}-{raw[8:12]}-{raw[12:16]}-{raw[16:20]}-{raw[20:32]}"
major, minor = struct.unpack(">HH", data[16:20])
tx_power = struct.unpack(">b", data[20:21])[0]
return {"uuid": uuid, "major": major, "minor": minor, "tx_power": tx_power}
# 解析前面示例包(注意 raw 已含 Company ID 0x4C00 + 子类型 02 15)
raw = bytes.fromhex("4C00" + "0215" +
"FB0B57A2822844CD913A94A122BA1206" + "0100" + "02D1" + "00")
print(parse_ibeacon(raw))
# -> {'uuid': 'fb0b57a2-8228-44cd-913a-94a122ba1206', 'major': 256, 'minor': 721, 'tx_power': 0}
注意:示例里的 Tx Power = 0x00 是占位,真实部署必须填校准值(如 0xC5 = -59)。
Eddystone 不用 Company ID,而是把 0xFEAA 放进 Service Data(AD Type 0x16) 或 "Complete List of 16-bit Service UUIDs(0x03 / 0x07)"。它定义了多种帧(frame):
| 帧类型 | 用途 |
|---|---|
| UID | 10 字节(Namespace 10 + Instance 6)标识,类似 iBeacon 但更紧凑 |
| URL | 直接广播一个压缩 URL,手机点开即跳网页,无需 App |
| TLM | 遥测帧:电池电压、温度、上电秒数、广播计数,给运维用 |
| EID | 短暂可变的加密 ID,用于隐私 / 防追踪场景 |
Eddystone-URL 是最大的差异点:它让"扫码"变成"靠近即推送网页",对没有 App 的用户也友好。
| 维度 | iBeacon | Eddystone |
|---|---|---|
| 提出方 | Apple | |
| 标识载体 | Manufacturer Data 0xFF + Company 0x004C | Service UUID 0xFEAA + Service Data |
| 标识结构 | UUID + Major + Minor | UID / URL / TLM / EID 多帧 |
| 是否需 App | 一般需要(iOS 原生 CoreLocation 支持) | URL 帧可免 App 直接打开网页 |
| 遥测 | 无 | 有 TLM |
| 隐私 | 固定 UUID 易被追踪 | 有 EID 临时 ID |
02 01 06。iOS 26(含 iPhone 17)开始对缺失 Flags 的广播直接忽略,很多老模组因此"看不见"。ADV_NONCONN_IND(不可连接、可扫描)或 ADV_SCAN_IND,别用可连接类型。