Tuesday, December 13, 2011

Dell Keyboard Problems (writing a service in VS2010)

It seems that whenever I resume the laptop out of sleep (or undock) for some reason even Google can't explain my keyboard repeat rate and delay slow down.  If I go into Control Panel and check everything is still set the way I like it -- and if I OK out, then everything is fine again?!?!  I can go through this Control Panel process every time, but this sort of annoying crap ruins my day.  I can't find a way to fix it, so Plan B is to write a program to do the process for me automatically.

(Update: see version 2 below for a slightly different fix)

Tame the Keyboard Service
I decided to write a service because: this is a long running program, it needs to start automatically, and I haven't written a service in VS2010 yet.  (It turns out the need to interact with the desktop probably would have made a forms project easier, like something with a tray-icon).

Creating a Service in VS2010
You start the usual way by selecting a service template to make a new project and solution.  I haven't made a service in a while, and I usually do all the setup and install manually so the next step did not seem familiar:  Using the designer, you right-click inside to get a menu with "Add Installer".  This simple service is going to have three parts (but they are easy to create with VS2010): 1. The service, 2. an installer that adds the service to "Services" and 3. a Setup program that will "Install" by deploying and running the installer.

Fig 1. Add Installer
image

Note: before you go too far, change the names of everything (classes, titles, other text) to something better that Service1 for example.  It will be harder to change later.   In the Designer, you can also modify the service properties, e.g., CanHandlePowerEvents=True, ServiceName="TameKeyboardService".

A service process can have more than one service so you end up with two new components (see Fig 2). Again, using the Designer, select each to set the desired properties.  For me, this Process needs to use LocalSystem account, and the Service needs StartType=Automatic, for example.  This is also where you set the DisplayName and Description that will show up in Services.

Fig 2. Configure Installer
image

Writing the Service
The VS2010 template gives you OnStart() and OnStop() overrides.  In OnStart, I'm going to get the current repeat and delay rate.  To do this I need to P/Invoke a Win32 function (not strictly true, there is a managed function, but it is read-only).

''' <summary>The SystemParametersInfo API call can be used to get and set Windows settings that are
''' normally set from the Desktop by using the Control Panel
''' </summary>
''' <param name="uAction">system parameter to query or set</param>
''' <param name="uparam">depends on system parameter</param>
''' <param name="lpvParam">depends on system parameter</param>
''' <param name="fuWinIni">WIN.INI update flag</param>
''' <returns></returns>
''' <remarks></remarks>
<
DllImport("user32", CharSet:=CharSet.Auto)> _
Shared Function SystemParametersInfo(
          
ByVal uAction As Integer,
          
ByVal uparam As Integer,
          
ByRef lpvParam As Integer,
          
ByVal fuWinIni As Integer) As Integer
End Function

Get like this:

Protected Overrides Sub OnStart(ByVal args() As String)
    SystemParametersInfo(SPI_GETKEYBOARDDELAY, 0, Me.repeatDelay, 0)
    SystemParametersInfo(SPI_GETKEYBOARDSPEED, 0,
Me.repeatRate, 0)
End Sub

ServiceBase also provides OnPowerEvent() to override:

Protected Overrides Function OnPowerEvent(powerStatus As System.ServiceProcess.PowerBroadcastStatus) As Boolean
   
If powerStatus = ServiceProcess.PowerBroadcastStatus.PowerStatusChange OrElse
       powerStatus = ServiceProcess.
PowerBroadcastStatus.ResumeSuspend Then
       ApplyKeyboardSettings()
   
End If
   
Return MyBase.OnPowerEvent(powerStatus)
End Function

(Another option, here or in WinForms, is to use AddHandler Microsoft.Win32.SystemEvents.PowerModeChanged, AddressOf PowerEventHandler -- This works for coming out of suspend, not sure about undock, though.)

When the laptop comes out of suspend, or undocked (or unplugged, plugged , etc -- maybe too many things when on power but I don't know how to just filter this to docking and sleep events) I re-apply the desired settings:

Private Sub ApplyKeyboardSettings()
    SystemParametersInfo(SPI_SETKEYBOARDDELAY,
Me.repeatDelay, notUsed, 0)
    SystemParametersInfo(SPI_SETKEYBOARDSPEED,
Me.repeatRate, notUsed, 0)
End Sub

Allow Service to Interact with Desktop
Normally, this would be enough, but it turns out SPI_SET.... doesn't work unless the service can "interact with desktop".  There inexplicably seems to be no way to configure your service to install with this option (!?!?)  You can do it manually:image but that's not good enough is it :)

Googling found two solutions, one using the registry and another using WMI -- the WMI method seemed less of a hack:

   ''' <summary>allow service to interact with desktop
  
''' </summary>
  
''' <remarks>Other alternative appear to be manually, or through registry</remarks>
  
Public Shared Sub DesktopPermissions()
     
Try
        
Dim controller As New ServiceController("TameKeyBoardService")
        
Dim options As New ConnectionOptions
         options.Impersonation =
ImpersonationLevel.Impersonate

        
' CIMV2 is a namespace that contains all of the core OS and hardware classes.
        
' CIM (Common Information Model) which is an industry standard for describing
        
'        data about applications and devices so that administrators and software
        
'        management programs can control applications and devices on different
        
'        platforms in the same way, ensuring interoperability across a network.

        
Dim mgmtScope As New ManagementScope("root\CIMV2", options)
        
Dim wmiService As New ManagementObject("Win32_Service.Name='" & "TameKeyBoardService" & "'")
        
Dim inParam As ManagementBaseObject = wmiService.GetMethodParameters("Change")
         inParam(
"DesktopInteract") = True
        
Dim outParam As ManagementBaseObject = wmiService.InvokeMethod("Change", inParam, Nothing)
         controller.Start()
'-- e.g., if called from ProjectInstaller_AfterInstall()
     
Catch ex As Exception
     
End Try
  
End Sub

This has to be done before the service starts, so I call it right after the service is installed:

Public Class ProjectInstaller
   Private Sub ProjectInstaller_AfterInstall(sender As Object, e As System.Configuration.Install.InstallEventArgs) Handles Me.AfterInstall
     
TameKeyboardService.DesktopPermissions()
  
End Sub
End Class

That pretty much covers the service, next create a setup program to install it....

Setup Project
Add a new project to the solution using the Setup project template. Right-click on the setup project and "Add New Project Output...".  Pick Primary Output and the service project (they are probably already selected), hit OK.  Then, you need a Custom Action to install your service (I'll just paste this from the MSDN walkthrough):

To add a custom action to the setup project

  1. In Solution Explorer, right-click the setup project, point to View, and then click Custom Actions.

    The Custom Actions editor appears.

  2. In the Custom Actions editor, right-click the Custom Actions node and click Add Custom Action.

    The Select Item in Project dialog box appears.

  3. Double-click the Application Folder in the list to open it, select Primary Output from TameKeyboard (Active), and click OK.

    The primary output is added to all four nodes of the custom actions — Install, Commit, Rollback, and Uninstall.

  4. In Solution Explorer, right-click the Setup1 project and click Build.

To install the Windows Service

  1. To install TameKeyboardService.exe, right-click the setup project in Solution Explorer and select Install.

  2. Follow the steps in the Setup Wizard. Build and save your solution.

 

Source Code, Setup.msi
Still looking for the right place for source, exe, msi, and zips; currently you can find this project on my SkyDrive: (the setup msi will expect .Net 4.0, forgot to target it lower)

It has a lot of qualities of a made-for-personal-use quick one-off, but I learned some interesting things getting it to work.

Version 2

The original tried to track your preference and continuously maintain those settings even if Dell changed them.  Sometimes that might not be enough, e.g., if Dell manages to change them before the service starts tracking.  In this case, try this one, it always sets the delay to 0 -- that works for me and anyone else who is happy with no delay.

UPDATE: Keyboard Fix 2

8 comments:

  1. After a while this version quit working. I think during startup the delay got changed before the service started, so I made a new "dumber" version that always sets zero delay -- no problems with this one yet.

    ReplyDelete
  2. Hey Jon,

    Saw your post on the MS Technet forum (http://social.technet.microsoft.com/Forums/en/w7itproui/thread/98b338cb-6f37-451d-9e32-5f6bdac9d436). I'm hazymat on that forum...

    Just installed KeyboardFix2, but still no joy :(

    A little refresher: when I dock *or* undock my Dell laptop (which is a newish model - Latitude 6510), my keyboard repeat rate and delay both slow down. In order to set them back to the fastest, I have to go into keyboard settings. However it doesn't actually require I move the sliders: I only have to press OK and they are set back to normal!

    Is there anything else I need to do to make your little service work?

    ReplyDelete
    Replies
    1. That is the problem I had too and this should fix it. If you can confirm it is running (e.g., TameKeyboard.exe in Task Manager) I can look into it some more after work.
      Does it ever seem to work, e.g., if you plug and unplug the AC adapter, sleep/un-sleep or any power change besides dock/undock?

      Delete
    2. The service is definitely running in services.msc, and also the process shows up in task manager.

      So the keyboard slowdown only happens when I dock-undock. It doesn't happen if I sleep-resume, unplug-replug.

      Interestingly, if I undock then sleep-resume, the keyboard settings do return back to normal. However I have tested doing that with the tamekeyboard service stopped, and it still does that, therefore I'm concluding that the tamekeyboard service isn't working for me for some reason.

      I've tried unplugging / plugging-in the AC adapter too - doesn't make any difference.

      Thanks for this. Surely there must be hundreds of thousands of other people this affects too? It's happened with literally every Dell laptop and docking station I've ever owned over the last 10 years!

      Delete
  3. I tested this on my Precision M6400 (running Windows 7) and, unfortunately, it does not appear to be working. After resuming from standby, the problem returned, as usual. Both the executable file and service were both seen running as they should be. The problem manifests both with and without the dock. Any ideas?

    ReplyDelete
  4. Sorry, my M6400 is long gone. I'll take a look at the code to refresh my memory, but no guarantees...

    ReplyDelete
  5. With great interest I read your suggestion. And tried out your app. It installed, but when I click the TameKeyboard.exe, I get an error message:

    [quote]
    Cannot start service from the command line or a debugger. A Windows Service must first be installed (etc.)
    [/quote]

    Does this mean that to use your app one must first be able to make/install a service as your are describing in your article? If so, is there no solution for users who are not such programmers?

    ReplyDelete
  6. After a rather long period of observations, it seems that KeyboardFix2.msi is working. But could it be that it doesn't reset it to the highest repeat rate possible, which is 31?

    ReplyDelete