持续创造,加速成长!这是我参加「日新方案 6 月更文挑战」的第30天,点击查看活动详情
一、简介
1.1 低功耗蓝牙(BLE)协议栈
链路层(LL) 操控设备的射频状况,有五个设备状况:待机、播送、扫描、初始化和衔接。
播送 为播送数据包,而 扫描 则是监听播送。
GAP通讯中人物,中心设备(Central – 主机) 用来扫描和衔接 外围设备(Peripheral – 从机)。
大部分状况下外围设备经过播送自己来让中心设备发现自己,并树立 GATT 衔接,然后进行更多的数据交流。
也有些状况是不需求衔接的,只需外设播送自己的数据即可,用这种方法首要意图是让外围设备,把自己的信息发送给多个中心设备。
1.2 通用特点协议(GATT)
GATT是用Attribute Protocal(特点协议)界说的一个service(服务)结构。这个结构界说了Services以及它们的Characteristics的格式和规程。规程便是界说了包括发现、读、写、告诉、指示以及装备播送的characteristics。
为完结装备文件(Profile)的设备界说了两种人物:Client(客户端)、Server(服务器)。esp32的ble一般就处于Server形式。
一旦两个设备树立了衔接,GATT就开端发挥效用,一起意味着GAP协议办理的播送进程完毕了。
1.2.1 Profile(标准)
profile 能够理解为一种标准,树立的蓝牙运用使命,蓝牙使命实际上分为两类:标准蓝牙使命标准 profile(公有使命),非标准蓝牙使命标准 profile(私有使命)。
-
标准蓝牙使命标准 profile:指的是从蓝牙特别兴趣小组 SIG 的官网上现已发布的 GATT 标准列表,包括警告告诉(alert notification),血压丈量(blood pressure),心率(heart rate),电池(battery)等等。它们都是针对详细的低功耗蓝牙的运用实例来规划的。现在蓝牙技能联盟还在不断的制定新的标准,而且发布。
-
非标准蓝牙使命标准 profile:指的是供应商自界说的使命,在蓝牙 SIG 小组内未界说的使命标准。
1.2.2 Service(服务)
service 能够理解为一个服务,在 BLE 从机中有多个服务,例如:电量信息服务、体系信息服务等; 每个 service 中又包括多个 characteristic 特征值; 每个详细的 characteristic 特征值才是 BLE 通讯的主题,比方当前的电量是 80%,电量的 characteristic 特征值存在从机的 profile 里,这样主机就能够经过这个 characteristic 来读取 80% 这个数据。 GATT 服务一般包括几个具有相关的功用,比方特定传感器的读取和设置,人机接口的输入输出。安排具有相关的特性到服务中既有用又有效,因为它使得逻辑上和用户数据上的鸿沟变得更加清晰,一起它也有助于不同运用程序间代码的重用。
1.2.3 Characteristic(特征)
characteristic 特征,BLE 主从机的通讯均是经过 characteristic 来完结,能够理解为一个标签,经过这个标签能够获取或许写入想要的内容。
1.2.4 UUID(通用仅有识别码)
uuid 通用仅有识别码,咱们刚才提到的 service 和 characteristic 都需求一个仅有的 uuid 来标识; 每个从机都会有一个 profile,不管是自界说的 simpleprofile,还是标准的防丢器 profile,他们都是由一些 service 组成,每个 service 又包括了多个 characteristic,主机和从机之间的通讯,均是经过characteristic来完结。
1.3 ESP32蓝牙运用结构
蓝牙是⼀种短距通讯体系,其关键特性包括鲁棒性、低功耗、低成本等。蓝牙体系分为两种不同的技能:经典蓝牙 (Classic Bluetooth) 和蓝牙低功耗 (Bluetooth Low Energy)。 ESP32 支撑双模蓝牙,即一起支撑经典蓝牙和蓝牙低功耗。
从全体结构上,蓝牙可分为操控器 (Controller) 和主机 (Host) 两⼤部分:操控器包括了 PHY、Baseband、Link Controller、Link Manager、Device Manager、HCI 等模块,用于硬件接⼝办理、链路办理等等;主机则包括了 L2CAP、SMP、SDP、ATT、GATT、GAP 以及各种标准,构建了向运用层供给接口的根底,便利运用层对蓝牙体系的拜访。主机能够与操控器运行在同⼀个宿主上,也能够分布在不同的宿主上。ESP32 能够支撑上述两种方法。
1.4 Bluedroid主机架构
在 ESP-IDF 中,运用经过很多修改后的 BLUEDROID 作为蓝牙主机 (Classic BT + BLE)。BLUEDROID 具有较为完善的功用,⽀持常用的标准和架构规划,一起也较为复杂。经过很多修改后,BLUEDROID 保留了大多数 BTA 层以下的代码,简直完全删去了 BTIF 层的代码,运用了较为精简的 BTC 层作为内置标准及 Misc 操控层。修改后的 BLUEDROID 及其与操控器之间的联系如下图:
二、API说明
以下操控器和虚拟 HCI 接口坐落 bt/include/esp32/include/esp_bt.h。
2.1 esp_bt_controller_mem_release
2.2 esp_bt_controller_init
2.3 esp_bt_controller_enable
以下 GATT 接口坐落 bt/host/bluedroid/api/include/api/esp_bt_main.h 和 bt/host/bluedroid/api/include/api/esp_gatts_api.h。
2.4 esp_bluedroid_init
2.5 esp_bluedroid_enable
2.6 esp_ble_gatts_register_callback
2.7 esp_ble_gatts_app_register
2.8 esp_ble_gatts_create_service
2.9 esp_ble_gatts_add_char
2.10 esp_ble_gatts_add_char_descr
2.11 esp_ble_gatts_start_service
2.12 esp_ble_gatts_send_indicate
2.13 esp_ble_gatts_send_response
2.14 esp_ble_gatts_get_attr_value
三、蓝牙4.0通讯完结进程
- 扫描蓝牙BLE终端设备,对应esp32便是播送给咱们供扫描
- 衔接蓝牙BLE终端设备,pad扫描到后去衔接
- 发动服务发现,衔接到esp32后获取相应的服务。 衔接成功后,咱们就要去寻找咱们所需求的服务,这里需求先发动服务发现。
- 获取Characteristic 之前咱们说过,咱们的最终意图便是获取Characteristic来进行通讯,正常状况下,咱们能够从硬件工程师那边得到serviceUUID和characteristicUUID,也便是咱们所比方的班级号和学号,以此来获得咱们的characteristic。
- 开端通讯 咱们在得到Characteristic后,就能够开端读写操作进行通讯了。 a. 关于读操作来说,读取BLE终端设备回来的数据会经过回调方法mGattCallback中的onCharacteristicChanged函数回来。 b. 关于写操作来说,能够经过向Characteristic写入指令以此来达到操控BLE终端设备的意图
四、Demo程序GATT发动流程
运用 esp-idf\examples\bluetooth\bluedroid\ble\gatt_server 中的例程
.........
//esp_bt_controller_config_t是蓝牙操控器装备结构体,这里运用了一个默认的参数
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
//初始化蓝牙操控器,此函数只能被调用一次,且有必要在其他蓝牙功用被调用之前调用
ret = esp_bt_controller_init(&bt_cfg);
if (ret) {
ESP_LOGE(GATTS_TAG, "%s initialize controller failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
//使能蓝牙操控器,mode是蓝牙形式,如果想要动态改动蓝牙形式不能直接调用该函数,
//应该先用disable封闭蓝牙再运用该API来改动蓝牙形式
ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);
if (ret) {
ESP_LOGE(GATTS_TAG, "%s enable controller failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
//初始化蓝牙并分配体系资源,它应该被第一个调用
/*
蓝牙栈bluedroid stack包括了BT和BLE运用的根本的define和API
初始化蓝牙栈今后并不能直接运用蓝牙功用,
还需求用FSM办理蓝牙衔接状况
*/
ret = esp_bluedroid_init();
if (ret) {
ESP_LOGE(GATTS_TAG, "%s init bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
//使能蓝牙栈
ret = esp_bluedroid_enable();
if (ret) {
ESP_LOGE(GATTS_TAG, "%s enable bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
return;
}
//树立蓝牙的FSM(有限状况机)
//这里运用回调函数来操控每个状况下的呼应,需求将其在GATT和GAP层的回调函数注册
/*gatts_event_handler和gap_event_handler处理蓝牙栈可能发生的一切状况,达到FSM的作用*/
ret = esp_ble_gatts_register_callback(gatts_event_handler);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts register error, error code = %x", ret);
return;
}
ret = esp_ble_gap_register_callback(gap_event_handler);
if (ret){
ESP_LOGE(GATTS_TAG, "gap register error, error code = %x", ret);
return;
}
//下面创立了BLE GATT服务A,相当于1个独立的运用程序
ret = esp_ble_gatts_app_register(PROFILE_A_APP_ID);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
return;
}
//下面创立了BLE GATT服务B,相当于1个独立的运用程序
ret = esp_ble_gatts_app_register(PROFILE_B_APP_ID);
if (ret){
ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);
return;
}
/*
设置了MTU的值(经过MTU交流,然后设置一个PDU中最大能够交流的数据量)。
例如:主设备宣布一个1000字节的MTU恳求,可是从设备回应的MTU是500字节,那么今后两边要以较小的值500字节作为今后的MTU。
即主从两边每次在做数据传输时不超越这个最大数据单元。
*/
esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500);
if (local_mtu_ret){
ESP_LOGE(GATTS_TAG, "set local MTU failed, error code = %x", local_mtu_ret);
}
.......
五、服务数据结构体设置
一个GATT 服务器运用程序架构(由Application Profiles安排起来)如下:
每个Profile界说为一个结构体,结构体成员依赖于该Application Profile 完结的services服务和characteristic特征。结构体成员还包括GATT interface(GATT 接口)、Application ID(运用程序ID)和处理profile工作的回调函数。
每个profile包括GATT interface(GATT 接口)、Application ID(运用程序ID)、 Connection ID(衔接ID)、Service Handle(服务句柄)、Service ID(服务ID)、Characteristic handle(特征句柄)、Characteristic UUID(特征UUID)、ATT权限、Characteristic Properties、描绘符句柄、描绘符UUID。
如果Characteristic支撑告诉(notifications)或指示(indicatons),它就有必要是完结CCCD(Client Characteristic Configuration Descriptor)—-这是额外的ATT。描绘符有一个句柄和UUID。如:
struct gatts_profile_inst {
esp_gatts_cb_t gatts_cb; //GATT的回调函数
uint16_t gatts_if; //GATT的接口
uint16_t app_id; //运用的ID
uint16_t conn_id; //衔接的ID
uint16_t service_handle; //服务Service句柄
esp_gatt_srvc_id_t service_id; //服务Service ID
uint16_t char_handle; //特征Characteristic句柄
esp_bt_uuid_t char_uuid; //特征Characteristic的UUID
esp_gatt_perm_t perm; //特征特点Attribute 授权
esp_gatt_char_prop_t property; //特征Characteristic的特性
uint16_t descr_handle; //描绘descriptor句柄
esp_bt_uuid_t descr_uuid; //描绘descriptorUUID
};
Application Profile存储在数组中,并分配相应的回调函数gatts_profile_a_event_handler()
和 gatts_profile_b_event_handler()
。GATT 客户端上的不同运用程序运用不同的接口,由 gatts_if 参数表明。关于初始化,此参数设置为ESP_GATT_IF_NONE
,这意味着运用程序装备文件没有链接到任何客户端。
/* One gatt-based profile one app_id and one gatts_if, this array will store the gatts_if returned by ESP_GATTS_REG_EVT */
static struct gatts_profile_inst gl_profile_tab[PROFILE_NUM] = {
[PROFILE_A_APP_ID] = {
.gatts_cb = gatts_profile_a_event_handler,
.gatts_if = ESP_GATT_IF_NONE, /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},
[PROFILE_B_APP_ID] = {
.gatts_cb = gatts_profile_b_event_handler, /* This demo does not implement, similar as profile A */
.gatts_if = ESP_GATT_IF_NONE, /* Not get the gatt_if, so initial is ESP_GATT_IF_NONE */
},
};
这是两个元素的数组。能够用Application ID来注册Application Profiles,Application ID是由运用程序分配的用来标识每个Profile。 经过这种方法,能够在一个Server中具有多个Application Profile。
esp_ble_gatts_app_register (PROFILE_A_APP_ID);
esp_ble_gatts_app_register (PROFILE_B_APP_ID);
六、GATT工作处理程序
其作用便是树立了蓝牙GATT的FSM(有限状况机),callback回调函数处理从BLE仓库推送到运用程序的一切工作。
回调函数的参数:
- event: esp_gatts_cb_event_t 这是一个枚举类型,表明调用该回调函数时的工作(或蓝牙的状况)
-
gatts_if: esp_gatt_if_t (uint8_t) 这是GATT拜访接口类型,一般在GATT客户端上不同的运用程序用不同的gatt_if(不同的Application profile对应不同的gatts_if) ,调用
esp_ble_gatts_app_register()
时,注册Application profile 就会有一个gatts_if。 - param: esp_ble_gatts_cb_param_t 指向回调函数的参数,是个联合体类型,不同的工作类型采用联合体内不同的成员结构体。
static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{
/*如果工作是注册工作,则为每个装备文件存储 gatts_if */
if (event == ESP_GATTS_REG_EVT) {
if (param->reg.status == ESP_GATT_OK) {
gl_profile_tab[param->reg.app_id].gatts_if = gatts_if;
} else {
ESP_LOGI(GATTS_TAG, "Reg app failed, app_id %04x, status %d\n",
param->reg.app_id,
param->reg.status);
return;
}
}
/*如果 gatts_if 等于 profile A,则调用 profile A cb handler,
* 所以这里调用每个 profile 的回调*/
do {
int idx;
for (idx = 0; idx < PROFILE_NUM; idx++) {
if (gatts_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
gatts_if == gl_profile_tab[idx].gatts_if) {
if (gl_profile_tab[idx].gatts_cb) {
gl_profile_tab[idx].gatts_cb(event, gatts_if, param);
}
}
}
} while (0);
}
七、注册创立服务
当调用esp_ble_gatts_app_register()
注册一个运用程序Profile(Application Profile),将触发ESP_GATTS_REG_EVT
工作,除了能够完结对应profile的gatts_if的注册,还能够调用esp_bel_create_attr_tab()
来创立profile Attributes 表或创立一个服务esp_ble_gatts_create_service()
。
static void gatts_profile_b_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
switch (event) {
case ESP_GATTS_REG_EVT:
ESP_LOGI(GATTS_TAG, "REGISTER_APP_EVT, status %d, app_id %d\n", param->reg.status, param->reg.app_id);
gl_profile_tab[PROFILE_B_APP_ID].service_id.is_primary = true;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.inst_id = 0x00;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_B_APP_ID].service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_B;
esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_B_APP_ID].service_id, GATTS_NUM_HANDLE_TEST_B);
break;
…
}
句柄数界说为4:
#define GATTS_NUM_HANDLE_TEST_B 4
句柄是:
- 服务句柄
GATTS_SERVICE_UUID_TEST_B 0x00EE
- 特征手柄
GATTS_CHAR_UUID_TEST_B 0xEE01
- 特征值句柄
- 特征描绘符句柄
GATTS_DESCR_UUID_TEST_B 0x2222
该服务被界说为具有 16 位 UUID 长度的首要服务。服务 ID 运用实例 ID = 0 和由 界说的 UUID 进行初始化GATTS_SERVICE_UUID_TEST_A。
服务实例 ID 可用于区分具有相同 UUID 的多个服务。在此示例中,因为每个运用程序装备文件只要一个服务而且服务具有不同的 UUID,因此在装备文件 A 和 B 中能够将服务实例 ID 界说为 0。可是,如果只要一个运用程序装备文件具有两个服务运用相同的 UUID,则有必要运用不同的实例 ID 来引证一个或另一个服务。
demo中的gatts_event_handler()
回调函数—调用esp_ble_gatts_app_register()
,触发ESP_GATTS_REG_EVT
时,完结对每个profile 的gatts_if 的注册。
gl_profile_tab[param->reg.app_id].gatts_if = gatts_if;
如果gatts_if == 某个Profile的gatts_if时,调用对应profile的回调函数处理工作。
if (gatts_if == ESP_GATT_IF_NONE || /* ESP_GATT_IF_NONE, not specify a certain gatt_if, need to call every profile cb function */
gatts_if == gl_profile_tab[idx].gatts_if) {
if (gl_profile_tab[idx].gatts_cb) {
gl_profile_tab[idx].gatts_cb(event, gatts_if, param);
}
}
八、发动服务和创立特征
8.1 发动服务
当一个服务service创立成功后,由该profile GATT handler 办理的 ESP_GATTS_CREATE_EVT
工作被触发,在这个工作能够发动服务和增加特征characteristics到服务中。调用esp_ble_gatts_start_service()
来发动指定服务。
case ESP_GATTS_CREATE_EVT:
ESP_LOGI(GATTS_TAG, "CREATE_SERVICE_EVT, status %d, service_handle %d\n", param->create.status, param->create.service_handle);
gl_profile_tab[PROFILE_A_APP_ID].service_handle = param->create.service_handle;
gl_profile_tab[PROFILE_A_APP_ID].char_uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_A_APP_ID].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_TEST_A;
esp_ble_gatts_start_service(gl_profile_tab[PROFILE_A_APP_ID].service_handle);
a_property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY;
esp_err_t add_char_ret =
esp_ble_gatts_add_char(gl_profile_tab[PROFILE_A_APP_ID].service_handle,
&gl_profile_tab[PROFILE_A_APP_ID].char_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
a_property,
&gatts_demo_char1_val,
NULL);
if (add_char_ret){
ESP_LOGE(GATTS_TAG, "add char failed, error code =%x",add_char_ret);
}
break;
首要,由BLE仓库生成生成的服务句柄(service handle)存储在装备文件Profile表中,运用层将用服务句柄来引证这个服务。调用esp_ble_gatts_start_service()
和从前产生服务句柄来发动服务。
8.2 创立特征
Characteristic是在GATT标准中最小的逻辑数据单元,由一个Value和多个描绘特性的Desciptior组成。实际上,在与蓝牙设备打交道,首要便是读写Characteristic的value来完结。 相同的,Characteristic也是经过16bit或128bit的UUID仅有标识。
咱们依据蓝牙设备的协议用对应的Characteristci进行读写即可达到与其通讯的意图。
增加特征到service中,调用esp_ble_gatts_add_char()
来增加characteristics连同characteristic权限和property(特点)到服务service中。
权限:
-
ESP_GATT_PERM_READ
: 答应读取特征值 -
ESP_GATT_PERM_WRITE
: 答应写入特征值
特性:
-
ESP_GATT_CHAR_PROP_BIT_READ
: 能够读取特性 -
ESP_GATT_CHAR_PROP_BIT_WRITE
: 特征可写 -
ESP_GATT_CHAR_PROP_BIT_NOTIFY
: 特性能够告诉值的改变
一起具有读写权限和特点似乎是剩余的。可是,特点的读写特点是向客户端显示的信息,意图是让客户端知道服务器是否承受读写恳求。从这个意义上说,这些特点充当客户端正确拜访服务器资源的提示。另一方面,权限是颁发客户端读取或写入该特点的授权。例如,如果客户端尝试写入它没有写入权限的特点,即使设置了写入特点,服务器也会拒绝该恳求。
此外,demo还为表明特征供给了一个初始值gatts_demo_char1_val。初始值界说如下:
#define GATTS_DEMO_CHAR_VAL_LEN_MAX 0x40
uint8_t char1_str[] = {0x11,0x22,0x33};
esp_attr_value_t gatts_demo_char1_val =
{
. attr_max_len = GATTS_DEMO_CHAR_VAL_LEN_MAX,
. attr_len = sizeof (char1_str),
. attr_value = char1_str,
};
特征初始值有必要是非空目标而且特征长度有必要一直大于零,不然仓库将回来错误。
最终,特性被装备为每次读取或写入特性时都需求手动发送呼应,而不是让仓库主动呼应。这是经过将esp_ble_gatts_add_char()
函数的最终一个参数(表明特点呼应操控参数)设置为ESP_GATT_RSP_BY_APP
或 NULL 来装备的。
七、创立特征描绘符
当特征增加到service中成功时,触发ESP_GATTS_ADD_CHAR_EVT
工作。该工作回来由仓库为刚刚增加的特征生成的句柄。该工作包括以下参数:
esp_gatt_status_t状况; /* !< 操作状况*/
uint16_t attr_handle; /* !< 特征特点句柄*/
uint16_t service_handle; /* !< 服务特点句柄*/
esp_bt_uuid_t char_uuid; /* !< 特征 uuid */
工作回来的特点句柄存储在装备文件表中,而且还设置了特征描绘符长度和 UUID。运用该esp_ble_gatts_get_attr_value()
函数读取特征长度和值,然后打印以供参阅。最终,运用该esp_ble_gatts_add_char_descr()
函数增加特征描绘。运用的参数是服务句柄、描绘符 UUID、写入和读取权限、初始值和主动呼应设置。特征描绘符的初始值能够是空指针,主动呼应参数也设置为空,这意味着需求呼应的恳求有必要手动回复。
case ESP_GATTS_ADD_CHAR_EVT: {
uint16_t length = 0;
const uint8_t *prf_char;
ESP_LOGI(GATTS_TAG, "ADD_CHAR_EVT, status %d, attr_handle %d, service_handle %d\n",
param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);
gl_profile_tab[PROFILE_A_APP_ID].char_handle = param->add_char.attr_handle;
gl_profile_tab[PROFILE_A_APP_ID].descr_uuid.len = ESP_UUID_LEN_16;
gl_profile_tab[PROFILE_A_APP_ID].descr_uuid.uuid.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;
esp_err_t get_attr_ret = esp_ble_gatts_get_attr_value(param->add_char.attr_handle, &length, &prf_char);
if (get_attr_ret == ESP_FAIL){
ESP_LOGE(GATTS_TAG, "ILLEGAL HANDLE");
}
ESP_LOGI(GATTS_TAG, "the gatts demo char length = %x\n", length);
for(int i = 0; i < length; i++){
ESP_LOGI(GATTS_TAG, "prf_char[%x] = %x\n",i,prf_char[i]);
}
esp_err_t add_descr_ret = esp_ble_gatts_add_char_descr(
gl_profile_tab[PROFILE_A_APP_ID].service_handle,
&gl_profile_tab[PROFILE_A_APP_ID].descr_uuid,
ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
NULL,NULL);
if (add_descr_ret){
ESP_LOGE(GATTS_TAG, "add char descr failed, error code = %x", add_descr_ret);
}
break;
}
增加描绘符后,将ESP_GATTS_ADD_CHAR_DESCR_EVT
触发工作,在此示例中用于打印信息音讯。
case ESP_GATTS_ADD_CHAR_DESCR_EVT:
ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d\n",
param->add_char.status, param->add_char.attr_handle,
param->add_char.service_handle);
break;
九、衔接工作
9.1 更新衔接参数
一个ESP_GATTS_CONNECT_EVT
当客户端已衔接到服务器GATT被触发。此工作用于更新衔接参数,例如延迟、最小衔接距离、最大衔接距离和超时。衔接参数存储在一个esp_ble_conn_update_params_t
结构中,然后传递给esp_ble_gap_update_conn_params()
函数。更新衔接参数进程只需履行一次,因此装备文件 B 衔接工作处理程序不包括该esp_ble_gap_update_conn_params()
函数。最终,工作回来的衔接 ID 存储在装备文件表中。
装备文件 A 衔接工作:
case ESP_GATTS_CONNECT_EVT: {
esp_ble_conn_update_params_t conn_params = {0};
memcpy(conn_params.bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
/* For the IOS system, please reference the apple official documents about the ble connection parameters restrictions. */
conn_params.latency = 0;
conn_params.max_int = 0x30; // max_int = 0x30*1.25ms = 40ms
conn_params.min_int = 0x10; // min_int = 0x10*1.25ms = 20ms
conn_params.timeout = 400; // timeout = 400*10ms = 4000ms
ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:, is_conn %d",
param->connect.conn_id,
param->connect.remote_bda[0],
param->connect.remote_bda[1],
param->connect.remote_bda[2],
param->connect.remote_bda[3],
param->connect.remote_bda[4],
param->connect.remote_bda[5],
param->connect.is_connected);
gl_profile_tab[PROFILE_A_APP_ID].conn_id = param->connect.conn_id;
//start sent the update connection parameters to the peer device.
esp_ble_gap_update_conn_params(&conn_params);
break;
}
装备文件 B 衔接工作:
case ESP_GATTS_CONNECT_EVT:
ESP_LOGI(GATTS_TAG, "CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:, is_conn %d\n",
param->connect.conn_id,
param->connect.remote_bda[0],
param->connect.remote_bda[1],
param->connect.remote_bda[2],
param->connect.remote_bda[3],
param->connect.remote_bda[4],
param->connect.remote_bda[5],
param->connect.is_connected);
gl_profile_tab[PROFILE_B_APP_ID].conn_id = param->connect.conn_id;
break;
该esp_ble_gap_update_conn_params()
函数触发一个 GAP 工作ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT
,用于打印衔接信息:
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
ESP_LOGI(GATTS_TAG, "update connection params status = %d, min_int = %d, max_int = %d,
conn_int = %d,latency = %d, timeout = %d",
param->update_conn_params.status,
param->update_conn_params.min_int,
param->update_conn_params.max_int,
param->update_conn_params.conn_int,
param->update_conn_params.latency,
param->update_conn_params.timeout);
break;
9.2 承认MTU巨细
当有手机(client客户端)连上server时,触发ESP_GATTS_MTU_EVT工作,其打印如下图所示
ESP_GATTS_MTU_EVT工作对应的回调函数中参数param的结构体为gatts_mtu_evt_param(包括衔接id和MTU巨细)
/**
* @brief ESP_GATTS_MTU_EVT
*/
struct gatts_mtu_evt_param {
uint16_t conn_id; /*!< Connection id */
uint16_t mtu; /*!< MTU size */
} mtu; /*!< Gatt server callback param of ESP_GATTS_MTU_EVT */
在例子中设置本地的MTU巨细为500,代码如下所示:
esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(500); 如上所述,设置了MTU的值(经过MTU交流,然后设置一个PDU中最大能够交流的数据量)。例如:主设备宣布一个150字节的MTU恳求,可是从设备回应的MTU是23字节,那么今后两边要以较小的值23字节作为今后的MTU。即主从两边每次在做数据传输时不超越这个最大数据单元。 MTU交流一般发生在主从两边树立衔接后。MTU比较小,便是为什么BLE不能传输大数据的原因地点。
参照一分钟读懂低功耗(BLE)MTU交流数据包 这篇文章就能够了解MTU交流进程。
MTU交流恳求用于client告诉server关于client最大接纳MTU巨细并恳求server呼应它的最大接纳MTU巨细。
Client的接纳MTU 应该大于或等于默认ATT_MTU(23).这个恳求已树立衔接就由client宣布。这个Client Rx MTU参数应该设置为client能够接纳的attribute protocol PDU最大尺寸。
MTU交流应对发送用于接纳到一个Exchange MTU恳求
这个应对由server宣布,server的接纳MTU有必要大于或等于默认ATT_MTU巨细。这里的Server Rx MTU应该设置为 服务器能够接纳的attribute protocol PDU 最大尺寸。
Server和Client应该设置ATT_MTU为Client Rx MTU和Server Rx MTU两者的较小值。
这个ATT_MTU在server在宣布这个应对后,在发其他特点协议PDU之前收效;在client收到这个应对并在发其他特点协议PDU之前收效。
十、办理读取工作
现在现已创立并发动了服务和特征,程序能够接纳读写工作。读取操作由ESP_GATTS_READ_EVT
工作表明,它具有以下参数:
uint16_t conn_id; /* !< 衔接 ID */
uint32_t trans_id; /* !< 传输 ID */
esp_bd_addr_t bda; /* !< 读取的蓝牙设备地址*/
uint16_t handle; /* !< 特点句柄*/
uint16_t offset; /* !< 值的偏移量,如果值太长*/
bool is_long; /* !< 值是否过长*/
bool need_rsp; /*!<读操作需求做呼应*/
demo中,呼应是用虚拟数据结构的,并运用工作给定的相同句柄发送回主机。除了呼应之外,GATT 接口、衔接 ID 和传输 ID 也作为参数包括在esp_ble_gatts_send_response()
函数中。如果在创立特征或描绘符时将主动呼应字节设置为 NULL,则此功用是必需的。
case ESP_GATTS_READ_EVT: {
ESP_LOGI(GATTS_TAG, "GATT_READ_EVT, conn_id %d, trans_id %d, handle %d\n",
param->read.conn_id, param->read.trans_id, param->read.handle);
esp_gatt_rsp_t rsp;
memset(&rsp, 0, sizeof(esp_gatt_rsp_t));
rsp.attr_value.handle = param->read.handle;
rsp.attr_value.len = 4;
rsp.attr_value.value[0] = 0xde;
rsp.attr_value.value[1] = 0xed;
rsp.attr_value.value[2] = 0xbe;
rsp.attr_value.value[3] = 0xef;
esp_ble_gatts_send_response(gatts_if,
param->read.conn_id,
param->read.trans_id,
ESP_GATT_OK, &rsp);
break;
}
十一、办理写入工作
写入工作由工作表明ESP_GATTS_WRITE_EVT
,它具有以下参数:
uint16_t conn_id; /* !< 衔接 ID */
uint32_t trans_id; /* !< 传输 ID */
esp_bd_addr_t bda; /* !< 写入的蓝牙设备地址*/
uint16_t handle; /* !< 特点句柄*/
uint16_t offset; /* !< 值的偏移量,如果值太长*/
bool need_rsp; /* !< 写操作需求做呼应*/
bool is_prep; /*!< 这个写操作是prepare write */
uint16_t len; /* !< 写入特点值长度*/
uint8_t *value; /* !< 写入特点值*/
demo中完结了两种类型的写工作,写特征值和写长特征值。当特征值能够容纳在一个特点协议最大传输单元 (ATT MTU) 中时,运用第一种类型的写入,该单元一般为 23 字节长。当要写入的特点善于单个 ATT 音讯中能够发送的特点时运用第二种类型,经过运用预备写入呼应将数据分红多个块,然后运用履行写入恳求来承认或撤销完好的写入恳求. 此行为在蓝牙标准版别 4.2,第 3 卷,G 部分,第 4.9 节中界说。写长特征音讯流如下图所示。
当触发写入工作时,此示例打印日志音讯,然后履行example_write_event_env()
函数。
case ESP_GATTS_WRITE_EVT: {
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %d, handle %d\n", param->write.conn_id, param->write.trans_id, param->write.handle);
if (!param->write.is_prep){
ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, value len %d, value :", param->write.len);
esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);
if (gl_profile_tab[PROFILE_B_APP_ID].descr_handle == param->write.handle && param->write.len == 2){
uint16_t descr_value= param->write.value[1]<<8 | param->write.value[0];
if (descr_value == 0x0001){
if (b_property & ESP_GATT_CHAR_PROP_BIT_NOTIFY){
ESP_LOGI(GATTS_TAG, "notify enable");
uint8_t notify_data[15];
for (int i = 0; i < sizeof(notify_data); ++i)
{
notify_data[i] = i%0xff;
}
//the size of notify_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id,
gl_profile_tab[PROFILE_B_APP_ID].char_handle,
sizeof(notify_data),
notify_data, false);
}
}else if (descr_value == 0x0002){
if (b_property & ESP_GATT_CHAR_PROP_BIT_INDICATE){
ESP_LOGI(GATTS_TAG, "indicate enable");
uint8_t indicate_data[15];
for (int i = 0; i < sizeof(indicate_data); ++i)
{
indicate_data[i] = i % 0xff;
}
//the size of indicate_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id,
gl_profile_tab[PROFILE_B_APP_ID].char_handle,
sizeof(indicate_data),
indicate_data, true);
}
}
else if (descr_value == 0x0000){
ESP_LOGI(GATTS_TAG, "notify/indicate disable ");
}else{
ESP_LOGE(GATTS_TAG, "unknown value");
}
}
}
example_write_event_env(gatts_if, &a_prepare_write_env, param);
break;
}
该example_write_event_env()
函数包括写长特征进程的逻辑:
void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param){
esp_gatt_status_t status = ESP_GATT_OK;
if (param->write.need_rsp){
if (param->write.is_prep){
if (prepare_write_env->prepare_buf == NULL){
prepare_write_env->prepare_buf = (uint8_t *)malloc(PREPARE_BUF_MAX_SIZE*sizeof(uint8_t));
prepare_write_env->prepare_len = 0;
if (prepare_write_env->prepare_buf == NULL) {
ESP_LOGE(GATTS_TAG, "Gatt_server prep no mem\n");
status = ESP_GATT_NO_RESOURCES;
}
} else {
if(param->write.offset > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_OFFSET;
}
else if ((param->write.offset + param->write.len) > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_ATTR_LEN;
}
}
esp_gatt_rsp_t *gatt_rsp = (esp_gatt_rsp_t *)malloc(sizeof(esp_gatt_rsp_t));
gatt_rsp->attr_value.len = param->write.len;
gatt_rsp->attr_value.handle = param->write.handle;
gatt_rsp->attr_value.offset = param->write.offset;
gatt_rsp->attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
memcpy(gatt_rsp->attr_value.value, param->write.value, param->write.len);
esp_err_t response_err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id,
param->write.trans_id, status, gatt_rsp);
if (response_err != ESP_OK){
ESP_LOGE(GATTS_TAG, "Send response error\n");
}
free(gatt_rsp);
if (status != ESP_GATT_OK){
return;
}
memcpy(prepare_write_env->prepare_buf + param->write.offset,
param->write.value,
param->write.len);
prepare_write_env->prepare_len += param->write.len;
}else{
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL);
}
}
}
当客户端发送写恳求或预备写恳求时,服务器应呼应。可是,如果客户端发送 Write without Response 指令,则服务器不需求回复呼应。这是在写入进程中经过查看 的值来查看的write.need_rsp parameter
。如果需求呼应,程序持续做呼应预备,如果不存在,客户端不需求呼应,因此程序完毕。呼应的话会影响数据传输速度,在需求大数据量的场合是否适宜需求试验?
void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare_write_env,
esp_ble_gatts_cb_param_t *param){
esp_gatt_status_t status = ESP_GATT_OK;
if (param->write.need_rsp){
…
然后该函数查看是否write.is_prep
设置了由 表明的 Prepare Write Request 参数,这意味着客户端正在恳求 Write Long Characteristic。如果存在,该进程持续预备多个写呼应,如果不存在,则服务器简略地发回单个写呼应。
…
if (param->write.is_prep){
…
}else{
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL);
}
…
为了处理长特征写入,界说并实例化了一个预备缓冲区结构:
typedef struct {
uint8_t *prepare_buf;
int prepare_len;
} prepare_type_env_t;
static prepare_type_env_t a_prepare_write_env;
static prepare_type_env_t b_prepare_write_env;
为了运用预备缓冲区,为其分配了一些内存空间。如果因为内存不足导致分配失利,则会打印错误:
else {
if(param->write.offset > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_OFFSET;
}
else if ((param->write.offset + param->write.len) > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_ATTR_LEN;
}
}
该进程现在预备esp_gatt_rsp_t
要发送回客户端的类型呼应。它运用写入恳求的相同参数结构的呼应,例如长度、句柄和偏移量。另外,写入该特性所需的GATT认证类型设置为ESP_GATT_AUTH_REQ_NONE
,这意味着客户端能够写入该特性而无需先进行身份验证。一旦发送呼应,分配给它运用的内存就会被开释。
esp_gatt_rsp_t *gatt_rsp = (esp_gatt_rsp_t *)malloc(sizeof(esp_gatt_rsp_t));
gatt_rsp->attr_value.len = param->write.len;
gatt_rsp->attr_value.handle = param->write.handle;
gatt_rsp->attr_value.offset = param->write.offset;
gatt_rsp->attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
memcpy(gatt_rsp->attr_value.value, param->write.value, param->write.len);
esp_err_t response_err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id,
param->write.trans_id, status, gatt_rsp);
if (response_err != ESP_OK){
ESP_LOGE(GATTS_TAG, "Send response error\n");
}
free(gatt_rsp);
if (status != ESP_GATT_OK){
return;
}
最终,传入的数据被复制到创立的缓冲区中,其长度按偏移量递加:
case ESP_GATTS_EXEC_WRITE_EVT:
ESP_LOGI(GATTS_TAG,"ESP_GATTS_EXEC_WRITE_EVT");
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
example_exec_write_event_env(&a_prepare_write_env, param);
break;
咱们来看看Executive Write函数:
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param){
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
esp_log_buffer_hex(GATTS_TAG, prepare_write_env->prepare_buf, prepare_write_env->prepare_len);
}
else{
ESP_LOGI(GATTS_TAG,"ESP_GATT_PREP_WRITE_CANCEL");
}
if (prepare_write_env->prepare_buf) {
free(prepare_write_env->prepare_buf);
prepare_write_env->prepare_buf = NULL;
}
#### prepare_write_env->prepare_len = 0;
}
履行写入用于承认或撤销之前完结的写入进程,由长特征写入进程。为此,该函数会查看exec_write_flag
随工作接纳到的参数中的 。如果标志等于 表明的履行标志exec_write_flag
,则承认写入并在日志中打印缓冲区;如果不是,则表明撤销写入并删除一切已写入的数据。
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
esp_log_buffer_hex(GATTS_TAG,
prepare_write_env->prepare_buf,
prepare_write_env->prepare_len);
}
else{
ESP_LOGI(GATTS_TAG,"ESP_GATT_PREP_WRITE_CANCEL");
}
最终,为存储来自长写操作的数据块而创立的缓冲区结构被开释,并将其指针设置为 NULL 以使其为下一个长写进程做好预备。
if (prepare_write_env->prepare_buf) {
free(prepare_write_env->prepare_buf);
prepare_write_env->prepare_buf = NULL;
}
prepare_write_env->prepare_len = 0;
11.1 使能告诉
使能notify并读取蓝牙发过来的数据,敞开这个后咱们就能实时获取蓝牙发过来的值了。
使能告诉(notify enable)的打印如下所示,翻开告诉实际上的一个WRITE。
如果write.handle和descr_handle相同,且长度==2,承认descr_value描绘值,依据描绘值敞开/封闭 告诉notify/indicate。
//the size of notify_data[] need less than MTU size
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_A_APP_ID].char_handle,
sizeof(notify_data), notify_data, false);
该函数将notify或indicate发给GATT的客户端;
need_confirm = false,则发送的是notification告诉;
==true,发送的是指示indication。
其他参数: 服务端拜访接口;衔接id; 特点句柄,value_len; 值
• 由 Leung 写于 2021 年 7 月 7 日
• 参阅:ESP32学习笔记(7)蓝牙GATT服务运用
Gatt 服务器示例演练