Monitor a water tank with an ultrasonic sensor

Measure how full a tank or bucket is from above with the ultrasonic sensor, show the level in Blynk and get an alert when it runs low.
  • Level:Advanced
  • Duration: 1 h
  • Ages 13+
  • Platform: ESP32

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Monitor a water tank with an ultrasonic sensor – EEMENTOR STEM™ lesson

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

Breadboard wiring diagram: Monitor a water tank with an ultrasonic sensor
Breadboard view. Check each connection against the wiring table below.
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.

Example screen for: Monitor a water tank with an ultrasonic sensor
Example screen with sample values. Yours may look different.

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_TOKEN line 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_CM and FULL_CM again 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.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

ESP32 IoT Starter Kit | Arduino IDE Compatible | Free Tutorials $57.00

Shop the kit