Author Topic: Simple menu system with dynamic keypad functions (embedded alphanumeric display)  (Read 991 times)

0 Members and 1 Guest are viewing this topic.

Online ricko_uk

  • Frequent Contributor
  • **
  • Posts: 906
  • Country: ie
Hi,
how can I implement a multi screen system with a menu at the bottom where every screen the keys labels on the display change (according to the specific screen functionality)?
Something that can be quickly implemented and easily maintained.

I am looking for infos about both setup and functionality details as well as data structure.

Ideally (although this second point might be separate to the above implementation) to also have keypad autorepeat functionality that has faster autorepeat rate the longer the user keeps the menu keys pressed.

I googled it but couldn't find much. Maybe I was searching for the wrong terms. Any pointers to tutorials/resources are welcome. :)

Many thanks
« Last Edit: December 30, 2019, 04:07:11 pm by ricko_uk »
 

Online ricko_uk

  • Frequent Contributor
  • **
  • Posts: 906
  • Country: ie
Any suggestion anybody? :)

Any input would be greatly appreciated. Thank you inn advance :)
 

Offline dmills

  • Super Contributor
  • ***
  • Posts: 1950
That sort of thing is usually sufficiently application specific that general purpose code has limited use.

What I generally do is have a mess of static const structures.

One per menu entry that has the text string, a menu type enum, maybe some min, max values and such and union of a couple of different pointer types, one of which is a pointer to a child menu, and one is a pointer to an editor function, call this struct menu_entry. 

Then you have struct menu that looks something like this :

struct menu {
    char * title;
    struct menu_entry entries[8];
    struct menu * parent;
 };

By using named initialisers it is easy to build the menu structure :

struct menu menu_toplevel = {
    .title = "Top level",
    .entries = {
        {.label = "foo", .child = &menu_foo, .type = MENU_SUBMENU},
        {.label = "bar", .child = &menu_bar, .type = MENU_SUBMENU},
        {.label = "baz", .child = &menu_baz, .type = MENU_SUBMENU},
    },
    .parent = NULL,
};

Then your menu driver code simply maintains a pointer to whichever menu is currently being displayed.

You can of course extend this sort of thing to add function pointers for specific actions and per item parameters, one that is often useful is a offset into a configuration structure of some sort (the offsetof macro is your friend!), that way you can make the menu handle annoyances like toggle options and small integers without needing to specialise every stupid editor function.

But really, you know your architecture, menus systems are fairly trivial things.

One slight trap, data driven is the way to go, but remember to design in enough space to cope with the weird edge cases.
 

Online ricko_uk

  • Frequent Contributor
  • **
  • Posts: 906
  • Country: ie
Thank you Dmills, I like that approach! :)

Could you elaborate on your line "One slight trap, data driven is the way to go, but remember to design in enough space to cope with the weird edge cases.". Can you give me perhaps an example or two that happened to you so I know what to watch out for?

Thank you again :)
 

Offline obiwanjacobi

  • Frequent Contributor
  • **
  • Posts: 987
  • Country: nl
  • What's this yippee-yayoh pin you talk about!?
    • Marctronix Blog
If your RAM is limited check how you can put those literals in flash (like PROGMEM in Arduino).
They can really add up and eat a lot of storage.

[2c]
Arduino Template Library | Zalt Z80 Computer
Wrong code should not compile!
 

Offline dmills

  • Super Contributor
  • ***
  • Posts: 1950
Sorry, those structures should of course be static const.

Weird edge cases:
A screen brightness control that should update dynamically (as you turn the knob rather then only once you commit the change which is what the rest of the options on that product do).
A menu item used as a text field that is not editable but should change in response to another menu item being changed.
Hiding a secret hidden pin entry function in a status menu.

Basically all the weird that product management come out with.

I generally find it useful to have function pointers for something to be run on menu entry and something to be run on button press available.

 
The following users thanked this post: ricko_uk

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 3605
  • Country: fi
    • My home page and email address
I'm with dmills on this, definitely.

Here is some Arduino example code, to show how to start with this.  This is not from real-world code, but is intended to get you started.  It assumes an 8-bit AVR, with the menu structures in Flash (except for a copy of the current one in RAM).
Code: [Select]
/* Forward-declare the menu structure, since it is used in the button structure. */
struct menu;

/* Button structure. */
struct button {
  const char *text;                   /* Text shown for the button. */
  const menu *next;                   /* Menu entered when button pressed, or NULL. */
  void      (*call)(unsigned char);   /* Function called when button pressed, or NULL. The parameter is the rate (speed). */
};

/* Menu structure. */
struct menu {
  const char    *text;                /* Descriptive text for this menu */
  void         (*draw)(menu *);       /* Function to draw this menu (values); menu in RAM */
  const button   up;                  /* Button marked 'up'. Usually action. */
  const button   down;                /* Button marked 'down'. Usually action. */
  const button   left;                /* Button marked 'left'. Usually menu. */
  const button   right;               /* Button marked 'right'. Usually menu. */
  const button   fine;                /* Fifth button, usually menu. */
};

/* Voltage and current changing functions */
void  voltage_change(int by)   { /* TODO */ }
void  current_change(int by)   { /* TODO */ }

/* Functions called (.call members in menu structures) when buttons are pressed. */
void  output_enable(unsigned char steps)       { /* TODO */ }
void  output_disable(unsigned char steps)      { /* TODO */ }
void  voltage_up_fine(unsigned char steps)     { voltage_change(     (int)steps ); }
void  voltage_down_fine(unsigned char steps)   { voltage_change(    -(int)steps ); }
void  voltage_up_coarse(unsigned char steps)   { voltage_change( +10*(int)steps ); }
void  voltage_down_coarse(unsigned char steps) { voltage_change( -10*(int)steps ); }
void  current_up_fine(unsigned char steps)     { current_change(     (int)steps ); }
void  current_down_fine(unsigned char steps)   { current_change(    -(int)steps ); }
void  current_up_coarse(unsigned char steps)   { current_change( +10*(int)steps ); }
void  current_down_coarse(unsigned char steps) { current_change( -10*(int)steps ); }

/* Functions to draw the display */
void  draw_output(const menu *curr)  { /* Draw menu 'curr' and current output state. */ }
void  draw_voltage(const menu *curr) { /* Draw menu 'curr' and the current voltage. */ }
void  draw_current(const menu *curr) { /* Draw menu 'curr' and the current current. */ }

/* Forward-declare all menus, so that they can refer to each other in any order. */
extern const menu  menu_output_fine     PROGMEM;  /* Main menu, fine adjust */
extern const menu  menu_voltage_fine    PROGMEM;  /* Fine voltage control */
extern const menu  menu_current_fine    PROGMEM;  /* Fine current control */
extern const menu  menu_output_coarse   PROGMEM;  /* Main menu, coarse adjust */
extern const menu  menu_voltage_coarse  PROGMEM;  /* Coarse voltage control */
extern const menu  menu_current_coarse  PROGMEM;  /* Coarse current control */

/* Define the menu structures. */

#define  menu_default  menu_output_coarse

const menu  menu_output_fine  PROGMEM = {
  .text  = "Output status",
  .draw  = draw_output,
  .up    = { .text = "On",      .next = NULL,                .call = output_enable  },
  .down  = { .text = "Off",     .next = NULL,                .call = output_disable },
  .left  = { .text = "Current", .next = &menu_current_fine,  .call = NULL           },
  .right = { .text = "Voltage", .next = &menu_voltage_fine,  .call = NULL           },
  .fine  = { .text = "",        .next = NULL,                .call = NULL           },
/*.fine  = { .text = "Coarse",  .next = &menu_output_coarse, .call = NULL           }, */
};

const menu  menu_voltage_fine  PROGMEM = {
  .text  = "Fine Voltage Control",
  .draw  = draw_voltage,
  .up    = { .text = "Increment", .next = NULL,                 .call = voltage_up_fine   },
  .down  = { .text = "Decrement", .next = NULL,                 .call = voltage_down_fine },
  .left  = { .text = "Output",    .next = &menu_output_fine,    .call = NULL              },
  .right = { .text = "Current",   .next = &menu_current_fine,   .call = NULL              },
  .fine  = { .text = "Coarse",    .next = &menu_voltage_coarse, .call = NULL              },
};

const menu  menu_current_fine  PROGMEM = {
  .text = "Fine Current Control",
  .draw  = draw_current,
  .up    = { .text = "Increment", .next = NULL,                 .call = current_up_fine   },
  .down  = { .text = "Decrement", .next = NULL,                 .call = current_down_fine },
  .left  = { .text = "Voltage",   .next = &menu_voltage_fine,   .call = NULL              },
  .right = { .text = "Output",    .next = &menu_output_fine,    .call = NULL              },
  .fine  = { .text = "Coarse",    .next = &menu_current_coarse, .call = NULL              },
};

const menu  menu_output_coarse  PROGMEM = {
  .text  = "Output Status",
  .draw  = draw_output,
  .up    = { .text = "On",      .next = NULL,                 .call = output_enable  },
  .down  = { .text = "Off",     .next = NULL,                 .call = output_disable },
  .left  = { .text = "Current", .next = &menu_current_coarse, .call = NULL           },
  .right = { .text = "Voltage", .next = &menu_voltage_coarse, .call = NULL           },
  .fine  = { .text = "",        .next = NULL,                 .call = NULL           },
/*.fine  = { .text = "Fine",    .next = &menu_output_fine,    .call = NULL           }, */
};

const menu  menu_voltage_coarse  PROGMEM = {
  .text  = "Coarse Voltage Control",
  .draw  = draw_voltage,
  .up    = { .text = "Increment", .next = NULL,                 .call = voltage_up_coarse   },
  .down  = { .text = "Decrement", .next = NULL,                 .call = voltage_down_coarse },
  .left  = { .text = "Output",    .next = &menu_output_coarse,  .call = NULL                },
  .right = { .text = "Current",   .next = &menu_current_coarse, .call = NULL                },
  .fine  = { .text = "Fine",      .next = &menu_voltage_fine,   .call = NULL                },
};

const menu  menu_current_coarse  PROGMEM = {
  .text  = "Coarse Current Control",
  .draw  = draw_current,
  .up    = { .text = "Increment", .next = NULL,                 .call = current_up_coarse   },
  .down  = { .text = "Decrement", .next = NULL,                 .call = current_down_coarse },
  .left  = { .text = "Voltage",   .next = &menu_voltage_coarse, .call = NULL                },
  .right = { .text = "Output",    .next = &menu_output_coarse,  .call = NULL                },
  .fine  = { .text = "Fine",      .next = &menu_current_fine,   .call = NULL                },
};

static const unsigned char  rate[] PROGMEM = {
  0,                          /* Released state causes no action! */
  1,                          /* Initial button press */
  0, 0, 0, 0, 0, 0, 0, 0, 0,  /* No action for nine display update cycles */
  1,                          /* First autorepeat */
  0, 0, 0, 0, 0, 0, 0, 0,     /* No action for eight display update cycles */
  1,                          /* Second autorepeat */
  0, 0, 0, 0, 0, 0, 0,        /* No action for seven display update cycles */
  1,                          /* Third autorepeat */
  0, 0, 0, 0, 0, 0,           /* No action for six display update cycles */
  1,                          /* Fourth autorepeat */
  0, 0, 0, 0, 0,
  1,
  0, 0, 0, 0,
  1,
  0, 0, 0,
  1,
  0, 0,
  1,
  0,                          /* Last delay */
  1,                          /* Final, BUTTONCOUNTER_MAX'th entry, repeats every display update cycle. */
};
#define  BUTTONCOUNTER_MAX  ((sizeof rate / sizeof rate[0]) - 1)

/* Button bits as a bit mask. */
#define  BUTTONMASK_NONE   0
#define  BUTTONMASK_UP     (1<<0)
#define  BUTTONMASK_DOWN   (1<<1)
#define  BUTTONMASK_LEFT   (1<<2)
#define  BUTTONMASK_RIGHT  (1<<3)
#define  BUTTONMASK_FINE   (1<<4)

/* Button state counters, supporting autorepeat and multiple buttons simultaneously. */
static unsigned char  buttoncounter_up;
static unsigned char  buttoncounter_down;
static unsigned char  buttoncounter_left;
static unsigned char  buttoncounter_right;
static unsigned char  buttoncounter_fine;

/* Current menu displayed. */
static const menu *currmenuref;
static menu        currmenu = {0};

void setup() {
  /* Clear button counters. */
  buttoncounter_up = 0;
  buttoncounter_down = 0;
  buttoncounter_left = 0;
  buttoncounter_right = 0;
  buttoncounter_fine = 0;
  /* Default menu */
  currmenuref = &(menu_default);
  memcpy_P(&currmenu, currmenuref, sizeof (menu));
  /* set button pins as inputs etc. */
}

/* Display update interval in milliseconds. */
#define  DISPLAY_UPDATE_MS  50

void loop() {
  /* Copy current menu structure to RAM. */
  memcpy_P(&currmenu, currmenuref, sizeof (menu));

  /* Update display. */
  currmenu.draw(&currmenu);

  /* Inner loop, checking for button presses.
   * This loops repeats for DISPLAY_UPDATE_MS milliseconds.
   */
  const unsigned long  now = millis();
  unsigned int         button_mask = 0;
  while ((unsigned long)(millis() - now) < DISPLAY_UPDATE_MS) {
    /* if (up pressed)    button_mask |= BUTTONMASK_UP;    */
    /* if (down pressed)  button_mask |= BUTTONMASK_DOWN;  */
    /* if (left pressed)  button_mask |= BUTTONMASK_LEFT;  */
    /* if (right pressed) button_mask |= BUTTONMASK_RIGHT; */
    /* if (fine pressed)  button_mask |= BUTTONMASK_FINE;  */
   
    /* Possibly other work, or maybe a delay(1); */

  }

  /* Update up button counter. */
  if (button_mask & BUTTONMASK_UP) {
    if (buttoncounter_up < BUTTONCOUNTER_MAX)
      buttoncounter_up++;
  } else
    buttoncounter_up = 0;

  /* Update down button counter. */
  if (button_mask & BUTTONMASK_DOWN) {
    if (buttoncounter_down < BUTTONCOUNTER_MAX)
      buttoncounter_down++;
  } else
    buttoncounter_down = 0;

  /* Update left button counter. */
  if (button_mask & BUTTONMASK_LEFT) {
    if (buttoncounter_left < BUTTONCOUNTER_MAX)
      buttoncounter_left++;
  } else
    buttoncounter_left = 0;

  /* Update right button counter. */
  if (button_mask & BUTTONMASK_RIGHT) {
    if (buttoncounter_right < BUTTONCOUNTER_MAX)
      buttoncounter_right++;
  } else
    buttoncounter_right = 0;

  /* Update fine button counter. */
  if (button_mask & BUTTONMASK_FINE) {
    if (buttoncounter_fine < BUTTONCOUNTER_MAX)
      buttoncounter_fine++;
  } else
    buttoncounter_fine = 0;

  /* Calculate button action rates. */
  const unsigned char  rate_up    = pgm_read_byte_near(rate + buttoncounter_up);
  const unsigned char  rate_down  = pgm_read_byte_near(rate + buttoncounter_down);
  const unsigned char  rate_left  = pgm_read_byte_near(rate + buttoncounter_left);
  const unsigned char  rate_right = pgm_read_byte_near(rate + buttoncounter_right);
  const unsigned char  rate_fine  = pgm_read_byte_near(rate + buttoncounter_fine);

  /* Apply actions first. */
  if (rate_up    && currmenu.up.call)    currmenu.up.call(rate_up);
  if (rate_down  && currmenu.down.call)  currmenu.down.call(rate_down);
  if (rate_left  && currmenu.left.call)  currmenu.left.call(rate_left);
  if (rate_right && currmenu.right.call) currmenu.right.call(rate_right);
  if (rate_fine  && currmenu.fine.call)  currmenu.fine.call(rate_fine);

  /* Menu change.  Menu changes only occur when only one button is pressed,
   * and only at the initial press time. This should be at the end of the loop() function.
  */
  if (buttoncounter_up == 1 && button_mask == BUTTONMASK_UP && currmenu.up.next)
    currmenuref = currmenu.up.next;
  else
  if (buttoncounter_down == 1 && button_mask == BUTTONMASK_DOWN && currmenu.down.next)
    currmenuref = currmenu.down.next;
  else
  if (buttoncounter_left == 1 && button_mask == BUTTONMASK_LEFT && currmenu.left.next)
    currmenuref = currmenu.left.next;
  else
  if (buttoncounter_right == 1 && button_mask == BUTTONMASK_RIGHT && currmenu.right.next)
    currmenuref = currmenu.right.next;
  else
  if (buttoncounter_fine == 1 && button_mask == BUTTONMASK_FINE && currmenu.fine.next)
    currmenuref = currmenu.fine.next;
}

If we use white boxes for the display/menu state, and gray boxes for button presses, the above implements the following menu transitions:

The grayed out transitions at the center are by default commented out.  (The idea is to show that you can use the fifth fine button for any purpose in other menus. Essentially, the "fine"/"coarse" adjustment is just a menu state here!)

Each menu structure consists of descriptive text (describing the current menu), a function pointer to a function that should draw the current display (it gets a pointer to the RAM copy of the current menu), and a member each for the five buttons: up, down, left, right, and fine. each of the five buttons.

Typically, all draw functions should call a common helper function that draws the static parts of the display, like the button labels.  (Note that you need to use _P functions to access the text fields, since they are in Flash/ROM; however, the menu structure the draw functions get a pointer to as a parameter resides in RAM -- it is the currmenu copy of the current menu.)

Each button structure (up, down, left, right, and fine, in each menu structure) consists of three members: the text label to be shown for that button, the next menu structure to switch to if the button changes the menu (NULL otherwise), and the function to call if pressing the button should cause a function to be called (or NULL otherwise).  Thus, the same physical button can change to another menu in some menus, and cause a function to be called in others.  Or even both, although that would be confusing to users.

The display is updated every DISPLAY_UPDATE_MS milliseconds, here 50, or 1000/50 = 20 times per second.  (The loop() function defines exactly one display update interval.)

Accelerating autorepeat is implemented via the rate[] array.  The initial entry must be 0, so that un-pressed buttons do nothing.  When the button is kept depressed, its buttoncounter increments by one every display update, but never exceeds BUTTONCOUNTER_MAX, which is the index to the last entry in the rate[] array.  So, the rate[] array tells how many "action steps" are taken every display update, when a button is being kept depressed.  Note that the counter variables are 8-bit, so the maximum size of this array is 256 entries; that corresponds to 12.8 seconds at 20 display update cycles per second (DISPLAY_UPDATE_MS=50).  Current array has 56 entries, which means the maximum autorepeat rate is reached in 2.8 seconds.

On an Arduino Leonardo (ATmega32u4), this uses 363 bytes of RAM and 4182 bytes of Flash; +214 bytes of RAM and +720 bytes of Flash compared to the default empty sketch.  (On an Arduino Uno, +214 bytes of RAM and +742 bytes of Flash compared to the default empty sketch.)
We can reduce the RAM use by 34 bytes, if we omit the RAM copy of the menu structure, but then we'll need some static inline accessor _P functions to examine the pointers in the currently used menu, directly from Flash.  That looks a bit nastier, so I avoided that for now.

Menu transitions are excluded from autorepeating: only the initial button press, and only if it is the only one being pressed at the same time, causes a menu transition.  If autorepeat is desired for menu transitions also, then the end of the code needs to be changed into
Code: [Select]
  /* Menu change.  Only the first matching transition is taken.
   *  This should be at the end of the loop() function.
  */
  if (rate.up    && currmenu.up.next)
    currmenuref = currmenu.up.next;
  else
  if (rate.down  && currmenu.down.next)
    currmenuref = currmenu.down.next;
  else
  if (rate.left  && currmenu.left.next)
    currmenuref = currmenu.left.next;
  else
  if (rate.right && currmenu.right.next)
    currmenuref = currmenu.right.next;
  else
  if (rate.fine  && currmenu.fine.next)
    currmenuref = currmenu.fine.next;
}


It is also possible to implement "shift" or "alternate" keys, by extending the menu structure suitably.  (Then, the menu graph can have edges to an alternate menu that is taken when the "shift" key is pressed, and back when the "shift" key is released.)

I recommend mocking up the interface first, on paper or on computer (say, HTML pages, one page per menu/display state).  Disable the autorepeat ( rate[] = { 0, 1, 0 };) for initial implementation; add it after the menu navigation works in an intuitive and non-aggravating manner.
« Last Edit: January 06, 2020, 08:11:38 pm by Nominal Animal »
 
The following users thanked this post: ricko_uk

Online ricko_uk

  • Frequent Contributor
  • **
  • Posts: 906
  • Country: ie
Thank you all for all the very detailed replies, very much appreciated!! :) :)
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf