Quick Start: Your First BLE Interaction

This guide provides a complete, runnable example to get you started with btleplug. We will scan for a BLE peripheral, connect to it, discover its services, and perform a simple interaction.

The Goal: A BLE Dance Party

The following example discovers a BLE-enabled LED light, connects to it, and sends commands to change its color randomly.

Full Example Code

This code is adapted from the lights.rs example in the btleplug repository. You can use it as a template for your own projects.

use btleplug::api::{bleuuid::uuid_from_u16, Central, Manager as _, Peripheral as _, ScanFilter, WriteType};
use btleplug::platform::{Adapter, Manager, Peripheral};
use rand::{Rng, thread_rng};
use std::error::Error;
use std::time::Duration;
use tokio::time;
use uuid::Uuid;

// The characteristic UUID for the light control.
const LIGHT_CHARACTERISTIC_UUID: Uuid = uuid_from_u16(0xFFE9);

// Asynchronous function to find a peripheral with "LEDBlue" in its name.
async fn find_light(central: &Adapter) -> Option<Peripheral> {
    for p in central.peripherals().await.unwrap() {
        if p.properties()
            .await
            .unwrap()
            .unwrap()
            .local_name
            .iter()
            .any(|name| name.contains("LEDBlue"))
        {
            return Some(p);
        }
    }
    None
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // 1. Initialize the Manager
    let manager = Manager::new().await?;

    // 2. Get the first available adapter
    let adapters = manager.adapters().await?;
    let central = adapters.into_iter().nth(0).expect("No Bluetooth adapters found.");

    // 3. Start scanning for peripherals
    central.start_scan(ScanFilter::default()).await?;
    println!("Scanning for 2 seconds...");
    time::sleep(Duration::from_secs(2)).await;

    // 4. Find our specific peripheral
    let light = find_light(&central).await.expect("Could not find a light peripheral.");
    let properties = light.properties().await?.unwrap();
    println!("Found peripheral: {}", properties.local_name.unwrap_or_else(|| "(unknown)".into()));

    // 5. Connect to the peripheral
    println!("Connecting to peripheral...");
    light.connect().await?;
    println!("Connected!");

    // 6. Discover services and characteristics
    light.discover_services().await?;

    // 7. Find the characteristic we want to write to
    let chars = light.characteristics();
    let cmd_char = chars
        .iter()
        .find(|c| c.uuid == LIGHT_CHARACTERISTIC_UUID)
        .expect("Unable to find the light control characteristic.");

    // 8. Send commands to the light
    println!("Starting dance party...");
    let mut rng = thread_rng();
    for _ in 0..20 {
        let color_cmd = vec![
            0x56,
            rng.gen(), // Red
            rng.gen(), // Green
            rng.gen(), // Blue
            0x00,
            0xF0,
            0xAA,
        ];
        light.write(cmd_char, &color_cmd, WriteType::WithoutResponse).await?;
        time::sleep(Duration::from_millis(200)).await;
    }

    println!("Dance party over. Disconnecting...");
    light.disconnect().await?;

    Ok(())
}

Breakdown of the Code

  1. Initialize the Manager: Manager::new().await? is the entry point to the library. It finds and initializes the underlying platform-specific Bluetooth backend.

  2. Get an Adapter: manager.adapters().await? returns a list of available Bluetooth adapters on the system. We take the first one for simplicity.

  3. Start Scanning: central.start_scan(ScanFilter::default()).await? begins scanning for nearby BLE devices. We use an empty ScanFilter to discover all devices. We then wait for a couple of seconds to allow time for advertisements to be received.

  4. Find the Peripheral: The find_light helper function iterates through the list of discovered peripherals (central.peripherals().await?) and checks their properties for a specific local name.

  5. Connect: light.connect().await? establishes a GATT connection with the peripheral.

  6. Discover Services: light.discover_services().await? is a crucial step. After connecting, you must query the peripheral to discover its available services and their associated characteristics.

  7. Find the Characteristic: We iterate through the cached characteristics (light.characteristics()) to find the specific one that controls the light, identified by its Uuid.

  8. Send Commands: light.write(...) sends a byte payload to the characteristic. We use WriteType::WithoutResponse because this particular light doesn't send a confirmation for color changes, which is common for high-frequency updates.

  9. Disconnect: light.disconnect().await? cleanly terminates the connection to the peripheral.