Plug and Playデバイスの扱い

このブログでは、USB(WinUSB/HID)やBluetooth LE/SerialPortでPCに接続する周辺機器をWindows 8.1 ストアアプリから制御する方法を幾つか解説してきました。これらの周辺機器は、PC実行中に抜き差しが可能ないわゆる、”Plug & Play”デバイスです。ということは、ストアアプリ起動時に周辺機器がPCに接続されていなかったり、アプリ実行中に周辺機器がPCから切断されたりする可能性があるわけです。

これまでの投稿で説明してきた、DeviceInformationクラスのFindAllAsyncメソッドは、つながっていない周辺機器は検索できず、また、実行中に切断された場合は、制御用の通信を止めたり通信用のソケットを閉じたりしないといけないので、動的な抜き差しに対応したプログラムを書く必要があるわけです。動的な管理は、DeviceWatcherクラスを使います。このクラスのオブジェクトは、DeviceInformationクラスのCreateWatcherメソッドを使って作成します。作成したDeviceWatcherオブジェクトのデバイス状態変化を通知するイベントにメソッドを登録し、デバイスの追加・削除を監視します。ストアアプリがバックグラウンドになった時、監視を止める必要があるので、クラスメンバー変数として、DeviceWatcherのオブジェクトを宣言します。

DeviceWatcher deviceWatcher;

周辺機器の監視を開始したいコードで、以下を実行します。Bluetooth SerialPortを例にして説明します。他の種類の周辺機器も処理の流れは一緒で、CreateWatcherの引数を周辺機器の種別に合わせて変更すれば対応可能です。

    if (deviceWatcher == null)
    {
        deviceWatcher = DeviceInformation.CreateWatcher(RfcommDeviceService.GetDeviceSelector(RfcommServiceId.SerialPort));
        deviceWatcher.Added += deviceWatcher_Added;
        deviceWatcher.Removed += deviceWatcher_Removed;
        deviceWatcher.Updated += deviceWatcher_Updated;
        deviceWatcher.EnumerationCompleted += deviceWatcher_EnumerationCompleted;
    }
    deviceWatcher.Start();

これで、PNPデバイスがシステムに追加されると、deviceWatcher_Addedがコールされ、削除されると、deviceWatcher_Removedがコールされます。このコードが実行された時点でシステムに複数PNPデバイスがシステムに追加されていると、順次deviceWatcher_Addedがコールされ、全てを列挙し終わった時点で、deviceWatcher_EnumarationCompletedがコールされます。ストアアプリで制御したいPNPデバイスが決まっているなら、deviceWatcher_Addedメソッドでお目当てのデバイスがシステムに追加された時点で処理を開始すればよいし、複数の中から選ばせたい場合は、Start()メソッドコールとともにProgressRing等の表示を開始し、Addedであらかじめ用意したリストに順次追加して、EnumerationCompletedのタイミングでProgressRingの表示を止め、選択をユーザーにお願いする、といった作りが可能です。deviceWatcher_Addedメソッドは、引数として、DeviceWatcherと追加されたデバイスのDeviceInformationオブジェクトが渡されます。FindAllAsyncメソッドで列挙した場合と同じ方法で、DeviceInromationオブジェクトから、周辺機器と接続するための処理が行えます。

お目当てのデバイスが見つかった、あるいは、アプリがバックグラウンドに回るタイミングで、デバイスの監視を止めます。

    deviceWatcher.Stop();

これで監視は止まりますが、アプリの中で監視を止めるロジックが複数あるようなプログラムの場合は、deviceWatcherのStatusプロパティをチェックして、Stopped、あるいは、Stopping以外の時だけStopメソッドをコールすると良いでしょう。

さて、以上の記述で、「システムに追加される」といった微妙な表現を使ったことにあれ?と思った読者もいらっしゃるでしょう。そうです。例えば、設定チャーム→PC設定の変更→PCとデバイス→Bluetoothでペアリングしたり、USBで機器を接続した時点で、インターフェイスに関する情報はシステムに追加されてしまいます。実は、DeviceWatcherクラスが監視できるのは、システムへのデバイス情報追加・削除・変更でしかありません。削除について言えば、PC設定の変更でBluetoothデバイスを削除したり、デバイスマネージャーでUSB機器を削除しない限りはRemovedメソッドはコールされないし、一度システムに追加されたデバイスは、PCに接続されていなくても、FindAllAsyncメソッド、DeviceWatcherクラスともに情報を取得できてしまいます。

これだと、まだPNPデバイスが一度もPCに接続されていない状態でストアアプリを起動したときにお目当てのデバイスをつなぐよう促す場合を除いて、実際にPNPデバイスがPCに接続状態なのか非接続状態なのか判断できません。

PNPデバイスの実際の接続状況を調べるには、PnpObject、PnpObjectWatcherを使います。先ずは、クラスのメンバー変数として、PnPObjectWatcherを宣言しておきます。

PnpObjectWatcher pnpWatcher;

そして、

    var props = new List<string>() { "System.Devices.Connected", "System.ItemNameDisplay" };
    pnpWatcher = PnpObject.CreateWatcher(PnpObjectType.DeviceContainer, props, string.Empty);
    pnpWatcher.Added += pnpWatcher_Added;
    ...
    pnpWatcher.Start();

こんな風にPnpObjectWatcherオブジェクトを作成して必要なイベントにメソッドを登録します。System.Devices.Connectedプロパティが機器の接続状態、System.ItemNameDisplayプロパティがDeviceInformationのNameプロパティに相当するプロパティです。propsリストで指定されたプロパティのどれかを持っているPNPデバイスを監視することができます。また、PnpObjectにはFindAllAsyncメソッドも用意されており、このメソッドでPNPオブジェクトを列挙して、現在の接続状況をチェックすることも可能です。

void pnpWatcher_Added(PnpObjectWatcher sender, PnpObject args)
{
    object itemNameDisplay;
    if (args.Properties.TryGetValue("System.ItemNameDisplay", out itemNameDisplay))
    {
        if (itemNameDisplay.ToString() == "TARGET")
        {
            object connected;
            if (args.Properties.TryGetValue("System.Devices.Connected", out connected))
            {
                Debug.WriteLine(" Connected - " + ((bool)connected).ToString());
            }
        }
    }
}

PnpObjectWatcherの場合も、システムに追加されているPNPデバイスが先ず列挙され、全てが列挙され終わるとEnumerationCompletedイベントが発生します。Addedの場合、引数としてPnpObjectが渡されるので、System.ItemNameDisplayプロパティでターゲットのPNPデバイスであるかを確認し、ターゲットである場合には、System.Devices.Connectedプロパティをチェックします。この時点でこのプロパティがfalseならPCには接続されていないことになります。PNPデバイスがPCに実際に接続されたタイミングで、Updatedイベントが発生します。これを捕捉してチェックをしてみてください。