| Author | Tokens | Token Proportion | Commits | Commit Proportion |
|---|---|---|---|---|
| Derek J. Clark | 6461 | 97.67% | 6 | 66.67% |
| Matthew Schwartz | 141 | 2.13% | 1 | 11.11% |
| Louis Clinckx | 12 | 0.18% | 1 | 11.11% |
| Colin Ian King | 1 | 0.02% | 1 | 11.11% |
| Total | 6615 | 9 |
// 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; 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; } ret = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, FEATURE_OS_MODE, NULL, 0); if (ret) { dev_err(&drvdata.hdev->dev, "Failed to retrieve OS Mode: %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_cfg_reset_resume(struct hid_device *hdev) { u8 os_mode = drvdata.os_mode; int ret; ret = mcu_property_out(drvdata.hdev, SET_GAMEPAD_CFG, FEATURE_OS_MODE, &os_mode, 1); if (ret < 0) return ret; ret = mcu_property_out(drvdata.hdev, GET_GAMEPAD_CFG, FEATURE_OS_MODE, NULL, 0); if (ret < 0) return ret; if (drvdata.os_mode != os_mode) return -ENODEV; return 0; } static int hid_gos_probe(struct hid_device *hdev, const struct hid_device_id *id) { int ret, ep; if (!hid_is_usb(hdev)) return -EINVAL; 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 int hid_gos_reset_resume(struct hid_device *hdev) { int ep = get_endpoint_address(hdev); switch (ep) { case GO_S_CFG_INTF_IN: return hid_gos_cfg_reset_resume(hdev); default: break; } return 0; } 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, .reset_resume = hid_gos_reset_resume, }; 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");
Information contained on this website is for historical information purposes only and does not indicate or represent copyright ownership.
Created with Cregit http://github.com/cregit/cregit
Version 2.0-RC1