Reverse Engineering a Keyboard for OpenRGB
Introduction
So I recently bought the Darmoshark K5 mechanical keyboard, and it has per-key RGB lighting which is good but the provided software is very limited and I wanted more customizable effects.
I came across OpenRGB which is a free and open-source software that allows users to control RGB lighting on a variety of devices.
Unfortunately, not all devices are supported. One solution is to reverse engineer the device and add support for it myself. In this guide, I will walk through the process of reverse engineering my keyboard and how I added support for it.
OpenRGB RGBController API
OpenRGB has its own API for supporting devices; this API can be broken down into three components which I had to build.
- The Detector
- The Controller
- The RGBController
Building The Detector
A device's Detector function scans the attached devices to see if a particular device (Controller/RGBController) exists.
So that was my first mission: Building the detector.
Well, it's not really that hard in theory, the REGISTER_DETECTOR
macros are used to register a detector function with the OpenRGB Resource Manager which is responsible for calling detector functions at detection time.
The problem was finding out which macro to use. Because some macros resulted in my keyboard getting detected as 6 different keyboards and some didn't detect it at all.
After some trial and error (and looking at the other already-supported devices and how they are doing it), this macro worked perfectly:
REGISTER_HID_DETECTOR_PU("Darmoshark K5", DetectDarmosharkK5, DARMOSHARK_K5_VID, DARMOSHARK_K5_PID, 0xFF19, 0xFF19);
So let's break it down.
- The first argument is just the display name of the keyboard.
- The second one is the method to actually get the HID, that method was included in the API docs example, and I just used it as is.
- The third and fourth arguments are the VID (Vendor ID) and PID (Product ID) of the keyboard, so I had to figure those out by just opening the Device Manager and in the details tab for my keyboard they were both provided.
- The fifth and sixth arguments are the Page and Usage IDs, The Microsoft docs have this table which lists the Usage IDs for different device types, the Usage ID for keyboards is
0x06
. So why am I using this weird0xFF19
that is not even listed in the table you ask?
Well, when I tried0x06
it resulted in my keyboard getting detected as four keyboards.
So I checked the code for some of the supported keyboards and I found that magic 0xFF19
code in some of them, that when I tried, it resulted in my keyboard getting detected once.
Building The Controller
A device's Controller class is a class that provides whatever functionality is necessary to communicate with a device. This class should implement functions to send > control packets to a device and receive information packets from a device. It should provide the capability to set device colors and modes.
Here is where the fun begins.
The plan is to understand how the keyboard's program sends the packets to my keyboard and then emulate that same behavior myself.
Wireshark
Wireshark is a free and open-source packet analyzer. It is used for network troubleshooting, analysis, software and communications protocol development, and education.
I opened Wireshark and began listening to the packets sent to and received from the keyboard whenever I changed the RGB color using the provided software.
At first, it was quite overwhelming, but eventually after analyzing the packets for some time, I noticed some specific packets that get sent and received every time I update the RGB colors.
1. First Packet
First of all, it sends a specific packet at the beginning, I have no idea what that packet does, probably just prepares the keyboard to receive a new input or something.
Now let's analyze that packet so we can implement it in our overridden "SetLedsDirect" method in the Controller class.
So if you look at the section in the bottom left, we're only interested in the last two lines: the wLength
and the Data Fragment
.
The wLength
is just the length of the packet which is 64, and the data fragment is the actual data being sent so as you can see in the right section the first two bits are 0x08 and 0x21 then the rest are just zeros.
So in code, that's how it should look like:
// No idea what this is but it's sent before every RGB packet
uint8_t thing[64] = { 0x08, 0x21 };
Then we can just write that packet like so:
// Send "thing", whatever that may be
hid_write(dev, thing, 64);
2. Second Packet
The next packet to send is the RGB buffer, that buffer contains RGB values for every key, let's analyze it.
The length of the packet is 267 and the data fragment being sent starts with 0x20
then it continues to include each of the R, G, and B channels, and for each single channel, it has a value from 0x00
to 0xff
for every key.
So if I set every key to red you can see the first channel which is the R channel of the packet is 0xff while the rest after it are zeros.
Let's implement that right after the code for our first packet.
Actually, not yet, the overridden method comes with the colors
parameter which contains the colors you are required to set in the RGB packet, but before we can use that we need to define each key bit in our packet, so we need to know which keys map to which bits, Fun!
So I turned off the LEDs and started turning them on one by one to see which bit in the RGB packet gets turned on and recorded which keys map to which bit, ordered by the bits in the packet:
/// LCTRL Z C F SPACE M RALT FN RCTRL L_A ?? WIN X V G B
// 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15,
/// J , ?? RSHIFT D_A PGD LALT A D R N U . / ' R_A
// 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
/// ?? LSHIFT S E T H I K ; [ U_A ?? CAPS Q 3 4
// 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
/// Y 8 L P ] ENTER PGU TAB W ?? 5 6 9 0 - =
// 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
/// \ ?? ` ?? ??
// 64, 65, 66, 67, 68
As you can see there are some question marks here, for some reason there were bits that just never got turned on so they don't seem to map to any key.
Now I need to construct an array of those indices but order them by keys instead of their bit orders (So it starts with the Esc key, 0, 1, 2, and so on..):
#define NP 0xFF
static uint8_t packet_map[75] =
{
77, 67, 78, 46, 47, 58, 59, 70, 49, 60, 72, 62, 63, 86, 66,
55, 45, 56, 35, 25, 36, 48, 27, 38, 61, 51, 41, 52, 64, 75,
44, 23, 34, 24, 3, 14, 37, 16, 39, 50, 40, 30, 53, 10, 54,
33, 1, 12, 2, 13, 15, 26, 5, 17, 28, 29, 19, NP, 42, 21,
00, 11, 22, NP, NP, 4, NP, NP, NP, 6, 7, 8, 9, 20, 31
};
Now I could finally send the RGB packet like so:
uint8_t RGBbuffer[267] = { 0x20 };
for(int i = 0; i < colors.size(); i++) {
RGBColor key = colors[i];
uint16_t offset = packet_map[i];
// Skipping unused keys
if (offset == NP)
continue;
RGBbuffer[1 + offset] = RGBGetRValue(key);
RGBbuffer[89 + offset] = RGBGetGValue(key);
RGBbuffer[177 + offset] = RGBGetBValue(key);
}
hid_send_feature_report(dev, RGBbuffer, 267);
That packet is different from the first packet, it is a SET_REPORT Request
, the first one is also marked as SET_REPORT Request
on Wireshark but when I tried to send the first packet as a SET_REPORT Request
it didn't work.
So for that packet I used the method hid_send_feature_report
instead of hid_write
.
Feature reports use the first bit of the data fragment as the ReportID, so as you can see I initialize the RGBbuffer with 0x20 which is the ReportID of that packet.
3. Third Packet
Next packet I also have no idea about, but it always gets sent right after the RGB packet and it looks like this:
The length is 8, it is a GET_REPORT Request
, and it has a ReportID of 22.
uint8_t thing2[9] = { 22, 0xa1, 0x01, 0x16, 0x03, 0x00, 0x00, 0x02, 0x00 };
hid_get_feature_report(dev, thing2, 8);
4. Forth Packet
The fourth packet is identical to the first one except for the second and fifth bits, so I just changed those bits in the same array and sent it.
// Edit things to be other things
thing[1] = 0x22;
thing[4] = 0x02;
// Send new things
hid_write(dev, thing, 64);
5. Fifth Packet
The fifth and final packet looks like this:
uint8_t thing3[9] = { 19, 0xa1, 0x01, 0x13, 0x03, 0x00, 0x00, 0x11, 0x01 };
hid_get_feature_report(dev, thing3, 8);
Building The RGBController
OpenRGB uses an internal API called RGBController to standardize the interface to RGB devices from multiple vendors and categories.
The RGBController class specification contains the following:
- Device Name
- Device Description
- Device Version
- Device Serial
- Device Location
- Vector of LEDs
- Vector of Zones
- Vector of Modes
- Vector of Colors (32-bit 0x00BBGGRR format)
- Device Type (enum)
- Active mode index
It's pretty straightforward, I just yoinked the code of some other supported keyboard and began editing it.
I needed first to define a matrix map for the keys in order, including "unused keys" which are for keys bigger than one key unit like the space bar and the shift keys.
#define NA 0xFFFFFFFF
static unsigned int matrix_map[5][15] =
{
{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14},
{ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29},
{ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, NA, 44},
{ 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, NA, 58, 59},
{ 60, 61, 62, NA, NA, 65, NA, NA, NA, 69, 70, 71, 72, 73, 74}
};
I also needed to define the key names in the order of that matrix, like so:
static const char *led_names[] =
{
/*00*/ KEY_EN_ESCAPE,
/*01*/ KEY_EN_1,
/*02*/ KEY_EN_2,
/*03*/ KEY_EN_3,
/*04*/ KEY_EN_4,
/*05*/ KEY_EN_5,
/*06*/ KEY_EN_6,
/*07*/ KEY_EN_7,
/*08*/ KEY_EN_8,
/*09*/ KEY_EN_9,
/*10*/ KEY_EN_0,
/*11*/ KEY_EN_MINUS,
/*12*/ KEY_EN_EQUALS,
/*13*/ KEY_EN_BACKSPACE,
/*14*/ KEY_EN_BACK_TICK,
.
.
.
}
And in the constructor of that class, I needed to provide some information about the keyboard:
name = "Darmoshark K5 Keyboard";
vendor = "Darmoshark";
type = DEVICE_TYPE_KEYBOARD;
description = controller->GetDeviceName();
serial = controller->GetSerial();
location = controller->GetLocation();
That's about it, everything else is the same as the yoinked code from the other keyboard.
Summary
So yeah that was my first experience with reverse engineering and using Wireshark, I learnt a lot.