//simple menu on a 20x4 I2C LCD
// options are:
// - set the # of blinks on the built-in LED
// - enable and disable the blinks
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x27, 20, 4);
//prototypes of menu pages
uint8_t mnMainPage( uint8_t );
uint8_t mnSetNumBlinks( uint8_t );
uint8_t mnEnableBlinks( uint8_t );
//give the menu page states names
enum menuStates
{
ST_INIT = 0,
ST_MAIN,
ST_WIPE
};
//possible return values from menu pages
enum menuReturns
{
MN_CONTINUE = 0,
MN_DONE
};
//up/dn/sel switch returns (includes no switch)
enum swReturns_e
{
NO_SW = 0,
UP,
DN,
SEL
};
//fixed values for LED toggle and inter-frame delays
const uint32_t BLNK_LEDDLY = 250ul;
const uint32_t BLNK_BIGDLY = 2000ul;
const uint8_t
grSwReturns[] = {NO_SW, UP, DN, SEL};
const uint8_t
grControlButtons[3] = {2, 3, 4};
const uint8_t pinLED = LED_BUILTIN;
//
bool
bBlinkEnable = false;
uint8_t
grPinLasts[3];
uint8_t
numBlinks = 10;
//pointer to a function that returns a u8 and which expects a u8
typedef uint8_t (*pFunc_t)(uint8_t);
//activeFunc is a pointer to the currently-active menu page
pFunc_t
activeFunc;
void setup()
{
//set up the up/dn/sel pins
for( uint8_t i=0; i<3; i++ )
{
pinMode( grControlButtons[i], INPUT_PULLUP );
grPinLasts[i] = digitalRead( grControlButtons[i] );
}//if
//init the LCD
lcd.init();
lcd.backlight();
//and set the LED to blink
pinMode( pinLED, OUTPUT );
//init the menu function pointer to the main menu page
activeFunc = &mnMainPage;
}//setup
void loop()
{
//blink the LED
doLED();
//handle the menu
mnHandleMenu();
}//loop
uint8_t readButtons( void )
{
static uint8_t
idx = 0;
static uint32_t
tRead = 0ul;
uint32_t
tNow = millis();
uint8_t
retval,
swNow;
//read the switches, one every 10mS
retval = NO_SW;
if( tNow - tRead >= 10ul )
{
tRead = tNow;
swNow = digitalRead( grControlButtons[idx] );
if( swNow != grPinLasts[idx] )
{
grPinLasts[idx] = swNow;
if( swNow == LOW )
retval = grSwReturns[idx+1];
}//if
idx++;
if( idx == 3 )
idx = 0;
}//if
return retval;
}//readButtons
void doLED( void )
{
static bool
lastEnable = bBlinkEnable;
static uint8_t
nBlinks = 10,
bLEDState = false;
static uint32_t
tBlink = 0ul,
tDelay = BLNK_BIGDLY;
uint32_t
tNow = millis();
//if not time for a blink or the blink is disabled, just leave
if( (tNow - tBlink) < tDelay || bBlinkEnable == false )
return;
//has bBlinkEnable changed to "true"?
if( lastEnable != bBlinkEnable )
{
lastEnable = bBlinkEnable;
if( bBlinkEnable == true )
//yes; set its blink count to the global count
nBlinks = numBlinks;
}//if
//blink the LED
tBlink = tNow;
bLEDState ^= true;
digitalWrite( pinLED, bLEDState ? HIGH:LOW );
tDelay = BLNK_LEDDLY;
if( bLEDState == false )
{
//if the LED has gone off, decrement the local blink count
nBlinks--;
//if it has reached zero...
if( nBlinks == 0 )
{
//set up the long "frame" delay
tDelay = BLNK_BIGDLY;
//and reset the local count to the global one
nBlinks = numBlinks;
}//if
}//if
}//doLED
void mnHandleMenu( void )
{
//call the active menu page and send it the latest up/dn/sel
//states
activeFunc( readButtons() );
}//mnHandleMenu
uint8_t mnMainPage( uint8_t ctlButton )
{
static int8_t
highlight = 0;
static uint8_t
state = ST_INIT;
switch( state )
{
case ST_INIT:
//initialize this menu page
lcd.clear();
lcd.setCursor(0,0);
lcd.print( F("Main Menu") );
lcd.setCursor(2,1);
lcd.print( F("Set # LED blinks") );
lcd.setCursor(2,2);
lcd.print( F("Tgl LED blinking") );
highlight = 0;
lcd.setCursor( 0, highlight + 1 );
lcd.print( F(">") );
state = ST_MAIN;
break;
case ST_MAIN:
//we stay in the main page until we hit SEL
switch( ctlButton )
{
case UP:
//user hit UP; move the selection pointer 'up'
lcd.setCursor( 0, highlight + 1 );
lcd.print( F(" ") );
highlight++;
//if we go too high 'up', recirculate to bottom
if( highlight == 2 )
highlight = 0;
lcd.setCursor( 0, highlight + 1 );
lcd.print( F(">") );
break;
case DN:
//user hit DN; move the selection pointer 'down'
lcd.setCursor( 0, highlight + 1 );
lcd.print( F(" ") );
highlight--;
//if we go too far 'down', recirculate to top
if( highlight < 0 )
highlight = 1;
lcd.setCursor( 0, highlight + 1 );
lcd.print( F(">") );
break;
case SEL:
//user hit select
//move to the "wipe" state to "animate" clearing the menu...
state = ST_WIPE;
break;
}//switch
break;
case ST_WIPE:
//when WipeMenu returns true, the old menu is erased so...
if( WipeMenu() == true )
{
//set the activeFunc to the menu item the user selected
activeFunc = (highlight == 0) ? &mnSetNumBlinks : &mnEnableBlinks;
//and set the local state here to INIT again so when
//we get back to this menu the screen is properly initialized
state = ST_INIT;
return MN_DONE;
}//if
break;
}//state
return MN_CONTINUE;
}//mnMainPage
uint8_t mnSetNumBlinks( uint8_t ctlButton )
{
char
szStr[10];
static int8_t
highlight = 0;
static uint8_t
state = ST_INIT;
//these states are similar to the main page but are specific to
//setting the number of blinks
switch( state )
{
case ST_INIT:
//initialize this menu page
lcd.clear();
lcd.setCursor(0,0);
lcd.print( F("Set # Blinks") );
lcd.setCursor(8,1);
sprintf( szStr, "%3d", numBlinks );
lcd.print( szStr );
lcd.setCursor(1,3);
lcd.print( F("UP:+ DN:- SEL:Done") );
state = ST_MAIN;
break;
case ST_MAIN:
switch( ctlButton )
{
case UP:
if( numBlinks < 255 )
numBlinks++;
lcd.setCursor(8,1);
sprintf( szStr, "%3d", numBlinks );
lcd.print( szStr );
break;
case DN:
if( numBlinks > 10 )
numBlinks--;
lcd.setCursor(8,1);
sprintf( szStr, "%3d", numBlinks );
lcd.print( szStr );
break;
case SEL:
state = ST_WIPE;
break;
}//switch
break;
case ST_WIPE:
if( WipeMenu() == true )
{
activeFunc = &mnMainPage;
state = ST_INIT;
return MN_DONE;
}//if
break;
}//state
return MN_CONTINUE;
}//mnSetNumBlinks
uint8_t mnEnableBlinks( uint8_t ctlButton )
{
char
szStr[10];
static int8_t
highlight = 0;
static uint8_t
state = ST_INIT;
//these states are similar to the main page but are specific to
//enabling or disabling the blinking
switch( state )
{
case ST_INIT:
//initialize this menu page
lcd.clear();
lcd.setCursor(0,0);
lcd.print( F("En/Disable Blinks") );
lcd.setCursor(2,1);
lcd.print( F("Enable") );
lcd.setCursor(2,2);
lcd.print( F("Disable") );
highlight = 0;
lcd.setCursor( 0, highlight + 1 );
lcd.print( F(">") );
state = ST_MAIN;
break;
case ST_MAIN:
switch( ctlButton )
{
case UP:
lcd.setCursor( 0, highlight + 1 );
lcd.print( F(" ") );
highlight++;
if( highlight == 2 )
highlight = 0;
lcd.setCursor( 0, highlight + 1 );
lcd.print( F(">") );
break;
case DN:
lcd.setCursor( 0, highlight + 1 );
lcd.print( F(" ") );
highlight--;
if( highlight < 0 )
highlight = 1;
lcd.setCursor( 0, highlight + 1 );
lcd.print( F(">") );
break;
case SEL:
state = ST_WIPE;
break;
}//switch
break;
case ST_WIPE:
if( WipeMenu() == true )
{
activeFunc = &mnMainPage;
bBlinkEnable = (highlight == 0) ? true : false;
state = ST_INIT;
return MN_DONE;
}//if
break;
}//state
return MN_CONTINUE;
}//mnEnableBlinks
//dumb little function to "wipe" the menu for effect
bool WipeMenu( void )
{
static uint8_t
col = 0;
static uint32_t
tChar = 0ul;
uint32_t
tNow = micros();
if( tNow - tChar >= 15000ul )
{
tChar = tNow;
for( uint8_t i=0; i<4; i++ )
{
lcd.setCursor( col, i );
lcd.print( F(" ") );
}//for
col++;
if( col == 20 )
{
col = 0;
return true;
}//if
}//if
return false;
}//WipeMenu