/*******************************************************
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);