32feetでBluetoothプログラミング ペアリング実践編

# はじめに
[前記事 32feetでBluetoothプログラミング ペアリング編](https://qiita.com/haruyan_hopemucci/items/95ff2bb88fb243f5df81)の続き。

以下の環境でBluetooth機器との接続関連処理を実装していく。
- Windows10 pro
- Visual Studio 2017 CommutyEdition
- WindowsFormプロジェクト(C#)
- .NET framework 4.6.1

# 画面仕様
![画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/70373964-2520-6e83-6b8a-0106c2027881.png)
1. listBoxNonPair 未ペアリングデバイス一覧
2. listBoxPaired ペアリング済みデバイス一覧
3. propertyGrid1 リストボックスで選択したデバイスの詳細情報を表示
4. Toggle_RefrashAsync CheckBox。On状態が継続している間非同期にデバイスの探索を行う。
5. button_refrash クリックするとデバイスの探索を行う。
6. button_pairing NonPaired側で選択されているデバイスとペアリングを行う。
7. button_remove Paired側で選択されているデバイスとのペアリングを解除する。
8. （一覧外）bindingSource_nonPair listBoxNonPair のBindingSource
9. （一覧外）bindingSource_Paired listBoxPaired のBindingSource
前回解説した一連の機能に加え、デバイスの非同期探索機能を実装する。

# 実装詳細

## コンストラクタ
```csharp
    public partial class Form_main : Form
    {
        BluetoothClient bc;
        BluetoothClient bcAsync;
        public Form_main()
        {
            InitializeComponent();
            // BluetoothClient オブジェクトの生成
            bc = new BluetoothClient();
            // 未ペアリングデバイスを探索する際の所要時間を設定。デフォルトは10秒。
            bc.InquiryLength = new TimeSpan(0, 0, 5); // この場合は5秒
            // 非同期BluetoothClient オブジェクトの生成
            bcAsync = new BluetoothClient();
            bcAsync.InquiryLength = new TimeSpan(0, 0, 2); // この場合は2秒
            // ペアリングリクエストが発生した場合、引数のメソッドをコールバックする。
            BluetoothWin32Authentication authenticator = new BluetoothWin32Authentication(Win32AuthCallbackHandler);
        }
        /// <summary>
        /// ペアリングリクエストが発生した際に呼び出される。
        /// </summary>
        private void Win32AuthCallbackHandler(Object sender, BluetoothWin32AuthenticationEventArgs e)
        {
            // pinの設定。デバイスによっては必須。
            e.Pin = "0000";
            // ペアリングの了承・拒否を設定できる。trueにしてメソッドを返すとペアリング了承にできる。
            e.Confirm = true;
        }
```
クラスフィールドとして`bc`,`bcAsync` 2つのBluetoothClientを用意する。デバイスの同期探索と非同期探索用途。
コンストラクタで双方の初期化を行う。非同期用では探索時間を短めにする。
最後の`authenticator `は前回同様ペアリングを自動で実施するためのコールバックハンドラ。

## デバイス探索（同期）
まずは同期版のデバイス探索を実装する。

```csharp
/// <summary>
/// refrashボタンクリック
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button_refresh_Click(object sender, EventArgs e)
{
    Btlist_refresh();
}

private void Btlist_refresh()
{
    // Bluetoothデバイスの探索を行う。
    // 引数：
    // int maxDevices    : 探索するデバイスの最大数。指定の数で探索を打ち切る。
    // bool authenticated: trueだと認証済み（ペアリング済み）のデバイス、falseだと認証していないデバイスを結果に含める。
    // bool remembered   : trueだとホストに記録済みのデバイスのみを結果に含める（認証したことのあるデバイス以外無視すると同じ？）
    // bool unknown      : falseだと詳細のわからないデバイスを結果に含めない。

    // 未ペアリングのデバイスを探索し、結果を返す
    BluetoothDeviceInfo[] devices_nonpaired = bc.DiscoverDevices(32, false, false, true);
    // ペアリング済みのデバイスを探索し、結果を返す
    BluetoothDeviceInfo[] devices_paired = bc.DiscoverDevices(32, true, false, false);

    bindingSource_nonPair.DataSource = devices_nonpaired;
    bindingSource_Paired.DataSource = devices_paired;
    // ListBoxの設定
    listBoxNonPair.DisplayMember = "DeviceName";
    listBoxPaired.DisplayMember = "DeviceName";
}
```
前回実装したデバイス探索と同様。
取得した`BluetoothDeviceInfo`の配列をListBoxのSourceにセットする。
`DisplayMember `は`DeviceName`にするとデバイスの名称がListBoxに出る。

## 非同期デバイス探索
次にデバイス探索の非同期版を実装する。

```csharp
/// <summary>
/// RefreshAsync
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Toggle_RefrashAsync_CheckedChanged(object sender, EventArgs e)
{
    CheckBox obj = (CheckBox)sender;
    if (obj.Checked)
    {
        Btlist_refrashAsync();
    }
}
/// <summary>
/// 非同期で未ペアリングデバイスを探索する
/// </summary>
private void Btlist_refrashAsync()
{
    // 探索を開始。探索時間経過後コールバックメソッドが実行される。
    bcAsync.BeginDiscoverDevices(32, false, true, true, true, RefrashAsyncCallbackHandler,"non-paired");
    bcAsync.BeginDiscoverDevices(32, true, true, false, false, RefrashAsyncCallbackHandler, "paired");
}

/// <summary>
/// 非同期デバイス探索後コールバック
/// </summary>
/// <param name="ar"></param>
private void RefrashAsyncCallbackHandler(IAsyncResult ar)
{
    if (ar.IsCompleted)
    {
        BluetoothDeviceInfo[] devices = bcAsync.EndDiscoverDevices(ar);
        if((string)ar.AsyncState == "non-paired") { 
            Invoke((MethodInvoker)(()=>{
                bindingSource_nonPair.DataSource = devices;
                listBoxNonPair.DisplayMember = "DeviceName";
            }));
            // non-pair側のみ継続条件をつける。こちらのほうが遅いので。
            if (Toggle_RefrashAsync.Checked)
            {
                bcAsync.BeginDiscoverDevices(32, false, false, true, true, RefrashAsyncCallbackHandler, "non-paired");
            }
        }
        else if ((string)ar.AsyncState == "paired")
        {
            Invoke((MethodInvoker)(() => {
                bindingSource_Paired.DataSource = devices;
                listBoxPaired.DisplayMember = "DeviceName";
            }));
        }
    }
}
```
ここで[BluetoothClient#BeginDiscoverDevices](http://inthehand.github.io/html/M_InTheHand_Net_Sockets_BluetoothClient_BeginDiscoverDevices.htm) を使用する。
- int maxDevices 探索するデバイス数の上限。DiscoverDevicesと同じ。
- bool authenticated trueだとペアリング済みのみ検索。falseだと未ペアリングのみ検索。DiscoverDevicesと同じ。
- bool remembered trueだと以前探索済みのデバイスを結果に含める。DiscoverDevicesと同じ。
- bool unknown trueだと未識別なデバイスを結果に含める。DiscoverDevicesと同じ。
- bool discoverableOnly DiscoverDevicesにはない引数。外部探索で見つかるデバイスのみを結果に含める。
- AsyncCallback callback デバイスの探索が完了した際にコールされるメソッド。
- Object state callbackの引数`IAsyncResult`オブジェクトの`AsyncState`にこのオブジェクトがセットされる。識別用。 

同期版と同様、未ペアリングとペアリング済み両方のデバイス一覧を非同期に取得しようとしている。
同期版と同じくペアリング済みのデバイス探索は一瞬で終了し、未ペアリングデバイスの探索は`InquiryLength`で設定した時間内でデバイスを探索し、時間経過後 `callback`で設定されたメソッドをコールする。
未ペアリング探索もペアリング済み探索も同じメソッドをコールバックするが、`state`を別で設定しているのでメソッド内部で処理を分岐させている。
`callback`はUIスレッドではないので、結果のセットは`invoke`でUIスレッドに実行させないと探索結果がListBoxに反映されない。
その後、`Toggle_RefrashAsync.Checked`を参照し、On状態であれば再び同じ非同期探索を実行する。これでCheckBoxがOnのままであれば継続して探索を実施させることができる。

## デバイス詳細表示

ListBoxでクリックしたアイテムのプロパティ一覧を`PropertiGrid`のほうに表示させる。

```csharp
/// <summary>
/// クリックしたアイテムをプロパティグリッドに表示する
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void listBox_Click(object sender, EventArgs e)
{
    SetDeviceInfoToPropertyGrid((BluetoothDeviceInfo)((ListBox)sender).SelectedItem);
}
/// <summary>
/// 対象のデバイス情報をプロパティグリッドに表示させる
/// </summary>
/// <param name="deviceInfo"></param>
private void SetDeviceInfoToPropertyGrid(BluetoothDeviceInfo deviceInfo)
{
    propertyGrid1.SelectedObject = deviceInfo;
}
```
`listBox_Click`を両ListBoxの`Click`イベントに接続すれば良し。
デバイスアドレスやインストール済みのサービス一覧などが表示される。

## ペアリング

`listBoxNonPair`でselectされているデバイスとペアリングを実施、サービスのインストールまで行う。

```csharp
 /// <summary>
 /// ペアリングとサービスのインストールを実施。
 /// </summary>
 /// <param name="deviceInfo">対象のデバイス</param>
 /// <returns></returns>
 private bool Pairing(BluetoothDeviceInfo deviceInfo)
 {
     // ペアリングリクエスト。完了するまで待ち合わせる。
     bool paired = BluetoothSecurity.PairRequest(deviceInfo.DeviceAddress, null);

     // ペアリング失敗したらそこで終了
     if (!paired)
     {
         return false;
     }

     deviceInfo = new BluetoothDeviceInfo(deviceInfo.DeviceAddress);

     // ペアリングしたデバイスが対応しているサービス一覧を取得し、リストに記憶する。
     List<Guid> serviceGuidList = new List<Guid>();

     // L2CapProtocolにてGetServiceRecordsを実行するとデバイスが利用可能なサービス一覧を取得できる。
     ServiceRecord[] serviceinfo = deviceInfo.GetServiceRecords(BluetoothService.L2CapProtocol);
     foreach (var record in serviceinfo)
     {
         // サービスレコードをテキストでダンプ
         //ServiceRecordUtilities.Dump(Console.Error, record);
         // 取得したサービスレコードのうち、ServiceDescription のレコードを取得する。
         ServiceAttribute sdpRecord = record.GetAttributeById(InTheHand.Net.Bluetooth.AttributeIds.UniversalAttributeId.ServiceDescription);
         if (sdpRecord != null)
         {
             // サービスのGuidを取得
             serviceGuidList.Add(sdpRecord.Value.GetValueAsElementArray()[0].GetValueAsUuid());
         }
     }
     // サービスのインストール
     foreach (Guid service in serviceGuidList)
     {
         try
         {
             // 個々のServiceに対し、有効を設定する。
             deviceInfo.SetServiceState(service, true, true);
         }
         catch (Exception ex)
         {
             Console.Error.WriteLine(ex.Message);
             Console.Error.WriteLine(ex.StackTrace);
         }
     }

     return true;
 }
/// <summary>
/// ペアリングボタンクリック
/// listBoxNonPairで選択されているデバイスとペアリングを実施。
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button_pairing_Click(object sender, EventArgs e)
{
    BluetoothDeviceInfo deviceInfo = (BluetoothDeviceInfo)listBoxNonPair.SelectedItem;
    if(deviceInfo != null)
    {
        Pairing(deviceInfo);
    }
}
```

こちらも前回実装した内容と同様。

## ペアリング解除

ペアリング解除も前回実装内容と同様。

```csharp
/// <summary>
/// 引数デバイスのペアリング解除
/// </summary>
/// <param name="deviceInfo"></param>
/// <returns></returns>
private bool RemovePairing(BluetoothDeviceInfo deviceInfo)
{
    return BluetoothSecurity.RemoveDevice(deviceInfo.DeviceAddress);
}

/// <summary>
/// removeボタンクリック
/// listBoxPairedで選択されているデバイスとペアリングを解除する
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button_remove_Click(object sender, EventArgs e)
{
    BluetoothDeviceInfo deviceInfo = (BluetoothDeviceInfo)listBoxPaired.SelectedItem;
    if(deviceInfo != null)
    {
        RemovePairing(deviceInfo);
    }
}
```

# おわりに
Bluetoothデバイスの探索、ペアリング等を行う一通りの機能を実装できた。
プログラム上でBluetoothデバイスのペアリングをコントロールする必要がある場合に役立つかと思われる。

### 今回のコード
プロジェクトごとgithubにアップロードしたので使ってみたい方はぜひ。
https://github.com/haruyan-hopemucci/BluetoothDemo

### 補注
筆者はこのアプリでマウスとワイヤレスイヤホンくらいしかペアリングを試していないので、デバイスによってはペアリングができなかったりサービスのインストールが不完全だったりするかもしれないが、免責事項としてご容赦いただきたい。