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

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

一、信道分布

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

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

二、数据组包

(一)广播数据包

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

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

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

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.广播数据包

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

3.蓝牙广播类型

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

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

4、信号握手流程

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

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

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

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

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

5.蓝牙状态机

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

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

蓝牙设备状态的转换:

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

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

序号 value name 备注
1 0x01  Flags
2 0x02 Incomplete List of 16-bit Service Class UUIDs
3 0x03 Complete List of 16-bit Service Class UUIDs
4 0x04 Incomplete List of 32-bit Service Class UUIDs
5 0x05 Complete List of 32-bit Service Class UUIDs
6 0x06 Incomplete List of 128-bit Service Class UUIDs
7 0x07 Complete List of 128-bit Service Class UUIDs
8 0x08 Shortened Local Name
9 0x09 Complete Local Name
10 0x0A Tx Power Level
11 0x0D Class of Device
12 0x0E Simple Pairing Hash C-192
13 0x0F Simple Pairing Randomizer R-192
14 0x10 Device ID
15 0x10 Security Manager TK Value
16 0x11 Security Manager Out of Band Flags
17 0x12 Peripheral Connection Interval Range
18 0x14 List of 16-bit Service Solicitation UUIDs
19 0x15 List of 128-bit Service Solicitation UUIDs
20 0x16 Service Data - 16-bit UUID
21 0x17 Public Target Address
22 0x18 Random Target Address
23 0x19 Appearance
24 0x1A Advertising Interval
25 0x1B LE Bluetooth Device Address
26 0x1C  LE Role
27 0x1D Simple Pairing Hash C-256
28 0x1E Simple Pairing Randomizer R-256
29 0x1F List of 32-bit Service Solicitation UUIDs
30 0x20 Service Data - 32-bit UUID
31 0x21 Service Data - 128-bit UUID
32 0x22 LE Secure Connections Confirmation Value
33 0x23 LE Secure Connections Random Value
34 0x24 URI
35 0x25 Indoor Positioning
36 0x26 Transport Discovery Data
37 0x27 LE Supported Features
38 0x28 Channel Map Update Indication
39 0x29 PB-ADV
40 0x2A Mesh Message
41 0x2B Mesh Beacon
42 0x2C BIGInfo
43 0x2D Broadcast_Code
44 0x2E Resolvable Set Identifier
45 0x2F Advertising Interval - long
46 0x30 Broadcast_Name
47 0x31 Encrypted Advertising Data
48 0x32 Periodic Advertising Response Timing Information
49 0x34 Electronic Shelf Label
50 0x3D 3D Information Data 3D Synchronization Profile
51 0xFF Manufacturer 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
低功耗蓝牙(BLE)基本概念汇总:信道分布、数据组包到数据交互_第9张图片

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

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

网上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。

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

    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("

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("

 这里面特性权限让人非常困惑,为什么设置的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

你可能感兴趣的:(物联网,网络,物联网,嵌入式硬件)