Home plant watering system with LCD and keypad - an issue with multiple loops

Hi, I have constructed a home plant watering system with an I2C 2x16 LCD display, 4x4 keypad and relay module (to control the water pump). I have a programming problem. In the code I wrote, the LCD display goes crazy and shows kind of simultaneous text from different loops, and the keyboard is completely unresponsive. The program was intended to work like this:

  1. display a message on the LCD: "Hello, take care of your plants!",
  2. display a message on the LCD asking you to input a variable ( for how many hours watering will happen):
    "Interval [h]:" (it must be possible to enter more than one number).
  3. after pressing "#" on the keypad, clearing the screen and displaying a message on the screen asking you to input a variable (how long the plants will be watered):
    "Watering [s]:" (as for the interval, more than one number).
  4. after pressing "A" on the keypad , start the timer and display a message on the display: "Wait :)"
  5. when the time expires, clearing the LCD display, displaying the message "Watering!!!" and activation of the pump relay
    for a previously specified time.
  6. after watering return to start timer function again and display message on the display: "Wait :)"
  7. after pressing the "B" button - Additional watering beyond the countdown and return to step 6.
  8. after the "C" button is pressed - reset the program, so that the variables can be re-entered.

I've tried many different approaches and I can't get over the problem. I would greatly appreciate your help.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <millisDelay.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

const int Relay= 10;
bool RelayState= HIGH;
int hoursToWater;
int wateringTime;

millisDelay mdTimer1;


void setup() {
  
  pinMode(Relay, OUTPUT);
  digitalWrite(Relay, HIGH);
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0,0);
  lcd.print("Hello, take care");
  lcd.setCursor(0,1);
  lcd.print("of your plants!");
  delay(3000);
}

void loop() {
    watering_interval();
    watering_time();
    watering_loop();
}

void watering_interval() {

  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("Interval [h]:");
  
  char key = keypad.getKey(); 
  
  static String firstNumber = ""; 

  if (key != NO_KEY) {
    if (key >= '0' && key <= '9') {
      firstNumber += key;
    }
    else if (key == '#') {
      int hoursToWater = atoi(firstNumber.c_str()); 
      lcd.setCursor(0,1);
      lcd.print(hoursToWater); 
      delay(3000);
      watering_time();
    }
  }
}
void watering_time() {  
  
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("Watering [s]:");
  
  char key = keypad.getKey(); 
  
  static String secondNumber = ""; 

  if (key != NO_KEY) {
    if (key >= '0' && key <= '9') {
      secondNumber += key;
    }
    else if (key == '#') {
      int wateringTime = atoi(secondNumber.c_str()); 
      lcd.setCursor(0,1);
      lcd.print(wateringTime); 
      delay(3000);
      watering_loop();    
    }
  }
}

void watering_loop()  {

  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("Press A)");
  
  char key = keypad.getKey(); 
  
  if (key != NO_KEY) {
    if (key == 'A') {
      lcd.clear(); 
      lcd.setCursor(0,0);
      lcd.print("Wait :)");
      int hoursToWater2= hoursToWater*3600000;
      mdTimer1.start(hoursToWater2);
      //delay(hoursToWater2);
      if (mdTimer1.justFinished()){
        lcd.print("Watering!!!");
        digitalWrite(Relay, LOW);
        int wateringTime2= wateringTime*1000;
        delay(wateringTime2);
        digitalWrite(Relay, HIGH);
      }
    }
    else if (key == 'B') {
      int hoursToWater2= hoursToWater*3600000;
      delay(hoursToWater2);
      lcd.print("Watering!!!");
      digitalWrite(Relay, LOW);
      int wateringTime2= wateringTime*1000;
      delay(wateringTime2);
      digitalWrite(Relay, HIGH);
      watering_loop();
    }
    else if (key == 'C') {
      watering_interval();
    }
  }
}

Welcome to the forum.

This millisDelay or this millisDelay ?

I made your project in Wokwi (I did not change your sketch):

Thank you for your answer!

It seems that it is first one from (https://github.com/ansonhe97).

I've also tried it on Wokwi and on my real system and the results is just the same.

Your code has a number of problems.

Firstly getkey() will either return NO_KEY or the key value. If it returns NO_KEY you just finish and go to the next part of the code... presumably you want to wait until you actually receive something... either the end of a number ('#'), or an instruction ('A', 'B', or 'C') ?

Secondly, you shouldn't call the 3 routines from inside of each other... they are already running sequentially in loop(). You just need to wait before moving to the next routine (as per the first point).

Also...

Maximum int is 32,767.

In general not a good idea to use delay to control timing... all code is blocked so if you wanted to do anything in parallel (like get a key press)... then you can't.

You need to implement this as a finite state machine. Google will give you lots of examples. That way you have it, watering_interval() calls watering_time() which calls watering_loop() which calls watering_interval().... an potential infinite loop.

Here's a starter for you... see if you get the idea.

is this issue resolved? if not try using modes. what i mean is like this,

enum Mode {
  WELCOME_MESSAGE_MODE,
  INTERVAL_WATERING_MODE,
  HOURS_WATERING_MODE,
  DISPLAY_TIMER_MODE,
  WATERING_MODE,
  etc...
  
};
Mode current_mode = WELCOME_MESSAGE_MODE;    //setting the mdoe to default.

after that your loop should look like this.

void loop(){
   switch (current_mode) {
        case WELCOME_MESSAGE_MODE:
                 //implement your welcome message here.
                 break;
        case INTERVAL_WATERING_MODE:
                  //implement your prompt here and wait wor user input in the keypad.
                  break;
//continue the switch statement.
   }
}

I hope you get the idea.

regards Ambabo

Hi, @aghadr
Welcome to the forum.

Did you write your code in stages?
Do you have some code that JUST reads the keyboard?
To prove your hardware and code?

Do you have some code that JUST displays on the LCD?

Tom.. :smiley: :+1: :coffee: :australia:

Hello aghadr

I have checked the code a little.
This sketch seems to have grown organically through nested and dupilcate function calls. This leads to spaghetti code and nodes in the logic that are functionally undesirable.
What to do.
There are two options.
Debug, using multiple Serial.println()'s to see what happens under different conditions. Nodes can be solved this way, but new ones can also be created in the process.
The second option is to rearrange the sketch. Based on the IPO model, structure the desired function of the sketch into basic functions. Take a sheet of paper and a pencil and draw a programme structure to identify the required functions, e.g. button, timer and display functions. All these functions can be coded and tested separately. To complete the project, you also need to design a control structure that uses events to call the above functions. In this way keep in mind to design objects and related services thus handle input/output actions.
That is all that needs to be done. Try them out.

Have a nice day and enjoy coding in C++.

This should be close...

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

const int Relay = 10;
bool RelayState = HIGH;
unsigned long waitInterval;   // time to wait before beginning to water, mseconds
unsigned long waterInterval;  // time to water, mseconds

unsigned long currentTime;
unsigned long startTime;

enum { INTERVAL_TIME, WATER_TIME, WAIT, WATER };
int currentState = INTERVAL_TIME;

char key;           // current key press, if any
int inputNumber;    // current value of keypad input
int keyCount;       // current count of digits presses

const int keyCountMax = 2;  // maximum digits allowed 00-99

void beginState ( int state ) {
  // things to do when you first move into a state

  currentState = state;
  Serial.print( "Beginning state " );
  switch ( state ) {

    case INTERVAL_TIME:
      Serial.println("Interval set");
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Interval [h]:");
      lcd.setCursor(0, 1);
      // reset keypad input
      inputNumber = 0;
      keyCount = 0;
      break;

    case WATER_TIME:
      Serial.println("Water time set" );
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Watering [s]:");
      lcd.setCursor(0, 1);
      // reset keypad input
      inputNumber = 0;
      keyCount = 0;
      break;

    case WAIT:
      Serial.println("Waiting" );
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Waiting");
      startTime = millis();
      digitalWrite(Relay, HIGH);
      break;

    case WATER:
      Serial.println("Watering" );
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Watering");
      startTime = millis();
      digitalWrite(Relay, LOW);
      break;

    default:
      Serial.println("Bad state");
      while (1);
  }
}


void checkState() {
  // things to do when you are monitoring a state and possibly
  // transitioning to a new state

  switch ( currentState ) {

    case INTERVAL_TIME:
      if (key != NO_KEY) {
        if (key >= '0' && key <= '9' && keyCount < keyCountMax ) {
          lcd.print(key);
          keyCount++;
          inputNumber = inputNumber * 10 + key - '0';
        }
        else if (key == '#') {
          waitInterval = inputNumber * 60UL * 60UL * 1000UL;    // hours to milliseconds
          beginState(WATER_TIME);
        }
      }
      break;

    case WATER_TIME:
      if (key != NO_KEY) {
        if (key >= '0' && key <= '9' && keyCount < keyCountMax ) {
          lcd.print(key);
          keyCount++;
          inputNumber = inputNumber * 10 + key - '0';
        }
        else if (key == '#') {
          waterInterval = inputNumber * 1000UL;   // seconds to milliseconds
          beginState(WAIT);
        }
      }
      break;

    case WAIT:
      if ( currentTime - startTime >= waitInterval ) {
        // done waiting, time to water
        beginState(WATER);
      }
      else {
        displayTime(currentTime - startTime);
      }

      if ( key == '#' ) {
        // cancel and start again
        beginState(INTERVAL_TIME);
      }
      break;

    case WATER:
      if ( currentTime - startTime >= waterInterval ) {
        // done watering, turn off relay and start waiting again
        digitalWrite(Relay, HIGH);
        beginState(WAIT);
      }
      else {
        displayTime(currentTime - startTime);
      }

      if ( key == '#' ) {
        // cancel and start again
        digitalWrite(Relay, HIGH);
        beginState(INTERVAL_TIME);
      }
      break;
  }
}


void displayTime(long msec) {
  static long lastTime;

  msec /= 1000;   // convert to seconds
  
  if ( msec == lastTime ) {
    // no update needed
    return;
  }

  lastTime = msec;
  int hours  = msec / 3600;
  msec %= 3600;   // seconds after the hour
  int minutes = msec / 60;
  int seconds = msec % 60;

  lcd.setCursor(0,1);
  if ( hours < 10 ) lcd.print('0');
  lcd.print(hours);
  lcd.print(':');
  if ( minutes < 10 ) lcd.print('0');
  lcd.print(minutes);
  lcd.print(':');
  if ( seconds < 10 ) lcd.print('0');
  lcd.print(seconds);
}


void setup() {

  pinMode(Relay, OUTPUT);
  digitalWrite(Relay, HIGH);
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("Hello, take care");
  lcd.setCursor(0, 1);
  lcd.print("of your plants!");
  delay(3000);
  beginState( INTERVAL_TIME );
}

void loop() {
  currentTime = millis();
  key = keypad.getKey();
  checkState();
}

Thank you all for your helpful answer. The problem has been resolved!
Plants are happy!