diff --git a/docs/Getting_started/zh/README.md b/docs/Getting_started/zh/README.md index a86b36e9af5b24b08e1cc55c73c176a762413f98..09b259251f5ce8aab7d3dbc0ea4c192387811b90 100644 --- a/docs/Getting_started/zh/README.md +++ b/docs/Getting_started/zh/README.md @@ -99,7 +99,7 @@ - [5.16 矩阵键盘](./hardware/matrix-keypad.md) -- [5.17 BT 和 BLE](./hardware/bt-and-ble.md) +- [5.17 BT 和 BLE](./hardware/bt/README.md) - [5.18 USB 网卡](./hardware/usb-wireless-card.md) diff --git a/docs/Getting_started/zh/hardware/README.md b/docs/Getting_started/zh/hardware/README.md index 67431785c092c86aaa8345e248f3f74e6535935a..db2ab206fb0a2b2ccf52c33ab06982e890950af7 100644 --- a/docs/Getting_started/zh/hardware/README.md +++ b/docs/Getting_started/zh/hardware/README.md @@ -12,7 +12,7 @@ - [8.6 外接存]() - [8.7 以太网卡]() - [8.8 矩阵键盘]() -- [8.9 BT 和 BLE]() +- [8.9 BT 和 BLE](./bt/README.md) - [8.10 USB 网卡]() - [8.10 USB 网卡]() - [8.11 外接 WiFi]() diff --git a/docs/Getting_started/zh/hardware/bt/README.md b/docs/Getting_started/zh/hardware/bt/README.md new file mode 100644 index 0000000000000000000000000000000000000000..458d3f3e5cb665c4b7468119527ff36acaa695f8 --- /dev/null +++ b/docs/Getting_started/zh/hardware/bt/README.md @@ -0,0 +1,90 @@ +# QuecPython 蓝牙功能使用说明 + +本文主要介绍QuecPython功能组件中的蓝牙功能,包括蓝牙基础概念、蓝牙功能的使用说明等。 + +## 概述 + +蓝牙技术是一种无线通信技术,可以让设备之间通过短距离的无线信号实现相互通信。如今蓝牙技术广泛应用各种电子设备,比如智能手机、蓝牙耳机、车载系统、音响等等。 + +目前,蓝牙技术主要有两种:经典蓝牙(BT)和低功耗蓝牙(BLE)。经典蓝牙技术是最早被开发出来,因此现在也经常会将经典蓝牙称为传统蓝牙。低功耗蓝牙则是一种相对较新的技术,设计初衷就是为了用于低功耗设备之间的通信,如智能手表、手环等设备。 + + + +## BT和BLE的区别 + +BT和BLE虽然都是蓝牙技术,但是两者在使用场景、功耗、传输速率等方面还是存在较大区别的。 + +| | BT | BLE | +| -------- | ------------------------------------------------------ | ------------------------------------------------------------ | +| 使用场景 | BT主要用于大数据量传输,如音乐、视频、图像文件等传输。 | BLE主要用于小数据量传输,常用于可穿戴设备,例如智能手环上,用于传输人体健康数据等。 | +| 传输速率 | BT传输速率较高,最高可以达到24Mbps。 | BLE传输速度相对较低,最高可达2Mbps。 | +| 功耗 | BT功耗较高。 | BLE功耗较低,适合需要长时间运行的设备,如智能手表、手环等。 | + + + +> 关于蓝牙的传输速率说明 +> +> 不同蓝牙协议版本的传输速率是不同的,比如: +> +> * 蓝牙1.x版本:最高传输速率为1 Mbps。 +> * 蓝牙2.x + EDR版本:最高传输速率为3 Mbps。 +> * 蓝牙3.x + HS版本:通过与802.11 Wi-Fi协议结合,理论上最高传输速率可以达到24 Mbps。 +> * 蓝牙4.x版本:该版本引入了低功耗蓝牙技术。BT模式下最高传输速率为3 Mbps,BLE模式下最高传输速率约为1 Mbps。 +> * 蓝牙5.x版本:最高传输速率为2 Mbps。 +> +> 需要注意,上述最高传输速率都是理论值,实际中受设备硬件限制、距离、环境干扰等因素影响,传输速率一般都是低于理论速度。 + + + +## 常见协议 + +蓝牙技术中包含了很多不同的协议和配置规范(profile),下面仅列出一些常见的协议规范: + +* GAP(Generic Access Profile):通用接入配置协议,主要用于设备之间的发现、配对和连接等功能。 +* GATT(Generic Attribute Protocol):通用属性配置协议,主要用于BLE设备之间的数据交换和通信。它定义了数据以服务和特征的形式进行组织的结构。 +* HFP(Hands-Free Profile):蓝牙免提配置协议,主要用于车载蓝牙和蓝牙耳机等设备的通话控制。如接听、挂断、拒接、语音拨号等。 +* A2DP(Advanced Audio Distribution Profile):高级音频分发配置协议,主要用于无线音频设备之间的高质量音频流传输。 +* AVRCP(Audio/Video Remote Control Profile):音频/视频远程控制配置协议,主要用于远程控制音频和视频的播放、暂停等操作。 +* SPP(Serial Port Profile):串行端口配置协议,用于在经典蓝牙设备之间建立虚拟串行端口进行通信。 + + + +## 常见概念 + +### 服务器和客户端 + +服务器(Server)和客户端(Client)指的是设备在通信过程中的角色。服务器负责存储和提供数据,而客户端则负责连接到服务器并请求访问其数据。在蓝牙通信中,服务器和客户端的角色是可以互相切换的。比如在BLE中,设备可以作为GATT服务器来提供数据,也可以作为GATT客户端来访问其他设备的数据服务。 + +### 主机和从机 + +主机(Master)和从机(Slave)指的是设备在连接过程中的角色。主机通常负责发起连接请求、管理连接参数并维护设备的连接状态。而从机则是被连接的设备,负责响应主机的请求。在蓝牙通信中,主机和从机的角色可以在不同连接过程中互相切换。 + + + +> 在蓝牙通信中,服务器/客户端与主机/从机是两个相互独立的概念,它们并没有直接的对应关系。具体的主从关系以及服务器和客户端关系,取决于设备间的连接和通信需求以及使用的是BT还是BLE。 + + + +## QuecPython蓝牙简介 + +QuecPython蓝牙功能组件包括`bt`和`ble`,使用的蓝牙协议是4.2版本。 + +### 低功耗蓝牙 + +QuecPython的`ble`支持做服务器和客户端,但是同一个时刻,只能工作在一种角色,不能同时作为服务器和客户端。 + +### 经典蓝牙 + +QuecPython的`bt`支持HFP、A2DP、AVRCP和SPP协议。当前仅支持做从机。 + + + +> 当前QuecPython经典蓝牙和低功耗蓝牙,不能同时使用,即同一个时刻,只能工作在其中一种模式。 + + + +## QuecPython蓝牙使用指导 + +- [QuecPython ble 使用说明](./ble.md) +- [QuecPython bt 使用说明](./bt.md) + diff --git a/docs/Getting_started/zh/hardware/bt/ble.md b/docs/Getting_started/zh/hardware/bt/ble.md new file mode 100644 index 0000000000000000000000000000000000000000..d9d89817a9764f08ce1ddb528eeca89678fcc693 --- /dev/null +++ b/docs/Getting_started/zh/hardware/bt/ble.md @@ -0,0 +1,1687 @@ +# QuecPython BLE 使用说明 + +本文主要介绍如何使用`QuecPython`的BLE功能,并提供一个基本的BLE的`Server`和`Client`示例来作为用户开发的参考。 + +## 1 BLE API使用说明 + +请参考`QuecPython`官方网站上的Wiki说明:[BLE API 说明](https://python.quectel.com/doc/API_reference/zh/QuecPython%E7%B1%BB%E5%BA%93/ble.html) + +## 2 GATT协议框架 + +GATT协议框架部分可以参考蓝牙官方协议文档中的如下框架图,通过该图可以清晰的看出profile、services、characteristics等之间的关系。 + +![GATT 协议框架](../../media/hardware/bt/GATT协议框架.png) + +通过上图可以看出,一个profile是由一个或多个服务(service)组成,一个服务(service)由一个或者多个特征(characteristic)组成。而每个特征(characteristic)又包含属性(property)、特征值(value)和特征描述(descriptor)。其中,用虚线框起来的部分,表示该部分内容是可选的,比如特征描述符。而一个服务至少需要包含一个特征,一个特征至少需要包含特征属性和特征值。 + + + +## 3 属性 + +在BLE GATT中,其实服务(Service)、特征(Characteristic)、特征值(Characteristic Value)以及特征描述符(Characteristic Descriptor)等,都可以称之为属性(Attribute)。而属性由四个部分构成,分别是句柄(Handle)、类型(Type)、属性值(Value)、权限(Permissions)。这部分可以参考蓝牙官方协议如下部分: + +![属性构成](../../media/hardware/bt/属性构成.png) + +下面分别针对服务(Service)、特征(Characteristic)、特征值(Characteristic Value)以及特征描述符(Descriptor)这些属性进行说明。 + +### 3.1 服务 + +服务(Service)是完成特定功能或特性的数据和相关行为的集合。在GATT中,服务是由服务定义(Service Defintion)来定义的。而服务定义应该包括服务声明(Service Declaration),也可能包括引用定义和特征定义。 + +服务声明(Service Declaration)如下 : + +![服务声明](../../media/hardware/bt/服务声明.png) + +在QuecPython BLE中,上述4个字段,用户可以配置的是`Attribute Type`字段和`Attribute Value`字段。QuecPython官网Wiki中有说明添加服务的接口如下: + +```python +ble.addService(primary, server_id, uuid_type, uuid_s, uuid_l) +``` + +* `primary`参数用来控制`Attribute Type`的值是0x2800还是0x2801。 + +* `uuid_type`、`uuid_s`和`uuid_l`参数即用来配置`Attribute Value`字段。 + +### 3.2 特征 + +特征(Characteristic)是服务中使用的一个值,同时还包含了关于如何访问该值以及如何显示或表示该值的属性和配置信息。在GATT中,特征是由特征定义(Characteristic Definition )来定义的。特征定义应该包含特征声明(Characteristic Declaration)、特征值声明(Characteristic Value Declaration ),可能包含特征描述符声明(Characteristic Descriptor Declaration )。 + +特征声明(Characteristic Declaration)如下: + +![特征声明](../../media/hardware/bt/特征声明.png) + +通过上图可知,特征的`Attribute Type`字段固定为0x2803。而用户可配置的是`Attribute Value`字段,针对该字段,蓝牙官方协议文档中描述如下: + +![属性值字段说明](../../media/hardware/bt/特征声明属性值字段说明.png) + +QuecPython官网Wiki中有说明添加特征的接口如下: + +```python +ble.addChara(server_id, chara_id, chara_prop, uuid_type, uuid_s, uuid_l) +``` + +* `chara_prop`参数配置的就是上图中的`Characteristic Properties`字段。 + +* `uuid_type`、`uuid_s`和`uuid_l`参数配置的就是上图中的`Characteristic UUID`字段。 + +* `Characteristic Value Handle`字段无需用户配置。 + +### 3.3 特征值 + +特征值(Characteristic Value)是特征的实际数据。一个特征必须包含一个特征值,即特征值是属于特征的一部分。 + +上文提到,特征定义应该包含特征声明(Characteristic Declaration)、特征值声明(Characteristic Value Declaration ),而特征值声明如下: + +![特征值声明](../../media/hardware/bt/特征值声明.png) + +QuecPython官网Wiki中有说明添加特征的接口如下: + +```python +ble.addCharaValue(server_id, chara_id, permission, uuid_type, uuid_s, uuid_l, value) +``` + +* `permission`参数配置的就是上图中的`Attribute Permissions`字段。 + +* `uuid_type`、`uuid_s`和`uuid_l`参数配置的就是上图中的`Attribute Type`字段。 + +* `value`参数配置的就是上图中的``Attribute Value`字段。 + +### 3.4 特征描述符 + +在GATT协议中,描述符是用于进一步描述特征值,比如特征值的单位、分辨率、精度、权限等。一个特征定义可以包含一个或者多个特征描述符声明。一些常见的特征描述符包括:特征扩展属性描述符、特征用户描述符、客户端特征配置描述符、服务端特征配置描述符、特征描述格式描述符以及特征聚合格式描述符。具体内容可以参考蓝牙官方协议文档中的[Vol 3, Part G, Characteristic Descriptor Declarations ] 部分的内容,这里不在详述。 + + + +## 4 BLE Server + +BLE Server指设备作为服务器,提供数据服务给其他设备来连接访问。 + +### 4.1 流程 + +要使设备作为BLE服务器,需要按照下面的流程来初始化并设置相关参数。 + +![BLE Server 流程](../../media/hardware/bt/BLE_Server流程.png) + +### 4.2 示例 + +下面提供一个BLE Server示例,该示例中设置了蓝牙名称为`Quectel_ble`,添加了一个电池相关的服务,并基于该服务添加了电池电量特征,同时启用了电池电量特征值的通知和指示功能。在支持蓝牙功能的模组上运行该例程后,可以通过手机端BLE相关的APP去搜索名为`Quectel_ble`的设备,并查看该设备提供的服务。 + +```python +# BLE Server + +import ble +import utime +import _thread +from queue import Queue + +BLE_GATT_SYS_SERVICE = 0 # 0-删除系统默认的GAP和GATT服务 1-保留系统默认的GAP和GATT服务 +BLE_SERVER_HANDLE = 0 +CONNECT_ID = 0 +BLE_IS_RUNNING = 1 + +_BLE_NAME = "Quectel_ble" + + +event_dict = { + 'BLE_START_STATUS_IND': 0, # ble start + 'BLE_STOP_STATUS_IND': 1, # ble stop + 'BLE_CONNECT_IND': 16, # ble connect + 'BLE_DISCONNECT_IND': 17, # ble disconnect + 'BLE_UPDATE_CONN_PARAM_IND': 18, # ble update connection parameter + 'BLE_SCAN_REPORT_IND': 19, # ble gatt client scan and report other devices + 'BLE_GATT_MTU': 20, # ble connection mtu + 'BLE_GATT_RECV_WRITE_IND': 21, # when ble client write characteristic value or descriptor,server get the notice + 'BLE_GATT_RECV_READ_IND': 22, # when ble client read characteristic value or descriptor,server get the notice + 'BLE_GATT_RECV_NOTIFICATION_IND': 23, # client receive notification + 'BLE_GATT_RECV_INDICATION_IND': 24, # client receive indication + 'BLE_GATT_SEND_END': 25, # server send notification,and receive send end notice +} + +class EVENT(dict): + def __getattr__(self, item): + return self[item] + + def __setattr__(self, key, value): + raise ValueError("{} is read-only.".format(key)) + + +event = EVENT(event_dict) +msg_queue = Queue(20) + + +def ble_callback(args): + global msg_queue + msg_queue.put(args) + + +def ble_gatt_server_event_handler(): + global BLE_GATT_SYS_SERVICE + global BLE_SERVER_HANDLE + global CONNECT_ID + global BLE_IS_RUNNING + global msg_queue + + while True: + msg = msg_queue.get() # 没有消息时会阻塞在这 + event_id = msg[0] + status = msg[1] + + if event_id == event.BLE_START_STATUS_IND: # ble start + print('[ble_callback]: event_id=BLE_START_STATUS_IND, status={}'.format(status)) + if status == 0: + print('[callback] BLE start success.') + mac = ble.getPublicAddr() + if mac != -1 and len(mac) == 6: + addr = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(mac[5], mac[4], mac[3], mac[2], mac[1], mac[0]) + print('BLE public addr : {}'.format(addr)) + ret = ble_gatt_set_name() + if ret != 0: + ble_gatt_close() + break + ret = ble_gatt_set_param() + if ret != 0: + ble_gatt_close() + break + ret = ble_gatt_set_data() + if ret != 0: + ble_gatt_close() + break + ret = ble_gatt_set_rsp_data() + if ret != 0: + ble_gatt_close() + break + ret = ble_gatt_add_service() + if ret != 0: + ble_gatt_close() + break + ret = ble_gatt_add_characteristic() + if ret != 0: + ble_gatt_close() + break + ret = ble_gatt_add_characteristic_value() + if ret != 0: + ble_gatt_close() + break + ret = ble_gatt_add_characteristic_desc() + if ret != 0: + ble_gatt_close() + break + ret = ble_gatt_add_service_complete() + if ret != 0: + ble_gatt_close() + break + ''' + 当BLE_GATT_SYS_SERVICE为1时,即保留系统默认的GAP和GATT服务,一些较低的句柄值通常被保留给系统默认的 + GAP(Generic Access Profile)和GATT(Generic Attribute Profile)服务;所以在保留系统默认的GAP + 和GATT服务的情况下,自定义服务和特征的句柄通常从较高的值开始分配,这样可以确保系统服务和自定义服务之间的 + 句柄不会发生冲突。 + ''' + if BLE_GATT_SYS_SERVICE == 0: + BLE_SERVER_HANDLE = 1 # 不可以修改该值 + else: + BLE_SERVER_HANDLE = 16 # 不可以修改该值 + + ret = ble_adv_start() + if ret != 0: + ble_gatt_close() + break + BLE_IS_RUNNING = 1 + else: + print('[callback] BLE start failed.') + elif event_id == event.BLE_STOP_STATUS_IND: # ble stop + print('[ble_callback]: event_id=BLE_STOP_STATUS_IND, status={}'.format(status)) + if status == 0: + print('[callback] ble stop successful.') + ble_status = ble.getStatus() + print('ble status is {}'.format(ble_status)) + ble_gatt_server_release() + else: + print('[callback] ble stop failed.') + elif event_id == event.BLE_CONNECT_IND: # ble connect + print('[ble_callback]: event_id=BLE_CONNECT_IND, status={}'.format(status)) + if status == 0: + print('[callback] ble connect successful.') + connect_id = msg[2] + addr = msg[3] + addr_str = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]) + print('[callback] connect_id = {}, addr = {}'.format(connect_id, addr_str)) + CONNECT_ID = connect_id + ret = ble_gatt_send_notification() + if ret == 0: + print('[callback] ble_gatt_send_notification successful.') + else: + print('[callback] ble_gatt_send_notification failed.') + ble_gatt_close() + break + else: + print('[callback] ble connect failed.') + elif event_id == event.BLE_DISCONNECT_IND: # ble disconnect + print('[ble_callback]: event_id=BLE_DISCONNECT_IND, status={}'.format(status)) + if status == 0: + print('[callback] ble disconnect successful.') + connect_id = msg[2] + addr = msg[3] + addr_str = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]) + ble_gatt_close() + print('[callback] connect_id = {}, addr = {}'.format(connect_id, addr_str)) + else: + print('[callback] ble disconnect failed.') + ble_gatt_close() + break + elif event_id == event.BLE_UPDATE_CONN_PARAM_IND: # ble update connection parameter + print('[ble_callback]: event_id=BLE_UPDATE_CONN_PARAM_IND, status={}'.format(status)) + if status == 0: + print('[callback] ble update parameter successful.') + connect_id = msg[2] + max_interval = msg[3] + min_interval = msg[4] + latency = msg[5] + timeout = msg[6] + print('[callback] connect_id={},max_interval={},min_interval={},latency={},timeout={}'.format(connect_id, max_interval, min_interval, latency, timeout)) + else: + print('[callback] ble update parameter failed.') + ble_gatt_close() + break + elif event_id == event.BLE_GATT_MTU: # ble connection mtu + print('[ble_callback]: event_id=BLE_GATT_MTU, status={}'.format(status)) + if status == 0: + print('[callback] ble connect mtu successful.') + handle = msg[2] + ble_mtu = msg[3] + print('[callback] handle = {:#06x}, ble_mtu = {}'.format(handle, ble_mtu)) + else: + print('[callback] ble connect mtu failed.') + ble_gatt_close() + break + elif event_id == event.BLE_GATT_RECV_WRITE_IND: + print('[ble_callback]: event_id=BLE_GATT_RECV_WRITE_IND, status={}'.format(status)) + if status == 0: + print('[callback] ble recv successful.') + data_len = msg[2] + data = msg[3] # 这是一个bytearray + attr_handle = msg[4] + short_uuid = msg[5] + long_uuid = msg[6] # 这是一个bytearray + print('len={}, data:{}'.format(data_len, data)) + print('attr_handle = {:#06x}'.format(attr_handle)) + print('short uuid = {:#06x}'.format(short_uuid)) + print('long uuid = {}'.format(long_uuid)) + else: + print('[callback] ble recv failed.') + ble_gatt_close() + break + elif event_id == event.BLE_GATT_RECV_READ_IND: + print('[ble_callback]: event_id=BLE_GATT_RECV_READ_IND, status={}'.format(status)) + if status == 0: + print('[callback] ble recv read successful.') + data_len = msg[2] + data = msg[3] # 这是一个bytearray + attr_handle = msg[4] + short_uuid = msg[5] + long_uuid = msg[6] # 这是一个bytearray + print('len={}, data:{}'.format(data_len, data)) + print('attr_handle = {:#06x}'.format(attr_handle)) + print('short uuid = {:#06x}'.format(short_uuid)) + print('long uuid = {}'.format(long_uuid)) + else: + print('[callback] ble recv read failed.') + ble_gatt_close() + break + elif event_id == event.BLE_GATT_SEND_END: + print('[ble_callback]: event_id=BLE_GATT_SEND_END, status={}'.format(status)) + if status == 0: + print('[callback] ble send data successful.') + else: + print('[callback] ble send data failed.') + else: + print('unknown event id.') + + +def ble_gatt_server_init(cb): + ret = ble.serverInit(cb) + if ret != 0: + print('ble_gatt_server_init failed.') + return -1 + print('ble_gatt_server_init success.') + return 0 + + +def ble_gatt_server_release(): + ret = ble.serverRelease() + if ret != 0: + print('ble_gatt_server_release failed.') + return -1 + print('ble_gatt_server_release success.') + return 0 + + +def ble_gatt_open(): + ret = ble.gattStart() + if ret != 0: + print('ble_gatt_open failed.') + return -1 + print('ble_gatt_open success.') + return 0 + + +def ble_gatt_close(): + global BLE_IS_RUNNING + + ret = ble.gattStop() + if ret != 0: + print('ble_gatt_close failed.') + return -1 + print('ble_gatt_close success.') + BLE_IS_RUNNING = 0 + return 0 + + +def ble_gatt_set_name(): + code = 0 # utf8 + name = _BLE_NAME + ret = ble.setLocalName(code, name) + if ret != 0: + print('ble_gatt_set_name failed.') + return -1 + print('ble_gatt_set_name success.') + return 0 + + +def ble_gatt_set_param(): + min_adv = 0x300 + max_adv = 0x320 + adv_type = 0 # 可连接的非定向广播,默认选择 + addr_type = 0 # 公共地址 + channel = 0x07 + filter_strategy = 0 # 处理所有设备的扫描和连接请求 + discov_mode = 2 # 该参数实际不起作用,需要在广播数据中设置 + no_br_edr = 1 # 该参数实际不起作用,需要在广播数据中设置 + enable_adv = 1 + ret = ble.setAdvParam(min_adv, max_adv, adv_type, addr_type, channel, filter_strategy, discov_mode, no_br_edr, enable_adv) + if ret != 0: + print('ble_gatt_set_param failed.') + return -1 + print('ble_gatt_set_param success.') + return 0 + +''' +如果我们希望其他设备扫描时,能在扫描结果中看到该设备的名称,就需要在广播数据中包含设备名称。 +''' +def ble_gatt_set_data(): + adv_data = [0x02, 0x01, 0x05] + ble_name = _BLE_NAME + length = len(ble_name) + 1 + adv_data.append(length) + adv_data.append(0x09) + name_encode = ble_name.encode('UTF-8') + for i in range(0, len(name_encode)): + adv_data.append(name_encode[i]) + print('set adv_data:{}'.format(adv_data)) + data = bytearray(adv_data) + ret = ble.setAdvData(data) + if ret != 0: + print('ble_gatt_set_data failed.') + return -1 + print('ble_gatt_set_data success.') + return 0 + + +def ble_gatt_set_rsp_data(): + adv_data = [] + ble_name = _BLE_NAME + length = len(ble_name) + 1 + adv_data.append(length) + adv_data.append(0x09) + name_encode = ble_name.encode('UTF-8') + for i in range(0, len(name_encode)): + adv_data.append(name_encode[i]) + print('set adv_rsp_data:{}'.format(adv_data)) + data = bytearray(adv_data) + ret = ble.setAdvRspData(data) + if ret != 0: + print('ble_gatt_set_rsp_data failed.') + return -1 + print('ble_gatt_set_rsp_data success.') + return 0 + + +def ble_gatt_add_service(): + primary = 1 + server_id = 0x01 + uuid_type = 1 # 短UUID + uuid_s = 0x180F # Battery Service 电池数据 + uuid_l = bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ret = ble.addService(primary, server_id, uuid_type, uuid_s, uuid_l) + if ret != 0: + print('ble_gatt_add_service failed.') + return -1 + print('ble_gatt_add_service success.') + return 0 + + +def ble_gatt_add_characteristic(): + server_id = 0x01 + chara_id = 0x01 + chara_prop = 0x02 | 0x10 | 0x20 # 0x02-可读 0x10-通知 0x20-指示 + uuid_type = 1 # 短UUID + uuid_s = 0x2A19 # Battery Level 电池电量 + uuid_l = bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ret = ble.addChara(server_id, chara_id, chara_prop, uuid_type, uuid_s, uuid_l) + if ret != 0: + print('ble_gatt_add_characteristic failed.') + return -1 + print('ble_gatt_add_characteristic success.') + return 0 + + +def ble_gatt_add_characteristic_value(): + data = [0x16] # 测试数据 + server_id = 0x01 + chara_id = 0x01 + permission = 0x0001 | 0x0002 + uuid_type = 1 # 短UUID + uuid_s = 0x2A19 # Battery Level 电池电量 + uuid_l = bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + value = bytearray(data) + ret = ble.addCharaValue(server_id, chara_id, permission, uuid_type, uuid_s, uuid_l, value) + if ret != 0: + print('ble_gatt_add_characteristic_value failed.') + return -1 + print('ble_gatt_add_characteristic_value success.') + return 0 + + +def ble_gatt_add_characteristic_desc(): + data = [0x00, 0x00] + server_id = 0x01 + chara_id = 0x01 + permission = 0x0001 | 0x0002 + uuid_type = 1 # 短UUID + ''' + 在BLE服务器中,UUID为0x2902的特征描述符是Client Characteristic Configuration Descriptor(CCCD), + 它用于管理特征值的通知和指示功能。如果某个Characteristic需要支持通知(notifications)或指示(indicatons), + 则它必须添加CCCD。 + 为特征添加UUID为0x2902的特征描述符后,客户端可以通过写入该描述符来启用或禁用特征的通知或指示功能。 + 例如,客户端可以写入0x0001来启用通知,写入0x0002来启用指示,或写入0x0000来禁用这两种功能。 + ''' + uuid_s = 0x2902 + uuid_l = bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + value = bytearray(data) + ret = ble.addCharaDesc(server_id, chara_id, permission, uuid_type, uuid_s, uuid_l, value) + if ret != 0: + print('ble_gatt_add_characteristic_desc failed.') + return -1 + print('ble_gatt_add_characteristic_desc success.') + return 0 + +''' +通知是通过特征值的句柄来发送的,因此发送之前需要先确定句柄值 +''' +def ble_gatt_send_notification(): + global BLE_SERVER_HANDLE + global CONNECT_ID + data = [0x20] # 测试数据 + conn_id = CONNECT_ID + ''' + 这里attr_handle之所以取值 BLE_SERVER_HANDLE + 2,是因为: + BLE_SERVER_HANDLE是定义的起始句柄,第一个添加的服务的句柄即为BLE_SERVER_HANDLE; + 后面每添加一个特征、特征值、特征描述符时,句柄值都会自动加1; + 通过前面代码可以看到,我们依次添加了一个服务、一个特征、一个特征值、一个特征描述符。 + 添加了一个服务(UUID是0x180F),对应服务句柄就是BLE_SERVER_HANDLE; + 添加了一个特征(UUID是0x2A19),对应的特征句柄是BLE_SERVER_HANDLE+1 + 添加了一个特征值(UUID是0x2A19),对应的特征值句柄是BLE_SERVER_HANDLE+2 + 添加了一个特征描述符(UUID是0x2902),对应的特征描述符句柄是BLE_SERVER_HANDLE+3,这个是CCCD的句柄。 + 而通知是通过特征值句柄来发送,在本例程中,即通过UUID为0x2A19(电池电量)这个特征值句柄来发送,因此attr_handle = BLE_SERVER_HANDLE + 2 + ''' + attr_handle = BLE_SERVER_HANDLE + 2 + value = bytearray(data) + ret = ble.sendNotification(conn_id, attr_handle, value) + if ret != 0: + print('sendNotification failed.') + return -1 + print('sendNotification success.') + return 0 + + +def ble_gatt_add_service_complete(): + global BLE_GATT_SYS_SERVICE + ret = ble.addOrClearService(1, BLE_GATT_SYS_SERVICE) + if ret != 0: + print('ble_gatt_add_service_complete failed.') + return -1 + print('ble_gatt_add_service_complete success.') + return 0 + + +def ble_gatt_clear_service_complete(): + global BLE_GATT_SYS_SERVICE + ret = ble.addOrClearService(0, BLE_GATT_SYS_SERVICE) + if ret != 0: + print('ble_gatt_clear_service_complete failed.') + return -1 + print('ble_gatt_clear_service_complete success.') + return 0 + + +def ble_adv_start(): + ret = ble.advStart() + if ret != 0: + print('ble_adv_start failed.') + return -1 + print('ble_adv_start success.') + return 0 + + +def ble_adv_stop(): + ret = ble.advStop() + if ret != 0: + print('ble_adv_stop failed.') + return -1 + print('ble_adv_stop success.') + return 0 + + +def main(): + global BLE_IS_RUNNING + _thread.start_new_thread(ble_gatt_server_event_handler, ()) + ret = ble_gatt_server_init(ble_callback) + if ret == 0: + ret = ble_gatt_open() + if ret != 0: + return -1 + else: + return -1 + count = 0 + while True: + utime.sleep(1) + count += 1 + if count % 5 == 0: + print('##### BLE running, count = {}......'.format(count)) + + if BLE_IS_RUNNING == 0: + count = 0 + print('!!!!! Ready to exit !!!!!') + return 0 + + +if __name__ == '__main__': + main() + +``` + + + +### 4.3 说明 + +下面就QuecPython的BLE和示例代码做一些说明。 + +#### 4.3.1 服务句柄取值说明 + +示例代码中可以看到`BLE_SERVER_HANDLE`的值根据`BLE_GATT_SYS_SERVICE`不同,可能是`1`,也可能是`16`。这里有如下两点需要说明: + +* QuecPython的`BLE Server`,句柄是在C代码中自动管理的,根据`BLE_GATT_SYS_SERVICE`取值不同,C代码中初始值为`1`或`16`;因此示例代码中`BLE_SERVER_HANDLE`的初始值也必须是`1`或`16`,与C代码中保持一致。 +* 当`BLE_GATT_SYS_SERVICE`为`1`时,即保留系统默认的`GAP`和`GATT`服务,一些较低的句柄值通常被保留给系统默认的`GAP(Generic Access Profile)`和`GATT(Generic Attribute Profile)`服务。所以在保留系统默认的`GAP`和`GATT`服务的情况下,自定义服务和特征的句柄通常从较高的值开始分配,这样的设计可以确保系统服务和自定义服务之间的句柄不会发生冲突。 + + + +#### 4.3.2 UUID为0x2902的特征描述符 + +在BLE服务器中,UUID为`0x2902`的特征描述符是指客户端特征配置描述符(Client Characteristic Configuration Descriptor),简称`CCCD`,它用于管理特征值的通知和指示功能。当需要向客户端设备发送特征值的实时更新时,可以使用通知(Notifications)和指示(Indications)这两种机制。如果某个`Characteristic`需要支持通知或指示,则它必须添加`CCCD`。 + + + +#### 4.3.3 关于通知和指示 + +通知和指示是通过特征值的句柄来发送的,因此发送之前需要先确定特征值句柄。特征值句柄是在定义`GATT`服务和特征时自动分配的,起始句柄是`BLE_SERVER_HANDLE`,每添加一个服务(service),分配的对应句柄值自动加1;给该服务添加一个特征时,分配的对应句柄值自动加1;给该特征添加一个特征值时,分配的对应句柄值自动加1;给该特征添加一个特征描述符时,分配的对应句柄值自动加1。可根据该原则计算出某个特征值句柄。 + +如果`BLE Server`发送通知或者指示失败,可能有如下原因: + +* 发送时,使用的特征值句柄不对,按照上述计算方法进一步确认是否计算有误。 +* 发送的数据长度不对,发送的数据长度大于添加特征值时的数据长度导致,须保证发送的数据长度不超过添加特征值时的数据长度。 + + + +#### 4.3.4 BLE广播数据 + +广播数据是指设备在广播状态下发送出去的数据包,通常一条广播数据最大长度限制为31个字节,用于通知周边设备自己的存在以及提供一些基本的设备信息。比如设备名称、设备类型、服务UUID等。 + +BLE的广播数据是有固定的数据结构要求的。具体结构为:长度+类型+数据。 + +* 长度:固定占一个字节,表示接下来的广播数据段的长度,包含“类型”字段。 +* 类型:固定占一个字节,表示广播数据段的类型,如设备名称、设备类型等。 +* 数据:长度由“长度”字段指定,包含实际的广播数据类容,根据“类型”字段不同,数据的结构和内容也会有所不同。 + +需要注意的是,设备可以在一条广播数据包中包含多个上述的数据结构。 + + + +#### 4.3.5 广播数据类型 + +完整的广播数据类型说明,请参考蓝牙官方的文档《Generic Access Profile》部分。下面仅列出一些常见的广播数据类型。 + +| Type值 | Type名称 | 说明 | +| ------ | ---------------------------------------------- | ------------------------------------------------------------ | +| 0x01 | Flags | 标识设备支持的通用属性,比如是否支持LE(低功耗)模式、是否支持广播等。 | +| 0x02 | Incomplete List of 16-bit Service Class UUIDs | 列出设备支持的 16 位服务 UUID(唯一标识符)列表。Incomplete 表示列表未完整列出,即只列出了部分服务 UUID。 | +| 0x03 | Complete List of 16-bit Service Class UUIDs | 列出设备支持的 16 位服务 UUID 列表。Complete 表示列表完整列出,即列出了设备支持的所有服务 UUID。 | +| 0x04 | Incomplete List of 32-bit Service Class UUIDs | 列出设备支持的 32 位服务 UUID 列表。Incomplete 表示列表未完整列出,即只列出了部分服务 UUID。 | +| 0x05 | Complete List of 32-bit Service Class UUIDs | 列出设备支持的 32 位服务 UUID 列表。Complete 表示列表完整列出,即列出了设备支持的所有服务 UUID。 | +| 0x06 | Incomplete List of 128-bit Service Class UUIDs | 列出设备支持的 128 位服务 UUID 列表。Incomplete 表示列表未完整列出,即只列出了部分服务 UUID。 | +| 0x07 | Complete List of 128-bit Service Class UUIDs | 列出设备支持的 128 位服务 UUID 列表。Complete 表示列表完整列出,即列出了设备支持的所有服务 UUID。 | +| 0x08 | Shortened Local Name | 设备的名称,以字符串形式表示。如果名称过长,可能会被截断,只列出了名称的一部分。 | +| 0x09 | Complete Local Name | 设备的完整名称,以字符串形式表示。 | + + + +#### 4.3.6 设置发现模式和是否支持BR/EDR + +在设置广播参数接口`ble.setAdvParam`中提供了这两个功能的参数设置项,实际该接口中关于发现模式和是否支持BR/EDR的参数并没有真正使用,目前仅作保留。真正设置这两个参数的方式是在设置广播数据接口`ble.setAdvData`中设置,具体如下: + +看示例代码中,设置广播数据的代码如下: + +```python +def ble_gatt_set_data(): + adv_data = [0x02, 0x01, 0x05] + ...... # 其他代码省略 +``` + +注意到广播数据中,我们给的第一组数据是`[0x02, 0x01, 0x05]`,其中`0x02`表示数据长度,表示后面有两个字节数据,第二个`0x01`表示类型,而第三个数据`0x05`则表示:有限发现模式且不支持BR/EDR。如果我们需要设置发现模式和是否支持BR/EDR,则需要修改该值,具体为: + +| 参数值 | 说明 | +| ------ | ------------------------------ | +| 0x01 | 有限发现模式,并且支持BR/EDR | +| 0x02 | 一般发现模式,并且支持BR/EDR | +| 0x05 | 有限发现模式,并且不支持BR/EDR | +| 0x06 | 一般发现模式,并且不支持BR/EDR | + + + +## 5 BLE Client + +BLE Client指设备作为客户端,主动去连接服务器并访问其数据。 + +### 5.1 流程 + +要使设备作为BLE客户端,需要按照下面的流程来初始化并设置相关参数。 + +![BLE Client 流程](../../media/hardware/bt/BLE_Client流程.png) + + + +### 5.2 示例 + +下面提供一个BLE Client示例,该示例中将设备作为客户端角色,主动去扫描周边的BLE设备,如果扫描到名称为`Quectel_ble`的设备就停止扫描,并向该设备发起连接,然后去发现该设备提供了哪些服务和服务特征等。 + + + +> 实际应用中,为了降低功耗,一般是根据UUID去发起服务发现请求,而不是发现所有的服务。 + + + +```python +# BLE Client + +import ble +import utime +import _thread +from queue import Queue + + +event_dict = { + 'BLE_START_STATUS_IND': 0, # ble start + 'BLE_STOP_STATUS_IND': 1, # ble stop + 'BLE_CONNECT_IND': 16, # ble connect + 'BLE_DISCONNECT_IND': 17, # ble disconnect + 'BLE_UPDATE_CONN_PARAM_IND': 18, # ble update connection parameter + 'BLE_SCAN_REPORT_IND': 19, # ble gatt client scan and report other devices + 'BLE_GATT_MTU': 20, # ble connection mtu + 'BLE_GATT_RECV_NOTIFICATION_IND': 23, # client receive notification + 'BLE_GATT_RECV_INDICATION_IND': 24, # client receive indication + 'BLE_GATT_START_DISCOVER_SERVICE_IND': 26, # start discover service + 'BLE_GATT_DISCOVER_SERVICE_IND': 27, # discover service + 'BLE_GATT_DISCOVER_CHARACTERISTIC_DATA_IND': 28, # discover characteristic + 'BLE_GATT_DISCOVER_CHARA_DESC_IND': 29, # discover characteristic descriptor + 'BLE_GATT_CHARA_WRITE_WITH_RSP_IND': 30, # write characteristic value with response + 'BLE_GATT_CHARA_WRITE_WITHOUT_RSP_IND': 31, # write characteristic value without response + 'BLE_GATT_CHARA_READ_IND': 32, # read characteristic value by handle + 'BLE_GATT_CHARA_READ_BY_UUID_IND': 33, # read characteristic value by uuid + 'BLE_GATT_CHARA_MULTI_READ_IND': 34, # read multiple characteristic value + 'BLE_GATT_DESC_WRITE_WITH_RSP_IND': 35, # write characteristic descriptor + 'BLE_GATT_DESC_READ_IND': 36, # read characteristic descriptor + 'BLE_GATT_ATT_ERROR_IND': 37, # attribute error +} + +gatt_status_dict = { + 'BLE_GATT_IDLE' : 0, + 'BLE_GATT_DISCOVER_SERVICE': 1, + 'BLE_GATT_DISCOVER_INCLUDES': 2, + 'BLE_GATT_DISCOVER_CHARACTERISTIC': 3, + 'BLE_GATT_WRITE_CHARA_VALUE': 4, + 'BLE_GATT_WRITE_CHARA_DESC': 5, + 'BLE_GATT_READ_CHARA_VALUE': 6, + 'BLE_GATT_READ_CHARA_DESC': 7, +} + + +class EVENT(dict): + def __getattr__(self, item): + return self[item] + + def __setattr__(self, key, value): + raise ValueError("{} is read-only.".format(key)) + + +class BleClient(object): + def __init__(self): + self.ble_server_name = 'Quectel_ble' # 目标设备ble名称 + self.connect_id = 0 + self.connect_addr = 0 + self.gatt_statue = 0 + self.discover_service_mode = 0 # 0-discover all service, 1-discover service by uuid + + self.scan_param = { + 'scan_mode': 1, # 积极扫描 + 'interval': 0x100, + 'scan_window': 0x50, + 'filter_policy': 0, + 'local_addr_type': 0, + } + + self.scan_report_info = { + 'event_type': 0, + 'name': '', + 'addr_type': 0, + 'addr': bytearray(0), + 'rssi': 0, + 'data_len': 0, + 'raw_data': 0, + } + + self.target_service = { + 'start_handle': 0, + 'end_handle': 0, + 'uuid_type': 1, # 短uuid + 'short_uuid': 0x180F, # 电池电量服务 + 'long_uuid': bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + } + + self.characteristic_list = [] + self.descriptor_list = [] + self.characteristic_count = 0 # ql_ble_gatt_chara_count + self.chara_descriptor_count = 0 # ql_ble_gatt_chara_desc_count + self.characteristic_index = 0 # ql_ble_gatt_chara_desc_index + self.current_chara_index = 0 # ql_ble_gatt_cur_chara + self.current_desc_index = 0 # ql_ble_gatt_chara_cur_desc + self.ble_short_uuid_pair_len = 7 + self.ble_long_uuid_pair_len = 21 + + ret = ble.clientInit(self.ble_client_callback) + if ret != 0: + print('ble client initialize failed.') + raise ValueError("BLE Client Init failed.") + else: + print('ble client initialize successful.') + print('') + + @staticmethod + def gatt_open(): + ret = ble.gattStart() + if ret != 0: + print('ble open failed.') + else: + print('ble open successful.') + print('') + return ret + + @staticmethod + def gatt_close(): + ret = ble.gattStop() + if ret != 0: + print('ble close failed.') + else: + print('ble close successful.') + print('') + return ret + + @staticmethod + def gatt_get_status(): + return ble.getStatus() + + @staticmethod + def release(): + ret = ble.clientRelease() + if ret != 0: + print('ble client release failed.') + else: + print('ble client release successful.') + print('') + return ret + + def set_scan_param(self): + scan_mode = self.scan_param['scan_mode'] + interval = self.scan_param['interval'] + scan_time = self.scan_param['scan_window'] + filter_policy = self.scan_param['filter_policy'] + local_addr_type = self.scan_param['local_addr_type'] + ret = ble.setScanParam(scan_mode, interval, scan_time, filter_policy, local_addr_type) + if ret != 0: + print('ble client set scan-parameters failed.') + else: + print('ble client set scan-parameters successful.') + print('') + return ret + + @staticmethod + def start_scan(): + ret = ble.scanStart() + if ret != 0: + print('ble client scan failed.') + else: + print('ble client scan successful.') + print('') + return ret + + @staticmethod + def stop_scan(): + ret = ble.scanStop() + if ret != 0: + print('ble client failed to stop scanning.') + else: + print('ble client scan stopped successfully.') + print('') + return ret + + def connect(self): + print('start to connect.....') + addr_type = self.scan_report_info['addr_type'] + addr = self.scan_report_info['addr'] + if addr != 0 and len(addr) == 6: + addr_str = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]) + print('addr_type : {}, addr : {}'.format(addr_type, addr_str)) + ret = ble.connect(addr_type, addr) + if ret != 0: + print('ble client connect failed.') + else: + print('ble client connect successful.') + print('') + return ret + + def cancel_connect(self): + ret = ble.cancelConnect(self.scan_report_info['addr']) + if ret != 0: + print('ble client cancel connect failed.') + else: + print('ble client cancel connect successful.') + print('') + return ret + + def disconnect(self): + ret = ble.disconnect(self.connect_id) + if ret != 0: + print('ble client disconnect failed.') + else: + print('ble client disconnect successful.') + print('') + return ret + + def discover_all_service(self): + ret = ble.discoverAllService(self.connect_id) + if ret != 0: + print('ble client discover all service failed.') + else: + print('ble client discover all service successful.') + print('') + return ret + + def discover_service_by_uuid(self): + connect_id = self.connect_id + uuid_type = self.target_service['uuid_type'] + short_uuid = self.target_service['short_uuid'] + long_uuid = self.target_service['long_uuid'] + ret = ble.discoverByUUID(connect_id, uuid_type, short_uuid, long_uuid) + if ret != 0: + print('ble client discover service by uuid failed.') + else: + print('ble client discover service by uuid successful.') + print('') + return ret + + def discover_all_includes(self): + connect_id = self.connect_id + start_handle = self.target_service['start_handle'] + end_handle = self.target_service['end_handle'] + ret = ble.discoverAllIncludes(connect_id, start_handle, end_handle) + if ret != 0: + print('ble client discover all includes failed.') + else: + print('ble client discover all includes successful.') + print('') + return ret + + def discover_all_characteristic(self): + connect_id = self.connect_id + start_handle = self.target_service['start_handle'] + end_handle = self.target_service['end_handle'] + ret = ble.discoverAllChara(connect_id, start_handle, end_handle) + if ret != 0: + print('ble client discover all characteristic failed.') + else: + print('ble client discover all characteristic successful.') + print('') + return ret + + def discover_all_characteristic_descriptor(self): + connect_id = self.connect_id + index = self.characteristic_index + start_handle = self.characteristic_list[index]['value_handle'] + 1 + + if self.characteristic_index == (self.characteristic_count - 1): + end_handle = self.target_service['end_handle'] + print('[1]start_handle = {:#06x}, end_handle = {:#06x}'.format(start_handle - 1, end_handle)) + ret = ble.discoverAllCharaDesc(connect_id, start_handle, end_handle) + else: + end_handle = self.characteristic_list[index+1]['handle'] - 1 + print('[2]start_handle = {:#06x}, end_handle = {:#06x}'.format(start_handle - 1, end_handle)) + ret = ble.discoverAllCharaDesc(connect_id, start_handle, end_handle) + self.characteristic_index += 1 + if ret != 0: + print('ble client discover all characteristic descriptor failed.') + else: + print('ble client discover all characteristic descriptor successful.') + print('') + return ret + + def read_characteristic_by_uuid(self): + connect_id = self.connect_id + index = self.current_chara_index # 根据需要改变该值 + start_handle = self.characteristic_list[index]['handle'] + end_handle = self.characteristic_list[index]['value_handle'] + uuid_type = 1 + short_uuid = self.characteristic_list[index]['short_uuid'] + long_uuid = bytearray([0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00]) + + ret = ble.readCharaByUUID(connect_id, start_handle, end_handle, uuid_type, short_uuid, long_uuid) + if ret != 0: + print('ble client read characteristic by uuid failed.') + else: + print('ble client read characteristic by uuid successful.') + print('') + return ret + + def read_characteristic_by_handle(self): + connect_id = self.connect_id + index = self.current_chara_index # 根据需要改变该值 + handle = self.characteristic_list[index]['value_handle'] + offset = 0 + is_long = 0 + + ret = ble.readCharaByHandle(connect_id, handle, offset, is_long) + if ret != 0: + print('ble client read characteristic by handle failed.') + else: + print('ble client read characteristic by handle successful.') + print('') + return ret + + def read_characteristic_descriptor(self): + connect_id = self.connect_id + index = self.current_desc_index # 根据需要改变该值 + handle = self.descriptor_list[index]['handle'] + print('handle = {:#06x}'.format(handle)) + is_long = 0 + ret = ble.readCharaDesc(connect_id, handle, is_long) + if ret != 0: + print('ble client read characteristic descriptor failed.') + else: + print('ble client read characteristic descriptor successful.') + print('') + return ret + + def write_characteristic(self): + connect_id = self.connect_id + index = self.current_chara_index # 根据需要改变该值 + handle = self.characteristic_list[index]['value_handle'] + offset = 0 + is_long = 0 + data = bytearray([0x40, 0x00]) + print('value_handle = {:#06x}, uuid = {:#06x}'.format(handle, self.characteristic_list[index]['short_uuid'])) + ret = ble.writeChara(connect_id, handle, offset, is_long, data) + if ret != 0: + print('ble client write characteristic failed.') + else: + print('ble client read characteristic successful.') + print('') + return ret + + def write_characteristic_no_rsp(self): + connect_id = self.connect_id + index = self.current_chara_index # 根据需要改变该值 + handle = self.characteristic_list[index]['value_handle'] + data = bytearray([0x20, 0x00]) + print('value_handle = {:#06x}, uuid = {:#06x}'.format(handle, self.characteristic_list[index]['short_uuid'])) + ret = ble.writeCharaNoRsp(connect_id, handle, data) + if ret != 0: + print('ble client write characteristic no rsp failed.') + else: + print('ble client read characteristic no rsp successful.') + print('') + return ret + + def write_characteristic_descriptor(self): + connect_id = self.connect_id + index = self.current_desc_index # 根据需要改变该值 + handle = self.descriptor_list[index]['handle'] + data = bytearray([0x01, 0x02]) + print('handle = {:#06x}'.format(handle)) + + ret = ble.writeCharaDesc(connect_id, handle, data) + if ret != 0: + print('ble client write characteristic descriptor failed.') + else: + print('ble client read characteristic descriptor successful.') + print('') + return ret + + @staticmethod + def ble_client_callback(args): + global msg_queue + msg_queue.put(args) + + +def ble_gatt_client_event_handler(): + global msg_queue + old_time = 0 + + while True: + cur_time = utime.localtime() + timestamp = "{:02d}:{:02d}:{:02d}".format(cur_time[3], cur_time[4], cur_time[5]) + if cur_time[5] != old_time and cur_time[5] % 5 == 0: + old_time = cur_time[5] + print('[{}]event handler running.....'.format(timestamp)) + print('') + msg = msg_queue.get() # 没有消息时会阻塞在这 + # print('msg : {}'.format(msg)) + event_id = msg[0] + status = msg[1] + + if event_id == event.BLE_START_STATUS_IND: + print('') + print('event_id : BLE_START_STATUS_IND, status = {}'.format(status)) + if status == 0: + print('BLE start successful.') + ble_status = ble_client.gatt_get_status() + if ble_status == 0: + print('BLE Status : stopped.') + break + elif ble_status == 1: + print('BLE Status : started.') + else: + print('get ble status error.') + ble_client.gatt_close() + break + + ret = ble_client.set_scan_param() + if ret != 0: + ble_client.gatt_close() + break + ret = ble_client.start_scan() + if ret != 0: + ble_client.gatt_close() + break + else: + print('BLE start failed.') + break + elif event_id == event.BLE_STOP_STATUS_IND: + print('') + print('event_id : BLE_STOP_STATUS_IND, status = {}'.format(status)) + if status == 0: + print('ble stop successful.') + else: + print('ble stop failed.') + break + elif event_id == event.BLE_CONNECT_IND: + print('') + print('event_id : BLE_CONNECT_IND, status = {}'.format(status)) + if status == 0: + ble_client.connect_id = msg[2] + ble_client.connect_addr = msg[3] + addr = ble_client.connect_addr + addr_str = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]) + print('connect_id : {:#x}, connect_addr : {}'.format(ble_client.connect_id, addr_str)) + else: + print('ble connect failed.') + break + elif event_id == event.BLE_DISCONNECT_IND: + print('') + print('event_id : BLE_DISCONNECT_IND, status = {}'.format(status)) + if status == 0: + ble_client.connect_id = msg[2] + ble_client.connect_addr = msg[3] + addr = ble_client.connect_addr + addr_str = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]) + print('connect_id : {:#x}, connect_addr : {}'.format(ble_client.connect_id, addr_str)) + else: + print('ble disconnect failed.') + ble_client.gatt_close() + break + elif event_id == event.BLE_UPDATE_CONN_PARAM_IND: + print('') + print('event_id : BLE_UPDATE_CONN_PARAM_IND, status = {}'.format(status)) + if status == 0: + connect_id = msg[2] + max_interval = msg[3] + min_interval = msg[4] + latency = msg[5] + timeout = msg[6] + print('connect_id={},max_interval={},min_interval={},latency={},timeout={}'.format(connect_id,max_interval,min_interval,latency,timeout)) + else: + print('ble update parameter failed.') + ble_client.gatt_close() + break + elif event_id == event.BLE_SCAN_REPORT_IND: + if status == 0: + # print(' ble scan successful.') + + ble_client.scan_report_info['event_type'] = msg[2] + ble_client.scan_report_info['name'] = msg[3] + ble_client.scan_report_info['addr_type'] = msg[4] + ble_client.scan_report_info['addr'] = msg[5] + ble_client.scan_report_info['rssi'] = msg[6] + ble_client.scan_report_info['data_len'] = msg[7] + ble_client.scan_report_info['raw_data'] = msg[8] + + device_name = ble_client.scan_report_info['name'] + addr = ble_client.scan_report_info['addr'] + rssi = ble_client.scan_report_info['rssi'] + addr_type = ble_client.scan_report_info['addr_type'] + addr_str = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]) + if device_name != '' and rssi != 0: + print('name: {}, addr: {}, rssi: {}, addr_type: {}'.format(device_name, addr_str, rssi, addr_type)) + print('raw_data: {}'.format(ble_client.scan_report_info['raw_data'])) + + if device_name == ble_client.ble_server_name: # 扫描到目标设备后就停止扫描 + ret = ble_client.stop_scan() + if ret != 0: + ble_client.gatt_close() + break + + ret = ble_client.connect() + if ret != 0: + ble_client.gatt_close() + break + else: + print('ble scan failed.') + ret = ble_client.stop_scan() + if ret != 0: + ble_client.gatt_close() + break + elif event_id == event.BLE_GATT_MTU: + print('') + print('event_id : BLE_GATT_MTU, status = {}'.format(status)) + if status == 0: + handle = msg[2] + ble_mtu = msg[3] + print('handle = {:#06x}, ble_mtu = {}'.format(handle, ble_mtu)) + else: + print('ble connect mtu failed.') + ble_client.gatt_close() + break + elif event_id == event.BLE_GATT_RECV_NOTIFICATION_IND: + print('') + print('event_id : BLE_GATT_RECV_NOTIFICATION_IND, status = {}'.format(status)) + if status == 0: + data_len = msg[2] + data = msg[3] + print('len={}, data:{}'.format(data_len, data)) + handle = (data[1] << 8) | data[0] + print('handle = {:#06x}'.format(handle)) + else: + print('ble receive notification failed.') + break + elif event_id == event.BLE_GATT_RECV_INDICATION_IND: + print('') + print('event_id : BLE_GATT_RECV_INDICATION_IND, status = {}'.format(status)) + if status == 0: + data_len = msg[2] + data = msg[3] + print('len={}, data:{}'.format(data_len, data)) + else: + print('ble receive indication failed.') + break + elif event_id == event.BLE_GATT_START_DISCOVER_SERVICE_IND: + ''' + 收到BLE_GATT_START_DISCOVER_SERVICE_IND事件之后,即可开始发现Server端的服务services + ''' + print('') + print('event_id : BLE_GATT_START_DISCOVER_SERVICE_IND, status = {}'.format(status)) + if status == 0: + ble_client.characteristic_count = 0 + ble_client.chara_descriptor_count = 0 + ble_client.characteristic_index = 0 + ble_client.gatt_statue = gatt_status.BLE_GATT_DISCOVER_SERVICE + ''' + 如果知道服务service的UUID,则可以直接通过UUID来查找特定的服务;也可以直接查找所有的服务; + 本示例中,默认直接查找所有服务,因此设置discover_service_mode为0 + ''' + if ble_client.discover_service_mode == 0: + print('execute the function discover_all_service.') + ret = ble_client.discover_all_service() + else: + print('execute the function discover_service_by_uuid.') + ret = ble_client.discover_service_by_uuid() + if ret != 0: + print('Execution result: Failed.') + ble_client.gatt_close() + break + else: + print('ble start discover service failed.') + ble_client.gatt_close() + break + elif event_id == event.BLE_GATT_DISCOVER_SERVICE_IND: + print('') + print('event_id : BLE_GATT_DISCOVER_SERVICE_IND, status = {}'.format(status)) + ''' + 每发现一个service,都会上报一次BLE_GATT_DISCOVER_SERVICE_IND事件,当end_handle为0xFFFF的时候, + 说明已经发现了所有的services + ''' + if status == 0: + start_handle = msg[2] + end_handle = msg[3] + short_uuid = msg[4] + print('start_handle = {:#06x}, end_handle = {:#06x}, short_uuid = {:#06x}'.format(start_handle, end_handle, short_uuid)) + if ble_client.discover_service_mode == 0: # discover service all + if ble_client.target_service['short_uuid'] == short_uuid: # 查找到所有服务后, 按指定uuid查找特征值 + ble_client.target_service['start_handle'] = start_handle + ble_client.target_service['end_handle'] = end_handle + ble_client.gatt_statue = gatt_status.BLE_GATT_DISCOVER_CHARACTERISTIC + print('execute the function discover_all_characteristic.') + ret = ble_client.discover_all_characteristic() + if ret != 0: + print('Execution result: Failed.') + ble_client.gatt_close() + break + else: + ble_client.target_service['start_handle'] = start_handle + ble_client.target_service['end_handle'] = end_handle + ble_client.gatt_statue = gatt_status.BLE_GATT_DISCOVER_CHARACTERISTIC + print('execute the function discover_all_characteristic.') + ret = ble_client.discover_all_characteristic() + if ret != 0: + print('Execution result: Failed.') + ble_client.gatt_close() + break + else: + print('ble discover service failed.') + ble_client.gatt_close() + break + elif event_id == event.BLE_GATT_DISCOVER_CHARACTERISTIC_DATA_IND: + print('') + print('event_id : BLE_GATT_DISCOVER_CHARACTERISTIC_DATA_IND, status = {}'.format(status)) + if status == 0: + data_len = msg[2] + payload = msg[3] + pair_len = payload[0] + print('data_len={}, pair_len={}, payload:{}'.format(data_len, pair_len, payload)) + ''' + payload的数据结构:pair_len+[特征句柄+特征属性+特征值句柄+特征UUID]+...+[特征句柄+特征属性+特征值句柄+特征UUID] + ''' + if data_len > 0: + if ble_client.gatt_statue == gatt_status.BLE_GATT_DISCOVER_CHARACTERISTIC: + i = 0 + while i < (data_len - 1) / pair_len: + chara_dict = { + 'handle': (payload[i * pair_len + 2] << 8) | payload[i * pair_len + 1], + 'properties': payload[i * pair_len + 3], + 'value_handle': (payload[i * pair_len + 5] << 8) | payload[i * pair_len + 4], + 'uuid_type': 0, + 'short_uuid': 0x0000, + 'long_uuid': bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + } + print('handle={:#06x}, properties={:#x}, value_handle={:#06x}'.format(chara_dict['handle'], chara_dict['properties'], chara_dict['value_handle'])) + if pair_len == ble_client.ble_short_uuid_pair_len: + chara_dict['uuid_type'] = 1 + chara_dict['short_uuid'] = (payload[i * pair_len + 7] << 8) | payload[i * pair_len + 6] + print('short_uuid:{:#06x}'.format(chara_dict['short_uuid'])) + elif pair_len == ble_client.ble_long_uuid_pair_len: + start_index = i * pair_len + 6 + end_index = start_index + 16 + chara_dict['uuid_type'] = 0 + chara_dict['long_uuid'] = payload[start_index : end_index] + print('long_uuid:{}'.format(chara_dict['long_uuid'])) + i += 1 + if ble_client.characteristic_count < 5: + ble_client.characteristic_list.append(chara_dict) + ble_client.characteristic_count = len(ble_client.characteristic_list) + print('characteristic_list len = {}'.format(ble_client.characteristic_count)) + elif ble_client.gatt_statue == gatt_status.BLE_GATT_READ_CHARA_VALUE: + print('data_len = {}'.format(data_len)) + print('payload = {:02x},{:02x},{:02x},{:02x}'.format(payload[0], payload[1], payload[2], payload[3])) + else: + print('ble discover characteristic failed.') + ble_client.gatt_close() + break + elif event_id == event.BLE_GATT_DISCOVER_CHARA_DESC_IND: + print('') + print('event_id : BLE_GATT_DISCOVER_CHARA_DESC_IND, status = {}'.format(status)) + if status == 0: + data_len = msg[2] + data = msg[3] + fmt = data[0] + print('fmt={}, len={}, data:{}'.format(fmt, data_len, data)) + if data_len > 0: + i = 0 + if fmt == 1: # 16 bit uuid + while i < (data_len - 1) / 4: + descriptor_dict = { + 'handle': (data[i * 4 + 2] << 8) | data[i * 4 + 1], + 'short_uuid': (data[i * 4 + 4] << 8) | data[i * 4 + 3], + } + print('handle={:#06x}, uuid={:#06x}'.format(descriptor_dict['handle'], descriptor_dict['short_uuid'])) + i += 1 + if ble_client.chara_descriptor_count < 5: + ble_client.descriptor_list.append(descriptor_dict) + ble_client.chara_descriptor_count = len(ble_client.descriptor_list) + print('descriptor_list len = {}'.format(ble_client.chara_descriptor_count)) + if ble_client.characteristic_index == ble_client.characteristic_count: + print('execute the function read_characteristic_by_uuid.') + # ble_client.gatt_statue = gatt_status.BLE_GATT_WRITE_CHARA_VALUE + # ret = ble_client.write_characteristic() + # ret = ble_client.write_characteristic_no_rsp() + + ble_client.gatt_statue = gatt_status.BLE_GATT_READ_CHARA_VALUE + ret = ble_client.read_characteristic_by_uuid() + # ret = ble_client.read_characteristic_by_handle() + + # ble_client.gatt_statue = gatt_status.BLE_GATT_READ_CHARA_DESC + # ret = ble_client.read_characteristic_descriptor() + + # ble_client.gatt_statue = gatt_status.BLE_GATT_WRITE_CHARA_DESC + # ret = ble_client.write_characteristic_descriptor() + else: + print('execute the function discover_all_characteristic_descriptor.') + ret = ble_client.discover_all_characteristic_descriptor() + if ret != 0: + print('Execution result: Failed.') + ble_client.gatt_close() + break + else: + print('ble discover characteristic descriptor failed.') + ble_client.gatt_close() + break + elif event_id == event.BLE_GATT_CHARA_WRITE_WITH_RSP_IND: + print('') + print('event_id : BLE_GATT_CHARA_WRITE_WITH_RSP_IND, status = {}'.format(status)) + if status == 0: + if ble_client.gatt_statue == gatt_status.BLE_GATT_WRITE_CHARA_VALUE: + pass + elif ble_client.gatt_statue == gatt_status.BLE_GATT_WRITE_CHARA_DESC: + pass + else: + print('ble write characteristic with response failed.') + break + elif event_id == event.BLE_GATT_CHARA_WRITE_WITHOUT_RSP_IND: + print('') + print('event_id : BLE_GATT_CHARA_WRITE_WITHOUT_RSP_IND, status = {}'.format(status)) + if status == 0: + print('write characteristic value without response successful.') + else: + print('write characteristic value without response failed.') + break + elif event_id == event.BLE_GATT_CHARA_READ_IND: + print('') + # read characteristic value by handle + print('event_id : BLE_GATT_CHARA_READ_IND, status = {}'.format(status)) + if status == 0: + data_len = msg[2] + data = msg[3] + print('data_len = {}, data : {}'.format(data_len, data)) + if ble_client.gatt_statue == gatt_status.BLE_GATT_READ_CHARA_VALUE: + # print('read characteristic value by handle.') + pass + else: + print('ble read characteristic failed.') + break + elif event_id == event.BLE_GATT_CHARA_READ_BY_UUID_IND: + print('') + # read characteristic value by uuid + print('event_id : BLE_GATT_CHARA_READ_BY_UUID_IND, status = {}'.format(status)) + if status == 0: + data_len = msg[2] + data = msg[3] + print('data_len = {}, data : {}'.format(data_len, data)) + handle = (data[2] << 8) | data[1] + print('handle = {:#06x}'.format(handle)) + else: + print('ble read characteristic by uuid failed.') + break + elif event_id == event.BLE_GATT_CHARA_MULTI_READ_IND: + print('') + # read multiple characteristic value + print('event_id : BLE_GATT_CHARA_MULTI_READ_IND, status = {}'.format(status)) + if status == 0: + data_len = msg[2] + data = msg[3] + print('data_len = {}, data : {}'.format(data_len, data)) + else: + print('ble read multiple characteristic by uuid failed.') + break + elif event_id == event.BLE_GATT_DESC_WRITE_WITH_RSP_IND: + print('') + print('event_id : BLE_GATT_DESC_WRITE_WITH_RSP_IND, status = {}'.format(status)) + if status == 0: + if ble_client.gatt_statue == gatt_status.BLE_GATT_WRITE_CHARA_VALUE: + pass + elif ble_client.gatt_statue == gatt_status.BLE_GATT_WRITE_CHARA_DESC: + pass + else: + print('ble write characteristic descriptor failed.') + break + elif event_id == event.BLE_GATT_DESC_READ_IND: + print('') + # read characteristic descriptor + print('event_id : BLE_GATT_DESC_READ_IND, status = {}'.format(status)) + if status == 0: + data_len = msg[2] + data = msg[3] + print('data_len = {}, data : {}'.format(data_len, data)) + if ble_client.gatt_statue == gatt_status.BLE_GATT_READ_CHARA_DESC: + # print('read characteristic descriptor.') + pass + else: + print('ble read characteristic descriptor failed.') + break + elif event_id == event.BLE_GATT_ATT_ERROR_IND: + print('') + print('event_id : BLE_GATT_ATT_ERROR_IND, status = {}'.format(status)) + if status == 0: + errcode = msg[2] + print('errcode = {:#06x}'.format(errcode)) + if ble_client.gatt_statue == gatt_status.BLE_GATT_DISCOVER_INCLUDES: + ble_client.gatt_statue = gatt_status.BLE_GATT_DISCOVER_CHARACTERISTIC + print('execute the function discover_all_characteristic.') + ret = ble_client.discover_all_characteristic() + if ret != 0: + print('Execution result: Failed.') + ble_client.gatt_close() + break + elif ble_client.gatt_statue == gatt_status.BLE_GATT_DISCOVER_CHARACTERISTIC: + ble_client.gatt_statue = gatt_status.BLE_GATT_IDLE + print('execute the function discover_all_characteristic_descriptor.') + ret = ble_client.discover_all_characteristic_descriptor() + if ret != 0: + print('Execution result: Failed.') + ble_client.gatt_close() + break + else: + print('ble attribute error.') + ble_client.gatt_close() + break + else: + print('unknown event id : {}.'.format(event_id)) + + # ble_client.release() + + +event = EVENT(event_dict) +gatt_status = EVENT(gatt_status_dict) +msg_queue = Queue(50) +ble_client = BleClient() + + +def main(): + print('create client event handler task.') + _thread.start_new_thread(ble_gatt_client_event_handler, ()) + # ble.setScanFilter(0) # 关闭扫描过滤功能 + ret = ble_client.gatt_open() + if ret != 0: + return -1 + + count = 0 + while True: + utime.sleep(1) + count += 1 + cur_time = utime.localtime() + timestamp = "{:02d}:{:02d}:{:02d}".format(cur_time[3], cur_time[4], cur_time[5]) + if count % 5 == 0: + print('[{}] BLE Client running, count = {}......'.format(timestamp, count)) + print('') + if count > 130: + count = 0 + print('!!!!! stop BLE Client now !!!!!') + ble_status = ble_client.gatt_get_status() + if ble_status == 1: + ble_client.gatt_close() + ble_client.release() + break + else: + ble_status = ble_client.gatt_get_status() + if ble_status == 0: # stopped + print('BLE connection has been disconnected.') + ble_client.release() + break + +if __name__ == '__main__': + main() + + +``` + + + +### 5.3 说明 + +#### 5.3.1 `event_id`为26的事件说明 + +该事件是在BLE建立连接后,C层代码自动返回的一个消息,作用是通知用户,可以开始查找服务了。 + +#### 5.3.2 `event_id`为27的事件说明 + +当用户使用`ble.discoverAllService`或`ble.discoverByUUID`方法发起查找服务,在发现服务后,会通过该事件来通知用户,并将发现的服务信息给到用户。每发现一个服务,都会上报一次该事件。注意和event_id为26的事件的区别。 + + + +> 上述提到,每发现一个服务,都会上报一次`event_id`为27的事件,那么如何判断已经发现对端设备的所有服务了? +> +> `event_id`为27的事件上报时,会将每个服务的起始句柄、结束句柄以及UUID给到用户,而当某个服务的结束句柄值为0xFFFF时,说明这个服务就是最后一个服务了。可以通过这个来判断是否已经发现了所有的服务。 + + + +#### 5.3.3 `event_id`为28的事件说明 + +该事件表示的是发现了某个服务的所有特征,同时将这些特征数据上报给用户。这里重点说一下该事件中携带的消息的数据结构,方便用户去解析。 + +通过wiki文档说明,可以知道该事件携带的消息有4个参数,前3个参数在wiki上已经有明确说明,这里重点说明第4个参数`data`的数据结构,`data`中包含了某个服务的所有特征信息,比如特征句柄、属性、特征值句柄和UUID。具体的数据结构分两种情况。 + +第一种,服务端添加特征时用的都是蓝牙定义的标准UUID。这些UUID都可以用2个字节的短UUID来表示。这种情况下,`data`数据结构可以参考蓝牙官方协议文档中《BLUETOOTH SPECIFICATION Version 4.2 [Vol 3, Part G] 部分的CHARACTERISTIC DISCOVERY 章节》,如下: + +![第4个参数数据结构](../../media/hardware/bt/Read_By_Type_Respone.png) + +`data`数据结构如上图中`Read By Type Response`部分。第一个参数是0x07,表示每个特征数据项的长度。而每个特征数据项结构为: + +`特征句柄(2字节)+ 特征属性(1字节)+ 特征值句柄(2字节)+ UUID(2字节)` + +示例: + +假如data数据如下: + +``` +data = (0x07,0x02,0x00,0x32,0x03,0x00,0x19,0x2a,0x05,0x00,0x32,0x06,0x00,0x1a,0x2a) +``` + +数据解析如下: + +| | 数据 | 说明 | +| ----------------- | --------- | ------------------------------------------------- | +| data[0] | 0x07 | 表示每个特征数据项的长度为7个字节。 | +| data[1]~data[2] | 0x02,0x00 | 表示第1个特征的句柄为0x0002。 | +| data[3] | 0x32 | 表示第1个特征的属性为0x32,表示可读、通知和指示。 | +| data[4]~data[5] | 0x03,0x00 | 表示第1个特征值的句柄为0x0003。 | +| data[6]~data[7] | 0x19,0x2a | 表示第1个特征的UUID为0x2a19。 | +| data[8]~data[9] | 0x05,0x00 | 表示第2个特征的句柄为0x0002。 | +| data[10] | 0x32 | 表示第2个特征的属性为0x32,表示可读、通知和指示。 | +| data[11]~data[12] | 0x06,0x00 | 表示第2个特征值的句柄为0x0003。 | +| data[13]~data[14] | 0x1a,0x2a | 表示第2个特征的UUID为0x2a19。 | + + + +第二种,服务端添加特征时用的自定义的UUID或者不在蓝牙SIG定义的范围内,那么UUID将会是16字节的长UUID。这种情况下,data的数据结构同样由1个或者多个特征项构成,特征项的长度为0x15,表示每个特征项长度为21字节,每个特征项的数据结构如下: + +` 特征句柄(2字节)+ 特征属性(1字节)+ 特征值句柄(2字节)+ UUID(16字节)` + +示例: + +假如data数据如下: + +``` +data = (0x15,0x02,0x00,0x32,0x03,0x00,0x00,0x00,0x20,0x11,0x00,0x00,0x10,0x00,0x80,0x00,0x00,0x80,0x5f,0x9b,0x34,0xfb) +``` + +数据解析如下: + +| | 数据 | 说明 | +| --------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| data[0] | 0x15 | 表示每个特征数据项的长度为21个字节。 | +| data[1]~data[2] | 0x02,0x00 | 表示第1个特征的句柄为0x0002。 | +| data[3] | 0x32 | 表示第1个特征的属性为0x32,表示可读、通知和指示。 | +| data[4]~data[5] | 0x03,0x00 | 表示第1个特征值的句柄为0x0003。 | +| data[6]~data[7] | 0x00,0x00,0x20,0x11,0x00,0x00,0x10,0x00,
0x80,0x00,0x00,0x80,0x5f,0x9b,0x34,0xfb | 表示第1个特征的UUID为`00002011-0000-1000-8000-00805F9B34FB`。 | + + + +> 自定义的16字节长UUID,并不是随便定义的,它是基于蓝牙规范中16字节的UUID模板扩展出来的。规范中的长UUID模板通常为:`0000xxxx-0000-1000-8000-00805F9B34FB ` +> +> 将`xxxx`部分替换为我们自定义的2字节UUID,就得到了一个16字节的长UUID。 + + + +#### 5.3.4 `event_id`为29的事件说明 + +该事件表示的是发现了某个特征的描述符,同时将这些特征描述符相关数据上报给用户。这里重点说一下该事件中携带消息的数据结构,方便用户去解析。 + +通过wiki文档说明,可以知道该事件携带的消息有4个参数,前3个参数在wiki上已经有明确说明,这里重点说明第4个参数`data`的数据结构,`data`中包含了一个或多个属性句柄和UUID对。数据结构如下: + +`格式字段format(1字节)+ 属性句柄(2字节)+ UUID(2字节或16字节)` + +其中,格式字段用来表示返回的属性句柄和UUID对的格式。其中format有如下两种取值: + +| format | 说明 | +| ------ | ------------------------------------------------------------ | +| 0x01 | 表示每个属性句柄和UUID对中的UUID是2字节的。这通常对应于Bluetooth SIG定义的标准UUID。 | +| 0x02 | 表示每个属性句柄和UUID对中的UUID是16字节的。这通常对应于自定义UUID或不在Bluetooth SIG定义范围内的UUID。 | + +示例: + +假如data数据如下: + +``` +data = 0x01,0x04,0x00,0x02,0x29 +``` + +数据解析如下: + +| | 数据 | 说明 | +| --------------- | --------- | ----------------------------------------- | +| data[0] | 0x01 | 表示每个属性句柄和UUID对的UUID是2字节的。 | +| data[1]~data[2] | 0x04,0x00 | 表示属性句柄是0x0004 | +| data[3]~data[4] | 0x02,0x29 | 表述属性的UUID是0x2902 | + diff --git a/docs/Getting_started/zh/hardware/bt/bt.md b/docs/Getting_started/zh/hardware/bt/bt.md new file mode 100644 index 0000000000000000000000000000000000000000..cd0261081a855b7acd54364cdf1a71e63af4eed0 --- /dev/null +++ b/docs/Getting_started/zh/hardware/bt/bt.md @@ -0,0 +1,951 @@ +# QuecPython BT 使用说明 + +本文主要介绍如何使用QuecPython的BT功能,包含对HFP、A2DP、AVRCP和SPP的介绍和使用,并提供相关示例说明。 + +## 1 BT API使用说明 + +请参考QuecPython官方网站上的Wiki说明:[BT API 说明](https://python.quectel.com/doc/API_reference/zh/QuecPython%E7%B1%BB%E5%BA%93/bt.html) + +## 2 BT HFP 功能 + +HFP(Hands-Free Profile)是一种蓝牙通信协议,常用于手机和车载系统或蓝牙耳机之间进行语音通信。它定义了车载系统或蓝牙耳机和手机之间进行语音通信的规范,使用户可以直接使用车载系统或蓝牙耳机进行电话呼叫、接听、挂断、拒接以及控制通话音量,而无需去操作手机。 + +HFP协议中有两种角色,分别是音频网关(Audio Gateway,简称AG)和免提设备(Hands-Free Unit,简称HF)。当手机连接到蓝牙耳机时,手机充当的就是AG的角色,而蓝牙耳机充当的就是HF的角色。HFP协议框架如下: + +![HFP协议框架](../../media/hardware/bt/HFP协议框架.png) + + + +> QuecPython 经典蓝牙的HFP功能,目前仅支持做HF端,不支持做AG端。 + + + +### 2.1 AG + +AG是作为音频网关的设备,它负责处理音频信号并将其传输到HF端。AG的主要功能如下: + +* 接收和发送音频信号:比如将来自电话的语音通过蓝牙连接发送到HF设备,并接收来自HF设备的音频信号。 + +* 执行控制命令:AG可以接收来自HF设备的控制命令,比如拨打电话、接听电话、挂断电话以及调整音量等命令,并根据这些命令执行相应的操作。 +* 提供一些状态信息:AG可以将一些设备的状态信息发送给HF,比如电池电量、网络注册状态、信号强度等。 + +### 2.2 HF + +HF是连接到AG并接收其音频信号的设备。而HF设备的主要功能如下: + +* 接收和发送音频信号:HF设备接收来自AG的音频信号并将其播放出来,同时HF设备也可以捕获音频信号并将其发给AG。 + +* 控制AG端行为:HF设备可以发送一些控制命令给AG,比如拨打电话、接听电话、挂断电话、控制通话音量等。 + +* 显示状态:HF设备可以接收来自AG设备的一些状态信息,并将其显示给用户。 + + + +### 2.3 流程 + +QuecPython的HFP功能使用流程如下: + +![BT HFP 流程](../../media/hardware/bt/BT_HFP流程.png) + +> 上述流程中,需要注意: +> +> “建立HFP连接”和“断开HFP连接”的操作,可以由AG端发起,也可以由HF端发起,具体由哪一端发起,通常取决于具体的使用场景。 + + + +### 2.4 示例 + +下面提供了一个关于HFP功能的使用示例,该示例中,模组是作为HF端(相当于蓝牙耳机),手机作为AG端,模组等待手机主动发起连接。要运行该例程,需要准备2部手机,手机A主动连接模组,建立HFP连接后,使用手机B拨打电话给手机A,模组在收到电话响铃事件后,接听电话。 + +```python +# -*- coding: UTF-8 -*- + +""" +示例说明:本例程提供一个通过HFP自动接听电话的功能 +运行平台:EC600UCN_LB 铀开发板 +运行本例程后,通过手机A搜索到设备名并点击连接;然后通过手机B拨打电话给手机A, +当手机A开始响铃震动时,设备会自动接听电话 +""" +import bt +import utime +import _thread +from queue import Queue +from machine import Pin + +# 如果对应播放通道外置了PA,且需要引脚控制PA开启,则需要下面步骤 +# 具体使用哪个GPIO取决于实际使用的引脚 +gpio11 = Pin(Pin.GPIO11, Pin.OUT, Pin.PULL_DISABLE, 0) +gpio11.write(1) + +BT_NAME = 'QuecPython-hfp' + +BT_EVENT = { + 'BT_START_STATUS_IND': 0, # bt/ble start + 'BT_STOP_STATUS_IND': 1, # bt/ble stop + 'BT_HFP_CONNECT_IND': 40, # bt hfp connected + 'BT_HFP_DISCONNECT_IND': 41, # bt hfp disconnected + 'BT_HFP_CALL_IND': 42, # bt hfp call state + 'BT_HFP_CALL_SETUP_IND': 43, # bt hfp call setup state + 'BT_HFP_NETWORK_IND': 44, # bt hfp network state + 'BT_HFP_NETWORK_SIGNAL_IND': 45, # bt hfp network signal + 'BT_HFP_BATTERY_IND': 46, # bt hfp battery level + 'BT_HFP_CALLHELD_IND': 47, # bt hfp callheld state + 'BT_HFP_AUDIO_IND': 48, # bt hfp audio state + 'BT_HFP_VOLUME_IND': 49, # bt hfp volume type + 'BT_HFP_NETWORK_TYPE': 50, # bt hfp network type + 'BT_HFP_RING_IND': 51, # bt hfp ring indication + 'BT_HFP_CODEC_IND': 52, # bt hfp codec type +} + +HFP_CONN_STATUS = 0 +HFP_CONN_STATUS_DICT = { + 'HFP_DISCONNECTED': 0, + 'HFP_CONNECTING': 1, + 'HFP_CONNECTED': 2, + 'HFP_DISCONNECTING': 3, +} +HFP_CALL_STATUS = 0 +HFP_CALL_STATUS_DICT = { + 'HFP_NO_CALL_IN_PROGRESS': 0, + 'HFP_CALL_IN_PROGRESS': 1, +} + +BT_IS_RUN = 0 + +msg_queue = Queue(30) + + +def get_key_by_value(val, d): + for key, value in d.items(): + if val == value: + return key + return None + +def bt_callback(args): + global msg_queue + msg_queue.put(args) + +def bt_event_proc_task(): + global msg_queue + global BT_IS_RUN + global BT_EVENT + global HFP_CONN_STATUS + global HFP_CONN_STATUS_DICT + global HFP_CALL_STATUS + global HFP_CALL_STATUS_DICT + + while True: + print('wait msg...') + msg = msg_queue.get() # 没有消息时会阻塞在这 + event_id = msg[0] + status = msg[1] + + if event_id == BT_EVENT['BT_START_STATUS_IND']: + print('event: BT_START_STATUS_IND') + if status == 0: + print('BT start successfully.') + BT_IS_RUN = 1 + bt_status = bt.getStatus() + if bt_status == 1: + print('BT status is 1, normal status.') + else: + print('BT status is {}, abnormal status.'.format(bt_status)) + bt.stop() + break + + retval = bt.getLocalName() + if retval != -1: + print('The current BT name is : {}'.format(retval[1])) + else: + print('Failed to get BT name.') + bt.stop() + break + + print('Set BT name to {}'.format(BT_NAME)) + retval = bt.setLocalName(0, BT_NAME) + if retval != -1: + print('BT name set successfully.') + else: + print('BT name set failed.') + bt.stop() + break + + retval = bt.getLocalName() + if retval != -1: + print('The new BT name is : {}'.format(retval[1])) + else: + print('Failed to get new BT name.') + bt.stop() + break + + # 设置蓝牙可见模式为:可以被发现并且可以被连接 + retval = bt.setVisibleMode(3) + if retval == 0: + mode = bt.getVisibleMode() + if mode == 3: + print('BT visible mode set successfully.') + else: + print('BT visible mode set failed.') + bt.stop() + break + else: + print('BT visible mode set failed.') + bt.stop() + break + else: + print('BT start failed.') + bt.stop() + break + elif event_id == BT_EVENT['BT_STOP_STATUS_IND']: + print('event: BT_STOP_STATUS_IND') + if status == 0: + BT_IS_RUN = 0 + print('BT stop successfully.') + else: + print('BT stop failed.') + break + elif event_id == BT_EVENT['BT_HFP_CONNECT_IND']: + HFP_CONN_STATUS = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_CONNECT_IND, {}, hfp_conn_status:{}, mac:{}'.format(status, get_key_by_value(msg[2], HFP_CONN_STATUS_DICT), mac)) + if status != 0: + print('BT HFP connect failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_HFP_DISCONNECT_IND']: + HFP_CONN_STATUS = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_DISCONNECT_IND, {}, hfp_conn_status:{}, mac:{}'.format(status, get_key_by_value(msg[2], HFP_CONN_STATUS_DICT), mac)) + if status != 0: + print('BT HFP disconnect failed.') + bt.stop() + elif event_id == BT_EVENT['BT_HFP_CALL_IND']: + call_sta = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_CALL_IND, {}, hfp_call_status:{}, mac:{}'.format(status, get_key_by_value(msg[2], HFP_CALL_STATUS_DICT), mac)) + if status != 0: + print('BT HFP call failed.') + bt.stop() + continue + + if call_sta == HFP_CALL_STATUS_DICT['HFP_NO_CALL_IN_PROGRESS']: + if HFP_CALL_STATUS == HFP_CALL_STATUS_DICT['HFP_CALL_IN_PROGRESS']: + HFP_CALL_STATUS = call_sta + if HFP_CONN_STATUS == HFP_CONN_STATUS_DICT['HFP_CONNECTED']: + print('call ended, ready to disconnect hfp.') + retval = bt.hfpDisconnect(addr) + if retval == 0: + HFP_CONN_STATUS = HFP_CONN_STATUS_DICT['HFP_DISCONNECTING'] + else: + print('Failed to disconnect hfp connection.') + bt.stop() + continue + else: + if HFP_CALL_STATUS == HFP_CALL_STATUS_DICT['HFP_NO_CALL_IN_PROGRESS']: + HFP_CALL_STATUS = call_sta + print('set audio output channel to 2.') + bt.setChannel(2) + print('set volume to 7.') + retval = bt.hfpSetVolume(addr, 7) + if retval != 0: + print('set volume failed.') + elif event_id == BT_EVENT['BT_HFP_CALL_SETUP_IND']: + call_setup_status = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_CALL_SETUP_IND, {}, hfp_call_setup_status:{}, mac:{}'.format(status, call_setup_status, mac)) + if status != 0: + print('BT HFP call setup failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_HFP_CALLHELD_IND']: + callheld_status = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_CALLHELD_IND, {}, callheld_status:{}, mac:{}'.format(status, callheld_status, mac)) + if status != 0: + print('BT HFP callheld failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_HFP_NETWORK_IND']: + network_status = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_NETWORK_IND, {}, network_status:{}, mac:{}'.format(status, network_status, mac)) + if status != 0: + print('BT HFP network status failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_HFP_NETWORK_SIGNAL_IND']: + network_signal = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_NETWORK_SIGNAL_IND, {}, signal:{}, mac:{}'.format(status, network_signal, mac)) + if status != 0: + print('BT HFP network signal failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_HFP_BATTERY_IND']: + battery_level = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_BATTERY_IND, {}, battery_level:{}, mac:{}'.format(status, battery_level, mac)) + if status != 0: + print('BT HFP battery level failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_HFP_AUDIO_IND']: + audio_status = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_AUDIO_IND, {}, audio_status:{}, mac:{}'.format(status, audio_status, mac)) + if status != 0: + print('BT HFP audio failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_HFP_VOLUME_IND']: + volume_type = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_VOLUME_IND, {}, volume_type:{}, mac:{}'.format(status, volume_type, mac)) + if status != 0: + print('BT HFP volume failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_HFP_NETWORK_TYPE']: + service_type = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_NETWORK_TYPE, {}, service_type:{}, mac:{}'.format(status, service_type, mac)) + if status != 0: + print('BT HFP network service type failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_HFP_RING_IND']: + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_RING_IND, {}, mac:{}'.format(status, mac)) + if status != 0: + print('BT HFP ring failed.') + bt.stop() + continue + retval = bt.hfpAnswerCall(addr) + if retval == 0: + print('The call was answered successfully.') + else: + print('Failed to answer the call.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_HFP_CODEC_IND']: + codec_type = msg[2] + addr = msg[3] # BT 主机端mac地址 + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('BT_HFP_CODEC_IND, {}, codec_type:{}, mac:{}'.format(status, codec_type, mac)) + if status != 0: + print('BT HFP codec failed.') + bt.stop() + continue + print('Ready to release hfp.') + bt.hfpRelease() + bt.release() + + +def main(): + global BT_IS_RUN + + _thread.start_new_thread(bt_event_proc_task, ()) + + retval = bt.init(bt_callback) + if retval == 0: + print('BT init successful.') + else: + print('BT init failed.') + return -1 + retval = bt.hfpInit() + if retval == 0: + print('HFP init successful.') + else: + print('HFP init failed.') + return -1 + retval = bt.start() + if retval == 0: + print('BT start successful.') + else: + print('BT start failed.') + retval = bt.hfpRelease() + if retval == 0: + print('HFP release successful.') + else: + print('HFP release failed.') + retval = bt.release() + if retval == 0: + print('BT release successful.') + else: + print('BT release failed.') + return -1 + + count = 0 + while True: + utime.sleep(1) + count += 1 + cur_time = utime.localtime() + timestamp = "{:02d}:{:02d}:{:02d}".format(cur_time[3], cur_time[4], cur_time[5]) + + if count % 5 == 0: + if BT_IS_RUN == 1: + print('[{}] BT HFP is running, count = {}......'.format(timestamp, count)) + print('') + else: + print('BT HFP has stopped running, ready to exit.') + break + + +if __name__ == '__main__': + main() + +``` + + + +## 3 BT A2DP/AVRCP 功能 + +A2DP和AVRCP是两种蓝牙音频设备中常用的协议。并且这两种协议是密切相关的,一般都放在一起使用,以提供完整的音频服务。因此我们将这两种协议放在一起进行说明。 + +### 3.1 A2DP + +A2DP (Advanced Audio Distribution Profile) ,是一种用于将高质量音频流从一个设备传输到另一个设备的协议。比如将手机上的音频流数据传输到蓝牙耳机上,由蓝牙耳机进行音频播放。 + +A2DP协议中,有两种角色,分别是Audio Source Side和Audio Sink Side。这两种角色在音频传输过程中负责不同的职责: + +* Audio Source Side:表示音频数据的来源端,即音频数据的发送方。比如使用蓝牙耳机播放手机端音乐时,手机就是Audio Source Side。 +* Audio Sink Side:表示音频数据的接收端。比如使用蓝牙耳机播放手机端音乐时,耳机就是Audio Sink Side。 + +A2DP协议框架可参考官方文档中的如下部分: + +![A2DP协议框架](../../media/hardware/bt/A2DP协议框架.png) + + + +> QuecPython 经典蓝牙的A2DP功能,目前仅支持做Audio Sink Side,不支持做Audio Source Side。 +> +> A2DP是单向传输协议,即数据只能从Audio Source Side传输到Audio Sink Side,不能反向传输。 + + + +### 3.2 AVRCP + +AVRCP (Audio/Video Remote Control Profile),是一种控制协议,它提供一种机制,允许一个设备去控制另一个设备的音频或视频播放,比如蓝牙耳机控制手机端音乐播放行为,进行播放、暂停、上一首、下一首等操作。 + +AVRCP协议中,有两种角色,分别是Controller Side(简称CT)和Target Side(简称TG)。两种角色说明如下: + +* Controller Side:表示远程控制的发送方,即发送控制命令的设备。比如使用蓝牙耳机来控制手机音乐的播放时,蓝牙耳机就是Controller Side,耳机可以发送播放、暂停、停止、上一首、下一首等控制命令给手机。 +* Target Side:表示远程控制的接收方,即被控制的设备。比如使用蓝牙耳机来控制手机音乐播放时,手机就是Target Side,手机接收来自于蓝牙耳机的控制命令并进行相应的操作。 + +AVRCP协议框架可参考官方文档中的如下部分: + +AVRCP协议框架 + + + +> QuecPython 经典蓝牙的AVRCP功能,目前仅支持做Controller Side,不支持做Target Side。 + + + +### 3.3 流程 + +QuecPython的HFP功能使用流程如下: + +![BT A2DP-AVRCP 流程](../../media/hardware/bt/BT_A2DP_AVRCP流程.png) + + + +### 3.4 示例 + +下面提供了一个关于A2DP和AVRCP功能的使用示例,该示例演示的是使用模组来控制手机进行音乐播放、暂停、上一首、下一首还有音量设置的功能。该例程中,在建立蓝牙连接后,会弹出一个简易的菜单,菜单中描述了不同的数字对应的功能,用户输入对应数字后敲回车键,即可触发对应的功能。 + +```python +#A2DP/AVRCP 示例程序 + +""" +示例说明:本例程提供一个通过A2DP/AVRCP实现的简易蓝牙音乐播放控制功能 +运行平台:EC600UCN_LB 铀开发板 +运行本例程后,通过手机搜索到设备名并点击连接;然后打开手机上的音乐播放软件, +回到例程运行界面,根据提示菜单输入对应的控制命令来实现音乐的播放,暂停,上一首, +下一首以及设置音量的功能 +""" +import bt +import utime +import _thread +from queue import Queue +from machine import Pin + +BT_STATUS_DICT = { + 'BT_NOT_RUNNING': 0, + 'BT_IS_RUNNING': 1 +} + +A2DP_AVRCP_CONNECT_STATUS = { + 'DISCONNECTED': 0, + 'CONNECTING': 1, + 'CONNECTED': 2, + 'DISCONNECTING': 3 +} + +host_addr = 0 +msg_queue = Queue(10) + +# 如果对应播放通道外置了PA,且需要引脚控制PA开启,则需要下面步骤 +# 具体使用哪个GPIO取决于实际使用的引脚 +gpio11 = Pin(Pin.GPIO11, Pin.OUT, Pin.PULL_DISABLE, 0) +gpio11.write(1) + + +def cmd_proc(cmd): + cmds = ('1', '2', '3', '4', '5') + vols = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11') + + if cmd in cmds: + if cmd == '5': + while True: + tmp = input('Please input volume: ') + if len(tmp) != 1: + vol = tmp.split('Please input volume: ')[1] + else: + vol = tmp + if vol in vols: + return cmd, int(vol) + else: + print('Volume should be in [0,11], try again.') + else: + return cmd, 0 + else: + print('Command {} is not supported!'.format(cmd)) + return -1 + +def avrcp_play(args): + return bt.avrcpStart() + +def avrcp_pause(args): + return bt.avrcpPause() + +def avrcp_prev(args): + return bt.avrcpPrev() + +def avrcp_next(args): + return bt.avrcpNext() + +def avrcp_set_volume(vol): + return bt.avrcpSetVolume(vol) + +def bt_callback(args): + pass + +def bt_a2dp_avrcp_proc_task(): + global msg_queue + + cmd_handler = { + '1': avrcp_play, + '2': avrcp_pause, + '3': avrcp_prev, + '4': avrcp_next, + '5': avrcp_set_volume, + } + while True: + # print('wait msg...') + msg = msg_queue.get() + print('recv msg: {}'.format(msg)) + cmd_handler.get(msg[0])(msg[1]) + + +def main(): + global host_addr + global msg_queue + + _thread.start_new_thread(bt_a2dp_avrcp_proc_task, ()) + bt.init(bt_callback) + bt.setChannel(2) # 音频输出通道切换,需要在bt.start()之前设置 + retval = bt.a2dpavrcpInit() + if retval == 0: + print('BT A2DP/AVRCP initialization succeeded.') + else: + print('BT A2DP/AVRCP initialization failed.') + return -1 + + retval = bt.start() + if retval != 0: + print('BT start failed.') + return -1 + + utime.sleep_ms(1500) + + old_name = bt.getLocalName() + if old_name == -1: + print('Get BT name error.') + return -1 + print('The current BT name is {}'.format(old_name[1])) + new_name = 'QuecPython-a2dp' + print('Set new BT name to {}'.format(new_name)) + retval = bt.setLocalName(0, new_name) + if retval == -1: + print('Set BT name failed.') + return -1 + cur_name = bt.getLocalName() + if cur_name == -1: + print('Get new BT name error.') + return -1 + else: + if cur_name[1] == new_name: + print('BT name changed successfully.') + else: + print('BT name changed failed.') + + visible_mode = bt.getVisibleMode() + if visible_mode != -1: + print('The current BT visible mode is {}'.format(visible_mode)) + else: + print('Get BT visible mode error.') + return -1 + + print('Set BT visible mode to 3.') + retval = bt.setVisibleMode(3) + if retval == -1: + print('Set BT visible mode error.') + return -1 + + print('BT reconnect check start......') + bt.reconnect_set(25, 2) + bt.reconnect() + + count = 0 + while True: + count += 1 + if count % 5 == 0: + print('waiting to be connected...') + if count >= 10000: + count = 0 + a2dp_status = bt.a2dpGetConnStatus() + avrcp_status = bt.avrcpGetConnStatus() + if a2dp_status == A2DP_AVRCP_CONNECT_STATUS['CONNECTED'] and avrcp_status == A2DP_AVRCP_CONNECT_STATUS['CONNECTED']: + print('========== BT connected! =========') + addr = bt.a2dpGetAddr() + if addr != -1: + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('The BT address on the host side: {}'.format(mac)) + host_addr = addr + else: + print('Get BT addr error.') + return -1 + print('Please open the music player software on your phone first.') + print('Please enter the following options to select a function:') + print('========================================================') + print('1 : play') + print('2 : pause') + print('3 : prev') + print('4 : next') + print('5 : set volume') + print('6 : exit') + print('========================================================') + while True: + tmp = input('> ') + if len(tmp) != 1: + cmd = tmp.split('> ')[1] + else: + cmd = tmp + if cmd == '6': + break + retval = cmd_proc(cmd) + if retval != -1: + msg_queue.put(retval) + break + else: + utime.sleep_ms(1000) + print('Ready to disconnect a2dp.') + retval = bt.a2dpDisconnect(host_addr) + if retval == 0: + print('a2dp connection disconnected successfully') + else: + print('Disconnect a2dp error.') + print('Ready to stop BT.') + retval = bt.stop() + if retval == 0: + print('BT has stopped.') + else: + print('BT stop error.') + bt.a2dpavrcpRelease() + bt.release() + + +if __name__ == '__main__': + main() + +``` + + + +## 4 BT SPP 功能 + +SPP(Serial Port Profile)即串行端口配置协议。它模拟了RS-232串行端口的全双工通信特性,可以在两个蓝牙设备之间建立一种类似于串行口的无线数据连接,使得两个设备能够通过蓝牙进行数据传输。 + +SPP协议框架可参考官方文档中的如下部分: + +![SPP协议框架](../../media/hardware/bt/SPP协议框架.png) + + + + + +### 4.1 流程 + +![BT SPP 流程](../../media/hardware/bt/BT_SPP流程.png) + + + +> SPP协议中,需要使用SPP进行数据传输的设备A和设备B,都可以主动发起SPP连接。 + + + +### 4.2 示例 + +下面提供了一个SPP功能的例程,该例程是模组与手机通过SPP进行数据通信,且由模组主动发起SPP连接。需要注意的是,运行如下程序要求手机端安装了蓝牙SPP相关的APP。如果手机端没有安装SPP相关的应用程序,那么当模组向手机端发起SPP连接请求时,手机的蓝牙功能可能无法找到合适的服务来响应处理这个SPP连接请求,因此手机可能会忽略这个连接请求,并且不会弹出配对窗口。 + +```python +# -*- coding: UTF-8 -*- + +""" +示例说明:本例程提供一个通过SPP实现与手机端进行数据传输的功能 +(1)运行之前,需要先在手机端(安卓)安装SPP相关的APP,然后打开该软件; +(2)修改本例程中的目标设备的蓝牙名称,即 DST_DEVICE_INFO['dev_name'] 的值改为用户准备连接的手机的蓝牙名称; +(3)运行本例程,例程中会先发起搜索周边设备的操作,直到搜索到目标设备,就会结束搜索,然后向目标设备发起SPP连接请求; +(4)用户注意查看手机界面是否弹出蓝牙配对请求的界面,当出现时,点击配对; +(5)配对成功后,用户即可进入到蓝牙串口界面,发送数据给设备,设备在收到数据后会回复"I have received the data you sent." +(6)手机端APP中点击断开连接,即可结束例程; +""" +import bt +import utime +import _thread +from queue import Queue + + +BT_NAME = 'QuecPython-SPP' + +BT_EVENT = { + 'BT_START_STATUS_IND': 0, # bt/ble start + 'BT_STOP_STATUS_IND': 1, # bt/ble stop + 'BT_SPP_INQUIRY_IND': 6, # bt spp inquiry ind + 'BT_SPP_INQUIRY_END_IND': 7, # bt spp inquiry end ind + 'BT_SPP_RECV_DATA_IND': 14, # bt spp recv data ind + 'BT_SPP_CONNECT_IND': 61, # bt spp connect ind + 'BT_SPP_DISCONNECT_IND': 62, # bt spp disconnect ind +} + +DST_DEVICE_INFO = { + 'dev_name':'HUAWEI Mate40 Pro',# 要连接设备的蓝牙名称 + 'bt_addr': None +} + +BT_IS_RUN = 0 +msg_queue = Queue(30) + + +def bt_callback(args): + global msg_queue + msg_queue.put(args) + + +def bt_event_proc_task(): + global msg_queue + global BT_IS_RUN + global DST_DEVICE_INFO + + while True: + print('wait msg...') + msg = msg_queue.get() # 没有消息时会阻塞在这 + event_id = msg[0] + status = msg[1] + + if event_id == BT_EVENT['BT_START_STATUS_IND']: + print('event: BT_START_STATUS_IND') + if status == 0: + print('BT start successfully.') + BT_IS_RUN = 1 + + print('Set BT name to {}'.format(BT_NAME)) + retval = bt.setLocalName(0, BT_NAME) + if retval != -1: + print('BT name set successfully.') + else: + print('BT name set failed.') + bt.stop() + continue + + retval = bt.setVisibleMode(3) + if retval == 0: + mode = bt.getVisibleMode() + if mode == 3: + print('BT visible mode set successfully.') + else: + print('BT visible mode set failed.') + bt.stop() + continue + else: + print('BT visible mode set failed.') + bt.stop() + continue + + retval = bt.startInquiry(15) + if retval != 0: + print('Inquiry error.') + bt.stop() + continue + else: + print('BT start failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_STOP_STATUS_IND']: + print('event: BT_STOP_STATUS_IND') + if status == 0: + BT_IS_RUN = 0 + print('BT stop successfully.') + else: + print('BT stop failed.') + + retval = bt.sppRelease() + if retval == 0: + print('SPP release successfully.') + else: + print('SPP release failed.') + retval = bt.release() + if retval == 0: + print('BT release successfully.') + else: + print('BT release failed.') + break + elif event_id == BT_EVENT['BT_SPP_INQUIRY_IND']: + print('event: BT_SPP_INQUIRY_IND') + if status == 0: + rssi = msg[2] + name = msg[4] + addr = msg[5] + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('name: {}, addr: {}, rssi: {}'.format(name, mac, rssi)) + + if name == DST_DEVICE_INFO['dev_name']: + print('The target device is found, device name {}'.format(name)) + DST_DEVICE_INFO['bt_addr'] = addr + retval = bt.cancelInquiry() + if retval != 0: + print('cancel inquiry failed.') + continue + else: + print('BT inquiry failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_SPP_INQUIRY_END_IND']: + print('event: BT_SPP_INQUIRY_END_IND') + if status == 0: + print('BT inquiry has ended.') + inquiry_sta = msg[2] + if inquiry_sta == 0: + if DST_DEVICE_INFO['bt_addr'] is not None: + print('Ready to connect to the target device : {}'.format(DST_DEVICE_INFO['dev_name'])) + retval = bt.sppConnect(DST_DEVICE_INFO['bt_addr']) + if retval != 0: + print('SPP connect failed.') + bt.stop() + continue + else: + print('Not found device [{}], continue to inquiry.'.format(DST_DEVICE_INFO['dev_name'])) + bt.cancelInquiry() + bt.startInquiry(15) + else: + print('Inquiry end failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_SPP_RECV_DATA_IND']: + print('event: BT_SPP_RECV_DATA_IND') + if status == 0: + datalen = msg[2] + data = msg[3] + print('recv {} bytes data: {}'.format(datalen, data)) + send_data = 'I have received the data you sent.' + print('send data: {}'.format(send_data)) + retval = bt.sppSend(send_data) + if retval != 0: + print('send data faied.') + else: + print('Recv data failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_SPP_CONNECT_IND']: + print('event: BT_SPP_CONNECT_IND') + if status == 0: + conn_sta = msg[2] + addr = msg[3] + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('SPP connect successful, conn_sta = {}, addr {}'.format(conn_sta, mac)) + else: + print('Connect failed.') + bt.stop() + continue + elif event_id == BT_EVENT['BT_SPP_DISCONNECT_IND']: + print('event: BT_SPP_DISCONNECT_IND') + conn_sta = msg[2] + addr = msg[3] + mac = '{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}'.format(addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]) + print('SPP disconnect successful, conn_sta = {}, addr {}'.format(conn_sta, mac)) + bt.stop() + continue + + +def main(): + global BT_IS_RUN + + _thread.start_new_thread(bt_event_proc_task, ()) + retval = bt.init(bt_callback) + if retval == 0: + print('BT init successful.') + else: + print('BT init failed.') + return -1 + retval = bt.sppInit() + if retval == 0: + print('SPP init successful.') + else: + print('SPP init failed.') + return -1 + retval = bt.start() + if retval == 0: + print('BT start successful.') + else: + print('BT start failed.') + retval = bt.sppRelease() + if retval == 0: + print('SPP release successful.') + else: + print('SPP release failed.') + return -1 + + count = 0 + while True: + utime.sleep(1) + count += 1 + cur_time = utime.localtime() + timestamp = "{:02d}:{:02d}:{:02d}".format(cur_time[3], cur_time[4], cur_time[5]) + + if count % 5 == 0: + if BT_IS_RUN == 1: + print('[{}] BT SPP is running, count = {}......'.format(timestamp, count)) + print('') + else: + print('BT SPP has stopped running, ready to exit.') + break + + +if __name__ == '__main__': + main() + +``` + diff --git "a/docs/Getting_started/zh/media/hardware/bt/A2DP\345\215\217\350\256\256\346\241\206\346\236\266.png" "b/docs/Getting_started/zh/media/hardware/bt/A2DP\345\215\217\350\256\256\346\241\206\346\236\266.png" new file mode 100644 index 0000000000000000000000000000000000000000..a00c041640032e2d22d82a5ab72ee8c05f9e3da4 Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/A2DP\345\215\217\350\256\256\346\241\206\346\236\266.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/AVRCP\345\215\217\350\256\256\346\241\206\346\236\266.png" "b/docs/Getting_started/zh/media/hardware/bt/AVRCP\345\215\217\350\256\256\346\241\206\346\236\266.png" new file mode 100644 index 0000000000000000000000000000000000000000..4a753c222c07ad75f61709d661d1eefb9efdcc0e Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/AVRCP\345\215\217\350\256\256\346\241\206\346\236\266.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/BLE_Client\346\265\201\347\250\213.png" "b/docs/Getting_started/zh/media/hardware/bt/BLE_Client\346\265\201\347\250\213.png" new file mode 100644 index 0000000000000000000000000000000000000000..7e9f5eb4a5f672e626618720f1c147226c55ef7a Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/BLE_Client\346\265\201\347\250\213.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/BLE_Client\346\265\201\347\250\213_en.png" "b/docs/Getting_started/zh/media/hardware/bt/BLE_Client\346\265\201\347\250\213_en.png" new file mode 100644 index 0000000000000000000000000000000000000000..6edd40a2927f6e9a61f74b9dbff6f823e43f95b3 Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/BLE_Client\346\265\201\347\250\213_en.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/BLE_Server\346\265\201\347\250\213.png" "b/docs/Getting_started/zh/media/hardware/bt/BLE_Server\346\265\201\347\250\213.png" new file mode 100644 index 0000000000000000000000000000000000000000..b2795ad9edb948e3d72a894fa000905c98bedcac Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/BLE_Server\346\265\201\347\250\213.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/BLE_Server\346\265\201\347\250\213_en.png" "b/docs/Getting_started/zh/media/hardware/bt/BLE_Server\346\265\201\347\250\213_en.png" new file mode 100644 index 0000000000000000000000000000000000000000..15869e58469fec75aa186ba4852c62cef0affef7 Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/BLE_Server\346\265\201\347\250\213_en.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/BT_A2DP_AVRCP\346\265\201\347\250\213.png" "b/docs/Getting_started/zh/media/hardware/bt/BT_A2DP_AVRCP\346\265\201\347\250\213.png" new file mode 100644 index 0000000000000000000000000000000000000000..e870bd88531efced011e3e6094966324fd0dfc52 Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/BT_A2DP_AVRCP\346\265\201\347\250\213.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/BT_A2DP_AVRCP\346\265\201\347\250\213_en.png" "b/docs/Getting_started/zh/media/hardware/bt/BT_A2DP_AVRCP\346\265\201\347\250\213_en.png" new file mode 100644 index 0000000000000000000000000000000000000000..5cffa56f983c5b4a55d04750e42dcad4e879c165 Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/BT_A2DP_AVRCP\346\265\201\347\250\213_en.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/BT_HFP\346\265\201\347\250\213.png" "b/docs/Getting_started/zh/media/hardware/bt/BT_HFP\346\265\201\347\250\213.png" new file mode 100644 index 0000000000000000000000000000000000000000..7b2da4f5725bc60bb9477fb9b2c9842aa8e966d7 Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/BT_HFP\346\265\201\347\250\213.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/BT_HFP\346\265\201\347\250\213_en.png" "b/docs/Getting_started/zh/media/hardware/bt/BT_HFP\346\265\201\347\250\213_en.png" new file mode 100644 index 0000000000000000000000000000000000000000..b93b93614bbc1428db1f29c79c87a3829be75b3d Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/BT_HFP\346\265\201\347\250\213_en.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/BT_SPP\346\265\201\347\250\213.png" "b/docs/Getting_started/zh/media/hardware/bt/BT_SPP\346\265\201\347\250\213.png" new file mode 100644 index 0000000000000000000000000000000000000000..cfbf8c015d4b7918c5bccf8f49b52f76f62b254b Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/BT_SPP\346\265\201\347\250\213.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/BT_SPP\346\265\201\347\250\213_en.png" "b/docs/Getting_started/zh/media/hardware/bt/BT_SPP\346\265\201\347\250\213_en.png" new file mode 100644 index 0000000000000000000000000000000000000000..8d40c83a259484fb3c5d05cead487228bf59522f Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/BT_SPP\346\265\201\347\250\213_en.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/GATT\345\215\217\350\256\256\346\241\206\346\236\266.png" "b/docs/Getting_started/zh/media/hardware/bt/GATT\345\215\217\350\256\256\346\241\206\346\236\266.png" new file mode 100644 index 0000000000000000000000000000000000000000..f0598280191cfc2b248bc29e206f3049393f0ebe Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/GATT\345\215\217\350\256\256\346\241\206\346\236\266.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/HFP\345\215\217\350\256\256\346\241\206\346\236\266.png" "b/docs/Getting_started/zh/media/hardware/bt/HFP\345\215\217\350\256\256\346\241\206\346\236\266.png" new file mode 100644 index 0000000000000000000000000000000000000000..d7c412fada7a4a67434e75d82f56a784944865fa Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/HFP\345\215\217\350\256\256\346\241\206\346\236\266.png" differ diff --git a/docs/Getting_started/zh/media/hardware/bt/Read_By_Type_Respone.png b/docs/Getting_started/zh/media/hardware/bt/Read_By_Type_Respone.png new file mode 100644 index 0000000000000000000000000000000000000000..8fe88b87da4394aedd698a69af32722d26441970 Binary files /dev/null and b/docs/Getting_started/zh/media/hardware/bt/Read_By_Type_Respone.png differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/SPP\345\215\217\350\256\256\346\241\206\346\236\266.png" "b/docs/Getting_started/zh/media/hardware/bt/SPP\345\215\217\350\256\256\346\241\206\346\236\266.png" new file mode 100644 index 0000000000000000000000000000000000000000..5a2be0c89bcdbc0a8e325572cbfd99246f40305b Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/SPP\345\215\217\350\256\256\346\241\206\346\236\266.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/\345\261\236\346\200\247\346\236\204\346\210\220.png" "b/docs/Getting_started/zh/media/hardware/bt/\345\261\236\346\200\247\346\236\204\346\210\220.png" new file mode 100644 index 0000000000000000000000000000000000000000..b4fb5259fc95d12ba03cd285af8f5b3e6bf3364f Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/\345\261\236\346\200\247\346\236\204\346\210\220.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/\346\234\215\345\212\241\345\243\260\346\230\216.png" "b/docs/Getting_started/zh/media/hardware/bt/\346\234\215\345\212\241\345\243\260\346\230\216.png" new file mode 100644 index 0000000000000000000000000000000000000000..7a8eaad78564d2871133627f754c354e2736294a Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/\346\234\215\345\212\241\345\243\260\346\230\216.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/\347\211\271\345\276\201\345\200\274\345\243\260\346\230\216.png" "b/docs/Getting_started/zh/media/hardware/bt/\347\211\271\345\276\201\345\200\274\345\243\260\346\230\216.png" new file mode 100644 index 0000000000000000000000000000000000000000..6bf9d14683e5ca5ae3e739c5d0dd8dba121a4907 Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/\347\211\271\345\276\201\345\200\274\345\243\260\346\230\216.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/\347\211\271\345\276\201\345\243\260\346\230\216.png" "b/docs/Getting_started/zh/media/hardware/bt/\347\211\271\345\276\201\345\243\260\346\230\216.png" new file mode 100644 index 0000000000000000000000000000000000000000..88fb72b964303513e8357074a0c16bafd30e628f Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/\347\211\271\345\276\201\345\243\260\346\230\216.png" differ diff --git "a/docs/Getting_started/zh/media/hardware/bt/\347\211\271\345\276\201\345\243\260\346\230\216\345\261\236\346\200\247\345\200\274\345\255\227\346\256\265\350\257\264\346\230\216.png" "b/docs/Getting_started/zh/media/hardware/bt/\347\211\271\345\276\201\345\243\260\346\230\216\345\261\236\346\200\247\345\200\274\345\255\227\346\256\265\350\257\264\346\230\216.png" new file mode 100644 index 0000000000000000000000000000000000000000..abfe5d1bd06724bf4b27fb45d59d35aee097503c Binary files /dev/null and "b/docs/Getting_started/zh/media/hardware/bt/\347\211\271\345\276\201\345\243\260\346\230\216\345\261\236\346\200\247\345\200\274\345\255\227\346\256\265\350\257\264\346\230\216.png" differ diff --git a/docs/Getting_started/zh/sidebar.yaml b/docs/Getting_started/zh/sidebar.yaml index 63405acda7d187a5af8f27084335ef044dd6e0d1..1f7e138ca545412b6c1ff5926e768bde1864ef4a 100644 --- a/docs/Getting_started/zh/sidebar.yaml +++ b/docs/Getting_started/zh/sidebar.yaml @@ -95,7 +95,7 @@ items: - label: "5.16 矩阵键盘" file: hardware/matrix-keypad.md - label: "5.17 BT和BLE" - file: hardware/bt-and-ble.md + file: hardware/bt/README.md - label: "5.18 USB网卡" file: hardware/usb-wireless-card.md - label: "5.19 以太网卡"