]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
HID: nintendo: add support for HORI Wireless Switch Pad
authorHector Zelaya <hector@hectorzelaya.dev>
Wed, 27 May 2026 16:01:32 +0000 (10:01 -0600)
committerJiri Kosina <jkosina@suse.com>
Wed, 10 Jun 2026 16:31:33 +0000 (18:31 +0200)
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>
drivers/hid/hid-ids.h
drivers/hid/hid-nintendo.c

index 426ff78c1c033d6b9ced1e5790aa6a39807ee309..c508bc3f02dadffa5ff4eabf50da714bd2d68044 100644 (file)
 #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
index 29008c2cc530491660335242fea87f5398ef55c6..2d37ddeffdb6ee3b68eebc9d161fb49af858fce1 100644 (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);