/*
File : Climate Control
Author : leveletech.com / wa 081214584114
Date :
Description :
*/
//--------------------------------- Include Libraries-----------------------------------------------
#include "DHT.h"
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
//--------------------------------- Include Libraries-----------------------------------------------
//--------------------------------- Hardware Setting------------------------------------------------
String label1 = "Leveletech.com";
String label2 = "Climate Control";
//Blynk
#define BLYNK_TEMPLATE_ID "TMPL6g6iGv_X3"
#define BLYNK_TEMPLATE_NAME "ESP32"
#define BLYNK_AUTH_TOKEN "awKh4JUGdgCJamBO60sZCUu0tqdtW-qY"
#define BLYNK_PRINT Serial
#include <WiFi.h>
#include <BlynkSimpleEsp32.h>
char auth[] = BLYNK_AUTH_TOKEN; //Auth Token
char ssid[] = "Wokwi-GUEST"; //nama hotspot yang digunakan
char pass[] = ""; //password hotspot yang digunakan
//--------------------------------- Hardware Setting------------------------------------------------
//--------------------------------- Define and Constant---------------------------------------------
const int S_num = 4;
const int S_pin[S_num] = {32, 33, 25, 26}; // Array of pins for switch
const int R_num = 4;
const int R_pin[R_num] = {19, 18, 5, 17}; // Array of pins for relays
const int DHT11_pin = 16;
const int MQ135_pin = 4;
//--------------------------------- Define and Constant---------------------------------------------
//--------------------------------- User Define Data Type-------------------------------------------
struct timer_data {
bool IN;
bool Q;
bool TT;
bool BP1; //button pressed
bool BP2; //button pressed
int PT; //preset time
int ET; //elapsed time
unsigned long TN;//time now
};
struct spwm_data {
bool IN;
bool Q;
bool BP; //button pressed
int PT_on; //preset time for ON state
int PT_off; //preset time for OFF state
int ET; //elapsed time
unsigned long TN;//time now
};
struct os_data {
bool IN;
bool Q;
bool BP;
};
//--------------------------------- User Define Data Type-------------------------------------------
//----------------------------------Global Variable-------------------------------------------------
//Blynk
int Analog1, Analog2, Analog3;
bool digital1, digital2;
int mode = 0;
bool S[S_num] = {false, false, false, false};
bool S_LastState[S_num] = {false, false, false, false};
unsigned long S_LastDebounceTime[S_num]; // Array to store the last debounce time for each button
unsigned long debounceDelay = 10; // Debounce delay in milliseconds
/*
S[0] : Select
S[1] : UP
S[2] : Down
S[3] : OK (confirm)
*/
bool R[R_num] = {false, false, false, false};
/*
R[0] : Heater Command
R[1] : Blower Command
R[2] : Lampu Command
R[3] : Pompa Air Command
*/
bool B[8] = {false, false, false, false, false, false, false, false};
/*
B[0] : Heater Active
B[1] : AM Heater 0 = auto, 1 = manual
B[2] : M_CMD Heater, manual command
B[3] : Blower Active
B[4] : AM Blower 0 = auto, 1 = manual
B[5] : H_CMD Blower, manual command
B[6] : Lampu_CMD
B[7] : Pompa Air_CMD
*/
//DHT11
float humidity, temperature;
DHT dht(DHT11_pin, DHT22); //simulation using DHT22
//MQ135
float AmoniaPPM;
//lcd
LiquidCrystal_I2C lcd(0x27, 16, 2);
int active_page = 1;
int active_page_selected;
int page3_menu = 0;
int page4_menu = 0;
//instance
timer_data T[9]; //TON and TOF
/*
T[0] : TON for change page 3 - 4
T[1] : delay for current tempt < setpoint
T[2] : delay for current tempt > setpoint
*/
spwm_data spwm_dat[4]; //for timer on off
/*
pwm_data spwm_dat[0] : On Off for heater
pwm_data spwm_dat[1] : On off for blower
*/
os_data os_dat[10]; //for one shoot rising and falling
/*
os_dat[0] : OSF PB Select for changging page 1 - 2
os_dat[1] : OSR for cahngging to page 3 - 4
os_dat[2] : OSR heating hysterisis
os_dat[3] : OSR cooling hysterisis
os_dat[4] : OSR PB UP
os_dat[5] : OSR PB DOWN
os_dat[6] : OSR PB OK
*/
//process paramater
int H_SP, B_SP, H_T, B_T;
int H_SP_S, B_SP_S, H_T_S, B_T_S;
int H_SP_Max = 15;
int H_SP_Min = 5;
int B_SP_Max = 100;
int B_SP_Min = 25;
int H_T_Max = 15;
int H_T_Min = 1;
int B_T_Max = 15;
int B_T_Min = 1;
/*
B : Blower
H : Heater
SP : Setpoint in C
T : timer on off for intermitend mode in Second
*/
//----------------------------------Global Variable-------------------------------------------------
//----------------------------------User Defined Function Stage 1-----------------------------------
//Blynk
BLYNK_WRITE(V0) {
Analog1 = param.asInt();
}
BLYNK_WRITE(V1) {
Analog2 = param.asInt();
}
BLYNK_WRITE(V2) {
Analog3 = param.asInt();
mode = Analog3;
}
BLYNK_WRITE(V3) {
digital1 = param.asInt();
}
BLYNK_WRITE(V4) {
digital2 = param.asInt();
}
void blynk_update() {
switch (mode) {
case 0:
Blynk.virtualWrite(V0, temperature);
Blynk.virtualWrite(V1, humidity);
break;
case 1:
Blynk.virtualWrite(V0, AmoniaPPM);
Blynk.virtualWrite(V1, AmoniaPPM);
break;
}
}
void TON(timer_data & Var) { //Timer On delay function
if (Var.IN)
{
if (!Var.BP1)
{
Var.TN = millis();
Var.BP1 = true;
Var.TT = true;
}
if (!Var.Q)
{
Var.ET = (millis() - Var.TN); //bagi 1000 untuk jadi detik
if (Var.ET >= Var.PT) {
Var.Q = true;
Var.TT = false;
}
}
} else
{
Var.Q = false;
Var.BP1 = false;
Var.ET = 0;
Var.TT = false;
}
}
void spwm(spwm_data & Var) { // Software PWM or Timer ON OFF
if (Var.IN)
{
if (!Var.BP)
{
Var.TN = millis();
Var.BP = true;
Var.Q = true; // Initially turn ON the output
}
Var.ET = (millis() - Var.TN); //bagi 1000 untuk jadi detik
if (Var.Q && Var.ET >= Var.PT_on) {
Var.Q = false; // If ON time is over, turn OFF the output
Var.TN = millis(); // Reset the timer for the next state
}
else if (!Var.Q && Var.ET >= Var.PT_off) {
Var.Q = true; // If OFF time is over, turn ON the output
Var.TN = millis(); // Reset the timer for the next state
}
}
else
{
Var.Q = false; // If input is inactive, turn OFF the output
Var.BP = false; // Reset button pressed flag
Var.ET = 0; // Reset elapsed time
}
}
void OSR(os_data & Var) {
if (Var.IN) {
if (!Var.BP) {
Var.Q = true;
Var.BP = true;
} else {
Var.Q = false;
}
} else {
Var.BP = false;
Var.Q = false;
}
}
void OSF(os_data & Var) {
if (!Var.IN) { // Check if the input is false
if (Var.BP) { // If the previous state was true (falling edge detected)
Var.Q = true; // Trigger the output
Var.BP = false; // Update the previous state to false
} else {
Var.Q = false; // Do not trigger the output
}
} else {
Var.BP = true; // Update the previous state to true
Var.Q = false; // Do not trigger the output
}
}
//Update Input
void S_udpate() {
for (int i = 0; i < S_num; i++) {
int currentState = digitalRead(S_pin[i]); // Read the current state of the button
if (currentState != S_LastState[i]) { // Check if the button state has changed and debounce it
S_LastDebounceTime[i] = millis(); // Reset the debounce timer
}
if ((millis() - S_LastDebounceTime[i]) > debounceDelay) { // Check if enough time has passed to consider it a valid state change
S[i] = !currentState; // If enough time has passed, update the state
}
S_LastState[i] = currentState; // Store the current state for the next iteration
}
}
void key_update() {
os_dat[0].IN = (S[0]); OSF(os_dat[0]); //OSF PB Select
T[0].IN = S[0]; TON(T[0]); // timer ON pressing 3 second
if (T[0].Q) os_dat[0].BP = false; //prevent OSF trigger output
os_dat[1].IN = T[0].Q; OSR(os_dat[1]); //OSR trigger after TON
os_dat[4].IN = (S[1]); OSR(os_dat[4]); //OSR PB UP
os_dat[5].IN = (S[2]); OSR(os_dat[5]); //OSR PB DOWN
os_dat[6].IN = (S[3]); OSR(os_dat[6]); //OSR PB OK
switch (active_page) {
case 1:
if (os_dat[0].Q) active_page = 2;
if (os_dat[1].Q) active_page = 3;
break;
case 2:
if (os_dat[0].Q) active_page = 1;
if (os_dat[1].Q) active_page = 3;
break;
case 3:
if (os_dat[1].Q) {
page3_menu = 0;
active_page = 4;
H_SP_S = H_SP; //restore setting if not saved
B_SP_S = B_SP;
}
switch (page3_menu) {
case 0:
if (os_dat[0].Q) {
page3_menu = 1;
lcd.setCursor(0, 1);
lcd.print("*");
Serial.println("page3_menu " + String(page3_menu));
}
break;
case 1:
if (os_dat[0].Q) {
page3_menu = 2;
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(8, 1);
lcd.print("*");
Serial.println("page3_menu " + String(page3_menu));
}
if (os_dat[4].Q) { //Up reference
B_SP_S++;
if (B_SP_S > B_SP_Max) B_SP_S = B_SP_Max;
}
if (os_dat[5].Q) { //Down reference
B_SP_S--;
if (B_SP_S < B_SP_Min) B_SP_S = B_SP_Min;
}
if (os_dat[6].Q) { //OK confirmation
B_SP = B_SP_S;
EEPROM.put(2, B_SP); //save to eeprom
page3_menu = 0; //back to menu0
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(8, 1);
lcd.print(" ");
}
break;
case 2:
if (os_dat[0].Q) {
page3_menu = 0;
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(8, 1);
lcd.print(" ");
Serial.println("page3_menu " + String(page3_menu));
}
if (os_dat[4].Q) { //Up reference
H_SP_S++;
if (H_SP_S > H_SP_Max) H_SP_S = H_SP_Max;
}
if (os_dat[5].Q) { //Down reference
H_SP_S--;
if (H_SP_S < H_SP_Min) H_SP_S = H_SP_Min;
}
if (os_dat[6].Q) { //OK confirmation
H_SP = H_SP_S;
EEPROM.put(0, B_SP); //save to eeprom
page3_menu = 0; //back to menu0
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(8, 1);
lcd.print(" ");
}
break;
}
break;
case 4:
if (os_dat[1].Q) {
page4_menu = 0;
active_page = 1;
H_T_S = H_T; //restore setting if not saved
B_T_S = B_T;
}
switch (page4_menu) {
case 0:
if (os_dat[0].Q) {
page4_menu = 1;
lcd.setCursor(1, 1);
lcd.print("*");
Serial.println("page4_menu " + String(page4_menu));
}
break;
case 1:
if (os_dat[0].Q) {
page4_menu = 2;
lcd.setCursor(1, 1);
lcd.print(" ");
lcd.setCursor(8, 1);
lcd.print("*");
Serial.println("page4_menu " + String(page4_menu));
}
if (os_dat[4].Q) {
B_T_S++;
if (B_T_S > B_T_Max) B_T_S = B_T_Max;
}
if (os_dat[5].Q) {
B_T_S--;
if (B_T_S < B_T_Min) B_T_S = B_T_Min;
}
if (os_dat[6].Q) { //OK confirmation
B_T = B_T_S;
EEPROM.put(6, B_T); //save to eeprom
page4_menu = 0; //back to menu0
lcd.setCursor(1, 1);
lcd.print(" ");
lcd.setCursor(8, 1);
lcd.print(" ");
spwm_dat[1].PT_on = (B_T * 1000); //update timer on off
spwm_dat[1].PT_off = (B_T * 1000);
}
break;
case 2:
if (os_dat[0].Q) {
page4_menu = 0;
lcd.setCursor(1, 1);
lcd.print(" ");
lcd.setCursor(8, 1);
lcd.print(" ");
Serial.println("page4_menu " + String(page4_menu));
}
if (os_dat[4].Q) {
H_T_S++;
if (H_T_S > H_T_Max) H_T_S = H_T_Max;
}
if (os_dat[5].Q) {
H_T_S--;
if (H_T_S < H_T_Min) H_T_S = H_T_Min;
}
if (os_dat[6].Q) { //OK confirmation
H_T = H_T_S;
EEPROM.put(4, H_T); //save to eeprom
page4_menu = 0; //back to menu0
lcd.setCursor(1, 1);
lcd.print(" ");
lcd.setCursor(8, 1);
lcd.print(" ");
spwm_dat[0].PT_on = (H_T * 1000); //update timer on off
spwm_dat[0].PT_off = (H_T * 1000);
}
break;
}
break;
}
}
void DHT11_update() {
humidity = dht.readHumidity();
temperature = dht.readTemperature();
}
void MQ135_update() {
int val = analogRead(MQ135_pin); //get raw data
//do calibration below
float CF = 100.0; // example calibaration factor
AmoniaPPM = val * (5.0 / 4095.0) * CF;
}
//Update Output
void R_udpate() {
for (int i = 0; i < R_num; i++) {
digitalWrite(R_pin[i], R[i]);
}
}
//LCD
void startup_display() {
int startPosLabel1 = (16 - label1.length()) / 2; // Calculate the starting position for label1 to center it
for (int i = 0; i < label1.length(); i++) { // Display label1 on the first row, centered
lcd.setCursor(startPosLabel1 + i, 0); // Centered position on row 0
lcd.print(label1[i]); // Print character
delay(200); // Wait 200ms for the next character
}
int startPosLabel2 = (16 - label2.length()) / 2; // Calculate the starting position for label2 to center it
for (int i = 0; i < label2.length(); i++) { // Display label2 on the second row, centered
lcd.setCursor(startPosLabel2 + i, 1); // Centered position on row 1
lcd.print(label2[i]); // Print character
delay(200); // Wait 200ms for the next character
}
delay(2000);
lcd.clear();
}
String format1(float data, String unit, int lengt) {
String formattedData = String(data, 0); // Convert float to String, no decimal places
formattedData += " " + unit; // Add space and unit
// Ensure the result is exactly lengt characters long by padding spaces if needed
while (formattedData.length() < lengt) {
formattedData = " " + formattedData;
}
return formattedData;
}
String format2(int data, String unit, int lengt) {
String formattedData = String(data); // Convert float to String, no decimal places
formattedData += " " + unit; // Add space and unit
// Ensure the result is exactly lengt characters long by padding spaces if needed
while (formattedData.length() < lengt) {
formattedData = " " + formattedData;
}
return formattedData;
}
void lcd_update() {
switch (active_page) {
case 1:
if (active_page_selected != active_page ) { //excecuted once
Serial.println("page 1");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Temp. & Humidity");
lcd.setCursor(0, 1);
lcd.print("S=");
lcd.setCursor(8, 1);
lcd.print("H=");
active_page_selected = active_page;
}
lcd.setCursor(2, 1); //excecuted forever
lcd.print(format1(temperature, "C", 5));
lcd.setCursor(10, 1);
lcd.print(format1(humidity, "%", 5));
break;
case 2:
if (active_page_selected != active_page ) { //excecuted once
Serial.println("page 2");
lcd.clear();
lcd.setCursor(1, 0);
lcd.print("Gas Monitoring");
lcd.setCursor(0, 1);
lcd.print("Amonia=");
active_page_selected = active_page;
}
lcd.setCursor(8, 1); //excecuted forever
lcd.print(format1(AmoniaPPM, "ppm", 7));
break;
case 3:
if (active_page_selected != active_page ) { //excecuted once
Serial.println("page 3");
lcd.clear();
lcd.setCursor(2, 0);
lcd.print("Target Suhu");
lcd.setCursor(1, 1);
lcd.print("B=>");
lcd.setCursor(9, 1);
lcd.print("H=<");
active_page_selected = active_page;
}
lcd.setCursor(4, 1); //excecuted forever
lcd.print(format2(B_SP_S, "C", 4));
lcd.setCursor(12, 1);
lcd.print(format2(H_SP_S, "C", 4));
break;
case 4:
if (active_page_selected != active_page ) { //excecuted once
Serial.println("page 4");
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Mode Intermitten");
lcd.setCursor(2, 1);
lcd.print("B=");
lcd.setCursor(9, 1);
lcd.print("H=");
active_page_selected = active_page;
}
lcd.setCursor(4, 1); //excecuted forever
lcd.print(format2(B_T_S, "m", 4));
lcd.setCursor(11, 1);
lcd.print(format2(H_T_S, "m", 4));
break;
}
}
void ReadEEPROM() {//Restore from EEPROM
EEPROM.get(0, H_SP); H_SP = (H_SP <= 0) ? 10 : H_SP;
EEPROM.get(2, B_SP); B_SP = (B_SP <= 0) ? 30 : B_SP;
EEPROM.get(4, H_T); H_T = (H_T <= 0) ? 2 : H_T;
EEPROM.get(6, B_T); B_T = (B_T <= 0) ? 2 : B_T;
H_SP_S = H_SP;
B_SP_S = B_SP;
H_T_S = H_T;
B_T_S = B_T;
}
//----------------------------------User Defined Function Stage 1-----------------------------------
//----------------------------------User Defined Function Stage 2-----------------------------------
void MainControl() {
T[1].IN = (temperature < H_SP ); TON(T[1]); //heater active heating process
os_dat[2].IN = T[1].Q; OSR(os_dat[2]);
if (os_dat[2].Q) {
Serial.println("Heating Active");
B[0] = true;
B[3] = false;
}
T[2].IN = (temperature > B_SP ); TON(T[2]); //blower active cooling process
os_dat[3].IN = T[2].Q; OSR(os_dat[3]);
if (os_dat[3].Q) {
Serial.println("Cooling Active");
B[0] = false;
B[3] = true;
}
//heater Control
//B[0] : Heater Active
//B[1] : AM Heater 0 = auto, 1 = manual
//B[2] : M_CMD Heater, manual command
//R[0] : Heater Command
spwm_dat[0].IN = (!B[1] & B[0]/*Auto and heater active*/);
spwm(spwm_dat[0]);
R[0] = (!B[1] & spwm_dat[0].Q) || (B[1] & B[2]) ;
//blower Control
//B[3] : Blower Active
//B[4] : AM Blower 0 = auto, 1 = manual
//B[5] : H_CMD Blower, manual command
//R[1] : Blower Command
spwm_dat[1].IN = (!B[4] & B[3]/*Auto and heater active*/);
spwm(spwm_dat[1]);
R[1] = (!B[4] & spwm_dat[1].Q) || (B[4] & B[25]) ;
//Lampu Control Remote Only
//R[2] : Lampu Command
//B[6] : Lampu_CMD
R[2] = B[6];
//Pompa Air Control Remote Only
//R[3] : Pompa Air Command
//B[7] : Pompa Air_CMD
R[3] = B[7];
}
//----------------------------------User Defined Function Stage 2-----------------------------------
//----------------------------------User Defined Function Stage 3-----------------------------------
//----------------------------------User Defined Function Stage 3-----------------------------------
//----------------------------------Testing Function------------------------------------------------
void testing() {
if (0) {
for (int i = 0; i < S_num; i++) {
R[i] = S[i];
}
}
if (0) {
Serial.print("Temperature: ");
Serial.print(temperature);
Serial.print("ºC ");
Serial.print("Humidity: ");
Serial.println(humidity);
}
if (0) {
Serial.print("Temperature: ");
Serial.println(temperature);
Serial.print("H_SP : ");
Serial.println(H_SP);
Serial.print("T[1].IN : ");
Serial.println( T[1].IN);
Serial.print("T[1].Q : ");
Serial.println( T[1].Q);
}
}
//----------------------------------Testing Function------------------------------------------------
//--------------------------------- Setup----------------------------------------------
void setup() {
//Pin Initialization
for (int i = 0; i < S_num; i++) { // Initialize input pins
pinMode(S_pin[i], INPUT_PULLUP);
}
for (int i = 0; i < R_num; i++) { // Initialize Output pins
pinMode(R_pin[i], OUTPUT);
}
//Get data from Epprom
ReadEEPROM();
//Timer Initialization (dalam ms)
T[0].PT = 300; // timer for pressing select
T[1].PT = 300; // delay for current tempt > setpoint
T[2].PT = 300; // delay for current tempt < setpoint
//heater intermitent on off
spwm_dat[0].PT_on = (H_T * 1000);
spwm_dat[0].PT_off = (H_T * 1000);
//blower intermitent on off
spwm_dat[1].PT_on = (B_T * 1000);
spwm_dat[1].PT_off = (B_T * 1000);
//Start Services
Serial.begin(115200);
dht.begin();
lcd.init(); //LCD
lcd.backlight(); // turn on LCD backlight
Blynk.begin(auth, ssid, pass); //memulai Blynk
//Startup
//startup_display();
//testing
if (0) {
Serial.println(spwm_dat[0].PT_on);
Serial.println(spwm_dat[0].PT_off);
Serial.println(spwm_dat[1].PT_on);
Serial.println(spwm_dat[1].PT_off);
}
}
//Testing
//--------------------------------- Setup----------------------------------------------
//--------------------------------- Loop-----------------------------------------------
void loop() {
//Update Input
Blynk.run(); //menjalankan blynk
S_udpate();
DHT11_update();
MQ135_update();
//Process
MainControl();
testing();
lcd_update();
key_update();
//Update Output
R_udpate();
blynk_update();
}
//--------------------------------- Loop-----------------------------------------------
//--------------------------------- Note-----------------------------------------------
/*
//EEPROM address Mapping
0 : INT H_SP
2 : INT B_SP
4 : INT H_T
6 : INT B_T
*/
//--------------------------------- Note-----------------------------------------------
Heater
Blower
Lampu
Pompa Air
MQ135