HID: nintendo: add support for HORI Wireless Switch Pad

Add support for the HORI Wireless Switch Pad (vendor 0x0f0d, product
0x00f6), a licensed third-party Nintendo Switch Pro Controller.

The controller reports controller type 0x06 (vs 0x03 for first-party
Pro Controllers) and has the following quirks:

 - SPI flash calibration data is incompatible; use default stick
   calibration values instead.
 - X and Y button bits are swapped compared to first-party controllers;
   add a dedicated button mapping table.
 - Rumble and IMU enable may timeout (no vibration motor in hardware);
   treat as non-fatal for licensed controllers.

Tested over Bluetooth on NixOS with kernel 7.0.5 and 7.0.10:
 - All 14 buttons map correctly
 - Player LED sets on connect
 - Sticks report correctly with default calibration
 - IMU/gyro data streams at 60Hz
 - D-pad reports on ABS_HAT0X/HAT0Y

Device information:
  Bluetooth name: Lic Pro Controller
  Bluetooth HID:  0005:0F0D:00F6

Assisted-by: Kiro:Auto [Amazon Kiro IDE]
Signed-off-by: Hector Zelaya <hector@hectorzelaya.dev>
Reviewed-by: Joshua Peisach <jpeisach@ubuntu.com>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
This commit is contained in:
Hector Zelaya 2026-05-27 10:01:32 -06:00 committed by Jiri Kosina
parent e6f4f084ec
commit 9146038120
2 changed files with 71 additions and 12 deletions

View File

@ -683,6 +683,9 @@
#define USB_DEVICE_ID_HARMONIX_WII_RB3_KEYBOARD 0x3330
#define USB_DEVICE_ID_HARMONIX_WII_RB3_MPA_KEYBOARD_MODE 0x3338
#define USB_VENDOR_ID_HORI 0x0f0d
#define USB_DEVICE_ID_HORI_WIRELESS_SWITCH_PAD 0x00f6
#define USB_VENDOR_ID_HP 0x03f0
#define USB_PRODUCT_ID_HP_ELITE_PRESENTER_MOUSE_464A 0x464a
#define USB_PRODUCT_ID_HP_LOGITECH_OEM_USB_OPTICAL_MOUSE_0A4A 0x0a4a

View File

@ -316,6 +316,7 @@ enum joycon_ctlr_type {
JOYCON_CTLR_TYPE_JCL = 0x01,
JOYCON_CTLR_TYPE_JCR = 0x02,
JOYCON_CTLR_TYPE_PRO = 0x03,
JOYCON_CTLR_TYPE_LIC_PRO = 0x06,
JOYCON_CTLR_TYPE_NESL = 0x09,
JOYCON_CTLR_TYPE_NESR = 0x0A,
JOYCON_CTLR_TYPE_SNES = 0x0B,
@ -433,6 +434,25 @@ static const struct joycon_ctlr_button_mapping procon_button_mappings[] = {
{ /* sentinel */ },
};
/* Licensed Pro Controllers (e.g. HORI) swap X/Y bits in the report */
static const struct joycon_ctlr_button_mapping lic_procon_button_mappings[] = {
{ BTN_EAST, JC_BTN_A, },
{ BTN_SOUTH, JC_BTN_B, },
{ BTN_NORTH, JC_BTN_Y, },
{ BTN_WEST, JC_BTN_X, },
{ BTN_TL, JC_BTN_L, },
{ BTN_TR, JC_BTN_R, },
{ BTN_TL2, JC_BTN_ZL, },
{ BTN_TR2, JC_BTN_ZR, },
{ BTN_SELECT, JC_BTN_MINUS, },
{ BTN_START, JC_BTN_PLUS, },
{ BTN_THUMBL, JC_BTN_LSTICK, },
{ BTN_THUMBR, JC_BTN_RSTICK, },
{ BTN_MODE, JC_BTN_HOME, },
{ BTN_Z, JC_BTN_CAP, },
{ /* sentinel */ },
};
static const struct joycon_ctlr_button_mapping nescon_button_mappings[] = {
{ BTN_SOUTH, JC_BTN_A, },
{ BTN_EAST, JC_BTN_B, },
@ -695,7 +715,8 @@ static inline bool joycon_type_is_right_joycon(struct joycon_ctlr *ctlr)
static inline bool joycon_type_is_procon(struct joycon_ctlr *ctlr)
{
return ctlr->ctlr_type == JOYCON_CTLR_TYPE_PRO;
return ctlr->ctlr_type == JOYCON_CTLR_TYPE_PRO ||
ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO;
}
static inline bool joycon_type_is_snescon(struct joycon_ctlr *ctlr)
@ -1710,7 +1731,10 @@ static void joycon_parse_report(struct joycon_ctlr *ctlr,
joycon_report_left_stick(ctlr, rep);
joycon_report_right_stick(ctlr, rep);
joycon_report_dpad(ctlr, rep);
joycon_report_buttons(ctlr, rep, procon_button_mappings);
if (ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO)
joycon_report_buttons(ctlr, rep, lic_procon_button_mappings);
else
joycon_report_buttons(ctlr, rep, procon_button_mappings);
} else if (joycon_type_is_any_nescon(ctlr)) {
joycon_report_dpad(ctlr, rep);
joycon_report_buttons(ctlr, rep, nescon_button_mappings);
@ -2156,7 +2180,10 @@ static int joycon_input_create(struct joycon_ctlr *ctlr)
joycon_config_left_stick(ctlr->input);
joycon_config_right_stick(ctlr->input);
joycon_config_dpad(ctlr->input);
joycon_config_buttons(ctlr->input, procon_button_mappings);
if (ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO)
joycon_config_buttons(ctlr->input, lic_procon_button_mappings);
else
joycon_config_buttons(ctlr->input, procon_button_mappings);
} else if (joycon_type_is_any_nescon(ctlr)) {
joycon_config_dpad(ctlr->input);
joycon_config_buttons(ctlr->input, nescon_button_mappings);
@ -2503,13 +2530,30 @@ static int joycon_init(struct hid_device *hdev)
if (joycon_has_joysticks(ctlr)) {
/* get controller calibration data, and parse it */
ret = joycon_request_calibration(ctlr);
if (ret) {
if (ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO) {
/*
* We can function with default calibration, but it may be
* inaccurate. Provide a warning, and continue on.
* Licensed controllers may have incompatible SPI flash
* layouts. Use default calibration values.
*/
hid_warn(hdev, "Analog stick positions may be inaccurate\n");
hid_info(hdev, "using default cal for licensed controller\n");
joycon_use_default_calibration(hdev,
&ctlr->left_stick_cal_x,
&ctlr->left_stick_cal_y,
"left", 0);
joycon_use_default_calibration(hdev,
&ctlr->right_stick_cal_x,
&ctlr->right_stick_cal_y,
"right", 0);
} else {
ret = joycon_request_calibration(ctlr);
if (ret) {
/*
* We can function with default calibration, but
* it may be inaccurate. Provide a warning, and
* continue on.
*/
hid_warn(hdev, "Analog stick positions may be inaccurate\n");
}
}
}
@ -2527,8 +2571,13 @@ static int joycon_init(struct hid_device *hdev)
/* Enable the IMU */
ret = joycon_enable_imu(ctlr);
if (ret) {
hid_err(hdev, "Failed to enable the IMU; ret=%d\n", ret);
goto out_unlock;
if (ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO) {
hid_dbg(hdev, "IMU enable failed for licensed controller, continuing\n");
ret = 0;
} else {
hid_err(hdev, "Failed to enable the IMU; ret=%d\n", ret);
goto out_unlock;
}
}
}
@ -2543,8 +2592,13 @@ static int joycon_init(struct hid_device *hdev)
/* Enable rumble */
ret = joycon_enable_rumble(ctlr);
if (ret) {
hid_err(hdev, "Failed to enable rumble; ret=%d\n", ret);
goto out_unlock;
if (ctlr->ctlr_type == JOYCON_CTLR_TYPE_LIC_PRO) {
hid_dbg(hdev, "rumble enable failed for licensed controller, continuing\n");
ret = 0;
} else {
hid_err(hdev, "Failed to enable rumble; ret=%d\n", ret);
goto out_unlock;
}
}
}
@ -2813,6 +2867,8 @@ static const struct hid_device_id nintendo_hid_devices[] = {
USB_DEVICE_ID_NINTENDO_GENCON) },
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_NINTENDO,
USB_DEVICE_ID_NINTENDO_N64CON) },
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_HORI,
USB_DEVICE_ID_HORI_WIRELESS_SWITCH_PAD) },
{ }
};
MODULE_DEVICE_TABLE(hid, nintendo_hid_devices);