Sunday, June 9, 2013

Logitech G13 as Keyboard+Mouse Replacement

I recently spent some time looking into computer interface devices for a 9 year old who only has use of his left hand.  The kids his age are really into Minecraft, a game that typically requires two hands whether played on the PC or Xbox 360.  I figured that this was one activity that he most certainly could enjoy with his peers; so I got to work.  It gave me a chance to play with some new toys and tinker with modding/coding so it was a win-win!

I looked into a few different devices.  If you want to skip the rest of this article and just start researching potential solutions here's some of the options I considered:

  • Keypad Gaming Mice (Razer Naga and Logitech G600)
  • Gamepads
  • Modified Controllers
  • Motion Capture Controllers
  • Joysticks
  • Leap Motion
  • IR camera +  pupil tracking

I haven't tried out a Leap Motion yet, I think they have the potential to be great for those with limited motor control.   I may apply for a development kit if they're still available.

I eventually decided that a Keypad Mouse like the Razer Naga or Logitech G600 would work best.  Sadly neither company has a left handed version yet (Razer announced they would make a left handed version in March 2013).  Instead I picked up a Logitech G13 gamepad.  I went with Logitech over the Belkin and Razer options because it is the only gamepad I found with a true analog thumbstick.  The other gamepads have a Directional-Pad (either 8 way or 4 way).  The analog stick seemed to be the better option for replacing mouse control.

I recorded a few of the setups I tried out.  They can be seen in the video below.



I started playing with Lua scripting to create more complex behavior than can be achieved with the standard Logitech macro editor.  The only experience I'd had with Lua was some game add-on debugging a few years ago, so it was a bit of a slow start.  One of my first tasks was to emulate a mouse wheel using the two keys near the thumbstick.  My first attempt resulted in the code below which uses Logitech's built in event handler.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
KEYMAP = {
     [23] = function(n) fn_WHEEL_UP(n) end,
     [24] = function(n) fn_WHEEL_DOWN(n) end,
}

REPEAT_DELAY = 100

function OnEvent(event, arg, family)
     if event=="PROFILE_ACTIVATED" then 
          ClearLog()
          OutputLogMessage("Script Started.\n")
          wheel_up = false
          wheel_down = false
     end
     if family == "lhc" then
          local action = KEYMAP[arg]
          if type(action) == "function" then 
               action(event)
          end
     end
     if event=="M_PRESSED" and wheel_up then
          RepeatWheel(1)
     elseif event=="M_PRESSED" and wheel_down then
          RepeatWheel(-1)
     end
end

function fn_WHEEL_UP(event)
     if event == "G_PRESSED" then
          wheel_up = true
          RepeatWheel(1)
     elseif event == "G_RELEASED" then
          wheel_up = false
          MoveMouseWheel(0)
     end
end

function fn_WHEEL_DOWN(event)
     if event == "G_PRESSED" then
          wheel_down = true
          RepeatWheel(-1)
     elseif event == "G_RELEASED" then
          wheel_down = false
          MoveMouseWheel(0)
     end
end

function RepeatWheel(movement)
     OutputLogMessage("Moving Wheel: %d\n", movement)
     MoveMouseWheel(movement)
     OutputLogMessage("Delaying Wheel DOWN: %d\n", REPEAT_DELAY)
     Sleep(REPEAT_DELAY)
     SetMKeyState(GetMKeyState("lhc"),"lhc")
end

This code uses setting of MKeyState to repeatedly fire events while the key is held down.  It works, but it runs into event pileup problems with sleep delays larger than a few milliseconds.  Trying to mouse wheel up and down in quick succession will result in a queue of events and a laggy user experience.  Some helpful people in the Logitech support forums pointed me towards llProject, a Logitech device polling library which solves a lot of common problems and allows for some pretty complex macros.

The llProject library was very well documented and easy to figure out.  I was able to create an emulated mouse wheel with a very nice feel.  I set it up to do an immediate tick of the wheel, followed by a large intitial delay before going into a steady repeat as long as the key is pressed.  Instead of sleeping for a delay, preventing all queued tasks from executing, llProject routines yield activity and the routine manager continually checks to see if the necessary delay has passed before continuing the yielding process.  This allows some routines to continue while delaying others.  Here's the code I ended up with:



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
--------------------->
ll=ll or {}; ll.SETUP = { Folder = [[C:\Logitech Scripts\llProject\]] }
dofile(ll.SETUP.Folder .. [[llProject.lua]])
-- llProject End -->

M_PROFILE = 0
INITIAL_DELAY = 300
REPEAT_DELAY = 100

function _onActivation()
     ll.lhc.map[M_PROFILE] = {
          [23] = fn_WHEEL_UP,
          [24] = fn_WHEEL_DOWN
     }
     initialUpTick = ll.Routines:Create(fn_WHEEL_TICK,1,INITIAL_DELAY)
     repeatUpTick = ll.Routines:Create(fn_WHEEL_TICK,1,REPEAT_DELAY)
     initialDownTick = ll.Routines:Create(fn_WHEEL_TICK,-1,INITIAL_DELAY)
     repeatDownTick = ll.Routines:Create(fn_WHEEL_TICK,-1,REPEAT_DELAY)
end

function fn_WHEEL_UP()
     if ll.Event.Pressed then
          initialUpTick:Restart()
          return
     end
     if ll.Event.Down and initialUpTick:IsDead() and not repeatUpTick:IsRunning() then
          repeatUpTick:Run()
     end
     return
end

function fn_WHEEL_DOWN()
     if ll.Event.Pressed then
          initialDownTick:Restart()
          return
     end
     if ll.Event.Down and initialDownTick:IsDead() and not repeatDownTick:IsRunning() then
        repeatDownTick:Run() 
     end
     return
end

function fn_WHEEL_TICK(direction, delay)
     MoveMouseWheel(direction)
     Sleep(delay)
end


Feeling the mouse emulation and key layout were as good as I was going to get them, I moved towards physically modifying the G13 for easier use.  I found the original G13 thumbstick to be uncomfortable and difficult to use.  It is narrow enough that your thumb can very easily slide off if it isn't perfectly centered.  Like many others, I decided to replace it with the thumbstick from a game controller.  Xbox 360 thumbsticks are nearly a perfect fit.  The only modifications necessary are widening the G13 case's thumbstick pass-through, and narrowing the 360 thumbstick's mounting hole.

I didn't have any epoxy to fill the mounting hole, so instead I drilled it out wider and glued in a section of 3/8th inch dowel.  I then drilled out and lengthened a new mounting hole with a 1/64th inch drill bit.

The thumb portion and the support peg of the 360 thumbstick are much wider than the original.  In order to achieve the same range of motion (and get the peg through the top of the case) you have to widen the case's thumbstick pass-through hole.  I started out trying to do this with a dremel, but found it cut too unevenly and was much too aggressive.  I gave up on the dremel and instead drilled the hole out to 3/4 inch with a Forstner bit.  After cleaning up the cut with some 200 and 400 grit sand paper I ended up with a nice clean edge as seen in the images below.