Commercial greenhouses are run by controllers that measure the air and the soil and then act: they water, open windows and switch on fans. In this capstone you build a small version. A soil sensor decides when to water, a DHT11 decides when to open the roof vent, a relay stands in for the water pump and a servo moves the vent. The LCD shows what the controller measures and what it's doing. You can run it on your desk, or build it into a shoebox or a clear plastic box with a real plant.
What you'll learn
- How to turn sensor readings into automatic actions
- Why controllers use two limits instead of one (hysteresis)
- How to limit an action with timers so it can never run away
- How to share one 5 V supply between a servo, a relay and a screen
What you need
- UNO R3 board and USB cable (UNO R3 Starter Kit)
- 1602 LCD display, 10 kΩ potentiometer and 1 resistor, 220 Ω (UNO R3 Starter Kit)
- DHT11 temperature and humidity sensor (UNO R3 Starter Kit)
- Servo motor (UNO R3 Starter Kit)
- Soil moisture module with probe (45-in-1 Sensor Kit)
- Relay module (45-in-1 Sensor Kit)
- A plant in a pot, and a glass of water for calibration
- Optional: cardboard and tape for a roof vent
- Breadboard and jumper wires (UNO R3 Starter Kit)
Before you start
The LCD has 16 connections in a row. If yours has no pins, solder 16 pins from the 40-pin male header in the kit first, so it plugs into the breadboard. Ask an adult to help if you're new to soldering.
Safety: the relay is used here only as a switch that you can hear and see. If you later connect a pump, use a small battery-powered or USB-powered pump (3–6 V). Never connect mains electricity to the relay, and keep water away from the electronics.
Wiring

| From | To |
|---|---|
| LCD VSS, RW and K | GND |
| LCD VDD | 5V |
| LCD V0 | Middle pin (W) of the potentiometer |
| LCD RS | UNO pin 12 |
| LCD E | UNO pin 11 |
| LCD D4, D5, D6, D7 | UNO pins 5, 4, 3, 2 |
| LCD A (backlight +) | 220 Ω resistor, then 5V |
| Potentiometer outer pins | 5V and GND |
| DHT11 S (signal) | UNO pin 7 |
| DHT11 + and − | 5V and GND |
| Soil module VCC | UNO pin 8 (powers the sensor only while measuring) |
| Soil module GND | GND |
| Soil module AO | UNO pin A0 |
| Soil module DO | Not used |
| Relay module S | UNO pin 6 |
| Relay module + and − | 5V and GND |
| Servo orange / yellow wire | UNO pin 9 |
| Servo red wire | 5V |
| Servo brown / black wire | GND |
Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.
Build it in stages
This is a bigger build than the earlier lessons, so don't wire everything at once. Add one part, test it, then add the next. When something stops working, you know it's the part you just added.
- Screen and air sensor. Wire the LCD, the potentiometer and the DHT11, and upload the sketch. The top line shows temperature and humidity.
- Soil sensor. Add the soil module and calibrate it (see below).
-
Roof vent. Add the servo. Warm the DHT11 gently between your fingers or with warm breath. To test without waiting, lower
VENT_OPEN_ATfor a moment to just above room temperature. - Watering. Add the relay module. Take the probe out of the soil: after the first minute the relay clicks on for three seconds, and the screen shows "Watering".
The sketch
Install the DHT sensor library by Adafruit. The LiquidCrystal and Servo libraries are built in.
#include <LiquidCrystal.h>
#include <DHT.h>
#include <Servo.h>
// LCD pins: RS, E, D4, D5, D6, D7
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
DHT dht(7, DHT11);
Servo vent;
const int SOIL_POWER = 8;
const int SOIL_PIN = A0;
const int RELAY_PIN = 6;
const int SERVO_PIN = 9;
// Calibrate these two for your own sensor (see "Calibrate the soil sensor")
const int DRY_READING = 1010; // probe in dry air
const int WET_READING = 400; // probe in a glass of water
const int WATER_BELOW = 35; // water when the soil is drier than this (%)
const unsigned long WATER_TIME = 3000; // the pump runs for 3 seconds
const unsigned long SOAK_TIME = 60000; // then wait 1 minute for the water to spread
const float VENT_OPEN_AT = 28.0; // open the vent at this temperature
const float VENT_CLOSE_AT = 26.0; // close it again at this one
bool ventOpen = false;
bool watering = false;
unsigned long lastRead = 0;
unsigned long waterStart = 0;
unsigned long lastWater = 0;
int waterCount = 0;
int readSoil() {
digitalWrite(SOIL_POWER, HIGH); // power on only while measuring
delay(100);
int reading = analogRead(SOIL_PIN);
digitalWrite(SOIL_POWER, LOW);
Serial.print(F("Soil reading: "));
Serial.println(reading);
return constrain(map(reading, DRY_READING, WET_READING, 0, 100), 0, 100);
}
void moveVent(bool open) {
vent.attach(SERVO_PIN);
vent.write(open ? 90 : 0);
delay(600); // time to get there
vent.detach(); // no power to the motor while it rests
ventOpen = open;
}
void setup() {
Serial.begin(9600);
pinMode(SOIL_POWER, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW);
lcd.begin(16, 2);
dht.begin();
lcd.print(F("Greenhouse"));
moveVent(false); // start with the vent closed
}
void loop() {
unsigned long now = millis();
// Stop the pump on time, whatever else happens
if (watering && now - waterStart >= WATER_TIME) {
digitalWrite(RELAY_PIN, LOW);
watering = false;
lastWater = now;
}
if (now - lastRead < 2000) return; // measure every 2 seconds
lastRead = now;
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
int soil = readSoil();
if (isnan(temperature) || isnan(humidity)) {
lcd.clear();
lcd.print(F("Sensor error"));
return;
}
// Roof vent: two limits, so it doesn't flap open and shut
if (!watering) {
if (temperature >= VENT_OPEN_AT && !ventOpen) moveVent(true);
if (temperature <= VENT_CLOSE_AT && ventOpen) moveVent(false);
}
// Watering: a short burst, then a pause
if (!watering && soil < WATER_BELOW && now - lastWater >= SOAK_TIME) {
watering = true;
waterStart = now;
waterCount++;
digitalWrite(RELAY_PIN, HIGH);
}
lcd.clear();
lcd.print(F("T:"));
lcd.print(temperature, 1);
lcd.print(F("C H:"));
lcd.print(humidity, 0);
lcd.print(F("%"));
lcd.setCursor(0, 1);
if (watering) {
lcd.print(F("Watering #"));
lcd.print(waterCount);
} else {
lcd.print(F("Soil:"));
lcd.print(soil);
lcd.print(ventOpen ? F("% V:open") : F("% V:shut"));
}
}
Calibrate the soil sensor
- Open the Serial Monitor at 9600 baud. It prints the soil reading every two seconds.
- Hold the probe in the air and note the reading. Put it in
DRY_READING. - Dip the probe in a glass of water up to the line where the metal ends and note the reading. Put it in
WET_READING. - Upload again and push the probe into the soil next to your plant.
How it works
Every two seconds the controller measures, decides and acts. That loop of measure, decide, act is how all automatic control works, from a room thermostat to a factory.
The vent uses two limits. It opens at 28 °C but closes only when the air is back down to 26 °C. If it used a single limit, the vent would open and close again and again while the temperature hovered around that number. The gap between the two limits is called hysteresis.
Watering is protected by two timers. The pump runs for only three seconds, and then the controller waits a full minute before it's allowed to water again. Water needs time to spread through the soil and reach the probe. Without the pause, the controller would keep pumping until the pot overflowed. Notice that the line that stops the pump is at the very top of loop(), before anything that could return early, so a sensor error can never leave the pump running.
The servo, the relay and the LCD backlight all share the UNO's 5 V supply. To keep the load low, the servo is switched off with detach() as soon as it has moved, and the vent never moves while the pump is on.
Try this next
- Add the photoresistor module and switch on an LED "grow light" when it gets dark.
- Show a warning on the LCD when the controller has watered more than five times: the water tank may be empty.
- Open the vent halfway at 27 °C and fully at 30 °C.
- Log the readings to an SD card with the code from the weather station capstone.
Troubleshooting
- The screen lights up but shows only blocks or nothing: turn the potentiometer slowly from one end to the other.
- The soil value is always 0 % or 100 %: calibrate again; your readings may be outside the default numbers.
-
The relay is on when it should be off: some relay modules switch on with LOW. Swap
HIGHandLOWin the two relay lines and insetup(). - The UNO restarts or the screen flickers when the servo moves: the USB port can't supply enough current. Use a different USB port or a powered hub, and check that the servo isn't blocked.
-
The vent never opens: the DHT11 reacts slowly. Check the temperature on the LCD and compare it with
VENT_OPEN_AT.