Program advice for hysteresis

I am curious if anyone has a better solution to have hysterisis in an if statement.

At the moment I am checking to see the moisture content of some soil. If moisture drops below a certain level then water the soil, increase the limit so the watering has to increase by the hysteresis value (but only add the hysteresis value to the limit once by using a flag = “dry”). Water for a short period and then wait for a minimum period until allowed to water again to allow soaking (using the “period” variable that gets reset to true/1 when the period function is run by using the interval timer). Once the moisture increases to above the (minimum level + hysteresis value), then return the minimum level to its original value (removing the hysteresis value)…

Hysteresis component:

  if (moisture < moistureLimit) // check if moisture is too low
  {
    if (dry = 0) // if the last state was the soil wet = first dry record
    {
      moistureLimitOrig = moistureLimit;
      moistureLimit = moistureLimit + moistureHysteresis; // increase moisture lower limit so the watering requires to increase moisture by the Hysteresis value
      dry = 1;
    }

    if (period == 1) // has period between soaks been exceeded - yes =1, no =0 . If no then likely the moisture will stay low until period = 1 and it soaks again or rain comes
    {
      checkSoilMoistureSensorFrequency = 1000; // make moisture check more frequent while watering until moisture exceeded
      wateringFunction(); // begin watering
      period = 0; //reset the flag until next minimum watering period is exceeded so this if statement only runs once per period
    }
    else // period between soaks has not been met do nothing
    {
    }
  }
  else // when moisture is above the limit
  {
    if (dry == 1) // first time the moisture goes above the limit
    {
      moistureLimit = moistureLimitOrig; // return moisture level to the defined limit with no Hysteresis vslue added to it
      dry = 0;
    }
    checkSoilMoistureSensorFrequency = 600000; // longer period between checking moisture a soil is above levels. Good for graph/history reduction in data
  }

Full code:

#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>

char auth[] = "####"; // Blynk code

// Your WiFi credentials.
char ssid[] = "####"; // wifi name
char pass[] = "####"; // wifi password

BlynkTimer timer;

// default variables
long periodBetweenSoaks = 600000; // default minimum period between soaks in minutes in millis
long durationToWater = 10000; // default length of time per watering/soak in millis
float moistureLimit = 512; // default lower limit of moisture between 0 and 1023

//blynk variables that change defaults above
unsigned long virtualpin1 = 10; // virtual pin for blynk - minimum period between soaks in minutes
unsigned long virtualpin2 = 1; // virtual pin for blynk - length of time per watering/soak in minutes
unsigned long virtualpin3 = 50; // virtual pin for blynk - lower limit of moisture  between 0 and 100

// hardware variables
int relayPin = 5; // relay pin for solenoid on D2
float moisture = 512; // current moisture value

// checking moisture frequencies
long checkSoilMoistureSensorFrequency = 1000; //default soil moisture period/frequency
long checkSoilMoistureSensorFrequencyOld = 1000; // default previous soil moisture period reading used to detect whether to intiate the timer interval again

//timer variables
unsigned long timercheckSoilMoistureSensorFrequency = 1000; // variable used to name the timer interval for period between reading soil moisture
unsigned long timerPeriodBetweenSoaks = 600000; // variable used to name the timer interval used for period between soaks
unsigned long timerStopWatering = 10000; // variables used to name the timer countdown
int period = 1; // used to monitor when the timer countdown has been reached to allow another soak
int timerWifi = 10000; // used to poll wifi signal every 10 seconds

// other variables
long signalStrength = -50;
float moistureLimitOrig = 512;
int dry = 0;
long moistureHysteresis = 5;

BLYNK_WRITE(V1)
{
  virtualpin1 = param.asInt(); // assigning incoming value from pin V1 to a variable
  periodBetweenSoaks = virtualpin1 * 60000; // period between soaking (milli) defined by blynk slider (minutes)
}

BLYNK_WRITE(V2)
{
  virtualpin2 = param.asInt(); // assigning incoming value from pin V2 to a variable
  durationToWater = virtualpin2 * 60000;  // length of soaking (milli) defined by blynk slider (minutes)
}

BLYNK_WRITE(V3)
{
  virtualpin3 = param.asInt(); // assigning incoming value from pin V3 to a variable
  moistureLimit = virtualpin3 * 10.23;  // minimum soil moisture (0-1023) defined from blynk slider (0-100%)
}

BLYNK_WRITE(V5)
{
  moistureHysteresis = param.asInt(); // assigning incoming value from pin V3 to a variable
}

void setup()
{

  Blynk.begin(auth, ssid, pass);

  timercheckSoilMoistureSensorFrequency = timer.setInterval(checkSoilMoistureSensorFrequency, checkSoilMoistureSensorFunction); //  check for moisture every # second
  timerPeriodBetweenSoaks = timer.setInterval(periodBetweenSoaks, periodBetweenSoaksFunction); //  check for moisture every # second
  // timerWifi = timer.setInterval(10000, wifiSignal); // check wifi strength every 10 seconds

  Blynk.virtualWrite(V1, virtualpin1); //set virtual pins to defaults
  Blynk.virtualWrite(V2, virtualpin2); //set virtual pins to defaults
  Blynk.virtualWrite(V3, virtualpin3); //set virtual pins to defaults
}

void checkSoilMoistureSensorFunction()
{
  wifiSignal();
  moisture = analogRead(A0); // read moisture
  Blynk.virtualWrite(V4, moisture / 10.23); // write moisture to blynk database
  if (moisture < moistureLimit) // check if moisture is too low
  {
    if (dry = 0) // if the last state was the soil wet = first dry record
    {
      moistureLimitOrig = moistureLimit;
      moistureLimit = moistureLimit + moistureHysteresis; // increase moisture lower limit so the watering requires to increase moisture by the Hysteresis value
      dry = 1;
    }

    if (period == 1) // has period between soaks been exceeded - yes =1, no =0 . If no then likely the moisture will stay low until period = 1 and it soaks again or rain comes
    {
      checkSoilMoistureSensorFrequency = 1000; // make moisture check more frequent while watering until moisture exceeded
      wateringFunction(); // begin watering
      period = 0; //reset the flag until next minimum watering period is exceeded so this if statement only runs once per period
    }
    else // period between soaks has not been met do nothing
    {
    }
  }
  else // when moisture is above the limit
  {
    if (dry == 1) // first time the moisture goes above the limit
    {
      moistureLimit = moistureLimitOrig; // return moisture level to the defined limit with no Hysteresis vslue added to it
      dry = 0;
    }
    checkSoilMoistureSensorFrequency = 600000; // longer period between checking moisture a soil is above levels. Good for graph/history reduction in data
  }

  if (checkSoilMoistureSensorFrequency = ! checkSoilMoistureSensorFrequencyOld) // if frequency or period between soaks has changed then reset the timer interval
  {
    timer.deleteTimer(timercheckSoilMoistureSensorFrequency);
    timercheckSoilMoistureSensorFrequency = timer.setInterval(checkSoilMoistureSensorFrequency, checkSoilMoistureSensorFunction); //  check for moisture every # second
  }
  checkSoilMoistureSensorFrequencyOld = checkSoilMoistureSensorFrequency; // assign current frequency to an old variable to compare with next frequency
}

void periodBetweenSoaksFunction() // called in periods defined by an interval timer
{
  period = 1; // period between soaks exceeded
}

void wateringFunction()
{
  digitalWrite(relayPin, HIGH); // pin high to close SSR and turn solenoid/water on
  timerStopWatering = timer.setTimeout(durationToWater, stopWateringFunction);  // Deactivare Relay/Watering after # seconds by running stopWatering
}

void stopWateringFunction()
{
  timer.deleteTimer(timerStopWatering); // delete timer just because I read it is safer somewhere
  digitalWrite(relayPin, LOW); // stop watering, after the duration/timeout specified in startWatering
}

void wifiSignal()
{
  signalStrength = WiFi.RSSI();
  Blynk.virtualWrite(V6, signalStrength + 100); // write singal strength to blynk database but add 100 so weak = 0 and strong = 100
}

void loop()
{
  Blynk.run();
}
    if (dry = 0) // if the last state was the soil wet = first dry record

= is the assignment operator. == is the equality (comparison) operator.

      wateringFunction(); // begin watering

If you need a comment, the function needs a better name.

    else // period between soaks has not been met do nothing
    {
    }

Having an else block just so you have a place to put a comment is just wrong. Comments do NOT have to follow a line of code. They can be on a line, or lines, by themselves. Truly useful comments explain WHY the code is doing what it does, NOT what it is doing.

Now, having said all that, I don't really understand what the question was.

PaulS:

    if (dry = 0) // if the last state was the soil wet = first dry record

= is the assignment operator. == is the equality (comparison) operator.

      wateringFunction(); // begin watering

If you need a comment, the function needs a better name.

    else // period between soaks has not been met do nothing

{
    }



Having an else block just so you have a place to put a comment is just wrong. Comments do NOT have to follow a line of code. They can be on a line, or lines, by themselves. Truly useful comments explain WHY the code is doing what it does, NOT what it is doing.

Now, having said all that, I don't really understand what the question was.

Thanks. Points taken on board. Yes "==" is correct. The code compiled OK, but I am on holiday away from the NodeMCU it is going on, so debugging isn't exactly thorough right now.

The question was is there a better way to do the hysteresis?

At the moment I am detecting the low moisture (say a value of 200), begin watering, increase the lower moisture limit by a set value (200 + hysteresis = 205) so it'll keep watering until moisture =205, where it will then reset the limit back to 200 until the soil dries up again. I am using the flag "dry" with values of 0 or 1 so the limit is only increased by the hysteresis value (5) only once during the watering phase. This means there is a hysteresis of 5 so the watering doesn't go on/off/on/off too close together...

I realise the soil will probably give some form of hysteresis due to the delay in the moisture content rising after the watering begins but I can't test this for another week so killing time wirting code that may not be needed.

That’s a rather unusual (and somewhat convoluted) way to implement hysteresis. This is more conventional:

float hysteresis = 5.0;
float setPoint = 200.0;

void loop() {
  if (moisture <= setPoint - hysteresis) {
    // Open Valve - Turn Water On
  } if (moisture >= setPoint + hysteresis) {
    // Close Valve - Turn Water Off
  }
}

Basically, if the measured state of the system lies in the dead band then you do nothing, leave the control variable (water on/off in this case) unchanged.

The above example would continually turn on or off the water every time through the loop as needed – even if it’s already in the required state. Probably not a problem if you’re controlling a solenoid with a Digital Output. But, you could add a state variable and only change the output as needed:

float hysteresis = 5.0;
float setPoint = 200.0;
boolean waterOn;

void setup() {
  // Close Valve - Turn Water Off
  waterOn = false;
}

void loop() {
  if (moisture <= setPoint - hysteresis) {
    if (!waterOn) {
      // Open Valve - Turn Water On
      waterOn = true;
    }
  } else if (moisture >= setPoint + hysteresis) {
    if (waterOn) {
      // Close Valve - Turn Water Off
      waterOn = false;
    }
  }
}

Finally, I used ‘float’ type variables in the examples because that’s what’s in your code. But, you should rethink that and if possible use integers. The computational overhead of floats is not insignificant.

I wrote that code with the vision of just single lower limit and then it grew from there and got a little confusing… although I’m pretty sure it appeared to be OK, although as I am away on holidays I couldn’t check it on the NodeMCU itself.

I have now written it again from scratch using the more conventional hysteresis using if, and else if conditions based off what gfvalvo mentioned so thanks. The program is reduced and less confusing I think.

I have added a little bit more complexity and I have used simpletimer (BlynkTimer) for the function timings as I believe this is the better method from advice elsewhere, definitely better than using the delay() I used to use where the code is held up and no monitoring of inputs is done during that time.

What added the complexity;

  • I want to water for 1 minute and then wait 10 minutes for the water to soak before watering again (these values can be changed from Blynk). This is achieved by the startWatering function using a simpletimer countdown to call the stopWatering function after 1 minute (default). Then the stopWatering function calling the startWatering function after the soaking period but only IF the soil needs watering (moistureLimitMet = 1 flag).
  • I have also included the the changing of the timer interval for the checking of the soil moisture so the Blynk records only update occasionally when the soil is reducing in moisture, but monitor more frequently when the watering is occurring.
  • And lastly the wifi signal of the NodeMCU
  • All this is recorded on the Blynk server when the moisture is measured and I will graph it in the app to monitor the moisture levels.

I need to check the flags for the watering part as I don’t want the watering starting again before the watering ans soaking period have finished… which I think I have dealt with. This has added complexities, and not sure if they will be needed but its been a good exercise anyway.

#include <ESP8266WiFi.h>
#include <BlynkSimpleEsp8266.h>

char auth[] = "###"; // Blynk code

// Your WiFi credentials.
char ssid[] = "###"; // wifi name
char pass[] = "###"; // wifi password

BlynkTimer timer;

// variables that change
long periodBetweenSoaks = 600000;
long wateringPeriod = 60000;
int moistureLimit = 512;
int moistureLimitHysteresis = 52;

// default variables
int virtualpin1 = 10;
int virtualpin2 = 1;
int virtualpin3 = 50;
int virtualpin5 = 5;

// other variables
int moistureLimitMet = 0;
long moistureCheckPeriod = 1000;
long moistureCheckPeriodOld = 1000;
int currentWateringCycle = 0;

//timer variables
long moistureCheckTimer = 1000;
long timerForWateringPeriod = 60000;
long timerForSoakingPeriod = 600000;
long timerForWaterCycle = 600000;

// hardware variables
int moisture = 1023;
int signalStrength = 0;
int relayPin = 5;

BLYNK_WRITE(V1)
{
  virtualpin1 = param.asInt(); // assigning incoming value from pin V1 to a variable
  periodBetweenSoaks = virtualpin1 * 60000; // period between soaking (milli) defined by blynk slider (minutes)
}

BLYNK_WRITE(V2)
{
  virtualpin2 = param.asInt(); // assigning incoming value from pin V2 to a variable
  wateringPeriod = virtualpin2 * 60000;  // length of soaking (milli) defined by blynk slider (minutes)
}

BLYNK_WRITE(V3)
{
  virtualpin3 = param.asInt(); // assigning incoming value from pin V3 to a variable
  moistureLimit = virtualpin3 * 10.23;  // minimum soil moisture (0-1023) defined from blynk slider (0-100%)
}

BLYNK_WRITE(V5)
{
  virtualpin5 = param.asInt(); // assigning incoming value from pin V3 to a variable
  moistureLimitHysteresis = virtualpin5 * 10.23;
}

void setup()
{
  Blynk.begin(auth, ssid, pass);

  Blynk.virtualWrite(V1, virtualpin1); //set virtual pins to defaults
  Blynk.virtualWrite(V2, virtualpin2); //set virtual pins to defaults
  Blynk.virtualWrite(V3, virtualpin3); //set virtual pins to defaults
  Blynk.virtualWrite(V5, moistureLimitHysteresis); //set virtual pins to defaults

  moistureCheckTimer = timer.setInterval(moistureCheckPeriod, MoistureCheckFunction);
}

void MoistureCheckFunction()
{
  moisture = analogRead(A0); // read moisture
  Blynk.virtualWrite(V4, moisture / 10.23); // write moisture to blynk database
  wifiSignal();

  if (moisture <= moistureLimit - moistureLimitHysteresis)
  {
    if (currentWateringCycle == 0)
    {
      moistureLimitMet = 1;
      moistureCheckPeriod = 10000;
      moistureCheckTimerResetFunction();
      wateringStartFunction();
    }
  }
  else if (moisture >= moistureLimit + moistureLimitHysteresis)
  {
    moistureLimitMet = 0;
    moistureCheckPeriod = 600000;
    moistureCheckTimerResetFunction();
    wateringStopFunction();
  }

}

void moistureCheckTimerResetFunction()
{

  if (moistureCheckPeriod = ! moistureCheckPeriodOld)
  {
    timer.deleteTimer(moistureCheckTimer);
    moistureCheckTimer = timer.setInterval(moistureCheckPeriod, MoistureCheckFunction);
  }

  moistureCheckPeriodOld = moistureCheckPeriod;
}

void wateringStartFunction()
{
  if (moistureLimitMet == 1)
  {
    currentWateringCycle = 1; // 1 is yes there is a current water cycle to stop request to water before current cycle has finished.
    digitalWrite(relayPin, HIGH); // start watering
    timerForWateringPeriod = timer.setTimeout(wateringPeriod, wateringStopFunction);
  }
}

void wateringStopFunction()
{
  digitalWrite(relayPin, LOW); // stop watering

  if (moistureLimitMet == 1)
  {
    timerForWaterCycle = timer.setTimeout(periodBetweenSoaks, wateringFunction);
    timerForSoakingPeriod = timer.setTimeout(periodBetweenSoaks, wateringStartFunction);
  }

}

void wateringFunction()
{
  currentWateringCycle = 0;
}

void wifiSignal()
{
  signalStrength = WiFi.RSSI();
  Blynk.virtualWrite(V6, signalStrength + 100); // write singal strength to blynk database but add 100 so weak = 0 and strong = 100
}

void loop()
{
  Blynk.run();
}

I believe mositureLimit and mostureimitHysteresis will end up being float's seeing I multiple the BlynkVirtualPins by 10.23

I have read isn't ideal... but hopng this won't be a problem on a relatively low processor intensive task huh?

Reason I multiply by 10.23 is because my outputs in Blynk are 0-100 so need to change this to 0-1023 for the 10bit analog input... I guess I could change the Blynk virtual pins from 0-1023 so no multiplication is needed, or I could multiply by just 10 (so limited to 0-1000 only) but don't think either of these are really necessary huh?

All the other long variables I think need to stay as they can all exceed 32k signed or 64k unsigned with the adjustments to the virtualpins in Blynk that define the variables after the code is uploaded.

Suggestions:

  • Consider using timing techniques based on the millis() function rather than the BlynkTimer, it will probably end up being simpler. And, it makes all your timing and control internal to the processor so the control will keep working even if the WiFi connection fails or the Blynk server is unavailable. See:
    Using millis() for timing. A beginners guide

  • Take your input values from Blynk (0-100), multiply by 1023, and store result in an ‘unsigned long’ integer (aka uint32_t). Take your moisture readings (0-1023), multiply by 100 and store result in an unsigned long. Now, do all your math and comparisons on these scaled parameters using 32-bit integer math. You get that for free on the 32-bit ESP8266. When writing back to Blynk, divide scaled values by 1023.0 (note the “.0” to force floating point).

  • If those are your real SSID, WiFi password, and Blynk authorization codes, you probably don’t want to be posting them on a public forum. Someone might just write their own code to kill your plants using your own Blynk authorization.

Hid those details now. Whoops.

All great points. I will do all of what you said. I've used the millis before to check a duration but hadn't realised it was advised over simple timer.

Isn't blynk timer almost the same and based on simple timer and doesn't rely on the internet connection though? I still take your advice about millis but curious.

Pete-Repeat:
Isn't blynk timer almost the same and based on simple timer and doesn't rely on the internet connection though? I still take your advice about millis but curious.

Actually, I don't know. Only every used the online features of Blynk. You could be right.

gfvalvo:
Actually, I don't know. Only every used the online features of Blynk. You could be right.

Pretty sure it's a modification of simple timer that fixes a few thing's and also allows more timer's...

There is a time based thing in Blynk to trigger something at certain times of the day which relies on the internet (server sends the command to change the pin or virtual pin) , but that's different.

Now after writing all my code based around simple timer, is there any justification to change it to using just millis() instead now I've written the code anyway?

I will definitely consider using just millis() (which I think simple timer is based on??) for future projects through, or if I have problems with my timer's.

I may even consider making the watering time closer to being a PID control system if the mosture tends to overshoot and undershoot depending on the weather/sun. I don't think that'll be too hard to do..but yet to see if it's needed.

Well more a PI control system where the watering time is proportional to error, and also the error over time (error x time).

Edit: I've made it so for every 5 watering cycles the watering time increases by a factor. So first 5 cycles are 1 minute watering, then next 5 are 2 minutes, next 5 are 3 minute's and so on... That should do the trick.