# 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.

[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/
[Umprella projects]: https://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-projects.html

## Poncho projects

[Poncho projects] are an project structure alternative to [Umprella projects].

Here is an example file structure.

```
hello_poncho
├── 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].

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

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

### Add the UI project as a dependency to 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_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)

Optionally we can edit the configuration for `wlan0`. We could 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

# 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.

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

### 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.

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

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

