first commit

This commit is contained in:
MohammadHakikiRiza 2026-07-22 20:12:42 +07:00
commit a53277d495
1 changed files with 733 additions and 0 deletions

View File

@ -0,0 +1,733 @@
// =============================================================
// ESP32 Irrigation Controller — Budidaya Jamur
// Mode : AUTO | MANUAL | OFFLINE
// Sensor: SHT31 | Aktuator: Relay | MQTT: HiveMQ TLS
// =============================================================
#include <Wire.h>
#include <WiFi.h>
#include <LiquidCrystal_I2C.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Adafruit_SHT31.h>
// ======================== 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<float>();
if (v > 0 && v < 100) _tempMax = v;
}
if (doc.containsKey("hum")) {
float v = doc["hum"].as<float>();
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]");
}
}
}
}
}
}