How to Write Rust Code for Using Bluetooth Low Energy (BLE) on ESP32
Let's create a simple program to demonstrate Bluetooth Low Energy (BLE). We'll define a GATT service with two characteristics:
- One characteristic supports both read and write operations.
- The other characteristic allows only write operations.
I have generated UUIDs for these attributes (services and characteristics), and you can use the same ones.
Connecting to ESP32 Bluetooth
To interact with the ESP32's Bluetooth, we'll use the nRF Connect for Mobile app:
This app lets us read and write data provided by the ESP32.
Generate project using esp-generate
We will enable async (Embassy) support for this project. To create the project, use the esp-generate
command. Run the following:
esp-generate --chip esp32 ble-demo
This will open a screen asking you to select options.
- Select the option "Enable BLE via the
esp-wifi
crate. Requiresalloc
". - Select the option "Adds embassy framework support".
Just save it by pressing "s" in the keyboard.
Helper Macro for StaticCell
In embedded systems, the StaticCell crate helps initialize variables at runtime while ensuring a static lifetime. We'll define a macro to create global static variables. It takes two arguments: the variable type and its initial value. The uninit function gives a mutable reference to uninitialized memory, where we store the value.
#![allow(unused)] fn main() { 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 }}; } }
Initialize Wi-Fi controller
The ESP32 shares a single radio for both Wi-Fi and Bluetooth. In order to initialize Bluetooth, We will use the same Wi-Fi controller that we used for Wi-Fi.
#![allow(unused)] fn main() { let timg0 = TimerGroup::new(peripherals.TIMG0); let init = &*mk_static!( EspWifiController<'static>, init( timg0.timer0, Rng::new(peripherals.RNG), peripherals.RADIO_CLK, ) .unwrap() ); }
Let's initialize the Bluetooth connector and set up BLE with the help of the bleps crate.
#![allow(unused)] fn main() { let mut bluetooth = peripherals.BT; let connector = BleConnector::new(init, &mut bluetooth); let now = || time::now().duration_since_epoch().to_millis(); let mut ble = Ble::new(connector, now); println!("{:?}", ble.init().await); }
Let's call this function to set BLE advertising parameters:
#![allow(unused)] fn main() { println!("{:?}", ble.cmd_set_le_advertising_parameters().await); }
BLE Advertising for Discovery
We will set the BLE advertising data to make the ESP32 discoverable, allowing other devices to find and connect to it. By enabling general discoverable mode, the device will advertise indefinitely until a connection is established. We will also disable Bluetooth Classic (BR/EDR). We are setting the device name to "implRust", but you can choose any name.
#![allow(unused)] fn main() { println!( "{:?}", ble.cmd_set_le_advertising_data( create_advertising_data(&[ AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), AdStructure::CompleteLocalName("implRust"), // AdStructure::ServiceUuids16(&[Uuid::Uuid16(0x1809)]), ]) .unwrap() ) .await ); println!("{:?}", ble.cmd_set_le_advertise_enable(true).await); }
Closures for read and write data
We will define three closures: one for handling read requests and two for write operations. The read closure will send a static string "Hello, Ferris" to the client. The write closures allow a client (e.g., a mobile device) to send data to the ESP32, which we will print to the console for demonstration. In a real scenario, we will store the received data for settings or other purposes.
#![allow(unused)] fn main() { let sensor_data = b"Hello, Ferris"; let mut read_func = |_offset: usize, data: &mut [u8]| { data[0..sensor_data.len()].copy_from_slice(&sensor_data[..]); sensor_data.len() }; let mut write_func = |offset: usize, data: &[u8]| { println!("RECEIVED: {} {:?}", offset, data); }; let mut write_func2 = |offset: usize, data: &[u8]| { println!("RECEIVED: {} {:?}", offset, data); }; }
Define Service and Characteristics
We will use the gatt
macro to define a service with two characteristics. We will set the first characteristic to allow both read and write and pass the read and write closures to it. We will set the second characteristic to allow only write and pass the second write closure to it.
#![allow(unused)] fn main() { gatt!([service { uuid: "a9c81b72-0f7a-4c59-b0a8-425e3bcf0a0e", characteristics: [ characteristic { uuid: "13c0ef83-09bd-4767-97cb-ee46224ae6db", read: read_func, write: write_func, }, characteristic { uuid: "c79b2ca7-f39d-4060-8168-816fa26737b7", write: write_func2, }, ], },]); }
Run Server
Finally, we will initialize the Attribute Server and start the server by calling the do_work function.
#![allow(unused)] fn main() { let mut rng = bleps::no_rng::NoRng; let mut srv = AttributeServer::new(&mut ble, &mut gatt_attributes, &mut rng); while srv.do_work().await.is_ok() {} }
Clone the existing project
You can clone (or refer) project I created and navigate to the bluetooth-low-energy
folder.
git clone https://github.com/ImplFerris/esp32-projects
cd esp32-projects/bluetooth-low-energy/
How to use?
Once you flash the code to the ESP32, open the nRF Connect mobile app. Scan for the Bluetooth name we set (mine is "implRust") and connect to it. The app will show the supported service and characteristics. Tap the down arrow under a characteristic to read data and the up arrow to write data.

If you send(i.e write) data, you will see it in the system console.
The Full code
#![no_std] #![no_main] use bleps::{ ad_structure::{ create_advertising_data, AdStructure, BR_EDR_NOT_SUPPORTED, LE_GENERAL_DISCOVERABLE, }, async_attribute_server::AttributeServer, asynch::Ble, gatt, }; use embassy_executor::Spawner; use esp_alloc as _; use esp_backtrace as _; use esp_hal::{prelude::*, rng::Rng, time, timer::timg::TimerGroup}; use esp_println::println; use esp_wifi::{ble::controller::BleConnector, init, EspWifiController}; extern crate alloc; 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 }}; } #[main] async fn main(_spawner: Spawner) { let peripherals = esp_hal::init({ let mut config = esp_hal::Config::default(); config.cpu_clock = CpuClock::max(); config }); esp_alloc::heap_allocator!(72 * 1024); esp_println::logger::init_logger_from_env(); let timg0 = TimerGroup::new(peripherals.TIMG0); let init = &*mk_static!( EspWifiController<'static>, init( timg0.timer0, Rng::new(peripherals.RNG), peripherals.RADIO_CLK, ) .unwrap() ); let timg1 = TimerGroup::new(peripherals.TIMG1); esp_hal_embassy::init(timg1.timer0); let mut bluetooth = peripherals.BT; let connector = BleConnector::new(init, &mut bluetooth); let now = || time::now().duration_since_epoch().to_millis(); let mut ble = Ble::new(connector, now); println!("Connector created"); println!("{:?}", ble.init().await); println!("{:?}", ble.cmd_set_le_advertising_parameters().await); println!( "{:?}", ble.cmd_set_le_advertising_data( create_advertising_data(&[ AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), AdStructure::CompleteLocalName("implRust"), // AdStructure::ServiceUuids16(&[Uuid::Uuid16(0x1809)]), ]) .unwrap() ) .await ); println!("{:?}", ble.cmd_set_le_advertise_enable(true).await); println!("started advertising"); let sensor_data = b"Hello, Ferris"; let mut read_func = |_offset: usize, data: &mut [u8]| { data[0..sensor_data.len()].copy_from_slice(&sensor_data[..]); sensor_data.len() }; let mut write_func = |offset: usize, data: &[u8]| { println!("RECEIVED: {} {:?}", offset, data); }; let mut write_func2 = |offset: usize, data: &[u8]| { println!("RECEIVED: {} {:?}", offset, data); }; gatt!([service { uuid: "a9c81b72-0f7a-4c59-b0a8-425e3bcf0a0e", characteristics: [ characteristic { uuid: "13c0ef83-09bd-4767-97cb-ee46224ae6db", read: read_func, write: write_func, }, characteristic { uuid: "c79b2ca7-f39d-4060-8168-816fa26737b7", write: write_func2, }, ], },]); let mut rng = bleps::no_rng::NoRng; let mut srv = AttributeServer::new(&mut ble, &mut gatt_attributes, &mut rng); while srv.do_work().await.is_ok() {} }