Built-in project · Arduino Uno R3

Automatic plant waterer on an Arduino Uno

A soil moisture stake, a relay and a small pump water a plant when it gets dry, in short bursts with time to soak. A tank sensor stops it running dry, and an LCD shows it all. The whole build, an Arduino Uno and 7 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 40 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $41. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • Converting a sensor reading to a percent
  • Switching a pump with a relay and a separate battery
  • Watering in bursts with time to soak
  • A three state machine: idle, pumping, soaking
  • Protecting a pump from running dry
  • Difficulty: Intermediate
  • Time: About 40 minutes
  • Board: Arduino Uno R3
  • Parts: 9

How it works

A capacitive soil sensor on A0 reads higher when the soil is drier. The sketch turns that into a percent using SOIL_DRY (522) and SOIL_WET (260). A water level sensor on A1 watches the tank. The LCD shows the soil percent and what the waterer is doing.

When the soil drops below 35 percent, the relay on pin 7 switches the pump on for three seconds. It then waits five seconds for the water to soak down to the sensor. If the soil is still under 60 percent, it waters again.

The pump has its own battery, switched by the relay's COM and NO contacts. If the tank reads low, the pump stays off and the screen says Refill the tank. The button on pin 2 gives one watering on demand.

Parts list

9 parts, plus the jumper wires. They retail for about $41; on Mokxi they're free.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Relay module pin INArduino Uno R3 pin 7
Pushbutton pin 1aArduino Uno R3 pin 2
Soil moisture sensor, capacitive pin VCCArduino Uno R3 pin 5V
Water level sensor pin VCCArduino Uno R3 pin 5V
Relay module pin VCCArduino Uno R3 pin 5V
Character LCD with I2C backpack pin VCCArduino Uno R3 pin 5V
Soil moisture sensor, capacitive pin GNDArduino Uno R3 pin GND
Water level sensor pin GNDArduino Uno R3 pin GND
Pushbutton pin 2aArduino Uno R3 pin GND
Relay module pin GNDArduino Uno R3 pin GND
DC motor pin 2Arduino Uno R3 pin GND
Battery: AA, coin cell, LiPo pin V-Arduino Uno R3 pin GND
Character LCD with I2C backpack pin GNDArduino Uno R3 pin GND
Soil moisture sensor, capacitive pin AOUTArduino Uno R3 pin A0
Water level sensor pin SArduino Uno R3 pin A1
Character LCD with I2C backpack pin SDAArduino Uno R3 pin A4
Character LCD with I2C backpack pin SCLArduino Uno R3 pin A5
DC motor pin 1Relay module pin NO
Battery: AA, coin cell, LiPo pin V+Relay module pin COM

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Automatic plant waterer: soil moisture, a relay, a pump and a tank sensor.
//
// Wiring:
//   soil moisture stake   AOUT -> A0   VCC -> 5 V   GND -> GND
//   tank water level      S -> A1      VCC -> 5 V   GND -> GND
//   relay module          IN -> 7      VCC -> 5 V   GND -> GND
//   pump circuit          battery + -> relay COM, relay NO -> pump -> GND
//                         (the battery minus joins GND, so there is one ground)
//   water now button      pin 2 -> pushbutton -> GND
//   16x2 LCD, I2C 0x27    SDA -> A4    SCL -> A5    VCC -> 5 V   GND -> GND
//
// When the soil reads drier than 35%, the relay runs the pump for three
// seconds, then waits for the water to soak down to the stake before it looks
// again, and repeats until the soil reads 60%. Watering in short bursts is
// what keeps a pot from flooding: the stake only sees the water once it has
// had time to spread.
//
// The tank sensor guards the pump. A small pump running dry burns out, so with
// the tank low the screen asks for a refill and the pump stays off.
//
// In the simulator nothing drinks the water: the tank starts empty, so fill it
// with its slider, then drag the soil's moisture down and watch it water.

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

const uint8_t SOIL_PIN = A0;
const uint8_t TANK_PIN = A1;
const uint8_t RELAY_PIN = 7;
const uint8_t BUTTON_PIN = 2;

// The stake reads higher when it is drier. These are the readings in air and
// in a glass of water; measure your own and put them here.
const int SOIL_DRY = 522;
const int SOIL_WET = 260;
const uint8_t START_BELOW = 35;  // percent
const uint8_t STOP_AT = 60;      // percent
const int TANK_LOW = 120;        // below this the tank is nearly empty

const uint32_t PUMP_MS = 3000;
const uint32_t SOAK_MS = 5000;

LiquidCrystal_I2C lcd(0x27, 16, 2);

enum State : uint8_t { IDLE, PUMPING, SOAKING };
State state = IDLE;
uint32_t stateAt = 0;
uint16_t waterings = 0;
bool buttonWas = false;
bool manual = false;  // this watering was asked for with the button

static uint8_t soilPercent()
{
  long raw = analogRead(SOIL_PIN);
  long p = (long)(SOIL_DRY - raw) * 100 / (SOIL_DRY - SOIL_WET);
  return (uint8_t)(p < 0 ? 0 : (p > 100 ? 100 : p));
}

static bool tankLow()
{
  return analogRead(TANK_PIN) < TANK_LOW;
}

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

/** The top line is the soil; the bottom line is what it is doing. */
static void screen(uint8_t moisture, const char *status, int count = -1)
{
  lcd.setCursor(0, 0);
  lcd.print("Soil ");
  if (moisture < 100) lcd.print(' ');
  if (moisture < 10) lcd.print(' ');
  lcd.print(moisture);
  lcd.print("%  ");
  lcd.print(moisture < START_BELOW ? "DRY " : (moisture >= STOP_AT ? "WET " : "OK  "));
  lcd.setCursor(0, 1);
  uint8_t used = (uint8_t)lcd.print(status);
  if (count >= 0) {
    used = (uint8_t)(used + lcd.print(count));
    used = (uint8_t)(used + lcd.print(" times"));
  }
  for (uint8_t i = used; i < 16; i++) {
    lcd.print(' ');
  }
}

static void startPump(const char *why)
{
  state = PUMPING;
  stateAt = millis();
  waterings++;
  pump(true);
  Serial.print("Pump on: ");
  Serial.println(why);
}

void setup()
{
  Serial.begin(9600);
  pinMode(RELAY_PIN, OUTPUT);
  pump(false);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  lcd.begin();
  lcd.backlight();
  lcd.print("Plant waterer");
  delay(800);
  lcd.clear();
  Serial.println("Plant waterer: starts below 35%, stops at 60%");
}

void loop()
{
  uint32_t now = millis();
  uint8_t moisture = soilPercent();
  bool low = tankLow();
  bool button = digitalRead(BUTTON_PIN) == LOW;
  bool pressed = button && !buttonWas;
  buttonWas = button;

  // The tank comes first: never run the pump dry.
  if (low && state == PUMPING) {
    pump(false);
    state = IDLE;
    Serial.println("Pump off: the tank is empty");
  }

  switch (state) {
    case IDLE:
      if (low) {
        screen(moisture, "Refill the tank");
      } else if (pressed) {
        manual = true;
        startPump("button");
      } else if (moisture < START_BELOW) {
        manual = false;
        startPump("soil is dry");
      } else {
        screen(moisture, "Watered ", waterings);
      }
      break;
    case PUMPING:
      screen(moisture, "Watering...");
      if (now - stateAt >= PUMP_MS) {
        pump(false);
        state = SOAKING;
        stateAt = now;
        Serial.println("Pump off, letting it soak");
      }
      break;
    case SOAKING:
      screen(moisture, "Soaking in...");
      if (now - stateAt >= SOAK_MS) {
        state = IDLE;
        if (!manual && moisture < STOP_AT && !low) {
          startPump("still dry");
        } else if (moisture >= STOP_AT) {
          Serial.print("Done: soil at ");
          Serial.print(moisture);
          Serial.println("%");
        }
      }
      break;
  }
  delay(100);
}

Code walkthrough

The key functions of the sketch, and what each one does.

soilPercent sketch.ino

Reads A0 and maps it between SOIL_DRY and SOIL_WET to get 0 to 100 percent. Because the sensor reads higher when dry, the math runs backward. The result is clamped so it never goes below 0 or above 100.

static uint8_t soilPercent()
{
  long raw = analogRead(SOIL_PIN);
  long p = (long)(SOIL_DRY - raw) * 100 / (SOIL_DRY - SOIL_WET);
  return (uint8_t)(p < 0 ? 0 : (p > 100 ? 100 : p));
}

startPump sketch.ino

Moves the state to PUMPING, records the time, adds one to the watering count and switches the pump on through pump. It also prints the reason to the serial monitor: the button, a dry soil, or still dry.

static void startPump(const char *why)
{
  state = PUMPING;
  stateAt = millis();
  waterings++;
  pump(true);
  Serial.print("Pump on: ");
  Serial.println(why);
}

loop sketch.ino

Reads the soil, tank and button each pass. If the tank is low while pumping, it cuts the pump first. Then a switch handles IDLE, PUMPING for PUMP_MS and SOAKING for SOAK_MS.

void loop()
{
  uint32_t now = millis();
  uint8_t moisture = soilPercent();
  bool low = tankLow();
  bool button = digitalRead(BUTTON_PIN) == LOW;
  bool pressed = button && !buttonWas;
  buttonWas = button;

  // The tank comes first: never run the pump dry.
  if (low && state == PUMPING) {
    pump(false);
    state = IDLE;
    Serial.println("Pump off: the tank is empty");
  }

  switch (state) {
    case IDLE:
      if (low) {
        screen(moisture, "Refill the tank");
      } else if (pressed) {
        manual = true;
        startPump("button");
      } else if (moisture < START_BELOW) {
        manual = false;
        startPump("soil is dry");
      } else {
        screen(moisture, "Watered ", waterings);
  // ... 27 more lines in the editor

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The pump never starts. This project
    The tank sensor may read low, which blocks the pump. Fill the tank with its slider first.
  • The soil percent looks wrong for your sensor. This project
    Measure the reading in air and in a glass of water, and put them in SOIL_DRY and SOIL_WET.
  • The pump does not run even though the relay clicks. This project
    Check the pump circuit: battery plus to relay COM, relay NO to the pump, and battery minus joined to the Uno's ground.
  • The reading seems backward: wet soil gives a lower number. Soil moisture sensor
    A capacitive sensor reads lower when wetter. Measure it in dry air and in a glass of water, then map those two values to 0 and 100 percent.
  • The reading is unstable or the sensor corrodes. Soil moisture sensor
    Keep the electronics above the soil line and do not submerge past the marked line. Power it from 3.3 V or 5 V as the board allows, and average several readings.
  • The reading keeps changing and the probe traces corrode. Water level sensor
    Do not leave power on all the time. Power the sensor only when you read it, since DC current across wet traces causes corrosion.
  • The values are not the same depth to depth. Water level sensor
    The output depends on the water, so it is not a precise gauge. Read it with analogRead and calibrate with your own liquid at known levels. Keep the electronics above the waterline.
  • The input reads random HIGH and LOW when the button is not pressed. Pushbutton
    A floating pin picks up noise. Add a 10 k pull-down or pull-up resistor, or use INPUT_PULLUP and wire the button between the pin and GND. Then pressed reads LOW.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.