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