ESP Embedded Rust: Ping CLI App Part 1
Tags: cli, esp32, tutorial, rust
Published: 2024-02-17 11:36:49

## Introduction

In the last blog post, I demonstrated the basic usage of `ping::EspPing`. Also in the [post](https://apollolabsblog.hashnode.dev/esp-embedded-rust-command-line-interface) of the week before that, I went through the process of creating a command line interface over UART. I figured, why not combine both to create a Ping CLI app replica?! As a result, in this post, a replica of a CLI ping application will be created. So that it's not overwhelming, the process is going to be broken up into two posts. In the first post, the basic framework of the app will be created. Next week, in the second post, more features will be added.

#### 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**

The content of this post is heavily dependent on the following past posts:

* [ESP Embedded Rust: Command Line Interface](https://apollolabsblog.hashnode.dev/esp-embedded-rust-command-line-interface)
    
* [Edge IoT with Rust on ESP: Ping!](https://apollolabsblog.hashnode.dev/edge-iot-with-rust-on-esp-ping)
    
* [Edge IoT with Rust on ESP: Connecting WiFi](https://apollolabsblog.hashnode.dev/edge-iot-with-rust-on-esp-connecting-wifi)
    

### **💾 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 then it means that 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/389975860067518465).

### **🛠 Hardware Setup**

#### Materials

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

## **👨‍🎨 Software Design**

This is a description of the app we're going to build:

```plaintext
Ping is a utility that sends ICMP Echo Request packets to a specified network host 
(either identified by its IP address or hostname) to test connectivity and measure round-trip time.

Usage: ping [options] <hostname/IP>
            
Options:
  -c, --count <number>     Number of ICMP Echo Request packets to send (default is 4).
  -i, --interval <seconds> Set the interval between successive ping packets in seconds.
  -t, --timeout <seconds>  Specify a timeout value for each ping attempt.
  -s, --size <bytes>       Set the size of the ICMP packets.
            
Examples:
  ping 192.168.1.1          # Ping the IP address 192.168.1.1
  ping example.com          # Ping the hostname 'example.com'
  ping -c 10 google.com     # Send 10 ping requests to google.com
  ping -i 0.5 -s 100 example.com  # Ping with interval of 0.5 seconds and packet size of 100 bytes to 'example.com'
```

This description is what will appear when `help ping` is entered. Also, the following is the type of output we want to recreate:

![pingout](https://cdn.hashnode.com/res/hashnode/image/upload/v1708101110294/048fd335-3691-49ed-a4c4-0ac01a57a045.png)

To create the application, the following steps are followed:

### 🐾 Step 1: Setup CLI Root Menu & Callback

In this first step, the following tasks will be accomplished:

1. The `ping` command `Item` needs to be added to the root menu `Menu` struct.
    
2. The callback function for `ping` setup.
    

### 🐾 Step 2: CLI Start-Up

The following are the actions that the application needs to take before spawning the CLI interface and invoking any commands:

1. Configure, Instantiate, and Connect to WiFi.
    
2. Configure and Instantiate UART
    
3. Instantiate and run the CLI runner with the root menu.
    

### 🐾 Step 3: Create Ping App Logic

The app logic will be contained in the `ping` command callback function. This is the logic that will be executed when the `ping` command is invoked. The following are the app logic steps:

1. Retrieve & Process CLI input
    
2. Instantiate `EspPing`
    
3. Setup `EspPing` Configuration
    
4. Perform `ping` and update CLI
    

For this week's post, in step 1, the only input that will be processed is the ip address. Hostname and option processing capability will be added in the next post. Additionally, `ping` stats printing will be added next week.

## **👨‍💻 Code Implementation**

### **📥 Crate Imports**

In this implementation, the following crates are required:

* The `esp_idf_hal` crate to import the peripherals needed for `uart`.
    
* The `esp_idf_svc` crate to import the device services needed for `wifi` and `ping`.
    
* The `menu` crate for creating the CLI.
    
* The `std::str` crate to import the `FromStr` abstraction.
    

```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 esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::ipv4::Ipv4Addr;
use esp_idf_svc::nvs::EspDefaultNvsPartition;
use esp_idf_svc::ping::{Configuration as PingConfiguration, EspPing};
use esp_idf_svc::wifi::{AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi};
use menu::*;
use std::fmt::Write;
use std::str::FromStr;
```

### 🐾 Step 1: Setup CLI Root Menu & Callback

**1️⃣ Add the** `ping` **command** `Item` **to root menu:** similar to what was done in the CLI post with the `hw` command for the hello app, a new `&Item` is added for `ping` in the `ROOT_MENU`. Note that the callback function name is `ping_app` also there's only one `parameter_name` that will be recognized which is `"hostname/IP"` . Also, the `help` message details how the command will work. Be mindful that the options are included in the description although not supported yet.

```rust
&Item {
    item_type: ItemType::Callback {
        function: ping_app,
        parameters: &[Parameter::Mandatory {
            parameter_name: "hostname/IP",
            help: Some("IP address or hostname"),
     }],
    },
    command: "ping",
    help: Some("
    Ping is a utility that sends ICMP Echo Request packets to a specified network host 
    (either identified by its IP address or hostname) to test connectivity and measure round-trip time.

    Usage: ping [options] <hostname/IP>
            
    Options:
      -c, --count <number>     Number of ICMP Echo Request packets to send (default is 4).
      -i, --interval <seconds> Set the interval between successive ping packets in seconds.
      -t, --timeout <seconds>  Specify a timeout value for each ping attempt.
      -s, --size <bytes>       Set the size of the ICMP packets.
      -h, --help               Display this help message and exit.
            
    Examples:
      ping 192.168.1.1          # Ping the IP address 192.168.1.1
      ping example.com          # Ping the hostname 'example.com'
      ping -c 10 google.com     # Send 10 ping requests to google.com
      ping -i 0.5 -s 100 example.com  # Ping with interval of 0.5 seconds and packet size of 100 bytes to 'example.com'
     "),
}
```

**2️⃣ Create the callback function:** The callback function named `ping_app` for the `ping` command specified in the `ROOT_MENU` `Item` is implemented as follows:

```rust
fn ping_app<'a>(
    _menu: &Menu<UartDriver>,
    item: &Item<UartDriver>,
    args: &[&str],
    context: &mut UartDriver,
) {
    // App code goes here
}
```

We're going to keep it empty for now, and fill in the logic implementation in the last step.

### 🐾 Step 2: CLI Start-Up

1️⃣ **Configure, Instantiate, and Connect to WiFi:** This involves the same steps taken in the [wifi post](https://apollolabsblog.hashnode.dev/edge-iot-with-rust-on-esp-connecting-wifi) to connect to WiFi. Inside the `main` function, the following code is added:

```rust
let peripherals = Peripherals::take().unwrap();
let sysloop = EspSystemEventLoop::take()?;
let nvs = EspDefaultNvsPartition::take()?;

let mut wifi = BlockingWifi::wrap(
    EspWifi::new(peripherals.modem, sysloop.clone(), Some(nvs))?,
    sysloop,
)?;

wifi.set_configuration(&Configuration::Client(ClientConfiguration {
    ssid: "Wokwi-GUEST".try_into().unwrap(),
    bssid: None,
    auth_method: AuthMethod::None,
    password: "".try_into().unwrap(),
    channel: None,
}))?;

// Start Wifi
wifi.start()?;

// Connect Wifi
wifi.connect()?;

// Wait until the network interface is up
wifi.wait_netif_up()?;

println!("Wifi Connected");
```

**2️⃣ Configure and Instantiate UART:** This and the following step are identical to what was accomplished in the [CLI post](https://apollolabsblog.hashnode.dev/esp-embedded-rust-command-line-interface). Here is the code associated with this step:

```rust
// 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();
```

**3️⃣ Instantiate and run the CLI runner with the root menu**: Again, following the the [CLI post](https://apollolabsblog.hashnode.dev/esp-embedded-rust-command-line-interface), this is the associated 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);

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]);
}
```

### 🐾 Step 3: Create Ping App Logic

**1️⃣ Retrieve & Process CLI input:** In the `ping_app` callback function, the first order of action will be to recover the user input. This is done using the `argument_finder` function in the `menu` crate. For now, the only entry supported is an IP address. Given that the retrieved IP address entry is a `&str` type, it needs to be converted to an `EspPing` compatible `Ipv4Addr` type using the `from_str` associated method which returns a `Result`. Finally, in case the user enters incorrect input, this can be handled by doing a pattern match on the `from_str` `Result`. Here is the code:

```rust
// Retreieve CLI Input
let ip_str = argument_finder(item, args, "hostname/IP").unwrap().unwrap();

// Process Input - Convert &str type to Ipv4Addr
let ip = Ipv4Addr::from_str(ip_str);

// Process Input - Make sure address formant is correct
let addr = match ip {
    Ok(addr) => addr,
    Err(_) => {
        writeln!(context, "Address error, try again").unwrap();
        return;
    }
};
```

**2️⃣ Instantiate** `EspPing`: This is a single-line action same to what was done before:

```rust
let mut ping = EspPing::new(0_u32);
```

**3️⃣ Setup** `EspPing` **Configuration**: for this post, a default configuration is going to be used. In next week's post, the configuration will be modified to accommodate any user-entered options.

```rust
let ping_config = &PingConfiguration::default();
```

**4️⃣ Perform** `ping` **and update CLI**: This is the part where the output of the ping app is replicated. First, we need to print `Pinging [IP address] with [bytes sent] bytes of data` . The IP address is the one entered by the user and the number of bytes sent is inside the `ping_config` struct.

```rust
// Update CLI
// Pinging {IP} with {x} bytes of data
writeln!(
    context,
    "Pinging {} with {} bytes of data\n",
    ip_str, ping_config.data_size
)
.unwrap();
```

Afterward, a ping needs to be performed 4 times and the output of each ping is reported in the format `Reply from [IP Address]: bytes=[bytes received] time=[response duration] TTL=[timeout duration]`. Here's the associated code:

```rust
// Ping 4 times and print results
for _n in 1..=4 {
    let summary = ping.ping(addr, ping_config).unwrap();

    writeln!(
        context,
        "Reply from {}: bytes = {}, time = {:?}, TTL = {:?}",
        ip_str, summary.received, summary.time, ping_config.timeout
    )
    .unwrap();
}
```

Thats it!

## **🧪 Testing**

Since the current version supports only IP addresses, for the sake of testing, local network addresses can be pinged if using physical hardware. If you desire to test with internet addresses or on Wokwi some possible addresses to ping include the following:

* **OpenDNS**: 208.67.222.222 and 208.67.220.220
    
* **Cloudflare**: 1.1.1.1 and 1.0.0.1
    
* **Google DNS**: 8.8.8.8 and 8.8.4.4
    

## **📱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/389975860067518465).

```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 esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::ipv4::Ipv4Addr;
use esp_idf_svc::nvs::EspDefaultNvsPartition;
use esp_idf_svc::ping::{Configuration as PingConfiguration, EspPing};
use esp_idf_svc::wifi::{AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi};
use menu::*;
use std::fmt::Write;
use std::str::FromStr;

// 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 the help for the hello, name hw command!"),
        },
        &Item {
            item_type: ItemType::Callback {
                function: ping_app,
                parameters: &[Parameter::Mandatory {
                    parameter_name: "hostname/IP",
                    help: Some("IP address or hostname"),
                }],
            },
            command: "ping",
            help: Some("
            Ping is a utility that sends ICMP Echo Request packets to a specified network host 
            (either identified by its IP address or hostname) to test connectivity and measure round-trip time.

            Usage: ping [options] <hostname/IP>
            
            Options:
              -c, --count <number>     Number of ICMP Echo Request packets to send (default is 4).
              -i, --interval <seconds> Set the interval between successive ping packets in seconds.
              -t, --timeout <seconds>  Specify a timeout value for each ping attempt.
              -s, --size <bytes>       Set the size of the ICMP packets.
              -h, --help               Display this help message and exit.
            
            Examples:
              ping 192.168.1.1          # Ping the IP address 192.168.1.1
              ping example.com          # Ping the hostname 'example.com'
              ping -c 10 google.com     # Send 10 ping requests to google.com
              ping -i 0.5 -s 100 example.com  # Ping with interval of 0.5 seconds and packet size of 100 bytes to 'example.com'
            "),
        },
    ],
    entry: None,
    exit: None,
};

fn main() -> anyhow::Result<()> {
    // Take Peripherals
    let peripherals = Peripherals::take().unwrap();
    let sysloop = EspSystemEventLoop::take()?;
    let nvs = EspDefaultNvsPartition::take()?;

    let mut wifi = BlockingWifi::wrap(
        EspWifi::new(peripherals.modem, sysloop.clone(), Some(nvs))?,
        sysloop,
    )?;

    wifi.set_configuration(&Configuration::Client(ClientConfiguration {
        ssid: "Wokwi-GUEST".try_into().unwrap(),
        bssid: None,
        auth_method: AuthMethod::None,
        password: "".try_into().unwrap(),
        channel: None,
    }))?;

    // Start Wifi
    wifi.start()?;

    // Connect Wifi
    wifi.connect()?;

    // Wait until the network interface is up
    wifi.wait_netif_up()?;

    println!("Wifi Connected");

    // 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 command
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();
}

// Callback function for ping command
fn ping_app<'a>(
    _menu: &Menu<UartDriver>,
    item: &Item<UartDriver>,
    args: &[&str],
    context: &mut UartDriver,
) {
    // Retreieve CLI Input
    let ip_str = argument_finder(item, args, "hostname/IP").unwrap().unwrap();

    // Process Input - Convert &str type to Ipv4Addr
    let ip = Ipv4Addr::from_str(ip_str);

    // Process Input - Make sure address formant is correct
    let addr = match ip {
        Ok(addr) => addr,
        Err(_) => {
            writeln!(context, "Address error, try again").unwrap();
            return;
        }
    };

    // Create EspPing instance
    let mut ping = EspPing::new(0_u32);

    // Setup Ping Config
    let ping_config = &PingConfiguration::default();

    // Update CLI
    // Pinging {IP} with {x} bytes of data
    writeln!(
        context,
        "Pinging {} with {} bytes of data\n",
        ip_str, ping_config.data_size
    )
    .unwrap();

    // Ping 4 times and print results
    // Reply from {IP}: bytes={summary.recieved} time={summary.time} TTL={summary.timeout}
    for _n in 1..=4 {
        let summary = ping.ping(addr, ping_config).unwrap();

        writeln!(
            context,
            "Reply from {}: bytes = {}, time = {:?}, TTL = {:?}",
            ip_str, summary.received, summary.time, ping_config.timeout
        )
        .unwrap();
    }
}
```

## Conclusion

In this post, a ping CLI application replica was built on a ESP32C3 using Rust and the supporting `std` library crates. The current version is limited to pinging IP addresses only. In the next blog post, hostnames and options support will be added. Additionally, ping statistics will be reported as part of the output. 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%}