diff --git a/MAINTAINERS b/MAINTAINERS index 627595e245f3..e52412894812 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19364,6 +19364,7 @@ F: drivers/scsi/nsp32* NINTENDO HID DRIVER M: Daniel J. Ogorchock +M: Vicki Pfau L: linux-input@vger.kernel.org S: Maintained F: drivers/hid/hid-nintendo* diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index a81bf51cbcf1..d1d0e24f1ac6 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -869,10 +869,13 @@ config HID_NINTENDO depends on LEDS_CLASS select POWER_SUPPLY help - Adds support for the Nintendo Switch Joy-Cons, NSO, Pro Controller. - All controllers support bluetooth, and the Pro Controller also supports - its USB mode. This also includes support for the Nintendo Switch Online - Controllers which include the NES, Genesis, SNES, and N64 controllers. + Adds support for the Nintendo Switch Joy-Cons, NSO, Pro Controller, as + well as Nintendo Switch 2 Joy-Cons, Pro Controller, and NSO GameCube + controllers. All Switch controllers support bluetooth, and the Pro + Controller also supports its USB mode. This also includes support for + the Nintendo Switch Online Controllers which include the NES, Genesis, + SNES, and N64 controllers. Switch 2 controllers currently only support + USB mode. To compile this driver as a module, choose M here: the module will be called hid-nintendo. @@ -887,6 +890,17 @@ config NINTENDO_FF controller, and the NSO N64 controller. For the pro controller, both rumble motors can be controlled individually. +config NINTENDO_SWITCH2_USB + tristate "Wired Nintendo Switch 2 controller support" + depends on HID_NINTENDO + depends on USB + help + Say Y here if you want to enable support for wired Nintendo Switch 2 + controllers. + + To compile this driver as a module, choose M here: the + module will be called hid-nintendo-switch2-usb. + config HID_NTI tristate "NTI keyboard adapters" help diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 48a863b245ee..35b2f86a26e2 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -96,6 +96,7 @@ obj-$(CONFIG_HID_MONTEREY) += hid-monterey.o obj-$(CONFIG_HID_MSI) += hid-msi.o obj-$(CONFIG_HID_MULTITOUCH) += hid-multitouch.o obj-$(CONFIG_HID_NINTENDO) += hid-nintendo.o +obj-$(CONFIG_NINTENDO_SWITCH2_USB) += hid-nintendo-switch2-usb.o obj-$(CONFIG_HID_NTI) += hid-nti.o obj-$(CONFIG_HID_NTRIG) += hid-ntrig.o obj-$(CONFIG_HID_NVIDIA_SHIELD) += hid-nvidia-shield.o diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 7f6c786535ea..98fc6abaf33d 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1116,6 +1116,10 @@ #define USB_DEVICE_ID_NINTENDO_SNESCON 0x2017 #define USB_DEVICE_ID_NINTENDO_GENCON 0x201e #define USB_DEVICE_ID_NINTENDO_N64CON 0x2019 +#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONR 0x2066 +#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONL 0x2067 +#define USB_DEVICE_ID_NINTENDO_NS2_PROCON 0x2069 +#define USB_DEVICE_ID_NINTENDO_NS2_GCCON 0x2073 #define USB_VENDOR_ID_NOVATEK 0x0603 #define USB_DEVICE_ID_NOVATEK_PCT 0x0600 diff --git a/drivers/hid/hid-nintendo-switch2-usb.c b/drivers/hid/hid-nintendo-switch2-usb.c new file mode 100644 index 000000000000..8b3b6841035d --- /dev/null +++ b/drivers/hid/hid-nintendo-switch2-usb.c @@ -0,0 +1,493 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * USB driver for Nintendo Switch 2 controllers configuration interface + * + * Copyright (c) 2025 Valve Software + * + * This driver is based on the following work: + * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64 + * https://github.com/ndeadly/switch2_controller_research + */ + +#include "hid-ids.h" +#include "hid-nintendo.h" +#include +#include + +#define NS2_BULK_SIZE 64 +#define NS2_IN_URBS 2 +#define NS2_OUT_URBS 4 + +static struct usb_driver switch2_usb; + +enum switch2_urb_state { + NS2_URB_FREE, + NS2_URB_OUT, + NS2_URB_IN, +}; + +struct switch2_urb { + struct urb *urb; + uint8_t *data; + enum switch2_urb_state state; +}; + +struct switch2_usb { + struct switch2_cfg_intf cfg; + struct usb_device *udev; + + struct switch2_urb bulk_in[NS2_IN_URBS]; + struct usb_anchor bulk_in_anchor; + bool shutdown; + spinlock_t bulk_in_lock; + + struct switch2_urb bulk_out[NS2_OUT_URBS]; + struct usb_anchor bulk_out_anchor; + spinlock_t bulk_out_lock; + + struct work_struct message_in_work; +}; + +static void switch2_bulk_in(struct urb *urb) +{ + struct switch2_usb *ns2_usb = urb->context; + int i; + bool schedule = false; + unsigned long flags; + + switch (urb->status) { + case 0: + schedule = true; + break; + case -ECONNRESET: + case -ENOENT: + case -ESHUTDOWN: + case -EPIPE: + dev_dbg(&ns2_usb->udev->dev, "shutting down input urb: %d\n", urb->status); + return; + default: + dev_dbg(&ns2_usb->udev->dev, "unknown input urb status: %d\n", urb->status); + break; + } + + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags); + if (ns2_usb->shutdown) + schedule = false; + + for (i = 0; i < NS2_IN_URBS; i++) { + int err; + struct switch2_urb *ns2_urb; + + if (ns2_usb->bulk_in[i].urb == urb) { + if (schedule) { + ns2_usb->bulk_in[i].state = NS2_URB_IN; + continue; + } else { + ns2_usb->bulk_in[i].state = NS2_URB_FREE; + } + } + + if (ns2_usb->bulk_in[i].state != NS2_URB_FREE) + continue; + + if (ns2_usb->shutdown) + continue; + + /* + * We want exactly one bulk in URB scheduled at a time, so only + * reschedule this immediately if nothing else is scheduled + * currently. + */ + if (!usb_anchor_empty(&ns2_usb->bulk_in_anchor)) + continue; + + ns2_urb = &ns2_usb->bulk_in[i]; + if (!ns2_urb) + continue; + + usb_anchor_urb(ns2_urb->urb, &ns2_usb->bulk_in_anchor); + err = usb_submit_urb(ns2_urb->urb, GFP_ATOMIC); + if (err) { + usb_unanchor_urb(ns2_urb->urb); + dev_dbg(&ns2_usb->udev->dev, "failed to queue input urb: %d\n", err); + } else { + ns2_urb->state = NS2_URB_OUT; + } + } + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags); + + if (schedule) + schedule_work(&ns2_usb->message_in_work); +} + +static void switch2_bulk_out(struct urb *urb) +{ + struct switch2_usb *ns2_usb = urb->context; + int i; + + guard(spinlock_irqsave)(&ns2_usb->bulk_out_lock); + + switch (urb->status) { + case 0: + break; + case -ECONNRESET: + case -ENOENT: + case -ESHUTDOWN: + dev_dbg(&ns2_usb->udev->dev, "shutting down output urb: %d\n", urb->status); + return; + case -EPIPE: + break; + default: + dev_dbg(&ns2_usb->udev->dev, "unknown output urb status: %d\n", urb->status); + break; + } + + for (i = 0; i < NS2_OUT_URBS; i++) { + if (ns2_usb->bulk_out[i].urb != urb) + continue; + + ns2_usb->bulk_out[i].state = NS2_URB_FREE; + break; + } +} + +static int switch2_usb_send_cmd(enum switch2_cmd command, uint8_t subcommand, + const void *message, size_t size, struct switch2_cfg_intf *cfg) +{ + struct switch2_usb *ns2_usb = (struct switch2_usb *)cfg; + struct switch2_urb *urb = NULL; + int i; + int ret; + unsigned long flags; + + struct switch2_cmd_header header = { + command, NS2_DIR_OUT | NS2_FLAG_OK, NS2_TRANS_USB, subcommand, 0, size + }; + + if (WARN_ON(size > 56)) + return -EINVAL; + + spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags); + for (i = 0; i < NS2_OUT_URBS; i++) { + if (ns2_usb->bulk_out[i].state != NS2_URB_FREE) + continue; + + urb = &ns2_usb->bulk_out[i]; + urb->state = NS2_URB_OUT; + break; + } + spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags); + + if (!urb) { + dev_warn(&ns2_usb->udev->dev, "output queue full, dropping message\n"); + return -ENOBUFS; + } + + memcpy(urb->data, &header, sizeof(header)); + if (message && size) + memcpy(&urb->data[8], message, size); + urb->urb->transfer_buffer_length = size + sizeof(header); + + print_hex_dump_debug("sending cmd: ", DUMP_PREFIX_OFFSET, 16, 1, urb->data, + size + sizeof(header), false); + + usb_anchor_urb(urb->urb, &ns2_usb->bulk_out_anchor); + ret = usb_submit_urb(urb->urb, GFP_KERNEL); + if (ret) { + if (ret != -ENODEV) + dev_warn(&ns2_usb->udev->dev, "failed to submit output urb: %i", ret); + spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags); + urb->state = NS2_URB_FREE; + spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags); + usb_unanchor_urb(urb->urb); + return ret; + } + + return 0; +} + +static void switch2_usb_free_in_urbs(struct switch2_usb *ns2_usb) +{ + struct urb *urb; + uint8_t *data; + int i; + unsigned long flags; + + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags); + ns2_usb->shutdown = true; + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags); + + cancel_work_sync(&ns2_usb->message_in_work); + + usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor); + for (i = 0; i < NS2_IN_URBS; i++) { + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags); + urb = ns2_usb->bulk_in[i].urb; + data = ns2_usb->bulk_in[i].data; + if (!urb) { + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags); + continue; + } + + ns2_usb->bulk_in[i].urb = NULL; + ns2_usb->bulk_in[i].data = NULL; + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags); + + usb_kill_urb(urb); + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma); + usb_free_urb(urb); + } +} + +static void switch2_usb_free_out_urbs(struct switch2_usb *ns2_usb) +{ + struct urb *urb; + uint8_t *data; + int i; + unsigned long flags; + + usb_kill_anchored_urbs(&ns2_usb->bulk_out_anchor); + for (i = 0; i < NS2_OUT_URBS; i++) { + spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags); + urb = ns2_usb->bulk_out[i].urb; + data = ns2_usb->bulk_out[i].data; + if (!urb) { + spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags); + continue; + } + + ns2_usb->bulk_out[i].urb = NULL; + ns2_usb->bulk_out[i].data = NULL; + spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags); + + usb_kill_urb(urb); + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma); + usb_free_urb(urb); + } +} + +static void switch2_usb_shutdown(struct switch2_cfg_intf *intf) +{ + struct switch2_usb *ns2_usb = (struct switch2_usb *)intf; + int i; + unsigned long flags; + + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags); + ns2_usb->shutdown = true; + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags); + + cancel_work_sync(&ns2_usb->message_in_work); + + usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor); + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags); + for (i = 0; i < NS2_IN_URBS; i++) + ns2_usb->bulk_in[i].state = NS2_URB_FREE; + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags); +} + +static void switch2_usb_message_in_work(struct work_struct *work) +{ + struct switch2_usb *ns2_usb = container_of(work, struct switch2_usb, message_in_work); + struct switch2_urb *urb; + int err; + int i; + unsigned long flags; + + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags); + for (i = 0; i < NS2_IN_URBS; i++) { + urb = &ns2_usb->bulk_in[i]; + if (urb->state != NS2_URB_IN) + continue; + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags); + + if (ns2_usb->cfg.parent) { + err = switch2_receive_command(ns2_usb->cfg.parent, + urb->urb->transfer_buffer, urb->urb->actual_length); + if (err) + dev_dbg(&ns2_usb->udev->dev, "receive command failed: %d\n", err); + } else { + dev_err(&ns2_usb->udev->dev, + "Got message before controller is fully set up; discarding\n"); + } + + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags); + if (!urb->urb) + /* We're shutting down and this URB was freed */ + break; + + urb->state = NS2_URB_FREE; + /* + * We want exactly one bulk in URB scheduled at a time, so only + * reschedule this immediately if nothing else is scheduled + * currently. + */ + if (!usb_anchor_empty(&ns2_usb->bulk_in_anchor)) + continue; + + usb_anchor_urb(urb->urb, &ns2_usb->bulk_in_anchor); + err = usb_submit_urb(urb->urb, GFP_ATOMIC); + if (err) { + usb_unanchor_urb(urb->urb); + dev_dbg(&ns2_usb->udev->dev, + "failed to queue input urb: %d\n", err); + } else { + urb->state = NS2_URB_OUT; + } + } + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags); +} + +static int switch2_usb_probe(struct usb_interface *intf, const struct usb_device_id *id) +{ + struct switch2_usb *ns2_usb; + struct usb_device *udev; + struct usb_endpoint_descriptor *bulk_in, *bulk_out; + struct urb *urb; + uint8_t *data; + char phys[64]; + int ret; + int i; + unsigned long flags; + + udev = interface_to_usbdev(intf); + if (usb_make_path(udev, phys, sizeof(phys)) < 0) + return -EINVAL; + + ret = usb_find_common_endpoints(intf->cur_altsetting, &bulk_in, &bulk_out, NULL, NULL); + if (ret) { + dev_err(&intf->dev, "failed to find bulk EPs\n"); + return ret; + } + + ns2_usb = devm_kzalloc(&intf->dev, sizeof(*ns2_usb), GFP_KERNEL); + if (!ns2_usb) + return -ENOMEM; + + init_usb_anchor(&ns2_usb->bulk_out_anchor); + spin_lock_init(&ns2_usb->bulk_out_lock); + init_usb_anchor(&ns2_usb->bulk_in_anchor); + spin_lock_init(&ns2_usb->bulk_in_lock); + INIT_WORK(&ns2_usb->message_in_work, switch2_usb_message_in_work); + + ns2_usb->udev = udev; + for (i = 0; i < NS2_IN_URBS; i++) { + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) { + ret = -ENOMEM; + goto err_free_in; + } + + data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL, + &urb->transfer_dma); + if (!data) { + usb_free_urb(urb); + ret = -ENOMEM; + goto err_free_in; + } + + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags); + usb_fill_bulk_urb(urb, udev, + usb_rcvbulkpipe(udev, bulk_in->bEndpointAddress), + data, NS2_BULK_SIZE, switch2_bulk_in, ns2_usb); + urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + ns2_usb->bulk_in[i].urb = urb; + ns2_usb->bulk_in[i].data = data; + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags); + } + + for (i = 0; i < NS2_OUT_URBS; i++) { + urb = usb_alloc_urb(0, GFP_KERNEL); + if (!urb) { + ret = -ENOMEM; + goto err_free_out; + } + + data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL, + &urb->transfer_dma); + if (!data) { + usb_free_urb(urb); + ret = -ENOMEM; + goto err_free_out; + } + + spin_lock_irqsave(&ns2_usb->bulk_out_lock, flags); + usb_fill_bulk_urb(urb, udev, + usb_sndbulkpipe(udev, bulk_out->bEndpointAddress), + data, NS2_BULK_SIZE, switch2_bulk_out, ns2_usb); + urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; + + ns2_usb->bulk_out[i].urb = urb; + ns2_usb->bulk_out[i].data = data; + spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags); + } + + usb_set_intfdata(intf, ns2_usb); + + ns2_usb->cfg.dev = &ns2_usb->udev->dev; + ns2_usb->cfg.send_command = switch2_usb_send_cmd; + + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags); + ns2_usb->bulk_in[0].state = NS2_URB_OUT; + usb_anchor_urb(ns2_usb->bulk_in[0].urb, &ns2_usb->bulk_in_anchor); + ret = usb_submit_urb(ns2_usb->bulk_in[0].urb, GFP_ATOMIC); + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags); + + if (ret < 0) + goto err_free_out; + + ns2_usb->cfg.shutdown = switch2_usb_shutdown; + ret = switch2_controller_attach_cfg(phys, &ns2_usb->cfg); + if (ret < 0) + goto err_free_out; + + return 0; + +err_free_out: + switch2_usb_free_out_urbs(ns2_usb); +err_free_in: + switch2_usb_free_in_urbs(ns2_usb); + devm_kfree(&intf->dev, ns2_usb); + + return ret; +} + +static void switch2_usb_disconnect(struct usb_interface *intf) +{ + struct switch2_usb *ns2_usb = usb_get_intfdata(intf); + + switch2_usb_free_in_urbs(ns2_usb); + + /* + * We need to detach *before* we kill the out URBs to make sure no + * further URBs get scheduled by the HID endpoint in the meantime. + */ + if (ns2_usb->cfg.parent) + switch2_controller_detach_cfg(ns2_usb->cfg.parent); + + switch2_usb_free_out_urbs(ns2_usb); +} + +#define SWITCH2_CONTROLLER(vend, prod) \ + USB_DEVICE_AND_INTERFACE_INFO(vend, prod, USB_CLASS_VENDOR_SPEC, 0, 0) + +static const struct usb_device_id switch2_usb_devices[] = { + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) }, + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) }, + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_PROCON) }, + { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_GCCON) }, + { } +}; +MODULE_DEVICE_TABLE(usb, switch2_usb_devices); + +static struct usb_driver switch2_usb = { + .name = "nintendo-switch2", + .id_table = switch2_usb_devices, + .probe = switch2_usb_probe, + .disconnect = switch2_usb_disconnect, +}; +module_usb_driver(switch2_usb); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Vicki Pfau "); +MODULE_DESCRIPTION("Driver for Nintendo Switch 2 Controllers"); diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c index b1fcf3e5767b..c31ded267be7 100644 --- a/drivers/hid/hid-nintendo.c +++ b/drivers/hid/hid-nintendo.c @@ -1,11 +1,13 @@ // SPDX-License-Identifier: GPL-2.0+ /* - * HID driver for Nintendo Switch Joy-Cons and Pro Controllers + * HID driver for Nintendo Switch Joy-Cons and Pro Controllers, as well as + * Nintendo Switch 2 Joy-Cons, Pro Controller, and GameCube Controller * * Copyright (c) 2019-2021 Daniel J. Ogorchock * Portions Copyright (c) 2020 Nadia Holmquist Pedersen * Copyright (c) 2022 Emily Strickland * Copyright (c) 2023 Ryan McClelland + * Copyright (c) 2026 Valve Software * * The following resources/projects were referenced for this driver: * https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering @@ -13,6 +15,8 @@ * https://github.com/FrotBot/SwitchProConLinuxUSB * https://github.com/MTCKC/ProconXInput * https://github.com/Davidobot/BetterJoyForCemu + * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64 + * https://github.com/ndeadly/switch2_controller_research * hid-wiimote kernel hid driver * hid-logitech-hidpp driver * hid-sony driver @@ -29,6 +33,7 @@ */ #include "hid-ids.h" +#include "hid-nintendo.h" #include #include #include @@ -41,6 +46,8 @@ #include #include #include +#include +#include "usbhid/usbhid.h" /* * Reference the url below for the following HID report defines: @@ -2703,7 +2710,7 @@ static int joycon_ctlr_handle_event(struct joycon_ctlr *ctlr, u8 *data, return ret; } -static int nintendo_hid_event(struct hid_device *hdev, +static int joycon_event(struct hid_device *hdev, struct hid_report *report, u8 *raw_data, int size) { struct joycon_ctlr *ctlr = hid_get_drvdata(hdev); @@ -2714,7 +2721,7 @@ static int nintendo_hid_event(struct hid_device *hdev, return joycon_ctlr_handle_event(ctlr, raw_data, size); } -static int nintendo_hid_probe(struct hid_device *hdev, +static int joycon_probe(struct hid_device *hdev, const struct hid_device_id *id) { int ret; @@ -2819,7 +2826,7 @@ static int nintendo_hid_probe(struct hid_device *hdev, return ret; } -static void nintendo_hid_remove(struct hid_device *hdev) +static void joycon_remove(struct hid_device *hdev) { struct joycon_ctlr *ctlr = hid_get_drvdata(hdev); unsigned long flags; @@ -2838,7 +2845,7 @@ static void nintendo_hid_remove(struct hid_device *hdev) hid_hw_stop(hdev); } -static int nintendo_hid_resume(struct hid_device *hdev) +static int joycon_resume(struct hid_device *hdev) { struct joycon_ctlr *ctlr = hid_get_drvdata(hdev); int ret; @@ -2861,7 +2868,7 @@ static int nintendo_hid_resume(struct hid_device *hdev) return ret; } -static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message) +static int joycon_suspend(struct hid_device *hdev, pm_message_t message) { struct joycon_ctlr *ctlr = hid_get_drvdata(hdev); @@ -2880,7 +2887,1210 @@ static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message) return 0; } +/* + * ============================================================================= + * Switch 2 support + * ============================================================================= + */ +#define NS2_BTNR_B BIT(0) +#define NS2_BTNR_A BIT(1) +#define NS2_BTNR_Y BIT(2) +#define NS2_BTNR_X BIT(3) +#define NS2_BTNR_R BIT(4) +#define NS2_BTNR_ZR BIT(5) +#define NS2_BTNR_PLUS BIT(6) +#define NS2_BTNR_RS BIT(7) + +#define NS2_BTNL_DOWN BIT(0) +#define NS2_BTNL_RIGHT BIT(1) +#define NS2_BTNL_LEFT BIT(2) +#define NS2_BTNL_UP BIT(3) +#define NS2_BTNL_L BIT(4) +#define NS2_BTNL_ZL BIT(5) +#define NS2_BTNL_MINUS BIT(6) +#define NS2_BTNL_LS BIT(7) + +#define NS2_BTN3_C BIT(4) +#define NS2_BTN3_SR BIT(6) +#define NS2_BTN3_SL BIT(7) + +#define NS2_BTN_JCR_HOME BIT(0) +#define NS2_BTN_JCR_GR BIT(2) +#define NS2_BTN_JCR_C NS2_BTN3_C +#define NS2_BTN_JCR_SR NS2_BTN3_SR +#define NS2_BTN_JCR_SL NS2_BTN3_SL + +#define NS2_BTN_JCL_CAPTURE BIT(0) +#define NS2_BTN_JCL_GL BIT(2) +#define NS2_BTN_JCL_SR NS2_BTN3_SR +#define NS2_BTN_JCL_SL NS2_BTN3_SL + +#define NS2_BTN_PRO_HOME BIT(0) +#define NS2_BTN_PRO_CAPTURE BIT(1) +#define NS2_BTN_PRO_GR BIT(2) +#define NS2_BTN_PRO_GL BIT(3) +#define NS2_BTN_PRO_C NS2_BTN3_C + +#define NS2_BTN_GC_HOME BIT(0) +#define NS2_BTN_GC_CAPTURE BIT(1) +#define NS2_BTN_GC_C NS2_BTN3_C + +#define NS2_TRIGGER_RANGE 4095 +#define NS2_AXIS_MIN -32768 +#define NS2_AXIS_MAX 32767 + +#define NS2_MAX_PLAYER_ID 8 + +#define NS2_MAX_INIT_RETRIES 4 + +#define NS2_FLASH_ADDR_SERIAL 0x13002 +#define NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB 0x130a8 +#define NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB 0x130e8 +#define NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB 0x13140 +#define NS2_FLASH_ADDR_USER_PRIMARY_CALIB 0x1fc040 +#define NS2_FLASH_ADDR_USER_SECONDARY_CALIB 0x1fc080 + +#define NS2_FLASH_SIZE_SERIAL 0x10 +#define NS2_FLASH_SIZE_FACTORY_AXIS_CALIB 9 +#define NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB 2 +#define NS2_FLASH_SIZE_USER_AXIS_CALIB 11 + +#define NS2_USER_CALIB_MAGIC 0xa1b2 + +#define NS2_FEATURE_BUTTONS BIT(0) +#define NS2_FEATURE_ANALOG BIT(1) +#define NS2_FEATURE_IMU BIT(2) +#define NS2_FEATURE_MOUSE BIT(4) +#define NS2_FEATURE_RUMBLE BIT(5) +#define NS2_FEATURE_MAGNETO BIT(7) + +enum switch2_subcmd_flash { + NS2_SUBCMD_FLASH_READ_BLOCK = 0x01, + NS2_SUBCMD_FLASH_WRITE_BLOCK = 0x02, + NS2_SUBCMD_FLASH_ERASE_BLOCK = 0x03, + NS2_SUBCMD_FLASH_READ = 0x04, + NS2_SUBCMD_FLASH_WRITE = 0x05, +}; + +enum switch2_subcmd_init { + NS2_SUBCMD_INIT_SELECT_REPORT = 0xa, + NS2_SUBCMD_INIT_USB = 0xd, +}; + +enum switch2_subcmd_feature_select { + NS2_SUBCMD_FEATSEL_GET_INFO = 0x1, + NS2_SUBCMD_FEATSEL_SET_MASK = 0x2, + NS2_SUBCMD_FEATSEL_CLEAR_MASK = 0x3, + NS2_SUBCMD_FEATSEL_ENABLE = 0x4, + NS2_SUBCMD_FEATSEL_DISABLE = 0x5, +}; + +enum switch2_subcmd_grip { + NS2_SUBCMD_GRIP_GET_INFO = 0x1, + NS2_SUBCMD_GRIP_ENABLE_BUTTONS = 0x2, + NS2_SUBCMD_GRIP_GET_INFO_EXT = 0x3, +}; + +enum switch2_subcmd_led { + NS2_SUBCMD_LED_P1 = 0x1, + NS2_SUBCMD_LED_P2 = 0x2, + NS2_SUBCMD_LED_P3 = 0x3, + NS2_SUBCMD_LED_P4 = 0x4, + NS2_SUBCMD_LED_ALL_ON = 0x5, + NS2_SUBCMD_LED_ALL_OFF = 0x6, + NS2_SUBCMD_LED_PATTERN = 0x7, + NS2_SUBCMD_LED_BLINK = 0x8, +}; + +enum switch2_subcmd_fw_info { + NS2_SUBCMD_FW_INFO_GET = 0x1, +}; + +enum switch2_ctlr_type { + NS2_CTLR_TYPE_JCL = 0x00, + NS2_CTLR_TYPE_JCR = 0x01, + NS2_CTLR_TYPE_PRO = 0x02, + NS2_CTLR_TYPE_GC = 0x03, +}; + +enum switch2_report_id { + NS2_REPORT_UNIFIED = 0x05, + NS2_REPORT_JCL = 0x07, + NS2_REPORT_JCR = 0x08, + NS2_REPORT_PRO = 0x09, + NS2_REPORT_GC = 0x0a, +}; + +enum switch2_init_step { + NS2_INIT_READ_SERIAL, + NS2_INIT_GET_FIRMWARE_INFO, + NS2_INIT_READ_FACTORY_PRIMARY_CALIB, + NS2_INIT_READ_FACTORY_SECONDARY_CALIB, + NS2_INIT_READ_FACTORY_TRIGGER_CALIB, + NS2_INIT_READ_USER_PRIMARY_CALIB, + NS2_INIT_READ_USER_SECONDARY_CALIB, + NS2_INIT_SET_FEATURE_MASK, + NS2_INIT_ENABLE_FEATURES, + NS2_INIT_GRIP_BUTTONS, + NS2_INIT_REPORT_FORMAT, + NS2_INIT_INPUT, + NS2_INIT_SET_PLAYER_LEDS, + NS2_INIT_FINISH, + NS2_INIT_DONE, +}; + +struct switch2_version_info { + uint8_t major; + uint8_t minor; + uint8_t patch; + uint8_t ctlr_type; + __le32 unk; + int8_t dsp_major; + int8_t dsp_minor; + int8_t dsp_patch; + int8_t dsp_type; +}; + +struct switch2_axis_calibration { + uint16_t neutral; + uint16_t negative; + uint16_t positive; +}; + +struct switch2_stick_calibration { + struct switch2_axis_calibration x; + struct switch2_axis_calibration y; +}; + +struct switch2_controller { + struct hid_device *hdev; + struct switch2_cfg_intf *cfg; + struct kref refcount; + + char name[64]; + char phys[64]; + struct list_head entry; + struct mutex lock; + + enum switch2_ctlr_type ctlr_type; + enum switch2_init_step init_step; + int init_retries; + struct input_dev __rcu *input; + char serial[NS2_FLASH_SIZE_SERIAL + 1]; + struct switch2_version_info version; + + struct switch2_stick_calibration stick_calib[2]; + uint8_t lt_zero; + uint8_t rt_zero; + + uint32_t player_id; + struct led_classdev *leds; +}; + +static DEFINE_MUTEX(switch2_controllers_lock); +static LIST_HEAD(switch2_controllers); + +struct switch2_ctlr_button_mapping { + uint32_t code; + int byte; + uint32_t bit; +}; + +static const struct switch2_ctlr_button_mapping ns2_left_joycon_button_mappings[] = { + { BTN_DPAD_LEFT, 0, NS2_BTNL_LEFT, }, + { BTN_DPAD_UP, 0, NS2_BTNL_UP, }, + { BTN_DPAD_DOWN, 0, NS2_BTNL_DOWN, }, + { BTN_DPAD_RIGHT, 0, NS2_BTNL_RIGHT, }, + { BTN_TL, 0, NS2_BTNL_L, }, + { BTN_TL2, 0, NS2_BTNL_ZL, }, + { BTN_SELECT, 0, NS2_BTNL_MINUS, }, + { BTN_THUMBL, 0, NS2_BTNL_LS, }, + { KEY_RECORD, 1, NS2_BTN_JCL_CAPTURE, }, + { BTN_GRIPR, 1, NS2_BTN_JCL_SL, }, + { BTN_GRIPR2, 1, NS2_BTN_JCL_SR, }, + { BTN_GRIPL, 1, NS2_BTN_JCL_GL, }, + { /* sentinel */ }, +}; + +static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_mappings[] = { + { BTN_SOUTH, 0, NS2_BTNR_A, }, + { BTN_EAST, 0, NS2_BTNR_B, }, + { BTN_NORTH, 0, NS2_BTNR_X, }, + { BTN_WEST, 0, NS2_BTNR_Y, }, + { BTN_TR, 0, NS2_BTNR_R, }, + { BTN_TR2, 0, NS2_BTNR_ZR, }, + { BTN_START, 0, NS2_BTNR_PLUS, }, + { BTN_THUMBR, 0, NS2_BTNR_RS, }, + { BTN_C, 1, NS2_BTN_JCR_C, }, + { BTN_MODE, 1, NS2_BTN_JCR_HOME, }, + { BTN_GRIPL2, 1, NS2_BTN_JCR_SL, }, + { BTN_GRIPL, 1, NS2_BTN_JCR_SR, }, + { BTN_GRIPR, 1, NS2_BTN_JCR_GR, }, + { /* sentinel */ }, +}; + +static const struct switch2_ctlr_button_mapping ns2_procon_mappings[] = { + { BTN_SOUTH, 0, NS2_BTNR_A, }, + { BTN_EAST, 0, NS2_BTNR_B, }, + { BTN_NORTH, 0, NS2_BTNR_X, }, + { BTN_WEST, 0, NS2_BTNR_Y, }, + { BTN_TL, 1, NS2_BTNL_L, }, + { BTN_TR, 0, NS2_BTNR_R, }, + { BTN_TL2, 1, NS2_BTNL_ZL, }, + { BTN_TR2, 0, NS2_BTNR_ZR, }, + { BTN_SELECT, 1, NS2_BTNL_MINUS, }, + { BTN_START, 0, NS2_BTNR_PLUS, }, + { BTN_THUMBL, 1, NS2_BTNL_LS, }, + { BTN_THUMBR, 0, NS2_BTNR_RS, }, + { BTN_MODE, 2, NS2_BTN_PRO_HOME }, + { KEY_RECORD, 2, NS2_BTN_PRO_CAPTURE }, + { BTN_GRIPR, 2, NS2_BTN_PRO_GR }, + { BTN_GRIPL, 2, NS2_BTN_PRO_GL }, + { BTN_C, 2, NS2_BTN_PRO_C }, + { /* sentinel */ }, +}; + +static const struct switch2_ctlr_button_mapping ns2_gccon_mappings[] = { + { BTN_SOUTH, 0, NS2_BTNR_A, }, + { BTN_EAST, 0, NS2_BTNR_B, }, + { BTN_NORTH, 0, NS2_BTNR_X, }, + { BTN_WEST, 0, NS2_BTNR_Y, }, + { BTN_TL2, 1, NS2_BTNL_L, }, + { BTN_TR2, 0, NS2_BTNR_R, }, + { BTN_TL, 1, NS2_BTNL_ZL, }, + { BTN_TR, 0, NS2_BTNR_ZR, }, + { BTN_SELECT, 1, NS2_BTNL_MINUS, }, + { BTN_START, 0, NS2_BTNR_PLUS, }, + { BTN_MODE, 2, NS2_BTN_GC_HOME }, + { KEY_RECORD, 2, NS2_BTN_GC_CAPTURE }, + { BTN_C, 2, NS2_BTN_GC_C }, + { /* sentinel */ }, +}; + +static const uint8_t switch2_init_cmd_data[] = { + /* + * The last 6 bytes of this packet are the MAC address of + * the console, but we don't need that for USB + */ + 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF +}; + +static const uint8_t switch2_one_data[] = { 0x01, 0x00, 0x00, 0x00 }; + +static const uint8_t switch2_feature_mask[] = { + NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU, + 0x00, 0x00, 0x00 +}; + +static int switch2_init_controller(struct switch2_controller *ns2); + +static void switch2_init_step_done(struct switch2_controller *ns2, enum switch2_init_step step) +{ + if (ns2->init_step != step) + return; + + ns2->init_retries = 0; + ns2->init_step++; +} + +static inline bool switch2_ctlr_is_joycon(enum switch2_ctlr_type type) +{ + return type == NS2_CTLR_TYPE_JCL || type == NS2_CTLR_TYPE_JCR; +} + +static struct switch2_controller *switch2_get_controller(const char *phys) +{ + struct switch2_controller *ns2; + + guard(mutex)(&switch2_controllers_lock); + list_for_each_entry(ns2, &switch2_controllers, entry) { + if (strncmp(ns2->phys, phys, sizeof(ns2->phys)) == 0) { + if (kref_get_unless_zero(&ns2->refcount)) + return ns2; + } + } + ns2 = kzalloc_obj(*ns2); + if (!ns2) + return ERR_PTR(-ENOMEM); + + kref_init(&ns2->refcount); + mutex_init(&ns2->lock); + INIT_LIST_HEAD(&ns2->entry); + list_add(&ns2->entry, &switch2_controllers); + strscpy(ns2->phys, phys, sizeof(ns2->phys)); + return ns2; +} + +static void switch2_controller_put(struct switch2_controller *ns2) +{ + struct input_dev *input; + + mutex_lock(&ns2->lock); + rcu_read_lock(); + input = rcu_dereference(ns2->input); + rcu_read_unlock(); + + rcu_assign_pointer(ns2->input, NULL); + synchronize_rcu(); + + ns2->init_step = 0; + mutex_unlock(&ns2->lock); + + if (input) + input_unregister_device(input); +} + +static void switch2_kref_put(struct kref *refcount) +{ + struct switch2_controller *ns2 = container_of(refcount, + struct switch2_controller, refcount); + + guard(mutex)(&switch2_controllers_lock); + list_del_init(&ns2->entry); + mutex_destroy(&ns2->lock); + kfree(ns2); +} + +static int switch2_set_leds(struct switch2_controller *ns2) +{ + int i; + uint8_t message[8] = { 0 }; + + for (i = 0; i < JC_NUM_LEDS; i++) + message[0] |= (!!ns2->leds[i].brightness) << i; + + if (!ns2->cfg) + return -ENOTCONN; + return ns2->cfg->send_command(NS2_CMD_LED, NS2_SUBCMD_LED_PATTERN, + &message, sizeof(message), + ns2->cfg); +} + +static int switch2_player_led_brightness_set(struct led_classdev *led, + enum led_brightness brightness) +{ + struct device *dev = led->dev->parent; + struct input_dev *input = to_input_dev(dev); + struct switch2_controller *ns2 = input_get_drvdata(input); + + if (!ns2) + return -ENODEV; + + guard(mutex)(&ns2->lock); + return switch2_set_leds(ns2); +} + +static void switch2_config_buttons(struct input_dev *idev, + const struct switch2_ctlr_button_mapping button_mappings[]) +{ + const struct switch2_ctlr_button_mapping *button; + + for (button = button_mappings; button->code; button++) + input_set_capability(idev, EV_KEY, button->code); +} + +static int switch2_input_ref(struct input_dev *input) +{ + struct switch2_controller *ns2 = input_get_drvdata(input); + + kref_get(&ns2->refcount); + + return 0; +} + +static void switch2_input_deref(struct input_dev *input) +{ + struct switch2_controller *ns2 = input_get_drvdata(input); + + kref_put(&ns2->refcount, switch2_kref_put); +} + +static int switch2_init_input(struct switch2_controller *ns2) +{ + struct input_dev *input; + struct hid_device *hdev = ns2->hdev; + int player_led_pattern; + int i; + int ret; + + rcu_read_lock(); + input = rcu_dereference(ns2->input); + rcu_read_unlock(); + + if (input) { + switch2_init_step_done(ns2, NS2_INIT_INPUT); + return 0; + } + + input = input_allocate_device(); + if (!input) + return -ENOMEM; + + input_set_drvdata(input, ns2); + input->open = switch2_input_ref; + input->close = switch2_input_deref; + input->dev.parent = &hdev->dev; + input->id.bustype = hdev->bus; + input->id.vendor = hdev->vendor; + input->id.product = hdev->product; + input->id.version = hdev->version; + input->uniq = ns2->serial; + input->name = ns2->name; + input->phys = hdev->phys; + + switch (ns2->ctlr_type) { + case NS2_CTLR_TYPE_JCL: + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + switch2_config_buttons(input, ns2_left_joycon_button_mappings); + break; + case NS2_CTLR_TYPE_JCR: + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + switch2_config_buttons(input, ns2_right_joycon_button_mappings); + break; + case NS2_CTLR_TYPE_GC: + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + input_set_abs_params(input, ABS_Z, 0, NS2_TRIGGER_RANGE, 32, 128); + input_set_abs_params(input, ABS_RZ, 0, NS2_TRIGGER_RANGE, 32, 128); + input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0); + input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0); + switch2_config_buttons(input, ns2_gccon_mappings); + break; + case NS2_CTLR_TYPE_PRO: + input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128); + input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0); + input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0); + switch2_config_buttons(input, ns2_procon_mappings); + break; + default: + input_free_device(input); + return -EINVAL; + } + + hid_info(ns2->hdev, "Firmware version %u.%u.%u (type %i)\n", ns2->version.major, + ns2->version.minor, ns2->version.patch, ns2->version.ctlr_type); + if (ns2->version.dsp_type >= 0) + hid_info(ns2->hdev, "DSP version %u.%u.%u\n", ns2->version.dsp_major, + ns2->version.dsp_minor, ns2->version.dsp_patch); + + ret = input_register_device(input); + if (ret < 0) { + hid_err(ns2->hdev, "Failed to register input; ret=%d\n", ret); + input_free_device(input); + return ret; + } + + player_led_pattern = ns2->player_id % JC_NUM_LED_PATTERNS; + hid_dbg(hdev, "assigned player %d led pattern", player_led_pattern + 1); + + ns2->leds = devm_kcalloc(&input->dev, JC_NUM_LEDS, sizeof(*ns2->leds), GFP_KERNEL); + if (!ns2->leds) { + input_unregister_device(input); + return -ENOMEM; + } + + for (i = 0; i < JC_NUM_LEDS; i++) { + struct led_classdev *led = &ns2->leds[i]; + + led->brightness = joycon_player_led_patterns[player_led_pattern][i]; + led->max_brightness = 1; + led->brightness_set_blocking = switch2_player_led_brightness_set; + led->flags = LED_CORE_SUSPENDRESUME | LED_HW_PLUGGABLE | LED_RETAIN_AT_SHUTDOWN; + char *name = devm_kasprintf(&input->dev, GFP_KERNEL, "%s:%s:%s", + dev_name(&input->dev), + "green", + joycon_player_led_names[i]); + + if (!name) { + dev_err(&input->dev, "Failed to allocate name for player %d LED; ret=%d\n", + i + 1, ret); + break; + } + + led->name = name; + ret = devm_led_classdev_register(&input->dev, led); + if (ret < 0) { + dev_err(&input->dev, "Failed to register player %d LED; ret=%d\n", + i + 1, ret); + break; + } + } + + rcu_assign_pointer(ns2->input, input); + synchronize_rcu(); + + switch2_init_step_done(ns2, NS2_INIT_INPUT); + return switch2_init_controller(ns2); +} + +static bool switch2_parse_stick_calibration(struct switch2_stick_calibration *calib, + const uint8_t *data) +{ + static const uint8_t UNCALIBRATED[9] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + }; + if (memcmp(UNCALIBRATED, data, sizeof(UNCALIBRATED)) == 0) + return false; + + calib->x.neutral = data[0]; + calib->x.neutral |= (data[1] & 0x0F) << 8; + + calib->y.neutral = data[1] >> 4; + calib->y.neutral |= data[2] << 4; + + calib->x.positive = data[3]; + calib->x.positive |= (data[4] & 0x0F) << 8; + + calib->y.positive = data[4] >> 4; + calib->y.positive |= data[5] << 4; + + calib->x.negative = data[6]; + calib->x.negative |= (data[7] & 0x0F) << 8; + + calib->y.negative = data[7] >> 4; + calib->y.negative |= data[8] << 4; + + return true; +} + +static void switch2_handle_flash_read(struct switch2_controller *ns2, uint8_t size, + uint32_t address, const uint8_t *data) +{ + bool ok; + + switch (address) { + case NS2_FLASH_ADDR_SERIAL: + if (size != NS2_FLASH_SIZE_SERIAL) + return; + memcpy(ns2->serial, data, size); + switch2_init_step_done(ns2, NS2_INIT_READ_SERIAL); + break; + case NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB: + if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB) + return; + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_PRIMARY_CALIB); + ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], data); + if (ns2->hdev) { + if (ok) { + hid_dbg(ns2->hdev, "Got factory primary stick calibration:\n"); + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n", + ns2->stick_calib[0].x.negative, + ns2->stick_calib[0].x.neutral, + ns2->stick_calib[0].x.positive); + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n", + ns2->stick_calib[0].y.negative, + ns2->stick_calib[0].y.neutral, + ns2->stick_calib[0].y.positive); + } else { + hid_dbg(ns2->hdev, "Factory primary stick calibration not present\n"); + } + } + break; + case NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB: + if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB) + return; + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_SECONDARY_CALIB); + ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], data); + if (ns2->hdev) { + if (ok) { + hid_dbg(ns2->hdev, "Got factory secondary stick calibration:\n"); + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n", + ns2->stick_calib[1].x.negative, + ns2->stick_calib[1].x.neutral, + ns2->stick_calib[1].x.positive); + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n", + ns2->stick_calib[1].y.negative, + ns2->stick_calib[1].y.neutral, + ns2->stick_calib[1].y.positive); + } else { + hid_dbg(ns2->hdev, "Factory secondary stick calibration not present\n"); + } + } + break; + case NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB: + if (size != NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB) + return; + switch2_init_step_done(ns2, NS2_INIT_READ_FACTORY_TRIGGER_CALIB); + if (data[0] != 0xFF && data[1] != 0xFF) { + ns2->lt_zero = data[0]; + ns2->rt_zero = data[1]; + + if (ns2->hdev) { + hid_dbg(ns2->hdev, "Got factory trigger calibration:\n"); + hid_dbg(ns2->hdev, "Left zero point: %i\n", ns2->lt_zero); + hid_dbg(ns2->hdev, "Right zero point: %i\n", ns2->rt_zero); + } + } else if (ns2->hdev) { + hid_dbg(ns2->hdev, "Factory trigger calibration not present\n"); + } + break; + case NS2_FLASH_ADDR_USER_PRIMARY_CALIB: + if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB) + return; + switch2_init_step_done(ns2, NS2_INIT_READ_USER_PRIMARY_CALIB); + if (get_unaligned_le16((__le16 *)data) != NS2_USER_CALIB_MAGIC) { + if (ns2->hdev) + hid_dbg(ns2->hdev, "No user primary stick calibration present\n"); + break; + } + + ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], &data[2]); + if (ns2->hdev) { + if (ok) { + hid_dbg(ns2->hdev, "Got user primary stick calibration:\n"); + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n", + ns2->stick_calib[0].x.negative, + ns2->stick_calib[0].x.neutral, + ns2->stick_calib[0].x.positive); + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n", + ns2->stick_calib[0].y.negative, + ns2->stick_calib[0].y.neutral, + ns2->stick_calib[0].y.positive); + } else { + hid_dbg(ns2->hdev, "No user primary stick calibration present\n"); + } + } + break; + case NS2_FLASH_ADDR_USER_SECONDARY_CALIB: + if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB) + return; + switch2_init_step_done(ns2, NS2_INIT_READ_USER_SECONDARY_CALIB); + if (get_unaligned_le16((__le16 *)data) != NS2_USER_CALIB_MAGIC) { + if (ns2->hdev) + hid_dbg(ns2->hdev, "No user secondary stick calibration present\n"); + break; + } + + ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], &data[2]); + if (ns2->hdev) { + if (ok) { + hid_dbg(ns2->hdev, "Got user secondary stick calibration:\n"); + hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n", + ns2->stick_calib[1].x.negative, + ns2->stick_calib[1].x.neutral, + ns2->stick_calib[1].x.positive); + hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n", + ns2->stick_calib[1].y.negative, + ns2->stick_calib[1].y.neutral, + ns2->stick_calib[1].y.positive); + } else { + hid_dbg(ns2->hdev, "No user secondary stick calibration present\n"); + } + } + break; + } +} + +static void switch2_report_buttons(struct input_dev *input, const uint8_t *bytes, + const struct switch2_ctlr_button_mapping button_mappings[]) +{ + const struct switch2_ctlr_button_mapping *button; + + for (button = button_mappings; button->code; button++) + input_report_key(input, button->code, bytes[button->byte] & button->bit); +} + +static void switch2_report_axis(struct input_dev *input, struct switch2_axis_calibration *calib, + int axis, bool invert, int value) +{ + if (calib && calib->neutral && calib->negative && calib->positive) { + value -= calib->neutral; + value *= NS2_AXIS_MAX + 1; + if (value < 0) + value /= calib->negative; + else + value /= calib->positive; + } else { + value = (value - 2048) * 16; + } + + if (invert) + value = -value; + input_report_abs(input, axis, + clamp(value, NS2_AXIS_MIN, NS2_AXIS_MAX)); +} + +static void switch2_report_stick(struct input_dev *input, struct switch2_stick_calibration *calib, + int x, bool invert_x, int y, bool invert_y, const uint8_t *data) +{ + switch2_report_axis(input, &calib->x, x, invert_x, data[0] | ((data[1] & 0x0F) << 8)); + switch2_report_axis(input, &calib->y, y, invert_y, (data[1] >> 4) | (data[2] << 4)); +} + +static void switch2_report_trigger(struct input_dev *input, uint8_t zero, int abs, uint8_t data) +{ + int value = (NS2_TRIGGER_RANGE + 1) * (data - zero); + + if (zero != 232) + value /= (232 - zero); + input_report_abs(input, abs, clamp(value, 0, NS2_TRIGGER_RANGE)); +} + +static int switch2_event(struct hid_device *hdev, struct hid_report *report, uint8_t *raw_data, + int size) +{ + struct switch2_controller *ns2 = hid_get_drvdata(hdev); + struct input_dev *input; + + if (report->type != HID_INPUT_REPORT) + return 0; + + if (size < 15) + return -EINVAL; + + guard(rcu)(); + input = rcu_dereference(ns2->input); + + if (!input) + return 0; + + switch (report->id) { + case NS2_REPORT_UNIFIED: + /* + * TODO + * This won't be sent unless the report type gets changed via command + * 03-0A, but we should support it at some point regardless. + */ + break; + case NS2_REPORT_JCL: + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false, + ABS_Y, true, &raw_data[6]); + switch2_report_buttons(input, &raw_data[3], ns2_left_joycon_button_mappings); + break; + case NS2_REPORT_JCR: + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false, + ABS_Y, true, &raw_data[6]); + switch2_report_buttons(input, &raw_data[3], ns2_right_joycon_button_mappings); + break; + case NS2_REPORT_GC: + input_report_abs(input, ABS_HAT0X, + !!(raw_data[4] & NS2_BTNL_RIGHT) - + !!(raw_data[4] & NS2_BTNL_LEFT)); + input_report_abs(input, ABS_HAT0Y, + !!(raw_data[4] & NS2_BTNL_DOWN) - + !!(raw_data[4] & NS2_BTNL_UP)); + switch2_report_buttons(input, &raw_data[3], ns2_gccon_mappings); + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false, + ABS_Y, true, &raw_data[6]); + switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, false, + ABS_RY, true, &raw_data[9]); + switch2_report_trigger(input, ns2->lt_zero, ABS_Z, raw_data[13]); + switch2_report_trigger(input, ns2->rt_zero, ABS_RZ, raw_data[14]); + break; + case NS2_REPORT_PRO: + input_report_abs(input, ABS_HAT0X, + !!(raw_data[4] & NS2_BTNL_RIGHT) - + !!(raw_data[4] & NS2_BTNL_LEFT)); + input_report_abs(input, ABS_HAT0Y, + !!(raw_data[4] & NS2_BTNL_DOWN) - + !!(raw_data[4] & NS2_BTNL_UP)); + switch2_report_buttons(input, &raw_data[3], ns2_procon_mappings); + switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false, + ABS_Y, true, &raw_data[6]); + switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, false, + ABS_RY, true, &raw_data[9]); + break; + default: + return -EINVAL; + } + + input_sync(input); + return 0; +} + +static int switch2_features_enable(struct switch2_controller *ns2, int features) +{ + __le32 feature_bits = __cpu_to_le32(features); + + if (!ns2->cfg) + return -ENOTCONN; + return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_ENABLE, + &feature_bits, sizeof(feature_bits), + ns2->cfg); +} + +static int switch2_read_flash(struct switch2_controller *ns2, uint32_t address, + uint8_t size) +{ + uint8_t message[8] = { size, 0x7e }; + + if (!ns2->cfg) + return -ENOTCONN; + put_unaligned_le32(address, &message[4]); + return ns2->cfg->send_command(NS2_CMD_FLASH, NS2_SUBCMD_FLASH_READ, message, + sizeof(message), ns2->cfg); +} + +static int switch2_set_player_id(struct switch2_controller *ns2, uint32_t player_id) +{ + int i; + int player_led_pattern = player_id % JC_NUM_LED_PATTERNS; + + for (i = 0; i < JC_NUM_LEDS; i++) + ns2->leds[i].brightness = joycon_player_led_patterns[player_led_pattern][i]; + + return switch2_set_leds(ns2); +} + +static int switch2_set_report_format(struct switch2_controller *ns2, enum switch2_report_id fmt) +{ + __le32 format_id = __cpu_to_le32(fmt); + + if (!ns2->cfg) + return -ENOTCONN; + return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_SELECT_REPORT, + &format_id, sizeof(format_id), + ns2->cfg); +} + +int switch2_init_controller(struct switch2_controller *ns2) +{ + if (ns2->init_step == NS2_INIT_DONE) + return 0; + + if (!ns2->cfg) + return -ENOTCONN; + + if (ns2->init_retries > NS2_MAX_INIT_RETRIES) { + if (ns2->init_retries == NS2_MAX_INIT_RETRIES + 1) { + dev_err(ns2->cfg->dev, "Failed to configure controller\n"); + ns2->init_retries++; + } + return -EIO; + } + + ns2->init_retries++; + switch (ns2->init_step) { + case NS2_INIT_READ_SERIAL: + return switch2_read_flash(ns2, NS2_FLASH_ADDR_SERIAL, + NS2_FLASH_SIZE_SERIAL); + case NS2_INIT_GET_FIRMWARE_INFO: + return ns2->cfg->send_command(NS2_CMD_FW_INFO, NS2_SUBCMD_FW_INFO_GET, + NULL, 0, ns2->cfg); + case NS2_INIT_READ_FACTORY_PRIMARY_CALIB: + return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB, + NS2_FLASH_SIZE_FACTORY_AXIS_CALIB); + case NS2_INIT_READ_FACTORY_SECONDARY_CALIB: + if (switch2_ctlr_is_joycon(ns2->ctlr_type)) { + switch2_init_step_done(ns2, ns2->init_step); + return switch2_init_controller(ns2); + } + return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB, + NS2_FLASH_SIZE_FACTORY_AXIS_CALIB); + case NS2_INIT_READ_FACTORY_TRIGGER_CALIB: + if (ns2->ctlr_type != NS2_CTLR_TYPE_GC) { + switch2_init_step_done(ns2, ns2->init_step); + return switch2_init_controller(ns2); + } + return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB, + NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB); + case NS2_INIT_READ_USER_PRIMARY_CALIB: + return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_PRIMARY_CALIB, + NS2_FLASH_SIZE_USER_AXIS_CALIB); + case NS2_INIT_READ_USER_SECONDARY_CALIB: + if (switch2_ctlr_is_joycon(ns2->ctlr_type)) { + switch2_init_step_done(ns2, ns2->init_step); + return switch2_init_controller(ns2); + } + return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_SECONDARY_CALIB, + NS2_FLASH_SIZE_USER_AXIS_CALIB); + case NS2_INIT_SET_FEATURE_MASK: + return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_SET_MASK, + switch2_feature_mask, sizeof(switch2_feature_mask), ns2->cfg); + case NS2_INIT_ENABLE_FEATURES: + return switch2_features_enable(ns2, NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG); + case NS2_INIT_GRIP_BUTTONS: + if (!switch2_ctlr_is_joycon(ns2->ctlr_type)) { + switch2_init_step_done(ns2, ns2->init_step); + return switch2_init_controller(ns2); + } + return ns2->cfg->send_command(NS2_CMD_GRIP, NS2_SUBCMD_GRIP_ENABLE_BUTTONS, + switch2_one_data, sizeof(switch2_one_data), + ns2->cfg); + case NS2_INIT_REPORT_FORMAT: + switch (ns2->ctlr_type) { + case NS2_CTLR_TYPE_JCL: + return switch2_set_report_format(ns2, NS2_REPORT_JCL); + case NS2_CTLR_TYPE_JCR: + return switch2_set_report_format(ns2, NS2_REPORT_JCR); + case NS2_CTLR_TYPE_PRO: + return switch2_set_report_format(ns2, NS2_REPORT_PRO); + case NS2_CTLR_TYPE_GC: + return switch2_set_report_format(ns2, NS2_REPORT_GC); + default: + switch2_init_step_done(ns2, ns2->init_step); + return switch2_init_controller(ns2); + } + case NS2_INIT_INPUT: + if (ns2->hdev) + return switch2_init_input(ns2); + break; + case NS2_INIT_SET_PLAYER_LEDS: + return switch2_set_player_id(ns2, ns2->player_id); + case NS2_INIT_FINISH: + return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_USB, + switch2_init_cmd_data, sizeof(switch2_init_cmd_data), ns2->cfg); + default: + WARN_ON_ONCE(1); + break; + } + return 0; +} + +int switch2_receive_command(struct switch2_controller *ns2, + const uint8_t *message, size_t length) +{ + const struct switch2_cmd_header *header; + int ret = 0; + + if (length < 8) + return -EINVAL; + + print_hex_dump_debug("got cmd: ", DUMP_PREFIX_OFFSET, 16, 1, message, length, false); + + mutex_lock(&ns2->lock); + + header = (const struct switch2_cmd_header *)message; + if (!(header->flags & NS2_FLAG_OK)) { + if (ns2->cfg) + dev_warn(ns2->cfg->dev, "Packet error %02x replying to command %x:%x", + header->flags, header->command, header->subcommand); + ret = -EIO; + goto exit; + } + message = &message[8]; + length -= 8; + + switch (header->command) { + case NS2_CMD_FLASH: + if (header->subcommand == NS2_SUBCMD_FLASH_READ) { + uint8_t read_size; + uint32_t read_address; + + if (length < 8) { + ret = -EINVAL; + goto exit; + } + read_size = message[0]; + read_address = get_unaligned_le32(&message[4]); + if (length < read_size + 8) { + ret = -EINVAL; + goto exit; + } + switch2_handle_flash_read(ns2, read_size, read_address, &message[8]); + } + break; + case NS2_CMD_INIT: + if (header->subcommand == NS2_SUBCMD_INIT_USB) + switch2_init_step_done(ns2, NS2_INIT_FINISH); + else if (header->subcommand == NS2_SUBCMD_INIT_SELECT_REPORT) + switch2_init_step_done(ns2, NS2_INIT_REPORT_FORMAT); + break; + case NS2_CMD_GRIP: + if (header->subcommand == NS2_SUBCMD_GRIP_ENABLE_BUTTONS) + switch2_init_step_done(ns2, NS2_INIT_GRIP_BUTTONS); + break; + case NS2_CMD_LED: + if (header->subcommand == NS2_SUBCMD_LED_PATTERN) + switch2_init_step_done(ns2, NS2_INIT_SET_PLAYER_LEDS); + break; + case NS2_CMD_FEATSEL: + if (header->subcommand == NS2_SUBCMD_FEATSEL_SET_MASK) + switch2_init_step_done(ns2, NS2_INIT_SET_FEATURE_MASK); + else if (header->subcommand == NS2_SUBCMD_FEATSEL_ENABLE) + switch2_init_step_done(ns2, NS2_INIT_ENABLE_FEATURES); + break; + case NS2_CMD_FW_INFO: + if (header->subcommand == NS2_SUBCMD_FW_INFO_GET) { + if (length < sizeof(ns2->version)) { + ret = -EINVAL; + goto exit; + } + memcpy(&ns2->version, message, sizeof(ns2->version)); + ns2->ctlr_type = ns2->version.ctlr_type; + switch2_init_step_done(ns2, NS2_INIT_GET_FIRMWARE_INFO); + } + break; + default: + break; + } + +exit: + if (ns2->init_step < NS2_INIT_DONE) + switch2_init_controller(ns2); + + mutex_unlock(&ns2->lock); + return ret; +} +EXPORT_SYMBOL_GPL(switch2_receive_command); + +int switch2_controller_attach_cfg(const char *phys, struct switch2_cfg_intf *cfg) +{ + struct switch2_controller *ns2 = switch2_get_controller(phys); + int ret = 0; + + if (IS_ERR(ns2)) + return PTR_ERR(ns2); + + mutex_lock(&ns2->lock); + if (ns2->cfg) { + ret = -EBUSY; + goto out; + } + cfg->parent = ns2; + ns2->cfg = cfg; + + if (ns2->hdev) + ret = switch2_init_controller(ns2); + + if (ret < 0) { + if (cfg->shutdown) + cfg->shutdown(cfg); + cfg->parent = NULL; + ns2->cfg = NULL; + } + +out: + mutex_unlock(&ns2->lock); + + if (ret < 0) + kref_put(&ns2->refcount, switch2_kref_put); + return ret; +} +EXPORT_SYMBOL_GPL(switch2_controller_attach_cfg); + +void switch2_controller_detach_cfg(struct switch2_controller *ns2) +{ + mutex_lock(&ns2->lock); + if (!ns2->cfg || WARN_ON(ns2 != ns2->cfg->parent)) { + mutex_unlock(&ns2->lock); + return; + } + if (ns2->cfg->shutdown) + ns2->cfg->shutdown(ns2->cfg); + ns2->cfg->parent = NULL; + ns2->cfg = NULL; + mutex_unlock(&ns2->lock); + switch2_controller_put(ns2); + kref_put(&ns2->refcount, switch2_kref_put); +} +EXPORT_SYMBOL_GPL(switch2_controller_detach_cfg); + +static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + struct switch2_controller *ns2; + struct usb_device *udev; + char phys[64]; + int ret; + + if (!hid_is_usb(hdev)) + return -ENODEV; + + udev = hid_to_usb_dev(hdev); + if (usb_make_path(udev, phys, sizeof(phys)) < 0) + return -EINVAL; + + ret = hid_parse(hdev); + if (ret) { + hid_err(hdev, "parse failed %d\n", ret); + return ret; + } + + ns2 = switch2_get_controller(phys); + if (IS_ERR(ns2)) + return PTR_ERR(ns2); + + mutex_lock(&ns2->lock); + if (ns2->hdev) { + mutex_unlock(&ns2->lock); + hid_err(hdev, + "Second hdev tried to claim same controller, first=%p vs second=%p\n", + ns2->hdev, hdev); + kref_put(&ns2->refcount, switch2_kref_put); + return -EBUSY; + } + ns2->hdev = hdev; + hid_set_drvdata(hdev, ns2); + + switch (hdev->product | (hdev->vendor << 16)) { + default: + strscpy(ns2->name, hdev->name, sizeof(ns2->name)); + break; + /* Some controllers have slightly wrong names so we override them */ + case USB_DEVICE_ID_NINTENDO_NS2_JOYCONR | (USB_VENDOR_ID_NINTENDO << 16): + /* Missing the "2" in the name */ + strscpy(ns2->name, "Nintendo Joy-Con 2 (R)", sizeof(ns2->name)); + break; + case USB_DEVICE_ID_NINTENDO_NS2_GCCON | (USB_VENDOR_ID_NINTENDO << 16): + /* Has "Nintendo" in the name twice */ + strscpy(ns2->name, "Nintendo GameCube Controller", sizeof(ns2->name)); + break; + } + + ns2->player_id = U32_MAX; + ret = ida_alloc(&nintendo_player_id_allocator, GFP_KERNEL); + if (ret < 0) + hid_warn(hdev, "Failed to allocate player ID, skipping; ret=%d\n", ret); + else + ns2->player_id = ret; + + ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); + if (ret) { + hid_err(hdev, "hw_start failed %d\n", ret); + goto err_cleanup; + } + + ret = hid_hw_open(hdev); + if (ret) { + hid_err(hdev, "hw_open failed %d\n", ret); + goto err_stop; + } + + ret = 0; + if (ns2->cfg) + ret = switch2_init_controller(ns2); + + if (!ret) { + mutex_unlock(&ns2->lock); + return 0; + } + + hid_hw_close(hdev); +err_stop: + hid_hw_stop(hdev); +err_cleanup: + ida_free(&nintendo_player_id_allocator, ns2->player_id); + ns2->hdev = NULL; + mutex_unlock(&ns2->lock); + switch2_controller_put(ns2); + kref_put(&ns2->refcount, switch2_kref_put); + + return ret; +} + +static void switch2_remove(struct hid_device *hdev) +{ + struct switch2_controller *ns2 = hid_get_drvdata(hdev); + + switch2_controller_put(ns2); + mutex_lock(&ns2->lock); + ns2->hdev = NULL; + ida_free(&nintendo_player_id_allocator, ns2->player_id); + mutex_unlock(&ns2->lock); + kref_put(&ns2->refcount, switch2_kref_put); + hid_hw_close(hdev); + hid_hw_stop(hdev); +} + static const struct hid_device_id nintendo_hid_devices[] = { + /* Switch devices */ { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_PROCON) }, { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, @@ -2907,10 +4117,67 @@ static const struct hid_device_id nintendo_hid_devices[] = { USB_DEVICE_ID_HORI_WIRELESS_SWITCH_PAD) }, { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_PDP, USB_DEVICE_ID_PDP_SWITCH_AFTERGLOW) }, + /* Switch 2 devices */ + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, + USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) }, + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, + USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) }, + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, + USB_DEVICE_ID_NINTENDO_NS2_PROCON) }, + { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, + USB_DEVICE_ID_NINTENDO_NS2_GCCON) }, { } }; MODULE_DEVICE_TABLE(hid, nintendo_hid_devices); +static bool nintendo_is_switch2(struct hid_device *hdev) +{ + return hdev->vendor == USB_VENDOR_ID_NINTENDO && + hdev->product >= USB_DEVICE_ID_NINTENDO_NS2_JOYCONR; +} + +static void nintendo_hid_remove(struct hid_device *hdev) +{ + if (nintendo_is_switch2(hdev)) + switch2_remove(hdev); + else + joycon_remove(hdev); +} + +static int nintendo_hid_event(struct hid_device *hdev, + struct hid_report *report, u8 *raw_data, int size) +{ + if (nintendo_is_switch2(hdev)) + return switch2_event(hdev, report, raw_data, size); + else + return joycon_event(hdev, report, raw_data, size); +} + +static int nintendo_hid_probe(struct hid_device *hdev, + const struct hid_device_id *id) +{ + if (nintendo_is_switch2(hdev)) + return switch2_probe(hdev, id); + else + return joycon_probe(hdev, id); +} + +static int nintendo_hid_resume(struct hid_device *hdev) +{ + if (nintendo_is_switch2(hdev)) + return 0; + else + return joycon_resume(hdev); +} + +static int nintendo_hid_suspend(struct hid_device *hdev, pm_message_t message) +{ + if (nintendo_is_switch2(hdev)) + return 0; + else + return joycon_suspend(hdev, message); +} + static struct hid_driver nintendo_hid_driver = { .name = "nintendo", .id_table = nintendo_hid_devices, @@ -2938,4 +4205,5 @@ MODULE_LICENSE("GPL"); MODULE_AUTHOR("Ryan McClelland "); MODULE_AUTHOR("Emily Strickland "); MODULE_AUTHOR("Daniel J. Ogorchock "); +MODULE_AUTHOR("Vicki Pfau "); MODULE_DESCRIPTION("Driver for Nintendo Switch Controllers"); diff --git a/drivers/hid/hid-nintendo.h b/drivers/hid/hid-nintendo.h new file mode 100644 index 000000000000..9bdef5fffa31 --- /dev/null +++ b/drivers/hid/hid-nintendo.h @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * HID driver for Nintendo Switch 2 controllers + * + * Copyright (c) 2025 Valve Software + * + * This driver is based on the following work: + * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64 + * https://github.com/ndeadly/switch2_controller_research + */ + +#ifndef __HID_NINTENDO_H +#define __HID_NINTENDO_H + +#include + +#define NS2_FLAG_OK BIT(0) +#define NS2_FLAG_NACK BIT(2) + +enum switch2_cmd { + NS2_CMD_NFC = 0x01, + NS2_CMD_FLASH = 0x02, + NS2_CMD_INIT = 0x03, + NS2_CMD_GRIP = 0x08, + NS2_CMD_LED = 0x09, + NS2_CMD_VIBRATE = 0x0a, + NS2_CMD_BATTERY = 0x0b, + NS2_CMD_FEATSEL = 0x0c, + NS2_CMD_FW_UPD = 0x0d, + NS2_CMD_FW_INFO = 0x10, + NS2_CMD_BT_PAIR = 0x15, +}; + +enum switch2_direction { + NS2_DIR_IN = 0x00, + NS2_DIR_OUT = 0x90, +}; + +enum switch2_transport { + NS2_TRANS_USB = 0x00, + NS2_TRANS_BT = 0x01, +}; + +struct switch2_cmd_header { + uint8_t command; + uint8_t flags; + uint8_t transport; + uint8_t subcommand; + uint8_t unk1; + uint8_t length; + uint16_t unk2; +}; +static_assert(sizeof(struct switch2_cmd_header) == 8); + +struct device; +struct switch2_controller; +struct switch2_cfg_intf { + struct switch2_controller *parent; + struct device *dev; + + int (*send_command)(enum switch2_cmd command, uint8_t subcommand, + const void *message, size_t length, + struct switch2_cfg_intf *intf); + void (*shutdown)(struct switch2_cfg_intf *intf); +}; + +int switch2_controller_attach_cfg(const char *phys, struct switch2_cfg_intf *cfg); +void switch2_controller_detach_cfg(struct switch2_controller *controller); + +int switch2_receive_command(struct switch2_controller *controller, + const uint8_t *message, size_t length); + +#endif