1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * Lenovo Yoga Book 9 keyboard-dock detection
4 *
5 * The Yoga Book 9 ships with a detachable Bluetooth keyboard that magnetically
6 * attaches to the bottom screen in one of two positions. The EC tracks
7 * attachment state in a 2-bit field called BKBD and signals changes via WMI
8 * event 0xEB on the WM10 ACPI device (_UID "GMZN").
9 *
10 * BKBD values:
11 * 0 = keyboard detached
12 * 1 = keyboard docked on the top half of the bottom screen
13 * 2 = keyboard docked on the bottom half of the bottom screen
14 * 3 = reserved / not observed
15 *
16 * Two WMI interfaces are used (documented in embedded BMOF, WQDD, 20705 bytes):
17 *
18 * LENOVO_BTKBD_EVENT (event GUID, 806BD2A2-...)
19 * WmiDataId(1) uint32 Status — _WED(0xEB) returns EC.BKBD directly.
20 * The notify callback receives BKBD as an integer; no separate query needed.
21 *
22 * LENOVO_FEATURE_STATUS_DATA (block GUID, E7F300FA-...)
23 * WmiDataId(1) uint32 IDs = 0x00060000 (feature selector)
24 * WmiDataId(2) uint32 Status = BKBD value
25 * Used on probe and resume to read initial state.
26 *
27 * The event driver (LENOVO_BTKBD_EVENT) fires a notifier chain on each WMI
28 * event. The block driver (LENOVO_FEATURE_STATUS_DATA) owns the input_dev
29 * and registers a notifier_block to receive those events, eliminating the
30 * need for shared global state or a mutex.
31 *
32 * SW_TABLET_MODE=1 is reported when the keyboard is detached;
33 * SW_TABLET_MODE=0 when docked in either position (keyboard present).
34 * The raw BKBD value is exposed via the sysfs attribute "keyboard_position".
35 *
36 * Copyright (C) 2026 Dave Carey <carvsdriver@gmail.com>
37 */
38
39 #include <linux/acpi.h>
40 #include <linux/cleanup.h>
41 #include <linux/compiler_attributes.h>
42 #include <linux/dev_printk.h>
43 #include <linux/dmi.h>
44 #include <linux/input.h>
45 #include <linux/module.h>
46 #include <linux/notifier.h>
47 #include <linux/pm.h>
48 #include <linux/slab.h>
49 #include <linux/spinlock.h>
50 #include <linux/sysfs.h>
51 #include <linux/types.h>
52 #include <linux/wmi.h>
53
54 #define YB9_KBDOCK_EVENT_GUID "806BD2A2-177B-481D-BFB5-3BA0BB4A2285"
55 #define YB9_KBDOCK_QUERY_GUID "E7F300FA-21CD-4003-ADAC-2696135982E6"
56
57 /* BKBD encoding */
58 #define BKBD_DETACHED 0
59
60 /* LENOVO_FEATURE_STATUS_DATA feature selector */
61 #define YB9_FEATURE_STATUS_ID 0x00060000u
62
63 /*
64 * LENOVO_FEATURE_STATUS_DATA: 8-byte buffer {uint32 IDs, uint32 Status}.
65 * IDs is always 0x00060000; Status holds the BKBD value (0–3).
66 */
67 struct lenovo_feature_status {
68 __le32 id;
69 __le32 status;
70 } __packed;
71
72 /* ------------------------------------------------------------------
73 * Notifier chain — event driver fires it, block driver listens
74 * ------------------------------------------------------------------ */
75
76 static BLOCKING_NOTIFIER_HEAD(yb9_kbdock_chain_head);
77
devm_yb9_kbdock_unregister_notifier(void * data)78 static void devm_yb9_kbdock_unregister_notifier(void *data)
79 {
80 struct notifier_block *nb = data;
81
82 blocking_notifier_chain_unregister(&yb9_kbdock_chain_head, nb);
83 }
84
devm_yb9_kbdock_register_notifier(struct device * dev,struct notifier_block * nb)85 static int devm_yb9_kbdock_register_notifier(struct device *dev,
86 struct notifier_block *nb)
87 {
88 int ret;
89
90 ret = blocking_notifier_chain_register(&yb9_kbdock_chain_head, nb);
91 if (ret < 0)
92 return ret;
93
94 return devm_add_action_or_reset(dev, devm_yb9_kbdock_unregister_notifier, nb);
95 }
96
97 /* ------------------------------------------------------------------
98 * Block WMI driver — LENOVO_FEATURE_STATUS_DATA
99 * (owns input_dev, sysfs, PM resume)
100 * ------------------------------------------------------------------ */
101
102 struct yb9_kbdock_data {
103 struct wmi_device *wdev;
104 struct input_dev *input_dev;
105 struct notifier_block nb;
106 spinlock_t lock; /* protects input_report_switch + input_sync */
107 };
108
yb9_kbdock_query(struct yb9_kbdock_data * d,u32 * bkbd)109 static int yb9_kbdock_query(struct yb9_kbdock_data *d, u32 *bkbd)
110 {
111 struct wmi_buffer out;
112 int ret;
113
114 ret = wmidev_query_block(d->wdev, 0, &out,
115 sizeof(struct lenovo_feature_status));
116 if (ret)
117 return ret;
118
119 struct lenovo_feature_status *fs __free(kfree) = out.data;
120
121 if (le32_to_cpu(fs->id) != YB9_FEATURE_STATUS_ID)
122 return -EIO;
123
124 *bkbd = le32_to_cpu(fs->status);
125 return 0;
126 }
127
yb9_kbdock_report(struct yb9_kbdock_data * d,u32 bkbd)128 static void yb9_kbdock_report(struct yb9_kbdock_data *d, u32 bkbd)
129 {
130 int tablet = (bkbd == BKBD_DETACHED) ? 1 : 0;
131
132 spin_lock(&d->lock);
133 input_report_switch(d->input_dev, SW_TABLET_MODE, tablet);
134 input_sync(d->input_dev);
135 spin_unlock(&d->lock);
136 dev_dbg(&d->wdev->dev, "BKBD=%u SW_TABLET_MODE=%d\n", bkbd, tablet);
137 }
138
yb9_kbdock_sync(struct yb9_kbdock_data * d)139 static int yb9_kbdock_sync(struct yb9_kbdock_data *d)
140 {
141 u32 bkbd;
142 int ret;
143
144 ret = yb9_kbdock_query(d, &bkbd);
145 if (ret)
146 return ret;
147
148 yb9_kbdock_report(d, bkbd);
149 return 0;
150 }
151
yb9_kbdock_nb_call(struct notifier_block * nb,unsigned long bkbd,void * unused)152 static int yb9_kbdock_nb_call(struct notifier_block *nb,
153 unsigned long bkbd, void *unused)
154 {
155 struct yb9_kbdock_data *d =
156 container_of(nb, struct yb9_kbdock_data, nb);
157
158 yb9_kbdock_report(d, bkbd);
159 return NOTIFY_DONE;
160 }
161
keyboard_position_show(struct device * dev,struct device_attribute * attr,char * buf)162 static ssize_t keyboard_position_show(struct device *dev,
163 struct device_attribute *attr,
164 char *buf)
165 {
166 struct yb9_kbdock_data *d = dev_get_drvdata(dev);
167 u32 bkbd;
168 int ret;
169
170 ret = yb9_kbdock_query(d, &bkbd);
171 if (ret)
172 return ret;
173 return sysfs_emit(buf, "%u\n", bkbd);
174 }
175 static DEVICE_ATTR_RO(keyboard_position);
176
177 static const struct attribute * const yb9_kbdock_attrs[] = {
178 &dev_attr_keyboard_position.attr,
179 NULL,
180 };
181 ATTRIBUTE_GROUPS(yb9_kbdock);
182
yb9_kbdock_resume(struct device * dev)183 static int yb9_kbdock_resume(struct device *dev)
184 {
185 struct yb9_kbdock_data *d = dev_get_drvdata(dev);
186
187 return yb9_kbdock_sync(d);
188 }
189 static DEFINE_SIMPLE_DEV_PM_OPS(yb9_kbdock_pm_ops, NULL, yb9_kbdock_resume);
190
yb9_kbdock_block_probe(struct wmi_device * wdev,const void * ctx)191 static int yb9_kbdock_block_probe(struct wmi_device *wdev, const void *ctx)
192 {
193 struct yb9_kbdock_data *d;
194 struct input_dev *input_dev;
195 int ret;
196
197 d = devm_kzalloc(&wdev->dev, sizeof(*d), GFP_KERNEL);
198 if (!d)
199 return -ENOMEM;
200
201 d->wdev = wdev;
202 spin_lock_init(&d->lock);
203
204 input_dev = devm_input_allocate_device(&wdev->dev);
205 if (!input_dev)
206 return -ENOMEM;
207
208 input_dev->name = "Lenovo Yoga Book 9 keyboard dock switch";
209 input_dev->phys = YB9_KBDOCK_QUERY_GUID "/input0";
210 input_dev->id.bustype = BUS_HOST;
211 input_set_capability(input_dev, EV_SW, SW_TABLET_MODE);
212
213 ret = input_register_device(input_dev);
214 if (ret)
215 return ret;
216
217 d->input_dev = input_dev;
218 d->nb.notifier_call = yb9_kbdock_nb_call;
219
220 ret = devm_yb9_kbdock_register_notifier(&wdev->dev, &d->nb);
221 if (ret)
222 return ret;
223
224 dev_set_drvdata(&wdev->dev, d);
225 return yb9_kbdock_sync(d);
226 }
227
228 static const struct wmi_device_id yb9_kbdock_block_id_table[] = {
229 { .guid_string = YB9_KBDOCK_QUERY_GUID },
230 { }
231 };
232
233 static struct wmi_driver yb9_kbdock_block_driver = {
234 .driver = {
235 .name = "lenovo-yb9-kbdock",
236 .dev_groups = yb9_kbdock_groups,
237 .pm = pm_sleep_ptr(&yb9_kbdock_pm_ops),
238 },
239 .id_table = yb9_kbdock_block_id_table,
240 .no_singleton = true,
241 .probe = yb9_kbdock_block_probe,
242 };
243
244 /* ------------------------------------------------------------------
245 * Event WMI driver — LENOVO_BTKBD_EVENT
246 * (fires the notifier chain on each WMI event)
247 * ------------------------------------------------------------------ */
248
yb9_kbdock_notify_new(struct wmi_device * wdev,const struct wmi_buffer * data)249 static void yb9_kbdock_notify_new(struct wmi_device *wdev,
250 const struct wmi_buffer *data)
251 {
252 /*
253 * _WED(0xEB) returns EC.BKBD directly as a 32-bit integer
254 * (LENOVO_BTKBD_EVENT WmiDataId(1) uint32 Status).
255 * Short-buffer guard is handled by .min_event_size below.
256 */
257 u32 bkbd = le32_to_cpu(*(const __le32 *)data->data);
258
259 blocking_notifier_call_chain(&yb9_kbdock_chain_head, bkbd, NULL);
260 }
261
262 static const struct wmi_device_id yb9_kbdock_event_id_table[] = {
263 { .guid_string = YB9_KBDOCK_EVENT_GUID },
264 { }
265 };
266 MODULE_DEVICE_TABLE(wmi, yb9_kbdock_event_id_table);
267
268 static struct wmi_driver yb9_kbdock_event_driver = {
269 .driver = {
270 .name = "lenovo-yb9-kbdock-event",
271 },
272 .id_table = yb9_kbdock_event_id_table,
273 .no_singleton = true,
274 .notify_new = yb9_kbdock_notify_new,
275 .min_event_size = sizeof(__le32),
276 };
277
278 /* ------------------------------------------------------------------
279 * Module init / exit
280 * ------------------------------------------------------------------ */
281
282 static const struct dmi_system_id yb9_kbdock_dmi_table[] __initconst = {
283 {
284 /* Lenovo Yoga Book 9 14IAH10 */
285 .matches = {
286 DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
287 DMI_MATCH(DMI_PRODUCT_NAME, "83KJ"),
288 },
289 },
290 { }
291 };
292
yb9_kbdock_init(void)293 static int __init yb9_kbdock_init(void)
294 {
295 int ret;
296
297 if (!dmi_check_system(yb9_kbdock_dmi_table))
298 return -ENODEV;
299
300 ret = wmi_driver_register(&yb9_kbdock_event_driver);
301 if (ret)
302 return ret;
303
304 ret = wmi_driver_register(&yb9_kbdock_block_driver);
305 if (ret) {
306 wmi_driver_unregister(&yb9_kbdock_event_driver);
307 return ret;
308 }
309
310 return 0;
311 }
312 module_init(yb9_kbdock_init);
313
yb9_kbdock_exit(void)314 static void __exit yb9_kbdock_exit(void)
315 {
316 wmi_driver_unregister(&yb9_kbdock_block_driver);
317 wmi_driver_unregister(&yb9_kbdock_event_driver);
318 }
319 module_exit(yb9_kbdock_exit);
320
321 MODULE_AUTHOR("Dave Carey <carvsdriver@gmail.com>");
322 MODULE_DESCRIPTION("Lenovo Yoga Book 9 keyboard dock detection");
323 MODULE_LICENSE("GPL");
324