低功耗蓝牙(BLE)基本概念汇总:信道分布、数据组包到数据交互

发布时间:2024年01月05日

本文根据网上资料收集整理,介绍了一些关于低功耗蓝牙的入门知识,非常基础,有经验的请略过。

一、信道分布

一共40个信道,频段范围从2402Mhz-2480Mhz,每2Mhz一个信道,其中3个广播信道,37个数据信道

?

二、数据组包

(一)广播数据包

一个广播数据包最长37字节,有6字节用作蓝牙设备的MAC地址,我们只需要关注剩余的31个字节就可以了,这31个字节又给分为若干个广播数据体,蓝牙规范中称为AD Structure,每个结构体又分为三部分组成,分别是长度,类型,内容,其中长度占用一个字节,类型一个字节,内容占用若干个字节,长度=类型的字节数+内容占用的字节数=1+N

?

1.蓝牙MAC地址

蓝牙MAC地址,也称作 Bluetooth MAC (Media Access Control) 地址,这个和网卡的MAC地址类似,是一个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 来标识正在交换数据的蓝牙设备。

2.广播数据包

?

3.蓝牙广播类型

?

?

4、信号握手流程

?

  • 扫描响应数据和广播数据格式是一样的

  • 扫描响应数据是非必须的

  • 扫描响应可作为广播数据的补充

  • 打捞响应需要一定的触发条件(收到扫描请求)

5.蓝牙状态机

蓝牙链路层一共有5种状态,分别是就绪态,广播态,扫描态,发起态,连接态

蓝牙设备状态的转换:

设备上电后就会处于就绪态,发起广播就会进入广播态,如果被别的设备连接就会进入连接态,断开连接会再次回到就绪态,蓝牙主机设备可以在就绪态发起扫描,进入扫描态。如果发现了想要连接的设备,可以发起连接,此时将进入连接态,如果对对方接受了连接,则双方都会进入连接态,这就是蓝牙的状态机,我们在编程的时候需要控制蓝牙的状态或者根据状态的改变来做出一些动作。

6.广播类型 AD type(1字节)

序号valuename备注
10x01?Flags
20x02Incomplete List of 16-bit Service Class UUIDs
30x03Complete List of 16-bit Service Class UUIDs
40x04Incomplete List of 32-bit Service Class UUIDs
50x05Complete List of 32-bit Service Class UUIDs
60x06Incomplete List of 128-bit Service Class UUIDs
70x07Complete List of 128-bit Service Class UUIDs
80x08Shortened Local Name
90x09Complete Local Name
100x0ATx Power Level
110x0DClass of Device
120x0ESimple Pairing Hash C-192
130x0FSimple Pairing Randomizer R-192
140x10Device ID
150x10Security Manager TK Value
160x11Security Manager Out of Band Flags
170x12Peripheral Connection Interval Range
180x14List of 16-bit Service Solicitation UUIDs
190x15List of 128-bit Service Solicitation UUIDs
200x16Service Data - 16-bit UUID
210x17Public Target Address
220x18Random Target Address
230x19Appearance
240x1AAdvertising Interval
250x1BLE Bluetooth Device Address
260x1C?LE Role
270x1DSimple Pairing Hash C-256
280x1ESimple Pairing Randomizer R-256
290x1FList of 32-bit Service Solicitation UUIDs
300x20Service Data - 32-bit UUID
310x21Service Data - 128-bit UUID
320x22LE Secure Connections Confirmation Value
330x23LE Secure Connections Random Value
340x24URI
350x25Indoor Positioning
360x26Transport Discovery Data
370x27LE Supported Features
380x28Channel Map Update Indication
390x29PB-ADV
400x2AMesh Message
410x2BMesh Beacon
420x2CBIGInfo
430x2DBroadcast_Code
440x2EResolvable Set Identifier
450x2FAdvertising Interval - long
460x30Broadcast_Name
470x31Encrypted Advertising Data
480x32Periodic Advertising Response Timing Information
490x34Electronic Shelf Label
500x3D3D Information Data3D Synchronization Profile
510xFFManufacturer Specific Data

7. 服务和特性

BLE设备之间通信都是基于服务和特性。一个蓝牙设备中可以包含若干个服务,一个服务中可以包含若干个特性,每一个服务或者特性都要有一个UUID。

蓝牙的数据交互都是基于某个特性进行的,数据交互有5种方式,分别是Read,Write,Write WithOutRespons,Notify,Indication。

READ:主机读,有流控

WRITE:主机写,有流控

WRITE_WITHOUT_RESPOND:主机写了不回应,没有流控

NOTIFY:从机可以通知主机,不检查使能通知 CCC (不需要对方回应答包,没有流控)。

INDICATE:从机可以指示主机,不检查使能通知 CCC (需要对方回应答包,有流控)

?主机–>从机:READ、WRITE、WRITE_WITHOUT_RESPOND。

从机–>主机:NOTIFY、INDICATE。

?WRITE、WRITE_WITHOUT_RESPONSE 是 CLIENT 端(GATT 主机角色)向 SERVER 端(GATT 从机角色)执行的发送数 据操作。而 NOTIFY 和 INDICATE 是 SERVER 端向 CLIENT 执行的发送数据操作。操作是以 handle 的方式标识。

WRITE、INDICATE 的操作是需要对方响应回复命令,多用于数据交互带流控和可靠的传输方式。

WRITE_WITHOUT_RESPONSE 、NOTIFY 是不需要对方响应回复,多用于数据快速传输的方式。

另外增加私有的特征的特性值关键字有 DYNAMIC,AUTHENTICATION_REQUIRED,分别代表意思如下: DYNAMIC —数据可变处理,当有READ,WRITE,WRITE_WITHOUT_RESPONSE,会产生对应的回调函数 read_callback 和 write_callback 处理,执行获取长度,填入对应的数据等操作。 AUTHENTICATION_REQUIRED —需要配对加密认证标记,代表 CLIENT 端操作该特征的读写必需要经过配对加密后才能被允许,否则操作失败。SERVER 端可以使用该关键字,指示 CLIENT 端需要发起配对加密流程(SERVER 端常用的请求加密方式)。


8. UUID

蓝牙设备在应用层是通过服务和特性去实现的,用下面这张图进行表示,一个服务里面包含若干个特性,每个特性里面又可以有读写,通知等权限,每一个服务和特性都要有一个UUID,UUID是蓝牙组织定义的,用于区分各个服务和特性的标识符,总长度是128bit,比如下面就是两个标准的UUID

考虑到UUID太长,蓝牙组织设置看一个基地址,允许用户使用16bit的UUID与该基地址拼接形成128bit的UUID,比如16bit的UUID 2A37对应128bit的UUID是这样的。

网上UUID生成网站:https://www.uuid.online/

(二)数据包示例

1. 蓝牙广播自身信息

我们将esp32作为外设设别,要想被发现,就需要不断的广播自己的信息,这样才能被中心设备发现。esp32自带的蓝牙模块

import bluetooth        #导入BLE功能模块

ble = bluetooth.BLE()   #创建BLE设备
ble.active(True)         #打开BLE
#设置BLE广播数据并开始广播
ble.gap_advertise(100, adv_data = b'\x02\x01\x06\x03\x09\x41\x42')

gap_advertise函数就是在不断的广播蓝牙的信息(间隔100ms),打开手机的蓝牙调试软件,我们就会发现这个蓝牙设备。所有的外设设备都是在不断广播的,但是当蓝牙设备被连接时,其他中心设备将无法再搜索到该设备。这个和前面所说的蓝牙状态机转换相对应。

2. 广播数据包

蓝牙的信息按照一定的数据结构编码成二进制,最终就得到了 b'\x02\x01\x06\x03\x09\x41\x42'。蓝牙广播包的最大长度是37个字节,其中设备地址(AdvA)占用了6个字节,只有31个字节(AdvData)是可用的。这31个可用的字节又按照一定的格式来组织,被分割为n个AD Structure。

?

??? AdvA:表示广播方的地址,即蓝牙设备的MAC地址,长度为6字节。
??? Data:表示数据包,AdvData由若干个广播数据单元(即AD Structure)组成。AD Structure的结构=Length+AD Type+AD data。
??????? Length:表示该AD Structure数据的总长度,即为AD Type与AD Data的长度和(即不含 Length字段本身的1字节)。
??????? AD Type:表示该广播数据代表的含义,如设备名、UUID等。
??????? AD Data:表示具体的数据内容。

我们按照AD Structure结构来解析一下上面的adv_data数据
在这里插入图片描述
AD Structure 1
字段?? ????? ? 长度?? ?? 取值?? ?说明
Length?? ??? 1字节?? ?0x02?? ?表示AD Type与AD Data的总长度。
AD Type?? ?1字节?? ?0x01?? ?表示该广播数据代表的含义。此处取值固定为0x01,表示设备标识
AD Data?? ?1字节?? ?0x06?? ?表示蓝牙设备的物理连接能力。esp32只支持LE(低功耗蓝牙),不支持BR/EDR(经典蓝牙),一般都将设备设为处于普通发现模式,所以我们只设置Bit1和Bit2,即0x06(b00000110)。

??? bit0:LE受限可发现模式。
??? bit1:LE通用可发现模式。
??? bit2:不支持BR/EDR。
??? bit3:对Same Device Capable(控制器)同时支持BLE和BR/EDR。
??? bit4:对Same Device Capable(主机)同时支持BLE和BR/EDR。
??? bit5~7:预留。

AD Structure 2
字段?? ???? ?? 长度(字节)?? ?取值?? ????????????? 说明
Length?? ??? 1字节?? ? ? ? ? ? ? ? 0x03?? ?????????? 表示AD Type与AD Data的总长度。
AD Type?? ?1字节?? ????????????? 0x09?? ??????????? 表示蓝牙的名称
AD Data?? ?2字节?? ??????????? 0x41,0x42?? ????? 蓝牙的名称,asiic表示的就是AB

3.修改中文蓝牙名称

蓝牙广播的数据最终都需要编码成utf-8,我们可以使用encode将中文名称编码成utf-8,然后构建成AD Structure的数据结构,将蓝牙模式与蓝牙名称拼接在一起广播出去

import bluetooth        #导入BLE功能模块
ble = bluetooth.BLE()   #创建BLE设备
ble.active(True)         #打开BLE
name = "中国蓝牙".encode() # 编码成utf-8格式
adv_mode = bytearray(b'\x02\x01\x06')  # 正常蓝牙模式, ad struct 1
adv_name = bytearray((len(name) + 1, 0x09)) + name # 0x09是蓝牙名称,ad struct 2
adv_data = adv_mode + adv_name
ble.gap_advertise(100, adv_data = adv_data)

? 4. 数据收发交互

连接到esp32的蓝牙之后,就要考虑怎么传输数据了,蓝牙的数据交互依靠的是服务和特性。

??? 服务services:蓝牙可以提供很多服务,例如键盘鼠标服务,心率监控服务,环境监测服务等,每个服务都有自己的uuid。
??? 特性:有了服务,用户就可以通过调用这个服务获取自己想要的数据了,特性可以理解为接口数据。

直接说蓝牙服务跟特性太抽象了,举几个不太恰当的例子来帮助理解。比如我们要开发一个环境监控服务,这个服务有几个数据接口,例如读取温度的接口,读取湿度的接口,通过每个接口就可以获取想要的数据了,同样我们可以再实现一个电量监控服务,监控设备的电量,也提供一个电量读取的接口,随着服务的增多,我们可以考虑设计一个工厂类

class EnvService:
    def get_temperature(self):
        return "38.5"
    
    def get_humidity(self):
        return "0.25"

class BatteryService:
    def get_battery(self):
        return "78"

class FactoryService:
    def __call__(self, fs):
        if fs == "env":
            return EnvService()
        elif fs == "battery":
            return BatteryService()

es = FactoryService("env")
bs = FactoryService("battery")

print(es.get_temperature())
print(es.get_humidity())
print(bs.get_batteryself())

??? 使用蓝牙的时候你怎么能让别人知道你定义的是什么服务和接口呢?蓝牙联盟使用uuid来区分不同的服务和特性。蓝牙联盟定义了非常多的标准服务和特性。当你使用0x1124大家就知道你是一个hid服务,当你使用0x1106大家就知道你是一个文件传输服务,当你使用0x180F就知道你是一个电量监控服务。知道了你是什么服务以后就可以通过相应的接口来进行通信。接口也是使用uuid来定义的,例如0x2A19就是电池电量的监控接口,通过这个uuid来进行数据交互。操作uuid来读取数据,听起来就很茫然,其实是你把这个uuid传入到蓝牙sdk中,sdk会返回一个句柄(接口)来给你操作

env_service_uuid = 0x181A # 定义服务的uuid,映射就是环境监控服务
# 定义特性(接口)的uuid, 还需要指明这个特性是否可读写
env_tmp_uuid = (0x2A6E, WRITE | READ) # 温度特性
env_hum_uuid = (0x2A6F, WRITE | READ) # 湿度特性
# 组成环境检测服务,服务的uuid以及特性的uuid
env_service = (service_uuid, (env_tmp_uuid, env_hum_uuid))

services = (env_service, )
# 到蓝牙的sdk注册服务,拿到数据接口,通过这个接口可以读写数据
((tem_handle, hum_handle, ), ) = ble.gatts_register_services(services)

import struct
# 温度特性写入数据
ble.gatts_write(tem_handle, struct.pack("<B", int(100)))
# 对所有连接上的蓝牙设备发送数据,蓝牙只能连接一个中心设备,所以conn_handles只有一个0值
# 很多文章直接写成ble.gatts_notify(0, tem_handle)
for handle in conn_handles:
     # Notify connected centrals to issue a read.
    ble.gatts_notify(handle, tem_handle)

conn_handles是所有连接的蓝牙设备,例如可能有很多手机都连接了这个esp32,就会有多个conn_handle,所以需要先把数据写到特性接口中,然后notify到所有连接的手机。

5. 蓝牙事件处理

现在的问题就是我们怎么知道哪些手机连接了esp32,esp32收到的数据又是来自哪个手机呢?这就需要蓝牙的中断事件来帮我们获取想要的数据了,蓝牙中断监听各种事件,例如有设备连接了,或者连接断开了,或者收到了其他设备的数据,都是通过事件触发的。我们在中断函数中监听蓝牙的各种事件

import bluetooth        #导入BLE功能模块
ble = bluetooth.BLE()   #创建BLE设备
ble.active(True)         #打开BLE
name = "中国蓝牙".encode() # 编码成utf-8格式
adv_mode = bytearray(b'\x02\x01\x06')  # 正常蓝牙模式, ad struct 1
adv_name = bytearray((len(name) + 1, 0x09)) + name # 0x09是蓝牙名称,ad struct 2
adv_data = adv_mode + adv_name
ble.gap_advertise(100, adv_data = adv_data)

def ble_irq(event, data): # 蓝牙中断函数
    if event == 1: #蓝牙已连接
        # 作为外设设备,一旦被中心设备连接之后就无法再被其他设备连接,所以conn_handle只能为0
        conn_handle, addr_type, addr = data
        print(f"fd = [{conn_handle}] connect")

    elif event == 2: #蓝牙断开连接
        conn_handle, addr_type, addr = data
        print(f"fd = [{conn_handle}] disconnect")
        ble.gap_advertise(100, adv_data = adv_data)

    elif event == 3: #收到数据
    # 作为中心设备,可能会连接很多外设设备,即各种各样的服务,例如hid服务,env服务,battery服务等,通过conn_handle来区分是哪个设备(服务)发来的数据
    # 通过attr_handle来区分收到的是哪个特性的数据
        conn_handle, attr_handle = data
        print(f"fd = [{conn_handle}], char = [{attr_handle}] recive msg")

ble.irq(ble_irq)

event=1就是蓝牙连接事件,event=2就是断开蓝牙,envent=3就是收到了数据
更多的事件可以在micropython的文档中找到,这里给出一部分常用的

from micropython import const
_IRQ_CENTRAL_CONNECT = const(1)
_IRQ_CENTRAL_DISCONNECT = const(2)
_IRQ_GATTS_WRITE = const(3)
_IRQ_GATTS_READ_REQUEST = const(4)
_IRQ_SCAN_RESULT = const(5)
_IRQ_SCAN_DONE = const(6)
_IRQ_PERIPHERAL_CONNECT = const(7)
_IRQ_PERIPHERAL_DISCONNECT = const(8)
_IRQ_GATTC_SERVICE_RESULT = const(9)
_IRQ_GATTC_SERVICE_DONE = const(10)
_IRQ_GATTC_CHARACTERISTIC_RESULT = const(11)
_IRQ_GATTC_CHARACTERISTIC_DONE = const(12)
_IRQ_GATTC_DESCRIPTOR_RESULT = const(13)
_IRQ_GATTC_DESCRIPTOR_DONE = const(14)
_IRQ_GATTC_READ_RESULT = const(15)
_IRQ_GATTC_READ_DONE = const(16)
_IRQ_GATTC_WRITE_DONE = const(17)
_IRQ_GATTC_NOTIFY = const(18)

? data是一个元组,其中conn_handle表示连接的句柄,不同的手机连接就会得到不同的句柄,通过conn_handle就可以区分不同的设备了。attr_handle则是特性句柄,event=3表示的是esp32接收数据的事件,通过conn_handle我们可以知道是哪个设备发来的数据,通过attr_handle我们则能知道发过来的是哪个特性的数据。

6. 数据收发实现

import struct
import time
import bluetooth        #导入BLE功能模块
ble = bluetooth.BLE()   #创建BLE设备
ble.active(True)         #打开BLE
name = "中国蓝牙".encode() # 编码成utf-8格式
adv_mode = bytearray(b'\x02\x01\x06')  # 正常蓝牙模式, ad struct 1
adv_name = bytearray((len(name) + 1, 0x09)) + name # 0x09是蓝牙名称,ad struct 2
adv_data = adv_mode + adv_name

env_service_uuid = bluetooth.UUID(0x181A) # 定义服务的uuid,映射就是环境监控服务
# 定义特性(接口)的uuid, 还需要指明这个特性是否可读写
env_tmp_uuid = (bluetooth.UUID(0x2A6E), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY ) # 温度特性
env_hum_uuid = (bluetooth.UUID(0x2A6F), bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY) # 湿度特性
# 组成环境检测服务,服务的uuid以及特性的uuid
env_service = (env_service_uuid, (env_tmp_uuid, env_hum_uuid))

services = (env_service, )
# 到蓝牙的sdk注册服务,拿到数据接口,通过这个接口可以读写数据
((tem_handle, hum_handle, ), ) = ble.gatts_register_services(services)
ble.gap_advertise(100, adv_data = adv_data)
conn_handles = []
def ble_irq(event, data): # 蓝牙中断函数
    if event == 1: #蓝牙已连接
        conn_handle, addr_type, addr = data
        conn_handles.append(conn_handle)
        print(f"fd = [{conn_handle}] connect")

    elif event == 2: #蓝牙断开连接
        conn_handle, addr_type, addr = data
        conn_handles.remove(conn_handle)
        print(f"fd = [{conn_handle}] disconnect")
        ble.gap_advertise(100, adv_data = adv_data)

    elif event == 3: #收到数据
        conn_handle, attr_handle = data
        print(f"fd = [{conn_handle}], char = [{attr_handle}] recive msg")

ble.irq(ble_irq)

while True:
    time.sleep(1)
    # 温度特性写入数据
    ble.gatts_write(tem_handle, struct.pack("<B", int(100)))
    # 对所有连接上的蓝牙设备发送数据
    for handle in conn_handles:
        ble.gatts_notify(handle, tem_handle)

?这里面特性权限让人非常困惑,为什么设置的READ权限?把READ权限给去掉之后再试,发现手机没办法读取数据了。突然意识到这个权限是开发给客户端的,就像我们平时写的服务一样,如果给用户提供了READ权限,客户端才能从服务端读取数据,否则是查询不到的。notify则表示客户端是否可以监听服务发送的数据,服务的数据是不断变化的,大部分时候我们希望服务端数据更新之后,可以收到服务端通知,所以需要设置notify权限

??? 权限,是指给客户端开通的权限,开通了read权限,客户端才能读取服务器的数据,开通了notify权限,用户才能收到服务器的通知
?

参考资料:

B站:蓝牙广播_哔哩哔哩_bilibili

esp32+micropython蓝牙讲解_esp32 蓝牙 micropython-CSDN博客

ESP32使用MicroPython设置低功耗蓝牙广播,通过Chrome Web蓝牙通信

bluetooth — low-level Bluetooth — MicroPython latest documentation

文章来源:https://blog.csdn.net/qq_58168857/article/details/135313693
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。