Logic question about a climate control function with temperature and humidity

Hi everyone,

I'm working on a project to replace a failing "smart habitat" for my dart frogs. I have connected 3 types of sensors, 2 of each type, and I can read and use the values and averages thereof.
I can set and save values for timers I'm using to control the lights and sprinklers through a relay module. So far so good :slight_smile:

Now I want to have the Arduino Mega 2560 control the climate by comparing the sensor data to user set values. I might move the whole project to a Nano 33 IoT to get easy app-access, but I think the Nano should be able to do this without issues.

While writing down my ideas I noticed some conditions can occur simultaniously, and I am unsure if this will cause problems.
The arduino can increase and decrease temperature with either a heating cable, or powering fans to drive the warm air out. To control the humidity I can use an ultrasonic fogger and the sprinklers to increase or the fans to decrease.

My initial idea is as follows:

void climateControl() {

  float AirTempA , HumidityA; // A for average
  static unsigned int maxAirTemp, minAirTemp;   //set or changed by user 
  static unsigned int maxHumidity, minHumidity;

  if (AirTempA > maxAirTemp) {
    //turn on fans
    if (AirTempA > maxAirTemp && HumidityA < minHumidity) {
      //turn on sprinklers and wait a bit (endothermic evaporation), then turn on fans again. unlikely to happen.
    }
    if (AirTempA > maxAirTemp && HumidityA > maxHumidity) {
      //turn on all the fans maximum power
    }
  }
  if (AirTempA < minAirTemp) {
    //turn on heating
    if (AirTempA < minAirTemp && HumidityA < minHumidity) {
      //turn on heating and ultrasonic fogger
    }
    if (AirTempA < minAirTemp && HumidityA > maxHumidity) {
      //turn on heating & fans? maybe postpone or skip next programmed rain
    }
  }

  if (HumidityA > maxHumidity) {
    //turn on fans
    if (HumidityA > maxHumidity && AirTempA < minAirTemp) {
      //turn on fans and heating
    }
    if (HumidityA > maxHumidity && AirTempA > maxAirTemp) {
      //turn on all fans maximum power
    }
  }
  if (HumidityA < minHumidity) {
    //turn on fogger
    if (HumidityA < maxHumidity && AirTempA < minAirTemp) {
      //turn on ultrasonic fogger and heating
    }
    if (HumidityA < maxHumidity && AirTempA > maxAirTemp) {
      //turn on sprinklers and wait for a bit, then turn fans on again
    }
  }
}

As you can see there are double events where conditions are the same after an initial reading triggers and action.

I think I could take the doubles out and make them unique events, not preceded by another if() condition. However the initial condition to trigger the event would be the most important, the secondary && triger would likely be caused by the initial event/action.

Also as it stands now multiple different conditions could trigger the same event. I am unsure if this will cause a problem or not. Should I add flags so that if 1 event is active, others won't be able to trigger? I could add a condition to every event if ( != active), so it won't trigger if it's already active.

I'm a little worried something might cascade and cause extremes that are bad for frogs and plants. I do plan on adding alerts if certain values pass certain limits for that.

If that trigger is (for example) to set a digital output HIGH to turn something on, no problem.

Conflicting triggers, or conflicting actions are obviously a problem.

It might be helpful to draw up a decision tree, or state diagram.

Thanks @jremington for the decicion tree. I have added a timer in the conditions to prevent conditions to influence each other rapidly. I am still looking into hysteresis for the sensor readings. Not sure how to implement it yet, but for now I think a 10 seond delay will be alright (if I wrote it corectly)

This is the current state, some parts left out since programming them will be much of the same.

void climateControl() {

  static unsigned int maxAirTemp, minAirTemp;
  static unsigned int maxHumidity, minHumidity;
  static unsigned int maxWaterTemp, minWaterTemp;
  bool hold = false;
  unsigned long startMillis;   
  unsigned long currentMillis;
  const unsigned long doubleCheck = 10000;  //wait for 10 seonds
  currentMillis = millis();
  int PWMVal;
  int PWM1 = 7;
  pinMode(PWM, OUTPUT);

  if (AirTempA > maxAirTemp && != hold) {
    if (currentMillis - startMillis > doubleCheck) {  // wait to see if reading was correct
      if (AirTempA < minAirTemp) {
        AirTempA = minAirTemp;  // this will probably mess up the data written on my screen/app. Need a secondary int for logic? Maybe AirTempTrigger = AirtempA
      }
      if (AirTempA > maxAirTemp) {
        AirTempA = maxAirTemp;
      }
      PWMVal = map(AirTempA, minAirTemp, maxAirTemp, 0, 255);  //still have to work out better thresholds
      digitalWrite(PWM1, PWMVal);
      startMillis = currentMillis;
    }
  }

if (AirTempA > maxAirTemp && HumidityA > maxHumidity) {
    //turn on all the fans maximum power
    PWMVal = map(AirTempA, minAirTemp, maxAirTemp, 0, 255);  //still have to work out better thresholds
    digitalWrite(PWM1, PWMVal);
    hold = true;  // flag to disable singular conditions
    if (AirTempA <= maxAirTemp || HumidityA <= maxHumidity) {
      if (currentMillis - startMillis > doubleCheck) {
        hold = false;
        startMillis = currentMillis;
      }
    }
  }
  if (AirTempA > maxAirTemp && HumidityA < minHumidity) {
    //turn on sprinklers and wait a bit (endothermic evaporation), then turn on fans again. unlikely to happen.
    hold = true;
    if (currentMillis - startMillis < 10000) {  // sprinklers on for 10 seconds
      relay6_state = LOW;                       // on
    } else {
      relay6_state = HIGH;  //sprinklers off
      startMillis = currentMillis;
      PWMVal = map(AirTempA, minAirTemp, maxAirTemp, 0, 255); //fan on
      digitalWrite(PWM1, PWMVal);
    }
    if (AirTempA <= maxAirTemp || HumidityA >= minHumidity) {
      if (currentMillis - startMillis > doubleCheck) {
        hold = false;
        startMillis = currentMillis;
      }
    }
  }

Is there anything I'm overlooking, or just plain wrong here?

By the end of the week I'm expecting some hardware to build a test setup. Dry and outside of the box, just putting in min and max values I know I can manupulate to test the output.

There is nothing much simpler than the hysteresis algorithm:

if value exceeds upper limit
{
  turn on
}
if value exceeds lower limit
{
 turn off
}
else
{
  do nothing
}

The principle is simple, but I am unsure where or how to implement it. Or if I already have it and should leave it at that.

I get data from sensor 1 and sensor 2 and combine them to make readingAverage.

If ( readingAverage > minimum value ) { do something }

That in itself is a sort of hysteresis, but I could also apply it to the initial sensor 1 and 2 values. This would make my average reading somewhat elastic and I'm wondering if that's good thing or not.

This is incorrect in C. Does it even compile?

Indeed it does not. I was just sketching my ideas.

I've now changed them to " hold == false "

If you make any changes, please repost the entire modified sketch. We can't offer concrete suggestions on "ideas", if we do, they are also just "ideas". It's a better policy to attempt to write actual code.

If you are attempting to communicate the logic of the program, C code is a bad way to do it. It is a language to talk to machines, not humans. Pseudocode, diagrams or very specific language can work.

Trying to deduce your intentions from the proposed code you posted, is like solving a Mensa puzzle.

Right now, your code ideas and design ideas are conflated. You need to separate them, explain your design, and then you can get some good help here about implementing it in C.

alright, I'll try to keep ideas and code seperate and compile any code before I post them.

Currently this is the state of the code:

void climateControl() {

  static unsigned int maxAirTemp, minAirTemp;
  static unsigned int maxHumidity, minHumidity;
  //static unsigned int maxWaterTemp, minWaterTemp;
  bool hold = false;
  unsigned long startMillis;    //both startMillis and currentMillisare defined at the top
  unsigned long currentMillis;
  const unsigned long doubleCheck = 10000;  //wait for 10 seonds
  currentMillis = millis();
  int PWMVal;
  int PWM1 = 7;

  if (AirTempA > maxAirTemp && hold == false) {
    if (currentMillis - startMillis > doubleCheck) {  // wait to see if reading was correct
      if (AirTempA < minAirTemp) {
        AirTempA = minAirTemp;  // this will probably mess up the data written on my screen/app. Need a secondary int for logic? Maybe AirTempTrigger = AirtempA
      }
      if (AirTempA > maxAirTemp) {
        AirTempA = maxAirTemp;
      }
      PWMVal = map(AirTempA, minAirTemp, maxAirTemp, 0, 255);  //still have to work out better thresholds
      digitalWrite(PWM1, PWMVal);
      startMillis = currentMillis;
    }
  }
  if (AirTempA < minAirTemp && hold == false) {
    //turn on heating
  }

  if (HumidityA > maxHumidity && hold == false) {
    //turn on fans
  }
  if (HumidityA < minHumidity && hold == false) {
    //turn on fogger
  }

  if (AirTempA > maxAirTemp && HumidityA > maxHumidity) {
    //turn on all the fans maximum power
    PWMVal = map(AirTempA, minAirTemp, maxAirTemp, 0, 255);  //still have to work out better thresholds
    digitalWrite(PWM1, PWMVal);
    hold = true;  // flag to disable singular conditions
    if (AirTempA <= maxAirTemp || HumidityA <= maxHumidity) {
      if (currentMillis - startMillis > doubleCheck) {
        hold = false;
        startMillis = currentMillis;
      }
    }
  }
  if (AirTempA > maxAirTemp && HumidityA < minHumidity) {
    //turn on sprinklers and wait a bit (endothermic evaporation), then turn on fans again. unlikely to happen.
    hold = true;
    if (currentMillis - startMillis < 10000) {  // sprinklers on for 10 seconds
      relay6_state = LOW;                       // on
    } else {
      relay6_state = HIGH;  //sprinklers off
      startMillis = currentMillis;
      PWMVal = map(AirTempA, minAirTemp, maxAirTemp, 0, 255);  //fan on
      digitalWrite(PWM1, PWMVal);
    }
    if (AirTempA <= maxAirTemp || HumidityA >= minHumidity) {
      if (currentMillis - startMillis > doubleCheck) {
        hold = false;
        startMillis = currentMillis;
      }
    }
  }

i find it difficult to understand for sure what the logic is

i think the code could be simplified by broadly creating an if statement for when the temp > max threshold and under that condition consider other conditions such as the humidity, ...

i think digitalWrite()s to motors and relays could all be done at the end of the code, regardless if needed. this includes the use of map() and hard limiting the pwm value instead of the temperature.

the goal is to be able to locate the one location in the code when such and such an decision or action takes place.

it's also convention to Capitalize Constants. PWM1 is proper but PWMVal should be pwmVal

And I thought I was too stupid to understand that too.

after trying to go thru the code, it seems it mostly boils down to the following.

the PWM value is proportional to the air temperature within the limits.
i don't understand your use of timers

void climateControl ()
{
    unsigned long msec     = millis();
    tempAir   = map (analogRead (A1), 0, 1023, 55, 75);
    humidity  = map (analogRead (A2), 0, 1023, 70, 95);

    pwmVal = map(tempAir, TempMin, TempMax, 0, 255);
    if (pwmVal > 255)
        pwmVal = 255;
    else if (pwmVal < 0)
        pwmVal = 0;

    if (HumidMax < humidty) {
        if (TempMax < tempAir) {
            pwmVal = 255;
    }
    else if (HumidMin > humidty) {
    }

    digitalWrite(PinPwm, pwmVal);
    digitalWrite(PinRly, relay6);
}

i think you would find it helpful to provide output such as the following every second

 temp  75, hum  95, pwm  382, relay 0, hold 1
 temp  75, hum  95, pwm  382, relay 0, hold 1
 temp  75, hum  95, pwm  382, relay 0, hold 1
 temp  75, hum  95, pwm  382, relay 0, hold 1
 temp  75, hum  95, pwm  382, relay 0, hold 1
 temp  75, hum  95, pwm  382, relay 0, hold 1
 temp  75, hum  95, pwm  382, relay 0, hold 1
 temp  75, hum  95, pwm  382, relay 0, hold 1
 temp  75, hum  95, pwm  382, relay 0, hold 1
 temp  75, hum  95, pwm  382, relay 0, hold 1
 temp  75, hum  95, pwm  382, relay 0, hold 1
    sprintf (s, " temp %3d, hum %3d, pwm %4d, relay %d, hold %d",
        tempAir, humidity, pwmVal, relay6, hold);
    Serial.println (s);

Thanks, I'll keep that in mind. And thank you for your input, let me try and explain the timers.

The 10 second delay (doubleCheck) is firstly to make sure the condition is not triggered by an abnormal sensor reading and secondly to make the transition a little slower. Should it happen that 1 factor keeps causing a trigger I would like it to be a slow ON and OFF loop instead of a fast one.

The other delay, after the sprinklers have come on, is so that the fans won't immediately come on after the sprinklers have finished. This is because the sprinklers produce a very fine mist that does not settle immediately. If the fans come on directly after the sprinklers they blow a lot of fine water droplets through the ventilation grids, into the lighting. This has previously caused corrosion.

Every morning there will also be a situation where the humidity will be 100% and the temperature will be below minimum. So humidity will cause the fans to come on (they will be inactive during the night) but at the same time I want to increase temperature. Come to think of it, I might put a pair of fans close to the LED's that are giving off heat. Maybe I can create warm air and cool air this way.

The sprintf does indeed look very usefull, maybe I can even use it to get a graph down the line. The DHT22 sensor's sampling rate in only once every 2 seconds though. I suppose it doesn't matter if I lower the print frequency, right?

not clear how this works. shouldn't there be a 2nd read of the sensor and some indication that there have been 2 "good" readings. perhaps a check of the current and previous measurements

this suggests the need to have a separate timer that can delay turning on the fans, or perhaps a sequencer that first turns off the sprinklers after a delay and then turns on the fans and possibly repeats this sequence

My thought is that if the first reading it true, if the measured value has crossed the threshold, so long as it stays above the threshold the second reading will be true as well, so then the execution of the action can take place. In my mind that was the same as 2 "good" readings.

this would indeed be handy, also for the pre-programmed times the sprinklers come on (2-4 times per day). I'll try to find out more about sequencers.

still don't see a description of a mechanism in the code to prevent activation due to an invalid reading. don't understand why your worried about an invalid reading. if the reading is noisy, a little bit of filtering can usually smooth things out to avoid false activation

what i'm suggesting is that there a separate piece of logic that is driven by a timer and en/disabled by the code above

breaking the problem into separate pieces is often a lot easier that than somehow bundling it all up together.

i don't have time now to put anything together to demonstrate

I think I have something like that. I'll try and adapt it to see if this is what you meant.

#include <DHT.h>
#define RelayPin6 45
#define DHT22_PIN 3
#define DHTTYPE DHT22
DHT dht(DHT22_PIN, DHTTYPE);

void delayedStart() {

  boolean relay6_state = HIGH;
  int inputGiven = 0;
  unsigned long msec = millis();
  unsigned long startMillis;
  static unsigned int humidity, maxHumidity, maxAirTemp, minAirTemp;
  humidity = map(analogRead(A2), 0, 1023, 70, 95);
  int pwmVal;
  int PWM1 = 7;
  float airTemp = dht.readTemperature();

  pwmVal = map(airTemp, minAirTemp, maxAirTemp, 0, 255);
    if (pwmVal > 255)
        pwmVal = 255;
    else if (pwmVal < 0)
        pwmVal = 0;

  while (inputGiven < 3) {
    switch (inputGiven) {
      case 0:
        if ((humidity > maxHumidity) == true) {
          inputGiven = 1;
        };
        break;
      case 1:
        if ((msec - startMillis > 10000) == true) {
          inputGiven = 2;
          startMillis = msec;
        };
        break;
      case 2:
        if ((humidity > maxHumidity) == true) {
          inputGiven = 3;
        };
        break;
    }
  }
  digitalWrite(PWM1, pwmVal);
  digitalWrite(RelayPin6, LOW); 
}

BTW I was wondering why you mapped humidity before putting it in another map for pwmVal. Is it to get a better resolution, or a hard min/max value?

this is a simple example of sequencer code and its output

111351 - sprinkler on
133311 - sprinkler off
142055 - fan on
204015 - fan off - sprinkler on
221151 - sprinkler off
225535 - fan on
251455 - fan off - sprinkler on
305031 - sprinkler off
313415 - fan on
335335 - fan off - sprinkler on
352511 - sprinkler off
401255 - fan on
423215 - fan off - sprinkler on
440351 - sprinkler off
unsigned long msecPeriod;
unsigned long msecLst;

enum { Off, Mist, Pause, Fan };
int state = Off;

// -----------------------------------------------------------------------------
void
loop (void)
{
    unsigned long msec = millis ();
    if (Off != state && msec - msecLst >= msecPeriod)  {
        msecLst = msec;
        Serial.print   (msec);

        switch (state) {
        case Mist:
            Serial.println (" - sprinkler off");
            msecPeriod = 1000;
            state = Pause;
            break;

        case Pause:
            Serial.println (" - fan on");
            msecPeriod = 3000;
            state      = Fan;
            break;

        case Fan:
            Serial.println (" - fan off - sprinkler on");
            msecPeriod = 2000;
            state      = Mist;
            break;

        }
    }

    if (Serial.available ())  {
        char c = Serial.read ();

        if ('m' == c)  {
            Serial.print   (msec, 6);
            Serial.println (" - sprinkler on");
            msecPeriod = 3000;
            msecLst    = msec;
            state = Mist;
        }
        else if ('s' == c)  {
            Serial.println ("sprinkler off");
            Serial.println ("fan off");
            state = Off;
        }
    }
}

void
setup (void)
{
    Serial.begin (9600);
}

i used a multifunction shield with button switches connected to the analog inputs which results in the input being either 0 or 1023. the map just made the values more realistic

I have been staring at the code for a while now, and I think I understand most of it... but some things I can't wrap my head around.

If I understand correctly the printed message of each state "announces" the next state, which starts after the delay set with the msecPeriod of each state. So state Mist executes 2 seconds after Fan, right?

But where do 'm' and 's' come from, when compared to the char?

Thank you btw for taking the time to write this for me. It's a little more than I expected, but I'm learning a freat deal, so thanks :slight_smile:

Here?

The 'm' is a constant in a yoda-notation comparison:

https://forum.arduino.cc/t/what-is-it-about-this-community/1124363/142?u=davex

1 Like