Simple menuing system

This stand-alone sketch demos a simple menuing system that interacts with the serial interface for key presses.

It was built in response to this post http://forum.arduino.cc/index.php?topic=323218

/**

In this post: http://forum.arduino.cc/index.php?topic=323218
The OP states that he has a complex system of menus and that he is using gotos
all over the place to manage it.

This is a terrible idea.

This stand-alone sketch demonstrates a menuing system.
The "working" item flash the LED on pin 13, but hopefully it's clear enough that you can
modify it to suit your own purposes.

It's built so as not to interfere too much with your loop(). All you need to do is
to push characters into it, which in this case I am getting from Serial. You could
probably rig it up to be driven by button-presses if you wanted.

Please credit Paul Murray<pmurray@bigpond.com>

*/

#include <ctype.h>

struct menuitem {
  // a menu item has a title and an action that is
  // performed when the item is selected
  // the title is indented with spaces to show the structure, and the first
  // non-indented character id the hot key to be pressed
  // the action is called with the index of the menu item that is being actuvated
  // this allows you to have an action that does different things
  // depending on which menu item activated it
  // aata allows you to attach some extra data (of any type) to a menu. It's the responsibiluity
  // opf the 'action' function to understand what it's supposed to do

  char *title;
  void (*action)(int);
  void *data;
};

// we have to make forward decalrations of these functions
void menuProcessKeyStroke(char key); // function that"drives" the menuy system

void submenuSelect(int selected); // move the menuing system to display a submenu
void menuUp(int selected); // go back to the previous menu
void menuHome(int selected); // go all the way to the home menu
void menuDisplay(int selected); // print out the selectedmenu

// these are the "working" functions of the menuing system

void printstring(int selected); // print the string in the extra data
void fastflash(int selected); // flash pin 13 10 times
void flash(int selected); // flash pin 13 a number of times specified in the extra data

// these need to be variables so that we can get a pointer to them
int FLASHDATA_3 = 3;
int FLASHDATA_5 = 5;

const struct menuitem menu[] = {
  { "# - MAIN MENU"},
  { "  1 - Print some sample strings", submenuSelect},
  { "    1 - Print hello world.", printstring, (char*) "Hello, World!"},
  { "    2 - Print birthday greeting.", printstring, (char*) "Happy Birthday!"},
  { "    * - Redisplay this menu", menuDisplay},
  { "    < - Back", menuUp},
  { "    # - Home", menuHome},
  { "  2 - Flash pin 13", submenuSelect},
  { "    F - Fast flash pin 13 10 times", fastflash},
  { "    S - Slow flash", submenuSelect},
  { "      3 - Flash pin 13 (3) times", flash, &FLASHDATA_3},
  { "      5 - Flash pin 13 (5) times", flash, &FLASHDATA_5},
  { "      * - Redisplay this menu", menuDisplay},
  { "      < - Back", menuUp},
  { "      # - Home", menuHome},
  { "    * - Redisplay this menu", menuDisplay},
  { "    < - Back", menuUp},
  { "    # - Home", menuHome},
  { "  * - Redisplay this menu", menuDisplay},
};

const int MENU_LEN = sizeof(menu) / sizeof(*menu);

int menuCurrent = 0;

void setup() {
  Serial.begin(9600);
  // five second delay to give me time to bring up the serial monitor :)
  for (int i = 5; i >= 0; i--) {
    delay(1000);
    Serial.print(i);
    Serial.print(' ');
  }
  Serial.println(' ');

  menuDisplay(menuCurrent); // always zero at this point.
}

void loop() {
  // put your main code here, to run repeatedly:

  // we may need to do a number of things in the loop. one thing we do is to
  // drive the menu by reading keypresses off Serial. You could also accomplish this
  // by reading buttons or whatever

  if (Serial.available()) {
    menuProcessKeyStroke(Serial.read());
  }

}

/////////////////////////////////////////////////////
// these functions are the working functions of our app. They flash the LED, and print stuff to serial.

void printstring(int selected) {
  Serial.println((char *) menu[selected].data);
}

void fastflash(int selected) {
  // this function doesn't read any data - it just flashes the LED a fixed number of times
  for(int i = 0; i< 10; i++) {
    digitalWrite(13, HIGH);
    delay(100);
    digitalWrite(13, LOW);
    delay(100);
  }
}

void flash(int selected) {
  // when this function is called, data wil be a pointer to int.
  
  int n = * (int *) menu[selected].data;
  for(int i = 0; i< n; i++) {
    digitalWrite(13, HIGH);
    // 0.616 is the golden ratio :)
    delay(616);
    digitalWrite(13, LOW);
    delay(1000-616);
  }
}


/////////////////////////////////////////////////////
// These are the functions that drive the menuing system itself

void menuProcessKeyStroke(char key) {
  // ignore whitespace characaters
  if (isspace(key)) return;

  // OK! Find a current menu item whose first character is the same as the key we just pressed.
  // note that this code is pretty much the same as the 'display menu' code

  const int currentSpaces = countSpaces(menu[menuCurrent].title);

  // run down the menu until we hit the end, or an item whose spaces are <= currentspaces)
  int i = menuCurrent + 1;

  while (i < MENU_LEN) {
    const int submenuSpaces = countSpaces(menu[i].title);
    if (submenuSpaces <= currentSpaces) {
      break;
    }
    
    // ok! This is a sub item of the menu. is it the one we are looking for?
    
    if(menu[i].title[submenuSpaces] == key) {
      // yay! Perform the menu action - whatever it may be
      menu[i].action(i);
      return;
    }
    
    i++;
    // now skip any menu items that are more hevily indented than the submenu we are on
    while (i < MENU_LEN && countSpaces(menu[i].title) > submenuSpaces) {
      i++;
    }
  }

  // right. We did not find an item corresponding to the key that was pressed
  
  Serial.print("? no menu for key ");
  Serial.println(key);
  
  // we *could* reprint the menu here, but I'd rather pring less than more. Most menus
  // have a '*' item, and if they dont there's probably a good reason.
}

void submenuSelect(int selected) {
  // move the menuing system to display a submenu
  menuCurrent = selected;
  menuDisplay(menuCurrent);
}

void menuUp(int selected) {
  // go back to the previous menu
  // the previous menu is the first one that has an indent less than the menu we are currently on

  const int currentSpaces = countSpaces(menu[menuCurrent].title);

  while (menuCurrent > 0 && countSpaces(menu[menuCurrent].title) >= currentSpaces) {
    menuCurrent--;
  }
  
  menuDisplay(menuCurrent);
}

void menuHome(int selected) {
  // go all the way to the home menu
  menuCurrent = 0;
  menuDisplay(menuCurrent); // always zero at this point.
}

void menuDisplay(int selected) {
  // we ignore the 'selected' value, because 'selected'
  // will be one of the submenus of the current menu.

  Serial.println("==========");
  Serial.print("Navigation: ");
  menuNavigation(menuCurrent);
  Serial.println();
  
  // optional - we could print out the title of the current menu here, but
  // the current menu includes the hotkey, which may be a tad confusing

  // print out the selectedmenu
  const int currentSpaces = countSpaces(menu[menuCurrent].title);
  // run down the menu until we hit the end, or an item whose spaces are <= currentspaces)

  int i = menuCurrent + 1;

  while (i < MENU_LEN) {
    const int submenuSpaces = countSpaces(menu[i].title);
    if (submenuSpaces <= currentSpaces) {
      break;
    }

    // ok! This is a sub item of the menu. Print it, zapping the initial spaces.
    Serial.println(menu[i].title + submenuSpaces);
    i++;

    // now skip any menu items that are more hevily indented than the submenu we are on

    while (i < MENU_LEN && countSpaces(menu[i].title) > submenuSpaces) {
      i++;
    }
  }

  Serial.println("==========");

}

void menuNavigation(const int n) {
  const int spaces = countSpaces(menu[n].title);
  if(spaces == 0) {
    Serial.print(menu[n].title);
    return;
  }

  for(int i = n-i; i>=0; i--) {
    if(countSpaces(menu[i].title) < spaces) {
       menuNavigation(i);
       break;
    }
  }
  
  Serial.print(" > ");
  Serial.print(menu[n].title + spaces);
}

int countSpaces(char *p) {
  int n = 0;
  while (p[n++] == ' ') /* do nothing */ ;
  return n - 1;
}