Over-the-air (OTA) updates with MQTT
Implement the Spotflow OTA MQTT protocol on devices that use a custom MQTT client.
This guide explains how a device can participate in Spotflow over-the-air (OTA) updates using MQTT directly, without using the Spotflow device module.
For basic concepts and deployment workflows in the web application, see Over-the-air (OTA) updates and Deploy over-the-air (OTA) updates.
Connect to the Spotflow MQTT broker
As the first step, connect to the Spotflow MQTT broker:
OTA updates use dedicated MQTT topics. Choose JSON or CBOR and use the matching topic names:
| Direction | JSON topic | CBOR topic | QoS |
|---|---|---|---|
| Cloud to device | ota-json-c2d | ota-cbor-c2d | 1 |
| Device to cloud | ota-json-d2c | ota-cbor-d2c | 0 or 1 |
Subscribe to the cloud-to-device topic for your chosen format. The broker routes the
subscription to the device-specific topic, such as
ota-cbor-c2d/{workspaceId}/{deviceId}.
CBOR payloads use numeric property keys.
Handle UPDATE_ARTIFACTS
The cloud sends the message UPDATE_ARTIFACTS when the device should perform an OTA update:
{
"messageType": "UPDATE_ARTIFACTS",
"updateAttemptId": 123,
"isCanceled": false,
"manifest": [
{
"artifactType": "FIRMWARE",
"slug": "main",
"isMain": true,
"url": "https://api.spotflow.io/.../content",
"secret": "artifact-download-secret",
"version": "2.0.0"
}
]
}update-artifacts = {
0 => 6, ; messageType: UPDATE_ARTIFACTS
32 => uint, ; updateAttemptId, non-zero 64-bit unsigned integer
? 33 => bool, ; isCanceled, defaults to false
34 => [* artifact]
}
artifact = {
35 => 0, ; artifactType: FIRMWARE
36 => tstr .size (1..32), ; slug
? 37 => bool, ; isMain, defaults to false
38 => tstr .size (1..117), ; url
39 => tstr .size (1..24), ; secret
40 => tstr .size (1..64) ; version
}The update attempt is uniquely identified by a non-zero updateAttemptId.
Each artifact in the manifest contains the following properties:
artifactType: Spotflow currently supports only firmware updates. Other kinds of artifacts will be added in the future.slug: The unique identifier of the firmware, such assample-firmware.isMain: Whether the firmware is meant to update the main MCU on the device. The Spotflow device module handles main firmware in a special way, but your custom implementation might not need it.url: The URL of the firmware image to download.secret: Use this secret in the HTTP headerAuthorization: OtaSecret <secret>when downloading the firmware image.version: The firmware version. Your implementation might persist the latest installed version for each firmware slug to avoid unnecessary downloads.
Publish UPDATE_RESULTS
After each firmware update, publish the message UPDATE_RESULTS to the device-to-cloud topic for your chosen format:
Publish JSON UPDATE_RESULTS messages to ota-json-d2c:
{
"messageType": "UPDATE_RESULTS",
"updateAttemptId": 123,
"succeeded": [0],
"failed": [],
"canceled": []
}If an error prevents the device from handling the attempt before any artifact is processed, send updateAttemptError instead of artifact result arrays:
{
"messageType": "UPDATE_RESULTS",
"updateAttemptId": 123,
"updateAttemptError": "CANNOT_PARSE_MESSAGE"
}The allowed values of updateAttemptError are UNKNOWN_ERROR, ARTIFACT_COUNT_EXCEEDED, UNKNOWN_ARTIFACT_TYPE, and CANNOT_PARSE_MESSAGE.
Publish CBOR UPDATE_RESULTS messages to ota-cbor-d2c:
update-results = {
0 => 9, ; messageType: UPDATE_RESULTS
32 => uint, ; updateAttemptId
? 45 => uint, ; updateAttemptError
? 42 => [* uint], ; succeeded artifact indexes
? 43 => [* uint], ; failed artifact indexes
? 44 => [* uint] ; canceled artifact indexes
}If an error prevents the device from handling the attempt before any artifact is processed, set updateAttemptError instead of artifact result arrays.
The allowed values of updateAttemptError are UNKNOWN_ERROR (0), ARTIFACT_COUNT_EXCEEDED (1), UNKNOWN_ARTIFACT_TYPE (2), and CANNOT_PARSE_MESSAGE (3).
The arrays succeeded, failed, and canceled contain the indexes of the firmware updates in the manifest.
You can publish one result at a time or publish several known results together.
The Spotflow device module processes the firmware updates in the manifest order. If an update fails, the module reports the remaining unprocessed updates as canceled.
Your implementation might process the updates in a different order (even in parallel) and have different failure handling logic. Still, it must eventually report the results of all the updates in the manifest.
Handle REPORT_UPDATE_RESULTS
Because the device is allowed to publish UPDATE_RESULTS with QoS 0, this message might be lost in rare cases.
To ensure that update results are eventually delivered to the cloud, the cloud can ask the device to resend them.
The device should publish an UPDATE_RESULTS message with all firmware update results for the current attempt when it receives the message REPORT_UPDATE_RESULTS:
{
"messageType": "REPORT_UPDATE_RESULTS",
"updateAttemptId": 123
}report-update-results = {
0 => 8, ; messageType: REPORT_UPDATE_RESULTS
32 => uint ; updateAttemptId
}(Optional) Handle cancellation
The cloud can request cancellation in two ways:
- When the device has already received the update attempt in the current MQTT session, the cloud sends the message
CANCEL_UPDATE:
{
"messageType": "CANCEL_UPDATE",
"updateAttemptId": 123
}cancel-update = {
0 => 7, ; messageType: CANCEL_UPDATE
32 => uint ; updateAttemptId
}- When the device subscribes to the OTA cloud-to-device topic while the update attempt is already canceled, the cloud sends the message
UPDATE_ARTIFACTSwithisCanceled: true. This message still contains the full manifest in case the device wants to ignore the cancellation.
If your implementation supports cancellation, handle it by publishing UPDATE_RESULTS with a canceled array containing the indexes of firmware updates that were not completed.
Cancellation should be handled on a best-effort basis so that the device is not left in a half-updated state. For example, the Spotflow device module accepts cancellation only if no firmware update from the manifest has already finished.
(Optional) Extend session metadata with OTA update attempt ID
If your device supports OTA updates, include lastUpdateAttemptId in Session Metadata.
It lets Spotflow detect, on a best-effort basis, when firmware may have changed outside OTA updates.
Protocol guarantees
UPDATE_ARTIFACTSmust be the first cloud-to-device message for an attempt.CANCEL_UPDATEandREPORT_UPDATE_RESULTSonly make sense after the device has the attempt context.- Only one firmware with
isMainset totruecan be present in the manifest. - Until the cloud receives results for all the artifacts or an
updateAttemptError, it sends the sameUPDATE_ARTIFACTSmessage whenever the device subscribes to the OTA cloud-to-device topic.
There is an exception to this rule: When you delete the device's deployment cohort before the device finishes the current attempt and put the device in a new cohort, the cloud will not wait for the results of the current attempt before sending the new one.
However, your implementation might ignore this edge case if you do not delete deployment cohorts.
Learn more
Fundamentals: Over-the-air (OTA) updates
Guide: Deploy Over-the-Air (OTA) Updates
Guide: MQTT Session metadata
Guide: Over-the-air (OTA) updates with Zephyr
How is this guide?