Introduction
In the last blog post, I demonstrated the basic usage of ping::EspPing
. Also in the post 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.
📚 Knowledge Pre-requisites
The content of this post is heavily dependent on the following past posts:
💾 Software Setup
All the code presented in this post is available on the apollolabs 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.
🛠 Hardware Setup
Materials
👨🎨 Software Design
This is a description of the app we're going to build:
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:
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:
The
ping
commandItem
needs to be added to the root menuMenu
struct.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:
Configure, Instantiate, and Connect to WiFi.
Configure and Instantiate UART
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:
Retrieve & Process CLI input
Instantiate
EspPing
Setup
EspPing
ConfigurationPerform
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 foruart
.The
esp_idf_svc
crate to import the device services needed forwifi
andping
.The
menu
crate for creating the CLI.The
std::str
crate to import theFromStr
abstraction.
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 theping
commandItem
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.
&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_MENUItem
is implemented as follows:
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 to connect to WiFi. Inside the main
function, the following code is added:
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. Here is the code associated with this step:
// 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, this is the associated code:
// 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_strResult
. Here is the code:
// 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️⃣ InstantiateEspPing
: This is a single-line action same to what was done before:
let mut ping = EspPing::new(0_u32);
3️⃣ SetupEspPing
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.
let ping_config = &PingConfiguration::default();
4️⃣ Performping
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.
// 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:
// 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 git repo. Also, the Wokwi project can be accessed here.
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 👇.