How to Connect ESP32 to Wi-Fi and Access Websites
In this exercise, we will configure the ESP32 in STA mode to connect to your Wi-Fi. Then, we will make a request to a web server from the ESP32 and print the response in the system console. This code is based on the esp-hal examples.
Generate project using esp-generate
To create the project, use the esp-generate command. Run the following:
esp-generate --chip esp32 wifi-webfetch
This will open a screen asking you to select options. In order to Enable Wi-Fi, we will first need to enable "unstable" and "alloc" features. If you noticed, until you select these two options, you wont be able to enable Wi-Fi option. So select one by one
- First, select the option "Enable unstable HAL features."
- Select the option "Enable allocations via the esp-alloc crate."
- Now, you can enable "Enable Wi-Fi via the esp-radio crate."
Enable the logging feature also
- So, scroll to "Flashing, logging and debugging (espflash)" and hit Enter.
- Then, Select "Use defmt to print messages".
Just save it by pressing "s" in the keyboard.
Update dependencies
Add the following crate to the Cargo.toml file
blocking-network-stack = { git = "https://github.com/bjoernQ/blocking-network-stack.git", rev = "b3ecefc222d8806edd221f266999ca339c52d34e", default-features = false, features = [
"dhcpv4",
"tcp",
] }
heapless = { version = "0.9.1" }
blocking-network-stack is a crate that provides non-async networking primitives for TCP/UDP communication.
I downgraded embedded-io to version 0.6.1 (from 0.7.1, which was generated by esp-generate) due to a conflict issue.
embedded-io = { version = "0.6.1" }
esp-alloc crate
"A simple no_std heap allocator for RISC-V and Xtensa processors from Espressif. Supports all currently available ESP32 devices." The esp-alloc crate provides the global allocator for ESP SoCs.
Initialize Peripherals
We will create a helper function that initializes the ESP HAL and returns the peripherals instance. We will also set up a 72 KiB heap using the esp_alloc::heap_allocator! macro.
#![allow(unused)] fn main() { fn init_hardware() -> Peripherals { let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let peripherals = esp_hal::init(config); esp_alloc::heap_allocator!(size: 72 * 1024); peripherals } }
Initializing the Wi-Fi Controller
We need to provide the Wi-Fi name and password to our program. Instead of hardcoding these credentials in the code, we'll load them from environment variables. When running the program, we will pass the environment variables along with it.
#![allow(unused)] fn main() { const SSID: &str = env!("SSID"); const PASSWORD: &str = env!("PASSWORD"); }
First, let's initialize the TimerGroup and the Random Number Generator (RNG) required for setting up the Wi-Fi controller. The RNG is also used to initialize the non-async TCP/IP network stack, so we will use clone so that we can re-use it.
#![allow(unused)] fn main() { let timg0 = TimerGroup::new(peripherals.TIMG0); let rng = Rng::new(); }
We initialize the WiFi controller using the hardware timer, RNG, and clock peripheral. Next, we create a WiFi driver instance to handle network connections and manage various interfaces. Finally, we configure the device to operate in station (STA) mode, enabling it to connect to WiFi networks as a client.
#![allow(unused)] fn main() { esp_rtos::start(timg0.timer0); let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller"); let (mut wifi_controller, interfaces) = esp_radio::wifi::new(&radio_init, peripherals.WIFI, Default::default()) .expect("Failed to initialize Wi-Fi controller"); let mut device = interfaces.sta; }
SocketSet Initialization
We will create a SocketSet with storage for up to 3 sockets to manage multiple sockets, such as DHCP and TCP, within the stack.
#![allow(unused)] fn main() { let mut socket_set_entries: [SocketStorage; 3] = Default::default(); let mut socket_set = SocketSet::new(&mut socket_set_entries[..]); }
DHCP Socket
We will create a DHCP socket to request an IP address from a DHCP server. We can set the outgoing options, including the hostname using DHCP Client Option 12 (you can read more about DHCP options here). We will add the dhcp socket to the SocketSet we created earlier.
#![allow(unused)] fn main() { let mut dhcp_socket = smoltcp::socket::dhcpv4::Socket::new(); // we can set a hostname here (or add other DHCP options) dhcp_socket.set_outgoing_options(&[DhcpOption { kind: 12, data: b"implRust", }]); socket_set.add(dhcp_socket); // socket_set.add(smoltcp::socket::dhcpv4::Socket::new()); }
Initializing the Network Stack
Let's initialize the Stack from the smoltcp crate using the network interface and device (which we obtained from the Wi-Fi network interface we created earlier), the sockets, a closure to get time, and a random number.
#![allow(unused)] fn main() { let now = || Instant::now().duration_since_epoch().as_millis(); let mut stack = Stack::new( create_interface(&mut device), device, socket_set, now, rng.random(), ); }
Boilerplate functions to set up a network interface for the smoltcp TCP/IP stack using the Wi-Fi device (taken from the official esp-hal examples)
#![allow(unused)] fn main() { pub fn create_interface(device: &mut esp_wifi::wifi::WifiDevice) -> smoltcp::iface::Interface { // users could create multiple instances but since they only have one WifiDevice // they probably can't do anything bad with that smoltcp::iface::Interface::new( smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ethernet( smoltcp::wire::EthernetAddress::from_bytes(&device.mac_address()), )), device, timestamp(), ) } // some smoltcp boilerplate fn timestamp() -> smoltcp::time::Instant { smoltcp::time::Instant::from_micros( esp_hal::time::Instant::now() .duration_since_epoch() .as_micros() as i64, ) } }
Wi-Fi Operation Mode:
Next, we configure the Wi-Fi operation mode using the ModeConfig enum. Since we want the ESP32 to act as a Wi-Fi client, we use the Client variant. We need to provide the SSID (Service Set Identifier, which is the name of your Wi-Fi network) and the Wi-Fi password.
#![allow(unused)] fn main() { fn configure_wifi(controller: &mut WifiController<'_>) { controller .set_power_saving(esp_radio::wifi::PowerSaveMode::None) .unwrap(); let client_config = ModeConfig::Client( ClientConfig::default() .with_ssid(SSID.into()) .with_password(PASSWORD.into()), ); let res = controller.set_config(&client_config); println!("wifi_set_configuration returned {:?}", res); controller.start().unwrap(); println!("is wifi started: {:?}", controller.is_started()); } }
Wi-Fi Scanning and Connecting to the Access Point
Next, we perform a blocking Wi-Fi network scan with the default options. This means the program will pause and wait until the scan is complete. Once the scan is finished, it will return a list of available Wi-Fi access points. We then loop through the results and print each access point's information to the console.
#![allow(unused)] fn main() { fn scan_wifi(controller: &mut WifiController<'_>) { println!("Start Wifi Scan"); let scan_config = ScanConfig::default().with_max(10); let res = controller.scan_with_config(scan_config).unwrap(); for ap in res { println!("{:?}", ap); } } }
Connect Wi-Fi
Then, we print the supported capabilities of the controller, which will show only "Client" since that's what we configured.
After that, we call the connect method on the Wi-Fi controller. This will connect the ESP32 to the Wi-Fi network that we specified earlier.
We will continuously check in a loop if the Wi-Fi is connected. Once it connects successfully, we will proceed further.
#![allow(unused)] fn main() { fn connect_wifi(controller: &mut WifiController<'_>) { println!("{:?}", controller.capabilities()); println!("wifi_connect {:?}", controller.connect()); println!("Wait to get connected"); loop { match controller.is_connected() { Ok(true) => break, Ok(false) => {} Err(err) => panic!("{:?}", err), } } println!("Connected: {:?}", controller.is_connected()); } }
Obtain IP address
We need to obtain an IP address, so we first wait for the DHCP process to complete. We will continuously loop until the interface is up and we have successfully obtained an IP address.
#![allow(unused)] fn main() { fn obtain_ip(stack: &mut Stack<'_, esp_radio::wifi::WifiDevice<'_>>) { println!("Wait for IP address"); loop { stack.work(); if stack.is_iface_up() { println!("IP acquired: {:?}", stack.get_ip_info()); break; } } } }
At this point, the ESP32 should be successfully connected to the Wi-Fi network. Next, we will set up the necessary code to send an HTTP request to a website. Once the request is sent, the ESP32 will receive the HTML response from the website, and we will print the HTML content to the console.
HTTP Client
To send a web request to a website, we need an HTTP client. For this, we will use the smoltcp crate, to send a raw HTTP request over a TCP socket.
"smoltcp is a standalone, event-driven TCP/IP stack that is designed for bare-metal, real-time systems. smoltcp does not need heap allocation at all, is extensively documented, and compiles on stable Rust 1.80 and later."
TCP Socket
We will initialize the TCP socket with read and write buffers to handle incoming and outgoing data for the socket.
#![allow(unused)] fn main() { let mut rx_buffer = [0u8; 1536]; let mut tx_buffer = [0u8; 1536]; let socket = stack.get_socket(&mut rx_buffer, &mut tx_buffer); http_loop(socket) }
HTTP Loop - Send Get Request
Let's send an HTTP request with GET method to the website "www.mobile-j.de". This website is used in the esp-hal examples, which returns "Hello fellow Rustaceans!" with ferris ascii art. We'll use the IP address of the website to open a connection on port 80, then send the HTTP request.
#![allow(unused)] fn main() { println!("Making HTTP request"); socket.work(); let remote_addr = IpAddress::v4(142, 250, 185, 115); socket.open(remote_addr, 80).unwrap(); socket .write(b"GET / HTTP/1.0\r\nHost: www.mobile-j.de\r\n\r\n") .unwrap(); socket.flush().unwrap(); }
Read the response
Next, we read the response from the server with a 20-second timeout.
#![allow(unused)] fn main() { let deadline = Instant::now() + Duration::from_secs(20); let mut buffer = [0u8; 512]; while let Ok(len) = socket.read(&mut buffer) { // let text = unsafe { core::str::from_utf8_unchecked(&buffer[..len]) }; let Ok(text) = core::str::from_utf8(&buffer[..len]) else { panic!("Invalid UTF-8 sequence encountered"); }; println!("{}", text); if Instant::now() > deadline { println!("Timeout"); break; } } }
Close the socket
After the HTTP request is complete, we close the socket and wait for 5 seconds.
#![allow(unused)] fn main() { socket.disconnect(); let deadline = Instant::now() + Duration::from_secs(5); while Instant::now() < deadline { socket.work(); } }
Clone the existing project
You can also clone (or refer) project I created and navigate to the wifi-webfetch folder.
git clone https://github.com/ImplFerris/esp32-projects
cd esp32-projects/wifi-webfetch
How to run?
Normally, we would simply run cargo run --release, but this time we also need to pass the environment variables for the Wi-Fi connection.
SSID=YOUR_WIFI_NAME PASSWORD=YOUR_WIFI_PASSWORD cargo run --release
The Full code
#![no_std] #![no_main] #![deny( clippy::mem_forget, reason = "mem::forget is generally not safe to do with esp_hal types, especially those \ holding buffers for the duration of a data transfer." )] use blocking_network_stack::Stack; use embedded_io::{Read, Write}; use esp_hal::clock::CpuClock; use esp_hal::delay::Delay; use esp_hal::main; use esp_hal::peripherals::Peripherals; use esp_hal::rng::Rng; use esp_hal::time::{Duration, Instant}; use esp_hal::timer::timg::TimerGroup; use esp_println::{self as _, println}; use esp_radio::wifi::{ClientConfig, ModeConfig, ScanConfig, WifiController}; use smoltcp::iface::{SocketSet, SocketStorage}; use smoltcp::wire::{DhcpOption, IpAddress}; #[panic_handler] fn panic(_: &core::panic::PanicInfo) -> ! { loop {} } extern crate alloc; // This creates a default app-descriptor required by the esp-idf bootloader. // For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description> esp_bootloader_esp_idf::esp_app_desc!(); const SSID: &str = env!("SSID"); const PASSWORD: &str = env!("PASSWORD"); #[main] fn main() -> ! { // generator version: 1.0.0 let peripherals = init_hardware(); esp_alloc::heap_allocator!(#[unsafe(link_section = ".dram2_uninit")] size: 98767); let timg0 = TimerGroup::new(peripherals.TIMG0); let rng = Rng::new(); esp_rtos::start(timg0.timer0); let radio_init = esp_radio::init().expect("Failed to initialize Wi-Fi/BLE controller"); let (mut wifi_controller, interfaces) = esp_radio::wifi::new(&radio_init, peripherals.WIFI, Default::default()) .expect("Failed to initialize Wi-Fi controller"); let mut device = interfaces.sta; // let mut stack = setup_network_stack(device, &mut rng); let mut socket_set_entries: [SocketStorage; 3] = Default::default(); let mut socket_set = SocketSet::new(&mut socket_set_entries[..]); let mut dhcp_socket = smoltcp::socket::dhcpv4::Socket::new(); // we can set a hostname here (or add other DHCP options) dhcp_socket.set_outgoing_options(&[DhcpOption { kind: 12, data: b"implRust", }]); socket_set.add(dhcp_socket); // sta_socket_set.add(smoltcp::socket::dhcpv4::Socket::new()); let now = || Instant::now().duration_since_epoch().as_millis(); let mut stack = Stack::new( create_interface(&mut device), device, socket_set, now, rng.random(), ); configure_wifi(&mut wifi_controller); scan_wifi(&mut wifi_controller); connect_wifi(&mut wifi_controller); obtain_ip(&mut stack); let mut rx_buffer = [0u8; 1536]; let mut tx_buffer = [0u8; 1536]; let socket = stack.get_socket(&mut rx_buffer, &mut tx_buffer); http_loop(socket) } pub fn create_interface(device: &mut esp_radio::wifi::WifiDevice) -> smoltcp::iface::Interface { // users could create multiple instances but since they only have one WifiDevice // they probably can't do anything bad with that smoltcp::iface::Interface::new( smoltcp::iface::Config::new(smoltcp::wire::HardwareAddress::Ethernet( smoltcp::wire::EthernetAddress::from_bytes(&device.mac_address()), )), device, timestamp(), ) } // some smoltcp boilerplate fn timestamp() -> smoltcp::time::Instant { smoltcp::time::Instant::from_micros( esp_hal::time::Instant::now() .duration_since_epoch() .as_micros() as i64, ) } fn init_hardware() -> Peripherals { let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let peripherals = esp_hal::init(config); esp_alloc::heap_allocator!(size: 72 * 1024); peripherals } fn configure_wifi(controller: &mut WifiController<'_>) { controller .set_power_saving(esp_radio::wifi::PowerSaveMode::None) .unwrap(); let client_config = ModeConfig::Client( ClientConfig::default() .with_ssid(SSID.into()) .with_password(PASSWORD.into()), ); let res = controller.set_config(&client_config); println!("wifi_set_configuration returned {:?}", res); controller.start().unwrap(); println!("is wifi started: {:?}", controller.is_started()); } fn scan_wifi(controller: &mut WifiController<'_>) { println!("Start Wifi Scan"); let scan_config = ScanConfig::default().with_max(10); let res = controller.scan_with_config(scan_config).unwrap(); for ap in res { println!("{:?}", ap); } } fn connect_wifi(controller: &mut WifiController<'_>) { println!("{:?}", controller.capabilities()); println!("wifi_connect {:?}", controller.connect()); println!("Wait to get connected"); loop { match controller.is_connected() { Ok(true) => break, Ok(false) => {} Err(err) => panic!("{:?}", err), } } println!("Connected: {:?}", controller.is_connected()); } fn obtain_ip(stack: &mut Stack<'_, esp_radio::wifi::WifiDevice<'_>>) { println!("Wait for IP address"); loop { stack.work(); if stack.is_iface_up() { println!("IP acquired: {:?}", stack.get_ip_info()); break; } } } fn http_loop( mut socket: blocking_network_stack::Socket<'_, '_, esp_radio::wifi::WifiDevice<'_>>, ) -> ! { println!("Starting HTTP client loop"); let delay = Delay::new(); loop { println!("Making HTTP request"); socket.work(); let remote_addr = IpAddress::v4(142, 250, 185, 115); socket.open(remote_addr, 80).unwrap(); socket .write(b"GET / HTTP/1.0\r\nHost: www.mobile-j.de\r\n\r\n") .unwrap(); socket.flush().unwrap(); let deadline = Instant::now() + Duration::from_secs(20); let mut buffer = [0u8; 512]; while let Ok(len) = socket.read(&mut buffer) { // let text = unsafe { core::str::from_utf8_unchecked(&buffer[..len]) }; let Ok(text) = core::str::from_utf8(&buffer[..len]) else { panic!("Invalid UTF-8 sequence encountered"); }; println!("{}", text); if Instant::now() > deadline { println!("Timeout"); break; } } socket.disconnect(); let deadline = Instant::now() + Duration::from_secs(5); while Instant::now() < deadline { socket.work(); } delay.delay_millis(1000); } }