使用Rust连接蓝牙BLE设备

上文介绍了蓝牙BLE的一些基础知识,本文介绍如何在Rust里连接蓝牙BLE设备。 本文使用小米的秒秒测,秒秒测会持续测量温度数据并广播给已连接的主设备。你也可以使用其他蓝牙BLE设备测试。

准备工作

在开始写代码前,我们需要熟悉蓝牙设备。

nRFConnect这个工具可以方便地调试蓝牙BLE设备的通信数据。以连接秒秒测为例,

  • 打开nRFConnect,点击搜索等待片刻,在出现的设备里找到MMC-T201并点击Connect,连接成功后就可以看到如下界面:

  • 我们需要在这些Service列表里找到秒秒测广播测量数据的Characteristic。把每个Service都展开看一下,很容易找到下面的一项:

  • 点击Intermediate Temperature右侧的图标,观察下面值的变化,这里显示了一个整数温度,不过看来不像是正确的值。点击右上角打开菜单,选择Show Log,从日志里查看16进制的数据,后面我们会使用这里显示的原始数据解析出温度。
  • 此时我们基本可以判定秒秒测广播测量数据的Service及Characteristic的UUID分别是0x18090x2A1E

发现设备

我们这里使用blurz库。没有使用看起来功能更多的btleplug是因为bteplug似乎有较多Bug。

use blurz::bluetooth_adapter::BluetoothAdapter;
use blurz::bluetooth_device::BluetoothDevice;
use blurz::bluetooth_discovery_session::BluetoothDiscoverySession;
use blurz::bluetooth_event::BluetoothEvent;
use blurz::bluetooth_event::BluetoothEvent::{Connected, ServicesResolved, Value, RSSI};
use blurz::bluetooth_gatt_characteristic::BluetoothGATTCharacteristic;
use blurz::bluetooth_gatt_descriptor::BluetoothGATTDescriptor;
use blurz::bluetooth_gatt_service::BluetoothGATTService;
use blurz::bluetooth_session::BluetoothSession;
use lazy_static::lazy_static;
use regex::Regex;
use std::str;
use std::thread;
use std::time::Duration;
use std::error::Error;

const MMC_SERVICE_UUID: &str = "1809";
const MMC_CHAR_UUID: &str = "2A1E";
const MMC_TITLE: &str = "MMC";

const UUID_REGEX: &str = r"([0-9a-f]{4})([0-9a-f]{4})-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}";

lazy_static! {
    static ref RE: Regex = Regex::new(UUID_REGEX).unwrap();
}


fn main() {
    let bt_session = &BluetoothSession::create_session(None).unwrap();
    let adapter: BluetoothAdapter = BluetoothAdapter::init(bt_session).unwrap();
    let adapter_id = adapter.get_id();
    // 创建蓝牙搜索的Session
    let discover_session =
        BluetoothDiscoverySession::create_session(&bt_session, adapter_id).unwrap();
    // 开始扫描设备
    discover_session.start_discovery().unwrap();
    // 等待几秒
    thread::sleep(Duration::from_secs(5));
    // 获取设备列便
    let device_list = adapter.get_device_list().unwrap();
    // 结束扫描
    discover_session.stop_discovery().unwrap();

    for device_path in device_list {
        let device = BluetoothDevice::new(bt_session, device_path.to_string());
        println!(
            "Device: {:?} Name: {:?}, RSSI: {:?}",
            device_path,
            device.get_name().ok(),
            device.get_rssi().ok()
        );
    }
}

在运行前可能需要开启本地的蓝牙适配器,在Arch Linux里可以通过下面的命令开启:

bluetoothctl power on

运行上面的代码后,可以看到类似下面的数据:

Device: "/org/bluez/hci0/dev_00_81_E8_DE_B0_80" Name: Some("MMC-T201"), RSSI: None
Device: "/org/bluez/hci0/dev_8A_8B_E8_8A_DB_22" Name: None, RSSI: None
Device: "/org/bluez/hci0/dev_10_38_C1_00_00_0C" Name: Some("Joystick"), RSSI: None

发现设备的三个问题

如果你运行上面的代码,仔细观察可能会发现adapter.get_device_list()的几个问题,

  1. adapter.get_device_list()返回的只是蓝牙设备的bluez路径,而非BluetoothDevice对象。
  2. RSSI的数据全部都为空。要想获取设备的RSSI,似乎只能依靠blurz库的通知推送。
  3. 当前电脑曾连接过,但当前并不在线的设备也出现在结果里。

连接设备

使用adapter.get_device_list()返回的路径创建BluetoothDevice并连接。device.connect(10000)表示超时时间为10秒:

    let device = BluetoothDevice::new(
        bt_session,
        String::from("/org/bluez/hci0/dev_00_81_E8_DE_B0_80"), // mmc
    );
    
    if let Err(e) = device.connect(10000) {
        println!("Failed to connect {:?}: {:?}", device.get_id(), e);
    } else {
        println!("Connected!");
    }

打印设备的蓝牙属性

添加explore_device方法,用于打印蓝牙设备的Service/Characteritics等信 息。注意这里我们使用了正则表达式从128Bit的UUID里截取`Assigned Number`用于标识单个属性。

pub fn list_characteritics(service: &BluetoothGATTService, session: &BluetoothSession) {
    let characteristics = service.get_gatt_characteristics().unwrap();
    for characteristic_path in characteristics {
        let characteristic = BluetoothGATTCharacteristic::new(session, characteristic_path);
        let uuid = characteristic.get_uuid().unwrap();
        let assigned_number = RE
            .captures(&uuid)
            .unwrap()
            .get(2)
            .map_or("", |m| m.as_str());
        let flags = characteristic.get_flags().unwrap();

        // println!("Characteristic: {:?}", characteristic);
        println!(
            " Characteristic UUID: {}, Assigned Number: 0x{:?} Flags: {:?}",
            uuid, assigned_number, flags
        );

        list_descriptors(&characteristic, session);
    }
}

/// List descriptors in characteristic
pub fn list_descriptors(characteristic: &BluetoothGATTCharacteristic, session: &BluetoothSession) {
    let descriptors = characteristic.get_gatt_descriptors().unwrap();
    for descriptor_path in descriptors {
        let descriptor = BluetoothGATTDescriptor::new(session, descriptor_path);
        let uuid = descriptor.get_uuid().unwrap();
        let assigned_number = RE
            .captures(&uuid)
            .unwrap()
            .get(2)
            .map_or("", |m| m.as_str());
        let value = descriptor.read_value(None).unwrap();
        let value = match &assigned_number[4..] {
            "2901" => str::from_utf8(&value).unwrap().to_string(),
            _ => format!("{:x?}", value),
        };

        println!(
            "    Descriptor UUID: {}, Assigned Number: 0x{:?} Read Value: {:?}",
            uuid, assigned_number, value
        );
    }
}

pub fn explore_device(device: &BluetoothDevice, session: &BluetoothSession) {
    // list services
    let services_list = device.get_gatt_services().unwrap();

    for service_path in services_list {
        let service = BluetoothGATTService::new(session, service_path.to_string());
        let uuid = service.get_uuid().unwrap();
        let assigned_number = RE
            .captures(&uuid)
            .unwrap()
            .get(2)
            .map_or("", |m| m.as_str());

        println!(
            "Service UUID: {:?} Assigned Number: 0x{:?}",
            uuid, assigned_number
        );

        list_characteritics(&service, session);
        println!("");
    }
}

在main函数中添加/修改如下内容,注意连接后需要等待片刻才能获取到设备属性:

    if let Err(e) = device.connect(10000) {
        println!("Failed to connect {:?}: {:?}", device.get_id(), e);
    } else {
        println!("Connected!");
        // 需要等待一段时间才能获取到设备GATT信息:
        thread::sleep(Duration::from_secs(5));

        // 打印 services, characteristics, descriptors
        explore_device(&device, bt_session);
    }

再运行后,可以看到如下结果:

Service UUID: "f000ffc0-0451-4000-b000-000000000000" Assigned Number: 0x"ffc0"
 Characteristic UUID: f000ffc3-0451-4000-b000-000000000000, Assigned Number: 0x"ffc3" Flags: ["write-without-response", "write"]
    Descriptor UUID: 00002901-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2901" Read Value: "[49, 6d, 67, 20, 43, 6f, 75, 6e, 74]"
 Characteristic UUID: f000ffc2-0451-4000-b000-000000000000, Assigned Number: 0x"ffc2" Flags: ["write-without-response", "write", "notify"]
    Descriptor UUID: 00002901-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2901" Read Value: "[49, 6d, 67, 20, 42, 6c, 6f, 63, 6b]"
    Descriptor UUID: 00002902-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2902" Read Value: "[0, 0]"
 Characteristic UUID: f000ffc1-0451-4000-b000-000000000000, Assigned Number: 0x"ffc1" Flags: ["write-without-response", "write", "notify"]
    Descriptor UUID: 00002901-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2901" Read Value: "[49, 6d, 67, 20, 49, 64, 65, 6e, 74, 69, 66, 79]"
    Descriptor UUID: 00002902-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2902" Read Value: "[0, 0]"

Service UUID: "0000fe95-0000-1000-8000-00805f9b34fb" Assigned Number: 0x"fe95"
 Characteristic UUID: 00000014-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"0014" Flags: ["read"]
 Characteristic UUID: 00000013-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"0013" Flags: ["read", "write"]
 Characteristic UUID: 00000010-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"0010" Flags: ["write"]
 Characteristic UUID: 00000007-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"0007" Flags: ["read", "write"]
    Descriptor UUID: 00002901-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2901" Read Value: "[45, 76, 65, 6e, 74, 20, 52, 75, 6c, 65]"
 Characteristic UUID: 00000004-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"0004" Flags: ["read"]
    Descriptor UUID: 00002901-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2901" Read Value: "[56, 45, 52]"
 Characteristic UUID: 00000002-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"0002" Flags: ["read"]
    Descriptor UUID: 00002901-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2901" Read Value: "[50, 72, 6f, 64, 75, 63, 74, 20, 49, 44]"
 Characteristic UUID: 00000001-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"0001" Flags: ["read", "write", "notify"]
    Descriptor UUID: 00002902-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2902" Read Value: "[0, 0]"

Service UUID: "00001809-0000-1000-8000-00805f9b34fb" Assigned Number: 0x"1809"
 Characteristic UUID: 00002a21-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a21" Flags: ["read"]
 Characteristic UUID: 00002a1e-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a1e" Flags: ["notify"]
    Descriptor UUID: 00002902-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2902" Read Value: "[0, 0]"
 Characteristic UUID: 00002a1d-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a1d" Flags: ["read"]
 Characteristic UUID: 00002a1c-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a1c" Flags: ["indicate"]
    Descriptor UUID: 00002902-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2902" Read Value: "[0, 0]"

Service UUID: "ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ccb0"
 Characteristic UUID: ebe0ccbd-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccbd" Flags: ["read", "write"]
 Characteristic UUID: ebe0ccbc-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccbc" Flags: ["notify"]
    Descriptor UUID: 00002902-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2902" Read Value: "[0, 0]"
 Characteristic UUID: ebe0ccbb-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccbb" Flags: ["read"]
 Characteristic UUID: ebe0ccba-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccba" Flags: ["read", "write"]
 Characteristic UUID: ebe0ccb9-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccb9" Flags: ["read"]
 Characteristic UUID: ebe0ccb8-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccb8" Flags: ["read", "write"]
 Characteristic UUID: ebe0ccb7-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccb7" Flags: ["read", "write"]
 Characteristic UUID: ebe0ccb6-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccb6" Flags: ["read"]
 Characteristic UUID: ebe0ccb5-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccb5" Flags: ["read"]
 Characteristic UUID: ebe0ccb4-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccb4" Flags: ["write"]
 Characteristic UUID: ebe0ccb3-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccb3" Flags: ["write"]
 Characteristic UUID: ebe0ccb2-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccb2" Flags: ["read"]
 Characteristic UUID: ebe0ccb1-7a0a-4b0c-8a1a-6ff2997da3a6, Assigned Number: 0x"ccb1" Flags: ["write"]

Service UUID: "0000180a-0000-1000-8000-00805f9b34fb" Assigned Number: 0x"180a"
 Characteristic UUID: 00002a29-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a29" Flags: ["read"]
 Characteristic UUID: 00002a28-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a28" Flags: ["read"]
 Characteristic UUID: 00002a27-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a27" Flags: ["read"]
 Characteristic UUID: 00002a26-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a26" Flags: ["read"]
 Characteristic UUID: 00002a25-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a25" Flags: ["read"]
 Characteristic UUID: 00002a24-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a24" Flags: ["read"]
 Characteristic UUID: 00002a23-0000-1000-8000-00805f9b34fb, Assigned Number: 0x"2a23" Flags: ["read"]

Service UUID: "00001801-0000-1000-8000-00805f9b34fb" Assigned Number: 0x"1801"

从上面的结果可以看出1809的Service里的2a1e - Characteristic具有notify属性,与在nRFConnect里看到的一致。

订阅广播数据

下面我们订阅广播数据,并打印结果。 bt_session.incoming(1000).map(BluetoothEvent::from)表示订阅蓝牙事件, 如果1000毫秒以内未收到数据,则返回None。

        // explore_device(&device, bt_session);

        let service = get_service(MMC_SERVICE_UUID, &device, bt_session).unwrap();
        let ch = get_characteritic(MMC_CHAR_UUID, &service, bt_session).unwrap();
        ch.start_notify().unwrap();
        loop {
            for event in bt_session.incoming(1000).map(BluetoothEvent::from) {
                println!("recv: {:?}", event);
            }
        }

蓝牙事件用enum表示,各种事件类型的定义可以参考BluetoothEvent文档

打印结果里的Value { object_path: "...", value: ... }正是我们需要的温度数据。

recv: None
recv: Some(Discovering { object_path: "/org/bluez/hci0", discovering: true })
recv: Some(RSSI { object_path: "/org/bluez/hci0/dev_60_C6_31_62_61_53", rssi: -88 })
recv: Some(RSSI { object_path: "/org/bluez/hci0/dev_00_81_E8_DE_B0_40", rssi: -52 })
recv: Some(Discovering { object_path: "/org/bluez/hci0", discovering: false })
recv: Some(None)
recv: Some(None)
recv: Some(Connected { object_path: "/org/bluez/hci0/dev_00_81_E8_DE_B0_40", connected: true })
recv: Some(ServicesResolved { object_path: "/org/bluez/hci0/dev_00_81_E8_DE_B0_40", services_resolved: true })
recv: Some(None)
recv: Some(Value { object_path: "/org/bluez/hci0/dev_00_81_E8_DE_B0_40/service0034/char003a", value: [0, 132, 11, 114, 11, 100] })
recv: Some(Value { object_path: "/org/bluez/hci0/dev_00_81_E8_DF_B0_40/service0034/char003a", value: [0, 144, 11, 116, 11, 100] })

解析秒秒测温度数据

添加如下解析方法parse_mmc_data,返回tuple里的第一与第四个值分别为原始温度及修正后的 温度。注意这里的计算是通过观察原始数据与秒秒测App显示的温度得出的,并 不保证准确。

fn parse_mmc_data(data: Box<[u8]>) -> Option<(f32, f32, f32, f32)> {
    if data.len() == 6 {
        let mut t0: f32 = data[2] as f32 * 256.0 + data[1] as f32; 
        let mut offset: f32 = 0.0;
        let mut t1: f32 = 0.0;
        let mut t4: f32 = t0;

        if data[3] != 0xf2 || data[4] != 0x7f {
            t1 = data[4] as f32 * 256.0 + data[3] as f32;
            let diff = t0 - t1;
            offset = diff / 2.0;

            if diff > 0.0 {
                let mut off = diff;
                while off > 200.0 {
                    off = off - 100.0;
                }

                if off < 100.0 {
                    off += 50.0;
                }

                t4 = t0 + off;
            }

            if offset > 1.0 {
                offset = 1.0;
            }
        }

        let mut toff = t0 + offset;
        t1 = t1 / 100.0;
        t0 = t0 / 100.0;
        toff = toff / 100.0;
        t4 = t4 / 100.0;

        println!("t0: {}, t1: {}, toff: {}, t4: {}", t0, t1, toff, t4);
        return Some((t0, t1, toff, t4));
    }
    None
}

// main函数里添加/修改
        loop {
            for event in bt_session.incoming(1000).map(BluetoothEvent::from) {
                if let Some(event) = event {
                    println!("recv: {:?}", event);
                    match event {
                        Value { object_path, value } => {
                            if let Some((raw, _, _, t)) = parse_mmc_data(value) {
                                println!("Raw t: {}, calibrated: {}", raw, t);
                            }
                        }
                        _ => {}
                    }
                }
            }
        }

运行显示解析出的温度:

recv: Value { object_path: "/org/bluez/hci0/dev_00_81_E8_DE_B0_80/service0034/char003a", value: [0, 41, 13, 115, 12, 100] }
t0: 33.69, t1: 31.87, toff: 33.7, t4: 35.51
Raw t: 33.69, calibrated: 35.51
recv: Value { object_path: "/org/bluez/hci0/dev_00_81_E8_DE_B0_80/service0034/char003a", value: [0, 43, 13, 121, 12, 100] }
t0: 33.71, t1: 31.93, toff: 33.72, t4: 35.49
Raw t: 33.71, calibrated: 35.49
recv: Value { object_path: "/org/bluez/hci0/dev_00_81_E8_DE_B0_80/service0034/char003a", value: [0, 45, 13, 128, 12, 100] }

已知问题

参考

Comment