* [PATCH v2 0/2] HID: logitech-hidpp: fix Signature M650 side button timing
From: Elliot Douglas @ 2026-07-04 23:10 UTC (permalink / raw)
To: linux-input; +Cc: lains, hadess, jikos, bentiss, linux-kernel, edouglas7358
In-Reply-To: <20260613175109.44365-1-edouglas7358@gmail.com>
The Logitech Signature M650 over Bluetooth exposes its side buttons in the
normal mouse report, but the reported BTN_SIDE/BTN_EXTRA events are short
click-like events emitted around button release rather than physical
press/release events with the real hold duration. The device appears to reserve
the held side-button state for a built-in gesture mode: holding a side button
long enough, or holding it while using the wheel for horizontal scrolling, can
mean the normal mouse report never emits a usable side-button press at all.
That makes the buttons unusable for standard Linux hold actions such as
push-to-talk, drag modifiers, or remapping rules that depend on key-up timing.
When HID++ 2.0 feature 0x1b04, SpecialKeysMseButtons /
REPROG_CONTROLS_V4, temporarily diverts the same controls, the device sends
diverted-control notifications with real press and release timing. This series
adds quirk-gated support for those notifications and enables it for the
Bluetooth Signature M650.
Before enabling diversion, the driver verifies that each mapped control is
present in the device's HID++ control table and is advertised as a divertable
mouse control.
The kernel only programs temporary diversion when the device connects. It
does not continuously force the setting. A userspace HID++ tool such as
Solaar can still issue HID++ commands through hidraw; if Solaar changes the
reporting mode for the same controls afterwards, the last writer wins. That
means Solaar can still take over those controls for custom actions, but the
kernel will no longer receive the diverted button notifications for normal
evdev reporting until the kernel diverts the controls again, for example after
reconnect. While the controls remain diverted, hidraw clients should still
receive the raw reports and the kernel reports the matching evdev state.
The diverted M650 controls are reported as BTN_BACK and BTN_FORWARD. Logitech's
Signature M650 getting-started page labels these physical controls as
Back/Forward buttons and describes their default page-navigation behavior:
https://support.logi.com/hc/en-nz/articles/4414473810583-Getting-Started-Signature-M650
The reprogrammable-control support is per-product and parses the full HID++
divertedButtonsEvent pressed-control list, so it can support devices with more
buttons without relying on a single last-control release heuristic. Only the
Signature M650 opts in for now. Other Logitech devices should only be enabled
after their HID++ control IDs and divertedButtonsEvent behavior are captured
and verified.
There is evidence that this is not unique to the M650. A prior MX Anywhere 3
patch used the same HID++ feature to fix thumb buttons that only activated on
release, and Logitech documents side-button + wheel horizontal scrolling for
both the MX Anywhere 3/3S and Signature M650. Solaar's device reports and rules
documentation also show HID++ divertable back/forward controls on MX Master 3
and MX Master 3S class devices. This series remains conservative and only
enables the device tested here.
Tested with a Logitech Signature M650 L over Bluetooth, HID ID
0005:046D:B02A. Baseline evtest showed short release-time BTN_SIDE/BTN_EXTRA
events. Earlier local testing of the same HID++ diversion path showed real
hold-duration press/release events, including holds longer than 4 seconds for
both buttons.
Changes in v2:
- Replace the profile/count wrapper with NULL-terminated mapping arrays.
- Cache the selected reprogrammable-control mapping in struct hidpp_device.
- Add a named constant for the M650 Bluetooth product ID.
- Use common Back/Forward names for the 0x0053/0x0056 HID++ control IDs.
Elliot Douglas (2):
HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
HID: logitech-hidpp: enable reprogrammable buttons on Signature M650
drivers/hid/hid-logitech-hidpp.c | 225 ++++++++++++++++++++++++++++++-
1 file changed, 224 insertions(+), 1 deletion(-)
base-commit: f0866517be9345d8245d32b722574b8aecccb348
--
2.54.0
^ permalink raw reply
* [PATCH v2 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
From: Elliot Douglas @ 2026-07-04 23:10 UTC (permalink / raw)
To: linux-input; +Cc: lains, hadess, jikos, bentiss, linux-kernel, edouglas7358
In-Reply-To: <cover.1783094954.git.edouglas7358@gmail.com>
Some Logitech HID++ 2.0 mice can report diverted reprogrammable controls
through HID++ feature 0x1b04, SpecialKeysMseButtons / REPROG_CONTROLS_V4,
instead of the normal HID mouse report.
Add a quirk-gated event path for those controls. The handler temporarily
diverts verified per-product controls, parses divertedButtonsEvent as the
current pressed-control list, and reports the corresponding evdev key state
for every mapped control.
Keep the control mappings in per-product arrays so adding support for
another mouse does not change the evdev capabilities advertised by
already-supported devices.
Documentation for feature 0x1b04 describes divertedButtonsEvent as a list
of currently pressed diverted buttons, which is the event format handled
here.
Link: https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html
Signed-off-by: Elliot Douglas <edouglas7358@gmail.com>
---
drivers/hid/hid-logitech-hidpp.c | 205 +++++++++++++++++++++++++++++++
1 file changed, 205 insertions(+)
diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 70ba1a5e40d8..f9189e14fb78 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -76,6 +76,7 @@ MODULE_PARM_DESC(disable_tap_to_click,
#define HIDPP_QUIRK_HI_RES_SCROLL_1P0 BIT(28)
#define HIDPP_QUIRK_WIRELESS_STATUS BIT(29)
#define HIDPP_QUIRK_RESET_HI_RES_SCROLL BIT(30)
+#define HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS BIT(31)
/* These are just aliases for now */
#define HIDPP_QUIRK_KBD_SCROLL_WHEEL HIDPP_QUIRK_HIDPP_WHEELS
@@ -178,6 +179,8 @@ struct hidpp_scroll_counter {
unsigned long long last_time;
};
+struct hidpp_reprog_control_mapping;
+
struct hidpp_device {
struct hid_device *hid_dev;
struct input_dev *input;
@@ -205,6 +208,8 @@ struct hidpp_device {
struct hidpp_scroll_counter vertical_wheel_counter;
u8 wireless_feature_index;
+ u8 reprog_controls_feature_index;
+ const struct hidpp_reprog_control_mapping *reprog_controls;
int hires_wheel_multiplier;
u8 hires_wheel_feature_index;
@@ -3601,6 +3606,195 @@ static int hidpp10_extra_mouse_buttons_raw_event(struct hidpp_device *hidpp,
return 1;
}
+/* -------------------------------------------------------------------------- */
+/* HID++2.0 reprogrammable controls */
+/* -------------------------------------------------------------------------- */
+
+#define HIDPP_PAGE_REPROG_CONTROLS_V4 0x1b04
+
+#define HIDPP_REPROG_CONTROLS_GET_COUNT 0x00
+#define HIDPP_REPROG_CONTROLS_GET_CID_INFO 0x10
+#define HIDPP_REPROG_CONTROLS_SET_CONTROL_REPORTING 0x30
+
+#define HIDPP_REPROG_CONTROLS_FLAG_MOUSE BIT(0)
+#define HIDPP_REPROG_CONTROLS_FLAG_DIVERT BIT(5)
+
+#define HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED BIT(0)
+#define HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT BIT(1)
+
+#define HIDPP_REPROG_CONTROLS_EVENT_DIVERTED 0x00
+
+struct hidpp_reprog_control_mapping {
+ u16 control;
+ u16 code;
+};
+
+static const struct hidpp_reprog_control_mapping *
+hidpp20_reprog_controls_get_mappings(struct hidpp_device *hidpp)
+{
+ return NULL;
+}
+
+static int hidpp20_reprog_controls_get_count(struct hidpp_device *hidpp)
+{
+ struct hidpp_report response;
+ u8 feature_index = hidpp->reprog_controls_feature_index;
+ u8 cmd = HIDPP_REPROG_CONTROLS_GET_COUNT;
+ int ret;
+
+ ret = hidpp_send_fap_command_sync(hidpp, feature_index, cmd, NULL, 0,
+ &response);
+ if (ret > 0)
+ return -EPROTO;
+ if (ret)
+ return ret;
+
+ return response.fap.params[0];
+}
+
+static int hidpp20_reprog_controls_get_cid_info(struct hidpp_device *hidpp,
+ u8 index, u16 *control,
+ u8 *flags)
+{
+ struct hidpp_report response;
+ u8 feature_index = hidpp->reprog_controls_feature_index;
+ u8 cmd = HIDPP_REPROG_CONTROLS_GET_CID_INFO;
+ int ret;
+
+ ret = hidpp_send_fap_command_sync(hidpp, feature_index, cmd, &index,
+ sizeof(index), &response);
+ if (ret > 0)
+ return -EPROTO;
+ if (ret)
+ return ret;
+
+ *control = get_unaligned_be16(&response.fap.params[0]);
+ *flags = response.fap.params[4];
+
+ return 0;
+}
+
+static bool hidpp20_reprog_controls_find_control(struct hidpp_device *hidpp,
+ u16 control)
+{
+ int count, ret;
+ u16 cid;
+ u8 flags;
+ int i;
+
+ count = hidpp20_reprog_controls_get_count(hidpp);
+ if (count < 0)
+ return false;
+
+ for (i = 0; i < count; i++) {
+ ret = hidpp20_reprog_controls_get_cid_info(hidpp, i, &cid,
+ &flags);
+ if (ret)
+ return false;
+
+ if (cid == control)
+ return (flags & HIDPP_REPROG_CONTROLS_FLAG_MOUSE) &&
+ (flags & HIDPP_REPROG_CONTROLS_FLAG_DIVERT);
+ }
+
+ return false;
+}
+
+static int hidpp20_reprog_controls_set_control_reporting(struct hidpp_device *hidpp,
+ u16 control, u8 flags)
+{
+ struct hidpp_report response;
+ u8 params[5];
+
+ put_unaligned_be16(control, ¶ms[0]);
+ params[2] = flags;
+ put_unaligned_be16(control, ¶ms[3]);
+
+ return hidpp_send_fap_command_sync(hidpp,
+ hidpp->reprog_controls_feature_index,
+ HIDPP_REPROG_CONTROLS_SET_CONTROL_REPORTING,
+ params, sizeof(params), &response);
+}
+
+static void hidpp20_reprog_controls_connect(struct hidpp_device *hidpp)
+{
+ const struct hidpp_reprog_control_mapping *mapping;
+ u8 flags = HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED |
+ HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT;
+
+ if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS))
+ return;
+
+ if (!hidpp->reprog_controls)
+ return;
+
+ if (hidpp_root_get_feature(hidpp, HIDPP_PAGE_REPROG_CONTROLS_V4,
+ &hidpp->reprog_controls_feature_index))
+ return;
+
+ for (mapping = hidpp->reprog_controls; mapping->control; mapping++) {
+ if (!hidpp20_reprog_controls_find_control(hidpp, mapping->control))
+ continue;
+
+ hidpp20_reprog_controls_set_control_reporting(hidpp,
+ mapping->control,
+ flags);
+ }
+}
+
+static int hidpp20_reprog_controls_raw_event(struct hidpp_device *hidpp,
+ u8 *data, int size)
+{
+ const struct hidpp_reprog_control_mapping *mapping;
+ struct hidpp_report *report = (struct hidpp_report *)data;
+ u16 controls[4];
+ bool pressed;
+ unsigned int i, j;
+
+ if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS) ||
+ !hidpp->input ||
+ !hidpp->reprog_controls ||
+ hidpp->reprog_controls_feature_index == 0xff)
+ return 0;
+
+ if (size < HIDPP_REPORT_LONG_LENGTH ||
+ report->fap.feature_index != hidpp->reprog_controls_feature_index ||
+ report->fap.funcindex_clientid != HIDPP_REPROG_CONTROLS_EVENT_DIVERTED)
+ return 0;
+
+ for (i = 0; i < ARRAY_SIZE(controls); i++)
+ controls[i] = get_unaligned_be16(&report->fap.params[i * 2]);
+
+ for (mapping = hidpp->reprog_controls; mapping->control; mapping++) {
+ pressed = false;
+
+ for (j = 0; j < ARRAY_SIZE(controls); j++) {
+ if (controls[j] == mapping->control) {
+ pressed = true;
+ break;
+ }
+ }
+
+ input_report_key(hidpp->input, mapping->code, pressed);
+ }
+
+ input_sync(hidpp->input);
+
+ return 1;
+}
+
+static void hidpp20_reprog_controls_populate_input(struct hidpp_device *hidpp,
+ struct input_dev *input_dev)
+{
+ const struct hidpp_reprog_control_mapping *mapping;
+
+ if (!hidpp->reprog_controls)
+ return;
+
+ for (mapping = hidpp->reprog_controls; mapping->control; mapping++)
+ input_set_capability(input_dev, EV_KEY, mapping->code);
+}
+
static void hidpp10_extra_mouse_buttons_populate_input(
struct hidpp_device *hidpp, struct input_dev *input_dev)
{
@@ -3859,6 +4053,9 @@ static void hidpp_populate_input(struct hidpp_device *hidpp,
if (hidpp->quirks & HIDPP_QUIRK_HIDPP_EXTRA_MOUSE_BTNS)
hidpp10_extra_mouse_buttons_populate_input(hidpp, input);
+
+ if (hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS)
+ hidpp20_reprog_controls_populate_input(hidpp, input);
}
static int hidpp_input_configured(struct hid_device *hdev,
@@ -3971,6 +4168,10 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data,
return ret;
}
+ ret = hidpp20_reprog_controls_raw_event(hidpp, data, size);
+ if (ret != 0)
+ return ret;
+
if (hidpp->quirks & HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS) {
ret = hidpp10_consumer_keys_raw_event(hidpp, data, size);
if (ret != 0)
@@ -4264,6 +4465,8 @@ static void hidpp_connect_event(struct work_struct *work)
return;
}
+ hidpp20_reprog_controls_connect(hidpp);
+
if (hidpp->quirks & HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS) {
ret = hidpp10_consumer_keys_connect(hidpp);
if (ret)
@@ -4436,6 +4639,8 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
hidpp->hid_dev = hdev;
hidpp->name = hdev->name;
hidpp->quirks = id->driver_data;
+ hidpp->reprog_controls_feature_index = 0xff;
+ hidpp->reprog_controls = hidpp20_reprog_controls_get_mappings(hidpp);
hid_set_drvdata(hdev, hidpp);
ret = hid_parse(hdev);
--
2.54.0
^ permalink raw reply related
* [PATCH v2 2/2] HID: logitech-hidpp: enable reprogrammable buttons on Signature M650
From: Elliot Douglas @ 2026-07-04 23:10 UTC (permalink / raw)
To: linux-input; +Cc: lains, hadess, jikos, bentiss, linux-kernel, edouglas7358
In-Reply-To: <cover.1783094954.git.edouglas7358@gmail.com>
The Bluetooth Signature M650 exposes its side buttons through the normal
mouse report, but the observed events are short click-like events emitted
around release rather than physical press/release state.
The device appears to use the held side-button state for its built-in
gesture and side-button + wheel horizontal-scroll mode. As a result,
holding a side button long enough can prevent the normal mouse report from
emitting a usable button event at all.
HID++ REPROG_CONTROLS_V4 diversion for control IDs 0x0053 and 0x0056
provides real press and release timing for those same controls. Logitech
documents the Signature M650 side buttons as Back/Forward buttons, so
report the diverted controls as BTN_BACK and BTN_FORWARD.
The HID++ 0x1b04 documentation lists those control IDs as Back and
Forward. The driver still verifies that the controls are present in the
device control table and advertised as divertable before changing their
reporting mode.
Link: https://support.logi.com/hc/en-nz/articles/4414473810583-Getting-Started-Signature-M650
Signed-off-by: Elliot Douglas <edouglas7358@gmail.com>
---
drivers/hid/hid-logitech-hidpp.c | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index f9189e14fb78..e71dd2457e80 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -3624,14 +3624,30 @@ static int hidpp10_extra_mouse_buttons_raw_event(struct hidpp_device *hidpp,
#define HIDPP_REPROG_CONTROLS_EVENT_DIVERTED 0x00
+#define HIDPP_REPROG_CONTROL_BACK 0x0053
+#define HIDPP_REPROG_CONTROL_FORWARD 0x0056
+
+#define HIDPP_PRODUCT_SIGNATURE_M650 0xb02a
+
struct hidpp_reprog_control_mapping {
u16 control;
u16 code;
};
+static const struct hidpp_reprog_control_mapping m650_reprog_control_mappings[] = {
+ { HIDPP_REPROG_CONTROL_BACK, BTN_BACK },
+ { HIDPP_REPROG_CONTROL_FORWARD, BTN_FORWARD },
+ { }
+};
+
static const struct hidpp_reprog_control_mapping *
hidpp20_reprog_controls_get_mappings(struct hidpp_device *hidpp)
{
+ switch (hidpp->hid_dev->product) {
+ case HIDPP_PRODUCT_SIGNATURE_M650:
+ return m650_reprog_control_mappings;
+ }
+
return NULL;
}
@@ -4911,7 +4927,9 @@ static const struct hid_device_id hidpp_devices[] = {
{ /* MX Vertical mouse over Bluetooth */
HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb020) },
{ /* Signature M650 over Bluetooth */
- HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb02a) },
+ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH,
+ HIDPP_PRODUCT_SIGNATURE_M650),
+ .driver_data = HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS },
{ /* MX Master 3 mouse over Bluetooth */
HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb023) },
{ /* MX Anywhere 3 mouse over Bluetooth */
--
2.54.0
^ permalink raw reply related
* Re: [PATCH v2 1/2] HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
From: sashiko-bot @ 2026-07-04 23:23 UTC (permalink / raw)
To: Elliot Douglas; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <d4c48a9d36c738abd02a0430a6ca8a5a792fb3ea.1783094954.git.edouglas7358@gmail.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Pre-existing issues:
- [High] Dangling pointer `hidpp->input` leads to Use-After-Free if `input_register_device()` fails.
- [High] Unbounded 64-byte structure copy in `hidpp_raw_hidpp_event` causes a Kernel Heap Out-of-Bounds Read.
--
commit 13dc21807e8eb6f76d9029e89353e4bd2e6aa1c3
Author: Elliot Douglas <edouglas7358@gmail.com>
Subject: HID: logitech-hidpp: add HID++ 2.0 reprogrammable button support
Some Logitech HID++ 2.0 mice can report diverted reprogrammable controls
through HID++ feature 0x1b04, SpecialKeysMseButtons / REPROG_CONTROLS_V4,
instead of the normal HID mouse report.
Link: https://lekensteyn.nl/files/logitech/x1b04_specialkeysmsebuttons.html
> diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
> index 70ba1a5e40d8e..f9189e14fb780 100644
> --- a/drivers/hid/hid-logitech-hidpp.c
> +++ b/drivers/hid/hid-logitech-hidpp.c
[ ... ]
> @@ -3601,6 +3606,195 @@ static int hidpp10_extra_mouse_buttons_raw_event(struct hidpp_device *hidpp,
[ ... ]
> +static int hidpp20_reprog_controls_raw_event(struct hidpp_device *hidpp,
> + u8 *data, int size)
> +{
> + const struct hidpp_reprog_control_mapping *mapping;
> + struct hidpp_report *report = (struct hidpp_report *)data;
> + u16 controls[4];
> + bool pressed;
> + unsigned int i, j;
> +
> + if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS) ||
> + !hidpp->input ||
[Severity: High]
This is a pre-existing issue, but could this lead to a use-after-free?
Looking at hidpp_connect_event(), if input_register_device() fails, it
cleans up by calling input_free_device() but leaves the hidpp->input
pointer dangling:
ret = input_register_device(input);
if (ret) {
input_free_device(input);
return;
}
Because hid_device_io_start() has already enabled incoming packets before
this point, could a newly arriving event trigger this new
hidpp20_reprog_controls_raw_event() handler, pass the !hidpp->input check,
and dereference the freed pointer?
> + !hidpp->reprog_controls ||
> + hidpp->reprog_controls_feature_index == 0xff)
> + return 0;
[ ... ]
> @@ -3971,6 +4168,10 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data,
> return ret;
> }
>
> + ret = hidpp20_reprog_controls_raw_event(hidpp, data, size);
> + if (ret != 0)
> + return ret;
> +
[Severity: High]
This is a pre-existing issue, but does hidpp_raw_hidpp_event() perform an
out-of-bounds read on the incoming data buffer?
Earlier in this function, if the packet matches a pending question, it does
a direct structure assignment:
if (hidpp_match_answer(question, report) ||
hidpp_match_error(question, report)) {
*answer = *report;
Since struct hidpp_report is a 64-byte packed structure, won't this pointer
assignment perform a fixed 64-byte copy regardless of the actual size of the
received buffer (which could be as small as 7 bytes for a
REPORT_ID_HIDPP_SHORT packet)?
Could this copy adjacent heap memory into the answer buffer, potentially
leaking it when the data is parsed later?
> if (hidpp->quirks & HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS) {
> ret = hidpp10_consumer_keys_raw_event(hidpp, data, size);
> if (ret != 0)
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1783094954.git.edouglas7358@gmail.com?part=1
^ permalink raw reply
* Re: [PATCH] Input: ims-pcu - add missing MODULE_DEVICE_TABLE()
From: Dmitry Torokhov @ 2026-07-04 23:35 UTC (permalink / raw)
To: Pengpeng Hou
Cc: open list:INPUT (KEYBOARD, MOUSE, JOYSTICK, TOUCHSCREEN)...,
linux-kernel
In-Reply-To: <20260704151730.42772-1-pengpeng@iscas.ac.cn>
On Sat, Jul 04, 2026 at 11:17:30PM +0800, Pengpeng Hou wrote:
> The driver has a match table for the usb bus wired into its driver
> structure, but the table is not exported with MODULE_DEVICE_TABLE().
>
> Add the missing MODULE_DEVICE_TABLE() entry so module alias information
> is generated for automatic module loading.
>
> This is a source-level fix. It does not claim dynamic hardware
> reproduction; the evidence is the driver-owned match table, its use by
> the driver registration structure, and the missing module alias
> publication.
>
> Signed-off-by: Pengpeng Hou <pengpeng@iscas.ac.cn>
Applied, thank you.
--
Dmitry
^ permalink raw reply
* Re: [PATCH v2 1/7] Input: elan_i2c - Wait for initialization after enabling regulator supply
From: Dmitry Torokhov @ 2026-07-04 23:44 UTC (permalink / raw)
To: Chen-Yu Tsai
Cc: Matthias Brugger, AngeloGioacchino Del Regno, Benson Leung,
Tzung-Bi Shih, Jiri Kosina, Andi Shyti, linux-mediatek,
devicetree, linux-arm-kernel, chrome-platform, linux-input,
linux-i2c, linux-kernel
In-Reply-To: <20260703115601.1323491-2-wenst@chromium.org>
Hi Chen-Yu,
On Fri, Jul 03, 2026 at 07:55:54PM +0800, Chen-Yu Tsai wrote:
> Elan trackpad controllers require some delay after enabling power to
> the controller for the hardware and firmware to initialize:
>
> - 2ms for hardware initialization
> - 100ms for firmware initialization
>
> Until then, the hardware will not respond to I2C transfers. This was
> observed on the MT8173 Chromebooks after the regulator supply for the
> trackpad was changed to "not always on".
>
> Add proper delays after regulator_enable() calls. To avoid impacting
> the boot time of existing devices that have the power rails always on,
> skip the delay if the regulator supply was already enabled. In this
> case the regulator is either always on, was on by default at power up,
> or was left on by some other driver, such as the I2C OF component
> prober. Either way the controller has had ample time to initialize.
Unfortunately we do not know that [it had ample time]. For this code be
reliable we need to record the time at which given regulator was turned
on and then execute/adjust the delay as needed. Until we have it we need
to assume the regulator was enabled at the time of regulator_enable()
call.
I am not concerned with increased boot time too much given the driver is
set up for asynchronous probing.
Thanks.
--
Dmitry
^ permalink raw reply
* Re: [PATCH 18/26] sh: maple: introduce callback_mutex in maple_device
From: Dmitry Torokhov @ 2026-07-04 23:50 UTC (permalink / raw)
To: Florian Fuchs
Cc: Miquel Raynal, Richard Weinberger, Vignesh Raghavendra,
Yoshinori Sato, Rich Felker, John Paul Adrian Glaubitz,
Adrian McMenamin, linux-kernel, linux-input, linux-mtd, linux-sh
In-Reply-To: <akkrQJfxSCvUptC4@lithos>
Hi Florian,
On Sat, Jul 04, 2026 at 05:48:16PM +0200, Florian Fuchs wrote:
> Hi Dmitry,
>
> On 03 Jul 22:57, Dmitry Torokhov wrote:
> > The Maple bus core invokes client callbacks asynchronously from a
> > workqueue (maple_dma_handler). If a device is removed (or closed) while
> > a callback is in flight, it can lead to UAF bugs if the driver's private
> > data is freed.
> >
> > Introduce callback_mutex in struct maple_device to synchronize
> > callback registration/modification and callback invocation.
...
>
> Thank you so much for your efforts. I can confirm sashikos finding in my
> test (that I saw later),
Thank you for giving the series a spin!
> that this patch hangs the bus if a VMU memcard
> is inserted. So that the peripherals don't work anymore after insertion
> of the memcard. With a last log like:
> Dreamcast_visual_memory 3:01.E: VMU at (3, 1) is busy
Yes, indeed, I overlooked the vmu-flash use case. I will introduce a
lockless way of setting up the callback and a wrapper that takes the
lock and that should solve the deadlock in vmu-flash driver.
I wonder if you had a chance top try out the preceding patches and if
they worked for you? If they do and you can add your tested-by I should
be able to pick up at least the input patches that do not need maple
core changes...
Thanks.
--
Dmitry
^ permalink raw reply
* Re: [PATCH v2 2/2] iio: inkern: Use namespaced exports
From: Jonathan Cameron @ 2026-07-05 1:07 UTC (permalink / raw)
To: Dmitry Torokhov
Cc: Romain Gantois, MyungJoo Ham, Chanwoo Choi, Guenter Roeck,
Peter Rosin, David Lechner, Nuno Sá, Andy Shevchenko,
Lars-Peter Clausen, Michael Hennerich, Mariel Tinaco, Kevin Tsai,
Linus Walleij, Eugen Hristev, Vinod Koul, Kishon Vijay Abraham I,
Sebastian Reichel, Chen-Yu Tsai, Hans de Goede,
Support Opensource, Paul Cercueil, Iskren Chernev,
Krzysztof Kozlowski, Marek Szyprowski, Matheus Castello,
Saravanan Sekar, Matthias Brugger, AngeloGioacchino Del Regno,
Casey Connolly, Pali Rohár, Orson Zhai, Baolin Wang,
Chunyan Zhang, Amit Kucheria, Thara Gopinath, Rafael J. Wysocki,
Daniel Lezcano, Zhang Rui, Lukasz Luba, Claudiu Beznea,
Liam Girdwood, Mark Brown, Jaroslav Kysela, Takashi Iwai,
Sylwester Nawrocki, Olivier Moysan, Arnaud Pouliquen,
Maxime Coquelin, Alexandre Torgue, Thomas Petazzoni, linux-kernel,
linux-hwmon, linux-iio, linux-input, linux-phy, linux-pm,
linux-mips, linux-mediatek, linux-arm-msm, linux-sound,
linux-stm32, Sebastian Reichel, Andy Shevchenko
In-Reply-To: <20260603182052.7d23c067@jic23-huawei>
On Wed, 3 Jun 2026 18:20:52 +0100
Jonathan Cameron <jic23@kernel.org> wrote:
> On Sun, 22 Mar 2026 15:24:21 -0700
> Dmitry Torokhov <dmitry.torokhov@gmail.com> wrote:
>
> > On Tue, Dec 09, 2025 at 09:25:56AM +0100, Romain Gantois wrote:
> > > Use namespaced exports for IIO consumer API functions.
> > >
> > > This will make it easier to manage the IIO export surface. Consumer drivers
> > > will only be provided access to a specific set of functions, thereby
> > > restricting usage of internal IIO functions by other parts of the kernel.
> > >
> > > This change cannot be split into several parts without breaking
> > > bisectability, thus all of the affected drivers are modified at once.
> > >
> > > Acked-by: Sebastian Reichel <sebastian.reichel@collabora.com> # for power-supply
> > > Acked-by: Guenter Roeck <linux@roeck-us.net>
> > > Reviewed-by: Andy Shevchenko <andriy.shevchenko@intel.com>
> > > Signed-off-by: Romain Gantois <romain.gantois@bootlin.com>
> >
> > For input:
> >
> > Acked-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>
> >
> > Thanks.
> >
>
> For anyone wondering what happened to this... I forgot to apply this at the
> beginning of the cycle and by the time I remembered we had too much queued up
> so it would have been messy to do an immutable branch. Anyhow, I plan to
> sort this at start of next cycle.
Immutable branch created as: iio-inkern-namespace-ib based on v7.2-rc1
on https://git.kernel.org/pub/scm/linux/kernel/git/jic23/iio.git
I've merged it into the testing branch of iio.git so if anyone else does
need to merge this to avoid conflicts, perhaps wait a day or two until
I've pushed it out as togreg for linux-next to pick up.
Thanks,
Jonathan
>
> Jonathan
^ permalink raw reply
* Re: [PATCH] Input: zinitix: iterate contact slots by finger count
From: Nikita Travkin @ 2026-07-05 7:13 UTC (permalink / raw)
To: Linus Walleij
Cc: Thanh Nguyen, dmitry.torokhov, linux-input, linux-kernel,
linus.walleij, timon37
In-Reply-To: <CAD++jLknVJYLc0-StNFcN67pUDRQunUJvNn-Npz+HJ-j2WXB5g@mail.gmail.com>
Linus Walleij писал(а) 30.06.2026 14:11:
> Hi Thanh,
>
> sorry for slow reply!
>
> On Wed, Apr 8, 2026 at 6:19 AM Thanh Nguyen <thanhnguyxn07@gmail.com> wrote:
>>
>> On affected devices (for example Samsung A3 2015), the value in
>> touch_event.finger_mask appears to behave as a count of reported slots
>> rather than a bitmask. Using for_each_set_bit() can then skip valid
>> contacts and break multitouch gestures.
>>
>> Keep filtering by SUB_BIT_EXIST to avoid reporting shadow contacts, but
>> iterate from slot 0 up to min(finger_mask, MAX_SUPPORTED_FINGER_NUM).
>> This follows the maintainer feedback to treat the field as a possible
>> count while preserving the anti-shadow check.
>>
>> Fixes: e941dc13fd37 ("Input: zinitix - do not report shadow fingers")
>> Link: https://bugzilla.kernel.org/show_bug.cgi?id=221278
>> Signed-off-by: Thanh Nguyen <thanhnguyxn07@gmail.com>
>> ---
>> v2:
>> - Address maintainer feedback: do not revert e941dc13fd37.
>> - Keep SUB_BIT_EXIST filtering to avoid shadow contacts.
>> - Treat finger_mask as a slot-count bound and iterate 0..min(mask, max).
>
> I can test this if need be, but maybe Nikita want to check it
> first?
>
Hi!
I've quickly checked it on my gt58 (bt532) but it made me remember that
on this device/chip it seems like only "TOUCH_MODE=0" is supported and
reported fingers don't have extra "width"/"angle" fields. (I have a
vague recollection that this was somehow related to touchkeys being
enabled or not) Otherwise on this chip (firmware? touch_mode?) the
finger count is indeed a count and not a mask.
Given we have an EXIST bit IMO it still makes sense to make this change.
Maybe also worth adding a short comment why we're treating mask as
count if Linus's devices report mask and your/mine report count...
Nikita
> Yours,
> Linus Walleij
^ permalink raw reply
* Re: [PATCH] Input: zinitix: iterate contact slots by finger count
From: Thành Nguyễn @ 2026-07-05 11:17 UTC (permalink / raw)
To: nikita
Cc: linusw, dmitry.torokhov, linux-input, linux-kernel, linus.walleij,
timon37
In-Reply-To: <8ca5a59eeb522bfd949f31e2416639fd@trvn.ru>
Hi Nikita, Linus, and Dmitry,
Thank you all for the feedback and for confirming the fix on your end, Nikita.
Unfortunately, since it's been a while since I submitted the v2 patch,
I recently had to completely wipe my development machine and no longer
have the local environment or the original patch files to create a v3.
Since the logic in v2 is sound and the only change needed is a brief
comment explaining the dual mask/count nature of `finger_mask`, would
one of you be willing to add that comment and apply the patch on my
behalf? I'd really appreciate it.
Thanks again for your time and help with getting this merged!
Best regards,
Thanh Nguyen
^ permalink raw reply
* Re: [PATCH v11 1/3] HID: nintendo: Add preliminary Switch 2 controller driver
From: Silvan Jegen @ 2026-07-04 19:36 UTC (permalink / raw)
To: Vicki Pfau; +Cc: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
In-Reply-To: <20260702214704.1859350-2-vi@endrift.com>
Heyhey!
Some comments and questions below.
Vicki Pfau <vi@endrift.com> wrote:
> This adds a new driver for the Switch 2 controllers. The Switch 2 uses an
> unusual split-interface design such that input and rumble occur on the main
> HID interface, but all other communication occurs over a "configuration"
> interface. This is the case on both USB and Bluetooth, so this new driver
> uses a split-driver design with the HID interface being the "main" driver
> and the configuration interface is a secondary driver that looks up to the
> HID interface, sharing resources on a common struct.
>
> Due to using a non-standard pairing interface as well as Bluetooth
> communications being extremely limited in the kernel, a custom interface
> between userspace and the kernel will need to be designed, along with
> bringup in BlueZ. That is beyond the scope of this initial patch, which
> only contains the generic HID and USB configuration interface drivers.
>
> This initial work supports general input for the Joy-Con 2, Pro Controller
> 2, and GameCube NSO controllers. IMU, rumble and battery support is not yet
> present.
>
> Signed-off-by: Vicki Pfau <vi@endrift.com>
> ---
> MAINTAINERS | 1 +
> drivers/hid/Kconfig | 11 +-
> drivers/hid/hid-ids.h | 4 +
> drivers/hid/hid-nintendo.c | 1278 ++++++++++++++++-
> drivers/hid/hid-nintendo.h | 72 +
> drivers/input/joystick/Kconfig | 11 +
> drivers/input/joystick/Makefile | 1 +
> drivers/input/joystick/nintendo-switch2-usb.c | 468 ++++++
> 8 files changed, 1836 insertions(+), 10 deletions(-)
> create mode 100644 drivers/hid/hid-nintendo.h
> create mode 100644 drivers/input/joystick/nintendo-switch2-usb.c
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 4ecd282f8f52..778982ab298e 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -19051,6 +19051,7 @@ F: drivers/scsi/nsp32*
>
> NINTENDO HID DRIVER
> M: Daniel J. Ogorchock <djogorchock@gmail.com>
> +M: Vicki Pfau <vi@endrift.com>
> L: linux-input@vger.kernel.org
> S: Maintained
> F: drivers/hid/hid-nintendo*
> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
> index f9bcaeb66385..19c77c323ec9 100644
> --- a/drivers/hid/Kconfig
> +++ b/drivers/hid/Kconfig
> @@ -843,10 +843,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.
> diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
> index 1059922baaac..9ba62b8fb894 100644
> --- a/drivers/hid/hid-ids.h
> +++ b/drivers/hid/hid-ids.h
> @@ -1103,6 +1103,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.c b/drivers/hid/hid-nintendo.c
> index e7302ec01ff1..e21c36921832 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 <djogorchock@gmail.com>
> * Portions Copyright (c) 2020 Nadia Holmquist Pedersen <nadia@nhp.sh>
> * Copyright (c) 2022 Emily Strickland <linux@emily.st>
> * Copyright (c) 2023 Ryan McClelland <rymcclel@gmail.com>
> + * 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 <linux/unaligned.h>
> #include <linux/delay.h>
> #include <linux/device.h>
> @@ -41,6 +46,8 @@
> #include <linux/module.h>
> #include <linux/power_supply.h>
> #include <linux/spinlock.h>
> +#include <linux/usb.h>
> +#include "usbhid/usbhid.h"
>
> /*
> * Reference the url below for the following HID report defines:
> @@ -2662,7 +2669,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);
> @@ -2673,7 +2680,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;
> @@ -2777,7 +2784,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;
> @@ -2796,7 +2803,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;
> @@ -2819,7 +2826,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);
>
> @@ -2838,7 +2845,1208 @@ 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)
Two tabs instead of one?
> +#define NS2_FEATURE_MOUSE BIT(4)
This one doesn't seem to be used (yet). Not sure if that is intended
or not.
> +#define NS2_FEATURE_RUMBLE BIT(5)
> +#define NS2_FEATURE_MAGNETO BIT(7)
Ditto.
> +
> +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(sizeof(*ns2), GFP_KERNEL);
> + 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) {
> + hid_err(ns2->hdev, "Failed to allocate LEDs\n");
> + 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;
nit: this break is redundant and could be removed.
> + }
> +}
> +
> +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;
We might want to make sure that calib->{negative,positive} are not zero
(at calibration parsing time) in order to avoid a div-by-zero issue
> + 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)
From what I can tell switch2_features_enable is only called from
switch2_init_controller, which is already checking that ns2->cfg is not
NULL (while holding the ns2->lock). So I would assume that this check is
not necessary
> + 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;
See above.
> + 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) {
Couldn't we get rid of this if-clause and the ns2->init_retries++ after
it? That would mean we would potentially log the error message earlier
but I don't think that would be an issue
> + if (ns2->cfg)
We check this a few lines up so unless this somehow can get set
to NULL again (even though we seem to be holding ns2->lock whenever
switch2_init_controller is called) I don't think we need to check this
a second time
> + 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)
Shouldn't this check for 'length < 9' as otherwise
message = &message[8];
would index the ninth byte which is out of range (i.e. off-by-one issue)?
> + 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;
Shouldn't this be 'length -= 9' (as arrays are zero-indexed)?
> +
> + 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) {
Shouldn't this be 'if (length < 9)' as well?
> + ret = -EINVAL;
> + goto exit;
> + }
> + read_size = message[0];
> + read_address = get_unaligned_le32(&message[4]);
> + if (length < read_size + 8) {
Should it check for
if (length < read_size + 9) {
as we are taking a pointer to the ninth byte with '&message[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) {
> + 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;
> + }
> + 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,
> @@ -2863,10 +4071,67 @@ static const struct hid_device_id nintendo_hid_devices[] = {
> USB_DEVICE_ID_NINTENDO_N64CON) },
> { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_HORI,
> USB_DEVICE_ID_HORI_WIRELESS_SWITCH_PAD) },
> + /* 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,
> @@ -2894,4 +4159,5 @@ MODULE_LICENSE("GPL");
> MODULE_AUTHOR("Ryan McClelland <rymcclel@gmail.com>");
> MODULE_AUTHOR("Emily Strickland <linux@emily.st>");
> MODULE_AUTHOR("Daniel J. Ogorchock <djogorchock@gmail.com>");
> +MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
> 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..7aff22f30266
> --- /dev/null
> +++ b/drivers/hid/hid-nintendo.h
> @@ -0,0 +1,72 @@
> +/* 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 <linux/bits.h>
> +
> +#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);
> +};
> +
> +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
> diff --git a/drivers/input/joystick/Kconfig b/drivers/input/joystick/Kconfig
> index 7755e5b454d2..868262c6ccd9 100644
> --- a/drivers/input/joystick/Kconfig
> +++ b/drivers/input/joystick/Kconfig
> @@ -422,4 +422,15 @@ config JOYSTICK_SEESAW
> To compile this driver as a module, choose M here: the module will be
> called adafruit-seesaw.
>
> +config JOYSTICK_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 nintendo-switch2-usb.
> +
> endif
> diff --git a/drivers/input/joystick/Makefile b/drivers/input/joystick/Makefile
> index 9976f596a920..8f92900ae885 100644
> --- a/drivers/input/joystick/Makefile
> +++ b/drivers/input/joystick/Makefile
> @@ -34,6 +34,7 @@ obj-$(CONFIG_JOYSTICK_SIDEWINDER) += sidewinder.o
> obj-$(CONFIG_JOYSTICK_SPACEBALL) += spaceball.o
> obj-$(CONFIG_JOYSTICK_SPACEORB) += spaceorb.o
> obj-$(CONFIG_JOYSTICK_STINGER) += stinger.o
> +obj-$(CONFIG_JOYSTICK_NINTENDO_SWITCH2_USB) += nintendo-switch2-usb.o
> obj-$(CONFIG_JOYSTICK_TMDC) += tmdc.o
> obj-$(CONFIG_JOYSTICK_TURBOGRAFX) += turbografx.o
> obj-$(CONFIG_JOYSTICK_TWIDJOY) += twidjoy.o
> diff --git a/drivers/input/joystick/nintendo-switch2-usb.c b/drivers/input/joystick/nintendo-switch2-usb.c
> new file mode 100644
> index 000000000000..a6999a0a26ae
> --- /dev/null
> +++ b/drivers/input/joystick/nintendo-switch2-usb.c
> @@ -0,0 +1,468 @@
> +// 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/hid-ids.h"
> +#include "../../hid/hid-nintendo.h"
> +#include <linux/module.h>
> +#include <linux/usb/input.h>
> +
> +#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:
> + break;
> + 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);
> + 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 {
nit: this else is redundant as we are continue-ing above anyways.
> + ns2_usb->bulk_in[i].state = NS2_URB_FREE;
> + }
> + }
> +
> + if (ns2_usb->bulk_in[i].state != NS2_URB_FREE)
> + 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) || ns2_usb->shutdown)
> + continue;
> +
> + ns2_urb = &ns2_usb->bulk_in[i];
> + if (!ns2_urb)
AFAICT ns2_urb will never be NULL here so we can remove this check.
> + 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:
> + 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_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);
> + 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) || ns2_usb->shutdown)
> + 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;
> +
> + ret = switch2_controller_attach_cfg(phys, &ns2_usb->cfg);
> + if (ret < 0)
> + goto err_free_out;
> +
> + return 0;
> +
> +err_free_out:
> + 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_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
> + usb_free_urb(urb);
> + }
> +err_free_in:
> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
> + ns2_usb->shutdown = true;
> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
> +
> + usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
> + cancel_work_sync(&ns2_usb->message_in_work);
> + 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_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
> + usb_free_urb(urb);
> + }
> + 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);
> + unsigned long flags;
> + struct urb *urb;
> + uint8_t *data;
> + int i;
> +
> + /* Prevent any further IN URBs from being scheduled */
> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
> + ns2_usb->shutdown = true;
> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
> +
> + usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
> + cancel_work_sync(&ns2_usb->message_in_work);
> + for (i = 0; i < NS2_IN_URBS; i++) {
> + spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
> + urb = ns2_usb->bulk_in[i].urb;
In other clean-up cases we are checking this to be NULL. We might have
to do so here as well unless we know that it will never be NULL on
switch2_usb_disconnect (otherwise we will have a NULL pointer deref a
few lines down).
> + data = ns2_usb->bulk_in[i].data;
> + ns2_usb->bulk_in[i].urb = NULL;
> + ns2_usb->bulk_in[i].data = NULL;
> + spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
> +
> + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
> + usb_free_urb(urb);
> + }
> +
> + /*
> + * 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.
> + */
> + switch2_controller_detach_cfg(ns2_usb->cfg.parent);
> +
> + 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;
See above.
> + data = ns2_usb->bulk_out[i].data;
> + ns2_usb->bulk_out[i].urb = NULL;
> + ns2_usb->bulk_out[i].data = NULL;
> + spin_unlock_irqrestore(&ns2_usb->bulk_out_lock, flags);
> +
> + usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, data, urb->transfer_dma);
> + usb_free_urb(urb);
> + }
> +}
> +
> +#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 <vi@endrift.com>");
> +MODULE_DESCRIPTION("Driver for Nintendo Switch 2 Controllers");
Thanks for all the work on this!
Cheers,
Silvan
^ permalink raw reply
* Re: [PATCH v11 2/3] HID: nintendo: Add rumble support for Switch 2 controllers
From: Silvan Jegen @ 2026-07-05 14:13 UTC (permalink / raw)
To: Vicki Pfau; +Cc: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
In-Reply-To: <20260702214704.1859350-3-vi@endrift.com>
Heyhey!
Just one comment below.
Vicki Pfau <vi@endrift.com> wrote:
> This adds rumble support for both the "HD Rumble" linear resonant actuator
> type as used in the Joy-Cons and Pro Controller, as well as the eccentric
> rotating mass type used in the GameCube controller. Note that since there's
> currently no API for exposing full control of LRAs with evdev, it only
> simulates a basic rumble for now.
>
> Signed-off-by: Vicki Pfau <vi@endrift.com>
> ---
> drivers/hid/Kconfig | 8 +-
> drivers/hid/hid-nintendo.c | 211 ++++++++++++++++++++++++++++++++++++-
> 2 files changed, 213 insertions(+), 6 deletions(-)
>
> diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
> index 19c77c323ec9..851eed76c236 100644
> --- a/drivers/hid/Kconfig
> +++ b/drivers/hid/Kconfig
> @@ -859,10 +859,10 @@ config NINTENDO_FF
> depends on HID_NINTENDO
> select INPUT_FF_MEMLESS
> help
> - Say Y here if you have a Nintendo Switch controller and want to enable
> - force feedback support for it. This works for both joy-cons, the pro
> - controller, and the NSO N64 controller. For the pro controller, both
> - rumble motors can be controlled individually.
> + Say Y here if you have a Nintendo Switch or Switch 2 controller and want
> + to enable force feedback support for it. This works for Joy-Cons, the Pro
> + Controllers, and the NSO N64 and GameCube controller. For the Pro
> + Controller, both rumble motors can be controlled individually.
>
> config HID_NTI
> tristate "NTI keyboard adapters"
> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
> index e21c36921832..a36f4fd9a1da 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c
> @@ -37,6 +37,7 @@
> #include <linux/unaligned.h>
> #include <linux/delay.h>
> #include <linux/device.h>
> +#include <linux/devm-helpers.h>
> #include <linux/kernel.h>
> #include <linux/hid.h>
> #include <linux/idr.h>
> @@ -2989,6 +2990,7 @@ enum switch2_init_step {
> NS2_INIT_READ_USER_SECONDARY_CALIB,
> NS2_INIT_SET_FEATURE_MASK,
> NS2_INIT_ENABLE_FEATURES,
> + NS2_INIT_ENABLE_RUMBLE,
> NS2_INIT_GRIP_BUTTONS,
> NS2_INIT_REPORT_FORMAT,
> NS2_INIT_INPUT,
> @@ -3020,6 +3022,18 @@ struct switch2_stick_calibration {
> struct switch2_axis_calibration y;
> };
>
> +struct switch2_hd_rumble {
> + uint16_t hi_freq : 10;
> + uint16_t hi_amp : 10;
> + uint16_t lo_freq : 10;
> + uint16_t lo_amp : 10;
> +} __packed;
> +
> +struct switch2_erm_rumble {
> + uint16_t error;
> + uint16_t amplitude;
> +};
> +
> struct switch2_controller {
> struct hid_device *hdev;
> struct switch2_cfg_intf *cfg;
> @@ -3043,8 +3057,45 @@ struct switch2_controller {
>
> uint32_t player_id;
> struct led_classdev *leds;
> +
> +#if IS_ENABLED(CONFIG_NINTENDO_FF)
> + spinlock_t rumble_lock;
> + uint8_t rumble_seq;
> + union {
> + struct switch2_hd_rumble hd;
> + struct switch2_erm_rumble sd;
> + } rumble;
> + uint64_t last_rumble_work;
> + struct delayed_work rumble_work;
> + uint8_t *rumble_buffer;
> +#endif
> };
>
> +enum gc_rumble {
> + GC_RUMBLE_OFF = 0,
> + GC_RUMBLE_ON = 1,
> + GC_RUMBLE_STOP = 2,
> +};
> +
> +/*
> + * The highest rumble level for "HD Rumble" is strong enough to potentially damage the controller,
> + * and also leaves your hands feeling like melted jelly, so we set a semi-arbitrary scaling factor
> + * to artificially limit the maximum for safety and comfort. It is currently unknown if the Switch
> + * 2 itself does something similar, but it's quite likely.
> + *
> + * This value must be between 0 and 1024, otherwise the math below will overflow.
> + */
> +#define RUMBLE_MAX 450u
> +
> +/*
> + * Semi-arbitrary values used to simulate the "rumble" sensation of an eccentric rotating
> + * mass type haptic motor on the Switch 2 controllers' linear resonant actuator type haptics.
> + *
> + * The units used are unknown, but the values must be between 0 and 1023.
> + */
> +#define RUMBLE_HI_FREQ 0x187
> +#define RUMBLE_LO_FREQ 0x112
> +
> static DEFINE_MUTEX(switch2_controllers_lock);
> static LIST_HEAD(switch2_controllers);
>
> @@ -3136,7 +3187,7 @@ static const uint8_t switch2_init_cmd_data[] = {
> 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,
> + NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU | NS2_FEATURE_RUMBLE,
> 0x00, 0x00, 0x00
> };
>
> @@ -3209,6 +3260,125 @@ static void switch2_kref_put(struct kref *refcount)
> kfree(ns2);
> }
>
> +#if IS_ENABLED(CONFIG_NINTENDO_FF)
> +static void switch2_encode_rumble(struct switch2_hd_rumble *rumble, uint8_t buffer[5])
> +{
> + buffer[0] = rumble->hi_freq;
> + buffer[1] = (rumble->hi_freq >> 8) | (rumble->hi_amp << 2);
> + buffer[2] = (rumble->hi_amp >> 6) | (rumble->lo_freq << 4);
> + buffer[3] = (rumble->lo_freq >> 4) | (rumble->lo_amp << 6);
> + buffer[4] = rumble->lo_amp >> 2;
> +}
> +
> +static int switch2_play_effect(struct input_dev *dev, void *data, struct ff_effect *effect)
> +{
> + struct switch2_controller *ns2 = input_get_drvdata(dev);
We might want to check ns2 for NULL here (like we do in
switch2_player_led_brightness_set).
Cheers,
Silvan
> + unsigned long flags;
> +
> + if (effect->type != FF_RUMBLE)
> + return 0;
> +
> + spin_lock_irqsave(&ns2->rumble_lock, flags);
> + if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
> + ns2->rumble.sd.amplitude = max(effect->u.rumble.strong_magnitude,
> + (uint16_t) (effect->u.rumble.weak_magnitude >> 1));
> + } else {
> + ns2->rumble.hd.hi_freq = RUMBLE_HI_FREQ;
> + ns2->rumble.hd.lo_freq = RUMBLE_LO_FREQ;
> + ns2->rumble.hd.hi_amp = effect->u.rumble.weak_magnitude * RUMBLE_MAX >> 16;
> + ns2->rumble.hd.lo_amp = effect->u.rumble.strong_magnitude * RUMBLE_MAX >> 16;
> + }
> + spin_unlock_irqrestore(&ns2->rumble_lock, flags);
> +
> + schedule_delayed_work(&ns2->rumble_work, 0);
> +
> + return 0;
> +}
> +
> +static void switch2_rumble_work(struct work_struct *work)
> +{
> + struct switch2_controller *ns2 = container_of(to_delayed_work(work),
> + struct switch2_controller, rumble_work);
> + unsigned long flags;
> + bool active;
> + int ret = 0;
> +
> + spin_lock_irqsave(&ns2->rumble_lock, flags);
> + ns2->rumble_buffer[0x1] = 0x50 | ns2->rumble_seq;
> + if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
> + ns2->rumble_buffer[0] = 3;
> + if (ns2->rumble.sd.amplitude == 0) {
> + ns2->rumble_buffer[2] = GC_RUMBLE_STOP;
> + ns2->rumble.sd.error = 0;
> + active = false;
> + } else {
> + if (ns2->rumble.sd.error < ns2->rumble.sd.amplitude) {
> + ns2->rumble_buffer[2] = GC_RUMBLE_ON;
> + ns2->rumble.sd.error += U16_MAX - ns2->rumble.sd.amplitude;
> + } else {
> + ns2->rumble_buffer[2] = GC_RUMBLE_OFF;
> + ns2->rumble.sd.error -= ns2->rumble.sd.amplitude;
> + }
> + active = true;
> + }
> + } else {
> + ns2->rumble_buffer[0] = 1;
> + switch2_encode_rumble(&ns2->rumble.hd, &ns2->rumble_buffer[0x2]);
> + active = ns2->rumble.hd.hi_amp || ns2->rumble.hd.lo_amp;
> + if (ns2->ctlr_type == NS2_CTLR_TYPE_PRO) {
> + /*
> + * The Pro Controller contains separate LRAs on each
> + * side that can be controlled individually.
> + */
> + ns2->rumble_buffer[0] = 2;
> + ns2->rumble_buffer[0x11] = 0x50 | ns2->rumble_seq;
> + switch2_encode_rumble(&ns2->rumble.hd, &ns2->rumble_buffer[0x12]);
> + }
> + }
> + ns2->rumble_seq = (ns2->rumble_seq + 1) & 0xF;
> + spin_unlock_irqrestore(&ns2->rumble_lock, flags);
> +
> + if (active) {
> + unsigned long interval = msecs_to_jiffies(4);
> + uint64_t current_jiffies = get_jiffies_64();
> +
> + if (!ns2->last_rumble_work)
> + ns2->last_rumble_work = current_jiffies;
> + else
> + ns2->last_rumble_work += interval;
> +
> + /* Reschedule a little early to make sure the buffer never underruns */
> + interval -= msecs_to_jiffies(2);
> + if (ns2->last_rumble_work + interval >= current_jiffies)
> + schedule_delayed_work(&ns2->rumble_work,
> + ns2->last_rumble_work + interval - current_jiffies);
> + else
> + schedule_delayed_work(&ns2->rumble_work, 0);
> + } else {
> + ns2->last_rumble_work = 0;
> + }
> +
> + mutex_lock(&ns2->lock);
> + if (!ns2->hdev) {
> + cancel_delayed_work(&ns2->rumble_work);
> + } else {
> + ret = hid_hw_output_report(ns2->hdev, ns2->rumble_buffer, 64);
> + /*
> + * Don't log on ENODEV, ESHUTDOWN, or EPROTO, which can happen
> + * mid-hotplug. Also cancel any further work on ENODEV or
> + * ESHUTDOWN as they're clear indications that the endpoint
> + * is dead.
> + */
> + if (ret == -ENODEV || ret == -ESHUTDOWN)
> + cancel_delayed_work(&ns2->rumble_work);
> + else if (ret < 0 && ret != -EPROTO)
> + hid_warn_ratelimited(ns2->hdev,
> + "Failed to send output report ret=%d\n", ret);
> + }
> + mutex_unlock(&ns2->lock);
> +}
> +#endif
> +
> static int switch2_set_leds(struct switch2_controller *ns2)
> {
> int i;
> @@ -3332,6 +3502,26 @@ static int switch2_init_input(struct switch2_controller *ns2)
> return -EINVAL;
> }
>
> +#if IS_ENABLED(CONFIG_NINTENDO_FF)
> + ns2->rumble_buffer = devm_kzalloc(&input->dev, 64, GFP_KERNEL);
> + if (!ns2->rumble_buffer) {
> + input_free_device(input);
> + return -ENOMEM;
> + }
> + ret = devm_delayed_work_autocancel(&input->dev, &ns2->rumble_work, switch2_rumble_work);
> + if (ret < 0) {
> + input_free_device(input);
> + return ret;
> + }
> +
> + input_set_capability(input, EV_FF, FF_RUMBLE);
> + ret = input_ff_create_memless(input, NULL, switch2_play_effect);
> + if (ret) {
> + input_free_device(input);
> + return ret;
> + }
> +#endif
> +
> 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)
> @@ -3765,7 +3955,16 @@ int switch2_init_controller(struct switch2_controller *ns2)
> 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);
> + return switch2_features_enable(ns2, NS2_FEATURE_BUTTONS |
> + NS2_FEATURE_ANALOG | NS2_FEATURE_RUMBLE);
> + case NS2_INIT_ENABLE_RUMBLE:
> + /*
> + * It is unclear what this packet is supposed to be for, but it
> + * appears to be needed for rumble to work reliably. The reply
> + * data indicates it might be a query of some sort, but we
> + * ignore the reply so long as it doesn't return an error.
> + */
> + return ns2->cfg->send_command(0x11, 1, NULL, 0, ns2->cfg);
> case NS2_INIT_GRIP_BUTTONS:
> if (!switch2_ctlr_is_joycon(ns2->ctlr_type)) {
> switch2_init_step_done(ns2, ns2->init_step);
> @@ -3878,6 +4077,10 @@ int switch2_receive_command(struct switch2_controller *ns2,
> switch2_init_step_done(ns2, NS2_INIT_GET_FIRMWARE_INFO);
> }
> break;
> + case 0x11:
> + if (header->subcommand == 1)
> + switch2_init_step_done(ns2, NS2_INIT_ENABLE_RUMBLE);
> + break;
> default:
> break;
> }
> @@ -3997,6 +4200,10 @@ static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id
> else
> ns2->player_id = ret;
>
> +#if IS_ENABLED(CONFIG_NINTENDO_FF)
> + spin_lock_init(&ns2->rumble_lock);
> +#endif
> +
> ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
> if (ret) {
> hid_err(hdev, "hw_start failed %d\n", ret);
^ permalink raw reply
* Re: [PATCH v11 3/3] HID: nintendo: Add unified report format support
From: Silvan Jegen @ 2026-07-05 14:27 UTC (permalink / raw)
To: Vicki Pfau; +Cc: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
In-Reply-To: <20260702214704.1859350-4-vi@endrift.com>
Vicki Pfau <vi@endrift.com> wrote:
> This adds support for the "unified" report format that all controllers also
> support, which has overlapping fields for like buttons and axes between
> them.
>
> Signed-off-by: Vicki Pfau <vi@endrift.com>
This LGTM!
Reviewed-by: Silvan Jegen <s.jegen@gmail.com>
> ---
> drivers/hid/hid-nintendo.c | 151 +++++++++++++++++++++++++++++++++++--
> 1 file changed, 146 insertions(+), 5 deletions(-)
>
> diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
> index a36f4fd9a1da..ca2126f85bb3 100644
> --- a/drivers/hid/hid-nintendo.c
> +++ b/drivers/hid/hid-nintendo.c
> @@ -2873,6 +2873,36 @@ static int joycon_suspend(struct hid_device *hdev, pm_message_t message)
> #define NS2_BTN3_SR BIT(6)
> #define NS2_BTN3_SL BIT(7)
>
> +#define NS2_BTN_U1_Y BIT(0)
> +#define NS2_BTN_U1_X BIT(1)
> +#define NS2_BTN_U1_B BIT(2)
> +#define NS2_BTN_U1_A BIT(3)
> +#define NS2_BTN_U1_SR BIT(4)
> +#define NS2_BTN_U1_SL BIT(5)
> +#define NS2_BTN_U1_R BIT(6)
> +#define NS2_BTN_U1_ZR BIT(7)
> +
> +#define NS2_BTN_U2_MINUS BIT(0)
> +#define NS2_BTN_U2_PLUS BIT(1)
> +#define NS2_BTN_U2_RS BIT(2)
> +#define NS2_BTN_U2_LS BIT(3)
> +#define NS2_BTN_U2_HOME BIT(4)
> +#define NS2_BTN_U2_CAPTURE BIT(5)
> +#define NS2_BTN_U2_C BIT(6)
> +
> +#define NS2_BTN_U3_DOWN BIT(0)
> +#define NS2_BTN_U3_UP BIT(1)
> +#define NS2_BTN_U3_RIGHT BIT(2)
> +#define NS2_BTN_U3_LEFT BIT(3)
> +#define NS2_BTN_U3_SR BIT(4)
> +#define NS2_BTN_U3_SL BIT(5)
> +#define NS2_BTN_U3_L BIT(6)
> +#define NS2_BTN_U3_ZL BIT(7)
> +
> +#define NS2_BTN_U4_GR BIT(0)
> +#define NS2_BTN_U4_GL BIT(1)
> +#define NS2_BTN_U4_HEADSET BIT(5)
> +
> #define NS2_BTN_JCR_HOME BIT(0)
> #define NS2_BTN_JCR_GR BIT(2)
> #define NS2_BTN_JCR_C NS2_BTN3_C
> @@ -3121,6 +3151,22 @@ static const struct switch2_ctlr_button_mapping ns2_left_joycon_button_mappings[
> { /* sentinel */ },
> };
>
> +static const struct switch2_ctlr_button_mapping ns2_left_joycon_button_unified_mappings[] = {
> + { BTN_DPAD_LEFT, 2, NS2_BTN_U3_LEFT, },
> + { BTN_DPAD_UP, 2, NS2_BTN_U3_UP, },
> + { BTN_DPAD_DOWN, 2, NS2_BTN_U3_DOWN, },
> + { BTN_DPAD_RIGHT, 2, NS2_BTN_U3_RIGHT, },
> + { BTN_TL, 2, NS2_BTN_U3_L, },
> + { BTN_TL2, 2, NS2_BTN_U3_ZL, },
> + { BTN_SELECT, 1, NS2_BTN_U2_MINUS, },
> + { BTN_THUMBL, 1, NS2_BTN_U2_LS, },
> + { KEY_RECORD, 1, NS2_BTN_U2_CAPTURE, },
> + { BTN_GRIPR, 2, NS2_BTN_U3_SL, },
> + { BTN_GRIPR2, 2, NS2_BTN_U3_SR, },
> + { BTN_GRIPL, 3, NS2_BTN_U4_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, },
> @@ -3138,6 +3184,23 @@ static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_mappings
> { /* sentinel */ },
> };
>
> +static const struct switch2_ctlr_button_mapping ns2_right_joycon_button_unified_mappings[] = {
> + { BTN_SOUTH, 0, NS2_BTN_U1_A, },
> + { BTN_EAST, 0, NS2_BTN_U1_B, },
> + { BTN_NORTH, 0, NS2_BTN_U1_X, },
> + { BTN_WEST, 0, NS2_BTN_U1_Y, },
> + { BTN_TR, 0, NS2_BTN_U1_R, },
> + { BTN_TR2, 0, NS2_BTN_U1_ZR },
> + { BTN_START, 1, NS2_BTN_U2_PLUS, },
> + { BTN_THUMBR, 1, NS2_BTN_U2_RS, },
> + { BTN_C, 1, NS2_BTN_U2_C, },
> + { BTN_MODE, 1, NS2_BTN_U2_HOME, },
> + { BTN_GRIPL2, 0, NS2_BTN_U1_SL, },
> + { BTN_GRIPL, 0, NS2_BTN_U1_SR, },
> + { BTN_GRIPR, 3, NS2_BTN_U4_GR, },
> + { /* sentinel */ },
> +};
> +
> static const struct switch2_ctlr_button_mapping ns2_procon_mappings[] = {
> { BTN_SOUTH, 0, NS2_BTNR_A, },
> { BTN_EAST, 0, NS2_BTNR_B, },
> @@ -3159,6 +3222,27 @@ static const struct switch2_ctlr_button_mapping ns2_procon_mappings[] = {
> { /* sentinel */ },
> };
>
> +static const struct switch2_ctlr_button_mapping ns2_procon_unified_mappings[] = {
> + { BTN_SOUTH, 0, NS2_BTN_U1_A, },
> + { BTN_EAST, 0, NS2_BTN_U1_B, },
> + { BTN_NORTH, 0, NS2_BTN_U1_X, },
> + { BTN_WEST, 0, NS2_BTN_U1_Y, },
> + { BTN_TL, 2, NS2_BTN_U3_L, },
> + { BTN_TR, 0, NS2_BTN_U1_R, },
> + { BTN_TL2, 2, NS2_BTN_U3_ZL, },
> + { BTN_TR2, 0, NS2_BTN_U1_ZR, },
> + { BTN_SELECT, 1, NS2_BTN_U2_MINUS, },
> + { BTN_START, 1, NS2_BTN_U2_PLUS, },
> + { BTN_THUMBL, 1, NS2_BTN_U2_LS, },
> + { BTN_THUMBR, 1, NS2_BTN_U2_RS, },
> + { BTN_MODE, 1, NS2_BTN_U2_HOME },
> + { KEY_RECORD, 1, NS2_BTN_U2_CAPTURE },
> + { BTN_GRIPR, 3, NS2_BTN_U4_GR },
> + { BTN_GRIPL, 3, NS2_BTN_U4_GL },
> + { BTN_C, 1, NS2_BTN_U2_C },
> + { /* sentinel */ },
> +};
> +
> static const struct switch2_ctlr_button_mapping ns2_gccon_mappings[] = {
> { BTN_SOUTH, 0, NS2_BTNR_A, },
> { BTN_EAST, 0, NS2_BTNR_B, },
> @@ -3176,6 +3260,23 @@ static const struct switch2_ctlr_button_mapping ns2_gccon_mappings[] = {
> { /* sentinel */ },
> };
>
> +static const struct switch2_ctlr_button_mapping ns2_gccon_unified_mappings[] = {
> + { BTN_SOUTH, 0, NS2_BTN_U1_A, },
> + { BTN_EAST, 0, NS2_BTN_U1_B, },
> + { BTN_NORTH, 0, NS2_BTN_U1_X, },
> + { BTN_WEST, 0, NS2_BTN_U1_Y, },
> + { BTN_TL2, 2, NS2_BTN_U3_L, },
> + { BTN_TR2, 0, NS2_BTN_U1_R, },
> + { BTN_TL, 2, NS2_BTN_U3_ZL },
> + { BTN_TR, 0, NS2_BTN_U1_ZR },
> + { BTN_SELECT, 1, NS2_BTN_U2_MINUS, },
> + { BTN_START, 1, NS2_BTN_U2_PLUS, },
> + { BTN_MODE, 1, NS2_BTN_U2_HOME },
> + { KEY_RECORD, 1, NS2_BTN_U2_CAPTURE },
> + { BTN_C, 1, NS2_BTN_U2_C },
> + { /* sentinel */ },
> +};
> +
> static const uint8_t switch2_init_cmd_data[] = {
> /*
> * The last 6 bytes of this packet are the MAC address of
> @@ -3802,11 +3903,51 @@ static int switch2_event(struct hid_device *hdev, struct hid_report *report, uin
>
> 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.
> - */
> + if (size < 0x3f)
> + return -EINVAL;
> +
> + switch (ns2->ctlr_type) {
> + case NS2_CTLR_TYPE_JCL:
> + switch2_report_stick(input, &ns2->stick_calib[0],
> + ABS_X, false, ABS_Y, true, &raw_data[11]);
> + switch2_report_buttons(input, &raw_data[5],
> + ns2_left_joycon_button_unified_mappings);
> + break;
> + case NS2_CTLR_TYPE_JCR:
> + switch2_report_stick(input, &ns2->stick_calib[0],
> + ABS_X, false, ABS_Y, true, &raw_data[14]);
> + switch2_report_buttons(input, &raw_data[5],
> + ns2_right_joycon_button_unified_mappings);
> + break;
> + case NS2_CTLR_TYPE_GC:
> + input_report_abs(input, ABS_HAT0X,
> + !!(raw_data[7] & NS2_BTN_U3_RIGHT) -
> + !!(raw_data[7] & NS2_BTN_U3_LEFT));
> + input_report_abs(input, ABS_HAT0Y,
> + !!(raw_data[7] & NS2_BTN_U3_DOWN) -
> + !!(raw_data[7] & NS2_BTN_U3_UP));
> + switch2_report_buttons(input, &raw_data[5], ns2_gccon_unified_mappings);
> + switch2_report_stick(input, &ns2->stick_calib[0],
> + ABS_X, false, ABS_Y, true, &raw_data[11]);
> + switch2_report_stick(input, &ns2->stick_calib[1],
> + ABS_RX, false, ABS_RY, true, &raw_data[14]);
> + switch2_report_trigger(input, ns2->lt_zero, ABS_Z, raw_data[0x3d]);
> + switch2_report_trigger(input, ns2->rt_zero, ABS_RZ, raw_data[0x3e]);
> + break;
> + case NS2_CTLR_TYPE_PRO:
> + input_report_abs(input, ABS_HAT0X,
> + !!(raw_data[7] & NS2_BTN_U3_RIGHT) -
> + !!(raw_data[7] & NS2_BTN_U3_LEFT));
> + input_report_abs(input, ABS_HAT0Y,
> + !!(raw_data[7] & NS2_BTN_U3_DOWN) -
> + !!(raw_data[7] & NS2_BTN_U3_UP));
> + switch2_report_buttons(input, &raw_data[5], ns2_procon_unified_mappings);
> + switch2_report_stick(input, &ns2->stick_calib[0],
> + ABS_X, false, ABS_Y, true, &raw_data[11]);
> + switch2_report_stick(input, &ns2->stick_calib[1],
> + ABS_RX, false, ABS_RY, true, &raw_data[14]);
> + break;
> + }
> break;
> case NS2_REPORT_JCL:
> switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, false,
^ permalink raw reply
* Re: [PATCH v2 3/6] iio: hid-sensors: Use implicit NULL pointer checks
From: Sanjay Chitroda @ 2026-07-05 18:17 UTC (permalink / raw)
To: Jonathan Cameron, Sanjay Chitroda via B4 Relay
Cc: David Lechner, Nuno Sá, Andy Shevchenko, Jiri Kosina,
Srinivas Pandruvada, linux-iio, linux-kernel, linux-input,
Maxwell Doose
In-Reply-To: <20260702182205.388efe24@jic23-huawei>
On 2 July 2026 10:52:05 pm IST, Jonathan Cameron <jic23@kernel.org> wrote:
>On Thu, 02 Jul 2026 21:48:00 +0530
>Sanjay Chitroda via B4 Relay <devnull+sanjayembeddedse.gmail.com@kernel.org> wrote:
>
>> From: Sanjay Chitroda <sanjayembeddedse@gmail.com>
>>
>> Replace explicit NULL pointer comparisons with implicit checks across
>> HID sensor IIO drivers to follow the preferred kernel coding style.
>Is there anything in the kernel wide style guides about this?
>
>I do prefer this style in IIO but perhaps we should document it
>as local IIO style rather than implying general guidance (unless
>there is some!)
>
Hi Jonathan,
I took reference of existing IIO commit message and followed the same.
There is no kernel coding guideline for NULL pointer. If you think it would be useful, I would be happy to work on documenting the preferred conventions under "Documentation/driver-api/iio/", for example as a new "coding-style.rst" or similar document.
Thanks, Sanjay
>>
>> Convert 'if (indio_dev == NULL)' -> 'if (!indio_dev)'.
>>
>> No functional change.
>>
>> Signed-off-by: Sanjay Chitroda <sanjayembeddedse@gmail.com>
>> Reviewed-by: Maxwell Doose <m32285159@gmail.com>
>> ---
>> drivers/iio/accel/hid-sensor-accel-3d.c | 2 +-
>> drivers/iio/magnetometer/hid-sensor-magn-3d.c | 2 +-
>> drivers/iio/orientation/hid-sensor-incl-3d.c | 2 +-
>> drivers/iio/orientation/hid-sensor-rotation.c | 2 +-
>> 4 files changed, 4 insertions(+), 4 deletions(-)
>>
>> diff --git a/drivers/iio/accel/hid-sensor-accel-3d.c b/drivers/iio/accel/hid-sensor-accel-3d.c
>> index 9197f3424c0c..225f8dd65ab1 100644
>> --- a/drivers/iio/accel/hid-sensor-accel-3d.c
>> +++ b/drivers/iio/accel/hid-sensor-accel-3d.c
>> @@ -325,7 +325,7 @@ static int hid_accel_3d_probe(struct platform_device *pdev)
>>
>> indio_dev = devm_iio_device_alloc(&pdev->dev,
>> sizeof(struct accel_3d_state));
>> - if (indio_dev == NULL)
>> + if (!indio_dev)
>> return -ENOMEM;
>>
>> platform_set_drvdata(pdev, indio_dev);
>> diff --git a/drivers/iio/magnetometer/hid-sensor-magn-3d.c b/drivers/iio/magnetometer/hid-sensor-magn-3d.c
>> index ad10fa20fae0..738bad65d74d 100644
>> --- a/drivers/iio/magnetometer/hid-sensor-magn-3d.c
>> +++ b/drivers/iio/magnetometer/hid-sensor-magn-3d.c
>> @@ -463,7 +463,7 @@ static int hid_magn_3d_probe(struct platform_device *pdev)
>>
>> indio_dev = devm_iio_device_alloc(&pdev->dev,
>> sizeof(struct magn_3d_state));
>> - if (indio_dev == NULL)
>> + if (!indio_dev)
>> return -ENOMEM;
>>
>> platform_set_drvdata(pdev, indio_dev);
>> diff --git a/drivers/iio/orientation/hid-sensor-incl-3d.c b/drivers/iio/orientation/hid-sensor-incl-3d.c
>> index 870c8929491e..c8efb0dab8b6 100644
>> --- a/drivers/iio/orientation/hid-sensor-incl-3d.c
>> +++ b/drivers/iio/orientation/hid-sensor-incl-3d.c
>> @@ -302,7 +302,7 @@ static int hid_incl_3d_probe(struct platform_device *pdev)
>>
>> indio_dev = devm_iio_device_alloc(&pdev->dev,
>> sizeof(struct incl_3d_state));
>> - if (indio_dev == NULL)
>> + if (!indio_dev)
>> return -ENOMEM;
>>
>> platform_set_drvdata(pdev, indio_dev);
>> diff --git a/drivers/iio/orientation/hid-sensor-rotation.c b/drivers/iio/orientation/hid-sensor-rotation.c
>> index 2dad0453fc67..6db253c1635d 100644
>> --- a/drivers/iio/orientation/hid-sensor-rotation.c
>> +++ b/drivers/iio/orientation/hid-sensor-rotation.c
>> @@ -274,7 +274,7 @@ static int hid_dev_rot_probe(struct platform_device *pdev)
>>
>> indio_dev = devm_iio_device_alloc(&pdev->dev,
>> sizeof(struct dev_rot_state));
>> - if (indio_dev == NULL)
>> + if (!indio_dev)
>> return -ENOMEM;
>>
>> platform_set_drvdata(pdev, indio_dev);
>>
>
^ permalink raw reply
page: | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox