|
|
\subsection{Build infrastructure and automatic deployment}
The CI system, which is based on \textit{drone} \cite{drone} allows to execute commands, whenever a new version is published into the projects \textit{Git} repository. A corresponding \textit{drone} configuration called \texttt{.drone.yml} exists beside the source code (Listing~\ref{lst:drone}). Within this configuration file, settings relevant to the build process are provided to the build environment. First, the \texttt{CONFIG=maglab} option lets the build system use an additional configuration file (\texttt{Configurion.mk.maglab}), which is stored inside the framework repository and provides environment specific information, such as the WiFi SSID. To keep secrets like the WiFi password and the private key unexposed, it is not written down in the configuration file. Instead, to include secrets into a build process while allowing to keep the configuration public, \textit{drone} allows to encrypt these with a repository specific key. Using this method, the password is stored as \texttt{.drone.sec} file inside the repository from where it is injected into the build environment. Also noticeable in Listing~\ref{lst:drone} is the firmware version, which is configured to be the first 8 letters of the \textit{Git} commit hash uniquely identifying a version of the source code.
\begin{lstlisting}[language=, caption={The \textit{drone} configuration for the \textit{ESPer} project.}, label=lst:drone, basicstyle=\ttfamily\scriptsize] build: image: maglab/sming environment: - CONFIG=maglab - WIFI_PWD=$$WIFI_PWD
- VERSION=$${COMMIT:0:8}
commands: - make clean - make publish: sftp: host: eddie.maglab.space username: esper files: [ dist/* ] destination_path: './' when: branch: master \end{lstlisting}
For deployment, only the master branch is considered. After a successful build, all distribution files (the firmware image and meta-information files) of all device types are copied to the repository server, where they are served by a \textit{HTTP 1.1} \cite{HTTP_1.1} server. The configuration file (\texttt{Configurion.mk.maglab}) references this repository server as the source for updates.
Support for multiple devices of different type is implemented in both, the \textit{ESPer} framework itself and the build system. The framework keeps control over the application life-cycle. It ensures that device unspecific code is executed at the right time and provides an API for device specific functionality. For this, a simple interface is specified by the framework, which must be implemented by each device. A single function \texttt{Device* getDevice()} must be defined exactly once in each device specific folder. To implement this interface, a static instance of \texttt{Device} is created and returned. Each \texttt{Device} is populated with device specific \texttt{Feature} instances. While the \texttt{Feature}-API leverages common run time polymorphism to share functionality between features, the initial \texttt{Device} creation uses compile time polymorphism, which reduces the need for memory management and increases performance by avoiding virtual function tables. Listing~\ref{lst:create_device_socket} shows the complete device specific code used for a simple power socket, which is mainly confined to the device type and its capabilities (e.g., the GPIO pin numbers to use).
\begin{lstlisting}[caption={Device specific code for a socket driver.}, label=lst:create_device_socket, basicstyle=\ttfamily\scriptsize] #include "Device.h" #include "features/Socket.h"
Device device:
constexpr const char NAME[] = "socket"; constexpr const uint16_t GPIO = 12; OnOffFeature<NAME, GPIO, false, 1> socket(&device);
Device* getDevice() { return &device; } \end{lstlisting}
The actual compilation of the source code is mainly controlled using two \textit{Makefiles}. The first one is a helper \textit{Makefile} built to accept a parameter for device type identifiers called \texttt{DEVICE}, and to create its whole output inside a subdirectory specific to the device type. In addition, the primary \textit{Makefile} scans a project subdirectory and uses each directory in there as a container for device specific code. For each of these directories, the helper \textit{Makefile} is called and the subdirectories name is used as the value of the \texttt{DEVICE} parameter. By splitting the build and recompiling the framework each time before intermixing it with the device specific code, the device type identifier can be used inside the shared framework code. While building a devices firmware, the meta-information file used during updates is also created and stored beside the firmware image. For development, each device can be build separately by using the device type identifier as \textit{Makefile} target. In addition, the suffix \texttt{/flash} can be used to flash a specific firmware to the device.
While building the firmware images for a device, the build environment provides some constants which are baked into the resulting firmware image. Beside the environmental configuration like the WiFi credentials, \textit{MQTT} topics and other configurable tweaks, the current device and version identifiers are provided as compile time constants. In addition, the public key used to verify firmware signatures during updates is derived from the private key and provided as an linker object which is linked into each firmware image (Listing~\ref{lst:public_key_object}). This allows to use all the information inside the code without any overhead while being configurable during build time.
\begin{lstlisting}[caption={Creating the linker object containing the public key.}, label=lst:public_key_object, basicstyle=\ttfamily\scriptsize] update_key_pub.bin: echo -n "$(UPDATE_KEY)" | \
ecdsakeygen -p | \ xxd -r -p > "$@"
update_key_pub.o: update_key_pub.bin $(OBJCOPY) -I binary $< \ -B xtensa \ -O elf32-xtensa-le $@
\end{lstlisting}
The build process will create the two firmware images, one for each ROM slot, and the meta-information file. To create the meta-information file, the current version identifier is written to the \texttt{.version} file. After the build, the signatures for both firmware images are created and attached to the file.
|