PRJ-002 Planned

Automatic Plant Watering System

Arduino-based soil moisture monitoring with automated pump control and LCD display.

A plant that waters itself is the perfect first "closed-loop" project: a sensor measures something, a microcontroller decides, and an actuator acts. This build reads soil moisture with a corrosion-proof capacitive sensor, runs a small pump through a MOSFET when the soil gets dry, and shows everything on a 16×2 display — with the safety rules that stop it from flooding your desk.

What you'll build

  • Reads soil moisture every 2 seconds and shows it as a percentage on the LCD.
  • Starts watering below 35 % and stops above 50 % — two thresholds (hysteresis) so the pump doesn't stutter on and off.
  • Never runs the pump longer than 8 seconds at a time, then waits 10 minutes for the water to soak in.
  • Detects an unplugged sensor and an empty tank (three waterings with no effect) and stops safely.

How it works

Soil sensor capacitive analog 0–5 V Arduino Uno reads %, decides MOSFET switches the pump Pump waters the plant LCD shows moisture and state
Sense → decide → act → report: the shape of almost every control system.

Why a capacitive sensor? Cheap two-prong resistive sensors pass current through the soil, so their exposed metal corrodes within weeks. A capacitive sensor (the common "v1.2" board) measures how the soil changes the capacitance of a coated copper pad, with no metal in contact with the soil. Wetter soil gives a lower reading — the code maps it the right way round.

Why a MOSFET? An Arduino pin can supply only a few tens of milliamps; a small pump needs hundreds of milliamps, and the motor's voltage spikes would damage the pin. A logic-level MOSFET switches the pump's negative lead, and a Schottky diode across the pump absorbs the spike when it turns off.

Parts list

QtyPartNotes
1Arduino Uno (or Nano)Any 5 V Arduino works.
1Capacitive soil moisture sensor v1.2Analog output, 3.3–5 V.
1Mini submersible pump, 3–6 V DCPlus ~1 m of matching silicone tube.
1IRLZ44N logic-level MOSFETFully on at 5 V gate drive. (A ready-made MOSFET module also works.)
11N5819 Schottky diodeFlyback diode across the pump.
1 + 1220 Ω and 100 kΩ resistorsGate resistor and gate pull-down.
116×2 LCD with I²C backpack (PCF8574)Address 0x27 or 0x3F.
1Dual-port USB charger, 5 V ≥ 2 AOne port for the Arduino, one for the pump (via a USB-to-wire cable).
—Breadboard or perfboard, jumper wires, a water containerKeep electronics above and away from the water.

Wiring

Arduino Uno 5V GND A0 A4 SDA A5 SCL D7 GND Soil sensor v1.2 VCC GND AOUT 16x2 LCD (I2C) VCC GND SDA SCL IRLZ44N stage IN GND DRAIN 5 V pump − + 5 V USB supply +5V GND Gate: 220 Ω from D7 · 100 kΩ gate → GND 1N5819 across pump, band (cathode) to + All grounds connected together
The pump has its own 5 V supply; only the grounds are shared. Never power the pump from the Arduino's 5 V pin.
FromTo
Sensor VCC / GND / AOUTUno 5V / GND / A0
LCD VCC / GND / SDA / SCLUno 5V / GND / A4 / A5
Uno D7220 Ω → MOSFET gate (100 kΩ from gate to GND)
MOSFET sourceGND (shared with the Uno and the pump supply)
MOSFET drainPump − (black)
Pump + (red)Pump supply +5 V
1N5819Across the pump: band (cathode) to pump +

IRLZ44N pins, looking at the front with the legs down: gate, drain, source (left to right).

The code

Install LiquidCrystal I2C (by Frank de Brabander) from the Library Manager, then upload:

/*  EFury Labs — PRJ-002 Automatic Plant Watering System
    Arduino Uno + capacitive soil sensor + logic-level MOSFET + 5 V pump + 16x2 I2C LCD
    Library: "LiquidCrystal I2C" (Frank de Brabander) from the Library Manager.            */

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// ---------- Pins ----------
const uint8_t SENSOR_PIN = A0;    // sensor AOUT
const uint8_t PUMP_PIN   = 7;     // MOSFET gate through 220 Ω — HIGH = pump on

// ---------- Calibration (replace with YOUR readings — see "Calibrate the sensor") ----------
const int RAW_DRY = 620;          // sensor held in dry air
const int RAW_WET = 280;          // sensor tip in a glass of water (up to the line)

// ---------- Watering rules ----------
const int START_BELOW = 35;                       // % moisture: start watering below this
const int STOP_ABOVE  = 50;                       // % moisture: stop above this (hysteresis)
const unsigned long MAX_PUMP_MS = 8000UL;         // never pump longer than 8 s in one go
const unsigned long SOAK_MS     = 10UL * 60000UL; // then wait 10 min for water to spread
const unsigned long READ_MS     = 2000UL;         // read the sensor every 2 s
const uint8_t MAX_FAILS = 3;                      // 3 waterings with no effect → "check tank"

LiquidCrystal_I2C lcd(0x27, 16, 2);               // try 0x3F if the screen stays blank

enum State { IDLE, PUMPING, SOAKING, LOCKED, SENSOR_FAULT };
State state = IDLE;
unsigned long stateSince = 0, lastRead = 0;
int moisture = 0, moistureBefore = 0;
uint8_t fails = 0;

int readRaw() {                                   // average 10 readings to calm noise
  long sum = 0;
  for (uint8_t i = 0; i < 10; i++) { sum += analogRead(SENSOR_PIN); delay(5); }
  return sum / 10;
}

int toPercent(int raw) {                          // dry → 0 %, wet → 100 %
  return constrain(map(raw, RAW_DRY, RAW_WET, 0, 100), 0, 100);
}

void pump(bool on) { digitalWrite(PUMP_PIN, on ? HIGH : LOW); }

void enter(State s) { state = s; stateSince = millis(); }

void show(int raw) {
  static const char* NAMES[] = { "Idle", "Watering", "Soaking", "CHECK TANK", "SENSOR?" };
  lcd.setCursor(0, 0);
  lcd.print("Soil ");
  lcd.print(moisture);
  lcd.print("%  raw ");
  lcd.print(raw);
  lcd.print("    ");
  lcd.setCursor(0, 1);
  lcd.print(NAMES[state]);
  lcd.print("                ");
}

void setup() {
  pinMode(PUMP_PIN, OUTPUT);
  pump(false);                                    // pump off before anything else
  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  lcd.print("EFury Labs");
  lcd.setCursor(0, 1);
  lcd.print("Plant waterer");
  delay(1500);
  lcd.clear();
}

void loop() {
  unsigned long now = millis();

  // Safety first: the pump can never run longer than MAX_PUMP_MS
  if (state == PUMPING && now - stateSince >= MAX_PUMP_MS) {
    pump(false);
    enter(SOAKING);
  }

  if (now - lastRead < READ_MS) return;
  lastRead = now;

  int raw = readRaw();
  moisture = toPercent(raw);

  // An unplugged or shorted sensor reads near 0 or 1023 — never water blind
  if (raw < 50 || raw > 1000) {
    pump(false);
    if (state != SENSOR_FAULT) enter(SENSOR_FAULT);
  } else {
    switch (state) {
      case SENSOR_FAULT:
        enter(IDLE);
        break;
      case IDLE:
        if (moisture < START_BELOW) {
          moistureBefore = moisture;
          pump(true);
          enter(PUMPING);
        }
        break;
      case PUMPING:
        if (moisture >= STOP_ABOVE) { pump(false); enter(SOAKING); }
        break;
      case SOAKING:
        if (now - stateSince >= SOAK_MS) {
          // Did watering help? If not, the tank may be empty or the tube off.
          fails = (moisture < moistureBefore + 3) ? fails + 1 : 0;
          enter(fails >= MAX_FAILS ? LOCKED : IDLE);
        }
        break;
      case LOCKED:                                // stays here until reset/power cycle
        pump(false);
        break;
    }
  }

  show(raw);
  Serial.print("raw=");      Serial.print(raw);
  Serial.print(" moisture="); Serial.print(moisture);
  Serial.print("% state=");   Serial.println(state);
}

Calibrate the sensor

Every sensor and soil is different, so the two numbers at the top of the sketch must come from your hardware:

  1. Upload the sketch with the pump's USB cable unplugged and open the Serial Monitor (9600 baud).
  2. Hold the sensor in dry air: note the raw= value → RAW_DRY.
  3. Stand the sensor in a glass of water up to the marked line (never deeper — the top electronics are not waterproof): note the value → RAW_WET.
  4. Upload again, then push the sensor into the pot and water by hand until the soil feels right. The percentage you see is a good STOP_ABOVE; set START_BELOW about 15 points lower.

Build steps

  1. Wire and test the LCD alone (the start-up text should appear; adjust the backpack's contrast pot if you see only blocks).
  2. Add the sensor and calibrate it.
  3. Build the MOSFET stage on the breadboard. Test with a 5 V LED + resistor in place of the pump first.
  4. Connect the pump in a bowl of water, with the tube outlet back into the bowl.
  5. Move to perfboard, mount everything in a box above the water level, and route the tube to the plant.

Testing checklist

  • Pump stays off during upload and reset (the 100 kΩ pull-down holds the gate low).
  • Dry sensor → pump starts; sensor into water → pump stops.
  • Hold the sensor dry: the pump stops after 8 s, the LCD shows Soaking.
  • Unplug the sensor's signal wire: SENSOR? appears and the pump stays off.
  • Run with the pump out of the water (dry sensor): after three cycles the LCD shows CHECK TANK.
  • The MOSFET stays cool while pumping.
Tip: for testing, temporarily set SOAK_MS to 10000UL (10 s) so you don't wait 10 minutes per cycle — and set it back afterwards.

Results

Build log

Status: planned. After the build this section will show photos, the calibration values used, a week-long moisture log and the video.

Ideas for version 2

  • Wi-Fi: move to an ESP32 and send moisture readings to your phone (the weather station shows how to serve a dashboard).
  • Water-level sensor in the tank instead of guessing from failed cycles.
  • Battery + solar: sleep between readings and power the sensor only while measuring.
  • Several plants: one sensor and pump (or valve) per pot.
← All projects