Uncannier Software

It's not enough to just be uncanny

Automated Software Versioning – Continuous Integration & Thunderboard

In this article I put my Three Laws of Software Versioning into practice. I show how to setup automated software versioning for an embedded firmware project. I use the Uncannier Thunderboard projects, and CircleCI continuous integration, as an example.


Let’s get straight to the point. To automate software versioning you need a way to inject version information during the build. There are many ways to do this. In this article however, the approach I take is to generate a header file.

Template Header File

Your source repository needs a template header file. For the Thunderboard projects, I’ve added a file named ci.h.

Template header designed to be overridden by the CI build plan

You could have extra or different fields in this file. For example, you might have the branch name as well. I have less information here than I’d normally like, because BLE limits me to 20-character version strings.

These default template values will be used for local builds. Thus local builds, often the source of much inefficiency and confusion in embedded software teams, will always identify with all zeroed versions.

For builds from the CI server, the CI build plan will write its own ci.h, replacing the template ci.h, prior to building the code. Thus all CI builds have non-zeroed versions. Local builds and CI builds are never confused.

CI Build Plan – Header File Generation

The CI build plan needs to replace ci.h file. Here’s how I do it for the Thunderboard Sense 2 project using CircleCI.

CircleCI version injection

You would need to adapt for your project and your CI tool.

With this approach, the build number and commit hash are always set for every build that comes from the CI server. For tagged CI builds (ie. formal releases), the version is set to the tag. For all other CI builds, the version is 0.0.0.

The End Result

We end up with a clear distinction between local developer builds, un-tagged CI builds (internal QA) and tagged CI builds (releases). An example for Uncannier Thunderboard Sense 2:

Build TypeVersion
Tagged CI Build (Formal Release)0.1.0-39 aeb3241
Un-tagged CI Build (Internal QA)0.0.0-38 aeb3241
Local Developer Build0.0.0-0 0000000

Where aeb3241 is the Git commit hash, build 38 was un-tagged, and build 39 was tagged as release 0.1.0.

The following screenshots show the versions reported at run-time for the Uncannier Thunderboard Sense 2 project. The automated software version is in the Software Revision String characteristic in the following screenshots. The Firmware Revision String is explained later in this post, if you are interested in the Thunderboard.


The rest of the post covers the specifics of automated software versioning on the Uncannier Thunderboard projects.


The need for improvements to the software versioning is plainly apparent if you consider that, to date, every Uncannier Thunderboard React CircleCI build reports the same version. Likewise every Uncannier Thunderboard Sense 2 CircleCI build. This would be an untenable situation if someone was using my builds.

Out of the Box Versioning

Silicon Labs deliver reference projects for both the Thunderboard React and the Thunderboard Sense 2. The application version is hard-coded and is reported in the standard Firmware Version characteristic of the Device Information BLE GATT Service.

Thunderboard React Firmware Revision BLE characteristic: hard-coded constant 1.1.2
Thunderboard Sense 2 Firmware Revision BLE characteristic: initialized to 0.1.0

The Device Information service is the conventional mechanism for a BLE peripheral to report its firmware or software version. However, with a hard-coded version, the projects fail all Three Laws of Software Versioning. In case you are in any doubt, it would be a truly terrible idea to version these projects by manually changing the Firmware Version each build.

The Sense 2 project includes the appl_ver.h file. As part of its boot-up, the Sense 2 loads the data from this file into the Firmware Revision characteristic. I could automate the Sense 2 versioning by simply having the CI build plan generate this file on every build. However I elect to use my ci.h file instead because I want the Git commit hash in the version.

BLE Characteristics Definition

I want to report more version information than what comes out of the box.

Firstly, I add a standard Software Version characteristic to the Device Information BLE service for both projects. Shown below for the Sense 2.

Thunderboard Sense 2 Software Revision BLE characteristic: up to 20 bytes

This will hold the application version string that shall be populated at run-time from the contents of ci.h.

Secondly, I modify the Firmware Version characteristic to be a 20-byte field. Shown below for the Sense 2.

Thunderboard Sense 2 Firmware Revision BLE characteristic: up to 20 bytes.

This will be populated at run-time with the Gecko Bootloader version and the Bluetooth stack version. The versions of these softwares are reported to the application as part of system boot.

The Gecko Bootloader is separate from the application and is not updated by the OTA firmware update process. Thus it is desirable, to the point of necessary, to make it reportable over BLE.

The Bluetooth stack is statically linked, and thus its version can be determined from CI build logs. For historical reasons however, it is still reported to the application during boot. It’s desirable to confirm the stack version by making it reportable over BLE.

BLE Characteristics Population

The di_service.c file has been added to each project. The functions in this file populate the Software Version and Firmware Version characteristics. Shown below for the Sense 2.

/// @file di_service.c
/// @brief Device Information service - custom parts
/// @copyright Copyright (c) Uncannier Software 2019
#include <stdint.h>
#include <stdio.h>
#include "native_gecko.h"
#include "gatt_db.h"
#include "ci.h"
#include "di_service.h"
/// @brief  Initializes the Software Version characteristic with the application version
void diServiceSwVerInit( void )
    char software_version[VER_CHARACTERISTIC_SIZE] = { 0 };
    snprintf( software_version, sizeof(software_version) - 1, "%s-%d %07x", CI_VERSION, CI_BUILD, CI_COMMIT );
    gecko_cmd_gatt_server_write_attribute_value( gattdb_software_rev, 0, strlen( software_version ),
                                                 (uint8_t *)software_version );
/// @brief  Initializes the Firmware Version characteristic with the stack and bootloader versions
/// @param evt_system_boot  Pointer to the Gecko stack boot event
void diServiceFwVerInit( struct gecko_msg_system_boot_evt_t *evt_system_boot )
    char firmware_version[40] = { 0 };
    snprintf( firmware_version, sizeof(firmware_version) - 1, "%d.%d.%d-%d %08lx",
              evt_system_boot->major, evt_system_boot->minor, evt_system_boot->patch,
              evt_system_boot->build, evt_system_boot->bootloader );
    // The string will be truncated if it exceeds the maximum characteristic size (unlikely)
    gecko_cmd_gatt_server_write_attribute_value( gattdb_firmware_rev, 0, VER_CHARACTERISTIC_SIZE,
                                                 (uint8_t *)firmware_version );

The End Result (Again)

OK, now some screenshots of the nRF Connect app, for the Thunderboard Sense 2. The Firmware Version characteristic reports the Bluetooth stack version 2.11.5-432 and Gecko Bootloader version 1.8.4. The Software Version characteristic reports the application version, for 3 different build types.

Uncannier Pull Requests

See the pull requests for the full changes.

Tagged , , , ,

Leave a Reply

Your email address will not be published. Required fields are marked *