Sunday, November 28, 2010

Implementing Menu's on the Arduino


I'm currently developing an irrigation controller (i.e. - a fairly generic controller with switched outputs -- hooked up to an irrigation system). This controller will run somewhat standalone in an IP67 rated box for some degree of splash/rain protection.

The controller will have an integrated keypad and LCD display, albeit I'm also planning on a WIFI interface so I can control/program the system remotely.

As with most systems with a display it will need a menu to provide access to the supported functionality for controlling and programming the system. Often this is a little painful, however I've stumbled across a library that takes a lot of the pain out of developing the user interface.

The menu system is called 'MenuBackend' and the details can be found here.

Using the menu is simple and well outlined in the code samples provided.

A simple example for my irrigation controller:

Mode
Manual
Automatic
Programing
Program 1
Set Start Time
Set Interval
Set Duration
Set Stations
Set Rain Override
Review
Enable/Disable
Enable
Disable
Configuration
Sensors
Temperature/Humidity
Tanking Gauges
Moisture Sensors
Other
Relays
Review Stations
Add Stations
IIC
Other
Set Clock

And you will get the idea. Using MenuBackend this becomes (showing a subset):

MenuBackend menu = MenuBackend(menuUseEvent,menuChangeEvent);
MenuItem miMode = MenuItem("Mode", 'A');
MenuItem miManual = MenuItem("Manual");
MenuItem miAutomatic = MenuItem("Automatic");
MenuItem miProgram = MenuItem("Program");
MenuItem miProgram1 = MenuItem("Program 1");
MenuItem miProgram1StartTime = MenuItem("Start Time");
MenuItem miProgram1Interval = MenuItem("Interval");
MenuItem miProgram1Duration = MenuItem("Duration");
MenuItem miProgram1Stations = MenuItem("Stations");
MenuItem miProgram1RainControl = MenuItem("Rain Control");
MenuItem miProgram1Review = MenuItem("Review");
MenuItem miProgram1Enable = MenuItem("Enable");
MenuItem miProgram1Disable = MenuItem("Disable");
MenuItem miConfigure = MenuItem("Configure");
MenuItem miSetTime = MenuItem("Set Clock");

which creates the required objects, then the initialisation creates the menu structure. Note the references that allow the menu to 'wrap around' from the bottom of the lists back to the top of the lists.

void menuSetup() {
logSerial("Setting up the menu...");

menu.getRoot().add(miMode);
miMode.addBefore(miConfigure); // loop to bottom item
miMode.addAfter(miProgram);
miMode.addRight(miManual);
miManual.addAfter(miAutomatic);

miProgram.addAfter(miConfigure);
miProgram.addRight(miProgram1);
miProgram1.addRight(miProgram1StartTime);
miProgram1StartTime.addAfter(miProgram1Interval);
miProgram1Interval.addAfter(miProgram1Duration);
miProgram1Duration.addAfter(miProgram1Stations);
miProgram1Stations.addAfter(miProgram1RainControl);
miProgram1RainControl.addAfter(miProgram1Review);
miProgram1Review.addAfter(miProgram1Enable);
miProgram1Enable.addAfter(miProgram1Disable);
miProgram1Disable.addAfter(miProgram1StartTime);
miProgram1StartTime.addBefore(miProgram1Disable);

miConfigure.addAfter(miMode); // loop to top item in this menu
miConfigure.addRight(miSetTime);

}

I'm using 4 buttons on the keypad as 'arrow' keys to navigate through the menu and an 'enter' button to execute the desired function. Navigation is as simple as reading the input from your desired device(s) and letting the menu system know what to do.

note - my keypad sends sequences of 3 chars to indicate a key press or release, the key pressed, and an end of sequence character. Your keypad possibly won't...
void checkKeyEvent() {
int type; // key press type -- press or release (capacitative keypad)
int number; // the key number (4*4)
int eos; // end of sequence flag

if(keypad.available()) {
while(eos != 0x0D) {
type=keypad.read();
while(!keypad.available()) continue;
number=keypad.read();
while(!keypad.available()) continue;
eos=keypad.read();
}

if(type == 'P') { // key press event
switch(number) {
case '2':
menu.moveUp();
break;
case '8':
menu.moveDown();
break;
case '4':
menu.moveLeft();
break;
case '6':
menu.moveRight();
break;
case 'A':
menu.use('A');
break;
case 'D':
menu.use();
break;
default:
char log[100];
sprintf(log, "Unused Keypress Detected %c", number);
logSerial(log);
}
} else { // key release event
}
}
}
And navigating through the menu gives me:
01/12/10 20:59:58 Keypad Initialised...
01/12/10 20:59:58 Menu Change: Mode --> Mode (down)
01/12/10 20:59:58 Menu Change: Program --> Program (right)
01/12/10 21:00:03 Menu Change: Program 1 --> Program 1 (right)
01/12/10 21:00:03 Menu Change: Start Time --> Start Time (right)
01/12/10 21:00:03 Menu Change: Program 1 --> Program 1 (left)
01/12/10 21:00:03 Menu Change: Program --> Program (left)
01/12/10 21:00:03 Menu Change: Configure --> Configure (down)
01/12/10 21:00:08 Menu Change: Mode --> Mode (down -- note the loop back to the top)
01/12/10 21:00:08 Menu Change: Program --> Program (down)
01/12/10 21:00:08 Menu Change: Configure --> Configure (down)
01/12/10 21:00:08 Menu Change: Set Clock --> Set Clock (right)
01/12/10 21:00:13 Menu Used: Set Clock (enter -- keypad D)
01/12/10 21:00:13 Setting the clock
01/12/10 21:00:13 Enter Day Of Month and press Enter
The clock setting routine was triggered by a call to menuUseEvent:
void menuUseEvent(MenuUseEvent used) {
char log[255];
sprintf(log, "Menu Used: %s", used.item.getName());
logSerial(log);
if (used.item == miSetTime) //comparison agains a known item
{
setClock();
}
logSerial(log);
}

In my case I'm receiving input from a number of possible channels, these will include:

  • LCD+Keypad (Serial 2)
  • Console (Serial)
  • WIFI (Serial 3)
  • Remote Control (TBA)

Of course with this solution any update on one channel will result in a screen update on all of them... I don't have a problem with that as I will be the only user and unlikely to use more than one access method at a time.

Now - I'm working on some code to render the above menu's on a graphical LCD... once I get the LCD working that is :/

9 comments:

  1. Updated to match the most recent MenuBackend release linked in the blog.

    ReplyDelete
  2. There is now a sample for the menu's in the arduino software kit. Nice ;)

    ReplyDelete
  3. Hi, how do you check if a key has been pressed once you are in the lower level?

    ReplyDelete
  4. Hi, I am also using menubackend on a project I am working on and am curious if you got yours working on an lcd.
    I want to list the menu items, use up/down buttons and have the current selection highlighted so that when the enter button is pressed that menu item gets used.
    I am fairly new to C++ and am having some difficulty with this.
    Thanks,
    Don

    ReplyDelete
  5. Gday, Did you get the menu working on your project?
    I am trying to get a project working with a menu using menubackend.h and am trying to list the menu items on a display.
    I want to use an up/down button to select different menu items, with the current selection displaying inverted text. Pressing enter on the selected item will then execute that menu item.
    I am fairly new to C++. I have patched a few things together from other code to get different displays working. (Nokia 5110, I2C 4X20 etc)
    Any help or advice you could offer would be greatly appreciated.
    Don

    ReplyDelete
  6. Example here;
    https://github.com/agronaught/Reflow-Controller/blob/master/reflow.zip

    ReplyDelete
  7. Hi Jason,

    I recently purchased an RGB Lcd Shield from Adafruit and have it working with the shield so far. I was wondering if you'd share the sketch so I can understand it more as far as being able to set the RTC. Even if its just the parts of the code that's directed towards the RTC in general. My email is liquidartstattoos@gmail.com

    Thanks,
    Martin

    ReplyDelete
    Replies
    1. I'll see if I can find it, my project sources are a little disorganised and spread across several machines. If I do I'll upload to github and post the URI here.

      Cheers.

      Delete