b09ee1cd59
We currently have a struct ssam_request_sync and a function ssam_request_sync(). While this is valid C, there are some downsides to it. One of these is that current Sphinx versions (>= 3.0) cannot disambiguate between the two (see disucssion and pull request linked below). It instead emits a "WARNING: Duplicate C declaration" and links for the struct and function in the resulting documentation link to the same entry (i.e. both to either function or struct documentation) instead of their respective own entries. While we could just ignore that and wait for a fix, there's also a point to be made that the current naming can be somewhat confusing when searching (e.g. via grep) or trying to understand the levels of abstraction at play: We currently have struct ssam_request_sync and associated functions ssam_request_sync_[alloc|free|init|wait|...]() operating on this struct. However, function ssam_request_sync() is one abstraction level above this. Similarly, ssam_request_sync_with_buffer() is not a function operating on struct ssam_request_sync, but rather a sibling to ssam_request_sync(), both using the struct under the hood. Therefore, rename the top level request functions: ssam_request_sync() -> ssam_request_do_sync() ssam_request_sync_with_buffer() -> ssam_request_do_sync_with_buffer() ssam_request_sync_onstack() -> ssam_request_do_sync_onstack() Link: https://lore.kernel.org/all/085e0ada65c11da9303d07e70c510dc45f21315b.1656756450.git.mchehab@kernel.org/ Link: https://github.com/sphinx-doc/sphinx/pull/8313 Signed-off-by: Maximilian Luz <luzmaximilian@gmail.com> Link: https://lore.kernel.org/r/20221220175608.1436273-2-luzmaximilian@gmail.com Reviewed-by: Hans de Goede <hdegoede@redhat.com> Signed-off-by: Hans de Goede <hdegoede@redhat.com>
301 lines
7.7 KiB
C
301 lines
7.7 KiB
C
// SPDX-License-Identifier: GPL-2.0+
|
|
/*
|
|
* Surface System Aggregator Module (SSAM) HID transport driver for the legacy
|
|
* keyboard interface (KBD/TC=0x08 subsystem). Provides support for the
|
|
* integrated HID keyboard on Surface Laptops 1 and 2.
|
|
*
|
|
* 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/platform_device.h>
|
|
#include <linux/types.h>
|
|
|
|
#include <linux/surface_aggregator/controller.h>
|
|
|
|
#include "surface_hid_core.h"
|
|
|
|
|
|
/* -- SAM interface (KBD). -------------------------------------------------- */
|
|
|
|
#define KBD_FEATURE_REPORT_SIZE 7 /* 6 + report ID */
|
|
|
|
enum surface_kbd_cid {
|
|
SURFACE_KBD_CID_GET_DESCRIPTOR = 0x00,
|
|
SURFACE_KBD_CID_SET_CAPSLOCK_LED = 0x01,
|
|
SURFACE_KBD_CID_EVT_INPUT_GENERIC = 0x03,
|
|
SURFACE_KBD_CID_EVT_INPUT_HOTKEYS = 0x04,
|
|
SURFACE_KBD_CID_GET_FEATURE_REPORT = 0x0b,
|
|
};
|
|
|
|
static int ssam_kbd_get_descriptor(struct surface_hid_device *shid, u8 entry, u8 *buf, size_t len)
|
|
{
|
|
struct ssam_request rqst;
|
|
struct ssam_response rsp;
|
|
int status;
|
|
|
|
rqst.target_category = shid->uid.category;
|
|
rqst.target_id = shid->uid.target;
|
|
rqst.command_id = SURFACE_KBD_CID_GET_DESCRIPTOR;
|
|
rqst.instance_id = shid->uid.instance;
|
|
rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
|
|
rqst.length = sizeof(entry);
|
|
rqst.payload = &entry;
|
|
|
|
rsp.capacity = len;
|
|
rsp.length = 0;
|
|
rsp.pointer = buf;
|
|
|
|
status = ssam_retry(ssam_request_do_sync_onstack, shid->ctrl, &rqst, &rsp, sizeof(entry));
|
|
if (status)
|
|
return status;
|
|
|
|
if (rsp.length != len) {
|
|
dev_err(shid->dev, "invalid descriptor length: got %zu, expected, %zu\n",
|
|
rsp.length, len);
|
|
return -EPROTO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int ssam_kbd_set_caps_led(struct surface_hid_device *shid, bool value)
|
|
{
|
|
struct ssam_request rqst;
|
|
u8 value_u8 = value;
|
|
|
|
rqst.target_category = shid->uid.category;
|
|
rqst.target_id = shid->uid.target;
|
|
rqst.command_id = SURFACE_KBD_CID_SET_CAPSLOCK_LED;
|
|
rqst.instance_id = shid->uid.instance;
|
|
rqst.flags = 0;
|
|
rqst.length = sizeof(value_u8);
|
|
rqst.payload = &value_u8;
|
|
|
|
return ssam_retry(ssam_request_do_sync_onstack, shid->ctrl, &rqst, NULL, sizeof(value_u8));
|
|
}
|
|
|
|
static int ssam_kbd_get_feature_report(struct surface_hid_device *shid, u8 *buf, size_t len)
|
|
{
|
|
struct ssam_request rqst;
|
|
struct ssam_response rsp;
|
|
u8 payload = 0;
|
|
int status;
|
|
|
|
rqst.target_category = shid->uid.category;
|
|
rqst.target_id = shid->uid.target;
|
|
rqst.command_id = SURFACE_KBD_CID_GET_FEATURE_REPORT;
|
|
rqst.instance_id = shid->uid.instance;
|
|
rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
|
|
rqst.length = sizeof(payload);
|
|
rqst.payload = &payload;
|
|
|
|
rsp.capacity = len;
|
|
rsp.length = 0;
|
|
rsp.pointer = buf;
|
|
|
|
status = ssam_retry(ssam_request_do_sync_onstack, shid->ctrl, &rqst, &rsp, sizeof(payload));
|
|
if (status)
|
|
return status;
|
|
|
|
if (rsp.length != len) {
|
|
dev_err(shid->dev, "invalid feature report length: got %zu, expected, %zu\n",
|
|
rsp.length, len);
|
|
return -EPROTO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool ssam_kbd_is_input_event(const struct ssam_event *event)
|
|
{
|
|
if (event->command_id == SURFACE_KBD_CID_EVT_INPUT_GENERIC)
|
|
return true;
|
|
|
|
if (event->command_id == SURFACE_KBD_CID_EVT_INPUT_HOTKEYS)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static u32 ssam_kbd_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);
|
|
|
|
/*
|
|
* Check against device UID manually, as registry and device target
|
|
* category doesn't line up.
|
|
*/
|
|
|
|
if (shid->uid.category != event->target_category)
|
|
return 0;
|
|
|
|
if (shid->uid.target != event->target_id)
|
|
return 0;
|
|
|
|
if (shid->uid.instance != event->instance_id)
|
|
return 0;
|
|
|
|
if (!ssam_kbd_is_input_event(event))
|
|
return 0;
|
|
|
|
hid_input_report(shid->hid, HID_INPUT_REPORT, (u8 *)&event->data[0], event->length, 0);
|
|
return SSAM_NOTIF_HANDLED;
|
|
}
|
|
|
|
|
|
/* -- Transport driver (KBD). ----------------------------------------------- */
|
|
|
|
static int skbd_get_caps_led_value(struct hid_device *hid, u8 rprt_id, u8 *buf, size_t len)
|
|
{
|
|
struct hid_field *field;
|
|
unsigned int offset, size;
|
|
int i;
|
|
|
|
/* Get LED field. */
|
|
field = hidinput_get_led_field(hid);
|
|
if (!field)
|
|
return -ENOENT;
|
|
|
|
/* Check if we got the correct report. */
|
|
if (len != hid_report_len(field->report))
|
|
return -ENOENT;
|
|
|
|
if (rprt_id != field->report->id)
|
|
return -ENOENT;
|
|
|
|
/* Get caps lock LED index. */
|
|
for (i = 0; i < field->report_count; i++)
|
|
if ((field->usage[i].hid & 0xffff) == 0x02)
|
|
break;
|
|
|
|
if (i == field->report_count)
|
|
return -ENOENT;
|
|
|
|
/* Extract value. */
|
|
size = field->report_size;
|
|
offset = field->report_offset + i * size;
|
|
return !!hid_field_extract(hid, buf + 1, size, offset);
|
|
}
|
|
|
|
static int skbd_output_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
|
|
{
|
|
int caps_led;
|
|
int status;
|
|
|
|
caps_led = skbd_get_caps_led_value(shid->hid, rprt_id, buf, len);
|
|
if (caps_led < 0)
|
|
return -EIO; /* Only caps LED output reports are supported. */
|
|
|
|
status = ssam_kbd_set_caps_led(shid, caps_led);
|
|
if (status < 0)
|
|
return status;
|
|
|
|
return len;
|
|
}
|
|
|
|
static int skbd_get_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
|
|
{
|
|
u8 report[KBD_FEATURE_REPORT_SIZE];
|
|
int status;
|
|
|
|
/*
|
|
* The keyboard only has a single hard-coded read-only feature report
|
|
* of size KBD_FEATURE_REPORT_SIZE. Try to load it and compare its
|
|
* report ID against the requested one.
|
|
*/
|
|
|
|
if (len < ARRAY_SIZE(report))
|
|
return -ENOSPC;
|
|
|
|
status = ssam_kbd_get_feature_report(shid, report, ARRAY_SIZE(report));
|
|
if (status < 0)
|
|
return status;
|
|
|
|
if (rprt_id != report[0])
|
|
return -ENOENT;
|
|
|
|
memcpy(buf, report, ARRAY_SIZE(report));
|
|
return len;
|
|
}
|
|
|
|
static int skbd_set_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
|
|
{
|
|
/* Not supported. See skbd_get_feature_report() for details. */
|
|
return -EIO;
|
|
}
|
|
|
|
|
|
/* -- Driver setup. --------------------------------------------------------- */
|
|
|
|
static int surface_kbd_probe(struct platform_device *pdev)
|
|
{
|
|
struct ssam_controller *ctrl;
|
|
struct surface_hid_device *shid;
|
|
|
|
/* Add device link to EC. */
|
|
ctrl = ssam_client_bind(&pdev->dev);
|
|
if (IS_ERR(ctrl))
|
|
return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl);
|
|
|
|
shid = devm_kzalloc(&pdev->dev, sizeof(*shid), GFP_KERNEL);
|
|
if (!shid)
|
|
return -ENOMEM;
|
|
|
|
shid->dev = &pdev->dev;
|
|
shid->ctrl = ctrl;
|
|
|
|
shid->uid.domain = SSAM_DOMAIN_SERIALHUB;
|
|
shid->uid.category = SSAM_SSH_TC_KBD;
|
|
shid->uid.target = SSAM_SSH_TID_KIP;
|
|
shid->uid.instance = 0;
|
|
shid->uid.function = 0;
|
|
|
|
shid->notif.base.priority = 1;
|
|
shid->notif.base.fn = ssam_kbd_event_fn;
|
|
shid->notif.event.reg = SSAM_EVENT_REGISTRY_SAM;
|
|
shid->notif.event.id.target_category = shid->uid.category;
|
|
shid->notif.event.id.instance = shid->uid.instance;
|
|
shid->notif.event.mask = SSAM_EVENT_MASK_NONE;
|
|
shid->notif.event.flags = 0;
|
|
|
|
shid->ops.get_descriptor = ssam_kbd_get_descriptor;
|
|
shid->ops.output_report = skbd_output_report;
|
|
shid->ops.get_feature_report = skbd_get_feature_report;
|
|
shid->ops.set_feature_report = skbd_set_feature_report;
|
|
|
|
platform_set_drvdata(pdev, shid);
|
|
return surface_hid_device_add(shid);
|
|
}
|
|
|
|
static int surface_kbd_remove(struct platform_device *pdev)
|
|
{
|
|
surface_hid_device_destroy(platform_get_drvdata(pdev));
|
|
return 0;
|
|
}
|
|
|
|
static const struct acpi_device_id surface_kbd_match[] = {
|
|
{ "MSHW0096" },
|
|
{ },
|
|
};
|
|
MODULE_DEVICE_TABLE(acpi, surface_kbd_match);
|
|
|
|
static struct platform_driver surface_kbd_driver = {
|
|
.probe = surface_kbd_probe,
|
|
.remove = surface_kbd_remove,
|
|
.driver = {
|
|
.name = "surface_keyboard",
|
|
.acpi_match_table = surface_kbd_match,
|
|
.pm = &surface_hid_pm_ops,
|
|
.probe_type = PROBE_PREFER_ASYNCHRONOUS,
|
|
},
|
|
};
|
|
module_platform_driver(surface_kbd_driver);
|
|
|
|
MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
|
|
MODULE_DESCRIPTION("HID legacy transport driver for Surface System Aggregator Module");
|
|
MODULE_LICENSE("GPL");
|