🌱 گلخانه هوشمند با ESP32: آبیاری خودکار، فن، نور و صفحهی وب (کد کامل)
یک گلخانه هوشمند با ESP32 میسازیم که خودش حواسش به گیاهها هست: وقتی خاک خشک شد آب میدهد، وقتی هوا گرم شد فن را روشن میکند، وقتی تاریک شد نور رشد را روشن میکند و همهی عددها را در یک صفحهی وب زنده روی گوشی نشان میدهد. از همان صفحه میشود به حالت دستی رفت و پمپ و فن را خودت روشن کنی. کد کامل است و همهی بخشها را قبل از خرید قطعه در شبیهساز امتحان میکنیم، همان کاری که در ووکوی (Wokwi) مرسوم است.
چه میسازیم؟
- آبیاری خودکار: رطوبت خاک زیر ۳۵ درصد ⇐ پمپ روشن، بالای ۶۰ درصد ⇐ خاموش. پمپ هرگز بیشتر از ۱۵ ثانیه پشت سر هم کار نمیکند و بعد از هر آبیاری یک دقیقه صبر میکند تا آب در خاک پخش شود.
- تهویه: دما بالای ۳۰ درجه ⇐ فن روشن، زیر ۲۷ درجه ⇐ خاموش.
- نور رشد: نور محیط زیر ۳۰ درصد ⇐ LED رشد روشن.
- صفحهی وب: دما، رطوبت هوا، رطوبت خاک و نور، هر دو ثانیه بدون رفرش؛ دکمههای حالت خودکار و دستی، پمپ و فن.
قطعهها
- یک برد ESP32
- یک سنسور DHT22 (یا DHT11)
- یک سنسور رطوبت خاک (ترجیحاً نوع خازنی)
- یک فتوسل LDR و یک مقاومت ۱۰ کیلواهم
- دو ماژول رله برای پمپ و فن (در شبیهساز: دو LED)
- یک LED با مقاومت ۲۲۰ اهم برای نور رشد
- روی نمونهی واقعی: پمپ آب کوچک ۵ یا ۱۲ ولتی، فن کوچک و منبع تغذیهی جدا برای آنها
سیمکشی
| قطعه | پایه | به ESP32 |
|---|---|---|
| DHT22 | + / DATA / − | 3V3 / GPIO15 / GND |
| رطوبت خاک | V+ / GND / AO | 3V3 / GND / GPIO34 (پایهی DO لازم نیست) |
| LDR | یک پایه / پایهی دیگر | 3V3 / GPIO35، و مقاومت ۱۰ کیلواهم از GPIO35 به GND |
| رلهی پمپ | IN / VCC / GND | GPIO26 / VIN (۵ ولت) / GND |
| رلهی فن | IN / VCC / GND | GPIO27 / VIN / GND |
| LED نور رشد | پایهی بلند از راه ۲۲۰ اهم | GPIO25؛ پایهی کوتاه به GND |
همهی سنسورهای آنالوگ روی پایههای ADC1 (یعنی GPIO32 تا GPIO39) هستند، چون ADC2 با روشن بودن وایفای کار نمیکند. جزئیات پایهها را در پین اوت ESP32 ببین. در شبیهساز بهجای دو رله، دو LED (آبی برای پمپ و زرد برای فن، هر کدام با مقاومت ۲۲۰ اهم) روی GPIO26 و GPIO27 بگذار؛ سیمپیچ رله بیشتر از توان یک پایهی ۳٫۳ ولتی جریان میخواهد و روی برد واقعی هم باید از ماژول رلهی آماده که ترانزیستور دارد استفاده کنی.
کد کامل
// Smart greenhouse on ESP32: DHT22 + soil moisture + LDR,
// a pump relay, a fan relay and a grow light, with a live web page.
#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>
// ---- pins (analog sensors on ADC1: ADC2 does not work while Wi-Fi is on) ----
const int DHT_PIN = 15; // DHT22 data
const int SOIL_PIN = 34; // soil moisture AO
const int LDR_PIN = 35; // LDR + 10k divider
const int PUMP_PIN = 26; // relay 1 -> water pump
const int FAN_PIN = 27; // relay 2 -> fan
const int LIGHT_PIN = 25; // grow light LED (through 220 ohm)
// ---- calibration and limits ----
const int SOIL_DRY = 4095; // raw value in dry soil (measure your own sensor!)
const int SOIL_WET = 0; // raw value in wet soil
const int SOIL_LOW = 35; // start watering below this %
const int SOIL_HIGH = 60; // stop watering above this %
const float FAN_ON = 30.0; // fan on above this temperature (C)
const float FAN_OFF = 27.0; // fan off below this
const int DARK = 30; // grow light on below this light %
const unsigned long PUMP_MAX_MS = 15000; // never pump longer than 15 s
const unsigned long PUMP_REST_MS = 60000; // then let the water soak in for 1 min
const bool RELAY_ACTIVE_LOW = false; // many real relay boards switch on with LOW
DHT dht(DHT_PIN, DHT22);
WebServer server(80);
float temperature = NAN, humidity = NAN;
int soil = 0, light = 0;
bool pumpOn = false, fanRunning = false, lightOn = false;
bool autoMode = true;
bool pumpHasRun = false;
unsigned long pumpStart = 0, pumpStop = 0;
const char page[] PROGMEM = R"rawliteral(<!DOCTYPE html><html><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Greenhouse</title>
<style>
body{font-family:sans-serif;text-align:center;background:#eef7ee;margin:0;padding:12px}
.card{display:inline-block;min-width:120px;margin:6px;padding:10px;border-radius:12px;background:#fff}
.v{font-size:1.8em;font-weight:bold}
button{margin:4px;padding:10px 14px;border-radius:8px;border:1px solid #888}
</style></head><body>
<h2>Smart greenhouse</h2>
<div class="card">Temp<div class="v" id="t">-</div>C</div>
<div class="card">Air humidity<div class="v" id="h">-</div>%</div>
<div class="card">Soil<div class="v" id="s">-</div>%</div>
<div class="card">Light<div class="v" id="l">-</div>%</div>
<p>Mode: <b id="m">-</b> | Pump: <b id="p">-</b> | Fan: <b id="f">-</b> | Grow light: <b id="g">-</b></p>
<button onclick="go('/mode?auto=1')">Auto</button>
<button onclick="go('/mode?auto=0')">Manual</button><br>
<button onclick="go('/pump?on=1')">Pump ON</button>
<button onclick="go('/pump?on=0')">Pump OFF</button>
<button onclick="go('/fan?on=1')">Fan ON</button>
<button onclick="go('/fan?on=0')">Fan OFF</button>
<script>
function txt(id, v) { document.getElementById(id).textContent = (v === null ? '-' : v); }
function show(d) {
txt('t', d.t); txt('h', d.h); txt('s', d.soil); txt('l', d.light);
txt('m', d.auto ? 'auto' : 'manual');
txt('p', d.pump ? 'ON' : 'off'); txt('f', d.fan ? 'ON' : 'off'); txt('g', d.grow ? 'ON' : 'off');
}
function load() { fetch('/data').then(function (r) { return r.json(); }).then(show); }
function go(url) { fetch(url).then(load); }
load();
setInterval(load, 2000);
</script>
</body></html>)rawliteral";
void relay(int pin, bool on) {
digitalWrite(pin, (on != RELAY_ACTIVE_LOW) ? HIGH : LOW);
}
void setPump(bool on) {
if (on == pumpOn) return;
pumpOn = on;
relay(PUMP_PIN, on);
if (on) {
pumpStart = millis();
} else {
pumpStop = millis();
pumpHasRun = true;
}
Serial.println(on ? "Pump ON" : "Pump OFF");
}
void setFan(bool on) {
fanRunning = on;
relay(FAN_PIN, on);
}
String num(float v) {
return isnan(v) ? String("null") : String(v, 1);
}
void readSensors() {
float t = dht.readTemperature();
float h = dht.readHumidity();
if (!isnan(t)) temperature = t; // keep the last good value after a bad read
if (!isnan(h)) humidity = h;
soil = constrain(map(analogRead(SOIL_PIN), SOIL_DRY, SOIL_WET, 0, 100), 0, 100);
light = map(analogRead(LDR_PIN), 0, 4095, 0, 100);
}
void control() {
// Safety first: this check runs in manual mode too.
if (pumpOn && millis() - pumpStart > PUMP_MAX_MS) setPump(false);
if (!autoMode) return;
bool rested = !pumpHasRun || millis() - pumpStop > PUMP_REST_MS;
if (!pumpOn && soil < SOIL_LOW && rested) setPump(true);
if (pumpOn && soil > SOIL_HIGH) setPump(false);
if (!isnan(temperature)) {
if (!fanRunning && temperature > FAN_ON) setFan(true);
if (fanRunning && temperature < FAN_OFF) setFan(false);
}
lightOn = light < DARK;
digitalWrite(LIGHT_PIN, lightOn ? HIGH : LOW);
}
void handleData() {
String j = "{\"t\":" + num(temperature);
j += ",\"h\":" + num(humidity);
j += ",\"soil\":" + String(soil);
j += ",\"light\":" + String(light);
j += ",\"auto\":" + String(autoMode ? "true" : "false");
j += ",\"pump\":" + String(pumpOn ? "true" : "false");
j += ",\"fan\":" + String(fanRunning ? "true" : "false");
j += ",\"grow\":" + String(lightOn ? "true" : "false");
j += "}";
server.send(200, "application/json", j);
}
void handleMode() {
autoMode = server.arg("auto") == "1";
server.send(200, "text/plain", "ok");
}
void handlePump() {
autoMode = false; // a manual command means manual mode
setPump(server.arg("on") == "1");
server.send(200, "text/plain", "ok");
}
void handleFan() {
autoMode = false;
setFan(server.arg("on") == "1");
server.send(200, "text/plain", "ok");
}
void setup() {
Serial.begin(115200);
pinMode(PUMP_PIN, OUTPUT);
pinMode(FAN_PIN, OUTPUT);
pinMode(LIGHT_PIN, OUTPUT);
relay(PUMP_PIN, false);
relay(FAN_PIN, false);
dht.begin();
WiFi.begin("Wokwi-GUEST", ""); // simulator network; use your own on a real board
Serial.print("Connecting");
while (WiFi.status() != WL_CONNECTED) {
delay(300);
Serial.print(".");
}
Serial.println();
Serial.print("Open http://");
Serial.println(WiFi.localIP());
server.on("/", []() { server.send(200, "text/html", page); });
server.on("/data", handleData);
server.on("/mode", handleMode);
server.on("/pump", handlePump);
server.on("/fan", handleFan);
server.begin();
}
void loop() {
server.handleClient();
static unsigned long last = 0;
if (millis() - last >= 2000) { // DHT22 needs about 2 s between reads
last = millis();
readSensors();
control();
Serial.printf("T=%.1f H=%.0f soil=%d%% light=%d%% pump=%d fan=%d\n",
temperature, humidity, soil, light, pumpOn, fanRunning);
}
}
کد چطور کار میکند؟
خواندن سنسورها
map(analogRead(SOIL_PIN), SOIL_DRY, SOIL_WET, 0, 100)عدد خام صفر تا ۴۰۹۵ را به درصد رطوبت تبدیل میکند. چون سنسور خاک در خاک خشک عدد بزرگتر میدهد، اول «خشک» و بعد «خیس» را مینویسیم.constrainهم نمیگذارد درصد از صفر کمتر یا از صد بیشتر شود.- LDR با مقاومت ۱۰ کیلواهم یک مقسم ولتاژ میسازد: نور بیشتر، مقاومت LDR کمتر، ولتاژ GPIO35 بیشتر. برای همین عدد خام مستقیم به درصد نور تبدیل میشود.
- اگر DHT یک بار جواب خراب داد (
NaN)، آخرین عدد سالم را نگه میداریم تا فن بیدلیل روشن و خاموش نشود.
منطق کنترل
- هیسترزیس: پمپ زیر ۳۵ درصد روشن و بالای ۶۰ درصد خاموش میشود و فن بالای ۳۰ و زیر ۲۷ درجه. فاصلهی بین دو آستانه جلوی روشن و خاموش شدن پشت سر هم را میگیرد.
- محدودیت ایمنی پمپ: اگر سنسور از خاک بیرون بیفتد، همیشه «خشک» میخواند و پمپ تا خالی شدن مخزن کار میکند.
PUMP_MAX_MSپمپ را بعد از ۱۵ ثانیه، حتی در حالت دستی، خاموش میکند وPUMP_REST_MSیک دقیقه صبر میکند تا آب به سنسور برسد. RELAY_ACTIVE_LOWبرای ماژولهای رلهای است که با LOW روشن میشوند. اگر رلهی تو برعکس کار کرد، فقط همین راtrueکن.
صفحهی وب
- صفحه یک بار با
/میآید و بعد جاوااسکریپت هر دو ثانیه آدرس/dataرا میخواند. جواب JSON است، مثل{"t":24.5,"h":61.0,"soil":42,...}، و فقط عددها عوض میشوند؛ کل صفحه دوباره بار نمیشود. - دکمهها آدرسهایی مثل
/pump?on=1را صدا میزنند و در C++ باserver.arg("on")خوانده میشوند. هر فرمان دستی، دستگاه را به حالت دستی میبرد تا منطق خودکار فوراً فرمانت را برنگرداند. - صفحهی HTML در یک «رشتهی خام»
R"rawliteral(...)rawliteral"است تا کوتیشنها دردسر درست نکنند. اگر با صفحهی وب ESP32 تازه آشنا شدهای، اول صفحهی وب دما و رطوبت را بخوان.
قدمبهقدم در شبیهساز
- یک پروژهی ESP32 باز کن و قطعهها را طبق جدول بچین؛ بهجای رلهها دو LED بگذار.
- اول فقط بخش سنسورها را امتحان کن: کد را اجرا کن و در سریال مانیتور (سرعت ۱۱۵۲۰۰) خطهای
T=... soil=...را ببین. - روی سنسور رطوبت خاک کلیک کن و رطوبت را کم کن. وقتی درصد زیر ۳۵ رفت، LED پمپ روشن میشود و پیام
Pump ONمیآید. رطوبت را بالا ببر تا خاموش شود. - دمای DHT را بالای ۳۰ ببر و فن را ببین؛ بعد زیر ۲۷ بیاور.
- زبانهی «مرورگر» ویرایشگر را باز کن، صفحهی گلخانه را ببین و دکمههای Manual و Pump ON را امتحان کن. پمپ بعد از ۱۵ ثانیه خودش خاموش میشود.
باز کردن و خواندن پروژهها رایگان است و اجرای برد ESP32 به اشتراک Plus نیاز دارد. اگر میخواهی منطق آبیاری را رایگان روی آردوینو اونو تمرین کنی، پروژهی آبیاری خودکار و درس آبیاری خودکار را ببین. بخش وب هم در پروژهی صفحهی وب دماسنج جداگانه آماده است.
کالیبره کردن سنسور خاک روی نمونهی واقعی
عددهای SOIL_DRY و SOIL_WET در کد برای شبیهساز تنظیم شدهاند. سنسور واقعی هر کدام عدد خودش را دارد. یک برنامهی ساده بنویس که فقط analogRead(34) را چاپ کند؛ عدد را یک بار وقتی سنسور در هوای خشک است و یک بار وقتی تا خط مجاز در لیوان آب است یادداشت کن و در کد بگذار. سنسورهای خازنی معمولاً در هوا حدود ۲۵۰۰ تا ۳۰۰۰ و در آب حدود ۱۰۰۰ تا ۱۴۰۰ میدهند. نوع خازنی را به نوع دوشاخهی مقاومتی ترجیح بده؛ دوشاخهها در خاک خیس زود زنگ میزنند. توضیح بیشتر در سنسور رطوبت خاک با آردوینو است.
ساخت نمونهی واقعی
- تغذیهی جدا: پمپ و فن را از منبع خودشان تغذیه کن و فقط از کنتاکتهای COM و NO رله رد کن. اگر از همان USB برد برق بگیرند، ESP32 هنگام روشن شدن پمپ ریست میشود.
- رلهی سازگار با ۳٫۳ ولت: بعضی ماژولهای رلهی ۵ ولتی با فرمان ۳٫۳ ولتی ESP32 مطمئن روشن نمیشوند. ماژولهای اپتوکوپلردار یا مدلهای ۳٫۳ ولتی را بخر.
- آب دور از برد: برد و منبع تغذیه را بالاتر از مخزن و داخل جعبه بگذار و سیمهای پمپ را با چسب حرارتی عایق کن.
- وایفای خانه: بهجای
Wokwi-GUESTاسم و رمز مودم را بنویس و یادت باشد ESP32 فقط باند ۲٫۴ گیگاهرتز را میبیند.
اشکالهای رایج
- درصد خاک همیشه صفر یا صد است: عددهای کالیبراسیون جابهجا هستند یا سنسور به پایهی ADC2 وصل شده است.
- دما و رطوبت «-» نشان داده میشود: سیم DATA جای دیگری است یا در کد DHT11 و DHT22 جابهجا شدهاند.
- پمپ مدام روشن و خاموش میشود: فاصلهی
SOIL_LOWوSOIL_HIGHخیلی کم است یا سنسور خیلی نزدیک محل ریختن آب است. - صفحه باز نمیشود:
server.handleClient()باید در هر دورloopصدا زده شود؛delayطولانی درloopنگذار.
ایده برای کاملتر کردن
آستانهها را از صفحهی وب قابل تنظیم کن و در حافظه ذخیره کن (SPIFFS روی ESP32)، مصرف آب روزانه را از مجموع زمان کار پمپ حساب کن، یا گلخانه را به یک داشبورد MQTT وصل کن تا از هر جای خانه دیده شود: پروژهی MQTT با ESP32. اگر این پروژه را برای مدرسه یا دانشگاه میسازی، راهنمای پروژه پایانی دانشگاه و ایدههای اینترنت اشیا را هم ببین.
گلخانهی هوشمند را در تینکرکد فارسی بساز 🌱سؤالهای رایج
چرا سنسور رطوبت خاک را به GPIO34 وصل کردیم و نه هر پایهای؟
وقتی وایفای ESP32 روشن است، مبدل ADC2 در اختیار وایفای است و پایههایش عدد درستی نمیدهند. GPIO32 تا GPIO39 روی ADC1 هستند و همیشه کار میکنند؛ 34 و 35 هم فقط ورودیاند که برای سنسور مناسب است.
پمپ ۱۲ ولتی را میشود مستقیم به ESP32 وصل کرد؟
نه. پایهی ESP32 فقط ۳٫۳ ولت و چند ده میلیآمپر میدهد. پمپ را با یک ماژول رله یا ماسفت و با منبع تغذیهی جدا روشن کن و ESP32 فقط فرمان بدهد.
هیسترزیس یعنی چه و چرا لازم است؟
یعنی روشن و خاموش شدن با دو آستانهی متفاوت؛ مثلاً پمپ زیر ۳۵ درصد روشن و بالای ۶۰ درصد خاموش شود. با یک آستانهی تنها، نزدیک آن عدد، پمپ یا فن مدام روشن و خاموش میشود و زود خراب میشود.