尧图精选

ESP32双协议网关:WiFi与BLE融合的智能家居实战

🕒 发布时间:2026/10/2 20:27:15 📁 来源:尧图网络
1. 项目概述用ESP32把WiFi和BLE捏合到一套智能家居里家里设备多起来之后我最大的痛点不是“缺一个遥控器”而是为了控制不同东西装了五六个App灯的App、插座App、加湿器App、体脂秤App界面各不相同数据还不互通。后来我用ESP32自己搭了一套方案把WiFi设备和BLE设备统一接入自己控制逻辑自己写数据也全部留在本地。核心思路很简单ESP32做中枢WiFi跑远程管理和数据上报BLE负责跟低功耗小设备通信两套协议在同一个芯片里协同工作。这套方案我用了大半年稳当、可控、成本也低所以整理出来分享给同样想折腾智能家居的人。这块内容适合谁如果你对嵌入式开发有一定基础比如用过Arduino或者写过一点C语言跟着走完全没问题就算你是纯小白只要愿意照着一行行抄代码、接线也能把基础版本跑起来。做完之后你会拥有一个能远程看温湿度、控制继电器开关、用手机蓝牙离线操控的本地智能家居节点而且后续扩展能力很强加传感器、加电机、接入Home Assistant都留好了口子。2. 整体设计思路为什么WiFi和BLE必须两个都要2.1 WiFi管“远”BLE管“省”两者是互补关系很多新手会问ESP32既然都支持WiFi和BLE那直接用WiFi连所有设备不就行了吗这个问题我一开始也想过但实际做了一圈就明白不是所有设备都适合走WiFi。你看智能家居里最常见的传感器比如温湿度、门磁、人体红外、纽扣电池供电的蓝牙标签这些设备的工作特点是数据量小、不频繁、要求功耗极低。BLE的广播和GATT连接在低功耗模式下能做到极低的平均电流一颗CR2032纽扣电池撑几个月甚至一年很正常。而WiFi本身是为高数据吞吐设计的模块休眠和连接建立的开销很大电池小的设备根本扛不住。反过来WiFi的优势在于传输距离、带宽和上层生态。家里隔一堵墙、两堵墙WiFi信号通常比BLE稳得多而且可以轻松把手上的数据推到手机、云端或者局域网内的服务端支持HTTP、MQTT、WebSocket这些现成协议。所以我最终定下来的架构是一台ESP32当“网关”WiFi负责连接家里的路由器、跟服务器的通信BLE负责跟各种低功耗终端打交道。这样既保住了WiFi设备的远程可控性也保留了蓝牙设备的低成本长续航特性。2.2 整体架构和数据流向整套方案分成三层设备层温湿度传感器、继电器、红外传感器、蓝牙小模块等负责采集物理世界的数据或执行动作。网关层ESP32位于中间一边通过GPIO/ADC/I2C直接连接简单传感器一边通过BLE连接无线传感器再统一处理这些数据。应用层手机App、Web浏览器、MQTT服务器、Home Assistant等人跟系统交互的窗口。数据流有两类一类是“上行数据”传感器把温度、湿度、开关状态通过GPIO或BLE送到ESP32ESP32打包后通过WiFi/MQTT发到服务端另一类是“下行控制”用户在手机或网页上点一下请求经WiFi到达ESP32ESP32判断后驱动继电器、开关或通过BLE转发控制指令给蓝牙设备。这里有个关键设计心得ESP32不直接连互联网也能工作。我把整个系统做成了局域网优先所有控制指令先走本地MQTT或本地Web只有需要远程查看时才通过加密隧道或云服务中转。这样即使断网家里最基本的开关控制、传感器读取不受影响。很多人做智能家居一上来就搞“云平台”结果外网一抖设备就全失控这是最不值得踩的坑。2.3 选型对比ESP32对比树莓派、STM32优势在哪你可能也看到热搜里有“基于树莓派的智能家居”“STM32智能家居系统”这里比较一下方便选型。树莓派的优势是跑完整Linux能装Python、Docker、Home Assistant扩展性极强但代价是功耗高、体积大、开机慢更重要的是价格贵。STM32功耗低、实时性好、非常适合纯粹的采集控制可它要自己做网络协议栈想同时挂WiFi和BLE基本得上双芯片方案开发成本直接拉高。ESP32属于中间路线双核240MHz、WiFi和BLE双协议栈都在片内支持Arduino、ESP-IDF、MicroPython功耗比树莓派低很多性能又比传统单片机强不少。做智能家居节点它是最不容易出错的选项。这里补充一句ESP32也有很多型号经典款ESP32、ESP32-S3、ESP32-C3等。经典款性价比最高外设全S3带AI加速和更多IO适合摄像头和语音类应用C3是单核RISC-V成本更低。我主力用的是经典ESP32 DevKit兼顾价格和资料丰富度。3. 硬件选型与环境搭建别在硬件上省不该省的钱3.1 开发板、传感器和执行器的选择清单这块我列一下我实际用下来的组合照抄就能跑ESP32开发板ESP32 DevKit V1或者NodeMCU-32S带USB转串口芯片CP2102或CH340都行几块钱到三十块钱不等买的时候留意引脚标注清晰不清晰。USB数据线一定要选带数据传输能力的线很多手机充电线只有电源没有数据信号插上后电脑根本识别不到串口。这个坑我见得太多了。温湿度传感器DHT11或DHT22也叫AM2302。DHT11便宜但精度低适合演示DHT22精度高适合认真测量环境。传感器供电接3.3V或5V都行注意DHT22的数据线要接一个4.7kΩ上拉电阻到VCC。继电器模块市面上常见的有1路、2路、4路用光耦隔离的版本更好。驱动电压选5V或3.3V的注意ESP32的GPIO电平是3.3V最好买低电平触发的继电器模块IO口默认高电平更安全。红外人体传感器HC-SR501检测人体移动做自动化触发很好用。BLE终端设备可以用另一块ESP32、或者HM-10/CC2541这类BLE透传模块模拟低功耗节点。说下接线。DHT22我接在GPIO4继电器接在GPIO25和GPIO26人体红外接在GPIO13这个引脚同时连接了板载LED方便调试。VCC和GND统一接好传感器供电和逻辑供电共地。这个共地很重要否则数字信号可能乱跳。3.2 开发环境Arduino IDE还是PlatformIOESP32的开发环境主要有三种Arduino IDE、PlatformIO、ESP-IDF。我的建议是刚上手用Arduino IDE逻辑简单库多搜“ESP32装库”能找到大量资料但如果你后续项目变大强烈建议切到PlatformIO它能管理多环境、多平台编译产物干净用VSCode写代码的体验比Arduino IDE舒服太多。这篇博客里的代码我把依赖库都列出来两种环境都能用。Arduino IDE安装ESP32支持有个容易卡住的地方是下载官方板卡包太慢。解决办法是在“文件→首选项→附加开发板管理器网址”里填国内镜像源然后到开发板管理器里搜esp32安装。我自己用阿里云镜像地址就是那串常用的https://espressif.github.io/arduino-esp32/package_esp32_index.json https://download.espressif.com/package_esp32_index.json如果自动下载特别慢你还可以手动下载乐鑫官方固件包放到本地的Arduino15目录。很多教程提到这个记住一句话不要反复重试官方源换镜像是最有效的方法。依赖库方面我主要用WiFi.h内置PubSubClient.hMQTT客户端WebServer.h内置ArduinoJson.h处理JSON数据DHT sensor library Adafruit Unified Sensor驱动DHT系列BLEDevice.h内置BLE GATT相关4. 核心实现WiFi通信与数据上报4.1 WiFi连接不仅要能连上还要能扛断线WiFi连接本身不复杂Arduino里调用WiFi.begin(ssid, password)就行。但在智能家居里路由器重启、信号波动太常见了默认的连接方式一旦掉线就可能卡死。所以我自己封装了一层WiFiManager逻辑上电先尝试连接上一次保存的WiFi配置连不上就进入配网模式用内置WebServer开一个热点或回调页面让用户填新的WiFi账号密码运行期间循环检测WiFi状态如果断开就重新连接连接次数有上限超过就重启重启。代码大致是这样#include WiFi.h const char* ssid your_ssid; const char* password your_password; unsigned long lastReconnectAttempt 0; void setupWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print(连接WiFi); while (WiFi.status() ! WL_CONNECTED millis() 15000) { delay(500); Serial.print(.); } if (WiFi.status() WL_CONNECTED) { Serial.println(\n已连接IP: WiFi.localIP().toString()); } else { Serial.println(\n连接失败进入配网模式); startConfigPortal(); } } void keepWiFiAlive() { if (WiFi.status() ! WL_CONNECTED millis() - lastReconnectAttempt 30000) { lastReconnectAttempt millis(); WiFi.disconnect(); WiFi.begin(ssid, password); Serial.println(正在重连WiFi...); } }这里有一个细节重连前一定要WiFi.disconnect()。因为ESP32的WiFi栈偶尔会进入一种“假连接”状态——状态码显示已连接实际上已经收不到数据了。断开重连相当于给它一次重新初始化协议栈的机会。另外如果你的板子离路由器特别远试试2.4GHz频段的固定信道不要用自动信道扫描自动扫描碰上网段拥挤时容易超时。4.2 用MQTT做消息通道而不是裸Socket我最早做这套系统时用TCP Socket自己定协议后来发现每加一个功能就要改协议、改客户端非常繁琐。换成MQTT清爽很多它本质上是一个基于发布/订阅的消息中间件适合传感器这种低频小数据。我本地跑了一个Mosquitto MQTT BrokerESP32作为客户端发布主题手机和PC订阅主题所有设备共用一套消息格式模块之间完全解耦。ESP32端用PubSubClient库关键代码#include PubSubClient.h WiFiClient espClient; PubSubClient mqttClient(espClient); const char* mqttServer 192.168.1.10; // 本地MQTT服务器地址 const int mqttPort 1883; void connectMQTT() { mqttClient.setServer(mqttServer, mqttPort); mqttClient.setCallback(mqttCallback); while (!mqttClient.connected()) { Serial.print(连接MQTT...); if (mqttClient.connect(ESP32_Gateway, user, pass)) { Serial.println(成功); mqttClient.subscribe(home/control/#); } else { Serial.print(失败错误码: ); Serial.println(mqttClient.state()); delay(2000); } } } void mqttCallback(char* topic, byte* payload, unsigned int length) { String msg String((char*)payload).substring(0, length); Serial.printf(收到主题 %s 消息 %s\n, topic, msg.c_str()); if (String(topic) home/control/relay1) { if (msg ON) { digitalWrite(RELAY1_PIN, LOW); mqttClient.publish(home/status/relay1, ON); } else if (msg OFF) { digitalWrite(RELAY1_PIN, HIGH); mqttClient.publish(home/status/relay1, OFF); } } }这里特别提醒一下PubSubClient的坑如果你在loop()里调用了很多delay()MQTT的心跳就会被卡住服务器以为客户端掉线然后断开连接。正确的做法是使用类似millis()的非阻塞定时器来处理采集和上报让loop()尽量快地循环。我见过不少人折腾半天MQTT时好时坏最后发现原因就是delay(1000)塞在消息推送函数里面了。4.3 内嵌Web页面不用装App也能控制手机装App确实方便但对于临时访客、家人改动设置来说一个内嵌网页是最省心的。ESP32的WebServer库可以托管静态HTML也可以处理表单和GET/POST请求。我做了两个页面一个是仪表盘实时显示温湿度和继电器状态另一个是配置页用来调整上报间隔、设置阈值。核心逻辑#include WebServer.h WebServer server(80); void handleRoot() { String html htmlheadmeta charsetUTF-8titleESP32 智能家居/title/headbody h2ESP32 状态/h2 p温度: String(tempC, 1) ℃/p p湿度: String(humidity, 1) %/p p继电器1: String(relay1State ? ON : OFF) /p pa href/relay1/on打开继电器1/a/p pa href/relay1/off关闭继电器1/a/p /body/html; server.send(200, text/html, html); } void setupWebServer() { server.on(/, handleRoot); server.on(/relay1/on, []() { digitalWrite(RELAY1_PIN, LOW); relay1State true; server.send(200, text/plain, OK); }); server.on(/relay1/off, []() { digitalWrite(RELAY1_PIN, HIGH); relay1State false; server.send(200, text/plain, OK); }); server.begin(); }对于局域网访问这样的页面已经够用。如果你希望外网也能访问千万不要直接把80端口映射出去——ESP32的Web服务很脆弱裸奔公网容易被扫描攻击。我自己的做法是ESP32只往本地MQTT上报外网用户通过Nginx反向代理Home Assistant之类的前端服务由前端再走内网MQTT控制ESP32这样ESP32永远不直接暴露到公网。5. 核心实现BLE蓝牙通道接入与离线控制5.1 用BLE GATT定义设备服务ESP32的BLE功能基于GATT协议简单理解就是定义一组“服务”和“特征值”手机上的蓝牙App通过读写这些特征值来获取状态或下发控制。我定义了一个自定义服务UUID自己生成包含一个温湿度特征值一个继电器控制特征值。关键代码#include BLEDevice.h #include BLEServer.h #include BLEUtils.h #include BLE2902.h #define SERVICE_UUID 6e400001-b5a3-f393-e0a9-e50e24dc9a91 #define CHAR_TEMP_UUID 6e400002-b5a3-f393-e0a9-e50e24dc9a91 #define CHAR_RELAY_UUID 6e400003-b5a3-f393-e0a9-e50e24dc9a91 BLECharacteristic *tempChar; BLECharacteristic *relayChar; class RelayCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic *pCharacteristic) override { std::string value pCharacteristic-getValue(); if (value.length() 2) { if (value[0] O value[1] N) { digitalWrite(RELAY1_PIN, LOW); } else if (value[0] O value[1] F value[2] F) { digitalWrite(RELAY1_PIN, HIGH); } } } }; void setupBLE() { BLEDevice::init(ESP32 SmartHome); BLEServer *server BLEDevice::createServer(); BLEService *service server-createService(SERVICE_UUID); tempChar service-createCharacteristic( CHAR_TEMP_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY ); relayChar service-createCharacteristic( CHAR_RELAY_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_WRITE ); relayChar-setCallbacks(new RelayCallbacks()); service-start(); BLEAdvertising *advertising server-getAdvertising(); advertising-addServiceUUID(SERVICE_UUID); advertising-setScanResponse(true); advertising-start(); }注意我给特征值开启了NOTIFY属性这样温湿度变化时ESP32可以主动推送数据到手机省得手机反复主动去读既省电又实时。5.2 用手机App控制推荐nRF Connect调试再用小程序收尾调试BLE时我强烈推荐先用nRF Connect这个工具它能扫描到设备、查看服务列表、读写特征值是最直观的调试利器。连接上ESP32之后你能直接看到“即时温度”“湿度”“继电器控制”这些特征值手动写入字符串就能测试完全不依赖自己写的代码。真正给家人用时蓝牙App我用微信小程序实现因为免安装、跨平台。小程序里调用wx.openBluetoothAdapter、wx.getBLEDeviceServices这些API去连ESP32逻辑跟原生App一样。小程序的好处是二维码一发就能用而且iOS和Android体验基本一致坏处是蓝牙API的兼容性细节多比如某些Android机型需要在“定位服务”打开后才能扫描到设备。这个坑让我排查了一下午后来疯狂吐槽不过也吸取了一个教训BLE调试永远把手机系统权限当成第一排查项。5.3 单芯片双协议栈协同注意共存问题ESP32同时启用WiFi和BLE是支持的但两者共用同一个2.4GHz射频前端极端情况下会互相抢资源。实际表现是“同时大量收发WiFi数据时BLE连接延迟变高”。对智能家居这种低频应用来说感知不明显。不过要注意几个发力点如果应用场景是WiFi跑OTA固件升级同时BLE在传输关键控制信息最好把BLE暂时断开或把WiFi的吞吐限制一下。在ESP32的Arduino环境下默认配置已经在BT控制器里做了共存仲裁不用自己改。如果遇到明显卡顿优先检查电源WiFi发射瞬间电流可达300mA以上如果USB线太长或供电能力不足电压会被拖低此时各种诡异问题都会出来。6. 实战场景温湿度监测、继电器控制与自动化联动6.1 采集温湿度并定时上报DHT22的读取逻辑非常简单但很多人忽略一个问题DHT系列传感器从初始化到第一次读取要间隔至少1秒否则数据会是零。我自己写的采集函数多做了几重保护连续读到NaN时保留上一次有效值同时记录错误计数方便判断传感器是不是真的挂了。#include DHT.h #define DHT_PIN 4 #define DHT_TYPE DHT22 DHT dht(DHT_PIN, DHT_TYPE); void readSensors() { float h dht.readHumidity(); float t dht.readTemperature(); if (isnan(h) || isnan(t)) { Serial.println(读取温湿度失败保留旧值); return; } tempC t; humidity h; Serial.printf(温度: %.1f ℃, 湿度: %.1f %%\n, tempC, humidity); }上报策略我建议做“变化上报”而不是“固定间隔上报”。比如温度变化超过0.5℃或湿度变化超过1%才上报数据频率低了MQTT服务器和电池供电节点压力都小很多。这个思路对BLE节点尤其重要频繁上报是最主要的耗电元凶。6.2 继电器控制低电平触发和高电平触发的坑继电器模块看起来简单实际很容易栽。很多低价继电器模块是“低电平触发”也就是GPIO输出LOW时继电器吸合GPIO输出HIGH时断开。原因在于这类模块使用了PNP三极管或者光耦反相设计设计初衷是兼容MCU上电瞬间IO默认高电平避免误动作。我在代码里因此写成了数字逻辑取反控制继电器开就要digitalWrite(LOW)关反而是HIGH。如果你的模块是高电平触发代码反过来。另外继电器控制感性负载比如电机、变压器时触点通断会产生火花会干扰ESP32的模拟信号。能做的措施包括负载两端并联阻容吸收电路继电器模块用光耦隔离版传感器线和继电器线分开走线不要绞在一起。6.3 外部中断实现感应联动红外人体传感器HC-SR501输出的是简单的数字信号有人的时候输出高电平。但如果你在loop()里用digitalRead去轮询会浪费大量CPU还可能漏掉脉冲。正确的做法是用外部中断#define PIR_PIN 13 volatile bool motionDetected false; void IRAM_ATTR pirInterrupt() { motionDetected true; } void setup() { pinMode(PIR_PIN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(PIR_PIN), pirInterrupt, RISING); } void loop() { if (motionDetected) { motionDetected false; Serial.println(检测到人体运动开灯); digitalWrite(RELAY1_PIN, LOW); // 开灯 } }这里有个重要的知识点中断处理函数里不能调用delay()、Serial.println()这类耗时函数因为它们不是在主循环上下文中执行的。正确做法是中断里只置标志位主循环快速判断并处理动作。这个习惯能避免大量莫名其妙的死机和复位问题。这套系统最爽的使用场景人一走进房间传感器通过中断唤醒ESP32ESP32开灯并把MQTT消息发给其他房间节点温度高于28℃自动打开风扇的继电器深夜2点以后再检测到人体运动只推送告警不上报灯控。所有自动化逻辑都写在ESP32端断网也照跑不误。7. 常见问题与排查我踩过的坑和速查表7.1 烧录失败不是代码问题往往是串口和boot模式问题ESP32最常遇到的报错是“A fatal error occurred: Failed to connect to ESP32: Timed out waiting for packet header”。新手第一反应是代码写错了其实大概率是板子没有进入下载模式。ESP32的下载流程需要把EN引脚短暂拉低再释放同时保持GPIO0为低电平。很多开发板设计成“按住BOOT键再按一下EN键松开BOOT键”就能进入下载模式。Arduino上传时如果看到Connecting...提示操作这个按键组合就行了。还有一种情况是USB转串口芯片驱动没装好尤其CH340在Windows下需要手动装驱动Mac和Linux一般免驱。在设备管理器里看看COM口是否出现如果没有换根线或者手动装驱动。7.2 WiFi连接不稳定排查电源再看路由器按我以往经验ESP32 WiFi不稳定大约有七成是供电问题USB线过长、电脑USB口供电不足、或者用充电宝供电时电流限制太大。ESP32在TCP传输瞬间电流会冲到300mA以上如果你从某个树莓派GPIO引脚或PC前置USB口取电电压很容易被拉低到2.7V左右随之而来的是WiFi掉线、重启循环、Flash写入失败。最稳的供电方案是独立5V/2A电源适配器给开发板供电或用优质的USB线接到独立充电头。排除供电后再考虑WiFi信道冲突。ESP32只能连接2.4GHz网络如果搜索不到你的WiFi可能是路由器只开了5GHz频段。把路由器打开双频合一之前最好先给ESP32连接一个单独命名的2.4GHz SSID免得手机连了5GHz后找不到设备。7.3 BLE扫描不到设备权限和广播参数优先检查手机扫描不到ESP32九成不是代码问题。iOS和Android现在都对蓝牙扫描增加了严格权限控制Android要求同时打开“定位服务”并且赋予App定位权限因为蓝牙扫描被归类为基于位置的服务。这个点特别反直觉。另外Android 12以上还需要在“附近设备”权限中单独授权。排除权限问题后再检查ESP32的广播参数。BLE广播名称、广播间隔都要设置正确广播数据里建议把Service UUID加进去这样手机端过滤服务UUID就能快速找到设备。7.4 常见问题速查表现象可能原因快速排查上传固件卡在Connecting未进入下载模式BOOTEN组合键重新触发下载串口能识别但写着乱码波特率不匹配设置115200或从头看日志WiFi连接超时供电不足 / 5GHz频段换独立电源、找2.4GHz SSIDMQTT时不时掉线delay()阻塞消息循环改用非阻塞定时器BLE扫描不到权限未开 / 广播没启动检查系统权限和广播代码DHT读数值为NaN上电后读取过早 / 接线问题延时2秒再读检查上拉电阻8. 一些实际使用中的体会和建议这套项目做完之后我对“智能家居”这四个字的理解变得更务实了。真正让人舒服的不是设备能连上网而是它能按人的习惯自动运转并且出问题时知道去哪查、怎么修。我建议第一次做的人先不要追求功能全把任何一个单一功能比如“用手机控制一个继电器”跑通拥有自己调试全链路的能力再逐步往上加温度传感器、湿度、人体感应、BLE子网关心态会稳很多。另外这套方案后续扩展的想象空间也很大。我正打算在ESP32上再接一块OLED小屏显示当前状态顺便把数据同步到Home Assistant如果后续接小车或机器人类项目也可以用ESP32做串口桥接跟ROS 2 Humble通信不过那就是另一个项目了。至少在当前这套智能家居系统里ESP32只花了不到五十块钱却同时把WiFi和BLE两条路走通了这种把钱花在刀刃上的感觉真的很爽。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →