Getting macro keys from a Razer BlackWidow to work on Linux

I picked up a Razer BlackWidow Ultimate that has additional keys meant for macros that are set using a tool that's installed on Windows. I'm assuming that these aren't some fancypants joojoo keys and should emit scancodes like any other keys.

Firstly, is there a standard way to check these scancodes in Linux? Secondly, how do I set these keys to do things in command line and X-based Linux setups? My current Linux install is Xubuntu 10.10, but I'll be switching to Kubuntu once I have a few things fixed up. Ideally the answer should be generic and system-wide.

Things I have tried so far:

  • showkeys from the built in kbd package (in a seperate vt) - macro keys not detected

  • xev - macro keys not detected

  • contents of /dev/input/by-path as well as lsusb and evdev output

  • This ahk script's output suggests the M keys are not outputting standard scancodes detectable by windows

Things I need to try

  • snoopy pro + reverse engineering (oh dear)

  • Wireshark - preliminary futzing around seems to indicate no scancodes emitted when what I seem to think is the keyboard is monitored and keys pressed. Might indicate additional keys are a separate device or need to be initialised somehow.

  • Need to cross reference that with lsusb output from Linux, in three scenarios: standalone, passed through to a Windows VM without the drivers installed, and the same with.

  • LSUSB only detects one device on a standalone Linux install

  • It might be useful to check if the mice use the same Razer Synapse driver , since that means some variation of razercfg might work (not detected, only seems to work for mice)

Things I have worked out:

  • In a Windows system with the driver, the keyboard is seen as a keyboard and a pointing device. The pointing device uses - in addition to your bog standard mouse drivers - a driver for something called a Razer Synapse.

  • Mouse driver seen in Linux under evdev and lsusb as well

  • Single device under OS X apparently, though I have yet to try lsusb equivalent on that

  • Keyboard goes into pulsing backlight mode in OS X upon initialisation with the driver. This should probably indicate that there's some initialisation sequence sent to the keyboard on activation.

  • They are, in fact, fancypants joojoo keys.

Extending this question a little:

I have access to a Windows system so if I need to use any tools on that to help answer the question, it's fine. I can also try it on systems with and without the config utility. The expected end result is still to make those keys usable on Linux however.

I also realise this is a very specific family of hardware. I would be willing to test anything that makes sense on a Linux system if I have detailed instructions - this should open up the question to people who have Linux skills, but no access to this keyboard.

The minimum end result I require:

I need these keys detected, and usable in any fashion on any of the current graphical mainstream Ubuntu variants, and naturally have to work with my keyboard. Virtual cookie and mad props if it's something nicely packaged and usable by the average user.

I will require compiled code that will work on my system, or a source that I can compile (with instructions if it's more complex than ./configure , make, make install) if additional software not on the Ubuntu repositories for the current LTS or standard desktop release at the time of the answer. I will also require sufficient information to replicate, and successfully use the keys on my own system.


M1-M5 are in fact regular keys - they just need to be specifically enabled before pressing them will generate a scancode. tux_mark_5 developed a small Haskell program which sends the correct SET_REPORT message to Razer keyboards to enable these keys, and ex-parrot ported the same code to Python.

On Arch Linux systems the Python port has been packaged and is available from https://aur.archlinux.org/packages.php?ID=60518.

On Debian or Ubuntu systems setting up the Python port of the code is relatively easy. You need to install PyUSB and libusb (as root):

    aptitude install python-usb

Then grab the blackwidow_enable.py file from http://finch.am/projects/blackwidow/ and execute it (also as root):

    chmod +x blackwidow_enable.py
    ./blackwidow_enable.py

This will enable the keys until the keyboard is unplugged or the machine is rebooted. To make this permanent call the script from whatever style of startup script you most prefer. For instructions on how to set this up in Debian have a look at the Debian documentation.

To use tux_mark_5's Haskell code you'll need to install Haskell and compile the code yourself. These instructions are for a Debian-like system (including Ubuntu).

  1. Install GHC, libusb-1.0-0-dev and cabal (as root):

    aptitude install ghc libusb-1.0-0-dev cabal-install git pkg-config
    
  2. Fetch the list of packages:

    cabal update
    
  3. Install USB bindings for Haskell (no need for root):

    cabal install usb
    
  4. Download the utility:

    git clone git://github.com/tuxmark5/EnableRazer.git
    
  5. Build the utility:

    cabal configure
    cabal build
    
  6. Run the utility (also as root):

    ./dist/build/EnableRazer/EnableRazer
    

After this you can copy EnableRazer binary anywhere you want and run it at startup.

Immediately after execution, X server should see M1 as XF86Tools, M2 as XF86Launch5, M3 as XF86Launch6, M4 as XF86Launch7 and M5 as XF86Launch8. Events for FN are emitted as well.

These keys can be bound within xbindkeys or KDE's system settings to arbitrary actions.

Since your keyboard might be different, you might need to change the product ID in Main.hs line 64:

withDevice 0x1532 0x<HERE GOES YOUR KEYBOARD's PRODUCT ID> $ \dev -> do

Razer seems to be forcing their cloud-based Synapse 2 configurator on all users nowadays, with accompanying firmware upgrade to version 2.*. Once you have upgraded the firmware, you cannot go back (keyboard is completely bricked if you try to flash it with older firmware).

The ‘magic bytes’ from the Haskell program in tux_mark_5's answer won't work with the latest firmware. Instead, the driver sends these bytes during the initialization sequence: ‘0200 0403’. These enable the macro keys, but the keyboard enters a peculiar mode in which instead of the standard HID protocol it sends 16-byte packets (presumably to increase the number of keys that can be pressed simultaneously). Linux HID system cannot quite cope with this, and while most keys work as expected, the macro keys stay unrecognized: the HID driver doesn't feed any data to the input layer when they are pressed.

To make your keyboard enter the legacy mode (in which the macro keys send XF86Launch* keycodes, and the FN key sends keycode 202), send these bytes: 0200 0402.

The full packet will be:

00000000 00020004 02000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000
00000000 00000000 0400

Here's a very rough and dirty program I wrote in less esoteric Python 3 to perform the task. Note the code to generate the Razer control packets in blackwidow.bwcmd() and the Razer logo LED commands as a bonus :)

#!/usr/bin/python3

import usb
import sys

VENDOR_ID = 0x1532  # Razer
PRODUCT_ID = 0x010e  # BlackWidow / BlackWidow Ultimate

USB_REQUEST_TYPE = 0x21  # Host To Device | Class | Interface
USB_REQUEST = 0x09  # SET_REPORT

USB_VALUE = 0x0300
USB_INDEX = 0x2
USB_INTERFACE = 2

LOG = sys.stderr.write

class blackwidow(object):
  kernel_driver_detached = False

  def __init__(self):
    self.device = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)

    if self.device is None:
      raise ValueError("Device {}:{} not found\n".format(VENDOR_ID, PRODUCT_ID))
    else:
      LOG("Found device {}:{}\n".format(VENDOR_ID, PRODUCT_ID))

    if self.device.is_kernel_driver_active(USB_INTERFACE):
      LOG("Kernel driver active. Detaching it.\n")
      self.device.detach_kernel_driver(USB_INTERFACE)
      self.kernel_driver_detached = True

    LOG("Claiming interface\n")
    usb.util.claim_interface(self.device, USB_INTERFACE)

  def __del__(self):
    LOG("Releasing claimed interface\n")
    usb.util.release_interface(self.device, USB_INTERFACE)

    if self.kernel_driver_detached:
      LOG("Reattaching the kernel driver\n")
      self.device.attach_kernel_driver(USB_INTERFACE)

    LOG("Done.\n")

  def bwcmd(self, c):
    from functools import reduce
    c1 = bytes.fromhex(c)
    c2 = [ reduce(int.__xor__, c1) ]
    b = [0] * 90
    b[5: 5+len(c1)] = c1
    b[-2: -1] = c2
    return bytes(b)

  def send(self, c):
    def _send(msg):
      USB_BUFFER = self.bwcmd(msg)
      result = 0
      try:
        result = self.device.ctrl_transfer(USB_REQUEST_TYPE, USB_REQUEST, wValue=USB_VALUE, wIndex=USB_INDEX, data_or_wLength=USB_BUFFER)
      except:
        sys.stderr.write("Could not send data.\n")

      if result == len(USB_BUFFER):
        LOG("Data sent successfully.\n")

      return result

    if isinstance(c, list):
      #import time
      for i in c:
        print(' >> {}\n'.format(i))
        _send(i)
        #time.sleep(.05)
    elif isinstance(c, str):
        _send(c)

###############################################################################

def main():
    init_new  = '0200 0403'
    init_old  = '0200 0402'
    pulsate = '0303 0201 0402'
    bright  = '0303 0301 04ff'
    normal  = '0303 0301 04a8'
    dim     = '0303 0301 0454'
    off     = '0303 0301 0400'

    bw = blackwidow()
    bw.send(init_old)

if __name__ == '__main__':
    main()