/* * Copyright (c) 1999-2021 Logitech, Inc. * Copyright (C) 2021 Richard Hughes * * SPDX-License-Identifier: LGPL-2.1+ */ #include "config.h" #include #include #include #include "fu-logitech-bulkcontroller-common.h" #include "fu-logitech-bulkcontroller-device.h" /* SYNC interface follows TLSV (Type, Length, SequenceID, Value) protocol */ /* UPD interface follows TLV (Type, Length, Value) protocol */ /* Payload size limited to 8k for both interfaces */ #define UPD_PACKET_HEADER_SIZE (2 * sizeof(guint32)) #define SYNC_PACKET_HEADER_SIZE (3 * sizeof(guint32)) #define HASH_TIMEOUT 30000 #define MAX_DATA_SIZE 8192 /* 8k */ #define PAYLOAD_SIZE MAX_DATA_SIZE - UPD_PACKET_HEADER_SIZE #define UPD_INTERFACE_SUBPROTOCOL_ID 117 #define SYNC_INTERFACE_SUBPROTOCOL_ID 118 #define BULK_TRANSFER_TIMEOUT 1000 #define HASH_VALUE_SIZE 16 #define LENGTH_OFFSET 0x4 #define COMMAND_OFFSET 0x0 #define SYNC_ACK_PAYLOAD_LENGTH 5 #define MAX_RETRIES 5 #define MAX_HANDSHAKE_RETRIES 3 #define MAX_WAIT_COUNT 150 enum { SHA_256, SHA_512, MD5 }; enum { EP_OUT, EP_IN, EP_LAST }; enum { BULK_INTERFACE_UPD, BULK_INTERFACE_SYNC }; typedef enum { CMD_CHECK_BUFFERSIZE = 0xCC00, CMD_INIT = 0xCC01, CMD_START_TRANSFER = 0xCC02, CMD_DATA_TRANSFER = 0xCC03, CMD_END_TRANSFER = 0xCC04, CMD_UNINIT = 0xCC05, CMD_BUFFER_READ = 0xCC06, CMD_BUFFER_WRITE = 0xCC07, CMD_UNINIT_BUFFER = 0xCC08, CMD_ACK = 0xFF01, CMD_TIMEOUT = 0xFF02, CMD_NACK = 0xFF03 } UsbCommands; struct _FuLogitechBulkcontrollerDevice { FuUsbDevice parent_instance; guint sync_ep[EP_LAST]; guint update_ep[EP_LAST]; guint sync_iface; guint update_iface; FuLogitechBulkcontrollerDeviceStatus status; FuLogitechBulkcontrollerDeviceUpdateState update_status; guint update_progress; /* percentage value */ gboolean is_sync_transfer_in_progress; }; typedef struct { FuLogitechBulkcontrollerDevice *self; /* no-ref */ GByteArray *device_response; GByteArray *buf_pkt; GMainLoop *loop; GError *error; } FuLogitechBulkcontrollerHelper; G_DEFINE_TYPE(FuLogitechBulkcontrollerDevice, fu_logitech_bulkcontroller_device, FU_TYPE_USB_DEVICE) static void fu_logitech_bulkcontroller_helper_free(FuLogitechBulkcontrollerHelper *helper) { if (helper->error != NULL) g_error_free(helper->error); g_byte_array_unref(helper->buf_pkt); g_byte_array_unref(helper->device_response); g_main_loop_unref(helper->loop); g_slice_free(FuLogitechBulkcontrollerHelper, helper); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunused-function" G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuLogitechBulkcontrollerHelper, fu_logitech_bulkcontroller_helper_free) #pragma clang diagnostic pop static void fu_logitech_bulkcontroller_device_to_string(FuDevice *device, guint idt, GString *str) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); fu_common_string_append_kx(str, idt, "SyncIface", self->sync_iface); fu_common_string_append_kx(str, idt, "UpdateIface", self->update_iface); fu_common_string_append_kv( str, idt, "Status", fu_logitech_bulkcontroller_device_status_to_string(self->status)); fu_common_string_append_kv( str, idt, "UpdateState", fu_logitech_bulkcontroller_device_update_state_to_string(self->update_status)); } static gboolean fu_logitech_bulkcontroller_device_probe(FuDevice *device, GError **error) { #if G_USB_CHECK_VERSION(0, 3, 3) FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); g_autoptr(GPtrArray) intfs = NULL; intfs = g_usb_device_get_interfaces(fu_usb_device_get_dev(FU_USB_DEVICE(self)), error); if (intfs == NULL) return FALSE; for (guint i = 0; i < intfs->len; i++) { GUsbInterface *intf = g_ptr_array_index(intfs, i); if (g_usb_interface_get_class(intf) == G_USB_DEVICE_CLASS_VENDOR_SPECIFIC && g_usb_interface_get_protocol(intf) == 0x1) { if (g_usb_interface_get_subclass(intf) == SYNC_INTERFACE_SUBPROTOCOL_ID) { g_autoptr(GPtrArray) endpoints = g_usb_interface_get_endpoints(intf); self->sync_iface = g_usb_interface_get_number(intf); if (endpoints == NULL) continue; for (guint j = 0; j < endpoints->len; j++) { GUsbEndpoint *ep = g_ptr_array_index(endpoints, j); if (j == EP_OUT) self->sync_ep[EP_OUT] = g_usb_endpoint_get_address(ep); else self->sync_ep[EP_IN] = g_usb_endpoint_get_address(ep); } } else if (g_usb_interface_get_subclass(intf) == UPD_INTERFACE_SUBPROTOCOL_ID) { g_autoptr(GPtrArray) endpoints = g_usb_interface_get_endpoints(intf); self->sync_iface = g_usb_interface_get_number(intf); if (endpoints == NULL) continue; for (guint j = 0; j < endpoints->len; j++) { GUsbEndpoint *ep = g_ptr_array_index(endpoints, j); if (j == EP_OUT) self->update_ep[EP_OUT] = g_usb_endpoint_get_address(ep); else self->update_ep[EP_IN] = g_usb_endpoint_get_address(ep); } } } } return TRUE; #else g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "this version of GUsb is not supported"); return FALSE; #endif } static gboolean fu_logitech_bulkcontroller_device_send(FuLogitechBulkcontrollerDevice *self, GByteArray *buf, gint interface_id, GError **error) { gsize transferred = 0; gint ep; GCancellable *cancellable = NULL; g_return_val_if_fail(buf != NULL, FALSE); if (interface_id == BULK_INTERFACE_SYNC) { ep = self->sync_ep[EP_OUT]; } else if (interface_id == BULK_INTERFACE_UPD) { ep = self->update_ep[EP_OUT]; } else { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "interface is invalid"); return FALSE; } if (!g_usb_device_bulk_transfer(fu_usb_device_get_dev(FU_USB_DEVICE(self)), ep, (guint8 *)buf->data, buf->len, &transferred, BULK_TRANSFER_TIMEOUT, cancellable, error)) { g_prefix_error(error, "failed to send using bulk transfer: "); return FALSE; } return TRUE; } static gboolean fu_logitech_bulkcontroller_device_recv(FuLogitechBulkcontrollerDevice *self, GByteArray *buf, gint interface_id, guint timeout, GError **error) { gsize received_length = 0; gint ep; g_return_val_if_fail(buf != NULL, FALSE); if (interface_id == BULK_INTERFACE_SYNC) { ep = self->sync_ep[EP_IN]; } else if (interface_id == BULK_INTERFACE_UPD) { ep = self->update_ep[EP_IN]; } else { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "interface is invalid"); return FALSE; } if (!g_usb_device_bulk_transfer(fu_usb_device_get_dev(FU_USB_DEVICE(self)), ep, buf->data, buf->len, &received_length, timeout, NULL, error)) { g_prefix_error(error, "failed to receive using bulk transfer: "); return FALSE; } return TRUE; } static gboolean fu_logitech_bulkcontroller_device_send_upd_cmd(FuLogitechBulkcontrollerDevice *self, guint32 cmd, GByteArray *buf, GError **error) { guint32 cmd_tmp = 0x0; guint timeout = BULK_TRANSFER_TIMEOUT; g_autoptr(GByteArray) buf_pkt = g_byte_array_new(); g_autoptr(GByteArray) buf_ack = g_byte_array_new(); fu_byte_array_append_uint32(buf_pkt, cmd, G_LITTLE_ENDIAN); /* Type(T) : Command type */ fu_byte_array_append_uint32(buf_pkt, buf != NULL ? buf->len : 0, G_LITTLE_ENDIAN); /*Length(L) : Length of payload */ if (buf != NULL) { g_byte_array_append(buf_pkt, buf->data, buf->len); /* Value(V) : Actual payload data */ } if (!fu_logitech_bulkcontroller_device_send(self, buf_pkt, BULK_INTERFACE_UPD, error)) return FALSE; /* receiving INIT ACK */ fu_byte_array_set_size(buf_ack, MAX_DATA_SIZE); /* extending the bulk transfer timeout value, as android device takes some time to calculate Hash and respond */ if (CMD_END_TRANSFER == cmd) timeout = HASH_TIMEOUT; if (!fu_logitech_bulkcontroller_device_recv(self, buf_ack, BULK_INTERFACE_UPD, timeout, error)) return FALSE; if (!fu_common_read_uint32_safe(buf_ack->data, buf_ack->len, COMMAND_OFFSET, &cmd_tmp, G_LITTLE_ENDIAN, error)) return FALSE; if (cmd_tmp != CMD_ACK) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "not CMD_ACK, got %x", cmd); return FALSE; } if (!fu_common_read_uint32_safe(buf_ack->data, buf_ack->len, UPD_PACKET_HEADER_SIZE, &cmd_tmp, G_LITTLE_ENDIAN, error)) return FALSE; if (cmd_tmp != cmd) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid upd message received, expected %x, got %x", cmd, cmd_tmp); return FALSE; } return TRUE; } static gboolean fu_logitech_bulkcontroller_device_send_sync_cmd(FuLogitechBulkcontrollerDevice *self, guint32 cmd, GByteArray *buf, GError **error) { g_autoptr(GByteArray) buf_pkt = g_byte_array_new(); g_autoptr(GByteArray) buf_ack = g_byte_array_new(); fu_byte_array_append_uint32(buf_pkt, cmd, G_LITTLE_ENDIAN); /* Type(T) : Command type */ fu_byte_array_append_uint32(buf_pkt, buf != NULL ? buf->len : 0, G_LITTLE_ENDIAN); /*Length(L) : Length of payload */ fu_byte_array_append_uint32(buf_pkt, g_random_int_range(0, G_MAXUINT16), G_LITTLE_ENDIAN); /*Sequence(S) : Sequence ID of the data */ if (buf != NULL) { g_byte_array_append(buf_pkt, buf->data, buf->len); /* Value(V) : Actual payload data */ } if (!fu_logitech_bulkcontroller_device_send(self, buf_pkt, BULK_INTERFACE_SYNC, error)) return FALSE; return TRUE; } static gchar * fu_logitech_bulkcontroller_device_compute_hash(GBytes *data) { guint8 md5buf[HASH_VALUE_SIZE] = {0}; gsize data_len = sizeof(md5buf); GChecksum *checksum = g_checksum_new(G_CHECKSUM_MD5); g_checksum_update(checksum, g_bytes_get_data(data, NULL), g_bytes_get_size(data)); g_checksum_get_digest(checksum, (guint8 *)&md5buf, &data_len); return g_base64_encode(md5buf, sizeof(md5buf)); } static gboolean fu_logitech_bulkcontroller_device_json_parser(FuDevice *device, GByteArray *decoded_pkt, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); JsonArray *json_devices; JsonNode *json_root; JsonObject *json_device; JsonObject *json_object; JsonObject *json_payload; g_autoptr(JsonParser) json_parser = json_parser_new(); /* parse JSON reply */ if (!json_parser_load_from_data(json_parser, (const gchar *)decoded_pkt->data, decoded_pkt->len, error)) { g_prefix_error(error, "failed to parse json data: "); return FALSE; } json_root = json_parser_get_root(json_parser); if (json_root == NULL) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "did not get JSON root"); return FALSE; } json_object = json_node_get_object(json_root); json_payload = json_object_get_object_member(json_object, "payload"); if (json_payload == NULL) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "did not get JSON payload"); return FALSE; } json_devices = json_object_get_array_member(json_payload, "devices"); if (json_devices == NULL) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "did not get JSON devices"); return FALSE; } json_device = json_array_get_object_element(json_devices, 0); if (json_device == NULL) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "did not get JSON device"); return FALSE; } if (json_object_has_member(json_device, "name")) fu_device_set_name(device, json_object_get_string_member(json_device, "name")); if (json_object_has_member(json_device, "sw")) fu_device_set_version(device, json_object_get_string_member(json_device, "sw")); if (json_object_has_member(json_device, "type")) fu_device_add_instance_id(device, json_object_get_string_member(json_device, "type")); if (json_object_has_member(json_device, "status")) self->status = json_object_get_int_member(json_device, "status"); if (json_object_has_member(json_device, "updateStatus")) self->update_status = json_object_get_int_member(json_device, "updateStatus"); /* updateProgress only available while firmware upgrade is going on */ if (json_object_has_member(json_device, "updateProgress")) self->update_progress = json_object_get_int_member(json_device, "updateProgress"); return TRUE; } /* async callback handler : read data from sync endpoint continuously */ static void fu_logitech_bulkcontroller_device_sync_cb(GObject *source_object, GAsyncResult *res, gpointer user_data) { FuLogitechBulkcontrollerHelper *helper = (FuLogitechBulkcontrollerHelper *)user_data; FuLogitechBulkcontrollerDevice *self = helper->self; guint32 cmd_tmp = 0x0; guint64 cmd_tmp_64 = 0x0; guint32 response_length = 0; guint8 ack_payload[SYNC_ACK_PAYLOAD_LENGTH] = {0}; g_autoptr(GByteArray) buf_ack = g_byte_array_new(); g_autoptr(GError) error_local = NULL; if (!g_usb_device_bulk_transfer_finish(G_USB_DEVICE(source_object), res, &error_local)) { g_propagate_prefixed_error(&helper->error, g_steal_pointer(&error_local), "failed to finish using bulk transfer: "); g_main_loop_quit(helper->loop); return; } if (!fu_common_read_uint32_safe(helper->buf_pkt->data, helper->buf_pkt->len, COMMAND_OFFSET, &cmd_tmp, G_LITTLE_ENDIAN, &helper->error)) { g_prefix_error(&helper->error, "failed to retrieve payload command: "); g_main_loop_quit(helper->loop); return; } if (!fu_common_read_uint32_safe(helper->buf_pkt->data, helper->buf_pkt->len, LENGTH_OFFSET, &response_length, G_LITTLE_ENDIAN, &helper->error)) { g_prefix_error(&helper->error, "failed to retrieve payload length: "); g_main_loop_quit(helper->loop); return; } if (!fu_common_read_uint64_safe(helper->buf_pkt->data, helper->buf_pkt->len, SYNC_PACKET_HEADER_SIZE, &cmd_tmp_64, G_LITTLE_ENDIAN, &helper->error)) { g_prefix_error(&helper->error, "failed to retrieve payload data: "); g_main_loop_quit(helper->loop); return; } if (!fu_memcpy_safe((guint8 *)ack_payload, sizeof(ack_payload), 0x0, (guint8 *)&cmd_tmp_64, sizeof(cmd_tmp_64), 0x0, SYNC_ACK_PAYLOAD_LENGTH, &helper->error)) { g_prefix_error(&helper->error, "failed to copy payload data: "); g_main_loop_quit(helper->loop); return; } if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) g_debug("Received 0x%x message on sync interface", cmd_tmp); switch (cmd_tmp) { case CMD_ACK: if (CMD_BUFFER_WRITE == fu_common_strtoull((const char *)ack_payload)) { if (!fu_logitech_bulkcontroller_device_send_sync_cmd(self, CMD_UNINIT_BUFFER, NULL, &helper->error)) { g_prefix_error(&helper->error, "failed to send %d while processing %d: ", CMD_UNINIT_BUFFER, CMD_BUFFER_WRITE); g_main_loop_quit(helper->loop); return; } } else if (CMD_UNINIT_BUFFER != fu_common_strtoull((const char *)ack_payload)) { g_set_error(&helper->error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "invalid message received: expected %s, but received %d: ", (const gchar *)ack_payload, CMD_UNINIT_BUFFER); g_main_loop_quit(helper->loop); return; } break; case CMD_BUFFER_READ: g_byte_array_append(helper->device_response, helper->buf_pkt->data + SYNC_PACKET_HEADER_SIZE, response_length); if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) { g_autofree gchar *strsafe = fu_common_strsafe((const gchar *)helper->device_response->data, helper->device_response->len); g_debug("Received data on sync interface. length: %u, buffer: %s", helper->device_response->len, strsafe); } fu_byte_array_append_uint32(buf_ack, cmd_tmp, G_LITTLE_ENDIAN); if (!fu_logitech_bulkcontroller_device_send_sync_cmd(self, CMD_ACK, buf_ack, &helper->error)) { g_prefix_error(&helper->error, "failed to send %d while processing %d: ", CMD_ACK, CMD_BUFFER_READ); g_main_loop_quit(helper->loop); return; } break; case CMD_UNINIT_BUFFER: fu_byte_array_append_uint32(buf_ack, cmd_tmp, G_LITTLE_ENDIAN); if (!fu_logitech_bulkcontroller_device_send_sync_cmd(self, CMD_ACK, buf_ack, &helper->error)) { g_prefix_error(&helper->error, "failed to send %d while processing %d: ", CMD_ACK, CMD_UNINIT_BUFFER); g_main_loop_quit(helper->loop); return; } self->is_sync_transfer_in_progress = FALSE; break; default: break; } g_main_loop_quit(helper->loop); return; } static gboolean fu_logitech_bulkcontroller_device_startlistening_sync(FuLogitechBulkcontrollerDevice *self, GByteArray *device_response, GError **error) { gint max_retry = MAX_RETRIES * 2; self->is_sync_transfer_in_progress = TRUE; while (self->is_sync_transfer_in_progress) { g_autoptr(FuLogitechBulkcontrollerHelper) helper = g_slice_new0(FuLogitechBulkcontrollerHelper); max_retry--; helper->self = self; helper->buf_pkt = g_byte_array_new(); helper->loop = g_main_loop_new(NULL, FALSE); helper->device_response = g_byte_array_ref(device_response); fu_byte_array_set_size(helper->buf_pkt, MAX_DATA_SIZE); g_usb_device_bulk_transfer_async(fu_usb_device_get_dev(FU_USB_DEVICE(self)), self->sync_ep[EP_IN], helper->buf_pkt->data, helper->buf_pkt->len, BULK_TRANSFER_TIMEOUT, NULL, /* cancellable */ fu_logitech_bulkcontroller_device_sync_cb, helper); g_main_loop_run(helper->loop); /* handle error scenario, e.g. device no longer responding */ if (max_retry == 0) { self->is_sync_transfer_in_progress = FALSE; if (helper->error != NULL) { g_propagate_prefixed_error(error, g_steal_pointer(&helper->error), "failed after %i retries: ", MAX_RETRIES); } else { g_set_error(&helper->error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "failed after %i retries: ", MAX_RETRIES); } return FALSE; } /* just show to console */ if (helper->error != NULL) g_warning("async error %s", helper->error->message); } /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_get_data(FuDevice *device, gboolean send_req, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); g_autoptr(GByteArray) decoded_pkt = g_byte_array_new(); g_autoptr(GByteArray) device_response = g_byte_array_new(); FuLogitechBulkcontrollerProtoId proto_id = kProtoId_UnknownId; /* sending GetDeviceInfoRequest. Device reports quite a few matrix, including status, * progress etc * Two ways to get data from device: * 1. Listen for the data broadcasted by device, while firmware upgrade is going on * 2. Make explicit request to the device. Used when data is needed before/after firmware * upgrade */ if (send_req) { g_autoptr(GByteArray) device_request = g_byte_array_new(); device_request = proto_manager_generate_get_device_info_request(); if (!fu_logitech_bulkcontroller_device_send_sync_cmd(self, CMD_BUFFER_WRITE, device_request, error)) { g_prefix_error( error, "failed to send write buffer packet for device info request: "); return FALSE; } } if (!fu_logitech_bulkcontroller_device_startlistening_sync(self, device_response, error)) { g_prefix_error(error, "failed to receive data packet for device info request: "); return FALSE; } /* handle error scenario, e.g. CMD_UNINIT_BUFFER arrived before CMD_BUFFER_READ */ if (device_response->len == 0) { g_prefix_error(error, "failed to receive expected packet for device info request: "); return FALSE; } decoded_pkt = proto_manager_decode_message(device_response->data, device_response->len, &proto_id, error); if (decoded_pkt == NULL) { g_prefix_error(error, "failed to unpack packet for device info request: "); return FALSE; } if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) { g_autofree gchar *strsafe = fu_common_strsafe((const gchar *)decoded_pkt->data, decoded_pkt->len); g_debug("Received device response: id: %u, length %u, data: %s", proto_id, device_response->len, strsafe); } if (proto_id != kProtoId_GetDeviceInfoResponse && proto_id != kProtoId_KongEvent) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "incorrect response for device info request"); return FALSE; } if (!fu_logitech_bulkcontroller_device_json_parser(device, decoded_pkt, error)) return FALSE; /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_send_upd_init_cmd_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); return fu_logitech_bulkcontroller_device_send_upd_cmd(self, CMD_INIT, NULL, error); } static gboolean fu_logitech_bulkcontroller_device_write_firmware(FuDevice *device, FuFirmware *firmware, FuProgress *progress, FwupdInstallFlags flags, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); gboolean query_device = FALSE; /* query or listen for events, periodically broadcasted */ gint max_wait = MAX_WAIT_COUNT; /* if firmware upgrade is taking forever to finish */ guint max_no_response_count = MAX_RETRIES; /* device doesn't respond */ guint no_response_count = 0; g_autofree gchar *base64hash = NULL; g_autoptr(GByteArray) end_pkt = g_byte_array_new(); g_autoptr(GByteArray) start_pkt = g_byte_array_new(); g_autoptr(GBytes) fw = NULL; g_autoptr(GPtrArray) chunks = NULL; g_autofree gchar *old_firmware_version = NULL; /* progress */ fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 49); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_VERIFY, 49); /* get default image */ fw = fu_firmware_get_bytes(firmware, error); if (fw == NULL) return FALSE; /* sending INIT. Retry if device is not in IDLE state to receive the file */ if (!fu_device_retry(device, fu_logitech_bulkcontroller_device_send_upd_init_cmd_cb, MAX_RETRIES, NULL, error)) { g_prefix_error(error, "failed to write init transfer packet: please reboot the device: "); return FALSE; } /* transfer sent */ fu_byte_array_append_uint64(start_pkt, g_bytes_get_size(fw), G_LITTLE_ENDIAN); if (!fu_logitech_bulkcontroller_device_send_upd_cmd(self, CMD_START_TRANSFER, start_pkt, error)) { g_prefix_error(error, "failed to write start transfer packet: "); return FALSE; } fu_progress_step_done(progress); /* push each block to device */ chunks = fu_chunk_array_new_from_bytes(fw, 0x0, 0x0, PAYLOAD_SIZE); for (guint i = 0; i < chunks->len; i++) { FuChunk *chk = g_ptr_array_index(chunks, i); g_autoptr(GByteArray) data_pkt = g_byte_array_new(); g_byte_array_append(data_pkt, fu_chunk_get_data(chk), fu_chunk_get_data_sz(chk)); if (!fu_logitech_bulkcontroller_device_send_upd_cmd(self, CMD_DATA_TRANSFER, data_pkt, error)) { g_prefix_error(error, "failed to send data packet 0x%x: ", i); return FALSE; } fu_progress_set_percentage_full(fu_progress_get_child(progress), i + 1, chunks->len); } fu_progress_step_done(progress); /* sending end transfer */ base64hash = fu_logitech_bulkcontroller_device_compute_hash(fw); fu_byte_array_append_uint32(end_pkt, 1, G_LITTLE_ENDIAN); /* update */ fu_byte_array_append_uint32(end_pkt, 0, G_LITTLE_ENDIAN); /* force */ fu_byte_array_append_uint32(end_pkt, MD5, G_LITTLE_ENDIAN); /* checksum type */ g_byte_array_append(end_pkt, (const guint8 *)base64hash, strlen(base64hash)); if (!fu_logitech_bulkcontroller_device_send_upd_cmd(self, CMD_END_TRANSFER, end_pkt, error)) { g_prefix_error(error, "failed to write end transfer transfer packet: "); return FALSE; } /* send uninit */ if (!fu_logitech_bulkcontroller_device_send_upd_cmd(self, CMD_UNINIT, NULL, error)) { g_prefix_error(error, "failed to write finish transfer packet: "); return FALSE; } fu_progress_step_done(progress); /* * image file pushed. Device validates and uploads new image on inactive partition. * Restart sync cb, to get the update progress * Normally status changes as follows: * While image being pushed: kUpdateStateCurrent->kUpdateStateDownloading (~5minutes) * After image push is complete: kUpdateStateDownloading->kUpdateStateReady * Validating image: kUpdateStateReady->kUpdateStateStarting * Uploading image: kUpdateStateStarting->kUpdateStateUpdating * Upload finished: kUpdateStateUpdating->kUpdateStateCurrent (~5minutes) * After upload is finished, device reboots itself */ g_usleep(G_TIME_SPAN_SECOND); /* save the current firmware version for troubleshooting purpose */ old_firmware_version = g_strdup(fu_device_get_version(device)); do { g_autoptr(GError) error_local = NULL; /* skip explicit device query as long as device is publishing update events * (kProtoId_KongEvent) */ if (self->update_progress == 100) { query_device = TRUE; } else { query_device = (no_response_count == 0) ? FALSE : TRUE; } g_usleep(500 * G_TIME_SPAN_MILLISECOND); /* lost Success/Failure message, device rebooting */ if (no_response_count == max_no_response_count) { g_debug("device not responding, rebooting..."); break; } /* update device obj with latest info from the device */ if (!fu_logitech_bulkcontroller_device_get_data(device, query_device, &error_local)) { no_response_count++; g_debug("no response for device info request %u", no_response_count); continue; } /* device responsive, no error and not rebooting yet */ no_response_count = 0; if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) { g_debug("firmware update status: %s. progress: %u", fu_logitech_bulkcontroller_device_update_state_to_string( self->update_status), self->update_progress); } /* existing device image version is same as newly pushed image */ if (self->update_status == kUpdateStateError) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "firmware upgrade failed"); return FALSE; } if (self->update_status == kUpdateStateCurrent) { if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) { g_debug("new firmware version: %s, old firmware version: %s, " "rebooting...", fu_device_get_version(device), old_firmware_version); } break; } if (self->update_progress == 100) { /* wait for state change: kUpdateStateUpdating->kUpdateStateCurrent * device no longer broadcast fu related events, need to query device * explicitly now */ g_usleep(G_USEC_PER_SEC); continue; } fu_progress_set_percentage_full(fu_progress_get_child(progress), self->update_progress, 100); } while (max_wait--); if (max_wait <= 0) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "firmware upgrade timeout: "); return FALSE; } fu_progress_step_done(progress); /* success! */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_open(FuDevice *device, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); /* FuUsbDevice->open */ if (!FU_DEVICE_CLASS(fu_logitech_bulkcontroller_device_parent_class)->open(device, error)) return FALSE; /* claim both interfaces */ if (!g_usb_device_claim_interface(usb_device, self->update_iface, G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER, error)) { g_prefix_error(error, "failed to claim update interface: "); return FALSE; } if (!g_usb_device_claim_interface(usb_device, self->sync_iface, G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER, error)) { g_prefix_error(error, "failed to claim sync interface: "); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_close(FuDevice *device, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); if (!g_usb_device_release_interface(usb_device, self->update_iface, G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER, error)) { g_prefix_error(error, "failed to release update interface: "); return FALSE; } if (!g_usb_device_release_interface(usb_device, self->sync_iface, G_USB_DEVICE_CLAIM_INTERFACE_BIND_KERNEL_DRIVER, error)) { g_prefix_error(error, "failed to release sync interface: "); return FALSE; } /* FuUsbDevice->close */ return FU_DEVICE_CLASS(fu_logitech_bulkcontroller_device_parent_class) ->close(device, error); } static gboolean fu_logitech_bulkcontroller_device_get_handshake_cb(FuDevice *device, gpointer user_data, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); FuLogitechBulkcontrollerProtoId proto_id = kProtoId_UnknownId; g_autoptr(GByteArray) decoded_pkt = g_byte_array_new(); g_autoptr(GByteArray) device_response = g_byte_array_new(); g_autoptr(GError) error_local = NULL; if (!fu_logitech_bulkcontroller_device_startlistening_sync(self, device_response, &error_local)) { if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) g_debug("failed to receive data packet for handshake request"); g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "failed to receive data packet for handshake request"); return FALSE; } /* handle error scenario, e.g. CMD_UNINIT_BUFFER arrived before CMD_BUFFER_READ */ if (device_response->len == 0) { if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) g_debug("failed to receive expected packet for handshake request"); g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "failed to receive expected packet for handshake request"); return FALSE; } decoded_pkt = proto_manager_decode_message(device_response->data, device_response->len, &proto_id, &error_local); if (decoded_pkt == NULL) { if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) g_debug("failed to unpack packet for handshake request"); g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, "failed to unpack packet for handshake request"); return FALSE; } if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) { g_autofree gchar *strsafe = fu_common_strsafe((const gchar *)decoded_pkt->data, decoded_pkt->len); g_debug("Received initialization response: id: %u, length %u, data: %s", proto_id, device_response->len, strsafe); } /* skip optional initialization events -- not an error if these events are missed */ if (proto_id != kProtoId_HandshakeEvent) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid initialization message received: %u", proto_id); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_set_time(FuDevice *device, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); g_autoptr(GByteArray) device_request = g_byte_array_new(); g_autoptr(GByteArray) decoded_pkt = g_byte_array_new(); g_autoptr(GByteArray) device_response = g_byte_array_new(); FuLogitechBulkcontrollerProtoId proto_id = kProtoId_UnknownId; /* send SetDeviceTimeRequest to sync device clock with host */ device_request = proto_manager_generate_set_device_time_request(); if (!fu_logitech_bulkcontroller_device_send_sync_cmd(self, CMD_BUFFER_WRITE, device_request, error)) { g_prefix_error(error, "failed to send write buffer packet for set device time request: "); return FALSE; } if (!fu_logitech_bulkcontroller_device_startlistening_sync(self, device_response, error)) { g_prefix_error(error, "failed to receive data packet for set device time request: "); return FALSE; } /* handle error scenario, e.g. CMD_UNINIT_BUFFER arrived before CMD_BUFFER_READ */ if (device_response->len == 0) { g_prefix_error(error, "failed to receive expected packet for set device time request: "); return FALSE; } decoded_pkt = proto_manager_decode_message(device_response->data, device_response->len, &proto_id, error); if (decoded_pkt == NULL) { g_prefix_error(error, "failed to unpack packet for set device time request: "); return FALSE; } if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) { g_autofree gchar *strsafe = fu_common_strsafe((const gchar *)decoded_pkt->data, decoded_pkt->len); g_debug("Received device response while processing set device time request: id: " "%u, length %u, data: %s", proto_id, device_response->len, strsafe); } if (proto_id != kProtoId_Ack) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "incorrect response for set device time request"); return FALSE; } /* success */ return TRUE; } static gboolean fu_logitech_bulkcontroller_device_setup(FuDevice *device, GError **error) { FuLogitechBulkcontrollerDevice *self = FU_LOGITECH_BULKCONTROLLER_DEVICE(device); g_autoptr(GByteArray) device_request = g_byte_array_new(); g_autoptr(GByteArray) decoded_pkt = g_byte_array_new(); g_autoptr(GByteArray) device_response = g_byte_array_new(); FuLogitechBulkcontrollerProtoId proto_id = kProtoId_UnknownId; guint32 success = 0; guint32 error_code = 0; g_autoptr(GError) error_local = NULL; /* FuUsbDevice->setup */ if (!FU_DEVICE_CLASS(fu_logitech_bulkcontroller_device_parent_class)->setup(device, error)) return FALSE; /* check for initialization events generated by the device * no error check needed here, possibly missed */ if (!fu_device_retry(device, fu_logitech_bulkcontroller_device_get_handshake_cb, MAX_HANDSHAKE_RETRIES, NULL, &error_local)) { g_warning("failed to receive initialization events: %s", error_local->message); } /* * device supports USB_Device mode, Appliance mode and BYOD mode. * Only USB_Device mode is supported here. * Ensure it is running in USB_Device mode * Response has two data: Request succeeded or failed, and error code in case of failure */ device_request = proto_manager_generate_transition_to_device_mode_request(); if (!fu_logitech_bulkcontroller_device_send_sync_cmd(self, CMD_BUFFER_WRITE, device_request, error)) { g_prefix_error(error, "failed to send buffer write packet for transition mode request: "); return FALSE; } if (!fu_logitech_bulkcontroller_device_startlistening_sync(self, device_response, error)) { g_prefix_error(error, "failed to receive data packet for transition mode request: "); return FALSE; } /* handle error scenario, e.g. CMD_UNINIT_BUFFER arrived before CMD_BUFFER_READ */ if (device_response->len == 0) { g_prefix_error(error, "failed to receive expected packet for transition mode request: "); return FALSE; } decoded_pkt = proto_manager_decode_message(device_response->data, device_response->len, &proto_id, error); if (decoded_pkt == NULL) { g_prefix_error(error, "failed to unpack packet for transition mode request: "); return FALSE; } if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) { g_autofree gchar *strsafe = fu_common_strsafe((const gchar *)decoded_pkt->data, decoded_pkt->len); g_debug("Received transition mode response: id: %u, length %u, data: %s", proto_id, device_response->len, strsafe); } if (proto_id != kProtoId_TransitionToDeviceModeResponse) { g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "incorrect response for transition mode request"); return FALSE; } if (!fu_common_read_uint32_safe(decoded_pkt->data, decoded_pkt->len, COMMAND_OFFSET, &success, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to retrieve result for transition mode request: "); return FALSE; } if (!fu_common_read_uint32_safe(decoded_pkt->data, decoded_pkt->len, LENGTH_OFFSET, &error_code, G_LITTLE_ENDIAN, error)) { g_prefix_error(error, "failed to retrieve error code for transition mode request: "); return FALSE; } if (g_getenv("FWUPD_LOGITECH_BULKCONTROLLER_VERBOSE") != NULL) { g_debug("Received transition mode response. Success: %u, Error: %u", success, error_code); } if (!success) { g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "transition mode request failed. error: %u", error_code); return FALSE; } /* set device time */ if (!fu_logitech_bulkcontroller_device_set_time(device, error)) return FALSE; /* load current device data */ if (!fu_logitech_bulkcontroller_device_get_data(device, TRUE, error)) return FALSE; /* success */ return TRUE; } static void fu_logitech_bulkcontroller_device_set_progress(FuDevice *self, FuProgress *progress) { fu_progress_set_id(progress, G_STRLOC); fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* detach */ fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 99); /* write */ fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 0); /* attach */ fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 1); /* reload */ } static void fu_logitech_bulkcontroller_device_init(FuLogitechBulkcontrollerDevice *self) { fu_device_add_protocol(FU_DEVICE(self), "com.logitech.vc.proto"); fu_device_set_version_format(FU_DEVICE(self), FWUPD_VERSION_FORMAT_TRIPLET); fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UPDATABLE); fu_device_retry_set_delay(FU_DEVICE(self), 1000); fu_device_set_remove_delay(FU_DEVICE(self), 100000); /* >1 min to finish init */ } static void fu_logitech_bulkcontroller_device_class_init(FuLogitechBulkcontrollerDeviceClass *klass) { FuDeviceClass *klass_device = FU_DEVICE_CLASS(klass); klass_device->to_string = fu_logitech_bulkcontroller_device_to_string; klass_device->write_firmware = fu_logitech_bulkcontroller_device_write_firmware; klass_device->probe = fu_logitech_bulkcontroller_device_probe; klass_device->setup = fu_logitech_bulkcontroller_device_setup; klass_device->open = fu_logitech_bulkcontroller_device_open; klass_device->close = fu_logitech_bulkcontroller_device_close; klass_device->set_progress = fu_logitech_bulkcontroller_device_set_progress; }