3、GAP广播与扫描:广播者与观察者角色、广播数据包格式、扫描请求与响应
好,咱们今天聊聊GAP层最基础、也最核心的两个角色——广播者和观察者。说白了,这就是蓝牙设备互相发现的过程。你想想看,两个设备要建立连接,总得先知道对方的存在吧?广播和扫描就是干这个活的。
3.1 广播者与观察者:谁在喊,谁在听?
在BLE的世界里,角色划分其实很简单:
- 广播者(Advertiser):周期性发送广播包,相当于在喊「我在这里!我在这里!」
- 观察者(Observer):被动监听广播包,相当于竖起耳朵听周围有没有人在喊。
我个人习惯把广播者想象成一个举着广告牌在街上走的人,观察者就是路过的行人。广播者不需要知道谁在听,观察者也不需要回应——除非它想进一步了解。
关键点:广播者和观察者之间没有连接。广播是单向的、无连接的通信方式。这是BLE低功耗特性的基石。
我在项目中遇到过一个问题:有个同事把广播间隔设得太短,结果设备功耗飙升。嗯,这里要注意——广播间隔越短,被发现得越快,但电池也掉得越快。这是个典型的trade-off。
3.2 广播数据包格式:里面到底装了啥?
广播包不是随便扔一堆字节出去就完事了。它有严格的格式要求。一个完整的广播包,结构是这样的:
| 字段 | 长度 | 说明 |
|---|---|---|
| 前导码(Preamble) | 1字节 | 用于接收端同步时钟 |
| 访问地址(Access Address) | 4字节 | 广播通道固定为0x8E89BED6 |
| PDU(Protocol Data Unit) | 2-39字节 | 真正的数据内容 |
| CRC | 3字节 | 校验用 |
咱们重点关注PDU部分。PDU里又分两种:
- 广播指示(ADV_IND):可连接的非定向广播,最常用
- 扫描响应(SCAN_RSP):对扫描请求的回复
PDU的数据部分,是由一个个AD Structure(广告数据结构)组成的。每个AD Structure长这样:
| Length (1字节) | AD Type (1字节) | AD Data (N字节) |
举个例子,如果你想让设备显示名字「MyDevice」,广播包里的AD Structure就是:
0x09 // Length = 9(1字节类型 + 8字节名字)
0x09 // AD Type = Complete Local Name
0x4D 0x79 0x44 0x65 0x76 0x69 0x63 0x65 // "MyDevice"的ASCII码
小技巧:我调试广播包时,常用nRF Connect或者Wireshark抓包看原始数据。看到一堆十六进制别慌,按AD Structure拆开读就行。
3.3 扫描请求与响应:你问我答
观察者光听广播还不够,有时候想了解更多信息。这时候就会发一个扫描请求(SCAN_REQ)。广播者收到后,可以回复一个扫描响应(SCAN_RSP)。
为什么会这样设计?你想想看,广播包只有31字节的有效载荷,装不下太多东西。所以通常的做法是:
- 广播包:放最重要的信息,比如设备地址、是否可连接
- 扫描响应:放次要信息,比如设备全名、支持的服务UUID
我曾经踩过一个坑:有个设备广播包里放了完整的设备名,结果扫描响应里又放了一遍。浪费啊!广播包空间那么宝贵,应该把名字挪到扫描响应里,广播包腾出位置放服务UUID才对。
扫描请求和响应的流程是这样的:
- 广播者发送ADV_IND广播包
- 观察者收到后,发送SCAN_REQ(包含自己的设备地址)
- 广播者回复SCAN_RSP(包含额外数据)
注意:扫描请求会暴露观察者的设备地址。如果你在做隐私保护相关的应用(比如防追踪),记得使用随机地址或者干脆禁用主动扫描。
3.4 实际代码示例:用Python模拟广播与扫描
咱们用Python的bleak库来演示一下。这个库封装了BLE的底层操作,用起来很方便。
广播者代码(模拟):
import asyncio
from bleak import BleakScanner
# 实际上,广播者通常由硬件实现
# 这里我们用扫描来模拟观察者行为
async def scan_for_devices():
print("开始扫描...")
devices = await BleakScanner.discover(timeout=5.0)
for d in devices:
print(f"发现设备: {d.name}, 地址: {d.address}")
print(f"广播数据: {d.metadata['manufacturer_data']}")
asyncio.run(scan_for_devices())
观察者代码(主动扫描):
import asyncio
from bleak import BleakScanner
def detection_callback(device, advertisement_data):
print(f"收到广播: {device.name}")
print(f"RSSI: {advertisement_data.rssi} dBm")
print(f"广播包内容: {advertisement_data.service_uuids}")
async def main():
scanner = BleakScanner(detection_callback)
await scanner.start()
await asyncio.sleep(10.0)
await scanner.stop()
asyncio.run(main())
实战建议:调试时先用手机上的nRF Connect扫一下,确认广播包格式对不对。我每次写广播代码都这么干,省了不少抓包时间。
3.5 避坑指南
最后分享几个我踩过的坑:
- 广播间隔别设太短:20ms以下会导致信道拥堵,而且功耗翻倍。我一般用100ms起步。
- 扫描窗口和扫描间隔:扫描窗口越大,发现设备越快,但功耗也高。我习惯设扫描窗口=扫描间隔的一半。
- 别忘了过滤重复包:观察者收到同一个广播包多次,如果不做去重,回调函数会被疯狂调用。
嗯,广播和扫描这部分内容,说白了就是BLE设备互相打招呼的方式。搞懂了这些,后面的连接建立就顺理成章了。下一章咱们聊聊连接参数和连接建立过程,到时候见!