Mains voltage (230 V) — can kill. The low-voltage side (board, sensors, display, relay module) is safe to build yourself. The mains side must be wired by a licensed electrician, and tested with the circuit switched off at the MCB.
- Never switch a motor with a relay module. Motors pull several times their running current at start-up; hobby relay contacts weld or overheat. The relay only drives a contactor’s coil (or your pump starter’s coil input).
- Contactor rated for motors (AC-3) at or above the load’s nameplate current.
- Protect and earth: MCB on live, RCCB/ELCB on the circuit, metal bodies earthed. Switch live only — never neutral or earth.
- Keep the two worlds apart: mains and low-voltage wiring in separate compartments of a closed, non-metallic enclosure; no bare 230 V terminals.
- Fail safe: the code switches the load off when a sensor stops answering. Still fit a manual override at the starter.
Parts list
| Part | Qty | Approx. | Buy |
|---|---|---|---|
| ESP32 DevKit V1 (30-pin) | 1 | ₹380 | Buy ESP32 DevKit V1 (30-pin) |
| HC-SR04 ultrasonic sensor (or JSN-SR04T waterproof) | 1 | ₹90 | Buy HC-SR04 ultrasonic sensor (or JSN-SR04T waterproof) |
| 16×2 LCD with I2C backpack (PCF8574, 0x27) | 1 | ₹180 | Buy 16×2 LCD with I2C backpack (PCF8574, 0x27) |
| 4-channel I2C level shifter (BSS138) | 1 | ₹40 | Buy 4-channel I2C level shifter (BSS138) |
| 1-channel relay module, 5 V coil, 3.3 V-trigger (active-LOW) | 1 | ₹70 | Buy 1-channel relay module, 5 V coil, 3.3 V-trigger (active-LOW) |
| Active buzzer module, high-level trigger | 1 | ₹25 | Buy Active buzzer module, high-level trigger |
| Resistors 1 kΩ + 2 kΩ (echo divider) | 1 | ₹5 | Buy Resistors 1 kΩ + 2 kΩ (echo divider) |
| Contactor, 230 V AC coil, AC-3 rated ≥ pump current (e.g. 9 A) | 1 | ₹450 | Buy Contactor, 230 V AC coil, AC-3 rated ≥ pump current (e.g. 9 A) |
| Jumper wires + 5 V 1 A adapter | 1 | ₹45 | Buy Jumper wires + 5 V 1 A adapter |
| Total | ₹1,285 |
Affiliate disclosure: “Buy” links may earn FlashReady a small commission at no extra cost to you. Prices are indicative.
Wiring
- 5V
- 3V3
- GND
- Flags with the same label are the same connection. Arcs mark crossings that do not connect.
| From | To | Wire | Why |
|---|---|---|---|
| ESP32 D18 | HC-SR04 TRIG | Purple | 3.3 V trigger is enough for the HC-SR04 |
| HC-SR04 ECHO | 1 kΩ → D19, 2 kΩ → GND | Green | Divider drops the 5 V echo to 3.3 V |
| ESP32 D21 (SDA) | Level shifter LV2 → HV2 → LCD SDA | Blue | Backpack pull-ups sit at 5 V; shifter protects the ESP32 |
| ESP32 D22 (SCL) | Level shifter LV1 → HV1 → LCD SCL | Yellow | |
| ESP32 D25 | Buzzer module I/O | Orange | High-level-trigger module (has its own transistor) |
| ESP32 D26 | Relay module IN | Teal | Active-LOW; module must accept a 3.3 V trigger |
| 5 V adapter + | ESP32 VIN, LCD VCC, HC-SR04 VCC, shifter HV, relay DC+ | Red | 5 V net |
| ESP32 3V3 | Shifter LV, buzzer VCC | Amber | 3.3 V net (from the board’s regulator) |
| 5 V adapter − | Every GND | Black | One common ground |
| Mains L (via MCB) | Contactor L1 and relay COM | Brown | 230 V. MCB sized for the pump (C-curve) |
| Relay NO | Contactor coil A1 | Brown | The relay only switches the coil (tens of mA), never the motor |
| Contactor A2 | Mains N | Blue | Coil return |
| Contactor T1 | Pump L | Brown | Contactor rated ≥ pump current, AC-3 (motor duty) |
| Mains N / E | Pump N / pump body (earth) | Blue / green-yellow | Never switch neutral or earth; earth the pump body |
Board quirks: Don't move TRIG to GPIO 5 or 15 — both output PWM at boot and the sensor will fire at random. Keep the level shifter. Most LCD backpacks pull SDA/SCL up to their 5 V supply, and ESP32 pins are 3.3 V only.
Code
// Water tank level alarm with pump cut-off — ESP32 DevKit V1
// HC-SR04 (ECHO through a 1k/2k divider), 16x2 I2C LCD via a BSS138 level shifter,
// active buzzer module, 3.3 V-trigger relay module. Wiring: see the diagram above.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
constexpr uint8_t TRIG_PIN = 18;
constexpr uint8_t ECHO_PIN = 19; // 5 V echo -> divider -> 3.3 V
constexpr uint8_t BUZZER_PIN = 25; // buzzer module I/O (high = on)
constexpr uint8_t RELAY_PIN = 26;
constexpr bool RELAY_ACTIVE_LOW = true; // most blue relay modules
constexpr float EMPTY_CM = 120.0; // sensor face to tank bottom
constexpr float FULL_CM = 20.0; // sensor face to full water line
constexpr int PUMP_ON_BELOW = 25; // percent
constexpr int PUMP_OFF_ABOVE = 95; // percent
LiquidCrystal_I2C lcd(0x27, 16, 2);
bool pumpOn = false;
float readDistanceCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long us = pulseIn(ECHO_PIN, HIGH, 30000UL); // ~5 m max
if (us == 0) return -1; // no echo
return us * 0.0343f / 2.0f;
}
// Median of 5 readings rejects ripples and stray echoes.
float medianDistanceCm() {
float r[5];
for (int i = 0; i < 5; i++) { r[i] = readDistanceCm(); delay(60); }
for (int i = 1; i < 5; i++) {
float k = r[i]; int j = i - 1;
while (j >= 0 && r[j] > k) { r[j + 1] = r[j]; j--; }
r[j + 1] = k;
}
return r[2];
}
void setPump(bool on) {
pumpOn = on;
digitalWrite(RELAY_PIN, (on != RELAY_ACTIVE_LOW) ? HIGH : LOW);
}
void setup() {
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
setPump(false);
Wire.begin(21, 22); // SDA, SCL
lcd.init();
lcd.backlight();
lcd.print("Tank monitor");
}
void loop() {
float d = medianDistanceCm();
lcd.clear();
if (d < 0) { // sensor unplugged or wet
setPump(false); // fail safe: never run blind
digitalWrite(BUZZER_PIN, !digitalRead(BUZZER_PIN));
lcd.print("Sensor error!");
Serial.println("ERR no echo");
delay(500);
return;
}
int level = constrain((int)((EMPTY_CM - d) * 100 / (EMPTY_CM - FULL_CM)), 0, 100);
if (!pumpOn && level < PUMP_ON_BELOW) setPump(true);
if (pumpOn && level > PUMP_OFF_ABOVE) setPump(false);
digitalWrite(BUZZER_PIN, level >= 98 ? HIGH : LOW); // overflow warning
lcd.printf("LVL %3d%%", level);
lcd.setCursor(0, 1);
lcd.print(pumpOn ? "PUMP ON " : "PUMP OFF");
Serial.printf("dist=%.1fcm level=%d%% pump=%d\n", d, level, pumpOn);
delay(500);
}// Water tank level alarm with pump cut-off — ESP32 DevKit V1
// HC-SR04 (ECHO through a 1k/2k divider), 16x2 I2C LCD via a BSS138 level shifter,
// active buzzer module, 3.3 V-trigger relay module. Wiring: see the diagram above.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
constexpr uint8_t TRIG_PIN = 18;
constexpr uint8_t ECHO_PIN = 19; // 5 V echo -> divider -> 3.3 V
constexpr uint8_t BUZZER_PIN = 25; // buzzer module I/O (high = on)
constexpr uint8_t RELAY_PIN = 26;
constexpr bool RELAY_ACTIVE_LOW = true; // most blue relay modules
constexpr float EMPTY_CM = 120.0; // sensor face to tank bottom
constexpr float FULL_CM = 20.0; // sensor face to full water line
constexpr int PUMP_ON_BELOW = 25; // percent
constexpr int PUMP_OFF_ABOVE = 95; // percent
LiquidCrystal_I2C lcd(0x27, 16, 2);
bool pumpOn = false;
float readDistanceCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long us = pulseIn(ECHO_PIN, HIGH, 30000UL); // ~5 m max
if (us == 0) return -1; // no echo
return us * 0.0343f / 2.0f;
}
// Median of 5 readings rejects ripples and stray echoes.
float medianDistanceCm() {
float r[5];
for (int i = 0; i < 5; i++) { r[i] = readDistanceCm(); delay(60); }
for (int i = 1; i < 5; i++) {
float k = r[i]; int j = i - 1;
while (j >= 0 && r[j] > k) { r[j + 1] = r[j]; j--; }
r[j + 1] = k;
}
return r[2];
}
void setPump(bool on) {
pumpOn = on;
digitalWrite(RELAY_PIN, (on != RELAY_ACTIVE_LOW) ? HIGH : LOW);
}
void setup() {
Serial.begin(115200);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(RELAY_PIN, OUTPUT);
setPump(false);
Wire.begin(21, 22); // SDA, SCL
lcd.init();
lcd.backlight();
lcd.print("Tank monitor");
}
void loop() {
float d = medianDistanceCm();
lcd.clear();
if (d < 0) { // sensor unplugged or wet
setPump(false); // fail safe: never run blind
digitalWrite(BUZZER_PIN, !digitalRead(BUZZER_PIN));
lcd.print("Sensor error!");
Serial.println("ERR no echo");
delay(500);
return;
}
int level = constrain((int)((EMPTY_CM - d) * 100 / (EMPTY_CM - FULL_CM)), 0, 100);
if (!pumpOn && level < PUMP_ON_BELOW) setPump(true);
if (pumpOn && level > PUMP_OFF_ABOVE) setPump(false);
digitalWrite(BUZZER_PIN, level >= 98 ? HIGH : LOW); // overflow warning
lcd.printf("LVL %3d%%", level);
lcd.setCursor(0, 1);
lcd.print(pumpOn ? "PUMP ON " : "PUMP OFF");
Serial.printf("dist=%.1fcm level=%d%% pump=%d\n", d, level, pumpOn);
delay(500);
}Compiled for esp32 on esp32 3.3.12 with LiquidCrystal I2C 1.1.2 · 3 Nov 2026
How it works
The HC-SR04 measures how long an ultrasonic ping takes to bounce off the water. Half that time multiplied by the speed of sound (0.0343 cm/µs) is the gap to the surface, which the sketch maps onto your tank’s empty and full marks.
Two thresholds — pump on below 25 %, off above 95 % — stop the relay from chattering when the level sits on a line. If the sensor stops answering, the pump turns off: running a motor blind is how tanks overflow.
The ESP32 never touches mains. Its relay module only switches the coil of a contactor, and the contactor switches the pump.
Troubleshooting
Level jumps around or reads 0%
Mount the sensor at least 20 cm above the full line and away from the inlet pipe. Splashing water causes stray echoes; the median filter handles small ripples only.
LCD lights up but shows blocks or nothing
Turn the contrast trimmer on the I2C backpack. Still blank? Run the I2C scanner — some backpacks answer at 0x3F instead of 0x27.
Relay clicks on at boot
Your module is active-LOW and GPIO26 floats before setup() runs. Keep RELAY_ACTIVE_LOW = true; for a hard guarantee add a 10 kΩ pull-up from IN to the module's VCC.
ESP32 resets when the pump starts
Contactor switching noise is coupling into the 5 V side. Fit an RC snubber across the contactor coil, keep mains and low-voltage wiring apart, and power the ESP32 from its own adapter.
Upgrades
- Send a Telegram alert when the tank is full or the sensor fails.
- Add a dry-run guard — a float switch in the sump so the pump never runs without water.
- Log levels to a web dashboard on your phone over Wi-Fi.



