Anniversary gift for GF. Please Help in Implementing a MultiLevel Menu using 16x2 LCD and 4 buttons (Up,Down,Enter,Back)

Hello Everyone,
I am having difficulty in implementing a multilevel menu.
I am following this Tutorial -

All the connections work fine. Everything is good.

However, the issue i am facing is that i wish to have some different outputs.
This is the way i want the program to work -

Arduino Powers up,
Waits for 500 ms,
Displays " Press Button 1 ",
Button1(Enter) is pressed and then a menu opens up. (I wish to have 10 items in this menu),
Button2 and Button3 are for moving up and down the menu.
Button4 is for going a step back, ie, if the user is in a submenu, it opens up the menu. If the user is in menu, it takes back to the homepage that said "Press Button 1".

Now, what i wish to implement is, each item in submenu, when selected using Enter Button, displays a text (I will be writing small messages for her in each submenu, If this text can me animated in some form that would be great, like, 2 lines of message appear and the after 300ms, the continuation of that message appears.)
And once the message is complete, the submenu opens up again automatically.
and the user can again interact using buttons, whether to select the same option again or go up or down or go back in menu and select another item there.

If the code given on the site is suffeicient, then please tell me what things i need to tweak in order to get the implementation i want.
I know this is a bit lazy of me to not do this on my own but trust me, i have tried many times, and due to my limited knowledge in coding i cant do this by myself. Please Help. Anniversary on 20th Sept.

If i have left some necessary information or should clarify something more, please let me know.
Also, Since i am new to the forum, Please forgive any wrongdoings from my part. Let me know what i did wrong and i will keep it in mind the next time i seek help.

Hello
Post your sketch with the project coding did until now.

I havent made any significant change to the code that was on the blog i am following.
I only tried messing with the lcd.print and adding delays in between lines but since i couldnt get anything substantial out of those changes so i didnt save them.
As of now, the arduino board has this particular program loaded in it -

#include "LiquidCrystal.h"

LiquidCrystal lcd(7, 6, 5, 4, 3, 2);

#define button_B A0
#define button_D A1
#define button_U A2
#define button_E A3

#define ONBOARD_LED 13

#define DEFAULT_DELAY 300

char buttonPressed = '0';

byte menuLevel = 0; // Level 0: no menu display, display anything you like
// Level 1: display main menu
// Level 2: display sub menu

byte menu = 1;
byte sub = 1;

unsigned long relay_val_1 = 0;
unsigned long relay_val_2 = 0;
unsigned long relay_val_3 = 0;

bool LED_STATE = false;

bool currState_B = HIGH;
bool currState_D = HIGH;
bool currState_U = HIGH;
bool currState_E = HIGH;

bool prevState_B = HIGH;
bool prevState_D = HIGH;
bool prevState_U = HIGH;
bool prevState_E = HIGH;

unsigned long prevTime_B = 0;
unsigned long prevTime_D = 0;
unsigned long prevTime_U = 0;
unsigned long prevTime_E = 0;

unsigned long waitTime_B = 50;
unsigned long waitTime_D = 50;
unsigned long waitTime_U = 50;
unsigned long waitTime_E = 50;

void setup() {
lcd.begin(16,2);

Serial.begin(9600);

pinMode(button_B, INPUT_PULLUP);
pinMode(button_D, INPUT_PULLUP);
pinMode(button_U, INPUT_PULLUP);
pinMode(button_E, INPUT_PULLUP);

pinMode(A4,INPUT);

pinMode(ONBOARD_LED, OUTPUT);
digitalWrite(ONBOARD_LED, LED_STATE);

showHomeScreen();
}

void loop() {

checkButton();

// You can do other things here below

}

void checkButton() {

// Button Debouncing
bool currRead_B = digitalRead(button_B);
bool currRead_D = digitalRead(button_D);
bool currRead_U = digitalRead(button_U);
bool currRead_E = digitalRead(button_E);

if (currRead_B != prevState_B) {
prevTime_B = millis();
}
if (currRead_D != prevState_D) {
prevTime_D = millis();
}
if (currRead_U != prevState_U) {
prevTime_U = millis();
}
if (currRead_E != prevState_E) {
prevTime_E = millis();
}

if ((millis() - prevTime_B) > waitTime_B) {
if (currRead_B != currState_B) {
currState_B = currRead_B;
if (currState_B == LOW) {
buttonPressed = 'B';
}
}
} else buttonPressed = '0';
if ((millis() - prevTime_D) > waitTime_D) {
if (currRead_D != currState_D) {
currState_D = currRead_D;
if (currState_D == LOW) {
buttonPressed = 'D';
}
}
} else buttonPressed = '0';
if ((millis() - prevTime_U) > waitTime_U) {
if (currRead_U != currState_U) {
currState_U = currRead_U;
if (currState_U == LOW) {
buttonPressed = 'U';
} else {
//buttonPressed = '0';
}
}
} else buttonPressed = '0';
if ((millis() - prevTime_E) > waitTime_E) {
if (currRead_E != currState_E) {
currState_E = currRead_E;
if (currState_E == LOW) {
buttonPressed = 'E';
}
}
} else buttonPressed = '0';

prevState_B = currRead_B;
prevState_D = currRead_D;
prevState_U = currRead_U;
prevState_E = currRead_E;

processButton(buttonPressed);

}

void processButton(char buttonPressed) {

switch(menuLevel) {
case 0: // Level 0
switch ( buttonPressed ) {
case 'E': // Enter
menuLevel = 1;
menu = 1;
updateMenu();
delay(DEFAULT_DELAY);
break;
case 'U': // Up
break;
case 'D': // Down
break;
case 'B': // Back
break;
default:
break;
}
break;
case 1: // Level 1, main menu
switch ( buttonPressed ) {
case 'E': // Enter
updateSub();
menuLevel = 2; // go to sub menu
updateSub();
delay(DEFAULT_DELAY);
break;
case 'U': // Up
menu++;
updateMenu();
delay(DEFAULT_DELAY);
break;
case 'D': // Down
menu--;
updateMenu();
delay(DEFAULT_DELAY);
break;
case 'B': // Back
menuLevel = 0; // hide menu, go back to level 0
showHomeScreen();
delay(DEFAULT_DELAY);
break;
default:
break;
}
break;
case 2: // Level 2, sub menu
switch ( buttonPressed ) {
case 'E':
menuLevel = 1;
updateMenu();
delay(DEFAULT_DELAY);
break;
case 'U': // U
if (menu == 1) {
if (relay_val_1 < 3600000) { // 1 hour max
relay_val_1 = relay_val_1 + 1;
} else {
relay_val_1 = 3600000;
}
} else if (menu == 2) {
if (relay_val_2 < 3600000) { // 1 hour max
relay_val_2 = relay_val_2 + 1;
} else {
relay_val_2 = 3600000;
}
} else if (menu == 3) {
if (relay_val_3 < 3600000) { // 1 hour max
relay_val_3 = relay_val_3 + 1;
} else {
relay_val_3 = 3600000;
}
}
updateSub();
delay(DEFAULT_DELAY);
break;
case 'D': // D
if (menu == 1) {
if (relay_val_1 == 0) {
relay_val_1 = 0;
} else {
relay_val_1 = relay_val_1 - 1;
}
} else if (menu == 2) {
if (relay_val_2 == 0) {
relay_val_2 = 0;
} else {
relay_val_2 = relay_val_2 - 1;
}
} else if (menu == 3) {
if (relay_val_3 == 0) {
relay_val_3 = 0;
} else {
relay_val_3 = relay_val_3 - 1;
}
}
updateSub();
delay(DEFAULT_DELAY);
break;
case 'B': // L
menuLevel = 1; // go back to main menu
updateMenu();
delay(DEFAULT_DELAY);
break;
default:
break;
}
break;
case 3: // Level 3, sub menu of sub menu

  break;
default:
  break;

}

}

void updateMenu() {

switch (menu) {
case 0:
menu = 1;
break;
case 1:
lcd.clear();
lcd.print(">Relay 1: ");
lcd.print(relay_val_1);
lcd.setCursor(0, 1);
lcd.print(" Relay 2 ");
break;
case 2:
lcd.clear();
lcd.print(" Relay 1 ");
lcd.setCursor(0, 1);
lcd.print(">Relay 2: ");
lcd.print(relay_val_2);
break;
case 3:
lcd.clear();
lcd.print(">Relay 3: ");
lcd.print(relay_val_3);
lcd.setCursor(0, 1);
lcd.print(" ");
break;
case 4:
menu = 3;
break;
}

}

void updateSub() {
switch (menu) {
case 0:

  break;
case 1:
  lcd.clear();
  lcd.print(" Relay 1:");
  lcd.setCursor(0, 1);
  lcd.print("  Val 1 = ");
  lcd.print(relay_val_1);
  break;
case 2:
  lcd.clear();
  lcd.print(" Relay 2:");
  lcd.setCursor(0, 1);
  lcd.print("  Val 2 = ");
  lcd.print(relay_val_2);
  break;
case 3:
  lcd.clear();
  lcd.print(" Relay 3:");
  lcd.setCursor(0, 1);
  lcd.print("  Val 3 = ");
  lcd.print(relay_val_3);
  break;
case 4:
  menu = 3;
  break;

}
}

void showHomeScreen() {
lcd.clear();
lcd.println(" TechToTinker ");
lcd.setCursor(0,1);
lcd.println(" - R for menu ");
}

Also, I am not incorporating any leds in it and no relays so i am thinking of removing the ONBOARDLED and relay_val_1/2/3 chunks... but i am afraid that i miss remove an important code so seeking help

Here is an example sketch I just wrote to display a menu tree. Instead of writing code for each item of each menu it uses a table of menu items to show the various menus and allow selection.

#include "LiquidCrystal.h"

LiquidCrystal lcd(7, 6, 5, 4, 3, 2);

const byte  button_B = A0;
const byte  button_D = A1;
const byte  button_U = A2;
const byte  button_E = A3;

bool prevState_B = false;
bool prevState_D = false;
bool prevState_U = false;
bool prevState_E = false;

struct MenuItem
{
  unsigned menuID;
  unsigned subMenuID; // Menu to go to for the ENTER button
  const char * contents;
} MenuItems[]
{
  {0, 1, "Press Button 1"},
  {1, 10, "Menu 1"},
  {1, 11, "Menu 2"},
  {1, 12, "Menu 3"},
  {1, 13, "Menu 4"},
  {2, 20, "Menu 2,A"},
  {2, 21, "Menu 2,B"},
  {2, 22, "Menu 2,C"},
  {2, 23, "Menu 2,D"},
  {20, 0, "A"},
  {21, 0, "B"},
  {22, 0, "C"},
  {23, 0, "D"},
};

unsigned MenuItemCount = sizeof MenuItems / sizeof MenuItems[0];

unsigned PreviousID[10];
unsigned PreviousItem[10];
unsigned PreviousIndex = 0;

unsigned CurrentID = 0;
unsigned CurrentItem = 0;
unsigned CurrentItemMax = 0;
unsigned CurrentItemSub;

void ShowMenu()
{
  lcd.clear();
  CurrentItemMax = 0;
  for (unsigned i = 0; i < MenuItemCount; i++)
  {
    if (MenuItems[i].menuID == CurrentID)
    {
      if (CurrentItemMax == CurrentItem)  // At top line item
      {
        CurrentItemSub = MenuItems[i].subMenuID;; // Go here on ENTER

        lcd.setCursor(0, 0);
        if (CurrentItemSub == 0)
          lcd.print("X ");  // ENTER does nothing
        else
          lcd.print("> ");  // ENTER goes to submenu
        lcd.print(MenuItems[i].contents);
      }

      if (CurrentItemMax == CurrentItem + 1)  // At second line item
      {
        lcd.setCursor(2, 1);
        lcd.print(MenuItems[i].contents);
      }

      CurrentItemMax++;
    }
  }
}

void MenuEnter()
{
  if (CurrentItemSub == 0)
    return; // No submenu
  PreviousID[PreviousIndex] = CurrentID;
  PreviousItem[PreviousIndex] = CurrentItem;
  PreviousIndex++;

  CurrentID = CurrentItemSub;
  CurrentItem = 0;
  ShowMenu();
}

void MenuBack()
{
  if (PreviousIndex == 0)
    return; // There was no previous menu item

  PreviousIndex--;
  CurrentID   = PreviousID[PreviousIndex];
  CurrentItem = PreviousItem[PreviousIndex];
  ShowMenu();
}

void MenuDown()
{
  if (CurrentItem >= CurrentItemMax)
    return; // Nothing DOWN from here
  CurrentItem++;
  ShowMenu();
}

void MenuUp()
{
  if (CurrentItem == 0)
    return; // Nothing UP from here

  CurrentItem--;
  ShowMenu();
}


void setup()
{
  lcd.begin(16, 2);

  Serial.begin(9600);

  pinMode(button_B, INPUT_PULLUP);
  pinMode(button_D, INPUT_PULLUP);
  pinMode(button_U, INPUT_PULLUP);
  pinMode(button_E, INPUT_PULLUP);

  ShowMenu();
}

void loop()
{

  checkButton();

  // You can do other things here below

}

void checkButton()
{

  // Button Debouncing
  static unsigned long DebounceTimer = 0;
  bool isPressed;

  // BACK button
  isPressed = digitalRead(button_B) == LOW;
  if (isPressed != prevState_B &&
      millis() - DebounceTimer > 20)
  {
    prevState_B = isPressed;
    DebounceTimer = millis();
    if (isPressed)
    {
      MenuBack();
    }
  }

  // ENTER button
  isPressed = digitalRead(button_E) == LOW;
  if (isPressed != prevState_E &&
      millis() - DebounceTimer > 20)
  {
    prevState_E = isPressed;
    DebounceTimer = millis();
    if (isPressed)
    {
      MenuEnter();
    }
  }

    // DOWN button
  isPressed = digitalRead(button_D) == LOW;
  if (isPressed != prevState_D &&
      millis() - DebounceTimer > 20)
  {
    prevState_D = isPressed;
    DebounceTimer = millis();
    if (isPressed)
    {
      MenuDown();
    }
  }

  // UP button
  isPressed = digitalRead(button_U) == LOW;
  if (isPressed != prevState_U &&
      millis() - DebounceTimer > 20)
  {
    prevState_U = isPressed;
    DebounceTimer = millis();
    if (isPressed)
    {
      MenuBack();
    }
  }
}
1 Like

Thanks a lot Sir, I'll give it a try.