// 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 * Copyright (c) 2026 Denis Benato * Copyright (c) 2026 Valve Corporation */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "hid-ids.h" #define CLAW_OUTPUT_REPORT_ID 0x0f #define CLAW_INPUT_REPORT_ID 0x10 #define CLAW_PACKET_SIZE 64 #define CLAW_DINPUT_CFG_INTF_IN 0x82 #define CLAW_XINPUT_CFG_INTF_IN 0x83 #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 "); MODULE_AUTHOR("Zhouwang Huang "); MODULE_AUTHOR("Derek J. Clark "); MODULE_DESCRIPTION("HID driver for MSI Claw Handheld PC gamepads");