500 lines
22 KiB
Markdown
500 lines
22 KiB
Markdown
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](https://gitlab.lindenaar.net/arduino/CLI)
|
|
|
|
Copyright (c) 2019 Frederik Lindenaar. free for distribution under the
|
|
GNU License, see [below](#license)
|
|
|
|
|
|
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](https://gitlab.lindenaar.net/arduino/CLI/repository/archive.zip)
|
|
* Follow the documentation for the Arduino IDE on
|
|
[importing a .zip Library](https://www.arduino.cc/en/Guide/Libraries#toc4).
|
|
Alternatively, you can also extract the downloaded .zip and follow the steps
|
|
for [manual Installation](https://www.arduino.cc/en/Guide/Libraries#toc5)
|
|
|
|
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](https://www.arduino.cc/en/Guide/Libraries#toc5).
|
|
|
|
|
|
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].
|
|
|
|
#### CLI is a `Stream`
|
|
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 in `PROGMEM`
|
|
* `print2digits(uint8_t num, char filler = '0', uint8_t base=10)`
|
|
to print a number with at least 2 digits (useful for `HEX` 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:
|
|
1. instantiate - the command's class is instantiated and registers with a CLI
|
|
2. set parameters - the command parses the user's parameters for the execution
|
|
3. 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:
|
|
1. instance of `CLI` class to register with (should be passed as parameter)
|
|
2. (static) string in `PROGMEM` with the name of the command
|
|
3. (static) string in `PROGMEM` with a short (1-line) command description
|
|
4. (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.
|
|
|
|
#### Set parameters (`setparams(const char *params)` method)
|
|
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 logic (`execute(CLI &cli)` method)
|
|
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](https://gitlab.lindenaar.net/arduino/CLI/issues).
|
|
|
|
|
|
### 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.
|
|
|
|
|
|
<a name="license">License</a>
|
|
=============================
|
|
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/>.
|
|
|