diff -r 72885c950813 -r cdc5f8f1b79e Hid/HidEvent.cs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Hid/HidEvent.cs Sun Mar 15 20:25:58 2015 +0100 @@ -0,0 +1,598 @@ +// +// Copyright (C) 2014-2015 Stéphane Lenclud. +// +// This file is part of SharpLibHid. +// +// SharpDisplayManager is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// SharpDisplayManager is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with SharpDisplayManager. If not, see . +// + + +using System; +using System.Windows.Forms; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Text; +using Microsoft.Win32.SafeHandles; +using SharpLib.Win32; +using System.Collections.Generic; +using System.Timers; +using SharpLib.Hid.Usage; + + +namespace SharpLib.Hid +{ + /// + /// We provide utility functions to interpret gamepad dpad state. + /// + public enum DirectionPadState + { + Rest=-1, + Up=0, + UpRight=1, + Right=2, + DownRight=3, + Down=4, + DownLeft=5, + Left=6, + UpLeft=7 + } + + /// + /// Represent a HID event. + /// TODO: Rename this into HidRawInput? + /// + public class HidEvent : IDisposable + { + public bool IsValid { get; private set; } + public bool IsForeground { get; private set; } + public bool IsBackground { get { return !IsForeground; } } + public bool IsMouse { get; private set; } + public bool IsKeyboard { get; private set; } + /// + /// If this not a mouse or keyboard event then it's a generic HID event. + /// + public bool IsGeneric { get; private set; } + public bool IsButtonDown { get { return Usages.Count == 1 && Usages[0] != 0; } } + public bool IsButtonUp { get { return Usages.Count == 0; } } + public bool IsRepeat { get { return RepeatCount != 0; } } + public uint RepeatCount { get; private set; } + + public HidDevice Device { get; private set; } + public RAWINPUT RawInput { get {return iRawInput;} } + private RAWINPUT iRawInput; + + public ushort UsagePage { get; private set; } + public ushort UsageCollection { get; private set; } + public uint UsageId { get { return ((uint)UsagePage << 16 | (uint)UsageCollection); } } + public List Usages { get; private set; } + /// + /// Sorted in the same order as Device.InputValueCapabilities. + /// + public Dictionary UsageValues { get; private set; } + //TODO: We need a collection of input report + public byte[] InputReport { get; private set; } + // + public delegate void HidEventRepeatDelegate(HidEvent aHidEvent); + public event HidEventRepeatDelegate OnHidEventRepeat; + + private System.Timers.Timer Timer { get; set; } + public DateTime Time { get; private set; } + public DateTime OriginalTime { get; private set; } + + //Compute repeat delay and speed based on system settings + //Those computations were taken from the Petzold here: ftp://ftp.charlespetzold.com/ProgWinForms/4%20Custom%20Controls/NumericScan/NumericScan/ClickmaticButton.cs + private int iRepeatDelay = 250 * (1 + SystemInformation.KeyboardDelay); + private int iRepeatSpeed = 405 - 12 * SystemInformation.KeyboardSpeed; + + /// + /// Tells whether this event has already been disposed of. + /// + public bool IsStray { get { return Timer == null; } } + + /// + /// We typically dispose of events as soon as we get the corresponding key up signal. + /// + public void Dispose() + { + Timer.Enabled = false; + Timer.Dispose(); + //Mark this event as a stray + Timer = null; + } + + /// + /// Initialize an HidEvent from a WM_INPUT message + /// + /// Device Handle as provided by RAWINPUTHEADER.hDevice, typically accessed as rawinput.header.hDevice + public HidEvent(Message aMessage, HidEventRepeatDelegate aRepeatDelegate) + { + RepeatCount = 0; + IsValid = false; + IsKeyboard = false; + IsGeneric = false; + + Time = DateTime.Now; + OriginalTime = DateTime.Now; + Timer = new System.Timers.Timer(); + Timer.Elapsed += (sender, e) => OnRepeatTimerElapsed(sender, e, this); + Usages = new List(); + UsageValues = new Dictionary(); + OnHidEventRepeat += aRepeatDelegate; + + if (aMessage.Msg != Const.WM_INPUT) + { + //Has to be a WM_INPUT message + return; + } + + if (Macro.GET_RAWINPUT_CODE_WPARAM(aMessage.WParam) == Const.RIM_INPUT) + { + IsForeground = true; + } + else if (Macro.GET_RAWINPUT_CODE_WPARAM(aMessage.WParam) == Const.RIM_INPUTSINK) + { + IsForeground = false; + } + + //Declare some pointers + IntPtr rawInputBuffer = IntPtr.Zero; + + try + { + //Fetch raw input + iRawInput = new RAWINPUT(); + if (!Win32.RawInput.GetRawInputData(aMessage.LParam, ref iRawInput, ref rawInputBuffer)) + { + Debug.WriteLine("GetRawInputData failed!"); + return; + } + + //Our device can actually be null. + //This is notably happening for some keyboard events + if (RawInput.header.hDevice != IntPtr.Zero) + { + //Get various information about this HID device + Device = new HidDevice(RawInput.header.hDevice); + } + + if (RawInput.header.dwType == Win32.RawInputDeviceType.RIM_TYPEHID) //Check that our raw input is HID + { + IsGeneric = true; + + Debug.WriteLine("WM_INPUT source device is HID."); + //Get Usage Page and Usage + //Debug.WriteLine("Usage Page: 0x" + deviceInfo.hid.usUsagePage.ToString("X4") + " Usage ID: 0x" + deviceInfo.hid.usUsage.ToString("X4")); + UsagePage = Device.Info.hid.usUsagePage; + UsageCollection = Device.Info.hid.usUsage; + + 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 + && RawInput.hid.dwCount > 0)) //Check that we have at least one HID msg + { + return; + } + + //Allocate a buffer for one HID input + InputReport = new byte[RawInput.hid.dwSizeHid]; + + Debug.WriteLine("Raw input contains " + RawInput.hid.dwCount + " HID input report(s)"); + + //For each HID input report in our raw input + for (int i = 0; i < RawInput.hid.dwCount; i++) + { + //Compute the address from which to copy our HID input + int hidInputOffset = 0; + unsafe + { + byte* source = (byte*)rawInputBuffer; + source += sizeof(RAWINPUTHEADER) + sizeof(RAWHID) + (RawInput.hid.dwSizeHid * i); + hidInputOffset = (int)source; + } + + //Copy HID input into our buffer + Marshal.Copy(new IntPtr(hidInputOffset), InputReport, 0, (int)RawInput.hid.dwSizeHid); + // + ProcessInputReport(InputReport); + } + } + else if (RawInput.header.dwType == RawInputDeviceType.RIM_TYPEMOUSE) + { + IsMouse = true; + + Debug.WriteLine("WM_INPUT source device is Mouse."); + // do mouse handling... + } + else if (RawInput.header.dwType == RawInputDeviceType.RIM_TYPEKEYBOARD) + { + IsKeyboard = true; + + Debug.WriteLine("WM_INPUT source device is Keyboard."); + // do keyboard handling... + if (Device != null) + { + Debug.WriteLine("Type: " + Device.Info.keyboard.dwType.ToString()); + Debug.WriteLine("SubType: " + Device.Info.keyboard.dwSubType.ToString()); + Debug.WriteLine("Mode: " + Device.Info.keyboard.dwKeyboardMode.ToString()); + Debug.WriteLine("Number of function keys: " + Device.Info.keyboard.dwNumberOfFunctionKeys.ToString()); + Debug.WriteLine("Number of indicators: " + Device.Info.keyboard.dwNumberOfIndicators.ToString()); + Debug.WriteLine("Number of keys total: " + Device.Info.keyboard.dwNumberOfKeysTotal.ToString()); + } + } + } + finally + { + //Always executed when leaving our try block + Marshal.FreeHGlobal(rawInputBuffer); + } + + // + if (IsButtonDown) + { + //TODO: Make this optional + //StartRepeatTimer(iRepeatDelay); + } + + IsValid = true; + } + + /// + /// + /// + private void ProcessInputReport(byte[] aInputReport) + { + //Print HID input report in our debug output + //string hidDump = "HID input report: " + InputReportString(); + //Debug.WriteLine(hidDump); + + //Get all our usages, those are typically the buttons currently pushed on a gamepad. + //For a remote control it's usually just the one button that was pushed. + GetUsages(aInputReport); + + //Now process direction pad (d-pad, dpad) and axes + GetUsageValues(aInputReport); + } + + /// + /// Typically fetches values of a joystick/gamepad axis and dpad directions. + /// + /// + private void GetUsageValues(byte[] aInputReport) + { + if (Device.InputValueCapabilities == null) + { + return; + } + + foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities) + { + if (caps.IsRange) + { + //What should we do with those guys? + continue; + } + + //Now fetch and add our usage value + uint usageValue = 0; + 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); + if (status == Win32.HidStatus.HIDP_STATUS_SUCCESS) + { + UsageValues[caps]=usageValue; + } + } + } + + /// + /// Get all our usages, those are typically the buttons currently pushed on a gamepad. + /// For a remote control it's usually just the one button that was pushed. + /// + private void GetUsages(byte[] aInputReport) + { + //Do proper parsing of our HID report + //First query our usage count + uint usageCount = 0; + Win32.USAGE_AND_PAGE[] usages = null; + Win32.HidStatus status = Win32.Function.HidP_GetUsagesEx(Win32.HIDP_REPORT_TYPE.HidP_Input, 0, usages, ref usageCount, Device.PreParsedData, aInputReport, (uint)aInputReport.Length); + if (status == Win32.HidStatus.HIDP_STATUS_BUFFER_TOO_SMALL) + { + //Allocate a large enough buffer + usages = new Win32.USAGE_AND_PAGE[usageCount]; + //...and fetch our usages + status = Win32.Function.HidP_GetUsagesEx(Win32.HIDP_REPORT_TYPE.HidP_Input, 0, usages, ref usageCount, Device.PreParsedData, aInputReport, (uint)aInputReport.Length); + if (status != Win32.HidStatus.HIDP_STATUS_SUCCESS) + { + Debug.WriteLine("Second pass could not parse HID data: " + status.ToString()); + } + } + else if (status != Win32.HidStatus.HIDP_STATUS_SUCCESS) + { + Debug.WriteLine("First pass could not parse HID data: " + status.ToString()); + } + + Debug.WriteLine("Usage count: " + usageCount.ToString()); + + //Copy usages into this event + if (usages != null) + { + foreach (USAGE_AND_PAGE up in usages) + { + //Debug.WriteLine("UsagePage: 0x" + usages[0].UsagePage.ToString("X4")); + //Debug.WriteLine("Usage: 0x" + usages[0].Usage.ToString("X4")); + //Add this usage to our list + Usages.Add(up.Usage); + } + } + } + + /// + /// + /// + /// + /// + /// + public uint GetUsageValue(ushort aUsagePage, ushort aUsage) + { + foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities) + { + if (caps.IsRange) + { + //What should we do with those guys? + continue; + } + + //Check if we have a match + if (caps.UsagePage == aUsagePage && caps.NotRange.Usage == aUsage) + { + return UsageValues[caps]; + } + } + + return 0; + } + + /// + /// + /// + /// + /// + /// + public int GetValueCapabilitiesIndex(ushort aUsagePage, ushort aUsage) + { + int i = -1; + foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities) + { + i++; + if (caps.IsRange) + { + //What should we do with those guys? + continue; + } + + //Check if we have a match + if (caps.UsagePage == aUsagePage && caps.NotRange.Usage == aUsage) + { + return i; + } + } + + return i; + } + + + /// + /// TODO: Move this to another level? + /// + /// + public void StartRepeatTimer(double aInterval) + { + if (Timer == null) + { + return; + } + Timer.Enabled = false; + //Initial delay do not use auto reset + //After our initial delay however we do setup our timer one more time using auto reset + Timer.AutoReset = (RepeatCount != 0); + Timer.Interval = aInterval; + Timer.Enabled = true; + } + + static private void OnRepeatTimerElapsed(object sender, ElapsedEventArgs e, HidEvent aHidEvent) + { + if (aHidEvent.IsStray) + { + //Skip events if canceled + return; + } + + aHidEvent.RepeatCount++; + aHidEvent.Time = DateTime.Now; + if (aHidEvent.RepeatCount == 1) + { + //Re-Start our timer only after the initial delay + aHidEvent.StartRepeatTimer(aHidEvent.iRepeatSpeed); + } + + //Broadcast our repeat event + aHidEvent.OnHidEventRepeat(aHidEvent); + } + + /// + /// Provide the state of the dpad or hat switch if any. + /// If no dpad is found we return 'at rest'. + /// + /// + public DirectionPadState GetDirectionPadState() + { + int index=GetValueCapabilitiesIndex((ushort)Hid.UsagePage.GenericDesktopControls, (ushort)GenericDesktop.HatSwitch); + if (index < 0) + { + //No hat switch found + return DirectionPadState.Rest; + } + + HIDP_VALUE_CAPS caps=Device.InputValueCapabilities[index]; + if (caps.IsRange) + { + //Defensive + return DirectionPadState.Rest; + } + + uint dpadUsageValue = UsageValues[caps]; + + if (dpadUsageValue < caps.LogicalMin || dpadUsageValue > caps.LogicalMax) + { + //Out of range means at rest + return DirectionPadState.Rest; + } + + //Normalize value to start at zero + //TODO: more error check here? + DirectionPadState res = (DirectionPadState)((int)dpadUsageValue - caps.LogicalMin); + return res; + } + + /// + /// Print information about this device to our debug output. + /// + public void DebugWrite() + { + if (!IsValid) + { + Debug.WriteLine("==== Invalid HidEvent"); + return; + } + + if (Device!=null) + { + Device.DebugWrite(); + } + + if (IsGeneric) Debug.WriteLine("==== Generic"); + if (IsKeyboard) Debug.WriteLine("==== Keyboard"); + if (IsMouse) Debug.WriteLine("==== Mouse"); + Debug.WriteLine("==== Foreground: " + IsForeground.ToString()); + Debug.WriteLine("==== UsagePage: 0x" + UsagePage.ToString("X4")); + Debug.WriteLine("==== UsageCollection: 0x" + UsageCollection.ToString("X4")); + Debug.WriteLine("==== InputReport: 0x" + InputReportString()); + foreach (ushort usage in Usages) + { + Debug.WriteLine("==== Usage: 0x" + usage.ToString("X4")); + } + } + + /// + /// + /// + /// + public string InputReportString() + { + if (InputReport == null) + { + return "null"; + } + + string hidDump = ""; + foreach (byte b in InputReport) + { + hidDump += b.ToString("X2"); + } + return hidDump; + } + + + /// + /// Create a list view item describing this HidEvent + /// + /// + public ListViewItem ToListViewItem() + { + string usageText = ""; + + foreach (ushort usage in Usages) + { + if (usageText != "") + { + //Add a separator + usageText += ", "; + } + + UsagePage usagePage = (UsagePage)UsagePage; + switch (usagePage) + { + case Hid.UsagePage.Consumer: + usageText += ((ConsumerControl)usage).ToString(); + break; + + case Hid.UsagePage.WindowsMediaCenterRemoteControl: + usageText += ((WindowsMediaCenterRemoteControl)usage).ToString(); + break; + + default: + usageText += usage.ToString("X2"); + break; + } + } + + //If we are a gamepad display axis and dpad values + if (Device.IsGamePad) + { + //uint dpadUsageValue = GetUsageValue((ushort)Hid.UsagePage.GenericDesktopControls, (ushort)Hid.Usage.GenericDesktop.HatSwitch); + //usageText = dpadUsageValue.ToString("X") + " (dpad), " + usageText; + + if (usageText != "") + { + //Add a separator + usageText += " (Buttons)"; + } + + if (usageText != "") + { + //Add a separator + usageText += ", "; + } + + usageText += GetDirectionPadState().ToString(); + + foreach (KeyValuePair entry in UsageValues) + { + if (entry.Key.IsRange) + { + continue; + } + + Type usageType = Utils.UsageType((UsagePage)entry.Key.UsagePage); + if (usageType == null) + { + //TODO: check why this is happening on Logitech rumble gamepad 2. + //Probably some of our axis are hiding in there. + continue; + } + string name = Enum.GetName(usageType, entry.Key.NotRange.Usage); + + if (usageText != "") + { + //Add a separator + usageText += ", "; + } + usageText += entry.Value.ToString("X") + " ("+ name +")"; + } + } + + //Now create our list item + ListViewItem item = new ListViewItem(new[] { usageText, InputReportString(), UsagePage.ToString("X2"), UsageCollection.ToString("X2"), RepeatCount.ToString(), Time.ToString("HH:mm:ss:fff") }); + return item; + } + + } + +} \ No newline at end of file