HidEvent.cs
author StephaneLenclud
Sun, 15 Mar 2015 12:44:58 +0100
changeset 73 5262f4b7c4ad
parent 72 471b1d45c46a
child 76 831ebeeecfdf
permissions -rw-r--r--
Adding support for usage value and thus joystick/gamepad.
Adding generic support for direction pad.
     1 using System;
     2 using System.Windows.Forms;
     3 using System.Runtime.InteropServices;
     4 using System.Diagnostics;
     5 using System.Text;
     6 using Microsoft.Win32.SafeHandles;
     7 using Win32;
     8 using System.Collections.Generic;
     9 using System.Timers;
    10 
    11 
    12 namespace Hid
    13 {
    14     /// <summary>
    15     /// We provide utility functions to interpret gamepad dpad state.
    16     /// </summary>
    17     public enum DirectionPadState
    18     {
    19         Rest=-1,
    20         Up=0,
    21         UpRight=1,
    22         Right=2,
    23         DownRight=3,
    24         Down=4,
    25         DownLeft=5,
    26         Left=6,
    27         UpLeft=7
    28     }
    29 
    30     /// <summary>
    31     /// Represent a HID event.
    32     /// TODO: Rename this into HidRawInput?
    33     /// </summary>
    34     public class HidEvent : IDisposable
    35     {
    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; }
    41         /// <summary>
    42         /// If this not a mouse or keyboard event then it's a generic HID event.
    43         /// </summary>
    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; }
    49 
    50         public HidDevice Device { get; private set; }
    51         public RAWINPUT RawInput { get {return iRawInput;} } 
    52         private RAWINPUT iRawInput;
    53 
    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; }
    58         /// <summary>
    59         /// Sorted in the same order as Device.InputValueCapabilities.
    60         /// </summary>
    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; }
    64         //
    65         public delegate void HidEventRepeatDelegate(HidEvent aHidEvent);
    66         public event HidEventRepeatDelegate OnHidEventRepeat;
    67 
    68         private System.Timers.Timer Timer { get; set; }
    69         public DateTime Time { get; private set; }
    70         public DateTime OriginalTime { get; private set; }
    71 
    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;
    76 
    77         /// <summary>
    78         /// Tells whether this event has already been disposed of.
    79         /// </summary>
    80         public bool IsStray { get { return Timer == null; } }
    81 
    82         /// <summary>
    83         /// We typically dispose of events as soon as we get the corresponding key up signal.
    84         /// </summary>
    85         public void Dispose()
    86         {
    87             Timer.Enabled = false;
    88             Timer.Dispose();
    89             //Mark this event as a stray
    90             Timer = null;
    91         }
    92 
    93         /// <summary>
    94         /// Initialize an HidEvent from a WM_INPUT message
    95         /// </summary>
    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)
    98         {
    99             RepeatCount = 0;
   100             IsValid = false;
   101             IsKeyboard = false;
   102             IsGeneric = false;
   103 
   104             Time = DateTime.Now;
   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;
   111 
   112             if (aMessage.Msg != Const.WM_INPUT)
   113             {
   114                 //Has to be a WM_INPUT message
   115                 return;
   116             }
   117 
   118             if (Macro.GET_RAWINPUT_CODE_WPARAM(aMessage.WParam) == Const.RIM_INPUT)
   119             {
   120                 IsForeground = true;
   121             }
   122             else if (Macro.GET_RAWINPUT_CODE_WPARAM(aMessage.WParam) == Const.RIM_INPUTSINK)
   123             {
   124                 IsForeground = false;
   125             }
   126 
   127             //Declare some pointers
   128             IntPtr rawInputBuffer = IntPtr.Zero;
   129 
   130             try
   131             {
   132                 //Fetch raw input
   133                 iRawInput = new RAWINPUT();
   134                 if (!Win32.RawInput.GetRawInputData(aMessage.LParam, ref iRawInput, ref rawInputBuffer))
   135                 {
   136                     Debug.WriteLine("GetRawInputData failed!");
   137                     return;
   138                 }
   139 
   140                 //Our device can actually be null.
   141                 //This is notably happening for some keyboard events
   142                 if (RawInput.header.hDevice != IntPtr.Zero)
   143                 {
   144                     //Get various information about this HID device
   145                     Device = new Hid.HidDevice(RawInput.header.hDevice);
   146                 }
   147 
   148                 if (RawInput.header.dwType == Win32.RawInputDeviceType.RIM_TYPEHID)  //Check that our raw input is HID                        
   149                 {
   150                     IsGeneric = true;
   151 
   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;
   157 
   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
   160                     {
   161                         return;
   162                     }
   163 
   164                     //Allocate a buffer for one HID input
   165                     InputReport = new byte[RawInput.hid.dwSizeHid];
   166 
   167                     Debug.WriteLine("Raw input contains " + RawInput.hid.dwCount + " HID input report(s)");
   168 
   169                     //For each HID input report in our raw input
   170                     for (int i = 0; i < RawInput.hid.dwCount; i++)
   171                     {
   172                         //Compute the address from which to copy our HID input
   173                         int hidInputOffset = 0;
   174                         unsafe
   175                         {
   176                             byte* source = (byte*)rawInputBuffer;
   177                             source += sizeof(RAWINPUTHEADER) + sizeof(RAWHID) + (RawInput.hid.dwSizeHid * i);
   178                             hidInputOffset = (int)source;
   179                         }
   180 
   181                         //Copy HID input into our buffer
   182                         Marshal.Copy(new IntPtr(hidInputOffset), InputReport, 0, (int)RawInput.hid.dwSizeHid);
   183                         //
   184                         ProcessInputReport(InputReport);
   185                     }
   186                 }
   187                 else if (RawInput.header.dwType == RawInputDeviceType.RIM_TYPEMOUSE)
   188                 {
   189                     IsMouse = true;
   190 
   191                     Debug.WriteLine("WM_INPUT source device is Mouse.");
   192                     // do mouse handling...
   193                 }
   194                 else if (RawInput.header.dwType == RawInputDeviceType.RIM_TYPEKEYBOARD)
   195                 {
   196                     IsKeyboard = true;
   197 
   198                     Debug.WriteLine("WM_INPUT source device is Keyboard.");
   199                     // do keyboard handling...
   200                     if (Device != null)
   201                     {
   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());
   208                     }
   209                 }
   210             }
   211             finally
   212             {
   213                 //Always executed when leaving our try block
   214                 Marshal.FreeHGlobal(rawInputBuffer);
   215             }
   216 
   217             //
   218             if (IsButtonDown)
   219             {
   220                 //TODO: Make this optional
   221                 //StartRepeatTimer(iRepeatDelay);
   222             }
   223 
   224             IsValid = true;
   225         }
   226 
   227         /// <summary>
   228         /// 
   229         /// </summary>
   230         private void ProcessInputReport(byte[] aInputReport)
   231         {
   232             //Print HID input report in our debug output
   233             //string hidDump = "HID input report: " + InputReportString();
   234             //Debug.WriteLine(hidDump);
   235 
   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);
   239 
   240             //Now process direction pad (d-pad, dpad) and axes
   241             GetUsageValues(aInputReport);
   242         }
   243 
   244         /// <summary>
   245         /// Typically fetches values of a joystick/gamepad axis and dpad directions.
   246         /// </summary>
   247         /// <param name="aInputReport"></param>
   248         private void GetUsageValues(byte[] aInputReport)
   249         {
   250             if (Device.InputValueCapabilities == null)
   251             {
   252                 return;
   253             }
   254 
   255             foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
   256             {                
   257                 if (caps.IsRange)
   258                 {
   259                     //What should we do with those guys?
   260                     continue;
   261                 }
   262 
   263                 //Now fetch and add our usage value
   264                 uint usageValue = 0;
   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)
   267                 {
   268                     UsageValues[caps]=usageValue;
   269                 }
   270             }
   271         }
   272 
   273         /// <summary>
   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.
   276         /// </summary>
   277         private void GetUsages(byte[] aInputReport)
   278         {
   279             //Do proper parsing of our HID report
   280             //First query our usage count
   281             uint usageCount = 0;
   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)
   285             {
   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)
   291                 {
   292                     Debug.WriteLine("Second pass could not parse HID data: " + status.ToString());
   293                 }
   294             }
   295             else if (status != Win32.HidStatus.HIDP_STATUS_SUCCESS)
   296             {
   297                 Debug.WriteLine("First pass could not parse HID data: " + status.ToString());
   298             }
   299 
   300             Debug.WriteLine("Usage count: " + usageCount.ToString());
   301 
   302             //Copy usages into this event
   303             if (usages != null)
   304             {
   305                 foreach (USAGE_AND_PAGE up in usages)
   306                 {
   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);
   311                 }
   312             }
   313         }
   314 
   315         /// <summary>
   316         /// 
   317         /// </summary>
   318         /// <param name="aUsagePage"></param>
   319         /// <param name="Usage"></param>
   320         /// <returns></returns>
   321         public uint GetUsageValue(ushort aUsagePage, ushort aUsage)
   322         {
   323             foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
   324             {                
   325                 if (caps.IsRange)
   326                 {
   327                     //What should we do with those guys?
   328                     continue;
   329                 }
   330 
   331                 //Check if we have a match
   332                 if (caps.UsagePage == aUsagePage && caps.NotRange.Usage == aUsage)
   333                 {
   334                     return UsageValues[caps];
   335                 }
   336             }
   337 
   338             return 0;
   339         }
   340 
   341         /// <summary>
   342         /// 
   343         /// </summary>
   344         /// <param name="aUsagePage"></param>
   345         /// <param name="aUsage"></param>
   346         /// <returns></returns>
   347         public int GetValueCapabilitiesIndex(ushort aUsagePage, ushort aUsage)
   348         {
   349             int i = -1;
   350             foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
   351             {
   352                 i++;
   353                 if (caps.IsRange)
   354                 {
   355                     //What should we do with those guys?
   356                     continue;
   357                 }
   358 
   359                 //Check if we have a match
   360                 if (caps.UsagePage == aUsagePage && caps.NotRange.Usage == aUsage)
   361                 {
   362                     return i;
   363                 }
   364             }
   365 
   366             return i;
   367         }        
   368 
   369 
   370         /// <summary>
   371         /// TODO: Move this to another level?
   372         /// </summary>
   373         /// <param name="aInterval"></param>
   374         public void StartRepeatTimer(double aInterval)
   375         {
   376             if (Timer == null)
   377             {
   378                 return;
   379             }
   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;
   386         }
   387 
   388         static private void OnRepeatTimerElapsed(object sender, ElapsedEventArgs e, HidEvent aHidEvent)
   389         {
   390             if (aHidEvent.IsStray)
   391             {
   392                 //Skip events if canceled
   393                 return;
   394             }
   395 
   396             aHidEvent.RepeatCount++;
   397             aHidEvent.Time = DateTime.Now;
   398             if (aHidEvent.RepeatCount == 1)
   399             {
   400                 //Re-Start our timer only after the initial delay 
   401                 aHidEvent.StartRepeatTimer(aHidEvent.iRepeatSpeed);
   402             }
   403 
   404             //Broadcast our repeat event
   405             aHidEvent.OnHidEventRepeat(aHidEvent);
   406         }
   407 
   408         /// <summary>
   409         /// Provide the state of the dpad or hat switch if any.
   410         /// If no dpad is found we return 'at rest'.
   411         /// </summary>
   412         /// <returns></returns>
   413         public DirectionPadState GetDirectionPadState()
   414         {
   415             int index=GetValueCapabilitiesIndex((ushort)Hid.UsagePage.GenericDesktopControls, (ushort)Hid.Usage.GenericDesktop.HatSwitch);
   416             if (index < 0)
   417             {
   418                 //No hat switch found
   419                 return DirectionPadState.Rest;
   420             }
   421 
   422             HIDP_VALUE_CAPS caps=Device.InputValueCapabilities[index];
   423             if (caps.IsRange)
   424             {
   425                 //Defensive
   426                 return DirectionPadState.Rest;
   427             }
   428 
   429             uint dpadUsageValue = UsageValues[caps]; 
   430 
   431             if (dpadUsageValue < caps.LogicalMin || dpadUsageValue > caps.LogicalMax)
   432             {
   433                 //Out of range means at rest
   434                 return DirectionPadState.Rest;
   435             }
   436 
   437             //Normalize value to start at zero
   438             //TODO: more error check here?
   439             DirectionPadState res = (DirectionPadState)((int)dpadUsageValue - caps.LogicalMin);            
   440             return res;
   441         }
   442 
   443         /// <summary>
   444         /// Print information about this device to our debug output.
   445         /// </summary>
   446         public void DebugWrite()
   447         {
   448             if (!IsValid)
   449             {
   450                 Debug.WriteLine("==== Invalid HidEvent");
   451                 return;
   452             }
   453 
   454             if (Device!=null)
   455             {
   456                 Device.DebugWrite();
   457             }
   458             
   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)
   467             {
   468                 Debug.WriteLine("==== Usage: 0x" + usage.ToString("X4"));
   469             }
   470         }
   471 
   472         /// <summary>
   473         /// 
   474         /// </summary>
   475         /// <returns></returns>
   476         public string InputReportString()
   477         {
   478             if (InputReport == null)
   479             {
   480                 return "null";
   481             }
   482 
   483             string hidDump = "";
   484             foreach (byte b in InputReport)
   485             {
   486                 hidDump += b.ToString("X2");
   487             }
   488             return hidDump;
   489         }
   490 
   491 
   492         /// <summary>
   493         /// Create a list view item describing this HidEvent
   494         /// </summary>
   495         /// <returns></returns>
   496         public ListViewItem ToListViewItem()
   497         {
   498             string usageText = "";
   499 
   500             foreach (ushort usage in Usages)
   501             {
   502                 if (usageText != "")
   503                 {
   504                     //Add a separator
   505                     usageText += ", ";
   506                 }
   507 
   508                 UsagePage usagePage = (UsagePage)UsagePage;
   509                 switch (usagePage)
   510                 {
   511                     case Hid.UsagePage.Consumer:
   512                         usageText += ((Hid.Usage.ConsumerControl)usage).ToString();
   513                         break;
   514 
   515                     case Hid.UsagePage.WindowsMediaCenterRemoteControl:
   516                         usageText += ((Hid.Usage.WindowsMediaCenterRemoteControl)usage).ToString();
   517                         break;
   518 
   519                     default:
   520                         usageText += usage.ToString("X2");
   521                         break;
   522                 }
   523             }
   524 
   525             //If we are a gamepad display axis and dpad values
   526             if (Device.IsGamePad)
   527             {
   528                 //uint dpadUsageValue = GetUsageValue((ushort)Hid.UsagePage.GenericDesktopControls, (ushort)Hid.Usage.GenericDesktop.HatSwitch);
   529                 //usageText = dpadUsageValue.ToString("X") + " (dpad), " + usageText;
   530               
   531                 if (usageText != "")
   532                 {
   533                     //Add a separator
   534                     usageText += " (Buttons)";
   535                 }
   536 
   537                 if (usageText != "")
   538                 {
   539                     //Add a separator
   540                     usageText += ", ";
   541                 }
   542 
   543                 usageText += GetDirectionPadState().ToString();
   544 
   545                 foreach (KeyValuePair<HIDP_VALUE_CAPS, uint> entry in UsageValues)
   546                 {
   547                     if (entry.Key.IsRange)
   548                     {
   549                         continue;
   550                     }
   551 
   552                     Type usageType = Utils.UsageType((UsagePage)entry.Key.UsagePage);
   553                     if (usageType == null)
   554                     {
   555                         //TODO: check why this is happening on Logitech rumble gamepad 2.
   556                         //Probably some of our axis are hiding in there.
   557                         continue;
   558                     }
   559                     string name = Enum.GetName(usageType, entry.Key.NotRange.Usage);
   560 
   561                     if (usageText != "")
   562                     {
   563                         //Add a separator
   564                         usageText += ", ";
   565                     }
   566                     usageText += entry.Value.ToString("X") + " ("+ name +")";        
   567                 }
   568             }
   569 
   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") });
   572             return item;
   573         }
   574 
   575     }
   576 
   577 }