蓝牙广播
包组成结构
低功耗蓝牙一共有40个信道,频段范围从2402Mhz-2480Mhz,每2Mhz一个信道,37 38 39 是广播信道,其余为数据信道
一个广播信道最长37字节,有6字节用作蓝牙设备的MAC地址,我们只需要关注剩余的31个字节就可以了,这31个字节又给分为若干个广播数据体,蓝牙规范中称为AD Structure,每个结构体又分为三部分组成,分别是长度,类型,内容,其中长度占用一个字节,类型一个字节,内容占用若干个字节,长度=类型的字节数+内容占用的字节数=1+N
我们来看一下一个具体的例子,下面是一个广播数据包包含两个AD structure,第一个数据包的长度是4,表示后面四个字节的第一个字节为类型,后面三个字节的为数据;第二个数据的长度为3,表示后面三个字节的第一个字节为类型,后两个字节为数据。如果不够31个字节系统会自动补全
我们再来看一下下面这个例子,包含三个AD structure,三个数据类型分别为设备名称,发射功率,厂商自定义数据,第一个AD structure表示这个数据设备名称为1234,第二个AD structure表示发射功率是8dBm
蓝牙广播分类
蓝牙广播类型可以分为如下四类,分别为
-
可连接非定向(最常用的广播方式)
-
可连接定向(用于已配对设备中的快速连接)
-
不可连接非定向(用于蓝牙信标,传感器)
-
可扫描非定向,(可扫描响应承载更多的数据)
蓝牙广播和扫描响应
广播数据是主动发射的,而扫描响应的数据是在收到其他设备的扫描请求之后才会触发的,蓝牙广播最多只能广播31个字节的数据,如果想发送更多的字节,我们可以把一部分数据放到扫描响应里面。
-
扫描响应数据和广播数据格式是一样的
-
扫描响应数据是非必须的
-
扫描响应可作为广播数据的补充
蓝牙状态切换
蓝牙链路层的状态机一共有5种状态,分别是就绪态,广播态,扫描态,发起态,连接态
设备上电后就会处于就绪态,发起广播就会进入广播态,如果被别的设备连接就会进入连接态,断开连接会再次回到就绪态,蓝牙主机设备可以在就绪态发起扫描,进入扫描态。如果发现了想要连接的设备,可以发起连接,此时将进入连接态,如果对对方接受了连接,则双方都会进入连接态,这就是蓝牙的状态机,我们在编程的时候需要控制蓝牙的状态或者根据状态的改变来做出一些动作,
服务和特性
低功耗蓝牙设备之间通信,都是基于服务和特性。一个蓝牙设备中可以包含若干个服务,一个服务中可以包含若干个特性,每一个服务或者特性都要有一个UUID。蓝牙的数据交互都是基于一个个特性进行的,数据交互的方式有五种,分别是Read
,Write
,Write WithOutRespons
,Notify
,Indication
。
-
NOTIFY
:从机可以通知主机,不检查使能通知 CCC (不需要对方回应答包,没有流控)。 -
INDICATE
:从机可以指示主机,不检查使能通知 CCC (需要对方回应答包,有流控) -
READ
:主机读,有流控 -
WRITE
:主机写,有流控 -
WRITE_WITHOUT_RESPOND
:主机写了不回应,没有流控
主机–>从机:READ、WRITE、WRITE_WITHOUT_RESPOND。
从机–>主机:NOTIFY、INDICATE。
(1)WRITE、WRITE_WITHOUT_RESPONSE 是 CLIENT 端(GATT 主机角色)向 SERVER 端(GATT 从机角色)执行的发送数 据操作。而 NOTIFY 和 INDICATE 是 SERVER 端向 CLIENT 执行的发送数据操作。操作是以 handle 的方式标识。
(2)WRITE、INDICATE 的操作是需要对方响应回复命令,多用于数据交互带流控和可靠的传输方式。 (3)WRITE_WITHOUT_RESPONSE 、NOTIFY 是不需要对方响应回复,多用于数据快速传输的方式。
另外增加私有的特征的特性值关键字有 DYNAMIC,AUTHENTICATION_REQUIRED,分别代表意思如下: DYNAMIC —数据可变处理,当有 READ,WRITE,WRITE_WITHOUT_RESPONSE,会产生对应的回调函数 read_callback 和 write_callback 处理,执行获取长度,填入对应的数据等操作。 AUTHENTICATION_REQUIRED —需要配对加密认证标记,代表 CLIENT 端操作该特征的读写必需要经过配对加密后才能被允许,否则操作失败。SERVER 端可以使用该关键字,指示 CLIENT 端需要发起配对加密流程(SERVER 端常用的请求加密方式)。
服务和特性创建
蓝牙设备要在进入广播态之前创建服务和特性,在MicroPython中大概分为四步:
① 创建要使用的UUID;
② 使用UUID创建特性并设置特性的读写权限;
③ 将创建好的特性添加到服务集合中;
④ 将服务集合注册到协议栈中。
比如我们要创建一个UUID为9011的服务,该服务里面包含两个特性,这两个特性的UUID分别是9012和9013,9012的特性拥有Read和Write的权限,9013的特性拥有Read和Notify的权限。
UUID
蓝牙设备在应用层是通过服务和特性去实现的,用下面这张图进行表示,一个服务里面包含若干个特性,每个特性里面又可以有读写,通知等权限,每一个服务和特性都要有一个UUID,UUID是蓝牙组织定义的,用于区分各个服务和特性的标识符,总长度是128bit,比如下面就是两个标准的UUID
考虑到UUID太长,蓝牙组织设置看一个基地址,允许用户使用16bit的UUID与该基地址拼接形成128bit的UUID,比如16bit的UUID 2A37
对应128bit的UUID是这样的,
数据交互
低功耗蓝牙之间的数据交互都是基于特性,以手机连接蓝牙模块为例,手机读取蓝牙模块的数据,使用的是Read方法,手机发送数据给蓝牙模块使用的是Write方法,蓝牙模块发送数据到手机,一般使用的是Notify方法,而且手机端还要打开Notify监听。通信示例代码如下:
from machine import Pin
from time import sleep_ms
import ubluetooth #导入BLE功能模块
ble = ubluetooth.BLE() #创建BLE设备
ble.active(True) #打开BLE
#创建要使用的UUID
SERVER_1_UUID = ubluetooth.UUID(0x9011)
CHAR_A_UUID = ubluetooth.UUID(0x9012)
CHAR_B_UUID = ubluetooth.UUID(0x9013)
#创建特性并设置特性的读写权限
CHAR_A = (CHAR_A_UUID, ubluetooth.FLAG_READ | ubluetooth.FLAG_WRITE | ubluetooth.FLAG_NOTIFY, )
CHAR_B = (CHAR_B_UUID, ubluetooth.FLAG_READ | ubluetooth.FLAG_NOTIFY, )
SERVER_1 = (SERVER_1_UUID, (CHAR_A , CHAR_B, ) , ) #把特性A和特性B放入服务1
SERVICES = (SERVER_1, ) #把服务1放入服务集和中
((char_a, char_b), ) = ble.gatts_register_services(SERVICES) #注册服务到gatts
#设置BLE广播数据并开始广播
ble.gap_advertise(100, adv_data = b'\x02\x01\x06\x03\x09\x41\x42')
def ble_irq(event, data): # 蓝牙中断函数
if event == 1: #蓝牙已连接
print("BLE 连接成功")
elif event == 2: #蓝牙断开连接
print("BLE 断开连接")
ble.gap_advertise(100, adv_data = b'\x02\x01\x06\x03\x09\x41\x42')
elif event == 3: #收到数据
onn_handle, char_handle = data #判断是来自那个特性的消息
buffer = ble.gatts_read(char_handle) #读取接收到的消息
print(char_handle, buffer) #打印消息内容
ble.gatts_notify(0, char_handle, 'Hello') #回复Hello
ble.irq(ble_irq)
在上述代码中,我们创建了一个UUID为9011的服务,并在该服务中创建了一个UUID为9012的特性,该特性的操作权限是 Read,Write,Notify。在手机端打开谷雨蓝牙小程序,连接设备,选择UUID为9012的特性,进入常规试图,点击监听,随便写入一下数据,可以看到,设备回复给手机Hello。
低功耗蓝牙设备之间通信常用的方式是Read,Write 和 Notify ,必须在创建特性时赋予对应的权限,才能在通信中使用。如果某个特性在创建的时候,没有开启 Write 权限,则手机将无法通过该特性发送数据到设备。
已定义的16Bit UUID
对于一些常用的功能,蓝牙组织联盟已经为其定义好了UUID,我们在开发产品的时候直接使用即可。
16BitUUID定义文档下载地址:https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf
电池电量显示
在上述文档中定义了电池电量服务的UUID是0x180F,电池电量特性的UUID是0x2A19,我们可以使用这两个UUID实现电池电量指示的功能,代码如下:
from machine import Pin
from time import sleep_ms
import ubluetooth #导入BLE功能模块
ble = ubluetooth.BLE() #创建BLE设备
ble.active(True) #打开BLE
#创建电池服务和特性的UUID
BATTERY_SERVER_UUID = ubluetooth.UUID(0x180F)
BATTERY_CHAR_UUID = ubluetooth.UUID(0x2A19)
#创建特性并设置特性的读写权限
BATTERY_CHAR = (BATTERY_CHAR_UUID, ubluetooth.FLAG_READ , )
BATTERY_SERVER = (BATTERY_SERVER_UUID, (BATTERY_CHAR, ) , ) #把电量特性放入电池服务
SERVICES = (BATTERY_SERVER, ) #把电池服务服务放入服务集和中
((battery_char,), ) = ble.gatts_register_services(SERVICES) #注册服务到gatts
ble.gatts_write(battery_char, b'\x50') #设置电池电量为80%
#设置BLE广播数据并开始广播
ble.gap_advertise(100, adv_data = b'\x02\x01\x06\x03\x09\x41\x42')
def ble_irq(event, data): # 蓝牙中断函数
if event == 1: #蓝牙已连接
print("BLE 连接成功")
elif event == 2: #蓝牙断开连接
print("BLE 断开连接")
ble.gap_advertise(100, adv_data = b'\x02\x01\x06\x03\x09\x41\x42')
elif event == 3: #收到数据
print("收到新消息")
ble.irq(ble_irq)
电池电量用一个字节来表示,其单位是电量百分比,16进制数0x50转换成十进制为80,所以运行上述代码,连接该设备后,可以看到该设备的电量显示,如下图所示:
通过手机微信小程序“谷雨蓝牙”连接该设备后,可以看到如下服务列表:
温湿度传感器
在蓝牙组织联盟发布的16Bit UUID 中,定义了环境传感器服务的UUID是 ,温度特性的UUID是0x2A6E,湿度特性的UUID是0x2A6F。使用如下代码可实现简单温湿度传感器功能:
from machine import Pin
from time import sleep_ms
import ubluetooth #导入BLE功能模块
ble = ubluetooth.BLE() #创建BLE设备
ble.active(True) #打开BLE
#创建环境传感器服务和特性的UUID
ENV_SERVER_UUID = ubluetooth.UUID(0x181A) #环境传感器服务
TEM_CHAR_UUID = ubluetooth.UUID(0x2A6E) #温度特性
HUM_CHAR_UUID = ubluetooth.UUID(0x2A6F) #湿度特性
#创建特性并设置特性的读写权限
TEM_CHAR = (TEM_CHAR_UUID, ubluetooth.FLAG_READ , )
HUM_CHAR = (HUM_CHAR_UUID, ubluetooth.FLAG_READ , )
ENV_SERVER = (ENV_SERVER_UUID, (TEM_CHAR, HUM_CHAR, ) , ) #把温湿度特性放入环境服务
SERVICES = (ENV_SERVER, ) #把环境服务放入服务集和中
((tem_char, hum_char, ), ) = ble.gatts_register_services(SERVICES) #注册服务到gatts
ble.gatts_write(tem_char, b'\x06\x08') #设置温度为20.54度(0x0806 = 2054)
ble.gatts_write(hum_char, b'\x09\x07') #设置湿度为18.01%(0x0709 = 1801)
#设置BLE广播数据并开始广播
ble.gap_advertise(100, adv_data = b'\x02\x01\x06\x03\x09\x41\x42')
def ble_irq(event, data): # 蓝牙中断函数
if event == 1: #蓝牙已连接
print("BLE 连接成功")
elif event == 2: #蓝牙断开连接
print("BLE 断开连接")
ble.gap_advertise(100, adv_data = b'\x02\x01\x06\x03\x09\x41\x42')
elif event == 3: #收到数据
print("收到新消息")
ble.irq(ble_irq)
运行上述代码,使用手机APP nRF Connect 连接设备后,可以读取到温湿度数据。温湿度数据使用两个字节来表示(低字节在前,高字节在后),温度的单位是0.01度,湿度的单位是0.01%
蓝牙组织联盟定义了很多常用的UUID,上面的示例只选取了其中的两个用作演示,有兴趣的同学可以自行尝试下别的UUID。
GATT
GATT全称为Generic Attribute Profile,是蓝牙(Bluetooth)4.0及以上版本中用于设备间通讯的协议。GATT定义了蓝牙设备之间如何传输数据,以及如何解释和使用这些数据。
在GATT协议中,蓝牙设备被视为包含一组服务(Service)和特征(Characteristic)的集合体。服务表示一个或多个相关的特征,而特征则包括属性值(Value)、类型(Type)和描述符(Descriptor)。通过GATT协议,蓝牙设备可以读取和写入对应特征的属性值,并针对特定的应用需求进行相应配置。
例如,在一个智能手环和智能手机之间的连接过程中,通过GATT协议,智能手环向智能手机提供了一组服务,这些服务包括手环实时步数监控等功能,并提供相应的特征及其属性值。当用户在智能手机的应用程序中选择查看手环步数时,智能手机会向手环发送请求,通过GATT协议来读取相应的特征属性值,从而数据交互得以完成。
总之,GATT协议为蓝牙设备间的通信提供了标准的规范和约束,使不同类型的蓝牙设备能够相互通信和交换数据。
profile
在BLE协议中,Profile是指一种特定的功能规范,定义了一组服务和特征,以便不同厂家的设备之间可以互相交流和理解数据。简单来说,Profile就是基于BLE通信的应用场景和特殊需求,为系统提供标准化的API接口。
Profile在BLE协议栈中位于应用层之上,在BLE协议的属性协议(ATT)和通用属性协议(GATT)之外。一个Profile通常由一个或多个服务组成,每个服务包含多个特征和描述符。具体而言,服务下面可以包括多个特征,而特征则对应了一个 UUID、一个属性值、一个权限等参数。
比如,心率测量Profile规定了在使用BLE连接心率传感器时需要实现的服务和特征,包括读取心率数值、测量开始和停止等操作。只要设备符合对应Profile的规范,就能通过这些服务和特征实现设备间的无缝交互和控制。
总之,BLE Profile提供了基于BLE通信的特定功能规范,使开发者可以更加方便地为自己的产品进行开发和设计,从而实现设备的互动和控制。
GATT Services profile结构如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HRQSTt0P-1686832458175)(null)]
GATT和profile有什么关系
Profile和GATT(Generic Attribute Profile)是紧密相关的概念,可以说Profile是建立在GATT之上的应用场景。GATT是BLE协议栈中定义的一个规范,它定义了设备间传输数据所用到的各种协议和格式。
在GATT中,所有设备都被分为服务器(Server)和客户端(Client)两种角色。服务器包含一项或多项服务(Service),而每个服务下则包含一项或多项特征(Characteristic)。每个特征都具有固定的UUID、属性值、权限等属性,并提供一个句柄(Handle)作为标识。
而Profile可以理解为一组特定的服务和特征的集合,通常由Bluetooth SIG或第三方厂商制定。每个Profile约束着设备厂商在使用BLE通信时需要遵循的规范,使得终端用户能够无缝地使用产品之间的BLE通信功能,即实现互操作性。
例如,假设有一个心率测量Profile,它必须包含一些特定的服务和特征,如Measurement、Body Sensor Location、Heart Rate Control Point等,以便不同厂家的设备之间可以快速且可靠地交换有效的心率数据。
因此,可以认为,Profile是建立在GATT之上的应用层。由于GATT协议灵活、可扩展且适用范围广,因此可以很容易地创建自定义Profile,以满足开发人员的特定需求。
那ATT又是啥?
ATT(Attribute Protocol)是BLE协议栈中负责管理设备间数据读写和交换的底层协议之一。在BLE通信中,一个设备可以做出Server或Client的角色。Server会包含若干个Service,每个Service中又包含多个Characteristic;而Client则可以通过这些Service和Characteristic进行读写、变更等操作。
ATT通过GATT来确定数据格式和属性,以实现不同设备间信息的传输和交换。当Server需要传输特定Service或Characteristic时,必须先将该Service或Characteristic分配一个唯一的UUID和Handle ID。由于ATT与GATT紧密结合,所以UUID和Handle ID也由它们统一管理。
对于一个BLE设备而言,所有可供访问的数据都被保存在Server端,通过连接后的数据通道来访问;而Client仅仅是一个访问Server上的 characteristic 的客户端。因此,在客户端向服务端发起数据请求时,使用ATT命令数据包传输数据、读取或写入Service和Characteristic是很常见的操作。
总之,ATT是低功耗蓝牙技术中重要的通信协议之一,主要负责规定了 Server 和 Client 之间的数据访问协议规范。其实现方式从底层确保了 BLE 数据传输的稳定和可靠性,并且可扩展性非常好,适用于多种各种类型的 BLE 设备。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aLEESSox-1686832458361)(null)]
什么是MTU协商
MTU(Maximum Transmission Unit)是指BLE连接中可用于一次性传输的最大字节数。MTU Negotiation(MTU协商)是指在BLE设备之间建立连接时,通过交换ATT MTU Request和ATT MTU Response消息,协商确定两台设备之间MTU的大小。
实际上,在建立BLE连接时,服务器会发送ATT MTU Request消息,要求客户端回复其支持的最大MTU大小。而客户端则会根据实际情况回复ATT MTU Response消息,并将自己支持的最大MTU大小告知服务器。在双方完成协商后,BLE连接使用的MTU大小就确定下来了,从而可以更高效地进行数据传输。
需要注意的是,BLE连接所使用的MTU大小可能受到设备硬件、协议栈以及操作系统等因素的影响,不同设备的MTU大小可能会有所差异。因此,编写BLE应用程序时,需要根据实际情况进行MTU设置和优化,以尽量提高数据传输效率。
蓝牙主机和从机的概念
蓝牙主机和从机是指在蓝牙通信中连接的两个设备之间的关系。
蓝牙主机(Bluetooth Host)是发起连接请求并控制通信链路的设备,也可以被称为“主设备”或“主机”。它负责与蓝牙从机建立连接、管理通信过程以及控制数据传输。
蓝牙从机(Bluetooth Peripheral)是响应主机连接请求并接受连接的设备,也可以被称为“从设备”或“外围设备”。它与主机建立连接后会等待传入的数据,并向主机发送响应。从机可以是各种类型的设备,包括无线耳机、智能手表、智能家居设备等等。
需要注意的是,在一次蓝牙通信中,一个主机可以连接多个从机,但一个从机只能连接一个主机。
ble主机和从机如何建立连接
BLE从机与主机建立连接需要经过以下步骤:
-
从机广播:从机不断发送广播包告诉主机自己的存在和可用性。
-
主机扫描:主机搜索附近的从机并扫描它们发出的广播包,以确定可用设备。
-
主机请求连接:主机选择想要连接的从机并向其发送连接请求。
-
从机响应连接请求:如果从机接受连接请求,它会返回一个连接确认。
-
安全配对:此时如果有安全配对需求,则主机和从机进行配对操作(加密等)。
-
建立连接:在完成了上述步骤之后,主机和从机就成功建立起了连接,并可以进行数据传输。
需要注意的是,BLE从机和主机之间的连接具有时限,如果长时间没有数据传输,连接可能会自动中断以节省电量。若需要维持连接,请在一定时间内进行数据交互以保持连接。
蓝牙地址
蓝牙地址,也称作 Bluetooth MAC (Media Access Control) 地址,是一个48位的唯一硬件标识符,用于在蓝牙设备之间建立连接和通信。它由全球唯一的组织,即 IEEE(Institute of Electrical and Electronics Engineers)负责管理分配。
蓝牙地址通常表示为 12 个十六进制数(例如:00:11:22:33:44:55),其中前6个数字代表蓝牙适配器的厂商 ID,后6个数字是该适配器的独特序列号。蓝牙地址不同于 IP 地址,它们只在网络层次结构上唯一标识设备,而蓝牙地址则更加接近于物理层面上的设备地址。
需要注意的是,在蓝牙通讯过程中,设备不是直接使用蓝牙地址相互通信,而是通过蓝牙协议栈上的 L2CAP(Logical Link Control and Adaption Protocol)层进行通信,L2CAP 层使用其自己的 Channel ID 和 Connection Handle 来标识正在交换数据的蓝牙设备。
BLE配对的相关概念
Paring(配对)和bonding(绑定)是实现蓝牙射频通信安全的一种机制,有两点需要注意:
-
一是paring/bonding实现的是蓝牙链路层的安全,对应用来说完全透明,也就是说,不管有没有paring/bonding,你发送或接收应用数据的方式是一样的,不会因为加了paring/bonding应用数据传输需要做某些特殊处理;
-
二安全有两种选项:加密或者签名,目前绝大多数应用都是选择加密,后续我们也会以加密为重点进行讲述
实现蓝牙通信安全,除了paring/bonding这种底层方式,用户也可以在应用层去实现相同功能,两者从功能上和安全性上没有本质区别,只不过应用层自己实现的话,需要自己选择密码算法,密钥生成,密钥交换等,如果你不是这方面的专家,你的应用就有可能会存在安全漏洞。Paring/bonding则把上述过程标准化,放在了蓝牙协议栈中,并且其安全性得到了充分评估,用户可以 “无感的” 用上安全的蓝牙通信。
Paring/bonding是蓝牙security manager(SM)的一部分,SM定义了蓝牙通信的安全框架,里面涉及安全架构,密码工具箱,paring协议等,其中paring协议是关键,所以我们经常把paring和SM二者等价,下面将对paring进行详细阐述。