Farms, rain barrels and fish tanks all need someone to check the water level. An ultrasonic sensor can do it without touching the water: mounted above the tank, it measures the distance down to the surface. The fuller the tank, the shorter the distance. In this lesson the ESP32 turns that distance into a percentage, shows it in the Blynk app and warns you when the level gets low.
What you'll learn
- How to measure a level from above with an ultrasonic sensor
- How to protect a 3.3 V input from a 5 V signal
- How to convert a distance into a fill level
- How to smooth readings with a median of several measurements
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- Ultrasonic sensor HC-SR04 (ESP32 IoT Starter Kit)
- 3 resistors of the same value, anything from 1 kΩ to 10 kΩ (ESP32 IoT Starter Kit)
- A bucket, box or large jar as the "tank"
- A free Blynk account and the Blynk IoT app on a phone
- A 2.4 GHz Wi-Fi network
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Protect the ESP32
The HC-SR04 runs on 5 V and answers with 5 V on its Echo pin, but the ESP32's pins are made for 3.3 V. Three equal resistors fix that: one from Echo to the ESP32 pin, and two in a row from the ESP32 pin to GND. The pin then sees two thirds of 5 V, which is about 3.3 V.
Wiring

| From | To |
|---|---|
| Ultrasonic sensor VCC | 5V |
| Ultrasonic sensor GND | GND |
| Ultrasonic sensor Trig | ESP32 GPIO 26 |
| Ultrasonic sensor Echo | Resistor R1 |
| Other end of R1 | ESP32 GPIO 27, and resistor R2 |
| Other end of R2 | Resistor R3 |
| Other end of R3 | GND |
Mount the sensor above the tank, looking straight down at the water, using female-to-male jumper wires. Keep the electronics well away from the water.
Set up Blynk
In the Blynk console, create a new template called Water tank (hardware ESP32, connection type Wi-Fi) and add these datastreams. Lesson 1 of this path shows each step in detail.
| Virtual pin | Name | Data type | Range | Phone widget |
|---|---|---|---|---|
| V0 | Level | Integer | 0 – 100, units % | Gauge |
| V1 | Distance | Integer | 0 – 400, units cm | Value Display |
| V2 | Status | String | n/a | Value Display |
Then open the template's Events & Notifications tab, add an event with the code tank_low (name: Water tank is low) 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) and two Value Displays (V1 and V2). Link each widget to its datastream.
Measure your tank
You need two numbers for the sketch. EMPTY_CM is the distance from the sensor to the bottom of the empty tank. FULL_CM is the distance from the sensor to the water when the tank is full. Keep at least 5 cm between the sensor and the highest water level: the sensor can't measure closer than about 2 cm.

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 "Water tank"
#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 TRIG_PIN = 26;
const int ECHO_PIN = 27;
const int EMPTY_CM = 40; // sensor to the bottom of the empty tank
const int FULL_CM = 8; // sensor to the water when the tank is full
const int LOW_PERCENT = 20; // alert below this level
BlynkTimer timer;
bool alertSent = false;
int measureOnce() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long echoTime = pulseIn(ECHO_PIN, HIGH, 30000); // give up after 30 ms
return echoTime == 0 ? -1 : echoTime / 58; // -1 = no echo
}
// Five measurements, sorted; the middle one ignores odd echoes
int measureCm() {
int v[5];
for (int i = 0; i < 5; i++) {
v[i] = measureOnce();
delay(30);
}
for (int i = 0; i < 5; i++)
for (int j = i + 1; j < 5; j++)
if (v[j] < v[i]) { int t = v[i]; v[i] = v[j]; v[j] = t; }
return v[2];
}
void checkTank() {
int cm = measureCm();
if (cm < 0) {
Blynk.virtualWrite(V2, "No echo: check the sensor");
return;
}
int level = constrain(map(cm, EMPTY_CM, FULL_CM, 0, 100), 0, 100);
Blynk.virtualWrite(V0, level);
Blynk.virtualWrite(V1, cm);
if (level < LOW_PERCENT) {
Blynk.virtualWrite(V2, "Low, please refill");
if (!alertSent) {
Blynk.logEvent("tank_low", "Tank is at " + String(level) + " %");
alertSent = true;
}
} else {
Blynk.virtualWrite(V2, "OK");
}
if (level > LOW_PERCENT + 10) alertSent = false; // refilled
}
void setup() {
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
timer.setInterval(5000L, checkTank); // every 5 seconds
}
void loop() {
Blynk.run();
timer.run();
}
How it works
The sensor sends a short burst of ultrasound and times the echo from the water surface: sound needs about 58 microseconds to travel one centimetre and back. map() then turns the distance into a level: EMPTY_CM becomes 0 % and FULL_CM becomes 100 %. The arguments are deliberately "backwards" (a bigger distance gives a smaller level), and map() handles that by itself.
Echoes from the tank wall or ripples on the water can give an odd reading now and then. The sketch measures five times, sorts the results and keeps the middle one, called the median. One wrong value can't move the median much, unlike an average.
Try this next
- Add a Chart widget to see how fast the tank empties.
- Calculate the volume in litres from the tank's length and width.
- Combine it with the plant watering capstone and use this sensor as the tank check.
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. - "No echo": check Trig on GPIO 26 and the divider on GPIO 27. Point the sensor straight at the water.
-
The level is wrong: measure
EMPTY_CMandFULL_CMagain from the front of the sensor. - The level jumps: narrow tanks cause echoes from the walls. Mount the sensor in the middle, or use a wider container.