ESP Embedded Rust: Command Line Interface
Tags: rust, tutorial, beginners, programming
Published: 2024-02-03 20:05:32

## Introduction

Command line interfaces (CLI) in embedded applications are probably not as common as non-embedded ones. One reason is that embedded applications do not often require direct user input from a terminal, but rather through some other physical interface. As such, if a CLI is required in embedded there are different considerations.

In a typical non-embedded CLI, there is an underlying operating system (OS) with a CLI (in a shell) meant for accepting user input and passing commands to the OS itself. The OS in turn processes the input appropriately. As such, a command signifies a precompiled executable stored in some directory followed by a collection of arguments. The passed arguments are then processed by the named program when the OS executes it. The arguments are also typically allocated to dynamic memory.

In case a CLI is needed in embedded, things take a different turn. First, an underlying OS with a shell interface doesn't necessarily exist, but rather an already running application or RTOS. Second, the interface itself could be on another host device. Third, dynamic allocation is not desirable. This means that to create a CLI, logic is incorporated within an existing running application. This comes in the form of accepting commands over some logging interface like UART and processing them in the board.

In this post, I'm going to create a simple embedded CLI interface on an ESP32C3 device. However, the CLI logic not going to be created from scratch, but rather leveraging an existing crate. There are several `no-std` CLI crates that I encountered including; [`terminal-cli`](https://crates.io/crates/terminal_cli) , [`embedded-cli`](https://crates.io/crates/embedded-cli/0.1.0) , [`light-cli`](https://crates.io/crates/light-cli) , and `menu` . Looking at each of the crates, they all had friendly abstractions and great features. I ended up going for `menu` based on the number of downloads since it seemed to be the most popular.

#### If you find this post useful, and if Embedded Rust interests you, stay in the know by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com/subscribe %} Subscribe Now to The Embedded Rustacean{% endcta%}

### **📚 Knowledge Pre-requisites**

To understand the content of this post, you need the following:

* Basic knowledge of coding in Rust.
    
* Familiarity with UART and [how to set it up for the ESP32C3](https://apollolabsblog.hashnode.dev/esp32-standard-library-embedded-rust-uart-communication).
    

### **💾 Software Setup**

All the code presented in this post is available on the [**apollolabs ESP32C3**](https://github.com/apollolabsdev/ESP32C3) git repo. Note that if the code on the git repo is slightly different, it was modified to enhance the code quality or accommodate any HAL/Rust updates.

Additionally, the full project (code and simulation) is available on Wokwi [**here**](https://wokwi.com/projects/388708808145859585).

### **🛠 Hardware Setup**

#### Materials

* [**ESP32-C3-DevKitM**](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html)
    
    ![Dev Kit](https://cdn.hashnode.com/res/hashnode/image/upload/v1681796942083/da81fb7b-1f90-4593-a848-11c53a87821d.jpeg)
    

## **👨‍🎨** Software Design

### **💻** The Application

For this post, I'm going to create a simple application called hello with a command `hw`. `hw` takes one argument and its a person's name. The application would then print hello followed by the name given. Here's a sample:

```bash
> hw Omar
Hello, Omar!
```

Also, the application would support a `help` command to provide a summary of commands the terminal supports. Also there is a `help` option for each command providing the user with information about the command.

### 📦 The `menu` Crate

The `menu` crate consists of four main components:

1. **The** `Menu` **struct**: Each `Menu` has a name or `label` , optional functions to call on entry and exit of the `Menu` and, most importantly, a collection of `Item`s.
    
2. **The** `Item` **struct**: `Item`s can be viewed as command buckets within a `Menu`. `Item` members include an `ItemType` which specifies the type of the `Item` , a command string, and a help message associated with the command. `ItemType` is also a struct that specifies whether the `Item` opens up another sub `Menu`, or calls a function upon the invocation of the command.
    
3. **The** `Runner` **struct**: This is the struct that handles all incoming input. When our application is executing incoming byte characters will be passed to an instantiation of this struct for processing. Obviously, in `Runner` instantiation, the the `Menu` would be required in addition to a source for the incoming bytes (Ex. UART channel).
    
4. **The Callback Functions**: These are the functions that are called upon the invocation of a command. In these functions the logic associated with each command will be executed.
    

### 👣 Implementation Steps

Following the earlier description, these are the steps we need to take:

1. Define the Root Menu
    
2. Implement any Callback functions
    
3. Configure peripherals in `main`
    
4. Instantiate the `Runner` in `main`
    
5. Create the Application `loop`
    

## **👨‍💻** Code Implementation

### **📥 Crate Imports**

In this implementation the crates required are as follows:

* The `esp_idf_hal` crate to import the needed device hardware abstractions.
    
* `menu` to provide the menu CLI structs and abstractions.
    
* `std::fmt` to import the `Write` trait.
    

```rust
use esp_idf_hal::delay::BLOCK;
use esp_idf_hal::gpio;
use esp_idf_hal::peripherals::Peripherals;
use esp_idf_hal::prelude::*;
use esp_idf_hal::uart::*;
use menu::*;
use std::fmt::Write;
```

### 📝 Define the Root Menu

Before implementing the logic, we need to define our root `Menu`. Since this this a simple application, the `Menu` will have one `Item` that contains the `hw` command. `Menu` has the following definition:

```rust
pub struct Menu<'a, T>
where
    T: 'a,
{
    pub label: &'a str,
    pub items: &'a [&'a Item<'a, T>],
    pub entry: Option<MenuCallbackFn<T>>,
    pub exit: Option<MenuCallbackFn<T>>,
}
```

`label` is the label of the menu, `entry` and `exit` specify functions to call on entry and exit of the `Menu` , and `items` is an array of `Item` s. Also each `Item` has the following definition:

```rust
pub struct Item<'a, T>
where
    T: 'a,
{
    pub command: &'a str,
    pub help: Option<&'a str>,
    pub item_type: ItemType<'a, T>,
}
```

where `command` is the command text for a particular `Item` , `help` is the help message associated with `command`, and `item_type` is an enum that specifies what the type of the item is. `ItemType` is defined as follows:

```rust
pub enum ItemType<'a, T>
where
    T: 'a,
{
    Callback {
        function: ItemCallbackFn<T>,
        parameters: &'a [Parameter<'a>],
    },
    Menu(&'a Menu<'a, T>),
    _Dummy,
}
```

One can see that `Item` can be either a `Callback` that would call a function on the invocation of an `Item` `command`, and `parameters` that are passed to that command. Alternatively, an `Item` could be another `Menu` which specifies a sub-menu.

Given the above, for our application, we define a `const` `Menu` called `ROOT_MENU` as follows:

```rust
// CLI Root Menu Struct Initialization
const ROOT_MENU: Menu<UartDriver> = Menu {
    label: "root",
    items: &[&Item {
        item_type: ItemType::Callback {
            function: hello_name,
            parameters: &[Parameter::Mandatory {
                parameter_name: "name",
                help: Some("Enter your name"),
            }],
        },
        command: "hw",
        help: Some("This is an embedded CLI terminal. Check the summary for the list of supported commands"),
    }],
    entry: None,
    exit: None,
};
```

Note here that `ROOT_MENU` is given a `label` "`root`". Additionally, our application would have only one `Item` with `command` text "`hw`" that, when entered by the user, would invoke a `function` called `hello_name` .The application `Item` also takes only one parameter defined as `name` . Also I've decided to not call any functions on `entry` and `exit` of the `root` `Menu`. The associated `help` messages are also defined at the `Menu` and `Item` level.

### 👨‍💻 Implement the Callback Function

Given the earlier step, we now need to define the behaviour when the callback function `hello_name` associated with `hw` is called. However, we still need to know what are the arguments that will be passed to this function. As such, the `menu` crate defines the signature of the callback function `ItemCallbackFn` as follows:

```rust
pub type ItemCallbackFn<T> = fn(menu: &Menu<'_, T>, item: &Item<'_, T>, args: &[&str], context: &mut T);
```

Following this signature, we implement the function `hello_name` as follows:

```rust
fn hello_name<'a>(
    _menu: &Menu<UartDriver>,
    item: &Item<UartDriver>,
    args: &[&str],
    context: &mut UartDriver,
) {
    // Print to console passed "name" argument
    writeln!(
        context,
        "Hello, {}!",
        argument_finder(item, args, "name").unwrap().unwrap()
    )
    .unwrap();
}
```

Note that all the function is doing is using the `writeln` macro to print to the console. `writeln` accepts a ‘writer’, a format string, and a list of arguments. Arguments will be formatted according to the specified format string and the result will be passed to the writer. The writer is any type that implements the `Write` trait in which case its the `UartDriver` type for the `context` thats being passed to `hello_name` by the runner. Note how for the list of arguments and `argument_finder` function is being used. `argument_finder` is a `menu` crate function and it looks for the named parameter in the parameter list of the `Item`, then finds the correct argument. Note how `argument_finder` takes three arguments, the `Item` we want to look for an argument in, the list of arguments that have been passed to the CLI (`args`), and the argument we want to look for ("`name`").

### **🎛** Configure Peripherals

In the `main` the following code is used to instantiate and configure UART:

```rust
// Take Peripherals
let peripherals = Peripherals::take().unwrap();

// Configure UART
// Create handle for UART config struct
let config = config::Config::default().baudrate(Hertz(115_200));

// Instantiate UART
let mut uart = UartDriver::new(
    peripherals.uart0,
    peripherals.pins.gpio21,
    peripherals.pins.gpio20,
    Option::<gpio::Gpio0>::None,
    Option::<gpio::Gpio1>::None,
    &config,
)
.unwrap();
```

The detail behind how this code works has been covered in a prior post [here](https://apollolabsblog.hashnode.dev/esp32-standard-library-embedded-rust-uart-communication). If anything seems unclear I recommend referring back to the past post. Only thing to note here is that `uart0` is used with `gpio21` and `gpio20` for Tx and Rx pins. This is because this is the peripheral and the pins used to connect to a host on the [**ESP32-C3-DevKitM**](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html)**.**

### 🏃‍♂️ Instantiate the `Runner`

In the [`menu` crate documentation](https://docs.rs/menu/0.4.0/menu/struct.Runner.html), `Runner` has a `new` method with the following signature:

```rust
pub fn new(menu: Menu<'a, T>, buffer: &'a mut [u8], context: T) -> Runner<'a, T>
```

The first parameter is a root `Menu` (defined in the first step!), the second is a buffer that the `Runner` can use (`clibuf`), finally, `context` could be any type that implements `Write` (this is our instantiated `uart` handle) As a result, the runner is instantiated with the following code:

```rust
// Create a buffer to store CLI input
let mut clibuf = [0u8; 64];
// Instantiate CLI runner with root menu, buffer, and uart
let mut r = Runner::new(ROOT_MENU, &mut clibuf, uart);
```

> 📝 **Note**: The generic type `T` that appears in all prior structs seen earlier is defined by the context object that the `Runner` carries around. In our case its the `UartDriver` type.

### 🔄 The Main Loop

In the `main` `loop`, all that needs to be done is read one byte at a time from the UART channel. Remember that the UART channel is now part of the runner `context`. This is done using the UART `read` method (again, for a refresher check out [this](https://apollolabsblog.hashnode.dev/esp32-standard-library-embedded-rust-uart-communication) post). Every read byte is passed to the instantiated runner using the `input_byte` runner method.

```rust
loop {
    // Create single element buffer for UART characters
    let mut buf = [0_u8; 1];
    // Read single byte from UART
    r.context.read(&mut buf, BLOCK).unwrap();
    // Pass read byte to CLI runner for processing
    r.input_byte(buf[0]);
}
```

That's it for code!

## 📱Full Application Code

Here is the full code for the implementation described in this post. You can additionally find the full project and others available on the [**apollolabs ESP32C3**](https://github.com/apollolabsdev/ESP32C3) git repo. Also, the Wokwi project can be accessed [**here**](https://wokwi.com/projects/388709326938299393).

```rust
use esp_idf_hal::delay::BLOCK;
use esp_idf_hal::gpio;
use esp_idf_hal::peripherals::Peripherals;
use esp_idf_hal::prelude::*;
use esp_idf_hal::uart::*;
use menu::*;
use std::fmt::Write;

// CLI Root Menu Struct Initialization
const ROOT_MENU: Menu<UartDriver> = Menu {
    label: "root",
    items: &[&Item {
        item_type: ItemType::Callback {
            function: hello_name,
            parameters: &[Parameter::Mandatory {
                parameter_name: "name",
                help: Some("Enter your name"),
            }],
        },
        command: "hw",
        help: Some("This is an embedded CLI terminal. Check the summary for the list of supported commands"),
    }],
    entry: None,
    exit: None,
};

fn main() {
    // Take Peripherals
    let peripherals = Peripherals::take().unwrap();

    // Configure UART
    // Create handle for UART config struct
    let config = config::Config::default().baudrate(Hertz(115_200));

    // Instantiate UART
    let mut uart = UartDriver::new(
        peripherals.uart0,
        peripherals.pins.gpio21,
        peripherals.pins.gpio20,
        Option::<gpio::Gpio0>::None,
        Option::<gpio::Gpio1>::None,
        &config,
    )
    .unwrap();

    // This line is for Wokwi only so that the console output is formatted correctly
    uart.write_str("\x1b[20h").unwrap();

    // Create a buffer to store CLI input
    let mut clibuf = [0u8; 64];
    // Instantiate CLI runner with root menu, buffer, and uart
    let mut r = Runner::new(ROOT_MENU, &mut clibuf, uart);

    loop {
        // Create single element buffer for UART characters
        let mut buf = [0_u8; 1];
        // Read single byte from UART
        r.context.read(&mut buf, BLOCK).unwrap();
        // Pass read byte to CLI runner for processing
        r.input_byte(buf[0]);
    }
}

// Callback function for hw commans
fn hello_name<'a>(
    _menu: &Menu<UartDriver>,
    item: &Item<UartDriver>,
    args: &[&str],
    context: &mut UartDriver,
) {
    // Print to console passed "name" argument
    writeln!(
        context,
        "Hello, {}!",
        argument_finder(item, args, "name").unwrap().unwrap()
    )
    .unwrap();
}
```

## **Conclusion**

This post showed how to create a simple embedded command line interface over a UART channel. In the post, the application was created on the ESP32C3 using the `menu` crate. The application also leveraged the `esp-idf-hal` asbtractions. Have any questions? Share your thoughts in the comments below 👇.

#### If you found this post useful, and if Embedded Rust interests you, stay in the know by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com/subscribe %} Subscribe Now to The Embedded Rustacean{% endcta%}