From bd49de1cf9d7f741fd2402ca2f2b6208f18fcfe5 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Sat, 30 Aug 2025 09:52:08 -0700 Subject: [PATCH 1/2] Added support for the Switch 2 Joy-Cons with charging grip --- src/hidapi/SDL_hidapi.c | 4 +- src/joystick/SDL_gamepad.c | 34 +- src/joystick/SDL_joystick.c | 16 +- src/joystick/controller_list.h | 4 + src/joystick/hidapi/SDL_hidapi_switch.c | 10 +- src/joystick/hidapi/SDL_hidapi_switch2.c | 594 ++++++++++++++++++----- src/joystick/hidapi/SDL_hidapijoystick.c | 10 +- src/joystick/usb_ids.h | 7 +- 8 files changed, 549 insertions(+), 130 deletions(-) diff --git a/src/hidapi/SDL_hidapi.c b/src/hidapi/SDL_hidapi.c index 7ec04e203998d..275a12553a8cf 100644 --- a/src/hidapi/SDL_hidapi.c +++ b/src/hidapi/SDL_hidapi.c @@ -844,8 +844,10 @@ static const struct { Uint16 product; } SDL_libusb_whitelist[] = { { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_GAMECUBE_ADAPTER }, - { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_PRO }, { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER }, + { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT }, + { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT }, + { USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_PRO }, }; static bool IsInWhitelist(Uint16 vendor, Uint16 product) diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c index 91de1d3b6e3c8..c5ef232b31b7c 100644 --- a/src/joystick/SDL_gamepad.c +++ b/src/joystick/SDL_gamepad.c @@ -716,12 +716,32 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid) // GameCube driver has 12 buttons and 6 axes SDL_strlcat(mapping_string, "a:b0,b:b2,dpdown:b6,dpleft:b4,dpright:b5,dpup:b7,lefttrigger:a4,leftx:a0,lefty:a1~,rightshoulder:b9,righttrigger:a5,rightx:a2,righty:a3~,start:b8,x:b1,y:b3,misc3:b11,misc4:b10,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); } else if (vendor == USB_VENDOR_NINTENDO && - (product == USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER)) { - // Switch 2 GameCube has additional buttons for ZL and C - SDL_strlcat(mapping_string, "a:b1,b:b3,dpdown:b8,dpleft:b10,dpright:b9,dpup:b11,guide:b16,leftshoulder:b13,lefttrigger:a4,leftx:a0,lefty:a1~,misc1:b17,misc2:b20,misc3:b4,misc4:b12,rightshoulder:b5,righttrigger:a5,rightx:a2,righty:a3~,start:b6,x:b0,y:b2,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); + product == USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER) { + SDL_strlcat(mapping_string, "a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b4,leftshoulder:b6,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a5,rightx:a2,righty:a3,start:b5,x:b2,y:b3,misc1:b8,misc2:b9,misc3:b10,misc4:b11,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); } else if (vendor == USB_VENDOR_NINTENDO && - (product == USB_PRODUCT_NINTENDO_SWITCH2_PRO)) { - SDL_strlcat(mapping_string, "a:b0,b:b1,dpdown:b8,dpleft:b10,dpright:b9,dpup:b11,guide:b16,leftshoulder:b12,lefttrigger:b13,leftx:a0,lefty:a1~,misc1:b17,misc2:b20,rightshoulder:b4,righttrigger:b5,rightx:a2,righty:a3~,start:b6,back:b14,x:b2,y:b3,leftstick:b15,rightstick:b7,paddle1:b18,paddle2:b19,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); + product == USB_PRODUCT_NINTENDO_SWITCH2_PRO) { + SDL_strlcat(mapping_string, "a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,misc1:b11,misc2:b12,paddle1:b13,paddle2:b14,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); + } else if (vendor == USB_VENDOR_NINTENDO && + product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT) { + if (SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, false)) { + // Vertical mode + SDL_strlcat(mapping_string, "back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b11,paddle2:b14,paddle4:b16,", sizeof(mapping_string)); + } else { + // Mini gamepad mode + SDL_strlcat(mapping_string, "a:b0,b:b1,guide:b5,leftshoulder:b9,leftstick:b7,leftx:a0,lefty:a1,rightshoulder:b10,start:b6,x:b2,y:b3,paddle2:b14,paddle4:b16,", sizeof(mapping_string)); + } + } else if (vendor == USB_VENDOR_NINTENDO && + product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT) { + if (SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, false)) { + // Vertical mode + SDL_strlcat(mapping_string, "a:b0,b:b1,guide:b5,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,misc2:b12,paddle1:b13,paddle3:b15,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); + } else { + // Mini gamepad mode + SDL_strlcat(mapping_string, "a:b0,b:b1,guide:b5,leftshoulder:b9,leftstick:b7,leftx:a0,lefty:a1,rightshoulder:b10,start:b6,x:b2,y:b3,misc2:b12,paddle1:b13,paddle3:b15,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); + } + } else if (vendor == USB_VENDOR_NINTENDO && + product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_PAIR) { + SDL_strlcat(mapping_string, "a:b0,b:b1,back:b4,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b5,leftshoulder:b9,leftstick:b7,lefttrigger:a4,leftx:a0,lefty:a1,rightshoulder:b10,rightstick:b8,righttrigger:a5,rightx:a2,righty:a3,start:b6,x:b2,y:b3,misc1:b11,misc2:b12,paddle1:b13,paddle2:b14,paddle3:b15,paddle4:b16,hint:!SDL_GAMECONTROLLER_USE_BUTTON_LABELS:=1,", sizeof(mapping_string)); } else if (vendor == USB_VENDOR_NINTENDO && (guid.data[15] == k_eSwitchDeviceInfoControllerType_HVCLeft || guid.data[15] == k_eSwitchDeviceInfoControllerType_HVCRight || @@ -840,7 +860,7 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid) // GC Ultimate Primary Map SDL_strlcat(mapping_string, "a:b0,b:b1,x:b2,y:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b6,leftstick:b4,lefttrigger:a4,leftx:a0,lefty:a1,misc1:b13,misc2:b14,rightshoulder:b7,rightstick:b5,righttrigger:a5,rightx:a2,righty:a3,start:b10,misc3:b8,misc4:b9,hint:!SDL_GAMECONTROLLER_USE_GAMECUBE_LABELS:=1,", sizeof(mapping_string)); break; - } + } break; case USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC: switch (sub_type) { @@ -850,7 +870,7 @@ static GamepadMapping_t *SDL_CreateMappingForHIDAPIGamepad(SDL_GUID guid) break; } break; - + case USB_PRODUCT_BONZIRICHANNEL_FIREBIRD: default: // Unmapped device diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c index d88a479186b77..a6d047bd14113 100644 --- a/src/joystick/SDL_joystick.c +++ b/src/joystick/SDL_joystick.c @@ -2953,10 +2953,14 @@ SDL_GamepadType SDL_GetGamepadTypeFromVIDPID(Uint16 vendor, Uint16 product, cons } else if (vendor == 0x0001 && product == 0x0001) { type = SDL_GAMEPAD_TYPE_STANDARD; - } else if (vendor == USB_VENDOR_NINTENDO && product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT) { + } else if (vendor == USB_VENDOR_NINTENDO && + (product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT || + product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT)) { type = SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT; - } else if (vendor == USB_VENDOR_NINTENDO && product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT) { + } else if (vendor == USB_VENDOR_NINTENDO && + (product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT || + product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT)) { if (name && SDL_strstr(name, "NES Controller") != NULL) { // We don't have a type for the Nintendo Online NES Controller type = SDL_GAMEPAD_TYPE_STANDARD; @@ -2971,7 +2975,9 @@ SDL_GamepadType SDL_GetGamepadTypeFromVIDPID(Uint16 vendor, Uint16 product, cons type = SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT; } - } else if (vendor == USB_VENDOR_NINTENDO && product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR) { + } else if (vendor == USB_VENDOR_NINTENDO && + (product == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR || + product == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_PAIR)) { type = SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR; } else if (forUI && SDL_IsJoystickGameCube(vendor, product)) { @@ -3161,7 +3167,9 @@ bool SDL_IsJoystickNintendoSwitchJoyConGrip(Uint16 vendor_id, Uint16 product_id) bool SDL_IsJoystickNintendoSwitchJoyConPair(Uint16 vendor_id, Uint16 product_id) { - return vendor_id == USB_VENDOR_NINTENDO && product_id == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR; + return vendor_id == USB_VENDOR_NINTENDO && + (product_id == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR || + product_id == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_PAIR); } bool SDL_IsJoystickGameCube(Uint16 vendor_id, Uint16 product_id) diff --git a/src/joystick/controller_list.h b/src/joystick/controller_list.h index cd7c4871e50dd..f1994e3756cdb 100644 --- a/src/joystick/controller_list.h +++ b/src/joystick/controller_list.h @@ -540,8 +540,11 @@ static const ControllerDescription_t arrControllers[] = { { MAKE_CONTROLLER_ID( 0x05ac, 0x0002 ), k_eControllerType_AppleController, NULL }, // MFI Standard Gamepad (generic entry for iOS/tvOS) { MAKE_CONTROLLER_ID( 0x057e, 0x2006 ), k_eControllerType_SwitchJoyConLeft, NULL }, // Nintendo Switch Joy-Con (Left) + { MAKE_CONTROLLER_ID( 0x057e, 0x2067 ), k_eControllerType_SwitchJoyConLeft, NULL }, // Nintendo Switch 2 Joy-Con (Left) { MAKE_CONTROLLER_ID( 0x057e, 0x2007 ), k_eControllerType_SwitchJoyConRight, NULL }, // Nintendo Switch Joy-Con (Right) + { MAKE_CONTROLLER_ID( 0x057e, 0x2066 ), k_eControllerType_SwitchJoyConRight, NULL }, // Nintendo Switch 2 Joy-Con (Right) { MAKE_CONTROLLER_ID( 0x057e, 0x2008 ), k_eControllerType_SwitchJoyConPair, NULL }, // Nintendo Switch Joy-Con (Left+Right Combined) + { MAKE_CONTROLLER_ID( 0x057e, 0x2068 ), k_eControllerType_SwitchJoyConPair, NULL }, // Nintendo Switch 2 Joy-Con (Left+Right Combined) // This same controller ID is spoofed by many 3rd-party Switch controllers. // The ones we currently know of are: @@ -550,6 +553,7 @@ static const ControllerDescription_t arrControllers[] = { // * ZhiXu Gamepad Wireless // * Sunwaytek Wireless Motion Controller for Nintendo Switch { MAKE_CONTROLLER_ID( 0x057e, 0x2009 ), k_eControllerType_SwitchProController, NULL }, // Nintendo Switch Pro Controller + { MAKE_CONTROLLER_ID( 0x057e, 0x2069 ), k_eControllerType_SwitchProController, NULL }, // Nintendo Switch 2 Pro Controller //{ MAKE_CONTROLLER_ID( 0x057e, 0x2017 ), k_eControllerType_SwitchProController, NULL }, // Nintendo Online SNES Controller //{ MAKE_CONTROLLER_ID( 0x057e, 0x2019 ), k_eControllerType_SwitchProController, NULL }, // Nintendo Online N64 Controller //{ MAKE_CONTROLLER_ID( 0x057e, 0x201e ), k_eControllerType_SwitchProController, NULL }, // Nintendo Online SEGA Genesis Controller diff --git a/src/joystick/hidapi/SDL_hidapi_switch.c b/src/joystick/hidapi/SDL_hidapi_switch.c index 7038e4f405bbf..92d14ab2f65df 100644 --- a/src/joystick/hidapi/SDL_hidapi_switch.c +++ b/src/joystick/hidapi/SDL_hidapi_switch.c @@ -1388,7 +1388,15 @@ static bool HIDAPI_DriverSwitch_IsSupportedDevice(SDL_HIDAPI_Device *device, con return false; } - return (type == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO); + if (type != SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO) { + return false; + } + + // The Nintendo Switch 2 Pro uses another driver + if (vendor_id == USB_VENDOR_NINTENDO && product_id == USB_PRODUCT_NINTENDO_SWITCH2_PRO) { + return false; + } + return true; } static void UpdateDeviceIdentity(SDL_HIDAPI_Device *device) diff --git a/src/joystick/hidapi/SDL_hidapi_switch2.c b/src/joystick/hidapi/SDL_hidapi_switch2.c index 95d1cb46c4d67..22762709aa2e3 100644 --- a/src/joystick/hidapi/SDL_hidapi_switch2.c +++ b/src/joystick/hidapi/SDL_hidapi_switch2.c @@ -32,6 +32,44 @@ #ifdef SDL_JOYSTICK_HIDAPI_SWITCH2 +// Define this if you want to log all packets from the controller +#if 0 +#define DEBUG_SWITCH2_PROTOCOL +#endif + +enum +{ + SDL_GAMEPAD_BUTTON_SWITCH2_PRO_SHARE = 11, + SDL_GAMEPAD_BUTTON_SWITCH2_PRO_C, + SDL_GAMEPAD_BUTTON_SWITCH2_PRO_RIGHT_PADDLE, + SDL_GAMEPAD_BUTTON_SWITCH2_PRO_LEFT_PADDLE, + SDL_GAMEPAD_NUM_SWITCH2_PRO_BUTTONS +}; + +enum +{ + SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_SHARE = 11, + SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_C, + SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_RIGHT_PADDLE1, + SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_LEFT_PADDLE1, + SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_RIGHT_PADDLE2, + SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_LEFT_PADDLE2, + SDL_GAMEPAD_NUM_SWITCH2_JOYCON_BUTTONS +}; + +enum +{ + SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_GUIDE = 4, + SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_START, + SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_LEFT_SHOULDER, + SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_RIGHT_SHOULDER, + SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_SHARE, + SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_C, + SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_LEFT_TRIGGER, // Full trigger pull click + SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_RIGHT_TRIGGER, // Full trigger pull click + SDL_GAMEPAD_NUM_SWITCH2_GAMECUBE_BUTTONS +}; + typedef struct { Uint16 neutral; @@ -58,6 +96,9 @@ typedef struct Switch2_StickCalibration right_stick; Uint8 left_trigger_max; Uint8 right_trigger_max; + + bool vertical_mode; + Uint8 last_state[USB_PACKET_LENGTH]; } SDL_DriverSwitch2_Context; static void ParseStickCalibration(Switch2_StickCalibration *stick_data, const Uint8 *data) @@ -127,7 +168,7 @@ static int RecvBulkData(SDL_DriverSwitch2_Context *ctx, Uint8 *data, unsigned si return total_transferred; } -static void MapJoystickAxis(Uint64 timestamp, SDL_Joystick *joystick, int axis, const Switch2_AxisCalibration *calib, float value) +static void MapJoystickAxis(Uint64 timestamp, SDL_Joystick *joystick, Uint8 axis, const Switch2_AxisCalibration *calib, float value, bool invert) { Sint16 mapped_value; if (calib && calib->neutral && calib->min && calib->max) { @@ -141,10 +182,13 @@ static void MapJoystickAxis(Uint64 timestamp, SDL_Joystick *joystick, int axis, } else { mapped_value = (Sint16) HIDAPI_RemapVal(value, 0, 4096, SDL_MIN_SINT16, SDL_MAX_SINT16); } + if (invert) { + mapped_value = ~mapped_value; + } SDL_SendJoystickAxis(timestamp, joystick, axis, mapped_value); } -static void MapTriggerAxis(Uint64 timestamp, SDL_Joystick *joystick, int axis, Uint8 max, float value) +static void MapTriggerAxis(Uint64 timestamp, SDL_Joystick *joystick, Uint8 axis, Uint8 max, float value) { Sint16 mapped_value = (Sint16) HIDAPI_RemapVal( SDL_clamp((value - max) / (232.f - max), 0, 1), @@ -174,6 +218,8 @@ static bool HIDAPI_DriverSwitch2_IsSupportedDevice(SDL_HIDAPI_Device *device, co if (vendor_id == USB_VENDOR_NINTENDO) { switch (product_id) { case USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER: + case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT: + case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT: case USB_PRODUCT_NINTENDO_SWITCH2_PRO: return true; } @@ -366,122 +412,31 @@ static void HIDAPI_DriverSwitch2_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, { } -static bool HIDAPI_DriverSwitch2_UpdateDevice(SDL_HIDAPI_Device *device) +static bool HIDAPI_DriverSwitch2_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) { SDL_DriverSwitch2_Context *ctx = (SDL_DriverSwitch2_Context *)device->context; - const struct { - int byte; - unsigned char mask; - } buttons[] = { - {3, 0x01}, // B - {3, 0x02}, // A - {3, 0x04}, // Y - {3, 0x08}, // X - {3, 0x10}, // R (GameCube R Click) - {3, 0x20}, // ZR (GameCube Z) - {3, 0x40}, // PLUS (GameCube Start) - {3, 0x80}, // RS (not on GameCube) - {4, 0x01}, // DPAD_DOWN - {4, 0x02}, // DPAD_RIGHT - {4, 0x04}, // DPAD_LEFT - {4, 0x08}, // DPAD_UP - {4, 0x10}, // L (GameCube L Click) - {4, 0x20}, // ZL - {4, 0x40}, // MINUS (not on GameCube) - {4, 0x80}, // LS (not on GameCube) - {5, 0x01}, // Home - {5, 0x02}, // Capture - {5, 0x04}, // GR (not on GameCube) - {5, 0x08}, // GL (not on GameCube) - {5, 0x10}, // C - }; - - SDL_Joystick *joystick = NULL; - if (device->num_joysticks > 0) { - joystick = SDL_GetJoystickFromID(device->joysticks[0]); - } - if (joystick == NULL) { - return true; - } - - // Read input packet - - Uint8 packet[USB_PACKET_LENGTH]; - int size; - while ((size = SDL_hid_read_timeout(device->dev, packet, sizeof(packet), 0)) > 0) { - if (size < 15) { - continue; - } - - Uint64 timestamp = SDL_GetTicksNS(); - for (size_t i = 0; i < SDL_arraysize(buttons); ++i) { - SDL_SendJoystickButton( - timestamp, - joystick, - (Uint8) i, - (packet[buttons[i].byte] & buttons[i].mask) != 0 - ); - } - - MapJoystickAxis( - timestamp, - joystick, - SDL_GAMEPAD_AXIS_LEFTX, - &ctx->left_stick.x, - (float) (packet[6] | ((packet[7] & 0x0F) << 8)) - ); - MapJoystickAxis( - timestamp, - joystick, - SDL_GAMEPAD_AXIS_LEFTY, - &ctx->left_stick.y, - (float) ((packet[7] >> 4) | (packet[8] << 4)) - ); - MapJoystickAxis( - timestamp, - joystick, - SDL_GAMEPAD_AXIS_RIGHTX, - &ctx->right_stick.x, - (float) (packet[9] | ((packet[10] & 0x0F) << 8)) - ); - MapJoystickAxis( - timestamp, - joystick, - SDL_GAMEPAD_AXIS_RIGHTY, - &ctx->right_stick.y, - (float) ((packet[10] >> 4) | (packet[11] << 4)) - ); - - if (device->product_id == USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER) { - MapTriggerAxis( - timestamp, - joystick, - SDL_GAMEPAD_AXIS_LEFT_TRIGGER, - ctx->left_trigger_max, - packet[13] - ); - MapTriggerAxis( - timestamp, - joystick, - SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, - ctx->right_trigger_max, - packet[14] - ); - } - } - return true; -} - -static bool HIDAPI_DriverSwitch2_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) -{ // Initialize the joystick capabilities - joystick->nbuttons = 21; - if (device->product_id == USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER) { - joystick->naxes = 6; - } else { - joystick->naxes = 4; + switch (device->product_id) { + case USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER: + joystick->nbuttons = SDL_GAMEPAD_NUM_SWITCH2_GAMECUBE_BUTTONS; + break; + case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT: + case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT: + joystick->nbuttons = SDL_GAMEPAD_NUM_SWITCH2_JOYCON_BUTTONS; + break; + case USB_PRODUCT_NINTENDO_SWITCH2_PRO: + joystick->nbuttons = SDL_GAMEPAD_NUM_SWITCH2_PRO_BUTTONS; + break; + default: + // FIXME: How many buttons does this have? + break; } + joystick->naxes = SDL_GAMEPAD_AXIS_COUNT; + joystick->nhats = 1; + + // Set up for vertical mode + ctx->vertical_mode = SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, false); return true; } @@ -516,6 +471,419 @@ static bool HIDAPI_DriverSwitch2_SetJoystickSensorsEnabled(SDL_HIDAPI_Device *de return SDL_Unsupported(); } +static void HandleGameCubeState(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size) +{ + if (data[3] != ctx->last_state[3]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x01) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x02) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x04) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x08) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_RIGHT_TRIGGER, ((data[3] & 0x10) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_RIGHT_SHOULDER, ((data[3] & 0x20) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_START, ((data[3] & 0x40) != 0)); + } + + if (data[4] != ctx->last_state[4]) { + Uint8 hat = 0; + + if (data[4] & 0x01) { + hat |= SDL_HAT_DOWN; + } + if (data[4] & 0x02) { + hat |= SDL_HAT_RIGHT; + } + if (data[4] & 0x04) { + hat |= SDL_HAT_LEFT; + } + if (data[4] & 0x08) { + hat |= SDL_HAT_UP; + } + SDL_SendJoystickHat(timestamp, joystick, 0, hat); + + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_LEFT_TRIGGER, ((data[4] & 0x10) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_LEFT_SHOULDER, ((data[4] & 0x20) != 0)); + } + + if (data[5] != ctx->last_state[5]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_GUIDE, ((data[5] & 0x01) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_SHARE, ((data[5] & 0x02) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_GAMECUBE_C, ((data[5] & 0x10) != 0)); + } + + MapTriggerAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFT_TRIGGER, + ctx->left_trigger_max, + data[13] + ); + MapTriggerAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, + ctx->right_trigger_max, + data[14] + ); + + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFTX, + &ctx->left_stick.x, + (float) (data[6] | ((data[7] & 0x0F) << 8)), + false + ); + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFTY, + &ctx->left_stick.y, + (float) ((data[7] >> 4) | (data[8] << 4)), + true + ); + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_RIGHTX, + &ctx->right_stick.x, + (float) (data[9] | ((data[10] & 0x0F) << 8)), + false + ); + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_RIGHTY, + &ctx->right_stick.y, + (float)((data[10] >> 4) | (data[11] << 4)), + true + ); +} + +static void HandleCombinedControllerStateL(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size) +{ + // FIXME: When we find out what the SL and SR buttons are, map them to paddles + + if (data[3] != ctx->last_state[3]) { + Uint8 hat = 0; + + if (data[3] & 0x01) { + hat |= SDL_HAT_DOWN; + } + if (data[3] & 0x02) { + hat |= SDL_HAT_RIGHT; + } + if (data[3] & 0x04) { + hat |= SDL_HAT_LEFT; + } + if (data[3] & 0x08) { + hat |= SDL_HAT_UP; + } + SDL_SendJoystickHat(timestamp, joystick, 0, hat); + + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, ((data[3] & 0x10) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, ((data[3] & 0x40) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[3] & 0x80) != 0)); + } + + if (data[4] != ctx->last_state[4]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_PRO_SHARE, ((data[4] & 0x01) != 0)); + } + + Sint16 axis = (data[3] & 0x20) ? 32767 : -32768; + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis); + + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFTX, + &ctx->left_stick.x, + (float) (data[6] | ((data[7] & 0x0F) << 8)), + false + ); + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFTY, + &ctx->left_stick.y, + (float) ((data[7] >> 4) | (data[8] << 4)), + true + ); +} + +static void HandleMiniControllerStateL(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size) +{ + // FIXME: When we find out what the SL and SR buttons are, map them to shoulder buttons + + if (data[3] != ctx->last_state[3]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x01) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x02) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x04) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x08) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[3] & 0x40) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_LEFT_PADDLE1, ((data[3] & 0x10) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_LEFT_PADDLE2, ((data[3] & 0x20) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[3] & 0x80) != 0)); + } + + if (data[4] != ctx->last_state[4]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[4] & 0x01) != 0)); + } + + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFTX, + &ctx->left_stick.y, + (float) ((data[7] >> 4) | (data[8] << 4)), + true + ); + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFTY, + &ctx->left_stick.x, + (float) (data[6] | ((data[7] & 0x0F) << 8)), + true + ); +} + +static void HandleCombinedControllerStateR(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size) +{ + // FIXME: When we find out what the SL and SR buttons are, map them to paddles + + if (data[3] != ctx->last_state[3]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x01) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x02) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x04) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x08) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, ((data[3] & 0x10) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[3] & 0x40) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[3] & 0x80) != 0)); + } + + if (data[4] != ctx->last_state[4]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[4] & 0x01) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_C, ((data[4] & 0x10) != 0)); + } + + Sint16 axis = (data[3] & 0x20) ? 32767 : -32768; + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, axis); + + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_RIGHTX, + &ctx->left_stick.x, + (float) (data[6] | ((data[7] & 0x0F) << 8)), + false + ); + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_RIGHTY, + &ctx->left_stick.y, + (float) ((data[7] >> 4) | (data[8] << 4)), + true + ); +} + +static void HandleMiniControllerStateR(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size) +{ + // FIXME: When we find out what the SL and SR buttons are, map them to shoulder buttons + + if (data[3] != ctx->last_state[3]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x01) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x02) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x04) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x08) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_RIGHT_PADDLE1, ((data[3] & 0x10) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_RIGHT_PADDLE2, ((data[3] & 0x20) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[3] & 0x40) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[3] & 0x80) != 0)); + } + + if (data[4] != ctx->last_state[4]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[4] & 0x01) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_JOYCON_C, ((data[4] & 0x10) != 0)); + } + + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFTX, + &ctx->left_stick.y, + (float) ((data[7] >> 4) | (data[8] << 4)), + false + ); + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFTY, + &ctx->left_stick.x, + (float) (data[6] | ((data[7] & 0x0F) << 8)), + false + ); +} + +static void HandleSwitchProState(Uint64 timestamp, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size) +{ + Sint16 axis; + + if (data[3] != ctx->last_state[3]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SOUTH, ((data[3] & 0x01) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_EAST, ((data[3] & 0x02) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_WEST, ((data[3] & 0x04) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_NORTH, ((data[3] & 0x08) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, ((data[3] & 0x10) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_START, ((data[3] & 0x40) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_RIGHT_STICK, ((data[3] & 0x80) != 0)); + } + + if (data[4] != ctx->last_state[4]) { + Uint8 hat = 0; + + if (data[4] & 0x01) { + hat |= SDL_HAT_DOWN; + } + if (data[4] & 0x02) { + hat |= SDL_HAT_RIGHT; + } + if (data[4] & 0x04) { + hat |= SDL_HAT_LEFT; + } + if (data[4] & 0x08) { + hat |= SDL_HAT_UP; + } + SDL_SendJoystickHat(timestamp, joystick, 0, hat); + + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, ((data[4] & 0x10) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_BACK, ((data[4] & 0x40) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_LEFT_STICK, ((data[4] & 0x80) != 0)); + } + + if (data[5] != ctx->last_state[5]) { + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_GUIDE, ((data[5] & 0x01) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_PRO_SHARE, ((data[5] & 0x02) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_PRO_RIGHT_PADDLE, ((data[5] & 0x04) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_PRO_LEFT_PADDLE, ((data[5] & 0x08) != 0)); + SDL_SendJoystickButton(timestamp, joystick, SDL_GAMEPAD_BUTTON_SWITCH2_PRO_C, ((data[5] & 0x10) != 0)); + } + + axis = (data[4] & 0x20) ? 32767 : -32768; + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, axis); + + axis = (data[3] & 0x20) ? 32767 : -32768; + SDL_SendJoystickAxis(timestamp, joystick, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER, axis); + + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFTX, + &ctx->left_stick.x, + (float) (data[6] | ((data[7] & 0x0F) << 8)), + false + ); + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_LEFTY, + &ctx->left_stick.y, + (float) ((data[7] >> 4) | (data[8] << 4)), + true + ); + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_RIGHTX, + &ctx->right_stick.x, + (float) (data[9] | ((data[10] & 0x0F) << 8)), + false + ); + MapJoystickAxis( + timestamp, + joystick, + SDL_GAMEPAD_AXIS_RIGHTY, + &ctx->right_stick.y, + (float)((data[10] >> 4) | (data[11] << 4)), + true + ); +} + +static void HIDAPI_DriverSwitch2_HandleStatePacket(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size) +{ + Uint64 timestamp = SDL_GetTicksNS(); + + if (size < 15) { + // We don't know how to handle this report + return; + } + + switch (device->product_id) { + case USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER: + HandleGameCubeState(timestamp, joystick, ctx, data, size); + break; + case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT: + if (device->parent || ctx->vertical_mode) { + HandleCombinedControllerStateL(timestamp, joystick, ctx, data, size); + } else { + HandleMiniControllerStateL(timestamp, joystick, ctx, data, size); + } + break; + case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT: + if (device->parent || ctx->vertical_mode) { + HandleCombinedControllerStateR(timestamp, joystick, ctx, data, size); + } else { + HandleMiniControllerStateR(timestamp, joystick, ctx, data, size); + } + break; + case USB_PRODUCT_NINTENDO_SWITCH2_PRO: + HandleSwitchProState(timestamp, joystick, ctx, data, size); + break; + default: + // FIXME: Need state handling implementation + break; + } + + SDL_memcpy(ctx->last_state, data, SDL_min(size, sizeof(ctx->last_state))); +} + +static bool HIDAPI_DriverSwitch2_UpdateDevice(SDL_HIDAPI_Device *device) +{ + SDL_DriverSwitch2_Context *ctx = (SDL_DriverSwitch2_Context *)device->context; + SDL_Joystick *joystick = NULL; + Uint8 data[USB_PACKET_LENGTH]; + int size = 0; + + if (device->num_joysticks > 0) { + joystick = SDL_GetJoystickFromID(device->joysticks[0]); + } else { + return false; + } + + while ((size = SDL_hid_read_timeout(device->dev, data, sizeof(data), 0)) > 0) { +#ifdef DEBUG_SWITCH2_PROTOCOL + if (device->product_id == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT) { + HIDAPI_DumpPacket("Nintendo Joy-Con(L) packet: size = %d", data, size); + } else if (device->product_id == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT) { + HIDAPI_DumpPacket("Nintendo Joy-Con(R) packet: size = %d", data, size); + } else { + HIDAPI_DumpPacket("Nintendo Switch2 packet: size = %d", data, size); + } +#endif + if (!joystick) { + continue; + } + + HIDAPI_DriverSwitch2_HandleStatePacket(device, joystick, ctx, data, size); + } + + if (size < 0) { + // Read error, device is disconnected + HIDAPI_JoystickDisconnected(device, device->joysticks[0]); + } + return (size >= 0); +} + static void HIDAPI_DriverSwitch2_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) { } diff --git a/src/joystick/hidapi/SDL_hidapijoystick.c b/src/joystick/hidapi/SDL_hidapijoystick.c index bbe1bdf06fe9a..e05ee930b3632 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick.c +++ b/src/joystick/hidapi/SDL_hidapijoystick.c @@ -452,7 +452,9 @@ static void HIDAPI_SetupDeviceDriver(SDL_HIDAPI_Device *device, bool *removed) S if (device->driver) { bool enabled; - if (device->vendor_id == USB_VENDOR_NINTENDO && device->product_id == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR) { + if (device->vendor_id == USB_VENDOR_NINTENDO && + (device->product_id == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR || + device->product_id == USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_PAIR)) { enabled = SDL_HIDAPI_combine_joycons; } else { enabled = device->driver->enabled; @@ -1068,7 +1070,11 @@ static bool HIDAPI_CreateCombinedJoyCons(void) SDL_zero(info); info.path = "nintendo_joycons_combined"; info.vendor_id = USB_VENDOR_NINTENDO; - info.product_id = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR; + if (joycons[0]->product_id == USB_PRODUCT_NINTENDO_SWITCH_JOYCON_LEFT) { + info.product_id = USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR; + } else { + info.product_id = USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_PAIR; + } info.interface_number = -1; info.usage_page = USB_USAGEPAGE_GENERIC_DESKTOP; info.usage = USB_USAGE_GENERIC_GAMEPAD; diff --git a/src/joystick/usb_ids.h b/src/joystick/usb_ids.h index e6b6f23dbeba6..8557e95831458 100644 --- a/src/joystick/usb_ids.h +++ b/src/joystick/usb_ids.h @@ -103,8 +103,11 @@ #define USB_PRODUCT_NINTENDO_SWITCH_JOYCON_PAIR 0x2008 // Used by joycond #define USB_PRODUCT_NINTENDO_SWITCH_JOYCON_RIGHT 0x2007 #define USB_PRODUCT_NINTENDO_SWITCH_PRO 0x2009 -#define USB_PRODUCT_NINTENDO_SWITCH2_PRO 0x2069 #define USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER 0x2073 +#define USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT 0x2067 +#define USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_PAIR 0x2068 +#define USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT 0x2066 +#define USB_PRODUCT_NINTENDO_SWITCH2_PRO 0x2069 #define USB_PRODUCT_NINTENDO_WII_REMOTE 0x0306 #define USB_PRODUCT_NINTENDO_WII_REMOTE2 0x0330 #define USB_PRODUCT_NVIDIA_SHIELD_CONTROLLER_V103 0x7210 @@ -167,7 +170,7 @@ #define USB_PRODUCT_HANDHELDLEGEND_SINPUT_GENERIC 0x10c6 #define USB_PRODUCT_HANDHELDLEGEND_PROGCC 0x10df #define USB_PRODUCT_HANDHELDLEGEND_GCULTIMATE 0x10dd -#define USB_PRODUCT_BONZIRICHANNEL_FIREBIRD 0x10e0 +#define USB_PRODUCT_BONZIRICHANNEL_FIREBIRD 0x10e0 // USB usage pages #define USB_USAGEPAGE_GENERIC_DESKTOP 0x0001 From 8f95c517c46e06fc2d020d440d78692e9632dbc8 Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Sat, 30 Aug 2025 10:37:26 -0700 Subject: [PATCH 2/2] Added support for the player LED on Nintendo Switch 2 controllers --- src/joystick/hidapi/SDL_hidapi_switch2.c | 81 ++++++++++++++++++++---- 1 file changed, 70 insertions(+), 11 deletions(-) diff --git a/src/joystick/hidapi/SDL_hidapi_switch2.c b/src/joystick/hidapi/SDL_hidapi_switch2.c index 22762709aa2e3..13367d4438a25 100644 --- a/src/joystick/hidapi/SDL_hidapi_switch2.c +++ b/src/joystick/hidapi/SDL_hidapi_switch2.c @@ -85,6 +85,9 @@ typedef struct typedef struct { + SDL_HIDAPI_Device *device; + SDL_Joystick *joystick; + SDL_LibUSBContext *libusb; libusb_device_handle *device_handle; bool interface_claimed; @@ -97,6 +100,9 @@ typedef struct Uint8 left_trigger_max; Uint8 right_trigger_max; + bool player_lights; + int player_index; + bool vertical_mode; Uint8 last_state[USB_PACKET_LENGTH]; } SDL_DriverSwitch2_Context; @@ -198,6 +204,37 @@ static void MapTriggerAxis(Uint64 timestamp, SDL_Joystick *joystick, Uint8 axis, SDL_SendJoystickAxis(timestamp, joystick, axis, mapped_value); } +static bool UpdateSlotLED(SDL_DriverSwitch2_Context *ctx) +{ + unsigned char SET_LED_DATA[] = { + 0x09, 0x91, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + unsigned char calibration_data[0x50] = {0}; + + if (ctx->player_lights && ctx->player_index >= 0) { + SET_LED_DATA[8] = (1 << (ctx->player_index % 4)); + } + int res = SendBulkData(ctx, SET_LED_DATA, sizeof(SET_LED_DATA)); + if (res < 0) { + return SDL_SetError("Couldn't set LED data: %d\n", res); + } + return (RecvBulkData(ctx, calibration_data, 0x40) > 0); +} + +static void SDLCALL SDL_PlayerLEDHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint) +{ + SDL_DriverSwitch2_Context *ctx = (SDL_DriverSwitch2_Context *)userdata; + bool player_lights = SDL_GetStringBoolean(hint, true); + + if (player_lights != ctx->player_lights) { + ctx->player_lights = player_lights; + + UpdateSlotLED(ctx); + HIDAPI_UpdateDeviceProperties(ctx->device); + } +} + static void HIDAPI_DriverSwitch2_RegisterHints(SDL_HintCallback callback, void *userdata) { SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_SWITCH2, callback, userdata); @@ -300,10 +337,6 @@ static bool HIDAPI_DriverSwitch2_InitUSB(SDL_HIDAPI_Device *device) 0x03, 0x91, 0x00, 0x0d, 0x00, 0x08, 0x00, 0x00, 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; - const unsigned char SET_LED_DATA[] = { - 0x09, 0x91, 0x00, 0x07, 0x00, 0x08, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 - }; unsigned char flash_read_command[] = { 0x02, 0x91, 0x00, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x01, 0x00 @@ -316,12 +349,6 @@ static bool HIDAPI_DriverSwitch2_InitUSB(SDL_HIDAPI_Device *device) } RecvBulkData(ctx, calibration_data, 0x40); - res = SendBulkData(ctx, SET_LED_DATA, sizeof(SET_LED_DATA)); - if (res < 0) { - return SDL_SetError("Couldn't set LED data: %d\n", res); - } - RecvBulkData(ctx, calibration_data, 0x40); - flash_read_command[12] = 0x80; res = SendBulkData(ctx, flash_read_command, sizeof(flash_read_command)); if (res < 0) { @@ -377,6 +404,7 @@ static bool HIDAPI_DriverSwitch2_InitDevice(SDL_HIDAPI_Device *device) if (!ctx) { return false; } + ctx->device = device; device->context = ctx; if (device->is_bluetooth) { @@ -410,12 +438,31 @@ static int HIDAPI_DriverSwitch2_GetDevicePlayerIndex(SDL_HIDAPI_Device *device, static void HIDAPI_DriverSwitch2_SetDevicePlayerIndex(SDL_HIDAPI_Device *device, SDL_JoystickID instance_id, int player_index) { + SDL_DriverSwitch2_Context *ctx = (SDL_DriverSwitch2_Context *)device->context; + + if (!ctx->joystick) { + return; + } + + ctx->player_index = player_index; + + UpdateSlotLED(ctx); } static bool HIDAPI_DriverSwitch2_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) { SDL_DriverSwitch2_Context *ctx = (SDL_DriverSwitch2_Context *)device->context; + ctx->joystick = joystick; + + // Initialize player index (needed for setting LEDs) + ctx->player_index = SDL_GetJoystickPlayerIndex(joystick); + ctx->player_lights = SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_PLAYER_LED, true); + UpdateSlotLED(ctx); + + SDL_AddHintCallback(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_PLAYER_LED, + SDL_PlayerLEDHintChanged, ctx); + // Initialize the joystick capabilities switch (device->product_id) { case USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER: @@ -453,7 +500,13 @@ static bool HIDAPI_DriverSwitch2_RumbleJoystickTriggers(SDL_HIDAPI_Device *devic static Uint32 HIDAPI_DriverSwitch2_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) { - return 0; + SDL_DriverSwitch2_Context *ctx = (SDL_DriverSwitch2_Context *)device->context; + Uint32 result = 0; + + if (ctx->player_lights) { + result |= SDL_JOYSTICK_CAP_PLAYER_LED; + } + return result; } static bool HIDAPI_DriverSwitch2_SetJoystickLED(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint8 red, Uint8 green, Uint8 blue) @@ -886,6 +939,12 @@ static bool HIDAPI_DriverSwitch2_UpdateDevice(SDL_HIDAPI_Device *device) static void HIDAPI_DriverSwitch2_CloseJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick) { + SDL_DriverSwitch2_Context *ctx = (SDL_DriverSwitch2_Context *)device->context; + + SDL_RemoveHintCallback(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_PLAYER_LED, + SDL_PlayerLEDHintChanged, ctx); + + ctx->joystick = NULL; } static void HIDAPI_DriverSwitch2_FreeDevice(SDL_HIDAPI_Device *device)