Automatic Water Dispensing Machine

Automatic Water Dispensing Machine

01 Overview

The system uses an ESP32 as the main controller. The user interacts with the machine through four push buttons and a 1.3-inch OLED display. The user can select a preset of 146, 183, or 365 droplets.

Once a preset is selected, the ESP32 controls a 12 V peristaltic pump through an L298N motor driver. The pump transfers water through tubing and dispenses it at the output. The dispensing duration is calculated using the calibrated droplet rate.

The system also includes an ultrasonic sensor for checking the water level. It takes readings for approximately five seconds and calculates an average distance from valid readings.

Project Use Case

The machine automatically dispenses a selected amount of water. The user selects a preset, starts the operation, and the system runs the pump for the calculated duration.

02 Hardware and Software Components

Gather the components below before starting the assembly.

Hardware Components

Component Description
ESP32 DevKit Main microcontroller
1.3-inch 128×64 OLED Displays menus, selected presets, progress, and sensor results
12 V Peristaltic Pump Moves and dispenses water
L298N Motor Driver Controls the pump motor
Ultrasonic Sensor Measures distance for water-level checking
4 Push Buttons Menu navigation, selection, and stopping
2 LEDs Indicates system status
Passive Buzzer Audible completion notification
12 V Power Supply Supplies power to the pump driver
Breadboard Temporary circuit connections
Jumper Wires Connects the components
Resistors Current limiting and appropriate signal connections

Software Tools

Software Purpose
Arduino IDE Used to compile and upload the program
ESP32 Board Package Adds ESP32 board support to Arduino IDE
U8g2 Library Controls the OLED display
Wire Library Provides I2C communication support

Project Files

Download the project files from the repository:

https://github.com/XyValB/Automatic-Water-Dispensing-Machine

File Description
Automatic_Water_Dispensing_Machine.ino Main program for the system
Safety Note: Disconnect power before changing wiring. Keep electronic components away from water, insulate connections, and use the proper external power supply for the pump.

03 Application Discussion

The ESP32 receives button inputs, controls the OLED display, operates the pump through the L298N driver, reads the ultrasonic sensor, and controls the LED and buzzer.

OLED Display and Push Buttons

  • OLED: Displays menus, presets, dispensing progress, and water-level results.
  • UP: Moves through menu options.
  • DOWN: Moves through menu options.
  • SELECT: Confirms an option or starts an operation.
  • BACK: Returns to the previous menu or stops dispensing.

Peristaltic Pump and L298N Driver

The peristaltic pump moves water through the tubing. The L298N provides the motor-driving interface between the ESP32 control signals and the pump's external power supply.

Ultrasonic Sensor, LED, and Buzzer

The ultrasonic sensor measures distance for water-level checking. The LED indicates the system's operating status, while the buzzer provides an audible notification when dispensing is complete.

04 Hardware Setup

Wiring Diagram

Block diagram of the Automatic Water Dispensing Machine

Connect each component to the assigned ESP32 pins.

Schematic Diagram

Block diagram of the Automatic Water Dispensing Machine

The wiring diagram shows how the ESP32, OLED, L298N, pump, ultrasonic sensor, buttons, LEDs, and buzzer are connected.

Pin Mapping

Component Pin ESP32 / Power Description
OLED VCC 3.3 V OLED power
OLED GND GND Ground
OLED SDA GPIO 21 I2C data
OLED SCL GPIO 22 I2C clock
L298N ENA GPIO 26 Pump speed control
L298N IN1 GPIO 27 Motor direction
L298N IN2 GPIO 14 Motor direction
L298N GND Common GND Shared ground
L298N +12V 12 V supply positive Motor power
L298N OUT1 Pump wire 1 Pump output
L298N OUT2 Pump wire 2 Pump output
Ultrasonic VCC Suitable sensor supply Sensor power
Ultrasonic GND GND Ground
Ultrasonic TRIG GPIO 5 Trigger
Ultrasonic ECHO GPIO 18 through level shifting/divider if needed Echo signal
UP Button GPIO 32 Input
DOWN Button GPIO 33 Input
SELECT Button GPIO 25 Input
BACK/STOP Button GPIO 13 Input
LEDs GPIO 23 through individual resistors Status indication
Buzzer positive GPIO 19 Buzzer signal
Buzzer negative GND Ground

Assembly Instructions

  1. Connect the OLED to the ESP32 using the I2C pins.
  2. Connect the four buttons to their assigned GPIO pins.
  3. Connect the L298N to the ESP32 and the external 12 V supply.
  4. Connect the pump to the L298N output terminals.
  5. Connect the ultrasonic sensor to its assigned pins.
  6. Connect the LEDs through individual current-limiting resistors.
  7. Connect the buzzer to its assigned pin and ground.
  8. Check all wiring and confirm a common ground before powering the system.
Important: Many ultrasonic modules use a 5 V supply and may output a 5 V ECHO signal. ESP32 GPIO pins are not 5 V tolerant. Use a suitable voltage divider or level shifter on ECHO, and verify the sensor's voltage requirements before connecting it.

05 Software Setup

Download the code from the "https://github.com/XyValB/Automatic-Water-Dispensing-Machine" project repository before proceeding.

Step 1 — Install Arduino IDE

  1. Install and open Arduino IDE.
  2. Install ESP32 board support through Boards Manager.

Step 2 — Board Settings

Setting Value
Board ESP32 Dev Module
Upload Speed 115200
Port COM port assigned to the ESP32

Step 3 — Install Libraries

  • U8g2
  • Wire (included with the ESP32 Arduino environment)

Step 4 — Upload the Code

  1. Connect the ESP32 to the computer using USB.
  2. Select the correct board and COM port.
  3. Open Automatic_Water_Dispensing_Machine.ino.
  4. Click Verify to compile the code.
  5. Click Upload and wait for the process to finish.
  6. Restart the ESP32 if necessary.

06 Code

The complete Arduino sketch is shown below. Scroll inside the black code box to view the entire program. Use the Copy button to copy the code.

Automatic_Water_Dispensing_Machine.ino

Arduino / C++
#include <Wire.h>
#include <U8g2lib.h>

// OLED
U8G2_SH1106_128X64_NONAME_F_HW_I2C oled(U8G2_R0, U8X8_PIN_NONE);
#define OLED_SDA 21
#define OLED_SCL 22

// L298N MOTOR DRIVER
#define ENA_PIN 26
#define IN1_PIN 27
#define IN2_PIN 14

// ULTRASONIC SENSOR
#define TRIG_PIN 5
#define ECHO_PIN 18

// BUTTONS
#define BUTTON_UP     32
#define BUTTON_DOWN   33
#define BUTTON_SELECT 25
#define BUTTON_BACK   13

// LED AND BUZZER
#define LED_PIN 23
#define BUZZER_PIN 19

// PUMP SETTINGS
float dropletsPerMinute = 365.0;
int pumpSpeed = 255;

const int droplets[] = {146, 183, 365};
const int numberOfDropletPresets = 3;

const unsigned long LEVEL_CHECK_TIME = 5000;
const unsigned long LEVEL_READ_INTERVAL = 100;

bool lastUpState = HIGH;
bool lastDownState = HIGH;
bool lastSelectState = HIGH;
bool lastBackState = HIGH;

enum SystemState {
  MODE_MENU, DROPLET_MENU, PRESET_CONFIRM, PRESET_RUNNING,
  LEVEL_MENU, LEVEL_CHECKING, LEVEL_RESULT
};

SystemState state = MODE_MENU;

int selectedMode = 0;
int selectedDroplet = 0;
float finalLevelDistance = 0.0;
bool levelCheckSuccessful = false;

unsigned long lastBlinkTime = 0;
bool blinkState = false;
const unsigned long blinkInterval = 500;

void showReadyLED() {
  digitalWrite(LED_PIN, HIGH);
}

void startDispensingLED() {
  lastBlinkTime = millis();
  blinkState = false;
  digitalWrite(LED_PIN, LOW);
}

void updateDispensingLEDs() {
  if (millis() - lastBlinkTime >= blinkInterval) {
    lastBlinkTime = millis();
    blinkState = !blinkState;
    digitalWrite(LED_PIN, blinkState);
  }
}

void drinkReadyBuzzer() {
  for (int i = 0; i < 3; i++) {
    tone(BUZZER_PIN, 2000);
    delay(500);
    noTone(BUZZER_PIN);
    delay(500);
  }
}

void pumpON() {
  digitalWrite(IN1_PIN, LOW);
  digitalWrite(IN2_PIN, HIGH);
  analogWrite(ENA_PIN, pumpSpeed);
}

void pumpOFF() {
  analogWrite(ENA_PIN, 0);
  digitalWrite(IN1_PIN, LOW);
  digitalWrite(IN2_PIN, LOW);
}

bool buttonPressed(int pin, bool &lastState) {
  bool currentState = digitalRead(pin);

  if (lastState == HIGH && currentState == LOW) {
    lastState = LOW;
    delay(300);
    return true;
  }

  lastState = currentState;
  return false;
}

float getDistanceCM() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  long duration = pulseIn(ECHO_PIN, HIGH, 30000);

  if (duration == 0) return -1;

  float distance = duration * 0.0343 / 2.0;

  if (distance < 2.0 || distance > 400.0) return -1;

  return distance;
}

void showModeMenu() {
  oled.clearBuffer();
  oled.setFont(u8g2_font_6x10_tf);
  oled.drawStr(0, 10, "AUTOMATIC DISPENSER");
  oled.drawStr(0, 27, "SELECT MODE:");

  if (selectedMode == 0) {
    oled.drawStr(0, 43, "> DROPLET MODE");
    oled.drawStr(0, 57, "  LEVEL SENSING");
  } else {
    oled.drawStr(0, 43, "  DROPLET MODE");
    oled.drawStr(0, 57, "> LEVEL SENSING");
  }

  oled.sendBuffer();
}

void showDropletMenu() {
  oled.clearBuffer();
  oled.setFont(u8g2_font_6x10_tf);
  oled.drawStr(0, 10, "SELECT DROPLETS");

  for (int i = 0; i < numberOfDropletPresets; i++) {
    int y = 27 + (i * 12);
    oled.drawStr(0, y, (i == selectedDroplet) ? ">" : " ");

    char buffer[20];
    sprintf(buffer, "%d DROPS", droplets[i]);
    oled.drawStr(15, y, buffer);
  }

  oled.sendBuffer();
}

void showPresetConfirm() {
  oled.clearBuffer();
  oled.setFont(u8g2_font_6x10_tf);
  oled.drawStr(0, 12, "DROPLET PRESET");

  char buffer[30];
  sprintf(buffer, "TARGET: %d DROPS", droplets[selectedDroplet]);
  oled.drawStr(0, 30, buffer);
  oled.drawStr(0, 45, "SELECT = START");
  oled.drawStr(0, 58, "BACK = CANCEL");
  oled.sendBuffer();
}

void showRunning(float currentDrops, float targetDrops) {
  oled.clearBuffer();
  oled.setFont(u8g2_font_6x10_tf);
  oled.drawStr(0, 10, "DISPENSING...");

  char buffer[30];
  sprintf(buffer, "TARGET: %.0f DROPS", targetDrops);
  oled.drawStr(0, 28, buffer);

  sprintf(buffer, "DONE: %.0f DROPS", currentDrops);
  oled.drawStr(0, 42, buffer);
  oled.drawStr(0, 58, "BACK = STOP");
  oled.sendBuffer();
}

void showLevelMenu() {
  oled.clearBuffer();
  oled.setFont(u8g2_font_6x10_tf);
  oled.drawStr(0, 10, "LEVEL SENSING");
  oled.drawStr(0, 27, "CHECK WATER LEVEL");
  oled.drawStr(0, 43, "SELECT = CHECK");
  oled.drawStr(0, 58, "BACK = RETURN");
  oled.sendBuffer();
}

void showLevelChecking(unsigned long elapsed) {
  oled.clearBuffer();
  oled.setFont(u8g2_font_6x10_tf);
  oled.drawStr(0, 12, "CHECKING LEVEL...");

  unsigned long seconds = elapsed / 1000;
  if (seconds > 5) seconds = 5;

  char buffer[30];
  sprintf(buffer, "TIME: %lu / 5 sec", seconds);
  oled.drawStr(0, 30, buffer);
  oled.drawStr(0, 46, "PLEASE WAIT...");
  oled.drawStr(0, 60, "BACK = CANCEL");
  oled.sendBuffer();
}

void showLevelResult() {
  oled.clearBuffer();
  oled.setFont(u8g2_font_6x10_tf);

  if (levelCheckSuccessful) {
    oled.drawStr(0, 10, "WATER LEVEL");

    char buffer[30];
    sprintf(buffer, "DISTANCE: %.1f cm", finalLevelDistance);
    oled.drawStr(0, 28, buffer);
    oled.drawStr(0, 44, "WATER DETECTED");
    oled.drawStr(0, 59, "SELECT/BACK");
  } else {
    oled.drawStr(0, 10, "LEVEL SENSOR");
    oled.drawStr(0, 28, "ERROR!");
    oled.drawStr(0, 43, "CHECK WIRING");
    oled.drawStr(0, 59, "SELECT/BACK");
  }

  oled.sendBuffer();
}

void checkWaterLevel() {
  unsigned long startTime = millis();
  unsigned long lastReadingTime = 0;
  float distanceSum = 0;
  int validReadings = 0;

  levelCheckSuccessful = false;
  finalLevelDistance = 0;

  pumpOFF();
  showReadyLED();

  while (millis() - startTime < LEVEL_CHECK_TIME) {
    if (digitalRead(BUTTON_BACK) == LOW) {
      delay(300);
      lastBackState = HIGH;
      state = MODE_MENU;
      showModeMenu();
      return;
    }

    if (millis() - lastReadingTime >= LEVEL_READ_INTERVAL) {
      lastReadingTime = millis();

      float distance = getDistanceCM();

      if (distance > 0) {
        distanceSum += distance;
        validReadings++;
      }

      showLevelChecking(millis() - startTime);
    }
  }

  if (validReadings > 0) {
    finalLevelDistance = distanceSum / validReadings;
    levelCheckSuccessful = true;
  } else {
    levelCheckSuccessful = false;
  }

  state = LEVEL_RESULT;
  showLevelResult();
}

void runPresetDispensing() {
  float targetDrops = droplets[selectedDroplet];
  float timeMinutes = targetDrops / dropletsPerMinute;
  unsigned long totalTime = timeMinutes * 60.0 * 1000.0;
  unsigned long startTime = millis();

  startDispensingLED();
  pumpON();

  while (millis() - startTime < totalTime) {
    updateDispensingLEDs();

    if (digitalRead(BUTTON_BACK) == LOW) {
      pumpOFF();
      showReadyLED();
      state = MODE_MENU;
      delay(300);
      lastBackState = HIGH;
      showModeMenu();
      return;
    }

    unsigned long elapsed = millis() - startTime;
    float progress = (float)elapsed / (float)totalTime;
    float currentDrops = targetDrops * progress;

    if (currentDrops > targetDrops) {
      currentDrops = targetDrops;
    }

    showRunning(currentDrops, targetDrops);
    delay(100);
  }

  pumpOFF();
  showReadyLED();
  drinkReadyBuzzer();

  state = MODE_MENU;
  showModeMenu();
}

void setup() {
  Serial.begin(115200);

  Wire.begin(OLED_SDA, OLED_SCL);
  oled.begin();

  pinMode(ENA_PIN, OUTPUT);
  pinMode(IN1_PIN, OUTPUT);
  pinMode(IN2_PIN, OUTPUT);
  pumpOFF();

  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);

  pinMode(BUTTON_UP, INPUT_PULLUP);
  pinMode(BUTTON_DOWN, INPUT_PULLUP);
  pinMode(BUTTON_SELECT, INPUT_PULLUP);
  pinMode(BUTTON_BACK, INPUT_PULLUP);

  pinMode(LED_PIN, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  noTone(BUZZER_PIN);

  showReadyLED();
  showModeMenu();
}

void loop() {
  if (state == MODE_MENU) {
    if (buttonPressed(BUTTON_UP, lastUpState)) {
      selectedMode = (selectedMode <= 0) ? 1 : selectedMode - 1;
      showModeMenu();
    }

    if (buttonPressed(BUTTON_DOWN, lastDownState)) {
      selectedMode = (selectedMode >= 1) ? 0 : selectedMode + 1;
      showModeMenu();
    }

    if (buttonPressed(BUTTON_SELECT, lastSelectState)) {
      if (selectedMode == 0) {
        selectedDroplet = 0;
        state = DROPLET_MENU;
        showDropletMenu();
      } else {
        state = LEVEL_MENU;
        showLevelMenu();
      }
    }
  }

  else if (state == DROPLET_MENU) {
    if (buttonPressed(BUTTON_UP, lastUpState)) {
      selectedDroplet =
        (selectedDroplet <= 0)
        ? numberOfDropletPresets - 1
        : selectedDroplet - 1;

      showDropletMenu();
    }

    if (buttonPressed(BUTTON_DOWN, lastDownState)) {
      selectedDroplet =
        (selectedDroplet >= numberOfDropletPresets - 1)
        ? 0
        : selectedDroplet + 1;

      showDropletMenu();
    }

    if (buttonPressed(BUTTON_SELECT, lastSelectState)) {
      state = PRESET_CONFIRM;
      showPresetConfirm();
    }

    if (buttonPressed(BUTTON_BACK, lastBackState)) {
      state = MODE_MENU;
      showModeMenu();
    }
  }

  else if (state == PRESET_CONFIRM) {
    if (buttonPressed(BUTTON_SELECT, lastSelectState)) {
      state = PRESET_RUNNING;
      runPresetDispensing();
    }

    if (buttonPressed(BUTTON_BACK, lastBackState)) {
      state = DROPLET_MENU;
      showDropletMenu();
    }
  }

  else if (state == LEVEL_MENU) {
    if (buttonPressed(BUTTON_SELECT, lastSelectState)) {
      state = LEVEL_CHECKING;
      checkWaterLevel();
    }

    if (buttonPressed(BUTTON_BACK, lastBackState)) {
      state = MODE_MENU;
      showModeMenu();
    }
  }

  else if (state == LEVEL_RESULT) {
    if (buttonPressed(BUTTON_SELECT, lastSelectState)) {
      state = LEVEL_CHECKING;
      checkWaterLevel();
    }

    if (buttonPressed(BUTTON_BACK, lastBackState)) {
      state = MODE_MENU;
      showModeMenu();
    }
  }
}

07 Code Breakdown

The program initializes the hardware, displays the menu, reads button inputs, operates the pump, and checks the water level.

Libraries

Library Purpose
Wire.h I2C communication with the OLED
U8g2lib.h Controls the SH1106 OLED display

Key Functions

Function Description
pumpON() Starts the pump through the L298N driver.
pumpOFF() Stops the pump.
getDistanceCM() Measures the ultrasonic sensor distance in centimeters.
checkWaterLevel() Collects and averages valid sensor readings for approximately five seconds.
showDropletMenu() Displays the available droplet presets.
showPresetConfirm() Displays the selected preset before dispensing.
runPresetDispensing() Calculates pump operating time and controls the dispensing operation.
drinkReadyBuzzer() Sounds three beeps after dispensing finishes.
showReadyLED() Turns on the ready indicator.
updateDispensingLEDs() Blinks the LED during dispensing.

General Program Workflow

  1. The ESP32 initializes the OLED, buttons, pump driver, sensor, LED, and buzzer.
  2. The main menu displays Droplet Mode and Level Sensing.
  3. The user selects a dispensing preset.
  4. The user confirms the selected preset.
  5. The pump starts and the LED blinks.
  6. The OLED displays the target and estimated progress.
  7. The pump stops when the calculated duration is reached, or when BACK is pressed.
  8. The LED returns to the ready state.
  9. The buzzer sounds when the dispensing operation completes.

08 Testing and Calibration

Time-Based Droplet Calibration

The pump is operated for a calibrated duration to estimate the number of droplets dispensed. The results below are the initial calibration values and should be verified with repeated tests.

Preset Approximate Time Target Droplets
Preset 1 24 seconds 146 droplets
Preset 2 30 seconds 183 droplets
Preset 3 60 seconds 365 droplets
Common Issue: Droplet output may vary with the water supply, tubing, pump speed, and operating conditions. Repeat the calibration several times and use the measured average to adjust the pump's droplet rate. The progress displayed by this sketch is estimated from elapsed time; it does not count actual droplets.

09 System Demonstration

The following diagrams show the system structure and component connections.

Video Demonstration

Automatic Water Dispensing Machine demonstration

Automatic Water Dispensing Machine tutorial

Automatic Water Dispensing Machine project video

10 Conclusion

An ESP32-based automatic water dispensing machine was designed using an OLED display, peristaltic pump, L298N driver, and ultrasonic sensor. The project demonstrates time-calibrated dispensing with status feedback through the display, LED, and buzzer.

Possible Improvements and Future Enhancements

  • Improve water-level detection.
  • Improve pump calibration for more accurate dispensing.
  • Improve tubing and nozzle design for consistent flow.
  • Add more dispensing presets.
  • Add a sensor to measure the actual amount dispensed.

11 References

  • U8g2 library documentation
  • L298N documentation and datasheet
  • INTLLAB peristaltic pump specifications
  • Ultrasonic sensor documentation

12 Project Authors

  • Xyrus Val Bustillo
  • Davie Jake Balutos
Automatic Water Dispensing Machine – CreateLabz

 

#buzzer, #esp32 devkit, #lm2596, #oled display, #peristaltic pump, #ultrasonic sensor

Leave a comment

All comments are moderated before being published