Index ¦ Archives  ¦ Atom  ¦ RSS

G13

I got a G13 recently, but not for the usual use-case. Instead of gaming, of which I do very little, I bought it for speeding up my own workflow both at home and at work. Since I'm not using it for the expected use-case by Logitech, their software obviously didn't cut it, not just because it's Windows-only and I run Linux at work.

Therefore, I spent some time with the various G13-interacting code that's already written on the interwebs and wrote my own Python code using libusb1/libusbx for it. I'll describe what was already out there to explain why I had to write my own.

First, there's the C library at sourceforge that's actually pretty extensive, but it's Linux-only and I run Windows at home (as well as Linux), so I couldn't use it. That library has an svn version that's a library and a git version that's a kernel module, neither of which help my cross-OS needs, though it's impressive work by rvinyard. I would probably prefer that if I didn't want to run on Windows too, though I'd have to write Python/SWIG bindings to benefit from it since, while I can write C, I prefer my prototype work in Python, especially since the G13 doesn't need the speed afforded by C.

Second, there's a Google code project 'linux-g13-driver' that's also Linux-specific, but also missing control of the LCD module. The USB code is in C while the UI is in Java, and is also impressive work by jim.gupta, though this code is a lot messier (sorry Jim!).

With these options and my requirements, I set out to write a libusb-based wrapper that could control the lights and LCD and get the key state, which is all the G13 does. Thankfully, having these two projects as code samples really sped up the process so I was able to do it in less than a weekend's time. It actually was so quick, I haven't gotten around to posting this for over a month.

My code uses python-libusb1 for the libusb1<->Python bindings, which is a rather convenient wrapper. It uses ctypes so it takes a second to initialize, but my code runs as a long-living process so I can take the hit; it was annoying during iteration though, I had to put print statements around the imports to make sure my code wasn't the culprit.

The resulting hardware-interacting file is a single class and weighs only 75 easy-to-understand lines and can read the key state to return a collections.namedtuple of the keys and joystick values, set the macro/mode LED state, set the backlight color, set individual pixels on the LCD and flush the LCD data to the device. Those latter two are separate because writing to the device takes a few ms and it allows a sort of double-buffering, one on the device and one in memory. The LCD state is stored as a bytearray of length 992 (160 pixels across, 43 down, and the way the pixels are stored, the 43 down becomes 6 bytes, plus 32 bytes at the front for a magic number).

I'm pasting the code as-is here, and also putting it on github so others can use it. I've written a small terminal UI program for testing that's about 200 lines and shows the key states and joystick position as a curses-esque UI with lots of terminal escape codes.

g13.py:

import collections
import platform

import libusb1
import usb1

G13_KEY_BYTES = collections.namedtuple('G13_KEY_BYTES', [
    'stick_x', 'stick_y', 'keys'])

class G13(object):
  VENDOR_ID = 0x046d
  PRODUCT_ID = 0xc21c
  INTERFACE = 0
  MODE_LED_CONTROL = 0x301 # Could be 0x302?
  COLOR_CONTROL = 0x301 # Could be 0x307?
  KEY_ENDPOINT = 1
  REPORT_SIZE = 8
  REQUEST_TYPE = libusb1.LIBUSB_TYPE_CLASS | libusb1.LIBUSB_RECIPIENT_INTERFACE

  def __init__(self):
    # 160 across and 43 down (6 bytes down)
    self.pixels = bytearray(992)
    self.pixels[0] = 3

  def open(self):
    self.ctx = usb1.USBContext()
    dev = self.ctx.getByVendorIDAndProductID(self.VENDOR_ID, self.PRODUCT_ID)

    self.handle = dev.open()
    if platform.system() == 'Linux' and \
            self.handle.kernelDriverActive(self.INTERFACE):
        self.handle.detachKernelDriver(self.INTERFACE)

    self.handle.claimInterface(self.INTERFACE)

    # interruptRead -> R
    # controlWrite -> Out

  def close(self):
    self.handle.releaseInterface(self.INTERFACE)
    self.handle.close()
    self.ctx.exit()

  def get_keys(self):
    data = self.handle.interruptRead(
        endpoint=self.KEY_ENDPOINT, length=self.REPORT_SIZE, timeout=100)
    keys = map(ord, data)
    keys[7] &= ~0x80 # knock out a floating-value key
    return G13_KEY_BYTES(keys[1], keys[2], keys[3:])

  def set_mode_leds(self, mode):
    data = ''.join(map(chr, [5, mode, 0, 0, 0]))
    self.handle.controlWrite(
        request_type=self.REQUEST_TYPE, request=9,
        value=self.MODE_LED_CONTROL, index=0, data=data,
        timeout=1000)

  def set_color(self, color):
    data = ''.join(map(chr, [7, color[0], color[1], color[2], 0]))
    self.handle.controlWrite(
        request_type=self.REQUEST_TYPE, request=9,
        value=self.COLOR_CONTROL, index=0, data=data,
        timeout=1000)

  def write_lcd(self):
    self.handle.interruptWrite(endpoint=2, data=str(self.pixels), timeout=1000)

  def set_pixel(self, x, y, val):
    x = min(x, 159)
    y = min(y, 43)
    idx = 32 + x + (y/8)*160
    if val:
      self.pixels[idx] |= 1 << (y%8)
    else:
      self.pixels[idx] &= ~(1 << (y%8))

ui_example.py

:::python import sys import threading import time

import usb1
import libusb1

from g13 import G13

G13_KEYS = [ # Which bit should be set
    # /* byte 3 */
    'G01',
    'G02',
    'G03',
    'G04',

    'G05',
    'G06',
    'G07',
    'G08',

    # /* byte 4 */
    'G09',
    'G10',
    'G11',
    'G12',

    'G13',
    'G14',
    'G15',
    'G16',

    # /* byte 5 */
    'G17',
    'G18',
    'G19',
    'G20',

    'G21',
    'G22',
    'UN1', # 'UNDEF1',
    'LST', # 'LIGHT_STATE',

    # /* byte 6 */
    'BD',
    'L1',
    'L2',
    'L3',

    'L4',
    'M1',
    'M2',
    'M3',

    # /* byte 7 */
    'MR',
    'LFT',
    'DWN',
    'TOP',

    'UN2', # 'UNDEF2',
    'LT1', # 'LIGHT',
    'LT2', # 'LIGHT2',
    # 'MISC_TOGGLE',
]

class TerminalUI(object):
  BASE_X, BASE_Y = 0, 10
  scale_x, scale_y = 64, 32

  def __init__(self):
    self.prev_x, self.prev_y = 0, 0
    self.prev_keys = {k: 1 for k in G13_KEYS}

  def init_stick(self):
    self.reset()
    sys.stdout.write('-'*(self.scale_x+2) + '\n')
    for l in range(self.scale_y):
      sys.stdout.write('|' + ' ' * self.scale_x + '|\n')
    sys.stdout.write('-'*(self.scale_x+2) + '\n')

  def reset(self):
    self.goto(0, 0)
  def goto(self, x, y):
    sys.stdout.write('\033[%s;%sH' % (self.BASE_Y + y, self.BASE_X + x))
  def down(self, num):
    sys.stdout.write('\033[%sB' % num)
  def right(self, num):
    sys.stdout.write('\033[%sC' % num)
  def print_at(self, x, y, s):
    self.goto(x + 1, y + 1)
    sys.stdout.write(s)
  def flush(self):
    sys.stdout.flush()

  def print_stick(self, x, y):
    x /= 4
    y /= 8

    self.print_at(self.prev_x + 1, self.prev_y, ' ')
    self.print_at(x + 1, y, 'x')

    self.prev_x, self.prev_y = x, y

  def clear_keys(self):
    if not any(self.prev_keys.values()):
      return
    for i in range(5):
      self.print_at(0, self.scale_y + 1 + i, ' '*(4*8))

  def set_key(self, key, value):
    if self.prev_keys[key] != value:
      idx = G13_KEYS.index(key)
      y = self.scale_y + 1 + idx / 8
      x = (idx % 8) * 4
      if value:
        out = key
      else:
        out = '    '
      self.print_at(x, y, out)

    self.prev_keys[key] = value

class G13UI(object):
  def __init__(self, g13):
    self.prev_x, self.prev_y = 0, 0
    self.g13 = g13

  def print_block(self, x, y, val):
    self.g13.set_pixel(x, y, val)
    self.g13.set_pixel(x+1, y, val)
    self.g13.set_pixel(x, y+1, val)
    self.g13.set_pixel(x+1, y+1, val)

  def print_stick(self, x, y):
    x = x * 158 / 255
    y = y *  41 / 255
    self.print_block(self.prev_x, self.prev_y, 0)
    self.print_block(x, y, 1)
    self.prev_x, self.prev_y = x, y

class G13Wrapper(G13):
  def __init__(self):
    super(G13Wrapper, self).__init__()
    self.lock = threading.Lock()

  def write_lcd_bg(self):
    threading.Thread(target=self.write_lcd).start()

  def write_lcd(self):
    if self.lock.acquire(False):
      super(G13Wrapper, self).write_lcd()
      self.lock.release()

def main(argv):
  g13 = G13Wrapper()
  g13.open()

  g13.set_mode_leds(int(time.time() % 16))
  g13.set_color((255, 0, 0))
  time.sleep(0.1)
  g13.set_mode_leds(int(time.time() % 16))
  g13.set_color((255, 255, 255))

  ui = TerminalUI()
  ui.init_stick()
  g13ui = G13UI(g13)

  try:
    while True:
      try:
        keys = g13.get_keys()

        g13ui.print_stick(keys.stick_x, keys.stick_y)
        ui.print_stick(keys.stick_x, keys.stick_y)

        parse_keys(ui, keys)

        ui.flush()
        write_lcd_bg(g13)
      except libusb1.USBError as e:
        if e.value == -7:
          pass
  except Exception as e:
    print e
  except KeyboardInterrupt:
    print '^C'

  g13.close()

def parse_keys(ui, keys):
  if not any(keys.keys):
    ui.clear_keys()
    return

  for i, key in enumerate(G13_KEYS):
    b = keys.keys[i/8]
    ui.set_key(key, b & 1 << (i%8))

if __name__ == '__main__':
  main(sys.argv[1:])

I'll update again when I get the rest of the code written. I hope to create a stateful system that updates the LCD with the current key state and the color with the current application state, both of which affect what the next key action (press or release) does. For instance, if the current application is Chrome, pressing and releasing G1-G7 might choose a tab 1-7 (using Ctrl-#) while G20 goes left a tab, G22 right a tab, and G20&G21&G22 closes the current tab (the bottom row). All of these change when using PuTTy or Gnome Terminal (depending on OS) and when in tmux or screen, when G1-G19 chooses a screen/window (top two rows), the bottom joystick key goes to the previous screen, holding the left joystick button and hitting two window keys (G1-G19) swaps them, etc.

Instead of writing each of these into some large behemoth of a state machine, I will be writing a framework for plugins to create and enter/exit a stack of states. The Chrome plugin will be triggered by an OS notification, polling or an OS-level key that checks the currently focused window, it will then exit all states and enter the 'Chrome' state. Pressing and releasing G1-G7 is straightforward, while pressing G20 and G22 enter a 'Tab' state that pressing G21 exits after closing a tab or releasing G20 or G22 exit after switching tabs.

© Fahrzin Hemmati. Built using Pelican. Theme by Giulio Fidente on github.