Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Direct-XIP without revert mode #117

Merged
merged 2 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,21 @@ public enum Mode {
* Use this mode when the new image supports SMP service and you want to test it
* before confirming.
*/
TEST_AND_CONFIRM
TEST_AND_CONFIRM,

/**
* When this flag is set, the manager will immediately send the reset command after
* the upload is complete. The device will reboot and will run the new image on its next
* boot.
* <p>
* This mode should be used with "Direct XIP without Revert" bootloader mode
* or when the confirm or test modes are not supported.
* <p>
* If the device supports <a href="https://developer.nordicsemi.com/nRF_Connect_SDK/doc/latest/zephyr/services/device_mgmt/smp_groups/smp_group_0.html#bootloader-information">Bootloader Information</a>
* command, the manager will check if the device is in "Direct XIP without Revert" mode
* and will use this mode automatically.
*/
NONE
}

//******************************************************************
Expand Down Expand Up @@ -310,9 +324,10 @@ public void setFirmwareUpgradeCallback(@Nullable final FirmwareUpgradeCallback c
* The mode may be set only before calling {@link #start(byte[])} method.
*
* @param mode the manager mode.
* @see Mode#TEST_ONLY TEST_ONLY
* @see Mode#CONFIRM_ONLY CONFIRM_ONLY
* @see Mode#TEST_AND_CONFIRM TEST_AND_CONFIRM
* @see Mode#TEST_ONLY
* @see Mode#CONFIRM_ONLY
* @see Mode#TEST_AND_CONFIRM
* @see Mode#NONE
*/
public void setMode(@NotNull final Mode mode) {
if (mPerformer.isBusy()) {
Expand All @@ -326,6 +341,9 @@ public void setMode(@NotNull final Mode mode) {
* Sets the estimated time required to swap images after uploading the image successfully.
* If the mode was set to {@link Mode#TEST_AND_CONFIRM}, the manager will wait this long
* before trying to reconnect to the device.
* <p>
* For DFU modes without swapping, i.e. Direct XIP with or without revert, this value can be
* set to 0, as there is no swap.
*
* @param swapTime estimated time required for swapping images, in milliseconds. 0 by default.
*/
Expand Down
11 changes: 10 additions & 1 deletion mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/task/Reset.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ class Reset extends FirmwareUpgradeTask {
*/
private long mResetResponseTime;

Reset() {
private final boolean mNoSwap;

Reset(final boolean noSwap) {
this.mNoSwap = noSwap;
}

@Override
Expand Down Expand Up @@ -59,6 +62,12 @@ public void onDisconnected() {

transport.removeObserver(this);

// If there is no swap, we're done. No need to wait anything.
if (mNoSwap) {
performer.onTaskCompleted(Reset.this);
return;
}

// Calculate the delay need that we need to wait until the swap is complete.
long now = SystemClock.elapsedRealtime();
long timeSinceReset = now - mResetResponseTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

class ResetBeforeUpload extends Reset {

ResetBeforeUpload() {
ResetBeforeUpload(final boolean noSwap) {
super(noSwap);
}

@Override
Expand Down
147 changes: 106 additions & 41 deletions mcumgr-core/src/main/java/io/runtime/mcumgr/dfu/task/Validate.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
import io.runtime.mcumgr.exception.McuMgrErrorException;
import io.runtime.mcumgr.exception.McuMgrException;
import io.runtime.mcumgr.image.McuMgrImage;
import io.runtime.mcumgr.managers.DefaultManager;
import io.runtime.mcumgr.managers.ImageManager;
import io.runtime.mcumgr.response.dflt.McuMgrBootloaderInfoResponse;
import io.runtime.mcumgr.response.img.McuMgrImageStateResponse;
import io.runtime.mcumgr.task.TaskManager;

Expand Down Expand Up @@ -51,7 +53,52 @@ public int getPriority() {
@Override
public void start(@NotNull final TaskManager<Settings, State> performer) {
final Settings settings = performer.getSettings();
final DefaultManager manager = new DefaultManager(settings.transport);

// Starting from NCS 2.5 different bootloader modes allow sending the image in
// slightly different ways. For that, we need to read bootloader info.
// If that command is not supported, we assume the old, normal way of sending.
manager.bootloaderInfo(DefaultManager.BOOTLOADER_INFO_QUERY_BOOTLOADER, new McuMgrCallback<>() {
@Override
public void onResponse(@NotNull final McuMgrBootloaderInfoResponse response) {
LOG.trace("Bootloader name: {}", response.bootloader);

if ("MCUboot".equals(response.bootloader)) {
manager.bootloaderInfo(DefaultManager.BOOTLOADER_INFO_MCUBOOT_QUERY_MODE, new McuMgrCallback<>() {
@Override
public void onResponse(@NotNull McuMgrBootloaderInfoResponse response) {
LOG.info("Bootloader is in mode: {}, no downgrade: {}", parseMode(response.mode), response.noDowngrade);
validate(performer,
response.mode == McuMgrBootloaderInfoResponse.MODE_DIRECT_XIP || response.mode == McuMgrBootloaderInfoResponse.MODE_DIRECT_XIP_WITH_REVERT,
response.mode != McuMgrBootloaderInfoResponse.MODE_DIRECT_XIP);
}

@Override
public void onError(@NotNull McuMgrException error) {
// Pretend nothing happened.
validate(performer, false, true);
}
});
} else {
// It's some unknown bootloader. Try sending the old way.
validate(performer, false, true);
}
}

@Override
public void onError(@NotNull final McuMgrException error) {
// Pretend nothing happened.
validate(performer, false, true);
}
});
}

private void validate(@NotNull final TaskManager<Settings, State> performer,
final boolean noSwap,
final boolean allowRevert) {
final Settings settings = performer.getSettings();
final ImageManager manager = new ImageManager(settings.transport);

manager.list(new McuMgrCallback<>() {
@Override
public void onResponse(@NotNull final McuMgrImageStateResponse response) {
Expand Down Expand Up @@ -86,7 +133,7 @@ public void onResponse(@NotNull final McuMgrImageStateResponse response) {

// The following flags will be updated based on the received slot information.
boolean found = false; // An image with the same hash was found on the device
boolean skip = false; // When this flag is set the image will not be uploaded
boolean skip = false; // When this flag is set the image will not be uploaded
boolean pending = false; // TEST command was sent
boolean permanent = false; // CONFIRM command was sent
boolean confirmed = false; // Image has booted and confirmed itself
Expand All @@ -105,10 +152,10 @@ public void onResponse(@NotNull final McuMgrImageStateResponse response) {
confirmed = slot.confirmed;
active = slot.active;

// If the image has been found on the secondary slot and it's confirmed,
// If the image has been found on its target slot and it's confirmed,
// we just need to restart the device in order for it to be swapped back to
// primary slot.
if (confirmed && slot.slot == image.slot) {
if (confirmed && slot.slot == image.slot && !noSwap) {
resetRequired = true;
}
break;
Expand Down Expand Up @@ -155,57 +202,62 @@ public void onResponse(@NotNull final McuMgrImageStateResponse response) {
}
if (!found) {
performer.enqueue(new Upload(mcuMgrImage, imageIndex));
}
switch (mode) {
case TEST_AND_CONFIRM -> {
// If the image is not pending (test command has not been sent) and not
// confirmed (another image is under test), and isn't the currently
// running image, send test command and update the flag.
if (!pending && !confirmed && !active) {
performer.enqueue(new Test(mcuMgrImage.getHash()));
pending = true;
}
// If the image is pending, reset is required.
if (pending) {
resetRequired = true;
}
if (!permanent && !confirmed) {
performer.enqueue(new ConfirmAfterReset(mcuMgrImage.getHash()));
}
if (!allowRevert || mode == Mode.NONE) {
resetRequired = true;
}
case TEST_ONLY -> {
// If the image is not pending (test command has not been sent) and not
// confirmed (another image is under test), and isn't the currently
// running image, send test command and update the flag.
if (!pending && !confirmed && !active) {
performer.enqueue(new Test(mcuMgrImage.getHash()));
pending = true;
}
// If the image is pending, reset is required.
if (pending) {
resetRequired = true;
}
if (allowRevert && mode != Mode.NONE) {
switch (mode) {
case TEST_AND_CONFIRM -> {
// If the image is not pending (test command has not been sent) and not
// confirmed (another image is under test), and isn't the currently
// running image, send test command and update the flag.
if (!pending && !confirmed && !active) {
performer.enqueue(new Test(mcuMgrImage.getHash()));
pending = true;
}
// If the image is pending, reset is required.
if (pending) {
resetRequired = true;
}
if (!permanent && !confirmed) {
performer.enqueue(new ConfirmAfterReset(mcuMgrImage.getHash()));
}
}
}
case CONFIRM_ONLY -> {
// If the firmware is not confirmed yet, confirm t.
if (!permanent && !confirmed) {
performer.enqueue(new Confirm(mcuMgrImage.getHash()));
permanent = true;
case TEST_ONLY -> {
// If the image is not pending (test command has not been sent) and not
// confirmed (another image is under test), and isn't the currently
// running image, send test command and update the flag.
if (!pending && !confirmed && !active) {
performer.enqueue(new Test(mcuMgrImage.getHash()));
pending = true;
}
// If the image is pending, reset is required.
if (pending) {
resetRequired = true;
}
}
if (permanent) {
resetRequired = true;
case CONFIRM_ONLY -> {
// If the firmware is not confirmed yet, confirm t.
if (!permanent && !confirmed) {
performer.enqueue(new Confirm(mcuMgrImage.getHash()));
permanent = true;
}
if (permanent) {
resetRequired = true;
}
}
}
}
}
// To make sure the reset command are added just once, they're added based on flags.
if (initialResetRequired) {
performer.enqueue(new ResetBeforeUpload());
performer.enqueue(new ResetBeforeUpload(noSwap));
}
if (resetRequired) {
if (eraseSettings)
performer.enqueue(new EraseStorage());
performer.enqueue(new Reset());
performer.enqueue(new Reset(noSwap));
}

performer.onTaskCompleted(Validate.this);
Expand All @@ -217,4 +269,17 @@ public void onError(@NotNull final McuMgrException e) {
}
});
}

private String parseMode(final int mode) {
switch (mode) {
case 0: return "Single App";
case 1: return "Swap Scratch";
case 2: return "Overwrite-only";
case 3: return "Swap Without Scratch";
case 4: return "Direct XIP Without Revert";
case 5: return "Direct XIP With Revert";
case 6: return "RAM Loader";
default: return "Unknown (" + mode + ")";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@
/** @noinspection unused*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class McuMgrBootloaderInfoResponse extends McuMgrOsResponse {
// MCUboot modes are explained here: https://docs.mcuboot.com/design.html#image-slots

/** Unknown mode of MCUboot. */
public static final int MODE_UNKNOWN = -1;
/** MCUboot is in single application mode. */
public static final int MODE_SINGLE_APP = 0;
/** MCUboot is in swap using scratch partition mode. */
public static final int MODE_SWAP_SCRATCH = 1;
/** MCUboot is in overwrite (upgrade-only) mode. */
public static final int MODE_SWAP_OVERWRITE_ONLY = 2;
public static final int MODE_OVERWRITE_ONLY = 2;
/** MCUboot is in swap without scratch mode. */
public static final int MODE_SWAP_WITHOUT_SCRATCH = 3;
/** MCUboot is in DirectXIP without revert mode. */
Expand All @@ -36,6 +38,15 @@ public class McuMgrBootloaderInfoResponse extends McuMgrOsResponse {
@JsonProperty("mode")
public int mode = MODE_UNKNOWN;

/**
* The "no-downgrade" is a flag, indicating that mode has downgrade prevention enabled;
* downgrade prevention means that if uploaded image has lower version than running,
* it will not be taken for execution by MCUboot.
* MCUmgr may reject image with lower version in that MCUboot configuration.
*/
@JsonProperty("no-downgrade")
public boolean noDowngrade = false;

@JsonProperty("bootloader")
public String bootloader;

Expand Down
14 changes: 11 additions & 3 deletions moustache/README.mo
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ dfuManager.setWindowUploadCapacity(mcumgrBuffers);
// devices. Each packet sent will be trimmed to have number of bytes dividable by given value.
// Since NCS 1.9 the flash implementation can buffer unaligned data instead of discarding.
dfuManager.setMemoryAlignment(memoryAlignment);
// Set a mode: Confirm only, Test only, or Test & Confirm. For multi-core update only the first is
// supported. See details below.
// Set a mode: Confirm only, Test only, Test & Confirm or None.
// For multi-core update only the first one is supported. See details below.
dfuManager.setMode(mode);

// Start the firmware upgrade with the image data.
Expand Down Expand Up @@ -152,10 +152,18 @@ The different firmware upgrade modes are as follows:
confirming it manually as the primary boot image.
This mode is recommended for devices that do not support reverting images, i.e. multi core devices.
The process for this mode is `UPLOAD`, `TEST`, `RESET`.
* **`NONE`**: This mode should be used if the bootloader does not support reverting images.
The process for this mode is `UPLOAD`, `RESET`. If the device supports bootloader information
command, and the bootloader is in DirectXIP without revert mode, this mode will be selected
automatically. This mode was added in library version 1.8.

> Note: Devices based on nRF5340 SoC support only `CONFIRM_ONLY` mode because the image from the
> **Note**
> Devices based on nRF5340 SoC support only `CONFIRM_ONLY` mode because the image from the
Network Core cannot be read from the Application Core, making it impossible to temporarily save it.

> **Note**
> Read about MCUboot modes [here](https://docs.mcuboot.com/design.html#image-slots).

### Firmware Upgrade State

`FirmwareUpgradeManager` acts as a simple, mostly linear state machine which is determined by the `Mode`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public void onSaveInstanceState(@NonNull final Bundle outState) {

private FirmwareUpgradeManager.Mode getMode() {
switch (selectedItem) {
case 3:
return FirmwareUpgradeManager.Mode.NONE;
case 2:
return FirmwareUpgradeManager.Mode.CONFIRM_ONLY;
case 1:
Expand Down
3 changes: 2 additions & 1 deletion sample/src/main/res/values/strings_image_upgrade.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@

<string name="image_upgrade_mode">Select Mode</string>
<string-array name="image_upgrade_mode_options">
<item>Test and confirm</item>
<item>Test and Confirm</item>
<item>Test only</item>
<item>Confirm only</item>
<item>No Revert</item>
</string-array>

<string name="image_upgrade_status_ready">READY</string>
Expand Down