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..a01c1d2b697025 --- /dev/null +++ b/Documentation/ABI/testing/sysfs-class-led-driver-hid-asus @@ -0,0 +1,41 @@ +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. 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. + +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 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/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 1e0d90fe2220a0..1a2c9a209a40f0 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -11856,6 +11856,15 @@ 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 M: Benjamin Tissoires @@ -15129,6 +15138,17 @@ 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: 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 + LED SUBSYSTEM M: Lee Jones M: Pavel Machek 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..30908db955fe2f 100644 --- a/drivers/hid/hid-asus.c +++ b/drivers/hid/hid-asus.c @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -30,6 +31,10 @@ #include /* For to_usb_interface for T100 touchpad intf check */ #include #include +#include +#include +#include +#include #include "hid-ids.h" @@ -37,6 +42,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 +64,178 @@ 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 14 +#define ROG_ALLY_MAX_KBD_LEDS 8 + +/* + * 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, + 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 +#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 +/* 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 +#define AURA_SPEED_FAST 0xf5 + +#define ROG_STRIX_LEDS_PER_PKT 16 +#define ROG_STRIX_PERKEY_FULL_PKTS 10 +#define ROG_STRIX_PERKEY_FINAL_PKT_LEDS 8 +#define ROG_STRIX_PERKEY_PACKETS (ROG_STRIX_PERKEY_FULL_PKTS + 1) +#define ROG_STRIX_DIRECT_LEDS \ + ((ROG_STRIX_PERKEY_FULL_PKTS * ROG_STRIX_LEDS_PER_PKT) + \ + ROG_STRIX_PERKEY_FINAL_PKT_LEDS) +#define ROG_STRIX_DIRECT_BUF_SIZE (ROG_STRIX_DIRECT_LEDS * 3) +#define ROG_STRIX_LIGHTBAR_LEDS 12 +#define ROG_STRIX_LIGHTBAR_BUF_SIZE (ROG_STRIX_LIGHTBAR_LEDS * 3) +#define ROG_STRIX_4ZONE_KBD_LEDS 4 +#define ROG_STRIX_4ZONE_KBD_BUF_SIZE (ROG_STRIX_4ZONE_KBD_LEDS * 3) +#define ROG_STRIX_4ZONE_LIGHTBAR_LEDS 6 +#define ROG_STRIX_4ZONE_LIGHTBAR_BUF_SIZE \ + (ROG_STRIX_4ZONE_LIGHTBAR_LEDS * 3) +#define ROG_STRIX_4ZONE_DIRECT_KBD_OFFSET 9 +#define ROG_STRIX_4ZONE_DIRECT_LB_OFFSET 27 +#define ROG_STRIX_PERKEY_DIRECT_PAYLOAD_OFFSET 9 + +#define AURA_DIRECT_FRAME_PERKEY 0x00 +#define AURA_DIRECT_FRAME_ZONED 0x01 +#define AURA_DIRECT_ROUTING_DEFAULT 0x01 +#define AURA_DIRECT_CHUNK_FLAG 0x01 + +/* + * Microsoft HID Lighting Illumination / LampArray (Usage Page 0x59). + * + * Linux Dynamic Lighting (led-class-dynamic / 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 @@ -161,8 +340,23 @@ struct asus_touchpad_info { int report_size; }; +#if IS_REACHABLE(CONFIG_LEDS_CLASS_DYNAMIC) +struct asus_lamparray_lamp { + u16 id; + s32 x; + bool keyboard; +}; + +struct asus_xbox_led; +#endif + struct asus_drvdata { unsigned long quirks; + struct led_classdev slash_led; + bool has_slash_led; + u8 slash_mode; + u8 slash_brightness; + u8 slash_interval; struct hid_device *hdev; struct input_dev *input; struct input_dev *tp_kbd_input; @@ -178,6 +372,66 @@ 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_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; + u8 lamparray_rid_base; + unsigned int lamparray_count; + 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 }; static int asus_report_battery(struct asus_drvdata *, u8 *, int); @@ -585,15 +839,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) @@ -971,6 +1247,162 @@ 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 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"); +} + +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: + * 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) +{ + 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); @@ -1011,182 +1443,2991 @@ 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, -}; - -#define QUERY_MIN_INTERVAL (60 * HZ) /* 60[sec] */ - -static int asus_battery_get_property(struct power_supply *psy, - enum power_supply_property psp, - union power_supply_propval *val) +static int asus_aura_set_feature(struct asus_drvdata *drvdata, + const u8 *buf, size_t buf_size) { - 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; - } + guard(mutex)(&drvdata->aura_lock); - return ret; + return asus_aura_set_feature_unlocked(drvdata, buf, buf_size); } -static int asus_battery_probe(struct hid_device *hdev) +static int asus_aura_get_feature(struct asus_drvdata *drvdata, + u8 *buf, size_t buf_size) { - struct asus_drvdata *drvdata = hid_get_drvdata(hdev); - struct power_supply_config pscfg = { .drv_data = drvdata }; - int ret = 0; + int ret; - drvdata->battery_capacity = 0; - drvdata->battery_stat = POWER_SUPPLY_STATUS_UNKNOWN; - drvdata->battery_in_query = false; + if (buf_size > AURA_FEATURE_REPORT_SIZE) + return -EINVAL; - 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; + guard(mutex)(&drvdata->aura_lock); - drvdata->battery_next_query = jiffies; + memset(drvdata->aura_buf, 0, AURA_FEATURE_REPORT_SIZE); + drvdata->aura_buf[0] = buf[0]; - drvdata->battery = devm_power_supply_register(&hdev->dev, - &(drvdata->battery_desc), &pscfg); - if (IS_ERR(drvdata->battery)) { - ret = PTR_ERR(drvdata->battery); - drvdata->battery = NULL; - hid_err(hdev, "Unable to register battery device\n"); + ret = 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; - } - - power_supply_powers(drvdata->battery, &hdev->dev); + memcpy(buf, drvdata->aura_buf, min_t(size_t, buf_size, ret)); return ret; } -static int asus_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value) +static int asus_aura_commit(struct asus_drvdata *drvdata) { - const u8 buf[ROG_XGM_REPORT_SIZE] = { - FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00 + 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, }; - 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); + /* + * 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; - } else if (ret != ROG_XGM_REPORT_SIZE) { - hid_err(xgm->hdev, "Unexpected partial transfer to XG mobile: %d\n", ret); - return -EIO; - } - return 0; + ret = asus_aura_set_feature(drvdata, buf_apply, sizeof(buf_apply)); + if (ret < 0) + return ret; + + return asus_aura_set_feature(drvdata, buf_set, sizeof(buf_set)); +} + +static int asus_aura_query_run_mode(struct asus_drvdata *drvdata, u8 selector, + u8 *buf, size_t buf_size) +{ + 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; + + 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_aura_get_effect_mask(struct asus_drvdata *drvdata, u8 effect_mask[2]) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE]; + int 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; + if (ret < 22) + return -ENODATA; + +found: + effect_mask[0] = buf[20]; + effect_mask[1] = buf[21]; + + 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, 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) }, +}; + +/* + * 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) +{ + unsigned int i, n = 0; + u16 mask = effect_mask ? (effect_mask[0] | ((u16)effect_mask[1] << 8)) + : (u16)~0; + + for (i = 0; i < ARRAY_SIZE(asus_aura_effect_map); i++) { + if (mask & asus_aura_effect_map[i].cap_bit) + dst[n++] = asus_aura_effect_map[i].name; + } + if (direct_capable) + dst[n++] = "direct"; + + return n; +} + +static int asus_aura_hw_mode_from_name(const char *name, u8 *aura_mode) +{ + unsigned int i; + + 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; + return 0; + } + } + + return -EINVAL; +} + +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; + } + + return false; +} + +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 int asus_aura_write_zone_effect(struct asus_drvdata *drvdata, u8 zone, + u8 aura_mode, u8 r, u8 g, u8 b, + u8 speed, u8 direction, + u8 r2, u8 g2, u8 b2, + bool commit) +{ + u8 buf[AURA_FEATURE_REPORT_SIZE] = { 0 }; + int ret; + + if (zone == AURA_ZONE_BAR_LEFT || zone == AURA_ZONE_BAR_RIGHT) { + ret = asus_aura_activate_zone(drvdata, AURA_ZONE_ACTIVATE_LIGHTBAR); + if (ret < 0) + return ret; + } + + buf[0] = FEATURE_KBD_LED_REPORT_ID1; + buf[1] = AURA_CMD_SET_EFFECT; + buf[2] = zone; + buf[3] = aura_mode; + buf[4] = r; + buf[5] = g; + buf[6] = b; + buf[7] = speed; + buf[8] = direction; + buf[9] = 0x00; + buf[10] = r2; + buf[11] = g2; + buf[12] = b2; + + ret = asus_aura_set_feature(drvdata, buf, sizeof(buf)); + if (ret < 0) + return ret; + + if (commit) + return asus_aura_commit(drvdata); + + return 0; +} + +static size_t asus_lamparray_report_len(struct hid_device *hdev, u8 id) +{ + struct hid_report *report; + + report = hdev->report_enum[HID_FEATURE_REPORT].report_id_hash[id]; + if (!report) + return 0; + return hid_report_len(report); +} + +static u8 asus_lamparray_rid(struct asus_drvdata *drvdata, u8 offset) +{ + return drvdata->lamparray_rid_base + offset; +} + +static int asus_lamparray_raw(struct asus_drvdata *drvdata, u8 *buf, size_t len, + bool get) +{ + int ret; + + if (!drvdata->lamparray_hdev || !len) + return -ENODEV; + + ret = hid_hw_raw_request(drvdata->lamparray_hdev, buf[0], buf, len, + HID_FEATURE_REPORT, + get ? HID_REQ_GET_REPORT : HID_REQ_SET_REPORT); + return ret < 0 ? ret : 0; +} + +static void asus_lamparray_prepare(struct asus_drvdata *drvdata, u8 rid) +{ + memset(drvdata->lamparray_buf, 0, drvdata->lamparray_buf_len); + drvdata->lamparray_buf[0] = rid; +} + +static struct hid_device *asus_find_lamparray_sibling(struct hid_device *hdev) +{ + struct usb_interface *intf; + struct usb_device *udev; + struct usb_host_config *config; + unsigned int i; + + if (!hid_is_usb(hdev)) + return NULL; + + intf = to_usb_interface(hdev->dev.parent); + udev = interface_to_usbdev(intf); + if (!udev->actconfig) + return NULL; + + config = udev->actconfig; + for (i = 0; i < config->desc.bNumInterfaces; i++) { + struct usb_interface *other = config->interface[i]; + struct hid_device *other_hdev; + + if (!other || other == intf) + continue; + 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) +{ + 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; +} + +/* + * 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, 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++) { + 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 -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; + 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; + + /* + * 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; + } + } + + return 0; +} + +/* 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) +{ + 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; + 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)); +} + +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 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); + drvdata->kbd_direct_valid = true; + + ret = asus_lamparray_try_apply_unlocked(drvdata, true); + 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); + drvdata->lb_direct_valid = true; + + ret = asus_lamparray_try_apply_unlocked(drvdata, false); + 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); + drvdata->lb_direct_valid = true; + 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 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, + const struct dl_rgb *palette, + 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")) { + 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; + + 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; + } + } + + /* + * 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_try_init_unlocked(drvdata); + if (drvdata->lamparray_count) + asus_lamparray_release_unlocked(drvdata); + } + + if (lightbar) + 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 (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, + 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; + + /* + * 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; +} + +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 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) +{ + 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; + 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) + 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); +} + +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 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) +{ + 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. + * + * 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 (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; +} + +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; + } + + /* + * 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) + 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 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) +{ + 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 (kbd && drvdata->kbd_effect_set) { + 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; + } + 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; +} + +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; + } + + 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); + + 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) { + hid_warn(hdev, + "Aura 0x9e capability probe failed: %d, using fallback effect list\n", + ret); + } else { + mask = effect_mask; + hid_info(hdev, "Aura effect capability mask %02x %02x\n", + effect_mask[0], effect_mask[1]); + } + + 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", + &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 (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"; + 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) + 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 (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"); + else + hid_info(hdev, "Leaving Aura controller state unchanged\n"); + + 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) +{ + return 0; +} + +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) +{ +} + +#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) @@ -1482,6 +4723,13 @@ 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); + if (drvdata->xbox_led) + asus_xbox_led_apply(drvdata->xbox_led); +#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 +4748,14 @@ 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); + if (drvdata->xbox_led) + asus_xbox_led_apply(drvdata->xbox_led); +#endif + + if (drvdata->tp) return asus_start_multitouch(hdev); @@ -1520,6 +4776,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 +4861,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; 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_NONE) { + hid_dbg(hdev, + "Skipping ambiguous 0x193b (not Slash/AniMe)\n"); + return -ENODEV; + } + } + /* Check for vendor for RGB init and handle generic devices properly. */ rep_enum = &hdev->report_enum[HID_INPUT_REPORT]; list_for_each_entry(rep, &rep_enum->report_list, list) { @@ -1609,6 +4891,23 @@ 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) { hid_warn(hdev, "Failed to initialize worker: %d\n", ret); @@ -1622,25 +4921,54 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id) return ret; } - if (!drvdata->tp) - asus_initialize_reports(hdev); + 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; + } + } - 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"); + + 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); } } - /* 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"); - /* * For ROG keyboards, skip rename for consistency and ->input check as * some devices do not have inputs. @@ -1687,6 +5015,26 @@ 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); @@ -1826,6 +5174,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 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 */