mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-14 19:37:54 +00:00

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
1050 lines
31 KiB
C
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);
|
|
}
|