mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-22 03:47:33 +00:00
495 lines
13 KiB
C
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);
|
|
}
|