TKK_E32231105/main.cpp

1003 lines
26 KiB
C++

#include <Arduino.h>
#include <WiFi.h>
#include <FirebaseESP32.h>
#include <WiFiManager.h>
#include <Wire.h>
#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"
);
}
}
}
}