ESP BluFi 配网
蓝牙
蓝牙技术分为经典蓝牙(Bluetooth Classic)和低功耗蓝牙(Bluetooth LE),例如蓝牙耳机使用的就是经典蓝牙,蓝牙手表使用的就是低功耗蓝牙。低功耗蓝牙和经典蓝牙并不互相兼容,在蓝牙4.0标准,SIG 才引入低功耗蓝牙。
低功耗蓝牙协议定义了三层软件结构,从上到下:
应用层(Application Layer)
应用层即以 Bluetooth LE 为底层通信技术所构建的应用,依赖于主机层向上提供的 API 接口。
主机层(Host Layer)
主机层负责实现 L2CAP、GATT/ATT、SMP、GAP 等底层蓝牙协议,向上对应用层提供 API 接口,向下通过主机控制器接口 (Host Controller Interface, HCI) 与控制器层通信。
控制器层(Controller Layer)
控制器层包括物理层 (Physical Layer, PHY) 和链路层 (Link Layer, LL) 两层,向下直接与控制器硬件进行交互,向上通过 HCI 与主机层进行通信。
在 SIG 发布的蓝牙核心规范 (Core Specification) 允许主机层和控制器层在物理上分离,此时 HCI 体现为物理接口,包括 SDIO、USB 以及 UART 等;当然,主机层和控制器层可以共存于同一芯片,以实现更高的集成度,此时 HCI 体现为逻辑接口,常被称为虚拟主机控制器接口 (Virtual Host Controller Interface, VHCI)。一般认为,主机层和控制器层组成了 Bluetooth LE 协议栈 (Bluetooth LE Stack)。
作为应用开发者,在开发过程中我们主要与主机层提供的 API 接口打交道,这要求我们对主机层中的蓝牙协议有一定的了解。
设备发现
广播 (Advertising) 与扫描 (Scanning) 是 Bluetooth LE 设备在进入连接前在设备发现 (Device Discovery) 阶段的工作状态。
广播是设备通过蓝牙天线,向外发送广播数据包的过程。由于广播者在广播时并不知道环境中是否存在接收方,也不知道接收方会在什么时候启动天线,所以需要周期性地发送广播数据包,直到有设备响应。
举个例子:智能手机连接蓝牙手表前,蓝牙手表要先作为广播者向空中三个广播信道(37、38、39)发送广播数据包,以便让智能手机能够找到它,然后智能手机作为扫描者开始向空中广播信道(37、38、39)扫描广播数据包。
蓝牙使用的是 2.4GHz ISM频段,经典蓝牙将 2.4GHz ISM频段划分为 79 个 1MHz 带宽的信道,而低功耗蓝牙则划分 40 个 2MHz 带宽的信道。为了解决数据冲突的问题,蓝牙使用自适应调频技术(Adaptive Frequency Hopping,AFH),该技术可以判断 RF(Radio Frequency) 信道的拥挤程度,通过调频避开拥挤的 RF 信道,以提高通信质量。
在 Bluetooth LE 4.2 标准中, RF 信道分为两种类型,如下
类型 | 数量 | 编号 | 作用 |
---|---|---|---|
广播信道 (Advertising Channel) | 3 | 37-39 | 用于发送广播数据包和扫描响应数据包 |
数据信道 (Data Channel) | 37 | 0-36 | 用于发送数据通道数据包 |
广播数据包结构
在 Bluetooth LE 4.2 标准给出了广播数据包的格式定义:
1 | ---------------------------------------------------------------------- |
- 预置码(Preamble):特殊的比特序列,用于设备时钟同步。
- 访问地址(Access Address):标记广播数据包的地址。
- 协议数据单元(Protocol Data Unit):有效数据的存放区域。
- 循环冗余校验和(CRC):用于循环用于校验
其中 Protocol Data Uint(PDU) 格式定义:
1 | ---------------------------- |
Header 字段细分为如下:
PDU 类型:4 bit,用于设备的广播行为
在蓝牙标准中,共有以下三对广播行为:
- 可连接(Connectable)和 不可连接(Non-connectable)
- 可扫描(Scannable)和 不可扫描(Non-scannable)
- 不定向(Undirected)和 定向(Directed)
根据上面的广播行为可以组合成以下 4 种常见的广播类型:
- ADV_IND(可连接、可扫描、定向):最常见的广播类型
- ADV_DIRECT_IND(可连接、不可扫描、不定向):常用于已知设备重连
- ADV_NONCONN_IND(不可连接、不可扫描、定向):作为信标设备,仅向外发送广播数据
- ADV_SVAN_IND(不可连接、可扫描、定向):作为信标设备,一般用于广播数据包长度不足的情况
保留位:1 bit
通道选择位:1 bit,标记广播者是否支持 LE 通道选择算法
发送地址类型:1 bit,0表示公共地址;1表示随机地址
接收地址类型:1 bit,同上
有效负载长度:8 bit
其中 Payload 格式定义:
1 | ------------------------------- |
- AdvA:广播设备的MAC地址
- AdvData:广播数据
其中 AdvData 格式定义:
1 | ---------------------------------------------- |
扫描
扫描行为分为以下两种:
- 被动扫描(Passive Scanning):扫描者只接收广播数据包
- 主动扫描(Active Scanning):扫描者在接收广播数据包后,还向可扫描广播者发送扫描请求(Scan Request)
可扫描广播者在接收到扫描请求之后,会广播扫描响应 (Scan Response) 数据包,以向感兴趣的扫描者发送更多的广播信息。扫描响应数据包的结构与广播数据包完全一致,区别在于 PDU 头中的 PDU 类型不同。
连接
在 Bluetooth LE 5.0 引入扩展广播特性以后, Legacy ADV 和 Extended ADV 对应的连接建立过程略有差异,下以 Legacy ADV 对应的连接建立过程为例。
当扫描者(智能手机)在某一个广播信道(37、38、39)接收到一个广播数据包时,若该广播者(智能手表)是可连接的,那么扫描者可以在同一广播信道发送连接请求 (Connection Request)。对于广播者来说,它可以设置 接受列表 (Filter Accept List) 以过滤不受信任的设备,或接受任一扫描者的连接请求。随后,广播者(Advertiser)转变为外围设备(Peripheral),扫描者(Scanner)转变为中央设备(Central),两者之间可以在数据信道(0 ~ 36)进行双向通信。
当连接建立后,中央设备与外围设备会周期性地进行数据交换,这个数据交换的周期被称为连接间隔 (Connection Interval)。一次数据交换的过程被称为连接事件 (Connection Event)。即便任意一方在连接间隔开始时无需发送数据,也必须发送空数据包以维持连接。
最大传输单元 (Maximum Transmission Unit, MTU) 指的是 单个 ATT 数据包 的最大字节数。**”数据通道数据包” 和 “广播数据包”(上文已有说明) 的最外层结构一致,区别在于 PDU 的结构。**
1 | ---------------------------------------------------------------------- |
注意:在广播通道(37、38、39)传输的数据包称为 广播数据包;而在数据通道(0 ~ 36)传输的数据包称为 数据通道数据包,它们之间的PDU是不同的。
数据通道数据包 的 PDU 可以分为三部分:
1 | ------------------------------------------------------------------- |
LL Header:头部
Payload:有效负载
在 Bluetooth LE 4.2 以前,有效负载最大值为 27 字节; Bluetooth LE 4.2 引入了数据长度扩展 (Data Length Extension, DLE) 特性,有效负载的最大值可达 251 字节。
Message Integrity Check:消息完整性检查。该项为可选项
其中 Payload 格式结构:
1 | ------------------------------------------------------ |
MTU 的默认值为 23 字节(即 ATT Data 大小),恰好是 Bluetooth LE 4.2 以前,单个数据 PDU 的最大可承载 ATT 数据字节数。
其中 L2CAP Header 的帧格式:
1 | ----------------------------------------------- |
L2CAP协议使用信令来控制两个设备之间的L2CAP层的连接建立,配置更新等功能。
其中ATT Data 的帧格式:
1 | ------------------------------------------------------ |
对于 ATT Header 的帧格式:
1 | ------------------------------------------------------ |
GAP
GAP 层的全称为 通用访问规范 (Generic Access Profile, GAP),定义了 Bluetooth LE 设备之间的连接行为以及设备在连接中所扮演的角色。
GAP 状态和角色,GAP 中共定义了三种设备的连接状态以及五种不同的设备角色:
空闲(Idle)
此时设备无角色,处于就绪状态(Standby)
设备发现(Device Discovey)
- 广播者(Advertiser)
- 扫描者(Scanner)
- 连接发起者(Initiator)
连接(Connection)
- 外围设备(Peripheral)
- 中央设备(Central)
GAP 根据不同状态下的角色变化:
外围设备(Peripheral)一般作为GATT Server,例如蓝牙手表;中心设备(Central)一般作为GATT Client,例如智能手机。
低功耗蓝牙网络拓扑:Bluetooth LE 设备可以同时与多个 Bluetooth LE 设备建立连接,扮演多个外围设备或中央设备角色,或同时作为外围设备和中央设备。例如我现在使用的 NIZ-X87-三模键盘 就可以连接三个不同的智能设备(Windows笔记本电脑、macOS笔记本电脑、平板电脑)。
数据交换
GATT 服务是 Bluetooth LE 连接中两个设备进行数据交换的基础设施,其最小数据单元是属性。
ATT
GATT/ATT 层定义了进入连接状态后,设备之间的数据交换方式,包括数据的表示与交换过程。
ATT 的全称是 属性协议 (Attribute Protocol, ATT),定义了一种称为属性 (Attribute) 的基本数据结构,以及基于服务器/客户端架构的数据访问方式。从编程的角度来看,属性是一个数据结构,它包括了数据结构和数据值,就如同C语言中结构体的概念。
属性的数据结构一般由以下四部分组成:
句柄(Handle)
类型(Type)
值(Value)
权限(Permission)
1 | ------------------------------------------------------ |
在协议栈实现中,属性一般被放置称为 属性表 的结构体数据中管理。一个属性在这张表中的索引,就是属性的句柄,常为无符号整型。
属性的类型由 UUID 表示,可以分为 2字节、4字节、16字节 三类。2字节 UUID 由 SIG 同一定义,可以在其公开发布的标准文档中查询;其他两种长度的UUID用于表示厂商自定义的属性类型,其中 16 字节 UUID 较为常用。
句柄
句柄犹如指向属性实体的指针,对端设备可通过句柄来访问该属性,它是一个2字节长度的十六进制码,起始于 0x0001
,在系统初始化时,各个属性的句柄逐步加一,最大不能超过 0xffff
。
类型
类型是用来区分当前属性是 服务项
或 特征值
,它用 UUID表示,UUID(Universally Unique Identified,通用唯一识别码)是一个软件构建标准,并非 BLE 独有的概念。一个合法的UUID,一定是随机的、全球唯一的,不应该出现两个相同的UUID。标准的UUID用一串 16字节大小 的十六进制字符串表示,例如:f6257d37-34e5-41dd-8f40-e308210498b4
。
BLE 的属性类型是有限的,分为4个大类:
- 首要服务项(Primary Service)
- 次要服务项(Secondary Service)
- 包含服务项(Include)
- 特征值(Characteristic)
这些属性类型分别对应了指定的UUID,BLE对这些UUID与属性类型的映射关系做了规定:
0x1800 ~ 0x26ff:服务项类型
0x2700 ~ 0x27ff:单位
0x2800 ~ 0x28ff:属性类型
0x2900 ~ 0x29ff:描述符类型
0x2a00 ~ 0x7fff:特征值类型
例如:UUID = 0x1800 时,表示它是一个首要服务项
标准的UUID是16字节,为什么可以用2字节表示?
因为用2字节表示的UUID是常用的,为了减少传输的数据量,BLE协议做了一个转换约定,给定了一个固定的 16字节模版,只设置2字节为变化量,其它为常量,2字节的UUID在系统内部会被替换,进而转换成标准的16 16字节UUID。
BLE 中UUID模板:
1 | 0000XXXX-0000-1000-8000-00805F9B34FB |
其中 XXXX
就是变化量,其它的都是常量。例如 2字节的UUID=0x2A00
将会转换为 UUID=00002A00-0000-1000-8000-00805F9B34FB
。
如果 2字节的UUID=0xABCD
,那么BLE的属性类型就是 ABCD
,很明显这个值已经超过了BLE定义最大上限,当主机端扫描到该属性类型,会将其当做是 “用户自定义的类型”,然后从其它位置获取该UUID的真实值。
值
属性值是用来存放数据的。如果该属性是服务项类型或者是特征值声明类型,那么它的属性值就是UUID信息。如果是普通的特征值,则属性值就是用户的数据。属性值需要预留空间以保存用户数据。
权限
权限主要分为以下四种:
访问权限(Access):只读、只写、读写
加密权限(Encryption):加密、不加密
认证权限(Authentication):需要认证、无需认证
认证是指相互确认对方身份。完成认证流程的两个设备,双方建立信任关系,二者之间的通信可以认为是安全的。BLE中,“认证” 过程就是配对,需要用户输入PIN码完成配对。
授权权限(Authorization):需要授权、无需授权
授权要求设备必须是可信的,因此授权的管控等级要高于认证。理解二者的关系,需要引入一个概念:Trusted Device。一个没有经过认证的设备,被称为Unknown Device(未知设备);经过了认证的设备会在绑定信息中被标记为Untrusted Device(不可信设备);经过了认证的设备,并且在绑定信息中被标记为Trusted 的设备,被称为Trusted Device(可信设备)。
授权要求设备为Trusted Device(可信任设备)。在实际使用中,经过配对以后设备即为Untrusted Device——认证,在代码中调用API可以设置设备为Trusted Device——授权。
GATT
GATT 的全称是通用属性规范 (Generic Attribute Profile),在 ATT 的基础上,定义了以下三个概念
- 特征数据 (Characteristic)
- 服务 (Service)
- 规范 (Profile)
特征数据(Characteristic)和服务(Service)都是以属性(Attribute)作为基本数据结构组合而成复合数据结构(即结构体嵌套)。
规范(Profile)是一个预定义的服务集合,实现了某种规范中所定义的所有服务的设备满足该规范。例如 Heart Rate Profile 规范由 Heart Rate Service 和 Device Information Service 两个服务组成,那么可以称实现了 Heart Rate Service 和 Device Information Service 服务的设备符合 Heart Rate Profile 规范。
三个概念层次关系用一张图表示:
一个服务(Service)的数据结构大致可以分为两部分:
序号 | 名称 |
---|---|
1 | 服务声明属性 (Service Declaration Attribute) |
2 | 特征数据定义属性 (Characteristic Definition Attributes) |
服务声明属性的 UUID 为 0x2800,访问权限为只读,值为标识服务类型的 UUID ,例如 Heart Rate Service 的 UUID 为 0x180D,那么其服务声明属性就可以表示为
Handle | UUID | Permissions | Value | Attribute Type |
---|---|---|---|---|
0 | 0x2800 | Read-only | 0x180D | Service Declaration |
一个特征数据(Characteristic )通常由以下三个属性组成:
序号 | 名称 | 作用 | 备注 |
---|---|---|---|
1 | 特征数据声明 (Characteristic Declaration) | 含有特征数据值的读写属性 (Properties)、句柄以及 UUID 信息 | UUID 为 0x2803,只读属性 |
2 | 特征数据值 (Characteristic Value) | 实际的用户数据 | UUID 标识特征数据的类型 |
3 | 特征数据描述符 (Characteristic Descriptor) | 特征数据的其他描述信息 | 可选属性 |
什么是特征数据描述符?
特征数据描述符起到对特征数据进行补充说明的作用。最常见的特征数据描述符是客户端特征数据配置描述符 (Client Characteristic Configuration Descriptor, CCCD),下由 CCCD 代指。当特征数据支持由服务器端发起的 数据操作 (通知或指示)时,必须使用 CCCD 描述相关信息;这是一个可读写属性,用于 GATT 客户端告知服务器是否需要启用通知或指示,写值操作也被称为订阅 (Subscribe) 或取消订阅。例如:智能手机向蓝牙手表订阅了温湿度信息,那么当蓝牙手表的温湿度传感器检测环境温湿度变化后,要主动向智能手机上报温湿度数据。
特征数据的三种特征数据属性都属于特征数据定义属性。
ATT和GATT的关系:
ATT层定义了一个通信的基本框架,数据的基本结构,以及通信的指令,而GATT层就是定义service和characteristic,GATT层用来赋予每个数据一个具体的内涵,让数据变得有结构和意义。换句话说,没有GATT层,低功耗蓝牙也可以通信起来,但会产生兼容性问题以及通信的低效率。
GATT 数据操作
数据操作指的是对 GATT 服务器上的特征数据进行访问的操作,主要可以分为以下两类:
- 由客户端发起的操作
- 由服务器发起的操作
由客户端发起的操作
由客户端发起的操作有以下三种:
读(Read)
从 GATT 服务器上拉取某一特征数据的当前值
写(Write)
普通的写操作要求 GATT 服务器在收到客户端的写请求以及写对应数据后,进行确认相应
写(无需响应)(Write without response)
快速写操作,不需要服务器进行确认响应
由服务器发起的操作
由服务器发起的操作分为两种:
通知(Notify)
通知是 GATT 服务器主动向客户端推送数据的操作,不需要客户端回复确认响应
指示(Indicate)
与通知相似,区别在于指示需要客户端回复确认,因此此数据推送速度比通知慢。
BluFi 数据帧
BluFi 定义的帧格式按照是否分片可以分为两种:
帧不分片格式:
1 | --------------------------------------------------------------------- |
帧分片格式:
如果 使能 “帧控制” 字段中的分片位,则 “数据” 字段中会出现 2 字节的内容总长度。该 内容总长度 表示帧的剩余部分的总长度,并用于报告终端需要分配的内容大小。
1 | ----------------------------------------------------------------------------------- |
注意:
1、通常情况下,控制帧不包含数据字段,ACK 帧类型除外。
2、校验字段不一定有,需要根据 帧控制 字段的b’0000 0010 位判断是否存在校验字段
类型字段
类型 字段占 1 字节,分为 类型 和 子类型 两部分。其中,类型 占低 2 位,表明该帧为 数据帧 或是 控制帧;子类型 占高 6 位,表示此数据帧或者控制帧的具体含义。
- 控制帧,暂不进行加密,可校验。低 2 位表示为 b’00
- 数据帧,可加密,可校验。低 2 位表示为 b’01
控制帧
低 2 位表示为 b’00,其中 高 6 位 定义的控制帧子类型有:
b’000000(0):ACK
b’000001(1):将 ESP 设备设置为安全模式
b’000010(2):设置 Wi-Fi 的 opmode
b’000011(3):将 ESP 设备连接至AP
b’000100(4):断开 ESP 设备与AP的连接
b’000101(5):获取 ESP 设备的 Wi-Fi 模式和状态等信息
b’000110(6):断开 STA 设备与SoftAP 的连接
b’000111(7):获取版本信息
b’001000(8):断开 BLE GATT 连接
b’001001(9):获取 Wi-Fi 列表
从 esp-idf/components/bt/common/btc/profile/esp/blufi/include/blufi_int.h
可以找到上述的子类型定义。
数据帧
低 2 位表示为 b’01,其中 高 6 位定义的数据帧子类型有:
b’000000(0):发送协商数据
b’000001(1):发送 STA 模式的 BSSID
b’000010(2):发送 STA 模式的 SSID
b’000011(3):发送 STA 模式的密码
b’000100(4):发送 SoftAP模式的SSID
b’000101(5):发送 SoftAP模式的密码
b’000110(6):设置 SoftAP模式的最大连接数
b’000111(7):设置 SoftAP模式的认证模式
b’001000(8):设置SoftAP模式的通道数量
b’001001(9):用户名
b’001010(10):CA认证
b’001011(11):客户端认证
b’001100(12):服务端认证
b’001101(13):客户端私钥
b’001110(14):服务端私钥
b’001111(15):Wi-Fi 连接状态报告
b’010000(16):版本
b’010001(17):Wi-Fi 热点列表
b’010010(18):报告异常
b’010011(19):自定义数据
b’010100(20):设置最大 Wi-Fi 重连次数
b’010101(21):设置 Wi-Fi 连接失败原因
b’010110(22):设置 Wi-Fi 连接失败的 RSSI
从 esp-idf/components/bt/common/btc/profile/esp/blufi/include/blufi_int.h
可以找到上述的子类型定义。
帧控制字段
- 0x01(b’0000 0001):表示帧是否加密。1表示加密,0表示未加密
- 0x02(b’0000 0010):表示帧尾是否包含校验位。0表示不包含校验位,1表示包含校验位
- 0x04(b’0000 0100):表示数据方向。0表示 手机 –> ESP,1表示 ESP –> 手机
- 0x08(b’0000 1000):表示是否要求对方回复ACK。0表示不要求,1表示要求
- 0x10(b’0001 0000):表示是否有后续的数据分片。0表示没有分片,1表示有分片
- 其他:保留
序列号
序列号用于序列控制。
帧发送时,无论帧的类型是什么,序列都会自动加 1,用来防止重放攻击 (Replay Attack)。每次重新连接后,序列清零。
数据长度
数据长度用于指示数据字段的总长度,不包含校验部分。
数据
对于不同的类型或子类型,数据字段的含义均不同
校验
此字段占两个字节,用来校验序列、数据长度以及明文。
BluFi UUID
BluFi Service UUID: 0xFFFF,16 bit
BluFi(手机 > ESP32)特性:0xFF01,主要权限:可写
BluFi(ESP32 > 手机)特性:0xFF02,主要权限:可读可通知
源码阅读
首先注册 BluFi Profile,它的UUID=0xFFFF。当ESP32接收到BluFi的数据后,交给 void btc_blufi_recv_handler(uint8_t *data, int len)
处理。
第一步:判断序列号是否为期待的值;
第二步:判断数据加密,若加密则先解密;
第三步,判断帧尾是否包含校验位,若有加密位则进行CRC校验;
第四步:判断是否有ACK位,若有发送ACK应答;
第五步:判断是否有后续的数据分片,若有则先缓存数据,接着等待下一帧数据。
一个数据包接收完成后(分片的数据数据要把多个数据包组合),交给 void btc_blufi_protocol_handler(uint8_t type, uint8_t *data, int len)
处理。第一步,判断数据类型:控制帧类型 or 数据帧类型。第二步,判断子类型。
void btc_blufi_protocol_handler(uint8_t type, uint8_t *data, int len)
函数根据 type 类型判断帧是否为 BLUFI 类型的帧,通过 type & 0x03
来得出帧是否为 BLUFI 类型。接着判断帧是控制帧,还是数据帧。如果是控制帧,则接着判断控制帧的子类型,通过 (type & 0xFC) >> 2
,首先 type & b’1111 1100,即低2位不考虑,然后右移两位,得到高6位。
BluFi 数据结构体定义:
1 | /* BLUFI protocol */ |
如果用户需要保证配网数据的安全性,那么在发送配网数据前,APP 和 ESP设备 需要进行DH秘钥协商。
什么是DH秘钥协商算法?
DH(Diffie-Hellman)秘钥协商算法是一种允许双方在不安全的通信信道上安全地建立共享密钥的方法。这个方法是由Whitfield Diffie和Martin Hellman于1976年提出,是第一个被公开发布的密钥交换协议,它解决了两个实体在没有事先交换秘密的情况下如何通过公共网络创建一个共同的秘密的问题。