1

I am trying to talk to the XBOX ONE Controller via the Microsoft HID API without using XINPUT. I'm currently able to control all the rumble motors (including the force feedback triggers) by sending the packet using HidD_SetOutputReport(HANDLE, VOID*, ULONG). But I'm stuck reading the button values using HidD_GetInputReport(HANDLE, VOID*, ULONG) or ReadFile() / ReadFileEx() with and without the HANDLE being created with FILE_FLAG_OVERLAPPED and using OVERLAPPED and Windows Events.

I have already reverse engineered the USB URB protocol with the help of the following article https://github.com/quantus/xbox-one-controller-protocol. The main goal is to overcome the XINPUT overhead and writing a flexible framework so that I can integrate other gamepads as well.

That is what I accomplished:

  1. I have connected the gamepad via USB with my computer (So that I can read all the USB Packages sent and received from the device)
  2. I have found the controller’s path using SetupDiGetClassDevs(...), SetupDiEnumDeviceInfo(...), SetupDiEnumDeviceInterfaces(...) and SetupDiGetDeviceInterfaceDetail(...)
  3. I have created a handle to the device using HANDLE gamePad = CreateFile(path, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL)
  4. Using HidP_GetCaps(HANDLE, HIDP_CAPS*) doesn’t seem to return valid data since it reports a OutputReportByteLength of 0 but I am able to send Output reports of size 5 (Turn ON) and 9 (Set rumble motors)
  5. All in and outcoming data (At least buttons and Rumble motors) seem to follow the following pattern
byte 0: Package type
byte 1: was always 0x00
byte 2: Package number (always incrementing every package)
byte 3: Size of following data
byte 4+: <Data>
  1. With that information i was able to let the motors and triggers rumble as I desire

For example: Two of my output rumble packets look like this (Using the pulse length to dirty turn on and off the motors): Output packages in Device Monitoring Studio URB View This turns on and of all motors with the rumble motors speed at 0xFF and the triggers at speed 0xF0. This is how I did it:

struct RumbleContinous{
    BYTE mode;
    BYTE mask; // Motor Mask 0b 0 0 0 0 LT RT L R
    BYTE lTForce;
    BYTE rTForce;
    BYTE lForce;
    BYTE rForce;
    BYTE pulseLength;
    BYTE offTime;
    BYTE terminator; // Terminator / Dummy / ?? (XINPUT sends that as 0xEB!) / Changing seems to not make any changes
};

RumbleContinous rc = {0x00, 0x0F, 0xF0, 0xF0, 0xFF, 0xFF, 0xFF 0x00, 0xEB};

HidD_SetOutputReport(gamePad, (PVOID)&rc, sizeof(RumbleContinous ));

Now to my problem

Looking at the input packages from the controller it looks like you need to create a buffer of size 0x0E = 14, ZeroMemory it (or just write the first byte to 0 like MSDN is suggesting) and just call HidD_GetInputReport(HANDLE, buffer, 14) Input package in Device Monitoring Studio URB View

So what I did was calling HidD_FlushQueue() to make sure the next package is the input package. Then I insert a little delay so that I am able to change some controller values. After that I tried reading into a BYTE array with HidD_GetInputReport(HANDLE, cmd_in, 14) but the function always failed with GetLastError() == 0x00000057 // ERROR_INVALID_PARAMETER

Since HID is able to filter packages it may be required to allocate a buffer one byte larger than expected and pass the required report id to the buffer at location 0. This is what I did:

BYTE cmd_in[15];
ZeroMemory(cmd_in, 15);
cmd_in[0] = 0x20;
HidD_GetInputReport(gamePad, cmd_in, 15);

Still no success. Since the HidP_GetCaps(...) function reported an input report of 16 (But I don't trust this since it already fooled me with output report size of 0) I tried sweeping over many buffer sizes:

BYTE cmd_in[30];
for (UINT bs = 0; bs < 30; bs++) {
    ZeroMemory(cmd_in, 30);

    HidD_FlushQueue(gamePad); // Flushing works
    Sleep(500);
        
    if (HidD_GetInputReport(gamePad, cmd_in, bs)) {
        // Output result (ommited)
    }
    else {
        // Print error (ommited)
    }
}

and

BYTE cmd_in[30];
for (UINT bs = 0; bs < 30; bs++) {
    ZeroMemory(cmd_in, 30);
    cmd_in[0] = 0x20;

    HidD_FlushQueue(gamePad); // Flushing works
    Sleep(500);
        
    if (HidD_GetInputReport(gamePad, cmd_in, bs)) {
        // Output result (ommited)
    }
    else {
        // Print error (ommited)
    }
}

Still no success. According to the special required output format and the wrong HidP_GetCaps(...) readings i suspect that the XBOX ONE Gamepad Driver requires a special header already beeing in the input buffer (As far as I know HidD_GetInputReport(...) just calls the User / Kernel Mode Driver call back; So the driver is free to perform checks and reject all data being send to it)

Maybe anyone out there does know how to call HidD_GetInputReport(...) for the XBOX One controller

I know that it is possible to retrieve that input data since the SimpleHIDWrite is able to see the button states. Even through the format is totally different (The two triggers for example are combined in one byte. In the USB Packed each trigger has its own byte):

SimpleHIDWrite XBOX ONE Controller

I should also mention that the HIDWrite sees the data without the press of any button! Looking at the log from the SimpleHIDWrite it looked like it is reading RD from 00 15 bytes of data, having a 16-byte array and element 0 at 00 (Didn't work in my application). Or does it just dump all data coming in. If yes how is this possible? That would be an option for me too!

Ohjurot
  • 47
  • 9
  • The Xbox game controllers are not natively HID devices. The Xbox 360 Game Controller uses a protocol called "XUSB" and the Xbox One Game Controller uses "GIP", both of which are proprietary. In order to support older games that only use DirectInput, however, the drivers expose a HID device with basic features which is what you are able to see with the HID utility. XINPUT/Gaming.Windows.Input is using the native protocol to talk to the driver, not HID. – Chuck Walbourn Nov 08 '20 at 20:19
  • BTW the protocol name is in the driver filename: ``xusb22.sys`` is the Xbox 360 Common Controller driver used by XINPUT. ``xboxgip.sys`` is the Xbox One driver used by Windows.Gaming.Input. ``xboxgip.sys`` is actually emulating both HID and XUSB as well as it's native protocol to work with DirectInput and XINPUT. – Chuck Walbourn Nov 08 '20 at 20:25
  • @ChuckWalbourn Thank you for your comment! That is exactly what I suspected! `xboxgip.sys` would be the thing I am looking for (And `xusb22.sys` in the future) but sadly the source code is not available. However, I found the Windows.Gaming.Input UWP Sample which uses the triggers motors. Maybe I can figure out the call to `DeviceIoControl` or maybe any other new function I may occur. Would be nice to control the trigger motors via Bluetooth in "normal" c++ like they did [here](https://gist.github.com/mmozeiko/b8ccc54037a5eaf35432396feabbe435) – Ohjurot Nov 08 '20 at 23:51
  • GIP and XUSB ioctls are not documented. I have reverse engeneered XUSB ioctls (but not exact structs that should be provided as in\out params): https://github.com/nefarius/XInputHooker/issues/1 but these will not work with Xbox One Controllers. – DJm00n Nov 13 '20 at 11:55
  • Also note that newer Xbox One Controllers works via HID natively via bluetooth. Easiest possible solution for controlling trigger vibration on any Xbox One Controller via any connection type is to use Windows.Gaming.Input WinRT API. Here is example: https://github.com/DJm00n/cppwinrtgamepad – DJm00n Nov 13 '20 at 12:06
  • @DJm00n Thanks for your information and links! The WinRT input API looks promising... Maybe it can also help me with getting the ioctls. It would be really nice if you could use it without WinRT... I will get back to it as soon as I’m done with my Dual Sense reverse engineering. What Product and vendor ids are the newer controller reporting? Do you mean the latest / series X controller or the later models before series X? From the hardware point of view the new controller isn’t that different. Through it may requires an additional byte for the share button. – Ohjurot Nov 14 '20 at 12:53
  • @Ohjurot I talking about Xbox One controllers Model 1708 - it supports bluetooth and HID natively. There are several different models of them listed there: https://github.com/medusalix/xow Newer Xbox Series controllers (Model 1914) have VID=0x045E PID=0x0B12 I made some dumps of USB descriptors: https://gist.github.com/DJm00n/a6bbcb810879daa9354dee4a02a6b34e – DJm00n Nov 14 '20 at 13:10
  • @DJm00n Thats really helpful thank you! – Ohjurot Nov 14 '20 at 18:44

1 Answers1

0

I looked at what XINPUT is doing when executing the following code:

XINPUT_STATE s;
XInputGetState(0, &s);

It turned out that XINPUT is doing the same stuff I did until it comes to reading the data from the controler. Insteal of HidD_GetInputReport(...) XINPUT is calling DeviceIoControl(...). So what I did was a fast google serch "DeviceIoControl xbox" and tada here it is without the need to to figure out the memory layout on my own: Getting xbox controller input without xinput

Edit: Using DeviceIoControl(...) works even if the gamepad is connected via bluetooth while HidD_SetOutputReport(...) does not work when the gamepad is connected via bluetooth. I rember reading something that DeviceIoControl(...) via bluetooth requires an addition parameter to be present in the output buffer. But I'm currently trying to figure out a way to controle the rumble motors via DeviceIoControl(...). If you have any sugestions feel free to comment! The article in the link above only activates the two rumble motors but not the triggers!

Edit 2: I tried sweeping over DeviceIoControl(HANDLE, j, CHAR*, i, NULL, 0, DWORD*, NULL) from j 0x0 to 0xFFFFFFFF and i 0x1 to 0x3F. Well it worked... at first... but after a few values of j I got a Blue-Screen: WDF_Violation (At least I know how to crash a computer ;) )

Ohjurot
  • 47
  • 9