一文迅速上手 ESP32 bluedroid 蓝牙从机开发

2024-08-25 18:52

本文主要是介绍一文迅速上手 ESP32 bluedroid 蓝牙从机开发,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

  1. 个人邮箱:zhangyixu02@gmail.com
  2. 该博客主要针对希望迅速上手 ESP32 蓝牙从机开发人员,因此,很多蓝牙技术细节知识并不会进行介绍,仅仅介绍我认为需要了解的 API 函数和回调内容。
  3. 本文主要是基于gatt_server demo来微调进行进行讲解。

代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_bt.h"#include "esp_gap_ble_api.h"
#include "esp_gatts_api.h"
#include "esp_bt_defs.h"
#include "esp_bt_main.h"
#include "esp_gatt_common_api.h"#include "sdkconfig.h"#define GATTS_TAG "GATTS_DEMO"///Declare the static function
static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
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);#define GATTS_SERVICE_UUID_TEST_A   0x00FF
#define GATTS_CHAR_UUID_TEST_A      0xFF01
#define GATTS_DESCR_UUID_TEST_A     0x3333
#define GATTS_NUM_HANDLE_TEST_A     4#define GATTS_SERVICE_UUID_TEST_B   0x00EE
#define GATTS_CHAR_UUID_TEST_B      0xEE01
#define GATTS_DESCR_UUID_TEST_B     0x2222
#define GATTS_NUM_HANDLE_TEST_B     4/* 广播设备名称,如果需要使用中文,则需要中文转 URL 编码。可以使用 https://tool.chinaz.com/tools/urlencode.aspx 进行转换 */
// #define TEST_DEVICE_NAME            "ESP_GATTS_DEMO"
const char TEST_DEVICE_NAME[] = {0xE9,0xA3,0x8E,0xE6,0xAD,0xA3,0xE8,0xB1,0xAA};
#define TEST_MANUFACTURER_DATA_LEN  17#define GATTS_DEMO_CHAR_VAL_LEN_MAX 0x40#define PREPARE_BUF_MAX_SIZE 1024static uint8_t char1_str[] = {0x11,0x22,0x33};
static esp_gatt_char_prop_t a_property = 0;
static esp_gatt_char_prop_t b_property = 0;static 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,
};#define adv_config_flag      (1 << 0)
#define scan_rsp_config_flag (1 << 1)#ifdef CONFIG_SET_RAW_ADV_DATA
static uint8_t raw_adv_data[] = {0x02, 0x01, 0x06,                  // Length 2, Data Type 1 (Flags), Data 1 (LE General Discoverable Mode, BR/EDR Not Supported)0x02, 0x0a, 0xeb,                  // Length 2, Data Type 10 (TX power leve), Data 2 (-21)0x03, 0x03, 0xab, 0xcd,            // Length 3, Data Type 3 (Complete 16-bit Service UUIDs), Data 3 (UUID)
};
static uint8_t raw_scan_rsp_data[] = {     // Length 15, Data Type 9 (Complete Local Name), Data 1 (ESP_GATTS_DEMO)0x0f, 0x09, 0x45, 0x53, 0x50, 0x5f, 0x47, 0x41, 0x54, 0x54, 0x53, 0x5f, 0x44,0x45, 0x4d, 0x4f
};
#elsestatic uint8_t adv_service_uuid128[32] = {/* LSB <--------------------------------------------------------------------------------> MSB *///first uuid, 16bit, [12],[13] is the value0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xEE, 0x00, 0x00, 0x00,//second uuid, 32bit, [12], [13], [14], [15] is the value0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00,
};// 广播报数据长度应当小于 31 bytes
//static uint8_t test_manufacturer[TEST_MANUFACTURER_DATA_LEN] =  {0x12, 0x23, 0x45, 0x56};
//adv data
static esp_ble_adv_data_t adv_data = {.set_scan_rsp = false,.include_name = true,.include_txpower = false,.min_interval = 0x0006, //slave connection min interval, Time = min_interval * 1.25 msec.max_interval = 0x0010, //slave connection max interval, Time = max_interval * 1.25 msec.appearance = ESP_BLE_APPEARANCE_SPORTS_WATCH,.manufacturer_len = 0, //TEST_MANUFACTURER_DATA_LEN,.p_manufacturer_data =  NULL, //&test_manufacturer[0],.service_data_len = 0,.p_service_data = NULL,.service_uuid_len = sizeof(adv_service_uuid128),.p_service_uuid = adv_service_uuid128,.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};
// scan response data
static esp_ble_adv_data_t scan_rsp_data = {.set_scan_rsp = true,.include_name = true,.include_txpower = true,//.min_interval = 0x0006,//.max_interval = 0x0010,// .appearance = 0x00,.manufacturer_len = 0, //TEST_MANUFACTURER_DATA_LEN,.p_manufacturer_data =  NULL, //&test_manufacturer[0],.service_data_len = 0,.p_service_data = NULL,.service_uuid_len = sizeof(adv_service_uuid128),.p_service_uuid = adv_service_uuid128,.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT),
};#endif /* CONFIG_SET_RAW_ADV_DATA */static esp_ble_adv_params_t adv_params = {.adv_int_min        = 0x20,.adv_int_max        = 0x40,.adv_type           = ADV_TYPE_IND,.own_addr_type      = BLE_ADDR_TYPE_PUBLIC,//.peer_addr            =//.peer_addr_type       =.channel_map        = ADV_CHNL_ALL,.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};enum {PROFILE_A,PROFILE_B,PROFILE_NUM
};/* 这个注册 APP ID 可为 0~65535 任意值,只要 APP ID 没有重复即可 */
#define PROFILE_A_APP_ID 0x55
#define PROFILE_B_APP_ID 0x66struct gatts_profile_inst {esp_gatts_cb_t gatts_cb;         /* GATT 回调函数 */uint16_t gatts_if;               /* 协议栈分配的,用于指定服务的标识符 */uint16_t app_id;                 /* 注册的 APP ID,可为 0~65535 任意值,但不可重复 */uint16_t conn_id;                /* 用于表示一个设备连接,因为 ESP32 作为从机可以被多个主机同时连接,那么就需要利用这个参数指定是那个主机连接 */uint16_t service_handle;         /* 服务(Service)句柄 */esp_gatt_srvc_id_t service_id;   /* 首要服务的一些参数 */uint16_t char_handle;            /* 特征(Characteristic)句柄 */esp_bt_uuid_t char_uuid;         /* 特征(Characteristic) UUID 值 */esp_gatt_perm_t perm;esp_gatt_char_prop_t property;uint16_t descr_handle;           /* 特征配置描述符(descriptor)句柄 */esp_bt_uuid_t descr_uuid;        /* UUID 长度 */
};/* 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   = PROFILE_A_APP_ID,.gatts_cb = gatts_profile_a_event_handler,.gatts_if = ESP_GATT_IF_NONE,},[PROFILE_B] = {.app_id   = PROFILE_B_APP_ID,.gatts_cb = gatts_profile_b_event_handler,    /* 配置 B 服务的回调函数 */.gatts_if = ESP_GATT_IF_NONE,                 /* 如果协议栈没有分配 gatts_if ,那么就是 ESP_GATT_IF_NONE */},
};typedef struct {uint8_t                 *prepare_buf;int                     prepare_len;
} prepare_type_env_t;static prepare_type_env_t a_prepare_write_env; /* 存储 A 服务准备写操作内容 */
static prepare_type_env_t b_prepare_write_env; /* 存储 B 服务准备写操作内容 */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);
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param);static void advertise_init(void)
{esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(TEST_DEVICE_NAME);if (set_dev_name_ret){/* 设置设备名称失败 */ESP_LOGE(GATTS_TAG, "set device name failed, error code = %x", set_dev_name_ret);}
#ifdef CONFIG_SET_RAW_ADV_DATA// 设置广播数据esp_err_t raw_adv_ret = esp_ble_gap_config_adv_data_raw(raw_adv_data, sizeof(raw_adv_data));if (raw_adv_ret){ESP_LOGE(GATTS_TAG, "config raw adv data failed, error code = %x ", raw_adv_ret);}// 设置扫描响应数据esp_err_t raw_scan_ret = esp_ble_gap_config_scan_rsp_data_raw(raw_scan_rsp_data, sizeof(raw_scan_rsp_data));if (raw_scan_ret){ESP_LOGE(GATTS_TAG, "config raw scan rsp data failed, error code = %x", raw_scan_ret);}
#else// 设置广播数据esp_err_t ret = esp_ble_gap_config_adv_data(&adv_data);if (ret){ESP_LOGE(GATTS_TAG, "config adv data failed, error code = %x", ret);}// 设置扫描响应数据ret = esp_ble_gap_config_adv_data(&scan_rsp_data);if (ret){ESP_LOGE(GATTS_TAG, "config scan response data failed, error code = %x", ret);}
#endif
}static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{ESP_LOGI(GATTS_TAG, "GAP Event:%d", event);switch (event) {
#ifdef CONFIG_SET_RAW_ADV_DATAcase ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: {if (param->adv_data_raw_cmpl.status != ESP_BT_STATUS_SUCCESS) {ESP_LOGE(GATTS_TAG, "advertising data set failed");}break;}case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT: {if (param->scan_rsp_data_raw_cmpl.status != ESP_BT_STATUS_SUCCESS) {ESP_LOGE(GATTS_TAG, "advertising data set failed");}esp_ble_gap_start_advertising(&adv_params);break;}
#elsecase ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: {if (param->adv_data_cmpl.status != ESP_BT_STATUS_SUCCESS) {ESP_LOGE(GATTS_TAG, "advertising data set failed");}break;}case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: {if (param->scan_rsp_data_cmpl.status != ESP_BT_STATUS_SUCCESS) {ESP_LOGE(GATTS_TAG, "advertising data set failed");}esp_ble_gap_start_advertising(&adv_params);break;}
#endifcase ESP_GAP_BLE_ADV_START_COMPLETE_EVT: {//advertising start complete event to indicate advertising start successfully or failedif (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {ESP_LOGE(GATTS_TAG, "Advertising start failed");}break;}case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: {if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {ESP_LOGE(GATTS_TAG, "Advertising stop failed");} else {ESP_LOGI(GATTS_TAG, "Stop adv successfully");}break;}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;}case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: {ESP_LOGI(GATTS_TAG, "packet length updated: rx = %d, tx = %d, status = %d",param->pkt_data_length_cmpl.params.rx_len,param->pkt_data_length_cmpl.params.tx_len,param->pkt_data_length_cmpl.status);break;}default:break;}
}/*** @brief 处理写事件的函数,用于处理普通写入和准备写入** @param gatts_if GATT 接口,用于标识 GATT 服务端* @param prepare_write_env 指向准备写环境结构体的指针,保存准备写的数据* @param param GATT 回调参数,包括写事件的信息*/
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) {/* 判断是否为准备写操作,注意 : 准备写入(Prepare Write)操作是必须有响应 */if (param->write.is_prep) {/* 检查写入偏移量是否超过缓冲区最大大小 */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;}/* 如果状态正常且准备缓冲区为空,则分配缓冲区内存 */if (status == ESP_GATT_OK && 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) {/* malloc 申请分配内存失败 */ESP_LOGE(GATTS_TAG, "Gatt_server prep no mem");status = ESP_GATT_NO_RESOURCES;}}esp_gatt_rsp_t *gatt_rsp = (esp_gatt_rsp_t *)malloc(sizeof(esp_gatt_rsp_t));if (gatt_rsp) {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);} else {/* 申请 esp_gatt_rsp_t 结构体内存数据失败 */ESP_LOGE(GATTS_TAG, "malloc failed, no resource to send response error\n");status = ESP_GATT_NO_RESOURCES;}if (status != ESP_GATT_OK){return;}/* 将写入的数据复制到 a_prepare_write_env 或者 b_prepare_write_env 准备缓冲区 */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_LOGI(GATTS_TAG, "write event, but not prepare");/* 对于普通写入,直接发送响应。注意,该函数必须有,否则会引起客户端(client)断连 */esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL);}} else{/* 如果不需要响应,则直接返回 */ESP_LOGI(GATTS_TAG, "write event, but not need to response");return;}
}/*** @brief 处理 GATT 服务端的执行写操作事件。* * @param prepare_write_env 准备写操作的环境参数,包括缓冲区和长度。* @param param 事件回调参数,其中包含执行写操作的标志和其他信息。*/
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;
}static void gatts_profile_a_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", param->reg.status, param->reg.app_id);/* 创造 GATTS 首要服务 */gl_profile_tab[PROFILE_A].service_id.is_primary = true;gl_profile_tab[PROFILE_A].service_id.id.inst_id = 0x00;gl_profile_tab[PROFILE_A].service_id.id.uuid.len = ESP_UUID_LEN_16;gl_profile_tab[PROFILE_A].service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_A;esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_A].service_id, GATTS_NUM_HANDLE_TEST_A);break;}case ESP_GATTS_READ_EVT: {ESP_LOGI(GATTS_TAG, "GATT_READ_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->read.conn_id, param->read.trans_id, param->read.handle);/* 读取 esp_ble_gatts_add_char 初始化时候的 char_val */uint16_t length = 0;const uint8_t *prf_char;esp_gatt_rsp_t rsp;memset(&rsp, 0, sizeof(esp_gatt_rsp_t));/* 获取特征值  */esp_err_t get_attr_ret = esp_ble_gatts_get_attr_value(param->read.handle,  &length, &prf_char);if (get_attr_ret == ESP_FAIL){ESP_LOGE(GATTS_TAG, "ILLEGAL HANDLE");}rsp.attr_value.handle = param->read.handle;rsp.attr_value.len = length;memcpy(rsp.attr_value.value, prf_char, length);for(int i = 0; i < length; i++){ESP_LOGD(GATTS_TAG, "ESP_GATTS_READ_EVT : prf_char[%x] =%x",i,prf_char[i]);ESP_LOGD(GATTS_TAG, "ESP_GATTS_READ_EVT : i,rsp.attr_value.value[%x] =%x",i,rsp.attr_value.value[i]);}esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id,ESP_GATT_OK, &rsp);// 更新特征的值rsp.attr_value.value[0] = 0x55;rsp.attr_value.value[1] = 0x66;rsp.attr_value.value[2] = 0x77;esp_ble_gatts_set_attr_value(rsp.attr_value.handle, 3, rsp.attr_value.value);break;}case ESP_GATTS_WRITE_EVT: {ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);if (!param->write.is_prep) {/* 打印客户端(client)写入的数据长度和值 */ESP_LOGI(GATTS_TAG, "It's not prepare write, value len %d, value", param->write.len);esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);/* 如果是 特征配置描述符(descriptor)操作句柄,并且写入数据长度为2 */if (gl_profile_tab[PROFILE_A].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 (a_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 sizeesp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_A].char_handle,sizeof(notify_data), notify_data, false);}} else if (descr_value == 0x0002){/*  */if (a_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 sizeesp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_A].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 descr value");esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);}}}example_write_event_env(gatts_if, &a_prepare_write_env, param);break;}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;}case ESP_GATTS_MTU_EVT: {ESP_LOGI(GATTS_TAG, "ESP_GATTS_MTU_EVT, MTU %d", param->mtu.mtu);break;}case ESP_GATTS_CREATE_EVT: {ESP_LOGI(GATTS_TAG, "CREATE_SERVICE_EVT, status %d,  service_handle %d", param->create.status, param->create.service_handle);/* 添加 特征申明 和 特征值 */gl_profile_tab[PROFILE_A].service_handle = param->create.service_handle;gl_profile_tab[PROFILE_A].char_uuid.len = ESP_UUID_LEN_16;gl_profile_tab[PROFILE_A].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_TEST_A;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].service_handle, &gl_profile_tab[PROFILE_A].char_uuid,ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,a_property,&gatts_demo_char1_val, NULL);/* 启动服务 */esp_ble_gatts_start_service(gl_profile_tab[PROFILE_A].service_handle);if (add_char_ret){ESP_LOGE(GATTS_TAG, "add char failed, error code =%x",add_char_ret);}break;}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",param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);/* 添加 特征配置描述符 */gl_profile_tab[PROFILE_A].char_handle = param->add_char.attr_handle;gl_profile_tab[PROFILE_A].descr_uuid.len = ESP_UUID_LEN_16;gl_profile_tab[PROFILE_A].descr_uuid.uuid.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;esp_err_t add_descr_ret = esp_ble_gatts_add_char_descr(gl_profile_tab[PROFILE_A].service_handle, &gl_profile_tab[PROFILE_A].descr_uuid,ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE, NULL, NULL);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", length);for(int i = 0; i < length; i++){ESP_LOGI(GATTS_TAG, "prf_char[%x] =%x",i,prf_char[i]);}if (add_descr_ret){ESP_LOGE(GATTS_TAG, "add char descr failed, error code =%x", add_descr_ret);}break;}case ESP_GATTS_ADD_CHAR_DESCR_EVT: {gl_profile_tab[PROFILE_A].descr_handle = param->add_char_descr.attr_handle;ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d",param->add_char_descr.status, param->add_char_descr.attr_handle, param->add_char_descr.service_handle);break;}case ESP_GATTS_START_EVT: {ESP_LOGI(GATTS_TAG, "SERVICE_START_EVT, status %d, service_handle %d",param->start.status, param->start.service_handle);break;}case ESP_GATTS_CONNECT_EVT: {ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:",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]);gl_profile_tab[PROFILE_A].conn_id = param->connect.conn_id;//向对端设备发起连接参数更新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 = 0x20;    // max_int = 0x20*1.25ms = 40msconn_params.min_int = 0x10;    // min_int = 0x10*1.25ms = 20msconn_params.timeout = 400;    // timeout = 400*10ms = 4000msesp_ble_gap_update_conn_params(&conn_params);break;}case ESP_GATTS_DISCONNECT_EVT: {ESP_LOGI(GATTS_TAG, "ESP_GATTS_DISCONNECT_EVT, disconnect reason 0x%x", param->disconnect.reason);break;}case ESP_GATTS_CONF_EVT: {ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONF_EVT, status %d attr_handle %d", param->conf.status, param->conf.handle);if (param->conf.status != ESP_GATT_OK){esp_log_buffer_hex(GATTS_TAG, param->conf.value, param->conf.len);}break;}default:break;}
}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", param->reg.status, param->reg.app_id);/* 创造 GATTS 首要服务 */gl_profile_tab[PROFILE_B].service_id.is_primary = true;gl_profile_tab[PROFILE_B].service_id.id.inst_id = 0x00;gl_profile_tab[PROFILE_B].service_id.id.uuid.len = ESP_UUID_LEN_16;gl_profile_tab[PROFILE_B].service_id.id.uuid.uuid.uuid16 = GATTS_SERVICE_UUID_TEST_B;esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_B].service_id, GATTS_NUM_HANDLE_TEST_B);break;}case ESP_GATTS_READ_EVT: {ESP_LOGI(GATTS_TAG, "GATT_READ_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", 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;}case ESP_GATTS_WRITE_EVT: {ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %" PRIu32 ", handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);if (!param->write.is_prep) {ESP_LOGI(GATTS_TAG, "It's not prepare write, value len %d, value :", param->write.len);esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len);/* 如果是 特征配置描述符(descriptor)操作句柄,并且写入数据长度为2 */if (gl_profile_tab[PROFILE_B].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, "profile b 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 sizeesp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_B].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 sizeesp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, gl_profile_tab[PROFILE_B].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, &b_prepare_write_env, param);break;}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(&b_prepare_write_env, param);break;}case ESP_GATTS_MTU_EVT: {ESP_LOGI(GATTS_TAG, "ESP_GATTS_MTU_EVT, MTU %d", param->mtu.mtu);break;}case ESP_GATTS_CREATE_EVT: {ESP_LOGI(GATTS_TAG, "CREATE_SERVICE_EVT, status %d,  service_handle %d", param->create.status, param->create.service_handle);/* 创建特征 */gl_profile_tab[PROFILE_B].service_handle = param->create.service_handle;gl_profile_tab[PROFILE_B].char_uuid.len = ESP_UUID_LEN_16;gl_profile_tab[PROFILE_B].char_uuid.uuid.uuid16 = GATTS_CHAR_UUID_TEST_B;b_property = ESP_GATT_CHAR_PROP_BIT_READ | ESP_GATT_CHAR_PROP_BIT_WRITE | ESP_GATT_CHAR_PROP_BIT_NOTIFY | ESP_GATT_CHAR_PROP_BIT_INDICATE;esp_err_t add_char_ret =esp_ble_gatts_add_char( gl_profile_tab[PROFILE_B].service_handle, &gl_profile_tab[PROFILE_B].char_uuid,ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,b_property,NULL, NULL);esp_ble_gatts_start_service(gl_profile_tab[PROFILE_B].service_handle);if (add_char_ret){ESP_LOGE(GATTS_TAG, "add char failed, error code =%x",add_char_ret);}break;}case ESP_GATTS_ADD_CHAR_EVT: {ESP_LOGI(GATTS_TAG, "ADD_CHAR_EVT, status %d,  attr_handle %d, service_handle %d",param->add_char.status, param->add_char.attr_handle, param->add_char.service_handle);/* 添加 特征配置描述符 */gl_profile_tab[PROFILE_B].char_handle = param->add_char.attr_handle;gl_profile_tab[PROFILE_B].descr_uuid.len = ESP_UUID_LEN_16;gl_profile_tab[PROFILE_B].descr_uuid.uuid.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG;esp_ble_gatts_add_char_descr(gl_profile_tab[PROFILE_B].service_handle, &gl_profile_tab[PROFILE_B].descr_uuid,ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,NULL, NULL);break;}case ESP_GATTS_ADD_CHAR_DESCR_EVT: {gl_profile_tab[PROFILE_B].descr_handle = param->add_char_descr.attr_handle;ESP_LOGI(GATTS_TAG, "ADD_DESCR_EVT, status %d, attr_handle %d, service_handle %d",param->add_char_descr.status, param->add_char_descr.attr_handle, param->add_char_descr.service_handle);break;}case ESP_GATTS_START_EVT: {ESP_LOGI(GATTS_TAG, "SERVICE_START_EVT, status %d, service_handle %d",param->start.status, param->start.service_handle);break;}case ESP_GATTS_CONNECT_EVT: {ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONNECT_EVT, conn_id %d, remote %02x:%02x:%02x:%02x:%02x:%02x:",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]);gl_profile_tab[PROFILE_B].conn_id = param->connect.conn_id;break;}case ESP_GATTS_CONF_EVT: {ESP_LOGI(GATTS_TAG, "ESP_GATTS_CONF_EVT status %d attr_handle %d", param->conf.status, param->conf.handle);if (param->conf.status != ESP_GATT_OK){esp_log_buffer_hex(GATTS_TAG, param->conf.value, param->conf.len);}break;}case ESP_GATTS_DISCONNECT_EVT: {ESP_LOGI(GATTS_TAG, "ESP_GATTS_DISCONNECT_EVT, disconnect reason 0x%x", param->disconnect.reason);}default:break;}
}static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
{ESP_LOGI(GATTS_TAG, "GATTS event = %d", event);int idx;/* 如果事件是注册事件,则存储每个概要文件的 gatts_if,该事件由 esp_ble_gatts_app_register() 函数触发 */if (event == ESP_GATTS_REG_EVT) {// 依次遍历 GATT profilefor (idx = 0; idx < PROFILE_NUM; idx++) {// 找到对应的注册的 app_id ,如果 GATT 处于正常运行,那么将 GATT 接口 gatts_if 进行存储if (param->reg.app_id == gl_profile_tab[idx].app_id && param->reg.status == ESP_GATT_OK) {/* 将协议栈分配的 gatts_if 存储进对应的结构体中 */gl_profile_tab[idx].gatts_if = gatts_if;ESP_LOGI(GATTS_TAG,"gatts_if = %d,param->reg.app_id = 0x%x",gatts_if,param->reg.app_id);}}}// 触发 GATT 回调事件,如下处理为判断是那个 GATT profile 事件,并作出响应的处理for (idx = 0; idx < PROFILE_NUM; idx++) {/* 如果 gatts_if 为 ESP_GATT_IF_NONE 表示当前事件或操作不对应于任何特定的 GATT 应用程序接口,* 因此所有的 GATT profile都会被调用一次。如果指定了特点的 GATT 应用,则调用对应的应用程序。*/if (gatts_if == ESP_GATT_IF_NONE || 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);}}}
}void app_main(void)
{esp_err_t ret;// 初始化 NVS.ret = nvs_flash_init();/* 如果 NVS 分区没有剩余空间或者 NVS 分区包含新格式数据,当前版本无法识别。*/if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {/* 清空 NVS 分区数据 */ESP_ERROR_CHECK(nvs_flash_erase());/* NVS 分区重新进行初始化 */ret = nvs_flash_init();}ESP_ERROR_CHECK( ret );/* 释放经典蓝牙协议栈空间 */ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));/* 初始化 Control 层 */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", __func__, esp_err_to_name(ret));return;}/* 使能 Control 层 */ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);if (ret) {ESP_LOGE(GATTS_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));return;}/* 初始化 HOST层,Bluedroid 协议栈 */ret = esp_bluedroid_init();if (ret) {ESP_LOGE(GATTS_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));return;}/* 使能 HOST层,Bluedroid 协议栈 */ret = esp_bluedroid_enable();if (ret) {ESP_LOGE(GATTS_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));return;}/* 注册 GATTS 事件回调函数 */ret = esp_ble_gatts_register_callback(gatts_event_handler);if (ret){ESP_LOGE(GATTS_TAG, "gatts register error, error code = %x", ret);return;}/* 注册 GAP 事件回调函数 */ret = esp_ble_gap_register_callback(gap_event_handler);if (ret){ESP_LOGE(GATTS_TAG, "gap register error, error code = %x", ret);return;}/* 注册一个app_id, 协议栈将会分配一个对应的 gatts_if,用于标识一个 GATT 服务。* 调用这个函数就会触发 esp_ble_gatts_register_callback() 注册的回调函数中的 ESP_GATTS_REG_EVT 事件*/ret = esp_ble_gatts_app_register(gl_profile_tab[PROFILE_A].app_id);if (ret){ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);return;}/* 注册一个app_id, 协议栈将会分配一个对应的 gatts_if,用于标识一个 GATT 服务。* 调用这个函数就会触发 esp_ble_gatts_register_callback() 注册的回调函数中的 ESP_GATTS_REG_EVT 事件*/ret = esp_ble_gatts_app_register(gl_profile_tab[PROFILE_B].app_id);if (ret){ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);return;}/* 设置本地 MTU 为 23 字节,实际交换时刻的 MTU 要通过与客户端(client) 协商后得到。* MTU 大小范围为 23 ~ 517 字节之间,如果没有调用该函数,则默认为 23 字节。*/esp_err_t local_mtu_ret = esp_ble_gatt_set_local_mtu(23);if (local_mtu_ret){ESP_LOGE(GATTS_TAG, "set local  MTU failed, error code = %x", local_mtu_ret);}/* 初始化广播参数 */advertise_init();return;
}

app_main

NVS 初始化

  1. 首先我们来看 app_main() 函数。在该函数中,我们先初始化了 NVS 分区。其实这个部分可以不要。他主要用于存储一些 RF(射频)校准数据,以确保无线通信的性能和稳定性
  2. 当 ESP32 第一次启动并运行无线功能(如 Wi-Fi 或蓝牙)时,它会进行 RF 校准,以确定在当前硬件和环境条件下的最佳射频参数。
  3. 校准过程的结果会被存储在 NVS 中,这样在后续启动时,设备可以直接使用这些校准数据,避免每次启动都需要重新校准。从而提高设备运行效率。
    // 初始化 NVS.ret = nvs_flash_init();/* 如果 NVS 分区没有剩余空间或者 NVS 分区包含新格式数据,当前版本无法识别。*/if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {/* 清空 NVS 分区数据 */ESP_ERROR_CHECK(nvs_flash_erase());/* NVS 分区重新进行初始化 */ret = nvs_flash_init();}ESP_ERROR_CHECK( ret );
  1. 如果是 NVS 分区没有 RF(射频)校准数据,那么就会出现如下日志信息,之后就开始 RF 校准,并将相关校准信息存放进 NVS 分区。
W (602) phy_init: failed to load RF calibration data (0x1102), falling back to full calibration
W (642) phy_init: saving new calibration data because of checksum failure, mode(2)
  1. 如果不初始化 NVS 分区,将会出现如下错误。虽然不影响程序的正常运行,但是这样每次芯片启动都需要进行 RF 校准,从而拖慢启动速度
// 将 NVS 初始化注释掉// // 初始化 NVS.// ret = nvs_flash_init();// /* 如果 NVS 分区没有剩余空间或者 NVS 分区包含新格式数据,当前版本无法识别。*/// if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {//     /* 清空 NVS 分区数据 *///     ESP_ERROR_CHECK(nvs_flash_erase());//     /* NVS 分区重新进行初始化 *///     ret = nvs_flash_init();// }// ESP_ERROR_CHECK( ret );
E (599) phy_init: esp_phy_load_cal_data_from_nvs: NVS has not been initialized. Call nvs_flash_init before starting WiFi/BT.
W (619) phy_init: failed to load RF calibration data (0x1101), falling back to full calibration
W (659) phy_init: saving new calibration data because of checksum failure, mode(2)
I (659) phy: libbtbb version: b97859f, Jun  4 2024, 16:44:27
E (669) BT_OSI: config_new: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
W (679) BT_BTC: btc_config_init unable to load config file; starting unconfigured.E (689) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (709) BT_OSI: config_save, err_code: 0x2E (729) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (729) BT_OSI: config_save, err_code: 0x2E (739) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (749) BT_OSI: config_save, err_code: 0x2E (759) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (779) BT_OSI: config_save, err_code: 0x2E (779) BT_OSI: config_save: NVS not initialized. Call nvs_flash_init before initializing bluetooth.
E (799) BT_OSI: config_save, err_code: 0x2

协议栈初始化

  1. 我们需要知道,ESP32 是存在两个存在两个蓝牙协议栈的。
    • Bluedroid(默认) : 支持传统蓝牙(BR/EDR)低功耗蓝牙(BLE)。同时涉及传统蓝牙(BR/EDR)低功耗蓝牙(BLE) 的用例应当使用该协议栈。
    • Apache NimBLE : 仅支持低功耗蓝牙(BLE)。仅涉及低功耗蓝牙(BLE),建议使用该协议栈,因为在代码占用和运行时,NimBLE 对内存的要求较低
  2. 这里我将介绍的是 Bluedroid 协议栈开发。但是需要注意,虽然我们用的是 Bluedroid 协议栈,但实际情况却仅仅用到了 低功耗蓝牙(BLE) 的功能。因为,我们需要释放 传统蓝牙(BR/EDR) 的协议栈。
  3. 对蓝牙开发结构不清楚的朋友,可以看看 BLE学习笔记(0.0) —— 基础概念(0)
    /* 释放经典蓝牙协议栈空间 */ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));/* 初始化 Control 层 */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", __func__, esp_err_to_name(ret));return;}/* 使能 Control 层 */ret = esp_bt_controller_enable(ESP_BT_MODE_BLE);if (ret) {ESP_LOGE(GATTS_TAG, "%s enable controller failed: %s", __func__, esp_err_to_name(ret));return;}/* 初始化 HOST层,Bluedroid 协议栈 */ret = esp_bluedroid_init();if (ret) {ESP_LOGE(GATTS_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));return;}/* 使能 HOST层,Bluedroid 协议栈 */ret = esp_bluedroid_enable();if (ret) {ESP_LOGE(GATTS_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));return;}
  1. 通过上述操作,我们成功的初始化了ESP32 的 bluedroid 协议栈。那么,此时就需要创建 GAP 和 GATT 任务。当我们触发 GAP 或者 GATT 事件时候,就会进入到如下的回调函数中。需要注意,如下的两个函数只能注册一次。
    /* 注册 GATTS 事件回调函数 */ret = esp_ble_gatts_register_callback(gatts_event_handler);if (ret){ESP_LOGE(GATTS_TAG, "gatts register error, error code = %x", ret);return;}/* 注册 GAP 事件回调函数 */ret = esp_ble_gap_register_callback(gap_event_handler);if (ret){ESP_LOGE(GATTS_TAG, "gap register error, error code = %x", ret);return;}
  1. 上述回调函数注册完成后,我们就可以根据需求来创建服务信息。这里我后续回进一步讲解。
    /* 注册一个app_id, 协议栈将会分配一个对应的 gatts_if,用于标识一个 GATT 服务。* 调用这个函数就会触发 esp_ble_gatts_register_callback() 注册的回调函数中的 ESP_GATTS_REG_EVT 事件*/ret = esp_ble_gatts_app_register(gl_profile_tab[PROFILE_A].app_id);if (ret){ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);return;}/* 注册一个app_id, 协议栈将会分配一个对应的 gatts_if,用于标识一个 GATT 服务。* 调用这个函数就会触发 esp_ble_gatts_register_callback() 注册的回调函数中的 ESP_GATTS_REG_EVT 事件*/ret = esp_ble_gatts_app_register(gl_profile_tab[PROFILE_B].app_id);if (ret){ESP_LOGE(GATTS_TAG, "gatts app register error, error code = %x", ret);return;}
  1. 当我们希望每一次传输的数据更多,能够拥有更大的吞吐量,那么就可以调用这个函数设置本地的 MTU。需要注意的一点是,这个实际的 MTU 大小是要根据与客户端(client) 协商后得到的。
    /* 设置本地 MTU 为 500 字节,实际交换时刻的 MTU 要通过与**客户端(client)**  协商后得到。* MTU 大小范围为 23 ~ 517 字节之间,如果没有调用该函数,则默认为 23 字节。*/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);}
  1. 当进行完成上述操作后,我们可以初始化广播包了。需要注意的一点是,这里有一个宏定义 CONFIG_SET_RAW_ADV_DATA 。如果我们希望广播包的内容更加灵活,就可以定义 CONFIG_SET_RAW_ADV_DATA 宏。如果是新手,还是不建议打开这个宏。
  2. 如果不打开这个宏,那么我们广播数据就会被局限为发送名称发射功率连接间隔外观厂商自定义数据服务 UUID 和数据广播发现模式标志位。如果你希望发送一些其他格式的数据例如 BTHome,使用 esp_ble_gap_config_adv_data() 函数就不是合适的选择。那么我们就需要使用原始数据包函数。
static void advertise_init(void)
{esp_err_t set_dev_name_ret = esp_ble_gap_set_device_name(TEST_DEVICE_NAME);if (set_dev_name_ret){ESP_LOGE(GATTS_TAG, "set device name failed, error code = %x", set_dev_name_ret);}
#ifdef CONFIG_SET_RAW_ADV_DATAesp_err_t raw_adv_ret = esp_ble_gap_config_adv_data_raw(raw_adv_data, sizeof(raw_adv_data));if (raw_adv_ret){ESP_LOGE(GATTS_TAG, "config raw adv data failed, error code = %x ", raw_adv_ret);}adv_config_done |= adv_config_flag;esp_err_t raw_scan_ret = esp_ble_gap_config_scan_rsp_data_raw(raw_scan_rsp_data, sizeof(raw_scan_rsp_data));if (raw_scan_ret){ESP_LOGE(GATTS_TAG, "config raw scan rsp data failed, error code = %x", raw_scan_ret);}adv_config_done |= scan_rsp_config_flag;
#else//config adv dataesp_err_t ret = esp_ble_gap_config_adv_data(&adv_data);if (ret){ESP_LOGE(GATTS_TAG, "config adv data failed, error code = %x", ret);}adv_config_done |= adv_config_flag;//config scan response dataret = esp_ble_gap_config_adv_data(&scan_rsp_data);if (ret){ESP_LOGE(GATTS_TAG, "config scan response data failed, error code = %x", ret);}adv_config_done |= scan_rsp_config_flag;
#endif
}

GATT 回调事件

GATT 服务启动流程

  1. 现在我们开始看程序执行流程。当我们调用 esp_ble_gatts_app_register() 注册一个 APP 任务,就会触发 ESP_GATTS_REG_EVT 事件。这里需要注意的一点是,APP ID 可以为 0 ~ 65535 的任意值,但是不能重复
  2. 之后我们需要在 ESP_GATTS_REG_EVT 事件中将协议栈分配的 gatts_if 进行存储。同时利用 esp_ble_gatts_create_service() 函数注册一个首要服务。
  3. esp_ble_gatts_create_service() 函数执行完成后,将会触发 ESP_GATTS_CREATE_EVT 事件。在该事件中,我们可以调用 esp_ble_gatts_add_char() 函数创建对应的特征信息,并且调用 esp_ble_gatts_start_service() 函数启动服务。
  4. 在调用 esp_ble_gatts_add_char() 函数之后,将会触发 ESP_GATTS_ADD_CHAR_EVT 事件。如果特征信息中,含有 ESP_GATT_CHAR_PROP_BIT_NOTIFY 或者 ESP_GATT_CHAR_PROP_BIT_INDICATE ,那么就必须调用 esp_ble_gatts_add_char_descr() 函数创建一个 特征配置描述符(characteristic descriptor)
  5. 在我们调用 esp_ble_gatts_start_service() 函数启动服务之后,将会触发 ESP_GATTS_START_EVT 事件,用于通知服务端(server)应用程序服务启动成功。
  6. 在我们调用 esp_ble_gatts_add_char_descr() 函数创建 特征配置描述符(characteristic descriptor) 后,将会触发 ESP_GATTS_ADD_CHAR_DESCR_EVT 事件。
  7. 例如下方,我们的日志打印信息可以看到任务执行流程。
I (741) GATTS_DEMO: GATTS event = 0
I (741) GATTS_DEMO: gatts_if = 4,param->reg.app_id = 0x66
I (751) GATTS_DEMO: REGISTER_APP_EVT, status 0, app_id 102
I (761) GATTS_DEMO: GATTS event = 7
I (771) GATTS_DEMO: CREATE_SERVICE_EVT, status 0,  service_handle 44
I (781) GATTS_DEMO: GATTS event = 9
I (791) GATTS_DEMO: ADD_CHAR_EVT, status 0,  attr_handle 46, service_handle 44
I (801) GATTS_DEMO: GATTS event = 12
I (801) GATTS_DEMO: SERVICE_START_EVT, status 0, service_handle 44
I (811) GATTS_DEMO: GATTS event = 10
  • ESP_GATTS_REG_EVT : APP 注册事件。当调用 esp_ble_gatts_app_register() 函数之后,会触发该。
  • ESP_GATTS_CREATE_EVT : 服务创建完成事件。当调用 esp_ble_gatts_create_service() 函数之后,会触发该事件。
  • ESP_GATTS_ADD_CHAR_EVT : 特征申明(characteristic declaration)特征值(characteristic value) 添加成功事件。 当调用esp_ble_gatts_add_char() 函数之后触发该事件。
  • ESP_GATTS_START_EVT : GATT 服务启动成功事件。当调用 esp_ble_gatts_start_service() 函数之后,会触发该事件。
  • ESP_GATTS_ADD_CHAR_DESCR_EVT : 特征配置描述符(characteristic descriptor) 添加成功事件。当调用 esp_ble_gatts_add_char_descr() 函数之后,触发该事件。

连接/断连事件

  1. 当成功与客户端(client) 建立连接,触发 ESP_GATTS_CONNECT_EVT 事件。我们在该事件中利用 esp_ble_gap_update_conn_params() 函数发起连接参数更新请求。
  2. 这里有两个注意的点,因为我们注册了两个 APP,因此我们会发现日志信息中有两次 ESP_GATTS_CONNECT_EVT 事件触发。
I (1619781) GATTS_DEMO: GATTS event = 14
I (1619791) GATTS_DEMO: ESP_GATTS_CONNECT_EVT, conn_id 0, remote 56:ae:b5:84:6f:16:
I (1619801) GATTS_DEMO: GATTS event = 14
I (1619811) GATTS_DEMO: CONNECT_EVT, conn_id 0, remote 56:ae:b5:84:6f:16:
  1. 正因为 ESP_GATTS_CONNECT_EVT 事件会触发两次,因此我们将连接参数更新的内容放在了 A 服务的回调函数中。如果放在公共的 GATT 回调函数中,那么连接参数更新将会被调用两次,而这两次的间隔事件太短,因此会出现如下警告。
W (22451) BT_L2CAP: l2cble_start_conn_update, the last connection update command still pending.
  1. 当与客户端(client) 连接断开,将会触发 ESP_GATTS_DISCONNECT_EVT 事件。
I (2275581) GATTS_DEMO: GATTS event = 15
I (2275581) GATTS_DEMO: ESP_GATTS_DISCONNECT_EVT, disconnect reason 0x13
  • ESP_GATTS_CONNECT_EVT : 当与客户端(client) 连接,触发该事件。
  • ESP_GATTS_DISCONNECT_EVT : 与客户端(client) 断开连接时候,触发该事件。

读事件触发流程

  1. 当我们客户端(client) 发送读请求时,将会触发 ESP_GATTS_READ_EVT 事件。
    在这里插入图片描述
  2. 在该事件中,我们将要返回给客户端(client) 的内容,通过 esp_ble_gatts_send_response() 函数返回。需要注意,一定要调用esp_ble_gatts_send_response() 函数进行返回,否则客户端(client) 将会一直死等数据,直到断连。
I (2029071) GATTS_DEMO: GATTS event = 1
I (2029071) GATTS_DEMO: GATT_READ_EVT, conn_id 0, trans_id 1, handle 46
I (2029071) GATTS_DEMO: GATTS event = 21
  1. 当调用 esp_ble_gatts_send_response() 函数之后,将会触发 ESP_GATTS_RESPONSE_EVT 事件,表示回包数据发送成功。
  • ESP_GATTS_READ_EVT : 当客户端(client) 发起读请求,触发该事件。
  • ESP_GATTS_RESPONSE_EVT : GATT 服务回报完成。该事件由 esp_ble_gatts_send_response() 函数执行完成后触发。

写事件触发流程

Command 写

  1. 当我们连接上设备后,点击发送按键。

在这里插入图片描述
2. 按照如下步骤发送数据。

在这里插入图片描述
3. 此时 ESP32 将会触发 ESP_GATTS_WRITE_EVT 事件。

  • param->write.conn_id : 如果是多连接,我们可以利用这个参数判断是那个主机发送的操作数据。因为该实验是一台手机作为客户端(client) ,一个 ESP32 作为服务端(Server)的点对点通讯,因此这个参数用不上。
  • param->write.trans_id : 用于标识一次写操作。因为客户端(client) 可能在同一时间对服务端(server)存在大量的写操作,那么就可以利用这个参数来确定服务端(server)到底是在对哪一个写操作回复。
  • param->write.handle : 用于表示是哪一个目标特征(Characteristic)或描述符(descriptor)的写操作。
  • param->write.need_rsp : 是否需要服务端(server)进行回复。因为这里我们是 Command,因此该参数是 false。
  • param->write.is_prep : 是否为准备写操作。当写入的数据大于 MTU-3 时候,将进行分段写数据,那么该参数为 true。
  • param->write.len : 写入数据值长度。
  • param->write.value : 写入的数据值。
  1. 通过上面的分析,我们现在就可以知道了,手机作为客户端(client) 进行 Command 写操作能够触发 ESP32 的 ESP_GATTS_WRITE_EVT 事件,而且无需回应,因此打印信息如下。
I (12711) GATTS_DEMO: GATTS event = 2
I (12711) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 1, handle 46
I (12711) GATTS_DEMO: It's not prepare write, value len 2, value :
I (12721) GATTS_DEMO: 55 66 
I (12731) GATTS_DEMO: write event, but not need to response
  • ESP_GATTS_WRITE_EVT : 当客户端(client) 发起写请求,触发该事件。

Request 写

  1. 同理,我们执行写操作,只不过这次我们需要服务端(server)进行数据回复。

在这里插入图片描述
2. 当客户端(client) 执行如上操作,将会触发 ESP_GATTS_WRITE_EVT 事件,在该事件中,我们调用 example_write_event_env() 处理写操作。
3. 因为客户端(client) 是需要数据回应。并且写入的数据小于 MTU - 3(初始化的时候。调用 esp_ble_gatt_set_local_mtu(),函数设置的 MTU 为23),因此 param->write.need_rsp 为 false ,不是准备写操作,直接调用 esp_ble_gatts_send_response() 进行数据包的回复。
4. 在调用 esp_ble_gatts_send_response() 函数之后,将会触发 ESP_GATTS_RESPONSE_EVT 事件告诉应用层表示回包数据发送完成。

I (56091) GATTS_DEMO: GATTS event = 2
I (56091) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 3, handle 46
I (56091) GATTS_DEMO: It's not prepare write, value len 2, value :
I (56101) GATTS_DEMO: 11 22 
I (56111) GATTS_DEMO: write event, but not prepare
I (56121) GATTS_DEMO: GATTS event = 21
  • ESP_GATTS_WRITE_EVT : 当客户端(client) 发起写请求,触发该事件。
  • ESP_GATTS_RESPONSE_EVT : GATT 服务回报完成。该事件由 esp_ble_gatts_send_response() 函数执行完成后触发。

prepare write

  1. 我们需要按照如下方法将双方的 MTU 设置为 23。

在这里插入图片描述
在这里插入图片描述

  1. 需要注意一点,prepare write 操作一定需要选择 Request。
    在这里插入图片描述
  2. 之后我们可以看到如下日志信息,我们一步一步来进行分析。
  3. 首先我们触发 ESP_GATTS_WRITE_EVT 事件发现这是一个准备写请求。那么就直接进入 example_write_event_env() 函数操作。因为在 prepare wirte 请求中,那么需要进行一些堆空间,以及判断传入数据是否超出堆空间大小。最终将收到的数据利用 esp_ble_gatts_send_response() 函数发送回去,并且存储在 a_prepare_write_env 或者 b_prepare_write_env 准备缓冲区。
  4. esp_ble_gatts_send_response() 函数会触发 ESP_GATTS_RESPONSE_EVT 事件表示发送了回包数据。
  5. 客户端(client) 收到回包数据后,继续将剩余的数据发送出来。此时再次触发 ESP_GATTS_WRITE_EVT 事件,因为这是 prepare wirte,因此进入 example_write_event_env() 函数操作,继续将发送回包,并且将剩余数据存储进 a_prepare_write_env 或者 b_prepare_write_env 准备缓冲区。
  6. esp_ble_gatts_send_response() 函数会触发 ESP_GATTS_RESPONSE_EVT 事件表示发送了回包数据。
  7. 客户端(client) 收到回包之后,发现数据发完了,此时执行 Execute Write Request,然后 ESP32 触发 ESP_GATTS_EXEC_WRITE_EVT 事件。在该事件中,ESP32 需要调用 esp_ble_gatts_send_response() 函数回发响应数据。然后调用 example_exec_write_event_env() 函数将收到的数据进行打印处理,并且释放申请到的缓冲区。
I (45331) GATTS_DEMO: GATTS event = 2
I (45331) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 1, handle 46
I (45331) GATTS_DEMO: GATTS event = 21
I (45391) GATTS_DEMO: GATTS event = 2
I (45391) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 2, handle 46
I (45391) GATTS_DEMO: GATTS event = 21
I (45451) GATTS_DEMO: GATTS event = 3
I (45451) GATTS_DEMO: ESP_GATTS_EXEC_WRITE_EVT
I (45451) GATTS_DEMO: 00 11 22 33 44 55 66 77 88 99 00 11 22 33 44 55 
I (45451) GATTS_DEMO: 66 77 88 99 00 
I (45461) GATTS_DEMO: GATTS event = 21
  • ESP_GATTS_WRITE_EVT : 当客户端(client) 发起写请求,触发该事件。
  • ESP_GATTS_RESPONSE_EVT : GATT 服务回报完成。该事件由 esp_ble_gatts_send_response() 函数执行完成后触发。
  • ESP_GATTS_EXEC_WRITE_EVT : 客户端(client) 发送 Execute Write Request 触发该事件。

特征配置描述符(characteristic descriptor)

  1. 我们点击如下按键可以使能 notify 功能。
    在这里插入图片描述
  2. 亦或者可以通过如下方法数据数据使能 notify。

在这里插入图片描述
在这里插入图片描述
3. 这里感觉有个 bug,理论上来说,notify 应该是不需要回包的,因此不会触发 ESP_GATTS_CONF_EVT 事件的,但是这里依旧有触发,这里建议抓包测试一下。

在这里插入图片描述
4. 除了 ESP_GATTS_CONF_EVT 事件,其他两个事件就很好理解了。客户端(client) 向 ESP32 特征配置描述 中写入数据是需要回包的,因此就会触发 ESP_GATTS_WRITE_EVT 和 ESP_GATTS_RESPONSE_EVT 事件。
在这里插入图片描述

I (14481) GATTS_DEMO: GATTS event = 2
I (14481) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 1, handle 47
I (14481) GATTS_DEMO: It's not prepare write, value len 2, value :
I (14491) GATTS_DEMO: 01 00 
I (14501) GATTS_DEMO: profile b notify enable
I (14511) GATTS_DEMO: write event, but not prepare
I (14521) GATTS_DEMO: GATTS event = 21
I (14521) GATTS_DEMO: GATTS event = 5
I (14531) GATTS_DEMO: ESP_GATTS_CONF_EVT status 0 attr_handle 46
  1. 当我们启动 indicate 或者输入 02 00 时候,将会触发如下事件。需要注意的一点是,notify 和 indicate 两者只能存在一个。
I (94731) GATTS_DEMO: GATTS event = 2
I (94731) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 3, handle 47
I (94731) GATTS_DEMO: It's not prepare write, value len 2, value :
I (94741) GATTS_DEMO: 02 00 
I (94751) GATTS_DEMO: indicate enable
I (94751) GATTS_DEMO: write event, but not prepare
I (94761) GATTS_DEMO: GATTS event = 21
I (94821) GATTS_DEMO: GATTS event = 5
I (94821) GATTS_DEMO: ESP_GATTS_CONF_EVT status 0 attr_handle 46
  1. 当关闭 indicate/notify 或者输入 00 00 时候,将触发如下事件。需要注意的是,如果要关,那么两个都会同时关闭。
I (42801) GATTS_DEMO: GATTS event = 2
I (42801) GATTS_DEMO: GATT_WRITE_EVT, conn_id 0, trans_id 2, handle 47
I (42801) GATTS_DEMO: It's not prepare write, value len 2, value :
I (42811) GATTS_DEMO: 00 00 
I (42821) GATTS_DEMO: notify/indicate disable 
I (42831) GATTS_DEMO: write event, but not prepare
I (42831) GATTS_DEMO: GATTS event = 21
  • ESP_GATTS_WRITE_EVT : 当客户端(client) 发起写请求,触发该事件。
  • ESP_GATTS_RESPONSE_EVT : GATT 服务回报完成。该事件由 esp_ble_gatts_send_response() 函数执行完成后触发。
  • ESP_GATTS_CONF_EVT : 调用 esp_ble_gatts_send_indicate() 函数触发。

GAP 回调事件

GAP 服务初始化

  1. 在注册完 APP 之后,我们调用 advertise_init() 函数初始化广播数据。当我们调用 esp_ble_gap_config_adv_data() 函数时候,将会触发 ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT 事件。
  2. 之后调用 esp_ble_gap_config_adv_data() 函数,触发 ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT 事件。在该事件里面,我们调用 esp_ble_gap_start_advertising() 函数启动广播。
  3. esp_ble_gap_start_advertising() 函数将会触发 ESP_GAP_BLE_ADV_START_COMPLETE_EVT 事件。我们可以在该事件中知道广播是否成功启动。
I (841) GATTS_DEMO: GAP Event:0
I (841) GATTS_DEMO: GAP Event:1
I (851) GATTS_DEMO: GAP Event:6
  • ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT : 设置广播数据完成事件。当调用 esp_ble_gap_config_adv_data() 函数,其中 set_scan_rsp 设置为 false 时触发该事件。
  • ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT : 设置广播回包数据完成事件。当调用 esp_ble_gap_config_adv_data() 函数,其中 set_scan_rsp 设置为 true 时触发该事件。
  • ESP_GAP_BLE_ADV_START_COMPLETE_EVT : 当调用 esp_ble_gap_start_advertising() 函数触发。

连接事件

  1. 在连接过程中需要协商双方的收发数据,因此会触发 ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT 事件。
  2. 在连接成功后,ESP_GATTS_CONNECT_EVT 事件中调用的 esp_ble_gap_update_conn_params() 函数,将会触发连接参数更新,因此将会触发 ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT 事件。我们可以在该事件中看到最终的从机延迟连接间隔监管超时等信息。
I (1619781) GATTS_DEMO: GAP Event:21
I (1619781) GATTS_DEMO: packet length updated: rx = 27, tx = 251, status = 0
# ... 这里省略 GATT 回调
I (1620171) GATTS_DEMO: GAP Event:20
I (1620171) GATTS_DEMO: update connection params status = 0, min_int = 16, max_int = 32,conn_int = 6,latency = 0, timeout = 500
I (1620501) GATTS_DEMO: GAP Event:20
I (1620501) GATTS_DEMO: update connection params status = 0, min_int = 0, max_int = 0,conn_int = 24,latency = 0, timeout = 400

参考

  1. ESP32 蓝牙 API
  2. BLE学习笔记(0.0) —— 基础概念(0)
  3. 乐鑫论坛 : Write a string to characteristic

这篇关于一文迅速上手 ESP32 bluedroid 蓝牙从机开发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1106374

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【区块链 + 人才服务】区块链集成开发平台 | FISCO BCOS应用案例

随着区块链技术的快速发展,越来越多的企业开始将其应用于实际业务中。然而,区块链技术的专业性使得其集成开发成为一项挑战。针对此,广东中创智慧科技有限公司基于国产开源联盟链 FISCO BCOS 推出了区块链集成开发平台。该平台基于区块链技术,提供一套全面的区块链开发工具和开发环境,支持开发者快速开发和部署区块链应用。此外,该平台还可以提供一套全面的区块链开发教程和文档,帮助开发者快速上手区块链开发。

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧

v0.dev快速开发

探索v0.dev:次世代开发者之利器 今之技艺日新月异,开发者之工具亦随之进步不辍。v0.dev者,新兴之开发者利器也,迅速引起众多开发者之瞩目。本文将引汝探究v0.dev之基本功能与优势,助汝速速上手,提升开发之效率。 何谓v0.dev? v0.dev者,现代化之开发者工具也,旨在简化并加速软件开发之过程。其集多种功能于一体,助开发者高效编写、测试及部署代码。无论汝为前端开发者、后端开发者

pico2 开发环境搭建-基于ubuntu

pico2 开发环境搭建-基于ubuntu 安装编译工具链下载sdk 和example编译example 安装编译工具链 sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib 注意cmake的版本,需要在3.17 以上 下载sdk 和ex