From ded386c1f32c6926a4af9c88ee8c6784068e6993 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 19 Oct 2026 10:12:16 +0200 Subject: [PATCH 1/7] leds: Add Dynamic Lighting class interface Add a dedicated Dynamic Lighting LED class for devices that expose multi-LED effects, palette programming, direct RGB streaming or lighting state persistence through sysfs. Define LED_DYNAMIC_LIGHTING on struct led_classdev and an optional led_dynamic back-pointer so the class can wrap a new LED or attach to an already registered one without replacing brightness or multi_intensity. Drivers supply their own effect name table and optional ops. Sysfs exposes only implemented attributes. Lighting off is enabled=false. Registration validates capabilities and serializes writes under led_access and a class-private lock. Re-registration without unregister is rejected; host attach rejects parent/init_data. direct_sample_size selects bytes per LED in direct_buffer (default 3). led_dynamic_fill_effects fails instead of truncating. KUnit covers host attach, enabled, power_states, bounds, direct_buffer and failed ops that must not mutate state. Signed-off-by: Marco Scardovi --- MAINTAINERS | 10 + drivers/leds/.kunitconfig | 1 + drivers/leds/Kconfig | 9 + drivers/leds/Makefile | 14 + drivers/leds/led-class-dynamic-test.c | 672 ++++++++++++++++++ drivers/leds/led-class-dynamic.c | 956 ++++++++++++++++++++++++++ include/linux/led-dynamic-lighting.h | 345 ++++++++++ include/linux/leds.h | 11 + 8 files changed, 2018 insertions(+) create mode 100644 drivers/leds/led-class-dynamic-test.c 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 1e0d90fe2220a0..a857e8e07d0e8d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11856,6 +11856,7 @@ F: include/linux/pm.h F: include/linux/suspend.h F: kernel/power/ + HID CORE LAYER M: Jiri Kosina M: Benjamin Tissoires @@ -15129,6 +15130,15 @@ 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-test.c +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/.kunitconfig b/drivers/leds/.kunitconfig index 5180f77910a115..4b5e48470c157e 100644 --- a/drivers/leds/.kunitconfig +++ b/drivers/leds/.kunitconfig @@ -1,4 +1,5 @@ CONFIG_KUNIT=y CONFIG_NEW_LEDS=y CONFIG_LEDS_CLASS=y +CONFIG_LEDS_CLASS_DYNAMIC=y CONFIG_LEDS_KUNIT_TEST=y diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index 7ec2c8d7854261..0dfd58a5dcd5af 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -46,6 +46,15 @@ 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 (effect, palette, per-key RGB streaming, and power + state persistence). + 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 4d4b089156e24a..efbe744a088d45 100644 --- a/drivers/leds/Makefile +++ b/drivers/leds/Makefile @@ -5,6 +5,20 @@ 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 +# Follow LEDS_KUNIT_TEST. Skip the built-in test when the class is modular, +# so vmlinux does not reference a module. +ifeq ($(CONFIG_LEDS_CLASS_DYNAMIC),y) +ifeq ($(CONFIG_LEDS_KUNIT_TEST),y) +obj-y += led-class-dynamic-test.o +endif +ifeq ($(CONFIG_LEDS_KUNIT_TEST),m) +obj-m += led-class-dynamic-test.o +endif +endif +ifeq ($(CONFIG_LEDS_CLASS_DYNAMIC)$(CONFIG_LEDS_KUNIT_TEST),mm) +obj-m += led-class-dynamic-test.o +endif obj-$(CONFIG_LEDS_TRIGGERS) += led-triggers.o obj-$(CONFIG_LEDS_KUNIT_TEST) += led-test.o diff --git a/drivers/leds/led-class-dynamic-test.c b/drivers/leds/led-class-dynamic-test.c new file mode 100644 index 00000000000000..138cb6347261a5 --- /dev/null +++ b/drivers/leds/led-class-dynamic-test.c @@ -0,0 +1,672 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * KUnit tests for the Dynamic Lighting LED class. + * + * Copyright (C) 2026 Open Gaming Collective + * Author: Marco Scardovi + * Author: Denis Benato + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +static const char * const dl_test_effects[] = { + "static", + "breathe", + "direct", +}; + +static const char * const dl_test_pool[] = { + "static", + "breathe", + "wave", + "pulse", +}; + +struct dl_test_priv { + struct led_classdev_dynamic ldev; + struct device *dev; + int fail_op; + unsigned int last_effect; + unsigned int last_speed; + enum dl_direction last_direction; + bool last_enabled; + u32 last_power_states; + size_t last_direct_len; +}; + +static int dl_test_set_effect(struct led_classdev_dynamic *ldev, unsigned int index) +{ + struct dl_test_priv *priv = ldev->driver_data; + + if (priv && priv->fail_op) + return -EIO; + + if (priv) + priv->last_effect = index; + return 0; +} + +static int dl_test_set_palette(struct led_classdev_dynamic *ldev, + const struct dl_rgb *palette, + unsigned int num_entries) +{ + struct dl_test_priv *priv = ldev->driver_data; + + if (priv && priv->fail_op) + return -EIO; + return 0; +} + +static int dl_test_set_speed(struct led_classdev_dynamic *ldev, unsigned int speed) +{ + struct dl_test_priv *priv = ldev->driver_data; + + if (priv && priv->fail_op) + return -EIO; + if (priv) + priv->last_speed = speed; + return 0; +} + +static int dl_test_set_direction(struct led_classdev_dynamic *ldev, + enum dl_direction direction) +{ + struct dl_test_priv *priv = ldev->driver_data; + + if (priv && priv->fail_op) + return -EIO; + if (priv) + priv->last_direction = direction; + return 0; +} + +static int dl_test_set_enabled(struct led_classdev_dynamic *ldev, bool enabled) +{ + struct dl_test_priv *priv = ldev->driver_data; + + if (priv && priv->fail_op) + return -EIO; + if (priv) + priv->last_enabled = enabled; + return 0; +} + +static int dl_test_set_power_states(struct led_classdev_dynamic *ldev, + u32 active_states) +{ + struct dl_test_priv *priv = ldev->driver_data; + + if (priv && priv->fail_op) + return -EIO; + if (priv) + priv->last_power_states = active_states; + return 0; +} + +static int dl_test_direct_write(struct led_classdev_dynamic *ldev, + const u8 *buffer, size_t size) +{ + struct dl_test_priv *priv = ldev->driver_data; + + if (priv && priv->fail_op) + return -EIO; + if (priv) + priv->last_direct_len = size; + return 0; +} + +static const struct led_dynamic_ops dl_test_ops = { + .set_effect = dl_test_set_effect, + .set_palette = dl_test_set_palette, +}; + +static const struct led_dynamic_ops dl_test_full_ops = { + .set_effect = dl_test_set_effect, + .set_palette = dl_test_set_palette, + .set_speed = dl_test_set_speed, + .set_direction = dl_test_set_direction, + .set_enabled = dl_test_set_enabled, + .set_power_states = dl_test_set_power_states, + .direct_write = dl_test_direct_write, +}; + +static bool dl_test_attr_visible(struct dl_test_priv *priv, const char *attr) +{ + struct kernfs_node *kn; + struct led_classdev *cdev = led_dynamic_cdev(&priv->ldev); + + kn = kernfs_find_and_get(cdev->dev->kobj.sd, attr); + if (!kn) + return false; + + kernfs_put(kn); + return true; +} + +/* + * Resolve the device_attribute behind a sysfs file through kernfs, so the + * show/store callbacks can be exercised without a mounted sysfs. sysfs + * stores the struct attribute pointer in kernfs_node->priv. + */ +static struct device_attribute *dl_test_attr(struct kunit *test, + struct dl_test_priv *priv, + const char *name) +{ + struct kernfs_node *kn; + struct attribute *attr; + struct led_classdev *cdev = led_dynamic_cdev(&priv->ldev); + + kn = kernfs_find_and_get(cdev->dev->kobj.sd, name); + KUNIT_ASSERT_NOT_NULL(test, kn); + + attr = kn->priv; + kernfs_put(kn); + KUNIT_ASSERT_NOT_NULL(test, attr); + + return container_of(attr, struct device_attribute, attr); +} + +static int dl_test_write(struct kunit *test, struct dl_test_priv *priv, + const char *name, const char *text) +{ + struct device_attribute *attr = dl_test_attr(test, priv, name); + struct led_classdev *cdev = led_dynamic_cdev(&priv->ldev); + ssize_t n; + + KUNIT_ASSERT_NOT_NULL(test, attr->store); + + n = attr->store(cdev->dev, attr, text, strlen(text)); + if (n < 0) + return n; + + return 0; +} + +static void dl_test_read(struct kunit *test, struct dl_test_priv *priv, + const char *name, char *buf) +{ + struct device_attribute *attr = dl_test_attr(test, priv, name); + struct led_classdev *cdev = led_dynamic_cdev(&priv->ldev); + ssize_t n; + + KUNIT_ASSERT_NOT_NULL(test, attr->show); + + n = attr->show(cdev->dev, attr, buf); + KUNIT_ASSERT_GE(test, n, 0); + buf[n] = '\0'; +} + +static void dl_test_unregister(struct dl_test_priv *priv) +{ + if (!priv) + return; + + led_classdev_dynamic_unregister(&priv->ldev); + if (priv->dev) { + put_device(priv->dev); + priv->dev = NULL; + } +} + +static int dl_test_register_basic(struct kunit *test, struct dl_test_priv *priv) +{ + struct device *dev; + int ret; + + dev = kunit_device_register(test, "dl_test"); + if (IS_ERR(dev)) + return PTR_ERR(dev); + + priv->dev = get_device(dev); + priv->ldev.cdev.name = "dl-test"; + priv->ldev.ops = &dl_test_ops; + priv->ldev.zone_type = "keyboard"; + priv->ldev.effects = dl_test_effects; + priv->ldev.num_effects = 2; /* static, breathe */ + priv->ldev.max_palette_entries = 2; + priv->ldev.speed_min = 0; + priv->ldev.speed_max = 2; + priv->ldev.driver_data = priv; + + ret = led_classdev_dynamic_register(priv->dev, &priv->ldev); + if (ret) { + put_device(priv->dev); + priv->dev = NULL; + return ret; + } + + return 0; +} + +static int dl_test_register_full(struct kunit *test, struct dl_test_priv *priv, + unsigned int led_count, + unsigned int sample_size) +{ + struct device *dev; + int ret; + + dev = kunit_device_register(test, "dl_full"); + if (IS_ERR(dev)) + return PTR_ERR(dev); + + priv->dev = get_device(dev); + priv->ldev.cdev.name = "dl-full"; + priv->ldev.ops = &dl_test_full_ops; + priv->ldev.zone_type = "generic"; + priv->ldev.led_count = led_count; + priv->ldev.direct_sample_size = sample_size; + priv->ldev.effects = dl_test_effects; + priv->ldev.num_effects = ARRAY_SIZE(dl_test_effects); + priv->ldev.max_palette_entries = 2; + priv->ldev.speed_min = 1; + priv->ldev.speed_max = 3; + priv->ldev.speed = 2; + priv->ldev.supported_directions = BIT(DL_DIRECTION_LEFT) | + BIT(DL_DIRECTION_RIGHT); + priv->ldev.direction = DL_DIRECTION_LEFT; + priv->ldev.enabled = true; + priv->ldev.supported_power_states = DL_POWER_STATE_ALL; + priv->ldev.active_power_states = DL_POWER_STATE_AWAKE; + priv->ldev.driver_data = priv; + + ret = led_classdev_dynamic_register(priv->dev, &priv->ldev); + if (ret) { + put_device(priv->dev); + priv->dev = NULL; + return ret; + } + + return 0; +} + +static void dl_test_unknown_effect_rejected(struct kunit *test) +{ + struct dl_test_priv *priv; + int ret; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + KUNIT_ASSERT_EQ(test, dl_test_register_basic(test, priv), 0); + + KUNIT_EXPECT_TRUE(test, dl_test_attr_visible(priv, "effect")); + KUNIT_EXPECT_TRUE(test, dl_test_attr_visible(priv, "effect_index")); + KUNIT_EXPECT_FALSE(test, dl_test_attr_visible(priv, "speed")); + KUNIT_EXPECT_FALSE(test, dl_test_attr_visible(priv, "enabled")); + KUNIT_EXPECT_FALSE(test, dl_test_attr_visible(priv, "direct_buffer")); + + ret = dl_test_write(test, priv, "effect", "no-such-effect"); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + + dl_test_unregister(priv); +} + +static void dl_test_effect_and_palette(struct kunit *test) +{ + struct dl_test_priv *priv; + char *buf; + int ret; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + KUNIT_ASSERT_EQ(test, dl_test_register_basic(test, priv), 0); + + buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, buf); + + ret = dl_test_write(test, priv, "effect", "breathe"); + KUNIT_ASSERT_EQ(test, ret, 0); + dl_test_read(test, priv, "effect", buf); + KUNIT_EXPECT_STREQ(test, buf, "breathe\n"); + + ret = dl_test_write(test, priv, "effects_palette", "#ff0000 #00ff00"); + KUNIT_ASSERT_EQ(test, ret, 0); + dl_test_read(test, priv, "effects_palette", buf); + KUNIT_EXPECT_STREQ(test, buf, "#ff0000 #00ff00\n"); + + ret = dl_test_write(test, priv, "effects_palette", + "#ff0000 #00ff00 #0000ff"); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + + ret = dl_test_write(test, priv, "effects_palette", "ff0000"); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + + dl_test_unregister(priv); +} + +static void dl_test_fill_effects_overflow(struct kunit *test) +{ + const char *dst[2]; + int n; + + n = led_dynamic_fill_effects(dst, ARRAY_SIZE(dst), dl_test_pool, + ARRAY_SIZE(dl_test_pool), + BIT(0) | BIT(1) | BIT(2)); + KUNIT_EXPECT_EQ(test, n, -EINVAL); + + n = led_dynamic_fill_effects(dst, ARRAY_SIZE(dst), dl_test_pool, + ARRAY_SIZE(dl_test_pool), + BIT(0) | BIT(1)); + KUNIT_ASSERT_EQ(test, n, 2); + KUNIT_EXPECT_STREQ(test, dst[0], "static"); + KUNIT_EXPECT_STREQ(test, dst[1], "breathe"); +} + +static void dl_test_zone_type_unknown(struct kunit *test) +{ + struct dl_test_priv *priv; + char *buf; + int ret; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + + priv->ldev.cdev.name = "dl-zone"; + priv->ldev.ops = &dl_test_ops; + priv->ldev.effects = dl_test_effects; + priv->ldev.num_effects = 2; + priv->ldev.driver_data = priv; + + { + struct device *dev = kunit_device_register(test, "dl_zone"); + + KUNIT_ASSERT_FALSE(test, IS_ERR(dev)); + priv->dev = get_device(dev); + } + + ret = led_classdev_dynamic_register(priv->dev, &priv->ldev); + KUNIT_ASSERT_EQ(test, ret, 0); + + buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, buf); + dl_test_read(test, priv, "zone_type", buf); + KUNIT_EXPECT_STREQ(test, buf, "unknown\n"); + + dl_test_unregister(priv); +} + +static void dl_test_reregister_rejected(struct kunit *test) +{ + struct dl_test_priv *priv; + int ret; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + KUNIT_ASSERT_EQ(test, dl_test_register_basic(test, priv), 0); + + ret = led_classdev_dynamic_register(priv->dev, &priv->ldev); + KUNIT_EXPECT_EQ(test, ret, -EBUSY); + + dl_test_unregister(priv); +} + +static void dl_test_host_rejects_parent(struct kunit *test) +{ + struct dl_test_priv *priv; + struct led_classdev host = { }; + struct device *dev; + int ret; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + + dev = kunit_device_register(test, "dl_host_parent"); + KUNIT_ASSERT_FALSE(test, IS_ERR(dev)); + + host.name = "dl-host-led"; + ret = led_classdev_register(dev, &host); + KUNIT_ASSERT_EQ(test, ret, 0); + + priv->ldev.host = &host; + priv->ldev.ops = &dl_test_ops; + priv->ldev.effects = dl_test_effects; + priv->ldev.num_effects = 2; + + ret = led_classdev_dynamic_register(dev, &priv->ldev); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + + led_classdev_unregister(&host); +} + +static void dl_test_host_attach(struct kunit *test) +{ + struct dl_test_priv *priv; + struct led_classdev host = { }; + struct device *dev; + char *buf; + int ret; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + + dev = kunit_device_register(test, "dl_host"); + KUNIT_ASSERT_FALSE(test, IS_ERR(dev)); + + host.name = "dl-host-led"; + ret = led_classdev_register(dev, &host); + KUNIT_ASSERT_EQ(test, ret, 0); + + priv->dev = get_device(dev); + priv->ldev.host = &host; + priv->ldev.ops = &dl_test_ops; + priv->ldev.zone_type = "generic"; + priv->ldev.effects = dl_test_effects; + priv->ldev.num_effects = 2; + priv->ldev.max_palette_entries = 2; + priv->ldev.driver_data = priv; + + ret = led_classdev_dynamic_register(NULL, &priv->ldev); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_TRUE(test, priv->ldev.attached); + KUNIT_EXPECT_PTR_EQ(test, host.led_dynamic, &priv->ldev); + + buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, buf); + + ret = dl_test_write(test, priv, "effect", "breathe"); + KUNIT_ASSERT_EQ(test, ret, 0); + dl_test_read(test, priv, "effect", buf); + KUNIT_EXPECT_STREQ(test, buf, "breathe\n"); + + led_classdev_dynamic_unregister(&priv->ldev); + KUNIT_EXPECT_NULL(test, host.led_dynamic); + led_classdev_unregister(&host); + put_device(priv->dev); +} + +static void dl_test_enabled_and_power_states(struct kunit *test) +{ + struct dl_test_priv *priv; + char *buf; + int ret; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + KUNIT_ASSERT_EQ(test, dl_test_register_full(test, priv, 2, 0), 0); + + buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, buf); + + KUNIT_EXPECT_TRUE(test, dl_test_attr_visible(priv, "enabled")); + KUNIT_EXPECT_TRUE(test, dl_test_attr_visible(priv, "power_states")); + + ret = dl_test_write(test, priv, "enabled", "false"); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_FALSE(test, priv->ldev.enabled); + dl_test_read(test, priv, "enabled", buf); + KUNIT_EXPECT_STREQ(test, buf, "false\n"); + + ret = dl_test_write(test, priv, "power_states", "boot awake"); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, priv->ldev.active_power_states, + DL_POWER_STATE_BOOT | DL_POWER_STATE_AWAKE); + + ret = dl_test_write(test, priv, "power_states", "\n"); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, priv->ldev.active_power_states, 0); + + ret = dl_test_write(test, priv, "power_states", "not-a-state"); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + + dl_test_unregister(priv); +} + +static void dl_test_speed_direction_bounds(struct kunit *test) +{ + struct dl_test_priv *priv; + char *buf; + int ret; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + KUNIT_ASSERT_EQ(test, dl_test_register_full(test, priv, 2, 0), 0); + + buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, buf); + + ret = dl_test_write(test, priv, "speed", "0"); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + ret = dl_test_write(test, priv, "speed", "4"); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + ret = dl_test_write(test, priv, "speed", "3"); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, priv->ldev.speed, 3); + + ret = dl_test_write(test, priv, "direction", "up"); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + ret = dl_test_write(test, priv, "direction", "right"); + KUNIT_ASSERT_EQ(test, ret, 0); + dl_test_read(test, priv, "direction", buf); + KUNIT_EXPECT_STREQ(test, buf, "right\n"); + + dl_test_unregister(priv); +} + +static void dl_test_set_ops_failure_preserves_state(struct kunit *test) +{ + struct dl_test_priv *priv; + int ret; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + KUNIT_ASSERT_EQ(test, dl_test_register_full(test, priv, 2, 0), 0); + + ret = dl_test_write(test, priv, "effect", "breathe"); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, priv->ldev.current_effect, 1); + + priv->fail_op = 1; + ret = dl_test_write(test, priv, "effect", "static"); + KUNIT_EXPECT_EQ(test, ret, -EIO); + KUNIT_EXPECT_EQ(test, priv->ldev.current_effect, 1); + + ret = dl_test_write(test, priv, "speed", "1"); + KUNIT_EXPECT_EQ(test, ret, -EIO); + KUNIT_EXPECT_EQ(test, priv->ldev.speed, 2); + + ret = dl_test_write(test, priv, "enabled", "false"); + KUNIT_EXPECT_EQ(test, ret, -EIO); + KUNIT_EXPECT_TRUE(test, priv->ldev.enabled); + + ret = dl_test_write(test, priv, "power_states", "boot"); + KUNIT_EXPECT_EQ(test, ret, -EIO); + KUNIT_EXPECT_EQ(test, priv->ldev.active_power_states, DL_POWER_STATE_AWAKE); + + dl_test_unregister(priv); +} + +static void dl_test_direct_buffer_size_and_effect(struct kunit *test) +{ + struct dl_test_priv *priv; + struct bin_attribute *bin; + struct kernfs_node *kn; + struct led_classdev *cdev; + u8 frame[6]; + u8 mono[4] = { 1, 2, 3, 4 }; + char *buf; + ssize_t n; + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + KUNIT_ASSERT_EQ(test, dl_test_register_full(test, priv, 2, 0), 0); + + cdev = led_dynamic_cdev(&priv->ldev); + kn = kernfs_find_and_get(cdev->dev->kobj.sd, "direct_buffer"); + KUNIT_ASSERT_NOT_NULL(test, kn); + bin = kn->priv; + kernfs_put(kn); + KUNIT_ASSERT_NOT_NULL(test, bin); + KUNIT_ASSERT_NOT_NULL(test, bin->write); + + memset(frame, 0x11, sizeof(frame)); + n = bin->write(NULL, &cdev->dev->kobj, bin, frame, 0, 3); + KUNIT_EXPECT_EQ(test, n, -EINVAL); + + n = bin->write(NULL, &cdev->dev->kobj, bin, frame, 0, sizeof(frame)); + KUNIT_ASSERT_EQ(test, n, sizeof(frame)); + KUNIT_EXPECT_EQ(test, priv->last_direct_len, sizeof(frame)); + + buf = kunit_kzalloc(test, PAGE_SIZE, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, buf); + dl_test_read(test, priv, "effect", buf); + KUNIT_EXPECT_STREQ(test, buf, "direct\n"); + + priv->fail_op = 1; + priv->ldev.current_effect = 0; + n = bin->write(NULL, &cdev->dev->kobj, bin, frame, 0, sizeof(frame)); + KUNIT_EXPECT_EQ(test, n, -EIO); + KUNIT_EXPECT_EQ(test, priv->ldev.current_effect, 0); + + dl_test_unregister(priv); + + priv = kunit_kzalloc(test, sizeof(*priv), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, priv); + KUNIT_ASSERT_EQ(test, dl_test_register_full(test, priv, 4, 1), 0); + + cdev = led_dynamic_cdev(&priv->ldev); + kn = kernfs_find_and_get(cdev->dev->kobj.sd, "direct_buffer"); + KUNIT_ASSERT_NOT_NULL(test, kn); + bin = kn->priv; + kernfs_put(kn); + + n = bin->write(NULL, &cdev->dev->kobj, bin, mono, 0, 3); + KUNIT_EXPECT_EQ(test, n, -EINVAL); + n = bin->write(NULL, &cdev->dev->kobj, bin, mono, 0, 4); + KUNIT_ASSERT_EQ(test, n, 4); + KUNIT_EXPECT_EQ(test, priv->last_direct_len, 4); + + dl_test_unregister(priv); +} + +static struct kunit_case dl_test_cases[] = { + KUNIT_CASE(dl_test_unknown_effect_rejected), + KUNIT_CASE(dl_test_effect_and_palette), + KUNIT_CASE(dl_test_fill_effects_overflow), + KUNIT_CASE(dl_test_zone_type_unknown), + KUNIT_CASE(dl_test_reregister_rejected), + KUNIT_CASE(dl_test_host_rejects_parent), + KUNIT_CASE(dl_test_host_attach), + KUNIT_CASE(dl_test_enabled_and_power_states), + KUNIT_CASE(dl_test_speed_direction_bounds), + KUNIT_CASE(dl_test_set_ops_failure_preserves_state), + KUNIT_CASE(dl_test_direct_buffer_size_and_effect), + { } +}; + +static struct kunit_suite dl_test_suite = { + .name = "led_class_dynamic", + .test_cases = dl_test_cases, +}; + +kunit_test_suite(dl_test_suite); + +MODULE_AUTHOR("Marco Scardovi "); +MODULE_AUTHOR("Denis Benato "); +MODULE_DESCRIPTION("KUnit tests for the Dynamic Lighting LED class"); +MODULE_LICENSE("GPL"); diff --git a/drivers/leds/led-class-dynamic.c b/drivers/leds/led-class-dynamic.c new file mode 100644 index 00000000000000..4e8fd9df17971d --- /dev/null +++ b/drivers/leds/led-class-dynamic.c @@ -0,0 +1,956 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * 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 +#include + +static const char * const dl_direction_names[] = { + [DL_DIRECTION_LEFT] = "left", + [DL_DIRECTION_RIGHT] = "right", + [DL_DIRECTION_UP] = "up", + [DL_DIRECTION_DOWN] = "down", +}; + +static const char * const dl_power_state_names[] = { + "boot", + "awake", + "sleep", + "shutdown", +}; + +static const char * const dl_enabled_names[] = { + "false", + "true", +}; + +static ssize_t dl_emit_indexed_names(char *buf, const char * const *names, + unsigned int count, u32 mask, bool filter) +{ + ssize_t len = 0; + unsigned int i; + + for (i = 0; i < count; i++) { + if (!names[i]) + continue; + if (filter && !(mask & BIT(i))) + continue; + + len += sysfs_emit_at(buf, len, "%s ", names[i]); + } + + if (len > 0) + buf[len - 1] = '\n'; + else + len = sysfs_emit(buf, "\n"); + + return len; +} + +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 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); + + guard(mutex)(&ldev->lock); + + return dl_emit_indexed_names(buf, ldev->effects, ldev->num_effects, 0, false); +} +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); + const char *name; + + guard(mutex)(&ldev->lock); + + name = led_dynamic_effect_name(ldev); + if (!name) + return sysfs_emit(buf, "unknown\n"); + + return sysfs_emit(buf, "%s\n", name); +} + +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(ldev->effects, ldev->num_effects, buf); + if (match < 0) + 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 enabled_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, "%s\n", ldev->enabled ? "true" : "false"); +} + +static ssize_t enabled_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_enabled) + return -EOPNOTSUPP; + + match = sysfs_match_string(dl_enabled_names, buf); + if (match < 0) + 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_enabled(ldev, match); + if (ret < 0) + return ret; + + ldev->enabled = !!match; + return count; +} +static DEVICE_ATTR_RW(enabled); + +static ssize_t enabled_index_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "false true\n"); +} +static DEVICE_ATTR_RO(enabled_index); + +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, "%u-%u\n", ldev->speed_min, ldev->speed_max); +} +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) + return -EOPNOTSUPP; + + ret = kstrtouint(buf, 10, &speed); + if (ret) + return ret; + + if (speed < ldev->speed_min || speed > ldev->speed_max) + 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_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); + + guard(mutex)(&ldev->lock); + + return dl_emit_indexed_names(buf, dl_direction_names, + ARRAY_SIZE(dl_direction_names), + ldev->supported_directions, true); +} +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; + led_trigger_remove(cdev); + 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 (strnlen(cur, 6) < 6) + return -EINVAL; + + if (hex2bin((u8 *)&temp_palette[num_parsed], cur, 3) < 0) + return -EINVAL; + cur += 6; + if (*cur && !isspace(*cur)) + return -EINVAL; + 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); + + guard(mutex)(&ldev->lock); + + return dl_emit_indexed_names(buf, dl_power_state_names, + ARRAY_SIZE(dl_power_state_names), + ldev->supported_power_states, true); +} +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); + + guard(mutex)(&ldev->lock); + + return dl_emit_indexed_names(buf, dl_power_state_names, + ARRAY_SIZE(dl_power_state_names), + ldev->active_power_states, true); +} + +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; + led_trigger_remove(cdev); + 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); + +/* + * kernfs delivers bin-attribute writes in at most PAGE_SIZE chunks. Stage + * partial writes and invoke the driver only when the full direct_buffer + * payload (led_count * direct_sample_size, default 3) has arrived. A short + * write that cannot grow into that size is rejected. Any other non-zero + * offset is accepted only as the next contiguous chunk. + */ +static ssize_t led_dynamic_stage_bin_write(struct led_classdev *cdev, + struct led_classdev_dynamic *ldev, + char *buf, loff_t off, size_t count, + size_t size, + int (*commit)(struct led_classdev_dynamic *ldev, + const u8 *data, size_t len)) +{ + size_t end; + int ret; + + u8 *staging __free(kfree) = NULL; + + if (!size || !count || off < 0) + return -EINVAL; + + if (check_add_overflow((size_t)off, count, &end) || end > size) + return -EINVAL; + + if (off == 0 && count < PAGE_SIZE && count != size) + return -EINVAL; + + /* Allocate before the LED locks. The buffer is adopted only if needed. */ + if (off == 0) { + staging = kmalloc(size, GFP_KERNEL); + if (!staging) + return -ENOMEM; + } + + guard(mutex)(&cdev->led_access); + if (led_sysfs_is_disabled(cdev)) + return -EBUSY; + guard(mutex)(&ldev->lock); + + if (off == 0) { + ldev->write_filled = 0; + if (!ldev->write_staging || ldev->write_staging_size != size) { + kfree(ldev->write_staging); + ldev->write_staging = no_free_ptr(staging); + ldev->write_staging_size = size; + } + } else if (!ldev->write_staging || ldev->write_staging_size != size || + (size_t)off != ldev->write_filled) { + return -EINVAL; + } + + memcpy(ldev->write_staging + off, buf, count); + ldev->write_filled = end; + + if (end != size) + return count; + + led_trigger_remove(cdev); + ret = commit(ldev, ldev->write_staging, size); + ldev->write_filled = 0; + if (ret < 0) + return ret; + + return count; +} + +static int led_dynamic_commit_direct(struct led_classdev_dynamic *ldev, + const u8 *data, size_t len) +{ + int ret, direct_idx; + + ret = ldev->ops->direct_write(ldev, data, len); + if (ret < 0) + return ret; + + direct_idx = led_dynamic_effect_index(ldev, "direct"); + if (direct_idx >= 0) + ldev->current_effect = direct_idx; + + return 0; +} + +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; + + if (!ldev->ops->direct_write) + return -EOPNOTSUPP; + + if (check_mul_overflow((size_t)ldev->led_count, + (size_t)(ldev->direct_sample_size ?: 3), + &expected_size)) + return -EOVERFLOW; + + return led_dynamic_stage_bin_write(cdev, ldev, buf, off, count, + expected_size, + led_dynamic_commit_direct); +} + +static int led_dynamic_validate(struct led_classdev_dynamic *ldev, size_t *direct_buffer_size) +{ + unsigned int i; + unsigned int stride = ldev->direct_sample_size ?: 3; + + if (check_mul_overflow((size_t)ldev->led_count, (size_t)stride, + direct_buffer_size)) + return -EOVERFLOW; + + if (ldev->ops->set_effect) { + if (!ldev->effects || !ldev->num_effects) + return -EINVAL; + + for (i = 0; i < ldev->num_effects; i++) { + if (!ldev->effects[i] || !*ldev->effects[i]) + return -EINVAL; + } + + if (ldev->current_effect >= ldev->num_effects) + return -EINVAL; + } + + if (ldev->speed_min > ldev->speed_max) + return -EINVAL; + + if (ldev->ops->set_speed && + (ldev->speed < ldev->speed_min || ldev->speed > ldev->speed_max)) + 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->host) { + if (!ldev->host->dev) + return -EINVAL; + if (ldev->host->led_dynamic || + (ldev->host->flags & LED_DYNAMIC_LIGHTING)) + return -EBUSY; + } + + 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_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->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->num_effects || !ldev->ops->set_effect) + return 0; + } + + if (attr == &dev_attr_enabled.attr || + attr == &dev_attr_enabled_index.attr) { + if (!ldev->ops->set_enabled) + 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) + return 0; + } + + return attr->attr.mode; +} + +static struct attribute *led_dynamic_attrs[] = { + &dev_attr_zone_type.attr, + &dev_attr_led_count.attr, + &dev_attr_effect_index.attr, + &dev_attr_effect.attr, + &dev_attr_enabled.attr, + &dev_attr_enabled_index.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, +}; + +static void led_dynamic_release_resources(struct led_classdev_dynamic *ldev, + struct led_classdev *cdev) +{ + if (ldev->palette_allocated) { + kfree(ldev->palette); + ldev->palette = NULL; + ldev->palette_allocated = false; + } + kfree(ldev->write_staging); + ldev->write_staging = NULL; + ldev->write_staging_size = 0; + ldev->write_filled = 0; + kfree(ldev->merged_groups); + ldev->merged_groups = NULL; + if (cdev) + cdev->groups = ldev->driver_groups; + mutex_destroy(&ldev->lock); +} + +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; + + /* + * Host attach adds attributes to an already-registered LED. parent and + * init_data apply only to standalone registration of ldev->cdev. + */ + if (ldev->host && (parent || init_data)) + return -EINVAL; + + /* + * One-shot registration: refuse a second register without unregister. + * mutex_init() must not run on a live lock. + */ + if (ldev->attached || + ldev->cdev.led_dynamic || + (ldev->cdev.flags & LED_DYNAMIC_LIGHTING) || + (ldev->host && ldev->host->led_dynamic)) + return -EBUSY; + + ret = led_dynamic_validate(ldev, &direct_buffer_size); + if (ret) + return ret; + + mutex_init(&ldev->lock); + ldev->attached = false; + ldev->palette_allocated = false; + ldev->merged_groups = NULL; + ldev->write_staging = NULL; + ldev->write_staging_size = 0; + ldev->write_filled = 0; + + 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; + + ldev->bin_attrs[0] = &ldev->bin_attr_direct; + ldev->bin_attrs[1] = 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; + + if (ldev->max_palette_entries > 0 && !ldev->palette) { + ldev->palette = + kcalloc(ldev->max_palette_entries, sizeof(*ldev->palette), + GFP_KERNEL); + if (!ldev->palette) { + mutex_destroy(&ldev->lock); + return -ENOMEM; + } + ldev->palette_allocated = true; + } + + if (ldev->host) { + cdev = ldev->host; + cdev->led_dynamic = ldev; + cdev->flags |= LED_DYNAMIC_LIGHTING; + ldev->attached = true; + + ret = device_add_group(cdev->dev, &ldev->group); + if (ret) { + cdev->led_dynamic = NULL; + cdev->flags &= ~LED_DYNAMIC_LIGHTING; + led_dynamic_release_resources(ldev, NULL); + return ret; + } + + return 0; + } + + cdev = &ldev->cdev; + cdev->flags |= LED_DYNAMIC_LIGHTING; + cdev->led_dynamic = ldev; + + ldev->groups[0] = &ldev->group; + ldev->groups[1] = NULL; + ldev->driver_groups = cdev->groups; + + 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) { + led_dynamic_release_resources(ldev, cdev); + cdev->led_dynamic = NULL; + cdev->flags &= ~LED_DYNAMIC_LIGHTING; + 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; + } + + ret = led_classdev_register_ext(parent, cdev, init_data); + if (ret) { + led_dynamic_release_resources(ldev, cdev); + cdev->led_dynamic = NULL; + cdev->flags &= ~LED_DYNAMIC_LIGHTING; + } + + return ret; +} +EXPORT_SYMBOL_GPL(led_classdev_dynamic_register_ext); + +void led_classdev_dynamic_unregister(struct led_classdev_dynamic *ldev) +{ + struct led_classdev *cdev; + + if (!ldev) + return; + + if (ldev->attached) { + cdev = ldev->host; + if (cdev && cdev->dev) + device_remove_group(cdev->dev, &ldev->group); + if (cdev) { + cdev->led_dynamic = NULL; + cdev->flags &= ~LED_DYNAMIC_LIGHTING; + } + led_dynamic_release_resources(ldev, NULL); + ldev->attached = false; + return; + } + + cdev = &ldev->cdev; + led_classdev_unregister(cdev); + cdev->led_dynamic = NULL; + led_dynamic_release_resources(ldev, cdev); +} +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..61e4a06ace3e66 --- /dev/null +++ b/include/linux/led-dynamic-lighting.h @@ -0,0 +1,345 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * 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 +#include +#include + +/** + * 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_MAX: Number of directions + */ +enum dl_direction { + DL_DIRECTION_LEFT = 0, + DL_DIRECTION_RIGHT, + DL_DIRECTION_UP, + DL_DIRECTION_DOWN, + 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) + +/** + * 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: Select the effect at @index in ldev->effects + * @set_speed: Configure effect speed (within speed_min..speed_max) + * @set_direction: Configure effect propagation direction + * @set_palette: Apply multi-color stacked palette + * @set_enabled: Turn the lighting engine on or off without changing effect + * @direct_write: Stream packed RGB buffer (size must equal led_count * 3) + * @set_power_states: Update active power state persistence bitmask + * + * Every callback is optional. Sysfs files are created only for implemented + * ops (and the matching capability fields). Existing LED drivers can keep + * their current brightness / multi_intensity path and implement only the + * ops they already have hardware for. + */ +struct led_dynamic_ops { + int (*set_effect)(struct led_classdev_dynamic *ldev, + unsigned int index); + 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 (*set_enabled)(struct led_classdev_dynamic *ldev, + bool enabled); + int (*direct_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 (unused when @host is set) + * @host: Optional already-registered LED to attach to (drop-in) + * @ops: Hardware callback dispatch table + * @lock: Internal mutex protecting ldev state and serialization + * @zone_type: Driver-defined physical topology string for the lighting zone. + * When NULL or empty, sysfs reports ``unknown`` + * @led_count: Total individual LEDs in this zone + * @direct_sample_size: Bytes per LED in @direct_buffer (0 means 3 / RGB). + * Drivers may set 1 for single-channel intensity streams + * @effects: Driver-owned table of effect names (not a global enum) + * @num_effects: Number of entries in @effects + * @current_effect: Index into @effects of the currently selected effect + * @speed: Current effect animation speed + * @speed_min: Minimum supported speed level + * @speed_max: Maximum supported speed level + * @direction: Current effect animation direction + * @supported_directions: Bitmask of supported enum dl_direction values + * @enabled: Lighting engine on/off; independent from @current_effect. The + * driver sets the initial value before registration; the class does not + * override it + * @palette: Stacked palette color entries. Drivers may point this at their + * own storage of @max_palette_entries entries before registration so + * the initial palette is in place when sysfs appears; otherwise the + * class allocates it + * @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_attrs: Array of binary attribute pointers for group + * @write_staging: Scratch buffer for kernfs PAGE_SIZE-chunked bin writes + * @write_staging_size: Allocated size of @write_staging + * @write_filled: Bytes accepted so far in the current chunked bin write + * @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 + * @attached: True when sysfs was added to @host instead of registering @cdev + * + * Drop-in on an existing LED class device + * --------------------------------------- + * Drivers that already register a LED (including LED_MULTI_COLOR) can attach + * Dynamic Lighting without replacing that registration. brightness stays the + * LED intensity; a driver may map it to a hardware dimmer. effects_palette is + * the multi-color stack. brightness 0 must not clear a persisted effect: + * removing a trigger sets brightness to LED_OFF. Lighting off is enabled=false. + * This class adds optional effect, speed, enabled, palette, and direct RGB nodes: + * + * struct led_classdev_dynamic ldev = { + * .host = existing_led_cdev, + * .ops = &my_ops, + * .effects = my_effects, + * .num_effects = ARRAY_SIZE(my_effects), + * }; + * led_classdev_dynamic_register(dev, &ldev); + * + * Effect names are defined by the driver. Userspace must read effect_index. + * Lighting off is enabled=false, not a dedicated off effect. + */ +struct led_classdev_dynamic { + struct led_classdev cdev; + struct led_classdev *host; + const struct led_dynamic_ops *ops; + struct mutex lock; /* Protects ldev state serialization */ + + const char *zone_type; + unsigned int led_count; + unsigned int direct_sample_size; + + const char * const *effects; + unsigned int num_effects; + unsigned int current_effect; + + unsigned int speed; + unsigned int speed_min; + unsigned int speed_max; + + enum dl_direction direction; + unsigned int supported_directions; + + bool enabled; + + 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)); + const struct bin_attribute *bin_attrs[2]; + u8 *write_staging; + size_t write_staging_size; + size_t write_filled; + struct attribute_group group; + const struct attribute_group *groups[2]; + const struct attribute_group **driver_groups; + const struct attribute_group **merged_groups; + bool attached; +}; + +/** + * led_dynamic_cdev - LED classdev backing a Dynamic Lighting instance + * + * Returns @host when this instance was attached to an existing LED, + * otherwise the embedded @cdev. + */ +static inline struct led_classdev *led_dynamic_cdev(struct led_classdev_dynamic *ldev) +{ + return ldev->host ? ldev->host : &ldev->cdev; +} + +static inline struct led_classdev_dynamic *lcdev_to_dldev(struct led_classdev *lcdev) +{ + /* + * Registration always sets the back-pointer before sysfs appears. + * Do not fall back to container_of(): host-attached instances live on + * a foreign led_classdev, not ldev->cdev. + */ + return lcdev->led_dynamic; +} + +static inline bool is_dynamic_lighting_led(struct led_classdev *lcdev) +{ + return !!(lcdev->flags & LED_DYNAMIC_LIGHTING); +} + +static inline const char * +led_dynamic_effect_name(const struct led_classdev_dynamic *ldev) +{ + if (!ldev->effects || ldev->current_effect >= ldev->num_effects) + return NULL; + + return ldev->effects[ldev->current_effect]; +} + +static inline bool led_dynamic_effect_is(const struct led_classdev_dynamic *ldev, + const char *name) +{ + const char *cur = led_dynamic_effect_name(ldev); + + return cur && name && !strcmp(cur, name); +} + +static inline int led_dynamic_effect_index(const struct led_classdev_dynamic *ldev, + const char *name) +{ + if (!ldev->effects || !name) + return -EINVAL; + + return __sysfs_match_string(ldev->effects, ldev->num_effects, name); +} + +/** + * led_dynamic_fill_effects - compact a name pool through a bitmask + * @dst: caller-owned array with at least hweight32(@mask) slots + * @dst_n: number of slots in @dst + * @pool: driver-owned names indexed by bit number + * @pool_n: number of entries in @pool + * @mask: bits selecting which names to include + * + * Helper for drivers that already track effects as capability bits and + * want to publish a Dynamic Lighting table without rewriting that logic. + * Returns -EINVAL if the selected names do not fit in @dst (no silent + * truncation). + * + * Return: number of names written, or -EINVAL. + */ +static inline int led_dynamic_fill_effects(const char **dst, unsigned int dst_n, + const char * const *pool, + unsigned int pool_n, u32 mask) +{ + unsigned int i, n = 0, need = 0; + + if (!dst || !pool) + return -EINVAL; + + /* mask is a u32, and BIT(i) is undefined for i >= 32 on 32-bit. */ + for (i = 0; i < pool_n && i < 32; i++) { + if ((mask & BIT(i)) && pool[i]) + need++; + } + + if (need > dst_n) + return -EINVAL; + + for (i = 0; i < pool_n && i < 32; i++) { + if (!(mask & BIT(i)) || !pool[i]) + continue; + dst[n++] = pool[i]; + } + + return n; +} + +#if IS_REACHABLE(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_REACHABLE(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 */ diff --git a/include/linux/leds.h b/include/linux/leds.h index beaf2399306393..5780d8f7bbffd2 100644 --- a/include/linux/leds.h +++ b/include/linux/leds.h @@ -19,6 +19,7 @@ struct attribute_group; struct device_node; +struct led_classdev_dynamic; struct fwnode_handle; struct gpio_desc; struct kernfs_node; @@ -110,6 +111,7 @@ struct led_classdev { #define LED_REJECT_NAME_CONFLICT BIT(24) #define LED_MULTI_COLOR BIT(25) #define LED_TRIG_HW_CHANGED BIT(26) +#define LED_DYNAMIC_LIGHTING BIT(27) /* set_brightness_work / blink_timer flags, atomic, private. */ unsigned long work_flags; @@ -163,6 +165,15 @@ struct led_classdev { struct device *dev; const struct attribute_group **groups; + /* + * Back-pointer to a Dynamic Lighting extension, if any. Set by + * led_classdev_dynamic_register_ext() (and the non-_ext wrappers) + * for both standalone devices and drop-in attach onto an existing + * LED. Forward-declared so leds.h does not include + * led-dynamic-lighting.h. + */ + struct led_classdev_dynamic *led_dynamic; + struct list_head node; /* LED Device list */ const char *default_trigger; /* Trigger to use */ From 063fa91fdc9de144b612046c1fb6a4fe1eafe54d Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 19 Oct 2026 10:12:42 +0200 Subject: [PATCH 2/7] docs: leds: Document the Dynamic Lighting class ABI Document the Dynamic Lighting LED class ABI and user-facing sysfs interface. Describe the common attributes, visibility rules for optional controls, and how a vendor driver can attach the class to an existing LED. Effect names are defined by the driver and discovered through effect_index. enabled/enabled_index turn lighting off without changing the selected effect. Writing power_states replaces the active bitmask (an empty list clears all enabled states). Direction values are left, right, up and down. Document zone_type=unknown when unset, host-attach parent/init_data rejection, trigger removal on speed/direction/power_states writes, and that a successful direct_buffer write may select effect \"direct\". direct_buffer size is led_count * sample_size (default 3 bytes/LED). 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 | 153 ++++++++++++ Documentation/leds/index.rst | 1 + Documentation/leds/leds-class-dynamic.rst | 219 ++++++++++++++++++ MAINTAINERS | 2 + 4 files changed, 375 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..56b1a299228cc9 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-class-leds-dynamic @@ -0,0 +1,153 @@ +What: /sys/class/leds//direction +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read/write + Animation propagation direction. Outputs or accepts one of + ``left``, ``right``, ``up``, or ``down``. Only visible when the driver supports + directional animations. + +What: /sys/class/leds//direction_index +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read + Space-separated list of supported animation directions. + Only visible when the driver supports directional animations. + +What: /sys/class/leds//direct_buffer +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: write-only (binary) + Raw packed per-LED stream. The default layout is 3 bytes per + LED (R, G, B in sequence); drivers may advertise a different + stride (for example 1 byte per LED on mono matrices) via + ``led_count`` and the driver's documented sample size. + Writing transmits a direct frame bypassing hardware effect + generators. On success, if the driver publishes an effect + named ``direct``, the class selects that effect. The complete + write must total exactly (led_count * sample_size) bytes; + kernfs may deliver that payload in PAGE_SIZE chunks starting + at offset 0. Only visible when the driver implements direct + streaming. + +What: /sys/class/leds//effect +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read/write + Currently selected hardware or driver-synthesized animation + effect. Reading outputs the effect name. Writing a name from + ``effect_index`` selects that effect. Names are defined by the + driver, not by a global enum; userspace must read + ``effect_index``. Any active trigger is automatically detached + upon switching effects. + +What: /sys/class/leds//effect_index +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read + Space-separated list of animation effect names supported by + this LED. The vocabulary is defined by the driver. Lighting + off is not an effect; use ``enabled``. + +What: /sys/class/leds//effects_palette +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +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``. This is the multi-color stack. + ``brightness`` remains intensity. Drivers must not encode + lighting-off, or a change of the persisted color, as + ``brightness`` 0: removing a trigger sets brightness to + LED_OFF. Only visible when the driver supports programmable + palettes. + +What: /sys/class/leds//enabled +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read/write + Lighting engine on/off as ``true`` or ``false``. Independent + from ``effect``: disabling turns the lights off without + forgetting the selected effect. Only visible when the driver + implements a dedicated enable callback. + +What: /sys/class/leds//enabled_index +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read + Space-separated list of values accepted by ``enabled`` + (``false true``). Only visible when the driver implements a + dedicated enable callback. + +What: /sys/class/leds//led_count +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read + Total number of individual, addressable LEDs in this zone. + +What: /sys/class/leds//max_palette_entries +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read + Maximum number of palette entries accepted by + ``effects_palette``. Only visible when the driver supports + programmable palettes. + +What: /sys/class/leds//power_states +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +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//power_states_index +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +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//speed +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read/write + Current animation speed level (integer within ``speed_range``). + Only visible when the driver supports adjustable speed. + +What: /sys/class/leds//speed_range +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read + Minimum and maximum animation speed level accepted by + ``speed``. Formatted as ``-``. Only visible when the + driver supports adjustable speed. + +What: /sys/class/leds//zone_type +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: read + Driver-defined string describing the physical topology of + this lighting zone. Example values include ``generic``, + ``keyboard``, ``keyboard_per_key``, ``logo``, and ``lightbar``. + When the driver does not set a zone string, the node reports + ``unknown``. 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..ffd4158f29cb4f --- /dev/null +++ b/Documentation/leds/leds-class-dynamic.rst @@ -0,0 +1,219 @@ +.. SPDX-License-Identifier: GPL-2.0 + +====================================== +Dynamic Lighting LED class under Linux +====================================== + +Author: Marco Scardovi +Author: Denis Benato + +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 class can either wrap a new LED class device or attach onto an LED that +the vendor driver already registered (including ``LED_MULTI_COLOR``). +``brightness`` stays the LED intensity. A driver may map it to a hardware +dimmer, and must not treat ``brightness`` 0 as clearing a persisted effect +or as the lighting-off switch: ``led_trigger_remove()`` sets brightness to +``LED_OFF``. Lighting off is ``enabled=false``. Multi-color programming is +``effects_palette``. Dynamic Lighting adds optional effect, speed, enable, +palette, and direct RGB attributes without requiring hidraw or a rewrite of +the existing LED registration. + +Directory Layout Example +======================== +The following examples use ```` as a placeholder for a Dynamic Lighting +LED class device name. Optional attributes are omitted when the driver does +not implement the matching callback. + +.. 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 + -rw-r--r-- 1 root root 4096 Sep 4 17:00 enabled + -r--r--r-- 1 root root 4096 Sep 4 17:00 enabled_index + -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 + +Attaching to an existing LED +============================ +Drivers that already expose a LED can publish Dynamic Lighting as extra +attributes on that same directory. Keep the existing color path, point +``host`` at the registered LED, and supply only the ops the hardware +already implements: + +.. code-block:: c + + static const struct led_dynamic_ops my_ops = { + .set_effect = my_set_effect, + .set_speed = my_set_speed, + .set_enabled = my_set_enabled, + }; + + ldev->host = existing_led_cdev; + ldev->ops = &my_ops; + ldev->effects = my_effects; + ldev->num_effects = ARRAY_SIZE(my_effects); + led_classdev_dynamic_register(NULL, ldev); + +When ``host`` is set, ``parent`` and ``init_data`` must be NULL: the class +only adds attributes to the already-registered LED. Registration is +one-shot; unregister before registering the same ``ldev`` again. + +``led_dynamic_fill_effects()`` can compact an existing capability bitmask +into a name table and fails if the selected names do not fit the destination +array. Effect names are defined by the driver; userspace must read +``effect_index``. + +Sysfs Attributes +================ + +``zone_type`` (read-only) + Driver-defined string describing the physical topology of the zone. + Example values include ``generic``, ``keyboard``, ``keyboard_per_key``, + ``logo``, or ``lightbar``. When unset, the node reports ``unknown``. + +``led_count`` (read-only) + Total number of individually addressable LEDs in this zone. + +``effect_index`` (read-only) + Space-separated list of animation effect names supported by this LED. + Names are defined by the driver. + +``effect`` (read/write) + Currently selected animation effect. Writing a name from ``effect_index`` + switches the mode. Any active trigger is automatically detached upon + effect change. + +``enabled`` (read/write) + Lighting engine on/off (``true`` / ``false``). Independent from + ``effect``. Only visible when the driver implements ``set_enabled``. + +``enabled_index`` (read-only) + Values accepted by ``enabled`` (``false true``). Only visible when the + driver implements ``set_enabled``. + +``speed_range`` (read-only) + Minimum and maximum effect animation speed accepted by ``speed``, + formatted as ``-``. 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``, or ``down``. + 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. This is the + multi-color stack. ``brightness`` remains intensity, including for a + single-color LED that also exposes ``multi_intensity``. + +``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-LED frames. The default layout is 3 bytes + per LED (R, G, B); drivers may use another sample size (for example 1 byte + on mono matrices). The complete write must total + ``led_count * sample_size`` bytes; kernfs may deliver that payload in + ``PAGE_SIZE`` chunks starting at offset 0. A successful write selects the + effect named ``direct`` when that name is present in ``effect_index``. + +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)``. This applies to ``effect``, + ``enabled``, ``speed``, ``direction``, ``effects_palette``, + ``power_states``, and completed ``direct_buffer`` writes. +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``, ``enabled``, ``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. ``led_trigger_remove()`` calls that hook with ``LED_OFF``. The +driver must treat that as intensity, not as powering the zone off and not +as forgetting the selected effect. Zone power belongs to ``enabled``. + +When Dynamic Lighting is attached to an existing LED, ``cdev`` in the lock +order is that host LED (``led_dynamic_cdev()``), not the unused embedded +``ldev->cdev``. + +``direct_buffer`` writes that are not exactly ``led_count * sample_size`` bytes, +and that cannot be completed by later ``PAGE_SIZE`` chunks from offset 0, are +rejected. Empty writes are rejected. + +Examples +======== + +Selecting an effect and speed advertised by the device: +------------------------------------------------------- +.. code-block:: console + + # cat /sys/class/leds//effect_index + # echo > /sys/class/leds//effect + # echo 1 > /sys/class/leds//speed + +Turning lighting off without changing the selected effect: +---------------------------------------------------------- +.. code-block:: console + + # echo false > /sys/class/leds//enabled + +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 a857e8e07d0e8d..822bccb26e6174 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -15135,6 +15135,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-test.c F: drivers/leds/led-class-dynamic.c F: include/linux/led-dynamic-lighting.h From cfc58ecbbff169224f035381193ce09272e99603 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 19 Oct 2026 10:34:31 +0200 Subject: [PATCH 3/7] HID: asus: Add Aura protocol and Dynamic Lighting nodes Add Dynamic Lighting class support to hid-asus for Aura-capable ROG keyboards and chassis lightbars. Discover Aura layout, lightbar, and per-key/direct RGB from HID feature reports rather than DMI board lists. Register asus::keyboard and, when the chassis lightbar is present, asus::lightbar. Each node keeps its own effect and palette. Publish the firmware effect list from the 0x9e capability mask and advertise direct when the keyboard path supports packed RGB. Lighting off uses enabled rather than a dedicated off effect. Commit firmware effects with Aura 0xb3/0xb5/0xb4. Direct frames use Aura 0xBC. Do not claim USB 0x193b in this change; Slash and AniMe bind in later commits after report-size and DMI discrimination. Signed-off-by: Marco Scardovi --- .../testing/sysfs-class-led-driver-hid-asus | 15 + MAINTAINERS | 8 + drivers/hid/Kconfig | 1 + drivers/hid/hid-asus.c | 1880 +++++++++++++++-- 4 files changed, 1724 insertions(+), 180 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..367b1ea2343171 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus @@ -0,0 +1,15 @@ +What: /sys/class/leds/asus::keyboard/ +What: /sys/class/leds/asus::lightbar/ +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: ASUS Aura keyboard and chassis lightbar Dynamic Lighting nodes. + Each node accepts its own effect, palette, and direct RGB + writes. Firmware effects, including static color, are + committed with Aura 0xb3/0xb5/0xb4 so the controller keeps + them across reboot. Brightness does not rewrite that packet + and does not gate the zone: on asus::keyboard it is the + firmware backlight, and the lightbar has no separate dimmer. + enabled gates the zone with the power command. The logo + tracks the keyboard power states. The lightbar node is absent + when the device has no chassis lightbar. diff --git a/MAINTAINERS b/MAINTAINERS index 822bccb26e6174..1a2c9a209a40f0 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11856,6 +11856,14 @@ F: include/linux/pm.h F: include/linux/suspend.h F: kernel/power/ +HID ASUS DRIVERS +M: Marco Scardovi +M: Denis Benato +L: linux-input@vger.kernel.org +S: Maintained +W: https://asus-linux.org/ +F: Documentation/ABI/testing/sysfs-class-led-driver-hid-asus +F: drivers/hid/hid-asus.c HID CORE LAYER M: Jiri Kosina diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig index 2481bc01c435b1..b98eb0181e8c19 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 613b12427ca435..0f7299fc9f1043 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -30,6 +30,10 @@ #include /* For to_usb_interface for T100 touchpad intf check */ #include #include +#include +#include +#include +#include #include "hid-ids.h" @@ -37,6 +41,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 @@ -57,6 +63,113 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #define ROG_ALLY_X_MIN_MCU 313 #define ROG_ALLY_MIN_MCU 319 +#define AURA_FEATURE_REPORT_SIZE 64 +#define ASUS_AURA_MAX_EFFECTS 13 + +#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_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 + /* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */ #define ASUS_SPURIOUS_CODE_0XEA 0xea #define ASUS_SPURIOUS_CODE_0XEC 0xec @@ -178,6 +291,45 @@ struct asus_drvdata { struct asus_hid_listener listener; bool fn_lock; struct asus_xgm_led *xgm_led; +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + struct mutex aura_lock; /* Serializes Aura HID reports and buffers */ + u8 *aura_buf; + /* + * 0 until the first successful write on that report id, then the HID + * path that worked. 0x5d and 0x5e are cached separately. + */ + u8 aura_write_5d; + u8 aura_write_5e; + bool kbd_power_on; + bool lb_power_on; + u32 kbd_power_states; + u32 lb_power_states; + struct led_classdev_dynamic dldev_kbd; + struct led_classdev_dynamic dldev_lightbar; + struct dl_rgb kbd_palette[2]; + struct dl_rgb lightbar_palette[2]; + const char *effects_kbd[ASUS_AURA_MAX_EFFECTS]; + const char *effects_lightbar[ASUS_AURA_MAX_EFFECTS]; + bool has_dldev_kbd; + bool has_dldev_lightbar; + bool kbd_effect_set; + bool lightbar_effect_set; + bool boot_effect_valid; + u8 boot_zone; + u8 boot_mode; + u8 boot_r; + u8 boot_g; + u8 boot_b; + u8 boot_r2; + u8 boot_g2; + u8 boot_b2; + u8 boot_speed; + u8 boot_direction; + bool is_strix_4zone; + bool has_lightbar; + u8 kbd_direct_buf[ROG_STRIX_4ZONE_KBD_BUF_SIZE]; + u8 lb_direct_buf[ROG_STRIX_LIGHTBAR_BUF_SIZE]; +#endif }; static int asus_report_battery(struct asus_drvdata *, u8 *, int); @@ -585,15 +737,37 @@ 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) { u8 *dmabuf __free(kfree) = kmemdup(buf, buf_size, GFP_KERNEL); + int ret; + if (!dmabuf) return -ENOMEM; + /* + * For Aura/Slash LED reports (0x5d/0x5e), match asus_aura_set_feature: + * interrupt OUT, then raw OUTPUT, then raw FEATURE. + */ + 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; + + ret = hid_hw_raw_request(hdev, buf[0], dmabuf, buf_size, + HID_OUTPUT_REPORT, HID_REQ_SET_REPORT); + if (ret >= 0) + return 0; + } + /* * 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, HID_FEATURE_REPORT, + HID_REQ_SET_REPORT); + if (ret < 0) + return ret; + + return 0; } static int asus_kbd_init(struct hid_device *hdev, u8 report_id) @@ -1011,245 +1185,1552 @@ static int asus_kbd_register_leds(struct hid_device *hdev) return ret; } +#if 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 + * Missing transfer type. A timeout or I/O error keeps the cached path. + * usbhid returns -ENOSYS when urbout is absent (no Interrupt OUT), which is + * the common case on Aura N-KEY interfaces that only expose Interrupt IN. */ -static int asus_parse_battery(struct asus_drvdata *drvdata, u8 *data, int size) +static bool asus_aura_path_missing(int ret) { - u8 sts; - u8 lvl; - int val; + return ret == -ENOSYS || ret == -EOPNOTSUPP || ret == -EINVAL || + ret == -EPIPE; +} - lvl = data[1]; - sts = data[8]; +static u8 *asus_aura_write_slot(struct asus_drvdata *drvdata, u8 report_id) +{ + if (report_id == FEATURE_KBD_LED_REPORT_ID2) + return &drvdata->aura_write_5e; - drvdata->battery_capacity = ((int)lvl * 100) / (int)BATTERY_LEVEL_MAX; + return &drvdata->aura_write_5d; +} - 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: +static int asus_aura_try_path(struct asus_drvdata *drvdata, u8 path) +{ + switch (path) { + case 1: + if (!drvdata->hdev->ll_driver || + !drvdata->hdev->ll_driver->output_report) + return -EOPNOTSUPP; + return hid_hw_output_report(drvdata->hdev, drvdata->aura_buf, + AURA_FEATURE_REPORT_SIZE); + case 2: + return hid_hw_raw_request(drvdata->hdev, drvdata->aura_buf[0], + drvdata->aura_buf, + AURA_FEATURE_REPORT_SIZE, + HID_OUTPUT_REPORT, + HID_REQ_SET_REPORT); + case 3: + return hid_hw_raw_request(drvdata->hdev, drvdata->aura_buf[0], + drvdata->aura_buf, + AURA_FEATURE_REPORT_SIZE, + HID_FEATURE_REPORT, + HID_REQ_SET_REPORT); default: - val = POWER_SUPPLY_STATUS_DISCHARGING; - break; + return -EINVAL; } - drvdata->battery_stat = val; - - return 0; } -static int asus_report_battery(struct asus_drvdata *drvdata, u8 *data, int size) +static int asus_aura_scan_paths(struct asus_drvdata *drvdata, u8 *slot) { - /* notify only the autonomous event by device */ - if ((drvdata->battery_in_query == false) && - (size == BATTERY_REPORT_SIZE)) - power_supply_changed(drvdata->battery); + static const u8 order[] = { 1, 2, 3 }; + unsigned int i; + int ret = -EIO; + + for (i = 0; i < ARRAY_SIZE(order); i++) { + ret = asus_aura_try_path(drvdata, order[i]); + if (ret >= 0) { + *slot = order[i]; + return 0; + } + if (!asus_aura_path_missing(ret)) + return ret; + } - return 0; + return ret; } -static int asus_battery_query(struct asus_drvdata *drvdata) +static int asus_aura_set_feature_unlocked(struct asus_drvdata *drvdata, + const u8 *buf, size_t buf_size) { - u8 *buf; - int ret = 0; + u8 *slot; + 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; + 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); - kfree(buf); + /* + * Match Armoury Crate: interrupt OUT, then raw OUTPUT, then FEATURE. + * Remember the path that works for this report id. A later timeout + * does not abandon it; only a missing transfer type does. + */ + slot = asus_aura_write_slot(drvdata, buf[0]); + if (*slot) { + ret = asus_aura_try_path(drvdata, *slot); + if (ret >= 0) + return 0; + if (!asus_aura_path_missing(ret)) + return ret; + *slot = 0; + } - return ret; + return asus_aura_scan_paths(drvdata, slot); } -static enum power_supply_property asus_battery_props[] = { - POWER_SUPPLY_PROP_STATUS, - POWER_SUPPLY_PROP_PRESENT, - POWER_SUPPLY_PROP_CAPACITY, - POWER_SUPPLY_PROP_SCOPE, - POWER_SUPPLY_PROP_MODEL_NAME, -}; +static int asus_aura_set_feature(struct asus_drvdata *drvdata, + const u8 *buf, size_t buf_size) +{ + guard(mutex)(&drvdata->aura_lock); -#define QUERY_MIN_INTERVAL (60 * HZ) /* 60[sec] */ + return asus_aura_set_feature_unlocked(drvdata, buf, buf_size); +} -static int asus_battery_get_property(struct power_supply *psy, - enum power_supply_property psp, - union power_supply_propval *val) +static int asus_aura_get_feature(struct asus_drvdata *drvdata, + u8 *buf, size_t buf_size) { - struct asus_drvdata *drvdata = power_supply_get_drvdata(psy); - int ret = 0; + 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; - } + if (buf_size > AURA_FEATURE_REPORT_SIZE) + return -EINVAL; + + guard(mutex)(&drvdata->aura_lock); + + 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 int asus_battery_probe(struct hid_device *hdev) +static int asus_aura_commit(struct asus_drvdata *drvdata) { - struct asus_drvdata *drvdata = hid_get_drvdata(hdev); - struct power_supply_config pscfg = { .drv_data = drvdata }; - 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; - drvdata->battery_capacity = 0; - drvdata->battery_stat = POWER_SUPPLY_STATUS_UNKNOWN; - drvdata->battery_in_query = false; + /* + * 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; - 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, buf_apply, sizeof(buf_apply)); + if (ret < 0) + return ret; - drvdata->battery_next_query = jiffies; + return asus_aura_set_feature(drvdata, buf_set, sizeof(buf_set)); +} - 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"); +static int asus_aura_query_run_mode(struct asus_drvdata *drvdata, u8 selector, + u8 *buf, size_t buf_size) +{ + u8 req[AURA_FEATURE_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, + AURA_CMD_RUN_MODE, + 0x01, + selector, + }; + int ret; + + ret = asus_aura_set_feature(drvdata, req, sizeof(req)); + if (ret < 0) return ret; - } - power_supply_powers(drvdata->battery, &hdev->dev); + memset(buf, 0, buf_size); + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + + ret = asus_aura_get_feature(drvdata, buf, buf_size); + if (ret < 0) + return ret; + + if (ret < 5 || buf[1] != AURA_CMD_RUN_MODE || buf[2] != 0x01 || + buf[3] != selector || buf[4] != 0x01) + return -ENODATA; return ret; } -static int asus_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value) +static int asus_aura_get_effect_mask(struct asus_drvdata *drvdata, u8 effect_mask[2]) { - const u8 buf[ROG_XGM_REPORT_SIZE] = { - FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00 - }; - struct asus_xgm_led *xgm = container_of(led_cdev, struct asus_xgm_led, cdev); + u8 buf[AURA_FEATURE_REPORT_SIZE]; int ret; - ret = asus_kbd_set_report(xgm->hdev, buf, ROG_XGM_REPORT_SIZE); - if (ret < 0) { - hid_err(xgm->hdev, "Unable to set XG mobile led state: %d\n", ret); + ret = asus_aura_query_run_mode(drvdata, 0x20, buf, sizeof(buf)); + if (ret >= 22) + goto found; + + ret = asus_aura_query_run_mode(drvdata, 0x15, buf, sizeof(buf)); + if (ret < 0) return ret; - } else if (ret != ROG_XGM_REPORT_SIZE) { - hid_err(xgm->hdev, "Unexpected partial transfer to XG mobile: %d\n", ret); - return -EIO; - } + if (ret < 22) + return -ENODATA; + +found: + effect_mask[0] = buf[20]; + effect_mask[1] = buf[21]; return 0; } -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); +static const struct { + const char *name; + u8 hw_mode; +} asus_aura_effect_map[] = { + { "static", AURA_EFFECT_STATIC }, + { "breathe", AURA_EFFECT_BREATHING }, + { "rainbow_cycle", AURA_EFFECT_SPECTRUM_CYCLE }, + { "rainbow_wave", AURA_EFFECT_RAINBOW }, + { "star", AURA_EFFECT_STARS }, + { "rain", AURA_EFFECT_RAIN }, + { "highlight", AURA_EFFECT_REACTIVE }, + { "laser", AURA_EFFECT_LASER }, + { "ripple", AURA_EFFECT_RIPPLE }, + { "pulse", AURA_EFFECT_PULSE }, + { "comet", AURA_EFFECT_COMET }, + { "flash", AURA_EFFECT_FLASH }, +}; - /* T100CHI uses MULTI_INPUT, bind the touchpad to the mouse hid_input */ - if (drvdata->quirks & QUIRK_T100CHI && - hi->report->id != T100CHI_MOUSE_REPORT_ID) - return 0; +static unsigned int asus_aura_fill_effects(const char **dst, + const u8 *effect_mask, + bool direct_capable) +{ + unsigned int i, n = 0; + u16 mask = effect_mask ? (effect_mask[0] | ((u16)effect_mask[1] << 8)) + : (u16)~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 */ - } + for (i = 0; i < ARRAY_SIZE(asus_aura_effect_map); i++) { + if (mask & BIT(asus_aura_effect_map[i].hw_mode)) + dst[n++] = asus_aura_effect_map[i].name; } + if (direct_capable) + dst[n++] = "direct"; - if (drvdata->tp) { - int ret; + return n; +} - input_set_abs_params(input, ABS_MT_POSITION_X, 0, - drvdata->tp->max_x, 0, 0); - input_set_abs_params(input, ABS_MT_POSITION_Y, 0, - drvdata->tp->max_y, 0, 0); - input_abs_set_res(input, ABS_MT_POSITION_X, drvdata->tp->res_x); - input_abs_set_res(input, ABS_MT_POSITION_Y, drvdata->tp->res_y); +static int asus_aura_hw_mode_from_name(const char *name, u8 *aura_mode) +{ + unsigned int i; - 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); + if (!name) + return -EINVAL; + + for (i = 0; i < ARRAY_SIZE(asus_aura_effect_map); i++) { + if (!strcmp(name, asus_aura_effect_map[i].name)) { + *aura_mode = asus_aura_effect_map[i].hw_mode; + return 0; } + } - __set_bit(BTN_LEFT, input->keybit); - __set_bit(INPUT_PROP_BUTTONPAD, input->propbit); + return -EINVAL; +} - ret = input_mt_init_slots(input, drvdata->tp->max_contacts, - INPUT_MT_POINTER); +static bool asus_aura_hw_mode_known(u8 mode) +{ + unsigned int i; - if (ret) { - hid_err(hdev, "Asus input mt init slots failed: %d\n", ret); - return ret; - } + for (i = 0; i < ARRAY_SIZE(asus_aura_effect_map); i++) { + if (asus_aura_effect_map[i].hw_mode == mode) + return true; } - drvdata->input = input; + return false; +} - if ((drvdata->quirks & QUIRK_HID_FN_LOCK) && - (asus_kbd_fn_lock_set(drvdata, true))) - hid_warn(hdev, "Error while setting FN lock to ON\n"); +static bool asus_aura_speed_known(u8 speed) +{ + return speed == 0 || speed == AURA_SPEED_SLOW || + speed == AURA_SPEED_MED || speed == AURA_SPEED_FAST; +} + +static const u8 asus_aura_hw_speed[] = { + [0] = AURA_SPEED_SLOW, + [1] = AURA_SPEED_MED, + [2] = AURA_SPEED_FAST, +}; + +static u8 asus_aura_speed_to_hw(unsigned int speed) +{ + if (speed < ARRAY_SIZE(asus_aura_hw_speed)) + return asus_aura_hw_speed[speed]; + + return AURA_SPEED_MED; +} + +static unsigned int asus_aura_speed_from_hw(u8 speed) +{ + if (speed == AURA_SPEED_SLOW) + return 0; + if (speed == AURA_SPEED_FAST) + return 2; + + return 1; +} + +/* Hardware direction byte for enum dl_direction. */ +static const u8 asus_aura_hw_direction[] = { + [DL_DIRECTION_LEFT] = 1, + [DL_DIRECTION_RIGHT] = 0, + [DL_DIRECTION_UP] = 2, + [DL_DIRECTION_DOWN] = 3, +}; + +static u8 asus_aura_direction_to_hw(enum dl_direction direction) +{ + if (direction < ARRAY_SIZE(asus_aura_hw_direction)) + return asus_aura_hw_direction[direction]; + + return asus_aura_hw_direction[DL_DIRECTION_RIGHT]; +} + +static enum dl_direction asus_aura_direction_from_hw(u8 direction) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(asus_aura_hw_direction); i++) { + if (asus_aura_hw_direction[i] == direction) + return i; + } + + return DL_DIRECTION_RIGHT; +} + +/* + * Aura 0xb3 is write-only. Read the feature report before any SET in probe: + * some firmware still holds the last committed Set Effect packet. Adopt that + * into the software node when the payload is a known Set Effect. Never write + * a color from probe: userspace owns the first commit. + */ +static void asus_aura_capture_boot_effect(struct hid_device *hdev) +{ + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + u8 buf[AURA_FEATURE_REPORT_SIZE]; + int 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; + + if (ret < 13 || buf[1] != AURA_CMD_SET_EFFECT || + buf[2] > AURA_ZONE_BAR_RIGHT || + !asus_aura_hw_mode_known(buf[3]) || + !asus_aura_speed_known(buf[7])) + return; + + drvdata->boot_effect_valid = true; + drvdata->boot_zone = buf[2]; + drvdata->boot_mode = buf[3]; + drvdata->boot_r = buf[4]; + drvdata->boot_g = buf[5]; + drvdata->boot_b = buf[6]; + drvdata->boot_speed = buf[7]; + drvdata->boot_direction = buf[8]; + drvdata->boot_r2 = buf[10]; + drvdata->boot_g2 = buf[11]; + drvdata->boot_b2 = buf[12]; +} + +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; + + if (!drvdata->has_lightbar) + return 0; + + return asus_aura_activate_zone(drvdata, AURA_ZONE_ACTIVATE_LIGHTBAR); +} + +static int asus_aura_apply_effect(struct led_classdev_dynamic *ldev); + +static void asus_aura_mark_effect_set(struct asus_drvdata *drvdata, + struct led_classdev_dynamic *ldev) +{ + if (ldev == &drvdata->dldev_kbd) + drvdata->kbd_effect_set = true; + else if (ldev == &drvdata->dldev_lightbar) + drvdata->lightbar_effect_set = true; +} + +static void asus_aura_restore(struct asus_drvdata *drvdata) +{ + int ret; + + if (!drvdata->kbd_effect_set && !drvdata->lightbar_effect_set) + return; + + ret = asus_aura_activate_zones(drvdata); + if (ret < 0) + hid_warn(drvdata->hdev, "Failed to activate Aura zones: %d\n", ret); + + if (drvdata->has_dldev_kbd && drvdata->kbd_effect_set) { + ret = asus_aura_apply_effect(&drvdata->dldev_kbd); + if (ret < 0) + hid_warn(drvdata->hdev, + "Failed to restore keyboard effect: %d\n", ret); + } + if (drvdata->has_dldev_lightbar && drvdata->lightbar_effect_set) { + ret = asus_aura_apply_effect(&drvdata->dldev_lightbar); + if (ret < 0) + hid_warn(drvdata->hdev, + "Failed to restore lightbar effect: %d\n", ret); + } +} + +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 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_try_apply_unlocked(struct asus_drvdata *drvdata) +{ + return -ENODEV; +} + +static void asus_lamparray_release_unlocked(struct asus_drvdata *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_lightbar(struct asus_drvdata *drvdata, + struct led_classdev_dynamic *ldev) +{ + return ldev == &drvdata->dldev_lightbar; +} + +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 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; + + if (size != ldev->led_count * 3) + return -EINVAL; + + if (!led_dynamic_effect_is(ldev, "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; + + if (size != ldev->led_count * 3) + return -EINVAL; + + if (!led_dynamic_effect_is(ldev, "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); +} + +/* + * Commit a firmware effect with the color stored in the palette. Brightness + * and enabled do not scale or clear this packet: 0xb3 is what the controller + * keeps across reboot, so it must stay the unscaled color. + */ +static int asus_aura_commit_effect(struct led_classdev_dynamic *ldev, + const char *name, unsigned int speed, + enum dl_direction direction, + const struct dl_rgb *palette, + unsigned int num_entries) +{ + struct asus_drvdata *drvdata = ldev->driver_data; + u8 aura_mode; + u8 r = 0, g = 0, b = 0; + u8 r2 = 0, g2 = 0, b2 = 0; + int ret; + + if (name && !strcmp(name, "direct")) + return 0; + + if (asus_aura_hw_mode_from_name(name, &aura_mode)) + return -EINVAL; + + if (palette && num_entries > 0) { + r = palette[0].r; + g = palette[0].g; + b = palette[0].b; + if (num_entries > 1) { + r2 = palette[1].r; + g2 = palette[1].g; + b2 = palette[1].b; + } + } + + /* + * Release LampArray before a firmware effect when a later commit uses + * it as the direct-RGB backend. + */ + scoped_guard(mutex, &drvdata->aura_lock) + asus_lamparray_release_unlocked(drvdata); + + if (asus_aura_is_lightbar(drvdata, ldev)) + ret = asus_aura_write_zone_range(drvdata, AURA_ZONE_BAR_LEFT, + AURA_ZONE_BAR_RIGHT, aura_mode, + r, g, b, + asus_aura_speed_to_hw(speed), + asus_aura_direction_to_hw(direction), + r2, g2, b2); + else if (drvdata->is_strix_4zone) + ret = asus_aura_write_zone_range(drvdata, AURA_ZONE_KEY1, + AURA_ZONE_KEY4, aura_mode, + r, g, b, + asus_aura_speed_to_hw(speed), + asus_aura_direction_to_hw(direction), + r2, g2, b2); + else + ret = asus_aura_write_zone_effect(drvdata, AURA_ZONE_ALL, + aura_mode, r, g, b, + asus_aura_speed_to_hw(speed), + asus_aura_direction_to_hw(direction), + r2, g2, b2, true); + if (ret < 0) + return ret; + + asus_aura_mark_effect_set(drvdata, ldev); + return 0; +} + +static int asus_aura_apply_effect(struct led_classdev_dynamic *ldev) +{ + return asus_aura_commit_effect(ldev, led_dynamic_effect_name(ldev), + ldev->speed, ldev->direction, + ldev->palette, ldev->num_palette_entries); +} + +static int asus_aura_set_effect(struct led_classdev_dynamic *ldev, + unsigned int index) +{ + if (!ldev->enabled) + return 0; + + if (index >= ldev->num_effects) + return -EINVAL; + + return asus_aura_commit_effect(ldev, ldev->effects[index], + ldev->speed, ldev->direction, + ldev->palette, ldev->num_palette_entries); +} + +static int asus_aura_sync_power(struct asus_drvdata *drvdata, + struct led_classdev_dynamic *which, + bool enabled, u32 states); + +static int asus_aura_set_enabled(struct led_classdev_dynamic *ldev, bool enabled) +{ + struct asus_drvdata *drvdata = ldev->driver_data; + int ret; + + ret = asus_aura_sync_power(drvdata, ldev, enabled, + ldev->active_power_states); + if (ret < 0 || !enabled) + return ret; + + return asus_aura_apply_effect(ldev); +} + +static int asus_aura_set_speed(struct led_classdev_dynamic *ldev, + unsigned int speed) +{ + if (!ldev->enabled) + return 0; + + return asus_aura_commit_effect(ldev, led_dynamic_effect_name(ldev), + speed, ldev->direction, + ldev->palette, ldev->num_palette_entries); +} + +static int asus_aura_set_direction(struct led_classdev_dynamic *ldev, + enum dl_direction direction) +{ + if (!ldev->enabled) + return 0; + + return asus_aura_commit_effect(ldev, led_dynamic_effect_name(ldev), + ldev->speed, direction, + ldev->palette, ldev->num_palette_entries); +} + +static int asus_aura_set_palette(struct led_classdev_dynamic *ldev, + const struct dl_rgb *palette, + unsigned int num_entries) +{ + if (!palette || !num_entries || num_entries > ldev->max_palette_entries) + return -EINVAL; + + if (!ldev->enabled) + return 0; + + return asus_aura_commit_effect(ldev, led_dynamic_effect_name(ldev), + ldev->speed, ldev->direction, + palette, num_entries); +} + +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; + + guard(mutex)(&ldev->lock); + + /* + * Firmware backlight only. Zone power belongs to enabled. brightness + * 0 must not gate the zone: led_trigger_remove() calls this with + * LED_OFF, and the persisted 0xb3 color stays in place. + */ + if (ldev == &drvdata->dldev_kbd) + asus_kbd_set_brightness(drvdata->hdev, brightness); + + return 0; +} + +static void asus_aura_power_bits(u32 states, u8 *kbd, u8 *lightbar) +{ + *kbd = 0; + *lightbar = 0; + + if (states & DL_POWER_STATE_BOOT) { + *kbd |= AURA_POWER_KBD_BOOT | AURA_POWER_LOGO_BOOT; + *lightbar |= AURA_POWER_LB_BOOT; + } + if (states & DL_POWER_STATE_AWAKE) { + *kbd |= AURA_POWER_KBD_AWAKE | AURA_POWER_LOGO_AWAKE; + *lightbar |= AURA_POWER_LB_AWAKE; + } + if (states & DL_POWER_STATE_SLEEP) { + *kbd |= AURA_POWER_KBD_SLEEP | AURA_POWER_LOGO_SLEEP; + *lightbar |= AURA_POWER_LB_SLEEP; + } + if (states & DL_POWER_STATE_SHUTDOWN) { + *kbd |= AURA_POWER_KBD_SHUTDOWN | AURA_POWER_LOGO_SHUTDOWN; + *lightbar |= AURA_POWER_LB_SHUTDOWN; + } +} + +static int asus_aura_write_power_unlocked(struct asus_drvdata *drvdata, + u8 kbd, u8 lightbar) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, + AURA_CMD_POWER, + AURA_POWER_CMD_ENABLE, + }; + + /* + * Logo bits are already in @kbd and track the keyboard states. + * Lid and rear stay enabled so unmanaged zones are not gated off. + */ + buf[3] = kbd; + 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_unlocked(drvdata, buf, sizeof(buf)); +} + +static int asus_aura_sync_power(struct asus_drvdata *drvdata, + struct led_classdev_dynamic *which, + bool enabled, u32 states) +{ + u8 kbd = 0, lightbar = 0, unused; + + guard(mutex)(&drvdata->aura_lock); + + if (which == &drvdata->dldev_kbd) { + drvdata->kbd_power_on = enabled; + drvdata->kbd_power_states = states; + } else if (which == &drvdata->dldev_lightbar) { + drvdata->lb_power_on = enabled; + drvdata->lb_power_states = states; + } + + if (drvdata->kbd_power_on) + asus_aura_power_bits(drvdata->kbd_power_states, &kbd, &unused); + if (drvdata->lb_power_on) + asus_aura_power_bits(drvdata->lb_power_states, &unused, &lightbar); + + return asus_aura_write_power_unlocked(drvdata, kbd, lightbar); +} + +static int asus_aura_set_power_states(struct led_classdev_dynamic *ldev, + u32 active_states) +{ + struct asus_drvdata *drvdata = ldev->driver_data; + + return asus_aura_sync_power(drvdata, ldev, ldev->enabled, active_states); +} + +static const struct led_dynamic_ops asus_aura_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_enabled = asus_aura_set_enabled, + .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_enabled = asus_aura_set_enabled, + .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 void asus_aura_dldev_set_default_palette(struct led_classdev_dynamic *ldev, + struct dl_rgb *palette) +{ + ldev->palette = palette; + palette[0].r = 255; + palette[0].g = 0; + 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, + const char **effects, + unsigned int num_effects) +{ + unsigned int i; + + ldev->cdev.name = name; + ldev->cdev.max_brightness = 255; + ldev->cdev.brightness = 255; + ldev->cdev.brightness_set_blocking = asus_aura_brightness_set_blocking; + ldev->ops = ops; + ldev->driver_data = drvdata; + ldev->speed = 1; + ldev->speed_min = 0; + ldev->speed_max = 2; + ldev->direction = DL_DIRECTION_RIGHT; + ldev->supported_directions = BIT(DL_DIRECTION_RIGHT) | + BIT(DL_DIRECTION_LEFT); + ldev->max_palette_entries = 2; + ldev->enabled = true; + ldev->effects = (const char * const *)effects; + ldev->num_effects = num_effects; + ldev->current_effect = 0; + for (i = 0; i < num_effects; i++) { + if (!strcmp(effects[i], "static")) { + ldev->current_effect = i; + break; + } + } + ldev->supported_power_states = DL_POWER_STATE_ALL; + ldev->active_power_states = DL_POWER_STATE_ALL; +} + +static int asus_aura_effect_index(struct led_classdev_dynamic *ldev, u8 hw_mode) +{ + const char *name = NULL; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(asus_aura_effect_map); i++) { + if (asus_aura_effect_map[i].hw_mode == hw_mode) { + name = asus_aura_effect_map[i].name; + break; + } + } + if (!name) + return -1; + + for (i = 0; i < ldev->num_effects; i++) { + if (!strcmp(ldev->effects[i], name)) + return i; + } + + return -1; +} + +static void asus_aura_apply_boot_levels(struct led_classdev_dynamic *ldev, + struct asus_drvdata *drvdata) +{ + ldev->speed = asus_aura_speed_from_hw(drvdata->boot_speed); + ldev->direction = asus_aura_direction_from_hw(drvdata->boot_direction); + + if (ldev->supported_directions && + !(ldev->supported_directions & BIT(ldev->direction))) + ldev->direction = DL_DIRECTION_RIGHT; +} + +static bool asus_aura_adopt_node(struct asus_drvdata *drvdata, + struct led_classdev_dynamic *ldev) +{ + int idx; + + if (!ldev->palette) + return false; + + idx = asus_aura_effect_index(ldev, drvdata->boot_mode); + if (idx < 0) + return false; + + ldev->current_effect = idx; + ldev->palette[0].r = drvdata->boot_r; + ldev->palette[0].g = drvdata->boot_g; + ldev->palette[0].b = drvdata->boot_b; + ldev->num_palette_entries = 1; + if (drvdata->boot_r2 || drvdata->boot_g2 || drvdata->boot_b2) { + ldev->palette[1].r = drvdata->boot_r2; + ldev->palette[1].g = drvdata->boot_g2; + ldev->palette[1].b = drvdata->boot_b2; + ldev->num_palette_entries = 2; + } + asus_aura_apply_boot_levels(ldev, drvdata); + return true; +} + +static bool asus_aura_adopt_boot_effect(struct asus_drvdata *drvdata) +{ + u8 zone = drvdata->boot_zone; + bool kbd = zone == AURA_ZONE_ALL || + (zone >= AURA_ZONE_KEY1 && zone <= AURA_ZONE_KEY4); + bool lightbar = zone == AURA_ZONE_ALL || + zone == AURA_ZONE_BAR_LEFT || + zone == AURA_ZONE_BAR_RIGHT; + bool any = false; + + if (kbd && drvdata->dldev_kbd.palette && + asus_aura_adopt_node(drvdata, &drvdata->dldev_kbd)) { + drvdata->kbd_effect_set = true; + any = true; + } + + if (lightbar && drvdata->dldev_lightbar.palette && + asus_aura_adopt_node(drvdata, &drvdata->dldev_lightbar)) { + drvdata->lightbar_effect_set = true; + any = true; + } + + if (any) { + asus_lamparray_fill_solid(drvdata->kbd_direct_buf, + ROG_STRIX_4ZONE_KBD_LEDS, + drvdata->boot_r, drvdata->boot_g, + drvdata->boot_b); + asus_lamparray_fill_solid(drvdata->lb_direct_buf, + asus_aura_lb_led_count(drvdata), + drvdata->boot_r, drvdata->boot_g, + drvdata->boot_b); + } + + return any; +} + +static int asus_init_dynamic_lighting(struct hid_device *hdev) +{ + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + unsigned int kbd_n, lightbar_n; + u8 effect_mask[2]; + const u8 *mask = NULL; + 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); + drvdata->has_lightbar = has_lightbar; + + ret = asus_aura_wake_all_zones(drvdata); + if (ret < 0) + hid_warn(hdev, "Failed to wake Aura hardware zones: %d\n", ret); + + kbd_direct = is_per_key || drvdata->is_strix_4zone; + 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); + } else { + mask = effect_mask; + } + + kbd_n = asus_aura_fill_effects(drvdata->effects_kbd, mask, kbd_direct); + lightbar_n = asus_aura_fill_effects(drvdata->effects_lightbar, mask, + lightbar_direct); + + /* Keyboard Dynamic Lighting zone: always registered on Aura models. */ + asus_aura_dldev_common_init(&drvdata->dldev_kbd, drvdata, "asus::keyboard", + &asus_aura_ops, + drvdata->effects_kbd, kbd_n); + asus_aura_dldev_set_default_palette(&drvdata->dldev_kbd, + drvdata->kbd_palette); + drvdata->kbd_power_on = true; + drvdata->kbd_power_states = DL_POWER_STATE_ALL; + 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; + } + + if (has_lightbar) { + asus_aura_dldev_common_init(&drvdata->dldev_lightbar, drvdata, + "asus::lightbar", + &asus_aura_lightbar_ops, + drvdata->effects_lightbar, lightbar_n); + drvdata->dldev_lightbar.zone_type = "lightbar"; + drvdata->dldev_lightbar.led_count = asus_aura_lb_led_count(drvdata); + asus_aura_dldev_set_default_palette(&drvdata->dldev_lightbar, + drvdata->lightbar_palette); + drvdata->lb_power_on = true; + drvdata->lb_power_states = DL_POWER_STATE_ALL; + } + + /* + * Adopt or keep the default palette before sysfs exists, so a + * concurrent reader cannot observe a half-initialized node. + */ + if (drvdata->boot_effect_valid) + asus_aura_adopt_boot_effect(drvdata); + + 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; + + if (has_lightbar) { + 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; + } + } + + hid_info(hdev, "Registered dynamic lighting zones: kbd=%d, lightbar=%d\n", + drvdata->has_dldev_kbd, drvdata->has_dldev_lightbar); + + if (drvdata->boot_effect_valid && + (drvdata->kbd_effect_set || drvdata->lightbar_effect_set)) + hid_info(hdev, "Preserving programmed Aura effect\n"); + else + hid_info(hdev, "Leaving Aura controller state unchanged\n"); + + return 0; +} + +#else /* !IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) */ + +static inline int asus_init_dynamic_lighting(struct hid_device *hdev) +{ + return 0; +} + +static inline void asus_aura_capture_boot_effect(struct hid_device *hdev) +{ +} + +#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_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value) +{ + const u8 buf[ROG_XGM_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00 + }; + struct asus_xgm_led *xgm = container_of(led_cdev, struct asus_xgm_led, cdev); + int ret; + + ret = asus_kbd_set_report(xgm->hdev, buf, ROG_XGM_REPORT_SIZE); + if (ret < 0) { + hid_err(xgm->hdev, "Unable to set XG mobile led state: %d\n", ret); + return ret; + } else if (ret != ROG_XGM_REPORT_SIZE) { + hid_err(xgm->hdev, "Unexpected partial transfer to XG mobile: %d\n", ret); + return -EIO; + } + + return 0; +} + +static int asus_input_configured(struct hid_device *hdev, struct hid_input *hi) +{ + struct input_dev *input = hi->input; + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + + /* T100CHI uses MULTI_INPUT, bind the touchpad to the mouse hid_input */ + if (drvdata->quirks & QUIRK_T100CHI && + hi->report->id != T100CHI_MOUSE_REPORT_ID) + return 0; + + /* Handle MULTI_INPUT on E1239T mouse/touchpad USB interface */ + if (drvdata->tp && (drvdata->quirks & QUIRK_MEDION_E1239T)) { + switch (hi->report->id) { + case E1239T_TP_TOGGLE_REPORT_ID: + input_set_capability(input, EV_KEY, KEY_F21); + input->name = "Asus Touchpad Keys"; + drvdata->tp_kbd_input = input; + return 0; + case INPUT_REPORT_ID: + break; /* Touchpad report, handled below */ + default: + return 0; /* Ignore other reports */ + } + } + + if (drvdata->tp) { + int ret; + + input_set_abs_params(input, ABS_MT_POSITION_X, 0, + drvdata->tp->max_x, 0, 0); + input_set_abs_params(input, ABS_MT_POSITION_Y, 0, + drvdata->tp->max_y, 0, 0); + input_abs_set_res(input, ABS_MT_POSITION_X, drvdata->tp->res_x); + input_abs_set_res(input, ABS_MT_POSITION_Y, drvdata->tp->res_y); + + if (drvdata->tp->contact_size >= 5) { + input_set_abs_params(input, ABS_TOOL_WIDTH, 0, + MAX_TOUCH_MAJOR, 0, 0); + input_set_abs_params(input, ABS_MT_TOUCH_MAJOR, 0, + MAX_TOUCH_MAJOR, 0, 0); + input_set_abs_params(input, ABS_MT_PRESSURE, 0, + MAX_PRESSURE, 0, 0); + } + + __set_bit(BTN_LEFT, input->keybit); + __set_bit(INPUT_PROP_BUTTONPAD, input->propbit); + + ret = input_mt_init_slots(input, drvdata->tp->max_contacts, + INPUT_MT_POINTER); + + if (ret) { + hid_err(hdev, "Asus input mt init slots failed: %d\n", ret); + return ret; + } + } + + drvdata->input = input; + + if ((drvdata->quirks & QUIRK_HID_FN_LOCK) && + (asus_kbd_fn_lock_set(drvdata, true))) + hid_warn(hdev, "Error while setting FN lock to ON\n"); return 0; } @@ -1482,6 +2963,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_lightbar) + 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 @@ -1500,6 +2986,12 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev) if (!drvdata->tp) asus_initialize_reports(hdev); +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + if (drvdata->has_dldev_kbd || drvdata->has_dldev_lightbar) + asus_aura_restore(drvdata); +#endif + + if (drvdata->tp) return asus_start_multitouch(hdev); @@ -1520,6 +3012,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; /* @@ -1594,6 +3097,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) return ret; } + /* 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) { @@ -1609,6 +3113,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) if (is_vendor && (drvdata->quirks & QUIRK_ROG_NKEY_KEYBOARD)) hdev->quirks |= HID_QUIRK_HIDINPUT_FORCE; + ret = asus_worker_create(hdev, drvdata); if (ret) { hid_warn(hdev, "Failed to initialize worker: %d\n", ret); @@ -1622,6 +3127,13 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) return ret; } + /* + * Read any committed Aura effect before the keyboard handshake + * overwrites the feature report. + */ + if (asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID1)) + asus_aura_capture_boot_effect(hdev); + if (!drvdata->tp) asus_initialize_reports(hdev); @@ -1641,6 +3153,13 @@ 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); + } + /* * For ROG keyboards, skip rename for consistency and ->input check as * some devices do not have inputs. @@ -1690,6 +3209,7 @@ static void asus_remove(struct hid_device *hdev) if (drvdata->xgm_led) devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev); + asus_worker_stop(drvdata->worker); hid_hw_stop(hdev); } From fc0318ef5d109f894b0b648d646f2b29abe4292d Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 19 Oct 2026 10:14:35 +0200 Subject: [PATCH 4/7] HID: asus: Use LampArray as in-kernel direct-RGB backend Prefer a sibling HID LampArray interface (Usage Page 0x59) for direct RGB when present, and fall back to Aura 0xBC on -ENODEV. Bind the LampArray interface without hidraw so lighting stays under this driver. When a zone has no matching lamps, return -ENODEV before take_control so an empty match does not claim the sibling and report success. Track lightbar direct frames with lb_direct_valid so non-4-zone lightbar restores and blanking keep the correct cached buffer. Still do not claim USB 0x193b; Slash and AniMe arrive in later commits. Signed-off-by: Marco Scardovi --- .../testing/sysfs-class-led-driver-hid-asus | 6 +- drivers/hid/hid-asus.c | 767 ++++++++++++++++-- 2 files changed, 702 insertions(+), 71 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus index 367b1ea2343171..fbdcccb7fbb38d 100644 --- a/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus +++ b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus @@ -11,5 +11,7 @@ Description: ASUS Aura keyboard and chassis lightbar Dynamic Lighting nodes. and does not gate the zone: on asus::keyboard it is the firmware backlight, and the lightbar has no separate dimmer. enabled gates the zone with the power command. The logo - tracks the keyboard power states. The lightbar node is absent - when the device has no chassis lightbar. + tracks the keyboard power states. Direct RGB may be backed by HID LampArray + (Usage Page 0x59) when Aura 0xBC cannot address the lightbar + independently. The lightbar node is absent when the device + has no chassis lightbar. diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 0f7299fc9f1043..24a6dbac5130b9 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -170,6 +170,31 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #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 / asus::*) 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 #define ASUS_SPURIOUS_CODE_0XEC 0xec @@ -274,6 +299,14 @@ struct asus_touchpad_info { int report_size; }; +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) +struct asus_lamparray_lamp { + u16 id; + s32 x; + bool keyboard; +}; +#endif + struct asus_drvdata { unsigned long quirks; struct hid_device *hdev; @@ -329,6 +362,16 @@ struct asus_drvdata { bool has_lightbar; u8 kbd_direct_buf[ROG_STRIX_4ZONE_KBD_BUF_SIZE]; u8 lb_direct_buf[ROG_STRIX_LIGHTBAR_BUF_SIZE]; + bool kbd_direct_valid; + bool lb_direct_valid; + 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 }; @@ -1145,6 +1188,32 @@ 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; +} static int asus_kbd_register_leds(struct hid_device *hdev) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); @@ -1394,22 +1463,28 @@ static int asus_aura_get_effect_mask(struct asus_drvdata *drvdata, u8 effect_mas return 0; } +/* + * Notebook 0x9e capability bits (Armoury Crate). hw_mode is the 0xb3 opcode; + * cap_bit is the bit in bytes 20..21. They match for modes 0..8, then skip the + * unused byte21 bit2: Pulse is mode 0x0a but cap bit 9, not BIT(0x0a). + */ static const struct { const char *name; u8 hw_mode; + u16 cap_bit; } asus_aura_effect_map[] = { - { "static", AURA_EFFECT_STATIC }, - { "breathe", AURA_EFFECT_BREATHING }, - { "rainbow_cycle", AURA_EFFECT_SPECTRUM_CYCLE }, - { "rainbow_wave", AURA_EFFECT_RAINBOW }, - { "star", AURA_EFFECT_STARS }, - { "rain", AURA_EFFECT_RAIN }, - { "highlight", AURA_EFFECT_REACTIVE }, - { "laser", AURA_EFFECT_LASER }, - { "ripple", AURA_EFFECT_RIPPLE }, - { "pulse", AURA_EFFECT_PULSE }, - { "comet", AURA_EFFECT_COMET }, - { "flash", AURA_EFFECT_FLASH }, + { "static", AURA_EFFECT_STATIC, BIT(0) }, + { "breathe", AURA_EFFECT_BREATHING, BIT(1) }, + { "rainbow_cycle", AURA_EFFECT_SPECTRUM_CYCLE, BIT(2) }, + { "rainbow_wave", AURA_EFFECT_RAINBOW, BIT(3) }, + { "star", AURA_EFFECT_STARS, BIT(4) }, + { "rain", AURA_EFFECT_RAIN, BIT(5) }, + { "highlight", AURA_EFFECT_REACTIVE, BIT(6) }, + { "laser", AURA_EFFECT_LASER, BIT(7) }, + { "ripple", AURA_EFFECT_RIPPLE, BIT(8) }, + { "pulse", AURA_EFFECT_PULSE, BIT(9) }, + { "comet", AURA_EFFECT_COMET, BIT(11) }, + { "flash", AURA_EFFECT_FLASH, BIT(12) }, }; static unsigned int asus_aura_fill_effects(const char **dst, @@ -1421,7 +1496,7 @@ static unsigned int asus_aura_fill_effects(const char **dst, : (u16)~0; for (i = 0; i < ARRAY_SIZE(asus_aura_effect_map); i++) { - if (mask & BIT(asus_aura_effect_map[i].hw_mode)) + if (mask & asus_aura_effect_map[i].cap_bit) dst[n++] = asus_aura_effect_map[i].name; } if (direct_capable) @@ -1627,31 +1702,6 @@ static void asus_aura_mark_effect_set(struct asus_drvdata *drvdata, drvdata->lightbar_effect_set = true; } -static void asus_aura_restore(struct asus_drvdata *drvdata) -{ - int ret; - - if (!drvdata->kbd_effect_set && !drvdata->lightbar_effect_set) - return; - - ret = asus_aura_activate_zones(drvdata); - if (ret < 0) - hid_warn(drvdata->hdev, "Failed to activate Aura zones: %d\n", ret); - - if (drvdata->has_dldev_kbd && drvdata->kbd_effect_set) { - ret = asus_aura_apply_effect(&drvdata->dldev_kbd); - if (ret < 0) - hid_warn(drvdata->hdev, - "Failed to restore keyboard effect: %d\n", ret); - } - if (drvdata->has_dldev_lightbar && drvdata->lightbar_effect_set) { - ret = asus_aura_apply_effect(&drvdata->dldev_lightbar); - if (ret < 0) - hid_warn(drvdata->hdev, - "Failed to restore lightbar effect: %d\n", ret); - } -} - 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, @@ -1691,6 +1741,322 @@ static int asus_aura_write_zone_effect(struct asus_drvdata *drvdata, u8 zone, 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; + if (!other->dev.driver || other->dev.driver != intf->dev.driver) + 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; + if (!other->dev.driver || other->dev.driver != intf->dev.driver) + 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 cmp_int(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) { @@ -1709,21 +2075,83 @@ static unsigned int asus_aura_lb_led_count(struct asus_drvdata *drvdata) 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); -} +/* + * Update only the lamps for one zone. MultiUpdate lists those lamp ids, so + * the other zone keeps whatever the controller already shows. The direct + * buffers start empty; they are not prefilled red. + */ +static int asus_lamparray_apply_unlocked(struct asus_drvdata *drvdata, + bool keyboard) +{ + unsigned int match[ASUS_LAMPARRAY_MAX_LAMPS]; + unsigned int nmatch = 0, idx = 0, i; + unsigned int src_leds; + const u8 *src; + u8 *buf = drvdata->lamparray_buf; + u8 rid_multi = asus_lamparray_rid(drvdata, ASUS_LAMPARRAY_RID_MULTI); + size_t multi_len; + int ret; -static int asus_lamparray_try_apply_unlocked(struct asus_drvdata *drvdata) -{ - return -ENODEV; + 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 == keyboard) + match[nmatch++] = i; + } + if (!nmatch) + return 0; + + src = keyboard ? drvdata->kbd_direct_buf : drvdata->lb_direct_buf; + src_leds = keyboard ? ROG_STRIX_4ZONE_KBD_LEDS : + asus_aura_lb_led_count(drvdata); + + multi_len = asus_lamparray_report_len(drvdata->lamparray_hdev, rid_multi); + if (multi_len < 51) + return -EPROTO; + + for (i = 0; i < nmatch; i += ASUS_LAMPARRAY_MULTI_MAX) { + unsigned int n = min_t(unsigned int, ASUS_LAMPARRAY_MULTI_MAX, + nmatch - 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 >= nmatch) ? ASUS_LAMPARRAY_FLAG_COMPLETE : 0; + + for (j = 0; j < n; j++) { + unsigned int lamp = match[i + j]; + u8 r, g, b; + u16 id = drvdata->lamparray_lamps[lamp].id; + + asus_lamparray_sample_rgb(src, src_leds, idx++, nmatch, + &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; } -static void asus_lamparray_release_unlocked(struct asus_drvdata *drvdata) +/* Prefer LampArray; callers keep Aura 0xBC as fallback on -ENODEV. */ +static int asus_lamparray_try_apply_unlocked(struct asus_drvdata *drvdata, + bool keyboard) { + asus_lamparray_try_init_unlocked(drvdata); + if (!drvdata->lamparray_count) + return -ENODEV; + return asus_lamparray_apply_unlocked(drvdata, keyboard); } static int asus_aura_write_4zone_direct_bc_unlocked(struct asus_drvdata *drvdata) @@ -1736,12 +2164,62 @@ static int asus_aura_write_4zone_direct_bc_unlocked(struct asus_drvdata *drvdata 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); + if (drvdata->kbd_direct_valid) + memcpy(&buf[ROG_STRIX_4ZONE_DIRECT_KBD_OFFSET], + drvdata->kbd_direct_buf, + sizeof(drvdata->kbd_direct_buf)); + if (drvdata->lb_direct_valid) + 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)); +} + +/* + * Push a cached direct-RGB frame for one zone. LampArray MultiUpdate keeps the + * sibling zone untouched; the 4-zone 0xBC fallback rewrites both zone slots + * from the cached buffers. + */ +static int asus_aura_push_direct_unlocked(struct asus_drvdata *drvdata, + bool keyboard) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE]; + size_t payload_len; + int ret; + + if (keyboard) { + if (!drvdata->kbd_direct_valid) + return 0; + } else if (!drvdata->lb_direct_valid) { + return 0; + } + + ret = asus_lamparray_try_apply_unlocked(drvdata, keyboard); + if (ret != -ENODEV) + return ret; + + if (drvdata->is_strix_4zone) + return asus_aura_write_4zone_direct_bc_unlocked(drvdata); + + /* Per-key keyboard frames are not retained in kbd_direct_buf. */ + if (keyboard) + return 0; + + payload_len = asus_aura_lb_led_count(drvdata) * 3; + payload_len = min_t(size_t, payload_len, ROG_STRIX_LIGHTBAR_BUF_SIZE); + + 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; + buf[7] = (u8)(payload_len / 3); + memcpy(&buf[ROG_STRIX_PERKEY_DIRECT_PAYLOAD_OFFSET], + drvdata->lb_direct_buf, payload_len); return asus_aura_set_feature_unlocked(drvdata, buf, sizeof(buf)); } @@ -1761,8 +2239,9 @@ static int asus_aura_strix_write_direct(struct asus_drvdata *drvdata, if (buffer != drvdata->kbd_direct_buf) memcpy(drvdata->kbd_direct_buf, buffer, leds * 3); + drvdata->kbd_direct_valid = true; - ret = asus_lamparray_try_apply_unlocked(drvdata); + ret = asus_lamparray_try_apply_unlocked(drvdata, true); if (ret != -ENODEV) return ret; @@ -1870,8 +2349,9 @@ static int asus_aura_lightbar_write_packet(struct asus_drvdata *drvdata, if (buffer != drvdata->lb_direct_buf) memcpy(drvdata->lb_direct_buf, buffer, leds * 3); + drvdata->lb_direct_valid = true; - ret = asus_lamparray_try_apply_unlocked(drvdata); + ret = asus_lamparray_try_apply_unlocked(drvdata, false); if (ret != -ENODEV) return ret; @@ -1926,6 +2406,18 @@ static int asus_aura_lightbar_set_direct(struct led_classdev_dynamic *ldev, * and enabled do not scale or clear this packet: 0xb3 is what the controller * keeps across reboot, so it must stay the unscaled color. */ +static bool asus_aura_zone_wants_direct(struct asus_drvdata *drvdata, + bool keyboard) +{ + struct led_classdev_dynamic *ldev = keyboard ? &drvdata->dldev_kbd : + &drvdata->dldev_lightbar; + bool registered = keyboard ? drvdata->has_dldev_kbd : + drvdata->has_dldev_lightbar; + + return registered && ldev->enabled && + led_dynamic_effect_is(ldev, "direct"); +} + static int asus_aura_commit_effect(struct led_classdev_dynamic *ldev, const char *name, unsigned int speed, enum dl_direction direction, @@ -1933,13 +2425,17 @@ static int asus_aura_commit_effect(struct led_classdev_dynamic *ldev, unsigned int num_entries) { struct asus_drvdata *drvdata = ldev->driver_data; + bool lightbar = asus_aura_is_lightbar(drvdata, ldev); u8 aura_mode; u8 r = 0, g = 0, b = 0; u8 r2 = 0, g2 = 0, b2 = 0; int ret; - if (name && !strcmp(name, "direct")) - return 0; + if (name && !strcmp(name, "direct")) { + guard(mutex)(&drvdata->aura_lock); + + return asus_aura_push_direct_unlocked(drvdata, !lightbar); + } if (asus_aura_hw_mode_from_name(name, &aura_mode)) return -EINVAL; @@ -1956,13 +2452,18 @@ static int asus_aura_commit_effect(struct led_classdev_dynamic *ldev, } /* - * Release LampArray before a firmware effect when a later commit uses - * it as the direct-RGB backend. + * Static and animations are firmware effects: commit them with 0xb3 + * so the controller keeps the color across reboot. LampArray stays + * the direct-RGB backend and must be released first. Re-apply the + * sibling zone afterwards if it is still in host direct mode. */ - scoped_guard(mutex, &drvdata->aura_lock) - asus_lamparray_release_unlocked(drvdata); + scoped_guard(mutex, &drvdata->aura_lock) { + asus_lamparray_try_init_unlocked(drvdata); + if (drvdata->lamparray_count) + asus_lamparray_release_unlocked(drvdata); + } - if (asus_aura_is_lightbar(drvdata, ldev)) + if (lightbar) ret = asus_aura_write_zone_range(drvdata, AURA_ZONE_BAR_LEFT, AURA_ZONE_BAR_RIGHT, aura_mode, r, g, b, @@ -1985,6 +2486,19 @@ static int asus_aura_commit_effect(struct led_classdev_dynamic *ldev, if (ret < 0) return ret; + /* + * Release above drops LampArray for every zone. If the sibling is + * still in direct mode, push its cached frame again so host control + * is not lost when this zone switches to a firmware effect. + */ + scoped_guard(mutex, &drvdata->aura_lock) { + if (asus_aura_zone_wants_direct(drvdata, lightbar)) { + ret = asus_aura_push_direct_unlocked(drvdata, lightbar); + if (ret < 0) + return ret; + } + } + asus_aura_mark_effect_set(drvdata, ldev); return 0; } @@ -1996,6 +2510,40 @@ static int asus_aura_apply_effect(struct led_classdev_dynamic *ldev) ldev->palette, ldev->num_palette_entries); } +static void asus_aura_restore(struct asus_drvdata *drvdata) +{ + int ret; + + if (!drvdata->kbd_effect_set && !drvdata->lightbar_effect_set && + !drvdata->kbd_direct_valid && !drvdata->lb_direct_valid) + return; + + ret = asus_aura_activate_zones(drvdata); + if (ret < 0) + hid_warn(drvdata->hdev, "Failed to activate Aura zones: %d\n", ret); + + if (drvdata->has_dldev_kbd && drvdata->dldev_kbd.enabled && + ((led_dynamic_effect_is(&drvdata->dldev_kbd, "direct") && + drvdata->kbd_direct_valid) || + (!led_dynamic_effect_is(&drvdata->dldev_kbd, "direct") && + drvdata->kbd_effect_set))) { + ret = asus_aura_apply_effect(&drvdata->dldev_kbd); + if (ret < 0) + hid_warn(drvdata->hdev, + "Failed to restore keyboard effect: %d\n", ret); + } + if (drvdata->has_dldev_lightbar && drvdata->dldev_lightbar.enabled && + ((led_dynamic_effect_is(&drvdata->dldev_lightbar, "direct") && + drvdata->lb_direct_valid) || + (!led_dynamic_effect_is(&drvdata->dldev_lightbar, "direct") && + drvdata->lightbar_effect_set))) { + ret = asus_aura_apply_effect(&drvdata->dldev_lightbar); + if (ret < 0) + hid_warn(drvdata->hdev, + "Failed to restore lightbar effect: %d\n", ret); + } +} + static int asus_aura_set_effect(struct led_classdev_dynamic *ldev, unsigned int index) { @@ -2017,13 +2565,50 @@ static int asus_aura_sync_power(struct asus_drvdata *drvdata, static int asus_aura_set_enabled(struct led_classdev_dynamic *ldev, bool enabled) { struct asus_drvdata *drvdata = ldev->driver_data; + bool lightbar = asus_aura_is_lightbar(drvdata, ldev); int ret; ret = asus_aura_sync_power(drvdata, ldev, enabled, ldev->active_power_states); - if (ret < 0 || !enabled) + if (ret < 0) return ret; + if (!enabled) { + /* + * Core updates ldev->enabled only after this callback returns. + * Blank the zone under LampArray/direct control, then release + * host control when the sibling does not still need it. + */ + guard(mutex)(&drvdata->aura_lock); + + if (drvdata->lamparray_controlled || + (lightbar ? drvdata->lb_direct_valid : + drvdata->kbd_direct_valid)) { + if (lightbar) { + memset(drvdata->lb_direct_buf, 0, + sizeof(drvdata->lb_direct_buf)); + drvdata->lb_direct_valid = true; + } else { + memset(drvdata->kbd_direct_buf, 0, + sizeof(drvdata->kbd_direct_buf)); + drvdata->kbd_direct_valid = true; + } + + ret = asus_aura_push_direct_unlocked(drvdata, !lightbar); + if (ret < 0) + return ret; + + if (lightbar) + drvdata->lb_direct_valid = false; + else + drvdata->kbd_direct_valid = false; + + if (!asus_aura_zone_wants_direct(drvdata, lightbar)) + asus_lamparray_release_unlocked(drvdata); + } + return 0; + } + return asus_aura_apply_effect(ldev); } @@ -2353,15 +2938,19 @@ static bool asus_aura_adopt_boot_effect(struct asus_drvdata *drvdata) any = true; } - if (any) { + if (kbd && drvdata->kbd_effect_set) { asus_lamparray_fill_solid(drvdata->kbd_direct_buf, ROG_STRIX_4ZONE_KBD_LEDS, drvdata->boot_r, drvdata->boot_g, drvdata->boot_b); + drvdata->kbd_direct_valid = true; + } + if (lightbar && drvdata->lightbar_effect_set) { asus_lamparray_fill_solid(drvdata->lb_direct_buf, asus_aura_lb_led_count(drvdata), drvdata->boot_r, drvdata->boot_g, drvdata->boot_b); + drvdata->lb_direct_valid = true; } return any; @@ -2388,14 +2977,17 @@ static int asus_init_dynamic_lighting(struct hid_device *hdev) return 0; } - asus_aura_init_direct_bufs(drvdata); drvdata->has_lightbar = has_lightbar; ret = asus_aura_wake_all_zones(drvdata); if (ret < 0) hid_warn(hdev, "Failed to wake Aura hardware zones: %d\n", ret); - kbd_direct = is_per_key || drvdata->is_strix_4zone; + scoped_guard(mutex, &drvdata->aura_lock) + asus_lamparray_try_init_unlocked(drvdata); + + kbd_direct = is_per_key || drvdata->is_strix_4zone || + drvdata->lamparray_hdev; lightbar_direct = has_lightbar; ret = asus_aura_get_effect_mask(drvdata, effect_mask); if (ret < 0) { @@ -2404,6 +2996,8 @@ static int asus_init_dynamic_lighting(struct hid_device *hdev) ret); } else { mask = effect_mask; + hid_info(hdev, "Aura effect capability mask %02x %02x\n", + effect_mask[0], effect_mask[1]); } kbd_n = asus_aura_fill_effects(drvdata->effects_kbd, mask, kbd_direct); @@ -3113,6 +3707,22 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) if (is_vendor && (drvdata->quirks & QUIRK_ROG_NKEY_KEYBOARD)) hdev->quirks |= HID_QUIRK_HIDINPUT_FORCE; +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) + /* + * 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; + } +#endif ret = asus_worker_create(hdev, drvdata); if (ret) { @@ -3206,10 +3816,29 @@ 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) { + struct hid_device *la = NULL; + + scoped_guard(mutex, &drvdata->aura_lock) { + asus_lamparray_release_unlocked(drvdata); + la = drvdata->lamparray_hdev; + drvdata->lamparray_hdev = NULL; + drvdata->lamparray_count = 0; + } + if (la) + put_device(&la->dev); + } +#endif + if (drvdata->xgm_led) devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev); - asus_worker_stop(drvdata->worker); hid_hw_stop(hdev); } From b778824b104d3c92dcd02418a0c3fd1857c35e00 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 19 Oct 2026 10:15:59 +0200 Subject: [PATCH 5/7] HID: asus: Add Slash lighting support Register Slash when feature report 0x5e is present on Aura keyboards, or on USB 0x193b when discrimination selects Slash. Identify Slash versus AniMe Matrix by feature report 0x5e size first (~640 B is AniMe), then known DMI board names; refuse ambiguous 0x193b interfaces. Expose asus::slash with mode, interval and brightness controls using the Aura feature-report path already used for keyboard lighting. Probe only runs the Slash handshake and does not overwrite a mode saved in the controller. AniMe Matrix remains unbound until a later commit. Signed-off-by: Marco Scardovi --- .../testing/sysfs-class-led-driver-hid-asus | 11 + drivers/hid/hid-asus.c | 415 +++++++++++++++++- drivers/hid/hid-ids.h | 5 + 3 files changed, 424 insertions(+), 7 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus index fbdcccb7fbb38d..719ff51d3d6d68 100644 --- a/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus +++ b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus @@ -15,3 +15,14 @@ Description: ASUS Aura keyboard and chassis lightbar Dynamic Lighting nodes. (Usage Page 0x59) when Aura 0xBC cannot address the lightbar independently. The lightbar node is absent when the device has no chassis lightbar. + +What: /sys/class/leds/asus::slash/ +Date: September 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: ASUS ROG Slash LED class device. Exposes brightness together with + ``slash_mode``, ``slash_mode_index``, and ``slash_interval`` + attributes. On Aura keyboards Slash uses feature report 0x5e. + USB ID 0x193b is shared with AniMe Matrix; the driver claims it + for Slash only when feature report 0x5e size and DMI board + discrimination select Slash rather than AniMe. diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 24a6dbac5130b9..db0d847ec1f2f0 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -66,6 +66,18 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #define AURA_FEATURE_REPORT_SIZE 64 #define ASUS_AURA_MAX_EFFECTS 13 +/* + * USB 0x193b is shared by Slash and AniMe Matrix. AniMe uses ~640-byte + * feature reports on 0x5e; Slash uses shorter Aura-sized reports. + */ +#define ASUS_ANIME_PACKET_SIZE 640 + +enum asus_pid_193b_kind { + ASUS_PID_193B_NONE = 0, + ASUS_PID_193B_SLASH, + ASUS_PID_193B_ANIME, +}; + #define AURA_CMD_PROBE 0x05 #define AURA_CMD_RUN_MODE 0x9e #define AURA_CMD_SET_EFFECT 0xb3 @@ -309,6 +321,11 @@ struct asus_lamparray_lamp { 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; @@ -1214,6 +1231,101 @@ static bool asus_is_lamparray_interface(struct hid_device *hdev) { return asus_lamparray_detect_base(hdev) >= 0; } + +static size_t asus_feature_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 bool asus_dmi_is_anime_board(void) +{ + const char *board = dmi_get_system_info(DMI_BOARD_NAME); + + if (!board) + return false; + + return strstr(board, "GA401I") || strstr(board, "GA401Q") || + strstr(board, "GA402R") || strstr(board, "GA402X") || + strstr(board, "GA402N") || strstr(board, "GU604V") || + strstr(board, "G635L") || strstr(board, "G835L"); +} + +/* + * Boards that ship a standalone Slash MCU on 0x193b and never AniMe. + * Keep this list conservative: unknown boards fall through to -ENODEV + * when report size alone cannot decide. + */ +static bool asus_dmi_is_slash_only_board(void) +{ + const char *board = dmi_get_system_info(DMI_BOARD_NAME); + + if (!board) + return false; + + return strstr(board, "G614") || strstr(board, "G614J") || + strstr(board, "G614JZ") || strstr(board, "G614JI") || + strstr(board, "G614JV") || strstr(board, "G614PR"); +} + + +/* + * USB 0x193b is shared by Slash and AniMe Matrix. Discriminate in order: + * 1) feature report 0x5e size (AniMe ~640 B, Slash Aura-sized) + * 2) known DMI boards when size is ambiguous + * 3) otherwise refuse to guess (-ENODEV / NONE) + */ +static enum asus_pid_193b_kind asus_classify_pid_193b(struct hid_device *hdev) +{ + size_t len_5e; + bool anime_dmi = asus_dmi_is_anime_board(); + bool slash_dmi = asus_dmi_is_slash_only_board(); + + if (hdev->product != USB_DEVICE_ID_ASUSTEK_ROG_SLASH) + return ASUS_PID_193B_NONE; + + len_5e = asus_feature_report_len(hdev, FEATURE_KBD_LED_REPORT_ID2); + + if (len_5e >= ASUS_ANIME_PACKET_SIZE) + return ASUS_PID_193B_ANIME; + + if (len_5e > 0 && len_5e < ASUS_ANIME_PACKET_SIZE) { + if (anime_dmi) + return ASUS_PID_193B_NONE; + return ASUS_PID_193B_SLASH; + } + + /* No usable 0x5e length: Slash may talk on 0x5d only. */ + if (asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID1)) { + if (anime_dmi) + return ASUS_PID_193B_NONE; + return ASUS_PID_193B_SLASH; + } + + if (anime_dmi) + return ASUS_PID_193B_ANIME; + if (slash_dmi) + return ASUS_PID_193B_SLASH; + + return ASUS_PID_193B_NONE; +} + +/* + * Slash on Aura keyboards exposes report 0x5e. Standalone Slash MCU reuses + * USB 0x193b and is accepted only when classify_pid_193b says SLASH. + */ +static bool asus_device_has_slash(struct hid_device *hdev) +{ + if (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_SLASH) + return asus_classify_pid_193b(hdev) == ASUS_PID_193B_SLASH; + + return asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID2); +} + static int asus_kbd_register_leds(struct hid_device *hdev) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); @@ -2092,16 +2204,21 @@ static int asus_lamparray_apply_unlocked(struct asus_drvdata *drvdata, 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 == keyboard) match[nmatch++] = i; } + /* + * No lamps for this zone: let callers fall back to Aura 0xBC. + * Check before take_control so an empty match does not claim the + * LampArray sibling and then report success. + */ if (!nmatch) - return 0; + return -ENODEV; + + ret = asus_lamparray_take_control_unlocked(drvdata); + if (ret < 0) + return ret; src = keyboard ? drvdata->kbd_direct_buf : drvdata->lb_direct_buf; src_leds = keyboard ? ROG_STRIX_4ZONE_KBD_LEDS : @@ -2377,6 +2494,7 @@ static int asus_aura_lightbar_write_packet(struct asus_drvdata *drvdata, if (buffer != drvdata->lb_direct_buf) memcpy(drvdata->lb_direct_buf, buffer, payload_len); + drvdata->lb_direct_valid = true; memcpy(&buf[ROG_STRIX_PERKEY_DIRECT_PAYLOAD_OFFSET], drvdata->lb_direct_buf, payload_len); @@ -2810,6 +2928,257 @@ static int asus_aura_discover(struct asus_drvdata *drvdata, bool *has_lightbar, 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 int asus_init_slash(struct hid_device *hdev) +{ + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + int ret; + + if (!asus_device_has_slash(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; + + 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"); + + return 0; +} + static void asus_aura_dldev_set_default_palette(struct led_classdev_dynamic *ldev, struct dl_rgb *palette) { @@ -3080,6 +3449,12 @@ 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; +} + + static inline void asus_aura_capture_boot_effect(struct hid_device *hdev) { } @@ -3691,6 +4066,21 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) return ret; } + /* + * USB 0x193b is shared by Slash and AniMe Matrix. LampArray siblings + * bind as usual. Claim Slash only; refuse AniMe and ambiguous + * interfaces (AniMe support is a separate change). + */ + if (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_SLASH && + !asus_is_lamparray_interface(hdev)) { + enum asus_pid_193b_kind kind = asus_classify_pid_193b(hdev); + + if (kind != ASUS_PID_193B_SLASH) { + hid_dbg(hdev, + "Skipping 0x193b (not Slash; kind=%d)\n", kind); + return -ENODEV; + } + } /* Check for vendor for RGB init and handle generic devices properly. */ rep_enum = &hdev->report_enum[HID_INPUT_REPORT]; @@ -3760,14 +4150,22 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) /* Laptops keyboard backlight is always at 0x5a */ if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) && (asus_has_report_id(hdev, FEATURE_KBD_REPORT_ID)) && - (asus_kbd_register_leds(hdev))) + (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); + 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); } /* @@ -3975,6 +4373,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 620723822b9ee7..d0b7b15da042ee 100644 --- a/drivers/hid/hid-ids.h +++ b/drivers/hid/hid-ids.h @@ -227,6 +227,11 @@ #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 shared by standalone Slash and AniMe Matrix. hid-asus + * discriminates by feature report 0x5e size, then DMI board name. + */ +#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 50ea6f466892644229a5beb4d3ad8bee3294cf86 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 19 Oct 2026 10:16:09 +0200 Subject: [PATCH 6/7] HID: asus: Add AniMe Matrix Dynamic Lighting support Bind USB 0x193b as AniMe Matrix when feature report 0x5e is ~640 bytes and/or DMI identifies a known AniMe board (GA401, GA402, GU604, G635L, G835L). Unknown AniMe boards use a safe GA402-like three-pane layout. Register asus::anime as a Dynamic Lighting node: brightness 0-3, enabled for display power, firmware stage animations as effects, and direct_buffer as one intensity byte per LED. Frames use the 0x5e/0xc0 pane protocol from asusctl rog-anime with flush, brightness, display, powersave and builtin commits. Signed-off-by: Marco Scardovi --- .../testing/sysfs-class-led-driver-hid-asus | 17 +- drivers/hid/hid-asus.c | 529 ++++++++++++++++-- 2 files changed, 503 insertions(+), 43 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus index 719ff51d3d6d68..a01c1d2b697025 100644 --- a/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus +++ b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus @@ -24,5 +24,18 @@ Description: ASUS ROG Slash LED class device. Exposes brightness together with ``slash_mode``, ``slash_mode_index``, and ``slash_interval`` attributes. On Aura keyboards Slash uses feature report 0x5e. USB ID 0x193b is shared with AniMe Matrix; the driver claims it - for Slash only when feature report 0x5e size and DMI board - discrimination select Slash rather than AniMe. + for Slash only when report size and DMI discrimination select + Slash rather than AniMe. + +What: /sys/class/leds/asus::anime/ +Date: October 2026 +Contact: Marco Scardovi +Contact: Denis Benato +Description: ASUS AniMe Matrix Dynamic Lighting node (USB 0x193b when + discriminated as AniMe). ``brightness`` is the firmware + display level (0-3). ``enabled`` maps to display on/off. + ``effect`` selects ``direct`` streaming or a builtin stage + animation (boot/awake/sleep/shutdown enums). ``direct_buffer`` + accepts one intensity byte per LED; ``led_count`` matches the + board layout (GA401, GA402, GU604, G635L, G835L). Unknown + AniMe boards use a GA402-like three-pane layout. diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index db0d847ec1f2f0..0768250e95d6c8 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -67,10 +67,25 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #define ASUS_AURA_MAX_EFFECTS 13 /* - * USB 0x193b is shared by Slash and AniMe Matrix. AniMe uses ~640-byte - * feature reports on 0x5e; Slash uses shorter Aura-sized reports. + * AniMe Matrix (USB 0x193b, feature report 0x5e). Packet layout and DMI + * board matching follow asusctl rog-anime / reverse-engineering notes. */ #define ASUS_ANIME_PACKET_SIZE 640 +#define ASUS_ANIME_BLOCK_START 7 +#define ASUS_ANIME_BLOCK_END 634 +#define ASUS_ANIME_PANE_LEN (ASUS_ANIME_BLOCK_END - ASUS_ANIME_BLOCK_START) +#define ASUS_ANIME_STRIX_PANE1_LEN 490 +#define ASUS_ANIME_MAX_LEDS (ASUS_ANIME_PANE_LEN * 3) +#define ASUS_ANIME_MAX_EFFECTS 9 + +enum asus_anime_type { + ASUS_ANIME_GA401 = 0, + ASUS_ANIME_GA402, + ASUS_ANIME_GU604, + ASUS_ANIME_G635L, + ASUS_ANIME_G835L, + ASUS_ANIME_UNKNOWN, +}; enum asus_pid_193b_kind { ASUS_PID_193B_NONE = 0, @@ -389,6 +404,16 @@ struct asus_drvdata { bool lamparray_controlled; bool lamparray_unavailable; struct asus_lamparray_lamp *lamparray_lamps; + struct led_classdev_dynamic dldev_anime; + const char *effects_anime[ASUS_ANIME_MAX_EFFECTS]; + bool has_dldev_anime; + enum asus_anime_type anime_type; + u8 anime_boot; + u8 anime_awake; + u8 anime_sleep; + u8 anime_shutdown; + bool anime_builtin; + u8 *anime_buf; #endif }; @@ -1272,6 +1297,27 @@ static bool asus_dmi_is_slash_only_board(void) strstr(board, "G614JV") || strstr(board, "G614PR"); } +static enum asus_anime_type asus_anime_type_from_dmi(void) +{ + const char *board = dmi_get_system_info(DMI_BOARD_NAME); + + if (!board) + return ASUS_ANIME_UNKNOWN; + + if (strstr(board, "GA401I") || strstr(board, "GA401Q")) + return ASUS_ANIME_GA401; + if (strstr(board, "GA402R") || strstr(board, "GA402X") || + strstr(board, "GA402N")) + return ASUS_ANIME_GA402; + if (strstr(board, "GU604V")) + return ASUS_ANIME_GU604; + if (strstr(board, "G635L")) + return ASUS_ANIME_G635L; + if (strstr(board, "G835L")) + return ASUS_ANIME_G835L; + + return ASUS_ANIME_UNKNOWN; +} /* * USB 0x193b is shared by Slash and AniMe Matrix. Discriminate in order: @@ -3442,6 +3488,397 @@ static int asus_init_dynamic_lighting(struct hid_device *hdev) return 0; } +static unsigned int asus_anime_led_count(enum asus_anime_type type) +{ + switch (type) { + case ASUS_ANIME_GA401: + return ASUS_ANIME_PANE_LEN * 2; + case ASUS_ANIME_G635L: + case ASUS_ANIME_G835L: + return 810; + case ASUS_ANIME_GA402: + case ASUS_ANIME_GU604: + case ASUS_ANIME_UNKNOWN: + default: + /* Safe GA402-like 3-pane layout for unknown AniMe boards. */ + return ASUS_ANIME_PANE_LEN * 3; + } +} + +static unsigned int asus_anime_pane_count(enum asus_anime_type type) +{ + switch (type) { + case ASUS_ANIME_GA401: + case ASUS_ANIME_G635L: + case ASUS_ANIME_G835L: + return 2; + default: + return 3; + } +} + +static void asus_anime_fill_prefix(enum asus_anime_type type, unsigned int pane, + u8 *hdr) +{ + static const u8 ga_pfx[3][7] = { + { 0x5e, 0xc0, 0x02, 0x01, 0x00, 0x73, 0x02 }, + { 0x5e, 0xc0, 0x02, 0x74, 0x02, 0x73, 0x02 }, + { 0x5e, 0xc0, 0x02, 0xe7, 0x04, 0x73, 0x02 }, + }; + static const u8 strix_pfx[2][7] = { + { 0x5e, 0xc0, 0x02, 0x01, 0x00, 0xea, 0x01 }, + { 0x5e, 0xc0, 0x02, 0xeb, 0x01, 0x40, 0x01 }, + }; + + if (type == ASUS_ANIME_G635L || type == ASUS_ANIME_G835L) { + memcpy(hdr, strix_pfx[min_t(unsigned int, pane, 1)], 7); + return; + } + + memcpy(hdr, ga_pfx[min_t(unsigned int, pane, 2)], 7); +} + +static int asus_anime_write(struct asus_drvdata *drvdata, u8 *buf, size_t len) +{ + int ret; + + if (!buf || len < 1 || len > ASUS_ANIME_PACKET_SIZE) + return -EINVAL; + + ret = hid_hw_raw_request(drvdata->hdev, buf[0], buf, len, + HID_FEATURE_REPORT, HID_REQ_SET_REPORT); + if (ret < 0) + return ret; + + return 0; +} + +static int asus_anime_clear_pkt(struct asus_drvdata *drvdata) +{ + memset(drvdata->anime_buf, 0, ASUS_ANIME_PACKET_SIZE); + drvdata->anime_buf[0] = FEATURE_KBD_LED_REPORT_ID2; + return 0; +} + +static int asus_anime_init_device(struct asus_drvdata *drvdata) +{ + static const char *asus_tech = "ASUS Tech.Inc."; + int ret; + + ret = asus_anime_clear_pkt(drvdata); + if (ret) + return ret; + + memcpy(drvdata->anime_buf + 1, asus_tech, strlen(asus_tech)); + ret = asus_anime_write(drvdata, drvdata->anime_buf, ASUS_ANIME_PACKET_SIZE); + if (ret < 0) + return ret; + + ret = asus_anime_clear_pkt(drvdata); + if (ret) + return ret; + + drvdata->anime_buf[1] = 0xc2; + return asus_anime_write(drvdata, drvdata->anime_buf, ASUS_ANIME_PACKET_SIZE); +} + +static int asus_anime_flush(struct asus_drvdata *drvdata) +{ + int ret; + + ret = asus_anime_clear_pkt(drvdata); + if (ret) + return ret; + + drvdata->anime_buf[1] = 0xc0; + drvdata->anime_buf[2] = 0x03; + return asus_anime_write(drvdata, drvdata->anime_buf, ASUS_ANIME_PACKET_SIZE); +} + +static int asus_anime_set_brightness(struct asus_drvdata *drvdata, u8 brightness) +{ + int ret; + + ret = asus_anime_clear_pkt(drvdata); + if (ret) + return ret; + + drvdata->anime_buf[1] = 0xc0; + drvdata->anime_buf[2] = 0x04; + drvdata->anime_buf[3] = brightness; + return asus_anime_write(drvdata, drvdata->anime_buf, ASUS_ANIME_PACKET_SIZE); +} + +static int asus_anime_set_display(struct asus_drvdata *drvdata, bool enabled) +{ + int ret; + + ret = asus_anime_clear_pkt(drvdata); + if (ret) + return ret; + + drvdata->anime_buf[1] = 0xc3; + drvdata->anime_buf[2] = 0x01; + drvdata->anime_buf[3] = enabled ? 0x00 : 0x80; + return asus_anime_write(drvdata, drvdata->anime_buf, ASUS_ANIME_PACKET_SIZE); +} + +static int asus_anime_set_powersave_anim(struct asus_drvdata *drvdata, bool enabled) +{ + int ret; + + ret = asus_anime_clear_pkt(drvdata); + if (ret) + return ret; + + drvdata->anime_buf[1] = 0xc4; + drvdata->anime_buf[2] = 0x01; + drvdata->anime_buf[3] = enabled ? 0x00 : 0x80; + return asus_anime_write(drvdata, drvdata->anime_buf, ASUS_ANIME_PACKET_SIZE); +} + +static int asus_anime_commit_builtins(struct asus_drvdata *drvdata) +{ + int ret; + + ret = asus_anime_clear_pkt(drvdata); + if (ret) + return ret; + + drvdata->anime_buf[1] = 0xc5; + drvdata->anime_buf[2] = drvdata->anime_awake | + (drvdata->anime_sleep << 1) | + (drvdata->anime_shutdown << 2) | + (drvdata->anime_boot << 3); + return asus_anime_write(drvdata, drvdata->anime_buf, ASUS_ANIME_PACKET_SIZE); +} + +static int asus_anime_write_frame(struct asus_drvdata *drvdata, + const u8 *data, size_t len) +{ + unsigned int panes = asus_anime_pane_count(drvdata->anime_type); + unsigned int pane, offset = 0; + int ret; + + if (len != asus_anime_led_count(drvdata->anime_type)) + return -EINVAL; + + for (pane = 0; pane < panes; pane++) { + unsigned int chunk; + + ret = asus_anime_clear_pkt(drvdata); + if (ret) + return ret; + + asus_anime_fill_prefix(drvdata->anime_type, pane, + drvdata->anime_buf); + + if (drvdata->anime_type == ASUS_ANIME_G635L || + drvdata->anime_type == ASUS_ANIME_G835L) { + chunk = pane ? (len - ASUS_ANIME_STRIX_PANE1_LEN) : + ASUS_ANIME_STRIX_PANE1_LEN; + } else { + chunk = min_t(unsigned int, ASUS_ANIME_PANE_LEN, len - offset); + } + + if (offset + chunk > len) + return -EINVAL; + + memcpy(drvdata->anime_buf + ASUS_ANIME_BLOCK_START, + data + offset, chunk); + offset += chunk; + + ret = asus_anime_write(drvdata, drvdata->anime_buf, + ASUS_ANIME_PACKET_SIZE); + if (ret < 0) + return ret; + } + + return asus_anime_flush(drvdata); +} + +static const char * const asus_anime_effect_names[] = { + "direct", + "glitch_construction", + "static_emergence", + "binary_banner_scroll", + "rog_logo_glitch", + "banner_swipe", + "starfield", + "glitch_out", + "see_ya", +}; + +static int asus_anime_apply_effect_unlocked(struct asus_drvdata *drvdata, + unsigned int index) +{ + int ret; + + if (index >= ARRAY_SIZE(asus_anime_effect_names)) + return -EINVAL; + + if (index == 0) { + drvdata->anime_builtin = false; + return asus_anime_set_powersave_anim(drvdata, false); + } + + switch (index) { + case 1: + drvdata->anime_boot = 0; + break; + case 2: + drvdata->anime_boot = 1; + break; + case 3: + drvdata->anime_awake = 0; + break; + case 4: + drvdata->anime_awake = 1; + break; + case 5: + drvdata->anime_sleep = 0; + break; + case 6: + drvdata->anime_sleep = 1; + break; + case 7: + drvdata->anime_shutdown = 0; + break; + case 8: + drvdata->anime_shutdown = 1; + break; + default: + return -EINVAL; + } + + drvdata->anime_builtin = true; + ret = asus_anime_commit_builtins(drvdata); + if (ret < 0) + return ret; + + return asus_anime_set_powersave_anim(drvdata, true); +} + +static int asus_anime_set_effect(struct led_classdev_dynamic *ldev, + unsigned int index) +{ + struct asus_drvdata *drvdata = ldev->driver_data; + + guard(mutex)(&drvdata->aura_lock); + return asus_anime_apply_effect_unlocked(drvdata, index); +} + +static int asus_anime_set_enabled(struct led_classdev_dynamic *ldev, bool enabled) +{ + struct asus_drvdata *drvdata = ldev->driver_data; + + guard(mutex)(&drvdata->aura_lock); + return asus_anime_set_display(drvdata, enabled); +} + +static int asus_anime_direct_write(struct led_classdev_dynamic *ldev, + const u8 *buffer, size_t size) +{ + struct asus_drvdata *drvdata = ldev->driver_data; + int ret; + + guard(mutex)(&drvdata->aura_lock); + + ret = asus_anime_write_frame(drvdata, buffer, size); + if (ret < 0) + return ret; + + drvdata->anime_builtin = false; + return asus_anime_set_powersave_anim(drvdata, false); +} + +static int asus_anime_brightness_set(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct led_classdev_dynamic *ldev = lcdev_to_dldev(cdev); + struct asus_drvdata *drvdata = ldev->driver_data; + + guard(mutex)(&drvdata->aura_lock); + return asus_anime_set_brightness(drvdata, brightness); +} + +static const struct led_dynamic_ops asus_anime_ops = { + .set_effect = asus_anime_set_effect, + .set_enabled = asus_anime_set_enabled, + .direct_write = asus_anime_direct_write, +}; + +static int asus_init_anime(struct hid_device *hdev) +{ + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + struct led_classdev_dynamic *ldev = &drvdata->dldev_anime; + unsigned int i, led_count; + int ret; + + if (asus_classify_pid_193b(hdev) != ASUS_PID_193B_ANIME) + return 0; + + drvdata->anime_buf = devm_kzalloc(&hdev->dev, ASUS_ANIME_PACKET_SIZE, + GFP_KERNEL); + if (!drvdata->anime_buf) + return -ENOMEM; + + drvdata->anime_type = asus_anime_type_from_dmi(); + if (drvdata->anime_type == ASUS_ANIME_UNKNOWN) { + drvdata->anime_type = ASUS_ANIME_GA402; + hid_info(hdev, + "AniMe Matrix on unknown board; using GA402-like 3-pane layout\n"); + } + + led_count = asus_anime_led_count(drvdata->anime_type); + + scoped_guard(mutex, &drvdata->aura_lock) { + ret = asus_anime_init_device(drvdata); + if (ret < 0) { + hid_warn(hdev, "AniMe Matrix init failed: %d\n", ret); + return ret; + } + + ret = asus_anime_set_display(drvdata, true); + if (ret < 0) + return ret; + + ret = asus_anime_set_brightness(drvdata, 2); + if (ret < 0) + return ret; + } + + for (i = 0; i < ARRAY_SIZE(asus_anime_effect_names); i++) + drvdata->effects_anime[i] = asus_anime_effect_names[i]; + + ldev->cdev.name = "asus::anime"; + ldev->cdev.max_brightness = 3; + ldev->cdev.brightness = 2; + ldev->cdev.brightness_set_blocking = asus_anime_brightness_set; + ldev->ops = &asus_anime_ops; + ldev->driver_data = drvdata; + ldev->zone_type = "matrix"; + ldev->led_count = led_count; + ldev->direct_sample_size = 1; + ldev->effects = drvdata->effects_anime; + ldev->num_effects = ARRAY_SIZE(asus_anime_effect_names); + ldev->current_effect = 0; + ldev->enabled = true; + + ret = devm_led_classdev_dynamic_register(&hdev->dev, ldev); + if (ret < 0) { + hid_warn(hdev, "Failed to register AniMe Dynamic Lighting: %d\n", + ret); + return ret; + } + + drvdata->has_dldev_anime = true; + hid_info(hdev, + "Registered AniMe Matrix Dynamic Lighting (%u LEDs, type %u)\n", + led_count, drvdata->anime_type); + return 0; +} + #else /* !IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) */ static inline int asus_init_dynamic_lighting(struct hid_device *hdev) @@ -3454,6 +3891,10 @@ static inline int asus_init_slash(struct hid_device *hdev) return 0; } +static inline int asus_init_anime(struct hid_device *hdev) +{ + return 0; +} static inline void asus_aura_capture_boot_effect(struct hid_device *hdev) { @@ -4068,16 +4509,15 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) /* * USB 0x193b is shared by Slash and AniMe Matrix. LampArray siblings - * bind as usual. Claim Slash only; refuse AniMe and ambiguous - * interfaces (AniMe support is a separate change). + * bind as usual; otherwise require a positive Slash/AniMe classification. */ if (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_SLASH && !asus_is_lamparray_interface(hdev)) { enum asus_pid_193b_kind kind = asus_classify_pid_193b(hdev); - if (kind != ASUS_PID_193B_SLASH) { + if (kind == ASUS_PID_193B_NONE) { hid_dbg(hdev, - "Skipping 0x193b (not Slash; kind=%d)\n", kind); + "Skipping ambiguous 0x193b (not Slash/AniMe)\n"); return -ENODEV; } } @@ -4127,45 +4567,52 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) return ret; } - /* - * Read any committed Aura effect before the keyboard handshake - * overwrites the feature report. - */ - if (asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID1)) - asus_aura_capture_boot_effect(hdev); - - if (!drvdata->tp) - asus_initialize_reports(hdev); - - if (asus_has_report_id(hdev, FEATURE_KBD_REPORT_ID) && - ((hdev->product == USB_DEVICE_ID_ASUSTEK_XGM_2022) || - (hdev->product == USB_DEVICE_ID_ASUSTEK_XGM_2023))) { - ret = asus_xgm_init(hdev, drvdata); - if (ret) { - hid_err(hdev, "Failed to initialize xg mobile: %d\n", ret); - goto err_stop_hw; + if (asus_classify_pid_193b(hdev) == ASUS_PID_193B_ANIME) { + ret = asus_init_anime(hdev); + if (ret < 0) + hid_warn(hdev, "Failed to initialize AniMe Matrix: %d\n", + ret); + } else { + /* + * Read any committed Aura effect before the keyboard handshake + * overwrites the feature report. Skip on AniMe Matrix. + */ + if (asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID1)) + asus_aura_capture_boot_effect(hdev); + + if (!drvdata->tp) + asus_initialize_reports(hdev); + + if (asus_has_report_id(hdev, FEATURE_KBD_REPORT_ID) && + ((hdev->product == USB_DEVICE_ID_ASUSTEK_XGM_2022) || + (hdev->product == USB_DEVICE_ID_ASUSTEK_XGM_2023))) { + ret = asus_xgm_init(hdev, drvdata); + if (ret) { + hid_err(hdev, "Failed to initialize xg mobile: %d\n", ret); + goto err_stop_hw; + } } - } - /* Laptops keyboard backlight is always at 0x5a */ - if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) && - (asus_has_report_id(hdev, FEATURE_KBD_REPORT_ID)) && - (asus_kbd_register_leds(hdev))) - hid_warn(hdev, "Failed to initialize backlight.\n"); + /* Laptops keyboard backlight is always at 0x5a */ + if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) && + (asus_has_report_id(hdev, FEATURE_KBD_REPORT_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); + 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); + ret = asus_init_slash(hdev); + if (ret < 0) + hid_warn(hdev, + "Failed to initialize Slash lighting: %d\n", + ret); + } } /* From 9f391faea6ba37834d3cc3cee42f21161068449e Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 26 Oct 2026 21:17:37 +0100 Subject: [PATCH 7/7] HID: asus: Fix Ally / Xbox Ally X Dynamic Lighting Address the Ally X and Xbox Ally X gaps from the PR #17 hardware report: zone 0 for both rings, Ally reactive 0x0e, brightness 0-3, d1 09 power, Xbox Guide LED, slash gating, and LampArray handoff reliability. Signed-off-by: Marco Scardovi --- drivers/hid/hid-asus.c | 438 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 396 insertions(+), 42 deletions(-) diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c index 0768250e95d6c8..30908db955fe2f 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -64,7 +65,8 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad"); #define ROG_ALLY_MIN_MCU 319 #define AURA_FEATURE_REPORT_SIZE 64 -#define ASUS_AURA_MAX_EFFECTS 13 +#define ASUS_AURA_MAX_EFFECTS 14 +#define ROG_ALLY_MAX_KBD_LEDS 8 /* * AniMe Matrix (USB 0x193b, feature report 0x5e). Packet layout and DMI @@ -168,6 +170,18 @@ enum asus_pid_193b_kind { #define AURA_EFFECT_PULSE 0x0a #define AURA_EFFECT_COMET 0x0b #define AURA_EFFECT_FLASH 0x0c +/* Ally / Xbox Ally MCU reactive mode (not notebook highlight 0x06). */ +#define AURA_EFFECT_ALLY_REACTIVE 0x0e + +/* Ally LED power (feature 5a d1 09) and Xbox Guide LED (OUT 5d d1 20/21). */ +#define ALLY_CMD_SET_LED_POWER 0x09 +#define ALLY_CMD_CHECK_XBOX_LED 0x1b +#define ALLY_CMD_SET_XBOX_LED_BRIGHTNESS 0x20 +#define ALLY_CMD_SET_XBOX_LED_EFFECT 0x21 +#define ALLY_LED_POWER_BOOT BIT(0) +#define ALLY_LED_POWER_AWAKE BIT(1) +#define ALLY_LED_POWER_SLEEP BIT(2) +#define ALLY_LED_POWER_SHUTDOWN BIT(3) #define AURA_SPEED_SLOW 0xe1 #define AURA_SPEED_MED 0xeb @@ -332,6 +346,8 @@ struct asus_lamparray_lamp { s32 x; bool keyboard; }; + +struct asus_xbox_led; #endif struct asus_drvdata { @@ -392,10 +408,11 @@ struct asus_drvdata { u8 boot_direction; bool is_strix_4zone; bool has_lightbar; - u8 kbd_direct_buf[ROG_STRIX_4ZONE_KBD_BUF_SIZE]; + u8 kbd_direct_buf[ROG_ALLY_MAX_KBD_LEDS * 3]; u8 lb_direct_buf[ROG_STRIX_LIGHTBAR_BUF_SIZE]; bool kbd_direct_valid; bool lb_direct_valid; + struct asus_xbox_led *xbox_led; struct hid_device *lamparray_hdev; u8 *lamparray_buf; size_t lamparray_buf_len; @@ -1366,12 +1383,26 @@ static enum asus_pid_193b_kind asus_classify_pid_193b(struct hid_device *hdev) */ static bool asus_device_has_slash(struct hid_device *hdev) { + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + + /* + * Ally / Xbox Ally expose report 0x5e on the vendor interface but have + * no Slash bar. Do not register asus::slash on those devices. + */ + if (drvdata && (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)) + return false; + if (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_SLASH) return asus_classify_pid_193b(hdev) == ASUS_PID_193B_SLASH; return asus_has_report_id(hdev, FEATURE_KBD_LED_REPORT_ID2); } +static bool asus_is_ally(struct asus_drvdata *drvdata) +{ + return !!(drvdata->quirks & QUIRK_ROG_ALLY_XPAD); +} + static int asus_kbd_register_leds(struct hid_device *hdev) { struct asus_drvdata *drvdata = hid_get_drvdata(hdev); @@ -1645,6 +1676,32 @@ static const struct { { "flash", AURA_EFFECT_FLASH, BIT(12) }, }; +/* + * Ally MCUs reject the 0x9e capability query. Advertising the notebook + * fallback list freezes the rings on unsupported modes; ship the modes the + * handheld firmware actually runs, plus Ally reactive (0x0e). + */ +static unsigned int asus_aura_fill_ally_effects(const char **dst, + bool direct_capable) +{ + static const char * const ally_effects[] = { + "static", + "breathe", + "rainbow_cycle", + "rainbow_wave", + "pulse", + "reactive", + }; + unsigned int i, n = 0; + + for (i = 0; i < ARRAY_SIZE(ally_effects); i++) + dst[n++] = ally_effects[i]; + if (direct_capable) + dst[n++] = "direct"; + + return n; +} + static unsigned int asus_aura_fill_effects(const char **dst, const u8 *effect_mask, bool direct_capable) @@ -1670,6 +1727,11 @@ static int asus_aura_hw_mode_from_name(const char *name, u8 *aura_mode) if (!name) return -EINVAL; + if (!strcmp(name, "reactive")) { + *aura_mode = AURA_EFFECT_ALLY_REACTIVE; + return 0; + } + for (i = 0; i < ARRAY_SIZE(asus_aura_effect_map); i++) { if (!strcmp(name, asus_aura_effect_map[i].name)) { *aura_mode = asus_aura_effect_map[i].hw_mode; @@ -1684,6 +1746,9 @@ static bool asus_aura_hw_mode_known(u8 mode) { unsigned int i; + if (mode == AURA_EFFECT_ALLY_REACTIVE) + return true; + for (i = 0; i < ARRAY_SIZE(asus_aura_effect_map); i++) { if (asus_aura_effect_map[i].hw_mode == mode) return true; @@ -2242,12 +2307,13 @@ static int asus_lamparray_apply_unlocked(struct asus_drvdata *drvdata, bool keyboard) { unsigned int match[ASUS_LAMPARRAY_MAX_LAMPS]; - unsigned int nmatch = 0, idx = 0, i; + unsigned int nmatch = 0, idx = 0, i, pass; unsigned int src_leds; const u8 *src; u8 *buf = drvdata->lamparray_buf; u8 rid_multi = asus_lamparray_rid(drvdata, ASUS_LAMPARRAY_RID_MULTI); size_t multi_len; + bool took_control; int ret; for (i = 0; i < drvdata->lamparray_count; i++) { @@ -2262,46 +2328,61 @@ static int asus_lamparray_apply_unlocked(struct asus_drvdata *drvdata, if (!nmatch) return -ENODEV; + took_control = !drvdata->lamparray_controlled; ret = asus_lamparray_take_control_unlocked(drvdata); if (ret < 0) return ret; src = keyboard ? drvdata->kbd_direct_buf : drvdata->lb_direct_buf; - src_leds = keyboard ? ROG_STRIX_4ZONE_KBD_LEDS : - asus_aura_lb_led_count(drvdata); + if (keyboard) { + src_leds = drvdata->dldev_kbd.led_count; + if (!src_leds || src_leds > ROG_ALLY_MAX_KBD_LEDS) + src_leds = ROG_STRIX_4ZONE_KBD_LEDS; + } else { + src_leds = asus_aura_lb_led_count(drvdata); + } multi_len = asus_lamparray_report_len(drvdata->lamparray_hdev, rid_multi); if (multi_len < 51) return -EPROTO; - for (i = 0; i < nmatch; i += ASUS_LAMPARRAY_MULTI_MAX) { - unsigned int n = min_t(unsigned int, ASUS_LAMPARRAY_MULTI_MAX, - nmatch - 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 >= nmatch) ? ASUS_LAMPARRAY_FLAG_COMPLETE : 0; - - for (j = 0; j < n; j++) { - unsigned int lamp = match[i + j]; - u8 r, g, b; - u16 id = drvdata->lamparray_lamps[lamp].id; - - asus_lamparray_sample_rgb(src, src_leds, idx++, nmatch, - &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; - } + /* + * Ally MCUs often drop the first MultiUpdate sent immediately after + * take_control. Push the frame twice when we just claimed the device. + */ + for (pass = 0; pass < (took_control ? 2 : 1); pass++) { + idx = 0; + for (i = 0; i < nmatch; i += ASUS_LAMPARRAY_MULTI_MAX) { + unsigned int n = min_t(unsigned int, + ASUS_LAMPARRAY_MULTI_MAX, + nmatch - 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 >= nmatch) ? + ASUS_LAMPARRAY_FLAG_COMPLETE : 0; + + for (j = 0; j < n; j++) { + unsigned int lamp = match[i + j]; + u8 r, g, b; + u16 id = drvdata->lamparray_lamps[lamp].id; + + asus_lamparray_sample_rgb(src, src_leds, idx++, + nmatch, &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; + ret = asus_lamparray_raw(drvdata, buf, multi_len, false); + if (ret < 0) + return ret; + } } return 0; @@ -2397,8 +2478,12 @@ static int asus_aura_strix_write_direct(struct asus_drvdata *drvdata, guard(mutex)(&drvdata->aura_lock); if (drvdata->is_strix_4zone) { - unsigned int leds = min_t(size_t, size / 3, - ROG_STRIX_4ZONE_KBD_LEDS); + unsigned int max_leds = drvdata->dldev_kbd.led_count; + unsigned int leds; + + if (!max_leds || max_leds > ROG_ALLY_MAX_KBD_LEDS) + max_leds = ROG_STRIX_4ZONE_KBD_LEDS; + leds = min_t(size_t, size / 3, max_leds); if (buffer != drvdata->kbd_direct_buf) memcpy(drvdata->kbd_direct_buf, buffer, leds * 3); @@ -2634,6 +2719,18 @@ static int asus_aura_commit_effect(struct led_classdev_dynamic *ldev, asus_aura_speed_to_hw(speed), asus_aura_direction_to_hw(direction), r2, g2, b2); + else if (asus_is_ally(drvdata)) + /* + * Ally X and Xbox Ally X share PID 0x1b4c. Discovery still + * reports 4-zone (byte 9 = 0x02), but zones 1-4 are only the + * left ring on Xbox Ally X. Zone 0 addresses both rings on + * both MCUs (Armoury Crate and the out-of-tree Ally driver). + */ + ret = asus_aura_write_zone_effect(drvdata, AURA_ZONE_ALL, + aura_mode, r, g, b, + asus_aura_speed_to_hw(speed), + asus_aura_direction_to_hw(direction), + r2, g2, b2, true); else if (drvdata->is_strix_4zone) ret = asus_aura_write_zone_range(drvdata, AURA_ZONE_KEY1, AURA_ZONE_KEY4, aura_mode, @@ -2813,6 +2910,192 @@ static int asus_aura_set_palette(struct led_classdev_dynamic *ldev, palette, num_entries); } +static bool asus_dmi_is_xbox_ally(void) +{ + const char *board = dmi_get_system_info(DMI_BOARD_NAME); + + return board && strstr(board, "RC74XA"); +} + +static int asus_ally_write_led_power(struct asus_drvdata *drvdata, u8 flags) +{ + u8 buf[FEATURE_KBD_REPORT_SIZE] = { + FEATURE_KBD_REPORT_ID, 0xd1, ALLY_CMD_SET_LED_POWER, 0x01, flags + }; + + return asus_kbd_set_report(drvdata->hdev, buf, sizeof(buf)); +} + +static u8 asus_ally_power_flags(u32 states) +{ + u8 flags = 0; + + if (states & DL_POWER_STATE_BOOT) + flags |= ALLY_LED_POWER_BOOT; + if (states & DL_POWER_STATE_AWAKE) + flags |= ALLY_LED_POWER_AWAKE; + if (states & DL_POWER_STATE_SLEEP) + flags |= ALLY_LED_POWER_SLEEP; + if (states & DL_POWER_STATE_SHUTDOWN) + flags |= ALLY_LED_POWER_SHUTDOWN; + + return flags; +} + +/* + * ROG Xbox Ally X (RC74XA) Guide button LED. Separate from the stick rings: + * brightness is interrupt OUT 5d d1 20, effect is feature 5a d1 21. Ring + * brightness 0 also turns this LED off; callers re-apply after leaving 0. + */ +struct asus_xbox_led { + struct hid_device *hdev; + struct led_classdev cdev; + struct mutex lock; + u8 brightness; + u8 effect; + u8 speed; +}; + +static int asus_xbox_led_set_effect(struct asus_xbox_led *led) +{ + u8 buf[FEATURE_KBD_REPORT_SIZE] = { + FEATURE_KBD_REPORT_ID, 0xd1, ALLY_CMD_SET_XBOX_LED_EFFECT, 0x02, + led->effect, led->speed, + }; + int ret; + + guard(mutex)(&led->lock); + + ret = asus_kbd_set_report(led->hdev, buf, sizeof(buf)); + if (ret < 0) + hid_warn(led->hdev, "Xbox LED effect failed: %d\n", ret); + + return ret; +} + +static int asus_xbox_led_set_brightness(struct asus_xbox_led *led) +{ + u8 buf[FEATURE_KBD_REPORT_SIZE] = { + FEATURE_KBD_LED_REPORT_ID1, 0xd1, ALLY_CMD_SET_XBOX_LED_BRIGHTNESS, + 0x01, led->brightness, + }; + int ret; + + guard(mutex)(&led->lock); + + ret = asus_kbd_set_report(led->hdev, buf, sizeof(buf)); + if (ret < 0) + hid_warn(led->hdev, "Xbox LED brightness failed: %d\n", ret); + + return ret; +} + +static int asus_xbox_led_apply(struct asus_xbox_led *led) +{ + int ret; + + if (!led) + return 0; + + ret = asus_xbox_led_set_effect(led); + if (ret < 0) + return ret; + + return asus_xbox_led_set_brightness(led); +} + +static int asus_xbox_led_set_blocking(struct led_classdev *cdev, + enum led_brightness brightness) +{ + struct asus_xbox_led *led = container_of(cdev, struct asus_xbox_led, cdev); + + if (brightness > 3) + brightness = 3; + + led->brightness = brightness; + cdev->brightness = brightness; + return asus_xbox_led_set_brightness(led); +} + +static bool asus_check_xbox_led_support(struct hid_device *hdev) +{ + u8 *set_buf __free(kfree) = kzalloc(ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + u8 *get_buf __free(kfree) = kzalloc(ROG_ALLY_REPORT_SIZE, GFP_KERNEL); + int ret; + + if (!set_buf || !get_buf) + return false; + + set_buf[0] = FEATURE_KBD_REPORT_ID; + set_buf[1] = 0xd1; + set_buf[2] = ALLY_CMD_CHECK_XBOX_LED; + set_buf[3] = 0x03; + + ret = hid_hw_raw_request(hdev, FEATURE_KBD_REPORT_ID, set_buf, + ROG_ALLY_REPORT_SIZE, HID_FEATURE_REPORT, + HID_REQ_SET_REPORT); + if (ret < 0) + return false; + + usleep_range(5000, 10000); + + get_buf[0] = FEATURE_KBD_REPORT_ID; + ret = hid_hw_raw_request(hdev, FEATURE_KBD_REPORT_ID, get_buf, + ROG_ALLY_REPORT_SIZE, HID_FEATURE_REPORT, + HID_REQ_GET_REPORT); + if (ret < 0) + return false; + + return get_buf[1] == 0xd1 && + get_buf[2] == ALLY_CMD_CHECK_XBOX_LED && + (get_buf[5] & 0x08); +} + +static int asus_init_xbox_led(struct hid_device *hdev) +{ + struct asus_drvdata *drvdata = hid_get_drvdata(hdev); + struct asus_xbox_led *led; + int ret; + + if (!asus_is_ally(drvdata)) + return 0; + + if (!asus_check_xbox_led_support(hdev) && !asus_dmi_is_xbox_ally()) + return 0; + + led = devm_kzalloc(&hdev->dev, sizeof(*led), GFP_KERNEL); + if (!led) + return -ENOMEM; + + led->hdev = hdev; + mutex_init(&led->lock); + led->brightness = 3; + led->effect = AURA_EFFECT_STATIC; + led->speed = 0x08; + + led->cdev.name = "asus::xbox_logo"; + led->cdev.max_brightness = 3; + led->cdev.brightness = led->brightness; + led->cdev.brightness_set_blocking = asus_xbox_led_set_blocking; + + ret = devm_led_classdev_register(&hdev->dev, &led->cdev); + if (ret) { + hid_err(hdev, "Failed to register Xbox logo LED: %d\n", ret); + return ret; + } + + /* + * RC74XA defaults leave the Guide LED gated off after the power-on + * flash. 0xff matches Armoury Crate / ally_module (zone bit mapping + * of the logo is not isolated). + */ + asus_ally_write_led_power(drvdata, 0xff); + asus_xbox_led_apply(led); + drvdata->xbox_led = led; + hid_info(hdev, "Registered Xbox Guide LED: asus::xbox_logo\n"); + return 0; +} + static int asus_aura_brightness_set_blocking(struct led_classdev *cdev, enum led_brightness brightness) { @@ -2825,9 +3108,21 @@ static int asus_aura_brightness_set_blocking(struct led_classdev *cdev, * Firmware backlight only. Zone power belongs to enabled. brightness * 0 must not gate the zone: led_trigger_remove() calls this with * LED_OFF, and the persisted 0xb3 color stays in place. + * + * Ally MCU levels are 0-3 (same as asus::kbd_backlight). Writing the + * sysfs 0-255 value straight into ba c5 c4 leaves the rings dark. */ - if (ldev == &drvdata->dldev_kbd) + if (ldev == &drvdata->dldev_kbd) { + if (asus_is_ally(drvdata) && brightness > 3) + brightness = 3; asus_kbd_set_brightness(drvdata->hdev, brightness); + /* + * On Xbox Ally X, ring brightness 0 also extinguishes the + * Guide LED. Re-apply its level when the rings leave 0. + */ + if (drvdata->xbox_led && brightness > 0) + asus_xbox_led_apply(drvdata->xbox_led); + } return 0; } @@ -2892,6 +3187,25 @@ static int asus_aura_sync_power(struct asus_drvdata *drvdata, drvdata->lb_power_states = states; } + /* + * Ally ignores Aura 0xbd power. Feature 5a d1 09 gates the rings + * (and the Xbox Guide LED on RC74XA). + */ + if (asus_is_ally(drvdata)) { + u8 flags = 0; + + if (drvdata->kbd_power_on) { + flags = asus_ally_power_flags(drvdata->kbd_power_states); + /* + * Xbox Guide LED zone bits are not in the low nibble; + * 0xff is what lights it (Armoury Crate / ally_module). + */ + if (drvdata->xbox_led) + flags = 0xff; + } + return asus_ally_write_led_power(drvdata, flags); + } + if (drvdata->kbd_power_on) asus_aura_power_bits(drvdata->kbd_power_states, &kbd, &unused); if (drvdata->lb_power_on) @@ -3354,8 +3668,11 @@ static bool asus_aura_adopt_boot_effect(struct asus_drvdata *drvdata) } if (kbd && drvdata->kbd_effect_set) { - asus_lamparray_fill_solid(drvdata->kbd_direct_buf, - ROG_STRIX_4ZONE_KBD_LEDS, + unsigned int n = drvdata->dldev_kbd.led_count; + + if (!n || n > ROG_ALLY_MAX_KBD_LEDS) + n = ROG_STRIX_4ZONE_KBD_LEDS; + asus_lamparray_fill_solid(drvdata->kbd_direct_buf, n, drvdata->boot_r, drvdata->boot_g, drvdata->boot_b); drvdata->kbd_direct_valid = true; @@ -3415,9 +3732,17 @@ static int asus_init_dynamic_lighting(struct hid_device *hdev) effect_mask[0], effect_mask[1]); } - kbd_n = asus_aura_fill_effects(drvdata->effects_kbd, mask, kbd_direct); - lightbar_n = asus_aura_fill_effects(drvdata->effects_lightbar, mask, - lightbar_direct); + if (asus_is_ally(drvdata) && !mask) { + kbd_n = asus_aura_fill_ally_effects(drvdata->effects_kbd, + kbd_direct); + lightbar_n = asus_aura_fill_ally_effects(drvdata->effects_lightbar, + lightbar_direct); + } else { + kbd_n = asus_aura_fill_effects(drvdata->effects_kbd, mask, + kbd_direct); + lightbar_n = asus_aura_fill_effects(drvdata->effects_lightbar, + mask, lightbar_direct); + } /* Keyboard Dynamic Lighting zone: always registered on Aura models. */ asus_aura_dldev_common_init(&drvdata->dldev_kbd, drvdata, "asus::keyboard", @@ -3427,11 +3752,26 @@ static int asus_init_dynamic_lighting(struct hid_device *hdev) drvdata->kbd_palette); drvdata->kbd_power_on = true; drvdata->kbd_power_states = DL_POWER_STATE_ALL; + if (asus_is_ally(drvdata)) { + drvdata->dldev_kbd.cdev.max_brightness = 3; + drvdata->dldev_kbd.cdev.brightness = 3; + } 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; + if (asus_is_ally(drvdata) && drvdata->lamparray_count) { + unsigned int n = 0, i; + + for (i = 0; i < drvdata->lamparray_count; i++) { + if (drvdata->lamparray_lamps[i].keyboard) + n++; + } + drvdata->dldev_kbd.led_count = + clamp_t(unsigned int, n, 1, ROG_ALLY_MAX_KBD_LEDS); + } else { + 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) @@ -3479,6 +3819,16 @@ static int asus_init_dynamic_lighting(struct hid_device *hdev) hid_info(hdev, "Registered dynamic lighting zones: kbd=%d, lightbar=%d\n", drvdata->has_dldev_kbd, drvdata->has_dldev_lightbar); + if (asus_is_ally(drvdata)) { + asus_kbd_set_brightness(hdev, 3); + asus_aura_sync_power(drvdata, &drvdata->dldev_kbd, true, + DL_POWER_STATE_ALL); + ret = asus_init_xbox_led(hdev); + if (ret < 0) + hid_warn(hdev, "Failed to initialize Xbox Guide LED: %d\n", + ret); + } + if (drvdata->boot_effect_valid && (drvdata->kbd_effect_set || drvdata->lightbar_effect_set)) hid_info(hdev, "Preserving programmed Aura effect\n"); @@ -4376,6 +4726,8 @@ static int __maybe_unused asus_resume(struct hid_device *hdev) #if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) if (drvdata->has_dldev_kbd || drvdata->has_dldev_lightbar) asus_aura_restore(drvdata); + if (drvdata->xbox_led) + asus_xbox_led_apply(drvdata->xbox_led); #endif /* @@ -4399,6 +4751,8 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev) #if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) if (drvdata->has_dldev_kbd || drvdata->has_dldev_lightbar) asus_aura_restore(drvdata); + if (drvdata->xbox_led) + asus_xbox_led_apply(drvdata->xbox_led); #endif