A simple Command Line Interface (CLI)

When developing libraries and other complex applications, I find that I often need to exercise specific parts of the library/application as it is being developed.

One way to do this is to write specific test code to exercise functionality. Another is to provide an interactive command line interface to achieve the same.

Until recently I hard coded these testing code CLI for each application. I now have a simple class that enables a flexible and consistent CLI.

The CLI is available as the Arduino library MD_CmdProcessor from my code repository.

Defining CLI command syntax

In the spirit of keeping things as simple as possible, the CLI commands are one or two characters in length, and can optionally have parameters needed to support that CLI command.

Multiple commands can follow each other, separated by a semicolon (‘;’), and an input line is terminated by a newline character (‘\n’).

The command line is highly structured and intolerant of anything outside this structure, a tradeoff to keep it as simple as possible.

A more formal command structure definition is as follows:

<command_line> = <cmd>|<cmd><space><parameters>[<separator><command_line>]<eoln>
<cmd> = <string>
<parameters> = <string>
<string> = <character><string>
<space> = ' '
<separator> = ';'
<eoln> = '\n'
<character> = any ASCII except for <space>, <separator> or <newline>

Implementing flexibility

A table driven approach allows the same code to be reused across all the different implementations of cmdProcessor. The table is made up of line items that are defined by this data structure

struct cmdItem_t
{
  char cmd[CMD_TXT_SIZE+1];  // the actual command string
  cmdHandler_t f;            // address of handler function
  char helpParam[CMD_PARAM_SIZE+1]; // help text for parameters
  char helpText[CMD_HELP_SIZE+1];   // text for this command's help
};

The only critical data elements are the command text and the handler function. The rest are there to provide help when I forget what is what (it happens!). The help function is implemented through the help() method, which produces a formatted output of the command table and associated help text.

Note that all text strings are fixed size to allow the command table to be located in flash memory (PROGMEM) to minimise RAM impact.

Each command eventually executes a small block of code called a handler. The handler is of type cmdHandler_t and looks like the code below. The only parameter that the handler gets is a pointer to the substring passed as the parameter for the command.

void handler(char *param)
{
  // Code goes here
}

Finally, the object initializer is given a reference to a Stream object that allows input and output. In most cases this will be the Serial object, but it could be any other object that implements Stream I/O.

Implementing the CLI

Implementing the CLI only needs a few steps:

  1. Write the command handler code. Code stubs are sufficient to provide the function definitions for the command table.
  2. Define the command table with associated parameter and help text.
  3. Declare the global cmdProcessor object.
  4. Initialise the object in setup().
  5. Invoke the run() method every time through loop().

The example code shows a trivial implementation that does nothing useful except illustrate the required code structure.

#include <MD_cmdProcessor.h>

#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))

// handler function prototypes
void handlerHelp(char* param);

// handler functions
void handlerA1(char *param)
{
  Serial.print("\nA1 param:");
  Serial.print(param);
}

void handlerZ9(char *param)
{
  Serial.print("\nZ9 param:");
  Serial.print(param);
}

const MD_cmdProcessor::cmdItem_t PROGMEM cmdTable[] =
{
  { "?",  handlerHelp, "",    "Help" },
  { "a1", handlerA1,   "123", "Command A1" },
  { "Z9", handlerZ9,   "456", "Command Z9" },
};

MD_cmdProcessor CP(Serial, cmdTable, ARRAY_SIZE(cmdTable));

void handlerHelp(char* param)
{
  Serial.print(F("\nHelp\n===="));
  CP.help();
  Serial.print(F("\n"));
}

void setup(void) 
{
  Serial.begin(57600);
  Serial.print(F("\nEnter command. Ensure line ending set to newline."));
  CP.begin();
}

void loop(void) 
{
  CP.run();
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s