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:
Device Registration and Management: Devices are registered on the Skycase platform, allowing centralized management and monitoring.
Update Package Creation: Developers create update packages containing firmware or software updates tailored to specific devices or groups.
Deployment Configuration: Tenant Admins can assign the OTA package to a device or device group.
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.
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.
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.