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 BluetoothAdapter
s 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 toPeripheral
s.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 ofValueNotification
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(())
# }