Hi All,
A few weeks ago I decided to try and turn an old AC unit and immersion heater I had into a glycol chiller - partly because it would be useful for my homebrewing hobby but mostly because it would give me a vested interest in learning to improve my coding skills.
I've hacked away at it for some time and have what I would call a 95% complete code.
I am able to communicate with an ESP 32 and the Arduino IoT Cloud to monitor temperature of both the fermenter (temp1) and the glycol (temp2) I am using a sliding widget to set the desired temperature for temp1 but there are some quirks in the program I have not been able to debug - mainly the compressor/heating/pump delay.
It seems that whenever there is a call for heating, cooling, or pumping the program will turn the relay on for the appropriate circuit but then at the next loop it will turn it off, starting the short cycle delay countdown after the delay the relay will turn on and stay on as desired.
I would greatly appreciate some fresh eyes on the code to point out where this is being commanded.
Also, if there are more efficient ways to code this system, I would love to hear suggestions.
Thanks!
/*
Sketch generated by the Arduino IoT Cloud Thing
https://create.arduino.cc/cloud/things/180cb205-21a4-4b99-a011-6addd679374e
Arduino IoT Cloud Variables description
The following variables are automatically generated and updated when changes are made to the Thing
String status_update; // Update Status to Message Widget for Information and Troubleshooting
float temp1; // Temperature read by the fermenter probe
float temp2; // Temperature read by the glycol probe
int set1; // Desired Temperature of the fermenter probe
bool cooling; //Lockout of the cooling relay via IoT Widget
bool heating; //Lockout of the heating relay via IoT Widget
bool mix; //Lockout of the mix relay via IoT Widget
bool pump; //Lockout of the pump relay via IoT Widget
Variables which are marked as READ/WRITE in the Cloud Thing will also have functions
which are called when their values are changed from the Dashboard.
These functions are generated with the Thing and added at the end of this sketch.
TL:DR
Description:
The connected device monitors temperature of a remote vessle (fermenter) as well as the working fluid vessle (glycol)
By heating or cooling the working fluid vessle and then pumping this fluid through a heat exchanger in the remote vessle the temperature can be controlled.
There is an additional aggitator (mix) within the glycol vessel to help maintain an accurate and uniform temperature within the glycol.
The temperature of the working fluid vessle is recorded to prevent over heating/cooling of the working fluid
Pseudocode:
1. Read Temperatures of glycol and fermenter
2. Take current time reading to compare against delays
3. Evaluate if the heating conditions are all met
If so: Turn it on
If not: Keep it off
4. Evaluate if the cooling conditions are all met
If so: Turn it on
If not: Keep it off
5. Evaluate if the pump conditions are all met
If so: Turn it on
If not: Keep it off
6. Evaluate if the mix conditions are all met
If so: Turn it on
If not: Keep it off
Repeat
*/
#include "thingProperties.h" // IoT Program
#include <OneWire.h> // Temperature Probe Program
#include <DallasTemperature.h> // Temperature Probe Program
//temp1 is the variable for Measured Fermentation Temperature in F
//temp2 is the variable for Measured Coolant Temperature in F
#define ONE_WIRE_BUS 13 // Temperature probes connected to GPIO13
#define heating_relay 12 // Relay Signal Pin connected to GPIO12
#define cooling_relay 14 // Relay Signal Pin connected to GPIO14
#define pump_relay 27 // Relay Signal Pin connected to GPIO27
#define mix_relay 26 // Relay Signal Pin connected to GPIO26
int min_coolant = 32; // Keep the glycol from freezing by not allowing it to get colder than 32F
int max_coolant = 100; // Keep the glycol from overheating by not allowing it to get hotter than 100F
int deadzone = 2.5; // Temperature buffer to keep hysteysis lower while maintaining somewhat consistent temperature
int set_temp = 62; // Default temperature if there is no input from IoT
bool last_heat_state = HIGH; //Initialize the last stored heating state to OFF
bool last_cool_state = HIGH; //Initialize the last stored heating state to OFF
bool last_pump_state = HIGH; //Initialize the last stored heating state to OFF
bool demand_heat; // Variable to determine if the heat should be turned on
bool demand_cool; // Variable to determine if the cooling should be turned on
bool demand_pump; // Variable to determine if the pump should be turned on
bool demand_mix; // Variable to determine if the agitation should be turned on
// All below conditions must be met to turn heating on
bool Heat_Condition1;
bool Heat_Condition2;
bool Heat_Condition3;
bool Heat_Condition4;
bool Heat_Condition5;
bool Heat_Condition6;
// All below conditions must be met to turn cooling on
bool Cool_Condition1;
bool Cool_Condition2;
bool Cool_Condition3;
bool Cool_Condition4;
bool Cool_Condition5;
// All below conditions must be met to turn pump on
bool Pump_Condition1;
bool Pump_Condition2;
bool Pump_Condition3;
bool Pump_Condition4;
// All below conditions must be met to turn the agitator on
bool Mix_Condition1;
bool Mix_Condition2;
bool Mix_Condition3;
unsigned long Current_Time = 0; // Intitialize current timer as 0
unsigned long Cooling_Last_Off = 0; // Initialize component to be last off at startup
unsigned long Cooling_Last_On = 0; // Initialize component to be last on at startup
unsigned long Heating_Last_Off = 0;// Initialize component to be last off at startup
unsigned long Heating_Last_On = 0; // Initialize component to be last on at startup
unsigned long Pump_Last_Off=0;// Initialize component to be last off at startup
unsigned long Cooling_Delay = 10 * 60 * 1000; // Make sure Cooling has been off for 10 min before starting again
unsigned long Heating_Delay = 2 * 60 * 1000; // Make sure Heating has been off for 10 min before starting again
unsigned long Pump_Delay = 2 * 60 * 1000; // Make sure Pump has been off for 10 min before starting again
unsigned long Mix_Delay = 2 * 60 * 1000; // Make sure Mix has been off for 10 min before starting again
signed long Cooling_Off_Time = 0; // Initialize the Off Time at startup
signed long Cooling_On_Time = 0; // Initialize the On Time at startup
signed long Heating_Off_Time = 0; // Initialize the Off Time at startup
signed long Heating_On_Time = 0; // Initialize the On Time at startup
signed long Pump_Off_Time = 0; // Initialize the Off Time at startup
// Initialize all state variables for troubleshooting to off
String cool_state = "OFF";
String heat_state = "OFF";
String pump_state = "OFF";
String mix_state = "OFF";
String mode = "Starting Up"; // Intialize the status update variable
// Setup a oneWire instance to communicate with any OneWire device
OneWire oneWire(ONE_WIRE_BUS);
// Pass oneWire reference to DallasTemperature library
DallasTemperature sensors(&oneWire);
void setup() {
//Declare Relays as OUTPUTS
pinMode(heating_relay, OUTPUT);
pinMode(cooling_relay, OUTPUT);
pinMode(pump_relay, OUTPUT);
pinMode(mix_relay, OUTPUT);
//Initialize all relays to "OFF"
digitalWrite(heating_relay, HIGH);
digitalWrite(cooling_relay, HIGH);
digitalWrite(pump_relay, HIGH);
digitalWrite(mix_relay, HIGH);
// Initialize serial and wait for port to open:
Serial.begin(9600);
// Defined in thingProperties.h
initProperties();
// Connect to Arduino IoT Cloud
ArduinoCloud.begin(ArduinoIoTPreferredConnection);
setDebugMessageLevel(2);
ArduinoCloud.printDebugInfo();
Serial.println(mode);
}
void loop()
{
//RETRIEVE CURRENT VALUES FOR TEMPS AND TIME FOR NEXT LOOP
ArduinoCloud.update();
sensors.requestTemperatures(); //Return Temperatures
temp1 = round(sensors.getTempFByIndex(0)); //Fermenter Temperature
temp2 = round(sensors.getTempFByIndex(1)); // Coolant Temperature
Current_Time = millis(); // Clock Update
//_________________________________________________________________________________________________________________
//TIMER SETUP FOR HEATING, COOLING AND PUMP DELAY
// Evaluate how long the cooling relay has been off
if (digitalRead(cooling_relay) == HIGH && last_cool_state != digitalRead(cooling_relay)) // If the Cooling Relay is OFF and it wasn't just OFF
{
Cooling_Last_Off = millis(); // The Current Timestamp is the moment after the Cooling was last OFF
last_cool_state = digitalRead(cooling_relay); //Reset the state to match
}
// Evaluate how long the cooling relay has been on
if (digitalRead(cooling_relay) == LOW && last_cool_state != digitalRead(cooling_relay))// If the Heating Relay is ON and it wasn't just ON
{
Cooling_Last_On = millis(); // The Current Timestamp is the moment after the Cooling was last ON
last_cool_state = digitalRead(cooling_relay); //Reset the state to match
}
// Evaluate how long the heating relay has been off
if (digitalRead(heating_relay) == HIGH && last_heat_state != digitalRead(heating_relay))// If the Heating Relay is OFF and it wasn't just OF
{
Heating_Last_Off = millis(); // The Current Timestamp is the moment after the Heating was last OFF
last_heat_state = digitalRead(heating_relay); //Reset the state to match
}
// Evaluate how long the heating relay has been on
if (digitalRead(heating_relay) == LOW && last_heat_state != digitalRead(heating_relay))// If the Heating Relay is ON and it wasn't just ON
{
Heating_Last_On = millis(); // The Current Timestamp is the moment after the Heating was last ON
last_heat_state = digitalRead(heating_relay); //Reset the state to match
}
// Evaluate how long the pump relay has been off
if (digitalRead(pump_relay) == HIGH && last_pump_state != digitalRead(pump_relay))// If the Pump Relay is OFF and it wasn't just OF
{
Pump_Last_Off = millis(); // The Current Timestamp is the moment after the Heating was last OFF
last_pump_state = digitalRead(pump_relay); //Reset the state to match
}
// Evaluate how long the pump relay has been on
if (digitalRead(pump_relay) == LOW && last_pump_state != digitalRead(pump_relay))// If the Pump Relay is ON and it wasn't just ON
{
last_pump_state = digitalRead(pump_relay); //Reset the state to match
}
//___________________________________________________________________________________________________________
//OPERATING CONDITIONS
//___________________________________________________________________________________________________________
//HEATING REQUIREMENTS : THE HEATING SHOULD TURN ON IF ALL OF THESE ARE YES/TURE/HIGH/1
Heat_Condition1 = temp1 > min_coolant && temp2 > min_coolant; // Are both temperatures warmer than minimum allowable temp? (An unplugged temp probe will default to -197F)
Heat_Condition2 = Current_Time >= Heating_Delay; // Has the ESP been on longer than the set Heating delay?
Heat_Condition3 = temp1 <= (set_temp - deadzone); // Is the Fermenter Temperature lower than or equal to the set temperatur minus the deadzone buffer?
Heat_Condition4 = temp2 < max_coolant; // Is the temperature of the glycol lower than the maximum allowed?
// Check if heating delay has been met
if (digitalRead(heating_relay) == LOW) // If the heat is on
{
Heat_Condition5 = 1; //Keep it On
}
else if ((digitalRead(heating_relay) == HIGH)) //If heat is off
{
Heat_Condition5 = Heating_Off_Time >= Heating_Delay; // Turn it on only IF it has been off longer than the Heating Delay
}
Heat_Condition6 = heating; //Check the IoT lockout input if heating is allowed
//___________________________________________________________________________________________________________________________
//COOLING REQUIREMENTS : THE COOLING SHOULD TURN ON IF ALL OF THESE ARE YES/TURE/HIGH/1
Cool_Condition1 = Current_Time >= Cooling_Delay; // Has the ESP been on longer than the set Cooling delay?
Cool_Condition2 = temp1 >= (set_temp + deadzone); // Is the Fermenter Temperature higher than or equal to the set temperatur plus the deadzone buffer?
Cool_Condition3 = temp2 > min_coolant; // Is the glycol warmer than the minimum allowable temperature?
// Condition to check if cooling delay has been met
if (digitalRead(cooling_relay) == LOW) //If the cooling relay is on
{
Cool_Condition4 = 1; // Keep it on
}
else if ((digitalRead(cooling_relay) == HIGH)) // If the cooling relay is off
{
Cool_Condition4 = Cooling_Off_Time >= Cooling_Delay; // Turn it on only IF it has been off longer than the Cooling Delay
}
Cool_Condition5 = cooling; //Check the IoT lockout input if cooling is allowed
//_____________________________________________________________________________________________________________
// PUMP REQUIREMENTS : THE PUMP SHOULD TURN ON IF ALL OF THESE ARE YES/TURE/HIGH/1
Pump_Condition1 = (abs(temp1 - set_temp) > (deadzone * 0.25) ); // If the fermenter temperature is more than 75% of the deadzone buffer away from the nominal desired temperature
Pump_Condition2 = (set_temp > temp2 > temp1) || (set_temp < temp2 < temp1) || (temp2 > set_temp > temp1) || (temp2 < set_temp < temp1); // Conditions to make sure the glycol will only pump if it is closer to the set temperature than the fermenter is.
// Condition to check if Pump Delay has been met
if (digitalRead(pump_relay) == LOW) // If the pump is already on
{
Pump_Condition3= 1; // Keep it On
}
else if ((digitalRead(pump_relay) == HIGH)) //If pump is off
{
Pump_Condition3 = Pump_Off_Time >= Pump_Delay; // Turn it on If it has been off longer than the Pump Delay
}
Pump_Condition4 = pump; //Check the IoT lockout input if the pump is allowed
//_________________________________________________________________________________________________________________
// MIX REQUIREMENTS : THE MIX SHOULD TURN ON IF ALL OF THESE ARE YES/TURE/HIGH/1
Mix_Condition1 = (Heating_Off_Time < Mix_Delay) || (Cooling_Off_Time < Mix_Delay); // Has the heating or cooling been on within the last 2 min? (Mix Delay Set Time)
Mix_Condition2 = Current_Time > Mix_Delay; // Has the ESP32 been on longer than the Mix Delay?
Mix_Condition3 = mix; //Check the IoT lockout input if the mix is allowed
// If all the respective conditions are Yes/True/High/1 - Rewrite the demand variable to High/True/1
demand_heat = (Heat_Condition1 && Heat_Condition2 && Heat_Condition3 && Heat_Condition4 && Heat_Condition5 && Heat_Condition6);
demand_cool = (Cool_Condition1 && Cool_Condition2 && Cool_Condition3 && Cool_Condition4 && Cool_Condition5);
demand_pump = (Pump_Condition1 && Pump_Condition2 && Pump_Condition3 && Pump_Condition4);
demand_mix = (Mix_Condition1 && Mix_Condition2 && Mix_Condition3);
Serial.println((String)"Demand_Heat:" + demand_heat + " Demand_Cool:" + demand_cool + " Demand_Pump:" + demand_pump + " Demand_Mix:" + demand_mix);
// Conditions are met to Turn Heat On
if (demand_heat == 1)
{
digitalWrite(heating_relay, LOW); // Turn Heating Relay ON
heat_state = "ON"; // Declare the Heating Relay ON
Heating_On_Time = Current_Time - Heating_Last_On; // Update the Timestamp that the Heating Relay was recently turned ON
Heating_Off_Time = 0; // Continually reset the Timestamp that the Heating Relay in no longer OFF
}
else if (demand_heat == 0)
{
digitalWrite(heating_relay, HIGH); // Turn Heating Relay ON
heat_state = "OFF"; // Declare the Heating Relay OFF
Heating_Off_Time = Current_Time - Heating_Last_Off; // Update the Timestamp that the Heating Relay was recently turned OFF
Heating_On_Time = 0; // Continually reset the Timestamp that the Heating Relay in no longer ON
}
// Conditions are met to Turn Cooling On
if (demand_cool == 1)
{
digitalWrite(cooling_relay, LOW);
cool_state = "ON";
Cooling_On_Time = Current_Time - Cooling_Last_On;
Cooling_Off_Time = 0;
}
else if (demand_cool == 0)
{
digitalWrite(cooling_relay, HIGH);
cool_state = "OFF";
Cooling_Off_Time = Current_Time - Cooling_Last_Off;
Cooling_On_Time = 0;
}
// Conditions to Turn Pump On
if (demand_pump == 1)
{
digitalWrite(pump_relay, LOW);
pump_state = "ON";
Pump_Off_Time=0;
}
else if (demand_pump == 0)
{
digitalWrite(pump_relay, HIGH);
pump_state = "OFF";
Pump_Off_Time = Current_Time - Pump_Last_Off;
}
// Conditions to Turn Mix
if (demand_mix == 1)
{
digitalWrite(mix_relay, LOW);
mix_state = "ON";
}
else if (demand_mix == 0)
{
digitalWrite(mix_relay, HIGH);
mix_state = "OFF";
}
//_____________________________________________________________________________________________________________
//Report Status and Pause Before Repeating Code
if (((Current_Time <= Cooling_Delay) || (Current_Time <= Heating_Delay)) && digitalRead(pump_relay) == HIGH && digitalRead(mix_relay) == HIGH)
{
if (Heat_Condition2 && Heat_Condition3 && Heat_Condition4 == 1)
{
mode = ((String)"START UP DELAY - HEATER ON IN: " + (Heating_Delay - Current_Time) / 1000 + " Seconds");
}
else if (Cool_Condition2 && Cool_Condition3 == 1)
{
mode = ((String)"START UP DELAY - COOLER ON IN: " + (Cooling_Delay - Current_Time) / 1000 + " Seconds");
}
}
if (temp1 <= 0 || temp2 <= 0)
{
mode = ((String)"THERMOCOUPLES NOT FOUND! :");
}
if (digitalRead(heating_relay) == LOW && digitalRead(pump_relay) == LOW && digitalRead(mix_relay) == LOW && digitalRead(cooling_relay) == HIGH)
{
mode = ((String)"ACTIVE HEATING: ");
}
if (digitalRead(cooling_relay) == LOW && digitalRead(pump_relay) == LOW && digitalRead(mix_relay) == LOW && digitalRead(heating_relay) == HIGH)
{
mode = ((String)"ACTIVE COOLING: ");
}
if (temp1 > set_temp && digitalRead(pump_relay) == LOW && digitalRead(heating_relay)==HIGH && digitalRead(cooling_relay) == HIGH)
{
if (Cool_Condition1 && Cool_Condition2 && Cool_Condition3 == 1)
{
mode = ((String)"SHORT CYCLE COOLING DELAY: PASSIVE COOLING INSTEAD - COOLER ON IN: " + (Cooling_Delay - (Current_Time-Cooling_Last_Off)) / 1000 + " Seconds");
}
else
{
mode = ((String)"PASSIVE COOLING: ");
}
}
if (temp1 < set_temp && digitalRead(pump_relay) == LOW && digitalRead(heating_relay)==HIGH && digitalRead(cooling_relay) == HIGH)
{
if (Heat_Condition1 && Heat_Condition2 && Heat_Condition3 && Heat_Condition4 == 1)
{
mode = ((String)"SHORT CYCLE HEATING DELAY: PASSIVE HEATING INSTEAD - HEATER ON IN: " + (Heating_Delay - (Current_Time-Heating_Last_Off)) / 1000 + " Seconds");
}
else
{
mode = ((String)"PASSIVE HEATING: ");
}
}
if (digitalRead(cooling_relay) == LOW && digitalRead(pump_relay) == HIGH)
{
mode = ((String)"CHILLING COOLANT: ");
}
if (digitalRead(heating_relay) == LOW && digitalRead(pump_relay) == HIGH)
{
mode = ((String)"HEATING COOLANT: ");
}
if (digitalRead(heating_relay) == HIGH && digitalRead(cooling_relay) == HIGH && digitalRead(pump_relay) == HIGH && abs(set_temp - temp1) <= deadzone)
{
mode = ((String)"TARGET TEMPERATURE REACHED - STANDBY: ");
}
else
{
status_update = ((String)mode + "Heating:" + heat_state + "/" + heating + " Cooling:" + cool_state + "/" + cooling + " Pump:" + pump_state + "/" + pump + " Agitation:" + mix_state + "/" + mix);
mode=((String)status_update);
}
delay(500); // 10 Second Update Delay
}
// State Change Actions
void onSet1Change() {
if (temp1>set_temp)
{
set_temp= set1-(0.75*deadzone);
}
else if (temp1<set_temp)
{
set_temp= set1+(0.75*deadzone);
}
else
{
set_temp=set1;
}
}
void onCoolingChange()
{
if (digitalRead(cooling)==0)
{
mode=((String)"COOLING DISABLED");
}
else
{
mode=((String)"COOLING ENABLED");
}
}
void onHeatingChange()
{
if (digitalRead(heating)==0)
{
mode=((String)"HEATING DISABLED");
}
else
{
mode=((String)"HEATING ENABLED");
}
}
void onPumpChange()
{
if (digitalRead(pump)==0)
{
mode=((String)"PUMP DISABLED");
}
else
{
mode=((String)"PUMP ENABLED");
}
}
void onMixChange()
{
if (digitalRead(mix)==0)
{
mode=((String)"COOLING DISABLED");
}
else
{
mode=((String)"COOLING ENABLED");
}
}