| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * HID driver for Lenovo Legion Go S devices. |
| * |
| * Copyright (c) 2026 Derek J. Clark <derekjohn.clark@gmail.com> |
| * Copyright (c) 2026 Valve Corporation |
| */ |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
| |
| #include <linux/array_size.h> |
| #include <linux/cleanup.h> |
| #include <linux/completion.h> |
| #include <linux/delay.h> |
| #include <linux/dev_printk.h> |
| #include <linux/device.h> |
| #include <linux/hid.h> |
| #include <linux/jiffies.h> |
| #include <linux/kstrtox.h> |
| #include <linux/led-class-multicolor.h> |
| #include <linux/mutex.h> |
| #include <linux/printk.h> |
| #include <linux/string.h> |
| #include <linux/sysfs.h> |
| #include <linux/types.h> |
| #include <linux/unaligned.h> |
| #include <linux/usb.h> |
| #include <linux/workqueue.h> |
| #include <linux/workqueue_types.h> |
| |
| #include "hid-ids.h" |
| |
| #define GO_S_CFG_INTF_IN 0x84 |
| #define GO_S_PACKET_SIZE 64 |
| |
| static struct hid_gos_cfg { |
| struct delayed_work gos_cfg_setup; |
| struct completion send_cmd_complete; |
| struct led_classdev *led_cdev; |
| struct hid_device *hdev; |
| struct mutex cfg_mutex; /*ensure single synchronous output report*/ |
| u8 gp_auto_sleep_time; |
| u8 gp_dpad_mode; |
| u8 gp_mode; |
| u8 gp_poll_rate; |
| u8 imu_bypass_en; |
| u8 imu_manufacturer; |
| u8 imu_sensor_en; |
| u8 mcu_id[12]; |
| u8 mouse_step; |
| u8 os_mode; |
| u8 rgb_effect; |
| u8 rgb_en; |
| u8 rgb_mode; |
| u8 rgb_profile; |
| u8 rgb_speed; |
| u8 tp_en; |
| u8 tp_linux_mode; |
| u8 tp_windows_mode; |
| u8 tp_version; |
| u8 tp_manufacturer; |
| } drvdata; |
| |
| struct gos_cfg_attr { |
| u8 index; |
| }; |
| |
| struct command_report { |
| u8 cmd; |
| u8 sub_cmd; |
| u8 data[63]; |
| } __packed; |
| |
| struct version_report { |
| u8 cmd; |
| u32 version; |
| u8 reserved[59]; |
| } __packed; |
| |
| enum mcu_command_index { |
| GET_VERSION = 0x01, |
| GET_MCU_ID, |
| GET_GAMEPAD_CFG, |
| SET_GAMEPAD_CFG, |
| GET_TP_PARAM, |
| SET_TP_PARAM, |
| GET_RGB_CFG = 0x0f, |
| SET_RGB_CFG, |
| GET_PL_TEST = 0xdf, |
| }; |
| |
| enum feature_enabled_index { |
| FEATURE_DISABLED, |
| FEATURE_ENABLED, |
| }; |
| |
| static const char *const feature_enabled_text[] = { |
| [FEATURE_DISABLED] = "false", |
| [FEATURE_ENABLED] = "true", |
| }; |
| |
| enum feature_status_index { |
| FEATURE_NONE = 0x00, |
| FEATURE_GAMEPAD_MODE = 0x01, |
| FEATURE_AUTO_SLEEP_TIME = 0x04, |
| FEATURE_IMU_BYPASS, |
| FEATURE_RGB_ENABLE, |
| FEATURE_IMU_ENABLE, |
| FEATURE_TOUCHPAD_ENABLE, |
| FEATURE_OS_MODE = 0x0A, |
| FEATURE_POLL_RATE = 0x10, |
| FEATURE_DPAD_MODE, |
| FEATURE_MOUSE_WHEEL_STEP, |
| }; |
| |
| enum gamepad_mode_index { |
| XINPUT, |
| DINPUT, |
| }; |
| |
| static const char *const gamepad_mode_text[] = { |
| [XINPUT] = "xinput", |
| [DINPUT] = "dinput", |
| }; |
| |
| enum os_type_index { |
| WINDOWS, |
| LINUX, |
| }; |
| |
| static const char *const os_type_text[] = { |
| [WINDOWS] = "windows", |
| [LINUX] = "linux", |
| }; |
| |
| enum poll_rate_index { |
| HZ125, |
| HZ250, |
| HZ500, |
| HZ1000, |
| }; |
| |
| static const char *const poll_rate_text[] = { |
| [HZ125] = "125", |
| [HZ250] = "250", |
| [HZ500] = "500", |
| [HZ1000] = "1000", |
| }; |
| |
| enum dpad_mode_index { |
| DIR8, |
| DIR4, |
| }; |
| |
| static const char *const dpad_mode_text[] = { |
| [DIR8] = "8-way", |
| [DIR4] = "4-way", |
| }; |
| |
| enum touchpad_mode_index { |
| TP_REL, |
| TP_ABS, |
| }; |
| |
| static const char *const touchpad_mode_text[] = { |
| [TP_REL] = "relative", |
| [TP_ABS] = "absolute", |
| }; |
| |
| enum touchpad_config_index { |
| CFG_WINDOWS_MODE = 0x03, |
| CFG_LINUX_MODE, |
| |
| }; |
| |
| enum rgb_mode_index { |
| RGB_MODE_DYNAMIC, |
| RGB_MODE_CUSTOM, |
| }; |
| |
| static const char *const rgb_mode_text[] = { |
| [RGB_MODE_DYNAMIC] = "dynamic", |
| [RGB_MODE_CUSTOM] = "custom", |
| }; |
| |
| enum rgb_effect_index { |
| RGB_EFFECT_MONO, |
| RGB_EFFECT_BREATHE, |
| RGB_EFFECT_CHROMA, |
| RGB_EFFECT_RAINBOW, |
| }; |
| |
| static const char *const rgb_effect_text[] = { |
| [RGB_EFFECT_MONO] = "monocolor", |
| [RGB_EFFECT_BREATHE] = "breathe", |
| [RGB_EFFECT_CHROMA] = "chroma", |
| [RGB_EFFECT_RAINBOW] = "rainbow", |
| }; |
| |
| enum rgb_config_index { |
| LIGHT_MODE_SEL = 0x01, |
| LIGHT_PROFILE_SEL, |
| USR_LIGHT_PROFILE_1, |
| USR_LIGHT_PROFILE_2, |
| USR_LIGHT_PROFILE_3, |
| }; |
| |
| enum test_command_index { |
| TEST_TP_MFR = 0x02, |
| TEST_IMU_MFR, |
| TEST_TP_VER, |
| }; |
| |
| enum tp_mfr_index { |
| TP_NONE, |
| TP_BETTERLIFE, |
| TP_SIPO, |
| }; |
| |
| static const char *const touchpad_manufacturer_text[] = { |
| [TP_NONE] = "none", |
| [TP_BETTERLIFE] = "BetterLife", |
| [TP_SIPO] = "SIPO", |
| }; |
| |
| enum imu_mfr_index { |
| IMU_NONE, |
| IMU_BOSCH, |
| IMU_ST, |
| }; |
| |
| static const char *const imu_manufacturer_text[] = { |
| [IMU_NONE] = "none", |
| [IMU_BOSCH] = "Bosch", |
| [IMU_ST] = "ST", |
| }; |
| |
| static int hid_gos_version_event(u8 *data) |
| { |
| struct version_report *ver_rep = (struct version_report *)data; |
| |
| drvdata.hdev->firmware_version = get_unaligned_le32(&ver_rep->version); |
| return 0; |
| } |
| |
| static int hid_gos_mcu_id_event(struct command_report *cmd_rep) |
| { |
| drvdata.mcu_id[0] = cmd_rep->sub_cmd; |
| memcpy(&drvdata.mcu_id[1], cmd_rep->data, 11); |
| |
| return 0; |
| } |
| |
| static int hid_gos_gamepad_cfg_event(struct command_report *cmd_rep) |
| { |
| int ret = 0; |
| |
| switch (cmd_rep->sub_cmd) { |
| case FEATURE_GAMEPAD_MODE: |
| drvdata.gp_mode = cmd_rep->data[0]; |
| break; |
| case FEATURE_AUTO_SLEEP_TIME: |
| drvdata.gp_auto_sleep_time = cmd_rep->data[0]; |
| break; |
| case FEATURE_IMU_BYPASS: |
| drvdata.imu_bypass_en = cmd_rep->data[0]; |
| break; |
| case FEATURE_RGB_ENABLE: |
| drvdata.rgb_en = cmd_rep->data[0]; |
| break; |
| case FEATURE_IMU_ENABLE: |
| drvdata.imu_sensor_en = cmd_rep->data[0]; |
| break; |
| case FEATURE_TOUCHPAD_ENABLE: |
| drvdata.tp_en = cmd_rep->data[0]; |
| break; |
| case FEATURE_OS_MODE: |
| drvdata.os_mode = cmd_rep->data[0]; |
| break; |
| case FEATURE_POLL_RATE: |
| drvdata.gp_poll_rate = cmd_rep->data[0]; |
| break; |
| case FEATURE_DPAD_MODE: |
| drvdata.gp_dpad_mode = cmd_rep->data[0]; |
| break; |
| case FEATURE_MOUSE_WHEEL_STEP: |
| drvdata.mouse_step = cmd_rep->data[0]; |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| |
| return ret; |
| } |
| |
| static int hid_gos_touchpad_event(struct command_report *cmd_rep) |
| { |
| int ret = 0; |
| |
| switch (cmd_rep->sub_cmd) { |
| case CFG_LINUX_MODE: |
| drvdata.tp_linux_mode = cmd_rep->data[0]; |
| break; |
| case CFG_WINDOWS_MODE: |
| drvdata.tp_windows_mode = cmd_rep->data[0]; |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| |
| return ret; |
| } |
| |
| static int hid_gos_pl_test_event(struct command_report *cmd_rep) |
| { |
| int ret = 0; |
| |
| switch (cmd_rep->sub_cmd) { |
| case TEST_TP_MFR: |
| drvdata.tp_manufacturer = cmd_rep->data[0]; |
| ret = 0; |
| break; |
| case TEST_IMU_MFR: |
| drvdata.imu_manufacturer = cmd_rep->data[0]; |
| ret = 0; |
| break; |
| case TEST_TP_VER: |
| drvdata.tp_version = cmd_rep->data[0]; |
| ret = 0; |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| return ret; |
| } |
| |
| static int hid_gos_light_event(struct command_report *cmd_rep) |
| { |
| struct led_classdev_mc *mc_cdev; |
| int ret = 0; |
| |
| switch (cmd_rep->sub_cmd) { |
| case LIGHT_MODE_SEL: |
| drvdata.rgb_mode = cmd_rep->data[0]; |
| ret = 0; |
| break; |
| case LIGHT_PROFILE_SEL: |
| drvdata.rgb_profile = cmd_rep->data[0]; |
| ret = 0; |
| break; |
| case USR_LIGHT_PROFILE_1: |
| case USR_LIGHT_PROFILE_2: |
| case USR_LIGHT_PROFILE_3: |
| mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); |
| drvdata.rgb_effect = cmd_rep->data[0]; |
| mc_cdev->subled_info[0].intensity = cmd_rep->data[1]; |
| mc_cdev->subled_info[1].intensity = cmd_rep->data[2]; |
| mc_cdev->subled_info[2].intensity = cmd_rep->data[3]; |
| drvdata.led_cdev->brightness = cmd_rep->data[4]; |
| drvdata.rgb_speed = cmd_rep->data[5]; |
| ret = 0; |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| return ret; |
| } |
| |
| static int hid_gos_set_event_return(struct command_report *cmd_rep) |
| { |
| if (cmd_rep->data[0] != 0) |
| return -EIO; |
| |
| return 0; |
| } |
| |
| static int get_endpoint_address(struct hid_device *hdev) |
| { |
| struct usb_interface *intf = to_usb_interface(hdev->dev.parent); |
| struct usb_host_endpoint *ep; |
| |
| if (intf) { |
| ep = intf->cur_altsetting->endpoint; |
| if (ep) |
| return ep->desc.bEndpointAddress; |
| } |
| |
| return -ENODEV; |
| } |
| |
| static int hid_gos_raw_event(struct hid_device *hdev, struct hid_report *report, |
| u8 *data, int size) |
| { |
| struct command_report *cmd_rep; |
| int ep, ret; |
| |
| ep = get_endpoint_address(hdev); |
| if (ep != GO_S_CFG_INTF_IN) |
| return 0; |
| |
| if (size != GO_S_PACKET_SIZE) |
| return -EINVAL; |
| |
| cmd_rep = (struct command_report *)data; |
| |
| switch (cmd_rep->cmd) { |
| case GET_VERSION: |
| ret = hid_gos_version_event(data); |
| break; |
| case GET_MCU_ID: |
| ret = hid_gos_mcu_id_event(cmd_rep); |
| break; |
| case GET_GAMEPAD_CFG: |
| ret = hid_gos_gamepad_cfg_event(cmd_rep); |
| break; |
| case GET_TP_PARAM: |
| ret = hid_gos_touchpad_event(cmd_rep); |
| break; |
| case GET_PL_TEST: |
| ret = hid_gos_pl_test_event(cmd_rep); |
| break; |
| case GET_RGB_CFG: |
| ret = hid_gos_light_event(cmd_rep); |
| break; |
| case SET_GAMEPAD_CFG: |
| case SET_RGB_CFG: |
| case SET_TP_PARAM: |
| ret = hid_gos_set_event_return(cmd_rep); |
| break; |
| default: |
| ret = -EINVAL; |
| break; |
| } |
| dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n", |
| GO_S_PACKET_SIZE, data); |
| |
| complete(&drvdata.send_cmd_complete); |
| return ret; |
| } |
| |
| static int mcu_property_out(struct hid_device *hdev, u8 command, u8 index, |
| u8 *data, size_t len) |
| { |
| unsigned char *dmabuf __free(kfree) = NULL; |
| u8 header[] = { command, index }; |
| size_t header_size = ARRAY_SIZE(header); |
| int timeout, ret; |
| |
| if (header_size + len > GO_S_PACKET_SIZE) |
| return -EINVAL; |
| |
| guard(mutex)(&drvdata.cfg_mutex); |
| /* We can't use a devm_alloc reusable buffer without side effects during suspend */ |
| dmabuf = kzalloc(GO_S_PACKET_SIZE, GFP_KERNEL); |
| if (!dmabuf) |
| return -ENOMEM; |
| |
| memcpy(dmabuf, header, header_size); |
| memcpy(dmabuf + header_size, data, len); |
| |
| dev_dbg(&hdev->dev, "Send data as raw output report: [%*ph]\n", |
| GO_S_PACKET_SIZE, dmabuf); |
| |
| ret = hid_hw_output_report(hdev, dmabuf, GO_S_PACKET_SIZE); |
| if (ret < 0) |
| return ret; |
| |
| ret = ret == GO_S_PACKET_SIZE ? 0 : -EINVAL; |
| if (ret) |
| return ret; |
| |
| /* PL_TEST commands can take longer because they go out to another device */ |
| timeout = (command == GET_PL_TEST) ? 200 : 5; |
| ret = wait_for_completion_interruptible_timeout(&drvdata.send_cmd_complete, |
| msecs_to_jiffies(timeout)); |
| |
| if (ret == 0) /* timeout occurred */ |
| ret = -EBUSY; |
| |
| reinit_completion(&drvdata.send_cmd_complete); |
| return 0; |
| } |
| |
| static ssize_t gamepad_property_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count, |
| enum feature_status_index index) |
| { |
| size_t size = 1; |
| u8 val = 0; |
| int ret; |
| |
| switch (index) { |
| case FEATURE_GAMEPAD_MODE: |
| ret = sysfs_match_string(gamepad_mode_text, buf); |
| if (ret < 0) |
| return ret; |
| val = ret; |
| break; |
| case FEATURE_AUTO_SLEEP_TIME: |
| ret = kstrtou8(buf, 10, &val); |
| if (ret) |
| return ret; |
| break; |
| case FEATURE_IMU_ENABLE: |
| ret = sysfs_match_string(feature_enabled_text, buf); |
| if (ret < 0) |
| return ret; |
| val = ret; |
| break; |
| case FEATURE_IMU_BYPASS: |
| ret = sysfs_match_string(feature_enabled_text, buf); |
| if (ret < 0) |
| return ret; |
| val = ret; |
| break; |
| case FEATURE_RGB_ENABLE: |
| ret = sysfs_match_string(feature_enabled_text, buf); |
| if (ret < 0) |
| return ret; |
| val = ret; |
| break; |
| case FEATURE_TOUCHPAD_ENABLE: |
| ret = sysfs_match_string(feature_enabled_text, buf); |
| if (ret < 0) |
| return ret; |
| val = ret; |
| break; |
| case FEATURE_OS_MODE: |
| ret = sysfs_match_string(os_type_text, buf); |
| if (ret < 0) |
| return ret; |
| val = ret; |
| break; |
| case FEATURE_POLL_RATE: |
| ret = sysfs_match_string(poll_rate_text, buf); |
| if (ret < 0) |
| return ret; |
| val = ret; |
| break; |
| case FEATURE_DPAD_MODE: |
| ret = sysfs_match_string(dpad_mode_text, buf); |
| if (ret < 0) |
| return ret; |
| val = ret; |
| break; |
| case FEATURE_MOUSE_WHEEL_STEP: |
| ret = kstrtou8(buf, 10, &val); |
| if (ret) |
| return ret; |
| if (val < 1 || val > 127) |
| return -EINVAL; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| if (!val) |
| size = 0; |
| |
| ret = mcu_property_out(drvdata.hdev, SET_GAMEPAD_CFG, index, &val, |
| size); |
| if (ret < 0) |
| return ret; |
| |
| return count; |
| } |
| |
| static ssize_t gamepad_property_show(struct device *dev, |
| struct device_attribute *attr, char *buf, |
| enum feature_status_index index) |
| { |
| ssize_t count = 0; |
| u8 i; |
| |
| count = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, index, NULL, 0); |
| if (count < 0) |
| return count; |
| |
| switch (index) { |
| case FEATURE_GAMEPAD_MODE: |
| i = drvdata.gp_mode; |
| if (i >= ARRAY_SIZE(gamepad_mode_text)) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%s\n", gamepad_mode_text[i]); |
| break; |
| case FEATURE_AUTO_SLEEP_TIME: |
| count = sysfs_emit(buf, "%u\n", drvdata.gp_auto_sleep_time); |
| break; |
| case FEATURE_IMU_ENABLE: |
| i = drvdata.imu_sensor_en; |
| if (i >= ARRAY_SIZE(feature_enabled_text)) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]); |
| break; |
| case FEATURE_IMU_BYPASS: |
| i = drvdata.imu_bypass_en; |
| if (i >= ARRAY_SIZE(feature_enabled_text)) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]); |
| break; |
| case FEATURE_RGB_ENABLE: |
| i = drvdata.rgb_en; |
| if (i >= ARRAY_SIZE(feature_enabled_text)) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]); |
| break; |
| case FEATURE_TOUCHPAD_ENABLE: |
| i = drvdata.tp_en; |
| if (i >= ARRAY_SIZE(feature_enabled_text)) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%s\n", feature_enabled_text[i]); |
| break; |
| case FEATURE_OS_MODE: |
| i = drvdata.os_mode; |
| if (i >= ARRAY_SIZE(os_type_text)) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%s\n", os_type_text[i]); |
| break; |
| case FEATURE_POLL_RATE: |
| i = drvdata.gp_poll_rate; |
| if (i >= ARRAY_SIZE(poll_rate_text)) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%s\n", poll_rate_text[i]); |
| break; |
| case FEATURE_DPAD_MODE: |
| i = drvdata.gp_dpad_mode; |
| if (i >= ARRAY_SIZE(dpad_mode_text)) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%s\n", dpad_mode_text[i]); |
| break; |
| case FEATURE_MOUSE_WHEEL_STEP: |
| i = drvdata.mouse_step; |
| if (i < 1 || i > 127) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%u\n", i); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return count; |
| } |
| |
| static ssize_t gamepad_property_options(struct device *dev, |
| struct device_attribute *attr, |
| char *buf, |
| enum feature_status_index index) |
| { |
| size_t count = 0; |
| unsigned int i; |
| |
| switch (index) { |
| case FEATURE_GAMEPAD_MODE: |
| for (i = 0; i < ARRAY_SIZE(gamepad_mode_text); i++) { |
| count += sysfs_emit_at(buf, count, "%s ", |
| gamepad_mode_text[i]); |
| } |
| break; |
| case FEATURE_AUTO_SLEEP_TIME: |
| return sysfs_emit(buf, "0-255\n"); |
| case FEATURE_IMU_ENABLE: |
| for (i = 0; i < ARRAY_SIZE(feature_enabled_text); i++) { |
| count += sysfs_emit_at(buf, count, "%s ", |
| feature_enabled_text[i]); |
| } |
| break; |
| case FEATURE_IMU_BYPASS: |
| case FEATURE_RGB_ENABLE: |
| case FEATURE_TOUCHPAD_ENABLE: |
| for (i = 0; i < ARRAY_SIZE(feature_enabled_text); i++) { |
| count += sysfs_emit_at(buf, count, "%s ", |
| feature_enabled_text[i]); |
| } |
| break; |
| case FEATURE_OS_MODE: |
| for (i = 0; i < ARRAY_SIZE(os_type_text); i++) { |
| count += sysfs_emit_at(buf, count, "%s ", |
| os_type_text[i]); |
| } |
| break; |
| case FEATURE_POLL_RATE: |
| for (i = 0; i < ARRAY_SIZE(poll_rate_text); i++) { |
| count += sysfs_emit_at(buf, count, "%s ", |
| poll_rate_text[i]); |
| } |
| break; |
| case FEATURE_DPAD_MODE: |
| for (i = 0; i < ARRAY_SIZE(dpad_mode_text); i++) { |
| count += sysfs_emit_at(buf, count, "%s ", |
| dpad_mode_text[i]); |
| } |
| break; |
| case FEATURE_MOUSE_WHEEL_STEP: |
| return sysfs_emit(buf, "1-127\n"); |
| default: |
| return count; |
| } |
| |
| if (count) |
| buf[count - 1] = '\n'; |
| |
| return count; |
| } |
| |
| static ssize_t touchpad_property_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t count, |
| enum touchpad_config_index index) |
| { |
| size_t size = 1; |
| u8 val = 0; |
| int ret; |
| |
| switch (index) { |
| case CFG_WINDOWS_MODE: |
| ret = sysfs_match_string(touchpad_mode_text, buf); |
| if (ret < 0) |
| return ret; |
| val = ret; |
| break; |
| case CFG_LINUX_MODE: |
| ret = sysfs_match_string(touchpad_mode_text, buf); |
| if (ret < 0) |
| return ret; |
| val = ret; |
| break; |
| default: |
| return -EINVAL; |
| } |
| if (!val) |
| size = 0; |
| |
| ret = mcu_property_out(drvdata.hdev, SET_TP_PARAM, index, &val, size); |
| if (ret < 0) |
| return ret; |
| |
| return count; |
| } |
| |
| static ssize_t touchpad_property_show(struct device *dev, |
| struct device_attribute *attr, char *buf, |
| enum touchpad_config_index index) |
| { |
| int ret = 0; |
| u8 i; |
| |
| ret = mcu_property_out(drvdata.hdev, GET_TP_PARAM, index, NULL, 0); |
| if (ret < 0) |
| return ret; |
| |
| switch (index) { |
| case CFG_WINDOWS_MODE: |
| i = drvdata.tp_windows_mode; |
| break; |
| case CFG_LINUX_MODE: |
| i = drvdata.tp_linux_mode; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| if (i >= ARRAY_SIZE(touchpad_mode_text)) |
| return -EINVAL; |
| |
| return sysfs_emit(buf, "%s\n", touchpad_mode_text[i]); |
| } |
| |
| static ssize_t touchpad_property_options(struct device *dev, |
| struct device_attribute *attr, |
| char *buf, |
| enum touchpad_config_index index) |
| { |
| size_t count = 0; |
| unsigned int i; |
| |
| switch (index) { |
| case CFG_WINDOWS_MODE: |
| case CFG_LINUX_MODE: |
| for (i = 0; i < ARRAY_SIZE(touchpad_mode_text); i++) { |
| count += sysfs_emit_at(buf, count, "%s ", |
| touchpad_mode_text[i]); |
| } |
| break; |
| default: |
| return count; |
| } |
| |
| if (count) |
| buf[count - 1] = '\n'; |
| |
| return count; |
| } |
| |
| static ssize_t test_property_show(struct device *dev, |
| struct device_attribute *attr, char *buf, |
| enum test_command_index index) |
| { |
| size_t count = 0; |
| u8 i; |
| |
| switch (index) { |
| case TEST_TP_MFR: |
| i = drvdata.tp_manufacturer; |
| if (i >= ARRAY_SIZE(touchpad_manufacturer_text)) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%s\n", touchpad_manufacturer_text[i]); |
| break; |
| case TEST_IMU_MFR: |
| i = drvdata.imu_manufacturer; |
| if (i >= ARRAY_SIZE(imu_manufacturer_text)) |
| return -EINVAL; |
| count = sysfs_emit(buf, "%s\n", imu_manufacturer_text[i]); |
| break; |
| case TEST_TP_VER: |
| count = sysfs_emit(buf, "%u\n", drvdata.tp_version); |
| break; |
| default: |
| count = -EINVAL; |
| break; |
| } |
| |
| return count; |
| } |
| |
| static ssize_t mcu_id_show(struct device *dev, struct device_attribute *attr, |
| char *buf) |
| { |
| return sysfs_emit(buf, "%*phN\n", 12, &drvdata.mcu_id); |
| } |
| |
| static int rgb_cfg_call(struct hid_device *hdev, enum mcu_command_index cmd, |
| enum rgb_config_index index, u8 *val, size_t size) |
| { |
| if (cmd != SET_RGB_CFG && cmd != GET_RGB_CFG) |
| return -EINVAL; |
| |
| if (index < LIGHT_MODE_SEL || index > USR_LIGHT_PROFILE_3) |
| return -EINVAL; |
| |
| return mcu_property_out(hdev, cmd, index, val, size); |
| } |
| |
| static int rgb_attr_show(void) |
| { |
| enum rgb_config_index index; |
| |
| index = drvdata.rgb_profile + 2; |
| |
| return rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, index, NULL, 0); |
| }; |
| |
| static ssize_t rgb_effect_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); |
| enum rgb_config_index index; |
| u8 effect; |
| int ret; |
| |
| ret = sysfs_match_string(rgb_effect_text, buf); |
| if (ret < 0) |
| return ret; |
| |
| effect = ret; |
| index = drvdata.rgb_profile + 2; |
| u8 rgb_profile[6] = { effect, |
| mc_cdev->subled_info[0].intensity, |
| mc_cdev->subled_info[1].intensity, |
| mc_cdev->subled_info[2].intensity, |
| drvdata.led_cdev->brightness, |
| drvdata.rgb_speed }; |
| |
| ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6); |
| if (ret) |
| return ret; |
| |
| drvdata.rgb_effect = effect; |
| return count; |
| }; |
| |
| static ssize_t rgb_effect_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| int ret; |
| |
| ret = rgb_attr_show(); |
| if (ret) |
| return ret; |
| |
| if (drvdata.rgb_effect >= ARRAY_SIZE(rgb_effect_text)) |
| return -EINVAL; |
| |
| return sysfs_emit(buf, "%s\n", rgb_effect_text[drvdata.rgb_effect]); |
| } |
| |
| static ssize_t rgb_effect_index_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| ssize_t count = 0; |
| unsigned int i; |
| |
| for (i = 0; i < ARRAY_SIZE(rgb_effect_text); i++) |
| count += sysfs_emit_at(buf, count, "%s ", rgb_effect_text[i]); |
| |
| if (count) |
| buf[count - 1] = '\n'; |
| |
| return count; |
| } |
| |
| static ssize_t rgb_speed_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); |
| enum rgb_config_index index; |
| int val = 0; |
| int ret; |
| |
| ret = kstrtoint(buf, 10, &val); |
| if (ret) |
| return ret; |
| |
| if (val < 0 || val > 100) |
| return -EINVAL; |
| |
| index = drvdata.rgb_profile + 2; |
| u8 rgb_profile[6] = { drvdata.rgb_effect, |
| mc_cdev->subled_info[0].intensity, |
| mc_cdev->subled_info[1].intensity, |
| mc_cdev->subled_info[2].intensity, |
| drvdata.led_cdev->brightness, |
| val }; |
| |
| ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6); |
| if (ret) |
| return ret; |
| |
| drvdata.rgb_speed = val; |
| |
| return count; |
| }; |
| |
| static ssize_t rgb_speed_show(struct device *dev, struct device_attribute *attr, |
| char *buf) |
| { |
| int ret; |
| |
| ret = rgb_attr_show(); |
| if (ret) |
| return ret; |
| |
| if (drvdata.rgb_speed > 100) |
| return -EINVAL; |
| |
| return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed); |
| } |
| |
| static ssize_t rgb_speed_range_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| return sysfs_emit(buf, "0-100\n"); |
| } |
| |
| static ssize_t rgb_mode_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| int ret; |
| u8 val; |
| |
| ret = sysfs_match_string(rgb_mode_text, buf); |
| if (ret <= 0) |
| return ret; |
| |
| val = ret; |
| |
| ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_MODE_SEL, &val, |
| 1); |
| if (ret) |
| return ret; |
| |
| drvdata.rgb_mode = val; |
| |
| return count; |
| }; |
| |
| static ssize_t rgb_mode_show(struct device *dev, struct device_attribute *attr, |
| char *buf) |
| { |
| int ret; |
| |
| ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_MODE_SEL, NULL, 0); |
| if (ret) |
| return ret; |
| |
| if (drvdata.rgb_mode >= ARRAY_SIZE(rgb_mode_text)) |
| return -EINVAL; |
| |
| return sysfs_emit(buf, "%s\n", rgb_mode_text[drvdata.rgb_mode]); |
| }; |
| |
| static ssize_t rgb_mode_index_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| ssize_t count = 0; |
| unsigned int i; |
| |
| for (i = 1; i < ARRAY_SIZE(rgb_mode_text); i++) |
| count += sysfs_emit_at(buf, count, "%s ", rgb_mode_text[i]); |
| |
| if (count) |
| buf[count - 1] = '\n'; |
| |
| return count; |
| } |
| |
| static ssize_t rgb_profile_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| size_t size = 1; |
| int ret; |
| u8 val; |
| |
| ret = kstrtou8(buf, 10, &val); |
| if (ret < 0) |
| return ret; |
| |
| if (val < 1 || val > 3) |
| return -EINVAL; |
| |
| ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, LIGHT_PROFILE_SEL, &val, size); |
| if (ret) |
| return ret; |
| |
| drvdata.rgb_profile = val; |
| |
| return count; |
| }; |
| |
| static ssize_t rgb_profile_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| int ret; |
| |
| ret = rgb_cfg_call(drvdata.hdev, GET_RGB_CFG, LIGHT_PROFILE_SEL, NULL, 0); |
| if (ret) |
| return ret; |
| |
| if (drvdata.rgb_profile < 1 || drvdata.rgb_profile > 3) |
| return -EINVAL; |
| |
| return sysfs_emit(buf, "%hhu\n", drvdata.rgb_profile); |
| }; |
| |
| static ssize_t rgb_profile_range_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| return sysfs_emit(buf, "1-3\n"); |
| } |
| |
| static void hid_gos_brightness_set(struct led_classdev *led_cdev, |
| enum led_brightness brightness) |
| { |
| struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(drvdata.led_cdev); |
| enum rgb_config_index index; |
| int ret; |
| |
| if (brightness > led_cdev->max_brightness) { |
| dev_err(led_cdev->dev, "Invalid argument\n"); |
| return; |
| } |
| |
| index = drvdata.rgb_profile + 2; |
| u8 rgb_profile[6] = { drvdata.rgb_effect, |
| mc_cdev->subled_info[0].intensity, |
| mc_cdev->subled_info[1].intensity, |
| mc_cdev->subled_info[2].intensity, |
| brightness, |
| drvdata.rgb_speed }; |
| |
| ret = rgb_cfg_call(drvdata.hdev, SET_RGB_CFG, index, rgb_profile, 6); |
| switch (ret) { |
| case 0: |
| led_cdev->brightness = brightness; |
| break; |
| case -ENODEV: /* during switch to IAP -ENODEV is expected */ |
| case -ENOSYS: /* during rmmod -ENOSYS is expected */ |
| dev_dbg(led_cdev->dev, "Failed to write RGB profile: %i\n", |
| ret); |
| break; |
| default: |
| dev_err(led_cdev->dev, "Failed to write RGB profile: %i\n", |
| ret); |
| } |
| } |
| |
| #define LEGOS_DEVICE_ATTR_RW(_name, _attrname, _rtype, _group) \ |
| static ssize_t _name##_store(struct device *dev, \ |
| struct device_attribute *attr, \ |
| const char *buf, size_t count) \ |
| { \ |
| return _group##_property_store(dev, attr, buf, count, \ |
| _name.index); \ |
| } \ |
| static ssize_t _name##_show(struct device *dev, \ |
| struct device_attribute *attr, char *buf) \ |
| { \ |
| return _group##_property_show(dev, attr, buf, _name.index); \ |
| } \ |
| static ssize_t _name##_##_rtype##_show( \ |
| struct device *dev, struct device_attribute *attr, char *buf) \ |
| { \ |
| return _group##_property_options(dev, attr, buf, _name.index); \ |
| } \ |
| static DEVICE_ATTR_RW_NAMED(_name, _attrname) |
| |
| #define LEGOS_DEVICE_ATTR_RO(_name, _attrname, _group) \ |
| static ssize_t _name##_show(struct device *dev, \ |
| struct device_attribute *attr, char *buf) \ |
| { \ |
| return _group##_property_show(dev, attr, buf, _name.index); \ |
| } \ |
| static DEVICE_ATTR_RO_NAMED(_name, _attrname) |
| |
| /* Gamepad */ |
| static struct gos_cfg_attr auto_sleep_time = { FEATURE_AUTO_SLEEP_TIME }; |
| LEGOS_DEVICE_ATTR_RW(auto_sleep_time, "auto_sleep_time", range, gamepad); |
| static DEVICE_ATTR_RO(auto_sleep_time_range); |
| |
| static struct gos_cfg_attr dpad_mode = { FEATURE_DPAD_MODE }; |
| LEGOS_DEVICE_ATTR_RW(dpad_mode, "dpad_mode", index, gamepad); |
| static DEVICE_ATTR_RO(dpad_mode_index); |
| |
| static struct gos_cfg_attr gamepad_mode = { FEATURE_GAMEPAD_MODE }; |
| LEGOS_DEVICE_ATTR_RW(gamepad_mode, "mode", index, gamepad); |
| static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index"); |
| |
| static struct gos_cfg_attr gamepad_poll_rate = { FEATURE_POLL_RATE }; |
| LEGOS_DEVICE_ATTR_RW(gamepad_poll_rate, "poll_rate", index, gamepad); |
| static DEVICE_ATTR_RO_NAMED(gamepad_poll_rate_index, "poll_rate_index"); |
| |
| static struct attribute *legos_gamepad_attrs[] = { |
| &dev_attr_auto_sleep_time.attr, |
| &dev_attr_auto_sleep_time_range.attr, |
| &dev_attr_dpad_mode.attr, |
| &dev_attr_dpad_mode_index.attr, |
| &dev_attr_gamepad_mode.attr, |
| &dev_attr_gamepad_mode_index.attr, |
| &dev_attr_gamepad_poll_rate.attr, |
| &dev_attr_gamepad_poll_rate_index.attr, |
| NULL, |
| }; |
| |
| static const struct attribute_group gamepad_attr_group = { |
| .name = "gamepad", |
| .attrs = legos_gamepad_attrs, |
| }; |
| |
| /* IMU */ |
| static struct gos_cfg_attr imu_bypass_enabled = { FEATURE_IMU_BYPASS }; |
| LEGOS_DEVICE_ATTR_RW(imu_bypass_enabled, "bypass_enabled", index, gamepad); |
| static DEVICE_ATTR_RO_NAMED(imu_bypass_enabled_index, "bypass_enabled_index"); |
| |
| static struct gos_cfg_attr imu_manufacturer = { TEST_IMU_MFR }; |
| LEGOS_DEVICE_ATTR_RO(imu_manufacturer, "manufacturer", test); |
| |
| static struct gos_cfg_attr imu_sensor_enabled = { FEATURE_IMU_ENABLE }; |
| LEGOS_DEVICE_ATTR_RW(imu_sensor_enabled, "sensor_enabled", index, gamepad); |
| static DEVICE_ATTR_RO_NAMED(imu_sensor_enabled_index, "sensor_enabled_index"); |
| |
| static struct attribute *legos_imu_attrs[] = { |
| &dev_attr_imu_bypass_enabled.attr, |
| &dev_attr_imu_bypass_enabled_index.attr, |
| &dev_attr_imu_manufacturer.attr, |
| &dev_attr_imu_sensor_enabled.attr, |
| &dev_attr_imu_sensor_enabled_index.attr, |
| NULL, |
| }; |
| |
| static const struct attribute_group imu_attr_group = { |
| .name = "imu", |
| .attrs = legos_imu_attrs, |
| }; |
| |
| /* MCU */ |
| static DEVICE_ATTR_RO(mcu_id); |
| |
| static struct gos_cfg_attr os_mode = { FEATURE_OS_MODE }; |
| LEGOS_DEVICE_ATTR_RW(os_mode, "os_mode", index, gamepad); |
| static DEVICE_ATTR_RO(os_mode_index); |
| |
| static struct attribute *legos_mcu_attrs[] = { |
| &dev_attr_mcu_id.attr, |
| &dev_attr_os_mode.attr, |
| &dev_attr_os_mode_index.attr, |
| NULL, |
| }; |
| |
| static const struct attribute_group mcu_attr_group = { |
| .attrs = legos_mcu_attrs, |
| }; |
| |
| /* Mouse */ |
| static struct gos_cfg_attr mouse_wheel_step = { FEATURE_MOUSE_WHEEL_STEP }; |
| LEGOS_DEVICE_ATTR_RW(mouse_wheel_step, "step", range, gamepad); |
| static DEVICE_ATTR_RO_NAMED(mouse_wheel_step_range, "step_range"); |
| |
| static struct attribute *legos_mouse_attrs[] = { |
| &dev_attr_mouse_wheel_step.attr, |
| &dev_attr_mouse_wheel_step_range.attr, |
| NULL, |
| }; |
| |
| static const struct attribute_group mouse_attr_group = { |
| .name = "mouse", |
| .attrs = legos_mouse_attrs, |
| }; |
| |
| /* Touchpad */ |
| static struct gos_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE }; |
| LEGOS_DEVICE_ATTR_RW(touchpad_enabled, "enabled", index, gamepad); |
| static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index"); |
| |
| static struct gos_cfg_attr touchpad_linux_mode = { CFG_LINUX_MODE }; |
| LEGOS_DEVICE_ATTR_RW(touchpad_linux_mode, "linux_mode", index, touchpad); |
| static DEVICE_ATTR_RO_NAMED(touchpad_linux_mode_index, "linux_mode_index"); |
| |
| static struct gos_cfg_attr touchpad_manufacturer = { TEST_TP_MFR }; |
| LEGOS_DEVICE_ATTR_RO(touchpad_manufacturer, "manufacturer", test); |
| |
| static struct gos_cfg_attr touchpad_version = { TEST_TP_VER }; |
| LEGOS_DEVICE_ATTR_RO(touchpad_version, "version", test); |
| |
| static struct gos_cfg_attr touchpad_windows_mode = { CFG_WINDOWS_MODE }; |
| LEGOS_DEVICE_ATTR_RW(touchpad_windows_mode, "windows_mode", index, touchpad); |
| static DEVICE_ATTR_RO_NAMED(touchpad_windows_mode_index, "windows_mode_index"); |
| |
| static struct attribute *legos_touchpad_attrs[] = { |
| &dev_attr_touchpad_enabled.attr, |
| &dev_attr_touchpad_enabled_index.attr, |
| &dev_attr_touchpad_linux_mode.attr, |
| &dev_attr_touchpad_linux_mode_index.attr, |
| &dev_attr_touchpad_manufacturer.attr, |
| &dev_attr_touchpad_version.attr, |
| &dev_attr_touchpad_windows_mode.attr, |
| &dev_attr_touchpad_windows_mode_index.attr, |
| NULL, |
| }; |
| |
| static const struct attribute_group touchpad_attr_group = { |
| .name = "touchpad", |
| .attrs = legos_touchpad_attrs, |
| }; |
| |
| static const struct attribute_group *top_level_attr_groups[] = { |
| &gamepad_attr_group, |
| &imu_attr_group, |
| &mcu_attr_group, |
| &mouse_attr_group, |
| &touchpad_attr_group, |
| NULL, |
| }; |
| |
| /* RGB */ |
| static struct gos_cfg_attr rgb_enabled = { FEATURE_RGB_ENABLE }; |
| LEGOS_DEVICE_ATTR_RW(rgb_enabled, "enabled", index, gamepad); |
| static DEVICE_ATTR_RO_NAMED(rgb_enabled_index, "enabled_index"); |
| |
| static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect"); |
| static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index"); |
| static DEVICE_ATTR_RW_NAMED(rgb_mode, "mode"); |
| static DEVICE_ATTR_RO_NAMED(rgb_mode_index, "mode_index"); |
| static DEVICE_ATTR_RW_NAMED(rgb_profile, "profile"); |
| static DEVICE_ATTR_RO_NAMED(rgb_profile_range, "profile_range"); |
| static DEVICE_ATTR_RW_NAMED(rgb_speed, "speed"); |
| static DEVICE_ATTR_RO_NAMED(rgb_speed_range, "speed_range"); |
| |
| static struct attribute *gos_rgb_attrs[] = { |
| &dev_attr_rgb_enabled.attr, |
| &dev_attr_rgb_enabled_index.attr, |
| &dev_attr_rgb_effect.attr, |
| &dev_attr_rgb_effect_index.attr, |
| &dev_attr_rgb_mode.attr, |
| &dev_attr_rgb_mode_index.attr, |
| &dev_attr_rgb_profile.attr, |
| &dev_attr_rgb_profile_range.attr, |
| &dev_attr_rgb_speed.attr, |
| &dev_attr_rgb_speed_range.attr, |
| NULL, |
| }; |
| |
| static struct attribute_group rgb_attr_group = { |
| .attrs = gos_rgb_attrs, |
| }; |
| |
| static struct mc_subled gos_rgb_subled_info[] = { |
| { |
| .color_index = LED_COLOR_ID_RED, |
| .brightness = 0x50, |
| .intensity = 0x24, |
| .channel = 0x1, |
| }, |
| { |
| .color_index = LED_COLOR_ID_GREEN, |
| .brightness = 0x50, |
| .intensity = 0x22, |
| .channel = 0x2, |
| }, |
| { |
| .color_index = LED_COLOR_ID_BLUE, |
| .brightness = 0x50, |
| .intensity = 0x99, |
| .channel = 0x3, |
| }, |
| }; |
| |
| static struct led_classdev_mc gos_cdev_rgb = { |
| .led_cdev = { |
| .name = "go_s:rgb:joystick_rings", |
| .brightness = 0x50, |
| .max_brightness = 0x64, |
| .brightness_set = hid_gos_brightness_set, |
| }, |
| .num_colors = ARRAY_SIZE(gos_rgb_subled_info), |
| .subled_info = gos_rgb_subled_info, |
| }; |
| |
| static void cfg_setup(struct work_struct *work) |
| { |
| int ret; |
| |
| /* MCU */ |
| ret = mcu_property_out(drvdata.hdev, GET_MCU_ID, FEATURE_NONE, NULL, 0); |
| if (ret) { |
| dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU ID: %i\n", |
| ret); |
| return; |
| } |
| |
| ret = mcu_property_out(drvdata.hdev, GET_VERSION, FEATURE_NONE, NULL, 0); |
| if (ret) { |
| dev_err(&drvdata.hdev->dev, "Failed to retrieve MCU Version: %i\n", ret); |
| return; |
| } |
| |
| ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, TEST_TP_MFR, NULL, 0); |
| if (ret) { |
| dev_err(&drvdata.hdev->dev, |
| "Failed to retrieve Touchpad Manufacturer: %i\n", ret); |
| return; |
| } |
| |
| ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, TEST_TP_VER, NULL, 0); |
| if (ret) { |
| dev_err(&drvdata.hdev->dev, |
| "Failed to retrieve Touchpad Firmware Version: %i\n", ret); |
| return; |
| } |
| |
| ret = mcu_property_out(drvdata.hdev, GET_PL_TEST, TEST_IMU_MFR, NULL, 0); |
| if (ret) { |
| dev_err(&drvdata.hdev->dev, |
| "Failed to retrieve IMU Manufacturer: %i\n", ret); |
| return; |
| } |
| } |
| |
| static int hid_gos_cfg_probe(struct hid_device *hdev, |
| const struct hid_device_id *_id) |
| { |
| int ret; |
| |
| hid_set_drvdata(hdev, &drvdata); |
| drvdata.hdev = hdev; |
| mutex_init(&drvdata.cfg_mutex); |
| |
| ret = sysfs_create_groups(&hdev->dev.kobj, top_level_attr_groups); |
| if (ret) { |
| dev_err_probe(&hdev->dev, ret, |
| "Failed to create gamepad configuration attributes\n"); |
| return ret; |
| } |
| |
| ret = devm_led_classdev_multicolor_register(&hdev->dev, &gos_cdev_rgb); |
| if (ret) { |
| dev_err_probe(&hdev->dev, ret, "Failed to create RGB device\n"); |
| return ret; |
| } |
| |
| ret = devm_device_add_group(gos_cdev_rgb.led_cdev.dev, &rgb_attr_group); |
| if (ret) { |
| dev_err_probe(&hdev->dev, ret, |
| "Failed to create RGB configuration attributes\n"); |
| return ret; |
| } |
| |
| drvdata.led_cdev = &gos_cdev_rgb.led_cdev; |
| |
| init_completion(&drvdata.send_cmd_complete); |
| |
| /* Executing calls prior to returning from probe will lock the MCU. Schedule |
| * initial data call after probe has completed and MCU can accept calls. |
| */ |
| INIT_DELAYED_WORK(&drvdata.gos_cfg_setup, &cfg_setup); |
| ret = schedule_delayed_work(&drvdata.gos_cfg_setup, msecs_to_jiffies(2)); |
| if (!ret) { |
| dev_err(&hdev->dev, "Failed to schedule startup delayed work\n"); |
| return -ENODEV; |
| } |
| |
| return 0; |
| } |
| |
| static void hid_gos_cfg_remove(struct hid_device *hdev) |
| { |
| guard(mutex)(&drvdata.cfg_mutex); |
| cancel_delayed_work_sync(&drvdata.gos_cfg_setup); |
| sysfs_remove_groups(&hdev->dev.kobj, top_level_attr_groups); |
| hid_hw_close(hdev); |
| hid_hw_stop(hdev); |
| hid_set_drvdata(hdev, NULL); |
| } |
| |
| static int hid_gos_probe(struct hid_device *hdev, |
| const struct hid_device_id *id) |
| { |
| int ret, ep; |
| |
| ret = hid_parse(hdev); |
| if (ret) { |
| hid_err(hdev, "Parse failed\n"); |
| return ret; |
| } |
| |
| ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW); |
| if (ret) { |
| hid_err(hdev, "Failed to start HID device\n"); |
| return ret; |
| } |
| |
| ret = hid_hw_open(hdev); |
| if (ret) { |
| hid_err(hdev, "Failed to open HID device\n"); |
| hid_hw_stop(hdev); |
| return ret; |
| } |
| |
| ep = get_endpoint_address(hdev); |
| if (ep != GO_S_CFG_INTF_IN) { |
| dev_dbg(&hdev->dev, "Started interface %x as generic HID device.\n", ep); |
| return 0; |
| } |
| |
| ret = hid_gos_cfg_probe(hdev, id); |
| if (ret) |
| dev_err_probe(&hdev->dev, ret, "Failed to start configuration interface"); |
| |
| dev_dbg(&hdev->dev, "Started interface %x as Go S configuration interface\n", ep); |
| return ret; |
| } |
| |
| static void hid_gos_remove(struct hid_device *hdev) |
| { |
| int ep = get_endpoint_address(hdev); |
| |
| switch (ep) { |
| case GO_S_CFG_INTF_IN: |
| hid_gos_cfg_remove(hdev); |
| break; |
| default: |
| hid_hw_close(hdev); |
| hid_hw_stop(hdev); |
| |
| break; |
| } |
| } |
| |
| static const struct hid_device_id hid_gos_devices[] = { |
| { HID_USB_DEVICE(USB_VENDOR_ID_QHE, |
| USB_DEVICE_ID_LENOVO_LEGION_GO_S_XINPUT) }, |
| { HID_USB_DEVICE(USB_VENDOR_ID_QHE, |
| USB_DEVICE_ID_LENOVO_LEGION_GO_S_DINPUT) }, |
| {} |
| }; |
| |
| MODULE_DEVICE_TABLE(hid, hid_gos_devices); |
| static struct hid_driver hid_lenovo_go_s = { |
| .name = "hid-lenovo-go-s", |
| .id_table = hid_gos_devices, |
| .probe = hid_gos_probe, |
| .remove = hid_gos_remove, |
| .raw_event = hid_gos_raw_event, |
| }; |
| module_hid_driver(hid_lenovo_go_s); |
| |
| MODULE_AUTHOR("Derek J. Clark"); |
| MODULE_DESCRIPTION("HID Driver for Lenovo Legion Go S Series gamepad."); |
| MODULE_LICENSE("GPL"); |