The first line tells us that we will be building off of an official image,
python:2.7-stretch. What this means is that we are running a debian based container that will have python 2.7 included. From here we can start defining, downloading, and configuring the system dependencies. We start by getting
gcc. Then we use
pip to grab a tool useful for testing, jumper, which we'll cover later.
This docker image can be used to build firmware using the Nordic CLI tools, jfprog and hexmerge. Also included in the Nordic SDK are Makefiles that provide a really clean interface for flashing the firmware and flashing the soft device. We only made minor tweaks to the defaults to get this container ready for action. A small note about the download URL. More than once I've seen it change, breaking the image build. If the URL has changed by the time you read this, update to the correct url and check if things are where you expect them to be - use something like Artifactory for more serious endeavors.
Using this image obviates each developer from having to install and configure a development environment. Using Docker also has the benefit of reproducible builds, since the dependencies are baked into the image. Further, we can also use this image in our CI environments.
The image we built above could have been built with any number of base images, so why choose the Python 2.7 Stretch base? Testing, of course! You likely noticed
pip install jumper and baking in a
.jumper/config.json. Even though I begrudgingly use legacy Python, we’re doing so to leverage a powerful testing tool, the Jumper Framework.
Once we have our firmware loaded onto a virtual device we can run unittest tests and make assertions on the behavior of our program without injecting testing libraries into the C codebase. Not only are tests easy to add this way, we can be confident that the behavior here matches our devices. In the test cases below, we could, for example, run the emulator for twenty seconds and then make the following assertions.
def test_pins(self): c = Counter(self.pin_events) self.assertTrue(c[(16, 0)] == 1) # One set low event self.assertTrue(c[(16, 1)] == 1) # One set high event def test_interrupts(self): c = Counter(self.interrupt_events) self.assertTrue(c[u"RNG"] > 100) self.assertTrue(c[u"Svcall"] == 12) self.assertTrue(c[u"SWI0_EGU0"] == 3) self.assertTrue(c[u"SAADC"] == 2) self.assertTrue(c[u"MWU"] == 1) self.assertTrue(c[u"RTC1"] == 1) self.assertTrue(c[u"POWER_CLOCK"] == 1) assert sorted(c.keys()) == sorted([ u"RNG", u"Svcall", u"SWI0_EGU0", u"SAADC", u"MWU", u"RTC1", u"POWER_CLOCK", ])
Now that we’ve got build and tests in reproducible environments, releasing is a matter of creating a release out of the merged hexfile from our build step. Leveraging CircleCI, we can run our build, test it with our Python tests, and use a tool like ghr to tag-and-release the firmware.
And We're Off!
In some cases, there isn’t an easy way to emulate a device, or the dependencies aren’t easily bundled. Jumper is great, but far from comprehensive in the images they offer. Your device may require more complex interactions to produce interesting states to test. As in all things in life, this approach might spend more time than it saves, and depends on the specifics of your project. I will attest that in the cases where it works, you'll be confidently shipping your firmware in no time. If your team is working on an IoT project, I hope you’ll consider containers as a viable approach for building, testing, and releasing.