2392 lines
83 KiB
Python
Executable File
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()
|