DEV Community

Cover image for Elixir "poncho" project with Nerves firmware and Phoenix 1.6 UI
Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on • Updated on

Elixir "poncho" project with Nerves firmware and Phoenix 1.6 UI

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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

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
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
Enter fullscreen mode Exit fullscreen mode

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.

# 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
Enter fullscreen mode Exit fullscreen mode

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

Add the UI project to the dependencies of 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()},
      # ...
    ]
  end
Enter fullscreen mode Exit fullscreen mode

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/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
Enter fullscreen mode Exit fullscreen mode

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.

# 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}
     }}
  ]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

# 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
Enter fullscreen mode Exit fullscreen mode

When it is 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
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
Enter fullscreen mode Exit fullscreen mode

Visit http://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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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.

# Be sure to specify correct app and module.
Application.get_env(:hello_poncho_ui, HelloPonchoUiWeb.Endpoint)
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)