| // SPDX-License-Identifier: GPL-2.0+ | |
| /* | |
| * Surface System Aggregator Module (SSAM) HID transport driver for the | |
| * generic HID interface (HID/TC=0x15 subsystem). Provides support for | |
| * integrated HID devices on Surface Laptop 3, Book 3, and later. | |
| * | |
| * Copyright (C) 2019-2021 Blaž Hrastnik <blaz@mxxn.io>, | |
| * Maximilian Luz <luzmaximilian@gmail.com> | |
| */ | |
| #include <linux/unaligned.h> | |
| #include <linux/hid.h> | |
| #include <linux/kernel.h> | |
| #include <linux/module.h> | |
| #include <linux/types.h> | |
| #include <linux/surface_aggregator/controller.h> | |
| #include <linux/surface_aggregator/device.h> | |
| #include "surface_hid_core.h" | |
| /* -- SAM interface. -------------------------------------------------------- */ | |
| struct surface_hid_buffer_slice { | |
| __u8 entry; | |
| __le32 offset; | |
| __le32 length; | |
| __u8 end; | |
| __u8 data[]; | |
| } __packed; | |
| static_assert(sizeof(struct surface_hid_buffer_slice) == 10); | |
| enum surface_hid_cid { | |
| SURFACE_HID_CID_OUTPUT_REPORT = 0x01, | |
| SURFACE_HID_CID_GET_FEATURE_REPORT = 0x02, | |
| SURFACE_HID_CID_SET_FEATURE_REPORT = 0x03, | |
| SURFACE_HID_CID_GET_DESCRIPTOR = 0x04, | |
| }; | |
| static int ssam_hid_get_descriptor(struct surface_hid_device *shid, u8 entry, u8 *buf, size_t len) | |
| { | |
| u8 buffer[sizeof(struct surface_hid_buffer_slice) + 0x76]; | |
| struct surface_hid_buffer_slice *slice; | |
| struct ssam_request rqst; | |
| struct ssam_response rsp; | |
| u32 buffer_len, offset, length; | |
| int status; | |
| /* | |
| * Note: The 0x76 above has been chosen because that's what's used by | |
| * the Windows driver. Together with the header, this leads to a 128 | |
| * byte payload in total. | |
| */ | |
| buffer_len = ARRAY_SIZE(buffer) - sizeof(struct surface_hid_buffer_slice); | |
| rqst.target_category = shid->uid.category; | |
| rqst.target_id = shid->uid.target; | |
| rqst.command_id = SURFACE_HID_CID_GET_DESCRIPTOR; | |
| rqst.instance_id = shid->uid.instance; | |
| rqst.flags = SSAM_REQUEST_HAS_RESPONSE; | |
| rqst.length = sizeof(struct surface_hid_buffer_slice); | |
| rqst.payload = buffer; | |
| rsp.capacity = ARRAY_SIZE(buffer); | |
| rsp.pointer = buffer; | |
| slice = (struct surface_hid_buffer_slice *)buffer; | |
| slice->entry = entry; | |
| slice->end = 0; | |
| offset = 0; | |
| length = buffer_len; | |
| while (!slice->end && offset < len) { | |
| put_unaligned_le32(offset, &slice->offset); | |
| put_unaligned_le32(length, &slice->length); | |
| rsp.length = 0; | |
| status = ssam_retry(ssam_request_do_sync_onstack, shid->ctrl, &rqst, &rsp, | |
| sizeof(*slice)); | |
| if (status) | |
| return status; | |
| offset = get_unaligned_le32(&slice->offset); | |
| length = get_unaligned_le32(&slice->length); | |
| /* Don't mess stuff up in case we receive garbage. */ | |
| if (length > buffer_len || offset > len) | |
| return -EPROTO; | |
| if (offset + length > len) | |
| length = len - offset; | |
| memcpy(buf + offset, &slice->data[0], length); | |
| offset += length; | |
| length = buffer_len; | |
| } | |
| if (offset != len) { | |
| dev_err(shid->dev, "unexpected descriptor length: got %u, expected %zu\n", | |
| offset, len); | |
| return -EPROTO; | |
| } | |
| return 0; | |
| } | |
| static int ssam_hid_set_raw_report(struct surface_hid_device *shid, u8 rprt_id, bool feature, | |
| u8 *buf, size_t len) | |
| { | |
| struct ssam_request rqst; | |
| u8 cid; | |
| if (feature) | |
| cid = SURFACE_HID_CID_SET_FEATURE_REPORT; | |
| else | |
| cid = SURFACE_HID_CID_OUTPUT_REPORT; | |
| rqst.target_category = shid->uid.category; | |
| rqst.target_id = shid->uid.target; | |
| rqst.instance_id = shid->uid.instance; | |
| rqst.command_id = cid; | |
| rqst.flags = 0; | |
| rqst.length = len; | |
| rqst.payload = buf; | |
| buf[0] = rprt_id; | |
| return ssam_retry(ssam_request_do_sync, shid->ctrl, &rqst, NULL); | |
| } | |
| static int ssam_hid_get_raw_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) | |
| { | |
| struct ssam_request rqst; | |
| struct ssam_response rsp; | |
| rqst.target_category = shid->uid.category; | |
| rqst.target_id = shid->uid.target; | |
| rqst.instance_id = shid->uid.instance; | |
| rqst.command_id = SURFACE_HID_CID_GET_FEATURE_REPORT; | |
| rqst.flags = SSAM_REQUEST_HAS_RESPONSE; | |
| rqst.length = sizeof(rprt_id); | |
| rqst.payload = &rprt_id; | |
| rsp.capacity = len; | |
| rsp.length = 0; | |
| rsp.pointer = buf; | |
| return ssam_retry(ssam_request_do_sync_onstack, shid->ctrl, &rqst, &rsp, sizeof(rprt_id)); | |
| } | |
| static u32 ssam_hid_event_fn(struct ssam_event_notifier *nf, const struct ssam_event *event) | |
| { | |
| struct surface_hid_device *shid = container_of(nf, struct surface_hid_device, notif); | |
| if (event->command_id != 0x00) | |
| return 0; | |
| hid_input_report(shid->hid, HID_INPUT_REPORT, (u8 *)&event->data[0], event->length, 0); | |
| return SSAM_NOTIF_HANDLED; | |
| } | |
| /* -- Transport driver. ----------------------------------------------------- */ | |
| static int shid_output_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) | |
| { | |
| int status; | |
| status = ssam_hid_set_raw_report(shid, rprt_id, false, buf, len); | |
| return status >= 0 ? len : status; | |
| } | |
| static int shid_get_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) | |
| { | |
| int status; | |
| status = ssam_hid_get_raw_report(shid, rprt_id, buf, len); | |
| return status >= 0 ? len : status; | |
| } | |
| static int shid_set_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) | |
| { | |
| int status; | |
| status = ssam_hid_set_raw_report(shid, rprt_id, true, buf, len); | |
| return status >= 0 ? len : status; | |
| } | |
| /* -- Driver setup. --------------------------------------------------------- */ | |
| static int surface_hid_probe(struct ssam_device *sdev) | |
| { | |
| struct surface_hid_device *shid; | |
| shid = devm_kzalloc(&sdev->dev, sizeof(*shid), GFP_KERNEL); | |
| if (!shid) | |
| return -ENOMEM; | |
| shid->dev = &sdev->dev; | |
| shid->ctrl = sdev->ctrl; | |
| shid->uid = sdev->uid; | |
| shid->notif.base.priority = 1; | |
| shid->notif.base.fn = ssam_hid_event_fn; | |
| shid->notif.event.reg = SSAM_EVENT_REGISTRY_REG(sdev->uid.target); | |
| shid->notif.event.id.target_category = sdev->uid.category; | |
| shid->notif.event.id.instance = sdev->uid.instance; | |
| shid->notif.event.mask = SSAM_EVENT_MASK_STRICT; | |
| shid->notif.event.flags = 0; | |
| shid->ops.get_descriptor = ssam_hid_get_descriptor; | |
| shid->ops.output_report = shid_output_report; | |
| shid->ops.get_feature_report = shid_get_feature_report; | |
| shid->ops.set_feature_report = shid_set_feature_report; | |
| ssam_device_set_drvdata(sdev, shid); | |
| return surface_hid_device_add(shid); | |
| } | |
| static void surface_hid_remove(struct ssam_device *sdev) | |
| { | |
| surface_hid_device_destroy(ssam_device_get_drvdata(sdev)); | |
| } | |
| static const struct ssam_device_id surface_hid_match[] = { | |
| { SSAM_SDEV(HID, ANY, SSAM_SSH_IID_ANY, 0x00) }, | |
| { }, | |
| }; | |
| MODULE_DEVICE_TABLE(ssam, surface_hid_match); | |
| static struct ssam_device_driver surface_hid_driver = { | |
| .probe = surface_hid_probe, | |
| .remove = surface_hid_remove, | |
| .match_table = surface_hid_match, | |
| .driver = { | |
| .name = "surface_hid", | |
| .pm = &surface_hid_pm_ops, | |
| .probe_type = PROBE_PREFER_ASYNCHRONOUS, | |
| }, | |
| }; | |
| module_ssam_device_driver(surface_hid_driver); | |
| MODULE_AUTHOR("Blaž Hrastnik <blaz@mxxn.io>"); | |
| MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>"); | |
| MODULE_DESCRIPTION("HID transport driver for Surface System Aggregator Module"); | |
| MODULE_LICENSE("GPL"); |