event logo
Kaa Documentation

Kaa Over-the-Air (OTA) Implementation with Python

Overview

In this tutorial, we’ll cover the basics of OTA (Over-the-Air) updates and demonstrate how to implement them using Python. This simple implementation will help you set up Kaa OTA and understand how it works.

What Is OTA?

OTA stands for “Over-the-Air update”. It’s a method of delivering firmware updates to your device wirelessly without a requirement of a direct USB connection. This is particularly useful for updating multiple IoT devices simultaneously or when the devices are located in hard-to-reach places. OTA is a crucial feature for IoT devices with internet access.

How OTA Works with Kaa

The Kaa Platform hosts and distributes firmware files, which devices can automatically download and apply. In this tutorial, the device connects to the Kaa Platform using MQTT to check for available firmware updates. When a new firmware version is detected, the device downloads the necessary files. After the download is complete, it’s up to you to update the device version and determine how to handle the file in device code, as demonstrated in this tutorial.

Prerequisites

  1. You have a Kaa Cloud account.
  2. You should already know how to connect a device to the Kaa Platform.
    If not, refer to Getting Started.

Setting Up Kaa OTA

Creating Your First OTA Update

  1. Log in to your Kaa Cloud account and create a new application and endpoint. Save the application version and the token, you’ll need them later.
  2. In the side menu, navigate to “Device Management” -> “Software OTA”. This is where we will manage and host our firmware updates.
  3. This update won’t be automatically downloaded by devices. It will serve as the starting point for your chain of firmware versions.
  4. Set the “Semantic version number” to 1.0.0, leave the other fields as they are, and click “Create”.

Create initial firmware

The version 1.0.0 should always be installed manually on your device. That is why we didn’t upload anything for this version.

Setting Up the Initial 1.0.0 Version Locally

To begin, install the code below:

import paho.mqtt.client as mqtt
import json
import random 
import time
import requests

# MQTT server details
# Provide 'app_version' and 'token' with your values
app_version = "<your-app-version>"
token = "<your-endpoint-token>"
mqtt_server = "mqtt.cloud.kaaiot.com"
mqtt_port = 1883
request_id = 1

# Endpoint MQTT topics
publish_topic = f"kp1/{app_version}/dcx/{token}/json/{request_id}"
response_topic = f"kp1/{app_version}/dcx/{token}/json/{request_id}/status"
error_topic = f"kp1/{app_version}/dcx/{token}/json/{request_id}/error"

# Ota MQTT topics
ota_response_topic = f"kp1/{app_version}/cmx_ota/{token}/config/json/{request_id}/status"
ota_error_topic = f"kp1/{app_version}/cmx_ota/{token}/config/json/{request_id}/error"

# Firmware details
current_version = "1.0.0"
time_sleep = 5
file_name = "file.txt"

# Callback when connected to MQTT server
def on_connect(client, userdata, flags, rc):
    print("Connected with result code " + str(rc))
    client.subscribe(response_topic)
    client.subscribe(error_topic)
    client.subscribe(ota_response_topic)
    client.subscribe(ota_error_topic)

# Callback when a message is received
def on_message(client, userdata, msg):
    topic = msg.topic
    payload = msg.payload.decode()
    print(f"\n//-----Message received on topic {topic}: {payload}")
    print(f"Current firmware version: {current_version}\n")
    if f"kp1/{app_version}/cmx_ota/{token}" in topic:
        handle_update(payload)

    client_loop(client)
    print("-----//\n")

def handle_update(payload):
    try:
        # Parse the JSON payload
        update_data = json.loads(payload)
        print("Update Data:", update_data)

        # Confirm that the payload is valid
        status_code = update_data.get("statusCode")

        if status_code != 200:
            reason_phrase = update_data.get("reasonPhrase", "No reason provided")
            print(f"Firmware message's status code is not 200, but: {status_code}")
            print(f"Response: {reason_phrase}")
            return
        
        # Check if the current version is up-to-date
        new_version = update_data.get("configId")

        print(f"Current version: {current_version}")
        print(f"Latest version: {new_version}")

        if new_version == current_version:
            print("Current version is up to date. Skipping...")
            return

        # Extract the firmware download link
        system_file_link = update_data["config"]["system"].get("files", [None])[0]
        download_link = update_data["config"].get("link")
        firmware_link = system_file_link or download_link or ""

        print(f"firmwareLink: {firmware_link}")

        # Download the file
        download_and_apply_update(firmware_link, file_name, new_version)
    except json.JSONDecodeError:
        print("Failed to parse JSON payload.")
    except KeyError as e:
        print(f"Missing expected key: {e}")

# Download the new version of the file
def download_and_apply_update(script_url, save_path, new_version):
    response = requests.get(script_url)
    
    if response.status_code == 200:
        global current_version
        
        # Update version number
        print(f"Updated current version from {current_version} to {new_version}")
        current_version = new_version

        # Apply your custom changes here    
        # Just save the updated file
        with open(save_path, "wb") as file:
            file.write(response.content)
        print(f"Updated file downloaded to {save_path}")
    else:
        print("Failed to download the new script.")  

# Report current firmware version
def report_current_firmware_version():
    report_topic = f"kp1/{app_version}/cmx_ota/{token}/applied/json"
    report_payload = json.dumps({"configId": current_version})
    print(f"Reporting current firmware version on topic: {report_topic} and payload: {report_payload}")
    client.publish(report_topic, report_payload)

# Request new version from Kaa
def request_new_firmware():
    request_id = random.randint(0, 99)  # Generate a random request ID to avoid conflicts
    firmware_request_topic = f"kp1/{app_version}/cmx_ota/{token}/config/json/{request_id}"
    print(f"Requesting firmware using topic: {firmware_request_topic}")
    client.publish(firmware_request_topic, json.dumps({"observe": True}))

# Handle infinite loop, to continuously check the firmware version from Kaa
def client_loop(client):
    time.sleep(time_sleep)

    # Maintain connection with Kaa endpoint
    payload = {}
    print(f"Publishing new data to Kaa: {payload}\n")
    client.publish(publish_topic, json.dumps(payload))

    # Check new version of the firmware
    report_current_firmware_version()
    request_new_firmware()

# Setup MQTT client
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message

client.connect(mqtt_server, mqtt_port, 60)

# Publishing to Kaa to get response and start the program
client.publish(publish_topic, json.dumps({}))
client.loop_forever()
  1. Create a main.py file anywhere on your system.

  2. Setup a python environment and install required packages:

pip install paho-mqtt==1.5.1 requests
  1. In the code, replace token and app_version with the values from the endpoint you created earlier in the Kaa Platform.
    Note that the variable current_version is set to 1.0.0, which represents the initial firmware version.

  2. Run the code. If token and app_version are set correctly, the code will connect to Kaa and begin checking for updates at regular intervals. Since only the initial 1.0.0 update is available, no updates will be found for the current version. To start benefiting from OTA, you’ll need to provision a new version.

Terminal Initial Version Launch

Defining a New Firmware Version 1.1.0

To provision the new firmware, go to the Kaa platform UI and navigate to “Device Management” -> “Software OTA” -> “Add software version”.

From there:

  1. Set the “Semantic version number” to 1.1.0.
  2. Upload any file, such as a picture or a text file.
  3. In the “Upgradable From” field, select 1.0.0 to specify that devices running version 1.0.0 can upgrade to 1.1.0.
  4. Choose “All” for the rollout configuration.
    This ensures that all endpoints in your application can update to this firmware version.
  5. Click “Create” to add the new firmware version.

New Firmware Version

Monitoring the Update Process

Once the new firmware version 1.1.0 is submitted via the Kaa UI, the device running version 1.0.0 will automatically detect the update through MQTT.
It will then download the provided file and save it under the name defined in the file_name variable (default is file.txt).
In essence, this process ‘updates’ the system by replacing the existing file.txt with the newly downloaded file.

Terminal New Version

OTA Use Case Overview

Updating a single file may seem simple, but you can customize the logic in the download_and_apply_update function to automatically perform specific actions each time your device receives a new update.
For instance, you could run a console command on the newly downloaded file, or unzip and execute it if the file is an archive.

A great example of an OTA (Over-the-Air) update is applying updates to IoT devices, such as demonstrated in the OTA ESP32 tutorial.
In this case, a binary file is uploaded, and the ESP32 is instructed to updates using that file.

Summary

That’s all about OTA.
You should now be able to create new OTA updates, such as from version 1.1.0 to 1.2.0, 1.3.0, and add custom logic to handle them properly in your code.
Simply follow the steps outlined in the article Defining a New Firmware Version 1.1.0.

Troubleshooting

To troubleshoot any issues related to communication between the client and the Kaa platform, you can use the following Python script:

import itertools
import json
import queue
import random
import string
import sys
import time

import paho.mqtt.client as mqtt

KPC_HOST = "mqtt.cloud.kaaiot.com"  # Kaa Cloud plain MQTT host
KPC_PORT = 1883                     # Kaa Cloud plain MQTT port

CURRENT_SOFTWARE_VERSION = ""   # Specify software that device currently uses (e.g., 0.0.1)

APPLICATION_VERSION = ""     # Paste your application version
ENDPOINT_TOKEN = ""          # Paste your endpoint token


class SoftwareClient:

    def __init__(self, client):
        self.client = client
        self.software_by_request_id = {}
        self.global_request_id = itertools.count()
        get_software_topic = f'kp1/{APPLICATION_VERSION}/cmx_ota/{ENDPOINT_TOKEN}/config/json/#'
        self.client.message_callback_add(get_software_topic, self.handle_software)

    def handle_software(self, client, userdata, message):
        if message.topic.split('/')[-1] == 'status':
            topic_part = message.topic.split('/')[-2]
            if topic_part.isnumeric():
                request_id = int(topic_part)
                print(f'<--- Received software response on topic {message.topic}')
                software_queue = self.software_by_request_id[request_id]
                software_queue.put_nowait(message.payload)
            else:
                print(f'<--- Received software push on topic {message.topic}:\n{str(message.payload.decode("utf-8"))}')
        else:
            print(f'<--- Received bad software response on topic {message.topic}:\n{str(message.payload.decode("utf-8"))}')

    def get_software(self):
        request_id = next(self.global_request_id)
        get_software_topic = f'kp1/{APPLICATION_VERSION}/cmx_ota/{ENDPOINT_TOKEN}/config/json/{request_id}'

        software_queue = queue.Queue()
        self.software_by_request_id[request_id] = software_queue

        print(f'---> Requesting software by topic {get_software_topic}')
        payload = {
            "configId": CURRENT_SOFTWARE_VERSION
        }
        self.client.publish(topic=get_software_topic, payload=json.dumps(payload))

        try:
            software = software_queue.get(True, 5)
            del self.software_by_request_id[request_id]
            return str(software.decode("utf-8"))
        except queue.Empty:
            print('Timed out waiting for software response from server')
            sys.exit()

def main():
    # Initiate server connection
    print(f'Connecting to Kaa server at {KPC_HOST}:{KPC_PORT} using application version {APPLICATION_VERSION} and endpoint token {ENDPOINT_TOKEN}')

    client_id = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6))
    client = mqtt.Client(client_id=client_id)
    client.connect(KPC_HOST, KPC_PORT, 60)
    client.loop_start()

    software_client = SoftwareClient(client)

    # Fetch available software
    retrieved_software = software_client.get_software()
    print(f'Retrieved software from server: {retrieved_software}')

    time.sleep(5)
    client.disconnect()


if __name__ == '__main__':
    main()