Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
Jackie Dong 878 100.00% 2 100.00%
Total 878 2


// SPDX-License-Identifier: GPL-2.0
/*
 *  Lenovo Super Hotkey Utility WMI extras driver for Ideapad laptop
 *
 *  Copyright (C) 2025	Lenovo
 */

#include <linux/cleanup.h>
#include <linux/dev_printk.h>
#include <linux/device.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/wmi.h>

/* Lenovo Super Hotkey WMI GUIDs */
#define LUD_WMI_METHOD_GUID	"CE6C0974-0407-4F50-88BA-4FC3B6559AD8"

/* Lenovo Utility Data WMI method_id */
#define WMI_LUD_GET_SUPPORT 1
#define WMI_LUD_SET_FEATURE 2

#define WMI_LUD_GET_MICMUTE_LED_VER   20
#define WMI_LUD_GET_AUDIOMUTE_LED_VER 26

#define WMI_LUD_SUPPORT_MICMUTE_LED_VER   25
#define WMI_LUD_SUPPORT_AUDIOMUTE_LED_VER 27

/* Input parameters to mute/unmute audio LED and Mic LED */
struct wmi_led_args {
	u8 id;
	u8 subid;
	u16 value;
};

/* Values of input parameters to SetFeature of audio LED and Mic LED */
enum hotkey_set_feature {
	MIC_MUTE_LED_ON		= 1,
	MIC_MUTE_LED_OFF	= 2,
	AUDIO_MUTE_LED_ON	= 4,
	AUDIO_MUTE_LED_OFF	= 5,
};

#define LSH_ACPI_LED_MAX 2

struct lenovo_super_hotkey_wmi_private {
	struct led_classdev cdev[LSH_ACPI_LED_MAX];
	struct wmi_device *led_wdev;
};

enum mute_led_type {
	MIC_MUTE,
	AUDIO_MUTE,
};

static int lsh_wmi_mute_led_set(enum mute_led_type led_type, struct led_classdev *led_cdev,
				enum led_brightness brightness)

{
	struct lenovo_super_hotkey_wmi_private *wpriv = container_of(led_cdev,
			struct lenovo_super_hotkey_wmi_private, cdev[led_type]);
	struct wmi_led_args led_arg = {0, 0, 0};
	struct acpi_buffer input;
	acpi_status status;

	switch (led_type) {
	case MIC_MUTE:
		led_arg.id = brightness == LED_ON ? MIC_MUTE_LED_ON : MIC_MUTE_LED_OFF;
		break;
	case AUDIO_MUTE:
		led_arg.id = brightness == LED_ON ? AUDIO_MUTE_LED_ON : AUDIO_MUTE_LED_OFF;
		break;
	default:
		return -EINVAL;
	}

	input.length = sizeof(led_arg);
	input.pointer = &led_arg;
	status = wmidev_evaluate_method(wpriv->led_wdev, 0, WMI_LUD_SET_FEATURE, &input, NULL);
	if (ACPI_FAILURE(status))
		return -EIO;

	return 0;
}

static int lsh_wmi_audiomute_led_set(struct led_classdev *led_cdev,
				     enum led_brightness brightness)

{
	return lsh_wmi_mute_led_set(AUDIO_MUTE, led_cdev, brightness);
}

static int lsh_wmi_micmute_led_set(struct led_classdev *led_cdev,
				   enum led_brightness brightness)
{
	return lsh_wmi_mute_led_set(MIC_MUTE, led_cdev, brightness);
}

static int lenovo_super_hotkey_wmi_led_init(enum mute_led_type led_type, struct device *dev)
{
	struct lenovo_super_hotkey_wmi_private *wpriv = dev_get_drvdata(dev);
	struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
	struct acpi_buffer input;
	int led_version, err = 0;
	unsigned int wmiarg;
	acpi_status status;

	switch (led_type) {
	case MIC_MUTE:
		wmiarg = WMI_LUD_GET_MICMUTE_LED_VER;
		break;
	case AUDIO_MUTE:
		wmiarg = WMI_LUD_GET_AUDIOMUTE_LED_VER;
		break;
	default:
		return -EINVAL;
	}

	input.length = sizeof(wmiarg);
	input.pointer = &wmiarg;
	status = wmidev_evaluate_method(wpriv->led_wdev, 0, WMI_LUD_GET_SUPPORT, &input, &output);
	if (ACPI_FAILURE(status))
		return -EIO;

	union acpi_object *obj __free(kfree) = output.pointer;
	if (!obj || obj->type != ACPI_TYPE_INTEGER)
		return -EIO;

	led_version = obj->integer.value;

	/*
	 * Output parameters define: 0 means mute LED is not supported, Non-zero means
	 * mute LED can be supported.
	 */
	if (led_version == 0)
		return 0;


	switch (led_type) {
	case MIC_MUTE:
		if (led_version != WMI_LUD_SUPPORT_MICMUTE_LED_VER) {
			pr_warn("The MIC_MUTE LED of this device isn't supported.\n");
			return 0;
		}

		wpriv->cdev[led_type].name = "platform::micmute";
		wpriv->cdev[led_type].brightness_set_blocking = &lsh_wmi_micmute_led_set;
		wpriv->cdev[led_type].default_trigger = "audio-micmute";
		break;
	case AUDIO_MUTE:
		if (led_version != WMI_LUD_SUPPORT_AUDIOMUTE_LED_VER) {
			pr_warn("The AUDIO_MUTE LED of this device isn't supported.\n");
			return 0;
		}

		wpriv->cdev[led_type].name = "platform::mute";
		wpriv->cdev[led_type].brightness_set_blocking = &lsh_wmi_audiomute_led_set;
		wpriv->cdev[led_type].default_trigger = "audio-mute";
		break;
	default:
		dev_err(dev, "Unknown LED type %d\n", led_type);
		return -EINVAL;
	}

	wpriv->cdev[led_type].max_brightness = LED_ON;
	wpriv->cdev[led_type].flags = LED_CORE_SUSPENDRESUME;

	err = devm_led_classdev_register(dev, &wpriv->cdev[led_type]);
	if (err < 0) {
		dev_err(dev, "Could not register mute LED %d : %d\n", led_type, err);
		return err;
	}
	return 0;
}

static int lenovo_super_hotkey_wmi_leds_setup(struct device *dev)
{
	int err;

	err = lenovo_super_hotkey_wmi_led_init(MIC_MUTE, dev);
	if (err)
		return err;

	err = lenovo_super_hotkey_wmi_led_init(AUDIO_MUTE, dev);
	if (err)
		return err;

	return 0;
}

static int lenovo_super_hotkey_wmi_probe(struct wmi_device *wdev, const void *context)
{
	struct lenovo_super_hotkey_wmi_private *wpriv;

	wpriv = devm_kzalloc(&wdev->dev, sizeof(*wpriv), GFP_KERNEL);
	if (!wpriv)
		return -ENOMEM;

	dev_set_drvdata(&wdev->dev, wpriv);
	wpriv->led_wdev = wdev;
	return lenovo_super_hotkey_wmi_leds_setup(&wdev->dev);
}

static const struct wmi_device_id lenovo_super_hotkey_wmi_id_table[] = {
	{ LUD_WMI_METHOD_GUID, NULL }, /* Utility data */
	{ }
};

MODULE_DEVICE_TABLE(wmi, lenovo_super_hotkey_wmi_id_table);

static struct wmi_driver lenovo_wmi_hotkey_utilities_driver = {
	 .driver = {
		 .name = "lenovo_wmi_hotkey_utilities",
		 .probe_type = PROBE_PREFER_ASYNCHRONOUS
	 },
	 .id_table = lenovo_super_hotkey_wmi_id_table,
	 .probe = lenovo_super_hotkey_wmi_probe,
	 .no_singleton = true,
};

module_wmi_driver(lenovo_wmi_hotkey_utilities_driver);

MODULE_AUTHOR("Jackie Dong <dongeg1@lenovo.com>");
MODULE_DESCRIPTION("Lenovo Super Hotkey Utility WMI extras driver");
MODULE_LICENSE("GPL");