Skip to content

Instantly share code, notes, and snippets.

@AldeRoberge
Created August 6, 2026 13:44
Show Gist options
  • Select an option

  • Save AldeRoberge/3be27d3f754f82c75808dc3c9318ccb6 to your computer and use it in GitHub Desktop.

Select an option

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.
"""
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