| // SPDX-License-Identifier: GPL-2.0+ | |
| /* | |
| * Common/core components for the Surface System Aggregator Module (SSAM) HID | |
| * transport driver. Provides support for integrated HID devices on Microsoft | |
| * Surface models. | |
| * | |
| * Copyright (C) 2019-2021 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/usb/ch9.h> | |
| #include <linux/surface_aggregator/controller.h> | |
| #include "surface_hid_core.h" | |
| /* -- Utility functions. ---------------------------------------------------- */ | |
| static bool surface_hid_is_hot_removed(struct surface_hid_device *shid) | |
| { | |
| /* | |
| * Non-ssam client devices, i.e. platform client devices, cannot be | |
| * hot-removed. | |
| */ | |
| if (!is_ssam_device(shid->dev)) | |
| return false; | |
| return ssam_device_is_hot_removed(to_ssam_device(shid->dev)); | |
| } | |
| /* -- Device descriptor access. --------------------------------------------- */ | |
| static int surface_hid_load_hid_descriptor(struct surface_hid_device *shid) | |
| { | |
| int status; | |
| if (surface_hid_is_hot_removed(shid)) | |
| return -ENODEV; | |
| status = shid->ops.get_descriptor(shid, SURFACE_HID_DESC_HID, | |
| (u8 *)&shid->hid_desc, sizeof(shid->hid_desc)); | |
| if (status) | |
| return status; | |
| if (shid->hid_desc.desc_len != sizeof(shid->hid_desc)) { | |
| dev_err(shid->dev, "unexpected HID descriptor length: got %u, expected %zu\n", | |
| shid->hid_desc.desc_len, sizeof(shid->hid_desc)); | |
| return -EPROTO; | |
| } | |
| if (shid->hid_desc.desc_type != HID_DT_HID) { | |
| dev_err(shid->dev, "unexpected HID descriptor type: got %#04x, expected %#04x\n", | |
| shid->hid_desc.desc_type, HID_DT_HID); | |
| return -EPROTO; | |
| } | |
| if (shid->hid_desc.num_descriptors != 1) { | |
| dev_err(shid->dev, "unexpected number of descriptors: got %u, expected 1\n", | |
| shid->hid_desc.num_descriptors); | |
| return -EPROTO; | |
| } | |
| if (shid->hid_desc.report_desc_type != HID_DT_REPORT) { | |
| dev_err(shid->dev, "unexpected report descriptor type: got %#04x, expected %#04x\n", | |
| shid->hid_desc.report_desc_type, HID_DT_REPORT); | |
| return -EPROTO; | |
| } | |
| return 0; | |
| } | |
| static int surface_hid_load_device_attributes(struct surface_hid_device *shid) | |
| { | |
| int status; | |
| if (surface_hid_is_hot_removed(shid)) | |
| return -ENODEV; | |
| status = shid->ops.get_descriptor(shid, SURFACE_HID_DESC_ATTRS, | |
| (u8 *)&shid->attrs, sizeof(shid->attrs)); | |
| if (status) | |
| return status; | |
| if (get_unaligned_le32(&shid->attrs.length) != sizeof(shid->attrs)) { | |
| dev_err(shid->dev, "unexpected attribute length: got %u, expected %zu\n", | |
| get_unaligned_le32(&shid->attrs.length), sizeof(shid->attrs)); | |
| return -EPROTO; | |
| } | |
| return 0; | |
| } | |
| /* -- Transport driver (common). -------------------------------------------- */ | |
| static int surface_hid_start(struct hid_device *hid) | |
| { | |
| struct surface_hid_device *shid = hid->driver_data; | |
| return ssam_notifier_register(shid->ctrl, &shid->notif); | |
| } | |
| static void surface_hid_stop(struct hid_device *hid) | |
| { | |
| struct surface_hid_device *shid = hid->driver_data; | |
| bool hot_removed; | |
| /* | |
| * Communication may fail for devices that have been hot-removed. This | |
| * also includes unregistration of HID events, so we need to check this | |
| * here. Only if the device has not been marked as hot-removed, we can | |
| * safely disable events. | |
| */ | |
| hot_removed = surface_hid_is_hot_removed(shid); | |
| /* Note: This call will log errors for us, so ignore them here. */ | |
| __ssam_notifier_unregister(shid->ctrl, &shid->notif, !hot_removed); | |
| } | |
| static int surface_hid_open(struct hid_device *hid) | |
| { | |
| return 0; | |
| } | |
| static void surface_hid_close(struct hid_device *hid) | |
| { | |
| } | |
| static int surface_hid_parse(struct hid_device *hid) | |
| { | |
| struct surface_hid_device *shid = hid->driver_data; | |
| size_t len = get_unaligned_le16(&shid->hid_desc.report_desc_len); | |
| u8 *buf; | |
| int status; | |
| if (surface_hid_is_hot_removed(shid)) | |
| return -ENODEV; | |
| buf = kzalloc(len, GFP_KERNEL); | |
| if (!buf) | |
| return -ENOMEM; | |
| status = shid->ops.get_descriptor(shid, SURFACE_HID_DESC_REPORT, buf, len); | |
| if (!status) | |
| status = hid_parse_report(hid, buf, len); | |
| kfree(buf); | |
| return status; | |
| } | |
| static int surface_hid_raw_request(struct hid_device *hid, unsigned char reportnum, u8 *buf, | |
| size_t len, unsigned char rtype, int reqtype) | |
| { | |
| struct surface_hid_device *shid = hid->driver_data; | |
| if (surface_hid_is_hot_removed(shid)) | |
| return -ENODEV; | |
| if (rtype == HID_OUTPUT_REPORT && reqtype == HID_REQ_SET_REPORT) | |
| return shid->ops.output_report(shid, reportnum, buf, len); | |
| else if (rtype == HID_FEATURE_REPORT && reqtype == HID_REQ_GET_REPORT) | |
| return shid->ops.get_feature_report(shid, reportnum, buf, len); | |
| else if (rtype == HID_FEATURE_REPORT && reqtype == HID_REQ_SET_REPORT) | |
| return shid->ops.set_feature_report(shid, reportnum, buf, len); | |
| return -EIO; | |
| } | |
| static const struct hid_ll_driver surface_hid_ll_driver = { | |
| .start = surface_hid_start, | |
| .stop = surface_hid_stop, | |
| .open = surface_hid_open, | |
| .close = surface_hid_close, | |
| .parse = surface_hid_parse, | |
| .raw_request = surface_hid_raw_request, | |
| }; | |
| /* -- Common device setup. -------------------------------------------------- */ | |
| int surface_hid_device_add(struct surface_hid_device *shid) | |
| { | |
| int status; | |
| status = surface_hid_load_hid_descriptor(shid); | |
| if (status) | |
| return status; | |
| status = surface_hid_load_device_attributes(shid); | |
| if (status) | |
| return status; | |
| shid->hid = hid_allocate_device(); | |
| if (IS_ERR(shid->hid)) | |
| return PTR_ERR(shid->hid); | |
| shid->hid->dev.parent = shid->dev; | |
| shid->hid->bus = BUS_HOST; | |
| shid->hid->vendor = get_unaligned_le16(&shid->attrs.vendor); | |
| shid->hid->product = get_unaligned_le16(&shid->attrs.product); | |
| shid->hid->version = get_unaligned_le16(&shid->hid_desc.hid_version); | |
| shid->hid->country = shid->hid_desc.country_code; | |
| snprintf(shid->hid->name, sizeof(shid->hid->name), "Microsoft Surface %04X:%04X", | |
| shid->hid->vendor, shid->hid->product); | |
| strscpy(shid->hid->phys, dev_name(shid->dev), sizeof(shid->hid->phys)); | |
| shid->hid->driver_data = shid; | |
| shid->hid->ll_driver = &surface_hid_ll_driver; | |
| status = hid_add_device(shid->hid); | |
| if (status) | |
| hid_destroy_device(shid->hid); | |
| return status; | |
| } | |
| EXPORT_SYMBOL_GPL(surface_hid_device_add); | |
| void surface_hid_device_destroy(struct surface_hid_device *shid) | |
| { | |
| hid_destroy_device(shid->hid); | |
| } | |
| EXPORT_SYMBOL_GPL(surface_hid_device_destroy); | |
| /* -- PM ops. --------------------------------------------------------------- */ | |
| #ifdef CONFIG_PM_SLEEP | |
| static int surface_hid_suspend(struct device *dev) | |
| { | |
| struct surface_hid_device *d = dev_get_drvdata(dev); | |
| return hid_driver_suspend(d->hid, PMSG_SUSPEND); | |
| } | |
| static int surface_hid_resume(struct device *dev) | |
| { | |
| struct surface_hid_device *d = dev_get_drvdata(dev); | |
| return hid_driver_resume(d->hid); | |
| } | |
| static int surface_hid_freeze(struct device *dev) | |
| { | |
| struct surface_hid_device *d = dev_get_drvdata(dev); | |
| return hid_driver_suspend(d->hid, PMSG_FREEZE); | |
| } | |
| static int surface_hid_poweroff(struct device *dev) | |
| { | |
| struct surface_hid_device *d = dev_get_drvdata(dev); | |
| return hid_driver_suspend(d->hid, PMSG_HIBERNATE); | |
| } | |
| static int surface_hid_restore(struct device *dev) | |
| { | |
| struct surface_hid_device *d = dev_get_drvdata(dev); | |
| return hid_driver_reset_resume(d->hid); | |
| } | |
| const struct dev_pm_ops surface_hid_pm_ops = { | |
| .freeze = surface_hid_freeze, | |
| .thaw = surface_hid_resume, | |
| .suspend = surface_hid_suspend, | |
| .resume = surface_hid_resume, | |
| .poweroff = surface_hid_poweroff, | |
| .restore = surface_hid_restore, | |
| }; | |
| EXPORT_SYMBOL_GPL(surface_hid_pm_ops); | |
| #else /* CONFIG_PM_SLEEP */ | |
| const struct dev_pm_ops surface_hid_pm_ops = { }; | |
| EXPORT_SYMBOL_GPL(surface_hid_pm_ops); | |
| #endif /* CONFIG_PM_SLEEP */ | |
| MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>"); | |
| MODULE_DESCRIPTION("HID transport driver core for Surface System Aggregator Module"); | |
| MODULE_LICENSE("GPL"); |