Rust で Bluetooth Low Energy デバイスと通信 (Windows10)


この記事はRust素人がWindows10上でBLE通信(GATTのみ)を行った記録です。
実はRustにもWindowsのAPIにも詳しくないので、とりあえず通信に成功した方法を書きました。
詳しい方からのツッコミをお待ちしております。


## やりたいこと

RustでBluetooth Low Energy（以下BLE）デバイスと通信したい。
対象OSはWindows10です。
接続対象デバイスはソニー (Sony Interactive Entertainment) から出ている[toio](https://toio.io/)の[コアキューブ](https://toio.github.io/toio-spec/)です。

## Windows10でBLE

[crates.io](https://crates.io)ではWindows上でBLEが扱えるパッケージを見つけられませんでした。

Windows10でBLEを使うにはどうやら[UWP経由で使う](https://docs.microsoft.com/en-us/windows/uwp/devices-sensors/bluetooth-low-energy-overview)のが正攻法らしいです。
次は[crates.io](https://crates.io)でuwpを探してみました。
uwpというパッケージは存在したのですが、GitHubのコードが非常に残念な状態だったので利用は諦めました。

さらに調べた結果、[winrtパッケージ](https://crates.io/crates/winrt)経由でUWP APIが利用可能だったので、これを使うことにしました。

### rust-winrt

winrtをRustから呼び出すためのパッケージですが、なんというかwinrtをそのまま呼び出して使う感じです。
winrtやuwpのことを知らないと何をしていいのか全く分かりません。

rust-winrtを使ってみて分かった注意点は以下です。

1. winrtのasync APIをRust側でawaitできない
2. Rust側で生成できないwinrtの型があり、その型の引数を必要とするwinrt APIは実質的に使えない

1 はblocking_get()を使ってwinrtのasync APIを同期待ちします。Rust側でasync/awaitが安定化する前に作られたパッケージなのでこれは仕方ないところです。
2 は具体的にはIIterable系が全滅で、この型でパラメーターリストみたいなのを必要とするAPIが全く使えませんでした。[GitHubのissueで聞いてみたら](https://github.com/contextfree/winrt-rust/issues/85)したら現在未実装とのことでした。別のAPIから戻ってきた同型のデータを無理やり加工して渡すとかできないかと思いましたが、ミュータビリティー管理に厳しいRustでそんなことができるわけもなく諦めました。


## やってみた

uwpのBLEまわりはWindows10のアップデートに伴って少しずつ拡張されてきた歴史があるようで、検索しても情報がそんなに多くない割にやり方がバラバラだったりして正直よくわかりません。
とりあえず[microsoftのBLEの説明](https://docs.microsoft.com/en-us/windows/uwp/devices-sensors/gatt-client)を参考にして、C#のコードで使われているAPIをほぼそのままRustから呼んでみることにしました。

今回は[toioコアキューブ](https://toio.github.io/toio-spec/)と接続したかったので、コードも接続対象に特化したものになっています。残念ながら汎用的なBLEデバイスとの接続に使える実装にはなっていません。
（API呼出し手順的にはほぼ同じでいけると思います）

### コアキューブ接続用のトレイト

toioコアキューブは仕様を見る限り、接続、キャラクタリスティックに対しての読み、書き、通知登録ができればなんとか制御できそうな気がします。


```rust
pub trait CoreCubeBLEAccess {
    fn new(name: String) -> Self;

    // 接続（ペアリング済みデバイス）
    fn connect_ref_id(&mut self, ref_id: &String) -> std::result::Result<bool, String>;

    // 接続（未ペアリングのデバイス、アドレス直接指定）
    fn connect(&mut self, address: u64) -> std::result::Result<bool, String>;

    // キャラクタリスティック read
    fn read(&self, characteristic_name: CoreCubeUuidName) -> std::result::Result<Vec<u8>, String>;

    // キャラクタリスティック write
    fn write(
        &self,
        characteristic_name: CoreCubeUuidName,
        bytes: &Vec<u8>,
    ) -> std::result::Result<bool, String>;

    // キャラクタリスティック notify 登録
    fn register_norify(
        &self,
        characteristic_name: CoreCubeUuidName,
        handler_func: fn(
            *mut CoreCubeNotifySender,
            *mut CoreCubeNotifyArgs,
        ) -> CoreCubeNotifyResult,
    ) -> std::result::Result<CoreCubeNotifyHandler, String>;
}
```


### connect

まずは接続から。
接続には大きく分けて以下の二種類があります。

* ペアリング済みデバイスへの接続
* 周辺デバイスをサーチして接続

ペアリング済みデバイスへの接続が簡単そうだったので、そっちから試しました。


#### ペアリングしているデバイスへの接続

ペアリングしているデバイスへの接続は

> 1. デバイスセレクタ（レジストリ情報のフィルタみたいなもの）を生成
> 2. レジストリからペアリング情報を取得
> 3. BLEデバイスの生成
> 4. GATTサービスの取得

という流れになります。

デバイスセレクタの生成は`GattDeviceService::get_device_selector_from_uuid(service_uuid)`を使います。service_uuid（ここではコアキューブのサービスUUIDとしています）を持っているBLEデバイス一覧を取得するデバイスセレクタが作られます。

`DeviceInformatio::find_all_async_aqs_filter()`でレジストリから情報を取得します。
これでservice_uuidを持ったデバイス一覧が得られます。

レジストリからのデバイス一覧を取得する関数です。
この関数はペアリング済みtoioコアキューブUUIDのVecを返します。

```rust
pub fn get_ble_devices() -> std::result::Result<Vec<String>, String> {
    let service_uuid = get_uuid(CoreCubeUuidName::Service).unwrap();
    let selector = GattDeviceService::get_device_selector_from_uuid(service_uuid).unwrap();
    let ref_selector = selector.make_reference();
    debug!("ref_selector: {}", ref_selector);

    let collection = DeviceInformation::find_all_async_aqs_filter(&ref_selector)
        .unwrap()
        .blocking_get()
        .expect("find_all_async failed")
        .unwrap();

    let mut uuid_list: Vec<String> = Vec::new();
    for device_info in collection.into_iter() {
        uuid_list.push(match device_info {
            Some(x) => x.get_id().unwrap().to_string(),
            None => return Err("Error: get_id()".to_string()),
        });
    }

    Ok(uuid_list)
}
```

ペアリング済みデバイスへの接続は`connect_ref_id()`で行います。
`BluetoothLEDevice::from_id_async()`でBLEデバイスを生成して、`ble_device.get_gatt_service()`でGATTサービスを取得します。

これで接続完了と思いますが、実際の接続タイミングはこれらのAPI呼出しとは一致しておらず、このconnect_ref_id()終了時にBLEデバイスに対して接続されていることはほぼありません。
実際に接続動作に入るのは、だいたいwriteやreadを行った時点です。
このあたりがuwpの謎で、なぜそんな動きになるのか詳細は不明です。

```rust
    fn connect_ref_id(&mut self, ref_id: &String) -> std::result::Result<bool, String> {
        // connect to device
        let ref_id = HString::new(ref_id);
        let ble_device = match BluetoothLEDevice::from_id_async(&ref_id.make_reference())
            .unwrap()
            .blocking_get()
        {
            Ok(bdev) => bdev.unwrap(),
            Err(_) => return Err("Error: from_id_async()".to_string()),
        };

        self.gatt_service =
            match ble_device.get_gatt_service(get_uuid(CoreCubeUuidName::Service).unwrap()) {
                Ok(service) => Some(service.unwrap()),
                Err(_) => return Err("Error: get_gatt_service()".to_string()),
            };
        self.ble_device = Some(ble_device);

        Ok(true)
    }
```

#### ペアリングしていないデバイスへの接続

ペアリングしていないデバイスへの接続は下記の手順で行います。

> 1. BLEアドレスを指定してBLEデバイスを生成(BluetoothLEDeviceができる)
> 2. BluetoothLEDeviceから**BluetoothLEDevice3**を生成する
> 3. **BluetoothLEDevice3**のAPI`getgatt_services_for_uuid_async()`を使ってGATTサービスを取得する
> 4. **BluetoothLEDevice3**のAPI`get_characteristics_async()`を使って全キャラクタリスティックを読み出す

いきなり登場しましたが、**BluetoothLEDevice3**がキモです。
[Microsoftの説明](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice.getgattservicesforuuidasync)を見ると普通にBluetoothLEDeviceで`getGattServicesForUuidAsync()`を呼べる様子ですが、rust-winrtではBluetoothLEDeviceからこのAPIを呼ぶことができません。

[BluetoothLEDevice APIとWindowsバージョンの対応](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice.getgattservicesforuuidasync)を見ると、
徐々にAPIが拡張され続けているのがわかります。rust-winrtはこの拡張が全部別扱いで

* BluetoothLEDdevice
* BluetoothLEDdevice2
* BluetoothLEDdevice3
* BluetoothLEDdevice4
* BluetoothLEDdevice5

と、5つに分かれています。

winrt-rust 0.6.0でのAPI対応表を作りました。
最新の対応関係は[rust-winrtのdevice.rs](https://github.com/contextfree/winrt-rust/blob/master/src/rt/gen/windows/devices.rs)を見てください。


|rust-winrt |Windows version|	SDK version|	Value added|
|---|---|---|---|
|<font color="violet">BluetoothLEDdevice2</font>|1511|	10586|	Appearance|
|<font color="violet">BluetoothLEDdevice2</font>|1511|	10586|	BluetoothAddressType|
|<font color="violet">BluetoothLEDdevice2</font>|1511|	10586|	DeviceInformation|
|BluetoothLEDdevice|1511|	10586|	FromBluetoothAddressAsync(UInt64,BluetoothAddressType)|
|BluetoothLEDdevice|1511|	10586|	GetDeviceSelectorFromAppearance|
|BluetoothLEDdevice|1511|	10586|	GetDeviceSelectorFromBluetoothAddress(UInt64)|
|BluetoothLEDdevice|1511|	10586|	GetDeviceSelectorFromBluetoothAddress(UInt64,BluetoothAddressType)|
|BluetoothLEDdevice|1511|	10586|	GetDeviceSelectorFromConnectionStatus|
|BluetoothLEDdevice|1511|	10586|	GetDeviceSelectorFromDeviceName|
|BluetoothLEDdevice|1511|	10586|	GetDeviceSelectorFromPairingState|
|<font color="orange">BluetoothLEDdevice3</font>|1703|	15063|	DeviceAccessInformation|
|<font color="orange">BluetoothLEDdevice3</font>|1703|	15063|	GetGattServicesAsync|
|<font color="orange">BluetoothLEDdevice3</font>|1703|	15063|	GetGattServicesAsync(BluetoothCacheMode)|
|<font color="orange">BluetoothLEDdevice3</font>|1703|	15063|	GetGattServicesForUuidAsync(Guid)|
|<font color="orange">BluetoothLEDdevice3</font>|1703|	15063|	GetGattServicesForUuidAsync(Guid,BluetoothCacheMode)|
|<font color="orange">BluetoothLEDdevice3</font>|1703|	15063|	RequestAccessAsync|
|<font color="green">BluetoothLEDdevice4</font>|1709|	16299|	BluetoothDeviceId|
|<font color="pink">BluetoothLEDdevice5</font>|1803|	17134|	WasSecureConnectionUsedForPairing|

2や3などの末尾に数字がついているものはquery_interfaceを使って生成します。
（`query_interface::<IBluetoothLEDevice2>()`や`query_interface::<IBluetoothLEDevice3>()`など）

手順の最後で行っている全キャラクタリスティックの読み出しですが、これをやらないとなぜか接続できませんでした。理由は不明です。

```rust
    fn connect(&mut self, address: u64) -> std::result::Result<bool, String> {
        // connect to device
        info!("search with address");
        let ble_device: ComPtr<BluetoothLEDevice>;
        let ble_device_async = BluetoothLEDevice::from_bluetooth_address_async(address).unwrap();
        ble_device = match ble_device_async.blocking_get() {
            Ok(bdev) => bdev.unwrap(),
            Err(_) => return Err("Error: from_id_async()".to_string()),
        };
        debug!("using IBluetoothLEDevice3 interface");
        let ble_dev3 = ble_device.query_interface::<IBluetoothLEDevice3>().unwrap();
        let gatt_services = match ble_dev3
            .get_gatt_services_for_uuid_async(get_uuid(CoreCubeUuidName::Service).unwrap())
            .unwrap()
            .blocking_get()
            .unwrap()
        {
            Some(s) => s.get_services().unwrap().unwrap(),
            None => {
                error!("error: get_gatt_services_async()");
                return Err("Error: get_gatt_service_async()".to_string());
            }
        };

        //thread::sleep(time::Duration::from_secs(3));
        self.gatt_service = match gatt_services.get_at(0) {
            Ok(service) => Some(service.unwrap()),
            Err(_) => return Err("Error: from_id_async()".to_string()),
        };
        self.ble_device = Some(ble_device);

        let gatt3 = self
            .gatt_service
            .clone()
            .unwrap()
            .query_interface::<IGattDeviceService3>()
            .unwrap();

        //dummy access
        let _chars = gatt3
            .get_characteristics_async()
            .unwrap()
            .blocking_get()
            .unwrap();

        debug!("complete");
        Ok(true)
    }
```


### read

読みだしたデータはIBuffer形式です。DataReaderを使ってVec\<u8\>にします。

```rust
    fn read(&self, characteristic_name: CoreCubeUuidName) -> std::result::Result<Vec<u8>, String> {
        let chr_list = self
            .gatt_service
            .clone()
            .unwrap()
            .get_characteristics(get_uuid(characteristic_name).unwrap())
            .unwrap()
            .unwrap();
        let chr = chr_list.get_at(0).expect("error: read").unwrap();

        let read_result = chr
            .read_value_with_cache_mode_async(BluetoothCacheMode::Uncached)
            .unwrap()
            .blocking_get()
            .expect("failed: read_value_with_cache_mode_async()")
            .unwrap();
        if read_result.get_status().unwrap() == GattCommunicationStatus::Success {
            let reader = DataReader::from_buffer(&read_result.get_value().unwrap().unwrap())
                .expect("error")
                .unwrap();
            let read_length = reader.get_unconsumed_buffer_length().expect("error") as usize;
            let mut read_result = Vec::<u8>::with_capacity(read_length);

            for _i in 0..read_length {
                read_result.push(0);
            }
            reader.read_bytes(read_result.as_mut()).expect("error");
            return Ok(read_result);
        } else {
            error!("Error: read failed");
        }

        Err("Error: read".to_string())
    }
```

### write

書き込むデータもIBuffer形式なので、今度はDataWirterを使ってVec\<u8\>からIBufferにします。

```rust
    fn write(
        &self,
        characteristic_name: CoreCubeUuidName,
        bytes: &Vec<u8>,
    ) -> std::result::Result<bool, String> {
        let chr_list = self
            .gatt_service
            .clone()
            .unwrap()
            .get_characteristics(get_uuid(characteristic_name).unwrap())
            .unwrap()
            .unwrap();

        let chr = chr_list.get_at(0).expect("error: read").unwrap();
        let writer = DataWriter::new();
        writer.write_bytes(bytes).expect("error");
        let buffer = writer.detach_buffer().expect("error").unwrap();

        debug!("start to write_value_async()");
        let write_result = chr
            .write_value_async(&buffer)
            .unwrap()
            .blocking_get()
            .expect("failed");

        if write_result != GattCommunicationStatus::Success {
            error!("failed: write_value_async()");
            return Err("Error: write failed".to_string());
        }

        Ok(true)
    }
```

### notify

#### notifyハンドラ

notifyはwinrt都合でハンドラの引数が下記のように決まっています。

```rust
fn notify(
    sender: *mut GattCharacteristic,
    arg: *mut GattValueChangedEventArgs,
) -> Result<()>
```

これではCoreCubeBLEAccessトレイトを使う側でも常にwinrt関係のuseが必要になってしまい、使い勝手が**非常に悪い**です。
senderはStringあたりに、argはVec\<u8\>にできないかとnotify登録関数内でクロージャを作ったりなど試してみたのですが、うまくいかなかったので諦めました。
とりあえずuse地獄を避けるためにtypeでごまかします。

```rust
pub type CoreCubeNotifySender = GattCharacteristic;
pub type CoreCubeNotifyArgs = GattValueChangedEventArgs;
pub type CoreCubeNotifyResult = Result<()>;
```

しかしCoreCubeNotifyArg(というかGattValueChangedEventArgs)は結局IBufferなので、変換処理も作ります。

```rust
pub fn get_notify_data(arg: *mut CoreCubeNotifyArgs) -> Vec<u8> {
    let mut notify_data = Vec::<u8>::new();
    unsafe {
        let arg_ref = arg.as_ref().unwrap();
        let arg_ibuffer = arg_ref
            .get_characteristic_value()
            .expect("error: get_characteristic_value")
            .unwrap();

        let reader = DataReader::from_buffer(&arg_ibuffer)
            .expect("error")
            .unwrap();

        let read_length = reader.get_unconsumed_buffer_length().expect("error") as usize;

        for _i in 0..read_length {
            notify_data.push(0);
        }
        reader.read_bytes(notify_data.as_mut()).expect("error");
    }
    notify_data
}
```

こうしてnotify関数は下記のようなwinrt感が少し薄まった雰囲気になりました。
もしかしてこういうのはマクロを使うとうまく隠せたりするんでしょうか。
（_senderは今回の用途では使っていないので無視しています）

```rust
fn handler_func(
    _sender: *mut CoreCubeNotifySender,
    arg: *mut CoreCubeNotifyArgs,
) -> CoreCubeNotifyResult {
    let data = get_notify_data(arg);

    // arg を見て何か処理する

    Ok(())
}
```



#### notify登録処理

notify登録は下記の手順で行います。

> 1. ハンドラ関数を`TypedEventHandler::new(handler_func)`でTypedEventHanderにする
> 2. イベントに追加（ここではvalue_changedに追加しています）

ハンドラの登録手順がよくわからなかったのですが、[GitHubのissue](https://github.com/contextfree/winrt-rust/issues/56)を見て解決できました。

```rust
    fn register_norify(
        &self,
        characteristic_name: CoreCubeUuidName,
        handler_func: fn(
            *mut CoreCubeNotifySender,
            *mut CoreCubeNotifyArgs,
        ) -> CoreCubeNotifyResult,
    ) -> std::result::Result<CoreCubeNotifyHandler, String> {
        let chr_name = characteristic_name.to_string();
        let chr_list = self
            .gatt_service
            .clone()
            .unwrap()
            .get_characteristics(get_uuid(characteristic_name).unwrap())
            .unwrap()
            .unwrap();

        let chr = chr_list.get_at(0).expect("error: read").unwrap();
        let winrt_handler = TypedEventHandler::new(handler_func);
        let token = Some(
            chr.add_value_changed(&winrt_handler.clone())
                .expect("error"),
        );

        chr.write_client_characteristic_configuration_descriptor_async(
            GattClientCharacteristicConfigurationDescriptorValue::Notify,
        )
        .unwrap()
        .blocking_get()
        .expect("failed");

        let handler = CoreCubeNotifyHandler {
            name: self.name.clone(),
            characteristic_name: chr_name.clone(),
            characteristic: chr,
            token: token,
        };

        Ok(handler)
    }
```



## 応用編 （パワポコントローラーをつくる）

BLE通信ができるようになったので、toioコアキューブを使ってパワポコントローラーを作ってみました。
詳細は[toio™(ロボットトイ | toio（トイオ）) Advent Calendar 2019 19日目](https://qiita.com/kaz399/private/8b41ae811c26a3c8bac8)に書きました。
興味のある方はこちらも読んでみてください。

## まとめ

winrtのお作法がよくわからず苦戦しました。
rust-winrtについては、もう少しドキュメントやサンプルコードが充実して欲しいです。

パワーポイントコントローラーのソースコードを[GitHubのリポジトリ](https://github.com/kaz399/rs_toio_cube)に置いておきます。
今回のBLE通信に関する部分は[このあたり](https://github.com/kaz399/rs_toio_cube/blob/master/core_cube/src/win10.rs)です。


とりあえず通信できるところまで作れたので満足なのですが、次はもっとRust感のある何かに挑戦してみたいです。

それでは皆様よいクリスマスを。