Всем привет! Я — новичок на Хабре, потому, набравшись смелости, хотел бы поделиться небольшим домашним проектом, над которым работаю последний год в свободное время, а затем интересным случаем взаимодействия с контроллером Xbox Series.
Введение
Я много пользуюсь Steam на Windows 10\11, но лишь последние пару лет я предпочитаю играть не за компьютером, потому стал часто пользоваться режимом Big Picture - это режим для игры на диване: целый отдельный UI с более консольным ощущением. Steam делает ваш телевизор основным монитором и запускает на нем игру.

Но у этого режима есть несколько проблем, которые меня никак не устраивали и которые я принялся решать.
Big Picture не меняет устройства аудиовывода. Да, картинка улетает на телевизор на запуске, но если у вас настольные колонки у монитора, звук останется в них.
Если включен Ночной Свет по расписанию, то во время игры он может активироваться и экран "потеплеет".
В Big Picture есть явная настройка "Turn off controllers when exiting Big Picture Mode" (Отключить джойстики при выходе из Big Picture), а так же есть горячие клавиши для Xbox (Chords) Guide + Y. Но ни на выходе из режима, ни по горячим клавишам джойстик не отключается. Как ни странно, но в интернете мало информации о том, почему джойстик не отключается, даже особо не нашлось каких-то постов-жалоб пользователей. Я смог лишь найти пост от 2018 года, где представитель Valve говорит о том, что действительно, джойстик Xbox через Steam отключить нельзя, но при этом до сих указывают горячие клавиши для этого.
Big Picture Manager
Почти всё это я решил в самописном C# приложении Big Picture Manager. Суть проста: приложение слушает запуск окна Steam с названием Steam Big Picture.
В приложении можно выбрать на какое аудиоустройство переключиться при включении Big Picture, а после выхода вернуться на исходное.
Благодаря ИИ-агентам, вроде Cursor, мне удалось разобраться как записываются байты в регистре Windows, отвечающие за работу Ночного Света. На запуске приложение проверяет каким образом запущен ночной свет, отключает его, а после выхода восстанавливает.
Пункт три становится тем местом, откуда начнется основное повествование статьи.

Я пользуюсь джойстиком Xbox Series в цвете Electric Volt. Пока не вышел Steam Controller 2, я считаю, что это лучший контроллер, доступный сегодня на рынке (вопрос, конечно, субъективный). Его можно подключить к ПК тремя способами: по Bluetooth, с помощью проприетарного радио-свистка от Microsoft, который работает по собственной закрытой технологии 2.4GHz и просто по USB кабелю.

В случае с Big Picture Manager решить отключение джойстика я смог только одним простым образом: на выходе из режима целиком отключается Bluetooth в системе. В таком сценарии джойстик сразу же уходит в отключение. Это то поведение, которое я пытаюсь добиться, но не путем полного отключения Bluetooth в системе. Идеально было бы отключать только джойстик при его работе от беспроводного адаптера.
Джойстик Xbox Series и GIPUSB
Я вообще не был знаком с тем, как игровые устройства взаимодействуют с компьютером, потому не было понятно с чего начать. На YouTube мне попалось видео с Дмитрием Рязанцовым, который раскрывает немного суть работы устройств ввода с играми, потому недолго думая я отыскал его на LinkedIn и обратился за советом. Можно сказать, что он дал главный толчок всему этому расследованию.
В 2024 году Microsoft выкатили документацию для разработчиков, создающих свои контроллеры, для того, чтобы они могли единообразно работать через GameInput. Кратко, GameInput - единый интерфейс для работы с игровыми аксессуарами, который под собой объединяет XInput, DirectInput, Raw Input, Human Interface Device (HID) и WinRT API. Сам же транспортный протокол для передачи данных называется GIP (Game Input Protocol). В него упирается всё.
Совет Дмитрия заключался в следующем:
Так что в теории можно получить хендл и попробовать отправить команду отключения контроллера.
В протоколе есть:
3.1.5.5.5 Set Device State Command
0x04 Off GIP device SHOULD transition to the GIP Off State. See State Machine for details on device behavior.
Как открыть хендл и отправить пакет на девайс описано тут: https://gist.github.com/TheNathannator/bcebc77e653f71e77634144940871596 (как я понял можно отправить пакет через WriteFile)
Суть совета в том, что есть определенный байт, который, при отправке на устройство, вызывает отключение. Его нужно правильно сформировать и передать по GIP на контроллер.
Ссылка, которую дал Дмитрий ведет на небольшую статью от TheNathannator, который исследовал работу с GIP до того, как вышла документация. Несмотря на то, что она является устаревшей, я нашел точку входа для работы с протоколом:
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);
У меня есть небольшой опыт работы с C#, потому я начал делать проект на этом языке. Прошу держать в уме, что все эти манипуляции я проводил при джойстике, подключенном именно по проприетарному беспроводу. Вот что мне удалось сделать:
using System.ComponentModel; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; public class XboxGipController { 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; 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(); } } public static void Main() { try { ReenumerateGipControllers(); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } } }
Что делает код:
Открывает устройство
\\.\XboxGIPдля чтения и записи.Отправляет управляющий код
0x40001CD0(IOCTL), который инициирует реенумерацию подключённых контроллеров.Формирует и отправляет команду выключения контроллера (power‑off) – фиксированная последовательность байт, включающая идентификатор, код команды и полезную нагрузку.
Запускает бесконечный цикл чтения данных из устройства. Полученные пакеты выводятся в консоль в шестнадцатеричном виде.
При ошибке чтения с кодом
259(ERROR_NO_MORE_ITEMS) цикл завершается; любые другие ошибки приводят к исключению.Закрывает дескриптор устройства.
Исполнение этого кода привело к получению нескольких сообщений:
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 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 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
Документация раскрывает, что при перечислении устройств, джойстик, работающий по GIP, выдает несколько сообщений.
Сообщение метаданных (Metadata Response, 315 байт). Это ответ с метаданными геймпада (идентификация, возможности, строка класса Gamepad, плюс бинарные блоки по GIP).
Приветственное сообщение (Hello message, 53 байта). Устройство шлёт Hello при установлении связи. Стандартное приветствие геймпада с VID/PID, прошивкой и версиями протоколов.
Состояние устройства (Status Device, 24 байта). Состояние устройства (в т.ч. батарея, тип питания и т.д.).
Здесь любопытно то, что все сообщения начинаются с 7E ED 82 C6 8B DA 00 00, что мне кажется, является ID контроллера.
Хорошо, с чтением данных разобрались, в этот момент вся затея показалась мне необычайно радужной, но при попытке записи данных в устройство я столкнулся с проблемами. Я начал с того, что составил пакет, описывающий команду выключения устройства: 0x05 0x20 0x01 0x01 0x04.
0x05 — Set Device State (Command Data Class, command 5)
0x20 — Flags (флаги). Одиночный пакет с системным сообщением к главному устройству, без ack.
0x01 — Инкрементирующийся,
0x00зарезервирован, я пробовал разные значения0x01 — Payload Length (Длина отпраляемой команды)
0x04 — State "Off" (Состояние Выкл.)
Я пытался отправлять эту команду несколькими способами:
Отправлял только эти 5 байт. В ответ получал
1167 Device Not Connected;Затем я решил добавить в начало ID моего джойстика.
7E ED 82 C6 8B DA 00 00 05 20 01 01 04. В результате я получил122 Wrong length;Почитав доки, я понял, что пакет должен быть 64 байта, потому я добавил нули в конце и на это получил
87 Invalid parameter;
О каком параметре идет речь - одному богу известно. В документации ничего не говорится о параметрах, потому на этом месте идеи стали заканчиваться. На Stackoverflow я наткнулся на пост пользователя Victor, который занимается разработкой собственного контроллера. После короткой имейл-переписки, он мне подал идею отправить пакет выключения при подключении по USB-проводу. Я так и поступил, воспользовавшись Bus Hound.

И вуаля, джойстик выключился. Успех был одновременно так близко, и так далеко. Это событие позволило сделать вывод, что сам контроллер способен принимать команды, и дело в самом протоколе GIP. Случайная идея заключалась в том, что данные каким-то образом шифруются при передаче и потому, когда я пытался отправлять команду, она отвергалась за неимением каких-то дополнительных или шифрованных данных.
Microsoft learn и CRC16
Я сделал пост на learn.microsoft с описанием того, что пытаюсь достичь. Честно сказать, первое впечатление от форума какое-то мусорное. Мне начали лететь ответы от ИИ, которые тут же удалялись модераторами, но затем мне ответил некий Varsha Dundigalla. Я до сих пор думаю, что он тоже какая-то версия ИИ (да и вообще интернет давно мёртв), потому что его ответы были иногда противоречивыми тому, что я уже установил, а его источники часто не содержали информации, на которую он ссылался.
Он дал следующую информацию:
The problem is that the Xbox controller expects a full GIP message, not just the raw payload. Your packet (
05 20 01 04) is correct for the USB layer but incomplete for GIP. The protocol requires the device ID, command ID, flags, sequence number, payload length, payload, and a CRC16 checksum. Without these, the driver rejects the command withERROR_INVALID_PARAMETER.
The GIP protocol uses CRC-16-CCITT (polynomial0x1021, initial value0xFFFF). You calculate it over the entire message (device ID, command ID, flags, sequence number, payload length, payload) and append the two-byte checksum at the end. Without this, the controller will reject the packet.
What it should look like conceptually: a downstream transport/report prefix (commonly0x02for GIP/USB), then the Set Device State header0x05 0x20 <seq> 0x01, then payload0x04(Off), then two CRC bytes right after that payload. Your analyzer screenshot matches this pattern (02 05 20 01 04 + CRC) and the device ACKs it—so formatting, not permissions, is the blocker.
Кратко говоря, структура должна выглядеть так: префикс + команда выключения (хедер + пэйлод) + CRC (на основе префикса и команды) + нули до 64 байт. Само собой, я пошел тестить, отправил 0x02 0x05 0x20 0x01 0x01 0x04 0x9D 0x3B, но, к сожалению, безуспешно. Получил ту же ошибку про неверный параметр. На этом момент я оставил попытки пытаться что-то записать в контроллер через WriteFile.
Hidapistester
Да, я перестал пытаться записывать что-то через SafeFileHandle, но это не значит, что я перестал пытаться вообще. Исследуя другие способы отправки команд, я открыл для себя hidapitester. Простая утилита для работы с HIDAPI. Используя аргументы, я попытался так же отправлять различные вариации команды отключения в свой джойстик, подключая его как по GIP, так и по Bluetooth:
.\hidapitester.exe --open-path "\\?\HID#VID_045E&PID_0B12&IG_00#a&14e160f7&d&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}" --send-output 0x02,0x05,0x20,0x01,0x01,0x04,0x9D,0x3B --length 64
Ответ:
Writing output report of 64-bytes...wrote 9 bytes: 02 05 20 01 01 04 9D 3B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Closing device
Именно в этом варианте команды джойстик начал неожиданно вибрировать. Интересно, что записалось лишь 9 байт из всей команды. Этот момент обозначил новую веху - я могу что-то записать в контроллер. Сейчас я думаю, что я просто каким-то образом попал в команду вибрации, передаваемую через GameInput. Но все дальнейшие эксперименты не были результативны.
Windows Management Instrumentation
Теряя надежду, я начал думать шире. Задавшись вопросом отключения джойстика средствами системы, не пытаясь напрямую отдавать команды, я решил погуглить чем бы еще можно было бы отключить его по аналогии с тем, как он отключается, когда в системе выключается Bluetooth. Здесь так же не обошлось без подсказок. На тот момент я уже был так одержим желанием реализовать задуманное, что просто начал писать всем причастным к C# на LinkedIn.
Юрий Худошин помог мне завайбкодить программу на С#, которая отключала джойстик и беспроводной адаптер на уровне Диспетчера устройств в системе (devmgmt.msc). Год назад итог был такой, что джойстик действительно отключался! Я не мог поверить глазам! У него гасла Guide кнопка и джойстик отключался! Неужели это был тот самый момент триумфа? Но скептики меня поймут, когда я скажу, что отказывался верить, пока не увижу весь цикл включения и выключения контроллера.
И действительно, при попытке включить джойстик он буквально на долю секунды подключался к компьютеру и тут же выключался обратно. Помогала лишь переустановка драйверов путем удаления устройства из Диспетчера и повторным подключением устройства к компьютеру. Как я понял, отключив адаптер через Диспетчер, он терял связь с контроллером, а тот в свою очередь отключался, но нюанс в том, что, по всей видимости, то, что отключал код, не включалось обратно, так как терялся линк с джойстиком, который было невозможно восстановить. Я пробовал этот путь пару недель назад снова, сейчас джойстик даже не выключается. Система ждет перезагрузки, чтобы отключить устройство. Вероятно, было какое-то обновление.
Вот код отключения и включения джойстика из репо, который я использовал:
using System.Management; class GamepadDisabler { static void Main() { Console.WriteLine("=== Список HID устройств ===\n"); ListDevices(); Console.WriteLine("\n1 - Отключить геймпад"); Console.WriteLine("2 - Включить геймпад"); Console.Write("\nВыбери: "); string choice = Console.ReadLine(); if (choice == "1") { DisableDevice(); } else if (choice == "2") { EnableDevice(); } Console.WriteLine("\nГотово! Нажми Enter..."); Console.ReadLine(); } static void ListDevices() { try { ManagementObjectSearcher searcher = new ManagementObjectSearcher( "SELECT * FROM Win32_PnPEntity WHERE PNPClass='HIDClass'" ); foreach (ManagementObject device in searcher.Get()) { string name = device["Name"]?.ToString() ?? "Unknown"; string deviceId = device["DeviceID"]?.ToString() ?? ""; string status = device["Status"]?.ToString() ?? ""; Console.WriteLine($"Имя: {name}"); Console.WriteLine($"ID: {deviceId}"); Console.WriteLine($"Статус: {status}"); Console.WriteLine("---"); } } catch (Exception ex) { Console.WriteLine($"Ошибка: {ex.Message}"); } } static void DisableDevice() { try { ManagementObjectSearcher searcher = new ManagementObjectSearcher( "SELECT * FROM Win32_PnPEntity WHERE PNPClass='HIDClass' OR DeviceID LIKE '%VID_045E%'" ); foreach (ManagementObject device in searcher.Get()) { string name = device["Name"]?.ToString() ?? ""; string deviceId = device["DeviceID"]?.ToString() ?? ""; string configManagerErrorCode = device["ConfigManagerErrorCode"]?.ToString() ?? ""; // Пропускаем виртуальные устройства AMD и уже отключённые if (name.Contains("AMD") || name.Contains("Emulation")) { continue; } // Ищем Xbox устройства по VID (045E = Microsoft) if (deviceId.Contains("VID_045E") || name.ToLower().Contains("xbox") || name.ToLower().Contains("wireless")) { Console.WriteLine($"\nНайдено: {name}"); Console.WriteLine($"DeviceID: {deviceId}"); // Вызываем метод Disable ManagementBaseObject outParams = device.InvokeMethod("Disable", null, null); if (outParams != null) { uint returnValue = (uint)outParams["ReturnValue"]; if (returnValue == 0) { Console.WriteLine("✓ Успешно отключено!"); } else { Console.WriteLine($"✗ Ошибка: код {returnValue}"); if (returnValue == 5) Console.WriteLine(" (Нет прав доступа - запусти от администратора)"); } } } } } catch (Exception ex) { Console.WriteLine($"Ошибка: {ex.Message}"); Console.WriteLine("\nЗапусти программу от имени администратора!"); } } static void EnableDevice() { try { ManagementObjectSearcher searcher = new ManagementObjectSearcher( "SELECT * FROM Win32_PnPEntity WHERE PNPClass='HIDClass' OR DeviceID LIKE '%VID_045E%'" ); foreach (ManagementObject device in searcher.Get()) { string name = device["Name"]?.ToString() ?? ""; string deviceId = device["DeviceID"]?.ToString() ?? ""; // Пропускаем виртуальные устройства AMD if (name.Contains("AMD") || name.Contains("Emulation")) { continue; } // Ищем Xbox устройства if (deviceId.Contains("VID_045E") || name.ToLower().Contains("xbox") || name.ToLower().Contains("wireless")) { Console.WriteLine($"\nНайдено: {name}"); // Вызываем метод Enable ManagementBaseObject outParams = device.InvokeMethod("Enable", null, null); if (outParams != null) { uint returnValue = (uint)outParams["ReturnValue"]; if (returnValue == 0) { Console.WriteLine("✓ Успешно включено!"); } else { Console.WriteLine($"✗ Ошибка: код {returnValue}"); } } } } } catch (Exception ex) { Console.WriteLine($"Ошибка: {ex.Message}"); } } }
Общение с Microsoft
На странице документации GIP есть имейл техподдержки по протоколу. Я не постеснялся и напрямую им задал свой вопрос. Они мне отвечали раз в неделю, но вот что получилось узнать:
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.
В общем, есть некий приватный метод, который можно было бы использовать, но он может поменяться в будущем, потому стабильности нет, а еще нужно каким-то образом получить этот метод через QueryInterface. К сожалению, для меня это оказалось сложным даже с Cursor и платной подпиской. Как бы я не гонял в шею несчастный чат, ничего толкового у меня здесь не вышло.
Затем мне написали это:
there are 4 different ways this task can be accomplished.
Use the D4XDevice SDK, This requires Non Disclosure Agreement between Microsoft and your Corporation.
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.
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.
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.
Здесь я начал ловить ощущение, что данный квест мне становится не по зубам. К тому же, у меня не было уверенности, что предложенные варианты действительно являются рабочими. В предоставлении SDK мне отказали, так как я не являюсь компанией-разработчиком. Варианты 2 и 3 я гонял вместе с чатом, но опять же IOCTL код надо каким-то чудом знать или откуда-то вычислить, чтобы понимать что отправлять, так же как и COM Class GUID. Вариант 4 тоже показался излишним. Ради моей задачи писать целый драйвер невероятно избыточно.
Еще мне показался интересным метод SendRawDeviceOutput из GameInput третьей версии. Судя по описанию, он делает ровно то, что мне нужно - отправляет сырые GIP команды джойстику. Мне удалось даже собрать небольшой пример кода с помощью чатаГПТ, но я исправлял такое количество ошибок, совместимости библиотек и сборок проекта, что неудивительно, что оно не взлетело.
Wireshark
Еще мне показалось хорошей идеей попробовать проанализировать пакеты данных и посмотреть, что вообще отправляет джойстик. При его подключении к адаптеру данные летят нонстопом. Разобраться что там к чему невероятно трудно, учитывая, что протокол поддерживает дробление пакетов. То есть, одна команда может быть разбита на несколько пакетов, а так же несколько маленьких команд могут быть в одном пакете. Однако, Victor, которого я нашел на стеке, оказался довольно добр ко мне и сделал Wireshark дамп со своего джойстика, подключенного к адаптеру. Мне удалось найти там пакеты выключения, но выглядят они очень странно. Команда выключения шлётся большое количество раз, постоянно инкрементируя sequence байт. Когда я пытался так же пронюхать свой джойстик, я такого не замечал. К тому же, ID устройства выглядит чуть по другому и данных больше. Например, вот ID моего контроллера при анализе в Wireshark: 7e ed 82 c6 8b da 62 45.

Итоги
На сегодня я исчерпал все возможности, которые у меня были, для реализации этой задачи. То, что я рассчитывал будет вечерним развлечением и веселой задачкой, превратилось в годовую одержимость и неугомонность, которая заставляла меня писать огромному количеству специалистов и спрашивать их мнения. Все воспринимали меня по-разному, но я благодарен, что находились те, у кого появлялся искренний энтузиазм относительно моей задачи и они давали ценные подсказки.
Задача оставила меня со списком неотвеченных вопросов:
В документации GIP говорится, что все устройства должны иметь ID, у которого обязательно должны быть байты
0x00,0x00,0xFF,0xFB. Однако, у моего джойстика их нет.Что вообще такое
@“\.\XboxGIP”? Похоже на какой-то путь, где происходит вообще весь транспорт данных, связанный с Xbox, между устройствами и хостом. C помощью hidapitester я писал команды напрямую в джойстик по его vidpid.Неизвестно, нужен ли в итоге CRC. Документация его не упоминает, говорится лишь, что GIP-устройство должно осуществлять шифрование, но не уточняет как.
Что за ошибка
87 ERROR_INVALID_PARAMETER? О каком параметре идет речь?Способен ли джойстик вообще принимать команды вне GameInput?
Почему команда через hidapitester вызвала вибрацию? Как она преобразовалась?
Где взять IOCTL или COM Class GUID?
Как получить доступ к IGameControllerProviderPrivate::PowerOff?
Должен ли я в итоге писать команду в контроллер или всё же через адаптер?
Почему при чтении данных из
@“\.\XboxGIP”ID контроллера кончается на00 00, но в Wireshark у него этих байт нет?

Все эти вопросы пока остаются без ответа. Пускай и не получилось добиться желаемого, но это было довольно увлекательно. Я буду рад любому продуктивному взаимодействию, направленному на раскрытие этих вопросов. Со мной можно связаться на Хабре, либо по контактам в профиле. Надеюсь, что вам тоже было любопытно ознакомиться с моим опытом. Спасибо за внимание!
Обновления (от 09.04.2026)
На сегодняшний день есть существенный прогресс.
@sinuke привнёс второе дыхание этой затее, найдя с помощью Claude Code точный байтовый пакет, который при отправке на “\.\XboxGIP” меняет яркость подсветки:7E-ED-82-C6-8B-DA-00-00-0A-20-00-00-03-00-00-00-00-00-00-00-00-01-05
Что здесь важно:
7E-ED-82-C6-8B-DA- ID джойстика00-00- не знаю зачем эти байты, но в пакетах Wireshark эти нули заменяются на62 450A-20-00-00-03-00-00-00-00-00-00-00-00-01-05- пакет самой команды (делает яркость LED-подсветки кнопки гайд в 10%). Что интересно, он отличается от сырой команды по документации.
По документации сырая команда выглядит так: 0A-20-00-03-00-01-05
0A- Класс команды. Команда 10.20- GIP Flag. Single packet system message from primary device. No acknowledgement required. (Отмечает, что это пакет с одной командной, не требует подтверждения получения)00- Sequence. Инкрементирующийся счетчик команд. По документации этот счетчик увеличивается по мере того, как посылаются команды одного пула. В моем понимании, это должно помогать отсеивать команды "вне потока". Если инкремент не совпадает, то команда отвергается. Однако при отправке по USB-кабелю и при отправке по беспроводу он как будто бы не учитывается. Команда измненеия подсветки кнопки выше отправляется с 00, что вообще зарезервировано и не должно использоваться, однако, команда проходит.03- длина полезной нагрузки 3 байта (payload).00- Команда кнопки LED01- Паттерн подсветки (есть разные: мигающий быстро или медленно, подъезжающий к определенной яркости и т.п.)05- Яркость подсветки. Примерно 10%, точно не вычислял, но гаснет так, чтобы не резало глаза.
При сравнении сырой команды и того, что мы отправляем на “\.\XboxGIP”, видно, что они отличаются.
|
|

Если отправить на “\.\XboxGIP” эту команду в виде (здесь всего 15 байт, потому добавляем 00 столько раз, чтобы стал как минимум 21 байт, иначе ошибка малого буфера): 7E-ED-82-C6-8B-DA-00-00-0A-20-00-03-00-01-05
То мы получаем ошибку Неверного параметра. То есть, вывод такой, что саму команду нужно еще как-то обернуть, чтобы она прошла валидацию и дошла до джойстика.
Начал экспериментировать с командой выключения. Напомню как она выглядит: 05 20 00 01 04
Затем попробовал ее отправить таким же образом, как команду яркости кнопки:7E-ED-82-C6-8B-DA-00-00-05-20-00-00-01-00-00-00-00-00-00-00-00-00-04
Так же пробовал двигать 04 по всей команде после 01, но все равно натыкался на ошибку Неверного параметра. Стала закрадываться мысль, что именно выключение джойстика было заблокировано на уровне драйвера. Я потратил два дня, безуспешно пытаясь подобрать пакет для “\.\XboxGIP”.
В итоге решил сделать шаг назад. Полез опять в Bus hound. Мне показалось важным попробовать отправить какую-то команду именно через Bus Commander напрямую в беспроводной адаптер. Я не придумал ничего лучше, кроме как тупо скопировать ее из WireShark (кроме байтов самого сниффера) и отправить на Bulk Out Endpoint 4 адаптера. Джойстик выключился.

Даже после ребута компьютера, байты остаются те же, я просто беру готовый .bin файл, шлю его на джойстик и он выключается. Фантастика. Однако, это не финал, так как еще есть ряд трудностей.
Bus Hound, насколько я понял, работает ниже уровня драйвера. Пока еще не удалось постичь глубоких тонкостей взаимоотношений драйверов с системой, но получается так, что отправить команду напрямую в адаптер не равно отправить ее через “\.\XboxGIP”. Действительно, может быть такое, что программно управлять питанием нельзя, но отправляя сырой пакет напрямую в адаптер мы минуем это ограничение.
Ответим на несколько ранее неотвеченных вопросов:
Что вообще такое
@“\.\XboxGIP”?
Это интерфейс взаимодействия с драйвером xboxgip.sys, который направляет данные через адаптер на джойстики при условии правильного оформления пакетов.Нужен ли в итоге CRC? На данный момент, кажется, что нет. Вероятно, шифрование происходит после отправки уже самим драйвером. Сами команды GIP в Wireshark не зашифрованы.
Способен ли джойстик вообще принимать команды вне GameInput? Да. Можно отправлять на
“\.\XboxGIP”, а так же через Bus Commander.Должен ли я в итоге писать команду в контроллер или всё же через адаптер? hidapitester позволяет писать команды напрямую в джойстик. Удалось вызвать вибрацию, но этот маршрут мало изучен и не кажется тем, который нужен для решения проблемы. Более верным подходом кажется отправка данных через адаптер. См. предыдущий вопрос.
Рассматриваю такой дальнейший путь: из C#-кода отправить весь 72 байтовый пакет из WireShark на тот же endpoint адаптера, но проблема в том, что я не могу найти подходящий инструмент. Либы, вроде libusbdotnet и похожие, требуют, чтобы USB-устройства работали из под WinUSB, но, как я смог понять, адаптер Xbox работает по-другому. Получить доступ к нему я не смог ни одним из используемых инструментов: LibUsbDotNet и Windows.Devices.Usb. Получаю 0 найденных по VID\PID устройств. Буду пробовать другие инструменты.
Предполагаю два исхода:
Я найду инструмент для записи байтов на адаптер из C#-кода.
Буду продолжать пытаться подобрать нужный формат команды отключения для отправки на
“\.\XboxGIP”- это, пожалуй, был бы лучший и чистый вариант.
Момент успеха
И так, этот день настал. Теперь известно как управлять питанием контроллера.
Вместе с @sinuke мы нашли репо на гите, где Leclowndu93150 таким же образом, как и мы, управляет подсветкой LED-кнопки. Я открыл issue в его репо, где описал свою цель отключить джойстик, и если вдруг у него есть какие-то находки, то я был бы рад с ним повзаимодействовать. Ответ не заставил себя долго ждать и результатом взаимодействия стал репозиторий готового функционала отключения джойстика. Что произошло:
Leclowndu93150 зареверсил весь драйвер xboxgip.sys с помощью Claude Code и IDA Pro 9.2 с MCP на файл драйвера. Первые анализы показали, что действительно, на “\.\XboxGIP” явным образом блокируются все команды, кроме команды подсветки.
if ((clientFlags & 0x20) && (commandId | (clientFlags << 8)) & 0x20FF != 0x200A) return STATUS_INVALID_PARAMETER;
Говоря именно про выключение, весь драйвер написан так, что выключение джойстика регулируется исключительно событиями в самой ОС. Это подтверждается словами Клинта Вуна из Discord-группы Microsoft Game Dev, с которым мне удалось поболтать в рамках Office Hours, проходящим в группе по пятницам. Он описал такое поведение контроллера, как безопасное для юзера, так как никто бы не хотел, чтобы вдруг его джойстик начал выключаться при атаке на компьютер. Утверждение неоднозначное, но по крайней мере мне официально подтвердили, что пользовательских способов управлять выключением джойстика нет и не предполагается, потому реверс был единственным вариантом добиться желаемого.

Дальнейшее расследование файла xboxgip.sys привело к тому, что оказывается, есть еще один интерфейс взаимодействия с GIP: “\.\XboxGIP_Admin”. И для работы с ним есть как раз тот самый незадокументированный системный вызов, про который мне говорили в поддержке Microsoft IOCTL 0x40001C4C с подкомандой 0x02, отвечающей за выключение контроллера. Важно указать, что команды должны идти с правами NT AUTHORITY\SYSTEM, иначе будет отказ.
Таким образом, логика работы получилась такая:
Открываем
“\.\XboxGIP”с IOCTL перенумерации устройств, получаем ID джойстика.Открываем
“\.\XboxGIP_Admin”от лица SYSTEM (я временно создаю службу, которую удаляю после работы кода).Шлём
DeviceIoControl(handle, 0x40001C4C, input, 9, NULL, 0, ...), где input это{deviceId (8 bytes), 0x02 (1 byte)}Вуаля! Джойстик отключается.
Я уже добавил эту фичу в свое приложение. Тестил полвечера и не мог перестать наслаждаться тем, как отключается джойстик при выходе из Big Picture.
Давайте попробуем ответить на оставшиеся вопросы:
В документации GIP говорится, что все устройства должны иметь ID, у которого обязательно должны быть байты
0x00,0x00,0xFF,0xFB.
Эта информация так и не была раскрыта, так как является не релевантной к решаемой задаче.Что за ошибка
87 ERROR_INVALID_PARAMETER? О каком параметре идет речь?
Оказалось, что это захардкоженый ответ на любой параметр, кроме0x0A, отвечающий за поведение LED.Почему команда через hidapitester вызвала вибрацию? Как она преобразовалась?
Так и не удалось выяснить, но это может быть хорошим отправным пунктом для поиска новых видов взаимодействия с контроллером. Копать нужно в сторону HID. Интересно, что контроллер работает по GIP, но принимает HID-комманды.Где взять IOCTL или COM Class GUID?
COM Class GUID найти не удалось, а вот IOCTL нашелся при реверсе xboxgip.sysКак получить доступ к IGameControllerProviderPrivate::PowerOff?
Любые попытки найти этот метод не увенчались успехом. Существование этого метода остается под знаком вопроса.Почему при чтении данных из
@“\.\XboxGIP”ID контроллера кончается на00 00, но в Wireshark у него этих байт нет?
Вероятнее всего пакет байтов дополняется различными данными транспорта при пересылке с хоста на устройство. Мы видели выше как меняется пакет уменьшения яркости LED. Пакет по документации и то, что приходит на джойстик - довольно разный набор данных.
Сейчас есть несколько путей, чтобы отключать джойстик, подключенный по GIP:
С помощью вышеописанной мной комбинации интерфейсов
“\.\XboxGIP”и“\.\XboxGIP_Admin”.Низкоуровневая отправка пакетов с помощью фильтр-драйверов, как у Bus Hound или UsbDk.
Програмнное отключение самих pnp-устройств в ОС (адаптер + джойстик), но мне метод показался нестабильным, будто на разных системах работает по-разному.
Однако нужно понимать, что подходы недокументированные и могут сломаться с очередным обновлением системы, но это уже не важно. Cаму идею мы задумали и реализовали, а это самое главное. Иногда интернет - это невероятно крутое место.

"Ведь одно приключение заканчивается, чтобы началось другое."
