Game Instance


Let the games begin

Scheduled plant irrigation system

The Arduino sketch

The previous article on the Scheduled irrigation system was giving the ins and outs of the circuitry involved in the project. Let's now investigate the code that drives it.

As prerequisite for this sketch you will need to install the Wire, RTClib and LowPower libraries in your Arduino IDE. The first two are needed for communicating with the I2C RTC module. The last is used for reducing the MCU's current consumption.

Configuration

The system can be configured at compile time only. The microcontroller has to be reprogrammed on each new configuration change, which is totally feasible if you kept an easy access to its serial communication pins.

It can be set to irrigate every given number of days - that value has to be written in the IRRIGATION_PERIOD_DAYS constant. The IRRIGATION_DAY indicates the modulo IRRIGATION_PERIOD_DAYS day in which the irrigation is to happen. The IRRIGATION_BEGIN_[HOUR|MINUTE] sets the moment the pump motor will be started. The IRRIGATION_END_[HOUR|MINUTE] sets the latest moment the pump will be allowed to run and acts as a safety net in case one of the cascade full indicating switches fails to engage.
All these limit the irrigation event to a configurable time interval, in certain days.

The CASCADE_ALTERNATION bool constant indicates that the cascades - the small and the big one - are being alternatively filled on every irrigation event. Setting this constant to false means no alternation is made but you will need to specify via the SMALL_CASCADE_ONLY bool constant which cascade - the small or the big one - will be filled every time.

Activate the DEBUG mode and the MCU will no longer go in low-power and that only to sustain serial communication. This is useful when tracing the execution of the sketch and especially in combination with the SELF_TEST set to true. Keep both to false for normal execution. The CONTINUOUS_TEST constant set to true will lock the system in a continuous test state and will never do its job. This mode is helpful when debugging or fixing the input switches and their cables.

In SELF_TEST mode, the MCU will perform a couple of tests upon (re)start. This mode was meant to give an indication about the system's functionality. First, it flashes the indicator LED twice with a 0.5 Hz frequency to prove that the indicator LED works. It then enables the pump motor for 8 seconds, just enough to see that it pumps water. After that it checks and logs the input switches for 30 seconds and to see it you'll need DEBUG mode and a serial logger connected to the MCU. The self-test completion is signaled by a 3 seconds LED flash and the normal function is resumed.

The code

Is composed of a single .ino file that needs to be compiled and burned onto the MCU. Remember that not all Arduino compatible MCUs have LowPower support. For those you'll need to alter the code and remove all the LowPower lib uses.

#include <LowPower.h>
#include <Wire.h> 
#include "RTClib.h"

/// self test
const bool SELF_TEST = true;
/// continuous test
/// WARNING: must remain false !!!
const bool CONTINUOUS_TEST = false;
/// debug mode
const bool DEBUG = true;
/// compile-time clock setting, goes like this:
/// 1. set it to true, compile the sketch and write it to the MCU
/// 2. reset the MCU and run it once, 
/// 3. set it to false, compile the sketch and write it to the MCU
const bool CLOCK_SETUP = false;

/// irrigation period: once every IRRIGATION_PERIOD_DAYS days
const int IRRIGATION_PERIOD_DAYS = 2;
/// irrigation day: between 0 and IRRIGATION_PERIOD_DAYS-1
const int IRRIGATION_DAY = 0;
/// irrigation begin hour: between 0 and 23
const int IRRIGATION_BEGIN_HOUR = 20;
/// irrigation end hour: between 0 and 23
const int IRRIGATION_END_HOUR = 20;
/// irrigation begin minute: between 1 and 60
/// SEEME: why 1-60 and not 0-59 ?!
const int IRRIGATION_BEGIN_MINUTE = 10;
/// irrigation end minute: between 1 and 60
/// SEEME: why 1-60 and not 0-59 ?!
const int IRRIGATION_END_MINUTE = 50;
/// alternating the cascades
const bool CASCADE_ALTERNATION = false;
/// fill just the small cascade
const bool SMALL_CASCADE_ONLY = false;
/// water tank level probing interval in case of low water level [seconds]
const uint32_t WARNING_TANK_LEVEL_PROBING_INTERVAL = 10;

/// pump motor port
const int PUMP_MOTOR_PORT = 11;
/// LED signaling port
const int SIGNALING_LED_PORT = 13;
/// analog input port: empty tank
const int EMPTY_TANK_PORT = A0;
/// analog input port: big cascade full
const int BIG_CASCADE_FULL_PORT = A1;
/// analog input port: small cascade full
const int SMALL_CASCADE_FULL_PORT = A2;

/// clock
RTC_DS1307 RTC;
/// temporal probe
DateTime time;
/// machine state
int state;
/// the unix timestamp of the last water level check in case of low water level
uint32_t tsProbeWarningTankLevel;
/// LED warning state
int stateWarningLED;
/// the unix timestamp of the warning LED state change
uint32_t tsChangeWarningLED;
/// indicates that the state machine needs to sleep longer between iterations
bool bSleepLonger;


void setup() {
  // 
  Wire.begin();
  RTC.begin();
  if (CLOCK_SETUP) {
    // sets the clock to the compile date and time
    RTC.adjust(DateTime(__DATE__, __TIME__));
  }
  Serial.begin(9600);
  pinMode(PUMP_MOTOR_PORT, OUTPUT);
  pinMode(SIGNALING_LED_PORT, OUTPUT);
  pinMode(EMPTY_TANK_PORT, INPUT);
  pinMode(BIG_CASCADE_FULL_PORT, INPUT);
  pinMode(SMALL_CASCADE_FULL_PORT, INPUT);
  tsProbeWarningTankLevel = 0;
  stateWarningLED = 0;
  tsChangeWarningLED = 0;
  bSleepLonger = false;
  state = 0;
}

void trace(const String& message) {
  //
  Serial.print(time.year(), DEC);
  Serial.print('/');
  Serial.print(time.month(), DEC);
  Serial.print('/');
  Serial.print(time.day(), DEC);
  Serial.print(' ');
  Serial.print(time.hour(), DEC);
  Serial.print(':');
  Serial.print(time.minute(), DEC);
  Serial.print(':');
  Serial.print(time.second(), DEC);
  Serial.print(" - ");
  Serial.print(message);
  Serial.println();
}

void displayConfig() {
  // 
  Serial.print("Irigates once every ");
  Serial.print(IRRIGATION_PERIOD_DAYS, DEC);
  Serial.print(" days, at day ");
  Serial.println(IRRIGATION_DAY, DEC);
  Serial.print("today: ");
  if ((today() % IRRIGATION_PERIOD_DAYS) == IRRIGATION_DAY) {
    // 
    Serial.println("yes, ");
  } else {
    // 
    Serial.println("no, ");
  }
  Serial.print("today + 1: ");
  if (((today() + 1) % IRRIGATION_PERIOD_DAYS) == IRRIGATION_DAY) {
    // 
    Serial.println("yes, ");
  } else {
    // 
    Serial.println("no, ");
  }
  Serial.print("today + 2: ");
  if (((today() + 2) % IRRIGATION_PERIOD_DAYS) == IRRIGATION_DAY) {
    // 
    Serial.println("yes, ");
  } else {
    // 
    Serial.println("no, ");
  }
  Serial.print("today + 3: ");
  if (((today() + 3) % IRRIGATION_PERIOD_DAYS) == IRRIGATION_DAY) {
    // 
    Serial.println("yes,");
  } else {
    // 
    Serial.println("no,");
  }

  if (CASCADE_ALTERNATION) {
    // 
    Serial.print("alternating the cascades - today: ");
    if (((int)(today() / IRRIGATION_PERIOD_DAYS) % 2) == 0) {
      // 
      Serial.println("the small cascade, ");
    } else {
      // 
      Serial.println("the big cascade, ");
    }
    Serial.print("today + 1: ");
    if (((int)((today() + 1) / IRRIGATION_PERIOD_DAYS) % 2) == 0) {
      // 
      Serial.println("the small cascade, ");
    } else {
      // 
      Serial.println("the big cascade, ");
    }
    Serial.print("today + 2: ");
    if (((int)((today() + 2) / IRRIGATION_PERIOD_DAYS) % 2) == 0) {
      // 
      Serial.println("the small cascade, ");
    } else {
      // 
      Serial.println("the big cascade, ");
    }
    Serial.print("today + 3: ");
    if (((int)((today() + 3) / IRRIGATION_PERIOD_DAYS) % 2) == 0) {
      // 
      Serial.println("the small cascade.");
    } else {
      // 
      Serial.println("the big cascade.");
    }
  } else {
    // 
    if (SMALL_CASCADE_ONLY) {
      // 
      Serial.println(" just the small cascade,");
    } else {
      //
      Serial.println(" just the big cascade,");
    }
  }
  Serial.print(" between ");
  Serial.print(IRRIGATION_BEGIN_HOUR, DEC);
  Serial.print(":");
  Serial.print(IRRIGATION_BEGIN_MINUTE, DEC);
  Serial.print(" and ");
  Serial.print(IRRIGATION_END_HOUR, DEC);
  Serial.print(":");
  Serial.print(IRRIGATION_END_MINUTE, DEC);
  Serial.println(". ");
  Serial.print("Checking the tank every ");
  Serial.print(WARNING_TANK_LEVEL_PROBING_INTERVAL, DEC);
  Serial.print(" seconds. Today is day #: ");
  Serial.print(today(), DEC);  
  Serial.println(".");
}

bool active(int analogInput, int threshold = 500) {
  //
  // Setup:
  //       
  //             /  
  // [+5v] o----/  ----+---[10k]----o [GND]
  //                   |
  //                   o
  //             [analogInput]
  // On: 1023
  // Off: 0
  // Prag: 500
  int value = analogRead(analogInput);
  if (value > threshold) {
    // 
    return true;
  }
  // 
  return false;
}

bool tankEmpty() {
  //
  return active(EMPTY_TANK_PORT, 1020);
}

bool bigCascadeFull() {
  //
  return active(BIG_CASCADE_FULL_PORT, 800);
}

bool smallCascadeFull() {
  //
  return active(SMALL_CASCADE_FULL_PORT, 800);
}

int today() {
  // a consistent day representation
  return (int) (time.secondstime() / 86400) + 1;
}

bool cascadeFull() {
  // 
  if (CASCADE_ALTERNATION) {
    // alternating the big and small cascade filling
    if (((int)(today() / IRRIGATION_PERIOD_DAYS) % 2) == 0) {
      // the small cascade;
      // for safety, tanking into account the big cascade filling
      return smallCascadeFull() 
        || bigCascadeFull();
    }
    // the big cascade
    return bigCascadeFull();
  }
  // filling just a cascade
  if (SMALL_CASCADE_ONLY) {
    // just the small one;
    // for safety, tanking into account the big cascade filling
    return smallCascadeFull() 
      || bigCascadeFull();
  }
  // just the big one
  return bigCascadeFull();
}

bool irrigationInterval() {
  // 
  if ((today() % IRRIGATION_PERIOD_DAYS) != IRRIGATION_DAY) {
    // not the irrigation day
    return false;
  }
  // the irrigation day
  if ((time.hour() < IRRIGATION_BEGIN_HOUR) 
    || (time.hour() > IRRIGATION_END_HOUR)) {
    // outside the irrigation hours
    return false;
  }
  // within the irrigation hours
  if ((time.minute() < IRRIGATION_BEGIN_MINUTE) 
    || (time.minute() > IRRIGATION_END_MINUTE)) {
    // outside the irrigation minutes
    return false;
  }
  // within the irrigation interval
  return true;
}

void tankEmptyWarning() {
  // 
  uint32_t timestamp = time.unixtime();
  if (timestamp - tsChangeWarningLED > 1) {
    // time to change the LED state
    tsChangeWarningLED = timestamp;
    stateWarningLED = !stateWarningLED;
    digitalWrite(SIGNALING_LED_PORT, stateWarningLED ? HIGH : LOW);
  }
}

bool execute() {
  //
  time = RTC.now();
  bSleepLonger = false;
  
  if (state == 0) {
    // init
    trace("System initialization (in 10 secunds).");
    delay(10000);
    displayConfig();
    trace("System start.");
    if (SELF_TEST) {
      // with self-testing
      state = 10;
    } else {
      // without self-testing
      state = 1;
    }
  }
  if (state == 10) {
    // self-test: empty tank LED on
    trace("Test: Start");
    digitalWrite(SIGNALING_LED_PORT, HIGH);
    delay(1000);
    state = 11;
  }
  if (state == 11) {
    // self-test: empty tank LED off
    digitalWrite(SIGNALING_LED_PORT, LOW);
    delay(1000);
    state = 12;
  }
  if (state == 12) {
    // self-test: empty tank LED on
    digitalWrite(SIGNALING_LED_PORT, HIGH);
    delay(1000);
    state = 13;
  }
  if (state == 13) {
    // self-test: empty tank LED off
    digitalWrite(SIGNALING_LED_PORT, LOW);
    delay(1000);
    state = 14;
  }
  if (state == 14) {
    // self-test: pump motor on
    digitalWrite(PUMP_MOTOR_PORT, HIGH);
    delay(8000);
    state = 15;
  }
  if (state == 15) {
    // self-test: pump motor off
    digitalWrite(PUMP_MOTOR_PORT, LOW);
    state = 16;
  }
  if (state == 16) {
    // self-test: testing cascade full switches
    for (int i = 0; i < 30; i ++) {
      // 
      int cnt = 0;
      if (smallCascadeFull()) {
        // 
        trace("Test: The small cascade is full.");
        cnt ++;
      }
      if (bigCascadeFull()) {
        //
        trace("Test: The big cascade is full.");
        cnt ++;
      }
      if (tankEmpty()) {
        //
        trace("Test: The water tank is empty.");
        cnt ++;
      }
      if (cnt == 0) {
        // 
        trace("Test: All switches are open.");
      }
      delay(1000);
    }
    if (CONTINUOUS_TEST) {
      // 
      state = 16;
    } else {
      trace("Test: Stop");
      digitalWrite(SIGNALING_LED_PORT, HIGH);
      delay(3000);
      digitalWrite(SIGNALING_LED_PORT, LOW);
      state = 1;
    }
  }
  
  
  if (state == 1) {
    // tests
    if (tankEmpty()) {
      // 
      trace("The water tank is empty!");
      state = 100;
    } 
    else {
      // the water tank is not empty
      if (irrigationInterval()) {
        // 
        trace("It is the irrigation time.");
        state = 2;
      }
      else {
        // other
        bSleepLonger = true;
      }
    }
  }
  if (state == 2) {
    // 
    trace("Starting the pump motor.");
    digitalWrite(PUMP_MOTOR_PORT, HIGH);
    state = 3;
  }
  if (state == 3) {
    // water level testing
    if (tankEmpty()) {
      // 
      trace("Water tank got empty! Emergency stop.");
      digitalWrite(PUMP_MOTOR_PORT, LOW);
      state = 100;
    }
    else if (cascadeFull()) {
      // 
      trace("The water cascade filled-up.");
      state = 4;
    }
    else if (!irrigationInterval()) {
      // 
      trace("The irrigation time is up.");
      state = 4;
    }
    else {
      // the bottle cascade is being filled
    }
  }
  if (state == 4) {
    // 
    trace("Stopping the pump motor.");
    digitalWrite(PUMP_MOTOR_PORT, LOW);
    state = 5;
  }
  if (state == 5) {
    // wating for the irrigation time to pass
    if (!irrigationInterval()) {
      // 
      trace("The irrigation time has passed.");
      state = 1;
    }
  }
  
  // WARNING
  if (state == 100) {
    // water tank is empty
    tsProbeWarningTankLevel = time.unixtime();
    state = 101;
  }
  if (state == 101) {
    // testing water tank level
    uint32_t timestamp = time.unixtime();
    tankEmptyWarning();
    if (timestamp - tsProbeWarningTankLevel > WARNING_TANK_LEVEL_PROBING_INTERVAL) {
      // time to test the water tank level
      if (tankEmpty()) {
        // the tank is still empty
        state = 100;
      } else {
        // 
        digitalWrite(SIGNALING_LED_PORT, LOW);
        trace("The tank is no longer empty.");
        state = 1;
      }
    }
  }
  return bSleepLonger;
}

void loop() {
  // 
  if (execute()) {
    // should sleep longer
    if (DEBUG) {
      // 
      trace("Hibernating for 8 seconds.");
      delay(8000);
      trace("Out of the hibernation.");
    } else {
      //
      LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
    }
  } else {
    // should sleep 1 second
    if (DEBUG) {
      //
      delay(1000);
    } else {
      // 
      LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF);
    }
  }
}


Do keep an eye on the irrigator on github.com for updates.

Feeling like you've missed the whole point of this post? Please review the Arduino plant irrigator and Off-the-grid plant irrigator articles.