614 lines
14 KiB
C++
614 lines
14 KiB
C++
/***************** KONFIGURASI BLYNK *****************/
|
|
#define BLYNK_TEMPLATE_ID "TMPL6IJqYTM5v"
|
|
#define BLYNK_TEMPLATE_NAME "IOT Hamster"
|
|
#define BLYNK_AUTH_TOKEN "cyVp3mgGDuB3pu0sGBoJlIGYKao-HKS1"
|
|
|
|
/***************** LIBRARY *****************/
|
|
#include <WiFi.h>
|
|
#include <WiFiClient.h>
|
|
#include <WiFiClientSecure.h>
|
|
#include <BlynkSimpleEsp32.h>
|
|
#include "esp_system.h"
|
|
#include "DHT.h"
|
|
#include <Wire.h>
|
|
#include <RTClib.h>
|
|
#include <ESP32Servo.h>
|
|
|
|
/***************** PIN SENSOR *****************/
|
|
#define DHTPIN 4
|
|
#define DHTTYPE DHT22
|
|
#define DHT_POWER 16
|
|
#define RELAY_KIPAS 27
|
|
#define RELAY_PUMP 26
|
|
#define SERVO_PIN 14
|
|
#define WATER_SENSOR_PIN 35
|
|
|
|
DHT dht(DHTPIN, DHTTYPE);
|
|
RTC_DS3231 rtc;
|
|
Servo servo;
|
|
|
|
/***************** VARIABEL *****************/
|
|
int lastDay = -1;
|
|
|
|
bool manualKipas = false;
|
|
bool manualPump = false;
|
|
float lastSuhu = NAN;
|
|
|
|
bool feedingDoneToday = false;
|
|
bool servoAutoAktif = false;
|
|
unsigned long servoAutoMulai = 0;
|
|
|
|
const int JAM_FEEDING = 18;
|
|
const int MENIT_FEEDING = 0;
|
|
const unsigned long DURASI_BUKA_SERVO = 300;
|
|
|
|
unsigned long lastFeedingTime = 0;
|
|
const unsigned long INTERVAL_FEEDING = 60000; // 1 menit (60000 ms)
|
|
|
|
/* ===== PERBAIKAN DHT ===== */
|
|
bool pumpAktif = false;
|
|
unsigned long waktuPompaBerubah = 0;
|
|
bool dhtValid = false;
|
|
bool dhtEnabled = true;
|
|
unsigned long lastPrint = 0;
|
|
/* ======================== */
|
|
|
|
/* ===== AUTO RESET DHT NON-BLOCKING (MAX 3 DETIK) ===== */
|
|
unsigned long dhtErrorSejak = 0;
|
|
unsigned long dhtResetMulai = 0;
|
|
bool dhtSedangReset = false;
|
|
|
|
const unsigned long DHT_TIMEOUT = 3000; //
|
|
const unsigned long DHT_RESET_OFF = 500; //
|
|
/* ==================================================== */
|
|
|
|
/* ===== TAMBAHAN AUTO KIPAS ===== */
|
|
unsigned long waktuKipasDimatikanManual = 0;
|
|
bool tungguAutoKipas = false;
|
|
/* ============================== */
|
|
|
|
/* ===== TAMBAHAN AUTO OFF POMPA ===== */
|
|
const unsigned long POMPA_AUTO_OFF = 10000;
|
|
/* ================================= */
|
|
|
|
/* ===== VARIABEL WATER SENSOR ===== */
|
|
int waterValue = 0;
|
|
int waterPercent = 0; // <-- TAMBAHAN
|
|
float waterFiltered = 0;
|
|
bool adaAir = false;
|
|
#define WATER_EMPTY 100 //air habis
|
|
#define WATER_FULL 950 //air penuh
|
|
|
|
bool pompaOtomatisAktif = false;
|
|
|
|
/* NAMA DAN PASSWORD WIFI */
|
|
|
|
char ssid[] = "OPPO Reno5 F";
|
|
char pass[] = "87654321";
|
|
|
|
WiFiClientSecure client;
|
|
BlynkTimer timer;
|
|
|
|
/***************** STATUS SERVO KE BLYNK (V5) *****************/
|
|
void updateServoStatus(int posisi) {
|
|
if (posisi == 0) {
|
|
Blynk.virtualWrite(V5, "Tertutup");
|
|
} else if (posisi == 90) {
|
|
Blynk.virtualWrite(V5, "Terbuka");
|
|
} else {
|
|
Blynk.virtualWrite(V5, "Bergerak");
|
|
}
|
|
}
|
|
|
|
/************** CONTROL MANUAL DARI BLYNK (Kipas) **************/
|
|
BLYNK_WRITE(V1) {
|
|
int state = param.asInt();
|
|
manualKipas = true;
|
|
digitalWrite(RELAY_KIPAS, state ? LOW : HIGH);
|
|
|
|
if (state == 0) {
|
|
waktuKipasDimatikanManual = millis();
|
|
tungguAutoKipas = true;
|
|
} else {
|
|
tungguAutoKipas = false;
|
|
}
|
|
}
|
|
|
|
/************** CONTROL MANUAL DARI BLYNK (Pompa) **************/
|
|
BLYNK_WRITE(V2) {
|
|
int state = param.asInt();
|
|
|
|
if (waterValue >= WATER_FULL && state == 1) {
|
|
Blynk.virtualWrite(V2, 0);
|
|
return;
|
|
}
|
|
|
|
manualPump = true;
|
|
pompaOtomatisAktif = false;
|
|
dhtEnabled = false;
|
|
|
|
digitalWrite(RELAY_PUMP, state ? LOW : HIGH);
|
|
pumpAktif = state;
|
|
dhtEnabled = false;
|
|
waktuPompaBerubah = millis();
|
|
|
|
if (state == 1) {
|
|
dhtEnabled = false;
|
|
|
|
String pesan = "💧 POMPA MANUAL MENYALA\n";
|
|
pesan += "Status: Minum Dibuka\n";
|
|
pesan += "Level Air: " + String(waterValue);
|
|
|
|
kirimTelegram(pesan);
|
|
|
|
} else {
|
|
dhtEnabled = true;
|
|
|
|
String pesan = "💧 POMPA MANUAL DIMATIKAN\n";
|
|
pesan += "Status: Minum Ditutup\n";
|
|
pesan += "Level Air: " + String(waterValue);
|
|
|
|
kirimTelegram(pesan);
|
|
}
|
|
}
|
|
|
|
/************** CONTROL MANUAL DARI BLYNK (SERVO) **************/
|
|
BLYNK_WRITE(V8) {
|
|
|
|
if (param.asInt() == 1) {
|
|
|
|
servo.write(90);
|
|
updateServoStatus(90);
|
|
|
|
kirimLogFeeding("Manual Feeding Dimulai");
|
|
|
|
delay(1000);
|
|
|
|
servo.write(0);
|
|
updateServoStatus(0);
|
|
|
|
kirimLogFeeding("Manual Feeding Selesai");
|
|
|
|
Blynk.virtualWrite(V8, 0);
|
|
}
|
|
}
|
|
|
|
/***************** KIRIM DATA KE BLYNK *****************/
|
|
void sendToBlynk() {
|
|
|
|
DateTime now = rtc.now();
|
|
int jam = now.hour();
|
|
int menit = now.minute();
|
|
|
|
Blynk.virtualWrite(V3, jam);
|
|
Blynk.virtualWrite(V4, menit);
|
|
|
|
char waktu[10];
|
|
sprintf(waktu, "%02d:%02d", jam, menit);
|
|
Blynk.virtualWrite(V7, waktu);
|
|
|
|
if (dhtValid) {
|
|
Blynk.virtualWrite(V10, 1);
|
|
Blynk.virtualWrite(V11, "DHT BERFUNGSI");
|
|
} else {
|
|
Blynk.virtualWrite(V10, 0);
|
|
Blynk.virtualWrite(V11, "DHT ERROR");
|
|
}
|
|
}
|
|
|
|
/***************** SETUP *****************/
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
|
|
pinMode(DHT_POWER, OUTPUT);
|
|
digitalWrite(DHT_POWER, HIGH);
|
|
delay(2000);
|
|
|
|
pinMode(RELAY_KIPAS, OUTPUT);
|
|
pinMode(RELAY_PUMP, OUTPUT);
|
|
digitalWrite(RELAY_KIPAS, HIGH);
|
|
digitalWrite(RELAY_PUMP, HIGH);
|
|
|
|
pinMode(WATER_SENSOR_PIN, INPUT); // === TAMBAHAN ===
|
|
|
|
dht.begin();
|
|
rtc.begin();
|
|
// rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
|
|
|
|
servo.attach(SERVO_PIN, 500, 2400);
|
|
servo.write(0);
|
|
updateServoStatus(0);
|
|
|
|
Serial.println("\n📶 WiFi Terhubung");
|
|
Blynk.begin(BLYNK_AUTH_TOKEN, ssid, pass);
|
|
|
|
timer.setInterval(2000L, sendToBlynk);
|
|
timer.setInterval(3000L, bacaDHT);
|
|
timer.setInterval(500L, bacaWaterSensor); // === TAMBAHAN ===
|
|
// timer.setInterval(300000L, printSuhuSerial);
|
|
|
|
Serial.println("Sistem IoT + Blynk berjalan...");
|
|
//Pompa set adc
|
|
analogSetPinAttenuation(WATER_SENSOR_PIN, ADC_11db);
|
|
analogReadResolution(12);
|
|
timer.setInterval(30000L, kirimSuhuTelegram); // tiap 10 detik
|
|
|
|
/***************** WAKTU *****************/
|
|
configTime(7 * 3600, 0, "pool.ntp.org"); // WIB = GMT+7
|
|
|
|
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
|
|
));
|
|
}
|
|
}
|
|
|
|
/************** TELEGRAM CONFIG **************/
|
|
const char* botToken = "8710332774:AAHdWZwsIB2KbkkuvWBnFwWQgeQ15Io_L_c";
|
|
const char* chatID = "5535845901";
|
|
|
|
void kirimTelegram(String pesan) {
|
|
|
|
client.setInsecure(); // bypass SSL
|
|
|
|
if (!client.connect("api.telegram.org", 443)) {
|
|
Serial.println("Gagal koneksi ke Telegram");
|
|
return;
|
|
}
|
|
|
|
String url = "/bot" + String(botToken) + "/sendMessage?chat_id=" + String(chatID) + "&text=" + pesan;
|
|
|
|
client.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: api.telegram.org\r\n" + "Connection: close\r\n\r\n");
|
|
|
|
delay(500);
|
|
while (client.available()) {
|
|
client.read();
|
|
}
|
|
|
|
client.stop();
|
|
}
|
|
|
|
/***************** LOOP *****************/
|
|
void loop() {
|
|
Blynk.run();
|
|
timer.run();
|
|
|
|
autoFeedingRTC();
|
|
|
|
|
|
if (dhtValid) {
|
|
if (!manualKipas) {
|
|
if (lastSuhu > 30) {
|
|
digitalWrite(RELAY_KIPAS, LOW);
|
|
Blynk.virtualWrite(V1, 1);
|
|
} else {
|
|
digitalWrite(RELAY_KIPAS, HIGH);
|
|
Blynk.virtualWrite(V1, 0);
|
|
}
|
|
}
|
|
} else {
|
|
digitalWrite(RELAY_KIPAS, HIGH);
|
|
}
|
|
|
|
if (tungguAutoKipas && manualKipas && dhtValid) {
|
|
if (lastSuhu > 30 && millis() - waktuKipasDimatikanManual >= 5000) {
|
|
digitalWrite(RELAY_KIPAS, LOW);
|
|
Blynk.virtualWrite(V1, 1);
|
|
tungguAutoKipas = false;
|
|
Serial.println("🔥 Kipas otomatis menyala kembali");
|
|
}
|
|
}
|
|
|
|
if (manualKipas && millis() - waktuKipasDimatikanManual > 10000) {
|
|
manualKipas = false;
|
|
}
|
|
|
|
if (millis() - lastPrint >= 300000) {
|
|
|
|
Serial.print("🌡 Suhu: ");
|
|
Serial.print(lastSuhu);
|
|
Serial.println(" °C");
|
|
|
|
lastPrint = millis();
|
|
}
|
|
|
|
// ===== AUTO RESET DHT NON-BLOCKING =====
|
|
if (!dhtValid) {
|
|
|
|
// mulai hitung error
|
|
if (dhtErrorSejak == 0 && !dhtSedangReset) {
|
|
dhtErrorSejak = millis();
|
|
}
|
|
|
|
// jika error >= 3 detik → mulai reset
|
|
if (!dhtSedangReset && millis() - dhtErrorSejak >= DHT_TIMEOUT) {
|
|
// Serial.println("⚠️ DHT error > 3 detik → reset dimulai");
|
|
digitalWrite(DHT_POWER, LOW); // matikan DHT
|
|
dhtResetMulai = millis();
|
|
dhtSedangReset = true;
|
|
}
|
|
|
|
if (dhtSedangReset && millis() - dhtResetMulai >= DHT_RESET_OFF) {
|
|
digitalWrite(DHT_POWER, HIGH);
|
|
|
|
// ===== RELEASE SYSTEM LOCK =====
|
|
dhtSedangReset = false;
|
|
dhtErrorSejak = 0;
|
|
dhtEnabled = true; // 🔓 izinkan baca DHT lagi
|
|
pumpAktif = false; // 🔓 reset status pompa
|
|
Blynk.virtualWrite(V2, 0);
|
|
|
|
Serial.println("🔄 DHT reset selesai, sistem dilepas");
|
|
}
|
|
|
|
} else {
|
|
// jika DHT normal → reset semua flag
|
|
dhtErrorSejak = 0;
|
|
dhtSedangReset = false;
|
|
}
|
|
// ======================================
|
|
|
|
// ===== AUTO WATER LEVEL =====
|
|
|
|
// Jika air habis → aktifkan mode otomatis
|
|
if (!adaAir && !manualPump) {
|
|
if (!pumpAktif) {
|
|
pompaOtomatisAktif = true;
|
|
}
|
|
}
|
|
|
|
// Jika tangki penuh → pompa harus mati
|
|
if (waterPercent >= 80) {
|
|
digitalWrite(RELAY_PUMP, HIGH);
|
|
pumpAktif = false;
|
|
pompaOtomatisAktif = false;
|
|
manualPump = false;
|
|
Blynk.virtualWrite(V2, 0);
|
|
String pesan = "✅ POMPA OTOMATIS BERHENTI\n";
|
|
pesan += "Alasan: Tangki Penuh\n";
|
|
pesan += "Level Air: " + String(waterValue);
|
|
kirimTelegram(pesan);
|
|
}
|
|
|
|
|
|
// Jika mode otomatis aktif dan belum penuh → pompa nyala
|
|
if (!manualPump && pompaOtomatisAktif && waterValue < WATER_FULL) {
|
|
if (!pumpAktif) {
|
|
digitalWrite(RELAY_PUMP, LOW);
|
|
pumpAktif = true;
|
|
waktuPompaBerubah = millis();
|
|
Blynk.virtualWrite(V2, 1);
|
|
String pesan = "🚰 POMPA OTOMATIS MENYALA\n";
|
|
pesan += "Alasan: Air Habis\n";
|
|
pesan += "Level Air: " + String(waterValue);
|
|
kirimTelegram(pesan);
|
|
}
|
|
}
|
|
|
|
// ===== AUTO OFF POMPA + RESET MANUAL =====
|
|
if (pumpAktif && !pompaOtomatisAktif && millis() - waktuPompaBerubah >= POMPA_AUTO_OFF) {
|
|
|
|
digitalWrite(RELAY_PUMP, HIGH);
|
|
pumpAktif = false;
|
|
Blynk.virtualWrite(V2, 0);
|
|
|
|
String pesan = "⏱ POMPA DIMATIKAN OTOMATIS\n";
|
|
pesan += "Alasan: Timeout 10 detik\n";
|
|
pesan += "Level Air: " + String(waterValue);
|
|
kirimTelegram(pesan);
|
|
}
|
|
|
|
// reset manual setelah 10 detik
|
|
if (manualPump && millis() - waktuPompaBerubah > 10000) {
|
|
manualPump = false;
|
|
}
|
|
|
|
// ===========================
|
|
}
|
|
|
|
/***************** FUNGSI BACA DHT *****************/
|
|
void bacaDHT() {
|
|
|
|
float suhuTotal = 0;
|
|
int berhasil = 0;
|
|
|
|
for (int i = 0; i < 5; i++) {
|
|
|
|
float t = dht.readTemperature();
|
|
|
|
if (!isnan(t)) {
|
|
suhuTotal += t;
|
|
berhasil++;
|
|
}
|
|
|
|
vTaskDelay(250 / portTICK_PERIOD_MS);
|
|
}
|
|
|
|
if (berhasil > 0) {
|
|
|
|
lastSuhu = suhuTotal / berhasil;
|
|
dhtValid = true;
|
|
|
|
Blynk.virtualWrite(V0, lastSuhu);
|
|
|
|
} else {
|
|
|
|
dhtValid = false;
|
|
|
|
Serial.println("❌ Gagal membaca DHT22");
|
|
}
|
|
}
|
|
/***************** FUNGSI WATER SENSOR (TAMBAHAN) *****************/
|
|
void bacaWaterSensor() {
|
|
|
|
// ===== AMBIL BEBERAPA SAMPLE =====
|
|
long totalADC = 0;
|
|
|
|
for (int i = 0; i < 10; i++) {
|
|
totalADC += analogRead(WATER_SENSOR_PIN);
|
|
vTaskDelay(2 / portTICK_PERIOD_MS);
|
|
}
|
|
|
|
// ===== RATA-RATA ADC =====
|
|
int rawValue = totalADC / 10;
|
|
|
|
// ===== FILTER SMOOTHING =====
|
|
// lebih stabil dan tidak loncat-loncat
|
|
waterFiltered = (0.8 * waterFiltered) + (0.2 * rawValue);
|
|
|
|
waterValue = (int)waterFiltered;
|
|
|
|
// ===== KALIBRASI SENSOR =====
|
|
// sesuaikan hasil Serial Monitor
|
|
int adcKosong = 250;
|
|
int adcPenuh = 950;
|
|
|
|
waterPercent = map(waterValue, adcKosong, adcPenuh, 0, 100);
|
|
waterPercent = constrain(waterPercent, 0, 100);
|
|
|
|
// ===== HYSTERESIS =====
|
|
// supaya status tidak berubah-ubah
|
|
|
|
static bool statusAir = false;
|
|
|
|
// baru dianggap ADA AIR jika >5%
|
|
if (waterPercent >= 5) {
|
|
statusAir = true;
|
|
}
|
|
|
|
// baru dianggap KOSONG jika <5%
|
|
if (waterPercent <= 5) {
|
|
statusAir = false;
|
|
}
|
|
|
|
adaAir = statusAir;
|
|
|
|
// ===== STATUS FULL =====
|
|
static bool statusFull = false;
|
|
|
|
if (waterPercent >= 100) {
|
|
statusFull = true;
|
|
}
|
|
|
|
if (waterPercent <= 90) {
|
|
statusFull = false;
|
|
}
|
|
|
|
// ===== KIRIM KE BLYNK =====
|
|
static unsigned long lastBlynk = 0;
|
|
|
|
if (millis() - lastBlynk >= 500) {
|
|
|
|
Blynk.virtualWrite(V12, waterPercent);
|
|
|
|
if (statusFull) {
|
|
Blynk.virtualWrite(V13, "FULL");
|
|
}
|
|
else if (adaAir) {
|
|
Blynk.virtualWrite(V13, "AIR TERSEDIA");
|
|
}
|
|
else {
|
|
Blynk.virtualWrite(V13, "KOSONG");
|
|
}
|
|
|
|
lastBlynk = millis();
|
|
}
|
|
|
|
// ===== SERIAL MONITOR =====
|
|
static unsigned long lastSerial = 0;
|
|
|
|
if (millis() - lastSerial >= 500) {
|
|
|
|
// Serial.print("RAW ADC : ");
|
|
// Serial.print(rawValue);
|
|
|
|
// Serial.print(" | FILTER : ");
|
|
// Serial.print(waterValue);
|
|
|
|
// Serial.print(" | LEVEL : ");
|
|
// Serial.print(waterPercent);
|
|
|
|
// Serial.print("% | STATUS : ");
|
|
|
|
if (adaAir) {
|
|
// Serial.println("ADA AIR");
|
|
} else {
|
|
// Serial.println("KOSONG");
|
|
}
|
|
|
|
lastSerial = millis();
|
|
}
|
|
}
|
|
|
|
/***************** LOG Feeding *****************/
|
|
void kirimLogFeeding(String status) {
|
|
|
|
DateTime now = rtc.now();
|
|
|
|
char waktu[20];
|
|
sprintf(waktu, "%02d:%02d:%02d",
|
|
now.hour(),
|
|
now.minute(),
|
|
now.second());
|
|
|
|
String logFeeding = String(waktu) + " - " + status;
|
|
|
|
Blynk.virtualWrite(V14, logFeeding);
|
|
}
|
|
|
|
|
|
/***************** AUTO FEEDING *****************/
|
|
void autoFeedingRTC() {
|
|
|
|
// === MULAI FEEDING SETIAP 1 MENIT ===
|
|
if (!servoAutoAktif && millis() - lastFeedingTime >= INTERVAL_FEEDING) {
|
|
|
|
servo.write(80);
|
|
updateServoStatus(80);
|
|
servoAutoMulai = millis();
|
|
servoAutoAktif = true;
|
|
lastFeedingTime = millis();
|
|
|
|
String pesan = "🐹 AUTO FEEDING DIMULAI\n";
|
|
pesan += "Interval: 1 Menit\n";
|
|
pesan += "Status: Pakan Dibuka\n";
|
|
|
|
kirimTelegram(pesan);
|
|
// Blynk.virtualWrite(V9, "Auto Feeding Dimulai");
|
|
kirimLogFeeding("Auto Feeding Dimulai");
|
|
}
|
|
|
|
// === TUTUP SETELAH DURASI ===
|
|
if (servoAutoAktif && millis() - servoAutoMulai >= DURASI_BUKA_SERVO) {
|
|
|
|
servo.write(0);
|
|
updateServoStatus(0);
|
|
servoAutoAktif = false;
|
|
|
|
String pesan = "🐹 AUTO FEEDING SELESAI\n";
|
|
pesan += "Status: Pakan Ditutup\n";
|
|
|
|
kirimTelegram(pesan);
|
|
// Blynk.virtualWrite(V9, "Auto Feeding Selesai");
|
|
kirimLogFeeding("Auto Feeding Selesai");
|
|
}
|
|
}
|
|
void kirimSuhuTelegram() {
|
|
if (dhtValid) {
|
|
String pesan = "🌡 Update Suhu:\n";
|
|
pesan += "Suhu saat ini: ";
|
|
pesan += String(lastSuhu);
|
|
pesan += " °C";
|
|
|
|
kirimTelegram(pesan);
|
|
}
|
|
}
|
|
|
|
|