Server/FormMain.Hid.cs
author StephaneLenclud
Thu, 18 Aug 2016 18:49:03 +0200
changeset 241 3b5a94f31400
parent 238 c92587ddabcd
child 245 448e6a616c22
permissions -rw-r--r--
Adding support for HID Keyboard events.
     1 using System;
     2 using System.IO;
     3 using System.Collections.Generic;
     4 using System.Linq;
     5 using System.Text;
     6 using System.Threading.Tasks;
     7 using System.Diagnostics;
     8 using System.Runtime.InteropServices;
     9 using System.Windows.Forms;
    10 using Microsoft.Win32.SafeHandles;
    11 using System.ComponentModel;
    12 //
    13 using Hid = SharpLib.Hid;
    14 using SharpLib.Win32;
    15 
    16 namespace SharpDisplayManager
    17 {
    18     /// <summary>
    19     /// Implement handling of HID input reports notably to be able to launch an application using the Green Start button from IR remotes.
    20     /// </summary>
    21     [System.ComponentModel.DesignerCategory("Code")]
    22     public class FormMainHid : Form
    23     {
    24         [System.Runtime.InteropServices.DllImportAttribute("user32.dll", EntryPoint = "SwitchToThisWindow")]
    25         public static extern void SwitchToThisWindow([System.Runtime.InteropServices.InAttribute()] System.IntPtr hwnd, [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.Bool)] bool fUnknown);
    26         //
    27         public delegate void OnHidEventDelegate(object aSender, Hid.Event aHidEvent);
    28 
    29         /// <summary>
    30         /// Use notably to handle green start key from IR remote control
    31         /// </summary>
    32         private Hid.Handler iHidHandler;
    33 
    34         /// <summary>
    35         /// Register HID devices so that we receive corresponding WM_INPUT messages.
    36         /// </summary>
    37         protected void RegisterHidDevices()
    38         {
    39             // Register the input device to receive the commands from the
    40             // remote device. See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwmt/html/remote_control.asp
    41             // for the vendor defined usage page.
    42 
    43             RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[6];
    44 
    45             int i = 0;
    46             rid[i].usUsagePage = (ushort)SharpLib.Hid.UsagePage.WindowsMediaCenterRemoteControl;
    47             rid[i].usUsage = (ushort)SharpLib.Hid.UsageCollection.WindowsMediaCenter.WindowsMediaCenterRemoteControl;
    48             rid[i].dwFlags = RawInputDeviceFlags.RIDEV_INPUTSINK;
    49             rid[i].hwndTarget = Handle;
    50 
    51             i++;
    52             rid[i].usUsagePage = (ushort)SharpLib.Hid.UsagePage.Consumer;
    53             rid[i].usUsage = (ushort)SharpLib.Hid.UsageCollection.Consumer.ConsumerControl;
    54             rid[i].dwFlags = RawInputDeviceFlags.RIDEV_INPUTSINK;
    55             rid[i].hwndTarget = Handle;
    56 
    57             i++;
    58             rid[i].usUsagePage = (ushort)SharpLib.Hid.UsagePage.Consumer;
    59             rid[i].usUsage = (ushort)SharpLib.Hid.UsageCollection.Consumer.Selection;
    60             rid[i].dwFlags = RawInputDeviceFlags.RIDEV_INPUTSINK;
    61             rid[i].hwndTarget = Handle;
    62 
    63             i++;
    64             rid[i].usUsagePage = (ushort)SharpLib.Hid.UsagePage.GenericDesktopControls;
    65             rid[i].usUsage = (ushort)SharpLib.Hid.UsageCollection.GenericDesktop.SystemControl;
    66             rid[i].dwFlags = RawInputDeviceFlags.RIDEV_INPUTSINK;
    67             rid[i].hwndTarget = Handle;
    68 
    69             i++;
    70             rid[i].usUsagePage = (ushort)SharpLib.Hid.UsagePage.GenericDesktopControls;
    71             rid[i].usUsage = (ushort)SharpLib.Hid.UsageCollection.GenericDesktop.GamePad;
    72             rid[i].dwFlags = RawInputDeviceFlags.RIDEV_INPUTSINK;
    73             rid[i].hwndTarget = Handle;
    74 
    75             i++;
    76             rid[i].usUsagePage = (ushort)SharpLib.Hid.UsagePage.GenericDesktopControls;
    77             rid[i].usUsage = (ushort)SharpLib.Hid.UsageCollection.GenericDesktop.Keyboard;
    78             rid[i].dwFlags = RawInputDeviceFlags.RIDEV_INPUTSINK;
    79             rid[i].hwndTarget = Handle;
    80 
    81             //i++;
    82             //rid[i].usUsagePage = (ushort)SharpLib.Hid.UsagePage.GenericDesktopControls;
    83             //rid[i].usUsage = (ushort)SharpLib.Hid.UsageCollection.GenericDesktop.Keyboard;
    84             //rid[i].dwFlags = Const.RIDEV_EXINPUTSINK;
    85             //rid[i].hwndTarget = Handle;
    86 
    87             //i++;
    88             //rid[i].usUsagePage = (ushort)Hid.UsagePage.GenericDesktopControls;
    89             //rid[i].usUsage = (ushort)Hid.UsageCollection.GenericDesktop.Mouse;
    90             //rid[i].dwFlags = Const.RIDEV_EXINPUTSINK;
    91             //rid[i].hwndTarget = aHWND;
    92 
    93 
    94             iHidHandler = new SharpLib.Hid.Handler(rid);
    95             if (!iHidHandler.IsRegistered)
    96             {
    97                 Debug.WriteLine("Failed to register raw input devices: " + Marshal.GetLastWin32Error().ToString());
    98             }
    99             iHidHandler.OnHidEvent += HandleHidEventThreadSafe;
   100 
   101         }
   102 
   103 
   104 
   105 
   106         /// <summary>
   107         /// Here we receive HID events from our HID library.
   108         /// </summary>
   109         /// <param name="aSender"></param>
   110         /// <param name="aHidEvent"></param>
   111         public void HandleHidEventThreadSafe(object aSender, SharpLib.Hid.Event aHidEvent)
   112         {
   113             if (aHidEvent.IsStray || !aHidEvent.IsValid)
   114             {
   115                 //Stray event just ignore it
   116                 return;
   117             }
   118 
   119             if (this.InvokeRequired)
   120             {
   121                 //Not in the proper thread, invoke ourselves
   122                 OnHidEventDelegate d = new OnHidEventDelegate(HandleHidEventThreadSafe);
   123                 this.Invoke(d, new object[] { aSender, aHidEvent });
   124             }
   125             else
   126             {
   127                 if (aHidEvent.IsGeneric)
   128                 {
   129                     if (aHidEvent.Usages.Count == 0)
   130                     {
   131                         //No usage, nothing to do then
   132                         return;
   133                     }
   134 
   135                     //We are in the proper thread
   136                     if (aHidEvent.UsagePage == (ushort) Hid.UsagePage.WindowsMediaCenterRemoteControl)
   137                     {
   138                         //Trigger events as needed
   139                         EventHidWindowsMediaCenter e = new EventHidWindowsMediaCenter
   140                         {
   141                             Usage = (Hid.Usage.WindowsMediaCenterRemoteControl) aHidEvent.Usages[0]
   142                         };
   143                         Properties.Settings.Default.EarManager.TriggerEvent(e);
   144 
   145                         //Old legacy hard coded stuff
   146                         //TODO: remove it
   147                         switch (aHidEvent.Usages[0])
   148                         {
   149                             case (ushort) Hid.Usage.WindowsMediaCenterRemoteControl.GreenStart:
   150                                 HandleGreenStart();
   151                                 break;
   152                             case (ushort) Hid.Usage.WindowsMediaCenterRemoteControl.Eject:
   153                             case (ushort) Hid.Usage.WindowsMediaCenterRemoteControl.Ext2:
   154                                 HandleEject();
   155                                 break;
   156                         }
   157                     }
   158                     else if (aHidEvent.UsagePage == (ushort) Hid.UsagePage.Consumer)
   159                     {
   160                         //Trigger matching events if any
   161                         EventHidConsumerControl e = new EventHidConsumerControl
   162                         {
   163                             Usage = (Hid.Usage.ConsumerControl) aHidEvent.Usages[0]
   164                         };
   165                         Properties.Settings.Default.EarManager.TriggerEvent(e);
   166 
   167                         //Keep this for debug when only ThinkPad keyboard is available
   168                         //if (aHidEvent.Usages[0] == (ushort)Hid.Usage.ConsumerControl.ThinkPadFullscreenMagnifier)
   169                         //{
   170                         //    HandleEject();
   171                         //}
   172                     }
   173                 }
   174                 else if (aHidEvent.IsKeyboard)
   175                 {
   176                     //Trigger matching events if any
   177                     EventHidKeyboard e = new EventHidKeyboard
   178                     {
   179                         Key = aHidEvent.VirtualKey,
   180                         IsKeyUp = aHidEvent.IsButtonUp,
   181                         HasModifierAlt = aHidEvent.HasModifierAlt,
   182                         HasModifierControl = aHidEvent.HasModifierControl,
   183                         HasModifierShift = aHidEvent.HasModifierShift,
   184                         HasModifierWindows = aHidEvent.HasModifierWindows,
   185                     };
   186                     Properties.Settings.Default.EarManager.TriggerEvent(e);
   187                 }                
   188 
   189             }
   190         }
   191 
   192         /// <summary>
   193         /// 
   194         /// </summary>
   195         /// <param name="aPrefix"></param>
   196         private void CheckLastError(string aPrefix)
   197         {
   198             string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message;
   199             Debug.WriteLine(aPrefix + Marshal.GetLastWin32Error().ToString() + ": " + errorMessage);
   200         }
   201 
   202         /// <summary>
   203         /// 
   204         /// </summary>
   205         /// <param name="data"></param>
   206         /// <returns></returns>
   207         private IntPtr MarshalToPointer(object data)
   208         {
   209             IntPtr buf = Marshal.AllocHGlobal(
   210                 Marshal.SizeOf(data));
   211             Marshal.StructureToPtr(data,
   212                 buf, false);
   213             return buf;
   214         }
   215 
   216         /// <summary>
   217         /// 
   218         /// </summary>
   219         /// <returns></returns>
   220         private SafeFileHandle OpenVolume(string aDriveName)
   221         {
   222             return Function.CreateFile("\\\\.\\" + aDriveName,
   223                                SharpLib.Win32.FileAccess.GENERIC_READ,
   224                                SharpLib.Win32.FileShare.FILE_SHARE_READ | SharpLib.Win32.FileShare.FILE_SHARE_WRITE,
   225                                IntPtr.Zero,
   226                                CreationDisposition.OPEN_EXISTING,
   227                                0,
   228                                IntPtr.Zero);
   229         }
   230 
   231         /// <summary>
   232         /// 
   233         /// </summary>
   234         /// <param name="aVolume"></param>
   235         /// <returns></returns>
   236         private bool LockVolume(SafeFileHandle aVolume)
   237         {
   238             //Hope that's doing what I think it does
   239             IntPtr dwBytesReturned=new IntPtr();
   240             //Should not be needed but I'm not sure how to pass NULL in there.
   241             OVERLAPPED overlapped=new OVERLAPPED();
   242 
   243             int tries = 0;
   244             const int KMaxTries = 100;
   245             const int KSleepTime = 10;
   246             bool success = false;
   247 
   248             while (!success && tries < KMaxTries)
   249             {
   250                 success = Function.DeviceIoControl(aVolume, Const.FSCTL_LOCK_VOLUME, IntPtr.Zero, 0, IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
   251                 System.Threading.Thread.Sleep(KSleepTime);
   252                 tries++;
   253             }
   254 
   255             CheckLastError("Lock volume: ");
   256 
   257             return success;
   258         }
   259 
   260         /// <summary>
   261         /// 
   262         /// </summary>
   263         /// <param name="aVolume"></param>
   264         /// <returns></returns>
   265         private bool DismountVolume(SafeFileHandle aVolume)
   266         {
   267             //Hope that's doing what I think it does
   268             IntPtr dwBytesReturned = new IntPtr();
   269             //Should not be needed but I'm not sure how to pass NULL in there.
   270             OVERLAPPED overlapped=new OVERLAPPED();
   271 
   272             bool res = Function.DeviceIoControl(aVolume, Const.FSCTL_DISMOUNT_VOLUME, IntPtr.Zero, 0, IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
   273             CheckLastError("Dismount volume: ");
   274             return res;
   275         }
   276 
   277 
   278 
   279         /// <summary>
   280         /// 
   281         /// </summary>
   282         /// <param name="aVolume"></param>
   283         /// <param name="aPreventRemoval"></param>
   284         /// <returns></returns>
   285         private bool PreventRemovalOfVolume(SafeFileHandle aVolume, bool aPreventRemoval)
   286         {
   287             //Hope that's doing what I think it does
   288             IntPtr dwBytesReturned = new IntPtr();
   289             //Should not be needed but I'm not sure how to pass NULL in there.
   290             OVERLAPPED overlapped = new OVERLAPPED();
   291             //
   292             PREVENT_MEDIA_REMOVAL preventMediaRemoval = new PREVENT_MEDIA_REMOVAL();
   293             preventMediaRemoval.PreventMediaRemoval = Convert.ToByte(aPreventRemoval);
   294             IntPtr preventMediaRemovalParam = MarshalToPointer(preventMediaRemoval);
   295 
   296             bool result = Function.DeviceIoControl(aVolume, Const.IOCTL_STORAGE_MEDIA_REMOVAL, preventMediaRemovalParam, Convert.ToUInt32(Marshal.SizeOf(preventMediaRemoval)), IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
   297             CheckLastError("Media removal: ");
   298             Marshal.FreeHGlobal(preventMediaRemovalParam);
   299 
   300             return result;
   301         }
   302 
   303         /// <summary>
   304         /// Eject optical drive media opening the tray if any.
   305         /// </summary>
   306         /// <param name="aVolume"></param>
   307         /// <returns></returns>
   308         private bool MediaEject(SafeFileHandle aVolume)
   309         {
   310             //Hope that's doing what I think it does
   311             IntPtr dwBytesReturned = new IntPtr();
   312             //Should not be needed but I'm not sure how to pass NULL in there.
   313             OVERLAPPED overlapped=new OVERLAPPED();
   314 
   315             bool res = Function.DeviceIoControl(aVolume, Const.IOCTL_STORAGE_EJECT_MEDIA, IntPtr.Zero, 0, IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
   316             CheckLastError("Media eject: ");
   317             return res;
   318         }
   319 
   320         /// <summary>
   321         /// Close an optical drive tray.
   322         /// </summary>
   323         /// <param name="aVolume"></param>
   324         /// <returns></returns>
   325         private bool MediaLoad(SafeFileHandle aVolume)
   326         {
   327             //Hope that's doing what I think it does
   328             IntPtr dwBytesReturned = new IntPtr();
   329             //Should not be needed but I'm not sure how to pass NULL in there.
   330             OVERLAPPED overlapped=new OVERLAPPED();
   331 
   332             bool res = Function.DeviceIoControl(aVolume, Const.IOCTL_STORAGE_LOAD_MEDIA, IntPtr.Zero, 0, IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
   333             CheckLastError("Media load: ");
   334             return res;
   335         }
   336 
   337         /// <summary>
   338         /// 
   339         /// </summary>
   340         /// <param name="aVolume"></param>
   341         /// <returns></returns>
   342         private bool StorageCheckVerify(SafeFileHandle aVolume)
   343         {
   344             //Hope that's doing what I think it does
   345             IntPtr dwBytesReturned = new IntPtr();
   346             //Should not be needed but I'm not sure how to pass NULL in there.
   347             OVERLAPPED overlapped = new OVERLAPPED();
   348 
   349             bool res = Function.DeviceIoControl(aVolume, Const.IOCTL_STORAGE_CHECK_VERIFY2, IntPtr.Zero, 0, IntPtr.Zero, 0, dwBytesReturned, ref overlapped);
   350 
   351             CheckLastError("Check verify: ");
   352 
   353             return res;
   354         }        
   355         
   356 
   357 
   358         /// <summary>
   359         /// Perform media ejection.
   360         /// </summary>
   361         private void HandleEject()
   362         {
   363             string drive = ((FormMain)this).OpticalDriveToEject();
   364             if (drive.Length!=2)
   365             {
   366                 //Not a proper drive spec.
   367                 //Probably 'None' selected.
   368                 return;
   369             }
   370 
   371             SafeFileHandle handle = OpenVolume(drive);
   372             if (handle.IsInvalid)
   373             {
   374                 CheckLastError("ERROR: Failed to open volume: ");
   375                 return;
   376             }
   377 
   378             if (LockVolume(handle) && DismountVolume(handle))
   379             {
   380                 Debug.WriteLine("Volume was dismounted.");
   381 
   382                 if (PreventRemovalOfVolume(handle,false))
   383                 {
   384                     //StorageCheckVerify(handle);
   385 
   386                     DateTime before;
   387                     before = DateTime.Now;
   388                     bool ejectSuccess = MediaEject(handle);
   389                     double ms = (DateTime.Now - before).TotalMilliseconds;
   390 
   391                     //We assume that if it take more than a certain time to for eject to execute it means we actually ejected.
   392                     //If our eject completes too rapidly we assume the tray is already open and we will try to close it. 
   393                     if (ejectSuccess && ms > 100)
   394                     {
   395                         Debug.WriteLine("Media was ejected");
   396                     }
   397                     else if (MediaLoad(handle))
   398                     {
   399                         Debug.WriteLine("Media was loaded");
   400                     }                    
   401                 }
   402             }
   403             else
   404             {
   405                 Debug.WriteLine("Volume lock or dismount failed.");
   406             }
   407 
   408             //This is needed to make sure we can open the volume next time around
   409             handle.Dispose();
   410         }
   411 
   412         /// <summary>
   413         /// 
   414         /// </summary>
   415         private void HandleGreenStart()
   416         {
   417             //First check if the process we want to launch already exists
   418             string procName = Path.GetFileNameWithoutExtension(Properties.Settings.Default.StartFileName);
   419             Process[] existingProcesses = Process.GetProcessesByName(procName);
   420             if (existingProcesses == null || existingProcesses.Length == 0)
   421             {
   422                 // Process do not exists just try to launch it
   423                 ProcessStartInfo start = new ProcessStartInfo();
   424                 // Enter in the command line arguments, everything you would enter after the executable name itself
   425                 //start.Arguments = arguments; 
   426                 // Enter the executable to run, including the complete path
   427                 start.FileName = Properties.Settings.Default.StartFileName;
   428                 start.WindowStyle = ProcessWindowStyle.Normal;
   429                 start.CreateNoWindow = true;
   430                 start.UseShellExecute = true;
   431                 // Run the external process & wait for it to finish
   432                 Process proc = Process.Start(start);
   433 
   434                 //SL: We could have used that too
   435                 //Shell32.Shell shell = new Shell32.Shell();
   436                 //shell.ShellExecute(Properties.Settings.Default.StartFileName);
   437             }
   438             else
   439             {
   440                 //This won't work properly until we have a manifest that enables uiAccess.
   441                 //However uiAccess just won't work with ClickOnce so we will have to use a different deployment system.
   442                 SwitchToThisWindow(existingProcesses[0].MainWindowHandle, true);
   443             }            
   444         }
   445 
   446 
   447         /// <summary>
   448         /// We need to handle WM_INPUT.
   449         /// </summary>
   450         /// <param name="message"></param>
   451         protected override void WndProc(ref Message message)
   452         {
   453             switch (message.Msg)
   454             {
   455                 case Const.WM_INPUT:
   456                     //Returning zero means we processed that message.
   457                     message.Result = new IntPtr(0);
   458                     iHidHandler.ProcessInput(ref message);
   459                     break;
   460             }
   461 
   462             //Pass this on to base class.
   463             base.WndProc(ref message);
   464         }
   465     }
   466 }