From 4ba8167b38c4ba5638f9bae53be3beaefe3f3d20 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 7bca7015586a78ba946c3bcc41f5cd8762c009fd 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 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. After a successful direct_write the class sets current_effect to DIRECT when that effect is advertised, so drivers must not mutate it before the HID/USB write succeeds. 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 | 827 +++++++++++++++++++++++++++ include/linux/led-dynamic-lighting.h | 248 ++++++++ 5 files changed, 1095 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..aeda9a420b0fb5 --- /dev/null +++ b/drivers/leds/led-class-dynamic.c @@ -0,0 +1,827 @@ +// 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; + + if (ldev->supported_effects & BIT(DL_EFFECT_DIRECT)) + ldev->current_effect = DL_EFFECT_DIRECT; + + 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 a0e0f7ad80874eec67c557b82ce2b75719372f9d 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 write semantics for the direct and frame binary interfaces. Writing power_states replaces the active bitmask (an empty list clears all enabled states). 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..30140d7f9c3657 --- /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 replaces + the active bitmask (an empty list clears all enabled 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..9080f7e5e1b9d4 --- /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 replaces 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 2815c08a5a109e73f3bb7b65a05a45529a616c73 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Sat, 12 Sep 2026 18:41:06 +0200 Subject: [PATCH 4/6] HID: asus: Add Dynamic Lighting support for Aura and Slash Add Dynamic Lighting class support to hid-asus for Aura-capable ROG keyboards, chassis lightbars, and Slash LED bars. Discover Aura layout, lightbar, and per-key/direct RGB from HID feature reports rather than DMI board lists. Register Slash when feature report 0x5e is present, or on USB 0x193b when Aura LED reports exist; skip AniMe Matrix interfaces that reuse 0x193b without those reports. Register aura:global / aura:keyboard / aura:lightbar with aura_mode (auto/unified/split). auto resolves to split so keyboard and lightbar stay independently writable. Advertise DIRECT on global when the keyboard path supports direct RGB. On N-KEY devices where Aura 0xBC cannot drive the chassis lightbar independently, use the sibling HID LampArray interface as the in-kernel direct-RGB backend (no hidraw) and drop the owner reference on unbind. Fall back to Aura 0xBC when LampArray is absent. Map boot/awake/sleep/shutdown via power_states to AURA_CMD_POWER (0xbd) without rewriting zone power from brightness or resume. Keep asus::kbd_backlight brightness behaviour unchanged. Signed-off-by: Marco Scardovi --- .../testing/sysfs-class-led-driver-hid-asus | 15 + MAINTAINERS | 1 + drivers/hid/Kconfig | 1 + drivers/hid/hid-asus.c | 2569 +++++++++++++++-- drivers/hid/hid-ids.h | 2 + 5 files changed, 2404 insertions(+), 184 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..7c77a1c60ee4e1 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus @@ -0,0 +1,15 @@ +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". Writing selects the + stored mode; reading shows the stored value with the active + choice in brackets (e.g. "[auto] unified split"). + + auto resolves to split: keyboard and lightbar nodes (when + present) accept effect/direct writes independently, and the + global node returns -EBUSY. unified inverts that: only the + global node is writable. Direct RGB may be backed by HID + LampArray (Usage Page 0x59) when Aura 0xBC cannot address the + lightbar independently; firmware animations stay on Aura 0xb3. 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..ea886b85ab457b 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -30,6 +30,9 @@ #include /* For to_usb_interface for T100 touchpad intf check */ #include #include +#include +#include +#include #include "hid-ids.h" @@ -37,6 +40,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,9 +56,138 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #define FEATURE_KBD_LED_REPORT_ID1 0x5d #define FEATURE_KBD_LED_REPORT_ID2 0x5e -#define ROG_ALLY_REPORT_SIZE 64 -#define ROG_ALLY_X_MIN_MCU 313 -#define ROG_ALLY_MIN_MCU 319 +#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_LOGO_ALL (AURA_POWER_LOGO_BOOT | \ + AURA_POWER_LOGO_AWAKE | \ + AURA_POWER_LOGO_SLEEP | \ + AURA_POWER_LOGO_SHUTDOWN) +#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_EFFECT_STATIC 0x00 +#define AURA_EFFECT_BREATHING 0x01 +#define AURA_EFFECT_SPECTRUM_CYCLE 0x02 +#define AURA_EFFECT_RAINBOW 0x03 +#define AURA_EFFECT_STARS 0x04 +#define AURA_EFFECT_RAIN 0x05 +#define AURA_EFFECT_REACTIVE 0x06 +#define AURA_EFFECT_LASER 0x07 +#define AURA_EFFECT_RIPPLE 0x08 +#define AURA_EFFECT_PULSE 0x0a +#define AURA_EFFECT_COMET 0x0b +#define AURA_EFFECT_FLASH 0x0c +#define AURA_EFFECT_STROBING AURA_EFFECT_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 + +/* + * Microsoft HID Lighting Illumination / LampArray (Usage Page 0x59). + * + * Linux Dynamic Lighting (led-class-dynamic / aura:*) is the userspace ABI. + * On some Strix N-KEY devices (e.g. G614PR) Aura feature 0xBC cannot address + * the chassis lightbar independently; the sibling LampArray interface is the + * correct direct-RGB backend (same path Windows DL / G-Helper LampArray use). + * When LampArray is absent, callers fall back to Aura 0xBC. Firmware effects + * (0xb3) remain on the Aura report ID 0x5d interface. + * + * The LampArray USB interface is bound without hidraw so lighting stays + * exclusively under this driver. + */ +#define ASUS_LAMPARRAY_MAX_LAMPS 64 +#define ASUS_LAMPARRAY_MULTI_MAX 8 +#define ASUS_LAMPARRAY_PURPOSE_CONTROL 0x01 +#define ASUS_LAMPARRAY_FLAG_COMPLETE 0x01 +#define ASUS_LAMPARRAY_RID_ATTR 0x01 +#define ASUS_LAMPARRAY_RID_REQUEST 0x02 +#define ASUS_LAMPARRAY_RID_RESPONSE 0x03 +#define ASUS_LAMPARRAY_RID_MULTI 0x04 +#define ASUS_LAMPARRAY_RID_CONTROL 0x06 +#define AURA_ZONE_ACTIVATE_LAMPARRAY 0x03 +#define AURA_ZONE_RELEASE_LAMPARRAY 0x04 + /* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */ #define ASUS_SPURIOUS_CODE_0XEA 0xea @@ -154,8 +288,28 @@ 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, +}; + +struct asus_lamparray_lamp { + u16 id; + s32 x; + bool keyboard; +}; +#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 +324,29 @@ 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]; + struct hid_device *lamparray_hdev; + u8 *lamparray_buf; + size_t lamparray_buf_len; + u8 lamparray_rid_base; + unsigned int lamparray_count; + bool lamparray_controlled; + bool lamparray_unavailable; + struct asus_lamparray_lamp *lamparray_lamps; +#endif }; static int asus_report_battery(struct asus_drvdata *, u8 *, int); @@ -576,16 +753,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) @@ -868,7 +1060,7 @@ static int mcu_parse_version_string(const u8 *response, size_t response_size) static int mcu_request_version(struct hid_device *hdev) { - u8 *response __free(kfree) = kzalloc(ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + u8 *response __free(kfree) = kzalloc(FEATURE_KBD_REPORT_SIZE, GFP_KERNEL); const u8 request[] = { 0x5a, 0x05, 0x03, 0x31, 0x00, 0x20 }; int ret; @@ -880,39 +1072,53 @@ static int mcu_request_version(struct hid_device *hdev) return ret; ret = hid_hw_raw_request(hdev, FEATURE_REPORT_ID, response, - ROG_ALLY_REPORT_SIZE, HID_FEATURE_REPORT, + FEATURE_KBD_REPORT_SIZE, HID_FEATURE_REPORT, HID_REQ_GET_REPORT); if (ret < 0) return ret; - ret = mcu_parse_version_string(response, ROG_ALLY_REPORT_SIZE); + ret = mcu_parse_version_string(response, FEATURE_KBD_REPORT_SIZE); if (ret < 0) { pr_err("Failed to parse MCU version: %d\n", ret); print_hex_dump(KERN_ERR, "MCU: ", DUMP_PREFIX_NONE, - 16, 1, response, ROG_ALLY_REPORT_SIZE, false); + 16, 1, response, FEATURE_KBD_REPORT_SIZE, false); } return ret; } +/* Minimum MCU FW versions that no longer need the WMI suspend quirk. */ +static const struct { + u16 product; + int min_version; +} asus_mcu_min_versions[] = { + { USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY, 319 }, + { USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X, 313 }, +}; + +static int asus_mcu_min_version(u16 id_product) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(asus_mcu_min_versions); i++) { + if (asus_mcu_min_versions[i].product == id_product) + return asus_mcu_min_versions[i].min_version; + } + + return 0; +} + static void validate_mcu_fw_version(struct hid_device *hdev, int idProduct) { - int min_version, version; + int min_version = asus_mcu_min_version(idProduct); + int version; version = mcu_request_version(hdev); if (version < 0) return; - switch (idProduct) { - case USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY: - min_version = ROG_ALLY_MIN_MCU; - break; - case USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X: - min_version = ROG_ALLY_X_MIN_MCU; - break; - default: - min_version = 0; - } + if (!min_version) + return; if (version < min_version) { hid_warn(hdev, @@ -939,6 +1145,48 @@ static bool asus_has_report_id(struct hid_device *hdev, u16 report_id) return false; } +/* + * Sibling USB interface with HID Lighting (LampArray) only — no Aura 0x5d. + * Bound by this driver without hidraw so lighting stays in-kernel DL. + */ +static int asus_lamparray_detect_base(struct hid_device *hdev) +{ + u8 base; + + if (asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID1) || + asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID2)) + return -ENODEV; + + for (base = 0; base <= 0x40; base += 0x40) { + if (asus_has_report_id(hdev, base + ASUS_LAMPARRAY_RID_ATTR) && + asus_has_report_id(hdev, base + ASUS_LAMPARRAY_RID_MULTI) && + asus_has_report_id(hdev, base + ASUS_LAMPARRAY_RID_CONTROL)) + return base; + } + + return -ENODEV; +} + +static bool asus_is_lamparray_interface(struct hid_device *hdev) +{ + return asus_lamparray_detect_base(hdev) >= 0; +} + +/* + * Slash is identified by HID feature reports, never DMI board lists: + * - report 0x5e is the dedicated Slash feature report on Aura keyboards + * - standalone Slash MCU reuses USB 0x193b (also AniMe) and talks over 0x5d + * when 0x5e is absent; AniMe is rejected earlier because it has neither + */ +static bool asus_device_has_slash(struct hid_device *hdev) +{ + if (asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID2)) + return true; + + return hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_SLASH && + asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID1); +} + static int asus_kbd_register_leds(struct hid_device *hdev) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); @@ -979,213 +1227,2086 @@ 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, -}; - -#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) +static int asus_aura_commit(struct asus_drvdata *drvdata) { - struct asus_drvdata *drvdata = power_supply_get_drvdata(psy); - int ret = 0; + 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; - 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; - } + /* + * 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; - return ret; + 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_battery_probe(struct hid_device *hdev) +static int asus_aura_query_run_mode(struct asus_drvdata *drvdata, u8 selector, + u8 *buf, size_t buf_size) { - 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; + u8 req[AURA_FEATURE_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, + AURA_CMD_RUN_MODE, + 0x01, + selector, + }; + int ret; - 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_set_feature(drvdata, req, sizeof(req)); + if (ret < 0) + return ret; - drvdata->battery_next_query = jiffies; + memset(buf, 0, buf_size); + buf[0] = FEATURE_KBD_LED_REPORT_ID1; - 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"); + ret = asus_aura_get_feature(drvdata, buf, buf_size); + if (ret < 0) return ret; - } - power_supply_powers(drvdata->battery, &hdev->dev); + 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_input_configured(struct hid_device *hdev, struct hid_input *hi) +static int asus_aura_get_effect_mask(struct asus_drvdata *drvdata, u8 effect_mask[2]) { - struct input_dev *input = hi->input; - struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + u8 buf[AURA_FEATURE_REPORT_SIZE]; + int ret; - /* 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; + ret = asus_aura_query_run_mode(drvdata, 0x20, buf, sizeof(buf)); + if (ret >= 22) + goto found; - /* 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 */ - } - } + ret = asus_aura_query_run_mode(drvdata, 0x15, buf, sizeof(buf)); + if (ret < 0) + return ret; + if (ret < 22) + return -ENODATA; - if (drvdata->tp) { - int ret; +found: + effect_mask[0] = buf[20]; + effect_mask[1] = buf[21]; - 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); + return 0; +} - 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); - } +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); - __set_bit(BTN_LEFT, input->keybit); - __set_bit(INPUT_PROP_BUTTONPAD, input->propbit); + if (direct_capable) + supported_effects |= BIT(DL_EFFECT_DIRECT); - ret = input_mt_init_slots(input, drvdata->tp->max_contacts, - INPUT_MT_POINTER); + return supported_effects; +} + +static unsigned int +asus_aura_supported_effects_from_mask(const u8 effect_mask[2], bool direct_capable) +{ + 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; +} + +static int asus_aura_activate_zone_unlocked(struct asus_drvdata *drvdata, u8 zone) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE] = { 0 }; + + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + buf[1] = AURA_CMD_ZONE_ENABLE; + buf[2] = zone; + buf[3] = 0x01; + buf[4] = 0x01; + + return asus_aura_set_feature_unlocked(drvdata, buf, sizeof(buf)); +} + +static int asus_aura_activate_zone(struct asus_drvdata *drvdata, u8 zone) +{ + guard(mutex)(&drvdata->aura_lock); + + return asus_aura_activate_zone_unlocked(drvdata, zone); +} + +static int asus_aura_activate_zones(struct asus_drvdata *drvdata) +{ + int ret; + + ret = asus_aura_activate_zone(drvdata, AURA_ZONE_ACTIVATE_KEYBOARD); + if (ret < 0) + return ret; + if (drvdata->has_lightbar) + return asus_aura_activate_zone(drvdata, AURA_ZONE_ACTIVATE_LIGHTBAR); + return 0; +} + +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; + + /* 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; + + /* 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 enum asus_aura_mode asus_aura_effective_mode(struct asus_drvdata *drvdata); +static int asus_aura_apply_effect(struct led_classdev_dynamic *ldev, + enum dl_effect_mode mode, + enum led_brightness brightness); + +static void asus_aura_restore(struct asus_drvdata *drvdata) +{ + int ret; + + if (!drvdata->has_dldev_kbd && !drvdata->has_dldev_global) + return; + + ret = asus_aura_activate_zones(drvdata); + if (ret < 0) + hid_warn(drvdata->hdev, "Failed to activate Aura zones: %d\n", ret); + + 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); + return; + } + + 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); +} + +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; + } + + 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 (commit) + return asus_aura_commit(drvdata); + + return 0; +} + +static size_t asus_lamparray_report_len(struct hid_device *hdev, u8 id) +{ + struct hid_report *report; + + report = hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[id]; + if (!report) + return 0; + return hid_report_len(report); +} + +static u8 asus_lamparray_rid(struct asus_drvdata *drvdata, u8 offset) +{ + return drvdata->lamparray_rid_base + offset; +} + +static int asus_lamparray_raw(struct asus_drvdata *drvdata, u8 *buf, size_t len, + bool get) +{ + int ret; + + if (!drvdata->lamparray_hdev || !len) + return -ENODEV; + + ret = hid_hw_raw_request(drvdata->lamparray_hdev, buf[0], buf, len, + HID_FEATURE_REPORT, + get ? HID_REQ_GET_REPORT : HID_REQ_SET_REPORT); + return ret < 0 ? ret : 0; +} + +static void asus_lamparray_prepare(struct asus_drvdata *drvdata, u8 rid) +{ + memset(drvdata->lamparray_buf, 0, drvdata->lamparray_buf_len); + drvdata->lamparray_buf[0] = rid; +} + +static struct hid_device *asus_find_lamparray_sibling(struct hid_device *hdev) +{ + struct usb_interface *intf; + struct usb_device *udev; + struct usb_host_config *config; + unsigned int i; + + if (!hid_is_usb(hdev)) + return NULL; + + intf = to_usb_interface(hdev->dev.parent); + udev = interface_to_usbdev(intf); + if (!udev->actconfig) + return NULL; + + config = udev->actconfig; + for (i = 0; i < config->desc.bNumInterfaces; i++) { + struct usb_interface *other = config->interface[i]; + struct hid_device *other_hdev; + + if (!other || other == intf) + continue; + + other_hdev = usb_get_intfdata(other); + if (!other_hdev) + continue; + if (other_hdev->vendor != hdev->vendor || + other_hdev->product != hdev->product) + continue; + if (asus_is_lamparray_interface(other_hdev)) + return other_hdev; + } + + return NULL; +} + +static void asus_lamparray_unbind_from_owners(struct hid_device *la_hdev) +{ + struct usb_interface *intf; + struct usb_device *udev; + struct usb_host_config *config; + unsigned int i; + + if (!hid_is_usb(la_hdev)) + return; + + intf = to_usb_interface(la_hdev->dev.parent); + udev = interface_to_usbdev(intf); + if (!udev->actconfig) + return; + + config = udev->actconfig; + for (i = 0; i < config->desc.bNumInterfaces; i++) { + struct usb_interface *other = config->interface[i]; + struct hid_device *other_hdev; + struct asus_drvdata *owner; + + if (!other || other == intf) + continue; + + other_hdev = usb_get_intfdata(other); + if (!other_hdev || other_hdev->driver != la_hdev->driver) + continue; + + owner = hid_get_drvdata(other_hdev); + if (!owner) + continue; + + mutex_lock(&owner->aura_lock); + if (owner->lamparray_hdev == la_hdev) { + owner->lamparray_controlled = false; + owner->lamparray_count = 0; + owner->lamparray_lamps = NULL; + owner->lamparray_hdev = NULL; + owner->lamparray_unavailable = true; + mutex_unlock(&owner->aura_lock); + put_device(&la_hdev->dev); + return; + } + mutex_unlock(&owner->aura_lock); + } +} + +static int asus_lamparray_cmp_x(const void *a, const void *b) +{ + const struct asus_lamparray_lamp *la = a; + const struct asus_lamparray_lamp *lb = b; + + return la->x - lb->x; +} + +static int asus_lamparray_init_from_hdev(struct asus_drvdata *drvdata, + struct hid_device *la_hdev) +{ + u8 *buf; + size_t attr_len, req_len, resp_len, multi_len, ctrl_len, max_len; + unsigned int count, i; + int base, ret; + + base = asus_lamparray_detect_base(la_hdev); + if (base < 0) + return base; + + attr_len = asus_lamparray_report_len(la_hdev, base + ASUS_LAMPARRAY_RID_ATTR); + req_len = asus_lamparray_report_len(la_hdev, base + ASUS_LAMPARRAY_RID_REQUEST); + resp_len = asus_lamparray_report_len(la_hdev, base + ASUS_LAMPARRAY_RID_RESPONSE); + multi_len = asus_lamparray_report_len(la_hdev, base + ASUS_LAMPARRAY_RID_MULTI); + ctrl_len = asus_lamparray_report_len(la_hdev, base + ASUS_LAMPARRAY_RID_CONTROL); + max_len = max3(max(attr_len, req_len), max(resp_len, multi_len), ctrl_len); + if (attr_len < 3 || req_len < 3 || resp_len < 23 || multi_len < 51 || + ctrl_len < 2 || !max_len) + return -EPROTO; + + buf = devm_kzalloc(&drvdata->hdev->dev, max_len, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + drvdata->lamparray_hdev = la_hdev; + drvdata->lamparray_buf = buf; + drvdata->lamparray_buf_len = max_len; + drvdata->lamparray_rid_base = base; + + asus_lamparray_prepare(drvdata, base + ASUS_LAMPARRAY_RID_ATTR); + ret = asus_lamparray_raw(drvdata, buf, attr_len, true); + if (ret < 0) + return ret; + + count = get_unaligned_le16(buf + 1); + if (!count || count > ASUS_LAMPARRAY_MAX_LAMPS) + return -EPROTO; + + drvdata->lamparray_lamps = devm_kcalloc(&drvdata->hdev->dev, count, + sizeof(*drvdata->lamparray_lamps), + GFP_KERNEL); + if (!drvdata->lamparray_lamps) + return -ENOMEM; + + for (i = 0; i < count; i++) { + u32 purposes; + + asus_lamparray_prepare(drvdata, base + ASUS_LAMPARRAY_RID_REQUEST); + put_unaligned_le16(i, buf + 1); + ret = asus_lamparray_raw(drvdata, buf, req_len, false); + if (ret < 0) + return ret; + + asus_lamparray_prepare(drvdata, base + ASUS_LAMPARRAY_RID_RESPONSE); + ret = asus_lamparray_raw(drvdata, buf, resp_len, true); + if (ret < 0) + return ret; + + /* LampId@1, PositionX@3, LampPurposes@19 (HID Lighting) */ + drvdata->lamparray_lamps[i].id = get_unaligned_le16(buf + 1); + drvdata->lamparray_lamps[i].x = (s32)get_unaligned_le32(buf + 3); + purposes = get_unaligned_le32(buf + 19); + drvdata->lamparray_lamps[i].keyboard = + !!(purposes & ASUS_LAMPARRAY_PURPOSE_CONTROL); + } + + drvdata->lamparray_count = count; + sort(drvdata->lamparray_lamps, count, sizeof(*drvdata->lamparray_lamps), + asus_lamparray_cmp_x, NULL); + get_device(&la_hdev->dev); + hid_info(drvdata->hdev, + "LampArray direct RGB backend (%u lamps, rid_base=0x%02x)\n", + count, base); + return 0; +} + +static void asus_lamparray_try_init_unlocked(struct asus_drvdata *drvdata) +{ + struct hid_device *sibling; + int ret; + + if (drvdata->lamparray_count || drvdata->lamparray_unavailable) + return; + + sibling = asus_find_lamparray_sibling(drvdata->hdev); + if (!sibling) + return; + + ret = asus_lamparray_init_from_hdev(drvdata, sibling); + if (ret < 0) { + hid_warn(drvdata->hdev, "LampArray init failed: %d\n", ret); + drvdata->lamparray_hdev = NULL; + drvdata->lamparray_lamps = NULL; + drvdata->lamparray_count = 0; + drvdata->lamparray_unavailable = true; + } +} + +static int asus_lamparray_set_control_unlocked(struct asus_drvdata *drvdata, + bool autonomous) +{ + u8 rid = asus_lamparray_rid(drvdata, ASUS_LAMPARRAY_RID_CONTROL); + size_t len = asus_lamparray_report_len(drvdata->lamparray_hdev, rid); + + if (len < 2) + return -EPROTO; + + asus_lamparray_prepare(drvdata, rid); + drvdata->lamparray_buf[1] = autonomous ? 0x01 : 0x00; + return asus_lamparray_raw(drvdata, drvdata->lamparray_buf, len, false); +} + +static int asus_lamparray_aura_handoff_unlocked(struct asus_drvdata *drvdata, + u8 zone, bool release) +{ + u8 aura[AURA_FEATURE_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, + AURA_CMD_ZONE_ENABLE, + zone, + 0x01, + }; + + if (release) + aura[4] = 0x01; + + return asus_aura_set_feature_unlocked(drvdata, aura, sizeof(aura)); +} + +static int asus_lamparray_take_control_unlocked(struct asus_drvdata *drvdata) +{ + int ret; + + if (drvdata->lamparray_controlled) + return 0; + + ret = asus_lamparray_aura_handoff_unlocked(drvdata, + AURA_ZONE_ACTIVATE_LAMPARRAY, + false); + if (ret < 0) + return ret; + + /* Match G-Helper: pulse then clear AutonomousMode for host control */ + ret = asus_lamparray_set_control_unlocked(drvdata, true); + if (ret < 0) + return ret; + ret = asus_lamparray_set_control_unlocked(drvdata, false); + if (ret < 0) + return ret; + + drvdata->lamparray_controlled = true; + return 0; +} + +static void asus_lamparray_release_unlocked(struct asus_drvdata *drvdata) +{ + if (!drvdata->lamparray_count || !drvdata->lamparray_controlled) + return; + + asus_lamparray_set_control_unlocked(drvdata, true); + asus_lamparray_aura_handoff_unlocked(drvdata, + AURA_ZONE_RELEASE_LAMPARRAY, + true); + drvdata->lamparray_controlled = false; +} + +static void asus_lamparray_sample_rgb(const u8 *buf, unsigned int nleds, + unsigned int idx, unsigned int n, + u8 *r, u8 *g, u8 *b) +{ + unsigned int zi; + + if (!buf || !nleds || !n) { + *r = *g = *b = 0; + return; + } + + zi = (idx * nleds) / n; + if (zi >= nleds) + zi = nleds - 1; + *r = buf[zi * 3]; + *g = buf[zi * 3 + 1]; + *b = buf[zi * 3 + 2]; +} + +static void asus_lamparray_fill_solid(u8 *buf, unsigned int nleds, + u8 r, u8 g, u8 b) +{ + unsigned int i; + + for (i = 0; i < nleds; i++) { + buf[i * 3 + 0] = r; + buf[i * 3 + 1] = g; + buf[i * 3 + 2] = b; + } +} + +static unsigned int asus_aura_lb_led_count(struct asus_drvdata *drvdata) +{ + return drvdata->is_strix_4zone ? ROG_STRIX_4ZONE_LIGHTBAR_LEDS : + ROG_STRIX_LIGHTBAR_LEDS; +} + +static void asus_aura_init_direct_bufs(struct asus_drvdata *drvdata) +{ + asus_lamparray_fill_solid(drvdata->kbd_direct_buf, ROG_STRIX_4ZONE_KBD_LEDS, + 255, 0, 0); + asus_lamparray_fill_solid(drvdata->lb_direct_buf, + asus_aura_lb_led_count(drvdata), 255, 0, 0); +} + +static int asus_lamparray_apply_unlocked(struct asus_drvdata *drvdata) +{ + unsigned int kbd_n = 0, lb_n = 0, kbd_i = 0, lb_i = 0, i; + unsigned int kbd_leds = ROG_STRIX_4ZONE_KBD_LEDS; + unsigned int lb_leds = asus_aura_lb_led_count(drvdata); + u8 *buf = drvdata->lamparray_buf; + u8 rid_multi = asus_lamparray_rid(drvdata, ASUS_LAMPARRAY_RID_MULTI); + size_t multi_len; + int ret; + + ret = asus_lamparray_take_control_unlocked(drvdata); + if (ret < 0) + return ret; + + for (i = 0; i < drvdata->lamparray_count; i++) { + if (drvdata->lamparray_lamps[i].keyboard) + kbd_n++; + else + lb_n++; + } + + multi_len = asus_lamparray_report_len(drvdata->lamparray_hdev, rid_multi); + if (multi_len < 51) + return -EPROTO; + + for (i = 0; i < drvdata->lamparray_count; i += ASUS_LAMPARRAY_MULTI_MAX) { + unsigned int n = min_t(unsigned int, ASUS_LAMPARRAY_MULTI_MAX, + drvdata->lamparray_count - i); + unsigned int j; + unsigned int id_off = 3; + unsigned int col_off = 3 + ASUS_LAMPARRAY_MULTI_MAX * 2; + + asus_lamparray_prepare(drvdata, rid_multi); + buf[1] = n; + buf[2] = (i + n >= drvdata->lamparray_count) ? + ASUS_LAMPARRAY_FLAG_COMPLETE : 0; + + for (j = 0; j < n; j++) { + unsigned int lamp = i + j; + u8 r, g, b; + u16 id = drvdata->lamparray_lamps[lamp].id; + + if (drvdata->lamparray_lamps[lamp].keyboard) { + asus_lamparray_sample_rgb(drvdata->kbd_direct_buf, + kbd_leds, kbd_i++, kbd_n, + &r, &g, &b); + } else { + asus_lamparray_sample_rgb(drvdata->lb_direct_buf, + lb_leds, lb_i++, lb_n, + &r, &g, &b); + } + + put_unaligned_le16(id, buf + id_off + j * 2); + buf[col_off + j * 4 + 0] = r; + buf[col_off + j * 4 + 1] = g; + buf[col_off + j * 4 + 2] = b; + buf[col_off + j * 4 + 3] = 0xff; + } + + ret = asus_lamparray_raw(drvdata, buf, multi_len, false); + if (ret < 0) + return ret; + } + + return 0; +} + +/* Prefer LampArray; callers keep Aura 0xBC as fallback on -ENODEV. */ +static int asus_lamparray_try_apply_unlocked(struct asus_drvdata *drvdata) +{ + asus_lamparray_try_init_unlocked(drvdata); + if (!drvdata->lamparray_count) + return -ENODEV; + return asus_lamparray_apply_unlocked(drvdata); +} + +static int asus_aura_write_4zone_direct_bc_unlocked(struct asus_drvdata *drvdata) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE]; + + 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)); +} + +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); + + ret = asus_lamparray_try_apply_unlocked(drvdata); + if (ret != -ENODEV) + return ret; + + return asus_aura_write_4zone_direct_bc_unlocked(drvdata); + } + + /* + * 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 int asus_lamparray_apply_solid_unlocked(struct asus_drvdata *drvdata, + struct led_classdev_dynamic *ldev, + u8 r, u8 g, u8 b) +{ + unsigned int lb_leds = asus_aura_lb_led_count(drvdata); + + asus_lamparray_try_init_unlocked(drvdata); + if (!drvdata->lamparray_count) + return -ENODEV; + + if (asus_aura_is_global(drvdata, ldev)) { + asus_lamparray_fill_solid(drvdata->kbd_direct_buf, + ROG_STRIX_4ZONE_KBD_LEDS, r, g, b); + asus_lamparray_fill_solid(drvdata->lb_direct_buf, lb_leds, r, g, b); + } else if (asus_aura_is_lightbar(drvdata, ldev)) { + asus_lamparray_fill_solid(drvdata->lb_direct_buf, lb_leds, r, g, b); + } else { + asus_lamparray_fill_solid(drvdata->kbd_direct_buf, + ROG_STRIX_4ZONE_KBD_LEDS, r, g, b); + } + + return asus_lamparray_apply_unlocked(drvdata); +} + +static int asus_aura_write_zone_range(struct asus_drvdata *drvdata, + u8 zone_first, u8 zone_last, + u8 aura_effect, u8 r, u8 g, u8 b, + u8 speed, u8 direction, + u8 r2, u8 g2, u8 b2) +{ + u8 z; + int ret; + + for (z = zone_first; z <= zone_last; z++) { + ret = asus_aura_write_zone_effect(drvdata, z, aura_effect, + r, g, b, speed, direction, + r2, g2, b2, false); + if (ret < 0) + return ret; + } + + return asus_aura_commit(drvdata); +} + +static enum asus_aura_mode asus_aura_effective_mode(struct asus_drvdata *drvdata) +{ + enum asus_aura_mode mode = READ_ONCE(drvdata->aura_mode); + + /* + * auto resolves to split: keyboard and lightbar (when present) stay + * independently writable. Userspace can select unified for a single + * global effect across zones. + */ + if (mode == AURA_MODE_AUTO) + return 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; + } + + 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; + int ret; + + 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); + + ret = asus_lamparray_try_apply_unlocked(drvdata); + if (ret != -ENODEV) + return ret; + + return asus_aura_write_4zone_direct_bc_unlocked(drvdata); + } + + 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; + } + + 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_EFFECT_STATIC; + r = 0; + g = 0; + b = 0; + r2 = 0; + g2 = 0; + b2 = 0; + } else { + switch (mode) { + case DL_EFFECT_STATIC: + aura_mode = AURA_EFFECT_STATIC; + break; + case DL_EFFECT_BREATHING: + aura_mode = AURA_EFFECT_BREATHING; + break; + case DL_EFFECT_STROBE: + aura_mode = AURA_EFFECT_STROBING; + break; + case DL_EFFECT_SPECTRUM_CYCLE: + aura_mode = AURA_EFFECT_SPECTRUM_CYCLE; + break; + case DL_EFFECT_RAINBOW: + aura_mode = AURA_EFFECT_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; + + /* + * Solid colours via LampArray (independent keyboard/lightbar). + * Firmware animations release LampArray and use Aura 0xb3. + */ + { + guard(mutex)(&drvdata->aura_lock); + + if (aura_mode == AURA_EFFECT_STATIC) { + ret = asus_lamparray_apply_solid_unlocked(drvdata, ldev, r, g, b); + if (ret != -ENODEV) + return ret; + } else { + asus_lamparray_try_init_unlocked(drvdata); + if (drvdata->lamparray_count) + asus_lamparray_release_unlocked(drvdata); + } + } + + if (asus_aura_is_global(drvdata, ldev)) + return asus_aura_write_zone_effect(drvdata, AURA_ZONE_ALL, aura_mode, + r, g, b, speed, direction, + r2, g2, b2, true); + + if (asus_aura_is_lightbar(drvdata, ldev)) + return asus_aura_write_zone_range(drvdata, AURA_ZONE_BAR_LEFT, + AURA_ZONE_BAR_RIGHT, aura_mode, + r, g, b, speed, direction, + r2, g2, b2); + + if (drvdata->is_strix_4zone) + return asus_aura_write_zone_range(drvdata, AURA_ZONE_KEY1, + AURA_ZONE_KEY4, aura_mode, + r, g, b, speed, direction, + r2, g2, b2); + + 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_activate_zones(drvdata); + if (ret < 0) + hid_warn(drvdata->hdev, "Failed to activate 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 int asus_aura_set_power_states(struct led_classdev_dynamic *ldev, + u32 active_states) +{ + struct asus_drvdata *drvdata = ldev->driver_data; + u8 buf[AURA_FEATURE_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, + AURA_CMD_POWER, + AURA_POWER_CMD_ENABLE, + }; + u8 kbd = 0; + u8 lightbar = 0; + + if (active_states & DL_POWER_STATE_BOOT) { + kbd |= AURA_POWER_KBD_BOOT; + lightbar |= AURA_POWER_LB_BOOT; + } + if (active_states & DL_POWER_STATE_AWAKE) { + kbd |= AURA_POWER_KBD_AWAKE; + lightbar |= AURA_POWER_LB_AWAKE; + } + if (active_states & DL_POWER_STATE_SLEEP) { + kbd |= AURA_POWER_KBD_SLEEP; + lightbar |= AURA_POWER_LB_SLEEP; + } + if (active_states & DL_POWER_STATE_SHUTDOWN) { + kbd |= AURA_POWER_KBD_SHUTDOWN; + lightbar |= AURA_POWER_LB_SHUTDOWN; + } + + /* + * Map generic DL boot/awake/sleep/shutdown onto keyboard bits, and also + * lightbar bits when the chassis lightbar is present. Leave lid/rear + * enabled so unmanaged zones are not accidentally gated off. + */ + buf[3] = kbd | AURA_POWER_MASK_LOGO_ALL; + buf[4] = drvdata->has_lightbar ? lightbar : 0; + buf[5] = AURA_POWER_MASK_LID_ALL; + buf[6] = AURA_POWER_MASK_REAR_ALL; + + return asus_aura_set_feature(drvdata, buf, sizeof(buf)); +} + +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, + .set_power_states = asus_aura_set_power_states, + .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, + .set_power_states = asus_aura_set_power_states, + .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, + .set_power_states = asus_aura_set_power_states, + .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, + .set_power_states = asus_aura_set_power_states, + .direct_write = asus_aura_lightbar_set_direct, +}; + +static int asus_aura_discover(struct asus_drvdata *drvdata, bool *has_lightbar, + bool *is_strix_4zone, bool *is_per_key) +{ + 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; + *is_per_key = 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); + *is_per_key = (buf[9] == 0x03); + *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) +{ + if (asus_has_report_id(drvdata->hdev, FEATURE_KBD_LED_REPORT_ID2)) + return FEATURE_KBD_LED_REPORT_ID2; + return 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(struct hid_device *hdev) +{ + return asus_device_has_slash(hdev); +} + +static int asus_init_slash(struct hid_device *hdev) +{ + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + int ret; + + if (!asus_has_slash_lighting(hdev)) + 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 void asus_aura_dldev_set_default_palette(struct led_classdev_dynamic *ldev) +{ + if (!ldev->palette) + return; + + ldev->palette[0].r = 255; + ldev->palette[0].g = 0; + ldev->palette[0].b = 0; + ldev->num_palette_entries = 1; +} + +static void asus_aura_dldev_common_init(struct led_classdev_dynamic *ldev, + struct asus_drvdata *drvdata, + const char *name, + const struct led_dynamic_ops *ops, + unsigned int effects) +{ + ldev->cdev.name = name; + ldev->cdev.max_brightness = 255; + ldev->cdev.brightness = 255; + ldev->cdev.brightness_set_blocking = asus_aura_brightness_set_blocking; + ldev->cdev.groups = asus_aura_groups; + ldev->ops = ops; + ldev->driver_data = drvdata; + ldev->speed = 1; + ldev->max_speed = 2; + ldev->direction = DL_DIRECTION_RIGHT; + ldev->supported_directions = BIT(DL_DIRECTION_RIGHT) | + BIT(DL_DIRECTION_LEFT); + ldev->max_palette_entries = 2; + ldev->current_effect = (effects & BIT(DL_EFFECT_STATIC)) ? + DL_EFFECT_STATIC : DL_EFFECT_OFF; + ldev->supported_effects = effects; + ldev->supported_power_states = DL_POWER_STATE_ALL; + ldev->active_power_states = DL_POWER_STATE_ALL; +} + +static int asus_init_dynamic_lighting(struct hid_device *hdev) +{ + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + unsigned int global_effects; + unsigned int kbd_effects; + unsigned int lightbar_effects; + u8 effect_mask[2]; + bool is_per_key = false; + bool has_lightbar = false; + bool kbd_direct; + bool lightbar_direct; + int ret; + + ret = asus_aura_discover(drvdata, &has_lightbar, &drvdata->is_strix_4zone, + &is_per_key); + if (ret == -ENODEV) + return 0; + if (ret < 0) { + hid_warn(hdev, "Aura device discovery failed: %d\n", ret); + return 0; + } + + asus_aura_init_direct_bufs(drvdata); + + ret = asus_aura_wake_all_zones(drvdata); + if (ret < 0) + hid_warn(hdev, "Failed to wake Aura hardware zones: %d\n", ret); + + scoped_guard(mutex, &drvdata->aura_lock) + asus_lamparray_try_init_unlocked(drvdata); + + kbd_direct = is_per_key || drvdata->is_strix_4zone || + drvdata->lamparray_hdev != NULL; + 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(kbd_direct); + } 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, kbd_direct); + } + + drvdata->has_lightbar = has_lightbar; + drvdata->aura_mode = AURA_MODE_AUTO; + + /* Keyboard Dynamic Lighting zone: always registered on Aura models. */ + asus_aura_dldev_common_init(&drvdata->dldev_kbd, drvdata, "aura:keyboard", + drvdata->is_strix_4zone ? + &asus_aura_kbd_4zone_ops : &asus_aura_kbd_ops, + 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 { + drvdata->dldev_kbd.zone_type = "keyboard"; + if (drvdata->is_strix_4zone) + drvdata->dldev_kbd.led_count = ROG_STRIX_4ZONE_KBD_LEDS; + } + + 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; + asus_aura_dldev_set_default_palette(&drvdata->dldev_kbd); + + if (has_lightbar) { + asus_aura_dldev_common_init(&drvdata->dldev_lightbar, drvdata, + "aura:lightbar", + &asus_aura_lightbar_ops, + lightbar_effects); + drvdata->dldev_lightbar.zone_type = "lightbar"; + drvdata->dldev_lightbar.led_count = asus_aura_lb_led_count(drvdata); + + 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; + asus_aura_dldev_set_default_palette(&drvdata->dldev_lightbar); + } + + asus_aura_dldev_common_init(&drvdata->dldev_global, drvdata, + "aura:global", + &asus_aura_global_ops, + global_effects); + drvdata->dldev_global.zone_type = "global"; + 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; + asus_aura_dldev_set_default_palette(&drvdata->dldev_global); + } + } + + 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); @@ -1365,6 +3486,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 || drvdata->has_dldev_global) + asus_aura_restore(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 +3506,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 || drvdata->has_dldev_global) + asus_aura_restore(drvdata); +#endif + if (drvdata->tp) return asus_start_multitouch(hdev); @@ -1400,6 +3531,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; /* @@ -1474,6 +3616,18 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) return ret; } + /* + * USB 0x193b is reused by AniMe Matrix. Bind only LampArray or + * interfaces that expose Aura/Slash LED reports. + */ + if (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_SLASH && + !asus_is_lamparray_interface(hdev) && + !asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID1) && + !asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID2)) { + hid_dbg(hdev, "Skipping 0x193b without Aura/Slash LED reports\n"); + return -ENODEV; + } + /* Check for vendor for RGB init and handle generic devices properly. */ rep_enum = &hdev->report_enum[HID_INPUT_REPORT]; list_for_each_entry(rep, &rep_enum->report_list, list) { @@ -1481,6 +3635,21 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) is_vendor = true; } + /* + * LampArray is a Dynamic Lighting backend owned in-kernel: do not + * export hidraw for that interface. Aura 0xBC remains the fallback + * when LampArray is absent. + */ + if (asus_is_lamparray_interface(hdev)) { + ret = hid_hw_start(hdev, 0); + if (ret) { + hid_err(hdev, "Asus LampArray hw start failed: %d\n", ret); + return ret; + } + hid_info(hdev, "Bound ASUS LampArray interface (no hidraw)\n"); + return 0; + } + /* * A vendor collection may be the only application collection on the * interface, which hidinput_connect() otherwise skips, leaving the @@ -1519,6 +3688,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 lighting: %d\n", ret); + } + /* * For ROG keyboards, skip rename for consistency and ->input check as * some devices do not have inputs. @@ -1562,6 +3742,24 @@ static void asus_remove(struct hid_device *hdev) if (drvdata->listener.brightness_set) asus_hid_unregister_listener(&drvdata->listener); +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (asus_is_lamparray_interface(hdev)) { + asus_lamparray_unbind_from_owners(hdev); + hid_hw_stop(hdev); + return; + } + if (drvdata->lamparray_hdev) { + scoped_guard(mutex, &drvdata->aura_lock) { + asus_lamparray_release_unlocked(drvdata); + if (drvdata->lamparray_hdev) { + put_device(&drvdata->lamparray_hdev->dev); + drvdata->lamparray_hdev = NULL; + } + drvdata->lamparray_count = 0; + } + } +#endif + asus_worker_stop(drvdata->worker); hid_hw_stop(hdev); } @@ -1698,6 +3896,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..4c5f47dd02597c 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -226,6 +226,8 @@ #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 +/* 0x193b is also AniMe Matrix; hid-asus binds it only when Aura/Slash LED reports exist. */ +#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 02dc9d381cd44dd04690a65bf71c71c3efb7eefd Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Sat, 12 Sep 2026 18:41:06 +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 via devm_led_classdev_dynamic_register with effects, speed, palette, and power-state persistence through ACPI WMI methods. Map DL_POWER_STATE_* bits directly; they are already BIT(n) values. Always initialize the keyboard LED cdev name before queuing registration work so HID listener registration cannot oops on a NULL name when WMI kbd backlight setup was skipped. Preserve legacy kbd_rgb_mode and kbd_rgb_state sysfs attributes for backward compatibility. Signed-off-by: Marco Scardovi --- drivers/platform/x86/Kconfig | 1 + drivers/platform/x86/asus-wmi.c | 328 ++++++++++++++++++++++++++++++-- 2 files changed, 312 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..84fa2a7444d12b 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 & DL_POWER_STATE_BOOT) + flags |= BIT(1); + if (active_states & DL_POWER_STATE_AWAKE) + flags |= BIT(3); + if (active_states & 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 |= DL_POWER_STATE_BOOT; + if (awake) + states |= DL_POWER_STATE_AWAKE; + if (sleep) + states |= DL_POWER_STATE_SLEEP; + + guard(mutex)(&ldev->lock); + ldev->active_power_states = states; + } +#endif + return count; } static DEVICE_ATTR_WO(kbd_rgb_state); @@ -1772,9 +2001,31 @@ static void kbd_led_update_all(struct work_struct *work) * completed and asus-wmi will keep running until it finishes. * Therefore, we can safely register the LED without holding * a spinlock. + * + * Guard against a NULL cdev.name: registration can be queued + * for HID listeners even when WMI kbd backlight setup was + * skipped; led_classdev_register_ext() strnlen()s the name. */ - ret = devm_led_classdev_register(&asus->platform_device->dev, - &asus->kbd_led); +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (asus->kbd_rgb_dev) { + if (!asus->kbd_dldev.cdev.name) + asus->kbd_dldev.cdev.name = "asus::kbd_backlight"; + 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 + { + if (!asus->kbd_led.name) + asus->kbd_led.name = "asus::kbd_backlight"; + 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 +2036,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 +2165,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 +2203,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 +2368,61 @@ 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; + /* + * Always initialize kbd_led (or TUF DL cdev) before queueing work. + * Registration can run for HID listeners even when WMI kbd backlight + * is absent (kbd_led_avail == false); a NULL cdev.name oopses in + * led_classdev_register_ext() via strnlen(). + */ +#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 = + DL_POWER_STATE_BOOT | + DL_POWER_STATE_AWAKE | + DL_POWER_STATE_SLEEP; + dldev->active_power_states = + dldev->supported_power_states; + } + } 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; + } + 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 (num_rgb_groups != 0) { +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (asus->kbd_rgb_dev) + asus->kbd_dldev.cdev.groups = kbd_rgb_mode_groups; + else +#endif + asus->kbd_led.groups = kbd_rgb_mode_groups; + } } else { asus->kbd_led_wk = -1; } @@ -5336,7 +5630,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 +5671,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 907aeed7429878700b2146ef0f9e59731dcccdef 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 Aura RGB Dynamic Lighting 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. direct_write updates current_mode after a successful apply. Signed-off-by: Marco Scardovi --- MAINTAINERS | 1 + drivers/leds/Kconfig | 15 + drivers/leds/Makefile | 1 + drivers/leds/leds-asus-aura-scsi.c | 514 +++++++++++++++++++++++++++++ 4 files changed, 531 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..b95a1fc777489f --- /dev/null +++ b/drivers/leds/leds-asus-aura-scsi.c @@ -0,0 +1,514 @@ +// 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. + * Discovery does not use scsi_dh; INQUIRY matching is sufficient. + * + * 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 */ + ret = ene_write(sdev, ENE_REG_APPLY, &apply, 1); + if (ret) + return ret; + + zone->current_mode = mode; + return 0; +} + +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");