/*
 * Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: LGPL-2.1+
 */

#include "config.h"

#include <fwupdplugin.h>

#include <errno.h>
#include <fcntl.h>
#include <glib/gstdio.h>
#include <string.h>

#include "fu-mbim-qdu-updater.h"
#include "fu-mm-device.h"
#include "fu-mm-utils.h"
#include "fu-qmi-pdc-updater.h"

#if MM_CHECK_VERSION(1, 17, 2)
#include "fu-firehose-updater.h"
#endif

#include "fu-sahara-loader.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 */

#define FU_MM_DEVICE_AT_RETRIES 3
#define FU_MM_DEVICE_AT_DELAY	3000 /* ms */

/* Amount of time for the modem to get firmware version */
#define MAX_WAIT_TIME_SECS 150 /* s */

/**
 * FU_MM_DEVICE_FLAG_DETACH_AT_FASTBOOT_HAS_NO_RESPONSE
 *
 * If no AT response is expected when entering fastboot mode.
 */
#define FU_MM_DEVICE_FLAG_DETACH_AT_FASTBOOT_HAS_NO_RESPONSE (1 << 0)

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;
	gchar *branch_at;
	gint port_at_ifnum;
	gint port_qmi_ifnum;
	gint port_mbim_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;
	gchar *firehose_prog_file;
	FuSaharaLoader *sahara_loader;
#if MM_CHECK_VERSION(1, 17, 2)
	FuFirehoseUpdater *firehose_updater;
#endif

	/* for sahara */
	FuUsbDevice *usb_device;

	/* 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_string_append(str, idt, "AtPort", self->port_at);
	if (self->port_qmi != NULL)
		fu_string_append(str, idt, "QmiPort", self->port_qmi);
	if (self->port_mbim != NULL)
		fu_string_append(str, idt, "MbimPort", self->port_mbim);
	if (self->port_qcdm != NULL)
		fu_string_append(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;
}

gint
fu_mm_device_get_port_mbim_ifnum(FuMmDevice *device)
{
	g_return_val_if_fail(FU_IS_MM_DEVICE(device), -1);
	return device->port_mbim_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
#if MM_CHECK_VERSION(1, 19, 1)
		MM_MODEM_FIRMWARE_UPDATE_METHOD_FIREHOSE | MM_MODEM_FIRMWARE_UPDATE_METHOD_SAHARA,
#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_info("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);
			else if (ports[i].type == MM_MODEM_PORT_TYPE_MBIM)
				self->port_mbim = g_strdup_printf("/dev/%s", ports[i].name);
			/* to read secboot status */
			else if (ports[i].type == MM_MODEM_PORT_TYPE_AT)
				self->port_at = g_strdup_printf("/dev/%s", ports[i].name);
		}
		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 or mbim port is required for firehose */
	if ((self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_FIREHOSE) &&
	    (self->port_qcdm == NULL && self->port_mbim == 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,
					  &self->port_mbim_ifnum,
					  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);
			}
		}
	}

	/* fix up vendor name */
	if (g_strcmp0(fu_device_get_vendor(device), "QUALCOMM INCORPORATED") == 0)
		fu_device_set_vendor(device, "Qualcomm");

	/* success */
	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);
	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)
{
	/* sanity check */
	if (self->port_qcdm == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no QCDM port provided for filename");
		return FALSE;
	}

	/* 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);
	fu_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;
	}
	fu_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) */

typedef struct {
	const gchar *cmd;
	gboolean has_response;
} FuMmDeviceAtCmdHelper;

static gboolean
fu_mm_device_at_cmd_cb(FuDevice *device, gpointer user_data, GError **error)
{
	FuMmDevice *self = FU_MM_DEVICE(device);
	FuMmDeviceAtCmdHelper *helper = (FuMmDeviceAtCmdHelper *)user_data;
	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", helper->cmd);

	/* command */
	at_req = g_bytes_new(cmd_cr, strlen(cmd_cr));
	fu_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: ", helper->cmd);
		return FALSE;
	}

	/* AT command has no response, return TRUE */
	if (!helper->has_response) {
		g_debug("no response expected for AT command: '%s', assuming succeed", helper->cmd);
		return TRUE;
	}

	/* 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: ", helper->cmd);
		return FALSE;
	}
	fu_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",
			    helper->cmd);
		return FALSE;
	}

	/* return error if AT command failed */
	if (g_strrstr(buf, "\r\nOK\r\n") == NULL) {
		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",
			    helper->cmd,
			    tmp);
		return FALSE;
	}

	/* set firmware branch if returned */
	if (self->branch_at != NULL && g_strcmp0(helper->cmd, self->branch_at) == 0) {
		/*
		 * example AT+GETFWBRANCH response:
		 *
		 * \r\nFOSS-002 \r\n\r\nOK\r\n
		 *
		 * remove \r\n, and OK to get branch name
		 */
		g_auto(GStrv) parts = g_strsplit(buf, "\r\n", -1);

		for (int j = 0; parts[j] != NULL; j++) {
			/* Ignore empty strings, and OK responses */
			if (g_strcmp0(parts[j], "") != 0 && g_strcmp0(parts[j], "OK") != 0) {
				/* Set branch */
				fu_device_set_branch(FU_DEVICE(self), parts[j]);
				g_info("firmware branch reported as '%s'", parts[j]);
				break;
			}
		}
	}

	if (g_strcmp0(helper->cmd, "AT+QSECBOOT=\"status\"") == 0) {
		/*
		 * example AT+QSECBOOT="status" response:
		 *
		 * \r\n+QSECBOOT: "STATUS",1\r\n\r\nOK\r\n
		 *
		 * Secure boot status:
		 * 1 - enabled
		 * 0 - disabled
		 */
		g_auto(GStrv) parts = g_strsplit(buf, "\r\n", -1);

		for (int j = 0; parts[j] != NULL; j++) {
			if (g_strcmp0(parts[j], "+QSECBOOT: \"status\",1") == 0) {
				fu_device_add_flag(FU_DEVICE(self),
						   FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD);
				break;
			}
			if (g_strcmp0(parts[j], "+QSECBOOT: \"status\",0") == 0) {
				fu_device_add_flag(FU_DEVICE(self),
						   FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
				break;
			}
		}
	}

	if (g_strcmp0(helper->cmd, "AT+QCFG=\"secbootstat\"") == 0) {
		/*
		 * example AT+QSECBOOT="status" response:
		 *
		 * \r\n+QSECBOOT: "STATUS",1\r\n\r\nOK\r\n
		 *
		 * Secure boot status:
		 * 1 - enabled
		 * 0 - disabled
		 */
		g_auto(GStrv) parts = g_strsplit(buf, "\r\n", -1);

		for (int j = 0; parts[j] != NULL; j++) {
			if (g_strcmp0(parts[j], "+QCFG: \"secbootstat\",1") == 0) {
				fu_device_add_flag(FU_DEVICE(self),
						   FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD);
				break;
			}
			if (g_strcmp0(parts[j], "+QCFG: \"secbootstat\",0") == 0) {
				fu_device_add_flag(FU_DEVICE(self),
						   FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
				break;
			}
		}
	}

	/* success */
	return TRUE;
}

static gboolean
fu_mm_device_at_cmd(FuMmDevice *self, const gchar *cmd, gboolean has_response, GError **error)
{
	FuMmDeviceAtCmdHelper helper = {.cmd = cmd, .has_response = has_response};
	return fu_device_retry_full(FU_DEVICE(self),
				    fu_mm_device_at_cmd_cb,
				    FU_MM_DEVICE_AT_RETRIES,
				    FU_MM_DEVICE_AT_DELAY,
				    &helper,
				    error);
}

static gboolean
fu_mm_device_io_open(FuMmDevice *self, GError **error)
{
	/* sanity check */
	if (self->port_at == NULL) {
		g_set_error_literal(error,
				    FWUPD_ERROR,
				    FWUPD_ERROR_NOT_SUPPORTED,
				    "no AT port provided for filename");
		return FALSE;
	}

	/* 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;
	gboolean has_response = TRUE;

	/* boot to fastboot mode */
	locker = fu_device_locker_new_full(device,
					   (FuDeviceLockerFunc)fu_mm_device_io_open,
					   (FuDeviceLockerFunc)fu_mm_device_io_close,
					   error);

	/* expect response for fastboot AT command */
	if (fu_device_has_private_flag(FU_DEVICE(self),
				       FU_MM_DEVICE_FLAG_DETACH_AT_FASTBOOT_HAS_NO_RESPONSE)) {
		has_response = FALSE;
	}

	if (locker == NULL)
		return FALSE;
	if (!fu_mm_device_at_cmd(self, "AT", TRUE, error))
		return FALSE;
	if (!fu_mm_device_at_cmd(self, self->detach_fastboot_at, has_response, 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, FuProgress *progress, 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,
				     FuProgress *progress,
				     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(GArray) digest = 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;
	}
	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_progress_set_status(progress, FWUPD_STATUS_DEVICE_WRITE);
	digest = fu_mbim_qdu_updater_write(self->mbim_qdu_updater,
					   filename,
					   data,
					   device,
					   progress,
					   error);
	if (digest == NULL)
		return FALSE;
	if (!fu_device_locker_close(locker, error))
		return FALSE;

	fu_progress_set_status(progress, 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_find_device_file(FuDevice *device, gpointer userdata, GError **error)
{
	FuMmDevice *self = FU_MM_DEVICE(device);
	const gchar *subsystem = (const gchar *)userdata;

	return fu_mm_utils_find_device_file(fu_device_get_physical_id(FU_DEVICE(self)),
					    subsystem,
					    &self->port_edl,
					    error);
}

static gboolean
fu_mm_device_find_edl_port(FuDevice *device, const gchar *subsystem, GError **error)
{
	FuMmDevice *self = FU_MM_DEVICE(device);

	g_clear_pointer(&self->port_edl, g_free);

	return fu_device_retry_full(device,
				    fu_mm_find_device_file,
				    30,
				    250,
				    (gpointer)subsystem,
				    error);
}

static gboolean
fu_mm_device_qcdm_switch_to_edl(FuDevice *device, gpointer userdata, GError **error)
{
	FuMmDevice *self = FU_MM_DEVICE(device);
	g_autoptr(GError) error_local = NULL;
	g_autoptr(FuDeviceLocker) locker = NULL;
	static const guint8 emergency_download[] = {0x4b, 0x65, 0x01, 0x00, 0x54, 0x0f, 0x7e};

	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 (g_error_matches(error_local, FWUPD_ERROR, FWUPD_ERROR_INVALID_FILE)) {
			return fu_mm_device_find_edl_port(device, "wwan", error);
		}
		g_propagate_error(error, g_steal_pointer(&error_local));
		return FALSE;
	}

	if (!fu_mm_device_qcdm_cmd(self,
				   emergency_download,
				   G_N_ELEMENTS(emergency_download),
				   error))
		return FALSE;

	g_set_error(error,
		    FWUPD_ERROR,
		    FWUPD_ERROR_NOT_FOUND,
		    "Device haven't switched to EDL yet");
	return FALSE;
}

#if MBIM_CHECK_VERSION(1, 27, 5)
static void
fu_mm_device_switch_to_edl_mbim_ready(MbimDevice *device, GAsyncResult *res, GMainLoop *loop)
{
	/* No need to check for a response since MBIM
	 * port goes away without sending one */

	g_main_loop_quit(loop);
}

static gboolean
fu_mm_device_mbim_switch_to_edl(FuDevice *device, GError **error)
{
	FuMmDevice *self = FU_MM_DEVICE(device);
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(MbimMessage) message = NULL;
	g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, 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;

	message = mbim_message_qdu_quectel_reboot_set_new(MBIM_QDU_QUECTEL_REBOOT_TYPE_EDL, NULL);
	mbim_device_command(fu_mbim_qdu_updater_get_mbim_device(self->mbim_qdu_updater),
			    message,
			    5,
			    NULL,
			    (GAsyncReadyCallback)fu_mm_device_switch_to_edl_mbim_ready,
			    mainloop);

	g_main_loop_run(mainloop);

	return TRUE;
}
#endif // MBIM_CHECK_VERSION(1, 27, 5)

static gboolean
fu_mm_device_firehose_open(FuMmDevice *self, GError **error)
{
	self->firehose_updater = fu_firehose_updater_new(self->port_edl, self->sahara_loader);
	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,
			    FuProgress *progress,
			    GError **error)
{
	g_autoptr(FuDeviceLocker) locker = NULL;
	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;
	return fu_firehose_updater_write(self->firehose_updater,
					 rawprogram_silo,
					 rawprogram_actions,
					 progress,
					 error);
}

#if MM_CHECK_VERSION(1, 19, 1) && MBIM_CHECK_VERSION(1, 25, 7)
static gboolean
fu_mm_device_sahara_open(FuMmDevice *self, GError **error)
{
	self->sahara_loader = fu_sahara_loader_new();
	return fu_sahara_loader_open(self->sahara_loader, self->usb_device, error);
}

static gboolean
fu_mm_device_sahara_close(FuMmDevice *self, GError **error)
{
	g_autoptr(FuSaharaLoader) loader = NULL;

	loader = g_steal_pointer(&self->sahara_loader);
	return fu_sahara_loader_close(loader, error);
}
#endif // MM_CHECK_VERSION(1, 19, 1) && MBIM_CHECK_VERSION(1, 25, 7)

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_path_from_kind(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_kernel_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, GBytes *prog, GError **error)
{
	g_autofree gchar *qcom_fw_dir = NULL;
	g_autofree gchar *firehose_file_path = NULL;

	if (self->firehose_prog_file == NULL) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_FOUND,
			    "Firehose prog filename is not set for the device");
		return FALSE;
	}

	qcom_fw_dir = g_build_filename(self->firmware_path, "qcom", NULL);
	if (!fu_path_mkdir_parent(qcom_fw_dir, error))
		return FALSE;

	firehose_file_path = g_build_filename(qcom_fw_dir, self->firehose_prog_file, NULL);

	if (!fu_bytes_set_contents(firehose_file_path, prog, error))
		return FALSE;

	return TRUE;
}

static gboolean
fu_mm_prepare_firmware_search_path(FuMmDevice *self, GError **error)
{
	self->restore_firmware_path = fu_kernel_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_kernel_set_firmware_search_path(self->restore_firmware_path, error);

	return fu_kernel_reset_firmware_search_path(error);
}

static gboolean
fu_mm_device_write_firmware_firehose(FuDevice *device,
				     GBytes *fw,
				     FuProgress *progress,
				     GError **error)
{
	FuMmDevice *self = FU_MM_DEVICE(device);
	MMModem *modem = mm_object_peek_modem(self->omodem);
	GBytes *firehose_rawprogram;
	GBytes *firehose_prog;
	g_autoptr(FuDeviceLocker) locker = NULL;
	g_autoptr(FuArchive) archive = NULL;
	g_autoptr(XbSilo) firehose_rawprogram_silo = NULL;
	g_autoptr(GPtrArray) firehose_rawprogram_actions = NULL;

	/* progress */
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED);
	fu_progress_add_step(progress, FWUPD_STATUS_DECOMPRESSING, 1, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 10, NULL);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 90, NULL);

	/* decompress entire archive ahead of time */
	archive = fu_archive_new(fw, FU_ARCHIVE_FLAG_IGNORE_PATH, error);
	if (archive == NULL)
		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;
	}

	/* lookup firehose-prog bootloader */
	firehose_prog = fu_archive_lookup_by_fn(archive, "firehose-prog.mbn", error);
	if (firehose_prog == NULL)
		return FALSE;
	fu_progress_step_done(progress);

	/* Firehose program needs to be loaded to the modem before firehose update process can
	 * start. Generally, modems use Sahara protocol to load the firehose binary.
	 *
	 * In case of MHI PCI modems, the mhi-pci-generic driver reads the firehose binary from the
	 * firmware-loader and writes it to the modem.
	 **/
	if (g_strv_contains(mm_modem_get_drivers(modem), "mhi-pci-generic") &&
	    self->port_qcdm != NULL) {
		/* 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 use mhi_pci drivers require firehose binary
		 * to be present in the firmware-loader search path. */
		if (!fu_mm_copy_firehose_prog(self, firehose_prog, error))
			return FALSE;
		/* trigger emergency download mode, up to 30s retrying until the QCDM
		 * port goes away; this takes us to the EDL (embedded downloader) execution
		 * environment */
		if (!fu_device_retry_full(FU_DEVICE(self),
					  fu_mm_device_qcdm_switch_to_edl,
					  30,
					  1000,
					  NULL,
					  error))
			return FALSE;

		g_debug("found edl port: %s", self->port_edl);
	}
#if MM_CHECK_VERSION(1, 19, 1) && MBIM_CHECK_VERSION(1, 25, 7)
	else if ((FU_MM_DEVICE(self)->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_SAHARA) &&
		 self->port_mbim != NULL) {
		/* switch to emergency download (EDL) execution environment */
		if (!fu_mm_device_mbim_switch_to_edl(device, error))
			return FALSE;

		locker = fu_device_locker_new_full(self,
						   (FuDeviceLockerFunc)fu_mm_device_sahara_open,
						   (FuDeviceLockerFunc)fu_mm_device_sahara_close,
						   error);
		if (locker == NULL)
			return FALSE;

		/* use sahara port to load firehose binary */
		if (!fu_sahara_loader_run(self->sahara_loader, firehose_prog, error))
			return FALSE;
	}
#endif // MM_CHECK_VERSION(1, 19, 1) && MBIM_CHECK_VERSION(1, 25, 7)
	else {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "suitable port not found");
		return FALSE;
	}
	fu_progress_step_done(progress);

	/* download all files in the firehose-rawprogram manifest via Firehose */
	if (!fu_mm_device_firehose_write(self,
					 firehose_rawprogram_silo,
					 firehose_rawprogram_actions,
					 fu_progress_get_child(progress),
					 error))
		return FALSE;
	fu_progress_step_done(progress);

	/* flag as restart again, the module is switching to modem mode */
	fu_progress_set_status(progress, FWUPD_STATUS_DEVICE_RESTART);
	return TRUE;
}

#endif /* MM_CHECK_VERSION(1,17,2) */

static gboolean
fu_mm_device_write_firmware(FuDevice *device,
			    FuFirmware *firmware,
			    FuProgress *progress,
			    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, progress, 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, progress, error);
#endif

	g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "unsupported update method");
	return FALSE;
}

static gboolean
fu_mm_device_set_quirk_kv(FuDevice *device, const gchar *key, const gchar *value, GError **error)
{
	FuMmDevice *self = FU_MM_DEVICE(device);

	/* load from quirks */
	if (g_strcmp0(key, "ModemManagerBranchAtCommand") == 0) {
		self->branch_at = g_strdup(value);
		return TRUE;
	}

	if (g_strcmp0(key, "ModemManagerFirehoseProgFile") == 0) {
		self->firehose_prog_file = g_strdup(value);
		return TRUE;
	}

	/* failed */
	g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "quirk key not supported");
	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, FuProgress *progress, 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 gboolean
fu_mm_device_setup_branch_at(FuMmDevice *self, GError **error)
{
	g_autoptr(FuDeviceLocker) locker = NULL;

	/* nothing to do if there is no AT port available or
	 * ModemManagerBranchAtCommand quirk is not set */
	if (self->port_at == NULL || self->branch_at == NULL)
		return TRUE;

	if (fu_device_has_flag(self, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD)) {
		g_set_error(error,
			    FWUPD_ERROR,
			    FWUPD_ERROR_NOT_SUPPORTED,
			    "Firmware branches are not supported if the devices is signed");
		return FALSE;
	}

	/* Create IO channel to send AT commands to the modem */
	locker = fu_device_locker_new_full(self,
					   (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, self->branch_at, TRUE, error))
		return FALSE;

	if (fu_device_get_branch(self) != NULL)
		g_info("using firmware branch: %s", fu_device_get_branch(self));
	else
		g_info("using firmware branch: default");

	return TRUE;
}

static void
fu_mm_device_setup_secboot_status_quectel(FuMmDevice *self)
{
	const gchar *version = fu_device_get_version(FU_DEVICE(self));
	g_autofree gchar *name = NULL;

	const gchar *at_cmd[] = {"AT+QSECBOOT=\"status\"", "AT+QCFG=\"secbootstat\"", NULL};

	struct {
		const gchar *name;
		const gchar *version;
	} secboot[] = {{"EM05GF", "EM05GFAR07A07M1G_01.005.01.005"},
		       {"EM05CE", "EM05CEFCR08A16M1G_LNV"},
		       {NULL, NULL}};

	if (self->port_at != NULL) {
		g_autoptr(FuDeviceLocker) locker = NULL;
		g_autoptr(GError) error_local = NULL;

		/* Create IO channel to send AT commands to the modem */
		locker = fu_device_locker_new_full(self,
						   (FuDeviceLockerFunc)fu_mm_device_io_open,
						   (FuDeviceLockerFunc)fu_mm_device_io_close,
						   &error_local);
		if (locker == NULL) {
			g_debug("failed to open AT port: %s", error_local->message);
			return;
		}

		/* try to query sec boot status with AT commands */
		for (guint i = 0; at_cmd[i] != NULL; i++) {
			g_autoptr(GError) error_loop = NULL;
			if (!fu_mm_device_at_cmd(self, at_cmd[i], TRUE, &error_loop)) {
				g_debug("AT command failed (%s): %s",
					at_cmd[i],
					error_loop->message);
			} else {
				return;
			}
		}
	}

	/* find model name and compare with table from Quectel */
	if (version == NULL)
		return;
	name = g_strndup(version, 6);
	for (guint i = 0; secboot[i].name != NULL; i++) {
		if (g_strcmp0(name, secboot[i].name) == 0) {
			if (fu_version_compare(version,
					       secboot[i].version,
					       FWUPD_VERSION_FORMAT_PLAIN) >= 0) {
				fu_device_add_flag(FU_DEVICE(self),
						   FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD);
			} else {
				fu_device_add_flag(FU_DEVICE(self),
						   FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
			}
			return;
		}
	}
}

static void
fu_mm_device_setup_secboot_status(FuDevice *device)
{
	FuMmDevice *self = FU_MM_DEVICE(device);

	if (fu_device_has_vendor_id(device, "USB:0x2C7C") ||
	    fu_device_has_vendor_id(device, "PCI:0x1EAC"))
		fu_mm_device_setup_secboot_status_quectel(self);
}

static gboolean
fu_mm_device_setup(FuDevice *device, GError **error)
{
	FuMmDevice *self = FU_MM_DEVICE(device);
	g_autoptr(GError) error_local = NULL;

	fu_mm_device_setup_secboot_status(device);

	if (!fu_mm_device_setup_branch_at(self, &error_local))
		g_warning("Failed to set firmware branch: %s", error_local->message);

	return TRUE;
}

static void
fu_mm_device_incorporate(FuDevice *device, FuDevice *donor_device)
{
	FuMmDevice *self = FU_MM_DEVICE(device);
	FuMmDevice *donor = FU_MM_DEVICE(donor_device);

	self->update_methods = fu_mm_device_get_update_methods(donor);
	self->detach_fastboot_at = g_strdup(fu_mm_device_get_detach_fastboot_at(donor));
	self->inhibition_uid = g_strdup(fu_mm_device_get_inhibition_uid(donor));
	self->port_at_ifnum = fu_mm_device_get_port_at_ifnum(donor);
	self->port_qmi_ifnum = fu_mm_device_get_port_qmi_ifnum(donor);
	self->port_mbim_ifnum = fu_mm_device_get_port_mbim_ifnum(donor);
	g_set_object(&self->manager, donor->manager);
}

static void
fu_mm_device_set_progress(FuDevice *self, FuProgress *progress)
{
	fu_progress_set_id(progress, G_STRLOC);
	fu_progress_add_flag(progress, FU_PROGRESS_FLAG_GUESSED);
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "detach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_WRITE, 94, "write");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_RESTART, 2, "attach");
	fu_progress_add_step(progress, FWUPD_STATUS_DEVICE_BUSY, 2, "reload");
}

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_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_REQUIRE_AC);
	fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_REPLUG_MATCH_GUID);
	fu_device_add_internal_flag(FU_DEVICE(self), FU_DEVICE_INTERNAL_FLAG_MD_SET_VERFMT);
	fu_device_add_flag(FU_DEVICE(self), FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD);
	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), "modem");
	fu_device_register_private_flag(FU_DEVICE(self),
					FU_MM_DEVICE_FLAG_DETACH_AT_FASTBOOT_HAS_NO_RESPONSE,
					"detach-at-fastboot-has-no-response");
}

static void
fu_mm_device_finalize(GObject *object)
{
	FuMmDevice *self = FU_MM_DEVICE(object);
	if (self->usb_device != NULL)
		g_object_unref(self->usb_device);
	if (self->attach_idle)
		g_source_remove(self->attach_idle);
	if (self->qmi_pdc_active_id)
		g_array_unref(self->qmi_pdc_active_id);
	if (self->manager != NULL)
		g_object_unref(self->manager);
	if (self->omodem != NULL)
		g_object_unref(self->omodem);
	g_free(self->detach_fastboot_at);
	g_free(self->branch_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_free(self->firehose_prog_file);
	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->setup = fu_mm_device_setup;
	klass_device->reload = fu_mm_device_setup;
	klass_device->to_string = fu_mm_device_to_string;
	klass_device->set_quirk_kv = fu_mm_device_set_quirk_kv;
	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;
	klass_device->set_progress = fu_mm_device_set_progress;
	klass_device->incorporate = fu_mm_device_incorporate;

	/**
	 * FuMmDevice::attach-finished:
	 * @self: the #FuMmDevice instance that emitted the signal
	 *
	 * The ::attach-finished signal is emitted when the device has attached.
	 **/
	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(FuContext *ctx, MMManager *manager, MMObject *omodem)
{
	FuMmDevice *self = g_object_new(FU_TYPE_MM_DEVICE, "context", ctx, NULL);
	self->manager = g_object_ref(manager);
	self->omodem = g_object_ref(omodem);
	self->port_at_ifnum = -1;
	self->port_qmi_ifnum = -1;
	self->port_mbim_ifnum = -1;
	return self;
}

FuMmDevice *
fu_mm_shadow_device_new(FuMmDevice *device)
{
	FuMmDevice *shadow_device = NULL;
	shadow_device = g_object_new(FU_TYPE_MM_DEVICE,
				     "context",
				     fu_device_get_context(FU_DEVICE(device)),
				     NULL);
	fu_device_incorporate(FU_DEVICE(shadow_device), FU_DEVICE(device));
	return shadow_device;
}

FuUsbDevice *
fu_mm_device_get_usb_device(FuMmDevice *self)
{
	g_return_val_if_fail(FU_IS_MM_DEVICE(self), NULL);
	return self->usb_device;
}

void
fu_mm_device_set_usb_device(FuMmDevice *self, FuUsbDevice *usb_device)
{
	g_return_if_fail(FU_IS_MM_DEVICE(self));
	g_return_if_fail(FU_IS_USB_DEVICE(usb_device));
	g_set_object(&self->usb_device, usb_device);
}

FuMmDevice *
fu_mm_device_udev_new(FuContext *ctx, MMManager *manager, FuMmDevice *shadow_device)
{
	FuMmDevice *self = g_object_new(FU_TYPE_MM_DEVICE, "context", ctx, NULL);
	g_debug("creating udev-based mm device at %s",
		fu_device_get_physical_id(FU_DEVICE(shadow_device)));
	fu_device_incorporate(FU_DEVICE(self), FU_DEVICE(shadow_device));
	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);
}
