Plants don't tell you when they're thirsty, but a soil moisture sensor can. In this lesson the ESP32 measures how wet the soil is every ten seconds, shows it as a percentage in the Blynk app, and sends a notification when the soil gets too dry. You calibrate the sensor for your own soil and choose the alert level with a slider.
What you'll learn
- How to calibrate an analog sensor into a percentage
- How to power a sensor only while measuring
- How to send text to a Value Display widget
- How to choose a sensible alert level
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- Soil moisture module with probe (45-in-1 Sensor Kit)
- A plant in a pot, and a glass of water for calibration
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
- A free Blynk account and the Blynk IoT app on a phone
- A 2.4 GHz Wi-Fi network
Wiring

| From | To |
|---|---|
| Soil module VCC | ESP32 GPIO 32 (powers the sensor only while measuring) |
| Soil module GND | GND |
| Soil module AO | ESP32 GPIO 35 |
| Soil module DO | Not used |
| Probe | The two pins on the other side of the module board |
Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.
Keep the ESP32, the breadboard and the module board dry. Only the fork-shaped probe goes into the soil, up to the line where the metal ends.
Set up Blynk
In the Blynk console, create a new template called Plant monitor (hardware ESP32, connection type Wi-Fi) and add these datastreams. Lesson 1 shows each step in detail.
| Virtual pin | Name | Data type | Range | Phone widget |
|---|---|---|---|---|
| V0 | Moisture | Integer | 0 – 100, units % | Gauge |
| V1 | Dry limit | Integer | 10 – 60, units % | Slider |
| V2 | Status | String | n/a | Value Display |
Then open the template's Events & Notifications tab, add an event with the code plant_dry (name: Plant needs water) and turn on push notifications for it. The event code must match the sketch exactly.
Save the template, create a new device from it and copy its three BLYNK_… lines. In the phone app, open the device, tap the wrench icon and add a Gauge (V0), a Slider (V1) and a Value Display (V2). Link each widget to its datastream.

The sketch
Replace the three BLYNK_… lines at the top with the ones from your device's Device info page, and add your Wi-Fi name and password (2.4 GHz only). The BLYNK_… lines must stay above #include <BlynkSimpleEsp32.h>.
#define BLYNK_TEMPLATE_ID "YOUR_TEMPLATE_ID"
#define BLYNK_TEMPLATE_NAME "Plant monitor"
#define BLYNK_AUTH_TOKEN "YOUR_AUTH_TOKEN"
#define BLYNK_PRINT Serial // show connection messages in the Serial Monitor
#include <WiFi.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>
char ssid[] = "YOUR_WIFI_NAME";
char pass[] = "YOUR_WIFI_PASSWORD";
const int SOIL_PIN = 35;
const int POWER_PIN = 32;
int DRY_READING = 3500; // reading with the probe in dry air (calibrate!)
int WET_READING = 1400; // reading with the probe in a glass of water
BlynkTimer timer;
int dryLimit = 30;
bool alertSent = false;
BLYNK_WRITE(V1) { dryLimit = param.asInt(); }
BLYNK_CONNECTED() { Blynk.syncVirtual(V1); }
int readMoisture() {
digitalWrite(POWER_PIN, HIGH); // power on only while measuring
delay(20);
int raw = analogRead(SOIL_PIN);
digitalWrite(POWER_PIN, LOW);
Serial.print("Raw reading: ");
Serial.println(raw);
return constrain(map(raw, DRY_READING, WET_READING, 0, 100), 0, 100);
}
void checkPlant() {
int moisture = readMoisture();
Blynk.virtualWrite(V0, moisture);
if (moisture < dryLimit) {
Blynk.virtualWrite(V2, "Needs water");
if (!alertSent) {
Blynk.logEvent("plant_dry", "Soil moisture is " + String(moisture) + " %");
alertSent = true;
}
} else {
Blynk.virtualWrite(V2, "OK");
}
if (moisture > dryLimit + 10) alertSent = false; // watered: ready for the next alert
}
void setup() {
Serial.begin(115200);
pinMode(POWER_PIN, OUTPUT);
digitalWrite(POWER_PIN, LOW);
Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
timer.setInterval(10000L, checkPlant); // every 10 seconds
}
void loop() {
Blynk.run();
timer.run();
}
Calibrate the sensor
- Upload the sketch and open the Serial Monitor at 115200 baud.
- Hold the probe in the air and note the raw reading. Put it in
DRY_READING. - Dip the probe in a glass of water up to the line and note the reading. Put it in
WET_READING. - Upload again and push the probe into the soil next to your plant.
How it works
Water conducts electricity, so wet soil lets more current flow between the two prongs of the probe. The module turns that into a voltage on AO: a high reading means dry, a low reading means wet. map() uses your two calibration readings to turn the raw number into 0–100 %, and constrain() keeps it in that range. The sensor is only powered for 20 ms every ten seconds, which slows down the corrosion of the probe. The alert resets only after the moisture rises 10 % above the limit, so a reading that hovers around the limit doesn't send a stream of notifications.
Try this next
- Measure every 30 minutes instead, since soil dries slowly.
- Add a Chart widget to see how fast the soil dries after watering.
- Add the DHT11 to the same project and send air temperature and humidity too.
Troubleshooting
-
The device shows "Offline" in the app: check the Serial Monitor at 115200 baud. "Invalid auth token" means the
BLYNK_AUTH_TOKENline is wrong; if it never connects, check the Wi-Fi name and password, and that your network is 2.4 GHz. - The value is always 0 % or 100 %: calibrate again; your readings may be outside the default numbers.
- The reading jumps a lot: push the probe deeper and firmly into the soil so both prongs touch it.
- The probe turns dark over weeks: that's corrosion. Clean it gently; this kind of probe wears out over time.