first commit
This commit is contained in:
commit
a53277d495
|
|
@ -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]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue