From nobody Sun May 24 20:33:27 2026 Received: from mail-dy1-f170.google.com (mail-dy1-f170.google.com [74.125.82.170]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A21B52BFC8F for ; Fri, 22 May 2026 01:55:23 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.170 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779414927; cv=none; b=Ek2ZLJuY7q/wwl3uo2M5EnrdNi/H7YFgwUgzlbhGWE9CKbLl4A3nbmz53iLmrfsPEOJa4WvqgYQNyOXQXkXLd88x4HcPcwTIjM1ov6bxAgJRNO1yM2DJb0IOC9sBiWvaa3a7tvW3qiheo2wRdLqR4L0t4ytz1d8sHC+wEHAhY8s= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779414927; c=relaxed/simple; bh=12EsC1EhL124KBMaSHul3bpipNfYrgDylJrDrXaHPAU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=OthM8fBwHvQYjhmu4hnPLYD8XWuGnHBIdWBOHMzpg2aTxhyp/twhBiQs+4JF1RTU5ipVGd8qVV9l3IYHWB7EJ0Txcj3CbSP1+mK8D7FoYSZ4Wzbm12kiGly7PlwS+Xx/aoLEs0lSPiOKqZ1Fgo498ubdCjKYy9c15kQsygoqIdA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=YdVvMcm4; arc=none smtp.client-ip=74.125.82.170 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="YdVvMcm4" Received: by mail-dy1-f170.google.com with SMTP id 5a478bee46e88-303f2fb7225so5348056eec.1 for ; Thu, 21 May 2026 18:55:23 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1779414923; x=1780019723; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=yQyjv+M9lb593ELx/aaI1TvpNJEEYR2cWVDSqI0+5AU=; b=YdVvMcm4Utj8cgK2KUcHRyB+hdHdGwquQDJn8is3xjiAz1kHSvAcIEMi8w3C3MXSed BUQngRVf0VMcPX3jtGzKiWbmtuwJlkf2j7tKuzzUTDnRFj3B6fvWzsitlBMn333eHq6g oIFFJ7rFBPumuMVqRfnzaOEOQ9o1ZQ0d8VFT/5qzyajC8LtE/kAszMYwYfK/DoptCKZC afnS6vmiayGR/vEZirhFxFAtt9zUfCGmk7guyH+At+2Kz34j9SU7jsG3OoSR3pnJqAn1 mTG9N9rsTjCLspliUb90jACGnIj7aDVZ7XPZ5A6rrimZZgyetZmmYakDlh10JCgO6Mfe HJEg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1779414923; x=1780019723; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=yQyjv+M9lb593ELx/aaI1TvpNJEEYR2cWVDSqI0+5AU=; b=f/emaKvhjihPRgDPl25rG+YwoRbqO8vbgo17cg59xWbTL9VSRJpJiL0LOo7Ivp997y Genp9q07K3YkfS4YwtaKpuBC3ckD3glp7b6WbJoX573ivOWzby5QAHkOllTxSgiawWco Ra8miR7Ehd24t7NGi9Sg5aZ9GwmYKvop0EErv5S8sRH2ibvgf6W7NSGJrh+pKp6E5wSM VXtn5eJEaEN2P7l9xHwmKQxvBMFlLwEIItZWunE3UH6YX21rWmMOV/KlbezYJxGwuiZW +WotA6rNy+b7nFXKTMwsQovhBd562n5eQuQGOzJy0hnpwuYJMI7fAoE5eUGWA5WVwKJ0 L0PQ== X-Forwarded-Encrypted: i=1; AFNElJ8ttn1Z+DsT7+pamJhP2I8Y1j4cdC1iiUHDiXGc0z4HiiDHQ2oYHpfaN8/78SF/l98Aaa7GqgFm5h98AnM=@vger.kernel.org X-Gm-Message-State: AOJu0YyWMzMxaOl2WsSPFZHhGhC3G0wMUWVzzN6Ho9NLVPhyNWAZTKFr 6JbnSHXbTk83Dh6Y6TRquy4PWDhgleyDhED+wcd3hqrB0f1orzfBCa7N X-Gm-Gg: Acq92OFTNIstDO7YIj8cXsf2MOrN0lmcHQfGXi3apGs8TDgVyRg2B72ZZskxvSJ/YAl 1MrPciPucu5yXwuVUgDWT4pP13p2+EpsjnP5xFtpRbZNmzaac4EdGC87BCmhpdq5uXmWsKSKHRH XrMe0MT1+UMjcecl4pZKZAB9gKOHLWaACRPilN/oDxMESjge1jqANQ6AYDJaEQGHhYUZrRfoGD7 hVPNKf24zU8kGxwiimOvEMQTKtE2kNO1HzAYgJGksaWlCrGpwYs8HkNVRvTNnyEW9hlPOydU7hV nUHR1mRgXP5H3az5b4OaBf07zKdIHpgTv+6AXptxH1ysGM4Cm4R397PrOwtseFR4kkkKDMEPzYL zO9Z3buKNSQpZOxrVoP/vKTEhXLZhsTZdLrlvlPRRm3ZMmdK5AUKg/pWIMhD/YUbW0pGc2OWyci N1/EFD4Svg4rQQmIOP16a3UJLdYYnrFW07WIUrESFF/U+HSX9K6GtuvIAnsVZ7R637yHX4RAgxb 1QXV0Yvc92jfqk= X-Received: by 2002:a05:7300:8607:b0:2c5:220c:5670 with SMTP id 5a478bee46e88-30448fd63d5mr943613eec.2.1779414922400; Thu, 21 May 2026 18:55:22 -0700 (PDT) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-3044b9123f7sm1027744eec.19.2026.05.21.18.55.21 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 21 May 2026 18:55:22 -0700 (PDT) From: "Derek J. Clark" To: Jiri Kosina , Benjamin Tissoires Cc: "Pierre-Loup A . Griffais" , Denis Benato , Zhouwang Huang , "Derek J . Clark" , linux-input@vger.kernel.org, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v8 1/4] HID: hid-msi: Add MSI Claw configuration driver Date: Fri, 22 May 2026 01:55:15 +0000 Message-ID: <20260522015518.1111290-2-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260522015518.1111290-1-derekjohn.clark@gmail.com> References: <20260522015518.1111290-1-derekjohn.clark@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Adds configuration HID driver for the MSI Claw series of handheld PC's. In this initial patch add the initial driver outline and attributes for changing the gamepad mode, M-key behavior, and add a WO reset function. Sending the SWITCH_MODE and RESET commands causes a USB disconnect in the device. The completion will therefore never get hit and would trigger an -EIO. To avoid showing the user an error for every write to these attrs a bypass for the completion handling is introduced when timeout =3D= =3D 0. The initial version of this patch was written by Denis Benato, which contained the initial reverse-engineering and implementation for the gamepad mode switching. This work was later expanded by Zhouwang Huang to include more gamepad modes. Finally, I refactored the drivers data in/out flow and overall format to conform to kernel driver best practices and style guides. Claude was used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Denis Benato Signed-off-by: Denis Benato Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- v8: - Use spinlock when accessing gamepad_registered. - Clear state machine on all errors in claw_hw_output_report. - Wrap all branches under single cmd_lock guard in claw_raw_event. - Reject generic ACK in claw_raw_event if waiting_cmd is for another branch. - Don't close hid devices that couldn't have been opened. v7: - Use smp_[store_release|load_acquire] pattern for checking gamepad_registered to avoid possible races during teardown. - Reorder reinit_completion in claw_hw_output_report to avoid race with possible incoming ACKs. - Reorder cancel_delayed_work_sync to ensure setup can't be re-armed after cancel. - Reset command state machine if hw_output_report has an error. - Add comments to (hopefully) silence sashinko-bot warnings about the use of endpoint matching and the impossible scenario of switching to the alternate endpoint from userspace while the driver is bound. - Don't use spinlock_irqsave when already in irq context. v6: - Add send/ack pattern to ensure synchronous acks. - Use spinlock_irqsave instead of mutex for read/write MODE event data. - add select NEW_LEDS to kconfig. - Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz config. - Gate all attribute show/store functions with gamepad_registered, enabling use of devm_device_add_group. - Re-arm cfg_setup in resume if it was canceled in an early suspend. - Don't set gamepad_mode on resume, MCU preserves state. - Ensure all count variables are checked for > 0 characters before setting buf - 1 to \n. v5: - Swap disabled & combination mkeys_function enum values. - Ensure mode_mutex is properly init. - Ensure claw_remove is calling hid_hw_close and not hid_hw_stop for all paths. v4: - Add msi_suspend/claw_suspend. - Reorder claw_remove to cancel all work before removing sysfs. - Add mutex lock for removing sysfs attributes. - Add mutex lock for MODE command data read/write. v3: - Ensure claw_hw_output_report is properly guarded. - Reoder claw_probe to ensure all mutex, completion, and variable assignments are in place prior to setting drvdata. - Ensure gamepad_mode is set to a valid enum value in claw_probe. v2: - Rename driver to hid-msi from hid-msi-claw. - Rename reusable/generic functions to msi_* from claw_*, retaining claw specific functions. - Add generic entrypoints for probe, remove, and raw event that route to claw specific functions. --- MAINTAINERS | 6 + drivers/hid/Kconfig | 13 + drivers/hid/Makefile | 1 + drivers/hid/hid-ids.h | 5 + drivers/hid/hid-msi.c | 703 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 728 insertions(+) create mode 100644 drivers/hid/hid-msi.c diff --git a/MAINTAINERS b/MAINTAINERS index 6f6517bf4f970..8e2de98b768f7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -17965,6 +17965,12 @@ S: Odd Fixes F: Documentation/devicetree/bindings/net/ieee802154/mrf24j40.txt F: drivers/net/ieee802154/mrf24j40.c =20 +MSI HID DRIVER +M: Derek J. Clark +L: linux-input@vger.kernel.org +S: Maintained +F: drivers/hid/hid-msi.c + MSI EC DRIVER M: Nikita Kravets L: platform-driver-x86@vger.kernel.org diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index 10c12d8e65579..7766676051a52 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -492,6 +492,19 @@ config HID_GT683R Currently the following devices are know to be supported: - MSI GT683R =20 +config HID_MSI + tristate "MSI Claw Gamepad Support" + depends on USB_HID + select NEW_LEDS + select LEDS_CLASS + select LEDS_CLASS_MULTICOLOR + help + Support for the MSI Claw RGB and controller configuration + + Say Y here to include configuration interface support for the MSI Claw Li= ne + of Handheld Console Controllers. Say M here to compile this driver as a + module. The module will be called hid-msi. + config HID_KEYTOUCH tristate "Keytouch HID devices" help diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile index 07dfdb6a49c59..80925a17b059c 100644 --- a/drivers/hid/Makefile +++ b/drivers/hid/Makefile @@ -92,6 +92,7 @@ obj-$(CONFIG_HID_MAYFLASH) +=3D hid-mf.o obj-$(CONFIG_HID_MEGAWORLD_FF) +=3D hid-megaworld.o obj-$(CONFIG_HID_MICROSOFT) +=3D hid-microsoft.o obj-$(CONFIG_HID_MONTEREY) +=3D hid-monterey.o +obj-$(CONFIG_HID_MSI) +=3D hid-msi.o obj-$(CONFIG_HID_MULTITOUCH) +=3D hid-multitouch.o obj-$(CONFIG_HID_NINTENDO) +=3D hid-nintendo.o obj-$(CONFIG_HID_NTI) +=3D hid-nti.o diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index 933b7943bdb50..94a9b89dc240a 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1047,7 +1047,12 @@ #define USB_DEVICE_ID_MOZA_R16_R21_2 0x0010 =20 #define USB_VENDOR_ID_MSI 0x1770 +#define USB_VENDOR_ID_MSI_2 0x0db0 #define USB_DEVICE_ID_MSI_GT683R_LED_PANEL 0xff00 +#define USB_DEVICE_ID_MSI_CLAW_XINPUT 0x1901 +#define USB_DEVICE_ID_MSI_CLAW_DINPUT 0x1902 +#define USB_DEVICE_ID_MSI_CLAW_DESKTOP 0x1903 +#define USB_DEVICE_ID_MSI_CLAW_BIOS 0x1904 =20 #define USB_VENDOR_ID_NATIONAL_SEMICONDUCTOR 0x0400 #define USB_DEVICE_ID_N_S_HARMONY 0xc359 diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c new file mode 100644 index 0000000000000..40b16253abbb3 --- /dev/null +++ b/drivers/hid/hid-msi.c @@ -0,0 +1,703 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * HID driver for MSI Claw Handheld PC gamepads. + * + * Provides configuration support for the MSI Claw series of handheld PC + * gamepads. Multiple iterations of the device firmware has led to some + * quirks for how certain attributes are handled. The original firmware + * did not support remapping of the M1 (right) and M2 (left) rear paddles. + * Additionally, the MCU RAM address for writing configuration data has + * changed twice. Checks are done during probe to enumerate these varianc= es. + * + * Copyright (c) 2026 Zhouwang Huang + * Copyright (c) 2026 Denis Benato + * Copyright (c) 2026 Valve Corporation + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hid-ids.h" + +#define CLAW_OUTPUT_REPORT_ID 0x0f +#define CLAW_INPUT_REPORT_ID 0x10 + +#define CLAW_PACKET_SIZE 64 + +#define CLAW_DINPUT_CFG_INTF_IN 0x82 +#define CLAW_XINPUT_CFG_INTF_IN 0x83 + +enum claw_command_index { + CLAW_COMMAND_TYPE_NONE =3D 0x00, + CLAW_COMMAND_TYPE_READ_PROFILE =3D 0x04, + CLAW_COMMAND_TYPE_READ_PROFILE_ACK =3D 0x05, + CLAW_COMMAND_TYPE_ACK =3D 0x06, + CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA =3D 0x21, + CLAW_COMMAND_TYPE_SYNC_TO_ROM =3D 0x22, + CLAW_COMMAND_TYPE_SWITCH_MODE =3D 0x24, + CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE =3D 0x26, + CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK =3D 0x27, + CLAW_COMMAND_TYPE_RESET_DEVICE =3D 0x28, +}; + +enum claw_gamepad_mode_index { + CLAW_GAMEPAD_MODE_XINPUT =3D 0x01, + CLAW_GAMEPAD_MODE_DINPUT =3D 0x02, + CLAW_GAMEPAD_MODE_DESKTOP =3D 0x04, +}; + +static const char * const claw_gamepad_mode_text[] =3D { + [CLAW_GAMEPAD_MODE_XINPUT] =3D "xinput", + [CLAW_GAMEPAD_MODE_DINPUT] =3D "dinput", + [CLAW_GAMEPAD_MODE_DESKTOP] =3D "desktop", +}; + +enum claw_mkeys_function_index { + CLAW_MKEY_FUNCTION_MACRO, + CLAW_MKEY_FUNCTION_DISABLED, + CLAW_MKEY_FUNCTION_COMBO, +}; + +static const char * const claw_mkeys_function_text[] =3D { + [CLAW_MKEY_FUNCTION_MACRO] =3D "macro", + [CLAW_MKEY_FUNCTION_DISABLED] =3D "disabled", + [CLAW_MKEY_FUNCTION_COMBO] =3D "combination", +}; + +struct claw_command_report { + u8 report_id; + u8 padding[2]; + u8 header_tail; + u8 cmd; + u8 data[59]; +} __packed; + +struct claw_drvdata { + /* MCU General Variables */ + struct completion send_cmd_complete; + struct delayed_work cfg_resume; + struct delayed_work cfg_setup; + spinlock_t registration_lock; /* Lock for registration read/write */ + struct hid_device *hdev; + struct mutex cfg_mutex; /* mutex for synchronous data */ + bool waiting_for_ack; + spinlock_t cmd_lock; /* Lock for cmd data read/write */ + u8 waiting_cmd; + int cmd_status; + u8 ep; + + /* Gamepad Variables */ + enum claw_mkeys_function_index mkeys_function; + enum claw_gamepad_mode_index gamepad_mode; + bool gamepad_registered; + spinlock_t mode_lock; /* Lock for mode data read/write */ +}; + +static int get_endpoint_address(struct hid_device *hdev) +{ + struct usb_host_endpoint *ep; + struct usb_interface *intf; + + intf =3D to_usb_interface(hdev->dev.parent); + ep =3D intf->cur_altsetting->endpoint; + if (ep) + return ep->desc.bEndpointAddress; + + return -ENODEV; +} + +static int claw_gamepad_mode_event(struct claw_drvdata *drvdata, + struct claw_command_report *cmd_rep) +{ + if (cmd_rep->data[0] >=3D ARRAY_SIZE(claw_gamepad_mode_text) || + !claw_gamepad_mode_text[cmd_rep->data[0]] || + cmd_rep->data[1] >=3D ARRAY_SIZE(claw_mkeys_function_text)) + return -EINVAL; + + scoped_guard(spinlock, &drvdata->mode_lock) { + drvdata->gamepad_mode =3D cmd_rep->data[0]; + drvdata->mkeys_function =3D cmd_rep->data[1]; + } + + return 0; +} + +static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report = *report, + u8 *data, int size) +{ + struct claw_command_report *cmd_rep; + int ret =3D 0; + + if (size !=3D CLAW_PACKET_SIZE) + return 0; + + cmd_rep =3D (struct claw_command_report *)data; + + if (cmd_rep->report_id !=3D CLAW_INPUT_REPORT_ID || cmd_rep->header_tail = !=3D 0x3c) + return 0; + + dev_dbg(&drvdata->hdev->dev, "Rx data as raw input report: [%*ph]\n", + CLAW_PACKET_SIZE, data); + + guard(spinlock)(&drvdata->cmd_lock); + switch (cmd_rep->cmd) { + case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK: + ret =3D claw_gamepad_mode_event(drvdata, cmd_rep); + if (drvdata->waiting_for_ack && + drvdata->waiting_cmd =3D=3D CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE) { + drvdata->cmd_status =3D ret; + drvdata->waiting_for_ack =3D false; + complete(&drvdata->send_cmd_complete); + } + + break; + case CLAW_COMMAND_TYPE_ACK: + if (drvdata->waiting_cmd =3D=3D CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE) + break; + if (drvdata->waiting_for_ack) { + drvdata->cmd_status =3D 0; + drvdata->waiting_for_ack =3D false; + complete(&drvdata->send_cmd_complete); + } + dev_dbg(&drvdata->hdev->dev, "Waiting CMD: %x\n", drvdata->waiting_cmd); + + break; + default: + dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd); + return 0; + } + + return ret; +} + +static int msi_raw_event(struct hid_device *hdev, struct hid_report *repor= t, + u8 *data, int size) +{ + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + + if (!drvdata || (drvdata->ep !=3D CLAW_XINPUT_CFG_INTF_IN && + drvdata->ep !=3D CLAW_DINPUT_CFG_INTF_IN)) + return 0; + + return claw_raw_event(drvdata, report, data, size); +} + +static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *da= ta, + size_t len, unsigned int timeout) +{ + unsigned char *dmabuf __free(kfree) =3D NULL; + u8 header[] =3D { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index }; + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + size_t header_size =3D ARRAY_SIZE(header); + int ret; + + if (header_size + len > CLAW_PACKET_SIZE) + return -EINVAL; + + /* We can't use a devm_alloc reusable buffer without side effects during = suspend */ + dmabuf =3D kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL); + if (!dmabuf) + return -ENOMEM; + + memcpy(dmabuf, header, header_size); + if (data && len) + memcpy(dmabuf + header_size, data, len); + + guard(mutex)(&drvdata->cfg_mutex); + if (timeout) { + reinit_completion(&drvdata->send_cmd_complete); + scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) { + drvdata->waiting_cmd =3D index; + drvdata->waiting_for_ack =3D true; + drvdata->cmd_status =3D -ETIMEDOUT; + } + } + + dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n", + CLAW_PACKET_SIZE, dmabuf); + + ret =3D hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE); + if (ret < 0) + goto err_clear_state; + + ret =3D ret =3D=3D CLAW_PACKET_SIZE ? 0 : -EIO; + if (ret) + goto err_clear_state; + + if (timeout) { + ret =3D wait_for_completion_interruptible_timeout(&drvdata->send_cmd_com= plete, + msecs_to_jiffies(timeout)); + + dev_dbg(&hdev->dev, "Remaining timeout: %u\n", ret); + ret =3D ret > 0 ? drvdata->cmd_status : ret ?: -EBUSY; + } + +err_clear_state: + if (timeout) { + guard(spinlock_irqsave)(&drvdata->cmd_lock); + drvdata->waiting_cmd =3D CLAW_COMMAND_TYPE_NONE; + drvdata->waiting_for_ack =3D false; + } + + return ret; +} + +static ssize_t gamepad_mode_store(struct device *dev, struct device_attrib= ute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + int i, ret =3D -EINVAL; + u8 data[2]; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + for (i =3D 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) { + if (claw_gamepad_mode_text[i] && sysfs_streq(buf, claw_gamepad_mode_text= [i])) { + ret =3D i; + break; + } + } + if (ret < 0) + return ret; + + data[0] =3D ret; + scoped_guard(spinlock_irqsave, &drvdata->mode_lock) + data[1] =3D drvdata->mkeys_function; + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, = ARRAY_SIZE(data), 0); + if (ret) + return ret; + + return count; +} + +static ssize_t gamepad_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + int ret, i; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, = NULL, 0, 25); + if (ret) + return ret; + + scoped_guard(spinlock_irqsave, &drvdata->mode_lock) + i =3D drvdata->gamepad_mode; + + if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] =3D=3D '\0= ') + return sysfs_emit(buf, "unsupported\n"); + + return sysfs_emit(buf, "%s\n", claw_gamepad_mode_text[i]); +} +static DEVICE_ATTR_RW(gamepad_mode); + +static ssize_t gamepad_mode_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + ssize_t count =3D 0; + int i; + + for (i =3D 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) { + if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] =3D=3D '\= 0') + continue; + count +=3D sysfs_emit_at(buf, count, "%s ", claw_gamepad_mode_text[i]); + } + + if (count) + buf[count - 1] =3D '\n'; + + return count; +} +static DEVICE_ATTR_RO(gamepad_mode_index); + +static ssize_t mkeys_function_store(struct device *dev, struct device_attr= ibute *attr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + int i, ret =3D -EINVAL; + u8 data[2]; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + for (i =3D 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) { + if (claw_mkeys_function_text[i] && sysfs_streq(buf, claw_mkeys_function_= text[i])) { + ret =3D i; + break; + } + } + if (ret < 0) + return ret; + + scoped_guard(spinlock_irqsave, &drvdata->mode_lock) + data[0] =3D drvdata->gamepad_mode; + data[1] =3D ret; + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data, = ARRAY_SIZE(data), 0); + if (ret) + return ret; + + return count; +} + +static ssize_t mkeys_function_show(struct device *dev, struct device_attri= bute *attr, + char *buf) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + int ret, i; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, = NULL, 0, 25); + if (ret) + return ret; + + scoped_guard(spinlock_irqsave, &drvdata->mode_lock) + i =3D drvdata->mkeys_function; + + if (i >=3D ARRAY_SIZE(claw_mkeys_function_text)) + return sysfs_emit(buf, "unsupported\n"); + + return sysfs_emit(buf, "%s\n", claw_mkeys_function_text[i]); +} +static DEVICE_ATTR_RW(mkeys_function); + +static ssize_t mkeys_function_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int i, count =3D 0; + + for (i =3D 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) + count +=3D sysfs_emit_at(buf, count, "%s ", claw_mkeys_function_text[i]); + + if (count) + buf[count - 1] =3D '\n'; + + return count; +} +static DEVICE_ATTR_RO(mkeys_function_index); + +static ssize_t reset_store(struct device *dev, struct device_attribute *at= tr, + const char *buf, size_t count) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + bool val; + int ret; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + ret =3D kstrtobool(buf, &val); + if (ret) + return ret; + + if (!val) + return -EINVAL; + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_RESET_DEVICE, NULL,= 0, 0); + if (ret) + return ret; + + return count; +} +static DEVICE_ATTR_WO(reset); + +static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct a= ttribute *attr, + int n) +{ + struct hid_device *hdev =3D to_hid_device(kobj_to_dev(kobj)); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + + if (!drvdata) { + dev_warn(&hdev->dev, + "Failed to get drvdata from kobj. Gamepad attributes are not available= .\n"); + return 0; + } + + return attr->mode; +} + +static struct attribute *claw_gamepad_attrs[] =3D { + &dev_attr_gamepad_mode.attr, + &dev_attr_gamepad_mode_index.attr, + &dev_attr_mkeys_function.attr, + &dev_attr_mkeys_function_index.attr, + &dev_attr_reset.attr, + NULL, +}; + +static const struct attribute_group claw_gamepad_attr_group =3D { + .attrs =3D claw_gamepad_attrs, + .is_visible =3D claw_gamepad_attr_is_visible, +}; + +static void cfg_setup_fn(struct work_struct *work) +{ + struct delayed_work *dwork =3D container_of(work, struct delayed_work, wo= rk); + struct claw_drvdata *drvdata =3D container_of(dwork, struct claw_drvdata,= cfg_setup); + int ret; + + ret =3D claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEP= AD_MODE, + NULL, 0, 25); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to setup device, can't read gamepad mode: %d\n", ret); + return; + } + + /* Add sysfs attributes after we get the device state */ + ret =3D devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_gro= up); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to setup device, can't create gamepad attrs: %d\n", ret); + return; + } + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) + /* Pairs with smp_load_acquire in attribute show/store functions */ + smp_store_release(&drvdata->gamepad_registered, true); + + kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE); +} + +static void cfg_resume_fn(struct work_struct *work) +{ + struct delayed_work *dwork =3D container_of(work, struct delayed_work, wo= rk); + struct claw_drvdata *drvdata =3D container_of(dwork, struct claw_drvdata,= cfg_resume); + + guard(spinlock_irqsave)(&drvdata->registration_lock); + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500)); +} + +static int claw_probe(struct hid_device *hdev, u8 ep) +{ + struct claw_drvdata *drvdata; + int ret; + + drvdata =3D devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL); + if (!drvdata) + return -ENOMEM; + + drvdata->gamepad_mode =3D CLAW_GAMEPAD_MODE_XINPUT; + drvdata->hdev =3D hdev; + drvdata->ep =3D ep; + + mutex_init(&drvdata->cfg_mutex); + spin_lock_init(&drvdata->registration_lock); + spin_lock_init(&drvdata->cmd_lock); + spin_lock_init(&drvdata->mode_lock); + init_completion(&drvdata->send_cmd_complete); + INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn); + INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn); + + /* For control interface: open the HID transport for sending commands. */ + ret =3D hid_hw_open(hdev); + if (ret) + return ret; + + hid_set_drvdata(hdev, drvdata); + schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500)); + + return 0; +} + +static int msi_probe(struct hid_device *hdev, const struct hid_device_id *= id) +{ + int ret; + u8 ep; + + if (!hid_is_usb(hdev)) { + ret =3D -ENODEV; + goto err_probe; + } + + ret =3D hid_parse(hdev); + if (ret) + goto err_probe; + + /* Set quirk to create separate input devices per HID application */ + hdev->quirks |=3D HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT; + ret =3D hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) + goto err_probe; + + /* For non-control interfaces (keyboard/mouse), allow userspace to grab t= he devices. */ + ret =3D get_endpoint_address(hdev); + if (ret < 0) + goto err_stop_hw; + + ep =3D ret; + if (ep =3D=3D CLAW_XINPUT_CFG_INTF_IN || ep =3D=3D CLAW_DINPUT_CFG_INTF_I= N) { + ret =3D claw_probe(hdev, ep); + if (ret) + goto err_stop_hw; + } + + return 0; + +err_stop_hw: + hid_hw_stop(hdev); +err_probe: + return dev_err_probe(&hdev->dev, ret, "Failed to init device\n"); +} + +static void claw_remove(struct hid_device *hdev) +{ + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + + if (!drvdata) + return; + + cancel_delayed_work_sync(&drvdata->cfg_resume); + cancel_delayed_work_sync(&drvdata->cfg_setup); + + guard(spinlock_irqsave)(&drvdata->registration_lock); + /* Pairs with smp_load_acquire in attribute show/store functions */ + smp_store_release(&drvdata->gamepad_registered, false); + + hid_hw_close(hdev); +} + +static void msi_remove(struct hid_device *hdev) +{ + int ret; + u8 ep; + + /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bou= nd */ + ret =3D get_endpoint_address(hdev); + if (ret <=3D 0) + goto hw_stop; + + ep =3D ret; + if (ep =3D=3D CLAW_XINPUT_CFG_INTF_IN || ep =3D=3D CLAW_DINPUT_CFG_INTF_I= N) + claw_remove(hdev); + +hw_stop: + hid_hw_stop(hdev); +} + +static int claw_resume(struct hid_device *hdev) +{ + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + + if (!drvdata) + return -ENODEV; + + /* MCU can take up to 500ms to be ready after resume */ + schedule_delayed_work(&drvdata->cfg_resume, msecs_to_jiffies(500)); + return 0; +} + +static int msi_resume(struct hid_device *hdev) +{ + int ret; + u8 ep; + + /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bou= nd */ + ret =3D get_endpoint_address(hdev); + if (ret <=3D 0) + return 0; + + ep =3D ret; + if (ep =3D=3D CLAW_XINPUT_CFG_INTF_IN || ep =3D=3D CLAW_DINPUT_CFG_INTF_I= N) + return claw_resume(hdev); + + return 0; +} + +static int claw_suspend(struct hid_device *hdev) +{ + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + + if (!drvdata) + return -ENODEV; + + cancel_delayed_work_sync(&drvdata->cfg_resume); + cancel_delayed_work_sync(&drvdata->cfg_setup); + + return 0; +} + +static int msi_suspend(struct hid_device *hdev, pm_message_t msg) +{ + int ret; + u8 ep; + + /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bou= nd */ + ret =3D get_endpoint_address(hdev); + if (ret <=3D 0) + return 0; + + ep =3D ret; + if (ep =3D=3D CLAW_XINPUT_CFG_INTF_IN || ep =3D=3D CLAW_DINPUT_CFG_INTF_I= N) + return claw_suspend(hdev); + + return 0; +} + +static const struct hid_device_id msi_devices[] =3D { + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_XINPUT) }, + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DINPUT) }, + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DESKTOP) }, + { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_BIOS) }, + { } +}; +MODULE_DEVICE_TABLE(hid, msi_devices); + +static struct hid_driver msi_driver =3D { + .name =3D "hid-msi", + .id_table =3D msi_devices, + .raw_event =3D msi_raw_event, + .probe =3D msi_probe, + .remove =3D msi_remove, + .resume =3D msi_resume, + .suspend =3D pm_ptr(msi_suspend), +}; +module_hid_driver(msi_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Denis Benato "); +MODULE_AUTHOR("Zhouwang Huang "); +MODULE_AUTHOR("Derek J. Clark "); +MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads"); --=20 2.53.0 From nobody Sun May 24 20:33:27 2026 Received: from mail-dy1-f171.google.com (mail-dy1-f171.google.com [74.125.82.171]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 5B91B2D0C94 for ; Fri, 22 May 2026 01:55:24 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.171 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779414927; cv=none; b=Ykvvn6dRE/jhwf1MEOAl7cMg9UfDuB+o206p6vfekbtraSjvtARcXOwPQLm9dxD/lr3sGfWE2BfI8BzJqnwT9f/Z3IWgl4/RQrTC5t3p0+QU7H7zafcqEVMjNulaSgbN53ce7dnSHx0kzIVY+Rt0AF5w5FYb0fi6Sp4CYdd6gEk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779414927; c=relaxed/simple; bh=bTkIT4LxnFycO3imBe+8uEWBl7HM1m7Ir/jEIjvnKSY=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=MNJuncAWA9CCZqXsb4MViiCJCpD7ev86RwDw6xR+8UeFU78RD7569arOBpMMRzBDxX2mQkjUtmjqcDnVXf6Bt7xQEVP0uFS62ojRzoSqtIiRDbW1eLgiBFt7D7r9ayzo4Ep9zlOv5if9ZseG+Ax5ZS/LtGg/hmssbbNcCYPfyLg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=YBwn8zwe; arc=none smtp.client-ip=74.125.82.171 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="YBwn8zwe" Received: by mail-dy1-f171.google.com with SMTP id 5a478bee46e88-2ee990e8597so16700182eec.1 for ; Thu, 21 May 2026 18:55:24 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1779414923; x=1780019723; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=P/pSWLtddGQkmdhRY3wqdmBOv5/+xd/Ew63rTEYvVW0=; b=YBwn8zwepZGSuX3JnOr3f2gGbPmzxBNOvYKUWSiByqcPGJDhMW13MMdVHIiwNVKHiy yb/aT1Tn/Mn3kjyManG31MPeFEjZNh0MiFlQvqre1N3mxDfX5D8stWWgLSPSR8XjpHVb tZLgXOMgS1T9qwc0QgX02s5jBxgNsomM4GuYXIF0P3w0Jn0tj0wkr1ggbZuFcKTzoBvr yWUbzVAUZIBDbukJIJw1a0e33L54USFduZv9hBT2ih1LmjWOBvJ8SiFHihe50ZWp83JF fylksBafcCG4GZGAB7zH8/lOXdblYO6UjbfoBt9unzOknPIRbB/l3YcVIvpGsDvUNlvY oP5g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1779414923; x=1780019723; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=P/pSWLtddGQkmdhRY3wqdmBOv5/+xd/Ew63rTEYvVW0=; b=sdY81/gWHWnIXyCFGopzaFRt6p77ijCiPQkvlhKUVOlMMbAVXVgt8tCamX5USHve8s OhK+SnDCKAgLZx4Q9kRCXpCF9Rfzk49gTtctzU/rwwCBVMIGDh65tbXuFDFCtKz8NGEf 5seIuHoYmQgQRlsIhP7XMbNxbo9y54O+03WmN5habUIKEl04el87CyVR/J7wO9soD3bT PO+ra39476id5LyjBXr/UBMpDRl5NOqjFDTI5nlHpea5vUXnYjLrBJqS62g6tBs8dPd5 flZNnp7InkFFknyY1J16TzlxxlDnsBnvWzBt+IC38e7hJBwZUOVArljrrtiq0IqEg8qU 2Oog== X-Forwarded-Encrypted: i=1; AFNElJ8gk6rb2OaGdJW7OAyVDNVcJVPKoBT5ik8F7XbjWQjyV6AZBOLpeybGSSsAnPbOCPSRZJdqVPhpW2mAX9c=@vger.kernel.org X-Gm-Message-State: AOJu0YyeVrTEbJlI+M8zFZMXh3vGyeywB6d3OE+U8U3+5m0EgmIo9Kux iddpCVEf4Se5R0ajD33aWyEKLm1Gnt620M+EdJdnigLo1R3eMW+pWc0C X-Gm-Gg: Acq92OG+B38g/5iwzU5Fp+7Wqv97lgNKJ7SlGjbh7PrwV4b6/O46pwpBcVTq8aG0dqK M3rpG0CvvBCDrrASFChmYSxntkWja+3J67p26WNS9e+p9qtg8q7sNUxb/R8JeGV1T5jYV4rpBNl gYwGnbzQG6TIxPE3XcOHP1/Rg/+0E/Av+W0HKft7hokI2Wb6PDnzAxzN86CWkUJAWOU1NhBdTsj 1LEkjam3canrbh+s4NJM5DIFIW0uLEypU8/G9DdeGLO65+Z4QVtKTHYlt34IByeP5tIiv1rGx5o ZhWeuGGITFM5yEvfkd59pvpju8oBqyIhF/Lgoi57YbMbJHlD9cli/0gRK35gQ+5IvlWrsTlRWmo 2VRYSIdjYHeMv+WdJrmqS5BzX/v/fJT0VW40RE2b0NMbJvE1CUHHQLNs0GXtY2fc6ECqcjtt3Oi 5E7I8t0aOrz3ak3cDssIvwjuESN3228To8mTbGlDjSpAoGWTmmoayPoLtzLoXE6UiM53h06gazi XwfDyppDJTL3Rw= X-Received: by 2002:a05:7300:b507:b0:2ed:e15:c927 with SMTP id 5a478bee46e88-304491471cemr1073900eec.35.1779414923266; Thu, 21 May 2026 18:55:23 -0700 (PDT) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-3044b9123f7sm1027744eec.19.2026.05.21.18.55.22 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 21 May 2026 18:55:22 -0700 (PDT) From: "Derek J. Clark" To: Jiri Kosina , Benjamin Tissoires Cc: "Pierre-Loup A . Griffais" , Denis Benato , Zhouwang Huang , "Derek J . Clark" , linux-input@vger.kernel.org, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v8 2/4] HID: hid-msi: Add M-key mapping attributes Date: Fri, 22 May 2026 01:55:16 +0000 Message-ID: <20260522015518.1111290-3-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260522015518.1111290-1-derekjohn.clark@gmail.com> References: <20260522015518.1111290-1-derekjohn.clark@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Adds attributes that allow for remapping the M-keys with up to 5 values when in macro mode. There are 2 mappable buttons on the rear of the device, M1 on the right and M2 on the left. When mapped, the events will fire from one of three event devices: gamepad buttons will fire from the device handled by xpad, while keyboard and mouse events will fire from respectively typed evdevs provided by the input core. Names of each mapping have been kept as close to the event that will fire from the evdev as possible, with context added to the ABS_ events on the direction of the movement. Initial reverse-engineering and implementation of this feature was done by Zhouwang Huang. I refactored the overall format to conform to kernel driver best practices and style guides. Claude was used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- v8: - Wrap all branches under single cmd_lock guard in claw_raw_event. - Reject generic ACK in claw_raw_event if waiting_cmd is for another branch. v7: - Use smp_[store_release|load_acquire] pattern for checking gamepad_registered to avoid possible races during teardown. - Add profile_lock for read/write profile_pending. - Match on write address for mkey reports to prevent late ACK from causing synchronization errors. - Use struct for mkey reports. v6: - Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz config. - Gate all attribute show/store functions with gamepad_registered. - Remove duplicated argv_free macro. v5: - Ensure adding "DISABLED" key to valid entries is done in the correct patch. - Re-enable sending an empty string to clear button mappings in addition to setting DISABLED. v4: - Change dev_warn to dev_dbg in claw_profile_event. - use __free with DEFINE_FREE macro for argv instead of manually running argv_free, cleaining up scoped_guard goto. v3: - Use scoped_guard where necessary. v2: - Add mutex for SYNC_TO_ROM commands to ensure every SYNC is completed before more data is written to the MCU volatile memory. - Add mutex for profile_pending to ensure every profile action response is serialized to the generating command. --- drivers/hid/hid-msi.c | 448 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 446 insertions(+), 2 deletions(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index 40b16253abbb3..b9901869075f6 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -42,6 +42,8 @@ #define CLAW_DINPUT_CFG_INTF_IN 0x82 #define CLAW_XINPUT_CFG_INTF_IN 0x83 =20 +#define CLAW_KEYS_MAX 5 + enum claw_command_index { CLAW_COMMAND_TYPE_NONE =3D 0x00, CLAW_COMMAND_TYPE_READ_PROFILE =3D 0x04, @@ -67,6 +69,17 @@ static const char * const claw_gamepad_mode_text[] =3D { [CLAW_GAMEPAD_MODE_DESKTOP] =3D "desktop", }; =20 +enum claw_profile_ack_pending { + CLAW_NO_PENDING, + CLAW_M1_PENDING, + CLAW_M2_PENDING, +}; + +enum claw_key_index { + CLAW_KEY_M1, + CLAW_KEY_M2, +}; + enum claw_mkeys_function_index { CLAW_MKEY_FUNCTION_MACRO, CLAW_MKEY_FUNCTION_DISABLED, @@ -79,6 +92,155 @@ static const char * const claw_mkeys_function_text[] = =3D { [CLAW_MKEY_FUNCTION_COMBO] =3D "combination", }; =20 +static const struct { + u8 code; + const char *name; +} claw_button_mapping_key_map[] =3D { + /* Gamepad buttons */ + { 0x01, "ABS_HAT0Y_UP" }, + { 0x02, "ABS_HAT0Y_DOWN" }, + { 0x03, "ABS_HAT0X_LEFT" }, + { 0x04, "ABS_HAT0X_RIGHT" }, + { 0x05, "BTN_TL" }, + { 0x06, "BTN_TR" }, + { 0x07, "BTN_THUMBL" }, + { 0x08, "BTN_THUMBR" }, + { 0x09, "BTN_SOUTH" }, + { 0x0a, "BTN_EAST" }, + { 0x0b, "BTN_NORTH" }, + { 0x0c, "BTN_WEST" }, + { 0x0d, "BTN_MODE" }, + { 0x0e, "BTN_SELECT" }, + { 0x0f, "BTN_START" }, + { 0x13, "BTN_TL2"}, + { 0x14, "BTN_TR2"}, + { 0x15, "ABS_Y_UP"}, + { 0x16, "ABS_Y_DOWN"}, + { 0x17, "ABS_X_LEFT"}, + { 0x18, "ABS_X_RIGHT"}, + { 0x19, "ABS_RY_UP"}, + { 0x1a, "ABS_RY_DOWN"}, + { 0x1b, "ABS_RX_LEFT"}, + { 0x1c, "ABS_RX_RIGHT"}, + /* Keyboard keys */ + { 0x32, "KEY_ESC" }, + { 0x33, "KEY_F1" }, + { 0x34, "KEY_F2" }, + { 0x35, "KEY_F3" }, + { 0x36, "KEY_F4" }, + { 0x37, "KEY_F5" }, + { 0x38, "KEY_F6" }, + { 0x39, "KEY_F7" }, + { 0x3a, "KEY_F8" }, + { 0x3b, "KEY_F9" }, + { 0x3c, "KEY_F10" }, + { 0x3d, "KEY_F11" }, + { 0x3e, "KEY_F12" }, + { 0x3f, "KEY_GRAVE" }, + { 0x40, "KEY_1" }, + { 0x41, "KEY_2" }, + { 0x42, "KEY_3" }, + { 0x43, "KEY_4" }, + { 0x44, "KEY_5" }, + { 0x45, "KEY_6" }, + { 0x46, "KEY_7" }, + { 0x47, "KEY_8" }, + { 0x48, "KEY_9" }, + { 0x49, "KEY_0" }, + { 0x4a, "KEY_MINUS" }, + { 0x4b, "KEY_EQUAL" }, + { 0x4c, "KEY_BACKSPACE" }, + { 0x4d, "KEY_TAB" }, + { 0x4e, "KEY_Q" }, + { 0x4f, "KEY_W" }, + { 0x50, "KEY_E" }, + { 0x51, "KEY_R" }, + { 0x52, "KEY_T" }, + { 0x53, "KEY_Y" }, + { 0x54, "KEY_U" }, + { 0x55, "KEY_I" }, + { 0x56, "KEY_O" }, + { 0x57, "KEY_P" }, + { 0x58, "KEY_LEFTBRACE" }, + { 0x59, "KEY_RIGHTBRACE" }, + { 0x5a, "KEY_BACKSLASH" }, + { 0x5b, "KEY_CAPSLOCK" }, + { 0x5c, "KEY_A" }, + { 0x5d, "KEY_S" }, + { 0x5e, "KEY_D" }, + { 0x5f, "KEY_F" }, + { 0x60, "KEY_G" }, + { 0x61, "KEY_H" }, + { 0x62, "KEY_J" }, + { 0x63, "KEY_K" }, + { 0x64, "KEY_L" }, + { 0x65, "KEY_SEMICOLON" }, + { 0x66, "KEY_APOSTROPHE" }, + { 0x67, "KEY_ENTER" }, + { 0x68, "KEY_LEFTSHIFT" }, + { 0x69, "KEY_Z" }, + { 0x6a, "KEY_X" }, + { 0x6b, "KEY_C" }, + { 0x6c, "KEY_V" }, + { 0x6d, "KEY_B" }, + { 0x6e, "KEY_N" }, + { 0x6f, "KEY_M" }, + { 0x70, "KEY_COMMA" }, + { 0x71, "KEY_DOT" }, + { 0x72, "KEY_SLASH" }, + { 0x73, "KEY_RIGHTSHIFT" }, + { 0x74, "KEY_LEFTCTRL" }, + { 0x75, "KEY_LEFTMETA" }, + { 0x76, "KEY_LEFTALT" }, + { 0x77, "KEY_SPACE" }, + { 0x78, "KEY_RIGHTALT" }, + { 0x79, "KEY_RIGHTCTRL" }, + { 0x7a, "KEY_INSERT" }, + { 0x7b, "KEY_HOME" }, + { 0x7c, "KEY_PAGEUP" }, + { 0x7d, "KEY_DELETE" }, + { 0x7e, "KEY_END" }, + { 0x7f, "KEY_PAGEDOWN" }, + { 0x8a, "KEY_KPENTER" }, + { 0x8b, "KEY_KP0" }, + { 0x8c, "KEY_KP1" }, + { 0x8d, "KEY_KP2" }, + { 0x8e, "KEY_KP3" }, + { 0x8f, "KEY_KP4" }, + { 0x90, "KEY_KP5" }, + { 0x91, "KEY_KP6" }, + { 0x92, "KEY_KP7" }, + { 0x93, "KEY_KP8" }, + { 0x94, "KEY_KP9" }, + { 0x95, "MD_PLAY" }, + { 0x96, "MD_STOP" }, + { 0x97, "MD_NEXT" }, + { 0x98, "MD_PREV" }, + { 0x99, "MD_VOL_UP" }, + { 0x9a, "MD_VOL_DOWN" }, + { 0x9b, "MD_VOL_MUTE" }, + { 0x9c, "KEY_F23" }, + /* Mouse events */ + { 0xc8, "BTN_LEFT" }, + { 0xc9, "BTN_MIDDLE" }, + { 0xca, "BTN_RIGHT" }, + { 0xcb, "BTN_SIDE" }, + { 0xcc, "BTN_EXTRA" }, + { 0xcd, "REL_WHEEL_UP" }, + { 0xce, "REL_WHEEL_DOWN" }, + { 0xff, "DISABLED" }, +}; + +static const u16 button_mapping_addr_old[] =3D { + 0x007a, /* M1 */ + 0x011f, /* M2 */ +}; + +static const u16 button_mapping_addr_new[] =3D { + 0x00bb, /* M1 */ + 0x0164, /* M2 */ +}; + struct claw_command_report { u8 report_id; u8 padding[2]; @@ -87,25 +249,47 @@ struct claw_command_report { u8 data[59]; } __packed; =20 +struct claw_profile_report { + u8 profile; + __be16 read_addr; +} __packed; + +struct claw_mkey_report { + struct claw_profile_report; + u8 padding_0; + u8 padding_1; + u8 padding_2; + u8 codes[5]; +} __packed; + struct claw_drvdata { /* MCU General Variables */ + enum claw_profile_ack_pending profile_pending; struct completion send_cmd_complete; struct delayed_work cfg_resume; struct delayed_work cfg_setup; spinlock_t registration_lock; /* Lock for registration read/write */ + struct mutex profile_mutex; /* mutex for profile_pending calls */ + spinlock_t profile_lock; /* Lock for profile_pending read/write */ struct hid_device *hdev; struct mutex cfg_mutex; /* mutex for synchronous data */ + struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */ bool waiting_for_ack; spinlock_t cmd_lock; /* Lock for cmd data read/write */ u8 waiting_cmd; int cmd_status; + u16 bcd_device; u8 ep; =20 /* Gamepad Variables */ enum claw_mkeys_function_index mkeys_function; enum claw_gamepad_mode_index gamepad_mode; + u8 m1_codes[CLAW_KEYS_MAX]; + u8 m2_codes[CLAW_KEYS_MAX]; bool gamepad_registered; spinlock_t mode_lock; /* Lock for mode data read/write */ + const u16 *bmap_addr; + bool bmap_support; }; =20 static int get_endpoint_address(struct hid_device *hdev) @@ -137,6 +321,39 @@ static int claw_gamepad_mode_event(struct claw_drvdata= *drvdata, return 0; } =20 +static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_co= mmand_report *cmd_rep) +{ + enum claw_profile_ack_pending profile; + struct claw_mkey_report *mkeys; + u8 *codes, key; + int i; + + scoped_guard(spinlock, &drvdata->profile_lock) + profile =3D drvdata->profile_pending; + + switch (profile) { + case CLAW_M1_PENDING: + case CLAW_M2_PENDING: + key =3D (profile =3D=3D CLAW_M1_PENDING) ? CLAW_KEY_M1 : CLAW_KEY_M2; + mkeys =3D (struct claw_mkey_report *)cmd_rep->data; + if (be16_to_cpu(mkeys->read_addr) !=3D drvdata->bmap_addr[key]) + return -EINVAL; + codes =3D (profile =3D=3D CLAW_M1_PENDING) ? drvdata->m1_codes : drvdata= ->m2_codes; + for (i =3D 0; i < CLAW_KEYS_MAX; i++) + codes[i] =3D (mkeys->codes[i]); + break; + default: + dev_dbg(&drvdata->hdev->dev, + "Got profile event without changes pending from command: %x\n", + cmd_rep->cmd); + return -EINVAL; + } + scoped_guard(spinlock, &drvdata->profile_lock) + drvdata->profile_pending =3D CLAW_NO_PENDING; + + return 0; +} + static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report = *report, u8 *data, int size) { @@ -165,9 +382,20 @@ static int claw_raw_event(struct claw_drvdata *drvdata= , struct hid_report *repor complete(&drvdata->send_cmd_complete); } =20 + break; + case CLAW_COMMAND_TYPE_READ_PROFILE_ACK: + ret =3D claw_profile_event(drvdata, cmd_rep); + if (drvdata->waiting_for_ack && + drvdata->waiting_cmd =3D=3D CLAW_COMMAND_TYPE_READ_PROFILE) { + drvdata->cmd_status =3D ret; + drvdata->waiting_for_ack =3D false; + complete(&drvdata->send_cmd_complete); + } + break; case CLAW_COMMAND_TYPE_ACK: - if (drvdata->waiting_cmd =3D=3D CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE) + if (drvdata->waiting_cmd =3D=3D CLAW_COMMAND_TYPE_READ_PROFILE || + drvdata->waiting_cmd =3D=3D CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE) break; if (drvdata->waiting_for_ack) { drvdata->cmd_status =3D 0; @@ -442,6 +670,177 @@ static ssize_t reset_store(struct device *dev, struct= device_attribute *attr, } static DEVICE_ATTR_WO(reset); =20 +static int mkey_mapping_name_to_code(const char *name) +{ + int i; + + for (i =3D 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) { + if (!strcmp(name, claw_button_mapping_key_map[i].name)) + return claw_button_mapping_key_map[i].code; + } + + return -EINVAL; +} + +static const char *mkey_mapping_code_to_name(u8 code) +{ + int i; + + if (code =3D=3D 0xff) + return NULL; + + for (i =3D 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) { + if (claw_button_mapping_key_map[i].code =3D=3D code) + return claw_button_mapping_key_map[i].name; + } + + return NULL; +} + +static int claw_mkey_store(struct device *dev, const char *buf, u8 mkey) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct claw_mkey_report report =3D { {0x01, cpu_to_be16(drvdata->bmap_add= r[mkey])}, + 0x07, 0x04, 0x00, {0xff, 0xff, 0xff, 0xff, 0xff} }; + char **raw_keys __free(argv_free) =3D NULL; + int ret, key_count, i; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + raw_keys =3D argv_split(GFP_KERNEL, buf, &key_count); + if (!raw_keys) + return -ENOMEM; + + if (key_count > CLAW_KEYS_MAX) + return -EINVAL; + + if (key_count =3D=3D 0) + goto set_buttons; + + for (i =3D 0; i < key_count; i++) { + ret =3D mkey_mapping_name_to_code(raw_keys[i]); + if (ret < 0) + return ret; + + report.codes[i] =3D ret; + } + +set_buttons: + scoped_guard(mutex, &drvdata->rom_mutex) { + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + (u8 *)&report, sizeof(report), 25); + if (ret) + return ret; + /* MCU will not send ACK until the USB transaction completes. ACK is sent + * immediately after and will hit the stale state machine, before the ne= xt + * command re-arms the state machine. Timeout 0 ensures no deadlock wait= ing + * for ACK that ill never come. + */ + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL,= 0, 0); + } + + return ret; +} + +static int claw_mkey_show(struct device *dev, char *buf, enum claw_key_ind= ex m_key) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + struct claw_mkey_report report =3D { {0x01, cpu_to_be16(drvdata->bmap_add= r[m_key])}, 0x07 }; + int i, ret, count =3D 0; + const char *name; + u8 *codes; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + codes =3D (m_key =3D=3D CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_co= des; + + guard(mutex)(&drvdata->profile_mutex); + scoped_guard(spinlock_irqsave, &drvdata->profile_lock) + drvdata->profile_pending =3D (m_key =3D=3D CLAW_KEY_M1) ? CLAW_M1_PENDING + : CLAW_M2_PENDING; + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, + (u8 *)&report, sizeof(report), 25); + if (ret) + return ret; + + for (i =3D 0; i < CLAW_KEYS_MAX; i++) { + name =3D mkey_mapping_code_to_name(codes[i]); + if (name) + count +=3D sysfs_emit_at(buf, count, "%s ", name); + } + + if (!count) + return sysfs_emit(buf, "(not set)\n"); + + buf[count - 1] =3D '\n'; + + return count; +} + +static ssize_t button_m1_store(struct device *dev, struct device_attribute= *attr, + const char *buf, size_t count) +{ + int ret; + + ret =3D claw_mkey_store(dev, buf, CLAW_KEY_M1); + if (ret) + return ret; + + return count; +} + +static ssize_t button_m1_show(struct device *dev, struct device_attribute = *attr, + char *buf) +{ + return claw_mkey_show(dev, buf, CLAW_KEY_M1); +} +static DEVICE_ATTR_RW(button_m1); + +static ssize_t button_m2_store(struct device *dev, struct device_attribute= *attr, + const char *buf, size_t count) +{ + int ret; + + ret =3D claw_mkey_store(dev, buf, CLAW_KEY_M2); + if (ret) + return ret; + + return count; +} + +static ssize_t button_m2_show(struct device *dev, struct device_attribute = *attr, + char *buf) +{ + return claw_mkey_show(dev, buf, CLAW_KEY_M2); +} +static DEVICE_ATTR_RW(button_m2); + +static ssize_t button_mapping_options_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int i, count =3D 0; + + for (i =3D 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) + count +=3D sysfs_emit_at(buf, count, "%s ", claw_button_mapping_key_map[= i].name); + + if (count) + buf[count - 1] =3D '\n'; + + return count; +} +static DEVICE_ATTR_RO(button_mapping_options); + static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct a= ttribute *attr, int n) { @@ -454,10 +853,22 @@ static umode_t claw_gamepad_attr_is_visible(struct ko= bject *kobj, struct attribu return 0; } =20 - return attr->mode; + /* Always show attrs available on all firmware */ + if (attr =3D=3D &dev_attr_gamepad_mode.attr || + attr =3D=3D &dev_attr_gamepad_mode_index.attr || + attr =3D=3D &dev_attr_mkeys_function.attr || + attr =3D=3D &dev_attr_mkeys_function_index.attr || + attr =3D=3D &dev_attr_reset.attr) + return attr->mode; + + /* Hide button mapping attrs if it isn't supported */ + return drvdata->bmap_support ? attr->mode : 0; } =20 static struct attribute *claw_gamepad_attrs[] =3D { + &dev_attr_button_m1.attr, + &dev_attr_button_m2.attr, + &dev_attr_button_mapping_options.attr, &dev_attr_gamepad_mode.attr, &dev_attr_gamepad_mode_index.attr, &dev_attr_mkeys_function.attr, @@ -510,8 +921,31 @@ static void cfg_resume_fn(struct work_struct *work) schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500)); } =20 +static void claw_features_supported(struct claw_drvdata *drvdata) +{ + u8 major =3D (drvdata->bcd_device >> 8) & 0xff; + u8 minor =3D drvdata->bcd_device & 0xff; + + if (major =3D=3D 0x01) { + drvdata->bmap_support =3D true; + if (minor >=3D 0x66) + drvdata->bmap_addr =3D button_mapping_addr_new; + else + drvdata->bmap_addr =3D button_mapping_addr_old; + return; + } + + if ((major =3D=3D 0x02 && minor >=3D 0x17) || major >=3D 0x03) { + drvdata->bmap_support =3D true; + drvdata->bmap_addr =3D button_mapping_addr_new; + return; + } +} + static int claw_probe(struct hid_device *hdev, u8 ep) { + struct usb_interface *intf =3D to_usb_interface(hdev->dev.parent); + struct usb_device *udev =3D interface_to_usbdev(intf); struct claw_drvdata *drvdata; int ret; =20 @@ -523,10 +957,20 @@ static int claw_probe(struct hid_device *hdev, u8 ep) drvdata->hdev =3D hdev; drvdata->ep =3D ep; =20 + /* Determine feature level from firmware version */ + drvdata->bcd_device =3D le16_to_cpu(udev->descriptor.bcdDevice); + claw_features_supported(drvdata); + + if (!drvdata->bmap_support) + dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to = enable.\n"); + mutex_init(&drvdata->cfg_mutex); + mutex_init(&drvdata->profile_mutex); + mutex_init(&drvdata->rom_mutex); spin_lock_init(&drvdata->registration_lock); spin_lock_init(&drvdata->cmd_lock); spin_lock_init(&drvdata->mode_lock); + spin_lock_init(&drvdata->profile_lock); init_completion(&drvdata->send_cmd_complete); INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn); INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn); --=20 2.53.0 From nobody Sun May 24 20:33:27 2026 Received: from mail-dy1-f177.google.com (mail-dy1-f177.google.com [74.125.82.177]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E61332D7398 for ; Fri, 22 May 2026 01:55:24 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.177 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779414929; cv=none; b=qwv7JLNo9jyMpXzy4R/4pSFcvtJRdAWwh/fTOVH1KcbYFArvQwryQzbRVeBTqsolCnqDxMS/9CmRlulOirlnuR0IaFp14m4Dw13Gx1JwadlFu3GtLk63iPkH5L8QSR04YzY93rELoHJuhrWEkYX19Jfcn/r6N7SkDGKtFXyozvs= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779414929; c=relaxed/simple; bh=nsgKmC24Q3Wacc+dHRP5oeBFwu1L7CQnimNp0uQaakw=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=lmS3RTKGQOroAebWnIR9ZwulNdU96C3PMKLOTB6wODlFJPboJV/aPjh1r6hSOsUPf0rK1FNsfhKwP5+aJ9bkmLHe3Xf+yjJopjj2tGavupVpBY4bYve/zKTE4Ha+tw7T5hET7coJMCSjNnuF3nHSJNNnzc1qeoGLDnZP9fa9oP4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=aNFSDJnj; arc=none smtp.client-ip=74.125.82.177 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="aNFSDJnj" Received: by mail-dy1-f177.google.com with SMTP id 5a478bee46e88-2f3c623322bso979899eec.0 for ; Thu, 21 May 2026 18:55:24 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1779414924; x=1780019724; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=UkhwicTuXqbtruN/bSd+0PHiiAyAEIUoztnPUiQyjPY=; b=aNFSDJnjiI6elEOg7/qjn1iWLHIT5MB/s01eBCSW9Utsvh6toL1BwjYkDuwdOs8NVw ijqwSizCgm8dgl9NpnG8JdwclQMlIxrwJa74FHU39y7TvCne7Gkr67PqOof+DCLStIgn mOgVQbfTMBSbtW45YHbRX3Clbp2HMPY7YcCOD+6XE229lLg3/PWNBRQ5jpIz+TgcTylE 7EPDNgQvfHHTLnuBT7nMhxh/M59IC6eySay6/5KI1x5d+DOwD2t45RFniXuv9yHsvREI sjzdzL1SY0uHbsVpLq67Sx/IfdeWqkSW/5DqhnRG8J85TgljdjTWrVEPDO78+Gk25huq GKLA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1779414924; x=1780019724; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=UkhwicTuXqbtruN/bSd+0PHiiAyAEIUoztnPUiQyjPY=; b=hJh2qNfcj7RIF+6TRSLdA58irE5vkqqZhMfpADnszZhYlIRtEeIa07mDeKG9YXzKjY naKMFuO4RPON/yzObo38YyJqn70dd4gAuYt0l6V31S2fOXS7NadlgbQHlHIYDybQPCVZ JAkilKKUUuaBQQYfxHHLAFGMscRTllPX8gejRZY5L4uTb27iOtz2gSIAEtLcmHf0GawK D5iCi+RlGUd6+Vqioidox88uMqcDSchoNO6kdTFIn+pSgmkVk6nv7loEAq7xgOH3I5AD kG6XKhQT40kPaXqNnB8gT9igiu0F+QLi23T/4IT9ZN30th4EvaBMiQexwLMjSy8KYj1j LsgA== X-Forwarded-Encrypted: i=1; AFNElJ8rXsNS1y4nCM5Kd8jQQwvSCryZe1xbiMU4BpBmUCUoh5EeTb0zrOWkxQbHk9HtP76zlnbkN37cm+7Nmao=@vger.kernel.org X-Gm-Message-State: AOJu0Yz+9b5qz0OgzP2vaJBh50Zi3TA0/8v7kwdz/932PKwMdqGJHbx9 lpJAbkajV7NKEXZn6ctyTaCuBoPAm1IFYz15a4ygI0IpwyF7KC44yL3b4GmDnA== X-Gm-Gg: Acq92OHDsvo5+8/4OslYNiScFxFQ0+Ax6IBWZx8Cjxw42T/vS7XtZyQRFHsJfVsbs1K DmtAijwSuRJPCAbCxDyNnWIJ4ccLDk1WKt7nusAZZrflBcOf/oCMbp4cNF9sH27pgsHVT4bAQmR U72uiIPBsCTwRNBi7aEtOYj1YZvFTyV2G+lD7h2nHWMjEz6Psz+Z1FmDT+XUcfpRu5UHTI8CfDr Uj4WO/BgoYD79pN3xeSnNBww529afiNo4d4Ba6ormNR1N+/qhVIg16EG/q+eJtu1gfnIerrmInL YsbYGjTkY8p3WukI4DCpchMPxzQzjzLeSHPGhBEDW7EVzOXzFowXLaYNFY8CPJdiRIoLcK0mdky pHY79hX7oEw3Bsp9A53S2C5cZCtZFqwamlgE/uTIo1nmuv/f+3dibUEURhHF2A9cRDY0myCXw+1 sKauphWZGlbT4HRMLdZQy4ev4AhtT32txrlQSVuuW17Ls8Yam+/9rY7xEy3R5pxxXOZhHLbNa8x koz X-Received: by 2002:a05:7301:6589:b0:2ed:e12:3770 with SMTP id 5a478bee46e88-304491f622amr856893eec.32.1779414924007; Thu, 21 May 2026 18:55:24 -0700 (PDT) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-3044b9123f7sm1027744eec.19.2026.05.21.18.55.23 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 21 May 2026 18:55:23 -0700 (PDT) From: "Derek J. Clark" To: Jiri Kosina , Benjamin Tissoires Cc: "Pierre-Loup A . Griffais" , Denis Benato , Zhouwang Huang , "Derek J . Clark" , linux-input@vger.kernel.org, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v8 3/4] HID: hid-msi: Add RGB control interface Date: Fri, 22 May 2026 01:55:17 +0000 Message-ID: <20260522015518.1111290-4-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260522015518.1111290-1-derekjohn.clark@gmail.com> References: <20260522015518.1111290-1-derekjohn.clark@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Adds RGB control interface for MSI Claw devices. The MSI Claw uses a fairly unique RGB interface. It has 9 total zones (4 per joystick ring and 1 for the ABXY buttons), and supports up to 8 sequential frames of RGB zone data. Each frame is written to a specific area of MCU memory by the profile command, the value of which changes based on the firmware of the device. Unlike other devices (such as the Legion Go or the OneXPlayer devices), there are no hard coded effects built into the MCU. Instead, the basic effects are provided as a series of frame data. I have mirrored the effects available in Windows in this driver, while keeping the effect names consistent with the Lenovo drivers for the effects that are similar. Initial reverse-engineering and implementation of this feature was done by Zhouwang Huang. I refactored the overall format to conform to kernel driver best practices and style guides. Claude was used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- v8: - Ensure led_classdev is unregistered if adding attribute group fails. - Reorder remove actions to ensure no use-after free or rearming cleared flags. v7: - Use smp_[store_release|load_acquire] pattern for checking rgb_registered to avoid possible races during teardown. - Add gating to cfg_setup_fn, allowing either gamepad settings or rgb settings to populate if the other fails for any reason. - Use spinlock when writing profile_pending. v6: - Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz config. - Gate all attribute show/store functions with rgb_registered, enabling use of devm_device_add_group. v5: - Move adding the RGB device into cfg_setup to prevent led core attributes from being written to prior to setup completing. - Ensure frame_lock is properly init. - Change variable names in RGB functions from frame and zone to f and z respectively to fit all scoped_guard actions in 100 columns. v4: - Fix frame_calc validity check to use >=3D. - USe spinlock instead of mutex in raw_event and related attribute _store function. - Ensure delayed work is canceled in suspend & canceled before sysfs attribute removal. v3: - Add mutex for read/write of rgb frame data. - Remove setting rgb_frame_count when reading rgb profiles as it always returns garbage data. - Ensure rgb_speed is getting drvdata from a valid lookup (not hdev). v2: - Use pending_profile mutex --- drivers/hid/hid-msi.c | 674 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 654 insertions(+), 20 deletions(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index b9901869075f6..0b875603dbb32 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,10 @@ =20 #define CLAW_KEYS_MAX 5 =20 +#define CLAW_RGB_ZONES 9 +#define CLAW_RGB_MAX_FRAMES 8 +#define CLAW_RGB_FRAME_OFFSET 0x24 + enum claw_command_index { CLAW_COMMAND_TYPE_NONE =3D 0x00, CLAW_COMMAND_TYPE_READ_PROFILE =3D 0x04, @@ -73,6 +78,7 @@ enum claw_profile_ack_pending { CLAW_NO_PENDING, CLAW_M1_PENDING, CLAW_M2_PENDING, + CLAW_RGB_PENDING, }; =20 enum claw_key_index { @@ -231,6 +237,22 @@ static const struct { { 0xff, "DISABLED" }, }; =20 +enum claw_rgb_effect_index { + CLAW_RGB_EFFECT_MONOCOLOR, + CLAW_RGB_EFFECT_BREATHE, + CLAW_RGB_EFFECT_CHROMA, + CLAW_RGB_EFFECT_RAINBOW, + CLAW_RGB_EFFECT_FROSTFIRE, +}; + +static const char * const claw_rgb_effect_text[] =3D { + [CLAW_RGB_EFFECT_MONOCOLOR] =3D "monocolor", + [CLAW_RGB_EFFECT_BREATHE] =3D "breathe", + [CLAW_RGB_EFFECT_CHROMA] =3D "chroma", + [CLAW_RGB_EFFECT_RAINBOW] =3D "rainbow", + [CLAW_RGB_EFFECT_FROSTFIRE] =3D "frostfire", +}; + static const u16 button_mapping_addr_old[] =3D { 0x007a, /* M1 */ 0x011f, /* M2 */ @@ -241,6 +263,9 @@ static const u16 button_mapping_addr_new[] =3D { 0x0164, /* M2 */ }; =20 +static const u16 rgb_addr_old =3D 0x01fa; +static const u16 rgb_addr_new =3D 0x024a; + struct claw_command_report { u8 report_id; u8 padding[2]; @@ -262,6 +287,27 @@ struct claw_mkey_report { u8 codes[5]; } __packed; =20 +struct rgb_zone { + u8 red; + u8 green; + u8 blue; +}; + +struct rgb_frame { + struct rgb_zone zone[CLAW_RGB_ZONES]; +}; + +struct claw_rgb_report { + struct claw_profile_report; + u8 frame_bytes; + u8 padding; + u8 frame_count; + u8 state; /* Always 0x09 */ + u8 speed; + u8 brightness; + struct rgb_frame zone_data; +} __packed; + struct claw_drvdata { /* MCU General Variables */ enum claw_profile_ack_pending profile_pending; @@ -290,6 +336,18 @@ struct claw_drvdata { spinlock_t mode_lock; /* Lock for mode data read/write */ const u16 *bmap_addr; bool bmap_support; + + /* RGB Variables */ + struct rgb_frame rgb_frames[CLAW_RGB_MAX_FRAMES]; + enum claw_rgb_effect_index rgb_effect; + struct led_classdev_mc led_mc; + struct delayed_work rgb_queue; + spinlock_t frame_lock; /* lock for rgb_frames read/write */ + bool rgb_registered; + u8 rgb_frame_count; + bool rgb_enabled; + u8 rgb_speed; + u16 rgb_addr; }; =20 static int get_endpoint_address(struct hid_device *hdev) @@ -325,8 +383,11 @@ static int claw_profile_event(struct claw_drvdata *drv= data, struct claw_command_ { enum claw_profile_ack_pending profile; struct claw_mkey_report *mkeys; - u8 *codes, key; - int i; + struct claw_rgb_report *frame; + u16 rgb_addr, read_addr; + u8 *codes, key, f_idx; + u16 frame_calc; + int i, ret =3D 0; =20 scoped_guard(spinlock, &drvdata->profile_lock) profile =3D drvdata->profile_pending; @@ -342,6 +403,34 @@ static int claw_profile_event(struct claw_drvdata *drv= data, struct claw_command_ for (i =3D 0; i < CLAW_KEYS_MAX; i++) codes[i] =3D (mkeys->codes[i]); break; + case CLAW_RGB_PENDING: + frame =3D (struct claw_rgb_report *)cmd_rep->data; + rgb_addr =3D drvdata->rgb_addr; + read_addr =3D be16_to_cpu(frame->read_addr); + frame_calc =3D (read_addr - rgb_addr) / CLAW_RGB_FRAME_OFFSET; + if (frame_calc >=3D CLAW_RGB_MAX_FRAMES) { + dev_err(&drvdata->hdev->dev, "Got unsupported frame index: %x\n", + frame_calc); + return -EINVAL; + } + f_idx =3D frame_calc; + + scoped_guard(spinlock, &drvdata->frame_lock) { + memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data, + sizeof(struct rgb_frame)); + + /* Only use frame 0 for remaining variable assignment */ + if (f_idx !=3D 0) + break; + + drvdata->rgb_speed =3D frame->speed; + drvdata->led_mc.led_cdev.brightness =3D frame->brightness; + drvdata->led_mc.subled_info[0].intensity =3D frame->zone_data.zone[0].r= ed; + drvdata->led_mc.subled_info[1].intensity =3D frame->zone_data.zone[0].g= reen; + drvdata->led_mc.subled_info[2].intensity =3D frame->zone_data.zone[0].b= lue; + } + + break; default: dev_dbg(&drvdata->hdev->dev, "Got profile event without changes pending from command: %x\n", @@ -351,7 +440,7 @@ static int claw_profile_event(struct claw_drvdata *drvd= ata, struct claw_command_ scoped_guard(spinlock, &drvdata->profile_lock) drvdata->profile_pending =3D CLAW_NO_PENDING; =20 - return 0; + return ret; } =20 static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report = *report, @@ -882,32 +971,545 @@ static const struct attribute_group claw_gamepad_att= r_group =3D { .is_visible =3D claw_gamepad_attr_is_visible, }; =20 +/* Read RGB config from device */ +static int claw_read_rgb_config(struct hid_device *hdev) +{ + u8 data[4] =3D { 0x01, 0x00, 0x00, CLAW_RGB_FRAME_OFFSET }; + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + u16 read_addr =3D drvdata->rgb_addr; + size_t len =3D ARRAY_SIZE(data); + int ret, i; + + if (!drvdata->rgb_addr) + return -ENODEV; + + /* Loop through all 8 pages of RGB data */ + guard(mutex)(&drvdata->profile_mutex); + for (i =3D 0; i < CLAW_RGB_MAX_FRAMES; i++) { + scoped_guard(spinlock_irqsave, &drvdata->profile_lock) + drvdata->profile_pending =3D CLAW_RGB_PENDING; + data[1] =3D (read_addr >> 8) & 0xff; + data[2] =3D read_addr & 0x00ff; + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data= , len, 25); + if (ret) + return ret; + + read_addr +=3D CLAW_RGB_FRAME_OFFSET; + } + + return 0; +} + +/* Send RGB configuration to device */ +static int claw_write_rgb_state(struct claw_drvdata *drvdata) +{ + struct claw_rgb_report report =3D { {0x01, 0}, CLAW_RGB_FRAME_OFFSET, 0x0= 0, + drvdata->rgb_frame_count, 0x09, drvdata->rgb_speed, + drvdata->led_mc.led_cdev.brightness }; + u16 write_addr =3D drvdata->rgb_addr; + int f, ret; + + if (!drvdata->rgb_addr) + return -ENODEV; + + if (!drvdata->rgb_frame_count) + return -EINVAL; + + guard(mutex)(&drvdata->rom_mutex); + /* Loop through (up to) 8 pages of RGB data */ + for (f =3D 0; f < drvdata->rgb_frame_count; f++) { + scoped_guard(spinlock_irqsave, &drvdata->frame_lock) + report.zone_data =3D drvdata->rgb_frames[f]; + + /* Set the MCU address to write the frame data to */ + report.read_addr =3D cpu_to_be16(write_addr); + + /* Serialize the rgb_report and write it to MCU */ + ret =3D claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_WRITE_PRO= FILE_DATA, + (u8 *)&report, sizeof(report), 25); + if (ret) + return ret; + + /* Increment the write addr by the offset for the next frame */ + write_addr +=3D CLAW_RGB_FRAME_OFFSET; + } + + /* MCU will not send ACK until the USB transaction completes. ACK is sent + * immediately after and will hit the stale state machine, before the next + * command re-arms the state machine. Timeout 0 ensures no deadlock waiti= ng + * for ACK that ill never come. + */ + ret =3D claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SYNC_TO_RO= M, NULL, 0, 0); + + return ret; +} + +/* Fill all zones with the same color */ +static void claw_frame_fill_solid(struct rgb_frame *frame, struct rgb_zone= zone) +{ + int z; + + for (z =3D 0; z < CLAW_RGB_ZONES; z++) + frame->zone[z] =3D zone; +} + +/* Apply solid effect (1 frame, no color) */ +static int claw_apply_disabled(struct claw_drvdata *drvdata) +{ + struct rgb_zone off =3D { 0x00, 0x00, 0x00}; + + scoped_guard(spinlock_irqsave, &drvdata->frame_lock) { + drvdata->rgb_frame_count =3D 1; + claw_frame_fill_solid(&drvdata->rgb_frames[0], off); + } + + return claw_write_rgb_state(drvdata); +} + +/* Apply solid effect (1 frame, all zones same color) */ +static int claw_apply_monocolor(struct claw_drvdata *drvdata) +{ + struct mc_subled *subleds =3D drvdata->led_mc.subled_info; + struct rgb_zone zone =3D { subleds[0].intensity, subleds[1].intensity, + subleds[2].intensity }; + + scoped_guard(spinlock_irqsave, &drvdata->frame_lock) { + drvdata->rgb_frame_count =3D 1; + claw_frame_fill_solid(&drvdata->rgb_frames[0], zone); + } + + return claw_write_rgb_state(drvdata); +} + +/* Apply breathe effect (2 frames: color -> off) */ +static int claw_apply_breathe(struct claw_drvdata *drvdata) +{ + struct mc_subled *subleds =3D drvdata->led_mc.subled_info; + struct rgb_zone zone =3D { subleds[0].intensity, subleds[1].intensity, + subleds[2].intensity }; + static const struct rgb_zone off =3D { 0, 0, 0 }; + + scoped_guard(spinlock_irqsave, &drvdata->frame_lock) { + drvdata->rgb_frame_count =3D 2; + claw_frame_fill_solid(&drvdata->rgb_frames[0], zone); + claw_frame_fill_solid(&drvdata->rgb_frames[1], off); + } + + return claw_write_rgb_state(drvdata); +} + +/* Apply chroma effect (6 frames: rainbow cycle, all zones sync) */ +static int claw_apply_chroma(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone colors[] =3D { + {255, 0, 0}, /* red */ + {255, 255, 0}, /* yellow */ + { 0, 255, 0}, /* green */ + { 0, 255, 255}, /* cyan */ + { 0, 0, 255}, /* blue */ + {255, 0, 255}, /* magenta */ + }; + u8 frame_count =3D ARRAY_SIZE(colors); + int f; + + scoped_guard(spinlock_irqsave, &drvdata->frame_lock) { + drvdata->rgb_frame_count =3D frame_count; + + for (f =3D 0; f < frame_count; f++) + claw_frame_fill_solid(&drvdata->rgb_frames[f], colors[f]); + } + + return claw_write_rgb_state(drvdata); +} + +/* Apply rainbow effect (4 frames: rotating colors around joysticks) */ +static int claw_apply_rainbow(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone colors[] =3D { + {255, 0, 0}, /* red */ + { 0, 255, 0}, /* green */ + { 0, 255, 255}, /* cyan */ + { 0, 0, 255}, /* blue */ + }; + u8 frame_count =3D ARRAY_SIZE(colors); + int f, z; + + scoped_guard(spinlock_irqsave, &drvdata->frame_lock) { + drvdata->rgb_frame_count =3D frame_count; + + for (f =3D 0; f < frame_count; f++) { + for (z =3D 0; z < 4; z++) { + drvdata->rgb_frames[f].zone[z] =3D colors[(z + f) % 4]; + drvdata->rgb_frames[f].zone[z + 4] =3D colors[(z + f) % 4]; + } + drvdata->rgb_frames[f].zone[8] =3D colors[f]; + } + } + + return claw_write_rgb_state(drvdata); +} + +/* + * Apply frostfire effect (4 frames: fire vs ice rotating) + * Right joystick: fire red -> dark -> ice blue -> dark (clockwise) + * Left joystick: ice blue -> dark -> fire red -> dark (counter-clockwise) + * ABXY: fire red -> dark -> ice blue -> dark + */ +static int claw_apply_frostfire(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone colors[] =3D { + {255, 0, 0}, /* fire red */ + { 0, 0, 0}, /* dark */ + { 0, 0, 255}, /* ice blue */ + { 0, 0, 0}, /* dark */ + }; + u8 frame_count =3D ARRAY_SIZE(colors); + int f, z; + + scoped_guard(spinlock_irqsave, &drvdata->frame_lock) { + drvdata->rgb_frame_count =3D frame_count; + + for (f =3D 0; f < frame_count; f++) { + for (z =3D 0; z < 4; z++) { + drvdata->rgb_frames[f].zone[z] =3D colors[(z + f) % 4]; + drvdata->rgb_frames[f].zone[z + 4] =3D colors[(z - f + 6) % 4]; + } + drvdata->rgb_frames[f].zone[8] =3D colors[f]; + } + } + + return claw_write_rgb_state(drvdata); +} + +/* Apply current state to device */ +static int claw_apply_rgb_state(struct claw_drvdata *drvdata) +{ + if (!drvdata->rgb_enabled) + return claw_apply_disabled(drvdata); + + switch (drvdata->rgb_effect) { + case CLAW_RGB_EFFECT_MONOCOLOR: + return claw_apply_monocolor(drvdata); + case CLAW_RGB_EFFECT_BREATHE: + return claw_apply_breathe(drvdata); + case CLAW_RGB_EFFECT_CHROMA: + return claw_apply_chroma(drvdata); + case CLAW_RGB_EFFECT_RAINBOW: + return claw_apply_rainbow(drvdata); + case CLAW_RGB_EFFECT_FROSTFIRE: + return claw_apply_frostfire(drvdata); + default: + dev_err(drvdata->led_mc.led_cdev.dev, + "No supported rgb_effect selected\n"); + return -EINVAL; + } +} + +static void claw_rgb_queue_fn(struct work_struct *work) +{ + struct delayed_work *dwork =3D container_of(work, struct delayed_work, wo= rk); + struct claw_drvdata *drvdata =3D container_of(dwork, struct claw_drvdata,= rgb_queue); + int ret; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->rgb_registered)) + return; + } + + ret =3D claw_apply_rgb_state(drvdata); + if (ret) + dev_err(drvdata->led_mc.led_cdev.dev, + "Failed to apply RGB state: %d\n", ret); +} + +static ssize_t effect_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev =3D dev_get_drvdata(dev); + struct led_classdev_mc *led_mc =3D container_of(led_cdev, struct led_clas= sdev_mc, led_cdev); + struct claw_drvdata *drvdata =3D container_of(led_mc, struct claw_drvdata= , led_mc); + int ret; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->rgb_registered)) + return -ENODEV; + } + + ret =3D sysfs_match_string(claw_rgb_effect_text, buf); + if (ret < 0) + return ret; + + drvdata->rgb_effect =3D ret; + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); + + return count; +} + +static ssize_t effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev =3D dev_get_drvdata(dev); + struct led_classdev_mc *led_mc =3D container_of(led_cdev, struct led_clas= sdev_mc, led_cdev); + struct claw_drvdata *drvdata =3D container_of(led_mc, struct claw_drvdata= , led_mc); + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->rgb_registered)) + return -ENODEV; + } + + if (drvdata->rgb_effect >=3D ARRAY_SIZE(claw_rgb_effect_text)) + return -EINVAL; + + return sysfs_emit(buf, "%s\n", claw_rgb_effect_text[drvdata->rgb_effect]); +} + +static DEVICE_ATTR_RW(effect); + +static ssize_t effect_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + int i, count =3D 0; + + for (i =3D 0; i < ARRAY_SIZE(claw_rgb_effect_text); i++) + count +=3D sysfs_emit_at(buf, count, "%s ", claw_rgb_effect_text[i]); + + if (count) + buf[count - 1] =3D '\n'; + + return count; +} +static DEVICE_ATTR_RO(effect_index); + +static ssize_t enabled_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev =3D dev_get_drvdata(dev); + struct led_classdev_mc *led_mc =3D container_of(led_cdev, struct led_clas= sdev_mc, led_cdev); + struct claw_drvdata *drvdata =3D container_of(led_mc, struct claw_drvdata= , led_mc); + bool val; + int ret; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->rgb_registered)) + return -ENODEV; + } + + ret =3D kstrtobool(buf, &val); + if (ret) + return ret; + + drvdata->rgb_enabled =3D val; + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); + + return count; +} + +static ssize_t enabled_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev =3D dev_get_drvdata(dev); + struct led_classdev_mc *led_mc =3D container_of(led_cdev, struct led_clas= sdev_mc, led_cdev); + struct claw_drvdata *drvdata =3D container_of(led_mc, struct claw_drvdata= , led_mc); + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->rgb_registered)) + return -ENODEV; + } + + return sysfs_emit(buf, "%s\n", drvdata->rgb_enabled ? "true" : "false"); +} +static DEVICE_ATTR_RW(enabled); + +static ssize_t enabled_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "true false\n"); +} +static DEVICE_ATTR_RO(enabled_index); + +static ssize_t speed_store(struct device *dev, struct device_attribute *at= tr, + const char *buf, size_t count) +{ + struct led_classdev *led_cdev =3D dev_get_drvdata(dev); + struct led_classdev_mc *led_mc =3D container_of(led_cdev, struct led_clas= sdev_mc, led_cdev); + struct claw_drvdata *drvdata =3D container_of(led_mc, struct claw_drvdata= , led_mc); + unsigned int val, speed; + int ret; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->rgb_registered)) + return -ENODEV; + } + + ret =3D kstrtouint(buf, 10, &val); + if (ret) + return ret; + + if (val > 20) + return -EINVAL; + + /* 0 is fastest, invert value for intuitive userspace speed */ + speed =3D 20 - val; + + drvdata->rgb_speed =3D speed; + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); + + return count; +} + +static ssize_t speed_show(struct device *dev, struct device_attribute *att= r, + char *buf) +{ + struct led_classdev *led_cdev =3D dev_get_drvdata(dev); + struct led_classdev_mc *led_mc =3D container_of(led_cdev, struct led_clas= sdev_mc, led_cdev); + struct claw_drvdata *drvdata =3D container_of(led_mc, struct claw_drvdata= , led_mc); + u8 speed =3D 20 - drvdata->rgb_speed; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->rgb_registered)) + return -ENODEV; + } + + return sysfs_emit(buf, "%u\n", speed); +} +static DEVICE_ATTR_RW(speed); + +static ssize_t speed_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "0-20\n"); +} +static DEVICE_ATTR_RO(speed_range); + +static void claw_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness _brightness) +{ + struct led_classdev_mc *led_mc =3D container_of(led_cdev, struct led_clas= sdev_mc, led_cdev); + struct claw_drvdata *drvdata =3D container_of(led_mc, struct claw_drvdata= , led_mc); + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->rgb_registered)) + return; + } + + mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50)); +} + +static struct attribute *claw_rgb_attrs[] =3D { + &dev_attr_effect.attr, + &dev_attr_effect_index.attr, + &dev_attr_enabled.attr, + &dev_attr_enabled_index.attr, + &dev_attr_speed.attr, + &dev_attr_speed_range.attr, + NULL, +}; + +static const struct attribute_group claw_rgb_attr_group =3D { + .attrs =3D claw_rgb_attrs, +}; + +static struct mc_subled claw_rgb_subled_info[] =3D { + { + .color_index =3D LED_COLOR_ID_RED, + .channel =3D 0x1, + }, + { + .color_index =3D LED_COLOR_ID_GREEN, + .channel =3D 0x2, + }, + { + .color_index =3D LED_COLOR_ID_BLUE, + .channel =3D 0x3, + }, +}; + static void cfg_setup_fn(struct work_struct *work) { struct delayed_work *dwork =3D container_of(work, struct delayed_work, wo= rk); struct claw_drvdata *drvdata =3D container_of(dwork, struct claw_drvdata,= cfg_setup); + bool gamepad_ready =3D false, rgb_ready =3D false; int ret; =20 ret =3D claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEP= AD_MODE, NULL, 0, 25); if (ret) { dev_err(&drvdata->hdev->dev, - "Failed to setup device, can't read gamepad mode: %d\n", ret); - return; + "Failed to read gamepad mode: %d\n", ret); + goto prep_rgb; } + gamepad_ready =3D true; =20 - /* Add sysfs attributes after we get the device state */ - ret =3D devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_gro= up); +prep_rgb: + ret =3D claw_read_rgb_config(drvdata->hdev); if (ret) { dev_err(&drvdata->hdev->dev, - "Failed to setup device, can't create gamepad attrs: %d\n", ret); - return; + "Failed to read RGB config: %d\n", ret); + goto try_gamepad; + } + rgb_ready =3D true; + + /* Add sysfs attributes after we get the device state */ +try_gamepad: + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from below */ + if (!smp_load_acquire(&drvdata->gamepad_registered) && gamepad_ready) { + ret =3D devm_device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_g= roup); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to create gamepad attrs: %d\n", ret); + goto try_rgb; + } + + /* Pairs with smp_load_acquire in attribute show/store functions */ + smp_store_release(&drvdata->gamepad_registered, true); + } } - scoped_guard(spinlock_irqsave, &drvdata->registration_lock) - /* Pairs with smp_load_acquire in attribute show/store functions */ - smp_store_release(&drvdata->gamepad_registered, true); =20 - kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE); +try_rgb: + /* Add and enable RGB interface once we have the device state */ + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from below */ + if (!smp_load_acquire(&drvdata->rgb_registered) && rgb_ready) { + ret =3D devm_led_classdev_multicolor_register(&drvdata->hdev->dev, + &drvdata->led_mc); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to create led device: %d\n", ret); + goto update_kobjects; + } + + ret =3D device_add_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_g= roup); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to create RGB attrs: %d\n", ret); + led_classdev_unregister(&drvdata->led_mc.led_cdev); + goto update_kobjects; + } + + /* Pairs with smp_load_acquire in attribute show/store functions */ + smp_store_release(&drvdata->rgb_registered, true); + } + } + +update_kobjects: + guard(spinlock_irqsave)(&drvdata->registration_lock); + /* Pairs with smp_store_release from above */ + if (smp_load_acquire(&drvdata->gamepad_registered)) + kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE); + /* Pairs with smp_store_release from above */ + if (smp_load_acquire(&drvdata->rgb_registered)) + kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE); } =20 static void cfg_resume_fn(struct work_struct *work) @@ -916,8 +1518,10 @@ static void cfg_resume_fn(struct work_struct *work) struct claw_drvdata *drvdata =3D container_of(dwork, struct claw_drvdata,= cfg_resume); =20 guard(spinlock_irqsave)(&drvdata->registration_lock); - /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ - if (!smp_load_acquire(&drvdata->gamepad_registered)) + /* Pairs with smp_store_release from cfg_setup_fn in system_wq contex= t */ + if (!smp_load_acquire(&drvdata->gamepad_registered) || + /* Pairs with smp_store_release from cfg_setup_fn in system_wq contex= t */ + !smp_load_acquire(&drvdata->rgb_registered)) schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500)); } =20 @@ -928,18 +1532,24 @@ static void claw_features_supported(struct claw_drvd= ata *drvdata) =20 if (major =3D=3D 0x01) { drvdata->bmap_support =3D true; - if (minor >=3D 0x66) + if (minor >=3D 0x66) { drvdata->bmap_addr =3D button_mapping_addr_new; - else + drvdata->rgb_addr =3D rgb_addr_new; + } else { drvdata->bmap_addr =3D button_mapping_addr_old; + drvdata->rgb_addr =3D rgb_addr_old; + } return; } =20 if ((major =3D=3D 0x02 && minor >=3D 0x17) || major >=3D 0x03) { drvdata->bmap_support =3D true; drvdata->bmap_addr =3D button_mapping_addr_new; + drvdata->rgb_addr =3D rgb_addr_new; return; } + + drvdata->rgb_addr =3D rgb_addr_old; } =20 static int claw_probe(struct hid_device *hdev, u8 ep) @@ -954,6 +1564,7 @@ static int claw_probe(struct hid_device *hdev, u8 ep) return -ENOMEM; =20 drvdata->gamepad_mode =3D CLAW_GAMEPAD_MODE_XINPUT; + drvdata->rgb_enabled =3D true; drvdata->hdev =3D hdev; drvdata->ep =3D ep; =20 @@ -964,6 +1575,17 @@ static int claw_probe(struct hid_device *hdev, u8 ep) if (!drvdata->bmap_support) dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to = enable.\n"); =20 + drvdata->led_mc.led_cdev.name =3D "msi_claw:rgb:joystick_rings"; + drvdata->led_mc.led_cdev.brightness =3D 0x50; + drvdata->led_mc.led_cdev.max_brightness =3D 0x64; + drvdata->led_mc.led_cdev.color =3D LED_COLOR_ID_RGB; + drvdata->led_mc.led_cdev.brightness_set =3D claw_led_brightness_set; + drvdata->led_mc.num_colors =3D 3; + drvdata->led_mc.subled_info =3D devm_kmemdup(&hdev->dev, claw_rgb_subled_= info, + sizeof(claw_rgb_subled_info), GFP_KERNEL); + if (!drvdata->led_mc.subled_info) + return -ENOMEM; + mutex_init(&drvdata->cfg_mutex); mutex_init(&drvdata->profile_mutex); mutex_init(&drvdata->rom_mutex); @@ -971,9 +1593,11 @@ static int claw_probe(struct hid_device *hdev, u8 ep) spin_lock_init(&drvdata->cmd_lock); spin_lock_init(&drvdata->mode_lock); spin_lock_init(&drvdata->profile_lock); + spin_lock_init(&drvdata->frame_lock); init_completion(&drvdata->send_cmd_complete); INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn); INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn); + INIT_DELAYED_WORK(&drvdata->rgb_queue, &claw_rgb_queue_fn); =20 /* For control interface: open the HID transport for sending commands. */ ret =3D hid_hw_open(hdev); @@ -1029,6 +1653,7 @@ static int msi_probe(struct hid_device *hdev, const s= truct hid_device_id *id) static void claw_remove(struct hid_device *hdev) { struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + bool rgb_registered; =20 if (!drvdata) return; @@ -1036,9 +1661,18 @@ static void claw_remove(struct hid_device *hdev) cancel_delayed_work_sync(&drvdata->cfg_resume); cancel_delayed_work_sync(&drvdata->cfg_setup); =20 - guard(spinlock_irqsave)(&drvdata->registration_lock); - /* Pairs with smp_load_acquire in attribute show/store functions */ - smp_store_release(&drvdata->gamepad_registered, false); + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_load_acquire in attribute show/store functions */ + smp_store_release(&drvdata->gamepad_registered, false); + /* Pairs with smp_load_acquire in attribute show/store functions */ + rgb_registered =3D smp_load_acquire(&drvdata->rgb_registered); + /* Pairs with smp_load_acquire in attribute show/store functions */ + smp_store_release(&drvdata->rgb_registered, false); + } + + cancel_delayed_work_sync(&drvdata->rgb_queue); + if (rgb_registered) + device_remove_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group); =20 hid_hw_close(hdev); } --=20 2.53.0 From nobody Sun May 24 20:33:27 2026 Received: from mail-dy1-f180.google.com (mail-dy1-f180.google.com [74.125.82.180]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id CB3EE2D94B5 for ; Fri, 22 May 2026 01:55:25 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.180 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779414931; cv=none; b=PMpnuoUaWNz7EAJLkI8lGAuVIR5H8VixKzoDXXP4uc+UYxJcSN9pB0LMruQIk8wd/aDgANbRAmlOiXBUYrDAnHromZMTm+3llDAIdDljxVRNR0ZV1/abe5PvQOVJFCpP+hNA8nEGPqBPayiJf/+IY6vEoetTBUAsjwi7RucLbE8= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1779414931; c=relaxed/simple; bh=rKZdWBzaElVeitVFPAhB8HsQPihPR3S3ba+frqtOXmw=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=d7uWpS/lV+so0OjuJsog9EA2yZhKGUZN6L4l3ph+QSj/Bp1wOXBalSKX8qhMRabWpclXhhDFlu/x/Th+bKVKftdufpHvCkH1oD7j+DGIxDDXhAey49MX0ammLn1u0v9TNrhUJziUriiSo9uTEpYZdMfEbYkeYqS/4u3MQU/KGJ0= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=NAhobQgp; arc=none smtp.client-ip=74.125.82.180 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="NAhobQgp" Received: by mail-dy1-f180.google.com with SMTP id 5a478bee46e88-3042dffd80bso2433620eec.0 for ; Thu, 21 May 2026 18:55:25 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1779414925; x=1780019725; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=33/HARrxzqaqtjCLOeSlyGSQZ0D0WzG3xs/wh21rVlo=; b=NAhobQgptpQu14yBmyBF4PcuCcfX0NrzOR5+NBdNTFLuVYi1YEjo/QmIylCaCRqMv4 LKkWbrPwh5sPIPAKT2Wr7EgzMPCGCnNacHAPUry+z/DfjpHGR4sCu6UpK1uxsBaYAQ6T 6dSapVTDut0HUx8MNcx/E+xRxjwpY1aaBIB9NOMvXNOluXSxn6zdomotn/HRiL8sQq3r JwOblebfbz6gcNcfHNGocIr4MWqw2uvuucQ7+zgCYWpKMsaMrhPOmbXpPBbZICoiIJM9 7pIxUwihLH7HC876XGRxzZYUXeW7ts4a6E/aaVa+Jqk+ct2qTq8pp+lsxyU0s8HQ6ZLs hozQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1779414925; x=1780019725; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=33/HARrxzqaqtjCLOeSlyGSQZ0D0WzG3xs/wh21rVlo=; b=YIgJTd5JGo1X5iXF9Bvmh54L43pW7VDshyJvoAbwyS+sIruHlJB36ajikMJ5ehCou6 0Ep575a+duA0rgr9N6zKHFpAC200X/MaGhTm9I28cvAw+UwB9Zbaw6udS7QWW8tU9F/v WTe0OwjkTqKeRlmScpmjWAMCOrG3+QSuTyBe0cFrZ6vwkCbg5Nq/bFeagWBojUES/dEQ R/YyAxtPyx5FtcqvUg8yST0qmSKOq3gX360xa9rOkuCzieB0CH0EjD8MHDPqENEb/xxo kzgx/SYCJGDoI/D0RQPtajhbPItNXFat9QHS8gXonrGe78W5ecK/tsZyZloZMBbNQS0w opww== X-Forwarded-Encrypted: i=1; AFNElJ/8mfQWQStJQE8WBlWwekcCvWSUauD/o66UG+e5scK52cZSisUIWplWUGnQM11HjARziFruV054pxnMM98=@vger.kernel.org X-Gm-Message-State: AOJu0YxVj6urs7dCooulNI2vreCeEejbv3+py3tW68o+oikVNyX4PLeF 4l/hF4WY10LtRASwpHIeBnGqN5nJyIVAI3wtYB/Ns09lUowFL4EXiNq5 X-Gm-Gg: Acq92OFjMr2Qp1W7qrqXHURgXfAHpVJTMRjrEy5UY2/c1nnUlbnvLQHqRZqnA/w0qQS hHhR8PJsR7BHGh1BkByFPVFNFrFKxNr2/3F/6Gr/3T5bdEzKVTmDZZelEmwPfPYD6pn8a5qlFoe 6zfVE7QSk5iQP9wjVbXC04qwB3t21HlI1vOYfQPx0lKU7/HiYJhGM+789BO3GvdeqW3GBN0Dr0e PxPjh1KHDblutIVEwJ1bZN0Vq2MNdXCUBcPRXC/IBGDlWfJIYFfiDAibk5qo6pOtUsDIh/8qskJ yAdcLg1LLFauZoGIYHt3jqRuIFwLBEXgcuq/kWslShvo/psuvjSB6mdQ/mfBpF4VerVAZWMbpZZ SEAiOHAmPQ1H17xkVm7GbGF4GCtQkFU33rwJbC5goQ/qndkEiXv3cY4MlU1l2qLYjFgrEVQ7nFD eKxLGPoWr38K7/Fa8330ZpE0jAJ0MzVEZUp7elFKT4GeT31g4S9HSQQ6evPxa+D/hiyOtfjOvbk 9Uj X-Received: by 2002:a05:7300:f196:b0:2c4:dd55:ffc1 with SMTP id 5a478bee46e88-3044904e2cemr912977eec.2.1779414924624; Thu, 21 May 2026 18:55:24 -0700 (PDT) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id 5a478bee46e88-3044b9123f7sm1027744eec.19.2026.05.21.18.55.24 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 21 May 2026 18:55:24 -0700 (PDT) From: "Derek J. Clark" To: Jiri Kosina , Benjamin Tissoires Cc: "Pierre-Loup A . Griffais" , Denis Benato , Zhouwang Huang , "Derek J . Clark" , linux-input@vger.kernel.org, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org Subject: [PATCH v8 4/4] HID: hid-msi: Add Rumble Intensity Attributes Date: Fri, 22 May 2026 01:55:18 +0000 Message-ID: <20260522015518.1111290-5-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260522015518.1111290-1-derekjohn.clark@gmail.com> References: <20260522015518.1111290-1-derekjohn.clark@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Adds intensity adjustment for the left and right rumble motors. Claude was used during the reverse-engineering data gathering for this feature done by Zhouwang Huang. As the code had already been affected, I used Claude to create the initial framing for the feature, then did manual cleanup of the _show and _store functions afterwards to fix bugs and keep the coding style consistent. Claude was also used as an initial reviewer of this patch. Assisted-by: Claude:claude-sonnet-4-6 Co-developed-by: Zhouwang Huang Signed-off-by: Zhouwang Huang Signed-off-by: Derek J. Clark --- v7: - Match on write address for rumble reports to prevent late ACK from causing synchronization errors. - Use spinlock for read/write profile_pending. - Use smp_[store_release|load_acquire] pattern for checking gamepad_registered to avoid possible races during teardown. - Use struct for rumble reports. v6: - Make all timeouts 25ms to ensure at least 2 jiffies in a 100Hz config. - Add spinlock_irqsave for read/write access on rumble_intensity variables. - Gate all attribute show/store functions with gamepad_registered. v5: - Remove mkey related changes. v2: - Use pending_profile and sync to rom mutexes. --- drivers/hid/hid-msi.c | 200 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index 0b875603dbb32..add790adfa462 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -79,6 +79,8 @@ enum claw_profile_ack_pending { CLAW_M1_PENDING, CLAW_M2_PENDING, CLAW_RGB_PENDING, + CLAW_RUMBLE_LEFT_PENDING, + CLAW_RUMBLE_RIGHT_PENDING, }; =20 enum claw_key_index { @@ -266,6 +268,11 @@ static const u16 button_mapping_addr_new[] =3D { static const u16 rgb_addr_old =3D 0x01fa; static const u16 rgb_addr_new =3D 0x024a; =20 +static const u16 rumble_addr[] =3D { + 0x0022, /* left */ + 0x0023, /* right */ +}; + struct claw_command_report { u8 report_id; u8 padding[2]; @@ -308,6 +315,12 @@ struct claw_rgb_report { struct rgb_frame zone_data; } __packed; =20 +struct claw_rumble_report { + struct claw_profile_report; + u8 padding; + u8 intensity; +} __packed; + struct claw_drvdata { /* MCU General Variables */ enum claw_profile_ack_pending profile_pending; @@ -332,9 +345,13 @@ struct claw_drvdata { enum claw_gamepad_mode_index gamepad_mode; u8 m1_codes[CLAW_KEYS_MAX]; u8 m2_codes[CLAW_KEYS_MAX]; + u8 rumble_intensity_right; + u8 rumble_intensity_left; bool gamepad_registered; + spinlock_t rumble_lock; /* lock for rumble_intensity read/write */ spinlock_t mode_lock; /* Lock for mode data read/write */ const u16 *bmap_addr; + bool rumble_support; bool bmap_support; =20 /* RGB Variables */ @@ -382,6 +399,7 @@ static int claw_gamepad_mode_event(struct claw_drvdata = *drvdata, static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_co= mmand_report *cmd_rep) { enum claw_profile_ack_pending profile; + struct claw_rumble_report *rumble; struct claw_mkey_report *mkeys; struct claw_rgb_report *frame; u16 rgb_addr, read_addr; @@ -431,6 +449,20 @@ static int claw_profile_event(struct claw_drvdata *drv= data, struct claw_command_ } =20 break; + case CLAW_RUMBLE_LEFT_PENDING: + rumble =3D (struct claw_rumble_report *)cmd_rep->data; + if (be16_to_cpu(rumble->read_addr) !=3D rumble_addr[0]) + return -EINVAL; + scoped_guard(spinlock, &drvdata->rumble_lock) + drvdata->rumble_intensity_left =3D rumble->intensity; + break; + case CLAW_RUMBLE_RIGHT_PENDING: + rumble =3D (struct claw_rumble_report *)cmd_rep->data; + if (be16_to_cpu(rumble->read_addr) !=3D rumble_addr[1]) + return -EINVAL; + scoped_guard(spinlock, &drvdata->rumble_lock) + drvdata->rumble_intensity_right =3D rumble->intensity; + break; default: dev_dbg(&drvdata->hdev->dev, "Got profile event without changes pending from command: %x\n", @@ -930,6 +962,162 @@ static ssize_t button_mapping_options_show(struct dev= ice *dev, } static DEVICE_ATTR_RO(button_mapping_options); =20 +static ssize_t rumble_intensity_left_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct claw_rumble_report report =3D { {0x01, cpu_to_be16(rumble_addr[0])= }, 0x01 }; + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + u8 val; + int ret; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + ret =3D kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val > 100) + return -EINVAL; + + report.intensity =3D val; + + guard(mutex)(&drvdata->rom_mutex); + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + (u8 *)&report, sizeof(report), 25); + if (ret) + return ret; + + /* MCU will not send ACK until the USB transaction completes. ACK is sent + * immediately after and will hit the stale state machine, before the next + * command re-arms the state machine. Timeout 0 ensures no deadlock waiti= ng + * for ACK that ill never come. + */ + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, = 0, 0); + if (ret) + return ret; + + return count; +} + +static ssize_t rumble_intensity_left_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct claw_rumble_report report =3D { {0x01, cpu_to_be16(rumble_addr[0])= }, 0x01 }; + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + int ret; + u8 val; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + guard(mutex)(&drvdata->profile_mutex); + scoped_guard(spinlock_irqsave, &drvdata->profile_lock) + drvdata->profile_pending =3D CLAW_RUMBLE_LEFT_PENDING; + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, + (u8 *)&report, sizeof(report), 25); + if (ret) + return ret; + + scoped_guard(spinlock_irqsave, &drvdata->rumble_lock) + val =3D drvdata->rumble_intensity_left; + + return sysfs_emit(buf, "%u\n", val); +} +static DEVICE_ATTR_RW(rumble_intensity_left); + +static ssize_t rumble_intensity_right_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct claw_rumble_report report =3D { {0x01, cpu_to_be16(rumble_addr[1])= }, 0x01 }; + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + u8 val; + int ret; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + ret =3D kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val > 100) + return -EINVAL; + + report.intensity =3D val; + + guard(mutex)(&drvdata->rom_mutex); + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + (u8 *)&report, sizeof(report), 25); + if (ret) + return ret; + + /* MCU will not send ACK until the USB transaction completes. ACK is sent + * immediately after and will hit the stale state machine, before the next + * command re-arms the state machine. Timeout 0 ensures no deadlock waiti= ng + * for ACK that ill never come. + */ + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, = 0, 0); + if (ret) + return ret; + + return count; +} + +static ssize_t rumble_intensity_right_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct claw_rumble_report report =3D { {0x01, cpu_to_be16(rumble_addr[1])= }, 0x01 }; + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + int ret; + u8 val; + + scoped_guard(spinlock_irqsave, &drvdata->registration_lock) { + /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */ + if (!smp_load_acquire(&drvdata->gamepad_registered)) + return -ENODEV; + } + + guard(mutex)(&drvdata->profile_mutex); + scoped_guard(spinlock_irqsave, &drvdata->profile_lock) + drvdata->profile_pending =3D CLAW_RUMBLE_RIGHT_PENDING; + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, + (u8 *)&report, sizeof(report), 25); + if (ret) + return ret; + + scoped_guard(spinlock_irqsave, &drvdata->rumble_lock) + val =3D drvdata->rumble_intensity_right; + + return sysfs_emit(buf, "%u\n", val); +} +static DEVICE_ATTR_RW(rumble_intensity_right); + +static ssize_t rumble_intensity_range_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "0-100\n"); +} +static DEVICE_ATTR_RO(rumble_intensity_range); + static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct a= ttribute *attr, int n) { @@ -950,6 +1138,12 @@ static umode_t claw_gamepad_attr_is_visible(struct ko= bject *kobj, struct attribu attr =3D=3D &dev_attr_reset.attr) return attr->mode; =20 + /* Hide rumble attrs if not supported */ + if (attr =3D=3D &dev_attr_rumble_intensity_left.attr || + attr =3D=3D &dev_attr_rumble_intensity_right.attr || + attr =3D=3D &dev_attr_rumble_intensity_range.attr) + return drvdata->rumble_support ? attr->mode : 0; + /* Hide button mapping attrs if it isn't supported */ return drvdata->bmap_support ? attr->mode : 0; } @@ -963,6 +1157,9 @@ static struct attribute *claw_gamepad_attrs[] =3D { &dev_attr_mkeys_function.attr, &dev_attr_mkeys_function_index.attr, &dev_attr_reset.attr, + &dev_attr_rumble_intensity_left.attr, + &dev_attr_rumble_intensity_right.attr, + &dev_attr_rumble_intensity_range.attr, NULL, }; =20 @@ -1534,6 +1731,7 @@ static void claw_features_supported(struct claw_drvda= ta *drvdata) drvdata->bmap_support =3D true; if (minor >=3D 0x66) { drvdata->bmap_addr =3D button_mapping_addr_new; + drvdata->rumble_support =3D true; drvdata->rgb_addr =3D rgb_addr_new; } else { drvdata->bmap_addr =3D button_mapping_addr_old; @@ -1545,6 +1743,7 @@ static void claw_features_supported(struct claw_drvda= ta *drvdata) if ((major =3D=3D 0x02 && minor >=3D 0x17) || major >=3D 0x03) { drvdata->bmap_support =3D true; drvdata->bmap_addr =3D button_mapping_addr_new; + drvdata->rumble_support =3D true; drvdata->rgb_addr =3D rgb_addr_new; return; } @@ -1594,6 +1793,7 @@ static int claw_probe(struct hid_device *hdev, u8 ep) spin_lock_init(&drvdata->mode_lock); spin_lock_init(&drvdata->profile_lock); spin_lock_init(&drvdata->frame_lock); + spin_lock_init(&drvdata->rumble_lock); init_completion(&drvdata->send_cmd_complete); INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn); INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn); --=20 2.53.0