1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/Hid/HidEvent.cs Sun Mar 15 20:25:58 2015 +0100
1.3 @@ -0,0 +1,598 @@
1.4 +//
1.5 +// Copyright (C) 2014-2015 Stéphane Lenclud.
1.6 +//
1.7 +// This file is part of SharpLibHid.
1.8 +//
1.9 +// SharpDisplayManager is free software: you can redistribute it and/or modify
1.10 +// it under the terms of the GNU General Public License as published by
1.11 +// the Free Software Foundation, either version 3 of the License, or
1.12 +// (at your option) any later version.
1.13 +//
1.14 +// SharpDisplayManager is distributed in the hope that it will be useful,
1.15 +// but WITHOUT ANY WARRANTY; without even the implied warranty of
1.16 +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1.17 +// GNU General Public License for more details.
1.18 +//
1.19 +// You should have received a copy of the GNU General Public License
1.20 +// along with SharpDisplayManager. If not, see <http://www.gnu.org/licenses/>.
1.21 +//
1.22 +
1.23 +
1.24 +using System;
1.25 +using System.Windows.Forms;
1.26 +using System.Runtime.InteropServices;
1.27 +using System.Diagnostics;
1.28 +using System.Text;
1.29 +using Microsoft.Win32.SafeHandles;
1.30 +using SharpLib.Win32;
1.31 +using System.Collections.Generic;
1.32 +using System.Timers;
1.33 +using SharpLib.Hid.Usage;
1.34 +
1.35 +
1.36 +namespace SharpLib.Hid
1.37 +{
1.38 + /// <summary>
1.39 + /// We provide utility functions to interpret gamepad dpad state.
1.40 + /// </summary>
1.41 + public enum DirectionPadState
1.42 + {
1.43 + Rest=-1,
1.44 + Up=0,
1.45 + UpRight=1,
1.46 + Right=2,
1.47 + DownRight=3,
1.48 + Down=4,
1.49 + DownLeft=5,
1.50 + Left=6,
1.51 + UpLeft=7
1.52 + }
1.53 +
1.54 + /// <summary>
1.55 + /// Represent a HID event.
1.56 + /// TODO: Rename this into HidRawInput?
1.57 + /// </summary>
1.58 + public class HidEvent : IDisposable
1.59 + {
1.60 + public bool IsValid { get; private set; }
1.61 + public bool IsForeground { get; private set; }
1.62 + public bool IsBackground { get { return !IsForeground; } }
1.63 + public bool IsMouse { get; private set; }
1.64 + public bool IsKeyboard { get; private set; }
1.65 + /// <summary>
1.66 + /// If this not a mouse or keyboard event then it's a generic HID event.
1.67 + /// </summary>
1.68 + public bool IsGeneric { get; private set; }
1.69 + public bool IsButtonDown { get { return Usages.Count == 1 && Usages[0] != 0; } }
1.70 + public bool IsButtonUp { get { return Usages.Count == 0; } }
1.71 + public bool IsRepeat { get { return RepeatCount != 0; } }
1.72 + public uint RepeatCount { get; private set; }
1.73 +
1.74 + public HidDevice Device { get; private set; }
1.75 + public RAWINPUT RawInput { get {return iRawInput;} }
1.76 + private RAWINPUT iRawInput;
1.77 +
1.78 + public ushort UsagePage { get; private set; }
1.79 + public ushort UsageCollection { get; private set; }
1.80 + public uint UsageId { get { return ((uint)UsagePage << 16 | (uint)UsageCollection); } }
1.81 + public List<ushort> Usages { get; private set; }
1.82 + /// <summary>
1.83 + /// Sorted in the same order as Device.InputValueCapabilities.
1.84 + /// </summary>
1.85 + public Dictionary<HIDP_VALUE_CAPS,uint> UsageValues { get; private set; }
1.86 + //TODO: We need a collection of input report
1.87 + public byte[] InputReport { get; private set; }
1.88 + //
1.89 + public delegate void HidEventRepeatDelegate(HidEvent aHidEvent);
1.90 + public event HidEventRepeatDelegate OnHidEventRepeat;
1.91 +
1.92 + private System.Timers.Timer Timer { get; set; }
1.93 + public DateTime Time { get; private set; }
1.94 + public DateTime OriginalTime { get; private set; }
1.95 +
1.96 + //Compute repeat delay and speed based on system settings
1.97 + //Those computations were taken from the Petzold here: ftp://ftp.charlespetzold.com/ProgWinForms/4%20Custom%20Controls/NumericScan/NumericScan/ClickmaticButton.cs
1.98 + private int iRepeatDelay = 250 * (1 + SystemInformation.KeyboardDelay);
1.99 + private int iRepeatSpeed = 405 - 12 * SystemInformation.KeyboardSpeed;
1.100 +
1.101 + /// <summary>
1.102 + /// Tells whether this event has already been disposed of.
1.103 + /// </summary>
1.104 + public bool IsStray { get { return Timer == null; } }
1.105 +
1.106 + /// <summary>
1.107 + /// We typically dispose of events as soon as we get the corresponding key up signal.
1.108 + /// </summary>
1.109 + public void Dispose()
1.110 + {
1.111 + Timer.Enabled = false;
1.112 + Timer.Dispose();
1.113 + //Mark this event as a stray
1.114 + Timer = null;
1.115 + }
1.116 +
1.117 + /// <summary>
1.118 + /// Initialize an HidEvent from a WM_INPUT message
1.119 + /// </summary>
1.120 + /// <param name="hRawInputDevice">Device Handle as provided by RAWINPUTHEADER.hDevice, typically accessed as rawinput.header.hDevice</param>
1.121 + public HidEvent(Message aMessage, HidEventRepeatDelegate aRepeatDelegate)
1.122 + {
1.123 + RepeatCount = 0;
1.124 + IsValid = false;
1.125 + IsKeyboard = false;
1.126 + IsGeneric = false;
1.127 +
1.128 + Time = DateTime.Now;
1.129 + OriginalTime = DateTime.Now;
1.130 + Timer = new System.Timers.Timer();
1.131 + Timer.Elapsed += (sender, e) => OnRepeatTimerElapsed(sender, e, this);
1.132 + Usages = new List<ushort>();
1.133 + UsageValues = new Dictionary<HIDP_VALUE_CAPS,uint>();
1.134 + OnHidEventRepeat += aRepeatDelegate;
1.135 +
1.136 + if (aMessage.Msg != Const.WM_INPUT)
1.137 + {
1.138 + //Has to be a WM_INPUT message
1.139 + return;
1.140 + }
1.141 +
1.142 + if (Macro.GET_RAWINPUT_CODE_WPARAM(aMessage.WParam) == Const.RIM_INPUT)
1.143 + {
1.144 + IsForeground = true;
1.145 + }
1.146 + else if (Macro.GET_RAWINPUT_CODE_WPARAM(aMessage.WParam) == Const.RIM_INPUTSINK)
1.147 + {
1.148 + IsForeground = false;
1.149 + }
1.150 +
1.151 + //Declare some pointers
1.152 + IntPtr rawInputBuffer = IntPtr.Zero;
1.153 +
1.154 + try
1.155 + {
1.156 + //Fetch raw input
1.157 + iRawInput = new RAWINPUT();
1.158 + if (!Win32.RawInput.GetRawInputData(aMessage.LParam, ref iRawInput, ref rawInputBuffer))
1.159 + {
1.160 + Debug.WriteLine("GetRawInputData failed!");
1.161 + return;
1.162 + }
1.163 +
1.164 + //Our device can actually be null.
1.165 + //This is notably happening for some keyboard events
1.166 + if (RawInput.header.hDevice != IntPtr.Zero)
1.167 + {
1.168 + //Get various information about this HID device
1.169 + Device = new HidDevice(RawInput.header.hDevice);
1.170 + }
1.171 +
1.172 + if (RawInput.header.dwType == Win32.RawInputDeviceType.RIM_TYPEHID) //Check that our raw input is HID
1.173 + {
1.174 + IsGeneric = true;
1.175 +
1.176 + Debug.WriteLine("WM_INPUT source device is HID.");
1.177 + //Get Usage Page and Usage
1.178 + //Debug.WriteLine("Usage Page: 0x" + deviceInfo.hid.usUsagePage.ToString("X4") + " Usage ID: 0x" + deviceInfo.hid.usUsage.ToString("X4"));
1.179 + UsagePage = Device.Info.hid.usUsagePage;
1.180 + UsageCollection = Device.Info.hid.usUsage;
1.181 +
1.182 + 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
1.183 + && RawInput.hid.dwCount > 0)) //Check that we have at least one HID msg
1.184 + {
1.185 + return;
1.186 + }
1.187 +
1.188 + //Allocate a buffer for one HID input
1.189 + InputReport = new byte[RawInput.hid.dwSizeHid];
1.190 +
1.191 + Debug.WriteLine("Raw input contains " + RawInput.hid.dwCount + " HID input report(s)");
1.192 +
1.193 + //For each HID input report in our raw input
1.194 + for (int i = 0; i < RawInput.hid.dwCount; i++)
1.195 + {
1.196 + //Compute the address from which to copy our HID input
1.197 + int hidInputOffset = 0;
1.198 + unsafe
1.199 + {
1.200 + byte* source = (byte*)rawInputBuffer;
1.201 + source += sizeof(RAWINPUTHEADER) + sizeof(RAWHID) + (RawInput.hid.dwSizeHid * i);
1.202 + hidInputOffset = (int)source;
1.203 + }
1.204 +
1.205 + //Copy HID input into our buffer
1.206 + Marshal.Copy(new IntPtr(hidInputOffset), InputReport, 0, (int)RawInput.hid.dwSizeHid);
1.207 + //
1.208 + ProcessInputReport(InputReport);
1.209 + }
1.210 + }
1.211 + else if (RawInput.header.dwType == RawInputDeviceType.RIM_TYPEMOUSE)
1.212 + {
1.213 + IsMouse = true;
1.214 +
1.215 + Debug.WriteLine("WM_INPUT source device is Mouse.");
1.216 + // do mouse handling...
1.217 + }
1.218 + else if (RawInput.header.dwType == RawInputDeviceType.RIM_TYPEKEYBOARD)
1.219 + {
1.220 + IsKeyboard = true;
1.221 +
1.222 + Debug.WriteLine("WM_INPUT source device is Keyboard.");
1.223 + // do keyboard handling...
1.224 + if (Device != null)
1.225 + {
1.226 + Debug.WriteLine("Type: " + Device.Info.keyboard.dwType.ToString());
1.227 + Debug.WriteLine("SubType: " + Device.Info.keyboard.dwSubType.ToString());
1.228 + Debug.WriteLine("Mode: " + Device.Info.keyboard.dwKeyboardMode.ToString());
1.229 + Debug.WriteLine("Number of function keys: " + Device.Info.keyboard.dwNumberOfFunctionKeys.ToString());
1.230 + Debug.WriteLine("Number of indicators: " + Device.Info.keyboard.dwNumberOfIndicators.ToString());
1.231 + Debug.WriteLine("Number of keys total: " + Device.Info.keyboard.dwNumberOfKeysTotal.ToString());
1.232 + }
1.233 + }
1.234 + }
1.235 + finally
1.236 + {
1.237 + //Always executed when leaving our try block
1.238 + Marshal.FreeHGlobal(rawInputBuffer);
1.239 + }
1.240 +
1.241 + //
1.242 + if (IsButtonDown)
1.243 + {
1.244 + //TODO: Make this optional
1.245 + //StartRepeatTimer(iRepeatDelay);
1.246 + }
1.247 +
1.248 + IsValid = true;
1.249 + }
1.250 +
1.251 + /// <summary>
1.252 + ///
1.253 + /// </summary>
1.254 + private void ProcessInputReport(byte[] aInputReport)
1.255 + {
1.256 + //Print HID input report in our debug output
1.257 + //string hidDump = "HID input report: " + InputReportString();
1.258 + //Debug.WriteLine(hidDump);
1.259 +
1.260 + //Get all our usages, those are typically the buttons currently pushed on a gamepad.
1.261 + //For a remote control it's usually just the one button that was pushed.
1.262 + GetUsages(aInputReport);
1.263 +
1.264 + //Now process direction pad (d-pad, dpad) and axes
1.265 + GetUsageValues(aInputReport);
1.266 + }
1.267 +
1.268 + /// <summary>
1.269 + /// Typically fetches values of a joystick/gamepad axis and dpad directions.
1.270 + /// </summary>
1.271 + /// <param name="aInputReport"></param>
1.272 + private void GetUsageValues(byte[] aInputReport)
1.273 + {
1.274 + if (Device.InputValueCapabilities == null)
1.275 + {
1.276 + return;
1.277 + }
1.278 +
1.279 + foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
1.280 + {
1.281 + if (caps.IsRange)
1.282 + {
1.283 + //What should we do with those guys?
1.284 + continue;
1.285 + }
1.286 +
1.287 + //Now fetch and add our usage value
1.288 + uint usageValue = 0;
1.289 + 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);
1.290 + if (status == Win32.HidStatus.HIDP_STATUS_SUCCESS)
1.291 + {
1.292 + UsageValues[caps]=usageValue;
1.293 + }
1.294 + }
1.295 + }
1.296 +
1.297 + /// <summary>
1.298 + /// Get all our usages, those are typically the buttons currently pushed on a gamepad.
1.299 + /// For a remote control it's usually just the one button that was pushed.
1.300 + /// </summary>
1.301 + private void GetUsages(byte[] aInputReport)
1.302 + {
1.303 + //Do proper parsing of our HID report
1.304 + //First query our usage count
1.305 + uint usageCount = 0;
1.306 + Win32.USAGE_AND_PAGE[] usages = null;
1.307 + Win32.HidStatus status = Win32.Function.HidP_GetUsagesEx(Win32.HIDP_REPORT_TYPE.HidP_Input, 0, usages, ref usageCount, Device.PreParsedData, aInputReport, (uint)aInputReport.Length);
1.308 + if (status == Win32.HidStatus.HIDP_STATUS_BUFFER_TOO_SMALL)
1.309 + {
1.310 + //Allocate a large enough buffer
1.311 + usages = new Win32.USAGE_AND_PAGE[usageCount];
1.312 + //...and fetch our usages
1.313 + status = Win32.Function.HidP_GetUsagesEx(Win32.HIDP_REPORT_TYPE.HidP_Input, 0, usages, ref usageCount, Device.PreParsedData, aInputReport, (uint)aInputReport.Length);
1.314 + if (status != Win32.HidStatus.HIDP_STATUS_SUCCESS)
1.315 + {
1.316 + Debug.WriteLine("Second pass could not parse HID data: " + status.ToString());
1.317 + }
1.318 + }
1.319 + else if (status != Win32.HidStatus.HIDP_STATUS_SUCCESS)
1.320 + {
1.321 + Debug.WriteLine("First pass could not parse HID data: " + status.ToString());
1.322 + }
1.323 +
1.324 + Debug.WriteLine("Usage count: " + usageCount.ToString());
1.325 +
1.326 + //Copy usages into this event
1.327 + if (usages != null)
1.328 + {
1.329 + foreach (USAGE_AND_PAGE up in usages)
1.330 + {
1.331 + //Debug.WriteLine("UsagePage: 0x" + usages[0].UsagePage.ToString("X4"));
1.332 + //Debug.WriteLine("Usage: 0x" + usages[0].Usage.ToString("X4"));
1.333 + //Add this usage to our list
1.334 + Usages.Add(up.Usage);
1.335 + }
1.336 + }
1.337 + }
1.338 +
1.339 + /// <summary>
1.340 + ///
1.341 + /// </summary>
1.342 + /// <param name="aUsagePage"></param>
1.343 + /// <param name="Usage"></param>
1.344 + /// <returns></returns>
1.345 + public uint GetUsageValue(ushort aUsagePage, ushort aUsage)
1.346 + {
1.347 + foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
1.348 + {
1.349 + if (caps.IsRange)
1.350 + {
1.351 + //What should we do with those guys?
1.352 + continue;
1.353 + }
1.354 +
1.355 + //Check if we have a match
1.356 + if (caps.UsagePage == aUsagePage && caps.NotRange.Usage == aUsage)
1.357 + {
1.358 + return UsageValues[caps];
1.359 + }
1.360 + }
1.361 +
1.362 + return 0;
1.363 + }
1.364 +
1.365 + /// <summary>
1.366 + ///
1.367 + /// </summary>
1.368 + /// <param name="aUsagePage"></param>
1.369 + /// <param name="aUsage"></param>
1.370 + /// <returns></returns>
1.371 + public int GetValueCapabilitiesIndex(ushort aUsagePage, ushort aUsage)
1.372 + {
1.373 + int i = -1;
1.374 + foreach (HIDP_VALUE_CAPS caps in Device.InputValueCapabilities)
1.375 + {
1.376 + i++;
1.377 + if (caps.IsRange)
1.378 + {
1.379 + //What should we do with those guys?
1.380 + continue;
1.381 + }
1.382 +
1.383 + //Check if we have a match
1.384 + if (caps.UsagePage == aUsagePage && caps.NotRange.Usage == aUsage)
1.385 + {
1.386 + return i;
1.387 + }
1.388 + }
1.389 +
1.390 + return i;
1.391 + }
1.392 +
1.393 +
1.394 + /// <summary>
1.395 + /// TODO: Move this to another level?
1.396 + /// </summary>
1.397 + /// <param name="aInterval"></param>
1.398 + public void StartRepeatTimer(double aInterval)
1.399 + {
1.400 + if (Timer == null)
1.401 + {
1.402 + return;
1.403 + }
1.404 + Timer.Enabled = false;
1.405 + //Initial delay do not use auto reset
1.406 + //After our initial delay however we do setup our timer one more time using auto reset
1.407 + Timer.AutoReset = (RepeatCount != 0);
1.408 + Timer.Interval = aInterval;
1.409 + Timer.Enabled = true;
1.410 + }
1.411 +
1.412 + static private void OnRepeatTimerElapsed(object sender, ElapsedEventArgs e, HidEvent aHidEvent)
1.413 + {
1.414 + if (aHidEvent.IsStray)
1.415 + {
1.416 + //Skip events if canceled
1.417 + return;
1.418 + }
1.419 +
1.420 + aHidEvent.RepeatCount++;
1.421 + aHidEvent.Time = DateTime.Now;
1.422 + if (aHidEvent.RepeatCount == 1)
1.423 + {
1.424 + //Re-Start our timer only after the initial delay
1.425 + aHidEvent.StartRepeatTimer(aHidEvent.iRepeatSpeed);
1.426 + }
1.427 +
1.428 + //Broadcast our repeat event
1.429 + aHidEvent.OnHidEventRepeat(aHidEvent);
1.430 + }
1.431 +
1.432 + /// <summary>
1.433 + /// Provide the state of the dpad or hat switch if any.
1.434 + /// If no dpad is found we return 'at rest'.
1.435 + /// </summary>
1.436 + /// <returns></returns>
1.437 + public DirectionPadState GetDirectionPadState()
1.438 + {
1.439 + int index=GetValueCapabilitiesIndex((ushort)Hid.UsagePage.GenericDesktopControls, (ushort)GenericDesktop.HatSwitch);
1.440 + if (index < 0)
1.441 + {
1.442 + //No hat switch found
1.443 + return DirectionPadState.Rest;
1.444 + }
1.445 +
1.446 + HIDP_VALUE_CAPS caps=Device.InputValueCapabilities[index];
1.447 + if (caps.IsRange)
1.448 + {
1.449 + //Defensive
1.450 + return DirectionPadState.Rest;
1.451 + }
1.452 +
1.453 + uint dpadUsageValue = UsageValues[caps];
1.454 +
1.455 + if (dpadUsageValue < caps.LogicalMin || dpadUsageValue > caps.LogicalMax)
1.456 + {
1.457 + //Out of range means at rest
1.458 + return DirectionPadState.Rest;
1.459 + }
1.460 +
1.461 + //Normalize value to start at zero
1.462 + //TODO: more error check here?
1.463 + DirectionPadState res = (DirectionPadState)((int)dpadUsageValue - caps.LogicalMin);
1.464 + return res;
1.465 + }
1.466 +
1.467 + /// <summary>
1.468 + /// Print information about this device to our debug output.
1.469 + /// </summary>
1.470 + public void DebugWrite()
1.471 + {
1.472 + if (!IsValid)
1.473 + {
1.474 + Debug.WriteLine("==== Invalid HidEvent");
1.475 + return;
1.476 + }
1.477 +
1.478 + if (Device!=null)
1.479 + {
1.480 + Device.DebugWrite();
1.481 + }
1.482 +
1.483 + if (IsGeneric) Debug.WriteLine("==== Generic");
1.484 + if (IsKeyboard) Debug.WriteLine("==== Keyboard");
1.485 + if (IsMouse) Debug.WriteLine("==== Mouse");
1.486 + Debug.WriteLine("==== Foreground: " + IsForeground.ToString());
1.487 + Debug.WriteLine("==== UsagePage: 0x" + UsagePage.ToString("X4"));
1.488 + Debug.WriteLine("==== UsageCollection: 0x" + UsageCollection.ToString("X4"));
1.489 + Debug.WriteLine("==== InputReport: 0x" + InputReportString());
1.490 + foreach (ushort usage in Usages)
1.491 + {
1.492 + Debug.WriteLine("==== Usage: 0x" + usage.ToString("X4"));
1.493 + }
1.494 + }
1.495 +
1.496 + /// <summary>
1.497 + ///
1.498 + /// </summary>
1.499 + /// <returns></returns>
1.500 + public string InputReportString()
1.501 + {
1.502 + if (InputReport == null)
1.503 + {
1.504 + return "null";
1.505 + }
1.506 +
1.507 + string hidDump = "";
1.508 + foreach (byte b in InputReport)
1.509 + {
1.510 + hidDump += b.ToString("X2");
1.511 + }
1.512 + return hidDump;
1.513 + }
1.514 +
1.515 +
1.516 + /// <summary>
1.517 + /// Create a list view item describing this HidEvent
1.518 + /// </summary>
1.519 + /// <returns></returns>
1.520 + public ListViewItem ToListViewItem()
1.521 + {
1.522 + string usageText = "";
1.523 +
1.524 + foreach (ushort usage in Usages)
1.525 + {
1.526 + if (usageText != "")
1.527 + {
1.528 + //Add a separator
1.529 + usageText += ", ";
1.530 + }
1.531 +
1.532 + UsagePage usagePage = (UsagePage)UsagePage;
1.533 + switch (usagePage)
1.534 + {
1.535 + case Hid.UsagePage.Consumer:
1.536 + usageText += ((ConsumerControl)usage).ToString();
1.537 + break;
1.538 +
1.539 + case Hid.UsagePage.WindowsMediaCenterRemoteControl:
1.540 + usageText += ((WindowsMediaCenterRemoteControl)usage).ToString();
1.541 + break;
1.542 +
1.543 + default:
1.544 + usageText += usage.ToString("X2");
1.545 + break;
1.546 + }
1.547 + }
1.548 +
1.549 + //If we are a gamepad display axis and dpad values
1.550 + if (Device.IsGamePad)
1.551 + {
1.552 + //uint dpadUsageValue = GetUsageValue((ushort)Hid.UsagePage.GenericDesktopControls, (ushort)Hid.Usage.GenericDesktop.HatSwitch);
1.553 + //usageText = dpadUsageValue.ToString("X") + " (dpad), " + usageText;
1.554 +
1.555 + if (usageText != "")
1.556 + {
1.557 + //Add a separator
1.558 + usageText += " (Buttons)";
1.559 + }
1.560 +
1.561 + if (usageText != "")
1.562 + {
1.563 + //Add a separator
1.564 + usageText += ", ";
1.565 + }
1.566 +
1.567 + usageText += GetDirectionPadState().ToString();
1.568 +
1.569 + foreach (KeyValuePair<HIDP_VALUE_CAPS, uint> entry in UsageValues)
1.570 + {
1.571 + if (entry.Key.IsRange)
1.572 + {
1.573 + continue;
1.574 + }
1.575 +
1.576 + Type usageType = Utils.UsageType((UsagePage)entry.Key.UsagePage);
1.577 + if (usageType == null)
1.578 + {
1.579 + //TODO: check why this is happening on Logitech rumble gamepad 2.
1.580 + //Probably some of our axis are hiding in there.
1.581 + continue;
1.582 + }
1.583 + string name = Enum.GetName(usageType, entry.Key.NotRange.Usage);
1.584 +
1.585 + if (usageText != "")
1.586 + {
1.587 + //Add a separator
1.588 + usageText += ", ";
1.589 + }
1.590 + usageText += entry.Value.ToString("X") + " ("+ name +")";
1.591 + }
1.592 + }
1.593 +
1.594 + //Now create our list item
1.595 + ListViewItem item = new ListViewItem(new[] { usageText, InputReportString(), UsagePage.ToString("X2"), UsageCollection.ToString("X2"), RepeatCount.ToString(), Time.ToString("HH:mm:ss:fff") });
1.596 + return item;
1.597 + }
1.598 +
1.599 + }
1.600 +
1.601 +}
1.602 \ No newline at end of file