blob: 3225d3bf612545e9b13eb5eb96f9b70fac919ce7 [file]
// 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/led-class-multicolor.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
#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,
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_profile_ack_pending {
CLAW_NO_PENDING,
CLAW_M1_PENDING,
CLAW_M2_PENDING,
CLAW_RGB_PENDING,
CLAW_RUMBLE_LEFT_PENDING,
CLAW_RUMBLE_RIGHT_PENDING,
};
enum claw_key_index {
CLAW_KEY_M1,
CLAW_KEY_M2,
};
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",
};
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" },
};
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 */
};
static const u16 button_mapping_addr_new[] = {
0x00bb, /* M1 */
0x0164, /* M2 */
};
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];
u8 header_tail;
u8 cmd;
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 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_rumble_report {
struct claw_profile_report;
u8 padding;
u8 intensity;
} __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];
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;
/* 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)
{
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_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;
u8 *codes, key, f_idx;
u16 frame_calc;
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;
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;
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",
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)
{
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_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) {
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 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 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)
{
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;
}
/* 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 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;
}
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,
&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,
};
static const struct attribute_group claw_gamepad_attr_group = {
.attrs = claw_gamepad_attrs,
.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 read gamepad mode: %d\n", ret);
goto prep_rgb;
}
gamepad_ready = true;
prep_rgb:
ret = claw_read_rgb_config(drvdata->hdev);
if (ret) {
dev_err(&drvdata->hdev->dev,
"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_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;
}
}
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)
{
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) ||
/* 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));
}
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;
drvdata->rumble_support = true;
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->rumble_support = true;
drvdata->rgb_addr = rgb_addr_new;
return;
}
drvdata->rgb_addr = rgb_addr_old;
}
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;
drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
if (!drvdata)
return -ENOMEM;
drvdata->gamepad_mode = CLAW_GAMEPAD_MODE_XINPUT;
drvdata->rgb_enabled = true;
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");
/* 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);
spin_lock_init(&drvdata->registration_lock);
spin_lock_init(&drvdata->cmd_lock);
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);
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);
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;
bool rgb_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);
/* 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);
}
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);
cancel_delayed_work_sync(&drvdata->rgb_queue);
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");