ESP32 Project Documentation · v1.0 → v3.0 Roadmap

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.

ESP32 Capacitive Sensor WiFi · REST API JSON Telemetry Future: Pump Control Future: Tank Level
SCROLL TO EXPLORE

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
The ESP32 ADC has a known non-linearity near 3.3V. Power the sensor from the 3.3V rail (not 5V) so the output stays within the 0–3.3V range. Consider adding an 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.

main.ino
// ============================================================
//  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());
  }
}
💡
Calibration tip: Before using, record ADC values with the sensor held in dry air and fully submerged in water. Update 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.

GET http://<ESP32_IP>/data

Returns full JSON payload: moisture %, raw ADC, status string, uptime, and device IP.

GET http://<ESP32_IP>/

Simple auto-refreshing HTML page viewable in any browser. Good for a quick glance.

Example Responses

GET /data response
{
  "moisture":  58.4,
  "raw_adc":   1876,
  "status":    "OPTIMAL",
  "uptime_ms": 483920,
  "ip":        "192.168.1.42"
}
Fetch with JavaScript
const res  = await fetch('http://192.168.1.42/data');
const data = await res.json();
console.log(`Moisture: ${data.moisture}% — ${data.status}`);
Python polling script
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)
🏠
Home Assistant integration: Use the rest sensor platform pointing at http://<ESP32_IP>/data with value_template: "{{ value_json.moisture }}" to pull moisture readings into your automations and dashboards.
Simulated Live Data Feed
SIMULATED · LOCAL DEMO
Soil Moisture
%
Raw ADC
0–4095 (12-bit)
DRYOPTIMALWET
0–20% CRITICAL
20–40% DRY
40–70% OPTIMAL
70–100% WET

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.

v1
Basic Soil Monitor
Current Build · WiFi · REST API

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
v2
Automated Watering
Next Iteration · Pump Control · Threshold Logic

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
🔌
Use a MOSFET rather than a relay for low-power 5V pumps — no audible click, no mechanical wear, and the ESP32 GPIO drives it directly. Connect the pump's negative to the MOSFET drain, positive to 5V supply, gate to GPIO via a 100Ω resistor, and add a flyback diode across the pump.
v3
Full Autonomous System
Future Build · Tank Level · Multi-Zone · Dashboard

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.local without 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
🚰
Ultrasonic tank level tip: Mount the HC-SR04 facing downward at the tank lid. The ESP32 triggers a 10µs pulse on TRIG, then measures echo pulse width on ECHO. Distance = (pulse_µs × 0.034) / 2 cm. Subtract from tank height to get water depth. Add a 1kΩ / 2kΩ voltage divider on the ECHO pin — the HC-SR04 outputs 5V logic, which would damage the ESP32 GPIO.

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.