6.2. Basic implementation

6.2.1. Project structure

We shall start with the code from Notifications support chapter. In the end, our project structure would look as follows:

examples/tutorial/firmware-update/basic-implementation/
├── CMakeLists.txt
└── src
    ├── firmware_update.c
    ├── firmware_update.h
    ├── main.c
    ├── time_object.c
    └── time_object.h

Note the firmware_update.c and firmware_update.h are introduced in this chapter.

6.2.2. Installing the Firmware Update module

In order to install the module, we are going to use:

int anjay_fw_update_install(
        anjay_t *anjay,
        const anjay_fw_update_handlers_t *handlers,
        void *user_arg,
        const anjay_fw_update_initial_state_t *initial_state);

The important arguments for us at this point are anjay, handlers and user_arg.

We already discussed handlers structure in API in Anjay, and we will shortly provide simple implementations of required callbacks.

Note

The user_arg is a pointer passed to every callback, when Anjay actually calls it. This pointer can be used by the callback implementation to store any kind of context to operate on. Alternatively, the implementation may set it to NULL and rely on the use of global variables.

In our code, firmware update module installation will be taken care of by the function declared in firmware_update.h:

#ifndef FIRMWARE_UPDATE_H
#define FIRMWARE_UPDATE_H
#include <anjay/anjay.h>
#include <anjay/fw_update.h>

/**
 * Buffer for the endpoint name that will be used when re-launching the client
 * after firmware upgrade.
 */
extern const char *ENDPOINT_NAME;

/**
 * Installs the firmware update module.
 *
 * @returns 0 on success, negative value otherwise.
 */
int fw_update_install(anjay_t *anjay);

#endif // FIRMWARE_UPDATE_H

We invoke it in main.c by performing two (highlighted) modifications:

#include <anjay/anjay.h>
#include <anjay/security.h>
#include <anjay/server.h>
#include <avsystem/commons/avs_log.h>

#include "firmware_update.h"
#include "time_object.h"

typedef struct {
    anjay_t *anjay;
    const anjay_dm_object_def_t **time_object;
} notify_job_args_t;

// Periodically notifies the library about Resource value changes
static void notify_job(avs_sched_t *sched, const void *args_ptr) {
    const notify_job_args_t *args = (const notify_job_args_t *) args_ptr;

    time_object_notify(args->anjay, args->time_object);

    // Schedule run of the same function after 1 second
    AVS_SCHED_DELAYED(sched, NULL, avs_time_duration_from_scalar(1, AVS_TIME_S),
                      notify_job, args, sizeof(*args));
}

// Installs Security Object and adds and instance of it.
// An instance of Security Object provides information needed to connect to
// LwM2M server.
static int setup_security_object(anjay_t *anjay) {
    if (anjay_security_object_install(anjay)) {
        return -1;
    }

    static const char PSK_IDENTITY[] = "identity";
    static const char PSK_KEY[] = "P4s$w0rd";

    anjay_security_instance_t security_instance = {
        .ssid = 1,
        .server_uri = "coaps://eu.iot.avsystem.cloud:5684",
        .security_mode = ANJAY_SECURITY_PSK,
        .public_cert_or_psk_identity = (const uint8_t *) PSK_IDENTITY,
        .public_cert_or_psk_identity_size = strlen(PSK_IDENTITY),
        .private_cert_or_psk_key = (const uint8_t *) PSK_KEY,
        .private_cert_or_psk_key_size = strlen(PSK_KEY)
    };

    // Anjay will assign Instance ID automatically
    anjay_iid_t security_instance_id = ANJAY_ID_INVALID;
    if (anjay_security_object_add_instance(anjay, &security_instance,
                                           &security_instance_id)) {
        return -1;
    }

    return 0;
}

// Installs Server Object and adds and instance of it.
// An instance of Server Object provides the data related to a LwM2M Server.
static int setup_server_object(anjay_t *anjay) {
    if (anjay_server_object_install(anjay)) {
        return -1;
    }

    const anjay_server_instance_t server_instance = {
        // Server Short ID
        .ssid = 1,
        // Client will send Update message often than every 60 seconds
        .lifetime = 60,
        // Disable Default Minimum Period resource
        .default_min_period = -1,
        // Disable Default Maximum Period resource
        .default_max_period = -1,
        // Disable Disable Timeout resource
        .disable_timeout = -1,
        // Sets preferred transport to UDP
        .binding = "U"
    };

    // Anjay will assign Instance ID automatically
    anjay_iid_t server_instance_id = ANJAY_ID_INVALID;
    if (anjay_server_object_add_instance(anjay, &server_instance,
                                         &server_instance_id)) {
        return -1;
    }

    return 0;
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        avs_log(tutorial, ERROR, "usage: %s ENDPOINT_NAME", argv[0]);
        return -1;
    }

    ENDPOINT_NAME = argv[1];

    const anjay_configuration_t CONFIG = {
        .endpoint_name = ENDPOINT_NAME,
        .in_buffer_size = 4000,
        .out_buffer_size = 4000,
        .msg_cache_size = 4000
    };

    anjay_t *anjay = anjay_new(&CONFIG);
    if (!anjay) {
        avs_log(tutorial, ERROR, "Could not create Anjay object");
        return -1;
    }

    int result = 0;
    // Setup necessary objects
    if (setup_security_object(anjay) || setup_server_object(anjay)
            || fw_update_install(anjay)) {
        result = -1;
    }

    const anjay_dm_object_def_t **time_object = NULL;
    if (!result) {
        time_object = time_object_create();
        if (time_object) {
            result = anjay_register_object(anjay, time_object);
        } else {
            result = -1;
        }
    }

    if (!result) {
        // Run notify_job the first time;
        // this will schedule periodic calls to itself via the scheduler
        notify_job(anjay_get_scheduler(anjay), &(const notify_job_args_t) {
                                                   .anjay = anjay,
                                                   .time_object = time_object
                                               });

        result = anjay_event_loop_run(
                anjay, avs_time_duration_from_scalar(1, AVS_TIME_S));
    }

    anjay_delete(anjay);
    time_object_release(time_object);
    return result;
}

Note

As you may see, there is also an additional ENDPOINT_NAME global variable that now stores the command line argument. As we will use the same kind of program binary as the update image, we will need this to properly launch it as part of the upgrade process.

This is usually not necessary in production code, as the endpoint name is usually either hard-coded, or configured through other means.

6.2.3. Implementing handlers and installation routine

First, let’s think about what would we need to implement I/O operations required to download the firmware. The approach we could take is to open a FILE during a call to the stream_open callback, write to it in stream_write, and close it in stream_finish. The only detail remaining is: how are we going to share FILE * pointer between all of these?

We can use a globally allocated structure and pack entire shared state into it. In firmware_update.c it looks like this:

#include "./firmware_update.h"

#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

static struct fw_state_t { FILE *firmware_file; } FW_STATE;

Note

The numerous headers included will be useful in further stages of the development.

Having the global state structure, we can proceed with implementation of: fw_stream_open, fw_stream_write and fw_stream_finish, keeping in mind our brief discussion at the beginning of the section:

static struct fw_state_t { FILE *firmware_file; } FW_STATE;

static const char *FW_IMAGE_DOWNLOAD_NAME = "/tmp/firmware_image.bin";

static int fw_stream_open(void *user_ptr,
                          const char *package_uri,
                          const struct anjay_etag *package_etag) {
    // For a moment, we don't need to care about any of the arguments passed.
    (void) user_ptr;
    (void) package_uri;
    (void) package_etag;

    // It's worth ensuring we start with a NULL firmware_file. In the end
    // it would be our responsibility to manage this pointer, and we want
    // to make sure we never leak any memory.
    assert(FW_STATE.firmware_file == NULL);
    // We're about to create a firmware file for writing
    FW_STATE.firmware_file = fopen(FW_IMAGE_DOWNLOAD_NAME, "wb");
    if (!FW_STATE.firmware_file) {
        fprintf(stderr, "Could not open %s\n", FW_IMAGE_DOWNLOAD_NAME);
        return -1;
    }
    // We've succeeded
    return 0;
}

static int fw_stream_write(void *user_ptr, const void *data, size_t length) {
    (void) user_ptr;
    // We only need to write to file and check if that succeeded
    if (fwrite(data, length, 1, FW_STATE.firmware_file) != 1) {
        fprintf(stderr, "Writing to firmware image failed\n");
        return -1;
    }
    return 0;
}

static int fw_stream_finish(void *user_ptr) {
    (void) user_ptr;
    assert(FW_STATE.firmware_file != NULL);

    if (fclose(FW_STATE.firmware_file)) {
        fprintf(stderr, "Closing firmware image failed\n");
        FW_STATE.firmware_file = NULL;
        return -1;
    }
    FW_STATE.firmware_file = NULL;
    return 0;
}

Next in queue is fw_reset, which is called when something on the Client or the Server side goes wrong, or if the Server decides to not perform firmware update. We can implement it as follows:

static void fw_reset(void *user_ptr) {
    // Reset can be issued even if the download never started.
    if (FW_STATE.firmware_file) {
        // We ignore the result code of fclose(), as fw_reset() can't fail.
        (void) fclose(FW_STATE.firmware_file);
        // and reset our global state to initial value.
        FW_STATE.firmware_file = NULL;
    }
    // Finally, let's remove any downloaded payload
    unlink(FW_IMAGE_DOWNLOAD_NAME);
}

And finally, fw_perform_upgrade as well as fw_update_install are to be implemented. However, up to this point, we did not specify what would the format of a downloaded image be, nor how would it be applied.

In our simplified example, we can require from the image to be an executable, and then in fw_perform_upgrade we could be using execl() to start a new (downloaded) version of our Client.

Note

In a more realistic scenario, one would be doing things such as:

  • firmware verification,

  • saving it to some persistent storage (e.g. flash), rather than to /tmp,

  • other platform specific stuff.

The other important thing to consider is this: how’s the newly running client going to know it was upgraded? After all, it would be nice if the Client could report this information to the Server for it to know the update actually succeeded.

The simplest solution here is to use a “marker” file, indicating the client successfully upgraded. Specifically, the idea is as follows:

  • just before performing the upgrade, a “marker” file is created,

  • the logic in the Client can check for the existence of the “marker” and conclude, if the upgrade was performed or not,

  • finally, the “marker” gets removed.

The code is self explanatory:

// A part of a rather simple logic checking if the firmware update was
// successfully performed.
static const char *FW_UPDATED_MARKER = "/tmp/fw-updated-marker";

static int fw_perform_upgrade(void *user_ptr) {
    if (chmod(FW_IMAGE_DOWNLOAD_NAME, 0700) == -1) {
        fprintf(stderr,
                "Could not make firmware executable: %s\n",
                strerror(errno));
        return -1;
    }
    // Create a marker file, so that the new process knows it is the "upgraded"
    // one
    FILE *marker = fopen(FW_UPDATED_MARKER, "w");
    if (!marker) {
        fprintf(stderr, "Marker file could not be created\n");
        return -1;
    }
    fclose(marker);

    assert(ENDPOINT_NAME);
    // If the call below succeeds, the firmware is considered as "upgraded",
    // and we hope the newly started client registers to the Server.
    (void) execl(FW_IMAGE_DOWNLOAD_NAME, FW_IMAGE_DOWNLOAD_NAME, ENDPOINT_NAME,
                 NULL);
    fprintf(stderr, "execl() failed: %s\n", strerror(errno));
    // If we are here, it means execl() failed. Marker file MUST now be removed,
    // as the firmware update failed.
    unlink(FW_UPDATED_MARKER);
    return -1;
}

static const anjay_fw_update_handlers_t HANDLERS = {
    .stream_open = fw_stream_open,
    .stream_write = fw_stream_write,
    .stream_finish = fw_stream_finish,
    .reset = fw_reset,
    .perform_upgrade = fw_perform_upgrade
};

const char *ENDPOINT_NAME = NULL;

int fw_update_install(anjay_t *anjay) {
    anjay_fw_update_initial_state_t state;
    memset(&state, 0, sizeof(state));

    if (access(FW_UPDATED_MARKER, F_OK) != -1) {
        // marker file exists, it means firmware update succeded!
        state.result = ANJAY_FW_UPDATE_INITIAL_SUCCESS;
        unlink(FW_UPDATED_MARKER);
    }
    // install the module, pass handlers that we implemented and initial state
    // that we discovered upon startup
    return anjay_fw_update_install(anjay, &HANDLERS, NULL, &state);
}