ESP32 Bluedroid 篇(1)—— ibeacon 广播

news/2024/9/28 13:35:37 标签: 单片机

前言

  1. 前面我们已经了解了 ESP32 的 BLE 整体架构,现在我们开始实际学习一下Bluedroid 从机篇的广播和扫描。
  2. 本文将会以 ble_ibeacon demo 为例子进行讲解,需要注意的一点是。ibeacon 分为两个部分,一个是作为广播者,一个是作为观察者IBEACON_RECEIVER 这个宏表示作为观察者IBEACON_SENDER 这个宏被置 1 表示为广播者
  3. 需要注意的一点是,本文先仅介绍广播相关内容

ibeacon 介绍

ibeacon 是什么?

  1. 作为一名初学者,当听到 ibeacon 时候,大概率是一脸懵逼的。即使网上搜索大量资料,没有亲身体验,也是一头雾水。为了方便各位理解,就以我来上海实习,周末逛的第一个景点 – 豫园 为例子进行分析。
  2. 当我们进入景点,肯定会有一个二维码建议我们扫描,然后之后就会有电子讲解功能。例如我现在扫描了豫园的二维码,打开了一个微信小程序,此时微信小程序上就能够显示出我的位置在哪里。

在这里插入图片描述
3. 如果你移动到一个地方,该小程序就会进行相关讲解当前景点的一些历史文化信息。此时,各位有没有想过一个问题,该小程序,是如何精确的知道我们当前是在哪个景点呢?
4. 此时,就是利用的 ibeacon 技术进行的。如果你有兴趣的话,可以在走到某个景点,发现微信小程序播报讲解时刻停下来,然后在这个附近十米内的范围转转,会惊奇的发现,一些地方藏有这种小方块。

在这里插入图片描述

在这里插入图片描述
5. 这个小方块,就是本文要进行讲解的,ESP32 作为广播者的功能。而你手机,就是充当的观察者

ibeacon 有什么用?

  1. 现在我们明白了 ibeacon 技术大概是什么东西了。那么这个有什么作用呢?从上面的例子我们就可以很好的知道,室内定位广播信息
  2. 当前,室内定位技术一直是一项值得探索的技术,ibeacon 可以说提供了一个不错的选择(不过个人感觉目前 BLE 室内定位更多的是聚焦于 AOA)。
  3. 同样,在商场,我们只需要打开微信小程序走到哪家店铺,就可以直接查看那家店铺的商品信息,这样一定程度上可以方便用户挑选商品。

ibeacon 工程介绍

工程源码

  1. 我们先拷贝 ble_ibeacon demo 例程出来,打开ibeacon_demo.c 文件,将其替换为如下内容。
/*
 * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Unlicense OR CC0-1.0
 */



/****************************************************************************
*
* This file is for iBeacon demo. It supports both iBeacon sender and receiver
* which is distinguished by macros IBEACON_SENDER and IBEACON_RECEIVER,
*
* iBeacon is a trademark of Apple Inc. Before building devices which use iBeacon technology,
* visit https://developer.apple.com/ibeacon/ to obtain a license.
*
****************************************************************************/

#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "nvs_flash.h"

#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_gatt_defs.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_ibeacon_api.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"

static const char* DEMO_TAG = "IBEACON_DEMO";
extern esp_ble_ibeacon_vendor_t vendor_config;

#if (IBEACON_MODE == IBEACON_RECEIVER)
// 在停止扫描请求发送后,蓝牙堆栈可能还会处理一些尚未完成的扫描结果。因此需要通过这个标志位来设置是否需要继续处理扫描完成事件
static bool is_scanning = false;
#endif

///Declare static functions
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);

#if (IBEACON_MODE == IBEACON_RECEIVER)
static esp_ble_scan_params_t ble_scan_params = {
    .scan_type              = BLE_SCAN_TYPE_ACTIVE, // 主动扫描
    .own_addr_type          = BLE_ADDR_TYPE_PUBLIC, // 公共地址
    .scan_filter_policy     = BLE_SCAN_FILTER_ALLOW_ALL, // 允许扫描所有设备
    .scan_interval          = 0x50,
    .scan_window            = 0x30,
    .scan_duplicate         = BLE_SCAN_DUPLICATE_DISABLE
};

#elif (IBEACON_MODE == IBEACON_SENDER)
static esp_ble_adv_params_t ble_adv_params = {
    .adv_int_min        = 0x20,                 // 0x20*0.625ms=20ms,Range: 0x0020 to 0x4000 (20ms to 10240ms)
    .adv_int_max        = 0x40,                 // 0x40*0.625ms=40ms
    .adv_type           = ADV_TYPE_NONCONN_IND, // 不可连接广播
    // .adv_type           = ADV_TYPE_DIRECT_IND_HIGH,  // 设置为高占空比定向广播
    // .peer_addr          = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6},  // 目标设备MAC地址
    // .peer_addr_type     = BLE_ADDR_TYPE_PUBLIC,      // 目标设备的地址类型

    .own_addr_type      = BLE_ADDR_TYPE_PUBLIC, // 公共地址
    .channel_map        = ADV_CHNL_ALL,         // 在 37,38,39 频道广播
    .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, // 允许任何设备扫描和连接
};
#endif


static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
    esp_err_t err;
    ESP_LOGI(DEMO_TAG, "====> ESP_GAP_BLE_EVT %d <====", event);
    switch (event) {
#if (IBEACON_MODE == IBEACON_SENDER)
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: { // 原始广播数据设置完成事件
        if ((err = param->adv_data_raw_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(DEMO_TAG, "Set raw adv data failed: %s", esp_err_to_name(err));
            return;
        }
        esp_ble_gap_start_advertising(&ble_adv_params);
        break;
    }
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: { // 广播启动完成事件
        //adv start complete event to indicate adv start successfully or failed
        if ((err = param->adv_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(DEMO_TAG, "Adv start failed: %s", esp_err_to_name(err));
        }
        break;
    }
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: { // 广播停止完成事件
        if ((err = param->adv_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS){
            ESP_LOGE(DEMO_TAG, "Adv stop failed: %s", esp_err_to_name(err));
        }
        else {
            ESP_LOGI(DEMO_TAG, "Stop adv successfully");
        }
        break;
    }
#endif
#if (IBEACON_MODE == IBEACON_RECEIVER)
    case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: { // 扫描参数设置完成事件
        //the unit of the duration is second, 0 means scan permanently
        uint32_t duration = 0;
        esp_ble_gap_start_scanning(duration);
        break;
    }
    case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: // 扫描启动完成事件
        //scan start complete event to indicate scan start successfully or failed
        if ((err = param->scan_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
            ESP_LOGE(DEMO_TAG, "Scan start failed: %s", esp_err_to_name(err));
        } else {
            is_scanning = true;
        }
        break;
    case ESP_GAP_BLE_SCAN_RESULT_EVT: { // 扫描结果准备完毕事件
        if (is_scanning == false) { // 如果没有在扫描,则不处理扫描结果
            ESP_LOGW(DEMO_TAG, "Scan is not started yet");
            break;
        }
        esp_ble_gap_cb_param_t *scan_result = (esp_ble_gap_cb_param_t *)param;
        switch (scan_result->scan_rst.search_evt) {
        case ESP_GAP_SEARCH_INQ_RES_EVT: {
            /* 搜索 BLE iBeacon 数据包 */
            if (esp_ble_is_ibeacon_packet(scan_result->scan_rst.ble_adv, scan_result->scan_rst.adv_data_len)) {
                esp_ble_ibeacon_t *ibeacon_data = (esp_ble_ibeacon_t*)(scan_result->scan_rst.ble_adv);
                ESP_LOGI(DEMO_TAG, "----------iBeacon Found----------");
                esp_log_buffer_hex("IBEACON_DEMO: Device address:", scan_result->scan_rst.bda, ESP_BD_ADDR_LEN );
                esp_log_buffer_hex("IBEACON_DEMO: Proximity UUID:", ibeacon_data->ibeacon_vendor.proximity_uuid, ESP_UUID_LEN_128);

                uint16_t major = ENDIAN_CHANGE_U16(ibeacon_data->ibeacon_vendor.major);
                uint16_t minor = ENDIAN_CHANGE_U16(ibeacon_data->ibeacon_vendor.minor);
                ESP_LOGI(DEMO_TAG, "Major: 0x%04x (%d)", major, major);
                ESP_LOGI(DEMO_TAG, "Minor: 0x%04x (%d)", minor, minor);
                ESP_LOGI(DEMO_TAG, "Measured power (RSSI at a 1m distance):%d dbm", ibeacon_data->ibeacon_vendor.measured_power);
                ESP_LOGI(DEMO_TAG, "RSSI of packet:%d dbm", scan_result->scan_rst.rssi);
                esp_err_t err = esp_ble_gap_stop_scanning();
                if (err != ESP_OK) {
                    ESP_LOGE(DEMO_TAG, "Stop scaning failed: %s", esp_err_to_name(err));
                } else {
                    is_scanning = false;
                    ESP_LOGI(DEMO_TAG, "Stop scaning"); 
                }
            }
            break;
        }
        default:
            break;
        }
        break;
    }
    case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: { // 扫描停止完成事件
        if ((err = param->scan_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS){
            ESP_LOGE(DEMO_TAG, "Scan stop failed: %s", esp_err_to_name(err));
        }
        else {
            ESP_LOGI(DEMO_TAG, "Stop scan successfully");
        }
        break;
    }
#endif
    default:
        break;
    }
}


void ble_ibeacon_appRegister(void)
{
    esp_err_t status;

    ESP_LOGI(DEMO_TAG, "register callback");

    /* 注册 GAP 回调函数 */
    if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) {
        ESP_LOGE(DEMO_TAG, "gap register error: %s", esp_err_to_name(status));
        return;
    }

}

void ble_ibeacon_init(void)
{
    /* 初始化蓝牙 HOST 层 */
    esp_bluedroid_init();
    /* 使能蓝牙 HOST 层 */
    esp_bluedroid_enable();
    /* 注册 ibeacon */
    ble_ibeacon_appRegister();
}

void app_main(void)
{
    /* 初始化 NVS */
    ESP_ERROR_CHECK(nvs_flash_init());
    /* 释放经典蓝牙 Control 层内存 */
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
    /* 初始化 BLE Control 层 */
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);
    /* 启动 BLE Control 层 */
    esp_bt_controller_enable(ESP_BT_MODE_BLE);
    /* BLE Ibeacon 功能初始化 */
    ble_ibeacon_init();

#if (IBEACON_MODE == IBEACON_RECEIVER)
    /* 设置扫描参数 */
    esp_ble_gap_set_scan_params(&ble_scan_params);

#elif (IBEACON_MODE == IBEACON_SENDER)
    esp_ble_ibeacon_t ibeacon_adv_data;
    /* 填充 ibeacon 数据 */
    esp_err_t status = esp_ble_config_ibeacon_data (&vendor_config, &ibeacon_adv_data);
    if (status == ESP_OK) {
        for (int i = 0; i < sizeof(ibeacon_adv_data); i++) {
            ESP_LOGI(DEMO_TAG, "ibeacon_adv_data[%d] = 0x%x", i, *((uint8_t*)(&ibeacon_adv_data) + i));
        }
        /* 设置广播原始数据,此函数将会触发 ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT 事件 */
        esp_ble_gap_config_adv_data_raw((uint8_t*)&ibeacon_adv_data, sizeof(ibeacon_adv_data));
    }
    else {
        ESP_LOGE(DEMO_TAG, "Config iBeacon data failed: %s", esp_err_to_name(status));
    }
#endif
}

解析代码流程

  1. 现在,我们开始捋一遍 ibeacon 工程代码顺序以方便我们后续理解。

NVS 分区

  1. 首先是 NVS 分区的初始化,他主要用于存储一些 RF(射频)校准数据,以确保无线通信的性能和稳定性。
  2. 当 ESP32 第一次启动并运行无线功能(如 Wi-Fi 或蓝牙)时,它会进行 RF 校准,以确定在当前硬件和环境条件下的最佳射频参数。
  3. 校准过程的结果会被存储在 NVS 中,这样在后续启动时,设备可以直接使用这些校准数据,避免每次启动都需要重新校准。从而提高设备运行效率。
    /* 初始化 NVS */
    ESP_ERROR_CHECK(nvs_flash_init());
  1. 为了更为方便的理解 NVS 分区在当前的作用,我们可以进行如下实验。我们会发现,只有第一次才会产生 RF 校验数据失败,而后就将不再出现该信息。
  2. 这是因为,在第一次芯片启动时,芯片会去检测 NVS 分区是否存在 RF 校验数据。如果有,那么就马上利用该数据进行启动射频模块。如果没有,那么就先进行 RF 校准,然后将校准数据存储在 NVS 分区,方便后续芯片快速启动。
# 将 Flash 全部擦除,该命令必须执行,否则现象可能无法出现
idf.py erase-flash
# 重新烧录程序
idf.py flash monitor
# 烧录完成后,我们将能够看到这样的日志打印信息
# -------------
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)
# -------------
# 看到这条信息后,复位芯片,我们将看不到这条信息。
idf.py monitor

bluedroid 协议栈启动

  1. 如下为 bluedroid 协议栈启动代码,为什么这样编写,我已经在 ESP32 的 BLE 整体架构 这篇博客讲解,不再进行赘述。
    /* 释放经典蓝牙 Control 层内存 */
    ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
    /* 初始化 BLE Control 层 */
    esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
    esp_bt_controller_init(&bt_cfg);
    /* 启动 BLE Control 层 */
    esp_bt_controller_enable(ESP_BT_MODE_BLE);
    /* BLE Ibeacon 功能初始化 */
    ble_ibeacon_init();

esp_ble_gap_config_adv_data_raw() 设置广播数据

  1. 在设置广播数据之前,我们需要先知道 BLE 的广播数据包格式是什么样的。下面两张图即可展示出广播包的数据格式,具体含义请阅读 BTHome数据格式解析 的 BLE 广播包格式 章节,此处不做赘述。
    在这里插入图片描述
    在这里插入图片描述
  2. 了解了 BLE 广播格式之后,现在我们要做的就是分析一下 ibeacon 的广播包 AD Structure 应该如何编写。
  3. 首先,一个 BLE 广播包必须存在 0x01 类型的广播数据,该广播主要是用于指示当前设备能进行的一些行为。因为我们当前是单模,因此需要置位 bit1 和 bit2,因此 AD Data 为 0x06。
bit描述
0有限可发现模式
1一般可发现模式
2不支持 BR/EDR ,当前为单模设备
3设备同时支持 LE 和 BR/EDR 的 Control
4设备同时支持 LE 和 BR/EDR 的 HOST,需要注意,在Core Specification Supplement 10 该位被取消
5~7保留
  1. 通过上面分析,因此我们可以得到,第一个 flags 应该填入的数据内容如下:
esp_ble_ibeacon_head_t ibeacon_common_head = {
    .flags = {0x02, 0x01, 0x06},
    // ...
};
  1. 分析完 Flags 的数据包,我们再来看看最关键的 ibeacon 数据格式。
    在这里插入图片描述
byte描述
0数据长度,固定为 0x1A
1AD Type,固定为 0xFF 表示厂商自定义数据
2~3固定为 0x004C ,此为 Apple 公司的标识符。可在Assigned Numbers Document (PDF) 7 Company Identifiers 章节查阅
4固定为 0x02,这个是由 Apple 公司定义的数据类型
5ibeacon 数据长度,固定为 0x15
6~21用户定义的 iBeacon UUID,用于唯一标识应用场景
22~23用户自定义的主要值,用于分组或区域标识
24~25用户自定义的次要值,用于更精细的分组或区域标识
26发射功率,可通过该值配合 RSSI 获得当前位置与广播设备距离
  1. 通过上述分析,我们就可以将固定的报头数据先设置好。
// 注意:BLE 广播为小端存储
esp_ble_ibeacon_head_t ibeacon_common_head = {
    .flags = {0x02, 0x01, 0x06},
    .length = 0x1A,         // iBeacon 数据长度为 26 bytes
    .type = 0xFF,           // 自定义数据类型
    .company_id = 0x004C,   // Apple 公司的标识符
    .beacon_type = 0x1502   // 0x02 表示 iBeacon 类型,0x15 表示接下来的 ibeacon 数据长度为 21 bytes。
};
  1. 之后我们就可以设置自己想要广播的数据了。
/* Vendor part of iBeacon data*/
esp_ble_ibeacon_vendor_t vendor_config = {
    .proximity_uuid = ESP_UUID,  // 16 字节,用户定义的 iBeacon UUID,用于唯一标识应用场景。
    .major = ENDIAN_CHANGE_U16(ESP_MAJOR), // 2 字节,用户自定义的主要值,用于分组或区域标识。
    .minor = ENDIAN_CHANGE_U16(ESP_MINOR), // 2 字节,用户自定义的次要值,用于更精细的分组或区域标识。
    .measured_power = 0xC5    // 发射功率为 -59 dBm
};
  1. 一切设置完成后,我们只需要调用 esp_ble_config_ibeacon_data() 函数将数据进行填充即可。该函数其实就是对 Payload 段数据进行了一下配置,看如下日志打印信息就可知道。如果感兴趣,各位可以看一下该函数实现,其实是非常简单的。

注意: esp_ble_config_ibeacon_data() 并不是 ESP32 官方的库函数!!!这个是编写例程的人写的自定义函数!!!

I (538) IBEACON_DEMO: ibeacon_adv_data[0] = 0x2
I (538) IBEACON_DEMO: ibeacon_adv_data[1] = 0x1
I (548) IBEACON_DEMO: ibeacon_adv_data[2] = 0x6
I (548) IBEACON_DEMO: ibeacon_adv_data[3] = 0x1a
I (558) IBEACON_DEMO: ibeacon_adv_data[4] = 0xff
I (558) IBEACON_DEMO: ibeacon_adv_data[5] = 0x4c
I (568) IBEACON_DEMO: ibeacon_adv_data[6] = 0x0
I (568) IBEACON_DEMO: ibeacon_adv_data[7] = 0x2
I (578) IBEACON_DEMO: ibeacon_adv_data[8] = 0x15
I (588) IBEACON_DEMO: ibeacon_adv_data[9] = 0xfd
I (588) IBEACON_DEMO: ibeacon_adv_data[10] = 0xa5
I (598) IBEACON_DEMO: ibeacon_adv_data[11] = 0x6
I (598) IBEACON_DEMO: ibeacon_adv_data[12] = 0x93
I (608) IBEACON_DEMO: ibeacon_adv_data[13] = 0xa4
I (608) IBEACON_DEMO: ibeacon_adv_data[14] = 0xe2
I (618) IBEACON_DEMO: ibeacon_adv_data[15] = 0x4f
I (618) IBEACON_DEMO: ibeacon_adv_data[16] = 0xb1
I (628) IBEACON_DEMO: ibeacon_adv_data[17] = 0xaf
I (628) IBEACON_DEMO: ibeacon_adv_data[18] = 0xcf
I (638) IBEACON_DEMO: ibeacon_adv_data[19] = 0xc6
I (638) IBEACON_DEMO: ibeacon_adv_data[20] = 0xeb
I (648) IBEACON_DEMO: ibeacon_adv_data[21] = 0x7
I (648) IBEACON_DEMO: ibeacon_adv_data[22] = 0x64
I (658) IBEACON_DEMO: ibeacon_adv_data[23] = 0x78
I (668) IBEACON_DEMO: ibeacon_adv_data[24] = 0x25
I (668) IBEACON_DEMO: ibeacon_adv_data[25] = 0x27
I (678) IBEACON_DEMO: ibeacon_adv_data[26] = 0xb7
I (678) IBEACON_DEMO: ibeacon_adv_data[27] = 0xf2
I (688) IBEACON_DEMO: ibeacon_adv_data[28] = 0x6
I (688) IBEACON_DEMO: ibeacon_adv_data[29] = 0xc5
  1. 配置好要广播的数据后,直接调用库函数 esp_ble_gap_config_adv_data_raw() 即可。我们来看看 nrf Connect 结合上述打印信息,就会发现这个函数就是将你需要广播的数据存入 Payload 段

在这里插入图片描述

esp_ble_gap_start_advertising() 启动广播

  1. 在上面,我们调用 esp_ble_gap_config_adv_data_raw() 函数设置原始广播数据完成之后,将会触发 ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT 事件。
  2. 我们可以在该事件中判断原始广播数据是否设置成功,如果设置成功,那么我们即可调用 esp_ble_gap_start_advertising() 函数启动广播。
  3. 在调用 esp_ble_gap_start_advertising() 函数时,需要传入如下结构体。
typedef struct {
    uint16_t                adv_int_min;        /*!< 最小的广告间隔无向和低占空比定向广告。
                                                  取值范围:0x0020 ~ 0x4000  
                                                  默认值:N = 0x0800(1.28秒)
                                                  时间 = N * 0.625 ms 
                                                  时间范围: 20 ms to 10.24 s */
    uint16_t                adv_int_max;        /*!< 无向和低占空比定向广告的最大广告间隔。
                                                  取值范围:0x0020 ~ 0x4000  
                                                  默认值:N = 0x0800(1.28秒)
                                                  时间 = N * 0.625 ms 
                                                  时间范围: 20 ms to 10.24 s */
    esp_ble_adv_type_t      adv_type;           /*!< 广播类型 */
    esp_ble_addr_type_t     own_addr_type;      /*!< 所有者蓝牙设备地址类型 */
    esp_bd_addr_t           peer_addr;          /*!< 对端设备蓝牙设备地址 */
    esp_ble_addr_type_t     peer_addr_type;     /*!< 对端设备蓝牙设备地址类型,仅支持公网地址类型和随机地址类型 */
    esp_ble_adv_channel_t   channel_map;        /*!< 广告频道图 */
    esp_ble_adv_filter_t    adv_filter_policy;  /*!< 广告过滤策略 */
} esp_ble_adv_params_t;
  1. 如果是对 BLE HCI 比较熟悉的朋友就会发现,这个函数其实就是让 HOST 层向 Control 层发送 LE Set Advertising Parameters command 命令。
  2. 我们可以打开 Core 5.3 的 2353 页,会发现这个命令传入的参数和 esp_ble_gap_start_advertising() 函数传入的参数一模一样。

在这里插入图片描述
12. 关于这个命令,规范书中的描述如下:

  1. HCI_LE_Set_Advertising_Parameters 命令用于设置广播参数。
  2. advertissing_interval_min 小于或等于 Advertising_Interval_Max 。advertissing_interval_min 和 Advertising_Interval_Max 不应该是相同的值,以使控制器能够确定给定其他活动的最佳广告间隔。
  3. 对于高占空比定向广告,即当 Advertising_Type 为 0x01 (ADV_DIRECT_IND,高占空比)时,不使用 Advertising_Interval_Min 和 Advertising_Interval_Max 参数,应忽略。
  4. Advertising_Type 用于确定在启用发布时用于发布的数据包类型。
  5. “Own_Address_Type” 参数表示发布报文使用的地址类型。
  6. 如果 Own_Address_Type 等于 0x02 或 0x03, Peer_Address 参数包含对端设备的身份地址,Peer_Address_Type 参数包含对端设备的身份类型(即0x00或0x01)。这些参数用于在解析表中定位相应的本地 IRK;此 IRK 用于生成广告中使用的自己的地址。
  7. 如果是定向广播,即当 Advertising_Type 设置为 0x01 (ADV_DIRECT_IND,高占空比)或0x04 (ADV_DIRECT_IND,低占空比模式)时,则 Peer_Address_Type 和 Peer_Address 有效。
  8. 如果 Own_Address_Type 等于 0x02 或 0x03,Control 使用 Peer_Address 参数中包含的对端设备的身份地址和 Peer_Address_Type 参数中包含的对端设备的身份地址类型(即0x00或0x01)对应的对端设备的 IRK 生成对端设备的可解析私有地址。
  9. Advertising_Channel_Map 是一个位字段,表示发送广播报文时应使用的广告通道索引。在 Advertising_Channel_Map 参数中至少要设置一个通道位。
  10. 当定向广播被启用时,Advertising_Filter_Policy 参数应该被忽略。
  11. 如果 Control 启用了广播功能,则 HOST 不得发出该命令;如果启用了广播功能,则应使用 "命令禁用 "错误代码。
  12. 如果 HOST 提供的发布间隔范围(Advertising_Interval_Min, Advertising_Interval_Max)超出了 Control 支持的发布间隔范围,则 Control 将返回不支持的特征或参数值(0x11)错误码。
adv_int_min 和 adv_int_max
  1. adv_int_minadv_int_max 用于指示广播的时间间隔。bluedroid 协议栈会选取该范围内的任意值作为广播间隔,但是实测后发现,他一般选取 adv_int_max 作为广播间隔

  2. adv_int_minadv_int_max 范围都要求在 0x0020 ~ 0x4000 之间,如果没有设置该值,默认为 0x0800 (1.28 s) 。时间计算公式为 Time = N * 0.625 ms,既最终的广播时间范围为 20 ms to 10.24 s。
    在这里插入图片描述

  3. 此时有人可能会有疑问了,我代码里面命令设置的最大广播间隔时间为 40ms 啊,怎么抓包数据有些广播间隔为 48ms 多呢?

  4. 这个就需要涉及到 SIG 规定的广播间隔内容了。SIG 规定,为了解决多个设备同时广播时的冲突问题,从而避免广播包的碰撞,确保更加可靠的广播和接收,在 BLE 广播过程中,存在一个 随机延迟(random delay),通常称为 广播延迟(advertising delay)

static esp_ble_adv_params_t ble_adv_params = {
    .adv_int_min        = 0x20,                 // 0x20*0.625ms=20ms,Range: 0x0020 to 0x4000 (20ms to 10240ms)
    .adv_int_max        = 0x40,                 // 0x40*0.625ms=40ms
	// ...
};

在这里插入图片描述

  1. 如果我们希望一个确切的广播间隔,那么就可以让adv_int_minadv_int_max 相等即可。

注: adv_int_minadv_int_max 相等只是让 advInterval 为我们设定的固定值,advDelay 依旧会存在!而且 SIG 不建议让 adv_int_minadv_int_max 相等!

  1. adv_int_max 必须大于 adv_int_min ,否则就会出现如下报错。(部分日志信息是本例程用于调试写的)
I (658) IBEACON_DEMO: ====> ESP_GAP_BLE_EVT 4 <====
E (668) BT_APPL: bta_dm_ble_set_adv_params_all(), fail to set ble adv params.
E (678) BT_HCI: hci write adv params error 0x12
I (678) IBEACON_DEMO: ====> ESP_GAP_BLE_EVT 6 <====
E (688) IBEACON_DEMO: Adv start failed: ERROR
adv_type
  1. 该参数用于设置 BLE 的广播类型。
typedef enum {
    ADV_TYPE_IND                = 0x00, // 可连接和可扫描的无定向广告(ADV_IND)(默认)
    ADV_TYPE_DIRECT_IND_HIGH    = 0x01, // 可连接的高占空比定向广告(ADV_DIRECT_IND,高占空比)
    ADV_TYPE_SCAN_IND           = 0x02, // 可扫描的非定向广告(ADV_SCAN_IND)
    ADV_TYPE_NONCONN_IND        = 0x03, // 不可连接非定向广告(ADV_NONCONN_IND)
    ADV_TYPE_DIRECT_IND_LOW     = 0x04, // 可连接低占空比定向广告(ADV_DIRECT_IND,低占空比)
} esp_ble_adv_type_t;
  1. 如果是学习 《低功耗权威指南》或者其他类型的熟记中会发现,广播类型只有四种:通用广播定向广播不可连接广播可发现广播。这个时候有人肯定会疑问,为什么这里的定向广播有两种。
  2. 这个就设计到版本更替的问题了,《低功耗权威指南》是用于讲解 BLE 4.0 的权威书籍,但是 ESP32 的 Bluedroid 是支持 BLE 5.0 的。从 BLE 5.0 开始,广播拥有第五种类型,即可连接低占空比定向广告
  3. 现在我就开始分别介绍一下这几种广播的区别:
广播类型描述关闭方式
ADV_TYPE_IND这种广播是最常用的广播方式。进行通用广播的设备是能够被扫描,被连接的调用 esp_ble_gap_stop_advertising() 函数主动关闭广播,或者连接建立。
ADV_TYPE_DIRECT_IND_HIGH该广播类型主要针对希望快速建立连接的需求开启定向广播后,完整的广播事件必须每 3.75ms 重复一次,正因如此之快的广播速率,导致该广播包将在占满整个广播信道,进而导致该区域内其他设备无法进行广播,因此定向广播不可以持续超过 1.28s 之上该广播只有两种结束方式,第一种是收到指定的对端设备连接请求,第二种是超过 1.28s。一旦超过 1.28s 还没有建立连接,Control 层应该向 HOST 层发送 Advertising Timeout 事件,告知广播超时。(这里需要注意,ESP32 的该广播事件似乎有 bug,并不会上报广播超时事件)
ADV_TYPE_SCAN_IND该类型广播不可以用于发起连接,但允许其他设备进行扫描,可以理解为将连接功能去除的 ADV_TYPE_IND调用 esp_ble_gap_stop_advertising() 函数主动关闭广播。
ADV_TYPE_NONCONN_IND该类型广播针对的是不想被连接,仅进行广播的设备,例如本文的 Ibeacon 设备。这也是唯一可用于只有发射机而没有接收机设备的广播类型调用 esp_ble_gap_stop_advertising() 函数主动关闭广播。
ADV_TYPE_DIRECT_IND_LOW该广播属于定向广播,但是并不会像 ADV_TYPE_DIRECT_IND_HIGH 那样快速的将整个广播信道占满,他是会在 adv_int_minadv_int_max 范围内保持一定的频率进行广播。调用 esp_ble_gap_stop_advertising() 函数主动关闭广播,或者和指定的对端设备连接建立。
own_addr_type
  1. 这里设置设备的地址类型,没有特殊需求,直接设置为公共地址即可。
typedef enum {
    BLE_ADDR_TYPE_PUBLIC        = 0x00,     /*!< 公共地址 */
    BLE_ADDR_TYPE_RANDOM        = 0x01,     /*!< 随机设备地址。要设置此地址,请使用esp_ble_gap_set_rand_addr(esp_bd_addr_t rand_addr)函数 */
    BLE_ADDR_TYPE_RPA_PUBLIC    = 0x02,     /*!< 具有公共身份地址的可解析私有地址(RPA) */
    BLE_ADDR_TYPE_RPA_RANDOM    = 0x03,     /*!< 带有随机身份地址的可解析私有地址(RPA)。要设置此地址,请使用esp_ble_gap_set_rand_addr(esp_bd_addr_t rand_addr)函数 */
} esp_ble_addr_type_t;

在这里插入图片描述

设备地址类型描述
公共地址(BLE_ADDR_TYPE_PUBLIC)全球唯一且固定的地址,需要向 IEEE 组织购买。因为全球唯一,因此容易被跟踪
静态地址(BLE_ADDR_TYPE_RANDOM)自己定义,上电初始化完成后不能再进行修改。每次芯片启动都可能会更换地址
可解析地址(BLE_ADDR_TYPE_RPA_PUBLIC)通讯双方共享 IRK ,生成随机可解析私有地址。只有拥有广播者的 IRK 时,才能跟踪其广播活动。目的是为了防止恶意第三方跟踪蓝牙设备。
不可解析地址(BLE_ADDR_TYPE_RPA_RANDOM)定期更新地址,SIG 推荐15 min 更新一次,更新时间间隔不要超过 1 小时。通常用于设备只需要一次性广播数据,且不需要被接收方识别或跟踪。例如,发送传感器数据的设备、匿名设备发现、或仅广播某些临时数据的场景。
peer_addr
  1. 对端设备 MAC 地址。只有当 adv_typeADV_TYPE_DIRECT_IND_HIGH 或者 ADV_TYPE_DIRECT_IND_LOW 才有效。
peer_addr_type
  1. 对端设备地址类型。只有当 adv_typeADV_TYPE_DIRECT_IND_HIGH 或者 ADV_TYPE_DIRECT_IND_LOW 才有效。
channel_map
  1. 广播的信道。你可以设置在指定的广播信道进行广播,或者是所有广播信道都广播。
typedef enum {
    ADV_CHNL_37     = 0x01,  // 仅在 37 信道广播
    ADV_CHNL_38     = 0x02,  // 仅在 38 信道广播
    ADV_CHNL_39     = 0x04,  // 仅在 39 信道广播
    ADV_CHNL_ALL    = 0x07,  // 在 37,38,39 信道广播
} esp_ble_adv_channel_t;
adv_filter_policy
  1. 因为我们是 Ibeacon 所有需要让所有人能够扫描到,因此设置为 ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY
  2. 白名单是指的特定的对端设备地址,我们可以调用 esp_ble_gap_update_whitelist() 函数更新白名单。

如果开发过 Nordic 的相关芯片,会发现一个问题,怎么 Nordic 的芯片还可以过滤 UUID,名称,外观等信息呢?这个就是 Nordic 的协议栈中软件实现的,和 SIG 相关规定无关,如果你希望有这样的功能,可以自行实现。

typedef enum {
    // 允许任何人的扫描和连接请求
    ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY  = 0x00,
    // 只允许来自白名单设备的扫描请求和来自任何人的连接请求
    ADV_FILTER_ALLOW_SCAN_WLST_CON_ANY,
    // 只允许任何人的扫描请求和来自白名单设备的连接请求
    ADV_FILTER_ALLOW_SCAN_ANY_CON_WLST,
    // 只允许来自白名单设备的扫描和连接请求
    ADV_FILTER_ALLOW_SCAN_WLST_CON_WLST,
} esp_ble_adv_filter_t;

广播启动完成

  1. 当调用 esp_ble_gap_start_advertising() 函数之后,将会触发 ESP_GAP_BLE_ADV_START_COMPLETE_EVT 事件。在该事件中,我们可以知道广播是否完成。
  2. 当广播结束之后,将会触发 ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT 事件,不过当前的 Ibeacon 例程中,不会停止广播,因此该事件不会触发。

参考

  1. Apple Ibeacon 官方介绍
  2. 低功耗蓝牙开发者手册
  3. BTHome数据格式解析
  4. nRF52832蓝牙iBeacon广播
  5. 谷雨文档中心:BLE技术揭秘
  6. Core 5.3

http://www.niftyadmin.cn/n/5681191.html

相关文章

深度学习自编码器 - 提供发现潜在原因的线索篇

序言 在探索复杂数据背后的秘密时&#xff0c;深度学习如同一把锐利的钥匙&#xff0c;特别是其核心的表示学习机制&#xff0c;为我们打开了一扇通往未知世界的大门。表示学习不仅仅是数据的简单编码或转换&#xff0c;它更是深度挖掘数据内在结构、关系与规律的过程。在这一…

web前端-CSS引入方式

一、内部样式表 内部样式表(内嵌样式表)是写到html页面内部,是将所有的 CSS 代码抽取出来,单独放到一个<styie>标签中。 注意: ① <style>标签理论上可以放在 HTML文档的任何地方&#xff0c;但一般会放在文档的<head>标签中 ② 通过此种方式&#xff0c;可…

【JavaEE初阶】深入理解wait和notify以及线程饿死的解决

前言&#xff1a; &#x1f308;上期博客&#xff1a;【JavaEE初阶】深入解析死锁的产生和避免以及内存不可见问题-CSDN博客 &#x1f525;感兴趣的小伙伴看一看小编主页&#xff1a;【JavaEE初阶】深入解析死锁的产生和避免以及内存不可见问题-CSDN博客 ⭐️小编会在后端开…

Reactor 反应堆模式

Reactor 反应堆模式 1、概念 Reactor&#xff08;反应堆&#xff09;模式是一种事件驱动的设计模式&#xff0c;通常用于处理高并发的I/O操作&#xff0c;尤其是在服务器或网络编程中。它基于事件多路复用机制&#xff0c;使得单个线程能够同时管理大量并发连接&#xff0c;而…

el-table给列加单位,表头加样式,加斑马纹

<el-table ref"table" class"dataTable" :data"detailList" :header-cell-style"tableHeaderColor" :row-class-name"tableRowClassName" highlight-current-row><el-table-column label"序号" al…

git 基本原理

文章内容来源于视频 举个案例&#xff0c;家族里面有一本记载祖传秘籍的菊花宝典&#xff0c;这本菊花宝典的正本存储在家族祠堂里面&#xff0c;每一个家庭从正本复制一本存在自己家中&#xff0c;称为副本。这个过程称为clone 一个家庭需要再菊花宝典中添加技能&#xff0c…

Django 配置邮箱服务,实现发送信息到指定邮箱

一、这里以qq邮箱为例&#xff0c;打开qq邮箱的SMTP服务 二、django项目目录设置setting.py 文件 setting.py 添加如下内容&#xff1a; # 发送邮件相关配置 EMAIL_BACKEND django.core.mail.backends.smtp.EmailBackend EMAIL_USE_TLS True EMAIL_HOST smtp.qq.com EMAIL…

Vue 技术入门 day1 模版语法、数据绑定、事件处理、计算属性与监视、class和style绑定、条件渲染v-if/v-show、列表渲染v-for

目录 1.Vue 核心 1.1. Vue 简介 1.1.1 介绍与描述 1.1.2 Vue 的特点 1.2 模板语法 1.2.1 模板的分类 1.2.2 插值语法 1.2.3 指令语法 1.2.4 实例 1.3 数据绑定 1.3.1 单向数据绑定 1.3.2 双向数据绑定 1.3.3 MVVM 模型 1.3.4 data与el的2种写法 1.3.5 实例 1.3.…