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.
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.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × Half breadboard
- 1 × Soil moisture sensor, capacitive
- 1 × Water level sensor
- 1 × PushbuttonIn Starter Kit, Christmas Lights Kit, Logic Lab Kit.
- 1 × Relay moduleIn Halloween Prop Kit.
- 1 × DC motor
- 1 × Battery: AA, coin cell, LiPoIn Handheld Console Kit, Laser Sword Kit.
- 1 × Character LCD with I2C backpackIn Claw Machine Kit, Vending Machine Kit, Rhythm Game Kit.
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 pin | Connects to |
|---|---|
| Relay module pin IN | Arduino Uno R3 pin 7 |
| Pushbutton pin 1a | Arduino Uno R3 pin 2 |
| Soil moisture sensor, capacitive pin VCC | Arduino Uno R3 pin 5V |
| Water level sensor pin VCC | Arduino Uno R3 pin 5V |
| Relay module pin VCC | Arduino Uno R3 pin 5V |
| Character LCD with I2C backpack pin VCC | Arduino Uno R3 pin 5V |
| Soil moisture sensor, capacitive pin GND | Arduino Uno R3 pin GND |
| Water level sensor pin GND | Arduino Uno R3 pin GND |
| Pushbutton pin 2a | Arduino Uno R3 pin GND |
| Relay module pin GND | Arduino Uno R3 pin GND |
| DC motor pin 2 | Arduino Uno R3 pin GND |
| Battery: AA, coin cell, LiPo pin V- | Arduino Uno R3 pin GND |
| Character LCD with I2C backpack pin GND | Arduino Uno R3 pin GND |
| Soil moisture sensor, capacitive pin AOUT | Arduino Uno R3 pin A0 |
| Water level sensor pin S | Arduino Uno R3 pin A1 |
| Character LCD with I2C backpack pin SDA | Arduino Uno R3 pin A4 |
| Character LCD with I2C backpack pin SCL | Arduino Uno R3 pin A5 |
| DC motor pin 1 | Relay 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.
// 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 editorTroubleshooting
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 inSOIL_DRYandSOIL_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.