Mr Green’s Greenhouse Waterer

Jpeg

Over the past couple of months I’ve been tinkering, now and then, with an automatic greenhouse waterer for my dad. Hopefully in time for summer. I “think” I have a working prototype. (I’ll add to this post and clean up the code when the first prototype is set-up in the greenhouse and working).

Features:

Mini Menu for Customization.
Settings saved between boots (EEPROM).
Displays Temperature/Humidity.
Turns off the display to save a little power.
Gravity fed (or hosepipe water source I think)
Short Parts List and cheapish.

Parts List:

Arduino Uno Dev board.
Soil Moisture Detector.
5v/12v Relay.
12v and 5v supplies.
12v Solenoid water valve.
DHT11 Sensor.
16×2 LCD.
Rotary Encoder(With button) + Knob.
5 x Resistors.

Pin Set-up:

{Image of set-up to come and finish desc}

Analogue moisture sensor To Analogue Pin 0
LCD Backlight (resistor) To Pin 6
DHT11 To Pin 7
Relay To Pin 9
Rotary encoder button To Pin 13
Rotary encoder pulse (A) To Pin 8
Rotary encoder pulse (B) To Pin 10
LCD RS To Pin 12
LCD Enable To Pin 11
LCD D4 To Pin 5
LCD D5 To Pin 4
LCD D6 To Pin 3
LCD D7 To Pin 2

Settings Explanation:

The settings menu lets you tune the system, hopefully making it more useful.

Set Watering Duration:

When the system decides that it’s time to water, this setting determines how long the relay/valve will be held open.

Set Delay Between Watering:

When the previous water has completed this setting determines how long the system will wait before watering again. This will cause the system to ignore the soil moisture reading until this time has elapsed. This setting is to safeguard against constant watering.

Set Watering Point:

The watering point is a value between 0 and 1024. This is compared against the reading we receive from the analogue sensor. When altering the value of this setting the current reading is displayed. When setting up the system stick the moisture sensor in some soil with the desired moist-ness and setting the value to the reading shown.

Set Display Timeout:

How long the LCD will continue to display after the last piece of user interaction. Save some power if running off batteries.

The Code

/*
** Includes
*/
#include 
#include 
#include 

/*
** Definitions
*/
#define MOISTUREONEPIN 0
#define MENUITEMCOUNT 5
#define LCDBACKLIGHT 6
#define DHT11PIN 7
#define RELAYPIN 9
#define PUSHBUTTON 13

/*
** EEPROM Stuff
** long values require 4 address' of 4 bytes
*/
#define EEPROM_WATERINGDURATIONSTART 0
#define EEPROM_WATERINGDELAYSTART 4
#define EEPROM_DISPLAYTIMEOUT 8
#define EEPROM_WATERINGPOINT 12

/*
** Objects
*/
dht11 DHT11;
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

/*
**Rotary Encoder stuff
*/
unsigned long currentTime;
unsigned long loopTime;
unsigned long lastButtonPress;
const int pin_A = 8;  
const int pin_B = 10;  
unsigned char encoder_A;
unsigned char encoder_B;
unsigned char encoder_A_prev=0;

/*
** Settings Menu Stuff
*/
boolean menuDirty = 0;
int menuPosition = 0;
boolean menu = 0;
int option = -1;
String optionArray[MENUITEMCOUNT] = {
  "Set Watering","Set Delay","Set Watering","Set Display","Exit",};
String optionArrayLineTwo[MENUITEMCOUNT] = {
  "Duration","Between Watering","Point","Timeout","",};

/*
**Watering Stuff
*/
boolean wateringState = 0;//wether or not watering is currently enabled
unsigned long wateringInitiated;//When Watering was started
unsigned long wateringFinished = 0;//When Watering was started
unsigned long wateringDurationRateOfChange = 10000;//For the settings menu
unsigned long wateringDuration = wateringDurationRateOfChange; //How long we water for
unsigned long minDurationBetweenWaterings = wateringDurationRateOfChange*10; //After watering, how long to wait before bothing to water again
long wateringPoint = 800;//0-1023
int wateringPointRateOfChange = 10;

/*
**Display Stuff
*/
boolean displayEnabled = 0;
unsigned long displayInitiated; //When the display was started for timeout
unsigned long displayDuration = 20000;// How long the display is active for

/*
**Dynamic stepping velocity
*/
unsigned long lastStepTime;
boolean lastStepDirection; //true = right
int timeout = 500; //after 100ms reset to default 
int stepCount = 0; //5 steps each within timeout, double the velocity
int stepMax = 3; //5 steps each within timeout, double the velocity
int velocityMultiplier = 1; // Current velocty multiplier

void setup() {
  //Load all settings from EEPROM
  initialLoadFromEEPROM();
  // set up the LCD's number of columns and rows: 
  lcd.begin(16, 2);
  // Print a message to the LCD.
  lcd.setCursor(0, 0);
  lcd.print("Mr Green's Green");
  lcd.setCursor(0, 1);
  lcd.print("House Waterer");

  pinMode(LCDBACKLIGHT, OUTPUT);
  pinMode(RELAYPIN, OUTPUT);
  pinMode(PUSHBUTTON, INPUT);   
  digitalWrite(LCDBACKLIGHT, HIGH);

  int chk = DHT11.read(DHT11PIN);
  switch (chk)
  {
  case DHTLIB_OK: 
    break;
  case DHTLIB_ERROR_CHECKSUM: 
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Checksum error"); 
    delay(5000);
    break;
  case DHTLIB_ERROR_TIMEOUT: 
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Time out error"); 
    delay(5000);
    break;
  default: 
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Unknown error"); 
    delay(5000);
    break;
  }
  delay(3000);

  displayEnabled = 1;
  displayInitiated = millis();
}

void loop() {
  checkDisplayTimeout();
  checkStepTimeout();

  int buttonState = LOW;
  if(wateringState == 1){ // if watering
    rotaryEncoderStuff();
    buttonState = digitalRead(PUSHBUTTON);
    if((millis()-wateringInitiated) > wateringDuration){
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Watering");
      lcd.setCursor(0, 1);
      lcd.print("Complete!");
      wateringState = 0;
      wateringFinished = millis();
      delay(500);
    } 
    else {
      lcd.setCursor(0, 0);
      lcd.print("Watering ...");
      lcd.setCursor(0, 1);
      lcd.print(millisToDHM(wateringDuration-(millis()-wateringInitiated))+"     "); //update duration
    }

    if (buttonState == HIGH ) { 
      if(displayEnabled == 1){
        wateringState = 0;
        menu = 1;  
        menuDirty = 1; 
        keepDisplayAlive();
      } 
      else {
        keepDisplayAlive();
      }

    }
    delay(75);
  } 
  else {
    rotaryEncoderStuff();
    if(millis() > (wateringFinished+minDurationBetweenWaterings) && wateringState == 0 || millis() <= wateringDuration && wateringState == 0 ){//enough time passed since last watering?

      //get soil moisture
      int moistOne = analogRead(MOISTUREONEPIN);
      //int moistTwo = analogRead(MOISTURETWOPIN);

      if(moistOne >= wateringPoint){ //&& moistTwo >= wateringPoint)
        if(menu == 0){ // Dont try and water while the user is attempting to use the menu
          wateringState = 1;//wether or not watering is enabled
          wateringInitiated = millis();
          if(displayEnabled == 1){
            lcd.clear();
            lcd.setCursor(0, 0);
            lcd.print("Watering ...");
          }
        } 
        else {
          display(); // just display the menu, wait for the user to finish
        }
      } 
      else {
        display();
      }
    } 
    else {
      if(wateringState == 0){
        display();
      }
    }

    if(wateringState == 1){
      digitalWrite(RELAYPIN, HIGH);
    } 
    else {
      digitalWrite(RELAYPIN, LOW);
    }

    buttonState = digitalRead(PUSHBUTTON);

    // check if the pushbutton is pressed.
    if (buttonState == HIGH && abs((millis()-lastButtonPress))>500) { 
      if(displayEnabled == 0){
        keepDisplayAlive();
      } 
      else {
        keepDisplayAlive();
        if(menu == 0){
          menu = 1;
          menuDirty = 1;
        } 
        else {
          switch(menuPosition){
          case 0:
            if(option == -1){ // nothing selected, select the submenu
              option = menuPosition;
              menuDirty = 1;
            } 
            else { // returning from submenu to main menu, save value of item0(watering duration)
              //Save to eeprom and return
              if(option == 0){
                //save to eeprom
                EEPROMWritelong(EEPROM_WATERINGDELAYSTART, wateringDuration);
              }
              option = -1;
              menuDirty = 1;
            }
            break;
          case 1:
            if(option == -1){
              option = menuPosition;
              menuDirty = 1;
            } 
            else {
              //Save to eeprom and return
              if(option == 1){
                //save to eeprom
                EEPROMWritelong(EEPROM_WATERINGDURATIONSTART, minDurationBetweenWaterings);
              }
              //Save to eeprom and return
              option = -1;
              menuDirty = 1;
            }
            break;
          case 2:
            if(option == -1){
              option = menuPosition;
              menuDirty = 1;
            } 
            else {
              //Save to eeprom and return
              if(option == 2){
                //save to eeprom
                EEPROMWritelong(EEPROM_WATERINGPOINT, wateringPoint);
              }
              //Save to eeprom and return
              option = -1;
              menuDirty = 1;
            }
            break;
          case 3:
            if(option == -1){
              option = menuPosition;
              menuDirty = 1;
            } 
            else {
              //Save to eeprom and return
              if(option == 3){
                //save to eeprm
                EEPROMWritelong(EEPROM_DISPLAYTIMEOUT, displayDuration);
              }
              option = -1;
              menuDirty = 1;
            }
            break;
          case 4:
            menu = 0;
            menuPosition = 0;
            break;
          case 5:
            break;
          }
        }
        lastButtonPress = millis();
        delay(150);
      }
    }
  }
}

void display(){
  if(displayEnabled == 1){//Display is enabled
    if(menu){
      if(option == -1){
        if(menuDirty == 1){
          menuDirty = 0;
          if(displayEnabled == 1){
            lcd.clear();
            lcd.setCursor(0, 0);
            lcd.print(optionArray[menuPosition]);
            lcd.setCursor(0, 1);
            lcd.print(optionArrayLineTwo[menuPosition]);
          }
          delay(5);
        }  
      } 
      else {
        if(menuDirty == 1){
          if(displayEnabled == 1){
            lcd.clear();
          }
          menuDirty = 0;
        }
        switch (option) {
        case 0:
          {
            if(displayEnabled == 1){
              lcd.setCursor(0, 0);
              lcd.print("Water Duration");
              lcd.setCursor(0, 1);
              lcd.print(millisToDHM(wateringDuration));
            }
          }
          break;
        case 1:
          {//do something when var equals 2
            if(displayEnabled == 1){
              int wminutes = ((minDurationBetweenWaterings/1000)/60);
              int wseconds = ((minDurationBetweenWaterings/1000)%60);
              lcd.setCursor(0, 0);
              lcd.print("Water Min Delay");
              lcd.setCursor(0, 1);
              lcd.print(millisToDHM(minDurationBetweenWaterings));
            }
          }
          break;
        case 2:
          if(displayEnabled == 1){
            int an = analogRead(MOISTUREONEPIN);
            lcd.setCursor(0, 0);
            lcd.print("Threshold: ");
            lcd.print(wateringPoint);
            lcd.setCursor(0, 1);
            lcd.print("Current:");
            lcd.print(an);
          }
          break;
        case 3:
          if(displayEnabled == 1){
            lcd.setCursor(0, 0);
            lcd.print("Display Timeout");
            lcd.setCursor(0, 1);
            lcd.print(millisToDHM(displayDuration));
          }
          break;
        default: 
          // if nothing else matches, do the default
          // default is optional
          break;
        }
      }
    } 
    else {
      DHT11.read(DHT11PIN);
      float humid = (float)DHT11.humidity;
      float temp = (float)DHT11.temperature;
      if(displayEnabled == 1){
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("H");
        lcd.print(humid);
        lcd.print("% T");
        lcd.print(temp);
        lcd.print("C");
        lcd.setCursor(0, 1);
        lcd.print("Lst Wtr:");
        lcd.print(millisToDHM(millis()-wateringFinished));
      }
      delay(1000);
    }
  }
}

void rotaryEncoderStuff(){
  // get the current elapsed time
  currentTime = millis();
  if(currentTime >= (loopTime + 5)){
    // 5ms since last check of encoder = 200Hz  
    encoder_A = digitalRead(pin_A);    // Read encoder pins
    encoder_B = digitalRead(pin_B);   
    if((!encoder_A) && (encoder_A_prev)){
      // A has gone from high to low 
      if(encoder_B) {
        // B is high so clockwise
        // increase
        keepDisplayAlive();
        next();
      }   
      else {
        // B is low so counter-clockwise      
        // decrease
        keepDisplayAlive();
        previous();
      }   
    }   
    encoder_A_prev = encoder_A;     // Store value of A for next time    
    loopTime = currentTime;  // Updates loopTime
  }
}

void next(){
  stepVelocity(true);
  if(menu ==1){
    if(option == -1){//on main menu
      if(menuPosition < MENUITEMCOUNT){
        menuPosition++;
      } 
      if(menuPosition == MENUITEMCOUNT){
        menuPosition = 0; 
      }
      menuDirty = 1;
    } 
    else {
      if(option == 0){//watering duration
        if((wateringDuration+(wateringDurationRateOfChange*velocityMultiplier))>=2147483647){
          wateringDuration = 2147483646;//max long
        } 
        else {
          wateringDuration += (wateringDurationRateOfChange*velocityMultiplier);
        }
        menuDirty = 1;
      }
      if(option == 1){//watering delay
        if(minDurationBetweenWaterings+(wateringDurationRateOfChange*velocityMultiplier)>=2147483647){
          minDurationBetweenWaterings = 2147483646;//max long
        } 
        else {
          minDurationBetweenWaterings += (wateringDurationRateOfChange*velocityMultiplier);
        }
        menuDirty = 1;
      }
      if(option == 2){//Watering Point
        if((wateringPoint+wateringPointRateOfChange) >=1023){
          wateringPoint = 1023;
        } 
        else {
          wateringPoint += wateringPointRateOfChange;
        }
        menuDirty = 1;
      }

      if(option == 3){//Disable watering when freezing
        if(displayDuration+wateringDurationRateOfChange>=2147483647){
          displayDuration = 2147483646;//max long
        } 
        else {
          displayDuration += (wateringDurationRateOfChange*velocityMultiplier);
        }
        menuDirty = 1;
      }
    }
  }

}

void previous(){
  stepVelocity(false);
  if(menu ==1){
    if(option == -1){//on main menu
      if(menuPosition == 0){
        menuPosition = MENUITEMCOUNT-1; 
      } 
      else {
        if(menuPosition > 0){
          menuPosition--;
        }
      }
      menuDirty = 1;
    } 
    else {
      if(option == 0){//watering duration

        if(wateringDuration<=(wateringDurationRateOfChange*velocityMultiplier)){
          wateringDuration = wateringDurationRateOfChange;//max long
        } 
        else {
          wateringDuration -= (wateringDurationRateOfChange*velocityMultiplier);
        }
        menuDirty = 1;

      }
      if(option == 1){//watering delay
        if(minDurationBetweenWaterings<=(wateringDurationRateOfChange*velocityMultiplier)){
          minDurationBetweenWaterings = wateringDurationRateOfChange;//max long
        } 
        else {
          minDurationBetweenWaterings -= (wateringDurationRateOfChange*velocityMultiplier);
        }
        menuDirty = 1;


      }
      if(option == 2){//watering point

        if((wateringPoint-wateringPointRateOfChange) <0){
          wateringPoint = 0;
        } 
        else {
          wateringPoint-=wateringPointRateOfChange;
        }
menuDirty = 1;
      }

      if(option == 3){//Disable watering when freezing
        if(displayDuration-(wateringDurationRateOfChange*velocityMultiplier)<=10000){
          displayDuration = 10000;//max long
        } 
        else {
          displayDuration -= (wateringDurationRateOfChange*velocityMultiplier);
        }
        menuDirty = 1;
      }
    }
  }
}

void stepVelocity(boolean right){//true right


    if(lastStepDirection == right){//we are going the same direction
       stepCount++;  
       lastStepTime = millis();
    } else {
      velocityMultiplier = 1;
      lastStepDirection = right;
      stepCount = 0;
      lastStepTime = millis();
    }

}

void checkStepTimeout(){
  
  if(stepCount > 0){
    if((millis()-lastStepTime)>timeout ){
      stepCount = 0;
      velocityMultiplier = 1;
    } else {
    
      if(stepCount >= stepMax){
        if(velocityMultiplier>100){
        stepCount = 0;
        }
        else {
        velocityMultiplier = (velocityMultiplier*2);
        stepCount = 0;
        }
      }
      
    }
    
  }

}

String millisToDHM(long mils){
  {
    String result = "";
    int hours = (((mils/1000)/60)/60)%24;
    int minutes = ((mils/1000)/60)%60;
    int days = ((((mils / 1000)/60)/60)/24);
    int seconds = (mils/1000)%60;

    if(days>0){
      result +=days;
      result +="d";
    }
    if(hours>0){
      result +=hours;
      result +="h";
    }
    if(minutes>0){
      result +=minutes;
      result +="m";
    }
    if(seconds>0){
      result +=seconds;
      result +="s";
    }

    if(result == ""){
      return "0s";
    }
    else{
      return result;
    }
  } 

}

void keepDisplayAlive(){
  displayInitiated = millis();
}

void checkDisplayTimeout(){

  if(displayEnabled == 0){//display is off
    if((millis()-displayInitiated) < displayDuration){
      digitalWrite(LCDBACKLIGHT, HIGH);

      lcd.display();
      lcd.clear();
      displayEnabled = 1;
    }
  } 
  else {
    if((millis()-displayInitiated) > displayDuration){
      lcd.noDisplay();
      digitalWrite(LCDBACKLIGHT, LOW);
      menuDirty = 0;
      menuPosition = 0;
      menu = 0;
      option = -1;
      displayEnabled = 0;  
    } 
  }
}

/*
** EEPROM Section
*/

void initialLoadFromEEPROM(){
  long _displayDuration = EEPROMReadlong(EEPROM_DISPLAYTIMEOUT);
  long _wateringPoint = EEPROMReadlong(EEPROM_WATERINGPOINT);
  long _minDurationBetweenWaterings =EEPROMReadlong(EEPROM_WATERINGDURATIONSTART);
  long _wateringDuration = EEPROMReadlong(EEPROM_WATERINGDELAYSTART);
  
  if(_displayDuration < 2000000000 && _displayDuration >= 10){
    displayDuration = _displayDuration;
  } else {
    EEPROMWritelong(EEPROM_DISPLAYTIMEOUT, displayDuration);
  }
  if(_wateringPoint< 2000000000 && _wateringPoint >= 10){
    wateringPoint = _wateringPoint;
  } else {
    EEPROMWritelong(EEPROM_WATERINGPOINT, wateringPoint);
  }
  if(_minDurationBetweenWaterings< 2000000000 && _minDurationBetweenWaterings >= 10){
    minDurationBetweenWaterings = _minDurationBetweenWaterings;
  } else {
    EEPROMWritelong(EEPROM_WATERINGDELAYSTART, minDurationBetweenWaterings);
  }
  if(_wateringDuration< 2000000000 && _wateringDuration >= 10){
    wateringDuration = _wateringDuration;
  } else {
    EEPROMWritelong(EEPROM_WATERINGDURATIONSTART, wateringDuration);
  }
}

//This function will write a 4 byte (32bit) long to the eeprom at
//the specified address to adress + 3.
void EEPROMWritelong(int address, long value)
{
  //Decomposition from a long to 4 bytes by using bitshift.
  //One = Most significant -> Four = Least significant byte
  byte four = (value & 0xFF);
  byte three = ((value >> 8) & 0xFF);
  byte two = ((value >> 16) & 0xFF);
  byte one = ((value >> 24) & 0xFF);

  //Write the 4 bytes into the eeprom memory.
  EEPROM.write(address, four);
  EEPROM.write(address + 1, three);
  EEPROM.write(address + 2, two);
  EEPROM.write(address + 3, one);
}

long EEPROMReadlong(long address)
{
  //Read the 4 bytes from the eeprom memory.
  long four = EEPROM.read(address);
  long three = EEPROM.read(address + 1);
  long two = EEPROM.read(address + 2);
  long one = EEPROM.read(address + 3);

  //Return the recomposed long by using bitshift.
  return ((four << 0) & 0xFF) + ((three << 8) & 0xFFFF) + ((two << 16) & 0xFFFFFF) + ((one << 24) & 0xFFFFFFFF);
}

Bug Fixes:
09/01/2015 - Save LCD Duration, Watering Point Increment/Decrement, Flickering Home Screen.

On a side note, box files make great temporary project cases.

This entry was posted in arduino, Programming, Tutorial and tagged , , , . Bookmark the permalink.

2 Responses to Mr Green’s Greenhouse Waterer

Leave a Reply to Zdenko Cancel reply

Your email address will not be published.