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 :/