Connection
Now we enter the main loop. In each loop, we advertise to accept a BLE connection. Once a connection is established, we start the GATT server and the battery notifier concurrently.
There are multiple things going on here; we will explain them one by one.
#![allow(unused)] fn main() { loop { let config = peripheral::Config::default(); let adv = ble::adv::get_adv(); let conn = unwrap!(peripheral::advertise_connectable(sd, adv, &config).await); info!("advertising done! I have a connection."); let battery_fut = server.notify_battery_value(&conn); let gatt_fut = gatt_server::run(&conn, &server, |e| match e { ServerEvent::Bas(e) => match e { BatteryServiceEvent::BatteryLevelCccdWrite { notifications } => { info!("battery notifications: {}", notifications) } }, }); pin_mut!(battery_fut); pin_mut!(gatt_fut); match select(battery_fut, gatt_fut).await { Either::Left((_, _)) => { info!("Battery Notification encountered an error and stopped!") } Either::Right((e, _)) => { info!("gatt_server run exited with error: {:?}", e); } }; } }
Advertise
We first initialize the default config for advertising with "peripheral::Config::default()". Then we get the advertisement payload by calling get_adv(), which we defined earlier. Next, we call "advertise_connectable" with the advertisement data and config.
#![allow(unused)] fn main() { let conn = unwrap!(peripheral::advertise_connectable(sd, adv, &config).await); }
This line starts broadcasting advertisement packets and waits until a central device (like a smartphone) connects . Once a connection is established, it returns a Connection object that we'll use to interact with the connected client.
GATT connection
Once we get a connection, we call "notify_battery_value" and "gatt_server::run". Both are async functions. However, if you notice, we have not used ".await". Instead, we store the futures in variables and call pin_mut! on them.
#![allow(unused)] fn main() { let battery_fut = server.notify_battery_value(&conn); let gatt_fut = gatt_server::run(&conn, &server, |e| match e { ServerEvent::Bas(e) => match e { BatteryServiceEvent::BatteryLevelCccdWrite { notifications } => { info!("battery notifications: {}", notifications) } }, }); }
The gatt_server::run function runs the GATT server and receives events using the callback we provide. These events are represented by an enum called ServerEvent, which is automatically generated by the #[gatt_server]
proc macro from the nrf_softdevice crate (we annotated the Server struct in the service.rs file with this macro).
We then use select on the two pinned futures. This waits until one of them completes and automatically cancels the other.
-
The battery notifier task runs in an infinite loop, continuously sending updates, and only exits if there is an error.
-
The GATT server also runs continuously until it hits an error or the client disconnects (such as with a DisconnectError).
This setup ensures that when either side exits, we cleanly break out and restart the loop to accept a new connection.
The full content of main.rs
#![no_std] #![no_main] mod ble; mod service; use crate::ble::get_soft_device; use crate::service::*; use {defmt_rtt as _, panic_probe as _}; use defmt::{info, unwrap}; use embassy_executor::Spawner; use embassy_nrf::interrupt; use futures::future::{Either, select}; use futures::pin_mut; use nrf_softdevice::Softdevice; use nrf_softdevice::ble::{gatt_server, peripheral}; // Application must run at a lower priority than softdevice fn nrf_config() -> embassy_nrf::config::Config { let mut config = embassy_nrf::config::Config::default(); config.gpiote_interrupt_priority = interrupt::Priority::P2; config.time_interrupt_priority = interrupt::Priority::P2; config } #[embassy_executor::main] async fn main(spawner: Spawner) { // First we get the peripherals access crate. let _ = embassy_nrf::init(nrf_config()); let sd = get_soft_device(); let server = unwrap!(Server::new(sd)); unwrap!(spawner.spawn(softdevice_task(sd))); loop { let config = peripheral::Config::default(); let adv = ble::adv::get_adv(); let conn = unwrap!(peripheral::advertise_connectable(sd, adv, &config).await); info!("advertising done! I have a connection."); let battery_fut = server.notify_battery_value(&conn); let gatt_fut = gatt_server::run(&conn, &server, |e| match e { ServerEvent::Bas(e) => match e { BatteryServiceEvent::BatteryLevelCccdWrite { notifications } => { info!("battery notifications: {}", notifications) } }, }); pin_mut!(battery_fut); pin_mut!(gatt_fut); match select(battery_fut, gatt_fut).await { Either::Left((_, _)) => { info!("Battery Notification encountered an error and stopped!") } Either::Right((e, _)) => { info!("gatt_server run exited with error: {:?}", e); } }; } } #[embassy_executor::task] pub async fn softdevice_task(sd: &'static Softdevice) -> ! { sd.run().await }