
聊点接地气的。前几天我从二手平台顺了一块ESP32开发板成色一般但插上电脑能识别芯片LED能闪这就算捡到宝了。板子到手第一件事就是让它跑MQTT。如果你手里也有一块吃灰的ESP32或者正要搞智能家居却不想在硬件上花大钱这篇文章就是写给你参考的。我会从硬件选型、协议原理、完整代码到实际踩坑把一个“ESP32 MQTT”的小项目从头到尾拆开讲明白保证你看完能直接复用而不是看完一堆术语之后继续对着开发板发呆。我自己的习惯是先用最便宜的方式把链路打通再考虑加传感器、加控制、加存储。所以这篇文章里我用到的硬件就只有一块ESP32开发板外加一个DHT11温湿度传感器成本加起来不到一杯咖啡钱。软件方面用的是Arduino框架和PubSubClient库Broker选择在本地局域网用开源方案自建全程不依赖任何公网服务器数据在自己的网络里流转稳定且可控。整个过程大约两小时能搞定适合初次接触MQTT的人也适合想从裸板直接跳到可用场景的老玩家。1. 项目底色为什么二手ESP32和MQTT是绝配1.1 咸鱼版ESP32的常见坑与挑选判断先说“咸鱼”这件外套。ESP32开发板本身就是物联网项目里的万金油双核240MHz、WiFi 蓝牙双模、几十个GPIO、硬件加密和安全启动该有的都有。二手平台上流通的ESP32大致分三类一是个人囤货没用完的半成品二是某些开发项目剩下的工程板三是极少数真正坏了又被挂出来卖的问题板。第三种价格确实最低但风险也最大我自己就买过一块“USB口接触不良”的板子到手发现芯片发热异常最终只能拆了Flash当废料。所以判断一块二手板子值不值得留我一般看三个地方。第一件事是看丝印和版本。ESP32家族看起来很乱但其实很好区分经典的ESP32双核、无内置天线增益、ESP32-S2单核、USB原生、ESP32-S3双核、AI加速、有原生USB和更多IO、ESP32-C3单核、RISC-V架构、性价比极高。对MQTT这种网络协议类应用来说哪怕是最老的ESP32都绰绰有余。我的建议是如果不是追求极低功耗别买ESP32-C3它的WiFi和经典版一样好但引脚内部上拉默认状态比较特殊新手容易在GPIO配置上栽跟头。第二件事是拿到手先做“裸板点灯”测试。先把板子通过MicroUSB或者Type-C线插到电脑正常情况下设备管理器里会冒出一个串口能打开串口监视器的基本就没死透。如果你用的是经典ESP32开发板下载时需要按住板子上标着“BOOT”或“IO0”的按键进入下载模式很多二手板子的按键都已经有瑕疵导致程序烧不进去。这时候别急着扔手动用杜邦线把IO0接到GND再上电同样能进下载模式——这是买二手板子必须会的庄稼把式。第三件事是测试板载稳压器。二手板子最容易暗病的地方就是USB转串口电路和3.3V稳压芯片具体表现是插上电脑能识别串口但一给模块供电就反复重启或者WiFi一开电流一大就掉压。测试方法很简单用串口监视器盯住启动日志如果看到反复打印“Brownout detector was triggered”说明供电不稳。这种板子别用来做需要长时间在线运行的MQTT节点但拿来做离线实验倒还凑合。1.2 MQTT在这个项目里到底解决什么问题回到项目本身。我为什么要在ESP32上跑MQTT而不是用经典的HTTP轮询举个例子你有三个传感器节点每个节点10秒钟上报一次数据用HTTP请求的话每个节点每次要建立TCP连接发完就断开服务端还要处理CORS、连接池这些乱七八糟的东西。而MQTT是长连接协议客户端和Broker之间保持一个TCP会话所有的消息通过主题发布和订阅服务端不需要知道客户端是谁、在哪、有几个——连接管理和流量控制全交给Broker。数据从“你拉”变成“它推”实时性和服务端压力完全是两个量级。MQTT的另一个实用价值是双向通信。传感器节点可以用同一个连接去发布温湿度数据同时订阅控制主题来接收LED开关、继电器通断的指令。这在HTTP模型里要么靠WebSocket要么靠轮询都很别扭。而MQTT天然支持全双工一个连接搞定“上报遥测”和“下发控制”两件事。对智能家居场景来说这就是最合适的底座协议。还有一个容易被忽略的点MQTT省电。长连接看起来一直在耗电流但ESP32的WiFi休眠模式配合MQTT的心跳机制可以让节点在空闲时进入Light Sleep电流从几十毫安降到几毫安。HTTP轮询则不同每次唤醒都要重新扫描WiFi、建立连接、解析响应一次完整请求的时间足够MQTT发好几十条消息了。这个差异在电池供电的场景下非常致命。2. 核心概念拆解搞懂MQTT的发布/订阅模型2.1 Topic、QoS和Broker用大白话讲明白MQTT的核心机制可以浓缩成一句话客户端通过Broker转发消息消息按照“主题”来分类想收消息的客户端去“订阅”对应主题想发消息的客户端往这个主题“发布”内容。你可以把Broker想象成小区收发室主题是门派号发布者是往门牌下塞信的人订阅者是每天来翻自己信箱的人。信件到了收发室收发室按照门牌分发别人家信不会塞到你家。这里要特别注意Topic的通配符。在MQTT里订阅方可以使用单层通配和#多层通配。比如订阅sensors//temperature可以同时匹配sensors/room1/temperature和sensors/room2/temperature订阅sensors/#则能匹配sensors/下面所有的层级。但发布方不能使用通配符你往sensors//temperature发消息这会被当成一个普通主题名而不是通配规则。不少新手在这里踩坑发布端用了通配符Broker端怎么等都等不到消息。记住通配符只存在于订阅端。QoS是MQTT里另一个绕不开的概念它定义了消息传输的保障级别。QoS 0是尽力而为消息丢了就丢了适合高频传感器数据丢一两个无所谓QoS 1是至少一次Broker收到消息后返回PUBACK客户端收到确认前会重发代价是可能重复QoS 2是恰好一次通过两阶段握手PUBREC、PUBREL、PUBCOMP保证不重不漏适合计费、指令这种语义严格的消息。在实际的ESP32项目里遥测数据我通常用QoS 0控制指令用QoS 1或者QoS 2。这已经是业内很稳的实践套路了。再说Broker。Broker是MQTT架构里唯一的中枢节点它负责接收所有客户端的连接、鉴权、转发消息。开源生态里我推荐Mosquitto和EMQX前者轻量、配置简单适合单机小规模后者功能全、带Web控制台和规则引擎适合要可视化监控、数据桥接的场景。如果你只是做ESP32的入门项目用Mosquitto就足够了一条命令装完改一下配置文件就能跑起来。后面我给的实操步骤会以Mosquitto为例。2.2 消息结构里的三个隐形机制保留消息、遗嘱和心跳在完成一个“能跑”的MQTT项目之前有三个机制很容易被忽略但它们恰恰决定了项目在真实环境中的可靠性。保留消息Retained Message。发布消息时你可以设置retain标志Broker会存下这个主题的最后一条消息并把它推给之后新加入的订阅者。这个特性对状态型数据特别有用。比如智能家居里有一个“客厅灯开关”的状态主题你希望每次网关重启后App都能立刻知道灯是亮的还是灭的而不是等传感器上报一次才有状态。把开关状态用保留消息发到home/livingroom/light/stateBroker会把最后的状态存下来新客户端订阅这个主题时立刻收到它。这在做设备状态同步的时候几乎是必须的。遗嘱消息LWT。客户端在连接Broker时可以指定一个遗嘱主题和遗嘱消息比如clients/esp32_a/status消息内容为“offline”。当客户端异常断开比如断电、断网超时时Broker会代替客户端把这个遗嘱消息发出去。这是个简单却极其有效的机制你的监控平台订阅所有客户端的status主题就能实时知道哪台设备掉线了。很多人在做多设备集群时才发现有这个功能其实一开始就应该用上。心跳与Keep Alive。MQTT协议里客户端可以设置一个Keep Alive间隔单位是秒。在这个间隔内客户端至少要发一个PINGREQ给Broker否则Broker会认为连接已死并清理会话。ESP32在低功耗场景下要注意不要因为想省电把Keep Alive设得特别大否则设备断网了Broker却迟迟不清理控制指令发不出去两边干瞪眼。我建议室内局域网设备设30秒到60秒需要跨公网时也尽量不超过120秒。3. 实操落地从搭Broker到ESP32双工通信3.1 本地搭建Mosquitto Broker的具体步骤我先说部署环境。一台普通的局域网电脑或者树莓派都可以不需要高性能。以Ubuntu/Debian系为例安装极其简单sudo apt update sudo apt install -y mosquitto mosquitto-clients安装完默认配置已经能启动服务了mosquitto监听1883端口。但为了稳妥我建议修改配置文件/etc/mosquitto/mosquitto.conf加上以下内容listener 1883 allow_anonymous true persistence true persistence_location /var/lib/mosquitto/allow_anonymous true的意思是不启用用户名密码验证适合在可信局域网内调试。如果你的WIFI环境里有陌生设备稳妥起见还是关掉匿名访问用mosquitto_passwd生成账号密码。创建账号密码的命令是sudo mosquitto_passwd -c /etc/mosquitto/passwd esp32client然后在配置文件里加上allow_anonymous false password_file /etc/mosquitto/passwd配置完成后重启服务sudo systemctl restart mosquitto sudo systemctl status mosquitto看到active (running)就说明服务起来了。你可以先用命令行工具自测一下开两个终端一个跑订阅一个跑发布# 终端A mosquitto_sub -h 127.0.0.1 -t test/topic # 终端B mosquitto_pub -h 127.0.0.1 -t test/topic -m hello终端A能收到hello说明Broker工作正常。有一个细节值得注意mosquitto安装后默认可能没有persistence true如果不想重启后所有保留消息清零这一行务必加上。保留消息丢失在开发阶段无所谓但一旦设备上线运行网关重启后所有设备状态都会变成未知排查起來极其麻烦。3.2 ESP32侧接入准备Arduino环境与第三方库硬件侧我在开头说过用ESP32开发板加DHT11。这里说一下开发环境。我用的是Arduino IDE 2.x在“开发板管理器”里搜索esp32安装官方支持包。安装过程要几分钟耐心等等。然后需要安装两个库PubSubClientMQTT客户端库轻量、稳定是Arduino生态里最常用的MQTT库。DHT sensor library用于读取DHT11/DHT22温湿度数据。装好之后选择开发板型号。我的板子是经典ESP32 Dev Module选ESP32 Dev Module即可。串口选择会对应到设备管理器里出现的那个COM口Windows下通常是COM3、COM4之类。这里有个坑如果烧录时提示Connecting...卡住十有八九是板子没进下载模式需要按住IO0引脚对应的按键再通电。3.3 完整代码实现上报数据 订阅控制一肩挑下面直接给出一段可用的完整代码。它的功能是连接WiFi和MQTT Broker后以随机生成的模拟温湿度作为传感器数据每5秒向sensors/dev_a/temperature和sensors/dev_a/humidity两个主题发布一次数据同时订阅actuators/dev_a/led主题收到字符串on时点亮板载LED收到off时熄灭。你可以在此基础上把“模拟数据”替换成DHT11的真实读取值。#include WiFi.h #include PubSubClient.h const char* ssid YourWiFiSSID; const char* password YourWiFiPassword; const char* mqtt_server 192.168.1.100; // Mosquitto所在主机的局域网IP const int mqtt_port 1883; WiFiClient espClient; PubSubClient client(espClient); unsigned long lastSendTime 0; const long sendInterval 5000; const int ledPin 2; // 板载LED引脚经典ESP32 Dev Module通常是GPIO2 void setup() { Serial.begin(115200); pinMode(ledPin, OUTPUT); digitalWrite(ledPin, LOW); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(\nWiFi connected); client.setServer(mqtt_server, mqtt_port); client.setCallback(callback); connectMQTT(); } void loop() { if (!client.connected()) { connectMQTT(); } client.loop(); if (millis() - lastSendTime sendInterval) { lastSendTime millis(); publishSensorData(); } } void connectMQTT() { while (!client.connected()) { // 客户端ID在Broker上必须是唯一的 if (client.connect(esp32_dev_a)) { Serial.println(MQTT connected); client.subscribe(actuators/dev_a/led); // 发布一条在线状态保留消息方便网关感知 client.publish(clients/dev_a/status, online, true); } else { Serial.print(MQTT connect failed, rc); Serial.println(client.state()); delay(3000); } } } void callback(char* topic, byte* payload, unsigned int length) { String message; for (unsigned int i 0; i length; i) { message (char)payload[i]; } Serial.print(Message arrived [); Serial.print(topic); Serial.print(] ); Serial.println(message); if (String(topic) actuators/dev_a/led) { if (message on) { digitalWrite(ledPin, HIGH); // 把执行结果发回去形成闭环 client.publish(clients/dev_a/led_state, on, true); } else if (message off) { digitalWrite(ledPin, LOW); client.publish(clients/dev_a/led_state, off, true); } } } void publishSensorData() { // 这里用随机数模拟DHT11读数接真传感器后替换为实际读取函数 float temperature random(2000, 3200) / 100.0; float humidity random(5000, 8000) / 100.0; String tempStr String(temperature, 2); String humStr String(humidity, 2); client.publish(sensors/dev_a/temperature, tempStr.c_str(), false); client.publish(sensors/dev_a/humidity, humStr.c_str(), false); Serial.print(Published: temp); Serial.print(tempStr); Serial.print(, hum); Serial.println(humStr); }这段代码里有两个细节值得展开讲。第一是client.connect(esp32_dev_a)后面的客户端ID。它在同一台Broker上必须是唯一的如果多个设备用了同一个ID前一个设备会被强制踢下线两个设备就会无限互踢。真实项目中客户端ID一般用设备MAC地址或自定义编号拼接比如esp32_ String((uint32_t)ESP.getEfuseMac())保证全局唯一。第二是client.publish(clients/dev_a/status, online, true);这行代码里最后一个参数true意味着发送的是保留消息。这样网关或者监控平台重启后订阅clients/dev_a/status立刻就能知道这个设备在线不用干等下一次心跳。3.4 如何把真实DHT11数据接入来代替模拟值上面的代码里用了random生成温湿度如果你手上正好有DHT11模块替换起来也很简单。接线如下DHT11的VCC接开发板3.3VDHT11的GND接GNDDHT11的DATA接GPIO4然后在代码开头引入库并定义对象#include DHT.h #define DHTPIN 4 #define DHTTYPE DHT11 DHT dht(DHTPIN, DHTTYPE);在setup()里加上dht.begin();把publishSensorData()函数里的随机数改成真实读取float temperature dht.readTemperature(); float humidity dht.readHumidity(); if (isnan(temperature) || isnan(humidity)) { Serial.println(Failed to read from DHT sensor!); return; }这里有个很实用的经验DHT11的数据引脚和杜邦线连接时经常接触不良导致偶尔读到nan。处理方式是在物理连接处上锡或者用热缩管固定同时在代码里加isnan判断别让异常数据刷爆你的消息流。DHT11本身精度一般精度只有±2摄氏度但做环境监测够用需要更高精度就换DHT22或者SHT30代码接口基本一样只改DHTTYPE和引脚定义就行。4. 实际运行过程中的常见问题与排查思路4.1 连接失败到底卡在哪一步如何定位MQTT项目最容易出的问题就是设备连不上Broker。很多新手一看“MQTT connect failed”就开始怀疑代码但实际上问题往往出在网络链路或者Broker配置上。我复盘过许多次总结了一套从下往上排查的顺序。第一步确认ESP32和Broker在同一局域网内能够互相访问。用电脑登录路由器后台查一下Mosquitto主机的IP再查一下ESP32获取到的IP确保网段一致。如果路由器开了AP隔离两个设备之间是完全不通的——这种坑藏得极深因为手机、电脑看起来都在同一个网络里但设备之间被隔离了。实在查不出原因时可以直接用ESP32的固件里ping一下Broker IP或者临时把Broker和ESP32都用网线/手机热点连到同一个路由器排除网络隔离干扰。第二步确认1883端口通不通。在Mosquitto主机上运行sudo netstat -tulpn | grep 1883看监听地址是不是0.0.0.0:1883。如果只监听了127.0.0.1那局域网其他设备当然连不上需要在监听配置里写明listener 1883 0.0.0.0。这一步非常常见因为新版Mosquitto在某些发行版上的默认配置只监听本地回环地址。第三步再看ESP32端返回的错误码。PubSubClient连接失败时client.state()会返回一个数字。-2是网络连接失败-4是连接超时-5是连接被拒绝通常是Broker拒绝了连接。如果卡在-5优先检查Broker端是否开启了allow_anonymous false但ESP32代码里没提供用户名密码。此时需要在client.connect(客户端ID, 用户名, 密码)里填写正确账号。另一个高频问题是WiFi.status()一直不等于WL_CONNECTED也就是WiFi都连不上。这种情况先看SSID和密码有没有填错再确认ESP32天线附近有没有金属遮挡。ESP32的板载天线对周边环境很敏感把它放到金属接线盒里信号强度会骤降实际项目中很常见。4.2 数据在发布与订阅间“丢失”从哪里入手MQTT里面“消息丢了”其实很少是协议本身丢更多是使用方式的问题。最常见的一个场景是订阅端比发布端启动得晚而去订阅时主题上已经有了历史消息于是觉得“之前的消息丢了”。如果这些历史消息是状态类数据解决方案是发布时用保留消息如果是一定周期内的最新值可以在订阅后请求设备主动发一次当前值相当于做一次“主动拉取”。第二个常见问题是QoS设置不当。控制指令用QoS 0发送偶尔网络抖动一次消息就没了。我的习惯是把所有控制类主题统一用QoS 1并且在ESP32的callback里返回一个确认消息比如发布到clients/dev_a/led_state。这样上层逻辑可以根据确认消息判断指令是否执行成功而不是只管发不管结果。第三个和主题通配符有关。我在接入多个设备的时候订阅端最忌讳用零散主题列表而是应该订阅带通配符的主题比如actuators/#和sensors/#。但要注意如果你订阅了ss也没有用。通配符只匹配完整的层级。举例来说订阅actuators/#会收到actuators/dev_a/led但不会收到actuatorsdev_a_led——因为后者不是一个层级结构。4.3 掉线重连和状态同步的避坑指南MQTT节点跑上几天后偶尔会掉线这是正常的。ESP32的WiFi模块在长时间运行后可能因为驱动异常或者供电波动断开连接关键是重连逻辑要写得健壮。我的建议是重连不仅要重连WiFi还要重新订阅主题并且重发在线状态。光有client.setServer和setCallback还不够connectMQTT()里的subscribe和publish online每次重连都要执行一遍这正是上面代码里把订阅写在连接成功之后的原因。关于保留消息还有一个容易踩的坑如果设备正常关机但程序写的是“发送offline遗嘱”那没问题如果设备是被直接拔电Broker会通过遗嘱消息发现设备离线。可是如果你在程序里同时发布了“online”保留消息和“offline”遗嘱消息主题却是同一个clients/dev_a/status就有一个顺序问题设备正常下线时程序主动发一条offline保留消息又发一条offline遗嘱两者会重复。我的经验是二者只用其一要么在正常关闭时主动发布状态要么依赖Broker的遗嘱机制别两条腿走路。5. 项目扩展从单节点到一套可用的智能家居底座5.1 多设备多主题的组织规范一个项目跑通了接下来自然想把所有传感器、开关都挂上去。这时候如果主题命名随手写后面维护会非常痛苦。我常用的命名规则是第一层级是设备类型或空间第二层级是设备唯一代号第三层级是数据类型最后一层是属性。例子如下sensors/livingroom/temperaturesensors/livingroom/humidityactuators/livingroom/light/stateactuators/livingroom/light/commandclients/livingroom/light/status这样组织的好处是订阅端可以按空间订阅sensors/livingroom/#获取这个房间全部传感器数据也可以按类型订阅sensors//temperature拿到所有房间的温度。如果将来要接进Home Assistant这类平台它们本身也理解这种层级化主题结构几乎可以不写胶水配置直接识别设备。另外我强烈建议区分state和command两个主题。command是别人发给设备让它执行的指令state是设备反馈给系统的真实状态。这样做的好处是控制逻辑和真实状态解耦避免出现“指令发了但设备没动”这种边界情况。系统以state为准去做展示和联动command只管下发。5.2 把ESP32的数据接到可视化监控上MQTT只是消息管道数据进了Broker之后还要有地方展示。最简单的方式是装一个Node-RED它自带MQTT输入节点可以订阅sensors/#然后通过Dashboard节点输出到网页。整个操作几乎不用写代码拖几个节点就完成了实时温度曲线图。但Node-RED要跑在常开的设备上这个门槛对很多入门用户来说有点高。更轻量的方案是用mosquitto_sub在命令行实时打印或者写一个Python脚本用paho-mqtt库来订阅并存入SQLite。Python方案的代码框架大致是这样import paho.mqtt.client as mqtt import sqlite3 conn sqlite3.connect(sensor_data.db) def on_message(client, userdata, msg): topic msg.topic payload msg.payload.decode() cur conn.cursor() cur.execute(INSERT INTO sensor_log(topic, value, ts) VALUES(?, ?, datetime(now)), (topic, payload)) conn.commit() print(topic, payload) client mqtt.Client() client.on_message on_message client.connect(192.168.1.100, 1883, 60) client.subscribe(sensors/#) client.loop_forever()这个脚本同样可以跑在树莓派或者一台老旧笔记本上。注意在Windows上跑Python脚本时记得在Broker配置里允许局域网内的连接否则客户端会被Broker拒绝。把采集、存储、展示分开是这套架构最有价值的地方。数据源ESP32、传输层MQTT、消费端监控程序各自独立任何一个环节坏掉另外两端都能继续运行。这种解耦设计放到智能家居里比把所有逻辑都塞进一个APP里要可靠太多。5.3 低功耗与远程控制思路后续画个饼等基础链路稳定之后再往深走的方向也很多。比如ESP32可以在发布完数据后进入esp_light_sleep_start()用定时器每隔15分钟唤醒一次搭配18650电池几个月换一次电不是梦。再比如把消息桥接到公网Broker上手机在户外也能查看家里温度。不过我先提醒一下把内部数据传到公网之前一定要先做好账号鉴权和消息加密MQTT默认是明文传输直接暴露在公网上相当于把家门钥匙挂在门口。安全这个话题能单独写一整篇这里先埋个引子。最后说点折腾过程中的心里话这块二手板子从我拿到手到现在已经连续运行了将近一个星期中间经历过几次调试断电也经历了WiFi信号抖动的时刻但MQTT链路本身一直没掉过链子。现在每次看到监控页面上那条平滑的温度曲线我都觉得当初花在调试连接逻辑上的时间没有白费。这个项目教给我最有价值的一点是不要在买新硬件上纠结用一个二手板子和一个轻量级协议先把端到端的通路跑通后面无论接什么传感器、接多少设备都是在同一条管道上做文章而已。MQTT真正强大的地方不在于它能“把数据发出去”而在于它把数据发送、接收、状态同步、掉线感知这些常见的物联网问题全部收敛到了一套统一规则里。你先动手把这条通道打通再回头想说做点什么都来得及。