#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <DHT.h>
#include <time.h>
#define DHT_PIN 27
#define FLOW_SENSOR_PIN 35
#define WET_SENSOR_PIN 34
#define WATER_VALVE_PIN 5
#define DRAIN_VALVE_PIN 4
const char ssid[] = "Wokwi-GUEST";
const char password[] = "";
const char mqtt_server[] = "mqtt3.thingspeak.com";
const int mqtt_port = 8883;
const char mqtt_username[] = "HgIvJQoWGSozCx0gEjQhNjE";
const char mqtt_password[] = "7NKH307OqDUuN+FRMrEsUk4p";
const char mqtt_client_id[] = "HgIvJQoWGSozCx0gEjQhNjE";
const char channel_id[] = "3245801";
const char write_api_key[] = "G3BZKMFUZZ9O6C5E";
const char command_channel_id[] = "3245803";
const char command_read_api_key[] = "IHQPZG8XAYRR6OU1";
String topic_publish = String("channels/") + channel_id + "/publish";
String topic_subscribe = String("channels/") + command_channel_id + "/subscribe";
#define DHT_TYPE DHT22
DHT dht(DHT_PIN, DHT_TYPE);
// Flow sensor variables
volatile unsigned int flowPulseCount = 0;
float flowRate = 0.0;
float currentWaterVolume = 0.0;
unsigned long oldTime = 0;
const float calibrationFactor = 5.5; // Pulses per liter for YF-S401
// Control variables
bool autoMode = true;
float targetWaterVolume = 5.0; // liters
int scheduledHour = 9; // Default watering hour (9 AM)
bool manualWaterValve = false;
bool manualDrainValve = false;
bool wateringActive = false;
bool overflowDetected = false;
// PID variables
float Kp = 2.0;
float Ki = 0.5;
float Kd = 0.1;
float previousError = 0.0;
float integral = 0.0;
unsigned long lastPIDTime = 0;
// Sensor data
float temperature = 0.0;
float humidity = 0.0;
bool wetSensorState = false;
// Timing
unsigned long lastSensorRead = 0;
unsigned long lastMQTTPublish = 0;
const unsigned long sensorInterval = 2000; // 2 seconds
const unsigned long mqttInterval = 15000; // 15 seconds
// WiFi and MQTT clients
WiFiClientSecure espClient;
PubSubClient client(espClient);
// ThingSpeak uses DigiCert's CA
const char root_ca[] = R"EOF(
-----BEGIN CERTIFICATE-----
MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN
MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT
HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN
NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs
IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi
MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+
ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0
2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp
wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM
pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD
nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po
sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx
Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd
Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX
KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe
XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL
tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv
TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN
AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw
GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H
PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF
O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ
REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik
AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv
/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+
p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw
MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF
qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK
ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+
-----END CERTIFICATE-----
)EOF";
void IRAM_ATTR flowPulseCounter();
void setupWiFi();
void reconnectMQTT();
void mqttCallback(char* topic, byte* payload, unsigned int length);
void readSensors();
void calculateFlowRate();
void pidControl();
void checkSchedule();
void handleOverflow();
void publishData();
void controlValves();
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("ESP32 Dragonfruit Plant Care System");
// Initialize pins
pinMode(WATER_VALVE_PIN, OUTPUT);
pinMode(DRAIN_VALVE_PIN, OUTPUT);
pinMode(FLOW_SENSOR_PIN, INPUT_PULLUP);
pinMode(WET_SENSOR_PIN, INPUT);
// Ensure valves are closed at start
digitalWrite(WATER_VALVE_PIN, LOW);
digitalWrite(DRAIN_VALVE_PIN, LOW);
// Initialize DHT sensor
dht.begin();
// Attach interrupt for flow sensor
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), flowPulseCounter, FALLING);
// Connect to WiFi
setupWiFi();
// Configure secure client
espClient.setCACert(root_ca);
// Setup MQTT
client.setServer(mqtt_server, mqtt_port);
client.setCallback(mqttCallback);
client.setKeepAlive(60);
client.setSocketTimeout(30);
// Configure NTP for time synchronization
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
Serial.println("Waiting for NTP time sync...");
delay(2000);
oldTime = millis();
lastPIDTime = millis();
Serial.println("Setup complete!");
}
void loop() {
// Maintain MQTT connection
if (!client.connected()) {
reconnectMQTT();
}
client.loop();
// Read sensors periodically
if (millis() - lastSensorRead >= sensorInterval) {
lastSensorRead = millis();
readSensors();
calculateFlowRate();
}
// Handle overflow detection
handleOverflow();
// Auto mode schedule check
if (autoMode && !overflowDetected) {
checkSchedule();
}
// PID control for water valve
if (wateringActive && !overflowDetected) {
pidControl();
}
// Control valves based on current state
controlValves();
// Publish data to ThingSpeak
if (millis() - lastMQTTPublish >= mqttInterval) {
lastMQTTPublish = millis();
publishData();
}
}
// Interrupt service routine for flow sensor
void IRAM_ATTR flowPulseCounter() {
flowPulseCount++;
}
void setupWiFi() {
Serial.print("Connecting to WiFi");
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nWiFi connection failed!");
}
}
void reconnectMQTT() {
int attempts = 0;
while (!client.connected() && attempts < 3) {
Serial.print("Attempting MQTT connection...");
if (client.connect(mqtt_client_id, mqtt_username, mqtt_password)) {
Serial.println("connected");
// Subscribe to command channel
String subscribeStr = String("channels/") + command_channel_id + "/subscribe/json/" + command_read_api_key;
client.subscribe(subscribeStr.c_str());
Serial.println("Subscribed to command channel");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" retrying in 5 seconds");
delay(5000);
}
attempts++;
}
}
void mqttCallback(char* topic, byte* payload, unsigned int length) {
String message = "";
for (unsigned int i = 0; i < length; i++) {
message += (char)payload[i];
}
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("]: ");
Serial.println(message);
// Parse JSON message from ThingSpeak
// Expected format: {"field1":"value1","field2":"value2",...}
// Simple JSON parsing (for basic ThingSpeak format)
// Field 1: Mode (0=manual, 1=auto)
int field1Start = message.indexOf("\"field1\":\"");
if (field1Start != -1) {
field1Start += 10; // Length of "field1":""
int field1End = message.indexOf("\"", field1Start);
if (field1End != -1) {
String modeStr = message.substring(field1Start, field1End);
int mode = modeStr.toInt();
autoMode = (mode == 1);
Serial.print("Mode set to: ");
Serial.println(autoMode ? "AUTO" : "MANUAL");
if (!autoMode) {
wateringActive = false;
}
}
}
// Field 2: Target water volume (liters)
int field2Start = message.indexOf("\"field2\":\"");
if (field2Start != -1) {
field2Start += 10;
int field2End = message.indexOf("\"", field2Start);
if (field2End != -1) {
String targetStr = message.substring(field2Start, field2End);
float newTarget = targetStr.toFloat();
if (newTarget > 0) {
targetWaterVolume = newTarget;
Serial.print("Target volume set to: ");
Serial.print(targetWaterVolume);
Serial.println(" L");
}
}
}
// Field 3: Scheduled hour (0-23)
int field3Start = message.indexOf("\"field3\":\"");
if (field3Start != -1) {
field3Start += 10;
int field3End = message.indexOf("\"", field3Start);
if (field3End != -1) {
String hourStr = message.substring(field3Start, field3End);
int newHour = hourStr.toInt();
if (newHour >= 0 && newHour <= 23) {
scheduledHour = newHour;
Serial.print("Scheduled hour set to: ");
Serial.println(scheduledHour);
}
}
}
// Field 4: Manual water valve control (0=close, 1=open)
int field4Start = message.indexOf("\"field4\":\"");
if (field4Start != -1) {
field4Start += 10;
int field4End = message.indexOf("\"", field4Start);
if (field4End != -1) {
String waterStr = message.substring(field4Start, field4End);
if (!autoMode) {
manualWaterValve = (waterStr.toInt() == 1);
Serial.print("Manual water valve: ");
Serial.println(manualWaterValve ? "OPEN" : "CLOSED");
}
}
}
// Field 5: Manual drain valve control (0=close, 1=open)
int field5Start = message.indexOf("\"field5\":\"");
if (field5Start != -1) {
field5Start += 10;
int field5End = message.indexOf("\"", field5Start);
if (field5End != -1) {
String drainStr = message.substring(field5Start, field5End);
if (!autoMode) {
manualDrainValve = (drainStr.toInt() == 1);
Serial.print("Manual drain valve: ");
Serial.println(manualDrainValve ? "OPEN" : "CLOSED");
}
}
}
}
void readSensors() {
// Read DHT22 sensor
float temp = dht.readTemperature();
float hum = dht.readHumidity();
if (!isnan(temp) && !isnan(hum)) {
temperature = temp;
humidity = hum;
}
// Read wet sensor (LOW = wet detected)
wetSensorState = (digitalRead(WET_SENSOR_PIN) == LOW);
Serial.print("Temp: ");
Serial.print(temperature);
Serial.print("°C, Humidity: ");
Serial.print(humidity);
Serial.print("%, Wet Sensor: ");
Serial.println(wetSensorState ? "WET" : "DRY");
}
void calculateFlowRate() {
unsigned long currentTime = millis();
unsigned long elapsedTime = currentTime - oldTime;
if (elapsedTime >= 1000) { // Calculate every second
// Disable interrupts while reading
noInterrupts();
unsigned int pulses = flowPulseCount;
flowPulseCount = 0;
interrupts();
// Calculate flow rate in L/min
flowRate = ((1000.0 / elapsedTime) * pulses) / calibrationFactor;
// Add to total volume (convert L/min to L)
float volumeIncrement = (flowRate / 60.0) * (elapsedTime / 1000.0);
currentWaterVolume += volumeIncrement;
Serial.print("Flow rate: ");
Serial.print(flowRate);
Serial.print(" L/min, Total volume: ");
Serial.print(currentWaterVolume);
Serial.println(" L");
oldTime = currentTime;
}
}
void pidControl() {
unsigned long currentTime = millis();
float deltaTime = (currentTime - lastPIDTime) / 1000.0; // Convert to seconds
if (deltaTime >= 0.1) { // Update PID every 100ms
// Calculate error
float error = targetWaterVolume - currentWaterVolume;
// PID calculations
integral += error * deltaTime;
float derivative = (error - previousError) / deltaTime;
float output = Kp * error + Ki * integral + Kd * derivative;
// Update for next iteration
previousError = error;
lastPIDTime = currentTime;
// Check if target reached
if (currentWaterVolume >= targetWaterVolume) {
wateringActive = false;
currentWaterVolume = 0.0; // Reset for next cycle
integral = 0.0; // Reset integral
previousError = 0.0;
Serial.println("Target volume reached! Stopping watering.");
}
Serial.print("PID Error: ");
Serial.print(error);
Serial.print(", Output: ");
Serial.println(output);
}
}
void checkSchedule() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
return;
}
int currentHour = timeinfo.tm_hour;
int currentMinute = timeinfo.tm_min;
// Start watering at scheduled hour
if (currentHour == scheduledHour && currentMinute == 0 && !wateringActive) {
wateringActive = true;
currentWaterVolume = 0.0;
integral = 0.0;
previousError = 0.0;
Serial.println("Auto watering started based on schedule");
}
}
void handleOverflow() {
if (wetSensorState && !overflowDetected) {
overflowDetected = true;
wateringActive = false;
Serial.println("OVERFLOW DETECTED! Emergency shutdown.");
} else if (!wetSensorState && overflowDetected) {
// Reset overflow flag when sensor reads dry again
overflowDetected = false;
Serial.println("Overflow cleared.");
}
}
void controlValves() {
bool waterValveOpen = false;
bool drainValveOpen = false;
if (overflowDetected) {
// Emergency: close water, open drain
waterValveOpen = false;
drainValveOpen = true;
} else if (autoMode) {
// Auto mode: control based on watering state
waterValveOpen = wateringActive;
drainValveOpen = false;
} else {
// Manual mode
waterValveOpen = manualWaterValve;
drainValveOpen = manualDrainValve;
}
// Control relays (assuming LOW = valve open for active-low relays)
digitalWrite(WATER_VALVE_PIN, waterValveOpen ? HIGH : LOW);
digitalWrite(DRAIN_VALVE_PIN, drainValveOpen ? HIGH : LOW);
}
void publishData() {
if (!client.connected()) {
return;
}
// Prepare payload with all field data
String payload = "field1=" + String(temperature) +
"&field2=" + String(humidity) +
"&field3=" + String(flowRate) +
"&field4=" + String(currentWaterVolume) +
"&field5=" + String(wetSensorState ? 1 : 0) +
"&field6=" + String(autoMode ? 1 : 0) +
"&field7=" + String(wateringActive ? 1 : 0) +
"&field8=" + String(overflowDetected ? 1 : 0);
// Publish to ThingSpeak
if (client.publish(topic_publish.c_str(), payload.c_str())) {
Serial.println("Data published to ThingSpeak");
} else {
Serial.println("Failed to publish data");
}
}