概要
WindowsにおけるHIDデバイスの扱いに関する話題です。
結論だけまとめると「Windows10 IoT Core向けにC#でHIDデバイスを扱うコードを書いたけど、デスクトップ上ではHIDが開けなかったのにRaspberry Pi3上では開けた イミフ」という話です。
個人的な振り返りとして記録したいと思います。
この投稿について
通常、WindowsのアプリケーションでHIDを扱う場合、setupapi.dll
とhid.dll
をリンクして、
いてこますのが普通だと思います。
なので win32API アプリとしてC/C++で開発することになります(以下、便宜上VC++アプリと表現します)。
P/Invoke とかいう仕組を使えば .NET アプリからもこれらDLLのAPIを呼び出して使えました。
HIDを扱うためのAPIをC#クラスでラップした ライブラリ も存在します。
通常の Windows PC 上であれば互換性などを特に気にすること無く使うことができました。
ところで、Windows 10 IoT Core が登場しました。大雑把に言えば、ラズパイでC#コードが動くようになりました。
基本的にアプリケーションは Universal Windows Platform という基盤のうえで作成するようです。
UWPアプリからのネイティブ関数コールが果たして可能なのかどうなのか、ちょっと調べただけではよく分かりませんでした。
仮に出来たとしてもおそらく Universal なものにならないだろうとは予想できます。
前置きが長くなりましたが、UWP のライブラリAPIにははじめからWindows.Devices.HumanInterfaceDevice名前空間が定義されていて、許可されているものに限り(後述)HIDデバイスにアクセスできるようになっています。
UWP 上で HIDデバイスを扱うにあたり、VC++ との比較や、注意点などを記録したいと思います。
やりたいこと
Raspberry Pi3 に Windows 10 IoT Core をインストールしてみたので、UWPアプリケーションから接続されたHIDデバイスにアクセスしてみたいと思います。
VC++でのHIDデバイスの扱い
先に、VC++でHIDデバイスにどのようにアクセスしていたか、おさらいします。
まずはデバイスパスを列挙します。
static std::vector<std::string> enumerate()
{
std::vector<std::string> paths; // デバイスパスのコンテナ
GUID guid;
HidD_GetHidGuid(&guid);
// 渡されたGUIDに該当するデバイス情報を取得
HDEVINFO deviceInfo;
deviceInfo = SetupDiGetClassDevs(&guid, NULL, NULL, (DIGCF_PRESENT | DIGCF_DEVICEINTERFACE));
if (deviceInfo != INVALID_HANDLE_VALUE)
{
SP_DEVICE_INTERFACE_DATA deviceInfoData;
deviceInfoData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
// インターフェースを列挙
for (DWORD index = 0;
SetupDiEnumDeviceInterfaces(deviceInfo, 0, &guid, index, &deviceInfoData);
index++)
{
DWORD requiredLength;
// デバイスパスを含む構造体のサイズを取得
SetupDiGetDeviceInterfaceDetail(deviceInfo, &deviceInfoData, NULL, 0, &requiredLength, NULL);
// 取得したサイズを元にバッファを確保
LPBYTE deviceDetailDataBuffer = new BYTE[requiredLength];
PSP_DEVICE_INTERFACE_DETAIL_DATA deviceDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)deviceDetailDataBuffer;
deviceDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
// デバイスパスを取得
SetupDiGetDeviceInterfaceDetail(deviceInfo, &deviceInfoData, deviceDetailData, requiredLength, &requiredLength, NULL);
// デバイスパスをコピー
paths.push_back(deviceDetailData->DevicePath);
// バッファ削除
delete[] deviceDetailDataBuffer;
}
// 使用したデバイス情報の破棄
SetupDiDestroyDeviceInfoList(deviceInfo);
}
return paths;
}
tchar.h
絡みの部分は適当に読み替えて下さい。
ほとんどの場合は内製ライブラリの中に押し込んでしまうのでいちいち意識することはありませんが、ここでやっているのはsetupapi.dll
にお願いしてHIDインターフェースを列挙してもらう処理です。
GUID部分を置き換えてやれば「USBホストコントローラのパスのみを列挙」などといったことも出来ます。
取得したデバイスパスからファイルハンドルを開きます。
HANDLE h = ::CreateFile(path.c_str(), // デバイスパス文字列
(GENERIC_READ|GENERIC_WRITE), (FILE_SHARE_READ | FILE_SHARE_WRITE),
NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
ファイルハンドルが開けたら、ココ に挙げられた関数を経由してHIDデバイスへのアクセスが可能になります。
HidD_* 関数は主にデバイスの制御や情報取得を、HidP_* 関数は主にHIDレポートのパース処理をそれぞれ行います。
では開いたHIDデバイスの VendorIDとProductID や トップコレクションのUsagePageとUsageを確認してみます。
HANDLE h; // HIDのファイルハンドル
HIDD_ATTRIBUTES attributes; // デバイスの基本情報
if (HidD_GetAttributes(h, &attributes))
{ // attributes が取得できた
attributes.VendorID; //!< VendorIDあったよ!
attributes.ProdcutID; //!< ProductIDあったよ!
PHIDP_PREPARSED_DATA preparsedData; // HID Report Descriptor 情報
if (HidD_GetPreparsedData(h, &preparsedData))
{ // preparsedData が取得できた
HIDP_CAPS capabilities; // デバイスが送受信するレポートに関する基本情報
if (HIDP_STATUS_SUCCESS == HidP_GetCaps(preparsedData, &capabilities))
{ // capabilities が取得できた
capabilities.UsagePage; //!< UsagePageあったよ!
capabilities.Usage; //!< Usageあったよ!
}
// preparsedData は必ず開放する
HidD_FreePreparsedData(preparsedData);
}
}
パスを列挙したHIDデバイスについては、それらが目的のものかどうかこれらの上記のデバイス情報を確認してから具体的な操作を行うことになります。
それと HidD_* 関数はすべて同期処理であるため、HidD_GetInputReport 、HidD_SetOutputReport 関数の代わりに普通のファイル操作APIの Read , Write 関数を使うことの方が一般的なようです。
UWPでやってみる
では、UWPで同じことをやってみたいと思います。
「久々のC#、変わり過ぎ・・・」「XAMLぜんぜんわからん・・・」「MVVMってどういうこと・・・」
といった個人的な戸惑いや苦しみがありましたが、ここでは割愛します。
まずはHIDを列挙してみます。
最初にSelectorという文字列を生成します。
string selector = HidDevice.GetDeviceSelector(usagePage, usageId, vendorId, ProductId);
HidDevice.GetDeviceSelector は目的とするHIDデバイスを探すための呪文を生成するstatic関数で、むりやりhid.dll
から似た関数を挙げるとしたら_HidD_GetHidGuid_あたりになるでしょうか。
戻り値の文字列の中身は「GUIDがコレコレで、現在接続されていて、基本情報がコレコレのデバイスをおくれ」というクエリ状の何かです。
文字列を直接書いても使えますが、UWP特有の事情(後述)によりHIDに限って言えばSelectorの中身を気にしてもあまり面白いことはなさそうです。
SelectorをDeviceInformationクラスに渡すと、ファイルパス(UWPではIDと呼ぶ)も含むデバイス情報を列挙してくれます。
DeviceInformationCollection collection = await DeviceInformation.FindAllAsync(selector);
これはsetupapi.dll
でいうところの_SetupDiEnumDeviceInterfaces_ 関数にあたるでしょうか。
なぜか非同期呼び出しです。理由を考えるに、引数なしの DeviceInformation.FindAllAsync()
が「これまでPCに接続されたことのあるすべてのデバイスの情報」を列挙するため、とても時間がかかるからではないかと。
とまあ、これだけの記述でデバイスパスが列挙できてしまったことになります。
デバイスを開いて、先程のデバイス情報を確認してみます。
foreach(DeviceInformation inf in collection)
{
HidDevice dev = await HidDevice.FromIdAsync(inf.Id, FileAccessMode.ReadWrite);
if (dev != null)
{ // あったよー!
dev.VendorId;
dev.ProductId;
dev.UsagePage;
dev.UsageId;
}
}
なんと、たったこれだけの記述でHIDのAttributesやCappabilitiesに相当する情報が取得できてしm・・・
・・・うまくいきません。
マニフェストの編集
UWPでは無条件にHIDデバイスにアクセスできるわけではなかったのです。
どうやらソリューション内に自動生成される Package.appxmanifest
ファイルを編集する必要が有るようです。
HID デバイスにアクセスする Windows ランタイム アプリでは、そのマニフェストの Capabilities ノードに特定の DeviceCapability データが含まれている必要があります。
<!-- HID Device -->
<DeviceCapability Name="humaninterfacedevice">
<Device Id="vidpid:0A81 0701">
<Function Type="usage:ffa0 0001"/>
</Device>
</DeviceCapability>
参考: HID のデバイス機能を指定する方法 (Windows ランタイム アプリ)
さらに、Capabilities に追記すれば何でもかんでも使えるようになるわけでもないようです。
他の Windows API や OS の動作との競合を避けるために、次の利用状況ページで表される最上位のアプリケーション コレクションをブロックします。
- HID_USAGE_PAGE_UNDEFINED
- HID_USAGE_PAGE_GENERIC
- HID_USAGE_GENERIC_KEYBOARD
- ...(以下略)
参考:ヒューマン インターフェイス デバイス (XAML) のサポート
基本的に、DeviceCapability には VendorID, ProductID, UsagePage, Usage の4つすべての値を指定する必要がありそうです。
<Device Id="any">
や <Function Type="usage:ff00 *"/>
といった指定が可能とは書いてありますが、
具体的にどのような条件においてこのようなワイルド指定が有効になるのか分かりません(ビルドしても実行できない)。
あらためてHIDを開いてみる
いちおうデバッグ用のトレース処理や例外処理も追記してみました。
try
{
HidDevice dev = await HidDevice.FromIdAsync(inf.Id, FileAccessMode.ReadWrite);
if (dev != null)
{
Debug.WriteLine($"Succeeded to open HID");
}
else
{
Debug.WriteLine($"Failed to open HID");
var dai = DeviceAccessInformation.CreateFromId(inf.Id);
Debug.WriteLine($"CurrentStatus:{dai.CurrentStatus.ToString()}");
}
catch (Exception e)
{
Debug.WriteLine(e.ToString());
}
さて、これでどうでしょう・・・?
> Failed to open HID
> System.IO.FileNotFoundException: 指定されたファイルが見つかりません。 (Exception from HRESULT: 0x80070002)
> at Windows.Devices.Enumeration.DeviceAccessInformation.CreateFromId(String deviceId)
・・・????
ちなみに
Raspberry Pi3 上で同じコードを実行するとすんなり開けました。
> Succeeded to open HID
これで当初の目的は果たせたのカモしれませんが、デスクトップでも同じコードが同じように走ってくれないとデバッグの効率が悪いです。
Windows.Devices.HumanInterfaceDevice
のバグなのかなあ、解せないなあ・・・。