ESP32气象站进阶玩法:用DHT22+BMP180打造智能浇花系统(附钉钉机器人报警)

去年夏天,我养了多年的几盆绿植差点全军覆没。出差一周回来,发现阳台上的琴叶榕叶子枯黄卷曲,龟背竹也蔫得不成样子。那一刻我突然意识到,对于经常出差或者偶尔会忘记浇水的人来说,一个真正智能、能自主决策的植物养护系统有多重要。市面上的智能花盆要么功能单一,要么价格昂贵,而且往往缺乏灵活的自定义能力。

于是我开始琢磨,能不能用手里闲置的ESP32开发板,结合几个常见的传感器,自己动手搭建一个既专业又实用的智能浇花系统?这个系统不仅要能自动监测土壤湿度并浇水,还要能记录环境温湿度和气压变化,甚至在出现异常时主动通知我。经过几个版本的迭代,我终于打磨出了一套稳定可靠的方案,今天就把这个项目的完整实现过程分享给大家。

这个系统最让我满意的地方在于它的高度集成和智能化。它不仅仅是简单的“干了就浇”,而是综合了多种环境因素,实现了更符合植物生长规律的养护策略。同时,通过钉钉机器人推送报警和状态报告,即使人不在家,也能对植物的状况了如指掌。

1. 系统架构设计与核心组件选型

在开始动手之前,我们需要先理清整个系统的设计思路。一个完整的智能浇花系统,本质上是一个小型的物联网(IoT)应用,它需要完成数据采集、逻辑判断、执行控制、状态上报这四大核心功能。

基于这个思路,我选择了以下硬件组件来构建系统:

  • 主控单元:ESP32开发板。选择ESP32而非ESP8266,主要是看中其更强大的处理能力、更多的GPIO引脚以及更低功耗的深度睡眠模式。对于需要长时间运行且可能连接多个传感器的系统,ESP32的双核处理器和蓝牙功能也为未来扩展留下了空间。
  • 环境感知模块
    • DHT22温湿度传感器:用于监测空气温度和湿度。相比DHT11,DHT22的精度更高(温度±0.5°C,湿度±2%),量程也更广。
    • BMP180气压传感器:除了测量气压,它还能提供精确的温度读数(可作为DHT22的补充校验),并且可以通过气压变化趋势辅助预测天气,为浇水策略提供更多参考维度。
    • 土壤湿度传感器(电容式):这是系统的核心输入。我强烈建议使用电容式而非电阻式传感器。电阻式传感器通过电极测量电导率,长期埋在土里会导致电极电解腐蚀,读数漂移严重。电容式传感器通过检测介电常数变化来测量湿度,不与土壤直接发生电化学反应,寿命和稳定性都好得多。
  • 执行机构:5V继电器模块 + 微型水泵。继电器作为电子开关,由ESP32的GPIO口控制,进而驱动12V或5V的直流微型水泵。选择水泵时要注意扬程和流量,对于家庭盆栽,扬程1-2米、流量2-3L/min的小水泵就足够了。
  • 人机交互与通知
    • OLED显示屏(可选):用于本地显示实时数据,方便调试和查看。
    • 钉钉机器人:用于远程报警和状态推送。相比自己搭建MQTT服务器或使用其他IoT平台,钉钉机器人配置简单、免费、且与日常办公沟通工具集成,报警信息能被即时看到。

下表对比了不同传感器组合方案的优劣,帮助你根据自身需求做选择:

组件方案 核心传感器 优点 缺点 适用场景
经济基础版 土壤湿度传感器(电阻式) + 继电器 成本极低,搭建快速 电阻式传感器易腐蚀,寿命短;无环境数据 对成本敏感,只需基本浇水功能
标准实用版 土壤湿度传感器(电容式) + DHT22 + 继电器 传感器稳定,增加环境温湿度监测 缺少气压数据,无法进行更复杂的天气关联分析 大多数家庭盆栽养护,追求稳定和性价比
进阶专业版 土壤湿度传感器(电容式) + DHT22 + BMP180 + 继电器 数据维度全,可进行环境综合判断,预测性更强 成本稍高,接线和程序略复杂 对养护有较高要求,或用于小型温室、植物实验

我本次分享的是进阶专业版的实现。整个系统的数据流是这样的:各类传感器周期性采集数据 -> ESP32主控分析数据并执行浇水逻辑 -> 通过Wi-Fi将重要状态或报警信息发送至钉钉群 -> 用户通过手机钉钉接收通知。系统架构清晰,每一层都可以独立优化和替换。

2. 硬件连接与电路搭建要点

正确的硬件连接是项目成功的基础。下面我将给出详细的接线图说明和实操中容易踩坑的注意事项。

首先,确保你手头有这些材料:ESP32开发板(如NodeMCU-32S)、DHT22模块、BMP180模块、电容式土壤湿度传感器、1路5V继电器模块、微型水泵、水管、一个盛水容器(如小桶),以及杜邦线若干。

接线示意图与对应引脚定义:

ESP32 GPIO     ->     外围设备
================== ======================
3.3V           ->     DHT22 VCC, BMP180 VCC, 土壤传感器 VCC
GND            ->     DHT22 GND, BMP180 GND, 土壤传感器 GND, 继电器模块 GND
GPIO 4 (D4)    ->     DHT22 DATA
GPIO 21 (SDA)  ->     BMP180 SDA
GPIO 22 (SCL)  ->     BMP180 SCL
GPIO 32        ->     土壤湿度传感器 AO (模拟输出)
GPIO 25        ->     继电器模块 IN (控制引脚)
5V/VIN         ->     继电器模块 VCC (注意:部分继电器模块需5V驱动)
继电器 COM/NO   ->     水泵正极
水泵负极        ->     GND (电源地)

注意:继电器的控制端(IN)一定要连接到ESP32的GPIO口,而继电器的负载端(COM, NO)则用来控制水泵的电源。请务必确认你的水泵工作电压(常见为5V或12V),并为其提供独立的、功率足够的电源(切勿直接从ESP32的USB口取电驱动水泵),否则可能烧毁USB接口或开发板。可以使用一个独立的5V/2A或12V/1A的电源适配器为水泵供电。

搭建过程中的几个关键技巧:

  1. 电源隔离:为MCU(ESP32)和执行器(水泵)使用独立的电源。可以在继电器模块的VCC和JD-VCC之间使用跳线帽断开,然后分别供电,这样可以避免水泵电机启停时产生的电流冲击干扰ESP32的稳定运行。
  2. 土壤传感器校准:电容式传感器虽然稳定,但出厂值仍需校准。准备一杯清水和一杯干燥的土壤,分别将传感器插入,通过串口监视器读取其模拟值(0-4095)。记录下这两个极值,后续在代码中会用到它们来映射百分比。
  3. 防潮处理:虽然DHT22和BMP180不像电阻式土壤传感器那样怕腐蚀,但长期处于高湿环境也可能影响寿命。可以考虑用热熔胶或硅胶对传感器引脚处做简单的防水密封。

硬件连接完成后,建议先不接水泵,用万用表测试继电器在ESP32控制下的吸合与断开是否正常,确认无误后再接入水泵进行整体测试。

3. 软件环境配置与核心代码解析

我们使用Arduino IDE进行开发。首先需要安装必要的库文件。打开Arduino IDE,依次点击“工具” -> “管理库”,搜索并安装以下库:

  • DHT sensor library (由Adafruit维护)
  • Adafruit BMP085 Library (BMP180兼容此库)
  • Adafruit Unified Sensor (DHT和BMP库的依赖库)

接下来是代码的核心部分。我们将程序逻辑分为几个模块:传感器数据读取、浇水逻辑判断、网络连接与钉钉消息推送。为了保持代码清晰和可维护性,我采用了面向对象的思想进行封装。

首先,定义关键的配置参数和全局对象:

// 网络配置
const char* ssid = "你的Wi-Fi名称";
const char* password = "你的Wi-Fi密码";

// 钉钉机器人Webhook地址 (需要在钉钉群中添加自定义机器人获取)
const char* dingtalk_webhook = "https://oapi.dingtalk.com/robot/send?access_token=你的token";

// 传感器引脚定义
#define DHTPIN 4
#define DHTTYPE DHT22
#define SOIL_MOISTURE_PIN 32 // 土壤湿度模拟引脚
#define RELAY_PIN 25 // 继电器控制引脚

// 浇水阈值 (需要根据实际校准调整)
const int DRY_THRESHOLD = 30; // 土壤湿度低于30%时触发浇水
const int WET_THRESHOLD = 70; // 土壤湿度高于70%时停止浇水

// 传感器对象初始化
DHT dht(DHTPIN, DHTTYPE);
Adafruit_BMP085 bmp;

// 全局变量存储传感器数据
float airTemp, airHumidity, pressure, altitude;
int soilMoistureRaw;
int soilMoisturePercent;

传感器数据读取函数:

这个函数负责从所有传感器获取数据,并进行必要的处理和校准。

void readAllSensors() {
  // 读取DHT22
  airHumidity = dht.readHumidity();
  airTemp = dht.readTemperature(); // 读取摄氏温度
  
  // 读取BMP180
  if (bmp.begin()) {
    pressure = bmp.readPressure() / 100.0F; // 转换为百帕(hPa)
    // BMP180也提供温度,可作为参考或备份
    // float bmpTemp = bmp.readTemperature();
  }
  
  // 读取土壤湿度并转换为百分比
  soilMoistureRaw = analogRead(SOIL_MOISTURE_PIN);
  // 假设校准后,空气中读数为RAW_AIR=1500,水中读数为RAW_WATER=2800
  const int RAW_AIR = 1500;
  const int RAW_WATER = 2800;
  // 将模拟值映射到0-100%,并限制范围
  soilMoisturePercent = map(soilMoistureRaw, RAW_AIR, RAW_WATER, 0, 100);
  soilMoisturePercent = constrain(soilMoisturePercent, 0, 100);
  
  // 简单的数据有效性检查
  if (isnan(airHumidity) || isnan(airTemp)) {
    Serial.println("读取DHT22失败!");
    // 可以在这里加入错误处理,比如使用BMP180的温度作为备用
  }
}

智能浇水控制逻辑:

浇水不是简单的“低于阈值就开,高于阈值就关”。我们需要加入一些“智能”判断,比如防止短时间内频繁启停水泵(称为“抖动”),以及考虑环境温湿度对浇水需求的影响。

bool shouldWaterNow() {
  // 条件1: 土壤干燥是首要条件
  if (soilMoisturePercent > DRY_THRESHOLD) {
    return false;
  }
  
  // 条件2: 环境辅助判断。例如,如果空气湿度已经很高,也许可以稍晚再浇
  // 这里可以引入一个“蒸发需求指数”,简单实现如下:
  float evaporationFactor = (airTemp / 30.0) * (1.0 - (airHumidity / 100.0));
  // 蒸发因子越大,表示水分蒸发越快,浇水需求越迫切
  // 可以设置一个蒸发因子阈值,只有同时满足土壤干燥且蒸发需求高时才立即浇水
  // 本例中我们先实现基础逻辑
  
  // 条件3: 时间间隔保护。记录上次浇水时间,确保至少间隔一段时间(如30分钟)
  static unsigned long lastWateringTime = 0;
  unsigned long currentTime = millis();
  if (currentTime - lastWateringTime < 30 * 60 * 1000UL) { // 30分钟
    return false;
  }
  
  // 所有条件通过,可以浇水
  lastWateringTime = currentTime;
  return true;
}

void controlWaterPump(bool turnOn) {
  digitalWrite(RELAY_PIN, turnOn ? HIGH : LOW); // 根据继电器模块逻辑调整HIGH/LOW
  if (turnOn) {
    Serial.println("水泵开启");
    // 可以在这里记录浇水开始时间、时长等
  } else {
    Serial.println("水泵关闭");
  }
}

钉钉机器人消息推送函数:

这是实现远程报警的关键。我们使用HTTP POST请求向钉钉机器人的Webhook地址发送JSON格式的消息。

void sendDingTalkAlert(String message, bool isAtAll = false) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Wi-Fi未连接,无法发送钉钉消息");
    return;
  }
  
  HTTPClient http;
  http.begin(dingtalk_webhook);
  http.addHeader("Content-Type", "application/json;charset=utf-8");
  
  // 构建JSON消息体
  String jsonPayload = "{";
  jsonPayload += "\"msgtype\":\"text\",";
  jsonPayload += "\"text\":{\"content\":\"【智能浇花系统报警】\\n";
  jsonPayload += message + "\"},";
  jsonPayload += "\"at\":{\"isAtAll\":" + String(isAtAll ? "true" : "false") + "}";
  jsonPayload += "}";
  
  int httpResponseCode = http.POST(jsonPayload);
  if (httpResponseCode > 0) {
    String response = http.getString();
    Serial.println("钉钉消息发送成功,响应: " + response);
  } else {
    Serial.println("钉钉消息发送失败,错误代码: " + String(httpResponseCode));
  }
  http.end();
}

在主循环loop()中,我们将上述模块组合起来:

void loop() {
  readAllSensors();
  
  // 在串口打印数据,用于调试
  Serial.printf("土壤湿度: %d%% | 空气温度: %.1fC | 空气湿度: %.1f%% | 气压: %.1fhPa\n", 
                soilMoisturePercent, airTemp, airHumidity, pressure);
  
  // 智能浇水判断与执行
  if (shouldWaterNow()) {
    controlWaterPump(true);
    delay(5000); // 浇水5秒,具体时长根据水泵流量和花盆大小调整
    controlWaterPump(false);
    
    // 浇水后发送通知
    String alertMsg = "已自动为植物浇水。浇水前土壤湿度:" + String(soilMoisturePercent) + "%";
    sendDingTalkAlert(alertMsg);
  }
  
  // 异常状态报警
  if (soilMoisturePercent < 20) {
    sendDingTalkAlert("警告:土壤极度干燥(" + String(soilMoisturePercent) + "%),请及时检查!", true);
  }
  if (airTemp > 35) {
    sendDingTalkAlert("警告:环境温度过高(" + String(airTemp) + "C),请注意植物防晒!");
  }
  
  // 每30分钟发送一次状态报告
  static unsigned long lastReportTime = 0;
  if (millis() - lastReportTime > 30 * 60 * 1000UL) {
    String reportMsg = "系统状态报告\\n";
    reportMsg += "土壤湿度: " + String(soilMoisturePercent) + "%\\n";
    reportMsg += "空气温度: " + String(airTemp) + "C\\n";
    reportMsg += "空气湿度: " + String(airHumidity) + "%\\n";
    reportMsg += "气压: " + String(pressure) + "hPa";
    sendDingTalkAlert(reportMsg);
    lastReportTime = millis();
  }
  
  delay(60000); // 主循环每分钟运行一次
}

4. 钉钉机器人配置与消息推送实战

让系统“说话”是提升体验的关键一步。钉钉群机器人的配置非常简单,但有几个细节需要注意。

第一步:创建钉钉机器人

  1. 打开任意一个钉钉群,点击右上角的群设置。
  2. 选择“智能群助手” -> “添加机器人” -> “自定义机器人”。
  3. 给机器人起个名字,比如“阳台园丁”,并选择需要发送消息的群。
  4. 在安全设置中,我建议选择“加签”方式,比单纯使用IP白名单更安全。系统会生成一个secret,请妥善保存。
  5. 创建完成后,你会得到一个Webhook地址,格式类似 https://oapi.dingtalk.com/robot/send?access_token=XXXXXX。这个地址就是代码中dingtalk_webhook变量的值。

第二步:在代码中集成加签逻辑 如果你在安全设置中选择了“加签”,那么每次发送请求时都需要生成一个时间戳和签名。需要在sendDingTalkAlert函数中做如下修改:

#include <HMAC_SHA256.h> // 需要安装此库

String generateSign(long timestamp, String secret) {
  String stringToSign = String(timestamp) + "\n" + secret;
  HMAC_SHA256 hmac;
  byte hash[32];
  hmac.doHMAC((byte*)stringToSign.c_str(), stringToSign.length(), (byte*)secret.c_str(), secret.length(), hash);
  
  // 将二进制哈希转换为URL编码的Base64字符串
  String sign = base64::encode(hash, 32); // 需要base64库
  sign.replace("+", "-");
  sign.replace("/", "_");
  return sign;
}

void sendDingTalkAlert(String message, bool isAtAll = false) {
  // ... [之前的Wi-Fi检查等代码]
  
  long timestamp = millis(); // 实际应用中应使用从网络获取的精确时间
  String secret = "你的加签密钥";
  String sign = generateSign(timestamp, secret);
  
  // 将时间戳和签名附加到Webhook URL
  String fullWebhook = String(dingtalk_webhook) + "&timestamp=" + String(timestamp) + "&sign=" + sign;
  
  HTTPClient http;
  http.begin(fullWebhook); // 使用完整的带签名的URL
  // ... [后续的POST请求代码不变]
}

第三步:设计更丰富的消息格式 钉钉机器人支持textlinkmarkdown等多种消息类型。对于状态报告,使用markdown格式可以让信息更清晰:

void sendMarkdownReport() {
  String jsonPayload = "{";
  jsonPayload += "\"msgtype\":\"markdown\",";
  jsonPayload += "\"markdown\":{\"title\":\"植物养护日报\",\"text\":\"";
  jsonPayload += "### 🌱 智能浇花系统状态报告  \\n";
  jsonPayload += "---  \\n";
  jsonPayload += "- **土壤湿度**: " + String(soilMoisturePercent) + "%  \\n";
  jsonPayload += "- **空气温度**: " + String(airTemp) + "°C  \\n";
  jsonPayload += "- **空气湿度**: " + String(airHumidity) + "%  \\n";
  jsonPayload += "- **大气压力**: " + String(pressure) + " hPa  \\n";
  jsonPayload += "---  \\n";
  jsonPayload += "> 数据采集于 " + getFormattedTime() + "  \\n";
  jsonPayload += "> 系统运行正常。\"},";
  jsonPayload += "\"at\":{\"isAtAll\":false}";
  jsonPayload += "}";
  // ... [发送HTTP请求]
}

通过这样的格式,每天定时推送到群里的报告就非常直观了。你还可以根据气压变化趋势,在消息中加入简单的天气预测提示,比如“气压持续下降,未来半天内可能有雨,建议暂缓浇水”。

5. 系统优化与高级功能拓展

基础功能实现后,我们可以从稳定性、功耗、智能化三个维度对系统进行深度优化。

1. 引入深度睡眠,大幅降低功耗 如果系统采用电池供电,功耗就是核心问题。ESP32的深度睡眠模式可以将电流降至10μA左右。

#define uS_TO_S_FACTOR 1000000ULL  // 微秒到秒的转换因子
#define TIME_TO_SLEEP  300          // 深度睡眠时间(秒)

void setup() {
  // ... [初始化代码]
  
  // 配置唤醒源(这里使用定时器唤醒)
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  
  Serial.println("准备进入深度睡眠,将于" + String(TIME_TO_SLEEP) + "秒后唤醒");
  delay(100);
  esp_deep_sleep_start(); // 进入深度睡眠,程序停止运行
  // 之后的所有代码都不会执行,直到被唤醒
}

void loop() {
  // 注意:一旦在setup中调用了深度睡眠,loop()将永远不会运行
  // 所有工作应在唤醒后的setup()中完成
}

唤醒后,ESP32会重新从setup()开始执行。你需要将主要的传感器读取、逻辑判断、浇水动作都放在setup()里,并在最后再次进入深度睡眠。同时,所有需要保持的数据(如上次浇水时间)必须保存在RTC内存或EEPROM中,因为深度睡眠会丢失RAM数据。

RTC_DATA_ATTR int bootCount = 0; // 保存在RTC内存,深度睡眠后不丢失

void setup() {
  bootCount++;
  Serial.println("启动次数: " + String(bootCount));
  
  // 从EEPROM读取上次浇水时间等状态
  // 执行传感器读取和浇水逻辑
  // ...
  
  // 将最新状态保存回EEPROM
  // ...
  
  // 再次进入睡眠
  esp_deep_sleep_start();
}

2. 实现OTA(空中升级)功能 当系统部署在阳台等不便插线的地方时,OTA功能至关重要。Arduino IDE配合ESP32的库可以很方便地实现。

#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

void setupOTA() {
  ArduinoOTA.setHostname("esp32-smart-watering"); // 设置主机名
  ArduinoOTA.setPassword("admin123"); // 设置OTA密码
  
  ArduinoOTA.onStart([]() {
    String type;
    if (ArduinoOTA.getCommand() == U_FLASH) {
      type = "sketch";
    } else { // U_SPIFFS
      type = "filesystem";
    }
    Serial.println("开始OTA更新: " + type);
  });
  
  ArduinoOTA.onEnd([]() {
    Serial.println("\nOTA更新完成");
  });
  
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
    Serial.printf("更新进度: %u%%\r", (progress / (total / 100)));
  });
  
  ArduinoOTA.onError([](ota_error_t error) {
    Serial.printf("错误[%u]: ", error);
    // ... 错误处理
  });
  
  ArduinoOTA.begin();
  Serial.println("OTA就绪");
}

void loop() {
  ArduinoOTA.handle(); // 必须经常调用以处理OTA请求
  // ... 其他循环任务
}

配置好后,在Arduino IDE的“工具” -> “端口”菜单中,你会看到一个网络端口,选择它就可以像串口一样上传程序了。

3. 数据持久化与历史趋势分析 在EEPROM或SPIFFS文件系统中存储历史数据,可以让你分析植物的生长环境变化。

// 定义一个结构体来存储每天的数据快照
struct DailyRecord {
  uint32_t timestamp;
  int avgSoilMoisture;
  float maxTemp;
  float minTemp;
  int wateringCount;
};

void saveDailyRecord() {
  DailyRecord today;
  today.timestamp = getCurrentEpochTime(); // 获取当前时间戳
  today.avgSoilMoisture = calculateAvgMoisture();
  // ... 计算其他指标
  
  // 写入EEPROM (假设从地址0开始)
  EEPROM.put(0, today);
  EEPROM.commit();
}

void generateWeeklyReport() {
  DailyRecord week[7];
  for (int i = 0; i < 7; i++) {
    EEPROM.get(i * sizeof(DailyRecord), week[i]);
  }
  
  // 分析数据,生成报告字符串
  String report = "**过去一周养护报告**\\n";
  report += "平均浇水频率: " + String(calculateAvgWatering(week)) + "次/天\\n";
  report += "土壤湿度趋势: " + analyzeTrend(week, &DailyRecord::avgSoilMoisture) + "\\n";
  // ... 更多分析
  
  sendDingTalkAlert(report);
}

通过这些优化,你的智能浇花系统就从“能用”升级到了“好用且可靠”的级别。它可以在无人值守的情况下稳定运行数月,并在需要维护或升级时,远程即可完成,真正实现了智能化的植物养护。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐