Gigabyte Aero W15 Keyboard and Linux (Ubuntu)

Introduction

TL;DR - Here is the GitHub link: https://github.com/paul-ridgway/aero-keyboard, which documents the protocol and has a few utility scripts and examples.

For the first time in a long while I have decided to get a personal laptop. After debating the costs of a new one I decided for the spec I was looking for getting something on eBay would save a fair bit (and in some cases come with active warranty).

That said, the spec I was looking for was not so common - high end i7/Xeon, a motherboard that could take 32GB RAM (if it did not have it already) and ideally two hard drives, plus a FHD (ideally non-gloss) screen (4k and linux don't mix well in my experience and touch is overrated, both of which seem to come as a package).

This leaves things like the Dell Precision 5520 (an excellent laptop in my experience) or 5530, Lenovo P1 and a few others that actually surface on eBay - few of which are available 'used' or 'refurbished', likely due their hefty (3k) price tag at full spec, and fewer were the full spec or upgradeable at a good cost. The Dells were hard to find with the extended battery too.

I started searching by spec rather than make/model and found the Gigabyte Aero series (some older and some newer). The Aero 15W is perfect - i7, space for two NVMe drives, FHD display and two RAM slots capable of 32GB. As a bonus the battery (along with the whole laptop) is reviewed well and it has actual ports(!) - including LAN, USB3 x 3, USB C, HDMI and Mini-DP. If that wasn't enough there were a couple available on eBay for a reasonable price.

Sorted.

Now, this laptop comes with a backlit keyboard, like most these days - however this one is a fancy RGB keyboard with effects and configurable mappings etc.

For Windows, Gigabyte provide all the drivers and software. However, I use Ubuntu for most things, and there is no support for the backlight options. It also seems some of the Fn control-keys are not supplied as normal keystrokes (like brightness controls - more on this later).

So, the challenge - support the keyboard at some level (not strictly driver level), ideally configuring the backlight and handling specical keys that provide material convenience, like the brightness controls.

For those Googling, it seems this is the same Keyboard as (at least some of) the Aorus-series laptops.

Steps

The following steps inspect the process of starting with a working configuation (the Keyboard on Windows) and reverse engineering the protocol to the point a demo app can be created that executes some of the functions.

Step 1: The Device

Inspecting hardware on Linux is often easily achieved with lshw, lspci and lsusb.

lspci showed Intel, NVIDIA, Realtek devices along with the hard drives.

There are two 'unknowns' in the lsusb output, Holtek and Sunplus.

paul@aero15 [19:44:49] [~] 
-> % lsusb
Bus 002 Device 002: ID 0bda:0316 Realtek Semiconductor Corp. 
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 003: ID 1bcf:2c6b Sunplus Innovation Technology Inc. 
Bus 001 Device 005: ID 04d9:8008 Holtek Semiconductor, Inc. 
Bus 001 Device 004: ID 8087:0a2b Intel Corp. 
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

The Sunplus device is the camera, and after some Googling Holtek is the Keyboard.

Searching the VID (04d9) and PID (8008) returned amongst other things this project's issue: https://github.com/openrazer/openrazer/issues/376, suggesting the device above is the Keyboard.

Step 2: Reverse Engineering

The keyboard presenting as a USB device is fortunate (to be honest, its not likely it would be much else), but it plays to prior experience, plus likely makes reverse engineering easier as the control messages are also sent over the USB channels.

Why? Because the official software is available for Windows, so I created a Windows 10 VM using VirtualBox and attached the keyboard device. Once installed on the VM the Gigabyte could successfully control the device.

At this point an external USB keyboard proved helpful because when the VM captures the USB device it is no longer available to the host, and while the touchpad was still accessible it to detatch and re-attach the device between the host and the VM, it was nice to avoid that overhead. Additionally, tinkering with HID libraries often cut off the keyboard if not done correctly...

Wireshark can capture USB traffic on both Windows and Linux - as far as I observed they both capture the same data, even when the device (keyboard) is attached to the VM, which for me made capture on Linux easier.

The external keyboard is also useful here because it means the traffic can be filtered to avoid control inputs (keyboard and mouse).

As part of my research I found another project to create a Windows SDK - https://github.com/Nesh108/MyAorusKeyboardSDK.

The reverse engineering strategy then split into using the Gigabyte App along with this project to create scenarios to understand the packets.

I modified the MyAorusKeyboardSDK to fabricate a rapidly changing battery level so that it was updating the keyboard more regularly.

My knowledge of USB is limited, with some experience working with HID in the past.

Devices are available on Busses, eg. /dev/bus/usb/001/004 where the bus is 1 and the device address is 4.

Devices have interfaces and endpoints (I won't pretend to understand all of this). But, with the Keyboard first noticed traffic going to 1.4.0 which I first presumed was Bus 1, Device 4, Endpoint 0:

Note the destination is 1.4.0, which contained the following data:

This message looks similar in structure to that documented on the MyAorusKeyboardSDK.

The code from the MyAorusKeyboardSDK project was hard to port directly as the terminology from the Windows HID Driver/SDK and methods does not relate to a lot of other documentation, which talks largely of feature reports, however there is a ruby project that focusses on USB HID, hidapi.

Looking through the code it seems the wIndex relates to the interface (see: https://github.com/barkerest/hidapi/blob/d4e8b7db3b0a742071b50fc282bdf8d7a3e062a7/lib/hidapi/device.rb#L385).

In the first tests, when connecting to interface 0 with libusb the keyboard would be unusuable and any IO would error.

Unfortunately with Wireshark I started by filtering on 1.4.0 thinking this was the only route the traffic took, however the SDK project which amongst other things does can fully customise the keyboard colours, highlighted that while the control commands are HID-based over 1.4.0 as sent feature reports, much of the mapping is sent as data using 1.4.6:

You can also see below the special function key presses for two brightness keys coming in via another route (1.4.6):

It seems interfaces have endpoints for different purposes (again not a USB expert), and the Feature Reports (control transfers) go via 1.4.0 whereas ad-hoc data (interrupts) goes via 1.4.6 for interface 3.

Connecting to interface 3 also fortunately did not prevent the keyboard from functioning normally, unlike interface 0 which required detatching the kernel access before connecting and re-attaching afterwards, which naturally could also be flimsy if the application crashed.

Step 3: A Test Project

Now knowing where to send data and having some example packets we can start trying to control the keyboard. The gist below is a crude example where the USB address is hard-coded, and uses some pre-defined packets (structure explained later) to cycle through a few keyboard colour configurations every 2 seconds. This uses the hidapi library as that nicely handles the sending of Feature Reports.

Here is the test script in action:

USB Permissions

Run as a normal user this will likely get access errors unless you change the premissions on your USB device, eg:

paul@aero15 [09:04:19] [~/Documents/Code/aero-keyboard] 
-> % sudo chmod o+w /dev/bus/usb/001/004

The bus and device are listed by the lsusb command:

Bus 001 Device 004: ID 04d9:8008 Holtek Semiconductor, Inc. 

As described on Stack Overflow and ask ubuntu you can add a udev rule to make this permanent:

Create a file /etc/udev/rules.d/60-holtec.rules with the contents:

SUBSYSTEM=="usb", ATTRS{idVendor}=="04d9", ATTR{idProduct}=="8008", MODE="0666"

Upon reloading the rules with sudo udevadm control --reload (or possibly logging out and in) the permissions for the device should allow global write (device 004 in folder 001 below):

-> % ls -l /dev/bus/usb/00*                   
/dev/bus/usb/001:
total 0
crw-rw-r-- 1 root root 189, 0 Jan  4 09:02 001
crw-rw-r-- 1 root root 189, 1 Jan  4 09:02 002
crw-rw-r-- 1 root root 189, 2 Jan  4 09:02 003
crw-rw-rw- 1 root root 189, 3 Jan  4 09:02 004

/dev/bus/usb/002:
total 0
crw-rw-r-- 1 root root 189, 128 Jan  4 09:02 001
crw-rw-r-- 1 root root 189, 130 Jan  4 09:02 003
crw-rw-r-- 1 root root 189, 131 Jan  4 09:02 004

The Arch Linux Wiki has a good page on udev and rules.

The Protocol

The control mechanism seems to be an 8-byte USB HID feature reports.

Built-in Functions

Below is the structure of the 8-byte feature report:

Byte Purpose Example Notes
0 Instruction? 0x08 Only 0x08 appears to issue commands
1 Unknown? 0x00 Values appear to make no difference.
2 Program 0x01 See below
3 Speed 0x06 See below
4 Brightness 0x32 See below
5 Colour 0x02 See below
6 Unknown? 0x01 Values appear to make no difference.
7 Checksum 0xbb 255 - sum(b0:b6)

Program:

Byte Value Program Notes
0x01 Static
0x02 Breathing
0x03 Wave
0x04 Fade on Keypress
0x05 Marquee Does not support random colour - value ignored
0x06 Ripple
0x07 Flash on Keypress
0x08 Neon
0x09 Rainbow Marquee Only supports random colour - other values ignored
0x0a Raindrop
0x0b Circle Marquee
0x0c Hedge
0x0d Rotate
0x33 Custom 1
0x34 Custom 2
0x35 Custom 3
0x36 Custom 4
0x37 Custom 5

Stored in code: here.

Speed:

Byte Value Speed
0x0a Slowest
0x09
0x08
0x07
0x06
0x05
0x04
0x03
0x02
0x01 Fastest

Colour:

Byte Value Colour
0x01 Red
0x02 Green
0x03 Yellow
0x04 Blue
0x05 Orange
0x06 Purple
0x07 White
0x08 Rainbow / Random

Stored in code: here.

Note: All other values inc 0x00 result in 0x08 - Rainbow/Random

Examples

A few examples below, with checksums.

Static, Green, Slowest, Full Brightness:

[0x08, 0x00, 0x01, 0x0A, 0x64, 0x02, 0x01, 0x85]

Fade on Press, Green, Slowest, Full Brightness:

[0x08, 0x00, 0x04, 0x0A, 0x64, 0x02, 0x01, 0x82]

Fade on Press, Yellow, Slowest, Full Brightness:

[0x08, 0x00, 0x04, 0x0A, 0x64, 0x03, 0x01, 0x81]

Fade on Press, Random, Slowest, Full Brightness:

[0x08, 0x00, 0x04, 0x0A, 0x64, 0x08, 0x01, 0x7C]

Marquee, Purple, Slowest, Full Brightness:

[0x08, 0x00, 0x05, 0x0A, 0x64, 0x06, 0x01, 0x7D]

Custom 1:

[0x08, 0x00, 0x33, 0x0A, 0x64, 0x00, 0x01, 0x55]

Custom Layouts

They keyboard supports 5 static custom layouts which are essentially an array of keys.

Programming

Programming requires the selection of the custom layout (as above), issuing a 'start programming' command, sending the data then selecting the layout again.

The data is sent as an array of [key_index, red, green, blue] in batches of 16 values (64 bytes each). There are about 120 key indexes, though not all are used on all keyboards.

It appears best to send them in order, even when some values are not used (such as key indicies less than 6, for example:

[0x01, red, grn, blu, 0x02, red, grn, blu,
 0x03, red, grn, blu, 0x04, red, grn, blu,
 0x05, red, grn, blu, 0x06, red, grn, blu,
 0x07, red, grn, blu, 0x08, red, grn, blu,
 0x09, red, grn, blu, 0x0a, red, grn, blu, 
 0x0b, red, grn, blu, 0x0c, red, grn, blu,
 0x0d, red, grn, blu, 0x0e, red, grn, blu,
 0x0f, red, grn, blu, 0x10, red, grn, blu] 

For Example

Select Custom 1 (as above):

[0x08, 0x00, 0x33, 0x0A, 0x64, 0x00, 0x01, 0x55]

Send 'start programming':

[0x12, 0x00, cidx, 0x08, 0x00, 0x00, 0x00, chks]

cidx represents the custom profile index, but zero based, so Custom 1 is 0, Custom 2 is 1, etc.

chks is the usual checksum.

Send the data (this example is the data from the group-based mapping described below).

There are 8 packets sent, each is 64 bytes, the start reference below is the offset in the overall keyboard colouring array

# Packet 1/8, len: 64 bytes, start element: 0

#ix1,  reg,  grn,  blu,  ix2,  red,  grn,  blu
0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x05, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00,
0x07, 0x40, 0xFF, 0x7F, 0x08, 0x40, 0xFF, 0x7F,
0x09, 0x40, 0xFF, 0x7F, 0x0A, 0x40, 0xFF, 0x7F,
0x0B, 0x7F, 0xFF, 0xFF, 0x0C, 0x7F, 0xFF, 0xFF,
0x0D, 0x40, 0xFF, 0x7F, 0x0E, 0x7F, 0x7F, 0xFF,
0x0F, 0xFF, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0x7F

# Packet 2/8, len: 64 bytes, start element: 16

#i17,  red,  grn,  blu, etc...
0x11, 0xFF, 0x7F, 0x7F, 0x12, 0x7F, 0x40, 0xFF,
0x13, 0x40, 0xFF, 0x7F, 0x14, 0xFF, 0xFF, 0x7F,
0x15, 0xFF, 0xFF, 0x7F, 0x16, 0xFF, 0xFF, 0x7F,
0x17, 0xFF, 0x7F, 0x7F, 0x18, 0x7F, 0x40, 0xFF,
0x19, 0x40, 0xFF, 0x7F, 0x1A, 0xFF, 0xFF, 0x7F,
0x1B, 0xFF, 0xFF, 0x7F, 0x1C, 0xFF, 0xFF, 0x7F,
0x1D, 0xFF, 0x7F, 0x7F, 0x1E, 0x7F, 0x40, 0xFF,
0x1F, 0x00, 0x00, 0x00, 0x20, 0xFF, 0xFF, 0x7F

# Packet 3/8, len: 64 bytes, start element: 32

0x21, 0xFF, 0xFF, 0x7F, 0x22, 0xFF, 0xFF, 0x7F,
0x23, 0xFF, 0x7F, 0x7F, 0x24, 0x7F, 0x40, 0xFF,
0x25, 0x00, 0x00, 0x00, 0x26, 0xFF, 0xFF, 0x7F,
0x27, 0xFF, 0xFF, 0x7F, 0x28, 0xFF, 0xFF, 0x7F,
0x29, 0xFF, 0x7F, 0x7F, 0x2A, 0x7F, 0x40, 0xFF,
0x2B, 0x64, 0x64, 0xFF, 0x2C, 0xFF, 0xFF, 0x7F,
0x2D, 0xFF, 0xFF, 0x7F, 0x2E, 0xFF, 0xFF, 0x7F,
0x2F, 0xFF, 0x7F, 0x7F, 0x30, 0x7F, 0x40, 0xFF

# Packet 4/8, len: 64 bytes, start element: 48

0x31, 0x00, 0x00, 0x00, 0x32, 0xFF, 0xFF, 0x7F,
0x33, 0xFF, 0xFF, 0x7F, 0x34, 0xFF, 0xFF, 0x7F,
0x35, 0xFF, 0x7F, 0x7F, 0x36, 0x7F, 0x40, 0xFF,
0x37, 0x00, 0x00, 0x00, 0x38, 0xFF, 0xFF, 0x7F,
0x39, 0xFF, 0xFF, 0x7F, 0x3A, 0xFF, 0xFF, 0x7F,
0x3B, 0xFF, 0x7F, 0x7F, 0x3C, 0x7F, 0x40, 0xFF,
0x3D, 0x40, 0xFF, 0x7F, 0x3E, 0x7F, 0x7F, 0xFF,
0x3F, 0xFF, 0xFF, 0x7F, 0x40, 0xFF, 0xFF, 0x7F

# Packet 5/8, len: 64 bytes, start element: 64

0x41, 0xFF, 0x7F, 0x7F, 0x42, 0x7F, 0x40, 0xFF,
0x43, 0x40, 0xFF, 0x7F, 0x44, 0x7F, 0x7F, 0xFF,
0x45, 0x7F, 0x7F, 0xFF, 0x46, 0xFF, 0xFF, 0x7F,
0x47, 0xFF, 0x7F, 0x7F, 0x48, 0x7F, 0x40, 0xFF,
0x49, 0x40, 0xFF, 0x7F, 0x4A, 0x7F, 0x7F, 0xFF,
0x4B, 0x7F, 0x7F, 0xFF, 0x4C, 0x7F, 0x7F, 0xFF,
0x4D, 0x7F, 0xFF, 0xFF, 0x4E, 0x7F, 0x40, 0xFF,
0x4F, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00

# Packet 6/8, len: 64 bytes, startelement: 80

0x51, 0x00, 0x00, 0x00, 0x52, 0x7F, 0x7F, 0xFF,
0x53, 0x7F, 0xFF, 0xFF, 0x54, 0x7F, 0x40, 0xFF,
0x55, 0xFF, 0x7F, 0xFF, 0x56, 0x40, 0xFF, 0x7F,
0x57, 0x7F, 0x7F, 0xFF, 0x58, 0x00, 0x00, 0x00,
0x59, 0x00, 0x00, 0x00, 0x5A, 0x7F, 0xFF, 0xFF,
0x5B, 0xFF, 0x7F, 0xFF, 0x5C, 0xFF, 0x7F, 0xFF,
0x5D, 0xFF, 0xFF, 0xFF, 0x5E, 0x00, 0x00, 0x00,
0x5F, 0x7F, 0xFF, 0xFF, 0x60, 0x7F, 0xFF, 0xFF

# Packet 7/8, len: 64 bytes, start element: 96

0x61, 0xFF, 0x7F, 0xFF, 0x62, 0x7F, 0xFF, 0x7F,
0x63, 0x7F, 0xFF, 0x7F, 0x64, 0x7F, 0xFF, 0x7F,
0x65, 0x7F, 0xFF, 0x7F, 0x66, 0xFF, 0xFF, 0x7F,
0x67, 0x7F, 0xFF, 0x7F, 0x68, 0x7F, 0xFF, 0x7F,
0x69, 0x7F, 0xFF, 0x7F, 0x6A, 0x7F, 0xFF, 0x7F,
0x6B, 0x7F, 0xFF, 0x7F, 0x6C, 0xFF, 0xFF, 0x7F,
0x6D, 0x7F, 0xFF, 0x7F, 0x6E, 0x7F, 0xFF, 0x7F,
0x6F, 0x7F, 0xFF, 0x7F, 0x70, 0x7F, 0xFF, 0x7F

# Packet 8/8, len: 64 bytes, start element: 112

0x71, 0x7F, 0xFF, 0x7F, 0x72, 0xFF, 0xFF, 0x7F,
0x73, 0x7F, 0xFF, 0x7F, 0x74, 0x00, 0x00, 0x00,
0x75, 0x7F, 0xFF, 0x7F, 0x76, 0x00, 0x00, 0x00,
0x77, 0x7F, 0xFF, 0x7F, 0x78, 0xFF, 0xFF, 0x7F,
0x79, 0x00, 0x00, 0x00, 0x7A, 0x00, 0x00, 0x00,
0x7B, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00,
0x7D, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00,
0x7F, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00

Finally, select Custom 1 again:

[0x08, 0x00, 0x33, 0x0A, 0x64, 0x00, 0x01, 0x55]

Keys

The Key index mappings for the UK keyboard can be found here.

There are also a number of groups defined here for convenience.

The scripts in GitHub allow custom layouts to be defined using keys and / or groups. Below is an example, unfortunately the picture quality is not amazing, but here is the screen from the utility too to show the group mappings:

Which is declared with something like:

  layout = {
      Keyboard::GROUPS.all => [64, 255, 127],
      Keyboard::GROUPS.fn_keys => [127, 64, 255],
      Keyboard::GROUPS.numbers_row => [127, 255, 255],
      Keyboard::GROUPS.numbers => [255, 127, 127],
      Keyboard::GROUPS.numpad => [127, 255, 127],
      Keyboard::GROUPS.arrows => [255, 127, 255],
      Keyboard::GROUPS.non_fn_keys => [127, 255, 255],
      Keyboard::GROUPS.navigation => [255, 255, 127],
      Keyboard::GROUPS.letters => [255, 255, 127],
      Keyboard::GROUPS.non_letters => [127, 127, 255],
      Keyboard::KEYS.enter => [255, 255, 255],
      Keyboard::KEYS.space => [100, 100, 255],
  }

Later entries override earlier ones, hence setting the fallback colour for all first.

Code

In the GitHub repository there are currently a number of examples to set built in animations and custom layouts.

I am working on a more general script but that will take a little longer so updates will follow!

Update (23 Jul 2019)

I sent my laptop to be repaired under warranty and as a result the keyboard was replaced, now I have:

Bus 001 Device 004: ID 1044:7a39 Chu Yuen Enterprise Co., Ltd

I have pushed an example which crudely supports this keyboard: https://github.com/paul-ridgway/aero-keyboard/tree/1044_7a39_Chu_Yuen_Enterprise_Co_Ltd

Ideally in future I will add support for various ones and select more dynamically.

The diff is very simple, this is the only change in device.rb:

-      if (device = HIDAPI.enumerate.select {|device| device.vendor_id == 0x04d9 && device.product_id == 0x8008}.first)
+      if (device = HIDAPI.enumerate.select {|device| device.vendor_id == 0x1044 && device.product_id == 0x7a39}.first)