1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* |
3 | * Surface System Aggregator Module (SSAM) tablet mode switch driver. |
4 | * |
5 | * Copyright (C) 2022 Maximilian Luz <luzmaximilian@gmail.com> |
6 | */ |
7 | |
8 | #include <asm/unaligned.h> |
9 | #include <linux/input.h> |
10 | #include <linux/kernel.h> |
11 | #include <linux/module.h> |
12 | #include <linux/types.h> |
13 | #include <linux/workqueue.h> |
14 | |
15 | #include <linux/surface_aggregator/controller.h> |
16 | #include <linux/surface_aggregator/device.h> |
17 | |
18 | |
19 | /* -- SSAM generic tablet switch driver framework. -------------------------- */ |
20 | |
21 | struct ssam_tablet_sw; |
22 | |
23 | struct ssam_tablet_sw_state { |
24 | u32 source; |
25 | u32 state; |
26 | }; |
27 | |
28 | struct ssam_tablet_sw_ops { |
29 | int (*get_state)(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state); |
30 | const char *(*state_name)(struct ssam_tablet_sw *sw, |
31 | const struct ssam_tablet_sw_state *state); |
32 | bool (*state_is_tablet_mode)(struct ssam_tablet_sw *sw, |
33 | const struct ssam_tablet_sw_state *state); |
34 | }; |
35 | |
36 | struct ssam_tablet_sw { |
37 | struct ssam_device *sdev; |
38 | |
39 | struct ssam_tablet_sw_state state; |
40 | struct work_struct update_work; |
41 | struct input_dev *mode_switch; |
42 | |
43 | struct ssam_tablet_sw_ops ops; |
44 | struct ssam_event_notifier notif; |
45 | }; |
46 | |
47 | struct ssam_tablet_sw_desc { |
48 | struct { |
49 | const char *name; |
50 | const char *phys; |
51 | } dev; |
52 | |
53 | struct { |
54 | u32 (*notify)(struct ssam_event_notifier *nf, const struct ssam_event *event); |
55 | int (*get_state)(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state); |
56 | const char *(*state_name)(struct ssam_tablet_sw *sw, |
57 | const struct ssam_tablet_sw_state *state); |
58 | bool (*state_is_tablet_mode)(struct ssam_tablet_sw *sw, |
59 | const struct ssam_tablet_sw_state *state); |
60 | } ops; |
61 | |
62 | struct { |
63 | struct ssam_event_registry reg; |
64 | struct ssam_event_id id; |
65 | enum ssam_event_mask mask; |
66 | u8 flags; |
67 | } event; |
68 | }; |
69 | |
70 | static ssize_t state_show(struct device *dev, struct device_attribute *attr, char *buf) |
71 | { |
72 | struct ssam_tablet_sw *sw = dev_get_drvdata(dev); |
73 | const char *state = sw->ops.state_name(sw, &sw->state); |
74 | |
75 | return sysfs_emit(buf, fmt: "%s\n" , state); |
76 | } |
77 | static DEVICE_ATTR_RO(state); |
78 | |
79 | static struct attribute *ssam_tablet_sw_attrs[] = { |
80 | &dev_attr_state.attr, |
81 | NULL, |
82 | }; |
83 | |
84 | static const struct attribute_group ssam_tablet_sw_group = { |
85 | .attrs = ssam_tablet_sw_attrs, |
86 | }; |
87 | |
88 | static void ssam_tablet_sw_update_workfn(struct work_struct *work) |
89 | { |
90 | struct ssam_tablet_sw *sw = container_of(work, struct ssam_tablet_sw, update_work); |
91 | struct ssam_tablet_sw_state state; |
92 | int tablet, status; |
93 | |
94 | status = sw->ops.get_state(sw, &state); |
95 | if (status) |
96 | return; |
97 | |
98 | if (sw->state.source == state.source && sw->state.state == state.state) |
99 | return; |
100 | sw->state = state; |
101 | |
102 | /* Send SW_TABLET_MODE event. */ |
103 | tablet = sw->ops.state_is_tablet_mode(sw, &state); |
104 | input_report_switch(dev: sw->mode_switch, SW_TABLET_MODE, value: tablet); |
105 | input_sync(dev: sw->mode_switch); |
106 | } |
107 | |
108 | static int __maybe_unused ssam_tablet_sw_resume(struct device *dev) |
109 | { |
110 | struct ssam_tablet_sw *sw = dev_get_drvdata(dev); |
111 | |
112 | schedule_work(work: &sw->update_work); |
113 | return 0; |
114 | } |
115 | static SIMPLE_DEV_PM_OPS(ssam_tablet_sw_pm_ops, NULL, ssam_tablet_sw_resume); |
116 | |
117 | static int ssam_tablet_sw_probe(struct ssam_device *sdev) |
118 | { |
119 | const struct ssam_tablet_sw_desc *desc; |
120 | struct ssam_tablet_sw *sw; |
121 | int tablet, status; |
122 | |
123 | desc = ssam_device_get_match_data(dev: sdev); |
124 | if (!desc) { |
125 | WARN(1, "no driver match data specified" ); |
126 | return -EINVAL; |
127 | } |
128 | |
129 | sw = devm_kzalloc(dev: &sdev->dev, size: sizeof(*sw), GFP_KERNEL); |
130 | if (!sw) |
131 | return -ENOMEM; |
132 | |
133 | sw->sdev = sdev; |
134 | |
135 | sw->ops.get_state = desc->ops.get_state; |
136 | sw->ops.state_name = desc->ops.state_name; |
137 | sw->ops.state_is_tablet_mode = desc->ops.state_is_tablet_mode; |
138 | |
139 | INIT_WORK(&sw->update_work, ssam_tablet_sw_update_workfn); |
140 | |
141 | ssam_device_set_drvdata(sdev, data: sw); |
142 | |
143 | /* Get initial state. */ |
144 | status = sw->ops.get_state(sw, &sw->state); |
145 | if (status) |
146 | return status; |
147 | |
148 | /* Set up tablet mode switch. */ |
149 | sw->mode_switch = devm_input_allocate_device(&sdev->dev); |
150 | if (!sw->mode_switch) |
151 | return -ENOMEM; |
152 | |
153 | sw->mode_switch->name = desc->dev.name; |
154 | sw->mode_switch->phys = desc->dev.phys; |
155 | sw->mode_switch->id.bustype = BUS_HOST; |
156 | sw->mode_switch->dev.parent = &sdev->dev; |
157 | |
158 | tablet = sw->ops.state_is_tablet_mode(sw, &sw->state); |
159 | input_set_capability(dev: sw->mode_switch, EV_SW, SW_TABLET_MODE); |
160 | input_report_switch(dev: sw->mode_switch, SW_TABLET_MODE, value: tablet); |
161 | |
162 | status = input_register_device(sw->mode_switch); |
163 | if (status) |
164 | return status; |
165 | |
166 | /* Set up notifier. */ |
167 | sw->notif.base.priority = 0; |
168 | sw->notif.base.fn = desc->ops.notify; |
169 | sw->notif.event.reg = desc->event.reg; |
170 | sw->notif.event.id = desc->event.id; |
171 | sw->notif.event.mask = desc->event.mask; |
172 | sw->notif.event.flags = SSAM_EVENT_SEQUENCED; |
173 | |
174 | status = ssam_device_notifier_register(sdev, n: &sw->notif); |
175 | if (status) |
176 | return status; |
177 | |
178 | status = sysfs_create_group(kobj: &sdev->dev.kobj, grp: &ssam_tablet_sw_group); |
179 | if (status) |
180 | goto err; |
181 | |
182 | /* We might have missed events during setup, so check again. */ |
183 | schedule_work(work: &sw->update_work); |
184 | return 0; |
185 | |
186 | err: |
187 | ssam_device_notifier_unregister(sdev, n: &sw->notif); |
188 | cancel_work_sync(work: &sw->update_work); |
189 | return status; |
190 | } |
191 | |
192 | static void ssam_tablet_sw_remove(struct ssam_device *sdev) |
193 | { |
194 | struct ssam_tablet_sw *sw = ssam_device_get_drvdata(sdev); |
195 | |
196 | sysfs_remove_group(kobj: &sdev->dev.kobj, grp: &ssam_tablet_sw_group); |
197 | |
198 | ssam_device_notifier_unregister(sdev, n: &sw->notif); |
199 | cancel_work_sync(work: &sw->update_work); |
200 | } |
201 | |
202 | |
203 | /* -- SSAM KIP tablet switch implementation. -------------------------------- */ |
204 | |
205 | #define SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED 0x1d |
206 | |
207 | enum ssam_kip_cover_state { |
208 | SSAM_KIP_COVER_STATE_DISCONNECTED = 0x01, |
209 | SSAM_KIP_COVER_STATE_CLOSED = 0x02, |
210 | SSAM_KIP_COVER_STATE_LAPTOP = 0x03, |
211 | SSAM_KIP_COVER_STATE_FOLDED_CANVAS = 0x04, |
212 | SSAM_KIP_COVER_STATE_FOLDED_BACK = 0x05, |
213 | SSAM_KIP_COVER_STATE_BOOK = 0x06, |
214 | }; |
215 | |
216 | static const char *ssam_kip_cover_state_name(struct ssam_tablet_sw *sw, |
217 | const struct ssam_tablet_sw_state *state) |
218 | { |
219 | switch (state->state) { |
220 | case SSAM_KIP_COVER_STATE_DISCONNECTED: |
221 | return "disconnected" ; |
222 | |
223 | case SSAM_KIP_COVER_STATE_CLOSED: |
224 | return "closed" ; |
225 | |
226 | case SSAM_KIP_COVER_STATE_LAPTOP: |
227 | return "laptop" ; |
228 | |
229 | case SSAM_KIP_COVER_STATE_FOLDED_CANVAS: |
230 | return "folded-canvas" ; |
231 | |
232 | case SSAM_KIP_COVER_STATE_FOLDED_BACK: |
233 | return "folded-back" ; |
234 | |
235 | case SSAM_KIP_COVER_STATE_BOOK: |
236 | return "book" ; |
237 | |
238 | default: |
239 | dev_warn(&sw->sdev->dev, "unknown KIP cover state: %u\n" , state->state); |
240 | return "<unknown>" ; |
241 | } |
242 | } |
243 | |
244 | static bool ssam_kip_cover_state_is_tablet_mode(struct ssam_tablet_sw *sw, |
245 | const struct ssam_tablet_sw_state *state) |
246 | { |
247 | switch (state->state) { |
248 | case SSAM_KIP_COVER_STATE_DISCONNECTED: |
249 | case SSAM_KIP_COVER_STATE_FOLDED_CANVAS: |
250 | case SSAM_KIP_COVER_STATE_FOLDED_BACK: |
251 | case SSAM_KIP_COVER_STATE_BOOK: |
252 | return true; |
253 | |
254 | case SSAM_KIP_COVER_STATE_CLOSED: |
255 | case SSAM_KIP_COVER_STATE_LAPTOP: |
256 | return false; |
257 | |
258 | default: |
259 | dev_warn(&sw->sdev->dev, "unknown KIP cover state: %d\n" , state->state); |
260 | return true; |
261 | } |
262 | } |
263 | |
264 | SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_get_cover_state, u8, { |
265 | .target_category = SSAM_SSH_TC_KIP, |
266 | .target_id = SSAM_SSH_TID_SAM, |
267 | .command_id = 0x1d, |
268 | .instance_id = 0x00, |
269 | }); |
270 | |
271 | static int ssam_kip_get_cover_state(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state) |
272 | { |
273 | int status; |
274 | u8 raw; |
275 | |
276 | status = ssam_retry(__ssam_kip_get_cover_state, sw->sdev->ctrl, &raw); |
277 | if (status < 0) { |
278 | dev_err(&sw->sdev->dev, "failed to query KIP lid state: %d\n" , status); |
279 | return status; |
280 | } |
281 | |
282 | state->source = 0; /* Unused for KIP switch. */ |
283 | state->state = raw; |
284 | return 0; |
285 | } |
286 | |
287 | static u32 ssam_kip_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event) |
288 | { |
289 | struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif); |
290 | |
291 | if (event->command_id != SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED) |
292 | return 0; /* Return "unhandled". */ |
293 | |
294 | if (event->length < 1) |
295 | dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n" , event->length); |
296 | |
297 | schedule_work(work: &sw->update_work); |
298 | return SSAM_NOTIF_HANDLED; |
299 | } |
300 | |
301 | static const struct ssam_tablet_sw_desc ssam_kip_sw_desc = { |
302 | .dev = { |
303 | .name = "Microsoft Surface KIP Tablet Mode Switch" , |
304 | .phys = "ssam/01:0e:01:00:01/input0" , |
305 | }, |
306 | .ops = { |
307 | .notify = ssam_kip_sw_notif, |
308 | .get_state = ssam_kip_get_cover_state, |
309 | .state_name = ssam_kip_cover_state_name, |
310 | .state_is_tablet_mode = ssam_kip_cover_state_is_tablet_mode, |
311 | }, |
312 | .event = { |
313 | .reg = SSAM_EVENT_REGISTRY_SAM, |
314 | .id = { |
315 | .target_category = SSAM_SSH_TC_KIP, |
316 | .instance = 0, |
317 | }, |
318 | .mask = SSAM_EVENT_MASK_TARGET, |
319 | }, |
320 | }; |
321 | |
322 | |
323 | /* -- SSAM POS tablet switch implementation. -------------------------------- */ |
324 | |
325 | static bool tablet_mode_in_slate_state = true; |
326 | module_param(tablet_mode_in_slate_state, bool, 0644); |
327 | MODULE_PARM_DESC(tablet_mode_in_slate_state, "Enable tablet mode in slate device posture, default is 'true'" ); |
328 | |
329 | #define SSAM_EVENT_POS_CID_POSTURE_CHANGED 0x03 |
330 | #define SSAM_POS_MAX_SOURCES 4 |
331 | |
332 | enum ssam_pos_source_id { |
333 | SSAM_POS_SOURCE_COVER = 0x00, |
334 | SSAM_POS_SOURCE_SLS = 0x03, |
335 | }; |
336 | |
337 | enum ssam_pos_state_cover { |
338 | SSAM_POS_COVER_DISCONNECTED = 0x01, |
339 | SSAM_POS_COVER_CLOSED = 0x02, |
340 | SSAM_POS_COVER_LAPTOP = 0x03, |
341 | SSAM_POS_COVER_FOLDED_CANVAS = 0x04, |
342 | SSAM_POS_COVER_FOLDED_BACK = 0x05, |
343 | SSAM_POS_COVER_BOOK = 0x06, |
344 | }; |
345 | |
346 | enum ssam_pos_state_sls { |
347 | SSAM_POS_SLS_LID_CLOSED = 0x00, |
348 | SSAM_POS_SLS_LAPTOP = 0x01, |
349 | SSAM_POS_SLS_SLATE = 0x02, |
350 | SSAM_POS_SLS_TABLET = 0x03, |
351 | }; |
352 | |
353 | struct ssam_sources_list { |
354 | __le32 count; |
355 | __le32 id[SSAM_POS_MAX_SOURCES]; |
356 | } __packed; |
357 | |
358 | static const char *ssam_pos_state_name_cover(struct ssam_tablet_sw *sw, u32 state) |
359 | { |
360 | switch (state) { |
361 | case SSAM_POS_COVER_DISCONNECTED: |
362 | return "disconnected" ; |
363 | |
364 | case SSAM_POS_COVER_CLOSED: |
365 | return "closed" ; |
366 | |
367 | case SSAM_POS_COVER_LAPTOP: |
368 | return "laptop" ; |
369 | |
370 | case SSAM_POS_COVER_FOLDED_CANVAS: |
371 | return "folded-canvas" ; |
372 | |
373 | case SSAM_POS_COVER_FOLDED_BACK: |
374 | return "folded-back" ; |
375 | |
376 | case SSAM_POS_COVER_BOOK: |
377 | return "book" ; |
378 | |
379 | default: |
380 | dev_warn(&sw->sdev->dev, "unknown device posture for type-cover: %u\n" , state); |
381 | return "<unknown>" ; |
382 | } |
383 | } |
384 | |
385 | static const char *ssam_pos_state_name_sls(struct ssam_tablet_sw *sw, u32 state) |
386 | { |
387 | switch (state) { |
388 | case SSAM_POS_SLS_LID_CLOSED: |
389 | return "closed" ; |
390 | |
391 | case SSAM_POS_SLS_LAPTOP: |
392 | return "laptop" ; |
393 | |
394 | case SSAM_POS_SLS_SLATE: |
395 | return "slate" ; |
396 | |
397 | case SSAM_POS_SLS_TABLET: |
398 | return "tablet" ; |
399 | |
400 | default: |
401 | dev_warn(&sw->sdev->dev, "unknown device posture for SLS: %u\n" , state); |
402 | return "<unknown>" ; |
403 | } |
404 | } |
405 | |
406 | static const char *ssam_pos_state_name(struct ssam_tablet_sw *sw, |
407 | const struct ssam_tablet_sw_state *state) |
408 | { |
409 | switch (state->source) { |
410 | case SSAM_POS_SOURCE_COVER: |
411 | return ssam_pos_state_name_cover(sw, state: state->state); |
412 | |
413 | case SSAM_POS_SOURCE_SLS: |
414 | return ssam_pos_state_name_sls(sw, state: state->state); |
415 | |
416 | default: |
417 | dev_warn(&sw->sdev->dev, "unknown device posture source: %u\n" , state->source); |
418 | return "<unknown>" ; |
419 | } |
420 | } |
421 | |
422 | static bool ssam_pos_state_is_tablet_mode_cover(struct ssam_tablet_sw *sw, u32 state) |
423 | { |
424 | switch (state) { |
425 | case SSAM_POS_COVER_DISCONNECTED: |
426 | case SSAM_POS_COVER_FOLDED_CANVAS: |
427 | case SSAM_POS_COVER_FOLDED_BACK: |
428 | case SSAM_POS_COVER_BOOK: |
429 | return true; |
430 | |
431 | case SSAM_POS_COVER_CLOSED: |
432 | case SSAM_POS_COVER_LAPTOP: |
433 | return false; |
434 | |
435 | default: |
436 | dev_warn(&sw->sdev->dev, "unknown device posture for type-cover: %u\n" , state); |
437 | return true; |
438 | } |
439 | } |
440 | |
441 | static bool ssam_pos_state_is_tablet_mode_sls(struct ssam_tablet_sw *sw, u32 state) |
442 | { |
443 | switch (state) { |
444 | case SSAM_POS_SLS_LAPTOP: |
445 | case SSAM_POS_SLS_LID_CLOSED: |
446 | return false; |
447 | |
448 | case SSAM_POS_SLS_SLATE: |
449 | return tablet_mode_in_slate_state; |
450 | |
451 | case SSAM_POS_SLS_TABLET: |
452 | return true; |
453 | |
454 | default: |
455 | dev_warn(&sw->sdev->dev, "unknown device posture for SLS: %u\n" , state); |
456 | return true; |
457 | } |
458 | } |
459 | |
460 | static bool ssam_pos_state_is_tablet_mode(struct ssam_tablet_sw *sw, |
461 | const struct ssam_tablet_sw_state *state) |
462 | { |
463 | switch (state->source) { |
464 | case SSAM_POS_SOURCE_COVER: |
465 | return ssam_pos_state_is_tablet_mode_cover(sw, state: state->state); |
466 | |
467 | case SSAM_POS_SOURCE_SLS: |
468 | return ssam_pos_state_is_tablet_mode_sls(sw, state: state->state); |
469 | |
470 | default: |
471 | dev_warn(&sw->sdev->dev, "unknown device posture source: %u\n" , state->source); |
472 | return true; |
473 | } |
474 | } |
475 | |
476 | static int ssam_pos_get_sources_list(struct ssam_tablet_sw *sw, struct ssam_sources_list *sources) |
477 | { |
478 | struct ssam_request rqst; |
479 | struct ssam_response rsp; |
480 | int status; |
481 | |
482 | rqst.target_category = SSAM_SSH_TC_POS; |
483 | rqst.target_id = SSAM_SSH_TID_SAM; |
484 | rqst.command_id = 0x01; |
485 | rqst.instance_id = 0x00; |
486 | rqst.flags = SSAM_REQUEST_HAS_RESPONSE; |
487 | rqst.length = 0; |
488 | rqst.payload = NULL; |
489 | |
490 | rsp.capacity = sizeof(*sources); |
491 | rsp.length = 0; |
492 | rsp.pointer = (u8 *)sources; |
493 | |
494 | status = ssam_retry(ssam_request_do_sync_onstack, sw->sdev->ctrl, &rqst, &rsp, 0); |
495 | if (status) |
496 | return status; |
497 | |
498 | /* We need at least the 'sources->count' field. */ |
499 | if (rsp.length < sizeof(__le32)) { |
500 | dev_err(&sw->sdev->dev, "received source list response is too small\n" ); |
501 | return -EPROTO; |
502 | } |
503 | |
504 | /* Make sure 'sources->count' matches with the response length. */ |
505 | if (get_unaligned_le32(p: &sources->count) * sizeof(__le32) + sizeof(__le32) != rsp.length) { |
506 | dev_err(&sw->sdev->dev, "mismatch between number of sources and response size\n" ); |
507 | return -EPROTO; |
508 | } |
509 | |
510 | return 0; |
511 | } |
512 | |
513 | static int ssam_pos_get_source(struct ssam_tablet_sw *sw, u32 *source_id) |
514 | { |
515 | struct ssam_sources_list sources = {}; |
516 | int status; |
517 | |
518 | status = ssam_pos_get_sources_list(sw, sources: &sources); |
519 | if (status) |
520 | return status; |
521 | |
522 | if (get_unaligned_le32(p: &sources.count) == 0) { |
523 | dev_err(&sw->sdev->dev, "no posture sources found\n" ); |
524 | return -ENODEV; |
525 | } |
526 | |
527 | /* |
528 | * We currently don't know what to do with more than one posture |
529 | * source. At the moment, only one source seems to be used/provided. |
530 | * The WARN_ON() here should hopefully let us know quickly once there |
531 | * is a device that provides multiple sources, at which point we can |
532 | * then try to figure out how to handle them. |
533 | */ |
534 | WARN_ON(get_unaligned_le32(&sources.count) > 1); |
535 | |
536 | *source_id = get_unaligned_le32(p: &sources.id[0]); |
537 | return 0; |
538 | } |
539 | |
540 | SSAM_DEFINE_SYNC_REQUEST_WR(__ssam_pos_get_posture_for_source, __le32, __le32, { |
541 | .target_category = SSAM_SSH_TC_POS, |
542 | .target_id = SSAM_SSH_TID_SAM, |
543 | .command_id = 0x02, |
544 | .instance_id = 0x00, |
545 | }); |
546 | |
547 | static int ssam_pos_get_posture_for_source(struct ssam_tablet_sw *sw, u32 source_id, u32 *posture) |
548 | { |
549 | __le32 source_le = cpu_to_le32(source_id); |
550 | __le32 rspval_le = 0; |
551 | int status; |
552 | |
553 | status = ssam_retry(__ssam_pos_get_posture_for_source, sw->sdev->ctrl, |
554 | &source_le, &rspval_le); |
555 | if (status) |
556 | return status; |
557 | |
558 | *posture = le32_to_cpu(rspval_le); |
559 | return 0; |
560 | } |
561 | |
562 | static int ssam_pos_get_posture(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state) |
563 | { |
564 | u32 source_id; |
565 | u32 source_state; |
566 | int status; |
567 | |
568 | status = ssam_pos_get_source(sw, source_id: &source_id); |
569 | if (status) { |
570 | dev_err(&sw->sdev->dev, "failed to get posture source ID: %d\n" , status); |
571 | return status; |
572 | } |
573 | |
574 | status = ssam_pos_get_posture_for_source(sw, source_id, posture: &source_state); |
575 | if (status) { |
576 | dev_err(&sw->sdev->dev, "failed to get posture value for source %u: %d\n" , |
577 | source_id, status); |
578 | return status; |
579 | } |
580 | |
581 | state->source = source_id; |
582 | state->state = source_state; |
583 | return 0; |
584 | } |
585 | |
586 | static u32 ssam_pos_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event) |
587 | { |
588 | struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif); |
589 | |
590 | if (event->command_id != SSAM_EVENT_POS_CID_POSTURE_CHANGED) |
591 | return 0; /* Return "unhandled". */ |
592 | |
593 | if (event->length != sizeof(__le32) * 3) |
594 | dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n" , event->length); |
595 | |
596 | schedule_work(work: &sw->update_work); |
597 | return SSAM_NOTIF_HANDLED; |
598 | } |
599 | |
600 | static const struct ssam_tablet_sw_desc ssam_pos_sw_desc = { |
601 | .dev = { |
602 | .name = "Microsoft Surface POS Tablet Mode Switch" , |
603 | .phys = "ssam/01:26:01:00:01/input0" , |
604 | }, |
605 | .ops = { |
606 | .notify = ssam_pos_sw_notif, |
607 | .get_state = ssam_pos_get_posture, |
608 | .state_name = ssam_pos_state_name, |
609 | .state_is_tablet_mode = ssam_pos_state_is_tablet_mode, |
610 | }, |
611 | .event = { |
612 | .reg = SSAM_EVENT_REGISTRY_SAM, |
613 | .id = { |
614 | .target_category = SSAM_SSH_TC_POS, |
615 | .instance = 0, |
616 | }, |
617 | .mask = SSAM_EVENT_MASK_TARGET, |
618 | }, |
619 | }; |
620 | |
621 | |
622 | /* -- Driver registration. -------------------------------------------------- */ |
623 | |
624 | static const struct ssam_device_id ssam_tablet_sw_match[] = { |
625 | { SSAM_SDEV(KIP, SAM, 0x00, 0x01), (unsigned long)&ssam_kip_sw_desc }, |
626 | { SSAM_SDEV(POS, SAM, 0x00, 0x01), (unsigned long)&ssam_pos_sw_desc }, |
627 | { }, |
628 | }; |
629 | MODULE_DEVICE_TABLE(ssam, ssam_tablet_sw_match); |
630 | |
631 | static struct ssam_device_driver ssam_tablet_sw_driver = { |
632 | .probe = ssam_tablet_sw_probe, |
633 | .remove = ssam_tablet_sw_remove, |
634 | .match_table = ssam_tablet_sw_match, |
635 | .driver = { |
636 | .name = "surface_aggregator_tablet_mode_switch" , |
637 | .probe_type = PROBE_PREFER_ASYNCHRONOUS, |
638 | .pm = &ssam_tablet_sw_pm_ops, |
639 | }, |
640 | }; |
641 | module_ssam_device_driver(ssam_tablet_sw_driver); |
642 | |
643 | MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>" ); |
644 | MODULE_DESCRIPTION("Tablet mode switch driver for Surface devices using the Surface Aggregator Module" ); |
645 | MODULE_LICENSE("GPL" ); |
646 | |