Created
August 6, 2026 13:44
-
-
Save AldeRoberge/3be27d3f754f82c75808dc3c9318ccb6 to your computer and use it in GitHub Desktop.
A Python uti.ity to debug print keys. It helped me solve an issue where a keyboard stuck in 'Verr' mode. Fn + Verr was the fix. Wrote by Claude Sonnet 5 Medium in 3 prompts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| keyboard_debug.py | |
| ------------------ | |
| Outil de debug clavier pour Windows utilisant l'API Raw Input (WM_INPUT). | |
| Ce script capte CHAQUE evenement clavier brut envoye par Windows, avant | |
| tout remapping logiciel, et affiche : | |
| - Le peripherique HID exact qui a envoye la touche (utile si tu as | |
| plusieurs claviers/recepteurs branches) | |
| - Le scan code brut (le vrai code materiel envoye par le clavier) | |
| - Le code virtuel (VK) que Windows en a deduit | |
| - Les flags (Make/Break, E0/E1 = "extended key") | |
| - Le nom lisible de la touche | |
| Pourquoi c'est utile pour ton probleme : | |
| - Si une touche censee etre "L" envoie un scan code de pave numerique | |
| (E0 + code numpad), ca confirme un probleme de Num Lock / overlay | |
| numerique plutot qu'un vrai bris materiel. | |
| - Si le flag E0 apparait sur des touches qui ne devraient pas l'avoir, | |
| ca indique que le clavier (ou son firmware/recepteur) pense qu'une | |
| touche modificatrice (Fn/Num Lock) est activee en permanence. | |
| - Si DEUX peripheriques HID differents envoient des evenements pour | |
| la meme touche physique, ca peut indiquer un probleme de pairage | |
| du recepteur sans fil. | |
| Installation : | |
| pip install pywin32 | |
| Utilisation : | |
| python keyboard_debug.py | |
| Appuie sur les touches problematiques, observe la console, puis | |
| ferme la fenetre (ou Ctrl+C dans le terminal) pour arreter. | |
| """ | |
| import ctypes | |
| from ctypes import wintypes | |
| import sys | |
| import time | |
| try: | |
| import win32api | |
| import win32con | |
| import win32gui | |
| except ImportError: | |
| print("Ce script requiert pywin32. Installe-le avec :") | |
| print(" pip install pywin32") | |
| sys.exit(1) | |
| user32 = ctypes.windll.user32 | |
| # --------------------------------------------------------------------------- | |
| # Structures Raw Input (definies manuellement car pywin32 n'expose pas tout) | |
| # --------------------------------------------------------------------------- | |
| class RAWINPUTDEVICE(ctypes.Structure): | |
| _fields_ = [ | |
| ("usUsagePage", wintypes.USHORT), | |
| ("usUsage", wintypes.USHORT), | |
| ("dwFlags", wintypes.DWORD), | |
| ("hwndTarget", wintypes.HWND), | |
| ] | |
| class RAWINPUTHEADER(ctypes.Structure): | |
| _fields_ = [ | |
| ("dwType", wintypes.DWORD), | |
| ("dwSize", wintypes.DWORD), | |
| ("hDevice", wintypes.HANDLE), | |
| ("wParam", wintypes.WPARAM), | |
| ] | |
| class RAWKEYBOARD(ctypes.Structure): | |
| _fields_ = [ | |
| ("MakeCode", wintypes.USHORT), | |
| ("Flags", wintypes.USHORT), | |
| ("Reserved", wintypes.USHORT), | |
| ("VKey", wintypes.USHORT), | |
| ("Message", wintypes.UINT), | |
| ("ExtraInformation", wintypes.ULONG), | |
| ] | |
| class RAWINPUT(ctypes.Structure): | |
| _fields_ = [ | |
| ("header", RAWINPUTHEADER), | |
| ("keyboard", RAWKEYBOARD), | |
| ] | |
| RIDEV_INPUTSINK = 0x00000100 | |
| RID_INPUT = 0x10000003 | |
| RIM_TYPEKEYBOARD = 1 | |
| RIDI_DEVICENAME = 0x20000007 | |
| RIDI_DEVICEINFO = 0x2000000B | |
| WM_INPUT = 0x00FF | |
| # Flags dans RAWKEYBOARD.Flags | |
| RI_KEY_MAKE = 0x0000 # touche pressee | |
| RI_KEY_BREAK = 0x0001 # touche relachee | |
| RI_KEY_E0 = 0x0002 # prefixe "extended" E0 | |
| RI_KEY_E1 = 0x0004 # prefixe "extended" E1 | |
| # --------------------------------------------------------------------------- | |
| # Cache des noms de peripheriques (chemin HID -> nom lisible si possible) | |
| # --------------------------------------------------------------------------- | |
| _device_name_cache = {} | |
| def get_device_path(hDevice): | |
| """Recupere le chemin systeme du peripherique HID (identifie le port/recepteur).""" | |
| if hDevice in _device_name_cache: | |
| return _device_name_cache[hDevice] | |
| size = wintypes.UINT(0) | |
| user32.GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, None, ctypes.byref(size)) | |
| if size.value == 0: | |
| _device_name_cache[hDevice] = "(inconnu)" | |
| return _device_name_cache[hDevice] | |
| buf = ctypes.create_unicode_buffer(size.value) | |
| user32.GetRawInputDeviceInfoW(hDevice, RIDI_DEVICENAME, buf, ctypes.byref(size)) | |
| path = buf.value | |
| _device_name_cache[hDevice] = path | |
| return path | |
| def vk_to_name(vk): | |
| """Traduit un code virtuel (VK) en nom lisible.""" | |
| try: | |
| return win32api.MapVirtualKey(vk, 2) or f"VK_{vk:#04x}" | |
| except Exception: | |
| return f"VK_{vk:#04x}" | |
| # --------------------------------------------------------------------------- | |
| # Fenetre invisible qui recoit les messages WM_INPUT | |
| # --------------------------------------------------------------------------- | |
| class KeyboardDebugWindow: | |
| def __init__(self): | |
| self.hwnd = None | |
| self._register_class() | |
| self._create_window() | |
| self._register_raw_input() | |
| self.start_time = time.time() | |
| self.event_count = 0 | |
| def _register_class(self): | |
| wc = win32gui.WNDCLASS() | |
| wc.lpfnWndProc = self._wnd_proc | |
| wc.lpszClassName = "KeyboardDebugWindowClass" | |
| wc.hInstance = win32api.GetModuleHandle(None) | |
| self.class_atom = win32gui.RegisterClass(wc) | |
| def _create_window(self): | |
| self.hwnd = win32gui.CreateWindow( | |
| self.class_atom, | |
| "KeyboardDebug", | |
| 0, 0, 0, 0, 0, | |
| 0, 0, | |
| win32api.GetModuleHandle(None), | |
| None, | |
| ) | |
| def _register_raw_input(self): | |
| # Usage Page 0x01 = Generic Desktop, Usage 0x06 = Keyboard | |
| rid = RAWINPUTDEVICE() | |
| rid.usUsagePage = 0x01 | |
| rid.usUsage = 0x06 | |
| rid.dwFlags = RIDEV_INPUTSINK # recoit meme sans focus | |
| rid.hwndTarget = self.hwnd | |
| if not user32.RegisterRawInputDevices(ctypes.byref(rid), 1, ctypes.sizeof(rid)): | |
| raise ctypes.WinError() | |
| def _wnd_proc(self, hwnd, msg, wparam, lparam): | |
| if msg == WM_INPUT: | |
| self._handle_raw_input(lparam) | |
| return 0 | |
| elif msg == win32con.WM_DESTROY: | |
| win32gui.PostQuitMessage(0) | |
| return 0 | |
| return win32gui.DefWindowProc(hwnd, msg, wparam, lparam) | |
| def _handle_raw_input(self, lparam): | |
| hrawinput = wintypes.HANDLE(lparam) | |
| size = wintypes.UINT(0) | |
| user32.GetRawInputData( | |
| hrawinput, RID_INPUT, None, | |
| ctypes.byref(size), ctypes.sizeof(RAWINPUTHEADER) | |
| ) | |
| if size.value == 0: | |
| return | |
| buf = ctypes.create_string_buffer(size.value) | |
| read = user32.GetRawInputData( | |
| hrawinput, RID_INPUT, buf, | |
| ctypes.byref(size), ctypes.sizeof(RAWINPUTHEADER) | |
| ) | |
| if read != size.value: | |
| return | |
| raw = ctypes.cast(buf, ctypes.POINTER(RAWINPUT)).contents | |
| if raw.header.dwType != RIM_TYPEKEYBOARD: | |
| return | |
| kb = raw.keyboard | |
| self.event_count += 1 | |
| self._print_event(raw.header.hDevice, kb) | |
| def _print_event(self, hDevice, kb): | |
| flags = kb.Flags | |
| is_break = bool(flags & RI_KEY_BREAK) | |
| is_e0 = bool(flags & RI_KEY_E0) | |
| is_e1 = bool(flags & RI_KEY_E1) | |
| state = "RELACHEE" if is_break else "PRESSEE " | |
| ext = [] | |
| if is_e0: | |
| ext.append("E0") | |
| if is_e1: | |
| ext.append("E1") | |
| ext_str = "+".join(ext) if ext else "-" | |
| device_path = get_device_path(hDevice) | |
| # Le chemin HID complet est long; on affiche juste la partie utile | |
| device_short = device_path.split("#")[1] if "#" in device_path else device_path | |
| vk_name = vk_to_name(kb.VKey) | |
| t = time.time() - self.start_time | |
| print( | |
| f"[{t:8.3f}s] #{self.event_count:<5} {state} | " | |
| f"MakeCode(scan)=0x{kb.MakeCode:02X} | " | |
| f"Extended={ext_str:5} | " | |
| f"VKey=0x{kb.VKey:02X} ({vk_name}) | " | |
| f"Message=0x{kb.Message:04X} | " | |
| f"Device={device_short}" | |
| ) | |
| def run(self): | |
| print("=" * 100) | |
| print("Outil de debug clavier - Raw Input API") | |
| print("Appuie sur les touches problematiques (ex: la moitie du clavier affectee).") | |
| print("Ferme cette fenetre console ou fais Ctrl+C pour arreter.") | |
| print("=" * 100) | |
| print() | |
| try: | |
| win32gui.PumpMessages() | |
| except KeyboardInterrupt: | |
| print("\nArret demande par l'utilisateur.") | |
| if __name__ == "__main__": | |
| if sys.platform != "win32": | |
| print("Ce script est specifique a Windows (utilise l'API Raw Input).") | |
| sys.exit(1) | |
| app = KeyboardDebugWindow() | |
| app.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment