ESP32学习笔记(34)——BLE一主多从连接

一、简介

由于蓝牙主机和从机组网,如果不使用 Mesh 的话,只能组微微网。蓝牙5.0的微微网最大可以连接20个从机。

1.1 连接句柄

在主机与从机发生连接的时候会进行连接句柄的分配。连接句柄的作用是在蓝牙数据进行分组的时候进行设备区分的。连接句柄相当于一个“令牌”,从设备一旦和主设备发生连接,主设备就给从设备分配一个“令牌”。主设备通过这个“令牌”来识别与区分从设备。因此对于连接句柄的分配将是实现一主多从连接,并且进行通信的关键。

1.2 Bluedroid主机架构

在 ESP-IDF 中,使用经过大量修改后的 BLUEDROID 作为蓝牙主机 (Classic BT + BLE)。BLUEDROID 拥有较为完善的功能,⽀持常用的规范和架构设计,同时也较为复杂。经过大量修改后,BLUEDROID 保留了大多数 BTA 层以下的代码,几乎完全删去了 BTIF 层的代码,使用了较为精简的 BTC 层作为内置规范及 Misc 控制层。修改后的 BLUEDROID 及其与控制器之间的关系如下图:

二、API说明

以下 GATT 接口位于 bt/host/bluedroid/api/include/api/esp_gattc_api.h

2.1 esp_ble_gattc_open

2.2 esp_ble_gattc_search_service

2.3 esp_ble_gattc_get_char_by_uuid

2.4 esp_ble_gattc_get_descr_by_char_handle

2.5 esp_ble_gattc_get_attr_count

2.6 esp_ble_gattc_write_char

2.7 esp_ble_gattc_write_char_descr


##2.8 esp_ble_gattc_register_for_notify

三、BT控制器和协议栈初始化

使用 esp-idf\examples\bluetooth\bluedroid\ble\gattc_multi_connect 中的例程

.........
//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(GATTC_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(GATTC_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(GATTC_TAG, "%s init bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
        return;
    }
    //使能蓝牙栈
    ret = esp_bluedroid_enable();
    if (ret) {
        ESP_LOGE(GATTC_TAG, "%s enable bluetooth failed: %s\n", __func__, esp_err_to_name(ret));
        return;
    }

    //建立蓝牙的FSM(有限状态机)
    //这里使用回调函数来控制每个状态下的响应,需要将其在GATT和GAP层的回调函数注册
    /*esp_gattc_cb和esp_gap_cb处理蓝牙栈可能发生的所有情况,达到FSM的效果*/
    ret = esp_ble_gap_register_callback(esp_gap_cb);
    if (ret){
        ESP_LOGE(GATTC_TAG, "gap register error, error code = %x", ret);
        return;
    }
    ret = esp_ble_gattc_register_callback(esp_gattc_cb);
    if(ret){
        ESP_LOGE(GATTC_TAG, "gattc register error, error code = %x", ret);
        return;
    }

    //下面创建了BLE GATT服务A、B、C,相当于3个独立的应用程序
    ret = esp_ble_gattc_app_register(PROFILE_A_APP_ID);
    if (ret){
        ESP_LOGE(GATTC_TAG, "gattc app register error, error code = %x", ret);
        return;
    }
    ret = esp_ble_gattc_app_register(PROFILE_B_APP_ID);
    if (ret){
        ESP_LOGE(GATTC_TAG, "gattc app register error, error code = %x", ret);
        return;
    }
    ret = esp_ble_gattc_app_register(PROFILE_C_APP_ID);
    if (ret){
        ESP_LOGE(GATTC_TAG, "gattc app register error, error code = %x", ret);
        return;
    }
    /*
    设置了MTU的值(经过MTU交换,从而设置一个PDU中最大能够交换的数据量)。
    例如:主设备发出一个1000字节的MTU请求,但是从设备回应的MTU是500字节,那么今后双方要以较小的值500字节作为以后的MTU。
    即主从双方每次在做数据传输时不超过这个最大数据单元。
    */
    ret = esp_ble_gatt_set_local_mtu(200);
    if (ret){
        ESP_LOGE(GATTC_TAG, "set local  MTU failed, error code = %x", ret);
    }
.......

四、应用程序配置文件

应用程序配置文件是一种对功能进行分组的方法。它们的设计使每个应用程序配置文件连接到一个对等设备,这样同一个 ESP32 可以通过为每个设备分配一个应用程序配置文件连接到多个设备。每个应用程序配置文件都会创建一个 GATT 接口以连接到其他设备。应用程序配置文件由 ID 号定义,在此示例中有三个配置文件:

#define PROFILE_NUM 3
#define PROFILE_A_APP_ID 0	
#define PROFILE_B_APP_ID 1
#define PROFILE_C_APP_ID 2

esp_ble_gattc_app_register()函数用于将每个应用程序配置文件注册到 BLE 堆栈。注册操作会生成一个 GATT 接口,该接口作为注册事件中的参数返回。此外,每个应用程序配置文件还由一个结构定义,该结构可用于在堆栈传播新数据时保持应用程序的状态并更新其参数。

代码中的应用程序配置文件是gattc_profile_inst结构的实例。有关详细信息,请参见应用程序配置文件GATT客户实例演练

五、扫描

5.1 设置扫描参数

参见第设置扫描参数GATT客户实例演练

5.2 开始扫描

参阅Section开始扫描GATT客户实例演练

5.3 获取扫描结果

参阅Section获取扫描结果GATT客户实例演练

5.4 名称比较

  • 首先,从广告数据中提取设备名称并存储在adv_name变量中:
adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len);
  • 然后,将找到的设备名称与客户端想要连接的服务器名称进行比较。服务器名称在remote_device_name数组中定义:
static const char remote_device_name[3][20] = {"ESP_GATTS_DEMO_1", "ESP_GATTS_DEMO_2", “ESP_GATTS_DEMO_3"};

名称比较发生如下:

 if (strlen(remote_device_name[0]) == adv_name_len && strncmp((char *)adv_name, remote_device_name[0], adv_name_len) == 0) {
                 if (find_device_1 == false) {
                     find_device_1 = true;
                     ESP_LOGI(GATTC_TAG, "Searched device %s", remote_device_name[0]);
                     memcpy(gl_profile_tab[PROFILE_A_APP_ID].remote_bda, scan_result->scan_rst.bda, 6);
                 }
                 break;
             }
             else if (strlen(remote_device_name[1]) == adv_name_len && strncmp((char *)adv_name, remote_device_name[1], adv_name_len) == 0) {
                 if (find_device_2 == false) {
                     find_device_2 = true;
                     ESP_LOGI(GATTC_TAG, "Searched device %s", remote_device_name[1]);
                     memcpy(gl_profile_tab[PROFILE_B_APP_ID].remote_bda, scan_result->scan_rst.bda, 6);
                 }
             }
             else if (strlen(remote_device_name[2]) == adv_name_len && strncmp((char *)adv_name, remote_device_name[2], adv_name_len) == 0) {
                 if (find_device_3 == false) {
                     find_device_3 = true;
                     ESP_LOGI(GATTC_TAG, "Searched device %s", remote_device_name[2]);
                     memcpy(gl_profile_tab[PROFILE_C_APP_ID].remote_bda, scan_result->scan_rst.bda, 6);
                 }
                 break;
             }                                    
  • 如果找到的任何设备名称对应于远程设备名称,find_device_X则设置该标志并将远程设备的地址存储在gl_profile_tab表中。设置所有标志后,客户端将停止扫描并连接到远程设备。

六、连接到远程设备

6.1 连接到第一个远程设备

找到所有设备后,客户端将停止扫描:

if (find_device_1 && find_device_2 && find_device_3 && stop_scan == false {
    stop_scan = true;
    esp_ble_gap_stop_scanning();
    }

扫描停止触发一个ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT事件,该事件用于打开与第一个远程设备的连接。一旦客户端搜索服务、获取特征并在第一个设备上注册通知,第二个和第三个设备就会建立连接。此工作流旨在测试每个远程设备之间的通信是否正常工作,然后再尝试连接到下一个设备,或者如果出现错误,请跳到下一个设备。

case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
    if (param->scan_stop_cmpl.status != ESP_BT_STATUS_SUCCESS){
        ESP_LOGE(GATTC_TAG, "Scan stop failed");
        break;
    }
    ESP_LOGI(GATTC_TAG, "Stop scan successfully");
    if (!stop_scan){
        ESP_LOGE(GATTC_TAG, "Did not find all devices");
    }
    if (find_device_1){
        esp_ble_gattc_open(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, gl_profile_tab[PROFILE_A_APP_ID].remote_bda, true);
    }
    break;
  • 使用esp_ble_gattc_open()GATT 接口、远程设备地址和布尔值的函数打开连接,直接连接设置为 true,后台自动连接设置为 false。为了断开物理连接,使用了 GAP API 函数esp_ble_gap_disconnect()

    连接到第一个设备时,ESP_GATTC_CONNECT_EVT会生成一个事件,该事件将转发到所有配置文件。它还触发ESP_GATTC_OPEN_EVT仅转发到 Profile A 事件处理程序或gattc_profile_a_event_handler()函数的事件。该事件检查连接是否成功打开,如果没有,则忽略该设备,客户端尝试打开与第二个设备的连接:

case ESP_GATTC_OPEN_EVT:
      if (p_data->open.status != ESP_GATT_OK){
          //open failed, ignore the first device, connect the second device
          ESP_LOGE(GATTC_TAG, "connect device failed, status %d", p_data->open.status);
          if (find_device_2){
              esp_ble_gattc_open(gl_profile_tab[PROFILE_B_APP_ID].gattc_if, gl_profile_tab[PROFILE_B_APP_ID].remote_bda, true);
          }
          break;
      }

如果连接成功,客户端保存连接 ID,打印远程设备信息并将 MTU 大小配置为 200 字节。

 gl_profile_tab[PROFILE_A_APP_ID].conn_id = p_data->open.conn_id;
 ESP_LOGI(GATTC_TAG, "ESP_GATTC_OPEN_EVT conn_id %d, if %d, status %d, mtu %d", p_data->open.conn_id, gattc_if, p_data->open.status, p_data->open.mtu);
 ESP_LOGI(GATTC_TAG, "REMOTE BDA:");
 esp_log_buffer_hex(GATTC_TAG, p_data->open.remote_bda, sizeof(esp_bd_addr_t));
 esp_err_t mtu_ret = esp_ble_gattc_config_mtu (gattc_if, p_data->open.conn_id, 200);
 if (mtu_ret){
 ESP_LOGE(GATTC_TAG, "config MTU error, error code = %x", mtu_ret);
 }
 break;
  • 配置好MTU大小后,ESP_GATTC_CFG_MTU_EVT生成一个。此事件用于搜索远程设备上可用的已知服务。搜索是通过使用esp_ble_gattc_search_service()函数和由以下定义的服务 ID 执行的:
 static esp_bt_uuid_t remote_filter_service_uuid = {
 .len = ESP_UUID_LEN_16,
 .uuid = {.uuid16 = REMOTE_SERVICE_UUID,},
 };

然后处理程序搜索服务:

 case ESP_GATTC_CFG_MTU_EVT:
     if (param->cfg_mtu.status != ESP_GATT_OK){
         ESP_LOGE(GATTC_TAG,"Config mtu failed");
     }
     ESP_LOGI(GATTC_TAG, "Status %d, MTU %d, conn_id %d", param->cfg_mtu.status, param->cfg_mtu.mtu, param->cfg_mtu.conn_id);
     esp_ble_gattc_search_service(gattc_if, param->cfg_mtu.conn_id, &remote_filter_service_uuid);
     break;

如果找到该服务,ESP_GATTC_SEARCH_RES_EVT则会触发一个允许将 设置get_service_1 flag为 true 的事件。该标志用于打印信息,稍后获取客户端感兴趣的特征。

  • 一旦完成对所有服务的搜索,ESP_GATTC_SEARCH_CMPL_EVT就会生成一个事件,用于获取刚刚发现的服务的特征。这是通过esp_ble_gattc_get_characteristic()函数完成的:
 case ESP_GATTC_SEARCH_CMPL_EVT:
     if (p_data->search_cmpl.status != ESP_GATT_OK){
         ESP_LOGE(GATTC_TAG, "search service failed, error status = %x", p_data->search_cmpl.status);
         break;
     }
     if (get_service_1){
         esp_ble_gattc_get_characteristic(gattc_if, p_data->search_cmpl.conn_id, &remote_service_id, NULL);
     }
     break;

esp_ble_gattc_get_characteristic()函数以 GATT 接口、连接 ID 和远程服务 ID 作为参数。此外,还传递了一个 NULL 值,以表明我们希望所有特征都从第一个开始。如果客户端对特定特征感兴趣,它可以在此字段中传递特征 ID 来指定该特征。一个ESP_GATTC_GET_CHAR_EVT当特征发现了触发事件。此事件用于打印有关特征的信息。

  • 如果特征 ID 与 定义的相同REMOTE_NOTIFY_CHAR_UUID,则客户端注册该特征值的通知。
  • 最后,使用相同的esp_ble_gattc_get_characteristic()函数请求下一个特征,这次,最后一个参数设置为当前特征。这会触发另一个,ESP_GATTC_GET_CHAR_EVT并重复该过程,直到获得所有特征。
 case ESP_GATTC_GET_CHAR_EVT:
     if (p_data->get_char.status != ESP_GATT_OK) {
         break;
     }
     ESP_LOGI(GATTC_TAG, "GET CHAR: conn_id = %x, status %d", p_data->get_char.conn_id, p_data->get_char.status);
     ESP_LOGI(GATTC_TAG, "GET CHAR: srvc_id = %04x, char_id = %04x", p_data->get_char.srvc_id.id.uuid.uuid.uuid16, p_data->get_char.char_id.uuid.uuid.uuid16);

     if (p_data->get_char.char_id.uuid.uuid.uuid16 == REMOTE_NOTIFY_CHAR_UUID) {
         ESP_LOGI(GATTC_TAG, "register notify");
         esp_ble_gattc_register_for_notify(gattc_if, gl_profile_tab[PROFILE_A_APP_ID].remote_bda, &remote_service_id, &p_data->get_char.char_id);
     }

     esp_ble_gattc_get_characteristic(gattc_if, p_data->get_char.conn_id, &remote_service_id, &p_data->get_char.char_id);
     break;

此时,客户端已从远程设备获取了所有特征,并订阅了有关感兴趣特征的通知。每次客户端注册通知时,ESP_GATTC_REG_FOR_NOTIFY_EVT都会触发一个事件。在此示例中,此事件设置为使用该esp_ble_gattc_write_char_descr()函数写入远程设备客户端配置特征 (CCC) 。反过来,此函数用于写入特征描述符。蓝牙规范定义了许多特征描述符,但是,对于本示例,感兴趣的描述符是处理启用通知的描述符,即客户端配置描述符。

6.2 连接到下一个远程设备

  • 为了将此描述符作为参数传递,我们首先将其定义为:
 static esp_gatt_id_t notify_descr_id = {
 .uuid = {
     .len = ESP_UUID_LEN_16,
     .uuid = {.uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG,},
     },
 .inst_id = 0,
 };

其中ESP_GATT_UUID_CHAR_CLIENT_CONFIG定义为 UUID 以识别 CCC:

#define ESP_GATT_UUID_CHAR_CLIENT_CONFIG            0x2902          /*  Client Characteristic Configuration */

要写入的值为“1”以启用通知。该参数ESP_GATT_WRITE_TYPE_RSP还传递给请求服务器响应写请求,以及ESP_GATT_AUTH_REQ_NONE指示写请求不需要授权的参数:

case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
    if (p_data->reg_for_notify.status != ESP_GATT_OK){
        ESP_LOGE(GATTC_TAG, "reg notify failed, error status =%x", p_data->reg_for_notify.status);
        break;
    }
    uint16_t notify_en = 1;
    ESP_LOGI(GATTC_TAG, "REG FOR NOTIFY: status %d, srvc_id = %04x, char_id = %04x",
            p_data->reg_for_notify.status,
            p_data->reg_for_notify.srvc_id.id.uuid.uuid.uuid16,
            p_data->reg_for_notify.char_id.uuid.uuid.uuid16);

    esp_ble_gattc_write_char_descr(gattc_if,
                                   gl_profile_tab[PROFILE_A_APP_ID].conn_id,
                                   &remote_service_id,
                                   &p_data->reg_for_notify.char_id,
                                   &notify_descr_id,
                                   sizeof(notify_en),
                                   (uint8_t *)&notify_en,
                                   ESP_GATT_WRITE_TYPE_RSP,
                                   ESP_GATT_AUTH_REQ_NONE);
    break;
}
  • 启用通知后,远程设备会发送通知,触发ESP_GATTC_NOTIFY_EVT客户端上的事件。处理此事件以使用以下esp_ble_gattc_write_char()函数写回特征:
 case ESP_GATTC_NOTIFY_EVT:
     ESP_LOGI(GATTC_TAG, "ESP_GATTC_NOTIFY_EVT, Receive notify value:");
     esp_log_buffer_hex(GATTC_TAG, p_data->notify.value, p_data->notify.value_len);
     //write  back
     esp_ble_gattc_write_char(gattc_if,
                             gl_profile_tab[PROFILE_A_APP_ID].conn_id,
                             &remote_service_id,
                             &p_data->notify.char_id,
                             p_data->notify.value_len,
                             p_data->notify.value,
                             ESP_GATT_WRITE_TYPE_RSP,
                             ESP_GATT_AUTH_REQ_NONE);
     break;
  • 如果写入过程得到确认,则远程设备已成功连接并且通信建立且没有错误。立即,写入过程生成一个ESP_GATTC_WRITE_CHAR_EVT事件,在此示例中用于打印信息并连接到第二个远程设备:
 case ESP_GATTC_WRITE_CHAR_EVT:
     if (p_data->write.status != ESP_GATT_OK){
         ESP_LOGE(GATTC_TAG, "write char failed, error status = %x", p_data->write.status);
     }else{
         ESP_LOGI(GATTC_TAG, "write char success");
     }
     //connect the second device
     if (find_device_2){
         esp_ble_gattc_open(gl_profile_tab[PROFILE_B_APP_ID].gattc_if, gl_profile_tab[PROFILE_B_APP_ID].remote_bda, true);
     }
     break;
  • 这将触发由 Profile B 事件处理程序处理的打开事件。此处理程序遵循相同的步骤来搜索服务、获取特征、注册通知并作为第一个设备写入特征。第二个设备的序列也以一个ESP_GATTC_WRITE_CHAR_EVT事件结束,该事件又用于连接到第三个设备:
 case ESP_GATTC_WRITE_CHAR_EVT:
     if (p_data->write.status != ESP_GATT_OK){
         ESP_LOGE(GATTC_TAG, "Write char failed, error status = %x", p_data->write.status);
     }else{
         ESP_LOGI(GATTC_TAG, "Write char success");
     }
     //connect the third device
     if (find_device_3){
         esp_ble_gattc_open(gl_profile_tab[PROFILE_C_APP_ID].gattc_if, gl_profile_tab[PROFILE_C_APP_ID].remote_bda, true);
     }
     break;
  • 第三设备还以与第一和第二设备相同的形式执行相同的配置和通信步骤。成功完成后,所有三个远程设备同时正确连接并无错误地接收通知。

• 由 Leung 写于 2021 年 7 月 14 日

• 参考:GATT 客户端多连接示例演练

  • 8
    点赞
  • 88
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
ESP32是一款功能强大的开源物联网开发板,它支持蓝牙低功耗(BLE连接。使用ESP32通过BLE连接服务器,可以实现与设备之间的无线通信。 要建立ESP32与服务器之间的BLE连接,首先需要确保ESP32上的BLE库和服务器上的BLE服务相互兼容。ESP32提供了许多BLE库,如ArduinoBLE、nRF Connect等,可以根据需要选择合适的库。 首先,在ESP32上启动BLE功能,并将其设置为外围设备(peripheral)。外围设备是指其它设备可以连接并与之通信的设备。然后,将ESP32与服务器建立BLE连接的相关设置(如设备名称、UUID等)配置到ESP32中。 接下来,ESP32将扫描周围可用的BLE服务器,并尝试连接到其中一个服务器。可以使用ESP32提供的BLE库中的扫描函数来实现这一点。一旦ESP32成功连接到服务器,就可以开始发送和接收数据。 在连接建立后,可以使用BLE库提供的函数来读取和写入数据。例如,可以使用BLE库中的`read()`函数从服务器读取数据,或使用`write()`函数将数据写入到服务器。 在进行数据传输时,需要在ESP32和服务器上分别实现相应的数据处理逻辑。例如,可以使用回调函数来处理接收到的数据,并在需要时发送响应。 要确保BLE连接的稳定性和安全性,可以使用加密和认证机制。例如,通过在连接过程中对数据进行加密和验证,可以确保数据的机密性和完整性。 总之,通过使用ESP32BLE功能,可以方便地实现与服务器的无线连接。无论是数据传输、设备控制还是传感器数据采集,ESP32BLE连接能够为物联网应用提供可靠、稳定和安全的通信方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leung_ManWah

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值