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.
manifest:
projects:
- name: spotflow
revision: main
path: modules/lib/spotflow
url: https://github.com/spotflow-io/device-sdkThen, install the west dependencies using the following command:
west updateEnable OTA updates in Kconfig
Add the following to your 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=yAdditionally, enable the MCUboot bootloader in your sysbuild.conf file:
SB_CONFIG_BOOTLOADER_MCUBOOT=yConfirm 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-v1Flash the first version to the device together with MCUboot:
west flash --build-dir build-v1Then prepare the version you want to deploy remotely and build it:
west build --sysbuild -b <board> <your-app> --pristine -d build-v2Deploy 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.binWhen 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_FIRMWAREis disabled, the module passes it tospotflow_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
UNCONFIRMEDuntil 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.
- If they match, the module waits in
- 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:
| Callback | Thread |
|---|---|
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:
| Option | Default | Purpose |
|---|---|---|
CONFIG_SPOTFLOW_OTA_AUTO_HANDLE_MAIN_FIRMWARE | y | Automatically handle main firmware OTA updates |
CONFIG_SPOTFLOW_OTA_MAX_ARTIFACTS | 4 | Maximum number of firmware images in an OTA update attempt |
CONFIG_SPOTFLOW_OTA_THREAD_STACK_SIZE | 6144 | OTA worker thread stack (includes TLS/HTTP download) |
CONFIG_SPOTFLOW_OTA_HTTP_TIMEOUT_MS | 120000 | Per-request HTTP timeout |
CONFIG_SPOTFLOW_OTA_DOWNLOAD_BUFFER_SIZE | image writer buffer size | HTTP receive buffer |
CONFIG_SPOTFLOW_OTA_DOWNLOAD_RETRY_DELAY_MS | 5000 | Delay before retrying transient download errors |
Reference repository materials
- Public API:
spotflow/ota.h,spotflow/downloader.h - OTA sample
- OTA Kconfig options
Learn more
Fundamentals: Over-the-air (OTA) updates
Guide: Deploy Over-the-Air (OTA) Updates
Guide: Over-the-air (OTA) updates of external MCUs with Zephyr
Guide: Over-the-air (OTA) updates with MQTT
How is this guide?