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

# 概要
Bluetoothのデバイス検索やペアリングを.NETプログラミングで制御する。
UWPにはBluetooth関連のライブラリがそろっているが、それ以前のWindowsFormやWPFではマネージドなライブラリがそろっていない。
そこで、[32feet](https://github.com/inthehand/32feet)というBluetoothや赤外線通信という近距離無線通信関連のWin32APIをマネージドで使用できるライブラリを使って簡単なBluetooth制御を行う。

(2020年7月時点)今更WindowsFormでのBluetooth操作が必要か？　いや単なる自己満足さ。[もしかしたら必要な人がいるのかも](https://teratail.com/questions/274412)しれないし。

# 詳細
以下の環境でBluetooth機器との接続関連処理を実装していきます。
- Windows10 pro
- Visual Studio 2017 CommutyEdition
- WindowsFormプロジェクト(C#)
- .NET framework 4.6.1
- Logicool M557 マウス のペアリング

## 32feetのインストール
32feetのライブラリ、ソースコード自体はgithubにあるが、VisualStdioで開発する際はNuGetからインストールするのが最も簡単。
プロジェクトを作成後、「ツール」→「NuGetパッケージマネージャー」→「ソリューションのNuGetパッケージの管理」を選択。
![NuGet](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/27ea3d98-669a-2a24-1e49-3b817dbb696d.png)
「32feet」で検索し、上記画像のライブラリをインストールする。

## デバイスの検索
まずは、Bluetoothデバイスを検索する機能を実装する。使用するクラスは[InTheHand.Net.Sockets.BluetoothClient](http://inthehand.github.io/html/T_InTheHand_Net_Sockets_BluetoothClient.htm)

```csharp
// BluetoothClient オブジェクトの生成
BluetoothClient bc = new BluetoothClient();

// 未ペアリングデバイスを探索する際の所要時間を設定。デフォルトは10秒。
bc.InquiryLength = new TimeSpan(0, 0, 5); // この場合は5秒

// 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);
```
ペアリング済み、未ペアリングのデバイス両方とも同じ`DiscoverDevices`メソッドで取得する。引数により取得対象を変更できる。
ペアリング済みのデバイス検索は一瞬で終わるが、未ペアリングのデバイス探索は周囲にペアリングリクエストをしているデバイスの信号を受信するので時間がかかる。普通にデバイスをペアリングするとき、デバイス一覧に表示されるまで少し時間がかかるのと同じ。
探索する時間は`InquiryLength`で設定できる。`authenticated=true`で探索するときっちり設定した時間ブロックされる。
戻り値は`BluetoothDeviceInfo`の配列。

## ペアリング
前項で取得した未ペアリングデバイスをペアリングする方法を考える。ペアリングは[InTheHand.Net.Bluetooth.BluetoothSecurity](http://inthehand.github.io/html/T_InTheHand_Net_Bluetooth_BluetoothSecurity.htm)クラスを使用する。

```cs
// とりあえず、1つ以上発見できたものとして。
BluetoothDeviceInfo deviceInfo = devices_nonpaired[0];
// ペアリングのリクエストを行う
bool paired = BluetoothSecurity.PairRequest(deviceInfo.DeviceAddress, null);
```
すると、Bluetoothデバイスを通常追加する際におなじみのポップアップが表示される。
![Bluetooth デバイス追加メッセージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/eb644efb-abc7-7b47-d14f-7ae279c10a74.png)

このポップアップをクリックすると以降は通常のペアリングと同じようにペアリング処理が進む。
この段階では単にペアリングのリクエストを送るだけで、プログラム側でペアリング処理を完了させることができない。

### プログラム側でペアリング処理を完了させるには
`PairRequest`メソッドでペアリングの強制了承を行うことはできないが、ペアリングのリクエストが発生したことをハンドルする方法がある。
[InTheHand.Net.Bluetooth.BluetoothWin32Authentication](http://inthehand.github.io/html/T_InTheHand_Net_Bluetooth_BluetoothWin32Authentication.htm)を利用する。

```cs
// ペアリングリクエストが発生した場合、引数のメソッドをコールバックする。
// PairRequestする前に1度設定すればOK.
BluetoothWin32Authentication authenticator = new BluetoothWin32Authentication(Win32AuthCallbackHandler);

/// <summary>
/// ペアリングリクエストが発生した際に呼び出される。
/// </summary>
private void Win32AuthCallbackHandler(Object sender, BluetoothWin32AuthenticationEventArgs e)
{
    // pinの設定。デバイスによっては必須。
    e.Pin = "0000";
    // ペアリングの了承・拒否を設定できる。trueにしてメソッドを返すとペアリング了承にできる。
    e.Confirm = true;
}
```
ペアリングリクエストのポップアップは表示されるが、ポップアップをクリックしなくてもペアリングは完了する。
ただし「ペアリング了承」しただけで、そのデバイスが使用できるようにはなっていない。デバイスを使えるようにするためには後述のサービスインストールを行う必要がある。
![コメント 2020-07-04 200303.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653626/6199aa5a-a371-9722-36f0-e913b0b96df4.png)
この方法でペアリングしたデバイスは「その他のデバイス」となっていて、マウスやキーボードの扱いになっていないのがわかる。

## サービス一覧取得
まずは、対象のBluetoothデバイスから「あなたはどんなサービスが使えるの？」と問い合わせする必要がある。
以下の方法で対応サービス一覧の内容を取得できる。

```cs
// L2CapProtocolにてGetServiceRecordsを実行するとデバイスが利用可能なサービス一覧を取得できる。
// この作業はペアリング前でも実施可能。
var serviceinfo = remoteEP.GetServiceRecords(BluetoothService.L2CapProtocol);
// レコードの内容をテキストでダンプしてみる。
foreach(var record in serviceinfo){
    ServiceRecordUtilities.Dump(Console.Error, record);
}
```

サービス一覧をテキストでダンプすると、以下のようなデータが取得できる。レコードは3つ取得でき、その1番目の内容がこちら。

```
AttrId: 0x0000 -- ServiceRecordHandle
UInt32: 0x0

AttrId: 0x0001 -- ServiceClassIdList
ElementSequence
    Uuid16: 0x1000 -- ServiceDiscoveryServer

AttrId: 0x0004 -- ProtocolDescriptorList
ElementSequence
    ElementSequence
        Uuid16: 0x100 -- L2CapProtocol
        UInt16: 0x1
    ElementSequence
        Uuid16: 0x1 -- SdpProtocol
( ( L2Cap, PSM=Sdp ), ( Sdp ) )

AttrId: 0x0005 -- BrowseGroupList
ElementSequence
    Uuid16: 0x1002 -- PublicBrowseGroup

AttrId: 0x0006 -- LanguageBaseAttributeIdList
ElementSequence
    UInt16: 0x656E
    UInt16: 0x6A
    UInt16: 0x100

AttrId: 0x0009 -- BluetoothProfileDescriptorList
ElementSequence
    ElementSequence
        Uuid16: 0x100 -- L2CapProtocol
        UInt16: 0x100

AttrId: 0x0100 -- ServiceName
TextString: [en] 'Logitech Bluetooth Wireless Mouse SDP Server'

AttrId: 0x0101 -- ServiceDescription
TextString: [en] 'Bluetooth Mouse'

AttrId: 0x0200 -- VersionNumberList
ElementSequence
    UInt16: 0x100
AttrId: 0x0000 -- ServiceRecordHandle
UInt32: 0x10000
```

このうち、`AttrId: 0x0001` の内容がそのデバイスが対応しているサービスのGuidである。

```
AttrId: 0x0001 -- ServiceClassIdList
ElementSequence
    Uuid16: 0x1000 -- ServiceDiscoveryServer
```
どのGuidがどのサービスを表しているかは[こちらのサイトの仕様：サービスディスカバリー](https://www.bluetooth.com/ja-jp/specifications/assigned-numbers/service-discovery/)を参考にしてほしい。

サービス一覧の2番目のレコードはこちら。

```
AttrId: 0x0001 -- ServiceClassIdList
ElementSequence
    Uuid16: 0x1124 -- HumanInterfaceDevice

AttrId: 0x0004 -- ProtocolDescriptorList
ElementSequence
    ElementSequence
        Uuid16: 0x100 -- L2CapProtocol
        UInt16: 0x11
    ElementSequence
        Uuid16: 0x11 -- HidpProtocol
( ( L2Cap, PSM=HidControl ), ( Hidp ) )

AttrId: 0x0005 -- BrowseGroupList
ElementSequence
    Uuid16: 0x1002 -- PublicBrowseGroup

AttrId: 0x0006 -- LanguageBaseAttributeIdList
ElementSequence
    UInt16: 0x656E
    UInt16: 0x6A
    UInt16: 0x100

AttrId: 0x0009 -- BluetoothProfileDescriptorList
ElementSequence
    ElementSequence
        Uuid16: 0x1124 -- HumanInterfaceDevice
        UInt16: 0x101

AttrId: 0x000D -- AdditionalProtocolDescriptorLists
ElementSequence
    ElementSequence
        ElementSequence
            Uuid16: 0x100 -- L2CapProtocol
            UInt16: 0x13
        ElementSequence
            Uuid16: 0x11 -- HidpProtocol
    ( ( L2Cap, PSM=HidInterrupt ), ( Hidp ) )

AttrId: 0x0100 -- ServiceName
TextString: [en] 'Logitech Bluetooth Wireless Mouse'

AttrId: 0x0101 -- ServiceDescription
TextString: [en] 'Bluetooth Mouse'

AttrId: 0x0102 -- ProviderName
TextString: [en] 'Logitech'

AttrId: 0x0201 -- ParserVersion
UInt16: 0x111

AttrId: 0x0202 -- DeviceSubclass
UInt8: 0x80

AttrId: 0x0203 -- CountryCode
UInt8: 0x21

AttrId: 0x0204 -- VirtualCable
Boolean: True

AttrId: 0x0205 -- ReconnectInitiate
Boolean: True

AttrId: 0x0206 -- DescriptorList
ElementSequence
    ElementSequence
        UInt8: 0x22

AttrId: 0x0207 -- LangIdBaseList
ElementSequence
    ElementSequence
        UInt16: 0x409
        UInt16: 0x100

AttrId: 0x0209 -- BatteryPower
Boolean: True

AttrId: 0x020A -- RemoteWake
Boolean: True

AttrId: 0x020C -- SupervisionTimeout
UInt16: 0x1F40

AttrId: 0x020D -- NormallyConnectable
Boolean: False

AttrId: 0x020E -- BootDevice
Boolean: True

AttrId: 0x020F
UInt16: 0x12

AttrId: 0x0210
UInt16: 0x0
AttrId: 0x0000 -- ServiceRecordHandle
UInt32: 0x10001
```
先頭にそれっぽいのがある。

```
AttrId: 0x0001 -- ServiceClassIdList
ElementSequence
    Uuid16: 0x1124 -- HumanInterfaceDevice
```
このデバイスはマウスなので、まさにHumanInterfaceDevice(HID)である。

レコードごとに、以下のコードでサービスのGuidを取得できる。

```cs
// 取得したサービスレコードのうち、ServiceDescription のレコードを取得する。
ServiceAttribute sdpRecord = record.GetAttributeById(InTheHand.Net.Bluetooth.AttributeIds.UniversalAttributeId.ServiceDescription);
if (sdpRecord != null)
{
    // サービスのGuidを取得
    Guid guid = sdpRecord.Value.GetValueAsElementArray()[0].GetValueAsUuid();
}
```

## サービスのインストール
前項で取得できたサービスのGuidをもとにサービスをインストールする。[BluetoothDeviceInfo.SetServiceState](https://inthehand.github.io/html/M_InTheHand_Net_Sockets_BluetoothDeviceInfo_SetServiceState_1.htm)で行う。
取得したGuidを一旦Listに保存し、サービス一覧取得が終わってからまとめてサービスのインストールを行う。

```cs
// ペアリングしたデバイスが対応しているサービス一覧を取得し、リストに記憶する。
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)
{
    // 個々のServiceに対し、有効を設定する。
    deviceInfo.SetServiceState(service, true);
}
```
これで実際にマウスが動かせるようになる。

## ペアリング解除
ペアリング解除も[InTheHand.Net.Bluetooth.BluetoothSecurity](http://inthehand.github.io/html/T_InTheHand_Net_Bluetooth_BluetoothSecurity.htm)クラスを使用する。

```csharp
BluetoothSecurity.RemoveDevice(deviceInfo.DeviceAddress);
```

# 終わりに
32feetのライブラリを用いてBluetoothのペアリング、サービスインストール、ペアリング解除までを実装した。

[次記事　32feetでBluetoothプログラミング ペアリング実践編](https://qiita.com/haruyan_hopemucci/items/e7031c1b696f1d04fc40)