【ESP32】ESP-IDF开发 | 低功耗蓝牙开发 | 蓝牙空中串口 + 服务端和客户端例程
1. 简介
在经典蓝牙开发的文章——经典蓝牙开发 | 蓝牙串口协议(SPP) + 客户端和服务端例程中,有介绍过 SPP 协议(蓝牙串口协议),它可以实现蓝牙设备间的虚拟串口;但在低功耗蓝牙中并不存在这种协议,因此,想实现同样的功能可以通过定义一个厂商自定义的服务来实现。
2. 例程
例程分为两个,一个是服务端的例程,另一个是客户端的例程。测试时,手机需下载一个蓝牙调试APP,应用商店随便下一个即可。
下面例程的代码解释只包含前面文章中没有涉及的,可以跳转栏目目录查看对应的文章
2.1 menuconfig
ESP-IDF是默认不使能蓝牙相关的编译环境的,所以开发前需要在menuconfig中打开对应选项。首先,配置蓝牙控制器为仅低功耗蓝牙模式。
然后配置主机协议栈为NimBLE。
2.2 服务端
2.2.1 代码
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <stdbool.h>#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"#include "nvs.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_log.h"#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"#define TAG "app"
#define OWN_NAME "ESP32"#define BLE_SVC_SPP_UUID16 0xABF0
#define BLE_SVC_SPP_CHR_UUID16 0xABF1#define BDASTR "%02X:%02X:%02X:%02X:%02X:%02X"
#define BDA2STR(x) (x)[0], (x)[1], (x)[2], (x)[3], (x)[4], (x)[5]static uint8_t ble_addr_type;
static uint16_t ble_gatt_handle;
static uint8_t device_name[] = OWN_NAME;static void ble_advertise(void);
static int ble_gap_event(struct ble_gap_event *event, void *arg);
static int ble_svc_gatt_handler(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg);
static void ble_print_conn_desc(struct ble_gap_conn_desc *desc);
static void ble_on_reset(int reason);
static void ble_on_sync(void);
static void ble_host_task(void *param);static const struct ble_gatt_svc_def ble_svc_gatt_defs[] = {{.type = BLE_GATT_SVC_TYPE_PRIMARY,.uuid = BLE_UUID16_DECLARE(BLE_SVC_SPP_UUID16),.characteristics = (struct ble_gatt_chr_def[]){ {.uuid = BLE_UUID16_DECLARE(BLE_SVC_SPP_CHR_UUID16),.access_cb = ble_svc_gatt_handler,.val_handle = &ble_gatt_handle,.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_NOTIFY,}, {0,}},},{0,},
};static void ble_print_conn_desc(struct ble_gap_conn_desc *desc)
{ESP_LOGI(TAG, "handle=%d our_ota_addr_type=%d our_ota_addr=" BDASTR,desc->conn_handle, desc->our_ota_addr.type, BDA2STR(desc->our_ota_addr.val));ESP_LOGI(TAG, " our_id_addr_type=%d our_id_addr=" BDASTR,desc->our_id_addr.type, BDA2STR(desc->our_ota_addr.val));ESP_LOGI(TAG, " peer_ota_addr_type=%d peer_ota_addr=" BDASTR,desc->peer_ota_addr.type, BDA2STR(desc->our_ota_addr.val));ESP_LOGI(TAG, " peer_id_addr_type=%d peer_id_addr=" BDASTR,desc->peer_id_addr.type, BDA2STR(desc->our_ota_addr.val));ESP_LOGI(TAG, " conn_itvl=%d conn_latency=%d supervision_timeout=%d ""encrypted=%d authenticated=%d bonded=%d\n",desc->conn_itvl, desc->conn_latency,desc->supervision_timeout,desc->sec_state.encrypted,desc->sec_state.authenticated,desc->sec_state.bonded);
}static void ble_advertise(void)
{ int rc;struct ble_hs_adv_fields fields;memset(&fields, 0, sizeof(fields));/* 标志位 */fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;/* 发射功率 */fields.tx_pwr_lvl_is_present = 1;fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;/* 设备名 */fields.name = device_name;fields.name_len = strlen(OWN_NAME);fields.name_is_complete = 1;/* UUID */fields.uuids16 = (const ble_uuid16_t *) BLE_UUID16_DECLARE(BLE_SVC_SPP_UUID16);fields.num_uuids16 = 1;fields.uuids16_is_complete = 1;rc = ble_gap_adv_set_fields(&fields);if (rc != 0) {ESP_LOGE(TAG, "error setting advertisement data; rc=%d", rc);return;}/* 开始广播 */struct ble_gap_adv_params adv_params;memset(&adv_params, 0, sizeof(adv_params));adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;rc = ble_gap_adv_start(ble_addr_type, NULL, BLE_HS_FOREVER, &adv_params, ble_gap_event, NULL);if (rc != 0) {ESP_LOGE(TAG, "error enabling advertisement, rc=%d", rc);return;}
}static int ble_gap_event(struct ble_gap_event *event, void *arg)
{int rc = 0;switch (event->type) {/* 连接建立事件 */case BLE_GAP_EVENT_LINK_ESTAB:ESP_LOGI(TAG, "connection %s, status=%d",event->connect.status == 0 ? "established" : "failed",event->connect.status);if (event->connect.status != 0) {ble_advertise();} else {struct ble_gap_conn_desc desc;rc = ble_gap_conn_find(event->connect.conn_handle, &desc);if (rc != 0) {ESP_LOGE(TAG, "error enabling advertisement, ret=%d", rc);return rc;}ble_print_conn_desc(&desc);}break;/* 连接断开事件 */case BLE_GAP_EVENT_DISCONNECT:ESP_LOGI(TAG, "disconnect, reason=%d", event->disconnect.reason);ble_advertise();break;/* 广播完成事件 */case BLE_GAP_EVENT_ADV_COMPLETE:ESP_LOGI(TAG, "advertise complete");ble_advertise();break;/* 订阅事件 */case BLE_GAP_EVENT_SUBSCRIBE:ESP_LOGI(TAG, "subscribe event, cur_notify=%d, val_handle=%d",event->subscribe.cur_notify,event->subscribe.attr_handle);break;/* MTU更新事件 */case BLE_GAP_EVENT_MTU:ESP_LOGI(TAG, "mtu updated, conn_handle=%d mtu=%d",event->mtu.conn_handle,event->mtu.value);break;default:break;}return rc;
}static void ble_on_reset(int reason)
{ESP_LOGE(TAG, "Resetting state; reason=%d", reason);
}static void ble_on_sync(void)
{int ret;/* 确保地址合法 */ret = ble_hs_util_ensure_addr(0);if (ret != ESP_OK) {ESP_LOGE(TAG, "ble_hs_util_ensure_addr failed, err: %d", ret);return;}/* 获取最佳地址类型 */ret = ble_hs_id_infer_auto(0, &ble_addr_type);if (ret) {ESP_LOGE(TAG, "ble_hs_id_infer_auto failed, err: %d", ret);return;}/* 获取地址 */uint8_t addr_val[6] = {0};ret = ble_hs_id_copy_addr(ble_addr_type, addr_val, NULL);if (ret) {ESP_LOGE(TAG, "ble_hs_id_copy_addr failed, err: %d", ret);return;}ESP_LOGI(TAG, "Device address: " BDASTR, BDA2STR(addr_val));/* 使能广播 */ble_advertise();
}static int ble_svc_gatt_handler(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg)
{switch (ctxt->op){case BLE_GATT_ACCESS_OP_READ_CHR:ESP_LOGI(TAG, "Callback for read");break;case BLE_GATT_ACCESS_OP_WRITE_CHR:ESP_LOGI(TAG, "Data received in write event,conn_handle = %x,attr_handle = %x", conn_handle, attr_handle);ESP_LOGI(TAG, "%s", ctxt->om->om_data);/* 发送回复 */const char* reply = "I have receive your message";struct os_mbuf *om = ble_hs_mbuf_from_flat(reply, strlen(reply));if (!om) {ESP_LOGE(TAG, "Failed to alloc os_mbuf");return -1;}ble_gatts_notify_custom(conn_handle, ble_gatt_handle, om);break;default:break;}return 0;
}static void ble_host_task(void *param)
{nimble_port_run();nimble_port_freertos_deinit();
}int app_main()
{/* 初始化NVS */esp_err_t ret = nvs_flash_init();if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {ESP_ERROR_CHECK(nvs_flash_erase());ESP_ERROR_CHECK(nvs_flash_init());}/* 初始化控制器和NimBLE协议栈 */ret = nimble_port_init();if (ret != ESP_OK) {ESP_LOGE(TAG, "Failed to init nimble %d ", ret);return -1;}/* 配置主机 */ble_hs_cfg.reset_cb = ble_on_reset;ble_hs_cfg.sync_cb = ble_on_sync;ble_hs_cfg.store_status_cb = ble_store_util_status_rr;/* 使能绑定 */ble_hs_cfg.sm_bonding = 1;ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;ble_hs_cfg.sm_sc = 1;ble_hs_cfg.sm_mitm = 1;/* 设置设备名 */if (ble_svc_gap_device_name_set(OWN_NAME) != 0) {ESP_LOGE(TAG, "set device name failed");return -1;}/* 使能蓝牙串口服务 */ble_svc_gap_init();ble_svc_gatt_init();ret = ble_gatts_count_cfg(ble_svc_gatt_defs);if (ret != ESP_OK) {ESP_LOGE(TAG, "Failed to config GATT service %d ", ret);return -1;}ret = ble_gatts_add_svcs(ble_svc_gatt_defs);if (ret != ESP_OK) {ESP_LOGE(TAG, "Failed to add GATT service %d ", ret);return -1;}/* 使能NimBLE协议栈 */nimble_port_freertos_init(ble_host_task);return 0;
}
初始化完蓝牙控制器和协议栈后,调用 ble_svc_gap_init 初始化 GAP 协议和 ble_svc_gatt_init 初始化 GATT 规范。
然后调用 ble_gatts_count_cfg 和 ble_gatts_add_svcs 去注册服务,它们传入的参数是一样的,结构体定义如下:
struct ble_gatt_svc_def {/*** One of the following:* o BLE_GATT_SVC_TYPE_PRIMARY - primary service* o BLE_GATT_SVC_TYPE_SECONDARY - secondary service* o 0 - No more services in this array.*/uint8_t type;/*** Pointer to service UUID; use BLE_UUIDxx_DECLARE macros to declare* proper UUID; NULL if there are no more characteristics in the service.*/const ble_uuid_t *uuid;/*** Array of pointers to other service definitions. These services are* reported as "included services" during service discovery. Terminate the* array with NULL.*/const struct ble_gatt_svc_def **includes;/*** Array of characteristic definitions corresponding to characteristics* belonging to this service.*/const struct ble_gatt_chr_def *characteristics;
};
- type:服务类型,BLE_GATT_SVC_TYPE_PRIMARY(主要服务)、BLE_GATT_SVC_TYPE_SECONDARY(次要服务);
- uuid:本服务对应的UUID;
- includes:其他服务列表,空节点表示结束;
- characteristics:特性列表,空节点表示结束。
特性列表的节点结构体定义如下:
struct ble_gatt_chr_def {/*** Pointer to characteristic UUID; use BLE_UUIDxx_DECLARE macros to declare* proper UUID; NULL if there are no more characteristics in the service.*/const ble_uuid_t *uuid;/*** Callback that gets executed when this characteristic is read or* written.*/ble_gatt_access_fn *access_cb;/** Optional argument for callback. */void *arg;/*** Array of this characteristic's descriptors. NULL if no descriptors.* Do not include CCCD; it gets added automatically if this* characteristic's notify or indicate flag is set.*/struct ble_gatt_dsc_def *descriptors;/** Specifies the set of permitted operations for this characteristic. */ble_gatt_chr_flags flags;/** Specifies minimum required key size to access this characteristic. */uint8_t min_key_size;/*** At registration time, this is filled in with the characteristic's value* attribute handle.*/uint16_t *val_handle;/** Client Presentation Format Descriptors */struct ble_gatt_cpfd *cpfd;
};
- uuid:本特征对应的UUID;
- access_cb:回调函数,当子节点访问该特性并进行操作时会调用该回调;
- arg:回调函数的用户参数;
- descriptors:当前特性的描述符列表;
- flags:访问权限,常用的有:BLE_GATT_CHR_F_READ(可读)、BLE_GATT_CHR_F_WRITE(可写)、BLE_GATT_CHR_F_NOTIFY(可通知)、BLE_GATT_CHR_F_INDICATE(可指示);
- min_key_size:访问该特性的最小键值;
- val_handle:特征句柄;
- cpfd:特征描述符。
看到服务的回调函数,参数一为连接句柄,当子节点成功连接时会分配一个句柄,用于识别子节点;参数二为特征句柄,每个特征注册时都会分配一个句柄,用于识别特征本身;参数三为数据包;参数四为用户参数。数据包结构体的定义如下:
struct ble_gatt_access_ctxt {/*** Indicates the gatt operation being performed. This is equal to one of* the following values:* o BLE_GATT_ACCESS_OP_READ_CHR* o BLE_GATT_ACCESS_OP_WRITE_CHR* o BLE_GATT_ACCESS_OP_READ_DSC* o BLE_GATT_ACCESS_OP_WRITE_DSC*/uint8_t op;/*** A container for the GATT access data.* o For reads: The application populates this with the value of the* characteristic or descriptor being read.* o For writes: This is already populated with the value being written* by the peer. If the application wishes to retain this mbuf for* later use, the access callback must set this pointer to NULL to* prevent the stack from freeing it.*/struct os_mbuf *om;/*** The GATT operation being performed dictates which field in this union is* valid. If a characteristic is being accessed, the chr field is valid.* Otherwise a descriptor is being accessed, in which case the dsc field* is valid.*/union {/*** The characteristic definition corresponding to the characteristic* being accessed. This is what the app registered at startup.*/const struct ble_gatt_chr_def *chr;/*** The descriptor definition corresponding to the descriptor being* accessed. This is what the app registered at startup.*/const struct ble_gatt_dsc_def *dsc;};
};
- op:操作的类型;
- om:数据包;
- chr:特征定义;
- dsc:描述符定义。
回调函数里面只处理 BLE_GATT_ACCESS_OP_WRITE_CHR(特征写)操作,先打印收到的字符串,再回复客户端。调用 ble_hs_mbuf_from_flat 去申请一个数据包并赋值,然后调用 ble_gatts_notify_custom 去发送一个特征通知,如果子节点订阅了该特征那么就会收到这个回复。
2.2.2 运行
编译并烧录程序运行,打开手机上的调试APP,搜索附近的蓝牙节点,找到我们ESP32的蓝牙并连接。
连接后可以查看它的服务信息。最后一个Unknown Service就是我们创建的串口服务,服务对应有一个Unknown Characteristic。
点击右侧这个向上的箭头就会进入写特征的界面,在下方文本框填写想发送的消息,点击“发送”即可发送到设备。
查看系统log可以看到收到了数据。
2.3 客户端
2.3.1 代码
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <stdbool.h>#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"#include "nvs.h"
#include "nvs_flash.h"
#include "esp_system.h"
#include "esp_log.h"#include "host/ble_gap.h"
#include "host/ble_hs.h"
#include "host/ble_gatt.h"
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"#define TAG "app"#define OWN_NAME "ESP32"
#define SERVER_NAME "OPPO Find X8"
#define SERVER_SERVICE_UUID 0xFFF0
#define SERVER_READ_UUID 0xFFF1
#define SERVER_WRITE_UUID 0xFFF2#define BDASTR "%02X:%02X:%02X:%02X:%02X:%02X"
#define BDA2STR(x) (x)[5], (x)[4], (x)[3], (x)[2], (x)[1], (x)[0]static bool connected = false;
static ble_addr_t server_addr;
static uint16_t conn_handle;
static uint16_t read_handle;
static uint16_t write_handle;
static uint8_t own_addr_type;
static TaskHandle_t write_task_handle;
static EventGroupHandle_t event_group;
static struct ble_gatt_svc* services;
static uint8_t svc_num = 0;static int ble_gap_event(struct ble_gap_event *event, void *arg);
static int ble_gatt_disc_svc_func(uint16_t conn_handle, const struct ble_gatt_error *error, const struct ble_gatt_svc *service, void *arg);
static int ble_gatt_chr_func(uint16_t conn_handle, const struct ble_gatt_error *error, const struct ble_gatt_chr *chr, void *arg);
static void ble_scan(void);
static void ble_on_reset(int reason);
static void ble_on_sync(void);
static void blecent_host_task(void *param);
static void ble_spp_write_task(void* args);
static const char* print_uuid(const ble_uuid_any_t* uuid);static int ble_gap_event(struct ble_gap_event *event, void *arg)
{int rc = 0;switch (event->type) {/* 设备发现事件 */case BLE_GAP_EVENT_DISC:{ESP_LOGI(TAG, "[" BDASTR "] type: %d, data_len: %d, rssi: %d", BDA2STR(event->disc.addr.val), event->disc.event_type, event->disc.length_data, event->disc.rssi);/* 查找目标主机 */struct ble_hs_adv_fields fields;rc = ble_hs_adv_parse_fields(&fields, event->disc.data, event->disc.length_data);if (rc) {break;}if (fields.name_is_complete) {ESP_LOGI(TAG, "device name: %s", (const char*) fields.name);}if (fields.name_is_complete && strstr((const char*) fields.name, SERVER_NAME)) {/* 取消设备发现任务 */ble_gap_disc_cancel();/* 获取地址类型 */rc = ble_hs_id_infer_auto(0, &own_addr_type);if (rc != 0) {ESP_LOGE(TAG, "error determining address type; rc=%d", rc);ble_scan();break;}/* 连接服务器 */rc = ble_gap_connect(own_addr_type, &event->disc.addr, 5000, NULL, ble_gap_event, NULL);if (rc != 0) {ESP_LOGE(TAG, "Error: Failed to connect to device; addr_type=%d addr=" BDASTR "; rc=%d", event->disc.addr.type, BDA2STR(event->disc.addr.val), rc);ble_scan();break;}}break;}/* 设备发现完成 */case BLE_GAP_EVENT_DISC_COMPLETE:ESP_LOGI(TAG, "discovery complete; reason=%d\n", event->disc_complete.reason);if (!connected) {ble_scan();}break;/* 连接建立事件 */case BLE_GAP_EVENT_LINK_ESTAB:if (event->connect.status == 0) {/* 获取连接参数 */struct ble_gap_conn_desc desc = {0};rc = ble_gap_conn_find(event->connect.conn_handle, &desc);if (rc) {ESP_LOGE(TAG, "get server connect descriptor failed, rc=%d", rc);break;}connected = true;conn_handle = event->connect.conn_handle;memcpy(&server_addr, &desc.peer_id_addr, sizeof(ble_addr_t));ESP_LOGI(TAG, "connected to server, addr: " BDASTR ", handle: %d", BDA2STR(desc.peer_id_addr.val), conn_handle);/* 使能服务获取任务 */services = calloc(16, sizeof(struct ble_gatt_svc));rc = ble_gattc_disc_all_svcs(conn_handle, ble_gatt_disc_svc_func, NULL);if (rc) {ESP_LOGE(TAG, "acquire server service failed, rc=%d", rc);break;}/* 创建任务 */event_group = xEventGroupCreate();xTaskCreate(ble_spp_write_task, "ble_spp_write_task", 4096, NULL, 5, &write_task_handle);} else {ESP_LOGE(TAG, "Error: Connection failed; status=%d", event->connect.status);ble_scan();}break;/* 连接断开事件 */case BLE_GAP_EVENT_DISCONNECT:ESP_LOGI(TAG, "disconnect; reason=%d, peer_addr: " BDASTR, event->disconnect.reason, BDA2STR(event->disconnect.conn.peer_id_addr.val));/* 删除任务 */if (write_task_handle) {vTaskDelete(write_task_handle);}/* 清除连接参数 */memset(&server_addr, 0, sizeof(ble_addr_t));conn_handle = 0;read_handle = 0;write_handle = 0;connected = false;/* 重启扫描 */ble_scan();break;/* 通知接收事件 */case BLE_GAP_EVENT_NOTIFY_RX:ESP_LOGI(TAG, "received %s; conn_handle=%d attr_handle=%d attr_len=%d",event->notify_rx.indication ?"indication" :"notification",event->notify_rx.conn_handle,event->notify_rx.attr_handle,OS_MBUF_PKTLEN(event->notify_rx.om));ESP_LOGI(TAG, "data:");ESP_LOGI(TAG, "%s", event->notify_rx.om->om_data);break;default:ESP_LOGI(TAG, "gap event: %d", event->type);break;}return rc;
}static int ble_gatt_disc_svc_func(uint16_t conn_handle, const struct ble_gatt_error *error, const struct ble_gatt_svc *service, void *arg)
{int ret = 0;switch (error->status){case 0:/* 获取属性 */ESP_LOGI(TAG, "[service] start_handle=%d, end_handle=%d, uuid=%s",service->start_handle,service->end_handle,print_uuid(&service->uuid));memcpy(&services[svc_num++], service, sizeof(struct ble_gatt_svc));break;case BLE_HS_EDONE:/* 获取目标服务特征 */for (uint8_t i = 0; i < svc_num; i++) {if (services[i].uuid.u16.value == SERVER_SERVICE_UUID) {ret = ble_gattc_disc_all_chrs(conn_handle, services[i].start_handle, services[i].end_handle, ble_gatt_chr_func, NULL);if (ret) {ESP_LOGE(TAG, "discover all chrs failed, rc=%d", ret);}break;}}break;default:break;}return 0;
}static int ble_gatt_chr_func(uint16_t conn_handle, const struct ble_gatt_error *error, const struct ble_gatt_chr *chr, void *arg)
{switch (error->status){case 0:ESP_LOGI(TAG, "[characteristic] val_handle=%d, uuid=%s, prop=%02X",chr->val_handle,print_uuid(&chr->uuid),chr->properties);/* 保存特征参数 */if (chr->uuid.u16.value == SERVER_READ_UUID) {read_handle = chr->val_handle;ESP_LOGI(TAG, "get server read chr, handle=%d", read_handle);xEventGroupSetBits(event_group, 0x01);} else if (chr->uuid.u16.value == SERVER_WRITE_UUID) {write_handle = chr->val_handle;ESP_LOGI(TAG, "get server write chr, handle=%d", write_handle);xEventGroupSetBits(event_group, 0x02);}break;default:break;}return 0;
}static void ble_scan(void)
{int rc;/* 获取地址类型 */rc = ble_hs_id_infer_auto(0, &own_addr_type);if (rc != 0) {ESP_LOGE(TAG, "error determining address type; rc=%d\n", rc);return;}struct ble_gap_disc_params disc_params;disc_params.filter_duplicates = 1;disc_params.passive = 0;disc_params.itvl = 0;disc_params.window = 0;disc_params.filter_policy = 0;disc_params.limited = 0;rc = ble_gap_disc(own_addr_type, 5000, &disc_params, ble_gap_event, NULL);if (rc != 0) {ESP_LOGE(TAG, "Error initiating GAP discovery procedure; rc=%d\n", rc);}
}static void ble_on_reset(int reason)
{ESP_LOGE(TAG, "Resetting state; reason=%d\n", reason);
}static void ble_on_sync(void)
{/* 配置地址 */if (ble_hs_util_ensure_addr(0) != 0) {return;}/* 启动扫描 */ble_scan();
}static void blecent_host_task(void *param)
{nimble_port_run();nimble_port_freertos_deinit();
}static void ble_spp_write_task(void* args)
{int ret = 0;const char* message = "Hello from ESP32";/* 等待服务发现完成 */xEventGroupWaitBits(event_group, 0x03, pdTRUE, pdTRUE, portMAX_DELAY);if (services) {free(services);services = NULL;}vEventGroupDelete(event_group);while (1) {/* 发送消息 */ret = ble_gattc_write_flat(conn_handle, write_handle, message, strlen(message), NULL, NULL);if (ret) {ESP_LOGE(TAG, "write message to server failed, ret=%d", ret);}vTaskDelay(2000 / portTICK_PERIOD_MS);}
}static const char* print_uuid(const ble_uuid_any_t* uuid)
{static char uuid_str[64] = {0};memset(uuid_str, 0, sizeof(uuid_str));if (uuid->u.type == BLE_UUID_TYPE_16) {ble_uuid16_t* uuid16 = (ble_uuid16_t*) uuid;snprintf(uuid_str, 64, "%04X", uuid16->value);} else if (uuid->u.type == BLE_UUID_TYPE_32) {ble_uuid32_t* uuid32 = (ble_uuid32_t*) uuid;snprintf(uuid_str, 64, "%08lX", uuid32->value);} else if (uuid->u.type == BLE_UUID_TYPE_128) {ble_uuid128_t* uuid128 = (ble_uuid128_t*) uuid;for (int i = 0; i < 16; i++) {sprintf(uuid_str + i * 2, "%02X", uuid128->value[i]);}}return uuid_str;
}int app_main()
{/* 初始化NVS */esp_err_t ret = nvs_flash_init();if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {ESP_ERROR_CHECK(nvs_flash_erase());ESP_ERROR_CHECK(nvs_flash_init());}/* 初始化控制器和NimBLE协议栈 */ret = nimble_port_init();if (ret != ESP_OK) {ESP_LOGE(TAG, "Failed to init nimble %d ", ret);return -1;}/* 配置协议栈 */ble_hs_cfg.reset_cb = ble_on_reset;ble_hs_cfg.sync_cb = ble_on_sync;ble_hs_cfg.store_status_cb = ble_store_util_status_rr;/* 设置设备名 */if (ble_svc_gap_device_name_set(OWN_NAME) != 0) {ESP_LOGE(TAG, "set device name failed");return -1;}/* 使能NimBLE协议栈 */nimble_port_freertos_init(blecent_host_task);return 0;
}
客户端重点讲GAP协议的回调。
设备发现事件(BLE_GAP_EVENT_DISC):
客户端是需要主动扫描服务端然后继续连接的,每扫描到一个服务端的广播包,就会触发一次这个事件,调用 ble_hs_adv_parse_fields 函数可以解析广播包。我这里是通过查看设备名来确认目标服务端的。确认服务端后可以调用 ble_gap_connect 来建立连接。
连接建立事件(BLE_GAP_EVENT_LINK_ESTAB):
当与服务端成功连接后会触发该事件,回调中会返回一个连接句柄,一定要保存好,后面所有的通信都需要。调用 ble_gap_conn_find 可以获取完整的连接参数。
一个BLE的服务端会包含很多的服务,每个服务下面也有很多的属性,所以我们要确认每一个业务需求对应哪一个具体的属性,然后使用对应属性的句柄进行通信。调用 ble_gattc_disc_all_svcs 去遍历服务端的所有服务,这个函数是非阻塞的,每发现一个服务就会调用一次回调。
我这里是先把所有的服务参数保存下来,当所有的服务都遍历完了去寻找目标服务,即UUID为0xFFF0的服务,然后调用 ble_gattc_disc_all_chrs 去遍历这个服务的所有属性,这个函数也是非阻塞的。也是通过UUID找到对应的属性,然后保存它们的句柄。
连接断开事件(BLE_GAP_EVENT_DISCONNECT):
当连接断开时触发,我这里的操作是删除任务、清除连接参数,然后重新开始扫描。
在连接建立的时候程序就已经创建了业务任务,但这时候还没有完成属性句柄的匹配,所以用了一个EventGroup去等待。发送消息可以调用 ble_gattc_write_flat ,传入前面保存好的句柄和要发送的数据即可,回调函数可以不需要。
2.3.2 运行
编译并烧录程序,ESP32会不断扫描寻找目标服务端。然后打开手机上的调试APP,选择服务端模式,开启广播;过一会ESP32就会成功连接到,下面的接收框就可以看到收到了ESP32发来的字符串消息了。