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 |
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
Connect each component to the assigned ESP32 pins.
Schematic Diagram
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
- Connect the OLED to the ESP32 using the I2C pins.
- Connect the four buttons to their assigned GPIO pins.
- Connect the L298N to the ESP32 and the external 12 V supply.
- Connect the pump to the L298N output terminals.
- Connect the ultrasonic sensor to its assigned pins.
- Connect the LEDs through individual current-limiting resistors.
- Connect the buzzer to its assigned pin and ground.
- Check all wiring and confirm a common ground before powering the system.
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
- Install and open Arduino IDE.
- 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
- Connect the ESP32 to the computer using USB.
- Select the correct board and COM port.
- Open Automatic_Water_Dispensing_Machine.ino.
- Click Verify to compile the code.
- Click Upload and wait for the process to finish.
- 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
#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
- The ESP32 initializes the OLED, buttons, pump driver, sensor, LED, and buzzer.
- The main menu displays Droplet Mode and Level Sensing.
- The user selects a dispensing preset.
- The user confirms the selected preset.
- The pump starts and the LED blinks.
- The OLED displays the target and estimated progress.
- The pump stops when the calculated duration is reached, or when BACK is pressed.
- The LED returns to the ready state.
- 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 |
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
