ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

ESP32 BLE Beacon测距实战:iBeacon扫描与RSSI距离换算

2026/9/12 14:22:16 拓冰建站 浏览量
ESP32 BLE Beacon测距实战:iBeacon扫描与RSSI距离换算 蓝牙这个坑拖了这么几讲终于要填了。前面几讲把Wi-Fi配网、HTTP请求、OTA升级都过了一遍结果群里天天有人问蓝牙。仔细想想也不奇怪beacon测距做室内定位、近场触发、考勤打卡、防丢提醒这需求太常见了。这讲就用ESP-IDF VSCode这套环境把ESP32当成蓝牙扫描器接收iBeacon广播把RSSI信号强度换算成距离。看完这一篇你能自己搞定一套能跑的beacon测距demo也知道怎么标定、怎么滤波、踩了坑怎么排查。我默认你已经装好了ESP-IDF和VSCode的插件如果还没装建议先别急着跳过第二小节环境这块有几个坑我多说一嘴能省你不少时间。这篇的内容我自己在ESP32-S3和ESP32-C3上各跑过一遍代码部分可以直接放进工程里用但原理部分建议仔细看因为你最后标定出来的参数一定和我标定出来的不一样这个躲不掉的。1. 蓝牙beacon测距项目拆解先搞清楚要做什么1.1 beacon广播和普通蓝牙连接的本质区别先把概念理一理。我们手机上连蓝牙音箱、连手环走的是经典蓝牙BR/EDR或者BLE的连接流程需要先配对、再建立连接然后才能传输数据。而beacon完全不是这个思路它不连接、不配对只管向外广播。beacon基站每隔固定时间常见的是100ms到1s发一包数据周围所有支持BLE扫描的设备都能收到。这个区别非常关键直接决定了代码写法。你在ESP-IDF里如果还想着去esp_ble_gattc_open建立连接那就走偏了。beacon测距只需要把ESP32的BLE协议栈初始化为扫描者Scanner模式然后监听广播包就行。广播包里有发送方的MAC地址、RSSI信号强度、以及厂商自定义的数据段。iBeacon就是把UUID、Major、Minor、发射功率这些信息塞进厂商自定义段里的。RSSI就是接收信号强度指示单位是dBm数值越接近0说明信号越强。比如你在路由器旁边看Wi-Fi信号满格可能显示-30dBm隔了两堵墙可能变成-75dBm。蓝牙beacon也一样距离越近RSSI越大距离越远RSSI越小。测距的核心就是建立RSSI和距离之间的数学关系。但RSSI这个东西非常不稳定多径效应、人体遮挡、金属反射都会让它上下跳动所以后面必须做滤波。1.2 方案选型为什么是ESP-IDF而不是Arduino网上ESP32蓝牙的教程一大半是Arduino框架的用BLEBeacon库几行代码就能跑。但为什么我坚持用ESP-IDF两个原因。第一Arduino的BLE库封装太重。它把所有GATT、Scan、广播逻辑都包了一层C对象看起来省事但你要做beacon解析、做RSSI滤波、做低功耗优化时会发现底层参数根本露不出来改一个扫描窗口都要去翻库源码。ESP-IDF的esp_gap_ble_api.h是直接面向蓝牙协议栈的扫描类型、扫描窗口、重复过滤策略、广播数据原始字节全部你能控制。第二和项目整体衔接。如果你只是玩一个beacon测距demoArduino够用。但现实项目里beacon测距往往是某个大功能的一环后面要接Wi-Fi上传数据、要接MQTT、要做OTA。ESP-IDF里这些都有完善组件你能在一个统一框架里完成所有事不用在两个平台之间来回横跳。用同一个工程管理整个设备逻辑长期维护会轻松很多。2. 环境准备与工程搭建从空文件夹到编译烧录2.1 VSCode ESP-IDF插件的安装要点如果你已经能用VSCode正常编译烧录ESP32工程这一节可以快速扫一眼或者直接跳到下一节。如果你还在环境阶段卡着下面这几点是我替很多人排过雷的。VSCode装ESP-IDF扩展后它会问你是用在线安装还是离线安装。我建议下载离线安装包因为在线安装需要从国外服务器拉取工具链网络稍不稳定就容易中断卡在某个下载进度半天不动。离线安装包包含了工具链、OpenOCD、IDF本体一次到位。装完后有个东西容易忽视ESP-IDF: Set Expression这个命令里IDF_PATH要指向实际的esp-idf目录。很多人报The path for ESP-IDF is not valid: /tools/idf.py not found.就是这里没设置对或者路径里有中文、空格导致解析失败。还有一点Win7老系统上跑新版本IDF会遇到Python兼容性问题新版本直接不支持Win7。官方支持列表里ESP-IDF v5.x要求Windows 10及以上。如果实在要老系统开发只能选v4.4及之前的版本分支但很多新API用不了。这个不算坑只是提醒一句用新版本就升级系统别在环境上浪费时间。创建工程你可以用VSCode的ESP-IDF扩展模板也可以手动建一个main目录加CMakeLists.txt。我习惯用ESP-IDF: New Project模板它会自动生成基本的CMake结构和main文件。改完代码后按F1输入ESP-IDF: Build, Flash and Monitor一条龙编译烧录调试。如果没有识别到串口重点检查USB驱动比如CP2102、CH340这类芯片的驱动装了没有。2.2 新建工程与关键配置项工程建好后在menuconfig里要确认几个蓝牙相关的配置。运行idf.py menuconfig依次检查Component config → Bluetooth → Bluetooth总开关要打开。Bluetooth controller → Bluetooth controller enabled要选上。Bluetooth → Bluedroid → Bluedroid Enable要选上。如果只用BLE可以在Bluetooth controller → Bluetooth modem task stack size保持默认大多数场景够用。内存紧张时可以把Bluetooth → Bluedroid → BT Classic相关的组件关掉经典蓝牙协议栈会占不少RAM。我在ESP32-C3上实测如果打开完整Bluedroid包含经典蓝牙空闲内存会少一截。所以我们代码里会调用esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)把经典蓝牙占用的RAM主动释放给BLE用。这个动作必须在controller init之前执行顺序错了会直接断言失败。另外sdkconfig里还有个重要参数CONFIG_BT_SCAN_DUPLICATE_ENABLE或者说扫描去重策略。如果开了去重协议栈在扫描窗口内发现同一MAC地址的包会自动丢弃那你滤波拿到的RSSI更新频率就会变低。我们做的beacon测距要保持连续采样所以代码里的扫描参数会把scan_duplicate设为BLE_SCAN_DUPLICATE_DISABLE。3. 核心逻辑BLE扫描与iBeacon广播包解析3.1 协议栈初始化的正确打开顺序ESP-IDF里BLE初始化的顺序非常讲究初学者最容易在这里遇到各种esp_bt_controller_init failed或者assert失败。正确顺序是nvs_flash_init()——先初始化NVS蓝牙协议栈要存一些系统参数。esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)——先释放经典蓝牙的RAM。esp_bt_controller_init()——初始化控制器。esp_bt_controller_enable(ESP_BT_MODE_BLE)——以BLE模式使能控制器。esp_bluedroid_init()——初始化Host协议栈。esp_bluedroid_enable()——使能Host协议栈。esp_ble_gap_register_callback()——注册GAP事件回调。esp_ble_gap_set_scan_params()——设置扫描参数。注意第2步和第3步的顺序不能反。我最开始学ESP32时就是把这个mem_release放在init之后结果一运行就崩。原因是控制器在init时已经尝试给经典蓝牙分配资源了你再释放就晚了导致后面BLE可用的RAM不足。扫描参数这个结构体也值得说说。我用的配置是esp_ble_scan_params_t scan_params { .scan_type BLE_SCAN_TYPE_PASSIVE, .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, }; esp_ble_gap_set_scan_params(scan_params);scan_interval和scan_window的单位是0.625ms所以0x50就是80ms0x30是48ms。被动扫描PASSIVE表示只收不发省电scan_filter_policy用ALLOW_ALL不做白名单过滤。这里我把scan_window设为48ms覆盖了大多数beacon的广播时隙能稳定收到包。如果你有低功耗需求可以把这个窗口调小比如0x2032ms代价是漏广播的概率会增加。等到ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT事件来了说明扫描参数设置成功这时候再调用esp_ble_gap_start_scanning(0)。最后一个参数是duration传0表示一直扫描。3.2 iBeacon数据格式逐字节拆解扫描结果事件ESP_GAP_BLE_SCAN_RESULT_EVT里结构体param-scan_rst包含好几个关键字段bda是广播者MAC地址rssi就是信号强度adv_data指向广播数据adv_data_len是长度。拿到这些裸数据之后真正要花功夫的是解析广播包。广播数据其实就是一串AD Structure每一段由一个长度字节表示后面跟几个字节、一个AD type字节、若干data字节组成。iBeacon具体的排列是这样假设数据从AdvData开头算起偏移内容说明00x02第一个AD结构长度10x01Flags类型20x1AFlags值表示LE通用可发现30x1A第二个AD结构长度26字节40xFFManufacturer Specific类型5-60x4C 0x00Apple公司ID0x004C70x02iBeacon类型标识80x15后面数据长度21字节9-24UUID16字节UUID区分不同beacon区域25-26Major高字节在前比如区分楼栋27-28Minor高字节在前比如区分点位29TX Power1米处的RSSI参考值有符号数注意里面的字节序。Major和Minor是大端序而Company ID 0x004C在低层传输时是低字节在前所以裸数据里看到的是4C 00。这也是新手解析最容易搞反的地方。我开始写解析函数时下意识把所有多字节字段都按小端处理了结果UUID对不上、Major变成奇怪的数。后来打印原始字节对照协议一查才发现是字节序搞反。这事儿提醒我调蓝牙底层时hexdump是最好的朋友别凭感觉写偏移。下面是一个能用的iBeacon解析函数static bool parse_ibeacon(const uint8_t *data, uint8_t len, ibeacon_info_t *out) { if (data NULL || out NULL || len 30) { return false; } if (data[0] ! 0x02 || data[1] ! 0x01) { return false; } if (data[3] ! 0x1A || data[4] ! 0xFF) { return false; } if (data[5] ! 0x4C || data[6] ! 0x00) { return false; } if (data[7] ! 0x02 || data[8] ! 0x15) { return false; } memcpy(out-uuid, data[9], 16); out-major (data[25] 8) | data[26]; out-minor (data[27] 8) | data[28]; out-tx_power (int8_t)data[29]; return true; }很多beacon芯片产商会做多段AD structure不一定只包含iBeacon这一段。如果广播包里还塞了名称、服务UUID之类的iBeacon段可能不在偏移0的位置。但绝大多数标准iBeacon广播就是这样先Flags再iBeacon所以这段代码覆盖了90%以上场景。3.3 RSSI采样与滑动滤波RSSI原始值噪声很大同一个beacon放在固定位置你连续采100个样本极差可能有20dBm甚至更多。直接拿原始RSSI算距离结果就会跳来跳去。滤波的目的就是让RSSI变化曲线平滑下来同时保留一定的响应速度。滑动平均是最简单有效的办法。维护一个长度为N的循环缓冲区每次新值进来替换最老的值取平均值。N取5到10比较合适。N太小平滑效果差N太大反应迟钝人走过去1米了滤波值还没跟上。如果想给新数据更高权重可以用加权移动平均比如最近的数据权重更大。不过滑动平均有个问题RSSI突发尖峰会把平均值拉偏需要先做一个简单的中值剔除把明显偏离的值丢掉再做平均。如果你要更平滑的曲线上卡尔曼滤波。一维卡尔曼滤波实现成本很低不需要矩阵运算几个浮点乘法而已。状态变量是RSSI估计值测量值就是每次扫描到的RSSI。过程噪声协方差Q和测量噪声协方差R需要根据实际环境调R越大表示越信任历史估计Q越大表示越信任当前测量。我通常在室内环境用Q0.125、R0.5。这个值不是标准的不同环境要去试但至少是一个可以起步的参考。typedef struct { float x; float p; } kalman_t; static float kalman_update(kalman_t *kf, float z, float q, float r) { float p_pred kf-p q; float kg p_pred / (p_pred r); kf-x kf-x kg * (z - kf-x); kf-p (1.0f - kg) * p_pred; return kf-x; }初始化时把x设为一个正常的初始RSSI比如-70dBmp设1.0。每次扫描结果进来对同一MAC地址的iBeacon执行一次更新。注意不同beacon要各自维护一个滤波器实例不能共用一个否则两个beacon的数据会互相污染。4. RSSI到距离的换算式与现场标定4.1 路径损耗模型是怎么来的RSSI换算成距离最常用的是对数路径损耗模型。无线电信号在自由空间传播时信号强度随距离增加按对数规律衰减。表达式是RSSI(d) A - 10 * n * log10(d)其中A是距离1米时测到的RSSIn是环境衰减指数。在自由空间理想情况下n等于2室内因为多径反射、墙壁吸收n通常在2到4之间。把这个公式反推一下就得到距离d 10^((A - RSSI) / (10 * n))这里有个关键点A就是你站在距离beacon 1米处实测的RSSI平均值不是beacon广播出去的TX Power。TX Power字段的含义是设备生产时在1米处测得的RSSI参考值但每个beacon安装环境、每台设备的射频性能都不一样直接套用TX Power会带来不小的误差。我通常的做法是让beacon放在某处拿着ESP32在它1米处采两三百个RSSI值取平均得到A。然后再到3米、5米、8米处分别采样用这些点反推本环境的n值。有条件的用Excel拟合对数趋势线斜率就是-10n没条件的直接套n2做个初版后面再微调。4.2 两个关键参数的标定过程以我自己的一次实测为例。在一个普通办公室环境里我用一个广播功率0dBm的iBeacon手机距离1米时RSSI均值大约是-58dBm所以A-58。然后在5米处采样均值大约-74dBm。代入公式-74 -58 - 10 * n * log10(5)计算log10(5)约等于0.699所以-74 58 -10 * n * 0.699-16 -6.99 * nn约等于2.29。这个n就是当前环境的衰减指数。要说明的是同一房间不同位置的n也不完全一样换一堵墙、换一个转角都要重新标。所以我在项目里通常把A和n做成可配置项存进NVS现场用串口命令行或者网页配置不用每次重新编译固件。还要注意一个细节esp32模组的天线方向、外壳材质也会影响RSSI测得值。我试过用塑料壳和金属外壳的ESP32开发板同样位置测同一个beacon的RSSI差了好几dB。所以在标定时建议直接使用最终产品形态来标别用裸板和最终用品混着标。4.3 提高测距精度的几个土办法多采集几次再判距离。我一般连续收5次广播再算一次距离比单次计算稳定得多。对距离结果做滞回判断。如果距离在阈值附近来回跳比如1m和1.2m反复横跳用滞回逻辑——进入某一档位要继续超出阈值一定时间或者一定余量才切换档位避免频繁抖动。减少无线干扰。2.4G频段很挤Wi-Fi、微波炉、USB3.0都会干扰BLE。如果现场Wi-Fi信道很忙可以尝试改变AP信道给BLE腾出干净的空隙。不要过度追求绝对精度。beacon测距的对手是室内多径环境天花板上金属吊顶、玻璃隔断、人来人往都会让RSSI剧烈变化。我在实际项目里的定位思路都是用“距离档位”而不是“精确到厘米的数值”。比如把0到2m定义为“近”2到5m定义为“中”5m以上定义为“远”这样阈值判断的抗干扰能力会好很多。5. 真机调试常见问题与排障思路5.1 扫描不到beacon怎么办这是我被问得最多的问题。先说检查顺序先在手机上下载一个BLE扫描工具nRF Connect、LightBlue之类确认beacon在广播。如果手机上能看到ESP32上看不到问题基本出在ESP32侧。如果手机上压根看不到那是beacon本身没配对好或者没电了。ESP32侧扫描不到beacon常见原因有这几个。第一经典蓝牙没有正确释放导致BLE启动失败或者资源不足。看日志里有没有esp_bt_controller_init failed相关报错如果有检查第2步esp_bt_controller_mem_release有没有在init之前执行。第二扫描参数错误。scan_interval和scan_window设置太小或者scan_window大于scan_interval都可能收不到包。第三回调里没有处理search_evt ESP_GAP_SEARCH_INQ_RES_EVT这个分支即使收到数据也被忽略掉了。第四某些esp32的BLE扫描还受scan_filter_policy影响如果设置成了自动接受白名单设备而beacon的MAC不在白名单里也会收不到。另外开发板的供电也有影响。beacon广播本身没问题但ESP32扫描时射频全开电流比待机时大不少劣质USB线压降严重会导致供电不稳蓝牙控制器自动重启。我遇到过一种诡异情况就是插上USB线正常工作拔了换电池供电就扫描不到最后发现是电池电压太低。5.2 RSSI数值震荡和距离跳变如果扫描结果能收到但RSSI上下跳得极其离谱比如同一位置一会儿-40dBm一会儿-85dBm那基本是多径和遮挡的问题。先确认天线区域没有被金属物体包围ESP32的PCB天线周围尽量留空别把开发板死死贴在金属支架上。滤波没有用对也会导致距离跳变。滑动平均滤波器如果窗口里混进了个别异常值比如瞬时干扰产生的-90dBm平均值会被拉低。可以在滤波之前先做限幅把偏离当前平滑值超过15dB的样本直接视为异常丢掉。这个预过滤动作看起来简单在实际项目里效果比任何复杂滤波算法都明显。代码里还可以对距离输出做变化率限制。上一次输出1.2m这一次不可能瞬间变成4m合理情况下相邻两次采样之间距离变化应该是有上限的。超过上限就用上一次的值这个逻辑对消除跳变很有帮助。5.3 编译和IDF路径的坑环境类的坑我提两个。一是The path for ESP-IDF is not valid: /tools/idf.py not found.这在VSCode里几乎都是IDF路径设置错误或者IDF没有完整安装的问题。用ESP-IDF: Show ESP-IDF Version命令看一下当前插件识别到的IDF版本如果没有显示版本说明路径没配好。二是离线安装时明明指定了安装目录结果espressif文件还是被装到了C盘。这是因为ESP-IDF插件在安装工具链时会使用用户目录下的espressif目录作为默认位置部分版本根本不看你在安装向导里选的路径。解决办法是安装完后用ESP-IDF: Configure ESP-IDF Extension手动把IDF_TOOLS_PATH改到你想放的盘符。这个问题在Windows上特别常见C盘空间紧张的朋友要注意。编译时如果报内存不足除了释放经典蓝牙外还可以把Bluedroid换成NimBLE。NimBLE是开源BLE协议栈内存占用比Bluedroid小很多Scan场景完全够用。不过NimBLE的API和Bluedroid不太一样如果你只是做beacon扫描迁移成本不高。我在ESP32-C3这样的低内存芯片上就优先用NimBLE在S3这种内存充裕的芯片上用Bluedroid。6. 完整的beacon扫描测距参考代码6.1 主要代码结构与调用流程给出一份可以直接参考的代码框架这里包含BLE初始化、GAP事件处理、iBeacon解析、卡尔曼滤波、距离计算五个部分。这个demo在ESP-IDF v5.0以上版本编译通过早些的v4.x版本也能用只是部分头文件路径需要微调。#include stdio.h #include string.h #include math.h #include freertos/FreeRTOS.h #include esp_log.h #include nvs_flash.h #include esp_bt.h #include esp_bt_main.h #include esp_gap_ble_api.h #define TAG BEACON #define KALMAN_Q 0.125f #define KALMAN_R 0.5f typedef struct { uint8_t uuid[16]; uint16_t major; uint16_t minor; int8_t tx_power; } ibeacon_info_t; typedef struct { float x; float p; } kalman_t; static kalman_t s_kf; static void kalman_init(kalman_t *kf, float x, float p) { kf-x x; kf-p p; } static float kalman_update(kalman_t *kf, float z) { float p_pred kf-p KALMAN_Q; float kg p_pred / (p_pred KALMAN_R); kf-x kf-x kg * (z - kf-x); kf-p (1.0f - kg) * p_pred; return kf-x; } static bool parse_ibeacon(const uint8_t *data, uint8_t len, ibeacon_info_t *out) { if (data NULL || out NULL || len 30) { return false; } if (data[0] ! 0x02 || data[1] ! 0x01) { return false; } if (data[3] ! 0x1A || data[4] ! 0xFF) { return false; } if (data[5] ! 0x4C || data[6] ! 0x00) { return false; } if (data[7] ! 0x02 || data[8] ! 0x15) { return false; } memcpy(out-uuid, data[9], 16); out-major (data[25] 8) | data[26]; out-minor (data[27] 8) | data[28]; out-tx_power (int8_t)data[29]; return true; } static float rssi_to_distance(float rssi, float tx_power, float n) { return powf(10.0f, (tx_power - rssi) / (10.0f * n)); } 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_PARAM_SET_COMPLETE_EVT: esp_ble_gap_start_scanning(0); ESP_LOGI(TAG, scan started); break; case ESP_GAP_BLE_SCAN_RESULT_EVT: if (param-scan_rst.search_evt ESP_GAP_SEARCH_INQ_RES_EVT) { ibeacon_info_t ib; if (parse_ibeacon(param-scan_rst.adv_data, param-scan_rst.adv_data_len, ib)) { float rssi kalman_update(s_kf, (float)param-scan_rst.rssi); float dist rssi_to_distance(rssi, (float)ib.tx_power, 2.0f); ESP_LOGI(TAG, major%d minor%d rssi%d kf%.1f dist%.2fm, ib.major, ib.minor, param-scan_rst.rssi, rssi, dist); } } break; default: break; } } static void ble_init(void) { esp_err_t ret nvs_flash_init(); if (ret ESP_ERR_NVS_NO_FREE_PAGES || ret ESP_ERR_NVS_NEW_VERSION_FOUND) { nvs_flash_erase(); nvs_flash_init(); } ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT)); esp_bt_controller_config_t bt_cfg BT_CONTROLLER_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_bt_controller_init(bt_cfg)); ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE)); ESP_ERROR_CHECK(esp_bluedroid_init()); ESP_ERROR_CHECK(esp_bluedroid_enable()); ESP_ERROR_CHECK(esp_ble_gap_register_callback(gap_event_handler)); esp_ble_scan_params_t scan_params { .scan_type BLE_SCAN_TYPE_PASSIVE, .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, }; ESP_ERROR_CHECK(esp_ble_gap_set_scan_params(scan_params)); } void app_main(void) { kalman_init(s_kf, -70.0f, 1.0f); ble_init(); }这里的rssi_to_distance里n我临时填了2.0这只适合理想开阔环境。你在实际项目中一定要按前面标定出来的值替换。回调里没有对同一beacon做去重因为scan_duplicate被禁用了每一个广播包都会触发事件。这也意味着日志会刷得比较快调试时可以加一个打印频率限制比如每3秒打印一次平均结果否则日志会滚得飞快其他调试信息都被淹没掉了。6.2 后续可以扩展的方向beacon测距作为基础能力可以接很多玩法。我自己做过的几个扩展方向供你参考。第一个是电子围栏。通过门的防出走管理、展厅展品的近场触发讲解这类场景不需要精确距离只需要判定用户是否进入了某个区域。基于测得的距离和一个可配置的阈值就能做进入事件和离开事件的触发逻辑。注意加个去抖延时别一进一出反复触发。第二个是蓝牙网关。ESP32扫描到beacon之后直接把距离和MAC通过Wi-Fi上报到MQTT服务器后端就能做人员位置轨迹。我之前用ESP32-S3做了个八通道的beacon扫描网关一台设备覆盖一个房间上报周期5秒功耗和稳定性都还不错。第三个是和低功耗结合。ESP32在主动扫描时功耗不算低如果项目对电池续航有要求可以把扫描窗口调小或者用定时唤醒的方式每隔几秒扫一次扫完立刻进入睡眠。这一套在ESP-IDF里做还是顺手电源管理API和蓝牙协议栈配合得很好。代码里目前用的是Bluedroid它对内存的占用略高但功能全面够日常使用。如果芯片内存比较紧张可以参考我刚才说的建议切换成NimBLE做同样的事情。这个内容后续还可以往两个方向扩展一是配合Wi-Fi和MQTT做真正的远程定位上报二是加多个beacon同时检测用三角定位算坐标位置。不过三角定位牵扯到不同beacon的距离误差权重分配那就不是这一讲能装得下的了。个人经验说两句。beacon测距这东西不要指望在复杂环境下做到厘米级它的定位是“近、中、远”这种档位判断做考勤、做接近感应、做区域级定位完全够用。如果项目有精度硬指标比如跌倒检测、AGV厘米级导航那直接上UWB蓝牙RSSI这条路再怎么优化也突破不了物理瓶颈。如果你调了很多天还是不理想先别折腾代码把环境的金属网、人体走动、蓝牙设备密集程度这些因素排查一遍再把滤波和标定做好剩下的交给时间。