From 72dd79752e268e197eecf7aa6555ddfac9bdfd98 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Tue, 6 May 2025 20:11:50 -0700 Subject: [PATCH] joystick: Add initial support for GIP flight sticks At the moment, only the ThrustMaster T.Flight Hotas One has full support. The documentation says you can query the extra buttons via a specific command, but the stick appears to reject the command. Further investigation is needed for automatically querying this state. --- src/joystick/hidapi/SDL_hidapi_gip.c | 92 ++++++++++++++++++++++++++++ src/joystick/usb_ids.h | 1 + 2 files changed, 93 insertions(+) diff --git a/src/joystick/hidapi/SDL_hidapi_gip.c b/src/joystick/hidapi/SDL_hidapi_gip.c index a1f72ce4bf..0fe53f7897 100644 --- a/src/joystick/hidapi/SDL_hidapi_gip.c +++ b/src/joystick/hidapi/SDL_hidapi_gip.c @@ -314,6 +314,8 @@ typedef struct GIP_Quirks Uint32 extra_in_system[8]; Uint32 extra_out_system[8]; GIP_AttachmentType device_type; + Uint8 extra_buttons; + Uint8 extra_axes; } GIP_Quirks; static const GIP_Quirks quirks[] = { @@ -348,6 +350,12 @@ static const GIP_Quirks quirks[] = { .filtered_features = GIP_FEATURE_MOTOR_CONTROL, .device_type = GIP_TYPE_ARCADE_STICK }, + { USB_VENDOR_THRUSTMASTER, USB_PRODUCT_THRUSTMASTER_T_FLIGHT_HOTAS_ONE, 0, + .filtered_features = GIP_FEATURE_MOTOR_CONTROL, + .device_type = GIP_TYPE_FLIGHT_STICK, + .extra_buttons = 5, + .extra_axes = 3 }, + {0}, }; @@ -451,6 +459,10 @@ typedef struct GIP_Attachment Uint8 share_button_idx; Uint8 paddle_idx; int paddle_offset; + + Uint8 extra_button_idx; + int extra_buttons; + int extra_axes; } GIP_Attachment; typedef struct GIP_Device @@ -658,6 +670,9 @@ static void GIP_HandleQuirks(GIP_Attachment *attachment) attachment->metadata.device.in_system_messages[j] |= quirks[i].extra_in_system[j]; attachment->metadata.device.out_system_messages[j] |= quirks[i].extra_out_system[j]; } + + attachment->extra_buttons = quirks[i].extra_buttons; + attachment->extra_axes = quirks[i].extra_axes; break; } } @@ -1168,6 +1183,10 @@ static bool GIP_SendInitSequence(GIP_Attachment *attachment) GIP_SendVendorMessage(attachment, GIP_CMD_INITIAL_REPORTS_REQUEST, 0, (const Uint8 *)&request, sizeof(request)); } + if (GIP_SupportsVendorMessage(attachment, GIP_CMD_DEVICE_CAPABILITIES, false)) { + GIP_SendVendorMessage(attachment, GIP_CMD_DEVICE_CAPABILITIES, 0, NULL, 0); + } + if (!attachment->joystick) { return HIDAPI_JoystickConnected(attachment->device->device, &attachment->joystick); } @@ -1833,6 +1852,67 @@ static void GIP_HandleArcadeStickReport( } } +static void GIP_HandleFlightStickReport( + GIP_Attachment *attachment, + SDL_Joystick *joystick, + Uint64 timestamp, + const Uint8 *bytes, + int num_bytes) +{ + Sint16 axis; + int i; + + if (num_bytes < 19) { + return; + } + + if (attachment->last_input[2] != bytes[2]) { + /* Fire 1 and 2 */ + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((bytes[2] & 0x01) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((bytes[2] & 0x02) != 0)); + } + for (i = 0; i < attachment->extra_buttons;) { + if (attachment->last_input[i / 8 + 3] != bytes[i / 8 + 3]) { + for (; i < attachment->extra_buttons; i++) { + SDL_SendJoystickButton(timestamp, + joystick, + (Uint8) (attachment->extra_button_idx + i), + ((bytes[i / 8 + 3] & (1u << i)) != 0)); + } + } else { + i += 8; + } + } + + /* Roll, pitch and yaw are signed. Throttle and any extra axes are unsigned. All values are full-range. */ + axis = bytes[11]; + axis |= bytes[12] << 8; + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTX, axis); + + axis = bytes[13]; + axis |= bytes[14] << 8; + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFTY, axis); + + axis = bytes[15]; + axis |= bytes[16] << 8; + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHTX, axis); + + /* There are no more signed values, so skip RIGHTY */ + + axis = (bytes[18] << 8) - 0x8000; + axis |= bytes[17]; + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis); + + for (i = 0; i < attachment->extra_axes; i++) { + if (20 + i * 2 >= num_bytes) { + return; + } + axis = (bytes[20 + i * 2] << 8) - 0x8000; + axis |= bytes[19 + i * 2]; + SDL_SendJoystickAxis(timestamp, joystick, (Uint8) (SDL_GAMEPAD_AXIS_RIGHT_TRIGGER + i), axis); + } +} + static bool GIP_HandleLLInputReport( GIP_Attachment *attachment, const GIP_Header *header, @@ -1875,6 +1955,9 @@ static bool GIP_HandleLLInputReport( case GIP_TYPE_ARCADE_STICK: GIP_HandleArcadeStickReport(attachment, joystick, timestamp, bytes, num_bytes); break; + case GIP_TYPE_FLIGHT_STICK: + GIP_HandleFlightStickReport(attachment, joystick, timestamp, bytes, num_bytes); + break; } if ((attachment->features & GIP_FEATURE_ELITE_BUTTONS) && @@ -2395,8 +2478,17 @@ static bool HIDAPI_DriverGIP_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystic attachment->share_button_idx = (Uint8) joystick->nbuttons; joystick->nbuttons++; } + if (attachment->extra_buttons > 0) { + attachment->extra_button_idx = (Uint8) joystick->nbuttons; + joystick->nbuttons += attachment->extra_buttons; + } joystick->naxes = SDL_GAMEPAD_AXIS_COUNT; + if (attachment->attachment_type == GIP_TYPE_FLIGHT_STICK) { + /* Flight sticks have at least 4 axes, but only 3 are signed values, so we leave RIGHTY unused */ + joystick->naxes += attachment->extra_axes - 1; + } + joystick->nhats = 1; return true; diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h index 32a8c94188..c2418e0a37 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -131,6 +131,7 @@ #define USB_PRODUCT_STEALTH_ULTRA_WIRED 0x7073 #define USB_PRODUCT_SWITCH_RETROBIT_CONTROLLER 0x0575 #define USB_PRODUCT_THRUSTMASTER_ESWAPX_PRO_PS4 0xd00e +#define USB_PRODUCT_THRUSTMASTER_T_FLIGHT_HOTAS_ONE 0xb68c #define USB_PRODUCT_VALVE_STEAM_CONTROLLER_DONGLE 0x1142 #define USB_PRODUCT_VICTRIX_FS_PRO 0x0203 #define USB_PRODUCT_VICTRIX_FS_PRO_V2 0x0207