| // 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 <asm/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" |
| |
| |
| /* -- Device descriptor access. --------------------------------------------- */ |
| |
| static int surface_hid_load_hid_descriptor(struct surface_hid_device *shid) |
| { |
| int status; |
| |
| 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; |
| |
| 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; |
| |
| /* Note: This call will log errors for us, so ignore them here. */ |
| ssam_notifier_unregister(shid->ctrl, &shid->notif); |
| } |
| |
| 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; |
| |
| 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 (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 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 = cpu_to_le16(shid->attrs.vendor); |
| shid->hid->product = cpu_to_le16(shid->attrs.product); |
| shid->hid->version = cpu_to_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); |
| |
| if (d->hid->driver && d->hid->driver->suspend) |
| return d->hid->driver->suspend(d->hid, PMSG_SUSPEND); |
| |
| return 0; |
| } |
| |
| static int surface_hid_resume(struct device *dev) |
| { |
| struct surface_hid_device *d = dev_get_drvdata(dev); |
| |
| if (d->hid->driver && d->hid->driver->resume) |
| return d->hid->driver->resume(d->hid); |
| |
| return 0; |
| } |
| |
| static int surface_hid_freeze(struct device *dev) |
| { |
| struct surface_hid_device *d = dev_get_drvdata(dev); |
| |
| if (d->hid->driver && d->hid->driver->suspend) |
| return d->hid->driver->suspend(d->hid, PMSG_FREEZE); |
| |
| return 0; |
| } |
| |
| static int surface_hid_poweroff(struct device *dev) |
| { |
| struct surface_hid_device *d = dev_get_drvdata(dev); |
| |
| if (d->hid->driver && d->hid->driver->suspend) |
| return d->hid->driver->suspend(d->hid, PMSG_HIBERNATE); |
| |
| return 0; |
| } |
| |
| static int surface_hid_restore(struct device *dev) |
| { |
| struct surface_hid_device *d = dev_get_drvdata(dev); |
| |
| if (d->hid->driver && d->hid->driver->reset_resume) |
| return d->hid->driver->reset_resume(d->hid); |
| |
| return 0; |
| } |
| |
| 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"); |