#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <math.h>
// ================= BLYNK =================
#define BLYNK_TEMPLATE_ID "TMPL6IhYBaZXU"
#define BLYNK_TEMPLATE_NAME "Rain Gauge System"
#define BLYNK_AUTH_TOKEN "szM7WI9a5xLZicVnrrfPavKIX9zYBaWc"
// ✅ NO BLYNK DEBUG PRINT (no banner)
// (jangan letak #define BLYNK_PRINT Serial)
#include <WiFi.h>
#include <BlynkSimpleEsp32.h>
char ssid[] = "Wokwi-GUEST";
char pass[] = "";
BlynkTimer timer;
// ================= LCD =================
LiquidCrystal_I2C lcd(0x27, 16, 2);
// ================= PINS =================
#define POT_PIN 34
#define RELAY_PIN 23
// ================= TANK & FUNNEL =================
const float tankCapacity_g = 500.0f; // FULL tank (g)
const float funnelArea_mm2 = 100.0f * 100.0f; // 100 cm^2 -> 10,000 mm^2
// ================= FLOOD SIMULATION =================
const float baseDistance_m = 2.50f;
const float minDistance_m = 0.20f;
const float floodDistance_m = 1.00f;
const float kRise = 0.0040f; // model sensitivity
const float recoverSpeed_mps = 0.03f;
// ================= DRAIN =================
#define RELAY_ON HIGH
#define RELAY_OFF LOW
const unsigned long DRAIN_RUN_MS = 1500;
const unsigned long STOP_DELAY_MS = 800;
// ================= UI =================
const unsigned long UI_INTERVAL_MS = 200;
// ================= SMOOTHING =================
const float ALPHA = 0.20f; // 0.15..0.30
const unsigned long SEND_MS = 2000; // 2s = graf lebih smooth
// ================= SCENARIO =================
enum Scenario { NORMAL, HEAVY, EXTREME };
Scenario readScenarioFromPotZone() {
int adc = analogRead(POT_PIN);
if (adc < 1200) return NORMAL;
if (adc < 2800) return HEAVY;
return EXTREME;
}
char scenarioChar(Scenario sc) {
if (sc == NORMAL) return 'N';
if (sc == HEAVY) return 'H';
return 'E';
}
const char* scenarioName(Scenario sc) {
if (sc == NORMAL) return "NORMAL";
if (sc == HEAVY) return "HEAVY";
return "EXTREME";
}
// ================= RAIN MODEL =================
float riseRateFor(Scenario sc) { // g/s
if (sc == NORMAL) return 30.0f;
if (sc == HEAVY) return 60.0f;
return 120.0f;
}
float targetFor(Scenario sc) {
if (sc == NORMAL) return 150.0f;
if (sc == HEAVY) return 300.0f;
return tankCapacity_g;
}
// ================= STATE =================
Scenario scenario = NORMAL;
float tankWeight_g = 0.0f;
float rainfall_mm = 0.0f;
float dist_m = baseDistance_m;
bool flood = false;
bool rainYes = true;
unsigned long rainStopMs = 0;
bool draining = false;
unsigned long drainStartMs = 0;
unsigned long lastMs = 0;
unsigned long lastUiMs = 0;
// ================= BLYNK CONTROL =================
// V5 = mode (0=AUTO, 1=MANUAL)
// V4 = relay switch (0/1)
// V6 = StatusText (String)
// V7 = ScenarioText (String)
// V8 = TimeToFlood_s (Double) (optional)
// V9 = Flood LED Widget (0..255)
bool manualMode = false;
int manualRelay = 0;
// ================= FILTERED VALUES =================
float w_f = 0.0f;
float r_f = 0.0f;
float d_f = baseDistance_m;
float tFlood_s = 0.0f;
// ================= HELPERS =================
String pad16(String s){ while(s.length() < 16) s += " "; return s.substring(0,16); }
void lcdPrint2(String a, String b){
lcd.setCursor(0,0); lcd.print(pad16(a));
lcd.setCursor(0,1); lcd.print(pad16(b));
}
void setRelay(int on){
digitalWrite(RELAY_PIN, on ? RELAY_ON : RELAY_OFF);
}
void startDrain(){
if(draining) return;
draining = true;
drainStartMs = millis();
setRelay(1);
}
void stopDrain(){
setRelay(0);
draining = false;
tankWeight_g = 0.0f;
rainfall_mm = 0.0f;
rainYes = true;
rainStopMs = 0;
}
void onScenarioChange(Scenario sc){
scenario = sc;
tankWeight_g = 0.0f;
rainfall_mm = 0.0f;
dist_m = baseDistance_m;
flood = false;
w_f = 0.0f;
r_f = 0.0f;
d_f = baseDistance_m;
rainYes = true;
rainStopMs = 0;
}
// ================= BLYNK CALLBACKS =================
BLYNK_WRITE(V5){
manualMode = (param.asInt() == 1);
if(!manualMode && !draining){
setRelay(0);
}
}
BLYNK_WRITE(V4){
manualRelay = param.asInt();
if(manualMode){
setRelay(manualRelay);
draining = (manualRelay == 1);
if(draining) drainStartMs = millis();
}
}
// ================= SEND TO BLYNK (NO SERIAL) =================
void tickSendBlynk(){
float sendW = w_f;
float sendR = r_f;
float sendD = d_f;
bool sendF = flood;
bool sendDrain = draining;
bool sendRain = rainYes;
Scenario sendSc = scenario;
if(sendRain && sendD > floodDistance_m){
float speed_mps = kRise * riseRateFor(sendSc);
tFlood_s = (sendD - floodDistance_m) / max(speed_mps, 0.0001f);
} else {
tFlood_s = 0.0f;
}
Blynk.virtualWrite(V0, sendD);
Blynk.virtualWrite(V1, sendW);
Blynk.virtualWrite(V2, sendR);
Blynk.virtualWrite(V3, sendF ? 1 : 0);
Blynk.virtualWrite(V4, sendDrain ? 1 : 0);
Blynk.virtualWrite(V5, manualMode ? 1 : 0);
// ✅ Flood indicator widget
Blynk.virtualWrite(V9, sendF ? 255 : 0);
Blynk.virtualWrite(V7, scenarioName(sendSc));
Blynk.virtualWrite(V8, tFlood_s);
String status =
String("W:") + String(sendW,1) + "g | " +
"R:" + String(sendR,1) + "mm | " +
"d:" + String(sendD,2) + "m | " +
"Flood:" + (sendF ? "YES" : "NO") + " | " +
"Drain:" + (sendDrain ? "ON" : "OFF") + " | " +
"S:" + String(scenarioChar(sendSc)) + " | " +
"Rain:" + (sendRain ? "YES" : "NO");
Blynk.virtualWrite(V6, status);
}
// ================= LCD =================
void printLCD(){
// line 1
String l1 = String("W") + String(w_f,1) + " R" + String(r_f,1) + " " + String(scenarioChar(scenario));
// ✅ line 2 (ini yang selalu orang rosakkan bila copy)
// FY/FN untuk flood indicator, D1/D0 untuk drain
String l2 = String(flood ? "FY " : "FN ") +
String("d") + String(d_f,2) +
String(" D") + String(draining ? "1" : "0");
lcdPrint2(l1, l2);
}
// ================= SETUP =================
void setup(){
pinMode(POT_PIN, INPUT);
analogReadResolution(12);
pinMode(RELAY_PIN, OUTPUT);
setRelay(0);
Wire.begin();
lcd.init();
lcd.backlight();
Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
timer.setInterval(SEND_MS, tickSendBlynk);
lastMs = millis();
lastUiMs = millis();
scenario = readScenarioFromPotZone();
onScenarioChange(scenario);
lcdPrint2("READY", "LED Flood=V9");
}
// ================= LOOP =================
void loop(){
Blynk.run();
timer.run();
unsigned long now = millis();
float dt = (now - lastMs) / 1000.0f;
if(dt <= 0) dt = 0.001f;
lastMs = now;
// Scenario change (AUTO only)
Scenario sc = readScenarioFromPotZone();
if(!manualMode){
if(sc != scenario && !draining){
onScenarioChange(sc);
}
}
// Rain process (AUTO only)
float target = targetFor(scenario);
if(!manualMode){
if(!draining && rainYes){
tankWeight_g += riseRateFor(scenario) * dt;
if(tankWeight_g > tankCapacity_g) tankWeight_g = tankCapacity_g;
if(scenario != EXTREME && tankWeight_g >= target){
rainYes = false;
rainStopMs = now;
}
}
}
// ✅ Rainfall formula BETUL
rainfall_mm = (tankWeight_g * 1000.0f) / funnelArea_mm2;
// Flood distance change (AUTO only)
if(!manualMode){
if(rainYes) dist_m -= (kRise * riseRateFor(scenario)) * dt;
else dist_m += recoverSpeed_mps * dt;
if(dist_m < minDistance_m) dist_m = minDistance_m;
if(dist_m > baseDistance_m) dist_m = baseDistance_m;
flood = (dist_m < floodDistance_m);
} else {
flood = (dist_m < floodDistance_m);
}
// Drain logic (AUTO only)
if(!manualMode){
if(!draining && tankWeight_g >= tankCapacity_g){
startDrain(); // FULL
}
if(!draining && !rainYes && tankWeight_g > 0.0f){
if(rainStopMs != 0 && (now - rainStopMs >= STOP_DELAY_MS)){
startDrain(); // RAIN STOP
}
}
if(draining && (now - drainStartMs >= DRAIN_RUN_MS)){
stopDrain();
}
}
// EMA smoothing
w_f = w_f + ALPHA * (tankWeight_g - w_f);
r_f = r_f + ALPHA * (rainfall_mm - r_f);
d_f = d_f + ALPHA * (dist_m - d_f);
// LCD update
if(now - lastUiMs >= UI_INTERVAL_MS){
lastUiMs = now;
printLCD();
}
delay(10);
}