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:
Manager: The entry point to the library. Its primary role is to provide a list of available BluetoothAdapters on the system.Adapter(implementsCentral): Represents a physical Bluetooth adapter (e.g., your laptop's Bluetooth radio). It is responsible for scanning for and managing connections toPeripherals.Peripheral: Represents a remote BLE device that you want to communicate with.
The typical workflow is:
Manager → Adapter → 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.
- Subscribe: Call
peripheral.subscribe(&characteristic). - Get Notification Stream: Call
peripheral.notifications().await?to get a stream ofValueNotificationevents.
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(())
# }