Roland's homepage

My random knot in the Web

Setting the Razer ornata chroma color from userspace

This documents how to set the (static) color on this keyboard from user space using libusb (built into FreeBSD) and pyusb.

Getting info

From dmesg, I see:

ugen1.2: <Razer Razer Ornata Chroma> at usbus1

So we turn to usbconfig.

> usbconfig -d 1.2 dump_device_desc
ugen1.2: <Razer Razer Ornata Chroma> at usbus1, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (500mA)

bLength = 0x0012
bDescriptorType = 0x0001
bcdUSB = 0x0200
bDeviceClass = 0x0000  <Probed by interface class>
bDeviceSubClass = 0x0000
bDeviceProtocol = 0x0000
bMaxPacketSize0 = 0x0040
idVendor = 0x1532
idProduct = 0x021e
bcdDevice = 0x0200
iManufacturer = 0x0001  <Razer>
iProduct = 0x0002  <Razer Ornata Chroma>
iSerialNumber = 0x0000  <no string>
bNumConfigurations = 0x0001

The vendor id is 0x1532 and the product id is 0x021e. This matches the source code for the linux driver.

Querying the device using pyusb

Let’s try if we can find the device using pyusb.

In [2]: import usb.core

In [3]: dev = usb.core.find(idVendor=0x1532, idProduct=0x021e)

 In [4]: str(dev).splitlines()[:15]
 ['DEVICE ID 1532:021e on Bus 001 Address 002 =================',
 ' bLength                :   0x12 (18 bytes)',
 ' bDescriptorType        :    0x1 Device',
 ' bcdUSB                 :  0x200 USB 2.0',
 ' bDeviceClass           :    0x0 Specified at interface',
 ' bDeviceSubClass        :    0x0',
 ' bDeviceProtocol        :    0x0',
 ' bMaxPacketSize0        :   0x40 (64 bytes)',
 ' idVendor               : 0x1532',
 ' idProduct              : 0x021e',
 ' bcdDevice              :  0x200 Device 2.0',
 ' iManufacturer          :    0x1 Razer',
 ' iProduct               :    0x2 Razer Ornata Chroma',
 ' iSerialNumber          :    0x0 ',
 ' bNumConfigurations     :    0x1']

This returns the same data as usbconfig, so all is well.

To port or not to port; the Linux openrazer driver

The openrazer kernel driver for Linux can be found on github. It supports a lot more devices than just my Ornata Chroma keyboard.

Initially I thought about porting this to FreeBSD. But in the end I decided against that, for several reasons:

  • The main purpose of openrazer is to expose the keyboard to Linux userspace via sysfs. That file system doesn’t exist on FreeBSD. I guess I could change it to use sysctl or ioctl, but that would be a complete rewrite.
  • FreeBSD comes with libusb, so we can control USB devices from user space. That means that a kernel driver isn’t really necessary.
  • Initially, I want to be able to set a static color. At the moment I do not require the other functionality that openrazer offers.
  • While I can work with C, I prefer to use Python 3 if possible.

Looking at the openrazer driver

My initial interest is in setting a static color. This is basically done by sending a control message using usb_control_msg. The usb_control_msg call is equivalent to the libusb_control_transfer function in FreeBSD. In pyusb it is handled by the ctrl_transfer method of a Device instance. The parameters of all these functions are similar.

To determine which parameters to use, we have to look at the openrazer driver. The call stack to set a static color is shown below:

razer_attr_write_mode_static [in driver/razerkbd_driver.c]
↳ razer_chroma_extended_matrix_effect_static [in driver/razerchromacommon.c],
   razer_send_payload [in driver/razerkbd_driver.c]
   ↳ razer_get_report [in driver/razerkbd_driver.c]
      ↳ razer_get_usb_response [in driver/razercommon.c]
         ↳ razer_send_control_msg [in driver/razercommon.c]
            ↳ usb_control_msg

The parameters in this case for usb_control_msg are:

  • request = 0x09 (set in razer_send_control_msg)
  • request_type = 0x21 (set in razer_send_control_msg)
  • value = 0x300 (set in razer_send_control_msg)
  • report_index = 0x01 (set in razer_get_report)
  • buf is a 90-byte data structure covered below.
  • size = 0x90 (set in razer_send_control_msg)

The following structure is sent in buf (see driver/razercommon.h):

struct razer_report {
    unsigned char status;
    union transaction_id_union transaction_id; /* */
    unsigned short remaining_packets; /* Big Endian */
    unsigned char protocol_type; /*0x0*/
    unsigned char data_size;
    unsigned char command_class;
    union command_id_union command_id;
    unsigned char arguments[80];
    unsigned char crc;/*xor'ed bytes of report*/
    unsigned char reserved; /*0x0*/

Initially, all values in this stucture are initialized to 0. The members of this structure for setting a constant color are:

  • status is always 0x00.
  • transaction_id is initialzed to 0x3F in razer_attr_write_mode_static.
  • remaining_packets is always 0x0000.
  • protocol_type is always 0x00.
  • data_size is set to 0x09 in razer_chroma_extended_matrix_effect_static
  • command_class is set to 0x0F in razer_chroma_extended_matrix_effect_base
  • command_id is set to 0x02 in razer_chroma_extended_matrix_effect_base
  • reserved is always 0x00.

The bytes of arguments are set as follows for a constant color in the call to razer_chroma_extended_matrix_effect_static:

  1. 0x01 (name: VARSTORE)
  2. 0x05 (name: BACKLIGHT_LED)
  3. 0x01
  4. 0x00
  5. 0x00
  6. 0x01
  7. red byte
  8. green byte
  9. blue byte

The checksum is generated by XOR-ing all the bytes from 2 up to 88. Creating such a message payload in Python is done as follows.

def static_color_msg(red, green, blue):
    msg = bytearray(90)
    # byte 0 is 0
    msg[1] = 0x3F  # see: razer_chroma_extended_matrix_effect_base
    # bytes 2-4 are 0.
    msg[5] = 0x09  # data_size
    msg[6] = 0x0F  # see razer_chroma_extended_matrix_effect_base
    msg[7] = 0x02  # see: razer_chroma_extended_matrix_effect_base
    # variable data
    msg[8] = 0x01  # VARSTORE, see razercommon.h
    msg[9] = 0x05  # BACKLIGHT_LED, see razercommon.h
    msg[10] = 0x01 # effect_id
    # bytes 11 and 12 are 0.
    msg[13] = 0x01  # see: razer_chroma_extended_matrix_effect_static
    msg[14] = red  # see: razer_chroma_extended_matrix_effect_static
    msg[15] = green  # see: razer_chroma_extended_matrix_effect_static
    msg[16] = blue  # see: razer_chroma_extended_matrix_effect_static
    # Calculate and set the checksum.
    crc = 0
    for i in msg[2:88]:
        crc ^= j
    msg[-2] = crc
    return msg

Sending such a message can be done as shown below. Note that the sequence of parameters is slightly different with pyusb compared to usb_control_msg.

The parameters are:

  • bmRequestTyperequest_type
  • bRequestrequest
  • wValuevalue
  • wIndexreport_index
  • databuf
  • timeout (unused, left at default)
import usb.core

dev = usb.core.find(idVendor=0x1532, idProduct=0x021e)
msg = static_color_msg(0, 0xff, 0)  # set color to Green.
dev.ctrl_transfer(0x21, 0x09, 0x300, 0x01, msg)

This proof of concept worked! Tested on 2019-06-16 with IPython.

Next I turned this into a script to set the keyboard to an arbitrary RGB color. You can find it on github in my scripts repository under the name set-ornata-chroma-rgb. It should work for the Ornata Chroma and Cynosa Chroma.

Update 2020-07-31: I wrote two GUI versions of this program. One using the tkinter toolkit that comes with python called tk-razer. And one using the GTK+ toolkit called gtk-razer.

For comments, please send me an e-mail.

Related articles

←  Using the FT232 with Python Repairing a USB flash drive  →