/**
* CS3243 — Automated Chemical Dosing System
* Wokwi Simulation | ESP32-C6 DevKitC-1 | v1.5.0
*
* PIN MAP
* GPIO 3 — Potentiometer (simulates flow meter, 0–100%)
* GPIO 4 — HC-SR04 TRIG
* GPIO 5 — HC-SR04 ECHO
* GPIO 6 — BTN_FLOAT (blue, hold = tank level LOW → start refill)
* GPIO 7 — BTN_RESET (green, press = clear alarm)
* GPIO 10 — LED green (pump)
* GPIO 11 — LED blue (water valve)
* GPIO 12 — LED yellow (chemical valve)
* GPIO 13 — LED white (dispensing valve)
* GPIO 14 — LED purple (alarm)
* GPIO 15 — Buzzer
* GPIO 20 — LCD SDA
* GPIO 21 — LCD SCL
*
* HOW TO TEST
* Refill : Hold BTN_FLOAT → all 4 LEDs ON. Release → cycle done.
* Overfill D5 : Hold BTN_FLOAT + pot at max → alarm at 750 mL.
* Water low W1: Drag HC-SR04 slider to ~75 cm → Serial warning.
* Water empty W2: Drag slider to ~97 cm → alarm, refill blocked.
* Timeout D6 : Hold BTN_FLOAT + pot at 0 → alarm after 30 s.
* Clear alarm : Press BTN_RESET.
*
* LIBRARY REQUIRED
* LiquidCrystal I2C by Frank de Brabander (Wokwi Library Manager)
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// ── Pins ──────────────────────────────────────────────────────────────────
#define PIN_TRIG 4
#define PIN_ECHO 5
#define PIN_POT 3
#define PIN_BTN_FLOAT 6
#define PIN_BTN_RESET 7
#define PIN_LED_PUMP 10
#define PIN_LED_WATER 11
#define PIN_LED_CHEM 12
#define PIN_LED_DISP 13
#define PIN_LED_ALARM 14
#define PIN_BUZZER 15
#define PIN_SDA 20
#define PIN_SCL 21
// ── Constants ─────────────────────────────────────────────────────────────
// HC-SR04 mounted above tank facing down.
// dist ≈ 2 cm = full, dist ≈ 102 cm = empty.
// W1 at ~75 cm → (102-75)/102 ≈ 26% < 30% ✓
// W2 at ~97 cm → (102-97)/102 ≈ 5% < 5% ✓
#define TANK_H_CM 102.0f // tank height = empty-tank sensor distance
#define TANK_VOL_L 2000.0f
#define WARN_PCT 30.0f // W1: low-water warning threshold (%)
#define EMPTY_PCT 5.0f // W2: refill-blocked threshold (%)
#define MAX_ML 750.0f // D5: overfill hard limit (mL)
#define MAX_FLOW_ML_S 25.0f // nominal flow rate at 100% pot
#define TICK_MS 100UL // volume integration interval
#define SENSOR_MS 500UL // sensor poll interval
#define LCD_MS 500UL // LCD refresh interval
#define DEBOUNCE_MS 50UL // button debounce window
#define BEEP_MS 400UL // alarm blink half-period
#define TIMEOUT_MS 30000UL // D6: max fill duration
#define SONAR_FAIL_MAX 3 // consecutive bad reads before W3 fires
#define ALARM_HZ 1000U // buzzer tone (Hz)
// ── State ─────────────────────────────────────────────────────────────────
enum State : uint8_t { IDLE, FILLING, ALARMED };
State sysState = IDLE;
// ── Globals ───────────────────────────────────────────────────────────────
float waterPct = 100.0f;
float waterL = TANK_VOL_L;
float dispensedML = 0.0f;
float flowPct = 0.0f;
bool alarmBlink = false;
uint8_t sonarFails = 0;
char faultMsg[17] = "OK";
unsigned long tSensor=0, tLcd=0, tTick=0, tBeep=0, tFillStart=0, tPage=0;
// ── Button debounce ───────────────────────────────────────────────────────
struct Btn { uint8_t pin; bool stable, last; unsigned long changed; };
Btn btnFloat = { PIN_BTN_FLOAT, HIGH, HIGH, 0 };
Btn btnReset = { PIN_BTN_RESET, HIGH, HIGH, 0 };
// Returns true on confirmed falling edge (button pressed).
bool fell(Btn &b) {
bool raw = digitalRead(b.pin);
unsigned long now = millis();
if (raw != b.last) { b.changed = now; b.last = raw; }
if (now - b.changed >= DEBOUNCE_MS) {
bool prev = b.stable; b.stable = raw;
return prev == HIGH && b.stable == LOW;
}
return false;
}
// ── LCD ───────────────────────────────────────────────────────────────────
LiquidCrystal_I2C lcd(0x27, 16, 2);
uint8_t lcdPage = 0;
void lcdPrint(uint8_t row, const char *fmt, ...) {
char buf[17]; va_list ap; va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap);
for (int i = strlen(buf); i < 16; i++) buf[i] = ' ';
buf[16] = '\0';
lcd.setCursor(0, row); lcd.print(buf);
}
// ── Actuators ─────────────────────────────────────────────────────────────
void actuators(bool on) {
digitalWrite(PIN_LED_PUMP, on);
digitalWrite(PIN_LED_WATER, on);
digitalWrite(PIN_LED_CHEM, on);
digitalWrite(PIN_LED_DISP, on);
}
void alarm(const char *msg) {
actuators(false);
sysState = ALARMED;
snprintf(faultMsg, sizeof(faultMsg), "%-16s", msg);
Serial.print(F("[ALARM] ")); Serial.println(msg);
}
void clearAlarm() {
sysState = IDLE; dispensedML = 0; alarmBlink = false; sonarFails = 0;
snprintf(faultMsg, sizeof(faultMsg), "%-16s", "OK");
digitalWrite(PIN_LED_ALARM, LOW); noTone(PIN_BUZZER);
Serial.println(F("[INFO] Alarm cleared."));
}
// ── HC-SR04 ───────────────────────────────────────────────────────────────
float readSonar() {
digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long d = pulseIn(PIN_ECHO, HIGH, 30000UL);
return d ? (d / 2.0f) * 0.0343f : -1.0f;
}
// ── Setup ─────────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
analogReadResolution(12);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
pinMode(PIN_BTN_FLOAT, INPUT_PULLUP);
pinMode(PIN_BTN_RESET, INPUT_PULLUP);
pinMode(PIN_LED_PUMP, OUTPUT);
pinMode(PIN_LED_WATER, OUTPUT);
pinMode(PIN_LED_CHEM, OUTPUT);
pinMode(PIN_LED_DISP, OUTPUT);
pinMode(PIN_LED_ALARM, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
actuators(false);
digitalWrite(PIN_LED_ALARM, LOW);
noTone(PIN_BUZZER);
Wire.begin(PIN_SDA, PIN_SCL);
lcd.init(); lcd.backlight();
lcdPrint(0, "Dosing System");
lcdPrint(1, "v1.5.0 ready");
delay(1500); lcd.clear();
Serial.println(F("[INFO] Ready — hold BTN_FLOAT to refill."));
}
// ── Loop ──────────────────────────────────────────────────────────────────
void loop() {
unsigned long now = millis();
// 1. BUTTONS ──────────────────────────────────────────────────────────────
bool wasHeld = (btnFloat.stable == LOW);
if (fell(btnFloat)) {
if (sysState == IDLE && waterPct > EMPTY_PCT) {
sysState = FILLING; dispensedML = 0;
tFillStart = tTick = now;
actuators(true);
Serial.println(F("[INFO] Refill STARTED."));
} else if (waterPct <= EMPTY_PCT) {
Serial.println(F("[WARN] Blocked — water empty (W2)."));
}
}
// Float released mid-fill → tank full, cycle done
if (wasHeld && btnFloat.stable == HIGH && sysState == FILLING && dispensedML > 20) {
actuators(false); sysState = IDLE;
Serial.print(F("[INFO] Refill DONE — ")); Serial.print(dispensedML, 0); Serial.println(F(" mL"));
dispensedML = 0;
}
if (fell(btnReset) && sysState == ALARMED) clearAlarm();
// 2. SENSORS (every 500 ms) ───────────────────────────────────────────────
if (now - tSensor >= SENSOR_MS) {
tSensor = now;
// HC-SR04 — water supply tank
float dist = readSonar();
if (dist > 0) {
sonarFails = 0;
float rem = constrain(TANK_H_CM - dist, 0, TANK_H_CM);
waterPct = (rem / TANK_H_CM) * 100;
waterL = (waterPct / 100) * TANK_VOL_L;
} else {
// Only raise W3 after 3 consecutive bad reads (avoids false alarm at boot)
if (++sonarFails >= SONAR_FAIL_MAX && sysState != ALARMED)
alarm("W3:Sonar Fail");
}
// W1 — low water warning (prints once per threshold crossing)
static bool w1 = false;
if (!w1 && waterPct <= WARN_PCT) { w1 = true;
Serial.print(F("[WARN] W1: Water low (")); Serial.print(waterPct,0); Serial.println(F("%)"));
}
if (waterPct > WARN_PCT) w1 = false;
// W2 — water empty, block refills
if (waterPct <= EMPTY_PCT && sysState != ALARMED)
alarm("W2:Water Empty");
// Potentiometer → simulated flow meter
flowPct = (analogRead(PIN_POT) / 4095.0f) * 100;
}
// 3. VOLUME INTEGRATION (every 100 ms while filling) ─────────────────────
if (sysState == FILLING && now - tTick >= TICK_MS) {
float dt = (now - tTick) / 1000.0f; tTick = now;
dispensedML += MAX_FLOW_ML_S * (flowPct / 100.0f) * dt;
if (dispensedML >= MAX_ML) alarm("D5:Overfill 750mL");
if (now - tFillStart >= TIMEOUT_MS) alarm("D6:Fill Timeout");
}
// 4. ALARM LED + BUZZER (blink/beep every 400 ms) ─────────────────────────
if (now - tBeep >= BEEP_MS) {
tBeep = now;
if (sysState == ALARMED) {
alarmBlink = !alarmBlink;
digitalWrite(PIN_LED_ALARM, alarmBlink);
alarmBlink ? tone(PIN_BUZZER, ALARM_HZ) : noTone(PIN_BUZZER);
} else if (alarmBlink) {
alarmBlink = false;
digitalWrite(PIN_LED_ALARM, LOW);
noTone(PIN_BUZZER);
}
}
// 5. LCD (refresh 500 ms, page rotates every 3 s) ─────────────────────────
if (now - tLcd >= LCD_MS) {
tLcd = now;
if (now - tPage > 3000UL) { tPage = now; lcdPage = (lcdPage + 1) % 3; lcd.clear(); }
switch (lcdPage) {
case 0:
lcdPrint(0, "State: %s",
sysState == IDLE ? "IDLE " :
sysState == FILLING ? "FILLING" : "ALARM ");
lcdPrint(1, "%s", faultMsg);
break;
case 1:
lcdPrint(0, "Water Supply:");
lcdPrint(1, "%.0f%% %.0fL", waterPct, waterL);
break;
case 2:
lcdPrint(0, "Dispense/Flow:");
lcdPrint(1, sysState == FILLING ? "%.0f/750 mL" : "Flow: %.0f%%",
sysState == FILLING ? dispensedML : flowPct);
break;
}
}
}Loading
esp32-c6-devkitc-1
esp32-c6-devkitc-1