fwupd/libfwupdplugin/fu-hid-device.c
2021-08-24 11:18:40 -05:00

495 lines
13 KiB
C

/*
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#define G_LOG_DOMAIN "FuHidDevice"
#include "config.h"
#include "fu-hid-device.h"
#define FU_HID_REPORT_GET 0x01
#define FU_HID_REPORT_SET 0x09
#define FU_HID_REPORT_TYPE_INPUT 0x01
#define FU_HID_REPORT_TYPE_OUTPUT 0x02
#define FU_HID_REPORT_TYPE_FEATURE 0x03
#define FU_HID_DEVICE_RETRIES 10
/**
* FuHidDevice:
*
* A Human Interface Device (HID) device.
*
* See also: [class@FuDevice], [class@FuUsbDevice]
*/
typedef struct {
guint8 interface;
gboolean interface_autodetect;
FuHidDeviceFlags flags;
} FuHidDevicePrivate;
G_DEFINE_TYPE_WITH_PRIVATE(FuHidDevice, fu_hid_device, FU_TYPE_USB_DEVICE)
enum { PROP_0, PROP_INTERFACE, PROP_LAST };
#define GET_PRIVATE(o) (fu_hid_device_get_instance_private(o))
static void
fu_hid_device_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
FuHidDevice *device = FU_HID_DEVICE(object);
FuHidDevicePrivate *priv = GET_PRIVATE(device);
switch (prop_id) {
case PROP_INTERFACE:
g_value_set_uint(value, priv->interface);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
break;
}
}
static void
fu_hid_device_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
FuHidDevice *device = FU_HID_DEVICE(object);
switch (prop_id) {
case PROP_INTERFACE:
fu_hid_device_set_interface(device, g_value_get_uint(value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
break;
}
}
static gboolean
fu_hid_device_open(FuDevice *device, GError **error)
{
#ifdef HAVE_GUSB
FuHidDevice *self = FU_HID_DEVICE(device);
FuHidDevicePrivate *priv = GET_PRIVATE(self);
GUsbDeviceClaimInterfaceFlags flags = 0;
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
/* FuUsbDevice->open */
if (!FU_DEVICE_CLASS(fu_hid_device_parent_class)->open(device, error))
return FALSE;
/* auto-detect */
if (priv->interface_autodetect) {
g_autoptr(GPtrArray) ifaces = NULL;
ifaces = g_usb_device_get_interfaces(usb_device, error);
if (ifaces == NULL)
return FALSE;
for (guint i = 0; i < ifaces->len; i++) {
GUsbInterface *iface = g_ptr_array_index(ifaces, i);
if (g_usb_interface_get_class(iface) == G_USB_DEVICE_CLASS_HID) {
priv->interface = g_usb_interface_get_number(iface);
priv->interface_autodetect = FALSE;
break;
}
}
if (priv->interface_autodetect) {
g_set_error_literal(error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"could not autodetect HID interface");
return FALSE;
}
g_debug("autodetected HID interface of 0x%02x", priv->interface);
}
/* claim */
if ((priv->flags & FU_HID_DEVICE_FLAG_NO_KERNEL_UNBIND) == 0)
flags |= G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER;
if (!g_usb_device_claim_interface(usb_device, priv->interface, flags, error)) {
g_prefix_error(error, "failed to claim HID interface: ");
return FALSE;
}
#endif
/* success */
return TRUE;
}
static gboolean
fu_hid_device_close(FuDevice *device, GError **error)
{
#ifdef HAVE_GUSB
FuHidDevice *self = FU_HID_DEVICE(device);
FuHidDevicePrivate *priv = GET_PRIVATE(self);
GUsbDeviceClaimInterfaceFlags flags = 0;
GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device));
g_autoptr(GError) error_local = NULL;
#endif
#ifdef HAVE_GUSB
/* release */
if ((priv->flags & FU_HID_DEVICE_FLAG_NO_KERNEL_REBIND) == 0)
flags |= G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER;
if (!g_usb_device_release_interface(usb_device, priv->interface, flags, &error_local)) {
if (g_error_matches(error_local,
G_USB_DEVICE_ERROR,
G_USB_DEVICE_ERROR_NO_DEVICE) ||
g_error_matches(error_local, G_USB_DEVICE_ERROR, G_USB_DEVICE_ERROR_INTERNAL)) {
g_debug("ignoring: %s", error_local->message);
return TRUE;
}
g_propagate_prefixed_error(error,
g_steal_pointer(&error_local),
"failed to release HID interface: ");
return FALSE;
}
#endif
/* FuUsbDevice->close */
return FU_DEVICE_CLASS(fu_hid_device_parent_class)->close(device, error);
}
/**
* fu_hid_device_set_interface:
* @self: a #FuHidDevice
* @interface: an interface number, e.g. `0x03`
*
* Sets the HID USB interface number.
*
* In most cases the HID interface is auto-detected, but this function can be
* used where there are multiple HID interfaces or where the device USB
* interface descriptor is invalid.
*
* Since: 1.4.0
**/
void
fu_hid_device_set_interface(FuHidDevice *self, guint8 interface)
{
FuHidDevicePrivate *priv = GET_PRIVATE(self);
g_return_if_fail(FU_HID_DEVICE(self));
priv->interface = interface;
priv->interface_autodetect = FALSE;
}
/**
* fu_hid_device_get_interface:
* @self: a #FuHidDevice
*
* Gets the HID USB interface number.
*
* Returns: integer
*
* Since: 1.4.0
**/
guint8
fu_hid_device_get_interface(FuHidDevice *self)
{
FuHidDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_HID_DEVICE(self), 0xff);
return priv->interface;
}
/**
* fu_hid_device_add_flag:
* @self: a #FuHidDevice
* @flag: HID device flags, e.g. %FU_HID_DEVICE_FLAG_RETRY_FAILURE
*
* Adds a flag to be used for all set and get report messages.
*
* Since: 1.5.2
**/
void
fu_hid_device_add_flag(FuHidDevice *self, FuHidDeviceFlags flag)
{
FuHidDevicePrivate *priv = GET_PRIVATE(self);
g_return_if_fail(FU_HID_DEVICE(self));
priv->flags |= flag;
}
typedef struct {
guint8 value;
guint8 *buf;
gsize bufsz;
guint timeout;
FuHidDeviceFlags flags;
} FuHidDeviceRetryHelper;
static gboolean
fu_hid_device_set_report_internal(FuHidDevice *self, FuHidDeviceRetryHelper *helper, GError **error)
{
#ifdef HAVE_GUSB
FuHidDevicePrivate *priv = GET_PRIVATE(self);
GUsbDevice *usb_device;
gsize actual_len = 0;
guint16 wvalue = (FU_HID_REPORT_TYPE_OUTPUT << 8) | helper->value;
/* special case */
if (helper->flags & FU_HID_DEVICE_FLAG_IS_FEATURE)
wvalue = (FU_HID_REPORT_TYPE_FEATURE << 8) | helper->value;
if (g_getenv("FU_HID_DEVICE_VERBOSE") != NULL) {
g_autofree gchar *title = NULL;
title = g_strdup_printf("HID::SetReport [wValue=0x%04x ,wIndex=%u]",
wvalue,
priv->interface);
fu_common_dump_raw(G_LOG_DOMAIN, title, helper->buf, helper->bufsz);
}
usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self));
if (!g_usb_device_control_transfer(usb_device,
G_USB_DEVICE_DIRECTION_HOST_TO_DEVICE,
G_USB_DEVICE_REQUEST_TYPE_CLASS,
G_USB_DEVICE_RECIPIENT_INTERFACE,
FU_HID_REPORT_SET,
wvalue,
priv->interface,
helper->buf,
helper->bufsz,
&actual_len,
helper->timeout,
NULL,
error)) {
g_prefix_error(error, "failed to SetReport: ");
return FALSE;
}
if ((helper->flags & FU_HID_DEVICE_FLAG_ALLOW_TRUNC) == 0 && actual_len != helper->bufsz) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"wrote %" G_GSIZE_FORMAT ", requested %" G_GSIZE_FORMAT " bytes",
actual_len,
helper->bufsz);
return FALSE;
}
#endif
return TRUE;
}
static gboolean
fu_hid_device_set_report_internal_cb(FuDevice *device, gpointer user_data, GError **error)
{
FuHidDevice *self = FU_HID_DEVICE(device);
FuHidDeviceRetryHelper *helper = (FuHidDeviceRetryHelper *)user_data;
return fu_hid_device_set_report_internal(self, helper, error);
}
/**
* fu_hid_device_set_report:
* @self: a #FuHidDevice
* @value: low byte of wValue
* @buf: (nullable): a mutable buffer of data to send
* @bufsz: size of @buf
* @timeout: timeout in ms
* @flags: HID device flags e.g. %FU_HID_DEVICE_FLAG_ALLOW_TRUNC
* @error: (nullable): optional return location for an error
*
* Calls SetReport on the hardware.
*
* Returns: %TRUE for success
*
* Since: 1.4.0
**/
gboolean
fu_hid_device_set_report(FuHidDevice *self,
guint8 value,
guint8 *buf,
gsize bufsz,
guint timeout,
FuHidDeviceFlags flags,
GError **error)
{
FuHidDeviceRetryHelper helper;
FuHidDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_HID_DEVICE(self), FALSE);
g_return_val_if_fail(buf != NULL, FALSE);
g_return_val_if_fail(bufsz != 0, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* create helper */
helper.value = value;
helper.buf = buf;
helper.bufsz = bufsz;
helper.timeout = timeout;
helper.flags = priv->flags | flags;
/* special case */
if (flags & FU_HID_DEVICE_FLAG_RETRY_FAILURE) {
return fu_device_retry(FU_DEVICE(self),
fu_hid_device_set_report_internal_cb,
FU_HID_DEVICE_RETRIES,
&helper,
error);
}
/* just one */
return fu_hid_device_set_report_internal(self, &helper, error);
}
static gboolean
fu_hid_device_get_report_internal(FuHidDevice *self, FuHidDeviceRetryHelper *helper, GError **error)
{
#ifdef HAVE_GUSB
FuHidDevicePrivate *priv = GET_PRIVATE(self);
GUsbDevice *usb_device;
gsize actual_len = 0;
guint16 wvalue = (FU_HID_REPORT_TYPE_INPUT << 8) | helper->value;
/* special case */
if (helper->flags & FU_HID_DEVICE_FLAG_IS_FEATURE)
wvalue = (FU_HID_REPORT_TYPE_FEATURE << 8) | helper->value;
if (g_getenv("FU_HID_DEVICE_VERBOSE") != NULL) {
g_autofree gchar *title = NULL;
title = g_strdup_printf("HID::GetReport [wValue=0x%04x, wIndex=%u]",
wvalue,
priv->interface);
fu_common_dump_raw(G_LOG_DOMAIN, title, helper->buf, actual_len);
}
usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(self));
if (!g_usb_device_control_transfer(usb_device,
G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST,
G_USB_DEVICE_REQUEST_TYPE_CLASS,
G_USB_DEVICE_RECIPIENT_INTERFACE,
FU_HID_REPORT_GET,
wvalue,
priv->interface,
helper->buf,
helper->bufsz,
&actual_len, /* actual length */
helper->timeout,
NULL,
error)) {
g_prefix_error(error, "failed to GetReport: ");
return FALSE;
}
if (g_getenv("FU_HID_DEVICE_VERBOSE") != NULL) {
g_autofree gchar *title = NULL;
title = g_strdup_printf("HID::GetReport [wValue=0x%04x, wIndex=%u]",
wvalue,
priv->interface);
fu_common_dump_raw(G_LOG_DOMAIN, title, helper->buf, actual_len);
}
if ((helper->flags & FU_HID_DEVICE_FLAG_ALLOW_TRUNC) == 0 && actual_len != helper->bufsz) {
g_set_error(error,
G_IO_ERROR,
G_IO_ERROR_INVALID_DATA,
"read %" G_GSIZE_FORMAT ", requested %" G_GSIZE_FORMAT " bytes",
actual_len,
helper->bufsz);
return FALSE;
}
#endif
return TRUE;
}
static gboolean
fu_hid_device_get_report_internal_cb(FuDevice *device, gpointer user_data, GError **error)
{
FuHidDevice *self = FU_HID_DEVICE(device);
FuHidDeviceRetryHelper *helper = (FuHidDeviceRetryHelper *)user_data;
return fu_hid_device_get_report_internal(self, helper, error);
}
/**
* fu_hid_device_get_report:
* @self: a #FuHidDevice
* @value: low byte of wValue
* @buf: (nullable): a mutable buffer of data to send
* @bufsz: size of @buf
* @timeout: timeout in ms
* @flags: HID device flags e.g. %FU_HID_DEVICE_FLAG_ALLOW_TRUNC
* @error: (nullable): optional return location for an error
*
* Calls GetReport on the hardware.
*
* Returns: %TRUE for success
*
* Since: 1.4.0
**/
gboolean
fu_hid_device_get_report(FuHidDevice *self,
guint8 value,
guint8 *buf,
gsize bufsz,
guint timeout,
FuHidDeviceFlags flags,
GError **error)
{
FuHidDeviceRetryHelper helper;
FuHidDevicePrivate *priv = GET_PRIVATE(self);
g_return_val_if_fail(FU_HID_DEVICE(self), FALSE);
g_return_val_if_fail(buf != NULL, FALSE);
g_return_val_if_fail(bufsz != 0, FALSE);
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
/* create helper */
helper.value = value;
helper.buf = buf;
helper.bufsz = bufsz;
helper.timeout = timeout;
helper.flags = priv->flags | flags;
/* special case */
if (flags & FU_HID_DEVICE_FLAG_RETRY_FAILURE) {
return fu_device_retry(FU_DEVICE(self),
fu_hid_device_get_report_internal_cb,
FU_HID_DEVICE_RETRIES,
&helper,
error);
}
/* just one */
return fu_hid_device_get_report_internal(self, &helper, error);
}
static void
fu_hid_device_init(FuHidDevice *self)
{
FuHidDevicePrivate *priv = GET_PRIVATE(self);
priv->interface_autodetect = TRUE;
}
/**
* fu_hid_device_new:
* @usb_device: a USB device
*
* Creates a new HID device.
*
* Returns: (transfer full): a #FuHidDevice
*
* Since: 1.4.0
**/
FuHidDevice *
fu_hid_device_new(GUsbDevice *usb_device)
{
FuHidDevice *device = g_object_new(FU_TYPE_HID_DEVICE, NULL);
fu_usb_device_set_dev(FU_USB_DEVICE(device), usb_device);
return FU_HID_DEVICE(device);
}
static void
fu_hid_device_class_init(FuHidDeviceClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
GObjectClass *object_class = G_OBJECT_CLASS(klass);
GParamSpec *pspec;
object_class->get_property = fu_hid_device_get_property;
object_class->set_property = fu_hid_device_set_property;
klass_device->open = fu_hid_device_open;
klass_device->close = fu_hid_device_close;
pspec = g_param_spec_uint("interface",
NULL,
NULL,
0x00,
0xff,
0x00,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
g_object_class_install_property(object_class, PROP_INTERFACE, pspec);
}