[PATCH] HID: nintendo: add support for HORI Wireless Switch Pad

From: Hector Zelaya

Date: Tue May 26 2026 - 13:24:15 EST


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@xxxxxxxxxxxxxxxx>
---
drivers/hid/hid-ids.h | 3 ++
drivers/hid/hid-nintendo.c | 78 +++++++++++++++++++++++++++++++++++++++-------
2 files changed, 69 insertions(+), 12 deletions(-)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index a1cfa436344a..3b0767cc47fd 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -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
diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index 29008c2cc530..b5e799ace249 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -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,12 @@ 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");
+ } else {
+ hid_err(hdev, "Failed to enable the IMU; ret=%d\n", ret);
+ goto out_unlock;
+ }
}
}

@@ -2543,8 +2591,12 @@ 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");
+ } else {
+ hid_err(hdev, "Failed to enable rumble; ret=%d\n", ret);
+ goto out_unlock;
+ }
}
}

@@ -2813,6 +2865,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);

---
base-commit: e71bac24ec1f517f399a9eb471255b8f1c330b93
change-id: 20260526-hori-support-08b08bca40d8

Best regards,
--
Hector Zelaya <hector@xxxxxxxxxxxxxxxx>