forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
272 lines
7.1 KiB
272 lines
7.1 KiB
// 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 <[email protected]> |
|
*/ |
|
|
|
#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 = 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); |
|
|
|
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 <[email protected]>"); |
|
MODULE_DESCRIPTION("HID transport driver core for Surface System Aggregator Module"); |
|
MODULE_LICENSE("GPL");
|
|
|