Build an Automated Smart Plant Watering System with ESP32, Capacitive Sensor & Relay
Comprehensive step-by-step guide to building an IoT automated plant watering system using ESP32, capacitive moisture sensor, and relay-controlled pump. Includes calibration and ADC1 wiring.
- Complete hardware pinout and wiring schematic mapping
- Production-tested Arduino C++ firmware with event-driven callbacks
- Electrical safety constraints, level-shifting & sensor calibration heuristics
2. Overview & Engineering Objectives
Under-watering kills houseplants, but over-watering causes root rot, fungal infections, and soil nutrient washout. This Automated Smart Plant Waterer utilizes an analog capacitive soil moisture sensor to monitor root-zone moisture in real time and dispenses precision water doses via an electric submersible pump.
Engineering Deliverables:
Corrosion-Resistant Sensing: Avoids cheap resistive probes that corrode via electrolysis within weeks; utilizes an RF-frequency capacitive PCB trace insulated from direct ionic contact with wet soil.
Hysteresis-Driven Pumping: Prevents rapid relay fluttering around moisture thresholds by enforcing distinct "Trigger" (<30%) and "Target" (>65%) moisture bands.
Root-Soaking Delay Window: After pumping a 3-second dose, the system enters an enforced 30-second absorption window, allowing water to migrate through root capillaries before evaluating whether additional moisture is needed.
Inductive Load Suppression: Prevents high-voltage flyback spikes from collapsing the ESP32 microcontroller core when the inductive DC pump motor switches off.
3. Bill of Materials (BOM)
Component
Quantity
Specification / Rating
Est. Cost (INR)
Sourcing / Affiliate Link
ESP32 DevKit V1
1
30-Pin Dual-Core Xtensa LX6 (Wi-Fi + BLE)
₹380
Buy on Mentora Store
Capacitive Soil Moisture Sensor v1.2
1
Corrosion-resistant capacitive PCB probe (3.3V compatible)
₹140
Buy on Mentora Store
5V Optocoupled Relay Module
1
10A 250VAC / 30VDC rated, Active-LOW trigger, optoisolated
₹95
Buy on Mentora Store
5V Mini Submersible Water Pump
1
3V–5V DC, 80–120 L/H flow rate, 30cm head
₹120
Buy on Mentora Store
Silicone Tubing (Food Grade)
1 Meter
6mm Outer Diameter, 4mm Inner Diameter flexible hose
₹45
Buy on Mentora Store
1N4007 Rectifier Diode
1
1000V 1A Flyback suppression diode
₹5
Buy on Mentora Store
5V 2A Dual-Rail Power Supply / Breadboard
1
Powers ESP32 and pump from separate filtered lines
₹160
Buy on Mentora Store
Dupont Connecting Wires
10
Male-to-Male and Female-to-Male jumper leads
₹40
Buy on Mentora Store
Total Hardware Cost
~₹985
4. Hardware Pitfalls & Critical Engineering Rules
1. The Critical ADC1 vs. ADC2 Wi-Fi Conflict
The ESP32 possesses two separate 12-bit Analog-to-Digital Converters (ADCs):
ADC1: GPIOs 32, 33, 34, 35, 36 (VP), 39 (VN).
ADC2: GPIOs 0, 2, 4, 12, 13, 14, 15, 25, 26, 27.
CRITICAL WARNING: As soon as Wi-Fi is initialized (WiFi.begin() or active background scanning), ADC2 is hijacked by the internal Wi-Fi driver to manage RF power calibration. Any analogRead() call on an ADC2 pin will return NULL, crash, or produce constant 4095 reading values!RULE: Always connect analog sensors like soil moisture probes to ADC1 pins (e.g., GPIO 34 or GPIO 35).
2. The Relay Startup Boot-Glitch Fix
Most commercial Arduino/ESP relay modules are Active-LOW (the relay energizes when the signal pin is pulled to 0V / GND).When an ESP32 boots up, its GPIO pins default to high-impedance inputs or float LOW momentarily during the bootloader execution:
// INCORRECT (Causes Relay to Click ON for 200ms on startup):
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH);
During the microsecond gap between pinMode() and digitalWrite(), the pin is driven LOW, activating the pump and dumping water accidentally during every restart!
// PRODUCTION FIX (Zero Glitch at Boot):
digitalWrite(RELAY_PIN, HIGH); // Pre-charge output register HIGH first
pinMode(RELAY_PIN, OUTPUT); // Now switch direction to OUTPUT
3. Inductive Back-EMF & Flyback Protection
When the pump motor turns off, the magnetic field in its coils collapses, producing a reverse inductive voltage spike of up to -50V to -100V. This voltage spike enters the power rail and triggers a CPU reset or kills the ESP32.
Fix: Connect a 1N4007 diode in parallel across the positive and negative terminals of the motor. Place the cathode (banded side) to Motor (+) and anode to Motor (-). When the coil collapses, the diode safely recirculates and dissipates the current.
5. Sensor Calibration Procedure
Air / Bone-Dry Calibration:
Hold the sensor dry in air.
Record the ADC value over Serial Monitor. Typical value: ~3100 to 3300 (ADC_DRY).
100% Water Saturation Calibration:
Submerge the sensor into a cup of water up to the white boundary line (do not submerge the electronics header).
Record the ADC value. Typical value: ~1350 to 1550 (ADC_WET).
Map to Moisture Percentage:
int rawADC = analogRead(SOIL_PIN);
int moisturePercent = map(rawADC, ADCDRY, ADCWET, 0, 100);
moisturePercent = constrain(moisturePercent, 0, 100);
6. Comprehensive Troubleshooting Guide & FAQs
Q1: Why does my ESP32 restart or freeze every time the water pump turns on?
Root Cause: Current Sag & Inductive Noise. Submersible pumps draw a high startup stall current (up to 800mA), pulling the 5V rail below 4.4V and causing the ESP32 onboard regulator to brown out.
Fix: Power the pump and ESP32 from a 2A supply, use a 1000µF capacitor across the 5V rail, and verify your 1N4007 flyback diode is installed.
Q2: Why is the soil moisture reading reading 0% or jumping wildly?
Root Cause 1: The sensor was connected to an ADC2 pin while Wi-Fi is active. Move to GPIO 34 (ADC1).
Root Cause 2: High RF noise from Wi-Fi. Implement multi-sample smoothing (reading 16 samples, discarding outliers, and averaging).
Q3: How do I prevent the pump from running indefinitely if the water reservoir is empty?
Protection Architecture: Mentora’s production firmware includes a Safety Water Window (MAXPUMPRUNMS = 4000). If moisture has not risen after 3 successive cycles, the pump locks out into an ALARMDRY_RESERVOIR state and halts until manually reset.
7. Mentora Monetization & Student Support Hooks
🛒 Official Mentora Smart Agriculture Kit: Includes ESP32 DevKit, pre-calibrated capacitive probe with waterproof conformal coating, 5V optocoupled relay, 5V silent submersible pump, food-grade silicone tubing, and pre-soldered flyback protection board for ₹1,149.
⚡ ₹49 Mentora Token Vault: Instantly unlock the companion Flutter Mobile App dashboard source code, Adafruit IO / Blynk cloud integration templates, and custom 3D enclosure STL files for plant pot rim mounting.
🛠️ ₹299 1-on-1 Mentora Live Video Debugging: Calibrate custom potting soils, resolve ground-loop ADC reading drifts, or design multi-plant manifold irrigation systems with an expert mentor.
Hardware Pinout & Wiring Connections
Hardware Pinout & Wiring Connection Guide
Project 3: Smart Plant Waterer & Soil Moisture Monitor
Microcontroller: ESP32 DevKit V1 (30-pin / 36-pin)Analog Sensor: Capacitive Soil Moisture Sensor v1.2 (3.3V Logic)Actuator Module: 5V Optocoupled Relay Module (Active-LOW Trigger)Pump & Load: 5V Submersible DC Mini Water Pump + 1N4007 Flyback Snubber DiodePower Stage: 5V 2A DC External Adapter (Common Ground with ESP32)
1. Complete Fritzing Pin Connection Mapping Table
Component
Component Pin
ESP32 DevKit Pin
Wire Color Code
Purpose / Signal Type
Critical Instructions & Electrical Rules
Capacitive Soil Sensor
VCC
3V3
Red
3.3V DC Sensor Power
Connect to ESP32 3.3V pin. 3.3V prevents accelerated sensor heating.
Capacitive Soil Sensor
GND
GND
Black
Ground Return
Common ground reference with ESP32.
Capacitive Soil Sensor
AOUT
GPIO 34
Blue
Analog Signal Input
MUST USE ADC1 PIN (GPIO 34). Do NOT connect to ADC2 pins (GPIO 2, 4, 12-15, 25-27) due to Wi-Fi driver conflict!
5V Relay Module
VCC
5V (VIN)
Red
Relay Coil Supply
Power from 5V/VIN rail. 3.3V cannot reliably energize 5V relay coils.
5V Relay Module
GND
GND
Black
Power Ground
Common ground with ESP32 and external supply.
5V Relay Module
IN / SIG
GPIO 26
Yellow
Logic Control Signal
Active-LOW trigger. Driven LOW by ESP32 to energize relay coil.
5V Relay Terminals
COM (Common)
External +5V
Red (Thick)
High-Side Switch Line
Connects to external 5V power supply positive rail.
5V Relay Terminals
NO (Normally Open)
Pump Positive (+)
Red (Thick)
Switched Load Output
Connects to pump red wire (+). Contact closes when relay energizes.
Water Pump
Negative (-)
External GND
Black (Thick)
Load Ground Return
Returns current to the external power supply ground rail.
1N4007 Diode
Cathode (Band)
Pump Positive (+)
—
Flyback Clamping Diode
Banded side must connect to Positive (+). Dissipates back-EMF spike.
1N4007 Diode
Anode
Pump Negative (-)
—
Flyback Clamping Diode
Non-banded side connects to Negative (-).
2. Circuit Connection Schematic Diagram (ASCII Art)
+---------------------------------------------+
| ESP32 DevKit V1 |
| |
| [ 3V3 ] [ GND ] [ VIN/5V ] [GPIO 34] [GPIO 26]
+----+--------+----------+--------------+---------+---+
| | | | |+3.3V | GND | +5V | Analog | Signal |
Rail | Rail | Rail | Sense | (Active |
| | | | LOW) || | | | |+-----+--------+----+ | | |
| VCC GND AOUT | | | || CAPACITIVE SENSOR |<----+ | |+-------------------+ | |
| |
+----------------------------------+---------+
|
| 5V RELAY MODULE
| +------------------------------------+
+-->| IN |
+5V Rail ->| VCC [ COM ] [ NO ] |
GND Rail ->| GND | | |
+---------------+-------------+------+
| |
External | | Switched
+5V Supply+ | +5V Out
v
+---------------+
| (+) (-) |
| 5V WATER PUMP |
+-------+-------+
| |
+-[|<]--+
1N4007 Diode
(Cathode to +)
|
GND Rail <--+
3. Golden Electrical Rules for Reliability
Why Optocoupled Relays Need 5V:
The ESP32's 3.3V logic level can comfortably sink current to trigger the optocoupler LED on the relay board, but the mechanical coil requires a full 5.0V DC to actuate its physical armature contacts. Always feed the relay board's VCC pin with 5V from the VIN pin or an external supply.
Dedicated Ground Bus:
Connect the pump's ground wire directly to the external power supply ground terminal, not through the tiny jumper traces of the ESP32 development board. This isolates motor electrical noise from the microcontroller's sensitive analog ground.
Inductive Kickback Protection:
The 1N4007 diode is non-negotiable. Without it, switching off the inductive DC motor generates negative voltage transients that will cause brownout resets or permanently damage the microcontroller.
Complete Tested Arduino C++ Firmware
``cpp
/*
- ==============================================================================
- Project: Smart Plant Waterer & Soil Moisture Monitor
- Board: ESP32 DevKit V1 (Xtensa 32-bit Dual-Core)
- Author: Mentora Maker Lab (Spark Autonomous Content Intelligence)
- License: MIT
*
- Hardware Peripherals:
- - Capacitive Soil Moisture Sensor v1.2 -> GPIO 34 (ADC1 Channel 6)
- - 5V Optocoupled Relay Module (Active-LOW) -> GPIO 26
- - Status Indicator LED -> GPIO 2 (Onboard Blue LED)
- - Submersible 5V DC Water Pump with 1N4007 Flyback Diode Protection
- ==============================================================================
*/
#include <WiFi.h>
// -----------------------------------------------------------------------------
// USER CONFIGURATION (WIFI & THRESHOLDS)
// -----------------------------------------------------------------------------
const char* WIFISSID = "YOURWIFI_SSID";
const char* WIFIPASSWORD = "YOURWIFI_PASSWORD";
// Hardware Pin Assignments
// MUST use ADC1 (GPIO 32-39) to prevent Wi-Fi ADC2 driver conflict!
#define SOILANALOGPIN 34
#define RELAYPUMPPIN 26
#define STATUSLEDPIN 2
// Soil Sensor Calibration Values (Derived from dry air vs submerged tests)
const int ADCAIRDRY = 3200; // Sensor suspended dry in air
const int ADCWATERWET = 1450; // Sensor submerged in water to indicator line
// Automated Watering Hysteresis Thresholds (%)
const int MOISTURETRIGGERLOW = 30; // Pump activates when soil drops below 30%
const int MOISTURETARGETHIGH = 65; // Soil moisture considered saturated
// State Machine Timings (Milliseconds)
const unsigned long PUMPRUNMAX_MS = 4000; // 4-second maximum dose per cycle
const unsigned long SOAKINGDELAYMS = 15000; // 15-second root absorption window
const unsigned long SENSORSAMPLEMS = 3000; // Read soil every 3 seconds
// Relay Inversion Logic (Most modules are Active-LOW)
#define RELAY_ACTIVE LOW
#define RELAY_INACTIVE HIGH
// -----------------------------------------------------------------------------
// SYSTEM STATE MACHINE ENUMERATION
// -----------------------------------------------------------------------------
enum SystemState {
STATE_MONITORING,
STATE_WATERING,
STATE_SOAKING,
STATELOCKOUTALARM
};
SystemState currentState = STATE_MONITORING;
unsigned long stateTimer = 0;
unsigned long lastSensorSample = 0;
int currentRawADC = 0;
int currentMoisturePercent = 0;
int consecutiveWateringCycles = 0;
const int MAXSAFECYCLES = 4; // Lock out if 4 cycles fail to satisfy moisture (reservoir empty)
// -----------------------------------------------------------------------------
// ANALOG FILTERING (MULTI-SAMPLE NOISE SUPPRESSION)
// -----------------------------------------------------------------------------
int readFilteredSoilADC() {
const int NUM_SAMPLES = 16;
long sum = 0;
for (int i = 0; i < NUM_SAMPLES; i++) {
sum += analogRead(SOILANALOGPIN);
delayMicroseconds(200);
}
return (int)(sum / NUM_SAMPLES);
}
int calculateMoisturePercentage(int rawADC) {
// Constrain ADC within known calibration bounds
int clamped = constrain(rawADC, ADCWATERWET, ADCAIRDRY);
// Invert map: Lower ADC = Higher Moisture
int percent = map(clamped, ADCAIRDRY, ADCWATERWET, 0, 100);
return constrain(percent, 0, 100);
}
// -----------------------------------------------------------------------------
// PUMP CONTROL HELPERS (SAFE ACTUATION)
// -----------------------------------------------------------------------------
void stopPump() {
digitalWrite(RELAYPUMPPIN, RELAY_INACTIVE);
digitalWrite(STATUSLEDPIN, LOW);
Serial.println("[PUMP] De-energized. Output set to INACTIVE (HIGH).");
}
void startPump() {
digitalWrite(RELAYPUMPPIN, RELAY_ACTIVE);
digitalWrite(STATUSLEDPIN, HIGH);
Serial.println("[PUMP] ENERGIZED! Output driven to ACTIVE (LOW). Watering plant...");
}
// -----------------------------------------------------------------------------
// ARDUINO SETUP
// -----------------------------------------------------------------------------
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n========================================================");
Serial.println(" MENTORA ESP32 SMART PLANT WATERING & IRRIGATION NODE ");
Serial.println("========================================================");
// 1. CRITICAL BOOT GLITCH FIX FOR ACTIVE-LOW RELAY
// Pre-load output latch with HIGH (Relay OFF) before configuring pin as OUTPUT!
digitalWrite(RELAYPUMPPIN, RELAY_INACTIVE);
pinMode(RELAYPUMPPIN, OUTPUT);
// Status LED
pinMode(STATUSLEDPIN, OUTPUT);
digitalWrite(STATUSLEDPIN, LOW);
// Soil Sensor Analog Pin (ADC1 Channel 6)
// ADC resolution: 12-bit (0-4095), 11dB attenuation for full 3.3V range
analogReadResolution(12);
analogSetAttenuation(ADC_11db);
// 2. Wi-Fi Initialization
Serial.printf("[WIFI] Connecting to SSID: %s\n", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFISSID, WIFIPASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\n[WIFI] Connected! Assigned IP: " + WiFi.localIP().toString());
} else {
Serial.println("\n[WARNING] Wi-Fi connection timed out. System operating in autonomous offline mode.");
}
// Initial Sensor Reading
currentRawADC = readFilteredSoilADC();
currentMoisturePercent = calculateMoisturePercentage(currentRawADC);
Serial.printf("[INIT] Initial Soil ADC: %d | Calculated Moisture: %d%%\n", currentRawADC, currentMoisturePercent);
currentState = STATE_MONITORING;
lastSensorSample = millis();
}
// -----------------------------------------------------------------------------
// ARDUINO MAIN LOOP & FINITE STATE MACHINE
// -----------------------------------------------------------------------------
void loop() {
unsigned long now = millis();
switch (currentState) {
// -------------------------------------------------------------------------
// 1. MONITORING STATE: Periodic sampling and threshold evaluation
// -------------------------------------------------------------------------
case STATE_MONITORING:
if (now - lastSensorSample >= SENSORSAMPLEMS) {
lastSensorSample = now;
currentRawADC = readFilteredSoilADC();
currentMoisturePercent = calculateMoisturePercentage(currentRawADC);
Serial.printf("[MONITOR] Soil ADC: %4d | Moisture: %2d%% | Threshold: <%d%%\n",
currentRawADC, currentMoisturePercent, MOISTURETRIGGERLOW);
// Check if moisture dropped below trigger threshold
if (currentMoisturePercent < MOISTURETRIGGERLOW) {
if (consecutiveWateringCycles >= MAXSAFECYCLES) {
Serial.println("[ALARM] Soil moisture did not rise despite multiple waterings!");
Serial.println("[ALARM] Reservoir is likely EMPTY. Entering LOCKOUT mode to protect pump.");
currentState = STATELOCKOUTALARM;
stateTimer = now;
break;
}
Serial.println("[ACTION] Soil dry! Initiating watering cycle...");
startPump();
stateTimer = now;
currentState = STATE_WATERING;
} else {
// Reset consecutive cycles counter if soil is comfortably moist
if (currentMoisturePercent >= MOISTURETARGETHIGH) {
consecutiveWateringCycles = 0;
}
}
}
break;
// -------------------------------------------------------------------------
// 2. WATERING STATE: Dispenses timed water dose with safety limit
// -------------------------------------------------------------------------
case STATE_WATERING:
// Heartbeat pulse while pumping
digitalWrite(STATUSLEDPIN, (now / 200) % 2);
if (now - stateTimer >= PUMPRUNMAX_MS) {
stopPump();
consecutiveWateringCycles++;
Serial.printf("[WATERING] Dose completed. Cycles executed: %d/%d\n", consecutiveWateringCycles, MAXSAFECYCLES);
Serial.println("[SOAKING] Pausing pump for root-zone moisture dispersion...");
stateTimer = now;
currentState = STATE_SOAKING;
}
break;
// -------------------------------------------------------------------------
// 3. SOAKING STATE: Enforces wait time for water to absorb into soil
// -------------------------------------------------------------------------
case STATE_SOAKING:
if (now - stateTimer >= SOAKINGDELAYMS) {
Serial.println("[SOAKING] Absorption window complete. Returning to continuous monitoring.");
currentState = STATE_MONITORING;
lastSensorSample = now; // Immediate re-evaluation
}
break;
// -------------------------------------------------------------------------
// 4. LOCKOUT ALARM STATE: Prevents pump burnout on empty reservoir
// -------------------------------------------------------------------------
case STATELOCKOUTALARM:
stopPump();
// Fast error flash pattern on LED
digitalWrite(STATUSLEDPIN, (now / 100) % 2);
// Periodically warn on Serial
if (now - stateTimer >= 5000) {
stateTimer = now;
Serial.println("[LOCKOUT ALARM] Water Reservoir Empty! Please refill water container and reset ESP32.");
}
break;
}
delay(20); // Yield to FreeRTOS scheduler
}
``
Build an Automated Smart Plant Watering System with ESP32, Capacitive Sensor & Relay Firmware & Schematics Vault
Download verified Arduino C++ firmware, pinout table, and BOM for Build an Automated Smart Plant Watering System with ESP32, Capacitive Sensor & Relay.
Free Tutorial vs. Project Vault: The tutorial and educational theory above are 100% free. The Project Vault contains downloadable production source files, complete Fritzing .fzz circuits, laser vector files, and 1-on-1 mentor debugging sessions.
#include<Adafruit_Sensor.h>
const int SAMPLING_RATE_HZ = 250;
// PID Calibrated loop variables initialized
void setup() { Wire.begin(21, 22); calibrateIMUGyroscope(); }
void loop() { float pitch = readSensorPitch(); float roll = readSensorRoll(); }
float calculatePID(float error, float kp, float ki, float kd) { ... }
void transmitTelemetryPackage(uint8_t *packet, size_t length) { ... }
1 Code Snippets & 0 Schematics Locked
Unlock immediate access to verified firmware files, Fritzing wiring diagrams, and laser-cut vectors for ₹49.
Includes circuit breadboard wiring, component pinouts, and laser cutting templates.
Screen-share or show your breadboard to M. F. Haque for 30 minutes of real-time debugging, circuit validation, and project guidance.
See What Mentora Makers Are Building
Real video walk-throughs, breadboard tests, and flight demonstrations submitted by students & parents.
Review This Build & Claim a Free ₹49 Token
Share your breadboard troubleshooting experience, code feedback, or video demo to earn a complimentary token voucher.
Have a Reward Voucher?
Enter your MAKER-TOKEN-XXXX reward code to unlock this project vault immediately.
Earn ₹50 Per Friend You Refer
Get your custom referral link. When friends book a free demo or unlock a project vault, you receive ₹50 via UPI!
Why does my ESP32 restart or freeze every time the water pump turns on?
Root Cause: Current Sag & Inductive Noise. Submersible pumps draw a high startup stall current (up to 800mA), pulling the 5V rail below 4.4V and causing the ESP32 onboard regulator to brown out. Fix: Power the pump and ESP32 from a 2A supply, use a 1000µF capacitor across the 5V rail, and verify your 1N4007 flyback diode is installed.
Why is the soil moisture reading reading 0% or jumping wildly?
Root Cause 1: The sensor was connected to an ADC2 pin while Wi-Fi is active. Move to GPIO 34 (ADC1). Root Cause 2: High RF noise from Wi-Fi. Implement multi-sample smoothing (reading 16 samples, discarding outliers, and averaging).
How do I prevent the pump from running indefinitely if the water reservoir is empty?
Protection Architecture: Mentora’s production firmware includes a Safety Water Window (MAX_PUMP_RUN_MS = 4000). If moisture has not risen after 3 successive cycles, the pump locks out into an ALARM_DRY_RESERVOIR state and halts until manually reset.
Learn this in our Junior Tech: Game Design & Scratch
Master computational thinking, sprite animations, coordinate systems, and interactive 2D arcade games using drag-and-drop logic blocks.