summaryrefslogtreecommitdiffstats
path: root/drivers/platform/surface/surface_aggregator_tabletsw.c
blob: af8b547cffdc6450168086a22a398d3254993e9b (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
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
// SPDX-License-Identifier: GPL-2.0+
/*
 * Surface System Aggregator Module (SSAM) tablet mode switch driver.
 *
 * Copyright (C) 2022 Maximilian Luz <luzmaximilian@gmail.com>
 */

#include <asm/unaligned.h>
#include <linux/input.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/workqueue.h>

#include <linux/surface_aggregator/controller.h>
#include <linux/surface_aggregator/device.h>


/* -- SSAM generic tablet switch driver framework. -------------------------- */

struct ssam_tablet_sw;

struct ssam_tablet_sw_ops {
	int (*get_state)(struct ssam_tablet_sw *sw, u32 *state);
	const char *(*state_name)(struct ssam_tablet_sw *sw, u32 state);
	bool (*state_is_tablet_mode)(struct ssam_tablet_sw *sw, u32 state);
};

struct ssam_tablet_sw {
	struct ssam_device *sdev;

	u32 state;
	struct work_struct update_work;
	struct input_dev *mode_switch;

	struct ssam_tablet_sw_ops ops;
	struct ssam_event_notifier notif;
};

struct ssam_tablet_sw_desc {
	struct {
		const char *name;
		const char *phys;
	} dev;

	struct {
		u32 (*notify)(struct ssam_event_notifier *nf, const struct ssam_event *event);
		int (*get_state)(struct ssam_tablet_sw *sw, u32 *state);
		const char *(*state_name)(struct ssam_tablet_sw *sw, u32 state);
		bool (*state_is_tablet_mode)(struct ssam_tablet_sw *sw, u32 state);
	} ops;

	struct {
		struct ssam_event_registry reg;
		struct ssam_event_id id;
		enum ssam_event_mask mask;
		u8 flags;
	} event;
};

static ssize_t state_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct ssam_tablet_sw *sw = dev_get_drvdata(dev);
	const char *state = sw->ops.state_name(sw, sw->state);

	return sysfs_emit(buf, "%s\n", state);
}
static DEVICE_ATTR_RO(state);

static struct attribute *ssam_tablet_sw_attrs[] = {
	&dev_attr_state.attr,
	NULL,
};

static const struct attribute_group ssam_tablet_sw_group = {
	.attrs = ssam_tablet_sw_attrs,
};

static void ssam_tablet_sw_update_workfn(struct work_struct *work)
{
	struct ssam_tablet_sw *sw = container_of(work, struct ssam_tablet_sw, update_work);
	int tablet, status;
	u32 state;

	status = sw->ops.get_state(sw, &state);
	if (status)
		return;

	if (sw->state == state)
		return;
	sw->state = state;

	/* Send SW_TABLET_MODE event. */
	tablet = sw->ops.state_is_tablet_mode(sw, state);
	input_report_switch(sw->mode_switch, SW_TABLET_MODE, tablet);
	input_sync(sw->mode_switch);
}

static int __maybe_unused ssam_tablet_sw_resume(struct device *dev)
{
	struct ssam_tablet_sw *sw = dev_get_drvdata(dev);

	schedule_work(&sw->update_work);
	return 0;
}
static SIMPLE_DEV_PM_OPS(ssam_tablet_sw_pm_ops, NULL, ssam_tablet_sw_resume);

static int ssam_tablet_sw_probe(struct ssam_device *sdev)
{
	const struct ssam_tablet_sw_desc *desc;
	struct ssam_tablet_sw *sw;
	int tablet, status;

	desc = ssam_device_get_match_data(sdev);
	if (!desc) {
		WARN(1, "no driver match data specified");
		return -EINVAL;
	}

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

	sw->sdev = sdev;

	sw->ops.get_state = desc->ops.get_state;
	sw->ops.state_name = desc->ops.state_name;
	sw->ops.state_is_tablet_mode = desc->ops.state_is_tablet_mode;

	INIT_WORK(&sw->update_work, ssam_tablet_sw_update_workfn);

	ssam_device_set_drvdata(sdev, sw);

	/* Get initial state. */
	status = sw->ops.get_state(sw, &sw->state);
	if (status)
		return status;

	/* Set up tablet mode switch. */
	sw->mode_switch = devm_input_allocate_device(&sdev->dev);
	if (!sw->mode_switch)
		return -ENOMEM;

	sw->mode_switch->name = desc->dev.name;
	sw->mode_switch->phys = desc->dev.phys;
	sw->mode_switch->id.bustype = BUS_HOST;
	sw->mode_switch->dev.parent = &sdev->dev;

	tablet = sw->ops.state_is_tablet_mode(sw, sw->state);
	input_set_capability(sw->mode_switch, EV_SW, SW_TABLET_MODE);
	input_report_switch(sw->mode_switch, SW_TABLET_MODE, tablet);

	status = input_register_device(sw->mode_switch);
	if (status)
		return status;

	/* Set up notifier. */
	sw->notif.base.priority = 0;
	sw->notif.base.fn = desc->ops.notify;
	sw->notif.event.reg = desc->event.reg;
	sw->notif.event.id = desc->event.id;
	sw->notif.event.mask = desc->event.mask;
	sw->notif.event.flags = SSAM_EVENT_SEQUENCED;

	status = ssam_device_notifier_register(sdev, &sw->notif);
	if (status)
		return status;

	status = sysfs_create_group(&sdev->dev.kobj, &ssam_tablet_sw_group);
	if (status)
		goto err;

	/* We might have missed events during setup, so check again. */
	schedule_work(&sw->update_work);
	return 0;

err:
	ssam_device_notifier_unregister(sdev, &sw->notif);
	cancel_work_sync(&sw->update_work);
	return status;
}

static void ssam_tablet_sw_remove(struct ssam_device *sdev)
{
	struct ssam_tablet_sw *sw = ssam_device_get_drvdata(sdev);

	sysfs_remove_group(&sdev->dev.kobj, &ssam_tablet_sw_group);

	ssam_device_notifier_unregister(sdev, &sw->notif);
	cancel_work_sync(&sw->update_work);
}


/* -- SSAM KIP tablet switch implementation. -------------------------------- */

#define SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED	0x1d

enum ssam_kip_cover_state {
	SSAM_KIP_COVER_STATE_DISCONNECTED  = 0x01,
	SSAM_KIP_COVER_STATE_CLOSED        = 0x02,
	SSAM_KIP_COVER_STATE_LAPTOP        = 0x03,
	SSAM_KIP_COVER_STATE_FOLDED_CANVAS = 0x04,
	SSAM_KIP_COVER_STATE_FOLDED_BACK   = 0x05,
	SSAM_KIP_COVER_STATE_BOOK          = 0x06,
};

static const char *ssam_kip_cover_state_name(struct ssam_tablet_sw *sw, u32 state)
{
	switch (state) {
	case SSAM_KIP_COVER_STATE_DISCONNECTED:
		return "disconnected";

	case SSAM_KIP_COVER_STATE_CLOSED:
		return "closed";

	case SSAM_KIP_COVER_STATE_LAPTOP:
		return "laptop";

	case SSAM_KIP_COVER_STATE_FOLDED_CANVAS:
		return "folded-canvas";

	case SSAM_KIP_COVER_STATE_FOLDED_BACK:
		return "folded-back";

	case SSAM_KIP_COVER_STATE_BOOK:
		return "book";

	default:
		dev_warn(&sw->sdev->dev, "unknown KIP cover state: %u\n", state);
		return "<unknown>";
	}
}

static bool ssam_kip_cover_state_is_tablet_mode(struct ssam_tablet_sw *sw, u32 state)
{
	switch (state) {
	case SSAM_KIP_COVER_STATE_DISCONNECTED:
	case SSAM_KIP_COVER_STATE_FOLDED_CANVAS:
	case SSAM_KIP_COVER_STATE_FOLDED_BACK:
	case SSAM_KIP_COVER_STATE_BOOK:
		return true;

	case SSAM_KIP_COVER_STATE_CLOSED:
	case SSAM_KIP_COVER_STATE_LAPTOP:
		return false;

	default:
		dev_warn(&sw->sdev->dev, "unknown KIP cover state: %d\n", sw->state);
		return true;
	}
}

SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_get_cover_state, u8, {
	.target_category = SSAM_SSH_TC_KIP,
	.target_id       = 0x01,
	.command_id      = 0x1d,
	.instance_id     = 0x00,
});

static int ssam_kip_get_cover_state(struct ssam_tablet_sw *sw, u32 *state)
{
	int status;
	u8 raw;

	status = ssam_retry(__ssam_kip_get_cover_state, sw->sdev->ctrl, &raw);
	if (status < 0) {
		dev_err(&sw->sdev->dev, "failed to query KIP lid state: %d\n", status);
		return status;
	}

	*state = raw;
	return 0;
}

static u32 ssam_kip_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
{
	struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif);

	if (event->command_id != SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED)
		return 0;	/* Return "unhandled". */

	if (event->length < 1)
		dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n", event->length);

	schedule_work(&sw->update_work);
	return SSAM_NOTIF_HANDLED;
}

static const struct ssam_tablet_sw_desc ssam_kip_sw_desc = {
	.dev = {
		.name = "Microsoft Surface KIP Tablet Mode Switch",
		.phys = "ssam/01:0e:01:00:01/input0",
	},
	.ops = {
		.notify = ssam_kip_sw_notif,
		.get_state = ssam_kip_get_cover_state,
		.state_name = ssam_kip_cover_state_name,
		.state_is_tablet_mode = ssam_kip_cover_state_is_tablet_mode,
	},
	.event = {
		.reg = SSAM_EVENT_REGISTRY_SAM,
		.id = {
			.target_category = SSAM_SSH_TC_KIP,
			.instance = 0,
		},
		.mask = SSAM_EVENT_MASK_TARGET,
	},
};


/* -- SSAM POS tablet switch implementation. -------------------------------- */

static bool tablet_mode_in_slate_state = true;
module_param(tablet_mode_in_slate_state, bool, 0644);
MODULE_PARM_DESC(tablet_mode_in_slate_state, "Enable tablet mode in slate device posture, default is 'true'");

#define SSAM_EVENT_POS_CID_POSTURE_CHANGED	0x03
#define SSAM_POS_MAX_SOURCES			4

enum ssam_pos_state {
	SSAM_POS_POSTURE_LID_CLOSED = 0x00,
	SSAM_POS_POSTURE_LAPTOP     = 0x01,
	SSAM_POS_POSTURE_SLATE      = 0x02,
	SSAM_POS_POSTURE_TABLET     = 0x03,
};

struct ssam_sources_list {
	__le32 count;
	__le32 id[SSAM_POS_MAX_SOURCES];
} __packed;

static const char *ssam_pos_state_name(struct ssam_tablet_sw *sw, u32 state)
{
	switch (state) {
	case SSAM_POS_POSTURE_LID_CLOSED:
		return "closed";

	case SSAM_POS_POSTURE_LAPTOP:
		return "laptop";

	case SSAM_POS_POSTURE_SLATE:
		return "slate";

	case SSAM_POS_POSTURE_TABLET:
		return "tablet";

	default:
		dev_warn(&sw->sdev->dev, "unknown device posture: %u\n", state);
		return "<unknown>";
	}
}

static bool ssam_pos_state_is_tablet_mode(struct ssam_tablet_sw *sw, u32 state)
{
	switch (state) {
	case SSAM_POS_POSTURE_LAPTOP:
	case SSAM_POS_POSTURE_LID_CLOSED:
		return false;

	case SSAM_POS_POSTURE_SLATE:
		return tablet_mode_in_slate_state;

	case SSAM_POS_POSTURE_TABLET:
		return true;

	default:
		dev_warn(&sw->sdev->dev, "unknown device posture: %u\n", state);
		return true;
	}
}

static int ssam_pos_get_sources_list(struct ssam_tablet_sw *sw, struct ssam_sources_list *sources)
{
	struct ssam_request rqst;
	struct ssam_response rsp;
	int status;

	rqst.target_category = SSAM_SSH_TC_POS;
	rqst.target_id = 0x01;
	rqst.command_id = 0x01;
	rqst.instance_id = 0x00;
	rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
	rqst.length = 0;
	rqst.payload = NULL;

	rsp.capacity = sizeof(*sources);
	rsp.length = 0;
	rsp.pointer = (u8 *)sources;

	status = ssam_retry(ssam_request_sync_onstack, sw->sdev->ctrl, &rqst, &rsp, 0);
	if (status)
		return status;

	/* We need at least the 'sources->count' field. */
	if (rsp.length < sizeof(__le32)) {
		dev_err(&sw->sdev->dev, "received source list response is too small\n");
		return -EPROTO;
	}

	/* Make sure 'sources->count' matches with the response length. */
	if (get_unaligned_le32(&sources->count) * sizeof(__le32) + sizeof(__le32) != rsp.length) {
		dev_err(&sw->sdev->dev, "mismatch between number of sources and response size\n");
		return -EPROTO;
	}

	return 0;
}

static int ssam_pos_get_source(struct ssam_tablet_sw *sw, u32 *source_id)
{
	struct ssam_sources_list sources = {};
	int status;

	status = ssam_pos_get_sources_list(sw, &sources);
	if (status)
		return status;

	if (get_unaligned_le32(&sources.count) == 0) {
		dev_err(&sw->sdev->dev, "no posture sources found\n");
		return -ENODEV;
	}

	/*
	 * We currently don't know what to do with more than one posture
	 * source. At the moment, only one source seems to be used/provided.
	 * The WARN_ON() here should hopefully let us know quickly once there
	 * is a device that provides multiple sources, at which point we can
	 * then try to figure out how to handle them.
	 */
	WARN_ON(get_unaligned_le32(&sources.count) > 1);

	*source_id = get_unaligned_le32(&sources.id[0]);
	return 0;
}

SSAM_DEFINE_SYNC_REQUEST_WR(__ssam_pos_get_posture_for_source, __le32, __le32, {
	.target_category = SSAM_SSH_TC_POS,
	.target_id       = 0x01,
	.command_id      = 0x02,
	.instance_id     = 0x00,
});

static int ssam_pos_get_posture_for_source(struct ssam_tablet_sw *sw, u32 source_id, u32 *posture)
{
	__le32 source_le = cpu_to_le32(source_id);
	__le32 rspval_le = 0;
	int status;

	status = ssam_retry(__ssam_pos_get_posture_for_source, sw->sdev->ctrl,
			    &source_le, &rspval_le);
	if (status)
		return status;

	*posture = le32_to_cpu(rspval_le);
	return 0;
}

static int ssam_pos_get_posture(struct ssam_tablet_sw *sw, u32 *state)
{
	u32 source_id;
	int status;

	status = ssam_pos_get_source(sw, &source_id);
	if (status) {
		dev_err(&sw->sdev->dev, "failed to get posture source ID: %d\n", status);
		return status;
	}

	status = ssam_pos_get_posture_for_source(sw, source_id, state);
	if (status) {
		dev_err(&sw->sdev->dev, "failed to get posture value for source %u: %d\n",
			source_id, status);
		return status;
	}

	return 0;
}

static u32 ssam_pos_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
{
	struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif);

	if (event->command_id != SSAM_EVENT_POS_CID_POSTURE_CHANGED)
		return 0;	/* Return "unhandled". */

	if (event->length != sizeof(__le32) * 3)
		dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n", event->length);

	schedule_work(&sw->update_work);
	return SSAM_NOTIF_HANDLED;
}

static const struct ssam_tablet_sw_desc ssam_pos_sw_desc = {
	.dev = {
		.name = "Microsoft Surface POS Tablet Mode Switch",
		.phys = "ssam/01:26:01:00:01/input0",
	},
	.ops = {
		.notify = ssam_pos_sw_notif,
		.get_state = ssam_pos_get_posture,
		.state_name = ssam_pos_state_name,
		.state_is_tablet_mode = ssam_pos_state_is_tablet_mode,
	},
	.event = {
		.reg = SSAM_EVENT_REGISTRY_SAM,
		.id = {
			.target_category = SSAM_SSH_TC_POS,
			.instance = 0,
		},
		.mask = SSAM_EVENT_MASK_TARGET,
	},
};


/* -- Driver registration. -------------------------------------------------- */

static const struct ssam_device_id ssam_tablet_sw_match[] = {
	{ SSAM_SDEV(KIP, 0x01, 0x00, 0x01), (unsigned long)&ssam_kip_sw_desc },
	{ SSAM_SDEV(POS, 0x01, 0x00, 0x01), (unsigned long)&ssam_pos_sw_desc },
	{ },
};
MODULE_DEVICE_TABLE(ssam, ssam_tablet_sw_match);

static struct ssam_device_driver ssam_tablet_sw_driver = {
	.probe = ssam_tablet_sw_probe,
	.remove = ssam_tablet_sw_remove,
	.match_table = ssam_tablet_sw_match,
	.driver = {
		.name = "surface_aggregator_tablet_mode_switch",
		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
		.pm = &ssam_tablet_sw_pm_ops,
	},
};
module_ssam_device_driver(ssam_tablet_sw_driver);

MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
MODULE_DESCRIPTION("Tablet mode switch driver for Surface devices using the Surface Aggregator Module");
MODULE_LICENSE("GPL");