Android
Xamarin
BLE

Xamarin でAndroid BLEを使う

はじめに

XamarinでAndroid BLEでの開発の記事があまりなかったので、書いていこうかと思います。

開発環境

PC

  • Xamarin(.netstandard 2.0)
  • Windows 10 バージョン1803

Android

  • バージョン5(Lollipop,API Level 21)

ソリューションの構成は

  • Project(Solution)
    • Project.Core(Project(共有))
      • IStatusObtainable.cs(Interface)
      • GetStatus.cs(ここからプラットフォーム毎にBLE通信機能を呼び出す)
    • Project.Droid(Project(Android固有))
      • BLEGattCallback.cs(今回の解説)
      • BLEScanCallback.cs(今回の解説)
      • GetStatusAndroid.cs(今回の解説)
      • ActivityMain.xml

となっています。

開発手段

XamarinでBLEアプリを開発するための様々なNuGetパッケージが提供されていますが、なかなかうまく使えなかったので、Android標準のAPIを使って開発していきました。

権限設定

AndroidManifest.xml
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

追加しておかないと、権限で弾かれます。特に、位置情報権限も必要なので要チェックです。

xmlを直接編集ではなく、Androidのプロジェクトを右クリック->「プロパティ」から設定もできます。

GATTプロファイルって?

様々な情報は

  • Device
    • Service
      • Characteristics
        • Descriptor

このような構造で格納されています。今回の目的はCharacteristicsの値を読むことと、Notifyの設定をDescriptorに書き込むことです。

大まかな流れ

Xamarin Fromsが提供しているDependency Serviceを利用する

Android固有の機能になるので、Dependency Serviceを使って処理を呼び出します。
Dependency Serviceの利用についてはここでは説明していません。

通信に必要なAdapterやScanCallbackなどを用意してスキャンする

Bluetooth AdapterやBluetooth ScannerなどはContext(アプリのインスタンス)から取り出して使う必要があります。
また、スキャンした結果などが格納されるScanCallbackやGattCallbackも用意する必要があります。また、ここからはスキャンした結果のインスタンスに対して処理を行っていくので、newしてはいけません。

スキャンした結果から、対象のデバイスに接続する

BLE端末との通信ではGATTプロファイルを使用します。
今回は、そのGATTプロファイルで通信を確立します。

Notifyの設定

BLEで値が変わったら通知してもらうように登録します。

つまり?

  • GattCallbackが入ったScanCallbackをStartScanに渡してスキャンを開始する。
  • スキャン結果があればScanCallbackを使って目的のデバイスかどうか判断する。(ここで、結果が入ったインスタンスが作られる。)
  • 目的のデバイスであった場合、ScanCallbackに入っているGattCallbackでキャタリスティックの値を読みに行く。ついでにNotifyの設定も行う。(ScanCallbackのインスタンスに対してGattCallbackで処理を行う)

STEP1 通信に必要なAdapterやScannerなどを用意してスキャンする

ここでの主な処理は

  • ContextからManager,Adapter,Scannerを取り出す
  • ScanCallback,ScanFilters,ScanSettingsをStartScanの引数として渡し、スキャンを開始する

です。

前提として、Dependency Serviceから呼び出されています。
Dependency Serviceについては、別途、まとめたいと思います。

GetStatusAndroid.cs
[assembly: Xamarin.Forms.Dependency(typeof(Project.Droid.GetStatusAndroid))]
namespace Project.Droid
{
    using System.Collections.Generic;
    using System.ComponentModel;
    using Android.Bluetooth;
    using Android.Bluetooth.LE;
    using Android.Content;
    using Android.OS;

    public class GetStatusAndroid : Project.Core.IStatusObtainable
    {
        /// <summary>
        /// ContextからBluetoothManagerを取り出して、格納します。
        /// </summary>
        private BluetoothManager manager = (BluetoothManager)Xamarin.Forms.Forms.Context.GetSystemService(Context.BluetoothService);

        /// <summary>
        /// BluetoothManagerからBluetoothAdapterを取り出して、格納します。
        /// </summary>
        private BluetoothAdapter adapter = default(BluetoothAdapter);

        /// <summary>
        /// BluetoothAdapterからBluetoothScannerを取り出して、格納します。
        /// </summary>
        private BluetoothLeScanner scanner;

        /// <summary>
        /// アドバタイズ中のペリフェラルに対してスキャンをする際、フィルタをかけます。
        /// </summary>
        private List<ScanFilter> filters = new List<ScanFilter>();

        /// <summary>
        /// アドバタイズ中のペリフェラルに対してスキャンをする際の設定を行います。
        /// </summary>
        private ScanSettings settings;

        /// <summary>
        /// スキャンした結果が格納されるコールバックです。
        /// </summary>
        private BLEScanCallback scanCallback = new BLEScanCallback();

        /// <summary>
        /// Gets or sets a value indicating whether true or false Scanを行っているかどうかが格納されます。
        /// </summary>
        public bool IsScanning { get; set; } = false;

        /// <summary>
        /// BLEデバイスから受信した値が格納されます。
        /// </summary>
        public string Status { get; set; } = string.Empty;

        /// <summary>
        /// BLEデバイスとのConnectionの状態が格納されます。
        /// </summary>
        public string ConnectionStatus { get; set; } = "Disconnected";

        /// <summary>
        /// BLEの状態を返却します。
        /// </summary>
        /// <returns>BLEの状態</returns>
        public string GetStatus()
        {
            if (this.IsScanning == true)
            {
                this.Status = this.scanCallback.CatchStatus();
                this.ConnectionStatus = this.scanCallback.ConnectionStatus;

                return this.Status;
            }
            else
            {
                this.adapter = this.manager.Adapter;

                // (Filters内容:Device.Name == "BLETest")
                this.filters.Add(new ScanFilter.Builder().SetDeviceName("BLETest").Build());
                this.settings = new ScanSettings.Builder().SetScanMode(Android.Bluetooth.LE.ScanMode.Balanced).SetCallbackType(ScanCallbackType.AllMatches).Build();

                this.ScanDevice();

                return this.Status;
            }
        }

        private void ScanDevice()
        {
            this.scanner = this.adapter.BluetoothLeScanner;
            this.scanner.StartScan(this.filters, this.settings, this.scanCallback);
            this.IsScanning = true;

            // scanCallbackのプロパティから取り出しています。
            this.Status = this.scanCallback.Status;

            // scanCallbackのプロパティから取り出しています。
            this.ConnectionStatus = this.scanCallback.ConnectionStatus;
        }
    }
}

以下、スクリプトの解説です。

AdapterやScannerを用意する

通信に必要なAdapterやScannerはContextから取り出す必要があります。

        private BluetoothManager manager = (BluetoothManager)Xamarin.Forms.Forms.Context.GetSystemService(Context.BluetoothService);

取り出したBluetooth ManagerからAdapterやScannerを取り出して、BLE端末をスキャンするのに使います。

Scanする際のフィルタと設定を準備する

宣言

        private List<ScanFilter> filters = new List<ScanFilter>();
        private ScanSettings settings;

値の代入

                // (Filters内容:Device.Name == "BLETest")
                this.filters.Add(new ScanFilter.Builder().SetDeviceName("BLETest").Build());
                this.settings = new ScanSettings.Builder().SetScanMode(Android.Bluetooth.LE.ScanMode.Balanced).SetCallbackType(ScanCallbackType.AllMatches).Build();

今回はデバイスの名前でフィルタをかけます。条件は"BLETest"という名前のデバイスです。

Scanを行う

        private void ScanDevice()
        {
            this.scanner = this.adapter.BluetoothLeScanner;
            this.scanner.StartScan(this.filters, this.settings, this.scanCallback);
            this.IsScanning = true;

            // scanCallbackのプロパティから取り出しています。
            this.Status = this.scanCallback.Status;

            // scanCallbackのプロパティから取り出しています。
            this.ConnectionStatus = this.scanCallback.ConnectionStatus;
        }

取り出したScannerにはStartScanという関数が用意されています。
引数は(ScanFilter filter,ScanSettings settings,ScanCallback callback)です。
スキャン中にスキャンを開始しないように、IsCanning変数を用意して、Trueを格納しています。
値の受け渡しはBLEのNotifyを全く考慮できていませんが、INotifyPropertyChangedインタフェースを実装すればよりスムーズになるかもしれません・・・。

STEP2 ScanCallbackを実装する

Android APIで提供されているScanCallbackを実装します。
ScanCallbackは抽象メソッドなので、overrideする必要があります。

主な処理は

  • フィルタと一致するデバイスが見つかったときに、GATTプロファイルで通信を行う

です。

BLEScanCallback.cs
namespace Project.Droid
{
    using Android.Bluetooth;
    using Android.Bluetooth.LE;
    using Android.Runtime;

    public class BLEScanCallback : ScanCallback
    {
        /// <summary>
        /// GATT Connectionを確立するのに必要な情報が格納されます。
        /// </summary>
        private BluetoothGatt bleGatt;

        /// <summary>
        /// GATT Connectionによって得られた結果が格納されます。
        /// </summary>
        private BLEGattCallback gattCallback;

        public BLEScanCallback()
        {
            this.gattCallback = new BLEGattCallback();
        }

        /// <summary>
        /// Gets or sets BLEデバイスの値の状態が格納されます。
        /// </summary>
        public string Status { get; set; } = string.Empty;

        /// <summary>
        /// Gets or sets BLEデバイスとのConnection状態が格納されます。
        /// </summary>
        public string ConnectionStatus { get; set; } = "Disconnected";

        /// <summary>
        /// スキャンされたペリフェラルがあった場合、呼び出されます。
        /// </summary>
        /// <param name="callbackType">callbackの種類</param>
        /// <param name="result">スキャン結果</param>
        public override void OnScanResult([GeneratedEnum] ScanCallbackType callbackType, ScanResult result)
        {
            if (result.Device.Name == "BLETest")
            {
                // 自動接続をfalseにしないと接続できない不具合があるそうです。
                this.bleGatt = result.Device.ConnectGatt(Xamarin.Forms.Forms.Context, false, this.gattCallback, BluetoothTransports.Le);

                this.bleGatt.Connect();

                this.Status = this.gattCallback.Status;
            }
        }

        /// <summary>
        /// Scanに失敗した場合、呼び出されます。
        /// </summary>
        /// <param name="errorCode">エラーコード</param>
        public override void OnScanFailed([GeneratedEnum] ScanFailure errorCode)
        {
            base.OnScanFailed(errorCode);
        }

        /// <summary>
        /// GattCallbackからBLEデバイスの値とBLEデバイスとのConnection状態を取り出します。
        /// </summary>
        /// <returns>BLEデバイスの値</returns>
        public string CatchStatus()
        {
            this.ConnectionStatus = this.gattCallback.ConnectionStatus;
            return this.gattCallback.Status;
        }
    }
}

以下、スクリプトの解説です。

        public override void OnScanResult([GeneratedEnum] ScanCallbackType callbackType, ScanResult result)
        {
            if (result.Device.Name == "BLETest")
            {
                // 自動接続をfalseにしないと接続できない不具合があるそうです。
                this.bleGatt = result.Device.ConnectGatt(Xamarin.Forms.Forms.Context, false, this.gattCallback, BluetoothTransports.Le);

                this.bleGatt.Connect();

                this.Status = this.gattCallback.Status;
            }
        }

Scanした結果があった場合に呼び出されます。
一応、フィルタをかけているのですが動作が不安定なようなので念のために、"BLETest"という名前のデバイスが検出されたときにGATTプロファイルで接続するようにしています。

STEP3 GattCallbackを実装する

今回はGATTプロファイルを利用して通信を行うので、GattCallbackを実装する必要があります。

ScanCallbackにて、対象のデバイスが見つかったらそのインスタンスに対して処理を行っていきます。主な処理は

  • GATTプロファイルでの接続を確立する
  • 目的のサービスを見つける
  • 目的のサービスの中から目的のキャラクタリスティックを見つける
  • Notifyの設定を行う

です。

BLEGattCallback.cs
namespace Project.Droid
{
    using System;
    using System.Linq;
    using Android.Bluetooth;
    using Android.Runtime;

    internal class BLEGattCallback : BluetoothGattCallback
    {
        /// <summary>
        /// スキャン対象のService UUIDです。
        /// </summary>
        private Java.Util.UUID serviceUUID = Java.Util.UUID.FromString("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");

        /// <summary>
        /// スキャン対象のCharacteristics UUIDです。
        /// </summary>
        private Java.Util.UUID characteristicsUUID = Java.Util.UUID.FromString("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");

        /// <summary>
        /// Notifyを設定する際に使用するUUIDです。(BLE共通)
        /// </summary>
        private Java.Util.UUID settingUUID = Java.Util.UUID.FromString("00002902-0000-1000-8000-00805f9b34fb");
        private BluetoothGattCharacteristic characteristic;

        public string Status { get; set; } = string.Empty;

        public string ConnectionStatus { get; set; } = "Disconnected";

        public BluetoothGatt Gatt { get; set; }

        public override void OnConnectionStateChange(BluetoothGatt gatt, [GeneratedEnum] GattStatus status, [GeneratedEnum] ProfileState newState)
        {
            if (newState == ProfileState.Connected)
            {
                this.ConnectionStatus = "Connected";
                gatt.DiscoverServices();
            }

            if (newState == ProfileState.Disconnected)
            {
                // 再接続機能を付けた方がよさそうです。
                this.ConnectionStatus = "Disconnected";
            }
        }

        public override void OnServicesDiscovered(BluetoothGatt gatt, [GeneratedEnum] GattStatus status)
        {
            base.OnServicesDiscovered(gatt, status);

            BluetoothGattService service = gatt.GetService(this.serviceUUID);

            try
            {
                this.characteristic = service.GetCharacteristic(this.characteristicsUUID);
                gatt.ReadCharacteristic(this.characteristic);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.StackTrace);
            }
        }

        /// <summary>
        /// Notifyにより値が変わったことを通知されたときに呼び出されます。
        /// </summary>
        /// <param name="gatt">通知されたGATT接続中のインスタンス</param>
        /// <param name="characteristic">Notifyを送信したCharacteristics</param>
        public override void OnCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
        {
            gatt.ReadCharacteristic(characteristic);
            this.Status = characteristic.GetValue()[0].ToString();
        }

        /// <summary>
        /// 初回に値を読んだときに呼び出されます。
        /// </summary>
        /// <param name="gatt">処理対象のGATTインスタンス</param>
        /// <param name="characteristic">characteristics</param>
        /// <param name="status">接続状態</param>
        public override void OnCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, [GeneratedEnum] GattStatus status)
        {
            base.OnCharacteristicRead(gatt, characteristic, status);
            if (characteristic.GetValue()[0].ToString() != null)
            {
                // Characteristicsの値を取得します。
                this.Status = characteristic.GetValue()[0].ToString();
            }

            if (characteristic != null)
            {
                // Notifyの設定を書き込みます。
                gatt.SetCharacteristicNotification(characteristic, true);
                BluetoothGattDescriptor descriptor = characteristic.GetDescriptor(this.settingUUID);
                if (descriptor != null)
                {
                    descriptor.SetValue(BluetoothGattDescriptor.EnableNotificationValue.ToArray());
                    gatt.WriteDescriptor(descriptor);
                }
            }
        }
    }
}

以下、スクリプトの解説です。

        /// <summary>
        /// スキャン対象のService UUIDです。
        /// </summary>
        private Java.Util.UUID serviceUUID = Java.Util.UUID.FromString("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");

        /// <summary>
        /// スキャン対象のCharacteristics UUIDです。
        /// </summary>
        private Java.Util.UUID characteristicsUUID = Java.Util.UUID.FromString("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX");

        /// <summary>
        /// Notifyを設定する際に使用するUUIDです。(BLE共通)
        /// </summary>
        private Java.Util.UUID settingUUID = Java.Util.UUID.FromString("00002902-0000-1000-8000-00805f9b34fb");

サービスの検出、キャラクタリスティックの検出、Notify設定に使用するUUIDを設定しています。
サービス、キャラクタリスティックUUIDは環境によって異なりますので各々設定をお願いします。

Notify設定のUUIDは指定されているので、このUUIDを使用します。

        public override void OnConnectionStateChange(BluetoothGatt gatt, [GeneratedEnum] GattStatus status, [GeneratedEnum] ProfileState newState)
        {
            if (newState == ProfileState.Connected)
            {
                this.ConnectionStatus = "Connected";
                gatt.DiscoverServices();
            }

            if (newState == ProfileState.Disconnected)
            {
                // 再接続機能を付けた方がよさそうです。
                this.ConnectionStatus = "Disconnected";
            }
        }

コネクションの状態がConnectedになった時(GATTプロファイルでの接続に成功した時)、目的のサービスを探します。また、サービスが見つかった場合、すぐ下のOnServicesDiscoveredが呼び出されます。

        public override void OnServicesDiscovered(BluetoothGatt gatt, [GeneratedEnum] GattStatus status)
        {
            base.OnServicesDiscovered(gatt, status);

            BluetoothGattService service = gatt.GetService(this.serviceUUID);

            try
            {
                this.characteristic = service.GetCharacteristic(this.characteristicsUUID);
                gatt.ReadCharacteristic(this.characteristic);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.StackTrace);
            }
        }

なんでもいいので、サービスが見つかった時に呼び出されます。
見つかったサービス一覧の中から、目的のサービスを探します。
UUIDを指定して目的のサービスをserviceに格納していますが、見つからなかったときはnullが格納されます。
また、サービスの中からUUIDを指定して、目的のキャラクタリスティックを探します。
同じく見つからなかったときにはnullが格納されます。

        public override void OnCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, [GeneratedEnum] GattStatus status)
        {
            base.OnCharacteristicRead(gatt, characteristic, status);
            if (characteristic.GetValue()[0].ToString() != null)
            {
                // Characteristicsの値を取得します。
                this.Status = characteristic.GetValue()[0].ToString();
            }

            if (characteristic != null)
            {
                // Notifyの設定を書き込みます。
                gatt.SetCharacteristicNotification(characteristic, true);
                BluetoothGattDescriptor descriptor = characteristic.GetDescriptor(this.settingUUID);
                if (descriptor != null)
                {
                    descriptor.SetValue(BluetoothGattDescriptor.EnableNotificationValue.ToArray());
                    gatt.WriteDescriptor(descriptor);
                }
            }
        }

キャラクタリスティックを読んだ際に呼び出されます。
キャラクタリスティックの値を読み、用意したStatus(プロパティ)に格納しています。
また、次回から通知をしてもらうようにするためにBluetoothGattDescriptorを使って設定を書き込んでいます。DescriptorはCharacteristicsから取り出す必要があります。UUIDで指定した場所に設定を書き込んでいます。

        public override void OnCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
        {
            gatt.ReadCharacteristic(characteristic);
            this.Status = characteristic.GetValue()[0].ToString();
        }

Notifyを設定したことにより、BLEデバイスの値が変わる度に呼び出されるようになります。
キャラクタリスティックの値を読んで用意したStatus(プロパティ)に格納しています。

さいごに

Dependency Serviceを使って、GetStatusAndroidのGetStatus()を呼び出せば値を受け取ることができます。
できれば値が変わったら受け取るようにしたいのですが、改造が終わったら編集や投稿をしたいと思います。

また、Android 6以降はPermissionの設定が新しくなりました。アプリ毎に権限を細かく変更できるようになったようで、MainActivityで権限チェックをした方がいいです。

(AndroidのBluetooth通信はこんなに難解じゃなかったのにどうしてBLEは・・・。)

間違っている点など、ご指摘があればぜひコメントをお願いします。