Using rust, blurz to read from a BLE device
Tags: rust, bluetooth
Published: 2020-06-23 22:05:18

After buying some cheap Xiaomi hygrometer-thermometers off AliExpress and learning they have Bluetooth connectivity, I decided to see if I could read the temperature and humidity values without the companion app.

![Xiaomi Mijia 2 Hygrometer-Thermometer](https://dev-to-uploads.s3.amazonaws.com/i/3kqeiooe8tmekr11ow63.jpg)
<figcaption>Xiaomi Mijia 2 Hygrometer-Thermometer</figcaption>

#### Project structure
```
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── explore_device.rs
│   └── main.rs
```

#### Adding dependencies
Assuming you have rust installed and ready to go (if not, go [here](https://www.rust-lang.org/learn/get-started)), generate a new project and add [blurz](https://crates.io/crates/blurz) as a dependency. We'll also be using `regex` and `lazy_static` (see [this](https://docs.rs/regex/1.3.9/regex/#example-avoid-compiling-the-same-regex-in-a-loop)).
```rust
# Cargo.toml
[dependencies]
blurz = "0.4.0"
regex = "1"
lazy_static = "1.4.0"
```
**NOTE:** `blurz` depends on [dbus](https://crates.io/crates/dbus), so you may need to install some
`libdbus` dev packages.
As I write this, the crate page has instructions for Ubuntu. For me, on Fedora 32, that just meant `dnf install dbus-devel`.

#### Finding devices
Then, the first thing to do is to look for devices.
```rust
# src/main.rs
let bt_session = &BluetoothSession::create_session(None).unwrap();
let adapter: BluetoothAdapter = BluetoothAdapter::init(bt_session).unwrap();
let adapter_id = adapter.get_id();
let discover_session = BluetoothDiscoverySession::create_session(&bt_session, adapter_id).unwrap();
discover_session.start_discovery().unwrap();
let device_list = adapter.get_device_list().unwrap();
discover_session.stop_discovery().unwrap();
```
```rust
# device_list
[
    "/org/bluez/hci0/dev_A4_C1_38_15_03_55",
    "/org/bluez/hci0/dev_A4_C1_38_64_7E_DB",
    "/org/bluez/hci0/dev_A4_C1_38_F3_C9_A9"
]
```
`get_device_list` gives us the `object_path` of each device in range. It's probably helpful to learn the names of the devices.
```rust
# src/main.rs
for device_path in device_list {
    let device = BluetoothDevice::new(bt_session, device_path.to_string());
    println!("Device: {:?} Name: {:?}", device_path, device.get_name().ok());
}
```
```rust
# Terminal output
Device: "/org/bluez/hci0/dev_A4_C1_38_15_03_55" Name: "LYWSD03MMC"
Device: "/org/bluez/hci0/dev_A4_C1_38_64_7E_DB" Name: "LYWSD03MMC"
Device: "/org/bluez/hci0/dev_A4_C1_38_F3_C9_A9" Name: "LYWSD03MMC"
```
I have three devices, but let's focus on just one of them.
```rust
# src/main.rs
let device = BluetoothDevice::new(bt_session, String::from("/org/bluez/hci0/dev_A4_C1_38_15_03_55"));
```

#### GATT Services
These are [BLE](https://en.wikipedia.org/wiki/Bluetooth_Low_Energy) devices. After we connect to them, we can explore the GATT profile and learn what information is available.
![GATT Profile](https://dev-to-uploads.s3.amazonaws.com/i/e65ltj6gk42co4a15biq.png)
<figcaption>GATT Profile diagram</figcaption>

##### Connecting to a device
```rust
# src/main.rs
if let Err(e) = device.connect(10000) {
    println!("Failed to connect {:?}: {:?}", device.get_id(), e);
} else {
    // We need to wait a bit after calling connect to safely
    // get the gatt services
    thread::sleep(Duration::from_millis(5000));
    // Interact with device
    device.disconnect().unwrap();
}
```
##### Listing Services
```rust
# src/explore_device.rs
const UUID_REGEX: &str = r"([0-9a-f]{8})-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}";

let services_list = match device.get_gatt_services().unwrap()

lazy_static! {
  static ref RE: Regex = Regex::new(UUID_REGEX).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(1).map_or("", |m| m.as_str());

    println!("Service UUID: {:?} Assigned Number: 0x{:?}", uuid, assigned_number);
}
```
>A UUID is a 128-bit value. To reduce the burden of storing and transferring 128-bit UUID values, a range of UUID values has been pre-allocated for assignment to often-used, registered purposes. UUID values in the pre-allocated range have aliases that are represented as 16-bit or 32-bit values.
>_in [Bluetooth Core Specification](https://www.bluetooth.com/specifications/bluetooth-core-specification/)_

In this case they appear to be 16-bit values. I removed trailing zeros for readability. Aything starting with `0x18XX` is defined in the [specification](https://www.bluetooth.com/specifications/gatt/services/), the others were probably added by the manufacturer.
```rust
# Terminal output
Service UUID: "00000100-0065-6c62-2e74-6f696d2e696d" Assigned Number: 0x"0100"
Service UUID: "0000fe95-0000-1000-8000-00805f9b34fb" Assigned Number: 0x"fe95"
Service UUID: "ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccb0"
Service UUID: "00010203-0405-0607-0809-0a0b0c0d1912" Assigned Number: 0x"00010203"
Service UUID: "0000180f-0000-1000-8000-00805f9b34fb" Assigned Number: 0x"180f"
Service UUID: "0000180a-0000-1000-8000-00805f9b34fb" Assigned Number: 0x"180a"
Service UUID: "00001801-0000-1000-8000-00805f9b34fb" Assigned Number: 0x"1801"
Service UUID: "00001800-0000-1000-8000-00805f9b34fb" Assigned Number: 0x"1800"
```
##### Listing characteristics
Now that we have a list of services, we can check what characteristics they have. Note, I'll be focusing on Service `0xebe0ccb0` since I already know it contains the information I'm looking for.
```rust
# src/explore_device.rs
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(1).map_or("", |m| m.as_str());
    let flags = characteristic.get_flags().unwrap();

    println!(" Characteristic Assigned Number: 0x{:?} Flags: {:?}", assigned_number, flags);
}
```

```rust
# Terminal output
Characteristic UUID: "ebe0ccd9-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccd9" Flags: ["write", "notify"]
Characteristic UUID: "ebe0ccd8-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccd8" Flags: ["write"]
Characteristic UUID: "ebe0ccd7-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccd7" Flags: ["read", "write"]
Characteristic UUID: "ebe0ccd1-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccd1" Flags: ["write"]
Characteristic UUID: "ebe0ccc8-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccc8" Flags: ["write"]
Characteristic UUID: "ebe0ccc4-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccc4" Flags: ["read"]
Characteristic UUID: "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccc1" Flags: ["read", "notify"]
Characteristic UUID: "ebe0ccbe-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccbe" Flags: ["read", "write"]
Characteristic UUID: "ebe0ccbc-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccbc" Flags: ["notify"]
Characteristic UUID: "ebe0ccbb-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccbb" Flags: ["read"]
Characteristic UUID: "ebe0ccba-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccba" Flags: ["read", "write"]
Characteristic UUID: "ebe0ccb9-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccb9" Flags: ["read"]
Characteristic UUID: "ebe0ccb7-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccb7" Flags: ["read", "write"]
```
Now... This doesn't look very useful, does it? Unless we're the manufacturer, we have no idea what any of this means. That is where descriptors come in handy!
##### Listing descriptors
```rust
# src/explore_device.rs
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(1).map_or("", |m| m.as_str());
    let value = descriptor.read_value(None).unwrap();

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

```rust
# Terminal output
Characteristic Assigned Number: 0x"ebe0ccd9" Flags: ["write", "notify"]
  Descriptor Assigned Number: 0x"00002902" Read Value: "[0, 0]"
  Descriptor Assigned Number: 0x"00002901" Read Value: "[112, 97, 114, 97, 95, 118, 97, 108, 117, 101, 95, 103, 101, 116, 0]"
Characteristic Assigned Number: 0x"ebe0ccd8" Flags: ["write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "[115, 101, 116, 32, 99, 111, 110, 110, 32, 105, 110, 116, 101, 114, 118, 97, 108, 0]"
Characteristic Assigned Number: 0x"ebe0ccd7" Flags: ["read", "write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "[99, 111, 109, 102, 111, 114, 116, 97, 98, 108, 101, 32, 116, 101, 109, 112, 32, 97, 110, 100, 32, 104, 117, 109, 105, 0]"
Characteristic Assigned Number: 0x"ebe0ccd1" Flags: ["write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "[99, 108, 101, 97, 114, 32, 100, 97, 116, 97, 0]"
Characteristic Assigned Number: 0x"ebe0ccc8" Flags: ["write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "[100, 105, 115, 99, 111, 110, 110, 101, 99, 116, 0]"
Characteristic Assigned Number: 0x"ebe0ccc4" Flags: ["read"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "[66, 97, 116, 116, 0]"
Characteristic Assigned Number: 0x"ebe0ccc1" Flags: ["read", "notify"]
  Descriptor Assigned Number: 0x"00002902" Read Value: "[0, 0]"
  Descriptor Assigned Number: 0x"00002901" Read Value: "[84, 101, 109, 112, 101, 114, 97, 116, 117, 114, 101, 32, 97, 110, 100, 32, 72, 117, 109, 105, 100, 105, 116, 121, 0]"
Characteristic Assigned Number: 0x"ebe0ccbe" Flags: ["read", "write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "[84, 101, 109, 112, 101, 114, 97, 116, 117, 114, 101, 32, 85, 105, 110, 116, 0]"
Characteristic Assigned Number: 0x"ebe0ccbc" Flags: ["notify"]
  Descriptor Assigned Number: 0x"00002902" Read Value: "[0, 0]"
  Descriptor Assigned Number: 0x"00002901" Read Value: "[68, 97, 116, 97, 32, 78, 111, 116, 105, 102, 121, 0]"
Characteristic Assigned Number: 0x"ebe0ccbb" Flags: ["read"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "[68, 97, 116, 97, 32, 82, 101, 97, 100, 0]"
Characteristic Assigned Number: 0x"ebe0ccba" Flags: ["read", "write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "[73, 110, 100, 101, 120, 0]"
Characteristic Assigned Number: 0x"ebe0ccb9" Flags: ["read"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "[68, 97, 116, 97, 32, 67, 111, 117, 110, 116, 0]"
Characteristic Assigned Number: 0x"ebe0ccb7" Flags: ["read", "write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "[84, 105, 109, 101, 0]"
```
Well, that certainly looks promising, but not yet useful, right? Luckily, descriptors are well defined so we can check the [specification](https://www.bluetooth.com/specifications/gatt/descriptors/) to help us make sense of this.
`0x2902`, or Client Characteristic Configuration, describes whether the device has Notifications or Indications enabled or disabled. I found [this](https://www.onethesis.com/2015/11/21/ble-introduction-notify-or-indicate/) to be a good summary on that subject. That still doesn't tells us what this characteristic is, though.
`0x2901`, however, is named Characteristic User **Description** and is formated as an utf-8. Hurray! Let's turn those `Vec<u8>` into readable strings then.
```rust
# src/explore_device.rs
let value = match &assigned_number[4..] {
  "2901" => str::from_utf8(&value).unwrap().to_string(),
  _ => format!("{:?}", value)
};

println!("    Descriptor Assigned Number: 0x{:?} Read Value: {:?}", assigned_number, value);
```
```rust
# Terminal output
Service UUID: "ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6" Assigned Number: 0x"ebe0ccb0"
Characteristic Assigned Number: 0x"ebe0ccd9" Flags: ["write", "notify"]
  Descriptor Assigned Number: 0x"00002902" Read Value: "[0, 0]"
  Descriptor Assigned Number: 0x"00002901" Read Value: "para_value_get\u{0}"
Characteristic Assigned Number: 0x"ebe0ccd8" Flags: ["write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "set conn interval\u{0}"
Characteristic Assigned Number: 0x"ebe0ccd7" Flags: ["read", "write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "comfortable temp and humi\u{0}"
Characteristic Assigned Number: 0x"ebe0ccd1" Flags: ["write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "clear data\u{0}"
Characteristic Assigned Number: 0x"ebe0ccc8" Flags: ["write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "disconnect\u{0}"
Characteristic Assigned Number: 0x"ebe0ccc4" Flags: ["read"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "Batt\u{0}"
Characteristic Assigned Number: 0x"ebe0ccc1" Flags: ["read", "notify"]
  Descriptor Assigned Number: 0x"00002902" Read Value: "[0, 0]"
  Descriptor Assigned Number: 0x"00002901" Read Value: "Temperature and Humidity\u{0}"
Characteristic Assigned Number: 0x"ebe0ccbe" Flags: ["read", "write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "Temperature Uint\u{0}"
Characteristic Assigned Number: 0x"ebe0ccbc" Flags: ["notify"]
  Descriptor Assigned Number: 0x"00002902" Read Value: "[0, 0]"
  Descriptor Assigned Number: 0x"00002901" Read Value: "Data Notify\u{0}"
Characteristic Assigned Number: 0x"ebe0ccbb" Flags: ["read"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "Data Read\u{0}"
Characteristic Assigned Number: 0x"ebe0ccba" Flags: ["read", "write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "Index\u{0}"
Characteristic Assigned Number: 0x"ebe0ccb9" Flags: ["read"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "Data Count\u{0}"
Characteristic Assigned Number: 0x"ebe0ccb7" Flags: ["read", "write"]
  Descriptor Assigned Number: 0x"00002901" Read Value: "Time\u{0}"
```
That's more like it! Now we know which of these
characteristics (`0xebe0ccc1`, with the description _Temperature and Humidity_) carries Temperature and Humidity values. Now, notice how it has the `notify` flag. We need to tell the device we're interested in being notified.
##### Notifications and Bluetooth Events
We're only focusing on one characteristic, so we'll call `start_notify` on it.
```rust
# main.rs
let temp_humidity = BluetoothGATTCharacteristic::new(bt_session, String::from("/org/bluez/hci0/dev_A4_C1_38_15_03_55/service0021/char0035"));
temp_humidity.start_notify().unwrap();
loop {
  for event in BluetoothSession::create_session(None).unwrap().incoming(1000).map(BluetoothEvent::from) {
    println!("event {:?}", event);
  }
}
```
```rust
# Terminal output
event Some(ServicesResolved { object_path: "/org/bluez/hci0/dev_A4_C1_38_15_03_55", services_resolved: true })
event None
event Some(Value { object_path: "/org/bluez/hci0/dev_A4_C1_38_15_03_55/service0021/char0035", value: [58, 10, 63, 192, 10] })
event Some(Value { object_path: "/org/bluez/hci0/dev_A4_C1_38_15_03_55/service0021/char0035", value: [56, 10, 63, 192, 10] })
event Some(Value { object_path: "/org/bluez/hci0/dev_A4_C1_38_15_03_55/service0021/char0035", value: [60, 10, 63, 192, 10] })
event Some(Value { object_path: "/org/bluez/hci0/dev_A4_C1_38_15_03_55/service0021/char0035", value: [57, 10, 63, 192, 10] })
```
Among other events, we can see the ones carrying the values that interest us. So now we need to decode this information. Humidity is straightforward, I can see it in `value[2]` but the Temperature actually kept me confused for a while, since I was looking for a float. Turns out, the first two bytes contain the temperature, transformed. Imagine the device's display reads 22.4ºC, the value of the two bytes as a `i16` will actually be `2240`.
`2240 * 0.01 = 22.4`.
_As I write this, I realize I didn't test with negative temperatures... Maybe I'll get around to see if this still holds._
```rust
# src/main.rs
let value = match event.unwrap() {
  BluetoothEvent::Value {object_path : _, value} => value,
  _ => continue
};

// Converting to i16
let mut temperature_array = [0; 2];
temperature_array.clone_from_slice(&value[..2]);
let temperature = i16::from_le_bytes(temperature_array) as f32 * 0.01;

let humidity = value[2];

println!("Temperature: {:?}ºC Humidity: {:?}%", temperature, humidity);
```
And there we finally have it, the values in a human readable format.
```rust
# Terminal output
Temperature: 26.12ºC Humidity: 63%
Temperature: 26.09ºC Humidity: 63%
Temperature: 26.12ºC Humidity: 63%
Temperature: 26.12ºC Humidity: 63%
```

I hope this was interesting and helpful. I tried to avoid boilerplate code on the snippets, so be sure to check out the [project on Github](https://github.com/lcsfelix/reading-xiaomi-temp/tree/devto-post). And if you have any questions or comments, leave them below.