From e237995ccda72dd4cc30b9e8398be08500afc21d Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 4 Sep 2026 22:34:06 +0200 Subject: [PATCH 1/6] leds: Add LED_DYNAMIC_LIGHTING flag to LED core Define LED_DYNAMIC_LIGHTING flag in struct led_classdev to enable runtime type identification for Dynamic Lighting class devices, matching the established pattern used by LED_MULTI_COLOR. Signed-off-by: Marco Scardovi --- include/linux/leds.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/linux/leds.h b/include/linux/leds.h index a515f075c29a11..b714d694960f34 100644 --- a/include/linux/leds.h +++ b/include/linux/leds.h @@ -109,6 +109,7 @@ struct led_classdev { #define LED_INIT_DEFAULT_TRIGGER BIT(23) #define LED_REJECT_NAME_CONFLICT BIT(24) #define LED_MULTI_COLOR BIT(25) +#define LED_DYNAMIC_LIGHTING BIT(26) /* set_brightness_work / blink_timer flags, atomic, private. */ unsigned long work_flags; From 1d2a9cdd5d39e29d3b11b353f77362e2e25e615c Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 4 Sep 2026 22:34:26 +0200 Subject: [PATCH 2/6] leds: dynamic: Add Dynamic Lighting core class interface Add a dedicated Dynamic Lighting LED class for devices that expose multi-LED effects, palette programming, direct frame streaming or lighting state persistence through sysfs. The new class extends struct led_classdev with common effect, speed, direction and power-state controls, plus binary write interfaces for packed RGB frames and device-specific frame payloads. Registration validates the exported capabilities, exposes only the attributes implemented by the driver, and serializes writes under led_access and the class-private lock so drivers can coexist safely with LED triggers. This provides a common kernel ABI for complex lighting devices without requiring each driver to invent its own sysfs layout. Signed-off-by: Marco Scardovi --- MAINTAINERS | 8 + drivers/leds/Kconfig | 11 + drivers/leds/Makefile | 1 + drivers/leds/led-class-dynamic.c | 824 +++++++++++++++++++++++++++ include/linux/led-dynamic-lighting.h | 248 ++++++++ 5 files changed, 1092 insertions(+) create mode 100644 drivers/leds/led-class-dynamic.c create mode 100644 include/linux/led-dynamic-lighting.h diff --git a/MAINTAINERS b/MAINTAINERS index a5d869a534bc30..2865d99cd4b136 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14831,6 +14831,14 @@ S: Supported F: Documentation/scsi/leapraid.rst F: drivers/scsi/leapraid/ +LED DYNAMIC LIGHTING CLASS +M: Marco Scardovi +M: Denis Benato +L: linux-leds@vger.kernel.org +S: Maintained +F: drivers/leds/led-class-dynamic.c +F: include/linux/led-dynamic-lighting.h + LED SUBSYSTEM M: Lee Jones M: Pavel Machek diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index d0d3182236be4c..13da18900a7d97 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -46,6 +46,17 @@ config LEDS_CLASS_MULTICOLOR for multicolor LEDs that are grouped together. This class is not intended for single color LEDs. It can be built as a module. +config LEDS_CLASS_DYNAMIC + tristate "LED Dynamic Lighting Class Support" + depends on LEDS_CLASS + help + This option enables support for the Dynamic Lighting LED class in + /sys/class/leds. It wraps the LED class and adds dynamic lighting + attributes (per-key RGB streaming, 2D matrix controls, hardware + animation effects, and power state persistence). + + It can be built as a module. + config LEDS_BRIGHTNESS_HW_CHANGED bool "LED Class brightness_hw_changed attribute support" depends on LEDS_CLASS diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index a68244bd50fb1a..ddccceca94b71e 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -5,6 +5,7 @@ obj-$(CONFIG_NEW_LEDS) += led-core.o obj-$(CONFIG_LEDS_CLASS) += led-class.o obj-$(CONFIG_LEDS_CLASS_FLASH) += led-class-flash.o obj-$(CONFIG_LEDS_CLASS_MULTICOLOR) += led-class-multicolor.o +obj-$(CONFIG_LEDS_CLASS_DYNAMIC) += led-class-dynamic.o obj-$(CONFIG_LEDS_TRIGGERS) += led-triggers.o obj-$(CONFIG_LEDS_KUNIT_TEST) += led-test.o diff --git a/drivers/leds/led-class-dynamic.c b/drivers/leds/led-class-dynamic.c new file mode 100644 index 00000000000000..4c036ad25151a6 --- /dev/null +++ b/drivers/leds/led-class-dynamic.c @@ -0,0 +1,824 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * LED Dynamic Lighting Class Interface + * + * Copyright (C) 2026 Open Gaming Collective + * Author: Marco Scardovi + * Author: Denis Benato + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char * const dl_effect_names[] = { + [DL_EFFECT_OFF] = "off", + [DL_EFFECT_STATIC] = "static", + [DL_EFFECT_BREATHING] = "breathing", + [DL_EFFECT_STROBE] = "strobe", + [DL_EFFECT_SPECTRUM_CYCLE] = "spectrum_cycle", + [DL_EFFECT_RAINBOW] = "rainbow", + [DL_EFFECT_DIRECT] = "direct", + [DL_EFFECT_CUSTOM] = "custom", +}; + +static const char * const dl_direction_names[] = { + [DL_DIRECTION_LEFT] = "left", + [DL_DIRECTION_RIGHT] = "right", + [DL_DIRECTION_UP] = "up", + [DL_DIRECTION_DOWN] = "down", + [DL_DIRECTION_CLOCKWISE] = "clockwise", + [DL_DIRECTION_COUNTER_CLOCKWISE] = "counter_clockwise", +}; + +static const char * const dl_power_state_names[] = { + "boot", + "awake", + "sleep", + "shutdown", +}; + +static ssize_t zone_type_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + + guard(mutex)(&ldev->lock); + + if (!ldev->zone_type || !*ldev->zone_type) + return sysfs_emit(buf, "unknown\n"); + + return sysfs_emit(buf, "%s\n", ldev->zone_type); +} +static DEVICE_ATTR_RO(zone_type); + +static ssize_t led_count_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + + guard(mutex)(&ldev->lock); + + return sysfs_emit(buf, "%u\n", ldev->led_count); +} +static DEVICE_ATTR_RO(led_count); + +static ssize_t matrix_dimensions_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + + guard(mutex)(&ldev->lock); + + return sysfs_emit(buf, "%u %u\n", ldev->matrix_width, ldev->matrix_height); +} +static DEVICE_ATTR_RO(matrix_dimensions); + +static ssize_t effect_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + int len = 0; + int i; + + guard(mutex)(&ldev->lock); + + for (i = 0; i < ARRAY_SIZE(dl_effect_names); i++) { + if (dl_effect_names[i] && (ldev->supported_effects & BIT(i))) + len += sysfs_emit_at(buf, len, "%s ", dl_effect_names[i]); + } + + if (len > 0) + buf[len - 1] = '\n'; + else + len = sysfs_emit(buf, "\n"); + + return len; +} +static DEVICE_ATTR_RO(effect_index); + +static ssize_t effect_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + + guard(mutex)(&ldev->lock); + + if (ldev->current_effect >= ARRAY_SIZE(dl_effect_names) || + !dl_effect_names[ldev->current_effect]) + return sysfs_emit(buf, "unknown\n"); + + return sysfs_emit(buf, "%s\n", dl_effect_names[ldev->current_effect]); +} + +static ssize_t effect_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + int match, ret; + + if (!ldev->ops->set_effect) + return -EOPNOTSUPP; + + match = sysfs_match_string(dl_effect_names, buf); + if (match < 0 || !(ldev->supported_effects & BIT(match))) + return -EINVAL; + + guard(mutex)(&cdev->led_access); + if (led_sysfs_is_disabled(cdev)) + return -EBUSY; + led_trigger_remove(cdev); + guard(mutex)(&ldev->lock); + + ret = ldev->ops->set_effect(ldev, match); + if (ret < 0) + return ret; + + ldev->current_effect = match; + return count; +} +static DEVICE_ATTR_RW(effect); + +static ssize_t speed_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + + guard(mutex)(&ldev->lock); + + return sysfs_emit(buf, "%u\n", ldev->speed); +} + +static ssize_t speed_range_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + + guard(mutex)(&ldev->lock); + + return sysfs_emit(buf, "0-%u\n", ldev->max_speed); +} +static DEVICE_ATTR_RO(speed_range); + +static ssize_t speed_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + unsigned int speed; + int ret; + + if (!ldev->ops->set_speed || !ldev->max_speed) + return -EOPNOTSUPP; + + ret = kstrtouint(buf, 10, &speed); + if (ret) + return ret; + + if (speed > ldev->max_speed) + return -EINVAL; + + guard(mutex)(&cdev->led_access); + if (led_sysfs_is_disabled(cdev)) + return -EBUSY; + guard(mutex)(&ldev->lock); + + ret = ldev->ops->set_speed(ldev, speed); + if (ret < 0) + return ret; + + ldev->speed = speed; + return count; +} +static DEVICE_ATTR_RW(speed); + +static ssize_t direction_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + + guard(mutex)(&ldev->lock); + + if (ldev->direction >= ARRAY_SIZE(dl_direction_names) || + !dl_direction_names[ldev->direction]) + return sysfs_emit(buf, "unknown\n"); + + return sysfs_emit(buf, "%s\n", dl_direction_names[ldev->direction]); +} + +static ssize_t direction_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + int len = 0; + int i; + + guard(mutex)(&ldev->lock); + + for (i = 0; i < ARRAY_SIZE(dl_direction_names); i++) { + if (dl_direction_names[i] && (ldev->supported_directions & BIT(i))) + len += sysfs_emit_at(buf, len, "%s ", dl_direction_names[i]); + } + + if (len > 0) + buf[len - 1] = '\n'; + else + len = sysfs_emit(buf, "\n"); + + return len; +} +static DEVICE_ATTR_RO(direction_index); + +static ssize_t direction_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + int match, ret; + + if (!ldev->ops->set_direction || !ldev->supported_directions) + return -EOPNOTSUPP; + + match = sysfs_match_string(dl_direction_names, buf); + if (match < 0 || !(ldev->supported_directions & BIT(match))) + return -EINVAL; + + guard(mutex)(&cdev->led_access); + if (led_sysfs_is_disabled(cdev)) + return -EBUSY; + guard(mutex)(&ldev->lock); + + ret = ldev->ops->set_direction(ldev, match); + if (ret < 0) + return ret; + + ldev->direction = match; + return count; +} +static DEVICE_ATTR_RW(direction); + +static ssize_t effects_palette_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + int len = 0; + unsigned int i; + + guard(mutex)(&ldev->lock); + + for (i = 0; i < ldev->num_palette_entries; i++) { + len += sysfs_emit_at(buf, len, "#%02x%02x%02x%c", + ldev->palette[i].r, + ldev->palette[i].g, + ldev->palette[i].b, + (i == ldev->num_palette_entries - 1) ? '\n' : ' '); + } + + if (!len) + len = sysfs_emit(buf, "\n"); + + return len; +} + +static ssize_t max_palette_entries_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + + guard(mutex)(&ldev->lock); + + return sysfs_emit(buf, "%u\n", ldev->max_palette_entries); +} +static DEVICE_ATTR_RO(max_palette_entries); + +static ssize_t effects_palette_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + const char *cur = buf; + unsigned int num_parsed = 0; + int ret; + + if (!ldev->ops->set_palette || !ldev->max_palette_entries) + return -EOPNOTSUPP; + + struct dl_rgb *temp_palette __free(kfree) = kmalloc_array(ldev->max_palette_entries, + sizeof(*temp_palette), + GFP_KERNEL); + if (!temp_palette) + return -ENOMEM; + + while (*cur) { + cur = skip_spaces(cur); + if (!*cur) + break; + + if (num_parsed >= ldev->max_palette_entries) + return -EINVAL; + + if (*cur != '#') + return -EINVAL; + cur++; + + if (hex2bin((u8 *)&temp_palette[num_parsed], cur, 3) < 0) + return -EINVAL; + cur += 6; + num_parsed++; + } + + if (!num_parsed) + return -EINVAL; + + guard(mutex)(&cdev->led_access); + if (led_sysfs_is_disabled(cdev)) + return -EBUSY; + led_trigger_remove(cdev); + guard(mutex)(&ldev->lock); + + ret = ldev->ops->set_palette(ldev, temp_palette, num_parsed); + if (ret < 0) + return ret; + + memcpy(ldev->palette, temp_palette, num_parsed * sizeof(*temp_palette)); + ldev->num_palette_entries = num_parsed; + + return count; +} +static DEVICE_ATTR_RW(effects_palette); + +static ssize_t power_states_index_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + int len = 0; + int i; + + guard(mutex)(&ldev->lock); + + for (i = 0; i < ARRAY_SIZE(dl_power_state_names); i++) { + if (ldev->supported_power_states & BIT(i)) + len += sysfs_emit_at(buf, len, "%s ", dl_power_state_names[i]); + } + + if (len > 0) + buf[len - 1] = '\n'; + else + len = sysfs_emit(buf, "\n"); + + return len; +} +static DEVICE_ATTR_RO(power_states_index); + +static ssize_t power_states_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + int len = 0; + int i; + + guard(mutex)(&ldev->lock); + + for (i = 0; i < ARRAY_SIZE(dl_power_state_names); i++) { + if (ldev->active_power_states & BIT(i)) + len += sysfs_emit_at(buf, len, "%s ", dl_power_state_names[i]); + } + + if (len > 0) + buf[len - 1] = '\n'; + else + len = sysfs_emit(buf, "\n"); + + return len; +} + +static ssize_t power_states_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + char state_name[16]; + const char *cur = buf; + u32 target_states = 0; + int ret, match; + size_t tok_len; + + if (!ldev->ops->set_power_states || !ldev->supported_power_states) + return -EOPNOTSUPP; + + while (*cur) { + cur = skip_spaces(cur); + if (!*cur || *cur == '\n') + break; + + tok_len = strcspn(cur, " \t\n"); + if (!tok_len || tok_len >= sizeof(state_name)) + return -EINVAL; + + memcpy(state_name, cur, tok_len); + state_name[tok_len] = '\0'; + cur += tok_len; + + match = sysfs_match_string(dl_power_state_names, state_name); + if (match < 0 || !(ldev->supported_power_states & BIT(match))) + return -EINVAL; + + target_states |= BIT(match); + } + + guard(mutex)(&cdev->led_access); + if (led_sysfs_is_disabled(cdev)) + return -EBUSY; + guard(mutex)(&ldev->lock); + + ret = ldev->ops->set_power_states(ldev, target_states); + if (ret < 0) + return ret; + + ldev->active_power_states = target_states; + return count; +} +static DEVICE_ATTR_RW(power_states); + +static ssize_t direct_buffer_write(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + size_t expected_size; + int ret; + + if (!ldev->ops->direct_write) + return -EOPNOTSUPP; + + if (check_mul_overflow((size_t)ldev->led_count, 3, &expected_size)) + return -EOVERFLOW; + + if (off != 0 || count != expected_size) + return -EINVAL; + + guard(mutex)(&cdev->led_access); + if (led_sysfs_is_disabled(cdev)) + return -EBUSY; + led_trigger_remove(cdev); + guard(mutex)(&ldev->lock); + + ret = ldev->ops->direct_write(ldev, buf, count); + if (ret < 0) + return ret; + + return count; +} + +static ssize_t frame_write(struct file *filp, struct kobject *kobj, + const struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + int ret; + + if (!ldev->ops->frame_write) + return -EOPNOTSUPP; + + if (!count || off != 0 || count > ldev->max_frame_size) + return -EINVAL; + + guard(mutex)(&cdev->led_access); + if (led_sysfs_is_disabled(cdev)) + return -EBUSY; + led_trigger_remove(cdev); + guard(mutex)(&ldev->lock); + + ret = ldev->ops->frame_write(ldev, buf, count); + if (ret < 0) + return ret; + + return count; +} + +static int led_dynamic_validate(struct led_classdev_dynamic *ldev, size_t *direct_buffer_size) +{ + if (check_mul_overflow((size_t)ldev->led_count, 3, direct_buffer_size)) + return -EOVERFLOW; + + if (!!ldev->matrix_width != !!ldev->matrix_height) + return -EINVAL; + + if (ldev->current_effect >= DL_EFFECT_MAX) + return -EINVAL; + + if (ldev->supported_effects && + !(ldev->supported_effects & BIT(ldev->current_effect))) + return -EINVAL; + + if (ldev->direction >= DL_DIRECTION_MAX) + return -EINVAL; + + if (ldev->supported_directions && + !(ldev->supported_directions & BIT(ldev->direction))) + return -EINVAL; + + if (ldev->num_palette_entries > ldev->max_palette_entries) + return -EINVAL; + + if (ldev->num_palette_entries && !ldev->palette) + return -EINVAL; + + if (ldev->active_power_states & ~ldev->supported_power_states) + return -EINVAL; + + if (ldev->ops->frame_write && + (!ldev->max_frame_size || ldev->max_frame_size > DL_MAX_FRAME_SIZE)) + return -EINVAL; + + return 0; +} + +static umode_t dl_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + + if (attr == &dev_attr_matrix_dimensions.attr) { + if (!ldev->matrix_width || !ldev->matrix_height) + return 0; + } + + if (attr == &dev_attr_power_states_index.attr || + attr == &dev_attr_power_states.attr) { + if (!ldev->supported_power_states || !ldev->ops->set_power_states) + return 0; + } + + if (attr == &dev_attr_speed_range.attr || + attr == &dev_attr_speed.attr) { + if (!ldev->max_speed || !ldev->ops->set_speed) + return 0; + } + + if (attr == &dev_attr_direction_index.attr || + attr == &dev_attr_direction.attr) { + if (!ldev->supported_directions || !ldev->ops->set_direction) + return 0; + } + + if (attr == &dev_attr_max_palette_entries.attr || + attr == &dev_attr_effects_palette.attr) { + if (!ldev->max_palette_entries || !ldev->ops->set_palette) + return 0; + } + + if (attr == &dev_attr_effect.attr || attr == &dev_attr_effect_index.attr) { + if (!ldev->supported_effects || !ldev->ops->set_effect) + return 0; + } + + return attr->mode; +} + +static umode_t dl_bin_attr_is_visible(struct kobject *kobj, + const struct bin_attribute *attr, int n) +{ + struct device *dev = kobj_to_dev(kobj); + struct led_classdev *cdev = dev_get_drvdata(dev); + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + + if (attr == &ldev->bin_attr_direct) { + if (!ldev->ops->direct_write || !ldev->led_count || + !(ldev->supported_effects & BIT(DL_EFFECT_DIRECT))) + return 0; + } + + if (attr == &ldev->bin_attr_frame) { + if (!ldev->ops->frame_write || !ldev->max_frame_size) + return 0; + } + + return attr->attr.mode; +} + +static struct attribute *led_dynamic_attrs[] = { + &dev_attr_zone_type.attr, + &dev_attr_led_count.attr, + &dev_attr_matrix_dimensions.attr, + &dev_attr_effect_index.attr, + &dev_attr_effect.attr, + &dev_attr_speed_range.attr, + &dev_attr_speed.attr, + &dev_attr_direction_index.attr, + &dev_attr_direction.attr, + &dev_attr_max_palette_entries.attr, + &dev_attr_effects_palette.attr, + &dev_attr_power_states_index.attr, + &dev_attr_power_states.attr, + NULL, +}; + +int led_classdev_dynamic_register_ext(struct device *parent, + struct led_classdev_dynamic *ldev, + struct led_init_data *init_data) +{ + struct led_classdev *cdev; + size_t direct_buffer_size; + unsigned int num_driver_groups = 0; + int ret; + + if (!ldev || !ldev->ops) + return -EINVAL; + + ret = led_dynamic_validate(ldev, &direct_buffer_size); + if (ret) + return ret; + + mutex_init(&ldev->lock); + cdev = &ldev->cdev; + cdev->flags |= LED_DYNAMIC_LIGHTING; + + sysfs_bin_attr_init(&ldev->bin_attr_direct); + ldev->bin_attr_direct.attr.name = "direct_buffer"; + ldev->bin_attr_direct.attr.mode = 0200; + ldev->bin_attr_direct.write = direct_buffer_write; + ldev->bin_attr_direct.size = direct_buffer_size; + + sysfs_bin_attr_init(&ldev->bin_attr_frame); + ldev->bin_attr_frame.attr.name = "frame"; + ldev->bin_attr_frame.attr.mode = 0200; + ldev->bin_attr_frame.write = frame_write; + ldev->bin_attr_frame.size = ldev->max_frame_size; + + ldev->bin_attrs[0] = &ldev->bin_attr_direct; + ldev->bin_attrs[1] = &ldev->bin_attr_frame; + ldev->bin_attrs[2] = NULL; + + ldev->group.attrs = led_dynamic_attrs; + ldev->group.bin_attrs = ldev->bin_attrs; + ldev->group.is_visible = dl_attr_is_visible; + ldev->group.is_bin_visible = dl_bin_attr_is_visible; + + ldev->groups[0] = &ldev->group; + ldev->groups[1] = NULL; + ldev->driver_groups = cdev->groups; + ldev->merged_groups = NULL; + ldev->palette_allocated = false; + + while (cdev->groups && cdev->groups[num_driver_groups]) + num_driver_groups++; + + if (num_driver_groups) { + unsigned int i; + + ldev->merged_groups = kcalloc(num_driver_groups + 2, + sizeof(*ldev->merged_groups), + GFP_KERNEL); + if (!ldev->merged_groups) { + mutex_destroy(&ldev->lock); + return -ENOMEM; + } + + for (i = 0; i < num_driver_groups; i++) + ldev->merged_groups[i] = cdev->groups[i]; + ldev->merged_groups[num_driver_groups] = &ldev->group; + ldev->merged_groups[num_driver_groups + 1] = NULL; + cdev->groups = ldev->merged_groups; + } else { + cdev->groups = ldev->groups; + } + + if (ldev->max_palette_entries > 0 && !ldev->palette) { + ldev->palette = + kcalloc(ldev->max_palette_entries, sizeof(*ldev->palette), + GFP_KERNEL); + if (!ldev->palette) { + kfree(ldev->merged_groups); + ldev->merged_groups = NULL; + cdev->groups = ldev->driver_groups; + mutex_destroy(&ldev->lock); + return -ENOMEM; + } + ldev->palette_allocated = true; + } + + ret = led_classdev_register_ext(parent, cdev, init_data); + if (ret) { + if (ldev->palette_allocated) { + kfree(ldev->palette); + ldev->palette = NULL; + ldev->palette_allocated = false; + } + kfree(ldev->merged_groups); + ldev->merged_groups = NULL; + cdev->groups = ldev->driver_groups; + mutex_destroy(&ldev->lock); + } + + return ret; +} +EXPORT_SYMBOL_GPL(led_classdev_dynamic_register_ext); + +void led_classdev_dynamic_unregister(struct led_classdev_dynamic *ldev) +{ + if (!ldev) + return; + + led_classdev_unregister(&ldev->cdev); + ldev->cdev.groups = ldev->driver_groups; + if (ldev->palette_allocated) { + kfree(ldev->palette); + ldev->palette = NULL; + ldev->palette_allocated = false; + } + kfree(ldev->merged_groups); + ldev->merged_groups = NULL; + mutex_destroy(&ldev->lock); +} +EXPORT_SYMBOL_GPL(led_classdev_dynamic_unregister); + +static void devm_led_classdev_dynamic_release(struct device *dev, void *res) +{ + led_classdev_dynamic_unregister(*(struct led_classdev_dynamic **)res); +} + +int devm_led_classdev_dynamic_register_ext(struct device *parent, + struct led_classdev_dynamic *ldev, + struct led_init_data *init_data) +{ + struct led_classdev_dynamic **dr; + int ret; + + dr = devres_alloc(devm_led_classdev_dynamic_release, + sizeof(*dr), GFP_KERNEL); + if (!dr) + return -ENOMEM; + + ret = led_classdev_dynamic_register_ext(parent, ldev, init_data); + if (ret) { + devres_free(dr); + return ret; + } + + *dr = ldev; + devres_add(parent, dr); + + return 0; +} +EXPORT_SYMBOL_GPL(devm_led_classdev_dynamic_register_ext); + +static int devm_led_classdev_dynamic_match(struct device *dev, + void *res, void *data) +{ + struct led_classdev_dynamic **p = res; + + if (WARN_ON(!p || !*p)) + return 0; + + return *p == data; +} + +void devm_led_classdev_dynamic_unregister(struct device *dev, + struct led_classdev_dynamic *ldev) +{ + WARN_ON(devres_release(dev, + devm_led_classdev_dynamic_release, + devm_led_classdev_dynamic_match, ldev)); +} +EXPORT_SYMBOL_GPL(devm_led_classdev_dynamic_unregister); + +MODULE_AUTHOR("Marco Scardovi "); +MODULE_AUTHOR("Denis Benato "); +MODULE_DESCRIPTION("LED Dynamic Lighting Class Interface"); +MODULE_LICENSE("GPL"); diff --git a/include/linux/led-dynamic-lighting.h b/include/linux/led-dynamic-lighting.h new file mode 100644 index 00000000000000..a8421fb6ed561d --- /dev/null +++ b/include/linux/led-dynamic-lighting.h @@ -0,0 +1,248 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * LED Dynamic Lighting Class Interface + * + * Copyright (C) 2026 Open Gaming Collective + * Author: Marco Scardovi + * Author: Denis Benato + */ + +#ifndef _LINUX_LED_DYNAMIC_LIGHTING_H +#define _LINUX_LED_DYNAMIC_LIGHTING_H + +#include +#include +#include +#include +#include +#include +#include + +/** + * enum dl_effect_mode - Hardware or driver-synthesized animation effect + * @DL_EFFECT_OFF: Lighting disabled + * @DL_EFFECT_STATIC: Fixed color across the zone + * @DL_EFFECT_BREATHING: Pulsing brightness modulation + * @DL_EFFECT_STROBE: Rapid intermittent flash + * @DL_EFFECT_SPECTRUM_CYCLE: Continuous smooth chromatic transition + * @DL_EFFECT_RAINBOW: Animated multi-color spectral wave + * @DL_EFFECT_DIRECT: Direct binary frame streaming bypass + * @DL_EFFECT_CUSTOM: Vendor-specific custom animation profile + * @DL_EFFECT_MAX: Number of effect modes + */ +enum dl_effect_mode { + DL_EFFECT_OFF = 0, + DL_EFFECT_STATIC, + DL_EFFECT_BREATHING, + DL_EFFECT_STROBE, + DL_EFFECT_SPECTRUM_CYCLE, + DL_EFFECT_RAINBOW, + DL_EFFECT_DIRECT, + DL_EFFECT_CUSTOM, + DL_EFFECT_MAX, +}; + +/** + * enum dl_direction - Effect animation propagation direction + * @DL_DIRECTION_LEFT: Animation moves toward the left + * @DL_DIRECTION_RIGHT: Animation moves toward the right + * @DL_DIRECTION_UP: Animation moves upward + * @DL_DIRECTION_DOWN: Animation moves downward + * @DL_DIRECTION_CLOCKWISE: Radial animation moving clockwise + * @DL_DIRECTION_COUNTER_CLOCKWISE: Radial animation moving counter-clockwise + * @DL_DIRECTION_MAX: Number of directions + */ +enum dl_direction { + DL_DIRECTION_LEFT = 0, + DL_DIRECTION_RIGHT, + DL_DIRECTION_UP, + DL_DIRECTION_DOWN, + DL_DIRECTION_CLOCKWISE, + DL_DIRECTION_COUNTER_CLOCKWISE, + DL_DIRECTION_MAX, +}; + +/* Power state bitmask flags */ +#define DL_POWER_STATE_BOOT BIT(0) +#define DL_POWER_STATE_AWAKE BIT(1) +#define DL_POWER_STATE_SLEEP BIT(2) +#define DL_POWER_STATE_SHUTDOWN BIT(3) +#define DL_POWER_STATE_ALL (DL_POWER_STATE_BOOT | \ + DL_POWER_STATE_AWAKE | \ + DL_POWER_STATE_SLEEP | \ + DL_POWER_STATE_SHUTDOWN) + +/* Upper bound for the optional frame binary attribute payload */ +#define DL_MAX_FRAME_SIZE 65536 + +/** + * struct dl_rgb - 24-bit RGB color representation + * @r: Red component (0-255) + * @g: Green component (0-255) + * @b: Blue component (0-255) + */ +struct dl_rgb { + u8 r; + u8 g; + u8 b; +}; + +struct led_classdev_dynamic; + +/** + * struct led_dynamic_ops - Hardware driver callback vector + * @set_effect: Configure active hardware animation effect + * @set_speed: Configure effect speed (0 to max_speed) + * @set_direction: Configure effect propagation direction + * @set_palette: Apply multi-color stacked palette + * @direct_write: Stream packed RGB buffer (size must equal led_count * 3) + * @frame_write: Stream raw grayscale/segment frame buffer; current sysfs ABI + * accepts offset-0 writes only and forwards each write as one frame + * @set_power_states: Update active power state persistence bitmask + */ +struct led_dynamic_ops { + int (*set_effect)(struct led_classdev_dynamic *ldev, + enum dl_effect_mode mode); + int (*set_speed)(struct led_classdev_dynamic *ldev, + unsigned int speed); + int (*set_direction)(struct led_classdev_dynamic *ldev, + enum dl_direction direction); + int (*set_palette)(struct led_classdev_dynamic *ldev, + const struct dl_rgb *palette, + unsigned int num_entries); + int (*direct_write)(struct led_classdev_dynamic *ldev, + const u8 *buffer, size_t size); + int (*frame_write)(struct led_classdev_dynamic *ldev, + const u8 *buffer, size_t size); + int (*set_power_states)(struct led_classdev_dynamic *ldev, + u32 active_states); +}; + +/** + * struct led_classdev_dynamic - Dynamic Lighting LED class device + * @cdev: Embedded standard LED classdev + * @ops: Hardware callback dispatch table + * @lock: Internal mutex protecting ldev state and serialization + * @zone_type: Driver-defined physical topology string for the lighting zone + * @led_count: Total individual LEDs in this zone + * @max_frame_size: Maximum accepted payload for frame_write (0 if unused) + * @matrix_width: Grid width when the driver exposes a 2D matrix layout + * @matrix_height: Grid height when the driver exposes a 2D matrix layout + * @supported_effects: Bitmask of supported enum dl_effect_mode values + * @current_effect: Currently active animation effect + * @speed: Current effect animation speed + * @max_speed: Maximum supported speed level (0 if speed not adjustable) + * @direction: Current effect animation direction + * @supported_directions: Bitmask of supported enum dl_direction values + * @palette: Allocated array of stacked palette color entries + * @num_palette_entries: Current number of valid palette entries + * @max_palette_entries: Maximum allowable palette entries + * @palette_allocated: True if @palette was allocated by the Dynamic Lighting core + * @supported_power_states: Bitmask of DL_POWER_STATE_* supported by hardware + * @active_power_states: Bitmask of currently active DL_POWER_STATE_* states + * @driver_data: Private driver reference pointer + * @bin_attr_direct: Per-instance direct RGB binary attribute + * @bin_attr_frame: Per-instance frame sink binary attribute + * @bin_attrs: Array of binary attribute pointers for group + * @group: Per-instance sysfs attribute group + * @groups: Inline sysfs attribute groups pointer array for cdev + * @driver_groups: Original driver-provided sysfs groups saved during registration + * @merged_groups: Optional dynamically allocated merge of driver and Dynamic Lighting groups + */ +struct led_classdev_dynamic { + struct led_classdev cdev; + const struct led_dynamic_ops *ops; + struct mutex lock; /* Protects ldev state serialization */ + + const char *zone_type; + unsigned int led_count; + size_t max_frame_size; + unsigned int matrix_width; + unsigned int matrix_height; + + unsigned int supported_effects; + enum dl_effect_mode current_effect; + + unsigned int speed; + unsigned int max_speed; + + enum dl_direction direction; + unsigned int supported_directions; + + struct dl_rgb *palette; + unsigned int num_palette_entries; + unsigned int max_palette_entries; + bool palette_allocated; + + u32 supported_power_states; + u32 active_power_states; + + void *driver_data; + + struct bin_attribute bin_attr_direct __aligned(__alignof__(const struct bin_attribute)); + struct bin_attribute bin_attr_frame __aligned(__alignof__(const struct bin_attribute)); + const struct bin_attribute *bin_attrs[3]; + struct attribute_group group; + const struct attribute_group *groups[2]; + const struct attribute_group **driver_groups; + const struct attribute_group **merged_groups; +}; + +static inline struct led_classdev_dynamic *lcdev_to_dldev(struct led_classdev *lcdev) +{ + return container_of(lcdev, struct led_classdev_dynamic, cdev); +} + +static inline bool is_dynamic_lighting_led(struct led_classdev *lcdev) +{ + return !!(lcdev->flags & LED_DYNAMIC_LIGHTING); +} + +#if IS_ENABLED(CONFIG_LEDS_CLASS_DYNAMIC) + +int led_classdev_dynamic_register_ext(struct device *parent, + struct led_classdev_dynamic *ldev, + struct led_init_data *init_data); +void led_classdev_dynamic_unregister(struct led_classdev_dynamic *ldev); +int devm_led_classdev_dynamic_register_ext(struct device *parent, + struct led_classdev_dynamic *ldev, + struct led_init_data *init_data); +void devm_led_classdev_dynamic_unregister(struct device *parent, + struct led_classdev_dynamic *ldev); + +#else + +static inline int led_classdev_dynamic_register_ext(struct device *parent, + struct led_classdev_dynamic *ldev, + struct led_init_data *init_data) +{ + return -EOPNOTSUPP; +} + +static inline void led_classdev_dynamic_unregister(struct led_classdev_dynamic *ldev) {} + +static inline int devm_led_classdev_dynamic_register_ext(struct device *parent, + struct led_classdev_dynamic *ldev, + struct led_init_data *init_data) +{ + return -EOPNOTSUPP; +} + +static inline void devm_led_classdev_dynamic_unregister(struct device *parent, + struct led_classdev_dynamic *ldev) {} + +#endif /* IS_ENABLED(CONFIG_LEDS_CLASS_DYNAMIC) */ + +static inline int devm_led_classdev_dynamic_register(struct device *parent, + struct led_classdev_dynamic *ldev) +{ + return devm_led_classdev_dynamic_register_ext(parent, ldev, NULL); +} + +static inline int led_classdev_dynamic_register(struct device *parent, + struct led_classdev_dynamic *ldev) +{ + return led_classdev_dynamic_register_ext(parent, ldev, NULL); +} + +#endif /* _LINUX_LED_DYNAMIC_LIGHTING_H */ From ead0fb1f134297a04083d3262b35dfd7adb3ec9e Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 4 Sep 2026 22:34:39 +0200 Subject: [PATCH 3/6] docs: leds: Document the Dynamic Lighting class ABI Document the Dynamic Lighting LED class ABI and user-facing sysfs interface. Describe the common attributes, the visibility rules for optional controls, the discoverability attributes used by programmable devices, and the current write semantics for the direct and frame binary interfaces. Also add the new document to the LED documentation index and register it in MAINTAINERS. Signed-off-by: Marco Scardovi --- .../ABI/testing/sysfs-class-leds-dynamic | 125 ++++++++++++++ Documentation/leds/index.rst | 1 + Documentation/leds/leds-class-dynamic.rst | 163 ++++++++++++++++++ MAINTAINERS | 2 + 4 files changed, 291 insertions(+) create mode 100644 Documentation/ABI/testing/sysfs-class-leds-dynamic create mode 100644 Documentation/leds/leds-class-dynamic.rst diff --git a/Documentation/ABI/testing/sysfs-class-leds-dynamic b/Documentation/ABI/testing/sysfs-class-leds-dynamic new file mode 100644 index 00000000000000..6d240081935dc0 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-class-leds-dynamic @@ -0,0 +1,125 @@ +What: /sys/class/leds//zone_type +Date: September 2026 +Contact: Marco Scardovi +Description: read + Driver-defined string describing the physical topology of + this lighting zone. Example values include "generic", + "keyboard", "keyboard_per_key", "matrix_2d", "segment_strip", + "logo", "lightbar", and "global". + +What: /sys/class/leds//led_count +Date: September 2026 +Contact: Marco Scardovi +Description: read + Total number of individual, addressable LEDs in this zone. + +What: /sys/class/leds//matrix_dimensions +Date: September 2026 +Contact: Marco Scardovi +Description: read + Width and height of 2D matrix layouts formatted as two + space-separated integers (" "). This attribute + is only visible when the driver publishes non-zero matrix + dimensions for the zone. + +What: /sys/class/leds//effect_index +Date: September 2026 +Contact: Marco Scardovi +Description: read + Space-separated list of animation effect names supported by + the hardware or driver (not numeric indices). Possible names + include: "off", "static", "breathing", "strobe", + "spectrum_cycle", "rainbow", "direct", and "custom". + +What: /sys/class/leds//effect +Date: September 2026 +Contact: Marco Scardovi +Description: read/write + Current active hardware effect. Reading outputs the effect name. + Writing an effect name from effect_index selects that + effect. Any active trigger is automatically detached upon + switching effects to prevent lock conflicts. + +What: /sys/class/leds//speed_range +Date: September 2026 +Contact: Marco Scardovi +Description: read + Minimum and maximum animation speed level accepted by speed. + Formatted as "0-". Only visible when the driver + supports adjustable speed. + +What: /sys/class/leds//speed +Date: September 2026 +Contact: Marco Scardovi +Description: read/write + Current animation speed level (integer within speed_range). + Only visible when the driver supports adjustable speed. + +What: /sys/class/leds//direction_index +Date: September 2026 +Contact: Marco Scardovi +Description: read + Space-separated list of supported animation directions. + Only visible when the driver supports directional animations. + +What: /sys/class/leds//direction +Date: September 2026 +Contact: Marco Scardovi +Description: read/write + Animation propagation direction. Outputs or accepts one of: + "left", "right", "up", "down", "clockwise", "counter_clockwise". + Only visible when the driver supports directional animations. + +What: /sys/class/leds//max_palette_entries +Date: September 2026 +Contact: Marco Scardovi +Description: read + Maximum number of palette entries accepted by effects_palette. + Only visible when the driver supports programmable palettes. + +What: /sys/class/leds//effects_palette +Date: September 2026 +Contact: Marco Scardovi +Description: read/write + Multi-color stacked palette used by multi-color animation effects. + Reading outputs space-separated 24-bit hex colors ("#RRGGBB"). + Writing accepts a space-separated sequence of hex triplets. + The number of entries must not exceed max_palette_entries. + +What: /sys/class/leds//power_states_index +Date: September 2026 +Contact: Marco Scardovi +Description: read + Space-separated list of system power states supported for + lighting persistence ("boot", "awake", "sleep", "shutdown"). + Only visible on devices supporting power state configuration. + +What: /sys/class/leds//power_states +Date: September 2026 +Contact: Marco Scardovi +Description: read/write + Space-separated list of currently enabled persistence power + states. Writing a space-separated list of state names + idempotently updates active states. + Only visible on devices supporting power state configuration. + +What: /sys/class/leds//direct_buffer +Date: September 2026 +Contact: Marco Scardovi +Description: write-only (binary) + Raw packed RGB stream (3 bytes per LED: R, G, B in sequence). + Writing to this node transmits direct per-key or matrix frame data + bypassing hardware effect generators. The write must start at + offset 0 and the buffer size must exactly match (led_count * 3) + bytes. + +What: /sys/class/leds//frame +Date: September 2026 +Contact: Marco Scardovi +Description: write-only (binary) + Raw binary sink for monochrome pixel displays (e.g. AniMe Matrix) + or segment lighting strips. The current ABI only accepts writes + starting at offset 0; each write is forwarded to the hardware + driver as a single frame payload. Drivers that expose this + node advertise a finite size; writes larger than that size + are rejected. diff --git a/Documentation/leds/index.rst b/Documentation/leds/index.rst index 23fa9ff7aaf4b0..39d93f1f9842d1 100644 --- a/Documentation/leds/index.rst +++ b/Documentation/leds/index.rst @@ -10,6 +10,7 @@ LEDs leds-class leds-class-flash leds-class-multicolor + leds-class-dynamic ledtrig-oneshot ledtrig-transient ledtrig-usbport diff --git a/Documentation/leds/leds-class-dynamic.rst b/Documentation/leds/leds-class-dynamic.rst new file mode 100644 index 00000000000000..b79acf6efd3d45 --- /dev/null +++ b/Documentation/leds/leds-class-dynamic.rst @@ -0,0 +1,163 @@ +.. SPDX-License-Identifier: GPL-2.0 + +====================================== +Dynamic Lighting LED class under Linux +====================================== + +Author: Marco Scardovi + +Description +=========== +The Dynamic Lighting LED class provides a standardized sysfs interface for +complex, addressable illumination hardware such as per-key RGB keyboard +matrices, 2D LED matrix displays, addressable segment strips, and chassis +lightbars. + +The Dynamic Lighting class wraps the standard Linux LED class framework, +providing a unified sysfs ABI for hardware effects, stacked palette +configuration, power state persistence, and high-throughput binary frame +streaming without requiring raw character device access from userspace. + +Directory Layout Example +======================== +The following examples use ```` as a placeholder for a Dynamic Lighting +LED class device name. + +.. code-block:: console + + # ls -l /sys/class/leds// + -rw-r--r-- 1 root root 4096 Sep 4 17:00 brightness + -r--r--r-- 1 root root 4096 Sep 4 17:00 max_brightness + -r--r--r-- 1 root root 4096 Sep 4 17:00 zone_type + -r--r--r-- 1 root root 4096 Sep 4 17:00 led_count + -r--r--r-- 1 root root 4096 Sep 4 17:00 effect_index + -rw-r--r-- 1 root root 4096 Sep 4 17:00 effect + -r--r--r-- 1 root root 4096 Sep 4 17:00 speed_range + -rw-r--r-- 1 root root 4096 Sep 4 17:00 speed + -r--r--r-- 1 root root 4096 Sep 4 17:00 direction_index + -rw-r--r-- 1 root root 4096 Sep 4 17:00 direction + -r--r--r-- 1 root root 4096 Sep 4 17:00 max_palette_entries + -rw-r--r-- 1 root root 4096 Sep 4 17:00 effects_palette + -r--r--r-- 1 root root 4096 Sep 4 17:00 power_states_index + -rw-r--r-- 1 root root 4096 Sep 4 17:00 power_states + --w------- 1 root root 504 Sep 4 17:00 direct_buffer + +Sysfs Attributes +================ + +``zone_type`` (read-only) + Driver-defined string describing the physical topology of the zone. + Example values include ``generic``, ``keyboard``, ``keyboard_per_key``, + ``matrix_2d``, ``segment_strip``, ``logo``, ``lightbar``, or ``global``. + +``led_count`` (read-only) + Total number of individually addressable LEDs in this zone. + +``matrix_dimensions`` (read-only) + Width and height for 2D matrix layouts formatted as `` ``. + Only visible when the driver publishes non-zero matrix dimensions. + +``effect_index`` (read-only) + Space-separated list of animation effect names supported by the hardware + (not numeric indices). Names include: ``off``, ``static``, ``breathing``, + ``strobe``, ``spectrum_cycle``, ``rainbow``, ``direct``, and ``custom``. + +``effect`` (read/write) + Currently active hardware animation effect. Writing a supported effect name + switches the mode. Any active trigger is automatically detached upon effect + change to eliminate lock conflicts. + +``speed_range`` (read-only) + Minimum and maximum effect animation speed accepted by ``speed``. Only visible + when the hardware supports adjustable speed. + +``speed`` (read/write) + Current effect animation speed (within ``speed_range``). Only visible when the + hardware supports adjustable speed. + +``direction_index`` (read-only) + Space-separated list of directions accepted by ``direction``. Only + visible when directional effects are supported. + +``direction`` (read/write) + Animation propagation direction: ``left``, ``right``, ``up``, ``down``, + ``clockwise``, or ``counter_clockwise``. Only visible when directional + effects are supported. + +``max_palette_entries`` (read-only) + Maximum number of palette entries accepted by ``effects_palette``. Only + visible when programmable palettes are supported. + +``effects_palette`` (read/write) + Space-separated list of 24-bit RGB hex colors (e.g. ``#ff0000 #00ff00``). + Up to ``max_palette_entries`` colors can be defined. + +``power_states_index`` (read-only) + List of platform power states supported for illumination persistence + (``boot``, ``awake``, ``sleep``, ``shutdown``). + +``power_states`` (read/write) + Currently active persistence states. Writing a space-separated list of + state names idempotently updates the active state bitmask. + +``direct_buffer`` (write-only, binary) + Raw binary sink for streaming per-key RGB frames. Each LED requires 3 bytes + in sequence (R, G, B). The write must start at offset 0 and the buffer size + must equal ``led_count * 3`` bytes. Enables efficient high-rate streaming + for visualizers and canvas sinks. + +``frame`` (write-only, binary) + Raw binary sink for monochrome display chunks (e.g. 2D pixel matrices) or + segmented lighting bars. The current ABI only accepts writes starting at + offset 0; each write is forwarded to the driver as one frame payload and + must not exceed the size published on the binary attribute (at most 65536 + bytes). + +Locking Hierarchy & Invariants +============================== +To prevent kernel deadlocks between LED triggers, sysfs handlers, and bus +transfers, the subsystem enforces the following lock order: + +1. Acquire outer mutex: ``mutex_lock(&cdev->led_access)``. +2. If the operation replaces trigger-driven output, disengage/remove the active + LED trigger via ``led_trigger_remove(cdev)``. +3. Acquire internal mutex: ``mutex_lock(&ldev->lock)``. +4. Validate inputs, update state, and dispatch driver callbacks. +5. Release internal mutex: ``mutex_unlock(&ldev->lock)``. +6. Release outer mutex: ``mutex_unlock(&cdev->led_access)``. + +Driver callbacks must not persist class-owned fields (``current_effect``, +``speed``, palette, ``active_power_states``) on failure; the core writes those +fields only after a successful callback. ``brightness_set_blocking`` is not +called with ``ldev->lock`` held and must take it if it mutates the same state. + +``frame`` writes are rejected when the payload is empty, not at offset 0, or +larger than the driver-advertised ``max_frame_size`` (capped at 65536 bytes). + +Examples +======== + +Setting breathing effect at medium speed: +----------------------------------------- +.. code-block:: console + + # echo "breathing" > /sys/class/leds//effect + # echo 1 > /sys/class/leds//speed + +Configuring a custom 3-color palette: +------------------------------------- +.. code-block:: console + + # echo "#ff0000 #00ff00 #0000ff" > /sys/class/leds//effects_palette + +Enabling illumination during boot and awake states: +--------------------------------------------------- +.. code-block:: console + + # echo "boot awake" > /sys/class/leds//power_states + +Streaming a direct RGB frame (for a 168-LED device, 504 bytes): +---------------------------------------------------------------- +.. code-block:: console + + # dd if=/dev/urandom of=/sys/class/leds//direct_buffer bs=504 count=1 diff --git a/MAINTAINERS b/MAINTAINERS index 2865d99cd4b136..016b7621721789 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14836,6 +14836,8 @@ M: Marco Scardovi M: Denis Benato L: linux-leds@vger.kernel.org S: Maintained +F: Documentation/ABI/testing/sysfs-class-leds-dynamic +F: Documentation/leds/leds-class-dynamic.rst F: drivers/leds/led-class-dynamic.c F: include/linux/led-dynamic-lighting.h From 12a4c112d8627cabea9d2c5d810d62e24608102c Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Fri, 4 Sep 2026 22:34:49 +0200 Subject: [PATCH 4/6] HID: asus: Add Dynamic Lighting support for Aura devices Add Dynamic Lighting class support to hid-asus for Aura-capable ROG keyboards and chassis lightbars. The driver discovers the keyboard layout and lightbar presence through the Aura probe report. When a lightbar is present, it registers: - "aura:global": global aggregate node operating directly on AURA_ZONE_ALL (0) with zone_type="global" - "aura:keyboard": independent keyboard controller operating on AURA_ZONE_KEY1..4 with zone_type="keyboard" or "keyboard_per_key" - "aura:lightbar": independent lightbar controller operating on native hardware zones AURA_ZONE_BAR_LEFT and AURA_ZONE_BAR_RIGHT with zone_type="lightbar" On models without a lightbar, it registers only "aura:keyboard". A runtime sysfs attribute "aura_mode" is exposed on all registered Aura nodes, supporting "auto", "unified", and "split" (rendered as "[auto] unified split"). Under unified mode (the default resolved on lightbar hardware), commands to "aura:global" broadcast to AURA_ZONE_ALL while operations on inactive split nodes return -EBUSY. In split mode, "aura:keyboard" and "aura:lightbar" are driven independently while operations on "aura:global" return -EBUSY. For controllers with direct streaming support, expose packed RGB frame writes through the class direct buffer interface. Drive independent keyboard and chassis lightbar lighting via the hardware effect engine (0xb3) across zones 1..4 for keyboard and native hardware zones 6..7 for lightbar, followed by the firmware latch commit sequence (0xb5 SET -> 0xb4 COMMIT -> 0xb5 SET), ensuring setting static or animated effects on one zone does not touch or interrupt the other. Unmask keyboard and lightbar hardware power states during probe and resume using report 0xbd without invalid zone commands. Drive hardware animation and static effects independently on each zone, committing each update via the firmware latch sequence (0xb5 SET -> 0xb4 COMMIT -> 0xb5 SET). Probe the run-mode capability report before advertising supported effects, fall back to a conservative built-in effect set when the capability query is unavailable, and validate returned report lengths before parsing them. This adds a common sysfs ABI for Aura lighting without regressing the existing asus::kbd_backlight brightness interface. Signed-off-by: Marco Scardovi --- .../testing/sysfs-class-led-driver-hid-asus | 9 + MAINTAINERS | 1 + drivers/hid/Kconfig | 1 + drivers/hid/hid-asus.c | 1923 +++++++++++++++-- drivers/hid/hid-ids.h | 1 + 5 files changed, 1758 insertions(+), 177 deletions(-) create mode 100644 Documentation/ABI/testing/sysfs-class-led-driver-hid-asus diff --git a/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus new file mode 100644 index 00000000000000..15b3b9fdbaf6b3 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus @@ -0,0 +1,9 @@ +What: /sys/class/leds//aura_mode +Date: September 2026 +Contact: Marco Scardovi +Description: read/write + ASUS Aura topology mode on hid-asus Dynamic Lighting nodes. + One of "auto", "unified", or "split". In unified mode only the + global node accepts effect/direct writes; in split mode the + keyboard and lightbar nodes are independent and the global + node returns -EBUSY. diff --git a/MAINTAINERS b/MAINTAINERS index 016b7621721789..939a9ed581a509 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4125,6 +4125,7 @@ M: Denis Benato L: platform-driver-x86@vger.kernel.org S: Maintained W: https://asus-linux.org/ +F: Documentation/ABI/testing/sysfs-class-led-driver-hid-asus F: drivers/platform/x86/asus*.c F: drivers/platform/x86/eeepc*.c diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index 22fa55eb17685f..a501427f75d2a4 100644 --- a/drivers/hid/Kconfig +++ b/drivers/hid/Kconfig @@ -189,6 +189,7 @@ config HID_ASUS depends on USB_HID depends on LEDS_CLASS depends on ASUS_WMI || ASUS_WMI=n + imply LEDS_CLASS_DYNAMIC select POWER_SUPPLY help Support for Asus notebook built-in keyboard and touchpad via i2c, and diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 43f7aaa7d06927..9486040b0d9bd1 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -30,6 +30,7 @@ #include /* For to_usb_interface for T100 touchpad intf check */ #include #include +#include #include "hid-ids.h" @@ -37,6 +38,8 @@ MODULE_AUTHOR("Yusuke Fujimaki "); MODULE_AUTHOR("Brendan McGrath "); MODULE_AUTHOR("Victor Vlasenko "); MODULE_AUTHOR("Frederik Wenigwieser "); +MODULE_AUTHOR("Marco Scardovi "); +MODULE_AUTHOR("Denis Benato "); MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #define T100_TPAD_INTF 2 @@ -51,6 +54,109 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #define FEATURE_KBD_LED_REPORT_ID1 0x5d #define FEATURE_KBD_LED_REPORT_ID2 0x5e +#define AURA_FEATURE_REPORT_SIZE 64 + +#define AURA_CMD_PROBE 0x05 +#define AURA_CMD_RUN_MODE 0x9e +#define AURA_CMD_SET_EFFECT 0xb3 +#define AURA_CMD_COMMIT 0xb4 +#define AURA_CMD_SET 0xb5 +#define AURA_CMD_ZONE_ENABLE 0xc0 +#define AURA_CMD_DIRECT 0xbc +#define AURA_CMD_POWER 0xbd + +#define AURA_ZONE_ACTIVATE_KEYBOARD 0x00 +#define AURA_ZONE_ACTIVATE_LIGHTBAR 0x01 + +#define AURA_POWER_CMD_ENABLE 0x01 + +/* Power Byte 0: Logo (even bits) & Keyboard (odd bits) */ +#define AURA_POWER_LOGO_BOOT BIT(0) +#define AURA_POWER_KBD_BOOT BIT(1) +#define AURA_POWER_LOGO_AWAKE BIT(2) +#define AURA_POWER_KBD_AWAKE BIT(3) +#define AURA_POWER_LOGO_SLEEP BIT(4) +#define AURA_POWER_KBD_SLEEP BIT(5) +#define AURA_POWER_LOGO_SHUTDOWN BIT(6) +#define AURA_POWER_KBD_SHUTDOWN BIT(7) +#define AURA_POWER_MASK_KBD_LOGO_ALL 0xff + +/* Power Byte 1: Chassis Lightbar */ +#define AURA_POWER_LB_AUX BIT(0) +#define AURA_POWER_LB_BOOT BIT(1) +#define AURA_POWER_LB_AWAKE BIT(2) +#define AURA_POWER_LB_SLEEP BIT(3) +#define AURA_POWER_LB_SHUTDOWN BIT(4) +#define AURA_POWER_MASK_LIGHTBAR_ALL 0x1f + +/* Power Byte 2: Lid Display Bezel / Lid segments */ +#define AURA_POWER_LID_BOOT BIT(0) +#define AURA_POWER_LID_AWAKE BIT(1) +#define AURA_POWER_LID_SLEEP BIT(2) +#define AURA_POWER_LID_SHUTDOWN BIT(3) +#define AURA_POWER_LID_PERSISTENCE 0xd0 +#define AURA_POWER_MASK_LID_ALL (AURA_POWER_LID_PERSISTENCE | 0x0f) + +/* Power Byte 3: Rear Glow */ +#define AURA_POWER_REAR_BOOT BIT(0) +#define AURA_POWER_REAR_AWAKE BIT(1) +#define AURA_POWER_REAR_SLEEP BIT(2) +#define AURA_POWER_REAR_SHUTDOWN BIT(3) +#define AURA_POWER_MASK_REAR_ALL 0x0f + +#define AURA_ZONE_ALL 0x00 +#define AURA_ZONE_KEY1 0x01 +#define AURA_ZONE_KEY2 0x02 +#define AURA_ZONE_KEY3 0x03 +#define AURA_ZONE_KEY4 0x04 +#define AURA_ZONE_LOGO 0x05 +#define AURA_ZONE_BAR_LEFT 0x06 +#define AURA_ZONE_BAR_RIGHT 0x07 +#define AURA_ZONE_KEYBOARD_CHANNEL 0x01 +#define AURA_ZONE_LIGHTBAR_CHANNEL 0x04 + +#define AURA_MODE_STATIC 0x00 +#define AURA_MODE_BREATHING 0x01 +#define AURA_MODE_SPECTRUM_CYCLE 0x02 +#define AURA_MODE_RAINBOW 0x03 +#define AURA_MODE_STARS 0x04 +#define AURA_MODE_RAIN 0x05 +#define AURA_MODE_REACTIVE 0x06 +#define AURA_MODE_LASER 0x07 +#define AURA_MODE_RIPPLE 0x08 +#define AURA_MODE_PULSE 0x0a +#define AURA_MODE_COMET 0x0b +#define AURA_MODE_FLASH 0x0c +#define AURA_MODE_STROBING AURA_MODE_PULSE + +#define AURA_SPEED_SLOW 0xe1 +#define AURA_SPEED_MED 0xeb +#define AURA_SPEED_FAST 0xf5 + +#define ROG_STRIX_LEDS_PER_PKT 16 +#define ROG_STRIX_PERKEY_FULL_PKTS 10 +#define ROG_STRIX_PERKEY_FINAL_PKT_LEDS 8 +#define ROG_STRIX_PERKEY_PACKETS (ROG_STRIX_PERKEY_FULL_PKTS + 1) +#define ROG_STRIX_DIRECT_LEDS \ + ((ROG_STRIX_PERKEY_FULL_PKTS * ROG_STRIX_LEDS_PER_PKT) + \ + ROG_STRIX_PERKEY_FINAL_PKT_LEDS) +#define ROG_STRIX_DIRECT_BUF_SIZE (ROG_STRIX_DIRECT_LEDS * 3) +#define ROG_STRIX_LIGHTBAR_LEDS 12 +#define ROG_STRIX_LIGHTBAR_BUF_SIZE (ROG_STRIX_LIGHTBAR_LEDS * 3) +#define ROG_STRIX_4ZONE_KBD_LEDS 4 +#define ROG_STRIX_4ZONE_KBD_BUF_SIZE (ROG_STRIX_4ZONE_KBD_LEDS * 3) +#define ROG_STRIX_4ZONE_LIGHTBAR_LEDS 6 +#define ROG_STRIX_4ZONE_LIGHTBAR_BUF_SIZE \ + (ROG_STRIX_4ZONE_LIGHTBAR_LEDS * 3) +#define ROG_STRIX_4ZONE_DIRECT_KBD_OFFSET 9 +#define ROG_STRIX_4ZONE_DIRECT_LB_OFFSET 27 +#define ROG_STRIX_PERKEY_DIRECT_PAYLOAD_OFFSET 9 + +#define AURA_DIRECT_FRAME_PERKEY 0x00 +#define AURA_DIRECT_FRAME_ZONED 0x01 +#define AURA_DIRECT_ROUTING_DEFAULT 0x01 +#define AURA_DIRECT_CHUNK_FLAG 0x01 + #define ROG_ALLY_REPORT_SIZE 64 #define ROG_ALLY_X_MIN_MCU 313 #define ROG_ALLY_MIN_MCU 319 @@ -154,8 +260,22 @@ struct asus_touchpad_info { int report_size; }; +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) +enum asus_aura_mode { + AURA_MODE_AUTO = 0, + AURA_MODE_UNIFIED, + AURA_MODE_SPLIT, + AURA_MODE_MAX, +}; +#endif + struct asus_drvdata { unsigned long quirks; + struct led_classdev slash_led; + bool has_slash_led; + u8 slash_mode; + u8 slash_brightness; + u8 slash_interval; struct hid_device *hdev; struct input_dev *input; struct input_dev *tp_kbd_input; @@ -170,6 +290,21 @@ struct asus_drvdata { unsigned long battery_next_query; struct asus_hid_listener listener; bool fn_lock; +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + struct mutex aura_lock; /* Serializes Aura HID reports and buffers */ + u8 *aura_buf; + struct led_classdev_dynamic dldev_global; + struct led_classdev_dynamic dldev_kbd; + struct led_classdev_dynamic dldev_lightbar; + bool has_dldev_global; + bool has_dldev_kbd; + bool has_dldev_lightbar; + bool is_strix_4zone; + bool has_lightbar; + enum asus_aura_mode aura_mode; + u8 kbd_direct_buf[ROG_STRIX_4ZONE_KBD_BUF_SIZE]; + u8 lb_direct_buf[ROG_STRIX_LIGHTBAR_BUF_SIZE]; +#endif }; static int asus_report_battery(struct asus_drvdata *, u8 *, int); @@ -576,16 +711,31 @@ static int asus_raw_event(struct hid_device *hdev, static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size) { + unsigned char report_type = HID_FEATURE_REPORT; u8 *dmabuf __free(kfree) = kmemdup(buf, buf_size, GFP_KERNEL); + int ret; + if (!dmabuf) return -ENOMEM; + if (buf[0] == FEATURE_KBD_LED_REPORT_ID1 || buf[0] == FEATURE_KBD_LED_REPORT_ID2) { + ret = hid_hw_output_report(hdev, dmabuf, buf_size); + if (ret >= 0) + return 0; + + report_type = HID_OUTPUT_REPORT; + } + /* * The report ID should be set from the incoming buffer due to LED and key * interfaces having different pages */ - return hid_hw_raw_request(hdev, buf[0], dmabuf, buf_size, HID_FEATURE_REPORT, - HID_REQ_SET_REPORT); + ret = hid_hw_raw_request(hdev, buf[0], dmabuf, buf_size, report_type, + HID_REQ_SET_REPORT); + if (ret < 0) + return ret; + + return 0; } static int asus_kbd_init(struct hid_device *hdev, u8 report_id) @@ -979,235 +1129,1619 @@ static int asus_kbd_register_leds(struct hid_device *hdev) return ret; } -/* - * [0] REPORT_ID (same value defined in report descriptor) - * [1] rest battery level. range [0..255] - * [2]..[7] Bluetooth hardware address (MAC address) - * [8] charging status - * = 0 : AC offline / discharging - * = 1 : AC online / charging - * = 2 : AC online / fully charged - */ -static int asus_parse_battery(struct asus_drvdata *drvdata, u8 *data, int size) +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + +static const char * const asus_aura_mode_strings[] = { + [AURA_MODE_AUTO] = "auto", + [AURA_MODE_UNIFIED] = "unified", + [AURA_MODE_SPLIT] = "split", +}; + +static int asus_aura_set_feature_unlocked(struct asus_drvdata *drvdata, + const u8 *buf, size_t buf_size) { - u8 sts; - u8 lvl; - int val; + int ret; - lvl = data[1]; - sts = data[8]; + if (buf_size > AURA_FEATURE_REPORT_SIZE) + return -EINVAL; - drvdata->battery_capacity = ((int)lvl * 100) / (int)BATTERY_LEVEL_MAX; + memcpy(drvdata->aura_buf, buf, buf_size); + if (buf_size < AURA_FEATURE_REPORT_SIZE) + memset(drvdata->aura_buf + buf_size, 0, + AURA_FEATURE_REPORT_SIZE - buf_size); - switch (sts) { - case BATTERY_STAT_CHARGING: - val = POWER_SUPPLY_STATUS_CHARGING; - break; - case BATTERY_STAT_FULL: - val = POWER_SUPPLY_STATUS_FULL; - break; - case BATTERY_STAT_DISCONNECT: - default: - val = POWER_SUPPLY_STATUS_DISCHARGING; - break; - } - drvdata->battery_stat = val; + /* + * Try Output Report first matching Armoury Crate / asus_kbd_set_report. + * If the device lacks an interrupt OUT endpoint, fall back to + * hid_hw_raw_request() with HID_OUTPUT_REPORT, and finally to + * HID_FEATURE_REPORT. + */ + ret = hid_hw_output_report(drvdata->hdev, drvdata->aura_buf, + AURA_FEATURE_REPORT_SIZE); + if (ret >= 0) + return 0; + + ret = hid_hw_raw_request(drvdata->hdev, drvdata->aura_buf[0], + drvdata->aura_buf, AURA_FEATURE_REPORT_SIZE, + HID_OUTPUT_REPORT, HID_REQ_SET_REPORT); + if (ret >= 0) + return 0; + + ret = hid_hw_raw_request(drvdata->hdev, drvdata->aura_buf[0], + drvdata->aura_buf, AURA_FEATURE_REPORT_SIZE, + HID_FEATURE_REPORT, HID_REQ_SET_REPORT); + if (ret < 0) + return ret; return 0; } -static int asus_report_battery(struct asus_drvdata *drvdata, u8 *data, int size) +static int asus_aura_set_feature(struct asus_drvdata *drvdata, + const u8 *buf, size_t buf_size) { - /* notify only the autonomous event by device */ - if ((drvdata->battery_in_query == false) && - (size == BATTERY_REPORT_SIZE)) - power_supply_changed(drvdata->battery); + guard(mutex)(&drvdata->aura_lock); - return 0; + return asus_aura_set_feature_unlocked(drvdata, buf, buf_size); } -static int asus_battery_query(struct asus_drvdata *drvdata) +static int asus_aura_get_feature(struct asus_drvdata *drvdata, + u8 *buf, size_t buf_size) { - u8 *buf; - int ret = 0; + int ret; - buf = kmalloc(BATTERY_REPORT_SIZE, GFP_KERNEL); - if (!buf) - return -ENOMEM; + if (buf_size > AURA_FEATURE_REPORT_SIZE) + return -EINVAL; - drvdata->battery_in_query = true; - ret = hid_hw_raw_request(drvdata->hdev, BATTERY_REPORT_ID, - buf, BATTERY_REPORT_SIZE, - HID_INPUT_REPORT, HID_REQ_GET_REPORT); - drvdata->battery_in_query = false; - if (ret == BATTERY_REPORT_SIZE) - ret = asus_parse_battery(drvdata, buf, BATTERY_REPORT_SIZE); - else - ret = -ENODATA; + guard(mutex)(&drvdata->aura_lock); - kfree(buf); + memset(drvdata->aura_buf, 0, AURA_FEATURE_REPORT_SIZE); + drvdata->aura_buf[0] = buf[0]; + + ret = hid_hw_raw_request(drvdata->hdev, buf[0], drvdata->aura_buf, + AURA_FEATURE_REPORT_SIZE, + HID_FEATURE_REPORT, HID_REQ_GET_REPORT); + if (ret < 0) + return ret; + memcpy(buf, drvdata->aura_buf, min_t(size_t, buf_size, ret)); return ret; } -static enum power_supply_property asus_battery_props[] = { - POWER_SUPPLY_PROP_STATUS, - POWER_SUPPLY_PROP_PRESENT, - POWER_SUPPLY_PROP_CAPACITY, - POWER_SUPPLY_PROP_SCOPE, - POWER_SUPPLY_PROP_MODEL_NAME, -}; +static int asus_aura_commit(struct asus_drvdata *drvdata) +{ + u8 buf_set[AURA_FEATURE_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, + AURA_CMD_SET, + }; + u8 buf_apply[AURA_FEATURE_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, + AURA_CMD_COMMIT, + }; + int ret; -#define QUERY_MIN_INTERVAL (60 * HZ) /* 60[sec] */ + /* + * Apply staged 0xb3 effect programming: + * First send 0xb5 (AURA_CMD_SET) to latch parameters, + * then send 0xb4 (AURA_CMD_COMMIT) to apply them to hardware, + * then send 0xb5 (AURA_CMD_SET) to settle as captured in firmware traces. + */ + ret = asus_aura_set_feature(drvdata, buf_set, sizeof(buf_set)); + if (ret < 0) + return ret; -static int asus_battery_get_property(struct power_supply *psy, - enum power_supply_property psp, - union power_supply_propval *val) + ret = asus_aura_set_feature(drvdata, buf_apply, sizeof(buf_apply)); + if (ret < 0) + return ret; + + return asus_aura_set_feature(drvdata, buf_set, sizeof(buf_set)); +} + +static int asus_aura_query_run_mode(struct asus_drvdata *drvdata, u8 selector, + u8 *buf, size_t buf_size) { - struct asus_drvdata *drvdata = power_supply_get_drvdata(psy); - int ret = 0; + u8 req[AURA_FEATURE_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, + AURA_CMD_RUN_MODE, + 0x01, + selector, + }; + int ret; - switch (psp) { - case POWER_SUPPLY_PROP_STATUS: - case POWER_SUPPLY_PROP_CAPACITY: - if (time_before(drvdata->battery_next_query, jiffies)) { - drvdata->battery_next_query = - jiffies + QUERY_MIN_INTERVAL; - ret = asus_battery_query(drvdata); - if (ret) - return ret; - } - if (psp == POWER_SUPPLY_PROP_STATUS) - val->intval = drvdata->battery_stat; - else - val->intval = drvdata->battery_capacity; - break; - case POWER_SUPPLY_PROP_PRESENT: - val->intval = 1; - break; - case POWER_SUPPLY_PROP_SCOPE: - val->intval = POWER_SUPPLY_SCOPE_DEVICE; - break; - case POWER_SUPPLY_PROP_MODEL_NAME: - val->strval = drvdata->hdev->name; - break; - default: - ret = -EINVAL; - break; - } + ret = asus_aura_set_feature(drvdata, req, sizeof(req)); + if (ret < 0) + return ret; + + memset(buf, 0, buf_size); + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + + ret = asus_aura_get_feature(drvdata, buf, buf_size); + if (ret < 0) + return ret; + + if (ret < 5 || buf[1] != AURA_CMD_RUN_MODE || buf[2] != 0x01 || + buf[3] != selector || buf[4] != 0x01) + return -ENODATA; return ret; } -static int asus_battery_probe(struct hid_device *hdev) +static int asus_aura_get_effect_mask(struct asus_drvdata *drvdata, u8 effect_mask[2]) { - struct asus_drvdata *drvdata = hid_get_drvdata(hdev); - struct power_supply_config pscfg = { .drv_data = drvdata }; - int ret = 0; + u8 buf[AURA_FEATURE_REPORT_SIZE]; + int ret; - drvdata->battery_capacity = 0; - drvdata->battery_stat = POWER_SUPPLY_STATUS_UNKNOWN; - drvdata->battery_in_query = false; + ret = asus_aura_query_run_mode(drvdata, 0x20, buf, sizeof(buf)); + if (ret >= 22) + goto found; - drvdata->battery_desc.properties = asus_battery_props; - drvdata->battery_desc.num_properties = ARRAY_SIZE(asus_battery_props); - drvdata->battery_desc.get_property = asus_battery_get_property; - drvdata->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY; - drvdata->battery_desc.use_for_apm = 0; - drvdata->battery_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL, - "asus-keyboard-%s-battery", - strlen(hdev->uniq) ? - hdev->uniq : dev_name(&hdev->dev)); - if (!drvdata->battery_desc.name) - return -ENOMEM; + ret = asus_aura_query_run_mode(drvdata, 0x15, buf, sizeof(buf)); + if (ret < 0) + return ret; + if (ret < 22) + return -ENODATA; - drvdata->battery_next_query = jiffies; +found: + effect_mask[0] = buf[20]; + effect_mask[1] = buf[21]; - drvdata->battery = devm_power_supply_register(&hdev->dev, - &(drvdata->battery_desc), &pscfg); - if (IS_ERR(drvdata->battery)) { - ret = PTR_ERR(drvdata->battery); - drvdata->battery = NULL; - hid_err(hdev, "Unable to register battery device\n"); - return ret; - } + return 0; +} - power_supply_powers(drvdata->battery, &hdev->dev); +static unsigned int asus_aura_fallback_supported_effects(bool direct_capable) +{ + unsigned int supported_effects = BIT(DL_EFFECT_OFF) | + BIT(DL_EFFECT_STATIC) | + BIT(DL_EFFECT_BREATHING) | + BIT(DL_EFFECT_STROBE) | + BIT(DL_EFFECT_SPECTRUM_CYCLE) | + BIT(DL_EFFECT_RAINBOW); - return ret; + if (direct_capable) + supported_effects |= BIT(DL_EFFECT_DIRECT); + + return supported_effects; } -static int asus_input_configured(struct hid_device *hdev, struct hid_input *hi) +static unsigned int +asus_aura_supported_effects_from_mask(const u8 effect_mask[2], bool direct_capable) { - struct input_dev *input = hi->input; - struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + unsigned int supported_effects = BIT(DL_EFFECT_OFF); + + if (effect_mask[0] & 0x01) + supported_effects |= BIT(DL_EFFECT_STATIC); + if (effect_mask[0] & 0x02) + supported_effects |= BIT(DL_EFFECT_BREATHING); + if (effect_mask[0] & 0x04) + supported_effects |= BIT(DL_EFFECT_SPECTRUM_CYCLE); + if (effect_mask[0] & 0x08) + supported_effects |= BIT(DL_EFFECT_RAINBOW); + if (effect_mask[1] & (BIT(1) | BIT(2))) + supported_effects |= BIT(DL_EFFECT_STROBE); + if (direct_capable) + supported_effects |= BIT(DL_EFFECT_DIRECT); + + return supported_effects; +} - /* T100CHI uses MULTI_INPUT, bind the touchpad to the mouse hid_input */ - if (drvdata->quirks & QUIRK_T100CHI && - hi->report->id != T100CHI_MOUSE_REPORT_ID) - return 0; +static int asus_aura_activate_zone_unlocked(struct asus_drvdata *drvdata, u8 zone) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE] = { 0 }; - /* Handle MULTI_INPUT on E1239T mouse/touchpad USB interface */ - if (drvdata->tp && (drvdata->quirks & QUIRK_MEDION_E1239T)) { - switch (hi->report->id) { - case E1239T_TP_TOGGLE_REPORT_ID: - input_set_capability(input, EV_KEY, KEY_F21); - input->name = "Asus Touchpad Keys"; - drvdata->tp_kbd_input = input; - return 0; - case INPUT_REPORT_ID: - break; /* Touchpad report, handled below */ - default: - return 0; /* Ignore other reports */ - } - } + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + buf[1] = AURA_CMD_ZONE_ENABLE; + buf[2] = zone; + buf[3] = 0x01; + buf[4] = 0x01; - if (drvdata->tp) { - int ret; + return asus_aura_set_feature_unlocked(drvdata, buf, sizeof(buf)); +} - input_set_abs_params(input, ABS_MT_POSITION_X, 0, - drvdata->tp->max_x, 0, 0); - input_set_abs_params(input, ABS_MT_POSITION_Y, 0, - drvdata->tp->max_y, 0, 0); - input_abs_set_res(input, ABS_MT_POSITION_X, drvdata->tp->res_x); - input_abs_set_res(input, ABS_MT_POSITION_Y, drvdata->tp->res_y); +static int asus_aura_activate_zone(struct asus_drvdata *drvdata, u8 zone) +{ + guard(mutex)(&drvdata->aura_lock); - if (drvdata->tp->contact_size >= 5) { - input_set_abs_params(input, ABS_TOOL_WIDTH, 0, - MAX_TOUCH_MAJOR, 0, 0); - input_set_abs_params(input, ABS_MT_TOUCH_MAJOR, 0, - MAX_TOUCH_MAJOR, 0, 0); - input_set_abs_params(input, ABS_MT_PRESSURE, 0, - MAX_PRESSURE, 0, 0); - } + return asus_aura_activate_zone_unlocked(drvdata, zone); +} - __set_bit(BTN_LEFT, input->keybit); - __set_bit(INPUT_PROP_BUTTONPAD, input->propbit); +static int asus_aura_wake_all_zones(struct asus_drvdata *drvdata) +{ + u8 buf_pwr[AURA_FEATURE_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, + AURA_CMD_POWER, + AURA_POWER_CMD_ENABLE, + AURA_POWER_MASK_KBD_LOGO_ALL, + AURA_POWER_MASK_LIGHTBAR_ALL, + AURA_POWER_MASK_LID_ALL, + AURA_POWER_MASK_REAR_ALL, + 0x00, + }; + int ret; - ret = input_mt_init_slots(input, drvdata->tp->max_contacts, - INPUT_MT_POINTER); + /* Unmute power gating across keyboard, lightbar, logo, lid, and rear-glow */ + ret = asus_aura_set_feature(drvdata, buf_pwr, sizeof(buf_pwr)); + if (ret < 0) + return ret; - if (ret) { - hid_err(hdev, "Asus input mt init slots failed: %d\n", ret); + /* Activate keyboard zone */ + ret = asus_aura_activate_zone(drvdata, AURA_ZONE_ACTIVATE_KEYBOARD); + if (ret < 0) + return ret; + + /* Activate lightbar zone */ + return asus_aura_activate_zone(drvdata, AURA_ZONE_ACTIVATE_LIGHTBAR); +} + +static int asus_aura_write_zone_effect(struct asus_drvdata *drvdata, u8 zone, + u8 aura_mode, u8 r, u8 g, u8 b, + u8 speed, u8 direction, + u8 r2, u8 g2, u8 b2, + bool commit) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE] = { 0 }; + int ret; + + if (zone == AURA_ZONE_BAR_LEFT || zone == AURA_ZONE_BAR_RIGHT) { + ret = asus_aura_activate_zone(drvdata, AURA_ZONE_ACTIVATE_LIGHTBAR); + if (ret < 0) return ret; - } } - drvdata->input = input; + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + buf[1] = AURA_CMD_SET_EFFECT; + buf[2] = zone; + buf[3] = aura_mode; + buf[4] = r; + buf[5] = g; + buf[6] = b; + buf[7] = speed; + buf[8] = direction; + buf[9] = 0x00; + buf[10] = r2; + buf[11] = g2; + buf[12] = b2; + + ret = asus_aura_set_feature(drvdata, buf, sizeof(buf)); + if (ret < 0) + return ret; - if ((drvdata->quirks & QUIRK_HID_FN_LOCK) && - (asus_kbd_fn_lock_set(drvdata, true))) - hid_warn(hdev, "Error while setting FN lock to ON\n"); + if (commit) + return asus_aura_commit(drvdata); return 0; } -#define asus_map_key_clear(c) hid_map_usage_clear(hi, usage, bit, \ - max, EV_KEY, (c)) -static int asus_input_mapping(struct hid_device *hdev, - struct hid_input *hi, struct hid_field *field, - struct hid_usage *usage, unsigned long **bit, - int *max) +static int asus_aura_strix_write_direct(struct asus_drvdata *drvdata, + const u8 *buffer, size_t size) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE]; + unsigned int i; + int ret; + + guard(mutex)(&drvdata->aura_lock); + + if (drvdata->is_strix_4zone) { + unsigned int leds = min_t(size_t, size / 3, + ROG_STRIX_4ZONE_KBD_LEDS); + + if (buffer != drvdata->kbd_direct_buf) + memcpy(drvdata->kbd_direct_buf, buffer, leds * 3); + + memset(buf, 0, sizeof(buf)); + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + buf[1] = AURA_CMD_DIRECT; + buf[2] = AURA_DIRECT_FRAME_ZONED; + buf[3] = AURA_DIRECT_ROUTING_DEFAULT; + buf[4] = AURA_ZONE_LIGHTBAR_CHANNEL; + + memcpy(&buf[ROG_STRIX_4ZONE_DIRECT_KBD_OFFSET], + drvdata->kbd_direct_buf, + sizeof(drvdata->kbd_direct_buf)); + memcpy(&buf[ROG_STRIX_4ZONE_DIRECT_LB_OFFSET], + drvdata->lb_direct_buf, + ROG_STRIX_4ZONE_LIGHTBAR_BUF_SIZE); + + return asus_aura_set_feature_unlocked(drvdata, buf, sizeof(buf)); + } + + /* + * Stream ROG Strix per-key matrix in 16-LED chunks using + * Aura HID Feature Reports with opcode 0xbc: + * [0] = Report ID (0x5d) + * [1] = Direct frame command (0xbc) + * [2..5] = Routing header (0x00, 0x01, 0x01, 0x01) + * [6] = Start LED index (0, 16, 32, ..., 160) + * [7] = Number of LEDs in chunk (16 for chunks 0..9, 8 for chunk 10) + * [8] = Reserved / 0x00 + * [9..] = RGB payload (3 bytes per LED) + * + * Total LEDs: 168 (11 packets). Strictly terminate at packet 10; + * sending a 12th packet triggers a firmware defect that shuts off the + * rear and front lightbars. No 0xb4 commit command is issued for raw + * direct frames to avoid stepping hardware animation registers. + */ + for (i = 0; i < ROG_STRIX_DIRECT_LEDS; i += ROG_STRIX_LEDS_PER_PKT) { + unsigned int leds = min_t(unsigned int, ROG_STRIX_DIRECT_LEDS - i, + ROG_STRIX_LEDS_PER_PKT); + size_t payload_len = leds * 3; + + memset(buf, 0, sizeof(buf)); + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + buf[1] = AURA_CMD_DIRECT; + buf[2] = AURA_DIRECT_FRAME_PERKEY; + buf[3] = AURA_DIRECT_ROUTING_DEFAULT; + buf[4] = AURA_ZONE_KEYBOARD_CHANNEL; + buf[5] = AURA_DIRECT_CHUNK_FLAG; + buf[6] = (u8)i; + buf[7] = (u8)leds; + buf[8] = 0x00; + memcpy(&buf[ROG_STRIX_PERKEY_DIRECT_PAYLOAD_OFFSET], + buffer + (i * 3), payload_len); + + ret = asus_aura_set_feature_unlocked(drvdata, buf, sizeof(buf)); + if (ret < 0) + return ret; + } + + return 0; +} + +static bool asus_aura_is_global(struct asus_drvdata *drvdata, + struct led_classdev_dynamic *ldev) +{ + return ldev == &drvdata->dldev_global; +} + +static bool asus_aura_is_lightbar(struct asus_drvdata *drvdata, + struct led_classdev_dynamic *ldev) +{ + return ldev == &drvdata->dldev_lightbar; +} + +static enum asus_aura_mode asus_aura_effective_mode(struct asus_drvdata *drvdata) +{ + enum asus_aura_mode mode = READ_ONCE(drvdata->aura_mode); + + if (mode == AURA_MODE_AUTO) + return drvdata->has_lightbar ? AURA_MODE_UNIFIED : AURA_MODE_SPLIT; + return mode; +} + +static int asus_aura_check_node_active(struct asus_drvdata *drvdata, + struct led_classdev_dynamic *ldev) +{ + enum asus_aura_mode mode = asus_aura_effective_mode(drvdata); + + if (mode == AURA_MODE_UNIFIED) { + if (!asus_aura_is_global(drvdata, ldev)) + return -EBUSY; + } else if (mode == AURA_MODE_SPLIT) { + if (asus_aura_is_global(drvdata, ldev)) + return -EBUSY; + } + + return 0; +} + +static ssize_t aura_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led = dev_get_drvdata(dev); + struct led_classdev_dynamic *dldev = lcdev_to_dldev(led); + struct asus_drvdata *drvdata = dldev->driver_data; + int len = 0; + int i; + + for (i = 0; i < AURA_MODE_MAX; i++) { + if (drvdata->aura_mode == i) + len += sysfs_emit_at(buf, len, "[%s] ", asus_aura_mode_strings[i]); + else + len += sysfs_emit_at(buf, len, "%s ", asus_aura_mode_strings[i]); + } + if (len > 0) + buf[len - 1] = '\n'; + + return len; +} + +static ssize_t aura_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led = dev_get_drvdata(dev); + struct led_classdev_dynamic *dldev = lcdev_to_dldev(led); + struct asus_drvdata *drvdata = dldev->driver_data; + int val; + + val = sysfs_match_string(asus_aura_mode_strings, buf); + if (val < 0) { + u8 num; + + if (kstrtou8(buf, 0, &num) < 0 || num >= AURA_MODE_MAX) + return -EINVAL; + val = num; + } + + guard(mutex)(&drvdata->aura_lock); + WRITE_ONCE(drvdata->aura_mode, val); + + return count; +} +static DEVICE_ATTR_RW(aura_mode); + +static struct attribute *asus_aura_attrs[] = { + &dev_attr_aura_mode.attr, + NULL, +}; + +static const struct attribute_group asus_aura_group = { + .attrs = asus_aura_attrs, +}; + +static const struct attribute_group *asus_aura_groups[] = { + &asus_aura_group, + NULL, +}; + +static int asus_aura_strix_set_direct(struct led_classdev_dynamic *ldev, + const u8 *buffer, size_t size) +{ + struct asus_drvdata *drvdata = ldev->driver_data; + int ret; + + ret = asus_aura_check_node_active(drvdata, ldev); + if (ret < 0) + return ret; + + if (size != ldev->led_count * 3) + return -EINVAL; + + if (ldev->current_effect != DL_EFFECT_DIRECT) { + ret = asus_aura_activate_zone(drvdata, AURA_ZONE_ACTIVATE_KEYBOARD); + if (ret < 0) + return ret; + } + + ldev->current_effect = DL_EFFECT_DIRECT; + return asus_aura_strix_write_direct(drvdata, buffer, size); +} + +static int asus_aura_lightbar_write_packet(struct asus_drvdata *drvdata, + const u8 *buffer, size_t size) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE]; + size_t payload_len; + + guard(mutex)(&drvdata->aura_lock); + + if (drvdata->is_strix_4zone) { + unsigned int leds = min_t(size_t, size / 3, + ROG_STRIX_4ZONE_LIGHTBAR_LEDS); + + if (buffer != drvdata->lb_direct_buf) + memcpy(drvdata->lb_direct_buf, buffer, leds * 3); + + memset(buf, 0, sizeof(buf)); + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + buf[1] = AURA_CMD_DIRECT; + buf[2] = AURA_DIRECT_FRAME_ZONED; + buf[3] = AURA_DIRECT_ROUTING_DEFAULT; + buf[4] = AURA_ZONE_LIGHTBAR_CHANNEL; + + memcpy(&buf[ROG_STRIX_4ZONE_DIRECT_KBD_OFFSET], + drvdata->kbd_direct_buf, + sizeof(drvdata->kbd_direct_buf)); + memcpy(&buf[ROG_STRIX_4ZONE_DIRECT_LB_OFFSET], + drvdata->lb_direct_buf, + ROG_STRIX_4ZONE_LIGHTBAR_BUF_SIZE); + + return asus_aura_set_feature_unlocked(drvdata, buf, sizeof(buf)); + } + + payload_len = min_t(size_t, size, ROG_STRIX_LIGHTBAR_BUF_SIZE); + + /* + * Per-Key models use buf[2] = 0x00 (chunked packet), so the MCU + * expects valid chunk headers in bytes 5-7. Without them it reads + * "0 LEDs to update" and silently drops the packet. + */ + memset(buf, 0, sizeof(buf)); + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + buf[1] = AURA_CMD_DIRECT; + buf[2] = AURA_DIRECT_FRAME_PERKEY; + buf[3] = AURA_DIRECT_ROUTING_DEFAULT; + buf[4] = AURA_ZONE_LIGHTBAR_CHANNEL; + buf[5] = AURA_DIRECT_CHUNK_FLAG; + buf[6] = 0x00; /* start LED index */ + buf[7] = (u8)(payload_len / 3); /* number of LEDs in this packet */ + + if (buffer != drvdata->lb_direct_buf) + memcpy(drvdata->lb_direct_buf, buffer, payload_len); + memcpy(&buf[ROG_STRIX_PERKEY_DIRECT_PAYLOAD_OFFSET], + drvdata->lb_direct_buf, payload_len); + + return asus_aura_set_feature_unlocked(drvdata, buf, sizeof(buf)); +} + +static int asus_aura_lightbar_set_direct(struct led_classdev_dynamic *ldev, + const u8 *buffer, size_t size) +{ + struct asus_drvdata *drvdata = ldev->driver_data; + int ret; + + ret = asus_aura_check_node_active(drvdata, ldev); + if (ret < 0) + return ret; + + if (size != ldev->led_count * 3) + return -EINVAL; + + if (ldev->current_effect != DL_EFFECT_DIRECT) { + ret = asus_aura_activate_zone(drvdata, AURA_ZONE_ACTIVATE_LIGHTBAR); + if (ret < 0) + return ret; + } + + ldev->current_effect = DL_EFFECT_DIRECT; + return asus_aura_lightbar_write_packet(drvdata, buffer, size); +} + +static int asus_aura_apply_effect(struct led_classdev_dynamic *ldev, + enum dl_effect_mode mode, + enum led_brightness brightness) +{ + struct asus_drvdata *drvdata = ldev->driver_data; + u8 aura_mode; + u8 speed; + u8 direction = 0; + u8 r = 0, g = 0, b = 0; + u8 r2 = 0, g2 = 0, b2 = 0; + int ret; + + ret = asus_aura_check_node_active(drvdata, ldev); + if (ret < 0) + return ret; + + if (mode != DL_EFFECT_OFF && ldev->num_palette_entries > 0 && brightness > LED_OFF) { + r = (u8)(((unsigned int)ldev->palette[0].r * brightness) / 255); + g = (u8)(((unsigned int)ldev->palette[0].g * brightness) / 255); + b = (u8)(((unsigned int)ldev->palette[0].b * brightness) / 255); + if (ldev->num_palette_entries > 1) { + r2 = (u8)(((unsigned int)ldev->palette[1].r * brightness) / 255); + g2 = (u8)(((unsigned int)ldev->palette[1].g * brightness) / 255); + b2 = (u8)(((unsigned int)ldev->palette[1].b * brightness) / 255); + } + } + + if (mode == DL_EFFECT_DIRECT) + return 0; + + switch (ldev->speed) { + case 0: + speed = AURA_SPEED_SLOW; + break; + case 2: + speed = AURA_SPEED_FAST; + break; + case 1: + default: + speed = AURA_SPEED_MED; + break; + } + + if (brightness == LED_OFF || mode == DL_EFFECT_OFF) { + aura_mode = AURA_MODE_STATIC; + r = 0; + g = 0; + b = 0; + r2 = 0; + g2 = 0; + b2 = 0; + } else { + switch (mode) { + case DL_EFFECT_STATIC: + aura_mode = AURA_MODE_STATIC; + break; + case DL_EFFECT_BREATHING: + aura_mode = AURA_MODE_BREATHING; + break; + case DL_EFFECT_STROBE: + aura_mode = AURA_MODE_STROBING; + break; + case DL_EFFECT_SPECTRUM_CYCLE: + aura_mode = AURA_MODE_SPECTRUM_CYCLE; + break; + case DL_EFFECT_RAINBOW: + aura_mode = AURA_MODE_RAINBOW; + break; + default: + return -EINVAL; + } + } + + if (ldev->direction == DL_DIRECTION_LEFT) + direction = 1; + else if (ldev->direction == DL_DIRECTION_RIGHT) + direction = 0; + else if (ldev->direction == DL_DIRECTION_UP) + direction = 2; + else if (ldev->direction == DL_DIRECTION_DOWN) + direction = 3; + + if (asus_aura_is_global(drvdata, ldev)) { + ret = asus_aura_write_zone_effect(drvdata, AURA_ZONE_ALL, aura_mode, + r, g, b, speed, direction, + r2, g2, b2, true); + if (ret < 0) + return ret; + + return 0; + } + + if (asus_aura_is_lightbar(drvdata, ldev)) { + ret = asus_aura_write_zone_effect(drvdata, AURA_ZONE_BAR_LEFT, + aura_mode, r, g, b, speed, + direction, r2, g2, b2, false); + if (ret < 0) + return ret; + + ret = asus_aura_write_zone_effect(drvdata, AURA_ZONE_BAR_RIGHT, + aura_mode, r, g, b, speed, + direction, r2, g2, b2, false); + if (ret < 0) + return ret; + + return asus_aura_commit(drvdata); + } + + if (drvdata->is_strix_4zone) { + unsigned int z; + + for (z = AURA_ZONE_KEY1; z <= AURA_ZONE_KEY4; z++) { + ret = asus_aura_write_zone_effect(drvdata, (u8)z, aura_mode, + r, g, b, speed, direction, + r2, g2, b2, false); + if (ret < 0) + return ret; + } + + return asus_aura_commit(drvdata); + } + + return asus_aura_write_zone_effect(drvdata, AURA_ZONE_ALL, aura_mode, r, g, b, + speed, direction, r2, g2, b2, true); +} + +static int asus_aura_set_effect(struct led_classdev_dynamic *ldev, + enum dl_effect_mode mode) +{ + return asus_aura_apply_effect(ldev, mode, ldev->cdev.brightness); +} + +static int asus_aura_set_speed(struct led_classdev_dynamic *ldev, + unsigned int speed) +{ + unsigned int old_speed = ldev->speed; + int ret; + + ldev->speed = speed; + ret = asus_aura_apply_effect(ldev, ldev->current_effect, ldev->cdev.brightness); + ldev->speed = old_speed; + return ret; +} + +static int asus_aura_set_direction(struct led_classdev_dynamic *ldev, + enum dl_direction direction) +{ + enum dl_direction old_dir = ldev->direction; + int ret; + + ldev->direction = direction; + ret = asus_aura_apply_effect(ldev, ldev->current_effect, ldev->cdev.brightness); + ldev->direction = old_dir; + return ret; +} + +static int asus_aura_set_palette(struct led_classdev_dynamic *ldev, + const struct dl_rgb *palette, + unsigned int num_entries) +{ + struct dl_rgb saved[2]; + unsigned int old_n = ldev->num_palette_entries; + unsigned int copy_n; + int ret; + + if (!palette || !num_entries || num_entries > ldev->max_palette_entries) + return -EINVAL; + + copy_n = min_t(unsigned int, old_n, ARRAY_SIZE(saved)); + if (copy_n) + memcpy(saved, ldev->palette, copy_n * sizeof(*saved)); + + memcpy(ldev->palette, palette, num_entries * sizeof(*palette)); + ldev->num_palette_entries = num_entries; + + ret = asus_aura_apply_effect(ldev, ldev->current_effect, ldev->cdev.brightness); + + memcpy(ldev->palette, saved, copy_n * sizeof(*saved)); + ldev->num_palette_entries = old_n; + return ret; +} + +static enum dl_effect_mode asus_aura_resume_effect(struct led_classdev_dynamic *ldev) +{ + static const enum dl_effect_mode preferred_modes[] = { + DL_EFFECT_STATIC, + DL_EFFECT_BREATHING, + DL_EFFECT_STROBE, + DL_EFFECT_SPECTRUM_CYCLE, + DL_EFFECT_RAINBOW, + }; + unsigned int i; + + if (ldev->current_effect != DL_EFFECT_OFF && + (ldev->supported_effects & BIT(ldev->current_effect))) + return ldev->current_effect; + + for (i = 0; i < ARRAY_SIZE(preferred_modes); i++) { + if (ldev->supported_effects & BIT(preferred_modes[i])) + return preferred_modes[i]; + } + + return DL_EFFECT_OFF; +} + +static int asus_aura_brightness_set_blocking(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + struct asus_drvdata *drvdata = ldev->driver_data; + enum dl_effect_mode mode; + int ret; + + guard(mutex)(&ldev->lock); + + ret = asus_aura_check_node_active(drvdata, ldev); + if (ret < 0) + return ret; + + if (brightness == LED_OFF) + return asus_aura_apply_effect(ldev, DL_EFFECT_OFF, LED_OFF); + + ret = asus_aura_wake_all_zones(drvdata); + if (ret < 0) + hid_warn(drvdata->hdev, "Failed to wake Aura hardware zones: %d\n", ret); + + mode = asus_aura_resume_effect(ldev); + ret = asus_aura_apply_effect(ldev, mode, brightness); + if (ret < 0) + return ret; + + ldev->current_effect = mode; + return 0; +} + +static const struct led_dynamic_ops asus_aura_global_ops = { + .set_effect = asus_aura_set_effect, + .set_speed = asus_aura_set_speed, + .set_direction = asus_aura_set_direction, + .set_palette = asus_aura_set_palette, + .direct_write = asus_aura_strix_set_direct, +}; + +static const struct led_dynamic_ops asus_aura_kbd_ops = { + .set_effect = asus_aura_set_effect, + .set_speed = asus_aura_set_speed, + .set_direction = asus_aura_set_direction, + .set_palette = asus_aura_set_palette, + .direct_write = asus_aura_strix_set_direct, +}; + +static const struct led_dynamic_ops asus_aura_kbd_4zone_ops = { + .set_effect = asus_aura_set_effect, + .set_speed = asus_aura_set_speed, + .set_direction = asus_aura_set_direction, + .set_palette = asus_aura_set_palette, + .direct_write = asus_aura_strix_set_direct, +}; + +static const struct led_dynamic_ops asus_aura_lightbar_ops = { + .set_effect = asus_aura_set_effect, + .set_speed = asus_aura_set_speed, + .set_direction = asus_aura_set_direction, + .set_palette = asus_aura_set_palette, + .direct_write = asus_aura_lightbar_set_direct, +}; + +static int asus_aura_discover(struct asus_drvdata *drvdata, bool *has_lightbar, + bool *is_strix_4zone) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, + AURA_CMD_PROBE, + 0x20, + 0x31, + 0x00, + 0x20, + }; + int ret; + + *has_lightbar = false; + *is_strix_4zone = false; + + /* + * Query hardware configuration via Report 0x5D opcode 0x05. + * Byte 9 describes the keyboard layout class (0x02 = 4-zone, + * 0x03 = per-key) and byte 13 is the physical-region bitmap + * (bit 1 = lightbar present). + */ + ret = asus_aura_set_feature(drvdata, buf, sizeof(buf)); + if (ret < 0) + return ret; + + memset(buf, 0, sizeof(buf)); + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + ret = asus_aura_get_feature(drvdata, buf, sizeof(buf)); + if (ret < 0) + return ret; + + if (ret < 14) + return -EPROTO; + + if (buf[1] != AURA_CMD_PROBE || buf[2] != 0x20 || buf[3] != 0x31) + return -ENODEV; + + *is_strix_4zone = (buf[9] == 0x02); + *has_lightbar = !!(buf[13] & 0x02); + + return 0; +} + +static const struct asus_slash_mode { + const char *name; + u8 mode; +} asus_slash_modes[] = { + { "Static", 0x06 }, + { "Bounce", 0x10 }, + { "Slash", 0x12 }, + { "Loading", 0x13 }, + { "BitStream", 0x1d }, + { "Transmission", 0x1a }, + { "Flow", 0x19 }, + { "Flux", 0x25 }, + { "Phantom", 0x24 }, + { "Spectrum", 0x26 }, + { "Hazard", 0x32 }, + { "Interfacing", 0x33 }, + { "Ramp", 0x34 }, + { "GameOver", 0x42 }, + { "Start", 0x43 }, + { "Buzzer", 0x44 }, +}; + +static inline u8 asus_slash_report_id(struct asus_drvdata *drvdata) +{ + return (drvdata->hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_SLASH) ? + FEATURE_KBD_LED_REPORT_ID2 : FEATURE_KBD_LED_REPORT_ID1; +} + +static int asus_slash_init_unlocked(struct asus_drvdata *drvdata) +{ + u8 rpt = asus_slash_report_id(drvdata); + u8 pkt1[] = { rpt, 0xd7, 0x00, 0x00, 0x01, 0xac }; + u8 pkt2[] = { rpt, 0xd2, 0x02, 0x01, 0x08, 0xab }; + int ret; + + ret = asus_aura_set_feature_unlocked(drvdata, pkt1, sizeof(pkt1)); + if (ret < 0) + return ret; + + return asus_aura_set_feature_unlocked(drvdata, pkt2, sizeof(pkt2)); +} + +static int asus_slash_set_options_unlocked(struct asus_drvdata *drvdata, bool enabled, + u8 brightness, u8 interval) +{ + u8 rpt = asus_slash_report_id(drvdata); + u8 pkt[] = { + rpt, 0xd3, 0x03, 0x01, 0x08, 0xab, 0xff, 0x01, + enabled ? 1 : 0, 0x06, brightness, 0xff, interval + }; + + return asus_aura_set_feature_unlocked(drvdata, pkt, sizeof(pkt)); +} + +static int asus_slash_set_mode_unlocked(struct asus_drvdata *drvdata, u8 mode) +{ + u8 rpt = asus_slash_report_id(drvdata); + u8 pkt1[] = { rpt, 0xd2, 0x03, 0x00, 0x0c }; + u8 pkt2[] = { + rpt, 0xd3, 0x04, 0x00, 0x0c, 0x01, mode, 0x02, + 0x19, 0x03, 0x13, 0x04, 0x11, 0x05, 0x12, 0x06, 0x13 + }; + int ret; + + ret = asus_aura_set_feature_unlocked(drvdata, pkt1, sizeof(pkt1)); + if (ret < 0) + return ret; + + return asus_aura_set_feature_unlocked(drvdata, pkt2, sizeof(pkt2)); +} + +static int asus_slash_save_unlocked(struct asus_drvdata *drvdata) +{ + u8 rpt = asus_slash_report_id(drvdata); + u8 pkt[] = { rpt, 0xd4, 0x00, 0x00, 0x01, 0xab }; + + return asus_aura_set_feature_unlocked(drvdata, pkt, sizeof(pkt)); +} + +static int asus_slash_brightness_set_blocking(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + struct asus_drvdata *drvdata = container_of(led_cdev, struct asus_drvdata, slash_led); + int ret; + + guard(mutex)(&drvdata->aura_lock); + + drvdata->slash_brightness = brightness; + ret = asus_slash_set_options_unlocked(drvdata, brightness > 0, (u8)brightness, + drvdata->slash_interval); + if (ret < 0) + return ret; + + return asus_slash_save_unlocked(drvdata); +} + +static ssize_t slash_mode_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led = dev_get_drvdata(dev); + struct asus_drvdata *drvdata = container_of(led, struct asus_drvdata, slash_led); + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(asus_slash_modes); i++) { + if (asus_slash_modes[i].mode == drvdata->slash_mode) + return sysfs_emit(buf, "%s\n", asus_slash_modes[i].name); + } + + return sysfs_emit(buf, "0x%02x\n", drvdata->slash_mode); +} + +static ssize_t slash_mode_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led = dev_get_drvdata(dev); + struct asus_drvdata *drvdata = container_of(led, struct asus_drvdata, slash_led); + char mode_str[32]; + unsigned int i; + u8 mode_val = 0; + int ret; + + if (sscanf(buf, "%31s", mode_str) != 1) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(asus_slash_modes); i++) { + if (sysfs_streq(mode_str, asus_slash_modes[i].name)) { + mode_val = asus_slash_modes[i].mode; + break; + } + } + + if (!mode_val) { + if (kstrtou8(mode_str, 0, &mode_val)) + return -EINVAL; + } + + guard(mutex)(&drvdata->aura_lock); + + ret = asus_slash_set_mode_unlocked(drvdata, mode_val); + if (ret < 0) + return ret; + + ret = asus_slash_save_unlocked(drvdata); + if (ret < 0) + return ret; + + drvdata->slash_mode = mode_val; + return count; +} +static DEVICE_ATTR_RW(slash_mode); + +static ssize_t slash_mode_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, + "Static Bounce Slash Loading BitStream Transmission Flow Flux Phantom Spectrum Hazard Interfacing Ramp GameOver Start Buzzer\n"); +} +static DEVICE_ATTR_RO(slash_mode_index); + +static ssize_t slash_interval_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led = dev_get_drvdata(dev); + struct asus_drvdata *drvdata = container_of(led, struct asus_drvdata, slash_led); + + return sysfs_emit(buf, "%u\n", drvdata->slash_interval); +} + +static ssize_t slash_interval_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct led_classdev *led = dev_get_drvdata(dev); + struct asus_drvdata *drvdata = container_of(led, struct asus_drvdata, slash_led); + u8 interval; + int ret; + + if (kstrtou8(buf, 0, &interval)) + return -EINVAL; + + guard(mutex)(&drvdata->aura_lock); + + drvdata->slash_interval = interval; + ret = asus_slash_set_options_unlocked(drvdata, drvdata->slash_brightness > 0, + drvdata->slash_brightness, interval); + if (ret < 0) + return ret; + + ret = asus_slash_save_unlocked(drvdata); + if (ret < 0) + return ret; + + return count; +} +static DEVICE_ATTR_RW(slash_interval); + +static struct attribute *asus_slash_attrs[] = { + &dev_attr_slash_mode.attr, + &dev_attr_slash_mode_index.attr, + &dev_attr_slash_interval.attr, + NULL, +}; + +static const struct attribute_group asus_slash_group = { + .attrs = asus_slash_attrs, +}; + +static const struct attribute_group *asus_slash_groups[] = { + &asus_slash_group, + NULL, +}; + +static bool asus_has_slash_lighting(void) +{ + const char *board_name = dmi_get_system_info(DMI_BOARD_NAME); + const char *product_name = dmi_get_system_info(DMI_PRODUCT_NAME); + + if (board_name) { + if (strstr(board_name, "GA403") || + strstr(board_name, "GA605") || + strstr(board_name, "GU405") || + strstr(board_name, "GU605") || + strstr(board_name, "GU606") || + strstr(board_name, "G614F")) + return true; + } + + if (product_name) { + if (strstr(product_name, "GA403") || + strstr(product_name, "GA605") || + strstr(product_name, "GU405") || + strstr(product_name, "GU605") || + strstr(product_name, "GU606") || + strstr(product_name, "G614F")) + return true; + } + + return false; +} + +static int asus_init_slash(struct hid_device *hdev) +{ + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + int ret; + + if (!asus_has_slash_lighting()) + return 0; + + drvdata->slash_led.name = "asus::slash"; + drvdata->slash_led.max_brightness = 255; + drvdata->slash_led.brightness = 255; + drvdata->slash_led.brightness_set_blocking = asus_slash_brightness_set_blocking; + drvdata->slash_led.groups = asus_slash_groups; + + drvdata->slash_brightness = 255; + drvdata->slash_interval = 0; + drvdata->slash_mode = 0x19; + + scoped_guard(mutex, &drvdata->aura_lock) { + ret = asus_slash_init_unlocked(drvdata); + if (ret < 0) { + hid_warn(hdev, "Failed to initialize Slash lighting: %d\n", ret); + return ret; + } + } + + ret = devm_led_classdev_register(&hdev->dev, &drvdata->slash_led); + if (ret < 0) { + hid_warn(hdev, "Failed to register Slash LED classdev: %d\n", ret); + return ret; + } + + drvdata->has_slash_led = true; + hid_info(hdev, "Registered Slash lighting LED: asus::slash\n"); + + scoped_guard(mutex, &drvdata->aura_lock) { + asus_slash_set_options_unlocked(drvdata, true, 255, 0); + asus_slash_set_mode_unlocked(drvdata, 0x19); + asus_slash_save_unlocked(drvdata); + } + + return 0; +} + +static int asus_init_dynamic_lighting(struct hid_device *hdev) +{ + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + unsigned int i; + unsigned int global_effects; + unsigned int kbd_effects; + unsigned int lightbar_effects; + u8 effect_mask[2]; + bool is_strix_direct = false; + bool has_lightbar = false; + bool kbd_direct; + bool lightbar_direct; + int ret; + + if (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD2) + is_strix_direct = true; + + ret = asus_aura_discover(drvdata, &has_lightbar, &drvdata->is_strix_4zone); + if (ret == -ENODEV) + return 0; + if (ret < 0) { + hid_warn(hdev, "Aura device discovery failed: %d\n", ret); + return 0; + } + + for (i = 0; i < ROG_STRIX_4ZONE_KBD_LEDS; i++) { + drvdata->kbd_direct_buf[i * 3 + 0] = 255; + drvdata->kbd_direct_buf[i * 3 + 1] = 0; + drvdata->kbd_direct_buf[i * 3 + 2] = 0; + } + for (i = 0; i < (drvdata->is_strix_4zone ? ROG_STRIX_4ZONE_LIGHTBAR_LEDS : + ROG_STRIX_LIGHTBAR_LEDS); i++) { + drvdata->lb_direct_buf[i * 3 + 0] = 255; + drvdata->lb_direct_buf[i * 3 + 1] = 0; + drvdata->lb_direct_buf[i * 3 + 2] = 0; + } + + ret = asus_aura_wake_all_zones(drvdata); + if (ret < 0) + hid_warn(hdev, "Failed to wake Aura hardware zones: %d\n", ret); + + kbd_direct = is_strix_direct; + lightbar_direct = has_lightbar; + ret = asus_aura_get_effect_mask(drvdata, effect_mask); + if (ret < 0) { + hid_warn(hdev, + "Aura 0x9e capability probe failed: %d, using fallback effect list\n", + ret); + kbd_effects = asus_aura_fallback_supported_effects(kbd_direct); + lightbar_effects = asus_aura_fallback_supported_effects(lightbar_direct); + global_effects = asus_aura_fallback_supported_effects(false); + } else { + kbd_effects = asus_aura_supported_effects_from_mask(effect_mask, kbd_direct); + lightbar_effects = asus_aura_supported_effects_from_mask(effect_mask, + lightbar_direct); + global_effects = asus_aura_supported_effects_from_mask(effect_mask, false); + } + + drvdata->has_lightbar = has_lightbar; + drvdata->aura_mode = AURA_MODE_AUTO; + + /* + * Keyboard Dynamic Lighting zone: always registered on all Aura models. + */ + drvdata->dldev_kbd.cdev.name = "aura:keyboard"; + drvdata->dldev_kbd.cdev.max_brightness = 255; + drvdata->dldev_kbd.cdev.brightness = 255; + drvdata->dldev_kbd.cdev.brightness_set_blocking = + asus_aura_brightness_set_blocking; + drvdata->dldev_kbd.cdev.groups = asus_aura_groups; + drvdata->dldev_kbd.ops = drvdata->is_strix_4zone ? + &asus_aura_kbd_4zone_ops : &asus_aura_kbd_ops; + drvdata->dldev_kbd.driver_data = drvdata; + drvdata->dldev_kbd.speed = 1; + drvdata->dldev_kbd.max_speed = 2; + drvdata->dldev_kbd.direction = DL_DIRECTION_RIGHT; + drvdata->dldev_kbd.supported_directions = BIT(DL_DIRECTION_RIGHT) | + BIT(DL_DIRECTION_LEFT); + drvdata->dldev_kbd.max_palette_entries = 2; + drvdata->dldev_kbd.current_effect = + (kbd_effects & BIT(DL_EFFECT_STATIC)) ? + DL_EFFECT_STATIC : DL_EFFECT_OFF; + drvdata->dldev_kbd.supported_effects = kbd_effects; + + if (kbd_direct) { + drvdata->dldev_kbd.zone_type = drvdata->is_strix_4zone ? + "keyboard" : "keyboard_per_key"; + drvdata->dldev_kbd.led_count = drvdata->is_strix_4zone ? + ROG_STRIX_4ZONE_KBD_LEDS : ROG_STRIX_DIRECT_LEDS; + } else if (drvdata->is_strix_4zone) { + drvdata->dldev_kbd.zone_type = "keyboard"; + drvdata->dldev_kbd.led_count = ROG_STRIX_4ZONE_KBD_LEDS; + } else { + drvdata->dldev_kbd.zone_type = "keyboard"; + } + + ret = devm_led_classdev_dynamic_register(&hdev->dev, &drvdata->dldev_kbd); + if (ret < 0) { + hid_warn(hdev, "Failed to register kbd dynamic lighting: %d\n", ret); + return ret; + } + drvdata->has_dldev_kbd = true; + + /* Set initial default palette (#ff0000 ROG Red) */ + if (drvdata->dldev_kbd.palette) { + drvdata->dldev_kbd.palette[0].r = 255; + drvdata->dldev_kbd.palette[0].g = 0; + drvdata->dldev_kbd.palette[0].b = 0; + drvdata->dldev_kbd.num_palette_entries = 1; + } + + if (has_lightbar) { + /* Register chassis lightbar */ + drvdata->dldev_lightbar.cdev.name = "aura:lightbar"; + drvdata->dldev_lightbar.cdev.max_brightness = 255; + drvdata->dldev_lightbar.cdev.brightness = 255; + drvdata->dldev_lightbar.cdev.brightness_set_blocking = + asus_aura_brightness_set_blocking; + drvdata->dldev_lightbar.cdev.groups = asus_aura_groups; + drvdata->dldev_lightbar.ops = &asus_aura_lightbar_ops; + drvdata->dldev_lightbar.driver_data = drvdata; + drvdata->dldev_lightbar.speed = 1; + drvdata->dldev_lightbar.max_speed = 2; + drvdata->dldev_lightbar.direction = DL_DIRECTION_RIGHT; + drvdata->dldev_lightbar.supported_directions = BIT(DL_DIRECTION_RIGHT) | + BIT(DL_DIRECTION_LEFT); + drvdata->dldev_lightbar.zone_type = "lightbar"; + drvdata->dldev_lightbar.led_count = drvdata->is_strix_4zone ? + ROG_STRIX_4ZONE_LIGHTBAR_LEDS : ROG_STRIX_LIGHTBAR_LEDS; + drvdata->dldev_lightbar.max_palette_entries = 2; + drvdata->dldev_lightbar.current_effect = + (lightbar_effects & BIT(DL_EFFECT_STATIC)) ? + DL_EFFECT_STATIC : DL_EFFECT_OFF; + drvdata->dldev_lightbar.supported_effects = lightbar_effects; + + ret = devm_led_classdev_dynamic_register(&hdev->dev, + &drvdata->dldev_lightbar); + if (ret < 0) { + hid_warn(hdev, "Failed to register lightbar dynamic lighting: %d\n", + ret); + } else { + drvdata->has_dldev_lightbar = true; + if (drvdata->dldev_lightbar.palette) { + drvdata->dldev_lightbar.palette[0].r = 255; + drvdata->dldev_lightbar.palette[0].g = 0; + drvdata->dldev_lightbar.palette[0].b = 0; + drvdata->dldev_lightbar.num_palette_entries = 1; + } + } + + /* Register global aggregate zone */ + drvdata->dldev_global.cdev.name = "aura:global"; + drvdata->dldev_global.cdev.max_brightness = 255; + drvdata->dldev_global.cdev.brightness = 255; + drvdata->dldev_global.cdev.brightness_set_blocking = + asus_aura_brightness_set_blocking; + drvdata->dldev_global.cdev.groups = asus_aura_groups; + drvdata->dldev_global.ops = &asus_aura_global_ops; + drvdata->dldev_global.driver_data = drvdata; + drvdata->dldev_global.speed = 1; + drvdata->dldev_global.max_speed = 2; + drvdata->dldev_global.direction = DL_DIRECTION_RIGHT; + drvdata->dldev_global.supported_directions = BIT(DL_DIRECTION_RIGHT) | + BIT(DL_DIRECTION_LEFT); + drvdata->dldev_global.zone_type = "global"; + drvdata->dldev_global.max_palette_entries = 2; + drvdata->dldev_global.current_effect = + (global_effects & BIT(DL_EFFECT_STATIC)) ? + DL_EFFECT_STATIC : DL_EFFECT_OFF; + drvdata->dldev_global.supported_effects = global_effects; + + if (kbd_direct) { + drvdata->dldev_global.led_count = drvdata->is_strix_4zone ? + ROG_STRIX_4ZONE_KBD_LEDS : ROG_STRIX_DIRECT_LEDS; + } else if (drvdata->is_strix_4zone) { + drvdata->dldev_global.led_count = ROG_STRIX_4ZONE_KBD_LEDS; + } + + ret = devm_led_classdev_dynamic_register(&hdev->dev, + &drvdata->dldev_global); + if (ret < 0) { + hid_warn(hdev, "Failed to register global dynamic lighting: %d\n", + ret); + } else { + drvdata->has_dldev_global = true; + if (drvdata->dldev_global.palette) { + drvdata->dldev_global.palette[0].r = 255; + drvdata->dldev_global.palette[0].g = 0; + drvdata->dldev_global.palette[0].b = 0; + drvdata->dldev_global.num_palette_entries = 1; + } + } + } + + hid_info(hdev, "Registered dynamic lighting zones: global=%d, kbd=%d, lightbar=%d\n", + drvdata->has_dldev_global, drvdata->has_dldev_kbd, + drvdata->has_dldev_lightbar); + + /* Apply initial default static effect to illuminate active zones */ + if (asus_aura_effective_mode(drvdata) == AURA_MODE_UNIFIED) { + if (drvdata->has_dldev_global) + asus_aura_apply_effect(&drvdata->dldev_global, + drvdata->dldev_global.current_effect, + drvdata->dldev_global.cdev.brightness); + } else { + if (drvdata->has_dldev_kbd) + asus_aura_apply_effect(&drvdata->dldev_kbd, + drvdata->dldev_kbd.current_effect, + drvdata->dldev_kbd.cdev.brightness); + if (drvdata->has_dldev_lightbar) + asus_aura_apply_effect(&drvdata->dldev_lightbar, + drvdata->dldev_lightbar.current_effect, + drvdata->dldev_lightbar.cdev.brightness); + } + + return 0; +} + +#else /* !IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) */ + +static inline int asus_init_dynamic_lighting(struct hid_device *hdev) +{ + return 0; +} + +static inline int asus_init_slash(struct hid_device *hdev) +{ + return 0; +} + +#endif /* IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) */ + +/* + * [0] REPORT_ID (same value defined in report descriptor) + * [1] rest battery level. range [0..255] + * [2]..[7] Bluetooth hardware address (MAC address) + * [8] charging status + * = 0 : AC offline / discharging + * = 1 : AC online / charging + * = 2 : AC online / fully charged + */ +static int asus_parse_battery(struct asus_drvdata *drvdata, u8 *data, int size) +{ + u8 sts; + u8 lvl; + int val; + + lvl = data[1]; + sts = data[8]; + + drvdata->battery_capacity = ((int)lvl * 100) / (int)BATTERY_LEVEL_MAX; + + switch (sts) { + case BATTERY_STAT_CHARGING: + val = POWER_SUPPLY_STATUS_CHARGING; + break; + case BATTERY_STAT_FULL: + val = POWER_SUPPLY_STATUS_FULL; + break; + case BATTERY_STAT_DISCONNECT: + default: + val = POWER_SUPPLY_STATUS_DISCHARGING; + break; + } + drvdata->battery_stat = val; + + return 0; +} + +static int asus_report_battery(struct asus_drvdata *drvdata, u8 *data, int size) +{ + /* notify only the autonomous event by device */ + if ((drvdata->battery_in_query == false) && + (size == BATTERY_REPORT_SIZE)) + power_supply_changed(drvdata->battery); + + return 0; +} + +static int asus_battery_query(struct asus_drvdata *drvdata) +{ + u8 *buf; + int ret = 0; + + buf = kmalloc(BATTERY_REPORT_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + drvdata->battery_in_query = true; + ret = hid_hw_raw_request(drvdata->hdev, BATTERY_REPORT_ID, + buf, BATTERY_REPORT_SIZE, + HID_INPUT_REPORT, HID_REQ_GET_REPORT); + drvdata->battery_in_query = false; + if (ret == BATTERY_REPORT_SIZE) + ret = asus_parse_battery(drvdata, buf, BATTERY_REPORT_SIZE); + else + ret = -ENODATA; + + kfree(buf); + + return ret; +} + +static enum power_supply_property asus_battery_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_SCOPE, + POWER_SUPPLY_PROP_MODEL_NAME, +}; + +#define QUERY_MIN_INTERVAL (60 * HZ) /* 60[sec] */ + +static int asus_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct asus_drvdata *drvdata = power_supply_get_drvdata(psy); + int ret = 0; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + case POWER_SUPPLY_PROP_CAPACITY: + if (time_before(drvdata->battery_next_query, jiffies)) { + drvdata->battery_next_query = + jiffies + QUERY_MIN_INTERVAL; + ret = asus_battery_query(drvdata); + if (ret) + return ret; + } + if (psp == POWER_SUPPLY_PROP_STATUS) + val->intval = drvdata->battery_stat; + else + val->intval = drvdata->battery_capacity; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 1; + break; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_DEVICE; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = drvdata->hdev->name; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int asus_battery_probe(struct hid_device *hdev) +{ + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + struct power_supply_config pscfg = { .drv_data = drvdata }; + int ret = 0; + + drvdata->battery_capacity = 0; + drvdata->battery_stat = POWER_SUPPLY_STATUS_UNKNOWN; + drvdata->battery_in_query = false; + + drvdata->battery_desc.properties = asus_battery_props; + drvdata->battery_desc.num_properties = ARRAY_SIZE(asus_battery_props); + drvdata->battery_desc.get_property = asus_battery_get_property; + drvdata->battery_desc.type = POWER_SUPPLY_TYPE_BATTERY; + drvdata->battery_desc.use_for_apm = 0; + drvdata->battery_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL, + "asus-keyboard-%s-battery", + strlen(hdev->uniq) ? + hdev->uniq : dev_name(&hdev->dev)); + if (!drvdata->battery_desc.name) + return -ENOMEM; + + drvdata->battery_next_query = jiffies; + + drvdata->battery = devm_power_supply_register(&hdev->dev, + &(drvdata->battery_desc), &pscfg); + if (IS_ERR(drvdata->battery)) { + ret = PTR_ERR(drvdata->battery); + drvdata->battery = NULL; + hid_err(hdev, "Unable to register battery device\n"); + return ret; + } + + power_supply_powers(drvdata->battery, &hdev->dev); + + return ret; +} + +static int asus_input_configured(struct hid_device *hdev, struct hid_input *hi) +{ + struct input_dev *input = hi->input; + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + + /* T100CHI uses MULTI_INPUT, bind the touchpad to the mouse hid_input */ + if (drvdata->quirks & QUIRK_T100CHI && + hi->report->id != T100CHI_MOUSE_REPORT_ID) + return 0; + + /* Handle MULTI_INPUT on E1239T mouse/touchpad USB interface */ + if (drvdata->tp && (drvdata->quirks & QUIRK_MEDION_E1239T)) { + switch (hi->report->id) { + case E1239T_TP_TOGGLE_REPORT_ID: + input_set_capability(input, EV_KEY, KEY_F21); + input->name = "Asus Touchpad Keys"; + drvdata->tp_kbd_input = input; + return 0; + case INPUT_REPORT_ID: + break; /* Touchpad report, handled below */ + default: + return 0; /* Ignore other reports */ + } + } + + if (drvdata->tp) { + int ret; + + input_set_abs_params(input, ABS_MT_POSITION_X, 0, + drvdata->tp->max_x, 0, 0); + input_set_abs_params(input, ABS_MT_POSITION_Y, 0, + drvdata->tp->max_y, 0, 0); + input_abs_set_res(input, ABS_MT_POSITION_X, drvdata->tp->res_x); + input_abs_set_res(input, ABS_MT_POSITION_Y, drvdata->tp->res_y); + + if (drvdata->tp->contact_size >= 5) { + input_set_abs_params(input, ABS_TOOL_WIDTH, 0, + MAX_TOUCH_MAJOR, 0, 0); + input_set_abs_params(input, ABS_MT_TOUCH_MAJOR, 0, + MAX_TOUCH_MAJOR, 0, 0); + input_set_abs_params(input, ABS_MT_PRESSURE, 0, + MAX_PRESSURE, 0, 0); + } + + __set_bit(BTN_LEFT, input->keybit); + __set_bit(INPUT_PROP_BUTTONPAD, input->propbit); + + ret = input_mt_init_slots(input, drvdata->tp->max_contacts, + INPUT_MT_POINTER); + + if (ret) { + hid_err(hdev, "Asus input mt init slots failed: %d\n", ret); + return ret; + } + } + + drvdata->input = input; + + if ((drvdata->quirks & QUIRK_HID_FN_LOCK) && + (asus_kbd_fn_lock_set(drvdata, true))) + hid_warn(hdev, "Error while setting FN lock to ON\n"); + + return 0; +} + +#define asus_map_key_clear(c) hid_map_usage_clear(hi, usage, bit, \ + max, EV_KEY, (c)) +static int asus_input_mapping(struct hid_device *hdev, + struct hid_input *hi, struct hid_field *field, + struct hid_usage *usage, unsigned long **bit, + int *max) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); @@ -1365,6 +2899,11 @@ static int __maybe_unused asus_resume(struct hid_device *hdev) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (drvdata->has_dldev_kbd) + asus_aura_wake_all_zones(drvdata); +#endif + /* * If we have a backlight listener registered, restore the previous state, * in case of error do not fail: most models restore the backlight @@ -1380,6 +2919,11 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (drvdata->has_dldev_kbd) + asus_aura_wake_all_zones(drvdata); +#endif + if (drvdata->tp) return asus_start_multitouch(hdev); @@ -1400,6 +2944,17 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) hid_set_drvdata(hdev, drvdata); +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + ret = devm_mutex_init(&hdev->dev, &drvdata->aura_lock); + if (ret) + return ret; + + drvdata->aura_buf = devm_kzalloc(&hdev->dev, AURA_FEATURE_REPORT_SIZE, + GFP_KERNEL); + if (!drvdata->aura_buf) + return -ENOMEM; +#endif + drvdata->quirks = id->driver_data; /* @@ -1519,6 +3074,17 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) (asus_kbd_register_leds(hdev))) hid_warn(hdev, "Failed to initialize backlight.\n"); + if (asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID1) || + asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID2)) { + ret = asus_init_dynamic_lighting(hdev); + if (ret < 0) + hid_warn(hdev, "Failed to initialize dynamic lighting: %d\n", ret); + + ret = asus_init_slash(hdev); + if (ret < 0) + hid_warn(hdev, "Failed to initialize Slash LED: %d\n", ret); + } + /* * For ROG keyboards, skip rename for consistency and ->input check as * some devices do not have inputs. @@ -1698,6 +3264,9 @@ static const struct hid_device_id asus_devices[] = { { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK, USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD), QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD }, + { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK, + USB_DEVICE_ID_ASUSTEK_ROG_SLASH), + QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD | QUIRK_HID_FN_LOCK }, { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK, USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD2), QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD | QUIRK_HID_FN_LOCK }, diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h index c48791c352aa2e..5fe5a0211b038e 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -226,6 +226,7 @@ #define USB_DEVICE_ID_ASUSTEK_ROG_KEYBOARD2 0x1837 #define USB_DEVICE_ID_ASUSTEK_ROG_KEYBOARD3 0x1822 #define USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD 0x1866 +#define USB_DEVICE_ID_ASUSTEK_ROG_SLASH 0x193b #define USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD2 0x19b6 #define USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD3 0x1ce6 #define USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD3_BT 0x1ce7 From 489e099cfe8c210f140a5aa4f62820e298409204 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Wed, 9 Sep 2026 00:23:28 +0200 Subject: [PATCH 5/6] platform/x86: asus-wmi: Add Dynamic Lighting support for TUF laptop RGB Expose Dynamic Lighting class attributes on asus::kbd_backlight when TUF RGB control is supported (kbd_rgb_dev). Register the keyboard backlight via devm_led_classdev_dynamic_register, providing native sysfs controls for standard effects (static, breathing, spectrum cycle, rainbow, strobe), speed, palette colors, and power state persistence via ACPI WMI method calls (0xb3 and 0xb4). Preserve legacy kbd_rgb_mode and kbd_rgb_state sysfs attributes under the device groups for backward compatibility with existing tools. Signed-off-by: Marco Scardovi --- drivers/platform/x86/Kconfig | 1 + drivers/platform/x86/asus-wmi.c | 309 ++++++++++++++++++++++++++++++-- 2 files changed, 293 insertions(+), 17 deletions(-) diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig index 957034f39e4e7a..4120d9aedb9716 100644 --- a/drivers/platform/x86/Kconfig +++ b/drivers/platform/x86/Kconfig @@ -280,6 +280,7 @@ config ASUS_WMI select LEDS_CLASS select NEW_LEDS select ACPI_PLATFORM_PROFILE + imply LEDS_CLASS_DYNAMIC help Say Y here if you have a WMI aware Asus laptop (like Eee PCs or new Asus Notebooks). diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c index a65090429ca703..fd50ebc766b17f 100644 --- a/drivers/platform/x86/asus-wmi.c +++ b/drivers/platform/x86/asus-wmi.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -255,6 +256,9 @@ struct asus_wmi { struct led_classdev tpd_led; int tpd_led_wk; struct led_classdev kbd_led; +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + struct led_classdev_dynamic kbd_dldev; +#endif int kbd_led_wk; bool kbd_led_notify; bool kbd_led_avail; @@ -1046,22 +1050,212 @@ static ssize_t gpu_mux_mode_store(struct device *dev, static DEVICE_ATTR_RW(gpu_mux_mode); #endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */ +static inline struct asus_wmi *asus_from_kbd_led(struct led_classdev *led_cdev) +{ +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (is_dynamic_lighting_led(led_cdev)) + return lcdev_to_dldev(led_cdev)->driver_data; +#endif + return container_of(led_cdev, struct asus_wmi, kbd_led); +} + +static inline struct led_classdev *asus_kbd_led_cdev(struct asus_wmi *asus) +{ +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (asus->kbd_rgb_dev) + return &asus->kbd_dldev.cdev; +#endif + return &asus->kbd_led; +} + +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) +#define ASUS_TUF_SUPPORTED_EFFECTS (BIT(DL_EFFECT_OFF) | \ + BIT(DL_EFFECT_STATIC) | \ + BIT(DL_EFFECT_BREATHING) | \ + BIT(DL_EFFECT_SPECTRUM_CYCLE) | \ + BIT(DL_EFFECT_RAINBOW) | \ + BIT(DL_EFFECT_STROBE)) + +static u8 dl_to_tuf_mode(enum dl_effect_mode mode) +{ + switch (mode) { + case DL_EFFECT_STATIC: + return 0; + case DL_EFFECT_BREATHING: + return 1; + case DL_EFFECT_SPECTRUM_CYCLE: + return 2; + case DL_EFFECT_RAINBOW: + return 3; + case DL_EFFECT_STROBE: + return 10; + case DL_EFFECT_OFF: + default: + return 0; + } +} + +static enum dl_effect_mode tuf_mode_to_dl(u8 mode) +{ + switch (mode) { + case 1: + return DL_EFFECT_BREATHING; + case 2: + return DL_EFFECT_SPECTRUM_CYCLE; + case 3: + return DL_EFFECT_RAINBOW; + case 10: + return DL_EFFECT_STROBE; + case 0: + default: + return DL_EFFECT_STATIC; + } +} + +static int asus_tuf_rgb_update_hardware(struct asus_wmi *asus) +{ + struct led_classdev_dynamic *ldev = &asus->kbd_dldev; + u8 mode; + u8 r = 0, g = 0, b = 0; + u8 speed_val = 0xeb; + int err; + + if (ldev->current_effect != DL_EFFECT_OFF && + ldev->palette && ldev->num_palette_entries > 0) { + r = ldev->palette[0].r; + g = ldev->palette[0].g; + b = ldev->palette[0].b; + } + + mode = dl_to_tuf_mode(ldev->current_effect); + + switch (ldev->speed) { + case 0: + speed_val = 0xe1; + break; + case 1: + speed_val = 0xeb; + break; + case 2: + speed_val = 0xf5; + break; + default: + speed_val = 0xeb; + break; + } + + err = asus_wmi_evaluate_method3(ASUS_WMI_METHODID_DEVS, asus->kbd_rgb_dev, + 0xb3 | (mode << 8) | (r << 16) | (g << 24), + b | (speed_val << 8), NULL); + if (err) + return err; + + return asus_wmi_evaluate_method3(ASUS_WMI_METHODID_DEVS, asus->kbd_rgb_dev, + 0xb4 | (mode << 8) | (r << 16) | (g << 24), + b | (speed_val << 8), NULL); +} + +static int asus_tuf_rgb_set_effect(struct led_classdev_dynamic *ldev, + enum dl_effect_mode mode) +{ + struct asus_wmi *asus = ldev->driver_data; + enum dl_effect_mode old = ldev->current_effect; + int err; + + ldev->current_effect = mode; + err = asus_tuf_rgb_update_hardware(asus); + ldev->current_effect = old; + return err; +} + +static int asus_tuf_rgb_set_speed(struct led_classdev_dynamic *ldev, + unsigned int speed) +{ + struct asus_wmi *asus = ldev->driver_data; + unsigned int old = ldev->speed; + int err; + + if (speed > ldev->max_speed) + return -EINVAL; + + ldev->speed = speed; + err = asus_tuf_rgb_update_hardware(asus); + ldev->speed = old; + return err; +} + +static int asus_tuf_rgb_set_palette(struct led_classdev_dynamic *ldev, + const struct dl_rgb *palette, + unsigned int num_entries) +{ + struct asus_wmi *asus = ldev->driver_data; + struct dl_rgb old_color; + unsigned int old_n = ldev->num_palette_entries; + int err; + + if (!num_entries || !palette) + return -EINVAL; + + old_color = ldev->palette[0]; + ldev->palette[0] = palette[0]; + ldev->num_palette_entries = 1; + + err = asus_tuf_rgb_update_hardware(asus); + + ldev->palette[0] = old_color; + ldev->num_palette_entries = old_n; + return err; +} + +static int asus_tuf_rgb_set_power_states(struct led_classdev_dynamic *ldev, + u32 active_states) +{ + struct asus_wmi *asus = ldev->driver_data; + u32 flags = BIT(7); + + if (!asus->kbd_rgb_state_available) + return 0; + + if (active_states & BIT(DL_POWER_STATE_BOOT)) + flags |= BIT(1); + if (active_states & BIT(DL_POWER_STATE_AWAKE)) + flags |= BIT(3); + if (active_states & BIT(DL_POWER_STATE_SLEEP)) + flags |= BIT(5); + + return asus_wmi_evaluate_method3(ASUS_WMI_METHODID_DEVS, + ASUS_WMI_DEVID_TUF_RGB_STATE, + 0xbd | (BIT(2) << 8) | (flags << 16), 0, NULL); +} + +static const struct led_dynamic_ops asus_tuf_rgb_ops = { + .set_effect = asus_tuf_rgb_set_effect, + .set_speed = asus_tuf_rgb_set_speed, + .set_palette = asus_tuf_rgb_set_palette, + .set_power_states = asus_tuf_rgb_set_power_states, +}; +#endif + /* TUF Laptop Keyboard RGB Modes **********************************************/ static ssize_t kbd_rgb_mode_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - u32 cmd, mode, r, g, b, speed; + u32 cmd, mode, r, g, b, speed, speed_level; struct led_classdev *led; struct asus_wmi *asus; int err; led = dev_get_drvdata(dev); - asus = container_of(led, struct asus_wmi, kbd_led); + asus = asus_from_kbd_led(led); if (sscanf(buf, "%d %d %d %d %d %d", &cmd, &mode, &r, &g, &b, &speed) != 6) return -EINVAL; + speed_level = speed; + if (speed_level > 2) + speed_level = 1; + /* B3 is set and B4 is save to BIOS */ switch (cmd) { case 0: @@ -1097,6 +1291,22 @@ static ssize_t kbd_rgb_mode_store(struct device *dev, if (err) return err; +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (is_dynamic_lighting_led(led)) { + struct led_classdev_dynamic *ldev = lcdev_to_dldev(led); + + guard(mutex)(&ldev->lock); + if (ldev->palette) { + ldev->palette[0].r = r; + ldev->palette[0].g = g; + ldev->palette[0].b = b; + ldev->num_palette_entries = 1; + } + ldev->speed = speed_level; + ldev->current_effect = tuf_mode_to_dl(mode); + } +#endif + return count; } static DEVICE_ATTR_WO(kbd_rgb_mode); @@ -1120,6 +1330,7 @@ static ssize_t kbd_rgb_state_store(struct device *dev, const char *buf, size_t count) { u32 flags, cmd, boot, awake, sleep, keyboard; + struct led_classdev *led; int err; if (sscanf(buf, "%d %d %d %d %d", &cmd, &boot, &awake, &sleep, &keyboard) != 5) @@ -1144,6 +1355,24 @@ static ssize_t kbd_rgb_state_store(struct device *dev, if (err) return err; + led = dev_get_drvdata(dev); +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (is_dynamic_lighting_led(led)) { + struct led_classdev_dynamic *ldev = lcdev_to_dldev(led); + u32 states = 0; + + if (boot) + states |= BIT(DL_POWER_STATE_BOOT); + if (awake) + states |= BIT(DL_POWER_STATE_AWAKE); + if (sleep) + states |= BIT(DL_POWER_STATE_SLEEP); + + guard(mutex)(&ldev->lock); + ldev->active_power_states = states; + } +#endif + return count; } static DEVICE_ATTR_WO(kbd_rgb_state); @@ -1773,8 +2002,22 @@ static void kbd_led_update_all(struct work_struct *work) * Therefore, we can safely register the LED without holding * a spinlock. */ - ret = devm_led_classdev_register(&asus->platform_device->dev, - &asus->kbd_led); +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (asus->kbd_rgb_dev) { + ret = devm_led_classdev_dynamic_register(&asus->platform_device->dev, + &asus->kbd_dldev); + if (!ret && asus->kbd_dldev.palette) { + asus->kbd_dldev.palette[0].r = 255; + asus->kbd_dldev.palette[0].g = 255; + asus->kbd_dldev.palette[0].b = 255; + asus->kbd_dldev.num_palette_entries = 1; + } + } else +#endif + { + ret = devm_led_classdev_register(&asus->platform_device->dev, + &asus->kbd_led); + } if (!ret) { scoped_guard(spinlock_irqsave, &asus_ref.lock) asus->kbd_led_registered = true; @@ -1785,11 +2028,11 @@ static void kbd_led_update_all(struct work_struct *work) } if (value >= 0) - do_kbd_led_set(&asus->kbd_led, value); + do_kbd_led_set(asus_kbd_led_cdev(asus), value); if (notify) { scoped_guard(spinlock_irqsave, &asus_ref.lock) asus->kbd_led_notify = false; - led_classdev_notify_brightness_hw_changed(&asus->kbd_led, value); + led_classdev_notify_brightness_hw_changed(asus_kbd_led_cdev(asus), value); } } @@ -1914,7 +2157,7 @@ static void do_kbd_led_set(struct led_classdev *led_cdev, int value) struct asus_hid_listener *listener; struct asus_wmi *asus; - asus = container_of(led_cdev, struct asus_wmi, kbd_led); + asus = asus_from_kbd_led(led_cdev); scoped_guard(spinlock_irqsave, &asus_ref.lock) asus->kbd_led_wk = clamp_val(value, 0, ASUS_EV_MAX_BRIGHTNESS); @@ -1952,7 +2195,7 @@ static enum led_brightness kbd_led_get(struct led_classdev *led_cdev) struct asus_wmi *asus; int retval, value; - asus = container_of(led_cdev, struct asus_wmi, kbd_led); + asus = asus_from_kbd_led(led_cdev); scoped_guard(spinlock_irqsave, &asus_ref.lock) { if (!asus->kbd_led_avail) @@ -2117,18 +2360,50 @@ static int asus_wmi_led_init(struct asus_wmi *asus) goto error; } - asus->kbd_led.name = "asus::kbd_backlight"; - asus->kbd_led.flags = LED_BRIGHT_HW_CHANGED; - asus->kbd_led.brightness_set_blocking = kbd_led_set; - asus->kbd_led.brightness_get = kbd_led_get; - asus->kbd_led.max_brightness = ASUS_EV_MAX_BRIGHTNESS; asus->kbd_led_avail = !kbd_led_read(asus, &led_val, NULL); INIT_WORK(&asus->kbd_led_work, kbd_led_update_all); if (asus->kbd_led_avail) { asus->kbd_led_wk = led_val; - if (num_rgb_groups != 0) - asus->kbd_led.groups = kbd_rgb_mode_groups; +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (asus->kbd_rgb_dev) { + struct led_classdev_dynamic *dldev = &asus->kbd_dldev; + + dldev->cdev.name = "asus::kbd_backlight"; + dldev->cdev.flags = LED_BRIGHT_HW_CHANGED; + dldev->cdev.brightness_set_blocking = kbd_led_set; + dldev->cdev.brightness_get = kbd_led_get; + dldev->cdev.max_brightness = ASUS_EV_MAX_BRIGHTNESS; + dldev->ops = &asus_tuf_rgb_ops; + dldev->driver_data = asus; + dldev->zone_type = "keyboard"; + dldev->led_count = 1; + dldev->speed = 1; + dldev->max_speed = 2; + dldev->max_palette_entries = 1; + dldev->current_effect = DL_EFFECT_STATIC; + dldev->supported_effects = ASUS_TUF_SUPPORTED_EFFECTS; + if (asus->kbd_rgb_state_available) { + dldev->supported_power_states = + BIT(DL_POWER_STATE_BOOT) | + BIT(DL_POWER_STATE_AWAKE) | + BIT(DL_POWER_STATE_SLEEP); + dldev->active_power_states = + dldev->supported_power_states; + } + if (num_rgb_groups != 0) + dldev->cdev.groups = kbd_rgb_mode_groups; + } else +#endif + { + asus->kbd_led.name = "asus::kbd_backlight"; + asus->kbd_led.flags = LED_BRIGHT_HW_CHANGED; + asus->kbd_led.brightness_set_blocking = kbd_led_set; + asus->kbd_led.brightness_get = kbd_led_get; + asus->kbd_led.max_brightness = ASUS_EV_MAX_BRIGHTNESS; + if (num_rgb_groups != 0) + asus->kbd_led.groups = kbd_rgb_mode_groups; + } } else { asus->kbd_led_wk = -1; } @@ -5336,7 +5611,7 @@ static int asus_hotk_resume(struct device *device) { struct asus_wmi *asus = dev_get_drvdata(device); - if (!IS_ERR_OR_NULL(asus->kbd_led.dev)) + if (!IS_ERR_OR_NULL(asus_kbd_led_cdev(asus)->dev)) kbd_led_update(asus); if (asus_wmi_has_fnlock_key(asus)) @@ -5377,7 +5652,7 @@ static int asus_hotk_restore(struct device *device) bl = !asus_wmi_get_devstate_simple(asus, ASUS_WMI_DEVID_UWB); rfkill_set_sw_state(asus->uwb.rfkill, bl); } - if (!IS_ERR_OR_NULL(asus->kbd_led.dev)) + if (!IS_ERR_OR_NULL(asus_kbd_led_cdev(asus)->dev)) kbd_led_update(asus); if (asus->oobe_state_available) { /* From 8ef5708ed054bb8b93a6017882101065313be2a1 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Wed, 9 Sep 2026 09:26:30 +0200 Subject: [PATCH 6/6] leds: asus-aura-scsi: Add ASUS Aura RGB Dynamic Lighting driver for ROG NVMe enclosures ASUS ROG external NVMe enclosures (such as the ROG STRIX Arion, USB ID 0b05:1932) are USB mass-storage devices with no HID interface. Their addressable Aura RGB LEDs hang off an onboard ENE microcontroller driven via 16-byte vendor SCSI commands on the same LUN as the storage. Add the leds-asus-aura-scsi driver using a class_interface registered with the SCSI class. Its add callback matches INQUIRY vendor "ROG" and model "ESD-S1C", retains the SCSI device while the LED is registered, and leaves disk ownership and access untouched for the sd driver. Each matching enclosure exposes a uniquely named Dynamic Lighting device: asus-aura-scsi-:rgb:indicator. Hardware animation offloads (Off, Static, Breathing, Strobe, Spectrum Cycle, Rainbow, Direct streaming), speed (0..4), direction (right/left), palette, and direct RGB frame streaming via direct_buffer are fully integrated. Remove the device from scsi_dh_blist because class-interface discovery does not require device-handler attachment. Signed-off-by: Marco Scardovi --- MAINTAINERS | 1 + drivers/leds/Kconfig | 15 + drivers/leds/Makefile | 1 + drivers/leds/leds-asus-aura-scsi.c | 508 +++++++++++++++++++++++++++++ 4 files changed, 525 insertions(+) create mode 100644 drivers/leds/leds-asus-aura-scsi.c diff --git a/MAINTAINERS b/MAINTAINERS index 939a9ed581a509..c7052926d0e22e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -14840,6 +14840,7 @@ S: Maintained F: Documentation/ABI/testing/sysfs-class-leds-dynamic F: Documentation/leds/leds-class-dynamic.rst F: drivers/leds/led-class-dynamic.c +F: drivers/leds/leds-asus-aura-scsi.c F: include/linux/led-dynamic-lighting.h LED SUBSYSTEM diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index 13da18900a7d97..69e6c721d33677 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -118,6 +118,21 @@ config LEDS_ARIEL Say Y to if your machine is a Dell Wyse 3020 thin client. +config LEDS_ASUS_AURA_SCSI + tristate "LED support for ASUS ROG Aura SCSI external enclosures" + depends on SCSI + depends on LEDS_CLASS_DYNAMIC + help + This option enables support for the addressable Aura RGB LEDs on + ASUS ROG external NVMe enclosures (such as the ROG STRIX Arion, + USB ID 0b05:1932). + + The LEDs are driven through vendor SCSI commands to an onboard ENE + microcontroller and exposed via the Dynamic Lighting LED class interface. + + To compile this driver as a module, choose M here: the module + will be called leds-asus-aura-scsi. + config LEDS_OSRAM_AMS_AS3668 tristate "LED support for Osram AMS AS3668" depends on LEDS_CLASS diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile index ddccceca94b71e..7bd80ea8eceabf 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -16,6 +16,7 @@ obj-$(CONFIG_LEDS_ADP5520) += leds-adp5520.o obj-$(CONFIG_LEDS_AN30259A) += leds-an30259a.o obj-$(CONFIG_LEDS_APU) += leds-apu.o obj-$(CONFIG_LEDS_ARIEL) += leds-ariel.o +obj-$(CONFIG_LEDS_ASUS_AURA_SCSI) += leds-asus-aura-scsi.o obj-$(CONFIG_LEDS_OSRAM_AMS_AS3668) += leds-as3668.o obj-$(CONFIG_LEDS_AW200XX) += leds-aw200xx.o obj-$(CONFIG_LEDS_AW2013) += leds-aw2013.o diff --git a/drivers/leds/leds-asus-aura-scsi.c b/drivers/leds/leds-asus-aura-scsi.c new file mode 100644 index 00000000000000..8c3a536d9b0616 --- /dev/null +++ b/drivers/leds/leds-asus-aura-scsi.c @@ -0,0 +1,508 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ASUS Aura RGB over SCSI for ROG external NVMe enclosures + * (e.g. ROG STRIX Arion, USB 0b05:1932). + * + * USB mass-storage device, no HID; the ENE LED controller is driven via + * vendor SCSI commands. Matched by INQUIRY (vendor "ROG", model "ESD-S1C") + * through the SCSI class interface so sd keeps owning the disk. + * + * Exposes a Dynamic Lighting class device (asus-aura-scsi-:rgb:indicator), + * providing hardware effect offload (Off, Static, Breathing, Strobe, + * Spectrum Cycle, Rainbow, Direct streaming), speed, direction, palette, + * and direct buffer streaming. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ASUS_SCSI_INQ_VENDOR "ROG" +#define ASUS_SCSI_INQ_MODEL "ESD-S1C" +#define ASUS_AURA_USB_VID 0x0b05 +#define ASUS_AURA_USB_PID 0x1932 + +#define ENE_OPCODE 0xec +#define ENE_REG_MODE 0x8021 +#define ENE_REG_SPEED 0x8022 +#define ENE_REG_DIRECTION 0x8023 +#define ENE_REG_APPLY 0x80a0 +#define ENE_REG_COLORS 0x8160 +#define ENE_REG_COLORS_DIRECT 0x8100 +#define ENE_APPLY 0x01 +#define ENE_SAVE 0xaa +#define ENE_CDB_LEN 16 +#define ENE_TIMEOUT (10 * HZ) + +#define ASUS_AURA_SCSI_NUM_LEDS 4 +#define ASUS_AURA_SCSI_COLOR_LEN 3 +#define ASUS_AURA_SCSI_DIRECT_BUF_SIZE (ASUS_AURA_SCSI_NUM_LEDS * ASUS_AURA_SCSI_COLOR_LEN) + +struct asus_aura_zone { + struct list_head list; + struct device *class_dev; + struct scsi_device *sdev; + struct led_classdev_dynamic dldev; + u8 colors[ASUS_AURA_SCSI_NUM_LEDS][ASUS_AURA_SCSI_COLOR_LEN]; + u8 current_mode; + u8 current_speed; + u8 current_direction; +}; + +static LIST_HEAD(asus_aura_list); +static DEFINE_MUTEX(asus_aura_list_lock); + +static const struct usb_device_id asus_aura_usb_ids[] = { + { + .match_flags = USB_DEVICE_ID_MATCH_VENDOR | + USB_DEVICE_ID_MATCH_PRODUCT, + .idVendor = ASUS_AURA_USB_VID, + .idProduct = ASUS_AURA_USB_PID, + }, + { } +}; +MODULE_DEVICE_TABLE(usb, asus_aura_usb_ids); + +static void ene_build_cdb(u8 *cdb, u16 reg, u8 arg_count) +{ + memset(cdb, 0, ENE_CDB_LEN); + cdb[0] = ENE_OPCODE; + cdb[1] = 'A'; + cdb[2] = 'S'; + cdb[3] = (reg >> 8) & 0xff; + cdb[4] = reg & 0xff; + cdb[13] = arg_count; +} + +static int ene_write(struct scsi_device *sdev, u16 reg, + const void *data, u8 arg_count) +{ + struct request *rq; + struct scsi_cmnd *scmd; + u8 cdb[ENE_CDB_LEN]; + int ret; + + ene_build_cdb(cdb, reg, arg_count); + + rq = scsi_alloc_request(sdev->request_queue, REQ_OP_DRV_OUT, 0); + if (IS_ERR(rq)) + return PTR_ERR(rq); + + if (arg_count) { + ret = blk_rq_map_kern(rq, (void *)data, arg_count, GFP_NOIO); + if (ret) + goto out; + } + + scmd = blk_mq_rq_to_pdu(rq); + scmd->cmd_len = ENE_CDB_LEN; + memcpy(scmd->cmnd, cdb, ENE_CDB_LEN); + scmd->allowed = 1; + rq->timeout = ENE_TIMEOUT; + rq->rq_flags |= RQF_QUIET; + + blk_execute_rq(rq, true); + ret = scmd->result ? -EIO : 0; +out: + blk_mq_free_request(rq); + return ret; +} + +static int asus_aura_sync_hardware(struct asus_aura_zone *zone, bool save_flash) +{ + struct scsi_device *sdev = zone->sdev; + u8 colors[ASUS_AURA_SCSI_DIRECT_BUF_SIZE]; + u8 mode = zone->current_mode; + u8 speed = zone->current_speed; + u8 dir = zone->current_direction; + u8 apply = ENE_APPLY; + u8 save = ENE_SAVE; + int i, ret; + + if (!scsi_device_online(sdev)) + return -ENODEV; + + /* Mode must be written first or hardware ignores sequence */ + ret = ene_write(sdev, ENE_REG_MODE, &mode, 1); + if (ret) + return ret; + + /* Convert RGB to ENE wire order: R, B, G */ + for (i = 0; i < ASUS_AURA_SCSI_NUM_LEDS; i++) { + colors[i * 3 + 0] = zone->colors[i][0]; + colors[i * 3 + 1] = zone->colors[i][2]; + colors[i * 3 + 2] = zone->colors[i][1]; + } + + ret = ene_write(sdev, ENE_REG_COLORS, colors, sizeof(colors)); + if (ret) + return ret; + + ret = ene_write(sdev, ENE_REG_COLORS_DIRECT, colors, sizeof(colors)); + if (ret) + return ret; + + ret = ene_write(sdev, ENE_REG_SPEED, &speed, 1); + if (ret) + return ret; + + ret = ene_write(sdev, ENE_REG_DIRECTION, &dir, 1); + if (ret) + return ret; + + ret = ene_write(sdev, ENE_REG_APPLY, &apply, 1); + if (ret) + return ret; + + if (save_flash) { + ret = ene_write(sdev, ENE_REG_APPLY, &save, 1); + if (ret) + return ret; + } + + return 0; +} + +static int asus_aura_set_effect(struct led_classdev_dynamic *ldev, + enum dl_effect_mode mode) +{ + struct asus_aura_zone *zone = ldev->driver_data; + u8 hw_mode; + + switch (mode) { + case DL_EFFECT_OFF: + hw_mode = 0; + break; + case DL_EFFECT_STATIC: + case DL_EFFECT_DIRECT: + hw_mode = 1; + break; + case DL_EFFECT_BREATHING: + hw_mode = 2; + break; + case DL_EFFECT_STROBE: + hw_mode = 3; + break; + case DL_EFFECT_SPECTRUM_CYCLE: + hw_mode = 4; + break; + case DL_EFFECT_RAINBOW: + hw_mode = 5; + break; + default: + return -EINVAL; + } + + zone->current_mode = hw_mode; + /* Persist the selected effect; speed/palette/direct stay RAM-only. */ + return asus_aura_sync_hardware(zone, true); +} + +static int asus_aura_set_speed(struct led_classdev_dynamic *ldev, + unsigned int speed) +{ + struct asus_aura_zone *zone = ldev->driver_data; + + if (speed > 4) + return -EINVAL; + + zone->current_speed = speed; + return asus_aura_sync_hardware(zone, false); +} + +static int asus_aura_set_direction(struct led_classdev_dynamic *ldev, + enum dl_direction direction) +{ + struct asus_aura_zone *zone = ldev->driver_data; + + switch (direction) { + case DL_DIRECTION_RIGHT: + zone->current_direction = 0; + break; + case DL_DIRECTION_LEFT: + zone->current_direction = 1; + break; + default: + return -EINVAL; + } + + return asus_aura_sync_hardware(zone, false); +} + +static int asus_aura_set_palette(struct led_classdev_dynamic *ldev, + const struct dl_rgb *palette, + unsigned int num_entries) +{ + struct asus_aura_zone *zone = ldev->driver_data; + unsigned int i; + + if (!palette || num_entries == 0) + return -EINVAL; + + for (i = 0; i < ASUS_AURA_SCSI_NUM_LEDS; i++) { + const struct dl_rgb *c = &palette[min_t(unsigned int, i, num_entries - 1)]; + + zone->colors[i][0] = c->r; + zone->colors[i][1] = c->g; + zone->colors[i][2] = c->b; + } + + return asus_aura_sync_hardware(zone, false); +} + +static int asus_aura_direct_write(struct led_classdev_dynamic *ldev, + const u8 *buffer, size_t size) +{ + struct asus_aura_zone *zone = ldev->driver_data; + struct scsi_device *sdev = zone->sdev; + u8 colors[ASUS_AURA_SCSI_DIRECT_BUF_SIZE]; + u8 mode = 1; + u8 apply = ENE_APPLY; + int i, ret; + + if (size != ASUS_AURA_SCSI_DIRECT_BUF_SIZE) + return -EINVAL; + + if (!scsi_device_online(sdev)) + return -ENODEV; + + for (i = 0; i < ASUS_AURA_SCSI_NUM_LEDS; i++) { + u8 r = buffer[i * 3 + 0]; + u8 g = buffer[i * 3 + 1]; + u8 b = buffer[i * 3 + 2]; + + zone->colors[i][0] = r; + zone->colors[i][1] = g; + zone->colors[i][2] = b; + + colors[i * 3 + 0] = r; + colors[i * 3 + 1] = b; + colors[i * 3 + 2] = g; + } + + ret = ene_write(sdev, ENE_REG_MODE, &mode, 1); + if (ret) + return ret; + + ret = ene_write(sdev, ENE_REG_COLORS, colors, sizeof(colors)); + if (ret) + return ret; + + ret = ene_write(sdev, ENE_REG_COLORS_DIRECT, colors, sizeof(colors)); + if (ret) + return ret; + + /* Apply to RAM only without wearing flash during streaming */ + return ene_write(sdev, ENE_REG_APPLY, &apply, 1); +} + +static int asus_aura_brightness_set_blocking(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + struct asus_aura_zone *zone = ldev->driver_data; + + guard(mutex)(&ldev->lock); + + if (brightness == LED_OFF) { + u8 mode = 0; + u8 apply = ENE_APPLY; + int ret; + + ret = ene_write(zone->sdev, ENE_REG_MODE, &mode, 1); + if (ret) + return ret; + return ene_write(zone->sdev, ENE_REG_APPLY, &apply, 1); + } + + return asus_aura_sync_hardware(zone, false); +} + +static const struct led_dynamic_ops asus_aura_dynamic_ops = { + .set_effect = asus_aura_set_effect, + .set_speed = asus_aura_set_speed, + .set_direction = asus_aura_set_direction, + .set_palette = asus_aura_set_palette, + .direct_write = asus_aura_direct_write, +}; + +static bool asus_aura_sdev_match(struct scsi_device *sdev) +{ + return !strncmp(sdev->vendor, ASUS_SCSI_INQ_VENDOR, + strlen(ASUS_SCSI_INQ_VENDOR)) && + !strncmp(sdev->model, ASUS_SCSI_INQ_MODEL, + strlen(ASUS_SCSI_INQ_MODEL)); +} + +static int asus_aura_add(struct device *dev) +{ + struct scsi_device *sdev = to_scsi_device(dev->parent); + struct asus_aura_zone *zone; + char hctl[32]; + int ret; + + if (!asus_aura_sdev_match(sdev)) + return 0; + + ret = scsi_device_get(sdev); + if (ret) + return ret; + + zone = kzalloc_obj(*zone, GFP_KERNEL); + if (!zone) { + scsi_device_put(sdev); + return -ENOMEM; + } + + zone->class_dev = dev; + zone->sdev = sdev; + zone->current_mode = 1; + zone->current_speed = 2; + zone->current_direction = 0; + + /* Default ROG red #a60000 */ + zone->colors[0][0] = 166; + zone->colors[0][1] = 0; + zone->colors[0][2] = 0; + + zone->colors[1][0] = 0; + zone->colors[1][1] = 0; + zone->colors[1][2] = 0; + + zone->colors[2][0] = 166; + zone->colors[2][1] = 0; + zone->colors[2][2] = 0; + + zone->colors[3][0] = 0; + zone->colors[3][1] = 0; + zone->colors[3][2] = 0; + + strscpy(hctl, dev_name(&sdev->sdev_gendev), sizeof(hctl)); + strreplace(hctl, ':', '_'); + + zone->dldev.cdev.name = kasprintf(GFP_KERNEL, "asus-aura-scsi-%s:rgb:indicator", hctl); + if (!zone->dldev.cdev.name) { + scsi_device_put(sdev); + kfree(zone); + return -ENOMEM; + } + + zone->dldev.cdev.max_brightness = 255; + zone->dldev.cdev.brightness = 255; + zone->dldev.cdev.brightness_set_blocking = asus_aura_brightness_set_blocking; + + zone->dldev.ops = &asus_aura_dynamic_ops; + zone->dldev.driver_data = zone; + zone->dldev.zone_type = "segment_strip"; + zone->dldev.led_count = ASUS_AURA_SCSI_NUM_LEDS; + zone->dldev.max_speed = 4; + zone->dldev.speed = 2; + zone->dldev.direction = DL_DIRECTION_RIGHT; + zone->dldev.supported_directions = BIT(DL_DIRECTION_RIGHT) | BIT(DL_DIRECTION_LEFT); + zone->dldev.max_palette_entries = ASUS_AURA_SCSI_NUM_LEDS; + zone->dldev.supported_effects = BIT(DL_EFFECT_OFF) | + BIT(DL_EFFECT_STATIC) | + BIT(DL_EFFECT_BREATHING) | + BIT(DL_EFFECT_STROBE) | + BIT(DL_EFFECT_SPECTRUM_CYCLE) | + BIT(DL_EFFECT_RAINBOW) | + BIT(DL_EFFECT_DIRECT); + zone->dldev.current_effect = DL_EFFECT_STATIC; + + mutex_lock(&asus_aura_list_lock); + list_add(&zone->list, &asus_aura_list); + mutex_unlock(&asus_aura_list_lock); + + ret = led_classdev_dynamic_register(&sdev->sdev_gendev, &zone->dldev); + if (ret) { + mutex_lock(&asus_aura_list_lock); + list_del(&zone->list); + mutex_unlock(&asus_aura_list_lock); + kfree(zone->dldev.cdev.name); + scsi_device_put(sdev); + kfree(zone); + return ret; + } + + if (zone->dldev.palette) { + zone->dldev.palette[0].r = 166; + zone->dldev.palette[0].g = 0; + zone->dldev.palette[0].b = 0; + + zone->dldev.palette[1].r = 0; + zone->dldev.palette[1].g = 0; + zone->dldev.palette[1].b = 0; + + zone->dldev.palette[2].r = 166; + zone->dldev.palette[2].g = 0; + zone->dldev.palette[2].b = 0; + + zone->dldev.palette[3].r = 0; + zone->dldev.palette[3].g = 0; + zone->dldev.palette[3].b = 0; + + zone->dldev.num_palette_entries = ASUS_AURA_SCSI_NUM_LEDS; + } + + return 0; +} + +static void asus_aura_remove(struct device *dev) +{ + struct asus_aura_zone *zone = NULL, *tmp; + + mutex_lock(&asus_aura_list_lock); + list_for_each_entry(tmp, &asus_aura_list, list) { + if (tmp->class_dev == dev) { + list_del(&tmp->list); + zone = tmp; + break; + } + } + mutex_unlock(&asus_aura_list_lock); + + if (!zone) + return; + + led_classdev_dynamic_unregister(&zone->dldev); + kfree(zone->dldev.cdev.name); + scsi_device_put(zone->sdev); + kfree(zone); +} + +static struct class_interface asus_aura_interface = { + .add_dev = asus_aura_add, + .remove_dev = asus_aura_remove, +}; + +static int __init asus_aura_init(void) +{ + return scsi_register_interface(&asus_aura_interface); +} + +static void __exit asus_aura_exit(void) +{ + scsi_unregister_interface(&asus_aura_interface); +} + +module_init(asus_aura_init); +module_exit(asus_aura_exit); + +MODULE_DESCRIPTION("ASUS Aura RGB Dynamic Lighting driver for ROG NVMe enclosures"); +MODULE_AUTHOR("Liang Haowen "); +MODULE_AUTHOR("Marco Scardovi "); +MODULE_LICENSE("GPL");