mirror of
https://git.proxmox.com/git/fwupd
synced 2025-05-10 04:51:39 +00:00

Adds firehose update protocol, qcdm port handling, program file copy to the cache directory
1560 lines
48 KiB
C
1560 lines
48 KiB
C
/*
|
|
* Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1+
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <fwupdplugin.h>
|
|
#include <string.h>
|
|
#include <fcntl.h>
|
|
#include <glib/gstdio.h>
|
|
#include <errno.h>
|
|
|
|
#include "fu-mm-device.h"
|
|
#include "fu-mm-utils.h"
|
|
#include "fu-qmi-pdc-updater.h"
|
|
#include "fu-mbim-qdu-updater.h"
|
|
|
|
#if MM_CHECK_VERSION(1,17,2)
|
|
#include "fu-firehose-updater.h"
|
|
#endif
|
|
|
|
/* Amount of time for the modem to boot in fastboot mode. */
|
|
#define FU_MM_DEVICE_REMOVE_DELAY_RE_ENUMERATE 20000 /* ms */
|
|
|
|
/* Amount of time for the modem to be re-probed and exposed in MM after being
|
|
* uninhibited. The timeout is long enough to cover the worst case, where the
|
|
* modem boots without SIM card inserted (and therefore the initialization
|
|
* may be very slow) and also where carrier config switching is explicitly
|
|
* required (e.g. if switching from the default (DF) to generic (GC).*/
|
|
#define FU_MM_DEVICE_REMOVE_DELAY_REPROBE 120000 /* ms */
|
|
|
|
/* Amount of time for the modem to get firmware version */
|
|
#define MAX_WAIT_TIME_SECS 150 /* s */
|
|
|
|
struct _FuMmDevice {
|
|
FuDevice parent_instance;
|
|
MMManager *manager;
|
|
|
|
/* ModemManager-based devices will have MMObject and inhibition_uid set,
|
|
* udev-based ones won't (as device is already inhibited) */
|
|
MMObject *omodem;
|
|
gchar *inhibition_uid;
|
|
|
|
/* Properties read from the ModemManager-exposed modem, and to be
|
|
* propagated to plain udev-exposed modem objects. We assume that
|
|
* the firmware upgrade operation doesn't change the USB layout, and
|
|
* therefore the USB interface of the modem device that was an
|
|
* AT-capable TTY is assumed to be the same one after the upgrade.
|
|
*/
|
|
MMModemFirmwareUpdateMethod update_methods;
|
|
gchar *detach_fastboot_at;
|
|
gint port_at_ifnum;
|
|
gint port_qmi_ifnum;
|
|
|
|
/* fastboot detach handling */
|
|
gchar *port_at;
|
|
FuIOChannel *io_channel;
|
|
|
|
/* qmi-pdc update logic */
|
|
gchar *port_qmi;
|
|
FuQmiPdcUpdater *qmi_pdc_updater;
|
|
GArray *qmi_pdc_active_id;
|
|
guint attach_idle;
|
|
|
|
/* mbim-qdu update logic */
|
|
gchar *port_mbim;
|
|
#if MBIM_CHECK_VERSION(1,25,3)
|
|
FuMbimQduUpdater *mbim_qdu_updater;
|
|
#endif /* MBIM_CHECK_VERSION(1,25,3) */
|
|
|
|
/* firehose update handling */
|
|
gchar *port_qcdm;
|
|
gchar *port_edl;
|
|
#if MM_CHECK_VERSION(1,17,2)
|
|
FuFirehoseUpdater *firehose_updater;
|
|
#endif
|
|
|
|
/* firmware path */
|
|
gchar *firmware_path;
|
|
gchar *restore_firmware_path;
|
|
};
|
|
|
|
enum {
|
|
SIGNAL_ATTACH_FINISHED,
|
|
SIGNAL_LAST
|
|
};
|
|
|
|
static guint signals [SIGNAL_LAST] = { 0 };
|
|
|
|
G_DEFINE_TYPE (FuMmDevice, fu_mm_device, FU_TYPE_DEVICE)
|
|
|
|
static void
|
|
fu_mm_device_to_string (FuDevice *device, guint idt, GString *str)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
if (self->port_at != NULL)
|
|
fu_common_string_append_kv (str, idt, "AtPort", self->port_at);
|
|
if (self->port_qmi != NULL)
|
|
fu_common_string_append_kv (str, idt, "QmiPort", self->port_qmi);
|
|
if (self->port_mbim != NULL)
|
|
fu_common_string_append_kv (str, idt, "MbimPort", self->port_mbim);
|
|
if (self->port_qcdm != NULL)
|
|
fu_common_string_append_kv (str, idt, "QcdmPort", self->port_qcdm);
|
|
}
|
|
|
|
const gchar *
|
|
fu_mm_device_get_inhibition_uid (FuMmDevice *device)
|
|
{
|
|
g_return_val_if_fail (FU_IS_MM_DEVICE (device), NULL);
|
|
return device->inhibition_uid;
|
|
}
|
|
|
|
MMModemFirmwareUpdateMethod
|
|
fu_mm_device_get_update_methods (FuMmDevice *device)
|
|
{
|
|
g_return_val_if_fail (FU_IS_MM_DEVICE (device), MM_MODEM_FIRMWARE_UPDATE_METHOD_NONE);
|
|
return device->update_methods;
|
|
}
|
|
|
|
const gchar *
|
|
fu_mm_device_get_detach_fastboot_at (FuMmDevice *device)
|
|
{
|
|
g_return_val_if_fail (FU_IS_MM_DEVICE (device), NULL);
|
|
return device->detach_fastboot_at;
|
|
}
|
|
|
|
gint
|
|
fu_mm_device_get_port_at_ifnum (FuMmDevice *device)
|
|
{
|
|
g_return_val_if_fail (FU_IS_MM_DEVICE (device), -1);
|
|
return device->port_at_ifnum;
|
|
}
|
|
|
|
gint
|
|
fu_mm_device_get_port_qmi_ifnum (FuMmDevice *device)
|
|
{
|
|
g_return_val_if_fail (FU_IS_MM_DEVICE (device), -1);
|
|
return device->port_qmi_ifnum;
|
|
}
|
|
|
|
static gboolean
|
|
validate_firmware_update_method (MMModemFirmwareUpdateMethod methods, GError **error)
|
|
{
|
|
static const MMModemFirmwareUpdateMethod supported_combinations[] = {
|
|
MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT,
|
|
MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC | MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT,
|
|
#if MM_CHECK_VERSION(1,17,1)
|
|
MM_MODEM_FIRMWARE_UPDATE_METHOD_MBIM_QDU,
|
|
#endif /* MM_CHECK_VERSION(1,17,1) */
|
|
#if MM_CHECK_VERSION(1,17,2)
|
|
MM_MODEM_FIRMWARE_UPDATE_METHOD_FIREHOSE,
|
|
#endif
|
|
};
|
|
g_autofree gchar *methods_str = NULL;
|
|
|
|
methods_str = mm_modem_firmware_update_method_build_string_from_mask (methods);
|
|
for (guint i = 0; i < G_N_ELEMENTS (supported_combinations); i++) {
|
|
if (supported_combinations[i] == methods) {
|
|
g_debug ("valid firmware update combination: %s", methods_str);
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED,
|
|
"invalid firmware update combination: %s", methods_str);
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_probe_default (FuDevice *device, GError **error)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
MMModemFirmware *modem_fw;
|
|
MMModem *modem = mm_object_peek_modem (self->omodem);
|
|
MMModemPortInfo *ports = NULL;
|
|
const gchar **device_ids;
|
|
const gchar *version;
|
|
guint n_ports = 0;
|
|
GPtrArray *vendors;
|
|
g_autoptr(MMFirmwareUpdateSettings) update_settings = NULL;
|
|
g_autofree gchar *device_sysfs_path = NULL;
|
|
g_autofree gchar *device_bus = NULL;
|
|
|
|
/* inhibition uid is the modem interface 'Device' property, which may
|
|
* be the device sysfs path or a different user-provided id */
|
|
self->inhibition_uid = mm_modem_dup_device (modem);
|
|
|
|
/* find out what update methods we should use */
|
|
modem_fw = mm_object_peek_modem_firmware (self->omodem);
|
|
update_settings = mm_modem_firmware_get_update_settings (modem_fw);
|
|
self->update_methods = mm_firmware_update_settings_get_method (update_settings);
|
|
if (self->update_methods == MM_MODEM_FIRMWARE_UPDATE_METHOD_NONE) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"modem cannot be put in programming mode");
|
|
return FALSE;
|
|
}
|
|
|
|
/* make sure the combination is supported */
|
|
if (!validate_firmware_update_method (self->update_methods, error))
|
|
return FALSE;
|
|
|
|
/* various fastboot commands */
|
|
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT) {
|
|
const gchar *tmp;
|
|
tmp = mm_firmware_update_settings_get_fastboot_at (update_settings);
|
|
if (tmp == NULL) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"modem does not set fastboot command");
|
|
return FALSE;
|
|
}
|
|
self->detach_fastboot_at = g_strdup (tmp);
|
|
}
|
|
|
|
/* get GUIDs */
|
|
device_ids = mm_firmware_update_settings_get_device_ids (update_settings);
|
|
if (device_ids == NULL || device_ids[0] == NULL) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"modem did not specify any device IDs");
|
|
return FALSE;
|
|
}
|
|
|
|
/* get version string, which is fw_ver+config_ver */
|
|
version = mm_firmware_update_settings_get_version (update_settings);
|
|
if (version == NULL) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"modem did not specify a firmware version");
|
|
return FALSE;
|
|
}
|
|
|
|
/* look for the AT and QMI/MBIM ports */
|
|
if (!mm_modem_get_ports (modem, &ports, &n_ports)) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to get port information");
|
|
return FALSE;
|
|
}
|
|
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT) {
|
|
for (guint i = 0; i < n_ports; i++) {
|
|
if (ports[i].type == MM_MODEM_PORT_TYPE_AT) {
|
|
self->port_at = g_strdup_printf ("/dev/%s", ports[i].name);
|
|
break;
|
|
}
|
|
}
|
|
fu_device_add_protocol (device, "com.google.fastboot");
|
|
}
|
|
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC) {
|
|
for (guint i = 0; i < n_ports; i++) {
|
|
if ((ports[i].type == MM_MODEM_PORT_TYPE_QMI) ||
|
|
(ports[i].type == MM_MODEM_PORT_TYPE_MBIM)) {
|
|
self->port_qmi = g_strdup_printf ("/dev/%s", ports[i].name);
|
|
break;
|
|
}
|
|
}
|
|
/* only set if fastboot wasn't already set */
|
|
if (fu_device_get_protocols (device)->len == 0)
|
|
fu_device_add_protocol (device, "com.qualcomm.qmi_pdc");
|
|
}
|
|
#if MM_CHECK_VERSION(1,17,1)
|
|
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_MBIM_QDU) {
|
|
for (guint i = 0; i < n_ports; i++) {
|
|
if (ports[i].type == MM_MODEM_PORT_TYPE_MBIM) {
|
|
self->port_mbim = g_strdup_printf ("/dev/%s", ports[i].name);
|
|
break;
|
|
}
|
|
}
|
|
fu_device_add_protocol (device, "com.qualcomm.mbim_qdu");
|
|
}
|
|
#endif /* MM_CHECK_VERSION(1,17,1) */
|
|
#if MM_CHECK_VERSION(1,17,2)
|
|
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FIREHOSE) {
|
|
for (guint i = 0; i < n_ports; i++) {
|
|
if (ports[i].type == MM_MODEM_PORT_TYPE_QCDM) {
|
|
self->port_qcdm = g_strdup_printf ("/dev/%s", ports[i].name);
|
|
break;
|
|
}
|
|
}
|
|
fu_device_add_protocol (device, "com.qualcomm.firehose");
|
|
}
|
|
#endif
|
|
|
|
mm_modem_port_info_array_free (ports, n_ports);
|
|
|
|
/* an at port is required for fastboot */
|
|
if ((self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT) &&
|
|
(self->port_at == NULL)) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to find AT port");
|
|
return FALSE;
|
|
}
|
|
|
|
/* a qmi port is required for qmi-pdc */
|
|
if ((self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC) &&
|
|
(self->port_qmi == NULL)) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to find QMI port");
|
|
return FALSE;
|
|
}
|
|
|
|
#if MM_CHECK_VERSION(1,17,1)
|
|
/* a mbim port is required for mbim-qdu */
|
|
if ((self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_MBIM_QDU) &&
|
|
(self->port_mbim == NULL)) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to find MBIM port");
|
|
return FALSE;
|
|
}
|
|
|
|
#endif /* MM_CHECK_VERSION(1,17,1) */
|
|
#if MM_CHECK_VERSION(1,17,2)
|
|
/* a qcdm port is required for firehose */
|
|
if ((self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FIREHOSE) &&
|
|
(self->port_qcdm == NULL)) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to find QCDM port");
|
|
return FALSE;
|
|
}
|
|
#endif
|
|
|
|
if (self->port_at != NULL) {
|
|
fu_mm_utils_get_port_info (self->port_at, &device_bus, &device_sysfs_path, &self->port_at_ifnum, NULL);
|
|
}
|
|
if (self->port_qmi != NULL) {
|
|
g_autofree gchar *qmi_device_sysfs_path = NULL;
|
|
g_autofree gchar *qmi_device_bus = NULL;
|
|
fu_mm_utils_get_port_info (self->port_qmi, &qmi_device_bus, &qmi_device_sysfs_path, &self->port_qmi_ifnum, NULL);
|
|
if (device_sysfs_path == NULL && qmi_device_sysfs_path != NULL) {
|
|
device_sysfs_path = g_steal_pointer (&qmi_device_sysfs_path);
|
|
} else if (g_strcmp0 (device_sysfs_path, qmi_device_sysfs_path) != 0) {
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"mismatched device sysfs path: %s != %s",
|
|
device_sysfs_path, qmi_device_sysfs_path);
|
|
return FALSE;
|
|
}
|
|
if (device_bus == NULL && qmi_device_bus != NULL) {
|
|
device_bus = g_steal_pointer (&qmi_device_bus);
|
|
} else if (g_strcmp0 (device_bus, qmi_device_bus) != 0) {
|
|
g_set_error (error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
"mismatched device bus: %s != %s",
|
|
device_bus, qmi_device_bus);
|
|
return FALSE;
|
|
}
|
|
}
|
|
if (self->port_mbim != NULL) {
|
|
g_autofree gchar *mbim_device_sysfs_path = NULL;
|
|
g_autofree gchar *mbim_device_bus = NULL;
|
|
fu_mm_utils_get_port_info (self->port_mbim, &mbim_device_bus, &mbim_device_sysfs_path, NULL, NULL);
|
|
if (device_sysfs_path == NULL && mbim_device_sysfs_path != NULL) {
|
|
device_sysfs_path = g_steal_pointer (&mbim_device_sysfs_path);
|
|
} else if (g_strcmp0 (device_sysfs_path, mbim_device_sysfs_path) != 0) {
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"mismatched device sysfs path: %s != %s",
|
|
device_sysfs_path, mbim_device_sysfs_path);
|
|
return FALSE;
|
|
}
|
|
if (device_bus == NULL && mbim_device_bus != NULL) {
|
|
device_bus = g_steal_pointer (&mbim_device_bus);
|
|
} else if (g_strcmp0 (device_bus, mbim_device_bus) != 0) {
|
|
g_set_error (error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
"mismatched device bus: %s != %s",
|
|
device_bus, mbim_device_bus);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
if (self->port_qcdm != NULL) {
|
|
g_autofree gchar *qcdm_device_sysfs_path = NULL;
|
|
g_autofree gchar *qcdm_device_bus = NULL;
|
|
fu_mm_utils_get_port_info (self->port_qcdm, &qcdm_device_bus, &qcdm_device_sysfs_path, NULL, NULL);
|
|
if (device_sysfs_path == NULL && qcdm_device_sysfs_path != NULL) {
|
|
device_sysfs_path = g_steal_pointer (&qcdm_device_sysfs_path);
|
|
} else if (g_strcmp0 (device_sysfs_path, qcdm_device_sysfs_path) != 0) {
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"mismatched device sysfs path: %s != %s",
|
|
device_sysfs_path, qcdm_device_sysfs_path);
|
|
return FALSE;
|
|
}
|
|
if (device_bus == NULL && qcdm_device_bus != NULL) {
|
|
device_bus = g_steal_pointer (&qcdm_device_bus);
|
|
} else if (g_strcmp0 (device_bus, qcdm_device_bus) != 0) {
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"mismatched device bus: %s != %s",
|
|
device_bus, qcdm_device_bus);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* if no device sysfs file, error out */
|
|
if (device_sysfs_path == NULL || device_bus == NULL) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to find device details");
|
|
return FALSE;
|
|
}
|
|
|
|
/* add properties to fwupd device */
|
|
fu_device_set_physical_id (device, device_sysfs_path);
|
|
if (mm_modem_get_manufacturer (modem) != NULL)
|
|
fu_device_set_vendor (device, mm_modem_get_manufacturer (modem));
|
|
if (mm_modem_get_model (modem) != NULL)
|
|
fu_device_set_name (device, mm_modem_get_model (modem));
|
|
fu_device_set_version (device, version);
|
|
for (guint i = 0; device_ids[i] != NULL; i++)
|
|
fu_device_add_instance_id (device, device_ids[i]);
|
|
vendors = fu_device_get_vendor_ids (device);
|
|
if (vendors == NULL || vendors->len == 0) {
|
|
g_autofree gchar *path = NULL;
|
|
g_autofree gchar *value_str = NULL;
|
|
g_autoptr(GError) error_local = NULL;
|
|
|
|
if (g_strcmp0 (device_bus, "USB") == 0)
|
|
path = g_build_filename (device_sysfs_path, "idVendor", NULL);
|
|
else if (g_strcmp0 (device_bus, "PCI") == 0)
|
|
path = g_build_filename (device_sysfs_path, "vendor", NULL);
|
|
|
|
if (path == NULL) {
|
|
g_warning ("failed to set vendor ID: unsupported bus: %s", device_bus);
|
|
} else if (!g_file_get_contents (path, &value_str, NULL, &error_local)) {
|
|
g_warning ("failed to set vendor ID: %s", error_local->message);
|
|
} else {
|
|
guint64 value_int;
|
|
|
|
/* note: the string value may be prefixed with '0x' (e.g. when reading
|
|
* the PCI 'vendor' attribute, or not prefixed with anything, as in the
|
|
* USB 'idVendor' attribute. */
|
|
value_int = g_ascii_strtoull (value_str, NULL, 16);
|
|
if (value_int > G_MAXUINT16) {
|
|
g_warning ("failed to set vendor ID: invalid value: %s", value_str);
|
|
} else {
|
|
g_autofree gchar *vendor_id = g_strdup_printf ("%s:0x%04X", device_bus, (guint) value_int);
|
|
fu_device_add_vendor_id (device, vendor_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_probe_udev (FuDevice *device, GError **error)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
|
|
/* an at port is required for fastboot */
|
|
if ((self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT) &&
|
|
(self->port_at == NULL)) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to find AT port");
|
|
return FALSE;
|
|
}
|
|
|
|
/* a qmi port is required for qmi-pdc */
|
|
if ((self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC) &&
|
|
(self->port_qmi == NULL)) {
|
|
g_set_error_literal (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to find QMI port");
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_probe (FuDevice *device, GError **error)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
|
|
if (self->omodem) {
|
|
return fu_mm_device_probe_default (device, error);
|
|
} else {
|
|
return fu_mm_device_probe_udev (device, error);
|
|
}
|
|
}
|
|
|
|
|
|
#if MM_CHECK_VERSION(1,17,2)
|
|
static gboolean
|
|
fu_mm_device_io_open_qcdm (FuMmDevice *self, GError **error)
|
|
{
|
|
/* open device */
|
|
self->io_channel = fu_io_channel_new_file (self->port_qcdm, error);
|
|
if (self->io_channel == NULL)
|
|
return FALSE;
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_qcdm_cmd (FuMmDevice *self, const guint8 *cmd, gsize cmd_len, GError **error)
|
|
{
|
|
g_autoptr(GBytes) qcdm_req = NULL;
|
|
g_autoptr(GBytes) qcdm_res = NULL;
|
|
|
|
/* command */
|
|
qcdm_req = g_bytes_new (cmd, cmd_len);
|
|
if (g_getenv ("FWUPD_MODEM_MANAGER_VERBOSE") != NULL)
|
|
fu_common_dump_bytes (G_LOG_DOMAIN, "writing", qcdm_req);
|
|
if (!fu_io_channel_write_bytes (self->io_channel, qcdm_req, 1500,
|
|
FU_IO_CHANNEL_FLAG_FLUSH_INPUT, error)) {
|
|
g_prefix_error (error, "failed to write qcdm command: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* response */
|
|
qcdm_res = fu_io_channel_read_bytes (self->io_channel, -1, 1500,
|
|
FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error);
|
|
if (qcdm_res == NULL) {
|
|
g_prefix_error (error, "failed to read qcdm response: ");
|
|
return FALSE;
|
|
}
|
|
if (g_getenv ("FWUPD_MODEM_MANAGER_VERBOSE") != NULL)
|
|
fu_common_dump_bytes (G_LOG_DOMAIN, "read", qcdm_res);
|
|
|
|
/* command == response */
|
|
if (g_bytes_compare (qcdm_res, qcdm_req) != 0) {
|
|
g_set_error (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to read valid qcdm response");
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
#endif /* MM_CHECK_VERSION(1,17,2) */
|
|
|
|
static gboolean
|
|
fu_mm_device_at_cmd (FuMmDevice *self, const gchar *cmd, GError **error)
|
|
{
|
|
const gchar *buf;
|
|
gsize bufsz = 0;
|
|
g_autoptr(GBytes) at_req = NULL;
|
|
g_autoptr(GBytes) at_res = NULL;
|
|
g_autofree gchar *cmd_cr = g_strdup_printf ("%s\r\n", cmd);
|
|
|
|
/* command */
|
|
at_req = g_bytes_new (cmd_cr, strlen (cmd_cr));
|
|
if (g_getenv ("FWUPD_MODEM_MANAGER_VERBOSE") != NULL)
|
|
fu_common_dump_bytes (G_LOG_DOMAIN, "writing", at_req);
|
|
if (!fu_io_channel_write_bytes (self->io_channel, at_req, 1500,
|
|
FU_IO_CHANNEL_FLAG_FLUSH_INPUT, error)) {
|
|
g_prefix_error (error, "failed to write %s: ", cmd);
|
|
return FALSE;
|
|
}
|
|
|
|
/* response */
|
|
at_res = fu_io_channel_read_bytes (self->io_channel, -1, 1500,
|
|
FU_IO_CHANNEL_FLAG_SINGLE_SHOT, error);
|
|
if (at_res == NULL) {
|
|
g_prefix_error (error, "failed to read response for %s: ", cmd);
|
|
return FALSE;
|
|
}
|
|
if (g_getenv ("FWUPD_MODEM_MANAGER_VERBOSE") != NULL)
|
|
fu_common_dump_bytes (G_LOG_DOMAIN, "read", at_res);
|
|
buf = g_bytes_get_data (at_res, &bufsz);
|
|
if (bufsz < 6) {
|
|
g_set_error (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to read valid response for %s", cmd);
|
|
return FALSE;
|
|
}
|
|
if (memcmp (buf, "\r\nOK\r\n", 6) != 0) {
|
|
g_autofree gchar *tmp = g_strndup (buf + 2, bufsz - 4);
|
|
g_set_error (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"failed to read valid response for %s: %s",
|
|
cmd, tmp);
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_io_open (FuMmDevice *self, GError **error)
|
|
{
|
|
/* open device */
|
|
self->io_channel = fu_io_channel_new_file (self->port_at, error);
|
|
if (self->io_channel == NULL)
|
|
return FALSE;
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_io_close (FuMmDevice *self, GError **error)
|
|
{
|
|
if (self->io_channel != NULL) {
|
|
if (!fu_io_channel_shutdown (self->io_channel, error))
|
|
return FALSE;
|
|
g_clear_object (&self->io_channel);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_detach_fastboot (FuDevice *device, GError **error)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
|
|
/* boot to fastboot mode */
|
|
locker = fu_device_locker_new_full (device,
|
|
(FuDeviceLockerFunc) fu_mm_device_io_open,
|
|
(FuDeviceLockerFunc) fu_mm_device_io_close,
|
|
error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
if (!fu_mm_device_at_cmd (self, "AT", error))
|
|
return FALSE;
|
|
if (!fu_mm_device_at_cmd (self, self->detach_fastboot_at, error)) {
|
|
g_prefix_error (error, "rebooting into fastboot not supported: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* success */
|
|
fu_device_set_remove_delay (device, FU_MM_DEVICE_REMOVE_DELAY_RE_ENUMERATE);
|
|
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
|
|
return TRUE;
|
|
}
|
|
|
|
#if MM_CHECK_VERSION(1,17,2)
|
|
static gboolean
|
|
fu_mm_device_find_edl_port (FuDevice *device, GError **error)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
|
|
for (guint i = 0; i < 30; i++) {
|
|
if (fu_mm_utils_find_device_file (fu_device_get_physical_id (
|
|
FU_DEVICE (self)), "wwan", &self->port_edl, NULL))
|
|
return TRUE;
|
|
|
|
g_usleep (250 * 1000);
|
|
}
|
|
|
|
g_set_error_literal (error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
"Couldn't find EDL port");
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_qcdm_switch_to_edl (FuDevice *device, GError **error)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
static const guint8 emergency_download[] = { 0x4b, 0x65, 0x01, 0x00, 0x54, 0x0f, 0x7e };
|
|
|
|
/* trigger emergency download mode, up to 30s retrying until the QCDM
|
|
* port goes away; this takes us to the EDL (embedded downloader) execution
|
|
* environment */
|
|
|
|
for (guint i = 0; i < 30; i++) {
|
|
g_autoptr(GError) error_local = NULL;
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
|
|
if (i > 0)
|
|
g_usleep (G_USEC_PER_SEC);
|
|
|
|
locker = fu_device_locker_new_full (self,
|
|
(FuDeviceLockerFunc) fu_mm_device_io_open_qcdm,
|
|
(FuDeviceLockerFunc) fu_mm_device_io_close,
|
|
&error_local);
|
|
if (locker == NULL) {
|
|
if (i > 0 && g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE))
|
|
return fu_mm_device_find_edl_port (device, error);
|
|
g_debug ("couldn't open QCDM port to switch to EDL mode: %s", error_local->message);
|
|
break;
|
|
}
|
|
|
|
if (!fu_mm_device_qcdm_cmd (self, emergency_download, G_N_ELEMENTS (emergency_download), &error_local)) {
|
|
g_debug ("couldn't send QCDM command to switch to EDL mode: %s", error_local->message);
|
|
break;
|
|
}
|
|
}
|
|
|
|
g_set_error_literal (error,
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
"Couldn't switch device to embedded downloader execution environment");
|
|
return FALSE;
|
|
}
|
|
#endif /* MM_CHECK_VERSION(1,17,2) */
|
|
|
|
static gboolean
|
|
fu_mm_device_detach (FuDevice *device, GError **error)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
|
|
locker = fu_device_locker_new (device, error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
|
|
/* This plugin supports several methods to download firmware:
|
|
* fastboot, qmi-pdc, firehose. A modem may require one of those,
|
|
* or several, depending on the update type or the modem type.
|
|
*
|
|
* The first time this detach() method is executed is always for a
|
|
* FuMmDevice that was created from a MM-exposed modem, which is the
|
|
* moment when we're going to decide the amount of retries we need to
|
|
* flash all firmware.
|
|
*
|
|
* If the FuMmModem is created from a MM-exposed modem and...
|
|
* a) we only support fastboot, we just trigger the fastboot detach.
|
|
* b) we support both fastboot and qmi-pdc, we will set the
|
|
* ANOTHER_WRITE_REQUIRED flag in the device and we'll trigger
|
|
* the fastboot detach.
|
|
* c) we only support firehose, skip detach and switch to embedded
|
|
* downloader mode (EDL) during write_firmware.
|
|
*
|
|
* If the FuMmModem is created from udev events...
|
|
* c) it means we're in the extra required write that was flagged
|
|
* in an earlier detach(), and we need to perform the qmi-pdc
|
|
* update procedure at this time, so we just exit without any
|
|
* detach.
|
|
*/
|
|
|
|
/* FuMmDevice created from MM... */
|
|
if (self->omodem != NULL) {
|
|
/* both fastboot and qmi-pdc supported? another write required */
|
|
if ((self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT) &&
|
|
(self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC)) {
|
|
g_debug ("both fastboot and qmi-pdc supported, so the upgrade requires another write");
|
|
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_ANOTHER_WRITE_REQUIRED);
|
|
}
|
|
/* fastboot */
|
|
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FASTBOOT)
|
|
return fu_mm_device_detach_fastboot (device, error);
|
|
/* otherwise, assume we don't need any detach */
|
|
return TRUE;
|
|
}
|
|
|
|
/* FuMmDevice created from udev...
|
|
* assume we don't need any detach */
|
|
return TRUE;
|
|
}
|
|
|
|
typedef struct {
|
|
gchar *filename;
|
|
GBytes *bytes;
|
|
GArray *digest;
|
|
gboolean active;
|
|
} FuMmFileInfo;
|
|
|
|
static void
|
|
fu_mm_file_info_free (FuMmFileInfo *file_info)
|
|
{
|
|
g_clear_pointer (&file_info->digest, g_array_unref);
|
|
g_free (file_info->filename);
|
|
g_bytes_unref (file_info->bytes);
|
|
g_free (file_info);
|
|
}
|
|
|
|
typedef struct {
|
|
FuMmDevice *device;
|
|
GError *error;
|
|
GPtrArray *file_infos;
|
|
} FuMmArchiveIterateCtx;
|
|
|
|
static gboolean
|
|
fu_mm_should_be_active (const gchar *version,
|
|
const gchar *filename)
|
|
{
|
|
g_auto(GStrv) split = NULL;
|
|
g_autofree gchar *carrier_id = NULL;
|
|
|
|
/* The filename of the mcfg file is composed of a "mcfg." prefix, then the
|
|
* carrier code, followed by the carrier version, and finally a ".mbn"
|
|
* prefix. Here we try to guess, based on the carrier code, whether the
|
|
* specific mcfg file should be activated after the firmware upgrade
|
|
* operation.
|
|
*
|
|
* This logic requires that the previous device version includes the carrier
|
|
* code also embedded in the version string. E.g. "xxxx.VF.xxxx". If we find
|
|
* this match, we assume this is the active config to use.
|
|
*/
|
|
|
|
split = g_strsplit (filename, ".", -1);
|
|
if (g_strv_length (split) < 4)
|
|
return FALSE;
|
|
if (g_strcmp0 (split[0], "mcfg") != 0)
|
|
return FALSE;
|
|
|
|
carrier_id = g_strdup_printf (".%s.", split[1]);
|
|
return (g_strstr_len (version, -1, carrier_id) != NULL);
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_qmi_pdc_archive_iterate_mcfg (FuArchive *archive,
|
|
const gchar *filename,
|
|
GBytes *bytes,
|
|
gpointer user_data,
|
|
GError **error)
|
|
{
|
|
FuMmArchiveIterateCtx *ctx = user_data;
|
|
FuMmFileInfo *file_info;
|
|
|
|
/* filenames should be named as 'mcfg.*.mbn', e.g.: mcfg.A2.018.mbn */
|
|
if (!g_str_has_prefix (filename, "mcfg.") || !g_str_has_suffix (filename, ".mbn"))
|
|
return TRUE;
|
|
|
|
file_info = g_new0 (FuMmFileInfo, 1);
|
|
file_info->filename = g_strdup (filename);
|
|
file_info->bytes = g_bytes_ref (bytes);
|
|
file_info->active = fu_mm_should_be_active (fu_device_get_version (FU_DEVICE (ctx->device)), filename);
|
|
g_ptr_array_add (ctx->file_infos, file_info);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_qmi_open (FuMmDevice *self, GError **error)
|
|
{
|
|
self->qmi_pdc_updater = fu_qmi_pdc_updater_new (self->port_qmi);
|
|
return fu_qmi_pdc_updater_open (self->qmi_pdc_updater, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_qmi_close (FuMmDevice *self, GError **error)
|
|
{
|
|
g_autoptr(FuQmiPdcUpdater) updater = NULL;
|
|
|
|
updater = g_steal_pointer (&self->qmi_pdc_updater);
|
|
return fu_qmi_pdc_updater_close (updater, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_qmi_close_no_error (FuMmDevice *self, GError **error)
|
|
{
|
|
g_autoptr(FuQmiPdcUpdater) updater = NULL;
|
|
|
|
updater = g_steal_pointer (&self->qmi_pdc_updater);
|
|
fu_qmi_pdc_updater_close (updater, NULL);
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_write_firmware_qmi_pdc (FuDevice *device, GBytes *fw, GArray **active_id, GError **error)
|
|
{
|
|
g_autoptr(FuArchive) archive = NULL;
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
g_autoptr(GPtrArray) file_infos = g_ptr_array_new_with_free_func ((GDestroyNotify)fu_mm_file_info_free);
|
|
gint active_i = -1;
|
|
FuMmArchiveIterateCtx archive_context = {
|
|
.device = FU_MM_DEVICE (device),
|
|
.error = NULL,
|
|
.file_infos = file_infos,
|
|
};
|
|
|
|
/* decompress entire archive ahead of time */
|
|
archive = fu_archive_new (fw, FU_ARCHIVE_FLAG_IGNORE_PATH, error);
|
|
if (archive == NULL)
|
|
return FALSE;
|
|
|
|
/* boot to fastboot mode */
|
|
locker = fu_device_locker_new_full (device,
|
|
(FuDeviceLockerFunc) fu_mm_device_qmi_open,
|
|
(FuDeviceLockerFunc) fu_mm_device_qmi_close,
|
|
error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
|
|
/* process the list of MCFG files to write */
|
|
if (!fu_archive_iterate (archive,
|
|
fu_mm_qmi_pdc_archive_iterate_mcfg,
|
|
&archive_context,
|
|
error))
|
|
return FALSE;
|
|
|
|
for (guint i = 0; i < file_infos->len; i++) {
|
|
FuMmFileInfo *file_info = g_ptr_array_index (file_infos, i);
|
|
file_info->digest = fu_qmi_pdc_updater_write (archive_context.device->qmi_pdc_updater,
|
|
file_info->filename,
|
|
file_info->bytes,
|
|
&archive_context.error);
|
|
if (file_info->digest == NULL) {
|
|
g_prefix_error (&archive_context.error,
|
|
"Failed to write file '%s':", file_info->filename);
|
|
break;
|
|
}
|
|
/* if we wrongly detect more than one, just assume the latest one; this
|
|
* is not critical, it may just take a bit more time to perform the
|
|
* automatic carrier config switching in ModemManager */
|
|
if (file_info->active)
|
|
active_i = i;
|
|
}
|
|
|
|
/* set expected active configuration */
|
|
if (active_i >= 0 && active_id != NULL) {
|
|
FuMmFileInfo *file_info = g_ptr_array_index (file_infos, active_i);
|
|
*active_id = g_array_ref (file_info->digest);
|
|
}
|
|
|
|
if (archive_context.error != NULL) {
|
|
g_propagate_error (error, archive_context.error);
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
#if MM_CHECK_VERSION(1,17,1) && MBIM_CHECK_VERSION(1,25,3)
|
|
typedef struct {
|
|
FuDevice *device;
|
|
GMainLoop *mainloop;
|
|
gchar *version;
|
|
GError *error;
|
|
} FuMmGetFirmwareVersionCtx;
|
|
|
|
static gboolean
|
|
fu_mm_device_mbim_open (FuMmDevice *self, GError **error)
|
|
{
|
|
self->mbim_qdu_updater = fu_mbim_qdu_updater_new (self->port_mbim);
|
|
return fu_mbim_qdu_updater_open (self->mbim_qdu_updater, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_mbim_close (FuMmDevice *self, GError **error)
|
|
{
|
|
g_autoptr(FuMbimQduUpdater) updater = NULL;
|
|
updater = g_steal_pointer (&self->mbim_qdu_updater);
|
|
return fu_mbim_qdu_updater_close (updater, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_locker_new_timeout (gpointer user_data)
|
|
{
|
|
FuMmGetFirmwareVersionCtx *ctx = user_data;
|
|
|
|
g_main_loop_quit (ctx->mainloop);
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_get_firmware_version_mbim_timeout (gpointer user_data)
|
|
{
|
|
FuMmGetFirmwareVersionCtx *ctx = user_data;
|
|
FuMmDevice *self = FU_MM_DEVICE (ctx->device);
|
|
|
|
g_clear_error (&ctx->error);
|
|
ctx->version = fu_mbim_qdu_updater_check_ready (self->mbim_qdu_updater, &ctx->error);
|
|
g_main_loop_quit (ctx->mainloop);
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static gchar *
|
|
fu_mm_device_get_firmware_version_mbim (FuDevice *device, GError **error)
|
|
{
|
|
GTimer *timer = g_timer_new ();
|
|
g_autoptr(GMainLoop) mainloop = g_main_loop_new (NULL, FALSE);
|
|
FuMmGetFirmwareVersionCtx ctx = {
|
|
.device = device,
|
|
.mainloop = mainloop,
|
|
.version = NULL,
|
|
.error = NULL,
|
|
};
|
|
|
|
while (ctx.version == NULL && g_timer_elapsed (timer, NULL) < MAX_WAIT_TIME_SECS) {
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
|
|
g_clear_error (&ctx.error);
|
|
locker = fu_device_locker_new_full (device,
|
|
(FuDeviceLockerFunc) fu_mm_device_mbim_open,
|
|
(FuDeviceLockerFunc) fu_mm_device_mbim_close,
|
|
&ctx.error);
|
|
|
|
if (locker == NULL) {
|
|
g_timeout_add_seconds (20, fu_mm_device_locker_new_timeout, &ctx);
|
|
g_main_loop_run (mainloop);
|
|
continue;
|
|
}
|
|
|
|
g_timeout_add_seconds (10, fu_mm_device_get_firmware_version_mbim_timeout, &ctx);
|
|
g_main_loop_run (mainloop);
|
|
}
|
|
|
|
g_timer_destroy (timer);
|
|
|
|
if (ctx.version == NULL && ctx.error != NULL) {
|
|
g_propagate_error (error, ctx.error);
|
|
return NULL;
|
|
}
|
|
|
|
return ctx.version;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_writeln (const gchar *fn, const gchar *buf, GError **error)
|
|
{
|
|
int fd;
|
|
g_autoptr(FuIOChannel) io = NULL;
|
|
|
|
fd = open (fn, O_WRONLY);
|
|
if (fd < 0) {
|
|
g_set_error (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INVALID_FILE,
|
|
"could not open %s", fn);
|
|
return FALSE;
|
|
}
|
|
io = fu_io_channel_unix_new (fd);
|
|
return fu_io_channel_write_raw (io, (const guint8 *) buf, strlen (buf),
|
|
1000, FU_IO_CHANNEL_FLAG_NONE, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_write_firmware_mbim_qdu (FuDevice *device, GBytes *fw, GError **error)
|
|
{
|
|
GBytes *data;
|
|
XbNode *part = NULL;
|
|
const gchar *filename = NULL;
|
|
const gchar *csum;
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
g_autofree gchar *device_sysfs_path = NULL;
|
|
g_autofree gchar *autosuspend_delay_filename = NULL;
|
|
g_autofree gchar *csum_actual = NULL;
|
|
g_autofree gchar *version = NULL;
|
|
g_autoptr(FuArchive) archive = NULL;
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
g_autoptr(XbBuilder) builder = xb_builder_new ();
|
|
g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
|
|
g_autoptr(XbSilo) silo = NULL;
|
|
|
|
/* decompress entire archive ahead of time */
|
|
archive = fu_archive_new (fw, FU_ARCHIVE_FLAG_IGNORE_PATH, error);
|
|
if (archive == NULL)
|
|
return FALSE;
|
|
|
|
locker = fu_device_locker_new_full (device,
|
|
(FuDeviceLockerFunc) fu_mm_device_mbim_open,
|
|
(FuDeviceLockerFunc) fu_mm_device_mbim_close,
|
|
error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
|
|
/* load the manifest of operations */
|
|
data = fu_archive_lookup_by_fn (archive, "flashfile.xml", error);
|
|
if (data == NULL)
|
|
return FALSE;
|
|
if (!xb_builder_source_load_bytes (source, data,
|
|
XB_BUILDER_SOURCE_FLAG_NONE, error))
|
|
return FALSE;
|
|
xb_builder_import_source (builder, source);
|
|
silo = xb_builder_compile (builder, XB_BUILDER_COMPILE_FLAG_NONE, NULL, error);
|
|
if (silo == NULL)
|
|
return FALSE;
|
|
|
|
part = xb_silo_query_first (silo, "parts/part", error);
|
|
if (part == NULL)
|
|
return FALSE;
|
|
filename = xb_node_get_attr (part, "filename");
|
|
csum = xb_node_get_attr (part, "MD5");
|
|
data = fu_archive_lookup_by_fn (archive, filename, error);
|
|
if (data == NULL)
|
|
return FALSE;
|
|
csum_actual = g_compute_checksum_for_bytes (G_CHECKSUM_MD5, data);
|
|
if (g_strcmp0 (csum, csum_actual) != 0) {
|
|
g_debug ("[%s] MD5 not matched", filename);
|
|
g_set_error (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_SUPPORTED,
|
|
"[%s] MD5 not matched", filename);
|
|
return FALSE;
|
|
} else {
|
|
g_debug ("[%s] MD5 matched", filename);
|
|
}
|
|
|
|
/* autosuspend delay updated for a proper firmware update */
|
|
fu_mm_utils_get_port_info (self->port_mbim, NULL, &device_sysfs_path, NULL, NULL);
|
|
autosuspend_delay_filename = g_build_filename (device_sysfs_path, "/power/autosuspend_delay_ms", NULL);
|
|
if(!fu_mm_device_writeln (autosuspend_delay_filename, "10000", error))
|
|
return FALSE;
|
|
|
|
fu_device_set_status (device, FWUPD_STATUS_DEVICE_WRITE);
|
|
fu_mbim_qdu_updater_write (self->mbim_qdu_updater, filename, data, device, error);
|
|
if (!fu_device_locker_close (locker, error))
|
|
return FALSE;
|
|
|
|
fu_device_set_status (device, FWUPD_STATUS_DEVICE_READ);
|
|
version = fu_mm_device_get_firmware_version_mbim (device, error);
|
|
if (version == NULL)
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
#endif /* MM_CHECK_VERSION(1,17,1) && MBIM_CHECK_VERSION(1,25,3) */
|
|
|
|
#if MM_CHECK_VERSION(1,17,2)
|
|
static gboolean
|
|
fu_mm_device_firehose_open (FuMmDevice *self, GError **error)
|
|
{
|
|
self->firehose_updater = fu_firehose_updater_new (self->port_edl);
|
|
return fu_firehose_updater_open (self->firehose_updater, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_firehose_close (FuMmDevice *self, GError **error)
|
|
{
|
|
g_autoptr(FuFirehoseUpdater) updater = NULL;
|
|
|
|
updater = g_steal_pointer (&self->firehose_updater);
|
|
return fu_firehose_updater_close (updater, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_firehose_write (FuMmDevice *self, XbSilo *rawprogram_silo,
|
|
GPtrArray *rawprogram_actions, GError **error)
|
|
{
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
guint progress_signal_id;
|
|
gboolean write_result;
|
|
|
|
locker = fu_device_locker_new_full (self,
|
|
(FuDeviceLockerFunc) fu_mm_device_firehose_open,
|
|
(FuDeviceLockerFunc) fu_mm_device_firehose_close,
|
|
error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
|
|
progress_signal_id = g_signal_connect_swapped (self->firehose_updater, "write-percentage",
|
|
G_CALLBACK (fu_device_set_progress), self);
|
|
|
|
write_result = fu_firehose_updater_write (self->firehose_updater,
|
|
rawprogram_silo,
|
|
rawprogram_actions,
|
|
error);
|
|
|
|
g_signal_handler_disconnect (self->firehose_updater, progress_signal_id);
|
|
|
|
return write_result;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_setup_firmware_dir (FuMmDevice *self, GError **error)
|
|
{
|
|
g_autofree gchar *cachedir = NULL;
|
|
g_autofree gchar *mm_fw_dir = NULL;
|
|
|
|
/* create a directory to store firmware files for modem-manager plugin */
|
|
cachedir = fu_common_get_path (FU_PATH_KIND_CACHEDIR_PKG);
|
|
mm_fw_dir = g_build_filename (cachedir, "modem-manager", "firmware", NULL);
|
|
if (g_mkdir_with_parents (mm_fw_dir, 0700) == -1) {
|
|
g_set_error (error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INTERNAL,
|
|
"Failed to create '%s': %s",
|
|
mm_fw_dir, g_strerror (errno));
|
|
return FALSE;
|
|
}
|
|
|
|
if (!fu_common_set_firmware_search_path (mm_fw_dir, error))
|
|
return FALSE;
|
|
|
|
self->firmware_path = g_steal_pointer (&mm_fw_dir);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_copy_firehose_prog (FuMmDevice *self, FuArchive *archive, GError **error)
|
|
{
|
|
g_autofree gchar *qcom_fw_dir = NULL;
|
|
g_autofree gchar *firehose_file_path = NULL;
|
|
g_autoptr(GBytes) firehose_prog = NULL;
|
|
|
|
/* lookup firehose-prog bootloader */
|
|
firehose_prog = fu_archive_lookup_by_fn (archive, "firehose-prog.mbn", error);
|
|
if (firehose_prog == NULL)
|
|
return FALSE;
|
|
|
|
qcom_fw_dir = g_build_filename (self->firmware_path, "qcom", NULL);
|
|
if (!fu_common_mkdir_parent (qcom_fw_dir, error))
|
|
return FALSE;
|
|
|
|
firehose_file_path = g_build_filename (qcom_fw_dir, "prog_firehose_sdx24.mbn", NULL);
|
|
|
|
if (!fu_common_set_contents_bytes (firehose_file_path, firehose_prog, error))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_prepare_firmware_search_path (FuMmDevice *self, GError **error)
|
|
{
|
|
self->restore_firmware_path = fu_common_get_firmware_search_path (NULL);
|
|
|
|
return fu_mm_setup_firmware_dir (self, error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_restore_firmware_search_path (FuMmDevice *self, GError **error)
|
|
{
|
|
if (self->restore_firmware_path != NULL && strlen (self->restore_firmware_path) > 0)
|
|
return fu_common_set_firmware_search_path (self->restore_firmware_path, error);
|
|
|
|
return fu_common_reset_firmware_search_path (error);
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_write_firmware_firehose (FuDevice *device, GBytes *fw, GError **error)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
|
|
GBytes *firehose_rawprogram;
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
g_autoptr(FuArchive) archive = NULL;
|
|
g_autoptr(XbSilo) firehose_rawprogram_silo = NULL;
|
|
g_autoptr(GPtrArray) firehose_rawprogram_actions = NULL;
|
|
|
|
/* decompress entire archive ahead of time */
|
|
archive = fu_archive_new (fw, FU_ARCHIVE_FLAG_IGNORE_PATH, error);
|
|
if (archive == NULL)
|
|
return FALSE;
|
|
|
|
/* modify firmware search path and restore it before function returns */
|
|
locker = fu_device_locker_new_full (self,
|
|
(FuDeviceLockerFunc) fu_mm_prepare_firmware_search_path,
|
|
(FuDeviceLockerFunc) fu_mm_restore_firmware_search_path,
|
|
error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
|
|
/* firehose modems that switch to the EDL mode over QCDM port require
|
|
* firehose binary to be present in the firmware-loader search path. */
|
|
if (!fu_mm_copy_firehose_prog (self, archive, error))
|
|
return FALSE;
|
|
|
|
/* flag as restart because the QCDM and MHI/BHI operations switch the
|
|
* device into FP execution environment */
|
|
fu_device_set_status (FU_DEVICE (self), FWUPD_STATUS_DEVICE_RESTART);
|
|
/* switch to embedded downloader (EDL) execution environment */
|
|
if (!fu_mm_device_qcdm_switch_to_edl (FU_DEVICE(self), error))
|
|
return FALSE;
|
|
|
|
/* lookup and validate firehose-rawprogram actions */
|
|
firehose_rawprogram = fu_archive_lookup_by_fn (archive, "firehose-rawprogram.xml", error);
|
|
if (firehose_rawprogram == NULL)
|
|
return FALSE;
|
|
if (!fu_firehose_validate_rawprogram (firehose_rawprogram, archive,
|
|
&firehose_rawprogram_silo,
|
|
&firehose_rawprogram_actions,
|
|
error)) {
|
|
g_prefix_error (error, "Invalid firehose rawprogram manifest: ");
|
|
return FALSE;
|
|
}
|
|
|
|
/* flag as write */
|
|
fu_device_set_status (FU_DEVICE (self), FWUPD_STATUS_DEVICE_WRITE);
|
|
|
|
/* download all files in the firehose-rawprogram manifest via Firehose */
|
|
if (!fu_mm_device_firehose_write (self,
|
|
firehose_rawprogram_silo,
|
|
firehose_rawprogram_actions,
|
|
error))
|
|
return FALSE;
|
|
|
|
/* flag as restart again, the module is switching to modem mode */
|
|
fu_device_set_status (FU_DEVICE (self), FWUPD_STATUS_DEVICE_RESTART);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
#endif /* MM_CHECK_VERSION(1,17,2) */
|
|
|
|
static gboolean
|
|
fu_mm_device_write_firmware (FuDevice *device,
|
|
FuFirmware *firmware,
|
|
FwupdInstallFlags flags,
|
|
GError **error)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
g_autoptr(GBytes) fw = NULL;
|
|
|
|
/* get default image */
|
|
fw = fu_firmware_get_bytes (firmware, error);
|
|
if (fw == NULL)
|
|
return FALSE;
|
|
|
|
/* lock device */
|
|
locker = fu_device_locker_new (device, error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
|
|
/* qmi pdc write operation */
|
|
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC)
|
|
return fu_mm_device_write_firmware_qmi_pdc (device, fw, &self->qmi_pdc_active_id, error);
|
|
|
|
#if MM_CHECK_VERSION(1,17,1) && MBIM_CHECK_VERSION(1,25,3)
|
|
/* mbim qdu write operation */
|
|
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_MBIM_QDU)
|
|
return fu_mm_device_write_firmware_mbim_qdu (device, fw, error);
|
|
|
|
#endif /* MM_CHECK_VERSION(1,17,1) && MBIM_CHECK_VERSION(1,25,3) */
|
|
#if MM_CHECK_VERSION(1,17,2)
|
|
/* firehose operation */
|
|
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FIREHOSE)
|
|
return fu_mm_device_write_firmware_firehose (device, fw, error);
|
|
#endif
|
|
|
|
g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED,
|
|
"unsupported update method");
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_attach_qmi_pdc (FuMmDevice *self, GError **error)
|
|
{
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
|
|
/* ignore action if there is no active id specified */
|
|
if (self->qmi_pdc_active_id == NULL)
|
|
return TRUE;
|
|
|
|
/* errors closing may be expected if the device really reboots itself */
|
|
locker = fu_device_locker_new_full (self,
|
|
(FuDeviceLockerFunc) fu_mm_device_qmi_open,
|
|
(FuDeviceLockerFunc) fu_mm_device_qmi_close_no_error,
|
|
error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
|
|
if (!fu_qmi_pdc_updater_activate (self->qmi_pdc_updater, self->qmi_pdc_active_id, error))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_attach_noop_idle (gpointer user_data)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (user_data);
|
|
self->attach_idle = 0;
|
|
g_signal_emit (self, signals [SIGNAL_ATTACH_FINISHED], 0);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_attach_qmi_pdc_idle (gpointer user_data)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (user_data);
|
|
g_autoptr(GError) error = NULL;
|
|
|
|
if (!fu_mm_device_attach_qmi_pdc (self, &error))
|
|
g_warning ("qmi-pdc attach operation failed: %s", error->message);
|
|
else
|
|
g_debug ("qmi-pdc attach operation successful");
|
|
|
|
self->attach_idle = 0;
|
|
g_signal_emit (self, signals [SIGNAL_ATTACH_FINISHED], 0);
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_mm_device_attach (FuDevice *device, GError **error)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (device);
|
|
g_autoptr(FuDeviceLocker) locker = NULL;
|
|
|
|
/* lock device */
|
|
locker = fu_device_locker_new (device, error);
|
|
if (locker == NULL)
|
|
return FALSE;
|
|
|
|
/* we want this attach operation to be triggered asynchronously, because the engine
|
|
* must learn that it has to wait for replug before we actually trigger the reset. */
|
|
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC)
|
|
self->attach_idle = g_idle_add ((GSourceFunc) fu_mm_device_attach_qmi_pdc_idle, self);
|
|
else
|
|
self->attach_idle = g_idle_add ((GSourceFunc) fu_mm_device_attach_noop_idle, self);
|
|
|
|
/* wait for re-probing after uninhibiting */
|
|
fu_device_set_remove_delay (device, FU_MM_DEVICE_REMOVE_DELAY_REPROBE);
|
|
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
fu_mm_device_init (FuMmDevice *self)
|
|
{
|
|
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_UPDATABLE);
|
|
fu_device_add_flag (FU_DEVICE (self), FWUPD_DEVICE_FLAG_USE_RUNTIME_VERSION);
|
|
fu_device_add_internal_flag (FU_DEVICE (self), FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID);
|
|
fu_device_set_version_format (FU_DEVICE (self), FWUPD_VERSION_FORMAT_PLAIN);
|
|
fu_device_set_summary (FU_DEVICE (self), "Mobile broadband device");
|
|
fu_device_add_icon (FU_DEVICE (self), "network-modem");
|
|
}
|
|
|
|
static void
|
|
fu_mm_device_finalize (GObject *object)
|
|
{
|
|
FuMmDevice *self = FU_MM_DEVICE (object);
|
|
if (self->attach_idle)
|
|
g_source_remove (self->attach_idle);
|
|
if (self->qmi_pdc_active_id)
|
|
g_array_unref (self->qmi_pdc_active_id);
|
|
g_object_unref (self->manager);
|
|
if (self->omodem != NULL)
|
|
g_object_unref (self->omodem);
|
|
g_free (self->detach_fastboot_at);
|
|
g_free (self->port_at);
|
|
g_free (self->port_qmi);
|
|
g_free (self->port_mbim);
|
|
g_free (self->port_qcdm);
|
|
g_free (self->inhibition_uid);
|
|
g_free (self->firmware_path);
|
|
g_free (self->restore_firmware_path);
|
|
G_OBJECT_CLASS (fu_mm_device_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
fu_mm_device_class_init (FuMmDeviceClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
FuDeviceClass *klass_device = FU_DEVICE_CLASS (klass);
|
|
object_class->finalize = fu_mm_device_finalize;
|
|
klass_device->to_string = fu_mm_device_to_string;
|
|
klass_device->probe = fu_mm_device_probe;
|
|
klass_device->detach = fu_mm_device_detach;
|
|
klass_device->write_firmware = fu_mm_device_write_firmware;
|
|
klass_device->attach = fu_mm_device_attach;
|
|
|
|
signals [SIGNAL_ATTACH_FINISHED] =
|
|
g_signal_new ("attach-finished",
|
|
G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
|
|
0, NULL, NULL, g_cclosure_marshal_VOID__VOID,
|
|
G_TYPE_NONE, 0);
|
|
}
|
|
|
|
FuMmDevice *
|
|
fu_mm_device_new (MMManager *manager, MMObject *omodem)
|
|
{
|
|
FuMmDevice *self = g_object_new (FU_TYPE_MM_DEVICE, NULL);
|
|
self->manager = g_object_ref (manager);
|
|
self->omodem = g_object_ref (omodem);
|
|
self->port_at_ifnum = -1;
|
|
self->port_qmi_ifnum = -1;
|
|
return self;
|
|
}
|
|
|
|
FuPluginMmInhibitedDeviceInfo *
|
|
fu_plugin_mm_inhibited_device_info_new (FuMmDevice *device)
|
|
{
|
|
FuPluginMmInhibitedDeviceInfo *info;
|
|
|
|
info = g_new0 (FuPluginMmInhibitedDeviceInfo, 1);
|
|
info->physical_id = g_strdup (fu_device_get_physical_id (FU_DEVICE (device)));
|
|
info->vendor = g_strdup (fu_device_get_vendor (FU_DEVICE (device)));
|
|
info->name = g_strdup (fu_device_get_name (FU_DEVICE (device)));
|
|
info->version = g_strdup (fu_device_get_version (FU_DEVICE (device)));
|
|
info->guids = fu_device_get_guids (FU_DEVICE (device));
|
|
info->update_methods = fu_mm_device_get_update_methods (device);
|
|
info->detach_fastboot_at = g_strdup (fu_mm_device_get_detach_fastboot_at (device));
|
|
info->port_at_ifnum = fu_mm_device_get_port_at_ifnum (device);
|
|
info->port_qmi_ifnum = fu_mm_device_get_port_qmi_ifnum (device);
|
|
info->inhibited_uid = g_strdup (fu_mm_device_get_inhibition_uid (device));
|
|
|
|
return info;
|
|
}
|
|
|
|
void
|
|
fu_plugin_mm_inhibited_device_info_free (FuPluginMmInhibitedDeviceInfo *info)
|
|
{
|
|
g_free (info->inhibited_uid);
|
|
g_free (info->physical_id);
|
|
g_free (info->vendor);
|
|
g_free (info->name);
|
|
g_free (info->version);
|
|
if (info->guids)
|
|
g_ptr_array_unref (info->guids);
|
|
g_free (info->detach_fastboot_at);
|
|
g_free (info);
|
|
}
|
|
|
|
FuMmDevice *
|
|
fu_mm_device_udev_new (MMManager *manager,
|
|
FuPluginMmInhibitedDeviceInfo *info)
|
|
{
|
|
FuMmDevice *self = g_object_new (FU_TYPE_MM_DEVICE, NULL);
|
|
g_debug ("creating udev-based mm device at %s", info->physical_id);
|
|
self->manager = g_object_ref (manager);
|
|
fu_device_set_physical_id (FU_DEVICE (self), info->physical_id);
|
|
fu_device_set_vendor (FU_DEVICE (self), info->vendor);
|
|
fu_device_set_name (FU_DEVICE (self), info->name);
|
|
fu_device_set_version (FU_DEVICE (self), info->version);
|
|
self->update_methods = info->update_methods;
|
|
self->detach_fastboot_at = g_strdup (info->detach_fastboot_at);
|
|
self->port_at_ifnum = info->port_at_ifnum;
|
|
self->port_qmi_ifnum = info->port_qmi_ifnum;
|
|
|
|
for (guint i = 0; i < info->guids->len; i++)
|
|
fu_device_add_guid (FU_DEVICE (self), g_ptr_array_index (info->guids, i));
|
|
|
|
return self;
|
|
}
|
|
|
|
void
|
|
fu_mm_device_udev_add_port (FuMmDevice *self,
|
|
const gchar *subsystem,
|
|
const gchar *path,
|
|
gint ifnum)
|
|
{
|
|
g_return_if_fail (FU_IS_MM_DEVICE (self));
|
|
|
|
if (g_str_equal (subsystem, "usbmisc") &&
|
|
self->port_qmi == NULL &&
|
|
ifnum >= 0 &&
|
|
ifnum == self->port_qmi_ifnum) {
|
|
g_debug ("added QMI port %s (%s)", path, subsystem);
|
|
self->port_qmi = g_strdup (path);
|
|
return;
|
|
}
|
|
|
|
if (g_str_equal (subsystem, "tty") &&
|
|
self->port_at == NULL &&
|
|
ifnum >= 0 &&
|
|
ifnum == self->port_at_ifnum) {
|
|
g_debug ("added AT port %s (%s)", path, subsystem);
|
|
self->port_at = g_strdup (path);
|
|
return;
|
|
}
|
|
|
|
/* otherwise, ignore all other ports */
|
|
g_debug ("ignoring port %s (%s)", path, subsystem);
|
|
}
|