Hacking My MSI Laptop's RGB Keyboard to Work on Linux
MSI ships RGB keyboard control as Windows-only vendor software. No datasheet, no Linux driver, no docs. Here's how the USB HID protocol got reverse engineered anyway.
BY ASHLIN DARIUS GOVINDASAMY
An MSI gaming laptop's keyboard lighting is controlled entirely by Windows-only vendor software — SteelSeries GG on the models that use a SteelSeries-made keyboard controller. Boot into Linux and the keyboard doesn't go dark, it just freezes on whatever colour it powered on with. No driver, no config file, no published protocol. The hardware is fully capable — it just never agreed to talk to anything except one vendor's app.
Finding the device
Step one is establishing that the keyboard is a USB HID device sitting on the internal bus like any other peripheral, not something soldered directly to firmware with no addressable interface. lsusb finds it immediately, and the vendor/product ID pair is the anchor for everything that follows.
lsusb | grep -i "steelseries\|1038"
# Bus 001 Device 004: ID 1038:1234 SteelSeries ApS Gaming Keyboard
# Confirm it's a HID device and pull the report descriptor
sudo usbhid-dump -d 1038:1234
sudo lsusb -v -d 1038:1234 | grep -A4 "HID Device Class"Sniffing the protocol
With no documentation, the only source of truth is the traffic the official Windows app actually sends. That means capturing it: Wireshark with usbmon on a Linux host, watching the raw HID reports while a Windows VM with USB passthrough drives the keyboard through the official app — set a zone to red, capture; set it to blue, capture; diff the two payloads byte for byte.
sudo modprobe usbmon
sudo wireshark -i usbmon1 -k -f "usb.device_address == 4"
# Trigger a colour change in the vendor app while capturing,
# then diff the report bytes between two known coloursThe diff is where the protocol actually reveals itself. Two captures that only differ in three consecutive bytes, at the same offset, are almost certainly the R, G and B channels of a colour command. Everything else in the payload — report ID, a command opcode, a zone index — stays constant across the diff and gets isolated the same way, one variable at a time.
offset bytes meaning
0 0x0D report ID (feature report)
1 0x01 command: set static colour
2 0x00 zone index (0 = zone 1)
3-5 RR GG BB
6 0x00 effect id (0 = static)
7-63 0x00 padding to report lengthTalking to it from Linux
Once the payload shape is known, replicating it from userspace is the easy part — hidapi opens the device by vendor/product ID and sends the same feature report the Windows app was sending, no kernel driver required. The only real friction is permissions: by default the HID device node is root-owned, so a udev rule granting the right group read/write access is what makes this usable without sudo on every run.
import hid
VENDOR, PRODUCT = 0x1038, 0x1234
def set_zone(zone: int, r: int, g: int, b: int):
report = [0x0D, 0x01, zone, r, g, b, 0x00] + [0x00] * 57
dev = hid.device()
dev.open(VENDOR, PRODUCT)
dev.send_feature_report(report)
dev.close()
set_zone(zone=0, r=34, g=189, b=242) # ADGSTUDIOS blue, zone 1From one zone to a wave effect
- Per-zone, not per-key — this controller exposes a handful of lighting zones, not individually addressable keys, so a 'wave' effect means phase-shifting the same colour cycle across each zone index rather than each key.
- Feature reports vs. interrupt transfers — getting the transfer type wrong is a silent failure, not an error: the call returns fine and nothing on the keyboard changes.
- No root, ever — a udev rule scoped to the exact vendor/product ID is what makes this a normal userspace tool instead of a sudo script.
- Hardware effect modes exist — byte 6 in the report turns out to select breathing/wave/static on-device, which is far cheaper than pushing a new frame from the host every 16ms.
Vendor lock-in is a business decision, not a technical one. The controller doesn't care what OS is asking it to light up — it only ever cared that nobody told it how to ask.
None of this required a datasheet — just a USB analyzer, a diff, and the discipline to change exactly one variable per capture. That's the same method that applies to any undocumented device: capture the known-good traffic, isolate what changes, replicate it in the smallest form that works.
TAGS
RELATED