diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/model/ImageSet.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/model/ImageSet.java index 046e168..449c212 100644 --- a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/model/ImageSet.java +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/model/ImageSet.java @@ -3,17 +3,49 @@ import android.util.Pair; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; +import io.runtime.mcumgr.dfu.suit.model.CacheImage; import io.runtime.mcumgr.exception.McuMgrException; -/** @noinspection unused*/ +/** + * Represents a set of images to be sent to the device using teh Image group (manager). + *

+ * The Image manager can be used to update devices with MCUboot and SUIT bootloaders. + * For SUIT bootloaders a dedicated SUIT manager should be used, but some devices support both + * or only Image manager (e.g. for recovery). + * @noinspection unused + */ public class ImageSet { + /** + * List of target images to be sent to the device. + *

+ * A device with MCUboot bootloader can have multiple images. Each image is identified by + * an image index. Images must be sent with the "image" parameter set to the image index. + * When all images are sent the client should send "test" or "confirm" command to confirm them + * and "reset" command to begin the update. + */ @NotNull private final List images; + /** + * Cache images are used to update devices supporting SUIT manifest. The cache images are + * sent after the SUIT manifest and contain parts of firmware that are not included in the + * manifest. + *

+ * In case the cache images are not null, {@link #images} must contain a single SUIT file. + *

+ * Flow: + * 1. Send .suit file ("image" is set to 0 (default)) + * 2. Send cache images, each with "image" parameter set to the partition ID. + * 3. Send "confirm" command (without hash) to begin the update. + */ + @Nullable + private List cacheImages; + /** * Creates an empty image set. Use {@link #add(TargetImage)} to add targets. */ @@ -37,27 +69,52 @@ public List getImages() { return images; } + @Nullable + public List getCacheImages() { + return cacheImages; + } + + @NotNull public ImageSet add(TargetImage binary) { images.add(binary); return this; } + @NotNull + public ImageSet add(CacheImage cacheImage) { + if (cacheImages == null) { + cacheImages = new ArrayList<>(); + } + cacheImages.add(cacheImage); + return this; + } + + @NotNull + public ImageSet set(List cacheImages) { + this.cacheImages = cacheImages; + return this; + } + + @NotNull public ImageSet add(byte[] image) throws McuMgrException { images.add(new TargetImage(image)); return this; } + @NotNull public ImageSet add(Pair image) throws McuMgrException { images.add(new TargetImage(image.first, image.second)); return this; } + @NotNull public ImageSet add(List> images) throws McuMgrException { for (Pair image : images) this.images.add(new TargetImage(image.first, image.second)); return this; } + @NotNull public ImageSet removeImagesWithImageIndex(int imageIndex) { for (int i = 0; i < images.size(); i++) { if (images.get(i).imageIndex == imageIndex) { diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/task/Confirm.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/task/Confirm.java index e187e5d..ec95ad1 100644 --- a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/task/Confirm.java +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/task/Confirm.java @@ -1,6 +1,7 @@ package io.runtime.mcumgr.dfu.mcuboot.task; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,12 +19,16 @@ class Confirm extends FirmwareUpgradeTask { private final static Logger LOG = LoggerFactory.getLogger(Confirm.class); - private final byte @NotNull [] hash; + private final byte @Nullable [] hash; Confirm(final byte @NotNull [] hash) { this.hash = hash; } + Confirm() { + this.hash = null; + } + @Override @NotNull public State getState() { @@ -47,21 +52,24 @@ public void onResponse(@NotNull final McuMgrImageStateResponse response) { performer.onTaskFailed(Confirm.this, new McuMgrErrorException(response.getReturnCode())); return; } - // Search for slot for which the confirm command was sent and check its status. - for (final McuMgrImageStateResponse.ImageSlot slot : response.images) { - if (Arrays.equals(slot.hash, hash)) { - if (slot.permanent || slot.confirmed) { - performer.onTaskCompleted(Confirm.this); - } else { - performer.onTaskFailed(Confirm.this, new McuMgrException("Image not confirmed.")); + + // MCUboot returns the list of images in the response. + final McuMgrImageStateResponse.ImageSlot[] images = response.images; + if (images != null) { + // Search for slot for which the confirm command was sent and check its status. + for (final McuMgrImageStateResponse.ImageSlot slot : images) { + if (Arrays.equals(slot.hash, hash)) { + if (slot.permanent || slot.confirmed) { + performer.onTaskCompleted(Confirm.this); + } else { + performer.onTaskFailed(Confirm.this, new McuMgrException("Image not confirmed.")); + } + return; } - return; } } - - // Some implementations do not report all primary slots. - // performer.onTaskFailed(Confirm.this, new McuMgrException("Confirmed image not found.")); - + // SUIT implementation of Image manager does not return images from Confirm command. + // Instead, the device will reset and the new image will be booted. performer.onTaskCompleted(Confirm.this); } diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/task/Validate.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/task/Validate.java index 5445728..7d492a4 100644 --- a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/task/Validate.java +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/mcuboot/task/Validate.java @@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory; import java.util.Arrays; +import java.util.List; import io.runtime.mcumgr.McuMgrCallback; import io.runtime.mcumgr.dfu.mcuboot.FirmwareUpgradeManager.Mode; @@ -12,9 +13,11 @@ import io.runtime.mcumgr.dfu.mcuboot.FirmwareUpgradeManager.State; import io.runtime.mcumgr.dfu.mcuboot.model.ImageSet; import io.runtime.mcumgr.dfu.mcuboot.model.TargetImage; +import io.runtime.mcumgr.dfu.suit.model.CacheImage; import io.runtime.mcumgr.exception.McuMgrErrorException; import io.runtime.mcumgr.exception.McuMgrException; import io.runtime.mcumgr.image.ImageWithHash; +import io.runtime.mcumgr.image.SUITImage; import io.runtime.mcumgr.managers.DefaultManager; import io.runtime.mcumgr.managers.ImageManager; import io.runtime.mcumgr.response.dflt.McuMgrBootloaderInfoResponse; @@ -238,6 +241,10 @@ public void onResponse(@NotNull final McuMgrImageStateResponse response) { } } if (!mcuMgrImage.needsConfirmation()) { + // Since nRF Connect SDK v.2.8 the SUIT image requires no confirmation. + if (mcuMgrImage instanceof SUITImage) { + performer.enqueue(new Confirm()); + } continue; } if (allowRevert && mode != Mode.NONE) { @@ -287,6 +294,15 @@ public void onResponse(@NotNull final McuMgrImageStateResponse response) { } } } + + // Enqueue uploading all cache images. + final List cacheImages = images.getCacheImages(); + if (cacheImages != null) { + for (final CacheImage cacheImage : cacheImages) { + performer.enqueue(new Upload(cacheImage.image, cacheImage.partitionId)); + } + } + // To make sure the reset command are added just once, they're added based on flags. if (initialResetRequired) { performer.enqueue(new ResetBeforeUpload(noSwap)); diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/SUITUpgradeManager.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/SUITUpgradeManager.java index 218305f..991fe17 100644 --- a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/SUITUpgradeManager.java +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/SUITUpgradeManager.java @@ -11,6 +11,7 @@ import io.runtime.mcumgr.dfu.FirmwareUpgradeCallback; import io.runtime.mcumgr.dfu.FirmwareUpgradeController; import io.runtime.mcumgr.dfu.FirmwareUpgradeSettings; +import io.runtime.mcumgr.dfu.suit.model.CacheImageSet; /** @noinspection unused*/ public class SUITUpgradeManager implements FirmwareUpgradeController { @@ -142,9 +143,28 @@ public void setResourceCallback(@Nullable final OnResourceRequiredCallback resou * Start the upgrade. *

* This method should be used for SUIT candidate envelopes files. + * + * @param settings the firmware upgrade settings. + * @param envelope the SUIT candidate envelope. */ public synchronized void start(@NotNull final FirmwareUpgradeSettings settings, final byte @NotNull [] envelope) { + start(settings, envelope, null); + } + + /** + * Start the upgrade. + *

+ * This method should be used for SUIT candidate envelopes files. + * + * @param settings the firmware upgrade settings. + * @param envelope the SUIT candidate envelope. + * @param cacheImages cache images to be uploaded together with the SUIT envelope before + * starting the update. + */ + public synchronized void start(@NotNull final FirmwareUpgradeSettings settings, + final byte @NotNull [] envelope, + @Nullable final CacheImageSet cacheImages) { if (mPerformer.isBusy()) { LOG.info("Firmware upgrade is already in progress"); return; @@ -154,7 +174,8 @@ public synchronized void start(@NotNull final FirmwareUpgradeSettings settings, mInternalCallback.onUpgradeStarted(this); final SUITUpgradePerformer.Settings performerSettings = new SUITUpgradePerformer.Settings(settings, mResourceCallback); - mPerformer.start(mTransport, performerSettings, envelope); + + mPerformer.start(mTransport, performerSettings, envelope, cacheImages); } //****************************************************************** diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/SUITUpgradePerformer.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/SUITUpgradePerformer.java index 5a61da7..671f533 100644 --- a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/SUITUpgradePerformer.java +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/SUITUpgradePerformer.java @@ -8,6 +8,7 @@ import io.runtime.mcumgr.McuMgrTransport; import io.runtime.mcumgr.dfu.FirmwareUpgradeCallback; import io.runtime.mcumgr.dfu.FirmwareUpgradeSettings; +import io.runtime.mcumgr.dfu.suit.model.CacheImageSet; import io.runtime.mcumgr.dfu.suit.task.PerformDfu; import io.runtime.mcumgr.dfu.suit.task.SUITUpgradeTask; import io.runtime.mcumgr.exception.McuMgrException; @@ -51,9 +52,10 @@ SUITUpgradeManager.State getState() { void start(@NotNull final McuMgrTransport transport, @NotNull final Settings settings, - final byte @NotNull [] envelope) { + final byte @NotNull [] envelope, + @Nullable final CacheImageSet cacheImageSet) { LOG.trace("Starting SUIT upgrade"); - super.start(transport, settings, new PerformDfu(envelope)); + super.start(transport, settings, new PerformDfu(envelope, cacheImageSet)); } @Override diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/model/CacheImage.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/model/CacheImage.java new file mode 100644 index 0000000..1049f07 --- /dev/null +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/model/CacheImage.java @@ -0,0 +1,26 @@ +package io.runtime.mcumgr.dfu.suit.model; + +import org.jetbrains.annotations.NotNull; + +/** @noinspection unused*/ +public class CacheImage { + + /** Target partition ID. */ + public final int partitionId; + + /** + * The image. + */ + public final byte @NotNull [] image; + + /** + * A wrapper for a partition cache raw image and the ID of the partition. + * + * @param partition the partition ID. + * @param data the signed binary to be sent. + */ + public CacheImage(int partition, byte @NotNull [] data) { + this.partitionId = partition; + this.image = data; + } +} diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/model/CacheImageSet.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/model/CacheImageSet.java new file mode 100644 index 0000000..a8407dc --- /dev/null +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/model/CacheImageSet.java @@ -0,0 +1,58 @@ +package io.runtime.mcumgr.dfu.suit.model; + +import android.util.Pair; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** @noinspection unused*/ +public class CacheImageSet { + @NotNull + private final List images; + + /** + * Creates an empty image set. Use {@link #add(CacheImage)} to add targets. + */ + public CacheImageSet() { + this.images = new ArrayList<>(4); + } + + /** + * Creates an image set with given targets. + * @param targets image targets. + */ + public CacheImageSet(@NotNull final List targets) { + this.images = targets; + } + + /** + * Returns list of targets. + */ + @NotNull + public List getImages() { + return images; + } + + public CacheImageSet add(CacheImage image) { + images.add(image); + return this; + } + + public CacheImageSet add(int partition, byte[] image) { + images.add(new CacheImage(partition, image)); + return this; + } + + public CacheImageSet add(Pair image) { + images.add(new CacheImage(image.first, image.second)); + return this; + } + + public CacheImageSet add(List> images) { + for (Pair image : images) + this.images.add(new CacheImage(image.first, image.second)); + return this; + } +} diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/BeginInstall.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/BeginInstall.java new file mode 100644 index 0000000..d5a2b0b --- /dev/null +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/BeginInstall.java @@ -0,0 +1,46 @@ +package io.runtime.mcumgr.dfu.suit.task; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.runtime.mcumgr.McuMgrCallback; +import io.runtime.mcumgr.dfu.suit.SUITUpgradeManager; +import io.runtime.mcumgr.dfu.suit.SUITUpgradePerformer; +import io.runtime.mcumgr.exception.McuMgrException; +import io.runtime.mcumgr.managers.SUITManager; +import io.runtime.mcumgr.response.McuMgrResponse; +import io.runtime.mcumgr.task.TaskManager; + +class BeginInstall extends SUITUpgradeTask { + private final static Logger LOG = LoggerFactory.getLogger(BeginInstall.class); + + @Override + public int getPriority() { + return PRIORITY_PROCESS; + } + + @Override + public @Nullable SUITUpgradeManager.State getState() { + return SUITUpgradeManager.State.UPLOADING_RESOURCE; + } + + @Override + public void start(@NotNull TaskManager performer) { + LOG.trace("Starting deferred install"); + + final SUITManager manager = new SUITManager(performer.getTransport()); + manager.beginDeferredInstall(new McuMgrCallback<>() { + @Override + public void onResponse(@NotNull McuMgrResponse response) { + performer.onTaskCompleted(BeginInstall.this); + } + + @Override + public void onError(@NotNull McuMgrException error) { + performer.onTaskFailed(BeginInstall.this, error); + } + }); + } +} diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/PerformDfu.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/PerformDfu.java index 258f974..bb417c7 100644 --- a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/PerformDfu.java +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/PerformDfu.java @@ -1,9 +1,14 @@ package io.runtime.mcumgr.dfu.suit.task; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; import io.runtime.mcumgr.dfu.suit.SUITUpgradeManager; import io.runtime.mcumgr.dfu.suit.SUITUpgradePerformer; +import io.runtime.mcumgr.dfu.suit.model.CacheImage; +import io.runtime.mcumgr.dfu.suit.model.CacheImageSet; import io.runtime.mcumgr.task.TaskManager; /** @@ -12,9 +17,27 @@ public class PerformDfu extends SUITUpgradeTask { private final byte @NotNull [] envelope; + private final CacheImageSet cacheImages; + /** + * Create a new PerformDfu task. + * @param envelope the SUIT candidate envelope. + * @noinspection unused + */ public PerformDfu(final byte @NotNull [] envelope) { this.envelope = envelope; + this.cacheImages = null; + } + + /** + * Create a new PerformDfu task. + * @param envelope the SUIT candidate envelope. + * @param cacheImages cache images to be uploaded before starting the update. + */ + public PerformDfu(final byte @NotNull [] envelope, + final @Nullable CacheImageSet cacheImages) { + this.envelope = envelope; + this.cacheImages = cacheImages; } @NotNull @@ -30,7 +53,24 @@ public int getPriority() { @Override public void start(final @NotNull TaskManager performer) { - performer.enqueue(new UploadEnvelope(envelope)); + // Upload the candidate envelope. + performer.enqueue(new UploadEnvelope(envelope, cacheImages != null)); + + // Upload the cache images, if any. + if (cacheImages != null) { + final List images = cacheImages.getImages(); + for (CacheImage image : images) { + performer.enqueue(new UploadCache(image.partitionId, image.image)); + } + // After the cache images are uploaded, begin the deferred install. + performer.enqueue(new BeginInstall()); + } + + // After the candidate envelope and cache images are uploaded, client should poll + // for more resources. + performer.enqueue(new PollTask()); + + // Enqueuing is done, notify the performer that this task is complete. performer.onTaskCompleted(this); } } diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/UploadCache.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/UploadCache.java new file mode 100644 index 0000000..0dde6a9 --- /dev/null +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/UploadCache.java @@ -0,0 +1,100 @@ +package io.runtime.mcumgr.dfu.suit.task; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.runtime.mcumgr.dfu.suit.SUITUpgradeManager; +import io.runtime.mcumgr.dfu.suit.SUITUpgradePerformer; +import io.runtime.mcumgr.exception.McuMgrException; +import io.runtime.mcumgr.managers.SUITManager; +import io.runtime.mcumgr.task.TaskManager; +import io.runtime.mcumgr.transfer.CacheUploader; +import io.runtime.mcumgr.transfer.TransferController; +import io.runtime.mcumgr.transfer.UploadCallback; + +class UploadCache extends SUITUpgradeTask { + private final static Logger LOG = LoggerFactory.getLogger(UploadCache.class); + + private final byte @NotNull [] data; + private final int targetId; + + /** + * Upload controller used to pause, resume, and cancel upload. Set when the upload is started. + */ + private TransferController mUploadController; + + public UploadCache( + final int targetId, + final byte @NotNull [] data + ) { + this.targetId = targetId; + this.data = data; + } + + @Override + public int getPriority() { + return PRIORITY_UPLOAD; + } + + @Override + public @Nullable SUITUpgradeManager.State getState() { + return SUITUpgradeManager.State.UPLOADING_RESOURCE; + } + + @Override + public void start(@NotNull TaskManager performer) { + // Should we resume? + if (mUploadController != null) { + mUploadController.resume(); + return; + } + + final UploadCallback callback = new UploadCallback() { + @Override + public void onUploadProgressChanged(final int current, final int total, final long timestamp) { + performer.onTaskProgressChanged(UploadCache.this, current, total, timestamp); + } + + @Override + public void onUploadFailed(@NotNull final McuMgrException error) { + LOG.info("Upload failed: {}", error.getMessage()); + performer.onTaskFailed(UploadCache.this, error); + } + + @Override + public void onUploadCanceled() { + LOG.info("Uploading cancelled"); + performer.onTaskCompleted(UploadCache.this); + } + + @Override + public void onUploadCompleted() { + LOG.info("Uploading complete"); + performer.onTaskCompleted(UploadCache.this); + } + }; + + LOG.info("Uploading cache image with target partition ID: {} ({} bytes)", targetId, data.length); + final SUITUpgradePerformer.Settings settings = performer.getSettings(); + final SUITManager manager = new SUITManager(performer.getTransport()); + mUploadController = new CacheUploader( + manager, + targetId, + data, + settings.settings.windowCapacity, + settings.settings.memoryAlignment + ).uploadAsync(callback); + } + + @Override + public void pause() { + mUploadController.pause(); + } + + @Override + public void cancel() { + mUploadController.cancel(); + } +} diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/UploadEnvelope.java b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/UploadEnvelope.java index 610f712..b281aff 100644 --- a/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/UploadEnvelope.java +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/suit/task/UploadEnvelope.java @@ -17,14 +17,16 @@ class UploadEnvelope extends SUITUpgradeTask { private final static Logger LOG = LoggerFactory.getLogger(UploadEnvelope.class); private final byte @NotNull [] envelope; + private final boolean deferInstall; /** * Upload controller used to pause, resume, and cancel upload. Set when the upload is started. */ private TransferController mUploadController; - public UploadEnvelope(final byte @NotNull [] envelope) { + public UploadEnvelope(final byte @NotNull [] envelope, final boolean deferInstall) { this.envelope = envelope; + this.deferInstall = deferInstall; } @Override @@ -45,9 +47,6 @@ public void start(@NotNull TaskManager + * When sent with len=0, the device will start processing the SUIT envelope. */ private final static int ID_ENVELOPE_UPLOAD = 2; @@ -57,12 +61,22 @@ public class SUITManager extends McuManager { * requested image in chunks. Due to the fact that SMP is designed in clients-server pattern * and lack of server-sent notifications, implementation bases on polling. */ - private final static int ID_POLL_IMAGE_STATE = 3; + private final static int ID_MISSING_IMAGE_STATE = 3; /** * Command delivers a packet of a resource requested by the target device. */ - private final static int ID_RESOURCE_UPLOAD = 4; + private final static int ID_MISSING_IMAGE_UPLOAD = 4; + + /** + * Commands uploads a raw cache image. + */ + private final static int ID_CACHE_RAW_UPLOAD = 5; + + /** + * Command cleans up the SUIT cache and envelopes. + */ + private final static int ID_CLEANUP = 6; /** * Construct a McuManager instance. @@ -157,7 +171,7 @@ public McuMgrManifestStateResponse getManifestState(int role) throws McuMgrExcep */ public void upload(byte @NotNull [] data, int offset, @NotNull McuMgrCallback callback) { - HashMap payloadMap = buildUploadPayload(data, offset); + HashMap payloadMap = buildUploadPayload(data, offset, false, -1, -1); // Timeout for the initial chunk is long, as the device may need to erase the flash. final long timeout = offset == 0 ? DEFAULT_TIMEOUT : SHORT_TIMEOUT; send(OP_WRITE, ID_ENVELOPE_UPLOAD, payloadMap, timeout, McuMgrUploadResponse.class, callback); @@ -183,14 +197,109 @@ public void upload(byte @NotNull [] data, int offset, @NotNull public McuMgrUploadResponse upload(byte @NotNull [] data, int offset) throws McuMgrException { - HashMap payloadMap = buildUploadPayload(data, offset); + HashMap payloadMap = buildUploadPayload(data, offset, false, -1, -1); + // Timeout for the initial chunk is long, as the device may need to erase the flash. + final long timeout = offset == 0 ? DEFAULT_TIMEOUT : SHORT_TIMEOUT; + return send(OP_WRITE, ID_ENVELOPE_UPLOAD, payloadMap, timeout, McuMgrUploadResponse.class); + } + + /** + * Command delivers a part of SUIT envelope to the device (asynchronous). + *

+ * When using deferred install, the device will not start the update process after the last + * chunk and will await the {@link #beginDeferredInstall()} command. This allows to send cache images + * using {@link #uploadCache(int, byte[], int)} before the actual update. + *

+ * The chunk size is limited by the current MTU. If the current MTU set by + * {@link #setUploadMtu(int)} is too large, the {@link McuMgrCallback#onError(McuMgrException)} + * with {@link InsufficientMtuException} error will be returned. + * Use {@link InsufficientMtuException#getMtu()} to get the current MTU and + * pass it to {@link #setUploadMtu(int)} and try again. + *

+ * Use {@link EnvelopeUploader#uploadAsync(UploadCallback)} to + * upload the whole envelope. + * + * @param data image data. + * @param offset the offset, from which the chunk will be sent. + * @param deferInstall if true, the device will not start the update process after the last chunk + * and will await the {@link #beginDeferredInstall()} command. + * @param callback the asynchronous callback. + */ + public void upload(byte @NotNull [] data, int offset, boolean deferInstall, + @NotNull McuMgrCallback callback) { + HashMap payloadMap = buildUploadPayload(data, offset, deferInstall, -1, -1); + // Timeout for the initial chunk is long, as the device may need to erase the flash. + final long timeout = offset == 0 ? DEFAULT_TIMEOUT : SHORT_TIMEOUT; + send(OP_WRITE, ID_ENVELOPE_UPLOAD, payloadMap, timeout, McuMgrUploadResponse.class, callback); + } + + /** + * Command delivers a part of SUIT envelope to the device (synchronous). + *

+ * When using deferred install, the device will not start the update process after the last + * chunk and will await the {@link #beginDeferredInstall()} command. This allows to send cache images + * before the actual update. + *

+ * The chunk size is limited by the current MTU. If the current MTU set by + * {@link #setUploadMtu(int)} is too large, the {@link InsufficientMtuException} error will be + * thrown. Use {@link InsufficientMtuException#getMtu()} to get the current MTU and + * pass it to {@link #setUploadMtu(int)} and try again. + *

+ * Use {@link EnvelopeUploader#uploadAsync(UploadCallback, CoroutineScope)} + * to upload the whole envelope. + * + * @param data image data. + * @param offset the offset, from which the chunk will be sent. + * @param deferInstall if true, the device will not start the update process after the last chunk + * and will await the {@link #beginDeferredInstall()} command. + * @return The upload response. + */ + @NotNull + public McuMgrUploadResponse upload(byte @NotNull [] data, int offset, boolean deferInstall) + throws McuMgrException { + HashMap payloadMap = buildUploadPayload(data, offset, deferInstall, -1, -1); // Timeout for the initial chunk is long, as the device may need to erase the flash. final long timeout = offset == 0 ? DEFAULT_TIMEOUT : SHORT_TIMEOUT; return send(OP_WRITE, ID_ENVELOPE_UPLOAD, payloadMap, timeout, McuMgrUploadResponse.class); } /** - * Poll for required image (asynchronous). + * Begins the update process (asynchronous). + *

+ * This method can be called after the SUIT envelope and cache partition images have been + * uploaded to the device. + * + * @param callback the asynchronous callback. + */ + public void beginDeferredInstall(@NotNull McuMgrCallback callback) { + HashMap payloadMap = new HashMap<>(); + payloadMap.put("len", 0); + payloadMap.put("off", 0); + // assuming defer_install is false by default, so not set + send(OP_WRITE, ID_ENVELOPE_UPLOAD, payloadMap, SHORT_TIMEOUT, McuMgrResponse.class, callback); + } + + /** + * Begins the update process (synchronous). + *

+ * This method can be called after the SUIT envelope and cache partition images have been + * uploaded to the device. + * + * @return The response. + */ + @NotNull + public McuMgrResponse beginDeferredInstall() throws McuMgrException { + HashMap payloadMap = new HashMap<>(); + payloadMap.put("len", 0); + payloadMap.put("off", 0); + // assuming defer_install is false by default, so not set + return send(OP_WRITE, ID_ENVELOPE_UPLOAD, payloadMap, SHORT_TIMEOUT, McuMgrResponse.class); + } + + /** + * Poll for required image (asynchronous). This should be called after the install was started + * either by sending the Envelope without deferred install, or by calling + * {@link #beginDeferredInstall(McuMgrCallback)}. *

* SUIT command sequence has the ability of conditional execution of directives, i.e. based * on the digest of installed image. That opens scenario where SUIT candidate envelope contains @@ -201,16 +310,18 @@ public McuMgrUploadResponse upload(byte @NotNull [] data, int offset) * and lack of server-sent notifications, implementation bases on polling. *

* After sending the Envelope, the client should periodically poll the device to check if an - * image is required. + * image is required. Use {{@link #uploadResource(int, byte[], int, McuMgrCallback)}} to + * deliver the image. * * @param callback the asynchronous callback. */ public void poll(@NotNull McuMgrCallback callback) { - send(OP_READ, ID_POLL_IMAGE_STATE, null, SHORT_TIMEOUT, McuMgrPollResponse.class, callback); + send(OP_READ, ID_MISSING_IMAGE_STATE, null, SHORT_TIMEOUT, McuMgrPollResponse.class, callback); } /** - * Poll for required image (synchronous). + * Poll for required image (synchronous). This should be called after the install was started + * either by sending the Envelope without deferred install, or by calling {@link #beginDeferredInstall()}. *

* SUIT command sequence has the ability of conditional execution of directives, i.e. based * on the digest of installed image. That opens scenario where SUIT candidate envelope contains @@ -221,14 +332,14 @@ public void poll(@NotNull McuMgrCallback callback) { * and lack of server-sent notifications, implementation bases on polling. *

* After sending the Envelope, the client should periodically poll the device to check if an - * image is required. + * image is required. Use {{@link #uploadResource(int, byte[], int)}} to deliver the image. * * @return The response. * @throws McuMgrException Transport error. See cause. */ @NotNull public McuMgrPollResponse poll() throws McuMgrException { - return send(OP_READ, ID_POLL_IMAGE_STATE, null, SHORT_TIMEOUT, McuMgrPollResponse.class); + return send(OP_READ, ID_MISSING_IMAGE_STATE, null, SHORT_TIMEOUT, McuMgrPollResponse.class); } /** @@ -251,10 +362,10 @@ public void uploadResource(int sessionId, byte @NotNull [] data, int offset, if (sessionId <= 0) { throw new IllegalArgumentException("Session ID must be greater than 0"); } - HashMap payloadMap = buildUploadPayload(data, offset, sessionId); + HashMap payloadMap = buildUploadPayload(data, offset, false, sessionId, -1); // Timeout for the initial chunk is long, as the device may need to erase the flash. final long timeout = offset == 0 ? DEFAULT_TIMEOUT : SHORT_TIMEOUT; - send(OP_WRITE, ID_RESOURCE_UPLOAD, payloadMap, timeout, McuMgrUploadResponse.class, callback); + send(OP_WRITE, ID_MISSING_IMAGE_UPLOAD, payloadMap, timeout, McuMgrUploadResponse.class, callback); } /** @@ -278,24 +389,75 @@ public McuMgrUploadResponse uploadResource(int sessionId, byte @NotNull [] data, if (sessionId <= 0) { throw new IllegalArgumentException("Session ID must be greater than 0"); } - HashMap payloadMap = buildUploadPayload(data, offset, sessionId); + HashMap payloadMap = buildUploadPayload(data, offset, false, sessionId, -1); // Timeout for the initial chunk is long, as the device may need to erase the flash. final long timeout = offset == 0 ? DEFAULT_TIMEOUT : SHORT_TIMEOUT; - return send(OP_WRITE, ID_RESOURCE_UPLOAD, payloadMap, timeout, McuMgrUploadResponse.class); + return send(OP_WRITE, ID_MISSING_IMAGE_UPLOAD, payloadMap, timeout, McuMgrUploadResponse.class); } - /* - * Build the upload payload. + /** + * Command delivers a part of a raw cache image to the device (asynchronous). + * + * @param partition the partition id (target id). + * @param data raw cache data. + * @param offset the offset, from which the chunk will be sent. + * @param callback the asynchronous callback. + */ + public void uploadCache(int partition, byte @NotNull [] data, int offset, + @NotNull McuMgrCallback callback) { + if (partition <= 0) { + throw new IllegalArgumentException("Partition ID must be greater than 0"); + } + HashMap payloadMap = buildUploadPayload(data, offset, false, -1, partition); + // Timeout for the initial chunk is long, as the device may need to erase the flash. + final long timeout = offset == 0 ? DEFAULT_TIMEOUT : SHORT_TIMEOUT; + send(OP_WRITE, ID_CACHE_RAW_UPLOAD, payloadMap, timeout, McuMgrUploadResponse.class, callback); + } + + /** + * Command delivers a part of a raw cache image to the device (synchronous). + * + * @param partition the partition id (target id). + * @param data raw cache data. + * @param offset the offset, from which the chunk will be sent. + * @return The upload response. */ @NotNull - private HashMap buildUploadPayload(byte @NotNull [] data, int offset) { - return buildUploadPayload(data, offset, -1); + public McuMgrUploadResponse uploadCache(int partition, byte @NotNull [] data, int offset) + throws McuMgrException { + if (partition <= 0) { + throw new IllegalArgumentException("Partition ID must be greater than 0"); + } + HashMap payloadMap = buildUploadPayload(data, offset, false, partition, -1); + // Timeout for the initial chunk is long, as the device may need to erase the flash. + final long timeout = offset == 0 ? DEFAULT_TIMEOUT : SHORT_TIMEOUT; + return send(OP_WRITE, ID_CACHE_RAW_UPLOAD, payloadMap, timeout, McuMgrUploadResponse.class); + } + + /** + * Erases the SUIT candidate envelope and cache images stored on the device (asynchronous). + * + * @param callback the asynchronous callback. + */ + public void cleanup(@NotNull McuMgrCallback callback) { + send(OP_WRITE, ID_CLEANUP, null, DEFAULT_TIMEOUT, McuMgrResponse.class, callback); + } + + /** + * Erases the SUIT candidate envelope and cache images stored on the device (synchronous). + * + * @return The response. + * @throws McuMgrException Transport error. See cause. + */ + @NotNull + public McuMgrResponse cleanup() throws McuMgrException { + return send(OP_WRITE, ID_CLEANUP, null, DEFAULT_TIMEOUT, McuMgrResponse.class); } @NotNull - private HashMap buildUploadPayload(byte @NotNull [] data, int offset, int sessionId) { + private HashMap buildUploadPayload(byte @NotNull [] data, int offset, boolean deferInstall, int sessionId, int partition) { // Get chunk of image data to send - int dataLength = Math.min(mMtu - calculatePacketOverhead(data, offset, sessionId), data.length - offset); + int dataLength = Math.min(mMtu - calculatePacketOverhead(data, offset, deferInstall, sessionId, partition), data.length - offset); byte[] sendBuffer = new byte[dataLength]; System.arraycopy(data, offset, sendBuffer, 0, dataLength); @@ -305,14 +467,20 @@ private HashMap buildUploadPayload(byte @NotNull [] data, int of payloadMap.put("off", offset); if (offset == 0) { payloadMap.put("len", data.length); + if (deferInstall) { + payloadMap.put("defer_install", true); + } if (sessionId > 0) { payloadMap.put("stream_session_id", sessionId); } + if (partition > 0) { + payloadMap.put("target_id", partition); + } } return payloadMap; } - private int calculatePacketOverhead(byte @NotNull [] data, int offset, int sessionId) { + private int calculatePacketOverhead(byte @NotNull [] data, int offset, boolean deferInstall, int sessionId, int partition) { try { if (getScheme().isCoap()) { HashMap overheadTestMap = new HashMap<>(); @@ -320,9 +488,15 @@ private int calculatePacketOverhead(byte @NotNull [] data, int offset, int sessi overheadTestMap.put("off", offset); if (offset == 0) { overheadTestMap.put("len", data.length); + if (deferInstall) { + overheadTestMap.put("defer_install", true); + } if (sessionId > 0) { overheadTestMap.put("stream_session_id", sessionId); } + if (partition > 0) { + overheadTestMap.put("target_id", partition); + } } byte[] header = {0, 0, 0, 0, 0, 0, 0, 0}; overheadTestMap.put("_h", header); @@ -337,8 +511,14 @@ private int calculatePacketOverhead(byte @NotNull [] data, int offset, int sessi size += 4 + CBOR.uintLength(offset); // "off": 0x636F6666 + offset if (offset == 0) { size += 4 + 5; // "len": 0x636C656E + len as 32-bit positive integer + if (deferInstall) { + size += 15; // "defer_install": 0x6D64656665725F696E7374616C6C + 0xF5 (true) + } if (sessionId > 0) { - size += 18 + CBOR.uintLength(sessionId); // "stream_session_id": 0x73747265616D5F73657373696F6E5F6964 + session ID + size += 18 + CBOR.uintLength(sessionId); // "stream_session_id": 0x7173747265616D5F73657373696F6E5F6964 + session ID + } + if (partition > 0) { + size += 10 + CBOR.uintLength(partition); // "target_id": 0x697461726765745F6964 + partition ID } } return size + 8; // 8 additional bytes for the SMP header diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/CacheUploader.kt b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/CacheUploader.kt new file mode 100644 index 0000000..5cac92e --- /dev/null +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/CacheUploader.kt @@ -0,0 +1,73 @@ +package io.runtime.mcumgr.transfer + +import io.runtime.mcumgr.McuMgrCallback +import io.runtime.mcumgr.exception.McuMgrException +import io.runtime.mcumgr.managers.SUITManager +import io.runtime.mcumgr.response.suit.McuMgrUploadResponse +import io.runtime.mcumgr.util.CBOR + +private const val OP_WRITE = 2 +private const val ID_CACHE_RAW_UPLOAD = 5 + +/** + * This uploader is using a [SUITManager] to upload the cache file during device firmware update. + * + * After sending a SUIT Envelope using [EnvelopeUploader] with _deferred install_, use this + * uploader to send the cache files. + * + * @property suitManager The SUIT Manager. + * @property partition The target partition ID. + * @param data The resource data, as bytes. + * @param windowCapacity Number of buffers available for sending data, defaults to 1. The more buffers + * are available, the more packets can be sent without awaiting notification with response, thus + * accelerating upload process. + * @param memoryAlignment The memory alignment of the device, defaults to 1. Some memory + * implementations may require bytes to be aligned to a certain value before saving them. + */ +open class CacheUploader( + private val suitManager: SUITManager, + private val partition: Int, + data: ByteArray, + windowCapacity: Int = 1, + memoryAlignment: Int = 1, +) : Uploader( + data, + windowCapacity, + memoryAlignment, + suitManager.mtu, + suitManager.scheme +) { + override fun write(requestMap: Map, timeout: Long, callback: (UploadResult) -> Unit) { + suitManager.uploadAsync(requestMap, timeout, callback) + } + + override fun getAdditionalData( + data: ByteArray, + offset: Int, + map: MutableMap + ) { + if (offset == 0) { + map["target_id"] = partition + } + } + + override fun getAdditionalSize(offset: Int): Int = + // "target_id": 0x697461726765745F6964 + partition ID + if (offset == 0) 18 + CBOR.uintLength(partition) else 0 +} + +private fun SUITManager.uploadAsync( + requestMap: Map, + timeout: Long, + callback: (UploadResult) -> Unit +) = send(OP_WRITE, ID_CACHE_RAW_UPLOAD, requestMap, timeout, McuMgrUploadResponse::class.java, + object : McuMgrCallback { + override fun onResponse(response: McuMgrUploadResponse) { + callback(UploadResult.Response(response, response.returnCode)) + } + + override fun onError(error: McuMgrException) { + callback(UploadResult.Failure(error)) + } + } +) \ No newline at end of file diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/EnvelopeUploader.kt b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/EnvelopeUploader.kt index d584a7b..9c7592f 100644 --- a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/EnvelopeUploader.kt +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/EnvelopeUploader.kt @@ -32,6 +32,7 @@ open class EnvelopeUploader( envelope: ByteArray, windowCapacity: Int = 1, memoryAlignment: Int = 1, + private val deferInstall: Boolean = false, ) : Uploader( envelope, windowCapacity, @@ -39,6 +40,16 @@ open class EnvelopeUploader( suitManager.mtu, suitManager.scheme ) { + override fun getAdditionalSize(offset: Int): Int = + // "defer_install": 0x6D64656665725F696E7374616C6C + 0xF5 (true) + if (offset == 0 && deferInstall) 15 else 0 + + override fun getAdditionalData(data: ByteArray, offset: Int, map: MutableMap) { + if (offset == 0 && deferInstall) { + map["defer_install"] = true + } + } + override fun write(requestMap: Map, timeout: Long, callback: (UploadResult) -> Unit) { suitManager.uploadAsync(requestMap, timeout, callback) } diff --git a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/ResourceUploader.kt b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/ResourceUploader.kt index 7b94ffa..7f2c3d1 100644 --- a/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/ResourceUploader.kt +++ b/mcumgr-core/src/main/java/io/runtime/mcumgr/transfer/ResourceUploader.kt @@ -3,12 +3,12 @@ package io.runtime.mcumgr.transfer import io.runtime.mcumgr.McuMgrCallback import io.runtime.mcumgr.exception.McuMgrException import io.runtime.mcumgr.managers.SUITManager -import io.runtime.mcumgr.response.suit.McuMgrUploadResponse import io.runtime.mcumgr.response.suit.McuMgrPollResponse +import io.runtime.mcumgr.response.suit.McuMgrUploadResponse import io.runtime.mcumgr.util.CBOR private const val OP_WRITE = 2 -private const val ID_RESOURCE_UPLOAD = 4 +private const val ID_MISSING_IMAGE_UPLOAD = 4 /** * This uploader is using a [SUITManager] to upload the resource requested by the device during @@ -48,10 +48,13 @@ open class ResourceUploader( offset: Int, map: MutableMap ) { + // Note: For some reason this has to be sent in each packet, not just when offset == 0 map["stream_session_id"] = sessionId } override fun getAdditionalSize(offset: Int): Int = + // Note: For some reason this has to be sent in each packet, not just when offset == 0 + // "stream_session_id": 0x73747265616D5F73657373696F6E5F6964 (18 bytes) + session ID 18 + CBOR.uintLength(sessionId) } @@ -60,7 +63,7 @@ private fun SUITManager.uploadAsync( requestMap: Map, timeout: Long, callback: (UploadResult) -> Unit -) = send(OP_WRITE, ID_RESOURCE_UPLOAD, requestMap, timeout, McuMgrUploadResponse::class.java, +) = send(OP_WRITE, ID_MISSING_IMAGE_UPLOAD, requestMap, timeout, McuMgrUploadResponse::class.java, object : McuMgrCallback { override fun onResponse(response: McuMgrUploadResponse) { callback(UploadResult.Response(response, response.returnCode)) diff --git a/moustache/README.mo b/moustache/README.mo index 4fcff97..2abca15 100644 --- a/moustache/README.mo +++ b/moustache/README.mo @@ -164,11 +164,6 @@ The different firmware upgrade modes are as follows: > [!Note] > Read about MCUboot modes [here](https://docs.mcuboot.com/design.html#image-slots). -### Software Update for Internet of Things (SUIT) - -Starting from version 1.9, the library supports SUIT (Software Update for Internet of Things) files. -In this case the selected mode is ignored. The process of upgrading is embedded in the SUIT file. - ### Firmware Upgrade State `FirmwareUpgradeManager` acts as a simple, mostly linear state machine which is determined by the `Mode`. @@ -183,6 +178,21 @@ has been set). If the uploaded image is already active, and confirmed in slot 0, succeed immediately. The `VALIDATE` state makes it easy to reattempt an upgrade without needing to re-upload the image or manually determine where to start. +### Software Update for Internet of Things (SUIT) + +Starting from version 1.9, the library supports SUIT (Software Update for Internet of Things) files. +In this case the selected mode is ignored. The process of upgrading is embedded in the SUIT file. + +A new firmware can be delivered using: +- single .suit file with SUIT Envelope +- a ZIP file with .suit file, cache images and additional binary files. + +The update is always started by sending a SUIT Envelope. When cache images are present in the ZIP +file, they are sent afterwards, each with a target partition ID. After sending a confirm command, +the library will poll every few seconds for additional resources. If the device requests a new +resource, it will be sent. The process is repeated until the device reboots, assuming successful +upgrade. + ## License This library is licensed under the Apache 2.0 license. For more info, see the `LICENSE` file. diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/ImageUploadFragment.java b/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/ImageUploadFragment.java index c5c290b..2f1eeef 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/ImageUploadFragment.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/fragment/mcumgr/ImageUploadFragment.java @@ -185,25 +185,23 @@ protected void onFileSelected(@NonNull final String fileName, final int fileSize @Override protected void onFileLoaded(@NonNull final byte[] data) { + binding.actionUpload.setEnabled(true); + binding.status.setText(R.string.image_upgrade_status_ready); + requiresImageSelection = true; try { final byte[] hash = McuMgrImage.getHash(data); binding.fileHash.setText(StringUtils.toHex(hash)); - binding.actionUpload.setEnabled(true); - binding.status.setText(R.string.image_upgrade_status_ready); - requiresImageSelection = true; } catch (final McuMgrException e) { // Support for SUIT (Software Update for Internet of Things) format. try { // Try parsing SUIT file. final byte[] hash = SUITImage.getHash(data); binding.fileHash.setText(StringUtils.toHex(hash)); - binding.actionUpload.setEnabled(true); - binding.status.setText(R.string.image_upgrade_status_ready); + // A .suit file goes to the main dfu partition, no need to choose. requiresImageSelection = false; } catch (final Exception e2) { + // Allow sending any file. binding.fileHash.setText(null); - clearFileContent(); - onFileLoadingFailed(R.string.image_error_file_not_valid); } } } diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/utils/ZipPackage.java b/sample/src/main/java/io/runtime/mcumgr/sample/utils/ZipPackage.java index 53a072a..413caf2 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/utils/ZipPackage.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/utils/ZipPackage.java @@ -2,7 +2,6 @@ import androidx.annotation.Keep; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; @@ -20,6 +19,7 @@ import io.runtime.mcumgr.dfu.mcuboot.model.ImageSet; import io.runtime.mcumgr.dfu.mcuboot.model.TargetImage; +import io.runtime.mcumgr.dfu.suit.model.CacheImageSet; import io.runtime.mcumgr.exception.McuMgrException; import timber.log.Timber; @@ -66,6 +66,10 @@ private static class File { * @since NCS v 2.5, nRF Connect Device Manager 1.8. */ private int slot = TargetImage.SLOT_SECONDARY; + /** + * The target partition ID. This parameter is valid for files with type `cache`. + */ + private int partition = 0; } } @@ -112,6 +116,12 @@ public ImageSet getBinaries() throws IOException, McuMgrException { return binaries; } + /** + * Returns the SUIT envelope. + *

+ * This is valid only for SUIT updates using SUIT manager. + * @return The SUIT envelope, or null if not present in the ZIP. + */ public byte[] getSuitEnvelope() { // First, search for an entry of type "suit-envelope". for (final Manifest.File file: manifest.files) { @@ -129,6 +139,32 @@ public byte[] getSuitEnvelope() { return null; } + /** + * Raw cache images are sent to the device together with the SUIT envelope before starting the + * update process. The cache images are stored in the cache partitions. + * + * @return The cache images, or null if not present in the ZIP. + * @throws IOException if at least one of the cache images is missing. + */ + public CacheImageSet getCacheBinaries() throws IOException { + final CacheImageSet cache = new CacheImageSet(); + + // Search for images. + for (final Manifest.File file: manifest.files) { + if (file.type.equals("cache")) { + final String name = file.file; + final byte[] content = entries.get(name); + if (content == null) + throw new IOException("File not found: " + name); + + cache.add(file.partition, content); + } + } + if (cache.getImages().isEmpty()) + return null; + return cache; + } + public byte[] getResource(@NonNull final String name) { return entries.get(name); } diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageControlViewModel.java b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageControlViewModel.java index 3ec4d56..85d0606 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageControlViewModel.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageControlViewModel.java @@ -26,6 +26,7 @@ import io.runtime.mcumgr.managers.DefaultManager; import io.runtime.mcumgr.managers.ImageManager; import io.runtime.mcumgr.managers.SUITManager; +import io.runtime.mcumgr.response.McuMgrResponse; import io.runtime.mcumgr.response.dflt.McuMgrBootloaderInfoResponse; import io.runtime.mcumgr.response.img.McuMgrImageResponse; import io.runtime.mcumgr.response.img.McuMgrImageStateResponse; @@ -257,8 +258,27 @@ public void onError(@NonNull final McuMgrException error) { } public void confirm(final int image) { - if (image < 0 || hashes.length < image || hashes[image] == null) + if (image < 0 || hashes.length < image || hashes[image] == null) { + // In SUIT hashes aren't used, but it's possible to send a Confirm command (without a hash). + if (image == 1 && hashes.length == 2 && hashes[0] == null && hashes[1] == null) { + setBusy(); + errorLiveData.setValue(null); + manager.confirm(null, new McuMgrCallback<>() { + @Override + public void onResponse(@NotNull McuMgrImageStateResponse response) { + confirmAvailableLiveData.postValue(true); + eraseAvailableLiveData.postValue(true); + postReady(); + } + + @Override + public void onError(@NotNull McuMgrException error) { + postError(error); + } + }); + } return; + } setBusy(); errorLiveData.setValue(null); @@ -276,8 +296,27 @@ public void onError(@NonNull final McuMgrException error) { } public void erase(final int image) { - if (image < 0 || hashes.length < image || hashes[image] == null) + if (image < 0 || hashes.length < image || hashes[image] == null) { + // In SUIT hashes aren't used, but it's possible to send CleanUp command. + if (image == 1 && hashes.length == 2 && hashes[0] == null && hashes[1] == null) { + setBusy(); + errorLiveData.setValue(null); + suitManager.cleanup(new McuMgrCallback<>() { + @Override + public void onResponse(@NotNull McuMgrResponse response) { + confirmAvailableLiveData.postValue(true); + eraseAvailableLiveData.postValue(true); + postReady(); + } + + @Override + public void onError(@NotNull McuMgrException error) { + postError(error); + } + }); + } return; + } setBusy(); errorLiveData.setValue(null); @@ -298,8 +337,8 @@ private void postReady(@Nullable final List manifes responseLiveData.postValue(null); manifestsLiveData.postValue(manifests); testAvailableLiveData.postValue(false); - confirmAvailableLiveData.postValue(false); - eraseAvailableLiveData.postValue(false); + confirmAvailableLiveData.postValue(manifests != null && !manifests.isEmpty()); + eraseAvailableLiveData.postValue(manifests != null && !manifests.isEmpty()); postReady(); } diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUpgradeViewModel.java b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUpgradeViewModel.java index b9d7191..4e20cf7 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUpgradeViewModel.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUpgradeViewModel.java @@ -32,6 +32,7 @@ import io.runtime.mcumgr.dfu.mcuboot.FirmwareUpgradeManager; import io.runtime.mcumgr.dfu.mcuboot.model.ImageSet; import io.runtime.mcumgr.dfu.suit.SUITUpgradeManager; +import io.runtime.mcumgr.dfu.suit.model.CacheImageSet; import io.runtime.mcumgr.exception.McuMgrErrorException; import io.runtime.mcumgr.exception.McuMgrException; import io.runtime.mcumgr.image.SUITImage; @@ -291,10 +292,14 @@ public void upgrade(@NonNull final byte[] data, final ZipPackage zip = new ZipPackage(data); final byte[] envelope = zip.getSuitEnvelope(); if (envelope != null) { - // SUIT envelope can also be sent using Image Manager. + // SUIT envelope and cache images can also be sent using Image Manager. // For example for device recovery. - // Usually, a single file wouldn't be placed in a ZIP file, but let's try. images = new ImageSet().add(envelope); + // Check if the ZIP contains cache images. + final CacheImageSet cacheImages = zip.getCacheBinaries(); + if (cacheImages != null) { + images.set(cacheImages.getImages()); + } } else { images = zip.getBinaries(); } @@ -323,7 +328,7 @@ public void onUploadCancelled() { // Ignore } }); - upgradeWithSUITManager(envelope, windowCapacity, memoryAlignment); + upgradeWithSUITManager(envelope, null, windowCapacity, memoryAlignment); } catch (final Exception e) { try { // Try reading SUIT envelope from ZIP file. @@ -332,6 +337,7 @@ public void onUploadCancelled() { final byte[] envelope = zip.getSuitEnvelope(); if (envelope != null) { final SUITImage suitImage = SUITImage.fromBytes(envelope); + final CacheImageSet cacheImages = zip.getCacheBinaries(); // During the upload, SUIT manager may request additional resources. // This callback will return the requested resource from the ZIP file. suitManager.setResourceCallback(new SUITUpgradeManager.OnResourceRequiredCallback() { @@ -354,7 +360,7 @@ public void onUploadCancelled() { // Ignore } }); - upgradeWithSUITManager(suitImage, windowCapacity, memoryAlignment); + upgradeWithSUITManager(suitImage, cacheImages, windowCapacity, memoryAlignment); return; } throw new NullPointerException(); @@ -404,6 +410,7 @@ private void upgradeWithImageManager( private void upgradeWithSUITManager( @NonNull final SUITImage envelope, + @Nullable final CacheImageSet cacheImages, final int windowCapacity, final int memoryAlignment ) { @@ -413,7 +420,7 @@ private void upgradeWithSUITManager( .setWindowCapacity(windowCapacity) .setMemoryAlignment(memoryAlignment) .build(); - suitManager.start(settings, envelope.getData()); + suitManager.start(settings, envelope.getData(), cacheImages); } public void pause() { diff --git a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUploadViewModel.java b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUploadViewModel.java index 3ce579e..1293a44 100644 --- a/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUploadViewModel.java +++ b/sample/src/main/java/io/runtime/mcumgr/sample/viewmodel/mcumgr/ImageUploadViewModel.java @@ -6,14 +6,15 @@ package io.runtime.mcumgr.sample.viewmodel.mcumgr; +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + import java.util.Arrays; import javax.inject.Inject; import javax.inject.Named; -import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import io.runtime.mcumgr.McuMgrCallback; import io.runtime.mcumgr.McuMgrTransport; import io.runtime.mcumgr.ble.McuMgrBleTransport; @@ -117,45 +118,59 @@ public void upload(@NonNull final byte[] data, final int image, boolean force) { try { tmpHash = SUITImage.getHash(data); } catch (final McuMgrException e2) { - errorLiveData.setValue(e); - return; + // If the file does not contain a hash, we will not be able to check if the + // firmware has already been uploaded. We will upload it anyway. + // This has been changed for the sake of cache images for devices with SUIT bootloader, + // which do not have a hash in the image. + // This will also allow to send any file to the device, not only firmware. + // Good for testing. + tmpHash = null; } } final byte[] hash = tmpHash; - manager.list(new McuMgrCallback<>() { - @Override - public void onResponse(@NonNull final McuMgrImageStateResponse response) { - // Check if the fw has already been sent before. - McuMgrImageStateResponse.ImageSlot theSameImage = null; - for (final McuMgrImageStateResponse.ImageSlot image : response.images) { - if (Arrays.equals(hash, image.hash)) { - theSameImage = image; - break; + // Sends the firmware. This may return NO MEMORY error if slot 1 is + // filled with an image with pending or confirmed flags set. + final Runnable upload = () -> { + requestHighConnectionPriority(); + stateLiveData.postValue(State.UPLOADING); + initialBytes = 0; + setLoggingEnabled(false); + controller = manager.imageUpload(data, image, ImageUploadViewModel.this); + }; + + // Hash is null in case of SUIT cache partitions, or other files which aren't known firmware. + if (tmpHash != null) { + manager.list(new McuMgrCallback<>() { + @Override + public void onResponse(@NonNull final McuMgrImageStateResponse response) { + // Check if the fw has already been sent before. + McuMgrImageStateResponse.ImageSlot theSameImage = null; + for (final McuMgrImageStateResponse.ImageSlot image : response.images) { + if (Arrays.equals(hash, image.hash)) { + theSameImage = image; + break; + } } + // If yes, no need to send again. + if (!force && theSameImage != null) { + hashAlreadyFound.postValue(theSameImage.active); + postReady(); + return; + } + + upload.run(); } - // If yes, no need to send again. - if (!force && theSameImage != null) { - hashAlreadyFound.postValue(theSameImage.active); + + @Override + public void onError(@NonNull final McuMgrException error) { + errorLiveData.postValue(error); postReady(); - return; } - - requestHighConnectionPriority(); - // Otherwise, send the firmware. This may return NO MEMORY error if slot 1 is - // filled with an image with pending or confirmed flags set. - stateLiveData.postValue(State.UPLOADING); - initialBytes = 0; - setLoggingEnabled(false); - controller = manager.imageUpload(data, image, ImageUploadViewModel.this); - } - - @Override - public void onError(@NonNull final McuMgrException error) { - errorLiveData.postValue(error); - postReady(); - } - }); + }); + } else { + upload.run(); + } } public void pause() { diff --git a/sample/src/main/res/drawable/ic_help.xml b/sample/src/main/res/drawable/ic_help.xml index 58ee0f5..e20702b 100644 --- a/sample/src/main/res/drawable/ic_help.xml +++ b/sample/src/main/res/drawable/ic_help.xml @@ -8,6 +8,7 @@ Select Image - Application Core (0) - Network Core (1) - Image 2 (2) - Image 3 (3) + Image 0 (main) + Image 1 + Image 2 + Image 3 Overwrite?