Netdev List
 help / color / mirror / Atom feed
From: Rong Zhang <i@rong.moe>
To: "Lee Jones" <lee@kernel.org>, "Pavel Machek" <pavel@kernel.org>,
	"Jonathan Corbet" <corbet@lwn.net>,
	"Shuah Khan" <skhan@linuxfoundation.org>,
	"Thomas Weißschuh" <linux@weissschuh.net>,
	"Benson Leung" <bleung@chromium.org>,
	"Guenter Roeck" <groeck@chromium.org>,
	"Marek Behún" <kabel@kernel.org>,
	"Mark Pearson" <mpearson-lenovo@squebb.ca>,
	"Derek J. Clark" <derekjohn.clark@gmail.com>,
	"Hans de Goede" <hansg@kernel.org>,
	"Ilpo Järvinen" <ilpo.jarvinen@linux.intel.com>,
	"Ike Panhc" <ikepanhc@gmail.com>
Cc: Andrew Lunn <andrew+netdev@lunn.ch>,
	Jakub Kicinski <kuba@kernel.org>,
	 Vishnu Sankar <vishnuocv@gmail.com>,
	Vishnu Sankar <vsankar@lenovo.com>,
	 linux-leds@vger.kernel.org, netdev@vger.kernel.org,
	 linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org,
	 chrome-platform@lists.linux.dev,
	platform-driver-x86@vger.kernel.org,  Rong Zhang <i@rong.moe>
Subject: [PATCH v6 12/12] platform/x86: ideapad-laptop: Fully support auto keyboard backlight
Date: Wed, 02 Sep 2026 02:09:31 +0800	[thread overview]
Message-ID: <20260902-leds-trigger-hw-changed-v6-12-55693cd78877@rong.moe> (raw)
In-Reply-To: <20260902-leds-trigger-hw-changed-v6-0-55693cd78877@rong.moe>

Currently, the auto brightness mode of keyboard backlight maps to
brightness=0 in LED classdev. The only method to switch to such a mode
is by pressing the manufacturer-defined shortcut (Fn+Space). However, 0
is a multiplexed brightness value; writing 0 simply results in the
backlight being turned off.

With brightness processing code decoupled from LED classdev, we can now
fully support the auto brightness mode. In this mode, the keyboard
backlight is controlled by the EC according to the ambient light sensor
(ALS).

To utilize this, a private hardware control trigger "ideapad-auto" is
added, with the event handling procedure calling the
led_trigger_notify_hw_control_changed() interface to activate/deactivate
the private trigger according to the current LED trigger state. To align
with LEDS_BRIGHTNESS_HW_CHANGED, the driver neither depends on
LEDS_TRIGGERS_HW_CHANGED nor selects it.

Meanwhile, block brightness changes on exit to prevent the side effect
of LED device unregistration when the private trigger is active from
resetting the brightness to zero, so that the state of auto mode can
retain among boots.

Acked-by: Ike Panhc <ikepanhc@gmail.com>
Signed-off-by: Rong Zhang <i@rong.moe>
---
Changes in v6:
- Simplify the auto mode state preservation logic by converting
  `priv->kbd_bl.initialized' into a tristate integer

Changes in v4:
- Add missing #include (Thanks Ilpo Järvinen)
- Remove needless code alignment (ditto)
- Address concerns from Sashiko
  - Do not notify the LED trigger core when the registration of the
    private trigger has failed
  - https://sashiko.dev/#/patchset/20260719-leds-trigger-hw-changed-v3-0-5fb55722e36e@rong.moe?part=11

Changes in v3:
- Address concerns from Sashiko
  - Fix a race condition in ideapad_kbd_bl_led_cdev_brightness_set()
  - Fix trigger re-registration of ideapad_kbd_bl_auto_trigger
  - https://sashiko.dev/#/patchset/20260618-leds-trigger-hw-changed-v2-0-c28c44053cf3%40rong.moe
- Make registration failures of ideapad_kbd_bl_auto_trigger non-fatal
---
 drivers/platform/x86/lenovo/Kconfig          |   1 +
 drivers/platform/x86/lenovo/ideapad-laptop.c | 119 ++++++++++++++++++++++++---
 2 files changed, 109 insertions(+), 11 deletions(-)

diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
index 516594993073..a97852b0b228 100644
--- a/drivers/platform/x86/lenovo/Kconfig
+++ b/drivers/platform/x86/lenovo/Kconfig
@@ -16,6 +16,7 @@ config IDEAPAD_LAPTOP
 	select INPUT_SPARSEKMAP
 	select NEW_LEDS
 	select LEDS_CLASS
+	select LEDS_TRIGGERS
 	help
 	  This is a driver for Lenovo IdeaPad netbooks contains drivers for
 	  rfkill switch, hotkey, fan control and backlight control.
diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.c b/drivers/platform/x86/lenovo/ideapad-laptop.c
index 712999b6900d..f86c11f51d0b 100644
--- a/drivers/platform/x86/lenovo/ideapad-laptop.c
+++ b/drivers/platform/x86/lenovo/ideapad-laptop.c
@@ -15,6 +15,7 @@
 #include <linux/bug.h>
 #include <linux/cleanup.h>
 #include <linux/compiler.h>
+#include <linux/container_of.h>
 #include <linux/debugfs.h>
 #include <linux/delay.h>
 #include <linux/device.h>
@@ -32,6 +33,7 @@
 #include <linux/platform_device.h>
 #include <linux/platform_profile.h>
 #include <linux/power_supply.h>
+#include <linux/printk.h>
 #include <linux/rfkill.h>
 #include <linux/seq_file.h>
 #include <linux/string_choices.h>
@@ -226,7 +228,10 @@ struct ideapad_private {
 		bool ymc_ec_trigger       : 1;
 	} features;
 	struct {
-		bool initialized;
+		int initialized; /*  0: initializion pending/ongoing/failed
+				  * >0: initializion finished
+				  * <0: unregisteration ongoing/finished
+				  */
 		int type;
 		struct led_classdev led;
 		unsigned int last_hw_brightness;
@@ -1716,14 +1721,68 @@ static int ideapad_kbd_bl_led_cdev_brightness_set(struct led_classdev *led_cdev,
 {
 	struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
 
+	/*
+	 * When unregistering: It must be the side effect of LED unregistration
+	 * when our private trigger is active. We've set LED_RETAIN_AT_SHUTDOWN
+	 * to retain led_cdev brightness level. To do the same for auto mode,
+	 * gate changes and return early.
+	 *
+	 * It's needless to gate changes when initializing, as there may be a
+	 * time margin between sysfs attribute creation and state change.
+	 */
+	if (unlikely(priv->kbd_bl.initialized < 0))
+		return 0;
+
 	return ideapad_kbd_bl_brightness_set(priv, brightness);
 }
 
+static bool ideapad_kbd_bl_auto_trigger_offloaded(struct led_classdev *led_cdev)
+{
+	struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
+
+	guard(mutex)(&priv->kbd_bl.mutex);
+
+	return priv->kbd_bl.last_hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+}
+
+static int ideapad_kbd_bl_auto_trigger_activate(struct led_classdev *led_cdev)
+{
+	struct ideapad_private *priv = container_of(led_cdev, struct ideapad_private, kbd_bl.led);
+
+	return ideapad_kbd_bl_hw_brightness_set(priv, KBD_BL_AUTO_MODE_HW_BRIGHTNESS);
+}
+
+static struct led_hw_trigger_type ideapad_kbd_bl_auto_trigger_type;
+
+static struct led_trigger ideapad_kbd_bl_auto_trigger = {
+	.name = "ideapad-auto",
+	.trigger_type = &ideapad_kbd_bl_auto_trigger_type,
+	.activate = ideapad_kbd_bl_auto_trigger_activate,
+	.offloaded = ideapad_kbd_bl_auto_trigger_offloaded,
+};
+
+static bool ideapad_kbd_bl_auto_trigger_registered;
+
+static void ideapad_kbd_bl_notify_hw_control(struct ideapad_private *priv,
+					     unsigned int hw_brightness)
+{
+	bool hw_control, last_hw_control;
+
+	if (!ideapad_kbd_bl_auto_trigger_registered || priv->kbd_bl.type != KBD_BL_TRISTATE_AUTO)
+		return;
+
+	hw_control = hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+	last_hw_control = priv->kbd_bl.last_hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS;
+
+	if (hw_control != last_hw_control)
+		led_trigger_notify_hw_control_changed(&priv->kbd_bl.led, hw_control);
+}
+
 static void ideapad_kbd_bl_notify(struct ideapad_private *priv)
 {
 	int hw_brightness, brightness;
 
-	if (!priv->kbd_bl.initialized)
+	if (unlikely(priv->kbd_bl.initialized <= 0))
 		return;
 
 	guard(mutex)(&priv->kbd_bl.mutex);
@@ -1739,6 +1798,8 @@ static void ideapad_kbd_bl_notify(struct ideapad_private *priv)
 	if (priv->kbd_bl.last_hw_brightness == hw_brightness)
 		return;
 
+	ideapad_kbd_bl_notify_hw_control(priv, hw_brightness);
+
 	priv->kbd_bl.last_hw_brightness = hw_brightness;
 
 	led_classdev_notify_brightness_hw_changed(&priv->kbd_bl.led, brightness);
@@ -1751,9 +1812,11 @@ static int ideapad_kbd_bl_init(struct ideapad_private *priv)
 	if (!priv->features.kbd_bl)
 		return -ENODEV;
 
-	if (WARN_ON(priv->kbd_bl.initialized))
+	if (WARN_ON(priv->kbd_bl.initialized > 0))
 		return -EEXIST;
 
+	priv->kbd_bl.initialized = 0;
+
 	err = devm_mutex_init(&priv->platform_device->dev, &priv->kbd_bl.mutex);
 	if (err)
 		return err;
@@ -1771,6 +1834,24 @@ static int ideapad_kbd_bl_init(struct ideapad_private *priv)
 
 	switch (priv->kbd_bl.type) {
 	case KBD_BL_TRISTATE_AUTO:
+		priv->kbd_bl.led.max_brightness = 2;
+
+		if (!ideapad_kbd_bl_auto_trigger_registered) {
+			dev_warn(&priv->platform_device->dev,
+				 "Cannot provide LED trigger %s for keyboard backlight\n",
+				 ideapad_kbd_bl_auto_trigger.name);
+			break;
+		}
+
+		priv->kbd_bl.led.flags |= LED_TRIG_HW_CHANGED;
+		priv->kbd_bl.led.trigger_type = &ideapad_kbd_bl_auto_trigger_type;
+		priv->kbd_bl.led.hw_control_trigger = ideapad_kbd_bl_auto_trigger.name;
+
+		/* Hardware remembers the last brightness level, including auto mode. */
+		if (hw_brightness == KBD_BL_AUTO_MODE_HW_BRIGHTNESS)
+			priv->kbd_bl.led.default_trigger = ideapad_kbd_bl_auto_trigger.name;
+
+		break;
 	case KBD_BL_TRISTATE:
 		priv->kbd_bl.led.max_brightness = 2;
 		break;
@@ -1791,17 +1872,17 @@ static int ideapad_kbd_bl_init(struct ideapad_private *priv)
 	if (err)
 		return err;
 
-	priv->kbd_bl.initialized = true;
+	priv->kbd_bl.initialized = 1;
 
 	return 0;
 }
 
 static void ideapad_kbd_bl_exit(struct ideapad_private *priv)
 {
-	if (!priv->kbd_bl.initialized)
+	if (priv->kbd_bl.initialized <= 0)
 		return;
 
-	priv->kbd_bl.initialized = false;
+	priv->kbd_bl.initialized = -1;
 
 	led_classdev_unregister(&priv->kbd_bl.led);
 }
@@ -2620,17 +2701,30 @@ static int __init ideapad_laptop_init(void)
 {
 	int err;
 
+	err = led_trigger_register(&ideapad_kbd_bl_auto_trigger);
+	if (err) {
+		pr_warn("Failed to register LED trigger %s: %d\n",
+			ideapad_kbd_bl_auto_trigger.name, err);
+	} else {
+		ideapad_kbd_bl_auto_trigger_registered = true;
+	}
+
 	err = ideapad_wmi_driver_register();
 	if (err)
-		return err;
+		goto err_ledtrig;
 
 	err = platform_driver_register(&ideapad_acpi_driver);
-	if (err) {
-		ideapad_wmi_driver_unregister();
-		return err;
-	}
+	if (err)
+		goto err_wmi;
 
 	return 0;
+
+err_wmi:
+	ideapad_wmi_driver_unregister();
+err_ledtrig:
+	if (ideapad_kbd_bl_auto_trigger_registered)
+		led_trigger_unregister(&ideapad_kbd_bl_auto_trigger);
+	return err;
 }
 module_init(ideapad_laptop_init)
 
@@ -2638,6 +2732,9 @@ static void __exit ideapad_laptop_exit(void)
 {
 	platform_driver_unregister(&ideapad_acpi_driver);
 	ideapad_wmi_driver_unregister();
+
+	if (ideapad_kbd_bl_auto_trigger_registered)
+		led_trigger_unregister(&ideapad_kbd_bl_auto_trigger);
 }
 module_exit(ideapad_laptop_exit)
 

-- 
2.55.0


      parent reply	other threads:[~2026-09-01 18:15 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-01 18:09 [PATCH v6 00/12] leds: Add support for hardware-initiated hardware control trigger transition Rong Zhang
2026-09-01 18:09 ` [PATCH v6 01/12] leds: class: Always protect brightness_show() with led_access Rong Zhang
2026-09-01 18:09 ` [PATCH v6 02/12] leds: Move led_trigger_is_hw_controlled() to the right place Rong Zhang
2026-09-01 18:09 ` [PATCH v6 03/12] leds: class: Remove hardware control trigger when writing brightness Rong Zhang
2026-09-01 18:09 ` [PATCH v6 04/12] leds: trigger: Add offloaded() callback and provide trigger_may_offload attribute Rong Zhang
2026-09-01 18:09 ` [PATCH v6 05/12] leds: cros_ec: Implement offloaded() trigger callback Rong Zhang
2026-09-01 18:09 ` [PATCH v6 06/12] leds: turris-omnia: Implement offloaded() trigger callback and declare hw_control_trigger Rong Zhang
2026-09-01 18:09 ` [PATCH v6 07/12] leds: trigger: netdev: Implement offloaded() callback Rong Zhang
2026-09-01 18:09 ` [PATCH v6 08/12] leds: trigger: Enforce strict checks in led_trigger_is_hw_controlled() Rong Zhang
2026-09-01 18:09 ` [PATCH v6 09/12] leds: trigger: Add led_trigger_notify_hw_control_changed() interface Rong Zhang
2026-09-01 18:09 ` [PATCH v6 10/12] platform/x86: ideapad-laptop: Serialize keyboard backlight tracking Rong Zhang
2026-09-01 18:09 ` [PATCH v6 11/12] platform/x86: ideapad-laptop: Decouple hardware & classdev brightness for keyboard backlight Rong Zhang
2026-09-01 18:09 ` Rong Zhang [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260902-leds-trigger-hw-changed-v6-12-55693cd78877@rong.moe \
    --to=i@rong.moe \
    --cc=andrew+netdev@lunn.ch \
    --cc=bleung@chromium.org \
    --cc=chrome-platform@lists.linux.dev \
    --cc=corbet@lwn.net \
    --cc=derekjohn.clark@gmail.com \
    --cc=groeck@chromium.org \
    --cc=hansg@kernel.org \
    --cc=ikepanhc@gmail.com \
    --cc=ilpo.jarvinen@linux.intel.com \
    --cc=kabel@kernel.org \
    --cc=kuba@kernel.org \
    --cc=lee@kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-leds@vger.kernel.org \
    --cc=linux@weissschuh.net \
    --cc=mpearson-lenovo@squebb.ca \
    --cc=netdev@vger.kernel.org \
    --cc=pavel@kernel.org \
    --cc=platform-driver-x86@vger.kernel.org \
    --cc=skhan@linuxfoundation.org \
    --cc=vishnuocv@gmail.com \
    --cc=vsankar@lenovo.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox