ESP32 Bluedroid 篇(1)—— ibeacon 广播
前言
- 前面我们已经了解了 ESP32 的 BLE 整体架构,现在我们开始实际学习一下Bluedroid 从机篇的广播和扫描。
- 本文将会以 ble_ibeacon demo 为例子进行讲解,需要注意的一点是。ibeacon 分为两个部分,一个是作为广播者,一个是作为观察者。
IBEACON_RECEIVER
这个宏表示作为观察者,IBEACON_SENDER
这个宏被置 1 表示为广播者。 - 需要注意的一点是,本文先仅介绍广播相关内容
ibeacon 介绍
ibeacon 是什么?
- 作为一名初学者,当听到 ibeacon 时候,大概率是一脸懵逼的。即使网上搜索大量资料,没有亲身体验,也是一头雾水。为了方便各位理解,就以我来上海实习,周末逛的第一个景点 – 豫园 为例子进行分析。
- 当我们进入景点,肯定会有一个二维码建议我们扫描,然后之后就会有电子讲解功能。例如我现在扫描了豫园的二维码,打开了一个微信小程序,此时微信小程序上就能够显示出我的位置在哪里。
3. 如果你移动到一个地方,该小程序就会进行相关讲解当前景点的一些历史文化信息。此时,各位有没有想过一个问题,该小程序,是如何精确的知道我们当前是在哪个景点呢?
4. 此时,就是利用的 ibeacon 技术进行的。如果你有兴趣的话,可以在走到某个景点,发现微信小程序播报讲解时刻停下来,然后在这个附近十米内的范围转转,会惊奇的发现,一些地方藏有这种小方块。
5. 这个小方块,就是本文要进行讲解的,ESP32 作为广播者的功能。而你手机,就是充当的观察者。
ibeacon 有什么用?
- 现在我们明白了 ibeacon 技术大概是什么东西了。那么这个有什么作用呢?从上面的例子我们就可以很好的知道,室内定位,广播信息。
- 当前,室内定位技术一直是一项值得探索的技术,ibeacon 可以说提供了一个不错的选择(不过个人感觉目前 BLE 室内定位更多的是聚焦于 AOA)。
- 同样,在商场,我们只需要打开微信小程序走到哪家店铺,就可以直接查看那家店铺的商品信息,这样一定程度上可以方便用户挑选商品。
ibeacon 工程介绍
工程源码
- 我们先拷贝 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, // 允许任何设备扫描和连接
};
#endifstatic 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 failedif ((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 permanentlyuint32_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 failedif ((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;}
#endifdefault: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
}
解析代码流程
- 现在,我们开始捋一遍 ibeacon 工程代码顺序以方便我们后续理解。
NVS 分区
- 首先是 NVS 分区的初始化,他主要用于存储一些 RF(射频)校准数据,以确保无线通信的性能和稳定性。
- 当 ESP32 第一次启动并运行无线功能(如 Wi-Fi 或蓝牙)时,它会进行 RF 校准,以确定在当前硬件和环境条件下的最佳射频参数。
- 校准过程的结果会被存储在 NVS 中,这样在后续启动时,设备可以直接使用这些校准数据,避免每次启动都需要重新校准。从而提高设备运行效率。
/* 初始化 NVS */ESP_ERROR_CHECK(nvs_flash_init());
- 为了更为方便的理解 NVS 分区在当前的作用,我们可以进行如下实验。我们会发现,只有第一次才会产生 RF 校验数据失败,而后就将不再出现该信息。
- 这是因为,在第一次芯片启动时,芯片会去检测 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 协议栈启动
- 如下为 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() 设置广播数据
- 在设置广播数据之前,我们需要先知道 BLE 的广播数据包格式是什么样的。下面两张图即可展示出广播包的数据格式,具体含义请阅读 BTHome数据格式解析 的 BLE 广播包格式 章节,此处不做赘述。
- 了解了 BLE 广播格式之后,现在我们要做的就是分析一下 ibeacon 的广播包 AD Structure 应该如何编写。
- 首先,一个 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 | 保留 |
- 通过上面分析,因此我们可以得到,第一个 flags 应该填入的数据内容如下:
esp_ble_ibeacon_head_t ibeacon_common_head = {.flags = {0x02, 0x01, 0x06},// ...
};
- 分析完 Flags 的数据包,我们再来看看最关键的 ibeacon 数据格式。
byte | 描述 |
---|---|
0 | 数据长度,固定为 0x1A |
1 | AD Type,固定为 0xFF 表示厂商自定义数据 |
2~3 | 固定为 0x004C ,此为 Apple 公司的标识符。可在Assigned Numbers Document (PDF) 7 Company Identifiers 章节查阅 |
4 | 固定为 0x02,这个是由 Apple 公司定义的数据类型 |
5 | ibeacon 数据长度,固定为 0x15 |
6~21 | 用户定义的 iBeacon UUID,用于唯一标识应用场景 |
22~23 | 用户自定义的主要值,用于分组或区域标识 |
24~25 | 用户自定义的次要值,用于更精细的分组或区域标识 |
26 | 发射功率,可通过该值配合 RSSI 获得当前位置与广播设备距离 |
- 通过上述分析,我们就可以将固定的报头数据先设置好。
// 注意: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。
};
- 之后我们就可以设置自己想要广播的数据了。
/* 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
};
- 一切设置完成后,我们只需要调用
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
- 配置好要广播的数据后,直接调用库函数
esp_ble_gap_config_adv_data_raw()
即可。我们来看看 nrf Connect 结合上述打印信息,就会发现这个函数就是将你需要广播的数据存入 Payload 段 。
esp_ble_gap_start_advertising() 启动广播
- 在上面,我们调用
esp_ble_gap_config_adv_data_raw()
函数设置原始广播数据完成之后,将会触发ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT
事件。 - 我们可以在该事件中判断原始广播数据是否设置成功,如果设置成功,那么我们即可调用
esp_ble_gap_start_advertising()
函数启动广播。 - 在调用
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;
- 如果是对 BLE HCI 比较熟悉的朋友就会发现,这个函数其实就是让 HOST 层向 Control 层发送 LE Set Advertising Parameters command 命令。
- 我们可以打开 Core 5.3 的 2353 页,会发现这个命令传入的参数和
esp_ble_gap_start_advertising()
函数传入的参数一模一样。
12. 关于这个命令,规范书中的描述如下:
- HCI_LE_Set_Advertising_Parameters 命令用于设置广播参数。
- advertissing_interval_min 小于或等于 Advertising_Interval_Max 。advertissing_interval_min 和 Advertising_Interval_Max 不应该是相同的值,以使控制器能够确定给定其他活动的最佳广告间隔。
- 对于高占空比定向广告,即当 Advertising_Type 为 0x01 (ADV_DIRECT_IND,高占空比)时,不使用 Advertising_Interval_Min 和 Advertising_Interval_Max 参数,应忽略。
- Advertising_Type 用于确定在启用发布时用于发布的数据包类型。
- “Own_Address_Type” 参数表示发布报文使用的地址类型。
- 如果 Own_Address_Type 等于 0x02 或 0x03, Peer_Address 参数包含对端设备的身份地址,Peer_Address_Type 参数包含对端设备的身份类型(即0x00或0x01)。这些参数用于在解析表中定位相应的本地 IRK;此 IRK 用于生成广告中使用的自己的地址。
- 如果是定向广播,即当 Advertising_Type 设置为 0x01 (ADV_DIRECT_IND,高占空比)或0x04 (ADV_DIRECT_IND,低占空比模式)时,则 Peer_Address_Type 和 Peer_Address 有效。
- 如果 Own_Address_Type 等于 0x02 或 0x03,Control 使用 Peer_Address 参数中包含的对端设备的身份地址和 Peer_Address_Type 参数中包含的对端设备的身份地址类型(即0x00或0x01)对应的对端设备的 IRK 生成对端设备的可解析私有地址。
- Advertising_Channel_Map 是一个位字段,表示发送广播报文时应使用的广告通道索引。在 Advertising_Channel_Map 参数中至少要设置一个通道位。
- 当定向广播被启用时,Advertising_Filter_Policy 参数应该被忽略。
- 如果 Control 启用了广播功能,则 HOST 不得发出该命令;如果启用了广播功能,则应使用 "命令禁用 "错误代码。
- 如果 HOST 提供的发布间隔范围(Advertising_Interval_Min, Advertising_Interval_Max)超出了 Control 支持的发布间隔范围,则 Control 将返回不支持的特征或参数值(0x11)错误码。
adv_int_min 和 adv_int_max
-
adv_int_min
和adv_int_max
用于指示广播的时间间隔。bluedroid 协议栈会选取该范围内的任意值作为广播间隔,但是实测后发现,他一般选取adv_int_max
作为广播间隔。 -
adv_int_min
和adv_int_max
范围都要求在 0x0020 ~ 0x4000 之间,如果没有设置该值,默认为 0x0800 (1.28 s) 。时间计算公式为 Time = N * 0.625 ms,既最终的广播时间范围为 20 ms to 10.24 s。
-
此时有人可能会有疑问了,我代码里面命令设置的最大广播间隔时间为 40ms 啊,怎么抓包数据有些广播间隔为 48ms 多呢?
-
这个就需要涉及到 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// ...
};
- 如果我们希望一个确切的广播间隔,那么就可以让
adv_int_min
和adv_int_max
相等即可。
注:
adv_int_min
和adv_int_max
相等只是让 advInterval 为我们设定的固定值,advDelay 依旧会存在!而且 SIG 不建议让adv_int_min
和adv_int_max
相等!
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
- 该参数用于设置 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;
- 如果是学习 《低功耗权威指南》或者其他类型的熟记中会发现,广播类型只有四种:通用广播,定向广播,不可连接广播,可发现广播。这个时候有人肯定会疑问,为什么这里的定向广播有两种。
- 这个就设计到版本更替的问题了,《低功耗权威指南》是用于讲解 BLE 4.0 的权威书籍,但是 ESP32 的 Bluedroid 是支持 BLE 5.0 的。从 BLE 5.0 开始,广播拥有第五种类型,即可连接低占空比定向广告。
- 现在我就开始分别介绍一下这几种广播的区别:
广播类型 | 描述 | 关闭方式 |
---|---|---|
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_min 和 adv_int_max 范围内保持一定的频率进行广播。 | 调用 esp_ble_gap_stop_advertising() 函数主动关闭广播,或者和指定的对端设备连接建立。 |
own_addr_type
- 这里设置设备的地址类型,没有特殊需求,直接设置为公共地址即可。
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
- 对端设备 MAC 地址。只有当
adv_type
为ADV_TYPE_DIRECT_IND_HIGH
或者ADV_TYPE_DIRECT_IND_LOW
才有效。
peer_addr_type
- 对端设备地址类型。只有当
adv_type
为ADV_TYPE_DIRECT_IND_HIGH
或者ADV_TYPE_DIRECT_IND_LOW
才有效。
channel_map
- 广播的信道。你可以设置在指定的广播信道进行广播,或者是所有广播信道都广播。
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
- 因为我们是 Ibeacon 所有需要让所有人能够扫描到,因此设置为
ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY
。 - 白名单是指的特定的对端设备地址,我们可以调用
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;
广播启动完成
- 当调用
esp_ble_gap_start_advertising()
函数之后,将会触发 ESP_GAP_BLE_ADV_START_COMPLETE_EVT 事件。在该事件中,我们可以知道广播是否完成。 - 当广播结束之后,将会触发
ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT
事件,不过当前的 Ibeacon 例程中,不会停止广播,因此该事件不会触发。
参考
- Apple Ibeacon 官方介绍
- 低功耗蓝牙开发者手册
- BTHome数据格式解析
- nRF52832蓝牙iBeacon广播
- 谷雨文档中心:BLE技术揭秘
- Core 5.3
相关文章:
ESP32 Bluedroid 篇(1)—— ibeacon 广播
前言 前面我们已经了解了 ESP32 的 BLE 整体架构,现在我们开始实际学习一下Bluedroid 从机篇的广播和扫描。本文将会以 ble_ibeacon demo 为例子进行讲解,需要注意的一点是。ibeacon 分为两个部分,一个是作为广播者,一个是作为观…...
【通配符】粗浅学习
1 背景说明 首先要注意,通配符中的符号和正则表达式中的特殊符号具备不同的匹配意义,例如:*在正则表达式中表示里面是指匹配前面的子表达式0次或者多次,而在通配符领域则是表示代表0个到无穷个任意字符。 此外,要注意…...
Spring MVC 常用注解
目录 基础概念 常用注解介绍 基础概念 1、MVC :代表一种软件架构设计思想,通俗的理解:客户端发送请求到后台服务器的Controller(C),控制器调用Model(M)来处理业务逻辑,处理完成后,返回处理后的数据到Vie…...
水泵模块(5V STM32)
目录 一、介绍 二、传感器原理 1.尺寸介绍 2.继电器控制水泵电路原理图 三、程序设计 main.c文件 bump.h文件 bump.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 水泵模块(bump)通常是指用于液体输送系统的组件,它负责将水或其他流体从低处提…...
需求6:如何写一个后端接口?
这两天一直在对之前做的工作做梳理总结,不过前两天我都是在总结一些bug的问题。尽管有些bug问题我还没写文章,但是,我今天不得不先停下对bug的总结了。因为在国庆之后,我需要自己开发一个IT资产管理的功能,这个功能需要…...
《Linux从小白到高手》理论篇(五):文件权限控制及文件操作相关的命令
本篇介绍Linux文件权限控制及文件操作相关的命令,看完本文,有关Linux文件权限控制及文件操作相关的常用命令你就掌握了99%了。 文件权限 在介绍文件权限之前先来复习下Linux的文件类型,始终记住那句话:Linux系统下,一…...
异常场景分析
优质博文:IT-BLOG-CN 为了防止黑客从前台异常信息,对系统进行攻击。同时,为了提高用户体验,我们都会都抛出的异常进行拦截处理。 一、异常处理类 Java把异常当做是破坏正常流程的一个事件,当事件发生后,…...
Leetcode: 0001-0010题速览
Leetcode: 0001-0010题速览 本文材料来自于LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解 遵从开源协议为知识共享 版权归属-相同方式…...
计算机的错误计算(一百一十二)
摘要 计算机的错误计算(六十三)与(六十八)以及(六十九)分别探讨了大数与 附近数以及 附近数 的余切函数的计算精度问题。本节讨论余切序列(即迭代 )的计算精度问题。 余切序列是指…...
C++基础(7)——STL简介及string类
目录 1.STL简介 1.1什么是 1.2STL的历史版本 1.3STL的六大组件 编辑 1.4有用的网址 2.string类 2.1string的多种定义方式 2.2string的插入 2.2.1尾插(push_back) 2.2.2insert插入 2.3拼接(append) 2.4删除 2.4.1尾…...
配置Nginx以支持通过HTTPS回源到CDN
要配置Nginx以支持通过HTTPS回源到CDN,你需要确保Nginx已正确配置SSL,并且能够处理来自CDN的HTTPS请求。以下是一个简化的Nginx配置示例,它配置了SSL并设置了代理服务器参数以回源到CDN: server {listen 443 ssl;server_name you…...
yolov10+strongsort的目标跟踪实现
此次yolov10deepsort不论是准确率还是稳定性,再次超越了之前的yolodeepsort系列。 yolov10介绍——实时端到端物体检测 YOLOv10 是清华大学研究人员在 UltralyticsPython 清华大学的研究人员在 YOLOv10软件包的基础上,引入了一种新的实时目标检测…...
C# 字符与字符串
本课要点: 1、字符类Char的使用 2、字符串类String的使用 3、可变字符串****StringBuilder 4、常见错误 一 何时用到字符与字符串 问题: 输出C#**课考试最高分:**98.5 输出最高分学生姓名:张三 输出最高分学生性别&#x…...
在Ubuntu 16.04上使用LEMP安装WordPress的方法
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 简介 WordPress 是互联网上最流行的 CMS(内容管理系统)。它允许您在 MySQL 后端和 PHP 处理的基础上轻松设置灵…...
显示器放大后,大漠识图识色坐标偏移解决方法
原因分析: 显示器分辨率较高,DPI设置放大125% or 150% or 200%,游戏打开时也会默认会根据显示器的放大比例自行放大,但是大漠综合管理工具抓图不会放大; 解决方法: 1、大漠综合管理…...
C++容器之list基本使用
目录 前言 一、list的介绍? 二、使用 1.list的构造 2.list iterator的使用 3.list capacity 🥇 empty 🥇size 4.list element access 🥇 front 🥇 back 5.list modifiers 🥇 push_front 🥇 po…...
Redis-哨兵
概念 Redis Sentinel 相关名词解释 注意: 哨兵机制不负责存储数据,只是对其它的redis-server进程起到监控的作用哨兵节点,也会搞一个集合,防止一个挂了 ⼈⼯恢复主节点故障 用户监控: 实际开发中,对于服务器后端开发,监控程序,是很重要的 服务器长期运行,总会有一些意外,…...
Pikachu-Sql-Inject - 基于时间的盲注
基于时间的盲注: 就是前端的基于time 的盲注,什么错误信息都看不到,但是还可以通过特定的输入,判断后台的执行时间,从而确定注入。 mysql 里函数sleep() 是延时的意思,sleep(10)就是数据库延时10 秒返回内…...
JAVA开源项目 旅游管理系统 计算机毕业设计
本文项目编号 T 063 ,文末自助获取源码 \color{red}{T063,文末自助获取源码} T063,文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析5.4 用例设计 六、核…...
景联文科技入选《2024中国AI大模型产业图谱2.0版》数据集代表厂商
近日,大数据产业领域头部媒体数据猿携手上海大数据联盟联合发布了备受瞩目的《2024中国AI大模型产业图谱2.0版》。以大数据与AI为代表的智能技术为主要视角,聚焦全产业链,为业内提供更为专业直观的行业指导。 景联文科技凭借高质量数据集&…...
【C语言】内存函数的使用和模拟实现
文章目录 一、memcpy的使用和模拟实现二、memmove的使用和模拟实现三、memset的使用四、memcmp的使用 一、memcpy的使用和模拟实现 在之前我们学习了使用和模拟实现strncpy函数,它是一个字符串函数,用来按照给定的字节个数来拷贝字符串,那么问…...
在WPF中实现多语言切换的四种方式
在WPF中有多种方式可以实现多语言,这里提供几种常用的方式。 一、使用XML实现多语言切换 使用XML实现多语言的思路就是使用XML作为绑定的数据源。主要用到XmlDataProvider类. 使用XmlDataProvider.Source属性指定XML文件的路径或通过XmlDataProvider.Document指定…...
30min 的OpenCV learning Note
1.安装python和pycharm与环境搭配 打开Windows终端:(winR)(一般使用清华镜像网站安装库比较快) pip install opencv-contrib-python -i https://pypi.mirrors.ustc.edu.cn/simple 或者 python -m pip install open…...
C--编译和链接见解
欢迎各位看官!如果您觉得这篇文章对您有帮助的话 欢迎您分享给更多人哦 感谢大家的点赞收藏评论 感谢各位看官的支持!!! 一:翻译环境和运行环境 在ANSIIC的任何一种实现中,存在两个不同的环境1,…...
【QT Quick】基础语法:基础类与控件
QML 的基础类和控件中,我们可以看到主要的几个分类:基础控件类、窗口类以及组件类。以下是对这些控件及其属性、继承关系等的详细讲解: 控件关系总结 QtObject 是所有 QML 对象的基类。它定义了基础属性,主要用于逻辑和数据封装…...
使用 SSH 连接 Docker 服务器:IntelliJ IDEA 高效配置与操作指南
使用 SSH 连接 Docker 服务器:IntelliJ IDEA 高效配置与操作指南 本文详细介绍了如何在 2375 端口未开放的情况下,通过 SSH 连接 Docker 服务器并在 Idea 中进行开发。通过修改用户权限、生成密钥对以及配置 SSH 访问,用户可以安全地远程操作…...
Gas费用是什么?
Gas费用是什么? 每5个Byte 需要1个GasGasLimit 用来限制合约最多执行多少次运算GasPrice 每次计算需要支付的费用在Web3的语境中,尤其是在以太坊(Ethereum)这样的区块链平台上,Gas费是一个核心概念。以下是关于Gas费的详细解释: 1. 定义 Gas是以太坊网络上的计算单位,…...
大语言模型(LLM)的子模块拆拆分进行联邦学习;大语言模型按照多头(Multi-Head)拆分进行联邦学习
目录 大语言模型(LLM)的子模块拆拆分进行联邦学习 方式概述 简单示例 大语言模型按照多头(Multi-Head)拆分进行联邦学习 场景设定 多头拆分与联邦学习 示例说明 大语言模型(LLM)的子模块拆拆分进行联邦学习 大语言模型(LLM)的子模块拆分进行联邦学习,主要涉及…...
Qt 概述
1. Qlabel HelloWorld 程序 使用纯代码实现 // widget.cpp Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);// 给当前这个lable对象,指定一个父对象QLabel* label new QLabel(this);// C语言风格的字符串可以直接…...
移动应用的界面配置-手机银行APP
设置登录界面为线性布局,组件垂直居中排列设置主页为滚动模式,包括布局、添加背景图片设置按钮样式,包括形状、边框线的宽度和颜色 设置登录界面 设置界面为线性布局,组件垂直居中排列 --android:gravity"center_vertical…...
网站建设工作组/零基础seo入门教学
准备工作: 创建 server.js 文件,使用 node.js 写一个简单的接口,并运行服务创建一个 index.html 页面,页面内使用 fetch 请求上面写的接口(http:127.0.0.1:8000/api/data),获取数据并输出到控制…...
怎么做免费网站被收录/深圳seo优化服务
我们在进行笔记本维修时常会遇到主板不能加电,不能开机等故障,那么作为维修人员就必须熟悉主板电路的每一步工作过程。其中,笔记本主板的开机电路,上电过程就是本篇文章的讲述重点。在本文武汉久龙电脑维修中心的笔者就简单详细介…...
网站如何做社群/培训心得体会感悟
我到底值多少钱;我不止一次的这样的问自己。金钱难道真的能体现我的价值吗。人活着究竟是为了什么啊。我们活在这个世界上首先是生存。生存需要物质上的支持。所以我们去追求金钱。当我们可以添饱自己的肚子的时候。我们迷失了我们的方向。我们忘记了我们现在可以生…...
霍尔果斯建设局网站/搜狗搜索引擎网页
JS的世界发展的非常快,有很多好用的框架,工具层出不穷,今天就来说说怎么结合browserify,gulp,react来构建前端应用. 下面我们先一一简单介绍下browserify,gulp,react Browserify browserify是一个以commonjs规范来定义模块的打包工具. browserify是一个开发工具,它允许我们以no…...
帝国cms是个人网站/建网站的软件有哪些
一、源码解析: 本文主要针对Handler机制原理进行源码分析,如果对Android的Handler机制不熟悉的可以查看Android Handler机制。 Android消息机制的本质是一个线程开启循环模式持续监听并依次处理其他线程给它发的消息。 总结来说,Handler是…...
小清新网站设计/怎么制作网页里面的内容
Leetcode刷题笔记(C)—— 栈队列 整理一下刷题过程中的思路,在这里进行一下总结与分享。 github地址:https://github.com/lvjian0706/Leetcode-solutions github项目是刚刚新建的,陆续会将整理的代码以及思路上传上去…...