Managed Library for Nintendo's Wiimote
| 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. |
Difficulty: Intermediate Time Required: 1-3 hours Cost: Less Than $50 (Free if you already own a Wiimote) Hardware: Nintendo Wii Remote (Wiimote), a compatible PC Bluetooth adapter and stack |
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...
- Start up your Bluetooth software and have it search for a device.
- 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.
- 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.
- 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.
- 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).
- 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:
- Get the GUID of the HID class defined by Windows
- Get a handle to the list of all devices which are part of the HID class
- Enumerate through those devices and get detailed information about each
- Compare the Vendor ID and Product ID of each device to the known Wiimote's VID and PID
- When found, create a FileStream to read/write to the device
- 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.
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.