1. 项目概述
1.1 核心需求解析
做智能家居有个绕不开的坑:市面上无论是WiFi插座、BLEmesh灯泡还是Zigbee网关,各玩各的协议,一个APP控制一个生态。我在家里组了几套设备之后,手机里光智能家居APP就有四五个,更别提有些设备断连之后重新配网那个折腾劲。后来接触到ESP32这颗芯片,发现它有意思的地方在于原生支持WiFi和BLE双模通信,一颗几块钱的国产芯片能把两套连接协议同时跑起来。于是决定自己动手,做一套能用手机统一控制的智能家居方案,把温湿度采集、开关控制、远程状态查询全部整合到一块板子上。
这次做的东西,核心思路是让ESP32当家庭网络的中枢节点,一边通过WiFi连路由器上云,实现手机远程控制;另一边通过BLE广播和连接与附近的小设备通信,比如BLE温湿度计、BLE开关模块,这样室内设备不用全部挤在2.4G WiFi频段上抢带宽。整套方案最终实现的效果是:手机APP通过云端MQTT下发命令给ESP32,ESP32通过GPIO控制继电器和采集温湿度,同时ESP32通过BLE连接收集周围低功耗传感器的数据,一条链路把远程控制和近场采集全部打通。
1.2 方案选型与适用场景
硬件核心选的是ESP32-WROOM-32模组,理由是这颗芯片在同类价格区间内几乎没有对手。乐鑫的ESP32集成2.4GHz WiFi和蓝牙双模,支持BLE 4.2,内置 Tensilica Xtensa双核处理器,主频能到240MHz,SRAM有520KB,Flash则看模组配置,常用的是4MB或8MB。对比同类方案,8266只有WiFi没有BLE,树莓派性能强但价格高功耗大,ESP32综合下来是智能家居中枢的最优解。
这套方案适合谁?如果你家里有几个设备需要统一管理,想入门嵌入式开发但不想碰Linux那么重的开发方式,或者纯就是想用最低成本搞定"手机远程控制家里电器"这件事,照着这篇文章走一遍就能跑起来。我会把从引脚规划、开发环境搭建、WiFi联网、BLE通信到传感器接入的完整链路都拆开讲,尽量把我在踩坑过程中解决掉的问题也一并交代清楚,免得你重复交学费。
2. 硬件准备与引脚规划
2.1 核心芯片选型对比
手头如果已经有ESP32开发板,直接上手就行。市面上常见的ESP32开发板有两大类:一类是合宙、NodeMCU等厂商出的经典32引脚小板,带USB转串口芯片,插上电脑就能烧录;另一类是纯模组加转接板,需要外部供电和下载电路,适合做产品原型。新手建议用前者,省心。
如果你手头同时有ESP8266和ESP32,纠结选哪颗来做智能家居,可以参考这个对比表格:
| 对比项 | ESP8266 | ESP32 | 我的结论 |
|---|---|---|---|
| WiFi | 2.4G b/g/n | 2.4G b/g/n,支持MIMO? | ESP32更稳 |
| BLE | 不支持 | 4.2标准BLE | 没有可比性 |
| 核心数 | 1个Xtensa | 2个Xtensa LX6 | 多任务不容易互相卡 |
| SRAM | 160KB可用户使用 | 520KB | 跑MQTT+BLE+传感器够用 |
| GPIO | 少量 | 丰富,支持触摸、DAC等 | 扩展性更强 |
| 价格 | 约6-10元 | 约12-20元 | 差价不大 |
问自己一个问题:后期要不要加蓝牙设备?要加,就不用犹豫了,直接ESP32。
2.2 引脚分配与接线说明
ESP32的引脚并不像Arduino Uno那样随便接都可以,很多引脚有复用功能,选择IO口时需要注意几点:GPIO 6到GPIO 11是接Flash的,绝对不能用作普通IO;GPIO 0在下载时必须拉高,否则会进入下载模式;GPIO 34到39是纯输入引脚,不能输出PWM。
我这次方案的引脚分配是这样的:
| 功能 | 引脚 | 说明 |
|---|---|---|
| DHT22温湿度数据 | GPIO 4 | 数据脚接4.7K上拉电阻到3.3V |
| 继电器1(控制灯) | GPIO 16 | 低电平触发 |
| 继电器2(控制插座) | GPIO 17 | 低电平触发 |
| 板载LED状态指示 | GPIO 2 | 上电闪一下,WiFi连上常亮 |
| BLE调试串口(备用) | GPIO 1/3 | 串口日志输出 |
| I2C接口(备用扩展) | GPIO 21/22 | SDA/SCL,后续接OLED等 |
接线这块要特别提醒:DHT22供电接3.3V,数据引脚和电源之间一定要加上拉电阻。我第一次偷懒没加上拉,读取的温度值隔一段时间就跳到-999,折腾了半天才发现是引脚电平飘了。继电器模块选择3.3V驱动的低电平触发型号,如果是5V高电平触发的那种,需要额外加三极管或光耦做电平转换,不能直接接GPIO输出。
2.3 外围器件清单
整个方案需要的物料不多,我整理了一份清单:
- ESP32-WROOM-32开发板 x1
- DHT22(AM2302)温湿度传感器 x1
- 1路继电器模块(3.3V驱动)x2
- 面包板或洞洞板 x1
- 4.7K欧电阻 x1
- 5V USB电源(最低2A) x1
- BLE温湿度计(成品,可读取蓝牙广播数据) x1
- 杜邦线若干
如果手头没有DHT22,用DHT11也可以,只是测温范围和精度略差一点。DHT11分辨率只有1摄氏度,DHT22能做到0.1摄氏度,做智能家居联动控制,DHT22更合适。预算也就差一块钱左右。
3. 开发环境搭建与烧录
3.1 Arduino IDE + ESP32扩展安装
开发方式我选了Arduino IDE而不是乐鑫官方的ESP-IDF。原因很简单:这个项目不需要深层次的芯片寄存器操作,Arduino框架已经把WiFi、BLE、传感器这些库封装得很完善,写出业务逻辑的时间大大缩短。等真要做量产固件优化内存的时候,再迁移到ESP-IDF也不迟。
安装过程有几个注意点,尤其是国内网络环境下:
打开Arduino IDE(我用的是2.1.1版本),进入"文件 -> 首选项 -> 附加开发板管理器网址",填入以下URL:
https://espressif.github.io/arduino-esp32/package_esp32_index.json如果这个地址下载极慢,可以改用乐鑫的国内镜像源:
https://esp32.alibaba.com/arduino-esp32/package_esp32_index.json然后打开"工具 -> 开发板 -> 开发板管理器",搜索esp32,选择安装。当前稳定版是2.0.14或3.x(我写这篇文章时,3.0.2已经可以正常使用了)。下载包大约几百MB,慢慢等。
注意:Arduino IDE 2.x版本安装ESP32扩展的时候,如果中途断开,重新点安装按钮一般可以继续下载,不需要删除重来。
安装完成后,在"工具 -> 开发板 -> ESP32 Arduino"下选择自己的开发板型号。用了经典款的就选"ESP32 Dev Module",用合宙ESP32C3就选对应的C3系列。板载Flash大小选择4MB(8MB的选8MB)。串口选择是重点,Windows下一般会出现COM3或COM4之类,如果驱动没装好,需要先装CP2102或CH340的USB转串口驱动,我这里用的是CP2102芯片,板子插上电脑后系统能自动识别。
3.2 第一次烧录与串口监视器配置
确认硬件连接没问题后,先把一个最基础的LED闪烁程序烧进去验证环境是否正常。打开"文件 -> 示例 -> 基础 -> Blink",把LED_BUILTIN改成2(对应板载GPIO 2),然后点烧录。
烧录过程中按住开发板上标着"BOOT"的按钮,在串口输出出现"Connecting..."时松开,可以防止烧录失败的情况。实际上大部分开发板不需要手动按BOOT,但偶尔会因为串口芯片兼容性导致连接超时,这时候按一下BOOT再试,十有八九能成。
首次烧录完成之后,打开串口监视器,波特率设置成115200,这时候能看到芯片打印的启动日志。正常启动会显示类似下面这样的内容:
ets Jun 8 2016 00:22:57 rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) ...看到boot:0x13说明启动模式正常,如果是boot:0x00则说明进入了下载模式,这时候需要按一下开发板上的RST,或者重新上电。
3.3 板子连不上电脑的排查
烧录环境搭好之后,最常卡住新手的其实是连不上电脑。我整理了几个高频问题供对照:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 电脑完全没有识别到新设备 | 数据线只有充电没有数据传输 | 换一根短线,最好是有品牌标识的USB数据线 |
| 设备管理器显示黄色感叹号 | 驱动没装或冲突 | 重新安装CP2102/CH340驱动,右键卸载后重装 |
| 烧录时报错"无法打开串口" | 串口被占用 | 关掉其他串口工具,确认Arduino IDE已选对串口 |
| 烧录进度卡在Connecting | 自动下载电路不工作 | 按住BOOT键再点烧录,出现Connecting时松开 |
4. WiFi通信链路实现
4.1 连接路由器的代码实现
WiFi这块用Arduino框架的WiFi库就能搞。核心目标有两个:ESP32要稳定联网,同时重连机制要可靠。智能家居设备最怕的就是路由器重启之后设备回不来,所以WiFi自动重连的逻辑必须写扎实。
我直接放一段实测可用的连接代码,放在setup函数里调用:
#include <WiFi.h> const char* ssid = "你的WiFi名称"; const char* password = "你的WiFi密码"; void setupWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 40) { delay(500); attempts++; Serial.print("."); } if (WiFi.status() == WL_CONNECTED) { Serial.println("\nWiFi connected"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); } else { Serial.println("\nWiFi connect failed, will retry in loop"); } }注意几个细节:WiFi.mode(WIFI_STA) 要先指定工作模式,有些教程直接调用WiFi.begin也行,但先设定模式会让连接更稳定;连接超时时间设置成20秒(40次乘500毫秒),超过这个时间就放弃连接,避免阻塞后续代码执行。如果经常因为路由器信号弱导致第一次连接失败,可以在loop里定期检查状态并重连。
4.2 Web控制页与HTTP服务器
联网之后最简单的控制方式就是直接在浏览器里访问ESP32的IP地址,看到一个网页,点按钮控制继电器。虽然项目后期的目标是用手机APP远程控制,但Web控制页作为调试辅助非常有用,不用打开APP就能快速验证设备响应。
用ESP32自带的WebServer库实现,代码如下:
#include <WebServer.h> WebServer server(80); void handleRoot() { String html = "<html><body>"; html += "<h1>ESP32 智能家居控制</h1>"; html += "<p>灯光:<a href=\"/relay1/on\">打开</a> "; html += "<a href=\"/relay1/off\">关闭</a></p>"; html += "<p>插座:<a href=\"/relay2/on\">打开</a> "; html += "<a href=\"/relay2/off\">关闭</a></p>"; html += "</body></html>"; server.send(200, "text/html", html); } void handleRelay1On() { digitalWrite(RELAY1_PIN, LOW); // 低电平触发 server.send(200, "text/plain", "relay1 on"); }这套Web控制页在局域网内测试非常方便,手机浏览器连同一个WiFi,输入192.168.x.x就能控制。我还加了一个根路径的JSON接口,返回当前温湿度和继电器状态,方便后面做APP的时候抓取状态:
String getStatusJSON() { float temp = readTemperature(); float hum = readHumidity(); String json = "{"; json += "\"temperature\":" + String(temp) + ","; json += "\"humidity\":" + String(hum) + ","; json += "\"relay1\":" + digitalRead(RELAY1_PIN); json += "}"; return json; }4.3 上云方案:MQTT远程控制
局域网控制只是第一步,真正实用的智能家居还需要支持出门在外远程控制。上云方案主流有几种,我最后选的是MQTT协议加公共/自建Broker。MQTT的优点是消息极其轻量、始终保活、支持遗嘱消息,非常适合像ESP32这种低功耗但一直挂网的设备。
我用的是Mosquitto作为Broker,如果不想自己搭服务器,也可以先用公共测试Broker(比如broker.emqx.io)做验证。设备端连接MQTT的核心代码:
#include <PubSubClient.h> WiFiClient espClient; PubSubClient mqtt(espClient); const char* mqtt_server = "你的MQTT服务器地址"; const int mqtt_port = 1883; const char* mqtt_topic_sub = "home/esp32/command"; const char* mqtt_topic_pub = "home/esp32/status"; void reconnectMQTT() { while (!mqtt.connected()) { if (mqtt.connect("ESP32Client", "用户名", "密码")) { mqtt.subscribe(mqtt_topic_sub); Serial.println("MQTT connected"); } else { delay(5000); } } } void callback(char* topic, byte* payload, unsigned int length) { String message; for (int i = 0; i < length; i++) { message += (char)payload[i]; } if (String(topic) == mqtt_topic_sub) { if (message == "relay1_on") { digitalWrite(RELAY1_PIN, LOW); } else if (message == "relay1_off") { digitalWrite(RELAY1_PIN, HIGH); } // 发布状态回执 mqtt.publish(mqtt_topic_pub, getStatusJSON().c_str()); } }这里最容易忽略的是MQTT消息格式的约定。自己玩还好,如果设备一多,比如客厅灯、卧室空调、厨房窗帘,最好统一topic格式,比如home/{room}/{device}/command,不然到后面回调函数里写一堆if-else,维护起来想哭。我在实际项目里用这种结构化了之后的topic,手机端APP只需要按房间订阅就能控制对应设备,互不干扰。
5. BLE通信链路实现
5.1 为何还需要BLE通道
WiFi链路搞通了之后,按道理远程控制已经没问题了,那为什么还要做BLE这部分?我的实际体会是:WiFi适合连互联网、传大量数据,但功耗高、连接繁琐、在弱信号区域容易断;BLE则相反,功耗极低,连接速度快(毫秒级),非常适合近距离小数据量通信。
智能家居的真实场景往往是两者配合:人在外面通过WiFi/MQTT远程控制;人在屋里,手机靠近设备,想快捷操作,这时候BLE就派上用场。更关键的是,很多温湿度传感器都是BLE广播协议,比如常见的米家蓝牙温湿度计,价格二十来块,ESP32作为中枢节点直接作为BLE GATT客户端去读取这些传感器数据,比自己接一堆模拟传感器省钱、省事、还省布线。
我做这套方案时,ESP32同时承担两个BLE角色:一个是BLE GATT服务器,把继电器状态、温湿度数据暴露给手机APP直接读取;另一个是BLE GATT客户端,主动扫描并连接外围的BLE传感器,采集数据。一台ESP32同时跑两个角色,Arduino上实现起来有点绕,但乐鑫的Bluedroid协议栈支持这种模式。
5.2 BLE GATT服务端配置
BLE-GATT服务端这块,我建了一个自定义服务,UUID用16位短UUID就够了(1110),服务下面放三个特征值:
| 特征值UUID | 属性 | 功能 |
|---|---|---|
| 0x1111 | 只读Notify | 返回温湿度数据 |
| 0x1112 | 可读可写 | 控制继电器1和2 |
| 0x1113 | 只读 | 返回设备状态信息 |
需要用到的完整128位UUID,Android端和iOS端在扫描时一般会自动转换成短UUID,但有些库要求填完整的,我把完整字符串放在下面,直接用就行:
服务UUID: 00001110-0000-1000-8000-00805F9B34FB 特征值1: 00001111-0000-1000-8000-00805F9B34FB 特征值2: 00001112-0000-1000-8000-00805F9B34FB 特征值3: 00001113-0000-1000-8000-00805F9B34FB服务端初始化代码核心片段:
#include <BLEDevice.h> #include <BLEServer.h> #include <BLEUtils.h> #include <BLE2902.h> BLEServer* pServer = NULL; BLECharacteristic* pRelayControl = NULL; #define SERVICE_UUID "00001110-0000-1000-8000-00805F9B34FB" #define CHAR_RELAY_UUID "00001112-0000-1000-8000-00805F9B34FB" class MyServerCallbacks : public BLEServerCallbacks { void onConnect(BLEServer* server) { Serial.println("BLE client connected"); } void onDisconnect(BLEServer* server) { Serial.println("BLE client disconnected"); BLEDevice::startAdvertising(); // 断开后重新广播 } }; class MyRelayCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic* pCharacteristic) { std::string value = pCharacteristic->getValue(); if (value.length() > 0) { if (value[0] == '1') digitalWrite(RELAY1_PIN, LOW); else if (value[0] == '0') digitalWrite(RELAY1_PIN, HIGH); } } }; void setupBLE() { BLEDevice::init("ESP32_SmartHome"); pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); BLEService* pService = pServer->createService(SERVICE_UUID); pRelayControl = pService->createCharacteristic( CHAR_RELAY_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE ); pRelayControl->setCallbacks(new MyRelayCallbacks()); pService->start(); BLEAdvertising* pAdvertising = BLEDevice::getAdvertising(); pAdvertising->addServiceUUID(SERVICE_UUID); BLEDevice::startAdvertising(); Serial.println("BLE server started, waiting for connections..."); }这段代码里面有一个关键点:断开连接之后必须重新启动广播,否则手机端再次搜索就找不到这个设备了。很多第一次用BLE库的开发者都会踩到这个坑。
5.3 BLE扫描客户端:读取低功耗传感器
第二部分是ESP32作为BLE客户端,扫描附近的BLE广播数据。现在市面上很多蓝牙温湿度计就是不断向外广播数据包的,不需要连接就能直接读取。比如有些传感器在广播包里会把温度和湿度放在特定字节位置,只需要解析广播数据里的厂商自定义字段就行。
我用ESP32扫描并读取一个成品蓝牙温湿度计的代码,思路是设置好扫描回调,匹配设备名之后解析广播数据:
#include <BLEDevice.h> #include <BLEUtils.h> #include <BLEScan.h> BLEScan* pBLEScan; class MyAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) { if (advertisedDevice.getName() == "ATC_123456") { // 解析广播数据中的温湿度 std::string rawData = advertisedDevice.getManufacturerData(); if (rawData.length() >= 6) { // 根据你手上设备的协议格式解析 int16_t tempRaw = (int16_t)(rawData[4] | (rawData[5] << 8)); float temperature = tempRaw / 100.0; Serial.printf("BLE Sensor Temp: %.2f C\n", temperature); } } } }; void setupBLEScan() { BLEDevice::init(""); pBLEScan = BLEDevice::getScan(); pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); pBLEScan->setActiveScan(true); pBLEScan->setInterval(100); pBLEScan->setWindow(99); pBLEScan->start(30); // 扫描30秒 }不同品牌的蓝牙传感器,广播数据格式完全不一样,有的是厂商字段放设备名,有的是放温湿度,还有的需要连上之后走GATT读取特征值。最稳妥的办法是先下载一个BLE调试助手类工具(手机应用商店搜"nRF Connect"或"BLE调试助手"),扫描到设备后查看原始广播包,再对着解析。这个我在后面"常见问题"部分会详细讲一遍实操流程。
5.4 手机APP端快速联调
调试BLE功能时我没一开始就写完整APP,而是先用现成的调试工具验证GATT服务和特征值。这里推荐两个工具:iOS端用LightBlue,Android端用nRF Connect。以nRF Connect为例,联调步骤是:
- 打开APP,扫描到名为"ESP32_SmartHome"的设备
- 点击连接,查看服务列表
- 找到UUID以1110开头的服务
- 点击服务下的特征值1112,选择"写入",输入"01"或"00",观察ESP32板载LED或继电器是否有动作
- 订阅特征值1111,看能不能收到温湿度推送
实测下来,nRF Connect能完美模拟手机APP的读写操作,比来回改代码烧录调试效率高太多了。等确认GATT服务一切正常之后,再去写正式的手机应用,会省掉很多调试时间。
6. 温湿度采集与数据联动
6.1 DHT22传感器接入与数据精度
DHT22是智能家居里性价比很高的温湿度传感器,温度范围-40度到80度,精度正负0.5度;湿度范围0到100%RH,精度正负2%。在室内环境完全够用。当然要求更高的场景比如温室大棚或实验室,可以考虑SHT30或BME280,但DHT22做家庭场景是平衡之选。
用Arduino库读取DHT22,代码非常简洁:
#include <DHT.h> #define DHTPIN 4 #define DHTTYPE DHT22 DHT dht(DHTPIN, DHTTYPE); void setup() { Serial.begin(115200); dht.begin(); } void readAndPublishDHT() { float h = dht.readHumidity(); float t = dht.readTemperature(); if (isnan(h) || isnan(t)) { Serial.println("Failed to read from DHT sensor!"); return; } mqtt.publish("home/esp32/temperature", String(t).c_str()); mqtt.publish("home/esp32/humidity", String(h).c_str()); // 并将数据同步到BLE特征值 pTemperature->setValue(String(t).c_str()); pTemperature->notify(); }读完数据后有件事一定别漏:把数据同时推送到MQTT和BLE。这样手机APP无论在远程还是局域网近场,都能拿到最新状态,不用拉取的时候才去读传感器。另外我建议不要每秒钟读一次,DHT22本身响应时间大概是2秒一次,频繁读取不但数据抖动厉害,传感器还容易锁死。正常情况下我设置的是每30秒读一次、推送一次,这样省电、占用网带宽也低。
6.2 温湿度数据与继电器联动策略
数据有了,联动控制才有智商,而不是人肉开关。我在代码里加了一个简单的环境判断逻辑:如果温度超过设定的阈值(比如30度),自动打开风扇(继电器2);当温度降回27度以下就关闭风扇。湿度超过60%就发送告警消息到手机。
代码实现:
float tempThresholdHigh = 30.0; float tempThresholdLow = 27.0; bool fanState = false; void handleAutoFan() { float t = dht.readTemperature(); if (isnan(t)) return; if (t > tempThresholdHigh && !fanState) { digitalWrite(RELAY2_PIN, LOW); // 打开风扇 fanState = true; mqtt.publish("home/esp32/event", "fan_on_by_temp"); } else if (t < tempThresholdLow && fanState) { digitalWrite(RELAY2_PIN, HIGH); // 关闭风扇 fanState = false; mqtt.publish("home/esp32/event", "fan_off_by_temp"); } }这种联动逻辑比较基础,但做智能家居的意义正在于此,用温度、湿度、光照等传感器数据自动驱动设备,而不是永远依赖人按按钮。后续想升级,思路可以是这样:接入光照传感器,天暗自动开灯;接入人体红外,人进卫生间自动亮灯;把联动规则放到MQTT Broker的消息转发里,设备端只上报数据和执行指令,规则引擎单独处理。这样ESP32端代码几乎不用改,联动策略在服务端改一下JSON就行。
7. WiFi与BLE联动控制与状态管理
7.1 双协议栈共存的实际调优
WiFi和BLE同时开启之后,最直观的感受是ESP32的功耗变高了,但正常运行时功耗在200mA到300mA之间,用USB供电毫无压力。真正需要注意的是协议栈共存时可能导致的连接稳定性问题,比如BLE连接时WiFi Ping延迟偶尔会飙升到几百毫秒。
原因是ESP32内部WiFi和BLE共用2.4GHz频段的射频前端。乐鑫官方推荐的解决方案是用ESP32的BT/WiFi共存接口,但Arduino框架里已经默认做了处理,常规使用基本不用干预。只有遇到WiFi频繁断流的情况,我建议做两件事:一是调整WiFi的PHY速率,二是减少BLE广播间隔。BLE广播间隔默认是100ms,如果改成200ms,对WiFi的干扰会小很多,代价是手机搜索设备的时候会慢一点。
7.2 统一状态模型与数据同步
两套通道共存后,状态管理就变得非常重要。所谓状态管理,核心是保证"手机APP看到的设备状态"和"ESP32真实引脚输出状态"保持一致。我之前吃过亏:手机显示灯亮着,实际上ESP32重启过了,引脚默认是高电平灯灭了,APP还停留在"亮"的状态。所以后来我设计了一个简单的状态模型,在ESP32端维护一个全局结构体:
typedef struct { bool relay1Status; bool relay2Status; float temperature; float humidity; unsigned long lastUpdate; bool bleSensorConnected; } DeviceState; DeviceState deviceState = {false, false, 0.0, 0.0, 0, false}; void updateStateAndNotify() { String statusJson = getStatusJSON(); // 推MQTT mqtt.publish("home/esp32/status", statusJson.c_str()); // 更新BLE特征值 pStatusChar->setValue(statusJson.c_str()); pStatusChar->notify(); // 更新Web服务器上的JSON currentStatus = statusJson; }每次继电器动作、传感器数据更新、BLE连接状态变化,都必须走一次updateStateAndNotify(),这样不论手机端当前使用的是MQTT还是BLE通道,看到的始终是同一份数据。后面我调试的时候发现,大部分"APP上操作没反应"的bug,根源都是状态同步没做好,而不是硬件问题。
7.3 连接策略与回退机制
设备启动之后,WiFi和BLE应该同时开始工作,但控制逻辑上需要区分优先级。我采用的是WiFi优先策略:
| 连接场景 | 首选通道 | 回退通道 |
|---|---|---|
| 手机在局域网内 | WiFi直连IP | BLE快速连接 |
| 手机在远程 | MQTT云服务器 | 不可用 |
| BLE传感器数据 | BLE扫描/连接 | 本机DHT22直采 |
| 近场快速操作 | BLE(无需等网络) | WiFi Web控制 |
实际写代码时,我在主循环里轮询两种协议的连接状态,并打印关键指标:
void loop() { // 检测WiFi和MQTT连接状态 if (WiFi.status() != WL_CONNECTED) { setupWiFi(); } if (!mqtt.connected()) { reconnectMQTT(); } server.handleClient(); mqtt.loop(); // 周期性读取本地传感器并发布 static unsigned long lastReadTime = 0; if (millis() - lastReadTime > 30000) { readAndPublishDHT(); lastReadTime = millis(); } // 周期性扫描BLE外围数据 static unsigned long lastScanTime = 0; if (millis() - lastScanTime > 60000) { // 触发一次短扫描,获取BLE传感器数据 startBLEScanOnce(); lastScanTime = millis(); } handleAutoFan(); delay(10); }这样写的好处是:WiFi断了不丢控制权,BLE依旧能用;MQTT没连上不卡死主线程,本地功能照常工作;所有周期性任务统一放在主循环里调度,逻辑清楚,后期有新人接手代码也不至于一头雾水。
8. 常见问题与排查实录
8.1 常见问题速查表
这节我整理了自己和身边朋友在实际搭建过程中遇到的高频问题,直接贴速查表,方便你遇到相同问题时按图索骥:
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 板子不上电,LED完全不亮 | USB线只供电无数据、TYPE-C线只充电 | 换线测试、检查USB转串口芯片驱动 | 换优质数据线;没有串口芯片的裸板需要用USB转TTL模块 |
| 烧录成功但设备不在WiFi路由器列表里 | SSID或密码错误、WiFi用了5G频段 | 检查代码中ssid/password;确认路由器有2.4G信号 | 使用2.4G WiFi;密码有特殊字符尝试转义 |
| DHT22读数偶尔变成0或-999 | 接线虚接、上拉电阻缺失 | 检查数据引脚电压是否在1.8V-3.3V | 补上4.7K上拉电阻;加长传感器与板子的线序 |
| BLE手机APP搜索不到设备 | 广播未启动、设备已连接 | 确认设备是否"已连接"状态 | 断开连接后等待30秒重新扫描;检查代码中startAdvertising |
| WiFi和BLE同时开时WiFi频繁断连 | 射频共存干扰 | 使用手机靠近板子时观察BLE连接状态 | 尝试减小BLE广播间隔为200ms;降低WiFi传输速率 |
| MQTT订阅不到消息 | topic不对、用户名密码错误、没设keepalive | 用MQTT客户端工具订阅测试 | 统一topic命名;检查Broker日志 |
| 手机远程控制时网页打开非常慢 | 大页面未压缩、HTTP GET请求太多 | 对比局域网打开速度 | 优化HTML大小;改用JSON接口传递数据 |
| 继电器频繁跳动 | 控制引脚悬空或电平不稳 | 用万用表测量引脚电平 | 加下拉电阻,上电初始化所有IO状态 |
8.2 实测调优经验与避坑技巧
我第一次跑完整方案的时候,发现了一个很有意思的问题:MQTT命令下发控制继电器没有任何响应,串口监视器也看不到数据。排查到最后,竟然是我的Broker的topic设置了权限,匿名用户只能发布到特定前缀,订阅另一个前缀的指令topic直接被拒了。如果自己搭的Mosquitto,配置项allow_anonymous可别只想着开方便之门,正确做法是设置用户名密码,并给ESP32设备分配专用账号,权限最小化,这样既安全又防止别人乱发指令。
另一个花了不少时间踩坑的环节是BLE扫描的主动式与被动式差异。BLE主动扫描可以请求传感器发送扫描响应包,很多设备把真正的温湿度数据放在扫描响应里,如果不开启主动扫描,解析出来的数据就是空壳。代码里必须调用setActiveScan(true)。
最后分享一下我调试这套系统的工具组合,效率极高:
- 串口监视器:用Arduino IDE自带或者用Putty直连串口,方便看日志
- MQTT客户端:电脑端用MQTTX,手机端用MQTTool,模拟并发数据非常方便
- BLE调试:手机用nRF Connect,尤其看GATT属性、订阅通知、直接写值
- WiFi扫描:手机用"WiFi分析仪"APP,用来查看家里2.4G信道拥堵程度,方便给ESP32配一个不拥堵的WiFi信道
- 协议分析:如果只用手头BLE传感器,可以用Wireshark加BLE嗅探器抓包,能精确定位广播包结构,但家庭环境大多数时候用nRF Connect就够了
这套东西调试完,你会发现其实"智能家居"没有一个神秘的门槛,不过是在芯片端把采集、控制和通信三条链路打通。ESP32给这套方案带来的价值在于它把两条不同属性的无线协议塞进一块开发板,成本和复杂度都压在芯片内部了,开发者只需要考虑业务逻辑。
我现在这版方案已经稳定运行了将近两个月,期间家里路由器断过三次,ESP32都能在WiFi恢复后30秒内自动重连回来,BLE的温湿度计也一直正常工作。中间遇到过电源不稳定导致ESP32自己重启,后来换了一个质量好一点的5V适配器就解决了。整体下来这套方案的可复制性很强,照着文章配置一遍基本都能跑通,后期想扩展加入更多继电器或者在线语音控制也都预留了升级空间。
如果让我重新选一次硬件方案,我还是会选ESP32。这颗芯片的文档和社区资源非常成熟,即使遇到冷门问题,搜索引擎上几乎都能找到对应的讨论。智能家居讲究的不是某一个设备的性能有多强,而是整个系统能不能按预期稳定运行。ESP32在这个"稳定"和"灵活"之间的平衡点,目前看还没有多少竞品能比得过。