Turn relay off when condition outside of loop is met

Hello community,
I started using arduino this week and here I am a couple of late night sessions in, in need of someone more experienced looking over my code.

I'm trying to make the popular soil irrigation system.
The idea:
I want the system to

  1. take a sensor reading
  2. write the reading to an SD and seriell monitor
  3. if the soil is dry -> turn on pump only as long as it's dry, then immediately turn off
  4. repeat after interval

My problem:
I tried many different versions now but they always end in one of these two scenarios if the soil is dry:

  1. the pump turns off immediately when the desired moisture is reached. However, the reading isn't printed beforehand (when it's still dry) but only after the moisture level is back to the desired level which makes the data look like the soil was never dry.
  2. the moisture value is recorded properly. However, the pump doesn't stop until the next cycle, thus probably flooding everything.

Here is one of the codes, in this one it's scenario 1. I think it comes down to the while loop and the use of return. Without return, I'm flooding everything, but with it my data isn't logged probably, and I just can't figure out how to rewrite the code.

/* 
Moisture sensor is in A0, Relais in D2.
SD-Karte CS in D4, MOSI in D11, MISO in D12, SCK in D13.
*/

// Feuchtigkeitssensor und Relais

#define Sensor  A0   // Feuchtigkeitssensoranschluss Analog Pin A0
#define Relais  2  // Verbindung arduino zu Relais IN

int interval = 1;  // in minutes
int Feuchtigkeit; // moisture
long ttimer;
long timer;
String runTime;   

// getting SD card ready

#include <SD.h>

#define PIN_SPI_CS 4

File myFile;

void setup() {
  Serial.begin(9600);
  pinMode(Relais, OUTPUT);
  
  if (!SD.begin(PIN_SPI_CS)) {
    Serial.println(F("SD-Karte nicht gefunden oder nicht lesbar!"));
    while (1); // Ende
  }

  Serial.println(F("SD-Karte OK!"));

  if (!SD.exists("arduino.txt")) {
    Serial.println(F("Kein Logfile auf der SD. Erstellt arduino.txt..."));
    myFile = SD.open("arduino.txt", FILE_WRITE);
    myFile.close();
  }

  // Prüft erneut
  if (SD.exists("arduino.txt"))
    Serial.println(F("arduino.txt auf der SD-Karte gefunden."));
  else
    Serial.println(F("arduino.txt nicht gefunden,"));
}

void loop() {
 runTime = millis() / 60000;
 ttimer = millis();
 if ((timer + interval * 60000) < ttimer) {
   timer = millis();
   hauptprogramm();
   write_data();
 }
}

void hauptprogramm() {
  
  int Feuchtigkeit = analogRead(Sensor);
  
  if (Feuchtigkeit > 350) {
       Serial.println("Wasser marsch!");
     while (Feuchtigkeit > 350) { 
      digitalWrite(Relais, HIGH);
        Feuchtigkeit = analogRead(Sensor); //re-check
        return; 
     }
  } 
  else {
      digitalWrite(Relais, LOW);
             Serial.println("Feuchtigkeit ok");
      Feuchtigkeit = analogRead(Sensor); //re-check 
  }
}

void write_data() {
    int Feuchtigkeit = analogRead(Sensor);
     runTime = millis() / 60000;
   Serial.print(" (");
   Serial.print(Feuchtigkeit);
   Serial.print(" @ ");
   Serial.print(runTime);
   Serial.println(" min)");

    myFile = SD.open("arduino.txt", FILE_WRITE);
 if (myFile) {
   myFile.print(" (");
   myFile.print(Feuchtigkeit);
   myFile.print(" @ ");
   myFile.print(runTime);
   myFile.println(" min)");
   myFile.close();
 }
 else {
   Serial.println("Fehler beim Datenschreiben.");
 }
}

Not sure if this is related to your issue, but using future time stamps is a big no-no. You should emulate the BlinkWithoutDelay example to see how millis() timing should be performed.

Your program logic is flawed.

You are determining the sensor value at multiple places in your code... by the time you get to the logging routine it will always be "not dry"... either because it was "not dry" to start with, or is was "dry" but you watered it in a previous step - hauptprogramm() .

Remove this statement from both write_data() and from hauptprogamm()

  int Feuchtigkeit = analogRead(Sensor);

Change the code in the main loop to

  if ((timer + interval * 60000) < ttimer) 
  {
    timer = millis();
    Feuchtigkeit = analogRead(Sensor);
    write_data();
    hauptprogramm();
  }

Change hauptprogramm() to this

void hauptprogramm() 
{

  if (Feuchtigkeit > 350) 
  {
    Serial.println("Wasser marsch!");
    while (Feuchtigkeit > 350) 
    {
      digitalWrite(Relais, HIGH);
      Feuchtigkeit = analogRead(Sensor); //re-check
    }
  }
  
  if (Feuchtigkeit <= 350)
  {
    digitalWrite(Relais, LOW);
    Serial.println("Feuchtigkeit ok");
  }
}

Also, as noted by @anon57585045 this is not the correct way to use millis() timing. Please refer to the example quoted.

Thank you both for your imput!
I had another look at BlinkWithouDelay and implemented the changes, it seems to work now!

using while-loops is blocking. It means you stay inside the while-loop until the while-condition is no longer true.

If you want to do a second, third, fourth thing in parallel to the main activity in the while-loop you have to add code for this second, third, fourth thing inside the while-loop.
This is unconvenient.

There is another approach to realise a functionality without any blocking:

The basic principle is:
let do all looping - and if I say all I really really REALLY mean ALL looping do by function void loop().

If void loop() does all the looping executing code is conditional to:

  • flag-variables
  • if-conditions
  • switch-case-break;-statememets used in state-machines

Your code has only two modes of operation:

  1. pumped switched on
  2. pumped switched off

With only two modes of operation a state-machine is not nescessary.
As soon as your code has 3 or more modes of operation a state-machine is useful

So here is a modified version of yur code that does

  • check feuchtigkeit all the time because the code is non-blocking.

Function loop() is looping all the time very fast and

  • prints the message "Wasser marsch" only one time per switching on the pump
  • writes value of variable Feuchtigkeit only once per minute
  • prints the message "Feuchtigkeit OK" only one time per switching pump off
  • switches on/off the relais depending on the sensor-threshold beeing higher or lower than 350
/*
  Moisture sensor is in A0, Relais in D2.
  SD-Karte CS in D4, MOSI in D11, MISO in D12, SCK in D13.
*/

// Feuchtigkeitssensor und Relais

#define Sensor  A0 // Feuchtigkeitssensoranschluss Analog Pin A0
#define Relais  2  // Verbindung arduino zu Relais IN

int interval = 1;  // in minutes
int Feuchtigkeit;  // moisture

// variables used with function millis() MUST be of type
// UNSIGNED long

unsigned long WriteDataTimer;
boolean MsgWasserMarschPrinted = false;
boolean MsgFeuchtigkeitOKPrinted = false;

String runTime;

#include <SD.h>

#define PIN_SPI_CS 4

File myFile;

void setup() {
  Serial.begin(115200);  // make sure to change baudrate in the serial monitor too
  pinMode(Relais, OUTPUT);

  if (!SD.begin(PIN_SPI_CS)) { // getting SD card ready
    Serial.println(F("SD-Karte nicht gefunden oder nicht lesbar!"));
    while (1); // Ende
  }

  Serial.println(F("SD-Karte OK!"));

  if (!SD.exists("arduino.txt") ) {
    Serial.println(F("Kein Logfile auf der SD. Erstellt arduino.txt..."));
    myFile = SD.open("arduino.txt", FILE_WRITE);
    myFile.close();
  }

  // Prüft erneut
  if (SD.exists("arduino.txt"))
    Serial.println(F("arduino.txt auf der SD-Karte gefunden."));
  else
    Serial.println(F("arduino.txt nicht gefunden,"));

  WriteDataTimer = millis(); // initialise timer-variable with actual time
}


// easy to use helper-function for non-blocking timing
boolean TimePeriodIsOver (unsigned long &startOfPeriod, unsigned long TimePeriod) {
  unsigned long currentMillis  = millis();
  if ( currentMillis - startOfPeriod >= TimePeriod ) {
    // more time than TimePeriod has elapsed since last time if-condition was true
    startOfPeriod = currentMillis; // a new period starts right here so set new starttime
    return true;
  }
  else return false;            // actual TimePeriod is NOT yet over
}


void loop() {
  // read sensor ONCE at top of loop and use this value
  // for everything until next iteration of function loop()
  Feuchtigkeit = analogRead(Sensor);

  // check if one minute has passed by since last time the function was true
  if ( TimePeriodIsOver(WriteDataTimer, 60000) ) {
    // if one minute has REALLY passed by
    write_data();
  }

  hauptprogramm();
}

void hauptprogramm() {

  if (Feuchtigkeit > 350) {
    digitalWrite(Relais, HIGH);

    if (!MsgWasserMarschPrinted) {      // if not yet printed
      Serial.println("Wasser marsch!");
      MsgWasserMarschPrinted = true;    // change flag to true (printed)
      MsgFeuchtigkeitOKPrinted = false; // reset oposite flag
    }
  }
  else {
    digitalWrite(Relais, LOW);

    if (!MsgFeuchtigkeitOKPrinted) {     // if not yet printed
      Serial.println("Feuchtigkeit ok");
      MsgFeuchtigkeitOKPrinted = true;   // change flag to true (printed)
      MsgWasserMarschPrinted = false;    // reset opposite flag
    }
  }
}

void write_data() {

  runTime = millis() / 60000;
  Serial.print(" (");
  Serial.print(Feuchtigkeit);
  Serial.print(" @ ");
  Serial.print(runTime);
  Serial.println(" min)");

  myFile = SD.open("arduino.txt", FILE_WRITE);
  if (myFile) {
    myFile.print(" (");
    myFile.print(Feuchtigkeit);
    myFile.print(" @ ");
    myFile.print(runTime);
    myFile.println(" min)");
    myFile.close();
  }
  else {
    Serial.println("Fehler beim Datenschreiben.");
  }
}

best regards Stefan

Thank you for the improvement suggestion & explanation @StefanL38

It's my first time coding and I basically just tried out all the control structures until I found the one that worked best :sweat_smile: ... I read about while blocking things in the code, but just couldn't figure out how to do everything in the loop function. Thanks again

aren't your measurements likely to always be the same, something just > 350?

and correct the problem pointed by @anon57585045, why not simply either just turn the water on or off every check interval if 1 minute rather than immediately checking to stop?

consider

// basic regulator

enum { Off = HIGH, On = LOW };

#define Period    10
#define Thresh    350
#define Margin    10

unsigned long msecLst;

// -----------------------------------------------------------------------------
void loop ()
{
    unsigned long msec = millis ();

    if ((msec - msecLst) > Period)  {
        msecLst = msec;

        int  val = analogRead (A0);
        Serial.println (val);
        if ((Thresh + Margin) < val)
            digitalWrite (LED_BUILTIN, On);
        else if ((Thresh - Margin) > val)
            digitalWrite (LED_BUILTIN, Off);
    }
}

// -----------------------------------------------------------------------------
void setup ()
{
    Serial.begin (9600);
    pinMode (LED_BUILTIN, Off);
}