MQTT

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:

DirectionJSON topicCBOR topicQoS
Cloud to deviceota-json-c2dota-cbor-c2d1
Device to cloudota-json-d2cota-cbor-d2c0 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 as sample-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 header Authorization: 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_ARTIFACTS with isCanceled: 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_ARTIFACTS must be the first cloud-to-device message for an attempt. CANCEL_UPDATE and REPORT_UPDATE_RESULTS only make sense after the device has the attempt context.
  • Only one firmware with isMain set to true can be present in the manifest.
  • Until the cloud receives results for all the artifacts or an updateAttemptError, it sends the same UPDATE_ARTIFACTS message 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

How is this guide?