* [PATCH v5] Input: gunze: replace deprecated APIs and fix warning style
From: Bivash Kumar Singh @ 2026-07-20 3:24 UTC (permalink / raw)
To: linux-input; +Cc: dmitry.torokhov, Bivash Kumar Singh
In-Reply-To: <20260718190040.10613-1-bivashraj750@gmail.com>
Replace printk(KERN_WARNING) with dev_warn_ratelimited() using the
serio device, which is the correct logging style for driver code and
prevents log spam on noisy serial lines.
Replace simple_strtoul() with sscanf() to parse the comma-separated
coordinate data. NUL-terminate the receive buffer in gunze_interrupt()
before calling gunze_process_packet() so sscanf() and the warning
message can safely treat the data as a string.
Signed-off-by: Bivash Kumar Singh <bivashraj750@gmail.com>
---
Changes in v5:
- Switch from kstrtoul() to sscanf() as suggested by Dmitry Torokhov.
sscanf() naturally handles comma-separated coordinate data without
needing a local buffer copy or manual NUL termination tricks.
- NUL-terminate receive buffer in gunze_interrupt() before processing.
- Use dev_warn_ratelimited() to prevent log spam on noisy serial lines.
Changes in v4:
- Remove early return on kstrtoul() failure to prevent touch state
getting permanently stuck if a release packet contains noisy data.
Initialize x and y to 0 as safe fallback values instead.
(reported by Sashiko AI review)
Changes in v3:
- Fix comment style: add space after /* and use NUL instead of NULL
- Add missing Changes section that was absent in v2
Changes in v2:
- Copy packet data to a local NUL-terminated buffer before calling
kstrtoul(), so the comma separator does not cause parsing to fail
on every valid touch event. (reported by Sashiko AI review)
---
drivers/input/touchscreen/gunze.c | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/drivers/input/touchscreen/gunze.c b/drivers/input/touchscreen/gunze.c
index 2baeb4f3b941..7a92052eba80 100644
--- a/drivers/input/touchscreen/gunze.c
+++ b/drivers/input/touchscreen/gunze.c
@@ -41,15 +41,20 @@ struct gunze {
static void gunze_process_packet(struct gunze *gunze)
{
struct input_dev *dev = gunze->dev;
+ unsigned int x, y;
- if (gunze->idx != GUNZE_MAX_LENGTH || gunze->data[5] != ',' ||
- (gunze->data[0] != 'T' && gunze->data[0] != 'R')) {
- printk(KERN_WARNING "gunze.c: bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
+ if (gunze->data[0] != 'T' && gunze->data[0] != 'R') {
+ dev_warn_ratelimited(&gunze->serio->dev, "bad packet: >%s<\n", gunze->data);
return;
}
- input_report_abs(dev, ABS_X, simple_strtoul(gunze->data + 1, NULL, 10));
- input_report_abs(dev, ABS_Y, 1024 - simple_strtoul(gunze->data + 6, NULL, 10));
+ if (sscanf(gunze->data + 1, "%4u,%4u", &x, &y) != 2) {
+ dev_warn_ratelimited(&gunze->serio->dev, "bad packet: >%s<\n", gunze->data);
+ return;
+ }
+
+ input_report_abs(dev, ABS_X, x);
+ input_report_abs(dev, ABS_Y, 1024 - y);
input_report_key(dev, BTN_TOUCH, gunze->data[0] == 'T');
input_sync(dev);
}
@@ -60,6 +65,7 @@ static irqreturn_t gunze_interrupt(struct serio *serio,
struct gunze *gunze = serio_get_drvdata(serio);
if (data == '\r') {
+ gunze->data[gunze->idx] = '\0';
gunze_process_packet(gunze);
gunze->idx = 0;
} else {
--
2.53.0
^ permalink raw reply related
* [PATCH v13 4/4] HID: hid-msi: Add Rumble Intensity Attributes
From: Derek J. Clark @ 2026-07-20 3:15 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260720031549.2272658-1-derekjohn.clark@gmail.com>
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 <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Link: https://patch.msgid.link/20260529072111.7565-5-derekjohn.clark@gmail.com
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v12:
- On address mismatch, assume stale message return and keep waiting for
correct message.
- Use spinlock_irqsave for raw_event-reachable locks since completion
context isn't guaranteed softirq-only across all HCDs.
v11:
- Restore dropped changes from v10.
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 | 202 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 201 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
index 5cb85fc2f053..3225d3bf6125 100644
--- a/drivers/hid/hid-msi.c
+++ b/drivers/hid/hid-msi.c
@@ -80,6 +80,8 @@ enum claw_profile_ack_pending {
CLAW_M1_PENDING,
CLAW_M2_PENDING,
CLAW_RGB_PENDING,
+ CLAW_RUMBLE_LEFT_PENDING,
+ CLAW_RUMBLE_RIGHT_PENDING,
};
enum claw_key_index {
@@ -272,6 +274,11 @@ static const u16 button_mapping_addr_new[] = {
static const u16 rgb_addr_old = 0x01fa;
static const u16 rgb_addr_new = 0x024a;
+static const u16 rumble_addr[] = {
+ 0x0022, /* left */
+ 0x0023, /* right */
+};
+
struct claw_command_report {
u8 report_id;
u8 padding[2];
@@ -314,6 +321,12 @@ struct claw_rgb_report {
struct rgb_frame zone_data;
} __packed;
+struct claw_rumble_report {
+ struct claw_profile_report;
+ u8 padding;
+ u8 intensity;
+} __packed;
+
struct claw_drvdata {
/* MCU General Variables */
enum claw_profile_ack_pending profile_pending;
@@ -339,8 +352,12 @@ struct claw_drvdata {
enum claw_gamepad_mode_index gamepad_mode;
u8 m1_codes[CLAW_KEYS_MAX];
u8 m2_codes[CLAW_KEYS_MAX];
- spinlock_t mode_lock; /* Lock for mode data read/write */
+ u8 rumble_intensity_right;
+ u8 rumble_intensity_left;
const u16 *bmap_addr;
+ spinlock_t rumble_lock; /* lock for rumble_intensity read/write */
+ spinlock_t mode_lock; /* Lock for mode data read/write */
+ bool rumble_support;
bool gp_registered;
bool bmap_support;
@@ -389,6 +406,7 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep)
{
enum claw_profile_ack_pending profile;
+ struct claw_rumble_report *rumble;
struct claw_mkey_report *mkeys;
struct claw_rgb_report *frame;
u16 rgb_addr, read_addr;
@@ -442,6 +460,20 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_
}
break;
+ case CLAW_RUMBLE_LEFT_PENDING:
+ rumble = (struct claw_rumble_report *)cmd_rep->data;
+ if (be16_to_cpu(rumble->read_addr) != rumble_addr[0])
+ return -EAGAIN;
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ drvdata->rumble_intensity_left = rumble->intensity;
+ break;
+ case CLAW_RUMBLE_RIGHT_PENDING:
+ rumble = (struct claw_rumble_report *)cmd_rep->data;
+ if (be16_to_cpu(rumble->read_addr) != rumble_addr[1])
+ return -EAGAIN;
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ drvdata->rumble_intensity_right = rumble->intensity;
+ break;
default:
dev_dbg(&drvdata->hdev->dev,
"Got profile event without changes pending from command: %x\n",
@@ -991,6 +1023,162 @@ static ssize_t button_mapping_options_show(struct device *dev,
}
static DEVICE_ATTR_RO(button_mapping_options);
+static ssize_t rumble_intensity_left_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[0])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val > 100)
+ return -EINVAL;
+
+ report.intensity = val;
+
+ guard(mutex)(&drvdata->rom_mutex);
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t rumble_intensity_left_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[0])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
+ u8 val;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ guard(mutex)(&drvdata->profile_mutex);
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_RUMBLE_LEFT_PENDING;
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ val = drvdata->rumble_intensity_left;
+
+ return sysfs_emit(buf, "%u\n", val);
+}
+static DEVICE_ATTR_RW(rumble_intensity_left);
+
+static ssize_t rumble_intensity_right_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[1])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val > 100)
+ return -EINVAL;
+
+ report.intensity = val;
+
+ guard(mutex)(&drvdata->rom_mutex);
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t rumble_intensity_right_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct claw_rumble_report report = { {0x01, cpu_to_be16(rumble_addr[1])}, 0x01 };
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
+ u8 val;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ guard(mutex)(&drvdata->profile_mutex);
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_RUMBLE_RIGHT_PENDING;
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->rumble_lock)
+ val = drvdata->rumble_intensity_right;
+
+ return sysfs_emit(buf, "%u\n", val);
+}
+static DEVICE_ATTR_RW(rumble_intensity_right);
+
+static ssize_t rumble_intensity_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0-100\n");
+}
+static DEVICE_ATTR_RO(rumble_intensity_range);
+
static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
int n)
{
@@ -1011,6 +1199,12 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu
attr == &dev_attr_reset.attr)
return attr->mode;
+ /* Hide rumble attrs if not supported */
+ if (attr == &dev_attr_rumble_intensity_left.attr ||
+ attr == &dev_attr_rumble_intensity_right.attr ||
+ attr == &dev_attr_rumble_intensity_range.attr)
+ return drvdata->rumble_support ? attr->mode : 0;
+
/* Hide button mapping attrs if it isn't supported */
return drvdata->bmap_support ? attr->mode : 0;
}
@@ -1024,6 +1218,9 @@ static struct attribute *claw_gamepad_attrs[] = {
&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,
};
@@ -1605,6 +1802,7 @@ static void claw_features_supported(struct claw_drvdata *drvdata)
drvdata->bmap_support = true;
if (minor >= 0x66) {
drvdata->bmap_addr = button_mapping_addr_new;
+ drvdata->rumble_support = true;
drvdata->rgb_addr = rgb_addr_new;
} else {
drvdata->bmap_addr = button_mapping_addr_old;
@@ -1616,6 +1814,7 @@ static void claw_features_supported(struct claw_drvdata *drvdata)
if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
drvdata->bmap_support = true;
drvdata->bmap_addr = button_mapping_addr_new;
+ drvdata->rumble_support = true;
drvdata->rgb_addr = rgb_addr_new;
return;
}
@@ -1666,6 +1865,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->orphan_ack_complete);
init_completion(&drvdata->send_cmd_complete);
INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
--
2.54.0
^ permalink raw reply related
* [PATCH v13 3/4] HID: hid-msi: Add RGB control interface
From: Derek J. Clark @ 2026-07-20 3:15 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260720031549.2272658-1-derekjohn.clark@gmail.com>
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 <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Link: https://patch.msgid.link/20260529072111.7565-4-derekjohn.clark@gmail.com
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v13:
- Add profile lock to rgb attributes store functions when mutating
read varaibles.
- Only use drvdata->hdev->dev for errors instead of
drvdata->led_mc.led_cdev.dev, preventing use-after-free in rare races.
v12:
- On remove, cancel the rgb_queue last to avoid a re-arming, and in
rgb_queue_fn check rgb_registered to avoid running after the sysfs
group has been removed.
- On address mismatch, assume stale message return and keep waiting for
correct message.
- Use spinlock_irqsave for raw_event-reachable locks since completion
context isn't guaranteed softirq-only across all HCDs.
- Add note to led_cdev.name to (hopefully) silence sashiko-bot about
an impossible double device collision.
- Explicitly check if the current address is less than the rgb_address
to avoid invalid overflow math.
v9:
- Limit guard usage in cfg_setup_fn to avoid holding a lock during
registration and add group events.
- Don't use devm_ functions in cfg_setup_fn. Manually setup/teardown
devices and groups to prevent use after free.
- Ensure rgb_queue work is canceled during suspend.
- Check for drvdata in rgb_queue_fn to avoid use after free during
teardown.
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 >=.
- 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 | 681 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 666 insertions(+), 15 deletions(-)
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
index 023d63cee6b0..5cb85fc2f053 100644
--- a/drivers/hid/hid-msi.c
+++ b/drivers/hid/hid-msi.c
@@ -22,6 +22,7 @@
#include <linux/device.h>
#include <linux/hid.h>
#include <linux/kobject.h>
+#include <linux/led-class-multicolor.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/mutex.h>
@@ -45,6 +46,10 @@
#define CLAW_KEYS_MAX 5
+#define CLAW_RGB_ZONES 9
+#define CLAW_RGB_MAX_FRAMES 8
+#define CLAW_RGB_FRAME_OFFSET 0x24
+
enum claw_command_index {
CLAW_COMMAND_TYPE_NONE = 0x00,
CLAW_COMMAND_TYPE_READ_PROFILE = 0x04,
@@ -74,6 +79,7 @@ enum claw_profile_ack_pending {
CLAW_NO_PENDING,
CLAW_M1_PENDING,
CLAW_M2_PENDING,
+ CLAW_RGB_PENDING,
};
enum claw_key_index {
@@ -237,6 +243,22 @@ static const struct {
{ 0xff, "DISABLED" },
};
+enum claw_rgb_effect_index {
+ CLAW_RGB_EFFECT_MONOCOLOR,
+ CLAW_RGB_EFFECT_BREATHE,
+ CLAW_RGB_EFFECT_CHROMA,
+ CLAW_RGB_EFFECT_RAINBOW,
+ CLAW_RGB_EFFECT_FROSTFIRE,
+};
+
+static const char * const claw_rgb_effect_text[] = {
+ [CLAW_RGB_EFFECT_MONOCOLOR] = "monocolor",
+ [CLAW_RGB_EFFECT_BREATHE] = "breathe",
+ [CLAW_RGB_EFFECT_CHROMA] = "chroma",
+ [CLAW_RGB_EFFECT_RAINBOW] = "rainbow",
+ [CLAW_RGB_EFFECT_FROSTFIRE] = "frostfire",
+};
+
static const u16 button_mapping_addr_old[] = {
0x007a, /* M1 */
0x011f, /* M2 */
@@ -247,6 +269,9 @@ static const u16 button_mapping_addr_new[] = {
0x0164, /* M2 */
};
+static const u16 rgb_addr_old = 0x01fa;
+static const u16 rgb_addr_new = 0x024a;
+
struct claw_command_report {
u8 report_id;
u8 padding[2];
@@ -268,6 +293,27 @@ struct claw_mkey_report {
u8 codes[5];
} __packed;
+struct rgb_zone {
+ u8 red;
+ u8 green;
+ u8 blue;
+};
+
+struct rgb_frame {
+ struct rgb_zone zone[CLAW_RGB_ZONES];
+};
+
+struct claw_rgb_report {
+ struct claw_profile_report;
+ u8 frame_bytes;
+ u8 padding;
+ u8 frame_count;
+ u8 state; /* Always 0x09 */
+ u8 speed;
+ u8 brightness;
+ struct rgb_frame zone_data;
+} __packed;
+
struct claw_drvdata {
/* MCU General Variables */
enum claw_profile_ack_pending profile_pending;
@@ -293,10 +339,22 @@ struct claw_drvdata {
enum claw_gamepad_mode_index gamepad_mode;
u8 m1_codes[CLAW_KEYS_MAX];
u8 m2_codes[CLAW_KEYS_MAX];
- const u16 *bmap_addr;
spinlock_t mode_lock; /* Lock for mode data read/write */
+ const u16 *bmap_addr;
bool gp_registered;
bool bmap_support;
+
+ /* RGB Variables */
+ struct rgb_frame rgb_frames[CLAW_RGB_MAX_FRAMES];
+ enum claw_rgb_effect_index rgb_effect;
+ struct led_classdev_mc led_mc;
+ struct delayed_work rgb_queue;
+ spinlock_t frame_lock; /* lock for rgb_frames read/write */
+ bool rgb_registered;
+ u8 rgb_frame_count;
+ bool rgb_enabled;
+ u8 rgb_speed;
+ u16 rgb_addr;
};
static int get_endpoint_address(struct hid_device *hdev)
@@ -332,7 +390,10 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_
{
enum claw_profile_ack_pending profile;
struct claw_mkey_report *mkeys;
- u8 *codes, key;
+ struct claw_rgb_report *frame;
+ u16 rgb_addr, read_addr;
+ u8 *codes, key, f_idx;
+ u16 frame_calc;
int i;
scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
@@ -349,6 +410,38 @@ static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_
for (i = 0; i < CLAW_KEYS_MAX; i++)
codes[i] = (mkeys->codes[i]);
break;
+ case CLAW_RGB_PENDING:
+ frame = (struct claw_rgb_report *)cmd_rep->data;
+ rgb_addr = drvdata->rgb_addr;
+ read_addr = be16_to_cpu(frame->read_addr);
+
+ if (read_addr < drvdata->rgb_addr)
+ return -EAGAIN;
+
+ frame_calc = (read_addr - rgb_addr) / CLAW_RGB_FRAME_OFFSET;
+ if (frame_calc >= CLAW_RGB_MAX_FRAMES) {
+ dev_err(&drvdata->hdev->dev, "Got unsupported frame index: %x\n",
+ frame_calc);
+ return -EAGAIN;
+ }
+ f_idx = frame_calc;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ memcpy(&drvdata->rgb_frames[f_idx], &frame->zone_data,
+ sizeof(struct rgb_frame));
+
+ /* Only use frame 0 for remaining variable assignment */
+ if (f_idx != 0)
+ break;
+
+ drvdata->rgb_speed = frame->speed;
+ drvdata->led_mc.led_cdev.brightness = frame->brightness;
+ drvdata->led_mc.subled_info[0].intensity = frame->zone_data.zone[0].red;
+ drvdata->led_mc.subled_info[1].intensity = frame->zone_data.zone[0].green;
+ drvdata->led_mc.subled_info[2].intensity = frame->zone_data.zone[0].blue;
+ }
+
+ break;
default:
dev_dbg(&drvdata->hdev->dev,
"Got profile event without changes pending from command: %x\n",
@@ -939,32 +1032,555 @@ static const struct attribute_group claw_gamepad_attr_group = {
.is_visible = claw_gamepad_attr_is_visible,
};
+/* Read RGB config from device */
+static int claw_read_rgb_config(struct hid_device *hdev)
+{
+ u8 data[4] = { 0x01, 0x00, 0x00, CLAW_RGB_FRAME_OFFSET };
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u16 read_addr = drvdata->rgb_addr;
+ size_t len = ARRAY_SIZE(data);
+ int ret, i;
+
+ if (!drvdata->rgb_addr)
+ return -ENODEV;
+
+ /* Loop through all 8 pages of RGB data */
+ guard(mutex)(&drvdata->profile_mutex);
+ for (i = 0; i < CLAW_RGB_MAX_FRAMES; i++) {
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_RGB_PENDING;
+ data[1] = (read_addr >> 8) & 0xff;
+ data[2] = read_addr & 0x00ff;
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE, data, len, 25);
+ if (ret)
+ return ret;
+
+ read_addr += CLAW_RGB_FRAME_OFFSET;
+ }
+
+ return 0;
+}
+
+/* Send RGB configuration to device */
+static int claw_write_rgb_state(struct claw_drvdata *drvdata)
+{
+ struct claw_rgb_report report = { {0x01, 0}, CLAW_RGB_FRAME_OFFSET, 0x00,
+ drvdata->rgb_frame_count, 0x09, drvdata->rgb_speed,
+ drvdata->led_mc.led_cdev.brightness };
+ u16 write_addr = drvdata->rgb_addr;
+ int f, ret;
+
+ if (!drvdata->rgb_addr)
+ return -ENODEV;
+
+ if (!drvdata->rgb_frame_count)
+ return -EINVAL;
+
+ guard(mutex)(&drvdata->rom_mutex);
+ /* Loop through (up to) 8 pages of RGB data */
+ for (f = 0; f < drvdata->rgb_frame_count; f++) {
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock)
+ report.zone_data = drvdata->rgb_frames[f];
+
+ /* Set the MCU address to write the frame data to */
+ report.read_addr = cpu_to_be16(write_addr);
+
+ /* Serialize the rgb_report and write it to MCU */
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ /* Increment the write addr by the offset for the next frame */
+ write_addr += CLAW_RGB_FRAME_OFFSET;
+ }
+
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+
+ return ret;
+}
+
+/* Fill all zones with the same color */
+static void claw_frame_fill_solid(struct rgb_frame *frame, struct rgb_zone zone)
+{
+ int z;
+
+ for (z = 0; z < CLAW_RGB_ZONES; z++)
+ frame->zone[z] = zone;
+}
+
+/* Apply solid effect (1 frame, no color) */
+static int claw_apply_disabled(struct claw_drvdata *drvdata)
+{
+ struct rgb_zone off = { 0x00, 0x00, 0x00};
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = 1;
+ claw_frame_fill_solid(&drvdata->rgb_frames[0], off);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply solid effect (1 frame, all zones same color) */
+static int claw_apply_monocolor(struct claw_drvdata *drvdata)
+{
+ struct mc_subled *subleds = drvdata->led_mc.subled_info;
+ struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity,
+ subleds[2].intensity };
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = 1;
+ claw_frame_fill_solid(&drvdata->rgb_frames[0], zone);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply breathe effect (2 frames: color -> off) */
+static int claw_apply_breathe(struct claw_drvdata *drvdata)
+{
+ struct mc_subled *subleds = drvdata->led_mc.subled_info;
+ struct rgb_zone zone = { subleds[0].intensity, subleds[1].intensity,
+ subleds[2].intensity };
+ static const struct rgb_zone off = { 0, 0, 0 };
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = 2;
+ claw_frame_fill_solid(&drvdata->rgb_frames[0], zone);
+ claw_frame_fill_solid(&drvdata->rgb_frames[1], off);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply chroma effect (6 frames: rainbow cycle, all zones sync) */
+static int claw_apply_chroma(struct claw_drvdata *drvdata)
+{
+ static const struct rgb_zone colors[] = {
+ {255, 0, 0}, /* red */
+ {255, 255, 0}, /* yellow */
+ { 0, 255, 0}, /* green */
+ { 0, 255, 255}, /* cyan */
+ { 0, 0, 255}, /* blue */
+ {255, 0, 255}, /* magenta */
+ };
+ u8 frame_count = ARRAY_SIZE(colors);
+ int f;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = frame_count;
+
+ for (f = 0; f < frame_count; f++)
+ claw_frame_fill_solid(&drvdata->rgb_frames[f], colors[f]);
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply rainbow effect (4 frames: rotating colors around joysticks) */
+static int claw_apply_rainbow(struct claw_drvdata *drvdata)
+{
+ static const struct rgb_zone colors[] = {
+ {255, 0, 0}, /* red */
+ { 0, 255, 0}, /* green */
+ { 0, 255, 255}, /* cyan */
+ { 0, 0, 255}, /* blue */
+ };
+ u8 frame_count = ARRAY_SIZE(colors);
+ int f, z;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = frame_count;
+
+ for (f = 0; f < frame_count; f++) {
+ for (z = 0; z < 4; z++) {
+ drvdata->rgb_frames[f].zone[z] = colors[(z + f) % 4];
+ drvdata->rgb_frames[f].zone[z + 4] = colors[(z + f) % 4];
+ }
+ drvdata->rgb_frames[f].zone[8] = colors[f];
+ }
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/*
+ * Apply frostfire effect (4 frames: fire vs ice rotating)
+ * Right joystick: fire red -> dark -> ice blue -> dark (clockwise)
+ * Left joystick: ice blue -> dark -> fire red -> dark (counter-clockwise)
+ * ABXY: fire red -> dark -> ice blue -> dark
+ */
+static int claw_apply_frostfire(struct claw_drvdata *drvdata)
+{
+ static const struct rgb_zone colors[] = {
+ {255, 0, 0}, /* fire red */
+ { 0, 0, 0}, /* dark */
+ { 0, 0, 255}, /* ice blue */
+ { 0, 0, 0}, /* dark */
+ };
+ u8 frame_count = ARRAY_SIZE(colors);
+ int f, z;
+
+ scoped_guard(spinlock_irqsave, &drvdata->frame_lock) {
+ drvdata->rgb_frame_count = frame_count;
+
+ for (f = 0; f < frame_count; f++) {
+ for (z = 0; z < 4; z++) {
+ drvdata->rgb_frames[f].zone[z] = colors[(z + f) % 4];
+ drvdata->rgb_frames[f].zone[z + 4] = colors[(z - f + 6) % 4];
+ }
+ drvdata->rgb_frames[f].zone[8] = colors[f];
+ }
+ }
+
+ return claw_write_rgb_state(drvdata);
+}
+
+/* Apply current state to device */
+static int claw_apply_rgb_state(struct claw_drvdata *drvdata)
+{
+ if (!drvdata->rgb_enabled)
+ return claw_apply_disabled(drvdata);
+
+ switch (drvdata->rgb_effect) {
+ case CLAW_RGB_EFFECT_MONOCOLOR:
+ return claw_apply_monocolor(drvdata);
+ case CLAW_RGB_EFFECT_BREATHE:
+ return claw_apply_breathe(drvdata);
+ case CLAW_RGB_EFFECT_CHROMA:
+ return claw_apply_chroma(drvdata);
+ case CLAW_RGB_EFFECT_RAINBOW:
+ return claw_apply_rainbow(drvdata);
+ case CLAW_RGB_EFFECT_FROSTFIRE:
+ return claw_apply_frostfire(drvdata);
+ default:
+ dev_err(&drvdata->hdev->dev, "No supported rgb_effect selected\n");
+ return -EINVAL;
+ }
+}
+
+static void claw_rgb_queue_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, rgb_queue);
+ int ret;
+
+ if (!drvdata)
+ return;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return;
+ }
+
+ ret = claw_apply_rgb_state(drvdata);
+ if (ret)
+ dev_err(&drvdata->hdev->dev, "Failed to apply RGB state: %d\n", ret);
+}
+
+static ssize_t effect_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ ret = sysfs_match_string(claw_rgb_effect_text, buf);
+ if (ret < 0)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->rgb_effect = ret;
+
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+ return count;
+}
+
+static ssize_t effect_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ if (drvdata->rgb_effect >= ARRAY_SIZE(claw_rgb_effect_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", claw_rgb_effect_text[drvdata->rgb_effect]);
+}
+
+static DEVICE_ATTR_RW(effect);
+
+static ssize_t effect_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_rgb_effect_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_rgb_effect_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(effect_index);
+
+static ssize_t enabled_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ bool val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtobool(buf, &val);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->rgb_enabled = val;
+
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+ return count;
+}
+
+static ssize_t enabled_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ return sysfs_emit(buf, "%s\n", drvdata->rgb_enabled ? "true" : "false");
+}
+static DEVICE_ATTR_RW(enabled);
+
+static ssize_t enabled_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "true false\n");
+}
+static DEVICE_ATTR_RO(enabled_index);
+
+static ssize_t speed_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ unsigned int val, speed;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtouint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val > 20)
+ return -EINVAL;
+
+ /* 0 is fastest, invert value for intuitive userspace speed */
+ speed = 20 - val;
+
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->rgb_speed = speed;
+
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+
+ return count;
+}
+
+static ssize_t speed_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+ u8 speed = 20 - drvdata->rgb_speed;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return -ENODEV;
+ }
+
+ return sysfs_emit(buf, "%u\n", speed);
+}
+static DEVICE_ATTR_RW(speed);
+
+static ssize_t speed_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "0-20\n");
+}
+static DEVICE_ATTR_RO(speed_range);
+
+static void claw_led_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness _brightness)
+{
+ struct led_classdev_mc *led_mc = container_of(led_cdev, struct led_classdev_mc, led_cdev);
+ struct claw_drvdata *drvdata = container_of(led_mc, struct claw_drvdata, led_mc);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->rgb_registered))
+ return;
+ }
+
+ mod_delayed_work(system_wq, &drvdata->rgb_queue, msecs_to_jiffies(50));
+}
+
+static struct attribute *claw_rgb_attrs[] = {
+ &dev_attr_effect.attr,
+ &dev_attr_effect_index.attr,
+ &dev_attr_enabled.attr,
+ &dev_attr_enabled_index.attr,
+ &dev_attr_speed.attr,
+ &dev_attr_speed_range.attr,
+ NULL,
+};
+
+static const struct attribute_group claw_rgb_attr_group = {
+ .attrs = claw_rgb_attrs,
+};
+
+static struct mc_subled claw_rgb_subled_info[] = {
+ {
+ .color_index = LED_COLOR_ID_RED,
+ .channel = 0x1,
+ },
+ {
+ .color_index = LED_COLOR_ID_GREEN,
+ .channel = 0x2,
+ },
+ {
+ .color_index = LED_COLOR_ID_BLUE,
+ .channel = 0x3,
+ },
+};
+
static void cfg_setup_fn(struct work_struct *work)
{
struct delayed_work *dwork = container_of(work, struct delayed_work, work);
struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup);
+ bool gamepad_ready = false, rgb_ready = false, gp_registered, rgb_registered;
int ret;
ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE,
NULL, 0, 25);
if (ret) {
dev_err(&drvdata->hdev->dev,
- "Failed to setup device, can't read gamepad mode: %d\n", ret);
- return;
+ "Failed to read gamepad mode: %d\n", ret);
+ goto prep_rgb;
}
+ gamepad_ready = true;
- /* Add sysfs attributes after we get the device state */
- ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+prep_rgb:
+ ret = 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 = true;
+
+ /* Add sysfs attributes after we get the device state */
+try_gamepad:
scoped_guard(spinlock_irqsave, &drvdata->registration_lock)
- /* Pairs with smp_load_acquire in attribute show/store functions */
- smp_store_release(&drvdata->gp_registered, true);
+ /* Pairs with smp_store_release from below */
+ gp_registered = smp_load_acquire(&drvdata->gp_registered);
+
+ if (!gp_registered && gamepad_ready) {
+ ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to create gamepad attrs: %d\n", ret);
+ goto try_rgb;
+ }
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->gp_registered, true);
+ gp_registered = true;
+ }
+ }
- 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 */
+ rgb_registered = smp_load_acquire(&drvdata->rgb_registered);
+
+ if (!rgb_registered && rgb_ready) {
+ ret = led_classdev_multicolor_register(&drvdata->hdev->dev,
+ &drvdata->led_mc);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev, "Failed to create led device: %d\n", ret);
+ goto update_kobjects;
+ }
+
+ ret = device_add_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev, "Failed to create RGB attrs: %d\n", ret);
+ led_classdev_multicolor_unregister(&drvdata->led_mc);
+ goto update_kobjects;
+ }
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->rgb_registered, true);
+ rgb_registered = true;
+ }
+ }
+
+update_kobjects:
+ if (gp_registered)
+ kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
+ if (rgb_registered)
+ kobject_uevent(&drvdata->led_mc.led_cdev.dev->kobj, KOBJ_CHANGE);
}
static void cfg_resume_fn(struct work_struct *work)
@@ -973,8 +1589,10 @@ static void cfg_resume_fn(struct work_struct *work)
struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume);
guard(spinlock_irqsave)(&drvdata->registration_lock);
- /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
- if (!smp_load_acquire(&drvdata->gp_registered))
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered) ||
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ !smp_load_acquire(&drvdata->rgb_registered))
schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
}
@@ -985,18 +1603,24 @@ static void claw_features_supported(struct claw_drvdata *drvdata)
if (major == 0x01) {
drvdata->bmap_support = true;
- if (minor >= 0x66)
+ if (minor >= 0x66) {
drvdata->bmap_addr = button_mapping_addr_new;
- else
+ drvdata->rgb_addr = rgb_addr_new;
+ } else {
drvdata->bmap_addr = button_mapping_addr_old;
+ drvdata->rgb_addr = rgb_addr_old;
+ }
return;
}
if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
drvdata->bmap_support = true;
drvdata->bmap_addr = button_mapping_addr_new;
+ drvdata->rgb_addr = rgb_addr_new;
return;
}
+
+ drvdata->rgb_addr = rgb_addr_old;
}
static int claw_probe(struct hid_device *hdev, u8 ep)
@@ -1011,6 +1635,7 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
return -ENOMEM;
drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT;
+ drvdata->rgb_enabled = true;
drvdata->hdev = hdev;
drvdata->ep = ep;
@@ -1021,6 +1646,18 @@ 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");
+ /* Device is hardwired and name is guaranteed to be unique */
+ drvdata->led_mc.led_cdev.name = "msi_claw:rgb:joystick_rings";
+ drvdata->led_mc.led_cdev.brightness = 0x50;
+ drvdata->led_mc.led_cdev.max_brightness = 0x64;
+ drvdata->led_mc.led_cdev.color = LED_COLOR_ID_RGB;
+ drvdata->led_mc.led_cdev.brightness_set = claw_led_brightness_set;
+ drvdata->led_mc.num_colors = 3;
+ drvdata->led_mc.subled_info = devm_kmemdup(&hdev->dev, claw_rgb_subled_info,
+ sizeof(claw_rgb_subled_info), GFP_KERNEL);
+ if (!drvdata->led_mc.subled_info)
+ return -ENOMEM;
+
mutex_init(&drvdata->cfg_mutex);
mutex_init(&drvdata->profile_mutex);
mutex_init(&drvdata->rom_mutex);
@@ -1028,10 +1665,12 @@ 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->orphan_ack_complete);
init_completion(&drvdata->send_cmd_complete);
INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
+ INIT_DELAYED_WORK(&drvdata->rgb_queue, &claw_rgb_queue_fn);
/* For control interface: open the HID transport for sending commands. */
ret = hid_hw_open(hdev);
@@ -1088,6 +1727,7 @@ static void claw_remove(struct hid_device *hdev)
{
struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
bool gp_registered;
+ bool rgb_registered;
if (!drvdata)
return;
@@ -1100,11 +1740,21 @@ static void claw_remove(struct hid_device *hdev)
gp_registered = smp_load_acquire(&drvdata->gp_registered);
/* Pairs with smp_load_acquire in attribute show/store functions */
smp_store_release(&drvdata->gp_registered, false);
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ rgb_registered = smp_load_acquire(&drvdata->rgb_registered);
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->rgb_registered, false);
}
if (gp_registered)
device_remove_group(&hdev->dev, &claw_gamepad_attr_group);
+ if (rgb_registered) {
+ device_remove_group(drvdata->led_mc.led_cdev.dev, &claw_rgb_attr_group);
+ led_classdev_multicolor_unregister(&drvdata->led_mc);
+ }
+ cancel_delayed_work_sync(&drvdata->rgb_queue);
+
hid_hw_close(hdev);
}
@@ -1164,6 +1814,7 @@ static int claw_suspend(struct hid_device *hdev)
cancel_delayed_work_sync(&drvdata->cfg_resume);
cancel_delayed_work_sync(&drvdata->cfg_setup);
+ cancel_delayed_work_sync(&drvdata->rgb_queue);
return 0;
}
--
2.54.0
^ permalink raw reply related
* [PATCH v13 2/4] HID: hid-msi: Add M-key mapping attributes
From: Derek J. Clark @ 2026-07-20 3:15 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260720031549.2272658-1-derekjohn.clark@gmail.com>
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 <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Link: https://patch.msgid.link/20260529072111.7565-3-derekjohn.clark@gmail.com
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v12:
- On address mismatch, assume stale message return and keep waiting for
correct message.
- Use spinlock_irqsave for raw_event-reachable locks since completion
context isn't guaranteed softirq-only across all HCDs.
v10:
- Remove additional gamepad_registered variable left over after rename
to gp_registered.
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 | 446 +++++++++++++++++++++++++++++++++++++++++-
1 file changed, 445 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
index 6687e4579faa..023d63cee6b0 100644
--- a/drivers/hid/hid-msi.c
+++ b/drivers/hid/hid-msi.c
@@ -43,6 +43,8 @@
#define CLAW_DINPUT_CFG_INTF_IN 0x82
#define CLAW_XINPUT_CFG_INTF_IN 0x83
+#define CLAW_KEYS_MAX 5
+
enum claw_command_index {
CLAW_COMMAND_TYPE_NONE = 0x00,
CLAW_COMMAND_TYPE_READ_PROFILE = 0x04,
@@ -68,6 +70,17 @@ static const char * const claw_gamepad_mode_text[] = {
[CLAW_GAMEPAD_MODE_DESKTOP] = "desktop",
};
+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,
@@ -85,6 +98,155 @@ static const char * const claw_mkeys_function_text[] = {
[CLAW_MKEY_FUNCTION_COMBO] = "combination",
};
+static const struct {
+ u8 code;
+ const char *name;
+} claw_button_mapping_key_map[] = {
+ /* Gamepad buttons */
+ { 0x01, "ABS_HAT0Y_UP" },
+ { 0x02, "ABS_HAT0Y_DOWN" },
+ { 0x03, "ABS_HAT0X_LEFT" },
+ { 0x04, "ABS_HAT0X_RIGHT" },
+ { 0x05, "BTN_TL" },
+ { 0x06, "BTN_TR" },
+ { 0x07, "BTN_THUMBL" },
+ { 0x08, "BTN_THUMBR" },
+ { 0x09, "BTN_SOUTH" },
+ { 0x0a, "BTN_EAST" },
+ { 0x0b, "BTN_NORTH" },
+ { 0x0c, "BTN_WEST" },
+ { 0x0d, "BTN_MODE" },
+ { 0x0e, "BTN_SELECT" },
+ { 0x0f, "BTN_START" },
+ { 0x13, "BTN_TL2"},
+ { 0x14, "BTN_TR2"},
+ { 0x15, "ABS_Y_UP"},
+ { 0x16, "ABS_Y_DOWN"},
+ { 0x17, "ABS_X_LEFT"},
+ { 0x18, "ABS_X_RIGHT"},
+ { 0x19, "ABS_RY_UP"},
+ { 0x1a, "ABS_RY_DOWN"},
+ { 0x1b, "ABS_RX_LEFT"},
+ { 0x1c, "ABS_RX_RIGHT"},
+ /* Keyboard keys */
+ { 0x32, "KEY_ESC" },
+ { 0x33, "KEY_F1" },
+ { 0x34, "KEY_F2" },
+ { 0x35, "KEY_F3" },
+ { 0x36, "KEY_F4" },
+ { 0x37, "KEY_F5" },
+ { 0x38, "KEY_F6" },
+ { 0x39, "KEY_F7" },
+ { 0x3a, "KEY_F8" },
+ { 0x3b, "KEY_F9" },
+ { 0x3c, "KEY_F10" },
+ { 0x3d, "KEY_F11" },
+ { 0x3e, "KEY_F12" },
+ { 0x3f, "KEY_GRAVE" },
+ { 0x40, "KEY_1" },
+ { 0x41, "KEY_2" },
+ { 0x42, "KEY_3" },
+ { 0x43, "KEY_4" },
+ { 0x44, "KEY_5" },
+ { 0x45, "KEY_6" },
+ { 0x46, "KEY_7" },
+ { 0x47, "KEY_8" },
+ { 0x48, "KEY_9" },
+ { 0x49, "KEY_0" },
+ { 0x4a, "KEY_MINUS" },
+ { 0x4b, "KEY_EQUAL" },
+ { 0x4c, "KEY_BACKSPACE" },
+ { 0x4d, "KEY_TAB" },
+ { 0x4e, "KEY_Q" },
+ { 0x4f, "KEY_W" },
+ { 0x50, "KEY_E" },
+ { 0x51, "KEY_R" },
+ { 0x52, "KEY_T" },
+ { 0x53, "KEY_Y" },
+ { 0x54, "KEY_U" },
+ { 0x55, "KEY_I" },
+ { 0x56, "KEY_O" },
+ { 0x57, "KEY_P" },
+ { 0x58, "KEY_LEFTBRACE" },
+ { 0x59, "KEY_RIGHTBRACE" },
+ { 0x5a, "KEY_BACKSLASH" },
+ { 0x5b, "KEY_CAPSLOCK" },
+ { 0x5c, "KEY_A" },
+ { 0x5d, "KEY_S" },
+ { 0x5e, "KEY_D" },
+ { 0x5f, "KEY_F" },
+ { 0x60, "KEY_G" },
+ { 0x61, "KEY_H" },
+ { 0x62, "KEY_J" },
+ { 0x63, "KEY_K" },
+ { 0x64, "KEY_L" },
+ { 0x65, "KEY_SEMICOLON" },
+ { 0x66, "KEY_APOSTROPHE" },
+ { 0x67, "KEY_ENTER" },
+ { 0x68, "KEY_LEFTSHIFT" },
+ { 0x69, "KEY_Z" },
+ { 0x6a, "KEY_X" },
+ { 0x6b, "KEY_C" },
+ { 0x6c, "KEY_V" },
+ { 0x6d, "KEY_B" },
+ { 0x6e, "KEY_N" },
+ { 0x6f, "KEY_M" },
+ { 0x70, "KEY_COMMA" },
+ { 0x71, "KEY_DOT" },
+ { 0x72, "KEY_SLASH" },
+ { 0x73, "KEY_RIGHTSHIFT" },
+ { 0x74, "KEY_LEFTCTRL" },
+ { 0x75, "KEY_LEFTMETA" },
+ { 0x76, "KEY_LEFTALT" },
+ { 0x77, "KEY_SPACE" },
+ { 0x78, "KEY_RIGHTALT" },
+ { 0x79, "KEY_RIGHTCTRL" },
+ { 0x7a, "KEY_INSERT" },
+ { 0x7b, "KEY_HOME" },
+ { 0x7c, "KEY_PAGEUP" },
+ { 0x7d, "KEY_DELETE" },
+ { 0x7e, "KEY_END" },
+ { 0x7f, "KEY_PAGEDOWN" },
+ { 0x8a, "KEY_KPENTER" },
+ { 0x8b, "KEY_KP0" },
+ { 0x8c, "KEY_KP1" },
+ { 0x8d, "KEY_KP2" },
+ { 0x8e, "KEY_KP3" },
+ { 0x8f, "KEY_KP4" },
+ { 0x90, "KEY_KP5" },
+ { 0x91, "KEY_KP6" },
+ { 0x92, "KEY_KP7" },
+ { 0x93, "KEY_KP8" },
+ { 0x94, "KEY_KP9" },
+ { 0x95, "MD_PLAY" },
+ { 0x96, "MD_STOP" },
+ { 0x97, "MD_NEXT" },
+ { 0x98, "MD_PREV" },
+ { 0x99, "MD_VOL_UP" },
+ { 0x9a, "MD_VOL_DOWN" },
+ { 0x9b, "MD_VOL_MUTE" },
+ { 0x9c, "KEY_F23" },
+ /* Mouse events */
+ { 0xc8, "BTN_LEFT" },
+ { 0xc9, "BTN_MIDDLE" },
+ { 0xca, "BTN_RIGHT" },
+ { 0xcb, "BTN_SIDE" },
+ { 0xcc, "BTN_EXTRA" },
+ { 0xcd, "REL_WHEEL_UP" },
+ { 0xce, "REL_WHEEL_DOWN" },
+ { 0xff, "DISABLED" },
+};
+
+static const u16 button_mapping_addr_old[] = {
+ 0x007a, /* M1 */
+ 0x011f, /* M2 */
+};
+
+static const u16 button_mapping_addr_new[] = {
+ 0x00bb, /* M1 */
+ 0x0164, /* M2 */
+};
+
struct claw_command_report {
u8 report_id;
u8 padding[2];
@@ -93,26 +255,48 @@ struct claw_command_report {
u8 data[59];
} __packed;
+struct claw_profile_report {
+ u8 profile;
+ __be16 read_addr;
+} __packed;
+
+struct claw_mkey_report {
+ struct claw_profile_report;
+ u8 padding_0;
+ u8 padding_1;
+ u8 padding_2;
+ u8 codes[5];
+} __packed;
+
struct claw_drvdata {
/* MCU General Variables */
+ enum claw_profile_ack_pending profile_pending;
struct completion orphan_ack_complete;
struct completion send_cmd_complete;
struct delayed_work cfg_resume;
struct delayed_work cfg_setup;
spinlock_t registration_lock; /* Lock for registration read/write */
+ struct mutex profile_mutex; /* mutex for profile_pending calls */
+ spinlock_t profile_lock; /* Lock for profile_pending read/write */
struct hid_device *hdev;
bool orphan_ack_pending;
struct mutex cfg_mutex; /* mutex for synchronous data */
+ struct mutex rom_mutex; /* mutex for SYNC_TO_ROM calls */
spinlock_t cmd_lock; /* Lock for cmd data read/write */
u8 waiting_cmd;
int cmd_status;
+ u16 bcd_device;
u8 ep;
/* Gamepad Variables */
enum claw_mkeys_function_index mkeys_function;
enum claw_gamepad_mode_index gamepad_mode;
+ u8 m1_codes[CLAW_KEYS_MAX];
+ u8 m2_codes[CLAW_KEYS_MAX];
+ const u16 *bmap_addr;
spinlock_t mode_lock; /* Lock for mode data read/write */
bool gp_registered;
+ bool bmap_support;
};
static int get_endpoint_address(struct hid_device *hdev)
@@ -144,6 +328,39 @@ static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
return 0;
}
+static int claw_profile_event(struct claw_drvdata *drvdata, struct claw_command_report *cmd_rep)
+{
+ enum claw_profile_ack_pending profile;
+ struct claw_mkey_report *mkeys;
+ u8 *codes, key;
+ int i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ profile = drvdata->profile_pending;
+
+ switch (profile) {
+ case CLAW_M1_PENDING:
+ case CLAW_M2_PENDING:
+ key = (profile == CLAW_M1_PENDING) ? CLAW_KEY_M1 : CLAW_KEY_M2;
+ mkeys = (struct claw_mkey_report *)cmd_rep->data;
+ if (be16_to_cpu(mkeys->read_addr) != drvdata->bmap_addr[key])
+ return -EAGAIN;
+ codes = (profile == CLAW_M1_PENDING) ? drvdata->m1_codes : drvdata->m2_codes;
+ for (i = 0; i < CLAW_KEYS_MAX; i++)
+ codes[i] = (mkeys->codes[i]);
+ break;
+ default:
+ dev_dbg(&drvdata->hdev->dev,
+ "Got profile event without changes pending from command: %x\n",
+ cmd_rep->cmd);
+ return -EINVAL;
+ }
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = CLAW_NO_PENDING;
+
+ return 0;
+}
+
static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
u8 *data, int size)
{
@@ -170,6 +387,17 @@ static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *repor
complete(&drvdata->send_cmd_complete);
}
+ break;
+ case CLAW_COMMAND_TYPE_READ_PROFILE_ACK:
+ ret = claw_profile_event(drvdata, cmd_rep);
+ /* Stale address received, ignore and keep waiting */
+ if (ret == -EAGAIN)
+ return 0;
+ if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_PROFILE) {
+ drvdata->cmd_status = ret;
+ complete(&drvdata->send_cmd_complete);
+ }
+
break;
case CLAW_COMMAND_TYPE_ACK:
if (drvdata->orphan_ack_pending) {
@@ -499,6 +727,177 @@ static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
}
static DEVICE_ATTR_WO(reset);
+static int mkey_mapping_name_to_code(const char *name)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
+ if (!strcmp(name, claw_button_mapping_key_map[i].name))
+ return claw_button_mapping_key_map[i].code;
+ }
+
+ return -EINVAL;
+}
+
+static const char *mkey_mapping_code_to_name(u8 code)
+{
+ int i;
+
+ if (code == 0xff)
+ return NULL;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++) {
+ if (claw_button_mapping_key_map[i].code == code)
+ return claw_button_mapping_key_map[i].name;
+ }
+
+ return NULL;
+}
+
+static int claw_mkey_store(struct device *dev, const char *buf, u8 mkey)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ struct claw_mkey_report report = { {0x01, cpu_to_be16(drvdata->bmap_addr[mkey])},
+ 0x07, 0x04, 0x00, {0xff, 0xff, 0xff, 0xff, 0xff} };
+ char **raw_keys __free(argv_free) = NULL;
+ int ret, key_count, i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ raw_keys = argv_split(GFP_KERNEL, buf, &key_count);
+ if (!raw_keys)
+ return -ENOMEM;
+
+ if (key_count > CLAW_KEYS_MAX)
+ return -EINVAL;
+
+ if (key_count == 0)
+ goto set_buttons;
+
+ for (i = 0; i < key_count; i++) {
+ ret = mkey_mapping_name_to_code(raw_keys[i]);
+ if (ret < 0)
+ return ret;
+
+ report.codes[i] = ret;
+ }
+
+set_buttons:
+ scoped_guard(mutex, &drvdata->rom_mutex) {
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+ /* MCU will not send ACK until the USB transaction completes. ACK is sent
+ * immediately after and will hit the stale state machine, before the next
+ * command re-arms the state machine. Timeout 0 ensures no deadlock waiting
+ * for ACK that ill never come.
+ */
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SYNC_TO_ROM, NULL, 0, 0);
+ }
+
+ return ret;
+}
+
+static int claw_mkey_show(struct device *dev, char *buf, enum claw_key_index m_key)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ struct claw_mkey_report report = { {0x01, cpu_to_be16(drvdata->bmap_addr[m_key])}, 0x07 };
+ int i, ret, count = 0;
+ const char *name;
+ u8 *codes;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ codes = (m_key == CLAW_KEY_M1) ? drvdata->m1_codes : drvdata->m2_codes;
+
+ guard(mutex)(&drvdata->profile_mutex);
+ scoped_guard(spinlock_irqsave, &drvdata->profile_lock)
+ drvdata->profile_pending = (m_key == CLAW_KEY_M1) ? CLAW_M1_PENDING
+ : CLAW_M2_PENDING;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_PROFILE,
+ (u8 *)&report, sizeof(report), 25);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < CLAW_KEYS_MAX; i++) {
+ name = mkey_mapping_code_to_name(codes[i]);
+ if (name)
+ count += sysfs_emit_at(buf, count, "%s ", name);
+ }
+
+ if (!count)
+ return sysfs_emit(buf, "(not set)\n");
+
+ buf[count - 1] = '\n';
+
+ return count;
+}
+
+static ssize_t button_m1_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+
+ ret = claw_mkey_store(dev, buf, CLAW_KEY_M1);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t button_m1_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return claw_mkey_show(dev, buf, CLAW_KEY_M1);
+}
+static DEVICE_ATTR_RW(button_m1);
+
+static ssize_t button_m2_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+
+ ret = claw_mkey_store(dev, buf, CLAW_KEY_M2);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t button_m2_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ return claw_mkey_show(dev, buf, CLAW_KEY_M2);
+}
+static DEVICE_ATTR_RW(button_m2);
+
+static ssize_t button_mapping_options_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_button_mapping_key_map); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_button_mapping_key_map[i].name);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(button_mapping_options);
+
static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
int n)
{
@@ -511,10 +910,22 @@ static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribu
return 0;
}
- return attr->mode;
+ /* Always show attrs available on all firmware */
+ if (attr == &dev_attr_gamepad_mode.attr ||
+ attr == &dev_attr_gamepad_mode_index.attr ||
+ attr == &dev_attr_mkeys_function.attr ||
+ attr == &dev_attr_mkeys_function_index.attr ||
+ attr == &dev_attr_reset.attr)
+ return attr->mode;
+
+ /* Hide button mapping attrs if it isn't supported */
+ return drvdata->bmap_support ? attr->mode : 0;
}
static struct attribute *claw_gamepad_attrs[] = {
+ &dev_attr_button_m1.attr,
+ &dev_attr_button_m2.attr,
+ &dev_attr_button_mapping_options.attr,
&dev_attr_gamepad_mode.attr,
&dev_attr_gamepad_mode_index.attr,
&dev_attr_mkeys_function.attr,
@@ -567,8 +978,31 @@ static void cfg_resume_fn(struct work_struct *work)
schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
}
+static void claw_features_supported(struct claw_drvdata *drvdata)
+{
+ u8 major = (drvdata->bcd_device >> 8) & 0xff;
+ u8 minor = drvdata->bcd_device & 0xff;
+
+ if (major == 0x01) {
+ drvdata->bmap_support = true;
+ if (minor >= 0x66)
+ drvdata->bmap_addr = button_mapping_addr_new;
+ else
+ drvdata->bmap_addr = button_mapping_addr_old;
+ return;
+ }
+
+ if ((major == 0x02 && minor >= 0x17) || major >= 0x03) {
+ drvdata->bmap_support = true;
+ drvdata->bmap_addr = button_mapping_addr_new;
+ return;
+ }
+}
+
static int claw_probe(struct hid_device *hdev, u8 ep)
{
+ struct usb_interface *intf = to_usb_interface(hdev->dev.parent);
+ struct usb_device *udev = interface_to_usbdev(intf);
struct claw_drvdata *drvdata;
int ret;
@@ -580,10 +1014,20 @@ static int claw_probe(struct hid_device *hdev, u8 ep)
drvdata->hdev = hdev;
drvdata->ep = ep;
+ /* Determine feature level from firmware version */
+ drvdata->bcd_device = le16_to_cpu(udev->descriptor.bcdDevice);
+ claw_features_supported(drvdata);
+
+ if (!drvdata->bmap_support)
+ dev_dbg(&hdev->dev, "M-Key mapping is not supported. Update firmware to enable.\n");
+
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->orphan_ack_complete);
init_completion(&drvdata->send_cmd_complete);
INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
--
2.54.0
^ permalink raw reply related
* [PATCH v13 1/4] HID: hid-msi: Add MSI Claw configuration driver
From: Derek J. Clark @ 2026-07-20 3:15 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
In-Reply-To: <20260720031549.2272658-1-derekjohn.clark@gmail.com>
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 ==
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 <denis.benato@linux.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v13:
- Add pm_ptr() to driver resume pointer.
- Refactor handling of pending acks with no timeout, avoids collisions
by arming a completion to wait on the next command, allowing the URB
to be dropped (so the ACK will fire) while forcing serial events.
v12:
- Fix race condition with mode events. Makes claw_hw_output_report() a
wrapper that holds cfg_mutex and adds __claw_hw_output_report(),
which has the old functionality but uses lockdep_assert_held(), and
claw_switch_mode(), which handles cfg_mutex holding and spinlock
holding for all mode functions, preventing the multiple callers issue.
- Remove goto based cleanup in __claw_hw_output_report(), adds some
duplicated code but avoids anti-pattern for cleanup.
- Use spinlock_irqsave for raw_event-reachable locks since completion
context isn't guaranteed softirq-only across all HCDs.
- Add bool to track if the driver is waiting on a sync ack with no
timeout to prevent those acks from clearing timeout acks.
v9:
- Don't use devm_device_add_group in cfg_setup_fn, do manual adding
and cleanup to prevent possible use after free.
- Use scoped_guard instead of guard in claw_remove.
- Rename gamepad_registered to gp_registered for brevity.
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 | 768 ++++++++++++++++++++++++++++++++++++++++++
5 files changed, 793 insertions(+)
create mode 100644 drivers/hid/hid-msi.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 6f6517bf4f97..8e2de98b768f 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
+MSI HID DRIVER
+M: Derek J. Clark <derekjohn.clark@gmail.com>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-msi.c
+
MSI EC DRIVER
M: Nikita Kravets <teackot@gmail.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 10c12d8e6557..7766676051a5 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
+config HID_MSI
+ tristate "MSI Claw Gamepad Support"
+ depends on USB_HID
+ select NEW_LEDS
+ select LEDS_CLASS
+ select LEDS_CLASS_MULTICOLOR
+ help
+ Support for the MSI Claw RGB and controller configuration
+
+ Say Y here to include configuration interface support for the MSI Claw Line
+ of Handheld Console Controllers. Say M here to compile this driver as a
+ module. The module will be called hid-msi.
+
config HID_KEYTOUCH
tristate "Keytouch HID devices"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 07dfdb6a49c5..80925a17b059 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -92,6 +92,7 @@ obj-$(CONFIG_HID_MAYFLASH) += hid-mf.o
obj-$(CONFIG_HID_MEGAWORLD_FF) += hid-megaworld.o
obj-$(CONFIG_HID_MICROSOFT) += hid-microsoft.o
obj-$(CONFIG_HID_MONTEREY) += hid-monterey.o
+obj-$(CONFIG_HID_MSI) += hid-msi.o
obj-$(CONFIG_HID_MULTITOUCH) += hid-multitouch.o
obj-$(CONFIG_HID_NINTENDO) += hid-nintendo.o
obj-$(CONFIG_HID_NTI) += hid-nti.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 933b7943bdb5..94a9b89dc240 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
#define USB_VENDOR_ID_MSI 0x1770
+#define USB_VENDOR_ID_MSI_2 0x0db0
#define USB_DEVICE_ID_MSI_GT683R_LED_PANEL 0xff00
+#define USB_DEVICE_ID_MSI_CLAW_XINPUT 0x1901
+#define USB_DEVICE_ID_MSI_CLAW_DINPUT 0x1902
+#define USB_DEVICE_ID_MSI_CLAW_DESKTOP 0x1903
+#define USB_DEVICE_ID_MSI_CLAW_BIOS 0x1904
#define USB_VENDOR_ID_NATIONAL_SEMICONDUCTOR 0x0400
#define USB_DEVICE_ID_N_S_HARMONY 0xc359
diff --git a/drivers/hid/hid-msi.c b/drivers/hid/hid-msi.c
new file mode 100644
index 000000000000..6687e4579faa
--- /dev/null
+++ b/drivers/hid/hid-msi.c
@@ -0,0 +1,768 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for MSI Claw Handheld PC gamepads.
+ *
+ * Provides configuration support for the MSI Claw series of handheld PC
+ * gamepads. Multiple iterations of the device firmware has led to some
+ * quirks for how certain attributes are handled. The original firmware
+ * did not support remapping of the M1 (right) and M2 (left) rear paddles.
+ * Additionally, the MCU RAM address for writing configuration data has
+ * changed twice. Checks are done during probe to enumerate these variances.
+ *
+ * Copyright (c) 2026 Zhouwang Huang <honjow311@gmail.com>
+ * Copyright (c) 2026 Denis Benato <denis.benato@linux.dev>
+ * Copyright (c) 2026 Valve Corporation
+ */
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/completion.h>
+#include <linux/container_of.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/kobject.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm.h>
+#include <linux/spinlock.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define CLAW_OUTPUT_REPORT_ID 0x0f
+#define CLAW_INPUT_REPORT_ID 0x10
+
+#define CLAW_PACKET_SIZE 64
+
+#define CLAW_DINPUT_CFG_INTF_IN 0x82
+#define CLAW_XINPUT_CFG_INTF_IN 0x83
+
+enum claw_command_index {
+ CLAW_COMMAND_TYPE_NONE = 0x00,
+ CLAW_COMMAND_TYPE_READ_PROFILE = 0x04,
+ CLAW_COMMAND_TYPE_READ_PROFILE_ACK = 0x05,
+ CLAW_COMMAND_TYPE_ACK = 0x06,
+ CLAW_COMMAND_TYPE_WRITE_PROFILE_DATA = 0x21,
+ CLAW_COMMAND_TYPE_SYNC_TO_ROM = 0x22,
+ CLAW_COMMAND_TYPE_SWITCH_MODE = 0x24,
+ CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE = 0x26,
+ CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK = 0x27,
+ CLAW_COMMAND_TYPE_RESET_DEVICE = 0x28,
+};
+
+enum claw_gamepad_mode_index {
+ CLAW_GAMEPAD_MODE_XINPUT = 0x01,
+ CLAW_GAMEPAD_MODE_DINPUT = 0x02,
+ CLAW_GAMEPAD_MODE_DESKTOP = 0x04,
+};
+
+static const char * const claw_gamepad_mode_text[] = {
+ [CLAW_GAMEPAD_MODE_XINPUT] = "xinput",
+ [CLAW_GAMEPAD_MODE_DINPUT] = "dinput",
+ [CLAW_GAMEPAD_MODE_DESKTOP] = "desktop",
+};
+
+enum claw_mkeys_function_index {
+ CLAW_MKEY_FUNCTION_MACRO,
+ CLAW_MKEY_FUNCTION_DISABLED,
+ CLAW_MKEY_FUNCTION_COMBO,
+};
+
+enum claw_mode_field {
+ CLAW_FIELD_GAMEPAD_MODE,
+ CLAW_FIELD_MKEYS_FUNCTION,
+};
+
+static const char * const claw_mkeys_function_text[] = {
+ [CLAW_MKEY_FUNCTION_MACRO] = "macro",
+ [CLAW_MKEY_FUNCTION_DISABLED] = "disabled",
+ [CLAW_MKEY_FUNCTION_COMBO] = "combination",
+};
+
+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 orphan_ack_complete;
+ struct completion send_cmd_complete;
+ struct delayed_work cfg_resume;
+ struct delayed_work cfg_setup;
+ spinlock_t registration_lock; /* Lock for registration read/write */
+ struct hid_device *hdev;
+ bool orphan_ack_pending;
+ struct mutex cfg_mutex; /* mutex for synchronous data */
+ 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;
+ spinlock_t mode_lock; /* Lock for mode data read/write */
+ bool gp_registered;
+};
+
+static int get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_host_endpoint *ep;
+ struct usb_interface *intf;
+
+ intf = to_usb_interface(hdev->dev.parent);
+ ep = intf->cur_altsetting->endpoint;
+ if (ep)
+ return ep->desc.bEndpointAddress;
+
+ return -ENODEV;
+}
+
+static int claw_gamepad_mode_event(struct claw_drvdata *drvdata,
+ struct claw_command_report *cmd_rep)
+{
+ if (cmd_rep->data[0] >= ARRAY_SIZE(claw_gamepad_mode_text) ||
+ !claw_gamepad_mode_text[cmd_rep->data[0]] ||
+ cmd_rep->data[1] >= ARRAY_SIZE(claw_mkeys_function_text))
+ return -EINVAL;
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock) {
+ drvdata->gamepad_mode = cmd_rep->data[0];
+ drvdata->mkeys_function = cmd_rep->data[1];
+ }
+
+ return 0;
+}
+
+static int claw_raw_event(struct claw_drvdata *drvdata, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct claw_command_report *cmd_rep;
+ int ret = 0;
+
+ if (size != CLAW_PACKET_SIZE)
+ return 0;
+
+ cmd_rep = (struct claw_command_report *)data;
+
+ if (cmd_rep->report_id != CLAW_INPUT_REPORT_ID || cmd_rep->header_tail != 0x3c)
+ return 0;
+
+ dev_dbg(&drvdata->hdev->dev, "Rx data as raw input report: [%*ph]\n",
+ CLAW_PACKET_SIZE, data);
+
+ guard(spinlock_irqsave)(&drvdata->cmd_lock);
+ switch (cmd_rep->cmd) {
+ case CLAW_COMMAND_TYPE_GAMEPAD_MODE_ACK:
+ ret = claw_gamepad_mode_event(drvdata, cmd_rep);
+ if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE) {
+ drvdata->cmd_status = ret;
+ complete(&drvdata->send_cmd_complete);
+ }
+
+ break;
+ case CLAW_COMMAND_TYPE_ACK:
+ if (drvdata->orphan_ack_pending) {
+ drvdata->orphan_ack_pending = false;
+ complete(&drvdata->orphan_ack_complete);
+ break;
+ }
+
+ if (drvdata->waiting_cmd == CLAW_COMMAND_TYPE_NONE) {
+ dev_warn(&drvdata->hdev->dev, "Got unexpected ACK from MCU, ignoring\n");
+ break;
+ }
+
+ drvdata->cmd_status = 0;
+ complete(&drvdata->send_cmd_complete);
+
+ dev_dbg(&drvdata->hdev->dev, "Waiting CMD: %x\n", drvdata->waiting_cmd);
+
+ break;
+ default:
+ dev_dbg(&drvdata->hdev->dev, "Unknown command: %x\n", cmd_rep->cmd);
+ return 0;
+ }
+
+ return ret;
+}
+
+static int msi_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata || (drvdata->ep != CLAW_XINPUT_CFG_INTF_IN &&
+ drvdata->ep != CLAW_DINPUT_CFG_INTF_IN))
+ return 0;
+
+ return claw_raw_event(drvdata, report, data, size);
+}
+
+/* Caller must hold drvdata->cfg_mutex. */
+static int __claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
+ size_t len, unsigned int timeout)
+{
+ unsigned char *dmabuf __free(kfree) = NULL;
+ u8 header[] = { CLAW_OUTPUT_REPORT_ID, 0, 0, 0x3c, index };
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ size_t header_size = ARRAY_SIZE(header);
+ bool orphaned;
+ int ret;
+
+ lockdep_assert_held(&drvdata->cfg_mutex);
+
+ /* If expecting an orphan ack, hold next event until MCU has time to clear it */
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock)
+ orphaned = drvdata->orphan_ack_pending;
+
+ if (orphaned) {
+ wait_for_completion_timeout(&drvdata->orphan_ack_complete, msecs_to_jiffies(25));
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock)
+ drvdata->orphan_ack_pending = false;
+ }
+
+ if (header_size + len > CLAW_PACKET_SIZE)
+ return -EINVAL;
+
+ /* We can't use a devm_alloc reusable buffer without side effects during suspend */
+ dmabuf = kzalloc(CLAW_PACKET_SIZE, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ memcpy(dmabuf, header, header_size);
+ if (data && len)
+ memcpy(dmabuf + header_size, data, len);
+
+ reinit_completion(&drvdata->send_cmd_complete);
+
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
+ if (timeout) {
+ drvdata->waiting_cmd = index;
+ drvdata->cmd_status = -ETIMEDOUT;
+ } else {
+ reinit_completion(&drvdata->orphan_ack_complete);
+ drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
+ drvdata->orphan_ack_pending = true;
+ }
+ }
+
+ dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n",
+ CLAW_PACKET_SIZE, dmabuf);
+
+ ret = hid_hw_output_report(hdev, dmabuf, CLAW_PACKET_SIZE);
+ if (ret < 0)
+ goto err;
+
+ ret = ret == CLAW_PACKET_SIZE ? 0 : -EIO;
+ if (ret)
+ goto err;
+
+ if (timeout) {
+ ret = wait_for_completion_interruptible_timeout(&drvdata->send_cmd_complete,
+ msecs_to_jiffies(timeout));
+
+ dev_dbg(&hdev->dev, "Remaining timeout: %u\n", ret);
+ ret = ret > 0 ? drvdata->cmd_status : ret ?: -EBUSY;
+ if (ret)
+ goto err;
+ }
+
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock)
+ drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
+
+ return ret;
+
+err:
+ scoped_guard(spinlock_irqsave, &drvdata->cmd_lock) {
+ drvdata->waiting_cmd = CLAW_COMMAND_TYPE_NONE;
+ drvdata->orphan_ack_pending = false;
+ }
+ return ret;
+}
+
+static int claw_hw_output_report(struct hid_device *hdev, u8 index, u8 *data,
+ size_t len, unsigned int timeout)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ guard(mutex)(&drvdata->cfg_mutex);
+ return __claw_hw_output_report(hdev, index, data, len, timeout);
+}
+
+static int claw_switch_mode(struct hid_device *hdev, enum claw_mode_field field, u8 val)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ u8 data[2];
+
+ guard(mutex)(&drvdata->cfg_mutex);
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock) {
+ switch (field) {
+ case CLAW_FIELD_GAMEPAD_MODE:
+ data[0] = val;
+ data[1] = drvdata->mkeys_function;
+ break;
+ case CLAW_FIELD_MKEYS_FUNCTION:
+ data[0] = drvdata->gamepad_mode;
+ data[1] = val;
+ break;
+ }
+ }
+
+ return __claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_SWITCH_MODE, data,
+ ARRAY_SIZE(data), 0);
+}
+
+static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int i, ret = -EINVAL;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
+ if (claw_gamepad_mode_text[i] && sysfs_streq(buf, claw_gamepad_mode_text[i])) {
+ ret = i;
+ break;
+ }
+ }
+ if (ret < 0)
+ return ret;
+
+ ret = claw_switch_mode(hdev, CLAW_FIELD_GAMEPAD_MODE, ret);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t gamepad_mode_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret, i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
+ i = drvdata->gamepad_mode;
+
+ if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
+ return sysfs_emit(buf, "unsupported\n");
+
+ return sysfs_emit(buf, "%s\n", claw_gamepad_mode_text[i]);
+}
+static DEVICE_ATTR_RW(gamepad_mode);
+
+static ssize_t gamepad_mode_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(claw_gamepad_mode_text); i++) {
+ if (!claw_gamepad_mode_text[i] || claw_gamepad_mode_text[i][0] == '\0')
+ continue;
+ count += sysfs_emit_at(buf, count, "%s ", claw_gamepad_mode_text[i]);
+ }
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(gamepad_mode_index);
+
+static ssize_t mkeys_function_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int i, ret = -EINVAL;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++) {
+ if (claw_mkeys_function_text[i] && sysfs_streq(buf, claw_mkeys_function_text[i])) {
+ ret = i;
+ break;
+ }
+ }
+ if (ret < 0)
+ return ret;
+
+ ret = claw_switch_mode(hdev, CLAW_FIELD_MKEYS_FUNCTION, ret);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t mkeys_function_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret, i;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE, NULL, 0, 25);
+ if (ret)
+ return ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->mode_lock)
+ i = drvdata->mkeys_function;
+
+ if (i >= ARRAY_SIZE(claw_mkeys_function_text))
+ return sysfs_emit(buf, "unsupported\n");
+
+ return sysfs_emit(buf, "%s\n", claw_mkeys_function_text[i]);
+}
+static DEVICE_ATTR_RW(mkeys_function);
+
+static ssize_t mkeys_function_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ int i, count = 0;
+
+ for (i = 0; i < ARRAY_SIZE(claw_mkeys_function_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", claw_mkeys_function_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(mkeys_function_index);
+
+static ssize_t reset_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ bool val;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ return -ENODEV;
+ }
+
+ ret = kstrtobool(buf, &val);
+ if (ret)
+ return ret;
+
+ if (!val)
+ return -EINVAL;
+
+ ret = claw_hw_output_report(hdev, CLAW_COMMAND_TYPE_RESET_DEVICE, NULL, 0, 0);
+ if (ret)
+ return ret;
+
+ return count;
+}
+static DEVICE_ATTR_WO(reset);
+
+static umode_t claw_gamepad_attr_is_visible(struct kobject *kobj, struct attribute *attr,
+ int n)
+{
+ struct hid_device *hdev = to_hid_device(kobj_to_dev(kobj));
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata) {
+ dev_warn(&hdev->dev,
+ "Failed to get drvdata from kobj. Gamepad attributes are not available.\n");
+ return 0;
+ }
+
+ return attr->mode;
+}
+
+static struct attribute *claw_gamepad_attrs[] = {
+ &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 = {
+ .attrs = claw_gamepad_attrs,
+ .is_visible = claw_gamepad_attr_is_visible,
+};
+
+static void cfg_setup_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_setup);
+ int ret;
+
+ ret = claw_hw_output_report(drvdata->hdev, CLAW_COMMAND_TYPE_READ_GAMEPAD_MODE,
+ NULL, 0, 25);
+ if (ret) {
+ dev_err(&drvdata->hdev->dev,
+ "Failed to setup device, can't read gamepad mode: %d\n", ret);
+ return;
+ }
+
+ /* Add sysfs attributes after we get the device state */
+ ret = device_add_group(&drvdata->hdev->dev, &claw_gamepad_attr_group);
+ 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->gp_registered, true);
+
+ kobject_uevent(&drvdata->hdev->dev.kobj, KOBJ_CHANGE);
+}
+
+static void cfg_resume_fn(struct work_struct *work)
+{
+ struct delayed_work *dwork = container_of(work, struct delayed_work, work);
+ struct claw_drvdata *drvdata = container_of(dwork, struct claw_drvdata, cfg_resume);
+
+ guard(spinlock_irqsave)(&drvdata->registration_lock);
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ if (!smp_load_acquire(&drvdata->gp_registered))
+ 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 = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
+ if (!drvdata)
+ return -ENOMEM;
+
+ drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT;
+ drvdata->hdev = hdev;
+ drvdata->ep = 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->orphan_ack_complete);
+ init_completion(&drvdata->send_cmd_complete);
+ INIT_DELAYED_WORK(&drvdata->cfg_resume, &cfg_resume_fn);
+ INIT_DELAYED_WORK(&drvdata->cfg_setup, &cfg_setup_fn);
+
+ /* For control interface: open the HID transport for sending commands. */
+ ret = hid_hw_open(hdev);
+ if (ret)
+ return ret;
+
+ hid_set_drvdata(hdev, drvdata);
+ schedule_delayed_work(&drvdata->cfg_setup, msecs_to_jiffies(500));
+
+ return 0;
+}
+
+static int msi_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ int ret;
+ u8 ep;
+
+ if (!hid_is_usb(hdev)) {
+ ret = -ENODEV;
+ goto err_probe;
+ }
+
+ ret = hid_parse(hdev);
+ if (ret)
+ goto err_probe;
+
+ /* Set quirk to create separate input devices per HID application */
+ hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ goto err_probe;
+
+ /* For non-control interfaces (keyboard/mouse), allow userspace to grab the devices. */
+ ret = get_endpoint_address(hdev);
+ if (ret < 0)
+ goto err_stop_hw;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN) {
+ ret = claw_probe(hdev, ep);
+ if (ret)
+ goto err_stop_hw;
+ }
+
+ return 0;
+
+err_stop_hw:
+ hid_hw_stop(hdev);
+err_probe:
+ return dev_err_probe(&hdev->dev, ret, "Failed to init device\n");
+}
+
+static void claw_remove(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+ bool gp_registered;
+
+ if (!drvdata)
+ return;
+
+ cancel_delayed_work_sync(&drvdata->cfg_resume);
+ cancel_delayed_work_sync(&drvdata->cfg_setup);
+
+ scoped_guard(spinlock_irqsave, &drvdata->registration_lock) {
+ /* Pairs with smp_store_release from cfg_setup_fn in system_wq context */
+ gp_registered = smp_load_acquire(&drvdata->gp_registered);
+ /* Pairs with smp_load_acquire in attribute show/store functions */
+ smp_store_release(&drvdata->gp_registered, false);
+ }
+
+ if (gp_registered)
+ device_remove_group(&hdev->dev, &claw_gamepad_attr_group);
+
+ hid_hw_close(hdev);
+}
+
+static void msi_remove(struct hid_device *hdev)
+{
+ int ret;
+ u8 ep;
+
+ /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ goto hw_stop;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ claw_remove(hdev);
+
+hw_stop:
+ hid_hw_stop(hdev);
+}
+
+static int claw_resume(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata)
+ return -ENODEV;
+
+ /* MCU can take up to 500ms to be ready after resume */
+ schedule_delayed_work(&drvdata->cfg_resume, msecs_to_jiffies(500));
+ return 0;
+}
+
+static int msi_resume(struct hid_device *hdev)
+{
+ int ret;
+ u8 ep;
+
+ /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ return 0;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ return claw_resume(hdev);
+
+ return 0;
+}
+
+static int claw_suspend(struct hid_device *hdev)
+{
+ struct claw_drvdata *drvdata = hid_get_drvdata(hdev);
+
+ if (!drvdata)
+ return -ENODEV;
+
+ cancel_delayed_work_sync(&drvdata->cfg_resume);
+ cancel_delayed_work_sync(&drvdata->cfg_setup);
+
+ return 0;
+}
+
+static int msi_suspend(struct hid_device *hdev, pm_message_t msg)
+{
+ int ret;
+ u8 ep;
+
+ /* Safe assumption. SET_INTERFACE ioctl can't be used while driver is bound */
+ ret = get_endpoint_address(hdev);
+ if (ret <= 0)
+ return 0;
+
+ ep = ret;
+ if (ep == CLAW_XINPUT_CFG_INTF_IN || ep == CLAW_DINPUT_CFG_INTF_IN)
+ return claw_suspend(hdev);
+
+ return 0;
+}
+
+static const struct hid_device_id msi_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_XINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DINPUT) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_DESKTOP) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_MSI_2, USB_DEVICE_ID_MSI_CLAW_BIOS) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, msi_devices);
+
+static struct hid_driver msi_driver = {
+ .name = "hid-msi",
+ .id_table = msi_devices,
+ .raw_event = msi_raw_event,
+ .probe = msi_probe,
+ .remove = msi_remove,
+ .resume = pm_ptr(msi_resume),
+ .suspend = pm_ptr(msi_suspend),
+};
+module_hid_driver(msi_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Denis Benato <denis.benato@linux.dev>");
+MODULE_AUTHOR("Zhouwang Huang <honjow311@gmail.com>");
+MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
+MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads");
--
2.54.0
^ permalink raw reply related
* [PATCH v13 0/4] Add MSI Claw HID Configuration Driver
From: Derek J. Clark @ 2026-07-20 3:15 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Denis Benato, Zhouwang Huang,
Derek J . Clark, linux-input, linux-doc, linux-kernel
This series adds an HID Configuration driver for the MSI Claw line of
Handheld Gaming PC's. The MSI Claw HID interface provides multiple
features, such as the ability to switch between xinput, dinput, and a
desktop mode, RGB control, rumble intensity, and mapping of the rear "M"
keys. There are additional gamepad modes that are not included in this
driver as they appear to be used in assembly line testing or are
incomplete in the firmware. During my testing I found them to be unstable.
The initial version of this driver 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 and additional features. Finally, I
refactored the entire driver, fixed multiple bugs, and refined the overall
format to conform to kernel driver best practices and style guide.
Claude was used initially by Zhouwang Huang to quickly parse HID captures
during the reverse-engineering of some of the features. Since Claude had
already been used, as a test of its capabilities I had it implement the
rumble intensity attribute after I had already rewritten most of the
driver, which I then manually edited to fix some mistakes. I also used
Claude to review the driver and these patches for any mistakes and bugs.
Assisted-by: Claude:claude-sonnet-4-6
Co-developed-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Zhouwang Huang <honjow311@gmail.com>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
v13:
- Add pm_ptr() to driver resume pointer.
- Refactor handling of pending acks with no timeout, avoids collisions
by arming a completion to wait on the next command, allowing the URB
to be dropped (so the ACK will fire) while forcing serial events.
v12: https://lore.kernel.org/linux-input/20260714102640.18075-1-derekjohn.clark@gmail.com/
- Fix race condition with mode events. Makes claw_hw_output_report() a
wrapper that holds cfg_mutex and adds __claw_hw_output_report(),
which has the old functionality but uses lockdep_assert_held(), and
claw_switch_mode(), which handles cfg_mutex holding and spinlock
holding for all mode functions, preventing the multiple callers issue.
- Remove goto based cleanup in __claw_hw_output_report(), adds some
duplicated code but avoids anti-pattern for cleanup.
- Use spinlock_irqsave for raw_event-reachable locks since completion
context isn't guaranteed softirq-only across all HCDs.
- Add bool to track if the driver is waiting on a sync ack with no
timeout to prevent those acks from clearing timeout acks.
- On address mismatch, assume stale message return and keep waiting for
correct message.
- On remove, cancel the rgb_queue last to avoid a re-arming, and in
rgb_queue_fn check rgb_registered to avoid running after the sysfs
group has been removed.
- Add note to led_cdev.name to (hopefully) silence sashiko-bot about
an impossible double device collision.
- Explicitly check if the current address is less than the rgb_address
to avoid invalid overflow math.
v11: https://lore.kernel.org/linux-input/20260529072111.7565-1-derekjohn.clark@gmail.com/#r
- Restore dropped changes from v10.
v10: https://lore.kernel.org/linux-input/20260527222122.10620-1-derekjohn.clark@gmail.com/
- Remove additional gamepad_registered variable left over after rename
to gp_registered.
v9: https://lore.kernel.org/linux-input/20260525020543.519082-1-derekjohn.clark@gmail.com/
- Don't use devm_* functions in cfg_setup_fn, do manual adding
and cleanup to prevent possible use after free.
- Use scoped_guard instead of guard in claw_remove.
- Rename gamepad_registered to gp_registered for brevity.
- Check for drvdata in rgb_queue_fn to avoid use after free during
teardown.
- Ensure rgb_queue work is canceled during suspend.
- Limit guard usage in cfg_setup_fn to avoid holding a lock during
registration and add group events.
v8: https://lore.kernel.org/linux-input/20260522015518.1111290-1-derekjohn.clark@gmail.com/
- 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.
- 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.
- Ensure led_classdev is unregistered if adding attribute group fails.
- Reorder remove actions to ensure no use-after free or rearming cleared
flags.
v7: https://lore.kernel.org/linux-input/20260520013158.3633277-1-derekjohn.clark@gmail.com/
- Use smp_[store_release|load_acquire] pattern for checking
gamepad_registered and rgb_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.
- Add profile_lock for read/write profile_pending.
- Use struct for mkey reports and rumble reports, following the
pattern established by rgb reports previously.
- Add gating to cfg_setup_fn, allowing either gamepad settings or rgb
settings to populate if the other fails for any reason.
- Match on write address for rumble and mkey reports to prevent late
ACK from causing synchronization errors.
v6: https://lore.kernel.org/linux-input/20260518222935.1802071-1-derekjohn.clark@gmail.com/
- 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 or
rgb_registered, enabling use of devm_device_add_group and ending
the need to hold a mutex during remove.
- 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.
- Re-arm cfg_setup in resume if it was canceled in an early suspend.
- Remove duplicated argv_free macro.
- Add spinlock_irqsave vice mutex for read/write access on attribute
variables.
v5: https://lore.kernel.org/linux-input/20260517013925.3120314-1-derekjohn.clark@gmail.com/
- Swap disabled & combination mkeys_function enum values.
- Fix bug introduced in v5 where claw_buttons_store would return
-EINVAL on all valid key entries.
- Ensure mode_mutex is properly init.
- Ensure claw_remove is calling hid_hw_close and not hid_hw_stop for
all paths.
- 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.
- 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: https://lore.kernel.org/linux-input/20260516042841.500299-1-derekjohn.clark@gmail.com/
- 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.
- 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.
- Fix frame_calc validity check to use >=.
- 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: https://lore.kernel.org/linux-input/20260515033622.2095277-1-derekjohn.clark@gmail.com/
- Add mutex for read/write if rgb frame data.
- Ensure claw_hw_output_report is properly guarded.
- 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).
- Use scoped_guard where necessary.
- 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: https://lore.kernel.org/linux-input/20260513231445.3213501-1-derekjohn.clark@gmail.com/
- Use mutexes to guard SYNC_TO_ROM calls and pending_profile calls.
- Rename driver to hid-msi and add generic entrypoints for
probe/resume/remove that call claw specific functions in order to
future proof the driver for other MSI HID interfaces.
- Fix various bugs and formatting issues.
v1: https://lore.kernel.org/linux-input/20260510043510.442807-1-derekjohn.clark@gmail.com/
Derek J. Clark (4):
HID: hid-msi: Add MSI Claw configuration driver
HID: hid-msi: Add M-key mapping attributes
HID: hid-msi: Add RGB control interface
HID: hid-msi: Add Rumble Intensity Attributes
MAINTAINERS | 6 +
drivers/hid/Kconfig | 13 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 5 +
drivers/hid/hid-msi.c | 2054 +++++++++++++++++++++++++++++++++++++++++
5 files changed, 2079 insertions(+)
create mode 100644 drivers/hid/hid-msi.c
--
2.54.0
^ permalink raw reply
* RE: [PATCH v2] HID: intel-thc-hid: intel-quickspi: validate report size before copy
From: Xu, Even @ 2026-07-20 2:02 UTC (permalink / raw)
To: HyeongJun An, Sun, Xinpeng, Jiri Kosina, Benjamin Tissoires
Cc: linux-input@vger.kernel.org, linux-kernel@vger.kernel.org,
stable@vger.kernel.org
In-Reply-To: <20260717091622.1744196-1-sammiee5311@gmail.com>
> -----Original Message-----
> From: HyeongJun An <sammiee5311@gmail.com>
> Sent: Friday, July 17, 2026 5:16 PM
> To: Xu, Even <even.xu@intel.com>; Sun, Xinpeng <xinpeng.sun@intel.com>; Jiri
> Kosina <jikos@kernel.org>; Benjamin Tissoires <bentiss@kernel.org>
> Cc: linux-input@vger.kernel.org; linux-kernel@vger.kernel.org;
> stable@vger.kernel.org; HyeongJun An <sammiee5311@gmail.com>
> Subject: [PATCH v2] HID: intel-thc-hid: intel-quickspi: validate report size before
> copy
>
> write_cmd_to_txdma() builds an output report in qsdev->report_buf, a heap
> buffer allocated in quickspi_alloc_report_buf() to the device-descriptor derived
> max_report_len (a few hundred bytes for a touch controller). It copies the caller-
> supplied report into that buffer:
>
> memcpy(write_buf->content, report_buf, report_buf_len);
>
> The HID core caps a report at HID_MAX_BUFFER_SIZE (16384) by default, and
> quickspi_hid_ll_driver does not set max_buffer_size, so the length reaches the
> driver unbounded. A hidraw SET_REPORT/SET_FEATURE ioctl carrying a report
> larger than max_report_len therefore overflows report_buf with attacker-
> controlled length and content.
>
> Record the report_buf allocation size and reject reports that do not fit before
> copying, matching the equivalent guard in the intel-quicki2c sibling
> (quicki2c_init_write_buf()) and the hid-goodix-spi fix.
>
> write_cmd_to_txdma() writes the output report header ahead of the content in
> the same buffer, so size the allocation to cover the header as well.
> That keeps the added bound from rejecting a maximum-sized report.
>
> Fixes: 9d8d51735a3a ("HID: intel-thc-hid: intel-quickspi: Add HIDSPI protocol
> implementation")
> Cc: stable@vger.kernel.org
> Assisted-by: Claude:claude-opus-4-8
> Signed-off-by: HyeongJun An <sammiee5311@gmail.com>
> ---
> v2: Size report_buf to cover the output report header as well, so the added
> bound cannot reject a valid maximum-sized report (raised by the Sashiko
> AI review of v1). No other change.
>
> v1: https://lore.kernel.org/all/20260628133717.941389-1-
> sammiee5311@gmail.com/
>
> drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c | 9 ++++++++-
> drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h | 1 + .../hid/intel-thc-
> hid/intel-quickspi/quickspi-protocol.c | 3 +++
> 3 files changed, 12 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
> b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
> index 4ae2e1718b30..da5ecfcd0fbf 100644
> --- a/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
> +++ b/drivers/hid/intel-thc-hid/intel-quickspi/pci-quickspi.c
> @@ -555,7 +555,14 @@ static int quickspi_alloc_report_buf(struct
> quickspi_device *qsdev)
> max_report_len = max(le16_to_cpu(qsdev->dev_desc.max_output_len),
> le16_to_cpu(qsdev->dev_desc.max_input_len));
>
> - qsdev->report_buf = devm_kzalloc(qsdev->dev, max_report_len,
> GFP_KERNEL);
> + /*
> + * write_cmd_to_txdma() writes the output report header ahead of the
> + * content in this buffer, so it has to hold both.
> + */
> + qsdev->report_buf_size =
> HIDSPI_OUTPUT_REPORT_SIZE(max_report_len);
> +
> + qsdev->report_buf = devm_kzalloc(qsdev->dev, qsdev->report_buf_size,
> + GFP_KERNEL);
> if (!qsdev->report_buf)
> return -ENOMEM;
>
> diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
> b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
> index bf5e18f5a5f4..0ed964bfe3dd 100644
> --- a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
> +++ b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-dev.h
> @@ -157,6 +157,7 @@ struct quickspi_device {
> u8 *report_descriptor;
> u8 *input_buf;
> u8 *report_buf;
> + u32 report_buf_size;
> u32 report_len;
>
> wait_queue_head_t reset_ack_wq;
> diff --git a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.c
> b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.c
> index cb19057f1191..db6054843e77 100644
> --- a/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.c
> +++ b/drivers/hid/intel-thc-hid/intel-quickspi/quickspi-protocol.c
> @@ -30,6 +30,9 @@ static int write_cmd_to_txdma(struct quickspi_device
> *qsdev,
>
> write_buf = (struct output_report *)qsdev->report_buf;
>
> + if (HIDSPI_OUTPUT_REPORT_SIZE(report_buf_len) > qsdev-
> >report_buf_size)
> + return -EINVAL;
> +
Thanks for the patch!
Reviewed-by: Even Xu <even.xu@intel.com>
> write_buf->output_hdr.report_type = report_type;
> write_buf->output_hdr.content_len = cpu_to_le16(report_buf_len);
> write_buf->output_hdr.content_id = report_id;
> --
> 2.43.0
^ permalink raw reply
* Re: [PATCH v4] Input: gunze: replace deprecated APIs and fix warning style
From: Dmitry Torokhov @ 2026-07-20 1:24 UTC (permalink / raw)
To: Bivash Kumar Singh; +Cc: linux-input
In-Reply-To: <20260719173700.4942-1-bivashraj750@gmail.com>
On Sun, Jul 19, 2026 at 11:07:00PM +0530, Bivash Kumar Singh wrote:
> Replace two uses of simple_strtoul() with kstrtoul() as the former
> is deprecated. kstrtoul() requires a NUL-terminated string, so copy
> the packet data into a local buffer and NUL-terminate the X and Y
> fields before parsing. Initialize x and y to 0 so that if parsing
> fails the coordinates fall back to 0 rather than reporting garbage,
> and BTN_TOUCH is always updated regardless of parse result.
Please stop forcing use of kstrtoul() where it is not fit. If you want
to stop using simple_strtoul() then add '\0' when receive '\r' in
gunze_interrupt(), and use
if (sscanf(gunze->data + 1, "%4u,%4u", &x, &y) != 2) {
dev_warn_ratelimited(...);
return;
}
Thanks.
--
Dmitry
^ permalink raw reply
* Re: [PATCH v4] Input: gunze: replace deprecated APIs and fix warning style
From: sashiko-bot @ 2026-07-19 17:54 UTC (permalink / raw)
To: Bivash Kumar Singh; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260719173700.4942-1-bivashraj750@gmail.com>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
New issues:
- [High] Un-ratelimited dev_warn() calls in the interrupt handler lead to severe kernel log spam (dmesg flooding).
- [High] Replacing simple_strtoul() with kstrtoul() causes a behavioral regression due to strict parsing requirements.
Pre-existing issues:
- [High] Unconditional reporting of fallback coordinates on parse failure causes out-of-bounds values and cursor jumps.
--
--- Patch [1]: [PATCH v4] Input: gunze: replace deprecated APIs and fix warning style ---
commit 7f14b2c0978a817cc80d8103302866bba7c3bf3f
Author: Bivash Kumar Singh <bivashraj750@gmail.com>
Input: gunze: replace deprecated APIs and fix warning style
This commit replaces simple_strtoul() with kstrtoul() by copying the packet
data into a local NUL-terminated buffer first. It also updates the logging
style from printk() to dev_warn().
> diff --git a/drivers/input/touchscreen/gunze.c b/drivers/input/touchscreen/gunze.c
> --- a/drivers/input/touchscreen/gunze.c
> +++ b/drivers/input/touchscreen/gunze.c
> @@ -41,15 +41,26 @@ static void gunze_process_packet(struct gunze *gunze)
> struct input_dev *dev = gunze->dev;
> + char buf[GUNZE_MAX_LENGTH + 1];
> + unsigned long x = 0, y = 0;
>
> if (gunze->idx != GUNZE_MAX_LENGTH || gunze->data[5] != ',' ||
> (gunze->data[0] != 'T' && gunze->data[0] != 'R')) {
> - printk(KERN_WARNING "gunze.c: bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
> + dev_warn(&gunze->serio->dev, "bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
> return;
> }
>
> - input_report_abs(dev, ABS_X, simple_strtoul(gunze->data + 1, NULL, 10));
> - input_report_abs(dev, ABS_Y, 1024 - simple_strtoul(gunze->data + 6, NULL, 10));
> + memcpy(buf, gunze->data, GUNZE_MAX_LENGTH);
> + buf[5] = '\0'; /* NUL-terminate X portion: "1234\0" */
> + buf[GUNZE_MAX_LENGTH] = '\0'; /* NUL-terminate Y portion */
> +
> + if (kstrtoul(buf + 1, 10, &x))
[Severity: High]
In gunze_process_packet(), kstrtoul() requires a perfectly clean string
without trailing spaces or non-digit characters, whereas simple_strtoul()
would successfully stop parsing at the first non-digit.
If the legacy fixed-width hardware protocol pads coordinates with spaces
or contains noisy data, will kstrtoul() reject the entire packet and
return -EINVAL, resulting in a loss of valid touch coordinate data?
> + dev_warn(&gunze->serio->dev, "failed to parse X coordinate\n");
> + if (kstrtoul(buf + 6, 10, &y))
> + dev_warn(&gunze->serio->dev, "failed to parse Y coordinate\n");
[Severity: High]
The commit message mentions that release packets can contain noisy data,
meaning parsing failures are an expected event.
Because gunze_process_packet() is called directly from the hardware interrupt
handler, could these un-ratelimited dev_warn() calls flood the kernel log and
cause high CPU overhead when users interact with the touchscreen?
> +
> + input_report_abs(dev, ABS_X, x);
> + input_report_abs(dev, ABS_Y, 1024 - y);
[Severity: High]
This is a pre-existing issue, but when parsing fails, the y coordinate falls
back to 0.
Does unconditionally reporting ABS_Y as 1024 - 0 cause the touch cursor to
instantaneously jump out of bounds and trigger spurious clicks or swipe
artifacts on the device?
> input_report_key(dev, BTN_TOUCH, gunze->data[0] == 'T');
> input_sync(dev);
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260719173700.4942-1-bivashraj750@gmail.com?part=1
^ permalink raw reply
* [PATCH v4] Input: gunze: replace deprecated APIs and fix warning style
From: Bivash Kumar Singh @ 2026-07-19 17:37 UTC (permalink / raw)
To: linux-input; +Cc: dmitry.torokhov, Bivash Kumar Singh
In-Reply-To: <20260718190040.10613-1-bivashraj750@gmail.com>
Replace two uses of simple_strtoul() with kstrtoul() as the former
is deprecated. kstrtoul() requires a NUL-terminated string, so copy
the packet data into a local buffer and NUL-terminate the X and Y
fields before parsing. Initialize x and y to 0 so that if parsing
fails the coordinates fall back to 0 rather than reporting garbage,
and BTN_TOUCH is always updated regardless of parse result.
Replace printk(KERN_WARNING) with dev_warn() using the serio device,
which is the correct logging style for driver code and removes the
redundant 'gunze.c:' filename prefix.
Signed-off-by: Bivash Kumar Singh <bivashraj750@gmail.com>
Changes in v4:
- Remove early return on kstrtoul() failure to prevent touch state
getting permanently stuck if a release packet contains noisy data.
Initialize x and y to 0 as safe fallback values instead.
- Add dev_warn() on kstrtoul() parse failure so errors are logged
but BTN_TOUCH is always updated regardless of parse result.
(reported by Sashiko AI review)
Changes in v3:
- Fix comment style: add space after /* and use NUL instead of NULL
- Add missing Changes section that was absent in v2
Changes in v2:
- Copy packet data to a local NUL-terminated buffer before calling
kstrtoul(), so the comma separator does not cause parsing to fail
on every valid touch event. (reported by Sashiko AI review)
---
drivers/input/touchscreen/gunze.c | 17 ++++++++++++++---
1 file changed, 14 insertions(+), 3 deletions(-)
diff --git a/drivers/input/touchscreen/gunze.c b/drivers/input/touchscreen/gunze.c
index 2baeb4f3b941..e787209dc965 100644
--- a/drivers/input/touchscreen/gunze.c
+++ b/drivers/input/touchscreen/gunze.c
@@ -41,15 +41,26 @@ struct gunze {
static void gunze_process_packet(struct gunze *gunze)
{
struct input_dev *dev = gunze->dev;
+ char buf[GUNZE_MAX_LENGTH + 1];
+ unsigned long x = 0, y = 0;
if (gunze->idx != GUNZE_MAX_LENGTH || gunze->data[5] != ',' ||
(gunze->data[0] != 'T' && gunze->data[0] != 'R')) {
- printk(KERN_WARNING "gunze.c: bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
+ dev_warn(&gunze->serio->dev, "bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
return;
}
- input_report_abs(dev, ABS_X, simple_strtoul(gunze->data + 1, NULL, 10));
- input_report_abs(dev, ABS_Y, 1024 - simple_strtoul(gunze->data + 6, NULL, 10));
+ memcpy(buf, gunze->data, GUNZE_MAX_LENGTH);
+ buf[5] = '\0'; /* NUL-terminate X portion: "1234\0" */
+ buf[GUNZE_MAX_LENGTH] = '\0'; /* NUL-terminate Y portion */
+
+ if (kstrtoul(buf + 1, 10, &x))
+ dev_warn(&gunze->serio->dev, "failed to parse X coordinate\n");
+ if (kstrtoul(buf + 6, 10, &y))
+ dev_warn(&gunze->serio->dev, "failed to parse Y coordinate\n");
+
+ input_report_abs(dev, ABS_X, x);
+ input_report_abs(dev, ABS_Y, 1024 - y);
input_report_key(dev, BTN_TOUCH, gunze->data[0] == 'T');
input_sync(dev);
}
--
2.53.0
^ permalink raw reply related
* [PATCH] dt-bindings: input: Convert TI Palmas Power Button to DT schema
From: Eduard Bostina @ 2026-07-19 14:17 UTC (permalink / raw)
To: Conor Dooley, devicetree, Dmitry Torokhov, Eduard Bostina,
Krzysztof Kozlowski, linux-input, linux-kernel, Rob Herring
Cc: daniel.baluta, simona.toaca, goledhruva, m-chawdhry
Convert the Texas Instruments Palmas power button bindings
to DT schema.
During the conversion, 'wakeup-source' was added.
It was missing from the original text binding but is actively
used by in-tree boards.
Signed-off-by: Eduard Bostina <egbostina@gmail.com>
---
.../bindings/input/ti,palmas-pwrbutton.txt | 35 -----------
.../bindings/input/ti,palmas-pwrbutton.yaml | 58 +++++++++++++++++++
2 files changed, 58 insertions(+), 35 deletions(-)
delete mode 100644 Documentation/devicetree/bindings/input/ti,palmas-pwrbutton.txt
create mode 100644 Documentation/devicetree/bindings/input/ti,palmas-pwrbutton.yaml
diff --git a/Documentation/devicetree/bindings/input/ti,palmas-pwrbutton.txt b/Documentation/devicetree/bindings/input/ti,palmas-pwrbutton.txt
deleted file mode 100644
index c829e18e1a05..000000000000
--- a/Documentation/devicetree/bindings/input/ti,palmas-pwrbutton.txt
+++ /dev/null
@@ -1,35 +0,0 @@
-Texas Instruments Palmas family power button module
-
-This module is part of the Palmas family of PMICs. For more details
-about the whole chip see:
-Documentation/devicetree/bindings/mfd/palmas.txt.
-
-This module provides a simple power button event via an Interrupt.
-
-Required properties:
-- compatible: should be one of the following
- - "ti,palmas-pwrbutton": For Palmas compatible power on button
-- interrupts: Interrupt number of power button submodule on device.
-
-Optional Properties:
-
-- ti,palmas-long-press-seconds: Duration in seconds which the power
- button should be kept pressed for Palmas to power off automatically.
- NOTE: This depends on OTP support and POWERHOLD signal configuration
- on platform. Valid values are 6, 8, 10 and 12.
-- ti,palmas-pwron-debounce-milli-seconds: Duration in milliseconds
- which the power button should be kept pressed for Palmas to register
- a press for debouncing purposes. NOTE: This depends on specific
- Palmas variation capability. Valid values are 15, 100, 500 and 1000.
-
-Example:
-
-&palmas {
- palmas_pwr_button: pwrbutton {
- compatible = "ti,palmas-pwrbutton";
- interrupt-parent = <&tps659038>;
- interrupts = <1 IRQ_TYPE_EDGE_FALLING>;
- ti,palmas-long-press-seconds = <12>;
- ti,palmas-pwron-debounce-milli-seconds = <15>;
- };
-};
diff --git a/Documentation/devicetree/bindings/input/ti,palmas-pwrbutton.yaml b/Documentation/devicetree/bindings/input/ti,palmas-pwrbutton.yaml
new file mode 100644
index 000000000000..9f64caaac00d
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/ti,palmas-pwrbutton.yaml
@@ -0,0 +1,58 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/ti,palmas-pwrbutton.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Texas Instruments Palmas Power Button
+
+maintainers:
+ - Eduard Bostina <egbostina@gmail.com>
+
+description:
+ This module is part of the Palmas PMIC. It provides a simple power button
+ event via an interrupt.
+
+properties:
+ compatible:
+ const: ti,palmas-pwrbutton
+
+ interrupts:
+ maxItems: 1
+
+ ti,palmas-long-press-seconds:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ enum: [ 6, 8, 10, 12 ]
+ description: |
+ Duration in seconds which the power button should be kept pressed
+ for Palmas to power off automatically. NOTE: This depends on OTP
+ support and POWERHOLD signal configuration on platform.
+
+ ti,palmas-pwron-debounce-milli-seconds:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ enum: [ 15, 100, 500, 1000 ]
+ description: |
+ Duration in milliseconds which the power button should be kept
+ pressed for Palmas to register a press for debouncing purposes.
+ NOTE: This depends on specific Palmas variation capability.
+
+ wakeup-source: true
+
+required:
+ - compatible
+ - interrupts
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ pmic {
+ power-button {
+ compatible = "ti,palmas-pwrbutton";
+ interrupts = <1 IRQ_TYPE_EDGE_FALLING>;
+ ti,palmas-long-press-seconds = <12>;
+ ti,palmas-pwron-debounce-milli-seconds = <15>;
+ };
+ };
--
2.43.0
^ permalink raw reply related
* [PATCH v2 2/2] ARM: dts: ti: omap: Disable keypad and enable it on the boards that use it
From: Eduard Bostina @ 2026-07-19 13:37 UTC (permalink / raw)
To: Aaro Koskinen, Andreas Kemnade, Conor Dooley, devicetree,
Dmitry Torokhov, Eduard Bostina, Kevin Hilman,
Krzysztof Kozlowski, linux-input, linux-kernel, linux-omap,
Rob Herring, Roger Quadros, Tony Lindgren
Cc: daniel.baluta, simona.toaca, goledhruva, m-chawdhry
In-Reply-To: <20260719133730.3551691-1-egbostina@gmail.com>
The keypad node in omap4-l4.dtsi and omap5-l4.dtsi lacks the matrix keymap,
which is board specific. The binding requires the matrix properties, so the
incomplete template nodes fail dtbs_check.
Mark the keypad disabled in the SoC files and enable it on the boards
that actually complete the node with a keymap.
Signed-off-by: Eduard Bostina <egbostina@gmail.com>
---
arch/arm/boot/dts/ti/omap/motorola-mapphone-mz607-mz617.dtsi | 2 ++
arch/arm/boot/dts/ti/omap/omap4-droid-bionic-xt875.dts | 2 ++
arch/arm/boot/dts/ti/omap/omap4-droid4-xt894.dts | 2 ++
arch/arm/boot/dts/ti/omap/omap4-epson-embt2ws.dts | 2 ++
arch/arm/boot/dts/ti/omap/omap4-l4.dtsi | 1 +
arch/arm/boot/dts/ti/omap/omap4-sdp.dts | 2 ++
arch/arm/boot/dts/ti/omap/omap5-l4.dtsi | 1 +
7 files changed, 12 insertions(+)
diff --git a/arch/arm/boot/dts/ti/omap/motorola-mapphone-mz607-mz617.dtsi b/arch/arm/boot/dts/ti/omap/motorola-mapphone-mz607-mz617.dtsi
index a356b3a2f24e..b97ab2906950 100644
--- a/arch/arm/boot/dts/ti/omap/motorola-mapphone-mz607-mz617.dtsi
+++ b/arch/arm/boot/dts/ti/omap/motorola-mapphone-mz607-mz617.dtsi
@@ -4,6 +4,8 @@
#include "motorola-mapphone-common.dtsi"
&keypad {
+ status = "okay";
+
keypad,num-rows = <8>;
keypad,num-columns = <8>;
linux,keymap = <MATRIX_KEY(5, 0, KEY_VOLUMEUP)>,
diff --git a/arch/arm/boot/dts/ti/omap/omap4-droid-bionic-xt875.dts b/arch/arm/boot/dts/ti/omap/omap4-droid-bionic-xt875.dts
index 1d9000f84f1b..e72ac17fdd96 100644
--- a/arch/arm/boot/dts/ti/omap/omap4-droid-bionic-xt875.dts
+++ b/arch/arm/boot/dts/ti/omap/omap4-droid-bionic-xt875.dts
@@ -18,6 +18,8 @@ aliases {
};
&keypad {
+ status = "okay";
+
keypad,num-rows = <8>;
keypad,num-columns = <8>;
linux,keymap = <
diff --git a/arch/arm/boot/dts/ti/omap/omap4-droid4-xt894.dts b/arch/arm/boot/dts/ti/omap/omap4-droid4-xt894.dts
index cc3f3e1b65ea..62f87cf95f74 100644
--- a/arch/arm/boot/dts/ti/omap/omap4-droid4-xt894.dts
+++ b/arch/arm/boot/dts/ti/omap/omap4-droid4-xt894.dts
@@ -46,6 +46,8 @@ slider {
};
&keypad {
+ status = "okay";
+
keypad,num-rows = <8>;
keypad,num-columns = <8>;
linux,keymap = <
diff --git a/arch/arm/boot/dts/ti/omap/omap4-epson-embt2ws.dts b/arch/arm/boot/dts/ti/omap/omap4-epson-embt2ws.dts
index e11d1931c42a..d51913620f06 100644
--- a/arch/arm/boot/dts/ti/omap/omap4-epson-embt2ws.dts
+++ b/arch/arm/boot/dts/ti/omap/omap4-epson-embt2ws.dts
@@ -428,6 +428,8 @@ mpu9150: imu@68 {
};
&keypad {
+ status = "okay";
+
pinctrl-names = "default";
pinctrl-0 = <&keypad_pins>;
keypad,num-rows = <2>;
diff --git a/arch/arm/boot/dts/ti/omap/omap4-l4.dtsi b/arch/arm/boot/dts/ti/omap/omap4-l4.dtsi
index c1afc49f456c..00f98991cc5c 100644
--- a/arch/arm/boot/dts/ti/omap/omap4-l4.dtsi
+++ b/arch/arm/boot/dts/ti/omap/omap4-l4.dtsi
@@ -1200,6 +1200,7 @@ keypad: keypad@0 {
reg = <0x0 0x80>;
interrupts = <GIC_SPI 120 IRQ_TYPE_LEVEL_HIGH>;
reg-names = "mpu";
+ status = "disabled";
};
};
diff --git a/arch/arm/boot/dts/ti/omap/omap4-sdp.dts b/arch/arm/boot/dts/ti/omap/omap4-sdp.dts
index b550105585a1..a04234b3be2a 100644
--- a/arch/arm/boot/dts/ti/omap/omap4-sdp.dts
+++ b/arch/arm/boot/dts/ti/omap/omap4-sdp.dts
@@ -531,6 +531,8 @@ &emif2 {
};
&keypad {
+ status = "okay";
+
keypad,num-rows = <8>;
keypad,num-columns = <8>;
linux,keymap = <0x00000012 /* KEY_E */
diff --git a/arch/arm/boot/dts/ti/omap/omap5-l4.dtsi b/arch/arm/boot/dts/ti/omap/omap5-l4.dtsi
index 72849e1c95b0..af32ca329930 100644
--- a/arch/arm/boot/dts/ti/omap/omap5-l4.dtsi
+++ b/arch/arm/boot/dts/ti/omap/omap5-l4.dtsi
@@ -2449,6 +2449,7 @@ target-module@c000 { /* 0x4ae1c000, ap 11 1c.0 */
keypad: keypad@0 {
compatible = "ti,omap4-keypad";
reg = <0x0 0x400>;
+ status = "disabled";
};
};
};
--
2.43.0
^ permalink raw reply related
* [PATCH v2 1/2] dt-bindings: input: Convert TI Keypad Controller to DT schema
From: Eduard Bostina @ 2026-07-19 13:37 UTC (permalink / raw)
To: Aaro Koskinen, Andreas Kemnade, Conor Dooley, devicetree,
Dmitry Torokhov, Eduard Bostina, Kevin Hilman,
Krzysztof Kozlowski, linux-input, linux-kernel, linux-omap,
Rob Herring, Roger Quadros, Tony Lindgren
Cc: daniel.baluta, simona.toaca, goledhruva, m-chawdhry
In-Reply-To: <20260719133730.3551691-1-egbostina@gmail.com>
Convert the Texas Instruments Keypad Controller bindings
to DT schema.
During the conversion, the following updates were made:
- Corrected the documented property 'linux,keypad-no-autorepeat'
to 'linux,input-no-autorepeat'. The old text binding documented
the property incorrectly. The standard input subsystem property is
'linux,input-no-autorepeat', which is actively used in device
trees and parsed by the kernel.
- Added the 'reg-names' property ("mpu"), which was omitted from
the original text binding but is actively used in device trees.
Signed-off-by: Eduard Bostina <egbostina@gmail.com>
---
.../devicetree/bindings/input/omap-keypad.txt | 28 ---------
.../bindings/input/ti,omap4-keypad.yaml | 63 +++++++++++++++++++
2 files changed, 63 insertions(+), 28 deletions(-)
delete mode 100644 Documentation/devicetree/bindings/input/omap-keypad.txt
create mode 100644 Documentation/devicetree/bindings/input/ti,omap4-keypad.yaml
diff --git a/Documentation/devicetree/bindings/input/omap-keypad.txt b/Documentation/devicetree/bindings/input/omap-keypad.txt
deleted file mode 100644
index 34ed1c60ff95..000000000000
--- a/Documentation/devicetree/bindings/input/omap-keypad.txt
+++ /dev/null
@@ -1,28 +0,0 @@
-* TI's Keypad Controller device tree bindings
-
-TI's Keypad controller is used to interface a SoC with a matrix-type
-keypad device. The keypad controller supports multiple row and column lines.
-A key can be placed at each intersection of a unique row and a unique column.
-The keypad controller can sense a key-press and key-release and report the
-event using a interrupt to the cpu.
-
-This binding is based on the matrix-keymap binding with the following
-changes:
-
-keypad,num-rows and keypad,num-columns are required.
-
-Required SoC Specific Properties:
-- compatible: should be one of the following
- - "ti,omap4-keypad": For controllers compatible with omap4 keypad
- controller.
-
-Optional Properties specific to linux:
-- linux,keypad-no-autorepeat: do no enable autorepeat feature.
-
-Example:
- keypad@4ae1c000{
- compatible = "ti,omap4-keypad";
- keypad,num-rows = <2>;
- keypad,num-columns = <8>;
- linux,keypad-no-autorepeat;
- };
diff --git a/Documentation/devicetree/bindings/input/ti,omap4-keypad.yaml b/Documentation/devicetree/bindings/input/ti,omap4-keypad.yaml
new file mode 100644
index 000000000000..5e0c101f9ba0
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/ti,omap4-keypad.yaml
@@ -0,0 +1,63 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/ti,omap4-keypad.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Texas Instruments Keypad Controller
+
+maintainers:
+ - Eduard Bostina <egbostina@gmail.com>
+
+description:
+ TI's Keypad controller is used to interface a SoC with a matrix-type
+ keypad device. The keypad controller supports multiple row and column lines.
+ A key can be placed at each intersection of a unique row and a unique column.
+ The keypad controller can sense a key-press and key-release and report the
+ event using a interrupt to the cpu.
+
+allOf:
+ - $ref: /schemas/input/matrix-keymap.yaml#
+
+properties:
+ compatible:
+ const: ti,omap4-keypad
+
+ reg:
+ maxItems: 1
+
+ reg-names:
+ const: mpu
+
+ interrupts:
+ maxItems: 1
+
+ linux,input-no-autorepeat:
+ type: boolean
+ description: Do not enable autorepeat feature.
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - keypad,num-rows
+ - keypad,num-columns
+ - linux,keymap
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+ keypad@4ae1c000 {
+ compatible = "ti,omap4-keypad";
+ reg = <0x4ae1c000 0x400>;
+ reg-names = "mpu";
+ interrupts = <GIC_SPI 120 IRQ_TYPE_LEVEL_HIGH>;
+ keypad,num-rows = <2>;
+ keypad,num-columns = <8>;
+ linux,keymap = <0x00000011 /* KEY_W */
+ 0x0001001f>; /* KEY_S */
+ linux,input-no-autorepeat;
+ };
--
2.43.0
^ permalink raw reply related
* [PATCH v2 0/2] dt-bindings: input: Convert TI Keypad Controller to DT schema
From: Eduard Bostina @ 2026-07-19 13:37 UTC (permalink / raw)
To: Aaro Koskinen, Andreas Kemnade, Conor Dooley, devicetree,
Dmitry Torokhov, Eduard Bostina, Kevin Hilman,
Krzysztof Kozlowski, linux-input, linux-kernel, linux-omap,
Rob Herring, Roger Quadros, Tony Lindgren
Cc: daniel.baluta, simona.toaca, goledhruva, m-chawdhry
The first patch converts the TI Keypad Controller binding to DT schema.
The second patch updates the OMAP4 and OMAP5 device trees, which is
needed because the schema requires the matrix properties.
Changes in v2:
- Added 'interrupts', 'keypad,num-rows', 'keypad,num-columns' and
'linux,keymap' to the required list.
- Dropped the '|' from the description.
- Added the DTS patch, so the OMAP4 and OMAP5 boards still pass
dtbs_check with the required properties in place.
v1:
https://lore.kernel.org/all/20260708123252.1768355-1-egbostina@gmail.com/
Eduard Bostina (2):
dt-bindings: input: Convert TI Keypad Controller to DT schema
ARM: dts: ti: omap: Disable keypad and enable it on the boards that
use it
.../devicetree/bindings/input/omap-keypad.txt | 28 ---------
.../bindings/input/ti,omap4-keypad.yaml | 63 +++++++++++++++++++
.../omap/motorola-mapphone-mz607-mz617.dtsi | 2 +
.../dts/ti/omap/omap4-droid-bionic-xt875.dts | 2 +
.../boot/dts/ti/omap/omap4-droid4-xt894.dts | 2 +
.../boot/dts/ti/omap/omap4-epson-embt2ws.dts | 2 +
arch/arm/boot/dts/ti/omap/omap4-l4.dtsi | 1 +
arch/arm/boot/dts/ti/omap/omap4-sdp.dts | 2 +
arch/arm/boot/dts/ti/omap/omap5-l4.dtsi | 1 +
9 files changed, 75 insertions(+), 28 deletions(-)
delete mode 100644 Documentation/devicetree/bindings/input/omap-keypad.txt
create mode 100644 Documentation/devicetree/bindings/input/ti,omap4-keypad.yaml
base-commit: 0718283ab28bc3907e10b61a6b4be6fefa1cbb2f
--
2.43.0
^ permalink raw reply
* Re: [PATCH v3] Input: gunze: replace deprecated APIs and fix warning style
From: sashiko-bot @ 2026-07-19 6:13 UTC (permalink / raw)
To: Bivash Kumar Singh; +Cc: linux-input, dmitry.torokhov
In-Reply-To: <20260719060238.6440-1-bivashraj750@gmail.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The newly added early return on coordinate parsing failure drops critical touch state transitions (like touch releases), causing stuck touches.
--
--- Patch [1]: [PATCH v3] Input: gunze: replace deprecated APIs and fix warning style ---
commit 7585c5c9a9ed51c8709aeaa8875aeb5e7e1646b2
Author: Bivash Kumar Singh <bivashraj750@gmail.com>
Input: gunze: replace deprecated APIs and fix warning style
This commit replaces deprecated simple_strtoul calls with kstrtoul and adds an
early return for parsing failures. It also updates the logging from printk to
dev_warn using the serio device.
> diff --git a/drivers/input/touchscreen/gunze.c b/drivers/input/touchscreen/gunze.c
> index 2baeb4f3b941c..0e8131cad647e 100644
> --- a/drivers/input/touchscreen/gunze.c
> +++ b/drivers/input/touchscreen/gunze.c
> @@ -41,15 +41,24 @@ static void gunze_process_packet(struct gunze *gunze)
> {
> struct input_dev *dev = gunze->dev;
> + char buf[GUNZE_MAX_LENGTH + 1];
> + unsigned long x, y;
>
> if (gunze->idx != GUNZE_MAX_LENGTH || gunze->data[5] != ',' ||
> (gunze->data[0] != 'T' && gunze->data[0] != 'R')) {
> - printk(KERN_WARNING "gunze.c: bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
> + dev_warn(&gunze->serio->dev, "bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
> return;
> }
>
> - input_report_abs(dev, ABS_X, simple_strtoul(gunze->data + 1, NULL, 10));
> - input_report_abs(dev, ABS_Y, 1024 - simple_strtoul(gunze->data + 6, NULL, 10));
> + memcpy(buf, gunze->data, GUNZE_MAX_LENGTH);
> + buf[5] = '\0'; /* NUL-terminate X portion: "1234\0" */
> + buf[GUNZE_MAX_LENGTH] = '\0'; /* NUL-terminate Y portion */
> +
> + if (kstrtoul(buf + 1, 10, &x) || kstrtoul(buf + 6, 10, &y))
> + return;
[Severity: High]
Does this early return risk dropping critical touch state transitions, such as
touch releases, causing stuck touches?
If a release packet ('R') contains unparseable coordinates (such as space
padding or noise), parsing will fail and trigger this early return.
Because the return bypasses the input_report_key call for BTN_TOUCH and the
subsequent input_sync call, the release event would be silently dropped.
Could this leave the touchscreen desynchronized and permanently stuck in the
pressed state?
> +
> + input_report_abs(dev, ABS_X, x);
> + input_report_abs(dev, ABS_Y, 1024 - y);
> input_report_key(dev, BTN_TOUCH, gunze->data[0] == 'T');
> input_sync(dev);
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260719060238.6440-1-bivashraj750@gmail.com?part=1
^ permalink raw reply
* [PATCH v3] Input: gunze: replace deprecated APIs and fix warning style
From: Bivash Kumar Singh @ 2026-07-19 6:02 UTC (permalink / raw)
To: linux-input; +Cc: dmitry.torokhov, Bivash Kumar Singh
In-Reply-To: <20260718190040.10613-1-bivashraj750@gmail.com>
Replace two uses of simple_strtoul() with kstrtoul() as the former
is deprecated. kstrtoul() also properly handles parse errors, so
add an early return if parsing fails.
Replace printk(KERN_WARNING) with dev_warn() using the serio device,
which is the correct logging style for driver code and removes the
redundant 'gunze.c:' filename prefix.
Signed-off-by: Bivash Kumar Singh <bivashraj750@gmail.com>
Changes in v3:
- Fix comment style: add space after /* and use NUL instead of NULL
- Add missing Changes section that was absent in v2
Changes in v2:
- Copy packet data to a local NUL-terminated buffer before calling
kstrtoul(), so the comma separator does not cause parsing to fail
on every valid touch event. (reported by Sashiko AI review)
---
drivers/input/touchscreen/gunze.c | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/drivers/input/touchscreen/gunze.c b/drivers/input/touchscreen/gunze.c
index 2baeb4f3b941..0e8131cad647 100644
--- a/drivers/input/touchscreen/gunze.c
+++ b/drivers/input/touchscreen/gunze.c
@@ -41,15 +41,24 @@ struct gunze {
static void gunze_process_packet(struct gunze *gunze)
{
struct input_dev *dev = gunze->dev;
+ char buf[GUNZE_MAX_LENGTH + 1];
+ unsigned long x, y;
if (gunze->idx != GUNZE_MAX_LENGTH || gunze->data[5] != ',' ||
(gunze->data[0] != 'T' && gunze->data[0] != 'R')) {
- printk(KERN_WARNING "gunze.c: bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
+ dev_warn(&gunze->serio->dev, "bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
return;
}
- input_report_abs(dev, ABS_X, simple_strtoul(gunze->data + 1, NULL, 10));
- input_report_abs(dev, ABS_Y, 1024 - simple_strtoul(gunze->data + 6, NULL, 10));
+ memcpy(buf, gunze->data, GUNZE_MAX_LENGTH);
+ buf[5] = '\0'; /* NUL-terminate X portion: "1234\0" */
+ buf[GUNZE_MAX_LENGTH] = '\0'; /* NUL-terminate Y portion */
+
+ if (kstrtoul(buf + 1, 10, &x) || kstrtoul(buf + 6, 10, &y))
+ return;
+
+ input_report_abs(dev, ABS_X, x);
+ input_report_abs(dev, ABS_Y, 1024 - y);
input_report_key(dev, BTN_TOUCH, gunze->data[0] == 'T');
input_sync(dev);
}
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v2] Input: gunze: replace deprecated APIs and fix warning style
From: sashiko-bot @ 2026-07-19 5:53 UTC (permalink / raw)
To: Bivash Kumar Singh; +Cc: dmitry.torokhov, linux-input
In-Reply-To: <20260719054121.5917-1-bivashraj750@gmail.com>
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Strict parsing with kstrtoul() and an early return drops packets on non-digit coordinate data, potentially causing permanently stuck touch states.
--
--- Patch [1]: [PATCH v2] Input: gunze: replace deprecated APIs and fix warning style ---
commit 448e772e264a7d37aec51432b84a51cdf06f350f
Author: Bivash Kumar Singh <bivashraj750@gmail.com>
Input: gunze: replace deprecated APIs and fix warning style
This commit replaces the deprecated simple_strtoul() API with kstrtoul()
and updates the logging style to use dev_warn(). It also adds an early
return in the packet processing path if coordinate parsing fails.
> diff --git a/drivers/input/touchscreen/gunze.c b/drivers/input/touchscreen/gunze.c
> index 2baeb4f3b941c..e9c028430420c 100644
> --- a/drivers/input/touchscreen/gunze.c
> +++ b/drivers/input/touchscreen/gunze.c
[ ... ]
> @@ -41,15 +41,24 @@ static void gunze_process_packet(struct gunze *gunze)
[ ... ]
> - input_report_abs(dev, ABS_X, simple_strtoul(gunze->data + 1, NULL, 10));
> - input_report_abs(dev, ABS_Y, 1024 - simple_strtoul(gunze->data + 6, NULL, 10));
> + memcpy(buf, gunze->data, GUNZE_MAX_LENGTH);
> + buf[5] = '\0'; /*NULL-terminate X portion: "1234\0" */
> + buf[GUNZE_MAX_LENGTH] = '\0'; /*NULL-terminate Y portion */
> +
> + if (kstrtoul(buf + 1, 10, &x) || kstrtoul(buf + 6, 10, &y))
> + return;
[Severity: High]
Could this strict parsing with kstrtoul() and early return drop valid events
and cause the touch state to get permanently stuck?
Unlike simple_strtoul(), which stops parsing at the first non-digit character,
kstrtoul() will return an error if the string contains trailing spaces or
serial line noise.
If a touch release packet (starting with 'R') contains non-digit padding,
kstrtoul() will return an error and trigger this early return.
> +
> + input_report_abs(dev, ABS_X, x);
> + input_report_abs(dev, ABS_Y, 1024 - y);
> input_report_key(dev, BTN_TOUCH, gunze->data[0] == 'T');
> input_sync(dev);
Bypassing the BTN_TOUCH state update and input_sync() would cause the
touchscreen to remain stuck in a pressed state until a subsequent valid
packet is received.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260719054121.5917-1-bivashraj750@gmail.com?part=1
^ permalink raw reply
* [PATCH v2] Input: gunze: replace deprecated APIs and fix warning style
From: Bivash Kumar Singh @ 2026-07-19 5:41 UTC (permalink / raw)
To: linux-input; +Cc: dmitry.torokhov, Bivash Kumar Singh
In-Reply-To: <20260718190040.10613-1-bivashraj750@gmail.com>
Replace two uses of simple_strtoul() with kstrtoul() as the former
is deprecated. kstrtoul() also properly handles parse errors, so
add an early return if parsing fails.
Replace printk(KERN_WARNING) with dev_warn() using the serio device,
which is the correct logging style for driver code and removes the
redundant 'gunze.c:' filename prefix.
Signed-off-by: Bivash Kumar Singh <bivashraj750@gmail.com>
---
drivers/input/touchscreen/gunze.c | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/drivers/input/touchscreen/gunze.c b/drivers/input/touchscreen/gunze.c
index 2baeb4f3b941..e9c028430420 100644
--- a/drivers/input/touchscreen/gunze.c
+++ b/drivers/input/touchscreen/gunze.c
@@ -41,15 +41,24 @@ struct gunze {
static void gunze_process_packet(struct gunze *gunze)
{
struct input_dev *dev = gunze->dev;
+ char buf[GUNZE_MAX_LENGTH + 1];
+ unsigned long x, y;
if (gunze->idx != GUNZE_MAX_LENGTH || gunze->data[5] != ',' ||
(gunze->data[0] != 'T' && gunze->data[0] != 'R')) {
- printk(KERN_WARNING "gunze.c: bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
+ dev_warn(&gunze->serio->dev, "bad packet: >%.*s<\n", GUNZE_MAX_LENGTH, gunze->data);
return;
}
- input_report_abs(dev, ABS_X, simple_strtoul(gunze->data + 1, NULL, 10));
- input_report_abs(dev, ABS_Y, 1024 - simple_strtoul(gunze->data + 6, NULL, 10));
+ memcpy(buf, gunze->data, GUNZE_MAX_LENGTH);
+ buf[5] = '\0'; /*NULL-terminate X portion: "1234\0" */
+ buf[GUNZE_MAX_LENGTH] = '\0'; /*NULL-terminate Y portion */
+
+ if (kstrtoul(buf + 1, 10, &x) || kstrtoul(buf + 6, 10, &y))
+ return;
+
+ input_report_abs(dev, ABS_X, x);
+ input_report_abs(dev, ABS_Y, 1024 - y);
input_report_key(dev, BTN_TOUCH, gunze->data[0] == 'T');
input_sync(dev);
}
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v2 3/3] dt-bindings: leds: backlight: Convert TPS65217 to DT schema
From: sashiko-bot @ 2026-07-18 22:04 UTC (permalink / raw)
To: Eduard Bostina
Cc: devicetree, dri-devel, lee, linux-leds, robh, linux-input,
dmitry.torokhov, conor+dt
In-Reply-To: <20260718215632.3295049-4-egbostina@gmail.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Stale and conflicting regulator text binding not deleted during schema conversion.
- [Low] Missing `required` constraint for `reg` property in regulator child nodes.
--
--- Patch [3]: [PATCH v2 3/3] dt-bindings: leds: backlight: Convert TPS65217 to DT schema ---
commit 0671f0f93114029deaa2c3fee6b27ad8abd2cb2d
Author: Eduard Bostina <egbostina@gmail.com>
dt-bindings: leds: backlight: Convert TPS65217 to DT schema
This commit converts the Texas Instruments TPS65217 backlight binding to YAML
schema. It also documents missing child nodes, interrupt properties, and
updates regulator names to match what the boards use.
> diff --git a/Documentation/devicetree/bindings/leds/backlight/ti,tps65217.yaml b/Documentation/devicetree/bindings/leds/backlight/ti,tps65217.yaml
> new file mode 100644
> index 0000000000000..9586e08f1b1fc
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/leds/backlight/ti,tps65217.yaml
> @@ -0,0 +1,123 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/leds/backlight/ti,tps65217.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: TPS65217 family of regulators
[Severity: Medium]
Should Documentation/devicetree/bindings/regulator/tps65217.txt also be
deleted in this patch?
The new YAML schema defines the full MFD device, but conflicts with the
undeleted regulator text binding. The old regulator text binding expects
regulator nodes to be named dcdc[1-3] and ldo[1-4], while the new schema
expects regulator@[0-6], which could cause validation inconsistencies.
[ ... ]
> + patternProperties:
> + "^regulator@[0-6]$":
> + type: object
> + $ref: /schemas/regulator/regulator.yaml#
> + unevaluatedProperties: false
> +
> + properties:
> + reg:
> + maxItems: 1
> +
> + regulator-compatible:
> + $ref: /schemas/types.yaml#/definitions/string
[Severity: Low]
Does this schema need to specify the reg property as required?
Since these regulator child nodes have a unit address in the pattern
(^regulator@[0-6]$), DT validation could incorrectly pass nodes that contain a
unit address but lack the mandatory reg property without a required block.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260718215632.3295049-1-egbostina@gmail.com?part=3
^ permalink raw reply
* Re: [PATCH v2 2/3] dt-bindings: input: Convert TI TPS65217 power button to DT schema
From: sashiko-bot @ 2026-07-18 22:02 UTC (permalink / raw)
To: Eduard Bostina
Cc: dmitry.torokhov, robh, devicetree, lee, dri-devel, conor+dt,
linux-input, linux-leds
In-Reply-To: <20260718215632.3295049-3-egbostina@gmail.com>
> Convert the Texas Instruments TPS65217 and TPS65218 Power Button
> bindings to DT schema.
>
> Signed-off-by: Eduard Bostina <egbostina@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260718215632.3295049-1-egbostina@gmail.com?part=2
^ permalink raw reply
* Re: [PATCH v2 1/3] ARM: dts: ti: am335x-guardian: Drop unused isink-en property
From: sashiko-bot @ 2026-07-18 21:59 UTC (permalink / raw)
To: Eduard Bostina
Cc: linux-leds, lee, dmitry.torokhov, devicetree, dri-devel,
linux-input, conor+dt, robh
In-Reply-To: <20260718215632.3295049-2-egbostina@gmail.com>
> The 'isink-en' property is not read by any driver, so remove it from the
> TPS65217 backlight node.
>
> Signed-off-by: Eduard Bostina <egbostina@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260718215632.3295049-1-egbostina@gmail.com?part=1
^ permalink raw reply
* [PATCH v2 3/3] dt-bindings: leds: backlight: Convert TPS65217 to DT schema
From: Eduard Bostina @ 2026-07-18 21:56 UTC (permalink / raw)
To: Aaro Koskinen, Andreas Kemnade, Conor Dooley, Daniel Thompson,
devicetree, Dmitry Torokhov, dri-devel, Eduard Bostina,
Jingoo Han, Kevin Hilman, Krzysztof Kozlowski, Lee Jones,
linux-input, linux-kernel, linux-leds, linux-omap, Pavel Machek,
Rob Herring, Roger Quadros, Tony Lindgren
Cc: daniel.baluta, simona.toaca, goledhruva, m-chawdhry
In-Reply-To: <20260718215632.3295049-1-egbostina@gmail.com>
Convert the Texas Instruments TPS65217 bindings to DT schema.
During the conversion, the following updates were made:
- Documented the 'regulators', 'charger' and 'pwrbutton' child nodes,
which are used by the boards but were missing from the old txt
binding.
- Documented the 'interrupts', 'interrupt-controller', '#interrupt-cells'
and 'ti,pmic-shutdown-controller' properties, which are used by the
am335x boards and read by the driver.
- Named the regulators 'regulator@[0-6]' to match what the boards use
instead of the dcdc/ldo node names from the txt binding.
Signed-off-by: Eduard Bostina <egbostina@gmail.com>
---
.../bindings/leds/backlight/ti,tps65217.yaml | 123 ++++++++++++++++++
.../leds/backlight/tps65217-backlight.txt | 27 ----
2 files changed, 123 insertions(+), 27 deletions(-)
create mode 100644 Documentation/devicetree/bindings/leds/backlight/ti,tps65217.yaml
delete mode 100644 Documentation/devicetree/bindings/leds/backlight/tps65217-backlight.txt
diff --git a/Documentation/devicetree/bindings/leds/backlight/ti,tps65217.yaml b/Documentation/devicetree/bindings/leds/backlight/ti,tps65217.yaml
new file mode 100644
index 000000000000..9586e08f1b1f
--- /dev/null
+++ b/Documentation/devicetree/bindings/leds/backlight/ti,tps65217.yaml
@@ -0,0 +1,123 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/leds/backlight/ti,tps65217.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: TPS65217 family of regulators
+
+maintainers:
+ - Eduard Bostina <egbostina@gmail.com>
+
+description:
+ The TPS65217 chip contains a boost converter and current sinks which can be
+ used to drive LEDs for use as backlights.
+
+properties:
+ compatible:
+ const: ti,tps65217
+
+ reg:
+ maxItems: 1
+ description: I2C slave address
+
+ interrupts:
+ maxItems: 1
+
+ interrupt-controller: true
+
+ "#interrupt-cells":
+ const: 1
+
+ ti,pmic-shutdown-controller:
+ type: boolean
+ description:
+ Set the PMIC to shutdown on PWR_EN toggle.
+
+ backlight:
+ type: object
+ additionalProperties: false
+ description:
+ Node for specifying WLED1 and WLED2 lines in TPS65217.
+
+ properties:
+ isel:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ enum: [1, 2]
+ description: |
+ Selection bit. Valid values:
+ 1 - ISEL1 (low-level)
+ 2 - ISEL2 (high-level)
+
+ fdim:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ enum: [100, 200, 500, 1000]
+ description:
+ PWM dimming frequency in Hz.
+
+ default-brightness:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 0
+ maximum: 100
+
+ required:
+ - isel
+ - fdim
+ - default-brightness
+
+ regulators:
+ type: object
+ additionalProperties: false
+ description:
+ List of child nodes that specify the regulator initialization data.
+ Not all regulators for the given device need to be present.
+
+ properties:
+ "#address-cells":
+ const: 1
+
+ "#size-cells":
+ const: 0
+
+ patternProperties:
+ "^regulator@[0-6]$":
+ type: object
+ $ref: /schemas/regulator/regulator.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ reg:
+ maxItems: 1
+
+ regulator-compatible:
+ $ref: /schemas/types.yaml#/definitions/string
+
+ charger:
+ $ref: /schemas/power/supply/tps65217-charger.yaml#
+
+ pwrbutton:
+ $ref: /schemas/input/ti,tps65217-pwrbutton.yaml#
+
+required:
+ - compatible
+ - reg
+
+additionalProperties: false
+
+examples:
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ tps@24 {
+ reg = <0x24>;
+ compatible = "ti,tps65217";
+
+ backlight {
+ isel = <1>; /* 1 - ISET1, 2 ISET2 */
+ fdim = <100>; /* TPS65217_BL_FDIM_100HZ */
+ default-brightness = <50>;
+ };
+ };
+ };
diff --git a/Documentation/devicetree/bindings/leds/backlight/tps65217-backlight.txt b/Documentation/devicetree/bindings/leds/backlight/tps65217-backlight.txt
deleted file mode 100644
index 5fb9279ac287..000000000000
--- a/Documentation/devicetree/bindings/leds/backlight/tps65217-backlight.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-TPS65217 family of regulators
-
-The TPS65217 chip contains a boost converter and current sinks which can be
-used to drive LEDs for use as backlights.
-
-Required properties:
-- compatible: "ti,tps65217"
-- reg: I2C slave address
-- backlight: node for specifying WLED1 and WLED2 lines in TPS65217
-- isel: selection bit, valid values: 1 for ISEL1 (low-level) and 2 for ISEL2 (high-level)
-- fdim: PWM dimming frequency, valid values: 100, 200, 500, 1000
-- default-brightness: valid values: 0-100
-
-Each regulator is defined using the standard binding for regulators.
-
-Example:
-
- tps: tps@24 {
- reg = <0x24>;
- compatible = "ti,tps65217";
- backlight {
- isel = <1>; /* 1 - ISET1, 2 ISET2 */
- fdim = <100>; /* TPS65217_BL_FDIM_100HZ */
- default-brightness = <50>;
- };
- };
-
--
2.43.0
^ permalink raw reply related
* [PATCH v2 2/3] dt-bindings: input: Convert TI TPS65217 power button to DT schema
From: Eduard Bostina @ 2026-07-18 21:56 UTC (permalink / raw)
To: Aaro Koskinen, Andreas Kemnade, Conor Dooley, Daniel Thompson,
devicetree, Dmitry Torokhov, dri-devel, Eduard Bostina,
Jingoo Han, Kevin Hilman, Krzysztof Kozlowski, Lee Jones,
linux-input, linux-kernel, linux-leds, linux-omap, Pavel Machek,
Rob Herring, Roger Quadros, Tony Lindgren
Cc: daniel.baluta, simona.toaca, goledhruva, m-chawdhry
In-Reply-To: <20260718215632.3295049-1-egbostina@gmail.com>
Convert the Texas Instruments TPS65217 and TPS65218 Power Button
bindings to DT schema.
Signed-off-by: Eduard Bostina <egbostina@gmail.com>
---
.../bindings/input/ti,tps65217-pwrbutton.yaml | 42 +++++++++++++++++++
.../bindings/input/tps65218-pwrbutton.txt | 30 -------------
2 files changed, 42 insertions(+), 30 deletions(-)
create mode 100644 Documentation/devicetree/bindings/input/ti,tps65217-pwrbutton.yaml
delete mode 100644 Documentation/devicetree/bindings/input/tps65218-pwrbutton.txt
diff --git a/Documentation/devicetree/bindings/input/ti,tps65217-pwrbutton.yaml b/Documentation/devicetree/bindings/input/ti,tps65217-pwrbutton.yaml
new file mode 100644
index 000000000000..3526d8b045fd
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/ti,tps65217-pwrbutton.yaml
@@ -0,0 +1,42 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/ti,tps65217-pwrbutton.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Texas Instruments TPS65217 and TPS65218 Power Button
+
+maintainers:
+ - Eduard Bostina <egbostina@gmail.com>
+
+description:
+ This module is part of the TPS65217/TPS65218 PMIC. It provides a simple
+ power button event via an interrupt.
+
+properties:
+ compatible:
+ enum:
+ - ti,tps65217-pwrbutton
+ - ti,tps65218-pwrbutton
+
+ interrupts:
+ maxItems: 1
+
+required:
+ - compatible
+ - interrupts
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+ pmic {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ power-button {
+ compatible = "ti,tps65218-pwrbutton";
+ interrupts = <3 IRQ_TYPE_EDGE_BOTH>;
+ };
+ };
diff --git a/Documentation/devicetree/bindings/input/tps65218-pwrbutton.txt b/Documentation/devicetree/bindings/input/tps65218-pwrbutton.txt
deleted file mode 100644
index 8682ab6d4a50..000000000000
--- a/Documentation/devicetree/bindings/input/tps65218-pwrbutton.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-Texas Instruments TPS65217 and TPS65218 power button
-
-This module is part of the TPS65217/TPS65218. For more details about the whole
-TPS65217 chip see Documentation/devicetree/bindings/regulator/tps65217.txt.
-
-This driver provides a simple power button event via an Interrupt.
-
-Required properties:
-- compatible: should be "ti,tps65217-pwrbutton" or "ti,tps65218-pwrbutton"
-
-Required properties:
-- interrupts: should be one of the following
- - <2>: For controllers compatible with tps65217
- - <3 IRQ_TYPE_EDGE_BOTH>: For controllers compatible with tps65218
-
-Examples:
-
-&tps {
- tps65217-pwrbutton {
- compatible = "ti,tps65217-pwrbutton";
- interrupts = <2>;
- };
-};
-
-&tps {
- power-button {
- compatible = "ti,tps65218-pwrbutton";
- interrupts = <3 IRQ_TYPE_EDGE_BOTH>;
- };
-};
--
2.43.0
^ permalink raw reply related
* [PATCH v2 1/3] ARM: dts: ti: am335x-guardian: Drop unused isink-en property
From: Eduard Bostina @ 2026-07-18 21:56 UTC (permalink / raw)
To: Aaro Koskinen, Andreas Kemnade, Conor Dooley, Daniel Thompson,
devicetree, Dmitry Torokhov, dri-devel, Eduard Bostina,
Jingoo Han, Kevin Hilman, Krzysztof Kozlowski, Lee Jones,
linux-input, linux-kernel, linux-leds, linux-omap, Pavel Machek,
Rob Herring, Roger Quadros, Tony Lindgren
Cc: daniel.baluta, simona.toaca, goledhruva, m-chawdhry
In-Reply-To: <20260718215632.3295049-1-egbostina@gmail.com>
The 'isink-en' property is not read by any driver, so remove it from the
TPS65217 backlight node.
Signed-off-by: Eduard Bostina <egbostina@gmail.com>
---
arch/arm/boot/dts/ti/omap/am335x-guardian.dts | 3 ---
1 file changed, 3 deletions(-)
diff --git a/arch/arm/boot/dts/ti/omap/am335x-guardian.dts b/arch/arm/boot/dts/ti/omap/am335x-guardian.dts
index 6ce3a2d029ee..d595e79d6b05 100644
--- a/arch/arm/boot/dts/ti/omap/am335x-guardian.dts
+++ b/arch/arm/boot/dts/ti/omap/am335x-guardian.dts
@@ -320,9 +320,6 @@ backlight {
isel = <1>; /* 1 - ISET1, 2 ISET2 */
fdim = <500>; /* TPS65217_BL_FDIM_500HZ */
default-brightness = <50>;
- /* 1(on) - enable current sink, while initialization */
- /* 0(off) - disable current sink, while initialization */
- isink-en = <1>;
};
regulators {
--
2.43.0
^ permalink raw reply related
* [PATCH v2 0/3] dt-bindings: Convert TI TPS65217 to DT schema
From: Eduard Bostina @ 2026-07-18 21:56 UTC (permalink / raw)
To: Aaro Koskinen, Andreas Kemnade, Conor Dooley, Daniel Thompson,
devicetree, Dmitry Torokhov, dri-devel, Eduard Bostina,
Jingoo Han, Kevin Hilman, Krzysztof Kozlowski, Lee Jones,
linux-input, linux-kernel, linux-leds, linux-omap, Pavel Machek,
Rob Herring, Roger Quadros, Tony Lindgren
Cc: daniel.baluta, simona.toaca, goledhruva, m-chawdhry
This series converts the Texas Instruments TPS65217 PMIC bindings to DT
schema.
Changes in v2:
- Set 'additionalProperties' to false and documented all child nodes
('regulators', 'charger', 'pwrbutton').
- Documented the 'interrupts', 'interrupt-controller', '#interrupt-cells'
and 'ti,pmic-shutdown-controller' properties.
- Dropped 'isink-en' from the schema. It is not read by any driver, so it
is now removed from am335x-guardian.dts instead.
- Added the power button conversion and referenced it from
the leds schema.
Eduard Bostina (3):
ARM: dts: ti: am335x-guardian: Drop unused isink-en property
dt-bindings: input: Convert TI TPS65217 power button to DT schema
dt-bindings: leds: backlight: Convert TPS65217 to DT schema
.../bindings/input/ti,tps65217-pwrbutton.yaml | 42 ++++++
.../bindings/input/tps65218-pwrbutton.txt | 30 -----
.../bindings/leds/backlight/ti,tps65217.yaml | 123 ++++++++++++++++++
.../leds/backlight/tps65217-backlight.txt | 27 ----
arch/arm/boot/dts/ti/omap/am335x-guardian.dts | 3 -
5 files changed, 165 insertions(+), 60 deletions(-)
create mode 100644 Documentation/devicetree/bindings/input/ti,tps65217-pwrbutton.yaml
delete mode 100644 Documentation/devicetree/bindings/input/tps65218-pwrbutton.txt
create mode 100644 Documentation/devicetree/bindings/leds/backlight/ti,tps65217.yaml
delete mode 100644 Documentation/devicetree/bindings/leds/backlight/tps65217-backlight.txt
--
2.43.0
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox