🌱 گلخانه هوشمند با ESP32: آبیاری خودکار، فن، نور و صفحه‌ی وب (کد کامل)

✍️ تیم ووکوی فارسی ·

یک گلخانه هوشمند با ESP32 می‌سازیم که خودش حواسش به گیاه‌ها هست: وقتی خاک خشک شد آب می‌دهد، وقتی هوا گرم شد فن را روشن می‌کند، وقتی تاریک شد نور رشد را روشن می‌کند و همه‌ی عددها را در یک صفحه‌ی وب زنده روی گوشی نشان می‌دهد. از همان صفحه می‌شود به حالت دستی رفت و پمپ و فن را خودت روشن کنی. کد کامل است و همه‌ی بخش‌ها را قبل از خرید قطعه در شبیه‌ساز امتحان می‌کنیم، همان کاری که در ووکوی (Wokwi) مرسوم است.

چه می‌سازیم؟

قطعه‌ها

سیم‌کشی

قطعهپایهبه ESP32
DHT22+ / DATA / −3V3 / GPIO15 / GND
رطوبت خاکV+ / GND / AO3V3 / GND / GPIO34 (پایه‌ی DO لازم نیست)
LDRیک پایه / پایه‌ی دیگر3V3 / GPIO35، و مقاومت ۱۰ کیلواهم از GPIO35 به GND
رله‌ی پمپIN / VCC / GNDGPIO26 / VIN (۵ ولت) / GND
رله‌ی فنIN / VCC / GNDGPIO27 / 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);
  }
}

کد چطور کار می‌کند؟

خواندن سنسورها

منطق کنترل

صفحه‌ی وب

قدم‌به‌قدم در شبیه‌ساز

  1. یک پروژه‌ی ESP32 باز کن و قطعه‌ها را طبق جدول بچین؛ به‌جای رله‌ها دو LED بگذار.
  2. اول فقط بخش سنسورها را امتحان کن: کد را اجرا کن و در سریال مانیتور (سرعت ۱۱۵۲۰۰) خط‌های T=... soil=... را ببین.
  3. روی سنسور رطوبت خاک کلیک کن و رطوبت را کم کن. وقتی درصد زیر ۳۵ رفت، LED پمپ روشن می‌شود و پیام Pump ON می‌آید. رطوبت را بالا ببر تا خاموش شود.
  4. دمای DHT را بالای ۳۰ ببر و فن را ببین؛ بعد زیر ۲۷ بیاور.
  5. زبانه‌ی «مرورگر» ویرایشگر را باز کن، صفحه‌ی گلخانه را ببین و دکمه‌های Manual و Pump ON را امتحان کن. پمپ بعد از ۱۵ ثانیه خودش خاموش می‌شود.

باز کردن و خواندن پروژه‌ها رایگان است و اجرای برد ESP32 به اشتراک Plus نیاز دارد. اگر می‌خواهی منطق آبیاری را رایگان روی آردوینو اونو تمرین کنی، پروژه‌ی آبیاری خودکار و درس آبیاری خودکار را ببین. بخش وب هم در پروژه‌ی صفحه‌ی وب دماسنج جداگانه آماده است.

کالیبره کردن سنسور خاک روی نمونه‌ی واقعی

عددهای SOIL_DRY و SOIL_WET در کد برای شبیه‌ساز تنظیم شده‌اند. سنسور واقعی هر کدام عدد خودش را دارد. یک برنامه‌ی ساده بنویس که فقط analogRead(34) را چاپ کند؛ عدد را یک بار وقتی سنسور در هوای خشک است و یک بار وقتی تا خط مجاز در لیوان آب است یادداشت کن و در کد بگذار. سنسورهای خازنی معمولاً در هوا حدود ۲۵۰۰ تا ۳۰۰۰ و در آب حدود ۱۰۰۰ تا ۱۴۰۰ می‌دهند. نوع خازنی را به نوع دوشاخه‌ی مقاومتی ترجیح بده؛ دوشاخه‌ها در خاک خیس زود زنگ می‌زنند. توضیح بیشتر در سنسور رطوبت خاک با آردوینو است.

ساخت نمونه‌ی واقعی

اشکال‌های رایج

ایده برای کامل‌تر کردن

آستانه‌ها را از صفحه‌ی وب قابل تنظیم کن و در حافظه ذخیره کن (SPIFFS روی ESP32)، مصرف آب روزانه را از مجموع زمان کار پمپ حساب کن، یا گلخانه را به یک داشبورد MQTT وصل کن تا از هر جای خانه دیده شود: پروژه‌ی MQTT با ESP32. اگر این پروژه را برای مدرسه یا دانشگاه می‌سازی، راهنمای پروژه پایانی دانشگاه و ایده‌های اینترنت اشیا را هم ببین.

گلخانه‌ی هوشمند را در تینکرکد فارسی بساز 🌱

سؤال‌های رایج

چرا سنسور رطوبت خاک را به GPIO34 وصل کردیم و نه هر پایه‌ای؟

وقتی وای‌فای ESP32 روشن است، مبدل ADC2 در اختیار وای‌فای است و پایه‌هایش عدد درستی نمی‌دهند. GPIO32 تا GPIO39 روی ADC1 هستند و همیشه کار می‌کنند؛ 34 و 35 هم فقط ورودی‌اند که برای سنسور مناسب است.

پمپ ۱۲ ولتی را می‌شود مستقیم به ESP32 وصل کرد؟

نه. پایه‌ی ESP32 فقط ۳٫۳ ولت و چند ده میلی‌آمپر می‌دهد. پمپ را با یک ماژول رله یا ماسفت و با منبع تغذیه‌ی جدا روشن کن و ESP32 فقط فرمان بدهد.

هیسترزیس یعنی چه و چرا لازم است؟

یعنی روشن و خاموش شدن با دو آستانه‌ی متفاوت؛ مثلاً پمپ زیر ۳۵ درصد روشن و بالای ۶۰ درصد خاموش شود. با یک آستانه‌ی تنها، نزدیک آن عدد، پمپ یا فن مدام روشن و خاموش می‌شود و زود خراب می‌شود.

مقاله‌های مرتبط

🔥

سیستم اعلام حریق با آردوینو: سنسور گاز MQ-2، سنسور دما، آژیر و ریست (کد کامل)

🅿️

پارکینگ هوشمند با آردوینو: دو سنسور اولتراسونیک، راهبند سروو و نمایش جای خالی روی LCD

💡

۳۰ ایده پروژه اینترنت اشیا با ESP32، از مبتدی تا پیشرفته (با قطعه‌ها و نقطه‌ی شروع)

🌗

PWM روی ESP32 با ledc: کم و زیاد کردن نور LED

همه‌ی آموزش‌های پروژه‌ها و ایده‌ها ← · همه‌ی مقاله‌ها