Panduan Lengkap Tandon Air Otomatis (ESP32 Master & Display) + Kode Lengkap
Artikel ini memandu Anda membangun sistem tandon air otomatis menggunakan dua ESP32 (Master & Display), lengkap dengan diagram wiring (SVG) dan kode sumber penuh untuk diunggah langsung ke Arduino IDE.
Daftar Isi
1. Perangkat yang Dibutuhkan
- ESP32 Dev Board (2 unit): Master & Display
- Sensor Ultrasonik HC-SR04
- Relay 2-Channel (pompa & kran/solenoid valve)
- LCD I2C (Master 16x2, Display 20x4)
- Push button (kontrol manual)
- LED indikator: merah (kran ON), hijau (tandon penuh)
- Power supply 5V/2A
2. Library
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <LiquidCrystal_I2C.h>
#include <HTTPClient.h> // (untuk unit Display)
3. Fungsi Pin
ESP32 Master
Pin | Fungsi | Keterangan |
---|---|---|
5 | TRIG_PIN | Trigger sensor ultrasonik HC-SR04 |
18 | ECHO_PIN | Echo sensor ultrasonik |
25 | RELAY_SOLENOID | Relay kran/valve |
26 | RELAY_POMPA | Relay pompa air |
ESP32 Display
Pin | Fungsi | Keterangan |
---|---|---|
4 | LED_RED | LED indikator kran (ON saat mengisi) |
16 | LED_GREEN | LED indikator tandon penuh |
33 | PIN_CMD_POMPA | Tombol toggle pompa |
32 | PIN_CMD_KRAN | Tombol toggle kran |
4. Diagram Wiring
Klik kanan → “Open image in new tab” untuk melihat/zoom versi besar (SVG).
5. Cara Kerja Sistem
- Master: Mengukur level air via HC-SR04, hitung % dan volume, kontrol relay pompa/kran (mode AUTO/MANUAL), serta menyediakan endpoint HTTP untuk data/status.
- Display: Mengambil data dari Master (HTTP GET) tiap interval, menampilkan % & liter, status pompa/kran & mode, menerima input tombol dan mengirim perintah (HTTP) ke Master.
6. Tampilan LCD
Master (16×2)
Level Air: 75% Volume Air: 200L
Display (20×4)
Level: 75% 200L Pompa: ON Kran: OFF Mode Pompa: AUTO Mode Kran : AUTO
7. Kode Lengkap
master.ino (ESP32 Master)
// ESP32 Tandon (Master) - v4 (Clean Web UI, ASCII-only, labels "Level Air"/"Volume Air")
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <LiquidCrystal_I2C.h>
#define AP_SSID "TandonAir_Master"
#define AP_PASS "12345678"
Preferences prefs;
WebServer server(80);
LiquidCrystal_I2C lcd(0x27, 16, 2);
// WiFi vars
String wifiSSID, wifiPASS, wifiIP, wifiGW, displayIP;
IPAddress localIP, gateway, subnet(255,255,255,0);
// Pins
#define TRIG_PIN 5
#define ECHO_PIN 18
#define RELAY_SOLENOID 25 // KRAN
#define RELAY_POMPA 26 // POMPA
// Tank params
float jarakPenuh = 30; // cm
float jarakKosong = 100; // cm
float tinggiTandon = 100; // cm
float diameterTandon = 60;// cm
float distanceCm, levelPersen, volumeLiter;
// Modes
bool pompaForcedOff = false; // OFF manual vs AUTO
bool kranForcedOff = false; // OFF manual vs AUTO
// Push timer
unsigned long lastSendTime = 0;
unsigned long sendInterval = 5000;
// Blink anim for bar
bool blinkState = false;
unsigned long lastBlink = 0;
const unsigned long blinkInterval = 400; // ms
// LCD alternate line (to show both labels on 16x2)
bool showLevelOnLCD = true;
unsigned long lastAlt = 0;
const unsigned long altInterval = 2000; // 2s
inline void setPompa(bool on) { digitalWrite(RELAY_POMPA, on ? LOW : HIGH); }
inline void setKran(bool on) { digitalWrite(RELAY_SOLENOID, on ? LOW : HIGH); }
inline bool isPompaOn() { return digitalRead(RELAY_POMPA) == LOW; }
inline bool isKranOn() { return digitalRead(RELAY_SOLENOID) == LOW; }
void setupWiFi() {
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(AP_SSID, AP_PASS);
Serial.print("AP IP: "); Serial.println(WiFi.softAPIP());
wifiSSID = prefs.getString("ssid", "");
wifiPASS = prefs.getString("pass", "");
wifiIP = prefs.getString("ip", "192.168.1.200");
wifiGW = prefs.getString("gw", "192.168.1.1");
displayIP = prefs.getString("display", "192.168.1.201");
if (wifiSSID != "") {
localIP.fromString(wifiIP);
gateway.fromString(wifiGW);
WiFi.config(localIP, gateway, subnet);
WiFi.begin(wifiSSID.c_str(), wifiPASS.c_str());
Serial.println("Connecting to WiFi...");
unsigned long start = millis();
while (WiFi.status()!=WL_CONNECTED && millis()-start<10000) { delay(500); Serial.print("."); }
Serial.println(WiFi.status()==WL_CONNECTED ? "\nConnected!" : "\nFailed, AP only");
}
}
long bacaJarak() {
long sum = 0; int n = 5;
for (int i=0;i<n;i++){
digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long d = pulseIn(ECHO_PIN, HIGH, 30000);
if (d == 0) d = 30000;
sum += d;
delay(20);
}
long duration = sum / n;
return duration * 0.034 / 2;
}
void updateSensor() {
distanceCm = bacaJarak();
levelPersen = (jarakKosong - distanceCm) / (jarakKosong - jarakPenuh) * 100.0;
if (levelPersen < 0) levelPersen = 0;
if (levelPersen > 100) levelPersen = 100;
float tinggiAir = tinggiTandon * (levelPersen / 100.0);
float radius = diameterTandon / 2.0;
volumeLiter = 3.1416 * radius * radius * tinggiAir / 1000.0;
}
void kontrolRelay() {
if (pompaForcedOff) setPompa(false);
else { if (levelPersen < 20) setPompa(false); else setPompa(true); }
if (kranForcedOff) setKran(false);
else { if (levelPersen < 90) setKran(true); else if (levelPersen >= 100) setKran(false); }
}
void tampilLCD() {
int totalCols = 16;
int bars = map((int)levelPersen, 0, 100, 0, totalCols);
bool doBlink = (isKranOn() && levelPersen < 100.0);
if (doBlink && (millis() - lastBlink >= blinkInterval)) { lastBlink = millis(); blinkState = !blinkState; }
// Row 0: bar
lcd.setCursor(0,0);
for (int i=0; i<totalCols; i++) {
char ch = ' ';
if (bars == 0) { if (doBlink && i == 0) ch = blinkState ? (char)255 : ' '; }
else {
if (i < bars - 1) ch = (char)255;
else if (i == bars - 1) ch = (doBlink ? (blinkState ? ' ' : (char)255) : (char)255);
else ch = ' ';
}
lcd.print(ch);
}
// Row 1: alternate text to fit labels
if (millis() - lastAlt >= altInterval) { lastAlt = millis(); showLevelOnLCD = !showLevelOnLCD; }
lcd.setCursor(0,1);
if (showLevelOnLCD) {
// "Level Air: 100%" fits (<=16)
lcd.print("Level Air: ");
int val = (int)levelPersen;
if (val<10) lcd.print(" "); // keep width stable (optional)
lcd.print(val); lcd.print("% ");
} else {
// "Volume Air: 999L" may be up to 16
lcd.print("Volume Air:");
int v = (int)volumeLiter;
// ensure a space before value if needed
lcd.print(" "); lcd.print(v); lcd.print("L ");
}
}
void kirimKeDisplay() {
if (WiFi.status() != WL_CONNECTED) return;
if (displayIP == "") return;
WiFiClient client;
if (client.connect(displayIP.c_str(), 80)) {
String data = "level=" + String(levelPersen, 1) +
"&volume=" + String(volumeLiter, 1) +
"&pompa=" + String(isPompaOn() ? "ON" : "OFF") +
"&kran=" + String(isKranOn() ? "ON" : "OFF") +
"&mode_pompa=" + String(pompaForcedOff ? "OFF" : "AUTO") +
"&mode_kran=" + String(kranForcedOff ? "OFF" : "AUTO");
client.println("POST /update HTTP/1.1");
client.println("Host: " + displayIP);
client.println("Content-Type: application/x-www-form-urlencoded");
client.print("Content-Length: "); client.println(data.length());
client.println();
client.print(data);
client.stop();
} else {
Serial.println("Gagal kirim ke display");
}
}
// ---------- Web UI ----------
String modeStr(bool forcedOff){ return forcedOff ? "OFF" : "AUTO"; }
void handleRoot() {
String html;
html += "<!DOCTYPE html><html><head><meta charset='utf-8'>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<style>body{font-family:system-ui,Arial,sans-serif;margin:20px}";
html += "button{padding:8px 12px;margin:4px 6px;cursor:pointer}";
html += ".card{border:1px solid #ddd;border-radius:8px;padding:12px;margin-bottom:12px}";
html += ".grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px}";
html += ".badge{display:inline-block;padding:2px 8px;border-radius:999px;background:#eee;margin-left:6px}";
html += "a{color:#0645AD;text-decoration:none;margin-right:10px}a:hover{text-decoration:underline}";
html += "</style></head><body>";
html += "<h2>Tandon Air - Master</h2>";
html += "<div class='grid'>";
// Info
html += "<div class='card'><h3>Status</h3>";
html += "Level Air: "+String(levelPersen,1)+"%";
if(levelPersen>=100.0) html += " <span class='badge'>FULL</span>";
html += "<br>Volume Air: "+String(volumeLiter,1)+" L";
html += "<br>Pompa: "+String(isPompaOn()?"ON":"OFF")+" ("+modeStr(pompaForcedOff)+")";
html += "<br>Kran : "+String(isKranOn()?"ON":"OFF") +" ("+modeStr(kranForcedOff )+")";
html += "<br>IP: "+WiFi.localIP().toString();
html += "</div>";
// Controls
html += "<div class='card'><h3>Kontrol Manual</h3>";
html += "<form method='POST' action='/command' style='display:inline-block'>"
"<input type='hidden' name='action' value='pompa-"+String(pompaForcedOff? "auto":"off")+"'>"
"<button type='submit'>Pompa: "+String(pompaForcedOff? "AUTO":"OFF")+"</button></form>";
html += "<form method='POST' action='/command' style='display:inline-block'>"
"<input type='hidden' name='action' value='kran-"+String(kranForcedOff? "auto":"off")+"'>"
"<button type='submit'>Kran: "+String(kranForcedOff? "AUTO":"OFF")+"</button></form>";
html += "<p><small>Mode OFF = paksa relay OFF (HIGH). Mode AUTO = kembali ke logika level air.</small></p>";
html += "</div>";
// Links
html += "<div class='card'><h3>Pengaturan</h3>";
html += "<a href='/wifi'>Konfigurasi WiFi</a> | <a href='/tandon'>Kalibrasi Tandon</a>";
html += "</div>";
html += "</div></body></html>";
server.send(200,"text/html; charset=utf-8",html);
}
void handleWiFiConfig() {
if (server.method()==HTTP_POST) {
prefs.putString("ssid", server.arg("ssid"));
prefs.putString("pass", server.arg("pass"));
prefs.putString("ip", server.arg("ip"));
prefs.putString("gw", server.arg("gw"));
prefs.putString("display", server.arg("display"));
server.send(200,"text/html; charset=utf-8","<h3>Disimpan! Restart ESP...</h3>");
delay(2000); ESP.restart(); return;
}
String html;
html += "<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "</head><body><h2>Konfigurasi WiFi Master</h2><form method='POST'>";
html += "SSID: <input name='ssid' value='"+wifiSSID+"'><br>";
html += "Password: <input name='pass' value='"+wifiPASS+"'><br>";
html += "IP Statis: <input name='ip' value='"+wifiIP+"'><br>";
html += "Gateway: <input name='gw' value='"+wifiGW+"'><br>";
html += "IP Display: <input name='display' value='"+displayIP+"'><br>";
html += "<input type='submit' value='Simpan & Restart'></form>";
html += "<br><a href='/'>Kembali</a></body></html>";
server.send(200,"text/html; charset=utf-8",html);
}
void handleTandonConfig() {
if (server.method()==HTTP_POST) {
jarakPenuh = server.arg("penuh").toFloat();
jarakKosong = server.arg("kosong").toFloat();
tinggiTandon = server.arg("tinggi").toFloat();
diameterTandon = server.arg("diameter").toFloat();
prefs.putFloat("penuh", jarakPenuh);
prefs.putFloat("kosong", jarakKosong);
prefs.putFloat("tinggi", tinggiTandon);
prefs.putFloat("diameter", diameterTandon);
server.send(200,"text/html; charset=utf-8","<h3>Disimpan!</h3><a href='/'>Kembali</a>");
return;
}
String html;
html += "<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'></head><body>";
html += "<h2>Set Parameter Tandon</h2><form method='POST'>";
html += "Jarak Penuh (cm): <input name='penuh' value='"+String(jarakPenuh)+"'><br>";
html += "Jarak Kosong (cm): <input name='kosong' value='"+String(jarakKosong)+"'><br>";
html += "Tinggi Tandon (cm): <input name='tinggi' value='"+String(tinggiTandon)+"'><br>";
html += "Diameter Tandon (cm): <input name='diameter' value='"+String(diameterTandon)+"'><br>";
html += "<input type='submit' value='Simpan'></form>";
html += "<br><a href='/'>Kembali</a></body></html>";
server.send(200,"text/html; charset=utf-8",html);
}
void handleData(){
String data = String(levelPersen,1)+","+String(volumeLiter,1)+","+
String(isPompaOn()? "ON":"OFF")+","+String(isKranOn()? "ON":"OFF")+","+
String(pompaForcedOff? "OFF":"AUTO")+","+String(kranForcedOff? "OFF":"AUTO");
server.send(200,"text/plain; charset=utf-8",data);
}
// JSON endpoint stays with same keys for compatibility
void handleJsonStatus(){
// Build JSON with proper escaping
String json = "{";
json += "\"level\":" + String(levelPersen, 1) + ",";
json += "\"volume\":" + String(volumeLiter, 1) + ",";
json += "\"pompa\":\"" + String(isPompaOn() ? "ON" : "OFF") + "\",";
json += "\"kran\":\"" + String(isKranOn() ? "ON" : "OFF") + "\",";
json += "\"mode_pompa\":\"" + String(pompaForcedOff ? "OFF" : "AUTO") + "\",";
json += "\"mode_kran\":\"" + String(kranForcedOff ? "OFF" : "AUTO") + "\",";
json += "\"full\":" + String(levelPersen>=100.0 ? "true" : "false");
json += "}";
server.send(200, "application/json; charset=utf-8", json);
}
// POST /command action=pompa-off|pompa-auto|kran-off|kran-auto
void handleCommand() {
if (server.method() != HTTP_POST) { server.send(405,"text/plain; charset=utf-8","Method Not Allowed"); return; }
String action = server.arg("action");
if (action == "pompa-off") { pompaForcedOff = true; server.send(200,"text/plain; charset=utf-8","OK"); }
else if (action == "pompa-auto") { pompaForcedOff = false; server.send(200,"text/plain; charset=utf-8","OK"); }
else if (action == "kran-off") { kranForcedOff = true; server.send(200,"text/plain; charset=utf-8","OK"); }
else if (action == "kran-auto") { kranForcedOff = false; server.send(200,"text/plain; charset=utf-8","OK"); }
else { server.send(400, "text/plain; charset=utf-8", "Unknown action"); }
}
void setup() {
Serial.begin(115200);
lcd.init(); lcd.backlight();
pinMode(TRIG_PIN, OUTPUT); pinMode(ECHO_PIN, INPUT);
pinMode(RELAY_SOLENOID, OUTPUT); pinMode(RELAY_POMPA, OUTPUT);
digitalWrite(RELAY_SOLENOID, HIGH); // default OFF
digitalWrite(RELAY_POMPA, HIGH); // default OFF
prefs.begin("config", false);
jarakPenuh = prefs.getFloat("penuh", jarakPenuh);
jarakKosong = prefs.getFloat("kosong", jarakKosong);
tinggiTandon = prefs.getFloat("tinggi", tinggiTandon);
diameterTandon = prefs.getFloat("diameter", diameterTandon);
setupWiFi();
server.on("/", handleRoot);
server.on("/wifi", handleWiFiConfig);
server.on("/tandon", handleTandonConfig);
server.on("/data", handleData);
server.on("/status.json", handleJsonStatus);
server.on("/command", HTTP_POST, handleCommand);
server.begin();
}
void loop() {
updateSensor();
kontrolRelay();
tampilLCD();
server.handleClient();
if (millis() - lastSendTime > sendInterval) { kirimKeDisplay(); lastSendTime = millis(); }
delay(120);
}
monitor.ino (ESP32 Display)
// ESP32 Monitor (Display) - v4_fix5 (periodic label refresh to prevent missing "LEV")
#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <LiquidCrystal_I2C.h>
#include <HTTPClient.h>
#define AP_SSID "Tandon_Display"
#define AP_PASS "12345678"
Preferences prefs;
WebServer server(80);
LiquidCrystal_I2C lcd(0x27, 20, 4);
// LED Pins
#define LED_RED 4 // blink when KRAN ON (filling)
#define LED_GREEN 16 // solid ON when FULL (>=100%)
// WiFi & IP Master
String ssid, pass, ipMaster, ipLocal, gwLocal;
IPAddress localIP, gateway, subnet(255,255,255,0);
// Data
String persenAir="--", literAir="--", statusPompa="--", statusKran="--";
String modePompa="AUTO", modeKran="AUTO";
// Row 4 toggle
int modeTeks = 0;
unsigned long lastToggle = 0;
const unsigned long toggleInterval = 5000;
// Buttons (toggle) with pull-up
#define PIN_CMD_POMPA 33
#define PIN_CMD_KRAN 32
int lastPompaPin = HIGH, lastKranPin = HIGH;
unsigned long lastDebouncePompa=0, lastDebounceKran=0;
const unsigned long debounceMs=80;
// Overlay 2s
bool showingOverlay=false;
unsigned long overlayUntil=0;
String overlayLine1="", overlayLine2="";
// Blink anim for bar
bool blinkState=false;
unsigned long lastBlink=0;
const unsigned long blinkInterval=400;
// --- Flicker-free state ---
String lastPersen = "";
String lastLiter = "";
int lastBarSeg = -1;
bool lastBlinkState = false;
bool lastKranOn = false;
int lastModeTeksShown = -1;
// --- Periodic label refresh ---
unsigned long lastLabelRefresh = 0;
const unsigned long labelRefreshMs = 1500; // refresh every 1.5s (small, no flicker)
void setupWiFi(){
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(AP_SSID, AP_PASS);
Serial.print("AP Display IP: "); Serial.println(WiFi.softAPIP());
ssid=prefs.getString("ssid",""); pass=prefs.getString("pass","");
ipMaster=prefs.getString("master","192.168.1.200");
ipLocal =prefs.getString("ip","192.168.1.201");
gwLocal =prefs.getString("gw","192.168.1.1");
if(ssid!=""){
localIP.fromString(ipLocal); gateway.fromString(gwLocal);
WiFi.config(localIP, gateway, subnet);
WiFi.begin(ssid.c_str(), pass.c_str());
Serial.println("Connecting...");
unsigned long start=millis();
while(WiFi.status()!=WL_CONNECTED && millis()-start<10000){ delay(500); Serial.print("."); }
Serial.println(WiFi.status()==WL_CONNECTED? "\nConnected!" : "\nFailed.");
}
}
void handleRoot(){
String html="<h2>Display Tandon</h2>";
html+="Level Air: "+persenAir+"%<br>Volume Air: "+literAir+" L<br>";
html+="Pompa: "+statusPompa+" ("+modePompa+") | Kran: "+statusKran+" ("+modeKran+")<br><br>";
html+="<a href='/wifi'>Konfigurasi WiFi</a>";
server.send(200,"text/html",html);
}
void handleWiFiConfig(){
if(server.method()==HTTP_POST){
prefs.putString("ssid", server.arg("ssid"));
prefs.putString("pass", server.arg("pass"));
prefs.putString("master", server.arg("master"));
prefs.putString("ip", server.arg("ip"));
prefs.putString("gw", server.arg("gw"));
server.send(200,"text/html","<h3>Disimpan! Restart...</h3>");
delay(2000); ESP.restart(); return;
}
String html="<h2>Konfigurasi WiFi Display</h2><form method='POST'>";
html+="SSID: <input name='ssid' value='"+ssid+"'><br>";
html+="Password: <input name='pass' value='"+pass+"'><br>";
html+="IP Master: <input name='master' value='"+ipMaster+"'><br>";
html+="IP Lokal: <input name='ip' value='"+ipLocal+"'><br>";
html+="Gateway: <input name='gw' value='"+gwLocal+"'><br>";
html+="<input type='submit' value='Simpan & Restart'></form><br><a href='/'>Kembali</a>";
server.send(200,"text/html",html);
}
bool kirimPerintah(const String& action){
if(WiFi.status()!=WL_CONNECTED) return false;
if(ipMaster=="") return false;
HTTPClient http;
String url="http://"+ipMaster+"/command";
http.begin(url);
http.addHeader("Content-Type","application/x-www-form-urlencoded");
int code=http.POST("action="+action);
http.end();
return code==200;
}
void showOverlay(const String& l1, const String& l2=""){
showingOverlay=true; overlayUntil=millis()+2000;
overlayLine1=l1; overlayLine2=l2;
lcd.clear();
int c1=max(0,(20-(int)l1.length())/2); lcd.setCursor(c1,1); lcd.print(l1);
int c2=max(0,(20-(int)l2.length())/2); if(l2.length()>0){ lcd.setCursor(c2,2); lcd.print(l2); }
}
void refreshLabelsIfNeeded(){
if (millis() - lastLabelRefresh >= labelRefreshMs){
lcd.setCursor(0,1); lcd.print("Level Air : ");
lcd.setCursor(0,2); lcd.print("Volume Air: ");
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
digitalWrite(LED_RED, LOW);
digitalWrite(LED_GREEN, LOW);
lastLabelRefresh = millis();
}
}
void drawBarIfNeeded(){
int barSeg=0;
if(persenAir!="--"){ int p=constrain(persenAir.toInt(),0,100); barSeg=map(p,0,100,0,18); }
bool kranOn = (statusKran=="ON");
bool doBlink=(kranOn && persenAir!="--" && persenAir.toInt()<100);
if(doBlink && (millis()-lastBlink>=blinkInterval)){ lastBlink=millis(); blinkState=!blinkState; }
if (barSeg != lastBarSeg || (doBlink && blinkState != lastBlinkState) || (kranOn != lastKranOn)) {
lcd.setCursor(0,0);
lcd.print("[");
for (int i=0;i<18;i++){
char ch=' ';
if(barSeg==0){ if(doBlink && i==0) ch = blinkState ? (char)255 : ' '; }
else{
if(i<barSeg-1) ch=(char)255;
else if(i==barSeg-1) ch = (doBlink ? (blinkState ? ' ' : (char)255) : (char)255);
else ch=' ';
}
lcd.print(ch);
}
lcd.print("]");
lastBarSeg = barSeg;
lastBlinkState = blinkState;
lastKranOn = kranOn;
}
}
void tampilNormal(){
refreshLabelsIfNeeded();
drawBarIfNeeded();
if (persenAir != lastPersen) {
lcd.setCursor(12, 1);
lcd.print(persenAir); lcd.print("% ");
lastPersen = persenAir;
}
if (literAir != lastLiter) {
lcd.setCursor(12, 2);
lcd.print(literAir); lcd.print(" L ");
lastLiter = literAir;
}
if (millis() - lastToggle >= toggleInterval) {
lastToggle = millis();
modeTeks = (modeTeks + 1) % 2;
}
if (modeTeks != lastModeTeksShown) {
lcd.setCursor(0, 3);
if (modeTeks == 0) {
lcd.print("Pompa: "); lcd.print(statusPompa); lcd.print(" ("); lcd.print(modePompa); lcd.print(") ");
} else {
lcd.print("Kran : "); lcd.print(statusKran ); lcd.print(" ("); lcd.print(modeKran ); lcd.print(") ");
}
lastModeTeksShown = modeTeks;
}
}
void updateLEDs(){
// Parse level percent
int p = -1;
if (persenAir != "--") { p = constrain(persenAir.toInt(), 0, 100); }
bool kranOn = (statusKran == "ON");
bool filling = (kranOn && p >= 0 && p < 100);
bool full = (p >= 100);
// Blink red when filling
static unsigned long lastBlinkLed = 0;
static bool redState = false;
if (filling){
if (millis() - lastBlinkLed >= 400){
lastBlinkLed = millis();
redState = !redState;
digitalWrite(LED_RED, redState ? HIGH : LOW);
}
} else {
digitalWrite(LED_RED, LOW);
}
// Green solid when full
digitalWrite(LED_GREEN, full ? HIGH : LOW);
}
void tampilLCD(){
if(showingOverlay){
if(millis()>overlayUntil){
showingOverlay=false;
lcd.clear();
lcd.setCursor(0,1); lcd.print("Level Air : ");
lcd.setCursor(0,2); lcd.print("Volume Air: ");
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
digitalWrite(LED_RED, LOW);
digitalWrite(LED_GREEN, LOW);
lastPersen=""; lastLiter=""; lastBarSeg=-1; lastModeTeksShown=-1;
lastLabelRefresh = 0; // force refresh soon
}
} else {
tampilNormal();
}
}
void handleUpdate(){ // POST from Master
if(server.method()!=HTTP_POST){ server.send(405,"text/plain","Method Not Allowed"); return; }
String lvl=server.arg("level"), vol=server.arg("volume"), pom=server.arg("pompa"), krn=server.arg("kran");
String mp=server.arg("mode_pompa"), mk=server.arg("mode_kran");
if(lvl.length()>0) persenAir=lvl;
if(vol.length()>0) literAir=vol;
if(pom.length()>0) statusPompa=pom;
if(krn.length()>0) statusKran=krn;
if(mp.length()>0) modePompa=mp;
if(mk.length()>0) modeKran=mk;
server.send(200,"text/plain","OK");
}
void setup(){
Serial.begin(115200);
lcd.init(); lcd.backlight();
lcd.setCursor(0,1); lcd.print("Level Air : ");
lcd.setCursor(0,2); lcd.print("Volume Air: ");
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
digitalWrite(LED_RED, LOW);
digitalWrite(LED_GREEN, LOW);
pinMode(PIN_CMD_POMPA, INPUT_PULLUP);
pinMode(PIN_CMD_KRAN, INPUT_PULLUP);
prefs.begin("disp", false);
setupWiFi();
server.on("/", handleRoot);
server.on("/wifi", handleWiFiConfig);
server.on("/update", HTTP_POST, handleUpdate);
server.begin();
}
void loop(){
int readPompa=digitalRead(PIN_CMD_POMPA);
int readKran =digitalRead(PIN_CMD_KRAN);
unsigned long now=millis();
if(readPompa!=lastPompaPin) lastDebouncePompa=now;
if((now-lastDebouncePompa)>debounceMs){
static int stablePompa=HIGH;
if(readPompa!=stablePompa){
stablePompa=readPompa;
if(stablePompa==LOW){
bool toOff=(modePompa=="AUTO");
if(kirimPerintah(toOff? "pompa-off":"pompa-auto")){
modePompa=toOff? "OFF":"AUTO";
showOverlay(String("POMPA ")+ (toOff?"OFF":"AUTO"));
} else showOverlay("CMD POMPA GAGAL");
}
}
}
lastPompaPin=readPompa;
if(readKran!=lastKranPin) lastDebounceKran=now;
if((now-lastDebounceKran)>debounceMs){
static int stableKran=HIGH;
if(readKran!=stableKran){
stableKran=readKran;
if(stableKran==LOW){
bool toOff=(modeKran=="AUTO");
if(kirimPerintah(toOff? "kran-off":"kran-auto")){
modeKran=toOff? "OFF":"AUTO";
showOverlay(String("KRAN ")+ (toOff?"OFF":"AUTO"));
} else showOverlay("CMD KRAN GAGAL");
}
}
}
lastKranPin=readKran;
tampilLCD();
updateLEDs();
server.handleClient();
delay(60);
}
8. Catatan & Tips
- Pastikan alamat IP Master dikonfigurasi sesuai jaringan Anda pada kode Display.
- Gunakan catu daya yang memadai untuk relay dan pompa/solenoid (isolasi opto jika tersedia).
- Kalibrasi parameter tinggi tandon & jarak sensor pada kode Master agar persentase/volume akurat.
No comments:
Post a Comment