ESP32 实战教程:做一个家庭温湿度监测节点

本文由 AI 协助整理,代码和验证步骤适用于学习记录。不同开发板、传感器型号、库版本与网络环境可能导致细节差异,请以实际硬件现象为准。

ESP32 温湿度监测节点数据路径

为什么要做这个小项目

如果只从“点亮 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 线,性格却不一样。有的线负责通讯,有的线只负责假装自己很忙。

ESP32 温湿度节点参考接线

接线建议

如果使用 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 扫描程序,确认设备地址是否存在。能扫描到地址,至少说明供电和总线通信不是完全断的;扫不到,先回到接线和供电。

程序结构

我会把程序拆成四个小职责:

  1. 采样:定时读取传感器,过滤无效值。
  2. 显示:OLED 展示最近一次有效数据和连接状态。
  3. 联网:维护 Wi-Fi 和 MQTT 连接。
  4. 上报:按固定周期发布 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 就不再只是一个“能跑示例”的开发板,而是一个有输入、有输出、有状态、有脾气的小设备。

真正的学习也从这里开始:当一切正常时让它工作,当一部分不正常时让它别乱。当设备能把自己的状态说清楚,排错就不再像摸黑找开关,而像按图索骥,虽然偶尔还是会想叹气,但至少知道从哪儿叹起。

打赏

如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏码

打赏支持:王庆岩

posted on 2026-09-15 10:35  潇洒石头  阅读(25)  评论(0)    收藏  举报

导航