mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-25 22:40:50 +00:00
1413 lines
46 KiB
C
1413 lines
46 KiB
C
/*
|
|
* Copyright (C) 2017 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-device.h"
|
|
#include "fu-logitech-hidpp-hidpp.h"
|
|
#include "fu-logitech-hidpp-radio.h"
|
|
#include "fu-logitech-hidpp-runtime-bolt.h"
|
|
|
|
typedef struct {
|
|
guint8 cached_fw_entity;
|
|
/*
|
|
* Device index:
|
|
* - HIDPP_DEVICE_IDX_RECEIVER for the receiver
|
|
* - HIDPP_DEVICE_IDX_BLE for BLE devices
|
|
* - pairing slot for paired Bolt devices.
|
|
*/
|
|
guint8 device_idx;
|
|
guint16 hidpp_pid;
|
|
guint8 hidpp_version;
|
|
FuIOChannel *io_channel;
|
|
gchar *model_id;
|
|
GPtrArray *feature_index; /* of FuLogitechHidPpHidppMap */
|
|
} FuLogitechHidPpDevicePrivate;
|
|
|
|
typedef struct {
|
|
guint8 idx;
|
|
guint16 feature;
|
|
} FuLogitechHidPpHidppMap;
|
|
|
|
G_DEFINE_TYPE_WITH_PRIVATE(FuLogitechHidPpDevice, fu_logitech_hidpp_device, FU_TYPE_UDEV_DEVICE)
|
|
|
|
#define GET_PRIVATE(o) (fu_logitech_hidpp_device_get_instance_private(o))
|
|
|
|
typedef enum {
|
|
FU_HIDPP_DEVICE_KIND_KEYBOARD,
|
|
FU_HIDPP_DEVICE_KIND_REMOTE_CONTROL,
|
|
FU_HIDPP_DEVICE_KIND_NUMPAD,
|
|
FU_HIDPP_DEVICE_KIND_MOUSE,
|
|
FU_HIDPP_DEVICE_KIND_TOUCHPAD,
|
|
FU_HIDPP_DEVICE_KIND_TRACKBALL,
|
|
FU_HIDPP_DEVICE_KIND_PRESENTER,
|
|
FU_HIDPP_DEVICE_KIND_RECEIVER,
|
|
FU_HIDPP_DEVICE_KIND_LAST
|
|
} FuLogitechHidPpDeviceKind;
|
|
|
|
void
|
|
fu_logitech_hidpp_device_set_device_idx(FuLogitechHidPpDevice *self, guint8 device_idx)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv;
|
|
g_return_if_fail(FU_IS_HIDPP_DEVICE(self));
|
|
priv = GET_PRIVATE(self);
|
|
priv->device_idx = device_idx;
|
|
}
|
|
|
|
guint16
|
|
fu_logitech_hidpp_device_get_hidpp_pid(FuLogitechHidPpDevice *self)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
g_return_val_if_fail(FU_IS_HIDPP_DEVICE(self), G_MAXUINT16);
|
|
return priv->hidpp_pid;
|
|
}
|
|
|
|
void
|
|
fu_logitech_hidpp_device_set_hidpp_pid(FuLogitechHidPpDevice *self, guint16 hidpp_pid)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
g_return_if_fail(FU_IS_HIDPP_DEVICE(self));
|
|
priv->hidpp_pid = hidpp_pid;
|
|
}
|
|
|
|
const gchar *
|
|
fu_logitech_hidpp_device_get_model_id(FuLogitechHidPpDevice *self)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
g_return_val_if_fail(FU_IS_HIDPP_DEVICE(self), NULL);
|
|
return priv->model_id;
|
|
}
|
|
|
|
static void
|
|
fu_logitech_hidpp_device_set_model_id(FuLogitechHidPpDevice *self, const gchar *model_id)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
g_return_if_fail(FU_IS_HIDPP_DEVICE(self));
|
|
if (g_strcmp0(priv->model_id, model_id) == 0)
|
|
return;
|
|
g_free(priv->model_id);
|
|
priv->model_id = g_strdup(model_id);
|
|
}
|
|
|
|
static const gchar *
|
|
fu_logitech_hidpp_device_get_icon(FuLogitechHidPpDeviceKind kind)
|
|
{
|
|
if (kind == FU_HIDPP_DEVICE_KIND_KEYBOARD)
|
|
return "input-keyboard";
|
|
if (kind == FU_HIDPP_DEVICE_KIND_REMOTE_CONTROL)
|
|
return "pda"; // ish
|
|
if (kind == FU_HIDPP_DEVICE_KIND_NUMPAD)
|
|
return "input-dialpad";
|
|
if (kind == FU_HIDPP_DEVICE_KIND_MOUSE)
|
|
return "input-mouse";
|
|
if (kind == FU_HIDPP_DEVICE_KIND_TOUCHPAD)
|
|
return "input-touchpad";
|
|
if (kind == FU_HIDPP_DEVICE_KIND_TRACKBALL)
|
|
return "input-mouse"; // ish
|
|
if (kind == FU_HIDPP_DEVICE_KIND_PRESENTER)
|
|
return "pda"; // ish
|
|
if (kind == FU_HIDPP_DEVICE_KIND_RECEIVER)
|
|
return "preferences-desktop-keyboard";
|
|
return NULL;
|
|
}
|
|
|
|
static const gchar *
|
|
fu_logitech_hidpp_device_get_summary(FuLogitechHidPpDeviceKind kind)
|
|
{
|
|
if (kind == FU_HIDPP_DEVICE_KIND_KEYBOARD)
|
|
return "Unifying Keyboard";
|
|
if (kind == FU_HIDPP_DEVICE_KIND_REMOTE_CONTROL)
|
|
return "Unifying Remote Control";
|
|
if (kind == FU_HIDPP_DEVICE_KIND_NUMPAD)
|
|
return "Unifying Number Pad";
|
|
if (kind == FU_HIDPP_DEVICE_KIND_MOUSE)
|
|
return "Unifying Mouse";
|
|
if (kind == FU_HIDPP_DEVICE_KIND_TOUCHPAD)
|
|
return "Unifying Touchpad";
|
|
if (kind == FU_HIDPP_DEVICE_KIND_TRACKBALL)
|
|
return "Unifying Trackball";
|
|
if (kind == FU_HIDPP_DEVICE_KIND_PRESENTER)
|
|
return "Unifying Presenter";
|
|
if (kind == FU_HIDPP_DEVICE_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_UNIFIED_BATTERY)
|
|
return "UnifiedBattery";
|
|
if (feature == HIDPP_FEATURE_DFU_CONTROL)
|
|
return "DfuControl";
|
|
if (feature == HIDPP_FEATURE_DFU_CONTROL_SIGNED)
|
|
return "DfuControlSigned";
|
|
if (feature == HIDPP_FEATURE_DFU_CONTROL_BOLT)
|
|
return "DfuControlBolt";
|
|
if (feature == HIDPP_FEATURE_DFU)
|
|
return "Dfu";
|
|
return NULL;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_ping(FuLogitechHidPpDevice *self, GError **error)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
gdouble version;
|
|
g_autoptr(GError) error_local = NULL;
|
|
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new();
|
|
GPtrArray *children = NULL;
|
|
|
|
/* handle failure */
|
|
msg->report_id = HIDPP_REPORT_ID_SHORT;
|
|
msg->device_id = priv->device_idx;
|
|
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 = priv->hidpp_version;
|
|
if (!fu_logitech_hidpp_transfer(priv->io_channel, msg, &error_local)) {
|
|
if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED)) {
|
|
priv->hidpp_version = 1;
|
|
return TRUE;
|
|
}
|
|
if (g_error_matches(error_local, G_IO_ERROR, G_IO_ERROR_HOST_UNREACHABLE)) {
|
|
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNREACHABLE);
|
|
fu_device_inhibit(FU_DEVICE(self), "unreachable", "device is unreachable");
|
|
return TRUE;
|
|
}
|
|
g_propagate_error(error, g_steal_pointer(&error_local));
|
|
return FALSE;
|
|
}
|
|
|
|
/* device no longer asleep */
|
|
fu_device_remove_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNREACHABLE);
|
|
fu_device_uninhibit(FU_DEVICE(self), "unreachable");
|
|
children = fu_device_get_children(FU_DEVICE(self));
|
|
for (guint i = 0; i < children->len; i++) {
|
|
FuDevice *radio = g_ptr_array_index(children, i);
|
|
fu_device_remove_flag(radio, FWUPD_DEVICE_FLAG_UNREACHABLE);
|
|
fu_device_uninhibit(radio, "unreachable");
|
|
}
|
|
|
|
/* if the device index is unset, grab it from the reply */
|
|
if (priv->device_idx == HIDPP_DEVICE_IDX_UNSET &&
|
|
msg->device_id != HIDPP_DEVICE_IDX_UNSET) {
|
|
priv->device_idx = msg->device_id;
|
|
g_debug("Device index is %02x", priv->device_idx);
|
|
}
|
|
|
|
/* format version in BCD format */
|
|
if (priv->hidpp_version != FU_HIDPP_VERSION_BLE) {
|
|
version = (gdouble)msg->data[0] + ((gdouble)msg->data[1]) / 100.f;
|
|
priv->hidpp_version = (guint)version;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_close(FuDevice *device, GError **error)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
|
|
if (priv->io_channel != NULL) {
|
|
if (!fu_io_channel_shutdown(priv->io_channel, error))
|
|
return FALSE;
|
|
g_clear_object(&priv->io_channel);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_poll(FuDevice *device, GError **error)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
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 = priv->device_idx;
|
|
msg->hidpp_version = priv->hidpp_version;
|
|
if (!fu_logitech_hidpp_receive(priv->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_device_ping(self, &error_local)) {
|
|
g_warning("failed to ping %s: %s",
|
|
fu_device_get_name(FU_DEVICE(self)),
|
|
error_local->message);
|
|
return TRUE;
|
|
}
|
|
|
|
/* this is the first time the device has been active */
|
|
if (priv->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_device_open(FuDevice *device, GError **error)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
GUdevDevice *udev_device = fu_udev_device_get_dev(FU_UDEV_DEVICE(device));
|
|
const gchar *devpath = g_udev_device_get_device_file(udev_device);
|
|
|
|
/* open */
|
|
priv->io_channel = fu_io_channel_new_file(devpath, error);
|
|
if (priv->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_device_to_string(FuDevice *device, guint idt, GString *str)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
|
|
/* FuUdevDevice->to_string */
|
|
FU_DEVICE_CLASS(fu_logitech_hidpp_device_parent_class)->to_string(device, idt, str);
|
|
|
|
fu_common_string_append_ku(str, idt, "HidppVersion", priv->hidpp_version);
|
|
fu_common_string_append_ku(str, idt, "HidppPid", priv->hidpp_pid);
|
|
fu_common_string_append_kx(str, idt, "DeviceIdx", priv->device_idx);
|
|
fu_common_string_append_kv(str, idt, "ModelId", priv->model_id);
|
|
for (guint i = 0; i < priv->feature_index->len; i++) {
|
|
FuLogitechHidPpHidppMap *map = g_ptr_array_index(priv->feature_index, i);
|
|
fu_logitech_hidpp_map_to_string(map, idt, str);
|
|
}
|
|
}
|
|
|
|
static guint8
|
|
fu_logitech_hidpp_device_feature_get_idx(FuLogitechHidPpDevice *self, guint16 feature)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
|
|
for (guint i = 0; i < priv->feature_index->len; i++) {
|
|
FuLogitechHidPpHidppMap *map = g_ptr_array_index(priv->feature_index, i);
|
|
if (map->feature == feature)
|
|
return map->idx;
|
|
}
|
|
return 0x00;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_create_radio_child(FuLogitechHidPpDevice *self,
|
|
guint8 entity,
|
|
guint16 build,
|
|
GError **error)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
FuContext *ctx = fu_device_get_context(FU_DEVICE(self));
|
|
g_autofree gchar *instance_id = NULL;
|
|
g_autofree gchar *logical_id = NULL;
|
|
g_autofree gchar *radio_version = NULL;
|
|
g_autoptr(FuLogitechHidPpRadio) radio = NULL;
|
|
GPtrArray *children = fu_device_get_children(FU_DEVICE(self));
|
|
|
|
/* sanity check */
|
|
if (priv->model_id == NULL) {
|
|
g_set_error_literal(error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
"model ID not set");
|
|
return FALSE;
|
|
}
|
|
|
|
radio_version = g_strdup_printf("0x%.4x", build);
|
|
radio = fu_logitech_hidpp_radio_new(ctx, entity);
|
|
fu_device_set_physical_id(FU_DEVICE(radio), fu_device_get_physical_id(FU_DEVICE(self)));
|
|
/*
|
|
* Use the parent logical id as well as the model id for the
|
|
* logical id of the radio child device. This allows the radio
|
|
* devices of two devices of the same type (same device type,
|
|
* BLE mode) to coexist correctly.
|
|
*/
|
|
logical_id =
|
|
g_strdup_printf("%s-%s", fu_device_get_logical_id(FU_DEVICE(self)), priv->model_id);
|
|
fu_device_set_logical_id(FU_DEVICE(radio), logical_id);
|
|
instance_id = g_strdup_printf("HIDRAW\\VEN_%04X&MOD_%s&ENT_05",
|
|
(guint)FU_UNIFYING_DEVICE_VID,
|
|
priv->model_id);
|
|
fu_device_add_instance_id(FU_DEVICE(radio), instance_id);
|
|
fu_device_set_version(FU_DEVICE(radio), radio_version);
|
|
if (!fu_device_setup(FU_DEVICE(radio), error))
|
|
return FALSE;
|
|
|
|
/* remove old radio device if it already existed */
|
|
for (guint i = 0; i < children->len; i++) {
|
|
FuDevice *child = g_ptr_array_index(children, i);
|
|
if (g_strcmp0(fu_device_get_physical_id(FU_DEVICE(radio)),
|
|
fu_device_get_physical_id(child)) == 0 &&
|
|
g_strcmp0(fu_device_get_logical_id(FU_DEVICE(radio)),
|
|
fu_device_get_logical_id(child)) == 0) {
|
|
fu_device_remove_child(FU_DEVICE(self), child);
|
|
break;
|
|
}
|
|
}
|
|
fu_device_add_child(FU_DEVICE(self), FU_DEVICE(radio));
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_fetch_firmware_info(FuLogitechHidPpDevice *self, GError **error)
|
|
{
|
|
guint8 idx;
|
|
guint8 entity_count;
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new();
|
|
gboolean radio_ok = FALSE;
|
|
|
|
/* get the feature index */
|
|
idx = fu_logitech_hidpp_device_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 = priv->device_idx;
|
|
msg->sub_id = idx;
|
|
msg->function_id = 0x00 << 4; /* getCount */
|
|
msg->hidpp_version = priv->hidpp_version;
|
|
if (!fu_logitech_hidpp_transfer(priv->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 = priv->device_idx;
|
|
msg->sub_id = idx;
|
|
msg->function_id = 0x01 << 4; /* getInfo */
|
|
msg->data[0] = i;
|
|
if (!fu_logitech_hidpp_transfer(priv->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);
|
|
priv->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);
|
|
} else if (msg->data[0] == 5 &&
|
|
fu_device_has_private_flag(FU_DEVICE(self),
|
|
FU_LOGITECH_HIDPP_DEVICE_FLAG_ADD_RADIO)) {
|
|
if (!fu_logitech_hidpp_device_create_radio_child(self, i, build, error)) {
|
|
g_prefix_error(error, "failed to create radio: ");
|
|
return FALSE;
|
|
}
|
|
radio_ok = TRUE;
|
|
}
|
|
}
|
|
|
|
/* the device is probably in bootloader mode and the last SoftDevice FW upgrade failed */
|
|
if (fu_device_has_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_FLAG_ADD_RADIO) &&
|
|
!radio_ok) {
|
|
g_debug("no radio found, creating a fake one for recovery");
|
|
if (!fu_logitech_hidpp_device_create_radio_child(self, 1, 0, error)) {
|
|
g_prefix_error(error, "failed to create radio: ");
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* not an error, the device just doesn't support this */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_fetch_model_id(FuLogitechHidPpDevice *self, GError **error)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
guint8 idx;
|
|
g_autofree gchar *devid = NULL;
|
|
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new();
|
|
g_autoptr(GString) str = g_string_new(NULL);
|
|
|
|
/* get the (optional) feature index */
|
|
idx = fu_logitech_hidpp_device_feature_get_idx(self, HIDPP_FEATURE_I_FIRMWARE_INFO);
|
|
if (idx == 0x00)
|
|
return TRUE;
|
|
|
|
msg->report_id = HIDPP_REPORT_ID_SHORT;
|
|
msg->device_id = priv->device_idx;
|
|
msg->sub_id = idx;
|
|
msg->function_id = 0x00 << 4; /* getDeviceInfo */
|
|
msg->hidpp_version = priv->hidpp_version;
|
|
if (!fu_logitech_hidpp_transfer(priv->io_channel, msg, error)) {
|
|
g_prefix_error(error, "failed to get the model ID: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* ignore extendedModelID in data[13] */
|
|
for (guint i = 7; i < 13; i++)
|
|
g_string_append_printf(str, "%02X", msg->data[i]);
|
|
fu_logitech_hidpp_device_set_model_id(self, str->str);
|
|
|
|
/* add one more instance ID */
|
|
devid = g_strdup_printf("HIDRAW\\VEN_%04X&MOD_%s",
|
|
(guint)FU_UNIFYING_DEVICE_VID,
|
|
priv->model_id);
|
|
fu_device_add_instance_id(FU_DEVICE(self), devid);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_fetch_battery_level(FuLogitechHidPpDevice *self, GError **error)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
|
|
/* try using HID++2.0 */
|
|
if (priv->hidpp_version >= 2.f) {
|
|
guint8 idx;
|
|
|
|
/* try the Unified Battery feature first */
|
|
idx = fu_logitech_hidpp_device_feature_get_idx(self, HIDPP_FEATURE_UNIFIED_BATTERY);
|
|
if (idx != 0x00) {
|
|
gboolean socc = FALSE; /* state of charge capability */
|
|
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new();
|
|
msg->report_id = HIDPP_REPORT_ID_SHORT;
|
|
msg->device_id = priv->device_idx;
|
|
msg->sub_id = idx;
|
|
msg->function_id = 0x00 << 4; /* get_capabilities */
|
|
msg->hidpp_version = priv->hidpp_version;
|
|
if (!fu_logitech_hidpp_transfer(priv->io_channel, msg, error)) {
|
|
g_prefix_error(error, "failed to get battery info: ");
|
|
return FALSE;
|
|
}
|
|
if (msg->data[1] & 0x02)
|
|
socc = TRUE;
|
|
msg->function_id = 0x01 << 4; /* get_status */
|
|
if (!fu_logitech_hidpp_transfer(priv->io_channel, msg, error)) {
|
|
g_prefix_error(error, "failed to get battery info: ");
|
|
return FALSE;
|
|
}
|
|
if (socc) {
|
|
fu_device_set_battery_level(FU_DEVICE(self), msg->data[0]);
|
|
} else {
|
|
switch (msg->data[1]) {
|
|
case 1: /* critical */
|
|
fu_device_set_battery_level(FU_DEVICE(self), 5);
|
|
break;
|
|
case 2: /* low */
|
|
fu_device_set_battery_level(FU_DEVICE(self), 20);
|
|
break;
|
|
case 4: /* good */
|
|
fu_device_set_battery_level(FU_DEVICE(self), 55);
|
|
break;
|
|
case 8: /* full */
|
|
fu_device_set_battery_level(FU_DEVICE(self), 90);
|
|
break;
|
|
default:
|
|
g_warning("unknown battery level: 0x%02x", msg->data[1]);
|
|
break;
|
|
}
|
|
}
|
|
return TRUE;
|
|
} else {
|
|
/* fall back to the legacy Battery Level feature */
|
|
idx = fu_logitech_hidpp_device_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 = priv->device_idx;
|
|
msg->sub_id = idx;
|
|
msg->function_id = 0x00 << 4; /* GetBatteryLevelStatus */
|
|
msg->hidpp_version = priv->hidpp_version;
|
|
if (!fu_logitech_hidpp_transfer(priv->io_channel, msg, error)) {
|
|
g_prefix_error(error, "failed to get battery info: ");
|
|
return FALSE;
|
|
}
|
|
if (msg->data[0] != 0x00)
|
|
fu_device_set_battery_level(FU_DEVICE(self), msg->data[0]);
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* try HID++1.0 battery mileage */
|
|
if (priv->hidpp_version == 1.f) {
|
|
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new();
|
|
msg->report_id = HIDPP_REPORT_ID_SHORT;
|
|
msg->device_id = priv->device_idx;
|
|
msg->sub_id = HIDPP_SUBID_GET_REGISTER;
|
|
msg->function_id = HIDPP_REGISTER_BATTERY_MILEAGE << 4;
|
|
msg->hidpp_version = priv->hidpp_version;
|
|
if (fu_logitech_hidpp_transfer(priv->io_channel, msg, NULL)) {
|
|
if (msg->data[0] != 0x7F)
|
|
fu_device_set_battery_level(FU_DEVICE(self), msg->data[0]);
|
|
else
|
|
g_warning("unknown battery level: 0x%02x", msg->data[0]);
|
|
return TRUE;
|
|
}
|
|
|
|
/* try HID++1.0 battery status instead */
|
|
msg->function_id = HIDPP_REGISTER_BATTERY_STATUS << 4;
|
|
if (fu_logitech_hidpp_transfer(priv->io_channel, msg, NULL)) {
|
|
switch (msg->data[0]) {
|
|
case 1: /* 0 - 10 */
|
|
fu_device_set_battery_level(FU_DEVICE(self), 5);
|
|
break;
|
|
case 3: /* 11 - 30 */
|
|
fu_device_set_battery_level(FU_DEVICE(self), 20);
|
|
break;
|
|
case 5: /* 31 - 80 */
|
|
fu_device_set_battery_level(FU_DEVICE(self), 55);
|
|
break;
|
|
case 7: /* 81 - 100 */
|
|
fu_device_set_battery_level(FU_DEVICE(self), 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)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
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 = priv->device_idx;
|
|
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 = priv->hidpp_version;
|
|
if (!fu_logitech_hidpp_transfer(priv->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(priv->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_device_probe(FuDevice *device, GError **error)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
|
|
/*
|
|
* FuUdevDevice->probe except for paired devices. We don't want
|
|
* paired devices to inherit the logical ids of the receiver.
|
|
*/
|
|
if (priv->device_idx == HIDPP_DEVICE_IDX_UNSET ||
|
|
priv->device_idx == HIDPP_DEVICE_IDX_BLE) {
|
|
if (!FU_DEVICE_CLASS(fu_logitech_hidpp_device_parent_class)->probe(device, error))
|
|
return FALSE;
|
|
}
|
|
|
|
/* set the physical ID */
|
|
if (!fu_udev_device_set_physical_id(FU_UDEV_DEVICE(device), "hid", error))
|
|
return FALSE;
|
|
|
|
/* nearly... */
|
|
fu_device_add_vendor_id(device, "USB:0x046D");
|
|
|
|
/*
|
|
* All devices connected to a Bolt receiver share the same
|
|
* physical id, make them unique by using their pairing slot
|
|
* (device index) as a basis for their logical id.
|
|
*/
|
|
if (priv->device_idx != HIDPP_DEVICE_IDX_UNSET &&
|
|
priv->device_idx != HIDPP_DEVICE_IDX_BLE) {
|
|
g_autoptr(GString) id_str = g_string_new(NULL);
|
|
g_string_append_printf(id_str, "DEV_IDX=%d", priv->device_idx);
|
|
fu_device_set_logical_id(device, id_str->str);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_setup(FuDevice *device, GError **error)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
guint8 idx;
|
|
const guint16 map_features[] = {HIDPP_FEATURE_GET_DEVICE_NAME_TYPE,
|
|
HIDPP_FEATURE_I_FIRMWARE_INFO,
|
|
HIDPP_FEATURE_BATTERY_LEVEL_STATUS,
|
|
HIDPP_FEATURE_UNIFIED_BATTERY,
|
|
HIDPP_FEATURE_DFU_CONTROL,
|
|
HIDPP_FEATURE_DFU_CONTROL_SIGNED,
|
|
HIDPP_FEATURE_DFU_CONTROL_BOLT,
|
|
HIDPP_FEATURE_DFU,
|
|
HIDPP_FEATURE_ROOT};
|
|
|
|
if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_DEVICE_FLAG_BLE)) {
|
|
priv->hidpp_version = FU_HIDPP_VERSION_BLE;
|
|
priv->device_idx = HIDPP_DEVICE_IDX_BLE;
|
|
/*
|
|
* Set the logical ID for BLE devices. Note that for BLE
|
|
* devices, physical_id = HID_PHYS = MAC of the BT adapter,
|
|
* logical_id = HID_UNIQ = MAC of the device. The physical id is
|
|
* not enough to differentiate two BLE devices connected to the
|
|
* same adapter. This is done here because private flags
|
|
* are not loaded when the probe method runs, so we
|
|
* can't tell the device is in BLE mode.
|
|
*/
|
|
if (!fu_udev_device_set_logical_id(FU_UDEV_DEVICE(device), "hid", error))
|
|
return FALSE;
|
|
/*
|
|
* BLE devices might not be ready for ping right after
|
|
* they come up -> wait a bit before pinging.
|
|
*/
|
|
g_usleep(G_USEC_PER_SEC);
|
|
}
|
|
if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_DEVICE_FLAG_FORCE_RECEIVER_ID))
|
|
priv->device_idx = HIDPP_DEVICE_IDX_RECEIVER;
|
|
|
|
/* ping device to get HID++ version */
|
|
if (!fu_logitech_hidpp_device_ping(self, error))
|
|
return FALSE;
|
|
|
|
/* did not get ID */
|
|
if (priv->device_idx == HIDPP_DEVICE_IDX_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(priv->feature_index, 0);
|
|
if (priv->hidpp_version >= 2.f) {
|
|
FuLogitechHidPpHidppMap *map = g_new0(FuLogitechHidPpHidppMap, 1);
|
|
map->idx = 0x00;
|
|
map->feature = HIDPP_FEATURE_ROOT;
|
|
g_ptr_array_add(priv->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 model ID, typically something like B3630000000000 */
|
|
if (!fu_logitech_hidpp_device_fetch_model_id(self, error))
|
|
return FALSE;
|
|
|
|
/* get the firmware information */
|
|
if (!fu_logitech_hidpp_device_fetch_firmware_info(self, error))
|
|
return FALSE;
|
|
|
|
/* get the battery level */
|
|
if (!fu_logitech_hidpp_device_fetch_battery_level(self, error))
|
|
return FALSE;
|
|
|
|
/* try using HID++2.0 */
|
|
idx = fu_logitech_hidpp_device_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 = priv->device_idx;
|
|
msg->sub_id = idx;
|
|
msg->function_id = 0x02 << 4; /* getDeviceType */
|
|
msg->hidpp_version = priv->hidpp_version;
|
|
if (!fu_logitech_hidpp_transfer(priv->io_channel, msg, error)) {
|
|
g_prefix_error(error, "failed to get device type: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* add nice-to-have data */
|
|
tmp = fu_logitech_hidpp_device_get_summary(msg->data[0]);
|
|
if (tmp != NULL)
|
|
fu_device_set_summary(FU_DEVICE(device), tmp);
|
|
tmp = fu_logitech_hidpp_device_get_icon(msg->data[0]);
|
|
if (tmp != NULL)
|
|
fu_device_add_icon(FU_DEVICE(device), tmp);
|
|
}
|
|
idx = fu_logitech_hidpp_device_feature_get_idx(self, HIDPP_FEATURE_DFU_CONTROL);
|
|
if (idx != 0x00) {
|
|
fu_device_remove_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
|
|
fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifying");
|
|
}
|
|
idx = fu_logitech_hidpp_device_feature_get_idx(self, HIDPP_FEATURE_DFU_CONTROL_BOLT);
|
|
if (idx == 0x00)
|
|
idx = fu_logitech_hidpp_device_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 = priv->device_idx;
|
|
msg->sub_id = idx;
|
|
msg->function_id = 0x00 << 4; /* getDfuStatus */
|
|
msg->hidpp_version = priv->hidpp_version;
|
|
if (!fu_logitech_hidpp_transfer(priv->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 {
|
|
fu_device_remove_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_IS_BOOTLOADER);
|
|
}
|
|
fu_device_add_protocol(FU_DEVICE(device), "com.logitech.unifyingsigned");
|
|
}
|
|
idx = fu_logitech_hidpp_device_feature_get_idx(self, HIDPP_FEATURE_DFU);
|
|
if (idx != 0x00) {
|
|
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");
|
|
}
|
|
/* we do not actually know which protocol when in recovery mode,
|
|
* so force the metadata to have the specific regex set up */
|
|
fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifying");
|
|
fu_device_add_protocol(FU_DEVICE(self), "com.logitech.unifyingsigned");
|
|
}
|
|
|
|
/* poll for pings to track active state */
|
|
fu_device_set_poll_interval(device, FU_HIDPP_DEVICE_POLLING_INTERVAL);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_detach(FuDevice *device, FuProgress *progress, GError **error)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
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;
|
|
}
|
|
|
|
/* these may require user action */
|
|
idx = fu_logitech_hidpp_device_feature_get_idx(self, HIDPP_FEATURE_DFU_CONTROL_BOLT);
|
|
if (idx == 0x00)
|
|
idx = fu_logitech_hidpp_device_feature_get_idx(self, HIDPP_FEATURE_DFU_CONTROL);
|
|
if (idx != 0x00) {
|
|
FuDevice *parent;
|
|
g_autoptr(FwupdRequest) request = fwupd_request_new();
|
|
g_autoptr(GError) error_local = NULL;
|
|
msg->report_id = HIDPP_REPORT_ID_LONG;
|
|
msg->device_id = priv->device_idx;
|
|
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 = priv->hidpp_version;
|
|
msg->flags = FU_UNIFYING_HIDPP_MSG_FLAG_IGNORE_SUB_ID |
|
|
FU_UNIFYING_HIDPP_MSG_FLAG_LONGER_TIMEOUT;
|
|
if (!fu_logitech_hidpp_transfer(priv->io_channel, msg, &error_local)) {
|
|
if (fu_device_has_private_flag(
|
|
device,
|
|
FU_LOGITECH_HIDPP_DEVICE_FLAG_NO_REQUEST_REQUIRED)) {
|
|
g_debug("ignoring %s", error_local->message);
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
|
|
return TRUE;
|
|
}
|
|
g_propagate_prefixed_error(error,
|
|
g_steal_pointer(&error_local),
|
|
"failed to put device into DFU mode: ");
|
|
return FALSE;
|
|
}
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
|
|
|
|
/* so we detect off then on */
|
|
parent = fu_device_get_parent(device);
|
|
if (parent != NULL)
|
|
fu_device_set_poll_interval(parent, 500);
|
|
|
|
/* generate a message if not already set */
|
|
if (!fu_device_has_private_flag(
|
|
device,
|
|
FU_LOGITECH_HIDPP_DEVICE_FLAG_NO_REQUEST_REQUIRED)) {
|
|
if (fu_device_get_update_message(device) == NULL) {
|
|
g_autofree gchar *str = NULL;
|
|
str = g_strdup_printf(
|
|
"%s needs to be manually restarted to complete the update. "
|
|
"Please turn it off and on.",
|
|
fu_device_get_name(device));
|
|
fu_device_set_update_message(device, str);
|
|
}
|
|
fwupd_request_set_message(request, fu_device_get_update_message(device));
|
|
fwupd_request_set_kind(request, FWUPD_REQUEST_KIND_IMMEDIATE);
|
|
fwupd_request_set_id(request, FWUPD_REQUEST_ID_REMOVE_REPLUG);
|
|
fu_device_emit_request(device, request);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/* this can reboot all by itself */
|
|
idx = fu_logitech_hidpp_device_feature_get_idx(self, HIDPP_FEATURE_DFU_CONTROL_SIGNED);
|
|
if (idx != 0x00) {
|
|
msg->report_id = HIDPP_REPORT_ID_LONG;
|
|
msg->device_id = priv->device_idx;
|
|
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(priv->io_channel, msg, error)) {
|
|
g_prefix_error(error, "failed to put device into DFU mode: ");
|
|
return FALSE;
|
|
}
|
|
g_usleep(200 * 1000);
|
|
return fu_logitech_hidpp_device_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_device_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_device_write_firmware_pkt(FuLogitechHidPpDevice *self,
|
|
guint8 idx,
|
|
guint8 cmd,
|
|
const guint8 *data,
|
|
GError **error)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
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 = priv->device_idx;
|
|
msg->sub_id = idx;
|
|
msg->function_id = cmd << 4; /* dfuStart or dfuCmdDataX */
|
|
msg->hidpp_version = priv->hidpp_version;
|
|
/* enable transfer workaround for devices paired to Bolt receiver */
|
|
if (priv->device_idx != HIDPP_DEVICE_IDX_UNSET && priv->device_idx != HIDPP_DEVICE_IDX_BLE)
|
|
msg->flags = FU_UNIFYING_HIDPP_MSG_FLAG_RETRY_STUCK;
|
|
memcpy(msg->data, data, 16);
|
|
if (!fu_logitech_hidpp_transfer(priv->io_channel, msg, error)) {
|
|
g_prefix_error(error, "failed to supply program data: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* check error */
|
|
if (!fu_common_read_uint32_safe(msg->data,
|
|
sizeof(msg->data),
|
|
0x0,
|
|
&packet_cnt,
|
|
G_BIG_ENDIAN,
|
|
error))
|
|
return FALSE;
|
|
if (g_getenv("FWUPD_LOGITECH_HIDPP_VERBOSE") != NULL)
|
|
g_debug("packet_cnt=0x%04x", packet_cnt);
|
|
if (fu_logitech_hidpp_device_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(priv->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_device_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_device_write_firmware(FuDevice *device,
|
|
FuFirmware *firmware,
|
|
FuProgress *progress,
|
|
FwupdInstallFlags flags,
|
|
GError **error)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
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_device_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_bytes(firmware, error);
|
|
if (fw == NULL)
|
|
return FALSE;
|
|
|
|
/* flash hardware -- the first data byte is the fw entity */
|
|
data = g_bytes_get_data(fw, &sz);
|
|
if (priv->cached_fw_entity != data[0]) {
|
|
g_warning("updating cached entity 0x%x with 0x%x", priv->cached_fw_entity, data[0]);
|
|
priv->cached_fw_entity = data[0];
|
|
}
|
|
fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
|
|
for (gsize i = 0; i < sz / 16; i++) {
|
|
/* send packet and wait for reply */
|
|
if (g_getenv("FWUPD_LOGITECH_HIDPP_VERBOSE") != NULL)
|
|
g_debug("send data at addr=0x%04x", (guint)i * 16);
|
|
if (!fu_logitech_hidpp_device_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_progress_set_percentage_full(progress, (i + 1) * 16, sz);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_reprobe_cb(FuDevice *device, gpointer user_data, GError **error)
|
|
{
|
|
return fu_logitech_hidpp_device_setup(device, error);
|
|
}
|
|
|
|
gboolean
|
|
fu_logitech_hidpp_device_attach(FuLogitechHidPpDevice *self,
|
|
guint8 entity,
|
|
FuProgress *progress,
|
|
GError **error)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
FuDevice *device = FU_DEVICE(self);
|
|
guint8 idx;
|
|
g_autoptr(FuLogitechHidPpHidppMsg) msg = fu_logitech_hidpp_msg_new();
|
|
g_autoptr(GError) error_local = NULL;
|
|
|
|
/* 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_device_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_LONG;
|
|
msg->device_id = priv->device_idx;
|
|
msg->sub_id = idx;
|
|
msg->function_id = 0x05 << 4; /* restart */
|
|
msg->data[0] = entity; /* fwEntity */
|
|
msg->hidpp_version = priv->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(priv->io_channel, msg, &error_local)) {
|
|
if (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_READ) ||
|
|
g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) {
|
|
g_debug("ignoring '%s' on reset", error_local->message);
|
|
} else {
|
|
g_prefix_error(&error_local, "failed to restart device: ");
|
|
g_propagate_error(error, g_steal_pointer(&error_local));
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_DEVICE_FLAG_REBIND_ATTACH)) {
|
|
fu_device_set_poll_interval(device, 0);
|
|
/*
|
|
* Wait for device to become ready after flashing.
|
|
* Possible race condition: after the device is reset, Linux might enumerate it as
|
|
* a different hidraw device depending on timing.
|
|
*/
|
|
fu_progress_sleep(progress, 1000); /* ms */
|
|
} else {
|
|
/* device file hasn't been unbound/re-bound, just probe again */
|
|
if (!fu_device_retry(device, fu_logitech_hidpp_device_reprobe_cb, 10, NULL, error))
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_attach_cached(FuDevice *device, FuProgress *progress, GError **error)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
|
|
if (fu_device_has_private_flag(device, FU_LOGITECH_HIDPP_DEVICE_FLAG_REBIND_ATTACH))
|
|
fu_device_add_flag(device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
|
|
return fu_logitech_hidpp_device_attach(self, priv->cached_fw_entity, progress, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_set_quirk_kv(FuDevice *device,
|
|
const gchar *key,
|
|
const gchar *value,
|
|
GError **error)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(device);
|
|
if (g_strcmp0(key, "LogitechHidppModelId") == 0) {
|
|
fu_logitech_hidpp_device_set_model_id(self, value);
|
|
return TRUE;
|
|
}
|
|
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "quirk key not supported");
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
fu_logitech_hidpp_device_set_progress(FuDevice *self, FuProgress *progress)
|
|
{
|
|
fu_progress_set_id(progress, G_STRLOC);
|
|
fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED);
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2); /* detach */
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94); /* write */
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2); /* attach */
|
|
fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2); /* reload */
|
|
}
|
|
|
|
static void
|
|
fu_logitech_hidpp_device_finalize(GObject *object)
|
|
{
|
|
FuLogitechHidPpDevice *self = FU_HIDPP_DEVICE(object);
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
g_ptr_array_unref(priv->feature_index);
|
|
g_free(priv->model_id);
|
|
G_OBJECT_CLASS(fu_logitech_hidpp_device_parent_class)->finalize(object);
|
|
}
|
|
|
|
static gboolean
|
|
fu_logitech_hidpp_device_cleanup(FuDevice *device, FwupdInstallFlags flags, GError **error)
|
|
{
|
|
FuDevice *parent = fu_device_get_parent(device);
|
|
if (parent != NULL)
|
|
fu_device_set_poll_interval(parent, FU_HIDPP_RECEIVER_RUNTIME_POLLING_INTERVAL);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_logitech_hidpp_device_class_init(FuLogitechHidPpDeviceClass *klass)
|
|
{
|
|
FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass);
|
|
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
|
|
|
object_class->finalize = fu_logitech_hidpp_device_finalize;
|
|
klass_device->setup = fu_logitech_hidpp_device_setup;
|
|
klass_device->open = fu_logitech_hidpp_device_open;
|
|
klass_device->close = fu_logitech_hidpp_device_close;
|
|
klass_device->write_firmware = fu_logitech_hidpp_device_write_firmware;
|
|
klass_device->attach = fu_logitech_hidpp_device_attach_cached;
|
|
klass_device->detach = fu_logitech_hidpp_device_detach;
|
|
klass_device->poll = fu_logitech_hidpp_device_poll;
|
|
klass_device->to_string = fu_logitech_hidpp_device_to_string;
|
|
klass_device->probe = fu_logitech_hidpp_device_probe;
|
|
klass_device->set_quirk_kv = fu_logitech_hidpp_device_set_quirk_kv;
|
|
klass_device->cleanup = fu_logitech_hidpp_device_cleanup;
|
|
klass_device->set_progress = fu_logitech_hidpp_device_set_progress;
|
|
}
|
|
|
|
static void
|
|
fu_logitech_hidpp_device_init(FuLogitechHidPpDevice *self)
|
|
{
|
|
FuLogitechHidPpDevicePrivate *priv = GET_PRIVATE(self);
|
|
priv->device_idx = HIDPP_DEVICE_IDX_UNSET;
|
|
priv->feature_index = g_ptr_array_new_with_free_func(g_free);
|
|
fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE);
|
|
fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_RE_ENUMERATE);
|
|
fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_PLAIN);
|
|
fu_device_retry_set_delay(FU_DEVICE(self), 1000);
|
|
fu_device_register_private_flag(FU_DEVICE(self),
|
|
FU_LOGITECH_HIDPP_DEVICE_FLAG_FORCE_RECEIVER_ID,
|
|
"force-receiver-id");
|
|
fu_device_register_private_flag(FU_DEVICE(self), FU_LOGITECH_HIDPP_DEVICE_FLAG_BLE, "ble");
|
|
fu_device_register_private_flag(FU_DEVICE(self),
|
|
FU_LOGITECH_HIDPP_DEVICE_FLAG_REBIND_ATTACH,
|
|
"rebind-attach");
|
|
fu_device_register_private_flag(FU_DEVICE(self),
|
|
FU_LOGITECH_HIDPP_DEVICE_FLAG_NO_REQUEST_REQUIRED,
|
|
"no-request-required");
|
|
fu_device_register_private_flag(FU_DEVICE(self),
|
|
FU_LOGITECH_HIDPP_DEVICE_FLAG_ADD_RADIO,
|
|
"add-radio");
|
|
fu_device_set_remove_delay(FU_DEVICE(self), FU_DEVICE_REMOVE_DELAY_USER_REPLUG);
|
|
fu_device_set_battery_threshold(FU_DEVICE(self), 20);
|
|
}
|
|
|
|
FuLogitechHidPpDevice *
|
|
fu_logitech_hidpp_device_new(FuUdevDevice *parent)
|
|
{
|
|
FuLogitechHidPpDevice *self = NULL;
|
|
FuLogitechHidPpDevicePrivate *priv;
|
|
self = g_object_new(FU_TYPE_HIDPP_DEVICE,
|
|
"context",
|
|
fu_device_get_context(FU_DEVICE(parent)),
|
|
"physical-id",
|
|
fu_device_get_physical_id(FU_DEVICE(parent)),
|
|
"udev-device",
|
|
fu_udev_device_get_dev(parent),
|
|
NULL);
|
|
priv = GET_PRIVATE(self);
|
|
priv->io_channel = fu_logitech_hidpp_runtime_get_io_channel(FU_HIDPP_RUNTIME(parent));
|
|
return self;
|
|
}
|