LCD Menu

I did a search and it seems like a lot of people are using this menu that I am posting, possible because its not too difficult. Like others I am going to give it a shot. The one question I have is how to add a bool selection for the second entry. Everything the author has are int values, but I would like to add one int and one bool value that allows a Yes or No entry.

On this line below I have an int, the other entry "Auto Run" how can I get that selection to toggle to either Yes or No? Or even have both Yes and No on the same line and toggle over to select which one I want. Appreicate some help, thanks.

i.e
Auto Run (toggle to yes/no)
No

or

Auto Run (move over to select entry)
Yes No

I have no clue on what to change to make this happen or if its even possible. Appreciate any help as menus are not easy to understand.

String screens[numOfScreens][2] = {{"Motor Voltage","Volts"}, {"Auto Run", ""}};
#include <LiquidCrystal.h>
LiquidCrystal lcd(6, 7, 5, 4, 3, 2);

//Input & Button Logic
const int numOfInputs = 2;
const int inputPins[numOfInputs] = {2,3};
int inputState[numOfInputs];
int lastInputState[numOfInputs] = {LOW,LOW};
bool inputFlags[numOfInputs] = {LOW,LOW};
long lastDebounceTime[numOfInputs] = {0,0};
long debounceDelay = 5;

//LCD Menu Logic
const int numOfScreens = ;
int currentScreen = 0;
String screens[numOfScreens][2] = {{"Motor Voltage","Volts"}, {"Auto Run", ""}};
int parameters[numOfScreens];

void setup() {
  for(int i = 0; i < numOfInputs; i++) {
    pinMode(inputPins[i], INPUT);
    digitalWrite(inputPins[i], HIGH); // pull-up 20k
  }
  //Serial.begin(9600);
  lcd.begin(16, 2);
}

void loop() {
  setInputFlags();
  resolveInputFlags();
}

void setInputFlags() {
  for(int i = 0; i < numOfInputs; i++) {
    int reading = digitalRead(inputPins[i]);
    if (reading != lastInputState[i]) {
      lastDebounceTime[i] = millis();
    }
    if ((millis() - lastDebounceTime[i]) > debounceDelay) {
      if (reading != inputState[i]) {
        inputState[i] = reading;
        if (inputState[i] == HIGH) {
          inputFlags[i] = HIGH;
        }
      }
    }
    lastInputState[i] = reading;
  }
}

void resolveInputFlags() {
  for(int i = 0; i < numOfInputs; i++) {
    if(inputFlags[i] == HIGH) {
      inputAction(i);
      inputFlags[i] = LOW;
      printScreen();
    }
  }
}

void inputAction(int input) {
  if(input == 0) {
    if (currentScreen == 0) {
      currentScreen = numOfScreens-1;
    }else{
      currentScreen--;
    }
  }else if(input == 1) {
    if (currentScreen == numOfScreens-1) {
      currentScreen = 0;
    }else{
      currentScreen++;
    }
  }else if(input == 2) {
    parameterChange(0);
  }else if(input == 3) {
    parameterChange(1);
  }
}

void parameterChange(int key) {
  if(key == 0) {
    parameters[currentScreen]++;
  }else if(key == 1) {
    parameters[currentScreen]--;
  }
}

void printScreen() {
  lcd.clear();
  lcd.print(screens[currentScreen][0]);
  lcd.setCursor(0,1);
  lcd.print(parameters[currentScreen]);
  lcd.print(" ");
  lcd.print(screens[currentScreen][1]);
}

I can't think of an easy way to do this. Probably the easiest way would be to define a Screen class, which has two methods called processInput(int key) and printScreen(LCD& lcd). You would then subclass from this class to override the behaviour for either IntScreen or BoolScreen.

However this involves virtual functions which add a bit of overhead to your program. But since you are using the String class anyway, I guess that doesn't do much.

EDIT: Here you go (very interesting topic). This implementation actually needs 4% less program memory.

You define a base class for all your possible screens

class Screen
{
  public:
    virtual void updateParameter(int input) = 0;
    virtual void printScreen(LiquidCrystal& lcd) = 0;
};

You can derive all other screens from this base screen (I've only done two)
For Numbers:

class NumberScreen : public Screen
{
  private:
    const char* text = "Motor Voltage";
    const char* unit = "Volts";
    int value;
  public:
    NumberScreen()
    {
      value = 0;
    }

    virtual void updateParameter(int input)
    {
      if (input == 0)
        value++;
      else
        value--;
    }

    virtual void printScreen(LiquidCrystal& lcd)
    {
      lcd.clear();
      lcd.print(text);
      lcd.setCursor(0, 1);
      lcd.print(value);
      lcd.print(" ");
      lcd.print(unit);
    }
};

For Boolean

class BoolScreen : public Screen
{
  private:
    const char* text = "Auto Run";
    bool value;
  public:
    BoolScreen()
    {
      value = true;
    }

    virtual void updateParameter(int input)
    {
      value = !value;
    }

    virtual void printScreen(LiquidCrystal& lcd)
    {
      lcd.clear();
      lcd.print(text);
      lcd.setCursor(0, 1);
      lcd.print( (value == true) ? "yes" : "no" );
    }
};

Do you see how they are very much the same but differ only in the way the updateParameter and printScreen methods are implemented?

So how do you use it? First replace all variables related to the screens with this:

BoolScreen boolScreen;
NumberScreen numScreen;
Screen* currentScreen;

Now you have created yourself a NumberScreen, a BoolScreen and a pointer to the current screen. Note that the pointer is of type Screen, which is the base for both Number- and BoolScreen.

Now you only need to change the inputAction method to

void inputAction(int input) {
  if(input == 0 || input == 1) {
    if(currentScreen == &numScreen)
      currentScreen = &boolScreen;
    else
      currentScreen = &numScreen;
    
  }else if(input == 2) {
    currentScreen->updateParameter(0);
  }else if(input == 3) {
    currentScreen->updateParameter(1);
  }
}

and the parameterChange and printScreen methods to:

void parameterChange(int key) {
  currentScreen->updateParameter(key);
}

void printScreen()
{
  currentScreen->printScreen(g_lcd);
}

This should give you a good starting point for implementing more screens. Or you could add methods to the existing classes like setText(const char*) and setUnit(const char*). To iterate through the list of screens you should use something else than I did (maybe an array or a linked list).

PS: Here is the full code. Tested under Arduino 1.8.3 IDE and Arduino Nano with a 20x4 lcd.

#include <LiquidCrystal.h>
LiquidCrystal g_lcd(12, 11, 5, 4, 3, 2);
//Input & Button Logic
const int numOfInputs = 4;
const int inputPins[numOfInputs] = {7,8,9,10};
int inputState[numOfInputs];
int lastInputState[numOfInputs] = {LOW,LOW};
bool inputFlags[numOfInputs] = {LOW,LOW};
long lastDebounceTime[numOfInputs] = {0,0};
long debounceDelay = 5;

class Screen
{
  public:
    virtual void updateParameter(int input) = 0;
    virtual void printScreen(LiquidCrystal& lcd) = 0;
};

class NumberScreen : public Screen
{
  private:
    const char* text = "Motor Voltage";
    const char* unit = "Volts";
    int value;
  public:
    NumberScreen()
    {
      value = 0;
    }

    virtual void updateParameter(int input)
    {
      if (input == 0)
        value++;
      else
        value--;
    }

    virtual void printScreen(LiquidCrystal& lcd)
    {
      lcd.clear();
      lcd.print(text);
      lcd.setCursor(0, 1);
      lcd.print(value);
      lcd.print(" ");
      lcd.print(unit);
    }
};

class BoolScreen : public Screen
{
  private:
    const char* text = "Auto Run";
    bool value;
  public:
    BoolScreen()
    {
      value = true;
    }

    virtual void updateParameter(int input)
    {
      value = !value;
    }

    virtual void printScreen(LiquidCrystal& lcd)
    {
      lcd.clear();
      lcd.print(text);
      lcd.setCursor(0, 1);
      lcd.print( (value == true) ? "yes" : "no" );
    }
};

BoolScreen boolScreen;
NumberScreen numScreen;
Screen* currentScreen;

void setup() {
  currentScreen = &numScreen;
  // put your setup code here, to run once:
  g_lcd.begin(16, 2);
}

void loop() {
  setInputFlags();
  resolveInputFlags();
}


void setInputFlags() {
  for(int i = 0; i < numOfInputs; i++) {
    int reading = digitalRead(inputPins[i]);
    if (reading != lastInputState[i]) {
      lastDebounceTime[i] = millis();
    }
    if ((millis() - lastDebounceTime[i]) > debounceDelay) {
      if (reading != inputState[i]) {
        inputState[i] = reading;
        if (inputState[i] == HIGH) {
          inputFlags[i] = HIGH;
        }
      }
    }
    lastInputState[i] = reading;
  }
}

void resolveInputFlags() {
  for(int i = 0; i < numOfInputs; i++) {
    if(inputFlags[i] == HIGH) {
      inputAction(i);
      inputFlags[i] = LOW;
      printScreen();
    }
  }
}

void inputAction(int input) {
  if(input == 0 || input == 1) {
    if(currentScreen == &numScreen)
      currentScreen = &boolScreen;
    else
      currentScreen = &numScreen;
    
  }else if(input == 2) {
    currentScreen->updateParameter(0);
  }else if(input == 3) {
    currentScreen->updateParameter(1);
  }
}

void parameterChange(int key) {
  currentScreen->updateParameter(key);
}

void printScreen()
{
  currentScreen->printScreen(g_lcd);
}

Thanks for the code, I tested it and it looks like something is messed up on my side. Only one of my buttons work and the code just sort of drifts from screen to screen without any input. Let me see if I can figure it out.

I changed the input pins to 7,8,9 and 10. Also I changed the lcd-pins. You need to put in your pins.

LightuC:
I changed the input pins to 7,8,9 and 10. Also I changed the lcd-pins. You need to put in your pins.

Thats the first thing I did when I tested the code, still looking to see whats going on.

Do you have pullups/pulldowns for your inputs enabled? Random changing might be due to floating inputs.

This is what I have, I just changed the necessary items and compiled.

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

//LiquidCrystal g_lcd(12, 11, 5, 4, 3, 2);
LiquidCrystal_I2C g_lcd(0x27, 16, 2);

//Input & Button Logic
//const int numOfInputs = 4;
//const int inputPins[numOfInputs] = {7,8,9,10};

const int numOfInputs = 3;
const int inputPins[numOfInputs] = {2,3,4};


int inputState[numOfInputs];
int lastInputState[numOfInputs] = {LOW,LOW,LOW};
bool inputFlags[numOfInputs] = {LOW,LOW,LOW};
long lastDebounceTime[numOfInputs] = {0,0,0};
long debounceDelay = 5;

class Screen
{
  public:
    virtual void updateParameter(int input) = 0;
    virtual void printScreen(LiquidCrystal_I2C& lcd) = 0;
};

class NumberScreen : public Screen
{
  private:
    const char* text = "Motor Voltage";
    const char* unit = "Volts";
    int value;
  public:
    NumberScreen()
    {
      value = 0;
    }

    virtual void updateParameter(int input)
    {
      if (input == 0)
        value++;
      else
        value--;
    }

    virtual void printScreen(LiquidCrystal_I2C& lcd)
    {
      lcd.clear();
      lcd.print(text);
      lcd.setCursor(0, 1);
      lcd.print(value);
      lcd.print(" ");
      lcd.print(unit);
    }
};

class BoolScreen : public Screen
{
  private:
    const char* text = "Auto Run";
    bool value;
  public:
    BoolScreen()
    {
      value = true;
    }

    virtual void updateParameter(int input)
    {
      value = !value;
    }

    virtual void printScreen(LiquidCrystal_I2C& lcd)
    {
      lcd.clear();
      lcd.print(text);
      lcd.setCursor(0, 1);
      lcd.print( (value == true) ? "yes" : "no" );
    }
};

BoolScreen boolScreen;
NumberScreen numScreen;
Screen* currentScreen;

void inputAction(int input) {
  if(input == 0 || input == 1) {
    if(currentScreen == &numScreen)
      currentScreen = &boolScreen;
    else
      currentScreen = &numScreen;
    
  }else if(input == 2) {
    currentScreen->updateParameter(0);
  }else if(input == 3) {
    currentScreen->updateParameter(1);
  }
}

void parameterChange(int key) {
  currentScreen->updateParameter(key);
}

void printScreen()
{
  currentScreen->printScreen(g_lcd);
}

void setInputFlags() {
  for(int i = 0; i < numOfInputs; i++) {
    int reading = digitalRead(inputPins[i]);
    if (reading != lastInputState[i]) {
      lastDebounceTime[i] = millis();
    }
    if ((millis() - lastDebounceTime[i]) > debounceDelay) {
      if (reading != inputState[i]) {
        inputState[i] = reading;
        if (inputState[i] == HIGH) {
          inputFlags[i] = HIGH;
        }
      }
    }
    lastInputState[i] = reading;
  }
}

void resolveInputFlags() {
  for(int i = 0; i < numOfInputs; i++) {
    if(inputFlags[i] == HIGH) {
      inputAction(i);
      inputFlags[i] = LOW;
      printScreen();
    }
  }
}

void setup() {
  currentScreen = &numScreen;
  // put your setup code here, to run once:
  //g_lcd.begin(16, 2);
  g_lcd.init();
  g_lcd.backlight();
}

void loop() {
  setInputFlags();
  resolveInputFlags();
}

I just compiled your sketch and tested it and it works just fine for me. How did you connect your buttons and the i2c-lcd? Do you have external pullup/pulldown resistors in your circuit?

Also you might want to initialize the input pins in your setup:

for(int i = 0; i < numOfInputs; i++)
{
   pinMode(inputPins[i], INPUT);
   // If you have no external pullup/pulldown try
   // pinMode(inputPins[i], INPUT_PULLUP);
}

If you use internal or external pullup resistors for the buttons you need to check for a low level in your setInputFlags method.

 if (inputState[i] == LOW)

Had to download Fritzing to make this drawing. When I saw the video of the guy that made this menu he said that the Arduino had 20k internal pullup resistors so by setting the switches high you were activating the resistors.

Arduino Menu

I was just going by what he was stating and went with that. I still can't seem to find a really simple menu that shows how to set two different values, one for int, the other for bool values. Most of the examples I have seen are just for int values, if you do see bool values you run into such a complicated setup that it will take forever to figure out. At least with a simple beginner setup you will know how to add on more screens if needed.

This is what I have.

JohnRandalls:
Had to download Fritzing to make this drawing. When I saw the video of the guy that made this menu he said that the Arduino had 20k internal pullup resistors so by setting the switches high you were activating the resistors.

I am not exactly sure how the digitalWrite method handles writes to an input pin. But anyways you should use INPUT_PULLUP for your pinMode command, as it is much clearer what is going on.

JohnRandalls:
I was just going by what he was stating and went with that. I still can't seem to find a really simple menu that shows how to set two different values, one for int, the other for bool values. Most of the examples I have seen are just for int values, if you do see bool values you run into such a complicated setup that it will take forever to figure out. At least with a simple beginner setup you will know how to add on more screens if needed.

Do you understand the code? If not, maybe you should try to implement the menu yourself. Creating such menu isn't that hard to do, you just need to get your head around one or two things.

Why did you connect AREF to digital pin 8?

I am not exactly sure, why the code I provided is not working, because for me it is working perfectly fine (same hardware setup). Could you litter it with Serial.println() statements to track the program flow?

EDIT: Okay, you didn't set your input pins in the setup function

void setup() {
  currentScreen = &numScreen;
  // put your setup code here, to run once:
  //g_lcd.begin(16, 2);
  g_lcd.init();
  g_lcd.backlight();
}

If I leave the pin initialization out, I get the same random behaviour (because the pins are picking up noise from the environment). Your setup method should look like this:

void setup() {
  currentScreen = &numScreen;
  // put your setup code here, to run once:
  //g_lcd.begin(16, 2);
  g_lcd.init();
  g_lcd.backlight();
  for(int i = 0; i < numOfInputs; i++)
  {
    pinMode(inputPins[i], INPUT_PULLUP);
  }
}

Do you understand the code? If not, maybe you should try to implement the menu yourself. Creating such menu isn't that hard to do, you just need to get your head around one or two things.

I only understand some of the code but I really like this idea of doing the menu myself, I still have to look at a lot more code to see how I can actually start from the beginning.

Why did you connect AREF to digital pin 8?

Where did you see that? I dont have anything connected to pins 8, only 2,3,4. WAIT!! I just saw it, that was a mistake when I was making the drawing, I just accidentally made another one as I was looking at the drawing again lol.

If I leave the pin initialization out, I get the same random behaviour (because the pins are picking up noise from the environment). Your setup method should look like this:

I am going to add that in but I am not going to bother you anymore. Normally I will only ask one question but not repeatedly. I am going to look at a lot more menu examples to see how I can actually start writing my own, I am sure I will figure it out eventually.

Thanks again for all your help.

JohnRandalls:
I am going to add that in but I am not going to bother you anymore. Normally I will only ask one question but not repeatedly. I am going to look at a lot more menu examples to see how I can actually start writing my own, I am sure I will figure it out eventually.

Thanks again for all your help.

You aren't bothering me at all - actually this was a pretty nice topic to respond to. I wish you the best of luck, keep learning new stuff. If you have additional questions, please feel free to ask them in this forum.