반응형

출처 : http://blogs.msdn.com/coding4fun/archive/2007/03/14/1879033.aspx

Managed Library for Nintendo's Wiimote

Published 14 March 07 01:05 AM | Coding4Fun 

  In this article, Brian Peek demonstrates how to connect to and use the Nintendo Wiimote from C# and VB.NET. The final output is an easy-to-use managed API for the Wiimote that can be used in any managed application.
ASPSOFT, Inc.

Difficulty: Intermediate
Time Required: 1-3 hours
Cost: Less Than $50 (Free if you already own a Wiimote)
Software: Visual C# or Visual Basic 2008 Express Editions
Hardware: Nintendo Wii Remote (Wiimote), a compatible PC Bluetooth adapter and stack
Download: CodePlex
 

Updates

6/15/08 – Version 1.5.2 is now up at CodePlex.  The Balance Board should really work this time….

6/12/08 - Version 1.5.1 has been removed from CodePlex...it has proven to be too buggy for most users (though it continues to work just fine for me).  Look for version 1.5.2 soon...

6/11/08 – Version 1.5.1 posted at CodePlex.  Fixed a bug with the Balance Board…  UPDATE:  It appears some people are still having issues with this build as well due to some Balance Boards being a bit finicky in their response times.  Stay tuned for build 1.5.2 soon…

6/9/08 – Version 1.5 posted at CodePlex.  The Wii Fit Balance Board is now supported.

6/3/08 – New version 1.4 posted at the download link above.  The big change is multiple Wiimotes are now finally supported.

5/27/08 - New version 1.3 posted at the download link above.  Lots of changes, so be sure to read the included docs!

1/29/08 - New version 1.2.1.0 posted at the download link above.  The only change for this release is adding support for IR3 and IR4 since I've gotten so many questions about it.

10/22/07 - New version 1.2.0.0 posted at the download link above.  There are a variety of fixes and new features added.  The source code and binary releases are now hosted at CodePlex.  A .chm help file is also included documenting the API itself.  Please note that a license is now included with the library which explicitly defines how the library can be used.  For 99% of you, this won't change anything, but I've received tons of emails asking about restrictions, so now there's something official attached.  Check out the readme.txt and license.txt in the docs directory included with the official distribution for more information.

6/12/07 - New version 1.1.0.0 posted at the download link above.  This fixes several issues, adds an alternate writing method which may help those with troubled bluetooth stacks/adapters, adds Vista/XP x64 support, and a Microsoft Robotics Studio service version of the library.  See the included readme.txt for more information.  Additionally, see my new article on using the new MSRS service to create a Wiimote-controlled car!

3/17/07 - New version 1.0.1.0 posted at the download link above.  This fixes a bug with the calibration data being stored by the API.  Thanks to James Darpinian for the catch!

Introduction

Nintendo's Wii Remote (forever known as the Wiimote) is a fantastic little controller for the Nintendo Wii system.  Because it uses Bluetooth to communicate with the Wii, it can be connected to and used by practically any Bluetooth capable device.

If you care to just use the library and are not concerned with the implementation details, skip down to the usage section.

Before we get started, there are two websites that one should peruse before reading the code contained here.  These two sites have done 99% of the work of figuring out the data that is sent and received with the Wiimote.  I am not going to repeat most of this information here since both of the these sites explain the protocol so thoroughly.  Without the work of the people posting to these sites, none of this would have been possible.

Getting Connected

This will likely be the biggest sticking point.  The Wiimote will not pair and communicate successfully with every Bluetooth device and stack on the planet.   There's little I can do to help get you connected if the following steps do not work.  Either it's going to work, or it isn't.  Cross your fingers...

  1. Start up your Bluetooth software and have it search for a device.
  2. Hold down the 1 and 2 buttons on the Wiimote.  You should see the LEDs at the bottom start flashing.  Do not let go of these buttons until this procedure is complete.  If pairing the Wii Fit Balance Board, open the battery cover on the underside of the balance board and hold the little red sync button.
  3. Wiimotes should show up in the list of devices found as Nintendo RVL-CNT-01.  Balance Boards will show up as Nintendo RVL-WBC-01.  If it's not there, start over and try again.
  4. Click Next to move your way through the wizard.  If at any point you are asked to enter a security code or PIN, leave the number blank or click Skip.  Do not enter a number.
  5. You may be asked which service to use from the Wiimote.  Select the keyboard/mouse/HID service if prompted (you should only see one service available).
  6. Finish the wizard.

That's it.  The LEDs at the bottom should continue to flash and you should see the device listed in your list of connected Bluetooth devices.  If you run the test application included with the source code and you see the numbers change, you are all set.  If you don't see them change or you get an error, try the above again.  If it continues to not function, you are likely stuck with an incompatible device or stack.

The Exciting World of HID and P/Invoke

When the Wiimote is paired with your PC, it will be identified as a HID-compliant device.  Therefore, to connect to the device, we must use the HID and Device Management Win32 APIs.  Unfortunately there is no built-in support for these APIs in the current .NET runtime, so we must enter the realm of P/Invoke.  These APIs are defined in the Windows Driver Kit (WDK) so if you wish to see the original C header files or read the API documentation, you will need to download and install the latest WDK.

P/Invoke, as you probably know, allows one to directly call methods of the Win32 API from .NET.  The difficulty of this is finding the right method signatures and structure definitions which will properly marshal the data through to Win32.  A great resource for these signatures is the P/Invoke wiki.  Almost all of the methods used in this project were defined by this resource.  For this project, all P/Invoke'd methods live in the HIDImports class.

The process to begin communication with the Wiimote is as follows:

  1. Get the GUID of the HID class defined by Windows
  2. Get a handle to the list of all devices which are part of the HID class
  3. Enumerate through those devices and get detailed information about each
  4. Compare the Vendor ID and Product ID of each device to the known Wiimote's VID and PID
  5. When found, create a FileStream to read/write to the device
  6. Clean up the device list

In code, the process is as follows (shortened from the original code for space):

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);

CreateFile and SafeFileHandles

If you read through the above code, you may have noticed that the handle to the Wiimote is opened using Win32's CreateFile function instead of directly using a FileStream object or some other managed way.  This is required due to the way that the handle needs to be created.  The DevicePath member of the detail struct contains a non-file system path that Win32 can use to open a handle to the device.  The .NET methods for performing this action will only allow file system paths, so we must use the Win32 method instead.

You will also notice that we use the SafeFileHandle object to wrap the handle returned from the call to CreateFile.  The SafeFileHandle object wraps a native Win32 handle and allows one to safely manage the native type and cleanly close things up at the end of the application.  One could just as easily use an IntPtr, but I have found that this is a much cleaner method for dealing with the native type.

Wiimote I/O and HID Reports

In the world of HID, data is sent and received as "reports".  Simply, it is a data buffer of a pre-defined length with a header that determines the report contained in the buffer.  The Wiimote will send and can receive various reports, all of which are 22 bytes in length, and all of which are explained at the links above.  Given the number and complexity, I suggest you read through the wikis above if you wish to know more about the Wiimote's reports and the data they contain.

Now that we have a a FileStream on which to communicate with the Wiimote, we can start communication.  Because reports will be sent and received almost constantly, it is essential that asynchronous I/O operations are used.  In .NET, this is quite simple.  The process is to start an asynchronous read operation and provide a callback method to be run when the buffer is full.  When the callback is run, the data is handled and the process is repeated.

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....
}

That's it!

You may not believe it, but that code is enough to open and start communicating with the Wiimote.  The rest of the code involves parsing the data sent from the Wiimote and writing properly formed data to the Wiimote.  As I said, I'm not going to go into the detail of the reports as the sites above will do a much better job than I, but one can write any command to the Wiimote simply by doing the following:

VB

mStream.Write(mBuff, 0, REPORT_LENGTH)

C#

mStream.Write(mBuff, 0, REPORT_LENGTH);

Reading is done with the async code you see above.  When a report of 22 bytes is received, the OnReadData method is called, and the data can be properly parsed and used.

Usage of the API

If you don't care about the implementation details, you've likely skipped down to this section to learn how to use the API in your own application.  The easiest way to learn how it all works is to look at the WiimoteTest application included with the source code.

captured_Image.png

First, you will need to add a reference to the WiimoteLib.dll included with the source code.  Next, you will need to pull the namespace into your application with the standard using/Imports statement.  With that in place, you can now create an instance of the Wiimote class and start using it.  Simply instantiate a new instance of the Wiimote class, setup events if you wish to use them, setup the report type of the data you want to be returned, and call the Connect method.

VB

Imports WiimoteLib

Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs)
' create a new instance of the Wiimote
Dim wm As Wiimote = New Wiimote()

' setup the event to handle state changes
AddHandler wm.WiimoteChanged, AddressOf wm_WiimoteChanged

' setup the event to handle insertion/removal of extensions
AddHandler wm.WiimoteExtensionChanged, AddressOf wm_WiimoteExtensionChanged

' connect to the Wiimote
wm.Connect()

' set the report type to return the IR sensor and accelerometer data (buttons always come back)
wm.SetReportType(Wiimote.InputReport.IRAccel, True)
End Sub


Private Sub wm_WiimoteExtensionChanged(ByVal sender As Object, ByVal args As WiimoteExtensionChangedEventArgs)
If args.Inserted Then
wm.SetReportType(Wiimote.InputReport.IRExtensionAccel, True) ' return extension data
Else
wm.SetReportType(Wiimote.InputReport.IRAccel, True) ' back to original mode
End If
End Sub

Private Sub wm_OnWiimoteChanged(ByVal sender As Object, ByVal args As WiimoteChangedEventArgs)
' current state information
Dim ws As WiimoteState = args.WiimoteState

' write out the state of the A button
Debug.WriteLine(ws.ButtonState.A)
End Sub

C#

using WiimoteLib;

private void Form1_Load(object sender, EventArgs e)
{
// create a new instance of the Wiimote
Wiimote wm = new Wiimote();

// setup the event to handle state changes
wm.WiimoteChanged += wm_WiimoteChanged;

// setup the event to handle insertion/removal of extensions
wm.WiimoteExtensionChanged += wm_WiimoteExtensionChanged;

// connect to the Wiimote
wm.Connect();

// set the report type to return the IR sensor and accelerometer data (buttons always come back)
wm.SetReportType(Wiimote.InputReport.IRAccel, true);
}


void wm_WiimoteExtensionChanged(object sender, WiimoteExtensionChangedEventArgs args)
{
if(args.Inserted)
wm.SetReportType(Wiimote.InputReport.IRExtensionAccel, true); // return extension data
else
wm.SetReportType(Wiimote.InputReport.IRAccel, true); // back to original mode
}

void wm_WiimoteChanged(object sender, WiimoteChangedEventArgs args)
{
// current state information
WiimoteState ws = args.WiimoteState;

// write out the state of the A button
Debug.WriteLine(ws.ButtonState.A);
}

If you wish to use multiple Wiimotes, instantiate a WiimoteCollection object, call the FindAllWiimotes method to initialize it, and then use each individual Wiimote object in the collection as a separate instance.

VB

Dim wc As New WiimoteCollection()
wc.FindAllWiimotes()

For Each wm As Wiimote In wc
AddHandler wm.WiimoteChanged, AddressOf wm_WiimoteChanged
AddHandler wm.WiimoteExtensionChanged, AddressOf wm_WiimoteExtensionChanged

wm.Connect()
wm.SetReportType(InputReport.IRAccel, True)
Next wm

C#

WiimoteCollection wc = new WiimoteCollection();
wc.FindAllWiimotes();

foreach(Wiimote wm in wc)
{
wm.WiimoteChanged += wm_WiimoteChanged;
wm.WiimoteExtensionChanged += wm_WiimoteExtensionChanged;

wm.Connect();
wm.SetReportType(InputReport.IRAccel, true);
}

Data can be retrieved from the API in two ways:  events and polling.  In event mode, one must subscribe to the WiimoteChanged event as shown above.  Then, when data is sent from the Wiimote to the PC, an event will be posted to your event handler for processing in your application.  If you elect to not use the event model, you may simply retrieve state information at any time from the WiimoteState property of the Wiimote class.

Report Types

The library currently supports only a handful of the many report types the Wiimote is capable of producing, however the ones that are implemented are enough to get all required data for the Wiimote and all current extensions.  Specifically, these reports are:

  • Buttons - Button data only
  • ButtonsAccel - Button and accelerometer data
  • IRAccel - Button, accelerometer and IR data
  • ButtonsExtension – Button and extension data
  • ExtensionAccel - Button, accelerometer and extension data
  • IRExtensionAccel - Button, accelerometer, extension and IR data

The report type can be set by calling the SetReportType method using the appropriate report type and whether or not you would like the data to be sent continuously or only when the state of the controller has changed.

Extensions

There are currently three Wii extensions supported by the library: the Nunchuk, the Classic Controller, and the Guitar Hero controller.  If you wish to use these, you must setup an event handler for the WiimoteExtensionChanged event.  When this event is called, you can check the event argument to determine if an extension was inserted or removed, and what type extension was inserted.  In this event handler, you will need to change the report type with the SetReportType method to one that supports extension data, otherwise the data will not be returned to you.

If you are using a strictly polled method, you may also check the Extension and ExtensionType parameters of the WiimoteState property to determine when an extension has been inserted or removed.

The Wii Fit Balance Board will show up as a Wiimote controller with an extension attached.  The report type is set internally and any attempt to set a new report type on this device will be ignored.  The power button maps to the Wiimote’s A button, and the LED maps to the Wiimote’s LED1.  The rest of the Wiimote’s properties are ignored.  The remainder of the balance board information can be found in the BalanceBoard struct inside the WiimoteState object.

State Information

The heart of the entire library is contained in the WiimoteState object.  Check the download's included .chm help file for the full set of properties available. 

To Do...

At the moment, usage of the speaker on the Wiimote is not supported.  I will likely add this in a future update.  Additionally, there are several report types which are not implemented, however the reports which are implemented return all necessary information.  I would also like to add some "higher level" functionality that will return pitch/roll angles, mouse cursor position for the IR sensor, etc.  Check this article and my blog for information on updates to the library.

Conclusion

And there we have it.  A fully functional managed library for the Wiimote.  Give it a try and integrate it into your existing applications or build something new!  I'm looking forward to some creative uses of the library...

If you have any issues using the library, or have any feature requests (other than sound), please don't hesitate to contact me directly or post a message in my forum dedicated to the project.

Enjoy!

Links

Bio

Brian is a Microsoft C# MVP who has been actively developing in .NET since its early betas in 2000, and who has been developing solutions using Microsoft technologies and platforms for even longer. Along with .NET, Brian is particularly skilled in the languages of C, C++ and assembly language for a variety of CPUs. He is also well-versed in a wide variety of technologies including web development, document imaging, GIS, graphics, game development, and hardware interfacing. Brian has a strong background in developing applications for the health-care industry, as well as developing solutions for portable devices, such as tablet PCs and PDAs. Additionally, Brian has co-authored the book "Debugging ASP.NET" published by New Riders, and is currently co-authoring a book titled "10 Coding4Fun Projects with .NET for Programmers, Hobbyists, and Game Developers" to be published by O'Reilly in late 2008. Brian also writes for MSDN's Coding4Fun website, contributing articles on a monthly basis.

반응형
Posted by Real_G