这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » [蓝牙系列]使用ESP-IDF实现GATTServer发送广播

共3条 1/1 1 跳转至

[蓝牙系列]使用ESP-IDF实现GATTServer发送广播

工程师
2025-08-19 20:15:02     打赏

简介

在上一篇文章中,我已经对蓝牙和蓝牙广播的相关知识进行了介绍。那么在本章节中我们将详细的了解蓝牙传输数据的几种方式,和使用ESP-IDF来基于官方Demo的修改里实现一个GATTServer实现手机端对ESP32设备的订阅(notice)

对于一个蓝牙 GATT(Generic Attribute Profile)服务而言,其主要的功能是有,Write, Read,Write with no response, Notify, 和 indicate其主要的区别如下所示

image.png

ACK的意思是否进行确认,比如写的情况,当我写成功之后要求从机给我一个相应来确认是否写入成功。这个就是ACK。需要ACK的交互方式复杂度较高。一般被用于需要回调的敏感数据。


蓝牙 GATT(Generic Attribute Profile)服务简介

蓝牙 GATT(Generic Attribute Profile)服务 是 BLE 通信的核心,它定义了设备间数据的组织和交互方式。一个 GATT 服务由若干特征(Characteristic)组成,特征是具体的数据点,可以支持 读(Read)、写(Write/Write Without Response)、通知(Notify)、指示(Indicate) 等操作。通常,外围设备(GATT Server)保存数据,中心设备(GATT Client)通过这些操作来读取或修改数据,从而实现如传感器数据采集、智能家居控制、可穿戴设备同步等功能。


在ESP-IDF的环境下来实现GATT服务非常简单。官方有已经创建好的Demo示例。

首先我们需要创建GATT 项目

image.png

这份示例主要演示了 ESP32 作为 GATT Server 的典型用法。其主要流程如下:

设备广播(Advertising):ESP32 会定期广播自己的名称、UUID 等信息。

客户端连接:比如手机 App 或另一台 ESP32(运行 GATT Client 示例)可以扫描并连接该 Server。

特征读写:客户端可以读取 ESP32 提供的特征值(如本文例子中模拟的湿度数据),也可以向其写入数据。

通知功能(Notify):客户端启用 Notify 后,ESP32 会定时主动向客户端推送数据 (我这里进行了修改,数据是逐渐改变的。 采用的indicate模式,因此在接受到数据之后ESP32会收到来自手机的ACK确认)。



 GATT Server 程序的执行流程大概是(建议配合Demo进行阅读):

1- 系统初始化(NVS、BLE 控制器、协议栈,已经在main方法中进行包含)。

 esp_err_t ret;

    // Initialize NVS.
    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());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

#if CONFIG_EXAMPLE_CI_PIPELINE_ID
    memcpy(test_device_name, esp_bluedroid_get_example_name(), ESP_BLE_ADV_NAME_LEN_MAX);
#endif

    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));

    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;
    }

    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;
    }

    ret = esp_bluedroid_init();
    if (ret)
    {
        ESP_LOGE(GATTS_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }
    ret = esp_bluedroid_enable();
    if (ret)
    {
        ESP_LOGE(GATTS_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
        return;
    }

2- 注册 GAP 和 GATTS 回调 (已经在main方法中进行包含)。

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;
    }
    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;
    }
    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);
    }

3- 设置广播包并启动广播。

static esp_ble_adv_data_t adv_data = {
    .set_scan_rsp = false,
    .include_name = true, // 说明蓝牙广播中包含name
    .include_txpower = false,
    .service_uuid_len = sizeof(adv_service_uuid128),
    .p_service_uuid = adv_service_uuid128, //指定的128位自定义服务ID
    .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), # 说明这是一个BLE设备,不支持传统蓝牙
};

之后便是设置广播数据,并且启动广播(这些数据代码都在官方代码的对应event处理中,实际开发的时候不需要手动去操作每一个event,只需要在对应的event里来增加自己的代码就可以了。)

 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);
        }
        adv_config_done |= adv_config_flag;
        // config scan response data
        ret = esp_ble_gap_config_adv_data(&scan_rsp_data);

然后就是注册GATT服务

  esp_ble_gatts_create_service(gatts_if, &gl_profile_tab[PROFILE_A_APP_ID].service_id, GATTS_NUM_HANDLE_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);

由于在上方我已经定义了原本的服务ID为 0x1809

#define GATTS_SERVICE_UUID_TEST_A 0x1809

所以当前的服务会被识别为Health Thermometer Service服务

4- 客户端连接 ESP32,建立 GATT 会话。

5- 处理客户端请求(读、写、订阅 Notify)。

然后在这里我对官方的代码进行了一点修改,使其连接上之后,如果进行读的话,那么我会发送给他一个固定的数据。

case ESP_GATTS_READ_EVT:
    {
        ESP_LOGI(GATTS_TAG, "Characteristic read, conn_id %d, trans_id %" PRIu32 ", handle %d",
                 param->read.conn_id, param->read.trans_id, param->read.handle);

        uint8_t humi = 56; // 湿度%
        esp_gatt_rsp_t rsp;
        memset(&rsp, 0, sizeof(rsp));
        rsp.attr_value.handle = param->read.handle;
        rsp.attr_value.len = 1; // 只发一个字节
        rsp.attr_value.value[0] = humi;

        esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &rsp);
        break;
    }

6- 若开启 Notify,ESP32 定时向客户端推送数据。

void notify_task(void *param) {
    while (notify_enabled) {
        uint8_t data[15];
        for (int i = 0; i < sizeof(data); ++i) {
            data[i] = notify_counter + i;
        }
        notify_counter++;
        esp_ble_gatts_send_indicate(gatts_if, conn_id, char_handle,
                                    sizeof(data), data, false);
        vTaskDelay(pdMS_TO_TICKS(1000)); // 每秒推送一次
    }
}

这里我也进行了修改,原本的订阅只是一个固定的数据,我在GATT的写事件里增加了一个任务的判断,使其如果当前的用户开启了通知的话,那么会判断当前的任务是否存在,如果不存在的话,启动一个task定期向手机端发送数据。如果用户点击关闭notify的话。那么则删除掉这个任务。这样的话就可以实现ESP32一直向手机端发送数据的功能了。

case ESP_GATTS_WRITE_EVT:
    {
        ESP_LOGI(GATTS_TAG, "Characteristic write, 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, "value len %d, value ", param->write.len);
            ESP_LOG_BUFFER_HEX(GATTS_TAG, param->write.value, param->write.len);
            if (gl_profile_tab[PROFILE_A_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 (a_property & ESP_GATT_CHAR_PROP_BIT_NOTIFY)
                    {
                        ESP_LOGI(GATTS_TAG, "Notification enable");
                        notify_enabled = true;

                        if (notify_task_handle == NULL)
                        {
                            uint16_t *conn_id_copy = malloc(sizeof(uint16_t));
                            *conn_id_copy = param->write.conn_id;

                            xTaskCreate(notify_task,
                                        "notify_task",
                                        4096,
                                        conn_id_copy,
                                        5,
                                        &notify_task_handle);
                        }
                    }
                }
                else if (descr_value == 0x0002)
                {
                    if (a_property & ESP_GATT_CHAR_PROP_BIT_INDICATE)
                    {
                        ESP_LOGI(GATTS_TAG, "Indication 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_A_APP_ID].char_handle,
                                                    sizeof(indicate_data), indicate_data, true);
                    }
                }
                else if (descr_value == 0x0000)
                {
                    ESP_LOGI(GATTS_TAG, "Notification/Indication disable");
                    notify_enabled = false;
                }
                else
                {
                    ESP_LOGE(GATTS_TAG, "Unknown descriptor 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;
    }


效果演示

1- 手机搜索BLE服务

a29be6cc6ee9f5bbbe0df4ee97f029f6.jpg


2- 查看其广播数据

43b30a408107f77149d42af62de079b0.jpg

3- 连接后查看具有的服务(已经成功的识别到我们定义的服务信息)

240620b7e63bb6d597ebbbcbba1b8c75.jpg

4- 读数据,成功读取到我们自定义的56,转换成Hex为0x38

373fdbb139267ffd5e0be1277a344003.jpg


5- 通知数据,自动刷新。点击notify后,数据自动发送,取消之后,数据停止发送

879020b74532bac88a8e84b302107b42.jpg

04e34a047f301fb9c4fb748e22fcf6fe.jpg

上图为不同的value值,从ESP32发送到手机。


附件

gatt-server.zip




关键词: 蓝牙系列          GATTServer     ESP-IDF    

专家
2025-08-19 22:09:53     打赏
2楼

感谢分享


专家
2025-08-19 22:11:53     打赏
3楼

感谢分享


共3条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]