Skip to content

Commit

Permalink
platform/x86: ideapad-laptop: support for more special keys in WMI
Browse files Browse the repository at this point in the history
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: Philipp Jungkamp <p.jungkamp@gmx.net>
Link: https://lore.kernel.org/r/20221116110647.3438-1-p.jungkamp@gmx.net
Reviewed-by: Hans de Goede <hdegoede@redhat.com>
Signed-off-by: Hans de Goede <hdegoede@redhat.com>
  • Loading branch information
Philipp Jungkamp authored and Hans de Goede committed Nov 16, 2022
1 parent be5dd7d commit f32e024
Showing 1 changed file with 202 additions and 59 deletions.
261 changes: 202 additions & 59 deletions drivers/platform/x86/ideapad-laptop.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,14 @@
#include <linux/seq_file.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/wmi.h>

#include <acpi/video.h>

#include <dt-bindings/leds/common.h>

#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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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 } },
Expand All @@ -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 },
};

Expand Down Expand Up @@ -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[] = {
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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");
Expand Down

0 comments on commit f32e024

Please sign in to comment.