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.
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.
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
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.
# 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
# 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:
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}},
{"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.
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
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.
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.