#include #include #include #include #include #include "RTClib.h" #include "time.h" // ================= FIREBASE ================= #define FIREBASE_HOST "telurku-fa78c-default-rtdb.asia-southeast1.firebasedatabase.app" #define FIREBASE_AUTH "3zofmC4xXE1g80NTQtB8X7JZp6eirMzUAwZC6iM8" // ================= PIN ================= const int pinENA = 13; const int pinIN1 = 12; const int pinIN2 = 14; const int pinIR1 = 34; const int pinIR2 = 35; const int pinIR3 = 32; const int pinSDA = 21; const int pinSCL = 22; // ================= OBJEK ================= RTC_DS3231 rtc; FirebaseData fbdo; FirebaseAuth auth; FirebaseConfig config; // ================= VARIABEL SISTEM ================= // Counter telur dari kandang int jumlahTelur1 = 0; int jumlahTelur2 = 0; // ===== CHECKPOINT UNTUK DELTA CALCULATION ===== // Ini TIDAK akan di-reset saat reset counter, hanya reset saat panen berhasil int ir1CheckpointPanen = 0; int ir2CheckpointPanen = 0; int jumlahTelurMasuk = 0; int totalTelurHarusKeBak = 0; // Total telur yang sudah dipanen sebelumnya int totalTelurSudahDipanen = 0; // Target panen saat conveyor berjalan int targetPanen = 0; // Status sensor sebelumnya int lastStatusIR1 = HIGH; int lastStatusIR2 = HIGH; int lastStatusIR3 = HIGH; // Status sistem bool motorBerjalan = false; bool modePanenAktif = false; bool dataTersinkron = false; bool firebaseInitialized = false; // Timer motor unsigned long waktuMulaiMotor = 0; unsigned long waktuMulaiFailsafe = 0; // Durasi dari Firebase int durasiTargetDetik = 0; // Durasi motor yang dapat diperpanjang unsigned long durasiMotorAkhir = 0; // Waktu telur terakhir masuk ke bak (untuk cek durasi tanpa telur) unsigned long waktuTelurTerakhirMasuk = 0; // Flag untuk mencegah penambahan durasi berkali-kali bool durasiSudahDitambahkan = false; // Waktu ketika target telur tercapai di IR3 unsigned long waktuTargetTercapai = 0; // Timeout setelah target tercapai sebelum motor OFF (10 detik) const unsigned long timeoutMotorAfterTarget = 10000; // Flag untuk motor yang sedang dalam proses "extra push" mengangkut sisa telur bool motorSedangPushSisa = false; // Timeout darurat 3 menit const unsigned long timeoutFailsafe = 180000; // ===== RESET COUNTER PENDING (event-based, bukan time-based) ===== bool resetCounterPending = false; // Flag: diminta untuk reset setelah panen selesai // ================= TIMER TASK ================= unsigned long lastTaskSensor = 0; unsigned long lastTaskFirebase = 0; unsigned long lastTaskJadwal = 0; unsigned long lastTaskResetWiFi = 0; unsigned long lastReconnectAttempt = 0; unsigned long lastWiFiStatusPrint = 0; const unsigned long intervalSensor = 50; const unsigned long intervalFirebase = 5000; const unsigned long intervalJadwal = 15000; const unsigned long intervalResetWiFi = 2000; const unsigned long intervalReconnect = 10000; // ================= MOTOR ================= void motorON() { // Hitung telur BARU yang masuk sejak panen terakhir selesai // Menggunakan checkpoint panen yang TIDAK di-reset dengan counter int telurBaruIR1 = jumlahTelur1 - ir1CheckpointPanen; int telurBaruIR2 = jumlahTelur2 - ir2CheckpointPanen; totalTelurHarusKeBak = telurBaruIR1 + telurBaruIR2; targetPanen = totalTelurHarusKeBak; if (targetPanen <= 0) { Serial.println( "[PANEN] Tidak ada telur baru" ); return; } jumlahTelurMasuk = 0; motorSedangPushSisa = false; waktuTargetTercapai = 0; modePanenAktif = true; digitalWrite(pinIN1, HIGH); digitalWrite(pinIN2, LOW); ledcWrite(0, 180); motorBerjalan = true; waktuMulaiMotor = millis(); waktuMulaiFailsafe = millis(); waktuTelurTerakhirMasuk = millis(); durasiSudahDitambahkan = false; durasiMotorAkhir = (unsigned long)durasiTargetDetik * 1000; if (Firebase.ready()) { Firebase.setBool( fbdo, "/aktuator/motor", true ); } Serial.printf( "[PANEN] Target telur = %d (IR1_delta: %d + IR2_delta: %d)\n", targetPanen, telurBaruIR1, telurBaruIR2 ); Serial.println( "[MOTOR] ON" ); } void motorOFF() { digitalWrite(pinIN1, LOW); digitalWrite(pinIN2, LOW); ledcWrite(0, 0); motorBerjalan = false; motorSedangPushSisa = false; if (Firebase.ready()) { Firebase.setBool( fbdo, "/aktuator/motor", false ); } Serial.println( "[MOTOR] OFF" ); } // ================= FIREBASE INIT ================= void initFirebase() { if (!firebaseInitialized) { config.host = FIREBASE_HOST; config.signer.tokens.legacy_token = FIREBASE_AUTH; Firebase.begin( &config, &auth ); Firebase.reconnectWiFi(true); firebaseInitialized = true; Serial.println( "[Firebase] Initialized" ); } } // ================= AMBIL DATA AWAL ================= void fetchInitialData() { if (!Firebase.ready()) return; Serial.println( "[Firebase] Mengambil data terakhir..." ); if ( Firebase.getInt( fbdo, "/data/infra1" ) ) { jumlahTelur1 = fbdo.intData(); Serial.printf( "IR1 terakhir: %d\n", jumlahTelur1 ); } if ( Firebase.getInt( fbdo, "/data/infra2" ) ) { jumlahTelur2 = fbdo.intData(); Serial.printf( "IR2 terakhir: %d\n", jumlahTelur2 ); } if ( Firebase.getInt( fbdo, "/data/sudah_dipanen" ) ) { totalTelurSudahDipanen = fbdo.intData(); Serial.printf( "Sudah dipanen: %d\n", totalTelurSudahDipanen ); } ir1CheckpointPanen = jumlahTelur1; ir2CheckpointPanen = jumlahTelur2; dataTersinkron = true; Serial.println( "[Firebase] Sinkronisasi awal berhasil" ); } // ================= NTP KE RTC ================= void sinkronisasiWaktuNTP() { if ( WiFi.status() != WL_CONNECTED ) { return; } configTime( 7 * 3600, 0, "id.pool.ntp.org", "time.nist.gov" ); 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 ) ); Serial.println( "[RTC] Sinkronisasi NTP berhasil" ); } else { Serial.println( "[RTC] Gagal sinkronisasi NTP" ); } } // ================= CEK KONEKSI ================= void cekKoneksiInternet() { unsigned long nowMillis = millis(); if ( WiFi.status() != WL_CONNECTED ) { if ( nowMillis - lastWiFiStatusPrint >= 5000 ) { lastWiFiStatusPrint = nowMillis; Serial.println( "[WiFi] Terputus, mencoba reconnect..." ); } if ( nowMillis - lastReconnectAttempt >= intervalReconnect ) { lastReconnectAttempt = nowMillis; WiFi.disconnect(false); WiFi.reconnect(); } return; } if (!firebaseInitialized) { initFirebase(); } if ( !dataTersinkron && Firebase.ready() ) { fetchInitialData(); } } // ================= SETUP ================= void setup() { Serial.begin(115200); Wire.begin( pinSDA, pinSCL ); if (!rtc.begin()) { Serial.println( "[RTC] Tidak terdeteksi!" ); while (1); } pinMode(pinIN1, OUTPUT); pinMode(pinIN2, OUTPUT); pinMode(pinIR1, INPUT); pinMode(pinIR2, INPUT); pinMode(pinIR3, INPUT); digitalWrite(pinIN1, LOW); digitalWrite(pinIN2, LOW); ledcSetup( 0, 30000, 8 ); ledcAttachPin( pinENA, 0 ); ledcWrite( 0, 0 ); WiFi.mode(WIFI_STA); WiFiManager wm; wm.setConnectTimeout(20); wm.setConfigPortalTimeout(180); bool wifiConnected = wm.autoConnect( "ESP32_Telurku" ); if (wifiConnected) { Serial.println( "[WiFi] Terhubung" ); sinkronisasiWaktuNTP(); initFirebase(); fetchInitialData(); } else { Serial.println( "[WiFi] Gagal, sistem berjalan offline" ); } } // ================= LOOP ================= void loop() { unsigned long currentMillis = millis(); cekKoneksiInternet(); // ================= TASK 1 SENSOR ================= if ( currentMillis - lastTaskSensor >= intervalSensor ) { lastTaskSensor = currentMillis; int s1 = digitalRead(pinIR1); int s2 = digitalRead(pinIR2); int s3 = digitalRead(pinIR3); //IR1 if ( lastStatusIR1 == HIGH && s1 == LOW ) { jumlahTelur1++; Serial.printf( "[IR1] Total: %d\n", jumlahTelur1 ); } //IR2 if ( lastStatusIR2 == HIGH && s2 == LOW ) { jumlahTelur2++; Serial.printf( "[IR2] Total: %d\n", jumlahTelur2 ); } //IR3 saat panen aktif if ( modePanenAktif && lastStatusIR3 == HIGH && s3 == LOW ) { jumlahTelurMasuk++; // Catat waktu telur terakhir masuk waktuTelurTerakhirMasuk = millis(); Serial.printf( "[IR3] %d/%d\n", jumlahTelurMasuk, targetPanen ); } lastStatusIR1 = s1; lastStatusIR2 = s2; lastStatusIR3 = s3; } // ================= CEK TARGET PANEN ================= if ( motorBerjalan && modePanenAktif && jumlahTelurMasuk >= targetPanen && targetPanen > 0 && waktuTargetTercapai == 0 ) { Serial.printf( "[PANEN] Target tercapai %d/%d\n", jumlahTelurMasuk, targetPanen ); // Catat waktu target tercapai HANYA SEKALI waktuTargetTercapai = millis(); } // ================= TASK 2 UPDATE FIREBASE ================= if ( currentMillis - lastTaskFirebase >= intervalFirebase ) { lastTaskFirebase = currentMillis; if (Firebase.ready()) { // RTC hanya dibaca saat dibutuhkan (task ini & task jadwal), // bukan setiap iterasi loop, supaya I2C tidak dibebani terus-menerus. DateTime now = rtc.now(); FirebaseJson update; update.set( "infra1", jumlahTelur1 ); update.set( "infra2", jumlahTelur2 ); update.set( "infra1_checkpoint_panen", ir1CheckpointPanen ); update.set( "infra2_checkpoint_panen", ir2CheckpointPanen ); update.set( "infra3", jumlahTelurMasuk ); update.set( "target_panen", targetPanen ); update.set( "total_harus_kebak", totalTelurHarusKeBak ); update.set( "sudah_dipanen", totalTelurSudahDipanen ); update.set( "sisa_panen", targetPanen - jumlahTelurMasuk ); update.set( "rtc_unix", (long long) now.unixtime() ); update.set( "reset_counter_pending", resetCounterPending ? 1 : 0 ); update.set( "last_update", now.timestamp() ); if ( Firebase.updateNode( fbdo, "/data", update ) ) { Serial.println( "[Firebase] Data berhasil dikirim" ); } else { Serial.print( "[Firebase] Gagal update: " ); Serial.println( fbdo.errorReason() ); } } else { Serial.println( "[Firebase] Offline" ); } } // ================= TASK 3 RESET WIFI ================= if ( currentMillis - lastTaskResetWiFi >= intervalResetWiFi ) { lastTaskResetWiFi = currentMillis; // RESET WIFI if ( Firebase.ready() && Firebase.getBool( fbdo, "/kontrol/reset_wifi" ) ) { if (fbdo.boolData()) { Firebase.setBool( fbdo, "/kontrol/reset_wifi", false ); delay(500); WiFiManager wm; wm.resetSettings(); Serial.println( "[WiFi] Reset konfigurasi" ); ESP.restart(); } } // RESET COUNTER if ( Firebase.ready() && Firebase.getBool( fbdo, "/kontrol/reset_counter" ) ) { if (fbdo.boolData()) { // Set flag: reset akan dilakukan setelah panen selesai // Ini memastikan panen sore bisa berjalan dengan checkpoint yang benar resetCounterPending = true; Firebase.setBool( fbdo, "/kontrol/reset_counter", false ); Serial.println( "[RESET] Menunggu panen selesai untuk melakukan reset counter..." ); } } } // ================= TASK 4 PENJADWALAN ================= if ( !motorBerjalan && currentMillis - lastTaskJadwal >= intervalJadwal ) { lastTaskJadwal = currentMillis; if ( Firebase.ready() && Firebase.getJSON( fbdo, "/kontrol/penjadwalan" ) ) { DateTime now = rtc.now(); char jamSekarang[6]; sprintf( jamSekarang, "%02d:%02d", now.hour(), now.minute() ); FirebaseJson &json = fbdo.jsonObject(); size_t len = json.iteratorBegin(); String key; String value; int type; for ( size_t i = 0; i < len; i++ ) { json.iteratorGet( i, type, key, value ); if ( type == FirebaseJson::JSON_OBJECT ) { FirebaseJsonData d; bool aktif = false; String jamJadwal = ""; String durStr = ""; // ===== PATH TANPA OPERATOR + ===== String pathAktif = key; pathAktif.concat("/aktif"); String pathJam = key; pathJam.concat("/jam"); String pathDurasi = key; pathDurasi.concat("/durasi"); // =============================== if (json.get(d, pathAktif.c_str())) aktif = d.boolValue; if (json.get(d, pathJam.c_str())) jamJadwal = d.stringValue; if (json.get(d, pathDurasi.c_str())) durStr = d.stringValue; if (aktif && jamJadwal == String(jamSekarang)) { // proses selanjutnya } { durStr.trim(); durStr.toLowerCase(); if (durStr.indexOf("detik") != -1) { durasiTargetDetik = durStr.substring(0, durStr.indexOf("detik")).toInt(); } else if (durStr.indexOf("menit") != -1) { durasiTargetDetik = durStr.substring(0, durStr.indexOf("menit")).toFloat() * 60; } else { // Jika hanya angka, dianggap menit (agar kompatibel dengan data lama) durasiTargetDetik = durStr.toFloat() * 60; } if (durasiTargetDetik <= 0) { durasiTargetDetik = 60; } Serial.printf( "[JADWAL] Durasi Firebase: %s -> %d detik\n", durStr.c_str(), durasiTargetDetik ); Serial.printf( "[JADWAL] Motor ON %d detik\n", durasiTargetDetik ); motorON(); break; } } } json.iteratorEnd(); } } // ================= TASK 5 MONITOR MOTOR ================= if (motorBerjalan) { unsigned long waktuBerjalan = millis() - waktuMulaiMotor; bool durasiHabis = waktuBerjalan >= durasiMotorAkhir; bool failsafeHabis = millis() - waktuMulaiFailsafe >= timeoutFailsafe; // ===== KONDISI 1: Target sudah tercapai ===== // Motor OFF 10 detik setelah target tercapai (bukan mengikuti durasi) if (waktuTargetTercapai != 0) { unsigned long waktuSetelahTarget = millis() - waktuTargetTercapai; if (waktuSetelahTarget >= timeoutMotorAfterTarget) { Serial.printf( "[PANEN] Semua telur masuk bak (%d/%d)\n", jumlahTelurMasuk, targetPanen ); totalTelurSudahDipanen += jumlahTelurMasuk; // Update checkpoint panen saat panen berhasil // Ini akan dipakai untuk panen berikutnya (panen sore) ir1CheckpointPanen = jumlahTelur1; ir2CheckpointPanen = jumlahTelur2; Serial.printf( "[PANEN] Checkpoint updated: IR1=%d, IR2=%d (siap untuk panen berikutnya)\n", ir1CheckpointPanen, ir2CheckpointPanen ); modePanenAktif = false; waktuTargetTercapai = 0; motorOFF(); // ===== RESET COUNTER JIKA PENDING ===== // Reset dilakukan EVENT-BASED setelah panen selesai, bukan time-based if (resetCounterPending) { jumlahTelur1 = 0; jumlahTelur2 = 0; ir1CheckpointPanen = 0; ir2CheckpointPanen = 0; jumlahTelurMasuk = 0; totalTelurSudahDipanen = 0; targetPanen = 0; durasiSudahDitambahkan = false; durasiMotorAkhir = 0; waktuTelurTerakhirMasuk = 0; motorSedangPushSisa = false; resetCounterPending = false; Serial.println( "[RESET] Counter berhasil direset setelah panen selesai" ); } } else { // Hanya print countdown setiap detik (saat countdown berubah) static unsigned long lastCountdownPrint = 0; unsigned long currentCountdown = (timeoutMotorAfterTarget - waktuSetelahTarget) / 1000; if (lastCountdownPrint != currentCountdown) { lastCountdownPrint = currentCountdown; Serial.printf( "[PANEN] Menunggu OFF: %lu detik lagi\n", currentCountdown ); } } } // ===== KONDISI 2: Durasi habis tapi target belum tercapai ===== // Ada telur tertinggal di konveyor, perpanjang durasi untuk push else if ( durasiHabis && jumlahTelurMasuk < targetPanen ) { // Cek apakah ada telur yang masuk dalam durasi // Jika tidak ada telur sejak durasi habis dan belum pernah ditambah if ( !durasiSudahDitambahkan && millis() - waktuTelurTerakhirMasuk >= durasiMotorAkhir ) { // Perpanjang durasi motor sebesar 2x dari durasi awal unsigned long durasiTambahan = (unsigned long)durasiTargetDetik * 1000 * 2; durasiMotorAkhir += durasiTambahan; durasiSudahDitambahkan = true; motorSedangPushSisa = true; Serial.printf( "[PANEN] Ada telur tertinggal! Durasi perpanjang 2x: %lu detik\n", durasiMotorAkhir / 1000 ); Serial.printf( "[PANEN] Menunggu telur masuk: %d/%d\n", jumlahTelurMasuk, targetPanen ); } // Motor tetap jalan, menunggu sisa telur masuk bak // atau sampai failsafe 5 menit tercapai else { Serial.printf( "[PANEN] Menunggu telur masuk: %d/%d\n", jumlahTelurMasuk, targetPanen ); } } // ===== KONDISI 3: Failsafe 5 menit ===== // Motor OFF paksa jika timeout 5 menit if (failsafeHabis) { Serial.println( "[FAILSAFE] Timeout 5 menit" ); totalTelurSudahDipanen += jumlahTelurMasuk; // Update checkpoint panen saat failsafe ir1CheckpointPanen = jumlahTelur1; ir2CheckpointPanen = jumlahTelur2; Serial.printf( "[FAILSAFE] Checkpoint updated: IR1=%d, IR2=%d\n", ir1CheckpointPanen, ir2CheckpointPanen ); modePanenAktif = false; waktuTargetTercapai = 0; motorOFF(); // ===== RESET COUNTER JIKA PENDING (FAILSAFE) ===== // Jika failsafe terjadi dan reset pending, jalankan reset if (resetCounterPending) { jumlahTelur1 = 0; jumlahTelur2 = 0; ir1CheckpointPanen = 0; ir2CheckpointPanen = 0; jumlahTelurMasuk = 0; totalTelurSudahDipanen = 0; targetPanen = 0; durasiSudahDitambahkan = false; durasiMotorAkhir = 0; waktuTelurTerakhirMasuk = 0; motorSedangPushSisa = false; resetCounterPending = false; Serial.println( "[RESET] Counter direset setelah failsafe" ); } } } }