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