#!/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/ - Macro data write (id = buffer slot 0-11) 0x08/0xC8 - LED RGB table (per-key colors) 0x09/

- LED matrix (per-key effects) 0x83/0xC6 - LED state query 0x84/0xD8 - Profile/key matrix query 0x85/ - 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, , 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 = 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] = 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] = 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 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=,,[,] 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 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=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: /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= Game preset key lists LedOpt1-22 LED pattern definitions [KEY] section format: K=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=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()