summaryrefslogtreecommitdiffstats
path: root/drivers/platform/x86/lenovo/yb9-kbdock.c
blob: 70d81f3af09ec813c27085d77973dd819d7456c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Lenovo Yoga Book 9 keyboard-dock detection
 *
 * The Yoga Book 9 ships with a detachable Bluetooth keyboard that magnetically
 * attaches to the bottom screen in one of two positions.  The EC tracks
 * attachment state in a 2-bit field called BKBD and signals changes via WMI
 * event 0xEB on the WM10 ACPI device (_UID "GMZN").
 *
 * BKBD values:
 *   0 = keyboard detached
 *   1 = keyboard docked on the top half of the bottom screen
 *   2 = keyboard docked on the bottom half of the bottom screen
 *   3 = reserved / not observed
 *
 * Two WMI interfaces are used (documented in embedded BMOF, WQDD, 20705 bytes):
 *
 *   LENOVO_BTKBD_EVENT (event GUID, 806BD2A2-...)
 *     WmiDataId(1) uint32 Status — _WED(0xEB) returns EC.BKBD directly.
 *     The notify callback receives BKBD as an integer; no separate query needed.
 *
 *   LENOVO_FEATURE_STATUS_DATA (block GUID, E7F300FA-...)
 *     WmiDataId(1) uint32 IDs   = 0x00060000 (feature selector)
 *     WmiDataId(2) uint32 Status = BKBD value
 *     Used on probe and resume to read initial state.
 *
 * The event driver (LENOVO_BTKBD_EVENT) fires a notifier chain on each WMI
 * event.  The block driver (LENOVO_FEATURE_STATUS_DATA) owns the input_dev
 * and registers a notifier_block to receive those events, eliminating the
 * need for shared global state or a mutex.
 *
 * SW_TABLET_MODE=1 is reported when the keyboard is detached;
 * SW_TABLET_MODE=0 when docked in either position (keyboard present).
 * The raw BKBD value is exposed via the sysfs attribute "keyboard_position".
 *
 * Copyright (C) 2026 Dave Carey <carvsdriver@gmail.com>
 */

#include <linux/acpi.h>
#include <linux/cleanup.h>
#include <linux/compiler_attributes.h>
#include <linux/dev_printk.h>
#include <linux/dmi.h>
#include <linux/input.h>
#include <linux/module.h>
#include <linux/notifier.h>
#include <linux/pm.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include <linux/wmi.h>

#define YB9_KBDOCK_EVENT_GUID	"806BD2A2-177B-481D-BFB5-3BA0BB4A2285"
#define YB9_KBDOCK_QUERY_GUID	"E7F300FA-21CD-4003-ADAC-2696135982E6"

/* BKBD encoding */
#define BKBD_DETACHED		0

/* LENOVO_FEATURE_STATUS_DATA feature selector */
#define YB9_FEATURE_STATUS_ID	0x00060000u

/*
 * LENOVO_FEATURE_STATUS_DATA: 8-byte buffer {uint32 IDs, uint32 Status}.
 * IDs is always 0x00060000; Status holds the BKBD value (0–3).
 */
struct lenovo_feature_status {
	__le32 id;
	__le32 status;
} __packed;

/* ------------------------------------------------------------------
 * Notifier chain — event driver fires it, block driver listens
 * ------------------------------------------------------------------ */

static BLOCKING_NOTIFIER_HEAD(yb9_kbdock_chain_head);

static void devm_yb9_kbdock_unregister_notifier(void *data)
{
	struct notifier_block *nb = data;

	blocking_notifier_chain_unregister(&yb9_kbdock_chain_head, nb);
}

static int devm_yb9_kbdock_register_notifier(struct device *dev,
					      struct notifier_block *nb)
{
	int ret;

	ret = blocking_notifier_chain_register(&yb9_kbdock_chain_head, nb);
	if (ret < 0)
		return ret;

	return devm_add_action_or_reset(dev, devm_yb9_kbdock_unregister_notifier, nb);
}

/* ------------------------------------------------------------------
 * Block WMI driver — LENOVO_FEATURE_STATUS_DATA
 * (owns input_dev, sysfs, PM resume)
 * ------------------------------------------------------------------ */

struct yb9_kbdock_data {
	struct wmi_device	*wdev;
	struct input_dev	*input_dev;
	struct notifier_block	 nb;
	spinlock_t		 lock;	/* protects input_report_switch + input_sync */
};

static int yb9_kbdock_query(struct yb9_kbdock_data *d, u32 *bkbd)
{
	struct wmi_buffer out;
	int ret;

	ret = wmidev_query_block(d->wdev, 0, &out,
				 sizeof(struct lenovo_feature_status));
	if (ret)
		return ret;

	struct lenovo_feature_status *fs __free(kfree) = out.data;

	if (le32_to_cpu(fs->id) != YB9_FEATURE_STATUS_ID)
		return -EIO;

	*bkbd = le32_to_cpu(fs->status);
	return 0;
}

static void yb9_kbdock_report(struct yb9_kbdock_data *d, u32 bkbd)
{
	int tablet = (bkbd == BKBD_DETACHED) ? 1 : 0;

	spin_lock(&d->lock);
	input_report_switch(d->input_dev, SW_TABLET_MODE, tablet);
	input_sync(d->input_dev);
	spin_unlock(&d->lock);
	dev_dbg(&d->wdev->dev, "BKBD=%u SW_TABLET_MODE=%d\n", bkbd, tablet);
}

static int yb9_kbdock_sync(struct yb9_kbdock_data *d)
{
	u32 bkbd;
	int ret;

	ret = yb9_kbdock_query(d, &bkbd);
	if (ret)
		return ret;

	yb9_kbdock_report(d, bkbd);
	return 0;
}

static int yb9_kbdock_nb_call(struct notifier_block *nb,
			       unsigned long bkbd, void *unused)
{
	struct yb9_kbdock_data *d =
		container_of(nb, struct yb9_kbdock_data, nb);

	yb9_kbdock_report(d, bkbd);
	return NOTIFY_DONE;
}

static ssize_t keyboard_position_show(struct device *dev,
				       struct device_attribute *attr,
				       char *buf)
{
	struct yb9_kbdock_data *d = dev_get_drvdata(dev);
	u32 bkbd;
	int ret;

	ret = yb9_kbdock_query(d, &bkbd);
	if (ret)
		return ret;
	return sysfs_emit(buf, "%u\n", bkbd);
}
static DEVICE_ATTR_RO(keyboard_position);

static const struct attribute * const yb9_kbdock_attrs[] = {
	&dev_attr_keyboard_position.attr,
	NULL,
};
ATTRIBUTE_GROUPS(yb9_kbdock);

static int yb9_kbdock_resume(struct device *dev)
{
	struct yb9_kbdock_data *d = dev_get_drvdata(dev);

	return yb9_kbdock_sync(d);
}
static DEFINE_SIMPLE_DEV_PM_OPS(yb9_kbdock_pm_ops, NULL, yb9_kbdock_resume);

static int yb9_kbdock_block_probe(struct wmi_device *wdev, const void *ctx)
{
	struct yb9_kbdock_data *d;
	struct input_dev *input_dev;
	int ret;

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

	d->wdev = wdev;
	spin_lock_init(&d->lock);

	input_dev = devm_input_allocate_device(&wdev->dev);
	if (!input_dev)
		return -ENOMEM;

	input_dev->name		= "Lenovo Yoga Book 9 keyboard dock switch";
	input_dev->phys		= YB9_KBDOCK_QUERY_GUID "/input0";
	input_dev->id.bustype	= BUS_HOST;
	input_set_capability(input_dev, EV_SW, SW_TABLET_MODE);

	ret = input_register_device(input_dev);
	if (ret)
		return ret;

	d->input_dev		= input_dev;
	d->nb.notifier_call	= yb9_kbdock_nb_call;

	ret = devm_yb9_kbdock_register_notifier(&wdev->dev, &d->nb);
	if (ret)
		return ret;

	dev_set_drvdata(&wdev->dev, d);
	return yb9_kbdock_sync(d);
}

static const struct wmi_device_id yb9_kbdock_block_id_table[] = {
	{ .guid_string = YB9_KBDOCK_QUERY_GUID },
	{ }
};

static struct wmi_driver yb9_kbdock_block_driver = {
	.driver = {
		.name		= "lenovo-yb9-kbdock",
		.dev_groups	= yb9_kbdock_groups,
		.pm		= pm_sleep_ptr(&yb9_kbdock_pm_ops),
	},
	.id_table	= yb9_kbdock_block_id_table,
	.no_singleton	= true,
	.probe		= yb9_kbdock_block_probe,
};

/* ------------------------------------------------------------------
 * Event WMI driver — LENOVO_BTKBD_EVENT
 * (fires the notifier chain on each WMI event)
 * ------------------------------------------------------------------ */

static void yb9_kbdock_notify_new(struct wmi_device *wdev,
				  const struct wmi_buffer *data)
{
	/*
	 * _WED(0xEB) returns EC.BKBD directly as a 32-bit integer
	 * (LENOVO_BTKBD_EVENT WmiDataId(1) uint32 Status).
	 * Short-buffer guard is handled by .min_event_size below.
	 */
	u32 bkbd = le32_to_cpu(*(const __le32 *)data->data);

	blocking_notifier_call_chain(&yb9_kbdock_chain_head, bkbd, NULL);
}

static const struct wmi_device_id yb9_kbdock_event_id_table[] = {
	{ .guid_string = YB9_KBDOCK_EVENT_GUID },
	{ }
};
MODULE_DEVICE_TABLE(wmi, yb9_kbdock_event_id_table);

static struct wmi_driver yb9_kbdock_event_driver = {
	.driver = {
		.name = "lenovo-yb9-kbdock-event",
	},
	.id_table	= yb9_kbdock_event_id_table,
	.no_singleton	= true,
	.notify_new	= yb9_kbdock_notify_new,
	.min_event_size	= sizeof(__le32),
};

/* ------------------------------------------------------------------
 * Module init / exit
 * ------------------------------------------------------------------ */

static const struct dmi_system_id yb9_kbdock_dmi_table[] __initconst = {
	{
		/* Lenovo Yoga Book 9 14IAH10 */
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR,   "LENOVO"),
			DMI_MATCH(DMI_PRODUCT_NAME, "83KJ"),
		},
	},
	{ }
};

static int __init yb9_kbdock_init(void)
{
	int ret;

	if (!dmi_check_system(yb9_kbdock_dmi_table))
		return -ENODEV;

	ret = wmi_driver_register(&yb9_kbdock_event_driver);
	if (ret)
		return ret;

	ret = wmi_driver_register(&yb9_kbdock_block_driver);
	if (ret) {
		wmi_driver_unregister(&yb9_kbdock_event_driver);
		return ret;
	}

	return 0;
}
module_init(yb9_kbdock_init);

static void __exit yb9_kbdock_exit(void)
{
	wmi_driver_unregister(&yb9_kbdock_block_driver);
	wmi_driver_unregister(&yb9_kbdock_event_driver);
}
module_exit(yb9_kbdock_exit);

MODULE_AUTHOR("Dave Carey <carvsdriver@gmail.com>");
MODULE_DESCRIPTION("Lenovo Yoga Book 9 keyboard dock detection");
MODULE_LICENSE("GPL");