mirror_ubuntu-kernels/drivers/platform/x86/lenovo-ymc.c
Hans de Goede 2b6aa6610d platform/x86: lenovo-ymc: Only bind on machines with a convertible DMI chassis-type
The lenovo-ymc driver is causing the keyboard + touchpad to stop working
on some regular laptop models such as the Lenovo ThinkBook 13s G2 ITL 20V9.

The problem is that there are YMC WMI GUID methods in the ACPI tables
of these laptops, despite them not being Yogas and lenovo-ymc loading
causes libinput to see a SW_TABLET_MODE switch with state 1.

This in turn causes libinput to ignore events from the builtin keyboard
and touchpad, since it filters those out for a Yoga in tablet mode.

Similar issues with false-positive SW_TABLET_MODE=1 reporting have
been seen with the intel-hid driver.

Copy the intel-hid driver approach to fix this and only bind to the WMI
device on machines where the DMI chassis-type indicates the machine
is a convertible.

Add a 'force' module parameter to allow overriding the chassis-type check
so that users can easily test if the YMC interface works on models which
report an unexpected chassis-type.

Fixes: e82882cdd2 ("platform/x86: Add driver for Yoga Tablet Mode switch")
Link: https://bugzilla.redhat.com/show_bug.cgi?id=2229373
Cc: André Apitzsch <git@apitzsch.eu>
Cc: stable@vger.kernel.org
Tested-by: Andrew Kallmeyer <kallmeyeras@gmail.com>
Tested-by: Gergő Köteles <soyer@irl.hu>
Signed-off-by: Hans de Goede <hdegoede@redhat.com>
Link: https://lore.kernel.org/r/20230812144818.383230-1-hdegoede@redhat.com
2023-08-13 14:50:25 +02:00

213 lines
5.2 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* lenovo-ymc.c - Lenovo Yoga Mode Control driver
*
* Copyright © 2022 Gergo Koteles <soyer@irl.hu>
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/acpi.h>
#include <linux/dmi.h>
#include <linux/input.h>
#include <linux/input/sparse-keymap.h>
#include <linux/wmi.h>
#include "ideapad-laptop.h"
#define LENOVO_YMC_EVENT_GUID "06129D99-6083-4164-81AD-F092F9D773A6"
#define LENOVO_YMC_QUERY_GUID "09B0EE6E-C3FD-4243-8DA1-7911FF80BB8C"
#define LENOVO_YMC_QUERY_INSTANCE 0
#define LENOVO_YMC_QUERY_METHOD 0x01
static bool ec_trigger __read_mostly;
module_param(ec_trigger, bool, 0444);
MODULE_PARM_DESC(ec_trigger, "Enable EC triggering work-around to force emitting tablet mode events");
static bool force;
module_param(force, bool, 0444);
MODULE_PARM_DESC(force, "Force loading on boards without a convertible DMI chassis-type");
static const struct dmi_system_id ec_trigger_quirk_dmi_table[] = {
{
/* Lenovo Yoga 7 14ARB7 */
.matches = {
DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
DMI_MATCH(DMI_PRODUCT_NAME, "82QF"),
},
},
{ }
};
static const struct dmi_system_id allowed_chasis_types_dmi_table[] = {
{
.matches = {
DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "31" /* Convertible */),
},
},
{
.matches = {
DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "32" /* Detachable */),
},
},
{ }
};
struct lenovo_ymc_private {
struct input_dev *input_dev;
struct acpi_device *ec_acpi_dev;
};
static void lenovo_ymc_trigger_ec(struct wmi_device *wdev, struct lenovo_ymc_private *priv)
{
int err;
if (!priv->ec_acpi_dev)
return;
err = write_ec_cmd(priv->ec_acpi_dev->handle, VPCCMD_W_YMC, 1);
if (err)
dev_warn(&wdev->dev, "Could not write YMC: %d\n", err);
}
static const struct key_entry lenovo_ymc_keymap[] = {
/* Laptop */
{ KE_SW, 0x01, { .sw = { SW_TABLET_MODE, 0 } } },
/* Tablet */
{ KE_SW, 0x02, { .sw = { SW_TABLET_MODE, 1 } } },
/* Drawing Board */
{ KE_SW, 0x03, { .sw = { SW_TABLET_MODE, 1 } } },
/* Tent */
{ KE_SW, 0x04, { .sw = { SW_TABLET_MODE, 1 } } },
{ KE_END },
};
static void lenovo_ymc_notify(struct wmi_device *wdev, union acpi_object *data)
{
struct lenovo_ymc_private *priv = dev_get_drvdata(&wdev->dev);
u32 input_val = 0;
struct acpi_buffer input = { sizeof(input_val), &input_val };
struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
union acpi_object *obj;
acpi_status status;
int code;
status = wmi_evaluate_method(LENOVO_YMC_QUERY_GUID,
LENOVO_YMC_QUERY_INSTANCE,
LENOVO_YMC_QUERY_METHOD,
&input, &output);
if (ACPI_FAILURE(status)) {
dev_warn(&wdev->dev,
"Failed to evaluate query method: %s\n",
acpi_format_exception(status));
return;
}
obj = output.pointer;
if (obj->type != ACPI_TYPE_INTEGER) {
dev_warn(&wdev->dev,
"WMI event data is not an integer\n");
goto free_obj;
}
code = obj->integer.value;
if (!sparse_keymap_report_event(priv->input_dev, code, 1, true))
dev_warn(&wdev->dev, "Unknown key %d pressed\n", code);
free_obj:
kfree(obj);
lenovo_ymc_trigger_ec(wdev, priv);
}
static void acpi_dev_put_helper(void *p) { acpi_dev_put(p); }
static int lenovo_ymc_probe(struct wmi_device *wdev, const void *ctx)
{
struct lenovo_ymc_private *priv;
struct input_dev *input_dev;
int err;
if (!dmi_check_system(allowed_chasis_types_dmi_table)) {
if (force)
dev_info(&wdev->dev, "Force loading Lenovo YMC support\n");
else
return -ENODEV;
}
ec_trigger |= dmi_check_system(ec_trigger_quirk_dmi_table);
priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
if (ec_trigger) {
pr_debug("Lenovo YMC enable EC triggering.\n");
priv->ec_acpi_dev = acpi_dev_get_first_match_dev("VPC2004", NULL, -1);
if (!priv->ec_acpi_dev) {
dev_err(&wdev->dev, "Could not find EC ACPI device.\n");
return -ENODEV;
}
err = devm_add_action_or_reset(&wdev->dev,
acpi_dev_put_helper, priv->ec_acpi_dev);
if (err) {
dev_err(&wdev->dev,
"Could not clean up EC ACPI device: %d\n", err);
return err;
}
}
input_dev = devm_input_allocate_device(&wdev->dev);
if (!input_dev)
return -ENOMEM;
input_dev->name = "Lenovo Yoga Tablet Mode Control switch";
input_dev->phys = LENOVO_YMC_EVENT_GUID "/input0";
input_dev->id.bustype = BUS_HOST;
input_dev->dev.parent = &wdev->dev;
err = sparse_keymap_setup(input_dev, lenovo_ymc_keymap, NULL);
if (err) {
dev_err(&wdev->dev,
"Could not set up input device keymap: %d\n", err);
return err;
}
err = input_register_device(input_dev);
if (err) {
dev_err(&wdev->dev,
"Could not register input device: %d\n", err);
return err;
}
priv->input_dev = input_dev;
dev_set_drvdata(&wdev->dev, priv);
/* Report the state for the first time on probe */
lenovo_ymc_trigger_ec(wdev, priv);
lenovo_ymc_notify(wdev, NULL);
return 0;
}
static const struct wmi_device_id lenovo_ymc_wmi_id_table[] = {
{ .guid_string = LENOVO_YMC_EVENT_GUID },
{ }
};
MODULE_DEVICE_TABLE(wmi, lenovo_ymc_wmi_id_table);
static struct wmi_driver lenovo_ymc_driver = {
.driver = {
.name = "lenovo-ymc",
},
.id_table = lenovo_ymc_wmi_id_table,
.probe = lenovo_ymc_probe,
.notify = lenovo_ymc_notify,
};
module_wmi_driver(lenovo_ymc_driver);
MODULE_AUTHOR("Gergo Koteles <soyer@irl.hu>");
MODULE_DESCRIPTION("Lenovo Yoga Mode Control driver");
MODULE_LICENSE("GPL");