Soil
Intelligence
System
A progressive plant monitoring platform — from a single capacitive sensor to a fully autonomous watering system with tank management and pump control.
How it Works
The ESP32 reads raw ADC voltage from a capacitive soil moisture sensor — no metallic probes to corrode. It hosts a lightweight web server over WiFi, exposing sensor readings as JSON endpoints that any device on the network can query.
Capacitive Sensing
Measures dielectric permittivity of the soil. More water = higher capacitance = lower ADC voltage output. Resistant to corrosion, unlike resistive sensors.
WiFi Data Server
The ESP32 connects to your local WiFi and hosts an HTTP server. Any browser or app on the network can poll the sensor data via simple GET requests.
JSON Telemetry
Readings are served as structured JSON — moisture percentage, raw ADC value, and status. Easy to log, chart, or integrate with Home Assistant, Node-RED, etc.
Components & Wiring
This first iteration requires only four components. The capacitive sensor outputs an analog voltage between roughly 1.2V (wet) and 2.8V (dry) — values that map cleanly to the ESP32's 12-bit ADC.
ESP32 Dev Board
Any 30-pin or 38-pin ESP32. The built-in WiFi handles connectivity. Use GPIO34–39 for ADC as they are input-only with no conflicting functions.
Capacitive Moisture Sensor
The common v1.2 or v2.0 boards (blue PCB). Operates on 3.3V–5V. The AOUT pin carries the analog voltage to read. Avoid the DOUT digital pin for precise readings.
Jumper Wires + USB
Three wires for VCC, GND, and AOUT. A USB power supply or power bank can run the whole system continuously for field deployment.
| Sensor Pin | ESP32 Pin | Type | Notes |
|---|---|---|---|
| VCC | 3.3V | Power | Use 3.3V — ESP32 is not 5V tolerant on ADC pins |
| GND | GND | Ground | Any ground pin on the ESP32 |
| AOUT | GPIO34 | Analog In | Input-only pin, ideal for ADC. Use 34, 35, 36, or 39 |
analogSetAttenuation(ADC_11db) call in setup for the full 0–3.3V range.ESP32 Sketch
Upload this sketch via Arduino IDE with the ESP32 board package installed. Fill in your WiFi credentials. After booting, open the Serial Monitor at 115200 baud to find the assigned IP address.
// ============================================================ // Soil Intelligence System · v1.0 // ESP32 + Capacitive Moisture Sensor + WiFi Web Server // ============================================================ #include <WiFi.h> #include <WebServer.h> // --- WiFi Credentials --- const char* ssid = "YOUR_SSID"; const char* password = "YOUR_PASSWORD"; // --- Hardware --- const int SENSOR_PIN = 34; // ADC1 channel 6 (GPIO34) const int ADC_DRY = 2800; // Raw ADC value in dry air (calibrate!) const int ADC_WET = 1200; // Raw ADC value fully submerged in water const int READ_INTERVAL = 2000; // ms between readings WebServer server(80); // --- Globals --- int rawADC = 0; float moisturePct = 0.0; unsigned long lastRead = 0; // Map raw ADC to 0–100% moisture float adcToPercent(int raw) { raw = constrain(raw, ADC_WET, ADC_DRY); return (1.0 - ((float)(raw - ADC_WET) / (ADC_DRY - ADC_WET))) * 100.0; } // Return a human-readable status string String getStatus() { if (moisturePct > 70) return "WET"; if (moisturePct > 40) return "OPTIMAL"; if (moisturePct > 20) return "DRY"; return "CRITICAL"; } // GET /data → JSON sensor payload void handleData() { server.sendHeader("Access-Control-Allow-Origin", "*"); String json = "{"; json += "\"moisture\":" + String(moisturePct, 1) + ","; json += "\"raw_adc\":" + String(rawADC) + ","; json += "\"status\":\"" + getStatus() + "\","; json += "\"uptime_ms\":" + String(millis()) + ","; json += "\"ip\":\"" + WiFi.localIP().toString() + "\""; json += "}"; server.send(200, "application/json", json); } // GET / → simple browser dashboard void handleRoot() { String html = "<!DOCTYPE html><html><head>" "<meta http-equiv='refresh' content='5'>" "<title>Soil Monitor</title></head><body>" "<h1>Moisture: " + String(moisturePct, 1) + "% (" + getStatus() + ")" "</h1></body></html>"; server.send(200, "text/html", html); } void setup() { Serial.begin(115200); analogSetAttenuation(ADC_11db); // Full 0–3.3V range WiFi.begin(ssid, password); Serial.print("Connecting"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nIP: " + WiFi.localIP().toString()); server.on("/", handleRoot); server.on("/data", handleData); server.begin(); } void loop() { server.handleClient(); // Process incoming HTTP requests // Non-blocking sensor reads using millis() if (millis() - lastRead >= READ_INTERVAL) { rawADC = analogRead(SENSOR_PIN); moisturePct = adcToPercent(rawADC); lastRead = millis(); Serial.printf("Moisture: %.1f%% Raw: %d Status: %s\n", moisturePct, rawADC, getStatus().c_str()); } }
ADC_DRY and ADC_WET with your real readings — sensor boards vary significantly from batch to batch.Getting the Data
Once the ESP32 boots and connects to WiFi, it hosts an HTTP server on port 80. You can retrieve data from any browser, script, or home automation platform on the same network.
Returns full JSON payload: moisture %, raw ADC, status string, uptime, and device IP.
Simple auto-refreshing HTML page viewable in any browser. Good for a quick glance.
Example Responses
{
"moisture": 58.4,
"raw_adc": 1876,
"status": "OPTIMAL",
"uptime_ms": 483920,
"ip": "192.168.1.42"
}
const res = await fetch('http://192.168.1.42/data'); const data = await res.json(); console.log(`Moisture: ${data.moisture}% — ${data.status}`);
import requests, time ESP32_IP = "192.168.1.42" while True: r = requests.get(f"http://{ESP32_IP}/data") data = r.json() print(f"Moisture: {data['moisture']:.1f}% | {data['status']}") time.sleep(5)
rest sensor platform pointing at http://<ESP32_IP>/data with value_template: "{{ value_json.moisture }}" to pull moisture readings into your automations and dashboards.Progressive Iterations
The system is designed to grow incrementally. Each version adds a layer of autonomy, from passive monitoring through to a fully self-managing irrigation system.
The foundation. A single capacitive sensor feeding real-time data over WiFi. Deployable in a pot or garden bed. Useful standalone, and the base everything else builds on.
- Capacitive moisture reading via ADC (GPIO34)
- WiFi connection — static or DHCP IP
- HTTP server on port 80 with JSON endpoint
- Serial debug output for calibration
- Non-blocking loop with millis() timing
Add a small 5V submersible pump controlled by a relay or MOSFET. The ESP32 monitors moisture and triggers watering cycles automatically when the soil drops below a set threshold.
- 5V mini submersible pump (e.g. RS-360) or peristaltic pump
- 5V single-channel relay module or N-channel MOSFET (IRLZ44N)
- Water reservoir — any sealed container with a lid
- Silicone tubing, zip ties, drip emitter
- Configurable dry threshold (default 30%) via API endpoint
/config - Watering duration in seconds — avoid overwatering
- Cooldown timer between cycles (e.g. 10 min minimum)
- Manual trigger endpoint:
POST /water?seconds=5 - Watering event log stored in RTC memory or SPIFFS
The complete plant care platform. A water-level sensor on the reservoir prevents the pump from running dry, multiple soil sensors manage several plants independently, and an OLED display gives at-a-glance status without needing a phone.
- Tank level sensor — HC-SR04 ultrasonic (non-contact) or a float switch for simpler builds
- Low-water alert: stops pump if tank below 20%, sends HTTP notification
- Multi-sensor support: up to 4 zones via GPIO34, 35, 36, 39 (ADC1 channels)
- Per-zone pump channels — 4-channel relay board, one pump per plant bed
- SSD1306 OLED (128×64, I²C) showing moisture bars and tank level
- Physical override button — manual water trigger, or display cycle
- mDNS: access at
http://soilmonitor.localwithout knowing the IP - OTA firmware updates via WiFi using ArduinoOTA library
- MQTT publishing for Home Assistant / Node-RED integration
- Historical data stored to SPIFFS as CSV — downloadable via
/history.csv - Configurable web dashboard served directly from the ESP32
- Deep sleep mode for battery-powered remote deployments
Calibration & Tips
Two-Point Calibration
Read the ADC with the sensor in dry air → that's your ADC_DRY. Then hold the sensor in a glass of water (don't submerge the electronics) → that's ADC_WET. Typical range: 1200–2800.
ADC Averaging
The ESP32 ADC is noisy. Average 10–20 readings in rapid succession: for(i=0;i<16;i++) sum += analogRead(pin); raw = sum / 16; This reduces noise from ~30 counts to ~5 counts.
Read Timing
Don't read the sensor more than once per second — the capacitive plate needs time to settle after being polled. A 2-second interval is a good balance for continuous monitoring.
Temperature Drift
Capacitive sensors drift slightly with temperature. For precision applications, log an ambient temperature (using a DHT22 or DS18B20) and apply a calibration offset. For most plant care uses, this is unnecessary.
Waterproofing
The sensor PCB itself isn't waterproof. Coat the top component side with clear nail varnish or conformal coating, leaving the capacitive strip and the connector uncoated. Heatshrink over the cable helps too.
WiFi Stability
Add a watchdog / reconnection check in loop: if (WiFi.status() != WL_CONNECTED) WiFi.reconnect(); Optionally configure a static IP in your router's DHCP settings so the address never changes.