Aug 12, 2024 Umbrella? Poncho? Go Naked!

There have been two prevalent methods for getting a Phoenix web server onto a Nerves embedded device: umbrella projects, and poncho projects. This is often done to host a browser-based user interface, although it can also host an API for a mobile app to connect to the device.

Umbrella projects are an Elixir design pattern to configure and manage multiple applications in a project. These applications are siblings of each other, rather than dependencies. Therefore a top-level project to organize them makes more sense than including them as any particular application’s dependencies. In a Nerves umbrella architecture, the Nerves project generated by mix nerves.new needs to be the umbrella application and contain the Nerves configuration. This is because when Nerves bundles the firmware image, you want it to bundle up all the applications below it. The firmware and Phoenix applications would then be siblings under the umbrella.

Poncho projects are a way to maintain the separation between the firmware and web applications, but without the need for the top-level shell application used in an umbrella project. In the poncho architecture, the project generated with mix nerves.new contains the Nerves configuration as well as the firmware application. A Phoenix project is generated and added as a firmware application dependency. Firmware and UI can still be maintained as separate projects.

Challenges


Something to be aware of when working with ponchos is that the Nerves project configuration is the one that is used when bundling the firmware. This means that your nice isolated UI project also has config files that must be duplicated in the Nerves config. A Phoenix configuration isn’t exactly trivial.

The thing that doesn't sit right with me about poncho projects is the reverse dependency issue: A dependency on the Phoenix project is declared in the Nerves project mix.exs file. However, the Phoenix application is the one making function calls into the Nerves firmware. Although this physically works due to the way the BEAM compiles files, I feel like this setup is misleading and harder to understand the relationship between components.

There is another elephant in the room as well: The claim for separating the UI from the firmware is for ease of development, without being tied to the hardware. But Nerves can run on the host development machine, firmware and all. We use resolve to inject virtual components on the host that physically exist on the hardware, like sensors. The other claim is that the UI can be versioned separately from the firmware. Although this is a nice thought, in practice I've seen the UI and firmware developed in lockstep. Agile product development focuses on delivering complete features, and that means a feature being developed in the UI is also going to have its firmware portion completed before that feature is released. This also allows for QA testing the vertical feature slice, rather than having dormant code lurking in the codebase.

A new approach


My favorite approach is to run Phoenix directly inside of the Nerves project. This looks very similar to a web-based Phoenix project: there is lib/project for the business logic and lib/project_web for the UI (presentation layer). The subtle difference in a Nerves firmware project is that instead of lib/project being context modules that interact with a database, this is your firmware, gathering data from sensors and interacting with other devices.


The downside to this approach is that it requires merging the output of two project generators. The good news is that it only happens when starting a new project, and becomes more comfortable the more you get familiar with Nerves and Phoenix.

Walkthrough


Visit the Naked Phoenix runbook in the Redwire Labs Nerves Guide for a walkthrough of combining a Phoenix web-based UI into a Nerves firmware project.