// SPDX-License-Identifier: MIT
// ESP-IDF FreeRTOS greenhouse/hydroponic flow (no MQTT)
// Board: ESP32 DevKit C V4 (Wokwi), Builder: esp-idf
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "driver/gpio.h"
#include "driver/adc.h"
#include "driver/ledc.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "esp_err.h"
// ---------- Pin Mapping ----------
#define PIN_DHT GPIO_NUM_23 // DHT22 data
#define PIN_DS18B20 GPIO_NUM_4 // 1-Wire DQ
#define PIN_TRIG GPIO_NUM_5 // HC-SR04 TRIG
#define PIN_ECHO GPIO_NUM_18 // HC-SR04 ECHO
#define ADC_PH_CH ADC1_CHANNEL_4 // GPIO32
#define ADC_SOIL_CH ADC1_CHANNEL_7 // GPIO35
#define ADC_LDR_CH ADC1_CHANNEL_6 // GPIO34
#define PIN_LDR_D0 GPIO_NUM_33
#define PIN_RELAY_PUMP GPIO_NUM_25
#define PIN_RELAY_GROW GPIO_NUM_26
#define PIN_RELAY_FAN GPIO_NUM_27
#define RELAY_ACTIVE_LOW 1
#define PIN_SERVO GPIO_NUM_13
#define PIN_BUZZER GPIO_NUM_14
// ---------- Config & thresholds (ubah sesuai kebutuhan) ----------
typedef struct {
float Tmax_air; // °C
float Hmax; // %RH
float L_min_adc; // threshold ADC LDR (0..4095)
float pH_min, pH_max; // target pH
float level_min_cm; // minimal level (HC-SR04 jarak >= ini => rendah)
float soil_wet_min; // % untuk mulai siram
float soil_wet_max; // % untuk berhenti siram
int pump_on_sec; // mode interval: ON detik
int pump_off_min; // mode interval: OFF menit
bool pump_mode_soil; // true: mode soil, false: interval
} app_config_t;
static app_config_t CFG = {
.Tmax_air = 30.0f,
.Hmax = 80.0f,
.L_min_adc = 300.0f, // sesuaikan dengan kalibrasi
.pH_min = 5.8f, .pH_max = 6.2f,
.level_min_cm = 18.0f, // contoh: 18 cm dianggap rendah
.soil_wet_min = 60.0f,
.soil_wet_max = 80.0f,
.pump_on_sec = 30,
.pump_off_min = 5,
.pump_mode_soil = false // awal: interval
};
// ---------- Shared State ----------
typedef struct {
// Sensors
float t_air, rh; // DHT22
float t_water; // DS18B20
float ph; // pH mapped from ADC32
float soil_pct; // Soil from ADC35 (0..100%)
int ldr_adc; // ADC34 (0..4095)
int ldr_d0; // 0/1
float level_cm; // HC-SR04 (cm)
// Health
bool dht_ok, ds_ok, hcsr_ok;
// Actuators
bool pump_on;
bool grow_on;
bool fan_on;
int servo_deg; // 0..180
bool alarm_low_level;
bool alarm_overheat;
} app_state_t;
static app_state_t ST; // global state
static SemaphoreHandle_t st_mutex; // protect ST
// ---------- Helpers ----------
static inline uint64_t usec_now() { return esp_timer_get_time(); }
static inline uint64_t msec_now() { return usec_now()/1000ULL; }
static void st_lock() { xSemaphoreTake(st_mutex, portMAX_DELAY); }
static void st_unlock() { xSemaphoreGive(st_mutex); }
// ---------- Relay control (dengan active-low & min on/off) ----------
typedef struct {
gpio_num_t pin;
bool active_low;
bool state; // logical state (ON/OFF)
uint64_t last_change_ms;
uint32_t min_on_ms;
uint32_t min_off_ms;
} relay_t;
static relay_t R_PUMP = {PIN_RELAY_PUMP, RELAY_ACTIVE_LOW, false, 0, 10000, 10000};
static relay_t R_GROW = {PIN_RELAY_GROW, RELAY_ACTIVE_LOW, false, 0, 5000, 5000};
static relay_t R_FAN = {PIN_RELAY_FAN, RELAY_ACTIVE_LOW, false, 0, 5000, 5000};
static void relay_apply(relay_t *r, bool on) {
uint64_t now = msec_now();
if (on != r->state) {
uint32_t dw = (r->state ? r->min_on_ms : r->min_off_ms);
if (now - r->last_change_ms < dw) return; // tahan min ON/OFF
r->state = on;
r->last_change_ms = now;
int level = r->active_low ? (on ? 0:1) : (on ? 1:0);
gpio_set_level(r->pin, level);
}
}
static void relay_init(relay_t *r) {
gpio_config_t io = {
.pin_bit_mask = (1ULL<<r->pin),
.mode = GPIO_MODE_OUTPUT,
.pull_down_en = 0, .pull_up_en = 0, .intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io);
r->state = false;
r->last_change_ms = msec_now();
gpio_set_level(r->pin, r->active_low ? 1:0); // OFF
}
// ---------- Servo (LEDC 50Hz) ----------
static void servo_init() {
ledc_timer_config_t t = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_0,
.duty_resolution = LEDC_TIMER_16_BIT,
.freq_hz = 50,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&t);
ledc_channel_config_t ch = {
.gpio_num = PIN_SERVO,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_0,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = LEDC_TIMER_0,
.duty = 0, .hpoint = 0
};
ledc_channel_config(&ch);
}
static void servo_write_deg(int deg) {
if (deg < 0) deg = 0; if (deg > 180) deg = 180;
// 1.0ms (0°) .. 2.0ms (180°)
float pulse_us = 1000.0f + (deg/180.0f)*1000.0f;
float duty = pulse_us / 20000.0f; // 20ms period
uint32_t maxd = (1<<16) - 1;
uint32_t duty_val = (uint32_t)(duty * maxd);
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty_val);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}
// ---------- Buzzer (LEDC tone) ----------
static void buzzer_init() {
ledc_timer_config_t t = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER_1,
.duty_resolution = LEDC_TIMER_10_BIT,
.freq_hz = 2000,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&t);
ledc_channel_config_t ch = {
.gpio_num = PIN_BUZZER,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL_1,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = LEDC_TIMER_1,
.duty = 0, .hpoint = 0
};
ledc_channel_config(&ch);
}
static void buzzer_on() { ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1, 512); ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1); }
static void buzzer_off() { ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1, 0); ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1); }
// ---------- ADC ----------
static void adc_init_all() {
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(ADC_PH_CH, ADC_ATTEN_DB_11);
adc1_config_channel_atten(ADC_SOIL_CH, ADC_ATTEN_DB_11);
adc1_config_channel_atten(ADC_LDR_CH, ADC_ATTEN_DB_11);
}
// helper: ADC → volt (kasar, cukup untuk simulasi)
static float adc_to_volt(int raw) {
// DB_11 ~ full-scale ~ 3.3V (aproksimasi)
return (3.3f * (float)raw) / 4095.0f;
}
// ---------- GPIO init ----------
static void inputs_init() {
gpio_config_t io = {
.pin_bit_mask = (1ULL<<PIN_LDR_D0) | (1ULL<<PIN_ECHO),
.mode = GPIO_MODE_INPUT,
.pull_up_en = 0, .pull_down_en = 0, .intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io);
gpio_config_t o = {
.pin_bit_mask = (1ULL<<PIN_TRIG),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = 0, .pull_down_en = 0, .intr_type = GPIO_INTR_DISABLE
};
gpio_config(&o);
gpio_set_level(PIN_TRIG, 0);
}
// ---------- DHT22 (bit-bang sederhana) ----------
static bool dht22_read(gpio_num_t pin, float *t, float *h) {
// protokol basic; gunakan timeout ketat
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
gpio_set_level(pin, 0);
ets_delay_us(20000); // >= 18ms
gpio_set_level(pin, 1);
ets_delay_us(40);
gpio_set_direction(pin, GPIO_MODE_INPUT);
// tunggu response: low ~80us, high ~80us
uint64_t timeout = usec_now() + 200;
while (gpio_get_level(pin) == 1) if (usec_now()>timeout) return false;
timeout = usec_now() + 200;
while (gpio_get_level(pin) == 0) if (usec_now()>timeout) return false;
timeout = usec_now() + 200;
while (gpio_get_level(pin) == 1) if (usec_now()>timeout) return false;
uint8_t data[5] = {0};
for (int i=0;i<40;i++){
// setiap bit: low ~50us
timeout = usec_now() + 100;
while (gpio_get_level(pin)==0) if (usec_now()>timeout) return false;
// ukur high width
uint64_t start = usec_now();
timeout = start + 120;
while (gpio_get_level(pin)==1) if (usec_now()>timeout) return false;
uint32_t high = (uint32_t)(usec_now() - start);
// > ~40us => '1', else '0'
data[i/8] <<= 1;
if (high > 45) data[i/8] |= 1;
}
uint8_t sum = data[0]+data[1]+data[2]+data[3];
if ((sum & 0xFF) != data[4]) return false;
int rh_i = data[0], rh_d = data[1];
int t_i = data[2] & 0x7F, t_d = data[3];
bool neg = data[2] & 0x80;
float rhv = rh_i + rh_d/10.0f;
float tv = t_i + t_d/10.0f;
if (neg) tv = -tv;
if (h) *h = rhv;
if (t) *t = tv;
return true;
}
// ---------- 1-Wire / DS18B20 (minimal) ----------
static void onewire_write_bit(gpio_num_t pin, int b) {
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
gpio_set_level(pin, 0);
ets_delay_us(b ? 6 : 60);
gpio_set_direction(pin, GPIO_MODE_INPUT);
ets_delay_us(b ? 64 : 10);
}
static int onewire_read_bit(gpio_num_t pin) {
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
gpio_set_level(pin, 0);
ets_delay_us(6);
gpio_set_direction(pin, GPIO_MODE_INPUT);
ets_delay_us(9);
int r = gpio_get_level(pin);
ets_delay_us(55);
return r;
}
static void onewire_write_byte(gpio_num_t pin, uint8_t v){
for (int i=0;i<8;i++) onewire_write_bit(pin, (v>>i)&1);
}
static uint8_t onewire_read_byte(gpio_num_t pin){
uint8_t v=0;
for (int i=0;i<8;i++) v |= (onewire_read_bit(pin)<<i);
return v;
}
static bool onewire_reset(gpio_num_t pin){
gpio_set_direction(pin, GPIO_MODE_OUTPUT);
gpio_set_level(pin, 0);
ets_delay_us(480);
gpio_set_direction(pin, GPIO_MODE_INPUT);
ets_delay_us(70);
bool presence = (gpio_get_level(pin)==0);
ets_delay_us(410);
return presence;
}
static bool ds18b20_read_celsius(gpio_num_t pin, float *tout) {
if (!onewire_reset(pin)) return false;
onewire_write_byte(pin, 0xCC); // SKIP ROM
onewire_write_byte(pin, 0x44); // CONVERT T
// tunggu selesai (maks 750ms @12-bit). Kita poll ready bit ~750ms.
uint64_t start = usec_now();
while (usec_now() - start < 800000) {
if (onewire_read_bit(pin)) break;
ets_delay_us(10000);
}
if (!onewire_reset(pin)) return false;
onewire_write_byte(pin, 0xCC); // SKIP ROM
onewire_write_byte(pin, 0xBE); // READ SCRATCHPAD
uint8_t d0 = onewire_read_byte(pin);
uint8_t d1 = onewire_read_byte(pin);
// skip remaining bytes
for (int i=0;i<7;i++) (void)onewire_read_byte(pin);
int16_t raw = (int16_t)((d1<<8) | d0);
float c = raw / 16.0f;
if (tout) *tout = c;
return true;
}
// ---------- HC-SR04 ----------
static bool hcsr04_read_cm(float *cm) {
// 10us TRIG pulse, measure ECHO high width
gpio_set_level(PIN_TRIG, 0);
ets_delay_us(2);
gpio_set_level(PIN_TRIG, 1);
ets_delay_us(10);
gpio_set_level(PIN_TRIG, 0);
uint64_t t0 = usec_now();
while (gpio_get_level(PIN_ECHO)==0) { if (usec_now()-t0 > 30000) return false; } // wait rise
uint64_t start = usec_now();
while (gpio_get_level(PIN_ECHO)==1) { if (usec_now()-start > 30000) return false; } // wait fall
uint32_t us = (uint32_t)(usec_now() - start);
float d = us / 58.0f; // cm approx
if (cm) *cm = d;
return true;
}
// ---------- SENSOR TASKS ----------
static void task_sensors_fast(void *arg) {
// 250 ms: HC-SR04; baca D0
for (;;) {
float level; bool ok = hcsr04_read_cm(&level);
st_lock();
ST.hcsr_ok = ok;
if (ok) ST.level_cm = level;
ST.ldr_d0 = gpio_get_level(PIN_LDR_D0);
st_unlock();
vTaskDelay(pdMS_TO_TICKS(250));
}
}
static void task_sensors_slow(void *arg) {
uint64_t last_dht = 0, last_adc = 0, last_ds = 0;
for (;;) {
uint64_t now = msec_now();
// DHT22 setiap 2000 ms
if (now - last_dht >= 2000) {
float t, h; bool ok = dht22_read(PIN_DHT, &t, &h);
st_lock();
ST.dht_ok = ok;
if (ok) { ST.t_air = t; ST.rh = h; }
st_unlock();
last_dht = now;
}
// DS18B20 setiap 1000 ms
if (now - last_ds >= 1000) {
float tw; bool ok = ds18b20_read_celsius(PIN_DS18B20, &tw);
st_lock();
ST.ds_ok = ok;
if (ok) ST.t_water = tw;
st_unlock();
last_ds = now;
}
// ADC pH/Soil/LDR setiap 2000 ms
if (now - last_adc >= 2000) {
int raw_ph = adc1_get_raw(ADC_PH_CH);
int raw_soil = adc1_get_raw(ADC_SOIL_CH);
int raw_ldr = adc1_get_raw(ADC_LDR_CH);
float v_ph = adc_to_volt(raw_ph);
float v_soil = adc_to_volt(raw_soil);
// pH mapping sederhana (sesuaikan dengan chip custom vMin/vMax)
const float vMin = 0.0f, vMax = 3.0f; // default chip
float nrm = (v_ph - vMin) / (vMax - vMin);
if (nrm < 0) nrm = 0; if (nrm > 1) nrm = 1;
float ph = nrm * 14.0f;
// Soil 0..100% (aproksimasi)
float soil = (v_soil / 3.3f) * 100.0f;
if (soil<0) soil=0; if (soil>100) soil=100;
st_lock();
ST.ph = ph;
ST.soil_pct = soil;
ST.ldr_adc = raw_ldr;
st_unlock();
last_adc = now;
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}
// ---------- CONTROL TASK ----------
static void task_control(void *arg) {
// Inisialisasi perangkat output
relay_init(&R_PUMP); relay_init(&R_GROW); relay_init(&R_FAN);
servo_init(); buzzer_init();
// Scheduler pump (interval)
uint64_t pump_next_on_ms = msec_now();
uint64_t pump_on_until_ms = 0;
for (;;) {
// Snapshot state aman (atomic read dengan mutex singkat)
st_lock();
float t_air = ST.t_air, rh = ST.rh, t_water = ST.t_water;
float ph = ST.ph, soil = ST.soil_pct;
int ldr_adc = ST.ldr_adc, ldr_d0 = ST.ldr_d0;
float level = ST.level_cm;
bool dht_ok = ST.dht_ok, ds_ok = ST.ds_ok;
st_unlock();
// ---------- SAFETY ----------
bool alarm_low_level = (level >= CFG.level_min_cm); // jarak besar = air rendah
bool alarm_overheat = (t_air >= (CFG.Tmax_air + 5.0f));
if (alarm_low_level) relay_apply(&R_PUMP, false);
if (alarm_overheat) { relay_apply(&R_FAN, true); relay_apply(&R_GROW, false); }
// ---------- MANUAL? (skip dulu: belum ada input manual) ----------
// ---------- AUTO CONTROL ----------
// PUMP
if (!alarm_low_level) {
if (CFG.pump_mode_soil) {
// mode Soil: ON bila < min, OFF bila >= max
if (soil < CFG.soil_wet_min) relay_apply(&R_PUMP, true);
else if (soil >= CFG.soil_wet_max) relay_apply(&R_PUMP, false);
} else {
// mode Interval
uint64_t now = msec_now();
if (now >= pump_next_on_ms) {
relay_apply(&R_PUMP, true);
pump_on_until_ms = now + (uint64_t)CFG.pump_on_sec*1000ULL;
pump_next_on_ms = now + (uint64_t)CFG.pump_off_min*60000ULL;
}
if (R_PUMP.state && msec_now() >= pump_on_until_ms) {
relay_apply(&R_PUMP, false);
}
}
}
// GROW LIGHT
if (!alarm_overheat) {
bool enough_light = (ldr_d0 == 1) || (ldr_adc >= (int)CFG.L_min_adc);
// contoh sederhana: ON jika kurang terang, OFF jika cukup terang
if (!enough_light) relay_apply(&R_GROW, true);
else relay_apply(&R_GROW, false);
}
// FAN: panas/ lembap
if ( (t_air >= CFG.Tmax_air) || (rh >= CFG.Hmax) ) {
relay_apply(&R_FAN, true);
} else if ( (t_air <= CFG.Tmax_air-1) && (rh <= CFG.Hmax-5) ) {
relay_apply(&R_FAN, false);
}
// SERVO demo: flush jika pH tinggi
if (ph >= 6.8f) servo_write_deg(90);
else servo_write_deg(0);
// BUZZER pattern sederhana
static uint64_t last_beep = 0;
uint64_t now = msec_now();
if (alarm_low_level) {
// 3 beep pendek tiap 30s
if (now - last_beep > 30000) last_beep = now;
uint32_t phase = (uint32_t)(now - last_beep);
// ON di 0..150ms, 400..550ms, 800..950ms
bool on = (phase<150) || (phase>400 && phase<550) || (phase>800 && phase<950);
on ? buzzer_on() : buzzer_off();
} else if (alarm_overheat) {
// 1 beep panjang tiap 10s
if (now - last_beep > 10000) last_beep = now;
bool on = (now - last_beep) < 1000;
on ? buzzer_on() : buzzer_off();
} else {
buzzer_off();
}
// tulis kembali state yang berubah
st_lock();
ST.pump_on = R_PUMP.state;
ST.grow_on = R_GROW.state;
ST.fan_on = R_FAN.state;
ST.servo_deg = (ph >= 6.8f) ? 90 : 0;
ST.alarm_low_level = alarm_low_level;
ST.alarm_overheat = alarm_overheat;
st_unlock();
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// ---------- LOGGER TASK ----------
static void task_logger(void *arg) {
for (;;) {
st_lock();
printf("[SNAPSHOT] Air=%.1fC %.0f%% Water=%.2fC pH=%.2f Soil=%.0f%% LDR=%d/D%d Level=%.1fcm | PUMP=%d GROW=%d FAN=%d SERVO=%d | ALM(level=%d,heat=%d) | OK(DHT=%d,DS=%d,HCSR=%d)\n",
ST.t_air, ST.rh, ST.t_water, ST.ph, ST.soil_pct, ST.ldr_adc, ST.ldr_d0, ST.level_cm,
ST.pump_on, ST.grow_on, ST.fan_on, ST.servo_deg,
ST.alarm_low_level, ST.alarm_overheat,
ST.dht_ok, ST.ds_ok, ST.hcsr_ok);
st_unlock();
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
// ---------- APP MAIN ----------
void app_main(void) {
// init mutex & defaults
st_mutex = xSemaphoreCreateMutex();
memset(&ST, 0, sizeof(ST));
// init IO
inputs_init();
adc_init_all();
relay_init(&R_PUMP); relay_init(&R_GROW); relay_init(&R_FAN);
servo_init(); buzzer_init();
// start tasks
xTaskCreatePinnedToCore(task_sensors_fast, "sens_fast", 4096, NULL, 7, NULL, 1);
xTaskCreatePinnedToCore(task_sensors_slow, "sens_slow", 6144, NULL, 6, NULL, 1);
xTaskCreatePinnedToCore(task_control, "control", 6144, NULL, 8, NULL, 0);
xTaskCreatePinnedToCore(task_logger, "logger", 4096, NULL, 3, NULL, 0);
}
pump
grow
fan