]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
HID: hid-lenovo-go: Add RGB LED control interface
authorDerek J. Clark <derekjohn.clark@gmail.com>
Tue, 10 Mar 2026 07:29:24 +0000 (07:29 +0000)
committerJiri Kosina <jkosina@suse.com>
Tue, 10 Mar 2026 16:53:17 +0000 (17:53 +0100)
Adds an LED multicolor class device and attribute group for controlling
the RGB of the Left and right handles. In addition to the standard
led_cdev attributes, additional attributes that allow for the control of
the effect (monocolor, breathe, rainbow, and chroma), speed of the
effect change, an enable toggle, and profile.

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>
drivers/hid/hid-lenovo-go.c

index 24f9444c93cdaeb45684bb7f14c7508446e8ef7d..ee7d4bf23a17938097b08887f009b02640d94884 100644 (file)
@@ -18,6 +18,7 @@
 #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>
@@ -37,6 +38,7 @@
 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;
@@ -68,7 +70,11 @@ static struct hid_go_cfg {
        u32 mcu_version_product;
        u32 mcu_version_protocol;
        u32 mouse_dpi;
+       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;
@@ -223,6 +229,41 @@ static const char *const rumble_mode_text[] = {
 
 #define FPS_MODE_DPI           0x02
 
+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",
+};
+
 static int hid_go_version_event(struct command_report *cmd_rep)
 {
        switch (cmd_rep->sub_cmd) {
@@ -440,6 +481,33 @@ static int hid_go_fps_dpi_event(struct command_report *cmd_rep)
        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_set_event_return(struct command_report *cmd_rep)
 {
        if (cmd_rep->data[0] != 0)
@@ -493,9 +561,13 @@ static int hid_go_raw_event(struct hid_device *hdev, struct hid_report *report,
                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 SET_FEATURE_STATUS:
                case SET_MOTOR_CFG:
                case SET_DPI_CFG:
+               case SET_RGB_CFG:
                        ret = hid_go_set_event_return(cmd_rep);
                        break;
                default:
@@ -565,6 +637,12 @@ static ssize_t version_show(struct device *dev, struct device_attribute *attr,
                            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:
@@ -1079,6 +1157,277 @@ static ssize_t fps_mode_dpi_index_show(struct device *dev,
        return sysfs_emit(buf, "500 800 1200 1800\n");
 }
 
+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,           \
@@ -1389,6 +1738,71 @@ static const struct attribute_group *top_level_attr_groups[] = {
        &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;
@@ -1579,6 +1993,21 @@ static int hid_go_cfg_probe(struct hid_device *hdev,
                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