A capacitive soil moisture sensor on an ESP32 ADC1 pin. The ESP32 turns the raw reading into a percentage with your own dry and wet calibration, and serves a live dashboard to any phone on your WiFi, with a "Water me!" warning below 30 %. No app and no cloud account.
The video covers how the sensor turns water into a voltage, the 555 chip to check, why ADC2
stops reading while WiFi is on, calibration, and the page that polls /data. The plant,
phone and Serial Monitor in the video are an animation.
| From | To (ESP32) |
|---|---|
| Sensor VCC | 3V3 |
| Sensor GND | GND |
| Sensor AOUT | GPIO34 (ADC1) |
Libraries: WiFi and WebServer (both come with the ESP32 board package; nothing to install).
// ESP32 soil moisture monitor: capacitive sensor + a phone dashboard
// Wiring (Robot Maker Toolkit, ESP32): VCC -> 3V3, GND -> GND, AOUT -> GPIO34
#include <WiFi.h>
#include <WebServer.h>
const char* WIFI_NAME = "your-wifi";
const char* WIFI_PASS = "your-password";
const int SOIL_PIN = 34; // ADC1: keeps working while WiFi is on
const int DRY_RAW = 2900; // sensor in air (measure yours)
const int WET_RAW = 1250; // sensor in water (measure yours)
const int THIRSTY = 30; // below this %, the page says "Water me"
WebServer server(80);
int raw = 0;
int moisture = 0;
unsigned long lastRead = 0;
int readRaw() {
long sum = 0;
for (int i = 0; i < 16; i++) { // average 16 readings
sum += analogRead(SOIL_PIN);
delay(2);
}
return sum / 16;
}
int toPercent(int r) {
int p = map(r, DRY_RAW, WET_RAW, 0, 100); // dry reads HIGH, wet reads LOW
return constrain(p, 0, 100);
}
const char PAGE[] = R"html(
<!doctype html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Plant</title><style>
body{background:#111;color:#eee;font-family:sans-serif;text-align:center}
#pct{font-size:72px;margin:24px 0 8px}
#bar{width:70%;height:28px;margin:16px auto;border:2px solid #888;
border-radius:14px;overflow:hidden}
#fill{height:100%;width:0;background:#3c3;transition:width .5s}
</style></head><body>
<h2>Soil moisture</h2>
<div id="pct">--</div>
<div id="bar"><div id="fill"></div></div>
<div id="msg"></div>
<script>
async function tick() {
try {
const d = await (await fetch('/data')).json();
pct.textContent = d.moisture + ' %';
fill.style.width = d.moisture + '%';
fill.style.background = d.thirsty ? '#e44' : '#3c3';
msg.textContent = d.thirsty ? 'Water me!' : 'OK';
} catch (e) { msg.textContent = 'offline'; }
}
tick(); setInterval(tick, 2000);
</script></body></html>
)html";
void handleRoot() {
server.send(200, "text/html", PAGE);
}
void handleData() {
String json = "{\"moisture\":" + String(moisture) + ",\"raw\":" + String(raw);
json += ",\"thirsty\":" + String(moisture < THIRSTY ? "true" : "false") + "}";
server.send(200, "application/json", json);
}
void setup() {
Serial.begin(115200);
analogReadResolution(12); // 0..4095
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_NAME, WIFI_PASS);
Serial.print("Connecting");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Open http://");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/data", handleData);
server.begin();
}
void loop() {
server.handleClient();
if (millis() - lastRead >= 2000) { // a new reading every 2 s
lastRead = millis();
raw = readRaw();
moisture = toPercent(raw);
Serial.print("raw ");
Serial.print(raw);
Serial.print(" ");
Serial.print(moisture);
Serial.println(" %");
}
}
| Symptom | Cause | Fix |
|---|---|---|
| Stuck at 0 or 4095 | AOUT isn't on GPIO34, or it is on an ADC2 pin while WiFi is on | Move AOUT to GPIO34 (or another ADC1 pin) and set SOIL_PIN to match |
| Barely changes between air and water | An NE555 on the sensor running from 3.3 V | Use a sensor board with a TLC555 |
| Drifts from day to day | The sensor goes in to a different depth, or water reaches the top of the board | Push it in to the same depth every time, up to the line, and keep the top dry |
| Percentage is wrong | DRY_RAW and WET_RAW are the example values | Read the raw number in air and in water on the Serial Monitor and set both |
| The page won't open | The phone is on mobile data or another network, or the WiFi is 5 GHz | Join the same 2.4 GHz WiFi and use the address printed on the Serial Monitor (115200 baud) |
The capacitive soil sensor's wiring for UNO, ESP32 and Pico, with its warnings, is free in Robot Maker Toolkit, alongside 56 modules — all offline.
Get it onGoogle Play