Ever since I managed to send my first character from my PC to
a microcontroller, I’ve had an itch to implement a beautiful
CLI for all of my hardware. So far I’ve settled to using single
character commands followed by positional arguments, which I parse
with sscanf()
like this:
getline(line); switch(line[0]) { case 'a': sscanf(&line[1], "%d %d", &a_arg1, &a_arg2); run_command_a(a_arg1, a_arg2); break; case 'b': sscanf(&line[1], "%u", &b_arg1); run_command_b(b_arg1); break; default: printf("Unkown command '%c'", line[0]); }
Although this works very well, I still have missed the word-length commands with
possible subcommands and options. I’d like my boards to be more responsive and I
want named arguments as I tend to forget the argument ordering quite often.
It would also be more convenient for others to operate with the board, if it responded like any
command-line program. In addition, consider the following command: a 1 2
. What if I only want
to change the second argument and leave the first one to be default?
I cannot do that as I still have to define the first one as well (and remember it’s default value)
in order to define the second one. The named arguments would solve this too.
Introducing docopt
It was in PyCon Finland 2012 when I heard about docopt, a CLI description language that generates the required code, created by Vladimir Keleshev. I still remember people applauding in the YouTube video when they saw docopt in action – it really was beautiful. Unfortunately Mr. Keleshev was unable to give a speech in PyCon Finland 2012, so we watched his earlier speech from YouTube. During the session I checked the docopt’s GitHub repository and realized that there was a port for C. Since then I’ve been thinking about using docopt with my embedded systems.
In short, using docopt we can just write our commands as a
help message in a man page styled file and then let docopt generate
the docopt.c file from this file. After that we only have to include
the generated docopt.c source file in our program and we are ready to use it.
Here’s an example how the man page styled .docopt
file might look like
for an imaginary board that does analog-to-digital conversion
Usage: board adc measure [--average=AVG] board adc freerun [--timeout=SECONDS] board adc channel CHAN board adc enable | disable board -h | --help | --version Options: -h, --help Show this screen. --version Show version. -a, --average AVG Averaging [default: 1]. -t, --timeout SECONDS Timeout for freerun [default: 30].
Unfortunately there was a major setback as soon as I started: I noticed that the current C port of docopt does not support commands yet, only options. This means that I cannot have separate commands like measure to start a measurement, or channel to change the channel. At the moment I only can work with the options like –average or -a to tell the ADC how much the signal should be averaged.
To test this example with docopt, go to http://try.docopt.org/ and copy-paste the upper help message into the input area there. Note that the program name board is not included into the argument vector (argv).
Testing with Aery32
Testing docopt with Aery32 was quite simple
(being one of the authors the choice of platform was obvious). I copied
the example/docopt.c file from the docopt.c GitHub repository and placed it
to my Aery32 project folder. I then took the Serial Port class driver
example file from Aery32 framework to be my main.cpp and modified
it a bit, as seen below. Additionally I had to make few changes to
the copied docopt.c example file to make it play nice in an embbed environment where,
for example, one usually doesn’t want to call exit()
, but return to
main()
. Lastly this file had to be excluded in the Makefile
to prevent Aery32 build system from compiling it as a separate source file.
The line_to_argv()
function was recently added to the Aery32 framework
in order to modify the read line into argument vector (argv) that can be
passed to docopt()
. While this function was originally written with the
docopt’s C port in mind, I’ve already found it useful in implementing
sscanf()
free communication as well.
#include <aery32/all.h> #include "board.h" #define LED AVR32_PIN_PC04 #define UART0_SERIAL_PINMASK 0x3 // PA0 = RX, PA01 = TX volatile uint8_t bufdma0[128] = {}; volatile uint8_t bufdma1[1024] = {}; using namespace aery; periph_idma dma0 = periph_idma(0, AVR32_PDCA_PID_USART0_RX, bufdma0, sizeof(bufdma0)); periph_odma dma1 = periph_odma(1, AVR32_PDCA_PID_USART0_TX, bufdma1, sizeof(bufdma1)); serial_port pc = serial_port(usart0, dma0, dma1); extern "C" { #include "docopt.c" } int main(void) { /* * The default board initializer defines all pins as input and * sets the CPU clock speed to 66 MHz. */ board::init(); gpio_init_pin(LED, GPIO_OUTPUT|GPIO_HIGH); gpio_init_pins(porta, UART0_SERIAL_PINMASK, GPIO_FUNCTION_A); pc.set_speed(115200).enable(); char line[32] = ""; size_t nread = 0; DocoptArgs args = {}; char *argv[8] = {}; int argc = 0; for(;;) { pc.getline(line, &nread, '\n'); if (nread == 0) continue; argc = line_to_argv(line, argv); args = docopt(argc, argv, /* help */ 1, "2.0rc2"); pc.printf("--help == %s\n", args.help ? "true" : "false"); pc.printf("--version == %s\n", args.version ? "true" : "false"); pc.printf("--tcp == %s\n", args.tcp ? "true" : "false"); pc.printf("--serial == %s\n", args.serial ? "true" : "false"); pc.printf("--host == %s\n", args.host); pc.printf("--port == %s\n", args.port); pc.printf("--timeout == %s\n", args.timeout); pc.printf("--baud == %s\n\n", args.baud); } return 0; }
Profit?
With the example code above it’s possible to specify options via
terminal. For example sending a line --baud=115200
gets parsed
and the value in args.baud
is set accordingly. -h
or --help
shows the help message. Missing arguments eg. -x
tell that
“-x is not recognized”.
Now someone (I?) should complete the docopt.c and make it work with
the commands. Some work has to be carried out as well to make docopt.c friendlier for
embedded software. From an embedded point of view the return value of the docopt()
function could be reserved for something more useful. At the moment it returns
the populated DocoptArgs struct, which make sense when used with the common
command-line application that would just exit when done. However, in an
embedded environment we would like to reuse the args record, so it would make
more sense to pass the pointer to this struct and reserve the return value
for something else.
The initial structure of the DocoptArgs could also be brainstormed a bit further.
In addition, there are few bugs to fix. For example, --foo
ends up in an infinite
loop saying ”–foo is not recognized”, and the C++ compiler gives several warnings
of “deprecated conversion from string constant to ‘char*’”.
Finally here’s an example how the embedded main loop could look like when
everything is in place. See that I’ve already modified the docopt()
function to take a pointer to the args record.
int run(DocoptArgs *args) { // do something with args } int main() { // ... for(;;) { getline(line, &nread, '\n'); if (nread == 0) continue; argc = line_to_argv(line, argv); docopt(argc, argv, &args, /* help */ 1, /* version */ "0.1"); if (docopt == /* ok */) run(&args); else // print a message like "-x is not recognized"? } return 0; }