在物联网和智能硬件的浪潮中,蓝牙信标(Beacon)技术正悄然改变着我们的生活方式。想象一下,当你走进一家商场,手机自动弹出优惠信息;当你靠近展品时,解说音频自动播放——这些场景的背后,往往都有蓝牙信标的身影。而ESP32这颗集Wi-Fi与蓝牙于一身的芯片,以其低廉的价格和强大的性能,成为了DIY蓝牙信标的绝佳选择。
本文将带你深入理解Eddystone协议的核心机制,并手把手教你用ESP32开发板打造一个功能完整的蓝牙信标。不同于简单的代码搬运,我们会从BLE广播原理出发,解析每个字节的含义,让你真正掌握信标技术的精髓。无论你是想实现物品追踪、室内导航,还是构建智能信息推送系统,这篇文章都能为你打下坚实基础。
蓝牙低功耗(BLE)信标本质上是一个小型发射器,它通过定期广播包含特定信息的信号包来宣告自己的存在。与传统的蓝牙设备不同,信标不需要建立连接即可传递信息,这使其在功耗和部署便捷性上具有显著优势。
Eddystone是Google推出的开源信标格式,相比其他协议更具灵活性。它定义了四种不同类型的广播帧:
表:Eddystone各帧类型适用场景对比
| 帧类型 | 数据内容 | 典型应用场景 | 广播频率建议 |
|---|---|---|---|
| UID | 16字节Namespace + 6字节Instance | 博物馆导览、固定资产追踪 | 每100ms |
| URL | 压缩URL(最长17字节) | 智能海报、商场促销 | 每300ms |
| TLM | 电压、温度、广播计数等 | 设备运维监控 | 每10秒 |
| EID | 8字节加密ID | 安全要求高的支付场景 | 动态调整 |
一个完整的Eddystone广播包实际上是由多个AD Structure(广告数据结构)组成的。理解这些数据结构对后续的代码配置至关重要:
c复制// 典型的BLE广播包结构示例
[Length][Type][Data][Length][Type][Data]...
其中每个AD Structure包含三个字段:
在ESP32的实现中,Eddystone帧通常被封装在制造商特定数据(0xFF类型)中,这是因为Eddystone本质上是Google的私有协议,而非SIG标准协议。
开始编码前,我们需要配置好ESP32的开发环境。推荐使用VS Code配合PlatformIO插件,它比传统的Arduino IDE更适合处理复杂的BLE项目。
所需工具清单:
注意:如果之前使用过Arduino开发ESP32,需要特别注意ESP-IDF的编译链配置可能有所不同。建议新建纯净的开发环境。
ESP32的蓝牙堆栈需要正确的初始化序列才能正常工作。以下是关键步骤的代码实现:
c复制#include "esp_bt.h"
#include "esp_bt_main.h"
#include "esp_bt_device.h"
#include "esp_gap_ble_api.h"
void ble_init() {
// 初始化NVS存储(蓝牙配置需要)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// 释放经典蓝牙内存(专用于BLE)
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
// 配置并初始化BLE控制器
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg));
// 启用BLE控制器
ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE));
// 初始化Bluedroid栈
ESP_ERROR_CHECK(esp_bluedroid_init());
ESP_ERROR_CHECK(esp_bluedroid_enable());
}
这段代码完成了几个关键操作:
Eddystone-UID帧包含几个核心字段:
构造帧数据的函数实现如下:
c复制typedef struct {
uint8_t len; // AD Structure长度
uint8_t type; // 数据类型(0x16表示服务数据)
uint16_t uuid; // 服务UUID(0xFEAA)
uint8_t frame_type; // Eddystone帧类型
union {
struct {
int8_t ranging_data;
uint8_t namespace_id[10];
uint8_t instance_id[6];
} uid;
} u;
} esp_eddystone_frame_t;
int build_eddystone_uid_frame(esp_eddystone_frame_t *frame,
int8_t tx_power,
const uint8_t *namespace,
const uint8_t *instance) {
if (!frame || !namespace || !instance) return -1;
memset(frame, 0, sizeof(esp_eddystone_frame_t));
frame->len = sizeof(esp_eddystone_frame_t) - 1; // 不包括len自身
frame->type = 0x16; // 服务数据类型
frame->uuid = 0xFEAA; // Eddystone服务UUID
frame->frame_type = 0x00; // UID帧类型
frame->u.uid.ranging_data = tx_power;
memcpy(frame->u.uid.namespace_id, namespace, 10);
memcpy(frame->u.uid.instance_id, instance, 6);
return frame->len + 1; // 返回总长度(包括len字节)
}
配置广播参数时,我们需要关注两个关键结构体:esp_ble_adv_data_t(广播数据)和esp_ble_adv_params_t(广播参数)。
c复制// 广播数据配置
static esp_ble_adv_data_t adv_config = {
.set_scan_rsp = false, // 这不是扫描响应
.include_name = false, // 不包含设备名
.include_txpower = false, // 不在广播中包含TX功率
.min_interval = 0x0640, // 最小广播间隔640ms
.max_interval = 0x0C80, // 最大广播间隔1280ms
.appearance = 0x00, // 默认外观
.manufacturer_len = 0, // 初始无制造商数据
.p_manufacturer_data = NULL,
.service_data_len = 0,
.p_service_data = NULL,
.service_uuid_len = 0,
.p_service_uuid = NULL,
.flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT)
};
// 广播参数配置
static esp_ble_adv_params_t adv_params = {
.adv_int_min = 0x0640, // 最小广播间隔640ms
.adv_int_max = 0x0C80, // 最大广播间隔1280ms
.adv_type = ADV_TYPE_NONCONN_IND, // 不可连接的非定向广播
.own_addr_type = BLE_ADDR_TYPE_PUBLIC, // 公共地址
.channel_map = ADV_CHNL_ALL, // 使用所有三个广播信道
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
关键参数解析:
adv_type:设置为ADV_TYPE_NONCONN_IND表示这是一个不可连接的非定向广播(信标的典型配置)adv_int_min/max:广播间隔影响功耗和发现概率,典型值在100ms-1s之间channel_map:BLE使用37/38/39三个广播信道,建议全部启用以提高可靠性将前面的模块组合起来,完整的广播启动流程如下:
c复制void start_eddystone_beacon() {
// 构造Eddystone-UID帧
esp_eddystone_frame_t uid_frame;
uint8_t namespace_id[10] = {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A};
uint8_t instance_id[6] = {0xB1,0xB2,0xB3,0xB4,0xB5,0xB6};
int frame_len = build_eddystone_uid_frame(&uid_frame, -55,
namespace_id, instance_id);
// 配置广播数据
adv_config.manufacturer_len = frame_len;
adv_config.p_manufacturer_data = (uint8_t*)&uid_frame;
// 设置广播数据
ESP_ERROR_CHECK(esp_ble_gap_config_adv_data(&adv_config));
// 开始广播
ESP_ERROR_CHECK(esp_ble_gap_start_advertising(&adv_params));
ESP_LOGI(TAG, "Eddystone beacon started successfully!");
}
实际应用中,我们可能需要同时广播多种类型的帧(如同时发送UID和TLM)。由于BLE广播包大小有限(通常31字节),我们需要实现帧交替广播:
c复制#define FRAME_TYPE_UID 0
#define FRAME_TYPE_TLM 1
uint8_t current_frame_type = FRAME_TYPE_UID;
void switch_adv_frame() {
esp_eddystone_frame_t frame;
if(current_frame_type == FRAME_TYPE_UID) {
// 构造TLM帧
frame.len = sizeof(esp_eddystone_frame_t) - 1;
frame.type = 0x16;
frame.uuid = 0xFEAA;
frame.frame_type = 0x20; // TLM帧类型
// 填充遥测数据...
current_frame_type = FRAME_TYPE_TLM;
} else {
// 构造UID帧
build_eddystone_uid_frame(&frame, -55, namespace_id, instance_id);
current_frame_type = FRAME_TYPE_UID;
}
// 更新广播数据
adv_config.manufacturer_len = frame.len + 1;
adv_config.p_manufacturer_data = (uint8_t*)&frame;
esp_ble_gap_config_adv_data(&adv_config);
}
// 在定时器回调中调用帧切换
void timer_callback(void* arg) {
switch_adv_frame();
}
nRF Connect是调试BLE信标的利器。当我们的信标正常工作时,应该能看到类似这样的广播包:
code复制Advertisement Data:
Flags: 0x06
Service Data (UUID 0xFEAA):
00 (Eddystone UID)
CE (TX Power)
0102030405060708090A (Namespace)
B1B2B3B4B5B6 (Instance)
常见问题排查指南:
信标不可见:
帧格式识别错误:
信号强度不稳定:
对于电池供电的信标,功耗优化至关重要:
优化措施:
ESP_PWR_LVL_N12等低发射功率级别c复制// 设置BLE发射功率(单位:dBm)
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_N12);
通过本文的实践,你不仅能够构建一个功能完整的Eddystone信标,更能深入理解BLE广播协议的设计哲学。在实际项目中,这些知识将帮助你定制符合特定需求的信标方案,无论是用于室内导航、智能家居触发,还是资产追踪系统。