Settings Menu for Arduino-based Devices

Settings Menu: the Origin

I was building a level meter for a water cistern based on Arduino and ultrasonic distance meters. Since I had no exact data about the cistern dimensions the meter had to support on-the-fly changing of parameters. I decided to use a display with a settings menu.

Many devices need at least some sort of settings, for example:

Unless you fix your settings for good at compile time you’ll need a way to change them later. One usually stores them to EEPROM and changes with a 16×2 display  that is common, simple and cheap.

Loading and saving settings and writing to a display is very simple on Arduino. However, when there are many values to set, wrestling with user interface quickly becomes incredibly messy. That’s why I decided to end it once and for all and write a structured menu interface for users and, more importantly, programmers.

Hardware

At least a two-line display and four push-buttons are required. 

User Interface

The display has two (or more) states, normal operation and menu. During normal operation you’re free to use the display freely. Once a button is pressed the device enters the menu mode where the display is controlled by this library.

Two of the buttons enter the menu mode and switch between menu items (settings). The other two change the value under the cursor.

Getting Rid of the Mess

Since Arduino supports C++ with everything that comes with it I decided to use some of the perks C++ provides. A menu item is an abstract base class that provides functionality common to all menu items – loading, saving, changing value. Another class handles the structure and interfaces with other parts of the sketch. Adding a menu item is as simple as adding a single line to a list of your menu items. Adding a new data type is just a matter of providing a few functions for loading, saving and changing value.

Usage

There’s an example sketch provided, here is a quick walkthrough.

Libraries needed: LiquidCrystal, Button from the HAL libraries, and this one. The Button library handles common nuisances such as pull-up/down resistors, debouncing, modes, states, …

#include <LiquidCrystal.h> // already in standard Arduino package
#include <Button.h> // http://playground.arduino.cc/Code/Button
#include <DLMenu.h> // https://github.com/damogranlabs/dl-menu

Hardware: in my case, I used analog inputs for display because I needed digital for other purposes.

// 4 push buttons; no additional resistors are needed on pins that have internal pull-ups
#define PB_LEFT   11
#define PB_RIGHT  12
#define PB_UP     10
#define PB_DOWN   13

// LCD connections
#define PD_RS     A0
#define PD_E      A1
#define PD_DB4    A2
#define PD_DB5    A3
#define PD_DB6    A4
#define PD_DB7    A5
#define PD_LIGHT  4

#defined values: how many digits should a int and float menu item display.
For consistency, I prefer to use #define. The same goes with addresses;
you have to manually assign EEPROM addresses of each setting. Be careful not to set overlapping addresses;
float – 4 bytes, int – 4 bytes, choices – 1 byte.

You also have to provide a default value. That is used when nothing has been saved to EEPROM yet.

The choice item uses only 1 byte of EEPROM memory because it stores the chosen index. The actual text needs to be provided separately. If you have a lot of text you may consider putting it into PROGMEM instead of RAM. See the documentation of PROGMEM keyword.

// default number of digits for numbers
#define DLM_FLOAT_DIGITS  5
#define DLM_INT_DIGITS    3

// addresses of each setting and its default value: integer setting
#define DLM_ADDR_TESTINT        0 // address of setting 1
#define DLM_DEFAULT_TESTINT     42 // default value of this setting

// float value setting;
#define DLM_ADDR_TESTFLOAT      4 
#define DLM_DEFAULT_TESTFLOAT   1.01325e+5

// choice menu setting;
#define DLM_ADDR_TESTCHOICE     8
#define DLM_DEFAULT_TESTCHOICE  1 // the index of the default choice in choices array (0-based)

// these values can be stored in flash memory instead of RAM; see PROGMEM keyword in Arduino documentation
const char *testChoices[] = {"Male", "Female", "Yes, please", NULL}; // ACHTUNG, the last entry must be NULL

Before initializing the actual Menu object you have to initialize the items first. Then you pass only the pointers to those initialized objects to the main Menu object. Also the list should end with a NULL pointer.

LiquidCrystal lcd(PD_RS, PD_E, PD_DB4, PD_DB5, PD_DB6, PD_DB7);

Button bLeft(PB_LEFT, PULLUP);
Button bRight(PB_RIGHT, PULLUP);
Button bUp(PB_UP, PULLUP);
Button bDown(PB_DOWN, PULLUP);

// menu items (it would be wise to store a lot of texts in PROGMEM)
DLIntMenuItem *miTestInt = new DLIntMenuItem( // integer setting
  &lcd,                 // your LiquidCrystal object
  "The Number",         // the title of this setting (first line)
  DLM_ADDR_TESTINT,     // EEPROM address of the setting
  DLM_INT_DIGITS,       // menu-item-specific setting (number of digits in this case)
  DLM_DEFAULT_TESTINT); // item-specific default value
  
DLFloatMenuItem *miTestFloat = new DLFloatMenuItem( // float setting
  &lcd,
  "Pressure [Pa]",
  DLM_ADDR_TESTFLOAT,
  DLM_FLOAT_DIGITS, 
  DLM_DEFAULT_TESTFLOAT);

DLChoiceMenuItem *miTestChoice = new DLChoiceMenuItem( // choice setting
  &lcd,
  "Sex", DLM_ADDR_TESTCHOICE,
  testChoices,
  DLM_DEFAULT_TESTCHOICE);


DLMenuItem *menuItems[] = {
  miTestInt, miTestFloat, miTestChoice, NULL // ACHTUNG, the last entry must be NULL
};

// create the menu
DLMenu menu(&lcd, &bLeft, &bRight, &bUp, &bDown, menuItems);

The menu is initialized now. You can initialize your other stuff and carry on with your app normally. There’s just one more thing – you should check user input in the loop() function. If menu.check() returns a non-falsy value that means the menu has taken over and you mustn’t do anything with the display or buttons. Otherwise, the display is yours.

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

  pinMode(PD_LIGHT, OUTPUT);
  digitalWrite(PD_LIGHT, HIGH);
}

void loop(){
  if(menu.check()){
    // showing menu, do nothing
  }
  else{
    // the menu is not activated; do whatever your gizmo does in 'normal' mode
    lcd.setCursor(0, 0);
    lcd.print("On for ");
    lcd.print(millis()/1000);
    lcd.print(" seconds");

    // be careful when using delays or blocking functions that take long to complete;
    // the menu.check() won't be able to check for button presses in the meanwhile;
    // short button presses will not be detected and if functions take too long,
    // buttons will be disabled altogether;
    delay(20);
  }  
}