ESP32 实战教程:做一个家庭温湿度监测节点
本文由 AI 协助整理,代码和验证步骤适用于学习记录。不同开发板、传感器型号、库版本与网络环境可能导致细节差异,请以实际硬件现象为准。

为什么要做这个小项目
如果只从“点亮 LED”开始学 ESP32,很容易觉得它像一块会闪灯的小板子;如果把它接上传感器、屏幕和网络,它就立刻有了设备的味道。家庭温湿度监测节点是一个很适合练手的项目,因为它足够真实,又不会一上来把人按进复杂协议和工业现场里。它有输入:传感器读数;有输出:OLED 显示;有通信:Wi-Fi 与 MQTT;还有一堆非常生活化的异常:断网、读数失败、传感器偶尔发呆、路由器换密码。
这类项目最有意思的地方,不在于温度到底是 25.6 度还是 25.7 度,而在于我们开始关心“设备如何长期运行”。一个 Demo 可以在桌面上跑三分钟,一个设备则要在角落里跑很多天。它不能因为一次读取失败就重启,也不能因为网络断开就彻底沉默。换句话说,ESP32 从学习板变成小设备,靠的不是多写几行代码,而是把正常路径和异常路径都安排明白。
本文的目标是做一个简洁但像样的温湿度节点:本地 OLED 显示当前温湿度,通过 MQTT 上报数据,断网时继续显示最近一次有效读数,传感器读取失败时不发布脏数据。我们先把它写成学习版,不追求花哨外壳,也不追求工业级精度。先能跑、能看、能解释,再逐步打磨。
硬件选择
传感器可以从三类里选。DHT11 最便宜,但精度和稳定性一般,更适合入门演示;DHT22 比 DHT11 好一些,刷新间隔也不能太快;SHT30、BME280 这类 I2C 传感器体验更舒服,读数更稳定,接线也比较整齐。我的建议是:如果你只想快速看到数据,用 DHT22;如果你想把文章写得更像教程,也更像后续能复用的项目,用 SHT30 或 BME280。
显示屏建议用 0.96 寸 I2C OLED。它不大,但足够显示温度、湿度、Wi-Fi 状态和 MQTT 状态。更重要的是,有了本地显示以后,设备不再完全依赖电脑串口。很多时候你把 ESP32 插在角落里,懒得再接 USB,屏幕上能看到“WiFi...”或者“MQTT OK”,调试体验会好很多。
开发板方面,普通 ESP32 DevKit 就够用。供电尽量稳定,不要用那种只能充电不能传数据的 USB 线。这里有个很朴素但很常见的坑:线看起来都叫 USB 线,性格却不一样。有的线负责通讯,有的线只负责假装自己很忙。

接线建议
如果使用 SHT30、BME280 或 OLED,常见 I2C 接线如下:
| 模块端口 | ESP32 常见接法 |
|---|---|
| VCC | 3V3 |
| GND | GND |
| SDA | GPIO21 |
| SCL | GPIO22 |
如果使用 DHT22,可以这样接:
| DHT22 端口 | ESP32 接法 |
|---|---|
| VCC | 3V3 |
| DATA | GPIO4 |
| GND | GND |
DHT22 的 DATA 线通常需要上拉电阻,很多模块板已经带了,但裸传感器不一定有。I2C 模块也一样,不要默认所有模块都“内部已经安排好了”。当屏幕不亮或传感器读不到时,第一步不是改代码,而是跑一个 I2C 扫描程序,确认设备地址是否存在。能扫描到地址,至少说明供电和总线通信不是完全断的;扫不到,先回到接线和供电。
程序结构
我会把程序拆成四个小职责:
- 采样:定时读取传感器,过滤无效值。
- 显示:OLED 展示最近一次有效数据和连接状态。
- 联网:维护 Wi-Fi 和 MQTT 连接。
- 上报:按固定周期发布 JSON 数据。
这样拆不是为了显得高级,而是为了排错时不把所有问题揉在一起。比如 OLED 不显示,不应该影响 MQTT 上报;网络断开,也不应该影响传感器采样。学习阶段可以先写在一个文件里,但脑子里要有边界:谁负责拿数据,谁负责展示,谁负责发出去。
MQTT 主题设计
主题命名不要随便写成 test。test 在第一天很方便,三个月后它就会变成“我也不知道谁在发”的悬案。可以按空间、设备、消息类型分层:
home/bedroom/esp32-env/telemetry
home/bedroom/esp32-env/status
home/bedroom/esp32-env/cmd
telemetry 用来上报温湿度,status 用来上报在线、离线、版本号,cmd 可以留给以后远程修改采样周期。即使现在只用一个主题,也建议先养成结构化命名的习惯。项目越小,越容易把规范当负担;项目一变大,就会发现规范其实是给未来的自己留一条回家的路。
示例代码骨架
下面是一个学习版骨架,重点是结构,不是让它覆盖所有库的差异。传感器库、OLED 库和 MQTT 库请按自己实际安装的版本调整。
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "your-ssid";
const char* WIFI_PASS = "your-password";
const char* MQTT_HOST = "192.168.1.10";
const int MQTT_PORT = 1883;
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
struct EnvSample {
float temperature;
float humidity;
bool valid;
unsigned long updatedAt;
};
EnvSample latest = {0, 0, false, 0};
unsigned long lastSampleAt = 0;
unsigned long lastPublishAt = 0;
bool readSensor(EnvSample& out) {
// 这里替换成 DHT22 / SHT30 / BME280 的真实读取代码。
float t = 25.6;
float h = 58.2;
if (isnan(t) || isnan(h)) {
return false;
}
if (t < -20 || t > 80 || h < 0 || h > 100) {
return false;
}
out.temperature = t;
out.humidity = h;
out.valid = true;
out.updatedAt = millis();
return true;
}
void ensureWifi() {
if (WiFi.status() == WL_CONNECTED) return;
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) {
delay(300);
Serial.print(".");
}
Serial.println();
}
void ensureMqtt() {
if (WiFi.status() != WL_CONNECTED || mqtt.connected()) return;
mqtt.setServer(MQTT_HOST, MQTT_PORT);
String clientId = "esp32-env-" + String((uint32_t)ESP.getEfuseMac(), HEX);
if (mqtt.connect(clientId.c_str())) {
mqtt.publish("home/bedroom/esp32-env/status", "online", true);
}
}
void publishSample() {
if (!latest.valid || !mqtt.connected()) return;
char payload[96];
snprintf(payload, sizeof(payload),
"{\"temperature\":%.1f,\"humidity\":%.1f,\"uptime\":%lu}",
latest.temperature, latest.humidity, millis() / 1000);
mqtt.publish("home/bedroom/esp32-env/telemetry", payload);
}
void setup() {
Serial.begin(115200);
Serial.println("ESP32 env node boot");
}
void loop() {
ensureWifi();
ensureMqtt();
mqtt.loop();
if (millis() - lastSampleAt >= 3000) {
lastSampleAt = millis();
EnvSample sample;
if (readSensor(sample)) {
latest = sample;
Serial.printf("T=%.1f H=%.1f\n", latest.temperature, latest.humidity);
} else {
Serial.println("sensor read failed");
}
}
if (millis() - lastPublishAt >= 30000) {
lastPublishAt = millis();
publishSample();
}
}
实际接入 OLED 时,可以把显示函数单独封装出来:
void renderDisplay() {
// display.clearDisplay();
// 第一行:温度和湿度
// 第二行:Wi-Fi 是否已连接
// 第三行:MQTT 是否已连接
// 第四行:最近更新时间
}
屏幕显示不必一开始就做得很漂亮。先把关键状态放上去:温度、湿度、Wi-Fi、MQTT、最近更新时间。调试设备时,“现在到底卡在哪一步”比字体大小更重要。等功能稳定后,再考虑图标、布局和刷新动画。
异常处理
这个项目真正值得写厚的地方,是异常处理。
传感器读取失败时,不要把失败值发布出去。比如 DHT 库常见的 NaN,如果不处理,后端收到的就是一堆无法画图的数据。更稳妥的做法是保留最近一次有效读数,并在状态里标记“本次读取失败”。屏幕可以显示 Sensor ERR,MQTT 可以只发布状态,不发布遥测。
网络断开时,不要让主循环永远卡在 Wi-Fi 等待里。很多示例代码会写:
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
学习连接流程时这么写没问题,但项目里最好加超时。否则路由器一重启,设备就站在原地等,其他功能也一起停工。更好的思路是:连接失败就记一笔日志,过一会儿再试;与此同时,传感器和屏幕继续工作。
MQTT 断开时也类似。重连要有节制,不要每一轮 loop 都疯狂连接。可以记录上次重连时间,每隔几秒尝试一次。设备要像一个有耐心的人,而不是一断网就开始拍桌子。
验证步骤
第一步,只验证传感器。先不接 OLED,也不接 MQTT,只在串口打印温湿度。用手捂住传感器或对着传感器轻轻呼气,观察湿度是否变化。这个动作看起来朴素,但非常有效,能证明数据确实来自外界,而不是程序里某个写死的数字。
第二步,加入 OLED。屏幕能显示温湿度以后,把 USB 线拔掉再上电,确认设备不依赖串口工具也能运行。这里要注意供电电流和线缆质量,有些 OLED 加上传感器后,劣质 USB 供电会让系统偶发重启。
第三步,加入 Wi-Fi。串口打印 IP 地址,确认设备连上的是 2.4GHz 网络。ESP32 经典款不支持 5GHz,这个坑很常见,而且它不吵不闹,只是连接不上。
第四步,加入 MQTT。先用电脑上的 MQTT 客户端订阅主题,确认能收到 JSON。收到以后不要立刻高兴得太早,继续测试断网、重启 Broker、拔掉传感器。真正的教程不只展示“成功截图”,还要展示系统如何面对小意外。
可以继续扩展什么
这个项目后续可以加很多东西,但我建议按顺序来。
先加数据平滑,比如最近 5 次取平均,避免数值跳动太明显。然后加配置保存,把 MQTT 地址、设备名、采样周期放进 NVS 或 LittleFS。再往后可以加 Web 配网页,让设备首次启动时进入配置模式。最后再考虑 OTA,让设备不用每次插线升级。
如果要写成系列文章,这篇可以作为“节点搭建篇”,下一篇写“工程化篇”。前者解决“设备能做事”,后者解决“设备能长期做事”。一个负责让读者看到成果,一个负责让读者相信这不是桌面魔术。
常见问题
OLED 不亮:先检查地址,常见是 0x3C,但不要想当然。跑扫描程序比盯着代码猜半小时更优雅。
DHT22 偶尔失败:降低读取频率,检查上拉电阻和线长。DHT 系列本来就不适合高频读取,不要把它当秒表使唤。
MQTT 连不上:检查 Broker 地址、端口、防火墙和客户端 ID。多个设备使用同一个客户端 ID,会互相挤下线。
温湿度看起来不准:先确认传感器位置。它如果贴着 ESP32 或稳压芯片,读到的就不只是房间温度,还包含开发板的热情。
本篇小结
这个温湿度节点不算复杂,但它把 ESP32 项目里最重要的几件事串了起来:传感器采样、本地显示、网络连接、消息上报、异常恢复。写到这里,ESP32 就不再只是一个“能跑示例”的开发板,而是一个有输入、有输出、有状态、有脾气的小设备。
真正的学习也从这里开始:当一切正常时让它工作,当一部分不正常时让它别乱。当设备能把自己的状态说清楚,排错就不再像摸黑找开关,而像按图索骥,虽然偶尔还是会想叹气,但至少知道从哪儿叹起。
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号