Page Header

VCS (PlatformIO Advanced Scripting)

16-04-2022

VCS (Version Control System) is a simple script that works with the PlatformIO build system to add useful metadata to the output binaries of embedded projects. The exact workings of the tool are documented on GitHub, so I won’t dive into detail here. This article focuses more on the internals of the library and some of the cool things you can do with the scripting engine.

VCS Tile

PlatformIO is built on Python. Any valid Python can be executed in the context of the build environment, and if you are somewhat familiar with the SCons build environment that PlatformIO uses, then you can pretty much mess with any part of the C/C++ build process!

The PlatformIO docs give a thorough breakdown of the SCons API, but it is quite light on real-life examples.

Below are a few examples of useful things that the PlatformIO scripting API can help you do:

Automated Upstream Library Configuration

The SCons build system allows the source and include paths to be edited programatically. I have used this in the past to configure libraries pulled directly from an upstream source. FreeRTOS is a good example of where this is useful. To use the library directly, you need to choose a target architecture port.c file, choose a memory management scheme, and configure some basic options.

In the scripting API you have access to all of your platform build variables, so it makes easy to configure these options automatically based on your target environment. An example of that could be:

# Get predefined C macro (could be defined by build system or .ini file)
for item in env.get("CPPDEFINES", []):
    if isinstance(item, tuple):
        if item[0] == 'PORT_TYPE':
            port_type = item[1]

# Configure source and include paths based on this macro
env.Append(CPPPATH=[os.path.realpath("FreeRTOS-Kernel/include")])
env.Append(CPPPATH=[os.path.realpath(os.path.join("FreeRTOS-Kernel/portable/GCC/", port_type))])
env.Replace(SRC_FILTER=[
        "+<FreeRTOS-Kernel/>",
        "-<FreeRTOS-Kernel/portable/>",
        "+<FreeRTOS-Kernel/portable/MemMang/heap_4.c>",
        os.path.join("+<FreeRTOS-Kernel/portable/GCC/", port_type, ">")
    ])

A more complete example of this behaviour is shown in my PlatformIO-FreeRTOS library.

Tagged Version Fetching

PlatformIO does have built-in support for fetching Git repositories, but this only works if that repository is in the default library format or contains a library.json file. With the advanced scripting engine you can fetch the library on-the-fly and modify it.

I have used the GitPython package in the past. It allows you to fetch and manage Git repositories programatically. Two functions I use extensively are:

def getRemoteTags(url):
    g = git.cmd.Git()
    blob = g.ls_remote(url, sort='-v:refname', tags=True, refs=True)
    entries = blob.split('\n')
    return list(map(lambda x: x.partition('refs/tags/')[2], entries))

def getLocalTag(path):
    g = git.Repo(path)
    return [str(next((tag for tag in g.tags if tag.commit == g.head.commit), None))]
    pass

This is handy for some automated library work, but where it really shines is in a corporate environment. Many teams at work have trouble keeping certain build data/settings synchronised across projects. Adding a lib-build.py to our default project template effectively ‘forces’ team members to keep certain dependencies up to date!

Asset Bundling

Projects involving UI and IoT often involve assets that cannot be natively compiled by a C compiler. Examples include UI Images and FileSystem data for web servers. There exist tools to do this sort of thing, but they are mostly web-based or use ancient scripting languages like perl. These assets are also tricky to incorporate into CI/CD toolchains, meaning compiled assets often just get left in source control. A nice alternative would be to have these assets treated as part of the compilation framework, similar to how web bundlers work.

Certificate Management and Authentication

IoT projects typically require API keys, certificates, MAC addresses and other product-specific information embedded in the firmware. In industry specialised tools exist to modify binaries when programming devices on a production line. This is impractical for most hobbyists, but for small production runs have a slightly more automated firmware build system is probably desirable.

Tips and Tricks

There are a few parts of the scripting API that initially caught me out.

The first of these is importing Third-Party python packages when in the build environment. Packages must be installed within PlatformIO’s own python virtual environment: this can be done from within the build script with the following snippet:

try:
    import example
except ImportError:
    env.Execute("$PYTHONEXE -m pip install example")
    import example

The second ‘Gotcha’ is the understanding of build scope from within libraries.

When using scripting from the main platformio.ini file, we are modifying the build environment of our main codebase: Aside from build_flags these changes do not propogate down to our libraries. Likewise, when calling build scripts from within library.json, we are modifying the build environment of that library, not the whole project.

This can be a bit confusing when it comes to global config files. Projects such as lwIP, freeRTOS and lvgl use global config files that must be added to your project. However, by default, your project include paths are not passed to libraries when they are complied. A simple workaround is to use the scripting API to add the root include directory to your libary build:

# This script snippet works from any build context!
env.Append(CPPPATH=env.get("PROJECT_INCLUDE_DIR", []))

The final tip is to run your builds using the -v (verbose) flag when writing scripts. Ultimately all SCons is doing is deciding what needs to be recompiled and passing the appropriate flags to gcc.

Anyway… I’ve got sidetracked… back to the library!

VCS Usage

The library doesn’t really require any interaction: all you need to do to activate it is add the library your platformio.ini file and the rest is handled automatically.

The library generates the following entry as a global C-struct

const vcs_t vcs = {
    {0xFF, 0xFE, 0xFD, 0xFC},   /* frame_start */
    1,                          /* schema */
    __VCS_COMPILE_TIME,         /* compile_time */
    __VCS_SHORT_HASH,           /* short_hash */
    __VCS_IS_DIRTY,             /* is_dirty */
    __VCS_TAG_DESCRIBE,         /* tag_describe */
    __VCS_LAST_AUTHOR,          /* author */
    {0x01, 0x02, 0x03, 0x04}    /* frame_end */
};

An example output for a typical compilation may look something like this:

{
    "schema": 1,
    "compile_time": "2022-01-13T12:03Z",
    "short_hash": "293b1c5",
    "is_dirty": true,
    "tag_describe": "1.0.0-2-g293b1c5",
    "last_author": "James Bennion-Pedley"
}

You can access the vcs struct anywhere you include the header, so the output can be used in your program.

Checking Versions

Looking through binaries for an embedded bit of metadata is tedious. Ideally I would have liked to place the metadata at the start or end of the firmware binary, but doing this would involve programatically editing the linker file, which just seems like asking for trouble.

Instead, the library includes a simple python script that searches for a ‘magic pattern’ that signifies the fixed-size metadata block.

simply get your binary file, download the script and run the following python command:

$ get_vcs.py ./PATH/TO-FIRMWARE.bin
# OR
$ python3 get_vcs.py ./PATH/TO-FIRMWARE.bin

# Pass the 'clean' flag for just VCS output (scripting)
$ get_vcs.py --clean ./PATH/TO-FIRMWARE.bin > something_else

This will return you the metadata in JSON format.

This library was an excellent excuse to play about with the python scripting capabilities of PlatformIO. Hopefully someone else finds it useful too!