mirror of
https://git.proxmox.com/git/fwupd
synced 2025-06-03 13:41:12 +00:00
554 lines
14 KiB
C
554 lines
14 KiB
C
/*
|
|
* Copyright (C) 2021 Quectel Wireless Solutions Co., Ltd.
|
|
* Ivan Mikhanchuk <ivan.mikhanchuk@quectel.com>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1+
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <linux/usb/ch9.h>
|
|
#include <string.h>
|
|
|
|
#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, 0x00);
|
|
|
|
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 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;
|
|
}
|
|
}
|
|
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_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, 0x00);
|
|
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_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_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);
|
|
}
|