/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2015 Richard Hughes * * Licensed under the GNU Lesser General Public License Version 2.1 * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /** * SECTION:dfu-device * @short_description: Object representing a DFU-capable device * * This object allows two things: * * - Downloading from the host to the device, optionally with * verification using a DFU or DfuSe firmware file. * * - Uploading from the device to the host to a DFU or DfuSe firmware * file. The file format is chosen automatically, with DfuSe being * chosen if the device contains more than one target. * * See also: #DfuTarget, #DfuFirmware */ #include "config.h" #include #include "dfu-common.h" #include "dfu-device-private.h" #include "dfu-error.h" #include "dfu-target-private.h" static void dfu_device_finalize (GObject *object); /** * DfuDevicePrivate: * * Private #DfuDevice data **/ typedef struct { GUsbDevice *dev; GPtrArray *targets; gboolean device_open; guint16 runtime_pid; guint16 runtime_vid; } DfuDevicePrivate; G_DEFINE_TYPE_WITH_PRIVATE (DfuDevice, dfu_device, G_TYPE_OBJECT) #define GET_PRIVATE(o) (dfu_device_get_instance_private (o)) /** * dfu_device_class_init: **/ static void dfu_device_class_init (DfuDeviceClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = dfu_device_finalize; } /** * dfu_device_init: **/ static void dfu_device_init (DfuDevice *device) { DfuDevicePrivate *priv = GET_PRIVATE (device); priv->runtime_vid = 0xffff; priv->runtime_pid = 0xffff; priv->targets = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); } /** * dfu_device_finalize: **/ static void dfu_device_finalize (GObject *object) { DfuDevice *device = DFU_DEVICE (object); DfuDevicePrivate *priv = GET_PRIVATE (device); /* don't rely on this */ if (priv->device_open) { g_debug ("auto-closing DfuDevice, call dfu_device_close()"); g_usb_device_close (priv->dev, NULL); } g_ptr_array_unref (priv->targets); G_OBJECT_CLASS (dfu_device_parent_class)->finalize (object); } /** * dfu_device_add_targets: **/ static gboolean dfu_device_add_targets (DfuDevice *device) { DfuDevicePrivate *priv = GET_PRIVATE (device); guint i; GUsbInterface *iface; g_autoptr(GPtrArray) ifaces = NULL; /* add all DFU-capable targets */ ifaces = g_usb_device_get_interfaces (priv->dev, NULL); if (ifaces == NULL) return FALSE; g_ptr_array_set_size (priv->targets, 0); for (i = 0; i < ifaces->len; i++) { DfuTarget *target; iface = g_ptr_array_index (ifaces, i); if (g_usb_interface_get_class (iface) != G_USB_DEVICE_CLASS_APPLICATION_SPECIFIC) continue; if (g_usb_interface_get_subclass (iface) != 0x01) continue; target = _dfu_target_new (device, iface); if (target == NULL) continue; g_ptr_array_add (priv->targets, target); } return priv->targets->len > 0; } /** * dfu_device_new: * @dev: A #GUsbDevice * * Creates a new DFU device object. * * Return value: a new #DfuDevice, or %NULL if @dev was not DFU-capable * * Since: 0.5.4 **/ DfuDevice * dfu_device_new (GUsbDevice *dev) { DfuDevicePrivate *priv; DfuDevice *device; device = g_object_new (DFU_TYPE_DEVICE, NULL); priv = GET_PRIVATE (device); priv->dev = g_object_ref (dev); if (!dfu_device_add_targets (device)) { g_object_unref (device); return NULL; } return device; } /** * dfu_device_get_targets: * @device: a #DfuDevice * * Gets all the targets for this device. * * Return value: (transfer none): (element-type DfuTarget): #DfuTarget, or %NULL * * Since: 0.5.4 **/ GPtrArray * dfu_device_get_targets (DfuDevice *device) { DfuDevicePrivate *priv = GET_PRIVATE (device); g_return_val_if_fail (DFU_IS_DEVICE (device), NULL); return priv->targets; } /** * dfu_device_get_target_by_alt_setting: * @device: a #DfuDevice * @alt_setting: the setting used to find * @error: a #GError, or %NULL * * Gets a target with a specific alternative setting. * * Return value: (transfer full): a #DfuTarget, or %NULL * * Since: 0.5.4 **/ DfuTarget * dfu_device_get_target_by_alt_setting (DfuDevice *device, guint8 alt_setting, GError **error) { DfuDevicePrivate *priv = GET_PRIVATE (device); DfuTarget *target; guint i; g_return_val_if_fail (DFU_IS_DEVICE (device), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); /* find by ID */ for (i = 0; i < priv->targets->len; i++) { target = g_ptr_array_index (priv->targets, i); if (dfu_target_get_interface_alt_setting (target) == alt_setting) return g_object_ref (target); } /* failed */ g_set_error (error, DFU_ERROR, DFU_ERROR_NOT_FOUND, "No target with alt-setting %i", alt_setting); return NULL; } /** * dfu_device_get_target_default: * @device: a #DfuDevice * @error: a #GError, or %NULL * * Gets the default target. * * Return value: (transfer full): a #DfuTarget, or %NULL * * Since: 0.5.4 **/ DfuTarget * dfu_device_get_target_default (DfuDevice *device, GError **error) { DfuDevicePrivate *priv = GET_PRIVATE (device); g_return_val_if_fail (DFU_IS_DEVICE (device), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); /* find first target */ if (priv->targets->len == 0) { g_set_error_literal (error, DFU_ERROR, DFU_ERROR_NOT_FOUND, "No default target"); return NULL; } return g_object_ref (g_ptr_array_index (priv->targets, 0)); } /** * dfu_device_get_target_by_alt_name: * @device: a #DfuDevice * @alt_name: the name used to find * @error: a #GError, or %NULL * * Gets a target with a specific alternative name. * * Return value: (transfer full): a #DfuTarget, or %NULL * * Since: 0.5.4 **/ DfuTarget * dfu_device_get_target_by_alt_name (DfuDevice *device, const gchar *alt_name, GError **error) { DfuDevicePrivate *priv = GET_PRIVATE (device); DfuTarget *target; guint i; g_return_val_if_fail (DFU_IS_DEVICE (device), NULL); g_return_val_if_fail (error == NULL || *error == NULL, NULL); /* find by ID */ for (i = 0; i < priv->targets->len; i++) { target = g_ptr_array_index (priv->targets, i); if (g_strcmp0 (dfu_target_get_interface_alt_name (target), alt_name) == 0) return g_object_ref (target); } /* failed */ g_set_error (error, DFU_ERROR, DFU_ERROR_NOT_FOUND, "No target with alt-name %s", alt_name); return NULL; } /** * dfu_device_get_runtime_vid: * @device: a #DfuDevice * * Gets the runtime vendor ID. * * Return value: vendor ID, or 0xffff for unknown * * Since: 0.5.4 **/ guint16 dfu_device_get_runtime_vid (DfuDevice *device) { DfuDevicePrivate *priv = GET_PRIVATE (device); g_return_val_if_fail (DFU_IS_DEVICE (device), 0xffff); return priv->runtime_vid; } /** * dfu_device_get_runtime_pid: * @device: a #DfuDevice * * Gets the runtime product ID. * * Return value: product ID, or 0xffff for unknown * * Since: 0.5.4 **/ guint16 dfu_device_get_runtime_pid (DfuDevice *device) { DfuDevicePrivate *priv = GET_PRIVATE (device); g_return_val_if_fail (DFU_IS_DEVICE (device), 0xffff); return priv->runtime_pid; } /** * _dfu_device_set_runtime_vid: * @device: a #DfuDevice * @runtime_vid: a vendor ID, or 0xffff for unknown * * Sets the runtime vendor ID. * * Since: 0.5.4 **/ void _dfu_device_set_runtime_vid (DfuDevice *device, guint16 runtime_vid) { DfuDevicePrivate *priv = GET_PRIVATE (device); g_return_if_fail (DFU_IS_DEVICE (device)); priv->runtime_vid = runtime_vid; } /** * _dfu_device_set_runtime_pid: * @device: a #DfuDevice * @runtime_pid: a product ID, or 0xffff for unknown * * Sets the runtime product ID. * * Since: 0.5.4 **/ void _dfu_device_set_runtime_pid (DfuDevice *device, guint16 runtime_pid) { DfuDevicePrivate *priv = GET_PRIVATE (device); g_return_if_fail (DFU_IS_DEVICE (device)); priv->runtime_pid = runtime_pid; } /** * _dfu_device_get_usb_dev: (skip) * @device: a #DfuDevice * * Gets the internal USB device for the #DfuDevice. * * NOTE: This may change at runtime if the device is replugged or * reset. * * Returns: (transfer none): the internal USB device **/ GUsbDevice * _dfu_device_get_usb_dev (DfuDevice *device) { DfuDevicePrivate *priv = GET_PRIVATE (device); g_return_val_if_fail (DFU_IS_DEVICE (device), NULL); return priv->dev; } /** * dfu_device_open: * @device: a #DfuDevice * @error: a #GError, or %NULL * * Opens a DFU-capable device. * * Return value: %TRUE for success * * Since: 0.5.4 **/ gboolean dfu_device_open (DfuDevice *device, GError **error) { DfuDevicePrivate *priv = GET_PRIVATE (device); g_autoptr(GError) error_local = NULL; g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); /* just ignore */ if (priv->device_open) return TRUE; /* open */ if (!g_usb_device_open (priv->dev, &error_local)) { g_set_error (error, DFU_ERROR, DFU_ERROR_INVALID_DEVICE, "cannot open device %s: %s", g_usb_device_get_platform_id (priv->dev), error_local->message); return FALSE; } priv->device_open = TRUE; return TRUE; } /** * dfu_device_close: * @device: a #DfuDevice * @error: a #GError, or %NULL * * Closes a DFU device. * * Return value: %TRUE for success * * Since: 0.5.4 **/ gboolean dfu_device_close (DfuDevice *device, GError **error) { DfuDevicePrivate *priv = GET_PRIVATE (device); /* only close if open */ if (priv->device_open) { if (!g_usb_device_close (priv->dev, error)) return FALSE; priv->device_open = FALSE; } return TRUE; } /** * dfu_device_set_new_usb_dev: **/ static gboolean dfu_device_set_new_usb_dev (DfuDevice *device, GUsbDevice *dev, GCancellable *cancellable, GError **error) { DfuDevicePrivate *priv = GET_PRIVATE (device); guint i; GUsbInterface *iface; g_autoptr(GPtrArray) ifaces = NULL; /* mark all existing interfaces as unclaimed */ for (i = 0; i < priv->targets->len; i++) { DfuTarget *target = g_ptr_array_index (priv->targets, i); dfu_target_close (target, NULL); } /* close existing device */ if (!dfu_device_close (device, error)) return FALSE; /* set the new USB device */ g_set_object (&priv->dev, dev); /* update each interface */ ifaces = g_usb_device_get_interfaces (dev, error); if (ifaces == NULL) return FALSE; for (i = 0; i < ifaces->len; i++) { guint8 alt_setting; g_autoptr(DfuTarget) target = NULL; iface = g_ptr_array_index (ifaces, i); if (g_usb_interface_get_class (iface) != G_USB_DEVICE_CLASS_APPLICATION_SPECIFIC) continue; if (g_usb_interface_get_subclass (iface) != 0x01) continue; alt_setting = g_usb_interface_get_alternate (iface); target = dfu_device_get_target_by_alt_setting (device, alt_setting, NULL); if (target == NULL) continue; if (!_dfu_target_update (target, iface, cancellable, error)) return FALSE; } return dfu_device_open (device, error); } /** * dfu_device_wait_for_replug: * @device: a #DfuDevice * @timeout: the maximum amount of time to wait * @cancellable: a #GCancellable, or %NULL * @error: a #GError, or %NULL * * Waits for a DFU device to disconnect and reconnect. * * Return value: %TRUE for success * * Since: 0.5.4 **/ gboolean dfu_device_wait_for_replug (DfuDevice *device, guint timeout, GCancellable *cancellable, GError **error) { DfuDevicePrivate *priv = GET_PRIVATE (device); const guint poll_interval_ms = 100; gboolean went_away = FALSE; guint16 pid; guint16 vid; guint i; g_autofree gchar *platform_id = NULL; g_autoptr(GUsbContext) usb_ctx = NULL; g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); /* keep copies */ platform_id = g_strdup (g_usb_device_get_platform_id (priv->dev)); vid = g_usb_device_get_vid (priv->dev); pid = g_usb_device_get_pid (priv->dev); /* keep trying */ g_object_get (priv->dev, "context", &usb_ctx, NULL); for (i = 0; i < timeout / poll_interval_ms; i++) { g_autoptr(GUsbDevice) dev_tmp = NULL; g_usleep (poll_interval_ms * 1000); g_usb_context_enumerate (usb_ctx); dev_tmp = g_usb_context_find_by_platform_id (usb_ctx, platform_id, NULL); if (dev_tmp == NULL) { went_away = TRUE; continue; } /* VID:PID changed so find a DFU iface with the same alt */ if (vid != g_usb_device_get_vid (dev_tmp) || pid != g_usb_device_get_pid (dev_tmp)) { return dfu_device_set_new_usb_dev (device, dev_tmp, cancellable, error); } } /* target went off into the woods */ if (went_away) { g_set_error_literal (error, DFU_ERROR, DFU_ERROR_INVALID_DEVICE, "target went away but did not come back"); return FALSE; } /* VID and PID did not change */ g_set_error_literal (error, DFU_ERROR, DFU_ERROR_INVALID_DEVICE, "target came back with same VID:PID values"); return FALSE; } /** * dfu_device_reset: * @device: a #DfuDevice * @error: a #GError, or %NULL * * Resets the USB device. * * Return value: %TRUE for success * * Since: 0.5.4 **/ gboolean dfu_device_reset (DfuDevice *device, GError **error) { DfuDevicePrivate *priv = GET_PRIVATE (device); g_autoptr(GError) error_local = NULL; g_return_val_if_fail (DFU_IS_DEVICE (device), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); if (!g_usb_device_reset (priv->dev, &error_local)) { g_set_error (error, DFU_ERROR, DFU_ERROR_INVALID_DEVICE, "cannot reset USB device: %s [%i]", error_local->message, error_local->code); return FALSE; } return TRUE; } /** * dfu_device_upload: * @device: a #DfuDevice * @expected_size: the expected size of the firmware, or 0 for unknown * @flags: flags to use, e.g. %DFU_TARGET_TRANSFER_FLAG_VERIFY * @cancellable: a #GCancellable, or %NULL * @progress_cb: a #GFileProgressCallback, or %NULL * @progress_cb_data: user data to pass to @progress_cb * @error: a #GError, or %NULL * * Uploads firmware from the target to the host. * * Return value: (transfer full): the uploaded firmware, or %NULL for error * * Since: 0.5.4 **/ DfuFirmware * dfu_device_upload (DfuDevice *device, gsize expected_size, DfuTargetTransferFlags flags, GCancellable *cancellable, DfuProgressCallback progress_cb, gpointer progress_cb_data, GError **error) { DfuDevicePrivate *priv = GET_PRIVATE (device); gboolean auto_opened = FALSE; guint i; g_autoptr(DfuFirmware) firmware = NULL; g_autoptr(DfuTarget) target_default = NULL; g_autoptr(GPtrArray) targets = NULL; /* auto-open */ if (!priv->device_open) { if (!dfu_device_open (device, error)) return FALSE; auto_opened = TRUE; } /* create ahead of time */ firmware = dfu_firmware_new (); dfu_firmware_set_vid (firmware, priv->runtime_vid); dfu_firmware_set_pid (firmware, priv->runtime_pid); dfu_firmware_set_release (firmware, 0xffff); /* APP -> DFU */ if (flags & DFU_TARGET_TRANSFER_FLAG_DETACH) { target_default = dfu_device_get_target_default (device, error); if (target_default == NULL) return NULL; if (dfu_target_get_mode (target_default) == DFU_MODE_RUNTIME) { g_debug ("detaching"); if (!dfu_target_detach (target_default, NULL, error)) return NULL; if (!dfu_device_wait_for_replug (device, 5000, NULL, error)) return NULL; } } /* upload from each target */ targets = dfu_device_get_targets (device); for (i = 0; i < targets->len; i++) { DfuTarget *target; g_autoptr(DfuImage) image = NULL; target = g_ptr_array_index (targets, i); image = dfu_target_upload (target, 0, DFU_TARGET_TRANSFER_FLAG_NONE, cancellable, progress_cb, progress_cb_data, error); if (image == NULL) return NULL; dfu_firmware_add_image (firmware, image); } /* choose the most appropriate type */ if (targets->len > 1) { g_debug ("switching to DefuSe automatically"); dfu_firmware_set_format (firmware, DFU_FIRMWARE_FORMAT_DFUSE); } else { dfu_firmware_set_format (firmware, DFU_FIRMWARE_FORMAT_DFU_1_0); } /* do host reset */ if ((flags & DFU_TARGET_TRANSFER_FLAG_HOST_RESET) > 0 || (flags & DFU_TARGET_TRANSFER_FLAG_BOOT_RUNTIME) > 0) { if (!dfu_device_reset (device, error)) return NULL; } /* boot to runtime */ if (flags & DFU_TARGET_TRANSFER_FLAG_BOOT_RUNTIME) { g_debug ("booting to runtime"); if (!dfu_device_wait_for_replug (device, 2000, cancellable, error)) return NULL; } /* auto-close */ if (auto_opened) { if (!dfu_device_close (device, error)) return FALSE; } /* success */ return g_object_ref (firmware); } /** * dfu_device_download: * @device: a #DfuDevice * @firmware: a #DfuFirmware * @flags: flags to use, e.g. %DFU_TARGET_TRANSFER_FLAG_VERIFY * @cancellable: a #GCancellable, or %NULL * @progress_cb: a #GFileProgressCallback, or %NULL * @progress_cb_data: user data to pass to @progress_cb * @error: a #GError, or %NULL * * Downloads firmware from the host to the target, optionally verifying * the transfer. * * Return value: %TRUE for success * * Since: 0.5.4 **/ gboolean dfu_device_download (DfuDevice *device, DfuFirmware *firmware, DfuTargetTransferFlags flags, GCancellable *cancellable, DfuProgressCallback progress_cb, gpointer progress_cb_data, GError **error) { DfuDevicePrivate *priv = GET_PRIVATE (device); GPtrArray *images; gboolean auto_opened = FALSE; guint i; g_autoptr(DfuTarget) target_default = NULL; g_autoptr(GPtrArray) targets = NULL; /* check vendor matches */ if (dfu_firmware_get_vid (firmware) != 0xffff && dfu_device_get_runtime_pid (device) != 0xffff && dfu_firmware_get_vid (firmware) != dfu_device_get_runtime_vid (device)) { g_set_error (error, DFU_ERROR, DFU_ERROR_NOT_SUPPORTED, "vendor ID incorrect, expected 0x%04x got 0x%04x\n", dfu_firmware_get_vid (firmware), dfu_device_get_runtime_vid (device)); return FALSE; } /* check product matches */ if (dfu_firmware_get_pid (firmware) != 0xffff && dfu_device_get_runtime_pid (device) != 0xffff && dfu_firmware_get_pid (firmware) != dfu_device_get_runtime_pid (device)) { g_set_error (error, DFU_ERROR, DFU_ERROR_NOT_SUPPORTED, "product ID incorrect, expected 0x%04x got 0x%04x", dfu_firmware_get_pid (firmware), dfu_device_get_runtime_pid (device)); return FALSE; } /* auto-open */ if (!priv->device_open) { if (!dfu_device_open (device, error)) return FALSE; auto_opened = TRUE; } /* APP -> DFU */ if (flags & DFU_TARGET_TRANSFER_FLAG_DETACH) { target_default = dfu_device_get_target_default (device, error); if (target_default == NULL) return FALSE; if (dfu_target_get_mode (target_default) == DFU_MODE_RUNTIME) { g_debug ("detaching"); if (!dfu_target_detach (target_default, NULL, error)) return FALSE; if (!dfu_device_wait_for_replug (device, 5000, NULL, error)) return FALSE; } } /* download each target */ images = dfu_firmware_get_images (firmware); if (images->len == 0) { g_set_error_literal (error, DFU_ERROR, DFU_ERROR_INVALID_FILE, "no images in firmware file"); return FALSE; } for (i = 0; i < images->len; i++) { DfuImage *image; DfuTargetTransferFlags flags_local = DFU_TARGET_TRANSFER_FLAG_NONE; g_autoptr(DfuTarget) target_tmp = NULL; image = g_ptr_array_index (images, i); target_tmp = dfu_device_get_target_by_alt_setting (device, dfu_image_get_alt_setting (image), error); if (target_tmp == NULL) return FALSE; if (flags & DFU_TARGET_TRANSFER_FLAG_VERIFY) flags_local = DFU_TARGET_TRANSFER_FLAG_VERIFY; if (!dfu_target_open (target_tmp, DFU_TARGET_OPEN_FLAG_NONE, cancellable, error)) return FALSE; if (!dfu_target_download (target_tmp, image, flags_local, cancellable, progress_cb, progress_cb_data, error)) return FALSE; } /* do a host reset */ if ((flags & DFU_TARGET_TRANSFER_FLAG_HOST_RESET) > 0 || (flags & DFU_TARGET_TRANSFER_FLAG_BOOT_RUNTIME) > 0) { if (!dfu_device_reset (device, error)) return FALSE; } /* boot to runtime */ if (flags & DFU_TARGET_TRANSFER_FLAG_BOOT_RUNTIME) { g_debug ("booting to runtime to set auto-boot"); if (!dfu_device_wait_for_replug (device, 2000, cancellable, error)) return FALSE; target_default = dfu_device_get_target_default (device, error); if (target_default == NULL) return FALSE; if (!dfu_target_open (target_default, DFU_TARGET_OPEN_FLAG_NONE, NULL, error)) return FALSE; } /* auto-close */ if (auto_opened) { if (!dfu_device_close (device, error)) return FALSE; } return TRUE; }