HydroNutrify_Ari_e41221567/Full code esp32.py

416 lines
17 KiB
Python

import network # Library untuk koneksi Wi-Fi
import urequests # Library untuk HTTP request (API Firebase/Firestore)
import time # Library untuk jeda (delay) dan pengambilan waktu
import machine # Library untuk kontrol pin hardware ESP32
import ntptime # Library untuk sinkronisasi waktu dari server internet
# ==================================================
# 1. KONFIGURASI UTAMA
# ==================================================
# Kredensial Jaringan Wi-Fi
WIFI_SSID = "WARKOP"
WIFI_PASSWORD = "redi1234"
# Kredensial Firebase & Firestore
API_KEY = "AIzaSyCvv9MrSLOQPlarND0iq8vjH3nUANHcKMI"
PROJECT_ID = "hydronutrify"
DOC_ID_FASE = "kqAxWYNRWqZtfqihpo31b5C0vNv1" # ID Dokumen untuk siklus tanam
DOC_ID_DATA = "latest_monitoring" # ID Dokumen untuk data real-time
# Autentikasi Pengguna Firebase
USER_EMAIL = "oktagendeveloper@gmail.com"
USER_PASSWORD = "Mahasiswa22"
# Variabel Global untuk menyimpan token sesi (diisi otomatis saat login)
auth_token = ""
my_user_id = ""
# Pengaturan Waktu & Aktuator (Pompa)
PUMP_DURATION = 10 # Lama pompa menyala (10 detik)
DELAY_AFTER_PUMP = 30 # Jeda tunggu agar larutan nutrisi tercampur rata di bak
LOOP_INTERVAL = 3600 # Jeda sistem membaca sensor kembali (3600 detik = 1 Jam)
MAX_RETRY_NTP = 5 # Maksimal percobaan sinkronisasi waktu jika gagal
# Logika Modul Relay (Active High)
RELAY_ON = 1 # Angka 1 mengirim sinyal HIGH (Pompa Menyala)
RELAY_OFF = 0 # Angka 0 mengirim sinyal LOW (Pompa Mati)
# ==================================================
# 2. PARAMETER KALIBRASI SENSOR
# ==================================================
# Didapat dari rumus regresi linear (y = mx + c) saat kalibrasi
PH_SLOPE = -17.33919
PH_INTERCEPT = 9.59919
TDS_SLOPE = 1424.404800
TDS_INTERCEPT = -736.856200
# ==================================================
# 3. INISIALISASI HARDWARE
# ==================================================
print("=== INISIALISASI HARDWARE ===")
# Inisialisasi Sensor Analog (ADC)
ph_pin = machine.ADC(machine.Pin(35))
ph_pin.atten(machine.ADC.ATTN_11DB) # Setting range pembacaan tegangan 0 - 3.3V
tds_pin = machine.ADC(machine.Pin(34))
tds_pin.atten(machine.ADC.ATTN_11DB)
# Inisialisasi Pin Relay untuk Pompa
pin_nutrisi_a = machine.Pin(13, machine.Pin.OUT)
pin_nutrisi_b = machine.Pin(12, machine.Pin.OUT)
pin_air_baku = machine.Pin(14, machine.Pin.OUT)
pin_ph_up = machine.Pin(27, machine.Pin.OUT)
pin_ph_down = machine.Pin(26, machine.Pin.OUT)
# Dikelompokkan dalam dictionary agar mudah dipanggil
pumps = {
"nutrisi": [pin_nutrisi_a, pin_nutrisi_b],
"air_baku": pin_air_baku,
"ph_up": pin_ph_up,
"ph_down": pin_ph_down
}
# Keamanan: Matikan semua relay saat alat baru dinyalakan
# Diberi jeda 0.1 detik antar pin untuk mencegah lonjakan listrik (Brownout)
for pin in [pin_nutrisi_a, pin_nutrisi_b, pin_air_baku, pin_ph_up, pin_ph_down]:
pin.value(RELAY_OFF)
time.sleep(0.1)
print("Semua relay berhasil di-OFF-kan.\n")
# Variabel untuk filter rata-rata pembacaan pH
ph_buffer = []
BUFFER_SIZE = 10
# ==================================================
# 4. HELPER WIFI, LOGIN & NTP
# ==================================================
def connect_wifi():
"""Fungsi untuk menyambungkan ESP32 ke jaringan Wi-Fi."""
wlan = network.WLAN(network.STA_IF)
wlan.active(False) # Mematikan radio Wi-Fi sesaat untuk membersihkan memori (mencegah error internal state)
time.sleep(0.5)
wlan.active(True) # Menyalakan ulang radio Wi-Fi
if not wlan.isconnected():
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
print("Connecting WiFi", end="")
timeout = 15
while not wlan.isconnected() and timeout > 0:
print(".", end="")
time.sleep(1)
timeout -= 1
if wlan.isconnected():
print("\nWiFi Connected!")
return True
return False
def login_firebase():
"""Fungsi untuk otentikasi (login) ke Firebase menggunakan Email & Password."""
global auth_token, my_user_id
url = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={API_KEY}"
payload = {"email": USER_EMAIL, "password": USER_PASSWORD, "returnSecureToken": True}
try:
res = urequests.post(url, json=payload)
data = res.json()
if res.status_code == 200:
auth_token, my_user_id = data["idToken"], data["localId"] # Menyimpan Token dan ID User
print("Login Firebase Berhasil!")
res.close()
return True
res.close()
except Exception as e:
print("Login error:", e)
return False
def sync_time():
"""Fungsi untuk mengambil waktu global (NTP) dan mengubahnya ke WIB (UTC+7)."""
print("Mencoba sinkronisasi NTP...")
for i in range(MAX_RETRY_NTP):
try:
ntptime.settime() # Ambil waktu UTC
# Konversi waktu dari detik UTC ditambah 7 jam (7 * 3600 detik)
t = time.localtime(time.time() + 7*3600)
print(f"Waktu NTP tersinkron: {t[0]:04d}-{t[1]:02d}-{t[2]:02d} {t[3]:02d}:{t[4]:02d}")
return True
except:
print(f"Gagal NTP ({i+1})... mencoba lagi.")
time.sleep(2)
return False
# ==================================================
# 5. PEMBACAAN SENSOR PENGKONDISI SINYAL
# ==================================================
def baca_tegangan(pin, samples=15):
"""Filter Median (Nilai Tengah) khusus untuk sensor pH."""
data = []
for _ in range(samples):
data.append(pin.read())
time.sleep_ms(10)
data.sort()
return data[len(data)//2] * (3.3 / 4095) # Mengubah nilai mentah ADC ke Voltase
def baca_tegangan_stabil(pin, samples=50):
"""Filter Trimmed Mean khusus untuk sensor TDS yang fluktuatif."""
data = []
for _ in range(samples):
data.append(pin.read())
time.sleep_ms(5)
data.sort()
# Membuang 10% data nilai ekstrem atas dan bawah (Noise Reduction)
potong = samples // 10
data_bersih = data[potong : samples - potong]
rata_rata = sum(data_bersih) / len(data_bersih)
return rata_rata * (3.3 / 4095)
def get_sensor_data():
"""Fungsi utama untuk membaca pH dan TDS, lalu memasukkannya ke rumus regresi."""
global ph_buffer
# === Membaca sensor pH ===
v_ph = baca_tegangan(ph_pin)
ph_raw = (PH_SLOPE * v_ph) + PH_INTERCEPT # Rumus kalibrasi
# Memasukkan nilai pH ke dalam antrean (buffer) rata-rata keliling (Moving Average)
ph_buffer.append(ph_raw)
if len(ph_buffer) > BUFFER_SIZE:
ph_buffer.pop(0)
ph_val = sum(ph_buffer) / len(ph_buffer)
if ph_val < 0 or ph_val > 14: ph_val = ph_raw # Proteksi nilai tidak masuk akal
# === Membaca sensor TDS ===
v_tds = baca_tegangan_stabil(tds_pin)
tds_val = (TDS_SLOPE * v_tds) + TDS_INTERCEPT # Rumus kalibrasi
if tds_val < 0: tds_val = 0 # TDS tidak boleh minus
return round(ph_val, 2), int(tds_val)
# ==================================================
# 6. KOMUNIKASI FIRESTORE
# ==================================================
def fetch_growth_phase():
"""Mengambil status fase pertumbuhan (Semai/Vegetatif/Generatif) dari Firestore."""
url = f"https://firestore.googleapis.com/v1/projects/{PROJECT_ID}/databases/(default)/documents/planting_cycles/{DOC_ID_FASE}"
headers = {"Authorization": f"Bearer {auth_token}"}
try:
res = urequests.get(url, headers=headers)
if res.status_code == 200:
data = res.json()
phase_str = data["fields"]["growth_phase"]["stringValue"]
res.close()
# Menerjemahkan string Firestore ke kode Logika (F1/F2/F3)
if phase_str == "Semai": return "F1"
elif phase_str == "Vegetatif": return "F2"
elif phase_str == "Generatif": return "F3"
else:
print(f"-> Gagal ambil fase dari DB (Code: {res.status_code})")
print("-> Alasan Firebase:", res.text)
res.close()
except Exception as e:
print("-> Error koneksi fetch fase:", e)
return "F1" # Jika internet putus, anggap berada di fase teraman (F1)
def upload_to_firestore(ph, tds, st):
"""Mengirim hasil bacaan sensor dan status pompa ke Firestore."""
# Validasi Tahun: Jika jam internal ESP32 menunjukkan tahun 2000 (belum sync), paksa sync
if time.localtime()[0] < 2024:
sync_time()
# Membentuk string timestamp (Format RFC 3339) dengan zona WIB (+07:00)
waktu_wib_sec = time.time() + (7 * 3600)
t = time.localtime(waktu_wib_sec)
timestamp_str = f"{t[0]:04d}-{t[1]:02d}-{t[2]:02d}T{t[3]:02d}:{t[4]:02d}:{t[5]:02d}+07:00"
# URL update API Firestore beserta updateMask agar divalidasi oleh Security Rules
url = f"https://firestore.googleapis.com/v1/projects/{PROJECT_ID}/databases/(default)/documents/hidroponik_data/{DOC_ID_DATA}?updateMask.fieldPaths=pH_value&updateMask.fieldPaths=nutrient_level&updateMask.fieldPaths=pump_nutrient_status&updateMask.fieldPaths=pump_water_status&updateMask.fieldPaths=pump_ph_up_status&updateMask.fieldPaths=pump_ph_down_status&updateMask.fieldPaths=timestamp&updateMask.fieldPaths=user_id"
headers = {"Authorization": f"Bearer {auth_token}"}
# Payload format JSON (Tipe data harus persis dengan Firestore Rules)
payload = {
"fields": {
"pH_value": {"doubleValue": float(ph)},
"nutrient_level": {"integerValue": int(tds)},
"pump_nutrient_status": {"booleanValue": st["nut"] == RELAY_ON},
"pump_water_status": {"booleanValue": st["air"] == RELAY_ON},
"pump_ph_up_status": {"booleanValue": st["pup"] == RELAY_ON},
"pump_ph_down_status": {"booleanValue": st["pdown"] == RELAY_ON},
"timestamp": {"timestampValue": timestamp_str},
"user_id": {"stringValue": my_user_id}
}
}
try:
print("Mengunggah ke Firestore...")
res = urequests.patch(url, json=payload, headers=headers)
if res.status_code == 200:
print("-> Upload Berhasil!")
else:
print(f"-> Upload Gagal (Code: {res.status_code})")
print("-> Detail Error:", res.text)
res.close()
except Exception as e:
print("-> Upload Error:", e)
# ==================================================
# 7. LOGIKA KONTROL SISTEM PAKAR (FORWARD CHAINING)
# ==================================================
def trigger_pump(pin, nama):
"""Menyalakan relay spesifik sesuai PUMP_DURATION lalu mematikannya kembali."""
print(f" ---> Mengaktifkan Pompa: {nama} ({PUMP_DURATION} Detik)")
pin.value(RELAY_ON)
time.sleep(PUMP_DURATION)
pin.value(RELAY_OFF)
print(f" ---> {nama} Selesai")
def eksekusi_pompa(st):
"""Fungsi jembatan untuk mengecek perintah logika mana yang bernilai ON."""
if st["pup"] == RELAY_ON: trigger_pump(pumps["ph_up"], "PH UP")
if st["pdown"] == RELAY_ON: trigger_pump(pumps["ph_down"], "PH DOWN")
if st["air"] == RELAY_ON: trigger_pump(pumps["air_baku"], "AIR BAKU")
if st["nut"] == RELAY_ON:
trigger_pump(pumps["nutrisi"][0], "NUTRISI A")
trigger_pump(pumps["nutrisi"][1], "NUTRISI B")
def run_forward_chaining(f_code, ph, tds):
"""
Mesin Inferensi (Forward Chaining).
Mengubah fakta pembacaan sensor menjadi aturan (Rule) keputusan status pompa.
Terdiri dari 24 Rule berdasarkan kombinasi (Fase, Status pH, Status TDS).
"""
# Fakta 1: Klasifikasi tingkat pH (P)
p_code = "P1" if ph < 4.0 else "P2" if ph > 6.0 else "P3"
# Fakta 2: Klasifikasi kebutuhan nutrisi TDS (T) bergantung pada Fase Tanam (F)
t_code = "T2"
if f_code == "F1":
if tds < 300: t_code = "T1"
elif tds > 500: t_code = "T3"
elif f_code == "F2":
if tds < 600: t_code = "T1"
elif tds > 800: t_code = "T3"
elif f_code == "F3":
if tds < 800: t_code = "T1"
elif tds > 1200: t_code = "T3"
# Default semua target keputusan adalah MATI
st = {"nut": RELAY_OFF, "air": RELAY_OFF, "pup": RELAY_OFF, "pdown": RELAY_OFF}
# Pemetaan 24 Rule
if f_code == "F1":
if p_code == "P1":
if t_code == "T1": st["pup"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pup"] = RELAY_ON
elif t_code == "T3": st["pup"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P2":
if t_code == "T1": st["pdown"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pdown"] = RELAY_ON
elif t_code == "T3": st["pdown"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P3":
if t_code == "T1": st["nut"] = RELAY_ON
elif t_code == "T3": st["air"] = RELAY_ON
elif f_code == "F2":
if p_code == "P1":
if t_code == "T1": st["pup"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pup"] = RELAY_ON
elif t_code == "T3": st["pup"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P2":
if t_code == "T1": st["pdown"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pdown"] = RELAY_ON
elif t_code == "T3": st["pdown"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P3":
if t_code == "T1": st["nut"] = RELAY_ON
elif t_code == "T3": st["air"] = RELAY_ON
elif f_code == "F3":
if p_code == "P1":
if t_code == "T1": st["pup"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pup"] = RELAY_ON
elif t_code == "T3": st["pup"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P2":
if t_code == "T1": st["pdown"] = RELAY_ON; st["nut"] = RELAY_ON
elif t_code == "T2": st["pdown"] = RELAY_ON
elif t_code == "T3": st["pdown"] = RELAY_ON; st["air"] = RELAY_ON
elif p_code == "P3":
if t_code == "T1": st["nut"] = RELAY_ON
elif t_code == "T3": st["air"] = RELAY_ON
# Mengembalikan nilai status pompa yang dihasilkan mesin inferensi (belum dijalankan fisiknya)
return st
# ==================================================
# 8. SIKLUS UTAMA (MAIN LOOP)
# ==================================================
print("=== MULAI KONEKSI ===")
if connect_wifi():
sync_time()
if login_firebase():
print("\nSistem Otonom HydroNutrify Aktif & Memasuki Main Loop.")
try:
while True:
print("\n" + "="*40)
# Langkah 1: Kumpulkan Data Lingkungan
f_code = fetch_growth_phase()
ph_val, tds_val = get_sensor_data()
print(f"Fase : {f_code} | pH : {ph_val} | TDS : {tds_val} PPM")
# Langkah 2: Proses Data menggunakan Algoritma Forward Chaining
status_pompa = run_forward_chaining(f_code, ph_val, tds_val)
# Langkah 3: Identifikasi apakah perlu tindakan aktuator
pompa_aktif = (status_pompa["nut"] == RELAY_ON or
status_pompa["air"] == RELAY_ON or
status_pompa["pup"] == RELAY_ON or
status_pompa["pdown"] == RELAY_ON)
# Langkah 4: Metode Double Update untuk User Interface (UI) Real-Time
if pompa_aktif:
# Update ke-1: Beri tahu Firebase (dan Dashboard User) bahwa pompa menyala
print("-> Pompa AKTIF! Mengirim status ON ke Firebase...")
upload_to_firestore(ph_val, tds_val, status_pompa)
# Tindakan Nyata: Nyalakan relay secara bergiliran
eksekusi_pompa(status_pompa)
# Update ke-2: Setelah 10 detik, lapor kembali bahwa semua pompa sudah mati
print("-> Pompa Selesai. Memperbarui status Firebase menjadi OFF...")
status_mati_semua = {"nut": RELAY_OFF, "air": RELAY_OFF, "pup": RELAY_OFF, "pdown": RELAY_OFF}
upload_to_firestore(ph_val, tds_val, status_mati_semua)
else:
# Jika sensor ideal, cukup laporkan nilai sensornya saja
print("-> Kondisi Ideal (Semua Pompa OFF). Mengirim ke Firebase...")
upload_to_firestore(ph_val, tds_val, status_pompa)
# Langkah 5: Masuk ke mode tunggu (1 Jam)
print(f"Menunggu {LOOP_INTERVAL} detik untuk siklus berikutnya...")
time.sleep(LOOP_INTERVAL)
# Penanganan jika program dihentikan secara manual di Thonny (Ctrl+C)
except KeyboardInterrupt:
print("\nPROGRAM DIHENTIKAN MANUAL")
for pin in [pin_nutrisi_a, pin_nutrisi_b, pin_air_baku, pin_ph_up, pin_ph_down]:
pin.value(RELAY_OFF)
print("Semua pompa dimatikan demi keamanan.")
# Penanganan jika terjadi error crash pada koneksi / memory ESP32
except Exception as e:
print("\nLoop Error Kritis:", e)
time.sleep(5)
else:
print("Sistem berhenti: Gagal Login Firebase.")
else:
print("Sistem berhenti: Tidak ada koneksi WiFi.")