Search code examples
pythonwindowspygamepygletpsychopy

How to get USB controller/gamepad to work with python


I have a USB controller that I'm trying to get inputs from, a Microsoft® SideWinder® Plug & Play Game Pad. I'm having difficulties trying to figure out how to receive its inputs correctly. Unfortunately, I cannot use pygame as it requires a window to receive inputs from, but I have to generate a pyglet window (via PsychoPy) to run my program. With pygame it can connect and show the state of buttons, but it cannot receive inputs without creating a window. I tried looking for other libraries, but all I encountered was Inputs, which isn't compatible with my controller (doesn't detect the device after installing). The controller itself works as I've tested it with an online gamepad tester. PsychoPy's joystick API is currently broken and does not work, so no luck there either.

I was really hoping anyone had advice on how to receive inputs from my controller/gamepad into my program?


Solution

  • For Windows you can use the WINMM.dll directly.

    Use the ctypes library to load the dll (see Loading shared libraries). Use ctypes.WINFUNCTYPE to create prototypes for the function:

    import ctypes
    
    winmmdll = ctypes.WinDLL('winmm.dll')
    
    # [joyGetNumDevs](https://learn.microsoft.com/en-us/windows/win32/api/joystickapi/nf-joystickapi-joygetnumdevs)
    """
    UINT joyGetNumDevs();
    """
    joyGetNumDevs_proto = ctypes.WINFUNCTYPE(ctypes.c_uint)
    joyGetNumDevs_func  = joyGetNumDevs_proto(("joyGetNumDevs", winmmdll))
    
    # [joyGetDevCaps](https://learn.microsoft.com/en-us/windows/win32/api/joystickapi/nf-joystickapi-joygetdevcaps)
    """
    MMRESULT joyGetDevCaps(UINT uJoyID, LPJOYCAPS pjc, UINT cbjc);
    
    32 bit: joyGetDevCapsA
    64 bit: joyGetDevCapsW
    
    sizeof(JOYCAPS): 728
    """
    joyGetDevCaps_proto = ctypes.WINFUNCTYPE(ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p, ctypes.c_uint)
    joyGetDevCaps_param = (1, "uJoyID", 0), (1, "pjc", None), (1, "cbjc", 0)
    joyGetDevCaps_func  = joyGetDevCaps_proto(("joyGetDevCapsW", winmmdll), joyGetDevCaps_param)
    
    # [joyGetPosEx](https://learn.microsoft.com/en-us/windows/win32/api/joystickapi/nf-joystickapi-joygetposex)
    """
    MMRESULT joyGetPosEx(UINT uJoyID, LPJOYINFOEX pji);
    sizeof(JOYINFOEX): 52
    """
    joyGetPosEx_proto = ctypes.WINFUNCTYPE(ctypes.c_uint, ctypes.c_uint, ctypes.c_void_p)
    joyGetPosEx_param = (1, "uJoyID", 0), (1, "pji", None)
    joyGetPosEx_func  = joyGetPosEx_proto(("joyGetPosEx", winmmdll), joyGetPosEx_param)
    

    Create the python function joyGetNumDevs, joyGetDevCaps and joyGetPosEx which delegate to the DLL. And create classes for the types JOYCAPS respectively JOYINFOEX:

    # joystickapi - joyGetNumDevs
    def joyGetNumDevs():
        try:
            num = joyGetNumDevs_func()
        except:
            num = 0
        return num
    
    # joystickapi - joyGetDevCaps
    def joyGetDevCaps(uJoyID):
        try:
            buffer = (ctypes.c_ubyte * JOYCAPS.SIZE_W)()
            p1 = ctypes.c_uint(uJoyID)
            p2 = ctypes.cast(buffer, ctypes.c_void_p)
            p3 = ctypes.c_uint(JOYCAPS.SIZE_W)
            ret_val = joyGetDevCaps_func(p1, p2, p3)
            ret = (False, None) if ret_val != JOYERR_NOERROR else (True, JOYCAPS(buffer))   
        except:
            ret = False, None
        return ret 
    
    # joystickapi - joyGetPosEx
    def joyGetPosEx(uJoyID):
        try:
            buffer = (ctypes.c_uint32 * (JOYINFOEX.SIZE // 4))()
            buffer[0] = JOYINFOEX.SIZE
            buffer[1] = JOY_RETURNALL
            p1 = ctypes.c_uint(uJoyID)
            p2 = ctypes.cast(buffer, ctypes.c_void_p)
            ret_val = joyGetPosEx_func(p1, p2)
            ret = (False, None) if ret_val != JOYERR_NOERROR else (True, JOYINFOEX(buffer))   
        except:
            ret = False, None
        return ret 
    
    JOYERR_NOERROR = 0
    JOY_RETURNX = 0x00000001
    JOY_RETURNY = 0x00000002
    JOY_RETURNZ = 0x00000004
    JOY_RETURNR = 0x00000008
    JOY_RETURNU = 0x00000010
    JOY_RETURNV = 0x00000020
    JOY_RETURNPOV = 0x00000040
    JOY_RETURNBUTTONS = 0x00000080
    JOY_RETURNRAWDATA = 0x00000100
    JOY_RETURNPOVCTS = 0x00000200
    JOY_RETURNCENTERED = 0x00000400
    JOY_USEDEADZONE = 0x00000800
    JOY_RETURNALL = (JOY_RETURNX | JOY_RETURNY | JOY_RETURNZ | \
                     JOY_RETURNR | JOY_RETURNU | JOY_RETURNV | \
                     JOY_RETURNPOV | JOY_RETURNBUTTONS)
    
    # joystickapi - JOYCAPS
    class JOYCAPS:
        SIZE_W = 728
        OFFSET_V = 4 + 32*2
        def __init__(self, buffer):
            ushort_array = (ctypes.c_uint16 * 2).from_buffer(buffer)
            self.wMid, self.wPid = ushort_array  
    
            wchar_array = (ctypes.c_wchar * 32).from_buffer(buffer, 4)
            self.szPname = ctypes.cast(wchar_array, ctypes.c_wchar_p).value
            
            uint_array = (ctypes.c_uint32 * 19).from_buffer(buffer, JOYCAPS.OFFSET_V) 
            self.wXmin, self.wXmax, self.wYmin, self.wYmax, self.wZmin, self.wZmax, \
            self.wNumButtons, self.wPeriodMin, self.wPeriodMax, \
            self.wRmin, self.wRmax, self.wUmin, self.wUmax, self.wVmin, self.wVmax, \
            self.wCaps, self.wMaxAxes, self.wNumAxes, self.wMaxButtons = uint_array
    
    # joystickapi - JOYINFOEX
    class JOYINFOEX:
      SIZE = 52
      def __init__(self, buffer):
          uint_array = (ctypes.c_uint32 * (JOYINFOEX.SIZE // 4)).from_buffer(buffer) 
          self.dwSize, self.dwFlags, \
          self.dwXpos, self.dwYpos, self.dwZpos, self.dwRpos, self.dwUpos, self.dwVpos, \
          self.dwButtons, self.dwButtonNumber, self.dwPOV, self.dwReserved1, self.dwReserved2 = uint_array
    

    See the simple example to test the API:

    import joystickapi
    import msvcrt
    import time
    
    print("start")
    
    num = joystickapi.joyGetNumDevs()
    ret, caps, startinfo = False, None, None
    for id in range(num):
        ret, caps = joystickapi.joyGetDevCaps(id)
        if ret:
            print("gamepad detected: " + caps.szPname)
            ret, startinfo = joystickapi.joyGetPosEx(id)
            break
    else:
        print("no gamepad detected")
    
    run = ret
    while run:
        time.sleep(0.1)
        if msvcrt.kbhit() and msvcrt.getch() == chr(27).encode(): # detect ESC
            run = False
    
        ret, info = joystickapi.joyGetPosEx(id)
        if ret:
            btns = [(1 << i) & info.dwButtons != 0 for i in range(caps.wNumButtons)]
            axisXYZ = [info.dwXpos-startinfo.dwXpos, info.dwYpos-startinfo.dwYpos, info.dwZpos-startinfo.dwZpos]
            axisRUV = [info.dwRpos-startinfo.dwRpos, info.dwUpos-startinfo.dwUpos, info.dwVpos-startinfo.dwVpos]
            if info.dwButtons:
                print("buttons: ", btns)
            if any([abs(v) > 10 for v in axisXYZ]):
                print("axis:", axisXYZ)
            if any([abs(v) > 10 for v in axisRUV]):
                print("roation axis:", axisRUV)
    
    print("end")
    

    The api binding and example is provided in the GitHub repository python_windows_joystickapi