#include <DHT.h> // DHT sensor library
#include <Wire.h> // I2C communication
#include <LiquidCrystal_I2C.h> // I2C LCD control
#include <PID_v1.h> // PID control algorithm
#include <WiFi.h> // WiFi connectivity
#include <PubSubClient.h> // MQTT for cloud comms
#include <RTClib.h> // RTC library for time control
// --------------RTC configuration---------------------------------------
RTC_DS3231 rtc; // RTC object to talk to hardware
bool timeControlEnabled = true; // Master ON/OFF for time restriction
// ---------------Two time intervals------------------------------------
// ----------------First interval: Evening (6PM to 10PM)-----------------
int heaterStartHour = 16; // Heater ON time (6PM in 24hr format)
int heaterEndHour = 22; // Heater OFF time (10PM)
// ----------------Second interval: Morning (5AM to 7AM)------------------
int heaterStartHour2 = 5; // Second interval start (5AM)
int heaterEndHour2 = 7; // Second interval end (7AM)
bool heaterAllowed = false; // Current status: true=heater can run now
// ----------Display configuration-------------------------------
LiquidCrystal_I2C lcd(0x27, 16, 2); // LCD: address 0x27, 16 cols, 2 rows
//LiquidCrystal_I2C lcd(0x27, 20, 4); // LCD: address 0x27, 20 cols, 4 rows
#define DHTTYPE DHT22 // Sensor type: DHT22
// -----------I/O Pin Assignments----------------------
#define DHT22_1PIN 4 // Sensor 1 (PID control) on GPIO4
#define DHT22_2PIN 0 // Sensor 2 (safety) on GPIO0
#define set_point_pin 34 // Potentiometer for temp adjustment
#define HEATER_PIN 16 // MOSFET gate for heater control
#define HEATER_SAFETY_PIN 17 // MOSFET for safety cutoff
#define temp_alarm 18 // LED/buzzer for high temp
#define humid_alarm 19 // LED/buzzer for high humidity
// --------------Process Control Alarm Setpoints-----------------------
#define temp_h 40 // High temp alarm threshold (°C)
#define humid_h 75 // High humidity alarm threshold (%)
// ----------------Safety Protection set points--------------------------
#define safety_temp_hh 50 // Safety high temp cutoff
#define safety_humid_hh 85 // Safety high humidity cutoff
#define safety_temp_ll 10 // Safety low temp cutoff
#define safety_humid_ll 15 // Safety low humidity cutoff
// ------------------- Safety Sensor Failure Detection ---------------------
#define SENSOR_READ_FAILURE_THRESHOLD 3 // Number of consecutive failures before cutoff
#define SENSOR_READ_INTERVAL 2000 // ms between sensor reads
// -------------------Create DHT sensor objects-----------------------------
DHT DHT22_PID(DHT22_1PIN, DHTTYPE); // Primary sensor for control
DHT DHT22_Safety(DHT22_2PIN, DHTTYPE); // Backup sensor for safety
// ------------------- VARIABLES----------------------------------------------
float pid_temp, pid_humid; // Sensor 1 readings
float safety_temp, safety_humid; // Sensor 2 readings
// ----------------------Safety sensor status variables -----------------------
bool safetySensorFailed = false; // Track if safety sensor has failed
int safetySensorFailureCount = 0; // Count consecutive failures
unsigned long lastSensorReadTime = 0; // Track last sensor read time
// ----------------------- PID Variables--------------------------------------
double Setpoint, Input, Output; // Target, current, output value
// PID tuning constants
double Kp = 10.0, Ki = 0.1, Kd = 1.0; // Proportional, Integral, Derivative
// ------------------------Direct mode: HEATER speeds UP when Temp goes UP-----------------
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT); // PID object
// ------------------------Timing Variables---------------------------------
unsigned long lastPIDTime = 0; // Last PID calculation time
const long pidInterval = 2000; // Run PID every 2 seconds
WiFiClient wifiClient; // Create WiFi client object
PubSubClient client(wifiClient); // Create MQTT client using WiFi
// --------------------------WiFi credentials------------------------------------------
const char* ssid = "Wokwi-GUEST"; // Your WiFi network name
const char* password = ""; // Your WiFi password (empty for Wokwi)
const char* mqtt_broker = "industrial.api.ubidots.com"; // Ubidots server
const int mqtt_port = 1883; // For unencrypted connection(std MQTT port)
const char* UBIDOTS_TOKEN ="BBUS-FLBitJEEOSUACNY1IaB1SjsL2nUTGQ"; // User ID from ubidits
unsigned long lastReconnectAttempt = 0; // Track last MQTT reconnect time
const long reconnectInterval = 5000; // Try to reconnect every 5 seconds
// ---------------------------------Function prototypes (declare before use)-----------------------
void readSensors(); // Reads both DHT sensors
void checkWiFiAndMQTT(); // Manages cloud connection
boolean attemptReconnect(); // Tries to reconnect to MQTT
bool isHeaterTimeAllowed(); // Tell compiler about this function
// ---------------------Safety Protection Sensor function prototypes---------------
void safetyCutoff(); // Emergency heater cutoff function
void checkSafetySensorStatus(); // Check if safety sensor is working
// ---------------------------------SETUP FUNCTION (runs once at start)---------------------------
void setup() {
Serial.begin(115200); // Start serial monitor at 115200 baud
// -----------------------------Initialize RTC------------------------------------
if (!rtc.begin()) { // Try to connect to RTC via I2C
Serial.println("RTC not found! Heater will run without time restriction."); // RTC missing
timeControlEnabled = false; // Continue without RTC
} else { // RTC connected successfully
Serial.println("RTC found!"); // Confirm RTC detected
if (rtc.lostPower()) { // Check if RTC battery died
Serial.println("RTC lost power, setting to compile time"); // RTC power lost
rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // Set RTC to PC compile time
}
// Display current time
DateTime now = rtc.now(); // Read current time from RTC
Serial.print("Current time: "); // Show current time in serial monitor
Serial.print(now.hour()); Serial.print(":"); //Extract hour from time
Serial.print(now.minute()); Serial.print(":"); //Extract minute from time
Serial.println(now.second()); //Extract second from time
}
// -------------Configure pins as inputs/outputs--------------------
pinMode(HEATER_SAFETY_PIN, OUTPUT); // Safety protection control
pinMode(temp_alarm, OUTPUT); // Temperature warning LED/buzzer
pinMode(humid_alarm, OUTPUT); // Humidity warning LED/buzzer
pinMode(set_point_pin, INPUT); // Manual setpoint adjustment for potentiometer at PIN 34
ledcAttach(HEATER_PIN, 5000, 8); // Setup PWM for heater (5kHz, 8-bit = 0-255)
digitalWrite(HEATER_SAFETY_PIN, HIGH); // Initially enable heater safety circuit (will be disabled if sensor fails)
// ---------------Initialize both DHT22 sensors---------------------
DHT22_PID.begin(); // Primary sensor for PID control
DHT22_Safety.begin(); // Backup sensor for safety monitoring
delay(2000); // Wait for sensors to stabilize (DHT22 requires 2s warm-up)
// ---------------LCD display setup---------------------------------------
lcd.init(); // Turn on the LCD so it can show text
lcd.backlight(); // Light up the screen so you can see it
lcd.setCursor(0, 0); // (column, row) - starts at 0
lcd.print("Temp & Humid"); // Display what this system does
lcd.setCursor(0, 1); // Move cursor to second line (row 1, column 0)
lcd.print("System is Ready"); // Show second line on LCD
// ---------------PID set up-------------------------------------------
myPID.SetMode(AUTOMATIC); // Turn PID on
myPID.SetSampleTime(pidInterval); // Compute every 1000ms
myPID.SetOutputLimits(0, 255); // Standard PWM limits (Analog Write)
// -------------------------WiFi and Cloud set up-----------------------------
Serial.println("Connecting to WiFi..."); // Status message
WiFi.begin(ssid, password); // Start WiFi connection
while (WiFi.status() != WL_CONNECTED) { // Wait for connection
delay(1000); // Wait 1 sec
Serial.print("."); // Print dot for each attempt
}
Serial.print("\nWiFi connected, IP:"); // Success message
Serial.println(WiFi.localIP()); // Show assigned IP
client.setServer(mqtt_broker, mqtt_port); //Set MQTT broker
}
// --------------------TIME CHECK FUNCTION FOR TWO INTERVALS---------------------
bool isHeaterTimeAllowed() { // Check if current hour is within allowed intervals
if (!timeControlEnabled) return true; // If no RTC, always allow
DateTime now = rtc.now(); // Get current time from RTC
int currentHour = now.hour(); // Extract just the hour (0-23)
int currentMinute = now.minute(); // Extract minute for more precise control
// Convert current time to minutes since midnight for easier comparison
int currentTimeInMinutes = (currentHour * 60) + currentMinute;
// Convert interval times to minutes since midnight
int start1Minutes = heaterStartHour * 60;
int end1Minutes = heaterEndHour * 60;
int start2Minutes = heaterStartHour2 * 60;
int end2Minutes = heaterEndHour2 * 60;
// Check if current time is within first interval (18:00 - 22:00)
bool inFirstInterval = (currentTimeInMinutes >= start1Minutes &&
currentTimeInMinutes < end1Minutes);
// Check if current time is within second interval (05:00 - 07:00)
bool inSecondInterval = (currentTimeInMinutes >= start2Minutes &&
currentTimeInMinutes < end2Minutes);
// Return TRUE if in either interval
return (inFirstInterval || inSecondInterval);
}
// ----------------------- Safety Sensor Status Check Function --------------------
void checkSafetySensorStatus() {
// Check if safety sensor readings are valid (not NaN)
if (isnan(safety_temp) || isnan(safety_humid)) {
safetySensorFailureCount++;
Serial.print("Safety sensor read failed. Failure count: ");
Serial.println(safetySensorFailureCount);
// If we've had multiple consecutive failures, declare sensor failed
if (safetySensorFailureCount >= SENSOR_READ_FAILURE_THRESHOLD) {
if (!safetySensorFailed) {
safetySensorFailed = true;
Serial.println("CRITICAL: Safety sensor permanently failed! Cutting heater!");
safetyCutoff(); // Immediate heater cutoff
}
}
} else {
// Successful read - reset failure counter
safetySensorFailureCount = 0;
// If sensor was failed but now working, we can re-enable (with caution)
if (safetySensorFailed) {
safetySensorFailed = false;
Serial.println("Safety sensor recovered. Re-enabling normal operation.");
}
}
}
// --------------------------- Safety Cutoff Function-----------------------
void safetyCutoff() {
// IMMEDIATELY cut off heater through both mechanisms
digitalWrite(HEATER_SAFETY_PIN, LOW); // Main safety cutoff
ledcWrite(HEATER_PIN, 0); // Ensure PWM is set to 0
// Activate alarms to indicate critical failure
digitalWrite(temp_alarm, HIGH);
digitalWrite(humid_alarm, HIGH);
// Display warning on LCD
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("!!! SAFETY !!!");
lcd.setCursor(0, 1);
lcd.print("SENSOR FAILED!");
// Send critical alert via serial
Serial.println("********** EMERGENCY SHUTDOWN **********");
Serial.println("Safety sensor failure - Heater forcibly cut off");
Serial.println("********** EMERGENCY SHUTDOWN **********");
}
// -------------------LOOP FUNCTION ----------------------------
void loop() {
unsigned long now = millis(); // Get current time in ms
float heating_percentage = 0; // Stores heater power as percentage (0-100%)
// ------------------------ Read sensors at regular intervals ---------------
if (now - lastSensorReadTime >= SENSOR_READ_INTERVAL) {
lastSensorReadTime = now;
readSensors(); // Get temp/humid from both sensors
checkSafetySensorStatus(); // Check if safety sensor is working
}
// ------------Handle PID control and display (runs every 2 seconds)-----------
if (now - lastPIDTime >= pidInterval) { // Checks if 2000ms has passed since last PID run
lastPIDTime = now; // Updates timer for next interval
// 1. READ SENSORS
readSensors(); // Get temp/humid from both sensors
lcd.clear(); // Clear screen
lcd.setCursor(0, 0); // Top row
// --------------------Check if safety sensor has failed------------------
if (safetySensorFailed) {
lcd.print("SAFETY SENSOR");
lcd.setCursor(0, 1);
lcd.print("FAILED - HEATER OFF");
// Ensure heater stays off
digitalWrite(HEATER_SAFETY_PIN, LOW);
ledcWrite(HEATER_PIN, 0);
Serial.println("Heater OFF: Safety sensor failed - MANUAL RESET REQUIRED");
} else {
// Normal operation - safety sensor is working
lcd.print("P:T1-"); // Label for sensor1 temp
lcd.print(pid_temp,1); // Show temp value
lcd.print(",H1-"); // Label for sensor1 humidity
lcd.print(pid_humid,1); // Show humidity value
// Read setpoint from potentiometer
float value = analogRead(set_point_pin); // 0-4095 ADC reading
Setpoint = map(value, 0, 4095, 0, 100); // Convert to 0-100°C
lcd.setCursor(0, 1); // Bottom row
lcd.print("Set_point:"); // Label
lcd.print(Setpoint,1); // Show target temperature
// Show time on second row
lcd.setCursor(12, 1); // Move to column 12 on bottom row
if (timeControlEnabled) { // If RTC is working
DateTime nowTime = rtc.now(); // Get current time
char timeStr[7]; // Buffer for "HH:MM" + null terminator
sprintf(timeStr, "%02d:%02d", nowTime.hour(), nowTime.minute()); // Format as 14:30
lcd.print(timeStr); // Show time on LCD
} else {
lcd.print("No RTC"); // Show error if RTC missing
}
// ---------------------Process alarms (check if thresholds exceeded)---------------------
if(pid_temp > temp_h) {
digitalWrite(temp_alarm, HIGH); // Turn on temp alarm
} else {
digitalWrite(temp_alarm, LOW); // Turn off temp alarm
}
if(pid_humid > humid_h) {
digitalWrite(humid_alarm, HIGH); // Turn on humid alarm
} else {
digitalWrite(humid_alarm, LOW); // Turn off humid alarm
}
// ---------------------Safety protection (emergency shutdown)--------------------------
if(safety_temp > safety_temp_hh || safety_humid < safety_humid_ll) {
digitalWrite(HEATER_SAFETY_PIN, LOW); // Kill heater (LOW = off)
Serial.println("Heater OFF: Safety shutdown");
} else {
heaterAllowed = isHeaterTimeAllowed(); // Check if time is allowed
if (!heaterAllowed) {
digitalWrite(HEATER_SAFETY_PIN, LOW); // Outside allowed times → turn heater OFF
Serial.println("Heater OFF: Outside operating hours (18:00-22:00 or 05:00-07:00)");
} else { // Heater allowed - proceed with PID control
digitalWrite(HEATER_SAFETY_PIN, HIGH); // 18:00-22:00? YES → enable heater
Input = pid_temp; // Feed current temp to PID
myPID.Compute(); // Calculate new output value
ledcWrite(HEATER_PIN, (int)Output); // Apply PWM to heater
// Calculate percentage (Output is 0-255)
heating_percentage = (Output / 255.0) * 100.0;
// Show BOTH raw and percentage in serial monitor
Serial.print("PID Raw: ");
Serial.print((int)Output); // Shows raw value
Serial.print(", Percentage: ");
Serial.print(heating_percentage, 1); // Shows with 1 decimal
Serial.println("%"); // Adds the % symbol
}
}
}
} else {
digitalWrite(HEATER_SAFETY_PIN, LOW); // Keep heater off between PID cycles
}
// 2. Check WiFi and MQTT connection (runs every loop iteration)
checkWiFiAndMQTT(); // Ensure cloud connection
// 3. Upload to Ubidots Cloud (runs every loop iteration)
String topic = String("/v1.6/devices/iot_22-2-2026"); // Ubidots device path
// Build JSON payload for Ubidots
String payload = String("{\"temperature-1\":") +
(isnan(pid_temp) ? "\"nan\"" : String(pid_temp, 1)) + // Temp1 or "nan"
"," + String("\"humidity-1\":") +
(isnan(pid_humid) ? "\"nan\"" : String(pid_humid, 1)) + // Humid1 or "nan"
"," + String("\"heater_percent\":") +
(isnan(Output) ? "\"nan\"" : String(heating_percentage, 1)) + // Heater % or "nan"
"," + String("\"current-setpoint\":") +
(isnan(Setpoint) ? "\"nan\"" : String(Setpoint, 1)) + // Setpoint or "nan"
"," + String("\"temperature-2\":") +
(isnan(safety_temp) ? "\"nan\"" : String(safety_temp, 1)) + // Temp2 or "nan"
"," + String("\"humidity-2\":") +
(isnan(safety_humid) ? "\"nan\"" : String(safety_humid, 1)) + // Humid2 or "nan"
"," + String("\"safety_sensor_status\":") + // Add safety sensor status
(safetySensorFailed ? "0" : "1") + // 0=failed, 1=working
"}"; // Close JSON
client.publish(topic.c_str(), payload.c_str()); // Send to cloud
Serial.println("Published: " + payload); // Confirm in serial monitor
delay(60000); // Wait 60 seconds before next publication
}
// -------------------------FUNCTION TO CHECK WIFI AND MQTT CONNECTION----------------------------
void checkWiFiAndMQTT() {
unsigned long now = millis(); // Current time
// A. Check WiFi connection
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected!"); // Alert
WiFi.reconnect(); // Try to reconnect
}
// B. Check MQTT connection (if WiFi is OK)
else {
if (!client.connected()) { // MQTT disconnected?
// Time to try reconnecting?
if (now - lastReconnectAttempt > reconnectInterval) {
lastReconnectAttempt = now; // Update timer
if (attemptReconnect()) { // Try to connect
lastReconnectAttempt = 0; // Reset timer on success
}
}
} else {
client.loop(); // Maintain MQTT connection
}
}
}
// ---------------------------MQTT RECONNECT FUNCTION-------------------------------
boolean attemptReconnect() {
Serial.println("Connecting to Ubidots MQTT..."); // Status
// Connect using token as username, empty password
if (client.connect("ESP32_Client", UBIDOTS_TOKEN, "")) {
Serial.println("Connected to Ubidots"); // Success
return true; // Report success
} else {
Serial.print("Failed, rc="); // Show error code
Serial.println(client.state());
return false; // Report failure
}
}
// ------------------------READ SENSORS FUNCTION--------------------------
void readSensors() {
// Read Sensor 1 (PID control)
pid_temp = DHT22_PID.readTemperature(); // Get temp in °C
pid_humid = DHT22_PID.readHumidity(); // Get humidity in %
// Read Sensor 2 (Safety protection)
safety_temp = DHT22_Safety.readTemperature(); // Get temp in °C
safety_humid = DHT22_Safety.readHumidity(); // Get humidity in %
// Check for sensor errors (NaN = Not a Number) but don't act here
// The checkSafetySensorStatus() function handles the failure logic
if (isnan(pid_temp) || isnan(pid_humid) || isnan(safety_temp) || isnan(safety_humid)) {
Serial.println("ERROR: Sensor reading failed!"); // Alert
return; // Exit function
}
// ---------------------------Print readings to serial monitor--------------------------------
Serial.println("PID DHT"); // Label for sensor1
Serial.print("Temperature : "); Serial.println(pid_temp,1);
Serial.print("Humidity : "); Serial.println(pid_humid,1);
Serial.println("Safety DHT"); // Label for sensor2
Serial.print("Temperature : "); Serial.println(safety_temp,1);
Serial.print("Humidity : "); Serial.println(safety_humid,1);
}