Skip to content

Commit fac55d2

Browse files
antheasij-intel
authored andcommitted
platform/x86: asus-wmi: Add support for multiple kbd led handlers
Some devices, such as the Z13 have multiple Aura devices connected to them by USB. In addition, they might have a WMI interface for RGB. In Windows, Armoury Crate exposes a unified brightness slider for all of them, with 3 brightness levels. Therefore, to be synergistic in Linux, and support existing tooling such as UPower, allow adding listeners to the RGB device of the WMI interface. If WMI does not exist, lazy initialize the interface. Since hid-asus and asus-wmi can both interact with the led objects including from an atomic context, protect the brightness access with a spinlock and update the values from a workqueue. Use this workqueue to also process WMI keyboard events, so they are handled asynchronously. Acked-by: Benjamin Tissoires <bentiss@kernel.org> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev> Reviewed-by: Denis Benato <benato.denis96@gmail.com> Link: https://patch.msgid.link/20260122075044.5070-8-lkml@antheas.dev Reviewed-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com> Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
1 parent 4ac74ea commit fac55d2

2 files changed

Lines changed: 173 additions & 25 deletions

File tree

drivers/platform/x86/asus-wmi.c

Lines changed: 158 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@
3131
#include <linux/pci.h>
3232
#include <linux/pci_hotplug.h>
3333
#include <linux/platform_data/x86/asus-wmi.h>
34-
#include <linux/platform_data/x86/asus-wmi-leds-ids.h>
3534
#include <linux/platform_device.h>
3635
#include <linux/platform_profile.h>
3736
#include <linux/power_supply.h>
3837
#include <linux/rfkill.h>
3938
#include <linux/seq_file.h>
4039
#include <linux/slab.h>
40+
#include <linux/spinlock.h>
4141
#include <linux/types.h>
4242
#include <linux/units.h>
4343

@@ -256,6 +256,9 @@ struct asus_wmi {
256256
int tpd_led_wk;
257257
struct led_classdev kbd_led;
258258
int kbd_led_wk;
259+
bool kbd_led_notify;
260+
bool kbd_led_avail;
261+
bool kbd_led_registered;
259262
struct led_classdev lightbar_led;
260263
int lightbar_led_wk;
261264
struct led_classdev micmute_led;
@@ -264,6 +267,7 @@ struct asus_wmi {
264267
struct work_struct tpd_led_work;
265268
struct work_struct wlan_led_work;
266269
struct work_struct lightbar_led_work;
270+
struct work_struct kbd_led_work;
267271

268272
struct asus_rfkill wlan;
269273
struct asus_rfkill bluetooth;
@@ -1615,6 +1619,106 @@ static void asus_wmi_battery_exit(struct asus_wmi *asus)
16151619

16161620
/* LEDs ***********************************************************************/
16171621

1622+
struct asus_hid_ref {
1623+
struct list_head listeners;
1624+
struct asus_wmi *asus;
1625+
/* Protects concurrent access from hid-asus and asus-wmi to leds */
1626+
spinlock_t lock;
1627+
};
1628+
1629+
static struct asus_hid_ref asus_ref = {
1630+
.listeners = LIST_HEAD_INIT(asus_ref.listeners),
1631+
.asus = NULL,
1632+
/*
1633+
* Protects .asus, .asus.kbd_led_{wk,notify}, and .listener refs. Other
1634+
* asus variables are read-only after .asus is set.
1635+
*
1636+
* The led cdev device is not protected because it calls backlight_get
1637+
* during initialization, which would result in a nested lock attempt.
1638+
*
1639+
* The led cdev is safe to access without a lock because if
1640+
* kbd_led_avail is true it is initialized before .asus is set and never
1641+
* changed until .asus is dropped. If kbd_led_avail is false, the led
1642+
* cdev is registered by the workqueue, which is single-threaded and
1643+
* cancelled before asus-wmi would access the led cdev to unregister it.
1644+
*
1645+
* A spinlock is used, because the protected variables can be accessed
1646+
* from an IRQ context from asus-hid.
1647+
*/
1648+
.lock = __SPIN_LOCK_UNLOCKED(asus_ref.lock),
1649+
};
1650+
1651+
/*
1652+
* Allows registering hid-asus listeners that want to be notified of
1653+
* keyboard backlight changes.
1654+
*/
1655+
int asus_hid_register_listener(struct asus_hid_listener *bdev)
1656+
{
1657+
struct asus_wmi *asus;
1658+
1659+
guard(spinlock_irqsave)(&asus_ref.lock);
1660+
list_add_tail(&bdev->list, &asus_ref.listeners);
1661+
asus = asus_ref.asus;
1662+
if (asus)
1663+
queue_work(asus->led_workqueue, &asus->kbd_led_work);
1664+
return 0;
1665+
}
1666+
EXPORT_SYMBOL_GPL(asus_hid_register_listener);
1667+
1668+
/*
1669+
* Allows unregistering hid-asus listeners that were added with
1670+
* asus_hid_register_listener().
1671+
*/
1672+
void asus_hid_unregister_listener(struct asus_hid_listener *bdev)
1673+
{
1674+
guard(spinlock_irqsave)(&asus_ref.lock);
1675+
list_del(&bdev->list);
1676+
}
1677+
EXPORT_SYMBOL_GPL(asus_hid_unregister_listener);
1678+
1679+
static void do_kbd_led_set(struct led_classdev *led_cdev, int value);
1680+
1681+
static void kbd_led_update_all(struct work_struct *work)
1682+
{
1683+
struct asus_wmi *asus;
1684+
bool registered, notify;
1685+
int ret, value;
1686+
1687+
asus = container_of(work, struct asus_wmi, kbd_led_work);
1688+
1689+
scoped_guard(spinlock_irqsave, &asus_ref.lock) {
1690+
registered = asus->kbd_led_registered;
1691+
value = asus->kbd_led_wk;
1692+
notify = asus->kbd_led_notify;
1693+
}
1694+
1695+
if (!registered) {
1696+
/*
1697+
* This workqueue runs under asus-wmi, which means probe has
1698+
* completed and asus-wmi will keep running until it finishes.
1699+
* Therefore, we can safely register the LED without holding
1700+
* a spinlock.
1701+
*/
1702+
ret = devm_led_classdev_register(&asus->platform_device->dev,
1703+
&asus->kbd_led);
1704+
if (!ret) {
1705+
scoped_guard(spinlock_irqsave, &asus_ref.lock)
1706+
asus->kbd_led_registered = true;
1707+
} else {
1708+
pr_warn("Failed to register keyboard backlight LED: %d\n", ret);
1709+
return;
1710+
}
1711+
}
1712+
1713+
if (value >= 0)
1714+
do_kbd_led_set(&asus->kbd_led, value);
1715+
if (notify) {
1716+
scoped_guard(spinlock_irqsave, &asus_ref.lock)
1717+
asus->kbd_led_notify = false;
1718+
led_classdev_notify_brightness_hw_changed(&asus->kbd_led, value);
1719+
}
1720+
}
1721+
16181722
/*
16191723
* These functions actually update the LED's, and are called from a
16201724
* workqueue. By doing this as separate work rather than when the LED
@@ -1661,7 +1765,8 @@ static void kbd_led_update(struct asus_wmi *asus)
16611765
{
16621766
int ctrl_param = 0;
16631767

1664-
ctrl_param = 0x80 | (asus->kbd_led_wk & 0x7F);
1768+
scoped_guard(spinlock_irqsave, &asus_ref.lock)
1769+
ctrl_param = 0x80 | (asus->kbd_led_wk & 0x7F);
16651770
asus_wmi_set_devstate(ASUS_WMI_DEVID_KBD_BACKLIGHT, ctrl_param, NULL);
16661771
}
16671772

@@ -1694,14 +1799,23 @@ static int kbd_led_read(struct asus_wmi *asus, int *level, int *env)
16941799

16951800
static void do_kbd_led_set(struct led_classdev *led_cdev, int value)
16961801
{
1802+
struct asus_hid_listener *listener;
16971803
struct asus_wmi *asus;
16981804
int max_level;
16991805

17001806
asus = container_of(led_cdev, struct asus_wmi, kbd_led);
17011807
max_level = asus->kbd_led.max_brightness;
17021808

1703-
asus->kbd_led_wk = clamp_val(value, 0, max_level);
1704-
kbd_led_update(asus);
1809+
scoped_guard(spinlock_irqsave, &asus_ref.lock)
1810+
asus->kbd_led_wk = clamp_val(value, 0, max_level);
1811+
1812+
if (asus->kbd_led_avail)
1813+
kbd_led_update(asus);
1814+
1815+
scoped_guard(spinlock_irqsave, &asus_ref.lock) {
1816+
list_for_each_entry(listener, &asus_ref.listeners, list)
1817+
listener->brightness_set(listener, asus->kbd_led_wk);
1818+
}
17051819
}
17061820

17071821
static int kbd_led_set(struct led_classdev *led_cdev, enum led_brightness value)
@@ -1716,10 +1830,11 @@ static int kbd_led_set(struct led_classdev *led_cdev, enum led_brightness value)
17161830

17171831
static void kbd_led_set_by_kbd(struct asus_wmi *asus, enum led_brightness value)
17181832
{
1719-
struct led_classdev *led_cdev = &asus->kbd_led;
1720-
1721-
do_kbd_led_set(led_cdev, value);
1722-
led_classdev_notify_brightness_hw_changed(led_cdev, asus->kbd_led_wk);
1833+
scoped_guard(spinlock_irqsave, &asus_ref.lock) {
1834+
asus->kbd_led_wk = value;
1835+
asus->kbd_led_notify = true;
1836+
}
1837+
queue_work(asus->led_workqueue, &asus->kbd_led_work);
17231838
}
17241839

17251840
static enum led_brightness kbd_led_get(struct led_classdev *led_cdev)
@@ -1729,10 +1844,18 @@ static enum led_brightness kbd_led_get(struct led_classdev *led_cdev)
17291844

17301845
asus = container_of(led_cdev, struct asus_wmi, kbd_led);
17311846

1847+
scoped_guard(spinlock_irqsave, &asus_ref.lock) {
1848+
if (!asus->kbd_led_avail)
1849+
return asus->kbd_led_wk;
1850+
}
1851+
17321852
retval = kbd_led_read(asus, &value, NULL);
17331853
if (retval < 0)
17341854
return retval;
17351855

1856+
scoped_guard(spinlock_irqsave, &asus_ref.lock)
1857+
asus->kbd_led_wk = value;
1858+
17361859
return value;
17371860
}
17381861

@@ -1844,7 +1967,9 @@ static int camera_led_set(struct led_classdev *led_cdev,
18441967

18451968
static void asus_wmi_led_exit(struct asus_wmi *asus)
18461969
{
1847-
led_classdev_unregister(&asus->kbd_led);
1970+
scoped_guard(spinlock_irqsave, &asus_ref.lock)
1971+
asus_ref.asus = NULL;
1972+
18481973
led_classdev_unregister(&asus->tpd_led);
18491974
led_classdev_unregister(&asus->wlan_led);
18501975
led_classdev_unregister(&asus->lightbar_led);
@@ -1882,22 +2007,26 @@ static int asus_wmi_led_init(struct asus_wmi *asus)
18822007
goto error;
18832008
}
18842009

1885-
if (!kbd_led_read(asus, &led_val, NULL) && !dmi_check_system(asus_use_hid_led_dmi_ids)) {
1886-
pr_info("using asus-wmi for asus::kbd_backlight\n");
1887-
asus->kbd_led_wk = led_val;
1888-
asus->kbd_led.name = "asus::kbd_backlight";
1889-
asus->kbd_led.flags = LED_BRIGHT_HW_CHANGED;
1890-
asus->kbd_led.brightness_set_blocking = kbd_led_set;
1891-
asus->kbd_led.brightness_get = kbd_led_get;
1892-
asus->kbd_led.max_brightness = 3;
2010+
asus->kbd_led.name = "asus::kbd_backlight";
2011+
asus->kbd_led.flags = LED_BRIGHT_HW_CHANGED;
2012+
asus->kbd_led.brightness_set_blocking = kbd_led_set;
2013+
asus->kbd_led.brightness_get = kbd_led_get;
2014+
asus->kbd_led.max_brightness = 3;
2015+
asus->kbd_led_avail = !kbd_led_read(asus, &led_val, NULL);
2016+
INIT_WORK(&asus->kbd_led_work, kbd_led_update_all);
18932017

2018+
if (asus->kbd_led_avail) {
2019+
asus->kbd_led_wk = led_val;
18942020
if (num_rgb_groups != 0)
18952021
asus->kbd_led.groups = kbd_rgb_mode_groups;
2022+
} else {
2023+
asus->kbd_led_wk = -1;
2024+
}
18962025

1897-
rv = led_classdev_register(&asus->platform_device->dev,
1898-
&asus->kbd_led);
1899-
if (rv)
1900-
goto error;
2026+
scoped_guard(spinlock_irqsave, &asus_ref.lock) {
2027+
asus_ref.asus = asus;
2028+
if (asus->kbd_led_avail || !list_empty(&asus_ref.listeners))
2029+
queue_work(asus->led_workqueue, &asus->kbd_led_work);
19012030
}
19022031

19032032
if (asus_wmi_dev_is_present(asus, ASUS_WMI_DEVID_WIRELESS_LED)
@@ -4372,6 +4501,7 @@ static int asus_wmi_get_event_code(union acpi_object *obj)
43724501

43734502
static void asus_wmi_handle_event_code(int code, struct asus_wmi *asus)
43744503
{
4504+
enum led_brightness led_value;
43754505
unsigned int key_value = 1;
43764506
bool autorelease = 1;
43774507

@@ -4388,19 +4518,22 @@ static void asus_wmi_handle_event_code(int code, struct asus_wmi *asus)
43884518
return;
43894519
}
43904520

4521+
scoped_guard(spinlock_irqsave, &asus_ref.lock)
4522+
led_value = asus->kbd_led_wk;
4523+
43914524
if (code == NOTIFY_KBD_BRTUP) {
4392-
kbd_led_set_by_kbd(asus, asus->kbd_led_wk + 1);
4525+
kbd_led_set_by_kbd(asus, led_value + 1);
43934526
return;
43944527
}
43954528
if (code == NOTIFY_KBD_BRTDWN) {
4396-
kbd_led_set_by_kbd(asus, asus->kbd_led_wk - 1);
4529+
kbd_led_set_by_kbd(asus, led_value - 1);
43974530
return;
43984531
}
43994532
if (code == NOTIFY_KBD_BRTTOGGLE) {
4400-
if (asus->kbd_led_wk == asus->kbd_led.max_brightness)
4533+
if (led_value >= asus->kbd_led.max_brightness)
44014534
kbd_led_set_by_kbd(asus, 0);
44024535
else
4403-
kbd_led_set_by_kbd(asus, asus->kbd_led_wk + 1);
4536+
kbd_led_set_by_kbd(asus, led_value + 1);
44044537
return;
44054538
}
44064539

include/linux/platform_data/x86/asus-wmi.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,20 @@ enum asus_ally_mcu_hack {
172172
ASUS_WMI_ALLY_MCU_HACK_DISABLED,
173173
};
174174

175+
/* Used to notify hid-asus when asus-wmi changes keyboard backlight */
176+
struct asus_hid_listener {
177+
struct list_head list;
178+
void (*brightness_set)(struct asus_hid_listener *listener, int brightness);
179+
};
180+
175181
#if IS_REACHABLE(CONFIG_ASUS_WMI)
176182
void set_ally_mcu_hack(enum asus_ally_mcu_hack status);
177183
void set_ally_mcu_powersave(bool enabled);
178184
int asus_wmi_get_devstate_dsts(u32 dev_id, u32 *retval);
179185
int asus_wmi_set_devstate(u32 dev_id, u32 ctrl_param, u32 *retval);
180186
int asus_wmi_evaluate_method(u32 method_id, u32 arg0, u32 arg1, u32 *retval);
187+
int asus_hid_register_listener(struct asus_hid_listener *cdev);
188+
void asus_hid_unregister_listener(struct asus_hid_listener *cdev);
181189
#else
182190
static inline void set_ally_mcu_hack(enum asus_ally_mcu_hack status)
183191
{
@@ -198,6 +206,13 @@ static inline int asus_wmi_evaluate_method(u32 method_id, u32 arg0, u32 arg1,
198206
{
199207
return -ENODEV;
200208
}
209+
static inline int asus_hid_register_listener(struct asus_hid_listener *bdev)
210+
{
211+
return -ENODEV;
212+
}
213+
static inline void asus_hid_unregister_listener(struct asus_hid_listener *bdev)
214+
{
215+
}
201216
#endif
202217

203218
#endif /* __PLATFORM_DATA_X86_ASUS_WMI_H */

0 commit comments

Comments
 (0)