From nobody Fri Jun 12 15:57:03 2026 Received: from mail-dl1-f46.google.com (mail-dl1-f46.google.com [74.125.82.46]) (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 F22D33CB919 for ; Wed, 13 May 2026 23:14:49 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.46 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778714092; cv=none; b=PevbBXBtsqJnvZdU7YHYq3bp6hHs8SlTCkgZOnX9xcNewzjgLWx19/EQANK0WJp/mjWJIguRSfJmN3y25aUV0jcq9pFgNxQfjPtvGJd8QkeN6eme/JVFj4BZPnk8ZDrHHeGrlQRFPEdePsTjLufb56lZDANVYSSSdZvJ7BDGeyY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778714092; c=relaxed/simple; bh=ea/SB3wzDURIN789YiALlpSNBgD551Z9zaSq7hTGe6k=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=FDOLXF22fUA1/mJwKsFGv2QNvVLKz2sVqr74jKt908+hPL/YELQGomf11d0bNeul9y8HCDQG8YGbIzvHiZi28zfRxZyifuWBH9ssEYAj2ANw3Q2FDG899SgAlRqENeNJOMIYOIFxl9JU5P4HQEx+0EFD2Yhl9Ie34+0yv1NUjT4= 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=rjGqTWwW; arc=none smtp.client-ip=74.125.82.46 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="rjGqTWwW" Received: by mail-dl1-f46.google.com with SMTP id a92af1059eb24-132d1b2519eso1257358c88.0 for ; Wed, 13 May 2026 16:14:49 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778714089; x=1779318889; 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=eDeN30QFEd94OqcrXw2MxtgeIpmGFkC9WfzRLXh526M=; b=rjGqTWwWiU4RHWCqlqMIG9V+1iA7wdoBGjO1WVU2rCMSKWkXUABQ4m31Gmn9T31OCL dwYTEuKoyycdVgQP8DhIpCEV4G+gQZoJ72JpwHG/9CtpN1V2ZPdMmKvOiV4frQhVCN9b AlTflMQ4bUFAtisHUBV/S1DDjCH03K1eWb/bhQ0iKb4eI6IeWZOnzYNVw0Csl8uDdzmz Lw6P2eeDcfoVc9JtdkTGBDM6R0oiM2yPtMvAf1/68DPEGT9nszmcYdpVO+2OefT67awL h+84c7Y925bGqKFETkHX1Qjs2nZtbzh/Z1cGjMGJprfrTR/ZHkiPtcpF06mPyXImvLrl 7ATg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778714089; x=1779318889; 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=eDeN30QFEd94OqcrXw2MxtgeIpmGFkC9WfzRLXh526M=; b=CQtE7CafJyLfT31jQO3C3G4fTn/wq3AZIxMN4/cbj852+LtV+Fnit/Wlx8VErHt9ln /kRfgxfNKPtTYc118qm93Z0t4pweZsfVj0VyimJ0KFeDthL0en0Ym56Rpn9oqV+ZpLAB LoNOOW4VqG0agHtvu1vABwnFnoRQzHLeIZ3nO3hm9rIz18cqqZYK1DYqoIepp9K0KjB9 2kUiIcmSo4uolkqCxDvYBSRt6nXx/0cs36NfQnXANetVBZ1E6g4oAIIK59UoDVb1ycGe BG8tlvxPTBRXsAjaSu5xEt7i8fnPcXeCFVAFgGq4bdNzG0BEGLcw+P0k0J6diVNJBXmt atFg== X-Forwarded-Encrypted: i=1; AFNElJ/H4YpgDVPre38Zp4I9dgl3oatTX/FsYSy6+1xPvaEN57MCm+RHPcU9GLVdlwMTOqOrK7Ca30w6kxcErZI=@vger.kernel.org X-Gm-Message-State: AOJu0YydLV1nhXr1qeNgwsHL5WnXWVMtU7LFHIzSSf358jAQ3r3lsJmt y/H13ETMJOyEJmD+hRvTWtPBAA8ko3rP42Nrc5NeUM8wbRWT42b7gPsw X-Gm-Gg: Acq92OGmVynNtC5Mt49Ip+XrH8hfU1JybdbCOs1yYe6KvLyUQEHFVFQuCyd/hludWoW wAhjh27CvO5gTGIS3M1o+wkBLTlgtjWGD59OmIwHnShOHgUeQjwXSevNgif4k3yOLPy0Ui31DVu RwK3ryYoQheguML7suRj7wedz6Fx5svY7A2vPqwF0lezRdaK+fsltK8wgx2GPKkrxlSn/Dmdcn2 EdzVZ4KXRZSbdL8O/Ju1wjAoqRpsrwQ5ofR36dyZ99OVL4FeOmkZHeXiCquV+abyGdK3CmPQsgE xxHCjT+fOi+HrLmOCS6X2OZhFXzyxKjgJNGKKPQtpgqrCeVKveOYnKbNE9AcD6rZ1lBoD2ZxvSq Js+C4AUk8BBpZTJWmP0jItSIVucyWvXEwcc4WI8VYspKcgw2MAUj1H20UA0BGRbej6KZpdxEyI7 7pDki5fKLE29Dt39v2HcjVNlPS1zScaDnyrqyj9dmOOD5jL/Ld9sfAkf0U4h/r0x+KMmtk1kl3Q usmLynpU5wF+To= X-Received: by 2002:a05:7022:fb09:b0:11b:bf3f:5240 with SMTP id a92af1059eb24-1349a6f83bdmr2926975c88.9.1778714088787; Wed, 13 May 2026 16:14:48 -0700 (PDT) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id a92af1059eb24-134cc2352f2sm1379258c88.10.2026.05.13.16.14.48 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 13 May 2026 16:14:48 -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 v2 1/4] HID: hid-msi: Add MSI Claw configuration driver Date: Wed, 13 May 2026 23:14:42 +0000 Message-ID: <20260513231445.3213501-2-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260513231445.3213501-1-derekjohn.clark@gmail.com> References: <20260513231445.3213501-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 --- 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. - Remove deadlock in cfg_setup, return on errors. --- MAINTAINERS | 6 + drivers/hid/Kconfig | 12 + drivers/hid/Makefile | 1 + drivers/hid/hid-ids.h | 6 + drivers/hid/hid-msi.c | 582 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 607 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..af146691bd481 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -492,6 +492,18 @@ 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 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..6d0d34806931f 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -1047,7 +1047,13 @@ #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..8915942af15e6 --- /dev/null +++ b/drivers/hid/hid-msi.c @@ -0,0 +1,582 @@ +// 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 "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_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_COMBO, + CLAW_MKEY_FUNCTION_DISABLED, +}; + +static const char * const claw_mkeys_function_text[] =3D { + [CLAW_MKEY_FUNCTION_MACRO] =3D "macro", + [CLAW_MKEY_FUNCTION_COMBO] =3D "combination", + [CLAW_MKEY_FUNCTION_DISABLED] =3D "disabled", +}; + +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; + struct hid_device *hdev; + struct mutex cfg_mutex; /* mutex for synchronous data */ + u8 ep; + + /* Gamepad Variables */ + enum claw_mkeys_function_index mkeys_function; + enum claw_gamepad_mode_index gamepad_mode; +}; + +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; + + 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); + + switch (cmd_rep->cmd) { + case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK: + ret =3D claw_gamepad_mode_event(drvdata, cmd_rep); + break; + case CLAW_COMMAND_TYPE_ACK: + break; + default: + dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd); + return 0; + } + + complete(&drvdata->send_cmd_complete); + + 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); + + /* Don't hold a mutex when timeout=3D0, those commands cause USB disconne= ct */ + if (timeout) { + guard(mutex)(&drvdata->cfg_mutex); + reinit_completion(&drvdata->send_cmd_complete); + } + + 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) + return ret; + + ret =3D ret =3D=3D CLAW_PACKET_SIZE ? 0 : -EIO; + if (ret) + return ret; + + 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); + if (ret >=3D 0) /* preserve errors */ + ret =3D ret =3D=3D 0 ? -EBUSY : 0; /* timeout occurred : time remained = */ + } + + 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); + u8 data[2] =3D { 0x00, drvdata->mkeys_function }; + int i, ret =3D -EINVAL; + + 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; + + 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; + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, = NULL, 0, 8); + if (ret) + return ret; + + 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]); + } + + 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); + u8 data[2] =3D { drvdata->gamepad_mode, 0x00 }; + int i, ret =3D -EINVAL; + + 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; + + 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; + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, = NULL, 0, 8); + if (ret) + return ret; + + 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]); + + 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); + bool val; + int ret; + + 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 < 0) + 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, 8); + 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; + } + + 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); + u8 data[2] =3D { drvdata->gamepad_mode, drvdata->mkeys_function }; + int ret; + + ret =3D claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SWITCH_MOD= E, data, + ARRAY_SIZE(data), 0); + if (ret) + dev_err(&drvdata->hdev->dev, "Failed to set gamepad mode settings: %d\n"= , ret); +} + +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; + + hid_set_drvdata(hdev, drvdata); + drvdata->hdev =3D hdev; + drvdata->ep =3D ep; + + mutex_init(&drvdata->cfg_mutex); + init_completion(&drvdata->send_cmd_complete); + + /* For control interface: open the HID transport for sending commands. */ + ret =3D hid_hw_open(hdev); + if (ret) + return ret; + + INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn); + INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn); + 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) { + hid_hw_stop(hdev); + return; + } + + cancel_delayed_work_sync(&drvdata->cfg_setup); + cancel_delayed_work_sync(&drvdata->cfg_resume); + hid_hw_close(hdev); +} + +static void msi_remove(struct hid_device *hdev) +{ + int ret; + u8 ep; + + 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); + + /* 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; + + 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 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, +}; +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 Fri Jun 12 15:57:03 2026 Received: from mail-dl1-f49.google.com (mail-dl1-f49.google.com [74.125.82.49]) (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 7FA9B3CBE8F for ; Wed, 13 May 2026 23:14:50 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.49 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778714092; cv=none; b=Rgwr0MRJ63ZxU6dZFgcKrgyygjYrRTVfuRlSnGRurSso0BYBRzsJuM3eDhICP7fO0+zV7alC+TCk1s3Y5qrVmlqKSSr+CmQfQH2rWmWYCjsdnNs7Ml8CHP+Npf3t5BZ9KixMHgO3DMf26IVG8CJrChBxRL8gteMhgqnAJ+ka72s= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778714092; c=relaxed/simple; bh=TyBMfaPN26klz75TDAjTA7ODXmVJGveHB7RAWFFC9zI=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=hi1I08nUTLYI9LwCi/WsuvhjPE0zMy+6E7Gvbh05Y5vmvUJ4sMUR4y76lUDZ1dMBHFbyFIRhWE1+1+20hH6K1i2AMEltwjPEGPQbhpjBCbDnqgALrgV07/xf8c/dB4g+pTPPcWt/yK8m/HprQi5u8qsPVr8KPx1oM2kuEV0OIIM= 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=fwUmYapd; arc=none smtp.client-ip=74.125.82.49 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="fwUmYapd" Received: by mail-dl1-f49.google.com with SMTP id a92af1059eb24-12c88e5f4aeso4276832c88.0 for ; Wed, 13 May 2026 16:14:50 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778714090; x=1779318890; 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=JaHPjWoXHdxCx0kf8Nc4kDCbwDBvgOTNTWDSn0zWbc8=; b=fwUmYapdr8RDhdptBufr6k9lR9W3fZ+uRVSCH2eQmO3Cp6ZABqrc79pkmqIoR2mvBR NPdas8U1JVBLzSn8/ze4lnYoAeLLXij3hMfktMWAKaCVncHf6VN6wPBpJ5EevnOexZON iBUPEJQqaYqwXoS78TC9DGY/YEziX62IyPlnqzPc6zx7OsdZdOocCYgqOvZnNq7Y+20w EDypA+zhD71EE3Di5xcCC7FcWyf8KVyxMHk+QA+6U172qL1+QdTQgFGrYhBLSw4b+rpC rVk56rVNasU0iy7yEVdqDQYdHhAy3Vi71TFS1kIcQU38qbsid8Rc5rHcknhJIq/6g2oK pznw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778714090; x=1779318890; 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=JaHPjWoXHdxCx0kf8Nc4kDCbwDBvgOTNTWDSn0zWbc8=; b=kyXsWEBcC39c9vxexoVi5hYZX17D8mbwtIov0qQ+wk6PSPM2Rqktas2tsQW7oj0+ov h9vuYV2uAEF0MrL4sTm+LjBE1eXJxq2q8f9V5F00+EuPjACkBQ1e8vBEjVltJkAS+VKR 7GFKmtnAyN37NA8CsCwyA4+aWtbBdijf6fLMLbWTe7g9xKwkAAn9xYKXpr06uCmt9JM4 a9b6G4r3ZejknpdZv5iGZ4uBgtMVeOrEX8aUf/Ih6bv32PU/eyX4wTVqPDLT2CaXvWhC aoGrAXWkh9GnrXA8UvGKAZr+nsPKHu36AcI1+xxDZXc9bw91oy+IxlL+KFbxSnr45QbH PY9Q== X-Forwarded-Encrypted: i=1; AFNElJ+AS0CNEyIX/RRHEQL+8tQEzDReFJch78dZBHJZxUPmrQvGWKp3DAaVsoAx0rVl5N/oUiwA5XZBxOZFY9E=@vger.kernel.org X-Gm-Message-State: AOJu0YxkKsAqlvK+5x+YhJnbTV2K+45vNhXTRNZIlBXN0oCTIAiJOtIU D9t0mh54qegk5YZGP/8r+CxI9EytUL+v8cZcz/NW2hVpmbgqvQCFmi1gklhQVw== X-Gm-Gg: Acq92OFSvDm0EDecW9Nbda3wwnpX/tMDRZrWdmxiHAXizKnGZEmqyS/t1qocjLwRkmP iFPVMYu4UH4ySEGy/UYxM4AQYPt9ncvTM4pNakpGJMtNroy0YKCVVPs97U6Yzd/qXB3OSC6K7DH k1RORFlshXGN9aAyIih9FkN4T1+qRJb/XCOFQpLqUkQKz2FJuSIYx4Ob9zdO/VKrzbWEMtq3Xf/ tgIVi1Inopt4EZjfoffytKZ5bwp0bCQCEcbf0k+dR59lW9ltFsWZB9PkzoUDuW+p9k5XfUzn9uR kiHf5AqV5f1Bp9qfV/q2HflklyeQHpXKlhRRjTzE7CeO5u3+oDgUFQ/FMMUCcDbMpHgxQfokLiM ObXG/okxxUtoZMc9o+ACGOwGS6pq6asrDFj/VNsZYYyCgYSBip4HA8UgnPOXK67psAg8Fql1Jf+ nzSZwmPWcnI5rbF4OvAUX3Hk7zQ05AmrmTSfcYMRdw4SCZn22IaObD87gVLjTCR9x17wmQ4u4PL eYp X-Received: by 2002:a05:7022:608b:b0:133:52ca:7dcb with SMTP id a92af1059eb24-134c8b337e3mr855553c88.10.1778714089568; Wed, 13 May 2026 16:14:49 -0700 (PDT) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id a92af1059eb24-134cc2352f2sm1379258c88.10.2026.05.13.16.14.49 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 13 May 2026 16:14:49 -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 v2 2/4] HID: hid-msi: Add M-key mapping attributes Date: Wed, 13 May 2026 23:14:43 +0000 Message-ID: <20260513231445.3213501-3-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260513231445.3213501-1-derekjohn.clark@gmail.com> References: <20260513231445.3213501-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 --- 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 | 398 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 397 insertions(+), 1 deletion(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index 8915942af15e6..13ba2747fdb66 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -40,6 +40,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_READ_PROFILE =3D 0x04, CLAW_COMMAND_TYPE_READ_PROFILE_ACK =3D 0x05, @@ -64,6 +66,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_COMBO, @@ -76,6 +89,154 @@ static const char * const claw_mkeys_function_text[] = =3D { [CLAW_MKEY_FUNCTION_DISABLED] =3D "disabled", }; =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_LEFT_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" }, +}; + +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]; @@ -86,16 +247,24 @@ struct claw_command_report { =20 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; + struct mutex profile_mutex; /* mutex for profile_pending calls */ struct hid_device *hdev; struct mutex cfg_mutex; /* mutex for synchronous data */ + struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */ + 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]; + const u16 *bmap_addr; + bool bmap_support; }; =20 static int get_endpoint_address(struct hid_device *hdev) @@ -125,6 +294,31 @@ 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) +{ + u8 *codes; + int i; + + switch (drvdata->profile_pending) { + case CLAW_M1_PENDING: + case CLAW_M2_PENDING: + codes =3D (drvdata->profile_pending =3D=3D CLAW_M1_PENDING) ? + drvdata->m1_codes : drvdata->m2_codes; + /* Extract key codes; replace disabled (0xff) with 0x00, which is (null)= in _show */ + for (i =3D 0; i < CLAW_KEYS_MAX; i++) + codes[i] =3D (cmd_rep->data[6 + i] !=3D 0xff) ? cmd_rep->data[6 + i] : = 0x00; + break; + default: + dev_warn(&drvdata->hdev->dev, + "Got profile event without changes pending from command: %x\n", + cmd_rep->cmd); + return -EINVAL; + } + 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) { @@ -146,6 +340,9 @@ static int claw_raw_event(struct claw_drvdata *drvdata,= struct hid_report *repor case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK: ret =3D claw_gamepad_mode_event(drvdata, cmd_rep); break; + case CLAW_COMMAND_TYPE_READ_PROFILE_ACK: + ret =3D claw_profile_event(drvdata, cmd_rep); + break; case CLAW_COMMAND_TYPE_ACK: break; default: @@ -366,6 +563,161 @@ static ssize_t reset_store(struct device *dev, struct= device_attribute *attr, } static DEVICE_ATTR_WO(reset); =20 +static int button_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 *button_mapping_code_to_name(u8 code) +{ + int i; + + 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_buttons_store(struct device *dev, const char *buf, u8 mkey= _idx) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + u8 data[] =3D { 0x01, (drvdata->bmap_addr[mkey_idx] >> 8) & 0xff, + drvdata->bmap_addr[mkey_idx] & 0xff, 0x07, + 0x04, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff }; + size_t len =3D ARRAY_SIZE(data); + int ret, key_count, i; + char **raw_keys; + + raw_keys =3D argv_split(GFP_KERNEL, buf, &key_count); + if (!raw_keys) + return -ENOMEM; + + guard(mutex)(&drvdata->rom_mutex); /* all err_free paths must be in scope= */ + if (key_count > CLAW_KEYS_MAX) { + ret =3D -EINVAL; + goto err_free; + } + + if (key_count =3D=3D 0) + goto set_buttons; + + for (i =3D 0; i < key_count; i++) { + ret =3D button_mapping_name_to_code(raw_keys[i]); + if (ret < 0) + goto err_free; + + data[6 + i] =3D ret; + } + +set_buttons: + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,= data, len, 8); + if (ret < 0) + goto err_free; + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, = 0, 8); + +err_free: + argv_free(raw_keys); + return ret; +} + +static int claw_buttons_show(struct device *dev, char *buf, enum claw_key_= index m_key) +{ + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + u8 data[] =3D { 0x01, (drvdata->bmap_addr[m_key] >> 8) & 0xff, + drvdata->bmap_addr[m_key] & 0xff, 0x07 }; + size_t len =3D ARRAY_SIZE(data); + int i, ret, count =3D 0; + const char *name; + u8 *codes; + + codes =3D (m_key =3D=3D CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_co= des; + + guard(mutex)(&drvdata->profile_mutex); + 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, data,= len, 8); + if (ret) { + drvdata->profile_pending =3D CLAW_NO_PENDING; + return ret; + } + for (i =3D 0; i < CLAW_KEYS_MAX; i++) { + name =3D button_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_buttons_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_buttons_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_buttons_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_buttons_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); + + 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) { @@ -378,10 +730,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, @@ -432,8 +796,31 @@ static void cfg_resume_fn(struct work_struct *work) dev_err(&drvdata->hdev->dev, "Failed to set gamepad mode settings: %d\n"= , ret); } =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 @@ -446,8 +833,17 @@ static int claw_probe(struct hid_device *hdev, u8 ep) drvdata->ep =3D ep; =20 mutex_init(&drvdata->cfg_mutex); + mutex_init(&drvdata->profile_mutex); + mutex_init(&drvdata->rom_mutex); init_completion(&drvdata->send_cmd_complete); =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"); + /* For control interface: open the HID transport for sending commands. */ ret =3D hid_hw_open(hdev); if (ret) --=20 2.53.0 From nobody Fri Jun 12 15:57:03 2026 Received: from mail-dl1-f50.google.com (mail-dl1-f50.google.com [74.125.82.50]) (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 58F103CC7C5 for ; Wed, 13 May 2026 23:14:51 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.50 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778714095; cv=none; b=AJaZBa9oIReepxrUOR6qkW9kvV/M4LGDs5QfQjKgeUujEDj20P7fF9ONbecbv3/X/Q1Rjh7kNncOjLmJXVP8ybrgywZIp6SXOJYrjRcKh41I3b7bXE2OFMj/xz+31cVMJb8GGPPMMvDRiOEFF3oaZdiUKR52QWDvjWuRot+W4t0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778714095; c=relaxed/simple; bh=yinaxrPGy3bVa4qxPo01++LKoNw3AHddFajkYACqI/A=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=jdSvGDWQqzwyukYKh6DopqgOXDBN74PQetKi52HQe9UAK14+i19xnr9krI8hW3dby6y/9BzCLXJYndd1ptC8c66B0mUOGkPRjQDgDLHlHu7mBPFtEjWWI9yufyNzjmqBw859PhroCXjz8/zJeMHSA2MWCAwmypn6mG3iTHH/9Kc= 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=CyjuWNsG; arc=none smtp.client-ip=74.125.82.50 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="CyjuWNsG" Received: by mail-dl1-f50.google.com with SMTP id a92af1059eb24-1309f4ee97fso9281858c88.1 for ; Wed, 13 May 2026 16:14:51 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778714090; x=1779318890; 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=0X5Pjn8JW9mngmAPViYioU1K3ytQf4QDi00V48Af3UM=; b=CyjuWNsG5vIHF9HqT89GDymWLa/o99x8mAgHpy0lhUU5eDFtxbvQByYpv0Te4YB3ZM z6FLJcM3rCDEQjm7thW4MIPp9NQosJ7AsmbmaDeN7civbmJJxDa4w5VMBJzTCH21p4st AIu1t1MmVgisNlG9RSsAzbiw7wdEwfi5/8eQ97vvy5m81zaAR0a7xvvYwMKJRnl8tqEu /PdDdDj4FTLv1p6CU4Uy3zHMstcqJBUDT8XKDhvS/reXT8oCPRDlvLJalMxSNhIOdYyn 6V0S9NVXJk29pBKw1CcJ6vPwFFq7IGEkRwn9OD9ffl8jP1UMML3lvcBw5lAq7zWF8ORp MYHw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778714090; x=1779318890; 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=0X5Pjn8JW9mngmAPViYioU1K3ytQf4QDi00V48Af3UM=; b=OkjJ5scYT9vOk3iWaT1RGbDp5BgoHU9qNAAAtjKGRMeWO0rbhI5ZEJ9z3FMKPnX8Py Zbs19STtv6IHshoTFe2fyZEHcqaG//AgjWjPBnt9E94+OxNm4OwSMg7cRmENROYJIDNU PTfoPsZKZAkXjZTGani/SYmAC9TDlUgUwliA19Up+mTz0RCg2D//k1JrC5flu7ZQw2lX 1GjyRKzbHj1ue9KFCZb+5butOVe4yc5UOG6YJr2I2lx5pEatdXVGa56hpO+WMb0M6XD5 MZAU8SfGmP3FB3ixwX4nUCCunU7fLcdXZVKT8PvXvBzr7eqFA0roh+3o8ttPohrKJLdy FH0w== X-Forwarded-Encrypted: i=1; AFNElJ/bIAX5o8xdslOJawshnSzkaRXotysp9TCuNVCZ0587onTAEFMJhQ1jUzLn9aDqqpjU6rln+QaOIvUA6RI=@vger.kernel.org X-Gm-Message-State: AOJu0YzkTd9ZSCrQXVSkRLJPEU0Hh1K9SeU61lHUn923n4z7fmsqcRUZ 3XjlnbqQMTpSFkfJUp4eXTO6wIuTtRL5Xco4rVEkEUyWKK1mVwq219Uy X-Gm-Gg: Acq92OFYiLtlvov2iM88DUghBN7Ryh/O254qWewloWfNb7ad2n7SLdWqdCq2y/jv4aB qRgomejc3Za5ouUvNL7F/UJ0gLTtjEadma7snmUCrTJYWPxVzvWrHfmTZJ0CTpmFnYrk2WILgAm X14STPbS9L29y0Jayqjm7cGI+mBbFZd54Pae978gbvYt6dKHz/3cPGjgokPESjxY5lN8qTCj58X AqbfMIrNME1cN80Kjplw3yprpXjl5lvEz5ff6X0SnCB+u+wcdes7W0E4f6CD9mUsztaVcTMxMyc TEYwit5gnctcD+6oxBsrnorJ3KlKzopwEGA/QbgqITY6/PfLci1cJXvHZ/UJCzf+RKTaha7drXV 72xz1Z27WjBQd/RM1ptGbemEDX7MP4enYHIW2p+R8EIl2yUfp2T1ChhlOArl0ORXUHbaQxAVIIP ruFn9+TJmuTSUta2nLUPMB70jVbMv/qDIAC8wWj2hrk90UkFj+qpY8PVsmK0Lq4jP3EFzuIqXvC 2zw X-Received: by 2002:a05:7022:6b98:b0:12c:3d3c:ac08 with SMTP id a92af1059eb24-1349a6f91e9mr3040493c88.4.1778714090310; Wed, 13 May 2026 16:14:50 -0700 (PDT) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id a92af1059eb24-134cc2352f2sm1379258c88.10.2026.05.13.16.14.49 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 13 May 2026 16:14:50 -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 v2 3/4] HID: hid-msi: Add RGB control interface Date: Wed, 13 May 2026 23:14:44 +0000 Message-ID: <20260513231445.3213501-4-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260513231445.3213501-1-derekjohn.clark@gmail.com> References: <20260513231445.3213501-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 --- v2: - Use pending_profile mutex - Remove deadlock in cfg_setup, return on errors. --- drivers/hid/hid-msi.c | 548 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 542 insertions(+), 6 deletions(-) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index 13ba2747fdb66..a628b77bfb7b5 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,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_READ_PROFILE =3D 0x04, CLAW_COMMAND_TYPE_READ_PROFILE_ACK =3D 0x05, @@ -70,6 +75,7 @@ enum claw_profile_ack_pending { CLAW_NO_PENDING, CLAW_M1_PENDING, CLAW_M2_PENDING, + CLAW_RGB_PENDING, }; =20 enum claw_key_index { @@ -227,6 +233,22 @@ static const struct { { 0xce, "REL_WHEEL_DOWN" }, }; =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 */ @@ -237,6 +259,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]; @@ -245,6 +270,28 @@ struct claw_command_report { u8 data[59]; } __packed; =20 +struct rgb_zone { + u8 red; + u8 green; + u8 blue; +}; + +struct rgb_frame { + struct rgb_zone zone[CLAW_RGB_ZONES]; +}; + +struct rgb_report { + u8 profile; + __be16 read_addr; + 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; @@ -265,6 +312,16 @@ struct claw_drvdata { u8 m2_codes[CLAW_KEYS_MAX]; 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; + u8 rgb_frame_count; + bool rgb_enabled; + u8 rgb_speed; + u16 rgb_addr; }; =20 static int get_endpoint_address(struct hid_device *hdev) @@ -296,8 +353,11 @@ static int claw_gamepad_mode_event(struct claw_drvdata= *drvdata, =20 static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_co= mmand_report *cmd_rep) { - u8 *codes; - int i; + struct rgb_report *frame; + u16 rgb_addr, read_addr; + u8 *codes, f_idx; + u16 frame_calc; + int i, ret =3D 0; =20 switch (drvdata->profile_pending) { case CLAW_M1_PENDING: @@ -308,15 +368,52 @@ static int claw_profile_event(struct claw_drvdata *dr= vdata, struct claw_command_ for (i =3D 0; i < CLAW_KEYS_MAX; i++) codes[i] =3D (cmd_rep->data[6 + i] !=3D 0xff) ? cmd_rep->data[6 + i] : = 0x00; break; + case CLAW_RGB_PENDING: + frame =3D (struct 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 > U8_MAX) { + dev_err(drvdata->led_mc.led_cdev.dev, "Got unsupported frame index: %x\= n", + frame_calc); + ret =3D -EINVAL; + goto err_pending; + } + f_idx =3D frame_calc; + + if (f_idx >=3D CLAW_RGB_MAX_FRAMES) { + dev_err(drvdata->led_mc.led_cdev.dev, "Got illegal frame index: %x\n", + f_idx); + ret =3D -EINVAL; + goto err_pending; + } + + /* Always treat the first frame as the truth for these constants */ + if (f_idx =3D=3D 0) { + drvdata->rgb_frame_count =3D frame->frame_count; + /* Invert device speed (20-0) to sysfs speed (0-20) */ + 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; + } + + memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data, + sizeof(struct rgb_frame)); + + break; default: dev_warn(&drvdata->hdev->dev, "Got profile event without changes pending from command: %x\n", cmd_rep->cmd); - return -EINVAL; + ret =3D -EINVAL; } + +err_pending: 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, @@ -759,6 +856,397 @@ static const struct attribute_group claw_gamepad_attr= _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 < 8; i++) { + 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, 8); + if (ret) { + drvdata->profile_pending =3D CLAW_NO_PENDING; + 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 rgb_report report =3D { 0x01, 0x0000, CLAW_RGB_FRAME_OFFSET, 0x00, + drvdata->rgb_frame_count, 0x09, drvdata->rgb_speed, + drvdata->led_mc.led_cdev.brightness }; + u16 write_addr =3D drvdata->rgb_addr; + size_t len =3D sizeof(report); + 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++) { + 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, len, 8); + if (ret) + return ret; + + /* Increment the write addr by the offset for the next frame */ + write_addr +=3D CLAW_RGB_FRAME_OFFSET; + } + + ret =3D claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SYNC_TO_RO= M, NULL, 0, 8); + + 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, 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 }; + + 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 }; + + 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 frame; + + drvdata->rgb_frame_count =3D frame_count; + + for (frame =3D 0; frame < frame_count; frame++) + claw_frame_fill_solid(&drvdata->rgb_frames[frame], colors[frame]); + + 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 frame, zone; + + drvdata->rgb_frame_count =3D frame_count; + + for (frame =3D 0; frame < frame_count; frame++) { + for (zone =3D 0; zone < 4; zone++) { + drvdata->rgb_frames[frame].zone[zone] =3D colors[(zone + frame) % 4= ]; + drvdata->rgb_frames[frame].zone[zone + 4] =3D colors[(zone + frame) % 4= ]; + } + drvdata->rgb_frames[frame].zone[8] =3D colors[frame]; + } + + 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 frame, zone; + + drvdata->rgb_frame_count =3D frame_count; + + for (frame =3D 0; frame < frame_count; frame++) { + for (zone =3D 0; zone < 4; zone++) { + drvdata->rgb_frames[frame].zone[zone] =3D colors[(zone + frame) % 4= ]; + drvdata->rgb_frames[frame].zone[zone + 4] =3D colors[(zone - frame + 6)= % 4]; + } + drvdata->rgb_frames[frame].zone[8] =3D colors[frame]; + } + + return claw_write_rgb_state(drvdata); +} + +/* Apply current state to device */ +static int claw_apply_rgb_state(struct claw_drvdata *drvdata) +{ + static const struct rgb_zone off =3D { 0, 0, 0 }; + + if (!drvdata->rgb_enabled) { + drvdata->rgb_frame_count =3D 1; + claw_frame_fill_solid(&drvdata->rgb_frames[0], off); + return claw_write_rgb_state(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; + + 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; + + 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); + + 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; + + 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); + + 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; + + 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 hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + u8 speed =3D 20 - drvdata->rgb_speed; + + 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); + + 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 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); @@ -772,6 +1260,13 @@ static void cfg_setup_fn(struct work_struct *work) return; } =20 + ret =3D claw_read_rgb_config(drvdata->hdev); + if (ret) { + dev_err(drvdata->led_mc.led_cdev.dev, + "Failed to setup device, can't read RGB config: %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) { @@ -780,7 +1275,15 @@ static void cfg_setup_fn(struct work_struct *work) return; } =20 + ret =3D devm_device_add_group(drvdata->led_mc.led_cdev.dev, &rgb_attr_gro= up); + if (ret) { + dev_err(&drvdata->hdev->dev, + "Failed to setup device, can't create led attributes: %d\n", ret); + return; + } + kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE); + kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE); } =20 static void cfg_resume_fn(struct work_struct *work) @@ -790,6 +1293,10 @@ static void cfg_resume_fn(struct work_struct *work) u8 data[2] =3D { drvdata->gamepad_mode, drvdata->mkeys_function }; int ret; =20 + ret =3D claw_read_rgb_config(drvdata->hdev); + if (ret) + dev_err(drvdata->led_mc.led_cdev.dev, "Failed to read RGB config: %d\n",= ret); + ret =3D claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SWITCH_MOD= E, data, ARRAY_SIZE(data), 0); if (ret) @@ -803,18 +1310,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) @@ -844,6 +1357,26 @@ 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 + /* Initialize RGB LED */ + INIT_DELAYED_WORK(&drvdata->rgb_queue, &claw_rgb_queue_fn); + + 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; + + drvdata->rgb_enabled =3D true; + + ret =3D devm_led_classdev_multicolor_register(&hdev->dev, &drvdata->led_m= c); + if (ret) + return ret; + /* For control interface: open the HID transport for sending commands. */ ret =3D hid_hw_open(hdev); if (ret) @@ -905,6 +1438,9 @@ static void claw_remove(struct hid_device *hdev) return; } =20 + /* Block writes to brightness/multi_intensity during teardown */ + drvdata->led_mc.led_cdev.brightness_set =3D NULL; + cancel_delayed_work_sync(&drvdata->rgb_queue); cancel_delayed_work_sync(&drvdata->cfg_setup); cancel_delayed_work_sync(&drvdata->cfg_resume); hid_hw_close(hdev); --=20 2.53.0 From nobody Fri Jun 12 15:57:03 2026 Received: from mail-dl1-f45.google.com (mail-dl1-f45.google.com [74.125.82.45]) (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 30D553CC7E3 for ; Wed, 13 May 2026 23:14:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.45 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778714094; cv=none; b=G3wZb8Nmo5RnWVNXZ37hKoVGfd9meWmANRLjK7CImq9sHqiwCgOfH0hnKWPtWAS2hji+CWZ4TYoZimk8klTlp2twwEDoSE5hE3Avt0dQ4gkkbcYIHxX+huZM5sAcZqSi38l15A6nY4ocHwUeyMa8uCBukaxqS5AROUFzM0KlryA= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778714094; c=relaxed/simple; bh=SHihGLXgYmsJaW9i6CKwl9JKAO4sf3kSHa49mlcZ5BE=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=q35ON0+k5Uu1oVaabOVK2GH6DDOd8PLpa8FJz6o9Zcc+ZAgbQrECmtnMAdp/qiEs3VFWMyGEVkv/0oxLEpIaf1RKJvqcY3QS5kNOZBm/IUC0eZ2ytUWpMMkiElw2Dk8gRqPgaZ3pHFE/XJQeKvqw6n1uTrWk4eg2J8McM6Y6rV0= 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=phGsSa4N; arc=none smtp.client-ip=74.125.82.45 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="phGsSa4N" Received: by mail-dl1-f45.google.com with SMTP id a92af1059eb24-130c9dcbd25so6757769c88.1 for ; Wed, 13 May 2026 16:14:52 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778714091; x=1779318891; 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=1jmRXpj4cHFfB9YL53UH4jX2G42efHj1Df9QupG0QTs=; b=phGsSa4NHUNNJ7kPT4tAa2A7SgX0EAOBOXiQmXqoxgx216cIpV+iSbMmFi7u8ReN13 OrLU1bIsUmoD/hAbiZm0FIBM6UYJyofaaH8SV6rmRE5MTg2F/zMd3MoNbnbC7ncJNOcv qIUZH4f6CYGht/KYWMtkm5HOyi6mGjf7yxI4f7NT2DUh2D/hSP+Bv/D3bEYAfNZqGKoS E6uaOOIyB7pC6rJfnzpdIjCwGaOPj/lBJ8JGvNyLVbniot1SyNjdzSLs9rtgErIf8QM8 yqSDI9GXh6IhkGXlSr4mhp1FhDPEJe6CZDnLFem1laCCpU3TLsMLH8uVkk/tHMpjjpYk cWBg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778714091; x=1779318891; 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=1jmRXpj4cHFfB9YL53UH4jX2G42efHj1Df9QupG0QTs=; b=rRFAPq8rw569yVEvSi31kO3Rj1XO4b7ZVuDWq0/GDSPK1hFSy7vo09DxyhL2zbElIb P2IdtxC8ikzWSWkej9k+WDJOGifjeo9lnzti8qlrNtJarplRPHY0Mb3vxyE7o8Bj/gEs I2hYjBrkJaXANSR4vO7ct3lXHaZ6BhrJXMClqjc8h/gGl4alKh3u16rZuR6SR5k0hp+u ++Sn5Gx6EQA0wygsa1zAh2Ozf8nmd+qNNKh4U3p6gzcQVLBnpE68xpaJXUNou7q7YTY0 lIk4AN5O6xhxqyXqBIwCfDkQEKEvnH3p6ZyoAFfCNY8Xa6Afskp2U2M27BddQs1QM/yZ vuQQ== X-Forwarded-Encrypted: i=1; AFNElJ/N4IUnRoVqEY+U5dYX35JpzxR4+HyQk54RPDD4IrwPnmbyx0GnSFtI3fosvb2GoQKsJzInfdxlNxiBrLo=@vger.kernel.org X-Gm-Message-State: AOJu0Yx5RWHLbQNkSGOKM+XMswnOp/gRCyffgIIUod4Z+LK0/drtUWHi 2bonI7HKjaGnYadYYv3frcY5TgK2VxufXL9VMsIF1NgAGQ6G2lDjGJHq X-Gm-Gg: Acq92OFrvm5ydErxHnK7TZgxEgkuDmRiL3AxnebPDKeoHXBtL3tFVj+dN9vpAunZmkA R/zxv9CFoMR3kjeg4jPzhwLf2IDZU1s9iFLf6ZQniNhl4+23N7Xlvnlq0lYA3ikiPdxAGS91PIp Yne2ShPdnWIVuqamECjmdEgvSp7vT88015Xkeh1YVMcU+is6Vj6mbLxrwN9GqKbENvEZ7v5F3qy gN3WOFlPZRnaaTrJ/HaSNUL12HxVXZ6a8ZbFSFGhkY4pIFfmtP1gOjS3axncJviTaHGgefomdnC Op9r6L7SSy9i0RZ/DSuN2KFbqFU6tW2aJivY//D7dFMOB4Pmg7QUfArbYnfwLhTM2zz47ZSWawZ 6nbYCXZYiDj6PDbn11MvtEmfLEk5A9O9xC4vRk/eZDpupxhrad4z0Mj1Ft2HWdUMVeNRpt2umIb 1BmgNtvmfrUm9uQagNBxsAdI6LQ7/Pylc4wBn/d2I/DjgOI7xta3DfidxQbIRbUsH5nV9vsXRAJ jhB X-Received: by 2002:a05:7022:90e:b0:123:3c24:b15 with SMTP id a92af1059eb24-134c8d41a3emr840976c88.19.1778714091128; Wed, 13 May 2026 16:14:51 -0700 (PDT) Received: from lappy (108-228-232-20.lightspeed.sndgca.sbcglobal.net. [108.228.232.20]) by smtp.gmail.com with ESMTPSA id a92af1059eb24-134cc2352f2sm1379258c88.10.2026.05.13.16.14.50 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 13 May 2026 16:14:50 -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 v2 4/4] HID: hid-msi: Add Rumble Intensity Attributes Date: Wed, 13 May 2026 23:14:45 +0000 Message-ID: <20260513231445.3213501-5-derekjohn.clark@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260513231445.3213501-1-derekjohn.clark@gmail.com> References: <20260513231445.3213501-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 --- v2: - Use pending_profile and sync to rom mutexes. --- drivers/hid/hid-msi.c | 147 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c index a628b77bfb7b5..cffd6ed253ec9 100644 --- a/drivers/hid/hid-msi.c +++ b/drivers/hid/hid-msi.c @@ -76,6 +76,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 { @@ -262,6 +264,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]; @@ -310,7 +317,10 @@ 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; const u16 *bmap_addr; + bool rumble_support; bool bmap_support; =20 /* RGB Variables */ @@ -402,6 +412,12 @@ static int claw_profile_event(struct claw_drvdata *drv= data, struct claw_command_ memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data, sizeof(struct rgb_frame)); =20 + break; + case CLAW_RUMBLE_LEFT_PENDING: + drvdata->rumble_intensity_left =3D cmd_rep->data[4]; + break; + case CLAW_RUMBLE_RIGHT_PENDING: + drvdata->rumble_intensity_right =3D cmd_rep->data[4]; break; default: dev_warn(&drvdata->hdev->dev, @@ -815,6 +831,126 @@ 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) +{ + u8 data[] =3D { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xff= , 0x01, 0x00 }; + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + u8 val; + int ret; + + ret =3D kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val > 100) + return -EINVAL; + + data[4] =3D val; + + guard(mutex)(&drvdata->rom_mutex); + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + data, ARRAY_SIZE(data), 8); + if (ret) + return ret; + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, = 0, 8); + if (ret) + return ret; + + drvdata->rumble_intensity_left =3D val; + + return count; +} + +static ssize_t rumble_intensity_left_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + u8 data[4] =3D { 0x01, (rumble_addr[0] >> 8) & 0xff, rumble_addr[0] & 0xf= f, 0x01 }; + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + int ret; + + guard(mutex)(&drvdata->profile_mutex); + drvdata->profile_pending =3D CLAW_RUMBLE_LEFT_PENDING; + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, + ARRAY_SIZE(data), 8); + if (ret) { + drvdata->profile_pending =3D CLAW_NO_PENDING; + return ret; + } + + return sysfs_emit(buf, "%u\n", drvdata->rumble_intensity_left); +} +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) +{ + u8 data[] =3D { 0x01, (rumble_addr[1] >> 8) & 0xff, rumble_addr[1] & 0xff= , 0x01, 0x00 }; + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + u8 val; + int ret; + + ret =3D kstrtou8(buf, 10, &val); + if (ret) + return ret; + + if (val > 100) + return -EINVAL; + + data[4] =3D val; + + guard(mutex)(&drvdata->rom_mutex); + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA, + data, ARRAY_SIZE(data), 8); + if (ret) + return ret; + + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, = 0, 8); + if (ret) + return ret; + + drvdata->rumble_intensity_right =3D val; + + return count; +} + +static ssize_t rumble_intensity_right_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + u8 data[4] =3D { 0x01, (rumble_addr[1] >> 8) & 0xff, rumble_addr[1] & 0xf= f, 0x01 }; + struct hid_device *hdev =3D to_hid_device(dev); + struct claw_drvdata *drvdata =3D hid_get_drvdata(hdev); + int ret; + + guard(mutex)(&drvdata->profile_mutex); + drvdata->profile_pending =3D CLAW_RUMBLE_RIGHT_PENDING; + ret =3D claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, + ARRAY_SIZE(data), 8); + if (ret) { + drvdata->profile_pending =3D CLAW_NO_PENDING; + return ret; + } + + return sysfs_emit(buf, "%u\n", drvdata->rumble_intensity_right); +} +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) { @@ -835,6 +971,12 @@ static umode_t claw_gamepad_attr_is_visible(struct kob= ject *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; } @@ -848,6 +990,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 @@ -1312,6 +1457,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; @@ -1323,6 +1469,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; } --=20 2.53.0