[PATCH] platform/x86: wireless-hotkey: Drive the HP airplane mode LED

From: ahmad baghereslami

Date: Fri Oct 30 2026 - 01:19:15 EST


HP laptops with the HPQ6001 wireless button have an airplane mode LED
next to it (white with radios on, amber in airplane mode) that stays in
whatever state the firmware left it on Linux. The HPQ6001 device has a
WLED method for it, and HP's Windows Wireless Button Driver (2.1.9.1)
calls it from the Wireless Radio LED output report (Generic Desktop
usage 0xc7) of the radio management HID collection it exposes:
WLED(1) with radios on, WLED(0) in airplane mode.

When the method exists, register a platform::wlan LED that calls it,
with rfkill-any as default trigger, which follows the same state.
Restore the LED on resume.

Tested on an HP ZBook 17 G3 (board 80D6) with Ubuntu's 7.0.0 kernel:
WLED(1) and WLED(0) switch the LED white and amber, the firmware does not
change it by itself when the button is pressed, and with this patch the
LED follows airplane mode toggled with the button and with rfkill.

Reading HP's Windows driver (Ghidra) and the ACPI tables, and writing
this patch, were done with the help of an AI coding assistant (Claude);
I tested the LED on the machine.

Signed-off-by: ahmad baghereslami <ahmad.b1995@xxxxxxxxx>
---
drivers/platform/x86/Kconfig | 1 +
drivers/platform/x86/wireless-hotkey.c | 65 +++++++++++++++++++++++++-
2 files changed, 65 insertions(+), 1 deletion(-)

diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
index 7ee9ff387..3bc09a694 100644
--- a/drivers/platform/x86/Kconfig
+++ b/drivers/platform/x86/Kconfig
@@ -458,6 +458,7 @@ config WIRELESS_HOTKEY
tristate "Wireless hotkey button"
depends on ACPI
depends on INPUT
+ depends on LEDS_CLASS || !LEDS_CLASS
help
This driver provides supports for the wireless buttons found on some AMD,
HP, & Xiaomi laptops.
diff --git a/drivers/platform/x86/wireless-hotkey.c b/drivers/platform/x86/wireless-hotkey.c
index 3151844d1..cbe92c967 100644
--- a/drivers/platform/x86/wireless-hotkey.c
+++ b/drivers/platform/x86/wireless-hotkey.c
@@ -10,7 +10,9 @@
#include <linux/module.h>
#include <linux/init.h>
#include <linux/input.h>
+#include <linux/leds.h>
#include <linux/platform_device.h>
+#include <linux/pm.h>
#include <linux/acpi.h>
#include <acpi/acpi_bus.h>

@@ -25,6 +27,9 @@ MODULE_ALIAS("acpi*:LGEX0815:*");
struct wl_button {
struct input_dev *input_dev;
char phys[32];
+ acpi_handle handle;
+ struct led_classdev led;
+ bool has_led;
};

static const struct acpi_device_id wl_ids[] = {
@@ -87,6 +92,41 @@ static void wl_notify(acpi_handle handle, u32 event, void *data)
input_sync(button->input_dev);
}

+/*
+ * HP's HPQ6001 device has a WLED method for the airplane mode LED (1: radios
+ * on, 0: airplane mode). HP's Windows driver calls it from the Wireless Radio
+ * LED output report of its radio management HID collection, so drive it from
+ * the rfkill-any trigger, which follows the same state.
+ */
+static int wireless_led_set(struct led_classdev *led, enum led_brightness value)
+{
+ struct wl_button *button = container_of(led, struct wl_button, led);
+ acpi_status status;
+
+ status = acpi_execute_simple_method(button->handle, "WLED", value ? 1 : 0);
+ return ACPI_SUCCESS(status) ? 0 : -EIO;
+}
+
+static int wireless_led_setup(struct device *dev)
+{
+ struct wl_button *button = dev_get_drvdata(dev);
+ int err;
+
+ if (!acpi_has_method(button->handle, "WLED"))
+ return 0;
+
+ button->led.name = "platform::wlan";
+ button->led.max_brightness = 1;
+ button->led.brightness_set_blocking = wireless_led_set;
+ button->led.default_trigger = "rfkill-any";
+ err = led_classdev_register(dev, &button->led);
+ if (err)
+ return err;
+
+ button->has_led = true;
+ return 0;
+}
+
static int wl_probe(struct platform_device *pdev)
{
struct acpi_device *adev;
@@ -102,6 +142,7 @@ static int wl_probe(struct platform_device *pdev)
return -ENOMEM;

platform_set_drvdata(pdev, button);
+ button->handle = adev->handle;

err = wireless_input_setup(&pdev->dev);
if (err) {
@@ -114,24 +155,46 @@ static int wl_probe(struct platform_device *pdev)
if (err) {
pr_err("Failed to install ACPI notify handler\n");
wireless_input_destroy(&pdev->dev);
+ return err;
}

- return err;
+ err = wireless_led_setup(&pdev->dev);
+ if (err)
+ dev_warn(&pdev->dev, "Failed to register the airplane mode LED: %d\n", err);
+
+ return 0;
}

static void wl_remove(struct platform_device *pdev)
{
+ struct wl_button *button = platform_get_drvdata(pdev);
+
+ if (button->has_led)
+ led_classdev_unregister(&button->led);
acpi_dev_remove_notify_handler(ACPI_COMPANION(&pdev->dev),
ACPI_DEVICE_NOTIFY, wl_notify);
wireless_input_destroy(&pdev->dev);
}

+/* The firmware does not keep the LED state across suspend. */
+static int wl_resume(struct device *dev)
+{
+ struct wl_button *button = dev_get_drvdata(dev);
+
+ if (button->has_led)
+ wireless_led_set(&button->led, button->led.brightness);
+ return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(wl_pm_ops, NULL, wl_resume);
+
static struct platform_driver wl_driver = {
.probe = wl_probe,
.remove = wl_remove,
.driver = {
.name = "wireless-hotkey",
.acpi_match_table = wl_ids,
+ .pm = pm_sleep_ptr(&wl_pm_ops),
},
};

--
2.53.0