做蓝牙 beacon 测距这个功能其实是去年给一个室内定位项目做技术选型时被逼出来的。当时需求很明确室内实现米级定位预算压得死UWB那套贵得离谱AOA到达角方案又得定制阵列天线算下来只有 RSSI 测距这条路最现实——ESP32 几乎人手一块蓝牙 beacon 几块钱一个ESP-IDF 原生就带 BLE 协议栈成本几乎可以忽略不计。这一讲是ESP-IDF VSCode 开发 ESP32 联网篇的第六讲专门聊蓝牙 beacon 测距。我会从原理讲到代码再到实际调试中那些文档里不会写的坑完整走一遍。适合已经会基本的 ESP-IDF 工程创建、想在室内定位、防丢提醒、人员考勤这类场景里用蓝牙做距离感知的朋友。老规矩不废话直接上干货。1. 为什么选蓝牙 beacon 测距方案对比与核心原理解读1.1 几种蓝牙测距方案横向对比先理清一个容易混淆的概念蓝牙测距不是一个方案而是分了好几条技术路线选错了后面全白干。第一类是RSSI 信号强度测距也是本讲的主角。原理特别朴素——信号在空中传播会衰减离得越近信号越强离得越远信号越弱拿信号强度反推距离。优点是什么不需要额外硬件beacon 端只要能广播就行接收端 ESP32 打开扫描接口就能拿到 RSSI 值成本几乎为零。缺点也很明显信号在室内会被墙壁、人体、金属家具反射和吸收RSSI 波动非常剧烈精度只能做到 1~5 米这个水平。第二类是AOA 到达角测距就是蓝牙 5.1 引入的测向功能。它靠阵列天线接收信号时不同天线振子之间的相位差来计算信号到达角度配合多个基站做三角定位精度可以到厘米级。但代价是接收端必须用专用阵列天线硬件目前只有少数几家芯片厂商在做成本比普通 BLE 模块高一大截而且算法授权、天线校准都是坑。第三类是TOF 飞行时间测距蓝牙协议栈里其实没有原生支持实际项目里基本会被 UWB超宽带替代。UWB 测距精度能做到 10 厘米级抗干扰能力也强但一颗 UWB 芯片十几块钱起步基础设施成本直接劝退很多项目。从落地角度讲RSSI 测距虽然精度不是最强但它是唯一一种只要有蓝牙就能跑的方案。这个特性决定了它特别适合两种场景一种是预算敏感、精度要求没那么变态的业务——比如展馆里判断参观者大概站在哪个展区误差两三米完全能接受另一种是粗定位 区域判断的组合——比如检测到某个位置放了信标就知道设备进入了对应区域这时候压根不需要精确距离只要一个稳定可重复的信号阈值就够了。1.2 从 RSSI 到距离的数学换算拿到 RSSIReceived Signal Strength Indicator接收信号强度指示之后怎么变成一个具体距离业界最常用的是对数距离路径损耗模型[ RSSI A - 10n \cdot \lg(d) ]反推距离就是[ d 10^{\frac{A - RSSI}{10n}} ]这里有两个关键参数A距离发射端 1 米处测到的 RSSI 绝对值。经验值一般取 59~60也就是 -59dBm 左右。但这个值不是固定的不同 beacon 的发射功率、天线增益、外壳材质都会影响它严谨的做法是拿到实物后在 1 米处实测。n环境衰减因子。开阔空间取 2.0普通办公室隔断多一点的取 2.5~3.0走廊、仓库这种狭长空间或者障碍物密集的环境要到 3.5~4.0。这个值直接决定距离曲线的斜率取错了测出来的距离会系统性偏大或偏小。打个比方RSSI 测距就像你在黑夜里看一个灯泡——你能大概判断灯离你多远但你不知道灯泡本身的瓦数A 值也说不清中间隔了几层雾n 值。所以工程上拿到一个新 beacon第一件事就是做标定实验而不是直接套公式。另外有个细节很多人会忽略在 BLE 广播包里的 TX Power 字段代表的是 1 米处的 RSSI 参考值。iBeacon 广播包最后一个字节就是 TX Power有些实现直接用这个值替代 A 参数会比拍脑袋填 -59 靠谱得多。后面解析协议时我会讲怎么把这个值读出来。1.3 iBeacon 广播包结构解剖这里以 iBeacon 协议为例因为它是目前市面占比最高的 beacon 格式——苹果定义众多信标厂商都在兼容。一个完整的 iBeacon 广播包在 BLE 链路层的数据部分大概长这样02 01 1A 1A FF 4C 00 02 15 [UUID 16字节] [Major 2字节] [Minor 2字节] [TX Power 1字节]拆开看02 01 1AAD Structure 1Length2Type0x01FlagsValue0x1A表示 LE General Discoverable Mode BR/EDR Not Supported。1A FFAD Structure 2Length0x1A即 26 字节Type0xFFManufacturer Specific Data厂商自定义数据。4C 00Company ID0x004C这是 Apple 的公司标识注意是小端字节序发送的所以代码里判断时要反过来。02 15iBeacon 类型标识0x02 iBeacon 数据长度0x15即 21 字节。接着是 16 字节 UUID、2 字节 Major、2 字节 Minor、1 字节 TX Power。Major 和 Minor 怎么用通常用 UUID 区分应用Major 区分区域/楼层Minor 区分单个信标点。比如一个商场项目UUID 固定为商场的应用标识1 楼所有 beacon 的 Major12 楼 Major2每个具体洗手间入口的 Minor 再单独编号。这样软件层拿到广播包三层信息一拆完连查询数据库的过程都省了。解析广播包的本质就是遍历 ADV 数据里的 TLVType-Length-Value结构找到 Type0xFF 的厂商段再校验 Company ID 和 iBeacon 标识。逻辑不复杂但细节决定成败——字节序、偏移量、长度校验哪个错了解析出来都是乱码。2. 开发环境搭建VSCode ESP-IDF 的准备细节2.1 环境版本选择与插件配置工欲善其事必先利其器。ESP-IDF 的开发方式这几年变化挺大早期大家用 Eclipse 插件后来官方力推 VSCode 插件现在又多了个 IDF VSCode Extension 的图形化配置界面整体体验已经比当年舒服太多了。我的建议是直接用乐鑫官方的 Espressif IDF 插件别自己折腾命令行编译加第三方插件拼凑。插件的核心能力是帮你管理多个 ESP-IDF 版本、自动配置环境变量和工具链路径、内置串口监视器和构建任务。装完插件后第一次使用时它会弹窗让你选择 IDF 的安装方式Express 模式插件自动下载某个固定版本的 ESP-IDF 和全部工具链适合新手上路。Existing 模式指定你手动安装好的 IDF 目录适合已经用命令行环境、不同项目需要不同 IDF 版本的老手。我自己习惯用 Existing 模式因为同时维护着 v4.4 和 v5.1 两套环境老项目不能乱动新项目直接上 v5.x。这里有个重要提醒不同版本的 ESP-IDF 蓝牙接口有差异比如 v4.x 里还能直接调esp_ble_scan_start()到 v5.x 就改名成esp_ble_gap_start_scanning()了。网上很多教程代码跑不起来十有八九是版本对不上。本讲工程基于ESP-IDF v5.1API 以这个版本为准。VSCode 本身除了官方 C/C 插件外我强烈建议加装Cortex-Debug——如果你不打算用 JTAG 调试器就算了但用 ESP32 的朋友迟早要面对硬 BUG到时候图形化断点调试比串口打印效率高一个量级。2.2 创建工程与常用配置项插件装好后创建一个新的 ESP-IDF 工程有两条路径。如果你用命令行走过一遍流程会更理解背后发生了什么idf.py create-project ble_beacon_rssi cd ble_beacon_rssi idf.py set-target esp32 idf.py menuconfig在 menuconfig 里依次确认这几个开关Component config → Bluetooth → Bluetooth必须置为 Enabled。Bluetooth controller → Bluetooth Controller ModeBLE Only。默认是 Bredr/LE Dual 模式如果你只做 beacon 接收不涉及经典蓝牙选 BLE Only 可以省下大量内存。Bluetooth Host → Bluedroid Options → BLE Scanned Device Store如果扫描设备数量可能很多把存储条数调大默认的 10 条很容易丢数据。这里我要专门说一下经典蓝牙的问题。ESP32 是双模蓝牙同时支持 BR/EDR经典蓝牙和 BLE。beacon 测距只用到 BLE所以可以在初始化控制器时释放掉经典蓝牙占用的内存esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);这行代码必须在初始化蓝牙控制器之前调用。释放出来的内存大概有 190KB 左右对 ESP32 这种内存不算宽裕的芯片来说省下来的空间可以用来增大扫描缓存或者做后续的滤波运算。我第一次做的时候没管这个结果后面加了个 TLS 功能直接内存不够排查了半天才发现是蓝牙协议栈吃掉了太多 RAM。2.3 工程目录与模块划分我习惯把 beacon 相关代码拆成独立模块而不是全堆在 main 里ble_beacon_rssi/ ├── main/ │ ├── CMakeLists.txt │ ├── main.c # 应用入口创建定时器任务 │ ├── ble_scanner.c # BLE 初始化、扫描配置、回调处理 │ ├── ble_scanner.h │ ├── ibeacon_parser.c # iBeacon 数据解析 │ ├── ibeacon_parser.h │ ├── rssi_filter.c # 滑动滤波和距离转换 │ └── rssi_filter.h └── CMakeLists.txt从功能上讲BLE 扫描回调里最忌讳的就是做重活——回调跑在蓝牙协议栈的上下文中如果你在里面做滤波、打印大量日志、甚至调用 WiFi API轻则丢扫描结果重则触发任务看门狗。正确的做法是回调里只做数据拷贝把原始广播帧和 RSSI 值丢进一个队列由专门的用户任务去解析和计算。队列深度一般设 20~50 就够了太深会积压旧数据导致测距实时性变差。3. beacon 扫描与协议解析代码实现3.1 BLE 初始化与扫描参数配置第一步是初始化蓝牙控制器和协议栈。标准流程大概三步释放不需要的内存、初始化控制器、初始化 Bluedroid 主机。esp_err_t ble_scanner_init(void) { esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); esp_bt_controller_config_t bt_cfg BT_CONTROLLER_INIT_CONFIG_DEFAULT(); esp_bt_controller_init(bt_cfg); esp_bt_controller_enable(ESP_BT_MODE_BLE); esp_bluedroid_init(); esp_bluedroid_enable(); esp_ble_gap_register_callback(gap_event_handler); return esp_ble_gap_set_scan_params(ble_scan_params); }这里最容易踩的坑是顺序。esp_bt_controller_init必须在esp_bluedroid_init之前反了直接断言报错。另外插件的示例工程默认可能把蓝牙配置成 A2DP 音频模式那个配置跑 beacon 扫描会白白浪费几百 KB 内存。接下来是扫描参数这个参数配得好不好直接影响扫描效率和丢包率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, // 单位 0.625ms这里约 50ms .scan_window 0x30, // 单位 0.625ms这里约 30ms .scan_duplicate BLE_SCAN_DUPLICATE_DISABLE // 不去重保证每个广播包都能收到 };逐项解释一下。scan_type选主动扫描除了接收广播包还会主动向 beacon 发扫描请求从而拿到广播响应帧——这能让你多收集一些数据对解析某些只把关键数据放扫描响应里的 beacon 很有必要。scan_interval和scan_window的关系是窗口占间隔的比例越高扫描越密集越不容易漏包但也越费电。0x50 和 0x30 的比例是 60%实际测试下来在 5 米范围内不漏包功耗也还能接受。最后那个scan_duplicate参数特别关键默认是 DISABLE意味着同一台 beacon 的每个广播包都会触发一次回调。如果你误开了去重可能会看到 RSSI 好长时间不变一次因为协议栈认为同一个设备的重复包直接被丢掉了。3.2 扫描回调处理与数据入队回调函数是 BLE 扫描的核心枢纽所有扫描结果都会从这个事件进来static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: handle_scan_result(param-scan_rst); break; case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: esp_ble_gap_start_scanning(0); // 参数设置完成开始扫描0 表示持续扫描 break; default: break; } }ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT这个事件提醒我们esp_ble_gap_set_scan_params()是异步的参数设置完事之后才会触发这个事件这时候才能去调用esp_ble_gap_start_scanning()。很多人把这两个调用写在一起结果第二次set_scan_params还没生效就开始扫了行为完全不可预期。到了handle_scan_result这一步同样不是直接干解析而是做一层过滤后入队static void handle_scan_result(esp_ble_gap_cb_param_t *scan_rst) { if (scan_rst-search_evt ! ESP_GAP_SEARCH_INQ_RES_EVT) { return; // 非扫描结果事件直接忽略 } // 这里只做浅层判断是不是 BLE 广播有没有厂商数据段 uint8_t adv_len scan_rst-adv_data_len; if (adv_len 30) { return; // 一个 iBeacon 广播包最少 30 字节左右太短直接丢弃 } raw_scan_item_t item; memcpy(item.adv_data, scan_rst-ble_adv, adv_len); item.adv_len adv_len; item.rssi scan_rst-rssi; if (xQueueSend(scan_queue, item, 0) ! pdTRUE) { ESP_LOGW(scanner, scan queue full, packet dropped); } }注意scan_rst-rssi是int8_t类型单位是 dBm直接带负数。这里顺手提一句esp_ble_gap_cb_param_t里的ble_adv指针保存的是完整广播数据包含前 6 字节 MAC 地址而adv_data_len是广播数据长度不是总长度。取数据的时候别把 MAC 也当广播数据一起解析了否则偏移量全错。3.3 解析 iBeacon 数据帧核心解析函数在独立的ibeacon_parser.c里逻辑是标准的 TLV 遍历typedef struct { uint8_t uuid[16]; uint16_t major; uint16_t minor; int8_t tx_power; int8_t rssi; } ibeacon_info_t; bool parse_ibeacon(const uint8_t *adv_data, uint8_t adv_len, int8_t rssi, ibeacon_info_t *out) { uint8_t idx 0; while (idx adv_len) { uint8_t len adv_data[idx]; if (len 0) break; uint8_t type adv_data[idx 1]; if (type 0xFF len 23) { // 厂商数据段长度至少 23 26 - 1(type) - 2(company) // 校验 Company ID 0x004C小端序 if (adv_data[idx 2] 0x4C adv_data[idx 3] 0x00) { // 校验 iBeacon 标识0x02 0x15 if (adv_data[idx 4] 0x02 adv_data[idx 5] 0x15) { const uint8_t *p adv_data idx 6; memcpy(out-uuid, p, 16); out-major (p[16] 8) | p[17]; out-minor (p[18] 8) | p[19]; out-tx_power (int8_t)p[20]; out-rssi rssi; return true; } } } idx (len 1); // 跳过一个 AD structure } return false; }这段代码里有几个细节值得展开。第一个是长度判断。len字段代表的是包含 Type 和 Value 在内的长度不含 Length 自身。iBeacon 厂商数据段里 Value 部分有 25 字节Company ID 2 字节 iBeacon 标识 2 字节 UUID 16 字节 Major 2 字节 Minor 2 字节 TX Power 1 字节加上 Type 自己 1 字节所以 len 至少 26。代码里写成len 23是因为后面还有两次偏移判断实际进入数据区后又检查了0x02 0x15双重保险。第二个是小端字节序陷阱。Major 和 Minor 在广播包里是高字节在前大端序所以要用(p[16] 8) | p[17]拼回来。TX Power 是单字节有符号数直接强转int8_t。第三个是索引推进策略。一个 AD structure 的总占用是1(len) 1(type) (len - 1)(value)所以跳转步长是len 1。这里必须用这个公式不能简单idx 2因为每个 structure 的长度不一样。我曾经见过有人直接用固定偏移 30 去取 UUID——在 iBeacon 恰好排在前两段时能跑通但一旦广播包结构调整数据全错。4. RSSI 滤波与距离换算从数据到距离的最后一公里4.1 干这行必会的滑动窗口滤波拿到 RSSI 原始值后你绝对会怀疑人生。同一台 beacon 放在 3 米处RSSI 会在 -55dBm 到 -65dBm 之间来回蹦跳幅 10 个 dB 是常态。如果直接把这种原始值套进公式算出来的距离会在 2 米和 5 米之间乱跳根本没法用。所以滤波这步省不了。实测下来滑动平均滤波和指数加权滤波效果最实用代码简单效果可控注意规避 Kalman 那个坎儿先用移动平均平抑信号再用指数平滑做实时响应。滑动平均的思路是维护一个最近 N 个 RSSI 的窗口输出窗口内平均值。N 太小滤波效果差N 太大响应太迟钝。我自己测试下来取5~8比较平衡大概对应 0.3~0.5 秒的窗口既能压住毛刺又不会让你在走动时感觉距离值黏住。指数加权滤波EMA是另一种流派实现更省内存static float rssi_ema -60.0f; // 初始值给个经验值 float rssi_filter_ema(float new_rssi) { rssi_ema rssi_ema * 0.7f new_rssi * 0.3f; return rssi_ema; }系数怎么选0.7/0.3意味着每次更新时新值占 30% 权重旧值占 70%。这个比例对 beacon 测距比较合适——既不会因为单次突变大幅跳动又能在设备真正移动时快速跟踪。想更平滑就把 0.3 调成 0.2想更灵敏就调成 0.4。注意这个滤波有一个隐藏前提不同 beacon 的 RSSI 要分开滤波。每个 beacon 建一个滤波器实例不能共享同一个rssi_ema否则多台设备一混距离全乱。工程上我会建立一个以 MAC 地址为 key 的哈希表哈希表里存每个 beacon 的滤波状态。4.2 标定 A 值和 n 值别偷懒实测才是王道公式里的 A 和 n 直接决定了最终距离的绝对精度。网上很多教程直接给 A-59, n2.0然后跑出来的距离偏差半层楼这是很正常的——因为不同牌子的 beacon 发射功率差异非常大外壳损耗也不一样。正确的标定流程其实很简单把 beacon 固定在开阔空间走廊尽头或者停车场的空旷角落旁边不要有金属物体。手机装上任意一个能显示 RSSI 的 BLE 扫描 AppESP32 这边可以把扫描后的 RSSI 通过串口打印出来两边对照着来。在 1 米处测量 30 秒取平均 RSSI绝对值就是 A 值。我实测过某品牌的 beaconA 值是 -56跟默认 -59 差了 3 个 dB——这个差值在 10 米处会变成至少 1.5 米的距离误差。然后在 5 米、10 米处分别测量用公式反推 n 值[ n \frac{A - RSSI_{dist}}{10 \cdot \log_{10}(dist)} ] 多做几个距离点取平均n 值就出来了。讲个实操细节标定的时候最好人离开测量路径。人体含有大量水分对 2.4GHz 信号吸收非常严重你站在测量设备和 beacon 之间RSSI 瞬间掉 5~8 个 dB标出来的数据全报废。4.3 距离换算和场景修正滤波后的 RSSI 和标定好的参数都齐了就可以正式算距离#include math.h float rssi_to_distance(float rssi, float a, float n) { return powf(10.0f, (a - rssi) / (10.0f * n)); }这里有个不少人会忽略的问题这个公式在近距离低于 0.5 米会严重失真。理论上 RSSI 在 1 米处就饱和了再近信号强度基本不变但公式计算出来的距离会趋向 0 甚至负数对数函数穿模。所以工程上要做输出限幅比如if (distance 0.3f) distance 0.3f; if (distance 50.0f) distance 50.0f;另外一个常见修正场景是带符号问题。有些代码写RSSI abs(rssi)然后距离公式变成pow(10, (abs_rssi - a) / (10*n))这和上面的写法是等价的但如果你混着用就完蛋了——一会儿带负号一会儿不带算出来的距离差出好几倍。我的建议是全程保留 RSSI 的 dBm 带符号值写进日志里也直观别给自己添乱。4.4 多 beacon 定位时怎么用距离值简单说下多 beacon 的场景。单个 beacon 只能告诉你距离某个点大约多远但有了 3 个以上 beacon 的距离就可以做三角定位。ESP32 作为接收端每收到一个 beacon 的广播包就更新一次对应 beacon 的距离然后按距离排序取最近的 3 个做三边测量。核心公式是解方程组[ (x - x_i)^2 (y - y_i)^2 d_i^2 ]不过实际工程里最小二乘比直接解方程组更抗干扰。RSSI 测出来的 d_i 误差很大直接解方程组往往无解最小二乘可以求一个误差平方和最小的坐标。这套算法在 ESP32 上跑没有性能压力但要考虑收敛速度和浮点精度。如果距离数据还是不够稳定最后一招是查表修正——实测几个固定距离点1 米、2 米、3 米、5 米、8 米把计算距离和实际距离做成映射表中间用线性插值。这招牺牲了一点通用性但换来的精度提升非常可观尤其在环境固定的室内场景下比调公式参数更直接。5. 调试中常见的坑与实测经验5.1 扫描不到设备的排查清单扫描不到 beacon 是最常见的问题通常集中在三个层面。第一是广播类型匹配问题。beacon 一般用的是可连接广播或不可连接广播ESP32 的扫描接口不做类型过滤时全部能收到。但是如果 beacon 配置成了定向广播只针对特定 MAC 地址那 ESP32 就收不到了。排查方法用手机上的 BLE 扫描 App 先确认 beacon 是不是在正常广播如果手机也看不到那铁定是 beacon 端配置问题。第二是协议栈没初始化成功。ESP-IDF 的蓝牙初始化失败时往往不会打印明显错误而是直接跳过扫描回调。我在代码里加了两个防护——初始化后立刻用日志打印esp_bt_controller_get_status()的状态扫描启动后再打印esp_ble_gap_get_scan_status()两个状态确认无误才继续后面的逻辑。第三是扫描窗口和间隔配得比例太低。scan_window远小于scan_interval时扫描器大部分时间在睡觉非常容易漏掉瞬间飞过的广播包。beacon 的广播间隔一般是 100ms~1s如果你扫描窗口只有 10ms一个广播包发过来你没在听就是没在听错过就没了。把窗口比例提到 50% 以上能明显改善漏包。5.2 距离跳变严重时的调参顺序距离结果忽远忽近别一上来就怀疑算法按下面这个顺序排查确认 RSSI 本身有多稳。串口打印滤波前的原始 RSSI如果原始值就在 3dBm 以内波动那滤波没问题问题在公式参数。复测 A 值。A 值偏了 1 个 dB10 米处距离误差约 23 厘米n2 时这个系统性偏差肉眼可见。调整 n 值。如果近距离基本准、远距离系统性偏大或偏小那就是 n 的问题。偏大说明环境衰减比预期的严重把 n 调大 0.3~0.5 再试。检查周围有没有 WiFi 路由器或微波炉。2.4GHz 频段是公共频段WiFi 和微波炉都会干扰 BLE 信号。如果现场 WiFi 流量特别大RSSI 底噪会被抬高这种情况神仙算法也救不了只能考虑换信道或者加信号均值窗口。5.3 数据格式相关坑位整理我整理了调试期间踩过的几个数据格式坑做成速查表方便大家对照。现象根本原因解决办法解析出的 UUID 全是 0xFF没按 TLV 遍历直接用固定偏移读数据改用 while 循环遍历 AD structure先找 0xFF 段Major/Minor 数值和配置对不上大小端字节序搞反了用(p[16]8) | p[17]的方式拼接RSSI 一直是一个固定值不变化开启了scan_duplicate去重把scan_duplicate设为BLE_SCAN_DUPLICATE_DISABLE距离经常跳出 100 米没做输出限幅公式在近饱和区失真加上最小/最大距离限幅多台 beacon 的距离互相串滤波状态共用了一个变量按 MAC 地址区分每个 beacon 的滤波器5.4 实测数据参考最后放一组我在普通办公室实测的数据供大家参考。条件过道宽度约 2 米两侧有工位隔断beacon 放在过道尽头接收端沿过道后退测量。实际距离 (m)原始 RSSI (dBm)EMA 滤波后 (dBm)计算距离 (m)1-57-581.13-66-652.85-72-735.48-79-788.212-85-8612.9标定的 A 值是 59n 值 2.3。整体误差在 1 米以内5 米内表现最好超过 10 米后波动明显加大。如果你的需求精度要求更高建议在同一位置放置多个 beacon 接收或者做时间维度的多次采样再取平均效果比单纯调参数强。6. 个人心得几个有用的扩展方向这个 beacon 测距工程做到后期我已经不满足于测个距离了。几个扩展方向分享给你。一是结合 ESP32 的低功耗模式做电池供电的定位标签。beacon 扫描的功耗大头在射频接收如果做成扫 5 秒休眠 25 秒的占空比模式用一块 18650 电池供电能跑大半年。ESP32 的深度睡眠加定时唤醒非常成熟这套机制对我们后面做资产追踪项目帮了大忙。二是把距离值通过 MQTT 上报到服务器做进入/离开检测。beacon 测距本身不产生业务价值但一旦连上网把距离数据传到后端就可以做很多事了——比如展柜前游客停留时间统计、仓库人员禁区告警、养老院老人房间定位。这正好接上联网篇的脉络ESP32 的 WiFi 和 BLE 是同时工作的测距离的同时把数据传出去完全无压力。三是注意 beacon 的电池寿命管理。市面很多 beacon 用 CR2032 纽扣电池广播功率 0dBm、广播间隔 100ms 的话大约能撑 6~12 个月。如果项目规模超过 50 个 beacon务必提前规划电池更换策略否则后期运维成本会超出你的想象。我的做法是在 beacon 选择上优先挑支持远程配置广播功率和间隔的型号比如 nRF51822 方案的不过市面上更多的是 TI CC2541 / DA14580 方案的前期省 20% 电量后期省 80% 的腿脚功夫。最后的最后再分享一个调试小技巧在解析代码里加一个原始广播帧打印开关。平时关掉遇到解析数据对不上时打开它把收到的完整十六进制广播帧打出来和协议文档逐字节对照。我见过太多人对着解析成乱码的 UUID抓瞎半天其实就是因为少了这步——协议这东西纸上谈兵永远发现不了字节序的魔幻。