From f683b5eb50c66f4600fdc2a8e7055a8d4fb5fe3a Mon Sep 17 00:00:00 2001 From: Kharisma Date: Thu, 2 Jul 2026 12:39:11 +0700 Subject: [PATCH] Upload files to "/" Kode alat IoT untuk pemanenan telur ayam otomatis menggunakan Platform.io --- main.cpp | 1003 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1003 insertions(+) create mode 100644 main.cpp diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..3868611 --- /dev/null +++ b/main.cpp @@ -0,0 +1,1003 @@ +#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" + ); + } + } + } +} \ No newline at end of file