CLI Library
Library to build a real-time serial (or network) command-line interface (CLI) to configure or control your Arduino or compatible microcontroller.
This is Version 1.0, the latest version, documentation and bugtracker are available on my GitLab instance
Copyright (c) 2019 Frederik Lindenaar. free for distribution under the GNU License, see below
Introduction
Frequently need to interact with a microcontroller to, for example:
- see what's stored in the embedded or an I2C EEPROM
- check that all I2C devices are detected and responding
- check or set a connected real-time clock
- check or set configuration options (I don't hard-code config)
Since I don't like reprogramming the microcontroller every time this is needed, I wrote this library to provide a Command Line Interface over a Serial / USB line. The library includes a number of commands that can be included where applicable (which I expect will grow over time as I build more) so that it can be used as a toolbox to start your development without having to worry about stuff that could be standard. At this moment it includes the following commands:
-
eeprom_dump
to display the contents of the built-in EEPROM -
i2c_scan
to scan the I2C bus for slave devices -
i2c_dump
to display the contents of I2C attached EEPROM or Memory -
reset
to restart the microcontroller (software reset) -
help
to display available commands and provided help on how to use them
See below how to use the Library, to get an idea on how to use the library, have a look at the examples included:
- Blink: control the Blink example (using the built-in LED) using a Serial/USB console to change its blink rate or turn it on or off
- Debug: CLI with the built-in debug commands listed above
- DS1307RTC: CLI to read/set an DS1307 Real-Time Clock module (includes the built-in commands to access a module's NVRAM and EEPROM)
Download / Installation
At this moment this library is not yet available directly from the Arduino IDE but has to be installed manually. For this, download the latest distribution .zip file and install it using the following links:
- From my GitLab instance, download to your computer the Latest .zip archive
- Follow the documentation for the Arduino IDE on importing a .zip Library. Alternatively, you can also extract the downloaded .zip and follow the steps for manual Installation
You can also use git
to checkout the latest version from my repository with
git clone https://gitlab.lindenaar.net/arduino/CLI.git
so that it is easy to upgrade in the future. To find where to checkout check the guide for manual Installation.
Using the library
Before adding this library to your code, it is important to realize that by
adding the CLI you are builing a so-called real-time system. The microcontroller
should be able to perform it's main task (normally not responding to Serial/USB
input) and in the background listen to commands given through the CLI. As most
microcontrollers have only a single CPU and no OS that can multitask, all logic
is handled from the main loop()
function. As a consequence, one should avoid
writing code that waits (e.g. using the delay()
function) but instead create
a loop that does not wait/block but determines whether to do something and if
not moves on to the next task.
Writing a real-time (i.e. non-blocking) loop
Looking at the Arduino IDE's standard Blink example (probably the starting point
for most when starting with the platform), you see the following code in loop
:
// the loop function runs over and over again forever
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // turn LED on (HIGH is the voltage level)
delay(1000); // wait for a second
digitalWrite(LED_BUILTIN, LOW); // turn LED off by making the voltage LOW
delay(1000); // wait for a second
}
While this is perfectly fine code when the microcontroller has nothing else to do, for a background CLI this would cause 1 second delays between each check for input (ignoring more complex interrupt-driven approaches, which have their own challenges). The key for making this loop non-blocking is to change it so that it no longer waits but each time checks if it needs to change the LED status instead, like in the example below:
// variable to be preserved
unsigned long last_blink = 0;
// the loop function runs over and over again forever
void loop() {
// millis() will wrap every 49 days, below code is wrap-proof
if (millis() - last_blink > 1000) { // last_blink was longer ago than delay?
last_blink = millis(); // led state will change, store when
if(digitalRead(LED_BUILTIN)) { // check if the LED is on or off
digitalWrite(LED_BUILTIN, LOW); // turn LED off by making the pin LOW
} else {
digitalWrite(LED_BUILTIN, HIGH); // turn LED on by making the pin HIGH
}
}
}
This code uses the Arduino platform function millis()
to obtain how long the
code is running (in milliseconds) and keeps track of when the LED state changed
last in variable last_blink
(stored outside the loop so it is preserved).
The loop now simply checks every time whether the last blink was more than 1000
milliseconds ago and if so changes the LED state, otherwise it does nothing.
Please note that the above code was kept as much in line with the original Blink
example though could also be written as:
// variable to be preserved
unsigned long last_blink = 0;
// the loop function runs over and over again forever
void loop() {
// millis() will wrap every 49 days, below code is wrap-proof
if (millis() - last_blink > 1000) { // last_blink was longer ago than delay?
last_blink = millis(); // led state will change, store when
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // invert LED PIN
}
}
Once the loop of your code is non-blocking, the CLI can be added to your Sketch.
Adding the CLI to a Sketch
Adding the CLI to your sketch is pretty straightforward, first include CLI.h
at the top of your sketch like this:
#include <CLI.h>
Next instantiate the CLI object by adding the following above (and outside) your
setup()
and loop()
functions:
// Initialize the Command Line Interface
CLI CLI(Serial); // Initialize the CLI, telling it to attach to Serial
Directly under the instantation of the CLI
object commands can be instantiated
(added) like is shown below for the built-in Help command:
Help_Command Help(CLI); // Initialize/Register (built-in) help command
The constructor of each command requires a CLI
to register with to make the
implemented command available in the CLI.
Next in your loop()
, place the following logic at the top:
// handle CLI, if this returns true a command is running so skip code block
if (!CLI.process()) {
// Code to run when no command is executing, make sure it is non-blocking...
}
// Code to execute every loop goes here, make sure it is non-blocking...
The CLI.process()
method handles the Command Line Interpreter; it responds to
user input, parses the command and executes the command code. To ensure that
this is also non-blocking, each of these steps is executed in smaller chunks so
that your main logic can be intertwined with the execution of the CLI logic. The
CLI.process()
method will return true
if a command is still executing so by
placing logic inside the if() { ... }
block, you ensure it only runs when the
CLI is not doing anything while if you put it outside the block it will always
be executed. This can also be used to add an LED indicator to display whether
the CLI is executing as is shown in the below full sketch example (which shows
how a basic full implementation would look like):
#include <CLI.h>
// Initialize the Command Line Interface
CLI CLI(Serial); // Initialize the CLI, telling it to attach to Serial
Help_Command Help(CLI); // Initialize/Register (built-in) help command
// the setup function runs once when you reset or power the board
void setup() {
// Initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
// Initialize the Serial port for the CLI
while (!Serial); // For Leonardo: wait for serial USB to connect
Serial.begin(9600);
}
// the loop function runs over and over again forever
void loop() {
// handle CLI, if this returns true a command is running so skip code block
if (CLI.process()) {
digitalWrite(LED_BUILTIN, HIGH); // turn LED on when processing CLI command
} else {
digitalWrite(LED_BUILTIN, LOW); // turn LED off when CLI command not active
// Code to run when no command is executing, make sure it is non-blocking...
}
// Code to execute every loop goes here, make sure it is non-blocking...
}
You can run the above code by creating a new sketch and replacing its contents with the full example above. Please also have a look at the examples provided as they give a better view on what can be done and how to include a CLI in your sketch.
CLI on the Serial port
As you can see in the above example, the CLI object is initiated and on Serial
and used to instantiate the Help_Command
while Serial.begin()
is only called
from setup() (i.e. afterwards). Due to the way the initialization of the Arduino
platform works, it is not possible to use Serial.begin()
before setup()
is
called as things like interrupts and other boot-strap initialization (including
that of the Serial port) has not taken place yet. For this reason the CLI object
cannot initialize a Serial
port but you have to do this from your setup()
routine (which doesn't really matter and also gives you full control over it)
but should not be forgotten (as the CLI won't work then).
Please note that due to this it is also not possible to use Serial.print()
in
a constructor (this has nothing do to with this library but is a quirk of the
Arduino platform).
CLI object parameters
The CLI object supports printing a banner upon startup and allows to configure
the defaults prompt ( >
). To reduce the memory usage, the passed string for
either must be stored in PROGMEM
(program memory, i.e. with the program in
Flash). Both parameters can be passed to the constructor when initializing the
CLI
object like this:
const char CLI_banner[] PROGMEM = "My Microcontroller v1.0 CLI";
const char CLI_prompt[] PROGMEM = "mm> ";
CLI CLI(Serial, CLI_banner, CLI_prompt);
Currently both must hence be hardcoded static strings and there is no way to make them dynamic or change them. This is a conscious design choice to reduce the size of the code and the memory it requires. In case you have a good use case to reconsider this decision, let's discuss and for that please do (raise an issue here)[https://gitlab.lindenaar.net/arduino/CLI/issues].
Stream
CLI is a The CLI Object implements a Stream so it can be used as well to interact with
the user, both from a command as well as from your main loop. Besides the CLI
implementation described in this document and the print()
family of functions,
it also provides:
-
print_P (const char *str PROGMEM)
to print a string stored inPROGMEM
-
print2digits(uint8_t num, char filler = '0', uint8_t base=10)
to print a number with at least 2 digits (useful forHEX
bytes) -
print_mem(uint16_t addr, const uint8_t buff[], uint8_t len, uint8_t width=16)
to dump a memory block in HEX and ASCII (addr
is the startaddress to print)
Implementing a Command
CLI commands are implemented as separate classes that register themselves with the CLI upon instantiation. The actions to implement for a command are:
- instantiate - the command's class is instantiated and registers with a CLI
- set parameters - the command parses the user's parameters for the execution
- execute - the command performs it's tasks using the parameters provided.
To implement a new CLI command, one should inherit from CLI_Command
class CLI_Command {
public:
CLI_Command(CLI &cli, const char *command PROGMEM,
const char *description PROGMEM,
const char *help PROGMEM = NULL);
virtual bool setparams(const char *params);
virtual bool execute(CLI &cli) = 0;
};
and implement it's public methods. At least a constructor (calling the one from
CLI_Command
) and the execute()
method must be implemented. The class has a
default implementation for the setparams()
method that accepts no parameters
that can be used for commands that do need parameters. The details of the
implementation steps are covered in the next sections to demonstrate how to
implement a "hello" command accepting a parameter and printing that.
Implementation (Class Definition)
The first step for the implementation of our "hello" command is to define a
class Hello_Command
inheriting from CLI_Command
. In this case we implement
all three methods as we want to accept parameter(s) and need the private
variable _params
to store it in setparams()
to be used by execute()
. The
definition of this basic class looks like:
class Hello_Command : CLI_Command {
const char *_params;
public:
Hello_Command(CLI &cli);
bool setparams(const char *params);
bool execute(CLI &cli);
};
Although it is possible to define the method in the definition, in this example they will be defined separately first. See the full code at the end of this section that implements the methods inline.
Instantiate (Class constructor)
The implementation of the constructor can be very simple; call the constructor
of CLI_Command
with the following parameters:
- instance of
CLI
class to register with (should be passed as parameter) - (static) string in
PROGMEM
with the name of the command - (static) string in
PROGMEM
with a short (1-line) command description - (static) string in
PROGMEM
with additional usage information
below the implementation of the example "hello" command with empty constructor
(as no functional initiation is required) only calling the parent CLI_Command
with the above parameters:
Hello_Command::Hello_Command(CLI &cli) : CLI_Command(&cli,
PSTR("hello"),
PSTR("Print an \"Hello\" greeting message"),
PSTR("Usage:\thello <name>\n"
"Where:\t<name>\ta string to include in the greeting")) { };
The above uses the PSTR()
macro to inline define the static strings for the
command name, description and help text. These normally are static and hence
hardcoded. The base implementation of the Command
class takes care of storing
the references and making them available. It will also register the command with
the CLI
instance provided. As already mentioned, the library assumes these are
in PROGMEM
so please make sure your sketch stores them there.
setparams(const char *params)
method)
Set parameters (When the user invokes a command, the setparams()
method is called with the
parameters the user provided after the command. This allows the command to
parse the parameters provided and do whatever is necessary for the command to
use this information (i.e. store it in an efficient way for execute()
).
The setparams()
method is always called once when the user enters a command so
that it can ensure that everything is ready for the command's execute()
method
to be invoked. It should return true
in case the parameters are valid. In case
setparams()
returns false
, the execution of the command is aborted and its
execute()
is never called but a standard error message is given instead.
The params
provided is a pointer to the start of the parameters with trailing
spaces removed and terminated with a char(0)
. In case no (or only whitespace)
parameters were provided, this method is called with NULL
so an Implementation
does not have to check for empty strings. As the CLI should not block the main
flow, make sure the parsing is efficient and keep it simple so that this method
(which is only called once for each command) does not take long.
A very simple implementation for setparams()
is provided below for our hello
command. This simply stores the pointer of the parameter string provided an will
result in a true
result in case a parameter is provided or a false
result
when the user did not provide any parameters.
bool Hello_Command::setparams(const char *params) {
_params = params;
return (params);
}
Often the parse will more complex as it is needs to parse the string provided. The design choice to have this implemented in the command is that this provides the greatest flexibility and does not assume anything w.r.t. how or which parameters are passed. The library does include a number of support functions that can be used to parse parameters and encode them in flags.
Please note that any attribute / variable used to store parameters are global (part of the object memory) so will eat up ram. Use them wisely so you don't run out of RAM on the smaller Arduino platforms that have only 2Kb or RAM.
execute(CLI &cli)
method)
Execute logic (The execute()
method contains the actual implementation of the command. It is
called with a reference to the CLI
so that there is no need to store that in
the object. To support a real-time implementation even if the command needs a
longer time (or is waiting), the implementation can return true
to pause the
current invocation of the execute
method and will be called in the next main
loop()
cycle again. This allows for returning to the main loop in between of
waiting or execution of the command so that the main loop can continue as well.
Before execute()
is the first time, setparams()
is called once to process
parameters from the user and perform initialization or setup needed. As long
as execute()
returns true
it will continue to be invoked again until it
returns false
to signal that the command's execution is complete. The Command
Line Interface will only process user input again when the command is completed
(so a command can also prompt the user for additional input)
Below the implementation of the hello
command, which is pretty simple as it
only prints "Hello" (stored in PROGMEM
followed by the string the user
provided. It then returns false
to signal to the CLI
that it is done.
bool Hello_Command::execute(CLI &cli) {
cli.print_P(PSTR("Hello "));
cli.println(_params);
return false;
}
Commands can be as complex as needed. For more extensive examples, please have a
look at the provided examples and built-in commands in Commands.cpp
.
Full Example Sketch for the Hello command
Below the full example sketch built up in the previous sections with the methods implemented inline:
#include <CLI.h>
class Hello_Command : CLI_Command {
const char *_params;
public:
Hello_Command(CLI &cli) :
CLI_Command(cli,
PSTR("hello"),
PSTR("Print an \"Hello\" greeting message"),
PSTR("Usage:\thello <name>\n"
"Where:\t<name>\tstring to include in greeting")) { };
bool setparams(const char *params) {
_params = params;
return (params);
}
bool execute(CLI &cli) {
cli.print_P(PSTR("Hello "));
cli.println(_params);
return false;
}
};
// Initialize the Command Line Interface
const char CLI_banner[] PROGMEM = "Hello CLI v1.0";
CLI CLI(Serial, CLI_banner); // Initialize CLI, telling it to attach to Serial
Hello_Command Hello(CLI); // Initialize/Register above defined hello command
Help_Command Help(CLI); // Initialize/Register (built-in) help command
// the setup function runs once when you reset or power the board
void setup() {
// Initialize digital pin LED_BUILTIN as an output.
pinMode(LED_BUILTIN, OUTPUT);
// Initialize the Serial port for the CLI
while (!Serial); // For Leonardo: wait for serial USB to connect
Serial.begin(9600);
}
// the loop function runs over and over again forever
void loop() {
// handle CLI, if this returns true a command is running so skip code block
if (CLI.process()) {
digitalWrite(LED_BUILTIN, HIGH); // turn LED on when processing CLI command
} else {
digitalWrite(LED_BUILTIN, LOW); // turn LED off when CLI command not active
// Code to run when no command is executing, make sure it is non-blocking...
}
// Code to execute every loop goes here, make sure it is non-blocking...
}
I hope this clarifies how this library can and should be used. In case you find any issues with the documentation, code or examples, please do raise an issue here.
Parser Support Functions
The library contains a number of support functions to ease with building a
parser for command parameters. These still need to be documented but can be found
already in CLI_Utils.cpp
within the library and are used by the built-in
commands found in Commands.cpp
and exampled included.
License
This library, documentation and examples are free software: you can redistribute them and/or modify them under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This script, documentation and configuration examples are distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, download it from http://www.gnu.org/licenses/.