Community project

Carwash Vendo Timer Dashboard

ESP32
Photo of Carwash Vendo Timer Dashboard

arby pullante

Published October 11, 2026

This carwash vendo timer dashboard turns an ESP32 microcontroller into a coin-operated control system for managing multiple wash bays or equipment. The system accepts coins via a 12V multi-coin acceptor, displays credits and machine status on a 20x4 LCD screen, and triggers four independent 12V loads through opto-isolated relays when customers select their wash cycle.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for mounting the low-voltage control electronics and connecting the coin acceptor, LCD display, push buttons, buzzer, and relay outputs. The included firmware handles coin debouncing, credit tracking, machine configuration, and a web-based admin interface for setting prices and cycle times—everything needed to deploy a functional carwash payment system.

Wiring diagram

Wiring diagram for Carwash Vendo Timer Dashboard

Gather all the parts

QtyComponent
1

I2C 20x4 Arduino LCD Display Module

20x4 character LCD with I2C interface, white text on blue backlight, adjustable contrast. Operates at 5V or 3.3V; reduces pin usage to 2 lines (SDA/SCL).

1

Buzzer

Piezo buzzer for sound output

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

4-Channel 5 V Opto-Isolated Relay Module (3.3 V trigger compatible)

A ready-made four-relay board that lets the timer safely switch the four machine control circuits.

1

12 V Multi-Coin Acceptor

A panel-mounted electronic coin slot that sends one pulse for each accepted one-peso coin.

1

12 V Coin Pulse Opto-Isolator Module

A ready-made opto-isolator board that safely converts the coin acceptor's 12 V pulse into a 3.3 V ESP32 input.

1

12V Barrel-Jack Adapter

12 V / 2 A wall adapter with a 5.5 mm / 2.1 mm barrel jack. Used to power motor drivers, LED strips, or boards that need a higher rail.

1

USB-C 5V Adapter

USB-C wall adapter delivering regulated 5 V to the board's USB or VBUS rail. Default wired power source for desktop / stationary projects.

Assemble it in 6 steps

1. Mount the low-voltage control parts

Put the ESP32 board, the 20×4 display, the four start buttons, the buzzer, the relay board, the coin-pulse opto-isolator, and both power supplies inside a dry plastic control enclosure. Keep the 12 V coin wiring and any machine wiring separated from the ESP32 wiring.

  • Label the buttons 1 through 4 to match WATER 1, WATER 2, BLOWER, and SHAMPOO before wiring.
  • Use a proper strain relief where the coin-slot and machine-control cables enter the enclosure.
  • Do not put mains-voltage wires on a breadboard or beside the ESP32. The relay contacts must only be connected by a qualified person to the machine’s documented low-voltage enable input or a suitably rated, enclosed contactor system.

2. Wire the display and buzzer

Connect the display VCC to ESP32 3V3 (power), display GND to ESP32 GND (ground), display SDA to GPIO21 (data), and display SCL to GPIO22 (data). Connect the buzzer SIGNAL to GPIO26 (sound signal) and buzzer GND to ESP32 GND (ground).

  • A 2004 I2C display must be powered at 3.3 V here so its SDA and SCL wires stay safe for the ESP32.
  • If the screen lights but shows no words after flashing, its I2C address may be 0x3F instead of 0x27.
  • Make sure VCC and GND are not swapped — swapped power can damage the display.

3. Wire the four start buttons

For each button, connect one terminal to GND and the other terminal to its ESP32 signal pin: button 1 to GPIO18 (start signal), button 2 to GPIO19 (start signal), button 3 to GPIO23 (start signal), and button 4 to GPIO25 (start signal).

  • The code uses the ESP32’s built-in pull-up resistors, so no separate resistor is needed for these buttons.
  • A quick tap counts exactly once.
  • Do not connect a button signal wire to 5 V; the ESP32 input pins are 3.3 V only.

4. Wire the isolated coin input

Power the coin acceptor from the 12 V adapter: adapter +12V to coin slot +12V (power) and adapter GND to coin slot GND (ground). Connect the coin slot PULSE wire to the opto-isolator COIN+ input (coin signal), and connect opto-isolator COIN- back to the coin slot GND (return). On the safe ESP32 side, connect the opto-isolator VCC to 3V3 (power), GND to ESP32 GND (ground), and OUT to GPIO27 (coin signal).

  • Set the coin acceptor to output one pulse for each accepted one-peso coin before testing.
  • The opto-isolator keeps the 12 V coin slot electrically separated from the ESP32 input.
  • Never connect the coin acceptor’s 12 V PULSE wire directly to GPIO27 — 12 V can permanently damage the ESP32.

5. Wire the relay control inputs

Connect the relay board VCC to regulated 5 V (power) and relay GND to the ESP32 GND (ground). Connect IN1 to GPIO13 (machine 1 control), IN2 to GPIO14 (machine 2 control), IN3 to GPIO16 (machine 3 control), and IN4 to GPIO17 (machine 4 control). Connect each relay COM and NO pair only to the matching machine’s low-voltage enable circuit: relay 1 for machine 1, relay 2 for machine 2, relay 3 for machine 3, and relay 4 for machine 4.

  • Use a relay board explicitly marked as accepting 3.3 V logic triggers; some 5 V relay boards do not reliably recognize ESP32 signals.
  • The software turns every relay off during startup and only allows one relay on at a time.
  • Do not wire the ESP32 directly to a pump, motor, valve, or mains supply. Switching a high-power machine needs correctly rated enclosed contactors, fuses, earthing, and a qualified electrician.

6. Power and test safely

Power the ESP32 from its USB port using the regulated 5 V USB supply (power). Turn on the separate 12 V adapter for the coin acceptor. Before connecting a real machine, test each relay using its indicator light and test the buttons, display, coin pulses, buzzer, and Wi-Fi page.

  • After the controller starts, connect a phone to Wi-Fi network Carwash-Vendo using password carwash123; the phone’s sign-in page should open the dashboard.
  • After a power interruption, the saved time is shown as paused and every relay remains off; pressing its start button resumes that one machine.
  • Use a fuse appropriate to each power supply and do not power the ESP32 from more than one 5 V source at the same time.

Review all connections

1. Connections between "lcd2004_1" and "ESP32"

Functionlcd2004_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

2. Connections between "buzzer_1" and "ESP32"

Functionbuzzer_1ESP32
groundGNDGND
digitalSIGNALGPIO 26

3. Connections between "start_button_1" and "ESP32"

Functionstart_button_1ESP32
groundGNDGND
digitalSIGNALGPIO 18

4. Connections between "start_button_2" and "ESP32"

Functionstart_button_2ESP32
groundGNDGND
digitalSIGNALGPIO 19

5. Connections between "start_button_3" and "ESP32"

Functionstart_button_3ESP32
groundGNDGND
digitalSIGNALGPIO 23

6. Connections between "start_button_4" and "ESP32"

Functionstart_button_4ESP32
groundGNDGND
digitalSIGNALGPIO 25

7. Connections between "relay4_1" and "ESP32"

Functionrelay4_1ESP32
powerVCC5V
groundGNDGND
digitalIN1GPIO 13
digitalIN2GPIO 14
digitalIN3GPIO 16
digitalIN4GPIO 17
dataCOM1/NO1 → Machine 1 low-voltage enable input (dry contact only)EXT
dataCOM2/NO2 → Machine 2 low-voltage enable input (dry contact only)EXT
dataCOM3/NO3 → Machine 3 low-voltage enable input (dry contact only)EXT
dataCOM4/NO4 → Machine 4 low-voltage enable input (dry contact only)EXT

8. Connections between "supply12v_1" and "ESP32"

Functionsupply12v_1ESP32
power+12V → 12 V Multi-Coin Acceptor +12VEXT
groundGND → 12 V Multi-Coin Acceptor GNDEXT

9. Connections between "coin_acceptor_1" and "ESP32"

Functioncoin_acceptor_1ESP32
dataPULSE → 12 V Coin Pulse Opto-Isolator Module COIN+EXT

10. Connections between "coin_opto_1" and "ESP32"

Functioncoin_opto_1ESP32
groundCOIN- → 12 V Multi-Coin Acceptor GNDEXT
powerVCC3V3
groundGNDGND
digitalOUTGPIO 27

11. Connections between "supply5v_1" and "ESP32"

Functionsupply5v_1ESP32
power+5VVIN
groundGNDGND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <Preferences.h>
constexpr uint8_t LCD_SDA=21,LCD_SCL=22,BUZZER_PIN=26,COIN_PIN=27;
const uint8_t buttonPins[4]={18,19,23,25},relayPins[4]={13,14,16,17};
constexpr uint8_t RELAY_ON=LOW,RELAY_OFF=HIGH; constexpr int MINIMUM_CREDIT=5; constexpr uint32_t COIN_DEBOUNCE_MS=90;
LiquidCrystal_I2C lcd(0x27,20,4); WebServer server(80); DNSServer dns; Preferences prefs;
struct Machine { String name; uint16_t seconds,price; bool enabled; uint32_t sales; } machines[4];
int credits=0,activeMachine=-1; uint32_t coinsInserted=0,remainingSeconds=0,lastSecondMs=0,lastSaveMs=0,lastDisplayMs=0,lastCoinMs=0; bool previousButton[4]={HIGH,HIGH,HIGH,HIGH};
void beep(uint16_t ms=70){ledcWriteTone(BUZZER_PIN,2400);delay(ms);ledcWriteTone(BUZZER_PIN,0);} void errorBeep(){beep(70);delay(90);beep(70);} void setRelaysOff(){for(uint8_t i=0;i<4;i++)digitalWrite(relayPins[i],RELAY_OFF);}
String esc(String s){s.replace("&","&amp;");s.replace("<","&lt;");s.replace(">","&gt;");s.replace("\"","&quot;");return s;}
void saveState(){prefs.putInt("credit",credits);prefs.putUInt("coins",coinsInserted);prefs.putInt("active",activeMachine);prefs.putUInt("remain",remainingSeconds);for(uint8_t i=0;i<4;i++){String k=String(i);prefs.putString(("n"+k).c_str(),machines[i].name);prefs.putUShort(("t"+k).c_str(),machines[i].seconds);prefs.putUShort(("p"+k).c_str(),machines[i].price);prefs.putBool(("e"+k).c_str(),machines[i].enabled);prefs.putUInt(("s"+k).c_str(),machines[i].sales);}}
void loadState(){const char*d[4]={"WATER 1","WATER 2","BLOWER","SHAMPOO"};for(uint8_t i=0;i<4;i++){String k=String(i);machines[i].name=prefs.getString(("n"+k).c_str(),d[i]);machines[i].seconds=prefs.getUShort(("t"+k).c_str(),120);machines[i].price=prefs.getUShort(("p"+k).c_str(),5);machines[i].enabled=prefs.getBool(("e"+k).c_str(),true);machines[i].sales=prefs.getUInt(("s"+k).c_str(),0);}credits=prefs.getInt("credit",0);coinsInserted=prefs.getUInt("coins",0);activeMachine=prefs.getInt("active",-1);remainingSeconds=prefs.getUInt("remain",0);if(activeMachine<0||activeMachine>3||!remainingSeconds){activeMachine=-1;remainingSeconds=0;}}
void line(uint8_t r,String s){while(s.length()<20)s+=' ';lcd.setCursor(0,r);lcd.print(s.substring(0,20));}
void display(){if(millis()-lastDisplayMs<250)return;lastDisplayMs=millis();if(activeMachine>=0){uint32_t m=remainingSeconds/60,s=remainingSeconds%60;line(0,"RUN "+String(activeMachine+1)+" "+machines[activeMachine].name);line(1,"TIME "+String(m)+":"+(s<10?"0":"")+String(s));line(2,"CREDIT P"+String(credits)+" COINS "+String(coinsInserted));line(3,"Other machines LOCKED");}else{line(0,"CARWASH VENDO 4-IN-1");line(1,"CREDIT: P"+String(credits)+" COINS:"+String(coinsInserted));line(2,"Press 1-4 to start");line(3,"Minimum credit: P5");}}
void stopMachine(bool done){if(activeMachine<0)return;digitalWrite(relayPins[activeMachine],RELAY_OFF);if(done)beep(350);activeMachine=-1;remainingSeconds=0;saveState();}
void startMachine(uint8_t i){if(activeMachine>=0){errorBeep();return;}if(!machines[i].enabled||credits<(int)machines[i].price||credits<MINIMUM_CREDIT){errorBeep();return;}credits-=machines[i].price;machines[i].sales+=machines[i].price;activeMachine=i;remainingSeconds=machines[i].seconds;setRelaysOff();digitalWrite(relayPins[i],RELAY_ON);beep();saveState();}
void buttons(){for(uint8_t i=0;i<4;i++){bool n=digitalRead(buttonPins[i]);if(previousButton[i]&& !n){delay(20);if(!digitalRead(buttonPins[i]))startMachine(i);}previousButton[i]=n;}}
void coin(){static bool last=HIGH;bool n=digitalRead(COIN_PIN);if(last&&!n&&millis()-lastCoinMs>COIN_DEBOUNCE_MS){lastCoinMs=millis();credits++;coinsInserted++;beep(35);saveState();}last=n;}
String page(){String p="<!doctype html><html><head><meta name=viewport content='width=device-width,initial-scale=1'><title>Carwash Vendo</title><style>body{font-family:Arial;background:#eef3f7;color:#17212b;max-width:800px;margin:auto;padding:14px}h1{color:#075497}.c{background:#fff;border-radius:12px;padding:14px;margin:12px 0;box-shadow:0 2px 8px #bbc}input{width:75px;padding:7px;margin:2px}.n{width:120px}button{background:#075497;color:#fff;border:0;border-radius:7px;padding:10px;margin:3px;font-weight:bold}.d{background:#a22}table{width:100%;border-collapse:collapse}td,th{padding:6px;border-bottom:1px solid #ddd;text-align:left}</style></head><body><h1>CARWASH VENDO</h1><div class=c><b>Available credit: P"+String(credits)+" &nbsp; Coins inserted: "+String(coinsInserted)+"</b>";if(activeMachine>=0)p+="<p>RUNNING: <b>"+esc(machines[activeMachine].name)+"</b> — "+String(remainingSeconds/60)+":"+String(remainingSeconds%60)+" remaining. Other machines are locked.</p>";else p+="<p>No machine is running. Insert coins, then press its physical start button.</p>";p+="</div><form action=/save><div class=c><h2>Services, time and price</h2><table><tr><th>Service</th><th>Seconds</th><th>Price (P)</th><th>Available</th></tr>";for(uint8_t i=0;i<4;i++)p+="<tr><td><input class=n name=n"+String(i)+" value=\""+esc(machines[i].name)+"\"></td><td><input type=number min=1 name=t"+String(i)+" value="+String(machines[i].seconds)+"></td><td><input type=number min=5 name=p"+String(i)+" value="+String(machines[i].price)+"></td><td><input type=checkbox name=e"+String(i)+(machines[i].enabled?" checked":"")+"></td></tr>";p+="</table><button>Save services</button></div></form><div class=c><h2>Sales report</h2><table><tr><th>Service</th><th>Income</th></tr>";uint32_t total=0;for(uint8_t i=0;i<4;i++){total+=machines[i].sales;p+="<tr><td>"+esc(machines[i].name)+"</td><td>P"+String(machines[i].sales)+"</td></tr>";}p+="</table><p><b>Total income: P"+String(total)+"</b></p><a href=/clear><button class=d>Clear sales report</button></a></div><div class=c><h2>Wi-Fi configuration</h2><p>Connect to <b>Carwash-Vendo</b>, password <b>carwash123</b>. Your phone's sign-in page opens this dashboard automatically.</p><a href=/reboot><button>Restart controller</button></a></div></body></html>";return p;}
void web(){WiFi.mode(WIFI_AP);WiFi.softAP("Carwash-Vendo","carwash123");dns.start(53,"*",WiFi.softAPIP());server.on("/",[](){server.send(200,"text/html",page());});server.on("/save",[](){for(uint8_t i=0;i<4;i++){String x=String(i);if(server.hasArg("n"+x))machines[i].name=server.arg("n"+x);if(server.hasArg("t"+x))machines[i].seconds=constrain(server.arg("t"+x).toInt(),1,36000);if(server.hasArg("p"+x))machines[i].price=constrain(server.arg("p"+x).toInt(),5,10000);machines[i].enabled=server.hasArg("e"+x);}saveState();server.sendHeader("Location","/");server.send(303);});server.on("/clear",[](){for(auto&m:machines)m.sales=0;saveState();server.sendHeader("Location","/");server.send(303);});server.on("/reboot",[](){server.send(200,"text/plain","Restarting...");delay(250);ESP.restart();});server.onNotFound([](){server.sendHeader("Location",String("http://")+WiFi.softAPIP().toString()+"/");server.send(302,"text/plain","Opening dashboard");});server.begin();}
void setup(){for(uint8_t i=0;i<4;i++){pinMode(relayPins[i],OUTPUT);digitalWrite(relayPins[i],RELAY_OFF);pinMode(buttonPins[i],INPUT_PULLUP);}pinMode(COIN_PIN,INPUT_PULLUP);ledcAttach(BUZZER_PIN,2000,8);Wire.begin(LCD_SDA,LCD_SCL);lcd.init();lcd.backlight();prefs.begin("carwash",false);loadState();setRelaysOff();web();line(0,"CARWASH VENDO READY");line(1,"WiFi: Carwash-Vendo");line(2,"Password: carwash123");line(3,"Recovered time paused");delay(1300);}
void loop(){dns.processNextRequest();server.handleClient();buttons();coin();if(activeMachine>=0&&millis()-lastSecondMs>=1000){lastSecondMs+=1000;if(remainingSeconds)remainingSeconds--;if(remainingSeconds&&remainingSeconds<=5)beep(55);if(!remainingSeconds)stopMachine(true);}if(millis()-lastSaveMs>=1000){lastSaveMs=millis();if(activeMachine>=0)saveState();}display();}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik