Pages

Wednesday 1 January 2014

Simple Serial Port Command Line Interface (CLI)

It's often very useful to have some sort of interface through which a basic management can be done of our application. Whether you're designing a giant Marquee with a LED display or a plotter or you simply need to get some diagnostics from your device occasionally, a simple CLI system can come very handy. 

Of course, designing something similar to "bash" or any other unix shell is completely out of scope since those applications are huge and are simply an overkill for our needs. It's pretty simple though to create some basic, yet flexible & easilly extensible CLI system.

First thing needed is a command type definition. This will bind a keyword with an actual underlying routine executed for that keyword typed in the CLI.


typedef struct _t_cmd {
const char *cmd;
void (*fh)(void*);
} t_cmd;


The command type is pretty simple. There's the CLI command/keyword pointer, it holds a pointer to the execution function and that's it.

OK, so far so good. Now we need to define a set of commands since a single command CLI isn't much useful. Let's create a CLI context type for that which will hold entire CLI state - this has two advantages, first - it aggregates all the data in one place, second - if we'll write our functions to depend only on the context (which I'm going to do), then it will be possible to declare a couple of independent CLIs on different interfaces for example running concurrently.


typedef struct _t_cli_ctx {
// command set
t_cmd *cmds;

// cmd
char cmd[CLI_CMD_BUFFER_SIZE];
unsigned char cpos;

} t_cli_ctx;


OK, the context is pretty simple. It contains the command set pointer and current command buffer "cmd" - this buffer will hold everything you type before hitting ENTER and interpreting the command. It contains current cursor position as well. Once again, at the moment the context is pretty simple - but it will be enhanced later on to implement some more features.


static t_cmd g_cmds[] = {

{ "hw", fh_hello },
{ "hi", fh_hi }

// null
{ {0x00}, 0x00, 0x00  }
};


Let's prepare an initialization function for the CLI context definition in order to prepare it for execution.


void cli_init(t_cli_ctx *a_ctx, t_cmd *a_cmds) {
memset(a_ctx, 0x00, sizeof(t_cli_ctx));
a_ctx->cmds = a_cmds;
}


Believe it or not but more or less that's it. We can now declare some commands and proceed with the implementation of the CLI interpreter. We haven't yet used or done anything hardware specific so, the code can be compiled on a usual x86 machine for tests. The only platform specific code will be IO related. We need to create it now in order to proceed. For the AVR it will be quite simple. Our CLI system needs to be fed one key at a time. We can use a function from libpca:

unsigned char serial_getc(unsigned char *a_data);

It returns one if there is a character available and place it in the pointer provided. In order to perform some offline tests a similar function for x86 Linux terminal is needed. Unfortunately such function doesn't exist. There are two options


  1. Use ncurses which implements getch() function - to retrieve a single character.
  2. Disabled so called "canonical mode" for a standard linux terminal and use getchar() in non blocking mode.


Let's focus on the second approach. Ncurses is a bit of an overkill as far as I'm concerned though using it would be platform independent - but it's only a test application so, it's not that important. Let's have a look on the test program bellow.


I implemented here two functions cm_off & cm_on in order to turn the "canonical" mode on & off. When this mode is turned off - I'm able to get a direct input using the getchar() routine without having to press ENTER after every key. Our final linux_getc() along with linux_putc() routine will look the following way:


Let's implement the CLI interpreter itself now.


It doesn't do much at the moment. In fact it doesn't even run the commands. All it does is handle some basic input. We echo back the incoming characters, interpret backspace key and the ENTER key, printing some fancy prompt:

$ ./a.out 

#> command

#> some other cmd 

#> aaa

#> 

That is not very exciting in particular. Let's implement a command interpretation first:


I'll place that function in the KEY_CODE_ENTER switch case:


case KEY_CODE_ENTER: // new line
a_ctx->cmd[POSINC(a_ctx->cpos)] = '\0';
CLI_IO_OUTPUT("\r\n", 2);

res = _cli_interpret_cmd(a_ctx);

a_ctx->cpos = 0;
memset(a_ctx->cmd, 0x00, CLI_CMD_BUFFER_SIZE);
_cli_prompt(a_ctx, 1);
break;


I need some dummy commands implementation as well:

static void fh_hw(void *a_data) {
CLI_IO_OUTPUT("hello_world", 11);
}

static void fh_hi(void *a_data) {
CLI_IO_OUTPUT("hi", 2);
}

Let's do the test

./a.out 
#> hw
hello_world
#> hi
hi
#> test

#> 

Looks good, although there are is a problem - there is no info that a particular command is unknown. This must be fixed. Let's create another function and place it just after _cli_interpret_cmd in the switch case for ENTER key.


The switch case for ENTER will look the following way now:

case KEY_CODE_ENTER: // new line
a_ctx->cmd[POSINC(a_ctx->cpos)] = '\0';
CLI_IO_OUTPUT("\r\n", 2);

res = _cli_interpret_cmd(a_ctx);
_cli_reinterpret_cmd(a_ctx, res);

a_ctx->cpos = 0;
memset(a_ctx->cmd, 0x00, CLI_CMD_BUFFER_SIZE);
_cli_prompt(a_ctx, 1);
break;

Let's test it:

./a.out

#> test

Command not found
#> other

Command not found
#> hw
hello_world
#>

That's starting to look very good. At the moment we have a functional very basic CLI system. Let's consider what else is missing. When pressing an arrow keys nothing happens - we are unable to correct any mistakes in the middle of the command. This can be implemented. But first, let's checkout the key codes produced by those keys. On my terminal those are:


  • Left: 1b 5b 44
  • Right: 1b 5b 43
  • Up: 1b 5b 41
  • Down: 1b 5b 42


They may be completely different on yours. It's all terminal dependent, things like character encoding and terminal settings have a major influence here. That complicates the situation slightly, since a single key produces 3 codes and the key itself can be distinguished only by the third one. Our present implementation is unable to cope with that kind of input. Let's define a structure for that kind of input representing a single key

#define MULTICODE_INPUT_MAX_LEN 5

typedef struct _t_multicode_input {
unsigned char buf[MULTICODE_INPUT_MAX_LEN];
unsigned char pos;
} t_multicode_input;


... and a related command:


typedef struct _t_multicode_cmd {
unsigned char pattern[MULTICODE_INPUT_MAX_LEN];
unsigned char len;
void (*fh)(void*);
} t_multicode_cmd;


OK. The "input code" can be up to 5 characters long. What I want to do is to simply read the whole multi-code input and compare it against known ones in order to trigger some actions. First the CLI context structure must be extended:


typedef struct _t_cli_ctx {
// command set
t_cmd *cmds;
t_multicode_cmd *mcmds;

// cmd
char cmd[CLI_CMD_BUFFER_SIZE];
unsigned char cpos;

// multicode input
t_multicode_input mchar;
} t_cli_ctx;


The interpreter must be extended as well:


What I'm doing here is simply matching a whole multi-code input sequence against the known ones and fire an action if there is a match. Still, there's no associated actions defined. Let's create a couple:

t_multicode_cmd g_mc_cmds[] = {

{ { 0x1b, 0x5b, 0x44 }, 3, _cli_key_left },
{ { 0x1b, 0x5b, 0x43 }, 3, _cli_key_right },

// null
{ {0x00}, 0, 0x00}
};

static void _cli_key_left(void *a_data) {
t_cli_ctx *ctx = (t_cli_ctx *)a_data;

if (ctx->cpos) {
ctx->cmd[ctx->cpos--] = '\0';
CLI_IO_OUTPUT("\b \b", 3);
}
}

static void _cli_key_right(void *a_data) {
}

Just to make things simple - the left arrow key will behave the same way as backspace key. The right key won't do anything. That's great we can now handle even more complex input type. Let's think what else is still missing ?

What happends when you hit an up arrow key in bash ? It brings back the previous command. Some simple history implementation would be very fancy and would make the CLI system very attractive. Let's implement it as well. First some additional data fields are needed in the CLI context. Let's have a look on it once again:

#define CLI_CMD_HISTORY_LEN 8

typedef struct _t_cli_ctx {
// command set
t_cmd *cmds;
t_multicode_cmd *mcmds;

// cmd
char cmd[CLI_CMD_BUFFER_SIZE];
unsigned char cpos;

// history
char history[CLI_CMD_HISTORY_LEN][CLI_CMD_BUFFER_SIZE];
unsigned char hpos;
unsigned char hhead, htail;

// multicode input
t_multicode_input mchar;
} t_cli_ctx;


The CLI will hold last 8 commands typed into it. After pressing ENTER we need to save current command line into the history. Let's do it in the _cli_reinterpret_cmd function to sift any junk input from contaminating the history:


Handlers for up/down arrow keys are needed to make this feature complete:


Now, those commands must be attached to our multicode_input command array.


t_multicode_cmd g_mc_cmds[] = {

{ { 0x1b, 0x5b, 0x44 }, 3, _cli_key_left },
{ { 0x1b, 0x5b, 0x43 }, 3, _cli_key_right },

{ { 0x1b, 0x5b, 0x41 }, 3, _cli_history_up },
{ { 0x1b, 0x5b, 0x42 }, 3, _cli_history_down },

// null
{ {0x00}, 0, 0x00}
};

And we're ready to go. Let's test it.

$ ./a.out

#> command1

Command not found
#> cmd2

Command not found
#> cmd3

Command not found
#>
(3/8)cmd3
(2/8)cmd2
(1/8)command1

Command not found
#>
(4/8)command1
(3/8)cmd3

Command not found
#>

That seems to work very well. Our CLI starts to look very attractive now. One thing is still missing though - the basic command argument support. I left it for the end deliberately. Let's not forget that the final product will run on a small micro-controller with not much RAM on board. The only generic useful information would be an argc - an integer defining how many space delimited words have been typed into the CLI. If the command really needs to parse arguments then it should do it itself, just to save resources. Let's modify the code for _cli_interpret_cmd first and add a call to the argument counting routine:

a_ctx->argc = _cli_count_arguments(a_ctx);

The routine itself:


Let's write a test command for that as well:

{ "argc", fh_argc },


static void fh_argc(void *a_data) {
char tmp[16] = { 0x00 };
t_cli_ctx *ctx = (t_cli_ctx *)a_data;
sprintf(tmp, "argc: %d", ctx->argc);
CLI_IO_OUTPUT(tmp, strlen(tmp));
}

and test it:

$ ./a.out

#> argc
argc: 1
#> argc a1 a2 a4
argc: 4
#>

That works fine as well. At this stage we are done with testing on the x86 platform and we can move the code to the MCU itself. It's a good point to summarize the complete code so far. I reorganized the whole thing and split the code into files



The test application can be compiled without a Makefile, the following way:

gcc main.c linux_io.c cli.c -I. -o cli

Moving the CLI to the MCU


There's not much to do actually since the CLI has been written with a hardware independent code. However there are some gotchas which will come out later on. First let's prepare the project, change the IO routines for the CLI, compile the project and try to run it. Of course, I'm going to use the serial routines implemented in libpca for the IO. The IO definitions look the following way:


/**
 * @brief IO input routine - change it accordingly to your implementation
 */
#define CLI_IO_INPUT(__data) \
serial_getc(__data)


/**
 * @brief IO output routine - change it accordingly to your implementation
 */
#define CLI_IO_OUTPUT(__data, __len) \
serial_poll_send(__data, __len)

After flashing the arduino and connecting a minicom terminal, first thing to notice is lack of any response for ENTER key. Minicom emulates VT102 terminal and sends only a 0x0d code for a new line character, the devinition for KEY_CODE_ENTER must be changed accordingly. Let's recompile and verify the rest of the keys. The arrow keys works. The backspace key and the delete key don't work though, again it's the matter of the incorrect keycodes. We can discover those by echoing them back after receiving:

sprintf(tmp, "%02x\n", i);
CLI_IO_OUTPUT(tmp,strlen(tmp));

Minicom on my machine sends 0x08 code for backspace and a set of codes for backspace key: 0x1b 0x5b 33 7e. The code needs to be adjusted slightly. First the value of KEY_CODE_BACKSPACE must be corrected. The DELETE key needs to be interpreted in "multicode" fashion. I'll attach the same action to it as for BACKSPACE and left arrow key:

{ { 0x1b, 0x5b, 0x33, 0x7e }, 4, _cli_delete }, // delete

Let's recompile and test once again. Everything seems to work now. The basic CLI code is complete. This code proves how much CLI systems and interpreters are terminal dependent. We can either try to be as much compliant and flexible with most of popular terminal emulators or simply require from the user to use only one specific type i.e. VT100 / VT102 or any other.

As usual a set of sources can be downloaded either from my google drive or from github. Snapshot, containing a version of libpca as well as the source code discussed in this post is available here

The code is available in my GitHub repositories. First you need libpca:

git clone git@github.com:dagon666/avr_Libpca pca

and my projects repository:

git clone git@github.com:dagon666/avr_ArduinoProjects projects

Navigate to projects/cli_mcu to find the source code.


4 comments:

  1. Tomasz, thank you for a post.
    In my case compiler made errors for null-value of static t_cmd g_cmds[] and replacement { {0x00}, 0x00, 0x00 } to { 0x00, 0x00 } fixed it.

    ReplyDelete
  2. Tomasz, thank you. This was so helpful with my arduino nano project.
    I've created a programmable servo sequencer to lower Rc undercarriage & doors. CLI needed to input 5 channel servo sequences and save to EEPROM. Now I flick a switch and wheels go up/down realistically.

    ReplyDelete
  3. Hey there, I get a "permission denied" when I try to clone libpca ..

    Help?

    ReplyDelete
    Replies
    1. The github interface worked though, just your commands in the blog above fail with permission denied..

      Delete