// ============================================================
// AUTOMATED PLANT WATERING SYSTEM – Hydroponic + IoT
// Student : Cisca Bougard | 47655232 | EIP3702
// Mentor : Prof. B.B. Monchusi | UNISA
//
// Platform : Wokwi – ESP32 DevKit V1
// Simulation : All sensors + fuzzy logic + LCD + pump LEDs
// ============================================================
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <DHT.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <WiFi.h>
#include <WebServer.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
// ── Pin definitions ──────────────────────────────────────────
#define DHT_PIN 4
#define DHT_TYPE DHT22
#define DS18B20_PIN 15
#define PH_PIN 34
#define EC_PIN 35
#define TRIG_PIN 5
#define ECHO_PIN 18
#define PUMP_IRRIGATE 25
#define PUMP_PH_UP 26
#define PUMP_PH_DOWN 27
#define PUMP_NUTRIENT 32
#define LED_OK 13
#define LED_ALERT 2
// ── Setpoints ────────────────────────────────────────────────
#define PH_MIN 5.5f
#define PH_MAX 6.5f
#define EC_MIN 1.2f
#define EC_MAX 2.4f
#define LEVEL_SAFE 15.0f
#define CYCLE_MS 10000UL
#define IRRIGATE_MS 5000UL
#define REPORT_INTERVAL 60000UL // 1 minute
// ── Objects ───────────────────────────────────────────────────
LiquidCrystal_I2C lcd(0x27, 16, 2);
DHT dht(DHT_PIN, DHT_TYPE);
OneWire ow(DS18B20_PIN);
DallasTemperature waterTempSensor(&ow);
// ── State ─────────────────────────────────────────────────────
float ph = 6.0, ec = 1.8, wt = 20.0, at = 23.0, hum = 65.0;
float levelCm = 8.0;
bool systemOK = true;
bool irrigating = false;
// FIX 1: All alert flags and timing variables declared together cleanly
bool alertLevelSent = false;
bool alertPhSent = false;
bool alertEcSent = false;
unsigned long lastReport = 0;
unsigned long lastTelegramTime = 0; // FIX 3: was referenced but never declared
int cycleNum = 0;
int lcdPage = 0;
unsigned long lastCycle = 0;
unsigned long irrigateStart = 0;
unsigned long lastLcdSwitch = 0;
// ── WiFi / Web Server ─────────────────────────────────────────
const char* ssid = "Wokwi-GUEST";
const char* password = "";
WebServer server(80);
// ── Telegram Settings ─────────────────────────────────────────
String botToken = "8764021108:AAFnamV_UHiXsKsKRGuT26Vp1g0T4fsAQ";
String chatID = "1402039912";
// ── Sensor reads ──────────────────────────────────────────────
float readPH() { return 3.0f + (analogRead(PH_PIN) / 4095.0f) * 6.0f; }
float readEC() { return (analogRead(EC_PIN) / 4095.0f) * 5.0f; }
float readLevel() {
digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long t = pulseIn(ECHO_PIN, HIGH, 30000UL);
return (t == 0) ? 99.0f : (t * 0.0343f) / 2.0f;
}
// ── Fuzzy logic ───────────────────────────────────────────────
float triMF(float x, float a, float b, float c) {
if (x<=a||x>=c) return 0;
return x<b ? (x-a)/(b-a) : (c-x)/(c-b);
}
float trapMF(float x, float a, float b, float c, float d) {
if (x<=a||x>=d) return 0;
if (x>=b&&x<=c) return 1;
return x<b ? (x-a)/(b-a) : (d-x)/(d-c);
}
float fuzzyDemand() {
float dp = abs(ph - 6.0f);
float ph_ok = triMF(dp,0,0,0.5f), ph_off=triMF(dp,0.3f,0.7f,1.2f), ph_bad=trapMF(dp,0.8f,1.2f,3,3);
float ec_lo = trapMF(ec,0,0,1.0f,1.4f), ec_ok=triMF(ec,1,1.8f,2.6f), ec_hi=trapMF(ec,2.2f,2.6f,5,5);
float r1=min(ph_ok,ec_ok), r2=ph_off, r3=ec_lo, r4=max(ph_bad,ec_hi);
float num=r1*0.10f+r2*0.45f+r3*0.60f+r4*0.90f;
float den=r1+r2+r3+r4;
return (den<0.001f) ? 0 : constrain(num/den,0,1);
}
// ── LCD update ────────────────────────────────────────────────
void updateLCD() {
if (millis() - lastLcdSwitch < 3000) return;
lastLcdSwitch = millis();
lcd.clear();
if (lcdPage == 0) {
lcd.setCursor(0, 0);
lcd.print("pH:");
lcd.print(ph, 1);
lcd.print(ph < PH_MIN ? " LOW " : ph > PH_MAX ? " HIGH" : " OK ");
lcd.setCursor(0, 1);
lcd.print("EC:");
lcd.print(ec, 1);
lcd.print(ec < EC_MIN ? " LOW " : ec > EC_MAX ? " HIGH" : " OK ");
} else if (lcdPage == 1) {
lcd.setCursor(0, 0);
lcd.print("WT:");
lcd.print(wt, 1);
lcd.print("C AT:");
lcd.print(at, 0);
lcd.setCursor(0, 1);
lcd.print("Lvl:");
lcd.print(levelCm, 0);
lcd.print("cm ");
lcd.print(systemOK ? "SAFE" : "LOW!");
} else {
lcd.setCursor(0, 0);
lcd.print("IRR:");
lcd.print(digitalRead(PUMP_IRRIGATE) ? "ON " : "off ");
lcd.print("pHU:");
lcd.print(digitalRead(PUMP_PH_UP) ? "ON" : "of");
lcd.setCursor(0, 1);
lcd.print("pHD:");
lcd.print(digitalRead(PUMP_PH_DOWN) ? "ON " : "off ");
lcd.print("NUT:");
lcd.print(digitalRead(PUMP_NUTRIENT) ? "ON" : "of");
}
lcdPage = (lcdPage + 1) % 3;
}
// ── Serial telemetry ──────────────────────────────────────────
void printTelemetry() {
float dem = fuzzyDemand();
Serial.println(F("\n=========================================="));
Serial.println(F(" AUTOMATED HYDROPONIC WATERING SYSTEM"));
Serial.println(F(" Cisca Bougard | 47655232 | UNISA"));
Serial.println(F("------------------------------------------"));
Serial.print(F(" Cycle #")); Serial.println(cycleNum);
Serial.println(F("------------------------------------------"));
Serial.print(F(" pH : ")); Serial.print(ph, 2);
Serial.println(ph<PH_MIN?" << TOO LOW -> pH-Up ON":ph>PH_MAX?" << TOO HIGH -> pH-Down ON":" [OK 5.5-6.5]");
Serial.print(F(" EC (mS/cm) : ")); Serial.print(ec, 2);
Serial.println(ec<EC_MIN?" << LOW -> Nutrient ON":ec>EC_MAX?" << HIGH -> Alert":" [OK 1.2-2.4]");
Serial.print(F(" Water temp : ")); Serial.print(wt, 1); Serial.println(F(" C"));
Serial.print(F(" Air temp : ")); Serial.print(at, 1); Serial.println(F(" C"));
Serial.print(F(" Humidity : ")); Serial.print(hum, 0); Serial.println(F(" %"));
Serial.print(F(" Level : ")); Serial.print(levelCm, 1);
Serial.println(systemOK ? F(" cm [SAFE]") : F(" cm [LOW - ALERT]"));
Serial.print(F(" Fuzzy demand: ")); Serial.print(dem, 2);
Serial.println(dem > 0.35f ? F(" -> IRRIGATE") : F(" -> hold"));
Serial.println(F("------------------------------------------"));
Serial.print(F(" Irrigation : ")); Serial.println(digitalRead(PUMP_IRRIGATE) ? F("ON <<") : F("off"));
Serial.print(F(" pH-Up pump : ")); Serial.println(digitalRead(PUMP_PH_UP) ? F("ON <<") : F("off"));
Serial.print(F(" pH-Dn pump : ")); Serial.println(digitalRead(PUMP_PH_DOWN) ? F("ON <<") : F("off"));
Serial.print(F(" Nutrient : ")); Serial.println(digitalRead(PUMP_NUTRIENT) ? F("ON <<") : F("off"));
Serial.print(F(" System : ")); Serial.println(systemOK ? F("OK") : F("ALERT"));
Serial.println(F("=========================================="));
}
// ── Web page builder ──────────────────────────────────────────
String makePage() {
String html = "<html><head><meta http-equiv='refresh' content='3'/>";
html += "<style>body{font-family:Arial;text-align:center;} h1{color:#2E8B57;}</style>";
html += "</head><body>";
html += "<h1>Hydroponics IoT System</h1>";
html += "<p><b>pH:</b> " + String(ph,2) + "</p>";
html += "<p><b>EC:</b> " + String(ec,2) + "</p>";
html += "<p><b>Water Temp:</b> " + String(wt,1) + " C</p>";
html += "<p><b>Air Temp:</b> " + String(at,1) + " C</p>";
html += "<p><b>Humidity:</b> " + String(hum,0) + " %</p>";
html += "<p><b>Level:</b> " + String(levelCm,1) + " cm</p>";
html += "<h3>Status: ";
html += systemOK ? "OK" : "ALERT";
html += "</h3>";
html += "<h3>Pumps</h3>";
html += "<p>Irrigation: " + String(digitalRead(PUMP_IRRIGATE) ? "ON" : "OFF") + "</p>";
html += "<p>pH Up: " + String(digitalRead(PUMP_PH_UP) ? "ON" : "OFF") + "</p>";
html += "<p>pH Down: " + String(digitalRead(PUMP_PH_DOWN) ? "ON" : "OFF") + "</p>";
html += "<p>Nutrient: " + String(digitalRead(PUMP_NUTRIENT) ? "ON" : "OFF") + "</p>";
html += "</body></html>";
return html;
}
// ✅ ADD TELEGRAM FUNCTION HERE (OUTSIDE)
void sendTelegram(String message) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
// URL encode
message.replace("%", "%25");
message.replace(" ", "%20");
message.replace("\n", "%0A");
message.replace("#", "%23");
String url =
"https://api.telegram.org/bot" + botToken +
"/sendMessage?chat_id=" + chatID +
"&text=" + message;
Serial.println(url);
http.begin(client, url);
int httpCode = http.GET();
Serial.print("Telegram HTTP Code: ");
Serial.println(httpCode);
if (httpCode > 0) {
String payload = http.getString();
Serial.println(payload);
}
else {
Serial.println("Telegram request failed");
}
http.end();
}
// ✅ COPY THE MISSING FUNCTION HERE
void sendFullReport() {
String msg = "📊 Hydroponic Report\n";
msg += "-------------------\n";
msg += "pH: " + String(ph, 2) + "\n";
msg += "EC: " + String(ec, 2) + " mS/cm\n";
msg += "Water Temp: " + String(wt, 1) + " C\n";
msg += "Air Temp: " + String(at, 1) + " C\n";
msg += "Humidity: " + String(hum, 0) + " %\n";
msg += "Water Level: " + String(levelCm, 1) + " cm\n";
msg += systemOK ? "Status: OK ✅\n" : "Status: ALERT ⚠️\n";
sendTelegram(msg);
}
// ── Setup ─────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
// Output pins
int outPins[] = {PUMP_IRRIGATE, PUMP_PH_UP, PUMP_PH_DOWN,
PUMP_NUTRIENT, LED_OK, LED_ALERT, TRIG_PIN};
for (int p : outPins) { pinMode(p, OUTPUT); digitalWrite(p, LOW); }
pinMode(ECHO_PIN, INPUT);
// Sensors
dht.begin();
waterTempSensor.begin();
// LCD init
Wire.begin(21, 22);
delay(100);
lcd.init();
lcd.backlight();
// Boot screen
lcd.clear();
lcd.setCursor(0, 0); lcd.print(" HydroponicSystem ");
lcd.setCursor(0, 1); lcd.print(" Cisca 47655232 ");
delay(2000);
lcd.clear();
lcd.setCursor(0, 0); lcd.print("EIP3702 ");
lcd.setCursor(0, 1); lcd.print("UNISA Booting...");
delay(2000);
lcd.clear();
lcd.setCursor(0, 0); lcd.print("System Ready! ");
lcd.setCursor(0, 1); lcd.print("Starting in 10s ");
Serial.println(F("\n=========================================="));
Serial.println(F(" HYDROPONIC SYSTEM BOOT"));
Serial.println(F(" Cisca Bougard | 47655232 | UNISA"));
Serial.println(F(" EIP3702"));
Serial.println(F("=========================================="));
// Flash all LEDs to confirm wiring
int allLEDs[] = {LED_OK, LED_ALERT, PUMP_IRRIGATE,
PUMP_PH_UP, PUMP_PH_DOWN, PUMP_NUTRIENT};
for (int p : allLEDs) digitalWrite(p, HIGH);
delay(600);
for (int p : allLEDs) digitalWrite(p, LOW);
digitalWrite(LED_OK, HIGH); // green stays ON
// WiFi connection
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected!");
Serial.println(WiFi.localIP());
sendTelegram("✅ Hydroponic system is ONLINE!");
// Web server
server.on("/", []() {
server.send(200, "text/html", makePage());
});
server.begin();
Serial.println("Web server started!");
lastCycle = millis();
lastLcdSwitch = millis();
lastReport = millis(); // FIX 4: initialise so first report isn't immediate
lastTelegramTime = millis();
}
// ── Main loop ─────────────────────────────────────────────────
void loop() {
unsigned long now = millis();
server.handleClient();
// ⏱️ Periodic Telegram report
if (now - lastReport > REPORT_INTERVAL) {
sendFullReport();
lastReport = now;
}
// Sensor + control cycle every CYCLE_MS
if (now - lastCycle >= CYCLE_MS) {
lastCycle = now;
cycleNum++;
// Read sensors
hum = dht.readHumidity();
at = dht.readTemperature();
if (isnan(hum) || isnan(at)) { hum = 65.0f; at = 23.0f; }
waterTempSensor.requestTemperatures();
wt = waterTempSensor.getTempCByIndex(0);
if (wt == DEVICE_DISCONNECTED_C) wt = 20.0f;
ph = readPH();
ec = readEC();
levelCm = readLevel();
systemOK = (levelCm < LEVEL_SAFE);
// Status LEDs
digitalWrite(LED_OK, systemOK ? HIGH : LOW);
digitalWrite(LED_ALERT, systemOK ? LOW : HIGH);
if (!systemOK) {
// Safety interlock – reservoir low, shut all pumps
digitalWrite(PUMP_PH_UP, LOW);
digitalWrite(PUMP_PH_DOWN, LOW);
digitalWrite(PUMP_NUTRIENT, LOW);
digitalWrite(PUMP_IRRIGATE, LOW);
irrigating = false;
} else {
// pH control
if (ph < PH_MIN) { digitalWrite(PUMP_PH_UP, HIGH); digitalWrite(PUMP_PH_DOWN, LOW); }
else if (ph > PH_MAX) { digitalWrite(PUMP_PH_UP, LOW); digitalWrite(PUMP_PH_DOWN, HIGH);}
else { digitalWrite(PUMP_PH_UP, LOW); digitalWrite(PUMP_PH_DOWN, LOW); }
// EC / nutrient control
digitalWrite(PUMP_NUTRIENT, ec < EC_MIN ? HIGH : LOW);
// Fuzzy irrigation
float dem = fuzzyDemand();
if (!irrigating && dem > 0.35f) {
digitalWrite(PUMP_IRRIGATE, HIGH);
irrigating = true;
irrigateStart = now;
Serial.print(F("[CTRL] Irrigation ON demand="));
Serial.println(dem, 2);
}
}
printTelemetry();
}
// Irrigation auto-off
if (irrigating && (millis() - irrigateStart >= IRRIGATE_MS)) {
digitalWrite(PUMP_IRRIGATE, LOW);
irrigating = false;
Serial.println(F("[CTRL] Irrigation OFF (5s cycle complete)\n"));
}
// LCD cycles every 3 seconds independently
updateLCD();
delay(50);
}