Example: Controlling a Smart Light

This example demonstrates a practical, real-world use case: controlling a generic BLE-enabled smart light. It showcases how to connect to a specific device and write data to a characteristic to change its color.

Full Code

This code is based on examples/lights.rs.

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

// The standard short UUID for a generic light control characteristic
const LIGHT_CHARACTERISTIC_UUID: Uuid = uuid_from_u16(0xFFE9);

// Helper 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() -> anyhow::Result<()> {
    pretty_env_logger::init();

    let manager = Manager::new().await.unwrap();
    let central = manager
        .adapters()
        .await?
        .into_iter()
        .nth(0)
        .expect("Unable to find adapters.");

    // Scan for devices and find our light
    central.start_scan(ScanFilter::default()).await?;
    time::sleep(Duration::from_secs(2)).await;
    let light = find_light(&central).await.expect("No lights found");

    // Connect and discover services
    light.connect().await?;
    light.discover_services().await?;

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

    // Create a "dance party" by sending random colors
    let mut rng = thread_rng();
    for _ in 0..20 {
        let color_cmd = vec![
            0x56,       // Command prefix for this specific light protocol
            rng.gen(),  // Red component (0-255)
            rng.gen(),  // Green component (0-255)
            rng.gen(),  // Blue component (0-255)
            0x00,
            0xF0,
            0xAA,       // Command suffix
        ];

        // Write the command to the characteristic
        light
            .write(&cmd_char, &color_cmd, WriteType::WithoutResponse)
            .await?;
        time::sleep(Duration::from_millis(200)).await;
    }

    light.disconnect().await?;
    Ok(())
}

Explanation

  1. Device Identification: The find_light function is similar to previous examples. It scans for peripherals and identifies the target light by looking for "LEDBlue" in its advertised local name.

  2. Characteristic Identification: We use a constant LIGHT_CHARACTERISTIC_UUID (0xFFE9) to identify the specific characteristic that accepts color commands. This UUID is common for a certain type of generic BLE light.

  3. The Command Protocol: The color_cmd vector is the most important part of this example. BLE communication is just an exchange of byte arrays. The meaning of these bytes is defined by the peripheral's manufacturer.

    For this particular light, the protocol is:

    • 0x56: A required prefix byte to indicate a color command.
    • rng.gen(): Three random bytes for the Red, Green, and Blue color components.
    • 0x00, 0xF0, 0xAA: Required suffix bytes.

    When developing for a new BLE device, you will need to find documentation or reverse-engineer the protocol to understand what byte commands it expects.

  4. Writing the Command: We use light.write() with WriteType::WithoutResponse. This is a "fire and forget" write, which is suitable for this light as it doesn't send a confirmation for each color change. This allows for rapid, successive commands to create the color-changing effect.