Community project

ESP32 Six-Mode Farm Monitor

Photo of ESP32 Six-Mode Farm Monitor
Generated with AI

Ihwal Lahia

Published November 5, 2026

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

Loading diagram…

Gather all the parts

QtyComponent
1

Gravity: IP65 Capacitive Soil Moisture Sensor

Capacitive soil moisture sensor with IP65 waterproof and corrosion-resistant construction. Compatible with Arduino, ESP32, and Raspberry Pi.

1

HC-SR04

HC-SR04 ultrasonic distance sensor module with separate 40 kHz transmitter and receiver transducers, measuring 2 to 400 cm. A 10 us pulse on TRIG starts a measurement and ECHO stays high for the echo's round-trip time. This HC-SR04+ board runs from 3.3 V or 5 V.

1

Adafruit DS3231 Precision RTC Breakout

DS3231 precision real-time clock breakout with integrated TCXO and crystal for accurate timekeeping over I2C.

1

KY-040 Rotary Encoder Module

5-pin incremental rotary encoder breakout with integrated momentary push switch. CLK and DT are the quadrature outputs; SW is the built-in push-button output and should not be modelled as a separate Push Button component.

1

Piezo Buzzer

Small passive piezo buzzer (TDK PS1240) for beeps, tones and alerts. It has no built-in oscillator, so drive one lead with a 3-30 V square wave from a microcontroller pin using tone() or PWM and connect the other lead to ground. It is loudest around 4 kHz and works from about 2 to 10 kHz; driving both leads in antiphase roughly doubles the volume.

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

yellow

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

green

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

220 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

220 Ω

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

220 Ω

Through-hole resistor (current-limiting in series with an LED)

1

DFRobot Gravity Analog Turbidity Sensor (SEN0189)

Sensor optik yang mengukur kejernihan relatif air dengan keluaran analog.

1

Pembagi tegangan ECHO HC-SR04 10 kΩ / 20 kΩ

Dua resistor yang menurunkan sinyal ECHO 5 volt dari HC-SR04 menjadi sekitar 3,3 volt agar aman masuk ke ESP32.

1

Voltage Divider

10 kΩ upper / 20 kΩ lower; 4.5 V maximum to 3.0 V maximum

Two resistors used to scale a higher analogue voltage down before it reaches a microcontroller ADC input.

1

LCD 16x2 I2C

16x2 character LCD display with I2C backpack

1

BSS138 konverter level I2C 3,3 V ke 5 V

Modul konverter level dua arah yang membuat jalur data dan clock I2C ESP32 3,3 volt aman dipakai LCD 5 volt.

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"

Functionsoil_moistureESP32
powerVCC → power3V3
groundGND → groundGND
analogAOUT → moisture signalGPIO 34

2. Connections between "water_level" and "ESP32"

Functionwater_levelESP32
powerVCC → power5V
groundGND → groundGND
digitalECHO → Pembagi tegangan ECHO HC-SR04 10 kΩ / 20 kΩ VINEXT
digitalTRIG → distance triggerGPIO 18

3. Connections between "rtc" and "ESP32"

FunctionrtcESP32
powerVIN → power3V3
groundGND → groundGND
i2cSDA → BSS138 konverter level I2C 3,3 V ke 5 V LV_SDAEXT
i2cSCL → BSS138 konverter level I2C 3,3 V ke 5 V LV_SCLEXT

4. Connections between "mode_encoder" and "ESP32"

Functionmode_encoderESP32
powerVCC → power3V3
groundGND → groundGND
digitalCLK → rotation channel AGPIO 33
digitalDT → rotation channel BGPIO 32
digitalSW → push buttonGPIO 27

5. Connections between "buzzer" and "ESP32"

FunctionbuzzerESP32
groundGND → groundGND
pwmSIG → tone signalGPIO 14

6. Connections between "r_led_red" and "ESP32"

Functionr_led_redESP32
digitalP2 → LED ANODEEXT
digitalP1 → red LED driveGPIO 25

7. Connections between "led_red" and "ESP32"

Functionled_redESP32
groundGND → groundGND

8. Connections between "r_led_yellow" and "ESP32"

Functionr_led_yellowESP32
digitalP2 → LED ANODEEXT
digitalP1 → yellow LED driveGPIO 26

9. Connections between "led_yellow" and "ESP32"

Functionled_yellowESP32
groundGND → groundGND

10. Connections between "r_led_green" and "ESP32"

Functionr_led_greenESP32
digitalP2 → LED ANODEEXT
digitalP1 → green LED driveGPIO 13

11. Connections between "led_green" and "ESP32"

Functionled_greenESP32
groundGND → groundGND

12. Connections between "turbidity_sensor" and "ESP32"

Functionturbidity_sensorESP32
powerVCC → power5V
groundGND → groundGND
analogAOUT → Voltage Divider VINEXT

13. Connections between "echo_divider" and "ESP32"

Functionecho_dividerESP32
groundGND → groundGND
digitalOUT → sinyal ECHO digital 3,3 V yang amanGPIO 23

14. Connections between "turbidity_divider" and "ESP32"

Functionturbidity_dividerESP32
groundGND → groundGND
analogOUT → safe scaled turbidity signalGPIO 35

15. Connections between "lcd" and "ESP32"

FunctionlcdESP32
powerVCC → LCD power5V
groundGND → LCD groundGND
i2cSDA → BSS138 konverter level I2C 3,3 V ke 5 V HV_SDAEXT
i2cSCL → BSS138 konverter level I2C 3,3 V ke 5 V HV_SCLEXT

16. Connections between "i2c_level_shifter" and "ESP32"

Functioni2c_level_shifterESP32
powerLV → 3.3 V reference for ESP32 I2C side3V3
powerHV → 5 V reference for LCD I2C side5V
groundGND → shared groundGND
i2cLV_SDA → data I2C 3,3 V bersama RTCGPIO 21
i2cLV_SCL → clock I2C 3,3 V bersama RTCGPIO 22

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.

Open in Schematik