SMART FARMING (IoT Berbasis ESP32)

/*******************************************************
Program   : SMART FARMING (IoT Berbasis ESP32)
Chip      : ESP32
Website   : https://bintangterang.or.id/iot/
Tanggal   : 01 November 2025
Pembuat   : OCIM, S.Kom
Tempat    : LPK BINTANG TERANG
Penguji   : -
Deskripsi :
  Sistem otomatisasi pertanian/hidroponik berbasis IoT
  yang memantau dan mengatur parameter lingkungan
  (suhu, kelembaban, TDS, pH, level air, irigasi tanah, dan sirkulasi).
  Semua data dikirim ke server, serta bisa menerima perintah manual dari server.
********************************************************/

#include
#include
#include
#include
#include 
#include
#include
#include
#include

// ----------------- CONFIG JARINGAN & SERVER -----------------
const char* ssid = "BINTANG TERANG";
const char* password = "1sampai8";
const char* host = "bintangterang.or.id";
const int httpsPort = 443;
const char* API_SENSOR_PATH = "/iot/get_sensor_esp32.php";
const char* API_RELAY_COMMAND_PATH = "/iot/get_relay_command.php";
const int USER_ID = 1;
const char* DEVICE_ID = "SMART_FARM_001";

// ----------------- SENSOR PIN -----------------
#define DHTPIN 15
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

#define TDS_PIN 34
#define SOIL_PIN 35
#define PH_PIN 39
#define ULTRASONIC_TRIG 16
#define ULTRASONIC_ECHO 17

// ----------------- RELAY PIN -----------------
#define RELAY_AIR           25
#define RELAY_NUTRISI_A     26
#define RELAY_NUTRISI_B     32
#define RELAY_SIRKULASI     27
#define RELAY_POMPA_HARIAN  13
#define RELAY_SALUR         14
#define RELAY_IRIGASI_TANAH 33
#define RELAY_FAN           12
#define RELAY_UP            2
#define RELAY_DOWN          4

const int RELAY_PINS[] = { RELAY_AIR, RELAY_NUTRISI_A, RELAY_NUTRISI_B, RELAY_SIRKULASI, RELAY_POMPA_HARIAN, RELAY_SALUR, RELAY_IRIGASI_TANAH, RELAY_FAN, RELAY_UP, RELAY_DOWN };
const char* RELAY_NAMES[] = { "relay", "relay1", "relay2", "relay3", "relay4", "relay5", "relay6", "relay7", "relay8", "relay9" };
const int RELAY_COUNT = 10;

// ----------------- KONFIGURASI OTOMATIS -----------------
const float PPM_TARGETS[] = {328.0, 321.0, 314.0};
const int PPM_PERIOD_DAYS = 14;
const float PPM_TOLERANCE = 10.0;
const unsigned long NUTRI_PUMP_DURATION = 1000;

const float PH_TARGET = 6.0;
const float PH_TOLERANCE = 0.2;
const unsigned long PH_PUMP_DURATION = 1000;
const unsigned long PH_CHECK_DELAY = 60000;

float MIN_VOLUME_LITERS = 7.0; // batas minimal air dari server atau default
const float FAN_TEMP_LIMIT = 30.0;
const float FAN_HUM_LIMIT = 70.0;

const int SOIL_DRY_VALUE = 3500;
const int SOIL_WET_VALUE = 1500;

// ----------------- GLOBAL -----------------
LiquidCrystal_I2C lcd(0x27, 16, 2);
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 7 * 3600);
Preferences preferences;
WiFiClientSecure client;

float targetPPM = 0;
float targetPH = 0;
float targetLevel = 0;

float temp = 0, hum = 0, currentPPM = 0, soilPercent = 0;
float currentVolumeL = 0, phValue = 7.0;
long daysSincePlanting = 0;

unsigned long lastSend = 0, lastOverride = 0, lastSlide = 0, lastNTPUpdate = 0;
unsigned long phPumpStartTime = 0, phLastDosedTime = 0;

// =================== INTERVALS (DI-OPTIMASI) ===================
const unsigned long SEND_INTERVAL = 30000;        // kirim data tiap 30 detik
const unsigned long OVERRIDE_INTERVAL = 60000;    // cek perintah tiap 60 detik
const unsigned long LCD_INTERVAL = 2000;          // slide LCD tiap 3 detik
const unsigned long NTP_UPDATE_INTERVAL = 900000; // update NTP tiap 15 menit



void loadTargetsFromPrefs() {
  preferences.begin("smartfarm", true);
  targetPPM = preferences.getFloat("target_ppm", 500);
  targetPH = preferences.getFloat("target_ph", 6.0);
  targetLevel = preferences.getFloat("target_level", 8.0);
  preferences.end();
}

// ----------------- LCD -----------------
String sensorNames[] = {"Suhu", "Kelembaban", "Soil", "TDS", "pH", "Level", "HST"};
float sensorValues[7] = {0};
int currentSensor = 0;

// ----------------- FUNGSI RELAY -----------------
// KONSISTEN: relay aktif = LOW (common pada modul relay 1/4 channel)
void relayWrite(int pin, bool on) { digitalWrite(pin, on ? HIGH : LOW ); }
int readRelayStatus(int pin) { return (digitalRead(pin) == HIGH) ? 1 : 0; }
void allRelaysOff() { for (int i = 0; i < RELAY_COUNT; i++) relayWrite(RELAY_PINS[i], false); }

// ----------------- SENSOR -----------------
float readTDSppm() { return constrain(analogRead(TDS_PIN) * (3.3 / 4095.0) * 500.0, 0, 3000); }
float readSoilPercent() { return constrain(map(analogRead(SOIL_PIN), SOIL_DRY_VALUE, SOIL_WET_VALUE, 0, 100), 0, 100); }

// --- Filter Ultrasonik (rata-rata 5x) ---
float readTankDistance() {
  float total = 0; int valid = 0;
  for (int i = 0; i < 5; i++) {
    digitalWrite(ULTRASONIC_TRIG, LOW); delayMicroseconds(2);
    digitalWrite(ULTRASONIC_TRIG, HIGH); delayMicroseconds(10);
    digitalWrite(ULTRASONIC_TRIG, LOW);
    // kurangi timeout supaya tidak blocking lama
    float d = pulseIn(ULTRASONIC_ECHO, HIGH, 100000) * 0.0343 / 2.0; // 100ms timeout
    if (d > 1 && d < 100) { total += d; valid++; }
    delay(20);
  }
  return (valid > 0) ? (total / valid) : 999;
}

// --- Kalibrasi Galon Le Minerale 15L ---
float readTankVolumeLiters() {
  float distance = readTankDistance();
  const float TANK_HEIGHT_CM = 35.0;
  const float SENSOR_OFFSET_CM = 4.0;
  const float MAX_VOLUME_LITERS = 15.0;
  float heightFilled = (TANK_HEIGHT_CM + SENSOR_OFFSET_CM) - distance;
  heightFilled = constrain(heightFilled, 0, TANK_HEIGHT_CM);
  float volume = (heightFilled / TANK_HEIGHT_CM) * MAX_VOLUME_LITERS;
  return constrain(volume, 0, MAX_VOLUME_LITERS);
}

float readPH() { return constrain(7 + (2.5 - analogRead(PH_PIN) * (3.3 / 4095.0)) * 3.0, 0, 14); }

void readDHT_Robust() {
  for (int i = 0; i < 5; i++) {
    float t = dht.readTemperature(), h = dht.readHumidity();
    if (!isnan(t) && !isnan(h) && t > 0) { temp = t; hum = h; return; }
    delay(150);
  }
}

// ----------------- HST & TIME -----------------
void setupDate() {
  preferences.begin("smartfarm", false);
  if (!preferences.isKey("plant_time")) preferences.putLong("plant_time", timeClient.getEpochTime());
  preferences.end();
}
void calculateHST() {
  preferences.begin("smartfarm", true);
  long plantTime = preferences.getLong("plant_time", 0);
  preferences.end();
  daysSincePlanting = (plantTime == 0) ? 0 : (now() - plantTime) / 86400;
}
void updateHSTFromServer(long hst) {
  preferences.begin("smartfarm", false);
  preferences.putLong("plant_time", timeClient.getEpochTime() - (hst * 86400));
  preferences.end();
  daysSincePlanting = hst;
}
void updateTimeFromNTP() {
  if (WiFi.status() == WL_CONNECTED && millis() - lastNTPUpdate > NTP_UPDATE_INTERVAL) {
    timeClient.update(); setTime(timeClient.getEpochTime()); lastNTPUpdate = millis();
  }
}

// ----------------- KONTROL OTOMATIS -----------------


void autoControlWaterLevel() {
  static bool pumpOn = false;  // lokal

  float limit = (targetLevel > 0) ? targetLevel : 8.0;

  if (!pumpOn && currentVolumeL <= (limit - 1.0)) {
    relayWrite(RELAY_AIR, true);
    pumpOn = true;
    Serial.println("???? Pompa AIR ON - Level di bawah batas target");
  }
  else if (pumpOn && currentVolumeL >= limit) {
    relayWrite(RELAY_AIR, false);
    pumpOn = false;
    Serial.println("???? Pompa AIR OFF - Level mencapai target");
  }
}

void autoControlPH() {
  if (phPumpStartTime != 0 && millis() - phPumpStartTime >= PH_PUMP_DURATION) {
    relayWrite(RELAY_UP, false);
    relayWrite(RELAY_DOWN, false);
    phPumpStartTime = 0;
    phLastDosedTime = millis();
    return;
  }

  if (millis() - phLastDosedTime < PH_CHECK_DELAY) return;

  if (targetPH == 0) targetPH = PH_TARGET;  // fallback default

  if (phValue < targetPH - PH_TOLERANCE) {
    relayWrite(RELAY_UP, true);
    phPumpStartTime = millis();
    Serial.println("⬆️ Menambah pH (pompa UP aktif)");
  }
  else if (phValue > targetPH + PH_TOLERANCE) {
    relayWrite(RELAY_DOWN, true);
    phPumpStartTime = millis();
    Serial.println("⬇️ Menurunkan pH (pompa DOWN aktif)");
  }
}

void autoControlNutrient() {
  static bool dosingActive = false;
  static unsigned long lastDoseTime = 0;

  if (targetPPM == 0) targetPPM = 320;  // fallback default

  const float NUTRISI_ON_DIFF  = 50.0;
  const float NUTRISI_OFF_DIFF = 10.0;
  const unsigned long NUTRISI_DELAY = 30000; // 30 detik

  float diff = targetPPM - currentPPM;

  if (dosingActive && diff <= NUTRISI_OFF_DIFF) {
    relayWrite(RELAY_NUTRISI_A, false);
    relayWrite(RELAY_NUTRISI_B, false);
    dosingActive = false;
    lastDoseTime = millis();
    Serial.printf("✅ PPM naik (%.0f ≤ %.0f), pompa nutrisi OFF.\n", currentPPM, targetPPM - NUTRISI_OFF_DIFF);
    return;
  }

  if (millis() - lastDoseTime < NUTRISI_DELAY) return;

  if (!dosingActive && diff > NUTRISI_ON_DIFF) {
    relayWrite(RELAY_NUTRISI_A, true);
    relayWrite(RELAY_NUTRISI_B, true);
    dosingActive = true;
    Serial.printf("???? PPM rendah (%.0f < %.0f), pompa nutrisi ON.\n", currentPPM, targetPPM - NUTRISI_ON_DIFF);
  }
}


void autoControlSalur() {
  static bool salurActive = false;

  if (targetPPM == 0) targetPPM = 320; // fallback

  const float SALUR_ON_DIFF  = 100.0;
  const float SALUR_OFF_DIFF = 50.0;

  float diff = currentPPM - targetPPM;

  if (!salurActive && diff > SALUR_ON_DIFF) {
    relayWrite(RELAY_SALUR, true);
    salurActive = true;
    Serial.printf("???? PPM tinggi (%.1f ppm > %.1f), SALUR ON.\n", currentPPM, targetPPM + SALUR_ON_DIFF);
  }
  else if (salurActive && diff <= SALUR_OFF_DIFF) {
    relayWrite(RELAY_SALUR, false);
    salurActive = false;
    Serial.printf("✅ PPM turun (%.1f ppm <= %.1f), SALUR OFF.\n", currentPPM, targetPPM + SALUR_OFF_DIFF);
  }
}


void autoControlIrigasi() { relayWrite(RELAY_IRIGASI_TANAH, soilPercent < 45); }
void autoControlHydroponik() {
  int currentHour = hour();
  unsigned long nowMillis = millis();
  static unsigned long lastCirculationStart = 0;
  static bool circulationOn = false;

  // --- Pompa Harian: tetap nyala full di siang hari ---
  if (currentHour >= 6 && currentHour < 18) {
    relayWrite(RELAY_POMPA_HARIAN, true);
  } else {
    relayWrite(RELAY_POMPA_HARIAN, false);
  }

  // --- Sirkulasi: hanya aktif di jam 6–18, nyala 3 menit tiap jam ---
  if (currentHour >= 6 && currentHour < 18) {
    const unsigned long CIRCULATION_ON_DURATION = 3UL * 60UL * 1000UL;  // 3 menit
    const unsigned long CIRCULATION_INTERVAL = 60UL * 60UL * 1000UL;    // 1 jam

    if (!circulationOn && nowMillis - lastCirculationStart >= CIRCULATION_INTERVAL) {
      relayWrite(RELAY_SIRKULASI, true);
      circulationOn = true;
      lastCirculationStart = nowMillis;
      Serial.println("♻️ Pompa sirkulasi ON (jadwal jam siang)");
    }

    // Matikan setelah 3 menit nyala
    if (circulationOn && nowMillis - lastCirculationStart >= CIRCULATION_ON_DURATION) {
      relayWrite(RELAY_SIRKULASI, false);
      circulationOn = false;
      Serial.println("???? Pompa sirkulasi OFF (selesai 3 menit)");
    }
  } else {
    // Di malam hari, pastikan pompa sirkulasi OFF
    relayWrite(RELAY_SIRKULASI, false);
    circulationOn = false;
  }
}

void autoControlFan() { relayWrite(RELAY_FAN, (temp > FAN_TEMP_LIMIT)); }

// ----------------- SERVER -----------------
void checkRelayOverride() {
  client.setInsecure();
  if (!client.connect(host, httpsPort)) return;

  String url = String(API_RELAY_COMMAND_PATH) + "?device_uid=" + DEVICE_ID;
  client.print(String("GET ") + url + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n");

  while (client.connected()) {
    String line = client.readStringUntil('\n');
    if (line == "\r") break;
  }

  String json = client.readString();
  client.stop();

  int start = json.indexOf('{'), end = json.lastIndexOf('}');
  if (start == -1 || end == -1) return;
  json = json.substring(start, end + 1);

  // perkecil ukuran document untuk hemat heap
  DynamicJsonDocument doc(2048);
  Serial.println(json);

  if (deserializeJson(doc, json)) {
    Serial.println("⚠️ Gagal parse JSON target dari server!");
    return;
  }

  for (int i = 0; i < RELAY_COUNT; i++) {
    const char* key = RELAY_NAMES[i];
    if (doc.containsKey(key)) relayWrite(RELAY_PINS[i], doc[key] == 1);
  }

  if (doc.containsKey("target_ppm")) {
    preferences.begin("smartfarm", false);
    preferences.putFloat("target_ppm", doc["target_ppm"].as());
    preferences.end();
  }
  if (doc.containsKey("target_ph")) {
    preferences.begin("smartfarm", false);
    preferences.putFloat("target_ph", doc["target_ph"].as());
    preferences.end();
  }
  if (doc.containsKey("target_level")) {
    preferences.begin("smartfarm", false);
    preferences.putFloat("target_level", doc["target_level"].as());
    preferences.end();
  }
  if (doc.containsKey("command_hst")) updateHSTFromServer(doc["command_hst"]);
  if (doc.containsKey("water_min_liters")) MIN_VOLUME_LITERS = doc["water_min_liters"].as();

  Serial.printf("???? Target from server -> PPM: %.1f | pH: %.2f | Level: %.1f L\n",
                doc["target_ppm"].as(),
                doc["target_ph"].as(),
                doc["target_level"].as());

  doc.clear();
}

void sendSensorData() {
  client.setInsecure();
  if (!client.connect(host, httpsPort)) return;
  DynamicJsonDocument doc(512);
  doc["id_device"] = DEVICE_ID; doc["user_id"] = USER_ID;
  doc["temperature"] = temp; doc["humidity"] = hum; doc["soil"] = soilPercent;
  doc["tds"] = currentPPM; doc["ph"] = phValue; doc["level"] = currentVolumeL; doc["hst"] = daysSincePlanting;
  for (int i = 0; i < RELAY_COUNT; i++) doc[RELAY_NAMES[i]] = readRelayStatus(RELAY_PINS[i]);
  String json; serializeJson(doc, json);
  client.print(String("POST ") + API_SENSOR_PATH + " HTTP/1.1\r\nHost: " + host +
               "\r\nContent-Type: application/json\r\nContent-Length:" + json.length() +
               "\r\nConnection: close\r\n\r\n" + json);
  client.stop();
  doc.clear();
}

// ----------------- LCD -----------------
void updateLCDSlide() {
  if (millis() - lastSlide < LCD_INTERVAL) return;
  lastSlide = millis();
  sensorValues[0] = temp; sensorValues[1] = hum; sensorValues[2] = soilPercent;
  sensorValues[3] = currentPPM; sensorValues[4] = phValue; sensorValues[5] = currentVolumeL; sensorValues[6] = daysSincePlanting;
  lcd.clear(); lcd.setCursor(0, 0); lcd.print(sensorNames[currentSensor]); lcd.setCursor(0, 1);
  switch (currentSensor) {
    case 0: lcd.printf("%.1f C", temp); break;
    case 1: lcd.printf("%.1f %%", hum); break;
    case 2: lcd.printf("%.0f %%", soilPercent); break;
    case 3: lcd.printf("%.0f ppm", currentPPM); break;
    case 4: lcd.printf("%.2f", phValue); break;
    case 5: lcd.printf("%.1f L", currentVolumeL); break;
    case 6: lcd.printf("%ld hari", daysSincePlanting); break;
  }
  currentSensor = (currentSensor + 1) % 7;
}

// ----------------- SETUP -----------------
void setup() {
  Serial.begin(115200);
  pinMode(ULTRASONIC_TRIG, OUTPUT); pinMode(ULTRASONIC_ECHO, INPUT);
  for (int i = 0; i < RELAY_COUNT; i++) pinMode(RELAY_PINS[i], OUTPUT);
  allRelaysOff();
  lcd.init(); lcd.backlight();
  dht.begin();

  WiFi.begin(ssid, password);
  Serial.print("Connecting WiFi");
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println(" connected!");
  timeClient.begin(); timeClient.update();
  setTime(timeClient.getEpochTime());
  setupDate(); updateTimeFromNTP();
  loadTargetsFromPrefs();
}



// ----------------- LOOP -----------------
void loop() {
  unsigned long now = millis();
  if (WiFi.status() != WL_CONNECTED) { WiFi.begin(ssid, password); delay(3000); return; }

  readDHT_Robust(); currentPPM = readTDSppm(); soilPercent = readSoilPercent();
  currentVolumeL = readTankVolumeLiters(); phValue = readPH();
  updateTimeFromNTP(); calculateHST();

  autoControlWaterLevel();
  autoControlNutrient();
  autoControlSalur();
  autoControlPH();
  autoControlIrigasi();
  autoControlHydroponik();
  autoControlFan();

  if (now - lastOverride > OVERRIDE_INTERVAL) { checkRelayOverride(); lastOverride = now; }
  if (now - lastSend > SEND_INTERVAL) { sendSensorData(); lastSend = now; }

  updateLCDSlide();

  Serial.printf("TDS: %.0f ppm | Soil: %.0f %% | pH: %.2f | Level: %.1f L | Temp: %.1f°C | Hum: %.1f%% | MIN_VOLUME: %.1fL\n",
                currentPPM, soilPercent, phValue, currentVolumeL, temp, hum, MIN_VOLUME_LITERS);

  // beri sedikit napas agar task background WiFi/FreeRTOS tidak terblokir
  delay(20);

Komentari Tulisan Ini
Tulisan Lainnya
Kabid PNF Disdik Ciamis Beri Motivasi di LPK Bintang Terang

Ciamis, 01 November 2025 — Kepala Bidang Pendidikan Nonformal (PNF) Dinas Pendidikan Kabupaten Ciamis, Bapak Eka Yudha Katresna, S.Sos., M.M., berkunjung ke LPK/LKP Bintang Terang

01/11/2025 15:05 - Oleh Admin - Dilihat 520 kali
CAMAT PANUMBANGAN KUNJUNGI LPK BINTANG TERANG

Camat Panumbangan Kunjungi LPK Bintang Terang, Beri Motivasi untuk Peserta Pelatihan Smart Farming Panumbangan, 31 Oktober 2025 — LPK Bintang Terang mendapat kunjungan istimewa

01/11/2025 02:06 - Oleh Admin - Dilihat 497 kali
Green Tech Farm Mini Lab Smart Farming Innovation Center

  Istilah “Green Tech Farm Mini Lab Smart Farming Innovation Center” terdiri dari beberapa konsep yang saling terkait — semuanya berhubungan dengan inovasi tekno

27/10/2025 07:48 - Oleh Admin - Dilihat 76 kali
SMART FARMING ESP32 - FULL AUTOMATIC

  Kelompok 1 – Water & Irrigation (4 relay) Relay Fungsi Pin ESP32 RELAY_AIR Isi air tandon 25 RELAY_POMPA_HARIAN

18/10/2025 18:51 - Oleh Admin - Dilihat 76 kali
PENJELASAN STATUS SISTEM HIDROPONIK

    Bagian Kode Nilai Parameter Arti H 1 1 Hari Tanam (Hari ke-) Tanaman Anda sedang berada di Hari ke-1 fase pertumbuhan.

14/10/2025 22:07 - Oleh Admin - Dilihat 130 kali
PRAKTIK RANGKAIAN LAMPU FLIP FLOP

kami akan melaksanakan praktik rangkaian lampu flip-flop, yaitu rangkaian sederhana yang menunjukkan prinsip kerja sistem penyalaan lampu bergantian secara otomatis. Tujuan dari prak

13/10/2025 14:11 - Oleh Admin - Dilihat 79 kali
PEMBUKAAN PELATIHAN SMART FARMING PROJECT BASED LEARING (PBL)

Pelatihan Smart Farming berbasis Internet of Things (IoT) ini merupakan langkah nyata dalam menghadapi tantangan era digital di sektor pertanian. Dengan penerapan teknologi, diharapk

13/10/2025 12:17 - Oleh Admin - Dilihat 77 kali
PENDAFTARAN PROJECT BASED LEARNING (PBL) TAHAP 4

Ikuti Pelatihan smart Farming berbasis Internet of Things (IoT) bersama para ahli dan praktisi terbaik. Materi mencakup: Pengenalan Teknologi IoT untuk Pertanian Sistem Iriga

13/10/2025 11:57 - Oleh Admin - Dilihat 97 kali
ESP 8266 lolin 3 relay https://bintangterang.or.id/iot

Materi Pembahasan: ESP8266 Lolin untuk Kontrol 3 Relay via Server HTTPS 1. Pendahuluan ESP8266 Lolin adalah modul mikrokontroler berbasis WiFi yang dapat diprogram menggunakan Arduino

20/09/2025 01:01 - Oleh Admin - Dilihat 179 kali
Implementasi CBT–CBA Di LPK BINTANG TERANG

Timeline Implementasi CBT–CBA LPK BINTANG TERANG (2024–2026)   No Kegiatan 2024 2025 2026 1 Perencanaan & Penyusunan K

19/09/2025 13:17 - Oleh Admin - Dilihat 93 kali