1

I am working on a C# program that would send a power off packet to my xbox series controller in order to turn it off.

In 2024 Microsoft released GIP docs and as far as I understand Xbox One and Series work with GIP.

I run a win10 pc and use an Xbox Series controller with a microsoft wireless dongle. From this post I learned that firstly I need to get a handle of XBOXGIP interface:

Usage of the GIP interface starts with acquiring a handle to the interface via a device path of \.\XboxGIP

HANDLE hFile = CreateFileW(L"\\\\.\\XboxGIP", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); 

Then I read the controller with ReadFile and was able to receive data that comes to the PC on the controller start. I got 3 different messages which are:

  1. A metadata message:
Received 315 bytes: 7E ED 82 C6 8B DA 00 00 04 20 00 00 27 01 00 00 00 00 00 00 5E 04 12 0B 10 00 01 00 00 00 00 00 00 00 00 00 00 00 23 01 CD 00 16 00 1B 00 1C 00 26 00 2F 00 4C 00 00 00 00 00 00 00 00 00 01 05 00 17 00 00 09 01 02 03 04 06 07 0C 0D 1E 08 01 04 05 06 0A 0C 0D 1E 01 1A 00 57 69 6E 64 6F 77 73 2E 58 62 6F 78 2E 49 6E 70 75 74 2E 47 61 6D 65 70 61 64 08 56 FF 76 97 FD 9B 81 45 AD 45 B6 45 BB A5 26 D6 2C 40 2E 08 DF 07 E1 45 A5 AB A3 12 7A F1 97 B5 E7 1F F3 B8 86 73 E9 40 A9 F8 2F 21 26 3A CF B7 FE D2 DD EC 87 D3 94 42 BD 96 1A 71 2E 3D C7 7D 6B E5 F2 87 BB C3 B1 49 82 65 FF FF F3 77 99 EE 1E 9B AD 34 AD 36 B5 4F 8A C7 17 23 4C 9F 54 6F 77 CE 34 7A E2 7D C6 45 8C A4 00 42 C0 8B D9 4A C0 C8 96 EA 16 B2 8B 44 BE 80 7E 5D EB 06 98 E2 03 17 00 20 2C 00 01 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 17 00 09 3C 00 01 00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 17 00 1E 40 00 01 00 22 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  1. A Hello message:
Received 53 bytes: 7E ED 82 C6 8B DA 00 00 02 20 00 00 21 00 00 00 00 00 00 00 7E ED 82 C6 8B DA 00 00 5E 04 12 0B 05 00 17 00 06 00 00 00 08 04 01 00 01 00 01 00 00 00 00 00 00 
  1. And device status messages which came every 500ms or so:
Received 24 bytes: 7E ED 82 C6 8B DA 00 00 03 20 00 00 04 00 00 00 00 00 00 00 8B 00 00 58 

Every message above starts with 7E ED 82 C6 8B DA 00 00 which is a unique ID of my device.

I was able to decode these messages with the help of the docs and chatGPT. They adhere to the docs but I can't figure out how to send a command back to the controller. Then I tried writing to the device with WriteFile. This is how a set device state should look like. I've tried two things:

  1. Sending just a packet with the command:
 powerOffCommand[0] = 0x05; // Command ID powerOffCommand[1] = 0x20; // Flags powerOffCommand[2] = 0x01; // Sequence number powerOffCommand[3] = 0x01; // payload length powerOffCommand[4] = 0x04; // payload. Power Off command. 

In this case I got an error saying that the device is not connected even though I am reading data from it.

  1. Sending a packet with the device id and the same command packet:
 powerOffCommand[0] = 0x7E; powerOffCommand[1] = 0xED; powerOffCommand[2] = 0x82; powerOffCommand[3] = 0xC6; powerOffCommand[4] = 0x8B; powerOffCommand[5] = 0xDA; powerOffCommand[6] = 0x00; powerOffCommand[7] = 0x00; powerOffCommand[8] = 0x05; // Command ID powerOffCommand[9] = 0x20; // Flags powerOffCommand[10] = 0x01; // Sequence number powerOffCommand[11] = 0x01; // payload length powerOffCommand[12] = 0x04; // payload. Power Off command. 

In this case I get an error saying that the parameter is incorrect which hints at the fact that it found the device but for some reason the packet I send doesn't satisfy it. Changing any of the ID bytes leads to Device is not connected error.

I am by no means proficient with C#. It is just a pet project that's why I am asking for help to send the command to the controller. The code below was written with the help of chatGPT and my meagre knowledge of programming in C# and Javascript.

Things to note:

  • There's might be something with the Sequence number. I tried sending different bytes like 0x01, 0x02, 0x03 and some other random but it didn't change anything.
  • Probably the info about xboxgip interface is outdated and I should write straight to the controller but when I try that I get an Access denied error. I looked up what proccesses locked the device but I couldn't figure out how to end dwm.exe in a way that wouldn't break my desktop GUI and also free the controller. I don't know how to stop the controller from sending input to the OS to control menus in Win10.
  • Also I found this table but I can't decipher it. Here the host sends a power off command but when I do the same it throws.But 0xD2 byte looks interesting. I don't understand what it's for. Figure 4-16: Downstream Set Device State USB Trace: Off Figure 4 16: Downstream Set Device State USB Trace: Off
  • According to the docs all of the devices ids should start with 0x00, 0x00, 0xFF, 0xFB. But here's my device ID: 7E ED 82 C6 8B DA 00 00. It doesn't have 0xFF, 0xFB.

All GIP devices MUST have a unique 64-bit Primary Device ID of which the four most significant bytes are 0x00, 0x00, 0xFF, 0xFB. The remaining bytes MUST be Random numbers, determined on bootup of the GIP device.

Here's the full code:

using System.ComponentModel; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; public class XboxGipController { // Constants private const uint GENERIC_READ = 0x80000000; private const uint GENERIC_WRITE = 0x40000000; private const uint FILE_SHARE_READ = 0x00000001; private const uint FILE_SHARE_WRITE = 0x00000002; private const uint OPEN_EXISTING = 3; private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080; // Define the specific IOCTL code private const uint GIP_ADD_REENUMERATE_CALLER_CONTEXT = 0x40001CD0; [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern SafeFileHandle CreateFileW( string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile ); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool DeviceIoControl( SafeFileHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped ); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool ReadFile( SafeFileHandle hFile, byte[] lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped ); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool WriteFile( SafeFileHandle hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, IntPtr lpOverlapped ); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr hObject); private static void ProcessGipMessage(byte[] data, int length) { Console.WriteLine($"Received {length} bytes:"); for (int i = 0; i < length; i++) { Console.Write($"{data[i]:X2} "); if ((i + 1) % 16 == 0) Console.WriteLine(); } Console.WriteLine(); } public static void ReenumerateGipControllers() { SafeFileHandle? hFile = null; try { // Open the GIP device interface hFile = CreateFileW( @"\\.\XboxGIP", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero ); if (hFile.IsInvalid) { throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); } // Send the re-enumeration command uint bytesReturned; bool success = DeviceIoControl( hFile, GIP_ADD_REENUMERATE_CALLER_CONTEXT, IntPtr.Zero, 0, IntPtr.Zero, 0, out bytesReturned, IntPtr.Zero ); if (!success) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); Console.WriteLine("GIP controller re-enumeration triggered successfully"); byte[] powerOffCommand = new byte[64]; // Xbox controller ID powerOffCommand[0] = 0x7E; powerOffCommand[1] = 0xED; powerOffCommand[2] = 0x82; powerOffCommand[3] = 0xC6; powerOffCommand[4] = 0x8B; powerOffCommand[5] = 0xDA; powerOffCommand[6] = 0x00; powerOffCommand[7] = 0x00; powerOffCommand[8] = 0x05; // Command ID powerOffCommand[9] = 0x20; // Flags powerOffCommand[10] = 0x01; // Sequence number powerOffCommand[11] = 0x01; // payload length powerOffCommand[12] = 0x04; // payload // Send the power off command // Comment the next 15 lines to skip writing and check how ReadFile works. // Otherwise the code will throw. uint bytesWritten; bool successWrite = WriteFile( hFile, powerOffCommand, (uint)powerOffCommand.Length, out bytesWritten, IntPtr.Zero ); if (!successWrite) { ProcessGipMessage(powerOffCommand, powerOffCommand.Length); throw new Win32Exception(Marshal.GetLastWin32Error()); } byte[] buffer = new byte[1000]; uint bytesRead; while (true) { bool successRead = ReadFile( hFile, // Handle to the GIP device buffer, // Buffer to receive data (uint)buffer.Length, // Buffer size out bytesRead, // Number of bytes actually read IntPtr.Zero ); if (!successRead) { int error = Marshal.GetLastWin32Error(); if (error == 259) break; throw new Win32Exception(error); } if (bytesRead > 0) { ProcessGipMessage(buffer, (int)bytesRead); } else { System.Threading.Thread.Sleep(10); } } } finally { hFile?.Close(); } } // Usage example public static void Main() { try { ReenumerateGipControllers(); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } } } 
2
  • Have you tried setting command[2] to something other than 0x00, eg 0x01 ? The docs say that field should be an incrementing id and that 0x00 is reserved. Commented Jul 15 at 5:29
  • I only tried setting it to 0x00 or commenting it out completely thinking maybe it will increment on it's own since 0x00 is reserved. The only theory I have right now is that I have to somehow id the device which I am writing to. Maybe take parts of the header in the hello message that contain device Id and somehow pass it with the payload so it will be clear where I am sending the data. I think I get device not connected because I don't send any ID data even though I am using a handle. Commented Jul 15 at 6:29

1 Answer 1

1

I was able to get in contact with the MS support for gipdocs and they said this:

The implementation of the protocol within Microsoft Windows sends these packets for various scenarios(such as device idle timeout etc) but we don’t have any public way for an application to send them. However, there is a private API no one is currently using that can do this :

IGameControllerProviderPrivate has a PowerOff method that would cause this packet to be sent to gip devices including the Xbox one devices, and also the series controller you are interested in. You may QueryInterface this from the public interface GipGameControllerProvider Class (Windows.Gaming.Input.Custom) - Windows apps | Microsoft Learn.

there are 4 different ways this task can be accomplished.

  1. Use the D4XDevice SDK, This requires Non Disclosure Agreement between Microsoft and your Corporation.

  2. Reverse engineer the ioctl (but we aware it may break in the future because its not a public API). You will be calling DeviceIOControl API with the IOCTL code that results in device power-off.

  3. Reverse engineer the COM PowerOff API (but we aware it may break in the future because its not a public API). Here you would be figuring out the COM Class GUID value.

  4. Uninstalling the Microsoft implementation of the protocol in the “xboxgip.sys” driver and replacing it with your own usb driver that implements the protocol in device manager.

Methods 2 and 3 are essentially unsupported by Microsoft. Hope this helps.

Which gives me hope that this is viable. But I feel like I am in over my head here.

New findings:
I made an unsuccesful attempt to write data to @"\\.\XboxGIP.
I've written this 0x7E 0xED 0x82 0xC6 0x8B 0xDA 0x00 0x00 0x05 0x20 0x01 0x01 0x04 where the first 8 bytes are my device's ID and the rest 5 are the power off packet padded with zeroes to have the length of 64 bytes.
Interesting part is that I get an error 87 Invalid parameter which to me means that I am writing to a device but the way the command is done is incorrect.

The proof of this is that if I send just the 5 byte command packet or change bytes in the ID i get the error 1167 Device Not Connected.

Also If I remove padding or send a message less that around 29 bytes I get 122 Wrong length.

I also tried writing commands with hidapitester. I succesfully opened the HID of the controller but when I try send commands to it it either not reacting or starts vibrating. I guess this is because I am supposed to send commands over gip while the device handle is
'\\?\HID#VID_045E&PID_0B12&IG_00#a&14e160f7&7&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}'

A colleague also sent me his wireshark capture where he's powering off his Xbox Controller. An interesting thing is that from D4XDevice software the command is being spammed, you can see it from the sequence number being incremented a great number of times. Example:

The first entry with power off command (line 0050, 05 20 00 01 04):

1131 2025-11-14 06:28:50,098304 host 1.50.4 USB 99 URB_BULK out 0000 1b 00 10 b0 fc 41 89 ab ff ff 00 00 00 00 09 00 0010 00 01 00 32 00 04 03 48 00 00 00 40 00 00 50 00 0020 00 00 00 00 00 00 00 a0 00 00 20 01 00 1f 00 00 0030 00 00 00 00 00 00 00 00 00 00 00 88 42 90 00 7e 0040 ed 8d 64 b0 ef 62 45 b5 04 6a e0 62 45 b5 04 6a 0050 e0 00 00 00 00 00 00 05 20 00 01 04 ab ff ff 00 0060 00 00 00 

The last entry with power off command (line 0050, 05 20 6b 01 04):

1395 2025-11-14 06:28:51,720058 host 1.50.4 USB 99 URB_BULK out 0000 1b 00 10 b0 fc 41 89 ab ff ff 00 00 00 00 09 00 0010 00 01 00 32 00 04 03 48 00 00 00 40 00 00 50 00 0020 00 00 00 00 00 00 00 a0 00 00 20 01 00 1f 00 00 0030 00 00 00 00 00 00 00 00 00 00 00 88 42 90 00 7e 0040 ed 8d 64 b0 ef 62 45 b5 04 6a e0 62 45 b5 04 6a 0050 e0 00 00 00 00 00 00 05 20 6b 01 04 ab ff ff 00 0060 00 00 00 
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.