Search code examples
pythonwinapictypes

Having trouble using winapi to read input from a device


I followed the steps here to try and read some input from a device. I've been trying for a couple hours now to figure out why GetMessage doesn't return anything. Originally I was trying to read from a certain device, but seeing as that wasn't working, I wanted to just try reading keyboard or mouse inputs. However, I've had no luck in doing so.

Edit: Some more info. I'm on Windows 10. I'm running the code in cmder (not sure if that makes any difference) with python main.py. There are no error messages and the output is Successfully registered input device! before the program just waits to receive a message from GetMessage.

Here's the running code:

main.py:

from ctypes import windll, sizeof, WinDLL, pointer, c_uint, create_string_buffer, POINTER
from ctypes.wintypes import *
from structures import *
from constants import *  # I put a comment specifying the value for each variable used from here


k32 = WinDLL('kernel32')
GetRawInputDeviceInfo = windll.user32.GetRawInputDeviceInfoA
GetRawInputDeviceInfo.argtypes = HANDLE, UINT, LPVOID, PUINT
RegisterRawInputDevices = windll.user32.RegisterRawInputDevices
RegisterRawInputDevices.argtypes = (RawInputDevice * 7), UINT, UINT
GetMessage = windll.user32.GetMessageA
GetMessage.argtypes = POINTER(Message), HWND, UINT, UINT


def print_error(code=None):
    print(f"Error code {k32.GetLastError() if code is None else code}")


def register_devices(hwnd_target=None):
    # Here I added all usages just to try and get any kind of response from GetMessage
    page = 0x01
    # DW_FLAGS is 0
    devices = (RawInputDevice * 7)(
        RawInputDevice(page, 0x01, DW_FLAGS, hwnd_target),
        RawInputDevice(page, 0x02, DW_FLAGS, hwnd_target),
        RawInputDevice(page, 0x04, DW_FLAGS, hwnd_target),
        RawInputDevice(page, 0x05, DW_FLAGS, hwnd_target),
        RawInputDevice(page, 0x06, DW_FLAGS, hwnd_target),
        RawInputDevice(page, 0x07, DW_FLAGS, hwnd_target),
        RawInputDevice(page, 0x08, DW_FLAGS, hwnd_target),
    )
    if not RegisterRawInputDevices(devices, len(devices), sizeof(devices[0])):
        print_error()
    else:
        print("Successfully registered input device!")


def get_message(h_wnd=None):
    msg = pointer(Message())
    # WM_INPUT is 0
    return_value = GetMessage(msg, h_wnd, WM_INPUT, WM_INPUT)
    if return_value == -1:
        print_error()
    elif return_value == 0:
        print("WM_QUIT message received.")
    else:
        print("Successfully got message!")
        return msg


register_devices()
print(get_message().contents.message)

structures.py:

from ctypes import Structure
from ctypes.wintypes import *


class RawInputDevice(Structure):
    _fields_ = [
        ("usUsagePage", USHORT),
        ("usUsage", USHORT),
        ("dwFlags", DWORD),
        ("hwndTarget", HWND),
    ]


class Message(Structure):
    _fields_ = [
        ("hwnd", HWND),
        ("message", UINT),
        ("wParam", WPARAM),
        ("lParam", LPARAM),
        ("time", DWORD),
        ("pt", POINT),
        ("lPrivate", DWORD)
    ]

I'd appreciate it if anyone helped me figure out what's going wrong, or I'd also be fine if someone can point out an alternative to reading input from an HID device on Windows.


Solution

  • I'm going to start with the (main) resources:

    I prepared an example.

    ctypes_wrappers.py:

    
    import ctypes as cts
    import ctypes.wintypes as wts
    
    
    HCURSOR = cts.c_void_p
    LRESULT = cts.c_ssize_t
    
    wndproc_args = (wts.HWND, wts.UINT, wts.WPARAM, wts.LPARAM)
    
    WNDPROC = cts.CFUNCTYPE(LRESULT, *wndproc_args)
    
    kernel32 = cts.WinDLL("Kernel32")
    user32 = cts.WinDLL("User32")
    
    
    def structure_to_string_method(self):
        ret = [f"{self.__class__.__name__} (size: {cts.sizeof(self.__class__)}) instance at 0x{id(self):016X}:"]
        for fn, _ in self._fields_:
            ret.append(f"  {fn}: {getattr(self, fn)}")
        return "\n".join(ret) + "\n"
    
    union_to_string_method = structure_to_string_method
    
    
    class Struct(cts.Structure):
        to_string = structure_to_string_method
    
    
    class Uni(cts.Union):
        to_string = union_to_string_method
    
    
    class WNDCLASSEXW(Struct):
        _fields_ = (
            ("cbSize", wts.UINT),
            ("style", wts.UINT),
            #("lpfnWndProc", cts.c_void_p),
            ("lpfnWndProc", WNDPROC),
            ("cbClsExtra", cts.c_int),
            ("cbWndExtra", cts.c_int),
            ("hInstance", wts.HINSTANCE),
            ("hIcon", wts.HICON),
            ("hCursor", HCURSOR),
            ("hbrBackground", wts.HBRUSH),
            ("lpszMenuName", wts.LPCWSTR),
            ("lpszClassName", wts.LPCWSTR),
            ("hIconSm", wts.HICON),
        )
    
    WNDCLASSEX = WNDCLASSEXW
    
    
    class RawInputDevice(Struct):
        _fields_ = (
            ("usUsagePage", wts.USHORT),
            ("usUsage", wts.USHORT),
            ("dwFlags", wts.DWORD),
            ("hwndTarget", wts.HWND),
        )
    
    PRawInputDevice = cts.POINTER(RawInputDevice)
    
    
    class RAWINPUTHEADER(Struct):
        _fields_ = (
            ("dwType", wts.DWORD),
            ("dwSize", wts.DWORD),
            ("hDevice", wts.HANDLE),
            ("wParam", wts.WPARAM),
        )
    
    
    class RAWMOUSE(Struct):
        _fields_ = (
            ("usFlags", wts.USHORT),
            ("ulButtons", wts.ULONG),  # unnamed union: 2 USHORTS: flags, data
            ("ulRawButtons", wts.ULONG),
            ("lLastX", wts.LONG),
            ("lLastY", wts.LONG),
            ("ulExtraInformation", wts.ULONG),
        )
    
    
    class RAWKEYBOARD(Struct):
        _fields_ = (
            ("MakeCode", wts.USHORT),
            ("Flags", wts.USHORT),
            ("Reserved", wts.USHORT),
            ("VKey", wts.USHORT),
            ("Message", wts.UINT),
            ("ExtraInformation", wts.ULONG),
        )
    
    
    class RAWHID(Struct):
        _fields_ = (
            ("dwSizeHid", wts.DWORD),
            ("dwCount", wts.DWORD),
            ("bRawData", wts.BYTE * 1),  # @TODO - cfati: https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawhid, but not very usable via CTypes
        )
    
    
    class RAWINPUT_U0(Uni):
        _fields_ = (
            ("mouse", RAWMOUSE),
            ("keyboard", RAWKEYBOARD),
            ("hid", RAWHID),
        )
    
    
    class RAWINPUT(Struct):
        _fields_ = (
            ("header", RAWINPUTHEADER),
            ("data", RAWINPUT_U0),
        )
    
    PRAWINPUT = cts.POINTER(RAWINPUT)
    
    
    GetLastError = kernel32.GetLastError
    GetLastError.argtypes = ()
    GetLastError.restype = wts.DWORD
    
    GetModuleHandle = kernel32.GetModuleHandleW
    GetModuleHandle.argtypes = (wts.LPWSTR,)
    GetModuleHandle.restype = wts.HMODULE
    
    
    DefWindowProc = user32.DefWindowProcW
    DefWindowProc.argtypes = wndproc_args
    DefWindowProc.restype = LRESULT
    
    RegisterClassEx = user32.RegisterClassExW
    RegisterClassEx.argtypes = (cts.POINTER(WNDCLASSEX),)
    RegisterClassEx.restype = wts.ATOM
    
    CreateWindowEx = user32.CreateWindowExW
    CreateWindowEx.argtypes = (wts.DWORD, wts.LPCWSTR, wts.LPCWSTR, wts.DWORD, cts.c_int, cts.c_int, cts.c_int, cts.c_int, wts.HWND, wts.HMENU, wts.HINSTANCE, wts.LPVOID)
    CreateWindowEx.restype = wts.HWND
    
    RegisterRawInputDevices = user32.RegisterRawInputDevices
    RegisterRawInputDevices.argtypes = (PRawInputDevice, wts.UINT, wts.UINT)
    RegisterRawInputDevices.restype = wts.BOOL
    
    GetRawInputData = user32.GetRawInputData
    GetRawInputData.argtypes = (PRAWINPUT, wts.UINT, wts.LPVOID, wts.PUINT, wts.UINT)
    GetRawInputData.restype = wts.UINT
    
    GetMessage = user32.GetMessageW
    GetMessage.argtypes = (wts.LPMSG, wts.HWND, wts.UINT, wts.UINT)
    GetMessage.restype = wts.BOOL
    
    PeekMessage = user32.PeekMessageW
    PeekMessage.argtypes = (wts.LPMSG, wts.HWND, wts.UINT, wts.UINT, wts.UINT)
    PeekMessage.restype = wts.BOOL
    
    TranslateMessage = user32.TranslateMessage
    TranslateMessage.argtypes = (wts.LPMSG,)
    TranslateMessage.restype = wts.BOOL
    
    DispatchMessage = user32.DispatchMessageW
    DispatchMessage.argtypes = (wts.LPMSG,)
    DispatchMessage.restype = LRESULT
    
    PostQuitMessage = user32.PostQuitMessage
    PostQuitMessage.argtypes = (cts.c_int,)
    PostQuitMessage.restype = None
    

    code00.py:

    #!/usr/bin/env python
    
    import ctypes as cts
    import ctypes.wintypes as wts
    import sys
    import time
    
    import ctypes_wrappers as cws
    
    
    HWND_MESSAGE = -3
    
    WM_QUIT = 0x0012
    WM_INPUT = 0x00FF
    WM_KEYUP = 0x0101
    WM_CHAR = 0x0102
    
    HID_USAGE_PAGE_GENERIC = 0x01
    
    RIDEV_NOLEGACY = 0x00000030
    RIDEV_INPUTSINK = 0x00000100
    RIDEV_CAPTUREMOUSE = 0x00000200
    
    RID_HEADER = 0x10000005
    RID_INPUT = 0x10000003
    
    RIM_TYPEMOUSE = 0
    RIM_TYPEKEYBOARD = 1
    RIM_TYPEHID = 2
    
    PM_NOREMOVE = 0x0000
    
    
    def wnd_proc(hwnd, msg, wparam, lparam):
        print(f"Handle message - hwnd: 0x{hwnd:016X} msg: 0x{msg:08X} wp: 0x{wparam:016X} lp: 0x{lparam:016X}")
        if msg == WM_INPUT:
            size = wts.UINT(0)
            res = cws.GetRawInputData(cts.cast(lparam, cws.PRAWINPUT), RID_INPUT, None, cts.byref(size), cts.sizeof(cws.RAWINPUTHEADER))
            if res == wts.UINT(-1) or size == 0:
                print_error(text="GetRawInputData 0")
                return 0
            buf = cts.create_string_buffer(size.value)
            res = cws.GetRawInputData(cts.cast(lparam, cws.PRAWINPUT), RID_INPUT, buf, cts.byref(size), cts.sizeof(cws.RAWINPUTHEADER))
            if res != size.value:
                print_error(text="GetRawInputData 1")
                return 0
            #print("kkt: ", cts.cast(lparam, cws.PRAWINPUT).contents.to_string())
            ri = cts.cast(buf, cws.PRAWINPUT).contents
            #print(ri.to_string())
            head = ri.header
            print(head.to_string())
            #print(ri.data.mouse.to_string())
            #print(ri.data.keyboard.to_string())
            #print(ri.data.hid.to_string())
            if head.dwType == RIM_TYPEMOUSE:
                data = ri.data.mouse
            elif head.dwType == RIM_TYPEKEYBOARD:
                data = ri.data.keyboard
                if data.VKey == 0x1B:
                    cws.PostQuitMessage(0)
            elif head.dwType == RIM_TYPEHID:
                data = ri.data.hid
            else:
                print("Wrong raw input type!!!")
                return 0
            print(data.to_string())
        return cws.DefWindowProc(hwnd, msg, wparam, lparam)
    
    
    def print_error(code=None, text=None):
        text = text + " - e" if text else "E"
        code = cws.GetLastError() if code is None else code
        print(f"{text}rror code: {code}")
    
    
    def register_devices(hwnd=None):
        flags = RIDEV_INPUTSINK  # @TODO - cfati: If setting to 0, GetMessage hangs
        generic_usage_ids = (0x01, 0x02, 0x04, 0x05, 0x06, 0x07, 0x08)
        devices = (cws.RawInputDevice * len(generic_usage_ids))(
            *(cws.RawInputDevice(HID_USAGE_PAGE_GENERIC, uid, flags, hwnd) for uid in generic_usage_ids)
        )
        #for d in devices: print(d.usUsagePage, d.usUsage, d.dwFlags, d.hwndTarget)
        if cws.RegisterRawInputDevices(devices, len(generic_usage_ids), cts.sizeof(cws.RawInputDevice)):
            print("Successfully registered input device(s)!")
            return True
        else:
            print_error(text="RegisterRawInputDevices")
            return False
    
    
    def main(*argv):
        wnd_cls = "SO049572093_RawInputWndClass"
        wcx = cws.WNDCLASSEX()
        wcx.cbSize = cts.sizeof(cws.WNDCLASSEX)
        #wcx.lpfnWndProc = cts.cast(cws.DefWindowProc, cts.c_void_p)
        wcx.lpfnWndProc = cws.WNDPROC(wnd_proc)
        wcx.hInstance = cws.GetModuleHandle(None)
        wcx.lpszClassName = wnd_cls
        #print(dir(wcx))
        res = cws.RegisterClassEx(cts.byref(wcx))
        if not res:
            print_error(text="RegisterClass")
            return 0
        hwnd = cws.CreateWindowEx(0, wnd_cls, None, 0, 0, 0, 0, 0, 0, None, wcx.hInstance, None)
        if not hwnd:
            print_error(text="CreateWindowEx")
            return 0
        #print("hwnd:", hwnd)
        if not register_devices(hwnd):
            return 0
        msg = wts.MSG()
        pmsg = cts.byref(msg)
        print("Start loop (press <ESC> to exit)...")
        while res := cws.GetMessage(pmsg, None, 0, 0):
            if res < 0:
                print_error(text="GetMessage")
                break
            cws.TranslateMessage(pmsg)
            cws.DispatchMessage(pmsg)
    
    
    if __name__ == "__main__":
        print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
                                                        64 if sys.maxsize > 0x100000000 else 32, sys.platform))
        rc = main(*sys.argv[1:])
        print("\nDone.\n")
        sys.exit(rc)
    

    Output:

    [cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q071994439]> "e:\Work\Dev\VEnvs\py_pc064_03.09_test0\Scripts\python.exe" code00.py
    Python 3.9.9 (tags/v3.9.9:ccb0e6a, Nov 15 2021, 18:08:50) [MSC v.1929 64 bit (AMD64)] 064bit on win32
    
    Handle message - hwnd: 0x00000000002F0606 msg: 0x00000024 wp: 0x0000000000000000 lp: 0x000000F5E0BEDDE0
    Handle message - hwnd: 0x00000000002F0606 msg: 0x00000081 wp: 0x0000000000000000 lp: 0x000000F5E0BEDD70
    Handle message - hwnd: 0x00000000002F0606 msg: 0x00000083 wp: 0x0000000000000000 lp: 0x000000F5E0BEDE00
    Handle message - hwnd: 0x00000000002F0606 msg: 0x00000001 wp: 0x0000000000000000 lp: 0x000000F5E0BEDD70
    Successfully registered input device(s)!
    Start loop (press <ESC> to exit)...
    Handle message - hwnd: 0x00000000002F0606 msg: 0x0000031F wp: 0x0000000000000001 lp: 0x0000000000000000
    Handle message - hwnd: 0x00000000002F0606 msg: 0x000000FF wp: 0x0000000000000001 lp: 0x-00000003849FCDF
    RAWINPUTHEADER (size: 24) instance at 0x00000296313BBBC0:
      dwType: 1
      dwSize: 40
      hDevice: 843780541
      wParam: 1
    
    RAWKEYBOARD (size: 16) instance at 0x00000296313BBCC0:
      MakeCode: 30
      Flags: 0
      Reserved: 0
      VKey: 65
      Message: 256
      ExtraInformation: 0
    
    Handle message - hwnd: 0x00000000002F0606 msg: 0x000000FF wp: 0x0000000000000001 lp: 0x0000000031AE1619
    RAWINPUTHEADER (size: 24) instance at 0x00000296313BBBC0:
      dwType: 1
      dwSize: 40
      hDevice: 843780541
      wParam: 1
    
    RAWKEYBOARD (size: 16) instance at 0x00000296313BBD40:
      MakeCode: 30
      Flags: 1
      Reserved: 0
      VKey: 65
      Message: 257
      ExtraInformation: 0
    
    Handle message - hwnd: 0x00000000002F0606 msg: 0x000000FF wp: 0x0000000000000001 lp: 0x000000007C851501
    RAWINPUTHEADER (size: 24) instance at 0x00000296313BBBC0:
      dwType: 0
      dwSize: 48
      hDevice: 4461491
      wParam: 1
    
    RAWMOUSE (size: 24) instance at 0x00000296313BBDC0:
      usFlags: 0
      ulButtons: 1
      ulRawButtons: 0
      lLastX: 0
      lLastY: 0
      ulExtraInformation: 0
    
    Handle message - hwnd: 0x00000000002F0606 msg: 0x000000FF wp: 0x0000000000000001 lp: 0x0000000031B41619
    RAWINPUTHEADER (size: 24) instance at 0x00000296313BBBC0:
      dwType: 0
      dwSize: 48
      hDevice: 4461491
      wParam: 1
    
    RAWMOUSE (size: 24) instance at 0x00000296313BBE40:
      usFlags: 0
      ulButtons: 2
      ulRawButtons: 0
      lLastX: 0
      lLastY: 0
      ulExtraInformation: 0
    
    Handle message - hwnd: 0x00000000002F0606 msg: 0x000000FF wp: 0x0000000000000001 lp: 0x0000000052D10665
    RAWINPUTHEADER (size: 24) instance at 0x00000296313BBBC0:
      dwType: 1
      dwSize: 40
      hDevice: 843780541
      wParam: 1
    
    RAWKEYBOARD (size: 16) instance at 0x00000296313BBEC0:
      MakeCode: 1
      Flags: 0
      Reserved: 0
      VKey: 27
      Message: 256
      ExtraInformation: 0
    
    
    Done.
    

    Notes:

    • The (above) output was generated by the following actions: a, LClick, ESC

    • I gave up on PyWin32, as it doesn't wrap the functions that we need (none of the Raw Input family), but it might be used (at least) for the constants from Win32Con (to avoid defining them)

    • I think things can be simplified, by moving functionality from wnd_proc to the while loop from main (and thus all the window class stuff (constants, structures, functions) could be dropped), but I started this way and I don't feel like changing it

    • A major breakthrough was RIDEV_INPUTSINK (since then, GetMessage stopped hanging)

    • RAWHID structure (@TODO) is wrapped "by the book", but it won't work OOTB (you mentioned working with other type of devices). That (1 sized) array at the end is just a way of stating that some additional data (dwSizeHid sized) will follow, which obviously won't fit in one byte. A "trick" is required there: the structure must be dynamically defined, based on the size of the data (example: [SO]: Setting _fields_ dynamically in ctypes.Structure (@CristiFati's answer)) - I remember that I wrote a newer answer on that topic, but I can't find or remember it), and all that behavior propagated (recursively) in all structures (unions) that encapsulate it