mirror of
				https://git.proxmox.com/git/fwupd
				synced 2025-10-31 19:18:47 +00:00 
			
		
		
		
	 f50ff2c27e
			
		
	
	
		f50ff2c27e
		
	
	
	
	
		
			
			If we say that the version format should be the same for the `version_lowest` and the `version_bootloader` then it does not always make sense to set it at the same time. Moving the `version_format` to a standalone first-class property also means it can be typically be set in the custom device `_init()` function, which means we don't need to worry about *changing* ther version format as set by the USB and UDev superclass helpers.
		
			
				
	
	
		
			846 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			846 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
 | |
|  *
 | |
|  * SPDX-License-Identifier: LGPL-2.1+
 | |
|  */
 | |
| 
 | |
| #include "config.h"
 | |
| 
 | |
| #include <string.h>
 | |
| 
 | |
| #include "fu-io-channel.h"
 | |
| #include "fu-archive.h"
 | |
| #include "fu-mm-device.h"
 | |
| #include "fu-device-private.h"
 | |
| #include "fu-mm-utils.h"
 | |
| #include "fu-qmi-pdc-updater.h"
 | |
| 
 | |
| /* 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 */
 | |
| 
 | |
| 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;
 | |
| 
 | |
| 	/* 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;
 | |
| };
 | |
| 
 | |
| 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);
 | |
| }
 | |
| 
 | |
| 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;
 | |
| }
 | |
| 
 | |
| 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;
 | |
| 	g_autoptr(MMFirmwareUpdateSettings) update_settings = NULL;
 | |
| 	g_autofree gchar *device_sysfs_path = 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;
 | |
| 	}
 | |
| 
 | |
| 	/* 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;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	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;
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	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 we have the at port reported, get sysfs path and interface number */
 | |
| 	if (self->port_at != NULL) {
 | |
| 		fu_mm_utils_get_port_info (self->port_at, &device_sysfs_path, &self->port_at_ifnum, NULL);
 | |
| 	} else if (self->port_qmi != NULL) {
 | |
| 		fu_mm_utils_get_port_info (self->port_qmi, &device_sysfs_path, NULL, NULL);
 | |
| 	} else {
 | |
| 		g_warn_if_reached ();
 | |
| 	}
 | |
| 
 | |
| 	/* if no device sysfs file, error out */
 | |
| 	if (device_sysfs_path == NULL) {
 | |
| 		g_set_error_literal (error,
 | |
| 				     FWUPD_ERROR,
 | |
| 				     FWUPD_ERROR_NOT_SUPPORTED,
 | |
| 				     "failed to find device sysfs path");
 | |
| 		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]);
 | |
| 
 | |
| 	/* convert the instance IDs to GUIDs */
 | |
| 	fu_device_convert_instance_ids (device);
 | |
| 
 | |
| 	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);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 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 (!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;
 | |
| }
 | |
| 
 | |
| 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 currently two methods to download firmware:
 | |
| 	 * fastboot and qmi-pdc. A modem may require one of those, or both,
 | |
| 	 * 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 only support qmi-pdc, we just exit without any detach.
 | |
| 	 *  c) 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.
 | |
| 	 *
 | |
| 	 * If the FuMmModem is created from udev events...
 | |
| 	 *  d) 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;
 | |
| 	gsize		 total_written;
 | |
| 	gsize		 total_bytes;
 | |
| } 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);
 | |
| 	ctx->total_bytes += g_bytes_get_size (file_info->bytes);
 | |
| 	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,
 | |
| 		.total_written = 0,
 | |
| 		.total_bytes = 0,
 | |
| 	};
 | |
| 
 | |
| 	/* 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;
 | |
| }
 | |
| 
 | |
| 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(FuArchive) archive = NULL;
 | |
| 	g_autoptr(GBytes) fw = NULL;
 | |
| 	g_autoptr(GPtrArray) array = NULL;
 | |
| 
 | |
| 	/* get default image */
 | |
| 	fw = fu_firmware_get_image_default_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);
 | |
| 
 | |
| 	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_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->inhibition_uid);
 | |
| 	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;
 | |
| 	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->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;
 | |
| 
 | |
| 	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));
 | |
| 
 | |
| 	/* cdc-wdm ports always added unless one already set */
 | |
| 	if (g_str_equal (subsystem, "usbmisc") &&
 | |
| 	    (self->port_qmi == NULL)) {
 | |
| 		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);
 | |
| }
 |