/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * qmi-firmware-update -- Command line tool to update firmware in QMI devices * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Copyright (C) 2016-2017 Zodiac Inflight Innovation * Copyright (C) 2016-2017 Aleksander Morgado */ #include "config.h" #include #include #include #include #include #include #include "qfu-log.h" #include "qfu-operation.h" #include "qfu-device-selection.h" #include "qfu-udev-helpers.h" #include "qfu-utils.h" #define PROGRAM_NAME "qmi-firmware-update" #define PROGRAM_VERSION PACKAGE_VERSION /*****************************************************************************/ /* Options */ /* Generic device selections */ #if defined WITH_UDEV static guint busnum; static guint devnum; static guint16 vid; static guint16 pid; #endif static gchar *cdc_wdm_str; static gchar *tty_str; #if defined MM_RUNTIME_CHECK_ENABLED static gboolean ignore_mm_runtime_check_flag; #endif #if defined WITH_UDEV /* Update */ static gboolean action_update_flag; static gchar *firmware_version_str; static gchar *config_version_str; static gchar *carrier_str; static gboolean ignore_version_errors_flag; static gboolean override_download_flag; static gint modem_storage_index_int; static gboolean skip_validation_flag; #endif /* Reset */ static gboolean action_reset_flag; /* Update (QDL mode) */ static gboolean action_update_qdl_flag; /* Verify */ static gboolean action_verify_flag; /* Main */ static gchar **image_strv; static gboolean device_open_proxy_flag; static gboolean device_open_qmi_flag; static gboolean device_open_mbim_flag; static gboolean device_open_auto_flag; static gboolean stdout_verbose_flag; static gboolean stdout_silent_flag; static gchar *verbose_log_str; static gboolean version_flag; static gboolean help_flag; static gboolean help_examples_flag; #if defined WITH_UDEV static gboolean parse_busnum_devnum (const gchar *option_name, const gchar *value, gpointer data, GError **error) { gchar **strv; gint busnum_idx = -1; gint devnum_idx = 0; gulong aux; gboolean result = FALSE; strv = g_strsplit (value, ":", -1); g_assert (strv[0]); if (strv[1]) { busnum_idx = 0; devnum_idx = 1; if (strv[2]) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid busnum-devnum string: too many fields"); goto out; } } if (busnum_idx != -1) { aux = strtoul (strv[busnum_idx], NULL, 10); if (aux == 0 || aux > G_MAXUINT) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid bus number: %s", strv[busnum_idx]); goto out; } busnum = (guint) aux; } aux = strtoul (strv[devnum_idx], NULL, 10); if (aux == 0 || aux > G_MAXUINT) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid dev number: %s", strv[devnum_idx]); goto out; } devnum = (guint) aux; result = TRUE; out: g_strfreev (strv); return result; } static gboolean parse_vid_pid (const gchar *option_name, const gchar *value, gpointer data, GError **error) { gchar **strv; gint vid_idx = 0; gint pid_idx = -1; gulong aux; gboolean result = FALSE; strv = g_strsplit (value, ":", -1); g_assert (strv[0]); if (strv[1]) { pid_idx = 1; if (strv[2]) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid vid-pid string: too many fields"); goto out; } } if (pid_idx != -1) { aux = strtoul (strv[pid_idx], NULL, 16); if (aux == 0 || aux > G_MAXUINT16) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid product id: %s", strv[pid_idx]); goto out; } pid = (guint) aux; } aux = strtoul (strv[vid_idx], NULL, 16); if (aux == 0 || aux > G_MAXUINT16) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid vendor id: %s", strv[vid_idx]); goto out; } vid = (guint16) aux; result = TRUE; out: g_strfreev (strv); return result; } #endif /* WITH_UDEV */ static GOptionEntry context_selection_entries[] = { #if defined WITH_UDEV { "busnum-devnum", 's', 0, G_OPTION_ARG_CALLBACK, &parse_busnum_devnum, "Select device by bus and device number (in decimal).", "[BUS:]DEV" }, { "vid-pid", 'd', 0, G_OPTION_ARG_CALLBACK, &parse_vid_pid, "Select device by device vendor and product id (in hexadecimal).", "VID[:PID]" }, #endif /* WITH_UDEV */ { "cdc-wdm", 'w', 0, G_OPTION_ARG_FILENAME, &cdc_wdm_str, "Select device by QMI/MBIM cdc-wdm device path (e.g. /dev/cdc-wdm0).", "[PATH]" }, { "tty", 't', 0, G_OPTION_ARG_FILENAME, &tty_str, "Select device by serial device path (e.g. /dev/ttyUSB2).", "[PATH]" }, { NULL } }; #if defined WITH_UDEV static GOptionEntry context_update_entries[] = { { "update", 'u', 0, G_OPTION_ARG_NONE, &action_update_flag, "Launch firmware update process.", NULL }, { "firmware-version", 'f', 0, G_OPTION_ARG_STRING, &firmware_version_str, "Firmware version (e.g. '05.05.58.00').", "[VERSION]", }, { "config-version", 'c', 0, G_OPTION_ARG_STRING, &config_version_str, "Config version (e.g. '005.025_002').", "[VERSION]", }, { "carrier", 'C', 0, G_OPTION_ARG_STRING, &carrier_str, "Carrier name (e.g. 'Generic').", "[CARRIER]", }, { "ignore-version-errors", 0, 0, G_OPTION_ARG_NONE, &ignore_version_errors_flag, "Run update operation even with version string errors.", NULL }, { "override-download", 0, 0, G_OPTION_ARG_NONE, &override_download_flag, "Download images even if module says it already has them.", NULL }, { "modem-storage-index", 0, 0, G_OPTION_ARG_INT, &modem_storage_index_int, "Index storage for the modem image.", "[INDEX]" }, { "skip-validation", 0, 0, G_OPTION_ARG_NONE, &skip_validation_flag, "Don't wait to validate the running firmware after update.", NULL }, { NULL } }; #endif /* WITH_UDEV */ static GOptionEntry context_reset_entries[] = { { "reset", 'b', 0, G_OPTION_ARG_NONE, &action_reset_flag, "Reset device into QDL download mode.", NULL }, { NULL } }; static GOptionEntry context_update_qdl_entries[] = { { "update-qdl", 'U', 0, G_OPTION_ARG_NONE, &action_update_qdl_flag, "Launch firmware update process in QDL mode.", NULL }, { NULL } }; static GOptionEntry context_verify_entries[] = { { "verify", 'z', 0, G_OPTION_ARG_NONE, &action_verify_flag, "Analyze and verify firmware images.", NULL }, { NULL } }; static GOptionEntry context_main_entries[] = { { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &image_strv, "", "FILE1 FILE2..." }, { "device-open-proxy", 'p', 0, G_OPTION_ARG_NONE, &device_open_proxy_flag, "Request to use the 'qmi-proxy' proxy.", NULL }, { "device-open-qmi", 0, 0, G_OPTION_ARG_NONE, &device_open_qmi_flag, "Open a cdc-wdm device explicitly in QMI mode", NULL }, { "device-open-mbim", 0, 0, G_OPTION_ARG_NONE, &device_open_mbim_flag, "Open a cdc-wdm device explicitly in MBIM mode", NULL }, { "device-open-auto", 0, 0, G_OPTION_ARG_NONE, &device_open_auto_flag, "Open a cdc-wdm device in either QMI or MBIM mode (default)", NULL }, #if defined MM_RUNTIME_CHECK_ENABLED { "ignore-mm-runtime-check", 0, 0, G_OPTION_ARG_NONE, &ignore_mm_runtime_check_flag, "Ignore ModemManager runtime check", NULL }, #endif { "verbose", 'v', 0, G_OPTION_ARG_NONE, &stdout_verbose_flag, "Run action with verbose messages in standard output, including the debug ones.", NULL }, { "silent", 'S', 0, G_OPTION_ARG_NONE, &stdout_silent_flag, "Run action with no messages in standard output; not even the error ones.", NULL }, { "verbose-log", 'L', 0, G_OPTION_ARG_FILENAME, &verbose_log_str, "Write verbose messages to an output file.", "[PATH]" }, { "version", 'V', 0, G_OPTION_ARG_NONE, &version_flag, "Print version.", NULL }, { "help", 'h', 0, G_OPTION_ARG_NONE, &help_flag, "Show help.", NULL }, { "help-examples", 'H', 0, G_OPTION_ARG_NONE, &help_examples_flag, "Show help examples.", NULL }, { NULL } }; static const gchar *context_description = " ***************************************************************************\n" " Warning!\n" " ***************************************************************************\n" "\n" " Use this program with caution. The authors take *no* responsibility if any\n" " device gets broken as a result of using this program.\n" "\n" " Please report issues to the libqmi mailing list at:\n" " libqmi-devel@lists.freedesktop.org\n"; /*****************************************************************************/ static void print_version (void) { g_print ("\n" PROGRAM_NAME " " PROGRAM_VERSION "\n" "\n" " Copyright (C) 2016-2017 Bjørn Mork\n" " Copyright (C) 2016-2017 Zodiac Inflight Innovations\n" " Copyright (C) 2016-2017 Aleksander Morgado\n" "\n" "License GPLv2+: GNU GPL version 2 or later \n" "This is free software: you are free to change and redistribute it.\n" "There is NO WARRANTY, to the extent permitted by law.\n" "\n"); } static void print_help (GOptionContext *context) { gchar *str; /* Always print --help-all */ str = g_option_context_get_help (context, FALSE, NULL); g_print ("%s", str); g_free (str); } static void print_help_examples (void) { #if defined WITH_UDEV g_print ("\n" "********************************************************************************\n" "\n" " Example: Updating a Sierra Wireless MC7354.\n" " (or other 9x15 or 9x30 devices, like the MC7304, MC7330, MC7455... ).\n" "\n" " The MC7354 is a 9x15 device which requires the firmware updater to specify the\n" " firmware version string, the config version string and the carrier string, so\n" " that they are included as identifiers of the firmware images downloaded. The\n" " core logic in the application will try to automatically detect these strings,\n" " although the user can also use specific options to override them or if the\n" " automatic detection failed.\n" "\n" " While in normal operation, the device will expose multiple cdc-wdm ports, and\n" " the updater application just needs one of those cdc-wdm ports to start the\n" " operation. The user can explicitly specify the cdc-wdm port to use, or\n" " otherwise use the generic device selection options (i.e. --busnum-devnum or\n" " --vid-pid) to do that automatically.\n" "\n" " Note that the firmware for the MC7354 is usually composed of a core system image\n" " (.cwe) and a carrier-specific image (.nvu). These two images need to be flashed\n" " on the same operation, unless upgrading the carrier-specific image on a device\n" " which already has the matching firmware version. The two images may be given\n" " combined into a single image (.spk) file.\n" "\n" " a) An update operation specifying the vid:pid of the device (fails if multiple\n" " devices with the same vid:pid are found):\n" " $ sudo " PROGRAM_NAME " \\\n" " --update \\\n" " -d 1199:68c0 \\\n" " SWI9X15C_05.05.58.00.cwe \\\n" " SWI9X15C_05.05.58.00_Generic_005.025_002.nvu\n" "\n" " b) An update operation specifying an explicit QMI cdc-wdm device:\n" " $ sudo " PROGRAM_NAME " \\\n" " --update \\\n" " --cdc-wdm /dev/cdc-wdm0 \\\n" " SWI9X15C_05.05.58.00.cwe \\\n" " SWI9X15C_05.05.58.00_Generic_005.025_002.nvu\n" "\n" " c) An update operation specifying explicit firmware, config and carrier strings:\n" " $ sudo " PROGRAM_NAME " \\\n" " --update \\\n" " -d 1199:68c0 \\\n" " --firmware-version 05.05.58.00 \\\n" " --config-version 005.025_002 \\\n" " --carrier Generic \\\n" " SWI9X15C_05.05.58.00.cwe \\\n" " SWI9X15C_05.05.58.00_Generic_005.025_002.nvu\n" "\n" " d) An update operation with a combined image containing both system and carrier\n" " specific images::\n" " $ sudo " PROGRAM_NAME " \\\n" " --update \\\n" " -d 1199:68c0 \\\n" " 9999999_9902574_SWI9X15C_05.05.66.00_00_GENNA-UMTS_005.028_000-field.spk\n"); g_print ("\n" "********************************************************************************\n" "\n" " Example: Updating a Sierra Wireless MC7700.\n" " (or other 9200 devices, like the MC7710).\n" "\n" " The MC7700 is a 9200 device which doesn't require the explicit firmware, config\n" " and carrier strings. Unlike the MC7354, which would reboot itself into QDL\n" " download mode once these previous strings were configured, the MC7700 requires\n" " a specific \"boot and hold\" command to be sent (either via QMI or AT) to request\n" " the reset in QDL download mode.\n" "\n" " a) An update operation specifying the vid:pid of the device (fails if multiple\n" " devices with the same vid:pid are found):\n" " $ sudo " PROGRAM_NAME " \\\n" " --update \\\n" " -d 1199:68a2 \\\n" " 9999999_9999999_9200_03.05.14.00_00_generic_000.000_001_SPKG_MC.cwe\n" "\n" " b) An update operation specifying an explicit QMI cdc-wdm device:\n" " $ sudo " PROGRAM_NAME " \\\n" " --update \\\n" " --cdc-wdm /dev/cdc-wdm0 \\\n" " 9999999_9999999_9200_03.05.14.00_00_generic_000.000_001_SPKG_MC.cwe\n"); #endif /* WITH_UDEV */ g_print ("\n" "********************************************************************************\n" "\n" " Example: Manual process to update a Sierra Wireless MC7354.\n" " (or other 9x15 or 9x30 devices, like the MC7304, MC7330, MC7455... ).\n" "\n" " The upgrade of devices from the 9x15 and 9x30 families is triggered via a\n" " 'firmware preference' setting. If the device accepts the setting, the user\n" " can request a device power cycle, which will boot in QDL download mode:\n" "\n" " a) Set firmware preference setting:\n" " $ sudo qmicli \\\n" " -d /dev/cdc-wdm0 \\\n" " --dms-set-firmware-preference=\"05.05.58.00,005.025_002,Generic\"\n" "\n" " b) Request power cycle:\n" " $ sudo qmicli \\\n" " -d /dev/cdc-wdm0 \\\n" " --dms-set-operating-mode=offline\n" " $ sudo qmicli \\\n" " -d /dev/cdc-wdm0 \\\n" " --dms-set-operating-mode=reset\n" "\n" " c) Wait for the /dev/ttyUSB device to appear.\n" "\n" " d) Run updater operation while in QDL download mode:\n" " $ sudo " PROGRAM_NAME " \\\n" " -t /dev/ttyUSB0 \\\n" " --update-qdl \\\n" " SWI9X15C_05.05.58.00.cwe \\\n" " SWI9X15C_05.05.58.00_Generic_005.025_002.nvu\n" "\n" " d) Now wait for the device to fully reboot, may take up to several minutes.\n"); g_print ("\n" "********************************************************************************\n" "\n" " Example: Manual process to update a Sierra Wireless MC7700.\n" " (or other 9200 devices, like the MC7710).\n" "\n" " Instead of setting a 'firmware preference', the devices from the 9200 family\n" " can just be rebooted in QDL mode:\n" "\n" " a) Request device to go into QDL download mode:\n" " $ sudo " PROGRAM_NAME " \\\n" " -d 1199:68a2 \\\n" " --reset\n" "\n" " b) Wait for the /dev/ttyUSB device to appear.\n" "\n" " c) Run updater operation while in QDL download mode:\n" " $ sudo " PROGRAM_NAME " \\\n" " -d 1199:68a2 \\\n" " --update-qdl \\\n" " 9999999_9999999_9200_03.05.14.00_00_generic_000.000_001_SPKG_MC.cwe\n" "\n" " d) Now wait for the device to fully reboot, may take up to several minutes.\n"); g_print ("\n" "********************************************************************************\n" "\n" " Example: Verify firmware images.\n" "\n" " a) Verify several images at once:\n" " $ " PROGRAM_NAME " \\\n" " --verify \\\n" " SWI9X15C_05.05.58.00.cwe \\\n" " SWI9X15C_05.05.58.00_Generic_005.025_002.nvu\n" "\n" " b) Verify all .cwe, .nvu and .spk images inside a directory:\n" " $ find . -regex \".*\\.\\(nvu\\|spk\\|cwe\\)\" -exec " PROGRAM_NAME " -v -z {} \\;\n" "\n" " c) Image files may be given within .exe files; extract them with 7-Zip:\n" " $ 7z x SWI9200M_3.5-Release13-SWI9200X_03.05.29.03.exe\n" " $ ls *.{cwe,nvu,spk} 2>/dev/null\n" " 9999999_9999999_9200_03.05.29.03_00_generic_000.000_001_SPKG_MC.cwe\n" "\n"); } /*****************************************************************************/ int main (int argc, char **argv) { GError *error = NULL; GOptionContext *context; GOptionGroup *group; guint n_actions; guint n_actions_images_needed; guint n_actions_device_needed; guint n_actions_cdc_wdm_needed; gboolean result = FALSE; QfuDeviceSelection *device_selection = NULL; QmiDeviceOpenFlags device_open_flags = QMI_DEVICE_OPEN_FLAGS_NONE; setlocale (LC_ALL, ""); /* Setup option context, process it and destroy it */ context = g_option_context_new ("- Update firmware in QMI devices"); g_option_context_set_description (context, context_description); group = g_option_group_new ("selection", "Generic device selection options", "", NULL, NULL); g_option_group_add_entries (group, context_selection_entries); g_option_context_add_group (context, group); #if defined WITH_UDEV group = g_option_group_new ("update", "Update options (normal mode)", "", NULL, NULL); g_option_group_add_entries (group, context_update_entries); g_option_context_add_group (context, group); #endif group = g_option_group_new ("reset", "Reset options (normal mode)", "", NULL, NULL); g_option_group_add_entries (group, context_reset_entries); g_option_context_add_group (context, group); group = g_option_group_new ("update-qdl", "Update options (QDL mode)", "", NULL, NULL); g_option_group_add_entries (group, context_update_qdl_entries); g_option_context_add_group (context, group); group = g_option_group_new ("verify", "Verify options", "", NULL, NULL); g_option_group_add_entries (group, context_verify_entries); g_option_context_add_group (context, group); g_option_context_add_main_entries (context, context_main_entries, NULL); g_option_context_set_help_enabled (context, FALSE); if (!g_option_context_parse (context, &argc, &argv, &error)) { g_printerr ("error: couldn't parse option context: %s\n", error->message); g_error_free (error); goto out; } if (version_flag) { print_version (); result = TRUE; goto out; } if (help_flag) { print_help (context); result = TRUE; goto out; } if (help_examples_flag) { print_help_examples (); result = TRUE; goto out; } /* Initialize logging */ qfu_log_init (stdout_verbose_flag, stdout_silent_flag, verbose_log_str); /* Total actions */ n_actions = (action_verify_flag + action_update_qdl_flag + action_reset_flag); /* Actions that require images specified */ n_actions_images_needed = (action_verify_flag + action_update_qdl_flag); /* Actions that require a device specified */ n_actions_device_needed = (action_update_qdl_flag + action_reset_flag); /* Actions that allow using a cdc-wdm device */ n_actions_cdc_wdm_needed = (action_reset_flag); #if defined WITH_UDEV n_actions += action_update_flag; n_actions_cdc_wdm_needed += action_update_flag; n_actions_images_needed += action_update_flag; n_actions_device_needed += action_update_flag; #endif /* We don't allow multiple actions at the same time */ if (n_actions == 0) { g_printerr ("error: no actions specified\n"); goto out; } if (n_actions > 1) { g_printerr ("error: too many actions specified\n"); goto out; } /* A list of images must be provided for update and verify operations */ if (n_actions_images_needed && !image_strv) { g_printerr ("error: no firmware images specified\n"); goto out; } /* device selection must be performed for update and reset operations */ if (n_actions_device_needed) { #if defined WITH_UDEV device_selection = qfu_device_selection_new (cdc_wdm_str, tty_str, vid, pid, busnum, devnum, &error); #else device_selection = qfu_device_selection_new (cdc_wdm_str, tty_str, 0, 0, 0, 0, &error); #endif if (!device_selection) { g_printerr ("error: couldn't select device:: %s\n", error->message); g_error_free (error); goto out; } #if defined MM_RUNTIME_CHECK_ENABLED /* For all those actions that require a device, we will be doing MM runtime checks */ if (!ignore_mm_runtime_check_flag) { gboolean mm_running; gboolean abort_needed = FALSE; if (!qfu_utils_modemmanager_running (&mm_running, &error)) { g_printerr ("error: couldn't check if ModemManager running: %s\n", error->message); g_error_free (error); abort_needed = TRUE; } else if (mm_running) { g_printerr ("error: ModemManager is running: cannot perform firmware upgrade operation at the same time\n"); abort_needed = TRUE; } if (abort_needed) { g_printerr ("\n" "Note: in order to do a clean firmware upgrade, ModemManager must NOT be running;\n" "please stop it and retry. On a 'systemd' based system this may be done as follows:\n" " # systemctl stop ModemManager\n" "\n" "You can also ignore this runtime check (at your own risk) using --ignore-mm-runtime-check.\n" "\n"); goto out; } } #endif } /* Validate device open flags */ if (n_actions_cdc_wdm_needed) { if (device_open_mbim_flag + device_open_qmi_flag + device_open_auto_flag > 1) { g_printerr ("error: cannot specify multiple mode flags to open device\n"); goto out; } if (device_open_proxy_flag) device_open_flags |= QMI_DEVICE_OPEN_FLAGS_PROXY; if (device_open_mbim_flag) device_open_flags |= QMI_DEVICE_OPEN_FLAGS_MBIM; if (device_open_auto_flag || (!device_open_qmi_flag && !device_open_mbim_flag)) device_open_flags |= QMI_DEVICE_OPEN_FLAGS_AUTO; } /* Run */ #if defined WITH_UDEV if (action_update_flag) { g_assert (QFU_IS_DEVICE_SELECTION (device_selection)); /* Validate storage index, just (0,G_MAXUINT8] for now. The value 0 is also not * valid, but we use it to flag when no specific index has been requested. */ if (modem_storage_index_int < 0 || modem_storage_index_int > G_MAXUINT8) { g_printerr ("error: invalid modem storage index\n"); goto out; } result = qfu_operation_update_run ((const gchar **) image_strv, device_selection, firmware_version_str, config_version_str, carrier_str, device_open_flags, ignore_version_errors_flag, override_download_flag, (guint8) modem_storage_index_int, skip_validation_flag); goto out; } #endif /* WITH_UDEV */ if (action_update_qdl_flag) { g_assert (QFU_IS_DEVICE_SELECTION (device_selection)); result = qfu_operation_update_qdl_run ((const gchar **) image_strv, device_selection); goto out; } if (action_reset_flag) { g_assert (QFU_IS_DEVICE_SELECTION (device_selection)); result = qfu_operation_reset_run (device_selection, device_open_flags); goto out; } if (action_verify_flag) { result = qfu_operation_verify_run ((const gchar **) image_strv); goto out; } g_assert_not_reached (); out: qfu_log_shutdown (); /* Clean exit for a clean memleak report */ if (context) g_option_context_free (context); if (device_selection) g_object_unref (device_selection); return (result ? EXIT_SUCCESS : EXIT_FAILURE); }