Mender blog

Mender OTA on the ESP32, Part 2

In the second of a two-part blog series, Josef Holzmayr, Head of Developer Relations, examines how community member Joël Guittet developed an integration for Mender with an ESP32 microcontroller (MCU) and Espressif.

ESP32_2

The first blog in this ESP32 series, covered the desire of Joël Guittet, development engineer for MCUs at Witekio, to get secure and robust over-the-air (OTA) updates for the ESP32 and Espressif platforms. The first blog also examined Joël’s motivations and decisions in developing the integration between Mender and the ESP32 microcontroller.

Miss the first blog? Catch up on Mender OTA on the ESP32, part one!

This second blog dives into how Joel made the different pieces work in the integration. The result? Today, there is now community support for using Zephyr with Mender OTA on an ESP32 MCU.

Basic architecture: The Espressif ESP-IDF framework

Espressif provides a well-documented and extensive SDK for its products under the name ESP-IDF, which stands for "ESP IoT Development Framework." The ESP-IDF framework consists of many things; some prominent ones are:

  • FreeRTOS as the operating system core
  • An IP stack, including connection management
  • Implementation of many IoT-relevant protocols, such as Message Queuing Telemetry Transport (MQTT)
  • Drivers and examples of the peripherals found on the ESP platform

The ESP-IDF framework enabled Joël to focus on the aspects providing new value, instead of reinventing and reimplementing major parts of an OTA client. He chose ESP-IDF version 4.4, and could build upon:

  • HTTPS, with TLS provided mbedTLS
  • Thread management, including semaphores
  • Partitioning
  • Non-volatile memory

Show me the code

With the abstractions that Espressif supplies in mind, Joël decided to split his client implementation into two main parts: core and platform. Let's take a look at their function. Please note: As the client is under active development, the code might have changed by the time you look at it. This article references Version 0.3.0, as directly accessible here.

core

The implementation of API call flows and logic lives here. This means essentially two things:

  1. The flow of API calls to the Mender server and handling the respective results, and
  2. Coordinating actions on the device.

Possibly the most prominent place to see this in action is the check for a new deployment. Let's look at this now.

core/src/mender-api.c
#define MENDER_API_PATH_GET_NEXT_DEPLOYMENT "/api/devices/v1/deployments/device/deployments/next"

mender_err_t mender_api_check_for_deployment(char **id, char **artifact_name, char **uri) {
    <snip/>

    /* Compute path */
    if (NULL
        == (path = malloc(strlen("?artifact_name=&device_type=") + strlen(MENDER_API_PATH_GET_NEXT_DEPLOYMENT) + strlen(mender_api_config.artifact_name)
                          + strlen(mender_api_config.device_type) + 1))) {
        mender_log_error("Unable to allocate memory");
        ret = MENDER_FAIL;
        goto END;
    }
    sprintf(path, "%s?artifact_name=%s&device_type=%s", MENDER_API_PATH_GET_NEXT_DEPLOYMENT, mender_api_config.artifact_name, mender_api_config.device_type);

    /* Perform HTTP request */
    if (MENDER_OK
        != (ret = mender_http_perform(mender_api_jwt, path, MENDER_HTTP_GET, NULL, NULL, &mender_client_http_text_callback, (void *)&response, &status))) {
        mender_log_error("Unable to perform HTTP request");
        goto END;
    }

    <snip/>
}

What you can see here is the construction of a call to the API endpoint to check for a new deployment. As described in the documentation https://docs.mender.io/api/#device-api-deployments-check-update, it is a plain HTTP GET operation requiring two parameters.

Note that there are a number of function calls with a mender_-prefix. We'll get to those in the platform section.

The other part of the core implementation, as already mentioned, is the logic. Sticking with the deployments example:

core/src/mender-client.c
static mender_err_t
mender_client_update_work_function(void) {
    <snip/>

        if (MENDER_OK != (ret = mender_api_check_for_deployment(&id, &artifact_name, &uri))) {
        mender_log_error("Unable to check for deployment");
        goto END;
    }

    /* Check if deployment is available */
    if ((NULL == id) || (NULL == artifact_name) || (NULL == uri)) {
        mender_log_info("No deployment available");
        goto END;
    }

    /* Check if artifact is already installed (should not occur) */
    if (!strcmp(artifact_name, mender_client_config.artifact_name)) {
        mender_log_error("Artifact is already installed");
        mender_client_publish_deployment_status(id, MENDER_DEPLOYMENT_STATUS_ALREADY_INSTALLED);
        goto END;
    }

    /* Download deployment artifact */
    mender_log_info("Downloading deployment artifact with id '%s', artifact name '%s' and uri '%s'", id, artifact_name, uri);
    mender_client_publish_deployment_status(id, MENDER_DEPLOYMENT_STATUS_DOWNLOADING);
    if (MENDER_OK != (ret = mender_api_download_artifact(uri))) {
        mender_log_error("Unable to download artifact");
        mender_client_publish_deployment_status(id, MENDER_DEPLOYMENT_STATUS_FAILURE);
        goto END;
    }

    /* Set boot partition */
    mender_log_info("Download done, installing artifact");
    mender_client_publish_deployment_status(id, MENDER_DEPLOYMENT_STATUS_INSTALLING);
    if (MENDER_OK != (ret = mender_client_callbacks.ota_set_boot_partition())) {
        mender_log_error("Unable to set boot partition");
        mender_client_publish_deployment_status(id, MENDER_DEPLOYMENT_STATUS_FAILURE);
        goto END;
    }

    /* Save OTA ID to publish deployment status after rebooting */
    if (MENDER_OK != (ret = mender_storage_set_ota_deployment(id, artifact_name))) {
        mender_log_error("Unable to save OTA ID");
        mender_client_publish_deployment_status(id, MENDER_DEPLOYMENT_STATUS_FAILURE);
        goto END;
    }

    /* Now need to reboot to apply the update */
    mender_client_publish_deployment_status(id, MENDER_DEPLOYMENT_STATUS_REBOOTING);

    <snip/>

You can easily follow the canonical flow Check → Download → Install → Reboot, and publish the corresponding state back to the Mender backend on each step. But again, there are a lot of mender_-prefixes. Now, it's time to look at those.

platform

The platform directory holds a number of subdirectories for specific functionality groups, such as HTTP, for example. Those are used to provide abstractions for the functionality groups, depending on the actual board/(RT)OS/libraries in use. Joël put this architecture in place right from the beginning to make the MCU client portable to environments other than the ESP-IDF framework. By now, there are two ports available: ESP-IDF and Zephyr. So, how does one group those abstractions in a library written in C? That's the mender_-prefix. For each function call that the core part needs, there is an abstraction wrapper/implementation in the platform which provides the required implementation. Sticking with the deployments topic, let's have a peek at the artifact writing:

platform/board/esp-idf/src/mender-ota.c mender_err_t mender_ota_write(void *handle, void *data, size_t data_length) {

    assert(NULL != handle);
    esp_err_t err;

    /* Write data received to the update partition */
    if (ESP_OK != (err = esp_ota_write((esp_ota_handle_t)handle, data, data_length))) {
        mender_log_error("esp_ota_write failed (%s)", esp_err_to_name(err));
        return MENDER_FAIL;
    }

    return MENDER_OK;
}

You can see that this essentially wraps the routine that writes to permanent storage in ESP-IDF. Whenever a new platform is being added, the core implementation does not need to be modified; simply create another abstraction wrapper that again provides the mender_ota_write(void *handle, void *data, size_t data_length) interface.

Conclusion and the road ahead

While still being a young and active project, the mender-mcu-client library already significantly benefits from the careful architectural decisions that Joël took right in the beginning. Specifically, having portability in mind is quite uncommon for projects in that area as most approaches follow a mental model of "make it work for me."

As such, all the more we want to thank Joël for this great contribution to the Mender ecosystem and highlight that his vision for this work is to provide value not just for him, but every developer who wants or needs robust OTA updates for their MCU-based projects.

And as pointed out, it already went beyond the ESP framework. Zephyr support has arrived.

Thanks again, Joël!

Recent articles

An Overview of EU Cyber Resilience Act (CRA) Compliance

An Overview of EU Cyber Resilience Act (CRA) Compliance

Learn how the EU Cyber Resilience Act (CRA) enforces stringent cybersecurity requirements for PDEs. Explore compliance essentials in part 1 of 4
Mender versioning: New releases by component

Mender versioning: New releases by component

Explore Mender's shift to independent component releases and versioning, designed to enhance update speed and user clarity.
Driving secure innovation: ISO/SAE 21434 & UNECE compliance

Driving secure innovation: ISO/SAE 21434 & UNECE compliance

ISO/SAE 21434 and UNECE R155/R156 standards reshape cybersecurity in software-defined vehicles. Compliance with these frameworks is essential for protecting consumers, ensuring vehicle safety, and driving innovation in the automotive industry.
View more articles

Learn why leading companies choose Mender

Discover how Mender empowers both you and your customers with secure and reliable over-the-air updates for IoT devices. Focus on your product, and benefit from specialized OTA expertise and best practices.

 
sales-pipeline_295756365