Smoothing out value transitions

Hi eveyone,

A new day a new question it seems, sorry :sweat_smile:

For the terrarium climate control project I use some fans with a PWM signal. I can manually set the temperature I'd like it to be inside and the fans (and heating) will do the rest.

At certain points during the day I want to simulate changing conditions like mid-day heat, rain or night. These conditions drive a multiplier that changes the 'desired temperature' at that moment. When there is a change the fans go full blast and conditions inside spike.

I'd like to make the transition gradually so I searched and found some ways to do that but I can't make it work, my head is spinning. Here is what I have at the moment:

uint8_t setp = 0;
float oldAirTemp = 23;

  if (desiredAirTemp != oldAirTemp) { //If the program sets a new target temperature, start to transition gradually. Step keeps track of the increments    
    
    desiredAirTemp = smoothTransition(oldAirTemp, desiredAirTemp, 5, 10000, step); //In this case 5 intervals of 1 minute
  } else {
    step = 0; //Reset step when target is reached and update oldAirTemp
    oldAirTemp = desiredAirTemp;
  }

float smoothTransition(float start, float end, uint8_t intervals, unsigned long intervalTime, uint8_t step) {

  float delta = (end - start) / intervals;

  if (millis() - lastUpdateTime >= intervalTime && step < intervals) { 
    start += delta;
    step++;
  }
  lastUpdateTime = millis();
    
  return start;
}

The problem is desiredAirTemp calls upon itself (maybe I'm not saying that right, I hope you know what I mean)
How can I get out of this self-mutilating loop?

I don't recall, have you posted here before? If not, please take a look at:

If you have, then I suspect it's time for a refresher. Why am I saying this?

You've repeated this disrespectful behavior multiple times. For programming problems, it's almost always necessary to have the complete code, which we can then quickly copy to the IDE to see what might be the problem. Only alternative to that is to produce a smaller, simple code that demonstrates the same problem, and post that, explaining the behavior. What you've done is attempt to force us to develop our own wrapper for your snippet, trying to see, dimly, what all the fuss is about.
Ain't happening. Good luck. I've you, because, well, I can.

The snippet I've posted has been kept as simple as possible on purpose because I don't want to force anyone to put a lot of effort into it. My reasoning is that I've made an error in logic. Perhaps easy to spot for someone with experience, where my lack thereof blinds me.

It seems I have achieved the opposite. Or at least as far as you are concerned.

Here is the larger part of the code:

void climateSettings() {
  
  //adjust settings for seasons with multiMap arrays. 
  //date[] is the 'x-axis' and the xModifier is the 'y-axis' for a graph to calculate all points based on weather data from Costa Rica      
  float date[13] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; //months. 13 is added so month 12 can have 'days'
  float tempertureModifier[13] = {0.96, 0.92, 0.92, 0.92, 0.92, 0.96, 1.12, 1.04, 1, 0.96, 0.92, 0.96, 0.96}; //based on 25 degrees celsius
  float humidityModifier[13] = {1.2, 1.15, 1.09, 1.04, 1.01, 1, 0.97, 1, 1.13, 1.2, 1.07, 1.13, 1.2}; //based on 75% RH --- depending on sensor positions you might need to change the base
  float today = getDay() * (1/32) + getMonth(); //today is number of the month plus a fraction for each day, filling out the date[] graph axis

  humidityVariation = multiMap(today, date, humidityModifier, 13);
  temperatureVariation = multiMap(today, date, tempertureModifier, 13);
  
  //adjust settings for daytime  
  if (getLocalHour() == 13 || getLocalHour() == 14) { //midday
    daytimeHumVariation = 0.9; 
    daytimeTempVariation = 1.1;
  } else if (getLocalHour() >= 23 || getLocalHour() <= 7) { //night
    daytimeHumVariation = 1.1; 
    daytimeTempVariation = 0.8;
  } else {
    daytimeHumVariation = 1; 
    daytimeTempVariation = 1;
  }

  //adjust settings for weather outside
  if (rainyDay) {
    weatherVariation = 1.1; //10% more humidity
  } else {    
    weatherVariation = 1;
  }

  humModifier = humidityVariation * daytimeHumVariation * weatherVariation;
  tempModifier = temperatureVariation * daytimeTempVariation;

  //everything times 10 for better resolution
  minAirTemp = EEPROM.readByte(60) * 10;
  maxAirTemp = EEPROM.readByte(61) * 10;
  minHumidity = EEPROM.readByte(62) * 10;
  maxHumidity = EEPROM.readByte(63) * 10;
  dayWaterTemp = EEPROM.readByte(dayTemperature) * 10;
  nightWaterTemp = EEPROM.readByte(nightTemperature) * 10;
  desiredAirTemp = (EEPROM.readByte(desiredTemp) * 10) * tempModifier;
  desiredHumidity = (EEPROM.readByte(desiredHumi) * 10) * humModifier;  
  if (desiredAirTemp < (waterTemp - hysteresis / 4)) { //cannot cool lower that outside temperature. For now waterTemp = external airTemp
    desiredAirTemp = waterTemp - hysteresis / 4; //compensate for hysteresis upper limit 
  }
  airHigh = desiredAirTemp + (hysteresis / 2);  
  airLow = desiredAirTemp - (hysteresis / 2); 
  humidityHigh = desiredHumidity + hysteresis;
  humidityLow = desiredHumidity - hysteresis;  
  
  /*
  outputDebug("Temperature modifier: ");
  outputDebugln(tempModifier);
  outputDebug("Humidity modifier: ");
  outputDebugln(humModifier);
  */
}

void climateControl() {  

  airTemp = airTempAverage * 10; //all average readings have 1 decimal number. Scale up by 10 to work with ints
  humidity = humidityAverage * 10;
  waterTemp = waterTempAverage * 10;

  if (dayTime) { //day schedule

    hold = false;

    if (humidity >= maxHumidity) { //disable sprinklers
      noRain = true;
    } else { noRain = false; }

    if (waterTemp - 10 < dayWaterTemp) {
      digitalWrite(relay[5], HIGH);  //land heat on
    }
    if (waterTemp >= dayWaterTemp) {
      digitalWrite(relay[5], LOW);   //land heat off
    }
    
    //if all conditions are within half the hysteresis margins, do nothing
    if (airTemp <= (desiredAirTemp + hysteresis / 4) && airTemp >= (desiredAirTemp - hysteresis / 4) && humidity <= (desiredHumidity + hysteresis / 2) && humidity >= (desiredHumidity - hysteresis / 2)) {
      climateState = CASE0;
    }
    
    if (humidityMonitoring) {
      if (airTemp > airHigh && humidity > humidityHigh) {
        climateState = CASE1;
      }

      else if (airTemp < airLow && humidity < humidityLow) {
        hold = true;
        climateState = CASE2;
      }

      else if (airTemp > airHigh && humidity < humidityLow) {
        hold = true;  // prevent other singular trigger
        climateState = CASE3;
      }

      else if (airTemp < airLow && humidity > humidityHigh) {
        hold = true;
        climateState = CASE4;
      }
    }

    if (!hold) {
      if (humidity > humidityHigh && humidityMonitoring) {
        climateState = CASE5;
      }    

      else if (airTemp > airHigh) {
        climateState = CASE6;
      }

      else if (humidity < humidityLow && humidityMonitoring) {
        climateState = CASE7;
      }

      else if (airTemp < airLow) {
        climateState = CASE8;
      }
    }
  }

  if (!dayTime) { //night schedule

    if (waterTemp - 10 < nightWaterTemp) {
      digitalWrite(relay[5], HIGH);  //land heat on
    }
    if (waterTemp >= nightWaterTemp) {
      digitalWrite(relay[5], LOW);   //land heat off
    }
    
    if (humidity >= humidityLow && humidity <= humidityHigh) { 
      climateState = CASE0; 
    }
    
    else if (airTemp > airHigh) {
      //climateState = CASE9; //pushes humidity too low in summer
    }
    else if (humidity < humidityLow) {
      climateState = CASE10;
    }

    else if (airTemp < minAirTemp) {
      climateState = CASE11;
    }

    else if (humidity > (970)) {
      climateState = CASE12;
    }
  }
}

void climateChange() { 

  airTemp = airTempAverage * 10; //all average readings have 1 decimal number
  humidity = humidityAverage * 10;
  waterTemp = waterTempAverage * 10;
  
  
  if (desiredAirTemp != oldAirTemp) { //If the program sets a new target temperature, start to transition gradually. Step keeps track of the increments    
    
    desiredAirTemp = smoothTransition(oldAirTemp, desiredAirTemp, 5, 10000, step); //In this case 5 intervals of 1 minute
  } else {
    step = 0; //Reset step when target is reached and update oldAirTemp
    oldAirTemp = desiredAirTemp;
  }
  /*
  if (desiredHumidity != oldHumidity) { 
    desiredHumidity = smoothTransition(oldHumidity, desiredHumidity, 5, 10000, humStep, 0); 
  } else {
    humStep = 0; 
    oldHumidity = desiredHumidity;  
  }
  */
  
  if (climateState != previousClimateState) {
    switch (climateState) {

      case CASE0:                      //default do nothing
        digitalWrite(relay[1], LOW);  //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1); 
        digitalWrite(relay[6], LOW);  //window heating off --- ??? maybe not?
        if (humidityMonitoring) {
          digitalWrite(relay[4], LOW);  //fogger off
        }
        outputDebugln("CASE 0");
        break;

      case CASE1:                      //decrease temperature and decrease humidity
        digitalWrite(relay[1], HIGH);  //fans on
        digitalWrite(relay[6], LOW);   //window heating off
        digitalWrite(relay[4], LOW);   //fogger off
        outputDebugln("CASE 1 airTemp high & humidity high --- fans on max");
        startup = 0;
        startDelay = millis();
        break;

      case CASE2:                      //increase temperature and increase humidity
        digitalWrite(relay[1], LOW);   //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1);
        digitalWrite(relay[4], HIGH);  //fogger on
        digitalWrite(relay[6], HIGH);  //window heating on
        outputDebugln("CASE 2 airTemp low & humidity low --- heating on & fogger on");
        break;

      case CASE3:                      //decrease temperature and increase humidity
        digitalWrite(relay[6], LOW);  //window heating off
        //running sequence below
        outputDebugln("CASE 3 airTemp high & humidity low --- fans off & sprinklers on");
        break;

      case CASE4:                      //increase temperature and decrease humidity
        digitalWrite(relay[6], HIGH);  //heating on
        digitalWrite(relay[1], HIGH);  //fans low        
        pwmVal1 = 60;                  //low state --- needs testing --- 23% is lowest setting for current fans to start moving
        analogWrite(pwmPin_1, pwmVal1);  
        outputDebugln("CASE 4 airTemp low && humidity high --- heating on & fans low");         
        break;

      //single states
      case CASE5:                      //decrease humidity
        if (humidityMonitoring) {
          digitalWrite(relay[4], LOW); //fogger off
          digitalWrite(relay[1], HIGH);//fans on
          startup = 0;
          startDelay = millis();
        }
        outputDebugln("CASE 5 humidity high --- fans on");
        break;

      case CASE6:                      //decrease temperature
        digitalWrite(relay[6], LOW);   //window heating off
        digitalWrite(relay[1], HIGH);  //fans on
        outputDebugln("CASE 6 airTemp high --- fans on");
        startup = 0;
        startDelay = millis();
        break;

      case CASE7:                      //increase humidity
        digitalWrite(relay[1], LOW);   //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1);
        digitalWrite(relay[4], HIGH);  //fogger on
        outputDebugln("CASE 7 humidity low --- fogger on");
        break;

      case CASE8:                      //increase temperature
        digitalWrite(relay[1], LOW);   //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1);
        digitalWrite(relay[6], HIGH);  //window heat on
        outputDebugln("CASE 8 airTemp low --- heat on");
        break;

      //night
      case CASE9:                      //decrease temperature
        digitalWrite(relay[6], LOW);   //window heating off
        digitalWrite(relay[1], HIGH);  //fans on
        outputDebugln("CASE 9 airTemp high --- fans on");
        startup = 0;
        startDelay = millis();
        break;

      case CASE10:                     //increase humidity
        digitalWrite(relay[1], LOW);   //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1);
        digitalWrite(relay[4], HIGH);  //fogger on
        outputDebugln("CASE 10 humidity low --- fogger on");
        break;

      case CASE11:                     //increase temperature
        digitalWrite(relay[1], LOW);   //fans off
        pwmVal1 = 0;
        analogWrite(pwmPin_1, pwmVal1);
        digitalWrite(relay[6], HIGH);  //window heat on
        outputDebugln("CASE 11 airTemp low --- heat on");
        break;

      case CASE12:                     //decrease humidity
        digitalWrite(relay[4], LOW);   //fogger off
        digitalWrite(relay[1], HIGH);  //fans on
        outputDebugln("CASE 12 humidity max --- fans on");
        startup = 0;
        startDelay = millis();
        break;
    }
    previousClimateState = climateState;
  }

  if (climateState == CASE1) {      //decrease temperature and decrease humidity
    fanSpeedController(2);    
  } 

  else if (climateState == CASE3) {  //decrease temperature and increase humidity
    //turn on sprinklers and wait a bit for endothermic evaporation, then turn on fans again. This event is unlikely to happen.
    static byte state = 0;  //at start go to state 0 with ++
    if (millis() - intervalMark >= sequenceInterval[state]) {
      // go to the next state
      state++;
      state = state % 5;
      outputDebug(F("state = "));
      outputDebugln(state);

      // act according to state
      switch (state) {
        case 0:  //fans off and wait 2 seconds
          digitalWrite(relay[1], LOW);
          pwmVal1 = 0;
          analogWrite(pwmPin_1, pwmVal1);
        break;
        case 1:  //sprinklers for 3 seconds
          digitalWrite(relay[2], HIGH);
        break;
        case 2:  //sprinklers off and wait another 30 seconds
          digitalWrite(relay[2], LOW);
        break;
        case 3:  //fans back on for 5 minutes
          digitalWrite(relay[1], HIGH);
          if (airTemp > maxAirTemp) { airTemp = maxAirTemp; }
          mappedTemp = map(airTemp, desiredAirTemp, (desiredAirTemp + 50), 0, 100);
          pwmVal1 = multiMap<float>(mappedTemp, input, output, 11);
          analogWrite(pwmPin_1, pwmVal1);          
        break;
        case 4:  //same as 3 because of max lenght difficulty
          if (airTemp > maxAirTemp) { airTemp = maxAirTemp; }
          mappedTemp = map(airTemp, desiredAirTemp, (desiredAirTemp + 50), 0, 100);
          pwmVal1 = multiMap<float>(mappedTemp, input, output, 11);
          analogWrite(pwmPin_1, pwmVal1); 
        break;
      }
      intervalMark = millis();
    }
  }

  else if (climateState == CASE5) {  //decrease humidity
    fanSpeedController(1);
  }

  else if (climateState == CASE6) {  //decrease temperature
    fanSpeedController(2);
  }

  else if (climateState == CASE8) {  //increase temperature
    //digitalWrite(relay[1], LOW); //fans_2 to be made icm with front window ventilation
    //mappedTemp = map(airTemp, desiredAirTemp, maxAirTemp, 0, 100);
    //pwmVal2 = multiMap<float>(mappedTemp, input, output, 11);
  }

  else if (climateState == CASE9) {  //decrease temperature
    fanSpeedController(2);    
  }

  else if (climateState == CASE12) {  //decrease humidity
    fanSpeedController(1);    
  }  
  pwm1Speed = map(pwmVal1, 0, 255, 0, 100);

}


void fanSpeedController(int type) {
  
  float data, targetValue, maxValue, rangeValue;
  
  if (type == 1) {
    data = humidity;
    maxValue = maxHumidity;
    targetValue = desiredHumidity;
    rangeValue = targetValue + (2 * hysteresis);
  }
  if (type == 2) {
    data = airTemp;
    maxValue = maxAirTemp;
    targetValue = desiredAirTemp;
    rangeValue = targetValue + hysteresis;
  }

  if (data > maxValue) { data = maxValue; }
  
  switch (startup) { //set PWM to minimum value to start the fan moving
    case 0:
      pwmVal1 = 60;
      analogWrite(pwmPin_1, pwmVal1);
      if (millis() - startDelay >= 1000) { startup++; }
    break;
    case 1: //switch to automatic variable control
      mappedTemp = map(data, targetValue, rangeValue, 0, 100);
      pwmVal1 = multiMap<float>(mappedTemp, input, output, 11);
      analogWrite(pwmPin_1, pwmVal1);
    break;
  }  

}

float smoothTransition(float start, float end, uint8_t intervals, unsigned long intervalTime, uint8_t step) {

  float delta = (end - start) / intervals;

  if (millis() - lastUpdateTime >= intervalTime && step < intervals) { 
    start += delta;
    step++;
  }
  lastUpdateTime = millis();
  outputDebug("Transitioning with new value: ");
  outputDebugln(start);
    
  return start;
}

Alright, my apologies.

I'm trying to make a standalone version of it but that is even more broken and I'm to frustrated right now to see what the problem is.

int step = 0;
int oldAirTemp = 23;
int desiredAirTemp = 27;
unsigned long lastUpdateTime = 0;

void setup() {  

  Serial.begin(9600);
  
}

void loop() {

  climateChange();
  delay(1000);

}

void climateChange() {
  
  Serial.println("doing nothing");

  if (desiredAirTemp != oldAirTemp) { //If the program sets a new target temperature, start to transition gradually. Step keeps track of the increments    
  
    Serial.println("Not equal");
    
    desiredAirTemp = smoothTransition(oldAirTemp, desiredAirTemp, 5, 10000, step); //In this case 5 intervals of 10 seconds

  } else {
    step = 0; //Reset step when target is reached and update oldAirTemp
    oldAirTemp = desiredAirTemp;
  }
}

int smoothTransition(float start, float end, int intervals, unsigned long intervalTime, int step) {

  float delta = (end - start) / intervals;
  Serial.print("Delta = ");
  Serial.println(delta);

  if (millis() - lastUpdateTime >= intervalTime && step < intervals) { 
    start += delta;
    step++;
  }
  lastUpdateTime = millis();
  Serial.print("transitioning with new value ");
  Serial.println(start);
    
  return start;
}

lastUpdateTime gets updated every time regardless of the interval. This makes the if condition true every time, so the transition is not “smooth”

Move that instruction inside the if so it only updates when it is due.

I was doing just that in an earlier version, but started trying all sorts of things when it wasn't working.

This is exactly what I thought the problem was. I was thinking something along the lines of isolating the start desiredAirTemp value in a new variable, but I don't know how to build the rest around it so it doesn't get overwritten as well

You are correct, it used to be

if (millis() - lastUpdateTime >= intervalTime ) { 
  if (step < intervals) {
    start += delta;
    step++;;
  }
    lastUpdateTime = millis()
}

but trying to slim it down I overlooked it, apologies

I think you'll need two functions.

One would establish the endpoints, number of steps and time between steps, a kind of setup() function if you will.

The second would keep track of having been called and stepped (because it was time), which would return the smoothly changing value that is in between the endpoints. You would call it very frequently, occasionally it would take a step and return something different to the last time it was called.

As has been observed, for a function to remember anything, it must use variables with the static qualifier.

Or it could simply access global variables.

In small sketches it don't make much difference, but good programming practice is to limit the scope of variables as severely as practical, this makes reading, fixing and enhancements easier.

Although with two,functions, you would pretty much be stuck using global variables. There are workarounds, some might be better than global variables but be a bit tnagled.

a7

it also occured using != between two floats wouldn't work out, so I tried using integers in the standalone, but that doesn't the help current state.

  • Suggest you cast the calculations to float
  • Suggest you get into the habit of using ( and ).
  • Seems odd lastUpdateTime = millis(); this appears outside the { }

I had the following idea:

void climateChange() {
  
  if (desiredAirTemp != oldAirTemp && !transition) { //If the program sets a new target temperature, start to transition gradually. Step keeps track of the increments    

    startVal = oldAirTemp;
    endVal = desiredAirTemp;

    transition = true;

    Serial.println("Not equal");   
  } 

  if (transition) {

    desiredAirTemp = smoothTransition(startVal, endVal, 5, 10000); //In this case 5 intervals of 10 seconds

  }

  if (desiredAirTemp == endVal) {

    oldAirTemp = desiredAirTemp;
    transition = false;
    
    Serial.println("finished"); 

  }
}

going to test with that.

I'm embarrased to say I don't know what that means, I don't have any programming education.

Do you mean for visual clarity here on the forum?

https://cplusplus.com/articles/iG3hAqkS/


  • To make sure math operations happen in the order you expect.

I forgot to multiply delta but now I have a working bit of code:

int step = 0;
int oldAirTemp = 23;
int desiredAirTemp = 27;
unsigned long lastUpdateTime;
bool transition = false;
int startVal, endVal;

void setup() {  

  Serial.begin(9600);
  
}

void loop() {

  climateChange();
  delay(1000);

}

void climateChange() {
  
  if (desiredAirTemp != oldAirTemp && !transition) { //If the program sets a new target temperature, start to transition gradually. Step keeps track of the increments    

    startVal = oldAirTemp;
    endVal = desiredAirTemp;

    transition = true;

    Serial.println("Not equal");   
  } 

  if (transition) {
    desiredAirTemp = smoothTransition(startVal, endVal, 5, 10000); //In this case 5 intervals of 10 seconds
  }

  if (desiredAirTemp == endVal) {

    oldAirTemp = desiredAirTemp;
    transition = false;    
    Serial.println("finished"); 
  }
}

int smoothTransition(float start, float end, int intervals, unsigned long intervalTime) {

  float delta = (end - start) / intervals;

  if (millis() - lastUpdateTime >= intervalTime && step < intervals) { 
    start += delta * step;
    step++;
    lastUpdateTime = millis();
    Serial.print("Delta = ");
    Serial.println(delta);
    Serial.print("transitioning with new value ");
    Serial.println(start);
  }
    
  return start;
}

I'm going to look into casting to convert float to int, thanks

  • When will this if(. . .) ever be true ? :thinking:

it won't, but I've changed after @mancera1979 pointed it out

Schermafbeelding 2024-07-02 214740

float delta = (float) (end - start) / intervals;

Have I got that right?

Also if I want to compare ints from floats can I use


float oldAirTemp = 23.0;
float desiredAirTemp = 27.5;
int startVal, endVal;

    startVal = (int)oldAirTemp;
    endVal = (int)desiredAirTemp;

?

Comparing floating point variables is tricky. Try this:

for (float a=0; a<2; a=a+0.1){
  Serial.print (a);
  if(a==1){
    Serial.print(“ equal”);
  }
  Serial.println();

—-

You should never (or in very specific cases) test

if (floatValue1 == floatValue2)

But rather

if (floatValue1 >= floatValue2)

Or

if(abs(floatValue1 - floatValue2) < floatValue3) // floatValue3 is a “tolerance”

Or something along these lines.

Whenever possible, choose to work with integer values to avoid this. And they are way faster. Just be sure they do not overflow.

I've opted to multiply the float by 10 and convert to int. Then I can compare ints with a little better resolution.

With to construction to divide by a fixed ammount and then multiply by the same fixed ammount the result should be perfect.

floating point variables are a binary representation of decimal values that not always are “perfect”.

For example,
A=1/3 =0.33333333333…
You can get as close to 1/3 as you care for, but there is just not a perfect representation of 1/3 in the decimal world. So 3*A is not exactly 1.

The same can be said about 0.1 not having a perfect representation in the binary world.

Some decimal numbers have one, such as 1/2, 3/8, and any other number that can be expressed as the sum of (negative) powers of 2.