Core Concepts and Usage

This guide provides a detailed walkthrough of the core concepts and common workflows in btleplug. It covers finding adapters, scanning for devices, connecting, and interacting with services and characteristics.

The Manager, Adapter, and Peripheral Model

btleplug's API is structured around three main traits:

  1. Manager: The entry point to the library. Its primary role is to provide a list of available Bluetooth Adapters on the system.
  2. Adapter (implements Central): Represents a physical Bluetooth adapter (e.g., your laptop's Bluetooth radio). It is responsible for scanning for and managing connections to Peripherals.
  3. Peripheral: Represents a remote BLE device that you want to communicate with.

The typical workflow is: ManagerAdapter → Scan → Peripheral → Connect → Interact.

use btleplug::platform::Manager;
use btleplug::api::Manager as _;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let manager = Manager::new().await?;
    let adapter_list = manager.adapters().await?;
    if let Some(adapter) = adapter_list.into_iter().nth(0) {
        // use the adapter
    } else {
        eprintln!("No Bluetooth adapters found");
    }
    Ok(())
}

Scanning for Devices

Once you have an Adapter, you can start scanning for peripherals. The start_scan method takes a ScanFilter to narrow down the search.

Polling for Discovered Devices

The simplest way to find devices is to scan for a short period and then get the list of discovered peripherals.

use btleplug::api::{Central, ScanFilter};
use btleplug::platform::Adapter;
use std::time::Duration;
use tokio::time;

async fn find_devices(adapter: &Adapter) -> anyhow::Result<()> {
    println!("Starting scan...");
    adapter.start_scan(ScanFilter::default()).await?;
    time::sleep(Duration::from_secs(5)).await;

    let peripherals = adapter.peripherals().await?;
    if peripherals.is_empty() {
        eprintln!("No peripherals found.");
    } else {
        for peripheral in peripherals.iter() {
            let properties = peripheral.properties().await?.unwrap();
            let local_name = properties.local_name.unwrap_or_else(|| "(unknown)".to_string());
            println!("Found peripheral: {}", local_name);
        }
    }
    Ok(())
}

This "polling" approach is great for simple applications. For more complex or long-running applications, see the Event-Driven Usage guide.

Using a Scan Filter

To avoid discovering irrelevant devices, you can filter by advertised service UUIDs.

use btleplug::api::ScanFilter;
use uuid::Uuid;

let heart_rate_service = Uuid::from_u16(0x180D);
let filter = ScanFilter {
    services: vec![heart_rate_service],
};
// adapter.start_scan(filter).await?;

Connecting and Disconnecting

Once you have a Peripheral object, you can establish a connection.

use btleplug::api::Peripheral as _;
# use btleplug::platform::Peripheral;

async fn connect_to_peripheral(peripheral: &Peripheral) -> anyhow::Result<()> {
    if !peripheral.is_connected().await? {
        println!("Connecting...");
        peripheral.connect().await?;
        println!("Connected successfully!");
    } else {
        println!("Already connected.");
    }

    // ... perform operations ...

    println!("Disconnecting...");
    peripheral.disconnect().await?;
    Ok(())
}

Working with Services and Characteristics

After connecting, you must discover the peripheral's services and characteristics before you can interact with them.

use btleplug::api::{Peripheral as _, CharPropFlags};
# use btleplug::platform::Peripheral;

async fn explore_peripheral(peripheral: &Peripheral) -> anyhow::Result<()> {
    peripheral.discover_services().await?;

    for service in peripheral.services() {
        println!(
            "Service: UUID {}, Primary: {}",
            service.uuid,
            service.primary
        );
        for characteristic in service.characteristics {
            println!("  Characteristic: UUID {}, Properties: {:?}", characteristic.uuid, characteristic.properties);
        }
    }
    Ok(())
}

Interacting with Characteristics

Characteristics are the primary channels for data exchange.

Reading a Value

If a characteristic has the READ property, you can read its value.

# use btleplug::api::{Peripheral as _, CharPropFlags};
# use btleplug::platform::Peripheral;
# use uuid::Uuid;
async fn read_from_char(peripheral: &Peripheral) -> anyhow::Result<()> {
    // Find a readable characteristic
    if let Some(characteristic) = peripheral.characteristics().into_iter().find(|c| c.properties.contains(CharPropFlags::READ)) {
        let value = peripheral.read(&characteristic).await?;
        println!("Read value: {:?}", value);
    }
    Ok(())
}

Writing a Value

There are two types of write operations, specified by WriteType:

  • WithResponse: The peripheral acknowledges the write. Use this for critical commands.
  • WithoutResponse: The peripheral does not acknowledge the write. This is faster and useful for frequent updates where occasional packet loss is acceptable.
# use btleplug::api::{Peripheral as _, CharPropFlags, WriteType};
# use btleplug::platform::Peripheral;
# async fn write_to_char(peripheral: &Peripheral) -> anyhow::Result<()> {
if let Some(characteristic) = peripheral.characteristics().into_iter().find(|c| c.properties.contains(CharPropFlags::WRITE)) {
    let data = vec![0x01, 0x02, 0x03];
    peripheral.write(&characteristic, &data, WriteType::WithResponse).await?;
    println!("Wrote data successfully.");
}
# Ok(())
# }

Subscribing to Notifications

For characteristics that support it (NOTIFY or INDICATE properties), you can subscribe to receive updates whenever their value changes.

  1. Subscribe: Call peripheral.subscribe(&characteristic).
  2. Get Notification Stream: Call peripheral.notifications().await? to get a stream of ValueNotification events.
use btleplug::api::{Peripheral as _, CharPropFlags};
use btleplug::platform::Peripheral;
use futures::stream::StreamExt;

async fn subscribe_to_notifications(peripheral: &Peripheral) -> anyhow::Result<()> {
    if let Some(characteristic) = peripheral.characteristics().into_iter().find(|c| c.properties.contains(CharPropFlags::NOTIFY)) {
        println!("Subscribing to characteristic {}", characteristic.uuid);
        peripheral.subscribe(&characteristic).await?;

        let mut notification_stream = peripheral.notifications().await?;

        while let Some(data) = notification_stream.next().await {
            println!(
                "Received notification from UUID {}: {:?}",
                data.uuid,
                data.value
            );
        }
    }
    Ok(())
}

Working with Descriptors

Descriptors provide additional information about a characteristic. btleplug supports discovering, reading, and writing to descriptors.

# use btleplug::api::{Peripheral as _};
# use btleplug::platform::Peripheral;
# async fn work_with_descriptors(peripheral: &Peripheral) -> anyhow::Result<()> {
for service in peripheral.services() {
    for characteristic in service.characteristics {
        for descriptor in characteristic.descriptors {
            println!("    Descriptor: UUID {}", descriptor.uuid);
            let value = peripheral.read_descriptor(&descriptor).await?;
            println!("      Value: {:?}", value);
        }
    }
}
# Ok(())
# }