LoginSignup
24
16

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-18

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

やりたいこと

RustでBluetooth Low Energy(以下BLE)デバイスと通信したい。
対象OSはWindows10です。
接続対象デバイスはソニー (Sony Interactive Entertainment) から出ているtoioコアキューブです。

Windows10でBLE

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

Windows10でBLEを使うにはどうやらUWP経由で使うのが正攻法らしいです。
次はcrates.ioでuwpを探してみました。
uwpというパッケージは存在したのですが、GitHubのコードが非常に残念な状態だったので利用は諦めました。

さらに調べた結果、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で聞いてみたらしたら現在未実装とのことでした。別のAPIから戻ってきた同型のデータを無理やり加工して渡すとかできないかと思いましたが、ミュータビリティー管理に厳しいRustでそんなことができるわけもなく諦めました。

やってみた

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

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

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

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

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を返します。

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の謎で、なぜそんな動きになるのか詳細は不明です。

    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のAPIgetgatt_services_for_uuid_async()を使ってGATTサービスを取得する
  4. BluetoothLEDevice3のAPIget_characteristics_async()を使って全キャラクタリスティックを読み出す

いきなり登場しましたが、BluetoothLEDevice3がキモです。
Microsoftの説明を見ると普通にBluetoothLEDeviceでgetGattServicesForUuidAsync()を呼べる様子ですが、rust-winrtではBluetoothLEDeviceからこのAPIを呼ぶことができません。

BluetoothLEDevice APIとWindowsバージョンの対応を見ると、
徐々にAPIが拡張され続けているのがわかります。rust-winrtはこの拡張が全部別扱いで

  • BluetoothLEDdevice
  • BluetoothLEDdevice2
  • BluetoothLEDdevice3
  • BluetoothLEDdevice4
  • BluetoothLEDdevice5

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

winrt-rust 0.6.0でのAPI対応表を作りました。
最新の対応関係はrust-winrtのdevice.rsを見てください。

rust-winrt Windows version SDK version Value added
BluetoothLEDdevice2 1511 10586 Appearance
BluetoothLEDdevice2 1511 10586 BluetoothAddressType
BluetoothLEDdevice2 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
BluetoothLEDdevice3 1703 15063 DeviceAccessInformation
BluetoothLEDdevice3 1703 15063 GetGattServicesAsync
BluetoothLEDdevice3 1703 15063 GetGattServicesAsync(BluetoothCacheMode)
BluetoothLEDdevice3 1703 15063 GetGattServicesForUuidAsync(Guid)
BluetoothLEDdevice3 1703 15063 GetGattServicesForUuidAsync(Guid,BluetoothCacheMode)
BluetoothLEDdevice3 1703 15063 RequestAccessAsync
BluetoothLEDdevice4 1709 16299 BluetoothDeviceId
BluetoothLEDdevice5 1803 17134 WasSecureConnectionUsedForPairing

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

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

    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>にします。

    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にします。

    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都合でハンドラの引数が下記のように決まっています。

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

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

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

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

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は今回の用途では使っていないので無視しています)

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を見て解決できました。

    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日目に書きました。
興味のある方はこちらも読んでみてください。

まとめ

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

パワーポイントコントローラーのソースコードをGitHubのリポジトリに置いておきます。
今回のBLE通信に関する部分はこのあたりです。

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

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

24
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
16