Contributors: 2
Author Tokens Token Proportion Commits Commit Proportion
Derek J. Clark 10652 99.89% 7 87.50%
Louis Clinckx 12 0.11% 1 12.50%
Total 10664 8


// SPDX-License-Identifier: GPL-2.0-or-later
/*
 *  HID driver for Lenovo Legion Go series gamepads.
 *
 *  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/device/devres.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/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_GP_INTF_IN		0x83
#define GO_OUTPUT_REPORT_ID	0x05
#define GO_GP_RESET_SUCCESS	0x01
#define GO_PACKET_SIZE		64

static struct hid_go_cfg {
	struct delayed_work go_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 fps_mode;
	u8 gp_left_auto_sleep_time;
	u8 gp_left_gyro_cal_status;
	u8 gp_left_joy_cal_status;
	u8 gp_left_notify_en;
	u8 gp_left_rumble_mode;
	u8 gp_left_trigg_cal_status;
	u32 gp_left_version_firmware;
	u8 gp_left_version_gen;
	u32 gp_left_version_hardware;
	u32 gp_left_version_product;
	u32 gp_left_version_protocol;
	u8 gp_mode;
	u8 gp_right_auto_sleep_time;
	u8 gp_right_gyro_cal_status;
	u8 gp_right_joy_cal_status;
	u8 gp_right_notify_en;
	u8 gp_right_rumble_mode;
	u8 gp_right_trigg_cal_status;
	u32 gp_right_version_firmware;
	u8 gp_right_version_gen;
	u32 gp_right_version_hardware;
	u32 gp_right_version_product;
	u32 gp_right_version_protocol;
	u8 gp_rumble_intensity;
	u8 imu_left_bypass_en;
	u8 imu_left_sensor_en;
	u8 imu_right_bypass_en;
	u8 imu_right_sensor_en;
	u32 mcu_version_firmware;
	u8 mcu_version_gen;
	u32 mcu_version_hardware;
	u32 mcu_version_product;
	u32 mcu_version_protocol;
	u32 mouse_dpi;
	u8 os_mode;
	u8 rgb_effect;
	u8 rgb_en;
	u8 rgb_mode;
	u8 rgb_profile;
	u8 rgb_speed;
	u8 tp_en;
	u8 tp_vibration_en;
	u8 tp_vibration_intensity;
	u32 tx_dongle_version_firmware;
	u8 tx_dongle_version_gen;
	u32 tx_dongle_version_hardware;
	u32 tx_dongle_version_product;
	u32 tx_dongle_version_protocol;
} drvdata;

struct go_cfg_attr {
	u8 index;
};

struct command_report {
	u8 report_id;
	u8 id;
	u8 cmd;
	u8 sub_cmd;
	u8 device_type;
	u8 data[59];
} __packed;

enum command_id {
	MCU_CONFIG_DATA = 0x00,
	OS_MODE_DATA = 0x06,
	GAMEPAD_DATA = 0x3c,
};

enum mcu_command_index {
	GET_VERSION_DATA = 0x02,
	GET_FEATURE_STATUS,
	SET_FEATURE_STATUS,
	GET_MOTOR_CFG,
	SET_MOTOR_CFG,
	GET_DPI_CFG,
	SET_DPI_CFG,
	SET_TRIGGER_CFG = 0x0a,
	SET_JOYSTICK_CFG = 0x0c,
	SET_GYRO_CFG = 0x0e,
	GET_RGB_CFG,
	SET_RGB_CFG,
	GET_DEVICE_STATUS = 0xa0,

};

enum dev_type {
	UNSPECIFIED,
	USB_MCU,
	TX_DONGLE,
	LEFT_CONTROLLER,
	RIGHT_CONTROLLER,
};

enum enabled_status_index {
	FEATURE_UNKNOWN,
	FEATURE_ENABLED,
	FEATURE_DISABLED,
};

static const char *const enabled_status_text[] = {
	[FEATURE_UNKNOWN] = "unknown",
	[FEATURE_ENABLED] = "true",
	[FEATURE_DISABLED] = "false",
};

enum version_data_index {
	PRODUCT_VERSION = 0x02,
	PROTOCOL_VERSION,
	FIRMWARE_VERSION,
	HARDWARE_VERSION,
	HARDWARE_GENERATION,
};

enum feature_status_index {
	FEATURE_RESET_GAMEPAD = 0x02,
	FEATURE_IMU_BYPASS,
	FEATURE_IMU_ENABLE = 0x05,
	FEATURE_TOUCHPAD_ENABLE = 0x07,
	FEATURE_LIGHT_ENABLE,
	FEATURE_AUTO_SLEEP_TIME,
	FEATURE_FPS_SWITCH_STATUS = 0x0b,
	FEATURE_GAMEPAD_MODE = 0x0e,
};

#define FEATURE_OS_MODE 0x69

enum fps_switch_status_index {
	FPS_STATUS_UNKNOWN,
	GAMEPAD,
	FPS,
};

static const char *const fps_switch_text[] = {
	[FPS_STATUS_UNKNOWN] = "unknown",
	[GAMEPAD] = "gamepad",
	[FPS] = "fps",
};

enum gamepad_mode_index {
	GAMEPAD_MODE_UNKNOWN,
	XINPUT,
	DINPUT,
};

static const char *const gamepad_mode_text[] = {
	[GAMEPAD_MODE_UNKNOWN] = "unknown",
	[XINPUT] = "xinput",
	[DINPUT] = "dinput",
};

enum motor_cfg_index {
	MOTOR_CFG_ALL = 0x01,
	MOTOR_INTENSITY,
	VIBRATION_NOTIFY_ENABLE,
	RUMBLE_MODE,
	TP_VIBRATION_ENABLE,
	TP_VIBRATION_INTENSITY,
};

enum intensity_index {
	INTENSITY_UNKNOWN,
	INTENSITY_OFF,
	INTENSITY_LOW,
	INTENSITY_MEDIUM,
	INTENSITY_HIGH,
};

static const char *const intensity_text[] = {
	[INTENSITY_UNKNOWN] = "unknown",
	[INTENSITY_OFF] = "off",
	[INTENSITY_LOW] = "low",
	[INTENSITY_MEDIUM] = "medium",
	[INTENSITY_HIGH] = "high",
};

enum rumble_mode_index {
	RUMBLE_MODE_UNKNOWN,
	RUMBLE_MODE_FPS,
	RUMBLE_MODE_RACE,
	RUMBLE_MODE_AVERAGE,
	RUMBLE_MODE_SPG,
	RUMBLE_MODE_RPG,
};

static const char *const rumble_mode_text[] = {
	[RUMBLE_MODE_UNKNOWN] = "unknown",
	[RUMBLE_MODE_FPS] = "fps",
	[RUMBLE_MODE_RACE] = "racing",
	[RUMBLE_MODE_AVERAGE] = "standard",
	[RUMBLE_MODE_SPG] = "spg",
	[RUMBLE_MODE_RPG] = "rpg",
};

#define FPS_MODE_DPI		0x02
#define TRIGGER_CALIBRATE	0x04
#define JOYSTICK_CALIBRATE	0x04
#define GYRO_CALIBRATE		0x06

enum cal_device_type {
	CALDEV_GYROSCOPE = 0x01,
	CALDEV_JOYSTICK,
	CALDEV_TRIGGER,
	CALDEV_JOY_TRIGGER,
};

enum cal_enable {
	CAL_UNKNOWN,
	CAL_START,
	CAL_STOP,
};

static const char *const cal_enabled_text[] = {
	[CAL_UNKNOWN] = "unknown",
	[CAL_START] = "start",
	[CAL_STOP] = "stop",
};

enum cal_status_index {
	CAL_STAT_UNKNOWN,
	CAL_STAT_SUCCESS,
	CAL_STAT_FAILURE,
};

static const char *const cal_status_text[] = {
	[CAL_STAT_UNKNOWN] = "unknown",
	[CAL_STAT_SUCCESS] = "success",
	[CAL_STAT_FAILURE] = "failure",
};

enum rgb_config_index {
	LIGHT_CFG_ALL = 0x01,
	LIGHT_MODE_SEL,
	LIGHT_PROFILE_SEL,
	USR_LIGHT_PROFILE_1,
	USR_LIGHT_PROFILE_2,
	USR_LIGHT_PROFILE_3,
};

enum rgb_mode_index {
	RGB_MODE_UNKNOWN,
	RGB_MODE_DYNAMIC,
	RGB_MODE_CUSTOM,
};

static const char *const rgb_mode_text[] = {
	[RGB_MODE_UNKNOWN] = "unknown",
	[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 device_status_index {
	GET_CAL_STATUS = 0x02,
	GET_UPGRADE_STATUS,
	GET_MACRO_REC_STATUS,
	GET_HOTKEY_TRIGG_STATUS,
};

enum os_mode_cfg_index {
	SET_OS_MODE = 0x09,
	GET_OS_MODE,
};

enum os_mode_type_index {
	OS_UNKNOWN,
	WINDOWS,
	LINUX,
};

static const char *const os_mode_text[] = {
	[OS_UNKNOWN] = "unknown",
	[WINDOWS] = "windows",
	[LINUX] = "linux",
};

static int hid_go_version_event(struct command_report *cmd_rep)
{
	switch (cmd_rep->sub_cmd) {
	case PRODUCT_VERSION:
		switch (cmd_rep->device_type) {
		case USB_MCU:
			drvdata.mcu_version_product =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case TX_DONGLE:
			drvdata.tx_dongle_version_product =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case LEFT_CONTROLLER:
			drvdata.gp_left_version_product =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case RIGHT_CONTROLLER:
			drvdata.gp_right_version_product =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		default:
			return -EINVAL;
		}
	case PROTOCOL_VERSION:
		switch (cmd_rep->device_type) {
		case USB_MCU:
			drvdata.mcu_version_protocol =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case TX_DONGLE:
			drvdata.tx_dongle_version_protocol =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case LEFT_CONTROLLER:
			drvdata.gp_left_version_protocol =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case RIGHT_CONTROLLER:
			drvdata.gp_right_version_protocol =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		default:
			return -EINVAL;
		}
	case FIRMWARE_VERSION:
		switch (cmd_rep->device_type) {
		case USB_MCU:
			drvdata.mcu_version_firmware =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case TX_DONGLE:
			drvdata.tx_dongle_version_firmware =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case LEFT_CONTROLLER:
			drvdata.gp_left_version_firmware =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case RIGHT_CONTROLLER:
			drvdata.gp_right_version_firmware =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		default:
			return -EINVAL;
		}
	case HARDWARE_VERSION:
		switch (cmd_rep->device_type) {
		case USB_MCU:
			drvdata.mcu_version_hardware =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case TX_DONGLE:
			drvdata.tx_dongle_version_hardware =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case LEFT_CONTROLLER:
			drvdata.gp_left_version_hardware =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		case RIGHT_CONTROLLER:
			drvdata.gp_right_version_hardware =
				get_unaligned_be32(cmd_rep->data);
			return 0;
		default:
			return -EINVAL;
		}
	case HARDWARE_GENERATION:
		switch (cmd_rep->device_type) {
		case USB_MCU:
			drvdata.mcu_version_gen = cmd_rep->data[0];
			return 0;
		case TX_DONGLE:
			drvdata.tx_dongle_version_gen = cmd_rep->data[0];
			return 0;
		case LEFT_CONTROLLER:
			drvdata.gp_left_version_gen = cmd_rep->data[0];
			return 0;
		case RIGHT_CONTROLLER:
			drvdata.gp_right_version_gen = cmd_rep->data[0];
			return 0;
		default:
			return -EINVAL;
		}
	default:
		return -EINVAL;
	}
}

static int hid_go_feature_status_event(struct command_report *cmd_rep)
{
	switch (cmd_rep->sub_cmd) {
	case FEATURE_RESET_GAMEPAD:
		return 0;
	case FEATURE_IMU_ENABLE:
		switch (cmd_rep->device_type) {
		case LEFT_CONTROLLER:
			drvdata.imu_left_sensor_en = cmd_rep->data[0];
			return 0;
		case RIGHT_CONTROLLER:
			drvdata.imu_right_sensor_en = cmd_rep->data[0];
			return 0;
		default:
			return -EINVAL;
		}
	case FEATURE_IMU_BYPASS:
		switch (cmd_rep->device_type) {
		case LEFT_CONTROLLER:
			drvdata.imu_left_bypass_en = cmd_rep->data[0];
			return 0;
		case RIGHT_CONTROLLER:
			drvdata.imu_right_bypass_en = cmd_rep->data[0];
			return 0;
		default:
			return -EINVAL;
		}
		break;
	case FEATURE_LIGHT_ENABLE:
		drvdata.rgb_en = cmd_rep->data[0];
		return 0;
	case FEATURE_AUTO_SLEEP_TIME:
		switch (cmd_rep->device_type) {
		case LEFT_CONTROLLER:
			drvdata.gp_left_auto_sleep_time = cmd_rep->data[0];
			return 0;
		case RIGHT_CONTROLLER:
			drvdata.gp_right_auto_sleep_time = cmd_rep->data[0];
			return 0;
		default:
			return -EINVAL;
		}
		break;
	case FEATURE_TOUCHPAD_ENABLE:
		drvdata.tp_en = cmd_rep->data[0];
		return 0;
	case FEATURE_GAMEPAD_MODE:
		drvdata.gp_mode = cmd_rep->data[0];
		return 0;
	case FEATURE_FPS_SWITCH_STATUS:
		drvdata.fps_mode = cmd_rep->data[0];
		return 0;
	default:
		return -EINVAL;
	}
}

static int hid_go_motor_event(struct command_report *cmd_rep)
{
	switch (cmd_rep->sub_cmd) {
	case MOTOR_CFG_ALL:
		return -EINVAL;
	case MOTOR_INTENSITY:
		drvdata.gp_rumble_intensity = cmd_rep->data[0];
		return 0;
	case VIBRATION_NOTIFY_ENABLE:
		switch (cmd_rep->device_type) {
		case LEFT_CONTROLLER:
			drvdata.gp_left_notify_en = cmd_rep->data[0];
			return 0;
		case RIGHT_CONTROLLER:
			drvdata.gp_right_notify_en = cmd_rep->data[0];
			return 0;
		default:
			return -EINVAL;
		}
		break;
	case RUMBLE_MODE:
		switch (cmd_rep->device_type) {
		case LEFT_CONTROLLER:
			drvdata.gp_left_rumble_mode = cmd_rep->data[0];
			return 0;
		case RIGHT_CONTROLLER:
			drvdata.gp_right_rumble_mode = cmd_rep->data[0];
			return 0;
		default:
			return -EINVAL;
		}
	case TP_VIBRATION_ENABLE:
		drvdata.tp_vibration_en = cmd_rep->data[0];
		return 0;
	case TP_VIBRATION_INTENSITY:
		drvdata.tp_vibration_intensity = cmd_rep->data[0];
		return 0;
	}
	return -EINVAL;
}

static int hid_go_fps_dpi_event(struct command_report *cmd_rep)
{
	if (cmd_rep->sub_cmd != FPS_MODE_DPI)
		return -EINVAL;

	drvdata.mouse_dpi = get_unaligned_le32(cmd_rep->data);

	return 0;
}

static int hid_go_light_event(struct command_report *cmd_rep)
{
	struct led_classdev_mc *mc_cdev;

	switch (cmd_rep->sub_cmd) {
	case LIGHT_MODE_SEL:
		drvdata.rgb_mode = cmd_rep->data[0];
		return 0;
	case LIGHT_PROFILE_SEL:
		drvdata.rgb_profile = cmd_rep->data[0];
		return 0;
	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 = 100 - cmd_rep->data[5];
		return 0;
	default:
		return -EINVAL;
	}
}

static int hid_go_device_status_event(struct command_report *cmd_rep)
{
	switch (cmd_rep->device_type) {
	case LEFT_CONTROLLER:
		switch (cmd_rep->data[0]) {
		case CALDEV_GYROSCOPE:
			drvdata.gp_left_gyro_cal_status = cmd_rep->data[1];
			return 0;
		case CALDEV_JOYSTICK:
			drvdata.gp_left_joy_cal_status = cmd_rep->data[1];
			return 0;
		case CALDEV_TRIGGER:
			drvdata.gp_left_trigg_cal_status = cmd_rep->data[1];
			return 0;
		default:
			return -EINVAL;
		}
		break;
	case RIGHT_CONTROLLER:
		switch (cmd_rep->data[0]) {
		case CALDEV_GYROSCOPE:
			drvdata.gp_right_gyro_cal_status = cmd_rep->data[1];
			return 0;
		case CALDEV_JOYSTICK:
			drvdata.gp_right_joy_cal_status = cmd_rep->data[1];
			return 0;
		case CALDEV_TRIGGER:
			drvdata.gp_right_trigg_cal_status = cmd_rep->data[1];
			return 0;
		default:
			return -EINVAL;
		}
		break;
	default:
		return -EINVAL;
	}
}

static int hid_go_os_mode_cfg_event(struct command_report *cmd_rep)
{
	switch (cmd_rep->sub_cmd) {
	case SET_OS_MODE:
		if (cmd_rep->data[0] != 1)
			return -EIO;
		return 0;
	case GET_OS_MODE:
		drvdata.os_mode = cmd_rep->data[0];
		return 0;
	default:
		return -EINVAL;
	}
}

static int hid_go_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 -ENODEV;

	return ep->desc.bEndpointAddress;
}

static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
			    u8 *data, int size)
{
	struct command_report *cmd_rep;
	int ep, ret;

	if (size != GO_PACKET_SIZE)
		goto passthrough;

	ep = get_endpoint_address(hdev);
	if (ep != GO_GP_INTF_IN)
		goto passthrough;

	cmd_rep = (struct command_report *)data;

	switch (cmd_rep->id) {
	case MCU_CONFIG_DATA:
		switch (cmd_rep->cmd) {
		case GET_VERSION_DATA:
			ret = hid_go_version_event(cmd_rep);
			break;
		case GET_FEATURE_STATUS:
			ret = hid_go_feature_status_event(cmd_rep);
			break;
		case GET_MOTOR_CFG:
			ret = hid_go_motor_event(cmd_rep);
			break;
		case GET_DPI_CFG:
			ret = hid_go_fps_dpi_event(cmd_rep);
			break;
		case GET_RGB_CFG:
			ret = hid_go_light_event(cmd_rep);
			break;
		case GET_DEVICE_STATUS:
			ret = hid_go_device_status_event(cmd_rep);
			break;
		case SET_FEATURE_STATUS:
		case SET_MOTOR_CFG:
		case SET_DPI_CFG:
		case SET_RGB_CFG:
		case SET_TRIGGER_CFG:
		case SET_JOYSTICK_CFG:
		case SET_GYRO_CFG:
			ret = hid_go_set_event_return(cmd_rep);
			break;
		default:
			ret = -EINVAL;
			break;
		}
		break;
	case OS_MODE_DATA:
		ret = hid_go_os_mode_cfg_event(cmd_rep);
		break;
	default:
		goto passthrough;
	}
	dev_dbg(&hdev->dev, "Rx data as raw input report: [%*ph]\n",
		GO_PACKET_SIZE, data);

	complete(&drvdata.send_cmd_complete);
	return ret;

passthrough:
	/* Forward other HID reports so they generate events */
	hid_input_report(hdev, HID_INPUT_REPORT, data, size, 1);
	return 0;
}

static int mcu_property_out(struct hid_device *hdev, u8 id, u8 command,
			    u8 index, enum dev_type device, u8 *data, size_t len)
{
	unsigned char *dmabuf __free(kfree) = NULL;
	u8 header[] = { GO_OUTPUT_REPORT_ID, id, command, index, device };
	size_t header_size = ARRAY_SIZE(header);
	int timeout = 50;
	int ret;

	if (header_size + len > GO_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_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_PACKET_SIZE, dmabuf);

	ret = hid_hw_output_report(hdev, dmabuf, GO_PACKET_SIZE);
	if (ret < 0)
		return ret;

	ret = ret == GO_PACKET_SIZE ? 0 : -EINVAL;
	if (ret)
		return ret;

	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 version_show(struct device *dev, struct device_attribute *attr,
			    char *buf, enum version_data_index index,
			    enum dev_type device_type)
{
	ssize_t count = 0;
	int ret;

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       index, device_type, NULL, 0);
	if (ret)
		return ret;

	switch (index) {
	case PRODUCT_VERSION:
		switch (device_type) {
		case USB_MCU:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.mcu_version_product);
			break;
		case TX_DONGLE:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.tx_dongle_version_product);
			break;
		case LEFT_CONTROLLER:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.gp_left_version_product);
			break;
		case RIGHT_CONTROLLER:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.gp_right_version_product);
			break;
		default:
			return -EINVAL;
		}
		break;
	case PROTOCOL_VERSION:
		switch (device_type) {
		case USB_MCU:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.mcu_version_protocol);
			break;
		case TX_DONGLE:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.tx_dongle_version_protocol);
			break;
		case LEFT_CONTROLLER:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.gp_left_version_protocol);
			break;
		case RIGHT_CONTROLLER:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.gp_right_version_protocol);
			break;
		default:
			return -EINVAL;
		}
		break;
	case FIRMWARE_VERSION:
		switch (device_type) {
		case USB_MCU:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.mcu_version_firmware);
			break;
		case TX_DONGLE:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.tx_dongle_version_firmware);
			break;
		case LEFT_CONTROLLER:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.gp_left_version_firmware);
			break;
		case RIGHT_CONTROLLER:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.gp_right_version_firmware);
			break;
		default:
			return -EINVAL;
		}
		break;
	case HARDWARE_VERSION:
		switch (device_type) {
		case USB_MCU:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.mcu_version_hardware);
			break;
		case TX_DONGLE:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.tx_dongle_version_hardware);
			break;
		case LEFT_CONTROLLER:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.gp_left_version_hardware);
			break;
		case RIGHT_CONTROLLER:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.gp_right_version_hardware);
			break;
		default:
			return -EINVAL;
		}
		break;
	case HARDWARE_GENERATION:
		switch (device_type) {
		case USB_MCU:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.mcu_version_gen);
			break;
		case TX_DONGLE:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.tx_dongle_version_gen);
			break;
		case LEFT_CONTROLLER:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.gp_left_version_gen);
			break;
		case RIGHT_CONTROLLER:
			count = sysfs_emit(buf, "%x\n",
					   drvdata.gp_right_version_gen);
			break;
		default:
			return -EINVAL;
		}
		break;
	}

	return count;
}

static ssize_t feature_status_store(struct device *dev,
				    struct device_attribute *attr,
				    const char *buf, size_t count,
				    enum feature_status_index index,
				    enum dev_type device_type)
{
	size_t size = 1;
	u8 val = 0;
	int ret;

	switch (index) {
	case FEATURE_IMU_ENABLE:
	case FEATURE_IMU_BYPASS:
	case FEATURE_LIGHT_ENABLE:
	case FEATURE_TOUCHPAD_ENABLE:
		ret = sysfs_match_string(enabled_status_text, buf);
		val = ret;
		break;
	case FEATURE_AUTO_SLEEP_TIME:
		ret = kstrtou8(buf, 10, &val);
		break;
	case FEATURE_RESET_GAMEPAD:
		ret = kstrtou8(buf, 10, &val);
		if (val != GO_GP_RESET_SUCCESS)
			return -EINVAL;
		break;
	case FEATURE_FPS_SWITCH_STATUS:
		ret = sysfs_match_string(fps_switch_text, buf);
		val = ret;
		break;
	case FEATURE_GAMEPAD_MODE:
		ret = sysfs_match_string(gamepad_mode_text, buf);
		val = ret;
		break;
	default:
		return -EINVAL;
	}

	if (ret < 0)
		return ret;

	if (!val)
		size = 0;

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA,
			       SET_FEATURE_STATUS, index, device_type, &val,
			       size);
	if (ret < 0)
		return ret;

	return count;
}

static ssize_t feature_status_show(struct device *dev,
				   struct device_attribute *attr, char *buf,
				   enum feature_status_index index,
				   enum dev_type device_type)
{
	ssize_t count = 0;
	int ret;
	u8 i;

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA,
			       GET_FEATURE_STATUS, index, device_type, NULL, 0);
	if (ret)
		return ret;

	switch (index) {
	case FEATURE_IMU_ENABLE:
		switch (device_type) {
		case LEFT_CONTROLLER:
			i = drvdata.imu_left_sensor_en;
			break;
		case RIGHT_CONTROLLER:
			i = drvdata.imu_right_sensor_en;
			break;
		default:
			return -EINVAL;
		}
		if (i >= ARRAY_SIZE(enabled_status_text))
			return -EINVAL;

		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
		break;
	case FEATURE_IMU_BYPASS:
		switch (device_type) {
		case LEFT_CONTROLLER:
			i = drvdata.imu_left_bypass_en;
			break;
		case RIGHT_CONTROLLER:
			i = drvdata.imu_right_bypass_en;
			break;
		default:
			return -EINVAL;
		}
		if (i >= ARRAY_SIZE(enabled_status_text))
			return -EINVAL;

		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
		break;
	case FEATURE_LIGHT_ENABLE:
		i = drvdata.rgb_en;
		if (i >= ARRAY_SIZE(enabled_status_text))
			return -EINVAL;

		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
		break;
	case FEATURE_TOUCHPAD_ENABLE:
		i = drvdata.tp_en;
		if (i >= ARRAY_SIZE(enabled_status_text))
			return -EINVAL;

		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
		break;
	case FEATURE_AUTO_SLEEP_TIME:
		switch (device_type) {
		case LEFT_CONTROLLER:
			i = drvdata.gp_left_auto_sleep_time;
			break;
		case RIGHT_CONTROLLER:
			i = drvdata.gp_right_auto_sleep_time;
			break;
		default:
			return -EINVAL;
		}
		count = sysfs_emit(buf, "%u\n", i);
		break;
	case FEATURE_FPS_SWITCH_STATUS:
		i = drvdata.fps_mode;
		if (i >= ARRAY_SIZE(fps_switch_text))
			return -EINVAL;

		count = sysfs_emit(buf, "%s\n", fps_switch_text[i]);
		break;
	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;
	default:
		return -EINVAL;
	}

	return count;
}

static ssize_t feature_status_options(struct device *dev,
				      struct device_attribute *attr, char *buf,
				      enum feature_status_index index)
{
	ssize_t count = 0;
	unsigned int i;

	switch (index) {
	case FEATURE_IMU_ENABLE:
	case FEATURE_IMU_BYPASS:
	case FEATURE_LIGHT_ENABLE:
	case FEATURE_TOUCHPAD_ENABLE:
		for (i = 1; i < ARRAY_SIZE(enabled_status_text); i++) {
			count += sysfs_emit_at(buf, count, "%s ",
					       enabled_status_text[i]);
		}
		break;
	case FEATURE_AUTO_SLEEP_TIME:
		return sysfs_emit(buf, "0-255\n");
	case FEATURE_FPS_SWITCH_STATUS:
		for (i = 1; i < ARRAY_SIZE(fps_switch_text); i++) {
			count += sysfs_emit_at(buf, count, "%s ",
					       fps_switch_text[i]);
		}
		break;
	case FEATURE_GAMEPAD_MODE:
		for (i = 1; i < ARRAY_SIZE(gamepad_mode_text); i++) {
			count += sysfs_emit_at(buf, count, "%s ",
					       gamepad_mode_text[i]);
		}
		break;
	default:
		return -EINVAL;
	}

	if (count)
		buf[count - 1] = '\n';

	return count;
}

static ssize_t motor_config_store(struct device *dev,
				  struct device_attribute *attr,
				  const char *buf, size_t count,
				  enum motor_cfg_index index,
				  enum dev_type device_type)
{
	size_t size = 1;
	u8 val = 0;
	int ret;

	switch (index) {
	case MOTOR_CFG_ALL:
		return -EINVAL;
	case MOTOR_INTENSITY:
		ret = sysfs_match_string(intensity_text, buf);
		val = ret;
		break;
	case VIBRATION_NOTIFY_ENABLE:
		ret = sysfs_match_string(enabled_status_text, buf);
		val = ret;
		break;
	case RUMBLE_MODE:
		ret = sysfs_match_string(rumble_mode_text, buf);
		val = ret;
		break;
	case TP_VIBRATION_ENABLE:
		ret = sysfs_match_string(enabled_status_text, buf);
		val = ret;
		break;
	case TP_VIBRATION_INTENSITY:
		ret = sysfs_match_string(intensity_text, buf);
		val = ret;
		break;
	}

	if (ret < 0)
		return ret;

	if (!val)
		size = 0;

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, SET_MOTOR_CFG,
			       index, device_type, &val, size);
	if (ret < 0)
		return ret;

	return count;
}

static ssize_t motor_config_show(struct device *dev,
				 struct device_attribute *attr, char *buf,
				 enum motor_cfg_index index,
				 enum dev_type device_type)
{
	ssize_t count = 0;
	int ret;
	u8 i;

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_MOTOR_CFG,
			       index, device_type, NULL, 0);
	if (ret)
		return ret;

	switch (index) {
	case MOTOR_CFG_ALL:
		return -EINVAL;
	case MOTOR_INTENSITY:
		i = drvdata.gp_rumble_intensity;
		if (i >= ARRAY_SIZE(intensity_text))
			return -EINVAL;

		count = sysfs_emit(buf, "%s\n", intensity_text[i]);
		break;
	case VIBRATION_NOTIFY_ENABLE:
		switch (device_type) {
		case LEFT_CONTROLLER:
			i = drvdata.gp_left_notify_en;
			break;
		case RIGHT_CONTROLLER:
			i = drvdata.gp_right_notify_en;
			break;
		default:
			return -EINVAL;
		}
		if (i >= ARRAY_SIZE(enabled_status_text))
			return -EINVAL;

		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
		break;
	case RUMBLE_MODE:
		switch (device_type) {
		case LEFT_CONTROLLER:
			i = drvdata.gp_left_rumble_mode;
			break;
		case RIGHT_CONTROLLER:
			i = drvdata.gp_right_rumble_mode;
			break;
		default:
			return -EINVAL;
		}
		if (i >= ARRAY_SIZE(rumble_mode_text))
			return -EINVAL;

		count = sysfs_emit(buf, "%s\n", rumble_mode_text[i]);
		break;
	case TP_VIBRATION_ENABLE:
		i = drvdata.tp_vibration_en;
		if (i >= ARRAY_SIZE(enabled_status_text))
			return -EINVAL;

		count = sysfs_emit(buf, "%s\n", enabled_status_text[i]);
		break;
	case TP_VIBRATION_INTENSITY:
		i = drvdata.tp_vibration_intensity;
		if (i >= ARRAY_SIZE(intensity_text))
			return -EINVAL;

		count = sysfs_emit(buf, "%s\n", intensity_text[i]);
		break;
	}

	return count;
}

static ssize_t motor_config_options(struct device *dev,
				    struct device_attribute *attr, char *buf,
				    enum motor_cfg_index index)
{
	ssize_t count = 0;
	unsigned int i;

	switch (index) {
	case MOTOR_CFG_ALL:
		break;
	case RUMBLE_MODE:
		for (i = 1; i < ARRAY_SIZE(rumble_mode_text); i++) {
			count += sysfs_emit_at(buf, count, "%s ",
					       rumble_mode_text[i]);
		}
		break;
	case MOTOR_INTENSITY:
	case TP_VIBRATION_INTENSITY:
		for (i = 1; i < ARRAY_SIZE(intensity_text); i++) {
			count += sysfs_emit_at(buf, count, "%s ",
					       intensity_text[i]);
		}
		break;
	case VIBRATION_NOTIFY_ENABLE:
	case TP_VIBRATION_ENABLE:
		for (i = 1; i < ARRAY_SIZE(enabled_status_text); i++) {
			count += sysfs_emit_at(buf, count, "%s ",
					       enabled_status_text[i]);
		}
		break;
	}

	if (count)
		buf[count - 1] = '\n';

	return count;
}

static ssize_t fps_mode_dpi_store(struct device *dev,
				  struct device_attribute *attr,
				  const char *buf, size_t count)

{
	size_t size = 4;
	u32 value;
	u8 val[4];
	int ret;

	ret = kstrtou32(buf, 10, &value);
	if (ret)
		return ret;

	if (value != 500 && value != 800 && value != 1200 && value != 1800)
		return -EINVAL;

	put_unaligned_le32(value, val);

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, SET_DPI_CFG,
			       FPS_MODE_DPI, UNSPECIFIED, val, size);
	if (ret < 0)
		return ret;

	return count;
}

static ssize_t fps_mode_dpi_show(struct device *dev,
				 struct device_attribute *attr, char *buf)
{
	int ret;

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_DPI_CFG,
			       FPS_MODE_DPI, UNSPECIFIED, NULL, 0);
	if (ret < 0)
		return ret;

	return sysfs_emit(buf, "%u\n", drvdata.mouse_dpi);
}

static ssize_t fps_mode_dpi_index_show(struct device *dev,
				       struct device_attribute *attr, char *buf)
{
	return sysfs_emit(buf, "500 800 1200 1800\n");
}

static ssize_t device_status_show(struct device *dev,
				  struct device_attribute *attr, char *buf,
				  enum device_status_index index,
				  enum dev_type device_type,
				  enum cal_device_type cal_type)
{
	u8 i;

	switch (index) {
	case GET_CAL_STATUS:
		switch (device_type) {
		case LEFT_CONTROLLER:
			switch (cal_type) {
			case CALDEV_GYROSCOPE:
				i = drvdata.gp_left_gyro_cal_status;
				break;
			case CALDEV_JOYSTICK:
				i = drvdata.gp_left_joy_cal_status;
				break;
			case CALDEV_TRIGGER:
				i = drvdata.gp_left_trigg_cal_status;
				break;
			default:
				return -EINVAL;
			}
			break;
		case RIGHT_CONTROLLER:
			switch (cal_type) {
			case CALDEV_GYROSCOPE:
				i = drvdata.gp_right_gyro_cal_status;
				break;
			case CALDEV_JOYSTICK:
				i = drvdata.gp_right_joy_cal_status;
				break;
			case CALDEV_TRIGGER:
				i = drvdata.gp_right_trigg_cal_status;
				break;
			default:
				return -EINVAL;
			}
			break;
		default:
			return -EINVAL;
		}
		break;
	default:
		return -EINVAL;
	}

	if (i >= ARRAY_SIZE(cal_status_text))
		return -EINVAL;

	return sysfs_emit(buf, "%s\n", cal_status_text[i]);
}

static ssize_t calibrate_config_store(struct device *dev,
				      struct device_attribute *attr,
				      const char *buf, u8 cmd, u8 sub_cmd,
				      size_t count, enum dev_type device_type)
{
	size_t size = 1;
	u8 val = 0;
	int ret;

	ret = sysfs_match_string(cal_enabled_text, buf);
	if (ret < 0)
		return ret;

	val = ret;
	if (!val)
		size = 0;

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, cmd, sub_cmd,
			       device_type, &val, size);
	if (ret < 0)
		return ret;

	return count;
}

static ssize_t calibrate_config_options(struct device *dev,
					struct device_attribute *attr,
					char *buf)
{
	ssize_t count = 0;
	unsigned int i;

	for (i = 1; i < ARRAY_SIZE(cal_enabled_text); i++)
		count += sysfs_emit_at(buf, count, "%s ", cal_enabled_text[i]);

	buf[count - 1] = '\n';

	return count;
}

static ssize_t os_mode_store(struct device *dev, struct device_attribute *attr,
			     const char *buf, size_t count)
{
	size_t size = 1;
	int ret;
	u8 val;

	ret = sysfs_match_string(os_mode_text, buf);
	if (ret <= 0)
		return ret;

	val = ret;
	ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE,
			       SET_OS_MODE, USB_MCU, &val, size);
	if (ret < 0)
		return ret;

	drvdata.os_mode = val;

	return count;
}

static ssize_t os_mode_show(struct device *dev, struct device_attribute *attr,
			    char *buf)
{
	ssize_t count = 0;
	int ret;
	u8 i;

	ret = mcu_property_out(drvdata.hdev, OS_MODE_DATA, FEATURE_OS_MODE,
			       GET_OS_MODE, USB_MCU, NULL, 0);
	if (ret)
		return ret;

	i = drvdata.os_mode;
	if (i >= ARRAY_SIZE(os_mode_text))
		return -EINVAL;

	count = sysfs_emit(buf, "%s\n", os_mode_text[i]);

	return count;
}

static ssize_t os_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(os_mode_text); i++)
		count += sysfs_emit_at(buf, count, "%s ", os_mode_text[i]);

	if (count)
		buf[count - 1] = '\n';

	return count;
}

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_CFG_ALL || index > USR_LIGHT_PROFILE_3)
		return -EINVAL;

	return mcu_property_out(hdev, MCU_CONFIG_DATA, cmd, index, UNSPECIFIED,
				val, size);
}

static int rgb_attr_show(void)
{
	enum rgb_config_index index;

	index = drvdata.rgb_profile + 3;

	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 + 3;
	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;

	/* This is a delay setting, invert logic for consistency with other drivers */
	val = 100 - val;

	index = drvdata.rgb_profile + 3;
	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, val;

	ret = rgb_attr_show();
	if (ret)
		return ret;

	if (drvdata.rgb_speed > 100)
		return -EINVAL;

	val = drvdata.rgb_speed;

	return sysfs_emit(buf, "%hhu\n", val);
}

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_go_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 + 3;
	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 LEGO_DEVICE_ATTR_RW(_name, _attrname, _dtype, _rtype, _group)         \
	static ssize_t _name##_store(struct device *dev,                      \
				     struct device_attribute *attr,           \
				     const char *buf, size_t count)           \
	{                                                                     \
		return _group##_store(dev, attr, buf, count, _name.index,     \
				      _dtype);                                \
	}                                                                     \
	static ssize_t _name##_show(struct device *dev,                       \
				    struct device_attribute *attr, char *buf) \
	{                                                                     \
		return _group##_show(dev, attr, buf, _name.index, _dtype);    \
	}                                                                     \
	static ssize_t _name##_##_rtype##_show(                               \
		struct device *dev, struct device_attribute *attr, char *buf) \
	{                                                                     \
		return _group##_options(dev, attr, buf, _name.index);         \
	}                                                                     \
	static DEVICE_ATTR_RW_NAMED(_name, _attrname)

#define LEGO_DEVICE_ATTR_WO(_name, _attrname, _dtype, _group)             \
	static ssize_t _name##_store(struct device *dev,                  \
				     struct device_attribute *attr,       \
				     const char *buf, size_t count)       \
	{                                                                 \
		return _group##_store(dev, attr, buf, count, _name.index, \
				      _dtype);                            \
	}                                                                 \
	static DEVICE_ATTR_WO_NAMED(_name, _attrname)

#define LEGO_DEVICE_ATTR_RO(_name, _attrname, _dtype, _group)                 \
	static ssize_t _name##_show(struct device *dev,                       \
				    struct device_attribute *attr, char *buf) \
	{                                                                     \
		return _group##_show(dev, attr, buf, _name.index, _dtype);    \
	}                                                                     \
	static DEVICE_ATTR_RO_NAMED(_name, _attrname)

#define LEGO_CAL_DEVICE_ATTR(_name, _attrname, _scmd, _dtype, _rtype)         \
	static ssize_t _name##_store(struct device *dev,                      \
				     struct device_attribute *attr,           \
				     const char *buf, size_t count)           \
	{                                                                     \
		return calibrate_config_store(dev, attr, buf, _name.index,    \
					      _scmd, count, _dtype);          \
	}                                                                     \
	static ssize_t _name##_##_rtype##_show(                               \
		struct device *dev, struct device_attribute *attr, char *buf) \
	{                                                                     \
		return calibrate_config_options(dev, attr, buf);              \
	}                                                                     \
	static DEVICE_ATTR_WO_NAMED(_name, _attrname)

#define LEGO_DEVICE_STATUS_ATTR(_name, _attrname, _scmd, _dtype)              \
	static ssize_t _name##_show(struct device *dev,                       \
				    struct device_attribute *attr, char *buf) \
	{                                                                     \
		return device_status_show(dev, attr, buf, _name.index, _scmd, \
					  _dtype);                            \
	}                                                                     \
	static DEVICE_ATTR_RO_NAMED(_name, _attrname)

/* Gamepad - MCU */
static struct go_cfg_attr version_product_mcu = { PRODUCT_VERSION };
LEGO_DEVICE_ATTR_RO(version_product_mcu, "product_version", USB_MCU, version);

static struct go_cfg_attr version_protocol_mcu = { PROTOCOL_VERSION };
LEGO_DEVICE_ATTR_RO(version_protocol_mcu, "protocol_version", USB_MCU, version);

static struct go_cfg_attr version_firmware_mcu = { FIRMWARE_VERSION };
LEGO_DEVICE_ATTR_RO(version_firmware_mcu, "firmware_version", USB_MCU, version);

static struct go_cfg_attr version_hardware_mcu = { HARDWARE_VERSION };
LEGO_DEVICE_ATTR_RO(version_hardware_mcu, "hardware_version", USB_MCU, version);

static struct go_cfg_attr version_gen_mcu = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_mcu, "hardware_generation", USB_MCU, version);

static struct go_cfg_attr fps_switch_status = { FEATURE_FPS_SWITCH_STATUS };
LEGO_DEVICE_ATTR_RO(fps_switch_status, "fps_switch_status", UNSPECIFIED,
		    feature_status);

static struct go_cfg_attr gamepad_mode = { FEATURE_GAMEPAD_MODE };
LEGO_DEVICE_ATTR_RW(gamepad_mode, "mode", UNSPECIFIED, index, feature_status);
static DEVICE_ATTR_RO_NAMED(gamepad_mode_index, "mode_index");

static struct go_cfg_attr reset_mcu = { FEATURE_RESET_GAMEPAD };
LEGO_DEVICE_ATTR_WO(reset_mcu, "reset_mcu", USB_MCU, feature_status);

static struct go_cfg_attr gamepad_rumble_intensity = { MOTOR_INTENSITY };
LEGO_DEVICE_ATTR_RW(gamepad_rumble_intensity, "rumble_intensity", UNSPECIFIED,
		    index, motor_config);
static DEVICE_ATTR_RO_NAMED(gamepad_rumble_intensity_index,
			    "rumble_intensity_index");

static DEVICE_ATTR_RW(fps_mode_dpi);
static DEVICE_ATTR_RO(fps_mode_dpi_index);

static DEVICE_ATTR_RW(os_mode);
static DEVICE_ATTR_RO(os_mode_index);

static struct attribute *mcu_attrs[] = {
	&dev_attr_fps_mode_dpi.attr,
	&dev_attr_fps_mode_dpi_index.attr,
	&dev_attr_fps_switch_status.attr,
	&dev_attr_gamepad_mode.attr,
	&dev_attr_gamepad_mode_index.attr,
	&dev_attr_gamepad_rumble_intensity.attr,
	&dev_attr_gamepad_rumble_intensity_index.attr,
	&dev_attr_os_mode.attr,
	&dev_attr_os_mode_index.attr,
	&dev_attr_reset_mcu.attr,
	&dev_attr_version_firmware_mcu.attr,
	&dev_attr_version_gen_mcu.attr,
	&dev_attr_version_hardware_mcu.attr,
	&dev_attr_version_product_mcu.attr,
	&dev_attr_version_protocol_mcu.attr,
	NULL,
};

static const struct attribute_group mcu_attr_group = {
	.attrs = mcu_attrs,
};

/* Gamepad - TX Dongle */
static struct go_cfg_attr version_product_tx_dongle = { PRODUCT_VERSION };
LEGO_DEVICE_ATTR_RO(version_product_tx_dongle, "product_version", TX_DONGLE, version);

static struct go_cfg_attr version_protocol_tx_dongle = { PROTOCOL_VERSION };
LEGO_DEVICE_ATTR_RO(version_protocol_tx_dongle, "protocol_version", TX_DONGLE, version);

static struct go_cfg_attr version_firmware_tx_dongle = { FIRMWARE_VERSION };
LEGO_DEVICE_ATTR_RO(version_firmware_tx_dongle, "firmware_version", TX_DONGLE, version);

static struct go_cfg_attr version_hardware_tx_dongle = { HARDWARE_VERSION };
LEGO_DEVICE_ATTR_RO(version_hardware_tx_dongle, "hardware_version", TX_DONGLE, version);

static struct go_cfg_attr version_gen_tx_dongle = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_tx_dongle, "hardware_generation", TX_DONGLE, version);

static struct go_cfg_attr reset_tx_dongle = { FEATURE_RESET_GAMEPAD };
LEGO_DEVICE_ATTR_RO(reset_tx_dongle, "reset", TX_DONGLE, feature_status);

static struct attribute *tx_dongle_attrs[] = {
	&dev_attr_reset_tx_dongle.attr,
	&dev_attr_version_hardware_tx_dongle.attr,
	&dev_attr_version_firmware_tx_dongle.attr,
	&dev_attr_version_gen_tx_dongle.attr,
	&dev_attr_version_product_tx_dongle.attr,
	&dev_attr_version_protocol_tx_dongle.attr,
	NULL,
};

static const struct attribute_group tx_dongle_attr_group = {
	.name = "tx_dongle",
	.attrs = tx_dongle_attrs,
};

/* Gamepad - Left */
static struct go_cfg_attr version_product_left = { PRODUCT_VERSION };
LEGO_DEVICE_ATTR_RO(version_product_left, "product_version", LEFT_CONTROLLER, version);

static struct go_cfg_attr version_protocol_left = { PROTOCOL_VERSION };
LEGO_DEVICE_ATTR_RO(version_protocol_left, "protocol_version", LEFT_CONTROLLER, version);

static struct go_cfg_attr version_firmware_left = { FIRMWARE_VERSION };
LEGO_DEVICE_ATTR_RO(version_firmware_left, "firmware_version", LEFT_CONTROLLER, version);

static struct go_cfg_attr version_hardware_left = { HARDWARE_VERSION };
LEGO_DEVICE_ATTR_RO(version_hardware_left, "hardware_version", LEFT_CONTROLLER, version);

static struct go_cfg_attr version_gen_left = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_left, "hardware_generation", LEFT_CONTROLLER, version);

static struct go_cfg_attr auto_sleep_time_left = { FEATURE_AUTO_SLEEP_TIME };
LEGO_DEVICE_ATTR_RW(auto_sleep_time_left, "auto_sleep_time", LEFT_CONTROLLER,
		    range, feature_status);
static DEVICE_ATTR_RO_NAMED(auto_sleep_time_left_range,
			    "auto_sleep_time_range");

static struct go_cfg_attr imu_bypass_left = { FEATURE_IMU_BYPASS };
LEGO_DEVICE_ATTR_RW(imu_bypass_left, "imu_bypass_enabled", LEFT_CONTROLLER,
		    index, feature_status);
static DEVICE_ATTR_RO_NAMED(imu_bypass_left_index, "imu_bypass_enabled_index");

static struct go_cfg_attr imu_enabled_left = { FEATURE_IMU_ENABLE };
LEGO_DEVICE_ATTR_RW(imu_enabled_left, "imu_enabled", LEFT_CONTROLLER, index,
		    feature_status);
static DEVICE_ATTR_RO_NAMED(imu_enabled_left_index, "imu_enabled_index");

static struct go_cfg_attr reset_left = { FEATURE_RESET_GAMEPAD };
LEGO_DEVICE_ATTR_WO(reset_left, "reset", LEFT_CONTROLLER, feature_status);

static struct go_cfg_attr rumble_mode_left = { RUMBLE_MODE };
LEGO_DEVICE_ATTR_RW(rumble_mode_left, "rumble_mode", LEFT_CONTROLLER, index,
		    motor_config);
static DEVICE_ATTR_RO_NAMED(rumble_mode_left_index, "rumble_mode_index");

static struct go_cfg_attr rumble_notification_left = { VIBRATION_NOTIFY_ENABLE };
LEGO_DEVICE_ATTR_RW(rumble_notification_left, "rumble_notification",
		    LEFT_CONTROLLER, index, motor_config);
static DEVICE_ATTR_RO_NAMED(rumble_notification_left_index,
			    "rumble_notification_index");

static struct go_cfg_attr cal_trigg_left = { TRIGGER_CALIBRATE };
LEGO_CAL_DEVICE_ATTR(cal_trigg_left, "calibrate_trigger", SET_TRIGGER_CFG,
		     LEFT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_trigg_left_index, "calibrate_trigger_index");

static struct go_cfg_attr cal_joy_left = { JOYSTICK_CALIBRATE };
LEGO_CAL_DEVICE_ATTR(cal_joy_left, "calibrate_joystick", SET_JOYSTICK_CFG,
		     LEFT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_joy_left_index, "calibrate_joystick_index");

static struct go_cfg_attr cal_gyro_left = { GYRO_CALIBRATE };
LEGO_CAL_DEVICE_ATTR(cal_gyro_left, "calibrate_gyro", SET_GYRO_CFG,
		     LEFT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_gyro_left_index, "calibrate_gyro_index");

static struct go_cfg_attr cal_trigg_left_status = { GET_CAL_STATUS };
LEGO_DEVICE_STATUS_ATTR(cal_trigg_left_status, "calibrate_trigger_status",
			LEFT_CONTROLLER, CALDEV_TRIGGER);

static struct go_cfg_attr cal_joy_left_status = { GET_CAL_STATUS };
LEGO_DEVICE_STATUS_ATTR(cal_joy_left_status, "calibrate_joystick_status",
			LEFT_CONTROLLER, CALDEV_JOYSTICK);

static struct go_cfg_attr cal_gyro_left_status = { GET_CAL_STATUS };
LEGO_DEVICE_STATUS_ATTR(cal_gyro_left_status, "calibrate_gyro_status",
			LEFT_CONTROLLER, CALDEV_GYROSCOPE);

static struct attribute *left_gamepad_attrs[] = {
	&dev_attr_auto_sleep_time_left.attr,
	&dev_attr_auto_sleep_time_left_range.attr,
	&dev_attr_cal_gyro_left.attr,
	&dev_attr_cal_gyro_left_index.attr,
	&dev_attr_cal_gyro_left_status.attr,
	&dev_attr_cal_joy_left.attr,
	&dev_attr_cal_joy_left_index.attr,
	&dev_attr_cal_joy_left_status.attr,
	&dev_attr_cal_trigg_left.attr,
	&dev_attr_cal_trigg_left_index.attr,
	&dev_attr_cal_trigg_left_status.attr,
	&dev_attr_imu_bypass_left.attr,
	&dev_attr_imu_bypass_left_index.attr,
	&dev_attr_imu_enabled_left.attr,
	&dev_attr_imu_enabled_left_index.attr,
	&dev_attr_reset_left.attr,
	&dev_attr_rumble_mode_left.attr,
	&dev_attr_rumble_mode_left_index.attr,
	&dev_attr_rumble_notification_left.attr,
	&dev_attr_rumble_notification_left_index.attr,
	&dev_attr_version_hardware_left.attr,
	&dev_attr_version_firmware_left.attr,
	&dev_attr_version_gen_left.attr,
	&dev_attr_version_product_left.attr,
	&dev_attr_version_protocol_left.attr,
	NULL,
};

static const struct attribute_group left_gamepad_attr_group = {
	.name = "left_handle",
	.attrs = left_gamepad_attrs,
};

/* Gamepad - Right */
static struct go_cfg_attr version_product_right = { PRODUCT_VERSION };
LEGO_DEVICE_ATTR_RO(version_product_right, "product_version", RIGHT_CONTROLLER, version);

static struct go_cfg_attr version_protocol_right = { PROTOCOL_VERSION };
LEGO_DEVICE_ATTR_RO(version_protocol_right, "protocol_version", RIGHT_CONTROLLER, version);

static struct go_cfg_attr version_firmware_right = { FIRMWARE_VERSION };
LEGO_DEVICE_ATTR_RO(version_firmware_right, "firmware_version", RIGHT_CONTROLLER, version);

static struct go_cfg_attr version_hardware_right = { HARDWARE_VERSION };
LEGO_DEVICE_ATTR_RO(version_hardware_right, "hardware_version", RIGHT_CONTROLLER, version);

static struct go_cfg_attr version_gen_right = { HARDWARE_GENERATION };
LEGO_DEVICE_ATTR_RO(version_gen_right, "hardware_generation", RIGHT_CONTROLLER, version);

static struct go_cfg_attr auto_sleep_time_right = { FEATURE_AUTO_SLEEP_TIME };
LEGO_DEVICE_ATTR_RW(auto_sleep_time_right, "auto_sleep_time", RIGHT_CONTROLLER,
		    range, feature_status);
static DEVICE_ATTR_RO_NAMED(auto_sleep_time_right_range,
			    "auto_sleep_time_range");

static struct go_cfg_attr imu_bypass_right = { FEATURE_IMU_BYPASS };
LEGO_DEVICE_ATTR_RW(imu_bypass_right, "imu_bypass_enabled", RIGHT_CONTROLLER,
		    index, feature_status);
static DEVICE_ATTR_RO_NAMED(imu_bypass_right_index, "imu_bypass_enabled_index");

static struct go_cfg_attr imu_enabled_right = { FEATURE_IMU_BYPASS };
LEGO_DEVICE_ATTR_RW(imu_enabled_right, "imu_enabled", RIGHT_CONTROLLER, index,
		    feature_status);
static DEVICE_ATTR_RO_NAMED(imu_enabled_right_index, "imu_enabled_index");

static struct go_cfg_attr reset_right = { FEATURE_RESET_GAMEPAD };
LEGO_DEVICE_ATTR_WO(reset_right, "reset", LEFT_CONTROLLER, feature_status);

static struct go_cfg_attr rumble_mode_right = { RUMBLE_MODE };
LEGO_DEVICE_ATTR_RW(rumble_mode_right, "rumble_mode", RIGHT_CONTROLLER, index,
		    motor_config);
static DEVICE_ATTR_RO_NAMED(rumble_mode_right_index, "rumble_mode_index");

static struct go_cfg_attr rumble_notification_right = { VIBRATION_NOTIFY_ENABLE };
LEGO_DEVICE_ATTR_RW(rumble_notification_right, "rumble_notification",
		    RIGHT_CONTROLLER, index, motor_config);
static DEVICE_ATTR_RO_NAMED(rumble_notification_right_index,
			    "rumble_notification_index");

static struct go_cfg_attr cal_trigg_right = { TRIGGER_CALIBRATE };
LEGO_CAL_DEVICE_ATTR(cal_trigg_right, "calibrate_trigger", SET_TRIGGER_CFG,
		     RIGHT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_trigg_right_index, "calibrate_trigger_index");

static struct go_cfg_attr cal_joy_right = { JOYSTICK_CALIBRATE };
LEGO_CAL_DEVICE_ATTR(cal_joy_right, "calibrate_joystick", SET_JOYSTICK_CFG,
		     RIGHT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_joy_right_index, "calibrate_joystick_index");

static struct go_cfg_attr cal_gyro_right = { GYRO_CALIBRATE };
LEGO_CAL_DEVICE_ATTR(cal_gyro_right, "calibrate_gyro", SET_GYRO_CFG,
		     RIGHT_CONTROLLER, index);
static DEVICE_ATTR_RO_NAMED(cal_gyro_right_index, "calibrate_gyro_index");

static struct go_cfg_attr cal_trigg_right_status = { GET_CAL_STATUS };
LEGO_DEVICE_STATUS_ATTR(cal_trigg_right_status, "calibrate_trigger_status",
			RIGHT_CONTROLLER, CALDEV_TRIGGER);

static struct go_cfg_attr cal_joy_right_status = { GET_CAL_STATUS };
LEGO_DEVICE_STATUS_ATTR(cal_joy_right_status, "calibrate_joystick_status",
			RIGHT_CONTROLLER, CALDEV_JOYSTICK);

static struct go_cfg_attr cal_gyro_right_status = { GET_CAL_STATUS };
LEGO_DEVICE_STATUS_ATTR(cal_gyro_right_status, "calibrate_gyro_status",
			RIGHT_CONTROLLER, CALDEV_GYROSCOPE);

static struct attribute *right_gamepad_attrs[] = {
	&dev_attr_auto_sleep_time_right.attr,
	&dev_attr_auto_sleep_time_right_range.attr,
	&dev_attr_cal_gyro_right.attr,
	&dev_attr_cal_gyro_right_index.attr,
	&dev_attr_cal_gyro_right_status.attr,
	&dev_attr_cal_joy_right.attr,
	&dev_attr_cal_joy_right_index.attr,
	&dev_attr_cal_joy_right_status.attr,
	&dev_attr_cal_trigg_right.attr,
	&dev_attr_cal_trigg_right_index.attr,
	&dev_attr_cal_trigg_right_status.attr,
	&dev_attr_imu_bypass_right.attr,
	&dev_attr_imu_bypass_right_index.attr,
	&dev_attr_imu_enabled_right.attr,
	&dev_attr_imu_enabled_right_index.attr,
	&dev_attr_reset_right.attr,
	&dev_attr_rumble_mode_right.attr,
	&dev_attr_rumble_mode_right_index.attr,
	&dev_attr_rumble_notification_right.attr,
	&dev_attr_rumble_notification_right_index.attr,
	&dev_attr_version_hardware_right.attr,
	&dev_attr_version_firmware_right.attr,
	&dev_attr_version_gen_right.attr,
	&dev_attr_version_product_right.attr,
	&dev_attr_version_protocol_right.attr,
	NULL,
};

static const struct attribute_group right_gamepad_attr_group = {
	.name = "right_handle",
	.attrs = right_gamepad_attrs,
};

/* Touchpad */
static struct go_cfg_attr touchpad_enabled = { FEATURE_TOUCHPAD_ENABLE };
LEGO_DEVICE_ATTR_RW(touchpad_enabled, "enabled", UNSPECIFIED, index,
		    feature_status);
static DEVICE_ATTR_RO_NAMED(touchpad_enabled_index, "enabled_index");

static struct go_cfg_attr touchpad_vibration_enabled = { TP_VIBRATION_ENABLE };
LEGO_DEVICE_ATTR_RW(touchpad_vibration_enabled, "vibration_enabled", UNSPECIFIED,
		    index, motor_config);
static DEVICE_ATTR_RO_NAMED(touchpad_vibration_enabled_index,
			    "vibration_enabled_index");

static struct go_cfg_attr touchpad_vibration_intensity = { TP_VIBRATION_INTENSITY };
LEGO_DEVICE_ATTR_RW(touchpad_vibration_intensity, "vibration_intensity",
		    UNSPECIFIED, index, motor_config);
static DEVICE_ATTR_RO_NAMED(touchpad_vibration_intensity_index,
			    "vibration_intensity_index");

static struct attribute *touchpad_attrs[] = {
	&dev_attr_touchpad_enabled.attr,
	&dev_attr_touchpad_enabled_index.attr,
	&dev_attr_touchpad_vibration_enabled.attr,
	&dev_attr_touchpad_vibration_enabled_index.attr,
	&dev_attr_touchpad_vibration_intensity.attr,
	&dev_attr_touchpad_vibration_intensity_index.attr,
	NULL,
};

static const struct attribute_group touchpad_attr_group = {
	.name = "touchpad",
	.attrs = touchpad_attrs,
};

static const struct attribute_group *top_level_attr_groups[] = {
	&mcu_attr_group,	  &tx_dongle_attr_group,
	&left_gamepad_attr_group, &right_gamepad_attr_group,
	&touchpad_attr_group,	  NULL,
};

/* RGB */
static struct go_cfg_attr rgb_enabled = { FEATURE_LIGHT_ENABLE };

LEGO_DEVICE_ATTR_RW(rgb_enabled, "enabled", UNSPECIFIED, index, feature_status);
static DEVICE_ATTR_RO_NAMED(rgb_effect_index, "effect_index");
static DEVICE_ATTR_RO_NAMED(rgb_enabled_index, "enabled_index");
static DEVICE_ATTR_RO_NAMED(rgb_mode_index, "mode_index");
static DEVICE_ATTR_RO_NAMED(rgb_profile_range, "profile_range");
static DEVICE_ATTR_RO_NAMED(rgb_speed_range, "speed_range");
static DEVICE_ATTR_RW_NAMED(rgb_effect, "effect");
static DEVICE_ATTR_RW_NAMED(rgb_mode, "mode");
static DEVICE_ATTR_RW_NAMED(rgb_profile, "profile");
static DEVICE_ATTR_RW_NAMED(rgb_speed, "speed");

static struct attribute *go_rgb_attrs[] = {
	&dev_attr_rgb_effect.attr,
	&dev_attr_rgb_effect_index.attr,
	&dev_attr_rgb_enabled.attr,
	&dev_attr_rgb_enabled_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 = go_rgb_attrs,
};

static struct mc_subled go_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 go_cdev_rgb = {
	.led_cdev = {
		.name = "go:rgb:joystick_rings",
		.color = LED_COLOR_ID_RGB,
		.brightness = 0x50,
		.max_brightness = 0x64,
		.brightness_set = hid_go_brightness_set,
	},
	.num_colors = ARRAY_SIZE(go_rgb_subled_info),
	.subled_info = go_rgb_subled_info,
};

static void cfg_setup(struct work_struct *work)
{
	int ret;

	/* MCU Version Attrs */
	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       PRODUCT_VERSION, USB_MCU, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve USB_MCU Product Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       PROTOCOL_VERSION, USB_MCU, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve USB_MCU Protocol Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       FIRMWARE_VERSION, USB_MCU, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve USB_MCU Firmware Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       HARDWARE_VERSION, USB_MCU, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve USB_MCU Hardware Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       HARDWARE_GENERATION, USB_MCU, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve USB_MCU Hardware Generation: %i\n", ret);
		return;
	}

	/* TX Dongle Version Attrs */
	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       PRODUCT_VERSION, TX_DONGLE, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve TX_DONGLE Product Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       PROTOCOL_VERSION, TX_DONGLE, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve TX_DONGLE Protocol Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       FIRMWARE_VERSION, TX_DONGLE, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve TX_DONGLE Firmware Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       HARDWARE_VERSION, TX_DONGLE, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve TX_DONGLE Hardware Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       HARDWARE_GENERATION, TX_DONGLE, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve TX_DONGLE Hardware Generation: %i\n", ret);
		return;
	}

	/* Left Handle Version Attrs */
	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       PRODUCT_VERSION, LEFT_CONTROLLER, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve LEFT_CONTROLLER Product Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       PROTOCOL_VERSION, LEFT_CONTROLLER, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve LEFT_CONTROLLER Protocol Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       FIRMWARE_VERSION, LEFT_CONTROLLER, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve LEFT_CONTROLLER Firmware Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       HARDWARE_VERSION, LEFT_CONTROLLER, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve LEFT_CONTROLLER Hardware Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       HARDWARE_GENERATION, LEFT_CONTROLLER, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve LEFT_CONTROLLER Hardware Generation: %i\n", ret);
		return;
	}

	/* Right Handle Version Attrs */
	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       PRODUCT_VERSION, RIGHT_CONTROLLER, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve RIGHT_CONTROLLER Product Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       PROTOCOL_VERSION, RIGHT_CONTROLLER, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve RIGHT_CONTROLLER Protocol Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       FIRMWARE_VERSION, RIGHT_CONTROLLER, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve RIGHT_CONTROLLER Firmware Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       HARDWARE_VERSION, RIGHT_CONTROLLER, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve RIGHT_CONTROLLER Hardware Version: %i\n", ret);
		return;
	}

	ret = mcu_property_out(drvdata.hdev, MCU_CONFIG_DATA, GET_VERSION_DATA,
			       HARDWARE_GENERATION, RIGHT_CONTROLLER, NULL, 0);
	if (ret < 0) {
		dev_err(&drvdata.hdev->dev,
			"Failed to retrieve RIGHT_CONTROLLER Hardware Generation: %i\n", ret);
		return;
	}
}

static int hid_go_cfg_probe(struct hid_device *hdev,
			    const struct hid_device_id *_id)
{
	unsigned char *buf;
	int ret;

	buf = devm_kzalloc(&hdev->dev, GO_PACKET_SIZE, GFP_KERNEL);
	if (!buf)
		return -ENOMEM;

	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, &go_cdev_rgb);
	if (ret) {
		dev_err_probe(&hdev->dev, ret, "Failed to create RGB device\n");
		return ret;
	}

	ret = devm_device_add_group(go_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 = &go_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.go_cfg_setup, &cfg_setup);
	ret = schedule_delayed_work(&drvdata.go_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_go_cfg_remove(struct hid_device *hdev)
{
	guard(mutex)(&drvdata.cfg_mutex);
	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_go_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
	int ret, ep;

	if (!hid_is_usb(hdev))
		return -EINVAL;

	hdev->quirks |= HID_QUIRK_INPUT_PER_APP | HID_QUIRK_MULTI_INPUT;

	ret = hid_parse(hdev);
	if (ret) {
		hid_err(hdev, "Parse failed\n");
		return ret;
	}

	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
	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_GP_INTF_IN) {
		dev_dbg(&hdev->dev, "Started interface %x as generic HID device\n", ep);
		return 0;
	}

	ret = hid_go_cfg_probe(hdev, id);
	if (ret)
		dev_err_probe(&hdev->dev, ret, "Failed to start configuration interface\n");

	dev_dbg(&hdev->dev, "Started Legion Go HID Device: %x\n", ep);

	return ret;
}

static void hid_go_remove(struct hid_device *hdev)
{
	int ep = get_endpoint_address(hdev);

	if (ep <= 0)
		return;

	switch (ep) {
	case GO_GP_INTF_IN:
		hid_go_cfg_remove(hdev);
		break;
	default:
		hid_hw_close(hdev);
		hid_hw_stop(hdev);
		break;
	}
}

static const struct hid_device_id hid_go_devices[] = {
	{ HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
			 USB_DEVICE_ID_LENOVO_LEGION_GO2_XINPUT) },
	{ HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
			 USB_DEVICE_ID_LENOVO_LEGION_GO2_DINPUT) },
	{ HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
			 USB_DEVICE_ID_LENOVO_LEGION_GO2_DUAL_DINPUT) },
	{ HID_USB_DEVICE(USB_VENDOR_ID_LENOVO,
			 USB_DEVICE_ID_LENOVO_LEGION_GO2_FPS) },
	{}
};
MODULE_DEVICE_TABLE(hid, hid_go_devices);

static struct hid_driver hid_lenovo_go = {
	.name = "hid-lenovo-go",
	.id_table = hid_go_devices,
	.probe = hid_go_probe,
	.remove = hid_go_remove,
	.raw_event = hid_go_raw_event,
};
module_hid_driver(hid_lenovo_go);

MODULE_AUTHOR("Derek J. Clark");
MODULE_DESCRIPTION("HID Driver for Lenovo Legion Go Series Gamepads.");
MODULE_LICENSE("GPL");