Simple menu system with dynamic keypad functions (embedded alphanumeric display) - Page 1
Forum section: electronics / general

author: ricko_uk
author_rank: Super Contributor
timestamp: December 20, 2019, 03:08:23 am
content: 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

author: ricko_uk
author_rank: Super Contributor
timestamp: January 02, 2020, 04:26:32 am
content: Any suggestion anybody?Any input would be greatly appreciated. Thank you inn advance

author: dmills
author_rank: Super Contributor
timestamp: January 04, 2020, 04:06:10 pm
content: 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;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.

author: ricko_uk
author_rank: Super Contributor
timestamp: January 04, 2020, 06:58:34 pm
content: 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

author: obiwanjacobi
author_rank: Super Contributor
timestamp: January 06, 2020, 08:27:12 am
content: 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.

author: dmills
author_rank: Super Contributor
timestamp: January 06, 2020, 10:56:12 am
content: 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.

author: Nominal Animal
author_rank: Super Contributor
timestamp: January 06, 2020, 07:58:46 pm
content: 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:/* 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_coarseconst 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) - 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 50void 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++;} elsebuttoncounter_up = 0;/* Update down button counter. */if (button_mask & BUTTONMASK_DOWN) {if (buttoncounter_down < BUTTONCOUNTER_MAX)buttoncounter_down++;} elsebuttoncounter_down = 0;/* Update left button counter. */if (button_mask & BUTTONMASK_LEFT) {if (buttoncounter_left < BUTTONCOUNTER_MAX)buttoncounter_left++;} elsebuttoncounter_left = 0;/* Update right button counter. */if (button_mask & BUTTONMASK_RIGHT) {if (buttoncounter_right < BUTTONCOUNTER_MAX)buttoncounter_right++;} elsebuttoncounter_right = 0;/* Update fine button counter. */if (button_mask & BUTTONMASK_FINE) {if (buttoncounter_fine < BUTTONCOUNTER_MAX)buttoncounter_fine++;} elsebuttoncounter_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;elseif (buttoncounter_down == 1 && button_mask == BUTTONMASK_DOWN && currmenu.down.next)currmenuref = currmenu.down.next;elseif (buttoncounter_left == 1 && button_mask == BUTTONMASK_LEFT && currmenu.left.next)currmenuref = currmenu.left.next;elseif (buttoncounter_right == 1 && button_mask == BUTTONMASK_RIGHT && currmenu.right.next)currmenuref = currmenu.right.next;elseif (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 fifthfinebutton for any purpose in other menus. Essentially, the "fine"/"coarse" adjustment is just a menu state here!)Eachmenustructure consists of descriptive text (describing the current menu), a function pointer to a function that shoulddrawthe 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, andfine. 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_Pfunctions 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 thecurrmenucopy of the current menu.)Eachbuttonstructure (up,down,left,right, andfine, in each menu structure) consists of three members: thetextlabel to be shown for that button, thenextmenu structure to switch to if the button changes the menu (NULLotherwise), and the function tocallif pressing the button should cause a function to be called (orNULLotherwise). 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 everyDISPLAY_UPDATE_MSmilliseconds, here 50, or 1000/50 = 20 times per second. (Theloop()function defines exactly one display update interval.)Accelerating autorepeat is implemented via therate[]array. The initial entry must be 0, so that un-pressed buttons do nothing. When the button is kept depressed, itsbuttoncounterincrements by one every display update, but never exceedsBUTTONCOUNTER_MAX, which is the index to the last entry in therate[]array. So, therate[]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 intoCode:/* 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;elseif (rate.down && currmenu.down.next)currmenuref = currmenu.down.next;elseif (rate.left && currmenu.left.next)currmenuref = currmenu.left.next;elseif (rate.right && currmenu.right.next)currmenuref = currmenu.right.next;elseif (rate.fine && currmenu.fine.next)currmenuref = currmenu.fine.next;}It is also possible to implement "shift" or "alternate" keys, by extending themenustructure 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.

author: ricko_uk
author_rank: Super Contributor
timestamp: January 06, 2020, 11:35:10 pm
content: Thank you all for all the very detailed replies, very much appreciated!!