New command bootmenu: ANSI terminal boot menu support
authorPali Rohár <pali.rohar@gmail.com>
Sat, 23 Mar 2013 14:53:08 +0000 (14:53 +0000)
committerAnatolij Gustschin <agust@denx.de>
Fri, 29 Mar 2013 08:35:34 +0000 (09:35 +0100)
The "bootmenu" command uses U-Boot menu interfaces and provides
a simple mechanism for creating menus with several boot items.
When running this command the menu will be assembled as defined
by a set of environment variables which contain a title and
command key-value pairs. The "Up" and "Down" keys are used for
navigation through the items. Current active menu item is
highlighted and can be selected using the "Enter" key.

The command interprets and generates various ANSI escape
sequencies, so for proper menu rendering and item selection
the used terminal should support them.

Signed-off-by: Pali Rohár <pali.rohar@gmail.com>
[agust: various fixes and documentation updates]
Signed-off-by: Anatolij Gustschin <agust@denx.de>
common/Makefile
common/cmd_bootmenu.c [new file with mode: 0644]
doc/README.bootmenu [new file with mode: 0644]
include/ansi.h [new file with mode: 0644]

index 1abf4ea794e56686c420faeab5024aa885ac7304..f6313110c94cf1852409905196ee77e17c10159b 100644 (file)
@@ -75,6 +75,7 @@ COBJS-$(CONFIG_CMD_SOURCE) += cmd_source.o
 COBJS-$(CONFIG_CMD_BDI) += cmd_bdinfo.o
 COBJS-$(CONFIG_CMD_BEDBUG) += bedbug.o cmd_bedbug.o
 COBJS-$(CONFIG_CMD_BMP) += cmd_bmp.o
+COBJS-$(CONFIG_CMD_BOOTMENU) += cmd_bootmenu.o
 COBJS-$(CONFIG_CMD_BOOTLDR) += cmd_bootldr.o
 COBJS-$(CONFIG_CMD_BOOTSTAGE) += cmd_bootstage.o
 COBJS-$(CONFIG_CMD_CACHE) += cmd_cache.o
diff --git a/common/cmd_bootmenu.c b/common/cmd_bootmenu.c
new file mode 100644 (file)
index 0000000..a3cbffa
--- /dev/null
@@ -0,0 +1,517 @@
+/*
+ * (C) Copyright 2011-2013 Pali Rohár <pali.rohar@gmail.com>
+ *
+ * See file CREDITS for list of people who contributed to this
+ * project.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+ * MA 02111-1307 USA
+ */
+
+#include <common.h>
+#include <command.h>
+#include <ansi.h>
+#include <menu.h>
+#include <hush.h>
+#include <watchdog.h>
+#include <malloc.h>
+#include <linux/string.h>
+
+/* maximum bootmenu entries */
+#define MAX_COUNT      99
+
+/* maximal size of bootmenu env
+ *  9 = strlen("bootmenu_")
+ *  2 = strlen(MAX_COUNT)
+ *  1 = NULL term
+ */
+#define MAX_ENV_SIZE   (9 + 2 + 1)
+
+struct bootmenu_entry {
+       unsigned short int num;         /* unique number 0 .. MAX_COUNT */
+       char key[3];                    /* key identifier of number */
+       char *title;                    /* title of entry */
+       char *command;                  /* hush command of entry */
+       struct bootmenu_data *menu;     /* this bootmenu */
+       struct bootmenu_entry *next;    /* next menu entry (num+1) */
+};
+
+struct bootmenu_data {
+       int delay;                      /* delay for autoboot */
+       int active;                     /* active menu entry */
+       int count;                      /* total count of menu entries */
+       struct bootmenu_entry *first;   /* first menu entry */
+};
+
+enum bootmenu_key {
+       KEY_NONE = 0,
+       KEY_UP,
+       KEY_DOWN,
+       KEY_SELECT,
+};
+
+static char *bootmenu_getoption(unsigned short int n)
+{
+       char name[MAX_ENV_SIZE] = "bootmenu_";
+
+       if (n > MAX_COUNT)
+               return NULL;
+
+       sprintf(name + 9, "%d", n);
+       return getenv(name);
+}
+
+static void bootmenu_print_entry(void *data)
+{
+       struct bootmenu_entry *entry = data;
+       int reverse = (entry->menu->active == entry->num);
+
+       /*
+        * Move cursor to line where the entry will be drown (entry->num)
+        * First 3 lines contain bootmenu header + 1 empty line
+        */
+       printf(ANSI_CURSOR_POSITION, entry->num + 4, 1);
+
+       puts("     ");
+
+       if (reverse)
+               puts(ANSI_COLOR_REVERSE);
+
+       puts(entry->title);
+
+       if (reverse)
+               puts(ANSI_COLOR_RESET);
+}
+
+static void bootmenu_autoboot_loop(struct bootmenu_data *menu,
+                               enum bootmenu_key *key, int *esc)
+{
+       int i, c;
+
+       if (menu->delay > 0) {
+               printf(ANSI_CURSOR_POSITION, menu->count + 5, 1);
+               printf("  Hit any key to stop autoboot: %2d ", menu->delay);
+       }
+
+       while (menu->delay > 0) {
+               for (i = 0; i < 100; ++i) {
+                       if (!tstc()) {
+                               WATCHDOG_RESET();
+                               mdelay(10);
+                               continue;
+                       }
+
+                       menu->delay = -1;
+                       c = getc();
+
+                       switch (c) {
+                       case '\e':
+                               *esc = 1;
+                               *key = KEY_NONE;
+                               break;
+                       case '\r':
+                               *key = KEY_SELECT;
+                               break;
+                       default:
+                               *key = KEY_NONE;
+                               break;
+                       }
+
+                       break;
+               }
+
+               if (menu->delay < 0)
+                       break;
+
+               --menu->delay;
+               printf("\b\b\b%2d ", menu->delay);
+       }
+
+       printf(ANSI_CURSOR_POSITION, menu->count + 5, 1);
+       puts(ANSI_CLEAR_LINE);
+
+       if (menu->delay == 0)
+               *key = KEY_SELECT;
+}
+
+static void bootmenu_loop(struct bootmenu_data *menu,
+               enum bootmenu_key *key, int *esc)
+{
+       int c;
+
+       while (!tstc()) {
+               WATCHDOG_RESET();
+               mdelay(10);
+       }
+
+       c = getc();
+
+       switch (*esc) {
+       case 0:
+               /* First char of ANSI escape sequence '\e' */
+               if (c == '\e') {
+                       *esc = 1;
+                       *key = KEY_NONE;
+               }
+               break;
+       case 1:
+               /* Second char of ANSI '[' */
+               if (c == '[') {
+                       *esc = 2;
+                       *key = KEY_NONE;
+               } else {
+                       *esc = 0;
+               }
+               break;
+       case 2:
+       case 3:
+               /* Third char of ANSI (number '1') - optional */
+               if (*esc == 2 && c == '1') {
+                       *esc = 3;
+                       *key = KEY_NONE;
+                       break;
+               }
+
+               *esc = 0;
+
+               /* ANSI 'A' - key up was pressed */
+               if (c == 'A')
+                       *key = KEY_UP;
+               /* ANSI 'B' - key down was pressed */
+               else if (c == 'B')
+                       *key = KEY_DOWN;
+               /* other key was pressed */
+               else
+                       *key = KEY_NONE;
+
+               break;
+       }
+
+       /* enter key was pressed */
+       if (c == '\r')
+               *key = KEY_SELECT;
+}
+
+static char *bootmenu_choice_entry(void *data)
+{
+       struct bootmenu_data *menu = data;
+       struct bootmenu_entry *iter;
+       enum bootmenu_key key = KEY_NONE;
+       int esc = 0;
+       int i;
+
+       while (1) {
+               if (menu->delay >= 0) {
+                       /* Autoboot was not stopped */
+                       bootmenu_autoboot_loop(menu, &key, &esc);
+               } else {
+                       /* Some key was pressed, so autoboot was stopped */
+                       bootmenu_loop(menu, &key, &esc);
+               }
+
+               switch (key) {
+               case KEY_UP:
+                       if (menu->active > 0)
+                               --menu->active;
+                       /* no menu key selected, regenerate menu */
+                       return NULL;
+               case KEY_DOWN:
+                       if (menu->active < menu->count - 1)
+                               ++menu->active;
+                       /* no menu key selected, regenerate menu */
+                       return NULL;
+               case KEY_SELECT:
+                       iter = menu->first;
+                       for (i = 0; i < menu->active; ++i)
+                               iter = iter->next;
+                       return iter->key;
+               default:
+                       break;
+               }
+       }
+
+       /* never happens */
+       debug("bootmenu: this should not happen");
+       return NULL;
+}
+
+static void bootmenu_destroy(struct bootmenu_data *menu)
+{
+       struct bootmenu_entry *iter = menu->first;
+       struct bootmenu_entry *next;
+
+       while (iter) {
+               next = iter->next;
+               free(iter->title);
+               free(iter->command);
+               free(iter);
+               iter = next;
+       }
+       free(menu);
+}
+
+static struct bootmenu_data *bootmenu_create(int delay)
+{
+       unsigned short int i = 0;
+       const char *option;
+       struct bootmenu_data *menu;
+       struct bootmenu_entry *iter = NULL;
+
+       int len;
+       char *sep;
+       struct bootmenu_entry *entry;
+
+       menu = malloc(sizeof(struct bootmenu_data));
+       if (!menu)
+               return NULL;
+
+       menu->delay = delay;
+       menu->active = 0;
+       menu->first = NULL;
+
+       while ((option = bootmenu_getoption(i))) {
+               sep = strchr(option, '=');
+               if (!sep) {
+                       printf("Invalid bootmenu entry: %s\n", option);
+                       break;
+               }
+
+               entry = malloc(sizeof(struct bootmenu_entry));
+               if (!entry)
+                       goto cleanup;
+
+               len = sep-option;
+               entry->title = malloc(len + 1);
+               if (!entry->title) {
+                       free(entry);
+                       goto cleanup;
+               }
+               memcpy(entry->title, option, len);
+               entry->title[len] = 0;
+
+               len = strlen(sep + 1);
+               entry->command = malloc(len + 1);
+               if (!entry->command) {
+                       free(entry->title);
+                       free(entry);
+                       goto cleanup;
+               }
+               memcpy(entry->command, sep + 1, len);
+               entry->command[len] = 0;
+
+               sprintf(entry->key, "%d", i);
+
+               entry->num = i;
+               entry->menu = menu;
+               entry->next = NULL;
+
+               if (!iter)
+                       menu->first = entry;
+               else
+                       iter->next = entry;
+
+               iter = entry;
+               ++i;
+
+               if (i == MAX_COUNT - 1)
+                       break;
+       }
+
+       /* Add U-Boot console entry at the end */
+       if (i <= MAX_COUNT - 1) {
+               entry = malloc(sizeof(struct bootmenu_entry));
+               if (!entry)
+                       goto cleanup;
+
+               entry->title = strdup("U-Boot console");
+               if (!entry->title) {
+                       free(entry);
+                       goto cleanup;
+               }
+
+               entry->command = strdup("");
+               if (!entry->command) {
+                       free(entry->title);
+                       free(entry);
+                       goto cleanup;
+               }
+
+               sprintf(entry->key, "%d", i);
+
+               entry->num = i;
+               entry->menu = menu;
+               entry->next = NULL;
+
+               if (!iter)
+                       menu->first = entry;
+               else
+                       iter->next = entry;
+
+               iter = entry;
+               ++i;
+       }
+
+       menu->count = i;
+       return menu;
+
+cleanup:
+       bootmenu_destroy(menu);
+       return NULL;
+}
+
+static void bootmenu_show(int delay)
+{
+       int init = 0;
+       void *choice = NULL;
+       char *title = NULL;
+       char *command = NULL;
+       struct menu *menu;
+       struct bootmenu_data *bootmenu;
+       struct bootmenu_entry *iter;
+       char *option, *sep;
+
+       /* If delay is 0 do not create menu, just run first entry */
+       if (delay == 0) {
+               option = bootmenu_getoption(0);
+               if (!option) {
+                       puts("bootmenu option 0 was not found\n");
+                       return;
+               }
+               sep = strchr(option, '=');
+               if (!sep) {
+                       puts("bootmenu option 0 is invalid\n");
+                       return;
+               }
+               run_command(sep+1, 0);
+               return;
+       }
+
+       bootmenu = bootmenu_create(delay);
+       if (!bootmenu)
+               return;
+
+       menu = menu_create(NULL, bootmenu->delay, 1, bootmenu_print_entry,
+                          bootmenu_choice_entry, bootmenu);
+       if (!menu) {
+               bootmenu_destroy(bootmenu);
+               return;
+       }
+
+       for (iter = bootmenu->first; iter; iter = iter->next) {
+               if (!menu_item_add(menu, iter->key, iter))
+                       goto cleanup;
+       }
+
+       /* Default menu entry is always first */
+       menu_default_set(menu, "0");
+
+       puts(ANSI_CURSOR_HIDE);
+       puts(ANSI_CLEAR_CONSOLE);
+       printf(ANSI_CURSOR_POSITION, 1, 1);
+
+       init = 1;
+
+       if (menu_get_choice(menu, &choice)) {
+               iter = choice;
+               title = strdup(iter->title);
+               command = strdup(iter->command);
+       }
+
+cleanup:
+       menu_destroy(menu);
+       bootmenu_destroy(bootmenu);
+
+       if (init) {
+               puts(ANSI_CURSOR_SHOW);
+               puts(ANSI_CLEAR_CONSOLE);
+               printf(ANSI_CURSOR_POSITION, 1, 1);
+       }
+
+       if (title && command) {
+               debug("Starting entry '%s'\n", title);
+               free(title);
+               run_command(command, 0);
+               free(command);
+       }
+
+#ifdef CONFIG_POSTBOOTMENU
+       run_command(CONFIG_POSTBOOTMENU, 0);
+#endif
+}
+
+void menu_display_statusline(struct menu *m)
+{
+       struct bootmenu_entry *entry;
+       struct bootmenu_data *menu;
+
+       if (menu_default_choice(m, (void *)&entry) < 0)
+               return;
+
+       menu = entry->menu;
+
+       printf(ANSI_CURSOR_POSITION, 1, 1);
+       puts(ANSI_CLEAR_LINE);
+       printf(ANSI_CURSOR_POSITION, 2, 1);
+       puts("  *** U-Boot Boot Menu ***");
+       puts(ANSI_CLEAR_LINE_TO_END);
+       printf(ANSI_CURSOR_POSITION, 3, 1);
+       puts(ANSI_CLEAR_LINE);
+
+       /* First 3 lines are bootmenu header + 2 empty lines between entries */
+       printf(ANSI_CURSOR_POSITION, menu->count + 5, 1);
+       puts(ANSI_CLEAR_LINE);
+       printf(ANSI_CURSOR_POSITION, menu->count + 6, 1);
+       puts("  Press UP/DOWN to move, ENTER to select");
+       puts(ANSI_CLEAR_LINE_TO_END);
+       printf(ANSI_CURSOR_POSITION, menu->count + 7, 1);
+       puts(ANSI_CLEAR_LINE);
+}
+
+#ifdef CONFIG_MENU_SHOW
+int menu_show(int bootdelay)
+{
+       bootmenu_show(bootdelay);
+       return -1; /* -1 - abort boot and run monitor code */
+}
+#endif
+
+int do_bootmenu(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
+{
+       char *delay_str = NULL;
+       int delay = 10;
+
+#if defined(CONFIG_BOOTDELAY) && (CONFIG_BOOTDELAY >= 0)
+       delay = CONFIG_BOOTDELAY;
+#endif
+
+       if (argc >= 2)
+               delay_str = argv[1];
+
+       if (!delay_str)
+               delay_str = getenv("bootmenu_delay");
+
+       if (delay_str)
+               delay = (int)simple_strtol(delay_str, NULL, 10);
+
+       bootmenu_show(delay);
+       return 0;
+}
+
+U_BOOT_CMD(
+       bootmenu, 2, 1, do_bootmenu,
+       "ANSI terminal bootmenu",
+       "[delay]\n"
+       "    - show ANSI terminal bootmenu with autoboot delay"
+);
diff --git a/doc/README.bootmenu b/doc/README.bootmenu
new file mode 100644 (file)
index 0000000..9e85b40
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * (C) Copyright 2011-2012 Pali Rohár <pali.rohar@gmail.com>
+ *
+ * See file CREDITS for list of people who contributed to this
+ * project.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+ * MA 02111-1307 USA
+ */
+
+ANSI terminal bootmenu command
+
+The "bootmenu" command uses U-Boot menu interfaces and provides
+a simple mechanism for creating menus with different boot items.
+The cursor keys "Up" and "Down" are used for navigation through
+the items. Current active menu item is highlighted and can be
+selected using the "Enter" key. The selection of the highlighted
+menu entry invokes an U-Boot command (or a list of commands)
+associated with this menu entry.
+
+The "bootmenu" command interprets ANSI escape sequencies, so
+an ANSI terminal is required for proper menu rendering and item
+selection.
+
+The assembling of the menu is done via a set of environment variables
+"bootmenu_<num>" and "bootmenu_delay", i.e.:
+
+  bootmenu_delay=<delay>
+  bootmenu_<num>="<title>=<commands>"
+
+  <delay> is the autoboot delay in seconds, after which the first
+  menu entry will be selected automatically
+
+  <num> is the boot menu entry number, starting from zero
+
+  <title> is the text of the menu entry shown on the console
+  or on the boot screen
+
+  <commands> are commands which will be executed when a menu
+  entry is selected
+
+  (title and commands are separated by first appearance of '='
+   character in the environment variable)
+
+First (optional) argument of the "bootmenu" command is a delay specifier
+and it overrides the delay value defined by "bootmenu_delay" environment
+variable. If the environment variable "bootmenu_delay" is not set or if
+the argument of the "bootmenu" command is not specified, the default delay
+will be CONFIG_BOOTDELAY. If delay is 0, no menu entries will be shown on
+the console (or on the screen) and the command of the first menu entry will
+be called immediately. If delay is less then 0, bootmenu will be shown and
+autoboot will be disabled.
+
+Bootmenu always adds menu entry "U-Boot console" at the end of all menu
+entries specified by environment variables. When selecting this entry
+the bootmenu terminates and the usual U-Boot command prompt is presented
+to the user.
+
+Example environment:
+
+  setenv bootmenu_0 Boot 1. kernel=bootm 0x82000000  # Set first menu entry
+  setenv bootmenu_1 Boot 2. kernel=bootm 0x83000000  # Set second menu entry
+  setenv bootmenu_2 Reset board=reset                # Set third menu entry
+  setenv bootmenu_3 U-Boot boot order=boot           # Set fourth menu entry
+  bootmenu 20        # Run bootmenu with autoboot delay 20s
+
+
+The above example will be rendered as below
+(without decorating rectangle):
+
+┌──────────────────────────────────────────┐
+│                                          │
+│  *** U-Boot Boot Menu ***                │
+│                                          │
+│     Boot 1. kernel                       │
+│     Boot 2. kernel                       │
+│     Reset board                          │
+│     U-Boot boot order                    │
+│     U-Boot console                       │
+│                                          │
+│  Hit any key to stop autoboot: 20        │
+│  Press UP/DOWN to move, ENTER to select  │
+│                                          │
+└──────────────────────────────────────────┘
+
+Selected menu entry will be highlighted - it will have inverted
+background and text colors.
+
+To enable the "bootmenu" command add following definitions to the
+board config file:
+
+  #define CONFIG_CMD_BOOTMENU
+  #define CONFIG_MENU
+
+To run the bootmenu at startup add these additional definitions:
+
+  #define CONFIG_AUTOBOOT_KEYED
+  #define CONFIG_BOOTDELAY 30
+  #define CONFIG_MENU_SHOW
+
+When you intend to use the bootmenu on color frame buffer console,
+make sure to additionally define CONFIG_CFB_CONSOLE_ANSI in the
+board config file.
diff --git a/include/ansi.h b/include/ansi.h
new file mode 100644 (file)
index 0000000..0e40b1d
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * (C) Copyright 2012
+ * Pali Rohár <pali.rohar@gmail.com>
+ *
+ * See file CREDITS for list of people who contributed to this
+ * project.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston,
+ * MA 02111-1307 USA
+ */
+
+/*
+ * ANSI terminal
+ */
+
+#define ANSI_CURSOR_UP                 "\e[%dA"
+#define ANSI_CURSOR_DOWN               "\e[%dB"
+#define ANSI_CURSOR_FORWARD            "\e[%dC"
+#define ANSI_CURSOR_BACK               "\e[%dD"
+#define ANSI_CURSOR_NEXTLINE           "\e[%dE"
+#define ANSI_CURSOR_PREVIOUSLINE       "\e[%dF"
+#define ANSI_CURSOR_COLUMN             "\e[%dG"
+#define ANSI_CURSOR_POSITION           "\e[%d;%dH"
+#define ANSI_CURSOR_SHOW               "\e[?25h"
+#define ANSI_CURSOR_HIDE               "\e[?25l"
+#define ANSI_CLEAR_CONSOLE             "\e[2J"
+#define ANSI_CLEAR_LINE_TO_END         "\e[0K"
+#define ANSI_CLEAR_LINE                        "\e[2K"
+#define ANSI_COLOR_RESET               "\e[0m"
+#define ANSI_COLOR_REVERSE             "\e[7m"