这套方案我前后做了差不多三个月,从最早只想搞定一个远程开关灯,到最后把温湿度采集、门窗告警、窗帘控制、遥控器聚合都塞进了一个以ESP32为核心的体系里。中间踩了不少坑,也推翻过好几次架构。今天这篇不写客套话,直接把这套WiFi+BLE一站式智能家居方案的选型逻辑、架构设计、核心代码、调试实录和教训全部分享出来。如果你正准备用ESP32做点自己的智能家居,这篇应该能帮你少走很多弯路。
做这套方案的初衷很简单:市面上的智能家居套装,一个网关一个App一套私有协议,设备买回来就被生态绑死了。我想用ESP32自己搭一套既能走WiFi远程控制、又能用BLE在近场快速配网和低功耗通信的方案,把“联网”和“控制”分成两层来做,既保留灵活性,又不会在功耗和响应速度之间被卡死。整套系统下来,成本控制在两百块以内,所有代码和应用场景完全可控,这才是DIY智能家居该有的样子。
1. 方案选型:为什么锁定ESP32,WiFi和BLE怎么分工
1.1 ESP32这颗芯片到底强在哪
很多人在选型时会纠结到底用ESP8266还是ESP32,甚至有人会考虑STM32加外部WiFi模块的方案。我的结论很简单:如果目标是“一站式”智能家居,直接上ESP32,别犹豫。
ESP32的核心优势在于它把WiFi、BLE、双核CPU、丰富外设、低功耗模式全部塞进了一块芯片。ESP8266只有WiFi,没有BLE,想要近场配网和低功耗广播就得外挂模块,麻烦不说,稳定性也是个隐患。STM32加外部WiFi模块的组合,性能确实强,但开发效率低很多,系统复杂度也上去了,而且WiFi协议栈的维护成本很高——你不可能像用ESP32那样轻松搞定OTA、TLS、MQTT这些上层应用。
我手头这批ESP32-WROOM-32模组,搭载双核Xtensa处理器,主频240MHz,SRAM 520KB,Flash 4MB。这个配置跑WiFi协议栈加BLE协议栈,再同时跑一个Web服务器和一个MQTT客户端,完全没有压力。更重要的是价格,正常渠道十几二十块钱一片,坏了不心疼,适合做遍布全屋的多节点部署。
1.2 WiFi和BLE在整套方案里各管什么
这是整套架构最核心的思考。一开始我也想过全部用WiFi,但很快发现配网环节是个大问题。新设备上电之后怎么知道你家路由器的SSID和密码?总不能让每个设备都先连上一个热点再手动输密码。BLE完美解决了这个痛点。
我最终的分工是这样:
BLE负责“近场配网”和“低功耗广播”。设备上电后先广播一个特定Service,手机App扫描到之后,通过BLE连接把WiFi的SSID、密码以及服务器地址写进设备Flash里。配网完成后,BLE通道可以继续保留,用于在设备附近时直接控制,也可以关闭以省电。
WiFi负责“远程通信”和“数据交换”。设备以STA模式连接家庭路由器,与局域网内的MQTT Broker或Web服务器交互。用户在外网时,通过端口映射或云端中转控制设备。
你可能会问,为什么不让BLE把配网和控制全包了?因为BLE在穿墙能力和传输速率上远不如WiFi,隔一堵墙信号就衰减得很厉害。而智能家居里“人在家里近距离操作”和“人在外面远程查看”这两种场景是并存的,分开处理才是最合理的。GPS定位还会漂呢,一套通信协议想通吃所有场景,结果一定两头都做不好。
1.3 智能家居无线协议横向对比
既然聊到选型,我把主流的几种方案放在一起对比一下,这样你能更清楚为什么ESP32的WiFi+BLE组合是当前DIY智能家居的“最优解”。
| 方案 | 频率 | 速率 | 功耗 | 配网体验 | 组网能力 | DIY友好度 |
|---|---|---|---|---|---|---|
| ESP32 WiFi | 2.4GHz | 最高150Mbps | 中高 | 需配网辅助 | 通过路由器/AP组网 | 高 |
| BLE | 2.4GHz | 约1Mbps | 低 | 天然适合,广播即发现 | 星型,可扩展Mesh | 高 |
| ZigBee | 2.4GHz | 约250kbps | 低 | 需要网关协调器 | 网状网络强 | 低,需硬件认证 |
| LoRa | 868/915MHz等 | 极低 | 极低 | 手动或专用工具 | 星型/点对点 | 一般,需网关 |
看到没有,ZigBee的网状网络确实好,但你要买认证模块、要买协调器网关、协议栈还要授权,DIY玩家很难玩转。BLE Mesh在ESP32上已经能通过蓝牙Mesh协议栈实现,但复杂度不低,我建议先跑通星型结构,后续再扩展。WiFi加BLE这套组合最大的优势就是:开发成本低、生态工具链成熟、配网体验顺畅,而且单芯片就能搞定,不需要额外的网关硬件。
2. 整体架构设计与功能拆分
2.1 设备端、控制端、服务端三层角色划分
任何智能家居项目,只要一展开就会乱成一锅粥。我的经验是,动手写代码之前,先想清楚整个系统的角色边界。这套方案我划分成三个层次:
设备端:就是一个个ESP32节点,负责采集环境数据(温湿度、光照、人体红外)和执行控制动作(开关灯、开合窗帘、继电器通断)。设备端我分成了“传感器节点”和“执行器节点”,但在代码层面统一抽象成了“受控终端”,一条WiFi链路加一条可选的BLE链路。
服务端:我家里的树莓派上跑着MQTT Broker,同时也跑了一个简单的Web服务。设备端上报数据到MQTT主题,控制指令通过MQTT下发。树莓派在这里扮演的是中枢和消息路由的角色。如果你不想额外搞一个树莓派,也可以让ESP32直接内嵌Web服务器,手机连上设备热点或同网段直接访问设备IP,这适合单设备场景,但对多设备的统一管理不够友好。
控制端:手机App是我自己写的,用的Flutter,同时做了一个网页版控制台。网页版挂在树莓派上,用浏览器就可以打开操作。这样做的好处是,无论你是Android、iOS还是电脑,打开浏览器就能控制家里所有设备,不用为平台发版操碎心。
2.2 我最后落了哪些功能
做智能家居最忌讳贪多嚼不烂。我最终收敛成五个子模块,每一块都真正跑通了:
环境监测:DHT22采集温湿度,BH1750采集光照强度。采集周期默认60秒,数据通过MQTT上报,树莓派端记录到SQLite里,可以生成温湿度曲线。
灯光控制:继电器控制卧室主灯和客厅灯带。支持手动开关、定时开关和亮度调节(配合PWM调光灯带)。
门窗告警:门磁传感器接在ESP32的GPIO上,中断触发检测门被打开。门窗状态变化时,设备端即时上报一条MQTT消息,树莓派推送到手机通知。这块用到了ESP32的深度睡眠唤醒,平时功耗极低。
遥控器聚合:家里的空调遥控器、电视遥控器用的都是红外,ESP32自带红外发射功能。我把各个遥控器的红外码学习一遍,存进Flash里,手机端点一下就能发对应指令。客厅的ESP32节点就兼任了红外“万能遥控器”。
远程OTA升级:每一个ESP32节点都支持OTA。新功能上线时,我会通过MQTT对设备端下发OTA触发指令,设备端从HTTP服务器拉取固件并更新。没有OTA的智能家居就是一个大型翻车现场,设备挨个拆下来刷固件根本不现实。
完整拓扑大概是:路由器 —— 树莓派(MQTT Broker + Web服务) —— 若干ESP32节点(传感器/执行器) —— 手机App/浏览器(控制端)。所有节点之间通过WiFi数据交互,首次配网走BLE,平时靠MQTT保活。
3. 核心模块实操:硬件准备、BLE配网、WiFi双模式、Web内嵌
3.1 硬件选型与接线清单
硬件这块我踩过的坑比代码多得多。先把清单给你,都是我测过稳定的:
| 设备/模块 | 型号/规格 | 用途 | 备注 |
|---|---|---|---|
| 主控 | ESP32-WROOM-32开发板或模组 | 主控芯片 | 建议买带屏蔽罩的 |
| 温湿度 | DHT22 | 采集温湿度 | 比DHT11精度高,注意上拉电阻 |
| 光照 | BH1750 | 采集光照强度 | I2C接口,不要接错地址引脚 |
| 人体感应 | HC-SR501 PIR传感器 | 检测人体活动 | 灵敏度与延时可通过电位器调 |
| 门磁 | 干簧管+小磁铁 | 门窗开关状态 | 中断触发,配合浅睡眠 |
| 继电器 | 一路或两路5V继电器模块 | 控制灯具/插座 | 注意隔离和电源别共用 |
| 调光 | MOSFET模块(或者PWM LED驱动器) | LED灯带调光 | 功率大时要加散热 |
| 红外 | 红外接收管(TSOP4838)+红外发射LED | 学习/发射红外码 | 发射时电流要限流 |
| 电源 | 5V/2A电源适配器加AMS1117-3.3 | 供电 | 别用劣质电源,无线稳定性跟电源纹波直接相关 |
接线记住几个关键点:DHT22数据引脚一般接GPIO4或GPIO5,我统一规定传感器类接GPIO4,继电器控制接GPIO16到GPIO21这一排,原因是这些引脚比较稳定,不容易在上电瞬间输出毛刺导致误动作。第二天你可能会问,为什么不是所有引脚都能用。有几个坑:GPIO0是烧录模式选择脚,不能接对电平敏感的设备;GPIO12是MTDI引脚,接入上拉的传感器可能启动失败;GPIO34到GPIO39只能做输入,不能做输出。把这些规则记住,能少烧好几块板子。
3.2 一路走到尾:BLE配网流程的完整设计
配网是整个体系体验好坏的分水岭。拿手机连摄像头的方式去配网(连上设备热点再输密码提交)也不是不行,但我试过之后果断放弃了。路由式配网要切换WiFi连接、设备要开AP热点、SSID和页面都对不上时排查体验非常差。BLE配网则直接把“找到设备”和“下发配置”合并成了一步。
第一步,ESP32上电后初始化BLE服务端,注册一个自定义Service。我定义的UUID是这样规划:
#define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e" #define CHARACTERISTIC_UUID "6e400003-b5a3-f393-e0a9-e50e24dcca9e"第二步,这个Characteristic支持写属性。手机App扫描到设备后,连接并写入一段JSON:
{ "ssid": "HomeWiFi", "password": "12345678", "mqtt_host": "192.168.1.100", "mqtt_port": 1883 }第三步,ESP32接收完这段数据,先存入Flash的NVS区(掉电不丢失),然后立即重启,重启后读取NVS里的配置,尝试连接WiFi并建立MQTT连接。整个配网过程不到五秒,体验上和商用智能家居的扫码配网几乎没差别。
代码核心部分大概是这样的:
#include <BLEDevice.h> #include <BLEUtils.h> #include <BLEServer.h> #include <Preferences.h> Preferences prefs; class MyCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { std::string value = pCharacteristic->getValue(); if (value.length() > 0) { prefs.begin("wifi", false); prefs.putString("config", value.c_str()); prefs.end(); delay(200); ESP.restart(); } } };注意,BLE写入MTU默认只有23字节,一段JSON可能会被拆成多包。我实测iPhone和Android的行为还不完全一样,Android可以协商MTU到185字节,iOS有时会按默认走。为了避免切片导致的脏数据,我建议在App端把JSON压缩到最小,或者干脆分别用三个Characteristic写三项配置。这个方法更稳妥,我最终采用的是三个Characteristic对应SSID、密码、MQTT服务器地址的写法。
3.3 WiFi的STA+AP双模式:保证设备永远“找得到”
设备配完网之后,如果路由器重启或者WiFi密码改了,设备会陷入离线状态。这时候你需要一个可靠的兜底机制。我用的是“STA+AP共存”模式:设备平时以STA模式连接家庭路由器,但在连接失败或者断连超过一定时间之后,自动开启一个名为“ESP32_Config_XXXX”的软AP热点。
AP热点上跑着一个极简化的配网页——就一个表单,填SSID、密码、服务器地址,提交之后ESP32收到数据存NVS并重启。这样就算你人在房子里没有电脑、没有装App,只要有手机打开WiFi列表连上这个热点,在浏览器里输入192.168.4.1就能完成重新配网。我把这个软AP设计成“仅当STA处于未连接状态时才启用”,一旦STA连上路由器就关闭AP,避免两个接口同时吃发射功率导致干扰。
STA模式的自动重连逻辑我也做了加强:
WiFi.setAutoReconnect(true); WiFi.persistent(true);实际跑下来,这两个配置能解决大部分家用路由器偶发重启引起的断线问题。但路由器死机时ESP32也无法自己恢复——它毕竟只是终端,不是网络设备。我更推荐在树莓派上写一个定时脚本,每五分钟ping一次各设备IP,连续三次不通就通过MQTT向设备下发一条重启指令。听起来粗暴,但在智能家居这种场景里,“能远程重启的设备才是好设备”。
3.4 内嵌Web服务器:每个ESP32都是一个迷你控制台
我采用分层架构时并没有完全放弃让ESP32自己做Web服务器的方案。事实上,单节点调试场景下,ESP32内嵌的Web页面是最快的问题定位工具。我用的库是ESPAsyncWebServer,异步模型,ADS和CSS全部内嵌到Flash里,页面加载快,资源占用可控。
我做的页面功能很简单但很实用:
- 显示实时温湿度、光照强度、设备在线状态
- 三个开关按钮控制继电器/灯带
- 一个WiFi配置框,用于STA模式下动态调整目标服务器
- 一个OTA固件上传进度条
给设备端网页“加餐”时有一个地方要特别注意:Web页面中用了比较密集的AJAX轮询去刷新数据会加重ESP32负担。一般两秒一次比较合适,开了WiFi省电模式后可以放宽到三秒。整个Web服务器跑起来SRAM占用大概35KB,在ESP32的520KB内存下完全没压力。
4. 具体实现:从开发环境到核心代码的全面解析
4.1 开发环境怎么搭:Arduino IDE还是ESP-IDF还是PlatformIO
我先说结论:家庭使用和快速开发选ArduinoIDE,企业往量产走选ESP-IDF,个人后期项目多了想管理依赖你可以试PlatformIO。
我这次所有代码跑在Arduino IDE框架下。有人可能会吐槽ArduinoIDE太简陋,它的优点其实被低估了——ESP32的Arduino核心包封装极其完善,BLE、WiFi、MQTT、WebServer这些组件全是现成库,加上Arduino的核心代码更新很快,你不需要去啃ESP-IDF的组件系统。而且ArduinoIDE支持直接从菜单栏安装ESP32开发包,我用的版本是2.x的Arduino IDE,配合esp32核心2.0.9。如果你是离线环境,记得手动下载板卡包,国内用户建议配一个镜像源,否则会卡死在下载那一步。
ESP-IDF是乐鑫的官方开发框架,性能上限更高、组件划分更精细,但学习曲线明显陡峭。对纯DIY方案来说,Arduino完全够用。反正底层是同一套协议栈,Arduino核心本质上是ESP-IDF的封装,真正跑起来差异不大。如果你后面要做商用产品,建议迁移到ESP-IDF,因为内存管理、功耗控制、加密模块这些精细配置在Arduino下憋屈得很。
4.2 从零开始写BLE配网全流程代码
我先把BLE配网部分的完整流程代码框架展示出来,你直接参照这个改就行:
#include <BLEDevice.h> #include <BLEUtils.h> #include <BLEServer.h> #include <Preferences.h> #include <WiFi.h> #define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e" #define CHAR_SSID_UUID "6e400002-b5a3-f393-e0a9-e50e24dcca9e" #define CHAR_PASS_UUID "6e400003-b5a3-f393-e0a9-e50e24dcca9e" #define CHAR_SERVER_UUID "6e400004-b5a3-f393-e0a9-e50e24dcca9e" Preferences prefs; String wifi_ssid = ""; String wifi_password = ""; String mqtt_server = ""; class ConfigCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) { String value = pCharacteristic->getValue().c_str(); if (pCharacteristic->getUUID().toString() == CHAR_SSID_UUID) { wifi_ssid = value; } else if (pCharacteristic->getUUID().toString() == CHAR_PASS_UUID) { wifi_password = value; } else if (pCharacteristic->getUUID().toString() == CHAR_SERVER_UUID) { mqtt_server = value; prefs.begin("cfg", false); prefs.putString("ssid", wifi_ssid); prefs.putString("pass", wifi_password); prefs.putString("server", mqtt_server); prefs.end(); ESP.restart(); } } }; void setupBLE() { BLEDevice::init("ESP32_Sensor_01"); BLEServer *pServer = BLEDevice::createServer(); BLEService *pService = pServer->createService(SERVICE_UUID); // 创建三个Characteristic并绑定回调... }这里有两个细节经验。第一个是服务名。BLE广播名称“ESP32_Sensor_01”决定了手机App里显示成什么样,建议按“设备类型_位置_编号”来命名,比如ESP32_Light_Bedroom_01。第二个是设备状态回写的必要性。配网结束后设备端应该往App反向写一条“配网成功,正在连接WiFi”,让用户知道发生了什么。只看设备重启,用户会以为卡死了。我加了三个Characteristic里的只读状态位,分别表示“等待配网”“正在连接WiFi”“已连接”三个阶段。
4.3 WiFi连接和数据上报:MQTT是比HTTP更优雅的通道
最初我用的是HTTP API——设备端定时POST JSON到树莓派的端点。后来发现两个问题:一是HTTP轮询太浪费资源,二是设备状态变化没法及时推送到控制端。MQTT以发布订阅模式解决了这两个痛点。
设备端的发送逻辑很简单。传感器节点每60秒读取一次DHT22和BH1750,把数据封装成JSON后发布到主题home/sensor/bedroom。执行器节点订阅home/devices/bedroom_light/set主题,收到消息后解析JSON,执行相应动作,再把执行结果回发到home/devices/bedroom_light/state。
#include <PubSubClient.h> #include <WiFi.h> WiFiClient espClient; PubSubClient mqttClient(espClient); void mqttCallback(char* topic, byte* payload, unsigned int length) { String message; for (unsigned int i = 0; i < length; i++) { message += (char)payload[i]; } // 解析JSON并执行控制逻辑 handleCommand(topic, message); } void publishSensorData() { StaticJsonDocument<256> doc; doc["temp"] = temperature; doc["hum"] = humidity; doc["lux"] = lux; char buffer[256]; serializeJson(doc, buffer); mqttClient.publish("home/sensor/bedroom", buffer); }MQTT的QoS(服务质量)默认是0,也就是发出去就不管了。指令控制类消息我使用QoS 1,保证至少送达一次,避免“按了开关却没反应”的诡异Bug。数据上报类保持QoS 0,少一点网络开销,丢了也无所谓,反正下一分钟还会上报。
4.4 OTA升级:没有这个功能,你一定会后悔
前面说了OTA重要,这里把实现要点讲透。我的每个ESP32节点都能通过HTTP拉取固件并更新。树莓派上跑了一个简单的Nginx,目录下放编译好的.bin固件文件,通过MQTT向目标设备下发OTA指令,设备收到后跳转到OTA升级流程。
核心逻辑:
#include <Update.h> #include <HTTPClient.h> void startOTA() { HTTPClient http; http.begin("http://192.168.1.100/firmware/esp32_sensor.bin"); int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { int contentLength = http.getSize(); if (Update.begin(contentLength)) { Update.writeStream(http.getStream()); if (Update.end()) { ESP.restart(); } } } }OTA有几个坑必须说清楚:第一,OTA期间断电会变砖,升级前一定要确保供电稳定,插座要插紧,不要边充电边搬动设备。第二,ESP32的Flash分区必须是默认的“Huge APP”或者自定义的“OTA”分区模式,否则无法使用OTA功能。第三,每次编译固件时,如果改了分区表,会导致旧的OTA缓存失效。第四,我记得最惨的一次是全家五台设备同时OTA,路由器直接假死,那之后我学乖了,OTA策略改成“先更新一台,等它上线并稳定24小时,再批量更新其余设备”。
5. 常见问题与调试实录
5.1 WiFi断连、连不上、一直重连怎么办
这是智能家居调试里最常遇到的问题。我的排查顺序和方法整理成表格给你:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| STA连不上路由器 | 密码错误、路由器2.4GHz被关闭、MAC地址被过滤 | 先用手机热点测试设备,逐步排查网络侧问题 |
| 连接成功但频繁掉线 | 电源纹波过大、同频干扰、路由器带机量不足 | 换一个稳定的5V/2A电源,调整ESP32发射功率从19.5dBm降到12dBm |
| 能连上但MQTT连接一直失败 | Broker IP错误、端口被防火墙拦截、用户名密码错误 | 在代码里加MQTT连接失败重试日志,检查树莓派防火墙规则 |
| 同一区块设备集体离线 | 路由器2.4GHz信道拥塞或AP重启 | 在路由器管理后台把2.4GHz信道改为1、6或11中空闲的那个 |
发射功率我多说一句。ESP32默认发射功率是19.5dBm,全功率跑功耗高、热量大,还会加剧对同频段蓝牙的干扰。实测家里小户型,12dBm完全足够,功耗和稳定性平衡最好,而且发热明显下降。设置在WiFi.begin()之后调用WiFi.setTxPower(WIFI_POWER_12dBm)即可。
5.2 BLE扫描不到设备、配网不成功的排查
BLE配网的失败通常集中在三块:
第一块是广播名称和Service UUID不匹配。App过滤时一般同时匹配设备广播名称前缀和Service UUID,两边不统一就看不到设备。调试时我用nRF Connect这个工具直连查看广播包,一目了然。
第二块是BLE连接建立后立刻断开。多半是两个设备之间的SPAM广播冲突,也就是ESP32在握手过程中持续广播导致连接不稳。解决方式是在连接建立后把广播停下来,等连接断开再恢复广播。BLE官方文档也是这么建议的。
第三块是Characteristic写入的MTU值太小。前面已经提过,给每个配置单独开Characteristic是绕过这个限制最省事的做法。如果一定要写一个大JSON,就要在接收端做重组和校验,还要自定义包序号,开发成本大不划算。
5.3 继电器误动作和GPIO毛刺问题
这个坑新手最容易忽视。ESP32上电瞬间,部分GPIO会输出一个短暂的高电平脉冲,如果这个引脚刚好接的是继电器控制端,就会出现“设备刚上电,灯就闪了一下”的诡异现象。
解决方法是:控制继电器模块的信号脚不要直接接在GPIO上,中间加一个下拉电阻(10K到GND),让引脚在默认状态下被强制拉到低电平。同时把代码里的引脚初始化和模块使能逻辑放到setup函数最前面,尽量提前把引脚设置为输出低电平,减少毛刺可以造成的影响窗口。
继电器模块本身也要注意:5V继电器线圈需要驱动电流较大,如果用ESP32的3.3V去驱动,最好选带光耦隔离的模块,并且单独给继电器供电,ESP32和继电器模块共用一个5V电源时,线圈吸合产生的电流尖峰会导致WiFi断流。要么分开供电,要么在5V进线处加一个大电容(470uF以上)缓冲。
5.4 Flash写入寿命和数据保存策略
智能家居设备会频繁保存状态,比如灯的开合状态、温湿度阈值配置。如果直接写Flash,不仅磨损快,还有可能在写入过程中断电损坏配置。ESP32的NVS底层是Flash,擦写寿命约10万次,看似很多,但如果你每秒写一次,一天就是8万多次,完全不设防用法不到两天就把Flash写废了。
我的经验是把状态变化通过MQTT上报到树莓派记录,本地存储的只有“最新服务器地址”“WiFi配置”这种极其低频的数据。那些“灯是开是关”的实时状态,设备重新上电后不一定要恢复——我采用“可控恢复”的方式:灯的状态变化后在内存里保存一份,每半小时持久化一次到NVS。这样就算掉电,最多丢失半小时内的状态变化,Flash的写入频率也不会成为瓶颈。
5.5 实测最顽固的一个Bug:MQTT长连接被路由器掐断
有一次我发现,设备运行几天后就会离线,但WiFi连接还是正常的,设备能Ping通路由器和树莓派,却就是收不到MQTT消息。排查了很久,最后定位为MQTT的Keep Alive机制和路由器的NAT超时踢除冲突。
TCP长连接在路由器上会有NAT会话老化时间,如果Broker和客户端之间长时间没有数据包,路由器会悄悄清掉这条NAT映射,但两端的TCP层并无感知,于是连接就“僵尸化”了。解决办法是两件事同时做:
- 客户端MQTT的Keep Alive设为60秒,并且在空闲时定时发送PINGREQ;
- 在树莓派的Mosquitto配置文件里把
keepalive_interval参数放宽到1800秒,避免Broker提前把客户端判定为超时。
加了这两个改动之后,设备在线时长从两三天提升到了四十多天,再没出现过僵尸连接。
6. 功耗、安全与从单设备到多设备的扩展方向
6.1 把电池供电节点搞到能用三个月:深度睡眠实战
家居节点不全是有线供电的,比如门窗磁传感器、人体感应传感器,装箱子里塞电池也能用半年,这就要靠ESP32的深度睡眠。ESP32在深度睡眠模式下可以做到10uA级别的电流,这个数值在电池场景下非常可怕——一节18650电池(约2000mAh)理论上能撑二十年(当然实际达不到,因为要考虑唤醒周期和工作电流)。实际测算下来,门窗磁节点一节18650电池用九个月到一年没问题。
深度睡眠的工程实现是这样的:
esp_sleep_enable_ext0_wakeup(GPIO_NUM_32, 0); // 低电平唤醒 esp_deep_sleep_start();GPIO32我接的是干簧管的一端,另一端接3.3V。门磁分开时,干簧管断开,GPIO32为高电平。磁铁靠近闭合时,干簧管接通,GPIO32被拉到低电平,产生下降沿事件,ESP32被唤醒,上报“门已关闭”并再次进入深度睡眠。几个节点靠这个机制,每天早上看手机就能知道门窗状态,功耗还控制得极低。
普通传感器节点也可以做定时唤醒。esp_sleep_enable_timer_wakeup定时器设置一小时唤醒一次,采集温湿度上报后继续睡,比一直挂WiFi能省掉一大半功耗。注意,深度睡眠期间WiFi是断开的,所以唤醒后要重新初始化WiFi并连接MQTT,这个连接过程要设计成容错性的,连不上不代表故障,下个周期重试就行。
6.2 安全加固:不能家里的门禁系统还是个裸奔的Web页面
其他都调试跑通后,如果你打算把这套方案用于真实家居,甚至让室外也能访问控制页面的时候,安全必须提到日程上。我强烈建议从第一天起就按下面的思路加固:
- 关闭所有不必要的开放端口。树莓派上只开放本地端口,外网访问通过内网穿透的方式走加密隧道,绝不把HTTP的1883端口或控制台8080端口直接映射到公网。
- MQTT开启用户名密码认证,并且接入设备使用独立账号,权限按主题范围隔离控制。Mosquitto的配置文件里可以针对每个用户名配置读写权限的最细粒度。
- OTA和Web服务启用TLS。ESP32支持BearSSL和mbedTLS,可以加载PEM证书做HTTPS加密。在局域网内,TLS开销不算大,但能防止局域网内的其他设备偷窥通信内容。
- 增加无线连接白名单。家用路由器都可以设置仅允许指定MAC地址的设备接入2.4GHz网络,把ESP32节点的MAC都加入白名单,能大幅压缩被恶意配网或劫持的风险面。
6.3 从单设备到全面布设的组网建议
最后聊聊扩展。我这套方案跑通后,把客厅、卧室、厨房、阳台都布上了ESP32节点,总数现在是十一台。但这十一台设备不是一上来就全部部署的,我的经验是先做一台,稳定一周,再复制。单台设备的代码调通后,批量节点之间只是改改GPIO映射、设备ID、传感器类型,核心框架完全一致。
如果你计划部署超过二十台ESP32,WiFi网络会成为瓶颈。2.4GHz在多个信道上的干扰会显著影响稳定性。这时候有两个方向:
一是把设备按区域分配到不同信道的路由器和AP上,避免竞抢一个信道。二是在核心区域部署ESP32做“边界路由器”,利用ESP32自带的BLE功能做低功耗控制的近场分支,减少所有节点都挤WiFi的情况。
我还没把BLE Mesh推上位,主要原因是ESP32的BLE Mesh开发复杂度较高,而现有场景在二三十台设备规模下,WiFi星型网络配合MQTT已经足够稳定。如果以后设备量突破五十台,我会认真考虑引入蓝牙Mesh作为低功耗子网,把光照传感器、门磁这类超低带宽节点从WiFi网络里解放出来。到时候整套方案的形态还会有一次大的变化。
说回这两年维护这套系统的体会:技术选型不是越新越好,而是越匹配你对系统的掌控力越好。ESP32的WiFi+BLE组合让我用一套硬件、一套代码同时解决配网、远程、低功耗三个维度的事,对整个系统每一行代码、每一个模块都有主动权。居家智能家居,最怕的不是动手能力不够,而是整个系统变成了无法维护的黑箱。自己搭这套方案的收获远不止“能用手机控制灯”这件事,当你把配网、状态上报、OTA、掉电恢复这一整条链路都盘顺之后,你会发现后面再做任何IoT开发都会顺手很多。