我把这两年用 ESP32 折腾智能家居的底子翻了出来,整理成这篇东西。ESP32 这块芯片最吸引我的地方,就是它把 WiFi 和 BLE 同时塞进了一颗芯片里,做智能家居网关或者终端节点都特别顺手。这篇文章会从方案选型讲起,一直聊到固件开发、设备接入、常见坑位排查,把我实际踩过的坑和验证过的做法都放进来。不管是准备入坑的爱好者,还是想把家里设备统一管理的折腾党,应该都能从里面找到能直接上手的东西。
1. 整体方案设计与思路拆解
1.1 为什么是 WiFi + BLE 这个双协议组合
先说结论:智能家居里没有一种通信协议能通吃所有场景,多协议融合才是常态。
WiFi 的优势非常明显:带宽大、传输距离远、可以直接接入家里现有的路由器,不需要额外买网关硬件。但它的代价是功耗高,一个 ESP32 开 WiFi 发射,峰值电流能到 240mA 甚至更高,如果用电池供电,几天就没了。另外一个容易被忽略的问题:WiFi 设备多了以后,路由器负载会明显上升,2.4GHz 频段互相干扰的情况在城区住宅里特别普遍。
BLE 的特性正好和 WiFi 互补。它的功耗只有 WiFi 的几十分之一,广播模式下的电流可以控制在 10mA 以内,非常适合做一些低功耗传感器节点。缺点也明显:传输速率低、覆盖范围小(空旷环境大概十几米,穿墙基本没戏),而且它本身不能直接连互联网,需要一个“桥”把数据送到外网。
ESP32 的价值就在这里——一颗芯片同时提供这两种能力,而且价格便宜、生态成熟。我在实际方案里通常是这么分工的:
| 角色 | 使用的协议 | 典型设备 | 原因 |
|---|---|---|---|
| 网关/主控 | WiFi + BLE 同时开启 | 客厅中枢、卧室控制面板 | 既要连外网,又要管理本地 BLE 节点 |
| 电池传感器 | 仅 BLE | 门窗磁吸、温湿度计、人体感应 | 功耗低,续航可以拉长到半年以上 |
| 执行终端 | 仅 WiFi | 灯、插座、窗帘电机 | 需要稳定可靠,供电充足不在乎功耗 |
这个组合的核心思路就是:让每个设备用最合适的协议做自己最擅长的事。
1.2 拓扑结构与设备角色分配
我家里目前的拓扑是这样的:
互联网 | WiFi路由器 |-- ESP32网关(WiFi连路由器 + BLE扫描) | |-- BLE温湿度传感器(客厅) | |-- BLE门窗传感器(入户门) | |-- BLE人体传感器(卧室) |-- WiFi智能灯(2盏) |-- WiFi智能插座(2个)核心节点是那个 ESP32 网关。它做的事情有三件:
第一,通过 WiFi 连接路由器,走 MQTT 协议与 Home Assistant 通信,接收云端下发的控制指令。第二,通过 BLE 扫描和连接周边的低功耗传感器,读取数据后统一转换成 MQTT 消息上报。第三,作为一个本地逻辑执行器,即使外网断了,也能根据预设规则联动开关灯和插座。
这里有个设计细节值得多说一句:不要把 ESP32 网关做成单点故障源。我一开始把所有联动逻辑都写在网关里,路由器重启的时候整个家都“瘫痪”了。后来改了一版,把关键联动(比如天黑开灯、人体触发)在网关本地跑,非关键逻辑(比如远程控制、定时任务)丢给 Home Assistant。这样断了外网,本地基本功能还能维持。
2. 硬件选型与准备
2.1 主控与模块选择
市面上的 ESP32 变体很多,我最常用的几款:
| 模块 | 核心特点 | 适合场景 |
|---|---|---|
| ESP32 DevKitC | 经典开发板,引出全部 GPIO | 原型验证、网关开发 |
| ESP32-WROOM-32E | 集成天线方案,稳定性好 | 正式设备的主控 |
| ESP32-C3 | 单核 RISC-V,体积小 | 简单传感器节点 |
| ESP32-S3 | 双核 + USB + 更多 GPIO | 需要外设扩展的终端 |
网关我选的是 ESP32-WROOM-32E,理由是带屏蔽罩、天线性能稳定、双核更适合同时跑 WiFi 和 BLE 协议栈。传感器节点用的是 ESP32-C3,单核够用,BLE 功能完整,功耗控制也不错。
选型的时候有几个坑要避开:
- 有些低价模块用的是翻新 flash,容量不足会导致固件反复擦写失败。
- 模块上的 PCB 天线方向会影响信号,设计外壳时要给天线留出空间,别用金属壳直接罩住。
- 电源部分别省,ESP32 启动瞬间电流很大,如果用了劣质 LDO 或者太细的飞线,会出现“上电即重启”的诡异问题。
2.2 传感器、执行器与控制板
传感器这块,我实测过几款常见的:
- DHT22 温湿度传感器:数据读取需要严格时序,在 BLE 节点上用没问题,但如果系统负载高、中断延迟大,读取容易失败。我后来换成了SHT30,走 I2C 总线,抗干扰能力和精度都更好。
- 门窗磁传感器:本质上就是一个干簧管,接一个 GPIO 配内部上拉,检测电平变化即可。注意要开内部下拉/上拉,避免浮空误触发。
- 人体红外传感器:推荐 HC-SR505 这种低功耗的,但对环境温度敏感,夏天误报率会上升。如果预算允许,直接上毫米波雷达模块(如 LD2410),体验完全不一样。
执行器方面,最简单可靠的方案是继电器模块 + ESP32,但控制感性负载(比如电机)时一定要加续流二极管,否则继电器断开瞬间的反向电动势很容易把 ESP32 的 GPIO 打坏。我一开始没在意这个问题,连续烧了两个 GPIO,后来学乖了。
如果做灯控,推荐用PWM + MOS 管方案,可以顺便实现无极调光调色。
3. 核心开发实操:从环境搭建到双协议协同
3.1 开发环境与工程初始化
我选择的框架是 ESP-IDF 5.x,官方维护、双协议栈支持最好。Arduino 框架虽然上手快,但 BLE API 封装得不够细致,出问题时不方便排查底层。
环境配置其实很简单,官方有完整的命令行流程。我给两个实用建议:
- 国内下载 ESP-IDF 仓库和工具链很慢,一定要设置国内源。具体做法是在
idf.py构建时,通过IDF_GITHUB_ASSETS环境变量指向乐鑫的国内镜像,把下载速度从几 KB 拉回几 MB。 - 如果只是开发测试,用IDF Docker 镜像能省掉一整套本机配置。我之前在 Windows 上折腾了半天工具链,换成 Docker 后五分钟左右就能开始写代码。
工程初始化命令:
idf.py create-project gateway cd gateway idf.py set-target esp32 idf.py menuconfigmenuconfig阶段要开启几个关键组件:WiFi、BLE(在Component config → Bluetooth → Bluedroid → Classic Bluetooth里关掉经典蓝牙,只保留 Low Energy)、MQTT 客户端。
3.2 BLE:建立低功耗传感器网络
BLE 开发的核心是 GATT 协议,理解了这个,读写数据就是顺手的事。
在我的方案里,传感器节点是被动广播型(server),网关是主动连接方(client)。具体流程:
传感器端启动广播,广播数据包里直接携带温湿度值和电池电量。这样做的好处是网关不需要与每个节点建立完整连接,只要持续扫描广播包就能拿到数据,功耗极低。
但广播数据包的长度有限,自定义数据最多只有 31 字节。如果需要交互式控制(比如让设备开启某种模式),就得走连接流程。我在设计里把传感器设置成“广播数据 + 可连接”两种模式共存:平时只广播,需要配置时用 APP 或者网关建立连接去改写特征值。
广播包的配置示例:
static const uint8_t adv_data[] = { 0x02, 0x01, 0x06, // Flags: LE General Discoverable 0x03, 0x03, 0x12, 0x18, // Complete List of 16-bit UUIDs: 0x1812 0x07, 0xFF, 0x50, 0x69, 0x01, 0x2C, 0x1A, 0x3C // Manufacturer Specific Data };第三段数据我用自定义的格式,前两个字节是自定义厂商 ID,后面是温度(0x01 代表 27.5°C)和湿度(0x2C 代表 45%)。这部分参考了蓝牙 SIG 定义的 Environmental Sensing Service(0x1812)的规范,但为了减少通信开销还是用了自定义负载。
3.3 WiFi:连接外网与 MQTT 上报
WiFi 连接这个环节其实没什么可说的,但有几个细节我花了很长时间才明白:
第一,WiFi 重连策略必须写。默认情况下,ESP32 掉线后会按内部算法重连,表现往往不理想。我隔一段时间就会遇到“明明路由器开着,设备却长时间离线”的情况。后来我在代码里加了一个逻辑:如果连接成功后在 30 秒内掉线,就主动调用esp_wifi_disconnect再重连,而不是等协议栈自己处理。
第二,稳定连接之后,TCP 保活和 MQTT 心跳要有节奏。MQTT 的keepalive我设定为 60 秒,同时让 ESP32 的 TCP 栈启用 keepalive,双保险。有些路由器默认断空闲连接很快,不做保活的话 MQTT 连接会一两个小时就断一次。
第三,MQTT 的 topic 设计要有层次,别什么都塞到一个 topic 下。我目前的风格是:
home/esp32-gateway/status # 在线状态 home/esp32-gateway/sensor/temp # 温度上报 home/esp32-gateway/sensor/hum # 湿度上报 home/esp32-gateway/bedroom/light # 卧室灯控制这样的好处一方面是后面接 Home Assistant 很方便(它的 MQTT 集成能自动按 topic 发现设备),另一方面是排查问题时能快速定位。
3.4 双协议协同:并发与资源分配
同时跑 WiFi 和 BLE,最核心的挑战是共存机制。ESP32 的 WiFi 和 BLE 共用同一个射频前端,协议栈虽然内置了时分复用,但如果使用不当,两者会互相抢占,导致信号质量下降。
几个我验证过的优化手段:
- 在
menuconfig里开启WiFi/BLE 共存仲裁器,并调整 BLE 的优先级为高。 - 降低 BLE 的广播间隔。广播间隔越低,射频占用越高,对 WiFi 吞吐影响越大。我把广播间隔设定在 200~400ms,对 WiFi 的实际影响可以忽略。
- 避免在 BLE 连接过程中做大量 TCP 长数据收发。如果非要这么做,建议把 BLE 连接改为短连接模式,数据传完就断开。
我做过一个压测:网关一边开着 BLE 扫描 5 个传感器,一边通过 MQTT 每 5 秒上报一次数据,持续 72 小时没有出现明显的连接丢失或数据乱序。说明只要配置得当,双协议共存是可靠的。
3.5 网关固件的整体流程
这是我的网关固件的主流程,实际经验总结起来就是一个事件驱动的状态机:
- 上电 → 读取配置(WiFi SSID/密码、MQTT broker 地址)
- 连接 WiFi → 连接 MQTT
- 启动 BLE 扫描器 → 解析传感器广播包
- 主循环处理三类事件:BLE 数据到达、MQTT 指令到达、定时器触发(心跳/查询)
- 收到 MQTT 指令 → 控制 GPIO → 执行本地联动
本地联动我用了一个简单的规则表,格式如下:
typedef struct { char trigger_sensor[16]; // 触发源 float threshold; // 阈值 char action_topic[32]; // 动作目标 topic int action_value; // 动作值 } rule_t;比如{"temperature", 28.0, "living/light/switch", 1}的含义是:当客厅温度超过 28°C 时,打开客厅灯(当然这只是测试用的规则,实际规则更合理一点,比如“当 BLE 人体传感器检测到人,且当前光照低于阈值,则打开灯”)。
4. 与智能家居平台集成
4.1 对接 Home Assistant
Home Assistant 是很多智能家居玩家的首选中心平台,放在树莓派或者其他小主机上就能跑。ESP32 网关与 HA 对接主要有两种方式:
方式一是用MQTT 自动发现。HA 的后台会自动订阅homeassistant/status和homeassistant/device/+/config这类 topic,设备端通过发布特定的 JSON 配置就能被自动识别并加入实体列表。这种方式的优点是无需手工写配置,缺点是调试复杂,JSON 格式错一个字母就识别不了。
方式二是用手工配置文件。在 HA 的configuration.yaml里写:
mqtt: sensor: - name: "客厅温度" state_topic: "home/esp32-gateway/sensor/temp" unit_of_measurement: "°C" switch: - name: "卧室灯" command_topic: "home/esp32-gateway/bedroom/light/command" state_topic: "home/esp32-gateway/bedroom/light/state"两种方式可以并行用,我目前是传感器用自动发现,开关用静态配置,这样可视化的细节更可控。
4.2 手机端控制与 BLE 调试
手机端如果不想装 Home Assistant 的 App,可以直接用现成的 MQTT 客户端或 BLE 调试工具。
BLE 调试强烈推荐nRF Connect这个 App,它可以扫描周边所有 BLE 设备、查看广播包内容、建立连接后逐个查看 GATT 服务和特征值,还能直接读写数据。我第一次调试传感器广播格式时,就是靠它确认广播数据没被截断或填充。
另外提醒一点:如果你用手机 App 和 ESP32 建立 BLE 连接,手机会自动占用连接参数,这可能导致网关扫描时出现短暂信号弱化。我测试时发现,手机连上 ESP32 后,其他节点的广播包经常丢失 10~20 秒。这是单天线射频的物理限制,不要尝试去“修复”,而是要在应用层容忍丢包。
5. 常见问题与排查时装
5.1 BLE 连接不稳定、扫描不到设备
遇到这个问题,先按顺序检查四件事:
- 确认两个设备的 BLE 地址类型一致。ESP32 默认用 public 地址,但有些手机/模块用 random 地址。如果类型不一致,可能扫描不到或者连接被拒。
- 检查广播数据里是否把
Flags字段设置成了非可发现模式。这个字段设错,设备扫描不到是正常现象。 - 检查广播间隔是否有抖动。过高的广播间隔(比如 1 秒以上)会让扫描方错过窗口,尤其是在节点多的时候。
- 确认信道避让机制没被误开启。如果开启了蓝牙信道避让,而 WiFi 又占满了 2.4GHz 的相邻信道,BLE 的可用信道会被压缩,表现为“时好时坏”。
我遇到过最典型的问题是:两台 ESP32 距离不到一米,但互相扫描不到。查到最后是因为我在两台上电时都默认开启了 WiFi,而它们用的是同一个路由器的 2.4GHz 信道。干扰极其严重。解决办法是把 WiFi 换成 5GHz 频段(但 ESP32 不支持 5GHz),或者把两个设备的 BLE 广播间隔错开。
5.2 MQTT 频繁断连与数据丢失
MQTT 断连通常有三个原因:
- 路由器 DHCP 租约问题。ESP32 长时间在线,IP 租约到期后如果没能成功续约,会直接断网。解决方法是给网关设备在路由器里绑定一个固定的 IP 地址。
- broker 的 keepalive 设置过短。如果 broker 是公共服务器(比如
test.mosquitto.org),它们通常会在 60~90 秒内断开不活跃的连接。自建 HA 内网 broker 的话,建议把 keepalive 设成 120 秒以内。 - 固件里的 MQTT 回调处理太慢。如果回调函数里做了耗时的阻塞操作(比如写 flash、去连另一个 WiFi),TCP 栈的数据包就会积压,最终被 broker 判定为卡死而踢下线。所有耗时操作一定要放在独立任务里。
数据丢失方面,最有效的手段是QoS 1。QoS 0 是“发完不管”,网络一抖就丢。QoS 1 至少保证 broker 收到一次,虽然有可能重复,但只要业务是幂等的(比如设置开关状态),重复也无所谓。QoS 2 我不建议用在 MQTT 上报场景,性能开销大,收益不明显。
5.3 设备离线、无法远程控制的综合排查流程
我在实际维护中总结了一套排查顺序:
- 先看网关串口日志,确认有没有报错。这一步能区分问题出在固件还是网络。
- 再 ping 网关的 IP。能通说明 WiFi 层没问题。
- 用 MQTT 客户端手动发一条消息,看网关有没有响应。有响应说明链路正常,问题在 Home Assistant 配置。
- 看 Home Assistant 的 MQTT 日志。如果连接正常但实体状态不变,大概率是 topic 不匹配(比如多打了一个
/)。 - 最后才怀疑传感器硬件。依次断开传感器、重启网关、再连接传感器,做最小化测试。
这套流程看起来简单,但能避免至少一半的无效调试。
6. 功耗优化与续航经验
6.1 传感器节点如何做到续航几个月
如果你的 BLE 传感器节点用电池供电,功耗优化会直接影响可用性。我最夸张的一个测试是双节 18650 电池供电,一个 SHT30 温湿度节点跑了 8 个月。
核心只有一件事:让 ESP32 尽可能长时间处于深睡状态。
工作模式设计如下:
- 上电 → 初始化外设 → 读取传感器数据
- 把数据写入广播包 → 广播 3 次(每次间隔 200ms)
- 关闭 WiFi(如果开启了的话) → 进入 deep sleep 6 分钟
- 醒来重复整个过程
平均电流可以算一下:广播期间 20mA,持续约 1 秒;深睡期间 10μA;6 分钟周期内平均电流大概是(20 * 1 / 3600) + 0.01 ≈ 5.6μA。如果电池容量 3000mAh,理论上可以撑 6 万多小时,也就是几年。当然这是理想值,实际电池自放电、稳压 LDO 漏电会吃掉一部分,但做七八个月到一年问题不大。
深入睡眠代码示例:
esp_sleep_enable_timer_wakeup(3600 * 1000000); // 6 minutes in microseconds esp_deep_sleep_start();注意:deep sleep 之后程序从头开始执行,所以要在启动早期判断是从深睡唤醒(esp_sleep_get_wakeup_cause())还是冷启动,避免每次唤醒都反复初始化 flash。
6.2 Deep Sleep 唤醒与广播时序的坑
有一个特别容易踩的坑:深睡唤醒后广播包不是立刻生效的。因为 ESP32 的 BLE 协议栈需要一两秒才能完全就绪。如果你在唤醒后立刻广播,会发现消息根本发不出去,或者被协议栈排队到很晚。我的做法是在广播前加一个 100~200ms 的vTaskDelay,让蓝牙栈先稳定。
另一个坑是 GPIO 唤醒。如果用EXT0中断唤醒,要保证触发引脚电平在整个深睡期间保持稳定,否则会出现按住不放手导致设备反复唤醒、电池快速耗尽的现象。这时可以加一个 RC 滤波电容,把引脚上的毛刺滤掉。
7. 一些实际项目的扩展方向
如果你已经完成了网关 + 传感器的基础方案,下面几个方向是可以直接往下做的:
本地语音控制。ESP32 本身性能有限,跑不了复杂的语音识别模型,但可以通过离线唤醒词(比如 ESP-SR 组件)配合 WiFi 连接远端语音服务,实现“本地唤醒 + 云端解析”的模式。
多网关分布式部署。一个 WiFi 路由器覆盖不了大户型,可以部署两三个 ESP32 网关,每个网关管一片区域的 BLE 节点,再通过 MQTT 上报到同一个中心。中心再按区域聚合数据,这样就避免了单网关的信号盲区。
OTA 固件升级机制。如果设备分布在各个角落,每次改固件都要拆下来连 USB 线太痛苦。ESP32 官方支持esp_https_ota,可以在固件里内置一个检查更新的循环,从自己的服务器拉取新固件。
8. 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| BLE 扫描不到设备 | 广播数据 Flags 错误 | 检查广播数据中 Flags 字段,设为可发现模式 |
| BLE 连接后数据读不出来 | 特征值属性(read/write/notify)配置缺失 | 在 GATT 表里正确设置特征值权限 |
| 频繁断线重连 | DHCP 租约到期 | 路由器里绑定固定 IP |
| MQTT 数据延迟大 | QoS 2 重传机制导致 | 改用 QoS 1 |
| 上电后反复重启 | 电源带载能力不足 | 换用大电流电源,加 470μF 电容 |
| 温度传感器读数异常 | I2C 地址冲突或总线被占用 | 检查是否有其他设备占用同一 I2C 总线 |
| Deep sleep 唤醒后发不出广播 | 蓝牙栈未就绪 | 唤醒后加 100ms 以上延时 |
| WiFi 连上了但 MQTT 连不上 | broker 地址或端口配置错误 | 用mosquitto_sub命令行工具逐层排查 |
| 网关本地联动失效 | 规则表里 topic 写错 | 在日志里打印触发事件对应的 topic 是否匹配 |
这两年从这个项目里学到最重要的一条经验是:智能家居的稳定性不是靠某一个高性能设备撑起来的,而是靠所有设备各司其职、互相兜底。ESP32 的 WiFi + BLE 双协议能力让我可以用最少的硬件覆盖最多的场景,但真正让整个系统可靠运行的,是那些看起来不起眼的细节——广播间隔、重连策略、心跳周期、最小化测试流程。希望这篇分享能让你少走几段弯路,在实际部署的时候省下几个晚上的调试时间。