Command Line Interface for embedded systems
Demo of CLI running on Arduino Nano. Memory usage: 16KiB of ROM and 0.6KiB of static RAM. Most of static RAM is used by help strings.
Dual-licensed under Apache 2.0 or MIT.
This library is not stable yet, meaning it's API is likely to change. Some of the API might be a bit ugly, but I don't see a better solution for now. If you have suggestions - open an Issue or a Pull Request.
embedded_io::Write
as output stream, input bytes are given one-by-one)Add embedded-cli
and necessary crates to your app:
[dependencies]
embedded-cli = "0.2.1"
embedded-io = "0.6.1"
ufmt = "0.2.0"
Define a writer that will be used to output bytes:
struct Writer {
// necessary fields (for example, uart tx handle)
};
impl embedded_io::ErrorType for Writer {
// your error type
}
impl embedded_io::Write for Writer {
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
todo!()
}
fn flush(&mut self) -> Result<(), Self::Error> {
todo!()
}
}
Build a CLI, specifying how much memory to use for command buffer (where bytes are stored until user presses enter) and history buffer (so user can navigate with up/down keypress):
let (command_buffer, history_buffer) = unsafe {
static mut COMMAND_BUFFER: [u8; 32] = [0; 32];
static mut HISTORY_BUFFER: [u8; 32] = [0; 32];
(COMMAND_BUFFER.as_mut(), HISTORY_BUFFER.as_mut())
};
let mut cli = CliBuilder::default()
.writer(writer)
.command_buffer(command_buffer)
.history_buffer(history_buffer)
.build()
.ok()?;
In this example static mut buffers were used, so we don't use stack memory.
Note that we didn't call unwrap()
. It's quite important to keep embedded code
without panics since every panic adds quite a lot to RAM and ROM usage. And
most embedded systems don't have a lot of it.
Define you command structure with enums and derive macro:
use embedded_cli::Command;
#[derive(Command)]
enum Base<'a> {
/// Say hello to World or someone else
Hello {
/// To whom to say hello (World by default)
name: Option<&'a str>,
},
/// Stop CLI and exit
Exit,
}
Doc-comments will be used in generated help.
And you're ready to provide all incoming bytes to cli and handle commands:
use ufmt::uwrite;
// read byte from somewhere (for example, uart)
// let byte = nb::block!(rx.read()).void_unwrap();
let _ = cli.process_byte::<Base, _>(
byte,
&mut Base::processor(|cli, command| {
match command {
Base::Hello { name } => {
// last write in command callback may or may not
// end with newline. so both uwrite!() and uwriteln!()
// will give identical results
uwrite!(cli.writer(), "Hello, {}", name.unwrap_or("World"))?;
}
Base::Exit => {
// We can write via normal function if formatting not needed
cli.writer().write_str("Cli can't shutdown now")?;
}
}
Ok(())
}),
);
If you have a lot of commands it may be useful to split them into multiple enums and place their logic into multiple modules. This is also supported via command groups.
Create extra command enum:
#[derive(Command)]
#[command(help_title = "Manage Hardware")]
enum GetCommand {
/// Get current LED value
GetLed {
/// ID of requested LED
led: u8,
},
/// Get current ADC value
GetAdc {
/// ID of requested ADC
adc: u8,
},
}
Group commands into new enum:
#[derive(CommandGroup)]
enum Group<'a> {
Base(Base<'a>),
Get(GetCommand),
/// This variant will capture everything, that
/// other commands didn't parse. You don't need
/// to add it, just for example
Other(RawCommand<'a>),
}
And then process it in similar way:
let _ = cli.process_byte::<Group, _>(
byte,
&mut Group::processor(|cli, command| {
match command {
Group::Base(cmd) => todo!("process base command"),
Group::Get(cmd) => todo!("process get command"),
Group::Other(cmd) => todo!("process all other, not parsed commands"),
}
Ok(())
}),
);
You can check full arduino example here. There is also a desktop example that runs in normal terminal. So you can play with CLI without flashing a real device.
Command can have any number of arguments. Types of argument must implement FromArgument
trait:
struct CustomArg<'a> {
// fields
}
impl<'a> embedded_cli::arguments::FromArgument<'a> for CustomArg<'a> {
fn from_arg(arg: &'a str) -> Result<Self, &'static str>
where
Self: Sized {
todo!()
}
}
Library provides implementation for following types:
Open an issue if you need some other type.
CLI uses whitespace (normal ASCII whitespace with code 0x20
) to split input into command
and its arguments. If you want to provide argument, that contains spaces, just wrap it
with quotes.
Input | Argument 1 | Argument 2 | Notes |
---|---|---|---|
cmd abc def | abc | def | Space is treated as argument separator |
cmd "abc def" | abc def | To use space inside argument, surround it with quotes | |
cmd "abc\" d\\ef" | abc" d\ef | To use quotes or slashes, escape them with \ | |
cmd "abc def" test | abc def | test | You can mix quoted arguments and non-quoted |
cmd "abc def"test | abc def | test | Space between quoted args is optional |
cmd "abc def""test 2" | abc def | test 2 | Space between quoted args is optional |
When using Command
derive macro, it automatically generates help from doc comments:
#[derive(Command)]
enum Base<'a> {
/// Say hello to World or someone else
Hello {
/// To whom to say hello (World by default)
name: Option<&'a str>,
},
/// Stop CLI and exit
Exit,
}
List all commands with help
:
$ help
Commands:
hello Say hello to World or someone else
exit Stop CLI and exit
Get help for specific command with help <COMMAND>
:
$ help hello
Say hello to World or someone else
Usage: hello [NAME]
Arguments:
[NAME] To whom to say hello (World by default)
Options:
-h, --help Print help
Or with <COMMAND> --help
or <COMMAND> -h
:
$ exit --help
Stop CLI and exit
Usage: exit
Options:
-h, --help Print help
You'll need to begin communication (usually through a UART) with a device running a CLI. Terminal is required for correct experience. Following control sequences are supported:
If you run CLI through a serial port (like on Arduino with its UART-USB converter), you can use for example PuTTY or tio.
Memory usage depends on version of crate, enabled features and complexity of your commands. Below is memory usage of arduino example when different features are enabled. Memory usage might change in future versions, but I'll try to keep this table up to date.
Features | ROM, bytes | Static RAM, bytes |
---|---|---|
10182 | 274 | |
autocomplete |
12112 | 290 |
history |
12032 | 315 |
autocomplete history |
13586 | 331 |
help |
14412 | 544 |
autocomplete help |
16110 | 556 |
history help |
16402 | 585 |
autocomplete history help |
16690 | 597 |
This table is generated using this script. As table shows, enabling help adds quite a lot to memory usage since help usually requires a lot of text to be stored. Also enabling all features almost doubles ROM usage comparing to all features disabled.