MicroCli is a lightweight, fully asynchronous Rust terminal implementation compatible with VT100 terminals. It is designed specifically for bare-metal embedded systems. By leveraging Rust's async features, MicroCli can run efficiently even on microcontrollers with extremely limited resources.
- Broad microcontroller compatibility: Leveraging embedded-hal, MicroCli supports a comprehensive range of microcontrollers, including stm32, nRF, rp2040(w), esp32, etc
- Asynchronous architecture: Thanks to its asynchronous design, MicroCli does not block operations, making it suitable for small microcontrollers with limited resources.
- VT100 compatibility: MicroCli is compatible with VT100 terminals, ensuring seamless integration with standard terminal emulators.
- No dynamic memory allocation: MicroCli avoids dynamic memory allocation, preventing memory leaks and ensuring stability in resource-constrained environments.
- Extensible: MicroCli is designed to be easily extendable, allowing developers to add custom features as needed.
Due to its asynchronous nature, MicroCli requires an async executor like Embassy to run. Below is an example of how to set up and use MicroCli on an STM32F103C8T6 microcontroller.
/// Declare your async command handler functions
async fn led_cmd1<'a>(cli: &mut MCli<'a, BufferedUartTx<'a>, BufferedUartRx<'a>>) {
let argv = cli.argv();
// usage: led[1|2] on|off|toggle
if argv.len() != 2 {
cli.write(b"usage: led[1|2] on|off|toggle\r\n")
.await
.expect("write failed");
return;
}
match argv[1] {
b"on" => CNT_CHANNEL.try_send((1, LedCommand::On)).ok(),
b"off" => CNT_CHANNEL.try_send((1, LedCommand::Off)).ok(),
b"toggle" => CNT_CHANNEL.try_send((1, LedCommand::Toggle)).ok(),
_ => None,
};
}
async fn led_cmd2<'a>(cli: &mut MCli<'a, BufferedUartTx<'a>, BufferedUartRx<'a>>) {
let argv = cli.argv();
// usage: led[1|2] on|off|toggle
if argv.len() != 2 {
cli.write(b"usage: led[1|2] on|off|toggle\r\n")
.await
.expect("write failed");
return;
}
match argv[1] {
b"on" => CNT_CHANNEL.try_send((2, LedCommand::On)).ok(),
b"off" => CNT_CHANNEL.try_send((2, LedCommand::Off)).ok(),
b"toggle" => CNT_CHANNEL.try_send((2, LedCommand::Toggle)).ok(),
_ => None,
};
}
/// Define your CLI task using the cli_runner! macro
cli_runner!(
pub async fn cli_inner_task(
tx: BufferedUartTx<'_>,
rx: BufferedUartRx<'_>
);
host = "F103C8TC6",
cli_size = 32,
commands = [
("led1", "Control LED 1") => led_cmd1,
("led2", "Control LED 2") => led_cmd2,
]
);
/// Spawn the CLI task in your executor and run cli_inner_task inside it
#[embassy_executor::task]
async fn cli_task(tx: BufferedUartTx<'static>, rx: BufferedUartRx<'static>) {
cli_inner_task(tx, rx).await;
}
Once you have set up the CLI task, you can interact with it using a terminal emulator like picocom
, minicom
, or screen
. Below is an example of how to use picocom
to connect to the microcontroller and interact with the CLI.
# Example terminal session using picocom
$ picocom -b 115200 /dev/ttyUSB0
[F103C8TC6]$
[F103C8TC6]$ help
Available commands:
help - list available commands
led1 - Control LED 1
led2 - Control LED 2
[F103C8TC6]$ led
Unavailable commands
[F103C8TC6]$ led1
usage: led[1|2] on|off|toggle
[F103C8TC6]$ led1 on
[F103C8TC6]$ led1 off
[F103C8TC6]$ led2 toggle
[F103C8TC6]$ help
Available commands:
help - list available commands
led1 - Control LED 1
led2 - Control LED 2
[F103C8TC6]$