diff --git a/redragon.py b/redragon.py index 9733c53..0f53e04 100755 --- a/redragon.py +++ b/redragon.py @@ -7,7 +7,22 @@ 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 and USB packet captures. +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 @@ -22,13 +37,44 @@ PRODUCT = 0x0049 INTERFACE = 1 # Report IDs and control transfer parameters -REPORT_LED = 0x06 # LED pattern/settings report -REPORT_STATE = 0x05 # State query/reset report -WVALUE_LED = 0x0306 # wValue for LED report -WVALUE_STATE = 0x0305 # wValue for state report +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) @@ -289,6 +335,76 @@ HID_USAGE_TO_NAME = { # 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 = { @@ -601,6 +717,217 @@ def build_custom_rgb(color_map): 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 ───────────────────────────────────────────── @@ -991,6 +1318,909 @@ def cmd_list_games(args): 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): @@ -1098,6 +2328,37 @@ def main(): # 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 @@ -1113,6 +2374,10 @@ def main(): "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: