Kaveh's Blog logo

Personal blog

Kaveh Tehrani

Legion Keyboard RGB Lights on Linux

Published on
|
3 mins read

I love the Lenovo Legion laptop that I have. Sadly Lenovo Vantage program doesn't have a Linux distribution so the keyboard lights are locked to the physical rotation you can do with the keys. This is a little script I whipped up after looking at the repos (see the end) just for changing the keyboard backlights.

Update 2026-08-11: As an LLM project, made this script to a TUI + CLI you can find here if you prefer.

Find Your Device

lsusb | grep 048d

You'll see something like 048d:c985. Note the product ID (c985 in this case).

Allow User Access

Create a udev rule so you don't need sudo:

sudo tee /etc/udev/rules.d/99-legion-keyboard.rules << 'EOF'
SUBSYSTEM=="usb", ATTR{idVendor}=="048d", ATTR{idProduct}=="c985", MODE="0666"
EOF

sudo udevadm control --reload-rules
sudo udevadm trigger

Install pyusb

pip install pyusb

The Script

Save as legion_kb_rgb.py:

#!/usr/bin/env python3
import sys
import usb.core

VENDOR_ID = 0x048d
PRODUCT_ID = 0xc985  # change if yours differs

COLORS = {
    'red':    (255, 0, 0),
    'green':  (0, 255, 0),
    'blue':   (0, 0, 255),
    'white':  (255, 255, 255),
    'orange': (255, 128, 0),
    'purple': (128, 0, 255),
    'cyan':   (0, 255, 255),
    'yellow': (255, 255, 0),
}

EFFECTS = {
    'static': 0x01,
    'breath': 0x03,
    'wave':   0x04,
    'hue':    0x06,
}

def parse_color(s):
    if s.lower() in COLORS:
        return COLORS[s.lower()]
    s = s.lstrip('#')
    return (int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16))

def send_command(effect, speed, brightness, colors):
    dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
    if dev is None:
        print("Device not found")
        sys.exit(1)
    try:
        if dev.is_kernel_driver_active(0):
            dev.detach_kernel_driver(0)
    except Exception:
        pass

    # pad to 4 zones
    while len(colors) < 4:
        colors.append(colors[-1] if colors else (0, 0, 0))

    packet = [0xCC, 0x16, effect, speed, brightness]
    for r, g, b in colors[:4]:
        packet += [r, g, b]
    packet += [0] * (32 - len(packet))

    dev.ctrl_transfer(0x21, 0x09, 0x03CC, 0, bytes(packet))
    print("Done")

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: legion_kb_rgb.py <effect|color> [colors...]")
        print("Effects: off, static, breath, wave, hue")
        print("Colors: red, green, blue, cyan, purple, orange, yellow, white, or hex")
        sys.exit(0)

    cmd = sys.argv[1].lower()

    if cmd == 'off':
        send_command(EFFECTS['static'], 1, 1, [(0, 0, 0)])
    elif cmd == 'wave':
        send_command(EFFECTS['wave'], 2, 2, [(0, 0, 0)])
    elif cmd == 'hue':
        send_command(EFFECTS['hue'], 2, 2, [(0, 0, 0)])
    elif cmd == 'breath':
        colors = [parse_color(c) for c in sys.argv[2:]] or [(255, 0, 0)]
        send_command(EFFECTS['breath'], 2, 2, colors)
    elif cmd == 'static':
        colors = [parse_color(c) for c in sys.argv[2:]] or [(255, 255, 255)]
        send_command(EFFECTS['static'], 1, 2, colors)
    else:
        # assume it's a color for static mode
        send_command(EFFECTS['static'], 1, 2, [parse_color(cmd)])

Usage

python legion_kb_rgb.py red
python legion_kb_rgb.py ff00ff
python legion_kb_rgb.py off
python legion_kb_rgb.py wave
python legion_kb_rgb.py hue
python legion_kb_rgb.py breath cyan
python legion_kb_rgb.py static ff0000 00ff00 0000ff ffffff  # 4 zones

How It Works

The keyboard uses an ITE controller that accepts USB HID SET_REPORT commands. The packet format is:

  • Bytes 0-1: Header (0xCC 0x16)
  • Byte 2: Effect (0x01=static, 0x03=breath, 0x04=wave, 0x06=hue)
  • Byte 3: Speed (1-4)
  • Byte 4: Brightness (1-2)
  • Bytes 5+: RGB values for each of the 4 zones

See Also