#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Preferences.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ArduinoJson.h>
// Forward declarations (unchanged)
void displayLoadingScreen(unsigned long currentMillis);
void handleSettingsButton();
void handleSalesMonitoringTrigger(unsigned long currentMillis);
void loadSettings();
void saveSettings();
void loadSalesData();
void saveSalesData();
void resetSalesData();
void displaySalesMonitoring();
void handleSettingsButtons();
void adjustSettingsValue(bool increment);
void handleNormalMode(unsigned long currentMillis);
int getCurrentCreditCost();
void handleSettingsMode(unsigned long currentMillis);
void Add();
void updateTime();
void handleButton(int button, int relay, int &time, bool &active, bool &paused, int &pauseCount, int defaultTime, bool &buttonPressed, unsigned long &lastButtonTime, int creditCost, String service);
void displayInitialScreen();
void blinkInsertCoin();
void displayActiveScreen();
void updateBalanceDisplay();
void blinkPressButton();
void updateTimeDisplay();
void displayTime(int seconds, bool paused, int column, bool &blinkState, unsigned long &lastBlinkTime);
void handlePausedBlinking();
void updateLEDs(unsigned long currentMillis);
void updateSettingsDisplay();
void beep();
Preferences prefs;
LiquidCrystal_I2C lcd(0x27, 20, 4);
WebServer server(80);
// Wi-Fi AP settings (unchanged)
String ssid = "CarWashESP32";
String password = "12345678";
// ESP32-compatible pins (unchanged)
const int coinSlotPin = 2;
const int waterButton = 4;
const int soapButton = 5;
const int blowerButton = 14;
const int settingsButton = 13;
const int relayWater = 16;
const int relaySoap = 17;
const int relayBlower = 18;
const int waterLED = 25;
const int soapLED = 26;
const int blowerLED = 27;
const int buzzerPin = 19; // Buzzer pin (active-high)
// Constants (unchanged except for buzzer duration)
const int LCD_CLEAR_DELAY = 500;
const unsigned long SALES_DISPLAY_DURATION = 10000;
const unsigned long BLOWER_LONG_PRESS_TIME = 2000;
const unsigned long SOAP_LONG_PRESS_TIME = 2000;
const unsigned long BUZZER_BEEP_DURATION = 300; // Increased to 500ms for better Wokwi audio detection
const unsigned long BUZZER_COOLDOWN = 300; // Added: 1-second cooldown between beeps
int DEFAULT_WATER_TIME = 60;
int DEFAULT_SOAP_TIME = 45;
int DEFAULT_BLOWER_TIME = 75;
int WATER_CREDIT_COST = 5;
int SOAP_CREDIT_COST = 5;
int BLOWER_CREDIT_COST = 5;
const int MAX_PAUSE_COUNT = 3;
const unsigned long BLINK_INTERVAL = 450;
const unsigned long COIN_DEBOUNCE_TIME = 300;
const unsigned long BUTTON_DEBOUNCE_TIME = 200;
// Variables (unchanged except for adding buzzer cooldown tracking)
int balance = 0;
int waterTime = 0, soapTime = 0, blowerTime = 0;
bool waterActive = false, soapActive = false, blowerActive = false;
bool waterPaused = false, soapPaused = false, blowerPaused = false;
int waterPauseCount = 0, soapPauseCount = 0, blowerPauseCount = 0;
unsigned long lastCoinTime = 0, lastUpdateTime = 0, lastButtonBlinkTime = 0;
unsigned long lastInsertCoinBlinkTime = 0;
bool showInsertCoin = true, showPressButton = true;
bool screenInitialized = false;
bool waterBlinkState = false, soapBlinkState = false, blowerBlinkState = false;
unsigned long waterLastBlinkTime = 0, soapLastBlinkTime = 0, blowerLastBlinkTime = 0;
// Buzzer state tracking (unchanged)
bool waterBeeped10 = false, waterBeeped6 = false, waterBeeped2 = false;
bool soapBeeped10 = false, soapBeeped6 = false, soapBeeped2 = false;
bool blowerBeeped10 = false, blowerBeeped6 = false, blowerBeeped2 = false;
// Added: Track last beep time for cooldown
unsigned long lastBeepTime = 0;
volatile int coinsInserted = 0;
const int COIN_VALUE = 1;
bool waterButtonPressed = false;
bool soapButtonPressed = false;
bool blowerButtonPressed = false;
unsigned long lastWaterButtonTime = 0;
unsigned long lastSoapButtonTime = 0;
unsigned long lastBlowerButtonTime = 0;
unsigned long lastSettingsButtonTime = 0;
int settingsMode = 0;
int settingsField = 1;
bool settingsBlinkState = false;
unsigned long lastSettingsBlinkTime = 0;
// Loading screen state (unchanged)
bool loadingComplete = false;
int loadingPercent = 0;
unsigned long lastLoadingUpdate = 0;
// Sales monitoring variables (unchanged)
bool salesMonitoringMode = false;
unsigned long salesDisplayStartTime = 0;
int waterSales = 0;
int soapSales = 0;
int blowerSales = 0;
// HTML for web interface (unchanged)
const char* htmlPage = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Car Wash Control Panel</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
background: linear-gradient(135deg, #1e3a8a, #3b82f6);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
color: #333;
padding: 1rem;
}
.container {
max-width: 400px;
width: 100%;
background: #ffffff;
border-radius: 15px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
padding: 2rem;
margin: 1rem 0;
transition: transform 0.3s ease;
}
.container:hover { transform: translateY(-5px); }
h1 {
font-size: 1.8rem;
font-weight: 700;
color: #1e3a8a;
text-align: center;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 1px;
}
h2 {
font-size: 1.3rem;
font-weight: 700;
color: #1e3a8a;
text-align: center;
margin: 0.5rem 0 0.5rem;
}
hr { border: none; border-top: 1px solid #cbd5e1; margin: 1rem 0; }
.sales-box, .wifi-box, .timer-box, .pricing-box { display: flex; flex-direction: column; gap: 1rem; }
.sales-item {
display: flex;
justify-content: space-between;
font-size: 1.1rem;
padding: 0.75rem;
background: #f8fafc;
border-radius: 8px;
transition: background 0.2s ease;
}
.sales-item:hover { background: #e2e8f0; }
.sales-item span { font-weight: 700; color: #2dd4bf; }
.wifi-item, .timer-item, .pricing-item { display: flex; flex-direction: column; gap: 0.5rem; }
.wifi-item label, .timer-item label, .pricing-item label {
font-size: 1rem;
font-weight: 500;
color: #1e3a8a;
}
.wifi-item input, .timer-item input, .pricing-item input {
padding: 0.75rem;
font-size: 1rem;
border: 1px solid #cbd5e1;
border-radius: 8px;
outline: none;
transition: border-color 0.3s ease;
}
.wifi-item input:focus, .timer-item input:focus, .pricing-item input:focus { border-color: #2dd4bf; }
.password-container { position: relative; display: flex; align-items: center; }
.password-container input { width: 100%; padding-right: 2.5rem; }
.toggle-password {
position: absolute;
right: 0.75rem;
cursor: pointer;
color: #2dd4bf;
font-size: 1.2rem;
transition: color 0.3s ease;
}
.toggle-password:hover { color: #1e3a8a; }
.btn {
display: block;
width: 100%;
padding: 0.75rem;
margin-top: 1rem;
background: #ef4444;
color: #fff;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 400;
cursor: pointer;
transition: background 0.3s ease, transform 0.2s ease;
}
.btn:hover { background: #dc2626; transform: scale(1.02); }
.btn:active { transform: scale(0.98); }
@media (max-width: 400px) {
.container { padding: 1.5rem; }
h1 { font-size: 1.5rem; }
h2 { font-size: 1.1rem; }
.sales-item, .wifi-item input, .timer-item input, .pricing-item input { font-size: 0.9rem; }
.toggle-password { font-size: 1rem; }
}
</style>
</head>
<body>
<div class="container">
<h1>CAR WASH SALES</h1>
<hr>
<div class="sales-box">
<div class="sales-item">Water Sales: <span id="waterSales">0</span></div>
<div class="sales-item">Soap Sales: <span id="soapSales">0</span></div>
<div class="sales-item">Blower Sales: <span id="blowerSales">0</span></div>
<div class="sales-item">Total Sales: <span id="totalSales">0</span></div>
</div>
<button class="btn" onclick="resetSales()">Reset Sales</button>
</div>
<div class="container">
<h2>Wi-Fi Configuration</h2>
<hr>
<div class="wifi-box">
<div class="wifi-item">
<label for="ssid">Wi-Fi SSID</label>
<input type="text" id="ssid" maxlength="32">
</div>
<div class="wifi-item">
<label for="password">Password</label>
<div class="password-container">
<input type="password" id="password" maxlength="64">
<span class="toggle-password" onclick="togglePassword()">👁️</span>
</div>
</div>
</div>
<button class="btn" onclick="saveWifi()">Save Wi-Fi Settings</button>
</div>
<div class="container">
<h2>Wash Timer</h2>
<hr>
<div class="timer-box">
<div class="timer-item">
<label for="waterTime">Water Time (seconds)</label>
<input type="number" id="waterTime" min="5" max="999">
</div>
<div class="timer-item">
<label for="soapTime">Soap Time (seconds)</label>
<input type="number" id="soapTime" min="5" max="999">
</div>
<div class="timer-item">
<label for="blowerTime">Blower Time (seconds)</label>
<input type="number" id="blowerTime" min="5" max="999">
</div>
</div>
<button class="btn" onclick="saveTimer()">Save Wash Timer</button>
</div>
<div class="container">
<h2>Service Pricing</h2>
<hr>
<div class="pricing-box">
<div class="pricing-item">
<label for="waterPrice">Water Price (credits)</label>
<input type="number" id="waterPrice" min="1" max="20">
</div>
<div class="pricing-item">
<label for="soapPrice">Soap Price (credits)</label>
<input type="number" id="soapPrice" min="1" max="20">
</div>
<div class="pricing-item">
<label for="blowerPrice">Blower Price (credits)</label>
<input type="number" id="blowerPrice" min="1" max="20">
</div>
</div>
<button class="btn" onclick="savePricing()">Save Service Pricing</button>
</div>
<div class="container">
<h2>System Control</h2>
<hr>
<button class="btn" onclick="rebootESP()">Reboot ESP32</button>
</div>
<script>
function updateSales() {
fetch('/sales')
.then(response => response.json())
.then(data => {
document.getElementById('waterSales').innerText = data.water;
document.getElementById('soapSales').innerText = data.soap;
document.getElementById('blowerSales').innerText = data.blower;
document.getElementById('totalSales').innerText = data.total;
})
.catch(error => console.error('Error fetching sales:', error));
}
function resetSales() {
if (confirm('Are you sure you want to reset all sales data?')) {
fetch('/reset', { method: 'POST' })
.then(() => updateSales())
.catch(error => console.error('Error resetting sales:', error));
}
}
function togglePassword() {
const passwordInput = document.getElementById('password');
const toggleIcon = document.querySelector('.toggle-password');
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
toggleIcon.textContent = '👁️';
} else {
passwordInput.type = 'password';
toggleIcon.textContent = '👁️';
}
}
function loadWifiSettings() {
fetch('/wifi-settings')
.then(response => response.json())
.then(data => {
document.getElementById('ssid').value = data.ssid;
document.getElementById('password').value = data.password;
})
.catch(error => console.error('Error fetching Wi-Fi settings:', error));
}
function saveWifi() {
const ssid = document.getElementById('ssid').value.trim();
const password = document.getElementById('password').value.trim();
if (!ssid) { alert('SSID cannot be empty.'); return; }
if (password.length < 8) { alert('Password must be at least 8 characters long.'); return; }
if (confirm('Save Wi-Fi settings? You will need to reconnect to the new network.')) {
fetch('/wifi', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ssid: ssid, password: password })
})
.then(response => {
if (response.ok) {
alert('Wi-Fi settings saved successfully. Please reconnect to the new network.');
setTimeout(() => window.location.reload(), 2000);
return;
}
return response.text().then(text => { throw new Error(text); });
})
.catch(error => {
if (error.message.includes('Failed to fetch')) {
alert('Wi-Fi settings saved successfully. Please reconnect to the new network.');
setTimeout(() => window.location.reload(), 2000);
} else {
console.error('Error saving Wi-Fi settings:', error);
alert('Error saving Wi-Fi settings: ' + error.message);
}
});
}
}
function loadTimerSettings() {
fetch('/timer-settings')
.then(response => response.json())
.then(data => {
document.getElementById('waterTime').value = data.water;
document.getElementById('soapTime').value = data.soap;
document.getElementById('blowerTime').value = data.blower;
})
.catch(error => console.error('Error fetching timer settings:', error));
}
function saveTimer() {
const waterTime = parseInt(document.getElementById('waterTime').value);
const soapTime = parseInt(document.getElementById('soapTime').value);
const blowerTime = parseInt(document.getElementById('blowerTime').value);
if (isNaN(waterTime) || waterTime < 5 || waterTime > 999) {
alert('Water time must be between 5 and 999 seconds.');
return;
}
if (isNaN(soapTime) || soapTime < 5 || soapTime > 999) {
alert('Soap time must be between 5 and 999 seconds.');
return;
}
if (isNaN(blowerTime) || blowerTime < 5 || blowerTime > 999) {
alert('Blower time must be between 5 and 999 seconds.');
return;
}
if (confirm('Save wash timer settings?')) {
fetch('/timer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ water: waterTime, soap: soapTime, blower: blowerTime })
})
.then(response => {
if (response.ok) {
alert('Wash timer settings saved successfully.');
} else {
return response.text().then(text => { throw new Error(text); });
}
})
.catch(error => {
console.error('Error saving timer settings:', error);
alert('Error saving wash timer settings: ' + error.message);
});
}
}
function loadPricingSettings() {
fetch('/pricing-settings')
.then(response => response.json())
.then(data => {
document.getElementById('waterPrice').value = data.water;
document.getElementById('soapPrice').value = data.soap;
document.getElementById('blowerPrice').value = data.blower;
})
.catch(error => console.error('Error fetching pricing settings:', error));
}
function savePricing() {
const waterPrice = parseInt(document.getElementById('waterPrice').value);
const soapPrice = parseInt(document.getElementById('soapPrice').value);
const blowerPrice = parseInt(document.getElementById('blowerPrice').value);
if (isNaN(waterPrice) || waterPrice < 1 || waterPrice > 20) {
alert('Water price must be between 1 and 20 credits.');
return;
}
if (isNaN(soapPrice) || soapPrice < 1 || soapPrice > 20) {
alert('Soap price must be between 1 and 20 credits.');
return;
}
if (isNaN(blowerPrice) || blowerPrice < 1 || blowerPrice > 20) {
alert('Blower price must be between 1 and 20 credits.');
return;
}
if (confirm('Save service pricing settings?')) {
fetch('/pricing', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ water: waterPrice, soap: soapPrice, blower: blowerPrice })
})
.then(response => {
if (response.ok) {
alert('Service pricing settings saved successfully.');
} else {
return response.text().then(text => { throw new Error(text); });
}
})
.catch(error => {
console.error('Error saving pricing settings:', error);
alert('Error saving pricing settings: ' + error.message);
});
}
}
function rebootESP() {
if (confirm('Are you sure you want to reboot the ESP32?')) {
alert('Rebooting... Please wait.');
fetch('/reboot', { method: 'POST' })
.then(() => {
setTimeout(() => window.location.reload(), 5000);
})
.catch(error => {
console.error('Error initiating reboot:', error);
alert('Reboot initiated. Please wait and reconnect if necessary.');
setTimeout(() => window.location.reload(), 5000);
});
}
}
setInterval(updateSales, 2000);
window.onload = () => {
updateSales();
loadWifiSettings();
loadTimerSettings();
loadPricingSettings();
};
</script>
</body>
</html>
)rawliteral";
void setup() {
Serial.begin(115200); // Initialize Serial for debugging
pinMode(coinSlotPin, INPUT_PULLUP);
pinMode(waterButton, INPUT_PULLUP);
pinMode(soapButton, INPUT_PULLUP);
pinMode(blowerButton, INPUT_PULLUP);
pinMode(settingsButton, INPUT_PULLUP);
pinMode(relayWater, OUTPUT);
pinMode(relaySoap, OUTPUT);
pinMode(relayBlower, OUTPUT);
pinMode(waterLED, OUTPUT);
pinMode(soapLED, OUTPUT);
pinMode(blowerLED, OUTPUT);
pinMode(buzzerPin, OUTPUT);
digitalWrite(relayWater, LOW);
digitalWrite(relaySoap, LOW);
digitalWrite(relayBlower, LOW);
digitalWrite(waterLED, HIGH);
digitalWrite(soapLED, HIGH);
digitalWrite(blowerLED, HIGH);
digitalWrite(buzzerPin, LOW);
// Modified: Use tone() for buzzer test at startup
tone(buzzerPin, 1000); // 1000 Hz tone
delay(500);
noTone(buzzerPin);
Serial.println("Buzzer test in setup");
lcd.init();
lcd.backlight();
prefs.begin("carwash", false);
loadSettings();
loadSalesData();
String savedSsid = prefs.getString("wifi_ssid", "");
String savedPassword = prefs.getString("wifi_password", "");
if (savedSsid != "" && savedPassword != "") {
ssid = savedSsid;
password = savedPassword;
}
WiFi.softAP(ssid.c_str(), password.c_str());
IPAddress IP = WiFi.softAPIP();
server.on("/", HTTP_GET, []() {
server.send(200, "text/html", htmlPage);
});
server.on("/sales", HTTP_GET, []() {
String json = "{\"water\":" + String(waterSales) +
",\"soap\":" + String(soapSales) +
",\"blower\":" + String(blowerSales) +
",\"total\":" + String(waterSales + soapSales + blowerSales) + "}";
server.send(200, "application/json", json);
});
server.on("/reset", HTTP_POST, []() {
resetSalesData();
server.send(200, "text/plain", "Sales reset");
});
server.on("/wifi-settings", HTTP_GET, []() {
String json = "{\"ssid\":\"" + ssid + "\",\"password\":\"" + password + "\"}";
server.send(200, "application/json", json);
});
server.on("/wifi", HTTP_POST, []() {
if (server.hasArg("plain")) {
String body = server.arg("plain");
DynamicJsonDocument doc(512);
DeserializationError error = deserializeJson(doc, body);
if (!error) {
String newSsid = doc["ssid"].as<String>();
String newPassword = doc["password"].as<String>();
if (newSsid != "" && newPassword.length() >= 8) {
prefs.putString("wifi_ssid", newSsid);
prefs.putString("wifi_password", newPassword);
ssid = newSsid;
password = newPassword;
server.send(200, "text/plain", "Wi-Fi settings updated");
WiFi.softAPdisconnect(true);
delay(100);
WiFi.softAP(newSsid.c_str(), newPassword.c_str());
return;
} else {
server.send(400, "text/plain", "Invalid SSID or password (minimum 8 characters)");
return;
}
} else {
server.send(400, "text/plain", "Invalid JSON format");
return;
}
}
server.send(400, "text/plain", "No data received");
});
server.on("/timer-settings", HTTP_GET, []() {
String json = "{\"water\":" + String(DEFAULT_WATER_TIME) +
",\"soap\":" + String(DEFAULT_SOAP_TIME) +
",\"blower\":" + String(DEFAULT_BLOWER_TIME) + "}";
server.send(200, "application/json", json);
});
server.on("/timer", HTTP_POST, []() {
if (server.hasArg("plain")) {
String body = server.arg("plain");
DynamicJsonDocument doc(512);
DeserializationError error = deserializeJson(doc, body);
if (!error) {
int waterTime = doc["water"];
int soapTime = doc["soap"];
int blowerTime = doc["blower"];
if (waterTime >= 5 && waterTime <= 999 &&
soapTime >= 5 && soapTime <= 999 &&
blowerTime >= 5 && blowerTime <= 999) {
DEFAULT_WATER_TIME = waterTime;
DEFAULT_SOAP_TIME = soapTime;
DEFAULT_BLOWER_TIME = blowerTime;
prefs.putInt("waterTime", DEFAULT_WATER_TIME);
prefs.putInt("soapTime", DEFAULT_SOAP_TIME);
prefs.putInt("blowerTime", DEFAULT_BLOWER_TIME);
server.send(200, "text/plain", "Timer settings updated");
return;
} else {
server.send(400, "text/plain", "Timer values must be between 5 and 999");
return;
}
} else {
server.send(400, "text/plain", "Invalid JSON format");
return;
}
}
server.send(400, "text/plain", "No data received");
});
server.on("/pricing-settings", HTTP_GET, []() {
String json = "{\"water\":" + String(WATER_CREDIT_COST) +
",\"soap\":" + String(SOAP_CREDIT_COST) +
",\"blower\":" + String(BLOWER_CREDIT_COST) + "}";
server.send(200, "application/json", json);
});
server.on("/pricing", HTTP_POST, []() {
if (server.hasArg("plain")) {
String body = server.arg("plain");
DynamicJsonDocument doc(512);
DeserializationError error = deserializeJson(doc, body);
if (!error) {
int waterPrice = doc["water"];
int soapPrice = doc["soap"];
int blowerPrice = doc["blower"];
if (waterPrice >= 1 && waterPrice <= 20 &&
soapPrice >= 1 && soapPrice <= 20 &&
blowerPrice >= 1 && blowerPrice <= 20) {
WATER_CREDIT_COST = waterPrice;
SOAP_CREDIT_COST = soapPrice;
BLOWER_CREDIT_COST = blowerPrice;
prefs.putInt("waterCredit", WATER_CREDIT_COST);
prefs.putInt("soapCredit", SOAP_CREDIT_COST);
prefs.putInt("blowerCredit", BLOWER_CREDIT_COST);
server.send(200, "text/plain", "Pricing settings updated");
return;
} else {
server.send(400, "text/plain", "Pricing values must be between 1 and 20");
return;
}
} else {
server.send(400, "text/plain", "Invalid JSON format");
return;
}
}
server.send(400, "text/plain", "No data received");
});
server.on("/reboot", HTTP_POST, []() {
server.send(200, "text/plain", "Rebooting");
delay(100);
ESP.restart();
});
server.begin();
attachInterrupt(digitalPinToInterrupt(coinSlotPin), Add, FALLING);
}
void loop() {
unsigned long currentMillis = millis();
server.handleClient();
if (!loadingComplete) {
displayLoadingScreen(currentMillis);
return;
}
handleSalesMonitoringTrigger(currentMillis);
if (salesMonitoringMode) {
static unsigned long soapPressStartTime = 0;
static bool soapLongPressHandled = false;
static bool soapWasPressed = false;
bool currentSoapState = digitalRead(soapButton) == LOW;
if (currentSoapState && !soapWasPressed) {
soapPressStartTime = currentMillis;
soapLongPressHandled = false;
soapWasPressed = true;
} else if (currentSoapState && soapWasPressed) {
if (!soapLongPressHandled && (currentMillis - soapPressStartTime >= SOAP_LONG_PRESS_TIME)) {
soapLongPressHandled = true;
resetSalesData();
displaySalesMonitoring();
}
} else if (!currentSoapState) {
soapWasPressed = false;
soapLongPressHandled = false;
}
if (currentMillis - salesDisplayStartTime >= SALES_DISPLAY_DURATION) {
salesMonitoringMode = false;
lcd.clear();
if (balance == 0 && waterTime == 0 && soapTime == 0 && blowerTime == 0) {
displayInitialScreen();
} else {
displayActiveScreen();
}
}
return;
}
handleSettingsButton();
if (settingsMode == 0) {
handleNormalMode(currentMillis);
} else {
handleSettingsMode(currentMillis);
}
}
// Rest of the functions (unchanged until beep())
void displayLoadingScreen(unsigned long currentMillis) {
const int barLength = 16;
const char block = 0xFF;
const int delayPerStep = 50;
static bool reached100Percent = false;
if (loadingPercent == 0) {
lcd.clear();
lcd.setCursor(4, 0);
lcd.print("Initializing!");
lcd.setCursor(1, 2);
lcd.print("[");
lcd.setCursor(18, 2);
lcd.print("]");
}
if (currentMillis - lastLoadingUpdate >= delayPerStep) {
lastLoadingUpdate = currentMillis;
int blocksToShow = map(loadingPercent, 0, 100, 0, barLength);
lcd.setCursor(2, 2);
for (int i = 0; i < barLength; i++) {
lcd.print(i < blocksToShow ? block : ' ');
}
lcd.setCursor(8, 3);
if (loadingPercent <= 100) {
lcd.print(loadingPercent);
} else {
lcd.print("100");
}
lcd.print("%");
if (loadingPercent < 100) lcd.print(" ");
if (loadingPercent < 100) {
loadingPercent++;
} else {
reached100Percent = true;
}
if (reached100Percent) {
static unsigned long pauseStart = 0;
if (pauseStart == 0) pauseStart = currentMillis;
if (currentMillis - pauseStart >= LCD_CLEAR_DELAY) {
lcd.clear();
loadingComplete = true;
loadingPercent = 0;
reached100Percent = false;
displayInitialScreen();
digitalWrite(waterLED, LOW);
digitalWrite(soapLED, LOW);
digitalWrite(buzzerPin, LOW);
}
}
}
}
void handleSettingsButton() {
static unsigned long pressStartTime = 0;
static bool longPressHandled = false;
const unsigned long LONG_PRESS_TIME = 900;
const unsigned long COOLDOWN_PERIOD = 500;
static bool buttonWasPressed = false;
static unsigned long lastExitTime = 0;
if (millis() - lastExitTime < COOLDOWN_PERIOD) return;
bool currentButtonState = digitalRead(settingsButton) == LOW;
if (currentButtonState && !buttonWasPressed && (millis() - lastSettingsButtonTime > BUTTON_DEBOUNCE_TIME)) {
buttonWasPressed = true;
pressStartTime = millis();
lastSettingsButtonTime = millis();
longPressHandled = false;
if (settingsMode == 0) {
settingsMode = 1;
lcd.clear();
updateSettingsDisplay();
digitalWrite(relayWater, LOW);
digitalWrite(relaySoap, LOW);
digitalWrite(relayBlower, LOW);
beep();
Serial.println("Beep on entering settings mode");
} else {
settingsMode = (settingsMode % 3) + 1;
updateSettingsDisplay();
}
} else if (!currentButtonState && buttonWasPressed) {
buttonWasPressed = false;
if (millis() - pressStartTime >= LONG_PRESS_TIME && !longPressHandled) {
longPressHandled = true;
if (settingsMode != 0) {
settingsMode = 0;
saveSettings();
lcd.clear();
displayActiveScreen();
digitalWrite(relayWater, waterTime > 0 ? HIGH : LOW);
digitalWrite(relaySoap, soapTime > 0 ? HIGH : LOW);
digitalWrite(relayBlower, blowerTime > 0 ? HIGH : LOW);
beep();
Serial.println("Beep on exiting settings mode");
lastExitTime = millis();
}
}
}
}
void loadSettings() {
DEFAULT_WATER_TIME = prefs.getInt("waterTime", 60);
DEFAULT_SOAP_TIME = prefs.getInt("soapTime", 45);
DEFAULT_BLOWER_TIME = prefs.getInt("blowerTime", 75);
WATER_CREDIT_COST = prefs.getInt("waterCredit", 5);
SOAP_CREDIT_COST = prefs.getInt("soapCredit", 5);
BLOWER_CREDIT_COST = prefs.getInt("blowerCredit", 5);
if (DEFAULT_WATER_TIME == -1) DEFAULT_WATER_TIME = 60;
if (DEFAULT_SOAP_TIME == -1) DEFAULT_SOAP_TIME = 45;
if (DEFAULT_BLOWER_TIME == -1) DEFAULT_BLOWER_TIME = 75;
if (WATER_CREDIT_COST == -1) WATER_CREDIT_COST = 5;
if (SOAP_CREDIT_COST == -1) SOAP_CREDIT_COST = 5;
if (BLOWER_CREDIT_COST == -1) BLOWER_CREDIT_COST = 5;
DEFAULT_WATER_TIME = constrain(DEFAULT_WATER_TIME, 5, 999);
DEFAULT_SOAP_TIME = constrain(DEFAULT_SOAP_TIME, 5, 999);
DEFAULT_BLOWER_TIME = constrain(DEFAULT_BLOWER_TIME, 5, 999);
WATER_CREDIT_COST = constrain(WATER_CREDIT_COST, 1, 20);
SOAP_CREDIT_COST = constrain(SOAP_CREDIT_COST, 1, 20);
BLOWER_CREDIT_COST = constrain(BLOWER_CREDIT_COST, 1, 20);
}
void saveSettings() {
prefs.putInt("waterTime", DEFAULT_WATER_TIME);
prefs.putInt("soapTime", DEFAULT_SOAP_TIME);
prefs.putInt("blowerTime", DEFAULT_BLOWER_TIME);
prefs.putInt("waterCredit", WATER_CREDIT_COST);
prefs.putInt("soapCredit", SOAP_CREDIT_COST);
prefs.putInt("blowerCredit", BLOWER_CREDIT_COST);
}
void loadSalesData() {
waterSales = prefs.getInt("waterSales", 0);
soapSales = prefs.getInt("soapSales", 0);
blowerSales = prefs.getInt("blowerSales", 0);
}
void saveSalesData() {
prefs.putInt("waterSales", waterSales);
prefs.putInt("soapSales", soapSales);
prefs.putInt("blowerSales", blowerSales);
}
void resetSalesData() {
waterSales = 0;
soapSales = 0;
blowerSales = 0;
saveSalesData();
}
void displaySalesMonitoring() {
lcd.clear();
lcd.setCursor(4, 0);
lcd.print("WATER = ");
lcd.print(waterSales);
lcd.setCursor(4, 1);
lcd.print("SOAP = ");
lcd.print(soapSales);
lcd.setCursor(4, 2);
lcd.print("BLOWER = ");
lcd.print(blowerSales);
lcd.setCursor(4, 3);
lcd.print("TOTAL = ");
lcd.print(waterSales + soapSales + blowerSales);
}
void handleSalesMonitoringTrigger(unsigned long currentMillis) {
static unsigned long blowerPressStartTime = 0;
static bool blowerLongPressHandled = false;
static bool blowerWasPressed = false;
bool currentBlowerState = digitalRead(blowerButton) == LOW;
if (settingsMode != 0) return;
if (currentBlowerState && !blowerWasPressed) {
blowerPressStartTime = currentMillis;
blowerLongPressHandled = false;
blowerWasPressed = true;
} else if (currentBlowerState && blowerWasPressed) {
if (!blowerLongPressHandled && (currentMillis - blowerPressStartTime >= BLOWER_LONG_PRESS_TIME)) {
blowerLongPressHandled = true;
salesMonitoringMode = true;
salesDisplayStartTime = currentMillis;
displaySalesMonitoring();
}
} else if (!currentBlowerState) {
blowerWasPressed = false;
blowerLongPressHandled = false;
}
}
void handleSettingsButtons() {
if (settingsMode == 0) return;
if (digitalRead(waterButton) == LOW && (millis() - lastWaterButtonTime > BUTTON_DEBOUNCE_TIME)) {
lastWaterButtonTime = millis();
settingsField = (settingsField == 1) ? 2 : 1;
updateSettingsDisplay();
}
if (digitalRead(soapButton) == LOW && (millis() - lastSoapButtonTime > BUTTON_DEBOUNCE_TIME)) {
lastSoapButtonTime = millis();
adjustSettingsValue(true);
updateSettingsDisplay();
}
if (digitalRead(blowerButton) == LOW && (millis() - lastBlowerButtonTime > BUTTON_DEBOUNCE_TIME)) {
lastBlowerButtonTime = millis();
adjustSettingsValue(false);
updateSettingsDisplay();
}
}
void adjustSettingsValue(bool increment) {
if (settingsField == 1) {
switch(settingsMode) {
case 1:
WATER_CREDIT_COST = constrain(WATER_CREDIT_COST + (increment ? 1 : -1), 1, 20);
break;
case 2:
SOAP_CREDIT_COST = constrain(SOAP_CREDIT_COST + (increment ? 1 : -1), 1, 20);
break;
case 3:
BLOWER_CREDIT_COST = constrain(BLOWER_CREDIT_COST + (increment ? 1 : -1), 1, 20);
break;
}
} else {
switch(settingsMode) {
case 1:
DEFAULT_WATER_TIME = constrain(DEFAULT_WATER_TIME + (increment ? 1 : -1), 5, 999);
break;
case 2:
DEFAULT_SOAP_TIME = constrain(DEFAULT_SOAP_TIME + (increment ? 1 : -1), 5, 999);
break;
case 3:
DEFAULT_BLOWER_TIME = constrain(DEFAULT_BLOWER_TIME + (increment ? 1 : -1), 5, 999);
break;
}
}
}
void handleNormalMode(unsigned long currentMillis) {
if (balance == 0 && waterTime == 0 && soapTime == 0 && blowerTime == 0) {
if (!screenInitialized) {
displayInitialScreen();
screenInitialized = true;
}
if (currentMillis - lastInsertCoinBlinkTime > BLINK_INTERVAL) {
lastInsertCoinBlinkTime = currentMillis;
showInsertCoin = !showInsertCoin;
blinkInsertCoin();
}
} else {
screenInitialized = false;
}
if (coinsInserted > 0) {
balance += coinsInserted * COIN_VALUE;
coinsInserted = 0;
updateBalanceDisplay();
}
if (currentMillis - lastUpdateTime >= 1000) {
lastUpdateTime = currentMillis;
updateTime();
}
if (balance >= 5 && (balance >= getCurrentCreditCost() || waterTime > 0 || soapTime > 0 || blowerTime > 0)) {
if (currentMillis - lastButtonBlinkTime > BLINK_INTERVAL) {
lastButtonBlinkTime = currentMillis;
showPressButton = !showPressButton;
blinkPressButton();
}
} else {
showPressButton = true;
blinkPressButton();
}
handleButton(waterButton, relayWater, waterTime, waterActive, waterPaused, waterPauseCount, DEFAULT_WATER_TIME, waterButtonPressed, lastWaterButtonTime, WATER_CREDIT_COST, "water");
handleButton(soapButton, relaySoap, soapTime, soapActive, soapPaused, soapPauseCount, DEFAULT_SOAP_TIME, soapButtonPressed, lastSoapButtonTime, SOAP_CREDIT_COST, "soap");
handleButton(blowerButton, relayBlower, blowerTime, blowerActive, blowerPaused, blowerPauseCount, DEFAULT_BLOWER_TIME, blowerButtonPressed, lastBlowerButtonTime, BLOWER_CREDIT_COST, "blower");
handlePausedBlinking();
updateLEDs(currentMillis);
if (balance == 0) {
if (currentMillis - lastInsertCoinBlinkTime > BLINK_INTERVAL) {
lastInsertCoinBlinkTime = currentMillis;
showInsertCoin = !showInsertCoin;
blinkInsertCoin();
}
}
}
int getCurrentCreditCost() {
if (waterActive) return WATER_CREDIT_COST;
if (soapActive) return SOAP_CREDIT_COST;
if (blowerActive) return BLOWER_CREDIT_COST;
return WATER_CREDIT_COST;
}
void handleSettingsMode(unsigned long currentMillis) {
if (currentMillis - lastSettingsBlinkTime > BLINK_INTERVAL) {
lastSettingsBlinkTime = currentMillis;
settingsBlinkState = !settingsBlinkState;
updateSettingsDisplay();
}
handleSettingsButtons();
}
void Add() {
noInterrupts();
coinsInserted++;
interrupts();
}
void updateTime() {
bool timeNeedsUpdate = false;
if (waterActive && !waterPaused && waterTime > 0) {
waterTime--;
timeNeedsUpdate = true;
if (waterTime <= 10 && waterTime >= 9 && !waterBeeped10) {
beep();
waterBeeped10 = true;
Serial.println("Water beep at 10s");
} else if (waterTime <= 6 && waterTime >= 5 && !waterBeeped6) { // Corrected condition
beep();
waterBeeped6 = true;
Serial.println("Water beep at 6s");
} else if (waterTime <= 2 && waterTime >= 1 && !waterBeeped2) {
beep();
waterBeeped2 = true;
Serial.println("Water beep at 2s");
}
} else if (waterTime == 0) {
waterBeeped10 = false;
waterBeeped6 = false;
waterBeeped2 = false;
}
if (soapActive && !soapPaused && soapTime > 0) {
soapTime--;
timeNeedsUpdate = true;
if (soapTime <= 10 && soapTime >= 9 && !soapBeeped10) {
beep();
soapBeeped10 = true;
Serial.println("Soap beep at 10s");
} else if (soapTime <= 6 && soapTime >= 5 && !soapBeeped6) {
beep();
soapBeeped6 = true;
Serial.println("Soap beep at 6s");
} else if (soapTime <= 2 && soapTime >= 1 && !soapBeeped2) {
beep();
soapBeeped2 = true;
Serial.println("Soap beep at 2s");
}
} else if (soapTime == 0) {
soapBeeped10 = false;
soapBeeped6 = false;
soapBeeped2 = false;
}
if (blowerActive && !blowerPaused && blowerTime > 0) {
blowerTime--;
timeNeedsUpdate = true;
if (blowerTime <= 10 && blowerTime >= 9 && !blowerBeeped10) {
beep();
blowerBeeped10 = true;
Serial.println("Blower beep at 10s");
} else if (blowerTime <= 6 && blowerTime >= 5 && !blowerBeeped6) {
beep();
blowerBeeped6 = true;
Serial.println("Blower beep at 6s");
} else if (blowerTime <= 2 && blowerTime >= 1 && !blowerBeeped2) {
beep();
blowerBeeped2 = true;
Serial.println("Blower beep at 2s");
}
} else if (blowerTime == 0) {
blowerBeeped10 = false;
blowerBeeped6 = false;
blowerBeeped2 = false;
}
if (waterTime == 0 && waterActive) {
waterActive = false;
digitalWrite(relayWater, LOW);
}
if (soapTime == 0 && soapActive) {
soapActive = false;
digitalWrite(relaySoap, LOW);
}
if (blowerTime == 0 && blowerActive) {
blowerActive = false;
digitalWrite(relayBlower, LOW);
}
if (balance == 0 && waterTime == 0 && soapTime == 0 && blowerTime == 0) {
displayInitialScreen();
} else if (timeNeedsUpdate) {
updateTimeDisplay();
}
}
void handleButton(int button, int relay, int &time, bool &active, bool &paused, int &pauseCount, int defaultTime, bool &buttonPressed, unsigned long &lastButtonTime, int creditCost, String service) {
if (digitalRead(button) == LOW) {
if (!buttonPressed && (millis() - lastButtonTime > BUTTON_DEBOUNCE_TIME)) {
buttonPressed = true;
lastButtonTime = millis();
if (!active && balance >= creditCost) {
balance -= creditCost;
time += defaultTime;
active = true;
paused = false;
pauseCount = 0;
digitalWrite(relay, HIGH);
updateBalanceDisplay();
beep();
Serial.println("Beep on " + service + " button press");
if (service == "water") {
waterSales += creditCost;
} else if (service == "soap") {
soapSales += creditCost;
} else if (service == "blower") {
blowerSales += creditCost;
}
saveSalesData();
} else if (active) {
if (!paused && pauseCount < MAX_PAUSE_COUNT) {
paused = true;
pauseCount++;
digitalWrite(relay, LOW);
} else if (paused) {
paused = false;
digitalWrite(relay, HIGH);
}
}
updateTimeDisplay();
}
} else {
buttonPressed = false;
}
}
void displayInitialScreen() {
lcd.setCursor(3, 0);
lcd.print(" 3in1 CARWASH");
lcd.setCursor(0, 2);
lcd.print("WATER SOAP BLOWER");
lcd.setCursor(0, 3);
lcd.print("00:00 00:00 00:00");
lcd.setCursor(0, 1);
lcd.print(" --Insert Coin!--");
}
void blinkInsertCoin() {
lcd.setCursor(0, 1);
if (showInsertCoin) {
lcd.print(" --Insert Coin!-- ");
} else {
lcd.print(" ");
}
}
void displayActiveScreen() {
lcd.setCursor(3, 0);
if (balance >= 5) {
lcd.print("PRESS A BUTTON");
} else if (balance > 0) {
lcd.print("PRESS A BUTTON");
} else {
lcd.print(" 3in1 CARWASH ");
}
lcd.setCursor(2, 1);
if (balance > 0) {
lcd.print(" Credit: ");
lcd.print(balance);
} else {
lcd.print("--Insert Coin!--");
}
lcd.setCursor(0, 2);
lcd.print("WATER SOAP BLOWER");
updateTimeDisplay();
}
void updateBalanceDisplay() {
lcd.setCursor(2, 1);
lcd.print(" Credit: ");
lcd.print(balance);
lcd.print(" ");
}
void blinkPressButton() {
lcd.setCursor(3, 0);
if (balance >= 5 && showPressButton) {
lcd.print("PRESS A BUTTON");
} else if (balance >= 5 && !showPressButton) {
lcd.print(" ");
} else if (balance > 0) {
lcd.print("PRESS A BUTTON");
} else {
lcd.print(" 3in1 CARWASH ");
}
}
void updateTimeDisplay() {
lcd.setCursor(0, 3);
displayTime(waterTime, waterPaused, 0, waterBlinkState, waterLastBlinkTime);
lcd.setCursor(7, 3);
displayTime(soapTime, soapPaused, 7, soapBlinkState, soapLastBlinkTime);
lcd.setCursor(14, 3);
displayTime(blowerTime, blowerPaused, 14, blowerBlinkState, blowerLastBlinkTime);
}
void displayTime(int seconds, bool paused, int column, bool &blinkState, unsigned long &lastBlinkTime) {
if (paused) {
if (millis() - lastBlinkTime > BLINK_INTERVAL) {
lastBlinkTime = millis();
blinkState = !blinkState;
}
lcd.setCursor(column, 3);
if (blinkState) {
lcd.print(" ");
} else {
int mins = seconds / 60;
int secs = seconds % 60;
lcd.print(mins / 10);
lcd.print(mins % 10);
lcd.print(":");
lcd.print(secs / 10);
lcd.print(secs % 10);
}
} else {
lcd.setCursor(column, 3);
int mins = seconds / 60;
int secs = seconds % 60;
lcd.print(mins / 10);
lcd.print(mins % 10);
lcd.print(":");
lcd.print(secs / 10);
lcd.print(secs % 10);
}
}
void handlePausedBlinking() {
unsigned long currentMillis = millis();
if (waterPaused) {
if (currentMillis - waterLastBlinkTime > BLINK_INTERVAL) {
waterLastBlinkTime = currentMillis;
waterBlinkState = !waterBlinkState;
lcd.setCursor(0, 3);
if (waterBlinkState) {
lcd.print(" ");
} else {
int mins = waterTime / 60;
int secs = waterTime % 60;
lcd.print(mins / 10);
lcd.print(mins % 10);
lcd.print(":");
lcd.print(secs / 10);
lcd.print(secs % 10);
}
}
}
if (soapPaused) {
if (currentMillis - soapLastBlinkTime > BLINK_INTERVAL) {
soapLastBlinkTime = currentMillis;
soapBlinkState = !soapBlinkState;
lcd.setCursor(7, 3);
if (soapBlinkState) {
lcd.print(" ");
} else {
int mins = soapTime / 60;
int secs = soapTime % 60;
lcd.print(mins / 10);
lcd.print(mins % 10);
lcd.print(":");
lcd.print(secs / 10);
lcd.print(secs % 10);
}
}
}
if (blowerPaused) {
if (currentMillis - blowerLastBlinkTime > BLINK_INTERVAL) {
blowerLastBlinkTime = currentMillis;
blowerBlinkState = !blowerBlinkState;
lcd.setCursor(14, 3);
if (blowerBlinkState) {
lcd.print(" ");
} else {
int mins = blowerTime / 60;
int secs = blowerTime % 60;
lcd.print(mins / 10);
lcd.print(mins % 10);
lcd.print(":");
lcd.print(secs / 10);
lcd.print(secs % 10);
}
}
}
}
void updateLEDs(unsigned long currentMillis) {
bool waterLEDState = LOW;
bool soapLEDState = LOW;
bool blowerLEDState = LOW;
if (waterPaused) {
waterLEDState = waterBlinkState ? HIGH : LOW;
}
if (soapPaused) {
soapLEDState = soapBlinkState ? HIGH : LOW;
}
if (blowerPaused) {
blowerLEDState = blowerBlinkState ? HIGH : LOW;
}
if (!waterPaused) {
waterLEDState = waterActive ? LOW : (balance >= WATER_CREDIT_COST ? HIGH : LOW);
}
if (!soapPaused) {
soapLEDState = soapActive ? LOW : (balance >= SOAP_CREDIT_COST ? HIGH : LOW);
}
if (!blowerPaused) {
blowerLEDState = blowerActive ? LOW : (balance >= BLOWER_CREDIT_COST ? HIGH : LOW);
}
digitalWrite(waterLED, waterLEDState);
digitalWrite(soapLED, soapLEDState);
digitalWrite(blowerLED, blowerLEDState);
}
void updateSettingsDisplay() {
lcd.setCursor(0, 0);
lcd.print(" SETTINGS ");
lcd.setCursor(0, 1);
lcd.print("MODE: ");
switch(settingsMode) {
case 1: lcd.print("WATER "); break;
case 2: lcd.print("SOAP "); break;
case 3: lcd.print("BLOWER"); break;
}
lcd.setCursor(0, 2);
lcd.print("Credit: ");
if (settingsField == 1 && settingsBlinkState) {
lcd.print(" ");
} else {
switch(settingsMode) {
case 1: lcd.print(WATER_CREDIT_COST); break;
case 2: lcd.print(SOAP_CREDIT_COST); break;
case 3: lcd.print(BLOWER_CREDIT_COST); break;
}
lcd.print(" ");
}
lcd.setCursor(0, 3);
lcd.print("Time(sec): ");
if (settingsField == 2 && settingsBlinkState) {
lcd.print(" ");
} else {
switch(settingsMode) {
case 1: lcd.print(DEFAULT_WATER_TIME); break;
case 2: lcd.print(DEFAULT_SOAP_TIME); break;
case 3: lcd.print(DEFAULT_BLOWER_TIME); break;
}
lcd.print(" ");
}
}
// Modified: Use tone() with cooldown to ensure reliable sound in Wokwi
void beep() {
unsigned long currentMillis = millis();
if (currentMillis - lastBeepTime >= BUZZER_COOLDOWN) {
tone(buzzerPin, 1000); // 1000 Hz tone
delay(BUZZER_BEEP_DURATION);
noTone(buzzerPin);
lastBeepTime = currentMillis;
Serial.println("Buzzer activated");
}
}