diff --git a/plugins/modem-manager/fu-sahara-loader.c b/plugins/modem-manager/fu-sahara-loader.c new file mode 100644 index 000000000..775325d28 --- /dev/null +++ b/plugins/modem-manager/fu-sahara-loader.c @@ -0,0 +1,558 @@ +/* + * Copyright (C) 2021 Quectel Wireless Solutions Co., Ltd. + * Ivan Mikhanchuk + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#include "config.h" + +#include +#include + +#include "fu-sahara-loader.h" + +#define SAHARA_VERSION 2 +#define SAHARA_VERSION_COMPATIBLE 1 + +#define SAHARA_RAW_BUFFER_SIZE (4 * 1024) + +#define IO_TIMEOUT_MS 15000 + +struct _FuSaharaLoader { + GObject parent_instance; + + FuUsbDevice *usb_device; + + int ep_in; + int ep_out; + gsize maxpktsize_in; + gsize maxpktsize_out; +}; + +G_DEFINE_TYPE(FuSaharaLoader, fu_sahara_loader, G_TYPE_OBJECT) + +/* Protocol definitions */ +typedef enum { + SAHARA_NO_CMD_ID = 0, + SAHARA_HELLO_ID, + SAHARA_HELLO_RESPONSE_ID, + SAHARA_READ_DATA_ID, + SAHARA_END_OF_IMAGE_TX_ID, + SAHARA_DONE_ID, + SAHARA_DONE_RESP_ID, + SAHARA_RESET_ID, + SAHARA_RESET_RESPONSE_ID, + SAHARA_READ_DATA_64_BIT_ID = 0x12, + SAHARA_LAST_CMD_ID +} FuSaharaCommandId; + +typedef enum { + SAHARA_STATUS_SUCCESS = 0, + SAHARA_STATUS_FAILED, + SAHARA_STATUS_LAST +} FuSaharaStatusCode; + +typedef enum { + SAHARA_MODE_IMAGE_TX_PENDING, + SAHARA_MODE_IMAGE_TX_COMPLETE, + SAHARA_MODE_LAST +} FuSaharaMode; + +/* Sahara packet definition */ +struct sahara_packet { + guint32 command_id; + guint32 length; + + union { + struct { + guint32 version; + guint32 version_compatible; + guint32 max_packet_length; + guint32 mode; + } hello; + struct { + guint32 version; + guint32 version_compatible; + guint32 status; + guint32 mode; + guint32 reserved[6]; + } hello_response; + struct { + guint32 image_id; + guint32 offset; + guint32 length; + } read_data; + struct { + guint32 image_id; + guint32 status; + } end_of_image_transfer; + /* done packet = header only */ + struct { + guint32 image_transfer_status; + } done_response; + /* reset packet = header only */ + /* reset response packet = header only */ + struct { + guint64 image_id; + guint64 offset; + guint64 length; + } read_data_64bit; + }; +} __attribute((packed)); + +/* helper functions */ +static FuSaharaCommandId +sahara_packet_get_command_id(GByteArray *packet) +{ + return ((struct sahara_packet *)(packet->data))->command_id; +} + +static FuSaharaCommandId +sahara_packet_get_length(GByteArray *packet) +{ + return ((struct sahara_packet *)(packet->data))->length; +} + +/* IO functions */ +static gboolean +fu_sahara_loader_find_interface(FuSaharaLoader *self, FuUsbDevice *usb_device, GError **error) +{ +#if G_USB_CHECK_VERSION(0, 3, 3) + g_autoptr(GPtrArray) intfs = NULL; + GUsbDevice *g_usb_device = fu_usb_device_get_dev(usb_device); + + /* all sahara devices use the same vid:pid pair */ + if (g_usb_device_get_vid(g_usb_device) != 0x05c6 || + g_usb_device_get_pid(g_usb_device) != 0x9008) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Wrong device and/or vendor id: 0x%04x 0x%04x", + g_usb_device_get_vid(g_usb_device), + g_usb_device_get_pid(g_usb_device)); + return FALSE; + } + + /* Parse usb interfaces and find suitable endpoints */ + intfs = g_usb_device_get_interfaces(g_usb_device, 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) == 0xFF && + g_usb_interface_get_subclass(intf) == 0xFF && + g_usb_interface_get_protocol(intf) == 0xFF) { + GUsbEndpoint *ep; + g_autoptr(GPtrArray) endpoints = NULL; + + endpoints = g_usb_interface_get_endpoints(intf); + if (endpoints == NULL || endpoints->len == 0) + continue; + + for (guint j = 0; j < endpoints->len; j++) { + ep = g_ptr_array_index(endpoints, j); + if (g_usb_endpoint_get_direction(ep) == + G_USB_DEVICE_DIRECTION_DEVICE_TO_HOST) { + self->ep_in = g_usb_endpoint_get_address(ep); + self->maxpktsize_in = + g_usb_endpoint_get_maximum_packet_size(ep); + } else { + self->ep_out = g_usb_endpoint_get_address(ep); + self->maxpktsize_out = + g_usb_endpoint_get_maximum_packet_size(ep); + } + } + + fu_usb_device_add_interface(usb_device, g_usb_interface_get_number(intf)); + + return TRUE; + } + } + + g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND, "no update interface found"); + return FALSE; +#else + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "this version of GUsb is not supported"); + return FALSE; +#endif +} + +gboolean +fu_sahara_loader_open(FuSaharaLoader *self, FuUsbDevice *usb_device, GError **error) +{ + if (!fu_sahara_loader_find_interface(self, usb_device, error)) + return FALSE; + if (!fu_device_open(FU_DEVICE(usb_device), error)) + return FALSE; + + self->usb_device = g_object_ref(usb_device); + + return TRUE; +} + +gboolean +fu_sahara_loader_close(FuSaharaLoader *self, GError **error) +{ + if (!fu_device_close(FU_DEVICE(self->usb_device), error)) + return FALSE; + g_clear_object(&self->usb_device); + return TRUE; +} + +gboolean +fu_sahara_loader_qdl_is_open(FuSaharaLoader *self) +{ + g_return_val_if_fail(self != NULL, FALSE); + + return fu_usb_device_is_open(self->usb_device); +} + +GByteArray * +fu_sahara_loader_qdl_read(FuSaharaLoader *self, GError **error) +{ + gsize actual_len = 0; + g_autoptr(GByteArray) buf = g_byte_array_sized_new(SAHARA_RAW_BUFFER_SIZE); + fu_byte_array_set_size(buf, SAHARA_RAW_BUFFER_SIZE); + + if (!g_usb_device_bulk_transfer(fu_usb_device_get_dev(self->usb_device), + self->ep_in, + buf->data, + buf->len, + &actual_len, + IO_TIMEOUT_MS, + NULL, + error)) { + g_prefix_error(error, "failed to do bulk transfer (read): "); + return NULL; + } + + g_byte_array_set_size(buf, actual_len); + + if (g_getenv("FWUPD_MODEM_MANAGER_VERBOSE") != NULL) + g_debug("received %" G_GSIZE_FORMAT " bytes", actual_len); + + return g_steal_pointer(&buf); +} + +gboolean +fu_sahara_loader_qdl_write(FuSaharaLoader *self, const guint8 *data, gsize sz, GError **error) +{ + gsize total_sent = 0; + gsize actual_len = 0; + g_autoptr(GPtrArray) chunks = NULL; + g_autoptr(GByteArray) bytes = NULL; + + /* copy const data to mutable GByteArray */ + bytes = g_byte_array_sized_new(sz); + bytes = g_byte_array_append(bytes, data, sz); + chunks = fu_chunk_array_mutable_new(bytes->data, bytes->len, 0, 0, self->maxpktsize_out); + for (guint i = 0; i < chunks->len; i++) { + FuChunk *chk = g_ptr_array_index(chunks, i); + + if (!g_usb_device_bulk_transfer(fu_usb_device_get_dev(self->usb_device), + self->ep_out, + fu_chunk_get_data_out(chk), + fu_chunk_get_data_sz(chk), + &actual_len, + IO_TIMEOUT_MS, + NULL, + error)) { + g_prefix_error(error, "failed to do bulk transfer (write data): "); + return FALSE; + } + if (actual_len != fu_chunk_get_data_sz(chk)) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_INVALID_DATA, + "only wrote %" G_GSIZE_FORMAT "bytes", + actual_len); + return FALSE; + } + total_sent += actual_len; + } + if (sz % self->maxpktsize_out == 0) { + /* sent zlp packet if needed */ + if (!g_usb_device_bulk_transfer(fu_usb_device_get_dev(self->usb_device), + self->ep_out, + NULL, + 0, + NULL, + IO_TIMEOUT_MS, + NULL, + error)) { + g_prefix_error(error, "failed to do bulk transfer (write zlp): "); + return FALSE; + } + } + + return TRUE; +} + +gboolean +fu_sahara_loader_qdl_write_bytes(FuSaharaLoader *self, GBytes *bytes, GError **error) +{ + gsize sz; + const guint8 *data = g_bytes_get_data(bytes, &sz); + return fu_sahara_loader_qdl_write(self, data, sz, error); +} + +static gboolean +fu_sahara_loader_write_prog(FuSaharaLoader *self, + guint32 offset, + guint32 length, + GBytes *prog, + GError **error) +{ + gsize sz; + const guint8 *data = g_bytes_get_data(prog, &sz); + + g_return_val_if_fail(offset + length <= sz, FALSE); + + if (g_getenv("FWUPD_MODEM_MANAGER_VERBOSE") != NULL) { + g_debug("SENDING --> RAW_DATA: %u bytes (offset = %u, total = %" G_GSIZE_FORMAT ")", + length, + offset, + sz); + } + return fu_sahara_loader_qdl_write(self, &data[offset], length, error); +} + +static gboolean +fu_sahara_loader_send_packet(FuSaharaLoader *self, GByteArray *pkt, GError **error) +{ + guint8 *data = pkt->data; + gsize sz = pkt->len; + + g_return_val_if_fail(pkt != NULL, FALSE); + + if (g_getenv("FWUPD_MODEM_MANAGER_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "tx packet", pkt->data, pkt->len); + return fu_sahara_loader_qdl_write(self, data, sz, error); +} + +/* packet composers */ +static GByteArray * +fu_sahara_create_byte_array_from_packet(const struct sahara_packet *pkt) +{ + GByteArray *self; + g_autoptr(GError) error_local = NULL; + + g_return_val_if_fail(pkt != NULL, NULL); + + self = g_byte_array_sized_new(pkt->length); + fu_byte_array_set_size(self, pkt->length); + if (!fu_memcpy_safe(self->data, + self->len, + 0, + (gconstpointer)pkt, + sizeof(struct sahara_packet), + 0, + pkt->length, + &error_local)) { + g_debug("sahara create packet failed: %s", error_local->message); + return NULL; + } + + return self; +} + +static GByteArray * +fu_sahara_loader_compose_reset_packet(void) +{ + guint32 len = 0x08; + struct sahara_packet pkt = {.command_id = GUINT32_TO_LE(SAHARA_RESET_ID), + .length = GUINT32_TO_LE(len), + {{0}}}; + + return fu_sahara_create_byte_array_from_packet(&pkt); +} + +static GByteArray * +fu_sahara_loader_compose_hello_response_packet(FuSaharaMode mode) +{ + guint32 len = 0x30; + struct sahara_packet pkt = {.command_id = GUINT32_TO_LE(SAHARA_HELLO_RESPONSE_ID), + .length = GUINT32_TO_LE(len), + {{0}}}; + + pkt.hello_response.version = GUINT32_TO_LE(SAHARA_VERSION); + pkt.hello_response.version_compatible = GUINT32_TO_LE(SAHARA_VERSION_COMPATIBLE); + pkt.hello_response.status = GUINT32_TO_LE(SAHARA_STATUS_SUCCESS); + pkt.hello_response.mode = GUINT32_TO_LE(SAHARA_MODE_IMAGE_TX_PENDING); + + return fu_sahara_create_byte_array_from_packet(&pkt); +} + +static GByteArray * +fu_sahara_loader_compose_done_packet(void) +{ + guint32 len = 0x08; + struct sahara_packet pkt = {.command_id = GUINT32_TO_LE(SAHARA_DONE_ID), + .length = GUINT32_TO_LE(len), + {{0}}}; + + return fu_sahara_create_byte_array_from_packet(&pkt); +} + +static gboolean +fu_sahara_loader_send_reset_packet(FuSaharaLoader *self, GError **error) +{ + g_autoptr(GByteArray) rx_packet = NULL; + g_autoptr(GByteArray) tx_packet = NULL; + + tx_packet = fu_sahara_loader_compose_reset_packet(); + if (!fu_sahara_loader_send_packet(self, tx_packet, error)) { + g_prefix_error(error, "Failed to send reset packet: "); + return FALSE; + } + + rx_packet = fu_sahara_loader_qdl_read(self, error); + if (rx_packet == NULL || + sahara_packet_get_command_id(rx_packet) != SAHARA_RESET_RESPONSE_ID) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Failed to receive RESET_RESPONSE packet"); + return FALSE; + } + + if (g_getenv("FWUPD_MODEM_MANAGER_VERBOSE") != NULL) + g_debug("reset succeeded"); + return TRUE; +} + +static gboolean +fu_sahara_loader_wait_hello_rsp(FuSaharaLoader *self, GError **error) +{ + g_autoptr(GByteArray) rx_packet = NULL; + g_autoptr(GByteArray) tx_packet = NULL; + + rx_packet = fu_sahara_loader_qdl_read(self, error); + if (rx_packet == NULL) { + g_autoptr(GByteArray) ping = NULL; + ping = g_byte_array_sized_new(1); + g_byte_array_set_size(ping, 1); + fu_sahara_loader_send_packet(self, ping, NULL); + rx_packet = fu_sahara_loader_qdl_read(self, error); + } + + g_return_val_if_fail(rx_packet != NULL, FALSE); + + if (g_getenv("FWUPD_MODEM_MANAGER_VERBOSE") != NULL) + fu_common_dump_raw(G_LOG_DOMAIN, "rx packet", rx_packet->data, rx_packet->len); + + if (sahara_packet_get_command_id(rx_packet) != SAHARA_HELLO_ID) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Received a different packet while waiting for the HELLO packet"); + fu_sahara_loader_send_reset_packet(self, NULL); + return FALSE; + } + + tx_packet = fu_sahara_loader_compose_hello_response_packet(SAHARA_MODE_IMAGE_TX_PENDING); + + return fu_sahara_loader_send_packet(self, tx_packet, error); +} + +/* main routine */ +gboolean +fu_sahara_loader_run(FuSaharaLoader *self, GBytes *prog, GError **error) +{ + g_return_val_if_fail(prog != NULL, FALSE); + + if (g_getenv("FWUPD_MODEM_MANAGER_VERBOSE") != NULL) + g_debug("STATE -- SAHARA_WAIT_HELLO"); + if (!fu_sahara_loader_wait_hello_rsp(self, error)) + return FALSE; + + while (TRUE) { + guint32 command_id; + struct sahara_packet *pkt; + g_autoptr(GByteArray) rx_packet = NULL; + g_autoptr(GByteArray) tx_packet = NULL; + g_autoptr(GError) error_local = NULL; + + if (g_getenv("FWUPD_MODEM_MANAGER_VERBOSE") != NULL) + g_debug("STATE -- SAHARA_WAIT_COMMAND"); + rx_packet = fu_sahara_loader_qdl_read(self, error); + if (rx_packet == NULL) + break; + if (rx_packet->len != sahara_packet_get_length(rx_packet)) { + g_set_error(error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "Received packet length is not matching"); + break; + } + + if (g_getenv("FWUPD_MODEM_MANAGER_VERBOSE") != NULL) { + fu_common_dump_raw(G_LOG_DOMAIN, + "rx_packet", + rx_packet->data, + rx_packet->len); + } + + command_id = sahara_packet_get_command_id(rx_packet); + pkt = (struct sahara_packet *)(rx_packet->data); + if (command_id == SAHARA_HELLO_ID) { + tx_packet = fu_sahara_loader_compose_hello_response_packet( + SAHARA_MODE_IMAGE_TX_PENDING); + fu_sahara_loader_send_packet(self, tx_packet, &error_local); + } else if (command_id == SAHARA_READ_DATA_ID) { + guint32 offset = pkt->read_data.offset; + guint32 length = pkt->read_data.length; + fu_sahara_loader_write_prog(self, offset, length, prog, &error_local); + } else if (command_id == SAHARA_READ_DATA_64_BIT_ID) { + guint64 offset = pkt->read_data_64bit.offset; + guint64 length = pkt->read_data_64bit.length; + fu_sahara_loader_write_prog(self, offset, length, prog, &error_local); + } else if (command_id == SAHARA_END_OF_IMAGE_TX_ID) { + guint32 status = pkt->end_of_image_transfer.status; + if (status == SAHARA_STATUS_SUCCESS) { + tx_packet = fu_sahara_loader_compose_done_packet(); + fu_sahara_loader_send_packet(self, tx_packet, &error_local); + } + } else if (command_id == SAHARA_DONE_RESP_ID) { + return TRUE; + } else { + g_warning("Unexpected packet received: cmd_id = %u, len = %u", + command_id, + sahara_packet_get_length(rx_packet)); + } + + if (error_local != NULL) + g_warning("%s", error_local->message); + } + + fu_sahara_loader_send_reset_packet(self, NULL); + return FALSE; +} + +static void +fu_sahara_loader_init(FuSaharaLoader *self) +{ +} + +static void +fu_sahara_loader_finalize(GObject *object) +{ + G_OBJECT_CLASS(fu_sahara_loader_parent_class)->finalize(object); +} + +static void +fu_sahara_loader_class_init(FuSaharaLoaderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS(klass); + object_class->finalize = fu_sahara_loader_finalize; +} + +FuSaharaLoader * +fu_sahara_loader_new(void) +{ + return g_object_new(FU_TYPE_SAHARA_LOADER, NULL); +} diff --git a/plugins/modem-manager/fu-sahara-loader.h b/plugins/modem-manager/fu-sahara-loader.h new file mode 100644 index 000000000..1baa7466f --- /dev/null +++ b/plugins/modem-manager/fu-sahara-loader.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 Quectel Wireless Solutions Co., Ltd. + * Ivan Mikhanchuk + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include + +#define FU_TYPE_SAHARA_LOADER (fu_sahara_loader_get_type()) +G_DECLARE_FINAL_TYPE(FuSaharaLoader, fu_sahara_loader, FU, SAHARA_LOADER, GObject) + +FuSaharaLoader * +fu_sahara_loader_new(void); + +gboolean +fu_sahara_loader_qdl_is_open(FuSaharaLoader *self); +GByteArray * +fu_sahara_loader_qdl_read(FuSaharaLoader *self, GError **error); +gboolean +fu_sahara_loader_qdl_write(FuSaharaLoader *self, const guint8 *data, gsize sz, GError **error); +gboolean +fu_sahara_loader_qdl_write_bytes(FuSaharaLoader *self, GBytes *bytes, GError **error); + +gboolean +fu_sahara_loader_open(FuSaharaLoader *self, FuUsbDevice *usb_device, GError **error); +gboolean +fu_sahara_loader_run(FuSaharaLoader *self, GBytes *prog, GError **error); +gboolean +fu_sahara_loader_close(FuSaharaLoader *self, GError **error); diff --git a/plugins/modem-manager/meson.build b/plugins/modem-manager/meson.build index e9de89332..73d7786af 100644 --- a/plugins/modem-manager/meson.build +++ b/plugins/modem-manager/meson.build @@ -23,6 +23,7 @@ shared_module('fu_plugin_modem_manager', 'fu-qmi-pdc-updater.c', 'fu-mbim-qdu-updater.c', 'fu-firehose-updater.c', + 'fu-sahara-loader.c', 'fu-mm-utils.c' ], include_directories : [ diff --git a/plugins/modem-manager/modem-manager.quirk b/plugins/modem-manager/modem-manager.quirk index 76616f04b..4e45861f3 100644 --- a/plugins/modem-manager/modem-manager.quirk +++ b/plugins/modem-manager/modem-manager.quirk @@ -53,3 +53,8 @@ Summary = Quectel EG25-G modem (fastboot) CounterpartGuid = USB\VID_2C7C&PID_0125 Flags = detach-at-fastboot-has-no-response ModemManagerBranchAtCommand = AT+GETFWBRANCH + +# Qualcomm Sahara device +[USB\VID_05C6&PID_9008] +Summary = Qualcomm based modems in EDL (sahara) +Plugin = modem_manager