1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "SDL_internal.h"
22
23#ifdef SDL_JOYSTICK_HIDAPI
24
25#include "../../SDL_hints_c.h"
26#include "../SDL_sysjoystick.h"
27#include "SDL_hidapijoystick_c.h"
28#include "SDL_hidapi_rumble.h"
29
30#ifdef SDL_JOYSTICK_HIDAPI_XBOX360
31
32// Define this if you want to log all packets from the controller
33// #define DEBUG_XBOX_PROTOCOL
34
35typedef struct
36{
37 SDL_HIDAPI_Device *device;
38 bool connected;
39 int player_index;
40 bool player_lights;
41 Uint8 last_state[USB_PACKET_LENGTH];
42} SDL_DriverXbox360W_Context;
43
44static void HIDAPI_DriverXbox360W_RegisterHints(SDL_HintCallback callback, void *userdata)
45{
46 SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX, callback, userdata);
47 SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360, callback, userdata);
48 SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_WIRELESS, callback, userdata);
49}
50
51static void HIDAPI_DriverXbox360W_UnregisterHints(SDL_HintCallback callback, void *userdata)
52{
53 SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX, callback, userdata);
54 SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360, callback, userdata);
55 SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_WIRELESS, callback, userdata);
56}
57
58static bool HIDAPI_DriverXbox360W_IsEnabled(void)
59{
60 return SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_WIRELESS,
61 SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360, SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_XBOX, SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI, SDL_HIDAPI_DEFAULT))));
62}
63
64static bool HIDAPI_DriverXbox360W_IsSupportedDevice(SDL_HIDAPI_Device *device, const char *name, SDL_GamepadType type, Uint16 vendor_id, Uint16 product_id, Uint16 version, int interface_number, int interface_class, int interface_subclass, int interface_protocol)
65{
66 const int XB360W_IFACE_PROTOCOL = 129; // Wireless
67
68 if ((vendor_id == USB_VENDOR_MICROSOFT && (product_id == USB_PRODUCT_XBOX360_WIRELESS_RECEIVER_THIRDPARTY2 || product_id == USB_PRODUCT_XBOX360_WIRELESS_RECEIVER_THIRDPARTY1 || product_id == USB_PRODUCT_XBOX360_WIRELESS_RECEIVER) && interface_protocol == 0) ||
69 (type == SDL_GAMEPAD_TYPE_XBOX360 && interface_protocol == XB360W_IFACE_PROTOCOL)) {
70 return true;
71 }
72 return false;
73}
74
75static bool SetSlotLED(SDL_hid_device *dev, Uint8 slot, bool on)
76{
77 const bool blink = false;
78 Uint8 mode = on ? ((blink ? 0x02 : 0x06) + slot) : 0;
79 Uint8 led_packet[] = { 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
80
81 led_packet[3] = 0x40 + (mode % 0x0e);
82 if (SDL_hid_write(dev, led_packet, sizeof(led_packet)) != sizeof(led_packet)) {
83 return false;
84 }
85 return true;
86}
87
88static void UpdateSlotLED(SDL_DriverXbox360W_Context *ctx)
89{
90 if (ctx->player_lights && ctx->player_index >= 0) {
91 SetSlotLED(ctx->device->dev, (ctx->player_index % 4), true);
92 } else {
93 SetSlotLED(ctx->device->dev, 0, false);
94 }
95}
96
97static void SDLCALL SDL_PlayerLEDHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
98{
99 SDL_DriverXbox360W_Context *ctx = (SDL_DriverXbox360W_Context *)userdata;
100 bool player_lights = SDL_GetStringBoolean(hint, true);
101
102 if (player_lights != ctx->player_lights) {
103 ctx->player_lights = player_lights;
104
105 UpdateSlotLED(ctx);
106 HIDAPI_UpdateDeviceProperties(ctx->device);
107 }
108}
109
110static void UpdatePowerLevel(SDL_Joystick *joystick, Uint8 level)
111{
112 int percent = (int)SDL_roundf((level / 255.0f) * 100.0f);
113 SDL_SendJoystickPowerInfo(joystick, SDL_POWERSTATE_ON_BATTERY, percent);
114}
115
116static bool HIDAPI_DriverXbox360W_InitDevice(SDL_HIDAPI_Device *device)
117{
118 SDL_DriverXbox360W_Context *ctx;
119
120 // Requests controller presence information from the wireless dongle
121 const Uint8 init_packet[] = { 0x08, 0x00, 0x0F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
122
123 HIDAPI_SetDeviceName(device, "Xbox 360 Wireless Controller");
124
125 ctx = (SDL_DriverXbox360W_Context *)SDL_calloc(1, sizeof(*ctx));
126 if (!ctx) {
127 return false;
128 }
129 ctx->device = device;
130
131 device->context = ctx;
132
133 if (SDL_hid_write(device->dev, init_packet, sizeof(init_packet)) != sizeof(init_packet)) {
134 SDL_SetError("Couldn't write init packet");
135 return false;
136 }
137
138 device->type = SDL_GAMEPAD_TYPE_XBOX360;
139
140 return true;
141}
142
143static int HIDAPI_DriverXbox360W_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id)
144{
145 return -1;
146}
147
148static void HIDAPI_DriverXbox360W_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index)
149{
150 SDL_DriverXbox360W_Context *ctx = (SDL_DriverXbox360W_Context *)device->context;
151
152 if (!ctx) {
153 return;
154 }
155
156 ctx->player_index = player_index;
157
158 UpdateSlotLED(ctx);
159}
160
161static bool HIDAPI_DriverXbox360W_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
162{
163 SDL_DriverXbox360W_Context *ctx = (SDL_DriverXbox360W_Context *)device->context;
164
165 SDL_AssertJoysticksLocked();
166
167 SDL_zeroa(ctx->last_state);
168
169 // Initialize player index (needed for setting LEDs)
170 ctx->player_index = SDL_GetJoystickPlayerIndex(joystick);
171 ctx->player_lights = SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_PLAYER_LED, true);
172 UpdateSlotLED(ctx);
173
174 SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_PLAYER_LED,
175 SDL_PlayerLEDHintChanged, ctx);
176
177 // Initialize the joystick capabilities
178 joystick->nbuttons = 11;
179 joystick->naxes = SDL_GAMEPAD_AXIS_COUNT;
180 joystick->nhats = 1;
181 joystick->connection_state = SDL_JOYSTICK_CONNECTION_WIRELESS;
182
183 return true;
184}
185
186static bool HIDAPI_DriverXbox360W_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
187{
188 Uint8 rumble_packet[] = { 0x00, 0x01, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
189
190 rumble_packet[5] = (low_frequency_rumble >> 8);
191 rumble_packet[6] = (high_frequency_rumble >> 8);
192
193 if (SDL_HIDAPI_SendRumble(device, rumble_packet, sizeof(rumble_packet)) != sizeof(rumble_packet)) {
194 return SDL_SetError("Couldn't send rumble packet");
195 }
196 return true;
197}
198
199static bool HIDAPI_DriverXbox360W_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
200{
201 return SDL_Unsupported();
202}
203
204static Uint32 HIDAPI_DriverXbox360W_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
205{
206 SDL_DriverXbox360W_Context *ctx = (SDL_DriverXbox360W_Context *)device->context;
207 Uint32 result = SDL_JOYSTICK_CAP_RUMBLE;
208
209 if (ctx->player_lights) {
210 result |= SDL_JOYSTICK_CAP_PLAYER_LED;
211 }
212 return result;
213}
214
215static bool HIDAPI_DriverXbox360W_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
216{
217 return SDL_Unsupported();
218}
219
220static bool HIDAPI_DriverXbox360W_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *data, int size)
221{
222 return SDL_Unsupported();
223}
224
225static bool HIDAPI_DriverXbox360W_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, bool enabled)
226{
227 return SDL_Unsupported();
228}
229
230static void HIDAPI_DriverXbox360W_HandleStatePacket(SDL_Joystick *joystick, SDL_hid_device *dev, SDL_DriverXbox360W_Context *ctx, Uint8 *data, int size)
231{
232 Sint16 axis;
233 const bool invert_y_axes = true;
234 Uint64 timestamp = SDL_GetTicksNS();
235
236 if (ctx->last_state[2] != data[2]) {
237 Uint8 hat = 0;
238
239 if (data[2] & 0x01) {
240 hat |= SDL_HAT_UP;
241 }
242 if (data[2] & 0x02) {
243 hat |= SDL_HAT_DOWN;
244 }
245 if (data[2] & 0x04) {
246 hat |= SDL_HAT_LEFT;
247 }
248 if (data[2] & 0x08) {
249 hat |= SDL_HAT_RIGHT;
250 }
251 SDL_SendJoystickHat(timestamp, joystick, 0, hat);
252
253 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[2] & 0x10) != 0));
254 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, ((data[2] & 0x20) != 0));
255 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[2] & 0x40) != 0));
256 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[2] & 0x80) != 0));
257 }
258
259 if (ctx->last_state[3] != data[3]) {
260 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, ((data[3] & 0x01) != 0));
261 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, ((data[3] & 0x02) != 0));
262 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[3] & 0x04) != 0));
263 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x10) != 0));
264 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x20) != 0));
265 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x40) != 0));
266 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x80) != 0));
267 }
268
269 axis = ((int)data[4] * 257) - 32768;
270 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis);
271 axis = ((int)data[5] * 257) - 32768;
272 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, axis);
273 axis = SDL_Swap16LE(*(Sint16 *)(&data[6]));
274 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, axis);
275 axis = SDL_Swap16LE(*(Sint16 *)(&data[8]));
276 if (invert_y_axes) {
277 axis = ~axis;
278 }
279 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, axis);
280 axis = SDL_Swap16LE(*(Sint16 *)(&data[10]));
281 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, axis);
282 axis = SDL_Swap16LE(*(Sint16 *)(&data[12]));
283 if (invert_y_axes) {
284 axis = ~axis;
285 }
286 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, axis);
287
288 SDL_memcpy(ctx->last_state, data, SDL_min(size, sizeof(ctx->last_state)));
289}
290
291static bool HIDAPI_DriverXbox360W_UpdateDevice(SDL_HIDAPI_Device *device)
292{
293 SDL_DriverXbox360W_Context *ctx = (SDL_DriverXbox360W_Context *)device->context;
294 SDL_Joystick *joystick = NULL;
295 Uint8 data[USB_PACKET_LENGTH];
296 int size;
297
298 if (device->num_joysticks > 0) {
299 joystick = SDL_GetJoystickFromID(device->joysticks[0]);
300 }
301
302 while ((size = SDL_hid_read_timeout(device->dev, data, sizeof(data), 0)) > 0) {
303#ifdef DEBUG_XBOX_PROTOCOL
304 HIDAPI_DumpPacket("Xbox 360 wireless packet: size = %d", data, size);
305#endif
306 if (size == 2 && data[0] == 0x08) {
307 bool connected = (data[1] & 0x80) ? true : false;
308#ifdef DEBUG_JOYSTICK
309 SDL_Log("Connected = %s", connected ? "TRUE" : "FALSE");
310#endif
311 if (connected != ctx->connected) {
312 ctx->connected = connected;
313
314 if (connected) {
315 SDL_JoystickID joystickID;
316
317 HIDAPI_JoystickConnected(device, &joystickID);
318
319 } else if (device->num_joysticks > 0) {
320 HIDAPI_JoystickDisconnected(device, device->joysticks[0]);
321 }
322 }
323 } else if (size == 29 && data[0] == 0x00 && data[1] == 0x0f && data[2] == 0x00 && data[3] == 0xf0) {
324 // Serial number is data[7-13]
325#ifdef DEBUG_JOYSTICK
326 SDL_Log("Battery status (initial): %d", data[17]);
327#endif
328 if (joystick) {
329 UpdatePowerLevel(joystick, data[17]);
330 }
331 } else if (size == 29 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x13) {
332#ifdef DEBUG_JOYSTICK
333 SDL_Log("Battery status: %d", data[4]);
334#endif
335 if (joystick) {
336 UpdatePowerLevel(joystick, data[4]);
337 }
338 } else if (size == 29 && data[0] == 0x00 && (data[1] & 0x01) == 0x01) {
339 if (joystick) {
340 HIDAPI_DriverXbox360W_HandleStatePacket(joystick, device->dev, ctx, data + 4, size - 4);
341 }
342 }
343 }
344
345 if (size < 0 && device->num_joysticks > 0) {
346 // Read error, device is disconnected
347 HIDAPI_JoystickDisconnected(device, device->joysticks[0]);
348 }
349 return (size >= 0);
350}
351
352static void HIDAPI_DriverXbox360W_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
353{
354 SDL_DriverXbox360W_Context *ctx = (SDL_DriverXbox360W_Context *)device->context;
355
356 SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_PLAYER_LED,
357 SDL_PlayerLEDHintChanged, ctx);
358}
359
360static void HIDAPI_DriverXbox360W_FreeDevice(SDL_HIDAPI_Device *device)
361{
362}
363
364SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverXbox360W = {
365 SDL_HINT_JOYSTICK_HIDAPI_XBOX_360_WIRELESS,
366 true,
367 HIDAPI_DriverXbox360W_RegisterHints,
368 HIDAPI_DriverXbox360W_UnregisterHints,
369 HIDAPI_DriverXbox360W_IsEnabled,
370 HIDAPI_DriverXbox360W_IsSupportedDevice,
371 HIDAPI_DriverXbox360W_InitDevice,
372 HIDAPI_DriverXbox360W_GetDevicePlayerIndex,
373 HIDAPI_DriverXbox360W_SetDevicePlayerIndex,
374 HIDAPI_DriverXbox360W_UpdateDevice,
375 HIDAPI_DriverXbox360W_OpenJoystick,
376 HIDAPI_DriverXbox360W_RumbleJoystick,
377 HIDAPI_DriverXbox360W_RumbleJoystickTriggers,
378 HIDAPI_DriverXbox360W_GetJoystickCapabilities,
379 HIDAPI_DriverXbox360W_SetJoystickLED,
380 HIDAPI_DriverXbox360W_SendJoystickEffect,
381 HIDAPI_DriverXbox360W_SetJoystickSensorsEnabled,
382 HIDAPI_DriverXbox360W_CloseJoystick,
383 HIDAPI_DriverXbox360W_FreeDevice,
384};
385
386#endif // SDL_JOYSTICK_HIDAPI_XBOX360
387
388#endif // SDL_JOYSTICK_HIDAPI
389