making a structured menu system, how to lay it up?

Hello everybody!

Im building a menu system as a part of my current project, but im not really getting anywhere..

So i would greatly appreciate some guidance, hints and suggestions. :slight_smile:

The menu system is meant to be hardware-agnostic, meaning i should be able to present the menu onto a 16x2char LCD as well as Serial Monitor.

I will be needing a couple of status-screens, a configuration-screen as well as a "tools"-screen.
The last two will need sub-menus, not only for each item, but also for data manipulation (modifying variables mostly).
I also would want a easily updatable menu structure, so i can add/remove menu screens without too much hazzle.

My current idea is somewhat like a 2D grid, left<>right flips between "root items", like "status1">"status2">"Config" and so on, while up<>down flips between "items" of each root item, if "rootItem"=="config" then "item"=="wheelsize">"battery">"timing" and so on.
While if "rootItem"=="tools", then item="shutdown">"hazard lights" and so on.

Most of my (non-root) items will need to have their own submenu for manipulating variables and so on, so it might be more correct to describe my current idea as "3d grid", up<>down being the item variables.

Im currently "hardcoding" each menu screen with a switch(currentScreen) case for each screen, witch seems to work allright, but it makes it somewhat hard for me to implement a neat navigation in the menu system.
Im not really grasping how i should step into or back out of a menu item.

But i think that for some reason a dynamic, single "screen" would be a better option, if i could just find a way to make the dynamic screen update its contents depending on user input.

What i DO have is a proper way of modifying values and a list of items i need to be able to view &/or control somehow, as well as about 7 tactile momentary buttons to work with.

There is an Ardiono kit built from an UNO and a shield carrying an LCD and 5 (6) buttons. Find that Project! If it was shown in Github or Arduino news, I'm not sure. There is a little menu system used in code.

There is an Adafruit display that has the LCD + 5 buttons which works very nicely.

It's close but that is not the device I thought about. Look for Arduino UNO + keyboard/LCD shield.

Im not looking for hardware, i have all the buttons and displays i can possibly need.. :slight_smile:
Also, i dont want to spend more money on yet another hardware revision for my custom PCBs, unless i find a non-repairable flaw in the current revision. :slight_smile:

(or reeeeeally want a specific feature that im missing and that simply cannot wait until the next "scheduled" hardware update..)

Im currently using a i2c 16x2char LCD for my project and a directly connected 16x2char LCD in the simulator. (libs: liquidcrystal/liquidcrystal_i2c)
The buttons im using are connected via i2c pin extenders (MCP23008) IRL and im using a bunch of buttons (currently 8x) via a analog pin as a simulator substitute.
So im very much capable of adding as many buttons as i need, but the target project hardware is designed for 8 buttons in total(handlebar-mounted 2x4btns).

Im planning on using one button to switch between default and modifier functions for the other buttons, witch means 14 controllable functions in total (or 28 if including longclick/shortclick).

So my idea is to use the buttons for normal operations (mainly lights/horn-control on a light vehicle), and if i press the "caps-lock", the buttons normally controlling turn indicators, horn and so on, instead manipulate my menu system.

My focus so far has been mainly how to effectively store strings in progmem, witch adds its own level of difficulty to the system.

So i figure its probably easier to get some inspiration and write something from the ground up instead of trying to make my current ideas work in "others frameworks" so to speak. :slight_smile:

What does your current code look like? It may be easier to 'hard code' it with switch statements to start with and then move on to making something more generic that's driven by data once you can see what the actual need is.

My "current code" is a mess, with alot of variations across multiple test-files....
..Thats the reason behind this thread, i need a proper concept to start from.

The display system im abandoning has numerous issues, its not intuitive, hard to modify and follow in the code and its not behaving the way i think i want..

But i´ll include my old display system, just to show my process.. :slight_smile:

Im using a bunch of macros to print to the LCD and they might be somewhat confusing so here´s a "translation":
writeLCD() requires line and column positions, clears the screen and then writes the new data.
printLCD() also requires cursor position, but doesnt clear the screen before writing new data.
addLCD() doesnt take cursor positions, instead it adds data at current cursor position.
"name"string() (addLCDstring()) reads the string from progmem instead of ram.

And since its not the code im working with, i figure that the required variables for the code below isnt strictly necessary so i´ll omit them to keep this reply length down.

The states[] array is a bool array to hold the status of each output "channel", like left turn indicator, brake light and so on.
any "CHAR_x" is a character stored in progmem, for instance "CHAR_DOT" = '.'
Any "STR_x" is a string stored in progmem, for instance "STR_CONFIG_MENU" = "config menu".

void display() {
  if (States[vectConfig]) {
    INDCHAR = CHAR_DOT;
    if (!lastConfigUpdate) {
      lastConfigUpdate = NOW;
    }
    if (hasChanged) {
      lastConfigUpdate = NOW;
    }
    if (checkElapsed(lastConfigUpdate,configTimeOut)) {
      States[vectConfig] = false;
      INDCHAR = CHAR_BLANK;
      lastConfigUpdate = 0;
    }
    if (screenSelected) {
      INDCHAR = CHAR_CFG;
    }
    if (valSelected) {
      INDCHAR = CHAR_HASH;
    }
  }
  else {
    INDCHAR = CHAR_BLANK;
    currentSel = 0;
    currentVal = 0;
    currentValPart = 0;
    screenSelected = false;
    valSelected = false;
    valPartSelected = false;
  }
  if (checkElapsed(lastDisplayUpdate,displayUpdateFreq)) {
    lastDisplayUpdate = NOW;
    switch (currentScreen) {
      case screenStart: {
        switch (defaultScreenRoller) {
          case 0: {
            displayVersionLCD();
            defaultScreenRoller++;
          } break;
          case 1: {
            checkBat();
            defaultScreenRoller++;
          } break;
          case 2: {
            printSpeed();
            defaultScreenRoller++;
          } break;
          case 3: {
            printSpeed();
            defaultScreenRoller++;
          } break;
          case 4: {
            printSpeed();
            defaultScreenRoller = 1;
          } break;
        }
      } break; 
      case screenBat: {
        writeLCDstring(0,0,STR_BATT_L);
        uint8_t _batV = batV / 10;
        uint8_t batVolt = _batV / 10;
        uint8_t batVdec = _batV % 10;
        addLCD(batVolt);
        addLCD(CHAR_DOT);
        addLCD(batVdec);
        addLCDstring(STR_BATT_V);

        uint8_t _batLevel = map(batV,batLimit,batFull,0,100);
        printLCD(0,1,_batLevel);
        addLCD(CHAR_PROCENT);
      } break;
      case screenSpdDist: 
      case screenSpdCad: {
        printSpeed();
      } break;
      case screenStates: {
        writeLCDstring(0,0,STR_STATES);
        for (uint8_t i = 0; i < numSysStates; i++) {
          if (States[i]) {
            printLCD(i,1,CHAR_HASH);
          }
          else {
            printLCD(i,1,CHAR_DOT);
          }
        }
      } break;
      case screenConfig: {
        if (!valSelected) {
          writeLCDstring(0,0,STR_CONFIG_MENU);
          switch(currentSel) {
            case conf_batFull: {
              printLCDstring(0,1,STR_CONFIG_BATFULL)
            } break;
            case conf_batLimit: {
              printLCDstring(0,1,STR_CONFIG_BATTLIM);
            } break;
            case conf_batWarn: {
              printLCDstring(0,1,STR_CONFIG_BATTWARN);
            } break;
            case conf_blinkVal: {
              printLCDstring(0,1,STR_CONFIG_BLINK);
            } break;
            case conf_sysTimeout: {
              printLCDstring(0,1,STR_CONFIG_TIMEOUT);
            } break;
            case conf_cirq: {
              printLCDstring(0,1,STR_CONFIG_CIRQ);
            } break;
            case conf_hazard: {
              printLCDstring(0,1,STR_CONFIG_HAZARD);
            } break;
            case conf_tripReset: {
              printLCDstring(0,1,STR_CONFIG_TRIPRESET);
            } break;
            case conf_shutdown: {
              printLCDstring(0,1,STR_SHUTDOWN);
            } break;
            case conf_info: {
              displayAuthorLCD();
            } break;
            default: {
              printLCDstring(0,1,STR_CONFIG_MENU);
            }
          }
        }
        else {
          switch(currentSel) {
            case conf_batFull: {
              writeLCDstring(0,0,STR_CONFIG_BATFULL)
            } break;
            case conf_batLimit: {
              writeLCDstring(0,0,STR_CONFIG_BATTLIM);
            } break;
            case conf_batWarn: {
              writeLCDstring(0,0,STR_CONFIG_BATTWARN);
            } break;
            case conf_blinkVal: {
              writeLCDstring(0,0,STR_CONFIG_BLINK);
            } break;
            case conf_sysTimeout: {
              writeLCDstring(0,0,STR_CONFIG_TIMEOUT);
            } break;
            case conf_cirq: {
              writeLCDstring(0,0,STR_CONFIG_CIRQ);
            } break;
            case conf_hazard: {
              writeLCDstring(0,0,STR_CONFIG_HAZARD);
            } break;
            case conf_tripReset: {
              writeLCDstring(0,0,STR_CONFIG_TRIPRESET);
            } break;
            case conf_shutdown: {
              writeLCDstring(0,0,STR_SHUTDOWN);
            } break;
            case 0:
            default: {
              writeLCDstring(0,0,STR_CONFIG_MENU);
            }
          }
          if (valSelected) {
            if (currentSel <= conf_tripReset -1) {
              printLCDstring(5,1,STR_CONF_VAL);
              uint8_t base_offset = 10;
              uint8_t offset = 0;
              if (currentVal > 999) {
                offset = base_offset + 1;
              }
              else if (currentVal > 99) {
                offset = base_offset + 2;
              }
              else if (currentVal > 9) {
                offset = base_offset + 3;
              }
              else {
                offset = base_offset + 4;
              }
              
              printLCD(offset,1,currentVal);
              offset = ((base_offset + 4) - currentValPart);
              printLCD(offset,0,CHAR_v);
            }
            else {
              printLCDstring(1,1,STR_CONFIG_CONFIRM)
              if (confirmSelection) {
                printLCDstring(10,1,STR_YES);
              }
              else {
                printLCDstring(10,1,STR_NO);
              }
            }
          }
        }
      } // end screenConfig
    } // end switch currentScreen
    printLCD(15,0,INDCHAR);
  }
}

xarvox:
Im not looking for hardware, i have all the buttons and displays i can possibly need.. :slight_smile:
Also, i dont want to spend more money on yet another hardware revision for my custom PCBs, unless i find a non-repairable flaw in the current revision. :slight_smile:

(or reeeeeally want a specific feature that im missing and that simply cannot wait until the next "scheduled" hardware update..)

Im currently using a i2c 16x2char LCD for my project and a directly connected 16x2char LCD in the simulator. (libs: liquidcrystal/liquidcrystal_i2c)
The buttons im using are connected via i2c pin extenders (MCP23008) IRL and im using a bunch of buttons (currently 8x) via a analog pin as a simulator substitute.
So im very much capable of adding as many buttons as i need, but the target project hardware is designed for 8 buttons in total(handlebar-mounted 2x4btns).

Im planning on using one button to switch between default and modifier functions for the other buttons, witch means 14 controllable functions in total (or 28 if including longclick/shortclick).

So my idea is to use the buttons for normal operations (mainly lights/horn-control on a light vehicle), and if i press the "caps-lock", the buttons normally controlling turn indicators, horn and so on, instead manipulate my menu system.

My focus so far has been mainly how to effectively store strings in progmem, witch adds its own level of difficulty to the system.

So i figure its probably easier to get some inspiration and write something from the ground up instead of trying to make my current ideas work in "others frameworks" so to speak. :slight_smile:

My idea was that You will pick up a little nice menue sketch that was available in that project, not buying hardware...

Ah of course.. :slight_smile:

Ive been checking out others menu systems but frankly they arent making much sense to me..

During my working days, now retired. menue work was a subject quite often. Sometimes a time out anf "fall back", one step or all, was deamnded. How to skip, escape etc...

Designeing a menue handler is a muti sided body.

Working for one big ball bearing company I found a very sofisticated menu handler. The heart of it was a matrix containing links to the codes to run, maxs, mins etc. You bet it took ssome time to find out hoe to use it.