SkyCase OTA Updates API Guide

This guide provides instructions on how to perform Over-The-Air (OTA) updates using the SkyCase IoT Cloud Platform. The OTA updates can be performed using both MQTT and HTTP APIs.

Overview

OTA updates allow you to remotely update firmware and software on IoT devices connected to SkyCase. This guide covers both MQTT and HTTP APIs for initiating and monitoring OTA updates.

OTA Update Process

The OTA update process with Skycase involves several key steps:

  1. Device Registration and Management: Devices are registered on the Skycase platform, allowing centralized management and monitoring.

  2. Update Package Creation: Developers create update packages containing firmware or software updates tailored to specific devices or groups.

  3. Deployment Configuration: Tenant Admins can assign the OTA package to a device or device group.

  4. Verification and Update: Post-update, Skycase IoT devices verify and install the OTA package. After the installation, the IoT device will send the OTA update status to the cloud.

Benefits of Using Skycase for OTA Updates

  • Security: Encrypted communications and secure protocols ensure data integrity during updates.

  • Efficiency: Centralized management reduces operational overhead and ensures updates are timely and consistent across all devices.

  • Flexibility: Supports a wide range of IoT devices and protocols, accommodating diverse deployment scenarios.

  • Scalability: Easily scales from a few devices to thousands, maintaining performance and reliability.

MQTT OTA Updates API

The MQTT OTA Updates API allows you to manage OTA updates programmatically over MQTT.

  1. Example MQTT Usage:

  • In this example FirmwareClient class is designed to handle OTA firmware updates over MQTT.

      1from paho.mqtt.client import Client
      2from time import sleep, time
      3from json import dumps, loads
      4from zlib import crc32
      5from hashlib import sha256, sha384, sha512, md5
      6from mmh3 import hash, hash128
      7from math import ceil
      8from threading import Thread
      9from random import randint
     10
     11FW_CHECKSUM_ATTR = "fw_checksum"
     12FW_CHECKSUM_ALG_ATTR = "fw_checksum_algorithm"
     13FW_SIZE_ATTR = "fw_size"
     14FW_TITLE_ATTR = "fw_title"
     15FW_VERSION_ATTR = "fw_version"
     16
     17FW_STATE_ATTR = "fw_state"
     18
     19REQUIRED_SHARED_KEYS = f"{FW_CHECKSUM_ATTR},{FW_CHECKSUM_ALG_ATTR},{FW_SIZE_ATTR},{FW_TITLE_ATTR},{FW_VERSION_ATTR}"
     20
     21def collect_required_data():
     22    config = {}
     23    print("\n\n", "="*80, sep="")
     24    print(" "*20, "Skycase getting firmware example script.", sep="")
     25    print("="*80, "\n\n", sep="")
     26    host = input("Please write your Skycase host or leave it blank to use default (localhost): ")
     27    config["host"] = host if host else "localhost"
     28    host = input("Please write your Skycase port or leave it blank to use default (1883): ")
     29    config["port"] = host if host else 1883
     30    token = ""
     31    while not token:
     32        token = input("Please write accessToken for device: ")
     33        if not token:
     34            print("Access token is required!")
     35    config["token"] = token
     36    chunk_size = input("Please write firmware chunk size in bytes or leave it blank to get all firmware by request: ")
     37    config["chunk_size"] = int(chunk_size) if chunk_size else 0
     38    print("\n", "="*80, "\n", sep="")
     39    return config
     40
     41def verify_checksum(firmware_data, checksum_alg, checksum):
     42    if firmware_data is None:
     43        print("Firmware wasn't received!")
     44        return False
     45    if checksum is None:
     46        print("Checksum was't provided!")
     47        return False
     48    checksum_of_received_firmware = None
     49    print(f"Checksum algorithm is: {checksum_alg}")
     50    if checksum_alg.lower() == "sha256":
     51        checksum_of_received_firmware = sha256(firmware_data).digest().hex()
     52    elif checksum_alg.lower() == "sha384":
     53        checksum_of_received_firmware = sha384(firmware_data).digest().hex()
     54    elif checksum_alg.lower() == "sha512":
     55        checksum_of_received_firmware = sha512(firmware_data).digest().hex()
     56    elif checksum_alg.lower() == "md5":
     57        checksum_of_received_firmware = md5(firmware_data).digest().hex()
     58    elif checksum_alg.lower() == "murmur3_32":
     59        reversed_checksum = f'{hash(firmware_data, signed=False):0>2X}'
     60        if len(reversed_checksum) % 2 != 0:
     61            reversed_checksum = '0' + reversed_checksum
     62        checksum_of_received_firmware = "".join(reversed([reversed_checksum[i:i+2] for i in range(0, len(reversed_checksum), 2)])).lower()
     63    elif checksum_alg.lower() == "murmur3_128":
     64        reversed_checksum = f'{hash128(firmware_data, signed=False):0>2X}'
     65        if len(reversed_checksum) % 2 != 0:
     66            reversed_checksum = '0' + reversed_checksum
     67        checksum_of_received_firmware = "".join(reversed([reversed_checksum[i:i+2] for i in range(0, len(reversed_checksum), 2)])).lower()
     68    elif checksum_alg.lower() == "crc32":
     69        reversed_checksum = f'{crc32(firmware_data) & 0xffffffff:0>2X}'
     70        if len(reversed_checksum) % 2 != 0:
     71            reversed_checksum = '0' + reversed_checksum
     72        checksum_of_received_firmware = "".join(reversed([reversed_checksum[i:i+2] for i in range(0, len(reversed_checksum), 2)])).lower()
     73    else:
     74        print("Client error. Unsupported checksum algorithm.")
     75    print(checksum_of_received_firmware)
     76    random_value = randint(0, 5)
     77    if random_value > 3:
     78        print("Dummy fail! Do not panic, just restart and try again the chance of this fail is ~20%")
     79        return False
     80    return checksum_of_received_firmware == checksum
     81
     82
     83def dummy_upgrade(version_from, version_to):
     84    print(f"Updating from {version_from} to {version_to}:")
     85    for x in range(5):
     86        sleep(1)
     87        print(20*(x+1),"%", sep="")
     88    print(f"Firmware is updated!\n Current firmware version is: {version_to}")
     89
     90
     91class FirmwareClient(Client):
     92    """
     93      FirmwareClient class handles OTA firmware updates over MQTT.
     94
     95      Attributes:
     96          chunk_size (int): Size of the firmware chunks to be requested.
     97          ota_request_id (str): The request ID for the current OTA update.
     98          received_firmware (bytes): The received firmware data.
     99          firmware_size (int): The size of the firmware to be downloaded.
    100          ota_in_progress (bool): Indicates if an OTA update is in progress.
    101          current_chunk (int): The current chunk number being processed.
    102          on_connect (function): Callback when the client connects to the broker.
    103          on_message (function): Callback when a message is received from the broker.
    104          current_firmware_info (dict): Information about the current firmware.
    105          firmware_data (dict): Data related to the firmware.
    106          firmware_received (bool): Indicates if the firmware has been fully received.
    107          firmware_info (dict): Information about the firmware.
    108    """
    109    def __init__(self, chunk_size = 0):
    110        """
    111            Initialize the FirmwareClient.
    112
    113            Parameters:
    114              chunk_size (int): Size of the firmware chunks to be requested. Default is 0 (request all at once).
    115        """
    116        super().__init__()
    117        self.on_connect = self.__on_connect
    118        self.on_message = self.__on_message
    119        self.__chunk_size = chunk_size
    120
    121        self.__request_id = 0
    122        self.__firmware_request_id = 0
    123
    124        self.current_firmware_info = {
    125            "current_" + FW_TITLE_ATTR: "Initial",
    126            "current_" + FW_VERSION_ATTR: "v0"
    127            }
    128        self.firmware_data = b''
    129        self.__target_firmware_length = 0
    130        self.__chunk_count = 0
    131        self.__current_chunk = 0
    132        self.firmware_received = False
    133        self.__updating_thread = Thread(target=self.__update_thread, name="Updating thread")
    134        self.__updating_thread.daemon = True
    135        self.__updating_thread.start()
    136
    137    def __on_connect(self, client, userdata, flags, result_code, *extra_params):
    138        """Callback when the client connects to the broker."""
    139        print(f"Requesting firmware info from {config['host']}:{config['port']}..")
    140        self.subscribe("v1/devices/me/attributes/response/+")
    141        self.subscribe("v1/devices/me/attributes")
    142        self.subscribe("v2/fw/response/+")
    143        self.send_telemetry(self.current_firmware_info)
    144        self.request_firmware_info()
    145
    146    def __on_message(self, client, userdata, msg):
    147        """
    148          Callback when a message is received from the broker.
    149
    150          Parameters:
    151              msg: An instance of MQTTMessage, which contains topic, payload, qos, retain.
    152        """
    153        update_response_pattern = "v2/fw/response/" + str(self.__firmware_request_id) + "/chunk/"
    154        if msg.topic.startswith("v1/devices/me/attributes"):
    155            self.firmware_info = loads(msg.payload)
    156            if "/response/" in msg.topic:
    157                self.firmware_info = self.firmware_info.get("shared", {}) if isinstance(self.firmware_info, dict) else {}
    158            if (self.firmware_info.get(FW_VERSION_ATTR) is not None and self.firmware_info.get(FW_VERSION_ATTR) != self.current_firmware_info.get("current_" + FW_VERSION_ATTR)) or \
    159                    (self.firmware_info.get(FW_TITLE_ATTR) is not None and self.firmware_info.get(FW_TITLE_ATTR) != self.current_firmware_info.get("current_" + FW_TITLE_ATTR)):
    160                print("Firmware is not the same")
    161                self.firmware_data = b''
    162                self.__current_chunk = 0
    163
    164                self.current_firmware_info[FW_STATE_ATTR] = "DOWNLOADING"
    165                self.send_telemetry(self.current_firmware_info)
    166                sleep(1)
    167
    168                self.__firmware_request_id = self.__firmware_request_id + 1
    169                self.__target_firmware_length = self.firmware_info[FW_SIZE_ATTR]
    170                self.__chunk_count = 0 if not self.__chunk_size else ceil(self.firmware_info[FW_SIZE_ATTR]/self.__chunk_size)
    171                self.get_firmware()
    172        elif msg.topic.startswith(update_response_pattern):
    173            firmware_data = msg.payload
    174
    175            self.firmware_data = self.firmware_data + firmware_data
    176            self.__current_chunk = self.__current_chunk + 1
    177
    178            print(f'Getting chunk with number: {self.__current_chunk}. Chunk size is : {self.__chunk_size} byte(s).')
    179
    180            if len(self.firmware_data) == self.__target_firmware_length:
    181                self.process_firmware()
    182            else:
    183                self.get_firmware()
    184
    185    def process_firmware(self):
    186        self.current_firmware_info[FW_STATE_ATTR] = "DOWNLOADED"
    187        self.send_telemetry(self.current_firmware_info)
    188        sleep(1)
    189
    190        verification_result = verify_checksum(self.firmware_data, self.firmware_info.get(FW_CHECKSUM_ALG_ATTR), self.firmware_info.get(FW_CHECKSUM_ATTR))
    191
    192        if verification_result:
    193            print("Checksum verified!")
    194            self.current_firmware_info[FW_STATE_ATTR] = "VERIFIED"
    195            self.send_telemetry(self.current_firmware_info)
    196            sleep(1)
    197        else:
    198            print("Checksum verification failed!")
    199            self.current_firmware_info[FW_STATE_ATTR] = "FAILED"
    200            self.send_telemetry(self.current_firmware_info)
    201            self.request_firmware_info()
    202            return
    203        self.firmware_received = True
    204
    205    def get_firmware(self):
    206        payload = '' if not self.__chunk_size or self.__chunk_size > self.firmware_info.get(FW_SIZE_ATTR, 0) else str(self.__chunk_size).encode()
    207        self.publish(f"v2/fw/request/{self.__firmware_request_id}/chunk/{self.__current_chunk}", payload=payload, qos=1)
    208
    209    def send_telemetry(self, telemetry):
    210        return self.publish("v1/devices/me/telemetry", dumps(telemetry), qos=1)
    211
    212    def request_firmware_info(self):
    213        self.__request_id = self.__request_id + 1
    214        self.publish(f"v1/devices/me/attributes/request/{self.__request_id}", dumps({"sharedKeys": REQUIRED_SHARED_KEYS}))
    215
    216    def __update_thread(self):
    217        while True:
    218            if self.firmware_received:
    219                self.current_firmware_info[FW_STATE_ATTR] = "UPDATING"
    220                self.send_telemetry(self.current_firmware_info)
    221                sleep(1)
    222
    223                with open(self.firmware_info.get(FW_TITLE_ATTR), "wb") as firmware_file:
    224                    firmware_file.write(self.firmware_data)
    225
    226                dummy_upgrade(self.current_firmware_info["current_" + FW_VERSION_ATTR], self.firmware_info.get(FW_VERSION_ATTR))
    227
    228                self.current_firmware_info = {
    229                    "current_" + FW_TITLE_ATTR: self.firmware_info.get(FW_TITLE_ATTR),
    230                    "current_" + FW_VERSION_ATTR: self.firmware_info.get(FW_VERSION_ATTR),
    231                    FW_STATE_ATTR: "UPDATED"
    232                }
    233                self.send_telemetry(self.current_firmware_info)
    234                self.firmware_received = False
    235                sleep(1)
    236
    237
    238if __name__ == '__main__':
    239    config = collect_required_data()
    240
    241    client = FirmwareClient(config["chunk_size"])
    242    client.username_pw_set(config["token"])
    243    client.connect(config["host"], config["port"])
    244    client.loop_forever()
    

HTTP OTA Updates API

The HTTP OTA Updates API allows you to initiate and monitor OTA updates via HTTP requests.

  1. Example HTTP Request:

      1from requests import get, post
      2from time import sleep
      3from zlib import crc32
      4from hashlib import sha256, sha384, sha512, md5
      5from mmh3 import hash, hash128
      6from math import ceil
      7from random import randint
      8
      9
     10FW_CHECKSUM_ATTR = "fw_checksum"
     11FW_CHECKSUM_ALG_ATTR = "fw_checksum_algorithm"
     12FW_SIZE_ATTR = "fw_size"
     13FW_TITLE_ATTR = "fw_title"
     14FW_VERSION_ATTR = "fw_version"
     15
     16FW_STATE_ATTR = "fw_state"
     17
     18REQUIRED_SHARED_KEYS = [FW_CHECKSUM_ATTR, FW_CHECKSUM_ALG_ATTR, FW_SIZE_ATTR, FW_TITLE_ATTR, FW_VERSION_ATTR]
     19
     20
     21def collect_required_data():
     22    config = {}
     23    print("\n\n", "="*80, sep="")
     24    print(" "*20, "Skycase getting firmware example script.", sep="")
     25    print("="*80, "\n\n", sep="")
     26    host = input("Please write your Skycase host or leave it blank to use default (localhost): ")
     27    config["host"] = host if host else "localhost"
     28    port = input("Please write your Skycase port or leave it blank to use default (8080): ")
     29    config["port"] = port if port else 8080
     30    token = ""
     31    while not token:
     32        token = input("Please write accessToken for device: ")
     33        if not token:
     34            print("Access token is required!")
     35    config["token"] = token
     36    chunk_size = input("Please write firmware chunk size in bytes or leave it blank to get all firmware by request: ")
     37    config["chunk_size"] = int(chunk_size) if chunk_size else 0
     38    print("\n", "="*80, "\n", sep="")
     39    return config
     40
     41
     42def send_telemetry(telemetry):
     43    print(f"Sending current info: {telemetry}")
     44    post(f"http://{config['host']}:{config['port']}/api/v1/{config['token']}/telemetry",json=telemetry)
     45
     46
     47def get_firmware_info():
     48    response = get(f"http://{config['host']}:{config['port']}/api/v1/{config['token']}/attributes", params={"sharedKeys": REQUIRED_SHARED_KEYS}).json()
     49    return response.get("shared", {})
     50
     51
     52def get_firmware(fw_info):
     53    chunk_count = ceil(fw_info.get(FW_SIZE_ATTR, 0)/config["chunk_size"]) if config["chunk_size"] > 0 else 0
     54    firmware_data = b''
     55    for chunk_number in range (chunk_count + 1):
     56        params = {"title": fw_info.get(FW_TITLE_ATTR),
     57                  "version": fw_info.get(FW_VERSION_ATTR),
     58                  "size": config["chunk_size"] if config["chunk_size"] < fw_info.get(FW_SIZE_ATTR, 0) else fw_info.get(FW_SIZE_ATTR, 0),
     59                  "chunk": chunk_number
     60                  }
     61        print(params)
     62        print(f'Getting chunk with number: {chunk_number + 1}. Chunk size is : {config["chunk_size"]} byte(s).')
     63        print(f"http{'s' if config['port'] == 443 else ''}://{config['host']}:{config['port']}/api/v1/{config['token']}/firmware", params)
     64        response = get(f"http{'s' if config['port'] == 443 else ''}://{config['host']}:{config['port']}/api/v1/{config['token']}/firmware", params=params)
     65        if response.status_code != 200:
     66            print("Received error:")
     67            response.raise_for_status()
     68            return
     69        firmware_data = firmware_data + response.content
     70    return firmware_data
     71
     72
     73def verify_checksum(firmware_data, checksum_alg, checksum):
     74    if firmware_data is None:
     75        print("Firmware wasn't received!")
     76        return False
     77    if checksum is None:
     78        print("Checksum was't provided!")
     79        return False
     80    checksum_of_received_firmware = None
     81    print(f"Checksum algorithm is: {checksum_alg}")
     82    if checksum_alg.lower() == "sha256":
     83        checksum_of_received_firmware = sha256(firmware_data).digest().hex()
     84    elif checksum_alg.lower() == "sha384":
     85        checksum_of_received_firmware = sha384(firmware_data).digest().hex()
     86    elif checksum_alg.lower() == "sha512":
     87        checksum_of_received_firmware = sha512(firmware_data).digest().hex()
     88    elif checksum_alg.lower() == "md5":
     89        checksum_of_received_firmware = md5(firmware_data).digest().hex()
     90    elif checksum_alg.lower() == "murmur3_32":
     91        reversed_checksum = f'{hash(firmware_data, signed=False):0>2X}'
     92        if len(reversed_checksum) % 2 != 0:
     93            reversed_checksum = '0' + reversed_checksum
     94        checksum_of_received_firmware = "".join(reversed([reversed_checksum[i:i+2] for i in range(0, len(reversed_checksum), 2)])).lower()
     95    elif checksum_alg.lower() == "murmur3_128":
     96        reversed_checksum = f'{hash128(firmware_data, signed=False):0>2X}'
     97        if len(reversed_checksum) % 2 != 0:
     98            reversed_checksum = '0' + reversed_checksum
     99        checksum_of_received_firmware = "".join(reversed([reversed_checksum[i:i+2] for i in range(0, len(reversed_checksum), 2)])).lower()
    100    elif checksum_alg.lower() == "crc32":
    101        reversed_checksum = f'{crc32(firmware_data) & 0xffffffff:0>2X}'
    102        if len(reversed_checksum) % 2 != 0:
    103            reversed_checksum = '0' + reversed_checksum
    104        checksum_of_received_firmware = "".join(reversed([reversed_checksum[i:i+2] for i in range(0, len(reversed_checksum), 2)])).lower()
    105    else:
    106        print("Client error. Unsupported checksum algorithm.")
    107    print(checksum_of_received_firmware)
    108    random_value = randint(0, 5)
    109    if random_value > 3:
    110        print("Dummy fail! Do not panic, just restart and try again the chance of this fail is ~20%")
    111        return False
    112    return checksum_of_received_firmware == checksum
    113
    114
    115def dummy_upgrade(version_from, version_to):
    116    print(f"Updating from {version_from} to {version_to}:")
    117    for x in range(5):
    118        sleep(1)
    119        print(20*x,"%", sep="")
    120    print(f"Firmware is updated!\n Current firmware version is: {version_to}")
    121
    122
    123if __name__ == '__main__':
    124    config = collect_required_data()
    125    current_firmware_info = {
    126        "current_fw_title": None,
    127        "current_fw_version": None
    128    }
    129    send_telemetry(current_firmware_info)
    130
    131    print(f"Getting firmware info from {config['host']}:{config['port']}..")
    132    while True:
    133
    134        firmware_info = get_firmware_info()
    135
    136        if (firmware_info.get(FW_VERSION_ATTR) is not None and firmware_info.get(FW_VERSION_ATTR) != current_firmware_info.get("current_" + FW_VERSION_ATTR)) \
    137                or (firmware_info.get(FW_TITLE_ATTR) is not None and firmware_info.get(FW_TITLE_ATTR) != current_firmware_info.get("current_" + FW_TITLE_ATTR)):
    138            print("New firmware available!")
    139
    140            current_firmware_info[FW_STATE_ATTR] = "DOWNLOADING"
    141            sleep(1)
    142            send_telemetry(current_firmware_info)
    143
    144            firmware_data = get_firmware(firmware_info)
    145
    146            current_firmware_info[FW_STATE_ATTR] = "DOWNLOADED"
    147            sleep(1)
    148            send_telemetry(current_firmware_info)
    149
    150            verification_result = verify_checksum(firmware_data, firmware_info.get(FW_CHECKSUM_ALG_ATTR), firmware_info.get(FW_CHECKSUM_ATTR))
    151
    152            if verification_result:
    153                print("Checksum verified!")
    154                current_firmware_info[FW_STATE_ATTR] = "VERIFIED"
    155                sleep(1)
    156                send_telemetry(current_firmware_info)
    157            else:
    158                print("Checksum verification failed!")
    159                current_firmware_info[FW_STATE_ATTR] = "FAILED"
    160                sleep(1)
    161                send_telemetry(current_firmware_info)
    162                firmware_data = get_firmware(firmware_info)
    163                continue
    164
    165            current_firmware_info[FW_STATE_ATTR] = "UPDATING"
    166            sleep(1)
    167            send_telemetry(current_firmware_info)
    168
    169            with open(firmware_info.get(FW_TITLE_ATTR), "wb") as firmware_file:
    170                firmware_file.write(firmware_data)
    171
    172            dummy_upgrade(current_firmware_info["current_" + FW_VERSION_ATTR], firmware_info.get(FW_VERSION_ATTR))
    173
    174            current_firmware_info = {
    175                "current_" + FW_TITLE_ATTR: firmware_info.get(FW_TITLE_ATTR),
    176                "current_" + FW_VERSION_ATTR: firmware_info.get(FW_VERSION_ATTR),
    177                FW_STATE_ATTR: "UPDATED"
    178            }
    179            sleep(1)
    180            send_telemetry(current_firmware_info)
    181        sleep(1)
    

Usage Notes

  • Ensure you have the correct device access token for authentication.

  • Verify the checksum of the received firmware to ensure data integrity.

  • Handle firmware updates carefully to avoid bricking the device.

  • Monitor OTA update progress using status messages received over MQTT or HTTP responses.

For further details and additional information, please refer to the SkyCase documentation or contact support.