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_sysjoystick.h"
26#include "SDL_hidapijoystick_c.h"
27#include "SDL_hidapi_rumble.h"
28#include "../SDL_joystick_c.h"
29
30#ifdef SDL_JOYSTICK_HIDAPI_STEAM_HORI
31
32/* Define this if you want to log all packets from the controller */
33/*#define DEBUG_HORI_PROTOCOL*/
34
35#define LOAD16(A, B) (Sint16)((Uint16)(A) | (((Uint16)(B)) << 8))
36
37enum
38{
39 SDL_GAMEPAD_BUTTON_HORI_QAM = 11,
40 SDL_GAMEPAD_BUTTON_HORI_FR,
41 SDL_GAMEPAD_BUTTON_HORI_FL,
42 SDL_GAMEPAD_BUTTON_HORI_M1,
43 SDL_GAMEPAD_BUTTON_HORI_M2,
44 SDL_GAMEPAD_BUTTON_HORI_JOYSTICK_TOUCH_L,
45 SDL_GAMEPAD_BUTTON_HORI_JOYSTICK_TOUCH_R,
46 SDL_GAMEPAD_NUM_HORI_BUTTONS
47};
48
49typedef struct
50{
51 Uint8 last_state[USB_PACKET_LENGTH];
52 Uint64 sensor_ticks;
53 Uint32 last_tick;
54 bool wireless;
55 bool serial_needs_init;
56} SDL_DriverSteamHori_Context;
57
58static bool HIDAPI_DriverSteamHori_UpdateDevice(SDL_HIDAPI_Device *device);
59
60static void HIDAPI_DriverSteamHori_RegisterHints(SDL_HintCallback callback, void *userdata)
61{
62 SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_STEAM_HORI, callback, userdata);
63}
64
65static void HIDAPI_DriverSteamHori_UnregisterHints(SDL_HintCallback callback, void *userdata)
66{
67 SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_STEAM_HORI, callback, userdata);
68}
69
70static bool HIDAPI_DriverSteamHori_IsEnabled(void)
71{
72 return SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_STEAM_HORI, SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI, SDL_HIDAPI_DEFAULT));
73}
74
75static bool HIDAPI_DriverSteamHori_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)
76{
77 return SDL_IsJoystickHoriSteamController(vendor_id, product_id);
78}
79
80static bool HIDAPI_DriverSteamHori_InitDevice(SDL_HIDAPI_Device *device)
81{
82 SDL_DriverSteamHori_Context *ctx;
83
84 ctx = (SDL_DriverSteamHori_Context *)SDL_calloc(1, sizeof(*ctx));
85 if (!ctx) {
86 return false;
87 }
88
89 device->context = ctx;
90 ctx->serial_needs_init = true;
91
92 HIDAPI_SetDeviceName(device, "Wireless HORIPAD For Steam");
93
94 return HIDAPI_JoystickConnected(device, NULL);
95}
96
97static int HIDAPI_DriverSteamHori_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id)
98{
99 return -1;
100}
101
102static void HIDAPI_DriverSteamHori_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index)
103{
104}
105
106static bool HIDAPI_DriverSteamHori_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
107{
108 SDL_DriverSteamHori_Context *ctx = (SDL_DriverSteamHori_Context *)device->context;
109
110 SDL_AssertJoysticksLocked();
111
112 SDL_zeroa(ctx->last_state);
113
114 /* Initialize the joystick capabilities */
115 joystick->nbuttons = SDL_GAMEPAD_NUM_HORI_BUTTONS;
116 joystick->naxes = SDL_GAMEPAD_AXIS_COUNT;
117 joystick->nhats = 1;
118
119 ctx->wireless = device->product_id == USB_PRODUCT_HORI_STEAM_CONTROLLER_BT;
120
121 if (ctx->wireless && device->serial) {
122 joystick->serial = SDL_strdup(device->serial);
123 ctx->serial_needs_init = false;
124 } else if (!ctx->wireless) {
125 // Need to actual read from the device to init the serial
126 HIDAPI_DriverSteamHori_UpdateDevice(device);
127 }
128
129 SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_GYRO, 250.0f);
130 SDL_PrivateJoystickAddSensor(joystick, SDL_SENSOR_ACCEL, 250.0f);
131
132 return true;
133}
134
135static bool HIDAPI_DriverSteamHori_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
136{
137 // Device doesn't support rumble
138 return SDL_Unsupported();
139}
140
141static bool HIDAPI_DriverSteamHori_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
142{
143 return SDL_Unsupported();
144}
145
146static Uint32 HIDAPI_DriverSteamHori_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
147{
148 return 0;
149}
150
151static bool HIDAPI_DriverSteamHori_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue)
152{
153 return SDL_Unsupported();
154}
155
156static bool HIDAPI_DriverSteamHori_SendJoystickEffect(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, const void *data, int size)
157{
158 return SDL_Unsupported();
159}
160
161static bool HIDAPI_DriverSteamHori_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, bool enabled)
162{
163 return true;
164}
165
166#undef clamp
167#define clamp(val, min, max) (((val) > (max)) ? (max) : (((val) < (min)) ? (min) : (val)))
168
169#ifndef DEG2RAD
170#define DEG2RAD(x) ((float)(x) * (float)(SDL_PI_F / 180.f))
171#endif
172
173//---------------------------------------------------------------------------
174// Scale and clamp values to a range
175//---------------------------------------------------------------------------
176static float RemapValClamped(float val, float A, float B, float C, float D)
177{
178 if (A == B) {
179 return (val - B) >= 0.0f ? D : C;
180 } else {
181 float cVal = (val - A) / (B - A);
182 cVal = clamp(cVal, 0.0f, 1.0f);
183
184 return C + (D - C) * cVal;
185 }
186}
187
188#define REPORT_HEADER_USB 0x07
189#define REPORT_HEADER_BT 0x00
190
191static void HIDAPI_DriverSteamHori_HandleStatePacket(SDL_Joystick *joystick, SDL_DriverSteamHori_Context *ctx, Uint8 *data, int size)
192{
193 Sint16 axis;
194 Uint64 timestamp = SDL_GetTicksNS();
195
196 // Make sure it's gamepad state and not OTA FW update info
197 if (data[0] != REPORT_HEADER_USB && data[0] != REPORT_HEADER_BT) {
198 /* We don't know how to handle this report */
199 return;
200 }
201
202 #define READ_STICK_AXIS(offset) \
203 (data[offset] == 0x80 ? 0 : (Sint16)HIDAPI_RemapVal((float)((int)data[offset] - 0x80), -0x80, 0xff - 0x80, SDL_MIN_SINT16, SDL_MAX_SINT16))
204 {
205 axis = READ_STICK_AXIS(1);
206 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, axis);
207 axis = READ_STICK_AXIS(2);
208 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, axis);
209 axis = READ_STICK_AXIS(3);
210 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, axis);
211 axis = READ_STICK_AXIS(4);
212 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTY, axis);
213 }
214#undef READ_STICK_AXIS
215
216 if (ctx->last_state[5] != data[5]) {
217 Uint8 hat;
218
219 switch (data[5] & 0xF) {
220 case 0:
221 hat = SDL_HAT_UP;
222 break;
223 case 1:
224 hat = SDL_HAT_RIGHTUP;
225 break;
226 case 2:
227 hat = SDL_HAT_RIGHT;
228 break;
229 case 3:
230 hat = SDL_HAT_RIGHTDOWN;
231 break;
232 case 4:
233 hat = SDL_HAT_DOWN;
234 break;
235 case 5:
236 hat = SDL_HAT_LEFTDOWN;
237 break;
238 case 6:
239 hat = SDL_HAT_LEFT;
240 break;
241 case 7:
242 hat = SDL_HAT_LEFTUP;
243 break;
244 default:
245 hat = SDL_HAT_CENTERED;
246 break;
247 }
248 SDL_SendJoystickHat(timestamp, joystick, 0, hat);
249 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[5] & 0x10) != 0));
250 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[5] & 0x20) != 0));
251 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_HORI_QAM, ((data[5] & 0x40) != 0));
252 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[5] & 0x80) != 0));
253
254 }
255
256 if (ctx->last_state[6] != data[6]) {
257 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[6] & 0x01) != 0));
258 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_HORI_M1 /* M1 */, ((data[6] & 0x02) != 0));
259 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, ((data[6] & 0x04) != 0));
260 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, ((data[6] & 0x08) != 0));
261
262 // TODO: can we handle the digital trigger mode? The data seems to come through analog regardless of the trigger state
263 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, ((data[6] & 0x40) != 0));
264 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[6] & 0x80) != 0));
265 }
266
267 if (ctx->last_state[7] != data[7]) {
268 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[7] & 0x01) != 0));
269 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[7] & 0x02) != 0));
270 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[7] & 0x04) != 0));
271 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_HORI_M2, ((data[7] & 0x08) != 0));
272 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_HORI_JOYSTICK_TOUCH_L, ((data[7] & 0x10) != 0));
273 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_HORI_JOYSTICK_TOUCH_R, ((data[7] & 0x20) != 0));
274 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_HORI_FR, ((data[7] & 0x40) != 0));
275 SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_HORI_FL, ((data[7] & 0x80) != 0));
276 }
277
278 if (!ctx->wireless && ctx->serial_needs_init) {
279 char serial[18];
280 (void)SDL_snprintf(serial, sizeof(serial), "%.2x-%.2x-%.2x-%.2x-%.2x-%.2x",
281 data[38], data[39], data[40], data[41], data[42], data[43]);
282
283 joystick->serial = SDL_strdup(serial);
284 ctx->serial_needs_init = false;
285 }
286
287#define READ_TRIGGER_AXIS(offset) \
288 (Sint16)(((int)data[offset] * 257) - 32768)
289 {
290 axis = READ_TRIGGER_AXIS(8);
291 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, axis);
292 axis = READ_TRIGGER_AXIS(9);
293 SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis);
294 }
295#undef READ_TRIGGER_AXIS
296
297 if (1) {
298 Uint64 sensor_timestamp;
299 float imu_data[3];
300
301 /* 16-bit timestamp */
302 Uint32 delta;
303 Uint16 tick = LOAD16(data[10],
304 data[11]);
305 if (ctx->last_tick < tick) {
306 delta = (tick - ctx->last_tick);
307 } else {
308 delta = (SDL_MAX_UINT16 - ctx->last_tick + tick + 1);
309 }
310
311 ctx->last_tick = tick;
312 ctx->sensor_ticks += delta;
313
314 /* Sensor timestamp is in 1us units, but there seems to be some issues with the values reported from the device */
315 sensor_timestamp = timestamp; // if the values were good we woudl call SDL_US_TO_NS(ctx->sensor_ticks);
316
317 const float accelScale = SDL_STANDARD_GRAVITY * 8 / 32768.0f;
318 const float gyroScale = DEG2RAD(2048);
319
320 imu_data[1] = RemapValClamped(-1.0f * LOAD16(data[12], data[13]), INT16_MIN, INT16_MAX, -gyroScale, gyroScale);
321 imu_data[2] = RemapValClamped(-1.0f * LOAD16(data[14], data[15]), INT16_MIN, INT16_MAX, -gyroScale, gyroScale);
322 imu_data[0] = RemapValClamped(-1.0f * LOAD16(data[16], data[17]), INT16_MIN, INT16_MAX, -gyroScale, gyroScale);
323
324
325 SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_GYRO, sensor_timestamp, imu_data, 3);
326
327 // SDL_Log("%u %f, %f, %f ", data[0], imu_data[0], imu_data[1], imu_data[2] );
328 imu_data[2] = LOAD16(data[18], data[19]) * accelScale;
329 imu_data[1] = -1 * LOAD16(data[20], data[21]) * accelScale;
330 imu_data[0] = LOAD16(data[22], data[23]) * accelScale;
331 SDL_SendJoystickSensor(timestamp, joystick, SDL_SENSOR_ACCEL, sensor_timestamp, imu_data, 3);
332 }
333
334 if (ctx->last_state[24] != data[24]) {
335 bool bCharging = (data[24] & 0x10) != 0;
336 int percent = (data[24] & 0xF) * 10;
337 SDL_PowerState state;
338 if (bCharging) {
339 state = SDL_POWERSTATE_CHARGING;
340 } else if (ctx->wireless) {
341 state = SDL_POWERSTATE_ON_BATTERY;
342 } else {
343 state = SDL_POWERSTATE_CHARGED;
344 }
345
346 SDL_SendJoystickPowerInfo(joystick, state, percent);
347 }
348
349 SDL_memcpy(ctx->last_state, data, SDL_min(size, sizeof(ctx->last_state)));
350}
351
352static bool HIDAPI_DriverSteamHori_UpdateDevice(SDL_HIDAPI_Device *device)
353{
354 SDL_DriverSteamHori_Context *ctx = (SDL_DriverSteamHori_Context *)device->context;
355 SDL_Joystick *joystick = NULL;
356 Uint8 data[USB_PACKET_LENGTH];
357 int size = 0;
358
359 if (device->num_joysticks > 0) {
360 joystick = SDL_GetJoystickFromID(device->joysticks[0]);
361 } else {
362 return false;
363 }
364
365 while ((size = SDL_hid_read_timeout(device->dev, data, sizeof(data), 0)) > 0) {
366#ifdef DEBUG_HORI_PROTOCOL
367 HIDAPI_DumpPacket("Google Hori packet: size = %d", data, size);
368#endif
369 if (!joystick) {
370 continue;
371 }
372
373 HIDAPI_DriverSteamHori_HandleStatePacket(joystick, ctx, data, size);
374 }
375
376 if (size < 0) {
377 /* Read error, device is disconnected */
378 HIDAPI_JoystickDisconnected(device, device->joysticks[0]);
379 }
380 return (size >= 0);
381}
382
383static void HIDAPI_DriverSteamHori_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
384{
385}
386
387static void HIDAPI_DriverSteamHori_FreeDevice(SDL_HIDAPI_Device *device)
388{
389}
390
391SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverSteamHori = {
392 SDL_HINT_JOYSTICK_HIDAPI_STEAM_HORI,
393 true,
394 HIDAPI_DriverSteamHori_RegisterHints,
395 HIDAPI_DriverSteamHori_UnregisterHints,
396 HIDAPI_DriverSteamHori_IsEnabled,
397 HIDAPI_DriverSteamHori_IsSupportedDevice,
398 HIDAPI_DriverSteamHori_InitDevice,
399 HIDAPI_DriverSteamHori_GetDevicePlayerIndex,
400 HIDAPI_DriverSteamHori_SetDevicePlayerIndex,
401 HIDAPI_DriverSteamHori_UpdateDevice,
402 HIDAPI_DriverSteamHori_OpenJoystick,
403 HIDAPI_DriverSteamHori_RumbleJoystick,
404 HIDAPI_DriverSteamHori_RumbleJoystickTriggers,
405 HIDAPI_DriverSteamHori_GetJoystickCapabilities,
406 HIDAPI_DriverSteamHori_SetJoystickLED,
407 HIDAPI_DriverSteamHori_SendJoystickEffect,
408 HIDAPI_DriverSteamHori_SetJoystickSensorsEnabled,
409 HIDAPI_DriverSteamHori_CloseJoystick,
410 HIDAPI_DriverSteamHori_FreeDevice,
411};
412
413#endif /* SDL_JOYSTICK_HIDAPI_STEAM_HORI */
414
415#endif /* SDL_JOYSTICK_HIDAPI */
416