Example: Subscribing to Notifications

Many BLE devices use notifications (or indications) to send data to a central device without being polled. This is common for sensors that report data periodically or when a value changes. This example shows how to connect to a specific device, find a characteristic that supports notifications, and listen for incoming data.

Full Code

This code is based on examples/subscribe_notify_characteristic.rs.

use btleplug::api::{Central, CharPropFlags, Manager as _, Peripheral, ScanFilter};
use btleplug::platform::Manager;
use futures::stream::StreamExt;
use std::time::Duration;
use tokio::time;
use uuid::Uuid;

/// Only devices whose name contains this string will be tried.
const PERIPHERAL_NAME_MATCH_FILTER: &str = "Neuro";
/// UUID of the characteristic for which we should subscribe to notifications.
const NOTIFY_CHARACTERISTIC_UUID: Uuid = Uuid::from_u128(0x6e400002_b534_f393_67a9_e50e24dccA9e);

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    pretty_env_logger::init();

    let manager = Manager::new().await?;
    let adapter_list = manager.adapters().await?;
    if adapter_list.is_empty() {
        eprintln!("No Bluetooth adapters found");
        return Ok(());
    }

    for adapter in adapter_list.iter() {
        println!("Starting scan...");
        adapter.start_scan(ScanFilter::default()).await?;
        time::sleep(Duration::from_secs(2)).await;
        let peripherals = adapter.peripherals().await?;

        for peripheral in peripherals.iter() {
            let properties = peripheral.properties().await?;
            if let Some(local_name) = properties.unwrap().local_name {
                if local_name.contains(PERIPHERAL_NAME_MATCH_FILTER) {
                    println!("Found matching peripheral: {}", &local_name);

                    // Connect to the peripheral
                    peripheral.connect().await?;
                    println!("Connected to {}", &local_name);

                    // Discover services
                    peripheral.discover_services().await?;

                    // Find the notification characteristic
                    for characteristic in peripheral.characteristics() {
                        if characteristic.uuid == NOTIFY_CHARACTERISTIC_UUID
                            && characteristic.properties.contains(CharPropFlags::NOTIFY)
                        {
                            println!("Subscribing to characteristic {}", characteristic.uuid);
                            peripheral.subscribe(&characteristic).await?;

                            // Get the notification stream
                            let mut notification_stream = peripheral.notifications().await?.take(4);

                            // Process the first 4 notifications
                            while let Some(data) = notification_stream.next().await {
                                println!(
                                    "Received data from {} [{}]: {:?}",
                                    local_name, data.uuid, data.value
                                );
                            }
                        }
                    }

                    // Disconnect
                    println!("Disconnecting from {}...", local_name);
                    peripheral.disconnect().await?;
                    return Ok(()); // Exit after handling the first matching device
                }
            }
        }
    }
    Ok(())
}

Explanation

  1. Constants: We define constants for the target peripheral's name and the specific characteristic UUID we are interested in. This makes the code clearer and easier to modify.

  2. Scan and Filter: The code scans for all devices but then iterates through the results, looking for a peripheral whose local_name contains our target string ("Neuro").

  3. Connect and Discover: Once a matching peripheral is found, it connects and discovers its services, just like in the previous example.

  4. Find the Target Characteristic: The code iterates through the discovered characteristics. It checks for two conditions:

    • The UUID matches NOTIFY_CHARACTERISTIC_UUID.
    • The characteristic's properties include the NOTIFY flag.
  5. Subscribe: peripheral.subscribe(&characteristic).await? tells the peripheral that we want to receive notifications for this characteristic.

  6. Get Notification Stream: peripheral.notifications().await? returns a Stream that will yield ValueNotification structs. We use .take(4) to limit our example to processing just the first four notifications.

  7. Process Notifications: The while let Some(data) = ... loop asynchronously waits for items from the stream. Each data item is a ValueNotification struct containing the UUID of the characteristic and the value (payload) of the notification.

  8. Disconnect: After receiving the desired number of notifications, the program disconnects from the peripheral.