Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v3 01/10] Input: xbox_gip - Add new driver for Xbox GIP
From: Vicki Pfau @ 2026-03-10  5:19 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau
In-Reply-To: <20260310052017.1289494-1-vi@endrift.com>

This introduces a new driver for the Xbox One/Series controller protocol,
officially known as the Gaming Input Protocol, or GIP for short.

Microsoft released documentation on (some of) GIP in late 2024, upon which
this driver is based. Though the documentation was incomplete, it still
provided enough information to warrant a clean start over the previous,
incomplete implementation.

This driver is already at feature parity with the GIP support in xpad,
along with several more enhancements:

- Proper support for parsing message length and fragmented messages
- Metadata parsing, allowing for auto-detection on various parameters,
  including the presence and location in the message of the share button,
  as well as detection of specific device types

The framework set out in this driver also allows future expansion for
specialized device types and additional features more cleanly than xpad.

Future plans include:

- Adding support for more device types, such as arcade sticks, racing
  wheels and flight sticks.
- Support for the security handshake, which is required for devices that
  use wireless dongles.
- Exposing a raw character device to enable sending vendor-specific
  commands from userspace.
- Event logging to either sysfs or dmesg.
- Support for the headphone jack.

Signed-off-by: Vicki Pfau <vi@endrift.com>
---
 MAINTAINERS                              |    6 +
 drivers/input/joystick/Kconfig           |    2 +
 drivers/input/joystick/Makefile          |    1 +
 drivers/input/joystick/gip/Kconfig       |   30 +
 drivers/input/joystick/gip/Makefile      |    3 +
 drivers/input/joystick/gip/gip-core.c    | 2536 ++++++++++++++++++++++
 drivers/input/joystick/gip/gip-drivers.c |  210 ++
 drivers/input/joystick/gip/gip.h         |  309 +++
 8 files changed, 3097 insertions(+)
 create mode 100644 drivers/input/joystick/gip/Kconfig
 create mode 100644 drivers/input/joystick/gip/Makefile
 create mode 100644 drivers/input/joystick/gip/gip-core.c
 create mode 100644 drivers/input/joystick/gip/gip-drivers.c
 create mode 100644 drivers/input/joystick/gip/gip.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 9ed6d11a77466..6c744d0af359d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27927,6 +27927,12 @@ S:	Maintained
 F:	drivers/media/rc/keymaps/rc-xbox-dvd.c
 F:	drivers/media/rc/xbox_remote.c
 
+XBOX GIP
+M:	Vicki Pfau <vi@endrift.com>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	drivers/input/joystick/gip/
+
 XC2028/3028 TUNER DRIVER
 M:	Mauro Carvalho Chehab <mchehab@kernel.org>
 L:	linux-media@vger.kernel.org
diff --git a/drivers/input/joystick/Kconfig b/drivers/input/joystick/Kconfig
index 7755e5b454d2c..d4665c80a3713 100644
--- a/drivers/input/joystick/Kconfig
+++ b/drivers/input/joystick/Kconfig
@@ -291,6 +291,8 @@ config JOYSTICK_JOYDUMP
 	  To compile this driver as a module, choose M here: the
 	  module will be called joydump.
 
+source "drivers/input/joystick/gip/Kconfig"
+
 config JOYSTICK_XPAD
 	tristate "Xbox gamepad support"
 	depends on USB_ARCH_HAS_HCD
diff --git a/drivers/input/joystick/Makefile b/drivers/input/joystick/Makefile
index 9976f596a9208..323392921b7dc 100644
--- a/drivers/input/joystick/Makefile
+++ b/drivers/input/joystick/Makefile
@@ -39,5 +39,6 @@ obj-$(CONFIG_JOYSTICK_TURBOGRAFX)	+= turbografx.o
 obj-$(CONFIG_JOYSTICK_TWIDJOY)		+= twidjoy.o
 obj-$(CONFIG_JOYSTICK_WARRIOR)		+= warrior.o
 obj-$(CONFIG_JOYSTICK_WALKERA0701)	+= walkera0701.o
+obj-$(CONFIG_JOYSTICK_XBOX_GIP)		+= gip/
 obj-$(CONFIG_JOYSTICK_XPAD)		+= xpad.o
 obj-$(CONFIG_JOYSTICK_ZHENHUA)		+= zhenhua.o
diff --git a/drivers/input/joystick/gip/Kconfig b/drivers/input/joystick/gip/Kconfig
new file mode 100644
index 0000000000000..83293df3b0410
--- /dev/null
+++ b/drivers/input/joystick/gip/Kconfig
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Gaming Input Protocol  driver configuration
+#
+config JOYSTICK_XBOX_GIP
+	tristate "Xbox One/Series controller support"
+	depends on USB_ARCH_HAS_HCD
+	select USB
+	help
+	  Say Y here if you want to use Xbox One and Series controllers with your
+	  computer. Make sure to say Y to "Joystick support" (CONFIG_INPUT_JOYDEV)
+	  and/or "Event interface support" (CONFIG_INPUT_EVDEV) as well.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called xbox_gip.
+
+config JOYSTICK_XBOX_GIP_FF
+	bool "Xbox One/Series controller rumble support"
+	depends on JOYSTICK_XBOX_GIP && INPUT
+	select INPUT_FF_MEMLESS
+	help
+	  Say Y here if you want to take advantage of Xbox One/Series rumble.
+
+config JOYSTICK_XBOX_GIP_LEDS
+	bool "LED Support for the Xbox One/Series controller Guide button"
+	depends on JOYSTICK_XBOX_GIP && LEDS_CLASS_MULTICOLOR
+	help
+	  This option enables support for the LED which surrounds the Big X on
+	  Xbox One/Series controllers.
+
diff --git a/drivers/input/joystick/gip/Makefile b/drivers/input/joystick/gip/Makefile
new file mode 100644
index 0000000000000..a75e0cace0f92
--- /dev/null
+++ b/drivers/input/joystick/gip/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+obj-$(CONFIG_JOYSTICK_XBOX_GIP)	+= xbox-gip.o
+xbox-gip-y := gip-core.o gip-drivers.o
diff --git a/drivers/input/joystick/gip/gip-core.c b/drivers/input/joystick/gip/gip-core.c
new file mode 100644
index 0000000000000..0881797592fea
--- /dev/null
+++ b/drivers/input/joystick/gip/gip-core.c
@@ -0,0 +1,2536 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gaming Input Protocol driver for Xbox One/Series controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * TODO:
+ * - Audio device support
+ * - Security packet handshake
+ * - Event logging
+ * - Sending fragmented messages
+ * - Raw character device
+ * - Wheel support
+ * - Flight stick support
+ * - Arcade stick support
+ * - Split into driver-per-attachment GIP-as-a-bus approach drivers
+ *
+ * This driver is based on the Microsoft GIP spec at:
+ * https://aka.ms/gipdocs
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
+ */
+
+#include <linux/module.h>
+#include <linux/uuid.h>
+#include "gip.h"
+
+#define GIP_WIRED_INTF_DATA 0
+#define GIP_WIRED_INTF_AUDIO 1
+
+#define MAX_MESSAGE_LENGTH 0x4000
+
+#define MAX_AUDIO_MESSAGES 9
+
+#define GIP_DATA_CLASS_COMMAND		(0u << 5)
+#define GIP_DATA_CLASS_LOW_LATENCY	(1u << 5)
+#define GIP_DATA_CLASS_STANDARD_LATENCY	(2u << 5)
+#define GIP_DATA_CLASS_AUDIO		(3u << 5)
+
+#define GIP_DATA_CLASS_SHIFT 5
+#define GIP_DATA_CLASS_MASK (7u << 5)
+
+/* Undocumented Elite 2 vendor messages */
+#define GIP_CMD_RAW_REPORT		0x0c
+#define GIP_CMD_GUIDE_COLOR		0x0e
+#define GIP_SL_ELITE_CONFIG		0x4d
+
+#define GIP_BTN_OFFSET_XBE1 28
+#define GIP_BTN_OFFSET_XBE2 14
+
+#define GIP_FLAG_FRAGMENT	BIT(7)
+#define GIP_FLAG_INIT_FRAG	BIT(6)
+#define GIP_FLAG_SYSTEM		BIT(5)
+#define GIP_FLAG_ACME		BIT(4)
+#define GIP_FLAG_ATTACHMENT_MASK 0x7
+
+#define GIP_AUDIO_FORMAT_NULL		0
+#define GIP_AUDIO_FORMAT_8000HZ_1CH	1
+#define GIP_AUDIO_FORMAT_8000HZ_2CH	2
+#define GIP_AUDIO_FORMAT_12000HZ_1CH	3
+#define GIP_AUDIO_FORMAT_12000HZ_2CH	4
+#define GIP_AUDIO_FORMAT_16000HZ_1CH	5
+#define GIP_AUDIO_FORMAT_16000HZ_2CH	6
+#define GIP_AUDIO_FORMAT_20000HZ_1CH	7
+#define GIP_AUDIO_FORMAT_20000HZ_2CH	8
+#define GIP_AUDIO_FORMAT_24000HZ_1CH	9
+#define GIP_AUDIO_FORMAT_24000HZ_2CH	10
+#define GIP_AUDIO_FORMAT_32000HZ_1CH	11
+#define GIP_AUDIO_FORMAT_32000HZ_2CH	12
+#define GIP_AUDIO_FORMAT_40000HZ_1CH	13
+#define GIP_AUDIO_FORMAT_40000HZ_2CH	14
+#define GIP_AUDIO_FORMAT_48000HZ_1CH	15
+#define GIP_AUDIO_FORMAT_48000HZ_2CH	16
+#define GIP_AUDIO_FORMAT_48000HZ_6CH	32
+#define GIP_AUDIO_FORMAT_48000HZ_8CH	33
+#define MAX_GIP_AUDIO_FORMAT GIP_AUDIO_FORMAT_48000HZ_8CH
+
+/* Protocol Control constants */
+#define GIP_CONTROL_CODE_ACK	0
+#define GIP_CONTROL_CODE_NACK	1 /* obsolete */
+#define GIP_CONTROL_CODE_UNK	2 /* obsolete */
+#define GIP_CONTROL_CODE_AB	3 /* obsolete */
+#define GIP_CONTROL_CODE_MPER	4 /* obsolete */
+#define GIP_CONTROL_CODE_STOP	5 /* obsolete */
+#define GIP_CONTROL_CODE_START	6 /* obsolete */
+#define GIP_CONTROL_CODE_ERR	7 /* obsolete */
+
+/* Status Device constants */
+#define GIP_POWER_LEVEL_OFF	0
+#define GIP_POWER_LEVEL_STANDBY	1 /* obsolete */
+#define GIP_POWER_LEVEL_FULL	2
+
+#define GIP_NOT_CHARGING	0
+#define GIP_CHARGING		1
+#define GIP_CHARGE_ERROR	2
+
+#define GIP_BATTERY_ABSENT		0
+#define GIP_BATTERY_STANDARD		1
+#define GIP_BATTERY_RECHARGEABLE	2
+
+#define GIP_BATTERY_CRITICAL	0
+#define GIP_BATTERY_LOW		1
+#define GIP_BATTERY_MEDIUM	2
+#define GIP_BATTERY_FULL	3
+
+#define GIP_EVENT_FAULT 0x0002
+
+#define GIP_FAULT_UNKNOWN	0
+#define GIP_FAULT_HARD		1
+#define GIP_FAULT_NMI		2
+#define GIP_FAULT_SVC		3
+#define GIP_FAULT_PEND_SV	4
+#define GIP_FAULT_SMART_PTR	5
+#define GIP_FAULT_MCU		6
+#define GIP_FAULT_BUS		7
+#define GIP_FAULT_USAGE		8
+#define GIP_FAULT_RADIO_HANG	9
+#define GIP_FAULT_WATCHDOG	10
+#define GIP_FAULT_LINK_STALL	11
+#define GIP_FAULT_ASSERTION	12
+
+/* Metadata constants */
+#define GIP_MESSAGE_FLAG_BIG_ENDIAN		BIT(0)
+#define GIP_MESSAGE_FLAG_RELIABLE		BIT(1)
+#define GIP_MESSAGE_FLAG_SEQUENCED		BIT(2)
+#define GIP_MESSAGE_FLAG_DOWNSTREAM		BIT(3)
+#define GIP_MESSAGE_FLAG_UPSTREAM		BIT(4)
+#define GIP_MESSAGE_FLAG_DS_REQUEST_RESPONSE	BIT(5)
+
+#define GIP_DATA_TYPE_CUSTOM	1
+#define GIP_DATA_TYPE_AUDIO	2
+#define GIP_DATA_TYPE_SECURITY	3
+#define GIP_DATA_TYPE_GIP	4
+
+/* Set Device State constants */
+#define GIP_STATE_START		0
+#define GIP_STATE_STOP		1
+#define GIP_STATE_STANDBY	2 /* obsolete */
+#define GIP_STATE_FULL_POWER	3
+#define GIP_STATE_OFF		4
+#define GIP_STATE_QUIESCE	5
+#define GIP_STATE_UNK6		6
+#define GIP_STATE_RESET		7
+
+/* Guide Button Status constants */
+#define GIP_LED_GUIDE	0
+#define GIP_LED_IR	1 /* deprecated, for Kinect */
+
+#define GIP_LED_GUIDE_OFF		0
+#define GIP_LED_GUIDE_ON		1
+#define GIP_LED_GUIDE_FAST_BLINK	2
+#define GIP_LED_GUIDE_SLOW_BLINK	3
+#define GIP_LED_GUIDE_CHARGING_BLINK	4
+#define GIP_LED_GUIDE_RAMP_TO_LEVEL	0xd
+
+#define GIP_LED_IR_OFF		0
+#define GIP_LED_IR_ON_100MS	1
+#define GIP_LED_IR_PATTERN	4
+
+/* Direct Motor Command constants */
+#define GIP_MOTOR_RIGHT_VIBRATION	BIT(0)
+#define GIP_MOTOR_LEFT_VIBRATION	BIT(1)
+#define GIP_MOTOR_RIGHT_IMPULSE		BIT(2)
+#define GIP_MOTOR_LEFT_IMPULSE		BIT(3)
+#define GIP_MOTOR_ALL 0xf
+
+/* Extended Command constants */
+#define GIP_EXTCMD_GET_CAPABILITIES	0x00
+#define GIP_EXTCMD_GET_TELEMETRY_DATA	0x01
+#define GIP_EXTCMD_GET_SERIAL_NUMBER	0x04
+
+#define GIP_EXTENDED_STATUS_OK			0
+#define GIP_EXTENDED_STATUS_NOT_SUPPORTED	1
+#define GIP_EXTENDED_STATUS_NOT_READY		2
+#define GIP_EXTENDED_STATUS_ACCESS_DENIED	3
+#define GIP_EXTENDED_STATUS_FAILED		4
+
+/* Internal constants, not part of protocol */
+#define GIP_DEFAULT_IN_SYSTEM_MESSAGES 0x5e
+#define GIP_DEFAULT_OUT_SYSTEM_MESSAGES 0x472
+
+#define GIP_FEATURE_CONTROLLER				BIT(0)
+#define GIP_FEATURE_CONSOLE_FUNCTION_MAP		BIT(1)
+#define GIP_FEATURE_CONSOLE_FUNCTION_MAP_OVERFLOW	BIT(2)
+#define GIP_FEATURE_ELITE_BUTTONS			BIT(3)
+#define GIP_FEATURE_DYNAMIC_LATENCY_INPUT		BIT(4)
+#define GIP_FEATURE_SECURITY_OPT_OUT			BIT(5)
+#define GIP_FEATURE_MOTOR_CONTROL			BIT(6)
+#define GIP_FEATURE_GUIDE_COLOR				BIT(7)
+#define GIP_FEATURE_EXTENDED_SET_DEVICE_STATE		BIT(8)
+
+#define GIP_LED_GUIDE_MAX_BRIGHTNESS	100 /* Spec says 47, but larger values work */
+#define GIP_LED_GUIDE_INIT_BRIGHTNESS	20
+
+#ifndef VK_LWIN
+#define VK_LWIN 0x5b
+#endif
+
+static const guid_t guid_console_function_map =
+	GUID_INIT(0xecddd2fe, 0xd387, 0x4294, 0xbd, 0x96, 0x1a, 0x71, 0x2e, 0x3d, 0xc7, 0x7d);
+static const guid_t guid_console_function_map_overflow =
+	GUID_INIT(0x137d4bd0, 0x9347, 0x4472, 0xaa, 0x26, 0x8c, 0x34, 0xa0, 0x8f, 0xf9, 0xbd);
+static const guid_t guid_controller =
+	GUID_INIT(0x9776ff56, 0x9bfd, 0x4581, 0xad, 0x45, 0xb6, 0x45, 0xbb, 0xa5, 0x26, 0xd6);
+static const guid_t guid_dev_auth_pc_opt_out =
+	GUID_INIT(0x7a34ce77, 0x7de2, 0x45c6, 0x8c, 0xa4, 0x00, 0x42, 0xc0, 0x8b, 0xd9, 0x4a);
+static const guid_t guid_dynamic_latency_input =
+	GUID_INIT(0x87f2e56b, 0xc3bb, 0x49b1, 0x82, 0x65, 0xff, 0xff, 0xf3, 0x77, 0x99, 0xee);
+static const guid_t guid_elite_buttons =
+	GUID_INIT(0x37d19ff7, 0xb5c6, 0x49d1, 0xa7, 0x5e, 0x03, 0xb2, 0x4b, 0xef, 0x8c, 0x89);
+static const guid_t guid_headset =
+	GUID_INIT(0xbc25d1a3, 0xc24e, 0x4992, 0x9d, 0xda, 0xef, 0x4f, 0x12, 0x3e, 0xf5, 0xdc);
+
+/*
+ * The following GUIDs are observed, but the exact meanings aren't known, so
+ * for now we document them but don't use them anywhere.
+ *
+ * GamepadEmu: GUID_INIT(0xe2e5f1bc, 0xa6e6, 0x41a2, 0x8f, 0x43, 0x33, 0xcf, 0xa2, 0x51, 0x09, 0x81)
+ * IAudioOnly: GUID_INIT(0x92844cd1, 0xf7c8, 0x49ef, 0x97, 0x77, 0x46, 0x7d, 0xa7, 0x08, 0xad, 0x10)
+ * IControllerProfileModeState: GUID_INIT(0xf758dc66, 0x022c, 0x48b8, 0xa4, 0xf6, 0x45, 0x7b, 0xa8, 0x0e, 0x2a, 0x5b)
+ * ICustomAudio: GUID_INIT(0x63fd9cc9, 0x94ee, 0x4b5d, 0x9c, 0x4d, 0x8b, 0x86, 0x4c, 0x14, 0x9c, 0xac)
+ * IExtendedDeviceFlags: GUID_INIT(0x34ad9b1e, 0x36ad, 0x4fb5, 0x8a, 0xc7, 0x17, 0x23, 0x4c, 0x9f, 0x54, 0x6f)
+ * IProgrammableGamepad: GUID_INIT(0x31c1034d, 0xb5b7, 0x4551, 0x98, 0x13, 0x87, 0x69, 0xd4, 0xa0, 0xe4, 0xf9)
+ * IVirtualDevice: GUID_INIT(0xdfd26825, 0x110a, 0x4e94, 0xb9, 0x37, 0xb2, 0x7c, 0xe4, 0x7b, 0x25, 0x40)
+ * OnlineDevAuth: GUID_INIT(0x632b1fd1, 0xa3e9, 0x44f9, 0x84, 0x20, 0x5c, 0xe3, 0x44, 0xa0, 0x64, 0x04)
+ *
+ * Seen on Elite Controller, Adaptive Controller: 9ebd00a3-b5e6-4c08-a33b-673126459ec4
+ * Seen on Adaptive Controller: ce1e58c5-221c-4bdb-9c24-bf3941601320
+ * Seen on Adaptive Joystick: db02f681-5038-4219-8668-c3459c5c3293
+ * Seen on Elite 2 Controller: f758dc66-022c-48b8-a4f6-457ba80e2a5b (IControllerProfileModeState)
+ * Seen on Elite 2 Controller: 31c1034d-b5b7-4551-9813-8769d4a0e4f9 (IProgrammableGamepad)
+ * Seen on Elite 2 Controller: 34ad9b1e-36ad-4fb5-8ac7-17234c9f546f (IExtendedDeviceFlags)
+ * Seen on Elite 2 Controller: 88e0b694-6bd9-4416-a560-e7fafdfa528f
+ * Seen on Elite 2 Controller: ea96c8c0-b216-448b-be80-7e5deb0698e2
+ */
+
+static const int gip_data_class_mtu[8] = { 64, 64, 64, 2048, 0, 0, 0, 0 };
+
+struct gip_audio_format {
+	uint16_t rate;
+	uint8_t channels;
+};
+
+static const struct gip_audio_format gip_audio_format_table[MAX_GIP_AUDIO_FORMAT + 1] = {
+	[GIP_AUDIO_FORMAT_8000HZ_1CH] = { .rate = 8000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_8000HZ_2CH] = { .rate = 8000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_12000HZ_1CH] = { .rate = 12000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_12000HZ_2CH] = { .rate = 12000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_16000HZ_1CH] = { .rate = 16000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_16000HZ_2CH] = { .rate = 16000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_20000HZ_1CH] = { .rate = 20000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_20000HZ_2CH] = { .rate = 20000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_24000HZ_1CH] = { .rate = 24000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_24000HZ_2CH] = { .rate = 24000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_32000HZ_1CH] = { .rate = 32000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_32000HZ_2CH] = { .rate = 32000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_40000HZ_1CH] = { .rate = 40000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_40000HZ_2CH] = { .rate = 40000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_48000HZ_1CH] = { .rate = 48000, .channels = 1 },
+	[GIP_AUDIO_FORMAT_48000HZ_2CH] = { .rate = 48000, .channels = 2 },
+	[GIP_AUDIO_FORMAT_48000HZ_6CH] = { .rate = 48000, .channels = 6 },
+	[GIP_AUDIO_FORMAT_48000HZ_8CH] = { .rate = 48000, .channels = 8 },
+};
+
+
+static const struct gip_quirks base_quirks[] = {
+	/* PDP Rock Candy */
+	{ 0x0e6f, 0x0246, 0, .quirks = GIP_QUIRK_NO_HELLO },
+
+	{0},
+};
+
+struct gip_audio_format_pair {
+	uint8_t inbound;
+	uint8_t outbound;
+};
+static_assert(sizeof(struct gip_audio_format_pair) == 2);
+
+struct gip_hello_device {
+	uint64_t device_id;
+	uint16_t vendor_id;
+	uint16_t product_id;
+	uint16_t firmware_major_version;
+	uint16_t firmware_minor_version;
+	uint16_t firmware_build_version;
+	uint16_t firmware_revision;
+	uint8_t hardware_major_version;
+	uint8_t hardware_minor_version;
+	uint8_t rf_proto_major_version;
+	uint8_t rf_proto_minor_version;
+	uint8_t security_major_version;
+	uint8_t security_minor_version;
+	uint8_t gip_major_version;
+	uint8_t gip_minor_version;
+};
+
+struct gip_direct_motor {
+	uint8_t command;
+	uint8_t motor_bitmap;
+	uint8_t left_impulse_level;
+	uint8_t right_impulse_level;
+	uint8_t left_vibration_level;
+	uint8_t right_vibration_level;
+	uint8_t duration;
+	uint8_t delay;
+	uint8_t repeat;
+};
+
+static const struct gip_driver* base_drivers[] = {
+	&gip_driver_navigation,
+	&gip_driver_gamepad,
+	NULL /* Sentinel */
+};
+
+static int gip_decode_length(uint64_t *length, const uint8_t *bytes, int num_bytes)
+{
+	*length = 0;
+	int offset;
+
+	for (offset = 0; offset < num_bytes; offset++) {
+		uint8_t byte = bytes[offset];
+
+		*length |= (byte & 0x7full) << (offset * 7);
+		if (!(byte & 0x80)) {
+			offset++;
+			break;
+		}
+	}
+	return offset;
+}
+
+static int gip_encode_length(uint64_t length, uint8_t *bytes, int num_bytes)
+{
+	int offset;
+
+	for (offset = 0; offset < num_bytes; offset++) {
+		uint8_t byte = length & 0x7f;
+
+		length >>= 7;
+		if (length)
+			byte |= 0x80;
+		bytes[offset] = byte;
+		if (!length) {
+			offset++;
+			break;
+		}
+	}
+	return offset;
+}
+
+static bool gip_supports_system_message(struct gip_attachment *attachment,
+	uint8_t command, bool upstream)
+{
+	if (upstream)
+		return attachment->metadata.device
+			.in_system_messages[command >> 5] & (1u << command);
+	else
+		return attachment->metadata.device
+			.out_system_messages[command >> 5] & (1u << command);
+}
+
+bool gip_supports_vendor_message(struct gip_attachment *attachment,
+	uint8_t command, bool upstream)
+{
+	size_t i;
+
+	for (i = 0; i < attachment->metadata.num_messages; i++) {
+		struct gip_message_metadata *metadata =
+			&attachment->metadata.message_metadata[i];
+
+		if (metadata->type != command)
+			continue;
+		if (metadata->flags & GIP_MESSAGE_FLAG_DS_REQUEST_RESPONSE)
+			return true;
+
+		if (upstream)
+			return metadata->flags & GIP_MESSAGE_FLAG_UPSTREAM;
+		else
+			return metadata->flags & GIP_MESSAGE_FLAG_DOWNSTREAM;
+	}
+	return false;
+}
+
+static uint8_t gip_sequence_next(struct gip_attachment *attachment,
+	uint8_t command, bool system)
+{
+	uint8_t seq;
+
+	if (system) {
+		switch (command) {
+		case GIP_CMD_SECURITY:
+			seq = attachment->seq_security++;
+			if (!seq)
+				seq = attachment->seq_security++;
+			break;
+		case GIP_CMD_EXTENDED:
+			seq = attachment->seq_extended++;
+			if (!seq)
+				seq = attachment->seq_extended++;
+			break;
+		case GIP_AUDIO_DATA:
+			seq = attachment->seq_audio++;
+			if (!seq)
+				seq = attachment->seq_audio++;
+			break;
+		default:
+			seq = attachment->seq_system++;
+			if (!seq)
+				seq = attachment->seq_system++;
+			break;
+		}
+	} else {
+		seq = attachment->seq_vendor++;
+		if (!seq)
+			seq = attachment->seq_vendor++;
+	}
+	return seq;
+}
+
+static void gip_handle_quirks_array(struct gip_attachment *attachment,
+	const struct gip_quirks *quirks)
+{
+	size_t i, j;
+
+	for (i = 0; quirks[i].vendor_id; i++) {
+		if (quirks[i].vendor_id != attachment->vendor_id)
+			continue;
+		if (quirks[i].product_id != attachment->product_id)
+			continue;
+		if (quirks[i].attachment_index != attachment->attachment_index)
+			continue;
+
+		attachment->features |= quirks[i].added_features;
+		attachment->features &= ~quirks[i].filtered_features;
+		attachment->quirks |= quirks[i].quirks;
+
+		if (quirks[i].override_name)
+			attachment->name = quirks[i].override_name;
+
+		for (j = 0; j < 8; ++j) {
+			struct gip_device_metadata *metadata = &attachment->metadata.device;
+
+			metadata->in_system_messages[j] |= quirks[i].extra_in_system[j];
+			metadata->out_system_messages[j] |= quirks[i].extra_out_system[j];
+		}
+
+		attachment->extra_buttons = quirks[i].extra_buttons;
+		attachment->extra_axes = quirks[i].extra_axes;
+		break;
+	}
+
+}
+
+static void gip_handle_quirks(struct gip_attachment *attachment)
+{
+	gip_handle_quirks_array(attachment, base_quirks);
+
+	if (attachment->driver && attachment->driver->quirks)
+		gip_handle_quirks_array(attachment, attachment->driver->quirks);
+}
+
+static int gip_send_raw_message(struct gip_device *device,
+	uint8_t message_type, uint8_t flags, uint8_t seq, const uint8_t *bytes,
+	int num_bytes)
+{
+	struct gip_interface *intf;
+	int offset = 3;
+	struct gip_urb *urb = NULL;
+	int i;
+	int rc = 0;
+
+	if (num_bytes < 0) {
+		dev_warn(GIP_DEV(device), "Invalid message length %d\n", num_bytes);
+		return -EINVAL;
+	}
+
+	if (num_bytes > gip_data_class_mtu[message_type >> GIP_DATA_CLASS_SHIFT]) {
+		dev_err(GIP_DEV(device),
+			"Attempted to send a message that requires fragmenting, which is not yet supported.\n");
+		return -ENOTSUPP;
+	}
+
+	if ((message_type & GIP_DATA_CLASS_MASK) == GIP_DATA_CLASS_AUDIO)
+		intf = &device->audio;
+	else
+		intf = &device->data;
+
+	if (intf->isoc_messages) {
+		/* TODO: Needed for audio support */
+		dev_warn(GIP_DEV(intf), "Unimplemented isochronous message output\n");
+		return -ENOTSUPP;
+	}
+
+	guard(spinlock_irqsave)(&device->message_lock);
+	for (i = 0; i < MAX_OUT_MESSAGES && !urb; i++) {
+		if (!intf->out_queue[i].urb)
+			continue;
+		if (!intf->out_queue[i].urb->anchor)
+			urb = &intf->out_queue[i];
+	}
+	if (!urb) {
+		dev_err(GIP_DEV(device), "Output queue is full; dropping message\n");
+		return -ENOSPC;
+	}
+	urb->data[0] = message_type;
+	urb->data[1] = flags;
+	urb->data[2] = seq;
+	offset += gip_encode_length(num_bytes, &urb->data[offset],
+		sizeof(urb->data) - offset);
+
+	if (num_bytes > 0)
+		memcpy(&urb->data[offset], bytes, num_bytes);
+
+	num_bytes += offset;
+	urb->urb->transfer_buffer_length = num_bytes;
+
+	print_hex_dump_debug(KBUILD_MODNAME ": Sending message: ",
+		DUMP_PREFIX_OFFSET, 16, 1, urb->data, num_bytes,
+		false);
+
+	usb_anchor_urb(urb->urb, &intf->out_anchor);
+	rc = usb_submit_urb(urb->urb, GFP_ATOMIC);
+	if (rc) {
+		dev_err(&intf->intf->dev,
+			"%s - usb_submit_urb failed with result %d\n",
+			__func__, rc);
+		usb_unanchor_urb(urb->urb);
+		rc = -EIO;
+	}
+
+	return rc;
+}
+
+int gip_send_system_message(struct gip_attachment *attachment,
+	uint8_t message_type, uint8_t flags, const void *bytes, int num_bytes)
+{
+	return gip_send_raw_message(attachment->device, message_type,
+		GIP_FLAG_SYSTEM | attachment->attachment_index | flags,
+		gip_sequence_next(attachment, message_type, true),
+		bytes, num_bytes);
+}
+
+int gip_send_vendor_message(struct gip_attachment *attachment,
+	uint8_t message_type, uint8_t flags, const void *bytes, int num_bytes)
+{
+	return gip_send_raw_message(attachment->device, message_type, flags,
+		gip_sequence_next(attachment, message_type, false),
+		bytes, num_bytes);
+}
+
+static void gip_metadata_free(struct device *dev, struct gip_metadata *metadata)
+{
+	devm_kfree(dev, metadata->device.audio_formats);
+
+	if (metadata->device.preferred_types) {
+		int i;
+
+		for (i = 0; i < metadata->device.num_preferred_types; i++)
+			devm_kfree(dev, metadata->device.preferred_types[i]);
+		devm_kfree(dev, metadata->device.preferred_types);
+	}
+	devm_kfree(dev, metadata->device.supported_interfaces);
+	devm_kfree(dev, metadata->device.hid_descriptor);
+	devm_kfree(dev, metadata->message_metadata);
+
+	memset(metadata, 0, sizeof(*metadata));
+}
+
+static int gip_parse_audio_format_metadata(struct device *dev,
+	struct gip_device_metadata *dev_metadata, const uint8_t *bytes,
+	int length, int buffer_offset)
+{
+	unsigned int i;
+
+	dev_metadata->num_audio_formats = bytes[buffer_offset];
+	if (buffer_offset + dev_metadata->num_audio_formats * 2 + 1 > length)
+		return -EINVAL;
+	dev_metadata->audio_formats = devm_kmalloc_array(dev,
+		dev_metadata->num_audio_formats, 2, GFP_KERNEL);
+	if (!dev_metadata->audio_formats)
+		return -ENOMEM;
+	memcpy(dev_metadata->audio_formats, &bytes[buffer_offset + 1],
+		dev_metadata->num_audio_formats * 2);
+
+	for (i = 0; i < dev_metadata->num_audio_formats; i++) {
+		const struct gip_audio_format_pair *pair = &dev_metadata->audio_formats[i];
+		const struct gip_audio_format *inbound = NULL;
+		const struct gip_audio_format *outbound = NULL;
+
+		if (pair->inbound <= MAX_GIP_AUDIO_FORMAT) {
+			inbound = &gip_audio_format_table[pair->inbound];
+			if (pair->inbound != GIP_AUDIO_FORMAT_NULL && inbound->rate == 0)
+				inbound = NULL;
+		}
+		if (!inbound)
+			dev_warn(dev, "Unknown audio format %u\n", pair->inbound);
+
+		if (pair->outbound <= MAX_GIP_AUDIO_FORMAT) {
+			outbound = &gip_audio_format_table[pair->outbound];
+			if (pair->outbound != GIP_AUDIO_FORMAT_NULL && outbound->rate == 0)
+				outbound = NULL;
+		}
+		if (!outbound)
+			dev_warn(dev, "Unknown audio format %u\n", pair->outbound);
+
+		if (inbound && outbound)
+			dev_dbg(dev,
+				"Supported audio format: %uHz %uch inbound, %uHz %uch outbound\n",
+				inbound->rate,
+				inbound->channels,
+				outbound->rate,
+				outbound->channels);
+	}
+	return 0;
+}
+
+static int gip_parse_preferred_types_metadata(struct device *dev,
+	struct gip_device_metadata *dev_metadata, const uint8_t *bytes,
+	int length, int buffer_offset)
+{
+	int i;
+	int count;
+
+	dev_metadata->num_preferred_types = bytes[buffer_offset];
+	dev_metadata->preferred_types = devm_kcalloc(dev,
+		dev_metadata->num_preferred_types, sizeof(char *), GFP_KERNEL);
+	if (!dev_metadata->preferred_types)
+		return -ENOMEM;
+
+	buffer_offset++;
+	for (i = 0; i < dev_metadata->num_preferred_types; i++) {
+		if (buffer_offset + 2 >= length)
+			return -EINVAL;
+
+		count = bytes[buffer_offset];
+		count |= bytes[buffer_offset];
+		buffer_offset += 2;
+		if (buffer_offset + count > length)
+			return -EINVAL;
+
+		dev_metadata->preferred_types[i] = devm_kcalloc(dev, count + 1,
+			sizeof(char), GFP_KERNEL);
+		if (!dev_metadata->preferred_types[i])
+			return -ENOMEM;
+		memcpy(dev_metadata->preferred_types[i], &bytes[buffer_offset], count);
+		buffer_offset += count;
+	}
+
+	return 0;
+}
+
+static int gip_parse_supported_interfaces_metadata(struct device *dev,
+	struct gip_device_metadata *dev_metadata, const uint8_t *bytes,
+	int length, int buffer_offset)
+{
+	dev_metadata->num_supported_interfaces = bytes[buffer_offset];
+	if (buffer_offset + 1 +
+		(int32_t) (dev_metadata->num_supported_interfaces * sizeof(guid_t)) > length)
+		return -EINVAL;
+
+	dev_metadata->supported_interfaces = devm_kmalloc_array(dev,
+		dev_metadata->num_supported_interfaces, sizeof(guid_t), GFP_KERNEL);
+	if (!dev_metadata->supported_interfaces)
+		return -ENOMEM;
+
+	memcpy(dev_metadata->supported_interfaces, &bytes[buffer_offset + 1],
+		sizeof(guid_t) * dev_metadata->num_supported_interfaces);
+
+	return 0;
+}
+
+static int gip_parse_hid_descriptor_metadata(struct device *dev,
+	struct gip_device_metadata *dev_metadata, const uint8_t *bytes,
+	int length, int buffer_offset)
+{
+	dev_metadata->hid_descriptor_size = bytes[buffer_offset];
+	if (buffer_offset + 1 + dev_metadata->hid_descriptor_size > length)
+		return -EINVAL;
+
+	dev_metadata->hid_descriptor = devm_kmalloc(dev,
+		dev_metadata->hid_descriptor_size, GFP_KERNEL);
+	if (!dev_metadata->hid_descriptor)
+		return -ENOMEM;
+
+	memcpy(dev_metadata->hid_descriptor, &bytes[buffer_offset + 1],
+		dev_metadata->hid_descriptor_size);
+	print_hex_dump_debug(KBUILD_MODNAME ": Received HID descriptor: ",
+		DUMP_PREFIX_OFFSET, 16, 1, dev_metadata->hid_descriptor,
+		dev_metadata->hid_descriptor_size, false);
+
+	return 0;
+}
+
+static int gip_parse_device_metadata(struct device *dev,
+	struct gip_metadata *metadata, const uint8_t *bytes, int num_bytes,
+	int *offset)
+{
+	struct gip_device_metadata *dev_metadata = &metadata->device;
+	int buffer_offset;
+	int count;
+	int length;
+	int i;
+	int rc;
+
+	bytes = &bytes[*offset];
+	num_bytes -= *offset;
+	if (num_bytes < 16)
+		return -EINVAL;
+
+	length = bytes[0];
+	length |= bytes[1] << 8;
+	if (num_bytes < length)
+		return -EINVAL;
+
+	/* Skip supported firmware versions for now */
+
+	buffer_offset = bytes[4];
+	buffer_offset |= bytes[5] << 8;
+	if (buffer_offset >= length)
+		return -EINVAL;
+
+	if (buffer_offset > 0) {
+		rc = gip_parse_audio_format_metadata(dev, dev_metadata,
+			bytes, length, buffer_offset);
+		if (rc)
+			return rc;
+	}
+
+	buffer_offset = bytes[6];
+	buffer_offset |= bytes[7] << 8;
+	if (buffer_offset >= length)
+		return -EINVAL;
+
+	if (buffer_offset > 0) {
+		count = bytes[buffer_offset];
+		if (buffer_offset + count + 1 > length)
+			return -EINVAL;
+
+		for (i = 0; i < count; i++) {
+			uint8_t message = bytes[buffer_offset + 1 + i];
+
+			dev_dbg(dev,
+				"Supported upstream system message %02x\n",
+				message);
+			dev_metadata->in_system_messages[message >> 5] |=
+				BIT(message & 0x1F);
+		}
+	}
+
+	buffer_offset = bytes[8];
+	buffer_offset |= bytes[9] << 8;
+	if (buffer_offset >= length)
+		return -EINVAL;
+
+	if (buffer_offset > 0) {
+		count = bytes[buffer_offset];
+		if (buffer_offset + count + 1 > length)
+			return -EINVAL;
+
+		for (i = 0; i < count; i++) {
+			uint8_t message = bytes[buffer_offset + 1 + i];
+
+			dev_dbg(dev,
+				"Supported downstream system message %02x\n",
+				message);
+			dev_metadata->out_system_messages[message >> 5] |=
+				BIT(message & 0x1F);
+		}
+	}
+
+	buffer_offset = bytes[10];
+	buffer_offset |= bytes[11] << 8;
+	if (buffer_offset >= length)
+		return -EINVAL;
+
+	if (buffer_offset > 0) {
+		rc = gip_parse_preferred_types_metadata(dev, dev_metadata,
+			bytes, length, buffer_offset);
+		if (rc)
+			return rc;
+	}
+
+	buffer_offset = bytes[12];
+	buffer_offset |= bytes[13] << 8;
+	if (buffer_offset >= length)
+		return -EINVAL;
+
+	if (buffer_offset > 0) {
+		rc = gip_parse_supported_interfaces_metadata(dev,
+			dev_metadata, bytes, length, buffer_offset);
+		if (rc)
+			return rc;
+	}
+
+	if (metadata->version_major > 1 || metadata->version_minor >= 1) {
+		/* HID descriptor support added in metadata version 1.1 */
+		buffer_offset = bytes[14];
+		buffer_offset |= bytes[15] << 8;
+		if (buffer_offset >= length)
+			return -EINVAL;
+
+		if (buffer_offset > 0) {
+			rc = gip_parse_hid_descriptor_metadata(dev,
+				dev_metadata, bytes, length, buffer_offset);
+			if (rc)
+				return rc;
+		}
+	}
+
+	*offset += length;
+	return 0;
+}
+
+static int gip_parse_message_metadata(struct device *dev,
+	struct gip_message_metadata *metadata, const uint8_t *bytes,
+	int num_bytes, int *offset)
+{
+	uint16_t length;
+
+	bytes = &bytes[*offset];
+	num_bytes -= *offset;
+
+	if (num_bytes < 2)
+		return -EINVAL;
+
+	length = bytes[0];
+	length |= bytes[1] << 8;
+	if (num_bytes < length)
+		return -EINVAL;
+
+	if (length < 15)
+		return -EINVAL;
+
+	metadata->type = bytes[2];
+	metadata->length = bytes[3];
+	metadata->length |= bytes[4] << 8;
+	metadata->data_type = bytes[5];
+	metadata->data_type |= bytes[6] << 8;
+	metadata->flags = bytes[7];
+	metadata->flags |= bytes[8] << 8;
+	metadata->flags |= bytes[9] << 16;
+	metadata->flags |= bytes[10] << 24;
+	metadata->period = bytes[11];
+	metadata->period |= bytes[12] << 8;
+	metadata->persistence_timeout = bytes[13];
+	metadata->persistence_timeout |= bytes[14] << 8;
+
+	dev_dbg(dev,
+		"Supported vendor message type %02x of length %d, %s, %s, %s\n",
+		metadata->type, metadata->length,
+		metadata->flags & GIP_MESSAGE_FLAG_UPSTREAM ?
+			(metadata->flags & GIP_MESSAGE_FLAG_DOWNSTREAM ? "bidirectional" : "upstream") :
+			metadata->flags & GIP_MESSAGE_FLAG_DOWNSTREAM ? "downstream" :
+			metadata->flags & GIP_MESSAGE_FLAG_DS_REQUEST_RESPONSE ? "downstream request response" :
+			"unknown direction",
+		metadata->flags & GIP_MESSAGE_FLAG_SEQUENCED ? "sequenced" : "not sequenced",
+		metadata->flags & GIP_MESSAGE_FLAG_RELIABLE ? "reliable" : "unreliable");
+
+	*offset += length;
+	return 0;
+}
+
+static bool gip_parse_metadata(struct device *dev,
+	struct gip_metadata *metadata, const uint8_t *bytes, int num_bytes)
+{
+	int header_size;
+	int metadata_size;
+	int offset = 0;
+	int i;
+	int rc;
+
+	if (num_bytes < 16)
+		return -EINVAL;
+
+	print_hex_dump_debug(KBUILD_MODNAME ": Received metadata: ", DUMP_PREFIX_OFFSET,
+		16, 1, bytes, num_bytes, false);
+
+	header_size = bytes[0];
+	header_size |= bytes[1] << 8;
+	if (num_bytes < header_size || header_size < 16)
+		return -EINVAL;
+
+	metadata->version_major = bytes[2];
+	metadata->version_major |= bytes[3] << 8;
+	metadata->version_minor = bytes[4];
+	metadata->version_minor |= bytes[5] << 8;
+	/* Middle bytes are reserved */
+	metadata_size = bytes[14];
+	metadata_size |= bytes[15] << 8;
+
+	if (num_bytes < metadata_size || metadata_size < header_size)
+		return -EINVAL;
+
+	offset = header_size;
+
+	rc = gip_parse_device_metadata(dev, metadata, bytes, num_bytes, &offset);
+	if (rc)
+		goto parse_err;
+
+	if (offset >= num_bytes)
+		goto parse_err;
+
+	metadata->num_messages = bytes[offset];
+	offset++;
+	if (metadata->num_messages > 0) {
+		metadata->message_metadata = devm_kcalloc(dev,
+			metadata->num_messages,
+			sizeof(*metadata->message_metadata), GFP_KERNEL);
+		if (!metadata->message_metadata)
+			return -ENOMEM;
+
+		for (i = 0; i < metadata->num_messages; i++) {
+			rc = gip_parse_message_metadata(dev,
+				&metadata->message_metadata[i], bytes,
+				num_bytes, &offset);
+			if (rc)
+				goto parse_err;
+		}
+	}
+
+	return 0;
+
+parse_err:
+	gip_metadata_free(dev, metadata);
+	return rc;
+}
+
+static int gip_acknowledge(struct gip_device *device,
+	const struct gip_header *header, uint32_t fragment_offset,
+	uint16_t bytes_remaining)
+{
+	uint8_t buffer[] = {
+		GIP_CONTROL_CODE_ACK,
+		header->message_type,
+		header->flags & GIP_FLAG_SYSTEM,
+		fragment_offset,
+		fragment_offset >> 8,
+		fragment_offset >> 16,
+		fragment_offset >> 24,
+		bytes_remaining,
+		bytes_remaining >> 8,
+	};
+
+	return gip_send_raw_message(device, GIP_CMD_PROTO_CONTROL,
+		GIP_FLAG_SYSTEM | (header->flags & GIP_FLAG_ATTACHMENT_MASK),
+		header->sequence_id, buffer, sizeof(buffer));
+}
+
+static int gip_fragment_failed(struct gip_attachment *attachment,
+	const struct gip_header *header)
+{
+	attachment->fragment_retries++;
+	if (attachment->fragment_retries > 8) {
+		devm_kfree(GIP_DEV(attachment), attachment->fragment_data);
+		attachment->fragment_data = NULL;
+		attachment->fragment_message = 0;
+	}
+	return gip_acknowledge(attachment->device, header,
+		attachment->fragment_offset,
+		attachment->total_length - attachment->fragment_offset);
+}
+
+static int gip_bind_driver(struct gip_attachment *attachment, const struct gip_driver *driver)
+{
+	if (driver->probe) {
+		int rc = driver->probe(attachment);
+
+		if (rc)
+			return rc;
+	}
+
+	attachment->driver = driver;
+	memcpy(attachment->vendor_handlers, driver->vendor_handlers,
+		sizeof(attachment->vendor_handlers));
+	return 0;
+}
+
+static int gip_enable_elite_buttons(struct gip_attachment *attachment)
+{
+	if (attachment->vendor_id == 0x045e) {
+		if (attachment->product_id == 0x02e3) {
+			attachment->xbe_format = GIP_BTN_FMT_XBE1;
+		} else if (attachment->product_id == 0x0b00) {
+			if (attachment->firmware_major_version == 4) {
+				attachment->xbe_format = GIP_BTN_FMT_XBE2_4;
+			} else if (attachment->firmware_major_version == 5) {
+				/*
+				 * The exact range for this being necessary is
+				 * unknown, but it starts at 5.11 and at either
+				 * 5.16 or 5.17. This approach still works on
+				 * 5.21, even if it's not necessary, so having
+				 * a loose upper limit is fine.
+				 */
+				if (attachment->firmware_minor_version >= 11 &&
+					attachment->firmware_minor_version < 17)
+					attachment->xbe_format = GIP_BTN_FMT_XBE2_RAW;
+				else
+					attachment->xbe_format = GIP_BTN_FMT_XBE2_5;
+			}
+		}
+	}
+
+	if (attachment->xbe_format == GIP_BTN_FMT_XBE2_RAW) {
+		/*
+		 * The meaning of this packet is unknown and not documented, but
+		 * it's needed for the Elite 2 controller to send raw reports
+		 */
+		static const uint8_t enable_raw_report[] = { 7, 0 };
+
+		return gip_send_vendor_message(attachment, GIP_SL_ELITE_CONFIG,
+			0, enable_raw_report, sizeof(enable_raw_report));
+	}
+
+	return 0;
+}
+
+#ifdef CONFIG_JOYSTICK_XBOX_GIP_FF
+static int gip_play_effect(struct input_dev *dev, void *data, struct ff_effect *effect)
+{
+	struct gip_attachment *attachment = input_get_drvdata(dev);
+	struct gip_direct_motor control = {
+		.motor_bitmap = GIP_MOTOR_LEFT_VIBRATION | GIP_MOTOR_RIGHT_VIBRATION
+	};
+
+	if (effect->type != FF_RUMBLE)
+		return 0;
+
+	control.left_vibration_level = effect->u.rumble.strong_magnitude * 100 / 0xFFFF;
+	control.right_vibration_level = effect->u.rumble.weak_magnitude * 100 / 0xFFFF;
+	control.duration = 255;
+
+	return gip_send_vendor_message(attachment, GIP_CMD_DIRECT_MOTOR,
+		0, &control, sizeof(control));
+}
+#endif
+
+static int gip_send_guide_button_led(struct gip_attachment *attachment,
+	uint8_t pattern, uint8_t intensity)
+{
+	uint8_t buffer[] = {
+		GIP_LED_GUIDE,
+		pattern,
+		intensity,
+	};
+
+	if (!gip_supports_system_message(attachment, GIP_CMD_LED, false))
+		return 0;
+
+	return gip_send_system_message(attachment, GIP_CMD_LED, 0, buffer, sizeof(buffer));
+}
+
+static bool gip_send_set_device_state(struct gip_attachment *attachment, uint8_t state)
+{
+	uint8_t buffer[] = { state };
+
+	return gip_send_system_message(attachment, GIP_CMD_SET_DEVICE_STATE,
+		attachment->attachment_index, buffer, sizeof(buffer));
+}
+
+static int gip_handle_command_raw_report(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct input_dev *input;
+
+	if (num_bytes < 17) {
+		dev_dbg(GIP_DEV(attachment), "Discarding too-short raw report\n");
+		return -EINVAL;
+	}
+	guard(rcu)();
+	input = rcu_dereference(attachment->input);
+	if (!input)
+		return -ENODEV;
+
+	if ((attachment->features & GIP_FEATURE_ELITE_BUTTONS)
+		&& attachment->xbe_format == GIP_BTN_FMT_XBE2_RAW) {
+		input_report_abs(input, ABS_PROFILE, bytes[15] & 3);
+		if (bytes[15] & 3) {
+			input_report_key(input, BTN_GRIPL, 0);
+			input_report_key(input, BTN_GRIPR, 0);
+			input_report_key(input, BTN_GRIPL2, 0);
+			input_report_key(input, BTN_GRIPR2, 0);
+		} else {
+			input_report_key(input, BTN_GRIPL,
+				bytes[GIP_BTN_OFFSET_XBE2] & BIT(2));
+			input_report_key(input, BTN_GRIPR,
+				bytes[GIP_BTN_OFFSET_XBE2] & BIT(0));
+			input_report_key(input, BTN_GRIPL2,
+				bytes[GIP_BTN_OFFSET_XBE2] & BIT(3));
+			input_report_key(input, BTN_GRIPR2,
+				bytes[GIP_BTN_OFFSET_XBE2] & BIT(1));
+		}
+
+		input_sync(input);
+	}
+	return 0;
+}
+
+static int gip_setup_input_device(struct gip_attachment *attachment)
+{
+	struct input_dev *input;
+	int rc;
+
+	if (!attachment->driver || !attachment->driver->setup_input)
+		return -ENODEV;
+
+	rcu_read_lock();
+	input = rcu_dereference(attachment->input);
+	rcu_read_unlock();
+	if (input)
+		return 0;
+
+	input = input_allocate_device();
+	if (!input)
+		return -ENOMEM;
+	input->id.bustype = BUS_USB;
+	input->id.vendor = attachment->vendor_id;
+	input->id.product = attachment->product_id;
+	input->uniq = attachment->uniq;
+	if (attachment->name)
+		input->name = attachment->name;
+	else if (attachment->attachment_index == 0)
+		input->name = attachment->device->udev->product;
+	input->phys = attachment->phys;
+
+	rc = attachment->driver->setup_input(attachment, input);
+	if (rc < 0)
+		goto err_free_device;
+
+	if (attachment->features & GIP_FEATURE_CONSOLE_FUNCTION_MAP)
+		input_set_capability(input, EV_KEY, KEY_RECORD);
+
+	if (attachment->features & GIP_FEATURE_ELITE_BUTTONS) {
+		input_set_capability(input, EV_KEY, BTN_GRIPL);
+		input_set_capability(input, EV_KEY, BTN_GRIPR);
+		input_set_capability(input, EV_KEY, BTN_GRIPL2);
+		input_set_capability(input, EV_KEY, BTN_GRIPR2);
+		if (attachment->xbe_format == GIP_BTN_FMT_XBE1)
+			input_set_abs_params(input, ABS_PROFILE, 0, 1, 0, 0);
+		else
+			input_set_abs_params(input, ABS_PROFILE, 0, 3, 0, 0);
+
+		attachment->vendor_handlers[GIP_CMD_RAW_REPORT] = gip_handle_command_raw_report;
+	}
+
+#ifdef CONFIG_JOYSTICK_XBOX_GIP_FF
+	if (attachment->features & GIP_FEATURE_MOTOR_CONTROL) {
+		input_set_capability(input, EV_FF, FF_RUMBLE);
+		input_ff_create_memless(input, NULL, gip_play_effect);
+	}
+#endif
+
+	input_set_drvdata(input, attachment);
+	rcu_assign_pointer(attachment->input, input);
+	rc = input_register_device(input);
+	if (rc)
+		goto err_free_device;
+
+	return 0;
+
+err_free_device:
+	input_free_device(input);
+	return rc;
+}
+
+static int gip_send_init_sequence(struct gip_attachment *attachment)
+{
+	int rc = 0;
+	size_t len;
+
+	if (attachment->features & GIP_FEATURE_EXTENDED_SET_DEVICE_STATE) {
+		/*
+		 * The meaning of this packet is unknown and not documented, but it's
+		 * needed for the Elite 2 controller to start up on older firmwares
+		 */
+		static const uint8_t set_device_state[] = {
+			GIP_STATE_UNK6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+			0x55, 0x53, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0
+		};
+
+		rc = gip_send_system_message(attachment,
+			GIP_CMD_SET_DEVICE_STATE, 0, set_device_state,
+			sizeof(set_device_state));
+		if (rc)
+			return rc;
+	}
+	rc = gip_enable_elite_buttons(attachment);
+	if (rc)
+		return rc;
+	if (!gip_supports_system_message(attachment, GIP_CMD_AUDIO_CONTROL, false)) {
+		rc = gip_send_set_device_state(attachment, GIP_STATE_START);
+		if (rc)
+			return rc;
+		attachment->device_state = GIP_STATE_START;
+	} else {
+		rc = gip_send_set_device_state(attachment, GIP_STATE_STOP);
+		if (rc)
+			return rc;
+		attachment->device_state = GIP_STATE_STOP;
+	}
+
+	rc = gip_send_guide_button_led(attachment,
+		GIP_LED_GUIDE_ON,
+		GIP_LED_GUIDE_INIT_BRIGHTNESS);
+	if (rc)
+		return rc;
+
+	if (gip_supports_system_message(attachment, GIP_CMD_SECURITY, false)
+		&& !(attachment->features & GIP_FEATURE_SECURITY_OPT_OUT)) {
+		/* TODO: Implement Security command property */
+		uint8_t buffer[] = { 0x1, 0x0 };
+
+		rc = gip_send_system_message(attachment, GIP_CMD_SECURITY, 0,
+			buffer, sizeof(buffer));
+		if (rc)
+			return rc;
+	}
+
+	usb_make_path(attachment->device->udev, attachment->phys,
+		sizeof(attachment->phys));
+	len = strlen(attachment->phys);
+	if (len < sizeof(attachment->phys) - 1)
+		snprintf(attachment->phys + len,
+			sizeof(attachment->phys) - len, "/input%d",
+			attachment->attachment_index);
+
+	if (attachment->driver && attachment->driver->init) {
+		rc = attachment->driver->init(attachment);
+		if (rc < 0)
+			return rc;
+	}
+
+	if (rc != GIP_INIT_NO_INPUT && (attachment->features & GIP_FEATURE_CONTROLLER)) {
+		rc = gip_setup_input_device(attachment);
+		if (rc == -ENODEV)
+			return 0;
+		if (rc)
+			return rc;
+	}
+
+	return 0;
+}
+
+static void gip_fragment_timeout(struct work_struct *work)
+{
+	struct gip_attachment *attachment = container_of(to_delayed_work(work),
+		struct gip_attachment, fragment_timeout);
+
+	guard(mutex)(&attachment->lock);
+	devm_kfree(GIP_DEV(attachment), attachment->fragment_data);
+	attachment->fragment_data = NULL;
+	attachment->fragment_message = 0;
+}
+
+static void gip_retry_metadata(struct work_struct *work)
+{
+	struct gip_attachment *attachment = container_of(to_delayed_work(work),
+		struct gip_attachment, metadata_next);
+
+	guard(mutex)(&attachment->lock);
+	if (attachment->metadata_retries < 4) {
+		attachment->metadata_retries++;
+		schedule_delayed_work(&attachment->metadata_next, HZ / 2);
+		gip_send_system_message(attachment, GIP_CMD_METADATA, 0, NULL, 0);
+	} else {
+		dev_info(GIP_DEV(attachment),
+			"Unable to obtain metadata, attempting to reset device\n");
+		gip_send_set_device_state(attachment, GIP_STATE_RESET);
+	}
+}
+
+static int gip_ensure_metadata(struct gip_attachment *attachment)
+{
+	switch (attachment->got_metadata) {
+	case GIP_METADATA_GOT:
+	case GIP_METADATA_FAKED:
+		return 0;
+	case GIP_METADATA_NONE:
+		attachment->got_metadata = GIP_METADATA_PENDING;
+		cancel_delayed_work_sync(&attachment->metadata_next);
+		schedule_delayed_work(&attachment->metadata_next, HZ / 2);
+		attachment->metadata_retries = 0;
+		return gip_send_system_message(attachment, GIP_CMD_METADATA, 0, NULL, 0);
+	default:
+		return 0;
+	}
+}
+
+static void gip_set_metadata_defaults(struct gip_attachment *attachment)
+{
+	if (attachment->got_metadata != GIP_METADATA_NONE)
+		return;
+
+	attachment->metadata.device.in_system_messages[0] =
+		GIP_DEFAULT_IN_SYSTEM_MESSAGES;
+	attachment->metadata.device.out_system_messages[0] =
+		GIP_DEFAULT_OUT_SYSTEM_MESSAGES;
+	if (attachment->attachment_index == 0) {
+		/* Some decent default settings */
+		attachment->features |= GIP_FEATURE_CONTROLLER;
+		attachment->metadata.device.in_system_messages[0] |= (1u << GIP_CMD_GUIDE_BUTTON);
+	}
+
+	gip_handle_quirks(attachment);
+	if (attachment->quirks & GIP_QUIRK_NO_HELLO)
+		gip_ensure_metadata(attachment);
+
+	attachment->got_metadata = GIP_METADATA_FAKED;
+}
+
+static bool gip_handle_command_protocol_control(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	/* TODO */
+	dev_warn(GIP_DEV(attachment), "Unimplemented Protocol Control message\n");
+	return -ENOTSUPP;
+}
+
+static bool gip_handle_command_hello_device(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct gip_hello_device message = {0};
+
+	if (num_bytes != 28)
+		return -EINVAL;
+
+	message.device_id = (uint64_t) bytes[0];
+	message.device_id |= (uint64_t) bytes[1] << 8;
+	message.device_id |= (uint64_t) bytes[2] << 16;
+	message.device_id |= (uint64_t) bytes[3] << 24;
+	message.device_id |= (uint64_t) bytes[4] << 32;
+	message.device_id |= (uint64_t) bytes[5] << 40;
+	message.device_id |= (uint64_t) bytes[6] << 48;
+	message.device_id |= (uint64_t) bytes[7] << 56;
+
+	message.vendor_id = bytes[8];
+	message.vendor_id |= bytes[9] << 8;
+
+	message.product_id = bytes[10];
+	message.product_id |= bytes[11] << 8;
+
+	message.firmware_major_version = bytes[12];
+	message.firmware_major_version |= bytes[13] << 8;
+
+	message.firmware_minor_version = bytes[14];
+	message.firmware_minor_version |= bytes[15] << 8;
+
+	message.firmware_build_version = bytes[16];
+	message.firmware_build_version |= bytes[17] << 8;
+
+	message.firmware_revision = bytes[18];
+	message.firmware_revision |= bytes[19] << 8;
+
+	message.hardware_major_version = bytes[20];
+	message.hardware_minor_version = bytes[21];
+
+	message.rf_proto_major_version = bytes[22];
+	message.rf_proto_minor_version = bytes[23];
+
+	message.security_major_version = bytes[24];
+	message.security_minor_version = bytes[25];
+
+	message.gip_major_version = bytes[26];
+	message.gip_minor_version = bytes[27];
+
+	dev_dbg(GIP_DEV(attachment), "Device hello from %llx (%04x:%04x)\n",
+		message.device_id, message.vendor_id, message.product_id);
+	dev_dbg(GIP_DEV(attachment), "Firmware version %d.%d.%d rev %d\n",
+		message.firmware_major_version, message.firmware_minor_version,
+		message.firmware_build_version, message.firmware_revision);
+
+	/*
+	 * The GIP spec specifies that the host should reject the device if any of these are wrong.
+	 * I don't know if Windows or an Xbox do, however, so let's just log warnings instead.
+	 */
+	if (message.rf_proto_major_version != 1 && message.rf_proto_minor_version != 0)
+		dev_warn(GIP_DEV(attachment),
+			"Invalid RF protocol version %d.%d, expected 1.0\n",
+			message.rf_proto_major_version, message.rf_proto_minor_version);
+
+	if (message.security_major_version != 1 && message.security_minor_version != 0)
+		dev_warn(GIP_DEV(attachment),
+			"Invalid security protocol version %d.%d, expected 1.0\n",
+			message.security_major_version, message.security_minor_version);
+
+	if (message.gip_major_version != 1 && message.gip_minor_version != 0)
+		dev_warn(GIP_DEV(attachment),
+			"Invalid GIP version %d.%d, expected 1.0\n",
+			message.gip_major_version, message.gip_minor_version);
+
+	attachment->firmware_major_version = message.firmware_major_version;
+	attachment->firmware_minor_version = message.firmware_minor_version;
+	attachment->vendor_id = message.vendor_id;
+	attachment->product_id = message.product_id;
+	attachment->uniq = devm_kasprintf(GIP_DEV(attachment),
+		GFP_KERNEL, "%llx", message.device_id);
+
+	if (header->flags & GIP_FLAG_ATTACHMENT_MASK)
+		return gip_send_system_message(attachment, GIP_CMD_METADATA, 0, NULL, 0);
+	if (attachment->got_metadata == GIP_METADATA_FAKED)
+		attachment->got_metadata = GIP_METADATA_NONE;
+	return gip_ensure_metadata(attachment);
+}
+
+static int gip_handle_command_status_device(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	int i;
+
+	if (num_bytes < 1)
+		return -EINVAL;
+
+	attachment->status.base.battery_level = bytes[0] & 3;
+	attachment->status.base.battery_type = (bytes[0] >> 2) & 3;
+	attachment->status.base.charge = (bytes[0] >> 4) & 3;
+	attachment->status.base.power_level = (bytes[0] >> 6) & 3;
+
+	if (num_bytes >= 4) {
+		attachment->status.device_active = bytes[1] & 1;
+		if (bytes[1] & 2) {
+			/* Events present */
+			if (num_bytes < 5)
+				return -EINVAL;
+
+			attachment->status.num_events = bytes[4];
+			if (attachment->status.num_events > 5) {
+				dev_info(GIP_DEV(attachment),
+					"Device reported too many events, %d > 5\n",
+					attachment->status.num_events);
+				return -EINVAL;
+			}
+			if (5 + attachment->status.num_events * 10 > num_bytes)
+				return -EINVAL;
+
+			for (i = 0; i < attachment->status.num_events; i++) {
+				struct gip_status_event *event = &attachment->status.events[i];
+
+				event->event_type = bytes[i * 10 + 5];
+				event->event_type |= bytes[i * 10 + 6] << 8;
+				event->fault_tag = bytes[i * 10 + 7];
+				event->fault_tag |= bytes[i * 10 + 8] << 8;
+				event->fault_tag |= bytes[i * 10 + 9] << 16;
+				event->fault_tag |= bytes[i * 10 + 10] << 24;
+				event->fault_address = bytes[i * 10 + 11];
+				event->fault_address |= bytes[i * 10 + 12] << 8;
+				event->fault_address |= bytes[i * 10 + 13] << 16;
+				event->fault_address |= bytes[i * 10 + 14] << 24;
+
+				dev_info(GIP_DEV(attachment),
+					"Attachment %i event type %i, tag %i address %x\n",
+					attachment->attachment_index,
+					event->event_type,
+					event->fault_tag,
+					event->fault_address);
+			}
+		}
+	}
+
+	return gip_ensure_metadata(attachment);
+}
+
+static int gip_handle_command_metadata_respose(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct gip_metadata metadata = {0};
+	const guid_t *expected_guid = NULL;
+	bool found_expected_guid;
+	bool found_controller_guid = false;
+	int i, j, k;
+	int rc;
+
+	rc = gip_parse_metadata(GIP_DEV(attachment), &metadata, bytes, num_bytes);
+	if (rc)
+		return rc;
+
+	if (attachment->got_metadata == GIP_METADATA_GOT) {
+		struct input_dev *input;
+
+		gip_metadata_free(GIP_DEV(attachment), &attachment->metadata);
+		rcu_read_lock();
+		input = rcu_dereference(attachment->input);
+		rcu_read_unlock();
+		if (input) {
+			rcu_assign_pointer(attachment->input, NULL);
+			synchronize_rcu();
+			input_unregister_device(input);
+		}
+	}
+
+	attachment->metadata = metadata;
+	attachment->got_metadata = GIP_METADATA_GOT;
+	attachment->features = 0;
+	cancel_delayed_work_sync(&attachment->metadata_next);
+
+	for (i = 0; i < metadata.device.num_preferred_types; i++) {
+		const char *type = metadata.device.preferred_types[i];
+
+		dev_dbg(GIP_DEV(attachment), "Device preferred type: %s\n",
+			type);
+	}
+	for (i = 0; i < metadata.device.num_preferred_types; i++) {
+		const char *type = metadata.device.preferred_types[i];
+
+		for (j = 0; base_drivers[j] && !expected_guid; j++) {
+			for (k = 0; base_drivers[j]->types[k] && !expected_guid; k++) {
+				if (strcmp(type, base_drivers[j]->types[k]) == 0) {
+					rc = gip_bind_driver(attachment, base_drivers[j]);
+					if (rc == 0)
+						expected_guid = &base_drivers[j]->guid;
+					else if (rc != -ENODEV)
+						return rc;
+				}
+			}
+		}
+		if (expected_guid)
+			break;
+
+		if (strcmp(type, "Windows.Xbox.Input.Chatpad") == 0) {
+			break;
+		}
+		if (strcmp(type, "Windows.Xbox.Input.Headset") == 0) {
+			expected_guid = &guid_headset;
+			break;
+		}
+	}
+
+	found_expected_guid = !expected_guid;
+	for (i = 0; i < metadata.device.num_supported_interfaces; i++) {
+		const guid_t *guid = &metadata.device.supported_interfaces[i];
+
+		dev_dbg(GIP_DEV(attachment), "Supported interface: %pUl\n", guid);
+		if (expected_guid && guid_equal(expected_guid, guid))
+			found_expected_guid = true;
+
+		if (guid_equal(&guid_controller, guid)) {
+			found_controller_guid = true;
+			continue;
+		}
+		if (guid_equal(&gip_driver_navigation.guid, guid)) {
+			attachment->features |= GIP_FEATURE_CONTROLLER;
+			continue;
+		}
+		if (guid_equal(&guid_dev_auth_pc_opt_out, guid)) {
+			attachment->features |= GIP_FEATURE_SECURITY_OPT_OUT;
+			continue;
+		}
+		if (guid_equal(&guid_console_function_map, guid)) {
+			attachment->features |= GIP_FEATURE_CONSOLE_FUNCTION_MAP;
+			continue;
+		}
+		if (guid_equal(&guid_console_function_map_overflow, guid)) {
+			attachment->features |= GIP_FEATURE_CONSOLE_FUNCTION_MAP_OVERFLOW;
+			continue;
+		}
+		if (guid_equal(&guid_elite_buttons, guid)) {
+			attachment->features |= GIP_FEATURE_ELITE_BUTTONS;
+			continue;
+		}
+		if (guid_equal(&guid_dynamic_latency_input, guid)) {
+			attachment->features |= GIP_FEATURE_DYNAMIC_LATENCY_INPUT;
+			continue;
+		}
+	}
+
+	for (i = 0; i < metadata.num_messages; i++) {
+		struct gip_message_metadata *message = &metadata.message_metadata[i];
+
+		if (message->type == GIP_CMD_DIRECT_MOTOR && message->length >= 9
+			&& (message->flags & GIP_MESSAGE_FLAG_DOWNSTREAM))
+			attachment->features |= GIP_FEATURE_MOTOR_CONTROL;
+	}
+
+	if (!found_expected_guid || !found_controller_guid)
+		dev_dbg(GIP_DEV(attachment),
+			"Controller was missing expected GUID. "
+			"This controller probably won't work on an actual Xbox.\n");
+
+	gip_handle_quirks(attachment);
+
+	if ((attachment->features & GIP_FEATURE_GUIDE_COLOR)
+		&& !gip_supports_vendor_message(attachment,
+			GIP_CMD_GUIDE_COLOR, false))
+		attachment->features &= ~GIP_FEATURE_GUIDE_COLOR;
+
+	dev_dbg(GIP_DEV(attachment),
+		"Attachment %i has features: %02x\n",
+		attachment->attachment_index, attachment->features);
+
+	return gip_send_init_sequence(attachment);
+}
+
+static int gip_handle_command_security(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	/* TODO: Needed for controllers that connect via dongles */
+	dev_warn(GIP_DEV(attachment), "Unimplemented Security message\n");
+	return -ENOTSUPP;
+}
+
+static int gip_handle_command_guide_button_status(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct input_dev *input;
+
+	if (num_bytes < 2)
+		return -EINVAL;
+
+	guard(rcu)();
+	input = rcu_dereference(attachment->input);
+	if (!input)
+		return -ENODEV;
+
+	if (bytes[1] == VK_LWIN) {
+		input_report_key(input, BTN_MODE, bytes[0] & 3);
+		input_sync(input);
+	}
+
+	return 0;
+}
+
+static int gip_handle_command_audio_control(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	/* TODO: Needed for audio */
+	dev_warn(GIP_DEV(attachment), "Unimplemented Audio Control message\n");
+	return -ENOTSUPP;
+}
+
+static int gip_handle_command_firmware(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	if (num_bytes < 1)
+		return -EINVAL;
+
+	if (bytes[0] == 1) {
+		uint16_t major, minor, build, rev;
+
+		if (num_bytes < 14) {
+			dev_dbg(GIP_DEV(attachment),
+				"Discarding too-short firmware message\n");
+
+			return -EINVAL;
+		}
+		major = bytes[6];
+		major |= bytes[7] << 8;
+		minor = bytes[8];
+		minor |= bytes[9] << 8;
+		build = bytes[10];
+		build |= bytes[11] << 8;
+		rev = bytes[12];
+		rev |= bytes[13] << 8;
+
+		dev_dbg(GIP_DEV(attachment),
+			"Firmware version: %d.%d.%d rev %d\n", major, minor, build, rev);
+
+		attachment->firmware_major_version = major;
+		attachment->firmware_minor_version = minor;
+
+		if (attachment->vendor_id == 0x045e
+			&& attachment->product_id == 0x0b00)
+			return gip_enable_elite_buttons(attachment);
+
+		return 0;
+	}
+
+	dev_warn(GIP_DEV(attachment), "Unimplemented Firmware message\n");
+
+	return -ENOTSUPP;
+}
+
+static int gip_handle_command_hid_report(struct gip_attachment *attachment,
+	const struct gip_header *header, uint8_t *bytes, int num_bytes)
+{
+	dev_warn(GIP_DEV(attachment), "Unimplemented HID report message\n");
+
+	return -ENOTSUPP;
+}
+
+static int gip_handle_command_extended(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	if (num_bytes < 2)
+		return -EINVAL;
+
+	if (bytes[1] != GIP_EXTENDED_STATUS_OK) {
+		dev_dbg(GIP_DEV(attachment),
+			"Extended message type %02x failed with status %i\n",
+			bytes[0], bytes[1]);
+		return -EPROTO;
+	}
+
+	switch (bytes[0]) {
+	case GIP_EXTCMD_GET_SERIAL_NUMBER:
+		memcpy(attachment->serial, &bytes[2],
+			min(sizeof(attachment->serial), (size_t)(num_bytes - 2)));
+		break;
+	default:
+		/* TODO */
+		dev_dbg(GIP_DEV(attachment), "Unimplemented extended message type %02x\n",
+			bytes[0]);
+		return -ENOTSUPP;
+	}
+
+	return 0;
+}
+
+static int gip_handle_elite_buttons(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	bool grip[4] = { 0, 0, 0, 0 };
+	int profile = -1;
+
+	if (attachment->xbe_format == GIP_BTN_FMT_XBE1
+		&& num_bytes > GIP_BTN_OFFSET_XBE1) {
+		profile = bytes[GIP_BTN_OFFSET_XBE1] >> 4;
+		if (profile) {
+			grip[0] = bytes[GIP_BTN_OFFSET_XBE1] & BIT(0);
+			grip[1] = bytes[GIP_BTN_OFFSET_XBE1] & BIT(1);
+			grip[2] = bytes[GIP_BTN_OFFSET_XBE1] & BIT(2);
+			grip[3] = bytes[GIP_BTN_OFFSET_XBE1] & BIT(3);
+		}
+	} else if ((attachment->xbe_format == GIP_BTN_FMT_XBE2_4
+		|| attachment->xbe_format == GIP_BTN_FMT_XBE2_5)
+		&& num_bytes > GIP_BTN_OFFSET_XBE2) {
+		int profile_offset;
+
+		if (attachment->xbe_format == GIP_BTN_FMT_XBE2_4)
+			profile_offset = 15;
+		else
+			profile_offset = 20;
+		profile = bytes[profile_offset] & 3;
+
+		if (!profile) {
+			grip[0] = bytes[GIP_BTN_OFFSET_XBE2] & BIT(2);
+			grip[1] = bytes[GIP_BTN_OFFSET_XBE2] & BIT(0);
+			grip[2] = bytes[GIP_BTN_OFFSET_XBE2] & BIT(3);
+			grip[3] = bytes[GIP_BTN_OFFSET_XBE2] & BIT(1);
+		}
+	}
+	if (profile >= 0) {
+		input_report_key(input, BTN_GRIPL, grip[0]);
+		input_report_key(input, BTN_GRIPR, grip[1]);
+		input_report_key(input, BTN_GRIPL2, grip[2]);
+		input_report_key(input, BTN_GRIPR2, grip[3]);
+		input_report_abs(input, ABS_PROFILE, profile);
+	}
+	return 0;
+}
+
+static int gip_handle_console_map(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	int function_map_offset = -1;
+	if (num_bytes < 32)
+		return 0;
+
+	if (attachment->features & GIP_FEATURE_DYNAMIC_LATENCY_INPUT) {
+		/* The dynamic latency input bytes are after the console function map */
+		if (num_bytes >= 40)
+			function_map_offset = num_bytes - 26;
+	} else {
+		function_map_offset = num_bytes - 18;
+	}
+	if (function_map_offset >= 14) {
+		input_report_key(input, KEY_RECORD,
+			bytes[function_map_offset] & BIT(0));
+	}
+	return 0;
+}
+
+static int gip_handle_ll_input_report(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	struct input_dev *input;
+	int rc = 0;
+
+	guard(rcu)();
+	input = rcu_dereference(attachment->input);
+	if (!input)
+		return -ENODEV;
+
+	if (attachment->device_state != GIP_STATE_START) {
+		dev_dbg(GIP_DEV(attachment), "Discarding early input report\n");
+		attachment->device_state = GIP_STATE_START;
+		return 0;
+	}
+
+	if (attachment->driver && attachment->driver->handle_input_report) {
+		rc = attachment->driver->handle_input_report(attachment, input, bytes, num_bytes);
+		if (rc < 0)
+			return rc;
+	}
+
+	if (attachment->features & GIP_FEATURE_ELITE_BUTTONS) {
+		rc = gip_handle_elite_buttons(attachment, input, bytes, num_bytes);
+		if (rc < 0)
+			goto exit;
+	}
+
+	rc = gip_handle_console_map(attachment, input, bytes, num_bytes);
+
+exit:
+	input_sync(input);
+
+	return rc;
+}
+
+static int gip_handle_ll_overflow_input_report(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	/* TODO: Unknown if any devices actually use this */
+	dev_dbg(GIP_DEV(attachment), "Unimplemented Overflow Input Report message\n");
+	return -ENOTSUPP;
+}
+
+static int gip_handle_audio_data(struct gip_attachment *attachment,
+	const struct gip_header *header, const uint8_t *bytes, int num_bytes)
+{
+	/* TODO: Needed for audio support */
+	dev_dbg(GIP_DEV(attachment), "Unimplemented Audio Data message\n");
+	return -ENOTSUPP;
+}
+
+static int gip_handle_system_message(struct gip_attachment *attachment,
+	const struct gip_header *header, uint8_t *bytes, int num_bytes)
+{
+	if (!gip_supports_system_message(attachment, header->message_type, true)) {
+		dev_warn(GIP_DEV(attachment),
+			"Received claimed-unsupported system message type %02x\n",
+			header->message_type);
+		return -EINVAL;
+	}
+	switch (header->message_type) {
+	case GIP_CMD_PROTO_CONTROL:
+		return gip_handle_command_protocol_control(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_HELLO_DEVICE:
+		return gip_handle_command_hello_device(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_STATUS_DEVICE:
+		return gip_handle_command_status_device(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_METADATA:
+		return gip_handle_command_metadata_respose(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_SECURITY:
+		return gip_handle_command_security(attachment, header, bytes,
+			num_bytes);
+	case GIP_CMD_GUIDE_BUTTON:
+		return gip_handle_command_guide_button_status(attachment,
+			header, bytes, num_bytes);
+	case GIP_CMD_AUDIO_CONTROL:
+		return gip_handle_command_audio_control(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_FIRMWARE:
+		return gip_handle_command_firmware(attachment, header, bytes,
+			num_bytes);
+	case GIP_CMD_HID_REPORT:
+		return gip_handle_command_hid_report(attachment, header,
+			bytes, num_bytes);
+	case GIP_CMD_EXTENDED:
+		return gip_handle_command_extended(attachment, header, bytes,
+			num_bytes);
+	case GIP_AUDIO_DATA:
+		return gip_handle_audio_data(attachment, header, bytes,
+			num_bytes);
+	default:
+		dev_warn(GIP_DEV(attachment),
+			"Received unknown system message type %02x\n",
+			header->message_type);
+		return -EINVAL;
+	}
+}
+
+static struct gip_attachment *gip_ensure_attachment(struct gip_device *device,
+	uint8_t attachment_index)
+{
+	struct gip_attachment *attachment = device->attachments[attachment_index];
+
+	if (!attachment) {
+		attachment = devm_kzalloc(GIP_DEV(device),
+			sizeof(*attachment), GFP_KERNEL);
+		if (!attachment)
+			return ERR_PTR(-ENOMEM);
+
+		attachment->attachment_index = attachment_index;
+		attachment->device = device;
+
+		if (attachment_index == 0) {
+			attachment->vendor_id = device->udev->descriptor.idVendor;
+			attachment->product_id = device->udev->descriptor.idProduct;
+		}
+
+		device->attachments[attachment_index] = attachment;
+
+		mutex_init(&attachment->lock);
+		INIT_DELAYED_WORK(&attachment->fragment_timeout, gip_fragment_timeout);
+		INIT_DELAYED_WORK(&attachment->metadata_next, gip_retry_metadata);
+
+		gip_set_metadata_defaults(attachment);
+	}
+	return attachment;
+}
+
+static int gip_handle_message(struct gip_attachment *attachment,
+	const struct gip_header *header, uint8_t *bytes, int num_bytes)
+{
+	if (header->flags & GIP_FLAG_SYSTEM)
+		return gip_handle_system_message(attachment, header, bytes,
+			num_bytes);
+
+	if (header->message_type < MAX_GIP_CMD && attachment->vendor_handlers[header->message_type])
+		return attachment->vendor_handlers[header->message_type](attachment,
+			header, bytes, num_bytes);
+
+	switch (header->message_type) {
+	case GIP_LL_INPUT_REPORT:
+		return gip_handle_ll_input_report(attachment, header, bytes,
+			num_bytes);
+	case GIP_LL_OVERFLOW_INPUT_REPORT:
+		return gip_handle_ll_overflow_input_report(attachment, header,
+			bytes, num_bytes);
+	}
+	dev_warn(GIP_DEV(attachment),
+		"Received unknown vendor message type %02x\n",
+		header->message_type);
+	return -ENOTSUPP;
+}
+
+static int gip_receive_fragment(struct gip_attachment *attachment,
+	const struct gip_header *header, int offset,
+	uint64_t *fragment_offset, uint16_t *bytes_remaining, uint8_t *bytes,
+	int num_bytes)
+{
+	int rc = 0;
+
+	if (header->flags & GIP_FLAG_INIT_FRAG) {
+		uint64_t total_length;
+
+		if (attachment->fragment_message) {
+			/*
+			 * Reset fragment buffer if we get a new initial
+			 * fragment before finishing the last message.
+			 * TODO: Is this the correct behavior?
+			 */
+			devm_kfree(GIP_DEV(attachment), attachment->fragment_data);
+			attachment->fragment_data = NULL;
+		}
+		offset += gip_decode_length(&total_length, &bytes[offset],
+			num_bytes - offset);
+		if (total_length > MAX_MESSAGE_LENGTH)
+			return -EINVAL;
+
+		attachment->total_length = total_length;
+		attachment->fragment_message = header->message_type;
+		if (header->length > num_bytes - offset) {
+			dev_warn(GIP_DEV(attachment),
+				"Received fragment that claims to be %llu bytes, expected %i\n",
+				header->length, num_bytes - offset);
+			return -EINVAL;
+		}
+		if (header->length > total_length) {
+			dev_warn(GIP_DEV(attachment),
+				"Received too long fragment, %llu bytes, exceeds %d\n",
+				header->length, attachment->total_length);
+			return -EINVAL;
+		}
+		attachment->fragment_data = devm_kmalloc(GIP_DEV(attachment),
+			attachment->total_length, GFP_KERNEL);
+		if (!attachment->fragment_data)
+			return -ENOMEM;
+		memcpy(attachment->fragment_data, &bytes[offset],
+			header->length);
+		*fragment_offset = header->length;
+		attachment->fragment_offset = header->length;
+		*bytes_remaining = attachment->total_length - header->length;
+	} else {
+		if (header->message_type != attachment->fragment_message) {
+			dev_warn(GIP_DEV(attachment),
+				"Received out of sequence message type %02x, expected %02x\n",
+				header->message_type, attachment->fragment_message);
+			gip_fragment_failed(attachment, header);
+			return -EINVAL;
+		}
+
+		offset += gip_decode_length(fragment_offset, &bytes[offset],
+			num_bytes - offset);
+		if (*fragment_offset != attachment->fragment_offset) {
+			dev_warn(GIP_DEV(attachment),
+				"Received out of sequence fragment, (claimed %llu, expected %d)\n",
+				*fragment_offset, attachment->fragment_offset);
+			gip_acknowledge(attachment->device, header,
+				attachment->fragment_offset,
+				attachment->total_length - attachment->fragment_offset);
+			return -EINVAL;
+		} else if (*fragment_offset + header->length > attachment->total_length) {
+			dev_warn(GIP_DEV(attachment),
+				"Received too long fragment, %llu exceeds %d\n",
+				*fragment_offset + header->length, attachment->total_length);
+			gip_fragment_failed(attachment, header);
+			return -EINVAL;
+		}
+
+		*bytes_remaining = attachment->total_length -
+			(*fragment_offset + header->length);
+		if (header->length != 0) {
+			memcpy(&attachment->fragment_data[*fragment_offset],
+				&bytes[offset], header->length);
+		} else {
+			rc = gip_handle_message(attachment, header,
+				attachment->fragment_data,
+				attachment->total_length);
+			devm_kfree(GIP_DEV(attachment), attachment->fragment_data);
+			attachment->fragment_data = NULL;
+			attachment->fragment_message = 0;
+		}
+		*fragment_offset += header->length;
+		attachment->fragment_offset = *fragment_offset;
+	}
+	cancel_delayed_work_sync(&attachment->fragment_timeout);
+	schedule_delayed_work(&attachment->fragment_timeout, HZ);
+
+	return rc;
+}
+
+static int gip_receive_message(struct gip_device *device, uint8_t *bytes,
+	int num_bytes)
+{
+	struct gip_header header;
+	int offset = 3;
+	int rc = 0;
+	uint64_t fragment_offset = 0;
+	uint16_t bytes_remaining = 0;
+	bool is_fragment;
+	uint8_t attachment_index;
+	struct gip_attachment *attachment;
+
+	if (num_bytes < 5)
+		return -EINVAL;
+
+	header.message_type = bytes[0];
+	header.flags = bytes[1];
+	header.sequence_id = bytes[2];
+	offset += gip_decode_length(&header.length, &bytes[offset], num_bytes - offset);
+
+	is_fragment = header.flags & GIP_FLAG_FRAGMENT;
+	attachment_index = header.flags & GIP_FLAG_ATTACHMENT_MASK;
+	attachment = gip_ensure_attachment(device, attachment_index);
+
+	print_hex_dump_debug(KBUILD_MODNAME ": Received message: ", DUMP_PREFIX_OFFSET,
+		16, 1, bytes, num_bytes, false);
+
+	guard(mutex)(&attachment->lock);
+	/* Handle coalescing fragmented messages */
+	if (is_fragment) {
+		rc = gip_receive_fragment(attachment, &header, offset,
+			&fragment_offset, &bytes_remaining, bytes, num_bytes);
+	} else if (header.length + offset > num_bytes) {
+		dev_warn(GIP_DEV(device),
+			"Received message with erroneous length (claimed %llu, actual %d), discarding\n",
+			header.length + offset, num_bytes);
+		rc = -EINVAL;
+	} else {
+		num_bytes -= offset;
+		bytes += offset;
+		fragment_offset = header.length;
+		rc = gip_handle_message(attachment, &header, bytes, num_bytes);
+	}
+
+	if (!rc && (header.flags & GIP_FLAG_ACME))
+		gip_acknowledge(device, &header, fragment_offset, bytes_remaining);
+
+	return rc;
+}
+
+static void gip_receive_work(struct work_struct *work)
+{
+	struct gip_device *device = container_of(work, struct gip_device,
+		receive_message);
+	unsigned long flags;
+
+	spin_lock_irqsave(&device->message_lock, flags);
+	while (device->pending_in_messages) {
+		struct gip_raw_message *message = &device->in_queue[device->next_in_message];
+
+		spin_unlock_irqrestore(&device->message_lock, flags);
+
+		gip_receive_message(device, message->bytes, message->num_bytes);
+
+		spin_lock_irqsave(&device->message_lock, flags);
+		device->next_in_message = (device->next_in_message + 1) % MAX_IN_MESSAGES;
+		device->pending_in_messages--;
+	}
+	spin_unlock_irqrestore(&device->message_lock, flags);
+}
+
+static void gip_urb_in(struct urb *urb)
+{
+	struct gip_interface *intf = urb->context;
+	struct gip_device *gip = intf->device;
+	struct device *dev = &intf->intf->dev;
+	int status = urb->status;
+	int message_id;
+	struct gip_raw_message *message;
+	unsigned long flags;
+
+	switch (status) {
+	case 0:
+		/* success */
+		break;
+	case -ECONNRESET:
+	case -ENOENT:
+	case -ESHUTDOWN:
+		/* this urb is terminated, clean up */
+		dev_dbg(dev, "%s - urb shutting down with status: %d\n",
+			__func__, status);
+		return;
+	default:
+		dev_dbg(dev, "%s - urb has status of: %d\n",
+			__func__, status);
+		goto exit;
+	}
+	if (intf->isoc_messages) {
+		/* TODO: Needed for audio support */
+		dev_warn(GIP_DEV(gip), "Unimplemented isochronous message input\n");
+		goto exit;
+	}
+
+	spin_lock_irqsave(&gip->message_lock, flags);
+	if (gip->pending_in_messages >= MAX_IN_MESSAGES) {
+		dev_err(GIP_DEV(gip), "Input queue is full; dropping message\n");
+	} else {
+		message_id = (gip->next_in_message + gip->pending_in_messages) % MAX_IN_MESSAGES;
+		message = &gip->in_queue[message_id];
+		gip->pending_in_messages++;
+		memcpy(message->bytes, intf->in_data, urb->actual_length);
+		message->num_bytes = urb->actual_length;
+	}
+	spin_unlock_irqrestore(&gip->message_lock, flags);
+	schedule_work(&gip->receive_message);
+
+exit:
+	status = usb_submit_urb(urb, GFP_ATOMIC);
+	if (status)
+		dev_err(dev, "%s - usb_submit_urb failed with result %d\n",
+			__func__, status);
+}
+
+static void gip_urb_out(struct urb *urb)
+{
+	struct gip_interface *intf = urb->context;
+	struct device *dev = &intf->intf->dev;
+	int status = urb->status;
+
+	guard(spinlock_irqsave)(&intf->device->message_lock);
+
+	switch (status) {
+	case 0:
+		/* success */
+		break;
+
+	case -ECONNRESET:
+	case -ENOENT:
+	case -ESHUTDOWN:
+		/* this urb is terminated, clean up */
+		dev_dbg(dev, "%s - urb shutting down with status: %d\n",
+			__func__, status);
+		break;
+
+	default:
+		dev_dbg(dev, "%s - nonzero urb status received: %d\n",
+			__func__, status);
+		break;
+	}
+}
+
+static int gip_init_input(struct gip_interface *intf,
+	struct usb_endpoint_descriptor *ep_in)
+{
+	int error;
+	struct usb_device *udev = interface_to_usbdev(intf->intf);
+
+	intf->urb_in = usb_alloc_urb(intf->isoc_messages, GFP_KERNEL);
+	if (!intf->urb_in)
+		return -ENOMEM;
+
+	intf->in_data = usb_alloc_coherent(udev, intf->mtu, GFP_KERNEL,
+		&intf->urb_in->transfer_dma);
+
+	if (!intf->in_data) {
+		return -ENOMEM;
+		goto err_free_urb;
+	}
+
+	usb_fill_int_urb(intf->urb_in, udev,
+		usb_rcvintpipe(udev, ep_in->bEndpointAddress),
+		intf->in_data, intf->mtu, gip_urb_in, intf,
+		ep_in->bInterval);
+	intf->urb_in->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+	if (intf->isoc_messages)
+		intf->urb_in->transfer_flags |= URB_ISO_ASAP;
+
+	return 0;
+
+err_free_urb:
+	usb_free_urb(intf->urb_in);
+	intf->urb_in = NULL;
+
+	return error;
+}
+
+static int gip_init_output(struct gip_interface *intf,
+	struct usb_endpoint_descriptor *ep_out)
+{
+	int error;
+	struct usb_device *udev = interface_to_usbdev(intf->intf);
+	int i;
+
+	if (usb_ifnum_to_if(udev, GIP_WIRED_INTF_AUDIO)) {
+		/*
+		 * Explicitly disable the audio interface. This is needed
+		 * for some controllers, such as the PowerA Enhanced Wired
+		 * Controller for Series X|S (0x20d6:0x200e) to report the
+		 * guide button.
+		 */
+		error = usb_set_interface(udev, GIP_WIRED_INTF_AUDIO, 0);
+		if (error)
+			dev_warn(GIP_DEV(intf),
+				"unable to disable audio interface: %d\n",
+				error);
+	}
+
+	init_usb_anchor(&intf->out_anchor);
+
+	for (i = 0; i < MAX_OUT_MESSAGES; i++) {
+		intf->out_queue[i].urb = usb_alloc_urb(intf->isoc_messages, GFP_KERNEL);
+		if (!intf->out_queue[i].urb) {
+			error = -ENOMEM;
+			goto err_free_urbs;
+		}
+
+		intf->out_queue[i].data = usb_alloc_coherent(udev, intf->mtu, GFP_KERNEL,
+			&intf->out_queue[i].urb->transfer_dma);
+
+		if (!intf->out_queue[i].data) {
+			return -ENOMEM;
+			goto err_free_urbs;
+		}
+
+		usb_fill_int_urb(intf->out_queue[i].urb, udev,
+			usb_sndintpipe(udev, ep_out->bEndpointAddress),
+			intf->out_queue[i].data, intf->mtu, gip_urb_out, intf,
+			ep_out->bInterval);
+		intf->out_queue[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+		if (intf->isoc_messages)
+			intf->out_queue[i].urb->transfer_flags |= URB_ISO_ASAP;
+	}
+
+	return 0;
+
+err_free_urbs:
+	for (i = 0; i < MAX_OUT_MESSAGES; i++) {
+		if (intf->out_queue[i].data)
+			usb_free_coherent(udev, intf->mtu, intf->out_queue[i].data,
+				intf->out_queue[i].urb->transfer_dma);
+		if (intf->out_queue[i].urb) {
+			usb_free_urb(intf->out_queue[i].urb);
+			intf->out_queue[i].urb = NULL;
+		}
+	}
+	return error;
+}
+
+static void gip_deinit_output(struct gip_interface *intf)
+{
+	int i;
+
+	for (i = 0; i < MAX_OUT_MESSAGES; i++) {
+		if (!intf->out_queue[i].urb)
+			continue;
+		usb_free_coherent(interface_to_usbdev(intf->intf), intf->mtu,
+			intf->out_queue[i].data, intf->out_queue[i].urb->transfer_dma);
+		usb_free_urb(intf->out_queue[i].urb);
+		intf->out_queue[i].data = NULL;
+		intf->out_queue[i].urb = NULL;
+	}
+}
+
+static void gip_deinit_input(struct gip_interface *intf)
+{
+	usb_free_coherent(interface_to_usbdev(intf->intf), intf->mtu,
+		intf->in_data, intf->urb_in->transfer_dma);
+	usb_free_urb(intf->urb_in);
+	intf->urb_in = NULL;
+}
+
+static int gip_interface_init(struct gip_interface *intf)
+{
+	struct usb_endpoint_descriptor *ep_in = NULL;
+	struct usb_endpoint_descriptor *ep_out = NULL;
+	int error = usb_find_common_endpoints(intf->intf->cur_altsetting,
+		NULL, NULL, &ep_in, &ep_out);
+
+	if (error)
+		return error;
+
+	if (!ep_in || !ep_out)
+		return -ENODEV;
+
+	error = gip_init_input(intf, ep_in);
+	if (error)
+		return error;
+
+	error = gip_init_output(intf, ep_out);
+	if (error)
+		goto err_free_input;
+
+	if (usb_submit_urb(intf->urb_in, GFP_KERNEL)) {
+		error = -EIO;
+		goto err_free_output;
+	}
+
+	return 0;
+
+err_free_output:
+	gip_deinit_output(intf);
+err_free_input:
+	gip_deinit_input(intf);
+	return error;
+}
+
+static int gip_probe(struct usb_interface *intf, const struct usb_device_id *id)
+{
+	struct usb_device *udev = interface_to_usbdev(intf);
+	struct gip_device *gip = NULL;
+	struct gip_attachment *attachment;
+	int rc;
+
+	if (intf->cur_altsetting->desc.bInterfaceNumber != GIP_WIRED_INTF_DATA) {
+		/*
+		 * The Xbox One controller lists three interfaces all with the
+		 * same interface class, subclass and protocol. Differentiate by
+		 * interface number.
+		 */
+		return 0;
+	}
+
+	gip = devm_kzalloc(&udev->dev, sizeof(*gip), GFP_KERNEL);
+	if (!gip)
+		return -ENOMEM;
+
+	gip->udev = udev;
+	gip->data.device = gip;
+	gip->data.intf = intf;
+	gip->data.mtu = BASE_GIP_MTU;
+	gip->audio.device = gip;
+	gip->audio.mtu = MAX_GIP_MTU;
+	gip->audio.isoc_messages = MAX_AUDIO_MESSAGES;
+
+	INIT_WORK(&gip->receive_message, gip_receive_work);
+	spin_lock_init(&gip->message_lock);
+
+	rc = gip_interface_init(&gip->data);
+	if (rc) {
+		devm_kfree(GIP_DEV(gip), gip);
+		return rc;
+	}
+	/* Don't init audio interface -- we aren't using it yet */
+
+	usb_set_intfdata(intf, gip);
+
+	/* Pre-create the first attachment, as it should always exist */
+	attachment = gip_ensure_attachment(gip, 0);
+	if (IS_ERR(attachment))
+		return PTR_ERR(attachment);
+
+	return 0;
+}
+
+static int gip_shutdown(struct gip_device *device)
+{
+	int i;
+
+	cancel_work_sync(&device->receive_message);
+
+	for (i = 0; i < MAX_ATTACHMENTS; i++) {
+		struct gip_attachment *attachment = device->attachments[i];
+		struct input_dev *input;
+
+		if (!attachment)
+			continue;
+
+		scoped_guard (mutex, &attachment->lock) {
+			cancel_delayed_work_sync(&attachment->metadata_next);
+			cancel_delayed_work_sync(&attachment->fragment_timeout);
+
+			rcu_read_lock();
+			input = rcu_dereference(attachment->input);
+			rcu_read_unlock();
+
+			rcu_assign_pointer(attachment->input, NULL);
+			synchronize_rcu();
+		}
+
+		if (input)
+			input_unregister_device(input);
+	}
+
+	return 0;
+}
+
+static void gip_disconnect(struct usb_interface *intf)
+{
+	struct gip_device *gip = usb_get_intfdata(intf);
+	unsigned long flags;
+	int i;
+
+	if (!gip)
+		return;
+
+	usb_kill_urb(gip->data.urb_in);
+	if (gip->audio.intf)
+		usb_kill_urb(gip->audio.urb_in);
+
+	gip_shutdown(gip);
+
+	spin_lock_irqsave(&gip->message_lock, flags);
+	gip_deinit_input(&gip->data);
+	gip_deinit_output(&gip->data);
+	if (gip->audio.intf) {
+		gip_deinit_input(&gip->audio);
+		gip_deinit_output(&gip->audio);
+	}
+	spin_unlock_irqrestore(&gip->message_lock, flags);
+
+	usb_set_intfdata(intf, NULL);
+
+	for (i = 0; i < MAX_ATTACHMENTS; i++) {
+		struct gip_attachment *attachment = gip->attachments[i];
+
+		if (!attachment)
+			continue;
+		devm_kfree(GIP_DEV(attachment), attachment->uniq);
+		devm_kfree(GIP_DEV(attachment), attachment);
+	}
+
+	devm_kfree(GIP_DEV(gip), gip);
+}
+
+static int gip_suspend(struct usb_interface *intf, pm_message_t message)
+{
+	struct gip_device *gip = usb_get_intfdata(intf);
+
+	if (!gip)
+		return 0;
+
+	usb_kill_urb(gip->data.urb_in);
+	if (gip->audio.intf)
+		usb_kill_urb(gip->audio.urb_in);
+
+	if (gip->attachments[0]) {
+		struct gip_attachment *attachment = gip->attachments[0];
+
+		guard(mutex)(&attachment->lock);
+		gip_send_set_device_state(attachment, GIP_STATE_OFF);
+		attachment->device_state = GIP_STATE_OFF;
+	}
+
+	return gip_shutdown(gip);
+}
+
+static int gip_resume(struct usb_interface *intf)
+{
+	struct gip_device *gip = usb_get_intfdata(intf);
+
+	if (!gip)
+		return 0;
+
+	if (usb_submit_urb(gip->data.urb_in, GFP_KERNEL))
+		return -EIO;
+
+	return 0;
+}
+
+/* The Xbox One controller uses subclass 71 and protocol 208. */
+#define GIP_VENDOR(vend) \
+	{ \
+		.match_flags = USB_DEVICE_ID_MATCH_VENDOR | USB_DEVICE_ID_MATCH_INT_INFO, \
+		.idVendor = (vend), \
+		.bInterfaceClass = USB_CLASS_VENDOR_SPEC, \
+		.bInterfaceSubClass = 71, \
+		.bInterfaceProtocol = 208 \
+	}
+
+static const struct usb_device_id gip_table[] = {
+	/*
+	 * Please keep this list sorted by vendor ID.
+	 */
+	GIP_VENDOR(0x03f0),		/* HP/HyperX */
+	GIP_VENDOR(0x044f),		/* ThrustMaster */
+	GIP_VENDOR(0x045e),		/* Microsoft */
+	GIP_VENDOR(0x046d),		/* Logitech */
+	GIP_VENDOR(0x0738),		/* Mad Catz */
+	GIP_VENDOR(0x0b05),		/* ASUS */
+	GIP_VENDOR(0x0e6f),		/* PDP */
+	GIP_VENDOR(0x0f0d),		/* Hori */
+	GIP_VENDOR(0x10f5),		/* Turtle Beach */
+	GIP_VENDOR(0x1532),		/* Razer */
+	GIP_VENDOR(0x20d6),		/* PowerA/BDA */
+	GIP_VENDOR(0x24c6),		/* PowerA/BDA/ThrustMaster */
+	GIP_VENDOR(0x294b),		/* Snakebyte */
+	GIP_VENDOR(0x2dc8),		/* 8BitDo */
+	GIP_VENDOR(0x2e24),		/* Hyperkin */
+	GIP_VENDOR(0x2e95),		/* SCUF Gaming */
+	GIP_VENDOR(0x3285),		/* Nacon */
+	GIP_VENDOR(0x3537),		/* GameSir */
+	GIP_VENDOR(0x366c),		/* ByoWave */
+	{ }
+};
+
+MODULE_DEVICE_TABLE(usb, gip_table);
+
+static struct usb_driver gip_driver = {
+	.name		= "xbox-gip",
+	.probe		= gip_probe,
+	.disconnect	= gip_disconnect,
+	.suspend	= gip_suspend,
+	.resume		= gip_resume,
+	.id_table	= gip_table,
+};
+
+module_usb_driver(gip_driver);
+
+MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
+MODULE_DESCRIPTION("Xbox Gaming Input Protocol driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/input/joystick/gip/gip-drivers.c b/drivers/input/joystick/gip/gip-drivers.c
new file mode 100644
index 0000000000000..f5507e6215a94
--- /dev/null
+++ b/drivers/input/joystick/gip/gip-drivers.c
@@ -0,0 +1,210 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Base drivers for common GIP devices
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the Microsoft GIP spec at:
+ * https://aka.ms/gipdocs
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
+ */
+#include "gip.h"
+
+struct gip_device_capabilities_response {
+	uint8_t extra_button_count;
+	uint8_t extra_axis_count;
+	uint8_t led_count;
+	uint8_t max_global_led_gain;
+};
+
+static bool dpad_as_buttons;
+
+static int gip_setup_gamepad_input(struct gip_attachment *attachment, struct input_dev* input)
+{
+	int ret = gip_driver_navigation.setup_input(attachment, input);
+
+	if (ret < 0)
+		return ret;
+	input_set_capability(input, EV_KEY, BTN_THUMBR);
+	input_set_capability(input, EV_KEY, BTN_THUMBL);
+	input_set_abs_params(input, ABS_X, -32768, 32767, 16, 128);
+	input_set_abs_params(input, ABS_Y, -32768, 32767, 16, 128);
+	input_set_abs_params(input, ABS_RX, -32768, 32767, 16, 128);
+	input_set_abs_params(input, ABS_RY, -32768, 32767, 16, 128);
+	input_set_abs_params(input, ABS_Z, 0, 1023, 0, 0);
+	input_set_abs_params(input, ABS_RZ, 0, 1023, 0, 0);
+
+	/* Xbox Adaptive Controller */
+	if (attachment->vendor_id == 0x045e && attachment->product_id == 0x0b0a)
+		input_set_abs_params(input, ABS_PROFILE, 0, 3, 0, 0);
+	return 0;
+}
+
+static int gip_handle_gamepad_report(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	int16_t axis;
+	int ret = gip_driver_navigation.handle_input_report(attachment, input, bytes, num_bytes);
+
+	if (ret < 0)
+		return ret;
+
+	if (num_bytes < 14) {
+		dev_dbg(GIP_DEV(attachment), "Discarding too-short input report\n");
+		return -EINVAL;
+	}
+
+	input_report_key(input, BTN_THUMBL, bytes[1] & BIT(6));
+	input_report_key(input, BTN_THUMBR, bytes[1] & BIT(7));
+
+	axis = bytes[2];
+	axis |= bytes[3] << 8;
+	input_report_abs(input, ABS_Z, axis);
+
+	axis = bytes[4];
+	axis |= bytes[5] << 8;
+	input_report_abs(input, ABS_RZ, axis);
+
+	axis = bytes[6];
+	axis |= bytes[7] << 8;
+	input_report_abs(input, ABS_X, axis);
+	axis = bytes[8];
+	axis |= bytes[9] << 8;
+	input_report_abs(input, ABS_Y, ~axis);
+	axis = bytes[10];
+	axis |= bytes[11] << 8;
+	input_report_abs(input, ABS_RX, axis);
+	axis = bytes[12];
+	axis |= bytes[13] << 8;
+	input_report_abs(input, ABS_RY, ~axis);
+
+	/* Xbox Adaptive Controller */
+	if (attachment->vendor_id == 0x045e && attachment->product_id == 0x0b0a && num_bytes >= 31)
+		input_report_abs(input, ABS_PROFILE, bytes[30] & 3);
+
+	return 0;
+}
+
+const struct gip_driver gip_driver_gamepad = {
+	.types = (const char* const[]) { "Windows.Xbox.Input.Gamepad", NULL },
+	.guid = GUID_INIT(0x082e402c, 0x07df, 0x45e1, 0xa5, 0xab,
+		0xa3, 0x12, 0x7a, 0xf1, 0x97, 0xb5),
+
+	.quirks = (const struct gip_quirks[]) {
+		/* Xbox One Controller (model 1573) */
+		{ 0x045e, 0x02d1, 0, .override_name = "Xbox One Controller" },
+
+		/* Xbox One Controller (model 1697) */
+		{ 0x045e, 0x02dd, 0, .override_name = "Xbox One Controller" },
+
+		/* Xbox Elite */
+		{ 0x045e, 0x02e3, 0,
+			.override_name = "Xbox Elite Controller",
+			.added_features = GIP_FEATURE_ELITE_BUTTONS,
+			.filtered_features = GIP_FEATURE_CONSOLE_FUNCTION_MAP },
+
+		/* Xbox One Controller (model 1708) */
+		{ 0x045e, 0x02ea, 0, .override_name = "Xbox One Controller" },
+
+		/* Xbox Elite 2 */
+		{ 0x045e, 0x0b00, 0,
+			.override_name = "Xbox Elite Series 2 Controller",
+			.added_features = GIP_FEATURE_GUIDE_COLOR |
+				GIP_FEATURE_EXTENDED_SET_DEVICE_STATE },
+
+		/* Xbox Adaptive Controller */
+		{ 0x045e, 0x0b0a, 0, .override_name = "Xbox Adaptive Controller" },
+
+		/* Xbox Wireless Controller */
+		{ 0x045e, 0x0b12, 0, .override_name = "Xbox Wireless Controller" },
+
+		{0},
+	},
+
+	.probe = NULL,
+	.remove = NULL,
+	.init = NULL,
+	.setup_input = gip_setup_gamepad_input,
+	.handle_input_report = gip_handle_gamepad_report,
+};
+
+static int gip_setup_navigation_input(struct gip_attachment *attachment, struct input_dev *input)
+{
+	input_set_capability(input, EV_KEY, BTN_Y);
+	input_set_capability(input, EV_KEY, BTN_B);
+	input_set_capability(input, EV_KEY, BTN_X);
+	input_set_capability(input, EV_KEY, BTN_A);
+	input_set_capability(input, EV_KEY, BTN_SELECT);
+	input_set_capability(input, EV_KEY, BTN_MODE);
+	input_set_capability(input, EV_KEY, BTN_START);
+	input_set_capability(input, EV_KEY, BTN_TR);
+	input_set_capability(input, EV_KEY, BTN_TL);
+
+	attachment->dpad_as_buttons = dpad_as_buttons;
+	if (attachment->dpad_as_buttons) {
+		input_set_capability(input, EV_KEY, BTN_DPAD_UP);
+		input_set_capability(input, EV_KEY, BTN_DPAD_RIGHT);
+		input_set_capability(input, EV_KEY, BTN_DPAD_LEFT);
+		input_set_capability(input, EV_KEY, BTN_DPAD_DOWN);
+	} else {
+		input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+		input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+	}
+
+	return 0;
+}
+
+static int gip_handle_navigation_report(struct gip_attachment *attachment,
+	struct input_dev *input, const uint8_t *bytes, int num_bytes)
+{
+	if (num_bytes < 2) {
+		dev_dbg(GIP_DEV(attachment), "Discarding too-short input report\n");
+		return -EINVAL;
+	}
+
+	input_report_key(input, BTN_START, bytes[0] & BIT(2));
+	input_report_key(input, BTN_SELECT, bytes[0] & BIT(3));
+	input_report_key(input, BTN_A, bytes[0] & BIT(4));
+	input_report_key(input, BTN_B, bytes[0] & BIT(5));
+	input_report_key(input, BTN_X, bytes[0] & BIT(6));
+	input_report_key(input, BTN_Y, bytes[0] & BIT(7));
+
+	if (attachment->dpad_as_buttons) {
+		input_report_key(input, BTN_DPAD_UP, bytes[1] & BIT(0));
+		input_report_key(input, BTN_DPAD_DOWN, bytes[1] & BIT(1));
+		input_report_key(input, BTN_DPAD_LEFT, bytes[1] & BIT(2));
+		input_report_key(input, BTN_DPAD_RIGHT, bytes[1] & BIT(3));
+	} else {
+		input_report_abs(input, ABS_HAT0X,
+			!!(bytes[1] & BIT(3)) - !!(bytes[1] & BIT(2)));
+		input_report_abs(input, ABS_HAT0Y,
+			!!(bytes[1] & BIT(1)) - !!(bytes[1] & BIT(0)));
+	}
+
+	if (attachment->quirks & GIP_QUIRK_SWAP_LB_RB) {
+		/* Previous */
+		input_report_key(input, BTN_TR, bytes[1] & BIT(4));
+		/* Next */
+		input_report_key(input, BTN_TL, bytes[1] & BIT(5));
+	} else {
+		input_report_key(input, BTN_TL, bytes[1] & BIT(4));
+		input_report_key(input, BTN_TR, bytes[1] & BIT(5));
+	}
+
+	return 0;
+}
+
+const struct gip_driver gip_driver_navigation = {
+	.types = (const char* const[]) { "Windows.Xbox.Input.NavigationController", NULL },
+	.guid = GUID_INIT(0xb8f31fe7, 0x7386, 0x40e9, 0xa9, 0xf8,
+		0x2f, 0x21, 0x26, 0x3a, 0xcf, 0xb7),
+
+	.probe = NULL,
+	.remove = NULL,
+	.init = NULL,
+	.setup_input = gip_setup_navigation_input,
+	.handle_input_report = gip_handle_navigation_report,
+};
+
+module_param(dpad_as_buttons, bool, 0444);
+MODULE_PARM_DESC(dpad_as_buttons, "Map the D-Pad as buttons instead of axes");
diff --git a/drivers/input/joystick/gip/gip.h b/drivers/input/joystick/gip/gip.h
new file mode 100644
index 0000000000000..2c60430c81590
--- /dev/null
+++ b/drivers/input/joystick/gip/gip.h
@@ -0,0 +1,309 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Gaming Input Protocol driver for Xbox One/Series controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the Microsoft GIP spec at:
+ * https://aka.ms/gipdocs
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gipusb/e7c90904-5e21-426e-b9ad-d82adeee0dbc
+ */
+
+#ifndef _GIP_H
+#define _GIP_H
+
+#include <linux/rcupdate.h>
+#include <linux/usb/input.h>
+
+#define BASE_GIP_MTU 64
+#define MAX_GIP_MTU 2048
+
+#define MAX_ATTACHMENTS 8
+
+#define MAX_IN_MESSAGES 8
+#define MAX_OUT_MESSAGES 8
+
+#define GIP_QUIRK_NO_HELLO		BIT(0)
+#define GIP_QUIRK_NO_IMPULSE_VIBRATION	BIT(1)
+#define GIP_QUIRK_SWAP_LB_RB		BIT(2)
+
+#define GIP_FEATURE_CONTROLLER				BIT(0)
+#define GIP_FEATURE_CONSOLE_FUNCTION_MAP		BIT(1)
+#define GIP_FEATURE_CONSOLE_FUNCTION_MAP_OVERFLOW	BIT(2)
+#define GIP_FEATURE_ELITE_BUTTONS			BIT(3)
+#define GIP_FEATURE_DYNAMIC_LATENCY_INPUT		BIT(4)
+#define GIP_FEATURE_SECURITY_OPT_OUT			BIT(5)
+#define GIP_FEATURE_MOTOR_CONTROL			BIT(6)
+#define GIP_FEATURE_GUIDE_COLOR				BIT(7)
+#define GIP_FEATURE_EXTENDED_SET_DEVICE_STATE		BIT(8)
+
+/* System messages */
+#define GIP_CMD_PROTO_CONTROL		0x01
+#define GIP_CMD_HELLO_DEVICE		0x02
+#define GIP_CMD_STATUS_DEVICE		0x03
+#define GIP_CMD_METADATA		0x04
+#define GIP_CMD_SET_DEVICE_STATE	0x05
+#define GIP_CMD_SECURITY		0x06
+#define GIP_CMD_GUIDE_BUTTON		0x07
+#define GIP_CMD_AUDIO_CONTROL		0x08
+#define GIP_CMD_LED			0x0a
+#define GIP_CMD_HID_REPORT		0x0b
+#define GIP_CMD_FIRMWARE		0x0c
+#define GIP_CMD_EXTENDED		0x1e
+#define GIP_CMD_DEBUG			0x1f
+#define GIP_AUDIO_DATA			0x60
+
+/* Navigation vendor messages */
+#define GIP_CMD_DIRECT_MOTOR		0x09
+#define GIP_LL_INPUT_REPORT		0x20
+#define GIP_LL_OVERFLOW_INPUT_REPORT	0x26
+
+/* Wheel and ArcadeStick vendor messages */
+#define GIP_CMD_INITIAL_REPORTS_REQUEST	0x0a
+#define GIP_LL_STATIC_CONFIGURATION	0x21
+#define GIP_LL_BUTTON_INFO_REPORT	0x22
+
+#define MAX_GIP_CMD 0x80
+
+#define GIP_DEV(p) \
+	_Generic((p), \
+		struct gip_attachment * : gip_attachment_dev, \
+		struct gip_interface * : gip_interface_dev, \
+		struct gip_device * : gip_device_dev)(p)
+
+enum gip_init_status {
+	GIP_INIT_OK = 0,
+	GIP_INIT_NO_INPUT = 1,
+};
+
+enum gip_metadata_status {
+	GIP_METADATA_NONE = 0,
+	GIP_METADATA_GOT = 1,
+	GIP_METADATA_FAKED = 2,
+	GIP_METADATA_PENDING = 3,
+};
+
+enum gip_elite_button_format {
+	GIP_BTN_FMT_UNKNOWN,
+	GIP_BTN_FMT_XBE1,
+	GIP_BTN_FMT_XBE2_RAW,
+	GIP_BTN_FMT_XBE2_4,
+	GIP_BTN_FMT_XBE2_5,
+};
+
+struct gip_header {
+	uint8_t message_type;
+	uint8_t flags;
+	uint8_t sequence_id;
+	uint64_t length;
+};
+
+struct gip_raw_message {
+	uint16_t num_bytes;
+	uint8_t bytes[BASE_GIP_MTU];
+};
+
+struct gip_device_metadata {
+	uint8_t num_audio_formats;
+	uint8_t num_preferred_types;
+	uint8_t num_supported_interfaces;
+	uint8_t hid_descriptor_size;
+
+	uint32_t in_system_messages[8];
+	uint32_t out_system_messages[8];
+
+	struct gip_audio_format_pair *audio_formats;
+	char **preferred_types;
+	guid_t *supported_interfaces;
+	uint8_t *hid_descriptor;
+};
+
+struct gip_message_metadata {
+	uint8_t type;
+	uint16_t length;
+	uint16_t data_type;
+	uint32_t flags;
+	uint16_t period;
+	uint16_t persistence_timeout;
+};
+
+struct gip_metadata {
+	uint16_t version_major;
+	uint16_t version_minor;
+
+	struct gip_device_metadata device;
+
+	uint8_t num_messages;
+	struct gip_message_metadata *message_metadata;
+};
+
+struct gip_status {
+	int power_level;
+	int charge;
+	int battery_type;
+	int battery_level;
+};
+
+struct gip_status_event {
+	uint16_t event_type;
+	uint32_t fault_tag;
+	uint32_t fault_address;
+};
+
+struct gip_extended_status {
+	struct gip_status base;
+	bool device_active;
+
+	int num_events;
+	struct gip_status_event events[5];
+};
+
+struct gip_attachment;
+typedef int (*gip_command_handler)(struct gip_attachment *a, const struct gip_header *header,
+		const uint8_t *bytes, int num_bytes);
+
+struct gip_device;
+struct gip_attachment {
+	const struct gip_driver *driver;
+	struct gip_device *device;
+	void *driver_data;
+	gip_command_handler vendor_handlers[MAX_GIP_CMD];
+
+	uint8_t attachment_index;
+	struct input_dev __rcu *input;
+	uint16_t vendor_id;
+	uint16_t product_id;
+	char *uniq;
+	const char *name;
+	char phys[32];
+	char serial[32];
+	struct mutex lock;
+
+	uint8_t fragment_message;
+	uint16_t total_length;
+	uint8_t *fragment_data;
+	uint32_t fragment_offset;
+	struct delayed_work fragment_timeout;
+	int fragment_retries;
+
+	uint16_t firmware_major_version;
+	uint16_t firmware_minor_version;
+
+	enum gip_metadata_status got_metadata;
+	struct delayed_work metadata_next;
+	int metadata_retries;
+	struct gip_metadata metadata;
+
+	uint8_t seq_system;
+	uint8_t seq_security;
+	uint8_t seq_extended;
+	uint8_t seq_audio;
+	uint8_t seq_vendor;
+
+	int device_state;
+
+	struct gip_extended_status status;
+
+	enum gip_elite_button_format xbe_format;
+	uint32_t features;
+	uint32_t quirks;
+
+	int extra_buttons;
+	int extra_axes;
+
+	bool dpad_as_buttons;
+	struct hid_device __rcu *hdev;
+};
+
+struct gip_urb {
+	struct urb *urb;
+	uint8_t *data;
+	unsigned int offset;
+};
+
+struct gip_interface {
+	struct gip_device *device;
+	struct usb_interface *intf;
+	uint32_t mtu;
+	int isoc_messages;
+
+	struct urb *urb_in;
+	uint8_t *in_data;
+
+	struct usb_anchor out_anchor;
+	struct gip_urb out_queue[MAX_OUT_MESSAGES];
+};
+
+struct gip_device {
+	struct usb_device *udev;
+
+	struct gip_interface data;
+	struct gip_interface audio;
+
+	struct gip_raw_message in_queue[MAX_IN_MESSAGES];
+	int pending_in_messages;
+	int next_in_message;
+
+	struct work_struct receive_message;
+	spinlock_t message_lock;
+
+	struct gip_attachment *attachments[MAX_ATTACHMENTS];
+};
+
+struct gip_quirks {
+	uint16_t vendor_id;
+	uint16_t product_id;
+	uint8_t attachment_index;
+	const char *override_name;
+	uint32_t added_features;
+	uint32_t filtered_features;
+	uint32_t quirks;
+	uint32_t extra_in_system[8];
+	uint32_t extra_out_system[8];
+	uint8_t extra_buttons;
+	uint8_t extra_axes;
+};
+
+struct gip_driver {
+	const char *const *types;
+	guid_t guid;
+
+	const struct gip_quirks *quirks;
+
+	int (*probe)(struct gip_attachment *a);
+	void (*remove)(struct gip_attachment *a);
+	int (*init)(struct gip_attachment *a);
+	int (*setup_input)(struct gip_attachment *a, struct input_dev* input);
+	int (*handle_input_report)(struct gip_attachment *a,
+		struct input_dev* input, const uint8_t *bytes, int num_bytes);
+	gip_command_handler vendor_handlers[MAX_GIP_CMD];
+};
+
+static inline struct device *gip_attachment_dev(struct gip_attachment *attachment)
+{
+	return &attachment->device->udev->dev;
+}
+
+static inline struct device *gip_interface_dev(struct gip_interface *intf)
+{
+	return &intf->device->udev->dev;
+}
+
+static inline struct device *gip_device_dev(struct gip_device *device)
+{
+	return &device->udev->dev;
+}
+
+bool gip_supports_vendor_message(struct gip_attachment *attachment, uint8_t command, bool upstream);
+
+int gip_send_system_message(struct gip_attachment *attachment,
+	uint8_t message_type, uint8_t flags, const void *bytes, int num_bytes);
+int gip_send_vendor_message(struct gip_attachment *attachment,
+	uint8_t message_type, uint8_t flags, const void *bytes, int num_bytes);
+
+extern const struct gip_driver gip_driver_navigation;
+extern const struct gip_driver gip_driver_gamepad;
+extern const struct gip_driver gip_driver_arcade_stick;
+extern const struct gip_driver gip_driver_wheel;
+extern const struct gip_driver gip_driver_flight_stick;
+#endif
-- 
2.53.0


^ permalink raw reply related

* [PATCH v3 00/10] Input: xbox_gip - Add new driver for Xbox GIP
From: Vicki Pfau @ 2026-03-10  5:19 UTC (permalink / raw)
  To: Dmitry Torokhov, linux-input; +Cc: Vicki Pfau

This is a new version of the previously submitted xbox-gip series.

This introduces a new driver for the Xbox One/Series controller protocol,
officially known as the Gaming Input Protocol, or GIP for short.

Microsoft released documentation on (some of) GIP in late 2024, upon which
this driver is based. Though the documentation was incomplete, it still
provided enough information to warrant a clean start over the previous,
incomplete implementation.

This driver is already at feature parity with the GIP support in xpad,
along with several more enhancements:

- Proper support for parsing message length and fragmented messages
- Metadata parsing, allowing for auto-detection on various parameters,
  including the presence and location in the message of the share button,
  as well as detection of specific device types
- Direct
- Controllable LED support
- HID passthrough for the Chatpad
- Battery information support
- Arcade stick support
- Racing wheel support

The framework set out in this driver also allows future expansion for
specialized device types and additional features more cleanly than xpad.

Future plans include:

- Adding support for more device types, such as flight sticks.
- Support for the security handshake, which is required for devices that
  use wireless dongles.
- Exposing a raw character device to enable sending vendor-specific
  commands from userspace.
- Event logging to either sysfs or dmesg.
- Support for the headphone jack.

Since the last series, this heavily refactors the logic with an eye towards
extensibility. Support for sepcific devices has been split out into
different files in the new input/joystick/gip directory, with a driver-like
interface for probing individual devices.

It also fixes some bugs,including shutdown logic with the input device
potentially freeing the led cdev before it's properly closed, and handling
of -ENOMEM in the led probe routine.

The series has also been split into several additional patches to make
reviewing it easier.

Vicki Pfau (10):
  Input: xbox_gip - Add new driver for Xbox GIP
  Input: xpad - Remove Xbox One support
  Input: xbox_gip - Add controllable LED support
  Input: xbox_gip - Add HID relaying
  Input: xbox_gip - Add battery support
  Input: xbox_gip - Add arcade stick support
  Input: Add ABS_CLUTCH, HANDBRAKE, and SHIFTER
  HID: Map more automobile simulation inputs
  Input: xbox_gip - Add wheel support
  Input: xbox_gip - Add flight stick support

 Documentation/input/devices/xpad.rst          |   17 +-
 MAINTAINERS                                   |    6 +
 drivers/hid/hid-debug.c                       |   16 +-
 drivers/hid/hid-input.c                       |    2 +
 drivers/input/joystick/Kconfig                |    2 +
 drivers/input/joystick/Makefile               |    1 +
 drivers/input/joystick/gip/Kconfig            |   30 +
 drivers/input/joystick/gip/Makefile           |    3 +
 drivers/input/joystick/gip/gip-arcade-stick.c |  169 +
 drivers/input/joystick/gip/gip-core.c         | 2843 +++++++++++++++++
 drivers/input/joystick/gip/gip-drivers.c      |  220 ++
 drivers/input/joystick/gip/gip-flight-stick.c |  179 ++
 drivers/input/joystick/gip/gip-wheel.c        |  348 ++
 drivers/input/joystick/gip/gip.h              |  327 ++
 drivers/input/joystick/xpad.c                 |  634 +---
 include/uapi/linux/input-event-codes.h        |    3 +
 16 files changed, 4153 insertions(+), 647 deletions(-)
 create mode 100644 drivers/input/joystick/gip/Kconfig
 create mode 100644 drivers/input/joystick/gip/Makefile
 create mode 100644 drivers/input/joystick/gip/gip-arcade-stick.c
 create mode 100644 drivers/input/joystick/gip/gip-core.c
 create mode 100644 drivers/input/joystick/gip/gip-drivers.c
 create mode 100644 drivers/input/joystick/gip/gip-flight-stick.c
 create mode 100644 drivers/input/joystick/gip/gip-wheel.c
 create mode 100644 drivers/input/joystick/gip/gip.h

-- 
2.53.0


^ permalink raw reply

* Re: [PATCH v7 1/3] input: trackpoint - Enable doubletap by default on capable devices
From: Vishnu Sankar @ 2026-03-10  2:11 UTC (permalink / raw)
  To: Ilpo Järvinen
  Cc: Mark Pearson, dmitry.torokhov, hmh, Hans de Goede, corbet,
	derekjohn.clark, linux-input, LKML, ibm-acpi-devel, linux-doc,
	platform-driver-x86, vsankar
In-Reply-To: <fdd1d48e-61b7-2543-8a30-c7ad416f5dd3@linux.intel.com>

Hi Ilpo,

Thank you so much for the review.

On Mon, Mar 9, 2026 at 5:01 PM Ilpo Järvinen
<ilpo.jarvinen@linux.intel.com> wrote:
>
> On Mon, 9 Feb 2026, Vishnu Sankar wrote:
>
> > Enable doubletap functionality by default on TrackPoint devices that
> > support it. The feature is detected using firmware ID pattern matching
> > (PNP: LEN03xxx) with a deny list of incompatible devices.
> >
> > This provides immediate doubletap functionality without requiring
> > userspace configuration. The hardware is enabled during device
> > detection, while event filtering continues to be handled by the
> > thinkpad_acpi driver as before.
> >
> > Signed-off-by: Vishnu Sankar <vishnuocv@gmail.com>
> > Suggested-by: Mark Pearson <mpearson-lenovo@squebb.ca>
> > Acked-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>
> > ---
> > Changes in v7:
> > - Removed unwanted comments
> > - Removed psmouse_info ()
> >
> > Changes in v6:
> > - No Changes
> >
> > Changes in v5:
> > - Renamed function to trackpoint_is_dt_capable()
> > - Simplified string comparison without sscanf()
> > - Removed wrapper function as suggested
> > - Fixed missing period in comment
> >
> > Changes in v4:
> > - Simplified approach: removed all sysfs attributes and user interface
> > - Enable doubletap by default during device detection
> > - Removed global variables and complex attribute infrastructure
> > - Uses minimal firmware ID detection with deny list
> > - Follows KISS principle as suggested by reviewers
> >
> > Changes in v3:
> > - No changes
> >
> > Changes in v2:
> > - Improve commit messages
> > - Sysfs attributes moved to trackpoint.c
> > - Removed unnecessary comments
> > - Removed unnecessary debug messages
> > - Using strstarts() instead of strcmp()
> > - is_trackpoint_dt_capable() modified
> > - Removed _BIT suffix and used BIT() define
> > - Reverse the trackpoint_doubletap_status() logic to return error first
> > - Removed export functions as a result of the design change
> > - Changed trackpoint_dev->psmouse to parent_psmouse
> > - The path of trackpoint.h is not changed
> > ---
> >  drivers/input/mouse/trackpoint.c | 45 ++++++++++++++++++++++++++++++++
> >  drivers/input/mouse/trackpoint.h |  5 ++++
> >  2 files changed, 50 insertions(+)
> >
> > diff --git a/drivers/input/mouse/trackpoint.c b/drivers/input/mouse/trackpoint.c
> > index 5f6643b69a2c..e12d76350252 100644
> > --- a/drivers/input/mouse/trackpoint.c
> > +++ b/drivers/input/mouse/trackpoint.c
> > @@ -393,6 +393,45 @@ static int trackpoint_reconnect(struct psmouse *psmouse)
> >       return 0;
> >  }
> >
> > +/* List of known incapable device PNP IDs */
> > +static const char * const dt_incompatible_devices[] = {
> > +     "LEN0304",
> > +     "LEN0306",
> > +     "LEN0317",
> > +     "LEN031A",
> > +     "LEN031B",
> > +     "LEN031C",
> > +     "LEN031D",
> > +};
> > +
> > +/*
> > + * Checks if it's a doubletap capable device.
> > + * The PNP ID format is "PNP: LEN030d PNP0f13".
> > + */
> > +static bool trackpoint_is_dt_capable(const char *pnp_id)
> > +{
> > +     size_t i;
> > +
> > +     if (!pnp_id)
> > +             return false;
> > +
> > +     /* Must start with "PNP: LEN03" */
> > +     if (!strstarts(pnp_id, "PNP: LEN03"))
>
> Missing include.
Sorry, I am a bit confused here:
strstarts() is already available through the existing
#include <linux/string.h> in thinkpad_acpi.c.

Do you think I should do anything else here?
>
> > +             return false;
> > +
> > +     /* Ensure enough length before comparing */
> > +     if (strlen(pnp_id) < 12)
> > +             return false;
> > +
> > +     /* Check deny-list */
> > +     for (i = 0; i < ARRAY_SIZE(dt_incompatible_devices); i++) {
>
> Missing include for ARRAY_SIZE().
Acked.
Will add linux/array_size.h to the include list.
>
> > +             if (!strncmp(pnp_id + 5,
> > +                          dt_incompatible_devices[i], 7))
>
> Fits to one line.
Understood.
will come up with v8 soon.
>
> > +                     return false;
> > +     }
> > +     return true;
> > +}
> > +
> >  int trackpoint_detect(struct psmouse *psmouse, bool set_properties)
> >  {
> >       struct ps2dev *ps2dev = &psmouse->ps2dev;
> > @@ -470,6 +509,12 @@ int trackpoint_detect(struct psmouse *psmouse, bool set_properties)
> >                    psmouse->vendor, firmware_id,
> >                    (button_info & 0xf0) >> 4, button_info & 0x0f);
> >
> > +     if (trackpoint_is_dt_capable(ps2dev->serio->firmware_id)) {
> > +             error = trackpoint_write(ps2dev, TP_DOUBLETAP, TP_DOUBLETAP_ENABLE);
> > +             if (error)
> > +                     psmouse_warn(psmouse, "Failed to enable doubletap: %d\n", error);
> > +     }
> > +
> >       return 0;
> >  }
> >
> > diff --git a/drivers/input/mouse/trackpoint.h b/drivers/input/mouse/trackpoint.h
> > index eb5412904fe0..3e03cdb39449 100644
> > --- a/drivers/input/mouse/trackpoint.h
> > +++ b/drivers/input/mouse/trackpoint.h
> > @@ -69,6 +69,8 @@
> >                                       /* (how hard it is to drag */
> >                                       /* with Z-axis pressed) */
> >
> > +#define TP_DOUBLETAP         0x58    /* TrackPoint doubletap register */
> > +
> >  #define TP_MINDRAG           0x59    /* Minimum amount of force needed */
> >                                       /* to trigger dragging */
> >
> > @@ -110,6 +112,9 @@
> >                                          external device will be forced to 1 */
> >  #define TP_MASK_EXT_TAG                      0x04
> >
> > +/* Doubletap register values */
> > +#define TP_DOUBLETAP_ENABLE  0xFF    /* Enable value */
> > +#define TP_DOUBLETAP_DISABLE 0xFE    /* Disable value */
> >
> >  /* Power on Self Test Results */
> >  #define TP_POR_SUCCESS               0x3B
> >
>
> --
>  i.
>


-- 

Regards,

      Vishnu Sankar

^ permalink raw reply

* Re: [PATCH v7 3/3] Documentation: thinkpad-acpi - Document doubletap_enable attribute
From: Vishnu Sankar @ 2026-03-10  1:37 UTC (permalink / raw)
  To: Ilpo Järvinen
  Cc: Mark Pearson, dmitry.torokhov, hmh, Hans de Goede, corbet,
	derekjohn.clark, linux-input, LKML, ibm-acpi-devel, linux-doc,
	platform-driver-x86, vsankar
In-Reply-To: <991f8a08-7c5d-5611-7904-6c5d336a74a3@linux.intel.com>

Ilpo,

Thank you for the review comments.

On Mon, Mar 9, 2026 at 5:04 PM Ilpo Järvinen
<ilpo.jarvinen@linux.intel.com> wrote:
>
> On Mon, 9 Feb 2026, Vishnu Sankar wrote:
>
> > Document the doubletap_enable sysfs attribute for ThinkPad ACPI driver.
> >
> > Signed-off-by: Vishnu Sankar <vishnuocv@gmail.com>
> > ---
>
> > +        * 1 - doubletap events are processed (default)
> > +     * 0 - doubletap events are filtered out (ignored)
>
> There's something odd in space vs tab here.
I will check this.
>
> --
>  i.
>


-- 

Regards,

      Vishnu Sankar

^ permalink raw reply

* Re: [PATCH v2 7/9] leds: Add driver for Asus Transformer LEDs
From: Lee Jones @ 2026-03-09 19:04 UTC (permalink / raw)
  To: Svyatoslav Ryhel
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Dmitry Torokhov,
	Pavel Machek, Arnd Bergmann, Greg Kroah-Hartman,
	Sebastian Reichel, Michał Mirosław, Ion Agorria,
	devicetree, linux-kernel, linux-input, linux-leds, linux-pm
In-Reply-To: <CAPVz0n0jnuhmvsSPckfFOQ0NZ3VMe6W2p_Bb5a9pdVZYhVSJhQ@mail.gmail.com>

On Fri, 06 Mar 2026, Svyatoslav Ryhel wrote:

> пт, 6 бер. 2026 р. о 12:04 Lee Jones <lee@kernel.org> пише:
> >
> > On Mon, 09 Feb 2026, Svyatoslav Ryhel wrote:
> >
> > > From: Michał Mirosław <mirq-linux@rere.qmqm.pl>
> > >
> > > Asus Transformer tablets have a green and an amber LED on both the Pad
> > > and the Dock. If both LEDs are enabled simultaneously, the emitted light
> > > will be yellow.
> > >
> > > Co-developed-by: Svyatoslav Ryhel <clamor95@gmail.com>
> > > Signed-off-by: Svyatoslav Ryhel <clamor95@gmail.com>
> > > Signed-off-by: Michał Mirosław <mirq-linux@rere.qmqm.pl>
> > > ---
> > >  drivers/leds/Kconfig        |  11 ++++
> > >  drivers/leds/Makefile       |   1 +
> > >  drivers/leds/leds-asus-ec.c | 104 ++++++++++++++++++++++++++++++++++++
> > >  3 files changed, 116 insertions(+)
> > >  create mode 100644 drivers/leds/leds-asus-ec.c
[
...]

> > > +static int asus_ec_led_probe(struct platform_device *pdev)
> > > +{
> > > +     struct asusec_info *ec = cell_to_ec(pdev);
> >
> > Please remove all of your abstraction layers.  They serve little purpose
> > other than to complicate things.  Just use dev_get_drvdata() here.
> >
> > Remove the "_info" part and change "ec" to "ddata".
> >
> 
> Controller exposes only required stuff, exposing controllers internal
> parts is not desired. Is there any written convention that driver
> private data pointer MUST be named "ddata"? "priv" is pretty well
> fitting to. You are just nitpicking.

[...] 

> > Lots of repetition here.
> >
> > I'd make a sub-function that takes the differences.
> >
> > Same with the set brightness functions.
> >
> > Think to yourself - what if I had to support 16 different LEDs?
> >
> 
> That is not of my concern what you would do. I have 2 LEDs and I see
> no point in "abstraction for he sake of abstraction".

Your attitude stinks!

All of your submissions are now on hold until I can be bothered to look
at them again.  In the mean time, I suggest you look inward and
re-evaluate how you choose to communicate with others.

-- 
Lee Jones [李琼斯]

^ permalink raw reply

* Re: (subset) [PATCH 2/3] dt-bindings: mfd: convert fsl-imx25-tsadc.txt to yaml format
From: Lee Jones @ 2026-03-09 18:51 UTC (permalink / raw)
  To: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam, Lee Jones,
	Frank Li
  Cc: linux-input, devicetree, imx, linux-arm-kernel, linux-kernel
In-Reply-To: <20260211-yaml_mfd-v1-2-05cb48bc6f09@nxp.com>

On Wed, 11 Feb 2026 16:41:05 -0500, Frank Li wrote:
> Convert fsl-imx25-tsadc.txt to yaml format.
> 
> Addtional changes:
> - Add ranges.
> 
> 

Applied, thanks!

[2/3] dt-bindings: mfd: convert fsl-imx25-tsadc.txt to yaml format
      commit: 61b80ec6685a08addd76ea6622636d709e7d989d

--
Lee Jones [李琼斯]


^ permalink raw reply

* Re: [PATCH] HID: wacom: fix out-of-bounds read in wacom_intuos_bt_irq
From: Jiri Kosina @ 2026-03-09 18:34 UTC (permalink / raw)
  To: Benoît Sevens
  Cc: Ping Cheng, Jason Gerecke, Benjamin Tissoires, linux-input,
	linux-kernel
In-Reply-To: <20260303135828.2374069-1-bsevens@google.com>

On Tue, 3 Mar 2026, Benoît Sevens wrote:

> The wacom_intuos_bt_irq() function processes Bluetooth HID reports
> without sufficient bounds checking. A maliciously crafted short report
> can trigger an out-of-bounds read when copying data into the wacom
> structure.
> 
> Specifically, report 0x03 requires at least 22 bytes to safely read
> the processed data and battery status, while report 0x04 (which
> falls through to 0x03) requires 32 bytes.
> 
> Add explicit length checks for these report IDs and log a warning if
> a short report is received.
> 
> Signed-off-by: Benoît Sevens <bsevens@google.com>

Applied to hid.git#for-7.0/upstream-fixes, thanks.

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply

* Re: [PATCH] HID: input: use __free(kfree) to clean up temporary buffers
From: Jiri Kosina @ 2026-03-09 18:21 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: Benjamin Tissoires, linux-input, linux-kernel
In-Reply-To: <aaPIp-z92hMsI7J5@google.com>

On Sat, 28 Feb 2026, Dmitry Torokhov wrote:

> The __free() cleanup automatically releases given resource when leaving
> the scope, so use it to make the code less cluttered with error
> handling.
> 
> Signed-off-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>

Applied to hid.git#for-7.1/core. Thanks,

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply

* Re: [PATCH 2/2] HID: input: Add HID_BATTERY_QUIRK_DYNAMIC for Elan touchscreens
From: Jiri Kosina @ 2026-03-09 18:11 UTC (permalink / raw)
  To: Sebastian Reichel
  Cc: Hans de Goede, Dmitry Torokhov, Benjamin Tissoires, linux-input,
	ggrundik
In-Reply-To: <aaWvxHt5RPeSRGxJ@venus>

On Mon, 2 Mar 2026, Sebastian Reichel wrote:

> The rationale and the patch LGTM.

Could I then please get your Acked-by: / Reviewed-by: recorded, so that it 
can be added when applying?

Thanks,

-- 
Jiri Kosina
SUSE Labs


^ permalink raw reply

* [PATCH v3] usbhid: tolerate intermittent errors
From: Liam Mitchell @ 2026-03-09 15:52 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Alan Stern, Oliver Neukum, linux-usb, linux-input, linux-kernel,
	Liam Mitchell

Modifies usbhid error handling to better tolerate intermittent
errors, reducing URB resubmission delay and device resets.

---
Protocol errors like EPROTO can occur randomly, sometimes frequently and
are often not fixed by a device reset.

The current error handling will only resubmit the URB after at least 13ms
delay and may reset the USB device if another error occurs 1-1.5s later, 
regardless of error type or count.

These delays and device resets increase the chance that input events will be
missed and that users see symptoms like missed or sticky keyboard keys.

This patch reduces the threshold used to assume device is working after
discussion in V2. Truly disconnected devices should error after 3 polling
intervals.

It allows single protocol errors to be retried immediately for minimal
downtime.

It changes the retry timeout to be relative to the last input URB
submission so time spent active counts towards the delay.
This makes it more likely that subsequent intermittent errors can be
retried on the next tick.

It only tries reset after 20 errors, roughly about 2s, longer than the
previous time based threshold of 1-1.5s.

Signed-off-by: Liam Mitchell <mitchell.liam@gmail.com>
Link: https://lore.kernel.org/linux-input/CAOQ1CL6Q+4GNy=kgisLzs0UBXFT3b02PG8t-0rPuW-Wf6NhQ6g@mail.gmail.com/
---
Changes in v3:
- uses shorter threshold to assume device is working
- stop_retry & retry_delay -> error_count & last_in
- retry after 20 errors
- includes running time in retry timeout calc
- immediate retry case is integrated with other error logic
- shortens initial retry delay to 1ms
- changes max delay to 128ms
- Link to v2: https://lore.kernel.org/r/20260307-usbhid-eproto-v2-1-e5a8abce4652@gmail.com

Changes in v2:
- revert changes to hid_io_error
- add more specific fix in hid_irq_in
- Link to v1: https://lore.kernel.org/r/20260208-usbhid-eproto-v1-1-5872c10d90bb@gmail.com
---
 drivers/hid/usbhid/hid-core.c | 44 ++++++++++++++++++++++++++++---------------
 drivers/hid/usbhid/usbhid.h   |  4 ++--
 2 files changed, 31 insertions(+), 17 deletions(-)

diff --git a/drivers/hid/usbhid/hid-core.c b/drivers/hid/usbhid/hid-core.c
index 758eb21430cd..233c1950632a 100644
--- a/drivers/hid/usbhid/hid-core.c
+++ b/drivers/hid/usbhid/hid-core.c
@@ -70,6 +70,12 @@ MODULE_PARM_DESC(quirks, "Add/modify USB HID quirks by specifying "
 		" quirks=vendorID:productID:quirks"
 		" where vendorID, productID, and quirks are all in"
 		" 0x-prefixed hex");
+
+/* Threshold at which we can assume a device is working correctly.
+ * A disconnected device should fail within 3 polling intervals.
+ * Most HID devices poll 8ms or faster. */
+#define HID_ASSUME_WORKING msecs_to_jiffies(100)
+
 /*
  * Input submission and I/O error handler.
  */
@@ -90,6 +96,7 @@ static int hid_start_in(struct hid_device *hid)
 	    !test_bit(HID_DISCONNECTED, &usbhid->iofl) &&
 	    !test_bit(HID_SUSPENDED, &usbhid->iofl) &&
 	    !test_and_set_bit(HID_IN_RUNNING, &usbhid->iofl)) {
+		usbhid->last_in = jiffies;
 		rc = usb_submit_urb(usbhid->urbin, GFP_ATOMIC);
 		if (rc != 0) {
 			clear_bit(HID_IN_RUNNING, &usbhid->iofl);
@@ -154,19 +161,13 @@ static void hid_io_error(struct hid_device *hid)
 		goto done;
 
 	/* If it has been a while since the last error, we'll assume
-	 * this a brand new error and reset the retry timeout. */
-	if (time_after(jiffies, usbhid->stop_retry + HZ/2))
-		usbhid->retry_delay = 0;
-
-	/* When an error occurs, retry at increasing intervals */
-	if (usbhid->retry_delay == 0) {
-		usbhid->retry_delay = 13;	/* Then 26, 52, 104, 104, ... */
-		usbhid->stop_retry = jiffies + msecs_to_jiffies(1000);
-	} else if (usbhid->retry_delay < 100)
-		usbhid->retry_delay *= 2;
+	 * this a brand new error and reset the error count. */
+	if (time_after(jiffies, usbhid->last_in + HID_ASSUME_WORKING))
+		usbhid->error_count = 0;
 
-	if (time_after(jiffies, usbhid->stop_retry)) {
+	usbhid->error_count++;
 
+	if (usbhid->error_count >= 20) {
 		/* Retries failed, so do a port reset unless we lack bandwidth*/
 		if (!test_bit(HID_NO_BANDWIDTH, &usbhid->iofl)
 		     && !test_and_set_bit(HID_RESET_PENDING, &usbhid->iofl)) {
@@ -176,8 +177,13 @@ static void hid_io_error(struct hid_device *hid)
 		}
 	}
 
-	mod_timer(&usbhid->io_retry,
-			jiffies + msecs_to_jiffies(usbhid->retry_delay));
+	/* Retry time is relative to the last start time and increases
+	 * with error count: 1, 2, 4, 8, 16, 32, 64, 128, 128... ms.
+	 * By including running time in the backoff, it should be possible
+	 * to retry many intermittent errors in the next tick. */
+	mod_timer(&usbhid->io_retry, usbhid->last_in +
+			msecs_to_jiffies(1U << (min(usbhid->error_count, 8U) - 1)));
+
 done:
 	spin_unlock_irqrestore(&usbhid->lock, flags);
 }
@@ -278,7 +284,7 @@ static void hid_irq_in(struct urb *urb)
 
 	switch (urb->status) {
 	case 0:			/* success */
-		usbhid->retry_delay = 0;
+		usbhid->error_count = 0;
 		if (!test_bit(HID_OPENED, &usbhid->iofl))
 			break;
 		usbhid_mark_busy(usbhid);
@@ -312,6 +318,13 @@ static void hid_irq_in(struct urb *urb)
 	case -EPROTO:		/* protocol error or unplug */
 	case -ETIME:		/* protocol error or unplug */
 	case -ETIMEDOUT:	/* Should never happen, but... */
+		/* Allow first error to retry immediately */
+		if (usbhid->error_count == 0 ||
+		    time_after(jiffies, usbhid->last_in + HID_ASSUME_WORKING)) {
+			dev_dbg(&usbhid->intf->dev, "retrying intr urb immediately\n");
+			usbhid->error_count = 1;
+			break;
+		}
 		usbhid_mark_busy(usbhid);
 		clear_bit(HID_IN_RUNNING, &usbhid->iofl);
 		hid_io_error(hid);
@@ -321,6 +334,7 @@ static void hid_irq_in(struct urb *urb)
 			 urb->status);
 	}
 
+	usbhid->last_in = jiffies;
 	status = usb_submit_urb(urb, GFP_ATOMIC);
 	if (status) {
 		clear_bit(HID_IN_RUNNING, &usbhid->iofl);
@@ -1504,7 +1518,7 @@ static void hid_restart_io(struct hid_device *hid)
 
 	if (clear_halt || reset_pending)
 		schedule_work(&usbhid->reset_work);
-	usbhid->retry_delay = 0;
+	usbhid->error_count = 0;
 	spin_unlock_irq(&usbhid->lock);
 
 	if (reset_pending || !test_bit(HID_STARTED, &usbhid->iofl))
diff --git a/drivers/hid/usbhid/usbhid.h b/drivers/hid/usbhid/usbhid.h
index 75fe85d3d27a..b5dbfa588dc8 100644
--- a/drivers/hid/usbhid/usbhid.h
+++ b/drivers/hid/usbhid/usbhid.h
@@ -84,8 +84,8 @@ struct usbhid_device {
 	spinlock_t lock;						/* fifo spinlock */
 	unsigned long iofl;                                             /* I/O flags (CTRL_RUNNING, OUT_RUNNING) */
 	struct timer_list io_retry;                                     /* Retry timer */
-	unsigned long stop_retry;                                       /* Time to give up, in jiffies */
-	unsigned int retry_delay;                                       /* Delay length in ms */
+	unsigned int error_count;                                       /* Number of consecutive errors */
+	unsigned long last_in;                                          /* Time of last in URB submission */
 	struct work_struct reset_work;                                  /* Task context for resets */
 	wait_queue_head_t wait;						/* For sleeping */
 };

---
base-commit: b91e36222ccfb1b0985d1fcc4fb13b68fb99c972
change-id: 20260208-usbhid-eproto-152c7abcb185

Best regards,
-- 
Liam Mitchell <mitchell.liam@gmail.com>


^ permalink raw reply related

* Re: [PATCH 1/2] iio: hid-sensor-gyro-3d: move iio_device_register() to end of probe()
From: srinivas pandruvada @ 2026-03-09 15:42 UTC (permalink / raw)
  To: Jonathan Cameron, Bhargav Joshi
  Cc: jikos, dlechner, nuno.sa, andy, linux-iio, linux-kernel,
	linux-input
In-Reply-To: <20260307142153.26bab5b0@jic23-huawei>

On Sat, 2026-03-07 at 14:21 +0000, Jonathan Cameron wrote:
> On Mon, 2 Mar 2026 01:00:06 +0530
> Bhargav Joshi <rougueprince47@gmail.com> wrote:
> 
> > On Sun, Mar 1, 2026 at 5:15 PM Jonathan Cameron <jic23@kernel.org>
> > wrote:
> > > 
> > > On Sun,  1 Mar 2026 00:43:59 +0530
> > > Bhargav Joshi <rougueprince47@gmail.com> wrote:
> > >  
> > > > Currently, calling iio_device_register() before
> > > > sensor_hub_register_callback() may create a race condition
> > > > where the
> > > > device is exposed to userspace before callbacks are wired.  
> > > 
> > > We needs some more here on 'why' this is a problem.
> > > Whilst this is an unusual arrangement, I couldn't immediately
> > > find
> > > anything that was actually broken.  It's possible data will turn
> > > up
> > > after we tear down the callbacks and before the userspace
> > > interfaces
> > > are removed, but I think that just results in dropping data.
> > > Given it's
> > > in a race anyway I don't think we care about that. The callbacks
> > > don't seem to be involved in device configuration which can go on
> > > whether
> > > or not the callbacks are registered.  Maybe there is something
> > > that
> > > won't get acknowledged if they aren't in place in time?
> > > 
> > > For a fix like this we'd normally want to see a clear flow that
> > > leads to a bug.
> > > 
> > > Also a fixes tag so we know how far to backport.
> > >  
> > 
> > Hi Jonathan,
> > 
> > I originally submitted the patch for the driver because calling
> > iio_device_register() before the callbacks are wired up violates
> > standard IIO LIFO ordering. I assumed this created a dangerous race
> > condition with userspace. However, as you correctly pointed out,
> > the
> > HID sensor hub safely drops those early events without crashing,
> > even
> > if it is unusual behaviour still won't have a hard crash.
> > 
> > My underlying goal was simply a structural cleanup to align the
> > probe() and remove() sequences with standard IIO architecture.
> > 
> > Would you like me to send a v2 of this patch with a revised commit
> > message classifying it purely as a structural cleanup (and without
> > a
> > Fixes tag), or is the current driver behavior acceptable enough
> > that I
> > should just drop this patch entirely?
> 
> My gut is leave this one alone. It's a rather unusual driver in lots
> of ways, so I don't really mind one more ;)
> 
> Lets see if anyone else has strong views one way or the other.
> 
> Srinivas?
> 
I don't see anything wrong with this. If you register callback before, 
then iio_push_to_buffers_with_timestamp() can be called before
iio_device_register(), although it will not happen because nobody
powered on sensors in the hub.

Thanks,
Srinivs






> > 
> > Thanks,
> > Bhargav
> > 
> > > > 
> > > > Move iio_device_register() to the end of the probe() function
> > > > to prevent
> > > > race condition.
> > > > 
> > > > Consequently, update the error handling path in probe() and in
> > > > remove()
> > > > ensuring that iio_device_unregister() is called first to cut
> > > > off
> > > > userspace access before the hardware callbacks are removed.
> > > > 
> > > > Signed-off-by: Bhargav Joshi <rougueprince47@gmail.com>
> > > > ---
> > > >  drivers/iio/gyro/hid-sensor-gyro-3d.c | 20 ++++++++++---------
> > > > -
> > > >  1 file changed, 10 insertions(+), 10 deletions(-)
> > > > 
> > > > diff --git a/drivers/iio/gyro/hid-sensor-gyro-3d.c
> > > > b/drivers/iio/gyro/hid-sensor-gyro-3d.c
> > > > index c43990c518f7..8e3628cd8529 100644
> > > > --- a/drivers/iio/gyro/hid-sensor-gyro-3d.c
> > > > +++ b/drivers/iio/gyro/hid-sensor-gyro-3d.c
> > > > @@ -333,12 +333,6 @@ static int hid_gyro_3d_probe(struct
> > > > platform_device *pdev)
> > > >               return ret;
> > > >       }
> > > > 
> > > > -     ret = iio_device_register(indio_dev);
> > > > -     if (ret) {
> > > > -             dev_err(&pdev->dev, "device register failed\n");
> > > > -             goto error_remove_trigger;
> > > > -     }
> > > > -
> > > >       gyro_state->callbacks.send_event = gyro_3d_proc_event;
> > > >       gyro_state->callbacks.capture_sample =
> > > > gyro_3d_capture_sample;
> > > >       gyro_state->callbacks.pdev = pdev;
> > > > @@ -346,13 +340,19 @@ static int hid_gyro_3d_probe(struct
> > > > platform_device *pdev)
> > > >                                       &gyro_state->callbacks);
> > > >       if (ret < 0) {
> > > >               dev_err(&pdev->dev, "callback reg failed\n");
> > > > -             goto error_iio_unreg;
> > > > +             goto error_remove_trigger;
> > > > +     }
> > > > +
> > > > +     ret = iio_device_register(indio_dev);
> > > > +     if (ret) {
> > > > +             dev_err(&pdev->dev, "device register failed\n");
> > > > +             goto error_remove_callback;
> > > >       }
> > > > 
> > > >       return ret;
> > > > 
> > > > -error_iio_unreg:
> > > > -     iio_device_unregister(indio_dev);
> > > > +error_remove_callback:
> > > > +     sensor_hub_remove_callback(hsdev,
> > > > HID_USAGE_SENSOR_GYRO_3D);
> > > >  error_remove_trigger:
> > > >       hid_sensor_remove_trigger(indio_dev, &gyro_state-
> > > > >common_attributes);
> > > >       return ret;
> > > > @@ -365,8 +365,8 @@ static void hid_gyro_3d_remove(struct
> > > > platform_device *pdev)
> > > >       struct iio_dev *indio_dev = platform_get_drvdata(pdev);
> > > >       struct gyro_3d_state *gyro_state = iio_priv(indio_dev);
> > > > 
> > > > -     sensor_hub_remove_callback(hsdev,
> > > > HID_USAGE_SENSOR_GYRO_3D);
> > > >       iio_device_unregister(indio_dev);
> > > > +     sensor_hub_remove_callback(hsdev,
> > > > HID_USAGE_SENSOR_GYRO_3D);
> > > >       hid_sensor_remove_trigger(indio_dev, &gyro_state-
> > > > >common_attributes);
> > > >  }
> > > >  
> > >  

^ permalink raw reply

* [RFC v3 2/2] HID: core: Check to ensure report responses match the request
From: Lee Jones @ 2026-03-09 14:59 UTC (permalink / raw)
  To: lee, Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel
In-Reply-To: <20260309145942.1496072-1-lee@kernel.org>

It is possible for a malicious (or clumsy) device to respond to a
specific report's feature request using a completely different report
ID.  This can cause confusion in the HID core resulting in nasty
side-effects such as OOB writes.

Add a check to ensure that the report ID in the response, matches the
one that was requested.

Signed-off-by: Lee Jones <lee@kernel.org>
---
v2 -> v3: Cover more bases by moving the check up a layer from MT to HID Core

RFC query: Is this always okay?
           Should the report number always match the request?
	   Are there legitimate times where the two would differ?

 drivers/hid/hid-core.c | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index da9231ca42bc..da4078554331 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2490,8 +2490,17 @@ int __hid_hw_raw_request(struct hid_device *hdev,
 	if (ret)
 		return ret;
 
-	return hdev->ll_driver->raw_request(hdev, reportnum, buf, len,
-					    rtype, reqtype);
+	ret = hdev->ll_driver->raw_request(hdev, reportnum, buf, len,
+					   rtype, reqtype);
+	if (ret)
+		return ret;
+
+	if (reportnum != buf[0]) {
+		hid_err(hdev, "Returned feature report did not match the request\n");
+		return -EINVAL;
+	}
+
+	return 0;
 }
 
 /**
-- 
2.53.0.473.g4a7958ca14-goog


^ permalink raw reply related

* [RFC v3 1/2] HID: core: Mitigate potential OOB by removing bogus memset()
From: Lee Jones @ 2026-03-09 14:59 UTC (permalink / raw)
  To: lee, Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel

The memset() in hid_report_raw_event() has the good intention of
clearing out bogus data by zeroing the area from the end of the incoming
data string to the assumed end of the buffer.  However, as we have
previously seen, doing so can easily result in OOB reads and writes in
the subsequent thread of execution.

The current suggestion from one of the HID maintainers is to remove the
memset() and simply return if the incoming event buffer size is not
large enough to fill the associated report.

Suggested-by Benjamin Tissoires <bentiss@kernel.org>
Signed-off-by: Lee Jones <lee@kernel.org>
---
v2 -> v3: Instead of removing the check entirely, show a warning and return early

RFC query: Is it better to return SUCCESS or -EINVAL?

 drivers/hid/hid-core.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index a5b3a8ca2fcb..da9231ca42bc 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2057,9 +2057,9 @@ int hid_report_raw_event(struct hid_device *hid, enum hid_report_type type, u8 *
 		rsize = max_buffer_size;
 
 	if (csize < rsize) {
-		dbg_hid("report %d is too short, (%d < %d)\n", report->id,
-				csize, rsize);
-		memset(cdata + csize, 0, rsize - csize);
+		hid_warn_ratelimited(hid, "Event data for report %d was too short (%d vs %d)\n",
+				     report->id, rsize, csize);
+		goto out;
 	}
 
 	if ((hid->claimed & HID_CLAIMED_HIDDEV) && hid->hiddev_report_event)
-- 
2.53.0.473.g4a7958ca14-goog


^ permalink raw reply related

* Re: [PATCH v2 0/5] iio: buffer: fix timestamp alignment (in rare case)
From: Nuno Sá @ 2026-03-09 14:15 UTC (permalink / raw)
  To: David Lechner, Jiri Kosina, Jonathan Cameron, Srinivas Pandruvada,
	Nuno Sá, Andy Shevchenko, Lars Möllendorf,
	Lars-Peter Clausen, Greg Kroah-Hartman
  Cc: Jonathan Cameron, Lixu Zhang, Francesco Lavra, linux-input,
	linux-iio, linux-kernel
In-Reply-To: <20260307-iio-fix-timestamp-alignment-v2-0-d1d48fbadbbf@baylibre.com>

On Sat, 2026-03-07 at 19:44 -0600, David Lechner wrote:
> In [1], it was pointed out that the iio_push_to_buffers_with_timestamp()
> function is not putting the timestamp at the correct offset in the scan
> buffer in rare cases where the largest scan element size is larger than
> sizeof(int64_t).
> 
> [1]: https://lore.kernel.org/linux-iio/20260215162351.79f40b32@jic23-huawei/
> 
> This only affected one driver, namely hid-sensor-rotation since it is
> the only driver that meets the condition. To fix things up, first we
> fix the hid-sensor-rotation driver in a way that preserves compatibility
> with the broken timestamp alignment. Then we are free to fix the core
> IIO code without affecting any users.
> 
> The first patch depends on [2] which is now in iio/fixes-togreg. It
> should be OK to apply the first patch there and let the rest of the
> patches go through iio/togreg (the later patches are just preventing
> future bugs).
> 
> [2]:
> https://lore.kernel.org/linux-iio/20260228-iio-fix-repeat-alignment-v2-0-d58bfaa2920d@baylibre.com/
> 
> Signed-off-by: David Lechner <dlechner@baylibre.com>
> ---

LGTM,

Reviewed-by: Nuno Sá <nuno.sa@analog.com>

> Changes in v2:
> - Don't say "HACK" in comments.
> - Cache timestamp offset instead of largest scan element size.
> - New patch to ensure size/alignment is always power of 2 bytes.
> - Link to v1:
> https://lore.kernel.org/r/20260301-iio-fix-timestamp-alignment-v1-0-1a54980bfb90@baylibre.com
> 
> ---
> David Lechner (5):
>       iio: orientation: hid-sensor-rotation: add timestamp hack to not break userspace
>       iio: buffer: check return value of iio_compute_scan_bytes()
>       iio: buffer: cache timestamp offset in scan buffer
>       iio: buffer: ensure repeat alignment is a power of two
>       iio: buffer: fix timestamp alignment when quaternion in scan
> 
>  drivers/iio/industrialio-buffer.c             | 46 ++++++++++++++++++++-------
>  drivers/iio/orientation/hid-sensor-rotation.c | 22 +++++++++++--
>  include/linux/iio/buffer.h                    | 12 +++++--
>  include/linux/iio/iio.h                       |  3 ++
>  4 files changed, 66 insertions(+), 17 deletions(-)
> ---
> base-commit: 6f25a6105c41a7d6b12986dbe80ded396a5667f8
> change-id: 20260228-iio-fix-timestamp-alignment-89ade1af458b
> prerequisite-message-id: <20260228-iio-fix-repeat-alignment-v2-0-d58bfaa2920d@baylibre.com>
> prerequisite-patch-id: e155a526d57c5759a2fcfbfca7f544cb419addfd
> prerequisite-patch-id: 6c69eaad0dd2ae69bd2745e7d387f739fc1a9ba0
> 
> Best regards,
> --  
> David Lechner <dlechner@baylibre.com>

^ permalink raw reply

* Re: [PATCH v4 1/2] dt-bindings: Input: Add Wacom W9000-series penabled touchscreens
From: Krzysztof Kozlowski @ 2026-03-09 13:21 UTC (permalink / raw)
  To: Hendrik Noack
  Cc: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Ferass El Hafidi, linux-input, devicetree, linux-kernel
In-Reply-To: <f067754d-229c-4418-a367-6df5feb6cee0@gmx.de>

On 09/03/2026 14:12, Hendrik Noack wrote:
> Hello Krzysztof,
> 
> 09.03.2026 13:56:41 Krzysztof Kozlowski <krzk@kernel.org>:
> 
>> On 09/03/2026 13:54, Hendrik Noack wrote:
>>> Hello Krzysztof,
>>>
>>> 08.03.2026 10:15:35 Krzysztof Kozlowski <krzk@kernel.org>:
>>>
>>>> You received review and instruction what to do. Did you read it?
>>>
>>> I read the review of Dmitry and incorporated it into this version.
>>
>> So you ignored my email completely or it did not reach you (it is on
>> lore.kernel.org though)?
> 
> I don't know what email you mean. You gave reviews on my first verison, which I already incorporated in v2 and then gave a review-by on v2, which I also added on v3, but now dropped, because I added a property to the DT binding.

The instruction I gave you when giving review.

Please wrap your emails to standard email style, so reading will be easier.

Best regards,
Krzysztof

^ permalink raw reply

* Re: [PATCH v4 1/2] dt-bindings: Input: Add Wacom W9000-series penabled touchscreens
From: Hendrik Noack @ 2026-03-09 13:12 UTC (permalink / raw)
  To: Krzysztof Kozlowski
  Cc: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Ferass El Hafidi, linux-input, devicetree, linux-kernel
In-Reply-To: <fddf0e3b-707e-41b3-805b-2abeffed6c05@kernel.org>

Hello Krzysztof,

09.03.2026 13:56:41 Krzysztof Kozlowski <krzk@kernel.org>:

> On 09/03/2026 13:54, Hendrik Noack wrote:
>> Hello Krzysztof,
>>
>> 08.03.2026 10:15:35 Krzysztof Kozlowski <krzk@kernel.org>:
>>
>>> You received review and instruction what to do. Did you read it?
>>
>> I read the review of Dmitry and incorporated it into this version.
>
> So you ignored my email completely or it did not reach you (it is on
> lore.kernel.org though)?

I don't know what email you mean. You gave reviews on my first verison, which I already incorporated in v2 and then gave a review-by on v2, which I also added on v3, but now dropped, because I added a property to the DT binding.

Best regards,
Hendrik

^ permalink raw reply

* Re: [PATCH v4 1/2] dt-bindings: Input: Add Wacom W9000-series penabled touchscreens
From: Krzysztof Kozlowski @ 2026-03-09 12:56 UTC (permalink / raw)
  To: Hendrik Noack
  Cc: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Ferass El Hafidi, linux-input, devicetree, linux-kernel
In-Reply-To: <feae85c4-bea1-40b2-87d5-952b26ff3ee3@gmx.de>

On 09/03/2026 13:54, Hendrik Noack wrote:
> Hello Krzysztof,
> 
> 08.03.2026 10:15:35 Krzysztof Kozlowski <krzk@kernel.org>:
> 
>> You received review and instruction what to do. Did you read it?
> 
> I read the review of Dmitry and incorporated it into this version.

So you ignored my email completely or it did not reach you (it is on
lore.kernel.org though)?

Best regards,
Krzysztof

^ permalink raw reply

* Re: [PATCH v4 1/2] dt-bindings: Input: Add Wacom W9000-series penabled touchscreens
From: Hendrik Noack @ 2026-03-09 12:54 UTC (permalink / raw)
  To: Krzysztof Kozlowski
  Cc: Dmitry Torokhov, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Ferass El Hafidi, linux-input, devicetree, linux-kernel
In-Reply-To: <20260308-vivacious-coucal-of-current-2d7ac8@quoll>

Hello Krzysztof,

08.03.2026 10:15:35 Krzysztof Kozlowski <krzk@kernel.org>:

> You received review and instruction what to do. Did you read it?

I read the review of Dmitry and incorporated it into this version.

> Your way of organizing your work makes it difficult for us. Look, try
> yourself:
>
> b4 diff '20260307181557.66927-2-hendrik-noack@gmx.de'
> Checking for older revisions
> Grabbing search results from lore.kernel.org
>   Added from v3: 2 patches
> ---
> Analyzing 16 messages in the thread
> Preparing fake-am for v3: dt-bindings: Input: Add Wacom W9000-series penabled touchscreens
> ERROR: v3 series incomplete; unable to create a fake-am range
> ---
> Could not create fake-am range for lower series v3
>
>
> Best regards,
> Krzysztof

The b4 diff error might happen because git didn't add the v3 label on
the 2/2 patch of v3. First running b4 diff on v3 with:
b4 diff '20251205152858.14415-2-hendrik-noack@gmx.de
resulted in a working diff on v4:

b4 diff '20260307181557.66927-2-hendrik-noack@gmx.de'
Grabbing thread from lore.kernel.org/all/20260307181557.66927-2-hendrik-noack@gmx.de/t.mbox.gz
Checking for older revisions
Grabbing search results from lore.kernel.org
  Added from v3: 2 patches
---
Analyzing 17 messages in the thread
---
Diffing v3 and v4
    Running: git range-diff c371c3410e81..d467d6770a1b 0bfd1357fb44..1c9f51e8b572
---
...

Best regards,
Hendrik

^ permalink raw reply

* Re: [6.18.] ThinkPad T14 Gen 2 AMD (LEN2073) - Synaptics touchpad remains PS/2, intertouch=1 ineffective, lost sync events
From: Rácz Máté @ 2026-03-09 11:54 UTC (permalink / raw)
  To: Thorsten Leemhuis; +Cc: linux-input, linux-i2c, Linux kernel regressions list
In-Reply-To: <68d90523-33dc-451f-a825-72eaa1c4bcb8@leemhuis.info>

Hi Thorsten,

Thanks for the response.

I have not yet tested with 7.0-rc, but I will try building and booting the
latest mainline release candidate and report whether the issue is still
present.

One additional observation: the touchpad PNP ID on this system is
LEN2073. I noticed that this ID does not appear in the Synaptics SMBus
whitelist in drivers/input/mouse/synaptics.c.

Because of this I am wondering whether the device might simply be
missing from the whitelist rather than this being a regression.

I'll first verify the behaviour on 7.0-rc and then investigate further
(if needed I can also attempt a bisect).

Best regards,
Mate

Thorsten Leemhuis <regressions@leemhuis.info> ezt írta (időpont: 2026.
márc. 9., H, 9:12):
>
> Hi!
>
> On 3/5/26 19:07, Rácz Máté wrote:
> >
> > I did some additional checks on my system with kernel 6.18.13-200.fc43.x86_64:
>
> Have you tried if 7.0-rc is still affected?
>
> And if it is: could you bisect? See
> https://docs.kernel.org/admin-guide/bug-bisect.html
> for details. Without a bisection developers developers from both
> subsystems involved (i2c, input) might not look into this, as everyone
> will suspect it might be some change to the other subsystems that causes
> this.
>
> Ciao, Thorsten
>
> > 1. Kernel configuration (relevant parts):
> >
> > $ grep -i "RMI\|SMBus" /boot/config-$(uname -r)
> > CONFIG_MOUSE_PS2_SYNAPTICS_SMBUS=y
> > CONFIG_RMI4_CORE=m
> > CONFIG_RMI4_I2C=m
> > CONFIG_RMI4_SMB=m
> > CONFIG_HID_RMI=m
> > CONFIG_I2C_HID=y
> > CONFIG_I2C_HID_ACPI=m
> >
> > 2. dmesg output (filtered):
> >
> > [ 1.615300] psmouse serio1: synaptics: Trying to set up SMBus access
> > [ 1.618151] psmouse serio1: synaptics: SMbus companion is not ready yet
> > [ 17.949336] psmouse serio1: TouchPad at isa0060/serio1/input0 lost
> > sync at byte 1
> > [ 17.962506] psmouse serio1: issuing reconnect request
> > (repeats multiple times)
> >
> > 3. libinput reports:
> >
> > $ sudo libinput list-devices | grep -i SynPS
> > Device: SynPS/2 Synaptics TouchPad
> >
> > Interpretation:
> >
> > - The touchpad is recognized as a SynPS/2 device, i.e., fallback PS/2 mode.
> > - SMBus/RMI4 driver stack does not get loaded, despite the kernel
> > config enabling modules.
> > - The "SMBus companion is not ready yet" messages indicate that
> > psmouse attempts SMBus access but cannot reach the device.
> > - I2C controllers are functional, yet the touchpad does not enumerate
> > as an I2C HID device.
> >
> > Workarounds:
> >
> > - Using psmouse.proto=imps avoids lost sync events, confirming
> > fallback to generic PS/2 handling.
> >
> > Observation regarding previous reports:
> >
> > - Unlike the 2021 report on ThinkPad P14s Gen 2 (AMD), I do not see
> > any "bad SMBus base address" messages in dmesg.
> > - The kernel attempts SMBus access, but fails because the RMI4/hid-rmi
> > stack does not initialize. The device simply falls back to PS/2 mode.
> > - Therefore, this does not appear to be the same base address issue as
> > the earlier P14s case.
> >
> > Conclusion:
> >
> > - Behavior strongly suggests a missing DMI quirk or regression in the
> > SMBus detection logic in combination with i2c_piix4 on this Lenovo
> > ThinkPad T14 Gen 2 (AMD) with LEN2073 touchpad.
> > - This is reproducible on multiple 6.18.x kernel versions on Fedora 43
> > stock kernels.
> >
> > I can provide full dmesg, libinput outputs, and acpidump if required.
> >
> > Best regards,
> > Mate Racz
> >
> > Rácz Máté <raczm0812@gmail.com> ezt írta (időpont: 2026. márc. 4., Sze, 16:02):
> >>
> >> This might be related to the earlier report from 2021:
> >>
> >> https://lore.kernel.org/linux-input/3d6f7f74-3214-4c03-b352-a2a0d27ea42b@amd.com/
> >>
> >> and a similar report from 2025:
> >>
> >> https://lore.kernel.org/linux-input/b2d0af40-876e-4a2d-99a2-236b583e9497@gmail.com/
> >>
> >> The 2021 report describes incorrect SMBus address detection with
> >> i2c_piix4 on ThinkPad P14s Gen 2 (AMD), resulting in RMI4/SMBus not
> >> functioning correctly and the device remaining in PS/2 mode.
> >>
> >> On my system (LEN2073), the behaviour appears very similar:
> >> - intertouch has no effect
> >> - "SMBus companion is not ready yet" in dmesg
> >> - device stays in PS/2 mode
> >>
> >> It seems possible that the same underlying piix4 SMBus detection
> >> issue is still present.
>

^ permalink raw reply

* [PATCH 2/2] Input: Touchscreen: tsc200x - delegate wakeup IRQ management to I2C core
From: phucduc.bui @ 2026-03-09 11:00 UTC (permalink / raw)
  To: Dmitry Torokhov, Rob Herring
  Cc: Krzysztof Kozlowski, Conor Dooley, Ingo Molnar, Thomas Gleixner,
	Marek Vasut, Michael Welling, linux-input, devicetree,
	linux-kernel, phucduc.bui
In-Reply-To: <20260309110045.108209-1-phucduc.bui@gmail.com>

From: bui duc phuc <phucduc.bui@gmail.com>

The tsc200x driver supports both I2C (tsc2004) and SPI (tsc2005)
 interfaces.
Currently, the driver attempts to manually manage the wakeup interrupt by
calling enable_irq_wake() and disable_irq_wake() during suspend and resume.

However, for I2C devices, the I2C core already automatically handles the
wakeup source initialization and IRQ management if the "wakeup-source"
property is present in the device tree. Manually managing it again in the
driver is redundant and can lead to unbalanced IRQ wake reference counts.

Clean up the wakeup IRQ handling by checking the bus type:
- For I2C (BUS_I2C): Rely entirely on the I2C core for wakeup management.
- For SPI (BUS_SPI): Explicitly call device_init_wakeup() in probe and
  manually manage enable/disable_irq_wake() during suspend/resume.

The ts->wake_irq_enabled flag is also updated accordingly to ensure the
driver accurately tracks the wakeup state across both buses.

Note: This patch is based on code analysis of the I2C subsystem and
has not been verified on actual hardware yet.

Signed-off-by: bui duc phuc <phucduc.bui@gmail.com>
---
 drivers/input/touchscreen/tsc200x-core.c | 18 +++++++++++++-----
 1 file changed, 13 insertions(+), 5 deletions(-)

diff --git a/drivers/input/touchscreen/tsc200x-core.c b/drivers/input/touchscreen/tsc200x-core.c
index eba53613b005..d14d967845c8 100644
--- a/drivers/input/touchscreen/tsc200x-core.c
+++ b/drivers/input/touchscreen/tsc200x-core.c
@@ -465,6 +465,7 @@ int tsc200x_probe(struct device *dev, int irq, const struct input_id *tsc_id,
 	ts->idev = input_dev;
 	ts->regmap = regmap;
 	ts->tsc200x_cmd = tsc200x_cmd;
+	ts->bustype = tsc_id->bustype;
 
 	error = device_property_read_u32(dev, "ti,x-plate-ohms", &x_plate_ohm);
 	ts->x_plate_ohm = error ? TSC200X_DEF_RESISTOR : x_plate_ohm;
@@ -547,8 +548,9 @@ int tsc200x_probe(struct device *dev, int irq, const struct input_id *tsc_id,
 		return error;
 	}
 
-	device_init_wakeup(dev,
-			   device_property_read_bool(dev, "wakeup-source"));
+	if (ts->bustype == BUS_SPI)
+		device_init_wakeup(dev,
+				 device_property_read_bool(dev, "wakeup-source"));
 
 	return 0;
 }
@@ -565,8 +567,13 @@ static int tsc200x_suspend(struct device *dev)
 
 	ts->suspended = true;
 
-	if (device_may_wakeup(dev))
-		ts->wake_irq_enabled = enable_irq_wake(ts->irq) == 0;
+	if (device_may_wakeup(dev)) {
+		if (ts->bustype == BUS_SPI)
+			ts->wake_irq_enabled = enable_irq_wake(ts->irq) == 0;
+		else
+			ts->wake_irq_enabled = true;
+	} else
+		ts->wake_irq_enabled = false;
 
 	return 0;
 }
@@ -578,7 +585,8 @@ static int tsc200x_resume(struct device *dev)
 	guard(mutex)(&ts->mutex);
 
 	if (ts->wake_irq_enabled) {
-		disable_irq_wake(ts->irq);
+		if (ts->bustype == BUS_SPI)
+			disable_irq_wake(ts->irq);
 		ts->wake_irq_enabled = false;
 	}
 
-- 
2.43.0


^ permalink raw reply related

* [PATCH 1/2] dt-bindings: input: touchscreen: ti,tsc2005: Add wakeup-source
From: phucduc.bui @ 2026-03-09 11:00 UTC (permalink / raw)
  To: Dmitry Torokhov, Rob Herring
  Cc: Krzysztof Kozlowski, Conor Dooley, Ingo Molnar, Thomas Gleixner,
	Marek Vasut, Michael Welling, linux-input, devicetree,
	linux-kernel, phucduc.bui
In-Reply-To: <20260309110045.108209-1-phucduc.bui@gmail.com>

From: bui duc phuc <phucduc.bui@gmail.com>

The tsc200x driver uses the "wakeup-source" device tree property to
determine whether the device should be configured as a system wakeup
source.

In the driver, this property is read with:

    device_init_wakeup(dev,
                       device_property_read_bool(dev, "wakeup-source"));

Document this property in the binding to make it visible to DT schema
validation tools and to clarify its usage in device tree descriptions.

Signed-off-by: bui duc phuc <phucduc.bui@gmail.com>
---
 .../devicetree/bindings/input/touchscreen/ti,tsc2005.yaml  | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml b/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml
index 7187c390b2f5..c0aae044d7d4 100644
--- a/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml
+++ b/Documentation/devicetree/bindings/input/touchscreen/ti,tsc2005.yaml
@@ -55,6 +55,9 @@ properties:
   touchscreen-size-x: true
   touchscreen-size-y: true
 
+  wakeup-source:
+    type: boolean
+
 allOf:
   - $ref: touchscreen.yaml#
   - if:
@@ -97,6 +100,8 @@ examples:
 
             ti,x-plate-ohms = <280>;
             ti,esd-recovery-timeout-ms = <8000>;
+
+            wakeup-source;
         };
     };
   - |
@@ -124,5 +129,7 @@ examples:
 
             ti,x-plate-ohms = <280>;
             ti,esd-recovery-timeout-ms = <8000>;
+
+            wakeup-source;
         };
     };
-- 
2.43.0


^ permalink raw reply related

* [PATCH 0/2] Input: tsc200x: Improve wakeup source handling
From: phucduc.bui @ 2026-03-09 11:00 UTC (permalink / raw)
  To: Dmitry Torokhov, Rob Herring
  Cc: Krzysztof Kozlowski, Conor Dooley, Ingo Molnar, Thomas Gleixner,
	Marek Vasut, Michael Welling, linux-input, devicetree,
	linux-kernel, phucduc.bui

From: bui duc phuc <phucduc.bui@gmail.com>

The tsc200x driver already uses device_init_wakeup() to read the
"wakeup-source" property from the Device Tree. However, this property
is currently not documented in the DT binding schema.

In addition, the I2C core already handles wakeup initialization and
IRQ wake management automatically when the "wakeup-source" property
is present. Therefore, the manual wakeup IRQ handling currently done
in the driver is redundant for I2C-based devices (TSC2004).

This series makes the following changes:

 1. Document the "wakeup-source" property in the DT bindings.

 2. Delegate wakeup IRQ management to the I2C core when running on
    BUS_I2C, while keeping manual management for BUS_SPI (TSC2005)
    to ensure correct behavior across both interfaces.

Note:
These changes are based on code inspection and the documented behavior
of the I2C core. They have not been tested on physical hardware yet.

bui duc phuc (2):
  dt-bindings: input: touchscreen: ti,tsc2005: Add wakeup-source
  Input: Touchscreen: tsc200x - delegate wakeup IRQ management to I2C
    core

 .../bindings/input/touchscreen/ti,tsc2005.yaml |  7 +++++++
 drivers/input/touchscreen/tsc200x-core.c       | 18 +++++++++++++-----
 2 files changed, 20 insertions(+), 5 deletions(-)

-- 
2.43.0


^ permalink raw reply

* Re: [PATCH] HID: logitech-hidpp: Add support for HID++ Multi-Platform feature (0x4531)
From: Bastien Nocera @ 2026-03-09  9:53 UTC (permalink / raw)
  To: DevExalt, jikos, bentiss
  Cc: lains, linux-input, linux-kernel, sari.kreitem, hbarnor
In-Reply-To: <20251215125319.33261-1-exalt.dev.team@gmail.com>

Hey,

Sorry for not looking at this earlier, it slipped through the cracks as
it arrived on the mailing-list as I was away.

On Mon, 2025-12-15 at 14:53 +0200, DevExalt wrote:
> From: "Baraa Atta (Dev Exalt)" <exalt.dev.team@gmail.com>
> 
> Add support in the Logitech HID++ driver for the HID++ Multi-Platform
> feature (0x4531), which enables HID++ devices to adjust their
> behavior
> based on the host operating system (Linux, ChromeOS, Android).

Can you please explain what the feature actually does ? (the Logitech
docs say "Set the right keyboard layout for your computer operating
system" and mention that some multimedia keys are inoperable unless a
compatible OS is set).

> 
> This patch:
>  * Adds device IDs for MX Keys S (046d:b378) and Casa Keys
> (046d:b371).
>  * Introduces the module parameter "hidpp_platform" to allow
> selecting a
>    target platform.
>  * Detects whether a device implements feature 0x4531.
>  * Validates that the requested platform is supported by the device.
>  * Applies the platform index when valid, otherwise leaves the device
>    unchanged.
>  * Keeps default behavior when "hidpp_platform" is unset or invalid.

Can you explain the benefits of setting this module parameter, compared
to using the keyboard shortcuts to switch to a specific OS
configuration?

What happens when 2 Logitech devices with different supported OSes are
used?

> Supported values for hidpp_platform:
>    Android, Linux, Chrome

Any reason why there aren't more supported OSes?

The Logitech docs[1] lists:
WebOS iOS MacOS Android Chrome Linux WinEmb Windows Tizen
as possible values.

[1]:
https://drive.google.com/file/d/1KyiBA5m_5V1s6jQ9eQrgRJN0SbbbI9_I/view

I recently got a K980 which has this functionality, it only documents
Windows, macOS and Chrome, but Solaar also lists Linux as an option.

So my questions would be:
- why not support the whole range of possible OSes in this option?
- why is it a module option instead of, say, a sysfs attribute that
could be changed per device?
- why not implement this in user-space through a udev callout?

Cheers

> 
> TEST=Pair MX Keys S and Casa Keys over Bluetooth and verify:
>      * Feature 0x4531 is detected.
>      * Valid platform values are accepted and applied.
>      * Invalid platform values result in no update.
>      * Devices without 0x4531 retain default behavior.
>      * Platform-specific key behavior is observed once applied.
> 
> Signed-off-by: Baraa Atta (Dev Exalt) <exalt.dev.team@gmail.com>
> ---
>  drivers/hid/hid-ids.h            |   2 +
>  drivers/hid/hid-logitech-hidpp.c | 280
> +++++++++++++++++++++++++++++++
>  drivers/hid/hid-quirks.c         |   2 +
>  3 files changed, 284 insertions(+)
> 
> diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
> index d31711f1aaec..12de1194d7fa 100644
> --- a/drivers/hid/hid-ids.h
> +++ b/drivers/hid/hid-ids.h
> @@ -866,6 +866,8 @@
>  #define USB_DEVICE_ID_LOGITECH_T651	0xb00c
>  #define USB_DEVICE_ID_LOGITECH_DINOVO_EDGE_KBD	0xb309
>  #define USB_DEVICE_ID_LOGITECH_CASA_TOUCHPAD	0xbb00
> +#define USB_DEVICE_ID_LOGITECH_CASA_KEYS_KEYBOARD	0xb371
> +#define USB_DEVICE_ID_LOGITECH_MX_KEYS_S_KEYBOARD	0xb378
>  #define USB_DEVICE_ID_LOGITECH_C007	0xc007
>  #define USB_DEVICE_ID_LOGITECH_C077	0xc077
>  #define USB_DEVICE_ID_LOGITECH_RECEIVER	0xc101
> diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-
> logitech-hidpp.c
> index d5011a5d0890..e94daed31981 100644
> --- a/drivers/hid/hid-logitech-hidpp.c
> +++ b/drivers/hid/hid-logitech-hidpp.c
> @@ -4373,6 +4373,280 @@ static bool hidpp_application_equals(struct
> hid_device *hdev,
>  	return report && report->application == application;
>  }
>  
> +/* -----------------------------------------------------------------
> --------- */
> +/* 0x4531: Multi-Platform
> Support                                             */
> +/* -----------------------------------------------------------------
> --------- */
> +
> +/*
> + * Some Logitech devices expose the HID++ feature 0x4531 (Multi-
> Platform) allowing
> + * the host to specify which operating system platform to use on the
> device. Changing device's
> + * platform may alter the behavior of the device to match the
> specified platform.
> + */
> +
> +static char *hidpp_platform;
> +module_param(hidpp_platform, charp, 0644);
> +MODULE_PARM_DESC(hidpp_platform, "Select host platform type for
> Logitech HID++ Multi-Platform feature "
> +		 "0x4531, valid values: (linux|chrome|android).  If
> unset, no "
> +		 "change is applied.");
> +
> +#define HIDPP_MULTIPLATFORM_FEAT_ID			0x4531
> +#define HIDPP_MULTIPLATFORM_GET_FEATURE_INFO		0x0F
> +#define HIDPP_MULTIPLATFORM_GET_PLATFORM_DESCRIPTOR	0x1F
> +#define HIDPP_MULTIPLATFORM_SET_CURRENT_PLATFORM	0x3F
> +
> +#define
> HIDPP_MULTIPLATFORM_PLATFORM_MASK_LINUX		BIT(10)
> +#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_CHROME	BIT(11)
> +#define HIDPP_MULTIPLATFORM_PLATFORM_MASK_ANDROID	BIT(12)
> +
> +struct hidpp_platform_desc {
> +	u8 plat_idx;
> +	u8 desc_idx;
> +	u16 plat_mask;
> +};
> +
> +/**
> + * hidpp_multiplatform_mask_from_str() - Convert platform name to an
> HID++ platform mask
> + * @pname: Platform name string
> + *
> + * Converts a platform name string to its corresponding HID++
> platform mask based on
> + * the Multi-Platform feature specification.
> + *
> + * Return: Platform mask corresponding to @pname on success,
> + * or 0 if @pname is NULL or unsupported.
> + */
> +static u16 hidpp_multiplatform_mask_from_str(const char *pname)
> +{
> +	if (!pname)
> +		return 0;
> +
> +	if (!strcasecmp(pname, "linux"))
> +		return HIDPP_MULTIPLATFORM_PLATFORM_MASK_LINUX;
> +	if (!strcasecmp(pname, "chrome"))
> +		return HIDPP_MULTIPLATFORM_PLATFORM_MASK_CHROME;
> +	if (!strcasecmp(pname, "android"))
> +		return HIDPP_MULTIPLATFORM_PLATFORM_MASK_ANDROID;
> +
> +	return 0;
> +}
> +
> +/**
> + * hidpp_multiplatform_get_num_pdesc() - Retrieve number of platform
> descriptors
> + * @hidpp: Pointer to the hidpp_device instance
> + * @feat_index: Feature index of the Multi-Platform feature
> + * @num_desc: Pointer to store the number of platform descriptors
> + *
> + * Retrieves the number of platform descriptors supported by the
> device through
> + * the Multi-Platform feature and stores it in @num_desc.
> + *
> + * Return: 0 on success, or non-zero on failure.
> + */
> +static int hidpp_multiplatform_get_num_pdesc(struct hidpp_device
> *hidpp,
> +					     u8 feat_index, u8
> *num_desc)
> +{
> +	int ret;
> +	struct hidpp_report response;
> +	struct hid_device *hdev = hidpp->hid_dev;
> +
> +	ret = hidpp_send_fap_command_sync(hidpp, feat_index,
> +					 
> HIDPP_MULTIPLATFORM_GET_FEATURE_INFO,
> +					  NULL, 0, &response);
> +	if (ret) {
> +		hid_warn(hdev, "Multiplatform: GET_FEATURE_INFO
> failed (err=%d)", ret);
> +		return ret;
> +	}
> +
> +	*num_desc = response.fap.params[3];
> +	hid_dbg(hdev, "Multiplatform: Device supports %d platform
> descriptors", *num_desc);
> +
> +	return 0;
> +}
> +
> +/**
> + * hidpp_multiplatform_get_platform_desc() - Retrieve a platform
> descriptor entry
> + * @hidpp: Pointer to the hidpp_device instance
> + * @feat_index: Feature index of the Multi-Platform feature
> + * @platform_idx: Index of the platform descriptor to retrieve
> + * @pdesc: Pointer to store the retrieved platform descriptor
> + *
> + * Retrieves a single platform descriptor identified by
> @platform_idx from the
> + * device and stores the parsed descriptor fields in @pdesc.
> + *
> + * Return: 0 on success, or non-zero on failure.
> + */
> +static int hidpp_multiplatform_get_platform_desc(struct hidpp_device
> *hidpp, u8 feat_index,
> +						 u8 platform_idx,
> struct hidpp_platform_desc *pdesc)
> +{
> +	int ret;
> +	struct hidpp_report response;
> +	u8 params[1] = { platform_idx };
> +	struct hid_device *hdev = hidpp->hid_dev;
> +
> +	ret = hidpp_send_fap_command_sync(hidpp, feat_index,
> +					 
> HIDPP_MULTIPLATFORM_GET_PLATFORM_DESCRIPTOR,
> +					  params, sizeof(params),
> &response);
> +
> +	if (ret) {
> +		hid_warn(hdev,
> +			 "Multiplatform: GET_PLATFORM_DESCRIPTOR
> failed for index %d (err=%d)",
> +			 platform_idx, ret);
> +		return ret;
> +	}
> +
> +	pdesc->plat_idx = response.fap.params[0];
> +	pdesc->desc_idx = response.fap.params[1];
> +	pdesc->plat_mask =
> get_unaligned_be16(&response.fap.params[2]);
> +
> +	hid_dbg(hdev,
> +		"Multiplatform: descriptor %d: plat_idx=%d,
> desc_idx=%d, plat_mask=0x%04x",
> +		platform_idx, pdesc->plat_idx, pdesc->desc_idx,
> pdesc->plat_mask);
> +
> +	return 0;
> +}
> +
> +/**
> + * hidpp_multiplatform_get_platform_index() - Find platform index
> for a mask
> + * @hidpp: Pointer to the hidpp_device instance
> + * @feat_index: Feature index of the Multi-Platform feature
> + * @plat_mask: Platform mask to search for
> + * @plat_index: Pointer to store the matched platform index
> + *
> + * Iterates through all platform descriptors exposed by the device
> via the
> + * Multi-Platform feature, retrieving each descriptor and comparing
> its
> + * platform mask to @plat_mask. A descriptor matches if its mask
> overlaps with
> + * the requested @plat_mask (i.e. (pdesc.plat_mask & plat_mask) is
> non-zero).
> + *
> + * When a matching descriptor is found, its platform index
> (plat_idx) is
> + * written to @plat_index and the function returns success.
> + *
> + * If no descriptor matches, -ENOENT is returned.
> + *
> + * Return: 0 on success; -ENOENT if no matching descriptor exists;
> + *         or non-zero on failure.
> + */
> +static int hidpp_multiplatform_get_platform_index(struct
> hidpp_device *hidpp,
> +						  u8 feat_index, u16
> plat_mask,
> +						  u8 *plat_index)
> +{
> +	int i;
> +	int ret;
> +	u8 num_desc;
> +	struct hidpp_platform_desc pdesc;
> +	struct hid_device *hdev = hidpp->hid_dev;
> +
> +	ret = hidpp_multiplatform_get_num_pdesc(hidpp, feat_index,
> &num_desc);
> +	if (ret)
> +		return ret;
> +
> +	for (i = 0; i < num_desc; i++) {
> +		ret = hidpp_multiplatform_get_platform_desc(hidpp,
> feat_index, i, &pdesc);
> +		if (ret)
> +			return ret;
> +
> +		if (pdesc.plat_mask & plat_mask) {
> +			*plat_index = pdesc.plat_idx;
> +			hid_dbg(hdev,
> +				"Multiplatform: Selected platform
> index %d for platform '%s'",
> +				*plat_index, hidpp_platform);
> +			return 0;
> +		}
> +	}
> +
> +	hid_dbg(hdev,
> +		"Multiplatform: No matching platform descriptor
> found for platform '%s'",
> +		hidpp_platform);
> +	return -ENOENT;
> +}
> +
> +/**
> + * hidpp_multiplatform_update_device_platform() - Update the device
> platform
> + * @hidpp: Pointer to the hidpp_device instance
> + * @feat_index: Feature index of the Multi-Platform feature
> + * @plat_index: Platform index to set on the device
> + *
> + * Sends the HID++ Multi-Platform 'SET_CURRENT_PLATFORM' command to
> the device to
> + * update its platform index to @plat_index.
> + *
> + * Return: 0 on success, or non-zero on failure.
> + */
> +static int hidpp_multiplatform_update_device_platform(struct
> hidpp_device *hidpp,
> +						      u8 feat_index,
> u8 plat_index)
> +{
> +	int ret;
> +	struct hidpp_report response;
> +	/* Byte 0 (hostIndex): 0xFF selects the current host. */
> +	u8 params[2] = { 0xFF, plat_index };
> +
> +	ret = hidpp_send_fap_command_sync(hidpp, feat_index,
> +					 
> HIDPP_MULTIPLATFORM_SET_CURRENT_PLATFORM,
> +					  params, sizeof(params),
> &response);
> +
> +	if (ret)
> +		hid_warn(hidpp->hid_dev,
> +			 "Multiplatform: SET_CURRENT_PLATFORM failed
> for index %d (err=%d)",
> +			 plat_index, ret);
> +
> +	return ret;
> +}
> +
> +/**
> + * hidpp_multiplatform_init() - Apply the HID++ Multi-Platform
> (0x4531) feature
> + * @hidpp: Pointer to the hidpp_device instance
> + *
> + * Initializes the Multi-Platform feature by selecting the device
> platform
> + * corresponding to the module parameter @hidpp_platform, if
> provided.
> + *
> + * The function performs the following steps:
> + *   1. Convert the @hidpp_platform string into a platform mask.
> + *   2. Check whether the device supports the Multi-Platform feature
> (0x4531).
> + *   3. Look up the device's platform index whose mask matches the
> host
> + *      platform mask.
> + *   4. Apply that platform index to the device via
> 'SET_CURRENT_PLATFORM'.
> + *
> + * If the module parameter is unset or invalid, or the device does
> not support
> + * the feature, or no matching platform descriptor is found, the
> function exits
> + * silently without modifying the device state.
> + *
> + * On success, the device's platform configuration is updated.
> + */
> +static void hidpp_multiplatform_init(struct hidpp_device *hidpp)
> +{
> +	int ret;
> +	u8 feat_index;
> +	u8 plat_index;
> +	u16 host_plat_mask;
> +	struct hid_device *hdev = hidpp->hid_dev;
> +
> +	if (!hidpp_platform)
> +		return;
> +
> +	host_plat_mask =
> hidpp_multiplatform_mask_from_str(hidpp_platform);
> +	if (!host_plat_mask) {
> +		hid_warn(hdev,
> +			 "Multiplatform: Invalid or unsupported
> platform name '%s'",
> +			 hidpp_platform);
> +		return;
> +	}
> +
> +	ret = hidpp_root_get_feature(hidpp,
> HIDPP_MULTIPLATFORM_FEAT_ID, &feat_index);
> +	if (ret) {
> +		hid_warn(hdev,
> +			 "Multiplatform: Failed to get the HID++
> multiplatform feature 0x4531");
> +		return;
> +	}
> +
> +	ret = hidpp_multiplatform_get_platform_index(hidpp,
> feat_index, host_plat_mask,
> +						     &plat_index);
> +	if (ret)
> +		return;
> +
> +	ret = hidpp_multiplatform_update_device_platform(hidpp,
> feat_index, plat_index);
> +	if (ret)
> +		return;
> +
> +	hid_info(hdev,
> +		 "Multiplatform: Device platform successfully set to
> '%s'", hidpp_platform);
> +}
> +
>  static int hidpp_probe(struct hid_device *hdev, const struct
> hid_device_id *id)
>  {
>  	struct hidpp_device *hidpp;
> @@ -4467,6 +4741,8 @@ static int hidpp_probe(struct hid_device *hdev,
> const struct hid_device_id *id)
>  	if (hidpp->quirks & HIDPP_QUIRK_DELAYED_INIT)
>  		connect_mask &= ~HID_CONNECT_HIDINPUT;
>  
> +	hidpp_multiplatform_init(hidpp);
> +
>  	/* Now export the actual inputs and hidraw nodes to the
> world */
>  	hid_device_io_stop(hdev);
>  	ret = hid_connect(hdev, connect_mask);
> @@ -4664,6 +4940,10 @@ static const struct hid_device_id
> hidpp_devices[] = {
>  	  HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb034) },
>  	{ /* MX Anywhere 3SB mouse over Bluetooth */
>  	  HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb038) },
> +	{ /* Casa Keys keyboard over Bluetooth */
> +	  HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH,
> USB_DEVICE_ID_LOGITECH_CASA_KEYS_KEYBOARD) },
> +	{ /* MX Keys S keyboard over Bluetooth */
> +	  HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH,
> USB_DEVICE_ID_LOGITECH_MX_KEYS_S_KEYBOARD) },
>  	{}
>  };
>  
> diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
> index c89a015686c0..99ca04b61bda 100644
> --- a/drivers/hid/hid-quirks.c
> +++ b/drivers/hid/hid-quirks.c
> @@ -520,6 +520,8 @@ static const struct hid_device_id
> hid_have_special_driver[] = {
>  #endif
>  #if IS_ENABLED(CONFIG_HID_LOGITECH_HIDPP)
>  	{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
> USB_DEVICE_ID_LOGITECH_G920_WHEEL) },
> +	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH,
> USB_DEVICE_ID_LOGITECH_CASA_KEYS_KEYBOARD) },
> +	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH,
> USB_DEVICE_ID_LOGITECH_MX_KEYS_S_KEYBOARD) },
>  #endif
>  #if IS_ENABLED(CONFIG_HID_MAGICMOUSE)
>  	{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_APPLE,
> USB_DEVICE_ID_APPLE_MAGICMOUSE) },

^ permalink raw reply

* Re: [6.18.] ThinkPad T14 Gen 2 AMD (LEN2073) - Synaptics touchpad remains PS/2, intertouch=1 ineffective, lost sync events
From: Thorsten Leemhuis @ 2026-03-09  8:12 UTC (permalink / raw)
  To: Rácz Máté, linux-input, linux-i2c
  Cc: Linux kernel regressions list
In-Reply-To: <CANkKV93wfs7oxU8kiyPWeafOs+ugpZSPLQrea7wtoSuGEG+pPw@mail.gmail.com>

Hi!

On 3/5/26 19:07, Rácz Máté wrote:
> 
> I did some additional checks on my system with kernel 6.18.13-200.fc43.x86_64:

Have you tried if 7.0-rc is still affected?

And if it is: could you bisect? See
https://docs.kernel.org/admin-guide/bug-bisect.html
for details. Without a bisection developers developers from both
subsystems involved (i2c, input) might not look into this, as everyone
will suspect it might be some change to the other subsystems that causes
this.

Ciao, Thorsten

> 1. Kernel configuration (relevant parts):
> 
> $ grep -i "RMI\|SMBus" /boot/config-$(uname -r)
> CONFIG_MOUSE_PS2_SYNAPTICS_SMBUS=y
> CONFIG_RMI4_CORE=m
> CONFIG_RMI4_I2C=m
> CONFIG_RMI4_SMB=m
> CONFIG_HID_RMI=m
> CONFIG_I2C_HID=y
> CONFIG_I2C_HID_ACPI=m
> 
> 2. dmesg output (filtered):
> 
> [ 1.615300] psmouse serio1: synaptics: Trying to set up SMBus access
> [ 1.618151] psmouse serio1: synaptics: SMbus companion is not ready yet
> [ 17.949336] psmouse serio1: TouchPad at isa0060/serio1/input0 lost
> sync at byte 1
> [ 17.962506] psmouse serio1: issuing reconnect request
> (repeats multiple times)
> 
> 3. libinput reports:
> 
> $ sudo libinput list-devices | grep -i SynPS
> Device: SynPS/2 Synaptics TouchPad
> 
> Interpretation:
> 
> - The touchpad is recognized as a SynPS/2 device, i.e., fallback PS/2 mode.
> - SMBus/RMI4 driver stack does not get loaded, despite the kernel
> config enabling modules.
> - The "SMBus companion is not ready yet" messages indicate that
> psmouse attempts SMBus access but cannot reach the device.
> - I2C controllers are functional, yet the touchpad does not enumerate
> as an I2C HID device.
> 
> Workarounds:
> 
> - Using psmouse.proto=imps avoids lost sync events, confirming
> fallback to generic PS/2 handling.
> 
> Observation regarding previous reports:
> 
> - Unlike the 2021 report on ThinkPad P14s Gen 2 (AMD), I do not see
> any "bad SMBus base address" messages in dmesg.
> - The kernel attempts SMBus access, but fails because the RMI4/hid-rmi
> stack does not initialize. The device simply falls back to PS/2 mode.
> - Therefore, this does not appear to be the same base address issue as
> the earlier P14s case.
> 
> Conclusion:
> 
> - Behavior strongly suggests a missing DMI quirk or regression in the
> SMBus detection logic in combination with i2c_piix4 on this Lenovo
> ThinkPad T14 Gen 2 (AMD) with LEN2073 touchpad.
> - This is reproducible on multiple 6.18.x kernel versions on Fedora 43
> stock kernels.
> 
> I can provide full dmesg, libinput outputs, and acpidump if required.
> 
> Best regards,
> Mate Racz
> 
> Rácz Máté <raczm0812@gmail.com> ezt írta (időpont: 2026. márc. 4., Sze, 16:02):
>>
>> This might be related to the earlier report from 2021:
>>
>> https://lore.kernel.org/linux-input/3d6f7f74-3214-4c03-b352-a2a0d27ea42b@amd.com/
>>
>> and a similar report from 2025:
>>
>> https://lore.kernel.org/linux-input/b2d0af40-876e-4a2d-99a2-236b583e9497@gmail.com/
>>
>> The 2021 report describes incorrect SMBus address detection with
>> i2c_piix4 on ThinkPad P14s Gen 2 (AMD), resulting in RMI4/SMBus not
>> functioning correctly and the device remaining in PS/2 mode.
>>
>> On my system (LEN2073), the behaviour appears very similar:
>> - intertouch has no effect
>> - "SMBus companion is not ready yet" in dmesg
>> - device stays in PS/2 mode
>>
>> It seems possible that the same underlying piix4 SMBus detection
>> issue is still present.


^ permalink raw reply

* Re: [PATCH v7 3/3] Documentation: thinkpad-acpi - Document doubletap_enable attribute
From: Ilpo Järvinen @ 2026-03-09  8:03 UTC (permalink / raw)
  To: Vishnu Sankar
  Cc: Mark Pearson, dmitry.torokhov, hmh, Hans de Goede, corbet,
	derekjohn.clark, linux-input, LKML, ibm-acpi-devel, linux-doc,
	platform-driver-x86, vsankar
In-Reply-To: <20260209063355.491189-4-vishnuocv@gmail.com>

On Mon, 9 Feb 2026, Vishnu Sankar wrote:

> Document the doubletap_enable sysfs attribute for ThinkPad ACPI driver.
> 
> Signed-off-by: Vishnu Sankar <vishnuocv@gmail.com>
> ---

> +        * 1 - doubletap events are processed (default)
> +	* 0 - doubletap events are filtered out (ignored)

There's something odd in space vs tab here.

-- 
 i.


^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox