diff mbox series

[2/4] platform/surface: Add KIP/POS tablet-mode switch driver

Message ID 20220624183642.910893-3-luzmaximilian@gmail.com (mailing list archive)
State Accepted, archived
Headers show
Series platform/surface: Add support for tablet mode switch via Surface Aggregator Module | expand

Commit Message

Maximilian Luz June 24, 2022, 6:36 p.m. UTC
Add a driver providing a tablet-mode switch input device for Microsoft
Surface devices using the Surface Aggregator KIP subsystem (to manage
detachable peripherals) or POS subsystem (to obtain device posture
information).

The KIP (full name unknown, abbreviation found through reverse
engineering) subsystem is used on the Surface Pro 8 and Surface Pro X to
manage the keyboard cover. Among other things, it provides information
on the positioning (posture) of the cover (closed, laptop-style,
detached, folded-back, ...), which can be used to implement an input
device providing the SW_TABLET_MODE event. Similarly, the POS (posture
information) subsystem provides such information on the Surface Laptop
Studio, with the difference being that the keyboard is not detachable.

As implementing the tablet-mode switch for both subsystems is largely
similar, the driver proposed in this commit, in large, acts as a generic
tablet mode switch driver framework for the Surface Aggregator Module.
Specific implementations using this framework are provided for the KIP
and POS subsystems, adding tablet-mode switch support to the
aforementioned devices.

A few more notes on the Surface Laptop Studio:

A peculiarity of the Surface Laptop Studio is its "slate/tent" mode
(symbolized: user> _/\). In this mode, the screen covers the keyboard
but leaves the touchpad exposed. This is essentially a mode in-between
tablet and laptop, and it is debatable whether tablet-mode should be
enabled in this mode. We therefore let the user decide this via a module
parameter.

In particular, tablet-mode may bring up the on-screen touch keyboard
more easily, which would be desirable in this mode. However, some
user-space software currently also decides to disable keyboard and, more
importantly, touchpad input, while the touchpad is still accessible in
the "slate/tent" mode. Furthermore, this mode shares its identifier with
"slate/flipped" mode where the screen is flipped 180° and the keyboard
points away from the user (symbolized: user> /_). In this mode we would
like to enable auto-rotation, something that user-space software may
only do when tablet-mode is enabled. We therefore default to the
slate-mode enabling the tablet-mode switch.

Signed-off-by: Maximilian Luz <luzmaximilian@gmail.com>
---
 .../sysfs-bus-surface_aggregator-tabletsw     |  57 ++
 MAINTAINERS                                   |   6 +
 drivers/platform/surface/Kconfig              |  23 +
 drivers/platform/surface/Makefile             |   1 +
 .../surface/surface_aggregator_tabletsw.c     | 533 ++++++++++++++++++
 5 files changed, 620 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-bus-surface_aggregator-tabletsw
 create mode 100644 drivers/platform/surface/surface_aggregator_tabletsw.c

Comments

kernel test robot June 25, 2022, 2:59 a.m. UTC | #1
Hi Maximilian,

I love your patch! Yet something to improve:

[auto build test ERROR on next-20220624]
[cannot apply to linus/master platform-drivers-x86/for-next v5.19-rc3 v5.19-rc2 v5.19-rc1 v5.19-rc3]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch]

url:    https://github.com/intel-lab-lkp/linux/commits/Maximilian-Luz/platform-surface-Add-support-for-tablet-mode-switch-via-Surface-Aggregator-Module/20220625-023751
base:    2f9cb3d3bd73fc2225d66aa8fcffb632ed3eb235
config: i386-allyesconfig
compiler: gcc-11 (Debian 11.3.0-3) 11.3.0
reproduce (this is a W=1 build):
        # https://github.com/intel-lab-lkp/linux/commit/190ffdd2194f4c5a5110d6a08f303c47e2da31ba
        git remote add linux-review https://github.com/intel-lab-lkp/linux
        git fetch --no-tags linux-review Maximilian-Luz/platform-surface-Add-support-for-tablet-mode-switch-via-Surface-Aggregator-Module/20220625-023751
        git checkout 190ffdd2194f4c5a5110d6a08f303c47e2da31ba
        # save the config file
        mkdir build_dir && cp config build_dir/.config
        make W=1 O=build_dir ARCH=i386 SHELL=/bin/bash

If you fix the issue, kindly add following tag where applicable
Reported-by: kernel test robot <lkp@intel.com>

All errors (new ones prefixed by >>):

   drivers/platform/surface/surface_aggregator_tabletsw.c: In function 'ssam_pos_get_sources_list':
>> drivers/platform/surface/surface_aggregator_tabletsw.c:373:32: error: 'SSAM_SSH_TC_POS' undeclared (first use in this function); did you mean 'SSAM_SSH_TC_PoM'?
     373 |         rqst.target_category = SSAM_SSH_TC_POS;
         |                                ^~~~~~~~~~~~~~~
         |                                SSAM_SSH_TC_PoM
   drivers/platform/surface/surface_aggregator_tabletsw.c:373:32: note: each undeclared identifier is reported only once for each function it appears in
   In file included from drivers/platform/surface/surface_aggregator_tabletsw.c:15:
   drivers/platform/surface/surface_aggregator_tabletsw.c: In function '__ssam_pos_get_posture_for_source':
   drivers/platform/surface/surface_aggregator_tabletsw.c:432:28: error: 'SSAM_SSH_TC_POS' undeclared (first use in this function); did you mean 'SSAM_SSH_TC_PoM'?
     432 |         .target_category = SSAM_SSH_TC_POS,
         |                            ^~~~~~~~~~~~~~~
   include/linux/surface_aggregator/controller.h:500:72: note: in definition of macro 'SSAM_DEFINE_SYNC_REQUEST_WR'
     500 |                 struct ssam_request_spec s = (struct ssam_request_spec)spec;    \
         |                                                                        ^~~~
   drivers/platform/surface/surface_aggregator_tabletsw.c: At top level:
>> drivers/platform/surface/surface_aggregator_tabletsw.c:502:44: error: 'SSAM_SSH_TC_POS' undeclared here (not in a function); did you mean 'SSAM_SSH_TC_PoM'?
     502 |                         .target_category = SSAM_SSH_TC_POS,
         |                                            ^~~~~~~~~~~~~~~
         |                                            SSAM_SSH_TC_PoM


vim +373 drivers/platform/surface/surface_aggregator_tabletsw.c

   366	
   367	static int ssam_pos_get_sources_list(struct ssam_tablet_sw *sw, struct ssam_sources_list *sources)
   368	{
   369		struct ssam_request rqst;
   370		struct ssam_response rsp;
   371		int status;
   372	
 > 373		rqst.target_category = SSAM_SSH_TC_POS;
   374		rqst.target_id = 0x01;
   375		rqst.command_id = 0x01;
   376		rqst.instance_id = 0x00;
   377		rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
   378		rqst.length = 0;
   379		rqst.payload = NULL;
   380	
   381		rsp.capacity = sizeof(*sources);
   382		rsp.length = 0;
   383		rsp.pointer = (u8 *)sources;
   384	
   385		status = ssam_retry(ssam_request_sync_onstack, sw->sdev->ctrl, &rqst, &rsp, 0);
   386		if (status)
   387			return status;
   388	
   389		/* We need at least the 'sources->count' field. */
   390		if (rsp.length < sizeof(__le32)) {
   391			dev_err(&sw->sdev->dev, "received source list response is too small\n");
   392			return -EPROTO;
   393		}
   394	
   395		/* Make sure 'sources->count' matches with the response length. */
   396		if (get_unaligned_le32(&sources->count) * sizeof(__le32) + sizeof(__le32) != rsp.length) {
   397			dev_err(&sw->sdev->dev, "mismatch between number of sources and response size\n");
   398			return -EPROTO;
   399		}
   400	
   401		return 0;
   402	}
   403	
   404	static int ssam_pos_get_source(struct ssam_tablet_sw *sw, u32 *source_id)
   405	{
   406		struct ssam_sources_list sources = {};
   407		int status;
   408	
   409		status = ssam_pos_get_sources_list(sw, &sources);
   410		if (status)
   411			return status;
   412	
   413		if (sources.count == 0) {
   414			dev_err(&sw->sdev->dev, "no posture sources found\n");
   415			return -ENODEV;
   416		}
   417	
   418		/*
   419		 * We currently don't know what to do with more than one posture
   420		 * source. At the moment, only one source seems to be used/provided.
   421		 * The WARN_ON() here should hopefully let us know quickly once there
   422		 * is a device that provides multiple sources, at which point we can
   423		 * then try to figure out how to handle them.
   424		 */
   425		WARN_ON(sources.count > 1);
   426	
   427		*source_id = get_unaligned_le32(&sources.id[0]);
   428		return 0;
   429	}
   430	
   431	SSAM_DEFINE_SYNC_REQUEST_WR(__ssam_pos_get_posture_for_source, __le32, __le32, {
   432		.target_category = SSAM_SSH_TC_POS,
   433		.target_id       = 0x01,
   434		.command_id      = 0x02,
   435		.instance_id     = 0x00,
   436	});
   437	
   438	static int ssam_pos_get_posture_for_source(struct ssam_tablet_sw *sw, u32 source_id, u32 *posture)
   439	{
   440		__le32 source_le = cpu_to_le32(source_id);
   441		__le32 rspval_le = 0;
   442		int status;
   443	
   444		status = ssam_retry(__ssam_pos_get_posture_for_source, sw->sdev->ctrl,
   445				    &source_le, &rspval_le);
   446		if (status)
   447			return status;
   448	
   449		*posture = le32_to_cpu(rspval_le);
   450		return 0;
   451	}
   452	
   453	static int ssam_pos_get_posture(struct ssam_tablet_sw *sw, u32 *state)
   454	{
   455		u32 source_id;
   456		int status;
   457	
   458		status = ssam_pos_get_source(sw, &source_id);
   459		if (status) {
   460			dev_err(&sw->sdev->dev, "failed to get posture source ID: %d\n", status);
   461			return status;
   462		}
   463	
   464		status = ssam_pos_get_posture_for_source(sw, source_id, state);
   465		if (status) {
   466			dev_err(&sw->sdev->dev, "failed to get posture value for source %u: %d\n",
   467				source_id, status);
   468			return status;
   469		}
   470	
   471		return 0;
   472	}
   473	
   474	static u32 ssam_pos_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
   475	{
   476		struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif);
   477	
   478		if (event->command_id != SSAM_EVENT_POS_CID_POSTURE_CHANGED)
   479			return 0;	/* Return "unhandled". */
   480	
   481		if (event->length != sizeof(__le32) * 3)
   482			dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n", event->length);
   483	
   484		schedule_work(&sw->update_work);
   485		return SSAM_NOTIF_HANDLED;
   486	}
   487	
   488	static const struct ssam_tablet_sw_desc ssam_pos_sw_desc = {
   489		.dev = {
   490			.name = "Microsoft Surface POS Tablet Mode Switch",
   491			.phys = "ssam/01:26:01:00:01/input0",
   492		},
   493		.ops = {
   494			.notify = ssam_pos_sw_notif,
   495			.get_state = ssam_pos_get_posture,
   496			.state_name = ssam_pos_state_name,
   497			.state_is_tablet_mode = ssam_pos_state_is_tablet_mode,
   498		},
   499		.event = {
   500			.reg = SSAM_EVENT_REGISTRY_SAM,
   501			.id = {
 > 502				.target_category = SSAM_SSH_TC_POS,
   503				.instance = 0,
   504			},
   505			.mask = SSAM_EVENT_MASK_TARGET,
   506		},
   507	};
   508	
   509
Maximilian Luz June 25, 2022, 12:58 p.m. UTC | #2
On 6/25/22 04:59, kernel test robot wrote:
> Hi Maximilian,
> 
> I love your patch! Yet something to improve:
> 
> [auto build test ERROR on next-20220624]
> [cannot apply to linus/master platform-drivers-x86/for-next v5.19-rc3 v5.19-rc2 v5.19-rc1 v5.19-rc3]
> [If your patch is applied to the wrong git tree, kindly drop us a note.
> And when submitting patch, we suggest to use '--base' as documented in
> https://git-scm.com/docs/git-format-patch]
> 
> url:    https://github.com/intel-lab-lkp/linux/commits/Maximilian-Luz/platform-surface-Add-support-for-tablet-mode-switch-via-Surface-Aggregator-Module/20220625-023751
> base:    2f9cb3d3bd73fc2225d66aa8fcffb632ed3eb235
> config: i386-allyesconfig
> compiler: gcc-11 (Debian 11.3.0-3) 11.3.0
> reproduce (this is a W=1 build):
>          # https://github.com/intel-lab-lkp/linux/commit/190ffdd2194f4c5a5110d6a08f303c47e2da31ba
>          git remote add linux-review https://github.com/intel-lab-lkp/linux
>          git fetch --no-tags linux-review Maximilian-Luz/platform-surface-Add-support-for-tablet-mode-switch-via-Surface-Aggregator-Module/20220625-023751
>          git checkout 190ffdd2194f4c5a5110d6a08f303c47e2da31ba
>          # save the config file
>          mkdir build_dir && cp config build_dir/.config
>          make W=1 O=build_dir ARCH=i386 SHELL=/bin/bash
> 
> If you fix the issue, kindly add following tag where applicable
> Reported-by: kernel test robot <lkp@intel.com>
> 
> All errors (new ones prefixed by >>):
> 
>     drivers/platform/surface/surface_aggregator_tabletsw.c: In function 'ssam_pos_get_sources_list':
>>> drivers/platform/surface/surface_aggregator_tabletsw.c:373:32: error: 'SSAM_SSH_TC_POS' undeclared (first use in this function); did you mean 'SSAM_SSH_TC_PoM'?
>       373 |         rqst.target_category = SSAM_SSH_TC_POS;
>           |                                ^~~~~~~~~~~~~~~
>           |                                SSAM_SSH_TC_PoM

Ah, I forgot to mention. This depends on "platform/surface: aggregator:
Reserve more event- and target-categories" which is already in
pdx86/review-hans as commit e244a46a529a. That commit adds the
`SSAM_SSH_TC_POS` symbol, so with it, this patch should compile.

Regards,
Max
Hans de Goede July 2, 2022, 9:05 a.m. UTC | #3
Hi,

On 6/24/22 20:36, Maximilian Luz wrote:
> Add a driver providing a tablet-mode switch input device for Microsoft
> Surface devices using the Surface Aggregator KIP subsystem (to manage
> detachable peripherals) or POS subsystem (to obtain device posture
> information).
> 
> The KIP (full name unknown, abbreviation found through reverse
> engineering) subsystem is used on the Surface Pro 8 and Surface Pro X to
> manage the keyboard cover. Among other things, it provides information
> on the positioning (posture) of the cover (closed, laptop-style,
> detached, folded-back, ...), which can be used to implement an input
> device providing the SW_TABLET_MODE event. Similarly, the POS (posture
> information) subsystem provides such information on the Surface Laptop
> Studio, with the difference being that the keyboard is not detachable.
> 
> As implementing the tablet-mode switch for both subsystems is largely
> similar, the driver proposed in this commit, in large, acts as a generic
> tablet mode switch driver framework for the Surface Aggregator Module.
> Specific implementations using this framework are provided for the KIP
> and POS subsystems, adding tablet-mode switch support to the
> aforementioned devices.
> 
> A few more notes on the Surface Laptop Studio:
> 
> A peculiarity of the Surface Laptop Studio is its "slate/tent" mode
> (symbolized: user> _/\). In this mode, the screen covers the keyboard
> but leaves the touchpad exposed. This is essentially a mode in-between
> tablet and laptop, and it is debatable whether tablet-mode should be
> enabled in this mode. We therefore let the user decide this via a module
> parameter.
> 
> In particular, tablet-mode may bring up the on-screen touch keyboard
> more easily, which would be desirable in this mode. However, some
> user-space software currently also decides to disable keyboard and, more
> importantly, touchpad input, while the touchpad is still accessible in
> the "slate/tent" mode. Furthermore, this mode shares its identifier with
> "slate/flipped" mode where the screen is flipped 180° and the keyboard
> points away from the user (symbolized: user> /_). In this mode we would
> like to enable auto-rotation, something that user-space software may
> only do when tablet-mode is enabled. We therefore default to the
> slate-mode enabling the tablet-mode switch.

Note that you could add a libinput quirk to ignore the tablet-switch
for keybd/touchpad event suprression since when not used the keyboard /
touchpad are covered I expect them to not send false input events so
this should be fine. Then the touchpad will keep working in "slate/tent"
mode while still enabling the onscreen-kbd since you are reporting
sw_tablet_mode=1.

The onscreen-kbd and auto-rotation enabling/disabling lives outside of
ibinput. Although gnome-shell does use libinput to read the touchpad and
currently I think the only way to get libinput to ignore the tablet-mode-sw
is to mark it as unreliable, which I guess might also stop it from reporting
it to gnome-shell.

Another option would be to mark the touchpad as "external" with a libinput
quirk (so not build into the hw) then it also won't suppress events based
on the tablet-mode-sw state. TL;DR: this can be fixed with a quirk on
the libinput side, how best to do that needs to be discussed with the libinput
devs.

And once solved at the libinput side, you probably don't need
the module-parameter. Note I'm fine with keeping it, but feel free to
submit a follow-up patch to remove it, explaining that this is fixed
in libinput now...

Regards,

Hans




> 
> Signed-off-by: Maximilian Luz <luzmaximilian@gmail.com>
> ---
>  .../sysfs-bus-surface_aggregator-tabletsw     |  57 ++
>  MAINTAINERS                                   |   6 +
>  drivers/platform/surface/Kconfig              |  23 +
>  drivers/platform/surface/Makefile             |   1 +
>  .../surface/surface_aggregator_tabletsw.c     | 533 ++++++++++++++++++
>  5 files changed, 620 insertions(+)
>  create mode 100644 Documentation/ABI/testing/sysfs-bus-surface_aggregator-tabletsw
>  create mode 100644 drivers/platform/surface/surface_aggregator_tabletsw.c
> 
> diff --git a/Documentation/ABI/testing/sysfs-bus-surface_aggregator-tabletsw b/Documentation/ABI/testing/sysfs-bus-surface_aggregator-tabletsw
> new file mode 100644
> index 000000000000..74cd9d754e60
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-bus-surface_aggregator-tabletsw
> @@ -0,0 +1,57 @@
> +What:		/sys/bus/surface_aggregator/devices/01:0e:01:00:01/state
> +Date:		July 2022
> +KernelVersion:	5.20
> +Contact:	Maximilian Luz <luzmaximilian@gmail.com>
> +Description:
> +		This attribute returns a string with the current type-cover
> +		or device posture, as indicated by the embedded controller.
> +		Currently returned posture states are:
> +
> +		- "disconnected": The type-cover has been disconnected.
> +
> +		- "closed": The type-cover has been folded closed and lies on
> +		  top of the display.
> +
> +		- "laptop": The type-cover is open and in laptop-mode, i.e.,
> +		  ready for normal use.
> +
> +		- "folded-canvas": The type-cover has been folded back
> +		  part-ways, but does not lie flush with the back side of the
> +		  device. In general, this means that the kick-stand is used
> +		  and extended atop of the cover.
> +
> +		- "folded-back": The type cover has been fully folded back and
> +		  lies flush with the back side of the device.
> +
> +		- "<unknown>": The current state is unknown to the driver, for
> +		  example due to newer as-of-yet unsupported hardware.
> +
> +		New states may be introduced with new hardware. Users therefore
> +		must not rely on this list of states being exhaustive and
> +		gracefully handle unknown states.
> +
> +What:		/sys/bus/surface_aggregator/devices/01:26:01:00:01/state
> +Date:		July 2022
> +KernelVersion:	5.20
> +Contact:	Maximilian Luz <luzmaximilian@gmail.com>
> +Description:
> +		This attribute returns a string with the current device posture, as indicated by the embedded controller. Currently
> +		returned posture states are:
> +
> +		- "closed": The lid of the device is closed.
> +
> +		- "laptop": The lid of the device is opened and the device
> +		  operates as a normal laptop.
> +
> +		- "slate": The screen covers the keyboard or has been flipped
> +		  back and the device operates mainly based on touch input.
> +
> +		- "tablet": The device operates as tablet and exclusively
> +		  relies on touch input (or external peripherals).
> +
> +		- "<unknown>": The current state is unknown to the driver, for
> +		  example due to newer as-of-yet unsupported hardware.
> +
> +		New states may be introduced with new hardware. Users therefore
> +		must not rely on this list of states being exhaustive and
> +		gracefully handle unknown states.
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 2a34deb24594..c6d8c0c6bf6e 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -13176,6 +13176,12 @@ F:	drivers/scsi/smartpqi/smartpqi*.[ch]
>  F:	include/linux/cciss*.h
>  F:	include/uapi/linux/cciss*.h
>  
> +MICROSOFT SURFACE AGGREGATOR TABLET-MODE SWITCH
> +M:	Maximilian Luz <luzmaximilian@gmail.com>
> +L:	platform-driver-x86@vger.kernel.org
> +S:	Maintained
> +F:	drivers/platform/surface/surface_aggregator_tablet_switch.c
> +
>  MICROSOFT SURFACE BATTERY AND AC DRIVERS
>  M:	Maximilian Luz <luzmaximilian@gmail.com>
>  L:	linux-pm@vger.kernel.org
> diff --git a/drivers/platform/surface/Kconfig b/drivers/platform/surface/Kconfig
> index eb79fbed8059..b152e930cc84 100644
> --- a/drivers/platform/surface/Kconfig
> +++ b/drivers/platform/surface/Kconfig
> @@ -99,6 +99,29 @@ config SURFACE_AGGREGATOR_REGISTRY
>  	  the respective client devices. Drivers for these devices still need to
>  	  be selected via the other options.
>  
> +config SURFACE_AGGREGATOR_TABLET_SWITCH
> +	tristate "Surface Aggregator Generic Tablet-Mode Switch Driver"
> +	depends on SURFACE_AGGREGATOR
> +	depends on SURFACE_AGGREGATOR_BUS
> +	depends on INPUT
> +	help
> +	  Provides a tablet-mode switch input device on Microsoft Surface models
> +	  using the KIP subsystem for detachable keyboards (e.g. keyboard covers)
> +	  or the POS subsystem for device/screen posture changes.
> +
> +	  The KIP subsystem is used on newer Surface generations to handle
> +	  detachable input peripherals, specifically the keyboard cover (containing
> +	  keyboard and touchpad) on the Surface Pro 8 and Surface Pro X. The POS
> +	  subsystem is used for device posture change notifications on the Surface
> +	  Laptop Studio. This module provides a driver to let user-space know when
> +	  the device should be considered in tablet-mode due to the keyboard cover
> +	  being detached or folded back (essentially signaling when the keyboard is
> +	  not available for input). It does so by creating a tablet-mode switch
> +	  input device, sending the standard SW_TABLET_MODE event on mode change.
> +
> +	  Select M or Y here, if you want to provide tablet-mode switch input
> +	  events on the Surface Pro 8, Surface Pro X, and Surface Laptop Studio.
> +
>  config SURFACE_DTX
>  	tristate "Surface DTX (Detachment System) Driver"
>  	depends on SURFACE_AGGREGATOR
> diff --git a/drivers/platform/surface/Makefile b/drivers/platform/surface/Makefile
> index 0fc9cd3e4dd9..18b27898543e 100644
> --- a/drivers/platform/surface/Makefile
> +++ b/drivers/platform/surface/Makefile
> @@ -10,6 +10,7 @@ obj-$(CONFIG_SURFACE_ACPI_NOTIFY)	+= surface_acpi_notify.o
>  obj-$(CONFIG_SURFACE_AGGREGATOR)	+= aggregator/
>  obj-$(CONFIG_SURFACE_AGGREGATOR_CDEV)	+= surface_aggregator_cdev.o
>  obj-$(CONFIG_SURFACE_AGGREGATOR_REGISTRY) += surface_aggregator_registry.o
> +obj-$(CONFIG_SURFACE_AGGREGATOR_TABLET_SWITCH) += surface_aggregator_tabletsw.o
>  obj-$(CONFIG_SURFACE_DTX)		+= surface_dtx.o
>  obj-$(CONFIG_SURFACE_GPE)		+= surface_gpe.o
>  obj-$(CONFIG_SURFACE_HOTPLUG)		+= surface_hotplug.o
> diff --git a/drivers/platform/surface/surface_aggregator_tabletsw.c b/drivers/platform/surface/surface_aggregator_tabletsw.c
> new file mode 100644
> index 000000000000..596ca6c80681
> --- /dev/null
> +++ b/drivers/platform/surface/surface_aggregator_tabletsw.c
> @@ -0,0 +1,533 @@
> +// 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,
> +};
> +
> +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";
> +
> +	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:
> +		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 (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(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");
Maximilian Luz July 3, 2022, 1:44 p.m. UTC | #4
On 7/2/22 11:05, Hans de Goede wrote:

[...]

>> A few more notes on the Surface Laptop Studio:
>>
>> A peculiarity of the Surface Laptop Studio is its "slate/tent" mode
>> (symbolized: user> _/\). In this mode, the screen covers the keyboard
>> but leaves the touchpad exposed. This is essentially a mode in-between
>> tablet and laptop, and it is debatable whether tablet-mode should be
>> enabled in this mode. We therefore let the user decide this via a module
>> parameter.
>>
>> In particular, tablet-mode may bring up the on-screen touch keyboard
>> more easily, which would be desirable in this mode. However, some
>> user-space software currently also decides to disable keyboard and, more
>> importantly, touchpad input, while the touchpad is still accessible in
>> the "slate/tent" mode. Furthermore, this mode shares its identifier with
>> "slate/flipped" mode where the screen is flipped 180° and the keyboard
>> points away from the user (symbolized: user> /_). In this mode we would
>> like to enable auto-rotation, something that user-space software may
>> only do when tablet-mode is enabled. We therefore default to the
>> slate-mode enabling the tablet-mode switch.
> 
> Note that you could add a libinput quirk to ignore the tablet-switch
> for keybd/touchpad event suprression since when not used the keyboard /
> touchpad are covered I expect them to not send false input events so
> this should be fine. Then the touchpad will keep working in "slate/tent"
> mode while still enabling the onscreen-kbd since you are reporting
> sw_tablet_mode=1.
> 
> The onscreen-kbd and auto-rotation enabling/disabling lives outside of
> ibinput. Although gnome-shell does use libinput to read the touchpad and
> currently I think the only way to get libinput to ignore the tablet-mode-sw
> is to mark it as unreliable, which I guess might also stop it from reporting
> it to gnome-shell.
> 
> Another option would be to mark the touchpad as "external" with a libinput
> quirk (so not build into the hw) then it also won't suppress events based
> on the tablet-mode-sw state. TL;DR: this can be fixed with a quirk on
> the libinput side, how best to do that needs to be discussed with the libinput
> devs.
> 
> And once solved at the libinput side, you probably don't need
> the module-parameter. Note I'm fine with keeping it, but feel free to
> submit a follow-up patch to remove it, explaining that this is fixed
> in libinput now...

Thanks for that hint!

Turns out libinput has a quirk for that (ModelTabletModeNoSuspend). I'm
working with someone actually owning a SLS and we will submit an entry
to the libinput hwdb once we've fleshed that out.

I'll yet have to think some more about whether it makes sense to keep
the module parameter or not. I think ideally there would be a way to
notify user-space for more states than just tablet/laptop, but that is
very device specific.

Regards,
Max
diff mbox series

Patch

diff --git a/Documentation/ABI/testing/sysfs-bus-surface_aggregator-tabletsw b/Documentation/ABI/testing/sysfs-bus-surface_aggregator-tabletsw
new file mode 100644
index 000000000000..74cd9d754e60
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-bus-surface_aggregator-tabletsw
@@ -0,0 +1,57 @@ 
+What:		/sys/bus/surface_aggregator/devices/01:0e:01:00:01/state
+Date:		July 2022
+KernelVersion:	5.20
+Contact:	Maximilian Luz <luzmaximilian@gmail.com>
+Description:
+		This attribute returns a string with the current type-cover
+		or device posture, as indicated by the embedded controller.
+		Currently returned posture states are:
+
+		- "disconnected": The type-cover has been disconnected.
+
+		- "closed": The type-cover has been folded closed and lies on
+		  top of the display.
+
+		- "laptop": The type-cover is open and in laptop-mode, i.e.,
+		  ready for normal use.
+
+		- "folded-canvas": The type-cover has been folded back
+		  part-ways, but does not lie flush with the back side of the
+		  device. In general, this means that the kick-stand is used
+		  and extended atop of the cover.
+
+		- "folded-back": The type cover has been fully folded back and
+		  lies flush with the back side of the device.
+
+		- "<unknown>": The current state is unknown to the driver, for
+		  example due to newer as-of-yet unsupported hardware.
+
+		New states may be introduced with new hardware. Users therefore
+		must not rely on this list of states being exhaustive and
+		gracefully handle unknown states.
+
+What:		/sys/bus/surface_aggregator/devices/01:26:01:00:01/state
+Date:		July 2022
+KernelVersion:	5.20
+Contact:	Maximilian Luz <luzmaximilian@gmail.com>
+Description:
+		This attribute returns a string with the current device posture, as indicated by the embedded controller. Currently
+		returned posture states are:
+
+		- "closed": The lid of the device is closed.
+
+		- "laptop": The lid of the device is opened and the device
+		  operates as a normal laptop.
+
+		- "slate": The screen covers the keyboard or has been flipped
+		  back and the device operates mainly based on touch input.
+
+		- "tablet": The device operates as tablet and exclusively
+		  relies on touch input (or external peripherals).
+
+		- "<unknown>": The current state is unknown to the driver, for
+		  example due to newer as-of-yet unsupported hardware.
+
+		New states may be introduced with new hardware. Users therefore
+		must not rely on this list of states being exhaustive and
+		gracefully handle unknown states.
diff --git a/MAINTAINERS b/MAINTAINERS
index 2a34deb24594..c6d8c0c6bf6e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13176,6 +13176,12 @@  F:	drivers/scsi/smartpqi/smartpqi*.[ch]
 F:	include/linux/cciss*.h
 F:	include/uapi/linux/cciss*.h
 
+MICROSOFT SURFACE AGGREGATOR TABLET-MODE SWITCH
+M:	Maximilian Luz <luzmaximilian@gmail.com>
+L:	platform-driver-x86@vger.kernel.org
+S:	Maintained
+F:	drivers/platform/surface/surface_aggregator_tablet_switch.c
+
 MICROSOFT SURFACE BATTERY AND AC DRIVERS
 M:	Maximilian Luz <luzmaximilian@gmail.com>
 L:	linux-pm@vger.kernel.org
diff --git a/drivers/platform/surface/Kconfig b/drivers/platform/surface/Kconfig
index eb79fbed8059..b152e930cc84 100644
--- a/drivers/platform/surface/Kconfig
+++ b/drivers/platform/surface/Kconfig
@@ -99,6 +99,29 @@  config SURFACE_AGGREGATOR_REGISTRY
 	  the respective client devices. Drivers for these devices still need to
 	  be selected via the other options.
 
+config SURFACE_AGGREGATOR_TABLET_SWITCH
+	tristate "Surface Aggregator Generic Tablet-Mode Switch Driver"
+	depends on SURFACE_AGGREGATOR
+	depends on SURFACE_AGGREGATOR_BUS
+	depends on INPUT
+	help
+	  Provides a tablet-mode switch input device on Microsoft Surface models
+	  using the KIP subsystem for detachable keyboards (e.g. keyboard covers)
+	  or the POS subsystem for device/screen posture changes.
+
+	  The KIP subsystem is used on newer Surface generations to handle
+	  detachable input peripherals, specifically the keyboard cover (containing
+	  keyboard and touchpad) on the Surface Pro 8 and Surface Pro X. The POS
+	  subsystem is used for device posture change notifications on the Surface
+	  Laptop Studio. This module provides a driver to let user-space know when
+	  the device should be considered in tablet-mode due to the keyboard cover
+	  being detached or folded back (essentially signaling when the keyboard is
+	  not available for input). It does so by creating a tablet-mode switch
+	  input device, sending the standard SW_TABLET_MODE event on mode change.
+
+	  Select M or Y here, if you want to provide tablet-mode switch input
+	  events on the Surface Pro 8, Surface Pro X, and Surface Laptop Studio.
+
 config SURFACE_DTX
 	tristate "Surface DTX (Detachment System) Driver"
 	depends on SURFACE_AGGREGATOR
diff --git a/drivers/platform/surface/Makefile b/drivers/platform/surface/Makefile
index 0fc9cd3e4dd9..18b27898543e 100644
--- a/drivers/platform/surface/Makefile
+++ b/drivers/platform/surface/Makefile
@@ -10,6 +10,7 @@  obj-$(CONFIG_SURFACE_ACPI_NOTIFY)	+= surface_acpi_notify.o
 obj-$(CONFIG_SURFACE_AGGREGATOR)	+= aggregator/
 obj-$(CONFIG_SURFACE_AGGREGATOR_CDEV)	+= surface_aggregator_cdev.o
 obj-$(CONFIG_SURFACE_AGGREGATOR_REGISTRY) += surface_aggregator_registry.o
+obj-$(CONFIG_SURFACE_AGGREGATOR_TABLET_SWITCH) += surface_aggregator_tabletsw.o
 obj-$(CONFIG_SURFACE_DTX)		+= surface_dtx.o
 obj-$(CONFIG_SURFACE_GPE)		+= surface_gpe.o
 obj-$(CONFIG_SURFACE_HOTPLUG)		+= surface_hotplug.o
diff --git a/drivers/platform/surface/surface_aggregator_tabletsw.c b/drivers/platform/surface/surface_aggregator_tabletsw.c
new file mode 100644
index 000000000000..596ca6c80681
--- /dev/null
+++ b/drivers/platform/surface/surface_aggregator_tabletsw.c
@@ -0,0 +1,533 @@ 
+// 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,
+};
+
+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";
+
+	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:
+		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 (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(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");