植物总是浇水过多或过少?不如自己动手做一个自动灌溉装置,成本不到50元还能用手机远程控制!

别让你的绿色小可爱”渴死”了

你有没有过这样的经历?回老家或者出差几天,回来发现花盆里的土干得像砖头,植物蔫巴巴地垂着脑袋;又或者刚浇完水,第二天就后悔”是不是浇多了”。作为一个植物爱好者,这个问题我踩过无数次坑。

直到我发现了自动灌溉这个神器,不仅解决了浇水难题,还让我能远程控制家里的植物。最关键的是——整套成本不到50元。今天就把这个DIY方案完整分享给你,阳台党、小菜园新手都能照着做。

我们需要准备些什么?

工具很简单,不用什么专业设备,家里能找到的就行。

材料清单

材料名称 数量 参考价格 说明
ESP32开发板 1块 ¥15-20 带Wi-Fi和蓝牙,比Arduino强多了
土壤湿度传感器 1个 ¥3-5 电阻式,检测土壤干湿度
小型水泵 1个 ¥5-8 5V USB接口,抽水管就行
4通道继电器模块 1个 ¥5-8 用来控制水泵开关
硅胶水管 1米 ¥3-5 连接水泵和花盆
储水容器 1个 0元 随便找个瓶子、水桶都行
杜邦线 若干 ¥2 连接电路用

总计:约¥33-51元,还没算你可能已经有的工具和容器。

工具准备

  • 电烙铁(可选,如果用杜邦线直接插就不用)
  • 热熔胶枪(固定元件用)
  • 剪刀
  • 电脑(用来烧录程序)

硬件连接,手把手教你

来,跟着这个接线图一步步来,别慌,其实很简单。

ESP32引脚定义

ESP32是这颗系统的”大脑”,它负责读取湿度传感器数据、连接Wi-Fi、接收手机指令。

ESP32引脚分配:

土壤湿度传感器:
  VCC  → 3.3V (ESP32的3.3伏电源脚)
  GND  → GND (接地)
  AO   → GPIO36 (ADC1_CH0,模拟信号输入)

继电器模块:
  VCC  → 5V (ESP32的5伏电源脚)
  GND  → GND
  IN   → GPIO26 (控制继电器开关)

水泵:
  正极 → 继电器常开触点
  负极 → 电源负极(与ESP32共地)

实际接线图

想象一下,把所有元件摆在桌上:

    ┌─────────────────────────────────────────────────┐
    │                 ESP32 开发板                      │
    │                                                  │
    │  GPIO36 ────┬─── AO (土壤湿度传感器)             │
    │  3.3V    ───┼─── VCC                           │
    │  GND     ───┼─── GND                           │
    │                                                  │
    │  GPIO26 ────┴─── IN (继电器模块)                 │
    │  5V      ────┬─── VCC                           │
    │  GND     ────┴─── GND                           │
    └─────────────────────────────────────────────────┘
                      │
                      ▼
            ┌─────────────────┐
            │   继电器模块     │
            │                 │
            │  COM ────────┬──水泵+
            │  NO  ────────┘
            │  GND ────────┘水泵-
            └─────────────────┘
                      │
                      ▼
            ┌─────────────────┐
            │  土壤湿度传感器   │
            │  插入土壤 deepest │
            └─────────────────┘

程序部分,直接能用

我用的是 Arduino IDE 来编写 ESP32 的程序。先安装好 ESP32 的板卡支持包(在 Arduino IDE 的”开发板管理器”里搜索 ESP32 安装即可)。

完整代码

/**
 * 智能自动灌溉系统
 * 功能:土壤湿度检测 + 自动浇水 + 手机远程控制
 * 作者:Agnes
 * 平台:ESP32 + Arduino IDE
 */

#include <WiFi.h>
#include <WebServer.h>
#include <EEPROM.h>

// ==================== WiFi 配置 ====================
// 把下面的用户名和密码换成你家路由器的
const char* ssid = "你的WiFi名称";
const char* password = "你的WiFi密码";

// ==================== 引脚定义 ====================
const int soilSensorPin = 36;   // 土壤湿度传感器模拟输入引脚
const int pumpPin = 26;         // 继电器控制引脚
const int thresholdPin = 34;    // 校准用(可选)

// ==================== 参数配置 ====================
int moistureThreshold = 4000;   // 湿度阈值,低于此值开始浇水
int pumpDuration = 5000;        // 每次浇水时长(毫秒),5秒
int checkInterval = 30000;      // 检查间隔(毫秒),30秒
bool autoMode = true;           // 自动模式开关
bool manualMode = false;        // 手动模式开关

// ==================== 系统变量 ====================
unsigned long lastCheckTime = 0;
WebServer server(80);

// ==================== 初始化函数 ====================
void setup() {
    Serial.begin(115200);
    
    // 初始化引脚
    pinMode(pumpPin, OUTPUT);
    digitalWrite(pumpPin, HIGH);  // 继电器默认关闭(低电平触发)
    
    // 初始化EEPROM(保存设置)
    EEPROM.begin(512);
    loadSettings();
    
    // 连接WiFi
    connectWiFi();
    
    // 设置Web服务器路由
    setupWebServer();
    
    Serial.println("========================================");
    Serial.println("   智能自动灌溉系统启动成功!");
    Serial.print("   设备IP: ");
    Serial.println(WiFi.localIP());
    Serial.println("========================================");
}

// ==================== WiFi连接 ====================
void connectWiFi() {
    Serial.printf("正在连接 WiFi: %s\n", ssid);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        Serial.print(".");
        attempts++;
    }
    
    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("\nWiFi连接成功!");
    } else {
        Serial.println("\nWiFi连接失败,请检查密码");
    }
}

// ==================== Web服务器设置 ====================
void setupWebServer() {
    // 主页面 - 控制面板
    server.on("/", handleRoot);
    
    // 控制水泵
    server.on("/pump/on", handlePumpOn);
    server.on("/pump/off", handlePumpOff);
    
    // 切换自动/手动模式
    server.on("/mode/auto", handleAutoMode);
    server.on("/mode/manual", handleManualMode);
    
    // 设置阈值
    server.on("/threshold/set", handleSetThreshold);
    
    // 获取当前状态(API)
    server.on("/api/status", handleStatusAPI);
    
    // 404处理
    server.onNotFound(handleNotFound);
    
    server.begin();
    Serial.println("HTTP服务器已启动");
}

// ==================== 主页面 ====================
void handleRoot() {
    int moisture = getMoisture();
    String ip = WiFi.localIP().toString();
    
    String html = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>智能灌溉控制面板</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { 
            font-family: 'Segoe UI', sans-serif; 
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            min-height: 100vh;
            color: #fff;
            padding: 20px;
        }
        .container { max-width: 500px; margin: 0 auto; }
        h1 { 
            text-align: center; 
            margin-bottom: 10px;
            font-size: 24px;
            color: #4ade80;
        }
        .subtitle {
            text-align: center;
            color: #94a3b8;
            font-size: 12px;
            margin-bottom: 25px;
        }
        .card {
            background: rgba(255,255,255,0.05);
            border-radius: 16px;
            padding: 20px;
            margin-bottom: 15px;
            border: 1px solid rgba(255,255,255,0.1);
        }
        .card-title {
            font-size: 14px;
            color: #94a3b8;
            margin-bottom: 10px;
        }
        .moisture-display {
            text-align: center;
            padding: 20px 0;
        }
        .moisture-value {
            font-size: 72px;
            font-weight: bold;
            color: #4ade80;
        }
        .moisture-unit {
            font-size: 18px;
            color: #64748b;
        }
        .moisture-bar {
            width: 100%;
            height: 8px;
            background: #334155;
            border-radius: 4px;
            margin-top: 15px;
            overflow: hidden;
        }
        .moisture-fill {
            height: 100%;
            background: linear-gradient(90deg, #ef4444, #f59e0b, #4ade80);
            transition: width 0.5s ease;
        }
        .status-badge {
            display: inline-block;
            padding: 6px 16px;
            border-radius: 20px;
            font-size: 14px;
            font-weight: 600;
        }
        .status-auto { background: #4ade80; color: #064e3b; }
        .status-manual { background: #f59e0b; color: #451a03; }
        .btn-group {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 10px;
            margin-top: 15px;
        }
        .btn {
            padding: 15px;
            border: none;
            border-radius: 12px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
        }
        .btn:active { transform: scale(0.95); }
        .btn-pump-on { 
            background: linear-gradient(135deg, #22c55e, #16a34a); 
            color: white;
        }
        .btn-pump-off { 
            background: linear-gradient(135deg, #ef4444, #dc2626); 
            color: white;
        }
        .btn-auto { 
            background: linear-gradient(135deg, #3b82f6, #2563eb); 
            color: white;
        }
        .btn-manual { 
            background: linear-gradient(135deg, #f59e0b, #d97706); 
            color: white;
        }
        .threshold-control {
            margin-top: 15px;
        }
        .threshold-control label {
            display: block;
            color: #94a3b8;
            font-size: 14px;
            margin-bottom: 8px;
        }
        input[type="range"] {
            width: 100%;
            accent-color: #4ade80;
        }
        .threshold-value {
            text-align: center;
            color: #4ade80;
            font-size: 18px;
            margin-top: 5px;
        }
        .info-row {
            display: flex;
            justify-content: space-between;
            padding: 8px 0;
            border-bottom: 1px solid rgba(255,255,255,0.1);
            font-size: 14px;
        }
        .info-row:last-child { border-bottom: none; }
        .info-label { color: #94a3b8; }
        .info-value { color: #e2e8f0; }
        .refresh-btn {
            display: block;
            margin: 20px auto 0;
            padding: 12px 30px;
            background: linear-gradient(135deg, #8b5cf6, #7c3aed);
            color: white;
            border: none;
            border-radius: 10px;
            font-size: 16px;
            cursor: pointer;
        }
        .water-icon {
            font-size: 40px;
            text-align: center;
            margin-bottom: 10px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🌱 智能自动灌溉系统</h1>
        <p class="subtitle">IP: <span id="ipAddr">%s</span> | 最后更新: <span id="lastUpdate">--</span></p>
        
        <div class="card">
            <div class="card-title">土壤湿度</div>
            <div class="moisture-display">
                <div class="water-icon" id="waterIcon">💧</div>
                <span class="moisture-value" id="moistureValue">%d</span>
                <span class="moisture-unit"> / 65535</span>
            </div>
            <div class="moisture-bar">
                <div class="moisture-fill" id="moistureFill" style="width: %d%%"></div>
            </div>
        </div>
        
        <div class="card">
            <div class="card-title">系统状态</div>
            <div class="info-row">
                <span class="info-label">运行模式</span>
                <span class="info-value"><span class="status-badge %s">%s</span></span>
            </div>
            <div class="info-row">
                <span class="info-label">水泵状态</span>
                <span class="info-value" id="pumpStatus">%s</span>
            </div>
            <div class="info-row">
                <span class="info-label">阈值设置</span>
                <span class="info-value">%d</span>
            </div>
            <div class="info-row">
                <span class="info-label">浇水时长</span>
                <span class="info-value">%d 秒</span>
            </div>
            <div class="info-row">
                <span class="info-label">检查间隔</span>
                <span class="info-value">%d 秒</span>
            </div>
        </div>
        
        <div class="card">
            <div class="card-title">手动控制</div>
            <div class="btn-group">
                <a href="/pump/on"><button class="btn btn-pump-on">💧 开始浇水</button></a>
                <a href="/pump/off"><button class="btn btn-pump-off">⏹ 停止浇水</button></a>
            </div>
            <div class="btn-group">
                <a href="/mode/auto"><button class="btn btn-auto">🤖 自动模式</button></a>
                <a href="/mode/manual"><button class="btn btn-manual">✋ 手动模式</button></a>
            </div>
        </div>
        
        <div class="card">
            <div class="card-title">阈值调节</div>
            <div class="threshold-control">
                <label>湿度阈值(低于此值自动浇水)</label>
                <input type="range" min="500" max="6000" step="100" 
                       value="%d" oninput="setThreshold(this.value)">
                <div class="threshold-value" id="thresholdDisplay">%d</div>
            </div>
        </div>
        
        <button class="refresh-btn" onclick="refreshStatus()">🔄 刷新状态</button>
    </div>
    
    <script>
        function refreshStatus() {
            fetch('/api/status')
                .then(r => r.json())
                .then(data => {
                    document.getElementById('moistureValue').textContent = data.moisture;
                    document.getElementById('moistureFill').style.width = 
                        Math.round(data.moisture / 655.35) + '%';
                    document.getElementById('pumpStatus').textContent = 
                        data.pumpState ? '💧 运行中' : '⏹ 已停止';
                    document.getElementById('lastUpdate').textContent = 
                        new Date().toLocaleTimeString();
                });
        }
        function setThreshold(val) {
            document.getElementById('thresholdDisplay').textContent = val;
            fetch('/threshold/set?val=' + val);
        }
        // 每10秒自动刷新
        setInterval(refreshStatus, 10000);
        // 页面加载时立即刷新
        window.onload = refreshStatus;
    </script>
</body>
</html>
)rawliteral";
    
    int moisture = getMoisture();
    int percentage = map(moisture, 0, 65535, 100, 0);
    String modeBadge = autoMode ? "status-auto" : "status-manual";
    String modeText = autoMode ? "🤖 自动模式" : "✋ 手动模式";
    String pumpText = digitalRead(pumpPin) == LOW ? "💧 运行中" : "⏹ 已停止";
    
    html.replace("%s", ip.c_str());
    html.replace("%d", String(moisture).c_str());
    html.replace("%d", String(percentage).c_str());
    html.replace("%s", modeBadge.c_str());
    html.replace("%s", modeText.c_str());
    html.replace("%s", pumpText.c_str());
    html.replace("%d", moistureThreshold);
    html.replace("%d", pumpDuration / 1000);
    html.replace("%d", checkInterval / 1000);
    html.replace("%d", moistureThreshold);
    html.replace("%d", moistureThreshold);
    
    server.send(200, "text/html", html);
}

// ==================== 控制路由 ====================
void handlePumpOn() {
    digitalWrite(pumpPin, LOW);  // 低电平触发,打开水泵
    Serial.println("水泵开启(手动)");
    server.sendHeader("Location", "/");
    server.send(303);
}

void handlePumpOff() {
    digitalWrite(pumpPin, HIGH); // 高电平,关闭水泵
    Serial.println("水泵关闭");
    server.sendHeader("Location", "/");
    server.send(303);
}

void handleAutoMode() {
    autoMode = true;
    manualMode = false;
    saveSettings();
    Serial.println("切换到自动模式");
    server.sendHeader("Location", "/");
    server.send(303);
}

void handleManualMode() {
    autoMode = false;
    manualMode = true;
    saveSettings();
    Serial.println("切换到手动模式");
    server.sendHeader("Location", "/");
    server.send(303);
}

void handleSetThreshold() {
    String val = server.arg("val");
    moistureThreshold = val.toInt();
    saveSettings();
    Serial.printf("阈值已更新为: %d\n", moistureThreshold);
    server.sendHeader("Location", "/");
    server.send(303);
}

void handleStatusAPI() {
    int moisture = getMoisture();
    String json = "{\"moisture\":" + String(moisture) + 
                  ",\"threshold\":" + String(moistureThreshold) +
                  ",\"autoMode\":" + String(autoMode ? "true" : "false") +
                  ",\"pumpState\":" + String(digitalRead(pumpPin) == LOW ? "true" : "false") +
                  "}";
    server.send(200, "application/json", json);
}

void handleNotFound() {
    server.send(404, "text/plain", "404 - 页面未找到\n");
}

// ==================== 核心功能函数 ====================
int getMoisture() {
    return analogRead(soilSensorPin);
}

void pumpOn() {
    digitalWrite(pumpPin, LOW);
    Serial.println("自动浇水启动");
}

void pumpOff() {
    digitalWrite(pumpPin, HIGH);
    Serial.println("浇水结束");
}

// ==================== 设置保存与读取 ====================
void saveSettings() {
    EEPROM.put(0, moistureThreshold);
    EEPROM.put(4, pumpDuration);
    EEPROM.put(8, checkInterval);
    EEPROM.put(12, autoMode);
    EEPROM.commit();
}

void loadSettings() {
    EEPROM.get(0, moistureThreshold);
    EEPROM.get(4, pumpDuration);
    EEPROM.get(8, checkInterval);
    EEPROM.get(12, autoMode);
    
    // 边界检查
    if (moistureThreshold < 500 || moistureThreshold > 6000) moistureThreshold = 4000;
    if (pumpDuration < 1000 || pumpDuration > 30000) pumpDuration = 5000;
    if (checkInterval < 5000 || checkInterval > 300000) checkInterval = 30000;
}

// ==================== 主循环 ====================
void loop() {
    server.handleClient();
    
    unsigned long currentMillis = millis();
    
    if (autoMode && (currentMillis - lastCheckTime >= checkInterval)) {
        lastCheckTime = currentMillis;
        
        int moisture = getMoisture();
        Serial.printf("湿度检测: %d (阈值: %d)\n", moisture, moistureThreshold);
        
        if (moisture < moistureThreshold) {
            pumpOn();
            delay(pumpDuration);
            pumpOff();
            Serial.println("自动浇水完成");
        }
    }
    
    delay(100);
}

把代码烧录进去

  1. 打开 Arduino IDE
  2. 安装 ESP32 开发板支持:菜单 → 工具 → 开发板管理器 → 搜索 “ESP32” → 安装
  3. 插入 ESP32 到电脑,选择对应的端口
  4. 把上面代码里的 ssidpassword 换成你家 WiFi 的信息
  5. 点击上传

上传成功后,打开串口监视器(右上角放大镜图标),你会看到:

正在连接 WiFi: 你的WiFi名称
..........
WiFi连接成功!
HTTP服务器已启动
========================================
   智能自动灌溉系统启动成功!
   设备IP: 192.168.1.100
========================================

记下这个 IP 地址。

手机上的控制体验

打开你的手机浏览器,输入那个 IP 地址,就能看到控制面板了:

手机浏览器地址栏输入: http://192.168.1.100

界面是这样的:

  • 大数字显示当前土壤湿度值,还有一个进度条直观展示干湿程度
  • 一键开关水泵,不用手机App,浏览器就能用
  • 自动/手动模式切换,出门前开自动,回来手动浇
  • 湿度阈值滑块,拉一拉就能调整自动浇水的敏感度
  • 每10秒自动刷新数据,不用手动点刷新

几个实用的小技巧

校准湿度传感器

不同土壤的电阻值不一样,建议你先测试一下:

把传感器插进干燥的土里 → 记下数值A
把传感器插进浇透的土里 → 记下数值B
阈值设在 A 和 B 的中间值左右

一般来说,干燥土壤的读数在 5000-6000 左右,湿润土壤在 2000-3000 左右。

水泵的安装

储水容器(放在高处,利用重力辅助)
      │
      │ 硅胶水管
      ▼
  水泵入口(注意进水口朝上)
      │
      │ 出水管
      ▼
   花盆土壤

把水泵放进储水容器,水管从容器边缘引到花盆,记得让容器比花盆高一些,这样水压更稳定。

多盆植物的扩展

如果你的阳台不止一盆花,可以在同一个 ESP32 上接多个湿度传感器:

// 扩展:双盆植物监控
const int sensor1Pin = 36;  // 花盆1
const int sensor2Pin = 39;  // 花盆2
const int pump1Pin = 26;    // 水泵1
const int pump2Pin = 27;    // 水泵2

只需要加一个双路继电器模块(大约多花 ¥5),就能同时监控两盆植物。

加水提醒功能

如果你想更智能,可以加个”水位低”提醒:

// 在代码里添加水位检测
const int waterLevelPin = 35;  // 超声波传感器或简单浮球开关

void checkWaterLevel() {
    // 如果水位太低,在网页上显示提醒
    if (isWaterLow()) {
        Serial.println("警告:储水箱水位过低!");
        // 可以加一个LED指示灯或者蜂鸣器
    }
}

遇到问题怎么办

Q: 手机连不上设备? A: 确认手机和 ESP32 在同一个 WiFi 下,检查路由器是否有”AP隔离”功能(有的路由器会阻止设备间通信),如果有就关掉它。

Q: 湿度读数不稳定? A: 传感器容易受温度影响,可以在代码里加简单的滤波:

int getMoistureSmoothed() {
    int sum = 0;
    for (int i = 0; i < 5; i++) {
        sum += analogRead(soilSensorPin);
        delay(50);
    }
    return sum / 5;  // 取5次平均值
}

Q: 浇水时间太短/太长? A: 修改代码里的 pumpDuration 变量,单位是毫秒。5000 = 5秒,10000 = 10秒,根据花盆大小调整。

Q: 想远程访问,不在家也能控制? A: 这个进阶玩法可以用内网穿透(如 ngrok、FRP)或者用现成的物联网平台(如 OneNET、Blinker),成本会略高一些,但体验更好。

最后说几句

做这个项目的过程其实特别有意思。我第一次把代码烧进去,看到手机浏览器里出现那个控制界面的时候,真的有点小激动——50块钱不到,就能让植物自己喝水,还能随时远程控制

如果你家里有老人或者经常出差的朋友,这个项目特别适合他们。把阈值调好,设为自动模式,出门半个月都不用担心植物渴死。

最重要的是,做完之后你会对”传感器”“物联网”“WiFi控制”这些概念有最直观的理解。以后想加更多功能——比如加个温湿度传感器、做个微信小程序控制、或者接个摄像头看看植物长势——都有基础了。

有什么问题的话随时问我,祝你的小花园越来越茂盛 🌿