Zephyr

Over-the-air (OTA) updates with Zephyr

Enable and customize Spotflow OTA updates on Zephyr devices.

This guide explains how to set up the Spotflow device module to automatically perform over-the-air (OTA) updates of the firmware that runs the module itself (the main firmware). If you want to update firmware on external MCUs connected to your Zephyr device, see Over-the-air (OTA) updates of external MCUs with Zephyr.

Prerequisites

Before you start integrating your device with Spotflow, make sure you:

  • have development environment with Zephyr 3.7, 4.1, 4.2, 4.3, or 4.4,
  • have established a connection to the internet from the device.

When creating a new west workspace, make sure to use a supported Zephyr version, for example, using west init --mr v4.3.0. By default, west init checks out the main branch, which may contain changes breaking the build.

Alternatively, you can follow the Quickstart: Zephyr Integration Guide to integrate Spotflow using sample application, instead of adding the spotflow module to an existing project.

Before you start integrating your device with Spotflow, make sure you:

  • have development environment with nRF Connect SDK v3.0.0 or later (v3.2.4 recommended),
  • have established a connection to the internet from the device.

Alternatively, you can follow the Nordic nRF Connect SDK Integration Guide to integrate Spotflow using sample application, instead of adding the spotflow module to an existing project.

OTA updates additionally require:

  • A persistent Zephyr settings backend (typically NVS) so that the update state and results survive reboot.
  • MCUboot enabled through Zephyr sysbuild.
  • A flash partition layout with an MCUboot secondary (update) slot.
  • Zephyr binary descriptors so that the Spotflow build ID can be embedded in the firmware image. See the dependencies of CONFIG_BINDESC.

Install Spotflow Device Module

Add the spotflow module as a west dependency to your west.yml file.

west.yml
manifest:
    projects:
    - name: spotflow
        revision: main
        path: modules/lib/spotflow
        url: https://github.com/spotflow-io/device-sdk

Then, install the west dependencies using the following command:

west update

Enable OTA updates in Kconfig

Add the following to your prj.conf:

prj.conf
CONFIG_SPOTFLOW=y
CONFIG_SPOTFLOW_DEVICE_ID="zephyr-device-001"
CONFIG_SPOTFLOW_INGEST_KEY="{your-ingest-key}"

CONFIG_SPOTFLOW_OTA=y

# Persistent storage for OTA attempt state and results
CONFIG_NVS=y
CONFIG_SETTINGS_NVS=y

Additionally, enable the MCUboot bootloader in your sysbuild.conf file:

sysbuild.conf
SB_CONFIG_BOOTLOADER_MCUBOOT=y

Confirm main firmware image after reboot

After MCUboot starts the new image in test mode, the image remains unconfirmed. Your application must validate that the new firmware works correctly and then call spotflow_confirm_main_firmware_image():

#include <zephyr/sys/reboot.h>
#include <spotflow/ota.h>

static void confirm_if_main_firmware_is_valid(void)
{
    struct spotflow_ota_main_firmware_state state;

    if (spotflow_get_main_firmware_update_state(&state) < 0) {
        return;
    }

    if (state.phase != SPOTFLOW_OTA_PHASE_UNCONFIRMED) {
        return;
    }

    /* Run your own validation first: hardware checks, self-tests, connectivity, etc. */
    if (!application_self_test_passed()) {
        /* Rebooting without confirmation makes MCUboot roll back to the previous image. */
        sys_reboot(SYS_REBOOT_COLD);
    }

    spotflow_confirm_main_firmware_image(&state);
}

When spotflow_confirm_main_firmware_image() is called, the module confirms the image with MCUboot, stores the installed firmware version, and reports success to Spotflow. If the image is not confirmed before the next reboot, MCUboot rolls back to the previous image and the module reports the main firmware update as failed.

Build and flash firmware

Build with sysbuild so the firmware image is signed for MCUboot and the bootloader is included in the build:

west build --sysbuild --board <board> <your-app> --pristine --build-dir build-v1

Flash the first version to the device together with MCUboot:

west flash --build-dir build-v1

Then prepare the version you want to deploy remotely and build it:

west build --sysbuild -b <board> <your-app> --pristine -d build-v2

Deploy OTA update

Once the device is online and connected to Spotflow MQTT, create a deployment in the portal as described in Deploy Over-the-Air (OTA) Updates. For the firmware image, use the signed image from the second build. It should be located in the following path:

<your-app>/build-v2/<your-app>/zephyr/zephyr.signed.bin

When you start the deployment, the Spotflow device module will download the firmware image to the secondary slot, schedule the new image to run once on the next reboot (while keeping the current image as a fallback), and reboot. After reboot, MCUboot will swap the images and the device will run the new firmware. After your code calls spotflow_confirm_main_firmware_image(), the module will make the image permanent with MCUboot, store the installed firmware version, and report success to Spotflow. If the new firmware is not confirmed before the next reboot, MCUboot will roll back to the previous image and the module will report the update as failed.

Advanced: Customize main firmware update handling

Although the device module handles the main firmware update automatically, your application code can observe and influence its progress. The main firmware update can be in one of the following phases:

  • NOT_RUNNING: No main firmware update in progress.
  • PENDING_DOWNLOAD: Download of the main firmware image is ready to start.
  • DOWNLOADING: Downloading the image into the MCUboot update slot.
  • PENDING_UPGRADE: The image has been downloaded and the module is about to perform the MCUboot test upgrade.
  • PENDING_REBOOT: The test upgrade has been requested and the device is about to reboot.
  • UNCONFIRMED: Device rebooted into the new image; awaiting confirmation.

Progress observation

Define the function spotflow_on_main_firmware_update_progressed() to be informed when the phase changes:

void spotflow_on_main_firmware_update_progressed(const struct spotflow_ota_main_firmware_state *state)
{
    /* React to state->phase and state->result. For example, update UI, logs, or LEDs. */
}

When the phase changes to NOT_RUNNING, the module has finished the current update attempt and state->result contains its result.

Pausing and resuming

In many real-world applications, the device might not be ready to update firmware immediately. Because spotflow_on_main_firmware_update_progressed() is called on the OTA worker thread, blocking its execution effectively postpones the update:

void spotflow_on_main_firmware_update_progressed(const struct spotflow_ota_main_firmware_state *state)
{
    if (state->phase == SPOTFLOW_OTA_PHASE_PENDING_REBOOT) {
        LOG_INF("Device will reboot in 10 seconds to finish the firmware update.");
        k_sleep(K_SECONDS(10));
    }
}

While this approach works for simple cases, it can be cumbersome in more complex scenarios, such as when the device needs confirmation from the user before continuing. You can use spotflow_pause_main_firmware_update() and spotflow_resume_main_firmware_update() for this purpose:

void spotflow_on_main_firmware_update_progressed(const struct spotflow_ota_main_firmware_state *state)
{
    if (state->phase == SPOTFLOW_OTA_PHASE_PENDING_DOWNLOAD) {
        LOG_INF("New firmware update is ready to be downloaded. Press A to perform the update.");
        spotflow_pause_main_firmware_update(NULL);
    }
}

void on_button_a_pressed(void)
{
    struct spotflow_ota_main_firmware_state state;
    if (spotflow_get_main_firmware_update_state(&state) < 0) {
        return;
    }

    if (!state.is_paused) {
        return;
    }

    if (spotflow_resume_main_firmware_update(&state) < 0) {
        return;
    }

    LOG_INF("Proceeding with the firmware update.");
}

Pausing an update effectively puts the OTA worker thread to sleep in its current phase until it is resumed. While pausing an update in spotflow_on_main_firmware_update_progressed() is most common, you can do it from any thread.

When your application pauses an update in the phase DOWNLOADING, the download will stop after the next firmware image block is received. After the update is resumed, the download will resume from the point where it was paused.

This feature is useful, for example, when the device does not have stable network connectivity and needs to prioritize other communication when the signal is low.

An update cannot be paused in the phase UNCONFIRMED, because it is not under the Spotflow device module's control when your application confirms the image.

Aborting

In certain cases, you might want to allow your application to completely reject the update instead of just pausing it. You can do this by calling spotflow_abort_main_firmware_update(). This aborts the update and reports the main firmware update as failed.

The function spotflow_abort_main_firmware_update() cannot be used in the phases PENDING_REBOOT and UNCONFIRMED. The only way to fail an update in these phases is to force a rollback by rebooting in the UNCONFIRMED phase.

How device module works

The Spotflow device module communicates with the cloud using CBOR messages over MQTT. See Over-the-air (OTA) updates with MQTT for the protocol details. In order to store state across reboots, the module persists the results of the latest update attempt, installed versions of each artifact, and main firmware probation information.

Whenever it receives a new update attempt, the device module processes each firmware update in the manifest as follows:

  • If the installed version for an artifact slug already matches the manifest version, the module immediately reports success.
  • If the firmware is not main or CONFIG_SPOTFLOW_OTA_AUTO_HANDLE_MAIN_FIRMWARE is disabled, the module passes it to spotflow_on_handle_firmware_update() and reports the result to the cloud. This guide explains how to use this API to handle firmware updates of external MCUs.
  • If the firmware is main and automatic handling is enabled, the module downloads the image over HTTPS into the MCUboot secondary slot, persists the expected build ID, requests a test upgrade, and reboots. On the next boot, it compares the running image build ID with the expected build ID:
    • If they match, the module waits in UNCONFIRMED until your application confirms the image. After confirmation, the module reports the main firmware update as succeeded.
    • If they do not match (because of a rollback or an unsuccessful swap), the module reports the main firmware update as failed.
  • When the firmware update does not succeed, the module cancels the updates of the remaining artifacts in the attempt.

When OTA updates are enabled, the module also adds lastUpdateAttemptId to MQTT session metadata so that Spotflow can detect, on a best-effort basis, firmware changes made outside OTA.

See the implementation notes in the GitHub repository for more details.

Threading

The MQTT processing thread handles the communication with the cloud and a dedicated OTA worker thread handles most of the work, such as downloading artifacts. Callbacks run on the following threads:

CallbackThread
spotflow_on_handle_firmware_update()OTA worker
spotflow_on_main_firmware_update_progressed()OTA worker
spotflow_on_update_canceled()Zephyr system workqueue

Prefer not to perform blocking work in spotflow_on_update_canceled() unless you are sure that it will not negatively impact other work items in the system workqueue.

Security

Artifacts are downloaded over HTTPS using the same TLS configuration as the Spotflow MQTT connection. Along with the URL, the device receives an OTA secret for each artifact and uses it in the HTTP Authorization header.

When handling firmware updates manually, do not log their URLs or secrets to reduce the risk of accidental exposure.

Kconfig options

Summary of OTA Kconfig options:

OptionDefaultPurpose
CONFIG_SPOTFLOW_OTA_AUTO_HANDLE_MAIN_FIRMWAREyAutomatically handle main firmware OTA updates
CONFIG_SPOTFLOW_OTA_MAX_ARTIFACTS4Maximum number of firmware images in an OTA update attempt
CONFIG_SPOTFLOW_OTA_THREAD_STACK_SIZE6144OTA worker thread stack (includes TLS/HTTP download)
CONFIG_SPOTFLOW_OTA_HTTP_TIMEOUT_MS120000Per-request HTTP timeout
CONFIG_SPOTFLOW_OTA_DOWNLOAD_BUFFER_SIZEimage writer buffer sizeHTTP receive buffer
CONFIG_SPOTFLOW_OTA_DOWNLOAD_RETRY_DELAY_MS5000Delay before retrying transient download errors

Reference repository materials

Learn more

How is this guide?