linux/drivers/hid/hid-lenovo-go-s.c
Colin Ian King dd800099c2 HID: hid-lenovo-go-s: Fix spelling mistake "configuratiion" -> "configuration"
There is a spelling mistake in a dev_err_probe message. Fix it.

Signed-off-by: Colin Ian King <colin.i.king@gmail.com>
Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
Reviewed-by: Mark Pearson <mpearson-lenovo@squebb.ca>
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
Signed-off-by: Jiri Kosina <jkosina@suse.com>
2026-03-10 17:53:18 +01:00

1505 lines
35 KiB
C

// 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");