HidEvent.cs
author StephaneLenclud
Sun, 15 Mar 2015 14:39:21 +0100
changeset 76 831ebeeecfdf
parent 73 5262f4b7c4ad
child 77 fb9ea5ad8c2d
permissions -rw-r--r--
Copyright and namespace.
     1 //
     2 // Copyright (C) 2014-2015 Stéphane Lenclud.
     3 //
     4 // This file is part of SharpLibHid.
     5 //
     6 // SharpDisplayManager is free software: you can redistribute it and/or modify
     7 // it under the terms of the GNU General Public License as published by
     8 // the Free Software Foundation, either version 3 of the License, or
     9 // (at your option) any later version.
    10 //
    11 // SharpDisplayManager is distributed in the hope that it will be useful,
    12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
    13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14 // GNU General Public License for more details.
    15 //
    16 // You should have received a copy of the GNU General Public License
    17 // along with SharpDisplayManager.  If not, see <http://www.gnu.org/licenses/>.
    18 //
    19 
    20 
    21 using System;
    22 using System.Windows.Forms;
    23 using System.Runtime.InteropServices;
    24 using System.Diagnostics;
    25 using System.Text;
    26 using Microsoft.Win32.SafeHandles;
    27 using Win32;
    28 using System.Collections.Generic;
    29 using System.Timers;
    30 using SharpLibHid.Usage;
    31 
    32 
    33 namespace SharpLibHid
    34 {
    35     /// <summary>
    36     /// We provide utility functions to interpret gamepad dpad state.
    37     /// </summary>
    38     public enum DirectionPadState
    39     {
    40         Rest=-1,
    41         Up=0,
    42         UpRight=1,
    43         Right=2,
    44         DownRight=3,
    45         Down=4,
    46         DownLeft=5,
    47         Left=6,
    48         UpLeft=7
    49     }
    50 
    51     /// <summary>
    52     /// Represent a HID event.
    53     /// TODO: Rename this into HidRawInput?
    54     /// </summary>
    55     public class HidEvent : IDisposable
    56     {
    57         public bool IsValid { get; private set; }
    58         public bool IsForeground { get; private set; }
    59         public bool IsBackground { get { return !IsForeground; } }
    60         public bool IsMouse { get; private set; }
    61         public bool IsKeyboard { get; private set; }
    62         /// <summary>
    63         /// If this not a mouse or keyboard event then it's a generic HID event.
    64         /// </summary>
    65         public bool IsGeneric { get; private set; }
    66         public bool IsButtonDown { get { return Usages.Count == 1 && Usages[0] != 0; } }
    67         public bool IsButtonUp { get { return Usages.Count == 0; } }
    68         public bool IsRepeat { get { return RepeatCount != 0; } }
    69         public uint RepeatCount { get; private set; }
    70 
    71         public HidDevice Device { get; private set; }
    72         public RAWINPUT RawInput { get {return iRawInput;} } 
    73         private RAWINPUT iRawInput;
    74 
    75         public ushort UsagePage { get; private set; }
    76         public ushort UsageCollection { get; private set; }
    77         public uint UsageId { get { return ((uint)UsagePage << 16 | (uint)UsageCollection); } }
    78         public List<ushort> Usages { get; private set; }
    79         /// <summary>
    80         /// Sorted in the same order as Device.InputValueCapabilities.
    81         /// </summary>
    82         public Dictionary<HIDP_VALUE_CAPS,uint> UsageValues { get; private set; }
    83         //TODO: We need a collection of input report
    84         public byte[] InputReport { get; private set; }
    85         //
    86         public delegate void HidEventRepeatDelegate(HidEvent aHidEvent);
    87         public event HidEventRepeatDelegate OnHidEventRepeat;
    88 
    89         private System.Timers.Timer Timer { get; set; }
    90         public DateTime Time { get; private set; }
    91         public DateTime OriginalTime { get; private set; }
    92 
    93         //Compute repeat delay and speed based on system settings
    94         //Those computations were taken from the Petzold here: ftp://ftp.charlespetzold.com/ProgWinForms/4%20Custom%20Controls/NumericScan/NumericScan/ClickmaticButton.cs
    95         private int iRepeatDelay = 250 * (1 + SystemInformation.KeyboardDelay);
    96         private int iRepeatSpeed = 405 - 12 * SystemInformation.KeyboardSpeed;
    97 
    98         /// <summary>
    99         /// Tells whether this event has already been disposed of.
   100         /// </summary>
   101         public bool IsStray { get { return Timer == null; } }
   102 
   103         /// <summary>
   104         /// We typically dispose of events as soon as we get the corresponding key up signal.
   105         /// </summary>
   106         public void Dispose()
   107         {
   108             Timer.Enabled = false;
   109             Timer.Dispose();
   110             //Mark this event as a stray
   111             Timer = null;
   112         }
   113 
   114         /// <summary>
   115         /// Initialize an HidEvent from a WM_INPUT message
   116         /// </summary>
   117         /// <param name="hRawInputDevice">Device Handle as provided by RAWINPUTHEADER.hDevice, typically accessed as rawinput.header.hDevice</param>
   118         public HidEvent(Message aMessage, HidEventRepeatDelegate aRepeatDelegate)
   119         {
   120             RepeatCount = 0;
   121             IsValid = false;
   122             IsKeyboard = false;
   123             IsGeneric = false;
   124 
   125             Time = DateTime.Now;
   126             OriginalTime = DateTime.Now;
   127             Timer = new System.Timers.Timer();
   128             Timer.Elapsed += (sender, e) => OnRepeatTimerElapsed(sender, e, this);
   129             Usages = new List<ushort>();
   130             UsageValues = new Dictionary<HIDP_VALUE_CAPS,uint>();
   131             OnHidEventRepeat += aRepeatDelegate;
   132 
   133             if (aMessage.Msg != Const.WM_INPUT)
   134             {
   135                 //Has to be a WM_INPUT message
   136                 return;
   137             }
   138 
   139             if (Macro.GET_RAWINPUT_CODE_WPARAM(aMessage.WParam) == Const.RIM_INPUT)
   140             {
   141                 IsForeground = true;
   142             }
   143             else if (Macro.GET_RAWINPUT_CODE_WPARAM(aMessage.WParam) == Const.RIM_INPUTSINK)
   144             {
   145                 IsForeground = false;
   146             }
   147 
   148             //Declare some pointers
   149             IntPtr rawInputBuffer = IntPtr.Zero;
   150 
   151             try
   152             {
   153                 //Fetch raw input
   154                 iRawInput = new RAWINPUT();
   155                 if (!Win32.RawInput.GetRawInputData(aMessage.LParam, ref iRawInput, ref rawInputBuffer))
   156                 {
   157                     Debug.WriteLine("GetRawInputData failed!");
   158                     return;
   159                 }
   160 
   161                 //Our device can actually be null.
   162                 //This is notably happening for some keyboard events
   163                 if (RawInput.header.hDevice != IntPtr.Zero)
   164                 {
   165                     //Get various information about this HID device
   166                     Device = new HidDevice(RawInput.header.hDevice);
   167                 }
   168 
   169                 if (RawInput.header.dwType == Win32.RawInputDeviceType.RIM_TYPEHID)  //Check that our raw input is HID                        
   170                 {
   171                     IsGeneric = true;
   172 
   173                     Debug.WriteLine("WM_INPUT source device is HID.");
   174                     //Get Usage Page and Usage
   175                     //Debug.WriteLine("Usage Page: 0x" + deviceInfo.hid.usUsagePage.ToString("X4") + " Usage ID: 0x" + deviceInfo.hid.usUsage.ToString("X4"));
   176                     UsagePage = Device.Info.hid.usUsagePage;
   177                     UsageCollection = Device.Info.hid.usUsage;
   178 
   179                     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
   180                         && RawInput.hid.dwCount > 0))    //Check that we have at least one HID msg
   181                     {
   182                         return;
   183                     }
   184 
   185                     //Allocate a buffer for one HID input
   186                     InputReport = new byte[RawInput.hid.dwSizeHid];
   187 
   188                     Debug.WriteLine("Raw input contains " + RawInput.hid.dwCount + " HID input report(s)");
   189 
   190                     //For each HID input report in our raw input
   191                     for (int i = 0; i < RawInput.hid.dwCount; i++)
   192                     {
   193                         //Compute the address from which to copy our HID input
   194                         int hidInputOffset = 0;
   195                         unsafe
   196                         {
   197                             byte* source = (byte*)rawInputBuffer;
   198                             source += sizeof(RAWINPUTHEADER) + sizeof(RAWHID) + (RawInput.hid.dwSizeHid * i);
   199                             hidInputOffset = (int)source;
   200                         }
   201 
   202                         //Copy HID input into our buffer
   203                         Marshal.Copy(new IntPtr(hidInputOffset), InputReport, 0, (int)RawInput.hid.dwSizeHid);
   204                         //
   205                         ProcessInputReport(InputReport);
   206                     }
   207                 }
   208                 else if (RawInput.header.dwType == RawInputDeviceType.RIM_TYPEMOUSE)
   209                 {
   210                     IsMouse = true;
   211 
   212                     Debug.WriteLine("WM_INPUT source device is Mouse.");
   213                     // do mouse handling...
   214                 }
   215                 else if (RawInput.header.dwType == RawInputDeviceType.RIM_TYPEKEYBOARD)
   216                 {
   217                     IsKeyboard = true;
   218 
   219                     Debug.WriteLine("WM_INPUT source device is Keyboard.");
   220                     // do keyboard handling...
   221                     if (Device != null)
   222                     {
   223                         Debug.WriteLine("Type: " + Device.Info.keyboard.dwType.ToString());
   224                         Debug.WriteLine("SubType: " + Device.Info.keyboard.dwSubType.ToString());
   225                         Debug.WriteLine("Mode: " + Device.Info.keyboard.dwKeyboardMode.ToString());
   226                         Debug.WriteLine("Number of function keys: " + Device.Info.keyboard.dwNumberOfFunctionKeys.ToString());
   227                         Debug.WriteLine("Number of indicators: " + Device.Info.keyboard.dwNumberOfIndicators.ToString());
   228                         Debug.WriteLine("Number of keys total: " + Device.Info.keyboard.dwNumberOfKeysTotal.ToString());
   229                     }
   230                 }
   231             }
   232             finally
   233             {
   234                 //Always executed when leaving our try block
   235                 Marshal.FreeHGlobal(rawInputBuffer);
   236             }
   237 
   238             //
   239             if (IsButtonDown)
   240             {
   241                 //TODO: Make this optional
   242                 //StartRepeatTimer(iRepeatDelay);
   243             }
   244 
   245             IsValid = true;
   246         }
   247 
   248         /// <summary>
   249         /// 
   250         /// </summary>
   251         private void ProcessInputReport(byte[] aInputReport)
   252         {
   253             //Print HID input report in our debug output
   254             //string hidDump = "HID input report: " + InputReportString();
   255             //Debug.WriteLine(hidDump);
   256 
   257             //Get all our usages, those are typically the buttons currently pushed on a gamepad.
   258             //For a remote control it's usually just the one button that was pushed.
   259             GetUsages(aInputReport);
   260 
   261             //Now process direction pad (d-pad, dpad) and axes
   262             GetUsageValues(aInputReport);
   263         }
   264 
   265         /// <summary>
   266         /// Typically fetches values of a joystick/gamepad axis and dpad directions.
   267         /// </summary>
   268         /// <param name="aInputReport"></param>
   269         private void GetUsageValues(byte[] aInputReport)
   270         {
   271             if (Device.InputValueCapabilities == null)
   272             {
   273                 return;
   274             }
   275 
   276             foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
   277             {                
   278                 if (caps.IsRange)
   279                 {
   280                     //What should we do with those guys?
   281                     continue;
   282                 }
   283 
   284                 //Now fetch and add our usage value
   285                 uint usageValue = 0;
   286                 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);
   287                 if (status == Win32.HidStatus.HIDP_STATUS_SUCCESS)
   288                 {
   289                     UsageValues[caps]=usageValue;
   290                 }
   291             }
   292         }
   293 
   294         /// <summary>
   295         /// Get all our usages, those are typically the buttons currently pushed on a gamepad.
   296         /// For a remote control it's usually just the one button that was pushed.
   297         /// </summary>
   298         private void GetUsages(byte[] aInputReport)
   299         {
   300             //Do proper parsing of our HID report
   301             //First query our usage count
   302             uint usageCount = 0;
   303             Win32.USAGE_AND_PAGE[] usages = null;
   304             Win32.HidStatus status = Win32.Function.HidP_GetUsagesEx(Win32.HIDP_REPORT_TYPE.HidP_Input, 0, usages, ref usageCount, Device.PreParsedData, aInputReport, (uint)aInputReport.Length);
   305             if (status == Win32.HidStatus.HIDP_STATUS_BUFFER_TOO_SMALL)
   306             {
   307                 //Allocate a large enough buffer 
   308                 usages = new Win32.USAGE_AND_PAGE[usageCount];
   309                 //...and fetch our usages
   310                 status = Win32.Function.HidP_GetUsagesEx(Win32.HIDP_REPORT_TYPE.HidP_Input, 0, usages, ref usageCount, Device.PreParsedData, aInputReport, (uint)aInputReport.Length);
   311                 if (status != Win32.HidStatus.HIDP_STATUS_SUCCESS)
   312                 {
   313                     Debug.WriteLine("Second pass could not parse HID data: " + status.ToString());
   314                 }
   315             }
   316             else if (status != Win32.HidStatus.HIDP_STATUS_SUCCESS)
   317             {
   318                 Debug.WriteLine("First pass could not parse HID data: " + status.ToString());
   319             }
   320 
   321             Debug.WriteLine("Usage count: " + usageCount.ToString());
   322 
   323             //Copy usages into this event
   324             if (usages != null)
   325             {
   326                 foreach (USAGE_AND_PAGE up in usages)
   327                 {
   328                     //Debug.WriteLine("UsagePage: 0x" + usages[0].UsagePage.ToString("X4"));
   329                     //Debug.WriteLine("Usage: 0x" + usages[0].Usage.ToString("X4"));
   330                     //Add this usage to our list
   331                     Usages.Add(up.Usage);
   332                 }
   333             }
   334         }
   335 
   336         /// <summary>
   337         /// 
   338         /// </summary>
   339         /// <param name="aUsagePage"></param>
   340         /// <param name="Usage"></param>
   341         /// <returns></returns>
   342         public uint GetUsageValue(ushort aUsagePage, ushort aUsage)
   343         {
   344             foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
   345             {                
   346                 if (caps.IsRange)
   347                 {
   348                     //What should we do with those guys?
   349                     continue;
   350                 }
   351 
   352                 //Check if we have a match
   353                 if (caps.UsagePage == aUsagePage && caps.NotRange.Usage == aUsage)
   354                 {
   355                     return UsageValues[caps];
   356                 }
   357             }
   358 
   359             return 0;
   360         }
   361 
   362         /// <summary>
   363         /// 
   364         /// </summary>
   365         /// <param name="aUsagePage"></param>
   366         /// <param name="aUsage"></param>
   367         /// <returns></returns>
   368         public int GetValueCapabilitiesIndex(ushort aUsagePage, ushort aUsage)
   369         {
   370             int i = -1;
   371             foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
   372             {
   373                 i++;
   374                 if (caps.IsRange)
   375                 {
   376                     //What should we do with those guys?
   377                     continue;
   378                 }
   379 
   380                 //Check if we have a match
   381                 if (caps.UsagePage == aUsagePage && caps.NotRange.Usage == aUsage)
   382                 {
   383                     return i;
   384                 }
   385             }
   386 
   387             return i;
   388         }        
   389 
   390 
   391         /// <summary>
   392         /// TODO: Move this to another level?
   393         /// </summary>
   394         /// <param name="aInterval"></param>
   395         public void StartRepeatTimer(double aInterval)
   396         {
   397             if (Timer == null)
   398             {
   399                 return;
   400             }
   401             Timer.Enabled = false;
   402             //Initial delay do not use auto reset
   403             //After our initial delay however we do setup our timer one more time using auto reset
   404             Timer.AutoReset = (RepeatCount != 0);
   405             Timer.Interval = aInterval;
   406             Timer.Enabled = true;
   407         }
   408 
   409         static private void OnRepeatTimerElapsed(object sender, ElapsedEventArgs e, HidEvent aHidEvent)
   410         {
   411             if (aHidEvent.IsStray)
   412             {
   413                 //Skip events if canceled
   414                 return;
   415             }
   416 
   417             aHidEvent.RepeatCount++;
   418             aHidEvent.Time = DateTime.Now;
   419             if (aHidEvent.RepeatCount == 1)
   420             {
   421                 //Re-Start our timer only after the initial delay 
   422                 aHidEvent.StartRepeatTimer(aHidEvent.iRepeatSpeed);
   423             }
   424 
   425             //Broadcast our repeat event
   426             aHidEvent.OnHidEventRepeat(aHidEvent);
   427         }
   428 
   429         /// <summary>
   430         /// Provide the state of the dpad or hat switch if any.
   431         /// If no dpad is found we return 'at rest'.
   432         /// </summary>
   433         /// <returns></returns>
   434         public DirectionPadState GetDirectionPadState()
   435         {
   436             int index=GetValueCapabilitiesIndex((ushort)SharpLibHid.UsagePage.GenericDesktopControls, (ushort)GenericDesktop.HatSwitch);
   437             if (index < 0)
   438             {
   439                 //No hat switch found
   440                 return DirectionPadState.Rest;
   441             }
   442 
   443             HIDP_VALUE_CAPS caps=Device.InputValueCapabilities[index];
   444             if (caps.IsRange)
   445             {
   446                 //Defensive
   447                 return DirectionPadState.Rest;
   448             }
   449 
   450             uint dpadUsageValue = UsageValues[caps]; 
   451 
   452             if (dpadUsageValue < caps.LogicalMin || dpadUsageValue > caps.LogicalMax)
   453             {
   454                 //Out of range means at rest
   455                 return DirectionPadState.Rest;
   456             }
   457 
   458             //Normalize value to start at zero
   459             //TODO: more error check here?
   460             DirectionPadState res = (DirectionPadState)((int)dpadUsageValue - caps.LogicalMin);            
   461             return res;
   462         }
   463 
   464         /// <summary>
   465         /// Print information about this device to our debug output.
   466         /// </summary>
   467         public void DebugWrite()
   468         {
   469             if (!IsValid)
   470             {
   471                 Debug.WriteLine("==== Invalid HidEvent");
   472                 return;
   473             }
   474 
   475             if (Device!=null)
   476             {
   477                 Device.DebugWrite();
   478             }
   479             
   480             if (IsGeneric) Debug.WriteLine("==== Generic");
   481             if (IsKeyboard) Debug.WriteLine("==== Keyboard");
   482             if (IsMouse) Debug.WriteLine("==== Mouse");
   483             Debug.WriteLine("==== Foreground: " + IsForeground.ToString());
   484             Debug.WriteLine("==== UsagePage: 0x" + UsagePage.ToString("X4"));
   485             Debug.WriteLine("==== UsageCollection: 0x" + UsageCollection.ToString("X4"));
   486             Debug.WriteLine("==== InputReport: 0x" + InputReportString());
   487             foreach (ushort usage in Usages)
   488             {
   489                 Debug.WriteLine("==== Usage: 0x" + usage.ToString("X4"));
   490             }
   491         }
   492 
   493         /// <summary>
   494         /// 
   495         /// </summary>
   496         /// <returns></returns>
   497         public string InputReportString()
   498         {
   499             if (InputReport == null)
   500             {
   501                 return "null";
   502             }
   503 
   504             string hidDump = "";
   505             foreach (byte b in InputReport)
   506             {
   507                 hidDump += b.ToString("X2");
   508             }
   509             return hidDump;
   510         }
   511 
   512 
   513         /// <summary>
   514         /// Create a list view item describing this HidEvent
   515         /// </summary>
   516         /// <returns></returns>
   517         public ListViewItem ToListViewItem()
   518         {
   519             string usageText = "";
   520 
   521             foreach (ushort usage in Usages)
   522             {
   523                 if (usageText != "")
   524                 {
   525                     //Add a separator
   526                     usageText += ", ";
   527                 }
   528 
   529                 UsagePage usagePage = (UsagePage)UsagePage;
   530                 switch (usagePage)
   531                 {
   532                     case SharpLibHid.UsagePage.Consumer:
   533                         usageText += ((ConsumerControl)usage).ToString();
   534                         break;
   535 
   536                     case SharpLibHid.UsagePage.WindowsMediaCenterRemoteControl:
   537                         usageText += ((WindowsMediaCenterRemoteControl)usage).ToString();
   538                         break;
   539 
   540                     default:
   541                         usageText += usage.ToString("X2");
   542                         break;
   543                 }
   544             }
   545 
   546             //If we are a gamepad display axis and dpad values
   547             if (Device.IsGamePad)
   548             {
   549                 //uint dpadUsageValue = GetUsageValue((ushort)Hid.UsagePage.GenericDesktopControls, (ushort)Hid.Usage.GenericDesktop.HatSwitch);
   550                 //usageText = dpadUsageValue.ToString("X") + " (dpad), " + usageText;
   551               
   552                 if (usageText != "")
   553                 {
   554                     //Add a separator
   555                     usageText += " (Buttons)";
   556                 }
   557 
   558                 if (usageText != "")
   559                 {
   560                     //Add a separator
   561                     usageText += ", ";
   562                 }
   563 
   564                 usageText += GetDirectionPadState().ToString();
   565 
   566                 foreach (KeyValuePair<HIDP_VALUE_CAPS, uint> entry in UsageValues)
   567                 {
   568                     if (entry.Key.IsRange)
   569                     {
   570                         continue;
   571                     }
   572 
   573                     Type usageType = Utils.UsageType((UsagePage)entry.Key.UsagePage);
   574                     if (usageType == null)
   575                     {
   576                         //TODO: check why this is happening on Logitech rumble gamepad 2.
   577                         //Probably some of our axis are hiding in there.
   578                         continue;
   579                     }
   580                     string name = Enum.GetName(usageType, entry.Key.NotRange.Usage);
   581 
   582                     if (usageText != "")
   583                     {
   584                         //Add a separator
   585                         usageText += ", ";
   586                     }
   587                     usageText += entry.Value.ToString("X") + " ("+ name +")";        
   588                 }
   589             }
   590 
   591             //Now create our list item
   592             ListViewItem item = new ListViewItem(new[] { usageText, InputReportString(), UsagePage.ToString("X2"), UsageCollection.ToString("X2"), RepeatCount.ToString(), Time.ToString("HH:mm:ss:fff") });
   593             return item;
   594         }
   595 
   596     }
   597 
   598 }