Adding support for usage value and thus joystick/gamepad.
Adding generic support for direction pad.
2 using System.Windows.Forms;
3 using System.Runtime.InteropServices;
4 using System.Diagnostics;
6 using Microsoft.Win32.SafeHandles;
8 using System.Collections.Generic;
15 /// We provide utility functions to interpret gamepad dpad state.
17 public enum DirectionPadState
31 /// Represent a HID event.
32 /// TODO: Rename this into HidRawInput?
34 public class HidEvent : IDisposable
36 public bool IsValid { get; private set; }
37 public bool IsForeground { get; private set; }
38 public bool IsBackground { get { return !IsForeground; } }
39 public bool IsMouse { get; private set; }
40 public bool IsKeyboard { get; private set; }
42 /// If this not a mouse or keyboard event then it's a generic HID event.
44 public bool IsGeneric { get; private set; }
45 public bool IsButtonDown { get { return Usages.Count == 1 && Usages[0] != 0; } }
46 public bool IsButtonUp { get { return Usages.Count == 0; } }
47 public bool IsRepeat { get { return RepeatCount != 0; } }
48 public uint RepeatCount { get; private set; }
50 public HidDevice Device { get; private set; }
51 public RAWINPUT RawInput { get {return iRawInput;} }
52 private RAWINPUT iRawInput;
54 public ushort UsagePage { get; private set; }
55 public ushort UsageCollection { get; private set; }
56 public uint UsageId { get { return ((uint)UsagePage << 16 | (uint)UsageCollection); } }
57 public List<ushort> Usages { get; private set; }
59 /// Sorted in the same order as Device.InputValueCapabilities.
61 public Dictionary<HIDP_VALUE_CAPS,uint> UsageValues { get; private set; }
62 //TODO: We need a collection of input report
63 public byte[] InputReport { get; private set; }
65 public delegate void HidEventRepeatDelegate(HidEvent aHidEvent);
66 public event HidEventRepeatDelegate OnHidEventRepeat;
68 private System.Timers.Timer Timer { get; set; }
69 public DateTime Time { get; private set; }
70 public DateTime OriginalTime { get; private set; }
72 //Compute repeat delay and speed based on system settings
73 //Those computations were taken from the Petzold here: ftp://ftp.charlespetzold.com/ProgWinForms/4%20Custom%20Controls/NumericScan/NumericScan/ClickmaticButton.cs
74 private int iRepeatDelay = 250 * (1 + SystemInformation.KeyboardDelay);
75 private int iRepeatSpeed = 405 - 12 * SystemInformation.KeyboardSpeed;
78 /// Tells whether this event has already been disposed of.
80 public bool IsStray { get { return Timer == null; } }
83 /// We typically dispose of events as soon as we get the corresponding key up signal.
87 Timer.Enabled = false;
89 //Mark this event as a stray
94 /// Initialize an HidEvent from a WM_INPUT message
96 /// <param name="hRawInputDevice">Device Handle as provided by RAWINPUTHEADER.hDevice, typically accessed as rawinput.header.hDevice</param>
97 public HidEvent(Message aMessage, HidEventRepeatDelegate aRepeatDelegate)
105 OriginalTime = DateTime.Now;
106 Timer = new System.Timers.Timer();
107 Timer.Elapsed += (sender, e) => OnRepeatTimerElapsed(sender, e, this);
108 Usages = new List<ushort>();
109 UsageValues = new Dictionary<HIDP_VALUE_CAPS,uint>();
110 OnHidEventRepeat += aRepeatDelegate;
112 if (aMessage.Msg != Const.WM_INPUT)
114 //Has to be a WM_INPUT message
118 if (Macro.GET_RAWINPUT_CODE_WPARAM(aMessage.WParam) == Const.RIM_INPUT)
122 else if (Macro.GET_RAWINPUT_CODE_WPARAM(aMessage.WParam) == Const.RIM_INPUTSINK)
124 IsForeground = false;
127 //Declare some pointers
128 IntPtr rawInputBuffer = IntPtr.Zero;
133 iRawInput = new RAWINPUT();
134 if (!Win32.RawInput.GetRawInputData(aMessage.LParam, ref iRawInput, ref rawInputBuffer))
136 Debug.WriteLine("GetRawInputData failed!");
140 //Our device can actually be null.
141 //This is notably happening for some keyboard events
142 if (RawInput.header.hDevice != IntPtr.Zero)
144 //Get various information about this HID device
145 Device = new Hid.HidDevice(RawInput.header.hDevice);
148 if (RawInput.header.dwType == Win32.RawInputDeviceType.RIM_TYPEHID) //Check that our raw input is HID
152 Debug.WriteLine("WM_INPUT source device is HID.");
153 //Get Usage Page and Usage
154 //Debug.WriteLine("Usage Page: 0x" + deviceInfo.hid.usUsagePage.ToString("X4") + " Usage ID: 0x" + deviceInfo.hid.usUsage.ToString("X4"));
155 UsagePage = Device.Info.hid.usUsagePage;
156 UsageCollection = Device.Info.hid.usUsage;
158 if (!(RawInput.hid.dwSizeHid > 1 //Make sure our HID msg size more than 1. In fact the first ushort is irrelevant to us for now
159 && RawInput.hid.dwCount > 0)) //Check that we have at least one HID msg
164 //Allocate a buffer for one HID input
165 InputReport = new byte[RawInput.hid.dwSizeHid];
167 Debug.WriteLine("Raw input contains " + RawInput.hid.dwCount + " HID input report(s)");
169 //For each HID input report in our raw input
170 for (int i = 0; i < RawInput.hid.dwCount; i++)
172 //Compute the address from which to copy our HID input
173 int hidInputOffset = 0;
176 byte* source = (byte*)rawInputBuffer;
177 source += sizeof(RAWINPUTHEADER) + sizeof(RAWHID) + (RawInput.hid.dwSizeHid * i);
178 hidInputOffset = (int)source;
181 //Copy HID input into our buffer
182 Marshal.Copy(new IntPtr(hidInputOffset), InputReport, 0, (int)RawInput.hid.dwSizeHid);
184 ProcessInputReport(InputReport);
187 else if (RawInput.header.dwType == RawInputDeviceType.RIM_TYPEMOUSE)
191 Debug.WriteLine("WM_INPUT source device is Mouse.");
192 // do mouse handling...
194 else if (RawInput.header.dwType == RawInputDeviceType.RIM_TYPEKEYBOARD)
198 Debug.WriteLine("WM_INPUT source device is Keyboard.");
199 // do keyboard handling...
202 Debug.WriteLine("Type: " + Device.Info.keyboard.dwType.ToString());
203 Debug.WriteLine("SubType: " + Device.Info.keyboard.dwSubType.ToString());
204 Debug.WriteLine("Mode: " + Device.Info.keyboard.dwKeyboardMode.ToString());
205 Debug.WriteLine("Number of function keys: " + Device.Info.keyboard.dwNumberOfFunctionKeys.ToString());
206 Debug.WriteLine("Number of indicators: " + Device.Info.keyboard.dwNumberOfIndicators.ToString());
207 Debug.WriteLine("Number of keys total: " + Device.Info.keyboard.dwNumberOfKeysTotal.ToString());
213 //Always executed when leaving our try block
214 Marshal.FreeHGlobal(rawInputBuffer);
220 //TODO: Make this optional
221 //StartRepeatTimer(iRepeatDelay);
230 private void ProcessInputReport(byte[] aInputReport)
232 //Print HID input report in our debug output
233 //string hidDump = "HID input report: " + InputReportString();
234 //Debug.WriteLine(hidDump);
236 //Get all our usages, those are typically the buttons currently pushed on a gamepad.
237 //For a remote control it's usually just the one button that was pushed.
238 GetUsages(aInputReport);
240 //Now process direction pad (d-pad, dpad) and axes
241 GetUsageValues(aInputReport);
245 /// Typically fetches values of a joystick/gamepad axis and dpad directions.
247 /// <param name="aInputReport"></param>
248 private void GetUsageValues(byte[] aInputReport)
250 if (Device.InputValueCapabilities == null)
255 foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
259 //What should we do with those guys?
263 //Now fetch and add our usage value
265 Win32.HidStatus status = Win32.Function.HidP_GetUsageValue(Win32.HIDP_REPORT_TYPE.HidP_Input, caps.UsagePage, caps.LinkCollection, caps.NotRange.Usage, ref usageValue, Device.PreParsedData, aInputReport, (uint)aInputReport.Length);
266 if (status == Win32.HidStatus.HIDP_STATUS_SUCCESS)
268 UsageValues[caps]=usageValue;
274 /// Get all our usages, those are typically the buttons currently pushed on a gamepad.
275 /// For a remote control it's usually just the one button that was pushed.
277 private void GetUsages(byte[] aInputReport)
279 //Do proper parsing of our HID report
280 //First query our usage count
282 Win32.USAGE_AND_PAGE[] usages = null;
283 Win32.HidStatus status = Win32.Function.HidP_GetUsagesEx(Win32.HIDP_REPORT_TYPE.HidP_Input, 0, usages, ref usageCount, Device.PreParsedData, aInputReport, (uint)aInputReport.Length);
284 if (status == Win32.HidStatus.HIDP_STATUS_BUFFER_TOO_SMALL)
286 //Allocate a large enough buffer
287 usages = new Win32.USAGE_AND_PAGE[usageCount];
288 //...and fetch our usages
289 status = Win32.Function.HidP_GetUsagesEx(Win32.HIDP_REPORT_TYPE.HidP_Input, 0, usages, ref usageCount, Device.PreParsedData, aInputReport, (uint)aInputReport.Length);
290 if (status != Win32.HidStatus.HIDP_STATUS_SUCCESS)
292 Debug.WriteLine("Second pass could not parse HID data: " + status.ToString());
295 else if (status != Win32.HidStatus.HIDP_STATUS_SUCCESS)
297 Debug.WriteLine("First pass could not parse HID data: " + status.ToString());
300 Debug.WriteLine("Usage count: " + usageCount.ToString());
302 //Copy usages into this event
305 foreach (USAGE_AND_PAGE up in usages)
307 //Debug.WriteLine("UsagePage: 0x" + usages[0].UsagePage.ToString("X4"));
308 //Debug.WriteLine("Usage: 0x" + usages[0].Usage.ToString("X4"));
309 //Add this usage to our list
310 Usages.Add(up.Usage);
318 /// <param name="aUsagePage"></param>
319 /// <param name="Usage"></param>
320 /// <returns></returns>
321 public uint GetUsageValue(ushort aUsagePage, ushort aUsage)
323 foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
327 //What should we do with those guys?
331 //Check if we have a match
332 if (caps.UsagePage == aUsagePage && caps.NotRange.Usage == aUsage)
334 return UsageValues[caps];
344 /// <param name="aUsagePage"></param>
345 /// <param name="aUsage"></param>
346 /// <returns></returns>
347 public int GetValueCapabilitiesIndex(ushort aUsagePage, ushort aUsage)
350 foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
355 //What should we do with those guys?
359 //Check if we have a match
360 if (caps.UsagePage == aUsagePage && caps.NotRange.Usage == aUsage)
371 /// TODO: Move this to another level?
373 /// <param name="aInterval"></param>
374 public void StartRepeatTimer(double aInterval)
380 Timer.Enabled = false;
381 //Initial delay do not use auto reset
382 //After our initial delay however we do setup our timer one more time using auto reset
383 Timer.AutoReset = (RepeatCount != 0);
384 Timer.Interval = aInterval;
385 Timer.Enabled = true;
388 static private void OnRepeatTimerElapsed(object sender, ElapsedEventArgs e, HidEvent aHidEvent)
390 if (aHidEvent.IsStray)
392 //Skip events if canceled
396 aHidEvent.RepeatCount++;
397 aHidEvent.Time = DateTime.Now;
398 if (aHidEvent.RepeatCount == 1)
400 //Re-Start our timer only after the initial delay
401 aHidEvent.StartRepeatTimer(aHidEvent.iRepeatSpeed);
404 //Broadcast our repeat event
405 aHidEvent.OnHidEventRepeat(aHidEvent);
409 /// Provide the state of the dpad or hat switch if any.
410 /// If no dpad is found we return 'at rest'.
412 /// <returns></returns>
413 public DirectionPadState GetDirectionPadState()
415 int index=GetValueCapabilitiesIndex((ushort)Hid.UsagePage.GenericDesktopControls, (ushort)Hid.Usage.GenericDesktop.HatSwitch);
418 //No hat switch found
419 return DirectionPadState.Rest;
422 HIDP_VALUE_CAPS caps=Device.InputValueCapabilities[index];
426 return DirectionPadState.Rest;
429 uint dpadUsageValue = UsageValues[caps];
431 if (dpadUsageValue < caps.LogicalMin || dpadUsageValue > caps.LogicalMax)
433 //Out of range means at rest
434 return DirectionPadState.Rest;
437 //Normalize value to start at zero
438 //TODO: more error check here?
439 DirectionPadState res = (DirectionPadState)((int)dpadUsageValue - caps.LogicalMin);
444 /// Print information about this device to our debug output.
446 public void DebugWrite()
450 Debug.WriteLine("==== Invalid HidEvent");
459 if (IsGeneric) Debug.WriteLine("==== Generic");
460 if (IsKeyboard) Debug.WriteLine("==== Keyboard");
461 if (IsMouse) Debug.WriteLine("==== Mouse");
462 Debug.WriteLine("==== Foreground: " + IsForeground.ToString());
463 Debug.WriteLine("==== UsagePage: 0x" + UsagePage.ToString("X4"));
464 Debug.WriteLine("==== UsageCollection: 0x" + UsageCollection.ToString("X4"));
465 Debug.WriteLine("==== InputReport: 0x" + InputReportString());
466 foreach (ushort usage in Usages)
468 Debug.WriteLine("==== Usage: 0x" + usage.ToString("X4"));
475 /// <returns></returns>
476 public string InputReportString()
478 if (InputReport == null)
484 foreach (byte b in InputReport)
486 hidDump += b.ToString("X2");
493 /// Create a list view item describing this HidEvent
495 /// <returns></returns>
496 public ListViewItem ToListViewItem()
498 string usageText = "";
500 foreach (ushort usage in Usages)
508 UsagePage usagePage = (UsagePage)UsagePage;
511 case Hid.UsagePage.Consumer:
512 usageText += ((Hid.Usage.ConsumerControl)usage).ToString();
515 case Hid.UsagePage.WindowsMediaCenterRemoteControl:
516 usageText += ((Hid.Usage.WindowsMediaCenterRemoteControl)usage).ToString();
520 usageText += usage.ToString("X2");
525 //If we are a gamepad display axis and dpad values
526 if (Device.IsGamePad)
528 //uint dpadUsageValue = GetUsageValue((ushort)Hid.UsagePage.GenericDesktopControls, (ushort)Hid.Usage.GenericDesktop.HatSwitch);
529 //usageText = dpadUsageValue.ToString("X") + " (dpad), " + usageText;
534 usageText += " (Buttons)";
543 usageText += GetDirectionPadState().ToString();
545 foreach (KeyValuePair<HIDP_VALUE_CAPS, uint> entry in UsageValues)
547 if (entry.Key.IsRange)
552 Type usageType = Utils.UsageType((UsagePage)entry.Key.UsagePage);
553 if (usageType == null)
555 //TODO: check why this is happening on Logitech rumble gamepad 2.
556 //Probably some of our axis are hiding in there.
559 string name = Enum.GetName(usageType, entry.Key.NotRange.Usage);
566 usageText += entry.Value.ToString("X") + " ("+ name +")";
570 //Now create our list item
571 ListViewItem item = new ListViewItem(new[] { usageText, InputReportString(), UsagePage.ToString("X2"), UsageCollection.ToString("X2"), RepeatCount.ToString(), Time.ToString("HH:mm:ss:fff") });