Commit f32e0241 authored by Philipp Jungkamp's avatar Philipp Jungkamp Committed by Hans de Goede
Browse files

platform/x86: ideapad-laptop: support for more special keys in WMI



The event data of the WMI event 0xD0, which is assumed to be the
fn_lock, is used to indicate several special keys on newer Yoga 7/9
laptops.

The notify_id 0xD0 is non-unique in the DSDT of the Yoga 9 14IAP7, this
causes wmi_get_event_data() to report wrong values.
Port the ideapad-laptop WMI code to the wmi bus infrastructure which
does not suffer from the shortcomings of wmi_get_event_data().

Signed-off-by: default avatarPhilipp Jungkamp <p.jungkamp@gmx.net>
Link: https://lore.kernel.org/r/20221116110647.3438-1-p.jungkamp@gmx.net


Reviewed-by: default avatarHans de Goede <hdegoede@redhat.com>
Signed-off-by: default avatarHans de Goede <hdegoede@redhat.com>
parent be5dd7d8
Loading
Loading
Loading
Loading
+202 −59
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@
#include <linux/seq_file.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/wmi.h>

#include <acpi/video.h>

@@ -37,14 +38,6 @@

#define IDEAPAD_RFKILL_DEV_NUM	3

#if IS_ENABLED(CONFIG_ACPI_WMI)
static const char *const ideapad_wmi_fnesc_events[] = {
	"26CAB2E5-5CF1-46AE-AAC3-4A12B6BA50E6", /* Yoga 3 */
	"56322276-8493-4CE8-A783-98C991274F5E", /* Yoga 700 */
	"8FC0DE0C-B4E4-43FD-B0F3-8871711C1294", /* Legion 5 */
};
#endif

enum {
	CFG_CAP_BT_BIT       = 16,
	CFG_CAP_3G_BIT       = 17,
@@ -141,7 +134,6 @@ struct ideapad_private {
	struct ideapad_dytc_priv *dytc;
	struct dentry *debug;
	unsigned long cfg;
	const char *fnesc_guid;
	struct {
		bool conservation_mode    : 1;
		bool dytc                 : 1;
@@ -182,6 +174,42 @@ MODULE_PARM_DESC(set_fn_lock_led,
	"Enable driver based updates of the fn-lock LED on fn-lock changes. "
	"If you need this please report this to: platform-driver-x86@vger.kernel.org");

/*
 * shared data
 */

static struct ideapad_private *ideapad_shared;
static DEFINE_MUTEX(ideapad_shared_mutex);

static int ideapad_shared_init(struct ideapad_private *priv)
{
	int ret;

	mutex_lock(&ideapad_shared_mutex);

	if (!ideapad_shared) {
		ideapad_shared = priv;
		ret = 0;
	} else {
		dev_warn(&priv->adev->dev, "found multiple platform devices\n");
		ret = -EINVAL;
	}

	mutex_unlock(&ideapad_shared_mutex);

	return ret;
}

static void ideapad_shared_exit(struct ideapad_private *priv)
{
	mutex_lock(&ideapad_shared_mutex);

	if (ideapad_shared == priv)
		ideapad_shared = NULL;

	mutex_unlock(&ideapad_shared_mutex);
}

/*
 * ACPI Helpers
 */
@@ -1110,6 +1138,8 @@ static void ideapad_sysfs_exit(struct ideapad_private *priv)
/*
 * input device
 */
#define IDEAPAD_WMI_KEY 0x100

static const struct key_entry ideapad_keymap[] = {
	{ KE_KEY,   6, { KEY_SWITCHVIDEOMODE } },
	{ KE_KEY,   7, { KEY_CAMERA } },
@@ -1123,6 +1153,28 @@ static const struct key_entry ideapad_keymap[] = {
	{ KE_KEY,  66, { KEY_TOUCHPAD_OFF } },
	{ KE_KEY,  67, { KEY_TOUCHPAD_ON } },
	{ KE_KEY, 128, { KEY_ESC } },

	/*
	 * WMI keys
	 */

	/* FnLock (handled by the firmware) */
	{ KE_IGNORE,	0x02 | IDEAPAD_WMI_KEY },
	/* Esc (handled by the firmware) */
	{ KE_IGNORE,	0x03 | IDEAPAD_WMI_KEY },
	/* Customizable Lenovo Hotkey ("star" with 'S' inside) */
	{ KE_KEY,	0x01 | IDEAPAD_WMI_KEY, { KEY_FAVORITES } },
	/* Dark mode toggle */
	{ KE_KEY,	0x13 | IDEAPAD_WMI_KEY, { KEY_PROG1 } },
	/* Sound profile switch */
	{ KE_KEY,	0x12 | IDEAPAD_WMI_KEY, { KEY_PROG2 } },
	/* Lenovo Virtual Background application */
	{ KE_KEY,	0x28 | IDEAPAD_WMI_KEY, { KEY_PROG3 } },
	/* Lenovo Support */
	{ KE_KEY,	0x27 | IDEAPAD_WMI_KEY, { KEY_HELP } },
	/* Refresh Rate Toggle */
	{ KE_KEY,	0x0a | IDEAPAD_WMI_KEY, { KEY_DISPLAYTOGGLE } },

	{ KE_END },
};

@@ -1526,33 +1578,6 @@ static void ideapad_acpi_notify(acpi_handle handle, u32 event, void *data)
	}
}

#if IS_ENABLED(CONFIG_ACPI_WMI)
static void ideapad_wmi_notify(u32 value, void *context)
{
	struct ideapad_private *priv = context;
	unsigned long result;

	switch (value) {
	case 128:
		ideapad_input_report(priv, value);
		break;
	case 208:
		if (!priv->features.set_fn_lock_led)
			break;

		if (!eval_hals(priv->adev->handle, &result)) {
			bool state = test_bit(HALS_FNLOCK_STATE_BIT, &result);

			exec_sals(priv->adev->handle, state ? SALS_FNLOCK_ON : SALS_FNLOCK_OFF);
		}
		break;
	default:
		dev_info(&priv->platform_device->dev,
			 "Unknown WMI event: %u\n", value);
	}
}
#endif

/* On some models we need to call exec_sals(SALS_FNLOCK_ON/OFF) to set the LED */
static const struct dmi_system_id set_fn_lock_led_list[] = {
	{
@@ -1643,6 +1668,118 @@ static void ideapad_check_features(struct ideapad_private *priv)
	}
}

#if IS_ENABLED(CONFIG_ACPI_WMI)
/*
 * WMI driver
 */
enum ideapad_wmi_event_type {
	IDEAPAD_WMI_EVENT_ESC,
	IDEAPAD_WMI_EVENT_FN_KEYS,
};

struct ideapad_wmi_private {
	enum ideapad_wmi_event_type event;
};

static int ideapad_wmi_probe(struct wmi_device *wdev, const void *context)
{
	struct ideapad_wmi_private *wpriv;

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

	*wpriv = *(const struct ideapad_wmi_private *)context;

	dev_set_drvdata(&wdev->dev, wpriv);
	return 0;
}

static void ideapad_wmi_notify(struct wmi_device *wdev, union acpi_object *data)
{
	struct ideapad_wmi_private *wpriv = dev_get_drvdata(&wdev->dev);
	struct ideapad_private *priv;
	unsigned long result;

	mutex_lock(&ideapad_shared_mutex);

	priv = ideapad_shared;
	if (!priv)
		goto unlock;

	switch (wpriv->event) {
	case IDEAPAD_WMI_EVENT_ESC:
		ideapad_input_report(priv, 128);
		break;
	case IDEAPAD_WMI_EVENT_FN_KEYS:
		if (priv->features.set_fn_lock_led &&
		    !eval_hals(priv->adev->handle, &result)) {
			bool state = test_bit(HALS_FNLOCK_STATE_BIT, &result);

			exec_sals(priv->adev->handle, state ? SALS_FNLOCK_ON : SALS_FNLOCK_OFF);
		}

		if (data->type != ACPI_TYPE_INTEGER) {
			dev_warn(&wdev->dev,
				 "WMI event data is not an integer\n");
			break;
		}

		dev_dbg(&wdev->dev, "WMI fn-key event: 0x%llx\n",
			data->integer.value);

		ideapad_input_report(priv,
				     data->integer.value | IDEAPAD_WMI_KEY);

		break;
	}
unlock:
	mutex_unlock(&ideapad_shared_mutex);
}

static const struct ideapad_wmi_private ideapad_wmi_context_esc = {
	.event = IDEAPAD_WMI_EVENT_ESC
};

static const struct ideapad_wmi_private ideapad_wmi_context_fn_keys = {
	.event = IDEAPAD_WMI_EVENT_FN_KEYS
};

static const struct wmi_device_id ideapad_wmi_ids[] = {
	{ "26CAB2E5-5CF1-46AE-AAC3-4A12B6BA50E6", &ideapad_wmi_context_esc }, /* Yoga 3 */
	{ "56322276-8493-4CE8-A783-98C991274F5E", &ideapad_wmi_context_esc }, /* Yoga 700 */
	{ "8FC0DE0C-B4E4-43FD-B0F3-8871711C1294", &ideapad_wmi_context_fn_keys }, /* Legion 5 */
	{},
};
MODULE_DEVICE_TABLE(wmi, ideapad_wmi_ids);

static struct wmi_driver ideapad_wmi_driver = {
	.driver = {
		.name = "ideapad_wmi",
	},
	.id_table = ideapad_wmi_ids,
	.probe = ideapad_wmi_probe,
	.notify = ideapad_wmi_notify,
};

static int ideapad_wmi_driver_register(void)
{
	return wmi_driver_register(&ideapad_wmi_driver);
}

static void ideapad_wmi_driver_unregister(void)
{
	return wmi_driver_unregister(&ideapad_wmi_driver);
}

#else
static inline int ideapad_wmi_driver_register(void) { return 0; }
static inline void ideapad_wmi_driver_unregister(void) { }
#endif

/*
 * ACPI driver
 */
static int ideapad_acpi_add(struct platform_device *pdev)
{
	struct acpi_device *adev = ACPI_COMPANION(&pdev->dev);
@@ -1724,30 +1861,16 @@ static int ideapad_acpi_add(struct platform_device *pdev)
		goto notification_failed;
	}

#if IS_ENABLED(CONFIG_ACPI_WMI)
	for (i = 0; i < ARRAY_SIZE(ideapad_wmi_fnesc_events); i++) {
		status = wmi_install_notify_handler(ideapad_wmi_fnesc_events[i],
						    ideapad_wmi_notify, priv);
		if (ACPI_SUCCESS(status)) {
			priv->fnesc_guid = ideapad_wmi_fnesc_events[i];
			break;
		}
	}

	if (ACPI_FAILURE(status) && status != AE_NOT_EXIST) {
		err = -EIO;
		goto notification_failed_wmi;
	}
#endif
	err = ideapad_shared_init(priv);
	if (err)
		goto shared_init_failed;

	return 0;

#if IS_ENABLED(CONFIG_ACPI_WMI)
notification_failed_wmi:
shared_init_failed:
	acpi_remove_notify_handler(priv->adev->handle,
				   ACPI_DEVICE_NOTIFY,
				   ideapad_acpi_notify);
#endif

notification_failed:
	ideapad_backlight_exit(priv);
@@ -1773,10 +1896,7 @@ static int ideapad_acpi_remove(struct platform_device *pdev)
	struct ideapad_private *priv = dev_get_drvdata(&pdev->dev);
	int i;

#if IS_ENABLED(CONFIG_ACPI_WMI)
	if (priv->fnesc_guid)
		wmi_remove_notify_handler(priv->fnesc_guid);
#endif
	ideapad_shared_exit(priv);

	acpi_remove_notify_handler(priv->adev->handle,
				   ACPI_DEVICE_NOTIFY,
@@ -1828,7 +1948,30 @@ static struct platform_driver ideapad_acpi_driver = {
	},
};

module_platform_driver(ideapad_acpi_driver);
static int __init ideapad_laptop_init(void)
{
	int err;

	err = ideapad_wmi_driver_register();
	if (err)
		return err;

	err = platform_driver_register(&ideapad_acpi_driver);
	if (err) {
		ideapad_wmi_driver_unregister();
		return err;
	}

	return 0;
}
module_init(ideapad_laptop_init)

static void __exit ideapad_laptop_exit(void)
{
	ideapad_wmi_driver_unregister();
	platform_driver_unregister(&ideapad_acpi_driver);
}
module_exit(ideapad_laptop_exit)

MODULE_AUTHOR("David Woodhouse <dwmw2@infradead.org>");
MODULE_DESCRIPTION("IdeaPad ACPI Extras");