Elixir "poncho" project with Nerves firmware and Phoenix UI

Elixir "poncho" project with Nerves firmware and Phoenix UI

Today I learned how to make a [poncho project] with the Nerves firmware and Phoenix UI.

Poncho projects

Poncho projects are an project structure alternative to Umprella projects.

Here is an example file structure.

├── README.md
├── hello_poncho_firmware
└── hello_poncho_ui

The folder names can be verbose if a top-level project name is prepended to projects. I once considered to shorten them somehow, but I decided to go with long names because it is nice to be obvious about what exactly they are.

My dev environment

MacOS BigSur 11.6

elixir          1.12.3-otp-24
erlang          24.1

How-to (A): hello_phoenix example

The easiest way to do it is just clone the Nerves Project's official hello_phoenix example. The Nerves core team and the community keep the example up-to-date. It should just work.

Clone the example project, then follow the instructions in hello_phoenix README.

cd some/location
git clone git@github.com:nerves-project/nerves_examples.git
cd nerves_examples/hello_phoenix

How-to (B): From scratch

We can make a poncho app like hello_phoenix quite easily. Here is what it boils down to.

Create a base project

cd some/location

# Decide on the project name

# Create the project directory and move into it

# Create README.md
echo "$(curl -L https://raw.githubusercontent.com/nerves-project/nerves_examples/main/hello_phoenix/README.md)" > README.md

# Create Nerves firmware project
mix archive.install hex nerves_bootstrap
mix nerves.new "$MY_PROJECT_NAME"_firmware

# Create Phoenix UI project
mix archive.install hex phx_new
mix phx.new "$MY_PROJECT_NAME"_ui --no-ecto --no-mailer

Adjust the UI project for the firmware project

We want to keep eslint from getting loaded at runtime. It causes the firmware to crash on load.

# hello_poncho/hello_poncho_ui/mix.exs

  defp deps do
      {:phoenix, "~> 1.6.0"},
      # ...
      {:esbuild, "~> 0.2", runtime: Mix.env() == :dev && Mix.target() == :host},
      # ...

Add the UI project as a dependency to the firmware project

# hello_poncho/hello_poncho_firmware/mix.exs

  defp deps do
      # Dependencies for all targets
      {:nerves, "~> 1.7", runtime: false},
      # ...
      {:hello_poncho_ui, path: "../hello_poncho_ui", targets: @all_targets, env: Mix.env()},
      # ...

Configure web server in the firmware project

According to the Nerves official User Interfaces documentation:

If we're using a poncho project structure, we'll need to keep in mind that the my_app_ui configuration won't be applied automatically, so we should either import it from there or duplicate the required configuration.

To me personally, I prefer to keep all the necessary settings in hello_poncho_firmware/config/target.exs.

# hello_poncho/hello_poncho_firmware/config/target.exs

# as of phoenix 1.6.2
config :hello_poncho_ui, MyAppUiWeb.Endpoint,
  url: [host: "nerves.local"],
  http: [port: 80],
  cache_static_manifest: "priv/static/cache_manifest.json",
  secret_key_base: "HEY05EB1dFVSu6KykKHuS4rQPQzSHv4F7mGVB/gnDLrIu75wE/ytBXy2TaL3A6RA",
  live_view: [signing_salt: "AAAABjEyERMkxgDh"],
  check_origin: false,
  render_errors: [view: MyAppUiWeb.ErrorView, accepts: ~w(html json), layout: false],
  pubsub_server: Ui.PubSub,
  # Start the server since we're running in a release instead of through `mix`
  server: true,
  # Nerves root filesystem is read-only, so disable the code reloader
  code_reloader: false

# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

Configure WiFi in the firmware project (optional)

Optionally we can edit the configuration for wlan0. We could either hard code our WiFi settings or provide them through environment variables.

# hello_poncho/hello_poncho_firmware/config/target.exs

config :vintage_net,
  regulatory_domain: "US",
  config: [
    {"usb0", %{type: VintageNetDirect}},
       type: VintageNetEthernet,
       ipv4: %{method: :dhcp}
       type: VintageNetWiFi,
       vintage_net_wifi: %{
         networks: [
             key_mgmt: :wpa_psk,
             ssid: System.get_env("NERVES_WIFI_SSID"),
             psk: System.get_env("NERVES_WIFI_PSK")
       ipv4: %{method: :dhcp}

Develop the UI

When developing the UI, we can simply run the Phoenix server from the hello_poncho/hello_poncho_ui project directory.

cd path/to/hello_poncho/hello_poncho_ui
iex -S mix phx.server

Deploy the firmware

First we build our assets in the hello_poncho/hello_poncho_ui project directory.

cd path/to/hello_poncho/hello_poncho_ui

# Specify our target device.
export MIX_TARGET=rpi0

mix deps.get

# This needs to be repeated when you change JS or CSS files.
mix assets.deploy

When it's time to deploy firmware to our hardware, we can do it from the hello_poncho/hello_poncho_firmware project directory.

cd path/to/hello_poncho/hello_poncho_firmware

# Specify our target device.
export MIX_TARGET=rpi0

mix deps.get

# Build the firmware.
mix firmware

# Connect the MicroSD card to our host machine, then burn the firmware.
mix firmware.burn

Visit nerves.local and we will see the familiar Phoenix page that is hosted on our target device.

For subsequent firmware updates can be done through the network.

mix firmware

mix upload nerves.local


Error resolving dependencies

Sometimes I have an issue resolving dependencies for some reason (TODO: What exactly happened?)

I remember these command fixed the issue

  • mix deps.clean --all
  • rm rf _build deps

Also make sure that both the firmware and the UI have the same MIX_ENV and MIX_TARGET set.

Verify the UI config is loaded correctly

In case, we need to check the config, we can run Application.get_env/2 from our taget device's IEx console.

Application.get_env :hello_poncho_ui, HelloPonchoUiWeb.Endpoint

Final Thoughts

At first, the poncho project looked scary to me, but it is actually pretty simple. The Nerves firmware combined with the Phoenix UI seems so powerful that I may consider using this pattern for my other Nerves projects.

That's it! Here are some resources I read and found helpful.