ESP32打造WiFi+BLE一站式智能家居方案
我这个项目其实是被几个电池供电的传感器“逼”出来的。最开始做智能家居,我只用WiFi模块接了几个墙装开关,局域网控制响应倒是爽快,但等我想加温度传感器、人体存在传感器、门窗磁的时候,问题全出来了——WiFi设备挂在路由器上,待机电流下不来,一节18350电池能撑一个月就算烧高香;某些角落信号不好,设备掉线之后重连逻辑又写得头疼。后来我换了个思路:把ESP32当网关,电池类小设备走BLE,需要高带宽、要联网的设备继续走WiFi,两条链路在ESP32内部汇成一套统一的事件流。这套方案跑了半年,家里二十多个接入点基本没有半夜掉链子的情况。这篇文章就把我从选型到实装调优的完整过程写下来,包括踩过的坑和取舍逻辑,适合正在纠结“WiFi和BLE怎么选”“ESP32做网关到底靠不靠谱”的DIY玩家和刚入门的嵌入式开发者参考。
1. 为什么是ESP32:从ESP8266、树莓派到STM32的选型逻辑
先说结论:ESP32不是参数最漂亮的芯片,但它是这个场景下“最不折腾”的选择。我一开始考虑过几个替代方案,各有各的别扭。
1.1 ESP8266缺一条腿
我最早用的就是ESP8266。它便宜、够用、Arduino生态成熟,做WiFi开关绰绰有余。但它天生没有BLE,想接蓝牙传感器只能外挂一个串口透传模块(比如HC-08、JDY-23),这就多了几根线、一个数据解析层和一个独立的电源轨。多一个模块就多一个故障点,而且模块化的BLE透传在数据量稍微大一点的时候非常难调试——你不知道哪个字节被丢弃了,也不知道从机没有回应时到底该不该重发。所以只要项目里出现“WiFi+BLE都要有”的需求,ESP8266基本可以直接出局。
1.2 树莓派Zero 2W“杀鸡用牛刀”
树莓派Zero 2W确实能跑完整的Linux,可以把蓝牙、WiFi、MQTT broker、Node-RED全塞进去,看起来像一个小型服务器。但我的实装感受是:开机要等十几秒,掉电容易损坏SD卡,供电要求也比单片机苛刻得多。作为常电设备放弱电箱里,夏天温度感人。更关键的是,它需要一个稳定的Linux环境和Python/Node生态,为了控制几十个传感器去维护一套Linux系统,运维成本对我来说完全不划算。如果你的项目需要跑复杂的本地推理、需要多个网络服务,树莓派无可替代;但只是一个协议转换和自动化的网关,用Linux是过度设计和额外麻烦。
1.3 STM32+外挂BLE模块的组合拳
STM32本身没有WiFi也没有BLE,要做网关必须同时外挂ESP8266/ESP32做WiFi和BLE模组,然后自己写AT指令解析、状态机和协议转换。这套组合的好处是可定制性极高、工业级稳定,坏处是开发周期会被拉得很长。我只能说,除非你是在做一个量产产品、对成本和BOM有严格规划,否则在个人DIY阶段选STM32就是自讨苦吃。
1.4 ESP32的本职工作恰好就是“桥”
ESP32把WiFi和BLE做在同一颗芯片上,两者共用天线,底层协议栈由乐鑫维护。它解决了一个非常实际的问题:协议桥接不需要再拼接物理模块,可以直接在内存里完成数据交换。芯片本身是双核,一般我会把WiFi协议栈和主逻辑跑在一个核,BLE协议栈跑在另一个核,再用队列做中间通信,两边的实时性都不会被对方拖垮。
我建议你根据具体需求选型号,别盲买:
| 型号 | 核心 | BLE版本 | 适合的场景 |
|---|---|---|---|
| 经典ESP32 | 双核 Xtensa LX6 | BLE 4.2 | 大部分网关和中枢项目,资源充足、资料最多 |
| ESP32-S3 | 双核 Xtensa LX7 | BLE 5.0 | 需要AI加速(语音命令)、更多GPIO或要跑本地识别时 |
| ESP32-C3 | 单核 RISC-V | BLE 5.0 | 纯桥接网关或低成本节点,够用且省电 |
我自己主网关用的是经典ESP32,几个子节点用C3。实测下来,C3做协议桥接完全够用,而且休眠功耗和唤醒响应都更优秀。选S3还是经典ESP32就看未来要不要做语音唤醒、屏幕驱动这样的重活。记住一个原则:芯片选型不是挑性能最强的,而是挑最适合你系统分层的那一个。
2. 网络架构先行:WiFi和BLE的分工边界与数据通路设计
很多人一上来就写代码,写到最后发现WiFi设备也会掉线、BLE传感器数据也丢,根本原因就是没在动手前把“什么数据走什么链路”这个边界画清楚。我在这个项目里主要按三个原则划分。
2.1 按供电和功耗划分设备角色
电池供电的设备,优先走BLE。道理很直接:BLE在低占空比扫描和广播模式下可以用极小的电流维持连接,比如我用的一款ESP32-C3做BLE传感器节点,广播间隔1秒,平均电流能压到0.3mA左右,一节CR2032能跑大半年。而WiFi设备哪怕处于省电模式(modem sleep),平均电流也通常在20-50mA量级,这个量级对电池来说就是灾难。
常电供应的设备,优先走WiFi。墙装开关、插座、网关本身都有稳定的电源,不需要为功耗牺牲带宽和路径。WiFi链路在局域网里能提供稳定的高吞吐,可以做固件OTA升级、批量状态同步、甚至临时传一点视频数据。这些功能用BLE来做会非常憋屈。
2.2 按通信方向和控制延迟划分数据通路
有的数据是“设备主动上报”(传感器温度、开关状态),有的数据是“中心下发指令”(打开灯、关闭空调),这两种数据的链路选择其实有讲究。我的做法是:
- 周期性的、小包的传感器数据:BLE上报,ESP32收到后统一转成内部标准化消息。
- 实时控制指令:WiFi链路走MQTT。为什么不用BLE?很简单,BLE连接的连接事件间隔(connection interval)即使设得再短,也有7.5ms的下限,加上中央设备调度、应用层排队,真实延迟常见在10-50ms。WiFi局域网内MQTT消息的端到端延迟一般在5-20ms。再把掉线重连、广播冲突这些因素算进去,控制类数据放WiFi链路明显更稳。
2.3 统一内部消息模型,别让上层感知两条链路
这是我这个项目里最值钱的设计。ESP32作为网关,把WiFi和BLE收进来的所有数据都转换成同一种结构化消息(类型+来源+值+时间戳),喂进同一个消息队列,上层自动化逻辑只消费这种统一消息,完全不关心底层是走WiFi还是BLE来的。这样做的好处非常明显:我后来在同一个自动化里既能订阅BLE温湿度传感器的数据、又能触发WiFi开关的动作,两个链路的数据可以在同一套规则引擎里任意组合,不需要为每条链路单独写一遍逻辑。
内部消息的典型格式大概是这样的:
typedef struct { char device_id[32]; char sensor_type[16]; // "temp", "humidity", "door", "switch" float value; uint32_t timestamp_ms; uint8_t link_type; // 0=BLE, 1=WiFi } uniform_event_t;这个结构体在后续所有模块之间传递,BLE扫描器、MQTT客户端、HTTP服务器都把数据往这个结构体里塞,自动化模块从这个结构体里取数据。后期维护成本能低一个数量级。
2.4 网络拓扑:单网关+多节点
我实装采用的拓扑是“一个ESP32主网关+若干WiFi子节点+一批BLE传感器”。主网关通过路由器接入局域网,同时开启BLE扫描器,常驻监听周围的BLE广播包。WiFi子节点(比如卧室的智能插座、客厅的调光模块)直接通过MQTT与网关通信。BLE传感器则完全以广播或可连接广播的形式存在,网关单向监听即可,不需要维护复杂的绑定关系。
这个拓扑对路由器的依赖其实很小——主网关和子节点都在同一局域网,就算外网断掉,局域网管理依旧成立,外网断了只是影响远程控制罢了。我会在下一节详细写WiFi链路的具体实现。
3. WiFi链路落地:配网、局域网发现和设备接入
WiFi链路这部分我拆成三块:首次配网、局域网发现问题、MQTT消息通道。每一块都有不少细节,真正写起来才发现坑比想象中多。
3.1 首次配网:SmartConfig和SoftAP两种方式都要写
配网是智能家居设备第一道坎。我最终同时实现了两种配网方式,因为不同用户的使用习惯不一样。一种是用手机App发送配网信息的SmartConfig(乐鑫官方叫ESP-Touch),利用手机和ESP32同时监听WiFi信道、通过UDP广播特殊编码的SSID和密码,设备在混杂模式下抓到这些数据后自动连接路由器。这种方式的体验最顺,手机不需要切换热点,但前提是路由器必须允许UDP广播。
另一种是SoftAP配网——设备自己开一个WiFi热点,手机连接这个热点后打开一个网页,在网页里填路由器账号密码。这种方式兼容性最好,任何手机都能用,但体验略繁琐。我的建议是两者都写上,用一个按键触发配网模式切换,长按按键进入SoftAP配网,连续双击进入SmartConfig配网。
EspTouch在ESP-IDF里实现很简洁:
esp_err_t start_smartconfig(void) { esp_smartconfig_set_type(SC_TYPE_ESPTOUCH); esp_smartconfig_start(&smartconfig_event_handler); // 配网结果通过smartconfig_event_handler回调通知 return ESP_OK; }配网成功的SSID和密码,建议立刻存到NVS。NVS操作频率要控制好,我在后面踩坑部分会专门讲NVS磨损问题。
3.2 局域网发现:让其他设备能找到网关
网关拿到IP之后,其他模块怎么知道“网关在哪”?最省事的是用mDNS(多播DNS),ESP32在网络上注册一个mygateway.local的名字,同局域网的手机、电脑和子节点都能通过这个名字访问它,不用关心IP是否变化。这一招在路由器启用DHCP租约短、设备容易换IP的环境下特别有用。
mDNS注册代码很简单,但有一个容易被忽略的点:设置主机名之后一定要等网络事件里的GOT_IP事件触发后再注册,否则设备还没拿到合法IP就急着注册,mDNS服务会以0.0.0.0去响应,最终解析失败。我的初始化顺序是:先等网络事件确认IP到手,再调用mdns_init()和mdns_hostname_set()。
3.3 MQTT:为什么局域网控制我最终选了MQTT而不是HTTP
智能家居的WiFi链路有一个经常被低估的问题:HTTP请求是半双工轮询式的,服务端没法主动把状态推给客户端。要实现实时状态同步,纯HTTP方案就得各种长轮询、WebSocket、SSE,代码复杂度直线上升。MQTT天然是发布/订阅模式,而且ESP-IDF本身带了稳定的MQTT客户端,局域网里跑一个本地MQTT broker(我用的是树莓派上部署的Mosquitto)就能把网关、子节点和手机App全部串起来。即使未来要把设备接入主流智能家居平台,MQTT和各类Iot平台之间也有现成的桥接插件。
MQTT主题的规划建议一开始就想好层次结构。我用了这样的分层:
home/room/{room_name}/sensor/{device_id}/state home/room/{room_name}/switch/{device_id}/command home/gateway/status层次化主题在自动化里写起来特别顺手。比如“客厅所有设备订阅home/room/livingroom/#”,这比给每个设备建独立主题好维护得多。主题设计尽量扁平、别把设备类型混进一个层级里,不然以后扩展新设备类型时会非常痛苦。
MQTT连接的一个大坑是重连时序。WiFi网络一波动,MQTT客户端会自动重连,但每次重连都要重新订阅主题,重连期间发出的命令会丢失。我的处理方式是:在客户端注册一个“重连完成”回调,重连后立刻重新执行所有订阅操作,并把自动化规则里的“期望状态”全部重发一遍,让子节点重新同步到当前目标状态。否则断网恢复后,开关就会停在断网那一刻的状态上,而不是回归到自动化期望的位置。
4. BLE链路落地:广播监听、主从连接和低功耗策略
BLE链路是这套方案里容易写崩的部分。踩过一轮坑后,我梳理出了低功耗、稳定、实用三个维度的实现方法论。
4.1 中央设备vs从机角色:别把网关做成“保姆式轮询”
BLE存在两种工作模式:广播(从机)和扫描(主机/中央设备)。我的网关常驻扫描模式,监听BLE传感器的广播数据。传感器节点用广播模式周期性发送数据,网关只负责听。这样传感器可以做到极低的占空比,平均功耗能压得很低;而网关不需要维护连接状态,不通信就没有连接事件开销。
如果你买了一些第三方BLE传感器(比如小米的温湿度计、青萍的传感器),它们通常也是广播工作。ESP32扫描时用esp_ble_gap_set_scan_params设置合理的扫描窗口。我实测的经验值:扫描窗口200ms、扫描间隔400ms,这个组合能覆盖绝大部分设备的广播间隔,同时CPU占用不高。如果扫描窗口太短(比如20ms),会频繁错过广播包,数据延迟和丢失率都会明显上升。
4.2 GATT服务设计:需要双向交互时的从机实现
一些设备需要网关主动下发数据(比如BLE灯带、开锁指令),这时候网关上还要挂一个BLE从机服务。我在这台网关上的做法是:主网关同时开一个GATT服务,定义几个特征(Characteristic),其中一个Write特征用来接收手机/其他主设备的控制指令,另一个Notify特征用来向外面推送传感器状态。手机App连接网关BLE后,通过这组特征就能双向通信。
UUID不要自己乱编,我直接用标准服务UUID或者自定一个16位UUID。这里最容易犯的错误是:把Notify特征的回调函数写太多逻辑,导致BLE事件处理被阻塞。BLE协议栈本身有严格的时序要求,回调里绝对不能跑I2C读取、延时等待这种操作,正确姿势是把数据交给队列,让别的任务去处理。我之前就是在这个回调里直接发MQTT消息,结果BLE连接反复断开,排查了一整天才发现是阻塞问题。
4.3 传感器节点端把功耗省到极限
BLE传感器节点我用的是ESP32-C3+一个SHT40温湿度传感器。核心目标是降低平均电流。三个措施最有效:
- 设置广播间隔为1秒,非连接状态功耗很低;
- 每次广播前只用2秒唤醒时间读取传感器和组装数据,其余时间全部进入深度睡眠;
- 深度睡眠期间关闭所有外设电源,SHT40直接由GPIO供电,不用时断电。
实测下来,CR2032电池在常温下能跑半年左右,如果换成两节AA,基本可以按两年规划。这个功耗表现是WiFi方案完全比不了的。关于广播数据格式,我建议尽量用厂商自定义的Manufacturer Data段,把温湿度直接压缩封装在广播包里。网关侧解析时就能少一轮“读特征值”的交互,又省电又省处理时间。
4.4 BLE绑定的取舍
要不要做配对绑定(Bonding)?我的结论是:数据敏感度不高、只是屋内信息上报的话,完全可以不绑定,用开放式广播+自定义MAC过滤就够了。绑定后会引入复杂的密钥管理和重连流程,个人DIY场景收益不大。但如果你的项目涉及门锁、人身安全这类数据,别省这一步,该加密加密。我现在只对门磁传感器做了绑定,其余的温湿度、光照传感器全部开放广播,省了很多心。
5. 状态同步与自动化联动:两条链路拧成一股绳
WiFi和BLE各跑各的只是第一步,真正的智能家居体验从“联动”才开始。这节我写联动实现和状态一致性问题。
5.1 基于统一消息模型的自动化规则
我在ESP32上跑了一个非常轻量的规则引擎:每个规则由“条件组+动作组”组成。条件可以引用统一事件里的任何字段,比如设备ID、传感器类型、数值阈值、事件时间。动作可以是发送MQTT命令、执行本地GPIO操作、触发BLE广播、或调用HTTP API。因为底层消息已经统一,跨链路联动写起来就是几行配置的事。
一个典型的联动例子:当BLE人体存在传感器判定“30分钟内无人且温度高于28度”时,自动控制WiFi空调插座关机。条件里订阅的是BLE事件源的状态,动作里发的是WiFi链路的控制指令,上下两层完全不感知链路差异。我后来加门磁联动玄关灯时,总共只花了不到十分钟,纯粹是加了一条规则,不用动任何链路代码。
5.2 超时与去重:并发事件下保证状态不抽风
两条链路同时上报时容易出现一个脏状态问题:比如BLE广播和WiFi推送几乎同时把同一个开关状态送到网关,自动化规则如果每条消息都触发一次动作,就可能重复执行操作。我在规则引擎里加了一个去重窗口——同一触发源在同一设备上的连续事件,如果时间间隔在5秒内、且值变化小于阈值,就自动忽略。这个简单策略帮我消灭了至少一半的重复触发问题。
另一个是命令下发后的执行确认。WiFi链路发下去的控制指令,子设备执行完成后应该反向再推到网关一次,以此标记“执行成功”。如果没有这个确认机制,网关会一直默认设备处于理想状态,一旦执行失败,所有后续联动都会基于错误的假设。我给自己定的规矩是:凡是涉及动作输出的自动化,必须等待子设备的确认消息,超时(一般设2秒)则标记为失败并触发告警。这套机制在初期调试阶段非常管用,很多接线错误、通信故障都是被它揪出来的。
5.3 断网不中断:局域网自治设计
智能家居最怕的就是外网一断,本地设备全变砖。我的架构天然规避了这个问题:核心联动都跑在ESP32本地,不需要云端中转;MQTT broker也在局域网内。外网断了,本地自动化依旧正常,只有远程控制暂时不可用。我曾经把家里路由器WAN口拔掉测试,所有本地联动照常执行,室内控制延迟完全不受影响。这一点如果你要用云平台做中央控制,几乎不可能做到。
6. 实装后的实测数据与稳定性调优
最后给一组我自己实装的实测数据,以及几个后来修复的问题。这些坑每个都是用熬夜换来的教训,值得单独记一笔。
6.1 稳定性与延迟实测
用了半年、稳定接入后的实测数据大致如下:
| 项目 | 实测值 | 说明 |
|---|---|---|
| WiFi局域网MQTT命令延迟 | 8~20ms | 同AP下,小报文,QoS=1 |
| BLE广播上报延迟 | 1000~1500ms | 受广播间隔1s影响,室内够用 |
| 网关整机功耗 | 约120mA @ 5V | 常电供电,不做深度休眠 |
| BLE传感器节点平均功耗 | 约0.3mA | CR2032实测,广播间隔1s |
| 系统连续运行时间 | 6个月+ | 期间未重启过网关 |
WiFi链路延迟和BLE链路的差距非常直观,所以我把控制指令全部放到WiFi侧,BLE只负责周期性感知数据。这个分工让整个系统的“按键反应速度”和“传感器耗电速度”都达到了我的心理预期。
6.2 坑1:NVS反复写入导致Flash磨损
调试阶段,因为我频繁在配网成功后把SSID/密码重新写入NVS,一度怀疑Flash是不是坏了——设备偶尔上电后连不上WiFi,配置还会丢。排查发现是NVS处在高频率写操作下,Flash的擦写寿命是有限的。修复方法:配置写入前加一个“是否一致”判断,一致就跳过写入;写入频率严格控制,只在配网完成和配置变更时操作。另外,把容易频繁变化的数据(比如状态缓存)放到另一块独立定义的自定义NVS分区,和系统配置分开,避免互相干扰。
6.3 坑2:BLE扫描窗口太短导致漏报
早期我把扫描窗口设成80ms、间隔400ms,结果客厅的BLE传感器经常断断续续——数据能收到,但延迟不稳定,有的数据甚至延迟十几秒。后来抓包才发现,因为各种BLE设备的广播间隔不同,80ms的窗口太小,经常在两次广播的间隙“恰好错过”。调到200ms/400ms后,漏报问题基本解决。这个参数在不同环境差异很大,别直接抄别人的,在自己家里实测几组再定。
6.4 坑3:天线布局与金属外壳的干扰
我把网关装进一个金属外壳后,BLE信号从“客厅能收到”变成“楼上楼下收不到”,WiFi的强度也跌了接近一成。原因很简单:金属外壳对2.4GHz的信号屏蔽非常严重。最后只能换掉金属外壳,用塑料外壳并把天线引出来,信号才恢复。如果你非要用金属外壳,至少在BLE/WiFi天线区域留出塑料窗口,并且把天线垂直放置。这是一个非常容易被忽视的物理层面的坑。
6.5 调优清单:给后来者的一份自查表
把这半年累积的调优项汇总成一张清单,按优先级排:
- WiFi连接和MQTT重连必须分开处理,重连时不能阻塞主循环。
- BLE扫描回调里只做数据拷贝,不执行协议栈以外的重活。
- 自动化规则一定要带去重窗口和超时补偿机制。
- 定期检查子节点的“最后心跳时间”,超时就主动重连或标记离线,别让系统“带病运行”。
- 给网关加一个简单的Web调试页,显示出所有子节点、信号强度和最近事件时间,排查问题效率会高很多。
- 电源质量直接影响所有无线通信的稳定性,尽量用低纹波的5V电源带网关,别用劣质充电头。
我这套方案离产品化还有距离,但作为一个“一站式”的本地智能家居中枢,它在稳定性、功耗和开发成本之间找到了一个很合适的平衡点。如果你也想搭一套同时利用WiFi和BLE的智能家居,先从这两个链路的分工开始规划,再去碰代码,会少走很多弯路。我后来还在往这个网关上扩展更多协议(比如Zigbee串口透传),但核心设计思路依然是:网络链路负责传输,统一消息模型负责解耦,自动化引擎负责聪明。这套思路换什么芯片、加什么协议都适用。