Search This Blog

Saturday, 9 August 2025

Tandon Air Otomatis (ESP32 Master & Display) + Kode Lengkap

 

Panduan Lengkap Tandon Air Otomatis (ESP32 Master & Display) + Kode Lengkap

Diperbarui: 09 August 2025

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.

⬇️ Unduh master.ino ⬇️ Unduh monitor.ino

1. Perangkat yang Dibutuhkan

  1. ESP32 Dev Board (2 unit): Master & Display
  2. Sensor Ultrasonik HC-SR04
  3. Relay 2-Channel (pompa & kran/solenoid valve)
  4. LCD I2C (Master 16x2, Display 20x4)
  5. Push button (kontrol manual)
  6. LED indikator: merah (kran ON), hijau (tandon penuh)
  7. 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

PinFungsiKeterangan
5TRIG_PINTrigger sensor ultrasonik HC-SR04
18ECHO_PINEcho sensor ultrasonik
25RELAY_SOLENOIDRelay kran/valve
26RELAY_POMPARelay pompa air

ESP32 Display

PinFungsiKeterangan
4LED_REDLED indikator kran (ON saat mengisi)
16LED_GREENLED indikator tandon penuh
33PIN_CMD_POMPATombol toggle pompa
32PIN_CMD_KRANTombol toggle kran

4. Diagram Wiring

Klik kanan → “Open image in new tab” untuk melihat/zoom versi besar (SVG).

Diagram Wiring ESP32 Master untuk Tandon Air Otomatis
Gambar 1. Diagram wiring ESP32 Master yang membaca sensor ultrasonik dan mengendalikan relay pompa & kran.
Diagram Wiring ESP32 Display/Monitor dengan LCD I2C dan tombol kontrol
Gambar 2. Diagram wiring ESP32 Display/Monitor yang menampilkan status di LCD I2C dan menyediakan tombol kontrol.

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.

© 2025 — Proyek Tandon Air Otomatis

No comments:

Post a Comment

Tandon Air Otomatis (ESP32 Master & Display) + Kode Lengkap

  Panduan Lengkap Tandon Air Otomatis (ESP32 Master & Display) + Kode Lengkap Diperbarui: 09 August 2025 Artikel ini memandu Anda memban...