Elixir "poncho" project with Nerves firmware and Phoenix 1.6 UI
Tags: elixir, nerves, phoenix, liveview
Published: 2021-06-12 21:05:51

Today I learned how to make [Poncho projects] with the [Nerves] firmware and [Phoenix] UI.

## Poncho projects

[Poncho projects] are project structure alternative to [Umbrella projects].

Here is an example file structure.

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

The directory names can be verbose if a top-level project name is prepended to projects. I have considered shortening them before, but after all I go with long names because it is nice to be obvious about what exactly the directories are for.

## My dev environment

```
MacOS BigSur 11.6

elixir          1.12.3-otp-24
erlang          24.1

nerves          1.7.11
phoenix         1.6.2
```

## How-to (A): [hello_phoenix] example

The easiest way to do it is just clone the [Nerves] official [hello_phoenix] example. The [Nerves] core team and the community keep the example up-to-date. It should just work. In case it does not, you should do your best to contribute and fix it with the community.

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

```bash
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

```sh
cd some/location

# Decide on the project name
MY_PROJECT_NAME=hello_poncho

# Create the project directory and move into it
mkdir $MY_PROJECT_NAME && cd $MY_PROJECT_NAME

# Create README.md copying the content from hello_phoenix
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 `esbuild` from getting loaded at runtime. It causes the firmware to crash on load.

```elixir
# hello_poncho/hello_poncho_ui/mix.exs

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

By the way, here is an error I saw in the log on my target device.

![](https://user-images.githubusercontent.com/7563926/136958188-3e636ed5-f2da-4012-a43f-37776acaf79d.png)

### Add the UI project to the dependencies of the firmware project

```elixir
# 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()},
      # ...
    ]
  end
```

### Configure web server in the firmware project

According to the [Nerves official User Interfaces documentation](https://hexdocs.pm/nerves/user-interfaces.html#phoenix-web-interfaces):

> 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/hello_poncho_firmware/config/target.exs`.

```elixir
# 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)

This step is not required if we connect to our target device over USB gadget mode.

We can set up WiFi by editing the configuration for `wlan0`. We can either hard code our WiFi settings or provide them through environment variables.

```elixir
# hello_poncho/hello_poncho_firmware/config/target.exs

config :vintage_net,
  regulatory_domain: "US",
  config: [
    {"usb0", %{type: VintageNetDirect}},
    {"eth0",
     %{
       type: VintageNetEthernet,
       ipv4: %{method: :dhcp}
     }},
    {"wlan0",
     %{
       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.

```bash
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.

```bash
cd path/to/hello_poncho/hello_poncho_ui

# We want to build assets on our host machine.
export MIX_TARGET=host
export MIX_ENV=dev

mix deps.get

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

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

```bash
cd path/to/hello_poncho/hello_poncho_firmware

# Specify our target device.
export MIX_TARGET=rpi0
export MIX_ENV=dev

mix deps.get

# Build the firmware.
mix firmware

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

Visit http://nerves.local and we will see the familiar [Phoenix] page that is hosted on our target device.

![](https://user-images.githubusercontent.com/7563926/136714201-a43c7b44-f2f6-427a-bce2-c52a69c89c48.gif)

For subsequent firmware updates can be done through the network.

```bash
mix firmware

mix upload nerves.local
```

## Troubleshooting

- I remember most errors go away after running `rm rf _build deps` and building the firmware again.
- Also make sure that both the firmware and the UI have the same `MIX_ENV` set.

The errors I saw includes:

- Error resolving dependencies
- `scrub-otp-release.sh: ERROR: Unexpected executable format for ...`

```
* creating _build/rpi0_dev/rel/firmware/releases/0.1.0/vm.args
Updating base firmware image with Erlang release...
scrub-otp-release.sh: ERROR: Unexpected executable format for '/Users/mnishiguchi/src/nerves_examples/hello_phoenix/firmware/_build/_nerves-tmp/rootfs_overlay/srv/erlang/lib/file_system-0.2.10/priv/mac_listener'

Got:
 file:Mach-O 64-bit executable x86_64

Expecting:
 readelf:ARM;0x5000400, Version5 EABI, hard-float ABI

This file was compiled for the host or a different target and probably
will not work.

Check the following:

1. Are you using a path dependency in your mix deps? If so, run
   'mix clean' in that directory to avoid pulling in any of its
   build products.

2. Did you recently upgrade to Elixir 1.9 or Nerves 1.5?
   Nerves 1.5 adds support for Elixir 1.9 Releases and requires
   you to either add an Elixir 1.9 Release configuration or add
   Distillery as a dependency. Without this, the OTP binaries
   for your build machine will get included incorrectly and cause
   this error. See
   https://hexdocs.pm/nerves/updating-projects.html#updating-from-v1-4-to-v1-5

3. Did you recently upgrade or change your Nerves system? If so,
   try cleaning and rebuilding this project and its deps.

4. Are you building outside of Nerves' mix integration? If so,
   make sure that you've sourced 'nerves-env.sh'.

If you are very sure you know what you are doing, you may place an empty
file in the same directory as the offending file(s) called '.noscrub'.
This will explicitly disable scrubbing for that directory.

If you're still having trouble, please file an issue on Github
at https://github.com/nerves-project/nerves_system_br/issues.

** (Mix) Nerves encountered an error. %IO.Stream{device: :standard_io, line_or_bytes: :line, raw: true}
```

### Check the UI configuration

When the UI configuration is wrong or not loaded, the UI won't work.
We can check the UI configuration by invoking [`Application.get_env/3`] from our taget device's IEx console.

```elixir
# Be sure to specify correct app and module.
Application.get_env(:hello_poncho_ui, HelloPonchoUiWeb.Endpoint)
```

## Final Thoughts

At first, the poncho project looked scary, 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.

## Resources

- [Poncho Projects | embedded-elixir.com](https://embedded-elixir.com/post/2017-05-19-poncho-projects/)
- [Nerves Phoenix Web interfaces](https://hexdocs.pm/nerves/user-interfaces.html#phoenix-web-interfaces)
- [nerves_examples/hello_phoenix](https://github.com/nerves-project/nerves_examples/tree/main/hello_phoenix)

<!-- Links -->

[Nerves]: https://www.nerves-project.org/
[Phoenix]: http://www.phoenixframework.org/
[livedashboard]: https://github.com/phoenixframework/phoenix_live_dashboard
[hello_phoenix]: https://github.com/nerves-project/nerves_examples/tree/main/hello_phoenix
[hello_phoenix README]: https://github.com/nerves-project/nerves_examples/tree/main/hello_phoenix
[Poncho projects]: http://embedded-elixir.com/post/2017-05-19-poncho-projects/
[Umbrella projects]: https://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-projects.html
[`Application.get_env/3`]: https://hexdocs.pm/elixir/1.12/Application.html#get_env/3
[esbuild]: https://hexdocs.pm/esbuild/Esbuild.html