Connecting ESP32 to Wi-Fi with Embassy support

So, we have discussed the dependencies we needed for this exercise and the features that needed to be enabled. Next, we'll focus on the coding. First, we will look at the code to connect Wi-Fi using Embassy.

Helper Macro for StaticCell

In an embedded environment, the StaticCell crate is useful when you need to initialize a variable at runtime but require it to have a static lifetime. We will define a macro to create globally accessible static variables. This macro takes two arguments: the type of the variable and the value to initialize it with. The uninit function provides a mutable reference to the uninitialized memory, and we write the value into it.

#![allow(unused)]
fn main() {
// If you are okay with using a nightly compiler, you can use the macro provided by the static_cell crate: https://docs.rs/static_cell/2.1.0/static_cell/macro.make_static.html

macro_rules! mk_static {
    ($t:ty,$val:expr) => {{
        static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
        #[deny(unused_attributes)]
        let x = STATIC_CELL.uninit().write(($val));
        x
    }};
}
}

Initialization Steps

We initialize the heap with a size of 72 KiB (72 * 1024) using the esp_alloc::heap_allocator! macro.

#![allow(unused)]
fn main() {
esp_alloc::heap_allocator!(72 * 1024);
}

Let's initialize Embassy with the usual setup:

#![allow(unused)]
fn main() {
let timer0 = esp_hal::timer::timg::TimerGroup::new(peripherals.TIMG1);
esp_hal_embassy::init(timer0.timer0);
}

We need a random number for both the TLS configuration and network stack initialization, and both require a u64. However, since rng generates only u32 values, we generate two random numbers and place one in the most significant bits (MSB) and the other in the least significant bits (LSB) using bitwise operation:

#![allow(unused)]
fn main() {
let mut rng = Rng::new(peripherals.RNG);
let net_seed = rng.random() as u64 | ((rng.random() as u64) << 32);
let tls_seed = rng.random() as u64 | ((rng.random() as u64) << 32);
}

Initializing the Wi-Fi Controller

Load the Wi-Fi credentials from environment variables:

#![allow(unused)]
fn main() {
const SSID: &str = env!("SSID");
const PASSWORD: &str = env!("PASSWORD");
}

First, we initialize the TimerGroup required for setting up the Wi-Fi controller. This is almost the same as what we did in the non-async version. However, this time we use the mk_static! macro to initialize wifi_init with a static lifetime. Using static ensures that the variable stays alive for the entire duration of the program.

The reason why we do this is that we will be running the Wi-Fi network stack as an async task ("to process network events"), which requires the wifi_init variable to remain available throughout the program's execution.

#![allow(unused)]
fn main() {
let timg0 = TimerGroup::new(peripherals.TIMG0);

let wifi_init = &*mk_static!(
    EspWifiController<'static>,
    esp_wifi::init(timg0.timer0, rng.clone(), peripherals.RADIO_CLK).unwrap()
);
}

Next, we will call the new_with_mode function with the initialized wifi_init, the Wi-Fi peripheral instance, and the Wi-Fi mode we want to use, which is STA (Station).

#![allow(unused)]
fn main() {
let mut wifi = peripherals.WIFI;
let (wifi_interface, controller) = esp_wifi::wifi::new_with_mode(&wifi_init, wifi, WifiStaDevice).unwrap();
}

Initialize the network stack

Let's initialize the network stack from the embassy_net crate using the network interface obtained from the Wi-Fi controller, a random number as the seed, the DHCP configuration, and stack resources with a size of 3.

#![allow(unused)]
fn main() {
let dhcp_config = DhcpConfig::default();
// dhcp_config.hostname = Some(String::from_str("implRust").unwrap());

let net_config = embassy_net::Config::dhcpv4(dhcp_config);

let stack = &*mk_static!(
    Stack<WifiDevice<'_, WifiStaDevice>>,
    Stack::new(
        wifi_interface,
        net_config,
        mk_static!(StackResources<3>, StackResources::<3>::new()),
        net_seed
    )
);
}

Next, we will start two background tasks: the connection_task will maintain the Wi-Fi connection, while the net_task will run the network stack and handle network events.

#![allow(unused)]
fn main() {
spawner.spawn(connection_task(controller)).ok();
spawner.spawn(net_task(stack)).ok();
}

We'll shortly discuss what happens in these two tasks and check these function definitions. But first, let's complete the flow.

Access Website

We will wait for the Wi-Fi link to be up, then obtain the IP address. Finally, we call the access_website function with the network stack reference and the random number we generated for the HTTP client. We will explore the access_website function also in more detail shortly.

#![allow(unused)]
fn main() {
loop {
    if stack.is_link_up() {
        break;
    }
    Timer::after(Duration::from_millis(500)).await;
}

println!("Waiting to get IP address...");
loop {
    if let Some(config) = stack.config_v4() {
        println!("Got IP: {}", config.address);
        break;
    }
    Timer::after(Duration::from_millis(500)).await;
}

access_website(stack, tls_seed).await;
}

Wi-Fi connection tasks

The connection_task function manages the Wi-Fi connection by continuously checking the status, configuring the Wi-Fi controller, and attempting to reconnect if the connection is lost or not started.

  1. First, we check the Wi-Fi state. If it is in StaConnected, we wait there until it gets disconnected. If it gets disconnected, we move to the other steps in the loop.
  2. We check if the Wi-Fi controller is started. If not, we initialize the Wi-Fi client configuration with the SSID (Wi-Fi name) and password, and start it.
  3. Finally, we attempt to connect to the Wi-Fi.
#![allow(unused)]

fn main() {
#[embassy_executor::task]
async fn connection_task(mut controller: WifiController<'static>) {
    println!("start connection task");
    println!("Device capabilities: {:?}", controller.capabilities());
    loop {
        match esp_wifi::wifi::wifi_state() {
            WifiState::StaConnected => {
                // wait until we're no longer connected
                controller.wait_for_event(WifiEvent::StaDisconnected).await;
                Timer::after(Duration::from_millis(5000)).await
            }
            _ => {}
        }

        if !matches!(controller.is_started(), Ok(true)) {
            let client_config = Configuration::Client(ClientConfiguration {
                ssid: SSID.try_into().unwrap(),
                password: PASSWORD.try_into().unwrap(),
                ..Default::default()
            });
            controller.set_configuration(&client_config).unwrap();
            println!("Starting wifi");
            controller.start_async().await.unwrap();
            println!("Wifi started!");
        }
        println!("About to connect...");

        match controller.connect_async().await {
            Ok(_) => println!("Wifi connected!"),
            Err(e) => {
                println!("Failed to connect to wifi: {e:?}");
                Timer::after(Duration::from_millis(5000)).await
            }
        }
    }
}
}

Run the network stack

#![allow(unused)]
fn main() {
#[embassy_executor::task]
async fn net_task(stack: &'static Stack<WifiDevice<'static, WifiStaDevice>>) {
    stack.run().await
}
}