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);
}