1. 项目概述与整体方案拆解
1.1 为什么最终选了ESP32做智能家居的主力
这个项目最初不是一拍脑袋定的。我最早用ESP8266做开关和灯控,价格确实便宜,也能跑WiFi,但做完整套智能家居后痛点很明显:没有BLE、内存和Flash偏小、跑HTTPS和异步Web吃力、多IO复用也不够灵活。后来换到ESP32,才算把"WiFi+BLE"这条链路彻底打通。你可以把它理解为"一个兼具无线网关和本地控制能力的通用平台":WiFi负责连接云端、局域网高速通信,BLE负责配网、近场控制、低功耗传感器采集,两个协议栈共存在一颗低成本的MCU里,不用再额外加蓝牙网关。这个组合对于小户型家庭或学生DIY项目来说,性价比极高。
1.2 "一站式"不是个口号,而是明确的职责分工
我设计这套方案时的核心理念很简单:WiFi管"连接稳定性",BLE管"连接便捷性"和"低功耗节点接入"。WiFi链路承载MQTT消息、Web页面、OTA升级这些高数据量或长连接任务;BLE链路承担三类事:第一是给无屏幕设备快速配网,第二是作为BLE网关接收周围温度计、门磁、开关这类设备的广播数据,第三是手机近距离直连控制,比如你站在设备旁,App哪怕没有路由器也能直接开灯。这三件事如果全交给WiFi做,要么配网体验很差,要么节点功耗压不下来。用表格看分工更直观:
| 链路 | 承担任务 | 典型场景 |
|---|---|---|
| WiFi | MQTT远程控制、Web页面、固件OTA、状态上报 | 人不在家、局域网统一控制 |
| BLE | 无屏配网、近场调试、低功耗传感器采集 | 首次安装、传感器待机、手机靠进设备直控 |
| 共用 | 设备状态维护、配网信息存储、掉线重连 | 日常运行 |
这套方案做下来,家里十几个设备不需要每台都接电源线去路由器附近配置,BLE先把WiFi账号密码传给设备,设备自己切换到WiFi,然后统一走MQTT进Home Assistant或自研App。整体调试下来,稳定性和灵活性确实比单WiFi方案好一大截。
2. 硬件搭建与关键器件选择
2.1 ESP32主控模组怎么选才不翻车
ESP32家族型号很多,这里不介绍太细,只说我实际用下来最推荐的三类。经典款ESP32-WROOM-32依然是性价比之王,双核240MHz、4MB Flash,Arduino和ESP-IDF生态最全,普通灯控、传感器、小屏显示完全够用。如果你的主控不仅要跑MQTT,还要同时做Web服务器和BLE网关,优先选它。ESP32-S3多了一些AI加速和更多GPIO,USB原生支持,做带摄像头或屏幕的面板很合适,但注意它用的是RISC-V核,少数老库要适配。ESP32-C3便宜、单核、体积小,做纯BLE低功耗节点或简单开关够用,但如果你要跑Web页面加MQTT加BLE扫描,内存会紧张,我建议新手第一块板子不要选C3。我自己最终部署的主节点用的是ESP32-WROOM-32,理由就是稳定、资料多、不折腾。
2.2 传感器、继电器和显示模块的搭配
智能家居最常用的几类外设我列一下:温湿度采集我用过DHT11、DHT22和SHT30,DHT11便宜但精度差,同一个环境测出来能差好几度,DHT22勉强够用,SHT30是I2C接口、精度高、长期稳定性好,预算够就直接上SHT30。温度单独监控可以用DS18B20,防水封装版本适合放在水箱、暖气管道旁边。继电器模块要选带光耦隔离的低电平触发版本,ESP32的GPIO输出能力有限,直接驱动线圈容易导致重启或烧引脚,必须经过三极管或光耦。显示方面,OLED用SSD1306,0.96寸的,I2C两线就能接,调优时把IP地址、配网状态直接显示出来,比看串口方便很多。另外如果你不打算做实体按键,最好留两个GPIO做触摸按钮,用于恢复出厂设置和手动开关,这是调试时的救命按键。
2.3 供电和IO分配的坑
供电是整个项目里最容易被忽视的部分。ESP32在WiFi发射瞬间电流能飙到300mA以上,如果用普通USB线接稳压模块,电压跌落会造成反复重启。我的经验是:使用5V 2A以上的适配器,通过AMS1117-3.3稳压给模组供电,但AMS1117压差大,输入5V时输出3.3V效率一般,实际做多个负载还是建议用MP1584或LM2596这类DC-DC降压模块,把5V降成3.3V给逻辑电路,同时保留5V给继电器模块。IO分配要避免使用GPIO0和GPIO2,它们和启动模式、Flash相关,容易造成下载失败;继电器、蜂鸣器这类电磁负载尽量安排在靠外的引脚,同时加续流二极管。我第一版就是因为继电器直接接在GPIO12上,上电偶尔启动异常,排查了很久。
3. WiFi链路实现:从配网到MQTT与Web控制
3.1 配网方式对比,为什么最终选了BLE配网
ESP32常见配网方式有三种:SmartConfig、AP配网、BLE配网。SmartConfig是手机App发UDP广播包,设备监听获取SSID和密码,体验不错但兼容性一般,有些路由器或者手机系统会限制组播。AP配网是设备自己开热点,手机连上热点后访问192.168.4.1填WiFi信息,这个最通用,但用户操作步骤多,尤其设备在客厅,手机要断开自己的WiFi去连设备,来回切换很烦。BLE配网是最适合"一站式"的方案:手机App直接扫描附近广播,通过BLE服务把WiFi账号和密码写入设备,设备马上切换WiFi并反馈结果。整个过程用户不需要切换网络,也不需要在网页上输密码,体验就像用蓝牙耳机一样。如果你的项目是给家人或朋友用,强烈建议优先上BLE配网。当然,如果只是自己调试,AP配网也挺方便。
3.2 MQTT通信协议与主题设计
设备连上WiFi之后,我统一走MQTT协议,它比HTTP适合设备间的长连接和实时消息。局域网部署Mosquitto,外网映射或云服务器部署同样的Broker,设备端通过TLS加密连接。MQTT主题设计如果不规划好,设备一多就乱。我的模板是:
- 状态上报:
device/{device_id}/state,payload为JSON,包括在线状态、传感器值、固件版本。 - 指令下发:
device/{device_id}/cmd,App或Home Assistant发到这里。 - 在线遗嘱:
device/{device_id}/online,使用MQTT的LWT机制,异常掉线自动发布offline。
特别注意QoS等级。控制继电器用QoS 1,状态上报用QoS 0就可以,不要全用QoS 2,资源占用高且对智能家居场景不划算。保留消息Retained flag建议在/state开启,这样新设备上线或者App刚打开就能立刻拿到最新状态,不用等下一次上报。
3.3 内嵌Web页面做本地控制,不依赖云端
很多方案把控制全部放到云平台,结果一旦外网断了,灯都开不了。我在ESP32里用LittleFS存了一个轻量级Web页面,通过AsyncWebServer提供本地HTTP服务,手机连同一个WiFi,浏览器访问设备IP就能看到控制界面。这个页面不依赖外网,平时作为备份。
实现上我用的是ESPAsyncWebServer配合ArduinoJson,页面里用原生HTML+JavaScript,轮询或者WebSocket拉取状态。WebSocket更实时,但代码量会多一点;简单场景可以用每1秒轮询一次/api/state,返回温度湿度和继电器状态。对于内联Web页面,有一个细节:小文件可以打包成.h数组直接烧进去,更新页面麻烦;用LittleFS存单独的HTML文件,更新页面时只要通过OTA上传文件系统镜像就行。我个人经验是这个local Web控制不需要做太复杂,保留开关、状态显示、配网重置三个功能就可以了。
4. BLE链路实现:配网、近场控制与网关采集
4.1 BLE的GATT基础概念,用一句话讲透
很多刚开始上手BLE的人被Service、Characteristic、UUID这些词绕晕。我用一个快递柜类比:GATT就是货运系统的规范,Service是快递柜的"分类格口",Characteristic是每个格口里的"小包裹"——实际交换的数据就装在里面。你要告诉App"往某个格口塞一个WiFiSSID包裹",就是在某个Characteristic上产生一次Write。反过来ESP32要主动告诉手机"配网成功了",就通过Notify把结果推送过去。
动手之前建议先安装esp32 BLE相关库,Arduino环境下用官方BLEUtils、BLEServer、BLECharacteristic就可以。不需要额外第三方库,官方库足够稳定。
4.2 用BLE快速配网的具体设计
我的配网流程是这样设计的:设备上电后先查NVS里是否已经存有WiFi配置,如果有就直接连WiFi;如果没有,进入BLE配网模式,广播名称类似ESP32_Config_XXXX。手机App扫描到后连接这个设备,往write_ssid特征写入WiFi名称,往write_pwd特征写入WiFi密码,然后写一个start_connect命令,ESP32收到后开始连接WiFi,并通过Notify反馈连接状态。为了避免WiFi密码明文暴露在广播里,app和设备之间走的是BLE连接加密通道,不需要密码本身就是BLE的配对绑定机制,我推荐使用Bonding模式,首次配对后设备信任手机,比直接开放特征值安全得多。
配网超时设定为5分钟,超时后自动广播停止并进入深睡,省电且避免一直处于可连接状态。成功连接WiFi后把SSID和密码写入NVS,下次开机直接读NVS,不再进配网模式。只有长按恢复键或连续三次连接失败时,才清除NVS重新进入配网。
4.3 把ESP32当BLE网关,收集低功耗传感器
除了自己作为BLE外设提供配网服务,ESP32还能切换到中心设备模式,扫描周围的BLE广播包。这套方案里我最满意的一点是:家里那些用纽扣电池的BLE温湿度计、门窗磁、人体传感器,本来都需要专用网关,现在直接用ESP32扫它们的广播就能接收数据。BLE广播包里的Manufacture Specific Data是各传感器的私有数据段,不同品牌协议不同,但大部分厂商会开源解析协议或公开文档。
实现上注意连续扫描和被动扫描的区别。ESP32默认可以做主动扫描,但为了省电,我一般设置被动扫描,只监听广播包不发起连接,然后设置窗口间隔比,扫描窗口占空比调到10%~50%之间。这样ESP32能同时保持WiFi连接和BLE扫描,实测对WiFi吞吐影响不大。收到广播后解析出温度、湿度、电池电量,包装成JSON再用MQTT上报,这一步就让一个普通ESP32变成了多协议网关。
4.4 低功耗电池设备怎么配合这个系统
如果你希望某些节点完全不拉电线,那要考虑省电策略。一颗ESP32-C3在深睡模式下电流可以到10µA左右,但要注意:深睡期间WiFi和BLE全停,只能定时唤醒发一包数据。我的做法是让C3每5分钟醒来一次,采集一次传感器数据,通过WiFi发MQTT,然后睡6分钟。由于WiFi从连接到发送要1~2秒,相当于0.4%的占空比,三节AA电池能撑很久。如果你用的是纯BLE节点,则可以用更省电的方案,让ESP32当BLE网关接收即可,传感器节点可以用带BLE协议的MCU,睡眠电流更低。整个系统的原则是:能交给BLE的低频低功耗通信不交给WiFi,WiFi留给实时控制和交互。
5. 一站式系统集成与实操记录
5.1 完整数据流从一个新设备上线说起
新设备上电后在配网模式,手机App通过BLE写入WiFi信息;设备连接WiFi成功后通过MQTT向device/xxx/state发布在线消息;Home Assistant通过MQTT Discovery自动发现设备,并把传感器数据展示到面板。用户点击"打开客厅灯"时,HA发布控制指令到device/004/relay/set,设备订阅到后驱动继电器,并回发状态确认。如果路由器断网,设备不会傻等,它检测到MQTT Broker断开后,每5秒重连,同时本地Web页面仍然可以访问,直接控制继电器。整套流程跑通后,给人的感觉就是"配网时用BLE,使用时全走WiFi,掉线时还能本地兜底"。
5.2 核心代码实现:BLE配网后连接WiFi
拿Arduino IDE举例,我习惯把BLE配网和WiFi连接分文件写,便于维护。核心流程大致如下:
#include <BLEDevice.h> #include <BLEUtils.h> #include <BLEServer.h> #include <WiFi.h> #include <Preferences.h> BLECharacteristic *pWriteSSID; BLECharacteristic *pNotify; Preferences prefs; String ssid = ""; String pwd = ""; bool shouldStartWiFi = false; class WiFiCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *p) override { if (p->getUUID().toString() == "0000ff01-0000-1000-8000-00805f9b34fb") { ssid = p->getValue().c_str(); } else if (p->getUUID().toString() == "0000ff02-0000-1000-8000-00805f9b34fb") { pwd = p->getValue().c_str(); } else if (p->getUUID().toString() == "0000ff03-0000-1000-8000-00805f9b34fb") { shouldStartWiFi = true; } } }; void startBLEConfig() { BLEDevice::init("ESP32_Config_XXXX"); BLEServer *server = BLEDevice::createServer(); BLEService *svc = server->createService("0000ff00-0000-1000-8000-00805f9b34fb"); // 创建三个Characteristic,分别用于写SSID、写密码、启动连接 pWriteSSID = svc->createCharacteristic("0000ff01-0000-1000-8000-00805f9b34fb", BLECharacteristic::PROPERTY_WRITE); ... svc->start(); BLEAdvertising *adv = BLEDevice::getAdvertising(); adv->addServiceUUID("0000ff00-0000-1000-8000-00805f9b34fb"); adv->start(); }这段代码里需要注意几个点:BLE特征值UUID不要随便写,保持服务UUID和特征UUID的128位格式;写入回调里不要做WiFi连接这种耗时操作,否则BLE协议栈会卡住,正确做法是在回调里置标志位,再在主循环里启动WiFi。Preferences用来存储NVS数据,调用putString("ssid", ssid)和putString("pwd", pwd)即可,下一次开机直接读。
5.3 MQTT订阅与传感器上报代码,可直接抄
我用PubSubClient库接MQTT,回调处理指令,loop里定期上报传感器。核心逻辑是:
#include <PubSubClient.h> WiFiClient net; PubSubClient mqtt(net); void mqttCallback(char* topic, byte* payload, unsigned int len) { String msg = String((char*)payload).substring(0, len); if (String(topic) == "device/004/relay/set") { int relayState = (msg == "ON"); digitalWrite(RELAY_PIN, relayState); mqtt.publish("device/004/state", "{\"relay\":\"ON\"}", true); } } void publishSensor() { float t = readSHT30Temperature(); float h = readSHT30Humidity(); String json = "{\"temp\":\"" + String(t) + "\",\"hum\":\"" + String(h) + "\"}"; mqtt.publish("device/004/tele", json.c_str(), false); }MQTT发布状态时建议开启retain,这样App或HA重启后能快速恢复界面。不过传感器遥测数据不需要retain,否则每次打开面板都显示旧值,消费者会混淆。实际跑起来后,传感器每10秒上报一次,WiFi连接异常断开时mqtt.connected()会变成false,在如果断线超时5秒就调用ESP.restart(),别在库层无限重连,那个重连机制不够健壮。
5.4 对接Home Assistant的细节
如果你用Home Assistant,最简单的做法是直接配置MQTT开关和传感器,但每次新增设备都要在HA的YAML里写配置,很繁琐。更好的方案是用HA的MQTT Discovery:设备向homeassistant/switch/device_004/config发布一段JSON描述,HA会自动添加设备。我这里给出关键字段:
{ "name": "Living Room Relay", "state_topic": "device/004/state", "command_topic": "device/004/relay/set", "payload_on": "ON", "payload_off": "OFF", "unique_id": "esp32_relay_004" }把 QoS设为1,Discovery消息要带retain,否则HA重启后就得重新注册。这个模式特别适合批量设备接入,你只需要在ESP32端的配置里写不同device_id,HA面板会自动出实体,不需要在服务器端改文件。
6. 常见问题与排查技巧实录
6.1 BLE配网失败或搜不到设备
如果手机搜不到广播,先确认GPIO0或RST有没有被外设占用,部分模组在低电平时射频关闭。其次看一下广播是否只在配网模式开启,很多情况下你已经把WiFi配置写进NVS,设备默认不广播了。遇到这种情况,长按恢复键清空NVS再试。手机端要注意Android的定位权限必须开启,BLE扫描在Android 6以上需要精确定位权限,iOS需要在App的Info.plist里声明NSBluetoothAlwaysUsageDescription。我踩过最狠的坑是:手机之前配对过同一MAC地址的旧设备,缓存了密钥,新固件换了Service UUID,结果连不上。解决办法是在App里取消绑定设备,或者把设备端蓝牙MAC地址随机化。
6.2 WiFi和BLE同时工作时的信号互相干扰
ESP32的WiFi和BLE共用2.4GHz频段,如果两者同时收发,射频前端会争抢资源,表现为WiFi延迟变大、BLE广播丢包。我的经验是把BLE扫描窗口调小,例如扫描窗口设置为100ms、间隔900ms,这样只占很短的射频时间;在WiFi传输大量数据时暂缓BLE扫描,用esp_bt_controller_disable()临时关闭蓝牙。如果你用的是ESP32-S3,它自带比较好的共存机制,但依然不建议在同一个设备上做高吞吐WiFi下载的同时做高频率BLE扫描。
6.3 烧录失败,开发环境让人崩溃
新用户最容易在Arduino IDE里卡住。核心是选对开发板型号和烧录参数,ESP32-WROOM-32对应开发板选择"ESP32 Dev Module",Flash大小选4MB,Partition Scheme选Huge APP,不然固件太大写不进去。国内网络下载慢的话,可以把Arduino ESP32的板级JSON换成国内镜像源,能省很多时间。另外烧录时按住BOOT键再点上传,特别是使用自动下载电路不稳定的开发板。如果你用PlatformIO,注意board_build.flash_mode设成dio,部分模组在QIO模式下会不稳定。
6.4 传感器读数异常或漂移
DHT系列如果数据跳变,大多数是线太长或供电不稳。SHT30我遇到过一次I2C总线锁死,原因是SCL上拉了4.7k电阻但板子内部已经有了,两个电阻并联导致信号边沿太缓。排查时用逻辑分析仪看波形,如果没有,就先恢复默认I2C引脚。DS18B20如果读到85℃这个著名的数值,说明传感器没在总线上,检查都在一个线上是否缺少4.7k上拉。另外所有模拟信号都要避开DC-DC电感区域,否则数据会有周期性噪声。
6.5 电池供电节点功耗还是太高
很多人说ESP32电池待机撑不了几天,没错,那是因为忘了唤醒后要先稳定电源再干活。电源轨电压在唤醒瞬间会掉,如果传感器还没初始化就读取,会上报错误值。我建议电池节点用一颗电容组做缓冲,MCU唤醒之后延时200ms再采集,然后MQTT连接之前先用WiFi连接探测,如果找不到路由器就直接回去睡,不然重试会消耗巨量电量。做低功耗设备要始终记住一句话:功耗大头是重试,不是工作本身。
7. 一点个人经验与后续扩展方向
这套方案前前后后改了三版,现在稳定跑了一个多月。最深的体会是:不要一开始就追求把所有功能都塞进一颗ESP32,先把WiFi+BLE配合的最小路径打通,再逐步叠加传感器、Web页面、Home Assistant接入。每一步只改一个变量,出现问题好定位。BLE配网和WiFi局域网控制这两个核心链路一旦跑通,后面的扩展都是加分项。
接下来我计划做的扩展方向:一是把BLE网关采集的传感器数据直接通过MQTT转发给Home Assistant,这样家里那些纽扣电池设备能统一入网;二是给设备加OTA升级,通过Web页面直接上传固件,不用再打开Arduino IDE插USB线;三是尝试用ESP32-C3做纯BLE开关节点,和主力ESP32网关配合,实现更灵活的低功耗布局。如果你们也在折腾智能家居,先从一个小房间的灯控加温湿度采集做起,跑通这套WiFi+BLE架构,再往全屋扩,会顺畅得多。