416 lines
17 KiB
Python
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.") |