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
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
| Qty | Part | Notes |
|---|---|---|
| 1 | Arduino Uno (or Nano) | Any 5 V Arduino works. |
| 1 | Capacitive soil moisture sensor v1.2 | Analog output, 3.3–5 V. |
| 1 | Mini submersible pump, 3–6 V DC | Plus ~1 m of matching silicone tube. |
| 1 | IRLZ44N logic-level MOSFET | Fully on at 5 V gate drive. (A ready-made MOSFET module also works.) |
| 1 | 1N5819 Schottky diode | Flyback diode across the pump. |
| 1 + 1 | 220 Ω and 100 kΩ resistors | Gate resistor and gate pull-down. |
| 1 | 16×2 LCD with I²C backpack (PCF8574) | Address 0x27 or 0x3F. |
| 1 | Dual-port USB charger, 5 V ≥ 2 A | One port for the Arduino, one for the pump (via a USB-to-wire cable). |
| — | Breadboard or perfboard, jumper wires, a water container | Keep electronics above and away from the water. |
Wiring
| From | To |
|---|---|
| Sensor VCC / GND / AOUT | Uno 5V / GND / A0 |
| LCD VCC / GND / SDA / SCL | Uno 5V / GND / A4 / A5 |
| Uno D7 | 220 Ω → MOSFET gate (100 kΩ from gate to GND) |
| MOSFET source | GND (shared with the Uno and the pump supply) |
| MOSFET drain | Pump − (black) |
| Pump + (red) | Pump supply +5 V |
| 1N5819 | Across 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:
- Upload the sketch with the pump's USB cable unplugged and open the Serial Monitor (9600 baud).
- Hold the sensor in dry air: note the
raw=value →RAW_DRY. - 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. - 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; setSTART_BELOWabout 15 points lower.
Build steps
- Wire and test the LCD alone (the start-up text should appear; adjust the backpack's contrast pot if you see only blocks).
- Add the sensor and calibrate it.
- Build the MOSFET stage on the breadboard. Test with a 5 V LED + resistor in place of the pump first.
- Connect the pump in a bowl of water, with the tube outlet back into the bowl.
- 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.
SOAK_MS to 10000UL (10 s) so you don't wait 10 minutes per cycle — and set it back afterwards.Results
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.