1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * lenovo-ymc.c - Lenovo Yoga Mode Control driver |
4 | * |
5 | * Copyright © 2022 Gergo Koteles <soyer@irl.hu> |
6 | */ |
7 | |
8 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
9 | |
10 | #include <linux/acpi.h> |
11 | #include <linux/dmi.h> |
12 | #include <linux/input.h> |
13 | #include <linux/input/sparse-keymap.h> |
14 | #include <linux/wmi.h> |
15 | #include "ideapad-laptop.h" |
16 | |
17 | #define LENOVO_YMC_EVENT_GUID "06129D99-6083-4164-81AD-F092F9D773A6" |
18 | #define LENOVO_YMC_QUERY_GUID "09B0EE6E-C3FD-4243-8DA1-7911FF80BB8C" |
19 | |
20 | #define LENOVO_YMC_QUERY_INSTANCE 0 |
21 | #define LENOVO_YMC_QUERY_METHOD 0x01 |
22 | |
23 | static bool ec_trigger __read_mostly; |
24 | module_param(ec_trigger, bool, 0444); |
25 | MODULE_PARM_DESC(ec_trigger, "Enable EC triggering work-around to force emitting tablet mode events" ); |
26 | |
27 | static bool force; |
28 | module_param(force, bool, 0444); |
29 | MODULE_PARM_DESC(force, "Force loading on boards without a convertible DMI chassis-type" ); |
30 | |
31 | static const struct dmi_system_id ec_trigger_quirk_dmi_table[] = { |
32 | { |
33 | /* Lenovo Yoga 7 14ARB7 */ |
34 | .matches = { |
35 | DMI_MATCH(DMI_SYS_VENDOR, "LENOVO" ), |
36 | DMI_MATCH(DMI_PRODUCT_NAME, "82QF" ), |
37 | }, |
38 | }, |
39 | { |
40 | /* Lenovo Yoga 7 14ACN6 */ |
41 | .matches = { |
42 | DMI_MATCH(DMI_SYS_VENDOR, "LENOVO" ), |
43 | DMI_MATCH(DMI_PRODUCT_NAME, "82N7" ), |
44 | }, |
45 | }, |
46 | { } |
47 | }; |
48 | |
49 | static const struct dmi_system_id allowed_chasis_types_dmi_table[] = { |
50 | { |
51 | .matches = { |
52 | DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "31" /* Convertible */), |
53 | }, |
54 | }, |
55 | { |
56 | .matches = { |
57 | DMI_EXACT_MATCH(DMI_CHASSIS_TYPE, "32" /* Detachable */), |
58 | }, |
59 | }, |
60 | { } |
61 | }; |
62 | |
63 | struct lenovo_ymc_private { |
64 | struct input_dev *input_dev; |
65 | struct acpi_device *ec_acpi_dev; |
66 | }; |
67 | |
68 | static void lenovo_ymc_trigger_ec(struct wmi_device *wdev, struct lenovo_ymc_private *priv) |
69 | { |
70 | int err; |
71 | |
72 | if (!priv->ec_acpi_dev) |
73 | return; |
74 | |
75 | err = write_ec_cmd(handle: priv->ec_acpi_dev->handle, cmd: VPCCMD_W_YMC, data: 1); |
76 | if (err) |
77 | dev_warn(&wdev->dev, "Could not write YMC: %d\n" , err); |
78 | } |
79 | |
80 | static const struct key_entry lenovo_ymc_keymap[] = { |
81 | /* Laptop */ |
82 | { KE_SW, 0x01, { .sw = { SW_TABLET_MODE, 0 } } }, |
83 | /* Tablet */ |
84 | { KE_SW, 0x02, { .sw = { SW_TABLET_MODE, 1 } } }, |
85 | /* Drawing Board */ |
86 | { KE_SW, 0x03, { .sw = { SW_TABLET_MODE, 1 } } }, |
87 | /* Tent */ |
88 | { KE_SW, 0x04, { .sw = { SW_TABLET_MODE, 1 } } }, |
89 | { KE_END }, |
90 | }; |
91 | |
92 | static void lenovo_ymc_notify(struct wmi_device *wdev, union acpi_object *data) |
93 | { |
94 | struct lenovo_ymc_private *priv = dev_get_drvdata(dev: &wdev->dev); |
95 | u32 input_val = 0; |
96 | struct acpi_buffer input = { sizeof(input_val), &input_val }; |
97 | struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; |
98 | union acpi_object *obj; |
99 | acpi_status status; |
100 | int code; |
101 | |
102 | status = wmi_evaluate_method(LENOVO_YMC_QUERY_GUID, |
103 | LENOVO_YMC_QUERY_INSTANCE, |
104 | LENOVO_YMC_QUERY_METHOD, |
105 | in: &input, out: &output); |
106 | |
107 | if (ACPI_FAILURE(status)) { |
108 | dev_warn(&wdev->dev, |
109 | "Failed to evaluate query method: %s\n" , |
110 | acpi_format_exception(status)); |
111 | return; |
112 | } |
113 | |
114 | obj = output.pointer; |
115 | |
116 | if (obj->type != ACPI_TYPE_INTEGER) { |
117 | dev_warn(&wdev->dev, |
118 | "WMI event data is not an integer\n" ); |
119 | goto free_obj; |
120 | } |
121 | code = obj->integer.value; |
122 | |
123 | if (!sparse_keymap_report_event(dev: priv->input_dev, code, value: 1, autorelease: true)) |
124 | dev_warn(&wdev->dev, "Unknown key %d pressed\n" , code); |
125 | |
126 | free_obj: |
127 | kfree(objp: obj); |
128 | lenovo_ymc_trigger_ec(wdev, priv); |
129 | } |
130 | |
131 | static void acpi_dev_put_helper(void *p) { acpi_dev_put(adev: p); } |
132 | |
133 | static int lenovo_ymc_probe(struct wmi_device *wdev, const void *ctx) |
134 | { |
135 | struct lenovo_ymc_private *priv; |
136 | struct input_dev *input_dev; |
137 | int err; |
138 | |
139 | if (!dmi_check_system(list: allowed_chasis_types_dmi_table)) { |
140 | if (force) |
141 | dev_info(&wdev->dev, "Force loading Lenovo YMC support\n" ); |
142 | else |
143 | return -ENODEV; |
144 | } |
145 | |
146 | ec_trigger |= dmi_check_system(list: ec_trigger_quirk_dmi_table); |
147 | |
148 | priv = devm_kzalloc(dev: &wdev->dev, size: sizeof(*priv), GFP_KERNEL); |
149 | if (!priv) |
150 | return -ENOMEM; |
151 | |
152 | if (ec_trigger) { |
153 | pr_debug("Lenovo YMC enable EC triggering.\n" ); |
154 | priv->ec_acpi_dev = acpi_dev_get_first_match_dev(hid: "VPC2004" , NULL, hrv: -1); |
155 | |
156 | if (!priv->ec_acpi_dev) { |
157 | dev_err(&wdev->dev, "Could not find EC ACPI device.\n" ); |
158 | return -ENODEV; |
159 | } |
160 | err = devm_add_action_or_reset(&wdev->dev, |
161 | acpi_dev_put_helper, priv->ec_acpi_dev); |
162 | if (err) { |
163 | dev_err(&wdev->dev, |
164 | "Could not clean up EC ACPI device: %d\n" , err); |
165 | return err; |
166 | } |
167 | } |
168 | |
169 | input_dev = devm_input_allocate_device(&wdev->dev); |
170 | if (!input_dev) |
171 | return -ENOMEM; |
172 | |
173 | input_dev->name = "Lenovo Yoga Tablet Mode Control switch" ; |
174 | input_dev->phys = LENOVO_YMC_EVENT_GUID "/input0" ; |
175 | input_dev->id.bustype = BUS_HOST; |
176 | input_dev->dev.parent = &wdev->dev; |
177 | err = sparse_keymap_setup(dev: input_dev, keymap: lenovo_ymc_keymap, NULL); |
178 | if (err) { |
179 | dev_err(&wdev->dev, |
180 | "Could not set up input device keymap: %d\n" , err); |
181 | return err; |
182 | } |
183 | |
184 | err = input_register_device(input_dev); |
185 | if (err) { |
186 | dev_err(&wdev->dev, |
187 | "Could not register input device: %d\n" , err); |
188 | return err; |
189 | } |
190 | |
191 | priv->input_dev = input_dev; |
192 | dev_set_drvdata(dev: &wdev->dev, data: priv); |
193 | |
194 | /* Report the state for the first time on probe */ |
195 | lenovo_ymc_trigger_ec(wdev, priv); |
196 | lenovo_ymc_notify(wdev, NULL); |
197 | return 0; |
198 | } |
199 | |
200 | static const struct wmi_device_id lenovo_ymc_wmi_id_table[] = { |
201 | { .guid_string = LENOVO_YMC_EVENT_GUID }, |
202 | { } |
203 | }; |
204 | MODULE_DEVICE_TABLE(wmi, lenovo_ymc_wmi_id_table); |
205 | |
206 | static struct wmi_driver lenovo_ymc_driver = { |
207 | .driver = { |
208 | .name = "lenovo-ymc" , |
209 | }, |
210 | .id_table = lenovo_ymc_wmi_id_table, |
211 | .probe = lenovo_ymc_probe, |
212 | .notify = lenovo_ymc_notify, |
213 | }; |
214 | |
215 | module_wmi_driver(lenovo_ymc_driver); |
216 | |
217 | MODULE_AUTHOR("Gergo Koteles <soyer@irl.hu>" ); |
218 | MODULE_DESCRIPTION("Lenovo Yoga Mode Control driver" ); |
219 | MODULE_LICENSE("GPL" ); |
220 | |