From a53277d495b3880e986ef9d87755d7e933ab511d Mon Sep 17 00:00:00 2001 From: MohammadHakikiRiza Date: Wed, 22 Jul 2026 20:12:42 +0700 Subject: [PATCH] first commit --- .../monolith__1_1_copy_2026062814114688.ino | 733 ++++++++++++++++++ 1 file changed, 733 insertions(+) create mode 100644 Kode Program Alat/monolith__1_1_copy_2026062814114688.ino diff --git a/Kode Program Alat/monolith__1_1_copy_2026062814114688.ino b/Kode Program Alat/monolith__1_1_copy_2026062814114688.ino new file mode 100644 index 0000000..4edb0ea --- /dev/null +++ b/Kode Program Alat/monolith__1_1_copy_2026062814114688.ino @@ -0,0 +1,733 @@ +// ============================================================= +// ESP32 Irrigation Controller — Budidaya Jamur +// Mode : AUTO | MANUAL | OFFLINE +// Sensor: SHT31 | Aktuator: Relay | MQTT: HiveMQ TLS +// ============================================================= + +#include +#include +#include +#include +#include +#include +#include + +// ======================== CONFIGURATION (KONFIGURASI) ======================== +// Konfigurasi WiFi +#define WIFI_SSID "Hotel_Bintang_5" +#define WIFI_PASSWORD "1sampai10" + +// Konfigurasi MQTT Broker (Cloud) +#define MQTT_SERVER "7567af42f48f4f9f9fc63b41958f2517.s1.eu.hivemq.cloud" +#define MQTT_PORT 8883 +#define MQTT_USER "bimabuana" +#define MQTT_PASS "25HikaruNakamura" + +// Identitas Unit +#define DEVICE_ID "esp32-new-05" + +// Topik MQTT (Jalur Komunikasi) +#define TOPIC_SENSOR "sensor/dht22" // Topik untuk mengirim data sensor +static const String topicRelay = "cmd/relay/" + String(DEVICE_ID); // Topik menerima perintah pompa (ON/OFF) +static const String topicMode = "cmd/mode/" + String(DEVICE_ID); // Topik menerima perubahan mode (auto/manual) +static const String topicThreshold = "config/threshold/" + String(DEVICE_ID); // Topik menerima batas suhu/kelembapan baru +static const String topicStatus = "status/" + String(DEVICE_ID); // Topik status keaktifan (LWT) + +// Konfigurasi Pin Perangkat Keras +#define RELAY_PIN 23 // Pin yang terhubung ke relay pompa +#define RELAY_ON LOW // Logika untuk menyalakan relay (Active Low) +#define RELAY_OFF HIGH // Logika untuk mematikan relay + +// Konfigurasi Waktu (dalam Milidetik / ms) +#define DURASI_RELAY 30000UL // Lama pompa menyiram: 30 detik +#define DURASI_COOLDOWN 60000UL // Jeda sebelum pompa bisa menyiram lagi: 1 menit +#define INTERVAL_SENSOR 3000UL // Interval membaca sensor SHT31: 3 detik +#define INTERVAL_WIFI 10000UL // Interval mengecek koneksi WiFi: 10 detik +#define INTERVAL_LCD 1000UL // Interval menyegarkan layar LCD: 1 detik + +// Batasan (Threshold) Bawaan Pabrik +#define DEFAULT_TEMP_MAX 30.0 // Suhu maksimal (jika lebih, pompa nyala) +#define DEFAULT_HUM_MIN 85.0 // Kelembapan minimal (jika kurang, pompa nyala) + + +// ======================== ENUMS & GLOBALS (VARIABEL GLOBAL) ======================== + +// Tipe mode yang tersedia +enum class DeviceMode { AUTO, MANUAL, OFFLINE }; +DeviceMode currentMode = DeviceMode::AUTO; // Mode awal saat perangkat menyala + +// Objek Perangkat Keras +LiquidCrystal_I2C lcd(0x27, 16, 2); // Alamat I2C layar LCD biasanya 0x27 +Adafruit_SHT31 sht31; // Objek sensor suhu & kelembapan + +// Objek Koneksi Jaringan +WiFiClientSecure espClient; // Client WiFi dengan keamanan TLS +PubSubClient mqttClient(espClient); // Client MQTT + +// Variabel Penyimpan Data Sensor Terakhir +float lastSuhu = 0.0; +float lastHum = 0.0; + +// Variabel Pewaktu (Timer) untuk mengeksekusi tugas secara berkala tanpa "delay()" +unsigned long lastSend = 0; +unsigned long lastWifiCheck = 0; +unsigned long lastLcdUpdate = 0; +unsigned long lcdMsgStartTime = 0; // Waktu mulai pesan LCD sementara +unsigned long lcdMsgDuration = 0; // Durasi pesan LCD sementara ditampilkan + +// Penanda apakah pompa sedang dikontrol paksa oleh jadwal (backend) +bool scheduleActive = false; + +// Status Relay (Pompa) saat ini +bool _relayState = false; + +// Variabel Penyimpan Nilai Batas Sensor (Threshold) +float _tempMax = DEFAULT_TEMP_MAX; +float _humMin = DEFAULT_HUM_MIN; + +// Penanda apakah ada perintah ganti mode dari aplikasi +bool _hasModeChange = false; +DeviceMode _pendingMode = DeviceMode::AUTO; + +// Penanda apakah ada perintah relay manual dari aplikasi +bool _hasRelayCmd = false; +bool _pendingRelayCmdOn = false; + +// Variabel Koneksi Ulang MQTT +unsigned long lastMqttReconnectAttempt = 0; +int mqttRetryCount = 0; + +// ======================== VARIABEL STATE (MODE AUTO) ======================== +bool _autoRelayAktif = false; // Sedang dalam proses menyiram? +bool _autoCooldownAktif = false; // Sedang dalam masa jeda (cooldown)? +unsigned long _autoWaktuMulaiRelay = 0; // Waktu saat pompa mulai menyala +unsigned long _autoWaktuMulaiCooldown = 0; // Waktu saat masa jeda dimulai + +// ======================== VARIABEL STATE (MODE OFFLINE) ======================== +bool _offlineRelayAktif = false; +bool _offlineCooldownAktif = false; +unsigned long _offlineWaktuMulaiRelay = 0; +unsigned long _offlineWaktuMulaiCooldown = 0; + +// ======================== PROTOTYPES (DEKLARASI FUNGSI AWAL) ======================== +void autoModeEnter(); +void manualModeEnter(); +void offlineModeEnter(); +const char* autoModeRun(float suhu, float hum); +const char* manualModeRun(); +const char* offlineModeRun(float suhu, float hum); + +// ======================== LCD HELPERS (FUNGSI LAYAR LCD) ======================== + +// Menampilkan pesan sementara di layar LCD (contoh: "Pompa Menyala!") +void showLcdMsg(const char* l0, const char* l1, unsigned long durasi) { + lcd.clear(); + lcd.setCursor(0, 0); lcd.print(l0); // Baris 1 + lcd.setCursor(0, 1); lcd.print(l1); // Baris 2 + lcdMsgStartTime = millis(); + lcdMsgDuration = durasi; +} + +// Mengecek apakah waktu tayang pesan sementara di LCD sudah habis +bool lcdMsgExpired() { + return (millis() - lcdMsgStartTime) >= lcdMsgDuration; +} + +// ======================== RELAY (FUNGSI POMPA AIR) ======================== + +void relayBegin() { + pinMode(RELAY_PIN, OUTPUT); + digitalWrite(RELAY_PIN, RELAY_OFF); + _relayState = false; +} + +void relayOn() { + digitalWrite(RELAY_PIN, RELAY_ON); + _relayState = true; +} + +void relayOff() { + digitalWrite(RELAY_PIN, RELAY_OFF); + _relayState = false; +} + +// Mengambil status pompa saat ini (true = ON, false = OFF) +bool relayState() { + return _relayState; +} + +// ======================== SENSOR (FUNGSI SHT31) ======================== + +bool sensorBegin() { + return sht31.begin(0x44); // 0x44 adalah alamat I2C default untuk SHT31 +} + +// Membaca suhu dan kelembapan. Mengembalikan 'true' jika berhasil. +bool sensorRead(float &suhu, float &hum) { + float rawSuhu = sht31.readTemperature(); + float rawHum = sht31.readHumidity(); + + if (!isnan(rawSuhu) && !isnan(rawHum)) { + suhu = rawSuhu; + hum = rawHum; + + // Batasi nilai kelembapan agar tetap berada di rentang valid 0% - 100% + if (hum > 100.0) hum = 100.0; + if (hum < 0.0) hum = 0.0; + + return true; + } + return false; +} + +// ======================== MQTT (FUNGSI KOMUNIKASI CLOUD) ======================== + +// Fungsi yang dipanggil otomatis saat alat menerima pesan MQTT dari server +void onMessage(char* topic, byte* payload, unsigned int length) { + // Cegah fragmentasi memori (Memory Leak), gunakan static/lokal buffer + unsigned int len = length < 255 ? length : 255; + char buf[256]; + memcpy(buf, payload, len); + buf[len] = '\0'; // Tambahkan penutup string karakter + String msg = String(buf); + + String t = String(topic); + + // Jika pesan adalah instruksi ganti mode + if (t == topicMode) { + if (msg == "auto") { _pendingMode = DeviceMode::AUTO; _hasModeChange = true; } + else if (msg == "manual") { _pendingMode = DeviceMode::MANUAL; _hasModeChange = true; } + else if (msg == "offline") { _pendingMode = DeviceMode::OFFLINE; _hasModeChange = true; } + Serial.println("[MQTT] Mode request: " + msg); + return; + } + + // Jika pesan adalah instruksi menyalakan/mematikan pompa (khusus mode MANUAL) + if (t == topicRelay) { + if (msg == "ON") { _pendingRelayCmdOn = true; _hasRelayCmd = true; } + else if (msg == "OFF") { _pendingRelayCmdOn = false; _hasRelayCmd = true; } + Serial.println("[MQTT] Relay cmd: " + msg); + return; + } + + // Jika pesan adalah instruksi mengubah batas suhu & kelembapan + if (t == topicThreshold) { + StaticJsonDocument<128> doc; + if (!deserializeJson(doc, msg)) { // Parse pesan JSON + if (doc.containsKey("temp")) { + float v = doc["temp"].as(); + if (v > 0 && v < 100) _tempMax = v; + } + if (doc.containsKey("hum")) { + float v = doc["hum"].as(); + if (v > 0 && v <= 100) _humMin = v; + } + Serial.printf("[MQTT] Threshold: temp=%.1f hum=%.1f\n", _tempMax, _humMin); + } + } +} + +// Fungsi untuk menyambungkan kembali koneksi MQTT secara non-blocking +void reconnect() { + // Coba menyambung setiap 5 detik tanpa membuat seluruh sistem freeze (delay) + if (millis() - lastMqttReconnectAttempt > 5000) { + lastMqttReconnectAttempt = millis(); + Serial.print("[MQTT] Menghubungkan..."); + + // Jika berhasil nyambung dengan konfigurasi LWT (Last Will and Testament) + if (mqttClient.connect(DEVICE_ID, MQTT_USER, MQTT_PASS, topicStatus.c_str(), 1, true, "offline")) { + Serial.println("OK"); + mqttRetryCount = 0; // Reset counter percobaan + + // Kirim status online segera setelah berhasil terhubung + mqttClient.publish(topicStatus.c_str(), "online", true); + + // Berlangganan kembali ke topik agar bisa menerima perintah + mqttClient.subscribe(topicRelay.c_str()); + mqttClient.subscribe(topicMode.c_str()); + mqttClient.subscribe(topicThreshold.c_str()); + } else { + // Jika gagal + Serial.printf("GAGAL rc=%d\n", mqttClient.state()); + // Alat akan terus mencoba tanpa restart, membiarkan relay OFFLINE menjaga tanaman + } + } +} + +void mqttBegin() { + espClient.setInsecure(); // Karena menggunakan TLS (port 8883) tanpa verifikasi sertifikat root + mqttClient.setServer(MQTT_SERVER, MQTT_PORT); + mqttClient.setCallback(onMessage); // Daftarkan fungsi pemroses pesan + mqttClient.setBufferSize(512); // Ukuran buffer cukup untuk pesan sensor JSON +} + +// Fungsi perawatan MQTT, dipanggil terus-menerus di loop() +void mqttLoop() { + if (!mqttClient.connected()) { + reconnect(); + } else { + mqttClient.loop(); // Proses pesan masuk + } +} + +bool mqttConnected() { + return mqttClient.connected(); +} + +// Mengirim data sensor (suhu, kelembapan, status relay) ke server +void mqttPublish(float suhu, float hum, bool rOn, const char* modeStr) { + if (!mqttClient.connected()) return; // Jangan kirim kalau sedang terputus + + StaticJsonDocument<256> doc; + doc["temp"] = round(suhu * 10.0) / 10.0; // Format 1 angka desimal + doc["hum"] = round(hum * 10.0) / 10.0; + doc["device_id"] = DEVICE_ID; + doc["relay"] = rOn; + doc["mode"] = modeStr; + + char buf[256]; + serializeJson(doc, buf); // Ubah objek JSON jadi string + + if (mqttClient.publish(TOPIC_SENSOR, buf)) { + Serial.println("[MQTT] Publish OK: " + String(buf)); + } else { + Serial.println("[MQTT] Publish GAGAL"); + } +} + +// ==== FUNGSI PENGAMBIL DATA MQTT ==== + +bool mqttHasModeChange(DeviceMode &out) { + if (!_hasModeChange) return false; + out = _pendingMode; + _hasModeChange = false; // Reset status setelah data diambil + return true; +} + +bool mqttHasRelayCmd(bool &out) { + if (!_hasRelayCmd) return false; + out = _pendingRelayCmdOn; + _hasRelayCmd = false; // Reset status setelah data diambil + return true; +} + +float mqttGetTempMax() { return _tempMax; } +float mqttGetHumMin() { return _humMin; } + +// ======================== MODES LOGIC (LOGIKA MASING-MASING MODE) ======================== + +// ---- MODE AUTO ---- +void autoModeEnter() { + _autoRelayAktif = false; + _autoCooldownAktif = false; + relayOff(); + Serial.println("[AUTO] Mode entered"); +} + +const char* autoModeRun(float suhu, float hum) { + const char* modeStr = "auto"; + + // 1. Jika pompa sedang menyiram + if (_autoRelayAktif) { + modeStr = "auto-on"; + unsigned long elapsed = millis() - _autoWaktuMulaiRelay; + + // Update LCD hitung mundur sekarang dipindah ke timer 1 detik di loop() + + // Jika waktu menyiram sudah habis + if (elapsed >= DURASI_RELAY) { + if (!scheduleActive) { // Hanya matikan pompa jika jadwal backend tidak mengunci + relayOff(); + showLcdMsg("Relay OFF", "Cooldown...", 1500); + } + _autoRelayAktif = false; + _autoCooldownAktif = true; // Mulai masa jeda (Cooldown) + _autoWaktuMulaiCooldown = millis(); + Serial.println("[AUTO] Selesai siram, masuk cooldown"); + } + + // 2. Jika sedang dalam masa Jeda (Cooldown) + } else if (_autoCooldownAktif) { + modeStr = "cooldown"; + unsigned long elapsed = millis() - _autoWaktuMulaiCooldown; + + // Update LCD hitung mundur sekarang dipindah ke timer 1 detik di loop() + + // Jika waktu jeda sudah habis, kembali memantau + if (elapsed >= DURASI_COOLDOWN) { + _autoCooldownAktif = false; + showLcdMsg("Siap", "Monitoring...", 800); + Serial.println("[AUTO] Cooldown selesai, monitoring"); + } + + // 3. Status Siaga (Memantau Sensor) + } else { + float tempMax = mqttGetTempMax(); + float humMin = mqttGetHumMin(); + + // Syarat menyiram: suhu melampaui batas ATAU kelembapan berada di bawah batas + bool kondisi = (suhu > tempMax) || (hum < humMin); + + // Pengaman: Jika sensor error (membaca 0.0), JANGAN nyalakan pompa untuk mencegah banjir + if (suhu == 0.0 && hum == 0.0) { + kondisi = false; + } + + if (kondisi) { + if (!scheduleActive) { // Nyalakan pompa jika tidak ada intervensi jadwal + relayOn(); + showLcdMsg("Relay ON!", suhu > tempMax ? "Suhu tinggi" : "Humid rendah", 1500); + Serial.printf("[AUTO] Relay ON! suhu=%.1f hum=%.1f\n", suhu, hum); + } + // Pindahkan state (status) menjadi "Menyiram" + _autoRelayAktif = true; + _autoWaktuMulaiRelay = millis(); + modeStr = "auto-on"; + } else { + if (lcdMsgExpired()) { + lcd.setCursor(0, 1); + lcd.print("[A] Monitoring "); // Status di layar LCD + } + } + } + return modeStr; +} + +// ---- MODE MANUAL ---- +void manualModeEnter() { + relayOff(); + Serial.println("[MANUAL] Mode entered, relay OFF"); + showLcdMsg("[MANUAL]", "Menunggu CMD...", 1500); +} + +const char* manualModeRun() { + bool cmdOn; + // Membaca perintah manual via MQTT. (Abaikan jika jadwal sedang aktif) + if (!scheduleActive && mqttHasRelayCmd(cmdOn)) { + if (cmdOn) { + relayOn(); + showLcdMsg("[M] POMPA ON", "Manual override", 1500); + Serial.println("[MANUAL] Relay ON"); + } else { + relayOff(); + showLcdMsg("[M] POMPA OFF", "Manual override", 1500); + Serial.println("[MANUAL] Relay OFF"); + } + } + + // Tampilkan status on/off ke layar LCD + if (lcdMsgExpired()) { + lcd.setCursor(0, 1); + lcd.print(relayState() ? "[M] POMPA ON " : "[M] POMPA OFF "); + } + + return relayState() ? "manual-on" : "manual-off"; +} + +// ---- MODE OFFLINE ---- +// Mode darurat saat MQTT/Internet putus. Mirip AUTO, namun beroperasi tanpa komunikasi cloud +void offlineModeEnter() { + _offlineRelayAktif = false; + _offlineCooldownAktif = false; + relayOff(); + Serial.println("[OFFLINE] Mode entered"); + showLcdMsg("[OFFLINE]", "Auto no MQTT", 1500); +} + +const char* offlineModeRun(float suhu, float hum) { + const char* modeStr = "offline"; + + if (_offlineRelayAktif) { + modeStr = "offline-on"; + unsigned long elapsed = millis() - _offlineWaktuMulaiRelay; + + // Update LCD dipindah ke loop() per detik + + if (elapsed >= DURASI_RELAY) { + relayOff(); + _offlineRelayAktif = false; + _offlineCooldownAktif = true; + _offlineWaktuMulaiCooldown = millis(); + showLcdMsg("Relay OFF", "Cooldown...", 1500); + Serial.println("[OFFLINE] Selesai siram, cooldown"); + } + + } else if (_offlineCooldownAktif) { + modeStr = "offline-cooldown"; + unsigned long elapsed = millis() - _offlineWaktuMulaiCooldown; + + // Update LCD dipindah ke loop() per detik + + if (elapsed >= DURASI_COOLDOWN) { + _offlineCooldownAktif = false; + showLcdMsg("[OFFLINE]", "Monitoring...", 800); + Serial.println("[OFFLINE] Cooldown selesai"); + } + + } else { + // Tetap menggunakan angka threshold terakhir dari variabel global + float tempMax = mqttGetTempMax(); + float humMin = mqttGetHumMin(); + bool kondisi = (suhu > tempMax) || (hum < humMin); + + // Pengaman: Jika sensor error (membaca 0.0), JANGAN nyalakan pompa untuk mencegah banjir + if (suhu == 0.0 && hum == 0.0) { + kondisi = false; + } + + if (kondisi) { + relayOn(); + _offlineRelayAktif = true; + _offlineWaktuMulaiRelay = millis(); + modeStr = "offline-on"; + showLcdMsg("Relay ON!", suhu > tempMax ? "Suhu tinggi" : "Humid rendah", 1500); + Serial.printf("[OFFLINE] Relay ON! suhu=%.1f hum=%.1f\n", suhu, hum); + } else { + if (lcdMsgExpired()) { + lcd.setCursor(0, 1); + lcd.print("[OFL] Monitor "); + } + } + } + + return modeStr; +} + +// ======================== MAIN (FUNGSI UTAMA ARDUINO) ======================== + +// Mengelola koneksi WiFi di awal nyala +bool setupWifi() { + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + lcd.clear(); + lcd.setCursor(0, 0); lcd.print("Connecting WiFi"); + lcd.setCursor(0, 1); lcd.print("Timeout: 120s"); + + unsigned long start = millis(); + unsigned long elapsed = 0; + + // Tunggu maksimal 120 detik (2 menit) + while (WiFi.status() != WL_CONNECTED) { + elapsed = (millis() - start) / 1000; + lcd.setCursor(0, 1); + lcd.print("Sisa: "); + lcd.print(120 - elapsed); + lcd.print("s "); + + if (elapsed >= 120) { + // Jika melebihi waktu tunggu, sistem masuk mode offline + lcd.clear(); + lcd.setCursor(0, 0); lcd.print("WiFi Timeout!"); + lcd.setCursor(0, 1); lcd.print("Mode: OFFLINE"); + Serial.println("[WIFI] Timeout 2 menit, masuk OFFLINE"); + delay(1500); + return false; + } + delay(500); + } + + lcd.clear(); + lcd.setCursor(0, 0); lcd.print("WiFi Connected!"); + lcd.setCursor(0, 1); lcd.print(WiFi.localIP()); // Tampilkan IP Alat + Serial.println("[WIFI] Terhubung: " + WiFi.localIP().toString()); + delay(1500); + return true; +} + +// Memindahkan operasional alat dari satu mode ke mode lain +void switchMode(DeviceMode newMode) { + if (newMode == currentMode) return; + currentMode = newMode; + + switch (currentMode) { + case DeviceMode::AUTO: autoModeEnter(); break; + case DeviceMode::MANUAL: manualModeEnter(); break; + case DeviceMode::OFFLINE: offlineModeEnter(); break; + } + + Serial.printf("[MAIN] Mode switched -> %s\n", + currentMode == DeviceMode::AUTO ? "AUTO" : + currentMode == DeviceMode::MANUAL ? "MANUAL" : "OFFLINE"); +} + +// setup() dieksekusi 1x saat alat baru dihidupkan (Boot) +void setup() { + Serial.begin(115200); // Buka komunikasi serial monitor + Wire.begin(21, 22); // Konfigurasi pin I2C (SDA=21, SCL=22) untuk LCD & Sensor + + relayBegin(); + + lcd.init(); + lcd.backlight(); + + if (!sensorBegin()) { + lcd.clear(); + lcd.print("SHT31 Error!"); + Serial.println("[MAIN] SHT31 tidak ditemukan! Melanjutkan tanpa sensor..."); + delay(3000); // Tampilkan pesan selama 3 detik lalu lanjut berjalan + } + + bool wifiOk = setupWifi(); + + mqttBegin(); // Setup konfigurasi koneksi Cloud (belum konek) + + // Pilih mode awal berdasarkan ketersediaan jaringan + if (wifiOk) { + currentMode = DeviceMode::AUTO; + autoModeEnter(); + showLcdMsg("[AUTO]", "Monitoring...", 1500); + Serial.println("[MAIN] Start -> AUTO"); + } else { + currentMode = DeviceMode::OFFLINE; + offlineModeEnter(); + Serial.println("[MAIN] Start -> OFFLINE"); + } +} + +// loop() dieksekusi berulang-ulang selamanya dengan cepat +void loop() { + // 1. WiFi Management (Memelihara Koneksi WiFi) + if (millis() - lastWifiCheck > INTERVAL_WIFI) { + lastWifiCheck = millis(); + if (WiFi.status() != WL_CONNECTED) { + Serial.println("[MAIN] WiFi putus, reconnect..."); + WiFi.disconnect(); + WiFi.begin(WIFI_SSID, WIFI_PASSWORD); + } else if (currentMode == DeviceMode::OFFLINE && !mqttConnected()) { + Serial.println("[MAIN] WiFi pulih saat OFFLINE, reconnect MQTT..."); + mqttBegin(); + showLcdMsg("[OFFLINE]", "WiFi pulih!", 1500); + } + } + + // 2. MQTT Management (Mengurus Pesan Masuk & Putus Nyambung Cloud) + mqttLoop(); + + // 3. Memproses Permintaan Ganti Mode via Aplikasi + DeviceMode requestedMode; + if (mqttHasModeChange(requestedMode)) { + switchMode(requestedMode); + } + + // 3b. Memproses Sistem Jadwal Paksa (Schedule Override) + if (currentMode != DeviceMode::OFFLINE) { + bool cmdOn; + if (mqttHasRelayCmd(cmdOn)) { + if (cmdOn && !scheduleActive) { // Jadwal mulai + scheduleActive = true; + + // Bersihkan state (waktu & timer) di balik layar agar mode Auto tidak diam-diam menghitung + _autoRelayAktif = false; + _autoCooldownAktif = false; + _offlineRelayAktif = false; + _offlineCooldownAktif = false; + + relayOn(); // Ambil alih kendali pompa paksa ON + showLcdMsg("[JADWAL] ON", "Sch override", 1500); + Serial.println("[SCH] Jadwal ON -> relay dikunci"); + + } else if (!cmdOn && scheduleActive) { // Jadwal usai + scheduleActive = false; + relayOff(); + showLcdMsg("[JADWAL] OFF", "Kembali ke mode", 1500); + Serial.println("[SCH] Jadwal OFF -> kembali ke mode aktif"); + + // Kembalikan ke mode awal setelah jadwal selesai + switch (currentMode) { + case DeviceMode::AUTO: autoModeEnter(); break; + case DeviceMode::MANUAL: manualModeEnter(); break; + default: break; + } + } + } + } + + // 4. Proses Pembacaan Sensor dan Evaluasi Timer Rutin + if (millis() - lastSend >= INTERVAL_SENSOR) { // Setiap 3 detik + lastSend = millis(); + float suhu, hum; + bool sensorOk = sensorRead(suhu, hum); + + // Cegah sistem stuck/diam jika terjadi error pembacaan sensor mendadak (Kabel Goyang) + if (!sensorOk) { + Serial.println("[MAIN] Sensor error: NaN, mencoba inisialisasi ulang..."); + sensorBegin(); // Coba inisialisasi ulang agar bisa pulih saat sensor dicolok lagi + suhu = lastSuhu; // Gunakan angka valid sebelumnya + hum = lastHum; + } else { + lastSuhu = suhu; + lastHum = hum; + } + + const char* modeStr = "auto"; + + // Jalankan logika operasional sesuai mode yang aktif + switch (currentMode) { + case DeviceMode::AUTO: + modeStr = scheduleActive ? "schedule-on" : autoModeRun(suhu, hum); + // Tetap publish status agar backend dapat memonitor perangkat + if(sensorOk) mqttPublish(suhu, hum, relayState(), modeStr); + break; + + case DeviceMode::MANUAL: + modeStr = scheduleActive ? "schedule-on" : manualModeRun(); + if(sensorOk) mqttPublish(suhu, hum, relayState(), modeStr); + break; + + case DeviceMode::OFFLINE: + modeStr = offlineModeRun(suhu, hum); + Serial.printf("[OFFLINE] suhu=%.1f hum=%.1f relay=%s\n", + suhu, hum, relayState() ? "ON" : "OFF"); + break; + } + + // Update layar LCD jika sedang berada di dalam Jadwal paksa + if (scheduleActive && lcdMsgExpired()) { + lcd.setCursor(0, 1); + lcd.print("[SCH] POMPA ON "); + } + } + + // 5. Menyegarkan Nilai Suhu & Kelembapan di Layar LCD + if (millis() - lastLcdUpdate >= INTERVAL_LCD) { // Setiap 1 detik + lastLcdUpdate = millis(); + + // Pastikan tidak menimpa pesan sementara (seperti "Relay ON!") + if (lcdMsgExpired()) { + // --- Update Baris 1: Suhu & Kelembapan --- + lcd.setCursor(0, 0); + if (lastSuhu == 0.0 && lastHum == 0.0) { + lcd.print("T:-- H:-- "); // Jika data kosong/baru nyala + } else { + lcd.print("T:"); lcd.print(lastSuhu, 1); // Desimal 1 angka + lcd.print(" H:"); lcd.print(lastHum, 0); // Tanpa desimal + lcd.print("% "); + } + + // --- Update Baris 2: Hitung Mundur (Countdown) Mulus 1 Detik --- + if (!scheduleActive) { // Jangan timpa jika sedang jadwal paksa + if (currentMode == DeviceMode::AUTO) { + if (_autoRelayAktif) { + unsigned long el = millis() - _autoWaktuMulaiRelay; + unsigned long sisa = (el < DURASI_RELAY) ? (DURASI_RELAY - el) / 1000 : 0; + lcd.setCursor(0, 1); lcd.print("Siram "); lcd.print(sisa); lcd.print("s "); + } else if (_autoCooldownAktif) { + unsigned long el = millis() - _autoWaktuMulaiCooldown; + unsigned long sisa = (el < DURASI_COOLDOWN) ? (DURASI_COOLDOWN - el) / 1000 : 0; + lcd.setCursor(0, 1); lcd.print("Jeda "); lcd.print(sisa); lcd.print("s "); + } + } + else if (currentMode == DeviceMode::OFFLINE) { + if (_offlineRelayAktif) { + unsigned long el = millis() - _offlineWaktuMulaiRelay; + unsigned long sisa = (el < DURASI_RELAY) ? (DURASI_RELAY - el) / 1000 : 0; + lcd.setCursor(0, 1); lcd.print("Siram "); lcd.print(sisa); lcd.print("s [OFL]"); + } else if (_offlineCooldownAktif) { + unsigned long el = millis() - _offlineWaktuMulaiCooldown; + unsigned long sisa = (el < DURASI_COOLDOWN) ? (DURASI_COOLDOWN - el) / 1000 : 0; + lcd.setCursor(0, 1); lcd.print("Jeda "); lcd.print(sisa); lcd.print("s [OFL]"); + } + } + } + } + } +} \ No newline at end of file