The ESP32 Six-Mode Farm Monitor is a comprehensive environmental sensing system designed to track soil moisture, water level, water clarity, and time-based alerts on a working farm or garden. Built around an ESP32 microcontroller, it integrates a capacitive soil sensor, ultrasonic distance sensor, turbidity sensor, real-time clock, rotary encoder interface, and status indicators to provide multi-parameter monitoring in a single device.
This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions to build the monitor from scratch. The included firmware implements six operational modes accessible via the rotary encoder, with an I2C LCD display showing real-time sensor readings and an RTC-based alarm system. Calibration guidance and sensor integration details ensure accurate measurements for soil conditions, water tank levels, and water quality assessment.
Wiring diagram
Gather all the parts
Assemble it in 7 steps
1. Buat jalur daya dan ground bersama
Pasang ESP32 DevKit v1 pada breadboard. Hubungkan semua pin GND ke satu jalur ground. Gunakan 3V3 untuk sensor tanah, RTC, dan rotary encoder. Gunakan 5V/VIN dari USB untuk HC-SR04, sensor kekeruhan, LCD, serta sisi HV pada konverter level.
- Semua ground harus tersambung bersama supaya pembacaan sensor tidak melompat.
- Jangan memasukkan 5V ke pin 3V3 ESP32.
- Jika VCC dan GND tertukar, sensor atau LCD dapat rusak.
2. Pasang sensor tanah dan kekeruhan
Hubungkan soil_moisture: VCC → 3V3 (power), GND → GND (ground), AOUT → GPIO34 (signal). Hubungkan turbidity_sensor: VCC → 5V (power), GND → GND (ground), AOUT → VIN turbidity_divider (signal). Hubungkan OUT turbidity_divider → GPIO35 (signal) dan GND turbidity_divider → GND (ground).
- Masukkan hanya batang sensor tanah ke tanah; bagian papan elektroniknya harus tetap kering.
- Celupkan hanya probe sensor kekeruhan ke air.
- Jangan menghubungkan AOUT sensor kekeruhan langsung ke GPIO35; pembagi tegangan diperlukan agar pin ESP32 tidak menerima tegangan terlalu tinggi.
3. Pasang sensor tinggi air
Hubungkan water_level: VCC → 5V (power), GND → GND (ground), TRIG → GPIO18 (signal), ECHO → VIN echo_divider (signal). Hubungkan OUT echo_divider → GPIO23 (signal) dan GND echo_divider → GND (ground).
- Arahkan dua mata sensor lurus ke permukaan air dari atas tangki.
- Ubah tinggi referensi tangki di firmware bila tangki Anda bukan 20 cm.
- Jangan menghubungkan ECHO HC-SR04 langsung ke GPIO23; ECHO membawa sinyal 5V yang dapat merusak ESP32 tanpa pembagi tegangan.
4. Hubungkan RTC dan LCD melalui konverter level
Hubungkan rtc: VIN → 3V3 (power), GND → GND (ground), SDA → LV_SDA i2c_level_shifter (data), SCL → LV_SCL i2c_level_shifter (clock). Hubungkan i2c_level_shifter: LV → 3V3 (power), HV → 5V (power), GND → GND (ground), LV_SDA → GPIO21 (data), LV_SCL → GPIO22 (clock). Hubungkan lcd: VCC → 5V (power), GND → GND (ground), SDA → HV_SDA i2c_level_shifter (data), SCL → HV_SCL i2c_level_shifter (clock).
- Konverter level adalah papan kecil di antara ESP32 dan LCD; fungsinya menurunkan sisi ESP32 ke 3,3V dan menjaga LCD tetap memakai 5V.
- RTC dan ESP32 berada pada sisi LV yang sama; LCD berada pada sisi HV.
- Jangan memberi RTC 5V pada rangkaian ini, karena pull-up I2C pada modul RTC dapat membuat GPIO21 dan GPIO22 menerima 5V.
5. Pasang knop pemilih mode
Hubungkan mode_encoder: VCC → 3V3 (power), GND → GND (ground), CLK → GPIO33 (signal), DT → GPIO32 (signal), SW → GPIO27 (signal). Putar knop untuk memilih mode 1 sampai 6; tekan knop untuk menghentikan alarm RTC yang sedang berbunyi.
- Jika arah putaran terbalik, tukar kabel CLK dan DT setelah USB dilepas.
- Gunakan kabel pendek agar putaran encoder tidak terbaca ganda.
- Jangan memberi VCC rotary encoder 5V; keluaran CLK, DT, dan SW dapat menjadi 5V dan merusak ESP32.
6. Pasang tiga LED indikator
Hubungkan GPIO25 → r_led_red 220 Ω → anoda led_red (signal), lalu katoda led_red → GND (ground). Hubungkan GPIO26 → r_led_yellow 220 Ω → anoda led_yellow (signal), lalu katoda led_yellow → GND (ground). Hubungkan GPIO13 → r_led_green 220 Ω → anoda led_green (signal), lalu katoda led_green → GND (ground).
- Kaki LED yang lebih panjang adalah anoda, yaitu sisi menuju resistor.
- Setiap LED menggunakan satu resistor 220 Ω sendiri.
- LED tanpa resistor dapat rusak dan dapat membebani pin ESP32.
7. Pasang buzzer dan lakukan pemeriksaan akhir
Hubungkan SIG buzzer → GPIO14 (signal) dan GND buzzer → GND (ground). Periksa kembali bahwa tidak ada kabel ECHO atau AOUT 5V yang langsung masuk ke ESP32, lalu hubungkan ESP32 ke USB.
- Buzzer akan berkedip bunyinya pada mode 2 dan berbunyi 10 detik pada alarm mode 5.
- LCD normalnya memakai alamat 0x27; jika layar menyala tetapi huruf tidak tampil, putar trimpot kontras kecil pada modul LCD.
- Jika buzzer Anda adalah modul tiga-pin bertanda VCC, I/O, GND dan VCC-nya memang harus 5V, jangan hubungkan jalur I/O 5V-nya kembali ke GPIO14. Gunakan hanya pin input I/O modul dari GPIO14.
Review all connections
1. Connections between "soil_moisture" and "ESP32"
2. Connections between "water_level" and "ESP32"
3. Connections between "rtc" and "ESP32"
4. Connections between "mode_encoder" and "ESP32"
5. Connections between "buzzer" and "ESP32"
6. Connections between "r_led_red" and "ESP32"
7. Connections between "led_red" and "ESP32"
8. Connections between "r_led_yellow" and "ESP32"
9. Connections between "led_yellow" and "ESP32"
10. Connections between "r_led_green" and "ESP32"
11. Connections between "led_green" and "ESP32"
12. Connections between "turbidity_sensor" and "ESP32"
13. Connections between "echo_divider" and "ESP32"
14. Connections between "turbidity_divider" and "ESP32"
15. Connections between "lcd" and "ESP32"
16. Connections between "i2c_level_shifter" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <Wire.h>
#include <RTClib.h>
#include <LiquidCrystal_I2C.h>
const int SOIL_PIN = 34;
const int TURBIDITY_PIN = 35;
const int TRIG_PIN = 18;
const int ECHO_PIN = 23;
const int ENCODER_CLK_PIN = 33;
const int ENCODER_DT_PIN = 32;
const int ENCODER_SW_PIN = 27;
const int BUZZER_PIN = 14;
const int LED_RED_PIN = 25;
const int LED_YELLOW_PIN = 26;
const int LED_GREEN_PIN = 13;
const int I2C_SDA_PIN = 21;
const int I2C_SCL_PIN = 22;
// Sesuaikan tiga nilai ini setelah kalibrasi dengan tanah benar-benar kering dan basah.
const int SOIL_DRY_RAW = 3600;
const int SOIL_WET_RAW = 1800;
const float TANK_REFERENCE_HEIGHT_CM = 20.0f;
const int RTC_ALARM_HOUR = 7;
const int RTC_ALARM_MINUTE = 0;
RTC_DS3231 rtc;
LiquidCrystal_I2C lcd(0x27, 16, 2);
bool rtcReady = false;
char lastLcdLine0[17] = "";
char lastLcdLine1[17] = "";
// mode adalah pilihan yang terlihat di LCD; activeMode baru berjalan setelah tombol encoder ditekan.
int mode = 1;
int activeMode = 0;
int lastEncoderClk = HIGH;
int lastButtonState = HIGH;
unsigned long lastButtonChangeMs = 0;
unsigned long lastSoilReadMs = 0;
unsigned long lastWaterReadMs = 0;
unsigned long lastTurbidityReadMs = 0;
unsigned long lastRtcReadMs = 0;
unsigned long mode4LightUntilMs = 0;
unsigned long rtcAlarmUntilMs = 0;
uint32_t rtcAlarmDateKey = 0;
float soilPercent = 0.0f;
float waterHeightCm = -1.0f;
float turbidityNtuEstimate = 0.0f;
int mode4LastBand = -1;
bool buzzerOn = false;
// Mode 5: tekan sekali untuk atur jam, tekan lagi untuk menit, lalu tekan lagi untuk simpan.
uint8_t rtcSettingStage = 0; // 0=normal, 1=atur jam, 2=atur menit
int rtcSetHour = 0;
int rtcSetMinute = 0;
void allLedsOff() {
digitalWrite(LED_RED_PIN, LOW);
digitalWrite(LED_YELLOW_PIN, LOW);
digitalWrite(LED_GREEN_PIN, LOW);
}
void setBuzzer(bool on) {
if (on == buzzerOn) return;
buzzerOn = on;
if (on) {
tone(BUZZER_PIN, 4000);
} else {
noTone(BUZZER_PIN);
}
}
const char *modeName(int selectedMode) {
switch (selectedMode) {
case 1: return "1 Viqri - kelembapan tanah";
case 2: return "2 Ihwal - alarm kelembapan";
case 3: return "3 Helmy - tinggi air berkedip";
case 4: return "4 Prama - tinggi air 5 detik";
case 5: return "5 Syamsidar - alarm RTC";
case 6: return "6 Yuri - kekeruhan air";
default: return "tidak dikenal";
}
}
void announceMode() {
Serial.print("Mode dipilih: ");
Serial.println(modeName(mode));
}
void announceStartedMode() {
Serial.print("Mode mulai berjalan: ");
Serial.println(modeName(activeMode));
}
float readSoilPercent() {
int raw = analogRead(SOIL_PIN);
float percent = (float)(SOIL_DRY_RAW - raw) * 100.0f / (SOIL_DRY_RAW - SOIL_WET_RAW);
percent = constrain(percent, 0.0f, 100.0f);
Serial.print("Kelembapan tanah: ");
Serial.print(percent, 1);
Serial.println(" %");
return percent;
}
float readWaterHeightCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(3);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long durationUs = pulseIn(ECHO_PIN, HIGH, 30000UL);
if (durationUs == 0) {
Serial.println("HC-SR04 tidak mendapat pantulan.");
return -1.0f;
}
float distanceCm = durationUs / 58.0f;
float heightCm = constrain(TANK_REFERENCE_HEIGHT_CM - distanceCm, 0.0f, TANK_REFERENCE_HEIGHT_CM);
Serial.print("Jarak sensor: ");
Serial.print(distanceCm, 1);
Serial.print(" cm, tinggi air: ");
Serial.print(heightCm, 1);
Serial.println(" cm");
return heightCm;
}
float readTurbidityNtuEstimate() {
// Pembagi 10k/20k menurunkan tegangan menjadi 2/3; kalikan 1,5 untuk tegangan sensor asli.
uint32_t millivolts = analogReadMilliVolts(TURBIDITY_PIN);
float sensorVoltage = (millivolts / 1000.0f) * 1.5f;
// Perkiraan empiris yang umum dipakai untuk SEN0189. Kalibrasikan dengan sampel standar bila perlu.
float ntu = -1120.4f * sensorVoltage * sensorVoltage + 5742.3f * sensorVoltage - 4352.9f;
ntu = constrain(ntu, 0.0f, 5000.0f);
Serial.print("Kekeruhan perkiraan: ");
Serial.print(ntu, 0);
Serial.println(" NTU");
return ntu;
}
int waterBand(float heightCm) {
if (heightCm < 0.0f || heightCm > 20.0f) return -1;
if (heightCm <= 5.0f) return 0;
if (heightCm <= 10.0f) return 1;
return 2;
}
void showWaterBand(int band, bool on) {
allLedsOff();
if (!on) return;
if (band == 0) digitalWrite(LED_RED_PIN, HIGH);
if (band == 1) digitalWrite(LED_YELLOW_PIN, HIGH);
if (band == 2) digitalWrite(LED_GREEN_PIN, HIGH);
}
void pollEncoder() {
int currentClk = digitalRead(ENCODER_CLK_PIN);
if (currentClk != lastEncoderClk && currentClk == LOW) {
int direction = (digitalRead(ENCODER_DT_PIN) != currentClk) ? 1 : -1;
if (activeMode == 5 && rtcSettingStage == 1) {
rtcSetHour = (rtcSetHour + direction + 24) % 24;
} else if (activeMode == 5 && rtcSettingStage == 2) {
rtcSetMinute = (rtcSetMinute + direction + 60) % 60;
} else {
// Memutar encoder hanya memilih mode. Mode sebelumnya berhenti sampai pilihan baru diklik.
mode += direction;
if (mode > 6) mode = 1;
if (mode < 1) mode = 6;
activeMode = 0;
rtcSettingStage = 0;
mode4LastBand = -1;
mode4LightUntilMs = 0;
rtcAlarmUntilMs = 0;
allLedsOff();
setBuzzer(false);
announceMode();
}
}
lastEncoderClk = currentClk;
int button = digitalRead(ENCODER_SW_PIN);
if (button != lastButtonState && millis() - lastButtonChangeMs > 40) {
lastButtonChangeMs = millis();
lastButtonState = button;
if (button == LOW) {
// Klik pertama pada pilihan selalu menyalakan mode itu.
if (activeMode != mode) {
activeMode = mode;
mode4LastBand = -1;
mode4LightUntilMs = 0;
rtcAlarmUntilMs = 0;
setBuzzer(false);
announceStartedMode();
} else if (activeMode == 5 && rtcReady) {
// Klik berikutnya di Mode 5 dipakai khusus untuk pengaturan jam RTC.
rtcAlarmUntilMs = 0;
setBuzzer(false);
if (rtcSettingStage == 0) {
DateTime current = rtc.now();
rtcSetHour = current.hour();
rtcSetMinute = current.minute();
rtcSettingStage = 1;
Serial.println("Atur jam RTC: putar encoder, lalu tekan untuk lanjut ke menit.");
} else if (rtcSettingStage == 1) {
rtcSettingStage = 2;
Serial.println("Atur menit RTC: putar encoder, lalu tekan untuk menyimpan.");
} else {
DateTime current = rtc.now();
rtc.adjust(DateTime(current.year(), current.month(), current.day(), rtcSetHour, rtcSetMinute, 0));
rtcSettingStage = 0;
Serial.println("Jam RTC sudah disimpan.");
}
} else {
// Pada mode lain, klik saat sudah aktif hanya menghentikan bunyi alarm/buzzer yang sedang berjalan.
rtcAlarmUntilMs = 0;
setBuzzer(false);
}
}
}
}
void writeLcdLine(uint8_t row, const char *text, char *previous) {
char padded[17];
snprintf(padded, sizeof(padded), "%-16.16s", text);
if (strcmp(padded, previous) == 0) return;
lcd.setCursor(0, row);
lcd.print(padded);
strcpy(previous, padded);
}
void updateLcd() {
char line0[17];
char line1[17];
snprintf(line0, sizeof(line0), "Pilih M%d %.7s", mode, modeName(mode) + 2);
if (activeMode == 0) {
snprintf(line1, sizeof(line1), "Tekan: mulai");
} else if (activeMode == 1 || activeMode == 2) snprintf(line1, sizeof(line1), "Tanah:%5.1f%%", soilPercent);
else if (activeMode == 3 || activeMode == 4) snprintf(line1, sizeof(line1), "Air:%6.1f cm", waterHeightCm);
else if (activeMode == 5 && rtcSettingStage == 1) {
snprintf(line0, sizeof(line0), "Atur JAM: putar");
snprintf(line1, sizeof(line1), "> %02d:%02d tekan", rtcSetHour, rtcSetMinute);
} else if (activeMode == 5 && rtcSettingStage == 2) {
snprintf(line0, sizeof(line0), "Atur MENIT:putar");
snprintf(line1, sizeof(line1), " %02d:>%02d tekan", rtcSetHour, rtcSetMinute);
} else if (activeMode == 5) snprintf(line1, sizeof(line1), "Alarm %02d:%02d", RTC_ALARM_HOUR, RTC_ALARM_MINUTE);
else snprintf(line1, sizeof(line1), "Keruh:%5.0f NTU", turbidityNtuEstimate);
writeLcdLine(0, line0, lastLcdLine0);
writeLcdLine(1, line1, lastLcdLine1);
}
void updateSensors(unsigned long nowMs) {
if ((activeMode == 1 || activeMode == 2) && nowMs - lastSoilReadMs >= 500) {
lastSoilReadMs = nowMs;
soilPercent = readSoilPercent();
}
if ((activeMode == 3 || activeMode == 4) && nowMs - lastWaterReadMs >= 200) {
lastWaterReadMs = nowMs;
waterHeightCm = readWaterHeightCm();
}
if (activeMode == 6 && nowMs - lastTurbidityReadMs >= 500) {
lastTurbidityReadMs = nowMs;
turbidityNtuEstimate = readTurbidityNtuEstimate();
}
}
void runMode(unsigned long nowMs) {
allLedsOff();
setBuzzer(false);
if (activeMode == 1) {
if (soilPercent < 35.0f) digitalWrite(LED_RED_PIN, HIGH);
else digitalWrite(LED_GREEN_PIN, HIGH);
} else if (activeMode == 2) {
if (soilPercent < 40.0f && (nowMs % 500UL) < 250UL) setBuzzer(true);
} else if (activeMode == 3) {
int band = waterBand(waterHeightCm);
showWaterBand(band, (nowMs % 500UL) < 250UL);
} else if (activeMode == 4) {
int band = waterBand(waterHeightCm);
if (band != mode4LastBand) {
mode4LastBand = band;
mode4LightUntilMs = nowMs + 5000UL;
}
showWaterBand(band, band >= 0 && nowMs < mode4LightUntilMs);
} else if (activeMode == 5 && rtcSettingStage == 0) {
if (rtcReady && nowMs - lastRtcReadMs >= 500) {
lastRtcReadMs = nowMs;
DateTime current = rtc.now();
uint32_t dateKey = (uint32_t)current.year() * 10000UL + (uint32_t)current.month() * 100UL + current.day();
if (current.hour() == RTC_ALARM_HOUR && current.minute() == RTC_ALARM_MINUTE && dateKey != rtcAlarmDateKey) {
rtcAlarmDateKey = dateKey;
rtcAlarmUntilMs = nowMs + 10000UL;
Serial.println("Alarm RTC aktif selama 10 detik.");
}
}
if (nowMs < rtcAlarmUntilMs) setBuzzer(true);
} else if (activeMode == 6) {
if (turbidityNtuEstimate > 1000.0f) digitalWrite(LED_YELLOW_PIN, HIGH);
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_RED_PIN, OUTPUT);
pinMode(LED_YELLOW_PIN, OUTPUT);
pinMode(LED_GREEN_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(ENCODER_CLK_PIN, INPUT_PULLUP);
pinMode(ENCODER_DT_PIN, INPUT_PULLUP);
pinMode(ENCODER_SW_PIN, INPUT_PULLUP);
analogReadResolution(12);
allLedsOff();
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
lcd.init();
lcd.backlight();
writeLcdLine(0, "Sistem tani", lastLcdLine0);
writeLcdLine(1, "Memulai...", lastLcdLine1);
rtcReady = rtc.begin();
if (rtcReady) {
if (rtc.lostPower()) {
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
Serial.println("RTC belum memiliki waktu; diatur ke waktu saat firmware dibuat.");
}
} else {
Serial.println("DS3231 tidak ditemukan; Mode 5 tidak dapat berjalan sampai kabel RTC diperiksa.");
}
lastEncoderClk = digitalRead(ENCODER_CLK_PIN);
lastButtonState = digitalRead(ENCODER_SW_PIN);
announceMode();
}
void loop() {
unsigned long nowMs = millis();
pollEncoder();
updateSensors(nowMs);
runMode(nowMs);
updateLcd();
delay(5);
}Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.




