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]
Out[4]:
['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 usesysctl
orioctl
, 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 inrazer_send_control_msg
)request_type
= 0x21 (set inrazer_send_control_msg
)value
= 0x300 (set inrazer_send_control_msg
)report_index
= 0x01 (set inrazer_get_report
)buf
is a 90-byte data structure covered below.size
= 0x90 (set inrazer_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 inrazer_attr_write_mode_static
.remaining_packets
is always 0x0000.protocol_type
is always 0x00.data_size
is set to 0x09 inrazer_chroma_extended_matrix_effect_static
command_class
is set to 0x0F inrazer_chroma_extended_matrix_effect_base
command_id
is set to 0x02 inrazer_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
:
- 0x01 (name: VARSTORE)
- 0x05 (name: BACKLIGHT_LED)
- 0x01
- 0x00
- 0x00
- 0x01
- red byte
- green byte
- 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:
bmRequestType
↔request_type
bRequest
↔request
wValue
↔value
wIndex
↔report_index
data
↔buf
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
- Have Python log to syslog
- Attempting a conky replacement in Python (part 2)
- Attempting a conky replacement in Python (part 1)
- Updating python3 to 3.6
- Switching IPython to Python 3