Water Level System Using Two Float Sensors and Two Pumps with an LCD Display

so I made a water sensor device where there are 2 sensors, bottom and top.

If the water runs out below the bottom sensor, the buzzer activates and the pump supplies water from the reserve tank to the water container. And if both the top and bottom sensors are active, buzzer active and the pump draws water into the reserve tank.

I'm asking because I tried wiring it roughly like this scheme, but the screen only turns on and shows nothing at all, and the buzzer is active. Previously I made it without the pump and relay and everything worked. fine,any advice and help....

----wiring :
=== LCD I2C 16x2 ===
VCC β†’ 5V Arduino
GND β†’ GND Arduino
SDA β†’ A4 Arduino
SCL β†’ A5 Arduino

=== LOWER FLOAT SENSOR ===
Wire 1 β†’ Pin 2 Arduino
Wire 2 β†’ GND Arduino

=== UPPER FLOAT SENSOR ===
Wire 1 β†’ Pin 3 Arduino
Wire 2 β†’ GND Arduino

=== BUZZER ===
(+) β†’ Pin 8 Arduino
(-) β†’ GND Arduino

=== 5V RELAY MODULE ===
VCC β†’ 5V Arduino
GND β†’ GND Arduino
IN β†’ Pin 6 Arduino

COM β†’ 5V Arduino
NO β†’ RED wire (+) Pump
BLACK wire (-) Pump β†’ GND Arduino

---Pump Specifications :

Mini Submersible Water Pump (DC 3–6V)**

Type: Submersible Motor Pump
Operating Voltage: 2.7 – 3.3 VDC
Rated Current: 60 – 90 mA
Noise Level: Max 50 dB(A)
Operating Temperature: βˆ’20Β°C to +60Β°C

And your code is?

Ron

Does it still work without the pump and relay as expected?

Here is my code ron,Thank you for your answer..

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// =====================
// PIN CONFIGURATION
// =====================
#define PIN_SENSOR_BOTTOM  2
#define PIN_SENSOR_TOP     3
#define PIN_BUZZER         8
#define PIN_PUMP1          6   // Relay IN1 - Filling pump
#define PIN_PUMP2          7   // Relay IN2 - Draining pump

// Active LOW relay
#define RELAY_ON  LOW
#define RELAY_OFF HIGH

LiquidCrystal_I2C lcd(0x27, 16, 2);

// =====================
// CUSTOM CHARACTER
// =====================
byte gel0[8] = {0,0,10,21,17,0,0,0};
byte gel1[8] = {0,10,21,17,0,0,0,0};
byte gel2[8] = {10,21,17,0,0,0,0,0};

byte tank0[8] = {31,17,17,17,17,17,17,31};
byte tank1[8] = {31,17,17,17,17,17,31,31};
byte tank2[8] = {31,17,17,17,17,31,31,31};
byte tank3[8] = {31,17,17,17,31,31,31,31};
byte tank4[8] = {31,31,31,31,31,31,31,31};

byte arrowUp[8] = {4,14,31,4,4,4,0,0};

#define DEBOUNCE_MS 300

bool rawBottom    = false;
bool rawTop       = false;
bool waterBottom  = false;
bool waterTop     = false;
bool pendingBottom = false;
bool pendingTop    = false;

unsigned long tDebounceB = 0;
unsigned long tDebounceA = 0;

int systemStatus = 0;
int lastStatus   = -1;

unsigned long tAnim   = 0;
unsigned long tBlink  = 0;
unsigned long tSerial = 0;

int  waveFrame  = 0;
int  tankFrame  = 2;
bool blinkState = false;

int  spinFrame = 0;
char SPINNER[] = "|/-\\";

// =====================
// BUZZER VARIABLES
// =====================
unsigned long tBuzzer    = 0;
bool          buzzerState = false;

// =====================
// PUMP VARIABLES
// =====================
bool pump1Active = false;
bool pump2Active = false;


void readSensor() {
  bool newBottom = digitalRead(PIN_SENSOR_BOTTOM) == HIGH;
  bool newTop    = digitalRead(PIN_SENSOR_TOP)    == HIGH;

  // Bottom sensor debounce
  if (newBottom != rawBottom) {
    tDebounceB     = millis();
    rawBottom      = newBottom;
    pendingBottom  = true;
  }
  if (pendingBottom && millis() - tDebounceB > DEBOUNCE_MS) {
    waterBottom    = rawBottom;
    pendingBottom  = false;
  }

  if (newTop != rawTop) {
    tDebounceA   = millis();
    rawTop       = newTop;
    pendingTop   = true;
  }
  if (pendingTop && millis() - tDebounceA > DEBOUNCE_MS) {
    waterTop     = rawTop;
    pendingTop   = false;
  }
}

void determineStatus() {
  if (!waterBottom && waterTop)  { systemStatus = 3; return; } // ERROR
  if (waterBottom  && waterTop)  { systemStatus = 2; return; } // FULL
  if (waterBottom  && !waterTop) { systemStatus = 0; return; } // NORMAL
  systemStatus = 1;                                             // EMPTY
}

void controlPump() {
  switch (systemStatus) {
    case 0: // NORMAL β†’ all pumps OFF
      digitalWrite(PIN_PUMP1, RELAY_OFF);
      digitalWrite(PIN_PUMP2, RELAY_OFF);
      pump1Active = false;
      pump2Active = false;
      break;

    case 1: // EMPTY β†’ Pump 1 ON
      digitalWrite(PIN_PUMP1, RELAY_ON);
      digitalWrite(PIN_PUMP2, RELAY_OFF);
      pump1Active = true;
      pump2Active = false;
      break;

    case 2: // FULL β†’ Pump 2 ON
      digitalWrite(PIN_PUMP1, RELAY_OFF);
      digitalWrite(PIN_PUMP2, RELAY_ON);
      pump1Active = false;
      pump2Active = true;
      break;

    case 3: // ERROR β†’ all pumps OFF (safety)
      digitalWrite(PIN_PUMP1, RELAY_OFF);
      digitalWrite(PIN_PUMP2, RELAY_OFF);
      pump1Active = false;
      pump2Active = false;
      break;
  }
}

// =====================
// FUNCTION: BUZZER
// =====================
void updateBuzzer() {
  if (systemStatus == 1) {
    // EMPTY: slow beep (600ms)
    if (millis() - tBuzzer > 600) {
      tBuzzer     = millis();
      buzzerState = !buzzerState;
      digitalWrite(PIN_BUZZER, buzzerState ? HIGH : LOW);
    }
  } else if (systemStatus == 2) {
    // FULL: fast beep (200ms)
    if (millis() - tBuzzer > 200) {
      tBuzzer     = millis();
      buzzerState = !buzzerState;
      digitalWrite(PIN_BUZZER, buzzerState ? HIGH : LOW);
    }
  } else if (systemStatus == 3) {
    // ERROR: very fast beep (100ms)
    if (millis() - tBuzzer > 100) {
      tBuzzer     = millis();
      buzzerState = !buzzerState;
      digitalWrite(PIN_BUZZER, buzzerState ? HIGH : LOW);
    }
  } else {
    digitalWrite(PIN_BUZZER, LOW);
    buzzerState = false;
  }
}

void setTank(int level) {
  if (level == 0) lcd.createChar(3, tank0);
  if (level == 1) lcd.createChar(3, tank1);
  if (level == 2) lcd.createChar(3, tank2);
  if (level == 3) lcd.createChar(3, tank3);
  if (level == 4) lcd.createChar(3, tank4);
}

void drawWave() {
  lcd.setCursor(0, 1);
  for (int i = 0; i < 5; i++) {
    lcd.write((i + waveFrame) % 3);
  }
}

void clearLine(int row) {
  lcd.setCursor(0, row);
  lcd.print("                ");
  lcd.setCursor(0, row);
}

// =====================
// SETUP
// =====================
void setup() {
  Serial.begin(9600);

  pinMode(PIN_SENSOR_BOTTOM, INPUT_PULLUP);
  pinMode(PIN_SENSOR_TOP,    INPUT_PULLUP);
  pinMode(PIN_BUZZER,        OUTPUT);
  pinMode(PIN_PUMP1,         OUTPUT);
  pinMode(PIN_PUMP2,         OUTPUT);

  digitalWrite(PIN_BUZZER, LOW);
  digitalWrite(PIN_PUMP1, RELAY_OFF);
  digitalWrite(PIN_PUMP2, RELAY_OFF);

  lcd.init();
  lcd.backlight();

  lcd.createChar(0, gel0);
  lcd.createChar(1, gel1);
  lcd.createChar(2, gel2);
  lcd.createChar(3, tank2);
  lcd.createChar(4, arrowUp);

  rawBottom = digitalRead(PIN_SENSOR_BOTTOM) == HIGH;
  rawTop    = digitalRead(PIN_SENSOR_TOP)    == HIGH;
  waterBottom = rawBottom;
  waterTop    = rawTop;

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Water Level");
  lcd.setCursor(0, 1);
  lcd.print("Monitoring v4");
  delay(1500);
  lcd.clear();
}

// =====================
// DISPLAY: NORMAL
// =====================
void displayNormal() {
  if (lastStatus != 0) {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Status: Normal");
    lcd.setCursor(0, 1);
    lcd.write(byte(3));
    lcd.print(" All OK");
    lastStatus = 0;
  }

  if (millis() - tAnim > 300) {
    tAnim = millis();
    waveFrame = (waveFrame + 1) % 3;
    drawWave();
  }
}

// =====================
// DISPLAY: EMPTY
// =====================
void displayEmpty() {
  if (millis() - tBlink > 500) {
    tBlink     = millis();
    blinkState = !blinkState;

    if (blinkState) {
      clearLine(0);
      lcd.print("WATER LOW");
      clearLine(1);
      lcd.print("EMPTY !!!");
    } else {
      clearLine(0);
      lcd.write(byte(3));
      lcd.print(" Filling...");
      clearLine(1);
      lcd.print("Pump 1 ON ");
      lcd.setCursor(10, 1);
      lcd.print(pump1Active ? "[ON] " : "[OFF]");
    }
  }
  lastStatus = 1;
}

// =====================
// DISPLAY: FULL
// =====================
void displayFull() {
  if (millis() - tBlink > 400) {
    tBlink     = millis();
    blinkState = !blinkState;

    if (blinkState) {
      clearLine(0);
      lcd.print("WARNING !!!");
      clearLine(1);
      lcd.print("TANK FULL");
    } else {
      clearLine(0);
      lcd.write(byte(3));
      lcd.print(" Draining...");
      clearLine(1);
      lcd.print("Pump 2 ON ");
      lcd.setCursor(10, 1);
      lcd.print(pump2Active ? "[ON] " : "[OFF]");
    }
  }
  lastStatus = 2;
}

// =====================
// DISPLAY: ERROR
// =====================
void displayError() {
  if (lastStatus != 3) {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("| ERROR !");
    lcd.setCursor(0, 1);
    lcd.print("Check Sensor!");
    lastStatus = 3;
  }

  if (millis() - tAnim > 180) {
    tAnim = millis();
    spinFrame = (spinFrame + 1) % 4;
    lcd.setCursor(0, 0);
    lcd.print(SPINNER[spinFrame]);
  }
}

// =====================
// MAIN LOOP
// =====================
void loop() {
  readSensor();
  determineStatus();
  controlPump();
  updateBuzzer();

  switch (systemStatus) {
    case 0: displayNormal(); break;
    case 1: displayEmpty();  break;
    case 2: displayFull();   break;
    case 3: displayError();  break;
  }

  if (millis() - tSerial > 500) {
    tSerial = millis();
    const char* label[] = {"NORMAL", "EMPTY", "FULL", "ERROR"};
    Serial.print("Bottom Sensor : "); Serial.print(waterBottom ? "WET" : "DRY");
    Serial.print(" | Top Sensor : "); Serial.print(waterTop ? "WET" : "DRY");
    Serial.print(" | Status : ");     Serial.print(label[systemStatus]);
    Serial.print(" | Pump1 : ");      Serial.print(pump1Active ? "ON" : "OFF");
    Serial.print(" | Pump2 : ");      Serial.println(pump2Active ? "ON" : "OFF");
  }
}

Yes, but the code are different and remove all wiring on the pump and relay
. the buzzer will sound when the bottom sensor does not detect water, and it will also sound when both sensors detect full water.”

So what help do you really need? And for which program? You really need to make up your mind on one program or the other.

I need help is there something wrong with my schematic? When I power it on, it only makes a buzzer sound and the screen turns on but stays blank. Sorry I'm newbie

Several things are wrong. First, that is NOT a schematic. A schematic would not have wires overlaying wires. A schematic would show the power for the relay board and other high powered stuff coming from a suitable power supply and not from the Arduino 5 volt pin.

  • As mentioned, the pumps need a separate power supply.
  • The pumps will need kickback diodes.
  • For a passive buzzer add a series resistore, ~220R.
    Otherwise, what current does it require ?
    Is it inductive ?
  • Please generate a proper schematic.

If you are a beginner, start with one pump, a separate power supply for the pump, a relay and one switch, and get that working before adding more parts. Baby steps!

The search phrase "arduino water pump" will turn up lots of tutorials.

Do your tanks EVER get filled? If so, the falling water will have a drastic effect on your water level sensors. The voice of experience. My tank fill is directly centered at the top of the tank. The waves from the falling water can make the sensors go on and off rapidly.

  • Would mounting the switch in an open 4” plastic pipe prevent this ?

Probably not, but programming to detect the first sensor change after several minutes of no change lets me know when to begin adding water or when to stop adding water.

@Paul_KD7HB
Modify your tank inlet to fill from a lower depth.
It could be just a bend and a pipe extension.
A "T" works as well. Sometimes referred to as a stilling tube.
Tank overflow tip.
Instead of a hole and a pipe, put a "T" inside the tank with one port extended below the surface and one above.
Useful if you get floating muck on the surface. The overflow water is always drawn from the body of the water, not the surface.
Essential on rainwater storage tanks where the overflow goes to soakaway and surface muck could clag up the soakaway.

Tank filling using high and low sensors/float switches and a pump can be easily achieved by using one or two relays in a hold-on configuration.
No microcontroller required.
If you understand the relay logic, the MCU logic works the same way.

If no water activates the sensor, when the bottom sensor is activated won't the top sensor also be activated?

Shouldn't it be.
Tank is full, top and bottom sensors de-activ.
Tank is half full, top sensor active, bottom sensor de-activ.
Tank is low, top and bottom sensors activ, reserve tank pumping to fill the tank.
Tank fills, bottom sensor de-activ, top sensor active, reserv still pumping to fill tank.
Tank full, top and bottom sensors de-activ. Reserv pump stops filling.

Is that what you are trying to accomplish?

Can you please post a diagram of the water tanks and sensors and pump arrangement.

A decent schematic.
Can you please post a copy of your circuit, a picture of a hand drawn circuit in jpg, png?
Hand drawn and photographed is perfectly acceptable.
Please include ALL hardware, power supplies, component names and pin labels.

Tom.... :smiley: :+1: :coffee: :australia:

The water source is a domestic well. I need the break to limit the potential back-flow into the house water.

Disregard this reply... you are modifying characters at run-time

Your LCD allows only eight custom characters.

Disregard this reply, too...

Your sensors are wired to be active when not engaged (this should mean "empty"), as it should with no water. The sequence should be: the bottom sensor is engaged before the top sensor is engaged, and if the order is reversed, there should be an error.

Your code is good.

  • When the "tank" is empty, the "fill" pump starts.
  • When the bottom float senses water and the top float senses no-water, the tank is NORMAL
  • When the bottom float and top float sense water, the tank is "overfull" and "drain" pump starts.
  • When the bottom float senses no-water but the top float senses water, "ERROR" occurs.
  • LCD animations work nicely.

The problem is in electric power, wiring or pumps.

code here
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

// =====================
// PIN CONFIGURATION
// =====================
#define PIN_SENSOR_BOTTOM  2
#define PIN_SENSOR_TOP     3
#define PIN_BUZZER         8
#define PIN_PUMP1          6   // Relay IN1 - Filling pump
#define PIN_PUMP2          7   // Relay IN2 - Draining pump

// Active LOW relay
#define RELAY_ON  LOW
#define RELAY_OFF HIGH

LiquidCrystal_I2C lcd(0x27, 16, 2);

// =====================
// CUSTOM CHARACTER
// =====================
byte gel0[8] = {0, 0, 10, 21, 17, 0, 0, 0};
byte gel1[8] = {0, 10, 21, 17, 0, 0, 0, 0};
byte gel2[8] = {10, 21, 17, 0, 0, 0, 0, 0};
byte tank0[8] = {31, 17, 17, 17, 17, 17, 17, 31};
byte tank1[8] = {31, 17, 17, 17, 17, 17, 31, 31};
byte tank2[8] = {31, 17, 17, 17, 17, 31, 31, 31};
byte tank3[8] = {31, 17, 17, 17, 31, 31, 31, 31};
byte tank4[8] = {31, 31, 31, 31, 31, 31, 31, 31};
byte arrowUp[8] = {4, 14, 31, 4, 4, 4, 0, 0};

#define DEBOUNCE_MS 300

bool rawBottom    = false;
bool rawTop       = false;
bool waterBottom  = false;
bool waterTop     = false;
bool pendingBottom = false;
bool pendingTop    = false;

unsigned long tDebounceB = 0;
unsigned long tDebounceA = 0;

int systemStatus = 0;
int lastStatus   = -1;

unsigned long tAnim   = 0;
unsigned long tBlink  = 0;
unsigned long tSerial = 0;

int  waveFrame  = 0;
int  tankFrame  = 2;
bool blinkState = false;

int  spinFrame = 0;
char SPINNER[] = "|/-\\";

// =====================
// BUZZER VARIABLES
// =====================
unsigned long tBuzzer    = 0;
bool          buzzerState = false;

// =====================
// PUMP VARIABLES
// =====================
bool pump1Active = false;
bool pump2Active = false;


void readSensor() {
  bool newBottom = digitalRead(PIN_SENSOR_BOTTOM) == HIGH;
  bool newTop    = digitalRead(PIN_SENSOR_TOP)    == HIGH;

  // Bottom sensor debounce
  if (newBottom != rawBottom) {
    tDebounceB     = millis();
    rawBottom      = newBottom;
    pendingBottom  = true;
  }
  if (pendingBottom && millis() - tDebounceB > DEBOUNCE_MS) {
    waterBottom    = rawBottom;
    pendingBottom  = false;
  }

  if (newTop != rawTop) {
    tDebounceA   = millis();
    rawTop       = newTop;
    pendingTop   = true;
  }
  if (pendingTop && millis() - tDebounceA > DEBOUNCE_MS) {
    waterTop     = rawTop;
    pendingTop   = false;
  }
}

void determineStatus() {
  if (!waterBottom && waterTop)  {
    systemStatus = 3;  // ERROR
    return;
  }
  if (waterBottom  && waterTop)  {
    systemStatus = 2;  // FULL
    return;
  }
  if (waterBottom  && !waterTop) {
    systemStatus = 0;  // NORMAL
    return;
  }
  systemStatus = 1; // EMPTY
}

void controlPump() {
  switch (systemStatus) {
    case 0: // NORMAL β†’ all pumps OFF
      digitalWrite(PIN_PUMP1, RELAY_OFF);
      digitalWrite(PIN_PUMP2, RELAY_OFF);
      pump1Active = false;
      pump2Active = false;
      break;

    case 1: // EMPTY β†’ Pump 1 ON
      digitalWrite(PIN_PUMP1, RELAY_ON);
      digitalWrite(PIN_PUMP2, RELAY_OFF);
      pump1Active = true;
      pump2Active = false;
      break;

    case 2: // FULL β†’ Pump 2 ON
      digitalWrite(PIN_PUMP1, RELAY_OFF);
      digitalWrite(PIN_PUMP2, RELAY_ON);
      pump1Active = false;
      pump2Active = true;
      break;

    case 3: // ERROR β†’ all pumps OFF (safety)
      digitalWrite(PIN_PUMP1, RELAY_OFF);
      digitalWrite(PIN_PUMP2, RELAY_OFF);
      pump1Active = false;
      pump2Active = false;
      break;
  }
}

// =====================
// FUNCTION: BUZZER
// =====================
void updateBuzzer() {
  if (systemStatus == 1) {
    // EMPTY: slow beep (600ms)
    if (millis() - tBuzzer > 600) {
      tBuzzer     = millis();
      buzzerState = !buzzerState;
      digitalWrite(PIN_BUZZER, buzzerState ? HIGH : LOW);
    }
  } else if (systemStatus == 2) {
    // FULL: fast beep (200ms)
    if (millis() - tBuzzer > 200) {
      tBuzzer     = millis();
      buzzerState = !buzzerState;
      digitalWrite(PIN_BUZZER, buzzerState ? HIGH : LOW);
    }
  } else if (systemStatus == 3) {
    // ERROR: very fast beep (100ms)
    if (millis() - tBuzzer > 100) {
      tBuzzer     = millis();
      buzzerState = !buzzerState;
      digitalWrite(PIN_BUZZER, buzzerState ? HIGH : LOW);
    }
  } else {
    digitalWrite(PIN_BUZZER, LOW);
    buzzerState = false;
  }
}

void setTank(int level) {
  if (level == 0) lcd.createChar(3, tank0);
  if (level == 1) lcd.createChar(3, tank1);
  if (level == 2) lcd.createChar(3, tank2);
  if (level == 3) lcd.createChar(3, tank3);
  if (level == 4) lcd.createChar(3, tank4);
}

void drawWave() {
  lcd.setCursor(0, 1);
  for (int i = 0; i < 5; i++) {
    lcd.write((i + waveFrame) % 3);
  }
}

void clearLine(int row) {
  lcd.setCursor(0, row);
  lcd.print("                ");
  lcd.setCursor(0, row);
}

// =====================
// SETUP
// =====================
void setup() {
  Serial.begin(9600);

  pinMode(PIN_SENSOR_BOTTOM, INPUT_PULLUP);
  pinMode(PIN_SENSOR_TOP,    INPUT_PULLUP);
  pinMode(PIN_BUZZER,        OUTPUT);
  pinMode(PIN_PUMP1,         OUTPUT);
  pinMode(PIN_PUMP2,         OUTPUT);

  digitalWrite(PIN_BUZZER, LOW);
  digitalWrite(PIN_PUMP1, RELAY_OFF);
  digitalWrite(PIN_PUMP2, RELAY_OFF);

  lcd.init();
  lcd.backlight();

  lcd.createChar(0, gel0);
  lcd.createChar(1, gel1);
  lcd.createChar(2, gel2);
  lcd.createChar(3, tank2);
  lcd.createChar(4, arrowUp);

  rawBottom = digitalRead(PIN_SENSOR_BOTTOM) == HIGH;
  rawTop    = digitalRead(PIN_SENSOR_TOP)    == HIGH;
  waterBottom = rawBottom;
  waterTop    = rawTop;

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Water Level");
  lcd.setCursor(0, 1);
  lcd.print("Monitoring v4");
  delay(1500);
  lcd.clear();
}

// =====================
// DISPLAY: NORMAL
// =====================
void displayNormal() {
  if (lastStatus != 0) {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Status: Normal");
    lcd.setCursor(0, 1);
    lcd.write(byte(3));
    lcd.print(" All OK");
    lastStatus = 0;
  }

  if (millis() - tAnim > 300) {
    tAnim = millis();
    waveFrame = (waveFrame + 1) % 3;
    drawWave();
  }
}

// =====================
// DISPLAY: EMPTY
// =====================
void displayEmpty() {
  if (millis() - tBlink > 500) {
    tBlink     = millis();
    blinkState = !blinkState;

    if (blinkState) {
      clearLine(0);
      lcd.print("WATER LOW");
      clearLine(1);
      lcd.print("EMPTY !!!");
    } else {
      clearLine(0);
      lcd.write(byte(3));
      lcd.print(" Filling...");
      clearLine(1);
      lcd.print("Pump 1 ON ");
      lcd.setCursor(10, 1);
      lcd.print(pump1Active ? "[ON] " : "[OFF]");
    }
  }
  lastStatus = 1;
}

// =====================
// DISPLAY: FULL
// =====================
void displayFull() {
  if (millis() - tBlink > 400) {
    tBlink     = millis();
    blinkState = !blinkState;

    if (blinkState) {
      clearLine(0);
      lcd.print("WARNING !!!");
      clearLine(1);
      lcd.print("TANK FULL");
    } else {
      clearLine(0);
      lcd.write(byte(3));
      lcd.print(" Draining...");
      clearLine(1);
      lcd.print("Pump 2 ON ");
      lcd.setCursor(10, 1);
      lcd.print(pump2Active ? "[ON] " : "[OFF]");
    }
  }
  lastStatus = 2;
}

// =====================
// DISPLAY: ERROR
// =====================
void displayError() {
  if (lastStatus != 3) {
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("| ERROR !");
    lcd.setCursor(0, 1);
    lcd.print("Check Sensor!");
    lastStatus = 3;
  }

  if (millis() - tAnim > 180) {
    tAnim = millis();
    spinFrame = (spinFrame + 1) % 4;
    lcd.setCursor(0, 0);
    lcd.print(SPINNER[spinFrame]);
  }
}

// =====================
// MAIN LOOP
// =====================
void loop() {
  readSensor();
  determineStatus();
  controlPump();
  updateBuzzer();

  switch (systemStatus) {
    case 0: displayNormal(); break;
    case 1: displayEmpty();  break;
    case 2: displayFull();   break;
    case 3: displayError();  break;
  }

  if (millis() - tSerial > 500) {
    tSerial = millis();
    const char* label[] = {"NORMAL", "EMPTY", "FULL", "ERROR"};
    Serial.print("Bottom Sensor : "); Serial.print(waterBottom ? "WET" : "DRY");
    Serial.print(" | Top Sensor : "); Serial.print(waterTop ? "WET" : "DRY");
    Serial.print(" | Status : ");     Serial.print(label[systemStatus]);
    Serial.print(" | Pump1 : ");      Serial.print(pump1Active ? "ON" : "OFF");
    Serial.print(" | Pump2 : ");      Serial.println(pump2Active ? "ON" : "OFF");
  }
}
wokwi diagram.json for the nerds
{
  "version": 1,
  "author": "foreignpigdog x",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-arduino-nano", "id": "nano", "top": -4.8, "left": 37.9, "attrs": {} },
    {
      "type": "wokwi-led",
      "id": "led1",
      "top": -126,
      "left": 189,
      "rotate": 90,
      "attrs": { "color": "red", "flip": "" }
    },
    {
      "type": "wokwi-led",
      "id": "led2",
      "top": -97.2,
      "left": 189,
      "rotate": 90,
      "attrs": { "color": "red", "flip": "" }
    },
    {
      "type": "wokwi-lcd1602",
      "id": "lcd1",
      "top": 35.2,
      "left": 216.8,
      "attrs": { "pins": "i2c" }
    },
    {
      "type": "wokwi-led",
      "id": "led3",
      "top": -109.2,
      "left": 71,
      "attrs": { "color": "white" }
    },
    {
      "type": "wokwi-slide-switch",
      "id": "sw1",
      "top": -152,
      "left": 192.5,
      "rotate": 90,
      "attrs": { "value": "1" }
    },
    {
      "type": "wokwi-slide-switch",
      "id": "sw2",
      "top": -56,
      "left": 192.5,
      "rotate": 90,
      "attrs": { "value": "" }
    },
    {
      "type": "wokwi-text",
      "id": "text1",
      "top": -115.2,
      "left": 240,
      "attrs": { "text": "FILL pump (LED ON = PUMP OFF)" }
    },
    {
      "type": "wokwi-text",
      "id": "text2",
      "top": -86.4,
      "left": 240,
      "attrs": { "text": "DRAIN pump (LED ON = PUMP OFF)" }
    },
    {
      "type": "wokwi-text",
      "id": "text3",
      "top": -153.6,
      "left": 240,
      "attrs": { "text": "TOP float (HIGH full) (LOW normal)" }
    },
    {
      "type": "wokwi-text",
      "id": "text4",
      "top": -48,
      "left": 240,
      "attrs": { "text": "BOT float (LOW empty) (HIGH normal)" }
    },
    {
      "type": "wokwi-text",
      "id": "text5",
      "top": -9.6,
      "left": 230.4,
      "attrs": { "text": "ERROR when TOP = HIGH while BOT = LOW" }
    }
  ],
  "connections": [
    [ "nano:GND.1", "led2:C", "black", [ "v0" ] ],
    [ "nano:GND.1", "led1:C", "black", [ "v0" ] ],
    [ "nano:6", "led1:A", "green", [ "v0" ] ],
    [ "nano:7", "led2:A", "green", [ "v0" ] ],
    [ "lcd1:GND", "nano:GND.1", "black", [ "h0" ] ],
    [ "lcd1:VCC", "nano:5V", "red", [ "h0" ] ],
    [ "lcd1:SDA", "nano:A4", "green", [ "h0" ] ],
    [ "lcd1:SCL", "nano:A5", "green", [ "h0" ] ],
    [ "nano:8", "led3:A", "green", [ "v0" ] ],
    [ "nano:GND.3", "led3:C", "black", [ "v0", "h-76.8" ] ],
    [ "nano:3", "sw1:2", "green", [ "v0" ] ],
    [ "nano:GND.1", "sw2:3", "black", [ "v0" ] ],
    [ "nano:GND.1", "sw1:3", "black", [ "v0" ] ],
    [ "nano:2", "sw2:2", "green", [ "v0" ] ]
  ],
  "dependencies": {}
}

I came to the same conclusion.

It's a bit complicated for what it does, but it reads well enough.

I stripped away the LDC junk.

I added a 20 millisecond delay to your loop and was able to completely koss the debouncing.

In a typical tank control program, the issue isn't that a sensor bounces, it is the lack of hysteresis.

Hysteresis…

Because you stop and start at the thresholds, losing the least bit of water at the bottom kicks the fill pump on briefly. And accumulating the least bit of water at the top kicks the drain pump on briefly.

This is an odd design decision. I spent time looking for the typical fill until it is full kind of thing.

A third sensor in the middle, or two somewhat spaced out sensors at each end would stop the chatter.

As would just letting either pump run for a certain limited time, nowhere near long enough to overflow or under drain.

So I am curious.

But if the logic works with proxies for the pumps and sensors, then indeed it is a hardware issue. Wiring, switch sense and pull-ups and power supply.

Those can and should be ferreted out independently of all the other stuff, that is to say with a test torture sketch that can survive turning on and off the pimps.

a7