fwupd/plugins/logitech-hidpp/fu-logitech-hidpp-peripheral.c
Mario Limonciello de60e04481 logitech_hidpp: decrease verbosity of messages that hid++ ID is missing
These are a regression of 9e755e2a5 when devices are asleep.
However due to the current kernel and daemon architecture, logitech devices
are not checked again at any time so if the device isn't awake when
fwupd is started or the unifying dongle is plugged in it won't be present.

This will be changed in the future when the kernel has change events
associated with devices waking up.

Fixes: #1973
2020-04-14 11:26:22 -05:00

1050 lines
31 KiB
C

/*
* Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com>
*
* SPDX-License-Identifier: LGPL-2.1+
*/
#include "config.h"
#include <string.h>
#include "fu-logitech-hidpp-common.h"
#include "fu-logitech-hidpp-peripheral.h"
#include "fu-logitech-hidpp-hidpp.h"
struct _FuLogitechHidPpPeripheral
{
FuUdevDevice parent_instance;
guint8 battery_level;
guint8 cached_fw_entity;
guint8 hidpp_id;
guint8 hidpp_version;
gboolean is_updatable;
gboolean is_active;
FuIOChannel *io_channel;
GPtrArray *feature_index; /* of FuLogitechHidPpHidppMap */
};
typedef struct {
guint8 idx;
guint16 feature;
} FuLogitechHidPpHidppMap;
G_DEFINE_TYPE (FuLogitechHidPpPeripheral, fu_logitech_hidpp_peripheral, FU_TYPE_UDEV_DEVICE)
typedef enum {
FU_UNIFYING_PERIPHERAL_KIND_KEYBOARD,
FU_UNIFYING_PERIPHERAL_KIND_REMOTE_CONTROL,
FU_UNIFYING_PERIPHERAL_KIND_NUMPAD,
FU_UNIFYING_PERIPHERAL_KIND_MOUSE,
FU_UNIFYING_PERIPHERAL_KIND_TOUCHPAD,
FU_UNIFYING_PERIPHERAL_KIND_TRACKBALL,
FU_UNIFYING_PERIPHERAL_KIND_PRESENTER,
FU_UNIFYING_PERIPHERAL_KIND_RECEIVER,
FU_UNIFYING_PERIPHERAL_KIND_LAST
} FuLogitechHidPpPeripheralKind;
static const gchar *
fu_logitech_hidpp_peripheral_get_icon (FuLogitechHidPpPeripheralKind kind)
{
if (kind == FU_UNIFYING_PERIPHERAL_KIND_KEYBOARD)
return "input-keyboard";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_REMOTE_CONTROL)
return "pda"; // ish
if (kind == FU_UNIFYING_PERIPHERAL_KIND_NUMPAD)
return "input-dialpad";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_MOUSE)
return "input-mouse";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_TOUCHPAD)
return "input-touchpad";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_TRACKBALL)
return "input-mouse"; // ish
if (kind == FU_UNIFYING_PERIPHERAL_KIND_PRESENTER)
return "pda"; // ish
if (kind == FU_UNIFYING_PERIPHERAL_KIND_RECEIVER)
return "preferences-desktop-keyboard";
return NULL;
}
static const gchar *
fu_logitech_hidpp_peripheral_get_summary (FuLogitechHidPpPeripheralKind kind)
{
if (kind == FU_UNIFYING_PERIPHERAL_KIND_KEYBOARD)
return "Unifying Keyboard";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_REMOTE_CONTROL)
return "Unifying Remote Control";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_NUMPAD)
return "Unifying Number Pad";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_MOUSE)
return "Unifying Mouse";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_TOUCHPAD)
return "Unifying Touchpad";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_TRACKBALL)
return "Unifying Trackball";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_PRESENTER)
return "Unifying Presenter";
if (kind == FU_UNIFYING_PERIPHERAL_KIND_RECEIVER)
return "Unifying Receiver";
return NULL;
}
static const gchar *
fu_logitech_hidpp_feature_to_string (guint16 feature)
{
if (feature == HIDPP_FEATURE_ROOT)
return "Root";
if (feature == HIDPP_FEATURE_I_FIRMWARE_INFO)
return "IFirmwareInfo";
if (feature == HIDPP_FEATURE_GET_DEVICE_NAME_TYPE)
return "GetDevicenameType";
if (feature == HIDPP_FEATURE_BATTERY_LEVEL_STATUS)
return "BatteryLevelStatus";
if (feature == HIDPP_FEATURE_DFU_CONTROL)
return "DfuControl";
if (feature == HIDPP_FEATURE_DFU_CONTROL_SIGNED)
return "DfuControlSigned";
if (feature == HIDPP_FEATURE_DFU)
return "Dfu";
return NULL;
}
static void
fu_logitech_hidpp_peripheral_refresh_updatable (FuLogitechHidPpPeripheral *self)
{
/* device can only be upgraded if it is capable, and active */
if (self->is_updatable && self->is_active) {
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
return;
}
fu_device_remove_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
}
static gboolean
fu_logitech_hidpp_peripheral_ping (FuLogitechHidPpPeripheral *self, GError **error)
{
gdouble version;
g_autoptr(GError) error_local = NULL;
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
/* handle failure */
msg->report_id = HIDPP_REPORT_ID_SHORT;
msg->device_id = self->hidpp_id;
msg->sub_id = 0x00; /* rootIndex */
msg->function_id = 0x01 << 4; /* ping */
msg->data[0] = 0x00;
msg->data[1] = 0x00;
msg->data[2] = 0xaa; /* user-selected value */
msg->hidpp_version = self->hidpp_version;
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, &error_local)) {
if (g_error_matches (error_local,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED)) {
self->hidpp_version = 1;
return TRUE;
}
if (g_error_matches (error_local,
G_IO_ERROR,
G_IO_ERROR_HOST_UNREACHABLE)) {
self->is_active = FALSE;
fu_logitech_hidpp_peripheral_refresh_updatable (self);
return TRUE;
}
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"failed to ping %s: %s",
fu_device_get_name (FU_DEVICE (self)),
error_local->message);
return FALSE;
}
/* device no longer asleep */
self->is_active = TRUE;
fu_logitech_hidpp_peripheral_refresh_updatable (self);
/* if the HID++ ID is unset, grab it from the reply */
if (self->hidpp_id == HIDPP_DEVICE_ID_UNSET &&
msg->device_id != HIDPP_DEVICE_ID_UNSET) {
self->hidpp_id = msg->device_id;
g_debug ("HID++ ID is %02x", self->hidpp_id);
}
/* format version in BCD format */
version = (gdouble) msg->data[0] + ((gdouble) msg->data[1]) / 100.f;
self->hidpp_version = (guint) version;
/* success */
return TRUE;
}
static gboolean
fu_logitech_hidpp_peripheral_close (FuDevice *device, GError **error)
{
FuLogitechHidPpPeripheral *self = FU_UNIFYING_PERIPHERAL (device);
if (!fu_io_channel_shutdown (self->io_channel, error))
return FALSE;
g_clear_object (&self->io_channel);
return TRUE;
}
static gboolean
fu_logitech_hidpp_peripheral_poll (FuDevice *device, GError **error)
{
FuLogitechHidPpPeripheral *self = FU_UNIFYING_PERIPHERAL (device);
const guint timeout = 1; /* ms */
g_autoptr(GError) error_local = NULL;
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
g_autoptr(FuDeviceLocker) locker = NULL;
/* open */
locker = fu_device_locker_new (self, error);
if (locker == NULL)
return FALSE;
/* flush pending data */
msg->device_id = self->hidpp_id;
msg->hidpp_version = self->hidpp_version;
if (!fu_logitech_hidpp_receive (self->io_channel, msg, timeout, &error_local)) {
if (!g_error_matches (error_local,
G_IO_ERROR,
G_IO_ERROR_TIMED_OUT)) {
g_warning ("failed to get pending read: %s", error_local->message);
return TRUE;
}
/* no data to receive */
g_clear_error (&error_local);
}
/* just ping */
if (!fu_logitech_hidpp_peripheral_ping (self, &error_local)) {
g_warning ("failed to ping device: %s", error_local->message);
return TRUE;
}
/* this is the first time the device has been active */
if (self->feature_index->len == 0) {
fu_device_probe_invalidate (FU_DEVICE (self));
if (!fu_device_setup (FU_DEVICE (self), error))
return FALSE;
}
/* success */
return TRUE;
}
static gboolean
fu_logitech_hidpp_peripheral_open (FuDevice *device, GError **error)
{
FuLogitechHidPpPeripheral *self = FU_UNIFYING_PERIPHERAL (device);
GUdevDevice *udev_device = fu_udev_device_get_dev (FU_UDEV_DEVICE (device));
const gchar *devpath = g_udev_device_get_device_file (udev_device);
/* open */
self->io_channel = fu_io_channel_new_file (devpath, error);
if (self->io_channel == NULL)
return FALSE;
return TRUE;
}
static void
fu_logitech_hidpp_map_to_string (FuLogitechHidPpHidppMap *map, guint idt, GString *str)
{
g_autofree gchar *title = g_strdup_printf ("Feature%02x", map->idx);
g_autofree gchar *tmp = g_strdup_printf ("%s [0x%04x]",
fu_logitech_hidpp_feature_to_string (map->feature),
map->feature);
fu_common_string_append_kv (str, idt, title, tmp);
}
static void
fu_logitech_hidpp_peripheral_to_string (FuDevice *device, guint idt, GString *str)
{
FuLogitechHidPpPeripheral *self = FU_UNIFYING_PERIPHERAL (device);
fu_common_string_append_ku (str, idt, "HidppVersion", self->hidpp_version);
fu_common_string_append_kx (str, idt, "HidppId", self->hidpp_id);
fu_common_string_append_ku (str, idt, "BatteryLevel", self->battery_level);
fu_common_string_append_kb (str, idt, "IsUpdatable", self->is_updatable);
fu_common_string_append_kb (str, idt, "IsActive", self->is_active);
for (guint i = 0; i < self->feature_index->len; i++) {
FuLogitechHidPpHidppMap *map = g_ptr_array_index (self->feature_index, i);
fu_logitech_hidpp_map_to_string (map, idt, str);
}
}
static guint8
fu_logitech_hidpp_peripheral_feature_get_idx (FuLogitechHidPpPeripheral *self, guint16 feature)
{
for (guint i = 0; i < self->feature_index->len; i++) {
FuLogitechHidPpHidppMap *map = g_ptr_array_index (self->feature_index, i);
if (map->feature == feature)
return map->idx;
}
return 0x00;
}
static gboolean
fu_logitech_hidpp_peripheral_fetch_firmware_info (FuLogitechHidPpPeripheral *self, GError **error)
{
guint8 idx;
guint8 entity_count;
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
/* get the feature index */
idx = fu_logitech_hidpp_peripheral_feature_get_idx (self, HIDPP_FEATURE_I_FIRMWARE_INFO);
if (idx == 0x00)
return TRUE;
/* get the entity count */
msg->report_id = HIDPP_REPORT_ID_SHORT;
msg->device_id = self->hidpp_id;
msg->sub_id = idx;
msg->function_id = 0x00 << 4; /* getCount */
msg->hidpp_version = self->hidpp_version;
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, error)) {
g_prefix_error (error, "failed to get firmware count: ");
return FALSE;
}
entity_count = msg->data[0];
g_debug ("firmware entity count is %u", entity_count);
/* get firmware, bootloader, hardware versions */
for (guint8 i = 0; i < entity_count; i++) {
guint16 build;
g_autofree gchar *version = NULL;
g_autofree gchar *name = NULL;
msg->report_id = HIDPP_REPORT_ID_SHORT;
msg->device_id = self->hidpp_id;
msg->sub_id = idx;
msg->function_id = 0x01 << 4; /* getInfo */
msg->data[0] = i;
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, error)) {
g_prefix_error (error, "failed to get firmware info: ");
return FALSE;
}
if (msg->data[1] == 0x00 &&
msg->data[2] == 0x00 &&
msg->data[3] == 0x00 &&
msg->data[4] == 0x00 &&
msg->data[5] == 0x00 &&
msg->data[6] == 0x00 &&
msg->data[7] == 0x00) {
g_debug ("no version set for entity %u", i);
continue;
}
name = g_strdup_printf ("%c%c%c",
msg->data[1],
msg->data[2],
msg->data[3]);
build = ((guint16) msg->data[6]) << 8 | msg->data[7];
version = fu_logitech_hidpp_format_version (name,
msg->data[4],
msg->data[5],
build);
g_debug ("firmware entity 0x%02x version is %s", i, version);
if (msg->data[0] == 0) {
fu_device_set_version (FU_DEVICE (self), version);
self->cached_fw_entity = i;
} else if (msg->data[0] == 1) {
fu_device_set_version_bootloader (FU_DEVICE (self), version);
} else if (msg->data[0] == 2) {
fu_device_set_metadata (FU_DEVICE (self), "version-hw", version);
}
}
/* not an error, the device just doesn't support this */
return TRUE;
}
static gboolean
fu_logitech_hidpp_peripheral_fetch_battery_level (FuLogitechHidPpPeripheral *self, GError **error)
{
/* try using HID++2.0 */
if (self->hidpp_version >= 2.f) {
guint8 idx;
idx = fu_logitech_hidpp_peripheral_feature_get_idx (self, HIDPP_FEATURE_BATTERY_LEVEL_STATUS);
if (idx != 0x00) {
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
msg->report_id = HIDPP_REPORT_ID_SHORT;
msg->device_id = self->hidpp_id;
msg->sub_id = idx;
msg->function_id = 0x00; /* GetBatteryLevelStatus */
msg->hidpp_version = self->hidpp_version;
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, error)) {
g_prefix_error (error, "failed to get battery info: ");
return FALSE;
}
if (msg->data[0] != 0x00)
self->battery_level = msg->data[0];
return TRUE;
}
}
/* try HID++1.0 battery mileage */
if (self->hidpp_version == 1.f) {
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
msg->report_id = HIDPP_REPORT_ID_SHORT;
msg->device_id = self->hidpp_id;
msg->sub_id = HIDPP_SUBID_GET_REGISTER;
msg->function_id = HIDPP_REGISTER_BATTERY_MILEAGE;
msg->hidpp_version = self->hidpp_version;
if (fu_logitech_hidpp_transfer (self->io_channel, msg, NULL)) {
if (msg->data[0] != 0x00)
self->battery_level = msg->data[0];
return TRUE;
}
/* try HID++1.0 battery status instead */
msg->function_id = HIDPP_REGISTER_BATTERY_STATUS;
if (fu_logitech_hidpp_transfer (self->io_channel, msg, NULL)) {
switch (msg->data[0]) {
case 1: /* 0 - 10 */
self->battery_level = 5;
break;
case 3: /* 11 - 30 */
self->battery_level = 20;
break;
case 5: /* 31 - 80 */
self->battery_level = 55;
break;
case 7: /* 81 - 100 */
self->battery_level = 90;
break;
default:
g_warning ("unknown battery percentage: 0x%02x",
msg->data[0]);
break;
}
return TRUE;
}
}
/* not an error, the device just doesn't support any of the methods */
return TRUE;
}
static gboolean
fu_logitech_hidpp_feature_search (FuDevice *device, guint16 feature, GError **error)
{
FuLogitechHidPpPeripheral *self = FU_UNIFYING_PERIPHERAL (device);
FuLogitechHidPpHidppMap *map;
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
/* find the idx for the feature */
msg->report_id = HIDPP_REPORT_ID_SHORT;
msg->device_id = self->hidpp_id;
msg->sub_id = 0x00; /* rootIndex */
msg->function_id = 0x00 << 4; /* getFeature */
msg->data[0] = feature >> 8;
msg->data[1] = feature;
msg->data[2] = 0x00;
msg->hidpp_version = self->hidpp_version;
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, error)) {
g_prefix_error (error,
"failed to get idx for feature %s [0x%04x]: ",
fu_logitech_hidpp_feature_to_string (feature), feature);
return FALSE;
}
/* zero index */
if (msg->data[0] == 0x00) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_NOT_SUPPORTED,
"feature %s [0x%04x] not found",
fu_logitech_hidpp_feature_to_string (feature), feature);
return FALSE;
}
/* add to map */
map = g_new0 (FuLogitechHidPpHidppMap, 1);
map->idx = msg->data[0];
map->feature = feature;
g_ptr_array_add (self->feature_index, map);
g_debug ("added feature %s [0x%04x] as idx %02x",
fu_logitech_hidpp_feature_to_string (feature), feature, map->idx);
return TRUE;
}
static gboolean
fu_logitech_hidpp_peripheral_probe (FuUdevDevice *device, GError **error)
{
g_autofree gchar *devid = NULL;
/* set the physical ID */
if (!fu_udev_device_set_physical_id (device, "hid", error))
return FALSE;
/* nearly... */
fu_device_set_vendor_id (FU_DEVICE (device), "USB:0x046D");
/* this is a non-standard extension */
devid = g_strdup_printf ("UFY\\VID_%04X&PID_%04X",
fu_udev_device_get_vendor (device),
fu_udev_device_get_model (device));
fu_device_add_instance_id (FU_DEVICE (device), devid);
return TRUE;
}
static gboolean
fu_logitech_hidpp_peripheral_setup (FuDevice *device, GError **error)
{
FuLogitechHidPpPeripheral *self = FU_UNIFYING_PERIPHERAL (device);
guint8 idx;
const guint16 map_features[] = {
HIDPP_FEATURE_GET_DEVICE_NAME_TYPE,
HIDPP_FEATURE_I_FIRMWARE_INFO,
HIDPP_FEATURE_BATTERY_LEVEL_STATUS,
HIDPP_FEATURE_DFU_CONTROL,
HIDPP_FEATURE_DFU_CONTROL_SIGNED,
HIDPP_FEATURE_DFU,
HIDPP_FEATURE_ROOT };
/* ping device to get HID++ version */
if (!fu_logitech_hidpp_peripheral_ping (self, error))
return FALSE;
/* did not get ID */
if (self->hidpp_id == HIDPP_DEVICE_ID_UNSET) {
g_set_error_literal (error,
FWUPD_ERROR,
FWUPD_ERROR_NOT_SUPPORTED,
"no HID++ ID");
return FALSE;
}
/* add known root for HID++2.0 */
g_ptr_array_set_size (self->feature_index, 0);
if (self->hidpp_version >= 2.f) {
FuLogitechHidPpHidppMap *map = g_new0 (FuLogitechHidPpHidppMap, 1);
map->idx = 0x00;
map->feature = HIDPP_FEATURE_ROOT;
g_ptr_array_add (self->feature_index, map);
}
/* map some *optional* HID++2.0 features we might use */
for (guint i = 0; map_features[i] != HIDPP_FEATURE_ROOT; i++) {
g_autoptr(GError) error_local = NULL;
if (!fu_logitech_hidpp_feature_search (device,
map_features[i],
&error_local)) {
g_debug ("%s", error_local->message);
if (g_error_matches (error_local,
G_IO_ERROR,
G_IO_ERROR_TIMED_OUT) ||
g_error_matches (error_local,
G_IO_ERROR,
G_IO_ERROR_HOST_UNREACHABLE)) {
/* timed out, so not trying any more */
break;
}
}
}
/* get the firmware information */
if (!fu_logitech_hidpp_peripheral_fetch_firmware_info (self, error))
return FALSE;
/* get the battery level */
if (!fu_logitech_hidpp_peripheral_fetch_battery_level (self, error))
return FALSE;
/* try using HID++2.0 */
idx = fu_logitech_hidpp_peripheral_feature_get_idx (self, HIDPP_FEATURE_GET_DEVICE_NAME_TYPE);
if (idx != 0x00) {
const gchar *tmp;
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
msg->report_id = HIDPP_REPORT_ID_SHORT;
msg->device_id = self->hidpp_id;
msg->sub_id = idx;
msg->function_id = 0x02 << 4; /* getDeviceType */
msg->hidpp_version = self->hidpp_version;
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, error)) {
g_prefix_error (error, "failed to get device type: ");
return FALSE;
}
/* add nice-to-have data */
tmp = fu_logitech_hidpp_peripheral_get_summary (msg->data[0]);
if (tmp != NULL)
fu_device_set_summary (FU_DEVICE (device), tmp);
tmp = fu_logitech_hidpp_peripheral_get_icon (msg->data[0]);
if (tmp != NULL)
fu_device_add_icon (FU_DEVICE (device), tmp);
}
idx = fu_logitech_hidpp_peripheral_feature_get_idx (self, HIDPP_FEATURE_DFU_CONTROL);
if (idx != 0x00) {
self->is_updatable = TRUE;
fu_device_remove_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
}
idx = fu_logitech_hidpp_peripheral_feature_get_idx (self, HIDPP_FEATURE_DFU_CONTROL_SIGNED);
if (idx != 0x00) {
/* check the feature is available */
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
msg->report_id = HIDPP_REPORT_ID_SHORT;
msg->device_id = self->hidpp_id;
msg->sub_id = idx;
msg->function_id = 0x00 << 4; /* getDfuStatus */
msg->hidpp_version = self->hidpp_version;
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, error)) {
g_prefix_error (error, "failed to get DFU status: ");
return FALSE;
}
if ((msg->data[2] & 0x01) > 0) {
g_warning ("DFU mode not available");
} else {
self->is_updatable = TRUE;
fu_device_remove_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
}
fu_device_set_protocol (FU_DEVICE (device), "com.logitech.unifyingsigned");
}
idx = fu_logitech_hidpp_peripheral_feature_get_idx (self, HIDPP_FEATURE_DFU);
if (idx != 0x00) {
self->is_updatable = TRUE;
fu_device_add_flag (FU_DEVICE (device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
if (fu_device_get_version (device) == NULL) {
g_debug ("repairing device in bootloader mode");
fu_device_set_version (FU_DEVICE (device), "MPK00.00_B0000");
}
}
/* this device may have changed state */
fu_logitech_hidpp_peripheral_refresh_updatable (self);
/* poll for pings to track active state */
fu_device_set_poll_interval (device, 30000);
return TRUE;
}
static gboolean
fu_logitech_hidpp_peripheral_detach (FuDevice *device, GError **error)
{
FuLogitechHidPpPeripheral *self = FU_UNIFYING_PERIPHERAL (device);
guint8 idx;
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
/* sanity check */
if (fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
g_debug ("already in bootloader mode, skipping");
return TRUE;
}
/* this requires user action */
idx = fu_logitech_hidpp_peripheral_feature_get_idx (self, HIDPP_FEATURE_DFU_CONTROL);
if (idx != 0x00) {
msg->report_id = HIDPP_REPORT_ID_LONG;
msg->device_id = self->hidpp_id;
msg->sub_id = idx;
msg->function_id = 0x01 << 4; /* setDfuControl */
msg->data[0] = 0x01; /* enterDfu */
msg->data[1] = 0x00; /* dfuControlParam */
msg->data[2] = 0x00; /* unused */
msg->data[3] = 0x00; /* unused */
msg->data[4] = 'D';
msg->data[5] = 'F';
msg->data[6] = 'U';
msg->hidpp_version = self->hidpp_version;
msg->flags = FU_UNIFYING_HIDPP_MSG_FLAG_IGNORE_SUB_ID |
FU_UNIFYING_HIDPP_MSG_FLAG_LONGER_TIMEOUT;
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, error)) {
g_prefix_error (error, "failed to put device into DFU mode: ");
return FALSE;
}
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
g_set_error (error,
FWUPD_ERROR,
FWUPD_ERROR_NEEDS_USER_ACTION,
"%s needs to be manually restarted to complete the update."
"Please unplug and reconnect the device and re-run the update",
fu_device_get_name (device));
return FALSE;
}
/* this can reboot all by itself */
idx = fu_logitech_hidpp_peripheral_feature_get_idx (self, HIDPP_FEATURE_DFU_CONTROL_SIGNED);
if (idx != 0x00) {
msg->report_id = HIDPP_REPORT_ID_LONG;
msg->device_id = self->hidpp_id;
msg->sub_id = idx;
msg->function_id = 0x01 << 4; /* setDfuControl */
msg->data[0] = 0x01; /* startDfu */
msg->data[1] = 0x00; /* dfuControlParam */
msg->data[2] = 0x00; /* unused */
msg->data[3] = 0x00; /* unused */
msg->data[4] = 'D';
msg->data[5] = 'F';
msg->data[6] = 'U';
msg->flags = FU_UNIFYING_HIDPP_MSG_FLAG_IGNORE_SUB_ID;
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, error)) {
g_prefix_error (error, "failed to put device into DFU mode: ");
return FALSE;
}
return fu_logitech_hidpp_peripheral_setup (FU_DEVICE (self), error);
}
/* we don't know how */
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"no method to detach");
return FALSE;
}
static gboolean
fu_logitech_hidpp_peripheral_check_status (guint8 status, GError **error)
{
switch (status & 0x7f) {
case 0x00:
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"invalid status value 0x%02x",
status);
break;
case 0x01: /* packet success */
case 0x02: /* DFU success */
case 0x05: /* DFU success: entity restart required */
case 0x06: /* DFU success: system restart required */
/* success */
return TRUE;
break;
case 0x03:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_PENDING,
"wait for event (command in progress)");
break;
case 0x04:
case 0x10: /* unknown */
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"generic error");
break;
case 0x11:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"bad voltage (power too low?)");
break;
case 0x12:
case 0x14: /* bad magic string */
case 0x21: /* bad firmware */
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"unsupported firmware");
break;
case 0x13:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"unsupported encryption mode");
break;
case 0x15:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"erase failure");
break;
case 0x16:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"DFU not started");
break;
case 0x17:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"bad sequence number");
break;
case 0x18:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"unsupported command");
break;
case 0x19:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"command in progress");
break;
case 0x1a:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"address out of range");
break;
case 0x1b:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"unaligned address");
break;
case 0x1c:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"bad size");
break;
case 0x1d:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"missing program data");
break;
case 0x1e:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"missing check data");
break;
case 0x1f:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"program failed to write");
break;
case 0x20:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"program failed to verify");
break;
case 0x22:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"firmware check failure");
break;
case 0x23:
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"blocked command (restart required)");
break;
default:
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"unhandled status value 0x%02x",
status);
break;
}
return FALSE;
}
static gboolean
fu_logitech_hidpp_peripheral_write_firmware_pkt (FuLogitechHidPpPeripheral *self,
guint8 idx,
guint8 cmd,
const guint8 *data,
GError **error)
{
guint32 packet_cnt;
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
g_autoptr(GError) error_local = NULL;
/* send firmware data */
msg->report_id = HIDPP_REPORT_ID_LONG;
msg->device_id = self->hidpp_id;
msg->sub_id = idx;
msg->function_id = cmd << 4; /* dfuStart or dfuCmdDataX */
msg->hidpp_version = self->hidpp_version;
memcpy (msg->data, data, 16);
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, &error_local)) {
g_prefix_error (error, "failed to supply program data: ");
return FALSE;
}
/* check error */
packet_cnt = fu_common_read_uint32 (msg->data, G_BIG_ENDIAN);
g_debug ("packet_cnt=0x%04x", packet_cnt);
if (fu_logitech_hidpp_peripheral_check_status (msg->data[4], &error_local))
return TRUE;
/* fatal error */
if (!g_error_matches (error_local,
G_IO_ERROR,
G_IO_ERROR_PENDING)) {
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
error_local->message);
return FALSE;
}
/* wait for the HID++ notification */
g_debug ("ignoring: %s", error_local->message);
for (guint retry = 0; retry < 10; retry++) {
g_autoptr(FuLogitechHidPpHidppMsg) msg2 = fu_logitech_hidpp_msg_new ();
msg2->flags = FU_UNIFYING_HIDPP_MSG_FLAG_IGNORE_FNCT_ID;
if (!fu_logitech_hidpp_receive (self->io_channel, msg2, 15000, error))
return FALSE;
if (fu_logitech_hidpp_msg_is_reply (msg, msg2)) {
g_autoptr(GError) error2 = NULL;
if (!fu_logitech_hidpp_peripheral_check_status (msg2->data[4], &error2)) {
g_debug ("got %s, waiting a bit longer", error2->message);
continue;
}
return TRUE;
} else {
g_debug ("got wrong packet, continue to wait...");
}
}
/* nothing in the queue */
g_set_error_literal (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"failed to get event after timeout");
return FALSE;
}
static gboolean
fu_logitech_hidpp_peripheral_write_firmware (FuDevice *device,
FuFirmware *firmware,
FwupdInstallFlags flags,
GError **error)
{
FuLogitechHidPpPeripheral *self = FU_UNIFYING_PERIPHERAL (device);
gsize sz = 0;
const guint8 *data;
guint8 cmd = 0x04;
guint8 idx;
g_autoptr(GBytes) fw = NULL;
/* if we're in bootloader mode, we should be able to get this feature */
idx = fu_logitech_hidpp_peripheral_feature_get_idx (self, HIDPP_FEATURE_DFU);
if (idx == 0x00) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"no DFU feature available");
return FALSE;
}
/* get default image */
fw = fu_firmware_get_image_default_bytes (firmware, error);
if (fw == NULL)
return FALSE;
/* flash hardware */
data = g_bytes_get_data (fw, &sz);
fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
for (gsize i = 0; i < sz / 16; i++) {
/* send packet and wait for reply */
g_debug ("send data at addr=0x%04x", (guint) i * 16);
if (!fu_logitech_hidpp_peripheral_write_firmware_pkt (self,
idx,
cmd,
data + (i * 16),
error)) {
g_prefix_error (error,
"failed to write @0x%04x: ",
(guint) i * 16);
return FALSE;
}
/* use sliding window */
cmd = (cmd + 1) % 4;
/* update progress-bar */
fu_device_set_progress_full (device, i * 16, sz);
}
return TRUE;
}
static gboolean
fu_logitech_hidpp_peripheral_attach (FuDevice *device, GError **error)
{
FuLogitechHidPpPeripheral *self = FU_UNIFYING_PERIPHERAL (device);
guint8 idx;
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new ();
/* sanity check */
if (!fu_device_has_flag (device, FWUPD_DEVICE_FLAG_IS_BOOTLOADER)) {
g_debug ("already in runtime mode, skipping");
return TRUE;
}
/* if we're in bootloader mode, we should be able to get this feature */
idx = fu_logitech_hidpp_peripheral_feature_get_idx (self, HIDPP_FEATURE_DFU);
if (idx == 0x00) {
g_set_error (error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"no DFU feature available");
return FALSE;
}
/* reboot back into firmware mode */
msg->report_id = HIDPP_REPORT_ID_SHORT;
msg->device_id = self->hidpp_id;
msg->sub_id = idx;
msg->function_id = 0x05 << 4; /* restart */
msg->data[0] = self->cached_fw_entity; /* fwEntity */
msg->hidpp_version = self->hidpp_version;
msg->flags = FU_UNIFYING_HIDPP_MSG_FLAG_IGNORE_SUB_ID |
FU_UNIFYING_HIDPP_MSG_FLAG_IGNORE_SWID | // inferred?
FU_UNIFYING_HIDPP_MSG_FLAG_LONGER_TIMEOUT;
if (!fu_logitech_hidpp_transfer (self->io_channel, msg, error)) {
g_prefix_error (error, "failed to restart device: ");
return FALSE;
}
/* reprobe */
if (!fu_logitech_hidpp_peripheral_setup (device, error))
return FALSE;
/* success */
return TRUE;
}
static void
fu_logitech_hidpp_peripheral_finalize (GObject *object)
{
FuLogitechHidPpPeripheral *self = FU_UNIFYING_PERIPHERAL (object);
g_ptr_array_unref (self->feature_index);
G_OBJECT_CLASS (fu_logitech_hidpp_peripheral_parent_class)->finalize (object);
}
static void
fu_logitech_hidpp_peripheral_class_init (FuLogitechHidPpPeripheralClass *klass)
{
FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
FuUdevDeviceClass *klass_device_udev = FU_UDEV_DEVICE_CLASS (klass);
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = fu_logitech_hidpp_peripheral_finalize;
klass_device->setup = fu_logitech_hidpp_peripheral_setup;
klass_device->open = fu_logitech_hidpp_peripheral_open;
klass_device->close = fu_logitech_hidpp_peripheral_close;
klass_device->write_firmware = fu_logitech_hidpp_peripheral_write_firmware;
klass_device->attach = fu_logitech_hidpp_peripheral_attach;
klass_device->detach = fu_logitech_hidpp_peripheral_detach;
klass_device->poll = fu_logitech_hidpp_peripheral_poll;
klass_device->to_string = fu_logitech_hidpp_peripheral_to_string;
klass_device_udev->probe = fu_logitech_hidpp_peripheral_probe;
}
static void
fu_logitech_hidpp_peripheral_init (FuLogitechHidPpPeripheral *self)
{
self->hidpp_id = HIDPP_DEVICE_ID_UNSET;
self->feature_index = g_ptr_array_new_with_free_func (g_free);
fu_device_add_parent_guid (FU_DEVICE (self), "HIDRAW\\VEN_046D&DEV_C52B");
fu_device_set_remove_delay (FU_DEVICE (self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE);
fu_device_set_protocol (FU_DEVICE (self), "com.logitech.unifying");
fu_device_set_version_format (FU_DEVICE (self), FWUPD_VERSION_FORMAT_PLAIN);
/* there are a lot of unifying peripherals, but not all respond
* well to opening -- so limit to ones with issued updates */
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_ONLY_SUPPORTED);
}