Initial Check-in of CLI Library
This commit is contained in:
499
README.md
Normal file
499
README.md
Normal file
@@ -0,0 +1,499 @@
|
||||
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/>.
|
||||
|
||||
Reference in New Issue
Block a user