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.
2 Responses to Mr Green’s Greenhouse Waterer