FOTA

When you move on from the “prototype on my desk” to multiple test devices you soon discover that upgrades are time consuming, particularly if the devices are far apart or in inaccessible places. Climbing ladders with a laptop and programming cable soon gets tedious.

Firmware over the air (FOTA) is quite simple in theory: Create a new firmware image, send it to the device and let the device update itself.

Versioning

Versioning is the first issue you should solve. Since the device (and you) needs to know “do I need an upgrade?” a simple version number or string must be used. A “version” is Span is just a string but for simplicity we’ll just use a number. It doesn’t have to be very complicated, a single #define VERSION "1" somewhere in the source code is sufficient. When you want to deploy a new version change the string to something different, build the new image and install it on the device.

Reporting the running version

Span doesn’t connect to the device in any way - the devices themselves manages the communication schedule so the device must report its running version once in a while. The firmware update protocol in Span is as simple as it gets - the version, plus three strings (model number, serial number, client manufacturer) are reported via the CoAP service and any new image is retrieved via the CoAP interface.

FOTA protocol

Report

The uses a relatively simple packet format. The strings are TLV encoded in a single buffer:

ID Description Type
1 Firmware version string
2 Model number string
3 Serial number string
4 Client manufacturer string

Span uses just the firmware version field to determine the running version. The other three fields are stored but not used. These can be used to report other kinds of hardware identification and versions such as modem firmware version, hardware revision numbers, build names or serial numbers.

The strings are encoded as two bytes followed by the string itself:

1 2 3 4 n
ID Length char 1 char 2 char N

Uknown IDs can be discarded and the bytes skipped.

Response

The report is sent via CoAP POST to coap://172.16.15.14:5683/fw and Span responds with another TLV encoded buffer that contains the following fields:

ID Description Type
1 Host name string
2 Port number uint32
3 Path string
4 Image available bool

The host, port and path fields can be used to build a CoAP URI that points to a firmware image if it should be downloaded. Fields 1-3 might not be included if there is no firmware image available for download.

Any unknown fields can be discarded.

If the last field is set to true, ie the value is 0x1 there is a new version available and it can be downloaded via a CoAP GET from the host, port and path.

Encoding the simple FOTA report

Encoding a string into a buffer is quite simple:

size_t encode_tlv_string(uint8_t *buf, uint8_t id, const uint8_t *str)
{
    size_t ret = 0;
    buf[ret++] = id;
    buf[ret++] = strlen(str);
    for (uint8_t i = 0; i < strlen(str); i++)
    {
        buf[ret++] = str[i];
    }
    return ret;
}

#define FIRMWARE_VER_ID 1
#define MODEL_NUMBER_ID 2
#define SERIAL_NUMBER_ID 3
#define CLIENT_MANUFACTURER_ID 4

int fota_encode_simple_report(uint8_t *buffer, size_t *len)
{
    size_t sz = encode_tlv_string(buffer, FIRMWARE_VER_ID, "version1");
    sz += encode_tlv_string(buffer + sz, CLIENT_MANUFACTURER_ID, "manufacturer");
    sz += encode_tlv_string(buffer + sz, SERIAL_NUMBER_ID, "serial");
    sz += encode_tlv_string(buffer + sz, MODEL_NUMBER_ID, "model");
    *len = sz;
    return 0;
}

Decoding the response from Span

Decoding the response from Span are a bit more involved but quite simple:

int decode_tlv_string(const uint8_t *buf, size_t *idx, char *str)
{
    int len = (int)buf[(*idx)++];
    int i = 0;
    for (i = 0; i < len; i++)
    {
        str[i] = buf[(*idx)++];
    }
    str[i] = 0;
    return 0;
}

int decode_tlv_uint32(const uint8_t *buf, size_t *idx, uint32_t *val)
{
    size_t len = (size_t)buf[(*idx)++];
    if (len != 4)
    {
        LOG_ERR("uint32 in TLV buffer isn't 4 bytes");
        return -1;
    }
    *val = 0;
    *val += (buf[(*idx)++] << 24);
    *val += (buf[(*idx)++] << 16);
    *val += (buf[(*idx)++] << 8);
    *val += (buf[(*idx)++]);
    return 0;
}

int decode_tlv_bool(const uint8_t *buf, size_t *idx, bool *val)
{
    size_t len = (size_t)buf[(*idx)++];
    if (len != 1)
    {
        LOG_ERR("bool in TLV buffer isn't 1 byte");
        return -1;
    }

    *val = (buf[(*idx)++] == 1);
    return 0;
}

#define HOST_ID 1
#define PORT_ID 2
#define PATH_ID 3
#define AVAILABLE_ID 4

typedef struct
{
    char host[25];
    uint32_t port;
    char path[25];
    bool scheduled_update;
} simple_fota_response_t;


int decode_simple_response(simple_fota_response_t *resp, const uint8_t *buf, size_t len)
{
    size_t idx = 0;
    int err = 0;
    while (idx < len)
    {
        uint8_t id = buf[idx++];
        switch (id)
        {
        case HOST_ID:
            err = decode_tlv_string(buf, &idx, resp->host);
            if (err)
            {
                return err;
            }
            break;
        case PORT_ID:
            err = decode_tlv_uint32(buf, &idx, &resp->port);
            if (err)
            {
                return err;
            }
            break;
        case PATH_ID:
            err = decode_tlv_string(buf, &idx, resp->path);
            if (err)
            {
                return err;
            }
            break;
        case AVAILABLE_ID:
            err = decode_tlv_bool(buf, &idx, &resp->scheduled_update);
            if (err)
            {
                return err;
            }
            break;
        default:
            printf("Unknown field id in FOTA response: %d\n", id);
            return -1;
        }
    }
    return 0;
}