#!/usr/bin/env python3 """ Dump the full Framework EC keyboard matrix. Reads all matrix positions and prints those with non-zero scancodes. Usage: sudo python3 dump-matrix.py # or on NixOS: # nix-shell -p python3 --run 'sudo python3 dump-matrix.py' """ import subprocess import sys # PS/2 Scan Code Set 2 -> Key name mapping SCANCODE_NAMES = { 0x00: "(none)", 0x01: "F9", 0x03: "F5", 0x04: "F3", 0x05: "F1", 0x06: "F2", 0x07: "F12", 0x09: "F10", 0x0A: "F8", 0x0B: "F6", 0x0C: "F4", 0x0D: "Tab", 0x0E: "`", 0x11: "L Alt", 0x12: "L Shift", 0x14: "L Ctrl", 0x15: "q", 0x16: "1", 0x1A: "z", 0x1B: "s", 0x1C: "a", 0x1D: "w", 0x1E: "2", 0x21: "c", 0x22: "x", 0x23: "d", 0x24: "e", 0x25: "4", 0x26: "3", 0x29: "Space", 0x2A: "v", 0x2B: "f", 0x2C: "t", 0x2D: "r", 0x2E: "5", 0x31: "n", 0x32: "b", 0x33: "h", 0x34: "g", 0x35: "y", 0x36: "6", 0x3A: "m", 0x3B: "j", 0x3C: "u", 0x3D: "7", 0x3E: "8", 0x41: ",", 0x42: "k", 0x43: "i", 0x44: "o", 0x45: "0", 0x46: "9", 0x49: ".", 0x4A: "/", 0x4B: "l", 0x4C: ";", 0x4D: "p", 0x4E: "-", 0x52: "'", 0x54: "[", 0x55: "=", 0x58: "Caps Lock", 0x59: "R Shift", 0x5A: "Enter", 0x5B: "]", 0x5D: "\\", 0x66: "Backspace", 0x69: "KP 1", 0x6B: "KP 4", 0x6C: "KP 7", 0x70: "KP 0", 0x71: "KP .", 0x72: "KP 2", 0x73: "KP 5", 0x74: "KP 6", 0x75: "KP 8", 0x76: "Esc", 0x77: "Num Lock", 0x78: "F11", 0x79: "KP +", 0x7A: "KP 3", 0x7B: "KP -", 0x7C: "KP *", 0x7D: "KP 9", 0x7E: "Scroll Lock", 0x83: "F7", 0xFF: "Fn", # Extended keys (E0 prefix) - stored as 16-bit values 0xE011: "R Alt", 0xE014: "R Ctrl", 0xE01F: "L GUI", 0xE027: "R GUI", 0xE02F: "Apps/Menu", 0xE04A: "KP /", 0xE05A: "KP Enter", 0xE069: "End", 0xE06B: "Left Arrow", 0xE06C: "Home", 0xE070: "Insert", 0xE071: "Delete", 0xE072: "Down Arrow", 0xE074: "Right Arrow", 0xE075: "Up Arrow", 0xE07A: "Page Down", 0xE07D: "Page Up", } def get_scancode_name(code): if code in SCANCODE_NAMES: return SCANCODE_NAMES[code] return f"unknown" def read_matrix_position(row, col): """Read the scancode at a given matrix position. Returns the 16-bit scancode.""" col_hex = format(col, 'x') cmd = f"ectool raw 0x3E0C d1,d0,b{row:x},b{col_hex},w0" try: result = subprocess.run( cmd.split(), capture_output=True, text=True, timeout=5 ) output = result.stdout + result.stderr # Parse the response bytes. Look for the "Read XXX bytes" line and # then parse the hex dump that follows. lines = output.strip().split('\n') response_bytes = [] reading = False for line in lines: if "Read" in line and "bytes" in line: reading = True continue if reading and '|' in line: # Extract hex bytes before the | hex_part = line.split('|')[0].strip() for byte_str in hex_part.split(): if len(byte_str) == 2: try: response_bytes.append(int(byte_str, 16)) except ValueError: pass # Scancode is at bytes 10-11 (little-endian 16-bit) if len(response_bytes) >= 12: scancode = response_bytes[10] | (response_bytes[11] << 8) return scancode return None except (subprocess.TimeoutExpired, Exception) as e: print(f" Error reading ({row},{col:x}): {e}", file=sys.stderr) return None def main(): if subprocess.os.geteuid() != 0: print("This script must be run as root (sudo).", file=sys.stderr) sys.exit(1) # Scan a wide range: rows 0-15, cols 0-15 # This covers well beyond the assumed 8x12 matrix max_row = 16 max_col = 16 print(f"Scanning matrix positions ({max_row} rows x {max_col} cols)...") print(f"{'Pos':>8} {'Scancode':>10} Key") print("-" * 40) found = [] for row in range(max_row): for col in range(max_col): scancode = read_matrix_position(row, col) if scancode is not None and scancode != 0: name = get_scancode_name(scancode) label = f"({row:x},{col:x})" sc_str = f"{scancode:04x}" if scancode > 0xFF else f"{scancode:02x}" print(f"{label:>8} {sc_str:>10} {name}") found.append((row, col, scancode, name)) # Print progress on stderr print(f"\r Scanning ({row:x},{col:x})... ", end='', file=sys.stderr, flush=True) print("\r ", file=sys.stderr) print("-" * 40) print(f"Found {len(found)} mapped positions.") # Print a visual grid print("\n=== Matrix Grid ===") print(f"{'':>6}", end='') for col in range(max_col): print(f" {col:x} ", end='') print() for row in range(max_row): print(f" {row:x} ", end='') for col in range(max_col): match = None for r, c, sc, name in found: if r == row and c == col: match = name break if match: print(f" {match:>6}", end='') else: print(f" .", end='') print() if __name__ == "__main__": main()