mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Qiling Zhu <kmatzkaczor244@gmail.com>
To: "Mark Pearson" <mpearson-lenovo@squebb.ca>,
	"Derek J. Clark" <derekjohn.clark@gmail.com>,
	"Rong Zhang" <i@rong.moe>, "Hans de Goede" <hansg@kernel.org>,
	"Ilpo Järvinen" <ilpo.jarvinen@linux.intel.com>,
	"Henrique de Moraes Holschuh" <hmh@hmh.eng.br>
Cc: Griffin Kroah-Hartman <griffin@kroah.com>,
	ibm-acpi-devel@lists.sourceforge.net, Lee Jones <lee@kernel.org>,
	linux-kernel@vger.kernel.org, linux-leds@vger.kernel.org,
	Pavel Machek <pavel@kernel.org>,
	platform-driver-x86@vger.kernel.org, vishnuocv@gmail.com,
	vsankar@lenovo.com, Qiling Zhu <kmatzkaczor244@gmail.com>
Subject: [RFC PATCH v2 2/2] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight
Date: Mon,  2 Nov 2026 23:10:19 +0800	[thread overview]
Message-ID: <20261102151211.40701-3-kmatzkaczor244@gmail.com> (raw)
In-Reply-To: <20261102151211.40701-1-kmatzkaczor244@gmail.com>

Recent ThinkPads with an ambient light sensor (ALS) support an auto mode
for the keyboard backlight: the EC controls the backlight according to
ambient light. The mode is EC keyboard backlight level 3, above the two
fixed levels exposed so far, and is currently reachable only through the
Fn+Space shortcut, which is handled directly by the firmware.

Wire the auto mode up as a private hardware control trigger, named
"tpacpi-kbd-auto", on top of the LED hw control trigger framework:

  - selecting the trigger writes level 3 (activate); the offloaded state
    is derived from the current EC level
  - when Fn+Space cycles into or out of auto mode, the driver notifies
    the LED core about the hardware-initiated hardware control
    transition, so the trigger state (shown bracketed in the trigger
    sysfs attribute) follows along
  - if the EC is already in auto mode at driver initialization, the
    trigger is set as the default one

The auto mode is not a brightness level: max_brightness stays 2 and the
LED core is always told a brightness of 0 while in auto mode. The proc
interface keeps reporting the raw EC level, including 3, and keeps
rejecting writes of 3 with -EINVAL; writing a regular level through it
while in auto mode drops the trigger, like writing a brightness through
sysfs does. In auto mode suspend and resume are left to the EC and the
firmware; on resume the restored EC level is observed and reconciled.

LEDS_TRIGGERS is selected to keep the trigger functional in
stripped-down configurations.

Deliberately unchanged: module unload still resets the backlight to off;
retaining auto mode across unload is left for future work.

Note that Documentation/admin-guide/laptops/thinkpad-acpi.rst has no
kbdlight section at all, so there is no in-tree documentation to update;
userspace-facing semantics of hardware control triggers are documented
in Documentation/leds/leds-class.rst.

Signed-off-by: Qiling Zhu <kmatzkaczor244@gmail.com>
---
 drivers/platform/x86/lenovo/Kconfig         |   1 +
 drivers/platform/x86/lenovo/thinkpad_acpi.c | 152 ++++++++++++++++++--
 2 files changed, 141 insertions(+), 12 deletions(-)

diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
index 516594993..77d159a3a 100644
--- a/drivers/platform/x86/lenovo/Kconfig
+++ b/drivers/platform/x86/lenovo/Kconfig
@@ -83,6 +83,7 @@ config THINKPAD_ACPI
 	select NVRAM
 	select NEW_LEDS
 	select LEDS_CLASS
+	select LEDS_TRIGGERS
 	select INPUT_SPARSEKMAP
 	help
 	  This is a driver for the IBM and Lenovo ThinkPad laptops. It adds
diff --git a/drivers/platform/x86/lenovo/thinkpad_acpi.c b/drivers/platform/x86/lenovo/thinkpad_acpi.c
index 3260eaa62..7b46ad6f9 100644
--- a/drivers/platform/x86/lenovo/thinkpad_acpi.c
+++ b/drivers/platform/x86/lenovo/thinkpad_acpi.c
@@ -5102,6 +5102,14 @@ static struct ibm_struct video_driver_data = {
  * Keyboard backlight subdriver
  */
 
+/* EC level that selects ALS-controlled auto mode for the keyboard backlight */
+#define TPACPI_KBD_LIGHT_AUTO_LEVEL	3
+
+/*
+ * Cached EC level, including TPACPI_KBD_LIGHT_AUTO_LEVEL. All state
+ * tracking compares EC levels; the auto mode is translated to a
+ * brightness value only when reporting to the LED core.
+ */
 static enum led_brightness kbdlight_brightness;
 static DEFINE_MUTEX(kbdlight_mutex);
 
@@ -5180,6 +5188,15 @@ static bool kbdlight_is_supported(void)
 	return status & BIT(9);
 }
 
+/* The auto mode is not a brightness level; report it as off to the LED core. */
+static enum led_brightness kbdlight_level_to_brightness(int level)
+{
+	if (level == TPACPI_KBD_LIGHT_AUTO_LEVEL)
+		return 0;
+
+	return level;
+}
+
 static int kbdlight_sysfs_set(struct led_classdev *led_cdev,
 			enum led_brightness brightness)
 {
@@ -5194,16 +5211,54 @@ static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev)
 	if (level < 0)
 		return 0;
 
-	return level;
+	return kbdlight_level_to_brightness(level);
+}
+
+static struct led_hw_trigger_type kbdlight_auto_trigger_type;
+
+static int kbdlight_auto_trigger_activate(struct led_classdev *led_cdev)
+{
+	/*
+	 * The EC may already be in auto mode (cycled there on Fn+Space, or
+	 * active since before the driver loaded), in which case there is
+	 * nothing to program.
+	 */
+	if (kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL)
+		return 0;
+
+	if (kbdlight_set_level(TPACPI_KBD_LIGHT_AUTO_LEVEL))
+		return -EIO;
+
+	/*
+	 * Check that the EC actually accepted the auto mode; on error the
+	 * trigger core reverts the LED to the "none" trigger.
+	 */
+	if (kbdlight_get_level() != TPACPI_KBD_LIGHT_AUTO_LEVEL)
+		return -EIO;
+
+	return 0;
 }
 
+static bool kbdlight_auto_trigger_hw_offloaded(struct led_classdev *led_cdev)
+{
+	return kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL;
+}
+
+static struct led_trigger kbdlight_auto_trigger = {
+	.name		= "tpacpi-kbd-auto",
+	.trigger_type	= &kbdlight_auto_trigger_type,
+	.activate	= kbdlight_auto_trigger_activate,
+	.hw_offloaded	= kbdlight_auto_trigger_hw_offloaded,
+};
+
 static struct tpacpi_led_classdev tpacpi_led_kbdlight = {
 	.led_classdev = {
 		.name		= "tpacpi::kbd_backlight",
 		.max_brightness	= 2,
-		.flags		= LED_BRIGHT_HW_CHANGED,
+		.flags		= LED_BRIGHT_HW_CHANGED | LED_TRIG_HW_CHANGED,
 		.brightness_set_blocking = &kbdlight_sysfs_set,
 		.brightness_get	= &kbdlight_sysfs_get,
+		.trigger_type	= &kbdlight_auto_trigger_type,
 	}
 };
 
@@ -5221,9 +5276,30 @@ static int __init kbdlight_init(struct ibm_init_struct *iibm)
 		return -ENODEV;
 	}
 
-	kbdlight_brightness = kbdlight_sysfs_get(NULL);
+	kbdlight_brightness = kbdlight_get_level();
+	if (kbdlight_brightness < 0)
+		kbdlight_brightness = 0;
 	tp_features.kbdlight = 1;
 
+	/*
+	 * Register the auto-mode trigger before the LED classdev: if the
+	 * EC is already in auto mode, the trigger can then be set as the
+	 * default one and gets activated during LED classdev registration.
+	 */
+	rc = devm_led_trigger_register(&tpacpi_pdev->dev,
+				       &kbdlight_auto_trigger);
+	if (rc) {
+		tp_features.kbdlight = 0;
+		return rc;
+	}
+
+	tpacpi_led_kbdlight.led_classdev.hw_control_trigger =
+		kbdlight_auto_trigger.name;
+
+	if (kbdlight_brightness == TPACPI_KBD_LIGHT_AUTO_LEVEL)
+		tpacpi_led_kbdlight.led_classdev.default_trigger =
+			kbdlight_auto_trigger.name;
+
 	rc = led_classdev_register(&tpacpi_pdev->dev,
 				   &tpacpi_led_kbdlight.led_classdev);
 	if (rc < 0) {
@@ -5246,9 +5322,22 @@ static int kbdlight_set_level_and_update(int level)
 	int ret;
 	struct led_classdev *led_cdev;
 
-	ret = kbdlight_set_level(level);
 	led_cdev = &tpacpi_led_kbdlight.led_classdev;
 
+	/*
+	 * Leaving auto mode through the proc interface has to drop the
+	 * trigger, like writing a brightness through sysfs does. Route the
+	 * new level through the LED core as well, so that it is applied
+	 * after the LED_OFF queued by dropping the trigger.
+	 */
+	if (led_trigger_is_hw_controlled(led_cdev)) {
+		led_trigger_remove_hw_control(led_cdev);
+		led_set_brightness(led_cdev, level);
+		return 0;
+	}
+
+	ret = kbdlight_set_level(level);
+
 	if (ret == 0 && !(led_cdev->flags & LED_SUSPENDED))
 		led_cdev->brightness = level;
 
@@ -5293,6 +5382,8 @@ static int kbdlight_write(char *buf)
 	return kbdlight_set_level_and_update(level);
 }
 
+static void kbdlight_notify_change(void);
+
 static void kbdlight_suspend(void)
 {
 	struct led_classdev *led_cdev;
@@ -5301,38 +5392,75 @@ static void kbdlight_suspend(void)
 		return;
 
 	led_cdev = &tpacpi_led_kbdlight.led_classdev;
+
+	/*
+	 * In auto mode the EC drives the backlight and the firmware turns
+	 * it off for suspend and restores it on resume by itself; keep the
+	 * LED core out of it entirely.
+	 */
+	if (kbdlight_get_level() == TPACPI_KBD_LIGHT_AUTO_LEVEL)
+		return;
+
 	led_update_brightness(led_cdev);
 	led_classdev_suspend(led_cdev);
 }
 
 static void kbdlight_resume(void)
 {
+	struct led_classdev *led_cdev;
+
 	if (!tp_features.kbdlight)
 		return;
 
-	led_classdev_resume(&tpacpi_led_kbdlight.led_classdev);
+	led_cdev = &tpacpi_led_kbdlight.led_classdev;
+
+	/* Only balance a suspend that actually suspended the LED. */
+	if (led_cdev->flags & LED_SUSPENDED) {
+		led_classdev_resume(led_cdev);
+		return;
+	}
+
+	/*
+	 * Auto mode was left running over suspend; observe what the
+	 * firmware actually restored.
+	 */
+	kbdlight_notify_change();
 }
 
 /*
- * The EC cycles the keyboard backlight level itself on Fn+Space; check
- * the current level and update the LED core state.
+ * The EC cycles the keyboard backlight level (and auto mode) itself on
+ * Fn+Space; check the current level and update the LED core state.
  */
 static void kbdlight_notify_change(void)
 {
 	struct led_classdev *led_cdev = &tpacpi_led_kbdlight.led_classdev;
 	enum led_brightness brightness;
+	bool was_auto, is_auto;
+	int level;
 
 	guard(mutex)(&kbdlight_mutex);
 
 	/*
-	 * Check the brightness actually changed, setting the brightness
-	 * through kbdlight_set_level() also triggers this event.
+	 * Check the level actually changed, setting the brightness through
+	 * kbdlight_set_level() also triggers this event.
 	 */
-	brightness = kbdlight_sysfs_get(NULL);
-	if (kbdlight_brightness == brightness)
+	level = kbdlight_get_level();
+	if (kbdlight_brightness == level)
 		return;
 
-	kbdlight_brightness = brightness;
+	/*
+	 * Notifying the trigger core is safe while holding
+	 * kbdlight_mutex: the transition is only queued, and the worker
+	 * re-reads the EC level (via hw_offloaded) after this critical
+	 * section has been left.
+	 */
+	is_auto = level == TPACPI_KBD_LIGHT_AUTO_LEVEL;
+	was_auto = kbdlight_brightness == TPACPI_KBD_LIGHT_AUTO_LEVEL;
+	if (is_auto != was_auto)
+		led_trigger_notify_hw_control_changed(led_cdev, is_auto);
+
+	brightness = kbdlight_level_to_brightness(level);
+	kbdlight_brightness = level;
 	led_classdev_notify_brightness_hw_changed(led_cdev, brightness);
 }
 
-- 
2.56.0


  parent reply	other threads:[~2026-11-02 15:13 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-01 10:01 [RFC PATCH 0/1] " Qiling Zhu
2026-11-01 10:01 ` [RFC PATCH 1/1] " Qiling Zhu
2026-11-01 18:24   ` Rong Zhang
2026-11-01 17:57 ` [RFC PATCH 0/1] " Rong Zhang
2026-11-02 15:10 ` [RFC PATCH v2 0/2] " Qiling Zhu
2026-11-02 15:10   ` [RFC PATCH v2 1/2] platform/x86: thinkpad_acpi: Extract kbdlight_notify_change() helper Qiling Zhu
2026-11-02 15:10   ` Qiling Zhu [this message]
2026-11-02 18:32     ` [RFC PATCH v2 2/2] platform/x86: thinkpad_acpi: Add support for auto keyboard backlight Rong Zhang
2026-11-02 18:13   ` [RFC PATCH v2 0/2] " Rong Zhang

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=20261102151211.40701-3-kmatzkaczor244@gmail.com \
    --to=kmatzkaczor244@gmail.com \
    --cc=derekjohn.clark@gmail.com \
    --cc=griffin@kroah.com \
    --cc=hansg@kernel.org \
    --cc=hmh@hmh.eng.br \
    --cc=i@rong.moe \
    --cc=ibm-acpi-devel@lists.sourceforge.net \
    --cc=ilpo.jarvinen@linux.intel.com \
    --cc=lee@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-leds@vger.kernel.org \
    --cc=mpearson-lenovo@squebb.ca \
    --cc=pavel@kernel.org \
    --cc=platform-driver-x86@vger.kernel.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

all inboxes | Powered by JetHome®