#include <Wire.h>
#include <RTClib.h>
#include <LiquidCrystal_I2C.h>
#include <ESP32Servo.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <time.h>
#include <Preferences.h>
// ================= WIFI & MQTT =================
const char* ssid = "Wokwi-GUEST"; // Ganti dengan nama WiFi Anda
const char* password = ""; // Ganti dengan password WiFi Anda
const char* mqtt_server = "silemon.duckdns.org"; // Domain/IP VPS Anda
WiFiClient espClient;
PubSubClient client(espClient);
RTC_DS3231 rtc;
// LCD I2C 16x2
LiquidCrystal_I2C lcd(0x27, 16, 2);
// ================= PIN =================
#define SERVO_PIN 26
#define SERVO2_PIN 27 // Penguras Air menggunakan Servo (Pengganti Relay 2)
#define RELAY3 14
Servo myServo; // Servo Pemberi Pakan
Servo myServo2; // Servo Penguras Air
// ================= DURASI =================
unsigned long servoDuration = 10000; // 10 detik
unsigned long relay23Duration = 15UL * 60UL * 1000UL;
// ================= STATUS =================
bool rtcConnected = false;
bool servoRunning = false;
bool relay2Running = false;
bool relay3Running = false;
bool autoMode = true; // true = Otomatis (default), false = Manual
// Dynamic Servo Detach Timers (Pencegah Jitter & Hemat Daya/Anti-Brownout)
bool servoDetaching = false;
unsigned long servoDetachMillis = 0;
bool servo2Detaching = false;
unsigned long servo2DetachMillis = 0;
// Memori Persisten (Preferences) untuk Status Pakan & Kuras Anti-Trigger Ulang saat Restart
Preferences preferences;
int lastFedHour = 99;
int lastDrainedDay = 99;
// ================= NTP TIME SYNC =================
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 7 * 3600; // GMT+7 (WIB)
const int daylightOffset_sec = 0; // Tidak ada DST di Indonesia
bool ntpSynced = false;
unsigned long lastNtpSyncMillis = 0;
unsigned long servoMillis = 0;
unsigned long relay2Millis = 0;
unsigned long relay3Millis = 0;
bool servoDone = false;
bool fridayDone = false;
String namaHari[7] = {
"MINGGU",
"SENIN",
"SELASA",
"RABU",
"KAMIS",
"JUMAT",
"SABTU"
};
// ================= FUNGSI WIFI & MQTT CALLBACK =================
void setup_wifi() {
Serial.println();
Serial.print("Memulai koneksi WiFi ke ");
Serial.println(ssid);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("KONEKSI WIFI...");
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
// Jika simulasi Wokwi (Wokwi-GUEST), arahkan langsung ke Channel 6 agar koneksi instan (< 1 detik)
if (String(ssid) == "Wokwi-GUEST") {
WiFi.begin(ssid, password, 6);
} else {
WiFi.begin(ssid, password);
}
// Berikan jeda inisialisasi awal (maksimal 20 x 300ms = 6 detik)
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(300);
Serial.print(".");
lcd.setCursor(attempts % 16, 1);
lcd.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi TERHUBUNG!");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("WIFI TERHUBUNG!");
lcd.setCursor(0, 1);
lcd.print(WiFi.localIP());
delay(1000);
} else {
Serial.println("\nKoneksi awal timeout, lanjut mencoba di latar belakang...");
}
lcd.clear();
}
void callback(char* topic, byte* payload, unsigned int length) {
String message = "";
for (int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("Pesan MQTT masuk [");
Serial.print(topic);
Serial.print("]: ");
Serial.println(message);
// KONTROL MODE (OTOMATIS / MANUAL)
if (message == "mode_auto") {
autoMode = true;
Serial.println("MQTT -> MODE OTOMATIS AKTIF");
}
else if (message == "mode_manual") {
autoMode = false;
Serial.println("MQTT -> MODE MANUAL AKTIF");
}
// KONTROL PAKAN (SERVO)
else if (message == "feed") {
if (!servoRunning) {
myServo.attach(SERVO_PIN, 500, 2400); // Attach dynamically
myServo.write(90);
servoRunning = true;
servoDetaching = false; // Batal detaching
servoMillis = millis();
Serial.println("MQTT -> SERVO ON (Pakan)");
}
}
// KONTROL SERVO 2 (PENGURAS AIR)
else if (message == "relay2_on") {
myServo2.attach(SERVO2_PIN, 500, 2400); // Attach dynamically
myServo2.write(90); // Membuka katup penguras (90 derajat)
relay2Running = true;
servo2Detaching = false; // Batal detaching
relay2Millis = millis();
Serial.println("MQTT -> SERVO 2 ON (Penguras Air)");
}
else if (message == "relay2_off") {
myServo2.write(0); // Menutup katup penguras (0 derajat)
relay2Running = false;
servo2Detaching = true; // Jadwalkan detach
servo2DetachMillis = millis();
Serial.println("MQTT -> SERVO 2 OFF (Penguras Air) - Detaching in 1.5s");
}
// KONTROL RELAY 3 (ISI AIR)
else if (message == "relay3_on") {
// Soft start safety: Berikan jeda 200ms jika Servo 2 baru saja dimatikan
if (millis() - servo2DetachMillis < 1000) {
delay(200);
}
digitalWrite(RELAY3, HIGH);
relay3Running = true;
relay3Millis = millis();
Serial.println("MQTT -> RELAY 3 ON (Isi Air)");
}
else if (message == "relay3_off") {
digitalWrite(RELAY3, LOW);
relay3Running = false;
Serial.println("MQTT -> RELAY 3 OFF (Isi Air)");
}
}
void reconnect() {
// Loop sampai terhubung kembali ke broker MQTT
static unsigned long lastReconnectAttempt = 0;
if (!client.connected()) {
unsigned long now = millis();
// Mencoba reconnect setiap 5 detik agar tidak blocking loop utama
if (now - lastReconnectAttempt > 5000) {
lastReconnectAttempt = now;
Serial.print("Menghubungkan ke MQTT Broker...");
// Buat ClientID acak
String clientID = "ESP32_Lele_";
clientID += String(random(0xffff), HEX);
if (client.connect(clientID.c_str())) {
Serial.println("terhubung!");
// Subscribe ke topik kontrol
client.subscribe("kolam/control");
} else {
Serial.print("gagal, rc=");
Serial.print(client.state());
Serial.println(" coba lagi dalam 5 detik.");
}
}
}
}
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
// ================= LCD =================
lcd.begin(16, 2);
lcd.backlight();
lcd.display();
lcd.setCursor(0, 0);
lcd.print("SISTEM MULAI");
delay(1000);
lcd.clear();
// ================= WIFI & MQTT SETUP =================
setup_wifi();
client.setServer(mqtt_server, 1883);
client.setCallback(callback);
// Inisialisasi NTP untuk sinkronisasi waktu otomatis
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("NTP Terkonfigurasi ke pool.ntp.org");
// ================= RTC (NON-BLOCKING FALLBACK SETUP) =================
if (!rtc.begin()) {
Serial.println("WARNING: RTC tidak ditemukan! Sistem akan menggunakan sinkronisasi waktu internet (NTP).");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("RTC NOT FOUND!");
lcd.setCursor(0, 1);
lcd.print("NTP FALLBACK ON");
delay(2000);
lcd.clear();
rtcConnected = false;
} else {
rtcConnected = true;
Serial.println("RTC DS3231 terdeteksi dan aktif.");
}
// ================= SERVO INITIALIZATION (Hemat Daya & Cegah Brownout) =================
myServo.setPeriodHertz(50);
myServo.attach(SERVO_PIN, 500, 2400);
myServo.write(0);
myServo2.setPeriodHertz(50);
myServo2.attach(SERVO2_PIN, 500, 2400);
myServo2.write(0);
// Biarkan servo bergerak ke posisi 0 selama 1.5 detik saat startup, lalu detach
delay(1500);
myServo.detach();
myServo2.detach();
Serial.println("Servos diset ke posisi 0 dan dinonaktifkan (detached) untuk hemat daya.");
// ================= RELAY =================
pinMode(RELAY3, OUTPUT);
digitalWrite(RELAY3, LOW);
// ================= PREFERENCES (PERSISTENT MEMORY) =================
preferences.begin("pond-sys", false);
lastFedHour = preferences.getInt("lastFed", 99);
lastDrainedDay = preferences.getInt("lastDrained", 99);
Serial.print("Memori Flash Terbuka. Jam Pakan Terakhir: ");
Serial.print(lastFedHour);
Serial.print(", Hari Kuras Terakhir: ");
Serial.println(lastDrainedDay);
}
void loop() {
// ================= WIFI & MQTT LOOP =================
if (WiFi.status() == WL_CONNECTED) {
if (!client.connected()) {
reconnect();
}
client.loop();
// ================= SINKRONISASI NTP KE RTC DS3231 =================
unsigned long currentMillis = millis();
if (rtcConnected && (!ntpSynced || (currentMillis - lastNtpSyncMillis > 24UL * 60UL * 60UL * 1000UL))) {
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
rtc.adjust(DateTime(timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec));
ntpSynced = true;
lastNtpSyncMillis = currentMillis;
Serial.println("BERHASIL: RTC DS3231 disinkronkan dengan waktu NTP Internet!");
}
}
} else {
// Non-blocking WiFi reconnect manager yang aman (tidak bentrok dengan proses connecting)
static unsigned long lastWiFiCheck = 0;
unsigned long now = millis();
if (now - lastWiFiCheck > 15000) { // Cek setiap 15 detik
lastWiFiCheck = now;
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi terputus! Mencoba menghubungkan kembali...");
WiFi.disconnect();
delay(100);
if (String(ssid) == "Wokwi-GUEST") {
WiFi.begin(ssid, password, 6);
} else {
WiFi.begin(ssid, password);
}
}
}
ntpSynced = false;
}
// ================= DETACH TIMERS (NON-BLOCKING) =================
// Menonaktifkan servo secara otomatis setelah gerakan selesai agar tidak bergetar & hemat daya
if (servoDetaching && (millis() - servoDetachMillis >= 1500)) {
myServo.detach();
servoDetaching = false;
Serial.println("Servo 1 (Pakan) detached.");
}
if (servo2Detaching && (millis() - servo2DetachMillis >= 1500)) {
myServo2.detach();
servo2Detaching = false;
Serial.println("Servo 2 (Kuras) detached.");
}
// ================= TIME ACQUISITION (WITH NTP FALLBACK & DATE) =================
int hari = 0;
int jam = 0;
int menit = 0;
int detik = 0;
int tanggal = 0;
String timestampStr = "00:00:00";
if (rtcConnected) {
DateTime now = rtc.now();
hari = now.dayOfTheWeek();
jam = now.hour();
menit = now.minute();
detik = now.second();
tanggal = now.day();
timestampStr = now.timestamp(DateTime::TIMESTAMP_TIME);
} else {
// Fallback menggunakan jam internal ESP32 yang disinkronkan NTP
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
hari = timeinfo.tm_wday;
jam = timeinfo.tm_hour;
menit = timeinfo.tm_min;
detik = timeinfo.tm_sec;
tanggal = timeinfo.tm_mday;
char buf[20];
sprintf(buf, "%02d:%02d:%02d", jam, menit, detik);
timestampStr = String(buf);
}
}
// ==================================================
// SERVO (PENGGANTI RELAY1) - Hanya jalan jika mode Otomatis
// ==================================================
if (autoMode && !servoRunning && !servoDone) {
if (((jam == 8 && menit == 0) ||
(jam == 15 && menit == 0) ||
(jam == 21 && menit == 0)) && (lastFedHour != jam)) {
myServo.attach(SERVO_PIN, 500, 2400); // Attach dynamically
myServo.write(90); // servo bergerak
servoRunning = true;
servoDetaching = false; // Batal detaching
servoMillis = millis();
servoDone = true;
// Simpan jam pakan terakhir ke memori flash agar tidak terulang jika restart
lastFedHour = jam;
preferences.putInt("lastFed", lastFedHour);
Serial.print("Servo ON (90 derajat) - Jam Pakan: ");
Serial.println(jam);
}
}
// Servo kembali setelah 10 detik
if (servoRunning &&
millis() - servoMillis >= servoDuration) {
myServo.write(0);
servoRunning = false;
servoDetaching = true; // Jadwalkan detach
servoDetachMillis = millis();
Serial.println("Servo OFF (0 derajat) - Detaching in 1.5s");
}
// reset trigger supaya tidak berulang
if (menit != 0) {
servoDone = false;
}
// ==================================================
// SERVO 2 (PENGURAS AIR) JUMAT 17:00 - Hanya jalan jika mode Otomatis
// ==================================================
if (autoMode &&
hari == 5 &&
jam == 17 &&
menit == 0 &&
!fridayDone &&
!relay2Running &&
(lastDrainedDay != tanggal)) {
myServo2.attach(SERVO2_PIN, 500, 2400); // Attach dynamically
myServo2.write(90); // Membuka katup penguras
relay2Running = true;
servo2Detaching = false; // Batal detaching
relay2Millis = millis();
fridayDone = true;
// Simpan tanggal kuras terakhir ke memori flash agar tidak terulang jika restart
lastDrainedDay = tanggal;
preferences.putInt("lastDrained", lastDrainedDay);
Serial.println("Servo 2 ON (Membuka Katup) - Proses kuras dimulai hari ini");
}
// Servo 2 OFF (Menutup Katup) -> Relay 3 ON
if (relay2Running &&
millis() - relay2Millis >= relay23Duration) {
myServo2.write(0); // Menutup katup penguras
relay2Running = false;
servo2Detaching = true; // Jadwalkan detach
servo2DetachMillis = millis();
Serial.println("Servo 2 OFF (Menutup Katup) - Detaching in 1.5s");
// Soft start safety: Berikan jeda 200ms jika baru mematikan servo untuk cegah lonjakan arus
delay(200);
digitalWrite(RELAY3, HIGH);
relay3Running = true;
relay3Millis = millis();
Serial.println("Relay 3 ON");
}
// Relay3 OFF
if (relay3Running &&
millis() - relay3Millis >= relay23Duration) {
digitalWrite(RELAY3, LOW);
relay3Running = false;
Serial.println("Relay 3 OFF");
}
// Reset Jumat
if (jam == 0 && menit == 1) {
fridayDone = false;
}
// ==================================================
// TAMPIL LCD & SERIAL (NON-BLOCKING 1 DETIK)
// ==================================================
static unsigned long lastUpdateMillis = 0;
if (millis() - lastUpdateMillis >= 1000) {
lastUpdateMillis = millis();
lcd.setCursor(0, 0);
lcd.print(namaHari[hari]);
lcd.print(" ");
if (jam < 10) lcd.print("0");
lcd.print(jam);
lcd.print(":");
if (menit < 10) lcd.print("0");
lcd.print(menit);
lcd.print(":");
if (detik < 10) lcd.print("0");
lcd.print(detik);
lcd.setCursor(0, 1);
lcd.print(" P=");
lcd.print(servoRunning ? 1 : 0);
lcd.print(" K=");
lcd.print(relay2Running ? 1 : 0);
lcd.print(" IA=");
lcd.print(relay3Running ? 1 : 0);
lcd.print(autoMode ? " A" : " M");
// Indikator status koneksi di pojok kanan bawah (Kolom ke-16)
if (WiFi.status() != WL_CONNECTED) {
lcd.print("."); // WiFi terputus/sedang mencoba
} else if (!client.connected()) {
lcd.print("?"); // WiFi terhubung, MQTT terputus
} else {
lcd.print("*"); // WiFi & MQTT Terhubung Sempurna!
}
// Kirim data status ke Serial
Serial.print(namaHari[hari]);
Serial.print(" ");
Serial.print(timestampStr);
Serial.print(" S=");
Serial.print(servoRunning);
Serial.print(" K=");
Serial.print(relay2Running);
Serial.print(" IA=");
Serial.print(relay3Running);
Serial.print(" Mode=");
Serial.println(autoMode ? "Auto" : "Manual");
// Kirim data status ke MQTT (Publish)
if (client.connected()) {
String payload = "{\"mode\":" + String(autoMode ? "\"auto\"" : "\"manual\"") +
",\"pakan\":" + String(servoRunning ? "1" : "0") +
",\"kuras\":" + String(relay2Running ? "1" : "0") +
",\"isi_air\":" + String(relay3Running ? "1" : "0") + "}";
client.publish("kolam/status", payload.c_str());
}
}
}