summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-08-19 10:00:31 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-08-19 10:00:31 -0700
commita93f3bf4e1d60777b1659b812c9e818cfc53b449 (patch)
treeb72ace6a8daab0659264d38399cc618f01c0ec2b
parent7acf90feab8b009fde7def08ff2c622d0f10e99f (diff)
parentd89f04ac41e3b83a25b02c8d818ce09d7afc74c9 (diff)
Merge tag 'hid-for-linus-2026081901' of git://git.kernel.org/pub/scm/linux/kernel/git/hid/hid
Pull HID updates from Jiri Kosina: "Core: - fix long-standing force-feedback initialization race across the subsystem (Dmitry Torokhov) - switch to system_dfl_wq (Marco Crivellari) AMD-SFH: - support for tablet-mode switch for AMD SFH-based systems (Basavaraj Natikar) HyperX: - support for HyperX QuadCast 2 (Benjamin Blume) I2C-HID: - support for devices that provide HID descriptor solely through the ACPI _DSM method (XIE Zhibang) Intel-THC-HID: - support for full I2C bus config parameters (Even Xu) Logitech: - HID++ 2.0 repogrammable button support (Elliot Douglas) - Bolt receiver support for HID++ devices (Erik Håkansson) MSI: - support for MSI Claw (Derek J. Clark) Steam: - initial support for 2026 Steam Controller (Vicki Pfau) - support for sensor events on the 2025 Steam Controller (Vicki Pfau) And many, many other fixes for various long standing issues that were found by new modern tools, and quite a few device ID additions" * tag 'hid-for-linus-2026081901' of git://git.kernel.org/pub/scm/linux/kernel/git/hid/hid: (146 commits) HID: tmff: Use 64-bit arithmetic for force feedback scaling HID: multitouch: reclassify HTIX5288 to WIN_8_FORCE_MULTI_INPUT_NSMU HID: sensor: custom: Fix field sysfs group cleanup on failure HID: sensor: custom: Fix use-after-free in enable_sensor HID: intel-thc-hid: intel-quickspi: bound GET_REPORT response to the caller buffer HID: haptic: don't write an uninitialized value to unhandled usages HID: intel-thc-hid: intel-quickspi: fix autosuspend cleanup during teardown HID: intel-thc-hid: intel-quicki2c: fix autosuspend cleanup during teardown HID: steam: Zero out inputs when disabling gamepad mode HID: steam: Clean up locking HID: steam: Don't set feature reports when disconnecting HID: steam: Fix wording of connect/disconnect logs HID: steam: Initial 2026 Steam Controller support HID: steam: Refactor registration HID: logitech: add Bolt receiver support for Logitech HID++ devices HID: sensor-hub: Fix out-of-bounds write in sensor_hub_get_feature HID: universal-pidff: stop the device when force-feedback init fails HID: haptic: move FF initialization into .input_configured() HID: logitech-hidpp: move FF initialization to .input_configured() HID: megaworld: move FF initialization to .input_configured() ...
-rw-r--r--Documentation/hid/hidintro.rst50
-rw-r--r--MAINTAINERS19
-rw-r--r--drivers/hid/Kconfig46
-rw-r--r--drivers/hid/Makefile4
-rw-r--r--drivers/hid/amd-sfh-hid/Kconfig1
-rw-r--r--drivers/hid/amd-sfh-hid/amd_sfh_client.c16
-rw-r--r--drivers/hid/amd-sfh-hid/amd_sfh_common.h13
-rw-r--r--drivers/hid/amd-sfh-hid/amd_sfh_hid.c16
-rw-r--r--drivers/hid/amd-sfh-hid/amd_sfh_pcie.c76
-rw-r--r--drivers/hid/amd-sfh-hid/sfh1_1/amd_sfh_interface.c39
-rw-r--r--drivers/hid/amd-sfh-hid/sfh1_1/amd_sfh_interface.h1
-rw-r--r--drivers/hid/hid-apple.c22
-rw-r--r--drivers/hid/hid-appletb-kbd.c6
-rw-r--r--drivers/hid/hid-asus.c421
-rw-r--r--drivers/hid/hid-axff.c40
-rw-r--r--drivers/hid/hid-betopff.c33
-rw-r--r--drivers/hid/hid-bigbenff.c89
-rw-r--r--drivers/hid/hid-core.c73
-rw-r--r--drivers/hid/hid-corsair-void.c17
-rw-r--r--drivers/hid/hid-corsair.c9
-rw-r--r--drivers/hid/hid-dr.c66
-rw-r--r--drivers/hid/hid-emsff.c50
-rw-r--r--drivers/hid/hid-ft260.c70
-rw-r--r--drivers/hid/hid-gaff.c53
-rw-r--r--drivers/hid/hid-goodix-spi.c5
-rw-r--r--drivers/hid/hid-google-stadiaff.c112
-rw-r--r--drivers/hid/hid-haptic.c47
-rw-r--r--drivers/hid/hid-haptic.h6
-rw-r--r--drivers/hid/hid-holtekff.c46
-rw-r--r--drivers/hid/hid-huawei.c5
-rw-r--r--drivers/hid/hid-hyperv.c144
-rw-r--r--drivers/hid/hid-hyperx.c117
-rw-r--r--drivers/hid/hid-ids.h36
-rw-r--r--drivers/hid/hid-input.c41
-rw-r--r--drivers/hid/hid-lg4ff.c8
-rw-r--r--drivers/hid/hid-logitech-dj.c52
-rw-r--r--drivers/hid/hid-logitech-hidpp.c339
-rw-r--r--drivers/hid/hid-magicmouse.c73
-rw-r--r--drivers/hid/hid-mcp2221.c17
-rw-r--r--drivers/hid/hid-megaworld.c51
-rw-r--r--drivers/hid/hid-mf.c77
-rw-r--r--drivers/hid/hid-microsoft.c38
-rw-r--r--drivers/hid/hid-msi.c2063
-rw-r--r--drivers/hid/hid-multitouch.c12
-rw-r--r--drivers/hid/hid-nintendo.c62
-rw-r--r--drivers/hid/hid-oxp.c8
-rw-r--r--drivers/hid/hid-picolcd_debugfs.c9
-rw-r--r--drivers/hid/hid-pl.c150
-rw-r--r--drivers/hid/hid-quirks.c19
-rw-r--r--drivers/hid/hid-rapoo.c2
-rw-r--r--drivers/hid/hid-rmi.c2
-rw-r--r--drivers/hid/hid-roccat-kone.c65
-rw-r--r--drivers/hid/hid-roccat.c13
-rw-r--r--drivers/hid/hid-sensor-custom.c26
-rw-r--r--drivers/hid/hid-sensor-hub.c42
-rw-r--r--drivers/hid/hid-sjoy.c83
-rw-r--r--drivers/hid/hid-sony.c276
-rw-r--r--drivers/hid/hid-steam.c1634
-rw-r--r--drivers/hid/hid-steelseries-arctis.c718
-rw-r--r--drivers/hid/hid-steelseries.c523
-rw-r--r--drivers/hid/hid-tmff.c56
-rw-r--r--drivers/hid/hid-uclogic-core.c12
-rw-r--r--drivers/hid/hid-universal-pidff.c4
-rw-r--r--drivers/hid/hid-zpff.c43
-rw-r--r--drivers/hid/i2c-hid/Makefile2
-rw-r--r--drivers/hid/i2c-hid/i2c-hid-acpi-prp0001.c104
-rw-r--r--drivers/hid/i2c-hid/i2c-hid-acpi.c52
-rw-r--r--drivers/hid/i2c-hid/i2c-hid-acpi.h33
-rw-r--r--drivers/hid/i2c-hid/i2c-hid-core.c2
-rw-r--r--drivers/hid/i2c-hid/i2c-hid-of-goodix.c4
-rw-r--r--drivers/hid/intel-ish-hid/ipc/pci-ish.c4
-rw-r--r--drivers/hid/intel-ish-hid/ishtp-hid-client.c5
-rw-r--r--drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c118
-rw-r--r--drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h19
-rw-r--r--drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c86
-rw-r--r--drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h7
-rw-r--r--drivers/hid/intel-thc-hid/intel-quickspi/quickspi-hid.c2
-rw-r--r--drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.c19
-rw-r--r--drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.h2
-rw-r--r--drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c69
-rw-r--r--drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.h23
-rw-r--r--drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c51
-rw-r--r--drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h1
-rw-r--r--drivers/hid/intel-thc-hid/intel-thc/intel-thc-hw.h3
-rw-r--r--drivers/hid/usbhid/hid-pidff.c13
-rw-r--r--drivers/hid/wacom_wac.c8
-rw-r--r--drivers/input/misc/Kconfig15
-rw-r--r--drivers/input/misc/Makefile1
-rw-r--r--drivers/input/misc/amd_sfh_tabletmode.c81
-rw-r--r--include/linux/amd-pmf-io.h14
-rw-r--r--include/linux/hid.h4
-rw-r--r--scripts/coccinelle/hid/ff_race.cocci34
92 files changed, 7093 insertions, 1949 deletions
diff --git a/Documentation/hid/hidintro.rst b/Documentation/hid/hidintro.rst
index 73523e315ebd..5d367dfca0b8 100644
--- a/Documentation/hid/hidintro.rst
+++ b/Documentation/hid/hidintro.rst
@@ -522,3 +522,53 @@ This should really be your last resort.
vendor: 0x093a
product: 0x2510
...
+
+Input Device Registration and Lifecycle
+========================================
+
+HID drivers that rely on the HID core to register input devices (by using the
+``HID_CONNECT_HIDINPUT`` flag, which is part of ``HID_CONNECT_DEFAULT``)
+must be aware of the registration timing.
+
+When ``hid_hw_start(hdev, flags)`` is called with ``HID_CONNECT_HIDINPUT``,
+the HID core immediately parses the report descriptor, allocates ``input_dev``
+structures, and calls ``input_register_device()`` for each of them.
+
+This means the input device becomes **live and visible to userspace** before
+``hid_hw_start()`` returns.
+
+If a driver needs to perform additional configuration on the input device (such
+as adding force-feedback support, setting extra bits in ``evbit``, or
+assigning custom event handlers), doing so in the ``probe`` function after
+``hid_hw_start()`` is **incorrect and racy**. Userspace may trigger
+callbacks (like ``play_effect``) via ioctls immediately after registration,
+leading to potential NULL pointer dereferences if the driver hasn't finished
+initializing its private data.
+
+The correct way to augment an input device before it is registered is to use the
+``.input_configured`` callback in ``struct hid_driver``. This hook is
+called by the HID core after the ``input_dev`` is fully formed but **before**
+``input_register_device()`` is invoked.
+
+Example:
+
+.. code-block:: c
+
+ static int my_input_configured(struct hid_device *hdev, struct hid_input *hidinput)
+ {
+ struct input_dev *input = hidinput->input;
+
+ /* Initialize private data and capabilities here */
+ set_bit(EV_FF, input->evbit);
+ return input_ff_create_memless(input, NULL, my_play_effect);
+ }
+
+ static struct hid_driver my_driver = {
+ .name = "my_driver",
+ .probe = my_probe,
+ .input_configured = my_input_configured,
+ };
+
+Drivers that require even more control over the lifecycle should mask out
+``HID_CONNECT_HIDINPUT`` and call ``input_register_device()`` manually
+when they are ready.
diff --git a/MAINTAINERS b/MAINTAINERS
index 7c498af6dd59..a6332a1ec903 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1301,6 +1301,7 @@ L: linux-input@vger.kernel.org
S: Maintained
F: Documentation/hid/amd-sfh*
F: drivers/hid/amd-sfh-hid/
+F: drivers/input/misc/amd_sfh_tabletmode.c
AMD SPI DRIVER
M: Raju Rangoju <Raju.Rangoju@amd.com>
@@ -11514,6 +11515,12 @@ F: include/uapi/linux/hid*
F: samples/hid/
F: tools/testing/selftests/hid/
+HID HYPERX DRIVER
+M: Benjamin Blume <benjaminblume@posteo.de>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-hyperx.c
+
HID LOGITECH DRIVERS
R: Filipe Laíns <lains@riseup.net>
L: linux-input@vger.kernel.org
@@ -11550,6 +11557,12 @@ F: drivers/hid/hid-sensor-*
F: drivers/iio/*/hid-*
F: include/linux/hid-sensor-*
+HID STEAM CONTROLLER
+M: Vicki Pfau <vi@endrift.com>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-steam.c
+
HID VRC-2 CAR CONTROLLER DRIVER
M: Marcus Folkesson <marcus.folkesson@gmail.com>
L: linux-input@vger.kernel.org
@@ -18350,6 +18363,12 @@ S: Odd Fixes
F: Documentation/devicetree/bindings/net/ieee802154/mrf24j40.txt
F: drivers/net/ieee802154/mrf24j40.c
+MSI HID DRIVER
+M: Derek J. Clark <derekjohn.clark@gmail.com>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-msi.c
+
MSI EC DRIVER
M: Nikita Kravets <teackot@gmail.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index f9bcaeb66385..aa7fa11a0197 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -433,6 +433,19 @@ config HOLTEK_FF
Say Y here if you have a Holtek On Line Grip based game controller
and want to have force feedback support for it.
+config HID_HYPERX
+ tristate "HyperX microphones"
+ depends on USB_HID
+ help
+ Support for the mute button of HyperX microphones.
+
+ The button is handled in the device firmware and only reports the
+ resulting mute state through a vendor-defined collection, so without
+ this driver userspace never learns that the microphone was muted.
+
+ Supported devices:
+ - HyperX QuadCast 2
+
config HID_VIVALDI_COMMON
tristate
help
@@ -485,6 +498,19 @@ config HID_GT683R
Currently the following devices are know to be supported:
- MSI GT683R
+config HID_MSI
+ tristate "MSI Claw Gamepad Support"
+ depends on USB_HID
+ select NEW_LEDS
+ select LEDS_CLASS
+ select LEDS_CLASS_MULTICOLOR
+ help
+ Support for the MSI Claw RGB and controller configuration
+
+ Say Y here to include configuration interface support for the MSI Claw Line
+ of Handheld Console Controllers. Say M here to compile this driver as a
+ module. The module will be called hid-msi.
+
config HID_KEYTOUCH
tristate "Keytouch HID devices"
help
@@ -1048,6 +1074,7 @@ config HID_PXRC
config HID_RAPOO
tristate "Rapoo non-fully HID-compliant devices"
+ depends on USB_HID
help
Support for Rapoo devices that are not fully compliant with the
HID standard.
@@ -1079,6 +1106,15 @@ config HID_ROCCAT
Say Y here if you have a Roccat mouse or keyboard and want
support for its special functionalities.
+config HID_ROCCAT_KONE_KUNIT_TEST
+ bool "KUnit tests for the Roccat Kone driver" if !KUNIT_ALL_TESTS
+ depends on HID_ROCCAT=y && KUNIT=y
+ default KUNIT_ALL_TESTS
+ help
+ Enable the KUnit regression tests for the Roccat Kone driver,
+ covering bounds checking of device-supplied profile indices.
+ If unsure, say N.
+
config HID_SAITEK
tristate "Saitek (Mad Catz) non-fully HID-compliant devices"
help
@@ -1215,6 +1251,16 @@ config HID_HYPERV_MOUSE
help
Select this option to enable the Hyper-V mouse driver.
+config HID_HYPERV_MOUSE_KUNIT_TEST
+ bool "KUnit tests for Hyper-V mouse driver" if !KUNIT_ALL_TESTS
+ depends on KUNIT && HID_HYPERV_MOUSE
+ default KUNIT_ALL_TESTS
+ help
+ Builds unit tests for the Hyper-V synthetic HID driver.
+ These tests exercise the initial device-info parser with
+ malformed host-provided HID descriptors and are only useful
+ for kernel developers running KUnit.
+
config HID_SMARTJOYPLUS
tristate "SmartJoy PLUS PS2/USB adapter support"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 23e6e3dd0c56..48a863b245ee 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -67,6 +67,7 @@ obj-$(CONFIG_HID_HOLTEK) += hid-holtek-kbd.o
obj-$(CONFIG_HID_HOLTEK) += hid-holtek-mouse.o
obj-$(CONFIG_HID_HOLTEK) += hid-holtekff.o
obj-$(CONFIG_HID_HYPERV_MOUSE) += hid-hyperv.o
+obj-$(CONFIG_HID_HYPERX) += hid-hyperx.o
obj-$(CONFIG_HID_ICADE) += hid-icade.o
obj-$(CONFIG_HID_ITE) += hid-ite.o
obj-$(CONFIG_HID_JABRA) += hid-jabra.o
@@ -92,6 +93,7 @@ obj-$(CONFIG_HID_MAYFLASH) += hid-mf.o
obj-$(CONFIG_HID_MEGAWORLD_FF) += hid-megaworld.o
obj-$(CONFIG_HID_MICROSOFT) += hid-microsoft.o
obj-$(CONFIG_HID_MONTEREY) += hid-monterey.o
+obj-$(CONFIG_HID_MSI) += hid-msi.o
obj-$(CONFIG_HID_MULTITOUCH) += hid-multitouch.o
obj-$(CONFIG_HID_NINTENDO) += hid-nintendo.o
obj-$(CONFIG_HID_NTI) += hid-nti.o
@@ -134,7 +136,7 @@ obj-$(CONFIG_HID_SMARTJOYPLUS) += hid-sjoy.o
obj-$(CONFIG_HID_SONY) += hid-sony.o
obj-$(CONFIG_HID_SPEEDLINK) += hid-speedlink.o
obj-$(CONFIG_HID_STEAM) += hid-steam.o
-obj-$(CONFIG_HID_STEELSERIES) += hid-steelseries.o
+obj-$(CONFIG_HID_STEELSERIES) += hid-steelseries.o hid-steelseries-arctis.o
obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o
obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o
obj-$(CONFIG_HID_THRUSTMASTER) += hid-tmff.o hid-thrustmaster.o
diff --git a/drivers/hid/amd-sfh-hid/Kconfig b/drivers/hid/amd-sfh-hid/Kconfig
index 3291786a5ee6..d86d83ffebd7 100644
--- a/drivers/hid/amd-sfh-hid/Kconfig
+++ b/drivers/hid/amd-sfh-hid/Kconfig
@@ -6,6 +6,7 @@ menu "AMD SFH HID Support"
config AMD_SFH_HID
tristate "AMD Sensor Fusion Hub"
depends on X86
+ select AUXILIARY_BUS
help
If you say yes to this option, support will be included for the
AMD Sensor Fusion Hub.
diff --git a/drivers/hid/amd-sfh-hid/amd_sfh_client.c b/drivers/hid/amd-sfh-hid/amd_sfh_client.c
index 96ae792beeb6..ae6add0b9ce3 100644
--- a/drivers/hid/amd-sfh-hid/amd_sfh_client.c
+++ b/drivers/hid/amd-sfh-hid/amd_sfh_client.c
@@ -383,3 +383,19 @@ int amd_sfh_hid_client_deinit(struct amd_mp2_dev *privdata)
return 0;
}
+
+bool amd_sfh_op_idx_enabled(struct amd_mp2_dev *mp2)
+{
+ struct amdtp_cl_data *cl = mp2->cl_data;
+ int i;
+
+ if (!cl)
+ return false;
+
+ for (i = 0; i < cl->num_hid_devices; i++)
+ if (cl->sensor_idx[i] == op_idx &&
+ READ_ONCE(cl->sensor_sts[i]) == SENSOR_ENABLED)
+ return true;
+
+ return false;
+}
diff --git a/drivers/hid/amd-sfh-hid/amd_sfh_common.h b/drivers/hid/amd-sfh-hid/amd_sfh_common.h
index 78f830c133e5..f8c6b7fc34fb 100644
--- a/drivers/hid/amd-sfh-hid/amd_sfh_common.h
+++ b/drivers/hid/amd-sfh-hid/amd_sfh_common.h
@@ -10,6 +10,7 @@
#ifndef AMD_SFH_COMMON_H
#define AMD_SFH_COMMON_H
+#include <linux/auxiliary_bus.h>
#include <linux/mutex.h>
#include <linux/pci.h>
#include "amd_sfh_hid.h"
@@ -35,6 +36,11 @@ enum cmd_id {
STOP_ALL_SENSORS = 8,
};
+enum amd_mp2_version {
+ MP2_VER_V2 = 1,
+ MP2_VER_1_1 = 2,
+};
+
struct amd_mp2_sensor_info {
u8 sensor_idx;
u32 period;
@@ -64,6 +70,8 @@ struct amd_mp2_dev {
struct mutex lock;
u8 init_done;
u8 rver;
+ u8 mp2_ver;
+ struct auxiliary_device *tm_auxdev;
};
struct amd_mp2_ops {
@@ -100,4 +108,9 @@ static inline u64 amd_get_p2c_val(struct amd_mp2_dev *mp2, u32 idx)
{
return mp2->rver == 1 ? AMD_P2C_MSG_V1(idx) : AMD_P2C_MSG(idx);
}
+
+bool amd_sfh_op_idx_enabled(struct amd_mp2_dev *mp2);
+void sfh_set_emp2(struct amd_mp2_dev *mp2);
+void sfh_deinit_emp2(void);
+
#endif
diff --git a/drivers/hid/amd-sfh-hid/amd_sfh_hid.c b/drivers/hid/amd-sfh-hid/amd_sfh_hid.c
index b04f675d49b0..0a851aff7229 100644
--- a/drivers/hid/amd-sfh-hid/amd_sfh_hid.c
+++ b/drivers/hid/amd-sfh-hid/amd_sfh_hid.c
@@ -87,16 +87,17 @@ static int amdtp_wait_for_response(struct hid_device *hid)
break;
}
- if (!cli_data->request_done[i])
+ if (!cli_data->request_done[i]) {
ret = wait_event_interruptible_timeout(hid_data->hid_wait,
cli_data->request_done[i],
msecs_to_jiffies(AMD_SFH_RESPONSE_TIMEOUT));
- if (ret == -ERESTARTSYS)
- return -ERESTARTSYS;
- else if (ret < 0)
- return -ETIMEDOUT;
- else
- return 0;
+ if (ret == -ERESTARTSYS)
+ return -ERESTARTSYS;
+ if (ret <= 0)
+ return -ETIMEDOUT;
+ }
+
+ return 0;
}
void amdtp_hid_wakeup(struct hid_device *hid)
@@ -162,6 +163,7 @@ int amdtp_hid_probe(u32 cur_hid_dev, struct amdtp_cl_data *cli_data)
return 0;
err_hid_device:
+ cli_data->hid_sensor_hubs[cur_hid_dev] = NULL;
kfree(hid_data);
err_hid_data:
hid_destroy_device(hid);
diff --git a/drivers/hid/amd-sfh-hid/amd_sfh_pcie.c b/drivers/hid/amd-sfh-hid/amd_sfh_pcie.c
index 4b81cebdc335..eda26a094d3f 100644
--- a/drivers/hid/amd-sfh-hid/amd_sfh_pcie.c
+++ b/drivers/hid/amd-sfh-hid/amd_sfh_pcie.c
@@ -8,11 +8,13 @@
* Basavaraj Natikar <Basavaraj.Natikar@amd.com>
*/
+#include <linux/auxiliary_bus.h>
#include <linux/bitops.h>
#include <linux/delay.h>
#include <linux/devm-helpers.h>
#include <linux/dma-mapping.h>
#include <linux/dmi.h>
+#include <linux/idr.h>
#include <linux/interrupt.h>
#include <linux/io-64-nonatomic-lo-hi.h>
#include <linux/iopoll.h>
@@ -122,19 +124,10 @@ static irqreturn_t amd_sfh_irq_handler(int irq, void *data)
int amd_sfh_irq_init_v2(struct amd_mp2_dev *privdata)
{
- int rc;
-
pcim_intx(privdata->pdev, true);
- rc = devm_request_irq(&privdata->pdev->dev, privdata->pdev->irq,
- amd_sfh_irq_handler, 0, DRIVER_NAME, privdata);
- if (rc) {
- dev_err(&privdata->pdev->dev, "failed to request irq %d err=%d\n",
- privdata->pdev->irq, rc);
- return rc;
- }
-
- return 0;
+ return devm_request_irq(&privdata->pdev->dev, privdata->pdev->irq,
+ amd_sfh_irq_handler, 0, DRIVER_NAME, privdata);
}
static int amd_sfh_dis_sts_v2(struct amd_mp2_dev *privdata)
@@ -251,6 +244,8 @@ int amd_mp2_get_sensor_num(struct amd_mp2_dev *privdata, u8 *sensor_id)
static void amd_mp2_pci_remove(void *privdata)
{
struct amd_mp2_dev *mp2 = privdata;
+
+ sfh_deinit_emp2();
amd_sfh_hid_client_deinit(privdata);
mp2->mp2_ops->stop_all(mp2);
pcim_intx(mp2->pdev, false);
@@ -285,6 +280,7 @@ static void mp2_select_ops(struct amd_mp2_dev *privdata)
switch (acs) {
case V2_STATUS:
privdata->mp2_ops = &amd_sfh_ops_v2;
+ privdata->mp2_ver = MP2_VER_V2;
break;
default:
privdata->mp2_ops = &amd_sfh_ops;
@@ -386,6 +382,51 @@ static const struct attribute_group *amd_sfh_groups[] = {
NULL,
};
+static DEFINE_IDA(sfh_tm_ida);
+
+static void amd_sfh_maybe_register_tm(struct amd_mp2_dev *mp2)
+{
+ struct auxiliary_device *adev;
+ bool present;
+ int id;
+
+ if (mp2->tm_auxdev)
+ return;
+
+ present = mp2->sfh1_1_ops ? mp2->dev_en.is_sra_present
+ : (mp2->mp2_ver == MP2_VER_V2 &&
+ amd_sfh_op_idx_enabled(mp2));
+ if (!present)
+ return;
+
+ id = ida_alloc(&sfh_tm_ida, GFP_KERNEL);
+ if (id < 0)
+ return;
+
+ adev = auxiliary_device_create(&mp2->pdev->dev, KBUILD_MODNAME,
+ "tabletmode", NULL, id);
+ if (!adev) {
+ ida_free(&sfh_tm_ida, id);
+ dev_warn(&mp2->pdev->dev, "tabletmode auxdev create failed\n");
+ return;
+ }
+
+ mp2->tm_auxdev = adev;
+}
+
+static void amd_sfh_unregister_tm(struct amd_mp2_dev *mp2)
+{
+ int id;
+
+ if (!mp2->tm_auxdev)
+ return;
+
+ id = mp2->tm_auxdev->id;
+ auxiliary_device_destroy(mp2->tm_auxdev);
+ mp2->tm_auxdev = NULL;
+ ida_free(&sfh_tm_ida, id);
+}
+
static void sfh1_1_init_work(struct work_struct *work)
{
struct amd_mp2_dev *mp2 = container_of(work, struct amd_mp2_dev, work);
@@ -402,6 +443,7 @@ static void sfh1_1_init_work(struct work_struct *work)
if (rc)
dev_warn(&mp2->pdev->dev, "failed to update sysfs group\n");
+ amd_sfh_maybe_register_tm(mp2);
}
static void sfh_init_work(struct work_struct *work)
@@ -418,8 +460,10 @@ static void sfh_init_work(struct work_struct *work)
return;
}
+ sfh_set_emp2(mp2);
amd_sfh_clear_intr(mp2);
mp2->init_done = 1;
+ amd_sfh_maybe_register_tm(mp2);
}
static void amd_sfh_remove(struct pci_dev *pdev)
@@ -427,6 +471,7 @@ static void amd_sfh_remove(struct pci_dev *pdev)
struct amd_mp2_dev *mp2 = pci_get_drvdata(pdev);
flush_work(&mp2->work);
+ amd_sfh_unregister_tm(mp2);
if (mp2->init_done)
mp2->mp2_ops->remove(mp2);
}
@@ -447,6 +492,7 @@ static int amd_mp2_pci_probe(struct pci_dev *pdev, const struct pci_device_id *i
privdata->pdev = pdev;
dev_set_drvdata(&pdev->dev, privdata);
+
rc = pcim_enable_device(pdev);
if (rc)
return rc;
@@ -471,8 +517,9 @@ static int amd_mp2_pci_probe(struct pci_dev *pdev, const struct pci_device_id *i
if (rc)
return rc;
- privdata->sfh1_1_ops = (const struct amd_sfh1_1_ops *)id->driver_data;
- if (privdata->sfh1_1_ops) {
+ privdata->mp2_ver = (enum amd_mp2_version)id->driver_data;
+ if (privdata->mp2_ver >= MP2_VER_1_1) {
+ privdata->sfh1_1_ops = &sfh1_1_ops;
if (boot_cpu_data.x86 >= 0x1A)
privdata->rver = 1;
@@ -540,8 +587,7 @@ static SIMPLE_DEV_PM_OPS(amd_mp2_pm_ops, amd_mp2_pci_suspend,
static const struct pci_device_id amd_mp2_pci_tbl[] = {
{ PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_MP2) },
- { PCI_VDEVICE(AMD, PCI_DEVICE_ID_AMD_MP2_1_1),
- .driver_data = (kernel_ulong_t)&sfh1_1_ops },
+ { PCI_DEVICE_DATA(AMD, MP2_1_1, MP2_VER_1_1) },
{ }
};
MODULE_DEVICE_TABLE(pci, amd_mp2_pci_tbl);
diff --git a/drivers/hid/amd-sfh-hid/sfh1_1/amd_sfh_interface.c b/drivers/hid/amd-sfh-hid/sfh1_1/amd_sfh_interface.c
index 837d59e7a661..097c5513ccd8 100644
--- a/drivers/hid/amd-sfh-hid/sfh1_1/amd_sfh_interface.c
+++ b/drivers/hid/amd-sfh-hid/sfh1_1/amd_sfh_interface.c
@@ -8,12 +8,14 @@
* Author: Basavaraj Natikar <Basavaraj.Natikar@amd.com>
*/
#include <linux/amd-pmf-io.h>
+#include <linux/cleanup.h>
#include <linux/io-64-nonatomic-lo-hi.h>
#include <linux/iopoll.h>
#include "amd_sfh_interface.h"
static struct amd_mp2_dev *emp2;
+static DEFINE_MUTEX(emp2_lock);
static int amd_sfh_wait_response(struct amd_mp2_dev *mp2, u8 sid, u32 cmd_id)
{
@@ -78,15 +80,48 @@ static struct amd_mp2_ops amd_sfh_ops = {
void sfh_deinit_emp2(void)
{
+ guard(mutex)(&emp2_lock);
emp2 = NULL;
}
+void sfh_set_emp2(struct amd_mp2_dev *mp2)
+{
+ guard(mutex)(&emp2_lock);
+ emp2 = mp2;
+}
+
void sfh_interface_init(struct amd_mp2_dev *mp2)
{
mp2->mp2_ops = &amd_sfh_ops;
+ guard(mutex)(&emp2_lock);
emp2 = mp2;
}
+static int amd_sfh_op_mode_info(u32 *op_mode)
+{
+ struct sfh_op_mode mode;
+ bool present;
+
+ if (!op_mode)
+ return -EINVAL;
+ if (!emp2)
+ return -ENODEV;
+
+ present = emp2->sfh1_1_ops ? emp2->dev_en.is_sra_present
+ : (emp2->mp2_ver == MP2_VER_V2 &&
+ amd_sfh_op_idx_enabled(emp2));
+ if (!present)
+ return -ENODEV;
+
+ mode.val = readl(emp2->mmio + amd_get_c2p_val(emp2, 3));
+ dev_dbg(&emp2->pdev->dev, "op-mode: %s (mode=%u)\n",
+ mode.op_mode.mode == SFH_MODE_TABLET ? "tablet" : "laptop",
+ mode.op_mode.mode);
+ *op_mode = mode.op_mode.mode;
+
+ return 0;
+}
+
static int amd_sfh_mode_info(u32 *platform_type, u32 *laptop_placement)
{
struct sfh_op_mode mode;
@@ -160,6 +195,8 @@ static int amd_sfh_als_info(u32 *ambient_light)
int amd_get_sfh_info(struct amd_sfh_info *sfh_info, enum sfh_message_type op)
{
+ guard(mutex)(&emp2_lock);
+
if (sfh_info) {
switch (op) {
case MT_HPD:
@@ -169,6 +206,8 @@ int amd_get_sfh_info(struct amd_sfh_info *sfh_info, enum sfh_message_type op)
case MT_SRA:
return amd_sfh_mode_info(&sfh_info->platform_type,
&sfh_info->laptop_placement);
+ case MT_OP_MODE:
+ return amd_sfh_op_mode_info(&sfh_info->op_mode);
}
}
return -EINVAL;
diff --git a/drivers/hid/amd-sfh-hid/sfh1_1/amd_sfh_interface.h b/drivers/hid/amd-sfh-hid/sfh1_1/amd_sfh_interface.h
index 665c99ad779f..56258c4d1b3a 100644
--- a/drivers/hid/amd-sfh-hid/sfh1_1/amd_sfh_interface.h
+++ b/drivers/hid/amd-sfh-hid/sfh1_1/amd_sfh_interface.h
@@ -185,7 +185,6 @@ struct sfh_op_mode {
};
void sfh_interface_init(struct amd_mp2_dev *mp2);
-void sfh_deinit_emp2(void);
void amd_sfh1_1_set_desc_ops(struct amd_mp2_ops *mp2_ops);
int amd_sfh_float_to_int(u32 flt32_val);
#endif
diff --git a/drivers/hid/hid-apple.c b/drivers/hid/hid-apple.c
index bf7dd0fbf249..f6134c5820ff 100644
--- a/drivers/hid/hid-apple.c
+++ b/drivers/hid/hid-apple.c
@@ -91,6 +91,9 @@ struct apple_sc_backlight {
struct hid_device *hdev;
};
+/* T2 VHCI re-enumerates the internal keyboard across system resume. */
+static int apple_backlight_resume_brightness = -1;
+
struct apple_backlight_config_report {
u8 report_id;
u8 version;
@@ -368,6 +371,8 @@ static const struct apple_non_apple_keyboard non_apple_keyboards[] = {
{ "TH87" }, /* EPOMAKER TH87 BT mode */
{ "HFD Epomaker TH87" }, /* EPOMAKER TH87 USB mode */
{ "2.4G Wireless Receiver" }, /* EPOMAKER TH87 dongle */
+ { "Thock TKL Wireless" }, /* ENDORFY THOCK TKL BT mode */
+ { "USB Dongle" }, /* ENDORFY THOCK TKL dongle */
};
static bool apple_is_non_apple_keyboard(struct hid_device *hdev)
@@ -825,6 +830,7 @@ static int apple_backlight_led_set(struct led_classdev *led_cdev,
static int apple_backlight_init(struct hid_device *hdev)
{
int ret;
+ int brightness;
struct apple_sc *asc = hid_get_drvdata(hdev);
struct apple_backlight_config_report *rep;
@@ -857,16 +863,23 @@ static int apple_backlight_init(struct hid_device *hdev)
}
asc->backlight->hdev = hdev;
- asc->backlight->cdev.name = "apple::kbd_backlight";
+ asc->backlight->cdev.name = ":white:" LED_FUNCTION_KBD_BACKLIGHT;
asc->backlight->cdev.max_brightness = rep->backlight_on_max;
asc->backlight->cdev.brightness_set_blocking = apple_backlight_led_set;
- asc->backlight->cdev.flags = LED_CORE_SUSPENDRESUME;
+ /* VHCI re-enumeration restores the cached brightness in the next probe. */
+
+ brightness = READ_ONCE(apple_backlight_resume_brightness);
+ if (brightness < 0)
+ brightness = LED_OFF;
+ else
+ brightness = min_t(int, brightness, rep->backlight_on_max);
- ret = apple_backlight_set(hdev, 0, 0);
+ ret = apple_backlight_set(hdev, brightness, 0);
if (ret < 0) {
hid_err(hdev, "backlight set request failed: %d\n", ret);
goto cleanup_and_exit;
}
+ asc->backlight->cdev.brightness = brightness;
ret = devm_led_classdev_register(&hdev->dev, &asc->backlight->cdev);
@@ -999,6 +1012,9 @@ static void apple_remove(struct hid_device *hdev)
if (asc->quirks & APPLE_RDESC_BATTERY)
timer_delete_sync(&asc->battery_timer);
+ if (asc->backlight)
+ WRITE_ONCE(apple_backlight_resume_brightness,
+ asc->backlight->cdev.brightness);
hid_hw_stop(hdev);
}
diff --git a/drivers/hid/hid-appletb-kbd.c b/drivers/hid/hid-appletb-kbd.c
index 462010a75899..5cc27066f602 100644
--- a/drivers/hid/hid-appletb-kbd.c
+++ b/drivers/hid/hid-appletb-kbd.c
@@ -175,7 +175,7 @@ static void appletb_inactivity_work(struct work_struct *work)
if (!kbd->has_dimmed) {
backlight_device_set_brightness(kbd->backlight_dev, 1);
kbd->has_dimmed = true;
- mod_delayed_work(system_wq, &kbd->inactivity_work,
+ mod_delayed_work(system_dfl_wq, &kbd->inactivity_work,
secs_to_jiffies(appletb_tb_idle_timeout));
} else if (!kbd->has_turned_off) {
backlight_device_set_brightness(kbd->backlight_dev, 0);
@@ -201,7 +201,7 @@ static void reset_inactivity_timer(struct appletb_kbd *kbd)
kbd->has_turned_off = false;
schedule_work(&kbd->restore_brightness_work);
}
- mod_delayed_work(system_wq, &kbd->inactivity_work,
+ mod_delayed_work(system_dfl_wq, &kbd->inactivity_work,
secs_to_jiffies(appletb_tb_dim_timeout));
}
}
@@ -423,7 +423,7 @@ static int appletb_kbd_probe(struct hid_device *hdev, const struct hid_device_id
INIT_DELAYED_WORK(&kbd->inactivity_work, appletb_inactivity_work);
INIT_WORK(&kbd->restore_brightness_work,
appletb_restore_brightness_work);
- mod_delayed_work(system_wq, &kbd->inactivity_work,
+ mod_delayed_work(system_dfl_wq, &kbd->inactivity_work,
secs_to_jiffies(appletb_tb_dim_timeout));
}
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 3f5e96900b67..ec966fc0a411 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -99,6 +99,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define QUIRK_ROG_CLAYMORE_II_KEYBOARD BIT(12)
#define QUIRK_ROG_ALLY_XPAD BIT(13)
#define QUIRK_HID_FN_LOCK BIT(14)
+#define QUIRK_FILTER_CAMERA_COMPANION BIT(15)
#define I2C_KEYBOARD_QUIRKS (QUIRK_FIX_NOTEBOOK_REPORT | \
QUIRK_NO_INIT_REPORTS | \
@@ -109,11 +110,36 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define TRKID_SGN ((TRKID_MAX + 1) >> 1)
-struct asus_kbd_leds {
- struct asus_hid_listener listener;
+enum asus_work_action_type {
+ FN_LOCK_SYNC,
+ BRIGHTNESS_SET,
+ WMI_FAN,
+};
+
+struct hid_raw_event_data {
+ u8 report_data[FEATURE_KBD_REPORT_SIZE];
+ size_t report_size;
+};
+
+struct asus_work_action {
+ struct list_head node;
+ enum asus_work_action_type type;
+ union {
+ /* Data for BRIGHTNESS_SET */
+ unsigned int brightness;
+
+ /* Data for FN_LOCK_SYNC */
+ bool fn_lock;
+
+ /* Data for WMI_FAN */
+ struct hid_raw_event_data fan_hid_data;
+ } data;
+};
+
+struct asus_worker {
struct hid_device *hdev;
struct work_struct work;
- unsigned int brightness;
+ struct list_head actions;
spinlock_t lock;
bool removed;
};
@@ -133,7 +159,8 @@ struct asus_drvdata {
struct hid_device *hdev;
struct input_dev *input;
struct input_dev *tp_kbd_input;
- struct asus_kbd_leds *kbd_backlight;
+ struct asus_worker *worker;
+ unsigned int kbd_backlight_brightness;
const struct asus_touchpad_info *tp;
struct power_supply *battery;
struct power_supply_desc battery_desc;
@@ -141,7 +168,7 @@ struct asus_drvdata {
int battery_stat;
bool battery_in_query;
unsigned long battery_next_query;
- struct work_struct fn_lock_sync_work;
+ struct asus_hid_listener listener;
bool fn_lock;
};
@@ -211,6 +238,29 @@ static const u8 asus_report_id_init[] = {
FEATURE_KBD_LED_REPORT_ID2
};
+/*
+ * Send events to asus-wmi driver for handling special keys
+ */
+static int asus_wmi_send_event(struct asus_drvdata *drvdata, u8 code)
+{
+ int err;
+ u32 retval;
+
+ err = asus_wmi_evaluate_method(ASUS_WMI_METHODID_DEVS,
+ ASUS_WMI_METHODID_NOTIF, code, &retval);
+ if (err) {
+ pr_warn("Failed to notify asus-wmi: %d\n", err);
+ return err;
+ }
+
+ if (retval != 0) {
+ pr_warn("Failed to notify asus-wmi (retval): 0x%x\n", retval);
+ return -EIO;
+ }
+
+ return 0;
+}
+
static void asus_report_contact_down(struct asus_drvdata *drvdat,
int toolType, u8 *data)
{
@@ -331,25 +381,71 @@ static int asus_e1239t_event(struct asus_drvdata *drvdat, u8 *data, int size)
}
/*
- * Send events to asus-wmi driver for handling special keys
+ * Used in atomic contexts to schedule work involving sleeps operations or
+ * asus-wmi interactions.
+ *
+ * Caller is responsible to store relevant data in the structure to carry out
+ * the required action.
+ *
+ * This function must be called while the spin lock protecting the workqueue
+ * is already being held.
*/
-static int asus_wmi_send_event(struct asus_drvdata *drvdata, u8 code)
+static void asus_worker_schedule(struct asus_worker *worker, struct asus_work_action *action)
{
- int err;
- u32 retval;
-
- err = asus_wmi_evaluate_method(ASUS_WMI_METHODID_DEVS,
- ASUS_WMI_METHODID_NOTIF, code, &retval);
- if (err) {
- pr_warn("Failed to notify asus-wmi: %d\n", err);
- return err;
+ if (worker->removed) {
+ kfree(action);
+ return;
}
- if (retval != 0) {
- pr_warn("Failed to notify asus-wmi (retval): 0x%x\n", retval);
- return -EIO;
+ list_add_tail(&action->node, &worker->actions);
+ schedule_work(&worker->work);
+}
+
+static int asus_kbd_fn_lock_set(struct asus_drvdata *drvdata, bool enabled)
+{
+ struct asus_work_action *action;
+ unsigned long flags;
+
+ action = kzalloc(sizeof(struct asus_work_action), GFP_ATOMIC);
+ if (!action)
+ return -ENOMEM;
+
+ drvdata->fn_lock = enabled;
+ action->type = FN_LOCK_SYNC;
+ action->data.fn_lock = drvdata->fn_lock;
+ INIT_LIST_HEAD(&action->node);
+
+ spin_lock_irqsave(&drvdata->worker->lock, flags);
+ asus_worker_schedule(drvdata->worker, action);
+ spin_unlock_irqrestore(&drvdata->worker->lock, flags);
+
+ return 0;
+}
+
+static int asus_kbd_wmi_fan_send(struct asus_drvdata *drvdata, u8 *report_data,
+ size_t report_size)
+{
+ struct asus_work_action *action;
+ unsigned long flags;
+
+ if (report_size > FEATURE_KBD_REPORT_SIZE) {
+ hid_err(drvdata->hdev, "Invalid report size for fan event: %zu\n", report_size);
+ return -EINVAL;
}
+ action = kzalloc(sizeof(struct asus_work_action), GFP_NOWAIT);
+ if (!action)
+ return -ENOMEM;
+
+ action->type = WMI_FAN;
+ action->data.fan_hid_data.report_size = report_size;
+ memcpy(action->data.fan_hid_data.report_data, report_data, report_size);
+ INIT_LIST_HEAD(&action->node);
+
+ spin_lock_irqsave(&drvdata->worker->lock, flags);
+ asus_worker_schedule(drvdata->worker, action);
+ spin_unlock_irqrestore(&drvdata->worker->lock, flags);
+
return 0;
}
@@ -357,6 +453,7 @@ static int asus_event(struct hid_device *hdev, struct hid_field *field,
struct hid_usage *usage, __s32 value)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
if ((usage->hid & HID_USAGE_PAGE) == HID_UP_ASUSVENDOR &&
(usage->hid & HID_USAGE) != 0x00 &&
@@ -375,8 +472,11 @@ static int asus_event(struct hid_device *hdev, struct hid_field *field,
return !asus_hid_event(ASUS_EV_BRTTOGGLE);
case KEY_FN_ESC:
if (drvdata->quirks & QUIRK_HID_FN_LOCK) {
- drvdata->fn_lock = !drvdata->fn_lock;
- schedule_work(&drvdata->fn_lock_sync_work);
+ ret = asus_kbd_fn_lock_set(drvdata, !drvdata->fn_lock);
+ if (ret) {
+ hid_err(hdev, "Error while toggling FN lock: %d\n", ret);
+ return ret;
+ }
}
break;
}
@@ -389,6 +489,12 @@ static int asus_raw_event(struct hid_device *hdev,
struct hid_report *report, u8 *data, int size)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
+
+ if (size < 2) {
+ hid_dbg(hdev, "Unexpected keyboard report size %d\n", size);
+ return 0;
+ }
if (drvdata->battery && data[0] == BATTERY_REPORT_ID)
return asus_report_battery(drvdata, data, size);
@@ -414,19 +520,13 @@ static int asus_raw_event(struct hid_device *hdev,
* pass to userspace so it can implement its own fan control.
*/
if (data[1] == ASUS_FAN_CTRL_KEY_CODE) {
- int ret = asus_wmi_send_event(drvdata, ASUS_FAN_CTRL_KEY_CODE);
+ ret = asus_kbd_wmi_fan_send(drvdata, data, size);
- if (ret == 0) {
- /* Successfully handled by asus-wmi, block event */
+ /* if execution deferred successfully block event */
+ if (ret == 0)
return -1;
- }
- /*
- * Warn if asus-wmi failed (but not if it's unavailable).
- * Let the event reach userspace in all failure cases.
- */
- if (ret != -ENODEV)
- hid_warn(hdev, "Failed to notify asus-wmi: %d\n", ret);
+ return ret;
}
/*
@@ -460,6 +560,17 @@ static int asus_raw_event(struct hid_device *hdev,
return -1;
}
+ /*
+ * The camera-toggle key reports its vendor usage (0x85) together with a
+ * companion state byte in the same array report, e.g. "5a 85 01" and
+ * "5a 85 10" for the two toggle positions. The 0x10 companion aliases the
+ * brightness-down vendor usage and would spuriously dim the panel, so drop
+ * the companion slots and leave only the camera usage for input mapping.
+ */
+ if (drvdata->quirks & QUIRK_FILTER_CAMERA_COMPANION &&
+ report->id == FEATURE_KBD_REPORT_ID && size >= 3 && data[1] == 0x85)
+ memset(&data[2], 0, size - 2);
+
return 0;
}
@@ -569,59 +680,157 @@ static int asus_kbd_disable_oobe(struct hid_device *hdev)
return 0;
}
-static int asus_kbd_set_fn_lock(struct hid_device *hdev, bool enabled)
+static void asus_kbd_set_fn_lock(struct hid_device *hdev, bool enabled)
{
- u8 buf[] = { FEATURE_KBD_REPORT_ID, 0xd0, 0x4e, !!enabled };
+ const u8 buf[FEATURE_KBD_REPORT_SIZE] = { FEATURE_KBD_REPORT_ID, 0xd0, 0x4e, !!enabled };
+ int ret;
- return asus_kbd_set_report(hdev, buf, sizeof(buf));
+ ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
+ if (ret < 0)
+ hid_err(hdev, "Asus failed to set fn lock: %d\n", ret);
}
-static void asus_sync_fn_lock(struct work_struct *work)
+static void asus_kbd_set_brightness(struct hid_device *hdev, u8 brightness)
{
- struct asus_drvdata *drvdata =
- container_of(work, struct asus_drvdata, fn_lock_sync_work);
+ const u8 buf[FEATURE_KBD_REPORT_SIZE] = {
+ FEATURE_KBD_REPORT_ID, 0xba, 0xc5, 0xc4, brightness
+ };
+ int ret;
- asus_kbd_set_fn_lock(drvdata->hdev, drvdata->fn_lock);
+ ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
+ if (ret < 0)
+ hid_err(hdev, "Asus failed to set keyboard backlight: %d\n", ret);
}
-static void asus_schedule_work(struct asus_kbd_leds *led)
+static void asus_kbd_wmi_fan(struct hid_device *hdev, struct hid_raw_event_data *data)
{
+ struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
+
+ ret = asus_wmi_send_event(drvdata, ASUS_FAN_CTRL_KEY_CODE);
+
+ /*
+ * Warn if asus-wmi failed (but not if it's unavailable).
+ * Let the event reach userspace in all failure cases.
+ */
+ switch (ret) {
+ case -ENODEV:
+ break;
+ case 0:
+ return;
+ default:
+ hid_warn(hdev, "Failed to notify asus-wmi: %d\n", ret);
+ break;
+ }
+
+ /*
+ * Fallback: pass the raw event to the HID core; to avoid
+ * racing against the hid_report_raw_event() that generated
+ * this event use the same locking mechanism and wait for
+ * that function to terminate and signal the deferred execution
+ * before raising the stored event.
+ */
+ down(&hdev->driver_input_lock);
+ hid_report_raw_event(hdev, HID_INPUT_REPORT,
+ data->report_data, data->report_size,
+ data->report_size, 1);
+ up(&hdev->driver_input_lock);
+}
+
+static void asus_kbd_backlight_set(struct asus_hid_listener *listener, int brightness)
+{
+ struct asus_drvdata *drvdata = container_of(listener, struct asus_drvdata, listener);
+ struct asus_worker *worker = drvdata->worker;
+ struct asus_work_action *action;
unsigned long flags;
- spin_lock_irqsave(&led->lock, flags);
- if (!led->removed)
- schedule_work(&led->work);
- spin_unlock_irqrestore(&led->lock, flags);
+ drvdata->kbd_backlight_brightness = brightness;
+
+ action = kzalloc(sizeof(struct asus_work_action), GFP_NOWAIT);
+ if (!action)
+ return;
+
+ action->type = BRIGHTNESS_SET;
+ action->data.brightness = brightness;
+ INIT_LIST_HEAD(&action->node);
+
+ spin_lock_irqsave(&worker->lock, flags);
+ asus_worker_schedule(worker, action);
+ spin_unlock_irqrestore(&worker->lock, flags);
}
-static void asus_kbd_backlight_set(struct asus_hid_listener *listener,
- int brightness)
+static void asus_work(struct work_struct *work)
{
- struct asus_kbd_leds *led = container_of(listener, struct asus_kbd_leds,
- listener);
+ struct asus_worker *worker = container_of(work, struct asus_worker, work);
+ struct asus_work_action *action = NULL;
unsigned long flags;
- spin_lock_irqsave(&led->lock, flags);
- led->brightness = brightness;
- spin_unlock_irqrestore(&led->lock, flags);
+ /* Save the action to be performed and clear the flag */
+ spin_lock_irqsave(&worker->lock, flags);
+ if (!list_empty(&worker->actions)) {
+ action = list_first_entry(&worker->actions,
+ struct asus_work_action, node);
+ list_del(&action->node);
+ }
+ spin_unlock_irqrestore(&worker->lock, flags);
+
+ if (!action)
+ return;
+
+ switch (action->type) {
+ case BRIGHTNESS_SET:
+ asus_kbd_set_brightness(worker->hdev, action->data.brightness);
+ break;
+ case FN_LOCK_SYNC:
+ asus_kbd_set_fn_lock(worker->hdev, action->data.fn_lock);
+ break;
+ case WMI_FAN:
+ asus_kbd_wmi_fan(worker->hdev, &action->data.fan_hid_data);
+ break;
+ default:
+ hid_err(worker->hdev, "Invalid action type: %d\n", action->type);
+ break;
+ }
+
+ kfree(action);
- asus_schedule_work(led);
+ /* Re-schedule if there are more pending actions */
+ spin_lock_irqsave(&worker->lock, flags);
+ if (!list_empty(&worker->actions))
+ schedule_work(&worker->work);
+ spin_unlock_irqrestore(&worker->lock, flags);
}
-static void asus_kbd_backlight_work(struct work_struct *work)
+static int asus_worker_create(struct hid_device *hdev, struct asus_drvdata *drvdata)
{
- struct asus_kbd_leds *led = container_of(work, struct asus_kbd_leds, work);
- u8 buf[] = { FEATURE_KBD_REPORT_ID, 0xba, 0xc5, 0xc4, 0x00 };
- int ret;
+ drvdata->worker = devm_kzalloc(&hdev->dev, sizeof(struct asus_worker), GFP_KERNEL);
+ if (!drvdata->worker)
+ return -ENOMEM;
+
+ drvdata->worker->removed = false;
+ drvdata->worker->hdev = hdev;
+ INIT_LIST_HEAD(&drvdata->worker->actions);
+
+ INIT_WORK(&drvdata->worker->work, asus_work);
+ spin_lock_init(&drvdata->worker->lock);
+
+ return 0;
+}
+
+static void asus_worker_stop(struct asus_worker *worker)
+{
+ struct asus_work_action *action, *tmp;
unsigned long flags;
- spin_lock_irqsave(&led->lock, flags);
- buf[4] = led->brightness;
- spin_unlock_irqrestore(&led->lock, flags);
+ spin_lock_irqsave(&worker->lock, flags);
+ worker->removed = true;
+ list_for_each_entry_safe(action, tmp, &worker->actions, node) {
+ list_del(&action->node);
+ kfree(action);
+ }
+ spin_unlock_irqrestore(&worker->lock, flags);
- ret = asus_kbd_set_report(led->hdev, buf, sizeof(buf));
- if (ret < 0)
- hid_err(led->hdev, "Asus failed to set keyboard backlight: %d\n", ret);
+ cancel_work_sync(&worker->work);
}
/*
@@ -644,7 +853,7 @@ static int mcu_parse_version_string(const u8 *response, size_t response_size)
dots++;
}
- if (dots != 2 || p >= end || (p + 3) >= end)
+ if (dots != 2 || end - p < 3)
return -EINVAL;
memcpy(buf, p, 3);
@@ -753,30 +962,18 @@ static int asus_kbd_register_leds(struct hid_device *hdev)
return ret;
}
- if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ if ((drvdata->quirks & QUIRK_ROG_ALLY_XPAD) && hid_is_usb(hdev)) {
intf = to_usb_interface(hdev->dev.parent);
udev = interface_to_usbdev(intf);
validate_mcu_fw_version(hdev,
le16_to_cpu(udev->descriptor.idProduct));
}
- drvdata->kbd_backlight = devm_kzalloc(&hdev->dev,
- sizeof(struct asus_kbd_leds),
- GFP_KERNEL);
- if (!drvdata->kbd_backlight)
- return -ENOMEM;
-
- drvdata->kbd_backlight->removed = false;
- drvdata->kbd_backlight->brightness = 0;
- drvdata->kbd_backlight->hdev = hdev;
- drvdata->kbd_backlight->listener.brightness_set = asus_kbd_backlight_set;
- INIT_WORK(&drvdata->kbd_backlight->work, asus_kbd_backlight_work);
- spin_lock_init(&drvdata->kbd_backlight->lock);
-
- ret = asus_hid_register_listener(&drvdata->kbd_backlight->listener);
+ drvdata->listener.brightness_set = asus_kbd_backlight_set;
+ ret = asus_hid_register_listener(&drvdata->listener);
if (ret < 0) {
- /* No need to have this still around */
- devm_kfree(&hdev->dev, drvdata->kbd_backlight);
+ hid_err(hdev, "Unable to register kbd brightness listener: %d\n", ret);
+ drvdata->listener.brightness_set = NULL;
}
return ret;
@@ -998,11 +1195,9 @@ static int asus_input_configured(struct hid_device *hdev, struct hid_input *hi)
drvdata->input = input;
- if (drvdata->quirks & QUIRK_HID_FN_LOCK) {
- drvdata->fn_lock = true;
- INIT_WORK(&drvdata->fn_lock_sync_work, asus_sync_fn_lock);
- asus_kbd_set_fn_lock(hdev, true);
- }
+ if ((drvdata->quirks & QUIRK_HID_FN_LOCK) &&
+ (asus_kbd_fn_lock_set(drvdata, true)))
+ hid_warn(hdev, "Error while setting FN lock to ON\n");
return 0;
}
@@ -1045,6 +1240,8 @@ static int asus_input_mapping(struct hid_device *hdev,
case 0x6c: asus_map_key_clear(KEY_SLEEP); break;
case 0x7c: asus_map_key_clear(KEY_MICMUTE); break;
case 0x82: asus_map_key_clear(KEY_CAMERA); break;
+ case 0x85: asus_map_key_clear(KEY_CAMERA); break;
+ case 0x86: asus_map_key_clear(KEY_PROG1); break; /* MyASUS key */
case 0x88: asus_map_key_clear(KEY_RFKILL); break;
case 0xb5: asus_map_key_clear(KEY_CALC); break;
case 0xc4: asus_map_key_clear(KEY_KBDILLUMUP); break;
@@ -1054,6 +1251,7 @@ static int asus_input_mapping(struct hid_device *hdev,
case 0x7e: asus_map_key_clear(KEY_EMOJI_PICKER); break;
case 0x8b: asus_map_key_clear(KEY_PROG1); break; /* ProArt Creator Hub key */
+ case 0x5f: asus_map_key_clear(KEY_PROG2); break; /* S-shaped programmable key */
case 0x6b: asus_map_key_clear(KEY_F21); break; /* ASUS touchpad toggle */
case 0x38: asus_map_key_clear(KEY_PROG1); break; /* ROG key */
case 0xba: asus_map_key_clear(KEY_PROG2); break; /* Fn+C ASUS Splendid */
@@ -1165,20 +1363,16 @@ static int asus_start_multitouch(struct hid_device *hdev)
static int __maybe_unused asus_resume(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
- int ret = 0;
- if (drvdata->kbd_backlight) {
- const u8 buf[] = { FEATURE_KBD_REPORT_ID, 0xba, 0xc5, 0xc4,
- drvdata->kbd_backlight->brightness };
- ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
- if (ret < 0) {
- hid_err(hdev, "Asus failed to set keyboard backlight: %d\n", ret);
- goto asus_resume_err;
- }
- }
+ /*
+ * If we have a backlight listener registered, restore the previous state,
+ * in case of error do not fail: most models restore the backlight
+ * automatically, and the error is non-fatal.
+ */
+ if (drvdata->listener.brightness_set)
+ asus_kbd_backlight_set(&drvdata->listener, drvdata->kbd_backlight_brightness);
-asus_resume_err:
- return ret;
+ return 0;
}
static int __maybe_unused asus_reset_resume(struct hid_device *hdev)
@@ -1200,10 +1394,8 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
int ret;
drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
- if (drvdata == NULL) {
- hid_err(hdev, "Can't alloc Asus descriptor\n");
+ if (drvdata == NULL)
return -ENOMEM;
- }
hid_set_drvdata(hdev, drvdata);
@@ -1288,8 +1480,15 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
is_vendor = true;
}
+ ret = asus_worker_create(hdev, drvdata);
+ if (ret) {
+ hid_warn(hdev, "Failed to initialize worker: %d\n", ret);
+ return ret;
+ }
+
ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (ret) {
+ asus_worker_stop(drvdata->worker);
hid_err(hdev, "Asus hw start failed: %d\n", ret);
return ret;
}
@@ -1337,6 +1536,10 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
return 0;
err_stop_hw:
+ if (drvdata->listener.brightness_set)
+ asus_hid_unregister_listener(&drvdata->listener);
+
+ asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
return ret;
}
@@ -1344,21 +1547,11 @@ err_stop_hw:
static void asus_remove(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
- unsigned long flags;
-
- if (drvdata->kbd_backlight) {
- asus_hid_unregister_listener(&drvdata->kbd_backlight->listener);
-
- spin_lock_irqsave(&drvdata->kbd_backlight->lock, flags);
- drvdata->kbd_backlight->removed = true;
- spin_unlock_irqrestore(&drvdata->kbd_backlight->lock, flags);
-
- cancel_work_sync(&drvdata->kbd_backlight->work);
- }
- if (drvdata->quirks & QUIRK_HID_FN_LOCK)
- cancel_work_sync(&drvdata->fn_lock_sync_work);
+ if (drvdata->listener.brightness_set)
+ asus_hid_unregister_listener(&drvdata->listener);
+ asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
}
@@ -1478,6 +1671,9 @@ static const struct hid_device_id asus_devices[] = {
{ HID_I2C_DEVICE(USB_VENDOR_ID_ASUSTEK,
USB_DEVICE_ID_ASUSTEK_I2C_KEYBOARD), I2C_KEYBOARD_QUIRKS},
{ HID_I2C_DEVICE(USB_VENDOR_ID_ASUSTEK,
+ USB_DEVICE_ID_ASUSTEK_I2C_ZENBOOK_KEYBOARD),
+ I2C_KEYBOARD_QUIRKS | QUIRK_FILTER_CAMERA_COMPANION },
+ { HID_I2C_DEVICE(USB_VENDOR_ID_ASUSTEK,
USB_DEVICE_ID_ASUSTEK_I2C_TOUCHPAD), I2C_TOUCHPAD_QUIRKS },
{ HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
USB_DEVICE_ID_ASUSTEK_ROG_KEYBOARD1), QUIRK_USE_KBD_BACKLIGHT },
@@ -1494,6 +1690,9 @@ static const struct hid_device_id asus_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD2),
QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD | QUIRK_HID_FN_LOCK },
+ { HID_I2C_DEVICE(USB_VENDOR_ID_ASUSTEK,
+ USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD2),
+ QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD | QUIRK_HID_FN_LOCK },
{ HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
USB_DEVICE_ID_ASUSTEK_ROG_Z13_LIGHTBAR),
QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD },
diff --git a/drivers/hid/hid-axff.c b/drivers/hid/hid-axff.c
index 3c5c2bf02425..2f46447086df 100644
--- a/drivers/hid/hid-axff.c
+++ b/drivers/hid/hid-axff.c
@@ -59,30 +59,24 @@ static int axff_play(struct input_dev *dev, void *data, struct ff_effect *effect
return 0;
}
-static int axff_init(struct hid_device *hid)
+static int ax_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct axff_device *axff;
struct hid_report *report;
- struct hid_input *hidinput;
- struct list_head *report_list =&hid->report_enum[HID_OUTPUT_REPORT].report_list;
- struct input_dev *dev;
+ struct list_head *report_list = &hid->report_enum[HID_OUTPUT_REPORT].report_list;
+ struct input_dev *dev = hidinput->input;
int field_count = 0;
int i, j;
int error;
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
- return -ENODEV;
- }
- hidinput = list_first_entry(&hid->inputs, struct hid_input, list);
- dev = hidinput->input;
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
- if (list_empty(report_list)) {
+ report = list_first_entry_or_null(report_list, struct hid_report, list);
+ if (!report) {
hid_err(hid, "no output reports found\n");
return -ENODEV;
}
-
- report = list_first_entry(report_list, struct hid_report, list);
for (i = 0; i < report->maxfield; i++) {
for (j = 0; j < report->field[i]->report_count; j++) {
report->field[i]->value[j] = 0x00;
@@ -100,13 +94,13 @@ static int axff_init(struct hid_device *hid)
if (!axff)
return -ENOMEM;
+ axff->report = report;
set_bit(FF_RUMBLE, dev->ffbit);
error = input_ff_create_memless(dev, axff, axff_play);
if (error)
goto err_free_mem;
- axff->report = report;
hid_hw_request(hid, axff->report, HID_REQ_SET_REPORT);
hid_info(hid, "Force Feedback for ACRUX game controllers by Sergei Kolzun <x0r@dv-life.ru>\n");
@@ -118,7 +112,8 @@ err_free_mem:
return error;
}
#else
-static inline int axff_init(struct hid_device *hid)
+static inline int ax_input_configured(struct hid_device *hid,
+ struct hid_input *hidinput)
{
return 0;
}
@@ -136,23 +131,11 @@ static int ax_probe(struct hid_device *hdev, const struct hid_device_id *id)
return error;
}
- error = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
+ error = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (error) {
hid_err(hdev, "hw start failed\n");
return error;
}
-
- error = axff_init(hdev);
- if (error) {
- /*
- * Do not fail device initialization completely as device
- * may still be partially operable, just warn.
- */
- hid_warn(hdev,
- "Failed to enable force feedback support, error: %d\n",
- error);
- }
-
/*
* We need to start polling device right away, otherwise
* it will go into a coma.
@@ -185,6 +168,7 @@ static struct hid_driver ax_driver = {
.id_table = ax_devices,
.probe = ax_probe,
.remove = ax_remove,
+ .input_configured = ax_input_configured,
};
module_hid_driver(ax_driver);
diff --git a/drivers/hid/hid-betopff.c b/drivers/hid/hid-betopff.c
index 8a7fe895926c..f802046a688a 100644
--- a/drivers/hid/hid-betopff.c
+++ b/drivers/hid/hid-betopff.c
@@ -52,31 +52,24 @@ static int hid_betopff_play(struct input_dev *dev, void *data,
return 0;
}
-static int betopff_init(struct hid_device *hid)
+static int betop_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct betopff_device *betopff;
struct hid_report *report;
- struct hid_input *hidinput;
struct list_head *report_list =
&hid->report_enum[HID_OUTPUT_REPORT].report_list;
- struct input_dev *dev;
+ struct input_dev *dev = hidinput->input;
int error;
int i, j;
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
- return -ENODEV;
- }
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
- hidinput = list_first_entry(&hid->inputs, struct hid_input, list);
- dev = hidinput->input;
-
- if (list_empty(report_list)) {
+ report = list_first_entry_or_null(report_list, struct hid_report, list);
+ if (!report) {
hid_err(hid, "no output reports found\n");
return -ENODEV;
}
-
- report = list_first_entry(report_list, struct hid_report, list);
/*
* Actually there are 4 fields for 4 Bytes as below:
* -----------------------------------------
@@ -104,6 +97,7 @@ static int betopff_init(struct hid_device *hid)
if (!betopff)
return -ENOMEM;
+ betopff->report = report;
set_bit(FF_RUMBLE, dev->ffbit);
error = input_ff_create_memless(dev, betopff, hid_betopff_play);
@@ -112,7 +106,6 @@ static int betopff_init(struct hid_device *hid)
return error;
}
- betopff->report = report;
hid_hw_request(hid, betopff->report, HID_REQ_SET_REPORT);
hid_info(hid, "Force feedback for betop devices by huangbo <huangbobupt@163.com>\n");
@@ -130,20 +123,15 @@ static int betop_probe(struct hid_device *hdev, const struct hid_device_id *id)
ret = hid_parse(hdev);
if (ret) {
hid_err(hdev, "parse failed\n");
- goto err;
+ return ret;
}
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (ret) {
hid_err(hdev, "hw start failed\n");
- goto err;
+ return ret;
}
-
- betopff_init(hdev);
-
return 0;
-err:
- return ret;
}
static const struct hid_device_id betop_devices[] = {
@@ -159,6 +147,7 @@ static struct hid_driver betop_driver = {
.name = "betop",
.id_table = betop_devices,
.probe = betop_probe,
+ .input_configured = betop_input_configured,
};
module_hid_driver(betop_driver);
diff --git a/drivers/hid/hid-bigbenff.c b/drivers/hid/hid-bigbenff.c
index 9f05465358d9..3c87317ccc6b 100644
--- a/drivers/hid/hid-bigbenff.c
+++ b/drivers/hid/hid-bigbenff.c
@@ -366,58 +366,29 @@ static void bigben_remove(struct hid_device *hid)
hid_hw_stop(hid);
}
-static int bigben_probe(struct hid_device *hid,
- const struct hid_device_id *id)
+static int bigben_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
- struct bigben_device *bigben;
- struct hid_input *hidinput;
+ struct bigben_device *bigben = hid_get_drvdata(hid);
+ struct input_dev *input_dev = hidinput->input;
struct led_classdev *led;
char *name;
size_t name_sz;
int n, error;
- bigben = devm_kzalloc(&hid->dev, sizeof(*bigben), GFP_KERNEL);
- if (!bigben)
- return -ENOMEM;
- hid_set_drvdata(hid, bigben);
- bigben->hid = hid;
- bigben->removed = false;
-
- error = hid_parse(hid);
- if (error) {
- hid_err(hid, "parse failed\n");
- return error;
- }
-
- error = hid_hw_start(hid, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
- if (error) {
- hid_err(hid, "hw start failed\n");
- return error;
- }
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
bigben->report = hid_validate_values(hid, HID_OUTPUT_REPORT, 0, 0, 8);
if (!bigben->report) {
hid_err(hid, "no output report found\n");
- error = -ENODEV;
- goto error_hw_stop;
- }
-
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
- error = -ENODEV;
- goto error_hw_stop;
+ return -ENODEV;
}
- hidinput = list_first_entry(&hid->inputs, struct hid_input, list);
- set_bit(FF_RUMBLE, hidinput->input->ffbit);
-
- INIT_WORK(&bigben->worker, bigben_worker);
- spin_lock_init(&bigben->lock);
+ set_bit(FF_RUMBLE, input_dev->ffbit);
- error = input_ff_create_memless(hidinput->input, NULL,
- hid_bigben_play_effect);
+ error = input_ff_create_memless(input_dev, NULL, hid_bigben_play_effect);
if (error)
- goto error_hw_stop;
+ return error;
name_sz = strlen(dev_name(&hid->dev)) + strlen(":red:bigben#") + 1;
@@ -427,10 +398,9 @@ static int bigben_probe(struct hid_device *hid,
sizeof(struct led_classdev) + name_sz,
GFP_KERNEL
);
- if (!led) {
- error = -ENOMEM;
- goto error_hw_stop;
- }
+ if (!led)
+ return -ENOMEM;
+
name = (void *)(&led[1]);
snprintf(name, name_sz,
"%s:red:bigben%d",
@@ -444,7 +414,7 @@ static int bigben_probe(struct hid_device *hid,
bigben->leds[n] = led;
error = devm_led_classdev_register(&hid->dev, led);
if (error)
- goto error_hw_stop;
+ return error;
}
/* initial state: LED1 is on, no rumble effect */
@@ -458,10 +428,36 @@ static int bigben_probe(struct hid_device *hid,
hid_info(hid, "LED and force feedback support for BigBen gamepad\n");
return 0;
+}
-error_hw_stop:
- hid_hw_stop(hid);
- return error;
+static int bigben_probe(struct hid_device *hid, const struct hid_device_id *id)
+{
+ struct bigben_device *bigben;
+ int error;
+
+ bigben = devm_kzalloc(&hid->dev, sizeof(*bigben), GFP_KERNEL);
+ if (!bigben)
+ return -ENOMEM;
+
+ hid_set_drvdata(hid, bigben);
+ bigben->hid = hid;
+ bigben->removed = false;
+ INIT_WORK(&bigben->worker, bigben_worker);
+ spin_lock_init(&bigben->lock);
+
+ error = hid_parse(hid);
+ if (error) {
+ hid_err(hid, "parse failed\n");
+ return error;
+ }
+
+ error = hid_hw_start(hid, HID_CONNECT_DEFAULT);
+ if (error) {
+ hid_err(hid, "hw start failed\n");
+ return error;
+ }
+
+ return 0;
}
static const __u8 *bigben_report_fixup(struct hid_device *hid, __u8 *rdesc,
@@ -487,6 +483,7 @@ static struct hid_driver bigben_driver = {
.probe = bigben_probe,
.report_fixup = bigben_report_fixup,
.remove = bigben_remove,
+ .input_configured = bigben_input_configured,
};
module_hid_driver(bigben_driver);
diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index cf123347a2af..a3ff0514f9cd 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -379,6 +379,9 @@ static int hid_add_field(struct hid_parser *parser, unsigned report_type, unsign
static u32 item_udata(struct hid_item *item)
{
+ if (item->format != HID_ITEM_FORMAT_SHORT)
+ return 0;
+
switch (item->size) {
case 1: return item->data.u8;
case 2: return item->data.u16;
@@ -389,6 +392,9 @@ static u32 item_udata(struct hid_item *item)
static s32 item_sdata(struct hid_item *item)
{
+ if (item->format != HID_ITEM_FORMAT_SHORT)
+ return 0;
+
switch (item->size) {
case 1: return item->data.s8;
case 2: return item->data.s16;
@@ -1933,13 +1939,14 @@ int hid_set_field(struct hid_field *field, unsigned offset, __s32 value)
size = field->report_size;
- hid_dump_input(field->report->device, field->usage + offset, value);
-
if (offset >= field->report_count) {
hid_err(field->report->device, "offset (%d) exceeds report_count (%d)\n",
offset, field->report_count);
return -1;
}
+
+ hid_dump_input(field->report->device, field->usage + offset, value);
+
if (field->logical_minimum < 0) {
if (value != snto32(s32ton(value, size), size)) {
hid_err(field->report->device, "value %d is out of range\n", value);
@@ -2312,8 +2319,8 @@ int hid_connect(struct hid_device *hdev, unsigned int connect_mask)
if (hid_hiddev(hdev))
connect_mask |= HID_CONNECT_HIDDEV_FORCE;
- if ((connect_mask & HID_CONNECT_HIDINPUT) && !hidinput_connect(hdev,
- connect_mask & HID_CONNECT_HIDINPUT_FORCE))
+ if ((connect_mask & HID_CONNECT_HIDINPUT) &&
+ !hidinput_connect(hdev, connect_mask))
hdev->claimed |= HID_CLAIMED_INPUT;
if ((connect_mask & HID_CONNECT_HIDDEV) && hdev->hiddev_connect &&
@@ -2335,10 +2342,6 @@ int hid_connect(struct hid_device *hdev, unsigned int connect_mask)
hid_process_ordering(hdev);
- if ((hdev->claimed & HID_CLAIMED_INPUT) &&
- (connect_mask & HID_CONNECT_FF) && hdev->ff_init)
- hdev->ff_init(hdev);
-
len = 0;
if (hdev->claimed & HID_CLAIMED_INPUT)
len += sprintf(buf + len, "input");
@@ -2447,9 +2450,16 @@ EXPORT_SYMBOL_GPL(hid_hw_start);
*
* This is usually called from remove function or from probe when something
* failed and hid_hw_start was called already.
+ *
+ * If the caller enabled HID input via hid_device_io_start() and is unwinding
+ * without an explicit hid_device_io_stop(), quiesce input first so that
+ * in-flight reports cannot reach handlers (e.g. hidraw_report_event) whose
+ * backing objects hid_disconnect() is about to free.
*/
void hid_hw_stop(struct hid_device *hdev)
{
+ if (hdev->io_started)
+ hid_device_io_stop(hdev);
hid_disconnect(hdev);
hdev->ll_driver->stop(hdev);
}
@@ -2842,6 +2852,8 @@ static int __hid_device_probe(struct hid_device *hdev, struct hid_driver *hdrv)
*/
if (ret) {
+ if (hdev->io_started)
+ hid_device_io_stop(hdev);
devres_release_group(&hdev->dev, hdev->devres_group_id);
hid_close_report(hdev);
hdev->driver = NULL;
@@ -2907,6 +2919,45 @@ static ssize_t modalias_show(struct device *dev, struct device_attribute *a,
}
static DEVICE_ATTR_RO(modalias);
+/*
+ * Expose this as bustype instead of bus as
+ * that's the name the input subsystem uses
+ */
+static ssize_t bustype_show(struct device *dev, struct device_attribute *a,
+ char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+
+ return sysfs_emit(buf, "%04x\n", hdev->bus);
+}
+static DEVICE_ATTR_RO(bustype);
+
+#define HID_DEV_ID_ATTR(name) \
+static ssize_t name##_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ struct hid_device *hdev = to_hid_device(dev); \
+ \
+ return sysfs_emit(buf, "%04x\n", hdev->name); \
+} \
+static DEVICE_ATTR_RO(name)
+
+HID_DEV_ID_ATTR(vendor);
+HID_DEV_ID_ATTR(product);
+HID_DEV_ID_ATTR(version);
+
+static struct attribute *hid_dev_id_attrs[] = {
+ &dev_attr_bustype.attr,
+ &dev_attr_vendor.attr,
+ &dev_attr_product.attr,
+ &dev_attr_version.attr,
+ NULL
+};
+static const struct attribute_group hid_dev_id_attr_group = {
+ .name = "id",
+ .attrs = hid_dev_id_attrs,
+};
static struct attribute *hid_dev_attrs[] = {
&dev_attr_modalias.attr,
NULL,
@@ -2919,7 +2970,11 @@ static const struct attribute_group hid_dev_group = {
.attrs = hid_dev_attrs,
.bin_attrs = hid_dev_bin_attrs,
};
-__ATTRIBUTE_GROUPS(hid_dev);
+static const struct attribute_group *hid_dev_groups[] = {
+ &hid_dev_group,
+ &hid_dev_id_attr_group,
+ NULL
+};
static int hid_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
diff --git a/drivers/hid/hid-corsair-void.c b/drivers/hid/hid-corsair-void.c
index 5e9a5b8f7f16..071a663a6c26 100644
--- a/drivers/hid/hid-corsair-void.c
+++ b/drivers/hid/hid-corsair-void.c
@@ -92,6 +92,9 @@
#define CORSAIR_VOID_STATUS_REPORT_ID 0x64
#define CORSAIR_VOID_FIRMWARE_REPORT_ID 0x66
+#define CORSAIR_VOID_STATUS_REPORT_SIZE 5
+#define CORSAIR_VOID_FIRMWARE_REPORT_SIZE 5
+
#define CORSAIR_VOID_USB_SIDETONE_REQUEST 0x1
#define CORSAIR_VOID_USB_SIDETONE_REQUEST_TYPE 0x21
#define CORSAIR_VOID_USB_SIDETONE_VALUE 0x200
@@ -742,6 +745,13 @@ static int corsair_void_raw_event(struct hid_device *hid_dev,
/* Description of packets are documented at the top of this file */
if (hid_report->id == CORSAIR_VOID_STATUS_REPORT_ID) {
+ if (size < CORSAIR_VOID_STATUS_REPORT_SIZE) {
+ hid_warn_ratelimited(hid_dev,
+ "unexpected status report of size %d",
+ size);
+ return 1;
+ }
+
drvdata->mic_up = FIELD_GET(CORSAIR_VOID_MIC_MASK, data[2]);
drvdata->connected = (data[3] == CORSAIR_VOID_WIRELESS_CONNECTED) ||
drvdata->is_wired;
@@ -750,6 +760,13 @@ static int corsair_void_raw_event(struct hid_device *hid_dev,
FIELD_GET(CORSAIR_VOID_CAPACITY_MASK, data[2]),
data[3], data[4]);
} else if (hid_report->id == CORSAIR_VOID_FIRMWARE_REPORT_ID) {
+ if (size < CORSAIR_VOID_FIRMWARE_REPORT_SIZE) {
+ hid_warn_ratelimited(hid_dev,
+ "unexpected firmware report of size %d",
+ size);
+ return 1;
+ }
+
drvdata->fw_receiver_major = data[1];
drvdata->fw_receiver_minor = data[2];
drvdata->fw_headset_major = data[3];
diff --git a/drivers/hid/hid-corsair.c b/drivers/hid/hid-corsair.c
index 21cd8b12a757..278c6efb565d 100644
--- a/drivers/hid/hid-corsair.c
+++ b/drivers/hid/hid-corsair.c
@@ -524,8 +524,8 @@ static void k90_cleanup_backlight(struct hid_device *dev)
if (drvdata->backlight) {
drvdata->backlight->removed = true;
- led_classdev_unregister(&drvdata->backlight->cdev);
cancel_work_sync(&drvdata->backlight->work);
+ led_classdev_unregister(&drvdata->backlight->cdev);
kfree(drvdata->backlight->cdev.name);
kfree(drvdata->backlight);
}
@@ -540,11 +540,12 @@ static void k90_cleanup_macro_functions(struct hid_device *dev)
sysfs_remove_group(&dev->dev.kobj, &k90_attr_group);
k90->record_led.removed = true;
- led_classdev_unregister(&k90->record_led.cdev);
cancel_work_sync(&k90->record_led.work);
+ led_classdev_unregister(&k90->record_led.cdev);
kfree(k90->record_led.cdev.name);
kfree(k90);
+ drvdata->k90 = NULL;
}
}
@@ -596,10 +597,10 @@ static int corsair_probe(struct hid_device *dev, const struct hid_device_id *id)
static void corsair_remove(struct hid_device *dev)
{
+ hid_hw_stop(dev);
+
k90_cleanup_macro_functions(dev);
k90_cleanup_backlight(dev);
-
- hid_hw_stop(dev);
}
static int corsair_event(struct hid_device *dev, struct hid_field *field,
diff --git a/drivers/hid/hid-dr.c b/drivers/hid/hid-dr.c
index 8a8f68a7feb0..a1e10ee8df4d 100644
--- a/drivers/hid/hid-dr.c
+++ b/drivers/hid/hid-dr.c
@@ -71,29 +71,26 @@ static int drff_play(struct input_dev *dev, void *data,
return 0;
}
-static int drff_init(struct hid_device *hid)
+static int dr_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct drff_device *drff;
struct hid_report *report;
- struct hid_input *hidinput;
struct list_head *report_list =
&hid->report_enum[HID_OUTPUT_REPORT].report_list;
- struct input_dev *dev;
+ struct input_dev *dev = hidinput->input;
int error;
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
- return -ENODEV;
- }
- hidinput = list_first_entry(&hid->inputs, struct hid_input, list);
- dev = hidinput->input;
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
+
+ if (hid->product != 0x0006)
+ return 0;
- if (list_empty(report_list)) {
+ report = list_first_entry_or_null(report_list, struct hid_report, list);
+ if (!report) {
hid_err(hid, "no output reports found\n");
return -ENODEV;
}
-
- report = list_first_entry(report_list, struct hid_report, list);
if (report->maxfield < 1) {
hid_err(hid, "no fields in the report\n");
return -ENODEV;
@@ -108,6 +105,7 @@ static int drff_init(struct hid_device *hid)
if (!drff)
return -ENOMEM;
+ drff->report = report;
set_bit(FF_RUMBLE, dev->ffbit);
error = input_ff_create_memless(dev, drff, drff_play);
@@ -116,7 +114,6 @@ static int drff_init(struct hid_device *hid)
return error;
}
- drff->report = report;
drff->report->field[0]->value[0] = 0xf3;
drff->report->field[0]->value[1] = 0x00;
drff->report->field[0]->value[2] = 0x00;
@@ -132,7 +129,8 @@ static int drff_init(struct hid_device *hid)
return 0;
}
#else
-static inline int drff_init(struct hid_device *hid)
+static inline int dr_input_configured(struct hid_device *hid,
+ struct hid_input *hidinput)
{
return 0;
}
@@ -266,43 +264,9 @@ static int dr_input_mapping(struct hid_device *hdev, struct hid_input *hi,
return 0;
}
-static int dr_probe(struct hid_device *hdev, const struct hid_device_id *id)
-{
- int ret;
-
- dev_dbg(&hdev->dev, "DragonRise Inc. HID hardware probe...");
-
- ret = hid_parse(hdev);
- if (ret) {
- hid_err(hdev, "parse failed\n");
- goto err;
- }
-
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
- if (ret) {
- hid_err(hdev, "hw start failed\n");
- goto err;
- }
-
- switch (hdev->product) {
- case 0x0006:
- ret = drff_init(hdev);
- if (ret) {
- dev_err(&hdev->dev, "force feedback init failed\n");
- hid_hw_stop(hdev);
- goto err;
- }
- break;
- }
-
- return 0;
-err:
- return ret;
-}
-
static const struct hid_device_id dr_devices[] = {
- { HID_USB_DEVICE(USB_VENDOR_ID_DRAGONRISE, 0x0006), },
- { HID_USB_DEVICE(USB_VENDOR_ID_DRAGONRISE, 0x0011), },
+ { HID_USB_DEVICE(USB_VENDOR_ID_DRAGONRISE, 0x0006), },
+ { HID_USB_DEVICE(USB_VENDOR_ID_DRAGONRISE, 0x0011), },
{ }
};
MODULE_DEVICE_TABLE(hid, dr_devices);
@@ -311,8 +275,8 @@ static struct hid_driver dr_driver = {
.name = "dragonrise",
.id_table = dr_devices,
.report_fixup = dr_report_fixup,
- .probe = dr_probe,
.input_mapping = dr_input_mapping,
+ .input_configured = dr_input_configured,
};
module_hid_driver(dr_driver);
diff --git a/drivers/hid/hid-emsff.c b/drivers/hid/hid-emsff.c
index 1b4ad18f6051..d8e559e0524f 100644
--- a/drivers/hid/hid-emsff.c
+++ b/drivers/hid/hid-emsff.c
@@ -43,29 +43,23 @@ static int emsff_play(struct input_dev *dev, void *data,
return 0;
}
-static int emsff_init(struct hid_device *hid)
+static int ems_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct emsff_device *emsff;
struct hid_report *report;
- struct hid_input *hidinput;
struct list_head *report_list =
&hid->report_enum[HID_OUTPUT_REPORT].report_list;
- struct input_dev *dev;
+ struct input_dev *dev = hidinput->input;
int error;
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
- return -ENODEV;
- }
- hidinput = list_first_entry(&hid->inputs, struct hid_input, list);
- dev = hidinput->input;
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
- if (list_empty(report_list)) {
+ report = list_first_entry_or_null(report_list, struct hid_report, list);
+ if (!report) {
hid_err(hid, "no output reports found\n");
return -ENODEV;
}
-
- report = list_first_entry(report_list, struct hid_report, list);
if (report->maxfield < 1) {
hid_err(hid, "no fields in the report\n");
return -ENODEV;
@@ -80,6 +74,7 @@ static int emsff_init(struct hid_device *hid)
if (!emsff)
return -ENOMEM;
+ emsff->report = report;
set_bit(FF_RUMBLE, dev->ffbit);
error = input_ff_create_memless(dev, emsff, emsff_play);
@@ -88,7 +83,6 @@ static int emsff_init(struct hid_device *hid)
return error;
}
- emsff->report = report;
emsff->report->field[0]->value[0] = 0x01;
emsff->report->field[0]->value[1] = 0x00;
emsff->report->field[0]->value[2] = 0x00;
@@ -103,34 +97,6 @@ static int emsff_init(struct hid_device *hid)
return 0;
}
-static int ems_probe(struct hid_device *hdev, const struct hid_device_id *id)
-{
- int ret;
-
- ret = hid_parse(hdev);
- if (ret) {
- hid_err(hdev, "parse failed\n");
- goto err;
- }
-
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
- if (ret) {
- hid_err(hdev, "hw start failed\n");
- goto err;
- }
-
- ret = emsff_init(hdev);
- if (ret) {
- dev_err(&hdev->dev, "force feedback init failed\n");
- hid_hw_stop(hdev);
- goto err;
- }
-
- return 0;
-err:
- return ret;
-}
-
static const struct hid_device_id ems_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_EMS, USB_DEVICE_ID_EMS_TRIO_LINKER_PLUS_II) },
{ }
@@ -140,7 +106,7 @@ MODULE_DEVICE_TABLE(hid, ems_devices);
static struct hid_driver ems_driver = {
.name = "hkems",
.id_table = ems_devices,
- .probe = ems_probe,
+ .input_configured = ems_input_configured,
};
module_hid_driver(ems_driver);
diff --git a/drivers/hid/hid-ft260.c b/drivers/hid/hid-ft260.c
index 70e2eedb465a..64c9c940db46 100644
--- a/drivers/hid/hid-ft260.c
+++ b/drivers/hid/hid-ft260.c
@@ -240,6 +240,8 @@ struct ft260_device {
struct mutex lock;
u8 write_buf[FT260_REPORT_MAX_LENGTH];
unsigned long need_wakeup_at;
+ /* Protects read_buf, read_idx and read_len against ft260_raw_event() */
+ spinlock_t read_lock;
u8 *read_buf;
u16 read_idx;
u16 read_len;
@@ -501,16 +503,25 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
int timeout, ret = 0;
struct ft260_i2c_read_request_report rep;
struct hid_device *hdev = dev->hdev;
+ unsigned long irqflags;
u8 bus_busy = 0;
+ /*
+ * STOP terminates the last chunk; clear means hold the bus so a
+ * follow-up call continues the same I2C transaction.
+ */
+ bool want_stop = !!(flag & FT260_FLAG_STOP);
if ((flag & FT260_FLAG_START_REPEATED) == FT260_FLAG_START_REPEATED)
flag = FT260_FLAG_START_REPEATED;
- else
+ else if (flag & FT260_FLAG_START)
flag = FT260_FLAG_START;
+ else
+ flag = 0; /* no fresh START - continue current transaction */
do {
if (len <= rd_data_max) {
rd_len = len;
- flag |= FT260_FLAG_STOP;
+ if (want_stop)
+ flag |= FT260_FLAG_STOP;
} else {
rd_len = rd_data_max;
}
@@ -526,9 +537,11 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
reinit_completion(&dev->wait);
+ spin_lock_irqsave(&dev->read_lock, irqflags);
dev->read_idx = 0;
dev->read_buf = data;
dev->read_len = rd_len;
+ spin_unlock_irqrestore(&dev->read_lock, irqflags);
ret = ft260_hid_output_report(hdev, (u8 *)&rep, sizeof(rep));
if (ret < 0) {
@@ -543,7 +556,9 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
goto ft260_i2c_read_exit;
}
+ spin_lock_irqsave(&dev->read_lock, irqflags);
dev->read_buf = NULL;
+ spin_unlock_irqrestore(&dev->read_lock, irqflags);
if (flag & FT260_FLAG_STOP)
bus_busy = FT260_I2C_STATUS_BUS_BUSY;
@@ -562,7 +577,9 @@ static int ft260_i2c_read(struct ft260_device *dev, u8 addr, u8 *data,
} while (len > 0);
ft260_i2c_read_exit:
+ spin_lock_irqsave(&dev->read_lock, irqflags);
dev->read_buf = NULL;
+ spin_unlock_irqrestore(&dev->read_lock, irqflags);
return ret;
}
@@ -708,14 +725,41 @@ static int ft260_smbus_xfer(struct i2c_adapter *adapter, u16 addr, u16 flags,
break;
case I2C_SMBUS_BLOCK_DATA:
if (read_write == I2C_SMBUS_READ) {
+ u8 count = 0;
+
+ /*
+ * SMBus 2.0 section 6.5.7 block read in one I2C
+ * transaction:
+ *
+ * S Addr+Wr A Reg A Sr Addr+Rd A Count A Data... P
+ *
+ * The count is read separately and validated
+ * before sizing the data read so a misbehaving
+ * slave cannot drive a write past data->block[].
+ */
ret = ft260_smbus_write(dev, addr, cmd, NULL, 0,
FT260_FLAG_START);
if (ret)
goto smbus_exit;
- ret = ft260_i2c_read(dev, addr, data->block,
- data->block[0] + 1,
- FT260_FLAG_START_STOP_REPEATED);
+ ret = ft260_i2c_read(dev, addr, &count, 1,
+ FT260_FLAG_START_REPEATED);
+ if (ret)
+ goto smbus_exit;
+
+ if (count == 0 || count > I2C_SMBUS_BLOCK_MAX) {
+ hid_warn(hdev,
+ "smbus block read: invalid count %u from slave 0x%02x\n",
+ count, addr);
+ ft260_i2c_reset(hdev);
+ ret = -EPROTO;
+ goto smbus_exit;
+ }
+
+ data->block[0] = count;
+
+ ret = ft260_i2c_read(dev, addr, data->block + 1,
+ count, FT260_FLAG_STOP);
} else {
ret = ft260_smbus_write(dev, addr, cmd, data->block,
data->block[0] + 1,
@@ -1018,6 +1062,7 @@ static int ft260_probe(struct hid_device *hdev, const struct hid_device_id *id)
"FT260 usb-i2c bridge");
mutex_init(&dev->lock);
+ spin_lock_init(&dev->read_lock);
init_completion(&dev->wait);
ret = ft260_xfer_status(dev, FT260_I2C_STATUS_BUS_BUSY);
@@ -1067,6 +1112,7 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
{
struct ft260_device *dev = hid_get_drvdata(hdev);
struct ft260_i2c_input_report *xfer = (void *)data;
+ unsigned long irqflags;
if (size < offsetof(struct ft260_i2c_input_report, data)) {
hid_err(hdev, "short report %d\n", size);
@@ -1075,6 +1121,8 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
if (xfer->report >= FT260_I2C_REPORT_MIN &&
xfer->report <= FT260_I2C_REPORT_MAX) {
+ bool complete_read;
+
ft260_dbg("i2c resp: rep %#02x len %d size %d\n",
xfer->report, xfer->length, size);
@@ -1085,8 +1133,15 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
return -1;
}
+ /*
+ * Hold read_lock so a timed-out ft260_i2c_read() cannot
+ * clear read_buf between the NULL check and the memcpy.
+ */
+ spin_lock_irqsave(&dev->read_lock, irqflags);
+
if ((dev->read_buf == NULL) ||
(xfer->length > dev->read_len - dev->read_idx)) {
+ spin_unlock_irqrestore(&dev->read_lock, irqflags);
hid_err(hdev, "unexpected report %#02x, length %d\n",
xfer->report, xfer->length);
return -1;
@@ -1095,8 +1150,11 @@ static int ft260_raw_event(struct hid_device *hdev, struct hid_report *report,
memcpy(&dev->read_buf[dev->read_idx], &xfer->data,
xfer->length);
dev->read_idx += xfer->length;
+ complete_read = dev->read_idx == dev->read_len;
+
+ spin_unlock_irqrestore(&dev->read_lock, irqflags);
- if (dev->read_idx == dev->read_len)
+ if (complete_read)
complete(&dev->wait);
} else {
diff --git a/drivers/hid/hid-gaff.c b/drivers/hid/hid-gaff.c
index 8b99686b63df..ec793a179b47 100644
--- a/drivers/hid/hid-gaff.c
+++ b/drivers/hid/hid-gaff.c
@@ -60,32 +60,23 @@ static int hid_gaff_play(struct input_dev *dev, void *data,
return 0;
}
-static int gaff_init(struct hid_device *hid)
+static int gaff_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct gaff_device *gaff;
struct hid_report *report;
- struct hid_input *hidinput;
struct list_head *report_list =
&hid->report_enum[HID_OUTPUT_REPORT].report_list;
- struct list_head *report_ptr = report_list;
- struct input_dev *dev;
+ struct input_dev *dev = hidinput->input;
int error;
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
- return -ENODEV;
- }
- hidinput = list_entry(hid->inputs.next, struct hid_input, list);
- dev = hidinput->input;
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
- if (list_empty(report_list)) {
+ report = list_first_entry_or_null(report_list, struct hid_report, list);
+ if (!report) {
hid_err(hid, "no output reports found\n");
return -ENODEV;
}
-
- report_ptr = report_ptr->next;
-
- report = list_entry(report_ptr, struct hid_report, list);
if (report->maxfield < 1) {
hid_err(hid, "no fields in the report\n");
return -ENODEV;
@@ -100,6 +91,7 @@ static int gaff_init(struct hid_device *hid)
if (!gaff)
return -ENOMEM;
+ gaff->report = report;
set_bit(FF_RUMBLE, dev->ffbit);
error = input_ff_create_memless(dev, gaff, hid_gaff_play);
@@ -108,7 +100,6 @@ static int gaff_init(struct hid_device *hid)
return error;
}
- gaff->report = report;
gaff->report->field[0]->value[0] = 0x51;
gaff->report->field[0]->value[1] = 0x00;
gaff->report->field[0]->value[2] = 0x00;
@@ -125,37 +116,13 @@ static int gaff_init(struct hid_device *hid)
return 0;
}
#else
-static inline int gaff_init(struct hid_device *hdev)
+static inline int gaff_input_configured(struct hid_device *hdev,
+ struct hid_input *hidinput)
{
return 0;
}
#endif
-static int ga_probe(struct hid_device *hdev, const struct hid_device_id *id)
-{
- int ret;
-
- dev_dbg(&hdev->dev, "Greenasia HID hardware probe...");
-
- ret = hid_parse(hdev);
- if (ret) {
- hid_err(hdev, "parse failed\n");
- goto err;
- }
-
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
- if (ret) {
- hid_err(hdev, "hw start failed\n");
- goto err;
- }
-
- gaff_init(hdev);
-
- return 0;
-err:
- return ret;
-}
-
static const struct hid_device_id ga_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_GREENASIA, 0x0012), },
{ }
@@ -165,7 +132,7 @@ MODULE_DEVICE_TABLE(hid, ga_devices);
static struct hid_driver ga_driver = {
.name = "greenasia",
.id_table = ga_devices,
- .probe = ga_probe,
+ .input_configured = gaff_input_configured,
};
module_hid_driver(ga_driver);
diff --git a/drivers/hid/hid-goodix-spi.c b/drivers/hid/hid-goodix-spi.c
index 288cb827e9d6..03d549efbdce 100644
--- a/drivers/hid/hid-goodix-spi.c
+++ b/drivers/hid/hid-goodix-spi.c
@@ -722,11 +722,8 @@ static int goodix_spi_probe(struct spi_device *spi)
error = devm_request_threaded_irq(&ts->spi->dev, ts->spi->irq,
NULL, goodix_hid_irq, IRQF_ONESHOT,
"goodix_spi_hid", ts);
- if (error) {
- dev_err(ts->dev, "could not register interrupt, irq = %d, %d",
- ts->spi->irq, error);
+ if (error)
goto err_destroy_hid;
- }
return 0;
diff --git a/drivers/hid/hid-google-stadiaff.c b/drivers/hid/hid-google-stadiaff.c
index 6b38d2421d3d..0214aae6b0fa 100644
--- a/drivers/hid/hid-google-stadiaff.c
+++ b/drivers/hid/hid-google-stadiaff.c
@@ -17,10 +17,7 @@
struct stadiaff_device {
struct hid_device *hid;
struct hid_report *report;
- spinlock_t lock;
- bool removed;
- uint16_t strong_magnitude;
- uint16_t weak_magnitude;
+ u32 magnitudes;
struct work_struct work;
};
@@ -29,12 +26,10 @@ static void stadiaff_work(struct work_struct *work)
struct stadiaff_device *stadiaff =
container_of(work, struct stadiaff_device, work);
struct hid_field *rumble_field = stadiaff->report->field[0];
- unsigned long flags;
+ u32 mags = READ_ONCE(stadiaff->magnitudes);
- spin_lock_irqsave(&stadiaff->lock, flags);
- rumble_field->value[0] = stadiaff->strong_magnitude;
- rumble_field->value[1] = stadiaff->weak_magnitude;
- spin_unlock_irqrestore(&stadiaff->lock, flags);
+ rumble_field->value[0] = mags & 0xffff;
+ rumble_field->value[1] = (mags >> 16) & 0xffff;
hid_hw_request(stadiaff->hid, stadiaff->report, HID_REQ_SET_REPORT);
}
@@ -44,33 +39,50 @@ static int stadiaff_play(struct input_dev *dev, void *data,
{
struct hid_device *hid = input_get_drvdata(dev);
struct stadiaff_device *stadiaff = hid_get_drvdata(hid);
- unsigned long flags;
+ u32 mags = (u32)effect->u.rumble.strong_magnitude |
+ ((u32)effect->u.rumble.weak_magnitude << 16);
- spin_lock_irqsave(&stadiaff->lock, flags);
- if (!stadiaff->removed) {
- stadiaff->strong_magnitude = effect->u.rumble.strong_magnitude;
- stadiaff->weak_magnitude = effect->u.rumble.weak_magnitude;
- schedule_work(&stadiaff->work);
- }
- spin_unlock_irqrestore(&stadiaff->lock, flags);
+ WRITE_ONCE(stadiaff->magnitudes, mags);
+ schedule_work(&stadiaff->work);
return 0;
}
-static int stadiaff_init(struct hid_device *hid)
+static int stadia_input_open(struct input_dev *dev)
+{
+ struct hid_device *hid = input_get_drvdata(dev);
+ struct stadiaff_device *stadiaff = hid_get_drvdata(hid);
+ int error;
+
+ error = hid_hw_open(hid);
+ if (error)
+ return error;
+
+ enable_work(&stadiaff->work);
+ return 0;
+}
+
+static void stadia_input_close(struct input_dev *dev)
+{
+ struct hid_device *hid = input_get_drvdata(dev);
+ struct stadiaff_device *stadiaff = hid_get_drvdata(hid);
+
+ WRITE_ONCE(stadiaff->magnitudes, 0);
+ stadiaff_work(&stadiaff->work);
+ disable_work_sync(&stadiaff->work);
+
+ hid_hw_close(hid);
+}
+
+static int stadia_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct stadiaff_device *stadiaff;
struct hid_report *report;
- struct hid_input *hidinput;
- struct input_dev *dev;
+ struct input_dev *dev = hidinput->input;
int error;
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
- return -ENODEV;
- }
- hidinput = list_entry(hid->inputs.next, struct hid_input, list);
- dev = hidinput->input;
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
report = hid_validate_values(hid, HID_OUTPUT_REPORT,
STADIA_FF_REPORT_ID, 0, 2);
@@ -90,56 +102,19 @@ static int stadiaff_init(struct hid_device *hid)
if (error)
return error;
- stadiaff->removed = false;
stadiaff->hid = hid;
stadiaff->report = report;
INIT_WORK(&stadiaff->work, stadiaff_work);
- spin_lock_init(&stadiaff->lock);
-
- hid_info(hid, "Force Feedback for Google Stadia controller\n");
+ disable_work_sync(&stadiaff->work);
- return 0;
-}
+ dev->open = stadia_input_open;
+ dev->close = stadia_input_close;
-static int stadia_probe(struct hid_device *hdev, const struct hid_device_id *id)
-{
- int ret;
-
- ret = hid_parse(hdev);
- if (ret) {
- hid_err(hdev, "parse failed\n");
- return ret;
- }
-
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
- if (ret) {
- hid_err(hdev, "hw start failed\n");
- return ret;
- }
-
- ret = stadiaff_init(hdev);
- if (ret) {
- hid_err(hdev, "force feedback init failed\n");
- hid_hw_stop(hdev);
- return ret;
- }
+ hid_info(hid, "Force Feedback for Google Stadia controller\n");
return 0;
}
-static void stadia_remove(struct hid_device *hid)
-{
- struct stadiaff_device *stadiaff = hid_get_drvdata(hid);
- unsigned long flags;
-
- spin_lock_irqsave(&stadiaff->lock, flags);
- stadiaff->removed = true;
- spin_unlock_irqrestore(&stadiaff->lock, flags);
-
- cancel_work_sync(&stadiaff->work);
- hid_hw_stop(hid);
-}
-
static const struct hid_device_id stadia_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_GOOGLE, USB_DEVICE_ID_GOOGLE_STADIA) },
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_GOOGLE, USB_DEVICE_ID_GOOGLE_STADIA) },
@@ -150,8 +125,7 @@ MODULE_DEVICE_TABLE(hid, stadia_devices);
static struct hid_driver stadia_driver = {
.name = "stadia",
.id_table = stadia_devices,
- .probe = stadia_probe,
- .remove = stadia_remove,
+ .input_configured = stadia_input_configured,
};
module_hid_driver(stadia_driver);
diff --git a/drivers/hid/hid-haptic.c b/drivers/hid/hid-haptic.c
index deadab28cdbe..8760eeb08b2c 100644
--- a/drivers/hid/hid-haptic.c
+++ b/drivers/hid/hid-haptic.c
@@ -82,16 +82,24 @@ int hid_haptic_input_configured(struct hid_device *hdev,
struct hid_haptic_device *haptic,
struct hid_input *hi)
{
+ int error;
- if (hi->application == HID_DG_TOUCHPAD) {
- if (haptic->auto_trigger_report &&
- haptic->manual_trigger_report) {
- __set_bit(INPUT_PROP_PRESSUREPAD, hi->input->propbit);
- return 1;
- }
+ if (hi->application != HID_DG_TOUCHPAD)
+ return -1;
+
+ if (!haptic->auto_trigger_report || !haptic->manual_trigger_report)
+ return 0;
+
+ __set_bit(INPUT_PROP_PRESSUREPAD, hi->input->propbit);
+
+ error = hid_haptic_init(hdev, haptic, hi->input);
+ if (error) {
+ dev_warn(&hdev->dev, "Cannot allocate haptic for %s\n",
+ hdev->name);
return 0;
}
- return -1;
+
+ return 1;
}
EXPORT_SYMBOL_GPL(hid_haptic_input_configured);
@@ -187,7 +195,7 @@ static void fill_effect_buf(struct hid_haptic_device *haptic,
value = waveform_ordinal;
break;
default:
- break;
+ continue;
}
field->value[j] = value;
@@ -401,11 +409,9 @@ static void hid_haptic_destroy(struct ff_device *ff)
}
int hid_haptic_init(struct hid_device *hdev,
- struct hid_haptic_device **haptic_ptr)
+ struct hid_haptic_device *haptic,
+ struct input_dev *dev)
{
- struct hid_haptic_device *haptic = *haptic_ptr;
- struct input_dev *dev = NULL;
- struct hid_input *hidinput;
struct ff_device *ff;
int ret = 0, r;
struct ff_haptic_effect stop_effect = {
@@ -447,19 +453,6 @@ int hid_haptic_init(struct hid_device *hdev,
for (r = 0; r < haptic->auto_trigger_report->maxfield; r++)
parse_auto_trigger_field(haptic, haptic->auto_trigger_report->field[r]);
- list_for_each_entry(hidinput, &hdev->inputs, list) {
- if (hidinput->application == HID_DG_TOUCHPAD) {
- dev = hidinput->input;
- break;
- }
- }
-
- if (!dev) {
- dev_err(&hdev->dev, "Failed to find the input device\n");
- ret = -ENODEV;
- goto duration_map;
- }
-
haptic->input_dev = dev;
haptic->manual_trigger_report_len =
hid_report_len(haptic->manual_trigger_report);
@@ -535,10 +528,6 @@ int hid_haptic_init(struct hid_device *hdev,
input_free:
input_ff_destroy(dev);
- /* Do not let double free happen, input_ff_destroy will call
- * hid_haptic_destroy.
- */
- *haptic_ptr = NULL;
/* Restore dev flush and event */
dev->flush = flush;
dev->event = event;
diff --git a/drivers/hid/hid-haptic.h b/drivers/hid/hid-haptic.h
index c6539ac04c1d..6332991a7844 100644
--- a/drivers/hid/hid-haptic.h
+++ b/drivers/hid/hid-haptic.h
@@ -69,7 +69,8 @@ int hid_haptic_input_mapping(struct hid_device *hdev,
int hid_haptic_input_configured(struct hid_device *hdev,
struct hid_haptic_device *haptic,
struct hid_input *hi);
-int hid_haptic_init(struct hid_device *hdev, struct hid_haptic_device **haptic_ptr);
+int hid_haptic_init(struct hid_device *hdev, struct hid_haptic_device *haptic,
+ struct input_dev *dev);
void hid_haptic_handle_press_release(struct hid_haptic_device *haptic);
void hid_haptic_pressure_reset(struct hid_haptic_device *haptic);
void hid_haptic_pressure_increase(struct hid_haptic_device *haptic,
@@ -107,7 +108,8 @@ static inline
void hid_haptic_reset(struct hid_device *hdev, struct hid_haptic_device *haptic)
{}
static inline
-int hid_haptic_init(struct hid_device *hdev, struct hid_haptic_device **haptic_ptr)
+int hid_haptic_init(struct hid_device *hdev, struct hid_haptic_device *haptic,
+ struct input_dev *dev)
{
return 0;
}
diff --git a/drivers/hid/hid-holtekff.c b/drivers/hid/hid-holtekff.c
index 32d08f7a660d..4834d42b2fa6 100644
--- a/drivers/hid/hid-holtekff.c
+++ b/drivers/hid/hid-holtekff.c
@@ -120,30 +120,24 @@ static int holtekff_play(struct input_dev *dev, void *data,
return 0;
}
-static int holtekff_init(struct hid_device *hid)
+static int holtek_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct holtekff_device *holtekff;
struct hid_report *report;
- struct hid_input *hidinput;
struct list_head *report_list =
&hid->report_enum[HID_OUTPUT_REPORT].report_list;
- struct input_dev *dev;
+ struct input_dev *dev = hidinput->input;
int error;
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
- return -ENODEV;
- }
- hidinput = list_entry(hid->inputs.next, struct hid_input, list);
- dev = hidinput->input;
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
- if (list_empty(report_list)) {
+ report = list_first_entry_or_null(report_list, struct hid_report, list);
+ if (!report) {
hid_err(hid, "no output report found\n");
return -ENODEV;
}
- report = list_entry(report_list->next, struct hid_report, list);
-
if (report->maxfield < 1 || report->field[0]->report_count != 7) {
hid_err(hid, "unexpected output report layout\n");
return -ENODEV;
@@ -172,35 +166,13 @@ static int holtekff_init(struct hid_device *hid)
return 0;
}
#else
-static inline int holtekff_init(struct hid_device *hid)
+static inline int holtek_input_configured(struct hid_device *hid,
+ struct hid_input *hidinput)
{
return 0;
}
#endif
-static int holtek_probe(struct hid_device *hdev, const struct hid_device_id *id)
-{
- int ret;
-
- ret = hid_parse(hdev);
- if (ret) {
- hid_err(hdev, "parse failed\n");
- goto err;
- }
-
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
- if (ret) {
- hid_err(hdev, "hw start failed\n");
- goto err;
- }
-
- holtekff_init(hdev);
-
- return 0;
-err:
- return ret;
-}
-
static const struct hid_device_id holtek_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_HOLTEK, USB_DEVICE_ID_HOLTEK_ON_LINE_GRIP) },
{ }
@@ -210,7 +182,7 @@ MODULE_DEVICE_TABLE(hid, holtek_devices);
static struct hid_driver holtek_driver = {
.name = "holtek",
.id_table = holtek_devices,
- .probe = holtek_probe,
+ .input_configured = holtek_input_configured,
};
module_hid_driver(holtek_driver);
diff --git a/drivers/hid/hid-huawei.c b/drivers/hid/hid-huawei.c
index 6a616bf21b38..ee3fc6f68475 100644
--- a/drivers/hid/hid-huawei.c
+++ b/drivers/hid/hid-huawei.c
@@ -44,11 +44,12 @@ static const __u8 huawei_cd30_kbd_rdesc_fixed[] = {
static const __u8 *huawei_report_fixup(struct hid_device *hdev, __u8 *rdesc,
unsigned int *rsize)
{
- struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_interface *intf = hid_is_usb(hdev) ?
+ to_usb_interface(hdev->dev.parent) : NULL;
switch (hdev->product) {
case USB_DEVICE_ID_HUAWEI_CD30KBD:
- if (intf->cur_altsetting->desc.bInterfaceNumber == 1) {
+ if (!intf || intf->cur_altsetting->desc.bInterfaceNumber == 1) {
if (*rsize != sizeof(huawei_cd30_kbd_rdesc_fixed) ||
memcmp(huawei_cd30_kbd_rdesc_fixed, rdesc,
sizeof(huawei_cd30_kbd_rdesc_fixed)) != 0) {
diff --git a/drivers/hid/hid-hyperv.c b/drivers/hid/hid-hyperv.c
index 7d2b0063df15..6579bd19da13 100644
--- a/drivers/hid/hid-hyperv.c
+++ b/drivers/hid/hid-hyperv.c
@@ -13,6 +13,9 @@
#include <linux/hiddev.h>
#include <linux/hyperv.h>
+#if IS_ENABLED(CONFIG_HID_HYPERV_MOUSE_KUNIT_TEST)
+#include <kunit/test.h>
+#endif
struct hv_input_dev_info {
unsigned int size;
@@ -171,18 +174,32 @@ static void mousevsc_free_device(struct mousevsc_dev *device)
}
static void mousevsc_on_receive_device_info(struct mousevsc_dev *input_device,
- struct synthhid_device_info *device_info)
+ struct synthhid_device_info *device_info,
+ u32 device_info_size)
{
int ret = 0;
struct hid_descriptor *desc;
struct mousevsc_prt_msg ack;
+ size_t desc_offset;
+ size_t desc_size;
input_device->dev_info_status = -ENOMEM;
+ if (device_info_size < sizeof(*device_info)) {
+ input_device->dev_info_status = -EINVAL;
+ goto cleanup;
+ }
+
input_device->hid_dev_info = device_info->hid_dev_info;
desc = &device_info->hid_descriptor;
+ desc_offset = offsetof(struct synthhid_device_info, hid_descriptor);
+ desc_size = device_info_size - desc_offset;
if (desc->bLength == 0)
goto cleanup;
+ if (desc->bLength < sizeof(*desc) || desc->bLength > desc_size) {
+ input_device->dev_info_status = -EINVAL;
+ goto cleanup;
+ }
/* The pointer is not NULL when we resume from hibernation */
kfree(input_device->hid_desc);
@@ -197,6 +214,10 @@ static void mousevsc_on_receive_device_info(struct mousevsc_dev *input_device,
input_device->dev_info_status = -EINVAL;
goto cleanup;
}
+ if (input_device->report_desc_size > desc_size - desc->bLength) {
+ input_device->dev_info_status = -EINVAL;
+ goto cleanup;
+ }
/* The pointer is not NULL when we resume from hibernation */
kfree(input_device->report_desc);
@@ -222,13 +243,18 @@ static void mousevsc_on_receive_device_info(struct mousevsc_dev *input_device,
ack.ack.header.size = 1;
ack.ack.reserved = 0;
- ret = vmbus_sendpacket(input_device->device->channel,
- &ack,
- sizeof(struct pipe_prt_msg) +
- sizeof(struct synthhid_device_info_ack),
- (unsigned long)&ack,
- VM_PKT_DATA_INBAND,
- VMBUS_DATA_PACKET_FLAG_COMPLETION_REQUESTED);
+ if (IS_ENABLED(CONFIG_HID_HYPERV_MOUSE_KUNIT_TEST) &&
+ !input_device->device) {
+ ret = 0;
+ } else {
+ ret = vmbus_sendpacket(input_device->device->channel,
+ &ack,
+ sizeof(struct pipe_prt_msg) +
+ sizeof(struct synthhid_device_info_ack),
+ (unsigned long)&ack,
+ VM_PKT_DATA_INBAND,
+ VMBUS_DATA_PACKET_FLAG_COMPLETION_REQUESTED);
+ }
if (!ret)
input_device->dev_info_status = 0;
@@ -273,14 +299,17 @@ static void mousevsc_on_receive(struct hv_device *device,
break;
case SYNTH_HID_INITIAL_DEVICE_INFO:
- WARN_ON(pipe_msg->size < sizeof(struct hv_input_dev_info));
+ if (WARN_ON_ONCE(pipe_msg->size <
+ sizeof(struct synthhid_device_info)))
+ break;
/*
* Parse out the device info into device attr,
* hid desc and report desc
*/
mousevsc_on_receive_device_info(input_dev,
- (struct synthhid_device_info *)pipe_msg->data);
+ (struct synthhid_device_info *)pipe_msg->data,
+ pipe_msg->size);
break;
case SYNTH_HID_INPUT_REPORT:
input_report =
@@ -614,5 +643,100 @@ static void __exit mousevsc_exit(void)
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Microsoft Hyper-V Synthetic HID Driver");
+#if IS_ENABLED(CONFIG_HID_HYPERV_MOUSE_KUNIT_TEST)
+static struct mousevsc_dev *mousevsc_kunit_alloc_dev(struct kunit *test)
+{
+ struct mousevsc_dev *input_dev;
+
+ input_dev = kunit_kzalloc(test, sizeof(*input_dev), GFP_KERNEL);
+ if (!input_dev)
+ return NULL;
+
+ init_completion(&input_dev->wait_event);
+
+ return input_dev;
+}
+
+static void mousevsc_device_info_zero_blength(struct kunit *test)
+{
+ struct synthhid_device_info *info;
+ struct mousevsc_dev *input_dev;
+
+ input_dev = mousevsc_kunit_alloc_dev(test);
+ KUNIT_ASSERT_NOT_NULL(test, input_dev);
+ info = kunit_kzalloc(test, sizeof(*info), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, info);
+
+ info->hid_descriptor.bLength = 0;
+
+ mousevsc_on_receive_device_info(input_dev, info, sizeof(*info));
+
+ KUNIT_EXPECT_EQ(test, input_dev->dev_info_status, -ENOMEM);
+}
+
+static void mousevsc_device_info_valid_descriptor(struct kunit *test)
+{
+ struct synthhid_device_info *info;
+ struct mousevsc_dev *input_dev;
+ u8 *report;
+
+ input_dev = mousevsc_kunit_alloc_dev(test);
+ KUNIT_ASSERT_NOT_NULL(test, input_dev);
+ info = kunit_kzalloc(test, sizeof(*info) + 4, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, info);
+
+ info->hid_descriptor.bLength = sizeof(struct hid_descriptor);
+ info->hid_descriptor.rpt_desc.wDescriptorLength = cpu_to_le16(4);
+ report = ((u8 *)&info->hid_descriptor) + info->hid_descriptor.bLength;
+ memset(report, 0x42, 4);
+
+ mousevsc_on_receive_device_info(input_dev, info, sizeof(*info) + 4);
+
+ KUNIT_EXPECT_EQ(test, input_dev->dev_info_status, 0);
+ KUNIT_EXPECT_EQ(test, input_dev->report_desc_size, 4);
+ KUNIT_EXPECT_MEMEQ(test, input_dev->report_desc, report, 4);
+
+ kfree(input_dev->hid_desc);
+ kfree(input_dev->report_desc);
+}
+
+static void mousevsc_device_info_report_desc_oob(struct kunit *test)
+{
+ struct synthhid_device_info *info;
+ struct mousevsc_dev *input_dev;
+ u8 *report;
+
+ input_dev = mousevsc_kunit_alloc_dev(test);
+ KUNIT_ASSERT_NOT_NULL(test, input_dev);
+ info = kunit_kzalloc(test, sizeof(*info) + 8, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, info);
+
+ info->hid_descriptor.bLength = sizeof(struct hid_descriptor);
+ info->hid_descriptor.rpt_desc.wDescriptorLength = cpu_to_le16(64);
+ report = ((u8 *)&info->hid_descriptor) + info->hid_descriptor.bLength;
+ memset(report, 0x42, 8);
+
+ mousevsc_on_receive_device_info(input_dev, info, sizeof(*info) + 8);
+
+ KUNIT_EXPECT_EQ(test, input_dev->dev_info_status, -EINVAL);
+
+ kfree(input_dev->hid_desc);
+}
+
+static struct kunit_case mousevsc_test_cases[] = {
+ KUNIT_CASE(mousevsc_device_info_zero_blength),
+ KUNIT_CASE(mousevsc_device_info_valid_descriptor),
+ KUNIT_CASE(mousevsc_device_info_report_desc_oob),
+ {}
+};
+
+static struct kunit_suite mousevsc_test_suite = {
+ .name = "hid_hyperv_mouse",
+ .test_cases = mousevsc_test_cases,
+};
+
+kunit_test_suite(mousevsc_test_suite);
+#endif
+
module_init(mousevsc_init);
module_exit(mousevsc_exit);
diff --git a/drivers/hid/hid-hyperx.c b/drivers/hid/hid-hyperx.c
new file mode 100644
index 000000000000..18d2dc24cc76
--- /dev/null
+++ b/drivers/hid/hid-hyperx.c
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for the HyperX QuadCast 2 microphone
+ *
+ * The tap-to-mute button is handled entirely in the device firmware: it gates
+ * the audio internally and does not send the Telephony "Phone Mute" usage its
+ * own report descriptor advertises. The resulting mute state is only reported
+ * through a vendor-defined collection, which hid-input cannot map, so the
+ * button is invisible to userspace.
+ *
+ * Copyright (c) 2026 Benjamin Blume <benjaminblume@posteo.de>
+ */
+
+#include <linux/hid.h>
+#include <linux/input.h>
+#include <linux/module.h>
+
+#include "hid-ids.h"
+
+#define HYPERX_QC2_REPORT_ID 0x77
+#define HYPERX_QC2_EVENT_MUTE 0x06
+#define HYPERX_QC2_MUTE_LEN 3
+
+struct hyperx_drvdata {
+ struct input_dev *input;
+ bool muted;
+ bool have_state;
+};
+
+static int hyperx_input_configured(struct hid_device *hdev,
+ struct hid_input *hi)
+{
+ struct hyperx_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ /*
+ * The device exposes several application collections. Prefer the
+ * telephony one, which already advertises KEY_MICMUTE, and fall back
+ * to the first collection otherwise.
+ */
+ if (!drvdata->input || test_bit(KEY_MICMUTE, hi->input->keybit)) {
+ drvdata->input = hi->input;
+ input_set_capability(drvdata->input, EV_KEY, KEY_MICMUTE);
+ }
+
+ return 0;
+}
+
+static int hyperx_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct hyperx_drvdata *drvdata = hid_get_drvdata(hdev);
+ bool muted;
+
+ if (!(hdev->claimed & HID_CLAIMED_INPUT) || !drvdata->input)
+ return 0;
+
+ if (size < HYPERX_QC2_MUTE_LEN || data[0] != HYPERX_QC2_REPORT_ID ||
+ data[1] != HYPERX_QC2_EVENT_MUTE)
+ return 0;
+
+ muted = data[2];
+ if (drvdata->have_state && muted == drvdata->muted)
+ return 0;
+
+ drvdata->muted = muted;
+ drvdata->have_state = true;
+
+ /*
+ * The device reports the resulting absolute state, while KEY_MICMUTE is
+ * a momentary key that userspace acts on as a toggle. Emit one
+ * keypress per state change.
+ */
+ input_report_key(drvdata->input, KEY_MICMUTE, 1);
+ input_sync(drvdata->input);
+ input_report_key(drvdata->input, KEY_MICMUTE, 0);
+ input_sync(drvdata->input);
+
+ return 0;
+}
+
+static int hyperx_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ struct hyperx_drvdata *drvdata;
+ int ret;
+
+ drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
+ if (!drvdata)
+ return -ENOMEM;
+
+ hid_set_drvdata(hdev, drvdata);
+
+ ret = hid_parse(hdev);
+ if (ret)
+ return ret;
+
+ return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+}
+
+static const struct hid_device_id hyperx_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_HP,
+ USB_PRODUCT_ID_HP_HYPERX_QUADCAST_2) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, hyperx_devices);
+
+static struct hid_driver hyperx_driver = {
+ .name = "hyperx",
+ .id_table = hyperx_devices,
+ .probe = hyperx_probe,
+ .input_configured = hyperx_input_configured,
+ .raw_event = hyperx_raw_event,
+};
+module_hid_driver(hyperx_driver);
+
+MODULE_DESCRIPTION("HID driver for the HyperX QuadCast 2 microphone");
+MODULE_AUTHOR("Benjamin Blume <benjaminblume@posteo.de>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 1059922baaac..341bf587863b 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -220,6 +220,7 @@
#define USB_DEVICE_ID_ASUSTEK_T101HA_KEYBOARD 0x183d
#define USB_DEVICE_ID_ASUSTEK_T304_KEYBOARD 0x184a
#define USB_DEVICE_ID_ASUSTEK_I2C_KEYBOARD 0x8585
+#define USB_DEVICE_ID_ASUSTEK_I2C_ZENBOOK_KEYBOARD 0x4b42
#define USB_DEVICE_ID_ASUSTEK_I2C_TOUCHPAD 0x0101
#define USB_DEVICE_ID_ASUSTEK_ROG_KEYBOARD1 0x1854
#define USB_DEVICE_ID_ASUSTEK_ROG_KEYBOARD2 0x1837
@@ -263,6 +264,9 @@
#define USB_VENDOR_ID_BAANTO 0x2453
#define USB_DEVICE_ID_BAANTO_MT_190W2 0x0100
+#define USB_VENDOR_ID_BEITONG 0x20dd
+#define USB_DEVICE_ID_BEITONG_KP20D 0x5159
+
#define USB_VENDOR_ID_BELKIN 0x050d
#define USB_DEVICE_ID_FLIP_KVM 0x3201
@@ -688,6 +692,7 @@
#define USB_DEVICE_ID_HORI_WIRELESS_SWITCH_PAD 0x00f6
#define USB_VENDOR_ID_HP 0x03f0
+#define USB_PRODUCT_ID_HP_HYPERX_QUADCAST_2 0x07b4
#define USB_PRODUCT_ID_HP_ELITE_PRESENTER_MOUSE_464A 0x464a
#define USB_PRODUCT_ID_HP_LOGITECH_OEM_USB_OPTICAL_MOUSE_0A4A 0x0a4a
#define USB_PRODUCT_ID_HP_LOGITECH_OEM_USB_OPTICAL_MOUSE_0B4A 0x0b4a
@@ -775,6 +780,7 @@
#define USB_DEVICE_ID_JESS_YUREX 0x1010
#define USB_DEVICE_ID_ASUS_MD_5112 0x5112
#define USB_DEVICE_ID_REDRAGON_ASURA 0x760b
+#define USB_DEVICE_ID_AULA_MINI_60_HE_PRO 0xfefe
#define USB_VENDOR_ID_JESS2 0x0f30
#define USB_DEVICE_ID_JESS2_COLOR_RUMBLE_PAD 0x0111
@@ -967,6 +973,7 @@
#define USB_DEVICE_ID_LOGITECH_NANO_RECEIVER_LIGHTSPEED_1_2 0xc543
#define USB_DEVICE_ID_LOGITECH_NANO_RECEIVER_LIGHTSPEED_1_3 0xc547
#define USB_DEVICE_ID_LOGITECH_NANO_RECEIVER_LIGHTSPEED_1_4 0xc54d
+#define USB_DEVICE_ID_LOGITECH_NANO_RECEIVER_LIGHTSPEED_1_5 0xc545
#define USB_DEVICE_ID_LOGITECH_NANO_RECEIVER_POWERPLAY 0xc53a
#define USB_DEVICE_ID_LOGITECH_BOLT_RECEIVER 0xc548
#define USB_DEVICE_ID_SPACETRAVELLER 0xc623
@@ -1071,7 +1078,12 @@
#define USB_DEVICE_ID_MOZA_R16_R21_2 0x0010
#define USB_VENDOR_ID_MSI 0x1770
+#define USB_VENDOR_ID_MSI_2 0x0db0
#define USB_DEVICE_ID_MSI_GT683R_LED_PANEL 0xff00
+#define USB_DEVICE_ID_MSI_CLAW_XINPUT 0x1901
+#define USB_DEVICE_ID_MSI_CLAW_DINPUT 0x1902
+#define USB_DEVICE_ID_MSI_CLAW_DESKTOP 0x1903
+#define USB_DEVICE_ID_MSI_CLAW_BIOS 0x1904
#define USB_VENDOR_ID_NATIONAL_SEMICONDUCTOR 0x0400
#define USB_DEVICE_ID_N_S_HARMONY 0xc359
@@ -1374,11 +1386,31 @@
#define USB_DEVICE_ID_STEAM_CONTROLLER 0x1102
#define USB_DEVICE_ID_STEAM_CONTROLLER_WIRELESS 0x1142
#define USB_DEVICE_ID_STEAM_DECK 0x1205
+#define USB_DEVICE_ID_STEAM_CONTROLLER_IBEX 0x1302
+#define USB_DEVICE_ID_STEAM_CONTROLLER_IBEX_BLE 0x1303
+#define USB_DEVICE_ID_STEAM_CONTROLLER_PROTEUS 0x1304
+#define USB_DEVICE_ID_STEAM_CONTROLLER_NEREID 0x1305
#define USB_VENDOR_ID_STEELSERIES 0x1038
#define USB_DEVICE_ID_STEELSERIES_SRWS1 0x1410
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1 0x12b6
-#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9 0x12c2
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X 0x12b6
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_9 0x12c2
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X 0x2253
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7 0x2202
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X 0x2206
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2 0x22a4
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO 0x223a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW 0x227a
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2026 0x22a1
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026 0x22a7
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026 0x22a5
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026 0x22a9
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2 0x227e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2 0x2258
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2 0x229e
+#define USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_3 0x22ad
+#define USB_DEVICE_ID_STEELSERIES_MSI_KLC 0x1122
+#define USB_DEVICE_ID_STEELSERIES_MSI_ALC 0x1161
#define USB_VENDOR_ID_SUN 0x0430
#define USB_DEVICE_ID_RARITAN_KVM_DONGLE 0xcdab
diff --git a/drivers/hid/hid-input.c b/drivers/hid/hid-input.c
index 3487600cadb4..edd6bbf8342b 100644
--- a/drivers/hid/hid-input.c
+++ b/drivers/hid/hid-input.c
@@ -375,6 +375,9 @@ static const struct hid_device_id hid_battery_quirks[] = {
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_APPLE,
USB_DEVICE_ID_APPLE_MAGICTRACKPAD),
HID_BATTERY_QUIRK_IGNORE },
+ { HID_BLUETOOTH_DEVICE(BT_VENDOR_ID_APPLE,
+ USB_DEVICE_ID_APPLE_MAGICTRACKPAD2_USBC),
+ HID_BATTERY_QUIRK_AVOID_QUERY },
{ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_ELECOM,
USB_DEVICE_ID_ELECOM_BM084),
HID_BATTERY_QUIRK_IGNORE },
@@ -432,17 +435,25 @@ static int hidinput_scale_battery_capacity(struct hid_battery *bat,
static int hidinput_query_battery_capacity(struct hid_battery *bat)
{
int ret;
+ /*
+ * The capacity field may not be the first field in the report: some
+ * devices (e.g. the Apple Magic Trackpad 2 over Bluetooth) precede it
+ * with status flags. Read it from its actual byte offset in the report
+ * (report_offset is in bits; the leading byte is the report id).
+ */
+ int offset = 1 + bat->report_offset / 8;
+ int len = offset + 1;
- u8 *buf __free(kfree) = kmalloc(4, GFP_KERNEL);
+ u8 *buf __free(kfree) = kmalloc(max(len, 4), GFP_KERNEL);
if (!buf)
return -ENOMEM;
- ret = hid_hw_raw_request(bat->dev, bat->report_id, buf, 4,
+ ret = hid_hw_raw_request(bat->dev, bat->report_id, buf, max(len, 4),
bat->report_type, HID_REQ_GET_REPORT);
- if (ret < 2)
+ if (ret < len)
return -ENODATA;
- return hidinput_scale_battery_capacity(bat, buf[1]);
+ return hidinput_scale_battery_capacity(bat, buf[offset]);
}
static int hidinput_get_battery_property(struct power_supply *psy,
@@ -593,6 +604,7 @@ static int hidinput_setup_battery(struct hid_device *dev, unsigned report_type,
bat->max = max;
bat->report_type = report_type;
bat->report_id = field->report->id;
+ bat->report_offset = field->report_offset;
bat->charge_status = POWER_SUPPLY_STATUS_DISCHARGING;
bat->status = HID_BATTERY_UNKNOWN;
@@ -2317,7 +2329,19 @@ static inline void hidinput_configure_usages(struct hid_input *hidinput,
* Read all reports and initialize the absolute field values.
*/
-int hidinput_connect(struct hid_device *hid, unsigned int force)
+static bool hid_has_ff_input(struct hid_device *hdev)
+{
+ struct hid_input *hidinput;
+
+ list_for_each_entry(hidinput, &hdev->inputs, list) {
+ if (test_bit(EV_FF, hidinput->input->evbit))
+ return true;
+ }
+
+ return false;
+}
+
+int hidinput_connect(struct hid_device *hid, unsigned int connect_mask)
{
struct hid_driver *drv = hid->driver;
struct hid_report *report;
@@ -2330,7 +2354,7 @@ int hidinput_connect(struct hid_device *hid, unsigned int force)
hid->status &= ~HID_STAT_DUP_DETECTED;
- if (!force) {
+ if (!(connect_mask & HID_CONNECT_HIDINPUT_FORCE)) {
for (i = 0; i < hid->maxcollection; i++) {
struct hid_collection *col = &hid->collection[i];
if (col->type == HID_COLLECTION_APPLICATION ||
@@ -2396,6 +2420,11 @@ int hidinput_connect(struct hid_device *hid, unsigned int force)
continue;
}
+ if (list_is_first(&hidinput->list, &hid->inputs) &&
+ (connect_mask & HID_CONNECT_FF) && hid->ff_init &&
+ !hid_has_ff_input(hid))
+ hid->ff_init(hid);
+
if (input_register_device(hidinput->input))
goto out_unwind;
hidinput->registered = true;
diff --git a/drivers/hid/hid-lg4ff.c b/drivers/hid/hid-lg4ff.c
index e901fdb7d033..62f99a93210c 100644
--- a/drivers/hid/hid-lg4ff.c
+++ b/drivers/hid/hid-lg4ff.c
@@ -336,6 +336,8 @@ int lg4ff_raw_event(struct hid_device *hdev, struct hid_report *report,
if (entry->wdata.combine) {
switch (entry->wdata.product_id) {
case USB_DEVICE_ID_LOGITECH_WHEEL:
+ if (size < 7)
+ return 0;
rd[5] = rd[3];
rd[6] = 0x7F;
return 1;
@@ -343,10 +345,14 @@ int lg4ff_raw_event(struct hid_device *hdev, struct hid_report *report,
case USB_DEVICE_ID_LOGITECH_WINGMAN_FFG:
case USB_DEVICE_ID_LOGITECH_MOMO_WHEEL:
case USB_DEVICE_ID_LOGITECH_MOMO_WHEEL2:
+ if (size < 6)
+ return 0;
rd[4] = rd[3];
rd[5] = 0x7F;
return 1;
case USB_DEVICE_ID_LOGITECH_DFP_WHEEL:
+ if (size < 7)
+ return 0;
rd[5] = rd[4];
rd[6] = 0x7F;
return 1;
@@ -366,6 +372,8 @@ int lg4ff_raw_event(struct hid_device *hdev, struct hid_report *report,
}
/* Compute a combined axis when wheel does not supply it */
+ if (size <= offset + 1)
+ return 0;
rd[offset] = (0xFF + rd[offset] - rd[offset+1]) >> 1;
rd[offset+1] = 0x7F;
return 1;
diff --git a/drivers/hid/hid-logitech-dj.c b/drivers/hid/hid-logitech-dj.c
index 9c574ab8b60b..1d619d2345e1 100644
--- a/drivers/hid/hid-logitech-dj.c
+++ b/drivers/hid/hid-logitech-dj.c
@@ -121,6 +121,7 @@ enum recvr_type {
recvr_type_27mhz,
recvr_type_bluetooth,
recvr_type_dinovo,
+ recvr_type_bolt,
};
struct dj_report {
@@ -1156,6 +1157,10 @@ static void logi_hidpp_recv_queue_notif(struct hid_device *hdev,
logi_hidpp_dev_conn_notif_equad(hdev, hidpp_report, &workitem);
workitem.reports_supported |= STD_KEYBOARD;
break;
+ case 0x10:
+ device_type = "Bolt";
+ logi_hidpp_dev_conn_notif_equad(hdev, hidpp_report, &workitem);
+ break;
}
/* custom receiver device (eg. powerplay) */
@@ -1746,6 +1751,24 @@ static int logi_dj_hidpp_event(struct hid_device *hdev,
dj_dev = djrcv_dev->paired_dj_devices[device_index];
/*
+ * Bolt receivers send explicit unpair notifications as HID++ events;
+ * queue device removal when we receive one.
+ */
+ if (djrcv_dev->type == recvr_type_bolt &&
+ hidpp_report->report_id == REPORT_ID_HIDPP_SHORT &&
+ hidpp_report->sub_id == REPORT_TYPE_NOTIF_DEVICE_UNPAIRED) {
+ struct dj_workitem workitem = {
+ .device_index = device_index,
+ .type = WORKITEM_TYPE_UNPAIRED,
+ };
+
+ kfifo_in(&djrcv_dev->notif_fifo, &workitem, sizeof(workitem));
+ schedule_work(&djrcv_dev->work);
+ spin_unlock_irqrestore(&djrcv_dev->lock, flags);
+ return false;
+ }
+
+ /*
* With 27 MHz receivers, we do not get an explicit unpair event,
* remove the old device if the user has paired a *different* device.
*/
@@ -1884,6 +1907,9 @@ static int logi_dj_probe(struct hid_device *hdev,
* treat these as logitech-dj interfaces then this causes input events
* reported through this extra interface to not be reported correctly.
* To avoid this, we treat these as generic-hid devices.
+ *
+ * Bolt receivers only use LOGITECH_DJ_INTERFACE_NUMBER for receiver
+ * reporting. Treat all other Bolt interfaces as generic-hid devices.
*/
switch (id->driver_data) {
case recvr_type_dj: no_dj_interfaces = 3; break;
@@ -1897,10 +1923,20 @@ static int logi_dj_probe(struct hid_device *hdev,
}
if (hid_is_usb(hdev)) {
intf = to_usb_interface(hdev->dev.parent);
- if (intf && intf->altsetting->desc.bInterfaceNumber >=
- no_dj_interfaces) {
- hdev->quirks |= HID_QUIRK_INPUT_PER_APP;
- return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (intf) {
+ bool generic_hid_interface;
+
+ if (id->driver_data == recvr_type_bolt)
+ generic_hid_interface =
+ intf->altsetting->desc.bInterfaceNumber !=
+ LOGITECH_DJ_INTERFACE_NUMBER;
+ else
+ generic_hid_interface =
+ intf->altsetting->desc.bInterfaceNumber >= no_dj_interfaces;
+ if (generic_hid_interface) {
+ hdev->quirks |= HID_QUIRK_INPUT_PER_APP;
+ return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ }
}
}
@@ -2103,10 +2139,18 @@ static const struct hid_device_id logi_dj_receivers[] = {
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_NANO_RECEIVER_LIGHTSPEED_1_3),
.driver_data = recvr_type_gaming_hidpp_ls_1_3},
+ { /* Logitech Bolt receiver (0xc548) */
+ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
+ USB_DEVICE_ID_LOGITECH_BOLT_RECEIVER),
+ .driver_data = recvr_type_bolt},
{ /* Logitech lightspeed receiver (0xc54d) */
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
USB_DEVICE_ID_LOGITECH_NANO_RECEIVER_LIGHTSPEED_1_4),
.driver_data = recvr_type_gaming_hidpp_ls_1_3},
+ { /* Logitech lightspeed receiver (0xc545) */
+ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
+ USB_DEVICE_ID_LOGITECH_NANO_RECEIVER_LIGHTSPEED_1_5),
+ .driver_data = recvr_type_gaming_hidpp_ls_1_3},
{ /* Logitech 27 MHz HID++ 1.0 receiver (0xc513) */
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, USB_DEVICE_ID_MX3000_RECEIVER),
diff --git a/drivers/hid/hid-logitech-hidpp.c b/drivers/hid/hid-logitech-hidpp.c
index 90b0184df777..1504de32b1c8 100644
--- a/drivers/hid/hid-logitech-hidpp.c
+++ b/drivers/hid/hid-logitech-hidpp.c
@@ -76,6 +76,7 @@ MODULE_PARM_DESC(disable_tap_to_click,
#define HIDPP_QUIRK_HI_RES_SCROLL_1P0 BIT(28)
#define HIDPP_QUIRK_WIRELESS_STATUS BIT(29)
#define HIDPP_QUIRK_RESET_HI_RES_SCROLL BIT(30)
+#define HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS BIT(31)
/* These are just aliases for now */
#define HIDPP_QUIRK_KBD_SCROLL_WHEEL HIDPP_QUIRK_HIDPP_WHEELS
@@ -178,6 +179,8 @@ struct hidpp_scroll_counter {
unsigned long long last_time;
};
+struct hidpp_reprog_control_mapping;
+
struct hidpp_device {
struct hid_device *hid_dev;
struct input_dev *input;
@@ -205,6 +208,8 @@ struct hidpp_device {
struct hidpp_scroll_counter vertical_wheel_counter;
u8 wireless_feature_index;
+ u8 reprog_controls_feature_index;
+ const struct hidpp_reprog_control_mapping *reprog_controls;
int hires_wheel_multiplier;
u8 hires_wheel_feature_index;
@@ -983,7 +988,8 @@ static int hidpp_root_get_protocol_version(struct hidpp_device *hidpp)
}
/* the device might not be connected */
- if (ret == HIDPP_ERROR_RESOURCE_ERROR ||
+ if (ret == HIDPP_ERROR_CONNECT_FAIL ||
+ ret == HIDPP_ERROR_RESOURCE_ERROR ||
ret == HIDPP_ERROR_UNKNOWN_DEVICE)
return -EIO;
@@ -2861,18 +2867,19 @@ static int hidpp_ff_init(struct hidpp_device *hidpp,
* ownership to FF core
*/
data = kmemdup(data, sizeof(*data), GFP_KERNEL);
- if (!data)
- return -ENOMEM;
+ if (!data) {
+ error = -ENOMEM;
+ goto err_destroy_ff;
+ }
data->effect_ids = kzalloc_objs(int, num_slots);
if (!data->effect_ids) {
- kfree(data);
- return -ENOMEM;
+ error = -ENOMEM;
+ goto err_free_data;
}
data->wq = create_singlethread_workqueue("hidpp-ff-sendqueue");
if (!data->wq) {
- kfree(data->effect_ids);
- kfree(data);
- return -ENOMEM;
+ error = -ENOMEM;
+ goto err_free_effect_ids;
}
data->hidpp = hidpp;
@@ -2902,6 +2909,14 @@ static int hidpp_ff_init(struct hidpp_device *hidpp,
version);
return 0;
+
+err_free_effect_ids:
+ kfree(data->effect_ids);
+err_free_data:
+ kfree(data);
+err_destroy_ff:
+ input_ff_destroy(dev);
+ return error;
}
/* ************************************************************************** */
@@ -3601,6 +3616,211 @@ static int hidpp10_extra_mouse_buttons_raw_event(struct hidpp_device *hidpp,
return 1;
}
+/* -------------------------------------------------------------------------- */
+/* HID++2.0 reprogrammable controls */
+/* -------------------------------------------------------------------------- */
+
+#define HIDPP_PAGE_REPROG_CONTROLS_V4 0x1b04
+
+#define HIDPP_REPROG_CONTROLS_GET_COUNT 0x00
+#define HIDPP_REPROG_CONTROLS_GET_CID_INFO 0x10
+#define HIDPP_REPROG_CONTROLS_SET_CONTROL_REPORTING 0x30
+
+#define HIDPP_REPROG_CONTROLS_FLAG_MOUSE BIT(0)
+#define HIDPP_REPROG_CONTROLS_FLAG_DIVERT BIT(5)
+
+#define HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED BIT(0)
+#define HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT BIT(1)
+
+#define HIDPP_REPROG_CONTROLS_EVENT_DIVERTED 0x00
+
+#define HIDPP_REPROG_CONTROL_BACK 0x0053
+#define HIDPP_REPROG_CONTROL_FORWARD 0x0056
+
+#define HIDPP_PRODUCT_SIGNATURE_M650 0xb02a
+
+struct hidpp_reprog_control_mapping {
+ u16 control;
+ u16 code;
+};
+
+static const struct hidpp_reprog_control_mapping m650_reprog_control_mappings[] = {
+ { HIDPP_REPROG_CONTROL_BACK, BTN_BACK },
+ { HIDPP_REPROG_CONTROL_FORWARD, BTN_FORWARD },
+ { }
+};
+
+static const struct hidpp_reprog_control_mapping *
+hidpp20_reprog_controls_get_mappings(struct hidpp_device *hidpp)
+{
+ switch (hidpp->hid_dev->product) {
+ case HIDPP_PRODUCT_SIGNATURE_M650:
+ return m650_reprog_control_mappings;
+ }
+
+ return NULL;
+}
+
+static int hidpp20_reprog_controls_get_count(struct hidpp_device *hidpp)
+{
+ struct hidpp_report response;
+ u8 feature_index = hidpp->reprog_controls_feature_index;
+ u8 cmd = HIDPP_REPROG_CONTROLS_GET_COUNT;
+ int ret;
+
+ ret = hidpp_send_fap_command_sync(hidpp, feature_index, cmd, NULL, 0,
+ &response);
+ if (ret > 0)
+ return -EPROTO;
+ if (ret)
+ return ret;
+
+ return response.fap.params[0];
+}
+
+static int hidpp20_reprog_controls_get_cid_info(struct hidpp_device *hidpp,
+ u8 index, u16 *control,
+ u8 *flags)
+{
+ struct hidpp_report response;
+ u8 feature_index = hidpp->reprog_controls_feature_index;
+ u8 cmd = HIDPP_REPROG_CONTROLS_GET_CID_INFO;
+ int ret;
+
+ ret = hidpp_send_fap_command_sync(hidpp, feature_index, cmd, &index,
+ sizeof(index), &response);
+ if (ret > 0)
+ return -EPROTO;
+ if (ret)
+ return ret;
+
+ *control = get_unaligned_be16(&response.fap.params[0]);
+ *flags = response.fap.params[4];
+
+ return 0;
+}
+
+static bool hidpp20_reprog_controls_find_control(struct hidpp_device *hidpp,
+ u16 control)
+{
+ int count, ret;
+ u16 cid;
+ u8 flags;
+ int i;
+
+ count = hidpp20_reprog_controls_get_count(hidpp);
+ if (count < 0)
+ return false;
+
+ for (i = 0; i < count; i++) {
+ ret = hidpp20_reprog_controls_get_cid_info(hidpp, i, &cid,
+ &flags);
+ if (ret)
+ return false;
+
+ if (cid == control)
+ return (flags & HIDPP_REPROG_CONTROLS_FLAG_MOUSE) &&
+ (flags & HIDPP_REPROG_CONTROLS_FLAG_DIVERT);
+ }
+
+ return false;
+}
+
+static int hidpp20_reprog_controls_set_control_reporting(struct hidpp_device *hidpp,
+ u16 control, u8 flags)
+{
+ struct hidpp_report response;
+ u8 params[5];
+
+ put_unaligned_be16(control, &params[0]);
+ params[2] = flags;
+ put_unaligned_be16(control, &params[3]);
+
+ return hidpp_send_fap_command_sync(hidpp,
+ hidpp->reprog_controls_feature_index,
+ HIDPP_REPROG_CONTROLS_SET_CONTROL_REPORTING,
+ params, sizeof(params), &response);
+}
+
+static void hidpp20_reprog_controls_connect(struct hidpp_device *hidpp)
+{
+ const struct hidpp_reprog_control_mapping *mapping;
+ u8 flags = HIDPP_REPROG_CONTROLS_TEMPORARY_DIVERTED |
+ HIDPP_REPROG_CONTROLS_CHANGE_TEMPORARY_DIVERT;
+
+ if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS))
+ return;
+
+ if (!hidpp->reprog_controls)
+ return;
+
+ if (hidpp_root_get_feature(hidpp, HIDPP_PAGE_REPROG_CONTROLS_V4,
+ &hidpp->reprog_controls_feature_index))
+ return;
+
+ for (mapping = hidpp->reprog_controls; mapping->control; mapping++) {
+ if (!hidpp20_reprog_controls_find_control(hidpp, mapping->control))
+ continue;
+
+ hidpp20_reprog_controls_set_control_reporting(hidpp,
+ mapping->control,
+ flags);
+ }
+}
+
+static int hidpp20_reprog_controls_raw_event(struct hidpp_device *hidpp,
+ u8 *data, int size)
+{
+ const struct hidpp_reprog_control_mapping *mapping;
+ struct hidpp_report *report = (struct hidpp_report *)data;
+ u16 controls[4];
+ bool pressed;
+ unsigned int i, j;
+
+ if (!(hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS) ||
+ !hidpp->input ||
+ !hidpp->reprog_controls ||
+ hidpp->reprog_controls_feature_index == 0xff)
+ return 0;
+
+ if (size < HIDPP_REPORT_LONG_LENGTH ||
+ report->fap.feature_index != hidpp->reprog_controls_feature_index ||
+ report->fap.funcindex_clientid != HIDPP_REPROG_CONTROLS_EVENT_DIVERTED)
+ return 0;
+
+ for (i = 0; i < ARRAY_SIZE(controls); i++)
+ controls[i] = get_unaligned_be16(&report->fap.params[i * 2]);
+
+ for (mapping = hidpp->reprog_controls; mapping->control; mapping++) {
+ pressed = false;
+
+ for (j = 0; j < ARRAY_SIZE(controls); j++) {
+ if (controls[j] == mapping->control) {
+ pressed = true;
+ break;
+ }
+ }
+
+ input_report_key(hidpp->input, mapping->code, pressed);
+ }
+
+ input_sync(hidpp->input);
+
+ return 1;
+}
+
+static void hidpp20_reprog_controls_populate_input(struct hidpp_device *hidpp,
+ struct input_dev *input_dev)
+{
+ const struct hidpp_reprog_control_mapping *mapping;
+
+ if (!hidpp->reprog_controls)
+ return;
+
+ for (mapping = hidpp->reprog_controls; mapping->control; mapping++)
+ input_set_capability(input_dev, EV_KEY, mapping->code);
+}
+
static void hidpp10_extra_mouse_buttons_populate_input(
struct hidpp_device *hidpp, struct input_dev *input_dev)
{
@@ -3859,17 +4079,37 @@ static void hidpp_populate_input(struct hidpp_device *hidpp,
if (hidpp->quirks & HIDPP_QUIRK_HIDPP_EXTRA_MOUSE_BTNS)
hidpp10_extra_mouse_buttons_populate_input(hidpp, input);
+
+ if (hidpp->quirks & HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS)
+ hidpp20_reprog_controls_populate_input(hidpp, input);
}
-static int hidpp_input_configured(struct hid_device *hdev,
- struct hid_input *hidinput)
+static int hidpp_input_configured(struct hid_device *hdev, struct hid_input *hidinput)
{
struct hidpp_device *hidpp = hid_get_drvdata(hdev);
struct input_dev *input = hidinput->input;
+ int ret;
if (!hidpp)
return 0;
+ if (hidpp->quirks & HIDPP_QUIRK_CLASS_G920) {
+ struct hidpp_ff_private_data data;
+
+ if (!list_is_first(&hidinput->list, &hdev->inputs))
+ return 0;
+
+ ret = g920_get_config(hidpp, &data);
+ if (!ret)
+ ret = hidpp_ff_init(hidpp, &data);
+
+ if (ret) {
+ hid_warn(hidpp->hid_dev,
+ "Unable to initialize force feedback support, errno %d\n",
+ ret);
+ }
+ }
+
hidpp_populate_input(hidpp, input);
return 0;
@@ -3971,6 +4211,10 @@ static int hidpp_raw_hidpp_event(struct hidpp_device *hidpp, u8 *data,
return ret;
}
+ ret = hidpp20_reprog_controls_raw_event(hidpp, data, size);
+ if (ret != 0)
+ return ret;
+
if (hidpp->quirks & HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS) {
ret = hidpp10_consumer_keys_raw_event(hidpp, data, size);
if (ret != 0)
@@ -4161,8 +4405,50 @@ static int hidpp_initialize_battery(struct hidpp_device *hidpp)
return ret;
}
+static bool hidpp_is_bolt_child(struct hid_device *hdev)
+{
+ struct device *parent = hdev->dev.parent;
+ struct hid_device *receiver_hdev;
+
+ if (!parent)
+ return false;
+
+ receiver_hdev = to_hid_device(parent);
+ return receiver_hdev->vendor == USB_VENDOR_ID_LOGITECH &&
+ receiver_hdev->product == USB_DEVICE_ID_LOGITECH_BOLT_RECEIVER;
+}
+
+static int hidpp_bolt_init(struct hidpp_device *hidpp)
+{
+ struct hid_device *hdev = hidpp->hid_dev;
+ char *name;
+ int ret;
+
+ ret = hidpp_serial_init(hidpp);
+ if (ret)
+ return ret;
+
+ name = hidpp_get_device_name(hidpp);
+ if (!name)
+ return -EIO;
+
+ snprintf(hdev->name, sizeof(hdev->name), "%s", name);
+ dbg_hid("HID++ Bolt: Got name: %s\n", name);
+
+ kfree(name);
+ return 0;
+}
+
+static int hidpp_receiver_init(struct hidpp_device *hidpp)
+{
+ if (hidpp_is_bolt_child(hidpp->hid_dev))
+ return hidpp_bolt_init(hidpp);
+
+ return hidpp_unifying_init(hidpp);
+}
+
/* Get name + serial for USB and Bluetooth HID++ devices */
-static void hidpp_non_unifying_init(struct hidpp_device *hidpp)
+static void hidpp_non_receiver_init(struct hidpp_device *hidpp)
{
struct hid_device *hdev = hidpp->hid_dev;
char *name;
@@ -4264,6 +4550,8 @@ static void hidpp_connect_event(struct work_struct *work)
return;
}
+ hidpp20_reprog_controls_connect(hidpp);
+
if (hidpp->quirks & HIDPP_QUIRK_HIDPP_CONSUMER_VENDOR_KEYS) {
ret = hidpp10_consumer_keys_connect(hidpp);
if (ret)
@@ -4437,6 +4725,8 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
hidpp->hid_dev = hdev;
hidpp->name = hdev->name;
hidpp->quirks = id->driver_data;
+ hidpp->reprog_controls_feature_index = 0xff;
+ hidpp->reprog_controls = hidpp20_reprog_controls_get_mappings(hidpp);
hid_set_drvdata(hdev, hidpp);
ret = hid_parse(hdev);
@@ -4510,9 +4800,9 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
/* Get name + serial, store in hdev->name + hdev->uniq */
if (id->group == HID_GROUP_LOGITECH_DJ_DEVICE)
- hidpp_unifying_init(hidpp);
+ hidpp_receiver_init(hidpp);
else
- hidpp_non_unifying_init(hidpp);
+ hidpp_non_receiver_init(hidpp);
if (hidpp->quirks & HIDPP_QUIRK_DELAYED_INIT)
connect_mask &= ~HID_CONNECT_HIDINPUT;
@@ -4530,21 +4820,6 @@ static int hidpp_probe(struct hid_device *hdev, const struct hid_device_id *id)
schedule_work(&hidpp->work);
flush_work(&hidpp->work);
- if (hidpp->quirks & HIDPP_QUIRK_CLASS_G920) {
- struct hidpp_ff_private_data data;
-
- ret = g920_get_config(hidpp, &data);
- if (!ret)
- ret = hidpp_ff_init(hidpp, &data);
-
- if (ret) {
- hid_warn(hidpp->hid_dev,
- "Unable to initialize force feedback support, errno %d\n",
- ret);
- ret = 0;
- }
- }
-
/*
* This relies on logi_dj_ll_close() being a no-op so that DJ connection
* events will still be received.
@@ -4647,6 +4922,8 @@ static const struct hid_device_id hidpp_devices[] = {
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0xC082) },
{ /* Logitech G502 Lightspeed Wireless Gaming Mouse over USB */
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0xC08D) },
+ { /* Logitech G502 X Plus Wireless Gaming Mouse over USB */
+ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0xC095) },
{ /* Logitech G703 Gaming Mouse over USB */
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0xC087) },
{ /* Logitech G703 Hero Gaming Mouse over USB */
@@ -4659,8 +4936,6 @@ static const struct hid_device_id hidpp_devices[] = {
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0xC088) },
{ /* MX Vertical over USB */
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0xC08A) },
- { /* Logitech G703 Hero Gaming Mouse over USB */
- HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0xC090) },
{ /* Logitech G903 Hero Gaming Mouse over USB */
HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH, 0xC091) },
{ /* Logitech G915 TKL Keyboard over USB */
@@ -4707,7 +4982,9 @@ static const struct hid_device_id hidpp_devices[] = {
{ /* MX Vertical mouse over Bluetooth */
HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb020) },
{ /* Signature M650 over Bluetooth */
- HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb02a) },
+ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH,
+ HIDPP_PRODUCT_SIGNATURE_M650),
+ .driver_data = HIDPP_QUIRK_HIDPP_REPROG_CONTROLS_BTNS },
{ /* MX Master 3 mouse over Bluetooth */
HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_LOGITECH, 0xb023) },
{ /* MX Anywhere 3 mouse over Bluetooth */
diff --git a/drivers/hid/hid-magicmouse.c b/drivers/hid/hid-magicmouse.c
index 802a3479e24b..d637c0477379 100644
--- a/drivers/hid/hid-magicmouse.c
+++ b/drivers/hid/hid-magicmouse.c
@@ -383,8 +383,8 @@ static void magicmouse_emit_touch(struct magicmouse_sc *msc, int raw_id, u8 *tda
}
}
-static int magicmouse_raw_event(struct hid_device *hdev,
- struct hid_report *report, u8 *data, int size)
+static int __magicmouse_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size, bool nested)
{
struct magicmouse_sc *msc = hid_get_drvdata(hdev);
struct input_dev *input = msc->input;
@@ -495,6 +495,15 @@ static int magicmouse_raw_event(struct hid_device *hdev,
* packet.
*/
+ /*
+ * A double report only ever wraps two normal reports, so it is
+ * never nested. Refuse to recurse a second time; otherwise a
+ * malicious device could chain DOUBLE_REPORT_ID packets to drive
+ * unbounded recursion and overflow the kernel stack.
+ */
+ if (nested)
+ return 0;
+
/* Ensure that we have at least 2 elements (report type and size) */
if (size < 2)
return 0;
@@ -506,9 +515,9 @@ static int magicmouse_raw_event(struct hid_device *hdev,
return 0;
}
- magicmouse_raw_event(hdev, report, data + 2, data[1]);
- magicmouse_raw_event(hdev, report, data + 2 + data[1],
- size - 2 - data[1]);
+ __magicmouse_raw_event(hdev, report, data + 2, data[1], true);
+ __magicmouse_raw_event(hdev, report, data + 2 + data[1],
+ size - 2 - data[1], true);
return 0;
default:
return 0;
@@ -534,6 +543,12 @@ static int magicmouse_raw_event(struct hid_device *hdev,
return 1;
}
+static int magicmouse_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ return __magicmouse_raw_event(hdev, report, data, size, false);
+}
+
static int magicmouse_event(struct hid_device *hdev, struct hid_field *field,
struct hid_usage *usage, __s32 value)
{
@@ -828,6 +843,12 @@ static bool is_usb_magictrackpad2(__u32 vendor, __u32 product)
product == USB_DEVICE_ID_APPLE_MAGICTRACKPAD2_USBC;
}
+static bool is_bt_magictrackpad2(__u32 vendor, __u32 product)
+{
+ return vendor == BT_VENDOR_ID_APPLE &&
+ product == USB_DEVICE_ID_APPLE_MAGICTRACKPAD2_USBC;
+}
+
static int magicmouse_fetch_battery(struct hid_device *hdev)
{
#ifdef CONFIG_HID_BATTERY_STRENGTH
@@ -838,7 +859,8 @@ static int magicmouse_fetch_battery(struct hid_device *hdev)
bat = hid_get_battery(hdev);
if (!bat ||
(!is_usb_magicmouse2(hdev->vendor, hdev->product) &&
- !is_usb_magictrackpad2(hdev->vendor, hdev->product)))
+ !is_usb_magictrackpad2(hdev->vendor, hdev->product) &&
+ !is_bt_magictrackpad2(hdev->vendor, hdev->product)))
return -1;
report_enum = &hdev->report_enum[bat->report_type];
@@ -900,6 +922,16 @@ static int magicmouse_probe(struct hid_device *hdev,
return ret;
}
+ /*
+ * When hidinput_connect() fails it frees every input device it
+ * created, but that does not fail hid_hw_start(): the core simply
+ * does not claim an input. msc->input, cached in ->input_mapping
+ * while the report descriptor was parsed, would then be a dangling
+ * pointer that passes every NULL check. Trust the core's claim.
+ */
+ if (!(hdev->claimed & HID_CLAIMED_INPUT))
+ msc->input = NULL;
+
if (is_usb_magicmouse2(id->vendor, id->product) ||
is_usb_magictrackpad2(id->vendor, id->product)) {
timer_setup(&msc->battery_timer, magicmouse_battery_timer_tick, 0);
@@ -971,6 +1003,16 @@ static int magicmouse_probe(struct hid_device *hdev,
schedule_delayed_work(&msc->work, msecs_to_jiffies(500));
}
+ /*
+ * Query the Bluetooth Magic Trackpad USB-C battery as done for USB.
+ * Start io first: probe holds driver_input_lock and the synchronous
+ * GET_REPORT reply would otherwise be dropped.
+ */
+ if (is_bt_magictrackpad2(id->vendor, id->product)) {
+ hid_device_io_start(hdev);
+ magicmouse_fetch_battery(hdev);
+ }
+
return 0;
err_stop_hw:
if (is_usb_magicmouse2(id->vendor, id->product) ||
@@ -995,6 +1037,22 @@ static void magicmouse_remove(struct hid_device *hdev)
hid_hw_stop(hdev);
}
+#ifdef CONFIG_PM
+static int magicmouse_reset_resume(struct hid_device *hdev)
+{
+ struct magicmouse_sc *msc = hid_get_drvdata(hdev);
+
+ /* The device drops out of multitouch mode on resume; re-send the
+ * enable report. Only the HID_TYPE_USBMOUSE interface accepts it, and
+ * it must be deferred. Sending it inline here is too early.
+ */
+ if (msc && hdev->type == HID_TYPE_USBMOUSE)
+ schedule_delayed_work(&msc->work, msecs_to_jiffies(500));
+
+ return 0;
+}
+#endif
+
static const __u8 *magicmouse_report_fixup(struct hid_device *hdev, __u8 *rdesc,
unsigned int *rsize)
{
@@ -1058,6 +1116,9 @@ static struct hid_driver magicmouse_driver = {
.event = magicmouse_event,
.input_mapping = magicmouse_input_mapping,
.input_configured = magicmouse_input_configured,
+#ifdef CONFIG_PM
+ .reset_resume = magicmouse_reset_resume,
+#endif
};
module_hid_driver(magicmouse_driver);
diff --git a/drivers/hid/hid-mcp2221.c b/drivers/hid/hid-mcp2221.c
index e4ddd8e9293b..d52ce3531ab7 100644
--- a/drivers/hid/hid-mcp2221.c
+++ b/drivers/hid/hid-mcp2221.c
@@ -343,7 +343,7 @@ static int mcp_i2c_smbus_read(struct mcp2221 *mcp,
ret = mcp_send_data_req_status(mcp, mcp->txbuf, 4);
if (ret)
- return ret;
+ goto out;
mcp->rxbuf_idx = 0;
@@ -365,7 +365,7 @@ static int mcp_i2c_smbus_read(struct mcp2221 *mcp,
} else {
usleep_range(980, 1000);
mcp_cancel_last_cmd(mcp);
- return ret;
+ goto out;
}
} else {
retries = 0;
@@ -375,6 +375,10 @@ static int mcp_i2c_smbus_read(struct mcp2221 *mcp,
usleep_range(980, 1000);
ret = mcp_chk_last_cmd_status_free_bus(mcp);
+out:
+ mcp->rxbuf = NULL;
+ mcp->rxbuf_size = 0;
+
return ret;
}
@@ -861,6 +865,9 @@ static int mcp2221_raw_event(struct hid_device *hdev,
u8 *buf;
struct mcp2221 *mcp = hid_get_drvdata(hdev);
+ if (size < 4)
+ return 0;
+
switch (data[0]) {
case MCP2221_I2C_WR_DATA:
@@ -926,6 +933,10 @@ static int mcp2221_raw_event(struct hid_device *hdev,
mcp->status = -EINVAL;
break;
}
+ if (4 + data[3] > size) {
+ mcp->status = -EINVAL;
+ break;
+ }
buf = mcp->rxbuf;
memcpy(&buf[mcp->rxbuf_idx], &data[4], data[3]);
mcp->rxbuf_idx = mcp->rxbuf_idx + data[3];
@@ -1049,6 +1060,8 @@ static void mcp2221_hid_unregister(void *ptr)
{
struct hid_device *hdev = ptr;
+ if (hdev->io_started)
+ hid_device_io_stop(hdev);
hid_hw_close(hdev);
hid_hw_stop(hdev);
}
diff --git a/drivers/hid/hid-megaworld.c b/drivers/hid/hid-megaworld.c
index 81acdbc3a00f..d5c868974275 100644
--- a/drivers/hid/hid-megaworld.c
+++ b/drivers/hid/hid-megaworld.c
@@ -35,21 +35,16 @@ static int mwctrl_play(struct input_dev *dev, void *data,
return 0;
}
-static int mwctrl_init(struct hid_device *hid)
+static int mwctrl_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct mwctrl_device *mwctrl;
struct hid_report *report;
- struct hid_input *hidinput;
- struct input_dev *dev;
+ struct input_dev *dev = hidinput->input;
int error;
int i;
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
- return -ENODEV;
- }
- hidinput = list_entry(hid->inputs.next, struct hid_input, list);
- dev = hidinput->input;
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
for (i = 0; i < 4; i++) {
report = hid_validate_values(hid, HID_OUTPUT_REPORT, 0, i, 1);
@@ -61,16 +56,7 @@ static int mwctrl_init(struct hid_device *hid)
if (!mwctrl)
return -ENOMEM;
- set_bit(FF_RUMBLE, dev->ffbit);
-
- error = input_ff_create_memless(dev, mwctrl, mwctrl_play);
- if (error) {
- kfree(mwctrl);
- return error;
- }
-
mwctrl->report = report;
-
/* Field 0 is always 2, and field 1 is always 0. The original
* windows driver has a 5 bytes command, where the 5th byte is
* a repeat of the 3rd byte, however the device has only 4
@@ -82,30 +68,15 @@ static int mwctrl_init(struct hid_device *hid)
mwctrl->strong = &report->field[2]->value[0];
mwctrl->weak = &report->field[3]->value[0];
- return 0;
-}
-
-static int mwctrl_probe(struct hid_device *hdev, const struct hid_device_id *id)
-{
- int ret;
-
- ret = hid_parse(hdev);
- if (ret) {
- hid_err(hdev, "parse failed\n");
- return ret;
- }
+ set_bit(FF_RUMBLE, dev->ffbit);
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
- if (ret) {
- hid_err(hdev, "hw start failed\n");
- return ret;
+ error = input_ff_create_memless(dev, mwctrl, mwctrl_play);
+ if (error) {
+ kfree(mwctrl);
+ return error;
}
- ret = mwctrl_init(hdev);
- if (ret)
- hid_hw_stop(hdev);
-
- return ret;
+ return 0;
}
static const struct hid_device_id mwctrl_devices[] = {
@@ -118,7 +89,7 @@ MODULE_DEVICE_TABLE(hid, mwctrl_devices);
static struct hid_driver mwctrl_driver = {
.name = "megaworld",
.id_table = mwctrl_devices,
- .probe = mwctrl_probe,
+ .input_configured = mwctrl_input_configured,
};
module_hid_driver(mwctrl_driver);
diff --git a/drivers/hid/hid-mf.c b/drivers/hid/hid-mf.c
index 6ff54a1ec697..136e8b41d5f4 100644
--- a/drivers/hid/hid-mf.c
+++ b/drivers/hid/hid-mf.c
@@ -54,61 +54,45 @@ static int mf_play(struct input_dev *dev, void *data, struct ff_effect *effect)
return 0;
}
-static int mf_init(struct hid_device *hid)
+static int mf_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct mf_device *mf;
-
struct list_head *report_list =
&hid->report_enum[HID_OUTPUT_REPORT].report_list;
-
- struct list_head *report_ptr;
struct hid_report *report;
-
- struct list_head *input_ptr = &hid->inputs;
- struct hid_input *input;
-
- struct input_dev *dev;
-
+ struct input_dev *dev = hidinput->input;
int error;
- /* Setup each of the four inputs */
- list_for_each(report_ptr, report_list) {
- report = list_entry(report_ptr, struct hid_report, list);
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
- if (report->maxfield < 1 || report->field[0]->report_count < 2) {
- hid_err(hid, "Invalid report, this should never happen!\n");
- return -ENODEV;
- }
-
- if (list_is_last(input_ptr, &hid->inputs)) {
- hid_err(hid, "Missing input, this should never happen!\n");
- return -ENODEV;
- }
-
- input_ptr = input_ptr->next;
- input = list_entry(input_ptr, struct hid_input, list);
+ report = list_first_entry_or_null(report_list, struct hid_report, list);
+ if (!report) {
+ hid_err(hid, "no output reports found\n");
+ return -ENODEV;
+ }
- mf = kzalloc_obj(struct mf_device);
- if (!mf)
- return -ENOMEM;
+ if (report->maxfield < 1 || report->field[0]->report_count < 2) {
+ hid_err(hid, "Invalid report, this should never happen!\n");
+ return -ENODEV;
+ }
- dev = input->input;
- set_bit(FF_RUMBLE, dev->ffbit);
+ mf = kzalloc_obj(struct mf_device);
+ if (!mf)
+ return -ENOMEM;
- error = input_ff_create_memless(dev, mf, mf_play);
- if (error) {
- kfree(mf);
- return error;
- }
+ mf->report = report;
+ set_bit(FF_RUMBLE, dev->ffbit);
- mf->report = report;
- mf->report->field[0]->value[0] = 0x00;
- mf->report->field[0]->value[1] = 0x00;
- hid_hw_request(hid, mf->report, HID_REQ_SET_REPORT);
+ error = input_ff_create_memless(dev, mf, mf_play);
+ if (error) {
+ kfree(mf);
+ return error;
}
- hid_info(hid, "Force feedback for HJZ Mayflash game controller "
- "adapters by Marcel Hasler <mahasler@gmail.com>\n");
+ mf->report->field[0]->value[0] = 0x00;
+ mf->report->field[0]->value[1] = 0x00;
+ hid_hw_request(hid, mf->report, HID_REQ_SET_REPORT);
return 0;
}
@@ -128,22 +112,14 @@ static int mf_probe(struct hid_device *hid, const struct hid_device_id *id)
return error;
}
- error = hid_hw_start(hid, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
+ error = hid_hw_start(hid, HID_CONNECT_DEFAULT);
if (error) {
hid_err(hid, "HID hw start failed\n");
return error;
}
- error = mf_init(hid);
- if (error) {
- hid_err(hid, "Force feedback init failed.\n");
- hid_hw_stop(hid);
- return error;
- }
-
return 0;
}
-
static const struct hid_device_id mf_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_DRAGONRISE, USB_DEVICE_ID_DRAGONRISE_PS3),
.driver_data = HID_QUIRK_MULTI_INPUT },
@@ -163,6 +139,7 @@ static struct hid_driver mf_driver = {
.name = "hid_mf",
.id_table = mf_devices,
.probe = mf_probe,
+ .input_configured = mf_input_configured,
};
module_hid_driver(mf_driver);
diff --git a/drivers/hid/hid-microsoft.c b/drivers/hid/hid-microsoft.c
index 18ac21c0bcb2..a7d3493a6141 100644
--- a/drivers/hid/hid-microsoft.c
+++ b/drivers/hid/hid-microsoft.c
@@ -323,22 +323,17 @@ static int ms_play_effect(struct input_dev *dev, void *data,
return 0;
}
-static int ms_init_ff(struct hid_device *hdev)
+static int ms_input_configured(struct hid_device *hdev, struct hid_input *hidinput)
{
- struct hid_input *hidinput;
- struct input_dev *input_dev;
struct ms_data *ms = hid_get_drvdata(hdev);
-
- if (list_empty(&hdev->inputs)) {
- hid_err(hdev, "no inputs found\n");
- return -ENODEV;
- }
- hidinput = list_entry(hdev->inputs.next, struct hid_input, list);
- input_dev = hidinput->input;
+ struct input_dev *input_dev = hidinput->input;
if (!(ms->quirks & MS_QUIRK_FF))
return 0;
+ if (!list_is_first(&hidinput->list, &hdev->inputs))
+ return 0;
+
ms->hdev = hdev;
INIT_WORK(&ms->ff_worker, ms_ff_worker);
@@ -352,16 +347,6 @@ static int ms_init_ff(struct hid_device *hdev)
return input_ff_create_memless(input_dev, NULL, ms_play_effect);
}
-static void ms_remove_ff(struct hid_device *hdev)
-{
- struct ms_data *ms = hid_get_drvdata(hdev);
-
- if (!(ms->quirks & MS_QUIRK_FF))
- return;
-
- cancel_work_sync(&ms->ff_worker);
-}
-
static int ms_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
unsigned long quirks = id->driver_data;
@@ -385,29 +370,21 @@ static int ms_probe(struct hid_device *hdev, const struct hid_device_id *id)
ret = hid_parse(hdev);
if (ret) {
hid_err(hdev, "parse failed\n");
- goto err_free;
+ return ret;
}
ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT | ((quirks & MS_HIDINPUT) ?
HID_CONNECT_HIDINPUT_FORCE : 0));
if (ret) {
hid_err(hdev, "hw start failed\n");
- goto err_free;
+ return ret;
}
- ret = ms_init_ff(hdev);
- if (ret)
- hid_err(hdev, "could not initialize ff, continuing anyway");
-
return 0;
-err_free:
- return ret;
}
-
static void ms_remove(struct hid_device *hdev)
{
hid_hw_stop(hdev);
- ms_remove_ff(hdev);
}
static const struct hid_device_id ms_devices[] = {
@@ -469,6 +446,7 @@ static struct hid_driver ms_driver = {
.report_fixup = ms_report_fixup,
.input_mapping = ms_input_mapping,
.input_mapped = ms_input_mapped,
+ .input_configured = ms_input_configured,
.event = ms_event,
.probe = ms_probe,
.remove = ms_remove,
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
new file mode 100644
index 000000000000..3225d3bf6125
--- /dev/null
+++ b/drivers/hid/hid-msi.c
@@ -0,0 +1,2063 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for MSI Claw Handheld PC gamepads.
+ *
+ * Provides configuration support for the MSI Claw series of handheld PC
+ * gamepads. Multiple iterations of the device firmware has led to some
+ * quirks for how certain attributes are handled. The original firmware
+ * did not support remapping of the M1 (right) and M2 (left) rear paddles.
+ * Additionally, the MCU RAM address for writing configuration data has
+ * changed twice. Checks are done during probe to enumerate these variances.
+ *
+ * Copyright (c) 2026 Zhouwang Huang <honjow311@gmail.com>
+ * Copyright (c) 2026 Denis Benato <denis.benato@linux.dev>
+ * Copyright (c) 2026 Valve Corporation
+ */
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/container_of.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/kobject.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/spinlock.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define CLAW_OUTPUT_REPORT_ID 0x0f
+#define CLAW_INPUT_REPORT_ID 0x10
+
+#define CLAW_PACKET_SIZE 64
+
+#define CLAW_DINPUT_CFG_INTF_IN 0x82
+#define CLAW_XINPUT_CFG_INTF_IN 0x83
+
+#define CLAW_KEYS_MAX 5
+
+#define CLAW_RGB_ZONES 9
+#define CLAW_RGB_MAX_FRAMES 8
+#define CLAW_RGB_FRAME_OFFSET 0x24
+
+enum claw_command_index {
+ CLAW_COMMAND_TYPE_NONE = 0x00,
+ CLAW_COMMAND_TYPE_READ_PROFILE = 0x04,
+ CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05,
+ CLAW_COMMAND_TYPE_ACK = 0x06,
+ CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA = 0x21,
+ CLAW_COMMAND_TYPE_SYNC_TO_ROM = 0x22,
+ CLAW_COMMAND_TYPE_SWITCH_MODE = 0x24,
+ CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE = 0x26,
+ CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK = 0x27,
+ CLAW_COMMAND_TYPE_RESET_DEVICE = 0x28,
+};
+
+enum claw_gamepad_mode_index {
+ CLAW_GAMEPAD_MODE_XINPUT = 0x01,
+ CLAW_GAMEPAD_MODE_DINPUT = 0x02,
+ CLAW_GAMEPAD_MODE_DESKTOP = 0x04,
+};
+
+static const char * const claw_gamepad_mode_text[] = {
+ [CLAW_GAMEPAD_MODE_XINPUT] = "xinput",
+ [CLAW_GAMEPAD_MODE_DINPUT] = "dinput",
+ [CLAW_GAMEPAD_MODE_DESKTOP] = "desktop",
+};
+
+enum claw_profile_ack_pending {
+ CLAW_NO_PENDING,
+ CLAW_M1_PENDING,
+ CLAW_M2_PENDING,
+ CLAW_RGB_PENDING,
+ CLAW_RUMBLE_LEFT_PENDING,
+ CLAW_RUMBLE_RIGHT_PENDING,
+};
+
+enum claw_key_index {
+ CLAW_KEY_M1,
+ CLAW_KEY_M2,
+};
+
+enum claw_mkeys_function_index {
+ CLAW_MKEY_FUNCTION_MACRO,
+ CLAW_MKEY_FUNCTION_DISABLED,
+ CLAW_MKEY_FUNCTION_COMBO,
+};
+
+enum claw_mode_field {
+ CLAW_FIELD_GAMEPAD_MODE,
+ CLAW_FIELD_MKEYS_FUNCTION,
+};
+
+static const char * const claw_mkeys_function_text[] = {
+ [CLAW_MKEY_FUNCTION_MACRO] = "macro",
+ [CLAW_MKEY_FUNCTION_DISABLED] = "disabled",
+ [CLAW_MKEY_FUNCTION_COMBO] = "combination",
+};
+
+static const struct {
+ u8 code;
+ const char *name;
+} claw_button_mapping_key_map[] = {
+ /* Gamepad buttons */
+ { 0x01, "ABS_HAT0Y_UP" },
+ { 0x02, "ABS_HAT0Y_DOWN" },
+ { 0x03, "ABS_HAT0X_LEFT" },
+ { 0x04, "ABS_HAT0X_RIGHT" },
+ { 0x05, "BTN_TL" },
+ { 0x06, "BTN_TR" },
+ { 0x07, "BTN_THUMBL" },
+ { 0x08, "BTN_THUMBR" },
+ { 0x09, "BTN_SOUTH" },
+ { 0x0a, "BTN_EAST" },
+ { 0x0b, "BTN_NORTH" },
+ { 0x0c, "BTN_WEST" },
+ { 0x0d, "BTN_MODE" },
+ { 0x0e, "BTN_SELECT" },
+ { 0x0f, "BTN_START" },
+ { 0x13, "BTN_TL2"},
+ { 0x14, "BTN_TR2"},
+ { 0x15, "ABS_Y_UP"},
+ { 0x16, "ABS_Y_DOWN"},
+ { 0x17, "ABS_X_LEFT"},
+ { 0x18, "ABS_X_RIGHT"},
+ { 0x19, "ABS_RY_UP"},
+ { 0x1a, "ABS_RY_DOWN"},
+ { 0x1b, "ABS_RX_LEFT"},
+ { 0x1c, "ABS_RX_RIGHT"},
+ /* Keyboard keys */
+ { 0x32, "KEY_ESC" },
+ { 0x33, "KEY_F1" },
+ { 0x34, "KEY_F2" },
+ { 0x35, "KEY_F3" },
+ { 0x36, "KEY_F4" },
+ { 0x37, "KEY_F5" },
+ { 0x38, "KEY_F6" },
+ { 0x39, "KEY_F7" },
+ { 0x3a, "KEY_F8" },
+ { 0x3b, "KEY_F9" },
+ { 0x3c, "KEY_F10" },
+ { 0x3d, "KEY_F11" },
+ { 0x3e, "KEY_F12" },
+ { 0x3f, "KEY_GRAVE" },
+ { 0x40, "KEY_1" },
+ { 0x41, "KEY_2" },
+ { 0x42, "KEY_3" },
+ { 0x43, "KEY_4" },
+ { 0x44, "KEY_5" },
+ { 0x45, "KEY_6" },
+ { 0x46, "KEY_7" },
+ { 0x47, "KEY_8" },
+ { 0x48, "KEY_9" },
+ { 0x49, "KEY_0" },
+ { 0x4a, "KEY_MINUS" },
+ { 0x4b, "KEY_EQUAL" },
+ { 0x4c, "KEY_BACKSPACE" },
+ { 0x4d, "KEY_TAB" },
+ { 0x4e, "KEY_Q" },
+ { 0x4f, "KEY_W" },
+ { 0x50, "KEY_E" },
+ { 0x51, "KEY_R" },
+ { 0x52, "KEY_T" },
+ { 0x53, "KEY_Y" },
+ { 0x54, "KEY_U" },
+ { 0x55, "KEY_I" },
+ { 0x56, "KEY_O" },
+ { 0x57, "KEY_P" },
+ { 0x58, "KEY_LEFTBRACE" },
+ { 0x59, "KEY_RIGHTBRACE" },
+ { 0x5a, "KEY_BACKSLASH" },
+ { 0x5b, "KEY_CAPSLOCK" },
+ { 0x5c, "KEY_A" },
+ { 0x5d, "KEY_S" },
+ { 0x5e, "KEY_D" },
+ { 0x5f, "KEY_F" },
+ { 0x60, "KEY_G" },
+ { 0x61, "KEY_H" },
+ { 0x62, "KEY_J" },
+ { 0x63, "KEY_K" },
+ { 0x64, "KEY_L" },
+ { 0x65, "KEY_SEMICOLON" },
+ { 0x66, "KEY_APOSTROPHE" },
+ { 0x67, "KEY_ENTER" },
+ { 0x68, "KEY_LEFTSHIFT" },
+ { 0x69, "KEY_Z" },
+ { 0x6a, "KEY_X" },
+ { 0x6b, "KEY_C" },
+ { 0x6c, "KEY_V" },
+ { 0x6d, "KEY_B" },
+ { 0x6e, "KEY_N" },
+ { 0x6f, "KEY_M" },
+ { 0x70, "KEY_COMMA" },
+ { 0x71, "KEY_DOT" },
+ { 0x72, "KEY_SLASH" },
+ { 0x73, "KEY_RIGHTSHIFT" },
+ { 0x74, "KEY_LEFTCTRL" },
+ { 0x75, "KEY_LEFTMETA" },
+ { 0x76, "KEY_LEFTALT" },
+ { 0x77, "KEY_SPACE" },
+ { 0x78, "KEY_RIGHTALT" },
+ { 0x79, "KEY_RIGHTCTRL" },
+ { 0x7a, "KEY_INSERT" },
+ { 0x7b, "KEY_HOME" },
+ { 0x7c, "KEY_PAGEUP" },
+ { 0x7d, "KEY_DELETE" },
+ { 0x7e, "KEY_END" },
+ { 0x7f, "KEY_PAGEDOWN" },
+ { 0x8a, "KEY_KPENTER" },
+ { 0x8b, "KEY_KP0" },
+ { 0x8c, "KEY_KP1" },
+ { 0x8d, "KEY_KP2" },
+ { 0x8e, "KEY_KP3" },
+ { 0x8f, "KEY_KP4" },
+ { 0x90, "KEY_KP5" },
+ { 0x91, "KEY_KP6" },
+ { 0x92, "KEY_KP7" },
+ { 0x93, "KEY_KP8" },
+ { 0x94, "KEY_KP9" },
+ { 0x95, "MD_PLAY" },
+ { 0x96, "MD_STOP" },
+ { 0x97, "MD_NEXT" },
+ { 0x98, "MD_PREV" },
+ { 0x99, "MD_VOL_UP" },
+ { 0x9a, "MD_VOL_DOWN" },
+ { 0x9b, "MD_VOL_MUTE" },
+ { 0x9c, "KEY_F23" },
+ /* Mouse events */
+ { 0xc8, "BTN_LEFT" },
+ { 0xc9, "BTN_MIDDLE" },
+ { 0xca, "BTN_RIGHT" },
+ { 0xcb, "BTN_SIDE" },
+ { 0xcc, "BTN_EXTRA" },
+ { 0xcd, "REL_WHEEL_UP" },
+ { 0xce, "REL_WHEEL_DOWN" },
+ { 0xff, "DISABLED" },
+};
+
+enum claw_rgb_effect_index {
+ CLAW_RGB_EFFECT_MONOCOLOR,
+ CLAW_RGB_EFFECT_BREATHE,
+ CLAW_RGB_EFFECT_CHROMA,
+ CLAW_RGB_EFFECT_RAINBOW,
+ CLAW_RGB_EFFECT_FROSTFIRE,
+};
+
+static const char * const claw_rgb_effect_text[] = {
+ [CLAW_RGB_EFFECT_MONOCOLOR] = "monocolor",
+ [CLAW_RGB_EFFECT_BREATHE] = "breathe",
+ [CLAW_RGB_EFFECT_CHROMA] = "chroma",
+ [CLAW_RGB_EFFECT_RAINBOW] = "rainbow",
+ [CLAW_RGB_EFFECT_FROSTFIRE] = "frostfire",
+};
+
+static const u16 button_mapping_addr_old[] = {
+ 0x007a, /* M1 */
+ 0x011f, /* M2 */
+};
+
+static const u16 button_mapping_addr_new[] = {
+ 0x00bb, /* M1 */
+ 0x0164, /* M2 */
+};
+
+static const u16 rgb_addr_old = 0x01fa;
+static const u16 rgb_addr_new = 0x024a;
+
+static const u16 rumble_addr[] = {
+ 0x0022, /* left */
+ 0x0023, /* right */
+};
+
+struct claw_command_report {
+ u8 report_id;
+ u8 padding[2];
+ u8 header_tail;
+ u8 cmd;
+ u8 data[59];
+} __packed;
+
+struct claw_profile_report {
+ u8 profile;
+ __be16 read_addr;
+} __packed;
+
+struct claw_mkey_report {
+ struct claw_profile_report;
+ u8 padding_0;
+ u8 padding_1;
+ u8 padding_2;
+ u8 codes[5];
+} __packed;
+
+struct rgb_zone {
+ u8 red;
+ u8 green;
+ u8 blue;
+};
+
+struct rgb_frame {
+ struct rgb_zone zone[CLAW_RGB_ZONES];
+};
+
+struct claw_rgb_report {
+ struct claw_profile_report;
+ u8 frame_bytes;
+ u8 padding;
+ u8 frame_count;
+ u8 state; /* Always 0x09 */
+ u8 speed;
+ u8 brightness;
+ struct rgb_frame zone_data;
+} __packed;
+
+struct claw_rumble_report {
+ struct claw_profile_report;
+ u8 padding;
+ u8 intensity;
+} __packed;
+
+struct claw_drvdata {
+ /* MCU General Variables */
+ enum claw_profile_ack_pending profile_pending;
+ struct completion orphan_ack_complete;
+ struct completion send_cmd_complete;
+ struct delayed_work cfg_resume;
+ struct delayed_work cfg_setup;
+ spinlock_t registration_lock; /* Lock for registration read/write */
+ struct mutex profile_mutex; /* mutex for profile_pending calls */
+ spinlock_t profile_lock; /* Lock for profile_pending read/write */
+ struct hid_device *hdev;
+ bool orphan_ack_pending;
+ struct mutex cfg_mutex; /* mutex for synchronous data */
+ struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */
+ spinlock_t cmd_lock; /* Lock for cmd data read/write */
+ u8 waiting_cmd;
+ int cmd_status;
+ u16 bcd_device;
+ u8 ep;
+
+ /* Gamepad Variables */
+ enum claw_mkeys_function_index mkeys_function;
+ enum claw_gamepad_mode_index gamepad_mode;
+ u8 m1_codes[CLAW_KEYS_MAX];
+ u8 m2_codes[CLAW_KEYS_MAX];
+ u8 rumble_intensity_right;
+ u8 rumble_intensity_left;
+ const u16 *bmap_addr;
+ spinlock_t rumble_lock; /* lock for rumble_intensity read/write */
+ spinlock_t mode_lock; /* Lock for mode data read/write */
+ bool rumble_support;
+ bool gp_registered;
+ bool bmap_support;
+
+ /* RGB Variables */
+ struct rgb_frame rgb_frames[CLAW_RGB_MAX_FRAMES];
+ enum claw_rgb_effect_index rgb_effect;
+ struct led_classdev_mc led_mc;
+ struct delayed_work rgb_queue;
+ spinlock_t frame_lock; /* lock for rgb_frames read/write */
+ bool rgb_registered;
+ u8 rgb_frame_count;
+ bool rgb_enabled;
+ u8 rgb_speed;
+ u16 rgb_addr;
+};
+
+static int get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_host_endpoint *ep;
+ struct usb_interface *intf;
+
+ intf = to_usb_interface(hdev->dev.parent);
+ ep = intf->cur_altsetting->endpoint;
+ if (ep)
+ return ep->desc.bEndpointAddress;
+
+ return -ENODEV;
+}
+
+static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
+ struct claw_command_report *cmd_rep)
+{
+ if (cmd_rep->data[0] >= ARRAY_SIZE(claw_gamepad_mode_text) ||
+ !claw_gamepad_mode_text[cmd_rep->data[0]] ||
+ cmd_rep->data[1] >= ARRAY_SIZE(claw_mkeys_function_text))
+ return -EINVAL;
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock) {
+ drvdata->gamepad_mode = cmd_rep->data[0];
+ drvdata->mkeys_function = cmd_rep->data[1];
+ }
+
+ return 0;
+}
+
+static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep)
+{
+ enum claw_profile_ack_pending profile;
+ struct claw_rumble_report *rumble;
+ struct claw_mkey_report *mkeys;
+ struct claw_rgb_report *frame;
+ u16 rgb_addr, read_addr;
+ u8 *codes, key, f_idx;
+ u16 frame_calc;
+ int i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ profile = drvdata->profile_pending;
+
+ switch (profile) {
+ case CLAW_M1_PENDING:
+ case CLAW_M2_PENDING:
+ key = (profile == CLAW_M1_PENDING) ? CLAW_KEY_M1 : CLAW_KEY_M2;
+ mkeys = (struct claw_mkey_report *)cmd_rep->data;
+ if (be16_to_cpu(mkeys->read_addr) != drvdata->bmap_addr[key])
+ return -EAGAIN;
+ codes = (profile == CLAW_M1_PENDING) ? drvdata->m1_codes : drvdata->m2_codes;
+ for (i = 0; i < CLAW_KEYS_MAX; i++)
+ codes[i] = (mkeys->codes[i]);
+ break;
+ case CLAW_RGB_PENDING:
+ frame = (struct claw_rgb_report *)cmd_rep->data;
+ rgb_addr = drvdata->rgb_addr;
+ read_addr = be16_to_cpu(frame->read_addr);
+
+ if (read_addr < drvdata->rgb_addr)
+ return -EAGAIN;
+
+ frame_calc = (read_addr - rgb_addr) / CLAW_RGB_FRAME_OFFSET;
+ if (frame_calc >= CLAW_RGB_MAX_FRAMES) {
+ dev_err(&drvdata->hdev->dev, "Got unsupported frame index: %x\n",
+ frame_calc);
+ return -EAGAIN;
+ }
+ f_idx = frame_calc;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data,
+ sizeof(struct rgb_frame));
+
+ /* Only use frame 0 for remaining variable assignment */
+ if (f_idx != 0)
+ break;
+
+ drvdata->rgb_speed = frame->speed;
+ drvdata->led_mc.led_cdev.brightness = frame->brightness;
+ drvdata->led_mc.subled_info[0].intensity = frame->zone_data.zone[0].red;
+ drvdata->led_mc.subled_info[1].intensity = frame->zone_data.zone[0].green;
+ drvdata->led_mc.subled_info[2].intensity = frame->zone_data.zone[0].blue;
+ }
+
+ break;
+ case CLAW_RUMBLE_LEFT_PENDING:
+ rumble = (struct claw_rumble_report *)cmd_rep->data;
+ if (be16_to_cpu(rumble->read_addr) != rumble_addr[0])
+ return -EAGAIN;
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ drvdata->rumble_intensity_left = rumble->intensity;
+ break;
+ case CLAW_RUMBLE_RIGHT_PENDING:
+ rumble = (struct claw_rumble_report *)cmd_rep->data;
+ if (be16_to_cpu(rumble->read_addr) != rumble_addr[1])
+ return -EAGAIN;
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ drvdata->rumble_intensity_right = rumble->intensity;
+ break;
+ default:
+ dev_dbg(&drvdata->hdev->dev,
+ "Got profile event without changes pending from command: %x\n",
+ cmd_rep->cmd);
+ return -EINVAL;
+ }
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_NO_PENDING;
+
+ return 0;
+}
+
+static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct claw_command_report *cmd_rep;
+ int ret = 0;
+
+ if (size != CLAW_PACKET_SIZE)
+ return 0;
+
+ cmd_rep = (struct claw_command_report *)data;
+
+ if (cmd_rep->report_id != CLAW_INPUT_REPORT_ID || cmd_rep->header_tail != 0x3c)
+ return 0;
+
+ dev_dbg(&drvdata->hdev->dev, "Rx data as raw input report: [%*ph]\n",
+ CLAW_PACKET_SIZE, data);
+
+ guard(spinlock_irqsave)(&drvdata->cmd_lock);
+ switch (cmd_rep->cmd) {
+ case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK:
+ ret = claw_gamepad_mode_event(drvdata, cmd_rep);
+ if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE) {
+ drvdata->cmd_status = ret;
+ complete(&drvdata->send_cmd_complete);
+ }
+
+ break;
+ case CLAW_COMMAND_TYPE_READ_PROFILE_ACK:
+ ret = claw_profile_event(drvdata, cmd_rep);
+ /* Stale address received, ignore and keep waiting */
+ if (ret == -EAGAIN)
+ return 0;
+ if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_PROFILE) {
+ drvdata->cmd_status = ret;
+ complete(&drvdata->send_cmd_complete);
+ }
+
+ break;
+ case CLAW_COMMAND_TYPE_ACK:
+ if (drvdata->orphan_ack_pending) {
+ drvdata->orphan_ack_pending = false;
+ complete(&drvdata->orphan_ack_complete);
+ break;
+ }
+
+ if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_NONE) {
+ dev_warn(&drvdata->hdev->dev, "Got unexpected ACK from MCU, ignoring\n");
+ break;
+ }
+
+ drvdata->cmd_status = 0;
+ complete(&drvdata->send_cmd_complete);
+
+ dev_dbg(&drvdata->hdev->dev, "Waiting CMD: %x\n", drvdata->waiting_cmd);
+
+ break;
+ default:
+ dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd);
+ return 0;
+ }
+
+ return ret;
+}
+
+static int msi_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata || (drvdata->ep != CLAW_XINPUT_CFG_INTF_IN &&
+ drvdata->ep != CLAW_DINPUT_CFG_INTF_IN))
+ return 0;
+
+ return claw_raw_event(drvdata, report, data, size);
+}
+
+/* Caller must hold drvdata->cfg_mutex. */
+static int __claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
+ size_t len, unsigned int timeout)
+{
+ unsigned char *dmabuf __free(kfree) = NULL;
+ u8 header[] = { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index };
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ size_t header_size = ARRAY_SIZE(header);
+ bool orphaned;
+ int ret;
+
+ lockdep_assert_held(&drvdata->cfg_mutex);
+
+ /* If expecting an orphan ack, hold next event until MCU has time to clear it */
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock)
+ orphaned = drvdata->orphan_ack_pending;
+
+ if (orphaned) {
+ wait_for_completion_timeout(&drvdata->orphan_ack_complete, msecs_to_jiffies(25));
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock)
+ drvdata->orphan_ack_pending = false;
+ }
+
+ if (header_size + len > CLAW_PACKET_SIZE)
+ return -EINVAL;
+
+ /* We can't use a devm_alloc reusable buffer without side effects during suspend */
+ dmabuf = kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ memcpy(dmabuf, header, header_size);
+ if (data && len)
+ memcpy(dmabuf + header_size, data, len);
+
+ reinit_completion(&drvdata->send_cmd_complete);
+
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
+ if (timeout) {
+ drvdata->waiting_cmd = index;
+ drvdata->cmd_status = -ETIMEDOUT;
+ } else {
+ reinit_completion(&drvdata->orphan_ack_complete);
+ drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
+ drvdata->orphan_ack_pending = true;
+ }
+ }
+
+ dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+ CLAW_PACKET_SIZE, dmabuf);
+
+ ret = hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE);
+ if (ret < 0)
+ goto err;
+
+ ret = ret == CLAW_PACKET_SIZE ? 0 : -EIO;
+ if (ret)
+ goto err;
+
+ if (timeout) {
+ ret = wait_for_completion_interruptible_timeout(&drvdata->send_cmd_complete,
+ msecs_to_jiffies(timeout));
+
+ dev_dbg(&hdev->dev, "Remaining timeout: %u\n", ret);
+ ret = ret > 0 ? drvdata->cmd_status : ret ?: -EBUSY;
+ if (ret)
+ goto err;
+ }
+
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock)
+ drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
+
+ return ret;
+
+err:
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
+ drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
+ drvdata->orphan_ack_pending = false;
+ }
+ return ret;
+}
+
+static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
+ size_t len, unsigned int timeout)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ guard(mutex)(&drvdata->cfg_mutex);
+ return __claw_hw_output_report(hdev, index, data, len, timeout);
+}
+
+static int claw_switch_mode(struct hid_device *hdev, enum claw_mode_field field, u8 val)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 data[2];
+
+ guard(mutex)(&drvdata->cfg_mutex);
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock) {
+ switch (field) {
+ case CLAW_FIELD_GAMEPAD_MODE:
+ data[0] = val;
+ data[1] = drvdata->mkeys_function;
+ break;
+ case CLAW_FIELD_MKEYS_FUNCTION:
+ data[0] = drvdata->gamepad_mode;
+ data[1] = val;
+ break;
+ }
+ }
+
+ return __claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data,
+ ARRAY_SIZE(data), 0);
+}
+
+static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int i, ret = -EINVAL;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
+ if (claw_gamepad_mode_text[i] && sysfs_streq(buf, claw_gamepad_mode_text[i])) {
+ ret = i;
+ break;
+ }
+ }
+ if (ret < 0)
+ return ret;
+
+ ret = claw_switch_mode(hdev, CLAW_FIELD_GAMEPAD_MODE, ret);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t gamepad_mode_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret, i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
+ i = drvdata->gamepad_mode;
+
+ if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
+ return sysfs_emit(buf, "unsupported\n");
+
+ return sysfs_emit(buf, "%s\n", claw_gamepad_mode_text[i]);
+}
+static DEVICE_ATTR_RW(gamepad_mode);
+
+static ssize_t gamepad_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
+ if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
+ continue;
+ count += sysfs_emit_at(buf, count, "%s ", claw_gamepad_mode_text[i]);
+ }
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(gamepad_mode_index);
+
+static ssize_t mkeys_function_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int i, ret = -EINVAL;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) {
+ if (claw_mkeys_function_text[i] && sysfs_streq(buf, claw_mkeys_function_text[i])) {
+ ret = i;
+ break;
+ }
+ }
+ if (ret < 0)
+ return ret;
+
+ ret = claw_switch_mode(hdev, CLAW_FIELD_MKEYS_FUNCTION, ret);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t mkeys_function_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret, i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
+ i = drvdata->mkeys_function;
+
+ if (i >= ARRAY_SIZE(claw_mkeys_function_text))
+ return sysfs_emit(buf, "unsupported\n");
+
+ return sysfs_emit(buf, "%s\n", claw_mkeys_function_text[i]);
+}
+static DEVICE_ATTR_RW(mkeys_function);
+
+static ssize_t mkeys_function_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_mkeys_function_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(mkeys_function_index);
+
+static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ bool val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtobool(buf, &val);
+ if (ret)
+ return ret;
+
+ if (!val)
+ return -EINVAL;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_RESET_DEVICE, NULL, 0, 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+static DEVICE_ATTR_WO(reset);
+
+static int mkey_mapping_name_to_code(const char *name)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
+ if (!strcmp(name, claw_button_mapping_key_map[i].name))
+ return claw_button_mapping_key_map[i].code;
+ }
+
+ return -EINVAL;
+}
+
+static const char *mkey_mapping_code_to_name(u8 code)
+{
+ int i;
+
+ if (code == 0xff)
+ return NULL;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
+ if (claw_button_mapping_key_map[i].code == code)
+ return claw_button_mapping_key_map[i].name;
+ }
+
+ return NULL;
+}
+
+static int claw_mkey_store(struct device *dev, const char *buf, u8 mkey)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ struct claw_mkey_report report = { {0x01, cpu_to_be16(drvdata->bmap_addr[mkey])},
+ 0x07, 0x04, 0x00, {0xff, 0xff, 0xff, 0xff, 0xff} };
+ char **raw_keys __free(argv_free) = NULL;
+ int ret, key_count, i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ raw_keys = argv_split(GFP_KERNEL, buf, &key_count);
+ if (!raw_keys)
+ return -ENOMEM;
+
+ if (key_count > CLAW_KEYS_MAX)
+ return -EINVAL;
+
+ if (key_count == 0)
+ goto set_buttons;
+
+ for (i = 0; i < key_count; i++) {
+ ret = mkey_mapping_name_to_code(raw_keys[i]);
+ if (ret < 0)
+ return ret;
+
+ report.codes[i] = ret;
+ }
+
+set_buttons:
+ scoped_guard(mutex, &drvdata->rom_mutex) {
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+ }
+
+ return ret;
+}
+
+static int claw_mkey_show(struct device *dev, char *buf, enum claw_key_index m_key)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ struct claw_mkey_report report = { {0x01, cpu_to_be16(drvdata->bmap_addr[m_key])}, 0x07 };
+ int i, ret, count = 0;
+ const char *name;
+ u8 *codes;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ codes = (m_key == CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_codes;
+
+ guard(mutex)(&drvdata->profile_mutex);
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = (m_key == CLAW_KEY_M1) ? CLAW_M1_PENDING
+ : CLAW_M2_PENDING;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < CLAW_KEYS_MAX; i++) {
+ name = mkey_mapping_code_to_name(codes[i]);
+ if (name)
+ count += sysfs_emit_at(buf, count, "%s ", name);
+ }
+
+ if (!count)
+ return sysfs_emit(buf, "(not set)\n");
+
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t button_m1_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+
+ ret = claw_mkey_store(dev, buf, CLAW_KEY_M1);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t button_m1_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return claw_mkey_show(dev, buf, CLAW_KEY_M1);
+}
+static DEVICE_ATTR_RW(button_m1);
+
+static ssize_t button_m2_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+
+ ret = claw_mkey_store(dev, buf, CLAW_KEY_M2);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t button_m2_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return claw_mkey_show(dev, buf, CLAW_KEY_M2);
+}
+static DEVICE_ATTR_RW(button_m2);
+
+static ssize_t button_mapping_options_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_button_mapping_key_map[i].name);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(button_mapping_options);
+
+static ssize_t rumble_intensity_left_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[0])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val > 100)
+ return -EINVAL;
+
+ report.intensity = val;
+
+ guard(mutex)(&drvdata->rom_mutex);
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t rumble_intensity_left_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[0])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
+ u8 val;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ guard(mutex)(&drvdata->profile_mutex);
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_RUMBLE_LEFT_PENDING;
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ val = drvdata->rumble_intensity_left;
+
+ return sysfs_emit(buf, "%u\n", val);
+}
+static DEVICE_ATTR_RW(rumble_intensity_left);
+
+static ssize_t rumble_intensity_right_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[1])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val > 100)
+ return -EINVAL;
+
+ report.intensity = val;
+
+ guard(mutex)(&drvdata->rom_mutex);
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t rumble_intensity_right_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[1])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
+ u8 val;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ guard(mutex)(&drvdata->profile_mutex);
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_RUMBLE_RIGHT_PENDING;
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ val = drvdata->rumble_intensity_right;
+
+ return sysfs_emit(buf, "%u\n", val);
+}
+static DEVICE_ATTR_RW(rumble_intensity_right);
+
+static ssize_t rumble_intensity_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0-100\n");
+}
+static DEVICE_ATTR_RO(rumble_intensity_range);
+
+static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
+ int n)
+{
+ struct hid_device *hdev = to_hid_device(kobj_to_dev(kobj));
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata) {
+ dev_warn(&hdev->dev,
+ "Failed to get drvdata from kobj. Gamepad attributes are not available.\n");
+ return 0;
+ }
+
+ /* Always show attrs available on all firmware */
+ if (attr == &dev_attr_gamepad_mode.attr ||
+ attr == &dev_attr_gamepad_mode_index.attr ||
+ attr == &dev_attr_mkeys_function.attr ||
+ attr == &dev_attr_mkeys_function_index.attr ||
+ attr == &dev_attr_reset.attr)
+ return attr->mode;
+
+ /* Hide rumble attrs if not supported */
+ if (attr == &dev_attr_rumble_intensity_left.attr ||
+ attr == &dev_attr_rumble_intensity_right.attr ||
+ attr == &dev_attr_rumble_intensity_range.attr)
+ return drvdata->rumble_support ? attr->mode : 0;
+
+ /* Hide button mapping attrs if it isn't supported */
+ return drvdata->bmap_support ? attr->mode : 0;
+}
+
+static struct attribute *claw_gamepad_attrs[] = {
+ &dev_attr_button_m1.attr,
+ &dev_attr_button_m2.attr,
+ &dev_attr_button_mapping_options.attr,
+ &dev_attr_gamepad_mode.attr,
+ &dev_attr_gamepad_mode_index.attr,
+ &dev_attr_mkeys_function.attr,
+ &dev_attr_mkeys_function_index.attr,
+ &dev_attr_reset.attr,
+ &dev_attr_rumble_intensity_left.attr,
+ &dev_attr_rumble_intensity_right.attr,
+ &dev_attr_rumble_intensity_range.attr,
+ NULL,
+};
+
+static const struct attribute_group claw_gamepad_attr_group = {
+ .attrs = claw_gamepad_attrs,
+ .is_visible = claw_gamepad_attr_is_visible,
+};
+
+/* Read RGB config from device */
+static int claw_read_rgb_config(struct hid_device *hdev)
+{
+ u8 data[4] = { 0x01, 0x00, 0x00, CLAW_RGB_FRAME_OFFSET };
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u16 read_addr = drvdata->rgb_addr;
+ size_t len = ARRAY_SIZE(data);
+ int ret, i;
+
+ if (!drvdata->rgb_addr)
+ return -ENODEV;
+
+ /* Loop through all 8 pages of RGB data */
+ guard(mutex)(&drvdata->profile_mutex);
+ for (i = 0; i < CLAW_RGB_MAX_FRAMES; i++) {
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_RGB_PENDING;
+ data[1] = (read_addr >> 8) & 0xff;
+ data[2] = read_addr & 0x00ff;
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 25);
+ if (ret)
+ return ret;
+
+ read_addr += CLAW_RGB_FRAME_OFFSET;
+ }
+
+ return 0;
+}
+
+/* Send RGB configuration to device */
+static int claw_write_rgb_state(struct claw_drvdata *drvdata)
+{
+ struct claw_rgb_report report = { {0x01, 0}, CLAW_RGB_FRAME_OFFSET, 0x00,
+ drvdata->rgb_frame_count, 0x09, drvdata->rgb_speed,
+ drvdata->led_mc.led_cdev.brightness };
+ u16 write_addr = drvdata->rgb_addr;
+ int f, ret;
+
+ if (!drvdata->rgb_addr)
+ return -ENODEV;
+
+ if (!drvdata->rgb_frame_count)
+ return -EINVAL;
+
+ guard(mutex)(&drvdata->rom_mutex);
+ /* Loop through (up to) 8 pages of RGB data */
+ for (f = 0; f < drvdata->rgb_frame_count; f++) {
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock)
+ report.zone_data = drvdata->rgb_frames[f];
+
+ /* Set the MCU address to write the frame data to */
+ report.read_addr = cpu_to_be16(write_addr);
+
+ /* Serialize the rgb_report and write it to MCU */
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ /* Increment the write addr by the offset for the next frame */
+ write_addr += CLAW_RGB_FRAME_OFFSET;
+ }
+
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+
+ return ret;
+}
+
+/* Fill all zones with the same color */
+static void claw_frame_fill_solid(struct rgb_frame *frame, struct rgb_zone zone)
+{
+ int z;
+
+ for (z = 0; z < CLAW_RGB_ZONES; z++)
+ frame->zone[z] = zone;
+}
+
+/* Apply solid effect (1 frame, no color) */
+static int claw_apply_disabled(struct claw_drvdata *drvdata)
+{
+ struct rgb_zone off = { 0x00, 0x00, 0x00};
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = 1;
+ claw_frame_fill_solid(&drvdata->rgb_frames[0], off);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply solid effect (1 frame, all zones same color) */
+static int claw_apply_monocolor(struct claw_drvdata *drvdata)
+{
+ struct mc_subled *subleds = drvdata->led_mc.subled_info;
+ struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity,
+ subleds[2].intensity };
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = 1;
+ claw_frame_fill_solid(&drvdata->rgb_frames[0], zone);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply breathe effect (2 frames: color -> off) */
+static int claw_apply_breathe(struct claw_drvdata *drvdata)
+{
+ struct mc_subled *subleds = drvdata->led_mc.subled_info;
+ struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity,
+ subleds[2].intensity };
+ static const struct rgb_zone off = { 0, 0, 0 };
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = 2;
+ claw_frame_fill_solid(&drvdata->rgb_frames[0], zone);
+ claw_frame_fill_solid(&drvdata->rgb_frames[1], off);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply chroma effect (6 frames: rainbow cycle, all zones sync) */
+static int claw_apply_chroma(struct claw_drvdata *drvdata)
+{
+ static const struct rgb_zone colors[] = {
+ {255, 0, 0}, /* red */
+ {255, 255, 0}, /* yellow */
+ { 0, 255, 0}, /* green */
+ { 0, 255, 255}, /* cyan */
+ { 0, 0, 255}, /* blue */
+ {255, 0, 255}, /* magenta */
+ };
+ u8 frame_count = ARRAY_SIZE(colors);
+ int f;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = frame_count;
+
+ for (f = 0; f < frame_count; f++)
+ claw_frame_fill_solid(&drvdata->rgb_frames[f], colors[f]);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply rainbow effect (4 frames: rotating colors around joysticks) */
+static int claw_apply_rainbow(struct claw_drvdata *drvdata)
+{
+ static const struct rgb_zone colors[] = {
+ {255, 0, 0}, /* red */
+ { 0, 255, 0}, /* green */
+ { 0, 255, 255}, /* cyan */
+ { 0, 0, 255}, /* blue */
+ };
+ u8 frame_count = ARRAY_SIZE(colors);
+ int f, z;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = frame_count;
+
+ for (f = 0; f < frame_count; f++) {
+ for (z = 0; z < 4; z++) {
+ drvdata->rgb_frames[f].zone[z] = colors[(z + f) % 4];
+ drvdata->rgb_frames[f].zone[z + 4] = colors[(z + f) % 4];
+ }
+ drvdata->rgb_frames[f].zone[8] = colors[f];
+ }
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/*
+ * Apply frostfire effect (4 frames: fire vs ice rotating)
+ * Right joystick: fire red -> dark -> ice blue -> dark (clockwise)
+ * Left joystick: ice blue -> dark -> fire red -> dark (counter-clockwise)
+ * ABXY: fire red -> dark -> ice blue -> dark
+ */
+static int claw_apply_frostfire(struct claw_drvdata *drvdata)
+{
+ static const struct rgb_zone colors[] = {
+ {255, 0, 0}, /* fire red */
+ { 0, 0, 0}, /* dark */
+ { 0, 0, 255}, /* ice blue */
+ { 0, 0, 0}, /* dark */
+ };
+ u8 frame_count = ARRAY_SIZE(colors);
+ int f, z;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = frame_count;
+
+ for (f = 0; f < frame_count; f++) {
+ for (z = 0; z < 4; z++) {
+ drvdata->rgb_frames[f].zone[z] = colors[(z + f) % 4];
+ drvdata->rgb_frames[f].zone[z + 4] = colors[(z - f + 6) % 4];
+ }
+ drvdata->rgb_frames[f].zone[8] = colors[f];
+ }
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply current state to device */
+static int claw_apply_rgb_state(struct claw_drvdata *drvdata)
+{
+ if (!drvdata->rgb_enabled)
+ return claw_apply_disabled(drvdata);
+
+ switch (drvdata->rgb_effect) {
+ case CLAW_RGB_EFFECT_MONOCOLOR:
+ return claw_apply_monocolor(drvdata);
+ case CLAW_RGB_EFFECT_BREATHE:
+ return claw_apply_breathe(drvdata);
+ case CLAW_RGB_EFFECT_CHROMA:
+ return claw_apply_chroma(drvdata);
+ case CLAW_RGB_EFFECT_RAINBOW:
+ return claw_apply_rainbow(drvdata);
+ case CLAW_RGB_EFFECT_FROSTFIRE:
+ return claw_apply_frostfire(drvdata);
+ default:
+ dev_err(&drvdata->hdev->dev, "No supported rgb_effect selected\n");
+ return -EINVAL;
+ }
+}
+
+static void claw_rgb_queue_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, rgb_queue);
+ int ret;
+
+ if (!drvdata)
+ return;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return;
+ }
+
+ ret = claw_apply_rgb_state(drvdata);
+ if (ret)
+ dev_err(&drvdata->hdev->dev, "Failed to apply RGB state: %d\n", ret);
+}
+
+static ssize_t effect_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ ret = sysfs_match_string(claw_rgb_effect_text, buf);
+ if (ret < 0)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->rgb_effect = ret;
+
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+ return count;
+}
+
+static ssize_t effect_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ if (drvdata->rgb_effect >= ARRAY_SIZE(claw_rgb_effect_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", claw_rgb_effect_text[drvdata->rgb_effect]);
+}
+
+static DEVICE_ATTR_RW(effect);
+
+static ssize_t effect_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_rgb_effect_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_rgb_effect_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(effect_index);
+
+static ssize_t enabled_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ bool val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtobool(buf, &val);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->rgb_enabled = val;
+
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+ return count;
+}
+
+static ssize_t enabled_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ return sysfs_emit(buf, "%s\n", drvdata->rgb_enabled ? "true" : "false");
+}
+static DEVICE_ATTR_RW(enabled);
+
+static ssize_t enabled_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "true false\n");
+}
+static DEVICE_ATTR_RO(enabled_index);
+
+static ssize_t speed_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ unsigned int val, speed;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtouint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val > 20)
+ return -EINVAL;
+
+ /* 0 is fastest, invert value for intuitive userspace speed */
+ speed = 20 - val;
+
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->rgb_speed = speed;
+
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+ return count;
+}
+
+static ssize_t speed_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ u8 speed = 20 - drvdata->rgb_speed;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ return sysfs_emit(buf, "%u\n", speed);
+}
+static DEVICE_ATTR_RW(speed);
+
+static ssize_t speed_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "0-20\n");
+}
+static DEVICE_ATTR_RO(speed_range);
+
+static void claw_led_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness _brightness)
+{
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return;
+ }
+
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+}
+
+static struct attribute *claw_rgb_attrs[] = {
+ &dev_attr_effect.attr,
+ &dev_attr_effect_index.attr,
+ &dev_attr_enabled.attr,
+ &dev_attr_enabled_index.attr,
+ &dev_attr_speed.attr,
+ &dev_attr_speed_range.attr,
+ NULL,
+};
+
+static const struct attribute_group claw_rgb_attr_group = {
+ .attrs = claw_rgb_attrs,
+};
+
+static struct mc_subled claw_rgb_subled_info[] = {
+ {
+ .color_index = LED_COLOR_ID_RED,
+ .channel = 0x1,
+ },
+ {
+ .color_index = LED_COLOR_ID_GREEN,
+ .channel = 0x2,
+ },
+ {
+ .color_index = LED_COLOR_ID_BLUE,
+ .channel = 0x3,
+ },
+};
+
+static void cfg_setup_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup);
+ bool gamepad_ready = false, rgb_ready = false, gp_registered, rgb_registered;
+ int ret;
+
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE,
+ NULL, 0, 25);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to read gamepad mode: %d\n", ret);
+ goto prep_rgb;
+ }
+ gamepad_ready = true;
+
+prep_rgb:
+ ret = claw_read_rgb_config(drvdata->hdev);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to read RGB config: %d\n", ret);
+ goto try_gamepad;
+ }
+ rgb_ready = true;
+
+ /* Add sysfs attributes after we get the device state */
+try_gamepad:
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock)
+ /* Pairs with smp_store_release from below */
+ gp_registered = smp_load_acquire(&drvdata->gp_registered);
+
+ if (!gp_registered && gamepad_ready) {
+ ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to create gamepad attrs: %d\n", ret);
+ goto try_rgb;
+ }
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->gp_registered, true);
+ gp_registered = true;
+ }
+ }
+
+try_rgb:
+ /* Add and enable RGB interface once we have the device state */
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock)
+ /* Pairs with smp_store_release from below */
+ rgb_registered = smp_load_acquire(&drvdata->rgb_registered);
+
+ if (!rgb_registered && rgb_ready) {
+ ret = led_classdev_multicolor_register(&drvdata->hdev->dev,
+ &drvdata->led_mc);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev, "Failed to create led device: %d\n", ret);
+ goto update_kobjects;
+ }
+
+ ret = device_add_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev, "Failed to create RGB attrs: %d\n", ret);
+ led_classdev_multicolor_unregister(&drvdata->led_mc);
+ goto update_kobjects;
+ }
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->rgb_registered, true);
+ rgb_registered = true;
+ }
+ }
+
+update_kobjects:
+ if (gp_registered)
+ kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
+ if (rgb_registered)
+ kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE);
+}
+
+static void cfg_resume_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume);
+
+ guard(spinlock_irqsave)(&drvdata->registration_lock);
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered) ||
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ !smp_load_acquire(&drvdata->rgb_registered))
+ schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
+}
+
+static void claw_features_supported(struct claw_drvdata *drvdata)
+{
+ u8 major = (drvdata->bcd_device >> 8) & 0xff;
+ u8 minor = drvdata->bcd_device & 0xff;
+
+ if (major == 0x01) {
+ drvdata->bmap_support = true;
+ if (minor >= 0x66) {
+ drvdata->bmap_addr = button_mapping_addr_new;
+ drvdata->rumble_support = true;
+ drvdata->rgb_addr = rgb_addr_new;
+ } else {
+ drvdata->bmap_addr = button_mapping_addr_old;
+ drvdata->rgb_addr = rgb_addr_old;
+ }
+ return;
+ }
+
+ if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
+ drvdata->bmap_support = true;
+ drvdata->bmap_addr = button_mapping_addr_new;
+ drvdata->rumble_support = true;
+ drvdata->rgb_addr = rgb_addr_new;
+ return;
+ }
+
+ drvdata->rgb_addr = rgb_addr_old;
+}
+
+static int claw_probe(struct hid_device *hdev, u8 ep)
+{
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_device *udev = interface_to_usbdev(intf);
+ struct claw_drvdata *drvdata;
+ int ret;
+
+ drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
+ if (!drvdata)
+ return -ENOMEM;
+
+ drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT;
+ drvdata->rgb_enabled = true;
+ drvdata->hdev = hdev;
+ drvdata->ep = ep;
+
+ /* Determine feature level from firmware version */
+ drvdata->bcd_device = le16_to_cpu(udev->descriptor.bcdDevice);
+ claw_features_supported(drvdata);
+
+ if (!drvdata->bmap_support)
+ dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n");
+
+ /* Device is hardwired and name is guaranteed to be unique */
+ drvdata->led_mc.led_cdev.name = "msi_claw:rgb:joystick_rings";
+ drvdata->led_mc.led_cdev.brightness = 0x50;
+ drvdata->led_mc.led_cdev.max_brightness = 0x64;
+ drvdata->led_mc.led_cdev.color = LED_COLOR_ID_RGB;
+ drvdata->led_mc.led_cdev.brightness_set = claw_led_brightness_set;
+ drvdata->led_mc.num_colors = 3;
+ drvdata->led_mc.subled_info = devm_kmemdup(&hdev->dev, claw_rgb_subled_info,
+ sizeof(claw_rgb_subled_info), GFP_KERNEL);
+ if (!drvdata->led_mc.subled_info)
+ return -ENOMEM;
+
+ mutex_init(&drvdata->cfg_mutex);
+ mutex_init(&drvdata->profile_mutex);
+ mutex_init(&drvdata->rom_mutex);
+ spin_lock_init(&drvdata->registration_lock);
+ spin_lock_init(&drvdata->cmd_lock);
+ spin_lock_init(&drvdata->mode_lock);
+ spin_lock_init(&drvdata->profile_lock);
+ spin_lock_init(&drvdata->frame_lock);
+ spin_lock_init(&drvdata->rumble_lock);
+ init_completion(&drvdata->orphan_ack_complete);
+ init_completion(&drvdata->send_cmd_complete);
+ INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
+ INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
+ INIT_DELAYED_WORK(&drvdata->rgb_queue, &claw_rgb_queue_fn);
+
+ /* For control interface: open the HID transport for sending commands. */
+ ret = hid_hw_open(hdev);
+ if (ret)
+ return ret;
+
+ hid_set_drvdata(hdev, drvdata);
+ schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
+
+ return 0;
+}
+
+static int msi_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ int ret;
+ u8 ep;
+
+ if (!hid_is_usb(hdev)) {
+ ret = -ENODEV;
+ goto err_probe;
+ }
+
+ ret = hid_parse(hdev);
+ if (ret)
+ goto err_probe;
+
+ /* Set quirk to create separate input devices per HID application */
+ hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ goto err_probe;
+
+ /* For non-control interfaces (keyboard/mouse), allow userspace to grab the devices. */
+ ret = get_endpoint_address(hdev);
+ if (ret < 0)
+ goto err_stop_hw;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) {
+ ret = claw_probe(hdev, ep);
+ if (ret)
+ goto err_stop_hw;
+ }
+
+ return 0;
+
+err_stop_hw:
+ hid_hw_stop(hdev);
+err_probe:
+ return dev_err_probe(&hdev->dev, ret, "Failed to init device\n");
+}
+
+static void claw_remove(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ bool gp_registered;
+ bool rgb_registered;
+
+ if (!drvdata)
+ return;
+
+ cancel_delayed_work_sync(&drvdata->cfg_resume);
+ cancel_delayed_work_sync(&drvdata->cfg_setup);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ gp_registered = smp_load_acquire(&drvdata->gp_registered);
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->gp_registered, false);
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ rgb_registered = smp_load_acquire(&drvdata->rgb_registered);
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->rgb_registered, false);
+ }
+
+ if (gp_registered)
+ device_remove_group(&hdev->dev, &claw_gamepad_attr_group);
+
+ if (rgb_registered) {
+ device_remove_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group);
+ led_classdev_multicolor_unregister(&drvdata->led_mc);
+ }
+ cancel_delayed_work_sync(&drvdata->rgb_queue);
+
+ hid_hw_close(hdev);
+}
+
+static void msi_remove(struct hid_device *hdev)
+{
+ int ret;
+ u8 ep;
+
+ /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ goto hw_stop;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ claw_remove(hdev);
+
+hw_stop:
+ hid_hw_stop(hdev);
+}
+
+static int claw_resume(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata)
+ return -ENODEV;
+
+ /* MCU can take up to 500ms to be ready after resume */
+ schedule_delayed_work(&drvdata->cfg_resume, msecs_to_jiffies(500));
+ return 0;
+}
+
+static int msi_resume(struct hid_device *hdev)
+{
+ int ret;
+ u8 ep;
+
+ /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ return 0;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ return claw_resume(hdev);
+
+ return 0;
+}
+
+static int claw_suspend(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata)
+ return -ENODEV;
+
+ cancel_delayed_work_sync(&drvdata->cfg_resume);
+ cancel_delayed_work_sync(&drvdata->cfg_setup);
+ cancel_delayed_work_sync(&drvdata->rgb_queue);
+
+ return 0;
+}
+
+static int msi_suspend(struct hid_device *hdev, pm_message_t msg)
+{
+ int ret;
+ u8 ep;
+
+ /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ return 0;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ return claw_suspend(hdev);
+
+ return 0;
+}
+
+static const struct hid_device_id msi_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_XINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DESKTOP) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_BIOS) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, msi_devices);
+
+static struct hid_driver msi_driver = {
+ .name = "hid-msi",
+ .id_table = msi_devices,
+ .raw_event = msi_raw_event,
+ .probe = msi_probe,
+ .remove = msi_remove,
+ .resume = pm_ptr(msi_resume),
+ .suspend = pm_ptr(msi_suspend),
+};
+module_hid_driver(msi_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Denis Benato <denis.benato@linux.dev>");
+MODULE_AUTHOR("Zhouwang Huang <honjow311@gmail.com>");
+MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
+MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads");
diff --git a/drivers/hid/hid-multitouch.c b/drivers/hid/hid-multitouch.c
index edb37b4c867e..2c41bacab1ca 100644
--- a/drivers/hid/hid-multitouch.c
+++ b/drivers/hid/hid-multitouch.c
@@ -2189,16 +2189,8 @@ static int mt_probe(struct hid_device *hdev, const struct hid_device_id *id)
mt_set_modes(hdev, HID_LATENCY_NORMAL, TOUCHPAD_REPORT_ALL);
- if (td->is_haptic_touchpad) {
- if (hid_haptic_init(hdev, &td->haptic)) {
- dev_warn(&hdev->dev, "Cannot allocate haptic for %s\n",
- hdev->name);
- td->is_haptic_touchpad = false;
- devm_kfree(&hdev->dev, td->haptic);
- }
- } else {
+ if (!td->is_haptic_touchpad)
devm_kfree(&hdev->dev, td->haptic);
- }
return 0;
}
@@ -2722,7 +2714,7 @@ static const struct hid_device_id mt_devices[] = {
HID_ANY_ID) },
/* Hantick */
- { .driver_data = MT_CLS_NSMU,
+ { .driver_data = MT_CLS_WIN_8_FORCE_MULTI_INPUT_NSMU,
HID_DEVICE(BUS_I2C, HID_GROUP_MULTITOUCH_WIN_8,
I2C_VENDOR_ID_HANTICK, I2C_PRODUCT_ID_HANTICK_5288) },
diff --git a/drivers/hid/hid-nintendo.c b/drivers/hid/hid-nintendo.c
index e7302ec01ff1..43e0f2aaea3b 100644
--- a/drivers/hid/hid-nintendo.c
+++ b/drivers/hid/hid-nintendo.c
@@ -609,6 +609,8 @@ struct joycon_ctlr {
unsigned int last_input_report_msecs;
unsigned int last_subcmd_sent_msecs;
unsigned int consecutive_valid_report_deltas;
+ unsigned int subcmd_rate_exhaustions;
+ bool subcmd_rate_relaxed;
/* factory calibration data */
struct joycon_stick_cal left_stick_cal_x;
@@ -841,10 +843,11 @@ static void joycon_wait_for_input_report(struct joycon_ctlr *ctlr)
#define JC_SUBCMD_TX_OFFSET_MS 4
#define JC_SUBCMD_VALID_DELTA_REQ 3
#define JC_SUBCMD_RATE_MAX_ATTEMPTS 25
+#define JC_SUBCMD_RATE_MAX_FAILURES 4
#define JC_SUBCMD_RATE_LIMITER_USB_MS 20
#define JC_SUBCMD_RATE_LIMITER_BT_MS 60
#define JC_SUBCMD_RATE_LIMITER_MS(ctlr) ((ctlr)->hdev->bus == BUS_USB ? JC_SUBCMD_RATE_LIMITER_USB_MS : JC_SUBCMD_RATE_LIMITER_BT_MS)
-static void joycon_enforce_subcmd_rate(struct joycon_ctlr *ctlr)
+static void joycon_enforce_subcmd_rate_strict(struct joycon_ctlr *ctlr)
{
unsigned int current_ms;
unsigned long subcmd_delta;
@@ -872,6 +875,14 @@ static void joycon_enforce_subcmd_rate(struct joycon_ctlr *ctlr)
if (attempts >= JC_SUBCMD_RATE_MAX_ATTEMPTS) {
hid_warn(ctlr->hdev, "%s: exceeded max attempts", __func__);
+
+ if (++ctlr->subcmd_rate_exhaustions == JC_SUBCMD_RATE_MAX_FAILURES) {
+ ctlr->subcmd_rate_relaxed = true;
+ hid_info(ctlr->hdev,
+ "input report cadence does not fit the %d-%dms window; using the legacy subcommand throttle\n",
+ JC_INPUT_REPORT_MIN_DELTA,
+ JC_INPUT_REPORT_MAX_DELTA);
+ }
return;
}
@@ -886,6 +897,32 @@ static void joycon_enforce_subcmd_rate(struct joycon_ctlr *ctlr)
msleep(JC_SUBCMD_TX_OFFSET_MS);
}
+/* The rate limiter as it was before commit d750d1480362, without the report
+ * cadence requirement.
+ */
+static void joycon_enforce_subcmd_rate_legacy(struct joycon_ctlr *ctlr)
+{
+ static const unsigned int max_subcmd_rate_ms = 25;
+ unsigned int current_ms = jiffies_to_msecs(jiffies);
+ unsigned int delta_ms = current_ms - ctlr->last_subcmd_sent_msecs;
+
+ while (delta_ms < max_subcmd_rate_ms &&
+ ctlr->ctlr_state == JOYCON_CTLR_STATE_READ) {
+ joycon_wait_for_input_report(ctlr);
+ current_ms = jiffies_to_msecs(jiffies);
+ delta_ms = current_ms - ctlr->last_subcmd_sent_msecs;
+ }
+ ctlr->last_subcmd_sent_msecs = current_ms;
+}
+
+static void joycon_enforce_subcmd_rate(struct joycon_ctlr *ctlr)
+{
+ if (ctlr->subcmd_rate_relaxed)
+ joycon_enforce_subcmd_rate_legacy(ctlr);
+ else
+ joycon_enforce_subcmd_rate_strict(ctlr);
+}
+
static int joycon_hid_send_sync(struct joycon_ctlr *ctlr, u8 *data, size_t len,
u32 timeout)
{
@@ -1474,7 +1511,6 @@ static void joycon_parse_imu_report(struct joycon_ctlr *ctlr,
dropped_threshold = ctlr->imu_avg_delta_ms * 3 / 2;
dropped_pkts = (delta - min(delta, dropped_threshold)) /
ctlr->imu_avg_delta_ms;
- ctlr->imu_timestamp_us += 1000 * ctlr->imu_avg_delta_ms;
if (dropped_pkts > JC_IMU_DROPPED_PKT_WARNING) {
hid_warn_ratelimited(ctlr->hdev,
"compensating for %u dropped IMU reports\n",
@@ -2162,10 +2198,6 @@ static int joycon_input_create(struct joycon_ctlr *ctlr)
ctlr->input->phys = hdev->phys;
input_set_drvdata(ctlr->input, ctlr);
- ret = input_register_device(ctlr->input);
- if (ret)
- return ret;
-
if (joycon_type_is_right_joycon(ctlr)) {
joycon_config_right_stick(ctlr->input);
joycon_config_buttons(ctlr->input, right_joycon_button_mappings);
@@ -2208,6 +2240,10 @@ static int joycon_input_create(struct joycon_ctlr *ctlr)
if (joycon_has_rumble(ctlr))
joycon_config_rumble(ctlr);
+ ret = input_register_device(ctlr->input);
+ if (ret)
+ return ret;
+
return 0;
}
@@ -2607,7 +2643,12 @@ static int joycon_ctlr_read_handler(struct joycon_ctlr *ctlr, u8 *data,
{
if (data[0] == JC_INPUT_SUBCMD_REPLY || data[0] == JC_INPUT_IMU_DATA ||
data[0] == JC_INPUT_MCU_DATA) {
- if (size >= 12) /* make sure it contains the input report */
+ /*
+ * The whole struct is cast and parsed below, including the
+ * IMU/subcmd union, not just the 12-byte partial header this
+ * used to check for.
+ */
+ if (size >= sizeof(struct joycon_input_report))
joycon_parse_report(ctlr,
(struct joycon_input_report *)data);
}
@@ -2736,14 +2777,14 @@ static int nintendo_hid_probe(struct hid_device *hdev,
ret = joycon_init(hdev);
if (ret) {
hid_err(hdev, "Failed to initialize controller; ret=%d\n", ret);
- goto err_close;
+ goto err_io_stop;
}
/* Initialize the leds */
ret = joycon_leds_create(ctlr);
if (ret) {
hid_err(hdev, "Failed to create leds; ret=%d\n", ret);
- goto err_close;
+ goto err_io_stop;
}
/* Initialize the battery power supply */
@@ -2766,7 +2807,8 @@ static int nintendo_hid_probe(struct hid_device *hdev,
err_ida:
ida_free(&nintendo_player_id_allocator, ctlr->player_id);
-err_close:
+err_io_stop:
+ hid_device_io_stop(hdev);
hid_hw_close(hdev);
err_stop:
hid_hw_stop(hdev);
diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index 20a54f337220..d2ded6b08ce9 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -398,7 +398,7 @@ static int oxp_hid_raw_event_gen_2(struct hid_device *hdev,
* Re-apply our settings after this has been received.
*/
if (data[3] == OXP_EFFECT_MONO_TRUE) {
- mod_delayed_work(system_wq, &drvdata.oxp_mcu_init, msecs_to_jiffies(50));
+ mod_delayed_work(system_dfl_wq, &drvdata.oxp_mcu_init, msecs_to_jiffies(50));
return 0;
}
@@ -788,7 +788,7 @@ static ssize_t map_button_store(struct device *dev,
default:
return -EINVAL;
}
- mod_delayed_work(system_wq, &drvdata.oxp_btn_queue, msecs_to_jiffies(50));
+ mod_delayed_work(system_dfl_wq, &drvdata.oxp_btn_queue, msecs_to_jiffies(50));
return count;
}
@@ -1349,7 +1349,7 @@ static void oxp_rgb_brightness_set(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
led_cdev->brightness = brightness;
- mod_delayed_work(system_wq, &drvdata.oxp_rgb_queue, msecs_to_jiffies(50));
+ mod_delayed_work(system_dfl_wq, &drvdata.oxp_rgb_queue, msecs_to_jiffies(50));
}
static struct attribute *oxp_rgb_attrs[] = {
@@ -1502,7 +1502,7 @@ skip_rgb:
drvdata.rumble_intensity = 5;
INIT_DELAYED_WORK(&drvdata.oxp_mcu_init, oxp_mcu_init_fn);
- mod_delayed_work(system_wq, &drvdata.oxp_mcu_init, msecs_to_jiffies(50));
+ mod_delayed_work(system_dfl_wq, &drvdata.oxp_mcu_init, msecs_to_jiffies(50));
ret = devm_device_add_group(&hdev->dev, &oxp_cfg_attrs_group);
if (ret)
diff --git a/drivers/hid/hid-picolcd_debugfs.c b/drivers/hid/hid-picolcd_debugfs.c
index 085847a92e07..1f7dfe60e9ba 100644
--- a/drivers/hid/hid-picolcd_debugfs.c
+++ b/drivers/hid/hid-picolcd_debugfs.c
@@ -99,6 +99,15 @@ static ssize_t picolcd_debug_eeprom_read(struct file *f, char __user *u,
ret = resp->raw_data[2];
if (ret > s)
ret = s;
+ /*
+ * raw_data[2] is a device-supplied length; also clamp it to
+ * what picolcd_raw_event() actually stored (raw_size), or a
+ * hostile device overruns the raw_data[] buffer.
+ */
+ if (ret > resp->raw_size - 3)
+ ret = resp->raw_size - 3;
+ if (ret < 0)
+ ret = 0;
if (copy_to_user(u, resp->raw_data+3, ret))
ret = -EFAULT;
else
diff --git a/drivers/hid/hid-pl.c b/drivers/hid/hid-pl.c
index 8bba29ef6c7a..cae56a6c941b 100644
--- a/drivers/hid/hid-pl.c
+++ b/drivers/hid/hid-pl.c
@@ -62,15 +62,13 @@ static int hid_plff_play(struct input_dev *dev, void *data,
return 0;
}
-static int plff_init(struct hid_device *hid)
+static int pl_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct plff_device *plff;
struct hid_report *report;
- struct hid_input *hidinput;
struct list_head *report_list =
&hid->report_enum[HID_OUTPUT_REPORT].report_list;
- struct list_head *report_ptr = report_list;
- struct input_dev *dev;
+ struct input_dev *dev = hidinput->input;
int error;
s32 maxval;
s32 *strong;
@@ -83,89 +81,80 @@ static int plff_init(struct hid_device *hid)
The input reports also contain a field which contains
8 ff00.0001 usages and 8 boolean values. Their meaning is
currently unknown.
-
+
A version of the 0e8f:0003 exists that has all the values in
separate fields and misses the extra input field, thus resembling
Zeroplus (hid-zpff) devices.
*/
- if (list_empty(report_list)) {
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
+
+ report = list_first_entry_or_null(report_list, struct hid_report, list);
+ if (!report) {
hid_err(hid, "no output reports found\n");
return -ENODEV;
}
+ if (report->maxfield < 1) {
+ hid_err(hid, "no fields in the report\n");
+ return -ENODEV;
+ }
- list_for_each_entry(hidinput, &hid->inputs, list) {
-
- report_ptr = report_ptr->next;
-
- if (report_ptr == report_list) {
- hid_err(hid, "required output report is missing\n");
- return -ENODEV;
- }
-
- report = list_entry(report_ptr, struct hid_report, list);
- if (report->maxfield < 1) {
- hid_err(hid, "no fields in the report\n");
- return -ENODEV;
- }
-
- maxval = 0x7f;
- if (report->field[0]->report_count >= 4) {
- report->field[0]->value[0] = 0x00;
- report->field[0]->value[1] = 0x00;
- strong = &report->field[0]->value[2];
- weak = &report->field[0]->value[3];
- hid_dbg(hid, "detected single-field device");
- } else if (report->field[0]->maxusage == 1 &&
- report->field[0]->usage[0].hid ==
- (HID_UP_LED | 0x43) &&
- report->maxfield >= 4 &&
- report->field[0]->report_count >= 1 &&
- report->field[1]->report_count >= 1 &&
- report->field[2]->report_count >= 1 &&
- report->field[3]->report_count >= 1) {
- report->field[0]->value[0] = 0x00;
- report->field[1]->value[0] = 0x00;
- strong = &report->field[2]->value[0];
- weak = &report->field[3]->value[0];
- if (hid->vendor == USB_VENDOR_ID_JESS2)
- maxval = 0xff;
- hid_dbg(hid, "detected 4-field device");
- } else {
- hid_err(hid, "not enough fields or values\n");
- return -ENODEV;
- }
-
- plff = kzalloc_obj(struct plff_device);
- if (!plff)
- return -ENOMEM;
-
- dev = hidinput->input;
-
- set_bit(FF_RUMBLE, dev->ffbit);
-
- error = input_ff_create_memless(dev, plff, hid_plff_play);
- if (error) {
- kfree(plff);
- return error;
- }
-
- plff->report = report;
- plff->strong = strong;
- plff->weak = weak;
- plff->maxval = maxval;
-
- *strong = 0x00;
- *weak = 0x00;
- hid_hw_request(hid, plff->report, HID_REQ_SET_REPORT);
+ maxval = 0x7f;
+ if (report->field[0]->report_count >= 4) {
+ report->field[0]->value[0] = 0x00;
+ report->field[0]->value[1] = 0x00;
+ strong = &report->field[0]->value[2];
+ weak = &report->field[0]->value[3];
+ hid_dbg(hid, "detected single-field device");
+ } else if (report->field[0]->maxusage == 1 &&
+ report->field[0]->usage[0].hid ==
+ (HID_UP_LED | 0x43) &&
+ report->maxfield >= 4 &&
+ report->field[0]->report_count >= 1 &&
+ report->field[1]->report_count >= 1 &&
+ report->field[2]->report_count >= 1 &&
+ report->field[3]->report_count >= 1) {
+ report->field[0]->value[0] = 0x00;
+ report->field[1]->value[0] = 0x00;
+ strong = &report->field[2]->value[0];
+ weak = &report->field[3]->value[0];
+ if (hid->vendor == USB_VENDOR_ID_JESS2)
+ maxval = 0xff;
+ hid_dbg(hid, "detected 4-field device");
+ } else {
+ hid_err(hid, "not enough fields or values\n");
+ return -ENODEV;
}
- hid_info(hid, "Force feedback for PantherLord/GreenAsia devices by Anssi Hannula <anssi.hannula@gmail.com>\n");
+ plff = kzalloc_obj(struct plff_device);
+ if (!plff)
+ return -ENOMEM;
+
+ dev = hidinput->input;
+
+ set_bit(FF_RUMBLE, dev->ffbit);
+
+ error = input_ff_create_memless(dev, plff, hid_plff_play);
+ if (error) {
+ kfree(plff);
+ return error;
+ }
+
+ plff->report = report;
+ plff->strong = strong;
+ plff->weak = weak;
+ plff->maxval = maxval;
+
+ *strong = 0x00;
+ *weak = 0x00;
+ hid_hw_request(hid, plff->report, HID_REQ_SET_REPORT);
return 0;
}
#else
-static inline int plff_init(struct hid_device *hid)
+static inline int pl_input_configured(struct hid_device *hid,
+ struct hid_input *hidinput)
{
return 0;
}
@@ -181,27 +170,17 @@ static int pl_probe(struct hid_device *hdev, const struct hid_device_id *id)
ret = hid_parse(hdev);
if (ret) {
hid_err(hdev, "parse failed\n");
- goto err;
+ return ret;
}
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (ret) {
hid_err(hdev, "hw start failed\n");
- goto err;
+ return ret;
}
- ret = plff_init(hdev);
- if (ret)
- goto stop;
-
return 0;
-
-stop:
- hid_hw_stop(hdev);
-err:
- return ret;
}
-
static const struct hid_device_id pl_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_GAMERON, USB_DEVICE_ID_GAMERON_DUAL_PSX_ADAPTOR),
.driver_data = 1 }, /* Twin USB Joystick */
@@ -217,6 +196,7 @@ static struct hid_driver pl_driver = {
.name = "pantherlord",
.id_table = pl_devices,
.probe = pl_probe,
+ .input_configured = pl_input_configured,
};
module_hid_driver(pl_driver);
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 57d8efdd9b89..8a0b51d47040 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -45,6 +45,7 @@ static const struct hid_device_id hid_quirks[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_ATEN, USB_DEVICE_ID_ATEN_CS682), HID_QUIRK_NOGET },
{ HID_USB_DEVICE(USB_VENDOR_ID_ATEN, USB_DEVICE_ID_ATEN_CS692), HID_QUIRK_NOGET },
{ HID_USB_DEVICE(USB_VENDOR_ID_ATEN, USB_DEVICE_ID_ATEN_UC100KM), HID_QUIRK_NOGET },
+ { HID_USB_DEVICE(USB_VENDOR_ID_BEITONG, USB_DEVICE_ID_BEITONG_KP20D), HID_QUIRK_ALWAYS_POLL },
{ HID_USB_DEVICE(USB_VENDOR_ID_CHICONY, USB_DEVICE_ID_CHICONY_MULTI_TOUCH), HID_QUIRK_MULTI_INPUT },
{ HID_USB_DEVICE(USB_VENDOR_ID_CHICONY, USB_DEVICE_ID_CHICONY_PIXART_USB_OPTICAL_MOUSE), HID_QUIRK_ALWAYS_POLL },
{ HID_USB_DEVICE(USB_VENDOR_ID_CHICONY, USB_DEVICE_ID_CHICONY_PIXART_USB_OPTICAL_MOUSE2), HID_QUIRK_ALWAYS_POLL },
@@ -112,6 +113,8 @@ static const struct hid_device_id hid_quirks[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_HP, USB_PRODUCT_ID_HP_PIXART_OEM_USB_OPTICAL_MOUSE_1f4a), HID_QUIRK_ALWAYS_POLL },
{ HID_USB_DEVICE(USB_VENDOR_ID_IDEACOM, USB_DEVICE_ID_IDEACOM_IDC6680), HID_QUIRK_MULTI_INPUT },
{ HID_USB_DEVICE(USB_VENDOR_ID_INNOMEDIA, USB_DEVICE_ID_INNEX_GENESIS_ATARI), HID_QUIRK_MULTI_INPUT },
+ { HID_USB_DEVICE(USB_VENDOR_ID_JESS, USB_DEVICE_ID_AULA_MINI_60_HE_PRO),
+ HID_QUIRK_ALWAYS_POLL },
{ HID_USB_DEVICE(USB_VENDOR_ID_KYE, USB_DEVICE_ID_PIXART_USB_OPTICAL_MOUSE_ID2), HID_QUIRK_ALWAYS_POLL },
{ HID_USB_DEVICE(USB_VENDOR_ID_KYE, USB_DEVICE_ID_KYE_EASYPEN_M406), HID_QUIRK_MULTI_INPUT },
{ HID_USB_DEVICE(USB_VENDOR_ID_KYE, USB_DEVICE_ID_KYE_EASYPEN_M506), HID_QUIRK_MULTI_INPUT },
@@ -747,8 +750,22 @@ static const struct hid_device_id hid_have_special_driver[] = {
#endif
#if IS_ENABLED(CONFIG_HID_STEELSERIES)
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1) },
- { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X) },
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2026) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_3) },
#endif
#if IS_ENABLED(CONFIG_HID_SUNPLUS)
{ HID_USB_DEVICE(USB_VENDOR_ID_SUNPLUS, USB_DEVICE_ID_SUNPLUS_WDESKTOP) },
diff --git a/drivers/hid/hid-rapoo.c b/drivers/hid/hid-rapoo.c
index 4c81f3086de4..5c9c396fabf7 100644
--- a/drivers/hid/hid-rapoo.c
+++ b/drivers/hid/hid-rapoo.c
@@ -36,7 +36,7 @@ static int rapoo_probe(struct hid_device *hdev, const struct hid_device_id *id)
return ret;
}
- if (hdev->bus == BUS_USB) {
+ if (hid_is_usb(hdev)) {
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
if (intf->cur_altsetting->desc.bInterfaceNumber != 1)
diff --git a/drivers/hid/hid-rmi.c b/drivers/hid/hid-rmi.c
index d4af17fdba46..2bd781f1e0f5 100644
--- a/drivers/hid/hid-rmi.c
+++ b/drivers/hid/hid-rmi.c
@@ -365,7 +365,7 @@ static int rmi_check_sanity(struct hid_device *hdev, u8 *data, int size)
* such reports here.
*/
- while ((data[valid_size - 1] == 0xff) && valid_size > 0)
+ while (valid_size > 0 && data[valid_size - 1] == 0xff)
valid_size--;
return valid_size;
diff --git a/drivers/hid/hid-roccat-kone.c b/drivers/hid/hid-roccat-kone.c
index 58654cf78f0d..3dae9eaa0b6f 100644
--- a/drivers/hid/hid-roccat-kone.c
+++ b/drivers/hid/hid-roccat-kone.c
@@ -36,6 +36,8 @@ static uint profile_numbers[5] = {0, 1, 2, 3, 4};
static void kone_profile_activated(struct kone_device *kone, uint new_profile)
{
+ if (new_profile < 1 || new_profile > ARRAY_SIZE(kone->profiles))
+ new_profile = 1;
kone->actual_profile = new_profile;
kone->actual_dpi = kone->profiles[new_profile - 1].startup_dpi;
}
@@ -793,8 +795,10 @@ static void kone_keep_values_up_to_date(struct kone_device *kone,
{
switch (event->event) {
case kone_mouse_event_switch_profile:
- kone->actual_dpi = kone->profiles[event->value - 1].
- startup_dpi;
+ if (event->value >= 1 &&
+ event->value <= ARRAY_SIZE(kone->profiles))
+ kone->actual_dpi =
+ kone->profiles[event->value - 1].startup_dpi;
fallthrough;
case kone_mouse_event_osd_profile:
kone->actual_profile = event->value;
@@ -915,3 +919,60 @@ module_exit(kone_exit);
MODULE_AUTHOR("Stefan Achatz");
MODULE_DESCRIPTION("USB Roccat Kone driver");
MODULE_LICENSE("GPL v2");
+
+#if IS_ENABLED(CONFIG_HID_ROCCAT_KONE_KUNIT_TEST)
+#include <kunit/test.h>
+
+/*
+ * Regression test for the out-of-bounds read in
+ * kone_keep_values_up_to_date(): a malicious USB device sends a
+ * "switch profile" HID event (event == kone_mouse_event_switch_profile)
+ * with an attacker-chosen value in 0..255, which is used unbounded as
+ * profiles[value - 1]. On an unpatched kernel the attack case triggers a
+ * KASAN slab-out-of-bounds read; the fix must leave actual_dpi unchanged.
+ */
+static void kone_profile_index_oob_test(struct kunit *test)
+{
+ struct kone_device *kone;
+ struct kone_mouse_event ev = {};
+ /*
+ * Allocate only up to the end of profiles[] so that any index past
+ * the 5-element array is IMMEDIATELY out of bounds and lands in the
+ * KASAN redzone (a far over-read would hit unrelated valid memory and
+ * escape KASAN).
+ */
+ size_t sz = offsetof(struct kone_device, profiles) +
+ sizeof(kone->profiles);
+
+ kone = kunit_kzalloc(test, sz, GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, kone);
+ kone->profiles[0].startup_dpi = 0x42;
+
+ /* benign control: a valid in-range value drives the SAME path and
+ * must succeed (proves the trigger reaches the real code).
+ */
+ ev.event = kone_mouse_event_switch_profile;
+ ev.value = 1;
+ kone_keep_values_up_to_date(kone, &ev);
+ KUNIT_EXPECT_EQ(test, kone->actual_dpi, 0x42);
+
+ /* attack: value == ARRAY_SIZE(profiles) + 1 reads profiles[5], one
+ * element past the array end -> KASAN slab-out-of-bounds read on an
+ * unpatched kernel. The fix must reject it (actual_dpi unchanged).
+ */
+ ev.value = ARRAY_SIZE(kone->profiles) + 1;
+ kone_keep_values_up_to_date(kone, &ev);
+ KUNIT_EXPECT_EQ(test, kone->actual_dpi, 0x42);
+}
+
+static struct kunit_case kone_test_cases[] = {
+ KUNIT_CASE(kone_profile_index_oob_test),
+ {}
+};
+
+static struct kunit_suite kone_test_suite = {
+ .name = "hid-roccat-kone",
+ .test_cases = kone_test_cases,
+};
+kunit_test_suite(kone_test_suite);
+#endif /* CONFIG_HID_ROCCAT_KONE_KUNIT_TEST */
diff --git a/drivers/hid/hid-roccat.c b/drivers/hid/hid-roccat.c
index d6fff53d4ee7..4f15eb951039 100644
--- a/drivers/hid/hid-roccat.c
+++ b/drivers/hid/hid-roccat.c
@@ -70,6 +70,15 @@ static struct roccat_device *devices[ROCCAT_MAX_DEVICES];
/* protects modifications of devices array */
static DEFINE_MUTEX(devices_lock);
+static void roccat_free_device(struct roccat_device *device)
+{
+ int i;
+
+ for (i = 0; i < ROCCAT_CBUF_SIZE; i++)
+ kfree(device->cbuf[i].value);
+ kfree(device);
+}
+
static ssize_t roccat_read(struct file *file, char __user *buffer,
size_t count, loff_t *ppos)
{
@@ -226,7 +235,7 @@ static int roccat_release(struct inode *inode, struct file *file)
hid_hw_power(device->hid, PM_HINT_NORMAL);
hid_hw_close(device->hid);
} else {
- kfree(device);
+ roccat_free_device(device);
}
}
@@ -374,7 +383,7 @@ void roccat_disconnect(int minor)
hid_hw_close(device->hid);
wake_up_interruptible(&device->wait);
} else {
- kfree(device);
+ roccat_free_device(device);
}
}
EXPORT_SYMBOL_GPL(roccat_disconnect);
diff --git a/drivers/hid/hid-sensor-custom.c b/drivers/hid/hid-sensor-custom.c
index afffea894021..c2b425afd951 100644
--- a/drivers/hid/hid-sensor-custom.c
+++ b/drivers/hid/hid-sensor-custom.c
@@ -609,7 +609,7 @@ static int hid_sensor_custom_add_attributes(struct hid_sensor_custom
&sensor_inst->fields[i].
hid_custom_attribute_group);
if (ret)
- break;
+ goto err_remove_groups;
/* For power or report field store indexes */
if (sensor_inst->fields[i].attribute.attrib_id ==
@@ -621,6 +621,13 @@ static int hid_sensor_custom_add_attributes(struct hid_sensor_custom
}
return ret;
+
+err_remove_groups:
+ while (--i >= 0)
+ sysfs_remove_group(&sensor_inst->pdev->dev.kobj,
+ &sensor_inst->fields[i].hid_custom_attribute_group);
+ kfree(sensor_inst->fields);
+ return ret;
}
static void hid_sensor_custom_remove_attributes(struct hid_sensor_custom *
@@ -1005,26 +1012,26 @@ static int hid_sensor_custom_probe(struct platform_device *pdev)
return ret;
}
- ret = sysfs_create_group(&sensor_inst->pdev->dev.kobj,
- &enable_sensor_attr_group);
+ ret = hid_sensor_custom_add_attributes(sensor_inst);
if (ret)
goto err_remove_callback;
- ret = hid_sensor_custom_add_attributes(sensor_inst);
+ ret = sysfs_create_group(&sensor_inst->pdev->dev.kobj,
+ &enable_sensor_attr_group);
if (ret)
- goto err_remove_group;
+ goto err_remove_attributes;
ret = hid_sensor_custom_dev_if_add(sensor_inst);
if (ret)
- goto err_remove_attributes;
+ goto err_remove_group;
return 0;
-err_remove_attributes:
- hid_sensor_custom_remove_attributes(sensor_inst);
err_remove_group:
sysfs_remove_group(&sensor_inst->pdev->dev.kobj,
&enable_sensor_attr_group);
+err_remove_attributes:
+ hid_sensor_custom_remove_attributes(sensor_inst);
err_remove_callback:
sensor_hub_remove_callback(hsdev, hsdev->usage);
@@ -1042,9 +1049,10 @@ static void hid_sensor_custom_remove(struct platform_device *pdev)
}
hid_sensor_custom_dev_if_remove(sensor_inst);
- hid_sensor_custom_remove_attributes(sensor_inst);
+ /* Remove enable_sensor first as it uses fields via power_state/report_state. */
sysfs_remove_group(&sensor_inst->pdev->dev.kobj,
&enable_sensor_attr_group);
+ hid_sensor_custom_remove_attributes(sensor_inst);
sensor_hub_remove_callback(hsdev, hsdev->usage);
}
diff --git a/drivers/hid/hid-sensor-hub.c b/drivers/hid/hid-sensor-hub.c
index 34f710c465b8..6470a290ebfc 100644
--- a/drivers/hid/hid-sensor-hub.c
+++ b/drivers/hid/hid-sensor-hub.c
@@ -239,12 +239,17 @@ int sensor_hub_get_feature(struct hid_sensor_hub_device *hsdev, u32 report_id,
u32 field_index, int buffer_size, void *buffer)
{
struct hid_report *report;
+ struct hid_field *field;
struct sensor_hub_data *data = hid_get_drvdata(hsdev->hdev);
- int report_size;
+ size_t field_size;
+ size_t report_size;
+ size_t copied = 0;
+ size_t to_copy;
int ret = 0;
- u8 *val_ptr;
- int buffer_index = 0;
- int i;
+ unsigned int i;
+
+ if (!buffer || buffer_size <= 0)
+ return -EINVAL;
memset(buffer, 0, buffer_size);
@@ -258,26 +263,29 @@ int sensor_hub_get_feature(struct hid_sensor_hub_device *hsdev, u32 report_id,
hid_hw_request(hsdev->hdev, report, HID_REQ_GET_REPORT);
hid_hw_wait(hsdev->hdev);
+ field = report->field[field_index];
+
/* calculate number of bytes required to read this field */
- report_size = DIV_ROUND_UP(report->field[field_index]->report_size,
- 8) *
- report->field[field_index]->report_count;
- if (!report_size) {
+ field_size = DIV_ROUND_UP(field->report_size, 8);
+ /* HID core stores each parsed report value in a __s32 slot. */
+ if (!field_size || field_size > sizeof(field->value[0])) {
+ ret = -EINVAL;
+ goto done_proc;
+ }
+ if (field->report_count > SIZE_MAX / field_size) {
ret = -EINVAL;
goto done_proc;
}
- ret = min(report_size, buffer_size);
- val_ptr = (u8 *)report->field[field_index]->value;
- for (i = 0; i < report->field[field_index]->report_count; ++i) {
- if (buffer_index >= ret)
- break;
+ report_size = field_size * field->report_count;
+ report_size = min_t(size_t, report_size, buffer_size);
- memcpy(&((u8 *)buffer)[buffer_index], val_ptr,
- report->field[field_index]->report_size / 8);
- val_ptr += sizeof(__s32);
- buffer_index += (report->field[field_index]->report_size / 8);
+ for (i = 0; i < field->report_count && copied < report_size; ++i) {
+ to_copy = min(field_size, report_size - copied);
+ memcpy(&((u8 *)buffer)[copied], &field->value[i], to_copy);
+ copied += to_copy;
}
+ ret = copied;
done_proc:
mutex_unlock(&data->mutex);
diff --git a/drivers/hid/hid-sjoy.c b/drivers/hid/hid-sjoy.c
index 963c45113204..193ab2a6146e 100644
--- a/drivers/hid/hid-sjoy.c
+++ b/drivers/hid/hid-sjoy.c
@@ -48,68 +48,56 @@ static int hid_sjoyff_play(struct input_dev *dev, void *data,
return 0;
}
-static int sjoyff_init(struct hid_device *hid)
+static int sjoy_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct sjoyff_device *sjoyff;
struct hid_report *report;
- struct hid_input *hidinput;
struct list_head *report_list =
&hid->report_enum[HID_OUTPUT_REPORT].report_list;
- struct list_head *report_ptr = report_list;
- struct input_dev *dev;
+ struct input_dev *dev = hidinput->input;
int error;
- if (list_empty(report_list)) {
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
+
+ report = list_first_entry_or_null(report_list, struct hid_report, list);
+ if (!report) {
hid_err(hid, "no output reports found\n");
return -ENODEV;
}
+ if (report->maxfield < 1) {
+ hid_err(hid, "no fields in the report\n");
+ return -ENODEV;
+ }
- list_for_each_entry(hidinput, &hid->inputs, list) {
- report_ptr = report_ptr->next;
-
- if (report_ptr == report_list) {
- hid_err(hid, "required output report is missing\n");
- return -ENODEV;
- }
-
- report = list_entry(report_ptr, struct hid_report, list);
- if (report->maxfield < 1) {
- hid_err(hid, "no fields in the report\n");
- return -ENODEV;
- }
-
- if (report->field[0]->report_count < 3) {
- hid_err(hid, "not enough values in the field\n");
- return -ENODEV;
- }
-
- sjoyff = kzalloc_obj(struct sjoyff_device);
- if (!sjoyff)
- return -ENOMEM;
+ if (report->field[0]->report_count < 3) {
+ hid_err(hid, "not enough values in the field\n");
+ return -ENODEV;
+ }
- dev = hidinput->input;
+ sjoyff = kzalloc_obj(struct sjoyff_device);
+ if (!sjoyff)
+ return -ENOMEM;
- set_bit(FF_RUMBLE, dev->ffbit);
+ set_bit(FF_RUMBLE, dev->ffbit);
- sjoyff->report = report;
- sjoyff->report->field[0]->value[0] = 0x01;
- sjoyff->report->field[0]->value[1] = 0x00;
- sjoyff->report->field[0]->value[2] = 0x00;
- hid_hw_request(hid, sjoyff->report, HID_REQ_SET_REPORT);
+ sjoyff->report = report;
+ sjoyff->report->field[0]->value[0] = 0x01;
+ sjoyff->report->field[0]->value[1] = 0x00;
+ sjoyff->report->field[0]->value[2] = 0x00;
+ hid_hw_request(hid, sjoyff->report, HID_REQ_SET_REPORT);
- error = input_ff_create_memless(dev, sjoyff, hid_sjoyff_play);
- if (error) {
- kfree(sjoyff);
- return error;
- }
+ error = input_ff_create_memless(dev, sjoyff, hid_sjoyff_play);
+ if (error) {
+ kfree(sjoyff);
+ return error;
}
- hid_info(hid, "Force feedback for SmartJoy PLUS PS2/USB adapter\n");
-
return 0;
}
#else
-static inline int sjoyff_init(struct hid_device *hid)
+static inline int sjoy_input_configured(struct hid_device *hid,
+ struct hid_input *hidinput)
{
return 0;
}
@@ -124,20 +112,16 @@ static int sjoy_probe(struct hid_device *hdev, const struct hid_device_id *id)
ret = hid_parse(hdev);
if (ret) {
hid_err(hdev, "parse failed\n");
- goto err;
+ return ret;
}
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (ret) {
hid_err(hdev, "hw start failed\n");
- goto err;
+ return ret;
}
- sjoyff_init(hdev);
-
return 0;
-err:
- return ret;
}
static const struct hid_device_id sjoy_devices[] = {
@@ -165,6 +149,7 @@ static struct hid_driver sjoy_driver = {
.name = "smartjoyplus",
.id_table = sjoy_devices,
.probe = sjoy_probe,
+ .input_configured = sjoy_input_configured,
};
module_hid_driver(sjoy_driver);
diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c
index e75246d29e16..50f5ad6bbeb4 100644
--- a/drivers/hid/hid-sony.c
+++ b/drivers/hid/hid-sony.c
@@ -29,6 +29,7 @@
* There will be no PIN request from the device.
*/
+#include <linux/cleanup.h>
#include <linux/device.h>
#include <linux/hid.h>
#include <linux/module.h>
@@ -81,6 +82,7 @@
#define SONY_FF_SUPPORT (SIXAXIS_CONTROLLER | MOTION_CONTROLLER)
#define SONY_BT_DEVICE (SIXAXIS_CONTROLLER_BT | MOTION_CONTROLLER_BT | NAVIGATION_CONTROLLER_BT)
#define NSG_MRXU_REMOTE (NSG_MR5U_REMOTE_BT | NSG_MR7U_REMOTE_BT)
+#define RB4_GUITAR_PS4 (RB4_GUITAR_PS4_USB | RB4_GUITAR_PS4_BT)
#define MAX_LEDS 4
#define NSG_MRXU_MAX_X 1667
@@ -503,7 +505,7 @@ struct motion_output_report_02 {
u8 r, g, b;
u8 zero2;
u8 rumble;
-};
+} __packed;
static_assert(sizeof(struct motion_output_report_02) == 7);
#define SIXAXIS_REPORT_0xF2_SIZE 17
@@ -521,10 +523,6 @@ static DEFINE_SPINLOCK(sony_dev_list_lock);
static LIST_HEAD(sony_device_list);
static DEFINE_IDA(sony_device_id_allocator);
-enum sony_worker {
- SONY_WORKER_STATE
-};
-
struct sony_sc {
spinlock_t lock;
struct list_head list_node;
@@ -534,6 +532,7 @@ struct sony_sc {
struct input_dev *sensor_dev;
struct led_classdev *leds[MAX_LEDS];
unsigned long quirks;
+ int (*raw_event)(struct sony_sc *sc, u8 *rd, int size);
struct work_struct state_worker;
void (*send_output_report)(struct sony_sc *sc);
struct power_supply *battery;
@@ -566,19 +565,11 @@ struct sony_sc {
static void sony_set_leds(struct sony_sc *sc);
-static inline void sony_schedule_work(struct sony_sc *sc,
- enum sony_worker which)
+static inline void sony_schedule_work(struct sony_sc *sc)
{
- unsigned long flags;
-
- switch (which) {
- case SONY_WORKER_STATE:
- spin_lock_irqsave(&sc->lock, flags);
- if (!sc->defer_initialization && sc->state_worker_initialized)
- schedule_work(&sc->state_worker);
- spin_unlock_irqrestore(&sc->lock, flags);
- break;
- }
+ guard(spinlock_irqsave)(&sc->lock);
+ if (!sc->defer_initialization && sc->state_worker_initialized)
+ schedule_work(&sc->state_worker);
}
static void ghl_magic_poke_cb(struct urb *urb)
@@ -946,15 +937,39 @@ static const u8 *sony_report_fixup(struct hid_device *hdev, u8 *rdesc,
return rdesc;
}
-static void sixaxis_parse_report(struct sony_sc *sc, u8 *rd, int size)
+static int sixaxis_raw_event(struct sony_sc *sc, u8 *rd, int size)
{
static const u8 sixaxis_battery_capacity[] = { 0, 1, 25, 50, 75, 100 };
- unsigned long flags;
int offset;
u8 index;
u8 battery_capacity;
int battery_status;
+ if (unlikely(size != 49 || rd[0] != 0x01))
+ return 0;
+
+ if (sc->quirks & SIXAXIS_CONTROLLER) {
+ /*
+ * When connected via Bluetooth the Sixaxis occasionally sends
+ * a report with the second byte 0xff and the rest zeroed.
+ *
+ * This report does not reflect the actual state of the
+ * controller must be ignored to avoid generating false input
+ * events.
+ */
+ if (rd[1] == 0xff)
+ return -EINVAL;
+
+ /*
+ * Sixaxis HID report has acclerometers/gyro with MSByte first, this
+ * has to be BYTE_SWAPPED before passing up to joystick interface
+ */
+ swap(rd[41], rd[42]);
+ swap(rd[43], rd[44]);
+ swap(rd[45], rd[46]);
+ swap(rd[47], rd[48]);
+ }
+
/*
* The sixaxis is charging if the battery value is 0xee
* and it is fully charged if the value is 0xef.
@@ -972,10 +987,10 @@ static void sixaxis_parse_report(struct sony_sc *sc, u8 *rd, int size)
battery_status = POWER_SUPPLY_STATUS_DISCHARGING;
}
- spin_lock_irqsave(&sc->lock, flags);
- sc->battery_capacity = battery_capacity;
- sc->battery_status = battery_status;
- spin_unlock_irqrestore(&sc->lock, flags);
+ scoped_guard(spinlock_irqsave, &sc->lock) {
+ sc->battery_capacity = battery_capacity;
+ sc->battery_status = battery_status;
+ }
if (sc->quirks & SIXAXIS_CONTROLLER) {
int val;
@@ -993,13 +1008,18 @@ static void sixaxis_parse_report(struct sony_sc *sc, u8 *rd, int size)
input_sync(sc->sensor_dev);
}
+
+ return 0;
}
-static void nsg_mrxu_parse_report(struct sony_sc *sc, u8 *rd, int size)
+static int nsg_mrxu_raw_event(struct sony_sc *sc, u8 *rd, int size)
{
int n, offset, relx, rely;
u8 active;
+ if (unlikely(size < 12 || rd[0] != 0x02))
+ return 0;
+
/*
* The NSG-MRxU multi-touch trackpad data starts at offset 1 and
* the touch-related data starts at offset 2.
@@ -1067,10 +1087,33 @@ static void nsg_mrxu_parse_report(struct sony_sc *sc, u8 *rd, int size)
input_mt_sync_frame(sc->touchpad);
input_sync(sc->touchpad);
+ return 0;
+}
+
+static int rb3_pro_instrument_raw_event(struct sony_sc *sc, u8 *rd, int size)
+{
+ /* Rock Band 3 PS3 Pro instruments set rd[24] to 0xE0 when they're
+ * sending full reports, and 0x02 when only sending navigation.
+ */
+ if (size < 25 || rd[24] != 0x02)
+ return 0;
+
+ /* Only attempt to enable full report every 8 seconds */
+ if (time_after(jiffies, sc->rb3_pro_poke_jiffies)) {
+ sc->rb3_pro_poke_jiffies = jiffies + secs_to_jiffies(8);
+ rb3_pro_instrument_enable_full_report(sc);
+ }
+
+ return 0;
}
-static void rb4_ps4_guitar_parse_report(struct sony_sc *sc, u8 *rd, int size)
+static int rb4_ps4_guitar_raw_event(struct sony_sc *sc, u8 *rd, int size)
{
+ const int expected_size = (sc->quirks & RB4_GUITAR_PS4_BT) ? 78 : 64;
+
+ if (unlikely(size != expected_size || rd[0] != 0x01))
+ return 0;
+
/*
* Rock Band 4 PS4 guitars have whammy and
* tilt functionality, they're located at
@@ -1084,15 +1127,18 @@ static void rb4_ps4_guitar_parse_report(struct sony_sc *sc, u8 *rd, int size)
input_report_abs(sc->input_dev, ABS_RZ, rd[45]);
input_sync(sc->input_dev);
+ return 0;
}
-static void rb4_ps5_guitar_parse_report(struct sony_sc *sc, u8 *rd, int size)
+static int rb4_ps5_guitar_raw_event(struct sony_sc *sc, u8 *rd, int size)
{
u8 charging_status;
u8 battery_data;
u8 battery_capacity;
u8 battery_status;
- unsigned long flags;
+
+ if (unlikely(size != 64 || rd[0] != 0x01))
+ return 0;
/*
* Rock Band 4 PS5 guitars have whammy and
@@ -1132,73 +1178,30 @@ static void rb4_ps5_guitar_parse_report(struct sony_sc *sc, u8 *rd, int size)
break;
}
- spin_lock_irqsave(&sc->lock, flags);
- sc->battery_capacity = battery_capacity;
- sc->battery_status = battery_status;
- spin_unlock_irqrestore(&sc->lock, flags);
+ scoped_guard(spinlock_irqsave, &sc->lock) {
+ sc->battery_capacity = battery_capacity;
+ sc->battery_status = battery_status;
+ }
input_sync(sc->input_dev);
+ return 0;
}
static int sony_raw_event(struct hid_device *hdev, struct hid_report *report,
u8 *rd, int size)
{
struct sony_sc *sc = hid_get_drvdata(hdev);
+ int ret;
- /*
- * Sixaxis HID report has acclerometers/gyro with MSByte first, this
- * has to be BYTE_SWAPPED before passing up to joystick interface
- */
- if ((sc->quirks & SIXAXIS_CONTROLLER) && rd[0] == 0x01 && size == 49) {
- /*
- * When connected via Bluetooth the Sixaxis occasionally sends
- * a report with the second byte 0xff and the rest zeroed.
- *
- * This report does not reflect the actual state of the
- * controller must be ignored to avoid generating false input
- * events.
- */
- if (rd[1] == 0xff)
- return -EINVAL;
-
- swap(rd[41], rd[42]);
- swap(rd[43], rd[44]);
- swap(rd[45], rd[46]);
- swap(rd[47], rd[48]);
-
- sixaxis_parse_report(sc, rd, size);
- } else if ((sc->quirks & MOTION_CONTROLLER_BT) && rd[0] == 0x01 && size == 49) {
- sixaxis_parse_report(sc, rd, size);
- } else if ((sc->quirks & NAVIGATION_CONTROLLER) && rd[0] == 0x01 && size == 49) {
- sixaxis_parse_report(sc, rd, size);
- } else if ((sc->quirks & NSG_MRXU_REMOTE) && rd[0] == 0x02 && size >= 12) {
- nsg_mrxu_parse_report(sc, rd, size);
- return 1;
- } else if ((sc->quirks & RB4_GUITAR_PS4_USB) && rd[0] == 0x01 && size == 64) {
- rb4_ps4_guitar_parse_report(sc, rd, size);
- return 1;
- } else if ((sc->quirks & RB4_GUITAR_PS4_BT) && rd[0] == 0x01 && size == 78) {
- rb4_ps4_guitar_parse_report(sc, rd, size);
- return 1;
- } else if ((sc->quirks & RB4_GUITAR_PS5) && rd[0] == 0x01 && size == 64) {
- rb4_ps5_guitar_parse_report(sc, rd, size);
- return 1;
- }
-
- /* Rock Band 3 PS3 Pro instruments set rd[24] to 0xE0 when they're
- * sending full reports, and 0x02 when only sending navigation.
- */
- if ((sc->quirks & RB3_PRO_INSTRUMENT) && size >= 25 && rd[24] == 0x02) {
- /* Only attempt to enable full report every 8 seconds */
- if (time_after(jiffies, sc->rb3_pro_poke_jiffies)) {
- sc->rb3_pro_poke_jiffies = jiffies + secs_to_jiffies(8);
- rb3_pro_instrument_enable_full_report(sc);
- }
+ if (sc->raw_event) {
+ ret = sc->raw_event(sc, rd, size);
+ if (unlikely(ret < 0))
+ return ret;
}
- if (sc->defer_initialization) {
+ if (unlikely(sc->defer_initialization)) {
sc->defer_initialization = 0;
- sony_schedule_work(sc, SONY_WORKER_STATE);
+ sony_schedule_work(sc);
}
return 0;
@@ -1256,7 +1259,7 @@ static int sony_mapping(struct hid_device *hdev, struct hid_input *hi,
if (sc->quirks & DJH_TURNTABLE)
return djh_turntable_mapping(hdev, hi, field, usage, bit, max);
- if (sc->quirks & (RB4_GUITAR_PS4_USB | RB4_GUITAR_PS4_BT))
+ if (sc->quirks & RB4_GUITAR_PS4)
return rb4_guitar_mapping(hdev, hi, field, usage, bit, max);
if (sc->quirks & RB4_GUITAR_PS5)
@@ -1269,8 +1272,6 @@ static int sony_mapping(struct hid_device *hdev, struct hid_input *hi,
static int sony_register_touchpad(struct sony_sc *sc, int touch_count,
int w, int h, int touch_major, int touch_minor, int orientation)
{
- size_t name_sz;
- char *name;
int ret;
sc->touchpad = devm_input_allocate_device(&sc->hdev->dev);
@@ -1292,12 +1293,10 @@ static int sony_register_touchpad(struct sony_sc *sc, int touch_count,
* a suffix. Other devices which were added later like Sony TV remotes
* inhirited this suffix.
*/
- name_sz = strlen(sc->hdev->name) + sizeof(TOUCHPAD_SUFFIX);
- name = devm_kzalloc(&sc->hdev->dev, name_sz, GFP_KERNEL);
- if (!name)
+ sc->touchpad->name = devm_kasprintf(&sc->hdev->dev, GFP_KERNEL, "%s" TOUCHPAD_SUFFIX,
+ sc->hdev->name);
+ if (!sc->touchpad->name)
return -ENOMEM;
- snprintf(name, name_sz, "%s" TOUCHPAD_SUFFIX, sc->hdev->name);
- sc->touchpad->name = name;
/* We map the button underneath the touchpad to BTN_LEFT. */
__set_bit(EV_KEY, sc->touchpad->evbit);
@@ -1334,8 +1333,6 @@ static int sony_register_touchpad(struct sony_sc *sc, int touch_count,
static int sony_register_sensors(struct sony_sc *sc)
{
- size_t name_sz;
- char *name;
int ret;
sc->sensor_dev = devm_input_allocate_device(&sc->hdev->dev);
@@ -1354,12 +1351,10 @@ static int sony_register_sensors(struct sony_sc *sc)
/* Append a suffix to the controller name as there are various
* DS4 compatible non-Sony devices with different names.
*/
- name_sz = strlen(sc->hdev->name) + sizeof(SENSOR_SUFFIX);
- name = devm_kzalloc(&sc->hdev->dev, name_sz, GFP_KERNEL);
- if (!name)
+ sc->sensor_dev->name = devm_kasprintf(&sc->hdev->dev, GFP_KERNEL, "%s" SENSOR_SUFFIX,
+ sc->hdev->name);
+ if (!sc->sensor_dev->name)
return -ENOMEM;
- snprintf(name, name_sz, "%s" SENSOR_SUFFIX, sc->hdev->name);
- sc->sensor_dev->name = name;
if (sc->quirks & SIXAXIS_CONTROLLER) {
/* For the DS3 we only support the accelerometer, which works
@@ -1507,7 +1502,7 @@ static void buzz_set_leds(struct sony_sc *sc)
static void sony_set_leds(struct sony_sc *sc)
{
if (!(sc->quirks & BUZZ_CONTROLLER))
- sony_schedule_work(sc, SONY_WORKER_STATE);
+ sony_schedule_work(sc);
else
buzz_set_leds(sc);
}
@@ -1618,7 +1613,7 @@ static int sony_led_blink_set(struct led_classdev *led, unsigned long *delay_on,
new_off != drv_data->led_delay_off[n]) {
drv_data->led_delay_on[n] = new_on;
drv_data->led_delay_off[n] = new_off;
- sony_schedule_work(drv_data, SONY_WORKER_STATE);
+ sony_schedule_work(drv_data);
}
return 0;
@@ -1846,7 +1841,7 @@ static int sony_play_effect(struct input_dev *dev, void *data,
sc->left = effect->u.rumble.strong_magnitude / 256;
sc->right = effect->u.rumble.weak_magnitude / 256;
- sony_schedule_work(sc, SONY_WORKER_STATE);
+ sony_schedule_work(sc);
return 0;
}
@@ -1869,15 +1864,14 @@ static int sony_battery_get_property(struct power_supply *psy,
union power_supply_propval *val)
{
struct sony_sc *sc = power_supply_get_drvdata(psy);
- unsigned long flags;
int ret = 0;
u8 battery_capacity;
int battery_status;
- spin_lock_irqsave(&sc->lock, flags);
- battery_capacity = sc->battery_capacity;
- battery_status = sc->battery_status;
- spin_unlock_irqrestore(&sc->lock, flags);
+ scoped_guard(spinlock_irqsave, &sc->lock) {
+ battery_capacity = sc->battery_capacity;
+ battery_status = sc->battery_status;
+ }
switch (psp) {
case POWER_SUPPLY_PROP_PRESENT:
@@ -1959,10 +1953,9 @@ static inline int sony_compare_connection_type(struct sony_sc *sc0,
static int sony_check_add_dev_list(struct sony_sc *sc)
{
struct sony_sc *entry;
- unsigned long flags;
int ret;
- spin_lock_irqsave(&sony_dev_list_lock, flags);
+ guard(spinlock_irqsave)(&sony_dev_list_lock);
list_for_each_entry(entry, &sony_device_list, list_node) {
ret = memcmp(sc->mac_address, entry->mac_address,
@@ -1976,27 +1969,23 @@ static int sony_check_add_dev_list(struct sony_sc *sc)
"controller with MAC address %pMR already connected\n",
sc->mac_address);
}
- goto unlock;
+ goto out;
}
}
ret = 0;
list_add(&(sc->list_node), &sony_device_list);
-unlock:
- spin_unlock_irqrestore(&sony_dev_list_lock, flags);
+out:
return ret;
}
static void sony_remove_dev_list(struct sony_sc *sc)
{
- unsigned long flags;
+ guard(spinlock_irqsave)(&sony_dev_list_lock);
- if (sc->list_node.next) {
- spin_lock_irqsave(&sony_dev_list_lock, flags);
- list_del(&(sc->list_node));
- spin_unlock_irqrestore(&sony_dev_list_lock, flags);
- }
+ if (!list_empty(&sc->list_node))
+ list_del_init(&sc->list_node);
}
static int sony_get_bt_devaddr(struct sony_sc *sc)
@@ -2110,6 +2099,12 @@ static void sony_release_device_id(struct sony_sc *sc)
}
}
+static inline void sony_init_raw_event_handler(struct sony_sc *sc,
+ int (*raw_event)(struct sony_sc *, u8 *, int))
+{
+ sc->raw_event = raw_event;
+}
+
static inline void sony_init_output_report(struct sony_sc *sc,
void (*send_output_report)(struct sony_sc *))
{
@@ -2123,16 +2118,21 @@ static inline void sony_init_output_report(struct sony_sc *sc,
static inline void sony_cancel_work_sync(struct sony_sc *sc)
{
- unsigned long flags;
-
if (sc->state_worker_initialized) {
- spin_lock_irqsave(&sc->lock, flags);
- sc->state_worker_initialized = 0;
- spin_unlock_irqrestore(&sc->lock, flags);
+ scoped_guard(spinlock_irqsave, &sc->lock) {
+ sc->state_worker_initialized = 0;
+ }
cancel_work_sync(&sc->state_worker);
}
}
+static void sony_cleanup(struct sony_sc *sc)
+{
+ sony_cancel_work_sync(sc);
+ sony_remove_dev_list(sc);
+ sony_release_device_id(sc);
+}
+
static int sony_input_configured(struct hid_device *hdev,
struct hid_input *hidinput)
{
@@ -2185,6 +2185,7 @@ static int sony_input_configured(struct hid_device *hdev,
goto err_stop;
}
+ sony_init_raw_event_handler(sc, sixaxis_raw_event);
sony_init_output_report(sc, sixaxis_send_output_report);
} else if (sc->quirks & NAVIGATION_CONTROLLER_BT) {
/*
@@ -2199,6 +2200,7 @@ static int sony_input_configured(struct hid_device *hdev,
goto err_stop;
}
+ sony_init_raw_event_handler(sc, sixaxis_raw_event);
sony_init_output_report(sc, sixaxis_send_output_report);
} else if (sc->quirks & RB3_PRO_INSTRUMENT) {
/*
@@ -2213,6 +2215,8 @@ static int sony_input_configured(struct hid_device *hdev,
*/
hdev->quirks |= HID_QUIRK_NO_OUTPUT_REPORTS_ON_INTR_EP;
hdev->quirks |= HID_QUIRK_SKIP_OUTPUT_REPORT_ID;
+
+ sony_init_raw_event_handler(sc, rb3_pro_instrument_raw_event);
} else if (sc->quirks & SIXAXIS_CONTROLLER_USB) {
/*
* The Sony Sixaxis does not handle HID Output Reports on the
@@ -2237,6 +2241,7 @@ static int sony_input_configured(struct hid_device *hdev,
goto err_stop;
}
+ sony_init_raw_event_handler(sc, sixaxis_raw_event);
sony_init_output_report(sc, sixaxis_send_output_report);
} else if (sc->quirks & SIXAXIS_CONTROLLER_BT) {
/*
@@ -2258,6 +2263,7 @@ static int sony_input_configured(struct hid_device *hdev,
goto err_stop;
}
+ sony_init_raw_event_handler(sc, sixaxis_raw_event);
sony_init_output_report(sc, sixaxis_send_output_report);
} else if (sc->quirks & NSG_MRXU_REMOTE) {
/*
@@ -2273,8 +2279,15 @@ static int sony_input_configured(struct hid_device *hdev,
goto err_stop;
}
+ sony_init_raw_event_handler(sc, nsg_mrxu_raw_event);
} else if (sc->quirks & MOTION_CONTROLLER) {
+ if (sc->quirks & MOTION_CONTROLLER_BT)
+ sony_init_raw_event_handler(sc, sixaxis_raw_event);
sony_init_output_report(sc, motion_send_output_report);
+ } else if (sc->quirks & RB4_GUITAR_PS4) {
+ sony_init_raw_event_handler(sc, rb4_ps4_guitar_raw_event);
+ } else if (sc->quirks & RB4_GUITAR_PS5) {
+ sony_init_raw_event_handler(sc, rb4_ps5_guitar_raw_event);
}
if (sc->quirks & SONY_LED_SUPPORT) {
@@ -2306,9 +2319,7 @@ static int sony_input_configured(struct hid_device *hdev,
err_close:
hid_hw_close(hdev);
err_stop:
- sony_cancel_work_sync(sc);
- sony_remove_dev_list(sc);
- sony_release_device_id(sc);
+ sony_cleanup(sc);
return ret;
}
@@ -2332,6 +2343,8 @@ static int sony_probe(struct hid_device *hdev, const struct hid_device_id *id)
return -ENOMEM;
spin_lock_init(&sc->lock);
+ INIT_LIST_HEAD(&sc->list_node);
+ sc->device_id = -1;
sc->quirks = quirks;
hid_set_drvdata(hdev, sc);
@@ -2360,6 +2373,7 @@ static int sony_probe(struct hid_device *hdev, const struct hid_device_id *id)
ret = hid_hw_start(hdev, connect_mask);
if (ret) {
hid_err(hdev, "hw start failed\n");
+ sony_cleanup(sc);
return ret;
}
@@ -2414,7 +2428,7 @@ static int sony_probe(struct hid_device *hdev, const struct hid_device_id *id)
err:
usb_free_urb(sc->ghl_urb);
-
+ sony_cleanup(sc);
hid_hw_stop(hdev);
return ret;
}
@@ -2424,18 +2438,14 @@ static void sony_remove(struct hid_device *hdev)
struct sony_sc *sc = hid_get_drvdata(hdev);
if (sc->quirks & (GHL_GUITAR_PS3WIIU | GHL_GUITAR_PS4)) {
- timer_delete_sync(&sc->ghl_poke_timer);
+ /* poison, not kill: a pending timer must not re-submit during teardown */
+ usb_poison_urb(sc->ghl_urb);
+ timer_shutdown_sync(&sc->ghl_poke_timer);
usb_free_urb(sc->ghl_urb);
}
hid_hw_close(hdev);
-
- sony_cancel_work_sync(sc);
-
- sony_remove_dev_list(sc);
-
- sony_release_device_id(sc);
-
+ sony_cleanup(sc);
hid_hw_stop(hdev);
}
diff --git a/drivers/hid/hid-steam.c b/drivers/hid/hid-steam.c
index 197126d6e081..ac08cb2d0368 100644
--- a/drivers/hid/hid-steam.c
+++ b/drivers/hid/hid-steam.c
@@ -43,11 +43,14 @@
#include <linux/rcupdate.h>
#include <linux/delay.h>
#include <linux/power_supply.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
#include "hid-ids.h"
MODULE_DESCRIPTION("HID driver for Valve Steam Controller");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Rodrigo Rivas Costa <rodrigorivascosta@gmail.com>");
+MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
static bool lizard_mode = true;
@@ -56,6 +59,8 @@ static LIST_HEAD(steam_devices);
#define STEAM_QUIRK_WIRELESS BIT(0)
#define STEAM_QUIRK_DECK BIT(1)
+#define STEAM_QUIRK_IBEX BIT(2)
+#define STEAM_QUIRK_BLE BIT(3)
/* Touch pads are 40 mm in diameter and 65535 units */
#define STEAM_PAD_RESOLUTION 1638
@@ -68,13 +73,14 @@ static LIST_HEAD(steam_devices);
/* Joystick runs are about 5 mm and 32768 units */
#define STEAM_DECK_JOYSTICK_RESOLUTION 6553
/* Accelerometer has 16 bit resolution and a range of +/- 2g */
-#define STEAM_DECK_ACCEL_RES_PER_G 16384
-#define STEAM_DECK_ACCEL_RANGE 32768
+#define STEAM_ACCEL_RES_PER_G 16384
+#define STEAM_ACCEL_RANGE 32768
+#define STEAM_ACCEL_FUZZ 128
#define STEAM_DECK_ACCEL_FUZZ 32
/* Gyroscope has 16 bit resolution and a range of +/- 2000 dps */
-#define STEAM_DECK_GYRO_RES_PER_DPS 16
-#define STEAM_DECK_GYRO_RANGE 32768
-#define STEAM_DECK_GYRO_FUZZ 1
+#define STEAM_GYRO_RES_PER_DPS 16
+#define STEAM_GYRO_RANGE 32768
+#define STEAM_GYRO_FUZZ 0
#define STEAM_PAD_FUZZ 256
@@ -149,7 +155,7 @@ enum {
SETTING_USB_DEBUG_MODE,
SETTING_LEFT_TRACKPAD_MODE,
SETTING_RIGHT_TRACKPAD_MODE,
- SETTING_MOUSE_POINTER_ENABLED,
+ SETTING_LIZARD_MODE,
/* 10 */
SETTING_DPAD_DEADZONE,
@@ -243,14 +249,39 @@ enum {
/* Input report identifiers */
enum
{
- ID_CONTROLLER_STATE = 1,
- ID_CONTROLLER_DEBUG = 2,
- ID_CONTROLLER_WIRELESS = 3,
- ID_CONTROLLER_STATUS = 4,
- ID_CONTROLLER_DEBUG2 = 5,
- ID_CONTROLLER_SECONDARY_STATE = 6,
- ID_CONTROLLER_BLE_STATE = 7,
- ID_CONTROLLER_DECK_STATE = 9
+ ID_CONTROLLER_STATE = 1,
+ ID_CONTROLLER_DEBUG = 2,
+ ID_CONTROLLER_WIRELESS = 3,
+ ID_CONTROLLER_STATUS = 4,
+ ID_CONTROLLER_DEBUG2 = 5,
+ ID_CONTROLLER_SECONDARY_STATE = 6,
+ ID_CONTROLLER_BLE_STATE = 7,
+ ID_CONTROLLER_DECK_STATE = 9,
+};
+
+/* Read-only attributes */
+enum {
+ ATTRIB_UNIQUE_ID, // deprecated
+ ATTRIB_PRODUCT_ID,
+ ATTRIB_PRODUCT_REVISON, // deprecated
+ ATTRIB_CAPABILITIES = ATTRIB_PRODUCT_REVISON, // intentional aliasing
+ ATTRIB_FIRMWARE_VERSION, // deprecated
+ ATTRIB_FIRMWARE_BUILD_TIME,
+ ATTRIB_RADIO_FIRMWARE_BUILD_TIME,
+ ATTRIB_RADIO_DEVICE_ID0,
+ ATTRIB_RADIO_DEVICE_ID1,
+ ATTRIB_DONGLE_FIRMWARE_BUILD_TIME,
+ ATTRIB_HW_ID, // AKA BOARD_REVISION,
+ ATTRIB_BOOTLOADER_BUILD_TIME,
+ ATTRIB_CONNECTION_INTERVAL_IN_US,
+ ATTRIB_SECONDARY_FIRMWARE_BUILD_TIME,
+ ATTRIB_SECONDARY_BOOTLOADER_BUILD_TIME,
+ ATTRIB_SECONDARY_HW_ID, // AKA BOARD_REVISION,
+ ATTRIB_STREAMING,
+ ATTRIB_TRACKPAD_ID,
+ ATTRIB_SECONDARY_TRACKPAD_ID,
+
+ ATTRIB_COUNT
};
/* String attribute identifiers */
@@ -259,14 +290,14 @@ enum {
ATTRIB_STR_UNIT_SERIAL,
};
-/* Values for GYRO_MODE (bitmask) */
+/* Values for IMU_MODE (bitmask) */
enum {
- SETTING_GYRO_MODE_OFF = 0,
- SETTING_GYRO_MODE_STEERING = BIT(0),
- SETTING_GYRO_MODE_TILT = BIT(1),
- SETTING_GYRO_MODE_SEND_ORIENTATION = BIT(2),
- SETTING_GYRO_MODE_SEND_RAW_ACCEL = BIT(3),
- SETTING_GYRO_MODE_SEND_RAW_GYRO = BIT(4),
+ SETTING_IMU_MODE_OFF = 0,
+ SETTING_IMU_MODE_STEERING = BIT(0),
+ SETTING_IMU_MODE_TILT = BIT(1),
+ SETTING_IMU_MODE_SEND_ORIENTATION = BIT(2),
+ SETTING_IMU_MODE_SEND_RAW_ACCEL = BIT(3),
+ SETTING_IMU_MODE_SEND_RAW_GYRO = BIT(4),
};
/* Trackpad modes */
@@ -282,6 +313,83 @@ enum {
TRACKPAD_GESTURE_KEYBOARD,
};
+/* Report identifiers (Ibex only) */
+enum {
+ /* Feature */
+ REPORT_ID_FEATURES_CONTROLLER = 1,
+ REPORT_ID_FEATURES_DONGLE = 2,
+
+ /* Input */
+ REPORT_ID_INPUT = 0x42,
+ REPORT_ID_BATTERY = 0x43,
+ REPORT_ID_INPUT2 = 0x45,
+ REPORT_ID_WIRELESS_EVENT = 0x79,
+
+ /* Output */
+ REPORT_ID_HAPTIC_RUMBLE = 0x80,
+ REPORT_ID_HAPTIC_PULSE = 0x81,
+ REPORT_ID_HAPTIC_COMMAND = 0x82,
+ REPORT_ID_HAPTIC_LFO_TONE = 0x83,
+ REPORT_ID_HAPTIC_LOG_SWEEP = 0x84,
+ REPORT_ID_HAPTIC_SCRIPT = 0x85,
+};
+
+/* Ibex charge state */
+enum {
+ CHARGE_STATE_DISCHARGING = 1,
+ CHARGE_STATE_CHARGING = 2,
+ CHARGE_STATE_CHARGING_DONE = 4,
+};
+
+/* Ibex wireless events */
+enum {
+ WIRELESS_EVENT_DISCONNECT = 1,
+ WIRELESS_EVENT_CONNECT = 2,
+ WIRELESS_EVENT_PAIR = 3,
+};
+
+struct steam_controller_attribute {
+ unsigned char tag;
+ __le32 value;
+} __packed;
+
+struct steam_ibex_battery_status {
+ u8 charge_state;
+ u8 battery_level;
+ __le16 battery_voltage;
+ __le16 system_voltage;
+ __le16 input_voltage;
+ __le16 battery_current;
+ __le16 input_current;
+ __le16 temperature;
+};
+
+struct steam_ibex_haptic_rumble {
+ u8 type;
+ __le16 intensity;
+ struct {
+ __le16 speed;
+ u8 gain;
+ } __packed left, right;
+} __packed;
+static_assert(sizeof(struct steam_ibex_haptic_rumble) == 9);
+
+struct steam_ibex_haptic_pulse {
+ u8 side;
+ __le16 on_us;
+ __le16 off_us;
+ __le16 repeat_count;
+} __packed;
+static_assert(sizeof(struct steam_ibex_haptic_pulse) == 7);
+
+struct steam_ibex_output_report {
+ u8 id;
+ union {
+ struct steam_ibex_haptic_rumble rumble;
+ struct steam_ibex_haptic_pulse pulse;
+ };
+} __packed;
+
/* Pad identifiers for the deck */
#define STEAM_PAD_LEFT 0
#define STEAM_PAD_RIGHT 1
@@ -295,41 +403,57 @@ struct steam_device {
spinlock_t lock;
struct hid_device *hdev, *client_hdev;
struct mutex report_mutex;
+ struct mutex registration_mutex;
unsigned long client_opened;
struct input_dev __rcu *input;
struct input_dev __rcu *sensors;
unsigned long quirks;
struct work_struct work_connect;
bool connected;
+ bool registered;
char serial_no[STEAM_SERIAL_LEN + 1];
struct power_supply_desc battery_desc;
struct power_supply __rcu *battery;
u8 battery_charge;
- u16 voltage;
+ u8 battery_status;
+ u16 battery_temp;
+ u16 battery_voltage;
+ u16 battery_current;
struct delayed_work mode_switch;
bool did_mode_switch;
bool gamepad_mode;
struct work_struct rumble_work;
+ struct delayed_work coalesce_rumble_work;
u16 rumble_left;
u16 rumble_right;
unsigned int sensor_timestamp_us;
- struct work_struct unregister_work;
+ unsigned int sensor_update_rate_us;
};
-static int steam_recv_report(struct steam_device *steam,
- u8 *data, int size)
+static int steam_recv_report_id(struct steam_device *steam,
+ u8 *data, int size, u8 report_id)
{
struct hid_report *r;
u8 *buf;
+ unsigned int retries = 50;
int ret;
+ u32 len;
+
+ /*
+ * All reports start with a two byte header.
+ * We must read at least two bytes to get a sensible output.
+ */
+ if (size < 2)
+ return -EINVAL;
- r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[0];
+ r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[report_id];
if (!r) {
- hid_err(steam->hdev, "No HID_FEATURE_REPORT submitted - nothing to read\n");
+ hid_err(steam->hdev, "No HID_FEATURE_REPORT present for ID %u\n", report_id);
return -EINVAL;
}
- if (hid_report_len(r) < 64)
+ len = hid_report_len(r);
+ if (len < 64)
return -EINVAL;
buf = hid_alloc_report_buf(r, GFP_KERNEL);
@@ -337,53 +461,100 @@ static int steam_recv_report(struct steam_device *steam,
return -ENOMEM;
/*
- * The report ID is always 0, so strip the first byte from the output.
+ * The report ID is consistent, so strip the first byte from the output.
* hid_report_len() is not counting the report ID, so +1 to the length
* or else we get a EOVERFLOW. We are safe from a buffer overflow
* because hid_alloc_report_buf() allocates +7 bytes.
*/
- ret = hid_hw_raw_request(steam->hdev, 0x00,
- buf, hid_report_len(r) + 1,
- HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
- if (ret > 0)
- memcpy(data, buf + 1, min(size, ret - 1));
+ if (!(steam->quirks & STEAM_QUIRK_IBEX))
+ len += 1;
+
+ /*
+ * Sometimes the wireless controller fails with EPIPE
+ * when sending a feature report.
+ * Doing a HID_REQ_GET_REPORT and waiting for a while
+ * seems to fix that.
+ */
+ do {
+ ret = hid_hw_raw_request(steam->hdev, report_id,
+ buf, len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (ret != -EPIPE)
+ break;
+ msleep(20);
+ } while (--retries);
+ if (ret > 0) {
+ /* Remove the report ID from the return buffer */
+ ret--;
+ size = min(size, ret);
+ memcpy(data, buf + 1, size);
+ }
+
kfree(buf);
- return ret;
+ /*
+ * Don't log if the failure is -ENODEV, as this
+ * can happen normally on disconnect.
+ */
+ if (ret < 0 && ret != -ENODEV)
+ hid_err(steam->hdev, "%s: error %d\n", __func__, ret);
+ else if (ret > 0)
+ hid_dbg(steam->hdev, "Received report %*ph\n", size, data);
+ if (ret < 0)
+ return ret;
+
+ if (ret < 2) {
+ hid_err(steam->hdev, "%s: reply too short\n", __func__);
+ return -EPROTO;
+ }
+ if (ret < data[1] + 2) {
+ hid_err(steam->hdev, "%s: expected %u bytes, read %i\n",
+ __func__, data[1] + 2, ret);
+ return -EPROTO;
+ }
+ return size;
}
-static int steam_send_report(struct steam_device *steam,
- u8 *cmd, int size)
+static int steam_send_report_id(struct steam_device *steam,
+ u8 *cmd, int size, u8 report_id)
{
struct hid_report *r;
u8 *buf;
unsigned int retries = 50;
int ret;
+ u32 len;
- r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[0];
+ r = steam->hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[report_id];
if (!r) {
- hid_err(steam->hdev, "No HID_FEATURE_REPORT submitted - nothing to read\n");
+ hid_err(steam->hdev, "No HID_FEATURE_REPORT present for ID %u\n", report_id);
return -EINVAL;
}
- if (hid_report_len(r) < 64)
+ len = hid_report_len(r);
+ if (len < 64)
return -EINVAL;
buf = hid_alloc_report_buf(r, GFP_KERNEL);
if (!buf)
return -ENOMEM;
- /* The report ID is always 0 */
+ /* The report ID is always consistent */
+ buf[0] = report_id;
memcpy(buf + 1, cmd, size);
+ hid_dbg(steam->hdev, "Sending report %*ph\n", size, cmd);
+
+ if (!(steam->quirks & STEAM_QUIRK_IBEX))
+ len += 1;
+
/*
* Sometimes the wireless controller fails with EPIPE
* when sending a feature report.
- * Doing a HID_REQ_GET_REPORT and waiting for a while
+ * Doing a HID_REQ_SET_REPORT and waiting for a while
* seems to fix that.
*/
do {
- ret = hid_hw_raw_request(steam->hdev, 0,
- buf, max(size, 64) + 1,
+ ret = hid_hw_raw_request(steam->hdev, report_id,
+ buf, max(size + 1, len),
HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
if (ret != -EPIPE)
break;
@@ -391,12 +562,29 @@ static int steam_send_report(struct steam_device *steam,
} while (--retries);
kfree(buf);
- if (ret < 0)
+ /*
+ * Don't log if the failure is -ENODEV, as this
+ * can happen normally on disconnect.
+ */
+ if (ret < 0 && ret != -ENODEV)
hid_err(steam->hdev, "%s: error %d (%*ph)\n", __func__,
ret, size, cmd);
return ret;
}
+static int steam_send_report(struct steam_device *steam,
+ u8 *cmd, int size)
+{
+ u8 report_id;
+
+ if (steam->quirks & STEAM_QUIRK_IBEX)
+ report_id = REPORT_ID_FEATURES_CONTROLLER;
+ else
+ report_id = 0;
+
+ return steam_send_report_id(steam, cmd, size, report_id);
+}
+
static inline int steam_send_report_byte(struct steam_device *steam, u8 cmd)
{
return steam_send_report(steam, &cmd, 1);
@@ -409,7 +597,6 @@ static int steam_write_settings(struct steam_device *steam,
u8 reg;
u16 val;
u8 cmd[64] = {ID_SET_SETTINGS_VALUES, 0x00};
- int ret;
va_list args;
va_start(args, steam);
@@ -425,16 +612,56 @@ static int steam_write_settings(struct steam_device *steam,
}
va_end(args);
- ret = steam_send_report(steam, cmd, 2 + cmd[1]);
- if (ret < 0)
- return ret;
+ return steam_send_report(steam, cmd, 2 + cmd[1]);
+}
- /*
- * Sometimes a lingering report for this command can
- * get read back instead of the last set report if
- * this isn't explicitly queried
- */
- return steam_recv_report(steam, cmd, 2 + cmd[1]);
+static int steam_exchange_report_id(struct steam_device *steam, u8 *cmd, int csize,
+ u8 *reply, int rsize, u8 report_id)
+{
+ unsigned int retries = 5;
+ int ret;
+
+ do {
+ ret = steam_send_report_id(steam, cmd, csize, report_id);
+ if (ret < 0)
+ return ret;
+ ret = steam_recv_report_id(steam, reply, rsize, report_id);
+ /*
+ * Sometimes this can fail on the first few tries on the Steam
+ * Controller (2015). It appears to be a firmware bug, and Steam
+ * itself just retries, so we should also retry a few times to
+ * see if we get it.
+ */
+ if (ret == -EPROTO)
+ continue;
+ if (ret < 0) {
+ hid_err(steam->hdev, "%s: error reading reply (%*ph)\n",
+ __func__, csize, cmd);
+ return ret;
+ }
+ if (reply[0] == cmd[0] && reply[1] >= 1)
+ break;
+ if (retries > 0)
+ continue;
+ hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
+ rsize, reply);
+ return -EPROTO;
+ } while (retries--);
+
+ return ret;
+}
+
+static int steam_exchange_report(struct steam_device *steam, u8 *cmd, int csize,
+ u8 *reply, int rsize)
+{
+ u8 report_id;
+
+ if (steam->quirks & STEAM_QUIRK_IBEX)
+ report_id = REPORT_ID_FEATURES_CONTROLLER;
+ else
+ report_id = 0;
+
+ return steam_exchange_report_id(steam, cmd, csize, reply, rsize, report_id);
}
static int steam_get_serial(struct steam_device *steam)
@@ -445,39 +672,74 @@ static int steam_get_serial(struct steam_device *steam)
*/
int ret = 0;
u8 cmd[] = {ID_GET_STRING_ATTRIBUTE, sizeof(steam->serial_no), ATTRIB_STR_UNIT_SERIAL};
- u8 reply[3 + STEAM_SERIAL_LEN + 1];
+ u8 reply[3 + STEAM_SERIAL_LEN + 1] = {0};
- mutex_lock(&steam->report_mutex);
- ret = steam_send_report(steam, cmd, sizeof(cmd));
- if (ret < 0)
- goto out;
- ret = steam_recv_report(steam, reply, sizeof(reply));
+ ret = steam_exchange_report(steam, cmd, sizeof(cmd), reply, sizeof(reply));
if (ret < 0)
- goto out;
- if (reply[0] != ID_GET_STRING_ATTRIBUTE || reply[1] < 1 ||
- reply[1] > sizeof(steam->serial_no) || reply[2] != ATTRIB_STR_UNIT_SERIAL) {
- ret = -EIO;
- goto out;
+ return ret;
+ if (reply[1] > sizeof(steam->serial_no) || reply[2] != ATTRIB_STR_UNIT_SERIAL) {
+ hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
+ (int)sizeof(reply), reply);
+ return -EIO;
}
- reply[3 + STEAM_SERIAL_LEN] = 0;
strscpy(steam->serial_no, reply + 3, reply[1]);
-out:
- mutex_unlock(&steam->report_mutex);
return ret;
}
-/*
- * This command requests the wireless adaptor to post an event
- * with the connection status. Useful if this driver is loaded when
- * the controller is already connected.
- */
-static inline int steam_request_conn_status(struct steam_device *steam)
+static int steam_get_attributes(struct steam_device *steam)
{
- int ret;
- mutex_lock(&steam->report_mutex);
- ret = steam_send_report_byte(steam, ID_DONGLE_GET_WIRELESS_STATE);
- mutex_unlock(&steam->report_mutex);
- return ret;
+ int ret = 0;
+ u8 cmd[] = {ID_GET_ATTRIBUTES_VALUES, 0};
+ u8 reply[64] = {};
+ u8 size;
+ int i;
+ struct steam_controller_attribute *attr;
+
+ ret = steam_exchange_report(steam, cmd, sizeof(cmd), reply, sizeof(reply));
+ if (ret < 0)
+ return ret;
+ if (reply[1] < 2) {
+ hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
+ (int)sizeof(reply), reply);
+ return -EIO;
+ }
+
+ size = min(reply[1], sizeof(reply) - 2);
+ for (i = 0; i + sizeof(*attr) <= size; i += sizeof(*attr)) {
+ attr = (struct steam_controller_attribute *)&reply[i + 2];
+ if (attr->tag == ATTRIB_CONNECTION_INTERVAL_IN_US) {
+ steam->sensor_update_rate_us = get_unaligned_le32(&attr->value);
+ hid_dbg(steam->hdev, "Sensor update rate: %uus\n",
+ steam->sensor_update_rate_us);
+ }
+ }
+
+ return 0;
+}
+
+static int steam_get_conn_status(struct steam_device *steam)
+{
+ int ret = 0;
+ u8 cmd[] = {ID_DONGLE_GET_WIRELESS_STATE};
+ u8 reply[3] = {};
+ u8 report_id;
+
+ if (steam->quirks & STEAM_QUIRK_IBEX)
+ report_id = REPORT_ID_FEATURES_DONGLE;
+ else
+ report_id = 0;
+
+ guard(mutex)(&steam->report_mutex);
+ ret = steam_exchange_report_id(steam, cmd, sizeof(cmd), reply, sizeof(reply), report_id);
+ if (ret < 0)
+ return ret;
+ if (reply[0] != ID_DONGLE_GET_WIRELESS_STATE || reply[1] < 1) {
+ hid_err(steam->hdev, "%s: invalid reply (%*ph)\n", __func__,
+ (int)sizeof(reply), reply);
+ return -EIO;
+ }
+
+ return reply[2];
}
/*
@@ -490,24 +752,41 @@ static inline int steam_haptic_pulse(struct steam_device *steam, u8 pad,
u16 duration, u16 interval, u16 count, u8 gain)
{
int ret;
- u8 report[10] = {ID_TRIGGER_HAPTIC_PULSE, 8};
/* Left and right are swapped on this report for legacy reasons */
if (pad < STEAM_PAD_BOTH)
pad ^= 1;
- report[2] = pad;
- report[3] = duration & 0xFF;
- report[4] = duration >> 8;
- report[5] = interval & 0xFF;
- report[6] = interval >> 8;
- report[7] = count & 0xFF;
- report[8] = count >> 8;
- report[9] = gain;
+ if (steam->quirks & STEAM_QUIRK_IBEX) {
+ struct steam_ibex_output_report *report =
+ kzalloc(sizeof(struct steam_ibex_output_report), GFP_KERNEL);
+
+ if (!report)
+ return -ENOMEM;
+
+ report->id = REPORT_ID_HAPTIC_PULSE;
+ report->pulse.side = pad;
+ put_unaligned_le16(duration, &report->pulse.on_us);
+ put_unaligned_le16(interval, &report->pulse.off_us);
+ put_unaligned_le16(count, &report->pulse.repeat_count);
+
+ ret = hid_hw_output_report(steam->hdev, (u8 *)report, 8);
+
+ kfree(report);
+ } else {
+ u8 report[10] = {ID_TRIGGER_HAPTIC_PULSE, 8, pad};
+
+ report[3] = duration & 0xFF;
+ report[4] = duration >> 8;
+ report[5] = interval & 0xFF;
+ report[6] = interval >> 8;
+ report[7] = count & 0xFF;
+ report[8] = count >> 8;
+ report[9] = gain;
+
+ ret = steam_send_report(steam, report, 10);
+ }
- mutex_lock(&steam->report_mutex);
- ret = steam_send_report(steam, report, sizeof(report));
- mutex_unlock(&steam->report_mutex);
return ret;
}
@@ -516,20 +795,38 @@ static inline int steam_haptic_rumble(struct steam_device *steam,
u8 left_gain, u8 right_gain)
{
int ret;
- u8 report[11] = {ID_TRIGGER_RUMBLE_CMD, 9};
- report[3] = intensity & 0xFF;
- report[4] = intensity >> 8;
- report[5] = left_speed & 0xFF;
- report[6] = left_speed >> 8;
- report[7] = right_speed & 0xFF;
- report[8] = right_speed >> 8;
- report[9] = left_gain;
- report[10] = right_gain;
+ if (steam->quirks & STEAM_QUIRK_IBEX) {
+ struct steam_ibex_output_report *report =
+ kzalloc(sizeof(struct steam_ibex_output_report), GFP_KERNEL);
- mutex_lock(&steam->report_mutex);
- ret = steam_send_report(steam, report, sizeof(report));
- mutex_unlock(&steam->report_mutex);
+ if (!report)
+ return -ENOMEM;
+
+ report->id = REPORT_ID_HAPTIC_RUMBLE;
+ put_unaligned_le16(intensity, &report->rumble.intensity);
+ put_unaligned_le16(left_speed, &report->rumble.left.speed);
+ report->rumble.left.gain = left_gain;
+ put_unaligned_le16(right_speed, &report->rumble.right.speed);
+ report->rumble.right.gain = right_gain;
+
+ ret = hid_hw_output_report(steam->hdev, (u8 *)report, 10);
+
+ kfree(report);
+ } else {
+ u8 report[11] = {ID_TRIGGER_RUMBLE_CMD, 9};
+
+ report[3] = intensity & 0xFF;
+ report[4] = intensity >> 8;
+ report[5] = left_speed & 0xFF;
+ report[6] = left_speed >> 8;
+ report[7] = right_speed & 0xFF;
+ report[8] = right_speed >> 8;
+ report[9] = left_gain;
+ report[10] = right_gain;
+
+ ret = steam_send_report(steam, report, sizeof(report));
+ }
return ret;
}
@@ -537,8 +834,26 @@ static void steam_haptic_rumble_cb(struct work_struct *work)
{
struct steam_device *steam = container_of(work, struct steam_device,
rumble_work);
+
+ mutex_lock(&steam->report_mutex);
+ steam_haptic_rumble(steam, 0, steam->rumble_left,
+ steam->rumble_right, 2, 0);
+ mutex_unlock(&steam->report_mutex);
+}
+
+static void steam_coalesce_rumble_cb(struct work_struct *work)
+{
+ struct steam_device *steam = container_of(to_delayed_work(work),
+ struct steam_device,
+ coalesce_rumble_work);
+
+ mutex_lock(&steam->report_mutex);
steam_haptic_rumble(steam, 0, steam->rumble_left,
steam->rumble_right, 2, 0);
+ mutex_unlock(&steam->report_mutex);
+
+ if (steam->rumble_left || steam->rumble_right)
+ schedule_delayed_work(&steam->coalesce_rumble_work, HZ / 20);
}
#ifdef CONFIG_STEAM_FF
@@ -550,6 +865,14 @@ static int steam_play_effect(struct input_dev *dev, void *data,
steam->rumble_left = effect->u.rumble.strong_magnitude;
steam->rumble_right = effect->u.rumble.weak_magnitude;
+ /*
+ * The interface gets somewhat overloaded when too many rumble
+ * packets are sent in a row, so Steam throttles it to 20 Hz
+ */
+ if (delayed_work_pending(&steam->coalesce_rumble_work))
+ return 0;
+
+ schedule_delayed_work(&steam->coalesce_rumble_work, HZ / 20);
return schedule_work(&steam->rumble_work);
}
#endif
@@ -559,7 +882,6 @@ static void steam_set_lizard_mode(struct steam_device *steam, bool enable)
if (steam->gamepad_mode)
enable = false;
- mutex_lock(&steam->report_mutex);
if (enable) {
/* enable esc, enter, cursors */
steam_send_report_byte(steam, ID_SET_DEFAULT_DIGITAL_MAPPINGS);
@@ -569,40 +891,38 @@ static void steam_set_lizard_mode(struct steam_device *steam, bool enable)
/* disable esc, enter, cursor */
steam_send_report_byte(steam, ID_CLEAR_DIGITAL_MAPPINGS);
- if (steam->quirks & STEAM_QUIRK_DECK) {
+ if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))
steam_write_settings(steam,
- SETTING_LEFT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
- SETTING_RIGHT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
- SETTING_LEFT_TRACKPAD_CLICK_PRESSURE, 0xFFFF, /* disable haptic click */
- SETTING_RIGHT_TRACKPAD_CLICK_PRESSURE, 0xFFFF, /* disable haptic click */
- SETTING_STEAM_WATCHDOG_ENABLE, 0, /* disable watchdog that tests if Steam is active */
+ /* disable lizard mode */
+ SETTING_LIZARD_MODE, 0,
+ /* disable watchdog that tests if Steam is active */
+ SETTING_STEAM_WATCHDOG_ENABLE, 0,
0);
- } else {
+ else
steam_write_settings(steam,
SETTING_LEFT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
SETTING_RIGHT_TRACKPAD_MODE, TRACKPAD_NONE, /* disable mouse */
0);
- }
}
- mutex_unlock(&steam->report_mutex);
}
static int steam_input_open(struct input_dev *dev)
{
struct steam_device *steam = input_get_drvdata(dev);
unsigned long flags;
- bool set_lizard_mode;
+ bool client_opened;
/*
* Disabling lizard mode automatically is only done on the Steam
* Controller. On the Steam Deck, this is toggled manually by holding
* the options button instead, handled by steam_mode_switch_cb.
*/
- if (!(steam->quirks & STEAM_QUIRK_DECK)) {
+ if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
spin_lock_irqsave(&steam->lock, flags);
- set_lizard_mode = !steam->client_opened && lizard_mode;
+ client_opened = steam->client_opened;
spin_unlock_irqrestore(&steam->lock, flags);
- if (set_lizard_mode)
+ guard(mutex)(&steam->report_mutex);
+ if (!client_opened && lizard_mode)
steam_set_lizard_mode(steam, false);
}
@@ -613,17 +933,48 @@ static void steam_input_close(struct input_dev *dev)
{
struct steam_device *steam = input_get_drvdata(dev);
unsigned long flags;
- bool set_lizard_mode;
+ bool client_opened;
- if (!(steam->quirks & STEAM_QUIRK_DECK)) {
+ if (!(steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX))) {
spin_lock_irqsave(&steam->lock, flags);
- set_lizard_mode = !steam->client_opened && lizard_mode;
+ client_opened = steam->client_opened;
spin_unlock_irqrestore(&steam->lock, flags);
- if (set_lizard_mode)
+ guard(mutex)(&steam->report_mutex);
+ if (!client_opened && lizard_mode)
steam_set_lizard_mode(steam, true);
}
}
+static int steam_sensor_open(struct input_dev *dev)
+{
+ struct steam_device *steam = input_get_drvdata(dev);
+
+ scoped_guard(spinlock_irqsave, &steam->lock) {
+ if (steam->client_opened)
+ return 0;
+ }
+
+ guard(mutex)(&steam->report_mutex);
+ steam_write_settings(steam, SETTING_IMU_MODE,
+ SETTING_IMU_MODE_SEND_RAW_ACCEL | SETTING_IMU_MODE_SEND_RAW_GYRO,
+ 0);
+
+ return 0;
+}
+
+static void steam_sensor_close(struct input_dev *dev)
+{
+ struct steam_device *steam = input_get_drvdata(dev);
+
+ scoped_guard(spinlock_irqsave, &steam->lock) {
+ if (steam->client_opened)
+ return;
+ }
+
+ guard(mutex)(&steam->report_mutex);
+ steam_write_settings(steam, SETTING_IMU_MODE, 0, 0);
+}
+
static enum power_supply_property steam_battery_props[] = {
POWER_SUPPLY_PROP_PRESENT,
POWER_SUPPLY_PROP_SCOPE,
@@ -637,25 +988,40 @@ static int steam_battery_get_property(struct power_supply *psy,
{
struct steam_device *steam = power_supply_get_drvdata(psy);
unsigned long flags;
- s16 volts;
+ u16 volts;
+ u16 curr;
+ u16 temp;
u8 batt;
+ u8 status;
int ret = 0;
spin_lock_irqsave(&steam->lock, flags);
- volts = steam->voltage;
+ volts = steam->battery_voltage;
+ curr = steam->battery_current;
batt = steam->battery_charge;
+ temp = steam->battery_temp;
+ status = steam->battery_status;
spin_unlock_irqrestore(&steam->lock, flags);
switch (psp) {
case POWER_SUPPLY_PROP_PRESENT:
val->intval = 1;
break;
+ case POWER_SUPPLY_PROP_STATUS:
+ val->intval = status;
+ break;
case POWER_SUPPLY_PROP_SCOPE:
val->intval = POWER_SUPPLY_SCOPE_DEVICE;
break;
case POWER_SUPPLY_PROP_VOLTAGE_NOW:
val->intval = volts * 1000; /* mV -> uV */
break;
+ case POWER_SUPPLY_PROP_CURRENT_NOW:
+ val->intval = curr * 1000; /* mA -> uA */
+ break;
+ case POWER_SUPPLY_PROP_TEMP:
+ val->intval = temp / 100; /* thousandths °C -> tenths °C */
+ break;
case POWER_SUPPLY_PROP_CAPACITY:
val->intval = batt;
break;
@@ -666,6 +1032,16 @@ static int steam_battery_get_property(struct power_supply *psy,
return ret;
}
+static enum power_supply_property steam_ibex_battery_props[] = {
+ POWER_SUPPLY_PROP_PRESENT,
+ POWER_SUPPLY_PROP_STATUS,
+ POWER_SUPPLY_PROP_SCOPE,
+ POWER_SUPPLY_PROP_VOLTAGE_NOW,
+ POWER_SUPPLY_PROP_CURRENT_NOW,
+ POWER_SUPPLY_PROP_CAPACITY,
+ POWER_SUPPLY_PROP_TEMP,
+};
+
static int steam_battery_register(struct steam_device *steam)
{
struct power_supply *battery;
@@ -674,25 +1050,42 @@ static int steam_battery_register(struct steam_device *steam)
int ret;
steam->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
- steam->battery_desc.properties = steam_battery_props;
- steam->battery_desc.num_properties = ARRAY_SIZE(steam_battery_props);
+ if (steam->quirks & STEAM_QUIRK_IBEX) {
+ steam->battery_desc.properties = steam_ibex_battery_props;
+ steam->battery_desc.num_properties = ARRAY_SIZE(steam_ibex_battery_props);
+ /*
+ * Ibex needs a shorter name as it has a temperature and the
+ * thermal zone name length limit is 20 characters. It's more
+ * ambiguous sounding, so let's only use it when needed.
+ */
+ steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
+ GFP_KERNEL, "steam-%s",
+ steam->serial_no);
+ } else {
+ steam->battery_desc.properties = steam_battery_props;
+ steam->battery_desc.num_properties = ARRAY_SIZE(steam_battery_props);
+ steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
+ GFP_KERNEL, "steam-controller-%s-battery",
+ steam->serial_no);
+ }
steam->battery_desc.get_property = steam_battery_get_property;
- steam->battery_desc.name = devm_kasprintf(&steam->hdev->dev,
- GFP_KERNEL, "steam-controller-%s-battery",
- steam->serial_no);
if (!steam->battery_desc.name)
return -ENOMEM;
/* avoid the warning of 0% battery while waiting for the first info */
spin_lock_irqsave(&steam->lock, flags);
- steam->voltage = 3000;
+ steam->battery_voltage = 3000;
steam->battery_charge = 100;
+ steam->battery_current = 0;
+ steam->battery_temp = 20000;
+ steam->battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
spin_unlock_irqrestore(&steam->lock, flags);
battery = power_supply_register(&steam->hdev->dev,
&steam->battery_desc, &battery_cfg);
if (IS_ERR(battery)) {
ret = PTR_ERR(battery);
+ devm_kfree(&steam->hdev->dev, steam->battery_desc.name);
hid_err(steam->hdev,
"%s:power_supply_register failed with error %d\n",
__func__, ret);
@@ -757,7 +1150,7 @@ static int steam_input_register(struct steam_device *steam)
input_set_capability(input, EV_KEY, BTN_THUMB2);
input_set_capability(input, EV_KEY, BTN_GRIPL);
input_set_capability(input, EV_KEY, BTN_GRIPR);
- if (steam->quirks & STEAM_QUIRK_DECK) {
+ if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
input_set_capability(input, EV_KEY, BTN_BASE);
input_set_capability(input, EV_KEY, BTN_GRIPL2);
input_set_capability(input, EV_KEY, BTN_GRIPR2);
@@ -771,7 +1164,7 @@ static int steam_input_register(struct steam_device *steam)
input_set_abs_params(input, ABS_HAT0Y, -32767, 32767,
STEAM_PAD_FUZZ, 0);
- if (steam->quirks & STEAM_QUIRK_DECK) {
+ if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
input_set_abs_params(input, ABS_HAT2Y, 0, 32767, 0, 0);
input_set_abs_params(input, ABS_HAT2X, 0, 32767, 0, 0);
@@ -811,7 +1204,7 @@ static int steam_input_register(struct steam_device *steam)
input_abs_set_res(input, ABS_HAT0Y, STEAM_PAD_RESOLUTION);
#ifdef CONFIG_STEAM_FF
- if (steam->quirks & STEAM_QUIRK_DECK) {
+ if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
input_set_capability(input, EV_FF, FF_RUMBLE);
ret = input_ff_create_memless(input, NULL, steam_play_effect);
if (ret)
@@ -836,9 +1229,7 @@ static int steam_sensors_register(struct steam_device *steam)
struct hid_device *hdev = steam->hdev;
struct input_dev *sensors;
int ret;
-
- if (!(steam->quirks & STEAM_QUIRK_DECK))
- return 0;
+ bool needs_open_close;
rcu_read_lock();
sensors = rcu_dereference(steam->sensors);
@@ -855,7 +1246,33 @@ static int steam_sensors_register(struct steam_device *steam)
input_set_drvdata(sensors, steam);
sensors->dev.parent = &hdev->dev;
- sensors->name = "Steam Deck Motion Sensors";
+ /*
+ * The open/close calls are needed in these specific cases:
+ *
+ * - Steam Controller (2015): Always
+ * - Steam Deck: Never
+ * - Steam Controller (2026): Only when wireless/BLE
+ */
+ if (steam->quirks & STEAM_QUIRK_DECK)
+ needs_open_close = false;
+ else if (steam->quirks & (STEAM_QUIRK_WIRELESS | STEAM_QUIRK_BLE))
+ /* Both wireless Steam Controller setups */
+ needs_open_close = true;
+ else if (steam->quirks & STEAM_QUIRK_IBEX)
+ /* Wired Steam Controller (2026) */
+ needs_open_close = false;
+ else
+ /* Wired Steam Controller (2015) */
+ needs_open_close = true;
+
+ if (needs_open_close) {
+ sensors->open = steam_sensor_open;
+ sensors->close = steam_sensor_close;
+ }
+
+ sensors->name = steam->quirks & STEAM_QUIRK_DECK ?
+ "Steam Deck Motion Sensors" :
+ "Steam Controller Motion Sensors";
sensors->phys = hdev->phys;
sensors->uniq = steam->serial_no;
sensors->id.bustype = hdev->bus;
@@ -867,25 +1284,34 @@ static int steam_sensors_register(struct steam_device *steam)
__set_bit(EV_MSC, sensors->evbit);
__set_bit(MSC_TIMESTAMP, sensors->mscbit);
- input_set_abs_params(sensors, ABS_X, -STEAM_DECK_ACCEL_RANGE,
- STEAM_DECK_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
- input_set_abs_params(sensors, ABS_Y, -STEAM_DECK_ACCEL_RANGE,
- STEAM_DECK_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
- input_set_abs_params(sensors, ABS_Z, -STEAM_DECK_ACCEL_RANGE,
- STEAM_DECK_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
- input_abs_set_res(sensors, ABS_X, STEAM_DECK_ACCEL_RES_PER_G);
- input_abs_set_res(sensors, ABS_Y, STEAM_DECK_ACCEL_RES_PER_G);
- input_abs_set_res(sensors, ABS_Z, STEAM_DECK_ACCEL_RES_PER_G);
-
- input_set_abs_params(sensors, ABS_RX, -STEAM_DECK_GYRO_RANGE,
- STEAM_DECK_GYRO_RANGE, STEAM_DECK_GYRO_FUZZ, 0);
- input_set_abs_params(sensors, ABS_RY, -STEAM_DECK_GYRO_RANGE,
- STEAM_DECK_GYRO_RANGE, STEAM_DECK_GYRO_FUZZ, 0);
- input_set_abs_params(sensors, ABS_RZ, -STEAM_DECK_GYRO_RANGE,
- STEAM_DECK_GYRO_RANGE, STEAM_DECK_GYRO_FUZZ, 0);
- input_abs_set_res(sensors, ABS_RX, STEAM_DECK_GYRO_RES_PER_DPS);
- input_abs_set_res(sensors, ABS_RY, STEAM_DECK_GYRO_RES_PER_DPS);
- input_abs_set_res(sensors, ABS_RZ, STEAM_DECK_GYRO_RES_PER_DPS);
+ if (steam->quirks & STEAM_QUIRK_DECK) {
+ input_set_abs_params(sensors, ABS_X, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_Y, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_Z, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_DECK_ACCEL_FUZZ, 0);
+ } else {
+ input_set_abs_params(sensors, ABS_X, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_ACCEL_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_Y, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_ACCEL_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_Z, -STEAM_ACCEL_RANGE,
+ STEAM_ACCEL_RANGE, STEAM_ACCEL_FUZZ, 0);
+ }
+ input_abs_set_res(sensors, ABS_X, STEAM_ACCEL_RES_PER_G);
+ input_abs_set_res(sensors, ABS_Y, STEAM_ACCEL_RES_PER_G);
+ input_abs_set_res(sensors, ABS_Z, STEAM_ACCEL_RES_PER_G);
+
+ input_set_abs_params(sensors, ABS_RX, -STEAM_GYRO_RANGE,
+ STEAM_GYRO_RANGE, STEAM_GYRO_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_RY, -STEAM_GYRO_RANGE,
+ STEAM_GYRO_RANGE, STEAM_GYRO_FUZZ, 0);
+ input_set_abs_params(sensors, ABS_RZ, -STEAM_GYRO_RANGE,
+ STEAM_GYRO_RANGE, STEAM_GYRO_FUZZ, 0);
+ input_abs_set_res(sensors, ABS_RX, STEAM_GYRO_RES_PER_DPS);
+ input_abs_set_res(sensors, ABS_RY, STEAM_GYRO_RES_PER_DPS);
+ input_abs_set_res(sensors, ABS_RZ, STEAM_GYRO_RES_PER_DPS);
ret = input_register_device(sensors);
if (ret)
@@ -916,9 +1342,6 @@ static void steam_sensors_unregister(struct steam_device *steam)
{
struct input_dev *sensors;
- if (!(steam->quirks & STEAM_QUIRK_DECK))
- return;
-
rcu_read_lock();
sensors = rcu_dereference(steam->sensors);
rcu_read_unlock();
@@ -943,97 +1366,119 @@ static void steam_battery_unregister(struct steam_device *steam)
RCU_INIT_POINTER(steam->battery, NULL);
synchronize_rcu();
power_supply_unregister(battery);
+ devm_kfree(&steam->hdev->dev, steam->battery_desc.name);
}
static int steam_register(struct steam_device *steam)
{
int ret;
- unsigned long client_opened;
- unsigned long flags;
+ mutex_lock(&steam->registration_mutex);
/*
* This function can be called several times in a row with the
* wireless adaptor, without steam_unregister() between them, because
* another client send a get_connection_status command, for example.
- * The battery and serial number are set just once per device.
*/
- if (!steam->serial_no[0]) {
- /*
- * Unlikely, but getting the serial could fail, and it is not so
- * important, so make up a serial number and go on.
- */
- if (steam_get_serial(steam) < 0)
- strscpy(steam->serial_no, "XXXXXXXXXX",
- sizeof(steam->serial_no));
+ if (steam->registered) {
+ mutex_unlock(&steam->registration_mutex);
+ return 0;
+ }
- hid_info(steam->hdev, "Steam Controller '%s' connected",
- steam->serial_no);
+ mutex_lock(&steam->report_mutex);
+ /*
+ * Unlikely, but getting the serial could fail, and it is not so
+ * important, so make up a serial number and go on.
+ */
+ if (steam_get_serial(steam) < 0 || !steam->serial_no[0])
+ strscpy(steam->serial_no, "XXXXXXXXXX",
+ sizeof(steam->serial_no));
- /* ignore battery errors, we can live without it */
- if (steam->quirks & STEAM_QUIRK_WIRELESS)
- steam_battery_register(steam);
+ ret = steam_get_attributes(steam);
+ if (ret < 0)
+ hid_err(steam->hdev,
+ "%s:steam_get_attributes failed with error %d\n",
+ __func__, ret);
- mutex_lock(&steam_devices_lock);
- if (list_empty(&steam->list))
- list_add(&steam->list, &steam_devices);
- mutex_unlock(&steam_devices_lock);
- }
+ hid_info(steam->hdev, "Steam %s '%s' connected",
+ steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
+ steam->serial_no);
- spin_lock_irqsave(&steam->lock, flags);
- client_opened = steam->client_opened;
- spin_unlock_irqrestore(&steam->lock, flags);
+ steam_set_lizard_mode(steam, lizard_mode);
+ mutex_unlock(&steam->report_mutex);
- if (!client_opened) {
- steam_set_lizard_mode(steam, lizard_mode);
- ret = steam_input_register(steam);
- if (ret != 0)
- goto steam_register_input_fail;
- ret = steam_sensors_register(steam);
- if (ret != 0)
- goto steam_register_sensors_fail;
- }
+ /* ignore battery errors, we can live without it */
+ if (steam->quirks & (STEAM_QUIRK_WIRELESS | STEAM_QUIRK_IBEX))
+ steam_battery_register(steam);
+
+ ret = steam_input_register(steam);
+ if (ret != 0)
+ goto steam_register_input_fail;
+ ret = steam_sensors_register(steam);
+ if (ret != 0)
+ goto steam_register_sensors_fail;
+
+ steam->registered = true;
+ mutex_unlock(&steam->registration_mutex);
+ mutex_lock(&steam_devices_lock);
+ if (list_empty(&steam->list))
+ list_add(&steam->list, &steam_devices);
+ mutex_unlock(&steam_devices_lock);
return 0;
steam_register_sensors_fail:
steam_input_unregister(steam);
steam_register_input_fail:
+ steam_battery_unregister(steam);
+ mutex_unlock(&steam->registration_mutex);
+ cancel_work_sync(&steam->rumble_work);
+ cancel_delayed_work_sync(&steam->mode_switch);
+ cancel_delayed_work_sync(&steam->coalesce_rumble_work);
return ret;
}
static void steam_unregister(struct steam_device *steam)
{
+ if (!steam->registered)
+ return;
+
+ hid_info(steam->hdev, "Steam %s '%s' disconnected",
+ steam->quirks & STEAM_QUIRK_DECK ? "Deck" : "Controller",
+ steam->serial_no);
+ mutex_lock(&steam->registration_mutex);
+ steam->registered = false;
steam_battery_unregister(steam);
steam_sensors_unregister(steam);
steam_input_unregister(steam);
- if (steam->serial_no[0]) {
- hid_info(steam->hdev, "Steam Controller '%s' disconnected",
- steam->serial_no);
- mutex_lock(&steam_devices_lock);
- list_del_init(&steam->list);
- mutex_unlock(&steam_devices_lock);
- steam->serial_no[0] = 0;
- }
+ mutex_unlock(&steam->registration_mutex);
+ cancel_work_sync(&steam->rumble_work);
+ cancel_delayed_work_sync(&steam->mode_switch);
+ cancel_delayed_work_sync(&steam->coalesce_rumble_work);
+ mutex_lock(&steam_devices_lock);
+ list_del_init(&steam->list);
+ mutex_unlock(&steam_devices_lock);
}
static void steam_work_connect_cb(struct work_struct *work)
{
struct steam_device *steam = container_of(work, struct steam_device,
work_connect);
+
unsigned long flags;
bool connected;
+ bool opened;
int ret;
spin_lock_irqsave(&steam->lock, flags);
+ opened = steam->client_opened;
connected = steam->connected;
spin_unlock_irqrestore(&steam->lock, flags);
- if (connected) {
+ if (connected && !opened) {
ret = steam_register(steam);
- if (ret) {
+ if (ret)
hid_err(steam->hdev,
"%s:steam_register failed with error %d\n",
__func__, ret);
- }
} else {
steam_unregister(steam);
}
@@ -1045,68 +1490,142 @@ static void steam_mode_switch_cb(struct work_struct *work)
struct steam_device, mode_switch);
unsigned long flags;
bool client_opened;
+ bool gamepad_mode;
+
if (!lizard_mode)
return;
+ spin_lock_irqsave(&steam->lock, flags);
steam->gamepad_mode = !steam->gamepad_mode;
- if (steam->gamepad_mode)
+ gamepad_mode = steam->gamepad_mode;
+ client_opened = steam->client_opened;
+ spin_unlock_irqrestore(&steam->lock, flags);
+
+ hid_dbg(steam->hdev, "%s: switching gamepad mode to %i\n", __func__, gamepad_mode);
+ if (gamepad_mode) {
+ guard(mutex)(&steam->report_mutex);
steam_set_lizard_mode(steam, false);
- else {
- spin_lock_irqsave(&steam->lock, flags);
- client_opened = steam->client_opened;
- spin_unlock_irqrestore(&steam->lock, flags);
- if (!client_opened)
+ } else {
+ struct input_dev *input;
+ struct input_dev *sensors;
+
+ if (!client_opened) {
+ guard(mutex)(&steam->report_mutex);
steam_set_lizard_mode(steam, lizard_mode);
+ }
+
+ /*
+ * Zero out inputs so it doesn't look like we're holding
+ * anything indefinitely.
+ */
+ guard(spinlock_irqsave)(&steam->lock);
+ rcu_read_lock();
+ input = rcu_dereference(steam->input);
+ if (likely(input)) {
+ input_report_key(input, BTN_TR2, 0);
+ input_report_key(input, BTN_TL2, 0);
+ input_report_key(input, BTN_TR, 0);
+ input_report_key(input, BTN_TL, 0);
+ input_report_key(input, BTN_Y, 0);
+ input_report_key(input, BTN_B, 0);
+ input_report_key(input, BTN_X, 0);
+ input_report_key(input, BTN_A, 0);
+ input_report_key(input, BTN_DPAD_UP, 0);
+ input_report_key(input, BTN_DPAD_RIGHT, 0);
+ input_report_key(input, BTN_DPAD_LEFT, 0);
+ input_report_key(input, BTN_DPAD_DOWN, 0);
+ input_report_key(input, BTN_SELECT, 0);
+ input_report_key(input, BTN_MODE, 0);
+ input_report_key(input, BTN_START, 0);
+ input_report_key(input, BTN_THUMBR, 0);
+ input_report_key(input, BTN_THUMBL, 0);
+ input_report_key(input, BTN_THUMB, 0);
+ input_report_key(input, BTN_THUMB2, 0);
+ input_report_key(input, BTN_GRIPL, 0);
+ input_report_key(input, BTN_GRIPR, 0);
+
+ input_report_abs(input, ABS_X, 0);
+ input_report_abs(input, ABS_Y, 0);
+ input_report_abs(input, ABS_RX, 0);
+ input_report_abs(input, ABS_RY, 0);
+ input_report_abs(input, ABS_HAT0X, 0);
+ input_report_abs(input, ABS_HAT0Y, 0);
+ input_report_abs(input, ABS_HAT2Y, 0);
+ input_report_abs(input, ABS_HAT2X, 0);
+
+ if (steam->quirks & (STEAM_QUIRK_DECK | STEAM_QUIRK_IBEX)) {
+ input_report_key(input, BTN_BASE, 0);
+ input_report_key(input, BTN_GRIPL2, 0);
+ input_report_key(input, BTN_GRIPR2, 0);
+
+ input_report_abs(input, ABS_HAT1X, 0);
+ input_report_abs(input, ABS_HAT1Y, 0);
+ }
+
+ input_sync(input);
+ }
+ sensors = rcu_dereference(steam->sensors);
+ if (likely(sensors)) {
+ /* Skip accelerometers since 0 isn't a neutral input */
+
+ input_report_abs(sensors, ABS_RX, 0);
+ input_report_abs(sensors, ABS_RY, 0);
+ input_report_abs(sensors, ABS_RZ, 0);
+
+ input_sync(sensors);
+ }
+ rcu_read_unlock();
}
+ guard(mutex)(&steam->report_mutex);
steam_haptic_pulse(steam, STEAM_PAD_RIGHT, 0x190, 0, 1, 0);
- if (steam->gamepad_mode) {
+ if (gamepad_mode) {
steam_haptic_pulse(steam, STEAM_PAD_LEFT, 0x14D, 0x14D, 0x2D, 0);
} else {
steam_haptic_pulse(steam, STEAM_PAD_LEFT, 0x1F4, 0x1F4, 0x1E, 0);
}
}
-static void steam_work_unregister_cb(struct work_struct *work)
-{
- struct steam_device *steam = container_of(work, struct steam_device,
- unregister_work);
- unsigned long flags;
- bool connected;
- bool opened;
-
- spin_lock_irqsave(&steam->lock, flags);
- opened = steam->client_opened;
- connected = steam->connected;
- spin_unlock_irqrestore(&steam->lock, flags);
-
- if (connected) {
- if (opened) {
- steam_sensors_unregister(steam);
- steam_input_unregister(steam);
- } else {
- steam_set_lizard_mode(steam, lizard_mode);
- steam_input_register(steam);
- steam_sensors_register(steam);
- }
- }
-}
-
-static bool steam_is_valve_interface(struct hid_device *hdev)
+static bool steam_is_valve_interface(struct hid_device *hdev, int quirks)
{
struct hid_report_enum *rep_enum;
/*
- * The wired device creates 3 interfaces:
+ * The 2015 wired controller creates 3 interfaces:
* 0: emulated mouse.
* 1: emulated keyboard.
* 2: the real game pad.
- * The wireless device creates 5 interfaces:
+ * The 2015 wireless adapter creates 5 interfaces:
* 0: emulated keyboard.
- * 1-4: slots where up to 4 real game pads will be connected to.
- * We know which one is the real gamepad interface because they are the
- * only ones with a feature report.
+ * 1-4: slots where up to 4 real controllers will be connected to.
+ * The Steam Deck creates 5 interfaces:
+ * 0: emulated mouse.
+ * 1: emulated keyboard.
+ * 2: the real game pad.
+ * 3-4: internal device comms (not HID).
+ * The 2026 wired controller creates 1 unified interface.
+ * The 2026 wireless puck creates 7 interfaces:
+ * 0-1: internal device comms (not HID).
+ * 2-5: slots where up to 4 real controllers will be connected to.
+ * 6: basic pogo pin interface.
+ *
+ * We know which one is the real controller interface for the pre-2026
+ * controllers because they are the only ones with a feature report.
+ *
+ * The puck's pogo pin interface should be ignored as it's stripped
+ * down. It has one collection with usage page FF00 with usage ID 2.
*/
+ if (quirks & STEAM_QUIRK_IBEX) {
+ /* There is only one BLE HID interface */
+ if (quirks & STEAM_QUIRK_BLE)
+ return true;
+
+ if (hdev->maxcollection < 1)
+ return true;
+
+ return hdev->collection[0].usage != 0xFF000002;
+ }
+
rep_enum = &hdev->report_enum[HID_FEATURE_REPORT];
return !list_empty(&rep_enum->report_list);
}
@@ -1137,7 +1656,7 @@ static int steam_client_ll_open(struct hid_device *hdev)
steam->client_opened++;
spin_unlock_irqrestore(&steam->lock, flags);
- schedule_work(&steam->unregister_work);
+ schedule_work(&steam->work_connect);
return 0;
}
@@ -1152,7 +1671,7 @@ static void steam_client_ll_close(struct hid_device *hdev)
steam->client_opened--;
spin_unlock_irqrestore(&steam->lock, flags);
- schedule_work(&steam->unregister_work);
+ schedule_work(&steam->work_connect);
}
static int steam_client_ll_raw_request(struct hid_device *hdev,
@@ -1166,6 +1685,14 @@ static int steam_client_ll_raw_request(struct hid_device *hdev,
report_type, reqtype);
}
+static int steam_client_ll_output_report(struct hid_device *hdev,
+ u8 *buf, size_t count)
+{
+ struct steam_device *steam = hdev->driver_data;
+
+ return hid_hw_output_report(steam->hdev, buf, count);
+}
+
static const struct hid_ll_driver steam_client_ll_driver = {
.parse = steam_client_ll_parse,
.start = steam_client_ll_start,
@@ -1173,6 +1700,7 @@ static const struct hid_ll_driver steam_client_ll_driver = {
.open = steam_client_ll_open,
.close = steam_client_ll_close,
.raw_request = steam_client_ll_raw_request,
+ .output_report = steam_client_ll_output_report,
};
static struct hid_device *steam_create_client_hid(struct hid_device *hdev)
@@ -1227,7 +1755,7 @@ static int steam_probe(struct hid_device *hdev,
* The non-valve interfaces (mouse and keyboard emulation) are
* connected without changes.
*/
- if (!steam_is_valve_interface(hdev))
+ if (!steam_is_valve_interface(hdev, id->driver_data))
return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
steam = devm_kzalloc(&hdev->dev, sizeof(*steam), GFP_KERNEL);
@@ -1238,13 +1766,18 @@ static int steam_probe(struct hid_device *hdev,
hid_set_drvdata(hdev, steam);
spin_lock_init(&steam->lock);
mutex_init(&steam->report_mutex);
+ mutex_init(&steam->registration_mutex);
steam->quirks = id->driver_data;
INIT_WORK(&steam->work_connect, steam_work_connect_cb);
INIT_DELAYED_WORK(&steam->mode_switch, steam_mode_switch_cb);
INIT_LIST_HEAD(&steam->list);
INIT_WORK(&steam->rumble_work, steam_haptic_rumble_cb);
+ INIT_DELAYED_WORK(&steam->coalesce_rumble_work, steam_coalesce_rumble_cb);
steam->sensor_timestamp_us = 0;
- INIT_WORK(&steam->unregister_work, steam_work_unregister_cb);
+ if (steam->quirks & STEAM_QUIRK_DECK)
+ steam->sensor_update_rate_us = 4000;
+ else
+ steam->sensor_update_rate_us = 9000;
/*
* With the real steam controller interface, do not connect hidraw.
@@ -1262,14 +1795,21 @@ static int steam_probe(struct hid_device *hdev,
goto err_hw_stop;
}
+ steam->connected = true;
+
if (steam->quirks & STEAM_QUIRK_WIRELESS) {
hid_info(hdev, "Steam wireless receiver connected");
/* If using a wireless adaptor ask for connection status */
steam->connected = false;
- steam_request_conn_status(steam);
- } else {
- /* A wired connection is always present */
- steam->connected = true;
+ ret = steam_get_conn_status(steam);
+ if (ret < 0)
+ hid_err(hdev,
+ "%s:steam_get_conn_status failed with error %d\n",
+ __func__, ret);
+ else if (ret == WIRELESS_EVENT_CONNECT)
+ steam->connected = true;
+ }
+ if (steam->connected) {
ret = steam_register(steam);
if (ret) {
hid_err(hdev,
@@ -1305,7 +1845,7 @@ err_cancel_work:
cancel_work_sync(&steam->work_connect);
cancel_delayed_work_sync(&steam->mode_switch);
cancel_work_sync(&steam->rumble_work);
- cancel_work_sync(&steam->unregister_work);
+ cancel_delayed_work_sync(&steam->coalesce_rumble_work);
return ret;
}
@@ -1313,36 +1853,32 @@ err_cancel_work:
static void steam_remove(struct hid_device *hdev)
{
struct steam_device *steam = hid_get_drvdata(hdev);
+ unsigned long flags;
if (!steam || hdev->group == HID_GROUP_STEAM) {
hid_hw_stop(hdev);
return;
}
+ hid_hw_close(hdev);
hid_destroy_device(steam->client_hdev);
- cancel_delayed_work_sync(&steam->mode_switch);
- cancel_work_sync(&steam->work_connect);
- cancel_work_sync(&steam->rumble_work);
- cancel_work_sync(&steam->unregister_work);
- steam->client_hdev = NULL;
+ spin_lock_irqsave(&steam->lock, flags);
steam->client_opened = 0;
+ spin_unlock_irqrestore(&steam->lock, flags);
+ cancel_work_sync(&steam->work_connect);
if (steam->quirks & STEAM_QUIRK_WIRELESS) {
hid_info(hdev, "Steam wireless receiver disconnected");
}
- hid_hw_close(hdev);
- hid_hw_stop(hdev);
steam_unregister(steam);
+ hid_hw_stop(hdev);
}
static void steam_do_connect_event(struct steam_device *steam, bool connected)
{
- unsigned long flags;
bool changed;
- spin_lock_irqsave(&steam->lock, flags);
changed = steam->connected != connected;
steam->connected = connected;
- spin_unlock_irqrestore(&steam->lock, flags);
if (changed && schedule_work(&steam->work_connect) == 0)
dbg_hid("%s: connected=%d event already queued\n",
@@ -1354,13 +1890,45 @@ static void steam_do_connect_event(struct steam_device *steam, bool connected)
* Clamp the values to 32767..-32767 so that the range is
* symmetrical and can be negated safely.
*/
-static inline s16 steam_le16(u8 *data)
+static inline s16 steam_le16(const u8 *data)
{
- s16 x = (s16) le16_to_cpup((__le16 *)data);
+ s16 x = (s16) get_unaligned_le16((const __le16 *)data);
return x == -32768 ? -32767 : x;
}
+struct steam_button_mapping {
+ int code;
+ u8 byte;
+ u8 bit;
+};
+
+struct steam_axis_mapping {
+ int code;
+ s8 sign;
+ u8 byte;
+};
+
+static void steam_map_buttons(struct input_dev *input,
+ const struct steam_button_mapping *mappings, const u8 *data)
+{
+ const struct steam_button_mapping *mapping;
+
+ for (mapping = mappings; mapping->code; mapping++)
+ input_report_key(input, mapping->code,
+ data[mapping->byte] & BIT(mapping->bit));
+}
+
+static void steam_map_axes(struct input_dev *input,
+ const struct steam_axis_mapping *mappings, const u8 *data)
+{
+ const struct steam_axis_mapping *mapping;
+
+ for (mapping = mappings; mapping->sign; mapping++)
+ input_report_abs(input, mapping->code,
+ mapping->sign * steam_le16(&data[mapping->byte]));
+}
+
/*
* The size for this message payload is 60.
* The known values are:
@@ -1413,9 +1981,9 @@ static inline s16 steam_le16(u8 *data)
* 9.1 | BTN_DPAD_RIGHT | left-pad right
* 9.2 | BTN_DPAD_LEFT | left-pad left
* 9.3 | BTN_DPAD_DOWN | left-pad down
- * 9.4 | BTN_SELECT | menu left
+ * 9.4 | BTN_SELECT | view
* 9.5 | BTN_MODE | steam logo
- * 9.6 | BTN_START | menu right
+ * 9.6 | BTN_START | menu
* 9.7 | BTN_GRIPL | left back lever
* 10.0 | BTN_GRIPR | right back lever
* 10.1 | -- | left-pad clicked
@@ -1427,18 +1995,52 @@ static inline s16 steam_le16(u8 *data)
* 10.7 | -- | lpad_and_joy
*/
+static const struct steam_button_mapping steam_controller_button_mappings[] = {
+ { BTN_TR2, 8, 0 },
+ { BTN_TL2, 8, 1 },
+ { BTN_TR, 8, 2 },
+ { BTN_TL, 8, 3 },
+ { BTN_Y, 8, 4 },
+ { BTN_B, 8, 5 },
+ { BTN_X, 8, 6 },
+ { BTN_A, 8, 7 },
+ { BTN_SELECT, 9, 4 },
+ { BTN_MODE, 9, 5 },
+ { BTN_START, 9, 6 },
+ { BTN_GRIPL, 9, 7 },
+ { BTN_GRIPR, 10, 0 },
+ { BTN_THUMBR, 10, 2 },
+ { BTN_THUMBL, 10, 6 },
+ { BTN_THUMB2, 10, 4 },
+ { BTN_DPAD_UP, 9, 0 },
+ { BTN_DPAD_RIGHT, 9, 1 },
+ { BTN_DPAD_LEFT, 9, 2 },
+ { BTN_DPAD_DOWN, 9, 3 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_controller_axis_mappings[] = {
+ { ABS_RX, 1, 20 },
+ { ABS_RY, -1, 22 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_controller_imu_mappings[] = {
+ { ABS_X, 1, 28 },
+ { ABS_Z, -1, 30 },
+ { ABS_Y, 1, 32 },
+ { ABS_RX, 1, 34 },
+ { ABS_RZ, 1, 36 },
+ { ABS_RY, 1, 38 },
+ { /* sentinel */ },
+};
+
static void steam_do_input_event(struct steam_device *steam,
struct input_dev *input, u8 *data)
{
- /* 24 bits of buttons */
- u8 b8, b9, b10;
s16 x, y;
bool lpad_touched, lpad_and_joy;
- b8 = data[8];
- b9 = data[9];
- b10 = data[10];
-
input_report_abs(input, ABS_HAT2Y, data[11]);
input_report_abs(input, ABS_HAT2X, data[12]);
@@ -1450,8 +2052,8 @@ static void steam_do_input_event(struct steam_device *steam,
* joystick values.
* (lpad_touched || lpad_and_joy) tells if the lpad is really touched.
*/
- lpad_touched = b10 & BIT(3);
- lpad_and_joy = b10 & BIT(7);
+ lpad_touched = data[10] & BIT(3);
+ lpad_and_joy = data[10] & BIT(7);
x = steam_le16(data + 16);
y = -steam_le16(data + 18);
@@ -1467,35 +2069,25 @@ static void steam_do_input_event(struct steam_device *steam,
input_report_abs(input, ABS_HAT0X, 0);
input_report_abs(input, ABS_HAT0Y, 0);
}
+ input_report_key(input, BTN_THUMB, lpad_touched || lpad_and_joy);
- input_report_abs(input, ABS_RX, steam_le16(data + 20));
- input_report_abs(input, ABS_RY, -steam_le16(data + 22));
-
- input_event(input, EV_KEY, BTN_TR2, !!(b8 & BIT(0)));
- input_event(input, EV_KEY, BTN_TL2, !!(b8 & BIT(1)));
- input_event(input, EV_KEY, BTN_TR, !!(b8 & BIT(2)));
- input_event(input, EV_KEY, BTN_TL, !!(b8 & BIT(3)));
- input_event(input, EV_KEY, BTN_Y, !!(b8 & BIT(4)));
- input_event(input, EV_KEY, BTN_B, !!(b8 & BIT(5)));
- input_event(input, EV_KEY, BTN_X, !!(b8 & BIT(6)));
- input_event(input, EV_KEY, BTN_A, !!(b8 & BIT(7)));
- input_event(input, EV_KEY, BTN_SELECT, !!(b9 & BIT(4)));
- input_event(input, EV_KEY, BTN_MODE, !!(b9 & BIT(5)));
- input_event(input, EV_KEY, BTN_START, !!(b9 & BIT(6)));
- input_event(input, EV_KEY, BTN_GRIPL, !!(b9 & BIT(7)));
- input_event(input, EV_KEY, BTN_GRIPR, !!(b10 & BIT(0)));
- input_event(input, EV_KEY, BTN_THUMBR, !!(b10 & BIT(2)));
- input_event(input, EV_KEY, BTN_THUMBL, !!(b10 & BIT(6)));
- input_event(input, EV_KEY, BTN_THUMB, lpad_touched || lpad_and_joy);
- input_event(input, EV_KEY, BTN_THUMB2, !!(b10 & BIT(4)));
- input_event(input, EV_KEY, BTN_DPAD_UP, !!(b9 & BIT(0)));
- input_event(input, EV_KEY, BTN_DPAD_RIGHT, !!(b9 & BIT(1)));
- input_event(input, EV_KEY, BTN_DPAD_LEFT, !!(b9 & BIT(2)));
- input_event(input, EV_KEY, BTN_DPAD_DOWN, !!(b9 & BIT(3)));
+ steam_map_buttons(input, steam_controller_button_mappings, data);
+ steam_map_axes(input, steam_controller_axis_mappings, data);
input_sync(input);
}
+static void steam_do_sensors_event(struct steam_device *steam,
+ struct input_dev *sensors, u8 *data)
+{
+ steam->sensor_timestamp_us += steam->sensor_update_rate_us;
+
+ input_event(sensors, EV_MSC, MSC_TIMESTAMP, steam->sensor_timestamp_us);
+ steam_map_axes(sensors, steam_controller_imu_mappings, data);
+
+ input_sync(sensors);
+}
+
/*
* The size for this message payload is 56.
* The known values are:
@@ -1541,9 +2133,9 @@ static void steam_do_input_event(struct steam_device *steam,
* 9.1 | BTN_DPAD_RIGHT | left-pad right
* 9.2 | BTN_DPAD_LEFT | left-pad left
* 9.3 | BTN_DPAD_DOWN | left-pad down
- * 9.4 | BTN_SELECT | menu left
+ * 9.4 | BTN_SELECT | view
* 9.5 | BTN_MODE | steam logo
- * 9.6 | BTN_START | menu right
+ * 9.6 | BTN_START | menu
* 9.7 | BTN_GRIPL2 | left bottom grip button
* 10.0 | BTN_GRIPR2 | right bottom grip button
* 10.1 | BTN_THUMB | left pad pressed
@@ -1594,23 +2186,68 @@ static void steam_do_input_event(struct steam_device *steam,
* 15.6 | -- | unknown
* 15.7 | -- | unknown
*/
+
+static const struct steam_button_mapping steam_deck_button_mappings[] = {
+ { BTN_TR2, 8, 0 },
+ { BTN_TL2, 8, 1 },
+ { BTN_TR, 8, 2 },
+ { BTN_TL, 8, 3 },
+ { BTN_Y, 8, 4 },
+ { BTN_B, 8, 5 },
+ { BTN_X, 8, 6 },
+ { BTN_A, 8, 7 },
+ { BTN_SELECT, 9, 4 },
+ { BTN_MODE, 9, 5 },
+ { BTN_START, 9, 6 },
+ { BTN_GRIPL2, 9, 7 },
+ { BTN_GRIPR2, 10, 0 },
+ { BTN_THUMBL, 10, 6 },
+ { BTN_THUMBR, 11, 2 },
+ { BTN_DPAD_UP, 9, 0 },
+ { BTN_DPAD_RIGHT, 9, 1 },
+ { BTN_DPAD_LEFT, 9, 2 },
+ { BTN_DPAD_DOWN, 9, 3 },
+ { BTN_THUMB, 10, 1 },
+ { BTN_THUMB2, 10, 2 },
+ { BTN_GRIPL, 13, 1 },
+ { BTN_GRIPR, 13, 2 },
+ { BTN_BASE, 14, 2 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_deck_axis_mappings[] = {
+ { ABS_X, 1, 48 },
+ { ABS_Y, -1, 50 },
+ { ABS_RX, 1, 52 },
+ { ABS_RY, -1, 54 },
+ { ABS_HAT2Y, 1, 44 },
+ { ABS_HAT2X, 1, 46 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_deck_imu_mappings[] = {
+ { ABS_X, 1, 24 },
+ { ABS_Z, -1, 26 },
+ { ABS_Y, 1, 28 },
+ { ABS_RX, 1, 30 },
+ { ABS_RZ, -1, 32 },
+ { ABS_RY, 1, 34 },
+ { /* sentinel */ },
+};
+
static void steam_do_deck_input_event(struct steam_device *steam,
struct input_dev *input, u8 *data)
{
- u8 b8, b9, b10, b11, b13, b14;
+ bool start_pressed;
bool lpad_touched, rpad_touched;
- b8 = data[8];
- b9 = data[9];
- b10 = data[10];
- b11 = data[11];
- b13 = data[13];
- b14 = data[14];
+ start_pressed = data[9] & BIT(6);
- if (!(b9 & BIT(6)) && steam->did_mode_switch) {
+ if (!start_pressed && steam->did_mode_switch) {
steam->did_mode_switch = false;
cancel_delayed_work(&steam->mode_switch);
- } else if (!steam->client_opened && (b9 & BIT(6)) && !steam->did_mode_switch) {
+ } else if (!steam->client_opened && start_pressed && !steam->did_mode_switch) {
+ hid_dbg(steam->hdev, "%s: doing mode switch\n", __func__);
steam->did_mode_switch = true;
schedule_delayed_work(&steam->mode_switch, 45 * HZ / 100);
}
@@ -1618,8 +2255,8 @@ static void steam_do_deck_input_event(struct steam_device *steam,
if (!steam->gamepad_mode && lizard_mode)
return;
- lpad_touched = b10 & BIT(3);
- rpad_touched = b10 & BIT(4);
+ lpad_touched = data[10] & BIT(3);
+ rpad_touched = data[10] & BIT(4);
if (lpad_touched) {
input_report_abs(input, ABS_HAT0X, steam_le16(data + 16));
@@ -1637,38 +2274,8 @@ static void steam_do_deck_input_event(struct steam_device *steam,
input_report_abs(input, ABS_HAT1Y, 0);
}
- input_report_abs(input, ABS_X, steam_le16(data + 48));
- input_report_abs(input, ABS_Y, -steam_le16(data + 50));
- input_report_abs(input, ABS_RX, steam_le16(data + 52));
- input_report_abs(input, ABS_RY, -steam_le16(data + 54));
-
- input_report_abs(input, ABS_HAT2Y, steam_le16(data + 44));
- input_report_abs(input, ABS_HAT2X, steam_le16(data + 46));
-
- input_event(input, EV_KEY, BTN_TR2, !!(b8 & BIT(0)));
- input_event(input, EV_KEY, BTN_TL2, !!(b8 & BIT(1)));
- input_event(input, EV_KEY, BTN_TR, !!(b8 & BIT(2)));
- input_event(input, EV_KEY, BTN_TL, !!(b8 & BIT(3)));
- input_event(input, EV_KEY, BTN_Y, !!(b8 & BIT(4)));
- input_event(input, EV_KEY, BTN_B, !!(b8 & BIT(5)));
- input_event(input, EV_KEY, BTN_X, !!(b8 & BIT(6)));
- input_event(input, EV_KEY, BTN_A, !!(b8 & BIT(7)));
- input_event(input, EV_KEY, BTN_SELECT, !!(b9 & BIT(4)));
- input_event(input, EV_KEY, BTN_MODE, !!(b9 & BIT(5)));
- input_event(input, EV_KEY, BTN_START, !!(b9 & BIT(6)));
- input_event(input, EV_KEY, BTN_GRIPL2, !!(b9 & BIT(7)));
- input_event(input, EV_KEY, BTN_GRIPR2, !!(b10 & BIT(0)));
- input_event(input, EV_KEY, BTN_THUMBL, !!(b10 & BIT(6)));
- input_event(input, EV_KEY, BTN_THUMBR, !!(b11 & BIT(2)));
- input_event(input, EV_KEY, BTN_DPAD_UP, !!(b9 & BIT(0)));
- input_event(input, EV_KEY, BTN_DPAD_RIGHT, !!(b9 & BIT(1)));
- input_event(input, EV_KEY, BTN_DPAD_LEFT, !!(b9 & BIT(2)));
- input_event(input, EV_KEY, BTN_DPAD_DOWN, !!(b9 & BIT(3)));
- input_event(input, EV_KEY, BTN_THUMB, !!(b10 & BIT(1)));
- input_event(input, EV_KEY, BTN_THUMB2, !!(b10 & BIT(2)));
- input_event(input, EV_KEY, BTN_GRIPL, !!(b13 & BIT(1)));
- input_event(input, EV_KEY, BTN_GRIPR, !!(b13 & BIT(2)));
- input_event(input, EV_KEY, BTN_BASE, !!(b14 & BIT(2)));
+ steam_map_buttons(input, steam_deck_button_mappings, data);
+ steam_map_axes(input, steam_deck_axis_mappings, data);
input_sync(input);
}
@@ -1676,25 +2283,13 @@ static void steam_do_deck_input_event(struct steam_device *steam,
static void steam_do_deck_sensors_event(struct steam_device *steam,
struct input_dev *sensors, u8 *data)
{
- /*
- * The deck input report is received every 4 ms on average,
- * with a jitter of +/- 4 ms even though the USB descriptor claims
- * that it uses 1 kHz.
- * Since the HID report does not include a sensor timestamp,
- * use a fixed increment here.
- */
- steam->sensor_timestamp_us += 4000;
+ steam->sensor_timestamp_us += steam->sensor_update_rate_us;
if (!steam->gamepad_mode && lizard_mode)
return;
input_event(sensors, EV_MSC, MSC_TIMESTAMP, steam->sensor_timestamp_us);
- input_report_abs(sensors, ABS_X, steam_le16(data + 24));
- input_report_abs(sensors, ABS_Z, -steam_le16(data + 26));
- input_report_abs(sensors, ABS_Y, steam_le16(data + 28));
- input_report_abs(sensors, ABS_RX, steam_le16(data + 30));
- input_report_abs(sensors, ABS_RZ, -steam_le16(data + 32));
- input_report_abs(sensors, ABS_RY, steam_le16(data + 34));
+ steam_map_axes(sensors, steam_deck_imu_mappings, data);
input_sync(sensors);
}
@@ -1712,8 +2307,6 @@ static void steam_do_deck_sensors_event(struct steam_device *steam,
static void steam_do_battery_event(struct steam_device *steam,
struct power_supply *battery, u8 *data)
{
- unsigned long flags;
-
s16 volts = steam_le16(data + 12);
u8 batt = data[14];
@@ -1721,15 +2314,219 @@ static void steam_do_battery_event(struct steam_device *steam,
rcu_read_lock();
battery = rcu_dereference(steam->battery);
if (likely(battery)) {
- spin_lock_irqsave(&steam->lock, flags);
- steam->voltage = volts;
+ steam->battery_voltage = volts;
steam->battery_charge = batt;
- spin_unlock_irqrestore(&steam->lock, flags);
power_supply_changed(battery);
}
rcu_read_unlock();
}
+/*
+ * The size for this message payload is 53 in REPORT_ID_INPUT and 45 in REPORT_ID_INPUT2.
+ * The values are:
+ * (* values only in REPORT_ID_INPUT)
+ * Offset| Type | Mapped to |Meaning
+ * -------+-------+-----------+--------------------------
+ * 1 | u8 | -- | sequence number
+ * 2-5 | u32 | see below | buttons
+ * 6-7 | s16 | ABS_HAT2Y | left trigger (uncalibrated)
+ * 8-9 | s16 | ABS_HAT2X | right trigger (uncalibrated)
+ * 10-11 | s16 | ABS_X | left joystick X
+ * 12-13 | s16 | ABS_Y | left joystick Y
+ * 14-15 | s16 | ABS_RX | right joystick X
+ * 16-17 | s16 | ABS_RY | right joystick Y
+ * 18-19 | s16 | ABS_HAT0X | left-pad X value
+ * 20-21 | s16 | ABS_HAT0Y | left-pad Y value
+ * 22-23 | u16 | -- | left pad pressure
+ * 24-25 | s16 | ABS_HAT1X | right-pad X value
+ * 26-27 | s16 | ABS_HAT1Y | right-pad Y value
+ * 28-29 | u16 | -- | right pad pressure
+ * 30-33 | u32 | IMU MSC_TIMESTAMP | IMU timestamp
+ * 34-35 | s16 | IMU ABS_X | accelerometer X value
+ * 36-37 | s16 | IMU ABS_Z | accelerometer Y value
+ * 38-39 | s16 | IMU ABS_Y | accelerometer Z value
+ * 40-41 | s16 | IMU ABS_RX | gyro X value
+ * 42-43 | s16 | IMU ABS_RZ | gyro Y value
+ * 44-45 | s16 | IMU ABS_RY | gyro Z value
+ * 46-47 | s16 | -- | * quaternion W value
+ * 48-49 | s16 | -- | * quaternion X value
+ * 50-51 | s16 | -- | * quaternion Y value
+ * 52-53 | s16 | -- | * quaternion Z value
+ *
+ * The buttons are:
+ * Bit | Mapped to | Description
+ * ------+------------+--------------------------------
+ * 2.0 | BTN_A | button A
+ * 2.1 | BTN_B | button B
+ * 2.2 | BTN_X | button X
+ * 2.3 | BTN_Y | button Y
+ * 2.4 | BTN_BASE | quick access button
+ * 2.5 | BTN_THUMBR | right joystick clicked
+ * 2.6 | BTN_START | menu
+ * 2.7 | BTN_GRIPR | right top grip button
+ * 3.0 | BTN_GRIPR2 | right bottom grip button
+ * 3.1 | BTN_TR | right shoulder
+ * 3.2 | BTN_DPAD_DOWN | left-pad down
+ * 3.3 | BTN_DPAD_RIGHT | left-pad right
+ * 3.4 | BTN_DPAD_LEFT | left-pad left
+ * 3.5 | BTN_DPAD_UP | left-pad up
+ * 3.6 | BTN_SELECT | view
+ * 3.7 | BTN_THUMBL | left joystick clicked
+ * 4.0 | BTN_MODE | steam logo
+ * 4.1 | BTN_GRIPL | left top grip button
+ * 4.2 | BTN_GRIPL2 | left bottom grip button
+ * 4.3 | BTN_TL | left shoulder
+ * 4.4 | -- | right joystick touched
+ * 4.5 | -- | right pad touched
+ * 4.6 | BTN_THUMB2 | right pad pressed
+ * 4.7 | BTN_TR2 | right trigger fully pressed
+ * 5.0 | -- | left joystick touched
+ * 5.1 | -- | left pad touched
+ * 5.2 | BTN_THUMB | left pad pressed
+ * 5.3 | BTN_TL2 | left trigger fully pressed
+ * 5.4 | -- | right grip touch
+ * 5.5 | -- | left grip touch
+ * 5.6 | -- | unmapped
+ * 5.7 | -- | unmapped
+ */
+
+static const struct steam_button_mapping steam_ibex_button_mappings[] = {
+ { BTN_A, 2, 0 },
+ { BTN_B, 2, 1 },
+ { BTN_X, 2, 2 },
+ { BTN_Y, 2, 3 },
+ { BTN_BASE, 2, 4 },
+ { BTN_THUMBR, 2, 5 },
+ { BTN_START, 2, 6 },
+ { BTN_GRIPR, 2, 7 },
+ { BTN_GRIPR2, 3, 0 },
+ { BTN_TR, 3, 1 },
+ { BTN_DPAD_DOWN, 3, 2 },
+ { BTN_DPAD_RIGHT, 3, 3 },
+ { BTN_DPAD_LEFT, 3, 4 },
+ { BTN_DPAD_UP, 3, 5 },
+ { BTN_SELECT, 3, 6 },
+ { BTN_THUMBL, 3, 7 },
+ { BTN_MODE, 4, 0 },
+ { BTN_GRIPL, 4, 1 },
+ { BTN_GRIPL2, 4, 2 },
+ { BTN_TL, 4, 3 },
+ { BTN_THUMB2, 4, 6 },
+ { BTN_TR2, 4, 7 },
+ { BTN_THUMB, 5, 2 },
+ { BTN_TL2, 5, 3 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_ibex_axis_mappings[] = {
+ { ABS_X, 1, 10 },
+ { ABS_Y, -1, 12 },
+ { ABS_RX, 1, 14 },
+ { ABS_RY, -1, 16 },
+ { ABS_HAT2Y, 1, 6 },
+ { ABS_HAT2X, 1, 8 },
+ { /* sentinel */ },
+};
+
+static const struct steam_axis_mapping steam_ibex_imu_mappings[] = {
+ { ABS_X, 1, 34 },
+ { ABS_Z, -1, 36 },
+ { ABS_Y, 1, 38 },
+ { ABS_RX, 1, 40 },
+ { ABS_RZ, -1, 42 },
+ { ABS_RY, 1, 44 },
+ { /* sentinel */ },
+};
+
+static void steam_do_ibex_input_event(struct steam_device *steam,
+ struct input_dev *input, const u8 *data)
+{
+ bool start_pressed;
+ bool lpad_touched, rpad_touched;
+
+ start_pressed = data[2] & BIT(6);
+
+ if (!start_pressed && steam->did_mode_switch) {
+ steam->did_mode_switch = false;
+ cancel_delayed_work(&steam->mode_switch);
+ } else if (!steam->client_opened && start_pressed && !steam->did_mode_switch) {
+ steam->did_mode_switch = true;
+ schedule_delayed_work(&steam->mode_switch, 45 * HZ / 100);
+ }
+
+ if (!steam->gamepad_mode && lizard_mode)
+ return;
+
+ lpad_touched = data[5] & BIT(1);
+ rpad_touched = data[4] & BIT(5);
+
+ if (lpad_touched) {
+ input_report_abs(input, ABS_HAT0X, steam_le16(data + 18));
+ input_report_abs(input, ABS_HAT0Y, steam_le16(data + 20));
+ } else {
+ input_report_abs(input, ABS_HAT0X, 0);
+ input_report_abs(input, ABS_HAT0Y, 0);
+ }
+
+ if (rpad_touched) {
+ input_report_abs(input, ABS_HAT1X, steam_le16(data + 24));
+ input_report_abs(input, ABS_HAT1Y, steam_le16(data + 26));
+ } else {
+ input_report_abs(input, ABS_HAT1X, 0);
+ input_report_abs(input, ABS_HAT1Y, 0);
+ }
+ steam_map_buttons(input, steam_ibex_button_mappings, data);
+ steam_map_axes(input, steam_ibex_axis_mappings, data);
+
+ input_sync(input);
+}
+
+static void steam_do_ibex_sensors_event(struct steam_device *steam,
+ struct input_dev *sensors, const u8 *data)
+{
+ u32 timestamp;
+
+ if (!steam->gamepad_mode && lizard_mode)
+ return;
+
+ timestamp = (u32) get_unaligned_le32((__le32 *)&data[30]);
+ input_event(sensors, EV_MSC, MSC_TIMESTAMP, timestamp);
+ steam_map_axes(sensors, steam_ibex_imu_mappings, data);
+
+ input_sync(sensors);
+}
+
+static void steam_do_ibex_battery_event(struct steam_device *steam,
+ struct power_supply *battery,
+ const struct steam_ibex_battery_status *data)
+{
+ /* Creating the battery may have failed */
+ guard(rcu)();
+ battery = rcu_dereference(steam->battery);
+ if (!likely(battery))
+ return;
+
+ steam->battery_voltage = get_unaligned_le16(&data->battery_voltage);
+ steam->battery_current = get_unaligned_le16(&data->battery_current);
+ steam->battery_temp = get_unaligned_le16(&data->temperature);
+ steam->battery_charge = data->battery_level;
+ switch (data->charge_state) {
+ case CHARGE_STATE_CHARGING:
+ steam->battery_status = POWER_SUPPLY_STATUS_CHARGING;
+ break;
+ case CHARGE_STATE_DISCHARGING:
+ steam->battery_status = POWER_SUPPLY_STATUS_DISCHARGING;
+ break;
+ case CHARGE_STATE_CHARGING_DONE:
+ steam->battery_status = POWER_SUPPLY_STATUS_FULL;
+ break;
+ default:
+ steam->battery_status = POWER_SUPPLY_STATUS_UNKNOWN;
+ break;
+ }
+ power_supply_changed(battery);
+}
+
static int steam_raw_event(struct hid_device *hdev,
struct hid_report *report, u8 *data,
int size)
@@ -1742,9 +2539,86 @@ static int steam_raw_event(struct hid_device *hdev,
if (!steam)
return 0;
+ guard(spinlock_irqsave)(&steam->lock);
if (steam->client_opened)
- hid_input_report(steam->client_hdev, HID_FEATURE_REPORT,
- data, size, 0);
+ hid_input_report(steam->client_hdev, report->type, data, size, 0);
+
+ /* Ibex uses a different report format */
+ if (steam->quirks & STEAM_QUIRK_IBEX) {
+ if (report->type != HID_INPUT_REPORT)
+ return 0;
+
+ switch (report->id) {
+ case REPORT_ID_INPUT:
+ if (size != 54)
+ return 0;
+ if (steam->client_opened)
+ return 0;
+ rcu_read_lock();
+ input = rcu_dereference(steam->input);
+ if (likely(input)) {
+ steam_do_ibex_input_event(steam, input, data);
+ } else {
+ dbg_hid("%s: input data without connect event\n",
+ __func__);
+ steam_do_connect_event(steam, true);
+ }
+ sensors = rcu_dereference(steam->sensors);
+ if (likely(sensors))
+ steam_do_ibex_sensors_event(steam, sensors, data);
+ rcu_read_unlock();
+ break;
+ case REPORT_ID_INPUT2:
+ if (size != 46)
+ return 0;
+ if (steam->client_opened)
+ return 0;
+ rcu_read_lock();
+ input = rcu_dereference(steam->input);
+ if (likely(input)) {
+ steam_do_ibex_input_event(steam, input, data);
+ } else {
+ dbg_hid("%s: input data without connect event\n",
+ __func__);
+ steam_do_connect_event(steam, true);
+ }
+ sensors = rcu_dereference(steam->sensors);
+ if (likely(sensors))
+ steam_do_ibex_sensors_event(steam, sensors, data);
+ rcu_read_unlock();
+ break;
+ case REPORT_ID_BATTERY:
+ if (size != 15)
+ return 0;
+ rcu_read_lock();
+ battery = rcu_dereference(steam->battery);
+ if (likely(battery)) {
+ steam_do_ibex_battery_event(steam, battery,
+ (const struct steam_ibex_battery_status *)&data[1]);
+ } else {
+ dbg_hid("%s: battery data without connect event\n",
+ __func__);
+ steam_do_connect_event(steam, true);
+ }
+ rcu_read_unlock();
+ break;
+ case REPORT_ID_WIRELESS_EVENT:
+ if (size != 2)
+ return 0;
+ switch (data[1]) {
+ case WIRELESS_EVENT_DISCONNECT:
+ steam_do_connect_event(steam, false);
+ break;
+ case WIRELESS_EVENT_CONNECT:
+ steam_do_connect_event(steam, true);
+ break;
+ }
+ break;
+ }
+
+ return 0;
+ }
+
/*
* All messages are size=64, all values little-endian.
* The format is:
@@ -1773,6 +2647,9 @@ static int steam_raw_event(struct hid_device *hdev,
input = rcu_dereference(steam->input);
if (likely(input))
steam_do_input_event(steam, input, data);
+ sensors = rcu_dereference(steam->sensors);
+ if (likely(sensors))
+ steam_do_sensors_event(steam, sensors, data);
rcu_read_unlock();
break;
case ID_CONTROLLER_DECK_STATE:
@@ -1826,6 +2703,8 @@ static int steam_param_set_lizard_mode(const char *val,
{
struct steam_device *steam;
int ret;
+ bool client_opened;
+ unsigned long flags;
ret = param_set_bool(val, kp);
if (ret)
@@ -1833,8 +2712,13 @@ static int steam_param_set_lizard_mode(const char *val,
mutex_lock(&steam_devices_lock);
list_for_each_entry(steam, &steam_devices, list) {
- if (!steam->client_opened)
+ spin_lock_irqsave(&steam->lock, flags);
+ client_opened = steam->client_opened;
+ spin_unlock_irqrestore(&steam->lock, flags);
+ if (!client_opened) {
+ guard(mutex)(&steam->report_mutex);
steam_set_lizard_mode(steam, lizard_mode);
+ }
}
mutex_unlock(&steam_devices_lock);
return 0;
@@ -1850,11 +2734,11 @@ MODULE_PARM_DESC(lizard_mode,
"Enable mouse and keyboard emulation (lizard mode) when the gamepad is not in use");
static const struct hid_device_id steam_controllers[] = {
- { /* Wired Steam Controller */
+ { /* Wired Steam Controller (2015) */
HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
USB_DEVICE_ID_STEAM_CONTROLLER)
},
- { /* Wireless Steam Controller */
+ { /* Wireless Steam Controller (2015) */
HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
USB_DEVICE_ID_STEAM_CONTROLLER_WIRELESS),
.driver_data = STEAM_QUIRK_WIRELESS
@@ -1864,6 +2748,26 @@ static const struct hid_device_id steam_controllers[] = {
USB_DEVICE_ID_STEAM_DECK),
.driver_data = STEAM_QUIRK_DECK
},
+ { /* Steam Controller (2026) wired */
+ HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
+ USB_DEVICE_ID_STEAM_CONTROLLER_IBEX),
+ .driver_data = STEAM_QUIRK_IBEX
+ },
+ { /* Steam Controller (2026) BLE */
+ HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_VALVE,
+ USB_DEVICE_ID_STEAM_CONTROLLER_IBEX_BLE),
+ .driver_data = STEAM_QUIRK_IBEX | STEAM_QUIRK_BLE
+ },
+ { /* Steam Controller (2026) Puck */
+ HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
+ USB_DEVICE_ID_STEAM_CONTROLLER_PROTEUS),
+ .driver_data = STEAM_QUIRK_IBEX | STEAM_QUIRK_WIRELESS
+ },
+ { /* Steam Controller (2026) Steam Machine internal receiver */
+ HID_USB_DEVICE(USB_VENDOR_ID_VALVE,
+ USB_DEVICE_ID_STEAM_CONTROLLER_NEREID),
+ .driver_data = STEAM_QUIRK_IBEX | STEAM_QUIRK_WIRELESS
+ },
{}
};
diff --git a/drivers/hid/hid-steelseries-arctis.c b/drivers/hid/hid-steelseries-arctis.c
new file mode 100644
index 000000000000..23fb0cebd72a
--- /dev/null
+++ b/drivers/hid/hid-steelseries-arctis.c
@@ -0,0 +1,718 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for Steelseries arctis headsets
+ *
+ * Copyright (c) 2023 Bastien Nocera
+ * Copyright (c) 2026 Sriman Achanta
+ */
+
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/kref.h>
+#include <linux/slab.h>
+#include <linux/module.h>
+#include <linux/power_supply.h>
+#include <linux/spinlock.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define SS_CAP_BATTERY BIT(0)
+
+struct steelseries_device;
+
+struct steelseries_device_info {
+ unsigned long capabilities;
+
+ u8 sync_interface;
+ u8 async_interface;
+
+ int (*request_status)(struct hid_device *hdev);
+ void (*parse_status)(struct steelseries_device *sd, u8 *data, int size);
+};
+
+struct steelseries_device {
+ struct kref refcnt;
+
+ struct hid_device *hdev;
+ const struct steelseries_device_info *info;
+
+ struct delayed_work status_work;
+
+ struct power_supply_desc battery_desc;
+ struct power_supply *battery;
+ bool headset_connected;
+ u8 battery_capacity;
+ bool battery_charging;
+
+ spinlock_t lock;
+ bool removed;
+};
+
+static void steelseries_device_release(struct kref *ref)
+{
+ struct steelseries_device *sd =
+ container_of(ref, struct steelseries_device, refcnt);
+
+ kfree(sd);
+}
+
+/*
+ * Headset report helpers
+ */
+
+static int steelseries_send_report(struct hid_device *hdev, const u8 *data,
+ int len, enum hid_report_type type)
+{
+ u8 *buf;
+ int ret;
+
+ buf = kmemdup(data, len, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = hid_hw_raw_request(hdev, data[0], buf, len, type,
+ HID_REQ_SET_REPORT);
+ kfree(buf);
+
+ if (ret < 0)
+ return ret;
+ if (ret < len)
+ return -EIO;
+
+ return 0;
+}
+
+static inline int steelseries_send_output_report(struct hid_device *hdev,
+ const u8 *data, int len)
+{
+ return steelseries_send_report(hdev, data, len, HID_OUTPUT_REPORT);
+}
+
+/*
+ * Headset status request functions
+ */
+
+static int steelseries_arctis_1_request_status(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x06, 0x12 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_9_request_status(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x00, 0x20 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+static int steelseries_arctis_nova_request_status(struct hid_device *hdev)
+{
+ const u8 data[] = { 0x00, 0xb0 };
+
+ return steelseries_send_output_report(hdev, data, sizeof(data));
+}
+
+/*
+ * Headset battery helpers
+ */
+
+static int battery_capacity_to_level(int capacity)
+{
+ if (capacity >= 50)
+ return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
+ if (capacity >= 20)
+ return POWER_SUPPLY_CAPACITY_LEVEL_LOW;
+ return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
+}
+
+static u8 steelseries_map_capacity(u8 capacity, u8 min_in, u8 max_in)
+{
+ if (capacity >= max_in)
+ return 100;
+ if (capacity <= min_in)
+ return 0;
+ return (capacity - min_in) * 100 / (max_in - min_in);
+}
+
+/*
+ * Headset status parse functions
+ */
+
+static void steelseries_arctis_1_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ /* Only the battery status report echoes the request header. */
+ if (size < 8 || data[0] != 0x06 || data[1] != 0x12)
+ return;
+
+ sd->headset_connected = (data[2] != 0x01);
+ sd->battery_capacity = data[3];
+}
+
+static void steelseries_arctis_9_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 5)
+ return;
+
+ if (data[0] == 0xaa && data[1] == 0x01) {
+ sd->headset_connected = true;
+ sd->battery_charging = (data[4] == 0x01);
+ sd->battery_capacity = steelseries_map_capacity(data[3], 0x64, 0x9a);
+ } else {
+ /* Device off: 0x55 (no status) or 0x03 (stale status). */
+ sd->headset_connected = false;
+ sd->battery_charging = false;
+ }
+}
+
+static void steelseries_arctis_nova_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 2)
+ return;
+
+ switch (data[0]) {
+ case 0xb0:
+ if (size < 4)
+ return;
+ sd->headset_connected = (data[1] == 0x03);
+ sd->battery_capacity = data[2];
+ sd->battery_charging = (data[3] == 0x01);
+ break;
+ case 0xb7:
+ sd->battery_capacity = data[1];
+ break;
+ case 0xb9:
+ sd->headset_connected = (data[1] == 0x03);
+ break;
+ case 0xbb:
+ sd->battery_charging = (data[1] == 0x01);
+ break;
+ }
+}
+
+static void steelseries_arctis_nova_7_parse_status(struct steelseries_device *sd,
+ u8 *data, int size)
+{
+ if (size < 2)
+ return;
+
+ switch (data[0]) {
+ case 0xb0:
+ if (size < 4)
+ return;
+ sd->headset_connected = (data[1] == 0x03);
+ sd->battery_capacity = steelseries_map_capacity(data[2], 0, 4);
+ sd->battery_charging = (data[3] == 0x01);
+ break;
+ case 0xb7:
+ sd->battery_capacity = steelseries_map_capacity(data[1], 0, 4);
+ break;
+ case 0xb9:
+ sd->headset_connected = (data[1] == 0x03);
+ break;
+ case 0xbb:
+ sd->battery_charging = (data[1] == 0x01);
+ break;
+ }
+}
+
+/*
+ * Device info definitions
+ */
+
+static const struct steelseries_device_info arctis_1_info = {
+ .sync_interface = 3,
+ .capabilities = SS_CAP_BATTERY,
+ .request_status = steelseries_arctis_1_request_status,
+ .parse_status = steelseries_arctis_1_parse_status,
+};
+
+static const struct steelseries_device_info arctis_9_info = {
+ .sync_interface = 0,
+ .capabilities = SS_CAP_BATTERY,
+ .request_status = steelseries_arctis_9_request_status,
+ .parse_status = steelseries_arctis_9_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_info = {
+ .sync_interface = 3,
+ .async_interface = 5,
+ .capabilities = SS_CAP_BATTERY,
+ .request_status = steelseries_arctis_nova_request_status,
+ .parse_status = steelseries_arctis_nova_parse_status,
+};
+
+static const struct steelseries_device_info arctis_nova_7_info = {
+ .sync_interface = 3,
+ .async_interface = 5,
+ .capabilities = SS_CAP_BATTERY,
+ .request_status = steelseries_arctis_nova_request_status,
+ .parse_status = steelseries_arctis_nova_7_parse_status,
+};
+
+/*
+ * Headset wireless status and battery infrastructure
+ */
+
+#define STEELSERIES_HEADSET_STATUS_TIMEOUT_MS 3000
+
+static void
+steelseries_headset_set_wireless_status(struct hid_device *hdev,
+ bool connected)
+{
+ struct usb_interface *intf;
+
+ if (!hid_is_usb(hdev))
+ return;
+
+ intf = to_usb_interface(hdev->dev.parent);
+ usb_set_wireless_status(intf, connected ?
+ USB_WIRELESS_STATUS_CONNECTED :
+ USB_WIRELESS_STATUS_DISCONNECTED);
+}
+
+#define STEELSERIES_PREFIX "SteelSeries "
+
+static int steelseries_battery_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct steelseries_device *sd = power_supply_get_drvdata(psy);
+ size_t prefix_len;
+ int ret = 0;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_MODEL_NAME:
+ val->strval = sd->hdev->name;
+ while ((prefix_len = str_has_prefix(val->strval, STEELSERIES_PREFIX)))
+ val->strval += prefix_len;
+ break;
+ case POWER_SUPPLY_PROP_MANUFACTURER:
+ val->strval = "SteelSeries";
+ break;
+ case POWER_SUPPLY_PROP_PRESENT:
+ val->intval = 1;
+ break;
+ case POWER_SUPPLY_PROP_STATUS:
+ if (!sd->headset_connected)
+ val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+ else if (sd->battery_charging)
+ val->intval = sd->battery_capacity >= 100 ?
+ POWER_SUPPLY_STATUS_FULL :
+ POWER_SUPPLY_STATUS_CHARGING;
+ else
+ val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+ break;
+ case POWER_SUPPLY_PROP_SCOPE:
+ val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+ break;
+ case POWER_SUPPLY_PROP_CAPACITY:
+ val->intval = sd->battery_capacity;
+ break;
+ case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
+ val->intval = battery_capacity_to_level(sd->battery_capacity);
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ return ret;
+}
+
+static enum power_supply_property steelseries_battery_props[] = {
+ POWER_SUPPLY_PROP_MODEL_NAME,
+ POWER_SUPPLY_PROP_MANUFACTURER,
+ POWER_SUPPLY_PROP_PRESENT,
+ POWER_SUPPLY_PROP_STATUS,
+ POWER_SUPPLY_PROP_SCOPE,
+ POWER_SUPPLY_PROP_CAPACITY,
+ POWER_SUPPLY_PROP_CAPACITY_LEVEL,
+};
+
+/*
+ * Delayed work handlers for status polling
+ */
+
+static void steelseries_status_timer_work_handler(struct work_struct *work)
+{
+ struct steelseries_device *sd = container_of(
+ work, struct steelseries_device, status_work.work);
+ unsigned long flags;
+
+ sd->info->request_status(sd->hdev);
+
+ spin_lock_irqsave(&sd->lock, flags);
+ /* Async devices push status events themselves; only poll once. */
+ if (!sd->removed && !sd->info->async_interface)
+ schedule_delayed_work(&sd->status_work,
+ msecs_to_jiffies(STEELSERIES_HEADSET_STATUS_TIMEOUT_MS));
+ spin_unlock_irqrestore(&sd->lock, flags);
+}
+
+static int steelseries_battery_register(struct steelseries_device *sd)
+{
+ struct power_supply_config battery_cfg = { .drv_data = sd, };
+ struct power_supply *battery;
+ int ret;
+
+ sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+ sd->battery_desc.properties = steelseries_battery_props;
+ sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_battery_props);
+ sd->battery_desc.get_property = steelseries_battery_get_property;
+ sd->battery_desc.use_for_apm = 0;
+ sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
+ "steelseries_headset_battery_%s",
+ sd->hdev->uniq[0] ? sd->hdev->uniq :
+ dev_name(&sd->hdev->dev));
+ if (!sd->battery_desc.name)
+ return -ENOMEM;
+
+ /* avoid the warning of 0% battery while waiting for the first info */
+ sd->battery_capacity = 100;
+ sd->battery_charging = false;
+ sd->headset_connected = false;
+ steelseries_headset_set_wireless_status(sd->hdev, false);
+
+ battery = power_supply_register(&sd->hdev->dev,
+ &sd->battery_desc, &battery_cfg);
+ if (IS_ERR(battery)) {
+ ret = PTR_ERR(battery);
+ hid_err(sd->hdev,
+ "%s:power_supply_register failed with error %d\n",
+ __func__, ret);
+ return ret;
+ }
+ power_supply_powers(battery, &sd->hdev->dev);
+
+ /* Assign on success only, so a concurrent raw_event never sees an ERR_PTR. */
+ sd->battery = battery;
+
+ return 0;
+}
+
+static struct hid_driver steelseries_arctis_driver;
+
+static struct steelseries_device *
+steelseries_get_sibling_sd(struct hid_device *hdev, int interface_num)
+{
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_device *usb_dev = interface_to_usbdev(intf);
+ struct usb_interface *sibling_intf;
+ struct hid_device *sibling_hdev;
+ struct steelseries_device *sd = NULL;
+
+ sibling_intf = usb_ifnum_to_if(usb_dev, interface_num);
+ if (!sibling_intf)
+ return NULL;
+
+ /*
+ * usb_get_intfdata() only yields a hid_device when usbhid is bound;
+ * gate on the descriptor class so a non-HID sibling (e.g. a crafted
+ * device exposing storage or audio here) is never treated as one.
+ */
+ if (sibling_intf->cur_altsetting->desc.bInterfaceClass != USB_INTERFACE_CLASS_HID)
+ return NULL;
+
+ /*
+ * Take the sibling's device lock across the intfdata read and the
+ * kref_get so a concurrent unbind cannot free the hid_device underneath
+ * us; usbhid leaves intfdata dangling on disconnect, so dev.driver is
+ * the reliable "still bound" test under this lock. Use device_trylock()
+ * to stay off the lockdep chain of the interface being probed and let
+ * the caller retry via -EPROBE_DEFER if the sibling is momentarily busy.
+ */
+ if (!device_trylock(&sibling_intf->dev))
+ return NULL;
+ if (sibling_intf->dev.driver) {
+ sibling_hdev = usb_get_intfdata(sibling_intf);
+ if (sibling_hdev &&
+ sibling_hdev->driver == &steelseries_arctis_driver) {
+ sd = hid_get_drvdata(sibling_hdev);
+ if (sd)
+ kref_get(&sd->refcnt);
+ }
+ }
+ device_unlock(&sibling_intf->dev);
+
+ return sd;
+}
+
+static int steelseries_arctis_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ const struct steelseries_device_info *info =
+ (const struct steelseries_device_info *)id->driver_data;
+ struct steelseries_device *sd;
+ struct usb_interface *intf;
+ u8 interface_num;
+ int ret;
+
+ if (hid_is_usb(hdev)) {
+ intf = to_usb_interface(hdev->dev.parent);
+ interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+ } else {
+ return -ENODEV;
+ }
+
+ ret = hid_parse(hdev);
+ if (ret)
+ return ret;
+
+ /* Let hid-generic handle non-vendor or unknown interfaces */
+ if (interface_num != info->sync_interface &&
+ (!info->async_interface || interface_num != info->async_interface))
+ return hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+
+ if (interface_num == info->sync_interface) {
+ sd = kzalloc_obj(*sd, GFP_KERNEL);
+ if (!sd)
+ return -ENOMEM;
+
+ kref_init(&sd->refcnt);
+ sd->hdev = hdev;
+ sd->info = info;
+ spin_lock_init(&sd->lock);
+ INIT_DELAYED_WORK(&sd->status_work, steelseries_status_timer_work_handler);
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ goto err_free;
+
+ ret = hid_hw_open(hdev);
+ if (ret)
+ goto err_stop;
+
+ if (info->capabilities & SS_CAP_BATTERY) {
+ ret = steelseries_battery_register(sd);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to register battery: %d\n", ret);
+ }
+
+ /*
+ * Publish drvdata only once fully initialised: the async sibling
+ * attaches by reading it, so it must never observe a half-built or
+ * failed instance. A failed probe never gets here, so the error
+ * path below has nothing to unpublish.
+ */
+ hid_set_drvdata(hdev, sd);
+ schedule_delayed_work(&sd->status_work, msecs_to_jiffies(100));
+
+ return 0;
+ }
+
+ /*
+ * The async interface shares the steelseries_device created by the
+ * sync interface. Defer until the sync interface has probed and
+ * published its drvdata.
+ */
+ if (info->async_interface && interface_num == info->async_interface) {
+ sd = steelseries_get_sibling_sd(hdev, info->sync_interface);
+ if (!sd)
+ return -EPROBE_DEFER;
+
+ hid_set_drvdata(hdev, sd);
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret) {
+ kref_put(&sd->refcnt, steelseries_device_release);
+ return ret;
+ }
+
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_hw_stop(hdev);
+ kref_put(&sd->refcnt, steelseries_device_release);
+ return ret;
+ }
+ return 0;
+ }
+
+ return -ENODEV;
+
+err_stop:
+ hid_hw_stop(hdev);
+err_free:
+ /* drvdata is unpublished until full success, so no sibling can hold sd. */
+ kref_put(&sd->refcnt, steelseries_device_release);
+ return ret;
+}
+
+static void steelseries_arctis_remove(struct hid_device *hdev)
+{
+ struct steelseries_device *sd;
+ struct power_supply *battery;
+ unsigned long flags;
+ struct usb_interface *intf;
+ u8 interface_num;
+
+ if (hid_is_usb(hdev)) {
+ intf = to_usb_interface(hdev->dev.parent);
+ interface_num = intf->cur_altsetting->desc.bInterfaceNumber;
+ } else {
+ return;
+ }
+
+ sd = hid_get_drvdata(hdev);
+
+ if (!sd) {
+ hid_hw_stop(hdev);
+ return;
+ }
+
+ if (interface_num == sd->info->sync_interface) {
+ spin_lock_irqsave(&sd->lock, flags);
+ sd->removed = true;
+ battery = sd->battery;
+ sd->battery = NULL;
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ cancel_delayed_work_sync(&sd->status_work);
+
+ if (battery)
+ power_supply_unregister(battery);
+ }
+
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+
+ kref_put(&sd->refcnt, steelseries_device_release);
+}
+
+static int steelseries_arctis_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct steelseries_device *sd = hid_get_drvdata(hdev);
+ u8 old_capacity;
+ bool old_connected;
+ bool old_charging;
+ bool is_async_interface;
+ unsigned long flags;
+
+ if (!sd)
+ return 0;
+
+ is_async_interface = (hdev != sd->hdev);
+
+ spin_lock_irqsave(&sd->lock, flags);
+
+ if (sd->removed) {
+ spin_unlock_irqrestore(&sd->lock, flags);
+ return 0;
+ }
+
+ old_capacity = sd->battery_capacity;
+ old_connected = sd->headset_connected;
+ old_charging = sd->battery_charging;
+
+ sd->info->parse_status(sd, data, size);
+
+ if (sd->headset_connected != old_connected) {
+ hid_dbg(hdev,
+ "Connected status changed from %sconnected to %sconnected\n",
+ old_connected ? "" : "not ",
+ sd->headset_connected ? "" : "not ");
+
+ if (sd->headset_connected && !old_connected &&
+ sd->info->async_interface && is_async_interface)
+ schedule_delayed_work(&sd->status_work, 0);
+
+ if (sd->battery) {
+ steelseries_headset_set_wireless_status(sd->hdev,
+ sd->headset_connected);
+ power_supply_changed(sd->battery);
+ }
+ }
+
+ if (sd->battery_capacity != old_capacity) {
+ hid_dbg(hdev, "Battery capacity changed from %d%% to %d%%\n",
+ old_capacity, sd->battery_capacity);
+ if (sd->battery)
+ power_supply_changed(sd->battery);
+ }
+
+ if (sd->battery_charging != old_charging) {
+ hid_dbg(hdev,
+ "Battery charging status changed from %scharging to %scharging\n",
+ old_charging ? "" : "not ",
+ sd->battery_charging ? "" : "not ");
+ if (sd->battery)
+ power_supply_changed(sd->battery);
+ }
+
+ spin_unlock_irqrestore(&sd->lock, flags);
+
+ return 0;
+}
+
+static const struct hid_device_id steelseries_arctis_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_1_X),
+ .driver_data = (unsigned long)&arctis_1_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
+ .driver_data = (unsigned long)&arctis_9_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_5_X),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_WOW),
+ .driver_data = (unsigned long)&arctis_nova_7_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_2026),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_P_2026),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_2026),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_DIABLO_2026),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_GEN2),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_2),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ { HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES,
+ USB_DEVICE_ID_STEELSERIES_ARCTIS_NOVA_7_X_GEN2_3),
+ .driver_data = (unsigned long)&arctis_nova_info },
+ {}
+};
+MODULE_DEVICE_TABLE(hid, steelseries_arctis_devices);
+
+static struct hid_driver steelseries_arctis_driver = {
+ .name = "hid-steelseries-arctis",
+ .id_table = steelseries_arctis_devices,
+ .probe = steelseries_arctis_probe,
+ .remove = steelseries_arctis_remove,
+ .raw_event = steelseries_arctis_raw_event,
+};
+
+module_hid_driver(steelseries_arctis_driver);
+MODULE_DESCRIPTION("HID driver for Steelseries arctis headsets");
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
+MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
+MODULE_AUTHOR("Sriman Achanta <srimanachanta@gmail.com>");
diff --git a/drivers/hid/hid-steelseries.c b/drivers/hid/hid-steelseries.c
index f98435631aa1..7292da0313e1 100644
--- a/drivers/hid/hid-steelseries.c
+++ b/drivers/hid/hid-steelseries.c
@@ -3,37 +3,45 @@
* HID driver for Steelseries devices
*
* Copyright (c) 2013 Simon Wood
- * Copyright (c) 2023 Bastien Nocera
*/
/*
*/
#include <linux/device.h>
+#include <linux/dmi.h>
#include <linux/hid.h>
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/leds.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/slab.h>
#include "hid-ids.h"
#define STEELSERIES_SRWS1 BIT(0)
-#define STEELSERIES_ARCTIS_1 BIT(1)
-#define STEELSERIES_ARCTIS_9 BIT(2)
+#define STEELSERIES_MSI_RGB BIT(1)
+
+#define STEELSERIES_MSI_RGB_WVALUE 0x0300 /* Feature report, ID 0 */
+#define STEELSERIES_MSI_RGB_REPORT_LEN 524
+#define STEELSERIES_MSI_RGB_OPCODE 0x0c
+#define STEELSERIES_MSI_RGB_KLC_MODE 0x66
+#define STEELSERIES_MSI_RGB_ALC_MODE 0x06
+
+#define STEELSERIES_HAS_LEDS_MULTICOLOR \
+ (IS_BUILTIN(CONFIG_LEDS_CLASS_MULTICOLOR) || \
+ (IS_MODULE(CONFIG_LEDS_CLASS_MULTICOLOR) && IS_MODULE(CONFIG_HID_STEELSERIES)))
struct steelseries_device {
struct hid_device *hdev;
unsigned long quirks;
- struct delayed_work battery_work;
- spinlock_t lock;
- bool removed;
-
- struct power_supply_desc battery_desc;
- struct power_supply *battery;
- uint8_t battery_capacity;
- bool headset_connected;
- bool battery_charging;
+#if STEELSERIES_HAS_LEDS_MULTICOLOR
+ struct led_classdev_mc mc_cdev;
+ struct mc_subled subled_info[3];
+ struct mutex rgb_lock; /* protects rgb_buf */
+ u8 *rgb_buf;
+#endif
};
#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
@@ -340,193 +348,221 @@ err:
}
#endif
-#define STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS 3000
-
-#define ARCTIS_1_BATTERY_RESPONSE_LEN 8
-#define ARCTIS_9_BATTERY_RESPONSE_LEN 64
-static const char arctis_1_battery_request[] = { 0x06, 0x12 };
-static const char arctis_9_battery_request[] = { 0x00, 0x20 };
+static const struct dmi_system_id steelseries_msi_rgb_dmi_table[] = {
+ {
+ .matches = {
+ DMI_MATCH(DMI_SYS_VENDOR, "Micro-Star International Co., Ltd."),
+ DMI_MATCH(DMI_PRODUCT_NAME, "Raider A18 HX A9WJG"),
+ DMI_MATCH(DMI_BOARD_NAME, "MS-182L"),
+ },
+ },
+ { }
+};
-static int steelseries_headset_request_battery(struct hid_device *hdev,
- const char *request, size_t len)
+static struct usb_interface *steelseries_hid_to_usb_intf(struct hid_device *hdev)
{
- u8 *write_buf;
- int ret;
-
- /* Request battery information */
- write_buf = kmemdup(request, len, GFP_KERNEL);
- if (!write_buf)
- return -ENOMEM;
-
- hid_dbg(hdev, "Sending battery request report");
- ret = hid_hw_raw_request(hdev, request[0], write_buf, len,
- HID_OUTPUT_REPORT, HID_REQ_SET_REPORT);
- if (ret < (int)len) {
- hid_err(hdev, "hid_hw_raw_request() failed with %d\n", ret);
- ret = -ENODATA;
- }
+ if (!hid_is_usb(hdev))
+ return NULL;
- kfree(write_buf);
- return ret;
+ return to_usb_interface(hdev->dev.parent);
}
-static void steelseries_headset_fetch_battery(struct hid_device *hdev)
+static bool steelseries_msi_rgb_is_interface0(struct hid_device *hdev)
{
- int ret = 0;
+ struct usb_interface *intf = steelseries_hid_to_usb_intf(hdev);
+ struct usb_device *udev;
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1)
- ret = steelseries_headset_request_battery(hdev,
- arctis_1_battery_request, sizeof(arctis_1_battery_request));
- else if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9)
- ret = steelseries_headset_request_battery(hdev,
- arctis_9_battery_request, sizeof(arctis_9_battery_request));
+ if (!intf)
+ return false;
- if (ret < 0)
- hid_dbg(hdev,
- "Battery query failed (err: %d)\n", ret);
-}
+ udev = interface_to_usbdev(intf);
-static int battery_capacity_to_level(int capacity)
-{
- if (capacity >= 50)
- return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
- if (capacity >= 20)
- return POWER_SUPPLY_CAPACITY_LEVEL_LOW;
- return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
+ return intf == usb_ifnum_to_if(udev, 0);
}
-static void steelseries_headset_battery_timer_tick(struct work_struct *work)
+#if STEELSERIES_HAS_LEDS_MULTICOLOR
+
+static struct usb_device *steelseries_hid_to_usb_dev(struct hid_device *hdev)
{
- struct steelseries_device *sd = container_of(work,
- struct steelseries_device, battery_work.work);
- struct hid_device *hdev = sd->hdev;
+ struct usb_interface *intf = steelseries_hid_to_usb_intf(hdev);
- steelseries_headset_fetch_battery(hdev);
-}
+ if (!intf)
+ return NULL;
-#define STEELSERIES_PREFIX "SteelSeries "
-#define STEELSERIES_PREFIX_LEN strlen(STEELSERIES_PREFIX)
+ return interface_to_usbdev(intf);
+}
-static int steelseries_headset_battery_get_property(struct power_supply *psy,
- enum power_supply_property psp,
- union power_supply_propval *val)
+static int steelseries_msi_rgb_set_blocking(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
{
- struct steelseries_device *sd = power_supply_get_drvdata(psy);
- int ret = 0;
-
- switch (psp) {
- case POWER_SUPPLY_PROP_MODEL_NAME:
- val->strval = sd->hdev->name;
- while (!strncmp(val->strval, STEELSERIES_PREFIX, STEELSERIES_PREFIX_LEN))
- val->strval += STEELSERIES_PREFIX_LEN;
- break;
- case POWER_SUPPLY_PROP_MANUFACTURER:
- val->strval = "SteelSeries";
- break;
- case POWER_SUPPLY_PROP_PRESENT:
- val->intval = 1;
- break;
- case POWER_SUPPLY_PROP_STATUS:
- if (sd->headset_connected) {
- val->intval = sd->battery_charging ?
- POWER_SUPPLY_STATUS_CHARGING :
- POWER_SUPPLY_STATUS_DISCHARGING;
- } else
- val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
- break;
- case POWER_SUPPLY_PROP_SCOPE:
- val->intval = POWER_SUPPLY_SCOPE_DEVICE;
- break;
- case POWER_SUPPLY_PROP_CAPACITY:
- val->intval = sd->battery_capacity;
- break;
- case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
- val->intval = battery_capacity_to_level(sd->battery_capacity);
- break;
- default:
- ret = -EINVAL;
- break;
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(led_cdev);
+ struct steelseries_device *sd = container_of(mc_cdev,
+ struct steelseries_device,
+ mc_cdev);
+ struct hid_device *hdev = sd->hdev;
+ struct usb_device *udev = steelseries_hid_to_usb_dev(hdev);
+ int i, ret;
+ u8 r, g, b;
+
+ static const u8 keys[] = {
+ 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
+ 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13,
+ 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
+ 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23,
+ 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
+ 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x33, 0x34,
+ 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c,
+ 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44,
+ 0x45, 0x46, 0x47, 0x49, 0x4b, 0x4c, 0x4e, 0x4f,
+ 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
+ 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
+ 0x60, 0x61, 0x62, 0x63, 0x64, 0x66, 0xe0, 0xe1,
+ 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xf0
+ };
+ static const u8 alc_zones[] = { 0x00, 0x01, 0x02, 0x03 };
+
+ if (!udev)
+ return -ENODEV;
+
+ mutex_lock(&sd->rgb_lock);
+
+ led_mc_calc_color_components(mc_cdev, brightness);
+
+ r = mc_cdev->subled_info[0].brightness;
+ g = mc_cdev->subled_info[1].brightness;
+ b = mc_cdev->subled_info[2].brightness;
+
+ /*
+ * Report layout (524 bytes):
+ * Byte 0: Opcode (0x0c)
+ * Byte 1: 0x00
+ * Byte 2: Mode (0x66 for Keyboard, 0x06 for Lightbar)
+ * Byte 3: 0x00
+ * Bytes 4+: 4-byte chunks per LED (Index, R, G, B)
+ */
+ memset(sd->rgb_buf, 0, STEELSERIES_MSI_RGB_REPORT_LEN);
+ sd->rgb_buf[0] = STEELSERIES_MSI_RGB_OPCODE;
+ sd->rgb_buf[1] = 0x00;
+ sd->rgb_buf[3] = 0x00;
+
+ for (i = 0; i < (STEELSERIES_MSI_RGB_REPORT_LEN - 4) / 4; i++)
+ sd->rgb_buf[4 + i * 4] = 0xff;
+
+ if (hdev->product == USB_DEVICE_ID_STEELSERIES_MSI_KLC) {
+ sd->rgb_buf[2] = STEELSERIES_MSI_RGB_KLC_MODE;
+ for (i = 0; i < ARRAY_SIZE(keys); i++) {
+ sd->rgb_buf[4 + i * 4] = keys[i];
+ sd->rgb_buf[5 + i * 4] = r;
+ sd->rgb_buf[6 + i * 4] = g;
+ sd->rgb_buf[7 + i * 4] = b;
+ }
+ } else {
+ sd->rgb_buf[2] = STEELSERIES_MSI_RGB_ALC_MODE;
+ for (i = 0; i < ARRAY_SIZE(alc_zones); i++) {
+ sd->rgb_buf[4 + i * 4] = alc_zones[i];
+ sd->rgb_buf[5 + i * 4] = r;
+ sd->rgb_buf[6 + i * 4] = g;
+ sd->rgb_buf[7 + i * 4] = b;
+ }
}
- return ret;
-}
-static void
-steelseries_headset_set_wireless_status(struct hid_device *hdev,
- bool connected)
-{
- struct usb_interface *intf;
+ /*
+ * Send the vendor report verbatim with usb_control_msg(): byte 0 is a
+ * protocol opcode (0x0c), not a HID report ID, and the controller
+ * expects it under report ID 0 (wValue 0x0300). hid_hw_raw_request()
+ * would write the report number into byte 0, so the direct control
+ * transfer is used to keep the payload byte-identical to the tested
+ * userspace implementation.
+ */
+ ret = hid_hw_power(hdev, PM_HINT_FULLON);
+ if (ret < 0)
+ goto out_unlock;
- if (!hid_is_usb(hdev))
- return;
+ ret = usb_control_msg(udev, usb_sndctrlpipe(udev, 0),
+ HID_REQ_SET_REPORT,
+ USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
+ STEELSERIES_MSI_RGB_WVALUE, 0,
+ sd->rgb_buf, STEELSERIES_MSI_RGB_REPORT_LEN,
+ USB_CTRL_SET_TIMEOUT);
+
+ hid_hw_power(hdev, PM_HINT_NORMAL);
- intf = to_usb_interface(hdev->dev.parent);
- usb_set_wireless_status(intf, connected ?
- USB_WIRELESS_STATUS_CONNECTED :
- USB_WIRELESS_STATUS_DISCONNECTED);
+out_unlock:
+ mutex_unlock(&sd->rgb_lock);
+ return ret < 0 ? ret : 0;
}
-static enum power_supply_property steelseries_headset_battery_props[] = {
- POWER_SUPPLY_PROP_MODEL_NAME,
- POWER_SUPPLY_PROP_MANUFACTURER,
- POWER_SUPPLY_PROP_PRESENT,
- POWER_SUPPLY_PROP_STATUS,
- POWER_SUPPLY_PROP_SCOPE,
- POWER_SUPPLY_PROP_CAPACITY,
- POWER_SUPPLY_PROP_CAPACITY_LEVEL,
-};
+static void steelseries_msi_rgb_free_buf(void *data)
+{
+ kfree(data);
+}
-static int steelseries_headset_battery_register(struct steelseries_device *sd)
+static int steelseries_msi_rgb_register(struct steelseries_device *sd)
{
- static atomic_t battery_no = ATOMIC_INIT(0);
- struct power_supply_config battery_cfg = { .drv_data = sd, };
- unsigned long n;
+ struct hid_device *hdev = sd->hdev;
+ struct led_classdev *led_cdev;
int ret;
- sd->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY;
- sd->battery_desc.properties = steelseries_headset_battery_props;
- sd->battery_desc.num_properties = ARRAY_SIZE(steelseries_headset_battery_props);
- sd->battery_desc.get_property = steelseries_headset_battery_get_property;
- sd->battery_desc.use_for_apm = 0;
- n = atomic_inc_return(&battery_no) - 1;
- sd->battery_desc.name = devm_kasprintf(&sd->hdev->dev, GFP_KERNEL,
- "steelseries_headset_battery_%ld", n);
- if (!sd->battery_desc.name)
+ sd->rgb_buf = kzalloc(STEELSERIES_MSI_RGB_REPORT_LEN, GFP_KERNEL);
+ if (!sd->rgb_buf)
return -ENOMEM;
- /* avoid the warning of 0% battery while waiting for the first info */
- steelseries_headset_set_wireless_status(sd->hdev, false);
- sd->battery_capacity = 100;
- sd->battery_charging = false;
-
- sd->battery = devm_power_supply_register(&sd->hdev->dev,
- &sd->battery_desc, &battery_cfg);
- if (IS_ERR(sd->battery)) {
- ret = PTR_ERR(sd->battery);
- hid_err(sd->hdev,
- "%s:power_supply_register failed with error %d\n",
- __func__, ret);
+ ret = devm_add_action_or_reset(&hdev->dev,
+ steelseries_msi_rgb_free_buf,
+ sd->rgb_buf);
+ if (ret) {
+ sd->rgb_buf = NULL;
return ret;
}
- power_supply_powers(sd->battery, &sd->hdev->dev);
- INIT_DELAYED_WORK(&sd->battery_work, steelseries_headset_battery_timer_tick);
- steelseries_headset_fetch_battery(sd->hdev);
+ ret = devm_mutex_init(&hdev->dev, &sd->rgb_lock);
+ if (ret) {
+ devm_remove_action(&hdev->dev, steelseries_msi_rgb_free_buf,
+ sd->rgb_buf);
+ kfree(sd->rgb_buf);
+ sd->rgb_buf = NULL;
+ return ret;
+ }
- if (sd->quirks & STEELSERIES_ARCTIS_9) {
- /* The first fetch_battery request can remain unanswered in some cases */
- schedule_delayed_work(&sd->battery_work,
- msecs_to_jiffies(STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
+ sd->subled_info[0].color_index = LED_COLOR_ID_RED;
+ sd->subled_info[1].color_index = LED_COLOR_ID_GREEN;
+ sd->subled_info[2].color_index = LED_COLOR_ID_BLUE;
+ sd->subled_info[0].intensity = 255;
+ sd->subled_info[1].intensity = 255;
+ sd->subled_info[2].intensity = 255;
+ sd->subled_info[0].channel = 0;
+ sd->subled_info[1].channel = 1;
+ sd->subled_info[2].channel = 2;
+
+ sd->mc_cdev.subled_info = sd->subled_info;
+ sd->mc_cdev.num_colors = 3;
+
+ led_cdev = &sd->mc_cdev.led_cdev;
+ if (hdev->product == USB_DEVICE_ID_STEELSERIES_MSI_KLC)
+ led_cdev->name = "steelseries::kbd_backlight";
+ else
+ led_cdev->name = "steelseries::lightbar";
+
+ led_cdev->max_brightness = 255;
+ led_cdev->brightness_set_blocking = steelseries_msi_rgb_set_blocking;
+
+ ret = devm_led_classdev_multicolor_register(&hdev->dev, &sd->mc_cdev);
+ if (ret) {
+ devm_remove_action(&hdev->dev, steelseries_msi_rgb_free_buf,
+ sd->rgb_buf);
+ kfree(sd->rgb_buf);
+ sd->rgb_buf = NULL;
+ return ret;
}
return 0;
}
-
-static bool steelseries_is_vendor_usage_page(struct hid_device *hdev, uint8_t usage_page)
+#else
+static int steelseries_msi_rgb_register(struct steelseries_device *sd)
{
- return hdev->rdesc[0] == 0x06 &&
- hdev->rdesc[1] == usage_page &&
- hdev->rdesc[2] == 0xff;
+ return -ENODEV;
}
+#endif
static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
@@ -549,36 +585,45 @@ static int steelseries_probe(struct hid_device *hdev, const struct hid_device_id
sd->hdev = hdev;
sd->quirks = id->driver_data;
+ if (sd->quirks & STEELSERIES_MSI_RGB) {
+ if (!dmi_check_system(steelseries_msi_rgb_dmi_table) ||
+ !steelseries_msi_rgb_is_interface0(hdev)) {
+ hid_dbg(hdev, "MSI RGB quirk not applicable, using generic HID path\n");
+ sd->quirks &= ~STEELSERIES_MSI_RGB;
+ }
+ }
+
ret = hid_parse(hdev);
if (ret)
return ret;
- if (sd->quirks & STEELSERIES_ARCTIS_9 &&
- !steelseries_is_vendor_usage_page(hdev, 0xc0))
- return -ENODEV;
-
- spin_lock_init(&sd->lock);
-
ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
if (ret)
return ret;
ret = hid_hw_open(hdev);
if (ret)
- return ret;
+ goto err_stop;
+
+ if (sd->quirks & STEELSERIES_MSI_RGB) {
+ ret = steelseries_msi_rgb_register(sd);
+ if (ret) {
+ hid_warn(hdev,
+ "Failed to register MSI RGB LEDs: %d, continuing without RGB support\n",
+ ret);
+ sd->quirks &= ~STEELSERIES_MSI_RGB;
+ }
+ }
- if (steelseries_headset_battery_register(sd) < 0)
- hid_err(sd->hdev,
- "Failed to register battery for headset\n");
+ return 0;
+err_stop:
+ hid_hw_stop(hdev);
return ret;
}
static void steelseries_remove(struct hid_device *hdev)
{
- struct steelseries_device *sd;
- unsigned long flags;
-
if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1) {
#if IS_BUILTIN(CONFIG_LEDS_CLASS) || \
(IS_MODULE(CONFIG_LEDS_CLASS) && IS_MODULE(CONFIG_HID_STEELSERIES))
@@ -587,14 +632,6 @@ static void steelseries_remove(struct hid_device *hdev)
return;
}
- sd = hid_get_drvdata(hdev);
-
- spin_lock_irqsave(&sd->lock, flags);
- sd->removed = true;
- spin_unlock_irqrestore(&sd->lock, flags);
-
- cancel_delayed_work_sync(&sd->battery_work);
-
hid_hw_close(hdev);
hid_hw_stop(hdev);
}
@@ -615,122 +652,19 @@ static const __u8 *steelseries_srws1_report_fixup(struct hid_device *hdev,
return rdesc;
}
-static uint8_t steelseries_headset_map_capacity(uint8_t capacity, uint8_t min_in, uint8_t max_in)
-{
- if (capacity >= max_in)
- return 100;
- if (capacity <= min_in)
- return 0;
- return (capacity - min_in) * 100 / (max_in - min_in);
-}
-
-static int steelseries_headset_raw_event(struct hid_device *hdev,
- struct hid_report *report, u8 *read_buf,
- int size)
-{
- struct steelseries_device *sd = hid_get_drvdata(hdev);
- int capacity = sd->battery_capacity;
- bool connected = sd->headset_connected;
- bool charging = sd->battery_charging;
- unsigned long flags;
-
- /* Not a headset */
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_SRWS1)
- return 0;
-
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_1) {
- hid_dbg(sd->hdev,
- "Parsing raw event for Arctis 1 headset (%*ph)\n", size, read_buf);
- if (size < ARCTIS_1_BATTERY_RESPONSE_LEN ||
- memcmp(read_buf, arctis_1_battery_request, sizeof(arctis_1_battery_request))) {
- if (!delayed_work_pending(&sd->battery_work))
- goto request_battery;
- return 0;
- }
- if (read_buf[2] == 0x01) {
- connected = false;
- capacity = 100;
- } else {
- connected = true;
- capacity = read_buf[3];
- }
- }
-
- if (hdev->product == USB_DEVICE_ID_STEELSERIES_ARCTIS_9) {
- hid_dbg(sd->hdev,
- "Parsing raw event for Arctis 9 headset (%*ph)\n", size, read_buf);
- if (size < ARCTIS_9_BATTERY_RESPONSE_LEN) {
- if (!delayed_work_pending(&sd->battery_work))
- goto request_battery;
- return 0;
- }
-
- if (read_buf[0] == 0xaa && read_buf[1] == 0x01) {
- connected = true;
- charging = read_buf[4] == 0x01;
-
- /*
- * Found no official documentation about min and max.
- * Values defined by testing.
- */
- capacity = steelseries_headset_map_capacity(read_buf[3], 0x68, 0x9d);
- } else {
- /*
- * Device is off and sends the last known status read_buf[1] == 0x03 or
- * there is no known status of the device read_buf[0] == 0x55
- */
- connected = false;
- charging = false;
- }
- }
-
- if (connected != sd->headset_connected) {
- hid_dbg(sd->hdev,
- "Connected status changed from %sconnected to %sconnected\n",
- sd->headset_connected ? "" : "not ",
- connected ? "" : "not ");
- sd->headset_connected = connected;
- steelseries_headset_set_wireless_status(hdev, connected);
- }
-
- if (capacity != sd->battery_capacity) {
- hid_dbg(sd->hdev,
- "Battery capacity changed from %d%% to %d%%\n",
- sd->battery_capacity, capacity);
- sd->battery_capacity = capacity;
- power_supply_changed(sd->battery);
- }
-
- if (charging != sd->battery_charging) {
- hid_dbg(sd->hdev,
- "Battery charging status changed from %scharging to %scharging\n",
- sd->battery_charging ? "" : "not ",
- charging ? "" : "not ");
- sd->battery_charging = charging;
- power_supply_changed(sd->battery);
- }
-
-request_battery:
- spin_lock_irqsave(&sd->lock, flags);
- if (!sd->removed)
- schedule_delayed_work(&sd->battery_work,
- msecs_to_jiffies(STEELSERIES_HEADSET_BATTERY_TIMEOUT_MS));
- spin_unlock_irqrestore(&sd->lock, flags);
-
- return 0;
-}
-
static const struct hid_device_id steelseries_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_SRWS1),
.driver_data = STEELSERIES_SRWS1 },
- { /* SteelSeries Arctis 1 Wireless for XBox */
- HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_1),
- .driver_data = STEELSERIES_ARCTIS_1 },
+#if STEELSERIES_HAS_LEDS_MULTICOLOR
+ { /* MSI Raider A18 KLC */
+ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_MSI_KLC),
+ .driver_data = STEELSERIES_MSI_RGB },
- { /* SteelSeries Arctis 9 Wireless for XBox */
- HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_ARCTIS_9),
- .driver_data = STEELSERIES_ARCTIS_9 },
+ { /* MSI Raider A18 ALC */
+ HID_USB_DEVICE(USB_VENDOR_ID_STEELSERIES, USB_DEVICE_ID_STEELSERIES_MSI_ALC),
+ .driver_data = STEELSERIES_MSI_RGB },
+#endif
{ }
};
@@ -742,12 +676,9 @@ static struct hid_driver steelseries_driver = {
.probe = steelseries_probe,
.remove = steelseries_remove,
.report_fixup = steelseries_srws1_report_fixup,
- .raw_event = steelseries_headset_raw_event,
};
module_hid_driver(steelseries_driver);
MODULE_DESCRIPTION("HID driver for Steelseries devices");
MODULE_LICENSE("GPL");
-MODULE_AUTHOR("Bastien Nocera <hadess@hadess.net>");
MODULE_AUTHOR("Simon Wood <simon@mungewell.org>");
-MODULE_AUTHOR("Christian Mayer <git@mayer-bgk.de>");
diff --git a/drivers/hid/hid-tmff.c b/drivers/hid/hid-tmff.c
index 423f395d01ac..303769b07628 100644
--- a/drivers/hid/hid-tmff.c
+++ b/drivers/hid/hid-tmff.c
@@ -17,6 +17,7 @@
#include <linux/hid.h>
#include <linux/input.h>
+#include <linux/math64.h>
#include <linux/slab.h>
#include <linux/module.h>
@@ -47,9 +48,9 @@ struct tmff_device {
/* Changes values from 0 to 0xffff into values from minimum to maximum */
static inline int tmff_scale_u16(unsigned int in, int minimum, int maximum)
{
- int ret;
+ s64 ret;
- ret = (in * (maximum - minimum) / 0xffff) + minimum;
+ ret = div_s64((s64)in * ((s64)maximum - minimum), 0xffff) + minimum;
if (ret < minimum)
return minimum;
if (ret > maximum)
@@ -60,9 +61,9 @@ static inline int tmff_scale_u16(unsigned int in, int minimum, int maximum)
/* Changes values from -0x80 to 0x7f into values from minimum to maximum */
static inline int tmff_scale_s8(int in, int minimum, int maximum)
{
- int ret;
+ s64 ret;
- ret = (((in + 0x80) * (maximum - minimum)) / 0xff) + minimum;
+ ret = div_s64((s64)(in + 0x80) * ((s64)maximum - minimum), 0xff) + minimum;
if (ret < minimum)
return minimum;
if (ret > maximum)
@@ -115,22 +116,25 @@ static int tmff_play(struct input_dev *dev, void *data,
return 0;
}
-static int tmff_init(struct hid_device *hid, const signed short *ff_bits)
+static int tm_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct tmff_device *tmff;
struct hid_report *report;
struct list_head *report_list;
- struct hid_input *hidinput;
- struct input_dev *input_dev;
+ struct input_dev *input_dev = hidinput->input;
+ const struct hid_device_id *id;
+ const signed short *ff_bits;
int error;
int i;
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
+
+ id = hid_match_device(hid, hid->driver);
+ if (!id)
return -ENODEV;
- }
- hidinput = list_entry(hid->inputs.next, struct hid_input, list);
- input_dev = hidinput->input;
+
+ ff_bits = (void *)id->driver_data;
tmff = kzalloc_obj(struct tmff_device);
if (!tmff)
@@ -204,35 +208,13 @@ fail:
return error;
}
#else
-static inline int tmff_init(struct hid_device *hid, const signed short *ff_bits)
+static inline int tm_input_configured(struct hid_device *hid,
+ struct hid_input *hidinput)
{
return 0;
}
#endif
-static int tm_probe(struct hid_device *hdev, const struct hid_device_id *id)
-{
- int ret;
-
- ret = hid_parse(hdev);
- if (ret) {
- hid_err(hdev, "parse failed\n");
- goto err;
- }
-
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
- if (ret) {
- hid_err(hdev, "hw start failed\n");
- goto err;
- }
-
- tmff_init(hdev, (void *)id->driver_data);
-
- return 0;
-err:
- return ret;
-}
-
static const struct hid_device_id tm_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_THRUSTMASTER, 0xb300),
.driver_data = (unsigned long)ff_rumble },
@@ -261,7 +243,7 @@ MODULE_DEVICE_TABLE(hid, tm_devices);
static struct hid_driver tm_driver = {
.name = "thrustmaster",
.id_table = tm_devices,
- .probe = tm_probe,
+ .input_configured = tm_input_configured,
};
module_hid_driver(tm_driver);
diff --git a/drivers/hid/hid-uclogic-core.c b/drivers/hid/hid-uclogic-core.c
index b73f09d26688..0b8a83fa6c5b 100644
--- a/drivers/hid/hid-uclogic-core.c
+++ b/drivers/hid/hid-uclogic-core.c
@@ -548,7 +548,17 @@ static void uclogic_remove(struct hid_device *hdev)
{
struct uclogic_drvdata *drvdata = hid_get_drvdata(hdev);
- timer_delete_sync(&drvdata->inrange_timer);
+ /*
+ * Shut the in-range timer down before stopping the device.
+ * uclogic_raw_event_pen() re-arms inrange_timer on every pen report
+ * and keeps running until hid_hw_stop() stops the transport, so a
+ * plain timer_delete_sync() here can be undone by a report landing in
+ * the window before hid_hw_stop(). timer_shutdown_sync() cancels the
+ * timer and makes any later re-arm a no-op, so it is provably dead
+ * before hid_hw_stop() frees the input device drvdata->pen_input
+ * points at.
+ */
+ timer_shutdown_sync(&drvdata->inrange_timer);
hid_hw_stop(hdev);
kfree(drvdata->desc_ptr);
uclogic_params_cleanup(&drvdata->params);
diff --git a/drivers/hid/hid-universal-pidff.c b/drivers/hid/hid-universal-pidff.c
index 549dac555d40..60180467a0bf 100644
--- a/drivers/hid/hid-universal-pidff.c
+++ b/drivers/hid/hid-universal-pidff.c
@@ -104,12 +104,14 @@ static int universal_pidff_probe(struct hid_device *hdev,
error = init_function(hdev, id->driver_data);
if (error) {
hid_warn(hdev, "Error initialising force feedback\n");
- goto err;
+ goto err_stop;
}
hid_info(hdev, "Universal pidff driver loaded successfully!");
return 0;
+err_stop:
+ hid_hw_stop(hdev);
err:
return error;
}
diff --git a/drivers/hid/hid-zpff.c b/drivers/hid/hid-zpff.c
index d8e023c8aa84..b565c59d3dfe 100644
--- a/drivers/hid/hid-zpff.c
+++ b/drivers/hid/hid-zpff.c
@@ -50,20 +50,15 @@ static int zpff_play(struct input_dev *dev, void *data,
return 0;
}
-static int zpff_init(struct hid_device *hid)
+static int zp_input_configured(struct hid_device *hid, struct hid_input *hidinput)
{
struct zpff_device *zpff;
struct hid_report *report;
- struct hid_input *hidinput;
- struct input_dev *dev;
+ struct input_dev *dev = hidinput->input;
int i, error;
- if (list_empty(&hid->inputs)) {
- hid_err(hid, "no inputs found\n");
- return -ENODEV;
- }
- hidinput = list_entry(hid->inputs.next, struct hid_input, list);
- dev = hidinput->input;
+ if (!list_is_first(&hidinput->list, &hid->inputs))
+ return 0;
for (i = 0; i < 4; i++) {
report = hid_validate_values(hid, HID_OUTPUT_REPORT, 0, i, 1);
@@ -75,6 +70,7 @@ static int zpff_init(struct hid_device *hid)
if (!zpff)
return -ENOMEM;
+ zpff->report = report;
set_bit(FF_RUMBLE, dev->ffbit);
error = input_ff_create_memless(dev, zpff, zpff_play);
@@ -83,7 +79,6 @@ static int zpff_init(struct hid_device *hid)
return error;
}
- zpff->report = report;
zpff->report->field[0]->value[0] = 0x00;
zpff->report->field[1]->value[0] = 0x02;
zpff->report->field[2]->value[0] = 0x00;
@@ -95,35 +90,13 @@ static int zpff_init(struct hid_device *hid)
return 0;
}
#else
-static inline int zpff_init(struct hid_device *hid)
+static inline int zp_input_configured(struct hid_device *hid,
+ struct hid_input *hidinput)
{
return 0;
}
#endif
-static int zp_probe(struct hid_device *hdev, const struct hid_device_id *id)
-{
- int ret;
-
- ret = hid_parse(hdev);
- if (ret) {
- hid_err(hdev, "parse failed\n");
- goto err;
- }
-
- ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT & ~HID_CONNECT_FF);
- if (ret) {
- hid_err(hdev, "hw start failed\n");
- goto err;
- }
-
- zpff_init(hdev);
-
- return 0;
-err:
- return ret;
-}
-
static const struct hid_device_id zp_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_ZEROPLUS, 0x0005) },
{ HID_USB_DEVICE(USB_VENDOR_ID_ZEROPLUS, 0x0030) },
@@ -134,7 +107,7 @@ MODULE_DEVICE_TABLE(hid, zp_devices);
static struct hid_driver zp_driver = {
.name = "zeroplus",
.id_table = zp_devices,
- .probe = zp_probe,
+ .input_configured = zp_input_configured,
};
module_hid_driver(zp_driver);
diff --git a/drivers/hid/i2c-hid/Makefile b/drivers/hid/i2c-hid/Makefile
index 55bd5e0f35af..38d5d827f3ce 100644
--- a/drivers/hid/i2c-hid/Makefile
+++ b/drivers/hid/i2c-hid/Makefile
@@ -8,7 +8,7 @@ obj-$(CONFIG_I2C_HID_CORE) += i2c-hid.o
i2c-hid-objs = i2c-hid-core.o
i2c-hid-$(CONFIG_DMI) += i2c-hid-dmi-quirks.o
-obj-$(CONFIG_I2C_HID_ACPI) += i2c-hid-acpi.o
+obj-$(CONFIG_I2C_HID_ACPI) += i2c-hid-acpi.o i2c-hid-acpi-prp0001.o
obj-$(CONFIG_I2C_HID_OF) += i2c-hid-of.o
obj-$(CONFIG_I2C_HID_OF_ELAN) += i2c-hid-of-elan.o
obj-$(CONFIG_I2C_HID_OF_GOODIX) += i2c-hid-of-goodix.o
diff --git a/drivers/hid/i2c-hid/i2c-hid-acpi-prp0001.c b/drivers/hid/i2c-hid/i2c-hid-acpi-prp0001.c
new file mode 100644
index 000000000000..d2cf4714ae7f
--- /dev/null
+++ b/drivers/hid/i2c-hid/i2c-hid-acpi-prp0001.c
@@ -0,0 +1,104 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * HID over I2C driver for PRP0001 devices missing hid-descr-addr
+ *
+ * Some devices, for example the Lenovo KaiTian N60d and Inspur CP300L3, use
+ * _HID "PRP0001" with _DSD compatible "hid-over-i2c" but lack "hid-descr-addr"
+ * from the _DSD. The HID descriptor address is provided only through an ACPI
+ * _DSM. The TPD0 node in the DSDT shows _DSM Function 1 returning 0x20.
+ *
+ * Copyright (C) 2026 谢致邦 (XIE Zhibang) <Yeking@Red54.com>
+ */
+
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/of.h>
+
+#include "i2c-hid.h"
+#include "i2c-hid-acpi.h"
+
+static int i2c_hid_acpi_prp0001_power_up(struct i2chid_ops *ops)
+{
+ /* give the device time to power up */
+ msleep(750);
+ return 0;
+}
+
+static struct i2chid_ops i2c_hid_acpi_prp0001_ops = {
+ .power_up = i2c_hid_acpi_prp0001_power_up,
+ /*
+ * No .restore_sequence needed: the _DSM on these devices returns a
+ * constant (0x20) with no side effects, unlike some PNP0C50 _DSM
+ * implementations that switch the hardware between PS/2 and I2C modes.
+ */
+};
+
+static int i2c_hid_acpi_prp0001_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct acpi_device *adev;
+ u16 hid_descriptor_address;
+ int ret;
+
+ /* If hid-descr-addr is present, let i2c-hid-of handle it */
+ if (device_property_present(dev, "hid-descr-addr"))
+ return -ENODEV;
+
+ adev = ACPI_COMPANION(dev);
+ if (!adev)
+ return -ENODEV;
+
+ ret = i2c_hid_acpi_get_descriptor(adev);
+ if (ret < 0)
+ return ret;
+ dev_warn(dev,
+ "hid-descr-addr device property NOT found, using ACPI _DSM fallback. Contact vendor for firmware update!\n");
+ hid_descriptor_address = ret;
+
+ /*
+ * No acpi_device_fix_up_power() needed: TPD0 has no _PS0, _PS3, _PSC
+ * or _PRx methods and follows I2C bus power.
+ */
+ return i2c_hid_core_probe(client, &i2c_hid_acpi_prp0001_ops,
+ hid_descriptor_address, 0);
+}
+
+static const struct of_device_id i2c_hid_acpi_prp0001_of_match[] = {
+ { .compatible = "hid-over-i2c" },
+ {},
+};
+MODULE_DEVICE_TABLE(of, i2c_hid_acpi_prp0001_of_match);
+
+static const struct i2c_device_id i2c_hid_acpi_prp0001_id[] = {
+ { .name = "hid-over-i2c" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, i2c_hid_acpi_prp0001_id);
+
+static struct i2c_driver i2c_hid_acpi_prp0001_driver = {
+ .driver = {
+ .name = "i2c_hid_acpi_prp0001",
+ .pm = &i2c_hid_core_pm,
+ .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+ /*
+ * of_match_ptr() makes this NULL when CONFIG_OF=n, but that's
+ * fine: the I2C id_table with "hid-over-i2c" handles matching
+ * via client->name (set by acpi_set_modalias() from the _DSD
+ * compatible property).
+ */
+ .of_match_table = of_match_ptr(i2c_hid_acpi_prp0001_of_match),
+ },
+
+ .probe = i2c_hid_acpi_prp0001_probe,
+ .remove = i2c_hid_core_remove,
+ .shutdown = i2c_hid_core_shutdown,
+ .id_table = i2c_hid_acpi_prp0001_id,
+};
+
+module_i2c_driver(i2c_hid_acpi_prp0001_driver);
+
+MODULE_DESCRIPTION("HID over I2C driver for PRP0001 devices missing hid-descr-addr");
+MODULE_AUTHOR("谢致邦 (XIE Zhibang) <Yeking@Red54.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/hid/i2c-hid/i2c-hid-acpi.c b/drivers/hid/i2c-hid/i2c-hid-acpi.c
index abd700a101f4..13f977d6aab6 100644
--- a/drivers/hid/i2c-hid/i2c-hid-acpi.c
+++ b/drivers/hid/i2c-hid/i2c-hid-acpi.c
@@ -25,9 +25,9 @@
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/pm.h>
-#include <linux/uuid.h>
#include "i2c-hid.h"
+#include "i2c-hid-acpi.h"
struct i2c_hid_acpi {
struct i2chid_ops ops;
@@ -48,39 +48,11 @@ static const struct acpi_device_id i2c_hid_acpi_blacklist[] = {
{ }
};
-/* HID I²C Device: 3cdff6f7-4267-4555-ad05-b30a3d8938de */
-static guid_t i2c_hid_guid =
- GUID_INIT(0x3CDFF6F7, 0x4267, 0x4555,
- 0xAD, 0x05, 0xB3, 0x0A, 0x3D, 0x89, 0x38, 0xDE);
-
-static int i2c_hid_acpi_get_descriptor(struct i2c_hid_acpi *ihid_acpi)
-{
- struct acpi_device *adev = ihid_acpi->adev;
- acpi_handle handle = acpi_device_handle(adev);
- union acpi_object *obj;
- u16 hid_descriptor_address;
-
- if (acpi_match_device_ids(adev, i2c_hid_acpi_blacklist) == 0)
- return -ENODEV;
-
- obj = acpi_evaluate_dsm_typed(handle, &i2c_hid_guid, 1, 1, NULL,
- ACPI_TYPE_INTEGER);
- if (!obj) {
- acpi_handle_err(handle, "Error _DSM call to get HID descriptor address failed\n");
- return -ENODEV;
- }
-
- hid_descriptor_address = obj->integer.value;
- ACPI_FREE(obj);
-
- return hid_descriptor_address;
-}
-
static void i2c_hid_acpi_restore_sequence(struct i2chid_ops *ops)
{
struct i2c_hid_acpi *ihid_acpi = container_of(ops, struct i2c_hid_acpi, ops);
- i2c_hid_acpi_get_descriptor(ihid_acpi);
+ i2c_hid_acpi_get_descriptor(ihid_acpi->adev);
}
static void i2c_hid_acpi_shutdown_tail(struct i2chid_ops *ops)
@@ -93,24 +65,28 @@ static void i2c_hid_acpi_shutdown_tail(struct i2chid_ops *ops)
static int i2c_hid_acpi_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
+ struct acpi_device *adev = ACPI_COMPANION(dev);
struct i2c_hid_acpi *ihid_acpi;
u16 hid_descriptor_address;
int ret;
- ihid_acpi = devm_kzalloc(&client->dev, sizeof(*ihid_acpi), GFP_KERNEL);
+ if (acpi_match_device_ids(adev, i2c_hid_acpi_blacklist) == 0)
+ return -ENODEV;
+
+ ret = i2c_hid_acpi_get_descriptor(adev);
+ if (ret < 0)
+ return ret;
+ hid_descriptor_address = ret;
+
+ ihid_acpi = devm_kzalloc(dev, sizeof(*ihid_acpi), GFP_KERNEL);
if (!ihid_acpi)
return -ENOMEM;
- ihid_acpi->adev = ACPI_COMPANION(dev);
+ ihid_acpi->adev = adev;
ihid_acpi->ops.shutdown_tail = i2c_hid_acpi_shutdown_tail;
ihid_acpi->ops.restore_sequence = i2c_hid_acpi_restore_sequence;
- ret = i2c_hid_acpi_get_descriptor(ihid_acpi);
- if (ret < 0)
- return ret;
- hid_descriptor_address = ret;
-
- acpi_device_fix_up_power(ihid_acpi->adev);
+ acpi_device_fix_up_power(adev);
return i2c_hid_core_probe(client, &ihid_acpi->ops,
hid_descriptor_address, 0);
diff --git a/drivers/hid/i2c-hid/i2c-hid-acpi.h b/drivers/hid/i2c-hid/i2c-hid-acpi.h
new file mode 100644
index 000000000000..0bbed1853313
--- /dev/null
+++ b/drivers/hid/i2c-hid/i2c-hid-acpi.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef _I2C_HID_ACPI_H
+#define _I2C_HID_ACPI_H
+
+#include <linux/acpi.h>
+#include <linux/uuid.h>
+
+static inline int i2c_hid_acpi_get_descriptor(struct acpi_device *adev)
+{
+ /* HID I²C Device: 3cdff6f7-4267-4555-ad05-b30a3d8938de */
+ static const guid_t i2c_hid_guid =
+ GUID_INIT(0x3CDFF6F7, 0x4267, 0x4555,
+ 0xAD, 0x05, 0xB3, 0x0A, 0x3D, 0x89, 0x38, 0xDE);
+
+ acpi_handle handle = acpi_device_handle(adev);
+ union acpi_object *obj;
+ u16 addr;
+
+ obj = acpi_evaluate_dsm_typed(handle, &i2c_hid_guid,
+ 1, 1, NULL, ACPI_TYPE_INTEGER);
+ if (!obj) {
+ acpi_handle_err(handle,
+ "Error _DSM call to get HID descriptor address failed\n");
+ return -ENODEV;
+ }
+
+ addr = obj->integer.value;
+ ACPI_FREE(obj);
+ return addr;
+}
+
+#endif
diff --git a/drivers/hid/i2c-hid/i2c-hid-core.c b/drivers/hid/i2c-hid/i2c-hid-core.c
index 3adb16366e93..0e725a0f0abe 100644
--- a/drivers/hid/i2c-hid/i2c-hid-core.c
+++ b/drivers/hid/i2c-hid/i2c-hid-core.c
@@ -792,7 +792,7 @@ static int i2c_hid_parse(struct hid_device *hid)
ihid->hdesc.wReportDescRegister,
rdesc, rsize);
if (ret) {
- hid_err(hid, "reading report descriptor failed\n");
+ dev_err(&client->dev, "reading report descriptor failed\n");
goto out;
}
}
diff --git a/drivers/hid/i2c-hid/i2c-hid-of-goodix.c b/drivers/hid/i2c-hid/i2c-hid-of-goodix.c
index f1597ad67e7c..f4dbcd1d1d47 100644
--- a/drivers/hid/i2c-hid/i2c-hid-of-goodix.c
+++ b/drivers/hid/i2c-hid/i2c-hid-of-goodix.c
@@ -51,8 +51,10 @@ static int goodix_i2c_hid_power_up(struct i2chid_ops *ops)
return ret;
ret = regulator_enable(ihid_goodix->vddio);
- if (ret)
+ if (ret) {
+ regulator_disable(ihid_goodix->vdd);
return ret;
+ }
if (ihid_goodix->timings->post_power_delay_ms)
msleep(ihid_goodix->timings->post_power_delay_ms);
diff --git a/drivers/hid/intel-ish-hid/ipc/pci-ish.c b/drivers/hid/intel-ish-hid/ipc/pci-ish.c
index 8d36ae96a3ee..e7196e429c8b 100644
--- a/drivers/hid/intel-ish-hid/ipc/pci-ish.c
+++ b/drivers/hid/intel-ish-hid/ipc/pci-ish.c
@@ -232,10 +232,8 @@ static int ish_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
ret = devm_request_irq(dev, pdev->irq, ish_irq_handler,
irq_flag, KBUILD_MODNAME, ishtp);
- if (ret) {
- dev_err(dev, "ISH: request IRQ %d failed\n", pdev->irq);
+ if (ret)
return ret;
- }
dev_set_drvdata(ishtp->devc, ishtp);
diff --git a/drivers/hid/intel-ish-hid/ishtp-hid-client.c b/drivers/hid/intel-ish-hid/ishtp-hid-client.c
index 6d64008f2ce0..ec5e8afe77bd 100644
--- a/drivers/hid/intel-ish-hid/ishtp-hid-client.c
+++ b/drivers/hid/intel-ish-hid/ishtp-hid-client.c
@@ -113,8 +113,7 @@ static void process_recv(struct ishtp_cl *hid_ishtp_cl, void *recv_buf,
switch (recv_msg->hdr.command & CMD_MASK) {
case HOSTIF_DM_ENUM_DEVICES:
- if ((!(recv_msg->hdr.command & ~CMD_MASK) ||
- client_data->init_done)) {
+ if (!(recv_msg->hdr.command & ~CMD_MASK)) {
++client_data->bad_recv_cnt;
report_bad_packet(hid_ishtp_cl, recv_msg,
cur_pos,
@@ -122,6 +121,8 @@ static void process_recv(struct ishtp_cl *hid_ishtp_cl, void *recv_buf,
ish_hw_reset(ishtp_get_ishtp_device(hid_ishtp_cl));
break;
}
+ if (client_data->init_done)
+ break;
client_data->hid_dev_count = (unsigned int)*payload;
if (!client_data->hid_devices)
client_data->hid_devices = devm_kcalloc(
diff --git a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
index 46d3e9a01999..0d2ad7bc3648 100644
--- a/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
+++ b/drivers/hid/intel-thc-hid/intel-quicki2c/pci-quicki2c.c
@@ -175,7 +175,9 @@ static int quicki2c_get_acpi_resources(struct quicki2c_device *qcdev)
if (i2c_param.addressing_mode != HIDI2C_ADDRESSING_MODE_7BIT)
return -EOPNOTSUPP;
- qcdev->i2c_slave_addr = i2c_param.device_address;
+ qcdev->i2c_config.addr_mode = HIDI2C_ADDRESSING_MODE_7BIT;
+
+ qcdev->i2c_config.target_addr = i2c_param.device_address;
ret = quicki2c_acpi_get_dsd_property(adev, QUICKI2C_ACPI_METHOD_NAME_ISUB,
ACPI_TYPE_BUFFER, &i2c_config);
@@ -184,24 +186,32 @@ static int quicki2c_get_acpi_resources(struct quicki2c_device *qcdev)
if (i2c_param.connection_speed > 0 &&
i2c_param.connection_speed <= QUICKI2C_SUBIP_STANDARD_MODE_MAX_SPEED) {
- qcdev->i2c_speed_mode = THC_I2C_STANDARD;
- qcdev->i2c_clock_hcnt = i2c_config.SMHX;
- qcdev->i2c_clock_lcnt = i2c_config.SMLX;
+ qcdev->i2c_config.speed = THC_I2C_STANDARD;
+ qcdev->i2c_config.scl_hcnt = (u32)i2c_config.SMHX;
+ qcdev->i2c_config.scl_lcnt = (u32)i2c_config.SMLX;
+ qcdev->i2c_config.sda_tx_hold = (u32)i2c_config.SMTD;
+ qcdev->i2c_config.sda_rx_hold = (u32)i2c_config.SMRD;
} else if (i2c_param.connection_speed > QUICKI2C_SUBIP_STANDARD_MODE_MAX_SPEED &&
i2c_param.connection_speed <= QUICKI2C_SUBIP_FAST_MODE_MAX_SPEED) {
- qcdev->i2c_speed_mode = THC_I2C_FAST_AND_PLUS;
- qcdev->i2c_clock_hcnt = i2c_config.FMHX;
- qcdev->i2c_clock_lcnt = i2c_config.FMLX;
+ qcdev->i2c_config.speed = THC_I2C_FAST_AND_PLUS;
+ qcdev->i2c_config.scl_hcnt = (u32)i2c_config.FMHX;
+ qcdev->i2c_config.scl_lcnt = (u32)i2c_config.FMLX;
+ qcdev->i2c_config.sda_tx_hold = (u32)i2c_config.FMTD;
+ qcdev->i2c_config.sda_rx_hold = (u32)i2c_config.FMRD;
} else if (i2c_param.connection_speed > QUICKI2C_SUBIP_FAST_MODE_MAX_SPEED &&
i2c_param.connection_speed <= QUICKI2C_SUBIP_FASTPLUS_MODE_MAX_SPEED) {
- qcdev->i2c_speed_mode = THC_I2C_FAST_AND_PLUS;
- qcdev->i2c_clock_hcnt = i2c_config.FPHX;
- qcdev->i2c_clock_lcnt = i2c_config.FPLX;
+ qcdev->i2c_config.speed = THC_I2C_FAST_AND_PLUS;
+ qcdev->i2c_config.scl_hcnt = (u32)i2c_config.FPHX;
+ qcdev->i2c_config.scl_lcnt = (u32)i2c_config.FPLX;
+ qcdev->i2c_config.sda_tx_hold = (u32)i2c_config.FPTD;
+ qcdev->i2c_config.sda_rx_hold = (u32)i2c_config.FPRD;
} else if (i2c_param.connection_speed > QUICKI2C_SUBIP_FASTPLUS_MODE_MAX_SPEED &&
i2c_param.connection_speed <= QUICKI2C_SUBIP_HIGH_SPEED_MODE_MAX_SPEED) {
- qcdev->i2c_speed_mode = THC_I2C_HIGH_SPEED;
- qcdev->i2c_clock_hcnt = i2c_config.HMHX;
- qcdev->i2c_clock_lcnt = i2c_config.HMLX;
+ qcdev->i2c_config.speed = THC_I2C_HIGH_SPEED;
+ qcdev->i2c_config.scl_hcnt = (u32)i2c_config.HMHX;
+ qcdev->i2c_config.scl_lcnt = (u32)i2c_config.HMLX;
+ qcdev->i2c_config.sda_tx_hold = (u32)i2c_config.HMTD;
+ qcdev->i2c_config.sda_rx_hold = (u32)i2c_config.HMRD;
} else {
return -EOPNOTSUPP;
}
@@ -245,28 +255,33 @@ static irqreturn_t quicki2c_irq_quick_handler(int irq, void *dev_id)
}
/**
- * try_recover - Try to recovery THC and Device
- * @qcdev: Pointer to quicki2c_device structure
+ * try_recover - Recover callback to recover THC
+ * @work: pointer to work_struct
*
* This function is an error handler, called when fatal error happens.
- * It try to reset touch device and re-configure THC to recovery
- * communication between touch device and THC.
- *
- * Return: 0 if successful or error code on failure
+ * It try to reset Touch Device and re-configure THC to recover
+ * transferring between Device and THC.
*/
-static int try_recover(struct quicki2c_device *qcdev)
+static void try_recover(struct work_struct *work)
{
- int ret;
+ struct quicki2c_device *qcdev = container_of(work, struct quicki2c_device, recover_work);
- thc_dma_unconfigure(qcdev->thc_hw);
+ if (READ_ONCE(qcdev->recovery_disabled))
+ return;
- ret = thc_dma_configure(qcdev->thc_hw);
- if (ret) {
- dev_err(qcdev->dev, "Reconfig DMA failed\n");
- return ret;
+ if (pm_runtime_resume_and_get(qcdev->dev))
+ return;
+
+ thc_interrupt_enable(qcdev->thc_hw, false);
+
+ if (thc_rxdma_reset(qcdev->thc_hw)) {
+ qcdev->state = QUICKI2C_DISABLED;
+ dev_err(qcdev->dev, "RxDMA reset failed during recover, disable QuickI2C\n");
+ } else {
+ thc_interrupt_enable(qcdev->thc_hw, true);
}
- return 0;
+ pm_runtime_put_autosuspend(qcdev->dev);
}
static int handle_input_report(struct quicki2c_device *qcdev)
@@ -343,11 +358,10 @@ static irqreturn_t quicki2c_irq_thread_handler(int irq, void *dev_id)
}
exit:
- thc_interrupt_enable(qcdev->thc_hw, true);
-
if (err_recover)
- if (try_recover(qcdev))
- qcdev->state = QUICKI2C_DISABLED;
+ schedule_work(&qcdev->recover_work);
+ else
+ thc_interrupt_enable(qcdev->thc_hw, true);
pm_runtime_put_autosuspend(qcdev->dev);
@@ -386,6 +400,8 @@ static struct quicki2c_device *quicki2c_dev_init(struct pci_dev *pdev, void __io
qcdev->ddata = ddata;
init_waitqueue_head(&qcdev->reset_ack_wq);
+ WRITE_ONCE(qcdev->recovery_disabled, false);
+ INIT_WORK(&qcdev->recover_work, try_recover);
/* THC hardware init */
qcdev->thc_hw = thc_dev_init(qcdev->dev, qcdev->mem_addr);
@@ -411,10 +427,7 @@ static struct quicki2c_device *quicki2c_dev_init(struct pci_dev *pdev, void __io
return ERR_PTR(ret);
}
- ret = thc_i2c_subip_init(qcdev->thc_hw, qcdev->i2c_slave_addr,
- qcdev->i2c_speed_mode,
- qcdev->i2c_clock_hcnt,
- qcdev->i2c_clock_lcnt);
+ ret = thc_i2c_subip_init(qcdev->thc_hw, &qcdev->i2c_config);
if (ret)
return ERR_PTR(ret);
@@ -439,6 +452,9 @@ static struct quicki2c_device *quicki2c_dev_init(struct pci_dev *pdev, void __io
*/
static void quicki2c_dev_deinit(struct quicki2c_device *qcdev)
{
+ WRITE_ONCE(qcdev->recovery_disabled, true);
+ cancel_work_sync(&qcdev->recover_work);
+
thc_interrupt_quiesce(qcdev->thc_hw, true);
thc_interrupt_enable(qcdev->thc_hw, false);
thc_ltr_unconfig(qcdev->thc_hw);
@@ -682,11 +698,8 @@ static int quicki2c_probe(struct pci_dev *pdev, const struct pci_device_id *id)
quicki2c_irq_thread_handler,
IRQF_ONESHOT, KBUILD_MODNAME,
qcdev);
- if (ret) {
- dev_err_once(&pdev->dev,
- "Failed to request threaded IRQ, irq = %d.\n", pdev->irq);
+ if (ret)
goto dev_deinit;
- }
ret = quicki2c_get_device_descriptor(qcdev);
if (ret) {
@@ -772,12 +785,14 @@ static void quicki2c_remove(struct pci_dev *pdev)
return;
quicki2c_hid_remove(qcdev);
+
+ quicki2c_dev_deinit(qcdev);
+
quicki2c_dma_deinit(qcdev);
+ pm_runtime_dont_use_autosuspend(qcdev->dev);
pm_runtime_get_noresume(qcdev->dev);
- quicki2c_dev_deinit(qcdev);
-
pci_clear_master(pdev);
}
@@ -796,10 +811,10 @@ static void quicki2c_shutdown(struct pci_dev *pdev)
if (!qcdev)
return;
+ quicki2c_dev_deinit(qcdev);
+
/* Must stop DMA before reboot to avoid DMA entering into unknown state */
quicki2c_dma_deinit(qcdev);
-
- quicki2c_dev_deinit(qcdev);
}
static int quicki2c_suspend(struct device *device)
@@ -826,6 +841,9 @@ static int quicki2c_suspend(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qcdev->recovery_disabled, true);
+ cancel_work_sync(&qcdev->recover_work);
+
ret = thc_interrupt_quiesce(qcdev->thc_hw, true);
if (ret)
return ret;
@@ -867,6 +885,8 @@ static int quicki2c_resume(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qcdev->recovery_disabled, false);
+
if (!device_may_wakeup(qcdev->dev))
return quicki2c_set_power(qcdev, HIDI2C_ON);
@@ -883,6 +903,9 @@ static int quicki2c_freeze(struct device *device)
if (!qcdev)
return -ENODEV;
+ WRITE_ONCE(qcdev->recovery_disabled, true);
+ cancel_work_sync(&qcdev->recover_work);
+
ret = thc_interrupt_quiesce(qcdev->thc_hw, true);
if (ret)
return ret;
@@ -914,6 +937,8 @@ static int quicki2c_thaw(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qcdev->recovery_disabled, false);
+
return 0;
}
@@ -938,6 +963,8 @@ static int quicki2c_poweroff(struct device *device)
thc_ltr_unconfig(qcdev->thc_hw);
+ quicki2c_dev_deinit(qcdev);
+
quicki2c_dma_deinit(qcdev);
return 0;
@@ -958,10 +985,7 @@ static int quicki2c_restore(struct device *device)
if (ret)
return ret;
- ret = thc_i2c_subip_init(qcdev->thc_hw, qcdev->i2c_slave_addr,
- qcdev->i2c_speed_mode,
- qcdev->i2c_clock_hcnt,
- qcdev->i2c_clock_lcnt);
+ ret = thc_i2c_subip_init(qcdev->thc_hw, &qcdev->i2c_config);
if (ret)
return ret;
diff --git a/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h b/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
index 61dbdece59a1..6d25a846153e 100644
--- a/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
+++ b/drivers/hid/intel-thc-hid/intel-quicki2c/quicki2c-dev.h
@@ -7,6 +7,8 @@
#include <linux/hid-over-i2c.h>
#include <linux/workqueue.h>
+#include "intel-thc-dev.h"
+
#define PCI_DEVICE_ID_INTEL_THC_LNL_DEVICE_ID_I2C_PORT1 0xA848
#define PCI_DEVICE_ID_INTEL_THC_LNL_DEVICE_ID_I2C_PORT2 0xA84A
#define PCI_DEVICE_ID_INTEL_THC_PTL_H_DEVICE_ID_I2C_PORT1 0xE348
@@ -159,7 +161,6 @@ struct quicki2c_ddata {
struct device;
struct pci_dev;
-struct thc_device;
struct hid_device;
struct acpi_device;
@@ -174,13 +175,10 @@ struct acpi_device;
* @state: THC I2C device state
* @mem_addr: MMIO memory address
* @dev_desc: Device descriptor for HIDI2C protocol
- * @i2c_slave_addr: HIDI2C device slave address
+ * @i2c_config: I2C bus configuration
* @hid_desc_addr: Register address for retrieve HID device descriptor
* @active_ltr_val: THC active LTR value
* @low_power_ltr_val: THC low power LTR value
- * @i2c_speed_mode: 0 - standard mode, 1 - fast mode, 2 - fast mode plus
- * @i2c_clock_hcnt: I2C CLK high period time (unit in cycle count)
- * @i2c_clock_lcnt: I2C CLK low period time (unit in cycle count)
* @report_descriptor: Store a copy of device report descriptor
* @input_buf: Store a copy of latest input report data
* @report_buf: Store a copy of latest input/output report packet from set/get feature
@@ -191,6 +189,8 @@ struct acpi_device;
* @i2c_max_frame_size: Max RX frame size (unit in Bytes)
* @i2c_int_delay_enable: Indicate interrupt delay feature enabled or not
* @i2c_int_delay: Interrupt detection delay value (unit in 10 us)
+ * @recover_work: Work structure for recovery
+ * @recovery_disabled: Whether recovery work is blocked during teardown
*/
struct quicki2c_device {
struct device *dev;
@@ -204,16 +204,12 @@ struct quicki2c_device {
void __iomem *mem_addr;
struct hidi2c_dev_descriptor dev_desc;
- u8 i2c_slave_addr;
+ struct thc_i2c_config i2c_config;
u16 hid_desc_addr;
u32 active_ltr_val;
u32 low_power_ltr_val;
- u32 i2c_speed_mode;
- u32 i2c_clock_hcnt;
- u32 i2c_clock_lcnt;
-
u8 *report_descriptor;
u8 *input_buf;
u8 *report_buf;
@@ -226,6 +222,9 @@ struct quicki2c_device {
u32 i2c_max_frame_size;
u32 i2c_int_delay_enable;
u32 i2c_int_delay;
+
+ struct work_struct recover_work;
+ bool recovery_disabled;
};
#endif /* _QUICKI2C_DEV_H_ */
diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
index 4ae2e1718b30..89226f5ce45e 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
@@ -252,34 +252,33 @@ static irqreturn_t quickspi_irq_quick_handler(int irq, void *dev_id)
}
/**
- * try_recover - Try to recovery THC and Device
- * @qsdev: pointer to quickspi device
+ * try_recover - Recover callback to recover THC
+ * @work: pointer to work_struct
*
- * This function is a error handler, called when fatal error happens.
- * It try to reset Touch Device and re-configure THC to recovery
+ * This function is an error handler, called when fatal error happens.
+ * It try to reset Touch Device and re-configure THC to recover
* transferring between Device and THC.
- *
- * Return: 0 if successful or error code on failed.
*/
-static int try_recover(struct quickspi_device *qsdev)
+static void try_recover(struct work_struct *work)
{
- int ret;
+ struct quickspi_device *qsdev = container_of(work, struct quickspi_device, recover_work);
- ret = reset_tic(qsdev);
- if (ret) {
- dev_err(qsdev->dev, "Reset touch device failed, ret = %d\n", ret);
- return ret;
- }
+ if (READ_ONCE(qsdev->recovery_disabled))
+ return;
- thc_dma_unconfigure(qsdev->thc_hw);
+ if (pm_runtime_resume_and_get(qsdev->dev))
+ return;
- ret = thc_dma_configure(qsdev->thc_hw);
- if (ret) {
- dev_err(qsdev->dev, "Re-configure THC DMA failed, ret = %d\n", ret);
- return ret;
+ thc_interrupt_enable(qsdev->thc_hw, false);
+
+ if (thc_rxdma_reset(qsdev->thc_hw)) {
+ qsdev->state = QUICKSPI_DISABLED;
+ dev_err(qsdev->dev, "RxDMA reset failed during recover, disable QuickSPI\n");
+ } else {
+ thc_interrupt_enable(qsdev->thc_hw, true);
}
- return 0;
+ pm_runtime_put_autosuspend(qsdev->dev);
}
/**
@@ -337,11 +336,10 @@ static irqreturn_t quickspi_irq_thread_handler(int irq, void *dev_id)
}
end:
- thc_interrupt_enable(qsdev->thc_hw, true);
-
if (err_recover)
- if (try_recover(qsdev))
- qsdev->state = QUICKSPI_DISABLED;
+ schedule_work(&qsdev->recover_work);
+ else
+ thc_interrupt_enable(qsdev->thc_hw, true);
pm_runtime_put_autosuspend(qsdev->dev);
@@ -385,6 +383,8 @@ static struct quickspi_device *quickspi_dev_init(struct pci_dev *pdev, void __io
init_waitqueue_head(&qsdev->report_desc_got_wq);
init_waitqueue_head(&qsdev->get_report_cmpl_wq);
init_waitqueue_head(&qsdev->set_report_cmpl_wq);
+ WRITE_ONCE(qsdev->recovery_disabled, false);
+ INIT_WORK(&qsdev->recover_work, try_recover);
/* thc hw init */
qsdev->thc_hw = thc_dev_init(qsdev->dev, qsdev->mem_addr);
@@ -461,6 +461,10 @@ static struct quickspi_device *quickspi_dev_init(struct pci_dev *pdev, void __io
*/
static void quickspi_dev_deinit(struct quickspi_device *qsdev)
{
+ WRITE_ONCE(qsdev->recovery_disabled, true);
+ cancel_work_sync(&qsdev->recover_work);
+
+ thc_interrupt_quiesce(qsdev->thc_hw, true);
thc_interrupt_enable(qsdev->thc_hw, false);
thc_ltr_unconfig(qsdev->thc_hw);
thc_wot_unconfig(qsdev->thc_hw);
@@ -555,7 +559,14 @@ static int quickspi_alloc_report_buf(struct quickspi_device *qsdev)
max_report_len = max(le16_to_cpu(qsdev->dev_desc.max_output_len),
le16_to_cpu(qsdev->dev_desc.max_input_len));
- qsdev->report_buf = devm_kzalloc(qsdev->dev, max_report_len, GFP_KERNEL);
+ /*
+ * write_cmd_to_txdma() writes the output report header ahead of the
+ * content in this buffer, so it has to hold both.
+ */
+ qsdev->report_buf_size = HIDSPI_OUTPUT_REPORT_SIZE(max_report_len);
+
+ qsdev->report_buf = devm_kzalloc(qsdev->dev, qsdev->report_buf_size,
+ GFP_KERNEL);
if (!qsdev->report_buf)
return -ENOMEM;
@@ -636,11 +647,8 @@ static int quickspi_probe(struct pci_dev *pdev,
quickspi_irq_thread_handler,
IRQF_ONESHOT, KBUILD_MODNAME,
qsdev);
- if (ret) {
- dev_err(&pdev->dev,
- "Failed to request threaded IRQ, irq = %d.\n", pdev->irq);
+ if (ret)
goto dev_deinit;
- }
ret = reset_tic(qsdev);
if (ret) {
@@ -711,12 +719,14 @@ static void quickspi_remove(struct pci_dev *pdev)
return;
quickspi_hid_remove(qsdev);
+
+ quickspi_dev_deinit(qsdev);
+
quickspi_dma_deinit(qsdev);
+ pm_runtime_dont_use_autosuspend(qsdev->dev);
pm_runtime_get_noresume(qsdev->dev);
- quickspi_dev_deinit(qsdev);
-
pci_clear_master(pdev);
}
@@ -737,10 +747,10 @@ static void quickspi_shutdown(struct pci_dev *pdev)
if (!qsdev)
return;
+ quickspi_dev_deinit(qsdev);
+
/* Must stop DMA before reboot to avoid DMA entering into unknown state */
quickspi_dma_deinit(qsdev);
-
- quickspi_dev_deinit(qsdev);
}
static int quickspi_suspend(struct device *device)
@@ -759,6 +769,9 @@ static int quickspi_suspend(struct device *device)
return ret;
}
+ WRITE_ONCE(qsdev->recovery_disabled, true);
+ cancel_work_sync(&qsdev->recover_work);
+
ret = thc_interrupt_quiesce(qsdev->thc_hw, true);
if (ret)
return ret;
@@ -784,6 +797,8 @@ static int quickspi_resume(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qsdev->recovery_disabled, false);
+
/*
* A wake-enabled device keeps its power and state across suspend, so
* only restore the THC context. Resetting it here would discard a
@@ -864,6 +879,9 @@ static int quickspi_freeze(struct device *device)
if (!qsdev)
return -ENODEV;
+ WRITE_ONCE(qsdev->recovery_disabled, true);
+ cancel_work_sync(&qsdev->recover_work);
+
ret = thc_interrupt_quiesce(qsdev->thc_hw, true);
if (ret)
return ret;
@@ -895,6 +913,8 @@ static int quickspi_thaw(struct device *device)
if (ret)
return ret;
+ WRITE_ONCE(qsdev->recovery_disabled, false);
+
return 0;
}
@@ -919,6 +939,8 @@ static int quickspi_poweroff(struct device *device)
thc_ltr_unconfig(qsdev->thc_hw);
+ quickspi_dev_deinit(qsdev);
+
quickspi_dma_deinit(qsdev);
return 0;
diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
index bf5e18f5a5f4..dc67a39ad546 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
@@ -8,6 +8,7 @@
#include <linux/hid-over-spi.h>
#include <linux/sizes.h>
#include <linux/wait.h>
+#include <linux/workqueue.h>
#include "quickspi-protocol.h"
@@ -126,6 +127,8 @@ struct acpi_device;
* @get_feature_cmpl: indicate get feature received or not
* @set_feature_cmpl_wq: workqueue for waiting set feature to device
* @set_feature_cmpl: indicate set feature send complete or not
+ * @recover_work: Work structure for recovery
+ * @recovery_disabled: Whether recovery work is blocked during teardown
*/
struct quickspi_device {
struct device *dev;
@@ -157,6 +160,7 @@ struct quickspi_device {
u8 *report_descriptor;
u8 *input_buf;
u8 *report_buf;
+ u32 report_buf_size;
u32 report_len;
wait_queue_head_t reset_ack_wq;
@@ -173,6 +177,9 @@ struct quickspi_device {
wait_queue_head_t set_report_cmpl_wq;
bool set_report_cmpl;
+
+ struct work_struct recover_work;
+ bool recovery_disabled;
};
#endif /* _QUICKSPI_DEV_H_ */
diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-hid.c b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-hid.c
index 91d5807b4a83..a60a0a7f16aa 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-hid.c
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-hid.c
@@ -61,7 +61,7 @@ static int quickspi_hid_raw_request(struct hid_device *hid,
switch (reqtype) {
case HID_REQ_GET_REPORT:
- ret = quickspi_get_report(qsdev, rtype, reportnum, buf);
+ ret = quickspi_get_report(qsdev, rtype, reportnum, buf, len);
break;
case HID_REQ_SET_REPORT:
ret = quickspi_set_report(qsdev, rtype, reportnum, buf, len);
diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.c b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.c
index cb19057f1191..847c5ec55569 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.c
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.c
@@ -30,6 +30,9 @@ static int write_cmd_to_txdma(struct quickspi_device *qsdev,
write_buf = (struct output_report *)qsdev->report_buf;
+ if (HIDSPI_OUTPUT_REPORT_SIZE(report_buf_len) > qsdev->report_buf_size)
+ return -EINVAL;
+
write_buf->output_hdr.report_type = report_type;
write_buf->output_hdr.content_len = cpu_to_le16(report_buf_len);
write_buf->output_hdr.content_id = report_id;
@@ -342,10 +345,12 @@ int reset_tic(struct quickspi_device *qsdev)
}
int quickspi_get_report(struct quickspi_device *qsdev,
- u8 report_type, unsigned int report_id, void *buf)
+ u8 report_type, unsigned int report_id, void *buf,
+ u32 buf_len)
{
int rep_type;
int ret;
+ u32 report_len;
if (report_type == HID_INPUT_REPORT) {
rep_type = GET_INPUT_REPORT;
@@ -372,9 +377,17 @@ int quickspi_get_report(struct quickspi_device *qsdev,
}
qsdev->get_report_cmpl = false;
- memcpy(buf, qsdev->report_buf, qsdev->report_len);
+ /* quickspi_handle_input_data() updates this from IRQ context. */
+ report_len = READ_ONCE(qsdev->report_len);
+ if (report_len > buf_len) {
+ dev_err_once(qsdev->dev, "Get report response too big, %u vs %u\n",
+ report_len, buf_len);
+ return -EINVAL;
+ }
+
+ memcpy(buf, qsdev->report_buf, report_len);
- return qsdev->report_len;
+ return report_len;
}
int quickspi_set_report(struct quickspi_device *qsdev,
diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.h b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.h
index 775e29c1ed13..8a2338bee808 100644
--- a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.h
+++ b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.h
@@ -12,7 +12,7 @@ struct quickspi_device;
void quickspi_handle_input_data(struct quickspi_device *qsdev, u32 buf_len);
int quickspi_get_report(struct quickspi_device *qsdev, u8 report_type,
- unsigned int report_id, void *buf);
+ unsigned int report_id, void *buf, u32 buf_len);
int quickspi_set_report(struct quickspi_device *qsdev, u8 report_type,
unsigned int report_id, void *buf, u32 buf_len);
int quickspi_get_report_descriptor(struct quickspi_device *qsdev);
diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c
index 9a8449428170..7b4a58e1416d 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.c
@@ -1422,14 +1422,25 @@ end:
#define I2C_SUBIP_DMA_TDLR_DEFAULT 7
#define I2C_SUBIP_DMA_RDLR_DEFAULT 7
-static int thc_i2c_subip_set_speed(struct thc_device *dev, const u32 speed,
- const u32 hcnt, const u32 lcnt)
+static int thc_i2c_subip_bus_config(struct thc_device *dev, const struct thc_i2c_config *i2c_config)
{
u32 hcnt_offset, lcnt_offset;
- u32 val;
+ u32 read_size = sizeof(u32);
+ u32 val = 0;
int ret;
- switch (speed) {
+ ret = thc_i2c_subip_pio_read(dev, THC_I2C_IC_TAR_OFFSET, &read_size, &val);
+ if (ret < 0)
+ return ret;
+
+ val &= ~(THC_I2C_IC_TAR_IC_TAR | THC_I2C_IC_TAR_IC_10BITADDR_MASTER);
+ val |= FIELD_PREP(THC_I2C_IC_TAR_IC_10BITADDR_MASTER, i2c_config->addr_mode);
+ val |= FIELD_PREP(THC_I2C_IC_TAR_IC_TAR, i2c_config->target_addr);
+ ret = thc_i2c_subip_pio_write(dev, THC_I2C_IC_TAR_OFFSET, sizeof(u32), &val);
+ if (ret < 0)
+ return ret;
+
+ switch (i2c_config->speed) {
case THC_I2C_STANDARD:
hcnt_offset = THC_I2C_IC_SS_SCL_HCNT_OFFSET;
lcnt_offset = THC_I2C_IC_SS_SCL_LCNT_OFFSET;
@@ -1446,25 +1457,43 @@ static int thc_i2c_subip_set_speed(struct thc_device *dev, const u32 speed,
break;
default:
- dev_err_once(dev->dev, "Unsupported i2c speed %d\n", speed);
+ dev_err_once(dev->dev, "Unsupported i2c speed %d\n", i2c_config->speed);
ret = -EINVAL;
return ret;
}
- ret = thc_i2c_subip_pio_write(dev, hcnt_offset, sizeof(u32), &hcnt);
+ ret = thc_i2c_subip_pio_write(dev, hcnt_offset, sizeof(u32), &i2c_config->scl_hcnt);
if (ret < 0)
return ret;
- ret = thc_i2c_subip_pio_write(dev, lcnt_offset, sizeof(u32), &lcnt);
+ ret = thc_i2c_subip_pio_write(dev, lcnt_offset, sizeof(u32), &i2c_config->scl_lcnt);
if (ret < 0)
return ret;
val = I2C_SUBIP_CON_DEFAULT & ~THC_I2C_IC_CON_SPEED;
- val |= FIELD_PREP(THC_I2C_IC_CON_SPEED, speed);
+ val |= FIELD_PREP(THC_I2C_IC_CON_SPEED, i2c_config->speed);
ret = thc_i2c_subip_pio_write(dev, THC_I2C_IC_CON_OFFSET, sizeof(u32), &val);
if (ret < 0)
return ret;
+ ret = thc_i2c_subip_pio_read(dev, THC_I2C_IC_SDA_HOLD_OFFSET, &read_size, &val);
+ if (ret < 0)
+ return ret;
+
+ if (i2c_config->sda_tx_hold) {
+ val &= ~THC_I2C_IC_SDA_HOLD_IC_SDA_TX_HOLD;
+ val |= FIELD_PREP(THC_I2C_IC_SDA_HOLD_IC_SDA_TX_HOLD, i2c_config->sda_tx_hold);
+ }
+
+ if (i2c_config->sda_rx_hold) {
+ val &= ~THC_I2C_IC_SDA_HOLD_IC_SDA_RX_HOLD;
+ val |= FIELD_PREP(THC_I2C_IC_SDA_HOLD_IC_SDA_RX_HOLD, i2c_config->sda_rx_hold);
+ }
+
+ ret = thc_i2c_subip_pio_write(dev, THC_I2C_IC_SDA_HOLD_OFFSET, sizeof(u32), &val);
+ if (ret < 0)
+ return ret;
+
return 0;
}
@@ -1474,6 +1503,7 @@ static u32 i2c_subip_regs[] = {
THC_I2C_IC_INTR_MASK_OFFSET,
THC_I2C_IC_RX_TL_OFFSET,
THC_I2C_IC_TX_TL_OFFSET,
+ THC_I2C_IC_SDA_HOLD_OFFSET,
THC_I2C_IC_DMA_CR_OFFSET,
THC_I2C_IC_DMA_TDLR_OFFSET,
THC_I2C_IC_DMA_RDLR_OFFSET,
@@ -1490,20 +1520,19 @@ static u32 i2c_subip_regs[] = {
* thc_i2c_subip_init - Initialize and configure THC I2C subsystem
*
* @dev: The pointer of THC private device context
- * @target_address: Slave address of touch device (TIC)
- * @speed: I2C bus frequency speed mode
- * @hcnt: I2C clock SCL high count
- * @lcnt: I2C clock SCL low count
+ * @i2c_config: The pointer of THC I2C bus configure structure
*
* Return: 0 on success, other error codes on failed.
*/
-int thc_i2c_subip_init(struct thc_device *dev, const u32 target_address,
- const u32 speed, const u32 hcnt, const u32 lcnt)
+int thc_i2c_subip_init(struct thc_device *dev, const struct thc_i2c_config *i2c_config)
{
u32 read_size = sizeof(u32);
u32 val;
int ret;
+ if (!dev || !i2c_config)
+ return -EINVAL;
+
ret = thc_i2c_subip_pio_read(dev, THC_I2C_IC_ENABLE_OFFSET, &read_size, &val);
if (ret < 0)
return ret;
@@ -1513,17 +1542,7 @@ int thc_i2c_subip_init(struct thc_device *dev, const u32 target_address,
if (ret < 0)
return ret;
- ret = thc_i2c_subip_pio_read(dev, THC_I2C_IC_TAR_OFFSET, &read_size, &val);
- if (ret < 0)
- return ret;
-
- val &= ~THC_I2C_IC_TAR_IC_TAR;
- val |= FIELD_PREP(THC_I2C_IC_TAR_IC_TAR, target_address);
- ret = thc_i2c_subip_pio_write(dev, THC_I2C_IC_TAR_OFFSET, sizeof(u32), &val);
- if (ret < 0)
- return ret;
-
- ret = thc_i2c_subip_set_speed(dev, speed, hcnt, lcnt);
+ ret = thc_i2c_subip_bus_config(dev, i2c_config);
if (ret < 0)
return ret;
diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.h b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.h
index 0db435335e24..be8a9605d02f 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.h
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dev.h
@@ -50,6 +50,26 @@ enum thc_int_type {
};
/**
+ * struct thc_i2c_config - THC I2C bus configuration
+ * @target_addr: Slave address of touch device (TIC)
+ * @addr_mode: Slave address mode of touch device (TIC), 7bit or 10bit
+ * @speed: I2C bus frequency speed mode
+ * @scl_hcnt: I2C clock SCL high count
+ * @scl_lcnt: I2C clock SCL low count
+ * @sda_tx_hold: I2C Data SDA transmit hold period
+ * @sda_rx_hold: I2C Data SDA receive hold period
+ */
+struct thc_i2c_config {
+ u16 target_addr;
+ u8 addr_mode;
+ u32 speed;
+ u32 scl_hcnt;
+ u32 scl_lcnt;
+ u32 sda_tx_hold;
+ u32 sda_rx_hold;
+};
+
+/**
* struct thc_device - THC private device struct
* @thc_regmap: MMIO regmap structure for accessing THC registers
* @mmio_addr: MMIO registers address
@@ -121,8 +141,7 @@ int thc_spi_write_config(struct thc_device *dev, u32 spi_freq_val,
u32 io_mode, u32 opcode, u32 spi_wr_mps, u32 perf_limit);
void thc_spi_input_output_address_config(struct thc_device *dev, u32 input_hdr_addr,
u32 input_bdy_addr, u32 output_addr);
-int thc_i2c_subip_init(struct thc_device *dev, const u32 target_address,
- const u32 speed, const u32 hcnt, const u32 lcnt);
+int thc_i2c_subip_init(struct thc_device *dev, const struct thc_i2c_config *i2c_config);
int thc_i2c_subip_regs_save(struct thc_device *dev);
int thc_i2c_subip_regs_restore(struct thc_device *dev);
int thc_i2c_set_rx_max_size(struct thc_device *dev, u32 max_rx_size);
diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
index 6ee675e0a738..7ceb8aeeccd3 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.c
@@ -561,6 +561,57 @@ static int thc_wait_for_dma_pause(struct thc_device *dev, enum thc_dma_channel c
return 0;
}
+/**
+ * thc_rxdma_reset - Reset all read DMA engines
+ *
+ * @dev: The pointer of THC private device context
+ *
+ * This is a helper function to reset RxDMA configure. It's typically used
+ * for RxDMA recovery when fatal error happens.
+ *
+ * Return: 0 if successful or error code on failure.
+ */
+int thc_rxdma_reset(struct thc_device *dev)
+{
+ int ret;
+
+ if (mutex_lock_interruptible(&dev->thc_bus_lock))
+ return -EINTR;
+
+ ret = thc_interrupt_quiesce(dev, true);
+ if (ret) {
+ dev_err(dev->dev, "Quiesce interrupt failed during RxDMA reset\n");
+ goto end;
+ }
+
+ ret = thc_wait_for_dma_pause(dev, THC_RXDMA1);
+ if (ret) {
+ dev_err(dev->dev, "Wait for RxDMA1 pause failed during RxDMA reset\n");
+ goto end;
+ }
+
+ ret = thc_wait_for_dma_pause(dev, THC_RXDMA2);
+ if (ret) {
+ dev_err(dev->dev, "Wait for RxDMA2 pause failed during RxDMA reset\n");
+ goto end;
+ }
+
+ thc_dma_unconfigure(dev);
+
+ ret = thc_dma_configure(dev);
+ if (ret) {
+ dev_err(dev->dev, "Re-config DMA failed during RxDMA reset\n");
+ goto end;
+ }
+
+ thc_interrupt_quiesce(dev, false);
+
+end:
+ mutex_unlock(&dev->thc_bus_lock);
+ return ret;
+}
+EXPORT_SYMBOL_NS_GPL(thc_rxdma_reset, "INTEL_THC");
+
static int read_dma_buffer(struct thc_device *dev,
struct thc_dma_configuration *read_config,
u8 prd_table_index, void *read_buff)
diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h
index 541d33995baf..715423453a9d 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-dma.h
@@ -145,6 +145,7 @@ int thc_dma_allocate(struct thc_device *dev);
int thc_dma_configure(struct thc_device *dev);
void thc_dma_unconfigure(struct thc_device *dev);
void thc_dma_release(struct thc_device *dev);
+int thc_rxdma_reset(struct thc_device *dev);
int thc_rxdma_read(struct thc_device *dev, enum thc_dma_channel dma_channel,
void *read_buff, size_t *read_len, int *read_finished);
int thc_swdma_read(struct thc_device *dev, void *write_buff, size_t write_len,
diff --git a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-hw.h b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-hw.h
index c6d026686b7a..a21222543ce4 100644
--- a/drivers/hid/intel-thc-hid/intel-thc/intel-thc-hw.h
+++ b/drivers/hid/intel-thc-hid/intel-thc/intel-thc-hw.h
@@ -887,4 +887,7 @@ enum THC_I2C_SPEED_MODE {
#define THC_I2C_IC_DMA_CR_RDMAE BIT(0)
#define THC_I2C_IC_DMA_CR_TDMAE BIT(1)
+#define THC_I2C_IC_SDA_HOLD_IC_SDA_TX_HOLD GENMASK(15, 0)
+#define THC_I2C_IC_SDA_HOLD_IC_SDA_RX_HOLD GENMASK(23, 16)
+
#endif /* _INTEL_THC_HW_H_ */
diff --git a/drivers/hid/usbhid/hid-pidff.c b/drivers/hid/usbhid/hid-pidff.c
index 5f4395f7c645..22951b7ecd17 100644
--- a/drivers/hid/usbhid/hid-pidff.c
+++ b/drivers/hid/usbhid/hid-pidff.c
@@ -1539,13 +1539,20 @@ static int pidff_check_autocenter(struct pidff_device *pidff,
int hid_pidff_init_with_quirks(struct hid_device *hid, u32 initial_quirks)
{
struct pidff_device *pidff;
- struct hid_input *hidinput =
- list_entry(hid->inputs.next, struct hid_input, list);
- struct input_dev *dev = hidinput->input;
+ struct hid_input *hidinput;
+ struct input_dev *dev;
struct ff_device *ff;
int max_effects;
int error;
+ if (list_empty(&hid->inputs)) {
+ hid_err(hid, "no inputs found\n");
+ return -ENODEV;
+ }
+
+ hidinput = list_first_entry(&hid->inputs, struct hid_input, list);
+ dev = hidinput->input;
+
hid_dbg(hid, "starting pid init\n");
if (list_empty(&hid->report_enum[HID_OUTPUT_REPORT].report_list)) {
diff --git a/drivers/hid/wacom_wac.c b/drivers/hid/wacom_wac.c
index da1f0ea85625..a29bf051ada7 100644
--- a/drivers/hid/wacom_wac.c
+++ b/drivers/hid/wacom_wac.c
@@ -1192,8 +1192,11 @@ static int int_dist(int x1, int y1, int x2, int y2)
static void wacom_intuos_bt_process_data(struct wacom_wac *wacom,
unsigned char *data)
{
- memcpy(wacom->data, data, 10);
+ u8 *saved_data = wacom->data;
+
+ wacom->data = data;
wacom_intuos_irq(wacom);
+ wacom->data = saved_data;
input_sync(wacom->pen_input);
if (wacom->pad_input)
@@ -1202,7 +1205,7 @@ static void wacom_intuos_bt_process_data(struct wacom_wac *wacom,
static int wacom_intuos_bt_irq(struct wacom_wac *wacom, size_t len)
{
- u8 *data = kmemdup(wacom->data, len, GFP_KERNEL);
+ u8 *data = wacom->data;
int i = 1;
unsigned power_raw, battery_capacity, bat_charging, ps_connected;
@@ -1242,7 +1245,6 @@ static int wacom_intuos_bt_irq(struct wacom_wac *wacom, size_t len)
break;
}
- kfree(data);
return 0;
}
diff --git a/drivers/input/misc/Kconfig b/drivers/input/misc/Kconfig
index abee43158d84..169768e3d5bb 100644
--- a/drivers/input/misc/Kconfig
+++ b/drivers/input/misc/Kconfig
@@ -914,6 +914,21 @@ config INPUT_SOC_BUTTON_ARRAY
To compile this driver as a module, choose M here: the
module will be called soc_button_array.
+config INPUT_AMD_SFH_TABLETMODE
+ tristate "AMD SFH tablet-mode switch"
+ depends on AMD_SFH_HID
+ select AUXILIARY_BUS
+ help
+ Expose SW_TABLET_MODE for AMD convertible laptops whose
+ operation-mode sensor is provided by the AMD Sensor Fusion Hub.
+
+ Userspace components such as systemd-logind and modern desktop
+ environments react to SW_TABLET_MODE when the device is folded
+ into tablet posture.
+
+ To compile this driver as a module, choose M here: the module
+ will be called amd_sfh_tabletmode.
+
config INPUT_DRV260X_HAPTICS
tristate "TI DRV260X haptics support"
depends on INPUT && I2C
diff --git a/drivers/input/misc/Makefile b/drivers/input/misc/Makefile
index e9f85ca20c33..1339619f0769 100644
--- a/drivers/input/misc/Makefile
+++ b/drivers/input/misc/Makefile
@@ -15,6 +15,7 @@ obj-$(CONFIG_INPUT_AD714X_SPI) += ad714x-spi.o
obj-$(CONFIG_INPUT_ADXL34X) += adxl34x.o
obj-$(CONFIG_INPUT_ADXL34X_I2C) += adxl34x-i2c.o
obj-$(CONFIG_INPUT_ADXL34X_SPI) += adxl34x-spi.o
+obj-$(CONFIG_INPUT_AMD_SFH_TABLETMODE) += amd_sfh_tabletmode.o
obj-$(CONFIG_INPUT_APANEL) += apanel.o
obj-$(CONFIG_INPUT_ARIEL_PWRBUTTON) += ariel-pwrbutton.o
obj-$(CONFIG_INPUT_ARIZONA_HAPTICS) += arizona-haptics.o
diff --git a/drivers/input/misc/amd_sfh_tabletmode.c b/drivers/input/misc/amd_sfh_tabletmode.c
new file mode 100644
index 000000000000..3931a1c6eb74
--- /dev/null
+++ b/drivers/input/misc/amd_sfh_tabletmode.c
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * AMD SFH tablet-mode switch driver
+ *
+ * Copyright (c) 2026, Advanced Micro Devices, Inc.
+ * All Rights Reserved.
+ *
+ * Author: Basavaraj Natikar <Basavaraj.Natikar@amd.com>
+ */
+
+#include <linux/amd-pmf-io.h>
+#include <linux/auxiliary_bus.h>
+#include <linux/input.h>
+#include <linux/module.h>
+#include <linux/pci_ids.h>
+
+#define POLL_INTERVAL_MS 200
+
+static void sfh_tm_poll(struct input_dev *input)
+{
+ struct amd_sfh_info info = {};
+
+ if (amd_get_sfh_info(&info, MT_OP_MODE))
+ return;
+
+ input_report_switch(input, SW_TABLET_MODE,
+ info.op_mode == SFH_MODE_TABLET);
+ input_sync(input);
+}
+
+static int sfh_tm_probe(struct auxiliary_device *auxdev,
+ const struct auxiliary_device_id *id)
+{
+ struct device *dev = &auxdev->dev;
+ struct amd_sfh_info info = {};
+ struct input_dev *input;
+ int error;
+
+ error = amd_get_sfh_info(&info, MT_OP_MODE);
+ if (error)
+ return error == -EINVAL ? -ENODEV : error;
+
+ input = devm_input_allocate_device(dev);
+ if (!input)
+ return -ENOMEM;
+
+ input->name = "AMD SFH tablet mode switch";
+ input->phys = "amd-sfh/tabletmode";
+ input->id.bustype = BUS_HOST;
+ input->id.vendor = PCI_VENDOR_ID_AMD;
+ input_set_capability(input, EV_SW, SW_TABLET_MODE);
+
+ error = input_setup_polling(input, sfh_tm_poll);
+ if (error)
+ return error;
+ input_set_poll_interval(input, POLL_INTERVAL_MS);
+
+ sfh_tm_poll(input);
+
+ error = input_register_device(input);
+ if (error)
+ return error;
+
+ return 0;
+}
+
+static const struct auxiliary_device_id sfh_tm_id_table[] = {
+ { .name = "amd_sfh.tabletmode" },
+ {}
+};
+MODULE_DEVICE_TABLE(auxiliary, sfh_tm_id_table);
+
+static struct auxiliary_driver sfh_tm_driver = {
+ .name = "tabletmode",
+ .id_table = sfh_tm_id_table,
+ .probe = sfh_tm_probe,
+};
+module_auxiliary_driver(sfh_tm_driver);
+
+MODULE_DESCRIPTION("AMD SFH tablet mode switch");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/amd-pmf-io.h b/include/linux/amd-pmf-io.h
index 55198d2875cc..dc59c43bd8f2 100644
--- a/include/linux/amd-pmf-io.h
+++ b/include/linux/amd-pmf-io.h
@@ -19,11 +19,13 @@
* @MT_HPD: Message ID to know the Human presence info from MP2 FW
* @MT_ALS: Message ID to know the Ambient light info from MP2 FW
* @MT_SRA: Message ID to know the SRA data from MP2 FW
+ * @MT_OP_MODE: Message ID to know the operating-mode (tablet/laptop) info
*/
enum sfh_message_type {
MT_HPD,
MT_ALS,
MT_SRA,
+ MT_OP_MODE,
};
/**
@@ -44,12 +46,24 @@ enum sfh_hpd_info {
* @user_present: Populates the user presence information
* @platform_type: Operating modes (clamshell, flat, tent, etc.)
* @laptop_placement: Device states (ontable, onlap, outbag)
+ * @op_mode: Operating-mode field (see enum sfh_dev_mode); used for tablet detection
*/
struct amd_sfh_info {
u32 ambient_light;
u8 user_present;
u32 platform_type;
u32 laptop_placement;
+ u32 op_mode;
+};
+
+/**
+ * enum sfh_dev_mode - SFH operating-mode field (sfh_op_mode.mode, bits 0-2)
+ * @SFH_MODE_LAPTOP: Device is in laptop/clamshell posture
+ * @SFH_MODE_TABLET: Device is in tablet posture
+ */
+enum sfh_dev_mode {
+ SFH_MODE_LAPTOP = 1,
+ SFH_MODE_TABLET = 3,
};
enum laptop_placement {
diff --git a/include/linux/hid.h b/include/linux/hid.h
index b240baa95ab5..8d17b741638c 100644
--- a/include/linux/hid.h
+++ b/include/linux/hid.h
@@ -642,6 +642,7 @@ enum hid_battery_status {
* @max: maximum battery value from HID descriptor
* @report_type: HID report type (input/feature)
* @report_id: HID report ID for this battery
+ * @report_offset: bit offset of the capacity field within its report
* @charge_status: current charging status
* @status: battery reporting status
* @capacity: current battery capacity (0-100)
@@ -657,6 +658,7 @@ struct hid_battery {
__s32 max;
__s32 report_type;
__s32 report_id;
+ __s32 report_offset;
__s32 charge_status;
enum hid_battery_status status;
__s32 capacity;
@@ -1021,7 +1023,7 @@ extern void hid_unregister_driver(struct hid_driver *);
extern void hidinput_hid_event(struct hid_device *, struct hid_field *, struct hid_usage *, __s32);
extern void hidinput_report_event(struct hid_device *hid, struct hid_report *report);
-extern int hidinput_connect(struct hid_device *hid, unsigned int force);
+extern int hidinput_connect(struct hid_device *hid, unsigned int connect_mask);
extern void hidinput_disconnect(struct hid_device *);
void hidinput_reset_resume(struct hid_device *hid);
diff --git a/scripts/coccinelle/hid/ff_race.cocci b/scripts/coccinelle/hid/ff_race.cocci
new file mode 100644
index 000000000000..479f5d1e3184
--- /dev/null
+++ b/scripts/coccinelle/hid/ff_race.cocci
@@ -0,0 +1,34 @@
+/// Detect HID drivers that initialize force-feedback after hid_hw_start()
+/// when HID_CONNECT_HIDINPUT is used. This is a lifecycle violation as
+/// the input device is already registered.
+//
+// Confidence: High
+// Copyright: (C) 2026 Gemini. GPLv2.
+
+virtual report
+
+@r@
+identifier probe_fn;
+expression hdev, flags;
+position p1, p2;
+@@
+
+probe_fn(struct hid_device *hdev, ...) {
+ <...
+ hid_hw_start@p1(hdev, flags)
+ ...
+ \(input_ff_create\|input_ff_create_memless\)@p2(...)
+ ...>
+}
+
+@script:python depends on report@
+p1 << r.p1;
+p2 << r.p2;
+flags << r.flags;
+@@
+
+# Check if flags include HID_CONNECT_HIDINPUT (0x01) or HID_CONNECT_DEFAULT (0x0f)
+# Note: HID_CONNECT_DEFAULT is 0x0f, HID_CONNECT_HIDINPUT is 0x01
+if "HID_CONNECT_HIDINPUT" in flags or "HID_CONNECT_DEFAULT" in flags:
+ msg = "WARNING: force-feedback initialized after hid_hw_start() with HID_CONNECT_HIDINPUT. Input device is already registered at this point. Use .input_configured() instead."
+ coccilib.report.print_report(p2[0], msg)