植物总是浇水过多或过少?不如自己动手做一个自动灌溉装置,成本不到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);
}
把代码烧录进去
- 打开 Arduino IDE
- 安装 ESP32 开发板支持:菜单 → 工具 → 开发板管理器 → 搜索 “ESP32” → 安装
- 插入 ESP32 到电脑,选择对应的端口
- 把上面代码里的
ssid和password换成你家 WiFi 的信息 - 点击上传
上传成功后,打开串口监视器(右上角放大镜图标),你会看到:
正在连接 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控制”这些概念有最直观的理解。以后想加更多功能——比如加个温湿度传感器、做个微信小程序控制、或者接个摄像头看看植物长势——都有基础了。
有什么问题的话随时问我,祝你的小花园越来越茂盛 🌿
