The sunrise alarm clock contains a software simulation of the real hardware. This simulator presents the state of the simulated hardware on a web page and the developer can interact with it. Since the software can be started on the developers local machine, this can drastically reduce the turnaround time during development.
With Elixir and Nerves, implementing the simulator is a piece of cake. Since every hardware module is implemented as a separate process, we can replace these processes with simulations, as long as they accept the same messages as the original ones.
Simulation can be done on different levels. We could either simulate our own hardware modules (like Buttons or Lcd), or we could simulate the underneath Nerves processes (like GPIO or I2C).
Simulating the Nerves processes gives us the oppurtunity that all of our own code is executed, even during simulation. The downside is, that the development of an I2C or GPIO simulation is much more complicated. So in the current version of this software, i wrote simulations for the higher level modules Buttons, Lcd, Leds, Settings and Touch. This is sufficient when we focus on the development of the business logic.
First of all if you haven't already done so, you need to install Erlang and Elixir. Please refer to this Elixir Installation Guide to find instructions for your specific environment.
Next we need to install the Nerves framework. The Nerves Installation Guide can be found on the Nerves web site.
Then clone the GitHub repository for this project and enter the directory of the firmware application. You also need to create a copy of the provided local config file example.
Now you can finally install the project dependencies and start the system in the default development (simulation) mode. Compilation happens during the first time you execute mix phx.server, which takes some time before the application starts up.
You can then access the simulators web page in your browser: http://localhost:4000/sim The clocks settings web page can be accesses through the URL: http://localhost:4000
The top part of the simulator shows the input and outout elements. The big box at the bottom shows all the current attributes of the system state.
The following tests (i prefer the term specifications) are implemented with ESpec and some parts of the specification code is omitted. You can execute all of the specifications by executing the mix task espec in the apps/fw directory:
When we want to run automated specifications for our hardware modules, we have the problem that this hardware doesn't exist on our development machines. For example the Lcd module uses the I2C module to write to the LCD via the I2C bus. So when we test this module on our local development workstation, this surely will gonna fail. We clearly need a way to replace these low level functionality with something else.
As with most modern languages, Elixir allows us to replace functions with mocks. These mocks are then executed when their original counterpart is called and can return prefabricated values. We can also check if these mocked functions got executed during our tests.
Here is the complete specification of the Leds module:
This option has some drawbacks. First of all, mocks tend to let implementation details of the functions under test leak into the test code. If for example you change the implementation during a refactoring, your tests will most likely fail and need to be refactored too.
Next look at the specification for the backlight(:on) function. Because i set the backlight LED to on in the initialization function, the function call I2C.write(:i2c, <<0x05, 0b00001101>>) will be executed two times in the end. So i need to expect the I2C.write() function to be called two times, despite the fact that it is called only once in the backlight() function. This makes the spec harder to understand and leads to misinterpretations.
One last peculiarity is the need for a wait_for macro. Since the functions in the Leds module cast messages to the Leds process, they return immediately. So we cannot expect the I2C functions to be called directly after a call to the Leds functions. The wait_for executes its do block repeatedly until the expectations succeed or until a timeout has occurred.
This last problem is not only related to mocking but to any form of testing where the subject under test is performing some actions asynchronously, like the following one.
Another option is to replace the low level IO processes of Nerves with our own ones, that simulate the functionality of the connected hardware. As said before, as long as our fake processes understand the same messages as the original ones and respond to them in the same way, there should be no issue.
When writing simulation modules, always try to keep them as simple as possible. We don't want to mimic every detail of the hardware, only their behaviour at the interface level. For example: we are not using the input register of the PCA9530 controller, so we dont' implement its access in our simulator. The PCA9530 simulator is a direct translation of the datasheet and looks like this:
As you can see, Elixir's pattern matching on binary data comes in quite handy when developing embedded hardware.
And here are the specifications that use this simulator:
I think that this is much better to read because it precisely expresses our expectations of the system behaviour. We only need to stub the I2C.start_link/2 function so that our own I2C process is returned.
The main drawback with this method is, that our PCA9530 simulator can of course contain programming errors. So i repeat my hint: keep your test code as simple as possible.
Testing the reducers is the easiest thing to do. Since the reducer functions are pure functions with no side effects, we simply prepare a given store state and call a reducer with this state and an action. We then compare the new state with the desired outcome. Lets look for example at the reducers that are called when button one is pressed on any page:
For each page we specify the page to go to when the button was pressed.
Here is a more complex example, where we specify the behaviour of our alarm logic.
Instead of testing only some concrete examples, a better option could be to use a property based testing tool like ExCheck . With ExCheck we can specify the behaviour of our system for all possible system states and actions. We can for example explicitly state that for every time that is before any sunrise start time, the alarm state should stay idle. And for any time that passes any sunrise start time, the alarm state should switch to sunrise (in reality though, only a (configurable) number of random test points will be checked).
For the subscribers we have the same options as for the input/output modules. You can either implement simulators for the lower level processes or you can use the stubbing mechanisms of a testing framework.
Subscribers are a somewhat easier to test than input/output modules, since in most cases a single subscriber contains exactly one single side effect.
On this last page of my article i wanted to demonstrate that developing thoroughly tested embedded software with Elixir and Nerves can be a great pleasure (instead of a necessary evil).
The easiness with which we can write our specifications to test our design, the terse programming style of Elixir which almost always avoids the need to write boilerplate code together with the robustness of the Erlang platform, lets us write better embedded software in a shorter amount of time.
Currently Elixir/Nerves has the downsides that we need to run a Linux kernel and also that the Erlang-VM has a big footprint. So we need a microprocessor and the necessary amount of RAM to run our system.
With the Gulon VM we may in the future get an Erlang VM, that can run on systems with much more constrained resources.
The boot time of the Linux kernel could also be a problem. On a Raspberry Pi Zero, it takes about 12 seconds until the Erlang VM is loaded and executes our Nerves application. On some devices like an IP telephone this is not a big deal, since the users are accustomed to see a "Booting up..." message on these devices. But with other devices like the alarm clock in this project, users might expect them to be instantaneously ready for use.
But as microcontrollers become more and more powerful, these problems will definitely disappear.