news 2026/9/16 22:53:46

OMI Glass 固件生态中的 NimBLE-Arduino Eddystone URL 信标:广播帧构造、URL 压缩编码与 Deep Sleep 低功耗循环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OMI Glass 固件生态中的 NimBLE-Arduino Eddystone URL 信标:广播帧构造、URL 压缩编码与 Deep Sleep 低功耗循环

OMI Glass 固件生态中的 NimBLE-Arduino Eddystone URL 信标:广播帧构造、URL 压缩编码与 Deep Sleep 低功耗循环

【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend

本文基于 Friend 仓库中 OMI Glass 固件依赖的 NimBLE-Arduino 库所附带的BLE_EddystoneURL_Beacon示例文档展开,完整解析一个 ESP32-S3 周期性 Eddystone URL 信标的完整实现:如何逐字节构造 BLE 广播数据、如何按 Eddystone URL 规范对 URL 做前缀/后缀压缩编码,以及如何通过“广播 10 秒 → 停止 → 深度睡眠 10 秒”的循环把功耗压到最低。读完后,你可以复刻出这个信标工作流,并理解NimBLEEddystoneURL类底层的数据结构与编解码逻辑。

1. 示例定位与设计目标

该文档位于仓库的 PlatformIO 依赖缓存目录中,是 OMI Glass 固件(目标板seeed_xiao_esp32s3,基于 Seeed XIAO ESP32S3)所链接的 NimBLE-Arduino 库自带示例之一:

  • 说明文档:BLE_EddystoneURL_Beacon.md
  • 示例源码:BLE_EddystoneURL_Beacon.ino
  • 同系列 TLM 帧示例:BLE_EddystoneTLM_Beacon.md

文档原文给出了示例的作者脉络(BeeGee 基于 pcbreflux 的 ESP32 Eddystone URL deepsleep 示例改写)与 Eddystone URL 帧规范的出处,并明确了整个 BLE 服务的设计骨架:

Create a BLE server that will send periodic Eddystone URL frames. The design of creating the BLE server is: 1. Create a BLE Server 2. Create advertising data 3. Start advertising. 4. wait 5. Stop advertising. 6. deep sleep

这套六步设计是典型的“低功耗信标(duty-cycled beacon)”模式:设备并不持续广播,而是以固定占空比(示例中广播 10 秒、睡眠 10 秒)周期性地向周围发布 URL 帧。对 OMI Glass 这类电池供电(仓库固件使用双 250mAh 电池)的穿戴设备而言,这种模式与 OMI Glass 固件指南 中描述的“Sleep Mode ~2mA”低功耗目标是同一类工程思路:用深度睡眠换取续航。

与 iBeacon 不同,Eddystone 是 Google 提出的开放蓝牙广播协议族,其 URL 帧类型码为0x10,TLM(Teleseal/Telemetry)帧类型码为0x20,对应库中还有 NimBLEEddystoneTLM.h 等配套类。本文聚焦 URL 帧。

2. 关键配置参数

示例源码头部的宏与全局变量定义了信标的核心参数:

#define GPIO_DEEP_SLEEP_DURATION 10 // sleep x seconds and then wake up // UUID 1 128-Bit (may use linux tool uuidgen or random numbers via https://www.uuidgenerator.net/) #define BEACON_UUID "8ec76ea3-6668-48da-9866-75be8bc86f4d" RTC_DATA_ATTR static time_t last; // remember last boot in RTC Memory RTC_DATA_ATTR static uint32_t bootcount; // remember number of boots in RTC Memory

各参数含义与取值说明:

参数示例值含义
GPIO_DEEP_SLEEP_DURATION10每次唤醒广播结束后进入深度睡眠的秒数,作为esp_deep_sleep(1000000LL * GPIO_DEEP_SLEEP_DURATION)的倍率,即睡眠 10,000,000 微秒
BEACON_UUID8ec76ea3-...128 位 UUID,用于标识信标身份。注意:在 URL 帧方案中该宏仅作为身份标识定义在代码里,示例并未把它写入广播负载(Eddystone URL 帧本身不携带 128 位 UUID,而是靠 URL 内容寻址)
last/bootcountRTC 变量RTC_DATA_ATTR修饰的变量存放在 ESP32 的 RTC 内存中,深度睡眠掉电后依然保留,用于跨唤醒周期记录“上次启动时间”和“累计启动次数”
BLE 设备名"MeBeacon"BLEDevice::init("MeBeacon")使用,并写入扫描响应数据
广播功率ESP_PWR_LVL_N12通过BLEDevice::setPower(ESP_PWR_LVL_N12)设为 -12 dBm,缩小广播半径以进一步省电
TX Power 字段0xF4广播帧中的“0 米处参考发射功率”字节,0xF4即补码表示的 -12 dBm

从源码结构看,lastbootcountsetup()中被用于串口打印本次唤醒距上次唤醒的时间差(now.tv_sec - last),这是排查“占空比周期是否被系统异常重置”的实用手段。

3. 广播负载的逐字节构造

示例没有使用库的高层封装,而是在setBeacon()中手工拼出整个广播结构体。这是理解 Eddystone URL 帧布局最直接的素材。先给出代码:

BLEAdvertisementData oAdvertisementData = BLEAdvertisementData(); BLEAdvertisementData oScanResponseData = BLEAdvertisementData(); const char url[] = "https://d.giesecke.tk"; int scheme_len, ext_len = 1, i, idx, url_idx; char *ret_data; int url_len = strlen(url); ret_data = (char *)calloc(1, url_len + 13); ret_data[0] = 2; // Len ret_data[1] = 0x01; // Type Flags ret_data[2] = 0x06; // GENERAL_DISC_MODE 0x02 | BR_EDR_NOT_SUPPORTED 0x04 ret_data[3] = 3; // Len ret_data[4] = 0x03; // Type 16-Bit UUID ret_data[5] = 0xAA; // Eddystone UUID 2 -> 0xFEAA LSB ret_data[6] = 0xFE; // Eddystone UUID 1 MSB ret_data[7] = 19; // Length of Beacon Data ret_data[8] = 0x16; // Type Service Data ret_data[9] = 0xAA; // Eddystone UUID 2 -> 0xFEAA LSB ret_data[10] = 0xFE; // Eddystone UUID 1 MSB ret_data[11] = 0x10; // Eddystone Frame Type ret_data[12] = 0xF4; // Beacons TX power at 0m

按 BLE 广播数据“长度 + 类型 + 值”的 AD 结构体(AD Structure)格式拆解:

偏移字节值AD 结构含义
0–202 01 06Flags 结构:LE General Discoverable(0x02)+ BR/EDR 不支持(0x04
3–603 03 AA FE16 位 UUID 列表,声明支持 Eddystone 服务 UUID0xFEAA(小端序,先低字节0xAA后高字节0xFE
719(后被重算)Service Data 结构体的长度字段:值字节共 19 个(UUID 2 + 帧类型 1 + TX Power 1 + URL 16 以内)
8–1016 AA FEService Data 结构:类型0x16+ Eddystone UUID0xFEAA
110x10Eddystone 帧类型:URL 帧
120xF40 米处参考发射功率(-12 dBm,有符号补码)
13 起压缩后的 URL最多 16 字节的 URL 编码字段

几点关键结论:

  1. URL 字段上限 16 字节。这是 BLE 广播数据 31 字节总上限约束下的结果:Flags(3)+ UUID 列表(4)+ Service Data(21)= 28,再加 16 字节 URL 恰好接近上限,因此 Eddystone URL 规范把 URL 字段定为 16 字节。
  2. 设备名不放在广播数据里,而放在扫描响应里。代码随后执行oScanResponseData.setName("MeBeacon"),把设备名放入 scan response,避免与 URL 内容争夺 31 字节的宝贵空间。
  3. ret_data[7] = idx - 8;一行在 URL 编码完成后回填真实长度,保证长度字段与实际编码结果一致。

4. Eddystone URL 压缩编码表

BLE 广播空间极其有限,Eddystone URL 规范因此定义了前缀表与后缀表,把最常见的 URL 片段编码成单个字节。示例源码内嵌了这两张表:

static const char *eddystone_url_prefix_subs[] = { "http://www.", // 0 "https://www.", // 1 "http://", // 2 "https://", // 3 "urn:uuid:", // 4 NULL }; static const char *eddystone_url_suffix_subs[] = { ".com/", // 0 ".org/", // 1 ".edu/", // 2 ".net/", // 3 ".info/", // 4 ".biz/", // 5 ".gov/", // 6 ".com", // 7 ".org", // 8 ".edu", // 9 ".net", // 10 ".info", // 11 ".biz", // 12 ".gov", // 13 NULL };
  • 前缀表:按数组下标编码,0x00=http://www.0x01=https://www.0x02=http://0x03=https://0x04=urn:uuid:
  • 后缀表:前 7 项带尾斜杠(如.com/),后 7 项不带(如.com),下标0x000x0D依次对应上表;
  • 未命中任何表项的普通字符按原值逐字节放入。

匹配由string_begin_with()辅助函数完成(用strncmp比较前缀,命中则返回前缀长度,否则返回 0)。

4.1 用示例 URL 走一遍编码过程

以示例 URLhttps://d.giesecke.tk(18 个字符)为例,编码过程如下:

  1. 前缀匹配:string_begin_with依次比对,命中下标 3 的https://(长度 8),ret_data[13] = 0x03url_idx前进 8;
  2. 剩余 10 个字符d.giesecke.tk逐个尝试后缀匹配,均未命中(.tk不在后缀表中),因此按原字符逐字节写入,各占 1 字节;
  3. 最终 URL 字段 = 1(前缀字节)+ 10(原文字符)= 11 字节,ret_data[7]被回填为13 + 11 - 8 = 16

由此得到完整服务数据:10 F4 03 64 2E 67 69 65 73 65 63 6B 65 2E 74 6B,即帧类型、TX 功率、前缀编码 3,随后是d.giesecke.tk的 ASCII。接收端按同一张表逆向展开即可还原出原始 URL。

4.2 库实现中的逆向解码

库源码 NimBLEEddystoneURL.cpp 的getDecodedURL()实现了上述编码的逆过程,其 switch 分支与上文编码表一一对应:0x01展开为https://www.,后缀0x00展开为.com/0x07展开为.com,等等;介于0x210x7E之间的可打印字符直接追加。对照这段库实现,可以快速验证自己手拼广播帧的编码是否正确——用任意 BLE 抓包工具(如 nRF Connect 的 Beacon 解析)扫到信标后,把解码 URL 与此函数输出比对即可。

5. NimBLEEddystoneURL 类:高层封装路径

除手工拼帧外,NimBLE-Arduino 还提供了面向对象的封装。类定义见 NimBLEEddystoneURL.h:

#define EDDYSTONE_URL_FRAME_TYPE 0x10 class NimBLEEddystoneURL { public: NimBLEEddystoneURL(); std::string getData(); NimBLEUUID getUUID(); int8_t getPower(); std::string getURL(); std::string getDecodedURL(); void setData(const std::string &data); void setUUID(const NimBLEUUID &l_uuid); void setPower(int8_t advertisedTxPower); void setURL(const std::string &url); private: uint16_t beaconUUID; uint8_t lengthURL; struct { uint8_t frameType; int8_t advertisedTxPower; uint8_t url[16]; } __attribute__((packed)) m_eddystoneData; };

实现要点(见 NimBLEEddystoneURL.cpp):

  • 构造函数固定beaconUUID = 0xFEAAframeType = 0x10,与手工拼帧示例中ret_data[9..11]的取值一致;
  • m_eddystoneData是一个__attribute__((packed))紧凑结构体(帧类型 1 + 发射功率 1 + URL 16),getData()直接把它序列化为字符串,即“帧类型 + TX Power + URL 字段”这 18 字节的服务数据负载;
  • setData()会校验输入长度不超过结构体总大小并回填lengthURL
  • 需要注意:setURL()是把传入字符串原样拷入 16 字节字段,并不做前缀/后缀压缩。从源码结构看,若走高层 API 发布压缩后的 URL,需要在调用侧先完成编码(正如示例.ino手工所做的那样),或者接受未压缩形式;而getDecodedURL()负责把压缩形式还原为可读 URL,二者并非严格互逆。

6. 广告-睡眠工作流与 RTC 状态保持

setup()的完整调用链体现了文档中“六步设计”:

void setup() { Serial.begin(115200); gettimeofday(&now, NULL); Serial.printf("start ESP32 %d\n", bootcount++); Serial.printf("deep sleep (%lds since last reset, %lds since last boot)\n", now.tv_sec, now.tv_sec - last); last = now.tv_sec; // Create the BLE Device BLEDevice::init("MeBeacon"); BLEDevice::setPower(ESP_PWR_LVL_N12); pAdvertising = BLEDevice::getAdvertising(); setBeacon(); // Start advertising pAdvertising->start(); Serial.println("Advertizing started..."); delay(10000); pAdvertising->stop(); Serial.printf("enter deep sleep\n"); esp_deep_sleep(1000000LL * GPIO_DEEP_SLEEP_DURATION); Serial.printf("in deep sleep\n"); } void loop() { }

流程说明:

  1. 初始化 BLE 栈BLEDevice::init("MeBeacon")完成控制器/主机栈初始化并设定设备名;
  2. 压低发射功率BLEDevice::setPower(ESP_PWR_LVL_N12)(-12 dBm)与广播帧中的0xF4TX Power 字段保持自洽,接收端据此修正 RSSI 测距;
  3. 填充广告数据setBeacon()内通过pAdvertising->setAdvertisementData(...)setScanResponseData(...)分别注入广播数据(第 3 节的 AD 结构体)和扫描响应数据(设备名);
  4. 广播窗口pAdvertising->start()启动广播,delay(10000)维持 10 秒;
  5. 停止并休眠pAdvertising->stop()后调用esp_deep_sleep(1000000LL * 10)进入 10 秒深度睡眠,唤醒后 CPU 重新执行setup(),形成周期性循环;
  6. loop():由于esp_deep_sleepsetup()中同步阻塞,主循环实际不会被调度,这是 ESP32 Arduino 框架下“一次性 setup 即完成全部工作”的惯用写法。

RTC_DATA_ATTR变量(lastbootcount)在此循环中的价值在于:深度睡眠会重置程序状态,但 RTC 内存内容保留,因此每次唤醒打印的 bootcount 递增计数与“距上次唤醒的秒数”都能跨周期连续,便于用串口日志(platformio device monitor --baud 115200,115200 波特率与示例Serial.begin(115200)一致)验证占空比是否按预期运转。

7. 工程要点与验证方法

  • 31 字节预算:构造 Eddystone URL 广播帧时,务必把设备名挪到 scan response,且压缩后 URL 不超过 16 字节,否则广告数据会被截断;
  • 长度字段回填:手工拼帧时,Service Data 的长度字节(示例中ret_data[7])必须在 URL 编码完成后用idx - 8重算,这是示例中容易被遗漏的一步;
  • UUID 字节序0xFEAA在广播中按小端序写作AA FE,16 位 UUID 列表(类型0x03)与 Service Data(类型0x16)两处都要保持该顺序;
  • 验证路径:烧录后可用手机端 BLE 扫描类应用查看广播;扫到Service Data 0xFEAA的帧后,比对解码出的 URL 是否与setBeacon()url常量一致,即可确认编码表、长度字段与帧类型均正确;
  • 低功耗调参:广播窗口(delay(10000))与睡眠时长(GPIO_DEEP_SLEEP_DURATION)可按检测概率需求调整。广播窗口越长,被手机扫到的概率越高,但平均功耗也随之上升;setPower(ESP_PWR_LVL_N12)则用于控制覆盖半径。

8. 小结与延伸阅读

本文以 BLE_EddystoneURL_Beacon.md 描述的六步设计为主线,结合 BLE_EddystoneURL_Beacon.ino 的完整源码,完整走通了 Eddystone URL 信标的实现:广播负载的 AD 结构布局、16 字节 URL 字段的压缩编码与长度回填、NimBLEEddystoneURL类的紧凑数据结构和解码逻辑,以及基于RTC_DATA_ATTR+esp_deep_sleep的低功耗占空比循环。对于 OMI Glass 固件这种电池供电的 BLE 场景,这套“周期广播 + 深度睡眠”的模式是可直接借鉴的参考实现。

延伸阅读(同库示例与类实现):

  • BLE_EddystoneTLM_Beacon.md、BLE_EddystoneTLM_Beacon.ino:同模式的 TLM(遥测)帧信标;
  • NimBLEBeacon.h:iBeacon 帧的对应封装(含 Major/Minor/Proximity UUID/信号功率字段);
  • ble_eddystone.c 与 ble_eddystone.h:NimBLE 协议栈底层的 Eddystone 支持。

最后说明:.pio/libdeps是 PlatformIO 的依赖缓存目录,上述文件随NimBLE-Arduino库版本变化,本文结论以仓库当前缓存的库代码为准;该示例本身面向独立 ESP32-S3 开发板演示,与 OMI Glass 主固件(firmware.ino、platformio.ini)中的音频/相机功能相互独立,可单独编译烧录用于验证。

【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 22:50:47

Linux日志深度解析:故障排查、安全审计与渗透复盘实战指南

1. 日志不是“事后翻箱倒柜”,而是系统运行的实时心电图很多人一提Linux日志,第一反应就是“出问题了才去看”。我干运维和安全分析十年,踩过最深的坑,恰恰就来自这种认知——把日志当备忘录,而不是当生命体征监测仪。…

作者头像 李华
网站建设 2026/9/16 22:49:38

Windows上用VSCode和Code Runner搭建Swift开发环境全指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 22:48:43

Claude-Red高级红队运营:杀伤链、C2与OPSEC深度解析

Claude-Red高级红队运营:杀伤链、C2与OPSEC深度解析 【免费下载链接】Claude-Red claude-red is a curated library of offensive security skills designed for the Claude skills system. Each skill is a structured SKILL.md file that primes Claude with expe…

作者头像 李华
网站建设 2026/9/16 22:48:22

Grafana PDF导出:Docker部署Image Renderer指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华