* Re: [PATCH 15/62] Input: synaptics-rmi4 - fix a locking bug in an error path
From: Bart Van Assche @ 2026-02-23 22:05 UTC (permalink / raw)
To: Dmitry Torokhov
Cc: Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long,
linux-kernel, Marco Elver, Christoph Hellwig, Steven Rostedt,
Nick Desaulniers, Nathan Chancellor, Kees Cook, Jann Horn,
Nick Dyer, linux-input
In-Reply-To: <aZzNSRIJdboXTV2-@google.com>
On 2/23/26 1:58 PM, Dmitry Torokhov wrote:
> Hi Bart,
>
> On Mon, Feb 23, 2026 at 01:50:30PM -0800, Bart Van Assche wrote:
>> Lock f54->data_mutex before the first 'goto error' statement since
>> jumping to the 'error' label causes that mutex to be unlocked.
>>
>> This bug has been detected by the Clang thread-safety checker.
>>
>> Cc: Dmitry Torokhov <dmitry.torokhov@gmail.com>
>> Cc: Nick Dyer <nick@shmanahar.org>
>> Cc: linux-input@vger.kernel.org
>> Fixes: 3a762dbd5347 ("[media] Input: synaptics-rmi4 - add support for F54 diagnostics")
>> Signed-off-by: Bart Van Assche <bvanassche@acm.org>
>> ---
>> drivers/input/rmi4/rmi_f54.c | 5 +++--
>> 1 file changed, 3 insertions(+), 2 deletions(-)
>>
>> diff --git a/drivers/input/rmi4/rmi_f54.c b/drivers/input/rmi4/rmi_f54.c
>> index ac4041a69fcd..fd57ebb1cb50 100644
>> --- a/drivers/input/rmi4/rmi_f54.c
>> +++ b/drivers/input/rmi4/rmi_f54.c
>> @@ -539,6 +539,9 @@ static void rmi_f54_work(struct work_struct *work)
>> int i;
>>
>> report_size = rmi_f54_get_report_size(f54);
>> +
>> + mutex_lock(&f54->data_mutex);
>> +
>
> Thank you for the patch. Do you mind if I move mutex_lock() above the
> call to rmi_f54_get_report_size()? It does not extend critical section
> by much, and I think logically makes more sense.
That sounds good to me. Please keep in mind that I'm not familiar with
the rmi4 driver.
Thanks,
Bart.
^ permalink raw reply
* [PATCH 37/37] PCI/MSI: Only check is_msi_managed in pcim_setup_msi_release()
From: Shawn Lin @ 2026-02-23 15:29 UTC (permalink / raw)
To: Bjorn Helgaas, Vaibhaav Ram T . L, Kumaravel Thiagarajan, Even Xu,
Xinpeng Sun, Srinivas Pandruvada, Jiri Kosina, Alexandre Belloni,
Zhou Wang, Longfang Liu, Vinod Koul, Lee Jones, Jijie Shao,
Jian Shen, Sunil Goutham, Andrew Lunn, Heiner Kallweit,
David S . Miller, Jeff Hugo, Oded Gabbay, Maciej Falkowski,
Karol Wachowski, Min Ma, Lizhi Hou, Andreas Noever,
Mika Westerberg, Tomasz Jeznach, Will Deacon, Xinliang Liu,
Tian Tao, Davidlohr Bueso, Jonathan Cameron, Srujana Challa,
Bharat Bhushan, Antoine Tenart, Herbert Xu, Raag Jadav,
Hans de Goede, Greg Kroah-Hartman, Jiri Slaby, Andy Shevchenko,
Manivannan Sadhasivam, Mika Westerberg, Andi Shyti,
Robert Richter, Mark Brown, Nirmal Patel, Kurt Schwemmer,
Logan Gunthorpe, Linus Walleij, Bartosz Golaszewski, Sakari Ailus,
Bingbu Cao, Ulf Hansson
Cc: Arnd Bergmann, Benjamin Tissoires, linux-input, linux-i3c,
dmaengine, Philipp Stanner, netdev, nic_swsd, linux-arm-msm,
dri-devel, linux-usb, iommu, linux-riscv, David Airlie,
Simona Vetter, linux-cxl, linux-crypto, platform-driver-x86,
linux-serial, mhi, Andy Shevchenko, Jan Dabros, linux-i2c,
Daniel Mack, Haojian Zhuang, linux-spi, Jonathan Derrick,
linux-pci, linux-gpio, Mauro Carvalho Chehab, linux-media,
linux-mmc, Shawn Lin
In-Reply-To: <1771860581-82092-1-git-send-email-shawn.lin@rock-chips.com>
The function pcim_enable_device() sets the is_managed flag, which
causes the device's IRQ vectors to be automatically managed and released
by the devres framework. If a driver subsequently calls
pci_free_irq_vectors() manually, it can lead to a double-free of the
interrupt resources.
Analysis reveals most PCI drivers call pci_free_irq_vectors()
while also using pcim_enable_device(), making them susceptible to this
double-free issue. In contrast, 35 drivers follow the pattern of
relying on devres to handle the cleanup.
To address this inconsistency and enforce explicit, driver-managed
control of IRQ vectors, this patch removes the pci_is_managed() check
from pcim_setup_msi_release() and let devres help cleanup if is_msi_managed
is true. This change ensures that interrupt vectors are not automatically
freed by the devres machinery when pcim_enable_device() is used, placing
the responsibility for their release squarely on the driver logic via
pci_free_irq_vectors(). If the driver need devres to help cleanup, newly added
pcim_alloc_irq_vectors() and pcim_alloc_irq_vectors_affinity() helpers could be used.
Signed-off-by: Shawn Lin <shawn.lin@rock-chips.com>
---
drivers/pci/msi/msi.c | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/drivers/pci/msi/msi.c b/drivers/pci/msi/msi.c
index 81d24a2..0727a0a 100644
--- a/drivers/pci/msi/msi.c
+++ b/drivers/pci/msi/msi.c
@@ -70,7 +70,6 @@ static void pcim_msi_release(void *pcidev)
{
struct pci_dev *dev = pcidev;
- dev->is_msi_managed = false;
pci_free_irq_vectors(dev);
}
@@ -92,14 +91,13 @@ static int pcim_setup_msi_release(struct pci_dev *dev)
{
int ret;
- if (!pci_is_managed(dev) || dev->is_msi_managed)
+ if (!dev->is_msi_managed)
return 0;
ret = devm_add_action(&dev->dev, pcim_msi_release, dev);
if (ret)
return ret;
- dev->is_msi_managed = true;
return 0;
}
--
2.7.4
^ permalink raw reply related
* [syzbot] [input?] possible deadlock in input_repeat_key
From: syzbot @ 2026-02-23 22:46 UTC (permalink / raw)
To: amir73il, jack, linux-fsdevel, linux-input, linux-kernel,
syzkaller-bugs
Hello,
syzbot found the following issue on:
HEAD commit: a95f71ad3e2e Merge tag 'for-linus' of git://git.kernel.org..
git tree: upstream
console output: https://syzkaller.appspot.com/x/log.txt?x=17db0152580000
kernel config: https://syzkaller.appspot.com/x/.config?x=665cbf0979cda6c5
dashboard link: https://syzkaller.appspot.com/bug?extid=422e601066bf071a9f8e
compiler: Debian clang version 21.1.8 (++20251221033036+2078da43e25a-1~exp1~20251221153213.50), Debian LLD 21.1.8
Unfortunately, I don't have any reproducer for this issue yet.
Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/3abe9dfc8715/disk-a95f71ad.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/0a4dab480d10/vmlinux-a95f71ad.xz
kernel image: https://storage.googleapis.com/syzbot-assets/d2eecb72d9bd/bzImage-a95f71ad.xz
IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+422e601066bf071a9f8e@syzkaller.appspotmail.com
=====================================================
WARNING: SOFTIRQ-safe -> SOFTIRQ-unsafe lock order detected
syzkaller #0 Tainted: G L
-----------------------------------------------------
syz.0.6889/23959 [HC0[0]:SC0[0]:HE0:SE1] is trying to acquire:
ffff88805533cf30 (&new->fa_lock){....}-{3:3}, at: kill_fasync_rcu fs/fcntl.c:1135 [inline]
ffff88805533cf30 (&new->fa_lock){....}-{3:3}, at: kill_fasync+0x199/0x4d0 fs/fcntl.c:1159
and this task is already holding:
ffff88802bb32230 (&dev->event_lock#2){..-.}-{3:3}, at: class_spinlock_irqsave_constructor include/linux/spinlock.h:618 [inline]
ffff88802bb32230 (&dev->event_lock#2){..-.}-{3:3}, at: input_inject_event+0xa5/0x340 drivers/input/input.c:419
which would create a new lock dependency:
(&dev->event_lock#2){..-.}-{3:3} -> (&new->fa_lock){....}-{3:3}
but this new dependency connects a SOFTIRQ-irq-safe lock:
(&dev->event_lock#2){..-.}-{3:3}
... which became SOFTIRQ-irq-safe at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:132 [inline]
_raw_spin_lock_irqsave+0x40/0x60 kernel/locking/spinlock.c:162
class_spinlock_irqsave_constructor include/linux/spinlock.h:618 [inline]
input_repeat_key+0x33/0x680 drivers/input/input.c:2220
call_timer_fn+0x192/0x640 kernel/time/timer.c:1748
expire_timers kernel/time/timer.c:1799 [inline]
__run_timers kernel/time/timer.c:2373 [inline]
__run_timer_base+0x652/0x8b0 kernel/time/timer.c:2385
run_timer_base kernel/time/timer.c:2394 [inline]
run_timer_softirq+0xb7/0x170 kernel/time/timer.c:2404
handle_softirqs+0x22a/0x870 kernel/softirq.c:622
__do_softirq kernel/softirq.c:656 [inline]
invoke_softirq kernel/softirq.c:496 [inline]
__irq_exit_rcu+0x5f/0x150 kernel/softirq.c:723
irq_exit_rcu+0x9/0x30 kernel/softirq.c:739
instr_sysvec_apic_timer_interrupt arch/x86/kernel/apic/apic.c:1056 [inline]
sysvec_apic_timer_interrupt+0xa6/0xc0 arch/x86/kernel/apic/apic.c:1056
asm_sysvec_apic_timer_interrupt+0x1a/0x20 arch/x86/include/asm/idtentry.h:697
rcu_is_watching+0x70/0xb0 kernel/rcu/tree.c:754
rcu_read_lock_held_common kernel/rcu/update.c:109 [inline]
rcu_read_lock_held+0x15/0x50 kernel/rcu/update.c:349
mas_root lib/maple_tree.c:780 [inline]
mas_start+0x1e9/0x560 lib/maple_tree.c:1200
mas_state_walk lib/maple_tree.c:3291 [inline]
mas_walk+0x8b/0x2e0 lib/maple_tree.c:4599
lock_vma_under_rcu+0x1bd/0x500 mm/mmap_lock.c:304
do_user_addr_fault+0x2d8/0x1340 arch/x86/mm/fault.c:1325
handle_page_fault arch/x86/mm/fault.c:1474 [inline]
exc_page_fault+0x6a/0xc0 arch/x86/mm/fault.c:1527
asm_exc_page_fault+0x26/0x30 arch/x86/include/asm/idtentry.h:618
to a SOFTIRQ-irq-unsafe lock:
(tasklist_lock){.+.+}-{3:3}
... which became SOFTIRQ-irq-unsafe at:
...
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_read_lock include/linux/rwlock_api_smp.h:161 [inline]
_raw_read_lock+0x36/0x50 kernel/locking/spinlock.c:228
__do_wait+0xde/0x740 kernel/exit.c:1672
do_wait+0x1e7/0x540 kernel/exit.c:1716
kernel_wait+0xd6/0x1c0 kernel/exit.c:1892
call_usermodehelper_exec_sync kernel/umh.c:136 [inline]
call_usermodehelper_exec_work+0xbe/0x230 kernel/umh.c:163
process_one_work kernel/workqueue.c:3275 [inline]
process_scheduled_works+0xb02/0x1830 kernel/workqueue.c:3358
worker_thread+0xa50/0xfc0 kernel/workqueue.c:3439
kthread+0x388/0x470 kernel/kthread.c:467
ret_from_fork+0x51e/0xb90 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
other info that might help us debug this:
Chain exists of:
&dev->event_lock#2 --> &new->fa_lock --> tasklist_lock
Possible interrupt unsafe locking scenario:
CPU0 CPU1
---- ----
lock(tasklist_lock);
local_irq_disable();
lock(&dev->event_lock#2);
lock(&new->fa_lock);
<Interrupt>
lock(&dev->event_lock#2);
*** DEADLOCK ***
6 locks held by syz.0.6889/23959:
#0: ffff88802bc7c118 (&evdev->mutex){+.+.}-{4:4}, at: evdev_write+0x1ae/0x4c0 drivers/input/evdev.c:511
#1: ffff88802bb32230 (&dev->event_lock#2){..-.}-{3:3}, at: class_spinlock_irqsave_constructor include/linux/spinlock.h:618 [inline]
#1: ffff88802bb32230 (&dev->event_lock#2){..-.}-{3:3}, at: input_inject_event+0xa5/0x340 drivers/input/input.c:419
#2: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: rcu_lock_acquire include/linux/rcupdate.h:312 [inline]
#2: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: rcu_read_lock include/linux/rcupdate.h:850 [inline]
#2: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: class_rcu_constructor include/linux/rcupdate.h:1193 [inline]
#2: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: input_inject_event+0xb6/0x340 drivers/input/input.c:420
#3: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: rcu_lock_acquire include/linux/rcupdate.h:312 [inline]
#3: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: rcu_read_lock include/linux/rcupdate.h:850 [inline]
#3: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: class_rcu_constructor include/linux/rcupdate.h:1193 [inline]
#3: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: input_pass_values+0x8d/0x890 drivers/input/input.c:119
#4: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: rcu_lock_acquire include/linux/rcupdate.h:312 [inline]
#4: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: rcu_read_lock include/linux/rcupdate.h:850 [inline]
#4: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: mousedev_notify_readers+0x2c/0xc00 drivers/input/mousedev.c:269
#5: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: rcu_lock_acquire include/linux/rcupdate.h:312 [inline]
#5: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: rcu_read_lock include/linux/rcupdate.h:850 [inline]
#5: ffffffff8e7602e0 (rcu_read_lock){....}-{1:3}, at: kill_fasync+0x53/0x4d0 fs/fcntl.c:1158
the dependencies between SOFTIRQ-irq-safe lock and the holding lock:
-> (&dev->event_lock#2){..-.}-{3:3} {
IN-SOFTIRQ-W at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:132 [inline]
_raw_spin_lock_irqsave+0x40/0x60 kernel/locking/spinlock.c:162
class_spinlock_irqsave_constructor include/linux/spinlock.h:618 [inline]
input_repeat_key+0x33/0x680 drivers/input/input.c:2220
call_timer_fn+0x192/0x640 kernel/time/timer.c:1748
expire_timers kernel/time/timer.c:1799 [inline]
__run_timers kernel/time/timer.c:2373 [inline]
__run_timer_base+0x652/0x8b0 kernel/time/timer.c:2385
run_timer_base kernel/time/timer.c:2394 [inline]
run_timer_softirq+0xb7/0x170 kernel/time/timer.c:2404
handle_softirqs+0x22a/0x870 kernel/softirq.c:622
__do_softirq kernel/softirq.c:656 [inline]
invoke_softirq kernel/softirq.c:496 [inline]
__irq_exit_rcu+0x5f/0x150 kernel/softirq.c:723
irq_exit_rcu+0x9/0x30 kernel/softirq.c:739
instr_sysvec_apic_timer_interrupt arch/x86/kernel/apic/apic.c:1056 [inline]
sysvec_apic_timer_interrupt+0xa6/0xc0 arch/x86/kernel/apic/apic.c:1056
asm_sysvec_apic_timer_interrupt+0x1a/0x20 arch/x86/include/asm/idtentry.h:697
rcu_is_watching+0x70/0xb0 kernel/rcu/tree.c:754
rcu_read_lock_held_common kernel/rcu/update.c:109 [inline]
rcu_read_lock_held+0x15/0x50 kernel/rcu/update.c:349
mas_root lib/maple_tree.c:780 [inline]
mas_start+0x1e9/0x560 lib/maple_tree.c:1200
mas_state_walk lib/maple_tree.c:3291 [inline]
mas_walk+0x8b/0x2e0 lib/maple_tree.c:4599
lock_vma_under_rcu+0x1bd/0x500 mm/mmap_lock.c:304
do_user_addr_fault+0x2d8/0x1340 arch/x86/mm/fault.c:1325
handle_page_fault arch/x86/mm/fault.c:1474 [inline]
exc_page_fault+0x6a/0xc0 arch/x86/mm/fault.c:1527
asm_exc_page_fault+0x26/0x30 arch/x86/include/asm/idtentry.h:618
INITIAL USE at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:132 [inline]
_raw_spin_lock_irqsave+0x40/0x60 kernel/locking/spinlock.c:162
class_spinlock_irqsave_constructor include/linux/spinlock.h:618 [inline]
input_inject_event+0xa5/0x340 drivers/input/input.c:419
kbd_led_trigger_activate+0xbc/0x100 drivers/tty/vt/keyboard.c:1021
led_trigger_set+0x535/0x960 drivers/leds/led-triggers.c:220
led_match_default_trigger drivers/leds/led-triggers.c:277 [inline]
led_trigger_set_default+0x260/0x2a0 drivers/leds/led-triggers.c:300
led_classdev_register_ext+0x787/0x9c0 drivers/leds/led-class.c:578
led_classdev_register include/linux/leds.h:274 [inline]
input_leds_connect+0x517/0x790 drivers/input/input-leds.c:145
input_attach_handler drivers/input/input.c:994 [inline]
input_register_device+0xd00/0x1160 drivers/input/input.c:2378
atkbd_connect+0x731/0xa50 drivers/input/keyboard/atkbd.c:1340
serio_connect_driver drivers/input/serio/serio.c:44 [inline]
serio_driver_probe+0x82/0xd0 drivers/input/serio/serio.c:748
call_driver_probe drivers/base/dd.c:-1 [inline]
really_probe+0x267/0xaf0 drivers/base/dd.c:661
__driver_probe_device+0x18c/0x320 drivers/base/dd.c:803
driver_probe_device+0x4f/0x240 drivers/base/dd.c:833
__driver_attach+0x3e7/0x710 drivers/base/dd.c:1227
bus_for_each_dev+0x23b/0x2c0 drivers/base/bus.c:383
serio_attach_driver drivers/input/serio/serio.c:777 [inline]
serio_handle_event+0x20a/0xdd0 drivers/input/serio/serio.c:214
process_one_work kernel/workqueue.c:3275 [inline]
process_scheduled_works+0xb02/0x1830 kernel/workqueue.c:3358
worker_thread+0xa50/0xfc0 kernel/workqueue.c:3439
kthread+0x388/0x470 kernel/kthread.c:467
ret_from_fork+0x51e/0xb90 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
}
... key at: [<ffffffff9a6053c0>] input_allocate_device.__key.7+0x0/0x20
the dependencies between the lock to be acquired
and SOFTIRQ-irq-unsafe lock:
-> (tasklist_lock){.+.+}-{3:3} {
HARDIRQ-ON-R at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_read_lock include/linux/rwlock_api_smp.h:161 [inline]
_raw_read_lock+0x36/0x50 kernel/locking/spinlock.c:228
__do_wait+0xde/0x740 kernel/exit.c:1672
do_wait+0x1e7/0x540 kernel/exit.c:1716
kernel_wait+0xd6/0x1c0 kernel/exit.c:1892
call_usermodehelper_exec_sync kernel/umh.c:136 [inline]
call_usermodehelper_exec_work+0xbe/0x230 kernel/umh.c:163
process_one_work kernel/workqueue.c:3275 [inline]
process_scheduled_works+0xb02/0x1830 kernel/workqueue.c:3358
worker_thread+0xa50/0xfc0 kernel/workqueue.c:3439
kthread+0x388/0x470 kernel/kthread.c:467
ret_from_fork+0x51e/0xb90 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
SOFTIRQ-ON-R at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_read_lock include/linux/rwlock_api_smp.h:161 [inline]
_raw_read_lock+0x36/0x50 kernel/locking/spinlock.c:228
__do_wait+0xde/0x740 kernel/exit.c:1672
do_wait+0x1e7/0x540 kernel/exit.c:1716
kernel_wait+0xd6/0x1c0 kernel/exit.c:1892
call_usermodehelper_exec_sync kernel/umh.c:136 [inline]
call_usermodehelper_exec_work+0xbe/0x230 kernel/umh.c:163
process_one_work kernel/workqueue.c:3275 [inline]
process_scheduled_works+0xb02/0x1830 kernel/workqueue.c:3358
worker_thread+0xa50/0xfc0 kernel/workqueue.c:3439
kthread+0x388/0x470 kernel/kthread.c:467
ret_from_fork+0x51e/0xb90 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
INITIAL USE at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_write_lock_irq include/linux/rwlock_api_smp.h:211 [inline]
_raw_write_lock_irq+0x3d/0x50 kernel/locking/spinlock.c:326
copy_process+0x247a/0x3cf0 kernel/fork.c:2369
kernel_clone+0x248/0x8e0 kernel/fork.c:2654
user_mode_thread+0x110/0x180 kernel/fork.c:2730
rest_init+0x23/0x300 init/main.c:725
start_kernel+0x385/0x3d0 init/main.c:1210
x86_64_start_reservations+0x24/0x30 arch/x86/kernel/head64.c:310
x86_64_start_kernel+0x143/0x1c0 arch/x86/kernel/head64.c:291
common_startup_64+0x13e/0x147
INITIAL READ USE at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_read_lock include/linux/rwlock_api_smp.h:161 [inline]
_raw_read_lock+0x36/0x50 kernel/locking/spinlock.c:228
__do_wait+0xde/0x740 kernel/exit.c:1672
do_wait+0x1e7/0x540 kernel/exit.c:1716
kernel_wait+0xd6/0x1c0 kernel/exit.c:1892
call_usermodehelper_exec_sync kernel/umh.c:136 [inline]
call_usermodehelper_exec_work+0xbe/0x230 kernel/umh.c:163
process_one_work kernel/workqueue.c:3275 [inline]
process_scheduled_works+0xb02/0x1830 kernel/workqueue.c:3358
worker_thread+0xa50/0xfc0 kernel/workqueue.c:3439
kthread+0x388/0x470 kernel/kthread.c:467
ret_from_fork+0x51e/0xb90 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
}
... key at: [<ffffffff8e40c058>] tasklist_lock+0x18/0x40
... acquired at:
__raw_read_lock include/linux/rwlock_api_smp.h:161 [inline]
_raw_read_lock+0x36/0x50 kernel/locking/spinlock.c:228
send_sigio+0x101/0x370 fs/fcntl.c:932
kill_fasync_rcu fs/fcntl.c:1144 [inline]
kill_fasync+0x24d/0x4d0 fs/fcntl.c:1159
sock_wake_async+0x137/0x160 net/socket.c:-1
sk_wake_async_rcu include/net/sock.h:2579 [inline]
sock_def_readable+0x3c1/0x580 net/core/sock.c:3613
unix_stream_sendmsg+0x8a3/0xe80 net/unix/af_unix.c:2480
sock_sendmsg_nosec net/socket.c:727 [inline]
__sock_sendmsg net/socket.c:742 [inline]
____sys_sendmsg+0xa68/0xad0 net/socket.c:2592
___sys_sendmsg+0x2a5/0x360 net/socket.c:2646
__sys_sendmsg net/socket.c:2678 [inline]
__do_sys_sendmsg net/socket.c:2683 [inline]
__se_sys_sendmsg net/socket.c:2681 [inline]
__x64_sys_sendmsg+0x1bd/0x2a0 net/socket.c:2681
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
-> (&f_owner->lock){....}-{3:3} {
INITIAL USE at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_write_lock_irq include/linux/rwlock_api_smp.h:211 [inline]
_raw_write_lock_irq+0x3d/0x50 kernel/locking/spinlock.c:326
__f_setown+0x67/0x370 fs/fcntl.c:136
fcntl_dirnotify+0x3f9/0x6a0 fs/notify/dnotify/dnotify.c:369
do_fcntl+0x77e/0x1a20 fs/fcntl.c:538
__do_sys_fcntl fs/fcntl.c:602 [inline]
__se_sys_fcntl+0xc8/0x150 fs/fcntl.c:587
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
INITIAL READ USE at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_read_lock_irqsave include/linux/rwlock_api_smp.h:172 [inline]
_raw_read_lock_irqsave+0x48/0x60 kernel/locking/spinlock.c:236
send_sigio+0x38/0x370 fs/fcntl.c:918
kill_fasync_rcu fs/fcntl.c:1144 [inline]
kill_fasync+0x24d/0x4d0 fs/fcntl.c:1159
sock_wake_async+0x137/0x160 net/socket.c:-1
sk_wake_async_rcu include/net/sock.h:2579 [inline]
sock_def_readable+0x3c1/0x580 net/core/sock.c:3613
unix_stream_sendmsg+0x8a3/0xe80 net/unix/af_unix.c:2480
sock_sendmsg_nosec net/socket.c:727 [inline]
__sock_sendmsg net/socket.c:742 [inline]
____sys_sendmsg+0xa68/0xad0 net/socket.c:2592
___sys_sendmsg+0x2a5/0x360 net/socket.c:2646
__sys_sendmsg net/socket.c:2678 [inline]
__do_sys_sendmsg net/socket.c:2683 [inline]
__se_sys_sendmsg net/socket.c:2681 [inline]
__x64_sys_sendmsg+0x1bd/0x2a0 net/socket.c:2681
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
}
... key at: [<ffffffff9a2e6160>] file_f_owner_allocate.__key+0x0/0x20
... acquired at:
__raw_read_lock_irqsave include/linux/rwlock_api_smp.h:172 [inline]
_raw_read_lock_irqsave+0x48/0x60 kernel/locking/spinlock.c:236
send_sigio+0x38/0x370 fs/fcntl.c:918
kill_fasync_rcu fs/fcntl.c:1144 [inline]
kill_fasync+0x24d/0x4d0 fs/fcntl.c:1159
sock_wake_async+0x137/0x160 net/socket.c:-1
sk_wake_async_rcu include/net/sock.h:2579 [inline]
sock_def_readable+0x3c1/0x580 net/core/sock.c:3613
unix_stream_sendmsg+0x8a3/0xe80 net/unix/af_unix.c:2480
sock_sendmsg_nosec net/socket.c:727 [inline]
__sock_sendmsg net/socket.c:742 [inline]
____sys_sendmsg+0xa68/0xad0 net/socket.c:2592
___sys_sendmsg+0x2a5/0x360 net/socket.c:2646
__sys_sendmsg net/socket.c:2678 [inline]
__do_sys_sendmsg net/socket.c:2683 [inline]
__se_sys_sendmsg net/socket.c:2681 [inline]
__x64_sys_sendmsg+0x1bd/0x2a0 net/socket.c:2681
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
-> (&new->fa_lock){....}-{3:3} {
INITIAL USE at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_write_lock_irq include/linux/rwlock_api_smp.h:211 [inline]
_raw_write_lock_irq+0x3d/0x50 kernel/locking/spinlock.c:326
fasync_remove_entry+0xf1/0x1c0 fs/fcntl.c:1012
sock_fasync+0x85/0xf0 net/socket.c:1480
__fput+0x8a5/0xa70 fs/file_table.c:466
task_work_run+0x1d9/0x270 kernel/task_work.c:233
resume_user_mode_work include/linux/resume_user_mode.h:50 [inline]
__exit_to_user_mode_loop kernel/entry/common.c:67 [inline]
exit_to_user_mode_loop+0xed/0x480 kernel/entry/common.c:98
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:226 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:256 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:325 [inline]
do_syscall_64+0x32d/0xf80 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
INITIAL READ USE at:
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_read_lock_irqsave include/linux/rwlock_api_smp.h:172 [inline]
_raw_read_lock_irqsave+0x48/0x60 kernel/locking/spinlock.c:236
kill_fasync_rcu fs/fcntl.c:1135 [inline]
kill_fasync+0x199/0x4d0 fs/fcntl.c:1159
sock_wake_async+0x137/0x160 net/socket.c:-1
sk_wake_async_rcu include/net/sock.h:2579 [inline]
sock_def_readable+0x3c1/0x580 net/core/sock.c:3613
unix_stream_sendmsg+0x8a3/0xe80 net/unix/af_unix.c:2480
sock_sendmsg_nosec net/socket.c:727 [inline]
__sock_sendmsg net/socket.c:742 [inline]
____sys_sendmsg+0xa68/0xad0 net/socket.c:2592
___sys_sendmsg+0x2a5/0x360 net/socket.c:2646
__sys_sendmsg net/socket.c:2678 [inline]
__do_sys_sendmsg net/socket.c:2683 [inline]
__se_sys_sendmsg net/socket.c:2681 [inline]
__x64_sys_sendmsg+0x1bd/0x2a0 net/socket.c:2681
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
}
... key at: [<ffffffff9a2e6180>] fasync_insert_entry.__key+0x0/0x20
... acquired at:
__raw_read_lock_irqsave include/linux/rwlock_api_smp.h:172 [inline]
_raw_read_lock_irqsave+0x48/0x60 kernel/locking/spinlock.c:236
kill_fasync_rcu fs/fcntl.c:1135 [inline]
kill_fasync+0x199/0x4d0 fs/fcntl.c:1159
mousedev_notify_readers+0x6f1/0xc00 drivers/input/mousedev.c:309
mousedev_event+0x602/0x1320 drivers/input/mousedev.c:394
input_handle_events_default+0xd4/0x1a0 drivers/input/input.c:2541
input_pass_values+0x288/0x890 drivers/input/input.c:128
input_event_dispose+0x330/0x6b0 drivers/input/input.c:342
input_inject_event+0x1dd/0x340 drivers/input/input.c:424
evdev_write+0x325/0x4c0 drivers/input/evdev.c:528
vfs_write+0x29a/0xb90 fs/read_write.c:686
ksys_write+0x150/0x270 fs/read_write.c:740
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
stack backtrace:
CPU: 1 UID: 0 PID: 23959 Comm: syz.0.6889 Tainted: G L syzkaller #0 PREEMPT(full)
Tainted: [L]=SOFTLOCKUP
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 02/12/2026
Call Trace:
<TASK>
dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
print_bad_irq_dependency kernel/locking/lockdep.c:2616 [inline]
check_irq_usage kernel/locking/lockdep.c:2857 [inline]
check_prev_add kernel/locking/lockdep.c:3169 [inline]
check_prevs_add kernel/locking/lockdep.c:3284 [inline]
validate_chain kernel/locking/lockdep.c:3908 [inline]
__lock_acquire+0x2a94/0x2cf0 kernel/locking/lockdep.c:5237
lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
__raw_read_lock_irqsave include/linux/rwlock_api_smp.h:172 [inline]
_raw_read_lock_irqsave+0x48/0x60 kernel/locking/spinlock.c:236
kill_fasync_rcu fs/fcntl.c:1135 [inline]
kill_fasync+0x199/0x4d0 fs/fcntl.c:1159
mousedev_notify_readers+0x6f1/0xc00 drivers/input/mousedev.c:309
mousedev_event+0x602/0x1320 drivers/input/mousedev.c:394
input_handle_events_default+0xd4/0x1a0 drivers/input/input.c:2541
input_pass_values+0x288/0x890 drivers/input/input.c:128
input_event_dispose+0x330/0x6b0 drivers/input/input.c:342
input_inject_event+0x1dd/0x340 drivers/input/input.c:424
evdev_write+0x325/0x4c0 drivers/input/evdev.c:528
vfs_write+0x29a/0xb90 fs/read_write.c:686
ksys_write+0x150/0x270 fs/read_write.c:740
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f1f91b9c629
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f1f92b1f028 EFLAGS: 00000246 ORIG_RAX: 0000000000000001
RAX: ffffffffffffffda RBX: 00007f1f91e15fa0 RCX: 00007f1f91b9c629
RDX: 0000000000000918 RSI: 0000200000000040 RDI: 0000000000000004
RBP: 00007f1f91c32b39 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f1f91e16038 R14: 00007f1f91e15fa0 R15: 00007f1f91f3fa48
</TASK>
---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.
syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.
If the report is already addressed, let syzbot know by replying with:
#syz fix: exact-commit-title
If you want to overwrite report's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)
If the report is a duplicate of another one, reply with:
#syz dup: exact-subject-of-another-report
If you want to undo deduplication, reply with:
#syz undup
^ permalink raw reply
* Re: [PATCH v2 1/2] dt-bindings: input: add GPIO charlieplex keypad
From: Rob Herring @ 2026-02-23 23:23 UTC (permalink / raw)
To: Hugo Villeneuve
Cc: hvilleneuve, dmitry.torokhov, krzk+dt, conor+dt, linux-input,
devicetree, linux-kernel
In-Reply-To: <20260223134738.00988a3d87165cb130292c89@hugovil.com>
On Mon, Feb 23, 2026 at 12:47 PM Hugo Villeneuve <hugo@hugovil.com> wrote:
>
> Hi Rob,
>
> On Mon, 23 Feb 2026 11:57:06 -0600
> Rob Herring <robh@kernel.org> wrote:
>
> > On Fri, Feb 13, 2026 at 12:14:25PM -0500, Hugo Villeneuve wrote:
> > > From: Hugo Villeneuve <hvilleneuve@dimonoff.com>
> > >
> > > Add DT bindings for GPIO charlieplex keypad.
> > >
> > > Signed-off-by: Hugo Villeneuve <hvilleneuve@dimonoff.com>
> > > ---
> > > .../input/gpio-charlieplex-keypad.yaml | 82 +++++++++++++++++++
> > > 1 file changed, 82 insertions(+)
> > > create mode 100644 Documentation/devicetree/bindings/input/gpio-charlieplex-keypad.yaml
> > >
> > > diff --git a/Documentation/devicetree/bindings/input/gpio-charlieplex-keypad.yaml b/Documentation/devicetree/bindings/input/gpio-charlieplex-keypad.yaml
> > > new file mode 100644
> > > index 0000000000000..1672491a75a85
> > > --- /dev/null
> > > +++ b/Documentation/devicetree/bindings/input/gpio-charlieplex-keypad.yaml
> > > @@ -0,0 +1,82 @@
> > > +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> > > +%YAML 1.2
> > > +---
> > > +
> > > +$id: http://devicetree.org/schemas/input/gpio-charlieplex-keypad.yaml#
> > > +$schema: http://devicetree.org/meta-schemas/core.yaml#
> > > +
> > > +title: GPIO charlieplex keypad
> > > +
> > > +maintainers:
> > > + - Hugo Villeneuve <hvilleneuve@dimonoff.com>
> > > +
> > > +description:
> > > + The charlieplex keypad supports N^2)-N different key combinations (where N is
> > > + the number of lines). Key presses and releases are detected by configuring
> > > + only one line as output at a time, and reading other line states. This process
> > > + is repeated for each line.
> > > + This mechanism doesn't allow to detect simultaneous key presses.
> > > +
> > > +allOf:
> > > + - $ref: input.yaml#
> > > + - $ref: /schemas/input/matrix-keymap.yaml#
> > > +
> > > +properties:
> > > + compatible:
> > > + const: gpio-charlieplex-keypad
> > > +
> > > + autorepeat: true
> > > +
> > > + line-scan-delay-us:
> > > + description:
> > > + Delay, measured in microseconds, that is needed
> > > + before we can scan keypad after activating one line.
> > > + default: 0
> >
> > Isn't this the same as "col-scan-delay-us" in gpio-matrix-keypad.yaml?
> > If so, move it to matrix-keymap.yaml to re-use it here.
>
> It is used in a similar fashion, but for charlieplex keyboard, there is
> no concept of "rows" and "columns". There are only
> lines, which are all equivalent in functionality.
>
> > If not, there's a bunch of other scan delay properties just from
> > grepping "delay" in the input bindings. Surely we can define something
> > common.
>
> Most of those delays refer to something quite different than what
> "col-scan-delay-us" or "line-scan-delay-us" are used for (it is a delay
> that we wait when activating a GPIO before we can safely/reliably read
> other GPIOs connected thru its circuitry).
>
> Maybe "col-scan-delay-us" and "line-scan-delay-us" could be
> combined into a common "line-scan-delay-us" ("line" is more generic
> than column), and defined in matrix-keymap.yaml.
What about "scan-delay-us"? I would assume all the scan delay
properties are just the delay after changing the outputs to reading
the inputs.
> Then would it be ok to remove "col-scan-delay-us" from
> gpio-matrix-keypad.yaml and use "line-scan-delay-us" (ABI change) ?
No!
Rob
^ permalink raw reply
* Re: [PATCH v7 0/3] TrackPoint doubletap enablement and user control
From: Vishnu Sankar @ 2026-02-23 23:28 UTC (permalink / raw)
To: mpearson-lenovo, dmitry.torokhov, hmh, hansg, corbet,
derekjohn.clark, ilpo.jarvinen
Cc: linux-input, linux-kernel, ibm-acpi-devel, linux-doc,
platform-driver-x86, vsankar
In-Reply-To: <20260209063355.491189-1-vishnuocv@gmail.com>
Hi,
Gentle ping on this series.
This is v7 addressing all previous review comments.
Please let me know if any further changes are needed.
Thanks,
Vishnu
On Mon, Feb 9, 2026 at 3:34 PM Vishnu Sankar <vishnuocv@gmail.com> wrote:
>
> This patch series adds support for TrackPoint doubletap with a clear and
> simple separation of responsibilities between drivers:
>
> 1. Firmware enablement (trackpoint.c):
> Automatically enables doubletap on capable hardware during device
> detection.
>
> 2. User control (thinkpad_acpi.c):
> Provides a sysfs interface to enable or disable delivery of doubletap
> events to userspace.
>
> The approach follows the KISS principle:
> - The TrackPoint driver enables hardware functionality by default.
> - The thinkpad_acpi driver controls whether ACPI doubletap events are
> delivered, using existing hotkey filtering infrastructure.
> - No cross-driver APIs or dual filtering paths are introduced.
>
> Changes in v7:
> - Removed unwanted comments and logs
>
> Changes in v6:
> - Documentation: fix formatting of the doubletap_enable sysfs attribute
> description (separate "Values" list)
>
> Changes in v5:
> - Rename sysfs attribute from doubletap_filter to doubletap_enable to
> reflect actual behavior.
> - Fix inverted logic so events are delivered only when doubletap is
> enabled.
> - Suppress ACPI hotkey delivery instead of injecting or filtering input
> events.
> - Register the sysfs attribute via hotkey_attributes[] instead of
> device_create_file().
> - Drop unnecessary helper wrappers and debug logging.
> - Update Documentation to reflect the new naming and semantics.
>
> Changes in v4:
> - Complete redesign based on reviewer feedback.
> - trackpoint.c: Simplified to only enable doubletap by default.
> - trackpoint.c: Removed all sysfs attributes and global variables.
> - trackpoint.c: Uses firmware ID detection with deny list.
> - thinkpad_acpi.c: Added sysfs interface for kernel-level event control.
> - thinkpad_acpi.c: No cross-driver dependencies.
> - Documentation: Updated to reflect simplified sysfs approach.
>
> Changes in v3:
> - No changes.
>
> Changes in v2:
> - Improved commit messages.
> - Removed unnecessary comments and debug messages.
> - Switched to strstarts() usage.
> - Simplified firmware capability detection logic.
>
> This version addresses the remaining review feedback by correcting the
> naming and logic inversion, aligning sysfs semantics with behavior, and
> fully integrating with existing thinkpad_acpi hotkey handling.
>
> Vishnu Sankar (3):
> input: trackpoint - Enable doubletap by default on capable devices
> platform/x86: thinkpad_acpi: Add sysfs control for TrackPoint
> double-tap
> Documentation: thinkpad-acpi - Document doubletap_enable attribute
>
> .../admin-guide/laptops/thinkpad-acpi.rst | 21 +++++++++
> drivers/input/mouse/trackpoint.c | 45 +++++++++++++++++++
> drivers/input/mouse/trackpoint.h | 5 +++
> drivers/platform/x86/lenovo/thinkpad_acpi.c | 42 ++++++++++++++---
> 4 files changed, 106 insertions(+), 7 deletions(-)
>
> --
> 2.51.0
>
--
Regards,
Vishnu Sankar
^ permalink raw reply
* Re: [PATCH 01/37] PCI/MSI: Add Devres managed IRQ vectors allocation
From: Jakub Kicinski @ 2026-02-24 0:04 UTC (permalink / raw)
To: Shawn Lin
Cc: Bjorn Helgaas, Vaibhaav Ram T . L, Kumaravel Thiagarajan, Even Xu,
Xinpeng Sun, Srinivas Pandruvada, Jiri Kosina, Alexandre Belloni,
Zhou Wang, Longfang Liu, Vinod Koul, Lee Jones, Jijie Shao,
Jian Shen, Sunil Goutham, Andrew Lunn, Heiner Kallweit,
David S . Miller, Jeff Hugo, Oded Gabbay, Maciej Falkowski,
Karol Wachowski, Min Ma, Lizhi Hou, Andreas Noever,
Mika Westerberg, Tomasz Jeznach, Will Deacon, Xinliang Liu,
Tian Tao, Davidlohr Bueso, Jonathan Cameron, Srujana Challa,
Bharat Bhushan, Antoine Tenart, Herbert Xu, Raag Jadav,
Hans de Goede, Greg Kroah-Hartman, Jiri Slaby, Andy Shevchenko,
Manivannan Sadhasivam, Mika Westerberg, Andi Shyti,
Robert Richter, Mark Brown, Nirmal Patel, Kurt Schwemmer,
Logan Gunthorpe, Linus Walleij, Bartosz Golaszewski, Sakari Ailus,
Bingbu Cao, Ulf Hansson, Arnd Bergmann, Benjamin Tissoires,
linux-input, linux-i3c, dmaengine, Philipp Stanner, netdev,
nic_swsd, linux-arm-msm, dri-devel, linux-usb, iommu, linux-riscv,
David Airlie, Simona Vetter, linux-cxl, linux-crypto,
platform-driver-x86, linux-serial, mhi, Andy Shevchenko,
Jan Dabros, linux-i2c, Daniel Mack, Haojian Zhuang, linux-spi,
Jonathan Derrick, linux-pci, linux-gpio, Mauro Carvalho Chehab,
linux-media, linux-mmc
In-Reply-To: <1771860581-82092-2-git-send-email-shawn.lin@rock-chips.com>
On Mon, 23 Feb 2026 23:29:40 +0800 Shawn Lin wrote:
> pcim_alloc_irq_vectors() and pcim_alloc_irq_vectors_affinity() are created for
> pci device drivers which rely on the devres machinery to help cleanup the IRQ
> vectors.
If you can please add this API with just a few users, and then convert
remaining users via the subsystem trees in the next cycle.
There's no need to risk wasting maintainer time on conflicts with
conversions like this.
^ permalink raw reply
* Re: [PATCH] ARM: dts: qcom: msm8960: expressatt: Add coreriver,tc360-touchkey
From: Rudraksha Gupta @ 2026-02-24 1:00 UTC (permalink / raw)
To: Dmitry Torokhov
Cc: Konrad Dybcio, Bjorn Andersson, Konrad Dybcio, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, linux-arm-msm, devicetree,
linux-kernel, beomho.seo, jcsing.lee, linux-input,
nick.reitemeyer
In-Reply-To: <aZvPUn2RxUHDahfO@google.com>
On 2/22/26 19:54, Dmitry Torokhov wrote:
> Hi Rudraksha,
>
> On Thu, Feb 19, 2026 at 08:33:43PM -0800, Rudraksha Gupta wrote:
>> Hello all,
>>
>>
>> Top posting for once (context below).
>>
>> Not too sure what the next steps are to get the tm2 touchkey in. Should I
>> resend the patch, contact someone else that can help provide guidance, or
>> something else?
>>
>>
>> Adding Dmitry Torokhov (official maintainer) and Nick Reitemeyer (person who
>> introduced this variant).
> Sorry, I am not sure what the question is... It seems that you made the
> driver work without any additional changes?
I believe this patch is blocked on Konrad's comment:
> This driver mentions a register called CYPRESS_MODULE_VER - maybe
it could help confirm the model?
This was in response to me saying that the "coreriver,tc360-touchkey"
tm2 variant works as is on my device, but I can't tell for sure if this
is actually the variant that is on my device. There isn't really any
documentation for how this peripheral works and I was primarily relying
on others in this thread to provide details to confirm that this is the
actual variant being used.
If I'm mistaken that this is a blocker, please let me know.
Thanks,
Rudraksha
^ permalink raw reply
* [PATCH v5 00/16] HID: Add Legion Go and Go S Drivers
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
This series adds configuration driver support for the Legion Go S,
Legion Go, and Legion Go 2 built-in controller HID interfaces. This
allows for configuring hardware specific attributes such as the auso
sleep timeout, rumble intensity, etc. non-configuration reports are
forwarded to the HID subsystem to ensure no loss of functionality in
userspace. Basic gamepad functionality is provided through xpad, while
advanced features are currently only implemented in userspace daemons
such as InputPlumber[1]. I plan to move this functionality into the
kernel in a later patch series.
Three new device.h macros are added that solve a fairly specific
problem. Many of the attributes need to have the same name as other
attributes when they are in separate attribute subdirectories. The
previous version of this series, along with the upcoming his-asus-ally
driver[2] use this macro to simplify the sysfs by removing redundancy.
An upcoming out of tree driver for the Zotac Zone [3] also found this
macro to be useful. This greatly reduces the path length and term
redundancy of file paths in the sysfs, while also allowing for cleaner
subdirectories that are grouped by functionality. Rather than carry the
same macro in four drivers, it seems beneficial to me that we include the
macro with the other device macros.
A new HID uevent property is also added, HID_FIRMWARE_VERSION, so as to
permit fwupd to read the firmware version of the Go S HID interface without
detaching the kernel driver.
Finally, there are some checkpatch warnings that will need to be supressed:
WARNING: ENOSYS means 'invalid syscall nr' and nothing else
1292: FILE: drivers/hid/lenovo-legos-hid/lenovo-legos-hid-config.c:1085:
+ case -ENOSYS: /* during rmmod -ENOSYS is expected */
This error handling case was added as it is experienced in the real world
when the driver is rmmod. The LED subsystem produces this error code in
its legacy code and this is not a new novel use of -ENOSYS, we are simply
catching the case to avoid spurious errors in dmesg when the drivers are
removed.
[1]: https://github.com/ShadowBlip/InputPlumber/tree/main/src/drivers/lego
[2]: https://lore.kernel.org/all/20240806081212.56860-1-luke@ljones.dev/
[3]: https://github.com/flukejones/linux/tree/wip/zotac-zone-6.15/drivers/hid/zotac-zone-hid
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
Change Log
v5:
- Make all RO attributes cache the data during probe using delayed
work for both drivers. All RW attributes are read in realtime to
ensure they match the device current state in the event of firmware
reset or a userspace application.
- Fix endianness of version strings and print as hex for Go driver.
- Remove reset__esume function. It was not being hit as the MCU of
both devices disconnects of suspend, forcing a reinit of the driver.
Udev or userpsace will need to set the OS Mode upon resume.
v4: https://lore.kernel.org/linux-input/20260220070533.4083667-1-derekjohn.clark@gmail.com/
- Use dmabuf allocated per request for both drivers instead of a devm
preallocated buffer that is reused. This solves a bug where some
attributes couldn't be restored without manual writing after resume.
- Reduce the number of quirks and flags in the Go S init to only those
necessary. Previously they were duplicated from the Go driver but
everything except HID_CONNECT_HIDRAW was found to be unnessary
during operational testing.
- Clean up formatting for debug prints in Go S driver.
- Fix bugs in RGB driver for Go that caused the effect to switch to
solid when the speed or brightness was changed.
- Remove extraneous setting of os_mode member of drvdata when setting
os_mode. It will be read from the hardware in _show.
v3: https://lore.kernel.org/linux-input/20260124014907.991265-1-derekjohn.clark@gmail.com/
- Fix Documentation formatting by removing extra + characters.
- Fix bugs in hid-lenovo-go-s IMU & TP RO attributes being tied to the
wrong _show function.
- Rename enume os_mode_index to os_mode_types_index to fix collision
with os_mode_index attribute.
- Remove accidental rename for enabled->enable attributes in patch 4
- Add SOB for Mario in patch 10 as Co-Developer.
v2: https://lore.kernel.org/linux-input/20251229031753.581664-1-derekjohn.clark@gmail.com/
- Break up adding the Go S driver into feature specific patches.
- Rename Go S driver from lenovo-legos-hid to hid-lenovo-go-s and move
it out of a subdirectory.
- Drop the arbitrary uevent properties patch.
- Add Go series driver.
- Move DEVICE_ATTR_NAMED macros to device.h.
v1: https://lore.kernel.org/linux-input/20250703004943.515919-1-derekjohn.clark@gmail.com/
Derek J. Clark (15):
include: device.h: Add named device attributes
HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver
HID: hid-lenovo-go: Add Feature Status Attributes
HID: hid-lenovo-go: Add Rumble and Haptic Settings
HID: hid-lenovo-go: Add FPS Mode DPI settings
HID: hid-lenovo-go: Add RGB LED control interface
HID: hid-lenovo-go: Add Calibration Settings
HID: hid-lenovo-go: Add OS Mode Toggle
HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver
HID: hid-lenovo-go-s: Add MCU ID Attribute
HID: hid-lenovo-go-s: Add Feature Status Attributes
HID: hid-lenovo-go-s: Add Touchpad Mode Attributes
HID: hid-lenovo-go-s: Add RGB LED control interface
HID: hid-lenovo-go-s: Add IMU and Touchpad RO Attributes
HID: Add documentation for Lenovo Legion Go drivers
Mario Limonciello (1):
HID: Include firmware version in the uevent
.../ABI/testing/sysfs-driver-hid-lenovo-go | 724 +++++
.../ABI/testing/sysfs-driver-hid-lenovo-go-s | 304 ++
MAINTAINERS | 11 +
drivers/hid/Kconfig | 24 +
drivers/hid/Makefile | 2 +
drivers/hid/hid-core.c | 5 +
drivers/hid/hid-ids.h | 7 +
drivers/hid/hid-lenovo-go-s.c | 1509 ++++++++++
drivers/hid/hid-lenovo-go.c | 2497 +++++++++++++++++
include/linux/device.h | 46 +
include/linux/hid.h | 1 +
11 files changed, 5130 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
create mode 100644 drivers/hid/hid-lenovo-go-s.c
create mode 100644 drivers/hid/hid-lenovo-go.c
--
2.52.0
^ permalink raw reply
* [PATCH v5 01/16] include: device.h: Add named device attributes
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds DEVICE_ATTR_[RW|RO|WO]_NAMED macros for adding attributes that
reuse the same sysfs name in a driver under separate subdirectories.
When dealing with some devices it can be useful to be able to reuse
the same name for similar attributes under a different subdirectory.
For example, a single logical HID endpoint may provide a configuration
interface for multiple physical devices. In such a case it is useful to
provide symmetrical attribute names under different subdirectories on
the configuration device. The Lenovo Legion Go is one such device,
providing configuration to a detachable left controller, detachable
right controller, the wireless transmission dongle, and the MCU. It is
therefore beneficial to treat each of these as individual devices in
the driver, providing a subdirectory for each physical device in the
sysfs. As some attributes are reused by each physical device, it
provides a much cleaner interface if the same driver can reuse the same
attribute name in sysfs while uniquely distinguishing the store/show
functions in the driver, rather than repeat string portions.
Example new WO attrs:
ATTRS{left_handle/reset}=="(not readable)"
ATTRS{right_handle/reset}=="(not readable)"
ATTRS{tx_dongle/reset}=="(not readable)"
vs old WO attrs in a subdir:
ATTRS{left_handle/left_handle_reset}=="(not readable)"
ATTRS{right_handle/right_handle_reset}=="(not readable)"
ATTRS{tx_dongle/tx_dongle_reset}=="(not readable)"
or old WO attrs with no subdir:
ATTRS{left_handle_reset}=="(not readable)"
ATTRS{right_handle_reset}=="(not readable)"
ATTRS{tx_dongle_reset}=="(not readable)"
While the third option is usable, it doesn't logically break up the
physical devices and creates a device directory with over 80 attributes
once all attrs are defined.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v4:
- Use dmabuf per request instead of devm allocated static buffer.
Resolves bug with side effects during suspend.
---
include/linux/device.h | 46 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/include/linux/device.h b/include/linux/device.h
index 0be95294b6e61..381463baed6d3 100644
--- a/include/linux/device.h
+++ b/include/linux/device.h
@@ -189,6 +189,22 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr,
#define DEVICE_ATTR_ADMIN_RW(_name) \
struct device_attribute dev_attr_##_name = __ATTR_RW_MODE(_name, 0600)
+/**
+ * DEVICE_ATTR_RW_NAMED - Define a read-write device attribute with a sysfs name
+ * that differs from the function name.
+ * @_name: Attribute function preface
+ * @_attrname: Attribute name as it wil be exposed in the sysfs.
+ *
+ * Like DEVICE_ATTR_RW(), but allows for reusing names under separate paths in
+ * the same driver.
+ */
+#define DEVICE_ATTR_RW_NAMED(_name, _attrname) \
+ struct device_attribute dev_attr_##_name = { \
+ .attr = { .name = _attrname, .mode = 0644 }, \
+ .show = _name##_show, \
+ .store = _name##_store, \
+ }
+
/**
* DEVICE_ATTR_RO - Define a readable device attribute.
* @_name: Attribute name.
@@ -207,6 +223,21 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr,
#define DEVICE_ATTR_ADMIN_RO(_name) \
struct device_attribute dev_attr_##_name = __ATTR_RO_MODE(_name, 0400)
+/**
+ * DEVICE_ATTR_RO_NAMED - Define a read-only device attribute with a sysfs name
+ * that differs from the function name.
+ * @_name: Attribute function preface
+ * @_attrname: Attribute name as it wil be exposed in the sysfs.
+ *
+ * Like DEVICE_ATTR_RO(), but allows for reusing names under separate paths in
+ * the same driver.
+ */
+#define DEVICE_ATTR_RO_NAMED(_name, _attrname) \
+ struct device_attribute dev_attr_##_name = { \
+ .attr = { .name = _attrname, .mode = 0444 }, \
+ .show = _name##_show, \
+ }
+
/**
* DEVICE_ATTR_WO - Define an admin-only writable device attribute.
* @_name: Attribute name.
@@ -216,6 +247,21 @@ ssize_t device_show_string(struct device *dev, struct device_attribute *attr,
#define DEVICE_ATTR_WO(_name) \
struct device_attribute dev_attr_##_name = __ATTR_WO(_name)
+/**
+ * DEVICE_ATTR_WO_NAMED - Define a read-only device attribute with a sysfs name
+ * that differs from the function name.
+ * @_name: Attribute function preface
+ * @_attrname: Attribute name as it wil be exposed in the sysfs.
+ *
+ * Like DEVICE_ATTR_WO(), but allows for reusing names under separate paths in
+ * the same driver.
+ */
+#define DEVICE_ATTR_WO_NAMED(_name, _attrname) \
+ struct device_attribute dev_attr_##_name = { \
+ .attr = { .name = _attrname, .mode = 0200 }, \
+ .store = _name##_store, \
+ }
+
/**
* DEVICE_ULONG_ATTR - Define a device attribute backed by an unsigned long.
* @_name: Attribute name.
--
2.52.0
^ permalink raw reply related
* [PATCH v5 02/16] HID: hid-lenovo-go: Add Lenovo Legion Go Series HID Driver
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds initial framework for a new HID driver, hid-lenovo-go, along with
attributes that report the firmware and hardware version for each
component of the HID device, of which there are 4 parts: The MCU, the
transmission dongle, the left "handle" controller half, and the right
"handle" controller half. Each of these devices are provided an attribute
group to contain its device specific attributes. Additionally, the touchpad
device attributes are logically separated from the other components in
another attribute group.
This driver primarily provides access to the configurable settings of the
Lenovo Legion Go and Lenovo Legion Go 2 controllers running the latest
firmware. As previously noted, the Legion Go controllers recently had a
firmware update[1] which switched from the original "SepentiaUSB" protocol
to a brand new protocol for the Go 2, primarily to ensure backwards and
forwards compatibility between the Go and Go 2 devices. As part of that
update the PIDs for the controllers were changed, so there is no risk of
this driver attaching to controller firmware that it doesn't support.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v5:
- Make version attributes static, retrieve them using delayed work
during probe.
- Fix endianness of version strings and print as hex.
v3:
- Add hid-lenovo.c and Mark Pearson to LENOVO HID DRIVERS entry in MAINTAINERS
---
MAINTAINERS | 8 +
drivers/hid/Kconfig | 12 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-lenovo-go.c | 914 ++++++++++++++++++++++++++++++++++++
5 files changed, 938 insertions(+)
create mode 100644 drivers/hid/hid-lenovo-go.c
diff --git a/MAINTAINERS b/MAINTAINERS
index e087673237636..9db6292c62ec6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14313,6 +14313,14 @@ L: platform-driver-x86@vger.kernel.org
S: Maintained
F: drivers/platform/x86/lenovo/wmi-hotkey-utilities.c
+LENOVO HID drivers
+M: Derek J. Clark <derekjohn.clark@gmail.com>
+M: Mark Pearson <mpearson-lenovo@squebb.ca>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-lenovo-go.c
+F: drivers/hid/hid-lenovo.c
+
LETSKETCH HID TABLET DRIVER
M: Hans de Goede <hansg@kernel.org>
L: linux-input@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 920a64b66b25b..d6c31a2cbaf3b 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -622,6 +622,18 @@ config HID_LENOVO
- ThinkPad Compact Bluetooth Keyboard with TrackPoint (supports Fn keys)
- ThinkPad Compact USB Keyboard with TrackPoint (supports Fn keys)
+config HID_LENOVO_GO
+ tristate "HID Driver for Lenovo Legion Go Series Controllers"
+ depends on USB_HID
+ select LEDS_CLASS
+ select LEDS_CLASS_MULTICOLOR
+ help
+ Support for Lenovo Legion Go devices with detachable controllers.
+
+ Say Y here to include configuration interface support for the Lenovo Legion Go
+ and Legion Go 2 Handheld Console Controllers. Say M here to compile this
+ driver as a module. The module will be called hid-lenovo-go.
+
config HID_LETSKETCH
tristate "Letsketch WP9620N tablets"
depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 361a7daedeb85..11435bce4e475 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -76,6 +76,7 @@ obj-$(CONFIG_HID_KYE) += hid-kye.o
obj-$(CONFIG_HID_KYSONA) += hid-kysona.o
obj-$(CONFIG_HID_LCPOWER) += hid-lcpower.o
obj-$(CONFIG_HID_LENOVO) += hid-lenovo.o
+obj-$(CONFIG_HID_LENOVO_GO) += hid-lenovo-go.o
obj-$(CONFIG_HID_LETSKETCH) += hid-letsketch.o
obj-$(CONFIG_HID_LOGITECH) += hid-logitech.o
obj-$(CONFIG_HID_LOGITECH) += hid-lg-g15.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 9c2bf584d9f6f..bd41ddbbbee15 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -847,7 +847,10 @@
#define USB_DEVICE_ID_LENOVO_PIXART_USB_MOUSE_602E 0x602e
#define USB_DEVICE_ID_LENOVO_PIXART_USB_MOUSE_6093 0x6093
#define USB_DEVICE_ID_LENOVO_LEGION_GO_DUAL_DINPUT 0x6184
+#define USB_DEVICE_ID_LENOVO_LEGION_GO2_XINPUT 0x61eb
+#define USB_DEVICE_ID_LENOVO_LEGION_GO2_DINPUT 0x61ec
#define USB_DEVICE_ID_LENOVO_LEGION_GO2_DUAL_DINPUT 0x61ed
+#define USB_DEVICE_ID_LENOVO_LEGION_GO2_FPS 0x61ee
#define USB_VENDOR_ID_LETSKETCH 0x6161
#define USB_DEVICE_ID_WP9620N 0x4d15
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
new file mode 100644
index 0000000000000..dbb7a2e036f23
--- /dev/null
+++ b/drivers/hid/hid-lenovo-go.c
@@ -0,0 +1,914 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for Lenovo Legion Go series gamepads.
+ *
+ * Copyright (c) 2026 Derek J. Clark <derekjohn.clark@gmail.com>
+ * Copyright (c) 2026 Valve Corporation
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/device/devres.h>
+#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/kstrtox.h>
+#include <linux/mutex.h>
+#include <linux/printk.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <linux/workqueue_types.h>
+
+#include "hid-ids.h"
+
+#define GO_GP_INTF_IN 0x83
+#define GO_OUTPUT_REPORT_ID 0x05
+#define GO_GP_RESET_SUCCESS 0x01
+#define GO_PACKET_SIZE 64
+
+struct hid_go_cfg {
+ struct delayed_work go_cfg_setup;
+ struct completion send_cmd_complete;
+ struct hid_device *hdev;
+ struct mutex cfg_mutex; /*ensure single synchronous output report*/
+ u32 gp_left_version_firmware;
+ u8 gp_left_version_gen;
+ u32 gp_left_version_hardware;
+ u32 gp_left_version_product;
+ u32 gp_left_version_protocol;
+ u32 gp_right_version_firmware;
+ u8 gp_right_version_gen;
+ u32 gp_right_version_hardware;
+ u32 gp_right_version_product;
+ u32 gp_right_version_protocol;
+ u32 mcu_version_firmware;
+ u8 mcu_version_gen;
+ u32 mcu_version_hardware;
+ u32 mcu_version_product;
+ u32 mcu_version_protocol;
+ u32 tx_dongle_version_firmware;
+ u8 tx_dongle_version_gen;
+ u32 tx_dongle_version_hardware;
+ u32 tx_dongle_version_product;
+ u32 tx_dongle_version_protocol;
+} drvdata;
+
+struct go_cfg_attr {
+ u8 index;
+};
+
+struct command_report {
+ u8 report_id;
+ u8 id;
+ u8 cmd;
+ u8 sub_cmd;
+ u8 device_type;
+ u8 data[59];
+} __packed;
+
+enum command_id {
+ MCU_CONFIG_DATA = 0x00,
+ OS_MODE_DATA = 0x06,
+ GAMEPAD_DATA = 0x3c,
+};
+
+enum mcu_command_index {
+ GET_VERSION_DATA = 0x02,
+ GET_FEATURE_STATUS,
+ SET_FEATURE_STATUS,
+ GET_MOTOR_CFG,
+ SET_MOTOR_CFG,
+ GET_DPI_CFG,
+ SET_DPI_CFG,
+ SET_TRIGGER_CFG = 0x0a,
+ SET_JOYSTICK_CFG = 0x0c,
+ SET_GYRO_CFG = 0x0e,
+ GET_RGB_CFG,
+ SET_RGB_CFG,
+ GET_DEVICE_STATUS = 0xa0,
+
+};
+
+enum dev_type {
+ UNSPECIFIED,
+ USB_MCU,
+ TX_DONGLE,
+ LEFT_CONTROLLER,
+ RIGHT_CONTROLLER,
+};
+
+enum version_data_index {
+ PRODUCT_VERSION = 0x02,
+ PROTOCOL_VERSION,
+ FIRMWARE_VERSION,
+ HARDWARE_VERSION,
+ HARDWARE_GENERATION,
+};
+
+static int hid_go_version_event(struct command_report *cmd_rep)
+{
+ switch (cmd_rep->sub_cmd) {
+ case PRODUCT_VERSION:
+ switch (cmd_rep->device_type) {
+ case USB_MCU:
+ drvdata.mcu_version_product =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case TX_DONGLE:
+ drvdata.tx_dongle_version_product =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_version_product =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_version_product =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ case PROTOCOL_VERSION:
+ switch (cmd_rep->device_type) {
+ case USB_MCU:
+ drvdata.mcu_version_protocol =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case TX_DONGLE:
+ drvdata.tx_dongle_version_protocol =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_version_protocol =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_version_protocol =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ case FIRMWARE_VERSION:
+ switch (cmd_rep->device_type) {
+ case USB_MCU:
+ drvdata.mcu_version_firmware =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case TX_DONGLE:
+ drvdata.tx_dongle_version_firmware =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_version_firmware =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_version_firmware =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ case HARDWARE_VERSION:
+ switch (cmd_rep->device_type) {
+ case USB_MCU:
+ drvdata.mcu_version_hardware =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case TX_DONGLE:
+ drvdata.tx_dongle_version_hardware =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_version_hardware =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_version_hardware =
+ get_unaligned_be32(cmd_rep->data);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ case HARDWARE_GENERATION:
+ switch (cmd_rep->device_type) {
+ case USB_MCU:
+ drvdata.mcu_version_gen = cmd_rep->data[0];
+ return 0;
+ case TX_DONGLE:
+ drvdata.tx_dongle_version_gen = cmd_rep->data[0];
+ return 0;
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_version_gen = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_version_gen = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ default:
+ return -EINVAL;
+ }
+}
+
+static int get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_host_endpoint *ep;
+
+ if (!intf)
+ return -ENODEV;
+
+ ep = intf->cur_altsetting->endpoint;
+ if (!ep)
+ return -ENODEV;
+
+ return ep->desc.bEndpointAddress;
+}
+
+static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct command_report *cmd_rep;
+ int ep, ret;
+
+ if (size != GO_PACKET_SIZE)
+ goto passthrough;
+
+ ep = get_endpoint_address(hdev);
+ if (ep != GO_GP_INTF_IN)
+ goto passthrough;
+
+ cmd_rep = (struct command_report *)data;
+
+ switch (cmd_rep->id) {
+ case MCU_CONFIG_DATA:
+ switch (cmd_rep->cmd) {
+ case GET_VERSION_DATA:
+ ret = hid_go_version_event(cmd_rep);
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ };
+ break;
+ default:
+ goto passthrough;
+ };
+ dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n",
+ GO_PACKET_SIZE, data);
+
+ complete(&drvdata.send_cmd_complete);
+ return ret;
+
+passthrough:
+ /* Forward other HID reports so they generate events */
+ hid_input_report(hdev, HID_INPUT_REPORT, data, size, 1);
+ return 0;
+}
+
+static int mcu_property_out(struct hid_device *hdev, u8 id, u8 command,
+ u8 index, enum dev_type device, u8 *data, size_t len)
+{
+ unsigned char *dmabuf __free(kfree) = NULL;
+ u8 header[] = { GO_OUTPUT_REPORT_ID, id, command, index, device };
+ size_t header_size = ARRAY_SIZE(header);
+ int timeout = 50;
+ int ret;
+
+ if (header_size + len > GO_PACKET_SIZE)
+ return -EINVAL;
+
+ guard(mutex)(&drvdata.cfg_mutex);
+ /* We can't use a devm_alloc reusable buffer without side effects during suspend */
+ dmabuf = kzalloc(GO_PACKET_SIZE, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ memcpy(dmabuf, header, header_size);
+ memcpy(dmabuf + header_size, data, len);
+
+ dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+ GO_PACKET_SIZE, dmabuf);
+
+ ret = hid_hw_output_report(hdev, dmabuf, GO_PACKET_SIZE);
+ if (ret < 0)
+ return ret;
+
+ ret = ret == GO_PACKET_SIZE ? 0 : -EINVAL;
+ if (ret)
+ return ret;
+
+ ret = wait_for_completion_interruptible_timeout(&drvdata.send_cmd_complete,
+ msecs_to_jiffies(timeout));
+
+ if (ret == 0) /* timeout occurred */
+ ret = -EBUSY;
+
+ reinit_completion(&drvdata.send_cmd_complete);
+ return 0;
+}
+
+static ssize_t version_show(struct device *dev, struct device_attribute *attr,
+ char *buf, enum version_data_index index,
+ enum dev_type device_type)
+{
+ ssize_t count = 0;
+
+ switch (index) {
+ case PRODUCT_VERSION:
+ switch (device_type) {
+ case USB_MCU:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.mcu_version_product);
+ break;
+ case TX_DONGLE:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.tx_dongle_version_product);
+ break;
+ case LEFT_CONTROLLER:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.gp_left_version_product);
+ break;
+ case RIGHT_CONTROLLER:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.gp_right_version_product);
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case PROTOCOL_VERSION:
+ switch (device_type) {
+ case USB_MCU:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.mcu_version_protocol);
+ break;
+ case TX_DONGLE:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.tx_dongle_version_protocol);
+ break;
+ case LEFT_CONTROLLER:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.gp_left_version_protocol);
+ break;
+ case RIGHT_CONTROLLER:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.gp_right_version_protocol);
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case FIRMWARE_VERSION:
+ switch (device_type) {
+ case USB_MCU:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.mcu_version_firmware);
+ break;
+ case TX_DONGLE:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.tx_dongle_version_firmware);
+ break;
+ case LEFT_CONTROLLER:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.gp_left_version_firmware);
+ break;
+ case RIGHT_CONTROLLER:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.gp_right_version_firmware);
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case HARDWARE_VERSION:
+ switch (device_type) {
+ case USB_MCU:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.mcu_version_hardware);
+ break;
+ case TX_DONGLE:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.tx_dongle_version_hardware);
+ break;
+ case LEFT_CONTROLLER:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.gp_left_version_hardware);
+ break;
+ case RIGHT_CONTROLLER:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.gp_right_version_hardware);
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case HARDWARE_GENERATION:
+ switch (device_type) {
+ case USB_MCU:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.mcu_version_gen);
+ break;
+ case TX_DONGLE:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.tx_dongle_version_gen);
+ break;
+ case LEFT_CONTROLLER:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.gp_left_version_gen);
+ break;
+ case RIGHT_CONTROLLER:
+ count = sysfs_emit(buf, "%x\n",
+ drvdata.gp_right_version_gen);
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ }
+
+ return count;
+}
+
+#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _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, \
+ _dtype); \
+ } \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_show(dev, attr, buf, _name.index, _dtype); \
+ } \
+ static ssize_t _name##_##_rtype##_show( \
+ struct device *dev, struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_options(dev, attr, buf, _name.index); \
+ } \
+ static DEVICE_ATTR_RW_NAMED(_name, _attrname)
+
+#define LEGO_DEVICE_ATTR_WO(_name, _attrname, _dtype, _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, \
+ _dtype); \
+ } \
+ static DEVICE_ATTR_WO_NAMED(_name, _attrname)
+
+#define LEGO_DEVICE_ATTR_RO(_name, _attrname, _dtype, _group) \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_show(dev, attr, buf, _name.index, _dtype); \
+ } \
+ static DEVICE_ATTR_RO_NAMED(_name, _attrname)
+
+/* Gamepad - MCU */
+struct go_cfg_attr version_product_mcu = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_mcu, "product_version", USB_MCU, version);
+
+struct go_cfg_attr version_protocol_mcu = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_mcu, "protocol_version", USB_MCU, version);
+
+struct go_cfg_attr version_firmware_mcu = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_mcu, "firmware_version", USB_MCU, version);
+
+struct go_cfg_attr version_hardware_mcu = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_mcu, "hardware_version", USB_MCU, version);
+
+struct go_cfg_attr version_gen_mcu = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_mcu, "hardware_generation", USB_MCU, version);
+
+static struct attribute *mcu_attrs[] = {
+ &dev_attr_version_firmware_mcu.attr,
+ &dev_attr_version_gen_mcu.attr,
+ &dev_attr_version_hardware_mcu.attr,
+ &dev_attr_version_product_mcu.attr,
+ &dev_attr_version_protocol_mcu.attr,
+ NULL,
+};
+
+static const struct attribute_group mcu_attr_group = {
+ .attrs = mcu_attrs,
+};
+
+/* Gamepad - TX Dongle */
+struct go_cfg_attr version_product_tx_dongle = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_tx_dongle, "product_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_protocol_tx_dongle = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_tx_dongle, "protocol_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_firmware_tx_dongle = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_tx_dongle, "firmware_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_hardware_tx_dongle = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_tx_dongle, "hardware_version", TX_DONGLE, version);
+
+struct go_cfg_attr version_gen_tx_dongle = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_tx_dongle, "hardware_generation", TX_DONGLE, version);
+
+static struct attribute *tx_dongle_attrs[] = {
+ &dev_attr_version_hardware_tx_dongle.attr,
+ &dev_attr_version_firmware_tx_dongle.attr,
+ &dev_attr_version_gen_tx_dongle.attr,
+ &dev_attr_version_product_tx_dongle.attr,
+ &dev_attr_version_protocol_tx_dongle.attr,
+ NULL,
+};
+
+static const struct attribute_group tx_dongle_attr_group = {
+ .name = "tx_dongle",
+ .attrs = tx_dongle_attrs,
+};
+
+/* Gamepad - Left */
+struct go_cfg_attr version_product_left = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_left, "product_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_protocol_left = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_left, "protocol_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_firmware_left = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_left, "firmware_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_hardware_left = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_left, "hardware_version", LEFT_CONTROLLER, version);
+
+struct go_cfg_attr version_gen_left = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_left, "hardware_generation", LEFT_CONTROLLER, version);
+
+static struct attribute *left_gamepad_attrs[] = {
+ &dev_attr_version_hardware_left.attr,
+ &dev_attr_version_firmware_left.attr,
+ &dev_attr_version_gen_left.attr,
+ &dev_attr_version_product_left.attr,
+ &dev_attr_version_protocol_left.attr,
+ NULL,
+};
+
+static const struct attribute_group left_gamepad_attr_group = {
+ .name = "left_handle",
+ .attrs = left_gamepad_attrs,
+};
+
+/* Gamepad - Right */
+struct go_cfg_attr version_product_right = { PRODUCT_VERSION };
+LEGO_DEVICE_ATTR_RO(version_product_right, "product_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_protocol_right = { PROTOCOL_VERSION };
+LEGO_DEVICE_ATTR_RO(version_protocol_right, "protocol_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_firmware_right = { FIRMWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_firmware_right, "firmware_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_hardware_right = { HARDWARE_VERSION };
+LEGO_DEVICE_ATTR_RO(version_hardware_right, "hardware_version", RIGHT_CONTROLLER, version);
+
+struct go_cfg_attr version_gen_right = { HARDWARE_GENERATION };
+LEGO_DEVICE_ATTR_RO(version_gen_right, "hardware_generation", RIGHT_CONTROLLER, version);
+
+static struct attribute *right_gamepad_attrs[] = {
+ &dev_attr_version_hardware_right.attr,
+ &dev_attr_version_firmware_right.attr,
+ &dev_attr_version_gen_right.attr,
+ &dev_attr_version_product_right.attr,
+ &dev_attr_version_protocol_right.attr,
+ NULL,
+};
+
+static const struct attribute_group right_gamepad_attr_group = {
+ .name = "right_handle",
+ .attrs = right_gamepad_attrs,
+};
+
+/* Touchpad */
+static struct attribute *touchpad_attrs[] = {
+ NULL,
+};
+
+static const struct attribute_group touchpad_attr_group = {
+ .name = "touchpad",
+ .attrs = touchpad_attrs,
+};
+
+static const struct attribute_group *top_level_attr_groups[] = {
+ &mcu_attr_group, &tx_dongle_attr_group,
+ &left_gamepad_attr_group, &right_gamepad_attr_group,
+ &touchpad_attr_group, NULL,
+};
+
+static void cfg_setup(struct work_struct *work)
+{
+ int ret;
+
+ /* MCU Version Attrs */
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ PRODUCT_VERSION, USB_MCU, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve USB_MCU Product Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ PROTOCOL_VERSION, USB_MCU, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve USB_MCU Protocol Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ FIRMWARE_VERSION, USB_MCU, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve USB_MCU Firmware Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ HARDWARE_VERSION, USB_MCU, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve USB_MCU Hardware Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ HARDWARE_GENERATION, USB_MCU, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve USB_MCU Hardware Generation: %i\n", ret);
+ return;
+ }
+
+ /* TX Dongle Version Attrs */
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ PRODUCT_VERSION, TX_DONGLE, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve TX_DONGLE Product Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ PROTOCOL_VERSION, TX_DONGLE, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve TX_DONGLE Protocol Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ FIRMWARE_VERSION, TX_DONGLE, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve TX_DONGLE Firmware Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ HARDWARE_VERSION, TX_DONGLE, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve TX_DONGLE Hardware Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ HARDWARE_GENERATION, TX_DONGLE, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve TX_DONGLE Hardware Generation: %i\n", ret);
+ return;
+ }
+
+ /* Left Handle Version Attrs */
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ PRODUCT_VERSION, LEFT_CONTROLLER, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve LEFT_CONTROLLER Product Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ PROTOCOL_VERSION, LEFT_CONTROLLER, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve LEFT_CONTROLLER Protocol Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ FIRMWARE_VERSION, LEFT_CONTROLLER, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve LEFT_CONTROLLER Firmware Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ HARDWARE_VERSION, LEFT_CONTROLLER, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve LEFT_CONTROLLER Hardware Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ HARDWARE_GENERATION, LEFT_CONTROLLER, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve LEFT_CONTROLLER Hardware Generation: %i\n", ret);
+ return;
+ }
+
+ /* Right Handle Version Attrs */
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ PRODUCT_VERSION, RIGHT_CONTROLLER, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve RIGHT_CONTROLLER Product Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ PROTOCOL_VERSION, RIGHT_CONTROLLER, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve RIGHT_CONTROLLER Protocol Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ FIRMWARE_VERSION, RIGHT_CONTROLLER, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve RIGHT_CONTROLLER Firmware Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ HARDWARE_VERSION, RIGHT_CONTROLLER, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve RIGHT_CONTROLLER Hardware Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ HARDWARE_GENERATION, RIGHT_CONTROLLER, 0, 0);
+ if (ret < 0) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve RIGHT_CONTROLLER Hardware Generation: %i\n", ret);
+ return;
+ }
+}
+
+static int hid_go_cfg_probe(struct hid_device *hdev,
+ const struct hid_device_id *_id)
+{
+ unsigned char *buf;
+ int ret;
+
+ buf = devm_kzalloc(&hdev->dev, GO_PACKET_SIZE, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ hid_set_drvdata(hdev, &drvdata);
+ drvdata.hdev = hdev;
+ mutex_init(&drvdata.cfg_mutex);
+
+ ret = sysfs_create_groups(&hdev->dev.kobj, top_level_attr_groups);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret,
+ "Failed to create gamepad configuration attributes\n");
+ return ret;
+ }
+
+ init_completion(&drvdata.send_cmd_complete);
+
+ /* Executing calls prior to returning from probe will lock the MCU. Schedule
+ * initial data call after probe has completed and MCU can accept calls.
+ */
+ INIT_DELAYED_WORK(&drvdata.go_cfg_setup, &cfg_setup);
+ ret = schedule_delayed_work(&drvdata.go_cfg_setup, msecs_to_jiffies(2));
+ if (!ret) {
+ dev_err(&hdev->dev,
+ "Failed to schedule startup delayed work\n");
+ return -ENODEV;
+ }
+ return 0;
+}
+
+static void hid_go_cfg_remove(struct hid_device *hdev)
+{
+ guard(mutex)(&drvdata.cfg_mutex);
+ sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups);
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ hid_set_drvdata(hdev, NULL);
+}
+
+static int hid_go_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ int ret, ep;
+
+ hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;
+
+ ret = hid_parse(hdev);
+ if (ret) {
+ hid_err(hdev, "Parse failed\n");
+ return ret;
+ }
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret) {
+ hid_err(hdev, "Failed to start HID device\n");
+ return ret;
+ }
+
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_err(hdev, "Failed to open HID device\n");
+ hid_hw_stop(hdev);
+ return ret;
+ }
+
+ ep = get_endpoint_address(hdev);
+ if (ep != GO_GP_INTF_IN) {
+ dev_dbg(&hdev->dev, "Started interface %x as generic HID device\n", ep);
+ return 0;
+ }
+
+ ret = hid_go_cfg_probe(hdev, id);
+ if (ret)
+ dev_err_probe(&hdev->dev, ret, "Failed to start configuration interface\n");
+
+ dev_dbg(&hdev->dev, "Started Legion Go HID Device: %x\n", ep);
+
+ return ret;
+}
+
+static void hid_go_remove(struct hid_device *hdev)
+{
+ int ep = get_endpoint_address(hdev);
+
+ if (ep <= 0)
+ return;
+
+ switch (ep) {
+ case GO_GP_INTF_IN:
+ hid_go_cfg_remove(hdev);
+ break;
+ default:
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ break;
+ }
+}
+
+static const struct hid_device_id hid_go_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+ USB_DEVICE_ID_LENOVO_LEGION_GO2_XINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+ USB_DEVICE_ID_LENOVO_LEGION_GO2_DINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+ USB_DEVICE_ID_LENOVO_LEGION_GO2_DUAL_DINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
+ USB_DEVICE_ID_LENOVO_LEGION_GO2_FPS) },
+ {}
+};
+MODULE_DEVICE_TABLE(hid, hid_go_devices);
+
+static struct hid_driver hid_lenovo_go = {
+ .name = "hid-lenovo-go",
+ .id_table = hid_go_devices,
+ .probe = hid_go_probe,
+ .remove = hid_go_remove,
+ .raw_event = hid_go_raw_event,
+};
+module_hid_driver(hid_lenovo_go);
+
+MODULE_AUTHOR("Derek J. Clark");
+MODULE_DESCRIPTION("HID Driver for Lenovo Legion Go Series Gamepads.");
+MODULE_LICENSE("GPL");
--
2.52.0
^ permalink raw reply related
* [PATCH v5 03/16] HID: hid-lenovo-go: Add Feature Status Attributes
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds various feature status indicators and toggles to hid-lenovo-go,
including the FPS mode switch setting, touchpad enable toggle, handle
automatic sleep timer, etc.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 396 +++++++++++++++++++++++++++++++++++-
1 file changed, 395 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index dbb7a2e036f23..62a997fa06276 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -39,21 +39,31 @@ struct hid_go_cfg {
struct completion send_cmd_complete;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
+ u8 fps_mode;
+ u8 gp_left_auto_sleep_time;
u32 gp_left_version_firmware;
u8 gp_left_version_gen;
u32 gp_left_version_hardware;
u32 gp_left_version_product;
u32 gp_left_version_protocol;
+ u8 gp_mode;
+ u8 gp_right_auto_sleep_time;
u32 gp_right_version_firmware;
u8 gp_right_version_gen;
u32 gp_right_version_hardware;
u32 gp_right_version_product;
u32 gp_right_version_protocol;
+ u8 imu_left_bypass_en;
+ u8 imu_left_sensor_en;
+ u8 imu_right_bypass_en;
+ u8 imu_right_sensor_en;
u32 mcu_version_firmware;
u8 mcu_version_gen;
u32 mcu_version_hardware;
u32 mcu_version_product;
u32 mcu_version_protocol;
+ u8 rgb_en;
+ u8 tp_en;
u32 tx_dongle_version_firmware;
u8 tx_dongle_version_gen;
u32 tx_dongle_version_hardware;
@@ -105,6 +115,18 @@ enum dev_type {
RIGHT_CONTROLLER,
};
+enum enabled_status_index {
+ FEATURE_UNKNOWN,
+ FEATURE_ENABLED,
+ FEATURE_DISABLED,
+};
+
+static const char *const enabled_status_text[] = {
+ [FEATURE_UNKNOWN] = "unknown",
+ [FEATURE_ENABLED] = "true",
+ [FEATURE_DISABLED] = "false",
+};
+
enum version_data_index {
PRODUCT_VERSION = 0x02,
PROTOCOL_VERSION,
@@ -113,6 +135,41 @@ enum version_data_index {
HARDWARE_GENERATION,
};
+enum feature_status_index {
+ FEATURE_RESET_GAMEPAD = 0x02,
+ FEATURE_IMU_BYPASS,
+ FEATURE_IMU_ENABLE = 0x05,
+ FEATURE_TOUCHPAD_ENABLE = 0x07,
+ FEATURE_LIGHT_ENABLE,
+ FEATURE_AUTO_SLEEP_TIME,
+ FEATURE_FPS_SWITCH_STATUS = 0x0b,
+ FEATURE_GAMEPAD_MODE = 0x0e,
+};
+
+enum fps_switch_status_index {
+ FPS_STATUS_UNKNOWN,
+ GAMEPAD,
+ FPS,
+};
+
+static const char *const fps_switch_text[] = {
+ [FPS_STATUS_UNKNOWN] = "unknown",
+ [GAMEPAD] = "gamepad",
+ [FPS] = "fps",
+};
+
+enum gamepad_mode_index {
+ GAMEPAD_MODE_UNKNOWN,
+ XINPUT,
+ DINPUT,
+};
+
+static const char *const gamepad_mode_text[] = {
+ [GAMEPAD_MODE_UNKNOWN] = "unknown",
+ [XINPUT] = "xinput",
+ [DINPUT] = "dinput",
+};
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -222,6 +279,71 @@ static int hid_go_version_event(struct command_report *cmd_rep)
}
}
+static int hid_go_feature_status_event(struct command_report *cmd_rep)
+{
+ switch (cmd_rep->sub_cmd) {
+ case FEATURE_RESET_GAMEPAD:
+ return 0;
+ case FEATURE_IMU_ENABLE:
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ drvdata.imu_left_sensor_en = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.imu_right_sensor_en = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+ case FEATURE_IMU_BYPASS:
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ drvdata.imu_left_bypass_en = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.imu_right_bypass_en = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+ break;
+ case FEATURE_LIGHT_ENABLE:
+ drvdata.rgb_en = cmd_rep->data[0];
+ return 0;
+ case FEATURE_AUTO_SLEEP_TIME:
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_auto_sleep_time = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_auto_sleep_time = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+ break;
+ case FEATURE_TOUCHPAD_ENABLE:
+ drvdata.tp_en = cmd_rep->data[0];
+ return 0;
+ case FEATURE_GAMEPAD_MODE:
+ drvdata.gp_mode = cmd_rep->data[0];
+ return 0;
+ case FEATURE_FPS_SWITCH_STATUS:
+ drvdata.fps_mode = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int hid_go_set_event_return(struct command_report *cmd_rep)
+{
+ if (cmd_rep->data[0] != 0)
+ return -EIO;
+
+ return 0;
+}
+
static int get_endpoint_address(struct hid_device *hdev)
{
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -258,6 +380,12 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_VERSION_DATA:
ret = hid_go_version_event(cmd_rep);
break;
+ case GET_FEATURE_STATUS:
+ ret = hid_go_feature_status_event(cmd_rep);
+ break;
+ case SET_FEATURE_STATUS:
+ ret = hid_go_set_event_return(cmd_rep);
+ break;
default:
ret = -EINVAL;
break;
@@ -442,6 +570,195 @@ static ssize_t version_show(struct device *dev, struct device_attribute *attr,
return count;
}
+static ssize_t feature_status_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count,
+ enum feature_status_index index,
+ enum dev_type device_type)
+{
+ size_t size = 1;
+ u8 val = 0;
+ int ret;
+
+ switch (index) {
+ case FEATURE_IMU_ENABLE:
+ case FEATURE_IMU_BYPASS:
+ case FEATURE_LIGHT_ENABLE:
+ case FEATURE_TOUCHPAD_ENABLE:
+ ret = sysfs_match_string(enabled_status_text, buf);
+ val = ret;
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ ret = kstrtou8(buf, 10, &val);
+ break;
+ case FEATURE_RESET_GAMEPAD:
+ ret = kstrtou8(buf, 10, &val);
+ if (val != GO_GP_RESET_SUCCESS)
+ return -EINVAL;
+ break;
+ case FEATURE_FPS_SWITCH_STATUS:
+ ret = sysfs_match_string(fps_switch_text, buf);
+ val = ret;
+ break;
+ case FEATURE_GAMEPAD_MODE:
+ ret = sysfs_match_string(gamepad_mode_text, buf);
+ val = ret;
+ break;
+ default:
+ return -EINVAL;
+ };
+
+ if (ret < 0)
+ return ret;
+
+ if (!val)
+ size = 0;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA,
+ SET_FEATURE_STATUS, index, device_type, &val,
+ size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t feature_status_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum feature_status_index index,
+ enum dev_type device_type)
+{
+ ssize_t count = 0;
+ int ret;
+ u8 i;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA,
+ GET_FEATURE_STATUS, index, device_type, 0, 0);
+ if (ret)
+ return ret;
+
+ switch (index) {
+ case FEATURE_IMU_ENABLE:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ i = drvdata.imu_left_sensor_en;
+ break;
+ case RIGHT_CONTROLLER:
+ i = drvdata.imu_right_sensor_en;
+ break;
+ default:
+ return -EINVAL;
+ }
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case FEATURE_IMU_BYPASS:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ i = drvdata.imu_left_bypass_en;
+ break;
+ case RIGHT_CONTROLLER:
+ i = drvdata.imu_right_bypass_en;
+ break;
+ default:
+ return -EINVAL;
+ }
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case FEATURE_LIGHT_ENABLE:
+ i = drvdata.rgb_en;
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case FEATURE_TOUCHPAD_ENABLE:
+ i = drvdata.tp_en;
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ i = drvdata.gp_left_auto_sleep_time;
+ break;
+ case RIGHT_CONTROLLER:
+ i = drvdata.gp_right_auto_sleep_time;
+ break;
+ default:
+ return -EINVAL;
+ };
+ count = sysfs_emit(buf, "%u\n", i);
+ break;
+ case FEATURE_FPS_SWITCH_STATUS:
+ i = drvdata.fps_mode;
+ if (i >= ARRAY_SIZE(fps_switch_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", fps_switch_text[i]);
+ break;
+ case FEATURE_GAMEPAD_MODE:
+ i = drvdata.gp_mode;
+ if (i >= ARRAY_SIZE(gamepad_mode_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", gamepad_mode_text[i]);
+ break;
+ default:
+ return -EINVAL;
+ };
+
+ return count;
+}
+
+static ssize_t feature_status_options(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum feature_status_index index)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ switch (index) {
+ case FEATURE_IMU_ENABLE:
+ case FEATURE_IMU_BYPASS:
+ case FEATURE_LIGHT_ENABLE:
+ case FEATURE_TOUCHPAD_ENABLE:
+ for (i = 1; i < ARRAY_SIZE(enabled_status_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ enabled_status_text[i]);
+ }
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ return sysfs_emit(buf, "0-255\n");
+ case FEATURE_FPS_SWITCH_STATUS:
+ for (i = 1; i < ARRAY_SIZE(fps_switch_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ fps_switch_text[i]);
+ }
+ break;
+ case FEATURE_GAMEPAD_MODE:
+ for (i = 1; i < ARRAY_SIZE(gamepad_mode_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ gamepad_mode_text[i]);
+ }
+ break;
+ default:
+ return -EINVAL;
+ };
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \
static ssize_t _name##_store(struct device *dev, \
struct device_attribute *attr, \
@@ -496,7 +813,22 @@ LEGO_DEVICE_ATTR_RO(version_hardware_mcu, "hardware_version", USB_MCU, version);
struct go_cfg_attr version_gen_mcu = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_mcu, "hardware_generation", USB_MCU, version);
+struct go_cfg_attr fps_switch_status = { FEATURE_FPS_SWITCH_STATUS };
+LEGO_DEVICE_ATTR_RO(fps_switch_status, "fps_switch_status", UNSPECIFIED,
+ feature_status);
+
+struct go_cfg_attr gamepad_mode = { FEATURE_GAMEPAD_MODE };
+LEGO_DEVICE_ATTR_RW(gamepad_mode, "mode", UNSPECIFIED, index, feature_status);
+static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");
+
+struct go_cfg_attr reset_mcu = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_WO(reset_mcu, "reset_mcu", USB_MCU, feature_status);
+
static struct attribute *mcu_attrs[] = {
+ &dev_attr_fps_switch_status.attr,
+ &dev_attr_gamepad_mode.attr,
+ &dev_attr_gamepad_mode_index.attr,
+ &dev_attr_reset_mcu.attr,
&dev_attr_version_firmware_mcu.attr,
&dev_attr_version_gen_mcu.attr,
&dev_attr_version_hardware_mcu.attr,
@@ -525,7 +857,11 @@ LEGO_DEVICE_ATTR_RO(version_hardware_tx_dongle, "hardware_version", TX_DONGLE, v
struct go_cfg_attr version_gen_tx_dongle = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_tx_dongle, "hardware_generation", TX_DONGLE, version);
+struct go_cfg_attr reset_tx_dongle = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_RO(reset_tx_dongle, "reset", TX_DONGLE, feature_status);
+
static struct attribute *tx_dongle_attrs[] = {
+ &dev_attr_reset_tx_dongle.attr,
&dev_attr_version_hardware_tx_dongle.attr,
&dev_attr_version_firmware_tx_dongle.attr,
&dev_attr_version_gen_tx_dongle.attr,
@@ -555,7 +891,33 @@ LEGO_DEVICE_ATTR_RO(version_hardware_left, "hardware_version", LEFT_CONTROLLER,
struct go_cfg_attr version_gen_left = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_left, "hardware_generation", LEFT_CONTROLLER, version);
+struct go_cfg_attr auto_sleep_time_left = { FEATURE_AUTO_SLEEP_TIME };
+LEGO_DEVICE_ATTR_RW(auto_sleep_time_left, "auto_sleep_time", LEFT_CONTROLLER,
+ range, feature_status);
+static DEVICE_ATTR_RO_NAMED(auto_sleep_time_left_range,
+ "auto_sleep_time_range");
+
+struct go_cfg_attr imu_bypass_left = { FEATURE_IMU_BYPASS };
+LEGO_DEVICE_ATTR_RW(imu_bypass_left, "imu_bypass_enabled", LEFT_CONTROLLER,
+ index, feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_bypass_left_index, "imu_bypass_enabled_index");
+
+struct go_cfg_attr imu_enabled_left = { FEATURE_IMU_ENABLE };
+LEGO_DEVICE_ATTR_RW(imu_enabled_left, "imu_enabled", LEFT_CONTROLLER, index,
+ feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_enabled_left_index, "imu_enabled_index");
+
+struct go_cfg_attr reset_left = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_WO(reset_left, "reset", LEFT_CONTROLLER, feature_status);
+
static struct attribute *left_gamepad_attrs[] = {
+ &dev_attr_auto_sleep_time_left.attr,
+ &dev_attr_auto_sleep_time_left_range.attr,
+ &dev_attr_imu_bypass_left.attr,
+ &dev_attr_imu_bypass_left_index.attr,
+ &dev_attr_imu_enabled_left.attr,
+ &dev_attr_imu_enabled_left_index.attr,
+ &dev_attr_reset_left.attr,
&dev_attr_version_hardware_left.attr,
&dev_attr_version_firmware_left.attr,
&dev_attr_version_gen_left.attr,
@@ -585,7 +947,33 @@ LEGO_DEVICE_ATTR_RO(version_hardware_right, "hardware_version", RIGHT_CONTROLLER
struct go_cfg_attr version_gen_right = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_right, "hardware_generation", RIGHT_CONTROLLER, version);
+struct go_cfg_attr auto_sleep_time_right = { FEATURE_AUTO_SLEEP_TIME };
+LEGO_DEVICE_ATTR_RW(auto_sleep_time_right, "auto_sleep_time", RIGHT_CONTROLLER,
+ range, feature_status);
+static DEVICE_ATTR_RO_NAMED(auto_sleep_time_right_range,
+ "auto_sleep_time_range");
+
+struct go_cfg_attr imu_bypass_right = { FEATURE_IMU_BYPASS };
+LEGO_DEVICE_ATTR_RW(imu_bypass_right, "imu_bypass_enabled", RIGHT_CONTROLLER,
+ index, feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_bypass_right_index, "imu_bypass_enabled_index");
+
+struct go_cfg_attr imu_enabled_right = { FEATURE_IMU_BYPASS };
+LEGO_DEVICE_ATTR_RW(imu_enabled_right, "imu_enabled", RIGHT_CONTROLLER, index,
+ feature_status);
+static DEVICE_ATTR_RO_NAMED(imu_enabled_right_index, "imu_enabled_index");
+
+struct go_cfg_attr reset_right = { FEATURE_RESET_GAMEPAD };
+LEGO_DEVICE_ATTR_WO(reset_right, "reset", LEFT_CONTROLLER, feature_status);
+
static struct attribute *right_gamepad_attrs[] = {
+ &dev_attr_auto_sleep_time_right.attr,
+ &dev_attr_auto_sleep_time_right_range.attr,
+ &dev_attr_imu_bypass_right.attr,
+ &dev_attr_imu_bypass_right_index.attr,
+ &dev_attr_imu_enabled_right.attr,
+ &dev_attr_imu_enabled_right_index.attr,
+ &dev_attr_reset_right.attr,
&dev_attr_version_hardware_right.attr,
&dev_attr_version_firmware_right.attr,
&dev_attr_version_gen_right.attr,
@@ -600,8 +988,14 @@ static const struct attribute_group right_gamepad_attr_group = {
};
/* Touchpad */
+struct go_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE };
+LEGO_DEVICE_ATTR_RW(touchpad_enabled, "enabled", UNSPECIFIED, index,
+ feature_status);
+static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
+
static struct attribute *touchpad_attrs[] = {
- NULL,
+ &dev_attr_touchpad_enabled.attr,
+ &dev_attr_touchpad_enabled_index.attr,
};
static const struct attribute_group touchpad_attr_group = {
--
2.52.0
^ permalink raw reply related
* [PATCH v5 04/16] HID: hid-lenovo-go: Add Rumble and Haptic Settings
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds attributes that control the handles rumble mode and intensity, as
well as touchpad haptic feedback settings.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v3:
- Remove erroneous renaming of enabled -> enable for some left & right
handle attributes.
---
drivers/hid/hid-lenovo-go.c | 312 ++++++++++++++++++++++++++++++++++++
1 file changed, 312 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 62a997fa06276..4581a1134d1db 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -41,6 +41,8 @@ struct hid_go_cfg {
struct mutex cfg_mutex; /*ensure single synchronous output report*/
u8 fps_mode;
u8 gp_left_auto_sleep_time;
+ u8 gp_left_notify_en;
+ u8 gp_left_rumble_mode;
u32 gp_left_version_firmware;
u8 gp_left_version_gen;
u32 gp_left_version_hardware;
@@ -48,11 +50,14 @@ struct hid_go_cfg {
u32 gp_left_version_protocol;
u8 gp_mode;
u8 gp_right_auto_sleep_time;
+ u8 gp_right_notify_en;
+ u8 gp_right_rumble_mode;
u32 gp_right_version_firmware;
u8 gp_right_version_gen;
u32 gp_right_version_hardware;
u32 gp_right_version_product;
u32 gp_right_version_protocol;
+ u8 gp_rumble_intensity;
u8 imu_left_bypass_en;
u8 imu_left_sensor_en;
u8 imu_right_bypass_en;
@@ -64,6 +69,8 @@ struct hid_go_cfg {
u32 mcu_version_protocol;
u8 rgb_en;
u8 tp_en;
+ u8 tp_vibration_en;
+ u8 tp_vibration_intensity;
u32 tx_dongle_version_firmware;
u8 tx_dongle_version_gen;
u32 tx_dongle_version_hardware;
@@ -170,6 +177,49 @@ static const char *const gamepad_mode_text[] = {
[DINPUT] = "dinput",
};
+enum motor_cfg_index {
+ MOTOR_CFG_ALL = 0x01,
+ MOTOR_INTENSITY,
+ VIBRATION_NOTIFY_ENABLE,
+ RUMBLE_MODE,
+ TP_VIBRATION_ENABLE,
+ TP_VIBRATION_INTENSITY,
+};
+
+enum intensity_index {
+ INTENSITY_UNKNOWN,
+ INTENSITY_OFF,
+ INTENSITY_LOW,
+ INTENSITY_MEDIUM,
+ INTENSITY_HIGH,
+};
+
+static const char *const intensity_text[] = {
+ [INTENSITY_UNKNOWN] = "unknown",
+ [INTENSITY_OFF] = "off",
+ [INTENSITY_LOW] = "low",
+ [INTENSITY_MEDIUM] = "medium",
+ [INTENSITY_HIGH] = "high",
+};
+
+enum rumble_mode_index {
+ RUMBLE_MODE_UNKNOWN,
+ RUMBLE_MODE_FPS,
+ RUMBLE_MODE_RACE,
+ RUMBLE_MODE_AVERAGE,
+ RUMBLE_MODE_SPG,
+ RUMBLE_MODE_RPG,
+};
+
+static const char *const rumble_mode_text[] = {
+ [RUMBLE_MODE_UNKNOWN] = "unknown",
+ [RUMBLE_MODE_FPS] = "fps",
+ [RUMBLE_MODE_RACE] = "racing",
+ [RUMBLE_MODE_AVERAGE] = "standard",
+ [RUMBLE_MODE_SPG] = "spg",
+ [RUMBLE_MODE_RPG] = "rpg",
+};
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -336,6 +386,47 @@ static int hid_go_feature_status_event(struct command_report *cmd_rep)
}
}
+static int hid_go_motor_event(struct command_report *cmd_rep)
+{
+ switch (cmd_rep->sub_cmd) {
+ case MOTOR_CFG_ALL:
+ return -EINVAL;
+ case MOTOR_INTENSITY:
+ drvdata.gp_rumble_intensity = cmd_rep->data[0];
+ return 0;
+ case VIBRATION_NOTIFY_ENABLE:
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_notify_en = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_notify_en = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+ break;
+ case RUMBLE_MODE:
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ drvdata.gp_left_rumble_mode = cmd_rep->data[0];
+ return 0;
+ case RIGHT_CONTROLLER:
+ drvdata.gp_right_rumble_mode = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+ case TP_VIBRATION_ENABLE:
+ drvdata.tp_vibration_en = cmd_rep->data[0];
+ return 0;
+ case TP_VIBRATION_INTENSITY:
+ drvdata.tp_vibration_intensity = cmd_rep->data[0];
+ return 0;
+ }
+ return -EINVAL;
+}
+
static int hid_go_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -383,7 +474,11 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_FEATURE_STATUS:
ret = hid_go_feature_status_event(cmd_rep);
break;
+ case GET_MOTOR_CFG:
+ ret = hid_go_motor_event(cmd_rep);
+ break;
case SET_FEATURE_STATUS:
+ case SET_MOTOR_CFG:
ret = hid_go_set_event_return(cmd_rep);
break;
default:
@@ -759,6 +854,168 @@ static ssize_t feature_status_options(struct device *dev,
return count;
}
+static ssize_t motor_config_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count,
+ enum motor_cfg_index index,
+ enum dev_type device_type)
+{
+ size_t size = 1;
+ u8 val = 0;
+ int ret;
+
+ switch (index) {
+ case MOTOR_CFG_ALL:
+ return -EINVAL;
+ case MOTOR_INTENSITY:
+ ret = sysfs_match_string(intensity_text, buf);
+ val = ret;
+ break;
+ case VIBRATION_NOTIFY_ENABLE:
+ ret = sysfs_match_string(enabled_status_text, buf);
+ val = ret;
+ break;
+ case RUMBLE_MODE:
+ ret = sysfs_match_string(rumble_mode_text, buf);
+ val = ret;
+ break;
+ case TP_VIBRATION_ENABLE:
+ ret = sysfs_match_string(enabled_status_text, buf);
+ val = ret;
+ break;
+ case TP_VIBRATION_INTENSITY:
+ ret = sysfs_match_string(intensity_text, buf);
+ val = ret;
+ break;
+ };
+
+ if (ret < 0)
+ return ret;
+
+ if (!val)
+ size = 0;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, SET_MOTOR_CFG,
+ index, device_type, &val, size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t motor_config_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum motor_cfg_index index,
+ enum dev_type device_type)
+{
+ ssize_t count = 0;
+ int ret;
+ u8 i;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_MOTOR_CFG,
+ index, device_type, 0, 0);
+ if (ret)
+ return ret;
+
+ switch (index) {
+ case MOTOR_CFG_ALL:
+ return -EINVAL;
+ case MOTOR_INTENSITY:
+ i = drvdata.gp_rumble_intensity;
+ if (i >= ARRAY_SIZE(intensity_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", intensity_text[i]);
+ break;
+ case VIBRATION_NOTIFY_ENABLE:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ i = drvdata.gp_left_notify_en;
+ break;
+ case RIGHT_CONTROLLER:
+ i = drvdata.gp_right_notify_en;
+ break;
+ default:
+ return -EINVAL;
+ };
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case RUMBLE_MODE:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ i = drvdata.gp_left_rumble_mode;
+ break;
+ case RIGHT_CONTROLLER:
+ i = drvdata.gp_right_rumble_mode;
+ break;
+ default:
+ return -EINVAL;
+ };
+ if (i >= ARRAY_SIZE(rumble_mode_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", rumble_mode_text[i]);
+ break;
+ case TP_VIBRATION_ENABLE:
+ i = drvdata.tp_vibration_en;
+ if (i >= ARRAY_SIZE(enabled_status_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
+ break;
+ case TP_VIBRATION_INTENSITY:
+ i = drvdata.tp_vibration_intensity;
+ if (i >= ARRAY_SIZE(intensity_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", intensity_text[i]);
+ break;
+ };
+
+ return count;
+}
+
+static ssize_t motor_config_options(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum motor_cfg_index index)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ switch (index) {
+ case MOTOR_CFG_ALL:
+ break;
+ case RUMBLE_MODE:
+ for (i = 1; i < ARRAY_SIZE(rumble_mode_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ rumble_mode_text[i]);
+ }
+ break;
+ case MOTOR_INTENSITY:
+ case TP_VIBRATION_INTENSITY:
+ for (i = 1; i < ARRAY_SIZE(intensity_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ intensity_text[i]);
+ }
+ break;
+ case VIBRATION_NOTIFY_ENABLE:
+ case TP_VIBRATION_ENABLE:
+ for (i = 1; i < ARRAY_SIZE(enabled_status_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ enabled_status_text[i]);
+ }
+ break;
+ };
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \
static ssize_t _name##_store(struct device *dev, \
struct device_attribute *attr, \
@@ -824,10 +1081,18 @@ static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");
struct go_cfg_attr reset_mcu = { FEATURE_RESET_GAMEPAD };
LEGO_DEVICE_ATTR_WO(reset_mcu, "reset_mcu", USB_MCU, feature_status);
+struct go_cfg_attr gamepad_rumble_intensity = { MOTOR_INTENSITY };
+LEGO_DEVICE_ATTR_RW(gamepad_rumble_intensity, "rumble_intensity", UNSPECIFIED,
+ index, motor_config);
+static DEVICE_ATTR_RO_NAMED(gamepad_rumble_intensity_index,
+ "rumble_intensity_index");
+
static struct attribute *mcu_attrs[] = {
&dev_attr_fps_switch_status.attr,
&dev_attr_gamepad_mode.attr,
&dev_attr_gamepad_mode_index.attr,
+ &dev_attr_gamepad_rumble_intensity.attr,
+ &dev_attr_gamepad_rumble_intensity_index.attr,
&dev_attr_reset_mcu.attr,
&dev_attr_version_firmware_mcu.attr,
&dev_attr_version_gen_mcu.attr,
@@ -910,6 +1175,17 @@ static DEVICE_ATTR_RO_NAMED(imu_enabled_left_index, "imu_enabled_index");
struct go_cfg_attr reset_left = { FEATURE_RESET_GAMEPAD };
LEGO_DEVICE_ATTR_WO(reset_left, "reset", LEFT_CONTROLLER, feature_status);
+struct go_cfg_attr rumble_mode_left = { RUMBLE_MODE };
+LEGO_DEVICE_ATTR_RW(rumble_mode_left, "rumble_mode", LEFT_CONTROLLER, index,
+ motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_mode_left_index, "rumble_mode_index");
+
+struct go_cfg_attr rumble_notification_left = { VIBRATION_NOTIFY_ENABLE };
+LEGO_DEVICE_ATTR_RW(rumble_notification_left, "rumble_notification",
+ LEFT_CONTROLLER, index, motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_notification_left_index,
+ "rumble_notification_index");
+
static struct attribute *left_gamepad_attrs[] = {
&dev_attr_auto_sleep_time_left.attr,
&dev_attr_auto_sleep_time_left_range.attr,
@@ -918,6 +1194,10 @@ static struct attribute *left_gamepad_attrs[] = {
&dev_attr_imu_enabled_left.attr,
&dev_attr_imu_enabled_left_index.attr,
&dev_attr_reset_left.attr,
+ &dev_attr_rumble_mode_left.attr,
+ &dev_attr_rumble_mode_left_index.attr,
+ &dev_attr_rumble_notification_left.attr,
+ &dev_attr_rumble_notification_left_index.attr,
&dev_attr_version_hardware_left.attr,
&dev_attr_version_firmware_left.attr,
&dev_attr_version_gen_left.attr,
@@ -966,6 +1246,17 @@ static DEVICE_ATTR_RO_NAMED(imu_enabled_right_index, "imu_enabled_index");
struct go_cfg_attr reset_right = { FEATURE_RESET_GAMEPAD };
LEGO_DEVICE_ATTR_WO(reset_right, "reset", LEFT_CONTROLLER, feature_status);
+struct go_cfg_attr rumble_mode_right = { RUMBLE_MODE };
+LEGO_DEVICE_ATTR_RW(rumble_mode_right, "rumble_mode", RIGHT_CONTROLLER, index,
+ motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_mode_right_index, "rumble_mode_index");
+
+struct go_cfg_attr rumble_notification_right = { VIBRATION_NOTIFY_ENABLE };
+LEGO_DEVICE_ATTR_RW(rumble_notification_right, "rumble_notification",
+ RIGHT_CONTROLLER, index, motor_config);
+static DEVICE_ATTR_RO_NAMED(rumble_notification_right_index,
+ "rumble_notification_index");
+
static struct attribute *right_gamepad_attrs[] = {
&dev_attr_auto_sleep_time_right.attr,
&dev_attr_auto_sleep_time_right_range.attr,
@@ -974,6 +1265,10 @@ static struct attribute *right_gamepad_attrs[] = {
&dev_attr_imu_enabled_right.attr,
&dev_attr_imu_enabled_right_index.attr,
&dev_attr_reset_right.attr,
+ &dev_attr_rumble_mode_right.attr,
+ &dev_attr_rumble_mode_right_index.attr,
+ &dev_attr_rumble_notification_right.attr,
+ &dev_attr_rumble_notification_right_index.attr,
&dev_attr_version_hardware_right.attr,
&dev_attr_version_firmware_right.attr,
&dev_attr_version_gen_right.attr,
@@ -993,9 +1288,26 @@ LEGO_DEVICE_ATTR_RW(touchpad_enabled, "enabled", UNSPECIFIED, index,
feature_status);
static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
+struct go_cfg_attr touchpad_vibration_enabled = { TP_VIBRATION_ENABLE };
+LEGO_DEVICE_ATTR_RW(touchpad_vibration_enabled, "vibration_enabled", UNSPECIFIED,
+ index, motor_config);
+static DEVICE_ATTR_RO_NAMED(touchpad_vibration_enabled_index,
+ "vibration_enabled_index");
+
+struct go_cfg_attr touchpad_vibration_intensity = { TP_VIBRATION_INTENSITY };
+LEGO_DEVICE_ATTR_RW(touchpad_vibration_intensity, "vibration_intensity",
+ UNSPECIFIED, index, motor_config);
+static DEVICE_ATTR_RO_NAMED(touchpad_vibration_intensity_index,
+ "vibration_intensity_index");
+
static struct attribute *touchpad_attrs[] = {
&dev_attr_touchpad_enabled.attr,
&dev_attr_touchpad_enabled_index.attr,
+ &dev_attr_touchpad_vibration_enabled.attr,
+ &dev_attr_touchpad_vibration_enabled_index.attr,
+ &dev_attr_touchpad_vibration_intensity.attr,
+ &dev_attr_touchpad_vibration_intensity_index.attr,
+ NULL,
};
static const struct attribute_group touchpad_attr_group = {
--
2.52.0
^ permalink raw reply related
* [PATCH v5 05/16] HID: hid-lenovo-go: Add FPS Mode DPI settings
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds attribute that enables selection of the DPI of the optical sensor
when the right handle toggle is set to FPS mode.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 68 +++++++++++++++++++++++++++++++++++++
1 file changed, 68 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 4581a1134d1db..4553dfd930c89 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -67,6 +67,7 @@ struct hid_go_cfg {
u32 mcu_version_hardware;
u32 mcu_version_product;
u32 mcu_version_protocol;
+ u32 mouse_dpi;
u8 rgb_en;
u8 tp_en;
u8 tp_vibration_en;
@@ -220,6 +221,8 @@ static const char *const rumble_mode_text[] = {
[RUMBLE_MODE_RPG] = "rpg",
};
+#define FPS_MODE_DPI 0x02
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -427,6 +430,16 @@ static int hid_go_motor_event(struct command_report *cmd_rep)
return -EINVAL;
}
+static int hid_go_fps_dpi_event(struct command_report *cmd_rep)
+{
+ if (cmd_rep->sub_cmd != FPS_MODE_DPI)
+ return -EINVAL;
+
+ drvdata.mouse_dpi = get_unaligned_le32(cmd_rep->data);
+
+ return 0;
+}
+
static int hid_go_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -477,8 +490,12 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_MOTOR_CFG:
ret = hid_go_motor_event(cmd_rep);
break;
+ case GET_DPI_CFG:
+ ret = hid_go_fps_dpi_event(cmd_rep);
+ break;
case SET_FEATURE_STATUS:
case SET_MOTOR_CFG:
+ case SET_DPI_CFG:
ret = hid_go_set_event_return(cmd_rep);
break;
default:
@@ -1016,6 +1033,52 @@ static ssize_t motor_config_options(struct device *dev,
return count;
}
+static ssize_t fps_mode_dpi_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+
+{
+ size_t size = 4;
+ u32 value;
+ u8 val[4];
+ int ret;
+
+ ret = kstrtou32(buf, 10, &value);
+ if (ret)
+ return ret;
+
+ if (value != 500 && value != 800 && value != 1200 && value != 1800)
+ return -EINVAL;
+
+ put_unaligned_le32(value, val);
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, SET_DPI_CFG,
+ FPS_MODE_DPI, UNSPECIFIED, val, size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t fps_mode_dpi_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int ret;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_DPI_CFG,
+ FPS_MODE_DPI, UNSPECIFIED, 0, 0);
+ if (ret < 0)
+ return ret;
+
+ return sysfs_emit(buf, "%u\n", drvdata.mouse_dpi);
+}
+
+static ssize_t fps_mode_dpi_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "500 800 1200 1800\n");
+}
+
#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \
static ssize_t _name##_store(struct device *dev, \
struct device_attribute *attr, \
@@ -1087,7 +1150,12 @@ LEGO_DEVICE_ATTR_RW(gamepad_rumble_intensity, "rumble_intensity", UNSPECIFIED,
static DEVICE_ATTR_RO_NAMED(gamepad_rumble_intensity_index,
"rumble_intensity_index");
+static DEVICE_ATTR_RW(fps_mode_dpi);
+static DEVICE_ATTR_RO(fps_mode_dpi_index);
+
static struct attribute *mcu_attrs[] = {
+ &dev_attr_fps_mode_dpi.attr,
+ &dev_attr_fps_mode_dpi_index.attr,
&dev_attr_fps_switch_status.attr,
&dev_attr_gamepad_mode.attr,
&dev_attr_gamepad_mode_index.attr,
--
2.52.0
^ permalink raw reply related
* [PATCH v5 06/16] HID: hid-lenovo-go: Add RGB LED control interface
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds an LED multicolor class device and attribute group for controlling
the RGB of the Left and right handles. In addition to the standard
led_cdev attributes, additional attributes that allow for the control of
the effect (monocolor, breathe, rainbow, and chroma), speed of the
effect change, an enable toggle, and profile.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v5:
- Don't retrieve RGB state during delayed work.
---
drivers/hid/hid-lenovo-go.c | 426 ++++++++++++++++++++++++++++++++++++
1 file changed, 426 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index 4553dfd930c89..cd4fb646aed24 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -18,6 +18,7 @@
#include <linux/hid.h>
#include <linux/jiffies.h>
#include <linux/kstrtox.h>
+#include <linux/led-class-multicolor.h>
#include <linux/mutex.h>
#include <linux/printk.h>
#include <linux/sysfs.h>
@@ -37,6 +38,7 @@
struct hid_go_cfg {
struct delayed_work go_cfg_setup;
struct completion send_cmd_complete;
+ struct led_classdev *led_cdev;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
u8 fps_mode;
@@ -68,7 +70,11 @@ struct hid_go_cfg {
u32 mcu_version_product;
u32 mcu_version_protocol;
u32 mouse_dpi;
+ u8 rgb_effect;
u8 rgb_en;
+ u8 rgb_mode;
+ u8 rgb_profile;
+ u8 rgb_speed;
u8 tp_en;
u8 tp_vibration_en;
u8 tp_vibration_intensity;
@@ -223,6 +229,41 @@ static const char *const rumble_mode_text[] = {
#define FPS_MODE_DPI 0x02
+enum rgb_config_index {
+ LIGHT_CFG_ALL = 0x01,
+ LIGHT_MODE_SEL,
+ LIGHT_PROFILE_SEL,
+ USR_LIGHT_PROFILE_1,
+ USR_LIGHT_PROFILE_2,
+ USR_LIGHT_PROFILE_3,
+};
+
+enum rgb_mode_index {
+ RGB_MODE_UNKNOWN,
+ RGB_MODE_DYNAMIC,
+ RGB_MODE_CUSTOM,
+};
+
+static const char *const rgb_mode_text[] = {
+ [RGB_MODE_UNKNOWN] = "unknown",
+ [RGB_MODE_DYNAMIC] = "dynamic",
+ [RGB_MODE_CUSTOM] = "custom",
+};
+
+enum rgb_effect_index {
+ RGB_EFFECT_MONO,
+ RGB_EFFECT_BREATHE,
+ RGB_EFFECT_CHROMA,
+ RGB_EFFECT_RAINBOW,
+};
+
+static const char *const rgb_effect_text[] = {
+ [RGB_EFFECT_MONO] = "monocolor",
+ [RGB_EFFECT_BREATHE] = "breathe",
+ [RGB_EFFECT_CHROMA] = "chroma",
+ [RGB_EFFECT_RAINBOW] = "rainbow",
+};
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -440,6 +481,33 @@ static int hid_go_fps_dpi_event(struct command_report *cmd_rep)
return 0;
}
+static int hid_go_light_event(struct command_report *cmd_rep)
+{
+ struct led_classdev_mc *mc_cdev;
+
+ switch (cmd_rep->sub_cmd) {
+ case LIGHT_MODE_SEL:
+ drvdata.rgb_mode = cmd_rep->data[0];
+ return 0;
+ case LIGHT_PROFILE_SEL:
+ drvdata.rgb_profile = cmd_rep->data[0];
+ return 0;
+ case USR_LIGHT_PROFILE_1:
+ case USR_LIGHT_PROFILE_2:
+ case USR_LIGHT_PROFILE_3:
+ mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ drvdata.rgb_effect = cmd_rep->data[0];
+ mc_cdev->subled_info[0].intensity = cmd_rep->data[1];
+ mc_cdev->subled_info[1].intensity = cmd_rep->data[2];
+ mc_cdev->subled_info[2].intensity = cmd_rep->data[3];
+ drvdata.led_cdev->brightness = cmd_rep->data[4];
+ drvdata.rgb_speed = cmd_rep->data[5];
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
static int hid_go_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -493,9 +561,13 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_DPI_CFG:
ret = hid_go_fps_dpi_event(cmd_rep);
break;
+ case GET_RGB_CFG:
+ ret = hid_go_light_event(cmd_rep);
+ break;
case SET_FEATURE_STATUS:
case SET_MOTOR_CFG:
case SET_DPI_CFG:
+ case SET_RGB_CFG:
ret = hid_go_set_event_return(cmd_rep);
break;
default:
@@ -565,6 +637,12 @@ static ssize_t version_show(struct device *dev, struct device_attribute *attr,
enum dev_type device_type)
{
ssize_t count = 0;
+ int ret;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
+ index, device_type, 0, 0);
+ if (ret)
+ return ret;
switch (index) {
case PRODUCT_VERSION:
@@ -1079,6 +1157,274 @@ static ssize_t fps_mode_dpi_index_show(struct device *dev,
return sysfs_emit(buf, "500 800 1200 1800\n");
}
+static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd,
+ enum rgb_config_index index, u8 *val, size_t size)
+{
+ if (cmd != SET_RGB_CFG && cmd != GET_RGB_CFG)
+ return -EINVAL;
+
+ if (index < LIGHT_CFG_ALL || index > USR_LIGHT_PROFILE_3)
+ return -EINVAL;
+
+ return mcu_property_out(hdev, MCU_CONFIG_DATA, cmd, index, UNSPECIFIED,
+ val, size);
+}
+
+static int rgb_attr_show(void)
+{
+ enum rgb_config_index index;
+
+ index = drvdata.rgb_profile + 3;
+
+ return rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, index, 0, 0);
+};
+
+static ssize_t rgb_effect_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ u8 effect;
+ int ret;
+
+ ret = sysfs_match_string(rgb_effect_text, buf);
+ if (ret < 0)
+ return ret;
+
+ effect = ret;
+ index = drvdata.rgb_profile + 3;
+ u8 rgb_profile[6] = { effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ drvdata.led_cdev->brightness,
+ drvdata.rgb_speed };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_effect = effect;
+ return count;
+};
+
+static ssize_t rgb_effect_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int ret;
+
+ ret = rgb_attr_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_effect >= ARRAY_SIZE(rgb_effect_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", rgb_effect_text[drvdata.rgb_effect]);
+}
+
+static ssize_t rgb_effect_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(rgb_effect_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", rgb_effect_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t rgb_speed_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ int val = 0;
+ int ret;
+
+ ret = kstrtoint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val < 0 || val > 100)
+ return -EINVAL;
+
+ index = drvdata.rgb_profile + 3;
+ u8 rgb_profile[6] = { drvdata.rgb_effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ drvdata.led_cdev->brightness,
+ val };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_speed = val;
+
+ return count;
+};
+
+static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = rgb_attr_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_speed > 100)
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed);
+}
+
+static ssize_t rgb_speed_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "0-100\n");
+}
+
+static ssize_t rgb_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+ u8 val;
+
+ ret = sysfs_match_string(rgb_mode_text, buf);
+ if (ret <= 0)
+ return ret;
+
+ val = ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_MODE_SEL, &val, 1);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_mode = val;
+
+ return count;
+};
+
+static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_MODE_SEL, 0, 0);
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_mode >= ARRAY_SIZE(rgb_mode_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", rgb_mode_text[drvdata.rgb_mode]);
+};
+
+static ssize_t rgb_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 1; i < ARRAY_SIZE(rgb_mode_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", rgb_mode_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t rgb_profile_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ size_t size = 1;
+ int ret;
+ u8 val;
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret < 0)
+ return ret;
+
+ if (val < 1 || val > 3)
+ return -EINVAL;
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_PROFILE_SEL, &val,
+ size);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_profile = val;
+
+ return count;
+};
+
+static ssize_t rgb_profile_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_PROFILE_SEL, 0,
+ 0);
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_profile < 1 || drvdata.rgb_profile > 3)
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%hhu\n", drvdata.rgb_profile);
+};
+
+static ssize_t rgb_profile_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "1-3\n");
+}
+
+static void hid_go_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ int ret;
+
+ if (brightness > led_cdev->max_brightness) {
+ dev_err(led_cdev->dev, "Invalid argument\n");
+ return;
+ }
+
+ index = drvdata.rgb_profile + 3;
+ u8 rgb_profile[6] = { drvdata.rgb_effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ brightness,
+ drvdata.rgb_speed };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ switch (ret) {
+ case 0:
+ led_cdev->brightness = brightness;
+ break;
+ case -ENODEV: /* during switch to IAP -ENODEV is expected */
+ case -ENOSYS: /* during rmmod -ENOSYS is expected */
+ dev_dbg(led_cdev->dev, "Failed to write RGB profile: %i\n", ret);
+ break;
+ default:
+ dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n", ret);
+ };
+}
+
#define LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group) \
static ssize_t _name##_store(struct device *dev, \
struct device_attribute *attr, \
@@ -1389,6 +1735,71 @@ static const struct attribute_group *top_level_attr_groups[] = {
&touchpad_attr_group, NULL,
};
+/* RGB */
+struct go_cfg_attr rgb_enabled = { FEATURE_LIGHT_ENABLE };
+
+LEGO_DEVICE_ATTR_RW(rgb_enabled, "enabled", UNSPECIFIED, index, feature_status);
+static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index");
+static DEVICE_ATTR_RO_NAMED(rgb_enabled_index, "enabled_index");
+static DEVICE_ATTR_RO_NAMED(rgb_mode_index, "mode_index");
+static DEVICE_ATTR_RO_NAMED(rgb_profile_range, "profile_range");
+static DEVICE_ATTR_RO_NAMED(rgb_speed_range, "speed_range");
+static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect");
+static DEVICE_ATTR_RW_NAMED(rgb_mode, "mode");
+static DEVICE_ATTR_RW_NAMED(rgb_profile, "profile");
+static DEVICE_ATTR_RW_NAMED(rgb_speed, "speed");
+
+static struct attribute *go_rgb_attrs[] = {
+ &dev_attr_rgb_effect.attr,
+ &dev_attr_rgb_effect_index.attr,
+ &dev_attr_rgb_enabled.attr,
+ &dev_attr_rgb_enabled_index.attr,
+ &dev_attr_rgb_mode.attr,
+ &dev_attr_rgb_mode_index.attr,
+ &dev_attr_rgb_profile.attr,
+ &dev_attr_rgb_profile_range.attr,
+ &dev_attr_rgb_speed.attr,
+ &dev_attr_rgb_speed_range.attr,
+ NULL,
+};
+
+static struct attribute_group rgb_attr_group = {
+ .attrs = go_rgb_attrs,
+};
+
+struct mc_subled go_rgb_subled_info[] = {
+ {
+ .color_index = LED_COLOR_ID_RED,
+ .brightness = 0x50,
+ .intensity = 0x24,
+ .channel = 0x1,
+ },
+ {
+ .color_index = LED_COLOR_ID_GREEN,
+ .brightness = 0x50,
+ .intensity = 0x22,
+ .channel = 0x2,
+ },
+ {
+ .color_index = LED_COLOR_ID_BLUE,
+ .brightness = 0x50,
+ .intensity = 0x99,
+ .channel = 0x3,
+ },
+};
+
+struct led_classdev_mc go_cdev_rgb = {
+ .led_cdev = {
+ .name = "go:rgb:joystick_rings",
+ .color = LED_COLOR_ID_RGB,
+ .brightness = 0x50,
+ .max_brightness = 0x64,
+ .brightness_set = hid_go_brightness_set,
+ },
+ .num_colors = ARRAY_SIZE(go_rgb_subled_info),
+ .subled_info = go_rgb_subled_info,
+};
+
static void cfg_setup(struct work_struct *work)
{
int ret;
@@ -1579,6 +1990,21 @@ static int hid_go_cfg_probe(struct hid_device *hdev,
return ret;
}
+ ret = devm_led_classdev_multicolor_register(&hdev->dev, &go_cdev_rgb);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret, "Failed to create RGB device\n");
+ return ret;
+ }
+
+ ret = devm_device_add_group(go_cdev_rgb.led_cdev.dev, &rgb_attr_group);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret,
+ "Failed to create RGB configuration attributes\n");
+ return ret;
+ }
+
+ drvdata.led_cdev = &go_cdev_rgb.led_cdev;
+
init_completion(&drvdata.send_cmd_complete);
/* Executing calls prior to returning from probe will lock the MCU. Schedule
--
2.52.0
^ permalink raw reply related
* [PATCH v5 07/16] HID: hid-lenovo-go: Add Calibration Settings
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds calibration enable and last calibration status indicators for the
triggers, joysticks, and handle gyros.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go.c | 284 +++++++++++++++++++++++++++++++++++-
1 file changed, 283 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index cd4fb646aed24..cd613d5ca37f7 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -43,8 +43,11 @@ struct hid_go_cfg {
struct mutex cfg_mutex; /*ensure single synchronous output report*/
u8 fps_mode;
u8 gp_left_auto_sleep_time;
+ u8 gp_left_gyro_cal_status;
+ u8 gp_left_joy_cal_status;
u8 gp_left_notify_en;
u8 gp_left_rumble_mode;
+ u8 gp_left_trigg_cal_status;
u32 gp_left_version_firmware;
u8 gp_left_version_gen;
u32 gp_left_version_hardware;
@@ -52,8 +55,11 @@ struct hid_go_cfg {
u32 gp_left_version_protocol;
u8 gp_mode;
u8 gp_right_auto_sleep_time;
+ u8 gp_right_gyro_cal_status;
+ u8 gp_right_joy_cal_status;
u8 gp_right_notify_en;
u8 gp_right_rumble_mode;
+ u8 gp_right_trigg_cal_status;
u32 gp_right_version_firmware;
u8 gp_right_version_gen;
u32 gp_right_version_hardware;
@@ -227,7 +233,41 @@ static const char *const rumble_mode_text[] = {
[RUMBLE_MODE_RPG] = "rpg",
};
-#define FPS_MODE_DPI 0x02
+#define FPS_MODE_DPI 0x02
+#define TRIGGER_CALIBRATE 0x04
+#define JOYSTICK_CALIBRATE 0x04
+#define GYRO_CALIBRATE 0x06
+
+enum cal_device_type {
+ CALDEV_GYROSCOPE = 0x01,
+ CALDEV_JOYSTICK,
+ CALDEV_TRIGGER,
+ CALDEV_JOY_TRIGGER,
+};
+
+enum cal_enable {
+ CAL_UNKNOWN,
+ CAL_START,
+ CAL_STOP,
+};
+
+static const char *const cal_enabled_text[] = {
+ [CAL_UNKNOWN] = "unknown",
+ [CAL_START] = "start",
+ [CAL_STOP] = "stop",
+};
+
+enum cal_status_index {
+ CAL_STAT_UNKNOWN,
+ CAL_STAT_SUCCESS,
+ CAL_STAT_FAILURE,
+};
+
+static const char *const cal_status_text[] = {
+ [CAL_STAT_UNKNOWN] = "unknown",
+ [CAL_STAT_SUCCESS] = "success",
+ [CAL_STAT_FAILURE] = "failure",
+};
enum rgb_config_index {
LIGHT_CFG_ALL = 0x01,
@@ -264,6 +304,13 @@ static const char *const rgb_effect_text[] = {
[RGB_EFFECT_RAINBOW] = "rainbow",
};
+enum device_status_index {
+ GET_CAL_STATUS = 0x02,
+ GET_UPGRADE_STATUS,
+ GET_MACRO_REC_STATUS,
+ GET_HOTKEY_TRIGG_STATUS,
+};
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -508,6 +555,44 @@ static int hid_go_light_event(struct command_report *cmd_rep)
}
}
+static int hid_go_device_status_event(struct command_report *cmd_rep)
+{
+ switch (cmd_rep->device_type) {
+ case LEFT_CONTROLLER:
+ switch (cmd_rep->data[0]) {
+ case CALDEV_GYROSCOPE:
+ drvdata.gp_left_gyro_cal_status = cmd_rep->data[1];
+ return 0;
+ case CALDEV_JOYSTICK:
+ drvdata.gp_left_joy_cal_status = cmd_rep->data[1];
+ return 0;
+ case CALDEV_TRIGGER:
+ drvdata.gp_left_trigg_cal_status = cmd_rep->data[1];
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case RIGHT_CONTROLLER:
+ switch (cmd_rep->data[0]) {
+ case CALDEV_GYROSCOPE:
+ drvdata.gp_right_gyro_cal_status = cmd_rep->data[1];
+ return 0;
+ case CALDEV_JOYSTICK:
+ drvdata.gp_right_joy_cal_status = cmd_rep->data[1];
+ return 0;
+ case CALDEV_TRIGGER:
+ drvdata.gp_right_trigg_cal_status = cmd_rep->data[1];
+ return 0;
+ default:
+ return -EINVAL;
+ }
+ break;
+ default:
+ return -EINVAL;
+ }
+}
+
static int hid_go_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -564,10 +649,16 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_RGB_CFG:
ret = hid_go_light_event(cmd_rep);
break;
+ case GET_DEVICE_STATUS:
+ ret = hid_go_device_status_event(cmd_rep);
+ break;
case SET_FEATURE_STATUS:
case SET_MOTOR_CFG:
case SET_DPI_CFG:
case SET_RGB_CFG:
+ case SET_TRIGGER_CFG:
+ case SET_JOYSTICK_CFG:
+ case SET_GYRO_CFG:
ret = hid_go_set_event_return(cmd_rep);
break;
default:
@@ -1157,6 +1248,101 @@ static ssize_t fps_mode_dpi_index_show(struct device *dev,
return sysfs_emit(buf, "500 800 1200 1800\n");
}
+static ssize_t device_status_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum device_status_index index,
+ enum dev_type device_type,
+ enum cal_device_type cal_type)
+{
+ u8 i;
+
+ switch (index) {
+ case GET_CAL_STATUS:
+ switch (device_type) {
+ case LEFT_CONTROLLER:
+ switch (cal_type) {
+ case CALDEV_GYROSCOPE:
+ i = drvdata.gp_left_gyro_cal_status;
+ break;
+ case CALDEV_JOYSTICK:
+ i = drvdata.gp_left_joy_cal_status;
+ break;
+ case CALDEV_TRIGGER:
+ i = drvdata.gp_left_trigg_cal_status;
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ case RIGHT_CONTROLLER:
+ switch (cal_type) {
+ case CALDEV_GYROSCOPE:
+ i = drvdata.gp_right_gyro_cal_status;
+ break;
+ case CALDEV_JOYSTICK:
+ i = drvdata.gp_right_joy_cal_status;
+ break;
+ case CALDEV_TRIGGER:
+ i = drvdata.gp_right_trigg_cal_status;
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ default:
+ return -EINVAL;
+ }
+ break;
+ default:
+ return -EINVAL;
+ };
+
+ if (i >= ARRAY_SIZE(cal_status_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", cal_status_text[i]);
+}
+
+static ssize_t calibrate_config_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, u8 cmd, u8 sub_cmd,
+ size_t count, enum dev_type device_type)
+{
+ size_t size = 1;
+ u8 val = 0;
+ int ret;
+
+ ret = sysfs_match_string(cal_enabled_text, buf);
+ if (ret < 0)
+ return ret;
+
+ val = ret;
+ if (!val)
+ size = 0;
+
+ ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, cmd, sub_cmd,
+ device_type, &val, size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t calibrate_config_options(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 1; i < ARRAY_SIZE(cal_enabled_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", cal_enabled_text[i]);
+
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd,
enum rgb_config_index index, u8 *val, size_t size)
{
@@ -1463,6 +1649,30 @@ static void hid_go_brightness_set(struct led_classdev *led_cdev,
} \
static DEVICE_ATTR_RO_NAMED(_name, _attrname)
+#define LEGO_CAL_DEVICE_ATTR(_name, _attrname, _scmd, _dtype, _rtype) \
+ static ssize_t _name##_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+ { \
+ return calibrate_config_store(dev, attr, buf, _name.index, \
+ _scmd, count, _dtype); \
+ } \
+ static ssize_t _name##_##_rtype##_show( \
+ struct device *dev, struct device_attribute *attr, char *buf) \
+ { \
+ return calibrate_config_options(dev, attr, buf); \
+ } \
+ static DEVICE_ATTR_WO_NAMED(_name, _attrname)
+
+#define LEGO_DEVICE_STATUS_ATTR(_name, _attrname, _scmd, _dtype) \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return device_status_show(dev, attr, buf, _name.index, _scmd, \
+ _dtype); \
+ } \
+ static DEVICE_ATTR_RO_NAMED(_name, _attrname)
+
/* Gamepad - MCU */
struct go_cfg_attr version_product_mcu = { PRODUCT_VERSION };
LEGO_DEVICE_ATTR_RO(version_product_mcu, "product_version", USB_MCU, version);
@@ -1600,9 +1810,45 @@ LEGO_DEVICE_ATTR_RW(rumble_notification_left, "rumble_notification",
static DEVICE_ATTR_RO_NAMED(rumble_notification_left_index,
"rumble_notification_index");
+struct go_cfg_attr cal_trigg_left = { TRIGGER_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_trigg_left, "calibrate_trigger", SET_TRIGGER_CFG,
+ LEFT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_trigg_left_index, "calibrate_trigger_index");
+
+struct go_cfg_attr cal_joy_left = { JOYSTICK_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_joy_left, "calibrate_joystick", SET_JOYSTICK_CFG,
+ LEFT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_joy_left_index, "calibrate_joystick_index");
+
+struct go_cfg_attr cal_gyro_left = { GYRO_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_gyro_left, "calibrate_gyro", SET_GYRO_CFG,
+ LEFT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_gyro_left_index, "calibrate_gyro_index");
+
+struct go_cfg_attr cal_trigg_left_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_trigg_left_status, "calibrate_trigger_status",
+ LEFT_CONTROLLER, CALDEV_TRIGGER);
+
+struct go_cfg_attr cal_joy_left_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_joy_left_status, "calibrate_joystick_status",
+ LEFT_CONTROLLER, CALDEV_JOYSTICK);
+
+struct go_cfg_attr cal_gyro_left_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_gyro_left_status, "calibrate_gyro_status",
+ LEFT_CONTROLLER, CALDEV_GYROSCOPE);
+
static struct attribute *left_gamepad_attrs[] = {
&dev_attr_auto_sleep_time_left.attr,
&dev_attr_auto_sleep_time_left_range.attr,
+ &dev_attr_cal_gyro_left.attr,
+ &dev_attr_cal_gyro_left_index.attr,
+ &dev_attr_cal_gyro_left_status.attr,
+ &dev_attr_cal_joy_left.attr,
+ &dev_attr_cal_joy_left_index.attr,
+ &dev_attr_cal_joy_left_status.attr,
+ &dev_attr_cal_trigg_left.attr,
+ &dev_attr_cal_trigg_left_index.attr,
+ &dev_attr_cal_trigg_left_status.attr,
&dev_attr_imu_bypass_left.attr,
&dev_attr_imu_bypass_left_index.attr,
&dev_attr_imu_enabled_left.attr,
@@ -1671,9 +1917,45 @@ LEGO_DEVICE_ATTR_RW(rumble_notification_right, "rumble_notification",
static DEVICE_ATTR_RO_NAMED(rumble_notification_right_index,
"rumble_notification_index");
+struct go_cfg_attr cal_trigg_right = { TRIGGER_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_trigg_right, "calibrate_trigger", SET_TRIGGER_CFG,
+ RIGHT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_trigg_right_index, "calibrate_trigger_index");
+
+struct go_cfg_attr cal_joy_right = { JOYSTICK_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_joy_right, "calibrate_joystick", SET_JOYSTICK_CFG,
+ RIGHT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_joy_right_index, "calibrate_joystick_index");
+
+struct go_cfg_attr cal_gyro_right = { GYRO_CALIBRATE };
+LEGO_CAL_DEVICE_ATTR(cal_gyro_right, "calibrate_gyro", SET_GYRO_CFG,
+ RIGHT_CONTROLLER, index);
+static DEVICE_ATTR_RO_NAMED(cal_gyro_right_index, "calibrate_gyro_index");
+
+struct go_cfg_attr cal_trigg_right_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_trigg_right_status, "calibrate_trigger_status",
+ RIGHT_CONTROLLER, CALDEV_TRIGGER);
+
+struct go_cfg_attr cal_joy_right_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_joy_right_status, "calibrate_joystick_status",
+ RIGHT_CONTROLLER, CALDEV_JOYSTICK);
+
+struct go_cfg_attr cal_gyro_right_status = { GET_CAL_STATUS };
+LEGO_DEVICE_STATUS_ATTR(cal_gyro_right_status, "calibrate_gyro_status",
+ RIGHT_CONTROLLER, CALDEV_GYROSCOPE);
+
static struct attribute *right_gamepad_attrs[] = {
&dev_attr_auto_sleep_time_right.attr,
&dev_attr_auto_sleep_time_right_range.attr,
+ &dev_attr_cal_gyro_right.attr,
+ &dev_attr_cal_gyro_right_index.attr,
+ &dev_attr_cal_gyro_right_status.attr,
+ &dev_attr_cal_joy_right.attr,
+ &dev_attr_cal_joy_right_index.attr,
+ &dev_attr_cal_joy_right_status.attr,
+ &dev_attr_cal_trigg_right.attr,
+ &dev_attr_cal_trigg_right_index.attr,
+ &dev_attr_cal_trigg_right_status.attr,
&dev_attr_imu_bypass_right.attr,
&dev_attr_imu_bypass_right_index.attr,
&dev_attr_imu_enabled_right.attr,
--
2.52.0
^ permalink raw reply related
* [PATCH v5 08/16] HID: hid-lenovo-go: Add OS Mode Toggle
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds OS Mode toggle, who's primary function is to change the built-in
functional chords to use the right handle legion button instead of the
left handle legion button as the mode shift key.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v5:
- Remove reset_resume as it doesn't run, the device disconnects are
reconnects during suspend. Udev or userspace will reset os_mode
after resume.
v3:
- Fix collision with os_mode_index attribute and os_mode_index enum.
---
drivers/hid/hid-lenovo-go.c | 101 ++++++++++++++++++++++++++++++++++++
1 file changed, 101 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go.c b/drivers/hid/hid-lenovo-go.c
index cd613d5ca37f7..6972d13802e20 100644
--- a/drivers/hid/hid-lenovo-go.c
+++ b/drivers/hid/hid-lenovo-go.c
@@ -76,6 +76,7 @@ struct hid_go_cfg {
u32 mcu_version_product;
u32 mcu_version_protocol;
u32 mouse_dpi;
+ u8 os_mode;
u8 rgb_effect;
u8 rgb_en;
u8 rgb_mode;
@@ -166,6 +167,8 @@ enum feature_status_index {
FEATURE_GAMEPAD_MODE = 0x0e,
};
+#define FEATURE_OS_MODE 0x69
+
enum fps_switch_status_index {
FPS_STATUS_UNKNOWN,
GAMEPAD,
@@ -311,6 +314,23 @@ enum device_status_index {
GET_HOTKEY_TRIGG_STATUS,
};
+enum os_mode_cfg_index {
+ SET_OS_MODE = 0x09,
+ GET_OS_MODE,
+};
+
+enum os_mode_type_index {
+ OS_UNKNOWN,
+ WINDOWS,
+ LINUX,
+};
+
+static const char *const os_mode_text[] = {
+ [OS_UNKNOWN] = "unknown",
+ [WINDOWS] = "windows",
+ [LINUX] = "linux",
+};
+
static int hid_go_version_event(struct command_report *cmd_rep)
{
switch (cmd_rep->sub_cmd) {
@@ -593,6 +613,21 @@ static int hid_go_device_status_event(struct command_report *cmd_rep)
}
}
+static int hid_go_os_mode_cfg_event(struct command_report *cmd_rep)
+{
+ switch (cmd_rep->sub_cmd) {
+ case SET_OS_MODE:
+ if (cmd_rep->data[0] != 1)
+ return -EIO;
+ return 0;
+ case GET_OS_MODE:
+ drvdata.os_mode = cmd_rep->data[0];
+ return 0;
+ default:
+ return -EINVAL;
+ };
+}
+
static int hid_go_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -666,6 +701,9 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
break;
};
break;
+ case OS_MODE_DATA:
+ ret = hid_go_os_mode_cfg_event(cmd_rep);
+ break;
default:
goto passthrough;
};
@@ -1343,6 +1381,64 @@ static ssize_t calibrate_config_options(struct device *dev,
return count;
}
+static ssize_t os_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ size_t size = 1;
+ int ret;
+ u8 val;
+
+ ret = sysfs_match_string(os_mode_text, buf);
+ if (ret <= 0)
+ return ret;
+
+ val = ret;
+ ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE,
+ SET_OS_MODE, USB_MCU, &val, size);
+ if (ret < 0)
+ return ret;
+
+ drvdata.os_mode = val;
+
+ return count;
+}
+
+static ssize_t os_mode_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ ssize_t count = 0;
+ int ret;
+ u8 i;
+
+ ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE,
+ GET_OS_MODE, USB_MCU, 0, 0);
+ if (ret)
+ return ret;
+
+ i = drvdata.os_mode;
+ if (i >= ARRAY_SIZE(os_mode_text))
+ return -EINVAL;
+
+ count = sysfs_emit(buf, "%s\n", os_mode_text[i]);
+
+ return count;
+}
+
+static ssize_t os_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 1; i < ARRAY_SIZE(os_mode_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", os_mode_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd,
enum rgb_config_index index, u8 *val, size_t size)
{
@@ -1709,6 +1805,9 @@ static DEVICE_ATTR_RO_NAMED(gamepad_rumble_intensity_index,
static DEVICE_ATTR_RW(fps_mode_dpi);
static DEVICE_ATTR_RO(fps_mode_dpi_index);
+static DEVICE_ATTR_RW(os_mode);
+static DEVICE_ATTR_RO(os_mode_index);
+
static struct attribute *mcu_attrs[] = {
&dev_attr_fps_mode_dpi.attr,
&dev_attr_fps_mode_dpi_index.attr,
@@ -1717,6 +1816,8 @@ static struct attribute *mcu_attrs[] = {
&dev_attr_gamepad_mode_index.attr,
&dev_attr_gamepad_rumble_intensity.attr,
&dev_attr_gamepad_rumble_intensity_index.attr,
+ &dev_attr_os_mode.attr,
+ &dev_attr_os_mode_index.attr,
&dev_attr_reset_mcu.attr,
&dev_attr_version_firmware_mcu.attr,
&dev_attr_version_gen_mcu.attr,
--
2.52.0
^ permalink raw reply related
* [PATCH v5 09/16] HID: Include firmware version in the uevent
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
From: Mario Limonciello <mario.limonciello@amd.com>
Userspace software fwupd probes some HID devices when the daemon starts
up to determine the current firmware version in order to be able to offer
updated firmware if the manufacturer has made it available.
In order to do this fwupd will detach the existing kernel driver if one
is present, send a HID command and then reattach the kernel driver.
This can be problematic if the user is using the HID device at the time
that fwupd probes the hardware and can cause a few frames of input to be
dropped. In some cases HID drivers already have a command to look up the
firmware version, and so if that is exported to userspace fwupd can discover
it and avoid needing to detach the kernel driver until it's time to update
the device.
Introduce a new member in the struct hid_device for the version and export
a new uevent variable HID_FIRMWARE_VERSION that will display the version
that HID drivers obtained.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Cc: Richard Hughes <hughsient@gmail.com>
Signed-off-by: Mario Limonciello <mario.limonciello@amd.com>
---
drivers/hid/hid-core.c | 5 +++++
include/linux/hid.h | 1 +
2 files changed, 6 insertions(+)
diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index a5b3a8ca2fcbc..524f2b9ed5121 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2887,6 +2887,11 @@ static int hid_uevent(const struct device *dev, struct kobj_uevent_env *env)
if (add_uevent_var(env, "MODALIAS=hid:b%04Xg%04Xv%08Xp%08X",
hdev->bus, hdev->group, hdev->vendor, hdev->product))
return -ENOMEM;
+ if (hdev->firmware_version) {
+ if (add_uevent_var(env, "HID_FIRMWARE_VERSION=0x%04llX",
+ hdev->firmware_version))
+ return -ENOMEM;
+ }
return 0;
}
diff --git a/include/linux/hid.h b/include/linux/hid.h
index dce862cafbbd3..ce728c8d5bdc4 100644
--- a/include/linux/hid.h
+++ b/include/linux/hid.h
@@ -698,6 +698,7 @@ struct hid_device {
char name[128]; /* Device name */
char phys[64]; /* Device physical location */
char uniq[64]; /* Device unique identifier (serial #) */
+ u64 firmware_version; /* Firmware version */
void *driver_data;
--
2.52.0
^ permalink raw reply related
* [PATCH v5 10/16] HID: hid-lenovo-go-s: Add Lenovo Legion Go S Series HID Driver
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds initial framework for a new HID driver, hid-lenovo-go-s, along with
a uevent to report the firmware version for the MCU.
This driver primarily provides access to the configurable settings of the
Lenovo Legion Go S controller. It will attach if the controller is in
xinput or dinput mode. Non-configuration raw reports are forwarded to
ensure the other endpoints continue to function as normal.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Co-developed-by: Mario Limonciello <mario.limonciello@amd.com>
Signed-off-by: Mario Limonciello <mario.limonciello@amd.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v4:
- Use dmabuf per request instead of devm allocated static buffer.
Resolves bug with side effects during suspend.
- Remove unnecessary HID quirks and return to HID_CONNECT_HIDRAW.
- Adjust delayed work time to 5ms to fix some side effects during
resume when the MCU disconnects in some circumstances.
- Cleaner formatting on multiple debug messages.
v3:
- Include Mario's SOB tag
---
MAINTAINERS | 1 +
drivers/hid/Kconfig | 12 ++
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 4 +
drivers/hid/hid-lenovo-go-s.c | 278 ++++++++++++++++++++++++++++++++++
5 files changed, 296 insertions(+)
create mode 100644 drivers/hid/hid-lenovo-go-s.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 9db6292c62ec6..1d0468906788a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14318,6 +14318,7 @@ M: Derek J. Clark <derekjohn.clark@gmail.com>
M: Mark Pearson <mpearson-lenovo@squebb.ca>
L: linux-input@vger.kernel.org
S: Maintained
+F: drivers/hid/hid-lenovo-go-s.c
F: drivers/hid/hid-lenovo-go.c
F: drivers/hid/hid-lenovo.c
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index d6c31a2cbaf3b..8a04a69b8f259 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -634,6 +634,18 @@ config HID_LENOVO_GO
and Legion Go 2 Handheld Console Controllers. Say M here to compile this
driver as a module. The module will be called hid-lenovo-go.
+config HID_LENOVO_GO_S
+ tristate "HID Driver for Lenovo Legion Go S Controller"
+ depends on USB_HID
+ select LEDS_CLASS
+ select LEDS_CLASS_MULTICOLOR
+ help
+ Support for Lenovo Legion Go S Handheld Console Controller.
+
+ Say Y here to include configuration interface support for the Lenovo Legion Go
+ S. Say M here to compile this driver as a module. The module will be called
+ hid-lenovo-go-s.
+
config HID_LETSKETCH
tristate "Letsketch WP9620N tablets"
depends on USB_HID
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 11435bce4e475..ef9169974bf00 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -77,6 +77,7 @@ obj-$(CONFIG_HID_KYSONA) += hid-kysona.o
obj-$(CONFIG_HID_LCPOWER) += hid-lcpower.o
obj-$(CONFIG_HID_LENOVO) += hid-lenovo.o
obj-$(CONFIG_HID_LENOVO_GO) += hid-lenovo-go.o
+obj-$(CONFIG_HID_LENOVO_GO_S) += hid-lenovo-go-s.o
obj-$(CONFIG_HID_LETSKETCH) += hid-letsketch.o
obj-$(CONFIG_HID_LOGITECH) += hid-logitech.o
obj-$(CONFIG_HID_LOGITECH) += hid-lg-g15.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index bd41ddbbbee15..486d8baae0257 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -729,6 +729,10 @@
#define USB_DEVICE_ID_ITE8595 0x8595
#define USB_DEVICE_ID_ITE_MEDION_E1239T 0xce50
+#define USB_VENDOR_ID_QHE 0x1a86
+#define USB_DEVICE_ID_LENOVO_LEGION_GO_S_XINPUT 0xe310
+#define USB_DEVICE_ID_LENOVO_LEGION_GO_S_DINPUT 0xe311
+
#define USB_VENDOR_ID_JABRA 0x0b0e
#define USB_DEVICE_ID_JABRA_SPEAK_410 0x0412
#define USB_DEVICE_ID_JABRA_SPEAK_510 0x0420
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
new file mode 100644
index 0000000000000..d8d32a9c90512
--- /dev/null
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -0,0 +1,278 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for Lenovo Legion Go S devices.
+ *
+ * Copyright (c) 2026 Derek J. Clark <derekjohn.clark@gmail.com>
+ * Copyright (c) 2026 Valve Corporation
+ */
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/mutex.h>
+#include <linux/printk.h>
+#include <linux/string.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+#include <linux/workqueue_types.h>
+
+#include "hid-ids.h"
+
+#define GO_S_CFG_INTF_IN 0x84
+#define GO_S_PACKET_SIZE 64
+
+struct hid_gos_cfg {
+ struct delayed_work gos_cfg_setup;
+ struct completion send_cmd_complete;
+ struct hid_device *hdev;
+ struct mutex cfg_mutex; /*ensure single synchronous output report*/
+} drvdata;
+
+struct command_report {
+ u8 cmd;
+ u8 sub_cmd;
+ u8 data[63];
+} __packed;
+
+struct version_report {
+ u8 cmd;
+ u32 version;
+ u8 reserved[59];
+} __packed;
+
+enum mcu_command_index {
+ GET_VERSION = 0x01,
+ GET_MCU_ID,
+ GET_GAMEPAD_CFG,
+ SET_GAMEPAD_CFG,
+ GET_TP_PARAM,
+ SET_TP_PARAM,
+ GET_RGB_CFG = 0x0f,
+ SET_RGB_CFG,
+ GET_PL_TEST = 0xdf,
+};
+
+#define FEATURE_NONE 0x00
+
+static int hid_gos_version_event(u8 *data)
+{
+ struct version_report *ver_rep = (struct version_report *)data;
+
+ drvdata.hdev->firmware_version = get_unaligned_le32(&ver_rep->version);
+ return 0;
+}
+
+static u8 get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_host_endpoint *ep;
+
+ if (intf) {
+ ep = intf->cur_altsetting->endpoint;
+ if (ep)
+ return ep->desc.bEndpointAddress;
+ }
+
+ return -ENODEV;
+}
+
+static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct command_report *cmd_rep;
+ int ep, ret;
+
+ ep = get_endpoint_address(hdev);
+ if (ep != GO_S_CFG_INTF_IN)
+ return 0;
+
+ if (size != GO_S_PACKET_SIZE)
+ return -EINVAL;
+
+ cmd_rep = (struct command_report *)data;
+
+ switch (cmd_rep->cmd) {
+ case GET_VERSION:
+ ret = hid_gos_version_event(data);
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n",
+ GO_S_PACKET_SIZE, data);
+
+ complete(&drvdata.send_cmd_complete);
+ return ret;
+}
+
+static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index,
+ u8 *data, size_t len)
+{
+ unsigned char *dmabuf __free(kfree) = NULL;
+ u8 header[] = { command, index };
+ size_t header_size = ARRAY_SIZE(header);
+ int timeout, ret;
+
+ if (header_size + len > GO_S_PACKET_SIZE)
+ return -EINVAL;
+
+ guard(mutex)(&drvdata.cfg_mutex);
+ /* We can't use a devm_alloc reusable buffer without side effects during suspend */
+ dmabuf = kzalloc(GO_S_PACKET_SIZE, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ memcpy(dmabuf, header, header_size);
+ memcpy(dmabuf + header_size, data, len);
+
+ dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+ GO_S_PACKET_SIZE, dmabuf);
+
+ ret = hid_hw_output_report(hdev, dmabuf, GO_S_PACKET_SIZE);
+ if (ret < 0)
+ return ret;
+
+ ret = ret == GO_S_PACKET_SIZE ? 0 : -EINVAL;
+ if (ret)
+ return ret;
+
+ /* PL_TEST commands can take longer because they go out to another device */
+ timeout = (command == GET_PL_TEST) ? 200 : 5;
+ ret = wait_for_completion_interruptible_timeout(&drvdata.send_cmd_complete,
+ msecs_to_jiffies(timeout));
+
+ if (ret == 0) /* timeout occurred */
+ ret = -EBUSY;
+
+ reinit_completion(&drvdata.send_cmd_complete);
+ return 0;
+}
+
+static void cfg_setup(struct work_struct *work)
+{
+ int ret;
+
+ ret = mcu_property_out(drvdata.hdev, GET_VERSION, FEATURE_NONE, 0, 0);
+ if (ret) {
+ dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU Version: %i\n", ret);
+ return;
+ }
+}
+
+static int hid_gos_cfg_probe(struct hid_device *hdev,
+ const struct hid_device_id *_id)
+{
+ int ret;
+
+ hid_set_drvdata(hdev, &drvdata);
+ drvdata.hdev = hdev;
+ mutex_init(&drvdata.cfg_mutex);
+
+ init_completion(&drvdata.send_cmd_complete);
+
+ /* Executing calls prior to returning from probe will lock the MCU. Schedule
+ * initial data call after probe has completed and MCU can accept calls.
+ */
+ INIT_DELAYED_WORK(&drvdata.gos_cfg_setup, &cfg_setup);
+ ret = schedule_delayed_work(&drvdata.gos_cfg_setup, msecs_to_jiffies(2));
+ if (!ret) {
+ dev_err(&hdev->dev, "Failed to schedule startup delayed work\n");
+ return -ENODEV;
+ }
+
+ return 0;
+}
+
+static void hid_gos_cfg_remove(struct hid_device *hdev)
+{
+ guard(mutex)(&drvdata.cfg_mutex);
+ cancel_delayed_work_sync(&drvdata.gos_cfg_setup);
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ hid_set_drvdata(hdev, NULL);
+}
+
+static int hid_gos_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ int ret, ep;
+
+ ret = hid_parse(hdev);
+ if (ret) {
+ hid_err(hdev, "Parse failed\n");
+ return ret;
+ }
+
+ ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+ if (ret) {
+ hid_err(hdev, "Failed to start HID device\n");
+ return ret;
+ }
+
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_err(hdev, "Failed to open HID device\n");
+ hid_hw_stop(hdev);
+ return ret;
+ }
+
+ ep = get_endpoint_address(hdev);
+ if (ep != GO_S_CFG_INTF_IN) {
+ dev_dbg(&hdev->dev, "Started interface %x as generic HID device.\n", ep);
+ return 0;
+ }
+
+ ret = hid_gos_cfg_probe(hdev, id);
+ if (ret)
+ dev_err_probe(&hdev->dev, ret, "Failed to start configuration interface");
+
+ dev_dbg(&hdev->dev, "Started interface %x as Go S configuration interface\n", ep);
+ return ret;
+}
+
+static void hid_gos_remove(struct hid_device *hdev)
+{
+ int ep = get_endpoint_address(hdev);
+
+ switch (ep) {
+ case GO_S_CFG_INTF_IN:
+ hid_gos_cfg_remove(hdev);
+ break;
+ default:
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+
+ break;
+ }
+}
+
+static const struct hid_device_id hid_gos_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_QHE,
+ USB_DEVICE_ID_LENOVO_LEGION_GO_S_XINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_QHE,
+ USB_DEVICE_ID_LENOVO_LEGION_GO_S_DINPUT) },
+ {}
+};
+
+MODULE_DEVICE_TABLE(hid, hid_gos_devices);
+static struct hid_driver hid_lenovo_go_s = {
+ .name = "hid-lenovo-go-s",
+ .id_table = hid_gos_devices,
+ .probe = hid_gos_probe,
+ .remove = hid_gos_remove,
+ .raw_event = hid_gos_raw_event,
+};
+module_hid_driver(hid_lenovo_go_s);
+
+MODULE_AUTHOR("Derek J. Clark");
+MODULE_DESCRIPTION("HID Driver for Lenovo Legion Go S Series gamepad.");
+MODULE_LICENSE("GPL");
--
2.52.0
^ permalink raw reply related
* [PATCH v5 11/16] HID: hid-lenovo-go-s: Add MCU ID Attribute
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds command to probe for the MCU ID of the Lenovo Legion Go S
Controller and assign it to a device attribute.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go-s.c | 56 +++++++++++++++++++++++++++++++++++
1 file changed, 56 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index d8d32a9c90512..cea015330c2b6 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -18,6 +18,7 @@
#include <linux/mutex.h>
#include <linux/printk.h>
#include <linux/string.h>
+#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/unaligned.h>
#include <linux/usb.h>
@@ -34,8 +35,13 @@ struct hid_gos_cfg {
struct completion send_cmd_complete;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
+ u8 mcu_id[12];
} drvdata;
+struct gos_cfg_attr {
+ u8 index;
+};
+
struct command_report {
u8 cmd;
u8 sub_cmd;
@@ -70,6 +76,14 @@ static int hid_gos_version_event(u8 *data)
return 0;
}
+static int hid_gos_mcu_id_event(struct command_report *cmd_rep)
+{
+ drvdata.mcu_id[0] = cmd_rep->sub_cmd;
+ memcpy(&drvdata.mcu_id[1], cmd_rep->data, 11);
+
+ return 0;
+}
+
static u8 get_endpoint_address(struct hid_device *hdev)
{
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -103,6 +117,9 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_VERSION:
ret = hid_gos_version_event(data);
break;
+ case GET_MCU_ID:
+ ret = hid_gos_mcu_id_event(cmd_rep);
+ break;
default:
ret = -EINVAL;
break;
@@ -157,10 +174,41 @@ static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index,
return 0;
}
+static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "%*phN\n", 12, &drvdata.mcu_id);
+}
+
+/* MCU */
+static DEVICE_ATTR_RO(mcu_id);
+
+static struct attribute *legos_mcu_attrs[] = {
+ &dev_attr_mcu_id.attr,
+ NULL,
+};
+
+static const struct attribute_group mcu_attr_group = {
+ .attrs = legos_mcu_attrs,
+};
+
+static const struct attribute_group *top_level_attr_groups[] = {
+ &mcu_attr_group,
+ NULL,
+};
+
static void cfg_setup(struct work_struct *work)
{
int ret;
+ /* MCU */
+ ret = mcu_property_out(drvdata.hdev, GET_MCU_ID, FEATURE_NONE, 0, 0);
+ if (ret) {
+ dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU ID: %i\n",
+ ret);
+ return;
+ }
+
ret = mcu_property_out(drvdata.hdev, GET_VERSION, FEATURE_NONE, 0, 0);
if (ret) {
dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU Version: %i\n", ret);
@@ -177,6 +225,13 @@ static int hid_gos_cfg_probe(struct hid_device *hdev,
drvdata.hdev = hdev;
mutex_init(&drvdata.cfg_mutex);
+ ret = sysfs_create_groups(&hdev->dev.kobj, top_level_attr_groups);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret,
+ "Failed to create gamepad configuration attributes\n");
+ return ret;
+ }
+
init_completion(&drvdata.send_cmd_complete);
/* Executing calls prior to returning from probe will lock the MCU. Schedule
@@ -196,6 +251,7 @@ static void hid_gos_cfg_remove(struct hid_device *hdev)
{
guard(mutex)(&drvdata.cfg_mutex);
cancel_delayed_work_sync(&drvdata.gos_cfg_setup);
+ sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups);
hid_hw_close(hdev);
hid_hw_stop(hdev);
hid_set_drvdata(hdev, NULL);
--
2.52.0
^ permalink raw reply related
* [PATCH v5 12/16] HID: hid-lenovo-go-s: Add Feature Status Attributes
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds features status attributes for the gamepad, MCU, touchpad/mouse,
and IMU devices.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v4:
- Cleaner formatting on debug message.
---
drivers/hid/hid-lenovo-go-s.c | 487 +++++++++++++++++++++++++++++++++-
1 file changed, 486 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index cea015330c2b6..423b64cd02902 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -15,6 +15,7 @@
#include <linux/device.h>
#include <linux/hid.h>
#include <linux/jiffies.h>
+#include <linux/kstrtox.h>
#include <linux/mutex.h>
#include <linux/printk.h>
#include <linux/string.h>
@@ -35,7 +36,17 @@ struct hid_gos_cfg {
struct completion send_cmd_complete;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
+ u8 gp_auto_sleep_time;
+ u8 gp_dpad_mode;
+ u8 gp_mode;
+ u8 gp_poll_rate;
+ u8 imu_bypass_en;
+ u8 imu_sensor_en;
u8 mcu_id[12];
+ u8 mouse_step;
+ u8 os_mode;
+ u8 rgb_en;
+ u8 tp_en;
} drvdata;
struct gos_cfg_attr {
@@ -66,7 +77,73 @@ enum mcu_command_index {
GET_PL_TEST = 0xdf,
};
-#define FEATURE_NONE 0x00
+enum feature_enabled_index {
+ FEATURE_DISABLED,
+ FEATURE_ENABLED,
+};
+
+static const char *const feature_enabled_text[] = {
+ [FEATURE_DISABLED] = "false",
+ [FEATURE_ENABLED] = "true",
+};
+
+enum feature_status_index {
+ FEATURE_NONE = 0x00,
+ FEATURE_GAMEPAD_MODE = 0x01,
+ FEATURE_AUTO_SLEEP_TIME = 0x04,
+ FEATURE_IMU_BYPASS,
+ FEATURE_RGB_ENABLE,
+ FEATURE_IMU_ENABLE,
+ FEATURE_TOUCHPAD_ENABLE,
+ FEATURE_OS_MODE = 0x0A,
+ FEATURE_POLL_RATE = 0x10,
+ FEATURE_DPAD_MODE,
+ FEATURE_MOUSE_WHEEL_STEP,
+};
+
+enum gamepad_mode_index {
+ XINPUT,
+ DINPUT,
+};
+
+static const char *const gamepad_mode_text[] = {
+ [XINPUT] = "xinput",
+ [DINPUT] = "dinput",
+};
+
+enum os_type_index {
+ WINDOWS,
+ LINUX,
+};
+
+static const char *const os_type_text[] = {
+ [WINDOWS] = "windows",
+ [LINUX] = "linux",
+};
+
+enum poll_rate_index {
+ HZ125,
+ HZ250,
+ HZ500,
+ HZ1000,
+};
+
+static const char *const poll_rate_text[] = {
+ [HZ125] = "125",
+ [HZ250] = "250",
+ [HZ500] = "500",
+ [HZ1000] = "1000",
+};
+
+enum dpad_mode_index {
+ DIR8,
+ DIR4,
+};
+
+static const char *const dpad_mode_text[] = {
+ [DIR8] = "8-way",
+ [DIR4] = "4-way",
+};
static int hid_gos_version_event(u8 *data)
{
@@ -84,6 +161,57 @@ static int hid_gos_mcu_id_event(struct command_report *cmd_rep)
return 0;
}
+static int hid_gos_gamepad_cfg_event(struct command_report *cmd_rep)
+{
+ int ret = 0;
+
+ switch (cmd_rep->sub_cmd) {
+ case FEATURE_GAMEPAD_MODE:
+ drvdata.gp_mode = cmd_rep->data[0];
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ drvdata.gp_auto_sleep_time = cmd_rep->data[0];
+ break;
+ case FEATURE_IMU_BYPASS:
+ drvdata.imu_bypass_en = cmd_rep->data[0];
+ break;
+ case FEATURE_RGB_ENABLE:
+ drvdata.rgb_en = cmd_rep->data[0];
+ break;
+ case FEATURE_IMU_ENABLE:
+ drvdata.imu_sensor_en = cmd_rep->data[0];
+ break;
+ case FEATURE_TOUCHPAD_ENABLE:
+ drvdata.tp_en = cmd_rep->data[0];
+ break;
+ case FEATURE_OS_MODE:
+ drvdata.os_mode = cmd_rep->data[0];
+ break;
+ case FEATURE_POLL_RATE:
+ drvdata.gp_poll_rate = cmd_rep->data[0];
+ break;
+ case FEATURE_DPAD_MODE:
+ drvdata.gp_dpad_mode = cmd_rep->data[0];
+ break;
+ case FEATURE_MOUSE_WHEEL_STEP:
+ drvdata.mouse_step = cmd_rep->data[0];
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ return ret;
+}
+
+static int hid_gos_set_event_return(struct command_report *cmd_rep)
+{
+ if (cmd_rep->data[0] != 0)
+ return -EIO;
+
+ return 0;
+}
+
static u8 get_endpoint_address(struct hid_device *hdev)
{
struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
@@ -120,6 +248,12 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_MCU_ID:
ret = hid_gos_mcu_id_event(cmd_rep);
break;
+ case GET_GAMEPAD_CFG:
+ ret = hid_gos_gamepad_cfg_event(cmd_rep);
+ break;
+ case SET_GAMEPAD_CFG:
+ ret = hid_gos_set_event_return(cmd_rep);
+ break;
default:
ret = -EINVAL;
break;
@@ -174,17 +308,332 @@ static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index,
return 0;
}
+static ssize_t gamepad_property_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count,
+ enum feature_status_index index)
+{
+ size_t size = 1;
+ u8 val = 0;
+ int ret;
+
+ switch (index) {
+ case FEATURE_GAMEPAD_MODE:
+ ret = sysfs_match_string(gamepad_mode_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val < 0 || val > 255)
+ return -EINVAL;
+ break;
+ case FEATURE_IMU_ENABLE:
+ ret = sysfs_match_string(feature_enabled_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_IMU_BYPASS:
+ ret = sysfs_match_string(feature_enabled_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_RGB_ENABLE:
+ ret = sysfs_match_string(feature_enabled_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_TOUCHPAD_ENABLE:
+ ret = sysfs_match_string(feature_enabled_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_OS_MODE:
+ ret = sysfs_match_string(os_type_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_POLL_RATE:
+ ret = sysfs_match_string(poll_rate_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_DPAD_MODE:
+ ret = sysfs_match_string(dpad_mode_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case FEATURE_MOUSE_WHEEL_STEP:
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+ if (val < 1 || val > 127)
+ return -EINVAL;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (!val)
+ size = 0;
+
+ ret = mcu_property_out(drvdata.hdev, SET_GAMEPAD_CFG, index, &val,
+ size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t gamepad_property_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum feature_status_index index)
+{
+ size_t count = 0;
+ u8 i;
+
+ count = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, index, 0, 0);
+ if (count < 0)
+ return count;
+
+ switch (index) {
+ case FEATURE_GAMEPAD_MODE:
+ i = drvdata.gp_mode;
+ if (i >= ARRAY_SIZE(gamepad_mode_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", gamepad_mode_text[i]);
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ count = sysfs_emit(buf, "%u\n", drvdata.gp_auto_sleep_time);
+ break;
+ case FEATURE_IMU_ENABLE:
+ i = drvdata.imu_sensor_en;
+ if (i >= ARRAY_SIZE(feature_enabled_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+ break;
+ case FEATURE_IMU_BYPASS:
+ i = drvdata.imu_bypass_en;
+ if (i >= ARRAY_SIZE(feature_enabled_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+ break;
+ case FEATURE_RGB_ENABLE:
+ i = drvdata.rgb_en;
+ if (i >= ARRAY_SIZE(feature_enabled_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+ break;
+ case FEATURE_TOUCHPAD_ENABLE:
+ i = drvdata.tp_en;
+ if (i >= ARRAY_SIZE(feature_enabled_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]);
+ break;
+ case FEATURE_OS_MODE:
+ i = drvdata.os_mode;
+ if (i >= ARRAY_SIZE(os_type_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", os_type_text[i]);
+ break;
+ case FEATURE_POLL_RATE:
+ i = drvdata.gp_poll_rate;
+ if (i >= ARRAY_SIZE(poll_rate_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", poll_rate_text[i]);
+ break;
+ case FEATURE_DPAD_MODE:
+ i = drvdata.gp_dpad_mode;
+ if (i >= ARRAY_SIZE(dpad_mode_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", dpad_mode_text[i]);
+ break;
+ case FEATURE_MOUSE_WHEEL_STEP:
+ i = drvdata.mouse_step;
+ if (i < 1 || i > 127)
+ return -EINVAL;
+ count = sysfs_emit(buf, "%u\n", i);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return count;
+}
+
+static ssize_t gamepad_property_options(struct device *dev,
+ struct device_attribute *attr,
+ char *buf,
+ enum feature_status_index index)
+{
+ size_t count = 0;
+ unsigned int i;
+
+ switch (index) {
+ case FEATURE_GAMEPAD_MODE:
+ for (i = 0; i < ARRAY_SIZE(gamepad_mode_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ gamepad_mode_text[i]);
+ }
+ break;
+ case FEATURE_AUTO_SLEEP_TIME:
+ return sysfs_emit(buf, "0-255\n");
+ case FEATURE_IMU_ENABLE:
+ for (i = 0; i < ARRAY_SIZE(feature_enabled_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ feature_enabled_text[i]);
+ }
+ break;
+ case FEATURE_IMU_BYPASS:
+ case FEATURE_RGB_ENABLE:
+ case FEATURE_TOUCHPAD_ENABLE:
+ for (i = 0; i < ARRAY_SIZE(feature_enabled_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ feature_enabled_text[i]);
+ }
+ break;
+ case FEATURE_OS_MODE:
+ for (i = 0; i < ARRAY_SIZE(os_type_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ os_type_text[i]);
+ }
+ break;
+ case FEATURE_POLL_RATE:
+ for (i = 0; i < ARRAY_SIZE(poll_rate_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ poll_rate_text[i]);
+ }
+ break;
+ case FEATURE_DPAD_MODE:
+ for (i = 0; i < ARRAY_SIZE(dpad_mode_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ dpad_mode_text[i]);
+ }
+ break;
+ case FEATURE_MOUSE_WHEEL_STEP:
+ return sysfs_emit(buf, "1-127\n");
+ default:
+ return count;
+ }
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
return sysfs_emit(buf, "%*phN\n", 12, &drvdata.mcu_id);
}
+#define LEGOS_DEVICE_ATTR_RW(_name, _attrname, _rtype, _group) \
+ static ssize_t _name##_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+ { \
+ return _group##_property_store(dev, attr, buf, count, \
+ _name.index); \
+ } \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_property_show(dev, attr, buf, _name.index); \
+ } \
+ static ssize_t _name##_##_rtype##_show( \
+ struct device *dev, struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_property_options(dev, attr, buf, _name.index); \
+ } \
+ static DEVICE_ATTR_RW_NAMED(_name, _attrname)
+
+#define LEGOS_DEVICE_ATTR_RO(_name, _attrname, _group) \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_property_show(dev, attr, buf, _name.index); \
+ } \
+ static DEVICE_ATTR_RO_NAMED(_name, _attrname)
+
+/* Gamepad */
+struct gos_cfg_attr auto_sleep_time = { FEATURE_AUTO_SLEEP_TIME };
+LEGOS_DEVICE_ATTR_RW(auto_sleep_time, "auto_sleep_time", range, gamepad);
+static DEVICE_ATTR_RO(auto_sleep_time_range);
+
+struct gos_cfg_attr dpad_mode = { FEATURE_DPAD_MODE };
+LEGOS_DEVICE_ATTR_RW(dpad_mode, "dpad_mode", index, gamepad);
+static DEVICE_ATTR_RO(dpad_mode_index);
+
+struct gos_cfg_attr gamepad_mode = { FEATURE_GAMEPAD_MODE };
+LEGOS_DEVICE_ATTR_RW(gamepad_mode, "mode", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");
+
+struct gos_cfg_attr gamepad_poll_rate = { FEATURE_POLL_RATE };
+LEGOS_DEVICE_ATTR_RW(gamepad_poll_rate, "poll_rate", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(gamepad_poll_rate_index, "poll_rate_index");
+
+static struct attribute *legos_gamepad_attrs[] = {
+ &dev_attr_auto_sleep_time.attr,
+ &dev_attr_auto_sleep_time_range.attr,
+ &dev_attr_dpad_mode.attr,
+ &dev_attr_dpad_mode_index.attr,
+ &dev_attr_gamepad_mode.attr,
+ &dev_attr_gamepad_mode_index.attr,
+ &dev_attr_gamepad_poll_rate.attr,
+ &dev_attr_gamepad_poll_rate_index.attr,
+ NULL,
+};
+
+static const struct attribute_group gamepad_attr_group = {
+ .name = "gamepad",
+ .attrs = legos_gamepad_attrs,
+};
+
+/* IMU */
+struct gos_cfg_attr imu_bypass_enabled = { FEATURE_IMU_BYPASS };
+LEGOS_DEVICE_ATTR_RW(imu_bypass_enabled, "bypass_enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(imu_bypass_enabled_index, "bypass_enabled_index");
+
+struct gos_cfg_attr imu_sensor_enabled = { FEATURE_IMU_ENABLE };
+LEGOS_DEVICE_ATTR_RW(imu_sensor_enabled, "sensor_enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(imu_sensor_enabled_index, "sensor_enabled_index");
+
+static struct attribute *legos_imu_attrs[] = {
+ &dev_attr_imu_bypass_enabled.attr,
+ &dev_attr_imu_bypass_enabled_index.attr,
+ &dev_attr_imu_sensor_enabled.attr,
+ &dev_attr_imu_sensor_enabled_index.attr,
+ NULL,
+};
+
+static const struct attribute_group imu_attr_group = {
+ .name = "imu",
+ .attrs = legos_imu_attrs,
+};
+
/* MCU */
static DEVICE_ATTR_RO(mcu_id);
+struct gos_cfg_attr os_mode = { FEATURE_OS_MODE };
+LEGOS_DEVICE_ATTR_RW(os_mode, "os_mode", index, gamepad);
+static DEVICE_ATTR_RO(os_mode_index);
+
static struct attribute *legos_mcu_attrs[] = {
&dev_attr_mcu_id.attr,
+ &dev_attr_os_mode.attr,
+ &dev_attr_os_mode_index.attr,
NULL,
};
@@ -192,8 +641,44 @@ static const struct attribute_group mcu_attr_group = {
.attrs = legos_mcu_attrs,
};
+/* Mouse */
+struct gos_cfg_attr mouse_wheel_step = { FEATURE_MOUSE_WHEEL_STEP };
+LEGOS_DEVICE_ATTR_RW(mouse_wheel_step, "step", range, gamepad);
+static DEVICE_ATTR_RO_NAMED(mouse_wheel_step_range, "step_range");
+
+static struct attribute *legos_mouse_attrs[] = {
+ &dev_attr_mouse_wheel_step.attr,
+ &dev_attr_mouse_wheel_step_range.attr,
+ NULL,
+};
+
+static const struct attribute_group mouse_attr_group = {
+ .name = "mouse",
+ .attrs = legos_mouse_attrs,
+};
+
+/* Touchpad */
+struct gos_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE };
+LEGOS_DEVICE_ATTR_RW(touchpad_enabled, "enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
+
+static struct attribute *legos_touchpad_attrs[] = {
+ &dev_attr_touchpad_enabled.attr,
+ &dev_attr_touchpad_enabled_index.attr,
+ NULL,
+};
+
+static const struct attribute_group touchpad_attr_group = {
+ .name = "touchpad",
+ .attrs = legos_touchpad_attrs,
+};
+
static const struct attribute_group *top_level_attr_groups[] = {
+ &gamepad_attr_group,
+ &imu_attr_group,
&mcu_attr_group,
+ &mouse_attr_group,
+ &touchpad_attr_group,
NULL,
};
--
2.52.0
^ permalink raw reply related
* [PATCH v5 13/16] HID: hid-lenovo-go-s: Add Touchpad Mode Attributes
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds attributes for managing the touchpad operating modes.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-lenovo-go-s.c | 142 ++++++++++++++++++++++++++++++++++
1 file changed, 142 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index 423b64cd02902..02d4e6548285e 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -47,6 +47,8 @@ struct hid_gos_cfg {
u8 os_mode;
u8 rgb_en;
u8 tp_en;
+ u8 tp_linux_mode;
+ u8 tp_windows_mode;
} drvdata;
struct gos_cfg_attr {
@@ -145,6 +147,22 @@ static const char *const dpad_mode_text[] = {
[DIR4] = "4-way",
};
+enum touchpad_mode_index {
+ TP_REL,
+ TP_ABS,
+};
+
+static const char *const touchpad_mode_text[] = {
+ [TP_REL] = "relative",
+ [TP_ABS] = "absolute",
+};
+
+enum touchpad_config_index {
+ CFG_WINDOWS_MODE = 0x03,
+ CFG_LINUX_MODE,
+
+};
+
static int hid_gos_version_event(u8 *data)
{
struct version_report *ver_rep = (struct version_report *)data;
@@ -204,6 +222,25 @@ static int hid_gos_gamepad_cfg_event(struct command_report *cmd_rep)
return ret;
}
+static int hid_gos_touchpad_event(struct command_report *cmd_rep)
+{
+ int ret = 0;
+
+ switch (cmd_rep->sub_cmd) {
+ case CFG_LINUX_MODE:
+ drvdata.tp_linux_mode = cmd_rep->data[0];
+ break;
+ case CFG_WINDOWS_MODE:
+ drvdata.tp_windows_mode = cmd_rep->data[0];
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ return ret;
+}
+
static int hid_gos_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -251,7 +288,11 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_GAMEPAD_CFG:
ret = hid_gos_gamepad_cfg_event(cmd_rep);
break;
+ case GET_TP_PARAM:
+ ret = hid_gos_touchpad_event(cmd_rep);
+ break;
case SET_GAMEPAD_CFG:
+ case SET_TP_PARAM:
ret = hid_gos_set_event_return(cmd_rep);
break;
default:
@@ -533,6 +574,95 @@ static ssize_t gamepad_property_options(struct device *dev,
return count;
}
+static ssize_t touchpad_property_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count,
+ enum touchpad_config_index index)
+{
+ size_t size = 1;
+ u8 val = 0;
+ int ret;
+
+ switch (index) {
+ case CFG_WINDOWS_MODE:
+ ret = sysfs_match_string(touchpad_mode_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ case CFG_LINUX_MODE:
+ ret = sysfs_match_string(touchpad_mode_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+ break;
+ default:
+ return -EINVAL;
+ }
+ if (!val)
+ size = 0;
+
+ ret = mcu_property_out(drvdata.hdev, SET_TP_PARAM, index, &val, size);
+ if (ret < 0)
+ return ret;
+
+ return count;
+}
+
+static ssize_t touchpad_property_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum touchpad_config_index index)
+{
+ int ret = 0;
+ u8 i;
+
+ ret = mcu_property_out(drvdata.hdev, GET_TP_PARAM, index, 0, 0);
+ if (ret < 0)
+ return ret;
+
+ switch (index) {
+ case CFG_WINDOWS_MODE:
+ i = drvdata.tp_windows_mode;
+ break;
+ case CFG_LINUX_MODE:
+ i = drvdata.tp_linux_mode;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (i >= ARRAY_SIZE(touchpad_mode_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", touchpad_mode_text[i]);
+}
+
+static ssize_t touchpad_property_options(struct device *dev,
+ struct device_attribute *attr,
+ char *buf,
+ enum touchpad_config_index index)
+{
+ size_t count = 0;
+ unsigned int i;
+
+ switch (index) {
+ case CFG_WINDOWS_MODE:
+ case CFG_LINUX_MODE:
+ for (i = 0; i < ARRAY_SIZE(touchpad_mode_text); i++) {
+ count += sysfs_emit_at(buf, count, "%s ",
+ touchpad_mode_text[i]);
+ }
+ break;
+ default:
+ return count;
+ }
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
@@ -662,9 +792,21 @@ struct gos_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE };
LEGOS_DEVICE_ATTR_RW(touchpad_enabled, "enabled", index, gamepad);
static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");
+struct gos_cfg_attr touchpad_linux_mode = { CFG_LINUX_MODE };
+LEGOS_DEVICE_ATTR_RW(touchpad_linux_mode, "linux_mode", index, touchpad);
+static DEVICE_ATTR_RO_NAMED(touchpad_linux_mode_index, "linux_mode_index");
+
+struct gos_cfg_attr touchpad_windows_mode = { CFG_WINDOWS_MODE };
+LEGOS_DEVICE_ATTR_RW(touchpad_windows_mode, "windows_mode", index, touchpad);
+static DEVICE_ATTR_RO_NAMED(touchpad_windows_mode_index, "windows_mode_index");
+
static struct attribute *legos_touchpad_attrs[] = {
&dev_attr_touchpad_enabled.attr,
&dev_attr_touchpad_enabled_index.attr,
+ &dev_attr_touchpad_linux_mode.attr,
+ &dev_attr_touchpad_linux_mode_index.attr,
+ &dev_attr_touchpad_windows_mode.attr,
+ &dev_attr_touchpad_windows_mode_index.attr,
NULL,
};
--
2.52.0
^ permalink raw reply related
* [PATCH v5 14/16] HID: hid-lenovo-go-s: Add RGB LED control interface
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds an LED multicolor class device and attribute group for controlling
the RGB of the Left and right joystick rings. In addition to the standard
led_cdev attributes, additional attributes that allow for the control of
the effect (monocolor, breathe, rainbow, and chroma), speed of the
effect change, an enable toggle, and profile.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v4:
- Cleaner formatting on multiple debug messages.
---
drivers/hid/hid-lenovo-go-s.c | 424 ++++++++++++++++++++++++++++++++++
1 file changed, 424 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index 02d4e6548285e..1a7de95e2a065 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -16,6 +16,7 @@
#include <linux/hid.h>
#include <linux/jiffies.h>
#include <linux/kstrtox.h>
+#include <linux/led-class-multicolor.h>
#include <linux/mutex.h>
#include <linux/printk.h>
#include <linux/string.h>
@@ -34,6 +35,7 @@
struct hid_gos_cfg {
struct delayed_work gos_cfg_setup;
struct completion send_cmd_complete;
+ struct led_classdev *led_cdev;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
u8 gp_auto_sleep_time;
@@ -45,7 +47,11 @@ struct hid_gos_cfg {
u8 mcu_id[12];
u8 mouse_step;
u8 os_mode;
+ u8 rgb_effect;
u8 rgb_en;
+ u8 rgb_mode;
+ u8 rgb_profile;
+ u8 rgb_speed;
u8 tp_en;
u8 tp_linux_mode;
u8 tp_windows_mode;
@@ -163,6 +169,38 @@ enum touchpad_config_index {
};
+enum rgb_mode_index {
+ RGB_MODE_DYNAMIC,
+ RGB_MODE_CUSTOM,
+};
+
+static const char *const rgb_mode_text[] = {
+ [RGB_MODE_DYNAMIC] = "dynamic",
+ [RGB_MODE_CUSTOM] = "custom",
+};
+
+enum rgb_effect_index {
+ RGB_EFFECT_MONO,
+ RGB_EFFECT_BREATHE,
+ RGB_EFFECT_CHROMA,
+ RGB_EFFECT_RAINBOW,
+};
+
+static const char *const rgb_effect_text[] = {
+ [RGB_EFFECT_MONO] = "monocolor",
+ [RGB_EFFECT_BREATHE] = "breathe",
+ [RGB_EFFECT_CHROMA] = "chroma",
+ [RGB_EFFECT_RAINBOW] = "rainbow",
+};
+
+enum rgb_config_index {
+ LIGHT_MODE_SEL = 0x01,
+ LIGHT_PROFILE_SEL,
+ USR_LIGHT_PROFILE_1,
+ USR_LIGHT_PROFILE_2,
+ USR_LIGHT_PROFILE_3,
+};
+
static int hid_gos_version_event(u8 *data)
{
struct version_report *ver_rep = (struct version_report *)data;
@@ -241,6 +279,39 @@ static int hid_gos_touchpad_event(struct command_report *cmd_rep)
return ret;
}
+static int hid_gos_light_event(struct command_report *cmd_rep)
+{
+ struct led_classdev_mc *mc_cdev;
+ int ret = 0;
+
+ switch (cmd_rep->sub_cmd) {
+ case LIGHT_MODE_SEL:
+ drvdata.rgb_mode = cmd_rep->data[0];
+ ret = 0;
+ break;
+ case LIGHT_PROFILE_SEL:
+ drvdata.rgb_profile = cmd_rep->data[0];
+ ret = 0;
+ break;
+ case USR_LIGHT_PROFILE_1:
+ case USR_LIGHT_PROFILE_2:
+ case USR_LIGHT_PROFILE_3:
+ mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ drvdata.rgb_effect = cmd_rep->data[0];
+ mc_cdev->subled_info[0].intensity = cmd_rep->data[1];
+ mc_cdev->subled_info[1].intensity = cmd_rep->data[2];
+ mc_cdev->subled_info[2].intensity = cmd_rep->data[3];
+ drvdata.led_cdev->brightness = cmd_rep->data[4];
+ drvdata.rgb_speed = cmd_rep->data[5];
+ ret = 0;
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ return ret;
+}
+
static int hid_gos_set_event_return(struct command_report *cmd_rep)
{
if (cmd_rep->data[0] != 0)
@@ -291,7 +362,11 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_TP_PARAM:
ret = hid_gos_touchpad_event(cmd_rep);
break;
+ case GET_RGB_CFG:
+ ret = hid_gos_light_event(cmd_rep);
+ break;
case SET_GAMEPAD_CFG:
+ case SET_RGB_CFG:
case SET_TP_PARAM:
ret = hid_gos_set_event_return(cmd_rep);
break;
@@ -669,6 +744,276 @@ static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
return sysfs_emit(buf, "%*phN\n", 12, &drvdata.mcu_id);
}
+static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd,
+ enum rgb_config_index index, u8 *val, size_t size)
+{
+ if (cmd != SET_RGB_CFG && cmd != GET_RGB_CFG)
+ return -EINVAL;
+
+ if (index < LIGHT_MODE_SEL || index > USR_LIGHT_PROFILE_3)
+ return -EINVAL;
+
+ return mcu_property_out(hdev, cmd, index, val, size);
+}
+
+static int rgb_attr_show(void)
+{
+ enum rgb_config_index index;
+
+ index = drvdata.rgb_profile + 2;
+
+ return rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, index, 0, 0);
+};
+
+static ssize_t rgb_effect_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ u8 effect;
+ int ret;
+
+ ret = sysfs_match_string(rgb_effect_text, buf);
+ if (ret < 0)
+ return ret;
+
+ effect = ret;
+ index = drvdata.rgb_profile + 2;
+ u8 rgb_profile[6] = { effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ drvdata.led_cdev->brightness,
+ drvdata.rgb_speed };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_effect = effect;
+ return count;
+};
+
+static ssize_t rgb_effect_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int ret;
+
+ ret = rgb_attr_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_effect >= ARRAY_SIZE(rgb_effect_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", rgb_effect_text[drvdata.rgb_effect]);
+}
+
+static ssize_t rgb_effect_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(rgb_effect_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", rgb_effect_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t rgb_speed_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ int val = 0;
+ int ret;
+
+ ret = kstrtoint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val < 0 || val > 100)
+ return -EINVAL;
+
+ index = drvdata.rgb_profile + 2;
+ u8 rgb_profile[6] = { drvdata.rgb_effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ drvdata.led_cdev->brightness,
+ val };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_speed = val;
+
+ return count;
+};
+
+static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = rgb_attr_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_speed > 100)
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed);
+}
+
+static ssize_t rgb_speed_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "0-100\n");
+}
+
+static ssize_t rgb_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+ u8 val;
+
+ ret = sysfs_match_string(rgb_mode_text, buf);
+ if (ret <= 0)
+ return ret;
+
+ val = ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_MODE_SEL, &val,
+ 1);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_mode = val;
+
+ return count;
+};
+
+static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_MODE_SEL, 0, 0);
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_mode >= ARRAY_SIZE(rgb_mode_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", rgb_mode_text[drvdata.rgb_mode]);
+};
+
+static ssize_t rgb_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 1; i < ARRAY_SIZE(rgb_mode_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", rgb_mode_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t rgb_profile_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ size_t size = 1;
+ int ret;
+ u8 val;
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret < 0)
+ return ret;
+
+ if (val < 1 || val > 3)
+ return -EINVAL;
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_PROFILE_SEL, &val,
+ size);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_profile = val;
+
+ return count;
+};
+
+static ssize_t rgb_profile_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int ret;
+
+ ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_PROFILE_SEL, 0,
+ 0);
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_profile < 1 || drvdata.rgb_profile > 3)
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%hhu\n", drvdata.rgb_profile);
+};
+
+static ssize_t rgb_profile_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "1-3\n");
+}
+
+static void hid_gos_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev);
+ enum rgb_config_index index;
+ int ret;
+
+ if (brightness > led_cdev->max_brightness) {
+ dev_err(led_cdev->dev, "Invalid argument\n");
+ return;
+ }
+
+ index = drvdata.rgb_profile + 2;
+ u8 rgb_profile[6] = { drvdata.rgb_effect,
+ mc_cdev->subled_info[0].intensity,
+ mc_cdev->subled_info[1].intensity,
+ mc_cdev->subled_info[2].intensity,
+ brightness,
+ drvdata.rgb_speed };
+
+ ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6);
+ switch (ret) {
+ case 0:
+ led_cdev->brightness = brightness;
+ break;
+ case -ENODEV: /* during switch to IAP -ENODEV is expected */
+ case -ENOSYS: /* during rmmod -ENOSYS is expected */
+ dev_dbg(led_cdev->dev, "Failed to write RGB profile: %i\n",
+ ret);
+ break;
+ default:
+ dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n",
+ ret);
+ };
+}
+
#define LEGOS_DEVICE_ATTR_RW(_name, _attrname, _rtype, _group) \
static ssize_t _name##_store(struct device *dev, \
struct device_attribute *attr, \
@@ -824,6 +1169,70 @@ static const struct attribute_group *top_level_attr_groups[] = {
NULL,
};
+/* RGB */
+struct gos_cfg_attr rgb_enabled = { FEATURE_RGB_ENABLE };
+LEGOS_DEVICE_ATTR_RW(rgb_enabled, "enabled", index, gamepad);
+static DEVICE_ATTR_RO_NAMED(rgb_enabled_index, "enabled_index");
+
+static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect");
+static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index");
+static DEVICE_ATTR_RW_NAMED(rgb_mode, "mode");
+static DEVICE_ATTR_RO_NAMED(rgb_mode_index, "mode_index");
+static DEVICE_ATTR_RW_NAMED(rgb_profile, "profile");
+static DEVICE_ATTR_RO_NAMED(rgb_profile_range, "profile_range");
+static DEVICE_ATTR_RW_NAMED(rgb_speed, "speed");
+static DEVICE_ATTR_RO_NAMED(rgb_speed_range, "speed_range");
+
+static struct attribute *gos_rgb_attrs[] = {
+ &dev_attr_rgb_enabled.attr,
+ &dev_attr_rgb_enabled_index.attr,
+ &dev_attr_rgb_effect.attr,
+ &dev_attr_rgb_effect_index.attr,
+ &dev_attr_rgb_mode.attr,
+ &dev_attr_rgb_mode_index.attr,
+ &dev_attr_rgb_profile.attr,
+ &dev_attr_rgb_profile_range.attr,
+ &dev_attr_rgb_speed.attr,
+ &dev_attr_rgb_speed_range.attr,
+ NULL,
+};
+
+static struct attribute_group rgb_attr_group = {
+ .attrs = gos_rgb_attrs,
+};
+
+struct mc_subled gos_rgb_subled_info[] = {
+ {
+ .color_index = LED_COLOR_ID_RED,
+ .brightness = 0x50,
+ .intensity = 0x24,
+ .channel = 0x1,
+ },
+ {
+ .color_index = LED_COLOR_ID_GREEN,
+ .brightness = 0x50,
+ .intensity = 0x22,
+ .channel = 0x2,
+ },
+ {
+ .color_index = LED_COLOR_ID_BLUE,
+ .brightness = 0x50,
+ .intensity = 0x99,
+ .channel = 0x3,
+ },
+};
+
+struct led_classdev_mc gos_cdev_rgb = {
+ .led_cdev = {
+ .name = "go_s:rgb:joystick_rings",
+ .brightness = 0x50,
+ .max_brightness = 0x64,
+ .brightness_set = hid_gos_brightness_set,
+ },
+ .num_colors = ARRAY_SIZE(gos_rgb_subled_info),
+ .subled_info = gos_rgb_subled_info,
+};
+
static void cfg_setup(struct work_struct *work)
{
int ret;
@@ -859,6 +1268,21 @@ static int hid_gos_cfg_probe(struct hid_device *hdev,
return ret;
}
+ ret = devm_led_classdev_multicolor_register(&hdev->dev, &gos_cdev_rgb);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret, "Failed to create RGB device\n");
+ return ret;
+ }
+
+ ret = devm_device_add_group(gos_cdev_rgb.led_cdev.dev, &rgb_attr_group);
+ if (ret) {
+ dev_err_probe(&hdev->dev, ret,
+ "Failed to create RGB configuratiion attributes\n");
+ return ret;
+ }
+
+ drvdata.led_cdev = &gos_cdev_rgb.led_cdev;
+
init_completion(&drvdata.send_cmd_complete);
/* Executing calls prior to returning from probe will lock the MCU. Schedule
--
2.52.0
^ permalink raw reply related
* [PATCH v5 15/16] HID: hid-lenovo-go-s: Add IMU and Touchpad RO Attributes
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds attributes for reporting the touchpad manufacturer, version, and
IMU manufacturer.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v4:
- Cache RO values at probe instead of calling them at request. Values
don't change and reading the touchpad data is slow.
v3:
- Fix bug where the touchpad attributes were assigned to the touchpad
_show function instead of the test _show function.
---
drivers/hid/hid-lenovo-go-s.c | 124 ++++++++++++++++++++++++++++++++++
1 file changed, 124 insertions(+)
diff --git a/drivers/hid/hid-lenovo-go-s.c b/drivers/hid/hid-lenovo-go-s.c
index 1a7de95e2a065..cacc5bd5ed2b6 100644
--- a/drivers/hid/hid-lenovo-go-s.c
+++ b/drivers/hid/hid-lenovo-go-s.c
@@ -43,6 +43,7 @@ struct hid_gos_cfg {
u8 gp_mode;
u8 gp_poll_rate;
u8 imu_bypass_en;
+ u8 imu_manufacturer;
u8 imu_sensor_en;
u8 mcu_id[12];
u8 mouse_step;
@@ -54,6 +55,8 @@ struct hid_gos_cfg {
u8 rgb_speed;
u8 tp_en;
u8 tp_linux_mode;
+ u8 tp_manufacturer;
+ u8 tp_version;
u8 tp_windows_mode;
} drvdata;
@@ -201,6 +204,36 @@ enum rgb_config_index {
USR_LIGHT_PROFILE_3,
};
+enum test_command_index {
+ TEST_TP_MFR = 0x02,
+ TEST_IMU_MFR,
+ TEST_TP_VER,
+};
+
+enum tp_mfr_index {
+ TP_NONE,
+ TP_BETTERLIFE,
+ TP_SIPO,
+};
+
+static const char *const touchpad_manufacturer_text[] = {
+ [TP_NONE] = "none",
+ [TP_BETTERLIFE] = "BetterLife",
+ [TP_SIPO] = "SIPO",
+};
+
+enum imu_mfr_index {
+ IMU_NONE,
+ IMU_BOSCH,
+ IMU_ST,
+};
+
+static const char *const imu_manufacturer_text[] = {
+ [IMU_NONE] = "none",
+ [IMU_BOSCH] = "Bosch",
+ [IMU_ST] = "ST",
+};
+
static int hid_gos_version_event(u8 *data)
{
struct version_report *ver_rep = (struct version_report *)data;
@@ -279,6 +312,30 @@ static int hid_gos_touchpad_event(struct command_report *cmd_rep)
return ret;
}
+static int hid_gos_pl_test_event(struct command_report *cmd_rep)
+{
+ int ret = 0;
+
+ switch (cmd_rep->sub_cmd) {
+ case TEST_TP_MFR:
+ drvdata.tp_manufacturer = cmd_rep->data[0];
+ ret = 0;
+ break;
+ case TEST_IMU_MFR:
+ drvdata.imu_manufacturer = cmd_rep->data[0];
+ ret = 0;
+ break;
+ case TEST_TP_VER:
+ drvdata.tp_version = cmd_rep->data[0];
+ ret = 0;
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ return ret;
+}
+
static int hid_gos_light_event(struct command_report *cmd_rep)
{
struct led_classdev_mc *mc_cdev;
@@ -362,6 +419,9 @@ static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report,
case GET_TP_PARAM:
ret = hid_gos_touchpad_event(cmd_rep);
break;
+ case GET_PL_TEST:
+ ret = hid_gos_pl_test_event(cmd_rep);
+ break;
case GET_RGB_CFG:
ret = hid_gos_light_event(cmd_rep);
break;
@@ -738,6 +798,37 @@ static ssize_t touchpad_property_options(struct device *dev,
return count;
}
+static ssize_t test_property_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ enum test_command_index index)
+{
+ size_t count = 0;
+ u8 i;
+
+ switch (index) {
+ case TEST_TP_MFR:
+ i = drvdata.tp_manufacturer;
+ if (i >= ARRAY_SIZE(touchpad_manufacturer_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", touchpad_manufacturer_text[i]);
+ break;
+ case TEST_IMU_MFR:
+ i = drvdata.imu_manufacturer;
+ if (i >= ARRAY_SIZE(imu_manufacturer_text))
+ return -EINVAL;
+ count = sysfs_emit(buf, "%s\n", imu_manufacturer_text[i]);
+ break;
+ case TEST_TP_VER:
+ count = sysfs_emit(buf, "%u\n", drvdata.tp_version);
+ break;
+ default:
+ count = -EINVAL;
+ break;
+ }
+
+ return count;
+}
+
static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
@@ -1081,6 +1172,9 @@ struct gos_cfg_attr imu_bypass_enabled = { FEATURE_IMU_BYPASS };
LEGOS_DEVICE_ATTR_RW(imu_bypass_enabled, "bypass_enabled", index, gamepad);
static DEVICE_ATTR_RO_NAMED(imu_bypass_enabled_index, "bypass_enabled_index");
+struct gos_cfg_attr imu_manufacturer = { TEST_IMU_MFR };
+LEGOS_DEVICE_ATTR_RO(imu_manufacturer, "manufacturer", test);
+
struct gos_cfg_attr imu_sensor_enabled = { FEATURE_IMU_ENABLE };
LEGOS_DEVICE_ATTR_RW(imu_sensor_enabled, "sensor_enabled", index, gamepad);
static DEVICE_ATTR_RO_NAMED(imu_sensor_enabled_index, "sensor_enabled_index");
@@ -1088,6 +1182,7 @@ static DEVICE_ATTR_RO_NAMED(imu_sensor_enabled_index, "sensor_enabled_index");
static struct attribute *legos_imu_attrs[] = {
&dev_attr_imu_bypass_enabled.attr,
&dev_attr_imu_bypass_enabled_index.attr,
+ &dev_attr_imu_manufacturer.attr,
&dev_attr_imu_sensor_enabled.attr,
&dev_attr_imu_sensor_enabled_index.attr,
NULL,
@@ -1141,6 +1236,12 @@ struct gos_cfg_attr touchpad_linux_mode = { CFG_LINUX_MODE };
LEGOS_DEVICE_ATTR_RW(touchpad_linux_mode, "linux_mode", index, touchpad);
static DEVICE_ATTR_RO_NAMED(touchpad_linux_mode_index, "linux_mode_index");
+struct gos_cfg_attr touchpad_manufacturer = { TEST_TP_MFR };
+LEGOS_DEVICE_ATTR_RO(touchpad_manufacturer, "manufacturer", test);
+
+struct gos_cfg_attr touchpad_version = { TEST_TP_VER };
+LEGOS_DEVICE_ATTR_RO(touchpad_version, "version", test);
+
struct gos_cfg_attr touchpad_windows_mode = { CFG_WINDOWS_MODE };
LEGOS_DEVICE_ATTR_RW(touchpad_windows_mode, "windows_mode", index, touchpad);
static DEVICE_ATTR_RO_NAMED(touchpad_windows_mode_index, "windows_mode_index");
@@ -1150,6 +1251,8 @@ static struct attribute *legos_touchpad_attrs[] = {
&dev_attr_touchpad_enabled_index.attr,
&dev_attr_touchpad_linux_mode.attr,
&dev_attr_touchpad_linux_mode_index.attr,
+ &dev_attr_touchpad_manufacturer.attr,
+ &dev_attr_touchpad_version.attr,
&dev_attr_touchpad_windows_mode.attr,
&dev_attr_touchpad_windows_mode_index.attr,
NULL,
@@ -1250,6 +1353,27 @@ static void cfg_setup(struct work_struct *work)
dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU Version: %i\n", ret);
return;
}
+
+ ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, TEST_TP_MFR, 0, 0);
+ if (ret) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve Touchpad Manufacturer: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, TEST_TP_VER, 0, 0);
+ if (ret) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve Touchpad Firmware Version: %i\n", ret);
+ return;
+ }
+
+ ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, TEST_IMU_MFR, 0, 0);
+ if (ret) {
+ dev_err(&drvdata.hdev->dev,
+ "Failed to retrieve IMU Manufacturer: %i\n", ret);
+ return;
+ }
}
static int hid_gos_cfg_probe(struct hid_device *hdev,
--
2.52.0
^ permalink raw reply related
* [PATCH v5 16/16] HID: Add documentation for Lenovo Legion Go drivers
From: Derek J. Clark @ 2026-02-24 1:32 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Richard Hughes, Mario Limonciello, Zhixin Zhang, Mia Shao,
Mark Pearson, Pierre-Loup A . Griffais, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260224013217.1363996-1-derekjohn.clark@gmail.com>
Adds ABI documentation for the hid-lenovo-go-s and hid-lenovo-go
drivers.
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
V3:
- Remove excess + from every line of patch.
---
.../ABI/testing/sysfs-driver-hid-lenovo-go | 724 ++++++++++++++++++
.../ABI/testing/sysfs-driver-hid-lenovo-go-s | 304 ++++++++
MAINTAINERS | 2 +
3 files changed, 1030 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
new file mode 100644
index 0000000000000..c8221373ef76a
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
@@ -0,0 +1,724 @@
+What: /sys/class/leds/go:rgb:joystick_rings/effect
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the display effect of the RGB interface.
+
+ Values are monocolor, breathe, chroma, or rainbow.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/class/leds/go:rgb:joystick_rings/effect_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the effect attribute.
+
+ Values are monocolor, breathe, chroma, or rainbow.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/class/leds/go:rgb:joystick_rings/enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling or disabling the RGB interface.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/class/leds/go:rgb:joystick_rings/enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the enabled attribute.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/class/leds/go:rgb:joystick_rings/mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the operating mode of the RGB interface.
+
+ Values are dynamic or custom. Custom allows setting the RGB effect and color.
+ Dynamic is a Windows mode for syncing Lenovo RGB interfaces not currently
+ supported under Linux.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/class/leds/go:rgb:joystick_rings/mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the mode attribute.
+
+ Values are dynamic or custom.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/class/leds/go:rgb:joystick_rings/profile
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls selecting the configured RGB profile.
+
+ Values are 1-3.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/class/leds/go:rgb:joystick_rings/profile_range
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the profile attribute.
+
+ Values are 1-3.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/class/leds/go:rgb:joystick_rings/speed
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the change rate for the breathe, chroma, and rainbow effects.
+
+ Values are 0-100.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/class/leds/go:rgb:joystick_rings/speed_range
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the speed attribute.
+
+ Values are 0-100.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/firmware_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the firmware version of the internal MCU.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/fps_mode_dpi
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the DPI of the right handle when the FPS mode switch is on.
+
+ Values are 500, 800, 1200, and 1800.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/fps_mode_dpi_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the fps_mode_dpi attribute.
+
+ Values are 500, 800, 1200, and 1800.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/hardware_generation
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the hardware generation of the internal MCU.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/hardware_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the hardware version of the internal MCU.
+
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/auto_sleep_time
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the sleep timer due to inactivity for the left removable controller.
+
+ Values are 0-255.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/auto_sleep_time_range
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the left_handle/auto_sleep_time attribute.
+
+ Values are 0-255.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_gyro
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This initiates or halts calibration of the left removable controller's IMU.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_gyro_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the left_handle/calibrate_gyro attribute.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_gyro_status
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the result of the last attempted calibration of the left removable controller's IMU.
+
+ Values are unknown, success, failure.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_joystick
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This initiates or halts calibration of the left removable controller's joystick.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_joystick_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the left_handle/calibrate_jotstick attribute.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_joystick_status
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the result of the last attempted calibration of the left removable controller's joystick.
+
+ Values are unknown, success, failure.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_tirgger
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This initiates or halts calibration of the left removable controller's trigger.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_gyro_trigger
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the left_handle/calibrate_trigger attribute.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/calibrate_trigger_status
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the result of the last attempted calibration of the left removable controller's trigger.
+
+ Values are unknown, success, failure.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/firmware_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the left removable controller's firmware version.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/hardware_generation
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the hardware generation of the left removable controller.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/hardware_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the hardware version of the left removable controller.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/imu_bypass_enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling or disabling the IMU bypass function of the left removable controller.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/imu_bypass_enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the left_handle/imu_bypass_enabled attribute.
+
+ Values are true or false.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/imu_enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling or disabling the IMU of the left removable controller.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/imu_enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the left_handle/imu_enabled attribute.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/product_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the product version of the left removable controller.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/protocol_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the protocol version of the left removable controller.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/reset
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: Resets the left removable controller to factory defaults.
+
+ Writing 1 to this path initiates.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/rumble_mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls setting the response behavior for rumble events for the left removable controller.
+
+ Values are fps, racing, standarg, spg, rpg.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/rumble_mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the left_handle/rumble_mode attribute.
+
+ Values are fps, racing, standarg, spg, rpg.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/rumble_notification
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling haptic rumble events for the left removable controller.
+
+ Values are true, false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/rumble_notification_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the left_handle/rumble_notification attribute.
+
+ Values are true, false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the operating mode of the built-in controller.
+
+ Values are xinput or dinput.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/left_handle/mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the mode attribute.
+
+ Values are xinput or dinput.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/os_mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the behavior of built in chord combinations.
+
+ Values are windows or linux.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/os_mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the os_mode attribute.
+
+ Values are windows or linux.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/product_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the product version of the internal MCU.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/protocol_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the protocol version of the internal MCU.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/reset_mcu
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: Resets the internal MCU to factory defaults.
+
+ Writing 1 to this path initiates.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/auto_sleep_time
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the sleep timer due to inactivity for the right removable controller.
+
+ Values are 0-255.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/auto_sleep_time_range
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the right_handle/auto_sleep_time attribute.
+
+ Values are 0-255.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_gyro
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This initiates or halts calibration of the right removable controller's IMU.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_gyro_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the right_handle/calibrate_gyro attribute.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_gyro_status
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the result of the last attempted calibration of the right removable controller's IMU.
+
+ Values are unknown, success, failure.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_joystick
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This initiates or halts calibration of the right removable controller's joystick.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_joystick_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the right_handle/calibrate_jotstick attribute.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_joystick_status
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the result of the last attempted calibration of the right removable controller's joystick.
+
+ Values are unknown, success, failure.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_tirgger
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This initiates or halts calibration of the right removable controller's trigger.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_gyro_trigger
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the right_handle/calibrate_trigger attribute.
+
+ Values are start, stop.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/calibrate_trigger_status
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the result of the last attempted calibration of the right removable controller's trigger.
+
+ Values are unknown, success, failure.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/firmware_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the right removable controller's firmware version.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/hardware_generation
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the hardware generation of the right removable controller.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/hardware_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the hardware version of the right removable controller.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/imu_bypass_enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling or disabling the IMU bypass function of the right removable controller.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/imu_bypass_enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the right_handle/imu_bypass_enabled attribute.
+
+ Values are true or false.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/imu_enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling or disabling the IMU of the right removable controller.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/imu_enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the right_handle/imu_enabled attribute.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/product_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the product version of the right removable controller.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/protocol_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the protocol version of the right removable controller.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/reset
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: Resets the right removable controller to factory defaults.
+
+ Writing 1 to this path initiates.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/rumble_mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls setting the response behavior for rumble events for the right removable controller.
+
+ Values are fps, racing, standarg, spg, rpg.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/rumble_mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the right_handle/rumble_mode attribute.
+
+ Values are fps, racing, standarg, spg, rpg.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/rumble_notification
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling haptic rumble events for the right removable controller.
+
+ Values are true, false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/right_handle/rumble_notification_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the right_handle/rumble_notification attribute.
+
+ Values are true, false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/rumble_intensity
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls setting the rumble intensity for both removable controllers.
+
+ Values are off, low, medium, high.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/rumble_intensity_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the rumble_intensity attribute.
+
+ Values are off, low, medium, high.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling or disabling the touchpad.
+
+ Values are true, false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the touchpad/enabled attribute.
+
+ Values are true, false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/vibration_enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling haptic rumble events for the touchpad.
+
+ Values are true, false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/vibration_enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the touchpad/vibration_enabled attribute.
+
+ Values are true, false.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/vibration_intensity
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls setting the intensity of the touchpad haptics.
+
+ Values are off, low, medium, high.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/vibration_intensity_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the touchpad/vibration_intensity attribute.
+
+ Values are off, low, medium, high.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/tx_dongle/firmware_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the firmware version of the internal wireless transmission dongle.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/tx_dongle/hardware_generation
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the hardware generation of the internal wireless transmission dongle.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/tx_dongle/hardware_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the hardware version of the internal wireless transmission dongle.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/tx_dongle/product_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the product version of the internal wireless transmission dongle.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/tx_dongle/protocol_version
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the protocol version of the internal wireless transmission dongle.
+
+ Applies to Lenovo Legion Go and Go 2 line of handheld devices.
+
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
new file mode 100644
index 0000000000000..4d317074bb7e6
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
@@ -0,0 +1,304 @@
+What: /sys/class/leds/go_s:rgb:joystick_rings/effect
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the display effect of the RGB interface.
+
+ Values are monocolor, breathe, chroma, or rainbow.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/class/leds/go_s:rgb:joystick_rings/effect_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the effect attribute.
+
+ Values are monocolor, breathe, chroma, or rainbow.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/class/leds/go_s:rgb:joystick_rings/enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling or disabling the RGB interface.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/class/leds/go_s:rgb:joystick_rings/enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the enabled attribute.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/class/leds/go_s:rgb:joystick_rings/mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the operating mode of the RGB interface.
+
+ Values are dynamic or custom. Custom allows setting the RGB effect and color.
+ Dynamic is a Windows mode for syncing Lenovo RGB interfaces not currently
+ supported under Linux.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/class/leds/go_s:rgb:joystick_rings/mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the mode attribute.
+
+ Values are dynamic or custom.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/class/leds/go_s:rgb:joystick_rings/profile
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls selecting the configured RGB profile.
+
+ Values are 1-3.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/class/leds/go_s:rgb:joystick_rings/profile_range
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the profile attribute.
+
+ Values are 1-3.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/class/leds/go_s:rgb:joystick_rings/speed
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the change rate for the breathe, chroma, and rainbow effects.
+
+ Values are 0-100.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/class/leds/go_s:rgb:joystick_rings/speed_range
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the speed attribute.
+
+ Values are 0-100.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/auto_sleep_time
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the sleep timer due to inactivity for the built-in controller.
+
+ Values are 0-255.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/auto_sleep_time_range
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the gamepad/auto_sleep_time attribute.
+
+ Values are 0-255.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/dpad_mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the operating mode of the built-in controllers D-pad.
+
+ Values are 4-way or 8-way.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/dpad_mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the gamepad/dpad_mode attribute.
+
+ Values are 4-way or 8-way.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the operating mode of the built-in controller.
+
+ Values are xinput or dinput.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the gamepad/mode attribute.
+
+ Values are xinput or dinput.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/poll_rate
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls the poll rate in Hz of the built-in controller.
+
+ Values are 125, 250, 500, or 1000.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/gamepad/poll_rate_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the gamepad/poll_rate attribute.
+
+ Values are 125, 250, 500, or 1000.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/imu/bypass_enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling or disabling the IMU bypass function. When enabled the IMU data is directly reported to the OS through
+an HIDRAW interface.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/imu/bypass_enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the imu/bypass_enabled attribute.
+
+ Values are true or false.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/imu/manufacturer
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the manufacturer of the intertial measurment unit.
+
+ Values are Bosch or ST.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/imu/sensor_enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling or disabling the IMU.
+
+ Values are true, false, or wake-2s.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/imu/sensor_enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the imu/sensor_enabled attribute.
+
+ Values are true, false, or wake-2s.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/mcu_id
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the MCU Identification Number
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/mouse/step
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls which value is used for the mouse sensitivity.
+
+ Values are 1-127.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/mouse/step_range
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the mouse/step attribute.
+
+ Values are 1-127.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/os_mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls which value is used for the touchpads operating mode.
+
+ Values are windows or linux.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/os_mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the os_mode attribute.
+
+ Values are windows or linux.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/enabled
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls enabling or disabling the built-in touchpad.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/enabled_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the touchpad/enabled attribute.
+
+ Values are true or false.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/linux_mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls behavior of the touchpad events when os_mode is set to linux.
+
+ Values are absolute or relative.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/linux_mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the touchpad/linux_mode attribute.
+
+ Values are absolute or relative.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/windows_mode
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This controls behavior of the touchpad events when os_mode is set to windows.
+
+ Values are absolute or relative.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
+
+What: /sys/bus/usb/devices/<busnum>-<devnum>:<config num>.<interface num>/<hid-bus>:<vendor-id>:<product-id>.<num>/touchpad/windows_mode_index
+Date: April 2026
+Contact: linux-input@vger.kernel.org
+Description: This displays the available options for the touchpad/windows_mode attribute.
+
+ Values are absolute or relative.
+
+ Applies to Lenovo Legion Go S line of handheld devices.
diff --git a/MAINTAINERS b/MAINTAINERS
index 1d0468906788a..8eea5f231e809 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14318,6 +14318,8 @@ M: Derek J. Clark <derekjohn.clark@gmail.com>
M: Mark Pearson <mpearson-lenovo@squebb.ca>
L: linux-input@vger.kernel.org
S: Maintained
+F: Documentation/ABI/testing/sysfs-driver-hid-lenovo-go
+F: Documentation/ABI/testing/sysfs-driver-hid-lenovo-go-s
F: drivers/hid/hid-lenovo-go-s.c
F: drivers/hid/hid-lenovo-go.c
F: drivers/hid/hid-lenovo.c
--
2.52.0
^ permalink raw reply related
* Re: [PATCH 01/37] PCI/MSI: Add Devres managed IRQ vectors allocation
From: Shawn Lin @ 2026-02-24 2:08 UTC (permalink / raw)
To: Jakub Kicinski
Cc: shawn.lin, Bjorn Helgaas, Vaibhaav Ram T . L,
Kumaravel Thiagarajan, Even Xu, Xinpeng Sun, Srinivas Pandruvada,
Jiri Kosina, Alexandre Belloni, Zhou Wang, Longfang Liu,
Vinod Koul, Lee Jones, Jijie Shao, Jian Shen, Sunil Goutham,
Andrew Lunn, Heiner Kallweit, David S . Miller, Jeff Hugo,
Oded Gabbay, Maciej Falkowski, Karol Wachowski, Min Ma, Lizhi Hou,
Andreas Noever, Mika Westerberg, Tomasz Jeznach, Will Deacon,
Xinliang Liu, Tian Tao, Davidlohr Bueso, Jonathan Cameron,
Srujana Challa, Bharat Bhushan, Antoine Tenart, Herbert Xu,
Raag Jadav, Hans de Goede, Greg Kroah-Hartman, Jiri Slaby,
Andy Shevchenko, Manivannan Sadhasivam, Mika Westerberg,
Andi Shyti, Robert Richter, Mark Brown, Nirmal Patel,
Kurt Schwemmer, Logan Gunthorpe, Linus Walleij,
Bartosz Golaszewski, Sakari Ailus, Bingbu Cao, Ulf Hansson,
Arnd Bergmann, Benjamin Tissoires, linux-input, linux-i3c,
dmaengine, Philipp Stanner, netdev, nic_swsd, linux-arm-msm,
dri-devel, linux-usb, iommu, linux-riscv, David Airlie,
Simona Vetter, linux-cxl, linux-crypto, platform-driver-x86,
linux-serial, mhi, Andy Shevchenko, Jan Dabros, linux-i2c,
Daniel Mack, Haojian Zhuang, linux-spi, Jonathan Derrick,
linux-pci, linux-gpio, Mauro Carvalho Chehab, linux-media,
linux-mmc
In-Reply-To: <20260223160402.3ad8f079@kernel.org>
在 2026/02/24 星期二 8:04, Jakub Kicinski 写道:
> On Mon, 23 Feb 2026 23:29:40 +0800 Shawn Lin wrote:
>> pcim_alloc_irq_vectors() and pcim_alloc_irq_vectors_affinity() are created for
>> pci device drivers which rely on the devres machinery to help cleanup the IRQ
>> vectors.
>
> If you can please add this API with just a few users, and then convert
> remaining users via the subsystem trees in the next cycle.
> There's no need to risk wasting maintainer time on conflicts with
> conversions like this.
Thanks for the suggestion, Jakub. I have little experience with
cross-subsystem cleanups like this, so your suggestion is very helpful.
>
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox