与Wii控制手柄通信的托管代码库(一)

来源:互联网 发布:广电网络辽中分公司 编辑:程序博客网 时间:2024/06/13 20:50

 

2009-01-16 翻译

HID Human Input Device     人工输入设备

Wii Fit Balance Board       平衡板

IR                          红外传感器

Windows Driver Kit          Windows驱动开发包

Wiimote                     Wii控制手柄

Report                      报文

 

2007-3-14 Coding4Fun发布

 

在本文中, Brian Peek演示了如何使用C#VB.NET连接和使用任天堂控制手柄(Wiimote)。最终成果是一个可以在托管代码应用中方便使用的Wiimot托管代码API

Brian Peek

ASPSOFT, Inc.

 

难度: 中级

时间: 1-3 小时

花费: 小于$50 (如果已经有Wiimote则免费)

软件: Visual C# or Visual Basic 2008 Express Editions

硬件: Nintendo Wii控制手柄(Wiimote), 一个兼容的PC蓝牙适配器

下载: CodePlex

 

第0章       更新历史

(最新版本已经为1.6, 以下为1.6的更新记录。译者注)

  •  
    • 新增对平衡板“重力中心”的计算(感谢Steven Battersby)
    • 结构声明为[Serializable] (Caio建议)
    • 电池(Battery)属性现在改为浮点型,能够反映电池剩余电量的百分比。
    • BatteryRaw为字节类型,原本用于保存电池属性。
    • WiimoteTest应用程序现在在启动时能够正确的读取扩展信息。
    • Wiimote对象中新增HIIDevicePath属性,以显示HID设备路径。
    • 写的时间延时由100毫秒改为50毫秒,这能够提高LED和滚轮的响应能力。

6/15/08Version 1.5.2,平衡板能够真正的工作了……

6/12/08Version 1.5.1 CodePlex删除...事实证明该版本对大多数用户存在太多的BUG(尽管对我而言一直不错),期待1.5.2的很快到来...

6/11/08Version 1.5.1CodePlex上发布。修改了一个关于平衡板的BUG更正似乎很多人在使用该版本时发现仍然存在问题,原因是平衡板对响应时间的要求比较精确。等待1.5.2的到来

6/9/08 – Version 1.5 CodePlex上发布。支持 Wii Fit Balance Board(平衡板)。

6/3/08 –Version 1.4 发布并提供下载。最重要的改进是支持多个Wiimote

5/27/08 - Version 1.3发布并提供下载。改动量很大,一定要阅读内置的文档!

1/29/08 - Version 1.2.1.0发布并提供下载。唯一的改进是对IR3IR4的支持,因为我遇到了很多关于它们的问题。

10/22/07 - Version 1.2.0.0发布并提供下载。修改了一些BUG并增加了新功能。源代码和二进制发布代码都在CodePlex上提供。同时提供了一个关于API使用的chm帮助文件。请留意库中包含的一个license文件详细描述的本软件的使用许可情况。使用许可对99%的用户没有任何变化,但由于我收到了大量的询问有关使用许可的电子邮件,因此附加了这个正式的说明。在正式发布中的docs目录下,readme.txt license.txt有详细描述。

6/12/07 - Version 1.1.0.0发布并提供下载。 修改了几个错误,新增了一个可选的写方法,这可能对使用蓝牙协议栈/适配器有麻烦的用户有所帮助。增加了对Vista/XP x64 的支持, 以及一个Microsoft Robotics Studio服务版本。更多信息见包含的 readme.txt的信息。另外,在我的新文档中描述了如何使用MSRS服务创建一个Wiimote遥控车。

3/17/07 - Version 1.0.1.0发布并提供下载。 修正了API中关于校准数据的BUG。感谢James Darpinian 的指正!

 

第1章       简介

任天堂的Wii 控制手柄 (被称为Wiimote)Wii系统中一个神奇的小控制器。它通过蓝牙系统与Wii连接, 因此能够与其它任何支持蓝牙的设备连接。

如果你仅仅关注于如何使用这个库文件,而不关心其实现细节,可以直接跳转到API应用章节(7)

在阅读代码之前,有两个网站应当仔细的浏览一下。99%的Wiimote发送和接收数据的辨识工作由这两个网站完成。在网站上有对协议很详细的描述,我在此就不再重复。没有在这些网站上发贴的人们,本文中的功能根本不可能实现。

·    WiiLi Wiki

·    WiiBrew Wiki

第2章       开始连接

这里可能是最关键的时刻,Wiimote不是能和地球上所有蓝牙设备和协议栈进行通信的, 如果下面几步不能通过,我也不能提供任何帮助。能够工作,或者不能,你只能进行祈祷了...

1.启动你的蓝牙软件并开始搜索设备。

2.按下Wiimote 1 2 按键,会看到底部的LED开始闪烁。在完成之前不要松开按键。对于平衡板,打开下部的电池盖,按下一个红色的小的同步按钮。

3.Wiimotes应当在设备列表中显示为Nintendo RVL-CNT-01 平衡板显示为 Nintendo RVL-WBC-01。如果没找到,重新启动并再试一下。

4.在向导中点击下一步。如果系统提示输入安全码或者PIN,不要输入或者选择跳过。

5.如果被要求选择Wiimote 使用何种服务,选择键盘/鼠标/HID服务其中的一种。

6.完成向导窗口。

这就可以了。底部的LED继续闪烁,而且设备显示在蓝牙设备列表中。运行源代码中的应用程序,你应该可以看到显示的数字变化,这标志着成功的连接了Wiimote设备。如果没有变化或者出现错误, 只能重新试一下。要是还不行, 很不幸,你可能使用的是一种不兼容的设备或协议栈。(这个过程确实需要多次尝试,可以参考附录中译者的连接方法作为参考。)

第3章       进入令人激动的HIDP/Invoke世界

Wiimote可以与你的PC匹配,它被视为一种HID兼容设备。因此,为了连接该设备,我们必须使用HID和设备管理WIN32 API。不幸的是,目前的.NET运行环境中没有内置支持这些API,因此需要进入P/Invoke的领域。这些APIWindows驱动开发包(WDK)中定义, 如果希望看到原始的C头文件或阅读API文档,需要下载并安装最新的WDK

P/Invoke, 或许你已经有所了解,允许用户在.NET中直接调用Win32 API。这里的难点是找到合法的方法名称和格式定义,能够正确的将串行化数据并传递到Win32。对此P/Invoke wiki是一个很好的资源,几乎所有本项目用到的方法都在此能找到。在本项目中,所有的P/Invoke方法在HIDImports类中定义。

Wiimote通信的过程如下:

1.Windows得到GUIDHID类定义。

2.得到HID类中所有设备的操作句柄。

3.在设备列表中进行遍历以得到每个设备的详细信息。

4.比较制造商ID和产品ID是否为已知的Wiimote制造商IDVID)和产品ID(PID)

5.在找到设备后,创建FileStream对设备进行读写操作。

6.清除设备列表。

 该步骤的实现代码如下(有省略)

VB

' read/write handle to the device
Private mHandle As SafeFileHandle

' a pretty .NET stream to read/write from/to

Private mStream As FileStream
Private found As Boolean = False

Private guid As Guid
Private index As
UInteger = 0

' 1. get the GUID of the HID class

HIDImports.HidD_GetHidGuid(guid)

' 2. get a handle to all devices that are part of the HID class
Dim hDevInfo As IntPtr = HIDImports.SetupDiGetClassDevs(guid, Nothing, IntPtr.Zero, HIDImports.DIGCF_DEVICEINTERFACE) ' | HIDImports.DIGCF_PRESENT);

' create a new interface data struct and initialize its size
Dim diData As HIDImports.SP_DEVICE_INTERFACE_DATA = New HIDImports.SP_DEVICE_INTERFACE_DATA()
diData.cbSize = Marshal.SizeOf(diData)

' 3. get a device interface to a single device (enumerate all devices)

Do While HIDImports.SetupDiEnumDeviceInterfaces(hDevInfo, IntPtr.Zero, guid, index, diData)
   
' create a detail struct and set its size

   
Dim diDetail As HIDImports.SP_DEVICE_INTERFACE_DETAIL_DATA = New HIDImports.SP_DEVICE_INTERFACE_DETAIL_DATA()
    diDetail.cbSize = 5
'should be: (uint)Marshal.SizeOf(diDetail);, but that's the wrong size


   
Dim size As UInt32 = 0

   
' get the buffer size for this device detail instance (returned in the size parameter)

    HIDImports.SetupDiGetDeviceInterfaceDetail(hDevInfo, diData, IntPtr.Zero, 0, size, IntPtr.Zero)

   
' actually get the detail struct
   
If HIDImports.SetupDiGetDeviceInterfaceDetail(hDevInfo, diData, diDetail, size, size, IntPtr.Zero) Then
       
' open a read/write handle to our device using the DevicePath returned
        mHandle = HIDImports.CreateFile(diDetail.DevicePath, FileAccess.ReadWrite, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, HIDImports.EFileAttributes.Overlapped, IntPtr.Zero)

       
' 4. create an attributes struct and initialize the size
       
Dim attrib As HIDImports.HIDD_ATTRIBUTES = New HIDImports.HIDD_ATTRIBUTES()
        attrib.Size = Marshal.SizeOf(attrib)

       
' get the attributes of the current device

       
If HIDImports.HidD_GetAttributes(mHandle.DangerousGetHandle(), attrib) Then
           
' if the vendor and product IDs match up
           
If attrib.VendorID = VID AndAlso attrib.ProductID = PID Then
               
' 5. create a nice .NET FileStream wrapping the handle above
                mStream =
New FileStream(mHandle, FileAccess.ReadWrite, REPORT_LENGTH, True)
           
Else

                mHandle.Close()
           
End If
       
End If
   
End If

   
' move to the next device
    index += 1
Loop

' 6. clean up our list
HIDImports.SetupDiDestroyDeviceInfoList(hDevInfo)

C#

// read/write handle to the device
private SafeFileHandle mHandle;

// a pretty .NET stream to read/write from/to

private FileStream mStream;
bool found = false
;
Guid guid;
uint
index = 0;

// 1. get the GUID of the HID class

HIDImports.HidD_GetHidGuid(
out guid);

// 2. get a handle to all devices that are part of the HID class

IntPtr hDevInfo = HIDImports.SetupDiGetClassDevs(
ref guid, null, IntPtr.Zero, HIDImports.DIGCF_DEVICEINTERFACE);// | HIDImports.DIGCF_PRESENT);

// create a new interface data struct and initialize its size
HIDImports.SP_DEVICE_INTERFACE_DATA diData =
new HIDImports.SP_DEVICE_INTERFACE_DATA();
diData.cbSize = Marshal.SizeOf(diData);

// 3. get a device interface to a single device (enumerate all devices)

while(HIDImports.SetupDiEnumDeviceInterfaces(hDevInfo, IntPtr.Zero, ref guid, index, ref diData))
{
   
// create a detail struct and set its size

    HIDImports.SP_DEVICE_INTERFACE_DETAIL_DATA diDetail =
new HIDImports.SP_DEVICE_INTERFACE_DETAIL_DATA();
    diDetail.cbSize = 5;
//should be: (uint)Marshal.SizeOf(diDetail);, but that's the wrong size


    UInt32 size = 0;

   
// get the buffer size for this device detail instance (returned in the size parameter)
    HIDImports.SetupDiGetDeviceInterfaceDetail(hDevInfo,
ref diData, IntPtr.Zero, 0, out size, IntPtr.Zero);

   
// actually get the detail struct

   
if(HIDImports.SetupDiGetDeviceInterfaceDetail(hDevInfo, ref diData, ref diDetail, size, out size, IntPtr.Zero))
    {
       
// open a read/write handle to our device using the DevicePath returned

        mHandle = HIDImports.CreateFile(diDetail.DevicePath, FileAccess.ReadWrite, FileShare.ReadWrite, IntPtr.Zero, FileMode.Open, HIDImports.EFileAttributes.Overlapped, IntPtr.Zero);

       
// 4. create an attributes struct and initialize the size
        HIDImports.HIDD_ATTRIBUTES attrib =
new HIDImports.HIDD_ATTRIBUTES();
        attrib.Size = Marshal.SizeOf(attrib);

       
// get the attributes of the current device

       
if(HIDImports.HidD_GetAttributes(mHandle.DangerousGetHandle(), ref attrib))
        {
           
// if the vendor and product IDs match up

           
if(attrib.VendorID == VID && attrib.ProductID == PID)
            {
               
// 5. create a nice .NET FileStream wrapping the handle above

                mStream =
new FileStream(mHandle, FileAccess.ReadWrite, REPORT_LENGTH, true);
            }
           
else

                mHandle.Close();
        }
    }

   
// move to the next device
    index++;
}

// 6. clean up our list
HIDImports.SetupDiDestroyDeviceInfoList(hDevInfo);

第4章       CreateFile SafeFileHandles

在看过上面的代码后,你可能注意到Wiimote的操作句柄是通过Win32CreateFile方法打开的,而没有直接使用FileStream对象或者其它托管方式。这是由句柄创建方式的需要所决定的。diDetail结构中的 DevicePath 成员保存了一个非文件系统路径,Win32可以使用该句柄打开设备,而.NET仅允许文件系统路径,因此我们必须使用Win32方法。

同时你可能注意到我们使用了SafeFileHandle 对象包装CreateFile调用返回的句柄。SafeFileHandle对象包装了本地(非托管的)的Win32句柄,允许安全的管理本地类型,并在应用退出时干净的关闭这些句柄。当然可以使用更容易的IntPtr,但我发现对本地类型这种方式是更为干净的处理方式。

第5章       Wiimote I/O HID 报文

HID世界中,数据以报文的方式发送和接收。简要的说,报文就是一个已定义长度的数据缓冲区,它带有的头信息决定了报文的内容。Wiimote接收和发送多种报文,都是22字节长,在上面提到的网站中有详细的描述。考虑到其数量和复杂性,如果你希望了解Wiimote的报文和数据内容,我建议你自行阅读wikis中的相关资料。

现在我们得到了与Wiimote通信的 FileStream对象。因为报文会在同一时刻进行收发,所以必须采用异步I/O操作。在.NET中做到这点并不困难。在方法开始时进行一个异步读操作,并在缓冲区满后提供一个回调函数。在回调函数中,进行数据处理并重复调用该方法。

VB 

' sure, we could find this out the hard way using HID, but trust me, it's 22
Private Const REPORT_LENGTH As Integer = 22

' report buffer

Private mBuff As Byte() = New Byte(REPORT_LENGTH - 1){}

Private Sub
BeginAsyncRead()
   
' if the stream is valid and ready

   
If mStream.CanRead Then
       
' create a read buffer of the report size
       
Dim buff As Byte() = New Byte(REPORT_LENGTH - 1){}

       
' setup the read and the callback

        mStream.BeginRead(buff, 0, REPORT_LENGTH,
New AsyncCallback(AddressOf OnReadData), buff)
   
End If

End Sub

Private Sub OnReadData(ByVal ar As IAsyncResult)
   
' grab the byte buffer

   
Dim buff As Byte() = CType(ar.AsyncState, Byte())

   
' end the current read

    mStream.EndRead(ar)

   
' start reading again
    BeginAsyncRead()

    
' handle data....
End Sub

C#

// sure, we could find this out the hard way using HID, but trust me, it's 22
private const int REPORT_LENGTH = 22;

// report buffer

private byte[] mBuff = new byte[REPORT_LENGTH];

private void
BeginAsyncRead()
{
   
// if the stream is valid and ready

   
if(mStream.CanRead)
    {
       
// create a read buffer of the report size

       
byte[] buff = new byte[REPORT_LENGTH];

       
// setup the read and the callback

        mStream.BeginRead(buff, 0, REPORT_LENGTH,
new AsyncCallback(OnReadData), buff);
    }
}

private void
OnReadData(IAsyncResult ar)
{
   
// grab the byte buffer

   
byte[] buff = (byte[])ar.AsyncState;

   
// end the current read

    mStream.EndRead(ar);

   
// start reading again
    BeginAsyncRead();
   
   
// handle data....
}

第6章       完成!

你可能不相信,但这些代码已经能够连接并与Wiimote通信。 接下来的代码包括解析接收的数据并向Wiimote发送格式正确的代码。正象上面提到的, 我没打算在此详细说明这些细节,网站上可以提供更好的说明。

Wiimote发送命令的方式如下:

VB

mStream.Write(mBuff, 0, REPORT_LENGTH)

C#

mStream.Write(mBuff, 0, REPORT_LENGTH);

读取功能在上面的异步代码中完成。在收到22字节数据后, 调用OnReadData方法,然后正确的解析和使用这些数据。