The CommandProcessor is the interface to install a run-time menu into an embedded system.
Dependents: A_CANAdapter USB2I2C
Diff: CommandProcessor.c
- Revision:
- 0:198f53da1bc8
- Child:
- 5:a98bd1f2fd59
diff -r 000000000000 -r 198f53da1bc8 CommandProcessor.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CommandProcessor.c Sun Mar 20 21:50:15 2011 +0000 @@ -0,0 +1,546 @@ +/// @file CommandProcessor.c is a simple interface to an interactive +/// command set of user defined commands. +/// +/// With this, you can create functions that are exposed to a console +/// interface. Each command may have 0 or more parameters. +/// Typing the command, or at least the set of characters that make +/// it unique from all other commands is enough to activate the command. +/// +/// Even though it is a c interface, it is somewhat object oriented. +/// +/// @version 1.0 +/// +/// @note Copyright © 2011 by Smartware Computing, all rights reserved. +/// This program may be used by others as long as this copyright notice +/// remains intact. +/// @author David Smart +/// +#include "stdio.h" +#include "string.h" +#include "stdlib.h" +#ifdef WIN32 +#include "windows.h" +#pragma warning (disable: 4996) +#endif + +#include "CommandProcessor.h" + +/// This holds the single linked list of commands +/// +typedef struct CMDLINK_T +{ + CMD_T * menu; // handle to the menu item + struct CMDLINK_T * next; // handle to the next link +} CMDLINK_T; + +static CMDLINK_T * head = NULL; + +static char *buffer; // buffer space must be allocated based on the longest command + +static struct +{ + int bufferSize; // size of the buffer + int caseinsensitive; // FALSE=casesensitive, TRUE=insensitive + int (*kbhit)(void); + int (*getch)(void); + int (*putch)(int ch); + int (*puts)(const char * s); +} cfg; + +static INITRESULT_T CommandProcessor_Init( + int defaultMenu, + int caseinsensitive, + int maxCmdLen, + int (*kbhit)(void), + int (*getch)(void), + int (*putch)(int ch), + int (*puts)(const char * s) + ); +static ADDRESULT_T CommandProcessor_Add(CMD_T *m); +static RUNRESULT_T CommandProcessor_Run(void); +static RUNRESULT_T CommandProcessor_End(void); + +static CMDP_T CommandProcessor = +{ + CommandProcessor_Init, + CommandProcessor_Add, + CommandProcessor_Run, + CommandProcessor_End +}; + +static RUNRESULT_T Help(char *p); +static RUNRESULT_T Exit(char *p); +static RUNRESULT_T About(char *p); + +static CMD_T HelpMenu = {"Help", "Shows this help, 'Help ?' shows more details.", Help, visible}; +static CMD_T AboutMenu = {"About", "About this CommandProcessor", About, visible}; +static CMD_T ExitMenu = {"Exit", "Exits the program", Exit, visible}; + +/// Gets a handle to the CommandProcessor +/// +/// This returns a handle to the CommandProcessor, which then permits +/// access to the CommandProcessor functions. +/// +/// @returns handle to the CommandProcessor +/// +CMDP_T * GetCommandProcessor(void) +{ + return &CommandProcessor; +} + + +static RUNRESULT_T About(char *p) +{ + cfg.puts("\r\n About this CommandProcessor:\r\n" + " This CommandProcessor provides easy facility for creating a\r\n" + " runtime CommandProcessor in an embedded systems.\r\n" + " author: David Smart, Smartware Computing\r\n" + ); + return runok; +} + +static RUNRESULT_T Exit(char *p) +{ + (void)p; + + cfg.puts("\r\nbye."); + return runexit; +} + +static RUNRESULT_T Help(char *p) +{ + CMDLINK_T *link = head; + char buffer[100]; + + cfg.puts("\r\n"); + sprintf(buffer, " %-10s: %s", "Command", "Description"); + cfg.puts(buffer); + while (link && link->menu) + { + if (link->menu->visible) + { + if (strlen(link->menu->command) + strlen(link->menu->helptext) + 5 < sizeof(buffer)) + { + sprintf(buffer, " %-10s: %s", link->menu->command, link->menu->helptext); + cfg.puts(buffer); + } + } + link = link->next; + } + cfg.puts(""); + if (*p == '?') + { + cfg.puts("\r\n Extended Help\r\n" + " The general form for entering commands is:\r\n" + " >command option1 option2 ...\r\n" + " [note that note all commands support optional parameters]\r\n" + " * Abbreviations of commands may be entered so long as there\r\n" + " is exactly one match in the list of commands. For example,\r\n" + " 'he' is the same as 'help', if there is no other command \r\n" + " that starts with 'he'.\r\n" + " * <bs> can be used to erase previously entered information.\r\n" + " * <esc> can be used to cancel a command.\r\n" + " * <tab> can be used to complete the entry of a partial command.\r\n" + ""); + } + return runok; +} + +/// mytolower exists because not all compiler libraries have this function +/// +/// This takes a character and if it is upper-case, it converts it to +/// lower-case and returns it. +/// +/// @param a is the character to convert +/// @returns the lower case equivalent to a +/// +char mytolower(char a) +{ + if (a >= 'A' && a <= 'Z') + return (a - 'A' + 'a'); + else + return a; +} + +/// mystrnicmp exists because not all compiler libraries have this function. +/// +/// Some have strnicmp, others _strnicmp, and others have C++ methods, which +/// is outside the scope of this C-portable set of functions. +/// +/// @param l is a pointer to the string on the left +/// @param r is a pointer to the string on the right +/// @param n is the number of characters to compare +/// @returns -1 if l < r +/// @returns 0 if l == r +/// @returns +1 if l > r +/// +int mystrnicmp(const char *l, const char *r, size_t n) +{ + int result = 0; + + if (n != 0) + { + do { + result = mytolower(*l++) - mytolower(*r++); + } while ((result == 0) && (*l != '\0') && (--n > 0)); + } + if (result < -1) + result = -1; + else if (result > 1) + result = 1; + return result; +} + +/// mystrcat exists because not all compiler libraries have this function +/// +/// This function concatinates one string onto another. It is generally +/// considered unsafe, because of the potential for buffer overflow. +/// Some libraries offer a strcat_s as the safe version, and others may have +/// _strcat. Because this is needed only internal to the CommandProcessor, +/// this version was created. +/// +/// @param dst is a pointer to the destination string +/// @param src is a pointer to the source string +/// @returns nothing +/// +void mystrcat(char *dst, char *src) +{ + while (*dst) + dst++; + do + *dst++ = *src; + while (*src++); +} + +/// myisprint exists because not all compiler libraries have this function +/// +/// This function tests a character to see if it is printable (a member +/// of the standard ASCII set). +/// +/// @param c is the character to test +/// @returns TRUE if the character is printable +/// @returns FALSE if the character is not printable +/// +static int myisprint(int c) +{ + if (c >= ' ' && c <= '~') + return TRUE; + else + return FALSE; +} + + +/// CommandMatches is the function that determines if the user is entering a valid +/// command +/// +/// This function gets called whenever the user types a printable character or when +/// they press \<enter\>. +/// The buffer of user input is evaluated to determine if the command is legitimate. +/// It also determines if they want to execute the command, or simply evaluate it. +/// Finally, it identifies writes to user provided pointers, the menu item that +/// matches and a pointer to any parameters that the user entered. +/// +/// @param buffer is the buffer that the user has entered commands into +/// @param exec indicates the intention to execute the command, if found +/// @param menu is a pointer to a pointer to a menu structure, which is updated based on the command +/// @param params is a pointer to a pointer to the user entered parameters +/// @returns the number of menu picks that match the user entered command +/// +static int CommandMatches(char * buffer, int exec, CMD_T **menu, char ** params) +{ + char *space; + int compareLength; + int foundCount = 0; + CMDLINK_T *link = head; + + if (strlen(buffer)) // simple sanity check + { + // Try to process the buffer. A command could be "Help", or it could be "Test1 123 abc" + space = strchr(buffer, ' '); // if the command has parameters, find the delimiter + if (space) + { + compareLength = space - buffer; + space++; // char after the space + } + else + { + compareLength = strlen(buffer); + space = buffer + compareLength; + } + while (link && link->menu) + { + int cmpResult; + + if (cfg.caseinsensitive) + { + cmpResult = mystrnicmp(buffer, link->menu->command, compareLength); + } + else + cmpResult = strncmp(buffer, link->menu->command, compareLength); + if (0 == cmpResult) // A match to what they typed + { + if (menu) // yes, we have a callback + { + if (exec) // command wants to execute it, not just validate the command syntax + { + // If they type "He 1234 5678", we backup and rewrite as "Help 1234 5678" + int diff = strlen(link->menu->command) - compareLength; // e.g. 5 - 3 + + if (diff > 0) + { + int back = 0; + char *p; + if (strlen(space)) + back = strlen(space) + 1; + + while (back--) + cfg.putch(0x08); + p = link->menu->command + compareLength; + while (*p) + cfg.putch(*p++); + cfg.putch(' '); + p = space; + while (*p) + cfg.putch(*p++); + //cfg.printf("%s %s", link->menu->command + compareLength, space); + } + } + *menu = link->menu; // accessor to the command they want to execute + *params = space; + } + foundCount++; // how many command match what they typed so far + } + link = link->next; // follow the link to the next command + } + } + return foundCount; +} + +/// Initialize the CommandProcessor +/// +/// This initializes the CommandProcessor by adding the built-in menus +/// +/// @param addDefaultMenu configures it to add the Help, About, and Exit menus +/// @param bufSize configures the size of the longest command, which must be +/// greater than 6 (the size of "About\0"). +/// @returns initok if it successfully initialized the CommandProcessor +/// @returns initfailed if it could not allocate memory +/// +INITRESULT_T CommandProcessor_Init( + int addDefaultMenu, + int caseinsensitive, + int maxCmdLen, + int (*kbhit)(void), + int (*getch)(void), + int (*putch)(int ch), + int (*puts)(const char * s) + ) +{ + if (maxCmdLen < 6) + maxCmdLen = 6; + buffer = (char *)malloc(maxCmdLen+1); + cfg.bufferSize = maxCmdLen; + if (buffer) + { + if (addDefaultMenu & 0x0004) + CommandProcessor.Add(&HelpMenu); + if (addDefaultMenu & 0x0002) + CommandProcessor.Add(&AboutMenu); + if (addDefaultMenu & 0x0001) + CommandProcessor.Add(&ExitMenu); + cfg.caseinsensitive = caseinsensitive; + cfg.kbhit = kbhit; + cfg.getch = getch; + cfg.putch = putch; + cfg.puts = puts; + return initok; + } + else + return initfailed; +} + +/// Add a command to the CommandProcessor +/// +/// This adds a command to the CommandProcessor. A command has several components +/// to it, including the command name, a brief description, the function to +/// activate when the command is entered, and a flag indicating if the command +/// should show up in the built-in help. +/// +/// @todo sort them when adding a menu item +/// +/// @param m is the menu to add to the CommandProcessor +/// @returns addok if the command was added +/// @returns addfail if the command could not be added (failure to allocate memory for the linked list) +/// +ADDRESULT_T CommandProcessor_Add(CMD_T * m) +{ + CMDLINK_T *p; + + if (head == NULL) + { + head = (CMDLINK_T *)malloc(sizeof(CMDLINK_T)); + if (!head) + return addfailed; + head->menu = NULL; + head->next = NULL; + } + p = head; + while (p->next) + p = p->next; + if (p->menu == NULL) + { + p->menu = m; + return addok; + } + else if (p->next == NULL) + { + p->next = (CMDLINK_T *)malloc(sizeof(CMDLINK_T)); + if (!p->next) + return addfailed; + p = p->next; + p->menu = m; + p->next = NULL; + return addok; + } + return addfailed; +} + +/// Run the CommandProcessor +/// +/// This will peek to see if there is a keystroke ready. It will pull that into a +/// buffer if it is part of a valid command in the command set. You may then enter +/// arguments to the command to be run. +/// +/// Primitive editing is permitted with <bs>. +/// +/// When you press <enter> it will evaluate the command and execute the command +/// passing it the parameter string. +/// +/// @returns runok if the command that was run allows continuation of the CommandProcessor +/// @returns runfail if the command that was run is asking the CommandProcessor to exit +/// +RUNRESULT_T CommandProcessor_Run(void) +{ + static int showPrompt = TRUE; + static int keycount = 0; // how full? + RUNRESULT_T val = runok; // return true when happy, false to exit the prog + CMD_T *cbk = NULL; + char * params = NULL; + + if (showPrompt) + { + cfg.putch('>'); + showPrompt = FALSE; + } + if (cfg.kbhit()) + { + int c = cfg.getch(); + + switch (c) + { + case 0x09: // <TAB> to request command completion + if (1 == CommandMatches(buffer, FALSE, &cbk, ¶ms)) + { + size_t n; + char *p = strchr(buffer, ' '); + if (p) + n = p - buffer; + else + n = strlen(buffer); + if (n < strlen(cbk->command)) + { + p = cbk->command + strlen(buffer); + mystrcat(buffer, p); + keycount = strlen(buffer); + while (*p) + cfg.putch(*p++); + //cfg.printf("%s", p); + } + } + break; + case 0x1b: // <ESC> to empty the command buffer + while (keycount--) + { + cfg.putch(0x08); // <bs> + cfg.putch(' '); + cfg.putch(0x08); + } + keycount = 0; + buffer[keycount] = '\0'; + break; + case '\x08': // <bs> + if (keycount) + { + buffer[--keycount] = '\0'; + cfg.putch(0x08); + cfg.putch(' '); + cfg.putch(0x08); + } + else + cfg.putch(0x07); // bell + break; + case '\r': + case '\n': + { + int foundCount = 0; + + if (strlen(buffer)) + { + foundCount = CommandMatches(buffer, TRUE, &cbk, ¶ms); + if (foundCount == 1) + { + val = (*cbk->callback)(params); // Execute the command + } + else if (foundCount > 1) + cfg.puts(" *** non-unique command ignored try 'Help' ***"); + else if (foundCount == 0) + cfg.puts(" *** huh? try 'Help' ***"); + } + else + cfg.puts(""); + keycount = 0; + buffer[keycount] = '\0'; + showPrompt = TRUE; // forces the prompt + } + break; + default: + if (myisprint(c) && keycount < cfg.bufferSize) + { + buffer[keycount++] = c; + buffer[keycount] = '\0'; + if (CommandMatches(buffer, FALSE, &cbk, ¶ms)) + cfg.putch(c); + else + { + buffer[--keycount] = '\0'; + cfg.putch(0x07); // bell + } + } + else + cfg.putch(0x07); // bell + break; + } + } + return val; +} + + +/// End the CommandProcessor by freeing all the memory that was allocated +/// +/// returns runok +/// +RUNRESULT_T CommandProcessor_End(void) +{ + CMDLINK_T *p = head; + CMDLINK_T *n; + + do + { + n = p->next; + free(p); // free each of the allocated links to menu items. + p = n; + } while (n); + free(buffer); // finally, free the command buffer + buffer = NULL; // flag it as deallocated + return runok; +}