Store
Training/ESP32 & ESP8266

Build an ESP32 water-tank monitor: see the level, understand the echo

Turn an ultrasonic echo into a useful water-level reading. Build a tabletop monitor, calibrate it, try the interactive tank and add a low-water alarm without alarm chatter.

KKadosh Build24 October 2026⏱ 5 min readBeginner1 views

On this page

0% read

A tank can look full from the outside and be almost empty inside. In this build, an ESP32 measures the air gap above the water, converts that distance to a percentage and lights an LED when the level becomes low.

Start with a dry-bench tabletop container, not a roof tank. The goal is to get the measurement right before adding a wireless display or pump control. You need basic breadboard skills and about 60–90 minutes. No mains wiring or pump switching is used.

First, make the measurement click

Try it yourself

Imagine measuring the empty space above a drink. A fuller glass leaves a shorter gap. The sensor measures that gap with an echo; it never measures the water directly.

Air gap: 15.0 cmEcho: 875 µsLevel: 60%Level OKEmpty gap 30 cm · full gap 5 cm

Air gap 15.0 cm · round-trip echo 875 µs

Lower the level to 20% to trigger the alarm.

Try 19%, then 22%, then 26%. The separate on/off thresholds prevent alarm chatter. This model assumes a vertical, constant-area tank; it is not an ultrasonic physics simulator.

Move the slider slowly through 20–25%. Then remove the echo and notice that a sensor fault does not become a zero-percent reading.

The ultrasonic module sends a short sound burst and waits for the reflection. Like calling into a valley, the measured delay contains both the outward and return journeys. Divide the sound travel distance by two.

The drink analogy explains the direction of change: more water means a smaller gap. It does not explain echoes from tank walls, splashing or foam. Those are real mounting and surface problems that this simple model leaves out.

Gather the parts

🧰 Build kit

  • 1×ESP32 development boardClassic ESP32 DevKit / ESP32-WROOM target; USB cableGet it →
  • 1×HC-SR04 ultrasonic sensorStandard 5 V version; keep it dryGet it →
  • 1×10k resistorUpper leg of the ECHO dividerGet it →
  • 1×20k resistorLower leg of the ECHO dividerGet it →
  • 1 each×LED and 220 ohm resistorExternal alarm indicatorGet it →
  • 1 set×Breadboard and jumper wiresPlus a stable sensor mount, ruler and containerGet it →
!

Protect the ESP32 input

The standard HC-SR04 uses 5 V. Its ECHO signal must pass through the 10 kΩ / 20 kΩ divider before GPIO19. Do not connect ECHO directly to an ESP32 GPIO. Unplug USB while changing wires. Keep the board and breadboard away from splashes.

Wire it with the power disconnected

Why the echo needs a voltage divider

ECHO 0 V10 kΩ20 kΩESP32 GPIO190 V5 V × 20 kΩ / (10 kΩ + 20 kΩ) ≈ 3.33 V

ECHO low: With ECHO at 0 V, the divider junction is also at 0 V.

Follow the pulse through the divider. The upper resistor is 10 kΩ; the lower resistor to common ground is 20 kΩ.

Pin connection map showing the HC-SR04 echo voltage divider, GPIO18 trigger, GPIO19 echo and GPIO23 alarm LED.
The resistor junction is the measurement point. Verify the pin names printed on your own board.

Connect ECHO to the 10 kΩ resistor, then the other end of that resistor to GPIO19. Connect 20 kΩ from the same GPIO19 junction to GND. The division is 5 × 20 / (10 + 20), approximately 3.33 V. Both sensor and ESP32 must share ground.

Power the module from a verified regulated 5 V rail. Some development boards label that rail 5V or VIN; check the board documentation before using it as an output. If you use a separate sensor supply, join the grounds and avoid back-feeding USB. TRIG is driven from GPIO18. The LED anode connects through 220 Ω to GPIO23; the cathode goes to GND.

Printable wiring sheet

A one-page connection map you can keep beside the breadboard.

tank-wiring.pdf · PDF · 76 KB
Download Printable wiring sheet↓

Mount the sensor and record two distances

Mount it, send the burst, read the echo

Horizontal sensor PCBAir gapBoth acoustic openings face the water, not the viewer

Face down: The PCB lies parallel to the water. Both acoustic openings face straight down, centred over the surface.

Side cutaway: transducer faces point vertically down at the calm water surface. The sound path is schematic, not drawn to scale.

Hold the sensor horizontal and looking straight down. Its face must stay above the highest intended water surface; keep at least a 5 cm gap in this tabletop exercise. The standard HC-SR04 board is not waterproof. A real outdoor installation needs a suitable enclosure and a sensor designed for that environment.

Measure from the sensor face to the empty reference surface: call that EMPTY_CM. Add water to the highest level you want to count as full, and measure again: FULL_CM. The example uses 30 cm and 5 cm. Replace those numbers with your measurements; copying them blindly gives a convincing but wrong percentage.

QuantityExampleMeaning
EMPTY_CM30 cmLargest calibrated air gap
FULL_CM5 cmSmallest calibrated air gap
Water level100 × (30 − gap) / 25Clamp the result to 0–100%
Alarm thresholdsOn ≤20%; off ≥25%A 5-point hysteresis band
✦

Percentage of what?

This is calibrated water height. It equals percentage of volume only when the container has a constant horizontal cross-sectional area. A horizontal cylinder or irregular tank needs its own height-to-volume conversion.

Upload the sketch

Side-view animation of downward-facing ultrasonic transducers transmitting toward the water and receiving the return echo.
Animated walkthrough
Watch the complete echo path before inspecting the code. The playback is slowed down for explanation.

Install the Espressif ESP32 board package in Arduino IDE. Select ESP32 Dev Module for a classic ESP32 DevKit and choose its USB port. This sketch was compile-checked with Arduino-ESP32 3.3.8. It uses the built-in Arduino functions, so no extra sensor library is required.

Open the sketch below, set your two calibration distances, upload and open Serial Monitor at 115200 baud. A good reading looks like 17.5,50.0,0: distance in centimetres, percentage and alarm state. These are calculated examples, not measurements from your tank.

tank_monitor.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* Kadosh Build: ESP32 tank monitor. Classic ESP32 DevKit; Arduino-ESP32 3.x.
   HC-SR04 ECHO -> 10k resistor -> GPIO19; GPIO19 -> 20k -> GND.
   Never connect a 5V ECHO signal directly to the ESP32.
   Original example code, MIT license. */
#include <Arduino.h>
#include <math.h>
constexpr uint8_t TRIG = 18, ECHO = 19, ALARM_LED = 23;
// Measure both distances from the sensor face, not from the tank rim.
constexpr float EMPTY_CM = 30.0f, FULL_CM = 5.0f;
constexpr float ALARM_ON_PCT = 20.0f, ALARM_OFF_PCT = 25.0f;
float filtered = NAN;
bool alarmLow = false;
uint32_t previous = 0;
unsigned invalidReadings = 0;

float readDistanceCm() {
  digitalWrite(TRIG, LOW); delayMicroseconds(2);
  digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW);
  const unsigned long echoUs = pulseIn(ECHO, HIGH, 30000UL);
  if (!echoUs) return NAN;
  const float cm = echoUs * 0.0343f / 2.0f;
  return cm >= 2.0f && cm <= 400.0f ? cm : NAN;
}
void setup() {
  Serial.begin(115200);
  pinMode(TRIG, OUTPUT); pinMode(ECHO, INPUT); pinMode(ALARM_LED, OUTPUT);
  digitalWrite(TRIG, LOW); digitalWrite(ALARM_LED, LOW);
  if (EMPTY_CM <= FULL_CM) {
    Serial.println("Calibration error: EMPTY_CM must exceed FULL_CM");
    while (true) delay(1000);
  }
  Serial.println("distance_cm,level_pct,low_alarm");
}
void loop() {
  const uint32_t now = millis();
  if (now - previous < 250) return;
  previous = now;
  const float distance = readDistanceCm();
  if (isnan(distance)) {
    ++invalidReadings;
    Serial.println("FAULT,no_echo_or_out_of_range");
    // Keep the existing low-level alarm; a fault is not a new level measurement.
    // After repeated faults, discard smoothing so recovery starts with fresh data.
    if (invalidReadings >= 4) filtered = NAN;
    return;
  }
  invalidReadings = 0;
  filtered = isnan(filtered) ? distance : .2f * distance + .8f * filtered;
  const float level = constrain(100.0f * (EMPTY_CM - filtered) / (EMPTY_CM - FULL_CM), 0.0f, 100.0f);
  if (level <= ALARM_ON_PCT) alarmLow = true;
  else if (level >= ALARM_OFF_PCT) alarmLow = false;
  digitalWrite(ALARM_LED, alarmLow ? HIGH : LOW);
  Serial.printf("%.1f,%.1f,%d\n", filtered, level, alarmLow ? 1 : 0);
}
Download the tank-monitor sketch

Open it in a folder named tank_monitor. Edit EMPTY_CM and FULL_CM before uploading.

tank_monitor.ino · INO · 2 KB
Download Download the tank-monitor sketch↓

Check it in three short experiments

ExperimentWhat to doWhat to look for
Direction checkRaise a flat target from 30 cm toward 5 cmGap decreases; percentage increases
Alarm checkMove to about 10% then 22%, then 30%LED turns on, stays on at 22%, clears above 25%
Fault checkPoint the sensor away from a reflecting surfaceFAULT message; no invented 0% reading

The exponential filter uses 20% of each new distance and 80% of the previous filtered value. It reduces jitter but creates lag. After changing the level, hold it steady for several seconds before judging the alarm. Four consecutive invalid readings clear the filter so recovery can start with fresh data.

Record ruler distance and reported distance at five positions. Repeat each position three times. Compare the errors instead of judging the result from a single reading. The worksheet is deliberately blank: fill it with observations from your own build.

Calibration worksheet

Five reference positions with space for your measurements and errors.

tank-checks.csv · CSV · 1 KB
Download Calibration worksheet↓

If the result looks wrong

SymptomLikely causeNext check
Only FAULT messagesMissing echo, bad divider wiring or wrong pinsTry a flat dry target 15–30 cm away; confirm common ground
Always 0% or 100%Wrong calibration, reversed values or unitsMeasure both sensor-to-surface gaps in cm
Reading jumps near the wallEcho from the container instead of waterRe-centre and level the sensor; use a wider container
Alarm clears slowlySmoothing and hysteresis are workingWait for the filter; compare with the interactive model

Make it your own

Once the basic measurement is trustworthy, add a display or send the readings with the companion ESP-NOW tutorial. For a research extension, compare mean absolute error for three mounting positions and discuss why they differ. Separate height accuracy from any volume estimate.

The firmware has been compiled, but the illustrations and browser model are teaching aids rather than proof of a physical build. Complete the checks above on your own hardware before leaving the monitor unattended.

References and further reading: HC-SR04 operating details · Arduino pulseIn reference · Arduino-ESP32 documentation

Did you build it? 🎉

Tell us if this tutorial helped and share it with your classmates.

Need parts or a hand with your project?

Get the components from our store, or talk to a Kadosh engineer about your build or research.

Shop componentsExplore project topics

Questions & comments

Stuck on a step? Ask here — our engineers and other builders reply.

Sign in to ask a question or leave a comment.

Sign inCreate a free account
  • Loading comments…

You might also like

All tutorials →
Robotics

Build a line-following robot—and understand every steering decision

Wire an Arduino Uno, two reflectance sensors and a TB6612FNG driver. Calibrate with the motors disabled, explore steering interactively and run your first track with a repeatable test plan.

5 min read·Beginner·1 viewsRead →
IoT

Send sensor readings between two ESP32 boards—without a router

Build a direct ESP-NOW temperature-and-humidity link. Understand channels with an interactive packet demo, configure the receiver MAC and recognise missing or stale readings.

5 min read·Beginner·1 viewsRead →