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?