The CommandProcessor is the interface to install a run-time menu into an embedded system.

Dependents:   A_CANAdapter USB2I2C

CommandProcessor.c

Committer:
WiredHome
Date:
2011-03-20
Revision:
0:198f53da1bc8
Child:
5:a98bd1f2fd59

File content as of revision 0:198f53da1bc8:

/// @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, &params))
            {
                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, &params);
                    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, &params))
                    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;
}