Files
utility-scripts/redragon.py

2392 lines
83 KiB
Python
Executable File

#!/usr/bin/python3
"""
Control utility for Redragon K621 Horus TKL RGB keyboard.
Supports LED patterns, brightness, speed, color, custom per-key RGB,
key remapping, macro programming, and profile management.
Protocol reverse-engineered from the official Redragon K621-RGB Setup V1.6.6
Windows application (KeyboardDrv.exe / Lowerdev.dll) and USB packet captures.
USB Protocol Summary:
All commands use HID SET_FEATURE via control transfer.
Write commands: Report ID 0x06, 1032 bytes total.
Query commands: Report ID 0x05, 6-8 bytes.
Sub-commands (byte [1] after report ID):
0x03/0xC6 - LED settings / profile save
0x04/0xD8 - Key matrix (remapping)
0x05/<id> - Macro data write (id = buffer slot 0-11)
0x08/0xC8 - LED RGB table (per-key colors)
0x09/<p> - LED matrix (per-key effects)
0x83/0xC6 - LED state query
0x84/0xD8 - Profile/key matrix query
0x85/<id> - Macro data query
"""
import argparse
import json
import sys
import time
import traceback
import usb.core
VENDOR = 0x258A
PRODUCT = 0x0049
INTERFACE = 1
# Report IDs and control transfer parameters
REPORT_WRITE = 0x06 # Write report (LED, key matrix, macro, profile)
REPORT_QUERY = 0x05 # Query/request report
REPORT_LED = 0x06 # Alias for backward compat
REPORT_STATE = 0x05 # Alias for backward compat
WVALUE_WRITE = 0x0306 # wValue for write reports (report ID 0x06)
WVALUE_QUERY = 0x0305 # wValue for query reports (report ID 0x05)
WVALUE_LED = 0x0306 # Alias
WVALUE_STATE = 0x0305 # Alias
BMREQUEST_OUT = 0x21 # bmRequestType (Host-to-device, class, interface)
BREQUEST = 0x09 # bRequest (SET_REPORT)
# Sub-commands (byte [1] in the payload, after report ID at [0])
SUBCMD_LED = 0x03 # LED settings write
SUBCMD_KEY_MATRIX = 0x04 # Key matrix write (remapping)
SUBCMD_MACRO_DATA = 0x05 # Macro data write
SUBCMD_LED_RGB_TAB = 0x08 # Per-key RGB table
SUBCMD_LED_MATRIX = 0x09 # Per-key LED effect matrix
SUBCMD_LED_QUERY = 0x83 # LED state query
SUBCMD_PROFILE_QUERY = 0x84 # Profile/key matrix query
SUBCMD_MACRO_QUERY = 0x85 # Macro data query
# Magic bytes (byte [2])
MAGIC_LED = 0xC6
MAGIC_KEY = 0xD8
MAGIC_RGB = 0xC8
# Key matrix data sizes
KEY_MATRIX_SIZE = 0x400 # 1024 bytes of matrix data
KEY_MATRIX_ENTRIES = 132 # Number of key matrix entries (0x84)
KEY_MATRIX_REGION_SIZE = 0x210 # Main key matrix region
FN_SINGLE_REGION_SIZE = 0xC0 # Fn single-key function region
FN_COMBO_REGION_SIZE = 0xC0 # Fn combo-key function region
# Macro constants
MACRO_BUFFER_COUNT = 12 # Number of macro buffer slots (0-11)
MACRO_BUFFER_SIZE = 0x400 # 1024 bytes per macro buffer
MACRO_START_ID = 0xE404 # Base macro identifier
# Payload structure constants
PAYLOAD_SIZE = 1032
SYNC = (0x5A, 0xA5)
OFFSET_REPORT_ID = 0
OFFSET_SUBCMD = 1
OFFSET_MAGIC = 2
OFFSET_SYNC1 = 14
OFFSET_PATTERN = 21
OFFSET_BRIGHTNESS = 22
OFFSET_SPEED1 = 28
OFFSET_SPEED2 = 29
OFFSET_COLOR_R = 36
OFFSET_COLOR_G = 37
OFFSET_COLOR_B = 38
OFFSET_ZONE1 = 39
OFFSET_SYNC2 = 78
OFFSET_ZONE2 = 80
OFFSET_CUSTOM_RGB = 106
# Brightness: 0x00 = 0%, 0x7F = 100%
BRIGHTNESS_MAX = 0x7F
# Speed: 0x00 = fastest, 0xFF = slowest (two bytes)
SPEED_MAX = 0xFF
# LED patterns: name -> (pattern_byte, has_speed, has_color)
# Extracted from Cfg.ini LedOpt entries and text.xml
PATTERNS = {
"off": (0x00, False, False),
"fixed_on": (0x01, False, True),
"respire": (0x02, True, True),
"rainbow": (0x03, True, False),
"flash_away": (0x04, True, True),
"raindrops": (0x05, True, True),
"rainbow_wheel": (0x06, True, False),
"ripples_shining": (0x07, True, True),
"stars_twinkle": (0x08, True, True),
"shadow_disappear": (0x09, True, True),
"retro_snake": (0x0A, True, True),
"neon_stream": (0x0B, True, True),
"reaction": (0x0C, True, True),
"sine_wave": (0x0D, True, True),
"retinue_scanning": (0x0E, True, True),
"rotating_windmill": (0x0F, True, False),
"colorful_waterfall": (0x10, True, False),
"blossoming": (0x11, True, False),
"rotating_storm": (0x12, True, True),
"collision": (0x13, True, True),
"perfect": (0x14, True, True),
"custom": (0x20, False, False),
}
# Convenience aliases
PATTERN_ALIASES = {
"solid": "fixed_on",
"breathe": "respire",
"breathing": "respire",
"stars": "stars_twinkle",
"shadow": "shadow_disappear",
"snake": "retro_snake",
"neon": "neon_stream",
"wave": "sine_wave",
"ripple": "ripples_shining",
"windmill": "rotating_windmill",
"waterfall": "colorful_waterfall",
"storm": "rotating_storm",
}
# Key name -> LED index mapping for custom per-key RGB.
# Extracted from Cfg.ini [KEY] section (last value = led_index).
# 87-key TKL layout.
KEY_LED_INDEX = {
"ESC": 0,
"F1": 2,
"F2": 3,
"F3": 4,
"F4": 5,
"F5": 6,
"F6": 7,
"F7": 8,
"F8": 9,
"F9": 10,
"F10": 11,
"F11": 12,
"F12": 13,
"PRTSC": 14,
"SCROLLLOCK": 15,
"PAUSE": 16,
"GRAVE": 21,
"`": 21,
"1": 22,
"2": 23,
"3": 24,
"4": 25,
"5": 26,
"6": 27,
"7": 28,
"8": 29,
"9": 30,
"0": 31,
"-": 32,
"MINUS": 32,
"=": 33,
"EQUAL": 33,
"BACKSPACE": 34,
"INSERT": 35,
"HOME": 36,
"PAGEUP": 37,
"TAB": 42,
"Q": 43,
"W": 44,
"E": 45,
"R": 46,
"T": 47,
"Y": 48,
"U": 49,
"I": 50,
"O": 51,
"P": 52,
"[": 53,
"LBRACKET": 53,
"]": 54,
"RBRACKET": 54,
"\\": 55,
"BACKSLASH": 55,
"DELETE": 56,
"END": 57,
"PAGEDOWN": 58,
"CAPSLOCK": 63,
"A": 64,
"S": 65,
"D": 66,
"F": 67,
"G": 68,
"H": 69,
"J": 70,
"K": 71,
"L": 72,
";": 73,
"SEMICOLON": 73,
"'": 74,
"APOSTROPHE": 74,
"ENTER": 76,
"LSHIFT": 84,
"Z": 86,
"X": 87,
"C": 88,
"V": 89,
"B": 90,
"N": 91,
"M": 92,
",": 93,
"COMMA": 93,
".": 94,
"PERIOD": 94,
"/": 95,
"SLASH": 95,
"RSHIFT": 97,
"UP": 99,
"LCTRL": 105,
"LWIN": 106,
"LALT": 107,
"SPACE": 110,
"RALT": 113,
"FN": 114,
"APP": 115,
"MENU": 115,
"RCTRL": 118,
"LEFT": 119,
"DOWN": 120,
"RIGHT": 121,
}
# HID Usage ID -> key name for key remapping
# Standard HID keyboard usage table (subset for common keys)
HID_USAGE_TO_NAME = {
0x00: "NONE",
0x04: "A",
0x05: "B",
0x06: "C",
0x07: "D",
0x08: "E",
0x09: "F",
0x0A: "G",
0x0B: "H",
0x0C: "I",
0x0D: "J",
0x0E: "K",
0x0F: "L",
0x10: "M",
0x11: "N",
0x12: "O",
0x13: "P",
0x14: "Q",
0x15: "R",
0x16: "S",
0x17: "T",
0x18: "U",
0x19: "V",
0x1A: "W",
0x1B: "X",
0x1C: "Y",
0x1D: "Z",
0x1E: "1",
0x1F: "2",
0x20: "3",
0x21: "4",
0x22: "5",
0x23: "6",
0x24: "7",
0x25: "8",
0x26: "9",
0x27: "0",
0x28: "ENTER",
0x29: "ESC",
0x2A: "BACKSPACE",
0x2B: "TAB",
0x2C: "SPACE",
0x2D: "MINUS",
0x2E: "EQUAL",
0x2F: "LBRACKET",
0x30: "RBRACKET",
0x31: "BACKSLASH",
0x33: "SEMICOLON",
0x34: "APOSTROPHE",
0x35: "GRAVE",
0x36: "COMMA",
0x37: "PERIOD",
0x38: "SLASH",
0x39: "CAPSLOCK",
0x3A: "F1",
0x3B: "F2",
0x3C: "F3",
0x3D: "F4",
0x3E: "F5",
0x3F: "F6",
0x40: "F7",
0x41: "F8",
0x42: "F9",
0x43: "F10",
0x44: "F11",
0x45: "F12",
0x46: "PRTSC",
0x47: "SCROLLLOCK",
0x48: "PAUSE",
0x49: "INSERT",
0x4A: "HOME",
0x4B: "PAGEUP",
0x4C: "DELETE",
0x4D: "END",
0x4E: "PAGEDOWN",
0x4F: "RIGHT",
0x50: "LEFT",
0x51: "DOWN",
0x52: "UP",
}
# Reverse mapping for key remapping
NAME_TO_HID_USAGE = {v: k for k, v in HID_USAGE_TO_NAME.items()}
# Key name -> matrix index mapping (from Cfg.ini [KEY] section, field "led_offset")
# The matrix index determines where in the 1024-byte key matrix the key's
# function code is stored. Each key occupies a 4-byte entry.
KEY_MATRIX_INDEX = {
"ESC": 0, "F1": 12, "F2": 18, "F3": 24, "F4": 30,
"F5": 36, "F6": 42, "F7": 48, "F8": 54,
"F9": 60, "F10": 66, "F11": 72, "F12": 78,
"PRTSC": 84, "SCROLLLOCK": 90, "PAUSE": 96,
"GRAVE": 1, "`": 1,
"1": 7, "2": 13, "3": 19, "4": 25, "5": 31,
"6": 37, "7": 43, "8": 49, "9": 55, "0": 61,
"MINUS": 67, "-": 67, "EQUAL": 73, "=": 73,
"BACKSPACE": 79, "INSERT": 85, "HOME": 91, "PAGEUP": 97,
"TAB": 2, "Q": 8, "W": 14, "E": 20, "R": 26, "T": 32,
"Y": 38, "U": 44, "I": 50, "O": 56, "P": 62,
"LBRACKET": 68, "[": 68, "RBRACKET": 74, "]": 74,
"BACKSLASH": 80, "\\": 80,
"DELETE": 86, "END": 92, "PAGEDOWN": 98,
"CAPSLOCK": 3, "A": 9, "S": 15, "D": 21, "F": 27,
"G": 33, "H": 39, "J": 45, "K": 51, "L": 57,
"SEMICOLON": 63, ";": 63, "APOSTROPHE": 69, "'": 69,
"ENTER": 81,
"LSHIFT": 4, "Z": 10, "X": 16, "C": 22, "V": 28,
"B": 34, "N": 40, "M": 46, "COMMA": 52, ",": 52,
"PERIOD": 58, ".": 58, "SLASH": 64, "/": 64,
"RSHIFT": 82,
"UP": 94,
"LCTRL": 5, "LWIN": 11, "LALT": 17, "SPACE": 35,
"RALT": 53, "FN": 59, "APP": 65, "MENU": 65,
"RCTRL": 83,
"LEFT": 89, "DOWN": 95, "RIGHT": 101,
}
# Modifier key HID usage codes (for key matrix encoding)
HID_MODIFIER_BITS = {
0xE0: 0x01, # Left Control
0xE1: 0x02, # Left Shift
0xE2: 0x04, # Left Alt
0xE3: 0x08, # Left GUI
0xE4: 0x10, # Right Control
0xE5: 0x20, # Right Shift
0xE6: 0x40, # Right Alt
0xE7: 0x80, # Right GUI
}
# VK-to-HID translation table (extracted from KeyboardDrv.exe at 0x5a51b8)
VK_TO_HID = {
0xE2: 0x64, 0x1B: 0x29, 0x70: 0x3A, 0x71: 0x3B, 0x72: 0x3C,
0x73: 0x3D, 0x74: 0x3E, 0x75: 0x3F, 0x76: 0x40, 0x77: 0x41,
0x78: 0x42, 0x79: 0x43, 0x7A: 0x44, 0x7B: 0x45,
0x41: 0x04, 0x42: 0x05, 0x43: 0x06, 0x44: 0x07, 0x45: 0x08,
0x46: 0x09, 0x47: 0x0A, 0x48: 0x0B, 0x49: 0x0C, 0x4A: 0x0D,
0x4B: 0x0E, 0x4C: 0x0F, 0x4D: 0x10, 0x4E: 0x11, 0x4F: 0x12,
0x50: 0x13, 0x51: 0x14, 0x52: 0x15, 0x53: 0x16, 0x54: 0x17,
0x55: 0x18, 0x56: 0x19, 0x57: 0x1A, 0x58: 0x1B, 0x59: 0x1C,
0x5A: 0x1D,
0x31: 0x1E, 0x32: 0x1F, 0x33: 0x20, 0x34: 0x21, 0x35: 0x22,
0x36: 0x23, 0x37: 0x24, 0x38: 0x25, 0x39: 0x26, 0x30: 0x27,
0x09: 0x2B, 0x14: 0x39, 0x0D: 0x28, 0x08: 0x2A, 0x20: 0x2C,
0xDB: 0x2F, 0xDD: 0x30, 0xDC: 0x31, 0xBA: 0x33, 0xDE: 0x34,
0xBC: 0x36, 0xBE: 0x37, 0xBF: 0x38, 0xC0: 0x35, 0xBB: 0x2E,
0xBD: 0x2D,
0x2C: 0x46, 0x91: 0x47, 0x13: 0x48, 0x2D: 0x49, 0x24: 0x4A,
0x21: 0x4B, 0x2E: 0x4C, 0x23: 0x4D, 0x22: 0x4E,
0x26: 0x52, 0x28: 0x51, 0x25: 0x50, 0x27: 0x4F,
0xA2: 0xE0, 0xA0: 0xE1, 0xA4: 0xE2, 0x5B: 0xE3,
0xA3: 0xE4, 0xA1: 0xE5, 0xA5: 0xE6, 0x5C: 0xE7,
0x5D: 0x65,
}
# Game mode presets from Cfg.ini (GaoshouKey entries)
# Each preset highlights a set of keys with the "Perfect" pattern
GAME_PRESETS = {
"wasd": ["ESC", "W", "A", "S", "D", "UP", "DOWN", "LEFT", "RIGHT"],
"fps": [
"F1",
"F2",
"F3",
"1",
"2",
"3",
"4",
"5",
"TAB",
"Q",
"W",
"E",
"R",
"A",
"S",
"D",
"G",
"LSHIFT",
"LCTRL",
"LALT",
"SPACE",
],
"moba": [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"Q",
"W",
"E",
"R",
"T",
"A",
"S",
"D",
"F",
"G",
"LSHIFT",
"LCTRL",
"LALT",
"B",
"SPACE",
],
"mmo": [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"Q",
"W",
"E",
"R",
"T",
"A",
"S",
"D",
"F",
"LSHIFT",
"LCTRL",
"Z",
"X",
"C",
"V",
"LEFT",
"RIGHT",
],
"arrows": [
"W",
"R",
"A",
"S",
"D",
"LSHIFT",
"LCTRL",
"LALT",
"SPACE",
"UP",
"DOWN",
"LEFT",
"RIGHT",
],
}
verbose = 0
def log(level, *args, **kwargs):
if verbose >= level:
print(*args, **kwargs)
def find_keyboard():
kbd = usb.core.find(idVendor=VENDOR, idProduct=PRODUCT)
if kbd is None:
print(
"Error: Keyboard not found. Is it connected in USB/wired mode?",
file=sys.stderr,
)
sys.exit(1)
log(1, f"Found: {kbd.manufacturer} {kbd.product}")
return kbd
def send_report(kbd, report_id, wvalue, data):
try:
ret = kbd.ctrl_transfer(BMREQUEST_OUT, BREQUEST, wvalue, INTERFACE, data)
assert ret == len(data), f"Short write: {ret}/{len(data)}"
time.sleep(0.05)
return ret
except usb.core.USBError as e:
if e.errno == 16:
print(f"USB error (resource busy): {e}", file=sys.stderr)
print("Attempting driver reset...", file=sys.stderr)
reset_driver(kbd)
# Retry once
ret = kbd.ctrl_transfer(BMREQUEST_OUT, BREQUEST, wvalue, INTERFACE, data)
assert ret == len(data)
time.sleep(0.05)
return ret
elif e.errno == 2:
print(f"USB error (no entity): {e}", file=sys.stderr)
print("Try unplugging and replugging the keyboard.", file=sys.stderr)
sys.exit(1)
else:
raise
def reset_driver(kbd):
interfaces = []
for cfg in kbd:
for intf in cfg:
interfaces.append(intf.bInterfaceNumber)
for intf_num in interfaces:
if kbd.is_kernel_driver_active(intf_num):
log(1, f"Detaching kernel driver for interface {intf_num}")
try:
kbd.detach_kernel_driver(intf_num)
except usb.core.USBError as e:
print(f"Could not detach kernel driver: {e}", file=sys.stderr)
print(traceback.format_exc(), file=sys.stderr)
# Send state query to reset the device
msg = [REPORT_STATE, 0x83, 0xC6, 0x00, 0x00, 0x00]
try:
log(1, "Sending state query to reset device")
kbd.ctrl_transfer(BMREQUEST_OUT, BREQUEST, WVALUE_STATE, INTERFACE, msg)
except usb.core.USBError:
pass
for intf_num in interfaces:
if not kbd.is_kernel_driver_active(intf_num):
log(1, f"Reattaching kernel driver for interface {intf_num}")
try:
kbd.attach_kernel_driver(intf_num)
except usb.core.USBError as e:
print(f"Could not reattach kernel driver: {e}", file=sys.stderr)
def build_led_payload(
pattern_byte,
brightness_byte=None,
speed_bytes=None,
color_rgb=None,
custom_rgb_data=None,
):
"""Build the LED control payload.
Uses the exact byte-for-byte payload from redragon-lights.py as the
template (710 bytes, including a third sync marker at [137:141]).
Only explicitly provided fields are patched.
Pass None (the default) for any field to leave it at the template value.
"""
# Exact payload from redragon-lights.py (710 bytes).
# This is the known-good packet that works with the keyboard.
# fmt: off
msg = [
0x06, 0x03, 0xC6, # [0:3] header
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, # [3:14] padding
0x00, 0x00, 0x00,
0x5A, 0xA5, # [14:16] sync 1
0x03, 0x03, 0x00, 0x00, 0x00, # [16:21]
0x09, # [21] pattern
0x20, # [22] brightness
0x00, 0x00, 0x00, 0x00, 0x00, # [23:28]
0x55, 0x55, # [28:30] speed
0x01, # [30]
0x00, 0x00, 0x00, 0x00, 0x00, # [31:36]
0xFF, 0xFF, 0x00, # [36:39] color R,G,B
# Zone parameter table
0x31, # [39]
0x07, 0x39, 0x07, 0x39, 0x07, 0x39, 0x07, 0x39, # [40:54]
0x07, 0x39, 0x07, 0x39, 0x07, 0x39,
0x00, 0x31, # [54:56]
0x07, 0x39, 0x07, 0x39, 0x07, 0x39, 0x07, 0x39, # [56:78]
0x07, 0x39, 0x07, 0x39, 0x07, 0x39, 0x07, 0x39,
0x07, 0x39, 0x07, 0x39, 0x07, 0x39,
0x5A, 0xA5, # [78:80] sync 2
0x00, 0x10, # [80:82]
0x07, 0x49, 0x07, 0x49, 0x07, 0x49, 0x07, 0x49, # [82:96]
0x07, 0x49, 0x07, 0x49, 0x07, 0x49,
0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, # [96:106]
0x09, 0x09,
# 31 zero bytes # [106:137]
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x5A, 0xA5, 0x03, 0x03, # [137:141] sync 3
# Remaining zeros to end of packet # [141:710]
]
msg += [0x00] * (PAYLOAD_SIZE - len(msg))
# fmt: on
# Only patch fields that were explicitly provided
msg[OFFSET_PATTERN] = pattern_byte
if brightness_byte is not None:
msg[OFFSET_BRIGHTNESS] = brightness_byte
if speed_bytes is not None:
msg[OFFSET_SPEED1] = speed_bytes[0]
msg[OFFSET_SPEED2] = speed_bytes[1]
if color_rgb is not None:
msg[OFFSET_COLOR_R] = color_rgb[0]
msg[OFFSET_COLOR_G] = color_rgb[1]
msg[OFFSET_COLOR_B] = color_rgb[2]
# Custom per-key RGB data (bytes 106-136, between zone data and sync 3)
if custom_rgb_data:
for i, b in enumerate(custom_rgb_data):
if OFFSET_CUSTOM_RGB + i < PAYLOAD_SIZE:
msg[OFFSET_CUSTOM_RGB + i] = b
return msg
def percent_to_brightness(pct):
"""Convert 0-100 percentage to brightness byte (0x00-0x7F)."""
pct = max(0, min(100, pct))
return round(pct * BRIGHTNESS_MAX / 100)
def percent_to_speed(pct):
"""Convert 0-100 percentage to speed bytes.
0% = slowest (0xFF), 100% = fastest (0x00)."""
pct = max(0, min(100, pct))
val = round((100 - pct) * SPEED_MAX / 100)
return (val, val)
def parse_color(color_str):
"""Parse a hex color string (RRGGBB) to (R, G, B) tuple."""
color_str = color_str.lstrip("#")
if len(color_str) != 6:
raise ValueError(f"Invalid color '{color_str}': expected 6 hex digits (RRGGBB)")
return (int(color_str[0:2], 16), int(color_str[2:4], 16), int(color_str[4:6], 16))
def resolve_pattern(name):
"""Resolve a pattern name (with alias support) to its entry."""
name = name.lower().replace(" ", "_").replace("-", "_")
if name in PATTERN_ALIASES:
name = PATTERN_ALIASES[name]
if name not in PATTERNS:
print(f"Error: Unknown pattern '{name}'", file=sys.stderr)
print(
f"Available patterns: {', '.join(sorted(PATTERNS.keys()))}", file=sys.stderr
)
sys.exit(1)
return name, PATTERNS[name]
def resolve_key(name):
"""Resolve a key name to its LED index."""
name = name.upper().replace(" ", "")
if name in KEY_LED_INDEX:
return KEY_LED_INDEX[name]
print(f"Error: Unknown key '{name}'", file=sys.stderr)
sys.exit(1)
def build_custom_rgb(color_map):
"""Build custom RGB data from a key->color mapping.
Returns bytes for the custom RGB area of the payload."""
max_index = max(KEY_LED_INDEX.values())
data = [0x00] * ((max_index + 1) * 3)
for key_name, color in color_map.items():
key_upper = key_name.upper().replace(" ", "")
if key_upper not in KEY_LED_INDEX:
print(f"Warning: Unknown key '{key_name}', skipping", file=sys.stderr)
continue
idx = KEY_LED_INDEX[key_upper]
r, g, b = parse_color(color) if isinstance(color, str) else color
data[idx * 3] = r
data[idx * 3 + 1] = g
data[idx * 3 + 2] = b
return data
# ── Key matrix / macro / profile protocol ──────────────────────────────
def build_write_packet(subcmd, magic_or_data_byte, data=None, header_size=7):
"""Build a 1032-byte write packet for non-LED commands.
Packet format:
[0] = Report ID (0x06)
[1] = sub-command
[2] = magic byte or data byte (e.g., macro buffer ID)
[3] = 0x00
[4] = 0x40 (indicates 1024-byte data section follows)
[5] = 0x00
[6] = 0x00
[7] = 0x00
[8..1031] = data (up to 1024 bytes)
"""
pkt = [0x00] * PAYLOAD_SIZE
pkt[0] = REPORT_WRITE
pkt[1] = subcmd
pkt[2] = magic_or_data_byte
pkt[3] = 0x00
pkt[4] = 0x40
pkt[5] = 0x00
pkt[6] = 0x00
pkt[7] = 0x00
if data:
for i, b in enumerate(data):
if header_size + i < PAYLOAD_SIZE:
pkt[header_size + i] = b
return pkt
def build_query_packet(subcmd, param=0x00):
"""Build a 6-byte query packet.
Packet format:
[0] = Report ID (0x05)
[1] = sub-command (0x83, 0x84, 0x85, etc.)
[2] = parameter (e.g., macro buffer ID)
[3..5] = 0x00
"""
return [REPORT_QUERY, subcmd, param, 0x00, 0x00, 0x00]
def send_and_receive(kbd, query_pkt, response_size=PAYLOAD_SIZE):
"""Send a query packet and read the response."""
send_report(kbd, REPORT_QUERY, WVALUE_QUERY, query_pkt)
time.sleep(0.05)
try:
response = kbd.read(0x82, response_size, timeout=2000)
return list(response)
except usb.core.USBError as e:
if e.errno == 110: # timeout
log(0, "No response from keyboard (timeout)")
return None
raise
def encode_key_function(hid_usage):
"""Encode a HID usage code into the 4-byte key matrix format.
Standard keys: the HID usage goes in the low byte.
Modifier keys (0xE0-0xE7): encoded as modifier bit pattern.
"""
if hid_usage in HID_MODIFIER_BITS:
# Modifier keys use a different encoding
return hid_usage & 0xFF
return hid_usage & 0xFF
def build_key_matrix(remappings=None):
"""Build the 1024-byte key matrix data.
The matrix has three regions:
[0x000..0x20F] Key Matrix: 132 entries x 4 bytes = 528 bytes
[0x210..0x2CF] Fn Single Key: 48 entries x 4 bytes = 192 bytes
[0x2D0..0x38F] Fn Combo Key: 48 entries x 4 bytes = 192 bytes
Each entry is a 32-bit LE value. For standard keys, the HID usage
is in the low byte. Macro assignments use special encoding.
"""
data = [0x00] * KEY_MATRIX_SIZE
# Fill with default key assignments (identity mapping)
for key_name, matrix_idx in KEY_MATRIX_INDEX.items():
# Skip aliases
if key_name in ("MENU", "`", "-", "=", "[", "]", "\\", ";", "'",
",", ".", "/"):
continue
if key_name == "FN":
continue # Fn key has no matrix entry (handled by firmware)
hid = NAME_TO_HID_USAGE.get(key_name)
if hid is not None and matrix_idx < KEY_MATRIX_ENTRIES:
offset = matrix_idx * 4
data[offset] = hid & 0xFF
data[offset + 1] = 0x00
data[offset + 2] = 0x00
data[offset + 3] = 0x00
# Apply remappings
if remappings:
for src_key, dst_key in remappings.items():
src_upper = src_key.upper().replace(" ", "")
dst_upper = dst_key.upper().replace(" ", "")
if src_upper not in KEY_MATRIX_INDEX:
print(f"Warning: Unknown source key '{src_key}', skipping",
file=sys.stderr)
continue
if dst_upper not in NAME_TO_HID_USAGE:
print(f"Warning: Unknown target key '{dst_key}', skipping",
file=sys.stderr)
continue
matrix_idx = KEY_MATRIX_INDEX[src_upper]
hid = NAME_TO_HID_USAGE[dst_upper]
if matrix_idx < KEY_MATRIX_ENTRIES:
offset = matrix_idx * 4
data[offset] = hid & 0xFF
data[offset + 1] = 0x00
data[offset + 2] = 0x00
data[offset + 3] = 0x00
return data
def build_macro_data(actions):
"""Build macro buffer data from a list of actions.
Each action is a tuple: (event_type, hid_keycode, delay_ms)
event_type: 'down' or 'up'
hid_keycode: HID usage code
delay_ms: delay after this event in milliseconds
The macro data format (per action, 4 bytes):
[0] = HID keycode
[1] = flags (0x01 = key down, 0x00 = key up)
[2:3] = delay in ms (little-endian)
"""
data = [0x00] * MACRO_BUFFER_SIZE
offset = 0
for action in actions:
if offset + 4 > MACRO_BUFFER_SIZE:
print("Warning: Macro too large, truncating", file=sys.stderr)
break
event_type, hid_code, delay_ms = action
data[offset] = hid_code & 0xFF
data[offset + 1] = 0x01 if event_type == "down" else 0x00
data[offset + 2] = delay_ms & 0xFF
data[offset + 3] = (delay_ms >> 8) & 0xFF
offset += 4
return data
def parse_macro_sequence(sequence_str):
"""Parse a human-readable macro sequence into actions.
Format: "key1+key2 key3 key4" where:
- '+' means simultaneous press (chord)
- ' ' separates sequential keypresses
- Each key is pressed then released with a short delay
Example: "ctrl+c" -> Ctrl down, C down, C up, Ctrl up
"""
actions = []
default_delay = 20 # ms
parts = sequence_str.strip().split()
for part in parts:
keys = part.split("+")
hid_codes = []
for key_name in keys:
key_upper = key_name.upper()
# Handle common modifier aliases
aliases = {
"CTRL": "LCTRL", "SHIFT": "LSHIFT", "ALT": "LALT",
"WIN": "LWIN", "GUI": "LWIN", "SUPER": "LWIN",
"RCTRL": "RCTRL", "RSHIFT": "RSHIFT", "RALT": "RALT",
}
if key_upper in aliases:
key_upper = aliases[key_upper]
# Map special names to HID modifier codes
modifier_names = {
"LCTRL": 0xE0, "LSHIFT": 0xE1, "LALT": 0xE2, "LWIN": 0xE3,
"RCTRL": 0xE4, "RSHIFT": 0xE5, "RALT": 0xE6,
}
if key_upper in modifier_names:
hid_codes.append(modifier_names[key_upper])
elif key_upper in NAME_TO_HID_USAGE:
hid_codes.append(NAME_TO_HID_USAGE[key_upper])
else:
print(f"Warning: Unknown key '{key_name}' in macro",
file=sys.stderr)
continue
# Press all keys in order
for hid in hid_codes:
actions.append(("down", hid, default_delay))
# Release in reverse order
for hid in reversed(hid_codes):
actions.append(("up", hid, default_delay))
return actions
# ── Command implementations ─────────────────────────────────────────────
def cmd_on(args):
"""Turn keyboard backlight on with a pattern."""
kbd = find_keyboard()
name, (pat, _, _) = resolve_pattern(args.pattern or "fixed_on")
# Only patch fields the user explicitly set; leave the rest at template
# defaults (the known-good captured values).
brightness = (
percent_to_brightness(args.brightness) if args.brightness is not None else None
)
speed = percent_to_speed(args.speed) if args.speed is not None else None
color = parse_color(args.color) if args.color else None
msg = build_led_payload(pat, brightness, speed, color)
log(
1,
f"Pattern: {name}, brightness: {args.brightness}, "
f"speed: {args.speed}, color: {args.color}",
)
send_report(kbd, REPORT_LED, WVALUE_LED, msg)
print(f"Backlight on: {name}")
def cmd_off(args):
"""Turn keyboard backlight off."""
kbd = find_keyboard()
# Only change the pattern byte to 0x00; leave all other fields at their
# template defaults — zeroing brightness/speed/color causes a reset.
msg = build_led_payload(0x00)
send_report(kbd, REPORT_LED, WVALUE_LED, msg)
print("Backlight off")
def cmd_pattern(args):
"""Set a specific LED pattern with options."""
kbd = find_keyboard()
name, (pat, has_speed, has_color) = resolve_pattern(args.name)
brightness = (
percent_to_brightness(args.brightness) if args.brightness is not None else None
)
speed = percent_to_speed(args.speed) if args.speed is not None else None
color = parse_color(args.color) if args.color else None
if args.color and not has_color:
print(
f"Note: Pattern '{name}' does not support custom colors (uses rainbow)",
file=sys.stderr,
)
if args.speed is not None and not has_speed:
print(
f"Note: Pattern '{name}' does not support speed adjustment", file=sys.stderr
)
msg = build_led_payload(pat, brightness, speed, color)
log(
1,
f"Pattern: {name} (0x{pat:02X}), brightness: {args.brightness}, "
f"speed: {args.speed}, color: {args.color}",
)
send_report(kbd, REPORT_LED, WVALUE_LED, msg)
print(f"Pattern set: {name}")
def cmd_custom(args):
"""Set custom per-key RGB colors."""
kbd = find_keyboard()
try:
with open(args.colors) as f:
color_map = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.colors}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON in {args.colors}: {e}", file=sys.stderr)
sys.exit(1)
custom_data = build_custom_rgb(color_map)
brightness = (
percent_to_brightness(args.brightness) if args.brightness is not None else None
)
msg = build_led_payload(0x20, brightness, custom_rgb_data=custom_data)
send_report(kbd, REPORT_LED, WVALUE_LED, msg)
print(f"Custom RGB set from {args.colors} ({len(color_map)} keys)")
def cmd_game(args):
"""Apply a game mode preset that highlights specific keys."""
kbd = find_keyboard()
preset_name = args.preset.lower()
if preset_name not in GAME_PRESETS:
print(f"Error: Unknown preset '{preset_name}'", file=sys.stderr)
print(f"Available: {', '.join(sorted(GAME_PRESETS.keys()))}", file=sys.stderr)
sys.exit(1)
keys = GAME_PRESETS[preset_name]
color = parse_color(args.color) if args.color else (0xFF, 0x00, 0x00)
bg_color = parse_color(args.background) if args.background else (0x00, 0x00, 0x00)
# Build custom color map: highlighted keys in color, rest in background
color_map = {}
for key_name in KEY_LED_INDEX:
# Skip aliases (lowercase/symbol variants)
if not key_name[0].isalpha() and key_name not in (
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"-",
"=",
"[",
"]",
"\\",
";",
"'",
",",
".",
"/",
"`",
):
continue
if key_name in keys:
color_map[key_name] = color
else:
color_map[key_name] = bg_color
custom_data = build_custom_rgb(color_map)
brightness = (
percent_to_brightness(args.brightness) if args.brightness is not None else None
)
msg = build_led_payload(0x20, brightness, custom_rgb_data=custom_data)
send_report(kbd, REPORT_LED, WVALUE_LED, msg)
print(f"Game preset: {preset_name} ({len(keys)} keys highlighted)")
def cmd_test(args):
"""Send the exact known-good packet from redragon-lights.py for diagnosis.
If --on: sends the original shadow_disappear packet (byte-identical).
If --off: sends the same packet with only byte [21] changed to 0x00.
This is exactly what the original redragon-lights.py does.
"""
kbd = find_keyboard()
# Byte-identical to the msg in redragon-lights.py
msg = build_led_payload(0x09) # shadow_disappear, all other fields from template
if not args.switch:
msg[OFFSET_PATTERN] = 0x00 # only change: pattern -> off
log(
1,
f"Sending {'ON (shadow_disappear)' if args.switch else 'OFF'} "
f"— byte-identical to redragon-lights.py",
)
if verbose >= 2:
print("First 106 bytes:")
hex_dump(msg[:106])
send_report(kbd, REPORT_LED, WVALUE_LED, msg)
print(f"Test packet sent: {'on' if args.switch else 'off'}")
def cmd_state(args):
"""Query the keyboard's current state."""
kbd = find_keyboard()
msg = [REPORT_STATE, 0x83, 0xC6, 0x00, 0x00, 0x00]
log(1, "Querying keyboard state...")
send_report(kbd, REPORT_STATE, WVALUE_STATE, msg)
# Try to read the response
try:
# Read from endpoint 0x82 (IN endpoint for interface 1)
response = kbd.read(0x82, 290, timeout=1000)
log(1, f"Response ({len(response)} bytes):")
if verbose >= 2:
hex_dump(response)
# Parse known fields if response is long enough
if len(response) >= 22:
pat_byte = response[21] if len(response) > 21 else 0
pat_name = "unknown"
for name, (code, _, _) in PATTERNS.items():
if code == pat_byte:
pat_name = name
break
print(f"Current pattern: {pat_name} (0x{pat_byte:02X})")
if len(response) > OFFSET_BRIGHTNESS:
bri = response[OFFSET_BRIGHTNESS]
print(f"Brightness: {round(bri * 100 / BRIGHTNESS_MAX)}% (0x{bri:02X})")
if len(response) > OFFSET_COLOR_B:
r, g, b = (
response[OFFSET_COLOR_R],
response[OFFSET_COLOR_G],
response[OFFSET_COLOR_B],
)
print(f"Color: #{r:02X}{g:02X}{b:02X}")
else:
print(f"Response: {len(response)} bytes")
hex_dump(response)
except usb.core.USBError as e:
if e.errno == 110: # timeout
print(
"No response from keyboard (this is normal for some firmware versions)"
)
else:
print(f"Read error: {e}", file=sys.stderr)
def cmd_reset(args):
"""Reset the USB driver for the keyboard."""
kbd = find_keyboard()
reset_driver(kbd)
print("Driver reset complete")
def cmd_list_patterns(args):
"""List all available LED patterns."""
print("Available LED patterns:")
print(f" {'Name':<24} {'Code':<8} {'Speed':<8} {'Color'}")
print(f" {'' * 24} {'' * 7} {'' * 7} {'' * 7}")
for name, (code, has_speed, has_color) in sorted(
PATTERNS.items(), key=lambda x: x[1][0]
):
speed_str = "yes" if has_speed else ""
color_str = "yes" if has_color else "rainbow"
if name == "off":
color_str = ""
print(f" {name:<24} 0x{code:02X} {speed_str:<8} {color_str}")
print("\nAliases:")
for alias, target in sorted(PATTERN_ALIASES.items()):
print(f" {alias} -> {target}")
def cmd_list_keys(args):
"""List all key names for custom RGB."""
# Deduplicate: show only the primary name for each LED index
seen = {}
for name, idx in sorted(KEY_LED_INDEX.items(), key=lambda x: x[1]):
if idx not in seen:
seen[idx] = name
print("Key names for custom RGB (87 keys):")
rows = [
(
"Row 0",
[
"ESC",
"F1",
"F2",
"F3",
"F4",
"F5",
"F6",
"F7",
"F8",
"F9",
"F10",
"F11",
"F12",
"PRTSC",
"SCROLLLOCK",
"PAUSE",
],
),
(
"Row 1",
[
"GRAVE",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"MINUS",
"EQUAL",
"BACKSPACE",
"INSERT",
"HOME",
"PAGEUP",
],
),
(
"Row 2",
[
"TAB",
"Q",
"W",
"E",
"R",
"T",
"Y",
"U",
"I",
"O",
"P",
"LBRACKET",
"RBRACKET",
"BACKSLASH",
"DELETE",
"END",
"PAGEDOWN",
],
),
(
"Row 3",
[
"CAPSLOCK",
"A",
"S",
"D",
"F",
"G",
"H",
"J",
"K",
"L",
"SEMICOLON",
"APOSTROPHE",
"ENTER",
],
),
(
"Row 4",
[
"LSHIFT",
"Z",
"X",
"C",
"V",
"B",
"N",
"M",
"COMMA",
"PERIOD",
"SLASH",
"RSHIFT",
"UP",
],
),
(
"Row 5",
[
"LCTRL",
"LWIN",
"LALT",
"SPACE",
"RALT",
"FN",
"APP",
"RCTRL",
"LEFT",
"DOWN",
"RIGHT",
],
),
]
for label, keys in rows:
print(f" {label}: {' '.join(keys)}")
print("\nShorthand aliases: ` - = [ ] \\ ; ' , . /")
print(f"Total keys: {len(seen)}")
def cmd_list_games(args):
"""List available game presets."""
print("Game mode presets:")
for name, keys in sorted(GAME_PRESETS.items()):
print(f" {name}: {', '.join(keys)}")
def cmd_remap(args):
"""Remap one or more keys on the keyboard.
Sends the full key matrix to the keyboard via the SetKeyMatrix protocol:
Report ID: 0x06
Header: [0x04, 0xD8, 0x00, 0x40, 0x00, 0x00, 0x00]
Data: 1024 bytes of key matrix
The key matrix contains 132 4-byte entries (528 bytes at offset 0x000),
followed by Fn-layer data (192+192 bytes at offsets 0x210 and 0x2D0).
"""
kbd = find_keyboard()
if args.reset:
# Send identity mapping (reset all remaps)
matrix_data = build_key_matrix()
pkt = build_write_packet(SUBCMD_KEY_MATRIX, MAGIC_KEY, matrix_data)
send_report(kbd, REPORT_WRITE, WVALUE_WRITE, pkt)
print("Key remapping reset to defaults")
return
if not args.mapping:
print("Error: Provide at least one mapping (SRC=DST) or --reset",
file=sys.stderr)
sys.exit(1)
# Parse mappings from "SRC=DST" format
remappings = {}
for m in args.mapping:
if "=" not in m:
print(f"Error: Invalid mapping '{m}', expected SRC=DST format",
file=sys.stderr)
sys.exit(1)
src, dst = m.split("=", 1)
remappings[src.strip()] = dst.strip()
matrix_data = build_key_matrix(remappings)
pkt = build_write_packet(SUBCMD_KEY_MATRIX, MAGIC_KEY, matrix_data)
log(1, f"Remapping {len(remappings)} key(s)")
for src, dst in remappings.items():
log(1, f" {src} -> {dst}")
if verbose >= 2:
print("Key matrix (first 64 bytes):")
hex_dump(matrix_data[:64])
send_report(kbd, REPORT_WRITE, WVALUE_WRITE, pkt)
print(f"Remapped {len(remappings)} key(s):")
for src, dst in remappings.items():
print(f" {src} -> {dst}")
def cmd_macro(args):
"""Program a macro on the keyboard.
Sends macro data via the SetMacroData protocol:
Report ID: 0x06
Header: [0x05, <buffer_id>, 0x00, 0x40, 0x00, 0x00, 0x00]
Data: 1024 bytes of macro actions
Each macro action is 4 bytes:
[0] = HID keycode
[1] = flags (0x01 = key down, 0x00 = key up)
[2:3] = delay in ms (little-endian uint16)
The keyboard has 12 macro buffer slots (0-11).
"""
kbd = find_keyboard()
slot = args.slot
if slot < 0 or slot >= MACRO_BUFFER_COUNT:
print(f"Error: Macro slot must be 0-{MACRO_BUFFER_COUNT - 1}",
file=sys.stderr)
sys.exit(1)
if args.clear:
# Clear the macro slot
data = [0x00] * MACRO_BUFFER_SIZE
pkt = build_write_packet(SUBCMD_MACRO_DATA, slot, data)
send_report(kbd, REPORT_WRITE, WVALUE_WRITE, pkt)
print(f"Macro slot {slot} cleared")
return
if args.read:
# Read the current macro from the slot
query = build_query_packet(SUBCMD_MACRO_QUERY, slot)
response = send_and_receive(kbd, query)
if response is None:
print(f"Could not read macro slot {slot}")
return
print(f"Macro slot {slot} contents:")
# Skip the header (first 7 bytes after report ID)
data_start = 8 # report ID + 7 header bytes
actions = []
for i in range(0, min(len(response) - data_start, MACRO_BUFFER_SIZE), 4):
offset = data_start + i
if offset + 3 >= len(response):
break
hid_code = response[offset]
flags = response[offset + 1]
delay = response[offset + 2] | (response[offset + 3] << 8)
if hid_code == 0 and flags == 0 and delay == 0:
break
event = "down" if flags & 0x01 else "up"
key_name = HID_USAGE_TO_NAME.get(hid_code, f"0x{hid_code:02X}")
actions.append((event, key_name, delay))
print(f" {event:4s} {key_name:<12s} delay={delay}ms")
if not actions:
print(" (empty)")
return
if not args.sequence:
print("Error: Provide --sequence, --read, or --clear",
file=sys.stderr)
sys.exit(1)
actions = parse_macro_sequence(args.sequence)
if not actions:
print("Error: No valid keys in macro sequence", file=sys.stderr)
sys.exit(1)
macro_data = build_macro_data(actions)
pkt = build_write_packet(SUBCMD_MACRO_DATA, slot, macro_data)
log(1, f"Programming macro slot {slot}: {args.sequence}")
log(1, f" {len(actions)} actions")
send_report(kbd, REPORT_WRITE, WVALUE_WRITE, pkt)
print(f"Macro programmed in slot {slot}: {args.sequence}")
print(f" {len(actions)} actions ({len(actions) * 4} bytes)")
def cmd_profile(args):
"""Save or load keyboard profile.
Save profile (SetProfile):
Report ID: 0x06
Header: [0x03, 0xC6, ...]
Data: 136 bytes (0x88) of LED/profile state
Load profile (GetProfile):
Query: Report ID 0x05, [0x84, 0xD8, 0x00, 0x00, 0x00, 0x00]
Response: Report ID 0x06, 1032 bytes (first data byte should be 0x83)
"""
kbd = find_keyboard()
if args.save:
# Save current state to a JSON file
query = build_query_packet(SUBCMD_PROFILE_QUERY, MAGIC_KEY)
response = send_and_receive(kbd, query)
if response is None:
print("Could not read profile from keyboard")
return
# Save raw response bytes to file
profile = {
"type": "redragon_k621_profile",
"version": 1,
"data": list(response),
}
with open(args.save, "w") as f:
json.dump(profile, f, indent=2)
print(f"Profile saved to {args.save} ({len(response)} bytes)")
elif args.load:
# Load profile from JSON file and send to keyboard
try:
with open(args.load) as f:
profile = json.load(f)
except FileNotFoundError:
print(f"Error: File not found: {args.load}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
if profile.get("type") != "redragon_k621_profile":
print("Error: Not a valid Redragon K621 profile file",
file=sys.stderr)
sys.exit(1)
data = profile["data"]
# The profile data is sent as an LED payload (sub-cmd 0x03, magic 0xC6)
# The first byte of profile data starts after the header
if len(data) >= PAYLOAD_SIZE:
send_report(kbd, REPORT_WRITE, WVALUE_WRITE, data[:PAYLOAD_SIZE])
print(f"Profile loaded from {args.load}")
else:
# Pad to full size
data = data + [0x00] * (PAYLOAD_SIZE - len(data))
send_report(kbd, REPORT_WRITE, WVALUE_WRITE, data)
print(f"Profile loaded from {args.load}")
elif args.dump:
# Dump current profile state
query = build_query_packet(SUBCMD_PROFILE_QUERY, MAGIC_KEY)
response = send_and_receive(kbd, query)
if response is None:
print("Could not read profile from keyboard")
return
print(f"Profile data ({len(response)} bytes):")
hex_dump(response[:256])
else:
print("Error: Provide --save FILE, --load FILE, or --dump",
file=sys.stderr)
sys.exit(1)
def cmd_protocol(args):
"""Display the complete USB protocol documentation."""
print("""Redragon K621 Horus TKL RGB - USB HID Protocol Reference
========================================================
Reverse-engineered from KeyboardDrv.exe (Delphi/MFC, compiled with MSVC 9.0)
and Lowerdev.dll (thin HID wrapper). Source: Redragon_K621-RGB_Setup_V1.6.6.
DEVICE IDENTIFICATION
VID/PID: 0x258A / 0x0049
Product: Redragon K621 Horus TKL RGB (87-key)
Interface: 1 (HID class, used for configuration)
Endpoints: 0x02 (OUT), 0x82 (IN) on interface 1
DRIVER LAYER (Lowerdev.dll)
Loaded dynamically via LoadLibrary/GetProcAddress. Exports:
OpenHidDevice(vid, pid) -> device handle
SetFeature(handle, buf, size) -> bool (HidD_SetFeature wrapper)
GetFeature(handle, buf, size) -> bool (HidD_GetFeature wrapper)
GetInputReport(handle, buf, sz) -> bool (HidD_GetInputReport)
GetProductString(handle) -> string
GetVersionNumber(handle) -> uint
SetOutputReport exists in the DLL but is NOT used by KeyboardDrv.exe.
All keyboard communication uses SetFeature/GetFeature exclusively.
TRANSPORT
USB HID SET_FEATURE via control transfer:
bmRequestType = 0x21 (Host-to-device, Class, Interface)
bRequest = 0x09 (SET_REPORT)
wValue = 0x0306 for write commands (Report ID 0x06 in high byte)
wValue = 0x0305 for query commands (Report ID 0x05 in high byte)
wIndex = 1 (Interface number)
All write packets are exactly 1032 bytes. Query packets are 6 or 8 bytes.
Responses are read via GetFeature (same endpoint, report ID 0x06, 1032 bytes)
or via the IN endpoint 0x82 for input reports.
Error handling: retry up to 3 times with 100ms delay between retries.
PACKET FORMAT OVERVIEW
Write packets (1032 bytes):
[0] Report ID (always 0x06)
[1] Sub-command byte
[2] Magic byte or parameter
[3..7] Header (command-specific)
[8..1031] Data payload (up to 1024 bytes)
Query packets (6 bytes):
[0] Report ID (always 0x05)
[1] Query sub-command (high bit set: 0x83, 0x84, 0x85)
[2] Parameter (magic byte or slot number)
[3..5] Padding (zeros)
Response packets (1032 bytes):
Same format as write, read back via GetFeature with Report ID 0x06.
══════════════════════════════════════════════════════════════════════════
WRITE COMMANDS (Report ID 0x06, 1032 bytes)
══════════════════════════════════════════════════════════════════════════
─── 1. LED Settings / Profile Save (sub-cmd 0x03, magic 0xC6) ─────────
This is the primary LED control command AND the profile save command.
The "profile" is the LED state — they share the same packet format.
Complete byte map of the 1032-byte payload:
Offset Bytes Description
------ ----- -----------
[0] 1 Report ID = 0x06
[1] 1 Sub-command = 0x03
[2] 1 Magic = 0xC6
[3:14] 11 Padding (zeros in captured packet)
[14:16] 2 Sync marker 1 = 0x5A 0xA5
[16:18] 2 Post-sync = 0x03 0x03
[18:21] 3 Unknown (zeros)
[21] 1 Pattern byte (see pattern table below)
[22] 1 Brightness: 0x00 (off) to 0x7F (100%)
[23:28] 5 Unknown (zeros)
[28] 1 Speed byte 1: 0x00 (fastest) to 0xFF (slowest)
[29] 1 Speed byte 2: same range, both bytes set identically
[30] 1 Unknown (0x01 in captured packet)
[31:36] 5 Unknown (zeros)
[36] 1 Color Red: 0x00-0xFF
[37] 1 Color Green: 0x00-0xFF
[38] 1 Color Blue: 0x00-0xFF
Zone parameter table (bytes 39-105):
[39] 1 Zone header = 0x31
[40:54] 14 Zone params: repeating (0x07, 0x39) pairs x7
[54:56] 2 Zone separator = 0x00, 0x31
[56:78] 22 Zone params: repeating (0x07, 0x39) pairs x11
[78:80] 2 Sync marker 2 = 0x5A 0xA5
[80:82] 2 Zone header 2 = 0x00, 0x10
[82:96] 14 Zone params: repeating (0x07, 0x49) pairs x7
[96:106] 10 Pattern echo bytes (kept at captured values = 0x09 x10)
WARNING: modifying these causes keyboard reset!
Custom RGB area and tail:
[106:137] 31 Custom per-key RGB data (all zeros for built-in patterns)
[137:139] 2 Sync marker 3 = 0x5A 0xA5
[139:141] 2 Post-sync = 0x03 0x03
[141:1032] 891 Padding (zeros)
CRITICAL NOTES:
- The payload MUST be exactly 1032 bytes (not 1031).
- All three sync markers (0x5A 0xA5) must be present at offsets
14, 78, and 137. Missing any causes a firmware reset/crash.
- Bytes [96:106] must be kept at their captured values (0x09).
Setting them to any other value causes keyboard reset.
- The known-good approach is to use an exact byte-for-byte template
from a working packet capture, and only patch the fields you need
(pattern, brightness, speed, color).
- Only change fields you intend to modify; pass None/omit the rest.
Pattern byte values (byte [21]):
Code Name Speed Color Cfg.ini hw code
---- -------------------- ----- ------ ---------------
0x00 Off no n/a 0 (LedOpt21)
0x01 Fixed On (solid) no yes 23
0x02 Respire (breathe) yes yes 31
0x03 Rainbow yes rainbow 2
0x04 Flash Away yes yes 19
0x05 Raindrops yes yes 15
0x06 Rainbow Wheel yes rainbow 13
0x07 Ripples Shining yes yes 20
0x08 Stars Twinkle yes yes 16
0x09 Shadow Disappear yes yes 18
0x0A Retro Snake yes yes 5
0x0B Neon Stream yes yes 7
0x0C Reaction yes yes 17
0x0D Sine Wave yes yes 12
0x0E Retinue Scanning yes yes 8
0x0F Rotating Windmill yes rainbow 28
0x10 Colorful Waterfall yes rainbow 30
0x11 Blossoming yes rainbow 9
0x12 Rotating Storm yes yes 29
0x13 Collision yes yes 27
0x14 Perfect yes yes 26
0x20 Custom (per-key RGB) no n/a 5 (LedOpt10)
Cfg.ini LedOpt format:
LedOpt<N> = ui_index, hw_code, has_speed, has_brightness,
has_direction, has_random_color, has_single_color
The "hw_code" is the value sent to firmware (not the same as pattern byte).
The pattern byte is the ui_index (1-based).
Brightness encoding:
Cfg.ini LightHW = 0,1,2,3,4,5,6,7,8,9 (10 levels)
Byte value = level * 0x0E (approx), range 0x00-0x7F
UI percentage maps linearly: 0% = 0x00, 100% = 0x7F
Speed encoding:
Cfg.ini SpeedHW = 1,2,3,4 (4 levels)
Both speed bytes [28] and [29] are set to the same value.
Range: 0x00 (fastest) to 0xFF (slowest).
UI percentage maps inversely: 100% speed = 0x00, 0% = 0xFF.
─── 2. Key Matrix / SetKeyMatrix (sub-cmd 0x04, magic 0xD8) ───────────
Sends the complete key remapping table to the keyboard.
Packet header (bytes [0:8]):
[0] = 0x06 Report ID
[1] = 0x04 Sub-command (Key Matrix)
[2] = 0xD8 Magic byte
[3] = 0x00 Reserved
[4] = 0x40 Data length indicator (0x40 = high byte of 0x0400 = 1024)
[5] = 0x00
[6] = 0x00
[7] = 0x00
Data payload (bytes [8:1032], 1024 bytes):
Three regions packed sequentially:
Region 1: Main Key Matrix
Offset: 0x000 (byte [8] of packet)
Size: 0x210 = 528 bytes = 132 entries x 4 bytes
Purpose: Maps each physical key to its output function.
Region 2: Fn Single Key Functions
Offset: 0x210 (byte [536] of packet)
Size: 0x0C0 = 192 bytes = 48 entries x 4 bytes
Purpose: Functions for Fn+key (single key, no modifiers).
Region 3: Fn Combo Key Functions
Offset: 0x2D0 (byte [728] of packet)
Size: 0x0C0 = 192 bytes = 48 entries x 4 bytes
Purpose: Functions for Fn+key combinations.
Region 4: Padding
Offset: 0x390 (byte [920] of packet)
Size: 0x070 = 112 bytes (zeros)
Key matrix entry format (4 bytes, little-endian uint32):
Standard key: 0x000000xx where xx = HID Usage Code (page 0x07)
Modifier key: 0x000000xx where xx = HID Usage (0xE0-0xE7)
Macro assigned: Low byte = macro_buffer_offset, bit pattern encodes slot
Media/system: Full 32-bit function code (see Fn function codes below)
Disabled: 0x00000000
Physical key to matrix index mapping (132 entries):
The matrix index determines where in Region 1 the key's 4-byte
entry is stored (at byte offset = matrix_index * 4).
Idx Key Idx Key Idx Key Idx Key
--- ---------- --- ---------- --- ---------- --- ----------
0 ESC 1 ` (GRAVE) 2 TAB 3 CAPSLOCK
4 LSHIFT 5 LCTRL 7 1 8 Q
9 A 10 Z 11 LWIN 12 F1
13 2 14 W 15 S 16 X
17 LALT 18 F2 19 3 20 E
21 D 22 C 24 F3 25 4
26 R 27 F 28 V 30 F4
31 5 32 T 33 G 34 B
35 SPACE 36 F5 37 6 38 Y
39 H 40 N 42 F6 43 7
44 U 45 J 46 M 48 F7
49 8 50 I 51 K 52 , (COMMA)
53 RALT 54 F8 55 9 56 O
57 L 58 . (PERIOD) 59 FN 60 F9
61 0 62 P 63 ; (SEMICOL) 64 / (SLASH)
65 APP (MENU) 66 F10 67 - (MINUS) 68 [ (LBRACK)
69 ' (APOSTR) 72 F11 73 = (EQUAL) 74 ] (RBRACK)
78 F12 79 BACKSPACE 80 \\ (BKSLASH) 81 ENTER
82 RSHIFT 83 RCTRL 84 PRTSC 85 INSERT
86 DELETE 89 LEFT 90 SCROLLLOCK 91 HOME
92 END 94 UP 95 DOWN 96 PAUSE
97 PAGEUP 98 PAGEDOWN 101 RIGHT
Gaps in the index sequence (6,23,29,41,47,70,71,75-77,87,88,93,
99,100,102-131) are unused/reserved slots.
HID Usage Codes (USB HID Keyboard/Keypad Page 0x07):
0x04-0x1D A-Z 0x1E-0x27 1-0
0x28 Enter 0x29 Escape
0x2A Backspace 0x2B Tab
0x2C Space 0x2D - (Minus)
0x2E = (Equal) 0x2F [ (Left Bracket)
0x30 ] (Right Bracket) 0x31 \\ (Backslash)
0x33 ; (Semicolon) 0x34 ' (Apostrophe)
0x35 ` (Grave Accent) 0x36 , (Comma)
0x37 . (Period) 0x38 / (Slash)
0x39 Caps Lock 0x3A-0x45 F1-F12
0x46 Print Screen 0x47 Scroll Lock
0x48 Pause 0x49 Insert
0x4A Home 0x4B Page Up
0x4C Delete 0x4D End
0x4E Page Down 0x4F Right Arrow
0x50 Left Arrow 0x51 Down Arrow
0x52 Up Arrow 0x65 Application (Menu)
0xE0 Left Control 0xE1 Left Shift
0xE2 Left Alt 0xE3 Left GUI (Win/Super)
0xE4 Right Control 0xE5 Right Shift
0xE6 Right Alt 0xE7 Right GUI
─── 3. Macro Data / SetMacroData (sub-cmd 0x05) ───────────────────────
Sends macro action data to one of 12 macro buffer slots.
Packet header (bytes [0:8]):
[0] = 0x06 Report ID
[1] = 0x05 Sub-command (Macro Data)
[2] = <slot> Macro buffer slot: 0x00-0x0B (0-11)
[3] = 0x00 Reserved
[4] = 0x40 Data length indicator
[5] = 0x00
[6] = 0x00
[7] = 0x00
Data payload (bytes [8:1032], 1024 bytes):
Sequence of 4-byte action entries, terminated by an all-zero entry.
Maximum 256 actions per buffer (1024 / 4).
Macro action entry format (4 bytes):
Byte [0]: HID Usage Code of the key (page 0x07)
Modifier keys use codes 0xE0-0xE7 directly.
Byte [1]: Event type flags
0x01 = Key press (down)
0x00 = Key release (up)
Byte [2]: Delay low byte (uint16 LE, milliseconds)
Byte [3]: Delay high byte
Delay is applied AFTER this action, before the next.
Typical values: 10-50ms for fast macros.
Terminator: 0x00 0x00 0x00 0x00 (first all-zero entry ends the macro)
Example: Ctrl+C macro (6 actions, 24 bytes + terminator):
E0 01 14 00 Left Control down, 20ms delay
06 01 14 00 C down, 20ms delay
06 00 14 00 C up, 20ms delay
E0 00 14 00 Left Control up, 20ms delay
00 00 00 00 Terminator
Multi-chunk macros:
When macro data exceeds 1024 bytes, it is sent in 1024-byte chunks.
The first chunk uses buffer_id = slot, subsequent chunks use
buffer_id = slot + 4. The firmware concatenates them.
From debug strings: hwParam.nMacroBufferSize gives the chunk size,
hwParam.nMacroBufferNum = 12, hwParam.nMaxAction = max per slot.
Assigning a macro to a key:
After uploading macro data to a slot, the key matrix entry for
the triggering key must be updated to reference the macro.
The key's matrix entry encodes the macro buffer ID with an offset
added from the base (MACRO_START_ID = 0xE404).
─── 4. LED RGB Table / SetLedRgbTab (sub-cmd 0x08, magic 0xC8) ────────
Sends per-key RGB color assignments used by the "custom" pattern mode.
Packet header (bytes [0:8]):
[0] = 0x06 Report ID
[1] = 0x08 Sub-command (LED RGB Table)
[2] = 0xC8 Magic byte
[3] = 0x00
[4] = 0x40 Data length indicator
[5] = 0x00
[6] = 0x00
[7] = 0x00
Data payload (bytes [8:1032]):
Before the RGB data, there is a 21-byte structure (zeros) for
sync/header purposes, followed by the actual color data copied
from the input buffer.
The RGB data is organized by LED index. Each key's color is
stored at its LED index position within sub-regions that are
0x7E (126) bytes apart, with R/G/B channels in separate planes
(planar layout, not interleaved).
The channel offsets within each plane are configured in Cfg.ini:
Byte 0x90 = R channel offset within the 126-byte plane
Byte 0x91 = G channel offset
Byte 0x92 = B channel offset
Stride between entries: 0x03 (every 3rd byte within a channel)
Size limit: esi (data size) is checked against 0x3EB (1003) max.
─── 5. LED Matrix / SetLedMatrix (sub-cmd 0x09) ───────────────────────
Assigns per-key LED effect parameters (which effect each key uses).
Packet header (bytes [0:8]):
[0] = 0x06 Report ID
[1] = 0x09 Sub-command (LED Matrix)
[2] = <param> Effect parameter byte (from stack argument)
[3] = 0x00
[4] = 0x40 Data length indicator
[5] = 0x00
[6] = 0x00
[7] = 0x00
Data payload (bytes [8:1032]):
Effect assignment data for each key LED. The data is copied from
the LED effect buffer. Size varies per call:
First call: 0xCC bytes (204) of effect data
Second call: 0xD0 bytes (208) of effect data
Both calls happen during ApplySetting, with a 50ms delay between them.
══════════════════════════════════════════════════════════════════════════
QUERY COMMANDS (Report ID 0x05, 6-8 bytes)
══════════════════════════════════════════════════════════════════════════
Queries are sent as short SET_FEATURE packets. The device responds
via GetFeature (read back using the same HID feature report mechanism,
Report ID 0x06, 1032 bytes).
─── 1. LED State Query (sub-cmd 0x83, magic 0xC6) ─────────────────────
Query: 05 83 C6 00 00 00 (6 bytes)
Response: 1032 bytes, same format as LED Settings write.
The response contains the current LED pattern, brightness, speed,
color, and zone parameters.
─── 2. Profile / Key Matrix Query (sub-cmd 0x84, magic 0xD8) ──────────
Query: 05 84 D8 00 00 00 (6 bytes)
Response: 1032 bytes.
First data byte after header = 0x83 (confirmation code).
Contains 136 bytes (0x88 = 34 dwords) of profile/LED configuration.
The response data is also used to read back key matrix state.
After receiving, firmware copies 0x22 dwords (136 bytes) from offset
[7] (after header) into the profile structure.
─── 3. Macro Data Query (sub-cmd 0x85) ────────────────────────────────
Query: 05 85 <slot> 00 00 00 (6 bytes, slot = 0x00-0x0B)
Response: 1032 bytes.
Contains macro action data for the specified slot, same format as
the SetMacroData write payload. Data starts at byte [7] after header.
─── 4. Password / Version Query (sub-cmd 0x81) ────────────────────────
Query: 05 05 81 00 00 00 00 00 (8 bytes, note: uses Report ID 0x05
for both the query report and in byte [0])
Response: 7 bytes (read via GetFeature, Report ID 0x05).
Byte [0] = 0x01 (success flag)
Bytes [1:5] = 4 bytes of device/firmware info
Bytes [5:7] = 2 bytes additional info
The response byte [0] must equal 0x01 for success.
Firmware version is decoded from the info bytes:
FW Version = value at global 0x5d7144 in KeyboardDrv.exe
Compared against Cfg.ini VerNeedToUpdate (0x100) for update checks.
══════════════════════════════════════════════════════════════════════════
FN-LAYER FUNCTION CODES (from Cfg.ini [FN] section)
══════════════════════════════════════════════════════════════════════════
Fn+key functions are stored in Regions 2 and 3 of the key matrix.
The Cfg.ini [FN] section defines default Fn-layer bindings.
Entry format: K<n>=<type>,<vkey>,<function_code>[,<game_index>]
type: 0x09 = system/LED function, 0x02 = key passthrough
vkey: Windows virtual key code (0x00 for system functions)
function_code: 32-bit function identifier
Function code encoding:
0x0e000000 = LED control functions:
0x0e000000 Fn+W: LED brightness up (game mode alternate)
0x0e000001 Fn+Win: Win key lock toggle
0x0e000005 Fn+Esc: LED mode cycle (next pattern)
0x0e000007 Fn+Tab: LED mode cycle (prev pattern)
0x0e00000c Fn+1: LED pattern 1 (profile slot)
0x0e00000d Fn+2: LED pattern 2
0x0e00000e Fn+3: LED pattern 3
0x0e00000f Fn+4: LED pattern 4
0x0e000010 Fn+5: LED pattern 5
0x0e000011 Fn+G: Game mode toggle
0x0e000013 Fn+F1: LED speed decrease
0x0e000014 Fn+F2: LED speed increase
0x0e000015 Fn+F3: LED brightness decrease
0x0e000016 Fn+F4: LED brightness increase
0x04000000 = Consumer/media control (USB HID Consumer page):
0x04000223 Fn+F9: WWW Home (browser)
0x04000221 Fn+F10: WWW Search
0x04000192 Fn+F11: Calculator
0x0400018a Fn+F12: Email client
0x0b000300 = System control:
0x0b000300 Fn+Ins: Scroll Lock (alternate)
0x12000300 = System control:
0x12000300 Fn+Del: Num Lock toggle (alternate)
0x0d000000 = Navigation:
0x0d000200 Fn+Left: Previous track (media)
0x0d000100 Fn+Right: Next track (media)
Game mode preset entries (Fn+G<n> keys):
K88=0x02,0x70,0x00,16 G1 -> F1 key, game preset index 16
K89=0x02,0x71,0x00,17 G2 -> F2 key, game preset index 17
K90=0x02,0x72,0x00,18 G3 -> F3 key, game preset index 18
K91=0x02,0x73,0x00,19 G4 -> F4 key, game preset index 19
K92=0x02,0x74,0x00,24 G5 -> F5 key, game preset index 24
Fn key passthrough entries (type 0x02):
These pass the VK code directly: Fn+A=0x41, Fn+S=0x53, Fn+D=0x44
Used for game mode WASD -> arrow key redirection.
══════════════════════════════════════════════════════════════════════════
KEY LED INDEX MAPPING (87 keys)
══════════════════════════════════════════════════════════════════════════
Each physical key has a unique LED index used for per-key RGB control.
From Cfg.ini [KEY] section: K<n>=x1,y1,x2,y2, type,vkey,mod,led_off,led_idx
The led_idx (last field) is the LED index:
Row 0 (Function):
ESC=0 F1=2 F2=3 F3=4 F4=5 F5=6 F6=7 F7=8 F8=9
F9=10 F10=11 F11=12 F12=13 PRTSC=14 SCRLK=15 PAUSE=16
Row 1 (Numbers):
`=21 1=22 2=23 3=24 4=25 5=26 6=27 7=28 8=29 9=30
0=31 -=32 ==33 BKSP=34 INS=35 HOME=36 PGUP=37
Row 2 (QWERTY):
TAB=42 Q=43 W=44 E=45 R=46 T=47 Y=48 U=49 I=50
O=51 P=52 [=53 ]=54 \\=55 DEL=56 END=57 PGDN=58
Row 3 (Home):
CAPS=63 A=64 S=65 D=66 F=67 G=68 H=69 J=70 K=71
L=72 ;=73 '=74 ENTER=76
Row 4 (Shift):
LSHIFT=84 Z=86 X=87 C=88 V=89 B=90 N=91 M=92
,=93 .=94 /=95 RSHIFT=97 UP=99
Row 5 (Control):
LCTRL=105 LWIN=106 LALT=107 SPACE=110 RALT=113
FN=114 APP=115 RCTRL=118 LEFT=119 DOWN=120 RIGHT=121
Gaps in LED indices (1,17-20,38-41,59-62,75,77-83,85,96,98,100-104,
106-109,111-112,116-117) are unused positions in the LED controller.
LED index vs matrix index:
These are DIFFERENT mappings! The matrix index (KEY_MATRIX_INDEX)
determines position in the key remapping data. The LED index
(KEY_LED_INDEX) determines position in the RGB color data.
They are NOT the same for any key.
══════════════════════════════════════════════════════════════════════════
VK-TO-HID TRANSLATION TABLE (108 entries)
══════════════════════════════════════════════════════════════════════════
Extracted from KeyboardDrv.exe at RVA 0x1a51b8. Each entry is 2 bytes:
[HID_code, VK_code]. Used to convert Windows virtual key codes to
USB HID usage codes for key matrix programming.
VK HID Key VK HID Key
---- ---- --------------- ---- ---- ---------------
0x1B 0x29 Escape 0x09 0x2B Tab
0x08 0x2A Backspace 0x0D 0x28 Enter
0x20 0x2C Space 0x14 0x39 Caps Lock
0x70 0x3A F1 0x71 0x3B F2
0x72 0x3C F3 0x73 0x3D F4
0x74 0x3E F5 0x75 0x3F F6
0x76 0x40 F7 0x77 0x41 F8
0x78 0x42 F9 0x79 0x43 F10
0x7A 0x44 F11 0x7B 0x45 F12
0x41 0x04 A 0x42 0x05 B
...through... 0x5A 0x1D Z
0x30 0x27 0 0x31 0x1E 1
...through... 0x39 0x26 9
0xBA 0x33 ; (OEM_1) 0xBB 0x2E = (OEM_PLUS)
0xBC 0x36 , (OEM_COMMA) 0xBD 0x2D - (OEM_MINUS)
0xBE 0x37 . (OEM_PERIOD) 0xBF 0x38 / (OEM_2)
0xC0 0x35 ` (OEM_3) 0xDB 0x2F [ (OEM_4)
0xDC 0x31 \\ (OEM_5) 0xDD 0x30 ] (OEM_6)
0xDE 0x34 ' (OEM_7)
0x2C 0x46 Print Screen 0x91 0x47 Scroll Lock
0x13 0x48 Pause 0x2D 0x49 Insert
0x24 0x4A Home 0x21 0x4B Page Up
0x2E 0x4C Delete 0x23 0x4D End
0x22 0x4E Page Down 0x26 0x52 Up
0x28 0x51 Down 0x25 0x50 Left
0x27 0x4F Right
0xA0 0xE1 Left Shift 0xA1 0xE5 Right Shift
0xA2 0xE0 Left Control 0xA3 0xE4 Right Control
0xA4 0xE2 Left Alt 0xA5 0xE6 Right Alt
0x5B 0xE3 Left Win (GUI) 0x5C 0xE7 Right Win (GUI)
0x5D 0x65 Application
0xE2 0x64 Non-US \\
══════════════════════════════════════════════════════════════════════════
TIMING AND SEQUENCING
══════════════════════════════════════════════════════════════════════════
Delay constants (from disassembly of KeyboardDrv.exe):
100ms (0x64) Retry delay on SetFeature/GetFeature failure
200ms (0xC8) Retry delay on SetLedMatrix/SetMacroData failure
50ms (0x32) Delay between sequential SetLedMatrix calls
30ms (0x1E) Delay after GetMacroData query before reading response
20ms (0x14) Delay between GetProfile query and response read
20ms (0x14) Delay after SetProfile before next operation
500ms (0x1F4) Final delay after all operations complete
100ms (0x64) Device detection retry (up to 20 attempts)
3 Maximum retry count for any single HID operation
Full ApplySetting sequence (from 0x411190 in KeyboardDrv.exe):
1. WaitForSingleObject on sync mutex (infinite timeout)
2. GetProfile via query 0x83/0xC6 (read current LED state)
If fail and not InitByFile mode: abort
3. Sleep(20ms)
4. Build LED RGB table from effect parameters
For each key (up to nEffectKeyNum keys):
Read key's effect index, map to RGB values
Store in 3 planar channels with stride 0x7E per channel
5. SetLedRgbTab (send per-key RGB data)
If fail: abort (unless in non-strict mode)
6. Sleep(50ms)
7. SetLedMatrix with param1, size 0xCC (204 bytes)
If fail: abort
8. Sleep(50ms)
9. SetLedMatrix with param2, size 0xD0 (208 bytes)
If fail: abort
10. Sleep(50ms)
11. If flag & 0x01 (key matrix dirty):
a. Build key/macro data from key objects
b. For each key (0-0x83 = 132 keys):
If key type == 0x05 (macro): look up macro, allocate buffer
Write 4-byte function code to matrix[key.matrix_index]
c. GetMacroData for already-used slots
d. Sleep(30ms)
e. SetMacroData for each modified slot
Multi-chunk: chunks of 0x400 bytes, buffer_id increments by 4
f. Sleep(50ms) between chunks
g. Format key matrix with FillMatrix debug helper
h. SetKeyMatrix (send 1024 bytes)
If fail: abort
i. Sleep(50ms)
12. If flag & 0x02 (profile dirty):
a. SetProfile (send LED state as profile)
If fail: abort
b. Sleep(20ms)
13. Sleep(500ms) (final settle time)
14. Signal completion event
══════════════════════════════════════════════════════════════════════════
CFG.INI FILE FORMAT
══════════════════════════════════════════════════════════════════════════
Located at: <install_dir>/K621RGB/Cfg.ini
Sections: [OPT], [FN], [KEY]
[OPT] section key fields:
VID=0x258A, PID=0x0049 Device identification
MaxEftKeyIndex=104 Max key index for effects
MacroBufferNum=12 Number of macro slots
StartMacro=0xE404 Base macro function ID
LEDParam=07,29,07,29,... Default LED zone parameters
SpeedHW=1,2,3,4 Hardware speed levels
LightHW=0,1,2,3,4,5,6,7,8,9 Hardware brightness levels
GaoshouKey1-5=<hex_list> Game preset key lists
LedOpt1-22 LED pattern definitions
[KEY] section format:
K<n>=x1,y1,x2,y2, type,vkey,modifier,led_offset,led_index
x1,y1,x2,y2: UI button rectangle coordinates (pixels)
type: 0x02 = standard key
vkey: Windows virtual key code
modifier: 0x00 for non-modifier keys
led_offset: Matrix index (position in key remapping data)
led_index: LED index (position in RGB color data)
[FN] section format:
K<n>=type,vkey,function_code[,game_index]
type: 0x09 = system/LED function, 0x02 = key passthrough
vkey: 0x00 for system, or VK code for passthrough
function_code: 32-bit function identifier (see Fn function codes)
game_index: Optional game preset binding index
══════════════════════════════════════════════════════════════════════════
INPUT REPORTS (Device Thread)
══════════════════════════════════════════════════════════════════════════
KeyboardDrv.exe runs a background thread (DThread) that reads input
reports from the device for status updates (device connect/disconnect).
Thread debug strings:
"DThread Start..."
"Buffer= %x %x %x %x, NumberOfBytesRead=%d"
"2 Buffer= %x %x %x %x, NumberOfBytesRead=%d"
"Thread exit!"
"m_hDevice=0x%x, m_hCmd=0x%x, hMedia=0x%x"
The thread monitors three handles: m_hDevice, m_hCmd, hMedia
Uses WaitForMultipleObjects to detect device events.
When data arrives, it reads 4+ bytes and dispatches based on content.
Device detection:
The software creates events via CreateEvent and polls via
WaitForMultipleObjects with device handles. On "Device IN" /
"Device OUT" events, it reinitializes or closes the connection.
══════════════════════════════════════════════════════════════════════════
BINARY ANALYSIS REFERENCE
══════════════════════════════════════════════════════════════════════════
Key functions in KeyboardDrv.exe (RVAs):
0x4010F0 Apply thread entry point
0x411190 ApplySetting (main protocol orchestrator)
0x446670 Lowerdev.dll initialization (LoadLibrary + GetProcAddress)
0x4467E0 SendCommand (SetFeature wrapper with retry)
0x446870 GetCommand (GetFeature wrapper with retry)
0x446900 SendCommand variant (used by SetLedMatrix, SetMacroData)
0x446980 GetPassword / firmware version query
0x446A80 SetProfile
0x446B20 GetProfile (LED state)
0x446C10 SetLedMatrix
0x446CC0 SetKeyMatrix
0x446D70 GetProfile (key matrix)
0x446E50 SetLedRgbTab
0x446F30 SetMacroData
0x446FE0 GetMacroData
0x4470D0 VK-to-HID keycode lookup
0x410D80 Build key/macro data from UI state
0x410FC0 Debug print key matrix
0x405A70 FillMatrix (format binary data as hex for debug)
Driver object vtable layout (esi-relative in init function):
+0x04 hModule (LoadLibrary handle)
+0x08 OpenHidDevice function pointer
+0x0C SetFeature function pointer
+0x10 GetFeature function pointer
+0x14 GetInputReport function pointer
+0x18 GetProductString function pointer
+0x1C GetVersionNumber function pointer
Lowerdev.dll exports (RVAs):
0x14E0 SetFeature(handle, buffer, size) -> bool
0x1500 GetFeature(handle, buffer, size) -> bool
Both are thin wrappers calling HidD_SetFeature / HidD_GetFeature.
""")
def hex_dump(data, width=16):
"""Print a hex dump of data."""
for i in range(0, len(data), width):
chunk = data[i : i + width]
hex_part = " ".join(f"{b:02X}" for b in chunk)
ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
print(f" {i:04X} {hex_part:<{width * 3}} {ascii_part}")
def main():
global verbose
parser = argparse.ArgumentParser(
description="Control utility for Redragon K621 Horus TKL RGB keyboard",
)
parser.add_argument(
"--verbose",
"-v",
action="count",
default=0,
help="Increase verbosity (repeat for more)",
)
sub = parser.add_subparsers(dest="command", help="Command to execute")
# on
p = sub.add_parser("on", help="Turn backlight on")
p.add_argument(
"--pattern", "-p", default="fixed_on", help="Pattern name (default: fixed_on)"
)
p.add_argument(
"--brightness", "-b", type=int, default=None, help="Brightness 0-100%%"
)
p.add_argument("--speed", "-s", type=int, default=None, help="Speed 0-100%%")
p.add_argument("--color", "-c", default=None, help="Color as RRGGBB hex")
# off
sub.add_parser("off", help="Turn backlight off")
# test — diagnostic: sends the byte-identical original packet
p = sub.add_parser(
"test", help="Send byte-identical packet from redragon-lights.py"
)
p.add_argument(
"--on",
"-1",
dest="switch",
action="store_true",
default=True,
help="Send ON (shadow_disappear)",
)
p.add_argument("--off", "-0", dest="switch", action="store_false", help="Send OFF")
# pattern
p = sub.add_parser("pattern", help="Set LED pattern")
p.add_argument("name", help="Pattern name (see list-patterns)")
p.add_argument(
"--brightness", "-b", type=int, default=None, help="Brightness 0-100%%"
)
p.add_argument("--speed", "-s", type=int, default=None, help="Speed 0-100%%")
p.add_argument("--color", "-c", default=None, help="Color as RRGGBB hex")
# custom
p = sub.add_parser("custom", help="Set custom per-key RGB")
p.add_argument(
"--colors",
"-c",
required=True,
help="JSON file mapping key names to RRGGBB colors",
)
p.add_argument(
"--brightness", "-b", type=int, default=None, help="Brightness 0-100%%"
)
# game
p = sub.add_parser("game", help="Apply game mode preset")
p.add_argument("preset", help="Preset name (see list-games)")
p.add_argument(
"--color",
"-c",
default="FF0000",
help="Highlight color as RRGGBB (default: FF0000)",
)
p.add_argument(
"--background",
"-g",
default="000000",
help="Background color as RRGGBB (default: 000000)",
)
p.add_argument(
"--brightness", "-b", type=int, default=None, help="Brightness 0-100%%"
)
# state
sub.add_parser("state", help="Query keyboard state")
# reset
sub.add_parser("reset", help="Reset USB driver")
# list-patterns
sub.add_parser("list-patterns", help="List available LED patterns")
# list-keys
sub.add_parser("list-keys", help="List key names for custom RGB")
# list-games
sub.add_parser("list-games", help="List game mode presets")
# remap
p = sub.add_parser("remap", help="Remap keys on the keyboard")
p.add_argument(
"mapping",
nargs="*",
help="Key mappings in SRC=DST format (e.g., CAPSLOCK=LCTRL)",
)
p.add_argument(
"--reset", "-r", action="store_true", help="Reset all remaps to default"
)
# macro
p = sub.add_parser("macro", help="Program a keyboard macro")
p.add_argument("slot", type=int, help=f"Macro slot number (0-{MACRO_BUFFER_COUNT - 1})")
p.add_argument(
"--sequence",
"-s",
help="Macro sequence (e.g., 'ctrl+c' or 'ctrl+a ctrl+c')",
)
p.add_argument("--read", action="store_true", help="Read current macro from slot")
p.add_argument("--clear", action="store_true", help="Clear macro slot")
# profile
p = sub.add_parser("profile", help="Save/load keyboard profile")
p.add_argument("--save", metavar="FILE", help="Save current profile to JSON file")
p.add_argument("--load", metavar="FILE", help="Load profile from JSON file")
p.add_argument("--dump", action="store_true", help="Dump current profile state")
# protocol
sub.add_parser("protocol", help="Display USB protocol documentation")
args = parser.parse_args()
verbose = args.verbose
commands = {
"on": cmd_on,
"off": cmd_off,
"test": cmd_test,
"pattern": cmd_pattern,
"custom": cmd_custom,
"game": cmd_game,
"state": cmd_state,
"reset": cmd_reset,
"list-patterns": cmd_list_patterns,
"list-keys": cmd_list_keys,
"list-games": cmd_list_games,
"remap": cmd_remap,
"macro": cmd_macro,
"profile": cmd_profile,
"protocol": cmd_protocol,
}
if args.command is None:
parser.print_help()
sys.exit(1)
commands[args.command](args)
if __name__ == "__main__":
main()