Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH] HID: winwing: add support for URSA MINOR combat joysticks
From: René Onier @ 2026-04-04 17:26 UTC (permalink / raw)
  To: linux-input; +Cc: jikos, bentiss, ivan.gorinov, rene.onier

Add device IDs for the Winwing URSA MINOR Combat Joystick L (0xbc29)
and R (0xbc2a). These joysticks declare 128 buttons in their HID
report descriptor, causing the generic HID driver to reject buttons
above KEY_MAX (767) with "Invalid code" errors.

The URSA MINOR joysticks use buttons in the 33-64 range for grip
controls including the index trigger, so map_more_buttons is set
to 1 to enable the extended KEY_MACRO mapping.

Tested with both left and right URSA MINOR joysticks on kernel 6.19.

Signed-off-by: René Onier <rene.onier@gmail.com>
---
 hid-winwing.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/hid-winwing.c b/hid-winwing.c
index ab65dc1..f3e6ea0 100644
--- a/hid-winwing.c
+++ b/hid-winwing.c
@@ -250,6 +250,8 @@ static const struct hid_device_id winwing_devices[] = {
 	{ HID_USB_DEVICE(0x4098, 0xbd64), .driver_data = 1 },  /* TGRIP-15EX */
 	{ HID_USB_DEVICE(0x4098, 0xbe68), .driver_data = 0 },  /* TGRIP-16EX */
 	{ HID_USB_DEVICE(0x4098, 0xbe62), .driver_data = 0 },  /* TGRIP-18   */
+	{ HID_USB_DEVICE(0x4098, 0xbc29), .driver_data = 1 },  /* URSA MINOR L */
+	{ HID_USB_DEVICE(0x4098, 0xbc2a), .driver_data = 1 },  /* URSA MINOR R */
 	{}
 };
 
-- 
2.43.0


^ permalink raw reply related

* [dtor-input:for-linus] BUILD SUCCESS 0d9363a764d9d601a05591f9695cea8b429e9be3
From: kernel test robot @ 2026-04-04 18:08 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input

tree/branch: https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git for-linus
branch HEAD: 0d9363a764d9d601a05591f9695cea8b429e9be3  Input: xpad - add support for BETOP BTP-KP50B/C controller's wireless mode

elapsed time: 735m

configs tested: 195
configs skipped: 2

The following configs have been built successfully.
More configs may be tested in the coming days.

tested configs:
alpha                             allnoconfig    gcc-15.2.0
alpha                            allyesconfig    gcc-15.2.0
alpha                               defconfig    gcc-15.2.0
arc                              allmodconfig    clang-16
arc                               allnoconfig    gcc-15.2.0
arc                              allyesconfig    clang-23
arc                                 defconfig    gcc-15.2.0
arc                   randconfig-001-20260404    gcc-15.2.0
arc                   randconfig-002-20260404    gcc-15.2.0
arm                               allnoconfig    gcc-15.2.0
arm                              allyesconfig    clang-16
arm                                 defconfig    gcc-15.2.0
arm                   randconfig-001-20260404    gcc-15.2.0
arm                   randconfig-002-20260404    gcc-15.2.0
arm                   randconfig-003-20260404    gcc-15.2.0
arm                   randconfig-004-20260404    gcc-15.2.0
arm64                            allmodconfig    clang-23
arm64                             allnoconfig    gcc-15.2.0
arm64                               defconfig    gcc-15.2.0
arm64                 randconfig-001-20260404    gcc-15.2.0
arm64                 randconfig-001-20260404    gcc-8.5.0
arm64                 randconfig-002-20260404    gcc-15.2.0
arm64                 randconfig-002-20260404    gcc-8.5.0
arm64                 randconfig-003-20260404    clang-23
arm64                 randconfig-003-20260404    gcc-15.2.0
arm64                 randconfig-004-20260404    gcc-15.2.0
csky                             allmodconfig    gcc-15.2.0
csky                              allnoconfig    gcc-15.2.0
csky                                defconfig    gcc-15.2.0
csky                  randconfig-001-20260404    gcc-15.2.0
csky                  randconfig-001-20260404    gcc-9.5.0
csky                  randconfig-002-20260404    gcc-15.2.0
hexagon                          allmodconfig    gcc-15.2.0
hexagon                           allnoconfig    gcc-15.2.0
hexagon                             defconfig    gcc-15.2.0
hexagon               randconfig-001-20260404    gcc-15.2.0
hexagon               randconfig-002-20260404    gcc-15.2.0
i386                             allmodconfig    clang-20
i386                              allnoconfig    gcc-15.2.0
i386                             allyesconfig    clang-20
i386        buildonly-randconfig-001-20260404    clang-20
i386        buildonly-randconfig-001-20260404    gcc-14
i386        buildonly-randconfig-002-20260404    clang-20
i386        buildonly-randconfig-003-20260404    clang-20
i386        buildonly-randconfig-003-20260404    gcc-14
i386        buildonly-randconfig-004-20260404    clang-20
i386        buildonly-randconfig-005-20260404    clang-20
i386        buildonly-randconfig-006-20260404    clang-20
i386                                defconfig    gcc-15.2.0
i386                  randconfig-001-20260404    clang-20
i386                  randconfig-002-20260404    clang-20
i386                  randconfig-003-20260404    clang-20
i386                  randconfig-004-20260404    clang-20
i386                  randconfig-005-20260404    clang-20
i386                  randconfig-006-20260404    clang-20
i386                  randconfig-007-20260404    clang-20
i386                  randconfig-011-20260404    clang-20
i386                  randconfig-012-20260404    clang-20
i386                  randconfig-013-20260404    clang-20
i386                  randconfig-014-20260404    clang-20
i386                  randconfig-015-20260404    clang-20
i386                  randconfig-016-20260404    clang-20
i386                  randconfig-017-20260404    clang-20
loongarch                        allmodconfig    clang-23
loongarch                         allnoconfig    gcc-15.2.0
loongarch                           defconfig    clang-19
loongarch             randconfig-001-20260404    gcc-15.2.0
loongarch             randconfig-002-20260404    gcc-15.2.0
m68k                             allmodconfig    gcc-15.2.0
m68k                              allnoconfig    gcc-15.2.0
m68k                             allyesconfig    clang-16
m68k                                defconfig    clang-19
microblaze                        allnoconfig    gcc-15.2.0
microblaze                       allyesconfig    gcc-15.2.0
microblaze                          defconfig    clang-19
mips                             allmodconfig    gcc-15.2.0
mips                              allnoconfig    gcc-15.2.0
mips                             allyesconfig    gcc-15.2.0
mips                        omega2p_defconfig    clang-23
nios2                            allmodconfig    clang-23
nios2                             allnoconfig    clang-23
nios2                             allnoconfig    gcc-11.5.0
nios2                               defconfig    clang-19
nios2                 randconfig-001-20260404    gcc-15.2.0
nios2                 randconfig-002-20260404    gcc-15.2.0
openrisc                         allmodconfig    clang-23
openrisc                          allnoconfig    clang-23
openrisc                          allnoconfig    gcc-15.2.0
openrisc                            defconfig    gcc-15.2.0
parisc                           allmodconfig    gcc-15.2.0
parisc                            allnoconfig    clang-23
parisc                            allnoconfig    gcc-15.2.0
parisc                           allyesconfig    clang-19
parisc                              defconfig    gcc-15.2.0
parisc                randconfig-001-20260404    gcc-13.4.0
parisc                randconfig-002-20260404    gcc-15.2.0
parisc64                            defconfig    clang-19
powerpc                          allmodconfig    gcc-15.2.0
powerpc                           allnoconfig    clang-23
powerpc                           allnoconfig    gcc-15.2.0
powerpc                     ksi8560_defconfig    gcc-15.2.0
powerpc                 mpc834x_itx_defconfig    clang-16
powerpc               randconfig-001-20260404    gcc-8.5.0
powerpc               randconfig-002-20260404    clang-23
powerpc64             randconfig-001-20260404    clang-23
powerpc64             randconfig-002-20260404    gcc-10.5.0
riscv                            allmodconfig    clang-23
riscv                             allnoconfig    clang-23
riscv                             allnoconfig    gcc-15.2.0
riscv                            allyesconfig    clang-16
riscv                               defconfig    gcc-15.2.0
riscv                 randconfig-001-20260404    clang-19
riscv                 randconfig-001-20260404    clang-20
riscv                 randconfig-002-20260404    clang-20
riscv                 randconfig-002-20260404    clang-23
s390                             allmodconfig    clang-19
s390                              allnoconfig    clang-23
s390                             allyesconfig    gcc-15.2.0
s390                                defconfig    gcc-15.2.0
s390                  randconfig-001-20260404    clang-20
s390                  randconfig-002-20260404    clang-20
s390                  randconfig-002-20260404    clang-23
sh                               allmodconfig    gcc-15.2.0
sh                                allnoconfig    clang-23
sh                                allnoconfig    gcc-15.2.0
sh                               allyesconfig    clang-19
sh                                  defconfig    gcc-14
sh                    randconfig-001-20260404    clang-20
sh                    randconfig-001-20260404    gcc-15.2.0
sh                    randconfig-002-20260404    clang-20
sh                    randconfig-002-20260404    gcc-9.5.0
sparc                             allnoconfig    clang-23
sparc                             allnoconfig    gcc-15.2.0
sparc                               defconfig    gcc-15.2.0
sparc                 randconfig-001-20260404    clang-20
sparc                 randconfig-002-20260404    clang-20
sparc64                          allmodconfig    clang-23
sparc64                             defconfig    gcc-14
sparc64               randconfig-001-20260404    clang-20
sparc64               randconfig-002-20260404    clang-20
um                               allmodconfig    clang-19
um                                allnoconfig    clang-23
um                               allyesconfig    gcc-15.2.0
um                                  defconfig    gcc-14
um                             i386_defconfig    gcc-14
um                    randconfig-001-20260404    clang-20
um                    randconfig-002-20260404    clang-20
um                           x86_64_defconfig    gcc-14
x86_64                           allmodconfig    clang-20
x86_64                            allnoconfig    clang-20
x86_64                            allnoconfig    clang-23
x86_64                           allyesconfig    clang-20
x86_64      buildonly-randconfig-001-20260404    gcc-13
x86_64      buildonly-randconfig-002-20260404    gcc-13
x86_64      buildonly-randconfig-003-20260404    gcc-13
x86_64      buildonly-randconfig-004-20260404    gcc-13
x86_64      buildonly-randconfig-005-20260404    gcc-13
x86_64      buildonly-randconfig-006-20260404    gcc-13
x86_64                              defconfig    gcc-14
x86_64                                  kexec    clang-20
x86_64                randconfig-001-20260404    gcc-14
x86_64                randconfig-002-20260404    gcc-14
x86_64                randconfig-003-20260404    gcc-14
x86_64                randconfig-004-20260404    gcc-14
x86_64                randconfig-005-20260404    gcc-14
x86_64                randconfig-006-20260404    gcc-14
x86_64                randconfig-011-20260404    clang-20
x86_64                randconfig-011-20260404    gcc-14
x86_64                randconfig-012-20260404    gcc-14
x86_64                randconfig-013-20260404    clang-20
x86_64                randconfig-013-20260404    gcc-14
x86_64                randconfig-014-20260404    clang-20
x86_64                randconfig-014-20260404    gcc-14
x86_64                randconfig-015-20260404    clang-20
x86_64                randconfig-015-20260404    gcc-14
x86_64                randconfig-016-20260404    clang-20
x86_64                randconfig-016-20260404    gcc-14
x86_64                randconfig-071-20260404    gcc-14
x86_64                randconfig-072-20260404    gcc-14
x86_64                randconfig-073-20260404    gcc-14
x86_64                randconfig-074-20260404    gcc-14
x86_64                randconfig-075-20260404    gcc-14
x86_64                randconfig-076-20260404    gcc-14
x86_64                               rhel-9.4    clang-20
x86_64                           rhel-9.4-bpf    gcc-14
x86_64                          rhel-9.4-func    clang-20
x86_64                    rhel-9.4-kselftests    clang-20
x86_64                         rhel-9.4-kunit    gcc-14
x86_64                           rhel-9.4-ltp    gcc-14
x86_64                          rhel-9.4-rust    clang-20
xtensa                            allnoconfig    clang-23
xtensa                            allnoconfig    gcc-15.2.0
xtensa                           allyesconfig    clang-23
xtensa                randconfig-001-20260404    clang-20
xtensa                randconfig-002-20260404    clang-20

--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* [dtor-input:next] BUILD SUCCESS 84e7a17d1813394b48b0641fce8217fc0bba1960
From: kernel test robot @ 2026-04-04 18:08 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input

tree/branch: https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git next
branch HEAD: 84e7a17d1813394b48b0641fce8217fc0bba1960  Input: xpad - add RedOctane Games vendor id

elapsed time: 735m

configs tested: 235
configs skipped: 3

The following configs have been built successfully.
More configs may be tested in the coming days.

tested configs:
alpha                             allnoconfig    gcc-15.2.0
alpha                            allyesconfig    gcc-15.2.0
alpha                               defconfig    gcc-15.2.0
arc                              allmodconfig    clang-16
arc                              allmodconfig    gcc-15.2.0
arc                               allnoconfig    gcc-15.2.0
arc                              allyesconfig    clang-23
arc                              allyesconfig    gcc-15.2.0
arc                                 defconfig    gcc-15.2.0
arc                   randconfig-001-20260404    gcc-15.2.0
arc                   randconfig-001-20260404    gcc-8.5.0
arc                   randconfig-002-20260404    gcc-15.2.0
arc                   randconfig-002-20260404    gcc-8.5.0
arm                               allnoconfig    clang-23
arm                               allnoconfig    gcc-15.2.0
arm                              allyesconfig    clang-16
arm                              allyesconfig    gcc-15.2.0
arm                                 defconfig    gcc-15.2.0
arm                   randconfig-001-20260404    gcc-14.3.0
arm                   randconfig-001-20260404    gcc-15.2.0
arm                   randconfig-002-20260404    gcc-15.2.0
arm                   randconfig-003-20260404    gcc-10.5.0
arm                   randconfig-003-20260404    gcc-15.2.0
arm                   randconfig-004-20260404    gcc-12.5.0
arm                   randconfig-004-20260404    gcc-15.2.0
arm64                            allmodconfig    clang-19
arm64                            allmodconfig    clang-23
arm64                             allnoconfig    gcc-15.2.0
arm64                               defconfig    gcc-15.2.0
arm64                 randconfig-001-20260404    gcc-15.2.0
arm64                 randconfig-001-20260404    gcc-8.5.0
arm64                 randconfig-002-20260404    gcc-15.2.0
arm64                 randconfig-002-20260404    gcc-8.5.0
arm64                 randconfig-003-20260404    clang-23
arm64                 randconfig-003-20260404    gcc-15.2.0
arm64                 randconfig-004-20260404    gcc-15.2.0
csky                             allmodconfig    gcc-15.2.0
csky                              allnoconfig    gcc-15.2.0
csky                                defconfig    gcc-15.2.0
csky                  randconfig-001-20260404    gcc-15.2.0
csky                  randconfig-001-20260404    gcc-9.5.0
csky                  randconfig-002-20260404    gcc-15.2.0
hexagon                          allmodconfig    clang-17
hexagon                          allmodconfig    gcc-15.2.0
hexagon                           allnoconfig    clang-23
hexagon                           allnoconfig    gcc-15.2.0
hexagon                             defconfig    gcc-15.2.0
hexagon               randconfig-001-20260404    gcc-15.2.0
hexagon               randconfig-002-20260404    gcc-15.2.0
i386                             allmodconfig    clang-20
i386                             allmodconfig    gcc-14
i386                              allnoconfig    gcc-14
i386                              allnoconfig    gcc-15.2.0
i386                             allyesconfig    clang-20
i386                             allyesconfig    gcc-14
i386        buildonly-randconfig-001-20260404    clang-20
i386        buildonly-randconfig-001-20260404    gcc-14
i386        buildonly-randconfig-002-20260404    clang-20
i386        buildonly-randconfig-003-20260404    clang-20
i386        buildonly-randconfig-003-20260404    gcc-14
i386        buildonly-randconfig-004-20260404    clang-20
i386        buildonly-randconfig-005-20260404    clang-20
i386        buildonly-randconfig-006-20260404    clang-20
i386                                defconfig    gcc-15.2.0
i386                  randconfig-001-20260404    clang-20
i386                  randconfig-002-20260404    clang-20
i386                  randconfig-003-20260404    clang-20
i386                  randconfig-004-20260404    clang-20
i386                  randconfig-005-20260404    clang-20
i386                  randconfig-005-20260404    gcc-14
i386                  randconfig-006-20260404    clang-20
i386                  randconfig-007-20260404    clang-20
i386                  randconfig-011-20260404    clang-20
i386                  randconfig-012-20260404    clang-20
i386                  randconfig-013-20260404    clang-20
i386                  randconfig-014-20260404    clang-20
i386                  randconfig-015-20260404    clang-20
i386                  randconfig-015-20260404    gcc-14
i386                  randconfig-016-20260404    clang-20
i386                  randconfig-017-20260404    clang-20
i386                  randconfig-017-20260404    gcc-12
loongarch                        allmodconfig    clang-19
loongarch                        allmodconfig    clang-23
loongarch                         allnoconfig    clang-23
loongarch                         allnoconfig    gcc-15.2.0
loongarch                           defconfig    clang-19
loongarch             randconfig-001-20260404    gcc-15.2.0
loongarch             randconfig-002-20260404    gcc-15.2.0
m68k                             allmodconfig    gcc-15.2.0
m68k                              allnoconfig    gcc-15.2.0
m68k                             allyesconfig    clang-16
m68k                             allyesconfig    gcc-15.2.0
m68k                                defconfig    clang-19
microblaze                        allnoconfig    gcc-15.2.0
microblaze                       allyesconfig    gcc-15.2.0
microblaze                          defconfig    clang-19
mips                             allmodconfig    gcc-15.2.0
mips                              allnoconfig    gcc-15.2.0
mips                             allyesconfig    gcc-15.2.0
mips                        omega2p_defconfig    clang-23
nios2                            allmodconfig    clang-23
nios2                            allmodconfig    gcc-11.5.0
nios2                             allnoconfig    clang-23
nios2                             allnoconfig    gcc-11.5.0
nios2                               defconfig    clang-19
nios2                 randconfig-001-20260404    gcc-15.2.0
nios2                 randconfig-002-20260404    gcc-15.2.0
openrisc                         allmodconfig    clang-23
openrisc                         allmodconfig    gcc-15.2.0
openrisc                          allnoconfig    clang-23
openrisc                          allnoconfig    gcc-15.2.0
openrisc                            defconfig    gcc-15.2.0
parisc                           allmodconfig    gcc-15.2.0
parisc                            allnoconfig    clang-23
parisc                            allnoconfig    gcc-15.2.0
parisc                           allyesconfig    clang-19
parisc                              defconfig    gcc-15.2.0
parisc                randconfig-001-20260404    gcc-13.4.0
parisc                randconfig-002-20260404    gcc-15.2.0
parisc64                            defconfig    clang-19
powerpc                          allmodconfig    gcc-15.2.0
powerpc                           allnoconfig    clang-23
powerpc                           allnoconfig    gcc-15.2.0
powerpc                      bamboo_defconfig    clang-23
powerpc                      ep88xc_defconfig    gcc-15.2.0
powerpc                     ksi8560_defconfig    gcc-15.2.0
powerpc                 mpc834x_itx_defconfig    clang-16
powerpc               randconfig-001-20260404    gcc-8.5.0
powerpc               randconfig-002-20260404    clang-23
powerpc64             randconfig-001-20260404    clang-23
powerpc64             randconfig-002-20260404    gcc-10.5.0
riscv                            allmodconfig    clang-23
riscv                             allnoconfig    clang-23
riscv                             allnoconfig    gcc-15.2.0
riscv                            allyesconfig    clang-16
riscv                               defconfig    clang-23
riscv                               defconfig    gcc-15.2.0
riscv                 randconfig-001-20260404    clang-19
riscv                 randconfig-001-20260404    clang-20
riscv                 randconfig-002-20260404    clang-20
riscv                 randconfig-002-20260404    clang-23
s390                             allmodconfig    clang-19
s390                              allnoconfig    clang-23
s390                             allyesconfig    gcc-15.2.0
s390                                defconfig    clang-23
s390                                defconfig    gcc-15.2.0
s390                  randconfig-001-20260404    clang-20
s390                  randconfig-002-20260404    clang-20
s390                  randconfig-002-20260404    clang-23
sh                               allmodconfig    gcc-15.2.0
sh                                allnoconfig    clang-23
sh                                allnoconfig    gcc-15.2.0
sh                               allyesconfig    clang-19
sh                                  defconfig    gcc-14
sh                                  defconfig    gcc-15.2.0
sh                    randconfig-001-20260404    clang-20
sh                    randconfig-001-20260404    gcc-15.2.0
sh                    randconfig-002-20260404    clang-20
sh                    randconfig-002-20260404    gcc-9.5.0
sparc                             allnoconfig    clang-23
sparc                             allnoconfig    gcc-15.2.0
sparc                               defconfig    gcc-15.2.0
sparc                 randconfig-001-20260404    clang-20
sparc                 randconfig-001-20260404    gcc-15.2.0
sparc                 randconfig-002-20260404    clang-20
sparc                 randconfig-002-20260404    gcc-8.5.0
sparc64                          allmodconfig    clang-23
sparc64                             defconfig    clang-20
sparc64                             defconfig    gcc-14
sparc64               randconfig-001-20260404    clang-20
sparc64               randconfig-001-20260404    gcc-14.3.0
sparc64               randconfig-002-20260404    clang-20
sparc64               randconfig-002-20260404    clang-23
um                               allmodconfig    clang-19
um                                allnoconfig    clang-23
um                               allyesconfig    gcc-14
um                               allyesconfig    gcc-15.2.0
um                                  defconfig    clang-23
um                                  defconfig    gcc-14
um                             i386_defconfig    gcc-14
um                    randconfig-001-20260404    clang-20
um                    randconfig-002-20260404    clang-20
um                    randconfig-002-20260404    gcc-14
um                           x86_64_defconfig    clang-23
um                           x86_64_defconfig    gcc-14
x86_64                           allmodconfig    clang-20
x86_64                            allnoconfig    clang-20
x86_64                            allnoconfig    clang-23
x86_64                           allyesconfig    clang-20
x86_64      buildonly-randconfig-001-20260404    gcc-13
x86_64      buildonly-randconfig-002-20260404    gcc-13
x86_64      buildonly-randconfig-003-20260404    gcc-13
x86_64      buildonly-randconfig-004-20260404    gcc-13
x86_64      buildonly-randconfig-005-20260404    gcc-13
x86_64      buildonly-randconfig-006-20260404    gcc-13
x86_64                              defconfig    gcc-14
x86_64                                  kexec    clang-20
x86_64                randconfig-001-20260404    gcc-14
x86_64                randconfig-002-20260404    gcc-14
x86_64                randconfig-003-20260404    gcc-14
x86_64                randconfig-004-20260404    gcc-14
x86_64                randconfig-005-20260404    gcc-14
x86_64                randconfig-006-20260404    gcc-14
x86_64                randconfig-011-20260404    clang-20
x86_64                randconfig-011-20260404    gcc-14
x86_64                randconfig-012-20260404    gcc-14
x86_64                randconfig-013-20260404    clang-20
x86_64                randconfig-013-20260404    gcc-14
x86_64                randconfig-014-20260404    clang-20
x86_64                randconfig-014-20260404    gcc-14
x86_64                randconfig-015-20260404    clang-20
x86_64                randconfig-015-20260404    gcc-14
x86_64                randconfig-016-20260404    clang-20
x86_64                randconfig-016-20260404    gcc-14
x86_64                randconfig-071-20260404    gcc-14
x86_64                randconfig-072-20260404    gcc-14
x86_64                randconfig-073-20260404    gcc-14
x86_64                randconfig-074-20260404    gcc-14
x86_64                randconfig-075-20260404    gcc-14
x86_64                randconfig-076-20260404    gcc-14
x86_64                               rhel-9.4    clang-20
x86_64                           rhel-9.4-bpf    gcc-14
x86_64                          rhel-9.4-func    clang-20
x86_64                    rhel-9.4-kselftests    clang-20
x86_64                         rhel-9.4-kunit    gcc-14
x86_64                           rhel-9.4-ltp    gcc-14
x86_64                          rhel-9.4-rust    clang-20
xtensa                            allnoconfig    clang-23
xtensa                            allnoconfig    gcc-15.2.0
xtensa                           allyesconfig    clang-23
xtensa                           allyesconfig    gcc-15.2.0
xtensa                randconfig-001-20260404    clang-20
xtensa                randconfig-001-20260404    gcc-11.5.0
xtensa                randconfig-002-20260404    clang-20
xtensa                randconfig-002-20260404    gcc-13.4.0

--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* Re: [PATCH v2 0/1] HID: add malicious HID device detection driver
From: Greg KH @ 2026-04-05  5:31 UTC (permalink / raw)
  To: Zubeyr Almaho
  Cc: Jiri Kosina, Benjamin Tissoires, linux-input, linux-kernel,
	security
In-Reply-To: <20260404133746.80914-1-zybo1000@gmail.com>

On Sat, Apr 04, 2026 at 04:37:44PM +0300, Zubeyr Almaho wrote:
> Hi Jiri, Benjamin,
> 
> This series introduces hid-omg-detect, a passive HID monitor that scores
> potentially malicious keyboard-like USB devices (BadUSB / O.MG style)
> using:
> 
> - keystroke timing entropy,
> - plug-and-type latency,
> - USB descriptor fingerprinting.
> 
> When the configurable threshold is crossed, the module emits a warning
> with a userspace mitigation hint (usbguard).
> 
> The driver does not block, delay, or modify HID input events.

That's cute, but no need to get security@kernel.org involved as this is
a new feature, not a bug triage.

Also, why not just do this as an ebpf program instead as you have full
access to the hid data stream there?

thanks,

greg k-h

^ permalink raw reply

* Re: [PATCH RESEND] Input: atkbd - skip cleanup when used as a wakeup source
From: Henry Barnor @ 2026-04-05  5:36 UTC (permalink / raw)
  To: dmitry.torokhov; +Cc: hbarnor, linux-input, linux-kernel
In-Reply-To: <acrwy_Y6QMapp5fw@google.com>

Hi Dmitry,

> We unfortunately need to support devices much older than that, so we can
> not be sure that this change is safe. Historically there we a lot of
> instances where systems were unhappy if we attempted to enter shutdown
> or suspend paths with devices in state other than freshly reset.

Thanks for the feedback and the context on legacy hardware risks. That makes sense. 

I will explore handling this within the Android system layer instead.

Thanks.

^ permalink raw reply

* [PATCH] HID: sony: fix style issues
From: Rosalie Wanders @ 2026-04-06  2:47 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires, Henrik Rydberg
  Cc: Rosalie Wanders, linux-input, linux-kernel

This commit fixes inconsistent quirk names and also fixes all the
checkpatch.pl issues alongside inconsistent code, it also adds static
asserts to assert struct sizes at compile time.

Signed-off-by: Rosalie Wanders <rosalie@mailbox.org>
---
 drivers/hid/hid-sony.c | 79 +++++++++++++++++++-----------------------
 1 file changed, 35 insertions(+), 44 deletions(-)

diff --git a/drivers/hid/hid-sony.c b/drivers/hid/hid-sony.c
index bd4b4a470869..695ca085b410 100644
--- a/drivers/hid/hid-sony.c
+++ b/drivers/hid/hid-sony.c
@@ -67,8 +67,8 @@
 #define RB4_GUITAR_PS4_USB        BIT(18)
 #define RB4_GUITAR_PS4_BT         BIT(19)
 #define RB4_GUITAR_PS5            BIT(20)
-#define RB3_PS3_PRO_INSTRUMENT    BIT(21)
-#define PS3_DJH_TURNTABLE         BIT(22)
+#define RB3_PRO_INSTRUMENT        BIT(21)
+#define DJH_TURNTABLE             BIT(22)
 
 #define SIXAXIS_CONTROLLER (SIXAXIS_CONTROLLER_USB | SIXAXIS_CONTROLLER_BT)
 #define MOTION_CONTROLLER (MOTION_CONTROLLER_USB | MOTION_CONTROLLER_BT)
@@ -110,13 +110,6 @@ static const char ghl_ps4_magic_data[] = {
 	0x30, 0x02, 0x08, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00
 };
 
-/* Rock Band 3 PS3 Pro Instruments require sending a report
- * once an instrument is connected to its dongle.
- * We need to retry sending these reports,
- * but to avoid doing this too often we delay the retries
- */
-#define RB3_PRO_INSTRUMENT_POKE_RETRY_INTERVAL 8 /* In seconds */
-
 /* PS/3 Motion controller */
 static const u8 motion_rdesc[] = {
 	0x05, 0x01,         /*  Usage Page (Desktop),               */
@@ -477,6 +470,7 @@ struct sixaxis_led {
 	u8 duty_off; /* % of duty_length the led is off (0xff means 100%) */
 	u8 duty_on;  /* % of duty_length the led is on (0xff mean 100%) */
 } __packed;
+static_assert(sizeof(struct sixaxis_led) == 5);
 
 struct sixaxis_rumble {
 	u8 padding;
@@ -485,6 +479,7 @@ struct sixaxis_rumble {
 	u8 left_duration;    /* Left motor duration (0xff means forever) */
 	u8 left_motor_force; /* left (large) motor, supports force values from 0 to 255 */
 } __packed;
+static_assert(sizeof(struct sixaxis_rumble) == 5);
 
 struct sixaxis_output_report {
 	u8 report_id;
@@ -494,11 +489,13 @@ struct sixaxis_output_report {
 	struct sixaxis_led led[4];    /* LEDx at (4 - x) */
 	struct sixaxis_led _reserved; /* LED5, not actually soldered */
 } __packed;
+static_assert(sizeof(struct sixaxis_output_report) == 36);
 
 union sixaxis_output_report_01 {
 	struct sixaxis_output_report data;
 	u8 buf[36];
 };
+static_assert(sizeof(union sixaxis_output_report_01) == 36);
 
 struct motion_output_report_02 {
 	u8 type, zero;
@@ -506,6 +503,7 @@ struct motion_output_report_02 {
 	u8 zero2;
 	u8 rumble;
 };
+static_assert(sizeof(struct motion_output_report_02) == 7);
 
 #define SIXAXIS_REPORT_0xF2_SIZE 17
 #define SIXAXIS_REPORT_0xF5_SIZE 8
@@ -536,7 +534,7 @@ struct sony_sc {
 	struct led_classdev *leds[MAX_LEDS];
 	unsigned long quirks;
 	struct work_struct state_worker;
-	void (*send_output_report)(struct sony_sc *);
+	void (*send_output_report)(struct sony_sc *sc);
 	struct power_supply *battery;
 	struct power_supply_desc battery_desc;
 	int device_id;
@@ -613,11 +611,11 @@ static int ghl_init_urb(struct sony_sc *sc, struct usb_device *usbdev,
 	pipe = usb_sndctrlpipe(usbdev, 0);
 
 	cr = devm_kzalloc(&sc->hdev->dev, sizeof(*cr), GFP_ATOMIC);
-	if (cr == NULL)
+	if (!cr)
 		return -ENOMEM;
 
 	databuf = devm_kzalloc(&sc->hdev->dev, poke_size, GFP_ATOMIC);
-	if (databuf == NULL)
+	if (!databuf)
 		return -ENOMEM;
 
 	cr->bRequestType =
@@ -952,6 +950,7 @@ static void sixaxis_parse_report(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;
 
@@ -967,7 +966,7 @@ static void sixaxis_parse_report(struct sony_sc *sc, u8 *rd, int size)
 		battery_capacity = 100;
 		battery_status = (rd[offset] & 0x01) ? POWER_SUPPLY_STATUS_FULL : POWER_SUPPLY_STATUS_CHARGING;
 	} else {
-		u8 index = rd[offset] <= 5 ? rd[offset] : 5;
+		index = rd[offset] <= 5 ? rd[offset] : 5;
 		battery_capacity = sixaxis_battery_capacity[index];
 		battery_status = POWER_SUPPLY_STATUS_DISCHARGING;
 	}
@@ -1005,7 +1004,7 @@ static void nsg_mrxu_parse_report(struct sony_sc *sc, u8 *rd, int size)
 	 *   the touch-related data starts at offset 2.
 	 * For the first byte, bit 0 is set when touchpad button is pressed.
 	 * Bit 2 is set when a touch is active and the drag (Fn) key is pressed.
-	 * This drag key is mapped to BTN_LEFT.  It is operational only when a 
+	 * This drag key is mapped to BTN_LEFT.  It is operational only when a
 	 *   touch point is active.
 	 * Bit 4 is set when only the first touch point is active.
 	 * Bit 6 is set when only the second touch point is active.
@@ -1152,11 +1151,10 @@ static int sony_raw_event(struct hid_device *hdev, struct hid_report *report,
 	/* 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_PS3_PRO_INSTRUMENT) && rd[24] == 0x02) {
-		/* Only attempt to enable report every 8 seconds */
+	if ((sc->quirks & RB3_PRO_INSTRUMENT) && 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 +
-				(RB3_PRO_INSTRUMENT_POKE_RETRY_INTERVAL * HZ);
+			sc->rb3_pro_poke_jiffies = jiffies + secs_to_jiffies(8);
 			rb3_pro_instrument_enable_full_report(sc);
 		}
 	}
@@ -1218,7 +1216,7 @@ static int sony_mapping(struct hid_device *hdev, struct hid_input *hi,
 	if (sc->quirks & GH_GUITAR_TILT)
 		return gh_guitar_mapping(hdev, hi, field, usage, bit, max);
 
-	if (sc->quirks & PS3_DJH_TURNTABLE)
+	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))
@@ -1273,19 +1271,18 @@ static int sony_register_touchpad(struct sony_sc *sc, int touch_count,
 	input_set_abs_params(sc->touchpad, ABS_MT_POSITION_Y, 0, h, 0, 0);
 
 	if (touch_major > 0) {
-		input_set_abs_params(sc->touchpad, ABS_MT_TOUCH_MAJOR, 
+		input_set_abs_params(sc->touchpad, ABS_MT_TOUCH_MAJOR,
 			0, touch_major, 0, 0);
 		if (touch_minor > 0)
-			input_set_abs_params(sc->touchpad, ABS_MT_TOUCH_MINOR, 
+			input_set_abs_params(sc->touchpad, ABS_MT_TOUCH_MINOR,
 				0, touch_minor, 0, 0);
 		if (orientation > 0)
-			input_set_abs_params(sc->touchpad, ABS_MT_ORIENTATION, 
+			input_set_abs_params(sc->touchpad, ABS_MT_ORIENTATION,
 				0, orientation, 0, 0);
 	}
 
-	if (sc->quirks & NSG_MRXU_REMOTE) {
+	if (sc->quirks & NSG_MRXU_REMOTE)
 		__set_bit(EV_REL, sc->touchpad->evbit);
-	}
 
 	ret = input_mt_init_slots(sc->touchpad, touch_count, INPUT_MT_POINTER);
 	if (ret < 0)
@@ -1440,7 +1437,7 @@ static void sixaxis_set_leds_from_id(struct sony_sc *sc)
 
 	int id = sc->device_id;
 
-	BUILD_BUG_ON(MAX_LEDS < ARRAY_SIZE(sixaxis_leds[0]));
+	BUILD_BUG_ON(ARRAY_SIZE(sixaxis_leds[0]) > MAX_LEDS);
 
 	if (id < 0)
 		return;
@@ -1458,7 +1455,7 @@ static void buzz_set_leds(struct sony_sc *sc)
 		struct hid_report, list);
 	s32 *value = report->field[0]->value;
 
-	BUILD_BUG_ON(MAX_LEDS < 4);
+	BUILD_BUG_ON(4 > MAX_LEDS);
 
 	value[0] = 0x00;
 	value[1] = sc->led_state[0] ? 0xff : 0x00;
@@ -1655,15 +1652,12 @@ static int sony_leds_init(struct sony_sc *sc)
 			name_sz = strlen(dev_name(&hdev->dev)) + strlen(color_name_str[n]) + 2;
 
 		led = devm_kzalloc(&hdev->dev, sizeof(struct led_classdev) + name_sz, GFP_KERNEL);
-		if (!led) {
-			hid_err(hdev, "Couldn't allocate memory for LED %d\n", n);
+		if (!led)
 			return -ENOMEM;
-		}
 
 		name = (void *)(&led[1]);
 		if (use_color_names)
-			snprintf(name, name_sz, name_fmt, dev_name(&hdev->dev),
-			color_name_str[n]);
+			snprintf(name, name_sz, name_fmt, dev_name(&hdev->dev), color_name_str[n]);
 		else
 			snprintf(name, name_sz, name_fmt, dev_name(&hdev->dev), n + 1);
 		led->name = name;
@@ -2180,7 +2174,7 @@ static int sony_input_configured(struct hid_device *hdev,
 		}
 
 		sony_init_output_report(sc, sixaxis_send_output_report);
-	} else if (sc->quirks & RB3_PS3_PRO_INSTRUMENT) {
+	} else if (sc->quirks & RB3_PRO_INSTRUMENT) {
 		/*
 		 * Rock Band 3 PS3 Pro Instruments also do not handle HID Output
 		 * Reports on the interrupt EP like they should, so we need to force
@@ -2309,10 +2303,8 @@ static int sony_probe(struct hid_device *hdev, const struct hid_device_id *id)
 		quirks |= SHANWAN_GAMEPAD;
 
 	sc = devm_kzalloc(&hdev->dev, sizeof(*sc), GFP_KERNEL);
-	if (sc == NULL) {
-		hid_err(hdev, "can't alloc sony descriptor\n");
+	if (!sc)
 		return -ENOMEM;
-	}
 
 	spin_lock_init(&sc->lock);
 
@@ -2360,9 +2352,8 @@ static int sony_probe(struct hid_device *hdev, const struct hid_device_id *id)
 		goto err;
 	}
 
-	if (sc->quirks & RB3_PS3_PRO_INSTRUMENT) {
+	if (sc->quirks & RB3_PRO_INSTRUMENT)
 		sc->rb3_pro_poke_jiffies = 0;
-	}
 
 	if (sc->quirks & (GHL_GUITAR_PS3WIIU | GHL_GUITAR_PS4)) {
 		if (!hid_is_usb(hdev)) {
@@ -2514,7 +2505,7 @@ static const struct hid_device_id sony_devices[] = {
 		.driver_data = INSTRUMENT },
 	/* DJ Hero PS3 Guitar Dongle */
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_DJH_TURNTABLE),
-		.driver_data = PS3_DJH_TURNTABLE | INSTRUMENT },
+		.driver_data = DJH_TURNTABLE | INSTRUMENT },
 	/* Guitar Hero Live PS4 guitar dongles */
 	{ HID_USB_DEVICE(USB_VENDOR_ID_REDOCTANE, USB_DEVICE_ID_REDOCTANE_PS4_GHLIVE_DONGLE),
 		.driver_data = GHL_GUITAR_PS4 | GH_GUITAR_TILT | INSTRUMENT },
@@ -2552,17 +2543,17 @@ static const struct hid_device_id sony_devices[] = {
 		.driver_data = INSTRUMENT },
 	/* Rock Band 3 PS3 Pro instruments */
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MUSTANG_GUITAR),
-		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+		.driver_data = INSTRUMENT | RB3_PRO_INSTRUMENT },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_SQUIRE_GUITAR),
-		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+		.driver_data = INSTRUMENT | RB3_PRO_INSTRUMENT },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_MUSTANG_MODE),
-		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+		.driver_data = INSTRUMENT | RB3_PRO_INSTRUMENT },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_SQUIRE_MODE),
-		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+		.driver_data = INSTRUMENT | RB3_PRO_INSTRUMENT },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_KEYBOARD),
-		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+		.driver_data = INSTRUMENT | RB3_PRO_INSTRUMENT },
 	{ HID_USB_DEVICE(USB_VENDOR_ID_SONY_RHYTHM, USB_DEVICE_ID_SONY_PS3_RB3_MPA_KEYBOARD_MODE),
-		.driver_data = INSTRUMENT | RB3_PS3_PRO_INSTRUMENT },
+		.driver_data = INSTRUMENT | RB3_PRO_INSTRUMENT },
 	/* Rock Band 4 PS4 guitars */
 	{ HID_USB_DEVICE(USB_VENDOR_ID_PDP, USB_DEVICE_ID_PDP_PS4_RIFFMASTER),
 		.driver_data = RB4_GUITAR_PS4_USB | INSTRUMENT },
-- 
2.53.0


^ permalink raw reply related

* [PATCH] HID: alps: fix NULL pointer dereference in alps_raw_event()
From: Greg Kroah-Hartman @ 2026-04-06 14:03 UTC (permalink / raw)
  To: linux-input
  Cc: linux-kernel, Greg Kroah-Hartman, stable, Jiri Kosina,
	Benjamin Tissoires, Masaki Ota

Commit ecfa6f34492c ("HID: Add HID_CLAIMED_INPUT guards in raw_event
callbacks missing them") attempted to fix up the HID drivers that had
missed the previous fix that was done in 2ff5baa9b527 ("HID: appleir:
Fix potential NULL dereference at raw event handle"), but the alps
driver was missed.

Fix this up by properly checking in the hid-alps driver that it had been
claimed correctly before attempting to process the raw event.

Fixes: 73196ebe134d ("HID: alps: add support for Alps T4 Touchpad device")
Cc: stable <stable@kernel.org>
Cc: Jiri Kosina <jikos@kernel.org>
Cc: Benjamin Tissoires <bentiss@kernel.org>
Cc: Masaki Ota <masaki.ota@jp.alps.com>
Cc: linux-input@vger.kernel.org
Assisted-by: gregkh_clanker_t1000
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
---
 drivers/hid/hid-alps.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/drivers/hid/hid-alps.c b/drivers/hid/hid-alps.c
index 21e55f3d0d1b..67179e3fe39b 100644
--- a/drivers/hid/hid-alps.c
+++ b/drivers/hid/hid-alps.c
@@ -437,6 +437,9 @@ static int alps_raw_event(struct hid_device *hdev,
 	int ret = 0;
 	struct alps_dev *hdata = hid_get_drvdata(hdev);
 
+	if (!(hdev->claimed & HID_CLAIMED_INPUT) || !hdata->input)
+		return 0;
+
 	switch (hdev->product) {
 	case HID_PRODUCT_ID_T4_BTNLESS:
 		ret = t4_raw_event(hdata, data, size);
-- 
2.53.0


^ permalink raw reply related

* [PATCH] HID: core: clamp report_size in s32ton() to avoid undefined shift
From: Greg Kroah-Hartman @ 2026-04-06 14:04 UTC (permalink / raw)
  To: linux-input
  Cc: linux-kernel, Greg Kroah-Hartman, stable, Jiri Kosina,
	Benjamin Tissoires

s32ton() shifts by n-1 where n is the field's report_size, a value that
comes directly from a HID device.  The HID parser bounds report_size
only to <= 256, so a broken HID device can supply a report descriptor
with a wide field that triggers shift exponents up to 256 on a 32-bit
type when an output report is built via hid_output_field() or
hid_set_field().

Commit ec61b41918587 ("HID: core: fix shift-out-of-bounds in
hid_report_raw_event") added the same n > 32 clamp to the function
snto32(), but s32ton() was never given the same fix as I guess syzbot
hadn't figured out how to fuzz a device the same way.

Fix this up by just clamping the max value of n, just like snto32()
does.

Cc: stable <stable@kernel.org>
Cc: Jiri Kosina <jikos@kernel.org>
Cc: Benjamin Tissoires <bentiss@kernel.org>
Cc: linux-input@vger.kernel.org
Assisted-by: gregkh_clanker_t1000
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
---
 drivers/hid/hid-core.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index 833df14ef68f..868c65684aa8 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -71,6 +71,9 @@ static u32 s32ton(__s32 value, unsigned int n)
 	if (!value || !n)
 		return 0;
 
+	if (n > 32)
+		n = 32;
+
 	a = value >> (n - 1);
 	if (a && a != -1)
 		return value < 0 ? 1 << (n - 1) : (1 << (n - 1)) - 1;
-- 
2.53.0


^ permalink raw reply related

* [PATCH] Input: gpio-keys - add hibernation support
From: Armando De Leon @ 2026-04-06 16:04 UTC (permalink / raw)
  To: dmitry.torokhov; +Cc: linux-input, linux-kernel, Armando De Leon

The gpio-keys driver uses DEFINE_SIMPLE_DEV_PM_OPS which maps .freeze
and .restore to the same callbacks as .suspend and .resume. This is
insufficient for hibernation (suspend-to-disk) because the SoC is fully
powered off, unlike suspend-to-RAM where hardware state is preserved.

After hibernation resume, GPIO keys fail to function correctly because
the interrupt controller (e.g. GIC) is fully re-initialized during
restore, resetting all IRQ trigger type configurations to defaults.
GPIO keys require IRQ_TYPE_EDGE_BOTH for proper press/release detection,
but after the interrupt controller re-initialization only a single edge
remains active. This causes buttons to report only release events but
not press events, or vice versa.

The existing gpio_keys_button_disable_wakeup() does restore
IRQ_TYPE_EDGE_BOTH, but only when wakeup_trigger_type is non-zero.
When the device tree does not specify wakeup-event-action (the common
case), wakeup_trigger_type defaults to zero and the IRQ type
restoration is skipped entirely.

Fix this by implementing dedicated .freeze and .restore callbacks:

- gpio_keys_freeze(): Marks all buttons as suspended before the
  hibernate image is created. Unlike .suspend, it does not configure
  wakeup IRQs since the SoC will be powered off.

- gpio_keys_restore(): Re-applies the pinctrl default state to restore
  GPIO pin configuration, explicitly reconfigures IRQ trigger type to
  IRQ_TYPE_EDGE_BOTH for all GPIO-backed buttons, clears the suspended
  flag, and re-syncs current GPIO state with the input subsystem.

The .thaw callback reuses gpio_keys_resume() since the SoC remains
powered when hibernation is aborted and hardware state is intact.

Signed-off-by: Armando De Leon <learmand@amazon.com>
---
 drivers/input/keyboard/gpio_keys.c | 60 +++++++++++++++++++++++++++++-
 1 file changed, 59 insertions(+), 1 deletion(-)

diff --git a/drivers/input/keyboard/gpio_keys.c b/drivers/input/keyboard/gpio_keys.c
index e19617485..9fb77c9aa 100644
--- a/drivers/input/keyboard/gpio_keys.c
+++ b/drivers/input/keyboard/gpio_keys.c
@@ -27,6 +27,7 @@
 #include <linux/gpio/consumer.h>
 #include <linux/of.h>
 #include <linux/of_irq.h>
+#include <linux/pinctrl/consumer.h>
 #include <linux/spinlock.h>
 #include <dt-bindings/input/gpio-keys.h>
 
@@ -1088,7 +1089,64 @@ static int gpio_keys_resume(struct device *dev)
 	return 0;
 }
 
-static DEFINE_SIMPLE_DEV_PM_OPS(gpio_keys_pm_ops, gpio_keys_suspend, gpio_keys_resume);
+static int gpio_keys_freeze(struct device *dev)
+{
+	struct gpio_keys_drvdata *ddata = dev_get_drvdata(dev);
+	struct gpio_button_data *bdata;
+	int i;
+
+	for (i = 0; i < ddata->pdata->nbuttons; i++) {
+		bdata = &ddata->data[i];
+		bdata->suspended = true;
+	}
+
+	return 0;
+}
+
+static int gpio_keys_restore(struct device *dev)
+{
+	struct gpio_keys_drvdata *ddata = dev_get_drvdata(dev);
+	struct gpio_button_data *bdata;
+	int error;
+	int i;
+
+	error = pinctrl_pm_select_default_state(dev);
+	if (error)
+		dev_warn(dev, "failed to restore pinctrl default state: %d\n",
+			 error);
+
+	for (i = 0; i < ddata->pdata->nbuttons; i++) {
+		bdata = &ddata->data[i];
+		bdata->suspended = false;
+
+		/*
+		 * After hibernation the interrupt controller is
+		 * re-initialized and loses its configuration.
+		 * Restore dual-edge triggering for GPIO-backed
+		 * buttons so both press and release are detected.
+		 */
+		if (bdata->gpiod) {
+			error = irq_set_irq_type(bdata->irq,
+						 IRQ_TYPE_EDGE_BOTH);
+			if (error)
+				dev_warn(dev,
+					 "failed to restore IRQ %d trigger: %d\n",
+					 bdata->irq, error);
+		}
+	}
+
+	gpio_keys_report_state(ddata);
+	return 0;
+}
+
+static const struct dev_pm_ops gpio_keys_pm_ops = {
+	.suspend	= gpio_keys_suspend,
+	.resume		= gpio_keys_resume,
+	.freeze		= gpio_keys_freeze,
+	.restore	= gpio_keys_restore,
+	.thaw		= gpio_keys_resume,
+	.poweroff	= gpio_keys_suspend,
+};
 
 static void gpio_keys_shutdown(struct platform_device *pdev)
 {
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH] Input: gpio-keys - add hibernation support
From: Dmitry Torokhov @ 2026-04-06 16:24 UTC (permalink / raw)
  To: Armando De Leon; +Cc: linux-input, linux-kernel, Armando De Leon
In-Reply-To: <20260406160437.3084755-1-learmand@amazon.com>

Hi Armando,

On Mon, Apr 06, 2026 at 09:04:37AM -0700, Armando De Leon wrote:
> The gpio-keys driver uses DEFINE_SIMPLE_DEV_PM_OPS which maps .freeze
> and .restore to the same callbacks as .suspend and .resume. This is
> insufficient for hibernation (suspend-to-disk) because the SoC is fully
> powered off, unlike suspend-to-RAM where hardware state is preserved.
> 
> After hibernation resume, GPIO keys fail to function correctly because
> the interrupt controller (e.g. GIC) is fully re-initialized during
> restore, resetting all IRQ trigger type configurations to defaults.
> GPIO keys require IRQ_TYPE_EDGE_BOTH for proper press/release detection,
> but after the interrupt controller re-initialization only a single edge
> remains active. This causes buttons to report only release events but
> not press events, or vice versa.

I believe you just described a bug in the interrupt controller handling
of hibernation. It needs to be fixed there, not in consumer driver.

Thanks.

-- 
Dmitry

^ permalink raw reply

* Re: [PATCH] Input: gpio-keys - add hibernation support
From: Armando De Leon @ 2026-04-06 16:58 UTC (permalink / raw)
  To: dmitry.torokhov; +Cc: Armando De Leon, linux-input, linux-kernel
In-Reply-To: <adPdc2Z1SYxvqDmP@google.com>

Hi Dmitry,

Thank you for the review.

The interrupt controller (GICv3) is re-initialized by platform
firmware during hibernate restore - this is expected behavior, not
a bug. The IRQ trigger type (EDGE_BOTH) was originally configured
by devm_request_any_context_irq() during probe(), which does not
run again after hibernate restore.

The TLMM/pinctrl registers are correctly saved and restored by the
platform's syscore_ops - I verified this with register dumps. The
issue is specifically that the IRQ trigger type configured at the
GIC level during probe is lost and not re-applied.

Should the generic IRQ core be responsible for restoring trigger
types across hibernate? Otherwise, consumer drivers like gpio-keys need to handle
this in their .restore callback.

Either way, gpio-keys currently lacks .freeze/.restore callbacks
entirely, which is needed for proper hibernation support.

Thanks,
Armando

^ permalink raw reply

* Re: [PATCH 1/2] input: pc110pad: change PCI check to get rid of orphaned no_pci_devices
From: Bjorn Helgaas @ 2026-04-06 17:23 UTC (permalink / raw)
  To: Heiner Kallweit
  Cc: Dmitry Torokhov, Bjorn Helgaas, open list:HID CORE LAYER,
	linux-pci@vger.kernel.org
In-Reply-To: <cf519463-775a-4d43-be38-20742fdad407@gmail.com>

On Fri, Apr 03, 2026 at 12:17:35AM +0200, Heiner Kallweit wrote:
> As a prerequisite for removing no_pci_devices(), replace its usage here
> with an equivalent check for presence of a PCI bus.
> 
> Signed-off-by: Heiner Kallweit <hkallweit1@gmail.com>
> ---
>  drivers/input/mouse/pc110pad.c | 2 +-
>  1 file changed, 1 insertion(+), 1 deletion(-)
> 
> diff --git a/drivers/input/mouse/pc110pad.c b/drivers/input/mouse/pc110pad.c
> index efa58049f..c7a6df210 100644
> --- a/drivers/input/mouse/pc110pad.c
> +++ b/drivers/input/mouse/pc110pad.c
> @@ -91,7 +91,7 @@ static int __init pc110pad_init(void)
>  {
>  	int err;
>  
> -	if (!no_pci_devices())
> +	if (pci_find_next_bus(NULL))
>  		return -ENODEV;

I'd certainly love to get rid of no_pci_devices(), although using
pci_find_next_bus() is not really much better.

I don't have much opinion about pc110pad.c change.  It's essentially
still the same kludge, which has been there at least since the
beginning of git history.

Dmitry, if you want to ack this, I can take both together.

Bjorn

^ permalink raw reply

* Re: [PATCH] Input: gpio-keys - add hibernation support
From: Dmitry Torokhov @ 2026-04-06 18:18 UTC (permalink / raw)
  To: Armando De Leon; +Cc: Armando De Leon, linux-input, linux-kernel
In-Reply-To: <20260406165833.3137128-1-learmand@amazon.com>

On Mon, Apr 06, 2026 at 09:58:06AM -0700, Armando De Leon wrote:
> Hi Dmitry,
> 
> Thank you for the review.
> 
> The interrupt controller (GICv3) is re-initialized by platform
> firmware during hibernate restore - this is expected behavior, not
> a bug. The IRQ trigger type (EDGE_BOTH) was originally configured
> by devm_request_any_context_irq() during probe(), which does not
> run again after hibernate restore.
> 
> The TLMM/pinctrl registers are correctly saved and restored by the
> platform's syscore_ops - I verified this with register dumps. The
> issue is specifically that the IRQ trigger type configured at the
> GIC level during probe is lost and not re-applied.
> 
> Should the generic IRQ core be responsible for restoring trigger
> types across hibernate? Otherwise, consumer drivers like gpio-keys need to handle
> this in their .restore callback.

I am not sure if it is job of IRQ core vs. particular interrupt
controller, but they should restore the trigger types along with
entirety of the interrupts and pins states to exactly the same condition
that they were before entering hibernation.

> 
> Either way, gpio-keys currently lacks .freeze/.restore callbacks
> entirely, which is needed for proper hibernation support.

Drivers only need dedicated freeze and restore handlers if the behavior
should be different between suspend-to-ram vs suspend-to-disk. For the
vast majority of the drivers they behave exactly the same and I do not
see why it would be different for gpio-keys.

Thanks.

-- 
Dmitry

^ permalink raw reply

* Re: [PATCH] HID: lg-g15: Add support for the Logitech G710/G710+ gaming keyboards
From: Hans de Goede @ 2026-04-06 18:50 UTC (permalink / raw)
  To: Xavier Bestel, Jiri Kosina, Benjamin Tissoires; +Cc: linux-input, linux-kernel
In-Reply-To: <20260402075239.3829699-1-xav@bes.tel>

Hi Xavier,

Thank you for your patch, it is always nice to have people helping
with improving Linux support for devices like this.

On 2-Apr-26 09:52, Xavier Bestel wrote:
> Add support for the Logitech G710 and G710+ (USB ID 046d:c24d) gaming
> keyboards to the hid-lg-g15 driver.
> 
> These keyboards have 6 G-keys (G1-G6), M-keys (M1-M3, MR), a game mode
> toggle, and two independent backlights: one for the main keyboard and one
> for the WASD keys, each with a physical button to cycle brightness through
> 5 levels (0-4).
> 
> Key implementation details:
> 
>  - G-keys and M-keys are reported via HID input report 0x03 (4 bytes)
>    using KEY_MACRO1-6, KEY_MACRO_PRESET1-3 and KEY_MACRO_RECORD_START.
> 
>  - The WASD backlight LED is registered as
>    "g15::kbd_zoned_backlight-wasd" rather than with a
>    "::kbd_backlight" suffix, because UPower currently only supports a
>    single kbd_backlight LED per device. This follows the nomenclature
>    from Documentation/leds/leds-class.rst

Actually that nomenclature is something which was suggested in the past
but has never been agreed upon.

Since the discussion about how to name the LED class devices for
multi-zone keyboard backlights keeps coming up I've submitted a patch
now to document how these should be named:

https://lore.kernel.org/linux-leds/20260406174638.320135-1-johannes.goede@oss.qualcomm.com/

Since the new userspace API for this needs to be agreed upon first, that
patch should be accepted upstream first, before we can move forward with
the G710 support from this patch.

> 
>  - The G710+ firmware GET_REPORT for the backlight feature (0x08)
>    always returns the power-on default values, ignoring any changes
>    made via SET_REPORT. To work around this, the backlight brightness
>    is tracked in the driver cache and brightness_get returns the
>    cached value. M-key and game mode LEDs read back correctly.
> 
>  - The physical brightness cycle buttons are handled following the
>    same pattern as the G15/G15v2: no key events are sent, instead the
>    driver cycles the cached brightness and calls
>    led_classdev_notify_brightness_hw_changed() from a work function,
>    which allows GNOME to show the brightness OSD.
> 
>  - The game mode toggle is handled entirely by the firmware (it
>    disables the Super key and lights an indicator LED). The driver
>    exposes a read-only LED "g15::gamemode" with the
>    LED_BRIGHT_HW_CHANGED flag, and notifies userspace of state
>    changes via led_classdev_notify_brightness_hw_changed() by reading
>    back the actual firmware state on each toggle.
> 
>  - Both brightness LEDs are registered with the LED_BRIGHT_HW_CHANGED
>    flag to enable the brightness_hw_changed sysfs attribute.
> 
>  - HID_QUIRK_NOGET is set because the keyboard has buggy GET_REPORT
>    handling that causes timeouts on some feature reports.
> 
>  - The G-keys feature report (0x09) uses 2 bytes per key rather than
>    1 as on the G510, so the report size calculation is adjusted
>    accordingly.
> 
>  - Also fix a pre-existing comment typo: "f000.0000" -> "ff00.0000"
>    for the application report ID.
> 
>  - The loop bounds in lg_g15_led_set() and lg_g510_led_set() are
>    tightened from "< LG_G15_LED_MAX" to "<= LG_G15_MACRO_RECORD" to
>    avoid iterating over the new LG_G15_GAMEMODE enum value which does
>    not apply to those keyboards.
> 
> Signed-off-by: Xavier Bestel <xav@bes.tel>
> ---
>  drivers/hid/hid-ids.h    |   1 +
>  drivers/hid/hid-lg-g15.c | 393 ++++++++++++++++++++++++++++++++++++++-
>  2 files changed, 384 insertions(+), 10 deletions(-)
> 
> diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
> index afcee13bad61..7c0f930bd014 100644
> --- a/drivers/hid/hid-ids.h
> +++ b/drivers/hid/hid-ids.h
> @@ -904,6 +904,7 @@
>  #define USB_DEVICE_ID_LOGITECH_G15_V2_LCD	0xc227
>  #define USB_DEVICE_ID_LOGITECH_G510		0xc22d
>  #define USB_DEVICE_ID_LOGITECH_G510_USB_AUDIO	0xc22e
> +#define USB_DEVICE_ID_LOGITECH_G710		0xc24d
>  #define USB_DEVICE_ID_LOGITECH_G29_WHEEL	0xc24f
>  #define USB_DEVICE_ID_LOGITECH_G920_WHEEL	0xc262
>  #define USB_DEVICE_ID_LOGITECH_G923_XBOX_WHEEL	0xc26e
> diff --git a/drivers/hid/hid-lg-g15.c b/drivers/hid/hid-lg-g15.c
> index 1a88bc44ada4..8a4c4eb22c07 100644
> --- a/drivers/hid/hid-lg-g15.c
> +++ b/drivers/hid/hid-lg-g15.c
> @@ -29,6 +29,11 @@
>  #define LG_G510_INPUT_MACRO_KEYS	0x03
>  #define LG_G510_INPUT_KBD_BACKLIGHT	0x04
>  
> +#define LG_G710_FEATURE_GAMEMODE        0x05
> +#define LG_G710_FEATURE_M_KEYS_LEDS     0x06
> +#define LG_G710_FEATURE_BACKLIGHT       0x08
> +#define LG_G710_FEATURE_EXTRA_KEYS      0x09
> +
>  #define LG_G13_INPUT_REPORT		0x01
>  #define LG_G13_FEATURE_M_KEYS_LEDS	0x05
>  #define LG_G13_FEATURE_BACKLIGHT_RGB	0x07
> @@ -48,6 +53,7 @@ enum lg_g15_model {
>  	LG_G15_V2,
>  	LG_G510,
>  	LG_G510_USB_AUDIO,
> +	LG_G710,
>  	LG_Z10,
>  };
>  
> @@ -59,6 +65,7 @@ enum lg_g15_led_type {
>  	LG_G15_MACRO_PRESET2,
>  	LG_G15_MACRO_PRESET3,
>  	LG_G15_MACRO_RECORD,
> +	LG_G15_GAMEMODE,
>  	LG_G15_LED_MAX
>  };
>  
> @@ -91,7 +98,9 @@ struct lg_g15_data {
>  	enum lg_g15_model model;
>  	struct lg_g15_led leds[LG_G15_LED_MAX];
>  	bool game_mode_enabled;
> +	u16 pressed_keys;
>  	bool backlight_disabled;	/* true == HW backlight toggled *OFF* */
> +	unsigned long brightness_changed;	/* bitmask of LEDs hw-cycled */
>  };
>  
>  /********* G13 LED functions ***********/
> @@ -334,7 +343,7 @@ static int lg_g15_led_set(struct led_classdev *led_cdev,
>  		g15->transfer_buf[1] = g15_led->led + 1;
>  		g15->transfer_buf[2] = brightness << (g15_led->led * 4);
>  	} else {
> -		for (i = LG_G15_MACRO_PRESET1; i < LG_G15_LED_MAX; i++) {
> +		for (i = LG_G15_MACRO_PRESET1; i <= LG_G15_MACRO_RECORD; i++) {
>  			if (i == g15_led->led)
>  				val = brightness;
>  			else
> @@ -567,7 +576,7 @@ static int lg_g510_mkey_led_set(struct led_classdev *led_cdev,
>  
>  	mutex_lock(&g15->mutex);
>  
> -	for (i = LG_G15_MACRO_PRESET1; i < LG_G15_LED_MAX; i++) {
> +	for (i = LG_G15_MACRO_PRESET1; i <= LG_G15_MACRO_RECORD; i++) {
>  		if (i == g15_led->led)
>  			val = brightness;
>  		else
> @@ -597,6 +606,239 @@ static int lg_g510_mkey_led_set(struct led_classdev *led_cdev,
>  	return ret;
>  }
>  
> +/******** G710 LED functions ********/
> +
> +static int lg_g710_update_game_led_brightness(struct lg_g15_data *g15)
> +{
> +	int ret;
> +
> +	ret = hid_hw_raw_request(g15->hdev, LG_G710_FEATURE_GAMEMODE,
> +				 g15->transfer_buf, 8,
> +				 HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
> +	if (ret != 8) {
> +		hid_err(g15->hdev, "Error getting gamemode LED brightness: %d\n", ret);
> +		return (ret < 0) ? ret : -EIO;
> +	}
> +
> +	g15->leds[LG_G15_GAMEMODE].brightness =
> +		!!g15->transfer_buf[1];
> +
> +	return 0;
> +}
> +
> +static int lg_g710_update_mkey_led_brightness(struct lg_g15_data *g15)
> +{
> +	int ret;
> +
> +	ret = hid_hw_raw_request(g15->hdev, LG_G710_FEATURE_M_KEYS_LEDS,
> +				 g15->transfer_buf, 2,
> +				 HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
> +	if (ret != 2) {
> +		hid_err(g15->hdev, "Error getting macro LED brightness: %d\n", ret);
> +		return (ret < 0) ? ret : -EIO;
> +	}
> +
> +	g15->leds[LG_G15_MACRO_PRESET1].brightness =
> +		!!(g15->transfer_buf[1] & 0x10);
> +	g15->leds[LG_G15_MACRO_PRESET2].brightness =
> +		!!(g15->transfer_buf[1] & 0x20);
> +	g15->leds[LG_G15_MACRO_PRESET3].brightness =
> +		!!(g15->transfer_buf[1] & 0x40);
> +	g15->leds[LG_G15_MACRO_RECORD].brightness =
> +		!!(g15->transfer_buf[1] & 0x80);
> +
> +	return 0;
> +}
> +
> +static int lg_g710_update_kbd_led_brightness(struct lg_g15_data *g15)
> +{
> +	int ret;
> +
> +	ret = hid_hw_raw_request(g15->hdev, LG_G710_FEATURE_BACKLIGHT,
> +				 g15->transfer_buf, 4,
> +				 HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
> +	if (ret != 4) {
> +		hid_err(g15->hdev, "Error getting LED brightness: %d\n", ret);
> +		return (ret < 0) ? ret : -EIO;
> +	}
> +
> +	g15->leds[LG_G15_KBD_BRIGHTNESS].brightness = 4 - g15->transfer_buf[2];
> +	g15->leds[LG_G15_LCD_BRIGHTNESS].brightness = 4 - g15->transfer_buf[1];

I think this needs a range-check on the buf contents to avoid e.g.
negative brightness values when we somehow get unexpected values back from
the keyboard.



> +
> +	return 0;
> +}
> +
> +static enum led_brightness lg_g710_led_get(struct led_classdev *led_cdev)
> +{
> +	struct lg_g15_led *g15_led =
> +		container_of(led_cdev, struct lg_g15_led, cdev);
> +	struct lg_g15_data *g15 = dev_get_drvdata(led_cdev->dev->parent);
> +	enum led_brightness brightness;
> +
> +	mutex_lock(&g15->mutex);
> +	/*
> +	 * The G710+ firmware's GET_REPORT for the backlight always returns
> +	 * the power-on default values, ignoring any changes made via
> +	 * SET_REPORT. Use the cached brightness which is kept in sync by
> +	 * the _set callbacks. M-key and gamemode LEDs read back correctly.
> +	 */
> +	if (g15_led->led >= LG_G15_BRIGHTNESS_MAX && g15_led->led < LG_G15_GAMEMODE)
> +		lg_g710_update_mkey_led_brightness(g15);
> +	else if (g15_led->led >= LG_G15_GAMEMODE)
> +		lg_g710_update_game_led_brightness(g15);
> +	brightness = g15->leds[g15_led->led].brightness;
> +	mutex_unlock(&g15->mutex);
> +
> +	return brightness;
> +}
> +
> +static int lg_g710_mkey_led_set(struct led_classdev *led_cdev,
> +			  enum led_brightness brightness)
> +{
> +	struct lg_g15_led *g15_led =
> +		container_of(led_cdev, struct lg_g15_led, cdev);
> +	struct lg_g15_data *g15 = dev_get_drvdata(led_cdev->dev->parent);
> +	u8 val, mask = 0;
> +	int i, ret;
> +
> +	/* Ignore LED off on unregister / keyboard unplug */
> +	if (led_cdev->flags & LED_UNREGISTERING)
> +		return 0;
> +
> +	mutex_lock(&g15->mutex);
> +
> +	g15->transfer_buf[0] = LG_G710_FEATURE_M_KEYS_LEDS;
> +
> +	for (i = LG_G15_MACRO_PRESET1; i <= LG_G15_MACRO_RECORD; i++) {
> +		if (i == g15_led->led)
> +			val = brightness;
> +		else
> +			val = g15->leds[i].brightness;
> +
> +		if (val)
> +			mask |= 1 << (i - LG_G15_MACRO_PRESET1 + 4);
> +	}
> +
> +	g15->transfer_buf[1] = mask;
> +
> +	ret = hid_hw_raw_request(g15->hdev, LG_G710_FEATURE_M_KEYS_LEDS,
> +				 g15->transfer_buf, 2,
> +				 HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
> +	if (ret == 2) {
> +		/* Success */
> +		g15_led->brightness = brightness;
> +		ret = 0;
> +	} else {
> +		hid_err(g15->hdev, "Error setting LED brightness: %d\n", ret);
> +		ret = (ret < 0) ? ret : -EIO;
> +	}
> +
> +	mutex_unlock(&g15->mutex);
> +
> +	return ret;
> +}
> +
> +static int lg_g710_kbd_led_set(struct led_classdev *led_cdev,
> +			  enum led_brightness brightness)
> +{
> +	struct lg_g15_led *g15_led =
> +		container_of(led_cdev, struct lg_g15_led, cdev);
> +	struct lg_g15_data *g15 = dev_get_drvdata(led_cdev->dev->parent);
> +	int ret;
> +
> +	/* Ignore LED off on unregister / keyboard unplug */
> +	if (led_cdev->flags & LED_UNREGISTERING)
> +		return 0;
> +
> +	mutex_lock(&g15->mutex);
> +
> +	g15->transfer_buf[0] = LG_G710_FEATURE_BACKLIGHT;
> +	g15->transfer_buf[3] = 0;
> +
> +	if (g15_led->led == LG_G15_KBD_BRIGHTNESS) {
> +		g15->transfer_buf[1] = 4 - g15->leds[LG_G15_LCD_BRIGHTNESS].brightness;
> +		g15->transfer_buf[2] = 4 - brightness;
> +	} else {
> +		g15->transfer_buf[1] = 4 - brightness;
> +		g15->transfer_buf[2] = 4 - g15->leds[LG_G15_KBD_BRIGHTNESS].brightness;
> +	}
> +
> +	ret = hid_hw_raw_request(g15->hdev, LG_G710_FEATURE_BACKLIGHT,
> +				 g15->transfer_buf, 4,
> +				 HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
> +	if (ret == 4) {
> +		/* Success */
> +		g15_led->brightness = brightness;
> +		ret = 0;
> +	} else {
> +		hid_err(g15->hdev, "Error setting LED brightness: %d\n", ret);
> +		ret = (ret < 0) ? ret : -EIO;
> +	}
> +
> +	mutex_unlock(&g15->mutex);
> +
> +	return ret;
> +}
> +
> +/*
> + * The G710+ has separate physical keys for cycling the main keyboard backlight
> + * and the WASD backlight. The firmware handles the actual brightness change
> + * internally, but GET_REPORT always returns the power-on defaults regardless
> + * of any changes. So we must track the brightness in our cache and cycle it
> + * ourselves when a hardware brightness key press is detected.
> + *
> + * The firmware cycles brightness DOWN: 4 → 3 → 2 → 1 → 0 → 4 (in wire
> + * format where 0 = brightest, 4 = off). In user-facing terms (inverted):
> + * 4 → 3 → 2 → 1 → 0 → 4.
> + *
> + * The game mode toggle is also handled here: the firmware toggles game mode
> + * internally and updates the LED, so we read back the actual state via
> + * GET_REPORT and notify userspace of the change.
> + */
> +static void lg_g710_leds_changed_work(struct work_struct *work)
> +{
> +	struct lg_g15_data *g15 = container_of(work, struct lg_g15_data, work);
> +	enum led_brightness brightness[LG_G15_BRIGHTNESS_MAX];
> +	bool changed[LG_G15_BRIGHTNESS_MAX] = {};
> +	bool gamemode_changed;

This can be simplified a bit, drop the brightness[] array here as 
well as the gamemode_changed bool and make the changed array
LG_G15_LED_MAX entries large.


> +	int i;
> +
> +	mutex_lock(&g15->mutex);
> +	for (i = 0; i < LG_G15_BRIGHTNESS_MAX; i++) {
> +		if (!test_and_clear_bit(i, &g15->brightness_changed))
> +			continue;
> +
> +		changed[i] = true;
> +
> +		if (g15->leds[i].brightness > 0)
> +			g15->leds[i].brightness--;
> +		else
> +			g15->leds[i].brightness =
> +				g15->leds[i].cdev.max_brightness;
> +
> +		brightness[i] = g15->leds[i].brightness;

No need to store in the brightness[i] array you can simply use
g15->leds[i].brightness below.

> +	}
> +
> +	gamemode_changed = test_and_clear_bit(LG_G15_GAMEMODE,
> +					      &g15->brightness_changed);

Use changed[LG_G15_GAMEMODE] here instead of the gamemode_changed bool,
replacing it twice both in the assignment and in the if below.

> +	if (gamemode_changed)
> +		lg_g710_update_game_led_brightness(g15);
> +	mutex_unlock(&g15->mutex);

move this mutex_unlock to below the below for loop.

> +
> +	for (i = 0; i < LG_G15_BRIGHTNESS_MAX; i++) {

and make this loop condition "i < LG_G15_LED_MAX"

> +		if (!changed[i])
> +			continue;
> +
> +		led_classdev_notify_brightness_hw_changed(&g15->leds[i].cdev,
> +							  brightness[i]);

use g15->leds[i].brightness here instead of brightness[i]

> +	}

mutex_unlock() ends up here.



> +
> +	if (gamemode_changed)
> +		led_classdev_notify_brightness_hw_changed(
> +			&g15->leds[LG_G15_GAMEMODE].cdev,
> +			g15->leds[LG_G15_GAMEMODE].brightness);

and this bit can be dropped since this is handled by the loop now :)


> +}
> +
>  /******** Generic LED functions ********/
>  static int lg_g15_get_initial_led_brightness(struct lg_g15_data *g15)
>  {
> @@ -619,6 +861,16 @@ static int lg_g15_get_initial_led_brightness(struct lg_g15_data *g15)
>  			return ret;
>  
>  		return lg_g510_update_mkey_led_brightness(g15);
> +	case LG_G710:
> +		ret = lg_g710_update_game_led_brightness(g15);
> +		if (ret)
> +			return ret;
> +
> +		ret = lg_g710_update_mkey_led_brightness(g15);
> +		if (ret)
> +			return ret;
> +
> +		return lg_g710_update_kbd_led_brightness(g15);
>  	case LG_Z10:
>  		/*
>  		 * Getting the LCD backlight brightness is not supported.
> @@ -890,6 +1142,74 @@ static int lg_g510_leds_event(struct lg_g15_data *g15, u8 *data)
>  	return 0;
>  }
>  
> +static int lg_g710_event(struct lg_g15_data *g15, u8 *data, int size)
> +{
> +	/*
> +	 * Bits 0-5:  G1-G6 keys
> +	 * Bits 6-8:  M1-M3 keys
> +	 * Bit 9:     MR key
> +	 * Bit 10:    WASD backlight cycle (handled as hw brightness change)
> +	 * Bit 11:    Kbd backlight cycle (handled as hw brightness change)
> +	 * Bit 12:    Game mode toggle (LED state change, handled by firmware)
> +	 */
> +	static const u16 keymap[] = {
> +		KEY_MACRO1,
> +		KEY_MACRO2,
> +		KEY_MACRO3,
> +		KEY_MACRO4,
> +		KEY_MACRO5,
> +		KEY_MACRO6,
> +		KEY_MACRO_PRESET1,
> +		KEY_MACRO_PRESET2,
> +		KEY_MACRO_PRESET3,
> +		KEY_MACRO_RECORD_START,
> +		0,	/* WASD illumination cycle - not a key event */
> +		0,	/* Kbd illumination cycle - not a key event */
> +		0,	/* Game mode toggle */
> +	};
> +	u16 pressed_keys, changed;
> +	int i;
> +
> +	if (size != 4 || data[0] != 3)
> +		return 1;
> +
> +	pressed_keys = (data[1] & 0x3f) | ((data[2] & 0xf0) << 2) |
> +		       ((data[3] & 0x7) << 10);
> +	changed = pressed_keys ^ g15->pressed_keys;
> +
> +	for (i = 0; i < ARRAY_SIZE(keymap); i++) {
> +		if (keymap[i] && (changed & BIT(i)))
> +			input_report_key(g15->input, keymap[i],
> +					 pressed_keys & BIT(i));
> +	}
> +	input_sync(g15->input);

There is no need for the changed thing / check here and thus also
no need to remember the previously pressed keys. The input subsystem
is smart enough to not send out events when keys are unchanged from
the previous input_sync() call.

> +
> +	/*
> +	 * Detect brightness key presses (0->1 transition) and schedule
> +	 * the work function to cycle cached brightness and notify userspace.
> +	 * Bit 10 = WASD backlight (maps to LG_G15_LCD_BRIGHTNESS slot).
> +	 * Bit 11 = Kbd backlight (maps to LG_G15_KBD_BRIGHTNESS slot).
> +	 */
> +	if ((changed & BIT(10)) && (pressed_keys & BIT(10))) {
> +		set_bit(LG_G15_LCD_BRIGHTNESS, &g15->brightness_changed);
> +		schedule_work(&g15->work);
> +	}
> +	if ((changed & BIT(11)) && (pressed_keys & BIT(11))) {
> +		set_bit(LG_G15_KBD_BRIGHTNESS, &g15->brightness_changed);
> +		schedule_work(&g15->work);
> +	}
> +
> +	/* Game mode toggle — bit 12 is a state bit, trigger on any change */
> +	if (changed & BIT(12)) {
> +		set_bit(LG_G15_GAMEMODE, &g15->brightness_changed);
> +		schedule_work(&g15->work);
> +	}
> +
> +	g15->pressed_keys = pressed_keys;
> +
> +	return 0;
> +}
> +
>  static int lg_g15_raw_event(struct hid_device *hdev, struct hid_report *report,
>  			    u8 *data, int size)
>  {
> @@ -924,6 +1244,10 @@ static int lg_g15_raw_event(struct hid_device *hdev, struct hid_report *report,
>  		if (data[0] == LG_G510_INPUT_KBD_BACKLIGHT && size == 2)
>  			return lg_g510_leds_event(g15, data);
>  		break;
> +	case LG_G710:
> +		if (data[0] == 0x03 && size == 4)
> +			return lg_g710_event(g15, data, size);
> +		break;
>  	}
>  
>  	return 0;
> @@ -1055,6 +1379,37 @@ static int lg_g15_register_led(struct lg_g15_data *g15, int i, const char *name)
>  			ret = devm_led_classdev_register(&g15->hdev->dev, &g15->leds[i].cdev);
>  		}
>  		break;
> +	case LG_G710:
> +		switch (i) {
> +		case LG_G15_LCD_BRIGHTNESS:
> +			/*
> +			 * The G710+ does not have a separate LCD brightness,
> +			 * but it does have a separate brightness for WASD keys.
> +			 * Do not use the ::kbd_backlight suffix here, UPower
> +			 * only supports one kbd_backlight LED per device.
> +			 */
> +			g15->leds[i].cdev.name = "g15::kbd_zoned_backlight-wasd";
> +			fallthrough;
> +		case LG_G15_KBD_BRIGHTNESS:
> +			g15->leds[i].cdev.brightness_set_blocking =
> +				lg_g710_kbd_led_set;
> +			g15->leds[i].cdev.brightness_get =
> +				lg_g710_led_get;
> +			g15->leds[i].cdev.max_brightness = 4;
> +			g15->leds[i].cdev.flags = LED_BRIGHT_HW_CHANGED;
> +			break;
> +		default:
> +			if (i != LG_G15_GAMEMODE)
> +				g15->leds[i].cdev.brightness_set_blocking =
> +					lg_g710_mkey_led_set;
> +			g15->leds[i].cdev.brightness_get =
> +				lg_g710_led_get;
> +			g15->leds[i].cdev.max_brightness = 1;
> +			if (i == LG_G15_GAMEMODE)
> +				g15->leds[i].cdev.flags = LED_BRIGHT_HW_CHANGED;
> +		}
> +		ret = devm_led_classdev_register(&g15->hdev->dev, &g15->leds[i].cdev);
> +		break;
>  	}
>  
>  	return ret;
> @@ -1079,13 +1434,16 @@ static void lg_g15_init_input_dev_core(struct hid_device *hdev, struct input_dev
>  static void lg_g15_init_input_dev(struct hid_device *hdev, struct input_dev *input,
>  				  const char *name)
>  {
> +	struct lg_g15_data *g15 = hid_get_drvdata(hdev);
>  	int i;
>  
>  	lg_g15_init_input_dev_core(hdev, input, name);
>  
> -	/* Keys below the LCD, intended for controlling a menu on the LCD */
> -	for (i = 0; i < 5; i++)
> -		input_set_capability(input, EV_KEY, KEY_KBD_LCD_MENU1 + i);
> +	if (g15->model != LG_G710) {
> +		/* Keys below the LCD, intended for controlling a menu on the LCD */
> +		for (i = 0; i < 5; i++)
> +			input_set_capability(input, EV_KEY, KEY_KBD_LCD_MENU1 + i);
> +	}

Maybe instead add the following above the for-loop and
leave the for-loop as is? :

	if (g15->model == LG_G710)
		return;

	

>  }
>  
>  static void lg_g13_init_input_dev(struct hid_device *hdev,
> @@ -1119,6 +1477,7 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
>  		"g15::macro_preset2",
>  		"g15::macro_preset3",
>  		"g15::macro_record",
> +		"g15::gamemode",
>  	};
>  	u8 gkeys_settings_output_report = 0;
>  	u8 gkeys_settings_feature_report = 0;
> @@ -1137,8 +1496,8 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
>  		return ret;
>  
>  	/*
> -	 * Some models have multiple interfaces, we want the interface with
> -	 * the f000.0000 application input report.
> +	 * Some models have multiple interfaces, we want the interface
> +	 * with the ff00.0000 application input report.
>  	 */
>  	rep_enum = &hdev->report_enum[HID_INPUT_REPORT];
>  	list_for_each_entry(rep, &rep_enum->report_list, list) {
> @@ -1212,6 +1571,13 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
>  	case LG_Z10:
>  		connect_mask = HID_CONNECT_HIDRAW;
>  		break;
> +	case LG_G710:
> +		INIT_WORK(&g15->work, lg_g710_leds_changed_work);
> +		hdev->quirks |= HID_QUIRK_NOGET;
> +		connect_mask = HID_CONNECT_DEFAULT;
> +		gkeys_settings_feature_report = LG_G710_FEATURE_EXTRA_KEYS;
> +		gkeys = 6;
> +		break;
>  	}
>  
>  	ret = hid_hw_start(hdev, connect_mask);
> @@ -1234,11 +1600,14 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
>  	}
>  
>  	if (gkeys_settings_feature_report) {
> +		int report_size = ((g15->model == LG_G710) ?
> +				   gkeys * 2 : gkeys) + 1;
> +

The [0] byte in the transfer buffer is the report-number,
so this should be:

		int report_size = (g15->model == LG_G710) ? gkeys * 2 : gkeys;

>  		g15->transfer_buf[0] = gkeys_settings_feature_report;
> -		memset(g15->transfer_buf + 1, 0, gkeys);
> +		memset(g15->transfer_buf + 1, 0, report_size - 1);

and drop the - 1 here.

>  		ret = hid_hw_raw_request(g15->hdev,
>  				gkeys_settings_feature_report,
> -				g15->transfer_buf, gkeys + 1,
> +				g15->transfer_buf, report_size,

and re-add / keep the + 1 here.

>  				HID_FEATURE_REPORT, HID_REQ_SET_REPORT);
>  	}
>  
> @@ -1327,7 +1696,7 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
>  		goto error_hw_stop;
>  
>  	/* Register LED devices */
> -	for (i = 0; i < LG_G15_LED_MAX; i++) {
> +	for (i = 0; i < LG_G15_LED_MAX - (g15->model != LG_G710); i++) {
>  		ret = lg_g15_register_led(g15, i, led_names[i]);
>  		if (ret)
>  			goto error_hw_stop;

The " - (g15->model != LG_G710)" you add here is a bit convoluted.

I would prefer if you change the for condition to:
"i <= LG_G15_MACRO_RECORD" like you've done in other places and then below
the loop add:

	if (g15->model == LG_G710) {
		ret = lg_g15_register_led(g15, LG_G15_GAMEMODE, "g15::gamemode");
		if (ret)
			goto error_hw_stop;
	}

You can then also drop the adding of "g15::gamemode" to led_names[].

This is somewhat more code, but a lot easier to parse / read so IMHO
this is better in the long run keeping readability of the code in mind.

Regards,

Hans


> @@ -1366,6 +1735,10 @@ static const struct hid_device_id lg_g15_devices[] = {
>  	{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
>  			 USB_DEVICE_ID_LOGITECH_G510_USB_AUDIO),
>  		.driver_data = LG_G510_USB_AUDIO },
> +	/* G710 or G710+ */
> +	{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
> +			 USB_DEVICE_ID_LOGITECH_G710),
> +		.driver_data = LG_G710 },
>  	/* Z-10 speakers */
>  	{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
>  			 USB_DEVICE_ID_LOGITECH_Z_10_SPK),


^ permalink raw reply

* Re: [PATCH 1/2] input: pc110pad: change PCI check to get rid of orphaned no_pci_devices
From: Dmitry Torokhov @ 2026-04-06 19:02 UTC (permalink / raw)
  To: Bjorn Helgaas
  Cc: Heiner Kallweit, Bjorn Helgaas, open list:HID CORE LAYER,
	linux-pci@vger.kernel.org
In-Reply-To: <20260406172324.GA170281@bhelgaas>

On Mon, Apr 06, 2026 at 12:23:24PM -0500, Bjorn Helgaas wrote:
> On Fri, Apr 03, 2026 at 12:17:35AM +0200, Heiner Kallweit wrote:
> > As a prerequisite for removing no_pci_devices(), replace its usage here
> > with an equivalent check for presence of a PCI bus.
> > 
> > Signed-off-by: Heiner Kallweit <hkallweit1@gmail.com>
> > ---
> >  drivers/input/mouse/pc110pad.c | 2 +-
> >  1 file changed, 1 insertion(+), 1 deletion(-)
> > 
> > diff --git a/drivers/input/mouse/pc110pad.c b/drivers/input/mouse/pc110pad.c
> > index efa58049f..c7a6df210 100644
> > --- a/drivers/input/mouse/pc110pad.c
> > +++ b/drivers/input/mouse/pc110pad.c
> > @@ -91,7 +91,7 @@ static int __init pc110pad_init(void)
> >  {
> >  	int err;
> >  
> > -	if (!no_pci_devices())
> > +	if (pci_find_next_bus(NULL))
> >  		return -ENODEV;
> 
> I'd certainly love to get rid of no_pci_devices(), although using
> pci_find_next_bus() is not really much better.
> 
> I don't have much opinion about pc110pad.c change.  It's essentially
> still the same kludge, which has been there at least since the
> beginning of git history.
> 
> Dmitry, if you want to ack this, I can take both together.

Given "x86/cpu: Remove M486/M486SX/ELAN support" I'd just drop the
driver completely.

I tried it in 2024 but Maciej was insistent that keeping these old
drivers doe snot hurt. Now I think we can get rid of them.

You can grab the patch removing it from
https://lore.kernel.org/all/20240808172733.1194442-4-dmitry.torokhov@gmail.com/
as I think it should still apply.

Thanks.

-- 
Dmitry

^ permalink raw reply

* Re: [PATCH] Input: gpio-keys - add hibernation support
From: Armando De Leon @ 2026-04-06 20:39 UTC (permalink / raw)
  To: dmitry.torokhov; +Cc: Armando De Leon, linux-input, linux-kernel
In-Reply-To: <adPdc2Z1SYxvqDmP@google.com>

Hi Dmitry,

Thanks for the guidance. I investigated further:

The upstream GICv3 driver (irq-gic-v3.c) has no suspend/resume or
syscore_ops for the distributor - it does not save or restore any
per-IRQ configuration across power cycles. On platforms where the
GIC is re-initialized by firmware during hibernate restore, all
trigger type configurations set by consumer drivers during probe()
are lost.

The IRQ core does preserve the trigger type in irq_data
(irqd_get_trigger_type), but resume_irq() in kernel/irq/pm.c only
re-enables the interrupt without re-applying the trigger type to
hardware.

A fix in the IRQ PM resume path (having resume_irq() call
irq_set_irq_type() using the saved trigger type) would fix all
consumer drivers generically. However, such a change would have
broad consequences and impact across all platforms and interrupt
controllers, and would need very careful review and testing.

Given that the gpio-keys fix is a single irq_set_irq_type() call
in the .restore callback - minimal, self-contained, and low risk -
would it be acceptable to handle it at the driver level for now?
This avoids the risk of a sweeping IRQ core change while solving
the immediate problem for gpio-keys users with hibernation support.

Thanks,
Armando

^ permalink raw reply

* Re: [PATCH] Input: gf2k: clamp hat values to the lookup table
From: Pengpeng Hou @ 2026-04-07  3:30 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, linux-kernel, kees, pengpeng
In-Reply-To: <20260323074541.93413-1-pengpeng@iscas.ac.cn>

Hi Dmitry,

Thanks, that makes sense.

I have updated this to skip reporting hat axes when the decoded value
falls outside the lookup table instead of clamping it to the neutral
entry, and will resend it that way.

Thanks,
Pengpeng



^ permalink raw reply

* [PATCH v2] Input: gf2k: skip invalid hat lookup values
From: Pengpeng Hou @ 2026-04-07  1:56 UTC (permalink / raw)
  To: Dmitry Torokhov; +Cc: linux-input, linux-kernel, kees, pengpeng
In-Reply-To: <20260323074541.93413-1-pengpeng@iscas.ac.cn>

gf2k_read() decodes the hat position from a 4-bit field and uses it
directly to index gf2k_hat_to_axis[]. The lookup table only has nine
entries, so malformed packets can read past the end of the fixed table.

Skip hat reporting when the decoded value falls outside the lookup
table instead of forcing it to the neutral position. This keeps the
fix local and avoids reporting a made-up axis state for malformed
packets.

Signed-off-by: Pengpeng Hou <pengpeng@iscas.ac.cn>
---
Changes since v1:
- skip reporting invalid hat values instead of clamping them to the
  neutral entry

drivers/input/joystick/gf2k.c | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/drivers/input/joystick/gf2k.c b/drivers/input/joystick/gf2k.c
index 5a1cdce0bc48..1d843115d674 100644
--- a/drivers/input/joystick/gf2k.c
+++ b/drivers/input/joystick/gf2k.c
@@ -165,8 +165,10 @@ static void gf2k_read(struct gf2k *gf2k, unsigned char *data)
 
 	t = GB(40,4,0);
 
-	for (i = 0; i < gf2k_hats[gf2k->id]; i++)
-		input_report_abs(dev, ABS_HAT0X + i, gf2k_hat_to_axis[t][i]);
+	if (t < ARRAY_SIZE(gf2k_hat_to_axis))
+		for (i = 0; i < gf2k_hats[gf2k->id]; i++)
+			input_report_abs(dev, ABS_HAT0X + i,
+					 gf2k_hat_to_axis[t][i]);
 
 	t = GB(44,2,0) | GB(32,8,2) | GB(78,2,10);
 
-- 
2.50.1 (Apple Git-155)


^ permalink raw reply related

* [PATCH v2 0/5] Add OneXPlayer Configuration HID Driver
From: Derek J. Clark @ 2026-04-07  4:13 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
	linux-input, linux-doc, linux-kernel

Adds an HID driver for OneXPlayer HID configuration devices. There are
currently 2 generations of OneXPlayer HID protocol. The first (OneXPlayer
F1 series) only provides an RGB control interface over HID. The Second
(X1 mini series, G1 series, AOKZOE A1X) also includes a hardware level
button mapping interface, vibration intensity settings, and the ability
to switch output between xinput and a debug mode that can be used to debug
the button mapping. Some devices (G1 Series, APEX) use a hybrid of Gen1
RGB control and Gen 2 controller settings. To ensure there is no conflicts
when the driver is loaded, we skip creating the RGB interface for Gen 2
devices if there is a DMI match.

I'll also add a note that Gen 1 devices also have an interface for
setting the key map and debug mode, but that is done entirely over a
serial TTY device so it is not able to be added to this driver. There
are also some "Gen 0" devices (OneXPlayer 2 Series) also use it, but
the TTY interface also handles the RGB control so no support is
provided by this driver for those devices.

Signed-off-by: Derel J. Clark <derekjohn.clark@gmail.com>
---
v2:
  - Add DMI quirks for certain devices that ship with both GEN1 and GEN2
    MCU to avoid clashing when initializing the RGB interface.
  - Add left & right vibration intensity attributes.
  - Add additional mappings for keyboard inputs.
  - Add a delayed work trigger to re-apply settings after the MCU
    completes initializing after a suspend/resume cycle.
v1: https://lore.kernel.org/linux-input/20260322031615.1524307-1-derekjohn.clark@gmail.com/
Derek J. Clark (5):
  HID: hid-oxp: Add OneXPlayer configuration driver
  HID: hid-oxp: Add Second Generation RGB Control
  HID: hid-oxp: Add Second Generation Gamepad Mode Switch
  HID: hid-oxp: Add Button Mapping Interface
  HID: hid-oxp: Add Virbation Intenstity Attributes

 MAINTAINERS           |    6 +
 drivers/hid/Kconfig   |   13 +
 drivers/hid/Makefile  |    1 +
 drivers/hid/hid-ids.h |    6 +
 drivers/hid/hid-oxp.c | 1595 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 1621 insertions(+)
 create mode 100644 drivers/hid/hid-oxp.c

-- 
2.53.0


^ permalink raw reply

* [PATCH v2 1/5] HID: hid-oxp: Add OneXPlayer configuration driver
From: Derek J. Clark @ 2026-04-07  4:13 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260407041354.2283201-1-derekjohn.clark@gmail.com>

Adds OneXPlayer HID configuration driver. In this initial driver patch,
add the RGB interface for the first generation of HID based RGB control.

This interface provides the following attributes:
- brightness: provided by the LED core, this works in a fairly unique
  way on this device. The hardware accepts 5 brightness values (0-4),
  which affects the brightness of the multicolor and animated effects
  built into the MCU firmware. For monocolor settings, the device
  expects the hardware brightness value to be pushed to maximum, then we
  apply brightness adjustments mathematically based on % (0-100). This
  leads to some odd conversion as we need the brightness slider to reach
  the full range, but it has no affect when incrementing between the
  division points for other effects.
- multi-intensity: provided by the LED core for red, green, and blue.
- effect: Allows the MCU to set 19 individual effects.
- effect_index: Lists the 19 valid effect names for the interface.
- enabled: Allows the MCU to toggle the RGB interface on/off.
- enabled_index: Lists the valid states for enabled.
- speed: Allows the MCU to set the animation rate for the various
  effects.
- speed_range: Lists the valid range of speed (0-9).

The MCU also has a few odd quirks that make sending multiple synchronous
events challenging. It will essentially freeze if it receives another
message before it has finished processing the last command. It also will
not reply if you wait on it using a completion. To get around this, we
do a 200ms sleep inside a work queue thread and debounce all but the most
recent message using a 50ms mod_delayed_work. This will cache the last
write, queue the work, then return so userspace can release its write
thread. The work queue is only used for brightness/multi-intensity as
that is the path likely to receive rapid successive writes.

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
 MAINTAINERS           |   6 +
 drivers/hid/Kconfig   |  12 +
 drivers/hid/Makefile  |   1 +
 drivers/hid/hid-ids.h |   3 +
 drivers/hid/hid-oxp.c | 651 ++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 673 insertions(+)
 create mode 100644 drivers/hid/hid-oxp.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 6f6517bf4f97..dae814192fa4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19707,6 +19707,12 @@ S:	Maintained
 F:	drivers/mtd/nand/onenand/
 F:	include/linux/mtd/onenand*.h
 
+ONEXPLAYER HID DRIVER
+M:	Derek J. Clark <derekjohn.clark@gmail.com>
+L:	linux-input@vger.kernel.org
+S:	Maintained
+F:	drivers/hid/hid-oxp.c
+
 ONEXPLAYER PLATFORM EC DRIVER
 M:	Antheas Kapenekakis <lkml@antheas.dev>
 M:	Derek John Clark <derekjohn.clark@gmail.com>
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 3c034cd32fa8..2deaec9f467d 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -919,6 +919,18 @@ config HID_ORTEK
 	   - Ortek WKB-2000
 	   - Skycable wireless presenter
 
+config HID_OXP
+	tristate "OneXPlayer handheld controller configuration support"
+	depends on USB_HID
+	depends on LEDS_CLASS
+	depends on LEDS_CLASS_MULTICOLOR
+	help
+	  Say Y here if you would like to enable support for OneXPlayer handheld
+	  devices that come with RGB LED rings around the joysticks and macro buttons.
+
+	  To compile this driver as a module, choose M here: the module will
+	  be called hid-oxp.
+
 config HID_PANTHERLORD
 	tristate "Pantherlord/GreenAsia game controller"
 	help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 03ef72ec4499..bda8a24c9257 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -99,6 +99,7 @@ obj-$(CONFIG_HID_NTI)			+= hid-nti.o
 obj-$(CONFIG_HID_NTRIG)		+= hid-ntrig.o
 obj-$(CONFIG_HID_NVIDIA_SHIELD)	+= hid-nvidia-shield.o
 obj-$(CONFIG_HID_ORTEK)		+= hid-ortek.o
+obj-$(CONFIG_HID_OXP)		+= hid-oxp.o
 obj-$(CONFIG_HID_PRODIKEYS)	+= hid-prodikeys.o
 obj-$(CONFIG_HID_PANTHERLORD)	+= hid-pl.o
 obj-$(CONFIG_HID_PENMOUNT)	+= hid-penmount.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 5bad81222c6e..dcc5a3a70eaf 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1131,6 +1131,9 @@
 #define USB_VENDOR_ID_NVIDIA				0x0955
 #define USB_DEVICE_ID_NVIDIA_THUNDERSTRIKE_CONTROLLER	0x7214
 
+#define USB_VENDOR_ID_CRSC			0x1a2c
+#define USB_DEVICE_ID_ONEXPLAYER_GEN1		0xb001
+
 #define USB_VENDOR_ID_ONTRAK		0x0a07
 #define USB_DEVICE_ID_ONTRAK_ADU100	0x0064
 
diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
new file mode 100644
index 000000000000..c4219ecd8d71
--- /dev/null
+++ b/drivers/hid/hid-oxp.c
@@ -0,0 +1,651 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *  HID driver for OneXPlayer gamepad configuration devices.
+ *
+ *  Copyright (c) 2026 Valve Corporation
+ */
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/kstrtox.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/mutex.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define OXP_PACKET_SIZE 64
+
+#define GEN1_MESSAGE_ID	0xff
+
+#define GEN1_USAGE_PAGE	0xff01
+
+enum oxp_function_index {
+	OXP_FID_GEN1_RGB_SET =		0x07,
+	OXP_FID_GEN1_RGB_REPLY =	0x0f,
+};
+
+static struct oxp_hid_cfg {
+	struct led_classdev_mc *led_mc;
+	struct hid_device *hdev;
+	struct mutex cfg_mutex; /*ensure single synchronous output report*/
+	u8 rgb_brightness;
+	u8 rgb_effect;
+	u8 rgb_speed;
+	u8 rgb_en;
+} drvdata;
+
+enum oxp_feature_en_index {
+	OXP_FEAT_DISABLED,
+	OXP_FEAT_ENABLED,
+};
+
+static const char *const oxp_feature_en_text[] = {
+	[OXP_FEAT_DISABLED] = "false",
+	[OXP_FEAT_ENABLED] = "true",
+};
+
+enum oxp_rgb_effect_index {
+	OXP_UNKNOWN,
+	OXP_EFFECT_AURORA,
+	OXP_EFFECT_BIRTHDAY,
+	OXP_EFFECT_FLOWING,
+	OXP_EFFECT_CHROMA_1,
+	OXP_EFFECT_NEON,
+	OXP_EFFECT_CHROMA_2,
+	OXP_EFFECT_DREAMY,
+	OXP_EFFECT_WARM,
+	OXP_EFFECT_CYBERPUNK,
+	OXP_EFFECT_SEA,
+	OXP_EFFECT_SUNSET,
+	OXP_EFFECT_COLORFUL,
+	OXP_EFFECT_MONSTER,
+	OXP_EFFECT_GREEN,
+	OXP_EFFECT_BLUE,
+	OXP_EFFECT_YELLOW,
+	OXP_EFFECT_TEAL,
+	OXP_EFFECT_PURPLE,
+	OXP_EFFECT_FOGGY,
+	OXP_EFFECT_MONO_LIST, /* placeholder for effect_index_show */
+};
+
+/* These belong to rgb_effect_index, but we want to hide them from
+ * rgb_effect_text
+ */
+
+#define OXP_GET_PROPERTY 0xfc
+#define OXP_SET_PROPERTY 0xfd
+#define OXP_EFFECT_MONO_TRUE 0xfe /* actual index for monocolor */
+
+static const char *const oxp_rgb_effect_text[] = {
+	[OXP_UNKNOWN] = "unknown",
+	[OXP_EFFECT_AURORA] = "aurora",
+	[OXP_EFFECT_BIRTHDAY] = "birthday_cake",
+	[OXP_EFFECT_FLOWING] = "flowing_light",
+	[OXP_EFFECT_CHROMA_1] = "chroma_popping",
+	[OXP_EFFECT_NEON] = "neon",
+	[OXP_EFFECT_CHROMA_2] = "chroma_breathing",
+	[OXP_EFFECT_DREAMY] = "dreamy",
+	[OXP_EFFECT_WARM] = "warm_sun",
+	[OXP_EFFECT_CYBERPUNK] = "cyberpunk",
+	[OXP_EFFECT_SEA] = "sea_foam",
+	[OXP_EFFECT_SUNSET] = "sunset_afterglow",
+	[OXP_EFFECT_COLORFUL] = "colorful",
+	[OXP_EFFECT_MONSTER] = "monster_woke",
+	[OXP_EFFECT_GREEN] = "green_breathing",
+	[OXP_EFFECT_BLUE] = "blue_breathing",
+	[OXP_EFFECT_YELLOW] = "yellow_breathing",
+	[OXP_EFFECT_TEAL] = "teal_breathing",
+	[OXP_EFFECT_PURPLE] = "purple_breathing",
+	[OXP_EFFECT_FOGGY] = "foggy_haze",
+	[OXP_EFFECT_MONO_LIST] = "monocolor",
+};
+
+struct oxp_gen_1_rgb_report {
+	u8 report_id;
+	u8 message_id;
+	u8 padding_2[2];
+	u8 effect;
+	u8 enabled;
+	u8 speed;
+	u8 brightness;
+	u8 red;
+	u8 green;
+	u8 blue;
+} __packed;
+
+static u16 get_usage_page(struct hid_device *hdev)
+{
+	return hdev->collection[0].usage >> 16;
+}
+
+static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
+				   struct hid_report *report, u8 *data,
+				   int size)
+{
+	struct led_classdev_mc *led_mc = drvdata.led_mc;
+	struct oxp_gen_1_rgb_report *rgb_rep;
+
+	if (data[1] != OXP_FID_GEN1_RGB_REPLY)
+		return 0;
+
+	rgb_rep = (struct oxp_gen_1_rgb_report *)data;
+	/* Ensure we save monocolor as the list value */
+	drvdata.rgb_effect = rgb_rep->effect == OXP_EFFECT_MONO_TRUE ?
+			     OXP_EFFECT_MONO_LIST :
+			     rgb_rep->effect;
+	drvdata.rgb_speed = rgb_rep->speed;
+	drvdata.rgb_en = rgb_rep->enabled == 0 ? OXP_FEAT_DISABLED :
+						 OXP_FEAT_ENABLED;
+	drvdata.rgb_brightness = rgb_rep->brightness;
+	led_mc->led_cdev.brightness = rgb_rep->brightness / 4 *
+				      led_mc->led_cdev.max_brightness;
+	/* If monocolor had less than 100% brightness on the previous boot,
+	 * there will be no reliable way to determine the real intensity.
+	 * Since intensity scaling is used with a hardware brightness set at max,
+	 * our brightness will always look like 100%. Use the last set value to
+	 * prevent successive boots from lowering the brightness further.
+	 * Brightness will be "wrong" but the effect will remain the same visually.
+	 */
+	led_mc->subled_info[0].intensity = rgb_rep->red;
+	led_mc->subled_info[1].intensity = rgb_rep->green;
+	led_mc->subled_info[2].intensity = rgb_rep->blue;
+
+	return 0;
+}
+
+static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report,
+			     u8 *data, int size)
+{
+	u16 up = get_usage_page(hdev);
+
+	dev_dbg(&hdev->dev, "raw event data: [%*ph]\n", OXP_PACKET_SIZE, data);
+
+	switch (up) {
+	case GEN1_USAGE_PAGE:
+		return oxp_hid_raw_event_gen_1(hdev, report, data, size);
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+static int mcu_property_out(u8 *header, size_t header_size, u8 *data,
+			    size_t data_size, u8 *footer, size_t footer_size)
+{
+	unsigned char *dmabuf __free(kfree) = kzalloc(OXP_PACKET_SIZE, GFP_KERNEL);
+	int ret;
+
+	if (!dmabuf)
+		return -ENOMEM;
+
+	if (header_size + data_size + footer_size > OXP_PACKET_SIZE)
+		return -EINVAL;
+
+	guard(mutex)(&drvdata.cfg_mutex);
+	memcpy(dmabuf, header, header_size);
+	memcpy(dmabuf + header_size, data, data_size);
+	if (footer_size)
+		memcpy(dmabuf + OXP_PACKET_SIZE - footer_size, footer, footer_size);
+
+	dev_dbg(&drvdata.hdev->dev, "raw data: [%*ph]\n", OXP_PACKET_SIZE, dmabuf);
+
+	ret = hid_hw_output_report(drvdata.hdev, dmabuf, OXP_PACKET_SIZE);
+	if (ret < 0)
+		return ret;
+
+	/* MCU takes 200ms to be ready for another command. */
+	msleep(200);
+	return ret == OXP_PACKET_SIZE ? 0 : -EIO;
+}
+
+static int oxp_gen_1_property_out(enum oxp_function_index fid, u8 *data,
+				  u8 data_size)
+{
+	u8 header[] = { fid, GEN1_MESSAGE_ID };
+	size_t header_size = ARRAY_SIZE(header);
+
+	return mcu_property_out(header, header_size, data, data_size, NULL, 0);
+}
+
+static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
+{
+	u16 up = get_usage_page(drvdata.hdev);
+	u8 *data;
+
+	/* Always default to max brightness and use intensity scaling when in
+	 * monocolor mode.
+	 */
+	switch (up) {
+	case GEN1_USAGE_PAGE:
+		data = (u8[4]) { OXP_SET_PROPERTY, enabled, speed, brightness };
+		if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST)
+			data[3] = 0x04;
+		return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 4);
+	default:
+		return -ENODEV;
+	}
+}
+
+static ssize_t oxp_rgb_status_show(void)
+{
+	u16 up = get_usage_page(drvdata.hdev);
+	u8 *data;
+
+	switch (up) {
+	case GEN1_USAGE_PAGE:
+		data = (u8[1]) { OXP_GET_PROPERTY };
+		return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
+	default:
+		return -ENODEV;
+	}
+}
+
+static int oxp_rgb_color_set(void)
+{
+	u8 max_br = drvdata.led_mc->led_cdev.max_brightness;
+	u8 br = drvdata.led_mc->led_cdev.brightness;
+	u16 up = get_usage_page(drvdata.hdev);
+	u8 green, red, blue;
+	size_t size;
+	u8 *data;
+	int i;
+
+	red = br * drvdata.led_mc->subled_info[0].intensity / max_br;
+	green = br * drvdata.led_mc->subled_info[1].intensity / max_br;
+	blue = br * drvdata.led_mc->subled_info[2].intensity / max_br;
+
+	switch (up) {
+	case GEN1_USAGE_PAGE:
+		size = 55;
+		data = (u8[55]) { OXP_EFFECT_MONO_TRUE };
+
+		for (i = 0; i < (size - 1) / 3; i++) {
+			data[3 * i + 1] = red;
+			data[3 * i + 2] = green;
+			data[3 * i + 3] = blue;
+		}
+		return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, size);
+	default:
+		return -ENODEV;
+	}
+}
+
+static int oxp_rgb_effect_set(u8 effect)
+{
+	u16 up = get_usage_page(drvdata.hdev);
+	u8 *data;
+	int ret;
+
+	switch (effect) {
+	case OXP_EFFECT_AURORA:
+	case OXP_EFFECT_BIRTHDAY:
+	case OXP_EFFECT_FLOWING:
+	case OXP_EFFECT_CHROMA_1:
+	case OXP_EFFECT_NEON:
+	case OXP_EFFECT_CHROMA_2:
+	case OXP_EFFECT_DREAMY:
+	case OXP_EFFECT_WARM:
+	case OXP_EFFECT_CYBERPUNK:
+	case OXP_EFFECT_SEA:
+	case OXP_EFFECT_SUNSET:
+	case OXP_EFFECT_COLORFUL:
+	case OXP_EFFECT_MONSTER:
+	case OXP_EFFECT_GREEN:
+	case OXP_EFFECT_BLUE:
+	case OXP_EFFECT_YELLOW:
+	case OXP_EFFECT_TEAL:
+	case OXP_EFFECT_PURPLE:
+	case OXP_EFFECT_FOGGY:
+		switch (up) {
+		case GEN1_USAGE_PAGE:
+			data = (u8[1]) { effect };
+			ret = oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
+			break;
+		default:
+			ret = -ENODEV;
+		}
+		break;
+	case OXP_EFFECT_MONO_LIST:
+		ret = oxp_rgb_color_set();
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (ret)
+		return ret;
+
+	drvdata.rgb_effect = effect;
+
+	return 0;
+}
+
+static ssize_t enabled_store(struct device *dev, struct device_attribute *attr,
+			     const char *buf, size_t count)
+{
+	int ret;
+	u8 val;
+
+	ret = sysfs_match_string(oxp_feature_en_text, buf);
+	if (ret < 0)
+		return ret;
+	val = ret;
+
+	ret = oxp_rgb_status_store(val, drvdata.rgb_speed,
+				   drvdata.rgb_brightness);
+	if (ret)
+		return ret;
+
+	drvdata.rgb_en = val;
+	return count;
+}
+
+static ssize_t enabled_show(struct device *dev, struct device_attribute *attr,
+			    char *buf)
+{
+	int ret;
+
+	ret = oxp_rgb_status_show();
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_en >= ARRAY_SIZE(oxp_feature_en_text))
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%s\n", oxp_feature_en_text[drvdata.rgb_en]);
+}
+static DEVICE_ATTR_RW(enabled);
+
+static ssize_t enabled_index_show(struct device *dev,
+				  struct device_attribute *attr, char *buf)
+{
+	size_t count = 0;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(oxp_feature_en_text); i++)
+		count += sysfs_emit_at(buf, count, "%s ", oxp_feature_en_text[i]);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(enabled_index);
+
+static ssize_t effect_store(struct device *dev, struct device_attribute *attr,
+			    const char *buf, size_t count)
+{
+	int ret;
+	u8 val;
+
+	ret = sysfs_match_string(oxp_rgb_effect_text, buf);
+	if (ret < 0)
+		return ret;
+
+	val = ret;
+
+	ret = oxp_rgb_status_store(drvdata.rgb_en, drvdata.rgb_speed,
+				   drvdata.rgb_brightness);
+	if (ret)
+		return ret;
+
+	ret = oxp_rgb_effect_set(val);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t effect_show(struct device *dev, struct device_attribute *attr,
+			   char *buf)
+{
+	int ret;
+
+	ret = oxp_rgb_status_show();
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_effect >= ARRAY_SIZE(oxp_rgb_effect_text))
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%s\n", oxp_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)
+{
+	size_t count = 0;
+	unsigned int i;
+
+	for (i = 1; i < ARRAY_SIZE(oxp_rgb_effect_text); i++)
+		count += sysfs_emit_at(buf, count, "%s ", oxp_rgb_effect_text[i]);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(effect_index);
+
+static ssize_t speed_store(struct device *dev, struct device_attribute *attr,
+			   const char *buf, size_t count)
+{
+	int ret;
+	u8 val;
+
+	ret = kstrtou8(buf, 10, &val);
+	if (ret)
+		return ret;
+
+	if (val > 9)
+		return -EINVAL;
+
+	ret = oxp_rgb_status_store(drvdata.rgb_en, val, drvdata.rgb_brightness);
+	if (ret)
+		return ret;
+
+	drvdata.rgb_speed = val;
+	return count;
+}
+
+static ssize_t speed_show(struct device *dev, struct device_attribute *attr,
+			  char *buf)
+{
+	int ret;
+
+	ret = oxp_rgb_status_show();
+	if (ret)
+		return ret;
+
+	if (drvdata.rgb_speed > 9)
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%hhu\n", drvdata.rgb_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-9\n");
+}
+static DEVICE_ATTR_RO(speed_range);
+
+static void oxp_rgb_queue_fn(struct work_struct *work)
+{
+	unsigned int max_brightness = drvdata.led_mc->led_cdev.max_brightness;
+	unsigned int brightness = drvdata.led_mc->led_cdev.brightness;
+	u8 val = 4 * brightness / max_brightness;
+	int ret;
+
+	if (drvdata.rgb_brightness != val) {
+		ret = oxp_rgb_status_store(drvdata.rgb_en, drvdata.rgb_speed, val);
+		if (ret)
+			dev_err(drvdata.led_mc->led_cdev.dev,
+				"Error: Failed to write RGB Status: %i\n", ret);
+
+		drvdata.rgb_brightness = val;
+	}
+
+	if (drvdata.rgb_effect != OXP_EFFECT_MONO_LIST)
+		return;
+
+	ret = oxp_rgb_effect_set(drvdata.rgb_effect);
+	if (ret)
+		dev_err(drvdata.led_mc->led_cdev.dev, "Error: Failed to write RGB color: %i\n",
+			ret);
+}
+
+static DECLARE_DELAYED_WORK(oxp_rgb_queue, oxp_rgb_queue_fn);
+
+static void oxp_rgb_brightness_set(struct led_classdev *led_cdev,
+				   enum led_brightness brightness)
+{
+	led_cdev->brightness = brightness;
+	mod_delayed_work(system_wq, &oxp_rgb_queue, msecs_to_jiffies(50));
+}
+
+static struct attribute *oxp_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 oxp_rgb_attr_group = {
+	.attrs = oxp_rgb_attrs,
+};
+
+static struct mc_subled oxp_rgb_subled_info[] = {
+	{
+		.color_index = LED_COLOR_ID_RED,
+		.intensity = 0x24,
+		.channel = 0x1,
+	},
+	{
+		.color_index = LED_COLOR_ID_GREEN,
+		.intensity = 0x22,
+		.channel = 0x2,
+	},
+	{
+		.color_index = LED_COLOR_ID_BLUE,
+		.intensity = 0x99,
+		.channel = 0x3,
+	},
+};
+
+static struct led_classdev_mc oxp_cdev_rgb = {
+	.led_cdev = {
+		.name = "oxp:rgb:joystick_rings",
+		.color = LED_COLOR_ID_RGB,
+		.brightness = 0x64,
+		.max_brightness = 0x64,
+		.brightness_set = oxp_rgb_brightness_set,
+	},
+	.num_colors = ARRAY_SIZE(oxp_rgb_subled_info),
+	.subled_info = oxp_rgb_subled_info,
+};
+
+static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
+{
+	int ret;
+
+	hid_set_drvdata(hdev, &drvdata);
+	mutex_init(&drvdata.cfg_mutex);
+	drvdata.hdev = hdev;
+	drvdata.led_mc = &oxp_cdev_rgb;
+
+	ret = devm_led_classdev_multicolor_register(&hdev->dev, &oxp_cdev_rgb);
+	if (ret)
+		return dev_err_probe(&hdev->dev, ret,
+				     "Failed to create RGB device\n");
+
+	ret = devm_device_add_group(drvdata.led_mc->led_cdev.dev,
+				    &oxp_rgb_attr_group);
+	if (ret)
+		return dev_err_probe(drvdata.led_mc->led_cdev.dev, ret,
+				     "Failed to create RGB configuration attributes\n");
+
+	ret = oxp_rgb_status_show();
+	if (ret)
+		dev_warn(drvdata.led_mc->led_cdev.dev,
+			 "Failed to query RGB initial state: %i\n", ret);
+
+	return 0;
+}
+
+static int oxp_hid_probe(struct hid_device *hdev,
+			 const struct hid_device_id *id)
+{
+	int ret;
+	u16 up;
+
+	ret = hid_parse(hdev);
+	if (ret)
+		return dev_err_probe(&hdev->dev, ret, "Failed to parse HID device\n");
+
+	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+	if (ret)
+		return dev_err_probe(&hdev->dev, ret, "Failed to start HID device\n");
+
+	ret = hid_hw_open(hdev);
+	if (ret) {
+		hid_hw_stop(hdev);
+		return dev_err_probe(&hdev->dev, ret, "Failed to open HID device\n");
+	}
+
+	up = get_usage_page(hdev);
+	dev_dbg(&hdev->dev, "Got usage page %04x\n", up);
+
+	switch (up) {
+	case GEN1_USAGE_PAGE:
+		ret = oxp_cfg_probe(hdev, up);
+		if (ret) {
+			hid_hw_close(hdev);
+			hid_hw_stop(hdev);
+		}
+
+		return ret;
+	default:
+		return 0;
+	}
+}
+
+static void oxp_hid_remove(struct hid_device *hdev)
+{
+	hid_hw_close(hdev);
+	hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id oxp_devices[] = {
+	{ HID_USB_DEVICE(USB_VENDOR_ID_CRSC, USB_DEVICE_ID_ONEXPLAYER_GEN1) },
+	{}
+};
+
+MODULE_DEVICE_TABLE(hid, oxp_devices);
+static struct hid_driver hid_oxp = {
+	.name = "hid-oxp",
+	.id_table = oxp_devices,
+	.probe = oxp_hid_probe,
+	.remove = oxp_hid_remove,
+	.raw_event = oxp_hid_raw_event,
+};
+module_hid_driver(hid_oxp);
+
+MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
+MODULE_DESCRIPTION("Driver for OneXPlayer HID Interfaces");
+MODULE_LICENSE("GPL");
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 2/5] HID: hid-oxp: Add Second Generation RGB Control
From: Derek J. Clark @ 2026-04-07  4:13 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260407041354.2283201-1-derekjohn.clark@gmail.com>

Adds support for the second generation of RGB Control for OneXPlayer
devices. The interface mirrors the first generation, with some
differences to how messages are formatted.

Some devices have both a GEN1 MCU for RGB control and a GEN2 MCU for
button mapping. To avoid conflicts, quirk these devices to skip RGB
setup for the GEN2_USAGE_PAGE.

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v2:
  - Add DMI quirks table.
---
 drivers/hid/Kconfig   |   1 +
 drivers/hid/hid-ids.h |   3 +
 drivers/hid/hid-oxp.c | 151 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 155 insertions(+)

diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 2deaec9f467d..b779088b80b6 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -924,6 +924,7 @@ config HID_OXP
 	depends on USB_HID
 	depends on LEDS_CLASS
 	depends on LEDS_CLASS_MULTICOLOR
+	depends on DMI
 	help
 	  Say Y here if you would like to enable support for OneXPlayer handheld
 	  devices that come with RGB LED rings around the joysticks and macro buttons.
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index dcc5a3a70eaf..0d1ff879e959 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1134,6 +1134,9 @@
 #define USB_VENDOR_ID_CRSC			0x1a2c
 #define USB_DEVICE_ID_ONEXPLAYER_GEN1		0xb001
 
+#define USB_VENDOR_ID_WCH			0x1a86
+#define USB_DEVICE_ID_ONEXPLAYER_GEN2		0xfe00
+
 #define USB_VENDOR_ID_ONTRAK		0x0a07
 #define USB_DEVICE_ID_ONTRAK_ADU100	0x0064
 
diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index c4219ecd8d71..25214356163e 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -10,6 +10,7 @@
 #include <linux/delay.h>
 #include <linux/dev_printk.h>
 #include <linux/device.h>
+#include <linux/dmi.h>
 #include <linux/hid.h>
 #include <linux/jiffies.h>
 #include <linux/kstrtox.h>
@@ -24,12 +25,15 @@
 #define OXP_PACKET_SIZE 64
 
 #define GEN1_MESSAGE_ID	0xff
+#define GEN2_MESSAGE_ID	0x3f
 
 #define GEN1_USAGE_PAGE	0xff01
+#define GEN2_USAGE_PAGE	0xff00
 
 enum oxp_function_index {
 	OXP_FID_GEN1_RGB_SET =		0x07,
 	OXP_FID_GEN1_RGB_REPLY =	0x0f,
+	OXP_FID_GEN2_STATUS_EVENT =	0xb8,
 };
 
 static struct oxp_hid_cfg {
@@ -121,6 +125,22 @@ struct oxp_gen_1_rgb_report {
 	u8 blue;
 } __packed;
 
+struct oxp_gen_2_rgb_report {
+	u8 report_id;
+	u8 header_id;
+	u8 padding_2;
+	u8 message_id;
+	u8 padding_4[2];
+	u8 enabled;
+	u8 speed;
+	u8 brightness;
+	u8 red;
+	u8 green;
+	u8 blue;
+	u8 padding_12[3];
+	u8 effect;
+} __packed;
+
 static u16 get_usage_page(struct hid_device *hdev)
 {
 	return hdev->collection[0].usage >> 16;
@@ -161,6 +181,44 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
 	return 0;
 }
 
+static int oxp_hid_raw_event_gen_2(struct hid_device *hdev,
+				   struct hid_report *report, u8 *data,
+				   int size)
+{
+	struct led_classdev_mc *led_mc = drvdata.led_mc;
+	struct oxp_gen_2_rgb_report *rgb_rep;
+
+	if (data[0] != OXP_FID_GEN2_STATUS_EVENT)
+		return 0;
+
+	if (data[3] != OXP_GET_PROPERTY)
+		return 0;
+
+	rgb_rep = (struct oxp_gen_2_rgb_report *)data;
+	/* Ensure we save monocolor as the list value */
+	drvdata.rgb_effect = rgb_rep->effect == OXP_EFFECT_MONO_TRUE ?
+			     OXP_EFFECT_MONO_LIST :
+			     rgb_rep->effect;
+	drvdata.rgb_speed = rgb_rep->speed;
+	drvdata.rgb_en = rgb_rep->enabled == 0 ? OXP_FEAT_DISABLED :
+						 OXP_FEAT_ENABLED;
+	drvdata.rgb_brightness = rgb_rep->brightness;
+	led_mc->led_cdev.brightness = rgb_rep->brightness / 4 *
+				      led_mc->led_cdev.max_brightness;
+	/* If monocolor had less than 100% brightness on the previous boot,
+	 * there will be no reliable way to determine the real intensity.
+	 * Since intensity scaling is used with a hardware brightness set at max,
+	 * our brightness will always look like 100%. Use the last set value to
+	 * prevent successive boots from lowering the brightness further.
+	 * Brightness will be "wrong" but the effect will remain the same visually.
+	 */
+	led_mc->subled_info[0].intensity = rgb_rep->red;
+	led_mc->subled_info[1].intensity = rgb_rep->green;
+	led_mc->subled_info[2].intensity = rgb_rep->blue;
+
+	return 0;
+}
+
 static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report,
 			     u8 *data, int size)
 {
@@ -171,6 +229,8 @@ static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report,
 	switch (up) {
 	case GEN1_USAGE_PAGE:
 		return oxp_hid_raw_event_gen_1(hdev, report, data, size);
+	case GEN2_USAGE_PAGE:
+		return oxp_hid_raw_event_gen_2(hdev, report, data, size);
 	default:
 		break;
 	}
@@ -216,6 +276,18 @@ static int oxp_gen_1_property_out(enum oxp_function_index fid, u8 *data,
 	return mcu_property_out(header, header_size, data, data_size, NULL, 0);
 }
 
+static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data,
+				  u8 data_size)
+{
+	u8 header[] = { fid, GEN2_MESSAGE_ID, 0x01 };
+	u8 footer[] = { GEN2_MESSAGE_ID, fid };
+	size_t header_size = ARRAY_SIZE(header);
+	size_t footer_size = ARRAY_SIZE(footer);
+
+	return mcu_property_out(header, header_size, data, data_size, footer,
+				footer_size);
+}
+
 static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
 {
 	u16 up = get_usage_page(drvdata.hdev);
@@ -230,6 +302,11 @@ static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
 		if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST)
 			data[3] = 0x04;
 		return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 4);
+	case GEN2_USAGE_PAGE:
+		data = (u8[6]) { OXP_SET_PROPERTY, 0x00, 0x02, enabled, speed, brightness };
+		if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST)
+			data[5] = 0x04;
+		return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 6);
 	default:
 		return -ENODEV;
 	}
@@ -244,6 +321,9 @@ static ssize_t oxp_rgb_status_show(void)
 	case GEN1_USAGE_PAGE:
 		data = (u8[1]) { OXP_GET_PROPERTY };
 		return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
+	case GEN2_USAGE_PAGE:
+		data = (u8[3]) { OXP_GET_PROPERTY, 0x00, 0x02 };
+		return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 3);
 	default:
 		return -ENODEV;
 	}
@@ -274,6 +354,16 @@ static int oxp_rgb_color_set(void)
 			data[3 * i + 3] = blue;
 		}
 		return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, size);
+	case GEN2_USAGE_PAGE:
+		size = 57;
+		data = (u8[57]) { OXP_EFFECT_MONO_TRUE, 0x00, 0x02 };
+
+		for (i = 1; i < size / 3; i++) {
+			data[3 * i] = red;
+			data[3 * i + 1] = green;
+			data[3 * i + 2] = blue;
+		}
+		return oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, size);
 	default:
 		return -ENODEV;
 	}
@@ -310,6 +400,10 @@ static int oxp_rgb_effect_set(u8 effect)
 			data = (u8[1]) { effect };
 			ret = oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
 			break;
+		case GEN2_USAGE_PAGE:
+			data = (u8[3]) { effect, 0x00, 0x02 };
+			ret = oxp_gen_2_property_out(OXP_FID_GEN2_STATUS_EVENT, data, 3);
+			break;
 		default:
 			ret = -ENODEV;
 		}
@@ -560,6 +654,56 @@ static struct led_classdev_mc oxp_cdev_rgb = {
 	.subled_info = oxp_rgb_subled_info,
 };
 
+struct quirk_entry {
+	bool hybrid_mcu;
+};
+
+static struct quirk_entry quirk_hybrid_mcu = {
+	.hybrid_mcu = true,
+};
+
+static const struct dmi_system_id oxp_hybrid_mcu_list[] = {
+	{
+		.ident = "OneXPlayer Apex",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER APEX"),
+		},
+		.driver_data = &quirk_hybrid_mcu,
+	},
+	{
+		.ident = "OneXPlayer G1 AMD",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER G1 A"),
+		},
+		.driver_data = &quirk_hybrid_mcu,
+	},
+	{
+		.ident = "OneXPlayer G1 Intel",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "ONE-NETBOOK"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "ONEXPLAYER G1 i"),
+		},
+		.driver_data = &quirk_hybrid_mcu,
+	},
+	{},
+};
+
+static bool oxp_hybrid_mcu_device(void)
+{
+	const struct dmi_system_id *dmi_id;
+	struct quirk_entry *quirks;
+
+	dmi_id = dmi_first_match(oxp_hybrid_mcu_list);
+	if (!dmi_id)
+		return false;
+
+	quirks = dmi_id->driver_data;
+
+	return quirks->hybrid_mcu;
+}
+
 static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 {
 	int ret;
@@ -567,6 +711,10 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 	hid_set_drvdata(hdev, &drvdata);
 	mutex_init(&drvdata.cfg_mutex);
 	drvdata.hdev = hdev;
+
+	if (up == GEN2_USAGE_PAGE && oxp_hybrid_mcu_device())
+		goto skip_rgb;
+
 	drvdata.led_mc = &oxp_cdev_rgb;
 
 	ret = devm_led_classdev_multicolor_register(&hdev->dev, &oxp_cdev_rgb);
@@ -585,6 +733,7 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 		dev_warn(drvdata.led_mc->led_cdev.dev,
 			 "Failed to query RGB initial state: %i\n", ret);
 
+skip_rgb:
 	return 0;
 }
 
@@ -613,6 +762,7 @@ static int oxp_hid_probe(struct hid_device *hdev,
 
 	switch (up) {
 	case GEN1_USAGE_PAGE:
+	case GEN2_USAGE_PAGE:
 		ret = oxp_cfg_probe(hdev, up);
 		if (ret) {
 			hid_hw_close(hdev);
@@ -633,6 +783,7 @@ static void oxp_hid_remove(struct hid_device *hdev)
 
 static const struct hid_device_id oxp_devices[] = {
 	{ HID_USB_DEVICE(USB_VENDOR_ID_CRSC, USB_DEVICE_ID_ONEXPLAYER_GEN1) },
+	{ HID_USB_DEVICE(USB_VENDOR_ID_WCH, USB_DEVICE_ID_ONEXPLAYER_GEN2) },
 	{}
 };
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 3/5] HID: hid-oxp: Add Second Generation Gamepad Mode Switch
From: Derek J. Clark @ 2026-04-07  4:13 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260407041354.2283201-1-derekjohn.clark@gmail.com>

Adds "gamepad_mode" attribute to second generation OneXPlayer
configuration HID devices. This attribute initiates a mode shift in the
device MCU that puts it into a state where all events are routed to an
hidraw interface instead of the xpad evdev interface. This allows for
debugging the hardware input mapping added in the next patch.

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v2:
  - Rename to gamepad_mode & show relevant gamepad modes instead of
    using a debug enable/disable paradigm, to match other drivers.
---
 drivers/hid/hid-oxp.c | 130 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 130 insertions(+)

diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index 25214356163e..c62952537d98 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -33,6 +33,7 @@
 enum oxp_function_index {
 	OXP_FID_GEN1_RGB_SET =		0x07,
 	OXP_FID_GEN1_RGB_REPLY =	0x0f,
+	OXP_FID_GEN2_TOGGLE_MODE =	0xb2,
 	OXP_FID_GEN2_STATUS_EVENT =	0xb8,
 };
 
@@ -41,11 +42,22 @@ static struct oxp_hid_cfg {
 	struct hid_device *hdev;
 	struct mutex cfg_mutex; /*ensure single synchronous output report*/
 	u8 rgb_brightness;
+	u8 gamepad_mode;
 	u8 rgb_effect;
 	u8 rgb_speed;
 	u8 rgb_en;
 } drvdata;
 
+enum oxp_gamepad_mode_index {
+	OXP_GP_MODE_XINPUT = 0x00,
+	OXP_GP_MODE_DEBUG = 0x03,
+};
+
+static const char *const oxp_gamepad_mode_text[] = {
+	[OXP_GP_MODE_XINPUT] = "xinput",
+	[OXP_GP_MODE_DEBUG] = "debug",
+};
+
 enum oxp_feature_en_index {
 	OXP_FEAT_DISABLED,
 	OXP_FEAT_ENABLED,
@@ -181,6 +193,32 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
 	return 0;
 }
 
+static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size);
+
+static void oxp_mcu_init_fn(struct work_struct *work)
+{
+	u8 gp_mode_data[3] = { OXP_GP_MODE_DEBUG, 0x01, 0x02 };
+	int ret;
+
+	/* Cycle the gamepad mode */
+	ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3);
+	if (ret)
+		dev_err(&drvdata.hdev->dev,
+			"Error: Failed to set gamepad mode: %i\n", ret);
+
+	/* Remainder only applies for xinput mode */
+	if (drvdata.gamepad_mode == OXP_GP_MODE_DEBUG)
+		return;
+
+	gp_mode_data[0] = OXP_GP_MODE_XINPUT;
+	ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3);
+	if (ret)
+		dev_err(&drvdata.hdev->dev,
+			"Error: Failed to set gamepad mode: %i\n", ret);
+}
+
+static DECLARE_DELAYED_WORK(oxp_mcu_init, oxp_mcu_init_fn);
+
 static int oxp_hid_raw_event_gen_2(struct hid_device *hdev,
 				   struct hid_report *report, u8 *data,
 				   int size)
@@ -191,6 +229,14 @@ static int oxp_hid_raw_event_gen_2(struct hid_device *hdev,
 	if (data[0] != OXP_FID_GEN2_STATUS_EVENT)
 		return 0;
 
+	/* Sent ~6s after resume event, indicating the MCU has fully reset.
+	 * Re-apply our settings after this has been received.
+	 */
+	if (data[3] == OXP_EFFECT_MONO_TRUE) {
+		mod_delayed_work(system_wq, &oxp_mcu_init, msecs_to_jiffies(50));
+		return 0;
+	}
+
 	if (data[3] != OXP_GET_PROPERTY)
 		return 0;
 
@@ -288,6 +334,77 @@ static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data,
 				footer_size);
 }
 
+static ssize_t gamepad_mode_store(struct device *dev,
+				  struct device_attribute *attr, const char *buf,
+				  size_t count)
+{
+	u16 up = get_usage_page(drvdata.hdev);
+	u8 data[3] = { 0x00, 0x01, 0x02 };
+	int ret = -EINVAL;
+	int i;
+
+	if (up != GEN2_USAGE_PAGE)
+		return ret;
+
+	for (i = 0; i < ARRAY_SIZE(oxp_gamepad_mode_text); i++) {
+		if (oxp_gamepad_mode_text[i] && sysfs_streq(buf, oxp_gamepad_mode_text[i])) {
+			ret = i;
+			break;
+		}
+	}
+	if (ret < 0)
+		return ret;
+
+	data[0] = ret;
+
+	ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, data, 3);
+	if (ret)
+		return ret;
+
+	drvdata.gamepad_mode = data[0];
+
+	return count;
+}
+
+static ssize_t gamepad_mode_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "%s\n", oxp_gamepad_mode_text[drvdata.gamepad_mode]);
+}
+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;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(oxp_gamepad_mode_text); i++) {
+		if (!oxp_gamepad_mode_text[i] ||
+		    oxp_gamepad_mode_text[i][0] == '\0')
+			continue;
+
+		count += sysfs_emit_at(buf, count, "%s ", oxp_gamepad_mode_text[i]);
+	}
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(gamepad_mode_index);
+
+static struct attribute *oxp_cfg_attrs[] = {
+	&dev_attr_gamepad_mode.attr,
+	&dev_attr_gamepad_mode_index.attr,
+	NULL,
+};
+
+static const struct attribute_group oxp_cfg_attrs_group = {
+	.attrs = oxp_cfg_attrs,
+};
+
 static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
 {
 	u16 up = get_usage_page(drvdata.hdev);
@@ -733,7 +850,20 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 		dev_warn(drvdata.led_mc->led_cdev.dev,
 			 "Failed to query RGB initial state: %i\n", ret);
 
+	/* Below features are only implemented in gen 2 */
+	if (up != GEN2_USAGE_PAGE)
+		return 0;
+
 skip_rgb:
+	mod_delayed_work(system_wq, &oxp_mcu_init, msecs_to_jiffies(50));
+
+	drvdata.gamepad_mode = OXP_GP_MODE_XINPUT;
+
+	ret = devm_device_add_group(&hdev->dev, &oxp_cfg_attrs_group);
+	if (ret)
+		return dev_err_probe(&hdev->dev, ret,
+				     "Failed to attach configuration attributes\n");
+
 	return 0;
 }
 
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 4/5] HID: hid-oxp: Add Button Mapping Interface
From: Derek J. Clark @ 2026-04-07  4:13 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260407041354.2283201-1-derekjohn.clark@gmail.com>

Adds button mapping interface for second generation OneXPlayer
configuration HID interfaces. This interface allows the MCU to swap
button mappings at the hardware level. The current state cannot be
retrieved, and the mappings may have been modified in Windows prior, so
we reset the button mapping at init and expose an attribute to allow
userspace to do this again at any time.

The interface requires two pages of button mapping data to be sent
before the settings will take place. Since the MCU requires a 200ms
delay after each message (total 400ms for these attributes) use the same
debounce work queue method we used for RGB. This will allow for
userspace or udev rules to rapidly map all buttons. The values will
be cached before the final write is finally sent to the device.

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v2:
  - Add detection of post-suspend MCU init to trigger setting the button
    map again.
---
 drivers/hid/hid-oxp.c | 565 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 565 insertions(+)

diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index c62952537d98..1100f1f14f35 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -34,10 +34,145 @@ enum oxp_function_index {
 	OXP_FID_GEN1_RGB_SET =		0x07,
 	OXP_FID_GEN1_RGB_REPLY =	0x0f,
 	OXP_FID_GEN2_TOGGLE_MODE =	0xb2,
+	OXP_FID_GEN2_KEY_STATE =	0xb4,
 	OXP_FID_GEN2_STATUS_EVENT =	0xb8,
 };
 
+#define OXP_MAPPING_GAMEPAD	0x01
+#define OXP_MAPPING_KEYBOARD	0x02
+
+struct oxp_button_data {
+	u8 mode;
+	u8 index;
+	u8 key_id;
+	u8 padding[2];
+} __packed;
+
+struct oxp_button_entry {
+	struct oxp_button_data data;
+	const char *name;
+};
+
+static const struct oxp_button_entry oxp_button_table[] = {
+	/* Gamepad Buttons */
+	{ { OXP_MAPPING_GAMEPAD, 0x01 }, "BTN_A" },
+	{ { OXP_MAPPING_GAMEPAD, 0x02 }, "BTN_B" },
+	{ { OXP_MAPPING_GAMEPAD, 0x03 }, "BTN_X" },
+	{ { OXP_MAPPING_GAMEPAD, 0x04 }, "BTN_Y" },
+	{ { OXP_MAPPING_GAMEPAD, 0x05 }, "BTN_LB" },
+	{ { OXP_MAPPING_GAMEPAD, 0x06 }, "BTN_RB" },
+	{ { OXP_MAPPING_GAMEPAD, 0x07 }, "BTN_LT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x08 }, "BTN_RT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x09 }, "BTN_START" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0a }, "BTN_SELECT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0b }, "BTN_L3" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0c }, "BTN_R3" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0d }, "DPAD_UP" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0e }, "DPAD_DOWN" },
+	{ { OXP_MAPPING_GAMEPAD, 0x0f }, "DPAD_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x10 }, "DPAD_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x11 }, "JOY_L_UP" },
+	{ { OXP_MAPPING_GAMEPAD, 0x12 }, "JOY_L_UP_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x13 }, "JOY_L_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x14 }, "JOY_L_DOWN_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x15 }, "JOY_L_DOWN" },
+	{ { OXP_MAPPING_GAMEPAD, 0x16 }, "JOY_L_DOWN_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x17 }, "JOY_L_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x18 }, "JOY_L_UP_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x19 }, "JOY_R_UP" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1a }, "JOY_R_UP_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1b }, "JOY_R_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1c }, "JOY_R_DOWN_RIGHT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1d }, "JOY_R_DOWN" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1e }, "JOY_R_DOWN_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x1f }, "JOY_R_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x20 }, "JOY_R_UP_LEFT" },
+	{ { OXP_MAPPING_GAMEPAD, 0x22 }, "BTN_GUIDE" },
+	/* Keyboard Keys */
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5a }, "KEY_F1" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5b }, "KEY_F2" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5c }, "KEY_F3" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5d }, "KEY_F4" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5e }, "KEY_F5" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x5f }, "KEY_F6" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x60 }, "KEY_F7" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x61 }, "KEY_F8" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x62 }, "KEY_F9" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x63 }, "KEY_F10" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x64 }, "KEY_F11" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x65 }, "KEY_F12" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x66 }, "KEY_F13" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x67 }, "KEY_F14" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x68 }, "KEY_F15" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x69 }, "KEY_F16" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6a }, "KEY_F17" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6b }, "KEY_F18" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6c }, "KEY_F19" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6d }, "KEY_F20" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6e }, "KEY_F21" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x6f }, "KEY_F22" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x70 }, "KEY_F23" },
+	{ { OXP_MAPPING_KEYBOARD, 0x01, 0x71 }, "KEY_F24" },
+};
+
+enum oxp_joybutton_index {
+	BUTTON_A =	0x01,
+	BUTTON_B,
+	BUTTON_X,
+	BUTTON_Y,
+	BUTTON_LB,
+	BUTTON_RB,
+	BUTTON_LT,
+	BUTTON_RT,
+	BUTTON_START,
+	BUTTON_SELECT,
+	BUTTON_L3,
+	BUTTON_R3,
+	BUTTON_DUP,
+	BUTTON_DDOWN,
+	BUTTON_DLEFT,
+	BUTTON_DRIGHT,
+	BUTTON_M1 =	0x22,
+	BUTTON_M2,
+	/* These are unused currently, reserved for future devices */
+	BUTTON_M3,
+	BUTTON_M4,
+	BUTTON_M5,
+	BUTTON_M6,
+};
+
+struct oxp_button_idx {
+	enum oxp_joybutton_index button_idx;
+	u8 mapping_idx;
+} __packed;
+
+struct oxp_bmap_page_1 {
+	struct oxp_button_idx btn_a;
+	struct oxp_button_idx btn_b;
+	struct oxp_button_idx btn_x;
+	struct oxp_button_idx btn_y;
+	struct oxp_button_idx btn_lb;
+	struct oxp_button_idx btn_rb;
+	struct oxp_button_idx btn_lt;
+	struct oxp_button_idx btn_rt;
+	struct oxp_button_idx btn_start;
+} __packed;
+
+struct oxp_bmap_page_2 {
+	struct oxp_button_idx btn_select;
+	struct oxp_button_idx btn_l3;
+	struct oxp_button_idx btn_r3;
+	struct oxp_button_idx btn_dup;
+	struct oxp_button_idx btn_ddown;
+	struct oxp_button_idx btn_dleft;
+	struct oxp_button_idx btn_dright;
+	struct oxp_button_idx btn_m1;
+	struct oxp_button_idx btn_m2;
+} __packed;
+
 static struct oxp_hid_cfg {
+	struct oxp_bmap_page_1 *bmap_1;
+	struct oxp_bmap_page_2 *bmap_2;
 	struct led_classdev_mc *led_mc;
 	struct hid_device *hdev;
 	struct mutex cfg_mutex; /*ensure single synchronous output report*/
@@ -48,6 +183,10 @@ static struct oxp_hid_cfg {
 	u8 rgb_en;
 } drvdata;
 
+#define OXP_FILL_PAGE_SLOT(page, btn)            \
+	{ .button_idx = (page)->btn.button_idx,  \
+	  .mapping_idx = (page)->btn.mapping_idx }
+
 enum oxp_gamepad_mode_index {
 	OXP_GP_MODE_XINPUT = 0x00,
 	OXP_GP_MODE_DEBUG = 0x03,
@@ -153,6 +292,10 @@ struct oxp_gen_2_rgb_report {
 	u8 effect;
 } __packed;
 
+struct oxp_attr {
+	u8 index;
+};
+
 static u16 get_usage_page(struct hid_device *hdev)
 {
 	return hdev->collection[0].usage >> 16;
@@ -194,12 +337,19 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
 }
 
 static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size);
+static int oxp_set_buttons(void);
 
 static void oxp_mcu_init_fn(struct work_struct *work)
 {
 	u8 gp_mode_data[3] = { OXP_GP_MODE_DEBUG, 0x01, 0x02 };
 	int ret;
 
+	/* Re-apply the button mapping */
+	ret = oxp_set_buttons();
+	if (ret)
+		dev_err(&drvdata.hdev->dev,
+			"Error: Failed to set button mapping: %i\n", ret);
+
 	/* Cycle the gamepad mode */
 	ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, gp_mode_data, 3);
 	if (ret)
@@ -395,9 +545,410 @@ static ssize_t gamepad_mode_index_show(struct device *dev,
 }
 static DEVICE_ATTR_RO(gamepad_mode_index);
 
+static void oxp_set_defaults_bmap_1(struct oxp_bmap_page_1 *bmap)
+{
+	bmap->btn_a.button_idx = BUTTON_A;
+	bmap->btn_a.mapping_idx = 0;
+	bmap->btn_b.button_idx = BUTTON_B;
+	bmap->btn_b.mapping_idx = 1;
+	bmap->btn_x.button_idx = BUTTON_X;
+	bmap->btn_x.mapping_idx = 2;
+	bmap->btn_y.button_idx = BUTTON_Y;
+	bmap->btn_y.mapping_idx = 3;
+	bmap->btn_lb.button_idx = BUTTON_LB;
+	bmap->btn_lb.mapping_idx = 4;
+	bmap->btn_rb.button_idx = BUTTON_RB;
+	bmap->btn_rb.mapping_idx = 5;
+	bmap->btn_lt.button_idx = BUTTON_LT;
+	bmap->btn_lt.mapping_idx = 6;
+	bmap->btn_rt.button_idx = BUTTON_RT;
+	bmap->btn_rt.mapping_idx = 7;
+	bmap->btn_start.button_idx = BUTTON_START;
+	bmap->btn_start.mapping_idx = 8;
+}
+
+static void oxp_set_defaults_bmap_2(struct oxp_bmap_page_2 *bmap)
+{
+	bmap->btn_select.button_idx = BUTTON_SELECT;
+	bmap->btn_select.mapping_idx = 9;
+	bmap->btn_l3.button_idx = BUTTON_L3;
+	bmap->btn_l3.mapping_idx = 10;
+	bmap->btn_r3.button_idx = BUTTON_R3;
+	bmap->btn_r3.mapping_idx = 11;
+	bmap->btn_dup.button_idx = BUTTON_DUP;
+	bmap->btn_dup.mapping_idx = 12;
+	bmap->btn_ddown.button_idx = BUTTON_DDOWN;
+	bmap->btn_ddown.mapping_idx = 13;
+	bmap->btn_dleft.button_idx = BUTTON_DLEFT;
+	bmap->btn_dleft.mapping_idx = 14;
+	bmap->btn_dright.button_idx = BUTTON_DRIGHT;
+	bmap->btn_dright.mapping_idx = 15;
+	bmap->btn_m1.button_idx = BUTTON_M1;
+	bmap->btn_m1.mapping_idx = 48; /* KEY_F15 */
+	bmap->btn_m2.button_idx = BUTTON_M2;
+	bmap->btn_m2.mapping_idx = 49; /* KEY_F16 */
+}
+
+static void oxp_page_fill_data(char *buf, const struct oxp_button_idx *buttons,
+			       size_t len)
+{
+	size_t offset_increment = sizeof(u8) + sizeof(struct oxp_button_idx);
+	size_t offset = 5;
+	unsigned int i;
+
+	for (i = 0; i < len; i++, offset += offset_increment) {
+		buf[offset] = (u8)buttons[i].button_idx;
+		memcpy(buf + offset + 1,
+		       &oxp_button_table[buttons[i].mapping_idx].data,
+		       sizeof(struct oxp_button_data));
+	}
+}
+
+static int oxp_set_buttons(void)
+{
+	u8 page_1[59] = { 0x02, 0x38, 0x20, 0x01, 0x01 };
+	u8 page_2[59] = { 0x02, 0x38, 0x20, 0x02, 0x01 };
+	u16 up = get_usage_page(drvdata.hdev);
+	int ret;
+
+	if (up != GEN2_USAGE_PAGE)
+		return -EINVAL;
+
+	const struct oxp_button_idx p1[] = {
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_a),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_b),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_x),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_y),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_lb),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_rb),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_lt),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_rt),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_1, btn_start),
+	};
+
+	const struct oxp_button_idx p2[] = {
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_select),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_l3),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_r3),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dup),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_ddown),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dleft),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_dright),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_m1),
+		OXP_FILL_PAGE_SLOT(drvdata.bmap_2, btn_m2),
+	};
+
+	oxp_page_fill_data(page_1, p1, ARRAY_SIZE(p1));
+	oxp_page_fill_data(page_2, p2, ARRAY_SIZE(p2));
+
+	ret = oxp_gen_2_property_out(OXP_FID_GEN2_KEY_STATE, page_1, ARRAY_SIZE(page_1));
+	if (ret)
+		return ret;
+
+	return oxp_gen_2_property_out(OXP_FID_GEN2_KEY_STATE, page_2, ARRAY_SIZE(page_2));
+}
+
+static int oxp_reset_buttons(void)
+{
+	oxp_set_defaults_bmap_1(drvdata.bmap_1);
+	oxp_set_defaults_bmap_2(drvdata.bmap_2);
+	return oxp_set_buttons();
+}
+
+static ssize_t reset_buttons_store(struct device *dev,
+				   struct device_attribute *attr, const char *buf,
+				   size_t count)
+{
+	int val, ret;
+
+	ret = kstrtoint(buf, 10, &val);
+	if (ret)
+		return ret;
+
+	if (val != 1)
+		return -EINVAL;
+
+	ret = oxp_reset_buttons();
+	if (ret)
+		return ret;
+
+	return count;
+}
+static DEVICE_ATTR_WO(reset_buttons);
+
+static void oxp_btn_queue_fn(struct work_struct *work)
+{
+	int ret;
+
+	ret = oxp_set_buttons();
+	if (ret)
+		dev_err(&drvdata.hdev->dev,
+			"Error: Failed to write button mapping: %i\n", ret);
+}
+
+static DECLARE_DELAYED_WORK(oxp_btn_queue, oxp_btn_queue_fn);
+
+static int oxp_button_idx_from_str(const char *buf)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(oxp_button_table); i++)
+		if (sysfs_streq(buf, oxp_button_table[i].name))
+			return i;
+
+	return -EINVAL;
+}
+
+static ssize_t map_button_store(struct device *dev,
+				struct device_attribute *attr, const char *buf,
+				size_t count, u8 index)
+{
+	int idx;
+
+	idx = oxp_button_idx_from_str(buf);
+	if (idx < 0)
+		return idx;
+
+	switch (index) {
+	case BUTTON_A:
+		drvdata.bmap_1->btn_a.mapping_idx = idx;
+		break;
+	case BUTTON_B:
+		drvdata.bmap_1->btn_b.mapping_idx = idx;
+		break;
+	case BUTTON_X:
+		drvdata.bmap_1->btn_x.mapping_idx = idx;
+		break;
+	case BUTTON_Y:
+		drvdata.bmap_1->btn_y.mapping_idx = idx;
+		break;
+	case BUTTON_LB:
+		drvdata.bmap_1->btn_lb.mapping_idx = idx;
+		break;
+	case BUTTON_RB:
+		drvdata.bmap_1->btn_rb.mapping_idx = idx;
+		break;
+	case BUTTON_LT:
+		drvdata.bmap_1->btn_lt.mapping_idx = idx;
+		break;
+	case BUTTON_RT:
+		drvdata.bmap_1->btn_rt.mapping_idx = idx;
+		break;
+	case BUTTON_START:
+		drvdata.bmap_1->btn_start.mapping_idx = idx;
+		break;
+	case BUTTON_SELECT:
+		drvdata.bmap_2->btn_select.mapping_idx = idx;
+		break;
+	case BUTTON_L3:
+		drvdata.bmap_2->btn_l3.mapping_idx = idx;
+		break;
+	case BUTTON_R3:
+		drvdata.bmap_2->btn_r3.mapping_idx = idx;
+		break;
+	case BUTTON_DUP:
+		drvdata.bmap_2->btn_dup.mapping_idx = idx;
+		break;
+	case BUTTON_DDOWN:
+		drvdata.bmap_2->btn_ddown.mapping_idx = idx;
+		break;
+	case BUTTON_DLEFT:
+		drvdata.bmap_2->btn_dleft.mapping_idx = idx;
+		break;
+	case BUTTON_DRIGHT:
+		drvdata.bmap_2->btn_dright.mapping_idx = idx;
+		break;
+	case BUTTON_M1:
+		drvdata.bmap_2->btn_m1.mapping_idx = idx;
+		break;
+	case BUTTON_M2:
+		drvdata.bmap_2->btn_m2.mapping_idx = idx;
+		break;
+	default:
+		return -EINVAL;
+	}
+	mod_delayed_work(system_wq, &oxp_btn_queue, msecs_to_jiffies(50));
+	return count;
+}
+
+static ssize_t map_button_show(struct device *dev,
+			       struct device_attribute *attr, char *buf,
+			       u8 index)
+{
+	u8 i;
+
+	switch (index) {
+	case BUTTON_A:
+		i = drvdata.bmap_1->btn_a.mapping_idx;
+		break;
+	case BUTTON_B:
+		i = drvdata.bmap_1->btn_b.mapping_idx;
+		break;
+	case BUTTON_X:
+		i = drvdata.bmap_1->btn_x.mapping_idx;
+		break;
+	case BUTTON_Y:
+		i = drvdata.bmap_1->btn_y.mapping_idx;
+		break;
+	case BUTTON_LB:
+		i = drvdata.bmap_1->btn_lb.mapping_idx;
+		break;
+	case BUTTON_RB:
+		i = drvdata.bmap_1->btn_rb.mapping_idx;
+		break;
+	case BUTTON_LT:
+		i = drvdata.bmap_1->btn_lt.mapping_idx;
+		break;
+	case BUTTON_RT:
+		i = drvdata.bmap_1->btn_rt.mapping_idx;
+		break;
+	case BUTTON_START:
+		i = drvdata.bmap_1->btn_start.mapping_idx;
+		break;
+	case BUTTON_SELECT:
+		i = drvdata.bmap_2->btn_select.mapping_idx;
+		break;
+	case BUTTON_L3:
+		i = drvdata.bmap_2->btn_l3.mapping_idx;
+		break;
+	case BUTTON_R3:
+		i = drvdata.bmap_2->btn_r3.mapping_idx;
+		break;
+	case BUTTON_DUP:
+		i = drvdata.bmap_2->btn_dup.mapping_idx;
+		break;
+	case BUTTON_DDOWN:
+		i = drvdata.bmap_2->btn_ddown.mapping_idx;
+		break;
+	case BUTTON_DLEFT:
+		i = drvdata.bmap_2->btn_dleft.mapping_idx;
+		break;
+	case BUTTON_DRIGHT:
+		i = drvdata.bmap_2->btn_dright.mapping_idx;
+		break;
+	case BUTTON_M1:
+		i = drvdata.bmap_2->btn_m1.mapping_idx;
+		break;
+	case BUTTON_M2:
+		i = drvdata.bmap_2->btn_m2.mapping_idx;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	if (i >= ARRAY_SIZE(oxp_button_table))
+		return -EINVAL;
+
+	return sysfs_emit(buf, "%s\n", oxp_button_table[i].name);
+}
+
+static ssize_t button_mapping_options_show(struct device *dev,
+					   struct device_attribute *attr, char *buf)
+{
+	ssize_t count = 0;
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(oxp_button_table); i++)
+		count += sysfs_emit_at(buf, count, "%s ", oxp_button_table[i].name);
+
+	if (count)
+		buf[count - 1] = '\n';
+
+	return count;
+}
+static DEVICE_ATTR_RO(button_mapping_options);
+
+#define OXP_DEVICE_ATTR_RW(_name, _group)                                     \
+	static ssize_t _name##_store(struct device *dev,                      \
+				     struct device_attribute *attr,           \
+				     const char *buf, size_t count)           \
+	{                                                                     \
+		return _group##_store(dev, attr, buf, count, _name.index);    \
+	}                                                                     \
+	static ssize_t _name##_show(struct device *dev,                       \
+				    struct device_attribute *attr, char *buf) \
+	{                                                                     \
+		return _group##_show(dev, attr, buf, _name.index);            \
+	}                                                                     \
+	static DEVICE_ATTR_RW(_name)
+
+static struct oxp_attr button_a = { BUTTON_A };
+OXP_DEVICE_ATTR_RW(button_a, map_button);
+
+static struct oxp_attr button_b = { BUTTON_B };
+OXP_DEVICE_ATTR_RW(button_b, map_button);
+
+static struct oxp_attr button_x = { BUTTON_X };
+OXP_DEVICE_ATTR_RW(button_x, map_button);
+
+static struct oxp_attr button_y = { BUTTON_Y };
+OXP_DEVICE_ATTR_RW(button_y, map_button);
+
+static struct oxp_attr button_lb = { BUTTON_LB };
+OXP_DEVICE_ATTR_RW(button_lb, map_button);
+
+static struct oxp_attr button_rb = { BUTTON_RB };
+OXP_DEVICE_ATTR_RW(button_rb, map_button);
+
+static struct oxp_attr button_lt = { BUTTON_LT };
+OXP_DEVICE_ATTR_RW(button_lt, map_button);
+
+static struct oxp_attr button_rt = { BUTTON_RT };
+OXP_DEVICE_ATTR_RW(button_rt, map_button);
+
+static struct oxp_attr button_start = { BUTTON_START };
+OXP_DEVICE_ATTR_RW(button_start, map_button);
+
+static struct oxp_attr button_select = { BUTTON_SELECT };
+OXP_DEVICE_ATTR_RW(button_select, map_button);
+
+static struct oxp_attr button_l3 = { BUTTON_L3 };
+OXP_DEVICE_ATTR_RW(button_l3, map_button);
+
+static struct oxp_attr button_r3 = { BUTTON_R3 };
+OXP_DEVICE_ATTR_RW(button_r3, map_button);
+
+static struct oxp_attr button_d_up = { BUTTON_DUP };
+OXP_DEVICE_ATTR_RW(button_d_up, map_button);
+
+static struct oxp_attr button_d_down = { BUTTON_DDOWN };
+OXP_DEVICE_ATTR_RW(button_d_down, map_button);
+
+static struct oxp_attr button_d_left = { BUTTON_DLEFT };
+OXP_DEVICE_ATTR_RW(button_d_left, map_button);
+
+static struct oxp_attr button_d_right = { BUTTON_DRIGHT };
+OXP_DEVICE_ATTR_RW(button_d_right, map_button);
+
+static struct oxp_attr button_m1 = { BUTTON_M1 };
+OXP_DEVICE_ATTR_RW(button_m1, map_button);
+
+static struct oxp_attr button_m2 = { BUTTON_M2 };
+OXP_DEVICE_ATTR_RW(button_m2, map_button);
+
 static struct attribute *oxp_cfg_attrs[] = {
+	&dev_attr_button_a.attr,
+	&dev_attr_button_b.attr,
+	&dev_attr_button_d_down.attr,
+	&dev_attr_button_d_left.attr,
+	&dev_attr_button_d_right.attr,
+	&dev_attr_button_d_up.attr,
+	&dev_attr_button_l3.attr,
+	&dev_attr_button_lb.attr,
+	&dev_attr_button_lt.attr,
+	&dev_attr_button_m1.attr,
+	&dev_attr_button_m2.attr,
+	&dev_attr_button_mapping_options.attr,
+	&dev_attr_button_r3.attr,
+	&dev_attr_button_rb.attr,
+	&dev_attr_button_rt.attr,
+	&dev_attr_button_select.attr,
+	&dev_attr_button_start.attr,
+	&dev_attr_button_x.attr,
+	&dev_attr_button_y.attr,
 	&dev_attr_gamepad_mode.attr,
 	&dev_attr_gamepad_mode_index.attr,
+	&dev_attr_reset_buttons.attr,
 	NULL,
 };
 
@@ -823,6 +1374,8 @@ static bool oxp_hybrid_mcu_device(void)
 
 static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 {
+	struct oxp_bmap_page_1 *bmap_1;
+	struct oxp_bmap_page_2 *bmap_2;
 	int ret;
 
 	hid_set_drvdata(hdev, &drvdata);
@@ -855,6 +1408,18 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 		return 0;
 
 skip_rgb:
+	bmap_1 = devm_kzalloc(&hdev->dev, sizeof(struct oxp_bmap_page_1), GFP_KERNEL);
+	if (!bmap_1)
+		return dev_err_probe(&hdev->dev, -ENOMEM,
+				     "Unable to allocate button map page 1\n");
+
+	bmap_2 = devm_kzalloc(&hdev->dev, sizeof(struct oxp_bmap_page_2), GFP_KERNEL);
+	if (!bmap_2)
+		return dev_err_probe(&hdev->dev, -ENOMEM,
+				     "Unable to allocate button map page 2\n");
+
+	drvdata.bmap_1 = bmap_1;
+	drvdata.bmap_2 = bmap_2;
 	mod_delayed_work(system_wq, &oxp_mcu_init, msecs_to_jiffies(50));
 
 	drvdata.gamepad_mode = OXP_GP_MODE_XINPUT;
-- 
2.53.0


^ permalink raw reply related

* [PATCH v2 5/5] HID: hid-oxp: Add Vibration Intensity Attributes
From: Derek J. Clark @ 2026-04-07  4:13 UTC (permalink / raw)
  To: Jiri Kosina, Benjamin Tissoires
  Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
	linux-input, linux-doc, linux-kernel
In-Reply-To: <20260407041354.2283201-1-derekjohn.clark@gmail.com>

Adds attribute for setting the rumble intensity level. This setting must
be re-applied after the gamepad mode is set as doing so resets this to
the default value.

Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
 drivers/hid/hid-oxp.c | 80 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 78 insertions(+), 2 deletions(-)

diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index 1100f1f14f35..cad6973089a0 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -34,6 +34,7 @@ enum oxp_function_index {
 	OXP_FID_GEN1_RGB_SET =		0x07,
 	OXP_FID_GEN1_RGB_REPLY =	0x0f,
 	OXP_FID_GEN2_TOGGLE_MODE =	0xb2,
+	OXP_FID_GEN2_RUMBLE_SET =	0xb3,
 	OXP_FID_GEN2_KEY_STATE =	0xb4,
 	OXP_FID_GEN2_STATUS_EVENT =	0xb8,
 };
@@ -178,6 +179,7 @@ static struct oxp_hid_cfg {
 	struct mutex cfg_mutex; /*ensure single synchronous output report*/
 	u8 rgb_brightness;
 	u8 gamepad_mode;
+	u8 rumble_intensity;
 	u8 rgb_effect;
 	u8 rgb_speed;
 	u8 rgb_en;
@@ -263,6 +265,11 @@ static const char *const oxp_rgb_effect_text[] = {
 	[OXP_EFFECT_MONO_LIST] = "monocolor",
 };
 
+enum oxp_rumble_side_index {
+	OXP_RUMBLE_LEFT = 0x00,
+	OXP_RUMBLE_RIGHT,
+};
+
 struct oxp_gen_1_rgb_report {
 	u8 report_id;
 	u8 message_id;
@@ -338,6 +345,7 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
 
 static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data, u8 data_size);
 static int oxp_set_buttons(void);
+static int oxp_rumble_intensity_set(u8 intensity);
 
 static void oxp_mcu_init_fn(struct work_struct *work)
 {
@@ -365,6 +373,12 @@ static void oxp_mcu_init_fn(struct work_struct *work)
 	if (ret)
 		dev_err(&drvdata.hdev->dev,
 			"Error: Failed to set gamepad mode: %i\n", ret);
+
+	/* Set vibration level */
+	ret = oxp_rumble_intensity_set(drvdata.rumble_intensity);
+	if (ret)
+		dev_err(&drvdata.hdev->dev,
+			"Error: Failed to set rumble intensity: %i\n", ret);
 }
 
 static DECLARE_DELAYED_WORK(oxp_mcu_init, oxp_mcu_init_fn);
@@ -513,6 +527,14 @@ static ssize_t gamepad_mode_store(struct device *dev,
 
 	drvdata.gamepad_mode = data[0];
 
+	if (drvdata.gamepad_mode == OXP_GP_MODE_DEBUG)
+		return count;
+
+	/* Re-apply rumble settings as switching gamepad mode will override */
+	ret = oxp_rumble_intensity_set(drvdata.rumble_intensity);
+	if (ret)
+		return ret;
+
 	return count;
 }
 
@@ -858,6 +880,59 @@ static ssize_t button_mapping_options_show(struct device *dev,
 }
 static DEVICE_ATTR_RO(button_mapping_options);
 
+static int oxp_rumble_intensity_set(u8 intensity)
+{
+	u8 header[15] = { 0x02, 0x38, 0x02, 0xe3, 0x39, 0xe3, 0x39, 0xe3,
+			  0x39, 0x01, intensity, 0x05, 0xe3, 0x39, 0xe3 };
+	u8 footer[9] = { 0x39, 0xe3, 0x39, 0xe3, 0xe3, 0x02, 0x04, 0x39, 0x39 };
+	size_t footer_size = ARRAY_SIZE(footer);
+	size_t header_size = ARRAY_SIZE(header);
+	u8 data[59] = { 0x0 };
+	size_t data_size = ARRAY_SIZE(data);
+
+	memcpy(data, header, header_size);
+	memcpy(data + data_size - footer_size, footer, footer_size);
+
+	return oxp_gen_2_property_out(OXP_FID_GEN2_RUMBLE_SET, data, data_size);
+}
+
+static ssize_t rumble_intensity_store(struct device *dev,
+				      struct device_attribute *attr, const char *buf,
+				      size_t count)
+{
+	int ret;
+	u8 val;
+
+	ret = kstrtou8(buf, 10, &val);
+	if (ret)
+		return ret;
+
+	if (val < 0 || val > 5)
+		return -EINVAL;
+
+	ret = oxp_rumble_intensity_set(val);
+	if (ret)
+		return ret;
+
+	drvdata.rumble_intensity = val;
+
+	return count;
+}
+
+static ssize_t rumble_intensity_show(struct device *dev,
+				     struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "%i\n", drvdata.rumble_intensity);
+}
+static DEVICE_ATTR_RW(rumble_intensity);
+
+static ssize_t rumble_intensity_range_show(struct device *dev,
+					   struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "0-5\n");
+}
+static DEVICE_ATTR_RO(rumble_intensity_range);
+
 #define OXP_DEVICE_ATTR_RW(_name, _group)                                     \
 	static ssize_t _name##_store(struct device *dev,                      \
 				     struct device_attribute *attr,           \
@@ -949,6 +1024,8 @@ static struct attribute *oxp_cfg_attrs[] = {
 	&dev_attr_gamepad_mode.attr,
 	&dev_attr_gamepad_mode_index.attr,
 	&dev_attr_reset_buttons.attr,
+	&dev_attr_rumble_intensity.attr,
+	&dev_attr_rumble_intensity_range.attr,
 	NULL,
 };
 
@@ -1420,10 +1497,9 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
 
 	drvdata.bmap_1 = bmap_1;
 	drvdata.bmap_2 = bmap_2;
+	drvdata.rumble_intensity = 5;
 	mod_delayed_work(system_wq, &oxp_mcu_init, msecs_to_jiffies(50));
 
-	drvdata.gamepad_mode = OXP_GP_MODE_XINPUT;
-
 	ret = devm_device_add_group(&hdev->dev, &oxp_cfg_attrs_group);
 	if (ret)
 		return dev_err_probe(&hdev->dev, ret,
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH] Input: gpio-keys - add hibernation support
From: Dmitry Torokhov @ 2026-04-07  4:31 UTC (permalink / raw)
  To: Armando De Leon; +Cc: Armando De Leon, linux-input, linux-kernel
In-Reply-To: <20260406203932.3391600-1-learmand@amazon.com>

Hi Armando,

On Mon, Apr 06, 2026 at 01:39:10PM -0700, Armando De Leon wrote:
> Hi Dmitry,
> 
> Thanks for the guidance. I investigated further:
> 
> The upstream GICv3 driver (irq-gic-v3.c) has no suspend/resume or
> syscore_ops for the distributor - it does not save or restore any
> per-IRQ configuration across power cycles. On platforms where the
> GIC is re-initialized by firmware during hibernate restore, all
> trigger type configurations set by consumer drivers during probe()
> are lost.
> 
> The IRQ core does preserve the trigger type in irq_data
> (irqd_get_trigger_type), but resume_irq() in kernel/irq/pm.c only
> re-enables the interrupt without re-applying the trigger type to
> hardware.
> 
> A fix in the IRQ PM resume path (having resume_irq() call
> irq_set_irq_type() using the saved trigger type) would fix all
> consumer drivers generically. However, such a change would have
> broad consequences and impact across all platforms and interrupt
> controllers, and would need very careful review and testing.
> 
> Given that the gpio-keys fix is a single irq_set_irq_type() call
> in the .restore callback - minimal, self-contained, and low risk -
> would it be acceptable to handle it at the driver level for now?
> This avoids the risk of a sweeping IRQ core change while solving
> the immediate problem for gpio-keys users with hibernation support.

I am sorry but as I mentioned, the proper fix is in IRQ/irqchip code,
which will ensure that not only gpio-keys but also the rest of the
consumer drivers have consistent state post hibernate.

I understand that you might need your change for a product; I recommend
applying the patch to your internal tree while you are sorting out a
generic solution.

Thanks.

-- 
Dmitry

^ permalink raw reply


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