# Introduction

This is a guide to developing commercial products with [Nerves](https://nerves-project.org/) based on the lessons learned by the team at [Redwire Labs](https://www.redwirelabs.com/). Product development can be a challenging journey, so we are sharing this resource with the community so that you can avoid the pitfalls we have seen and shortcut the path to success.

*Redwire Labs is a product development agency that specializes in commercial IoT products built with Nerves and Elixir.*

## Why we use Nerves

In our experience, Nerves has been an excellent technology that expedites the development of connected products, and is built on a platform that supports robustness and reliability.

Nerves is the framework for running an [Elixir](https://elixir-lang.org/) application on embedded Linux. It uses [Buildroot](http://buildroot.org/) to create a custom Linux system that's tailored to your project and boots the system into the application (as process 1), opposed to running a Linux distro, which tends to be heavier weight and comes with extra cruft. Nerves also provides facilities to update firmware in the field, and an A/B partition scheme that allows for rolling back in the case of a failed firmware update.

Elixir is used as the main application programming language. From a firmware development perspective, this allows developers to reason about the system at a higher level, avoiding the need to worry about things like memory management and segmentation faults. However, developers still have the low-level tools they need to interface with hardware, like bitwise operations. From the perspective of developing a connected product, Elixir is a language that can be used for both the firmware and backend server, making it easier for developers to understand each side of the system. Our full stack engineers are even able to develop features in vertical slices through the firmware and backend codebases.

The [BEAM VM](https://www.erlang-solutions.com/blog/erlangs-virtual-machine-the-beam/) runs the Elixir application, and is known for its use in developing low-latency, distributed, and fault-tolerant systems. It also comes with [OTP](https://www.erlang.org/doc/), which can be thought of as the standard library. OTP provides much more functionality out of the box than most other languages, like [state machines](https://www.erlang.org/doc/man/gen_statem), [directed graphs](https://www.erlang.org/doc/man/digraph), and [ETS tables](https://www.erlang.org/doc/man/ets) (in-memory database).


# Config files

An Elixir project configuration resides in the `<project>/config` directory. When creating a new Nerves project, the official Nerves project generator creates configuration files for the host (development computer) and target (hardware). While this makes things simple for people learning Nerves, we prefer a more configurable, granular approach for commercial projects.

For professional firmware projects, there are two axes we take into consideration: environment, and target. Environment (dev, prod, test) deals with the context in which the firmware is running. For example, in dev we may want to see debug logs over the serial console, whereas in prod where we don't have access to the serial port, we only want to write errors to disk to reduce flash wear. Target, on the other hand, is the hardware platform that the firmware is running on. This could be a development kit, production PCB, or even the computer used to develop the code or run CI. Since different targets have different capabilities, we need to turn various functionality on/off, select different drivers, or inject mocks depending on the target the firmware is running on.

To do this, we add a directory for environment configuration files, and a directory for target configuration files. The top level config directory includes `config.exs` which applies to all environments and targets, `target.exs` which applies to all targets, and `runtime.exs` which is able to access environment variables at runtime instead of compile time. The file structure looks like this:

```
config
├── env
│   ├── dev.exs
│   ├── prod.exs
│   └── test.exs
├── target
│   ├── host.exs
│   └── imx8.exs
├── config.exs
├── runtime.exs
└── target.exs
```

## Mix Task

It can be a fair amount of work to set up these config files by hand, so the `mix red.nerves.new` task is available from the [Redwire Tasks](https://github.com/redwirelabs/redwire_tasks) package.


# Development workstation

The right equipment will help significantly during development. The wrong equipment can be incredibly painful.

## Operating system

We have standardized on Linux for Nerves development, as well as for firmware development in general. The architecture should be `x86_64` to avoid issues with cross-compilation and host tooling.

Linux has become well-supported by many vendors, and provides tools and paradigms that make it easier to develop software. Since Nerves firmware runs embedded Linux, Linux experience can translate between the desktop, server, and embedded.

The Linux distribution is less critical. The important part is for a developer to be proficient with their Linux environment. For developers who are not yet experienced with Linux, we recommend starting with [Ubuntu Desktop](https://ubuntu.com/download/desktop) due to its wide support and ability to find information about it easily with a web search.

## Virtualization

It is a good idea to create the development environment inside of a virtual machine. This offers several benefits:

* Isolation of development data from business software.
* Isolated networking.
* Dedicated disk encryption on the virtual machine's virtual drive.
* Periodic snapshots that can be restored in case of a disaster.
* Portability to other computers in case of hardware failure or upgrades.

We prefer [VMware Workstation Pro](https://www.vmware.com/products/workstation-pro.html) for virtualization. A free alternative we have had good experience with is [Oracle VirtualBox](https://www.virtualbox.org/), but it is more limited.

## Hardware selection

For application firmware development that does not involve compiling Nerves systems, a computer with above-average specs will likely be sufficient. This will be the case if a platform development team creates the artifacts for an application development team.

For Nerves system development (platform development), we prefer [AMD Threadripper](https://www.amd.com/en/processors/ryzen-threadripper-pro) workstations. These high performance workstations are capable of compiling several Nerves systems per hour from a clean build, significantly reducing the amount of time it takes to iterate through the development of a Nerves system.

### More cores or faster clock?

When looking at Threadripper CPUs, there is a point where you have to choose between significantly more cores at the cost of significantly lower base clock speed. More cores will benefit parallel workloads, whereas a faster clock is better when workloads are single-threaded or don't parallelize well.

So is Nerves system compilation parallel or single-threaded? Well, unfortunately it's a mix of both. Linux and Erlang are packages that support multi-core compilation. A lot of the ancillary packages tend to compile single-threaded.

> ⚠️ **Trap**
>
> *"I can write a script that compiles the ancillary packages in parallel to utilize the idle cores!"*
>
> Buildroot already has a setting to enable [parallel builds](https://buildroot.org/downloads/manual/manual.html#top-level-parallel-build). The catch isn't compiling packages in parallel, it's setting up the dependency tree between packages so that the right ones can compile in parallel. Nerves just hasn't been optimized for this yet. It's a topic we occasionally revisit, but in general having the right workstation has mitigated most of this pain point.

The following diagram illustrates the difference between cores and clock speed for the Threadripper Pro 7000 series. For this CPU series, a good balance between cores and clock speed is the 7975WX 32 core 4.0GHz CPU. Also note that as of January 2024, VMware supports passing through a maximum of 32 cores to a virtual machine.

![](/files/LKIKDt6H4HZp7tKJe0V8)

| Model  | Cores | Base Clock |
| ------ | ----- | ---------- |
| 7945WX | 12    | 4.7        |
| 7955WX | 16    | 4.5        |
| 7965WX | 24    | 4.2        |
| 7975WX | 32    | 4.0        |
| 7985WX | 64    | 3.2        |
| 7995WX | 96    | 2.5        |


# Nerves systems for commercial products

The Nerves systems and development kits below are reference designs for creating the PCB and Nerves board support for your product. Prototyping can start on the development kit with one of these Nerves systems. When your PCB is manufactured, create a hard fork of the Nerves system, name it after your product, and modify it for your board.

If you need support for a hardware target, [Redwire Labs](https://www.redwirelabs.com/) can create a custom Nerves system for your project.

| Target                                  | System                                                                                                  | Tag    |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------ |
| ADLINK MXA-200                          | [Contact us](https://redwirelabs.com/#contact)                                                          |        |
| <p>Compulab</p><p>IOT-GATE-IMX8PLUS</p> | [nerves\_system\_iot\_gate\_imx8plus](https://github.com/redwirelabs/nerves_system_iot_gate_imx8plus)   | `gate` |
| Microchip SAMA5D27                      | [nerves\_system\_sama5d27\_wlsom1\_ek](https://github.com/redwirelabs/nerves_system_sama5d27_wlsom1_ek) | `sam`  |
| SolidRun Pulse                          | [Contact us](https://redwirelabs.com/#contact)                                                          |        |
| Texas Instruments AM62x                 | [nerves\_system\_ti\_am62x](https://github.com/redwirelabs/nerves_system_ti_am62x)                      | `am62` |
| Texas Instruments AM335x                | [nerves\_system\_bbb](https://github.com/nerves-project/nerves_system_bbb)                              | `bbb`  |
| Toradex Verdin AM62                     | [Contact us](https://redwirelabs.com/#contact)                                                          |        |
| Toradex Verdin i.MX 8                   | [Contact us](https://redwirelabs.com/#contact)                                                          |        |
| Toradex Ivy                             | [Contact us](https://redwirelabs.com/#contact)                                                          |        |

## Compulab IOT-GATE-IMX8PLUS

Industrial IoT gateway

[Manufacturer website](https://www.compulab.com/products/iot-gateways/iot-gate-imx8plus-industrial-arm-iot-gateway/)

<figure><img src="/files/2sABeWI5CLDS5etIis2n" alt=""><figcaption></figcaption></figure>

## Microchip SAMA5D27

500 MHz ARM Cortex-A5

[Manufacturer website](https://www.microchip.com/en-us/development-tool/dm320117)

![](/files/ozT2f4UR4qNWMHl0nTsj)

## Texas Instruments AM62x

Up to quad 1.4 GHz ARM Cortex-A53

[Manufacturer website](https://www.ti.com/tool/SK-AM62)

![](/files/sEM5ISAyohpk9lpMjUKQ)

## Texas Instruments AM335x

1 GHz ARM Cortex-A8

[Manufacturer website](https://www.ti.com/lit/ds/symlink/am3356.pdf)

![](/files/u5XqrSi5U7ZLdYtuw2bh)


# Serial cable

A serial cable is required for professional development. This is necessary to connect to the microprocessor's console port, which will output logs on bootup, allow access to the U-Boot console, and the Elixir IEx console. It is also helpful for troubleshooting other devices with serial ports, like microcontrollers, cellular modems, etc.

Serial cables come in many different varieties, and it doesn't hurt to have different types to suit the different projects you may work on. The two most important things to consider when selecting a serial cable are the electrical interface and connector type. Most development kits will expose a 3.3V TTL interface via a 0.1" pitch header, which is designed to connect to the [FTDI TTL-232R-3V3](https://www.digikey.com/en/products/detail/ftdi-future-technology-devices-international-ltd/TTL-232R-3V3/1836393) cable. FTDI's drivers are also included with modern versions of Linux and Windows, so the cables work out of the box. On Linux the serial port will show up as `/dev/ttyUSB0`.

Applications like `picocom` or `screen` can be used to connect to a serial port from the terminal.

![](/files/xAXycmeuuF4gCHfOMlHe)

[FTDI TTL-232R-3V3](https://www.digikey.com/en/products/detail/ftdi-future-technology-devices-international-ltd/TTL-232R-3V3/1836393)

> ⚠️ **Trap**
>
> *"I can buy a serial cable for much less on Amazon."*
>
> An off-brand cable may not come with drivers, may not support your operating system, may not follow the FTDI pinout, and may not be the right voltage. A serial cable will get used frequently and will be with you a long time. Get the quality cable.

## Tag Connect

Another popular connector for the console port is Tag Connect's 6-pin footprint. This tends to be found on prototype and production PCBs, and saves space. The connectors can be ordered with or without "legs". The legs are the plastic clips that hold the connector onto the PCB. The legged version is intended for prototyping, whereas the no legs version is designed to be mounted in a fixture. The no legs version also has a smaller footprint. If you need to use a no legs cable without a fixture, check out the [GRIP-6](https://www.tag-connect.com/product/grip-6-3-pack).

![](/files/l4y7A5WC1t6qCu5kKEv1)

[Tag Connect TC2030-FTDI-TTL-232R-3V3](https://www.tag-connect.com/product/tc2030-ftdi-ttl-232r-3v3)

## Jumper Wires

Sometimes it may be necessary to use an FTDI cable with a serial port header that doesn't match the FTDI pinout. In this case, 0.1" jumper wires can be used to connect the pins from the serial cable to the correct pins on the header.

![](/files/mX5ttbyQqm6NicAbuHJp)

[6" Mixed Jumper Wires](https://www.sparkfun.com/products/9194)

In this example, jumper wires are connected between the FTDI cable and a Raspberry Pi:

<div align="left"><figure><img src="/files/m8NU64O5iRLKNtzA5w0B" alt="" width="321"><figcaption></figcaption></figure></div>

## Gadget Mode

> ⚠️ **Trap**
>
> *"The Nerves docs say I can connect to the hardware with USB* [*gadget mode*](https://hexdocs.pm/nerves/connecting-to-a-nerves-target.html#gadget-mode-virtual-serial-connection)*."*

The official Nerves systems expose the IEx console via a USB gadget mode network interface. When a USB cable is connected between the computer and dev kit, an SSH connection can be used to get to the Nerves IEx console on the dev kit.

Although this is great for trying out Nerves, it's not a good method for professional development. Standing up or modifying a Nerves system often involves needing to read the logs while the system boots, before the SSH daemon comes up. It can also be necessary to access U-Boot before the kernel starts. The best way to do this is by connecting a serial cable. Not to mention your product typically shouldn't expose its console to end users when they plug in a USB cable.


# Authorize SSH key at runtime

To add an authorized SSH key at runtime, connect to the device's console with a serial cable or OTA platform. At the IEx prompt, replace your public key in the following snippet and paste it into IEx. You should now be able to SSH into the device with the new key.

```elixir
key = "<public key>"
Application.put_env(:nerves_ssh, :authorized_keys, [key], persistent: true)
Application.stop(:nerves_ssh); Application.ensure_all_started(:nerves_ssh)
```

The authorized keys will persist after a reboot.

## Background

When building the firmware bundle for a Nerves application (`mix firmware`), the SSH public key of the build computer is baked into the authorized keys of the firmware. This is defined in the Nerves application's `config/target.exs`. This is convenient for development, allowing the computer the firmware was built on to SSH into the device after it's flashed. If another computer tries to SSH into the device, it will be blocked and a password prompt will appear. This computer is locked out of the device.

```
$ ssh 192.168.1.2
SSH server
Enter password for "nerves"
password: 
```

This is good as a general security practice, but inconvenient when another computer is legitimately trying to connect to the device. This can be the case if more than one computer is used for development, multiple developers share hardware, or keys are baked into (or removed in) a CI pipeline.

Sharing SSH keys is bad practice for security, so it is better to add all of the authorized keys for the devices that should have SSH access.

## Authorize SSH keys at compile time

The device's authorized keys are set in the project's [`config/target.exs`](https://github.com/nerves-project/nerves_bootstrap/blob/main/templates/new/config/target.exs).

```elixir
config :nerves_ssh, authorized_keys: [...]
```

The source code above this line in `target.exs` searches the development computer for SSH keys. This can be removed and `authorized_keys` can be set from another source, like a hard-coded list, a file of authorized keys, or using the [GitHub SSH key API](https://docs.github.com/en/rest/users/keys?apiVersion=2022-11-28#list-public-ssh-keys-for-the-authenticated-user) to get the keys of your developers.

> 🛈 Tip
>
> The Redwire Labs [Nerves project generator](https://github.com/redwirelabs/redwire_tasks) creates a project that will pull users' SSH keys from GitHub at compile time if the environment variable `SSH_GITHUB_USERS` is set to a space-separated list of GitHub user names.


# Naked Phoenix

Run a Phoenix web-based UI directly within a Nerves firmware project. No umbrella, no poncho.

The Nerves project will be the main project, and the basis for the firmware. Phoenix boilerplate will be pulled into the Nerves project. This guide uses Elixir 1.17.2-otp-27, Erlang 27.0.1, Phoenix 1.17.14, and Nerves 1.11.1. You may need to adapt the source code examples on other versions.

Start by running the Nerves project generator first.

```
mix nerves.new --target=bbb my_firmware
```

Create the Phoenix project out of tree from the firmware, with the same name as the firmare project. This project will be thrown away at the end. The `--module` param is intentionally omitted here, as using it doesn’t work as well.

```
mix phx.new --no-ecto --no-mailer my_firmware
```

Copy the Phoenix web files to the Nerves project. The context layer won’t be copied.

```
cp <phoenix>/lib/my_firmware_web.ex <nerves>/lib/
cp -r <phoenix>/lib/my_firmware_web <nerves>/lib/
cp -r <phoenix>/assets <nerves>/
cp -r <phoenix>/priv <nerves>/
```

Copy the Phoenix project dependencies into the Nerves project’s `mix.exs` file. The host dependencies will need the host target specified so that they only run on the development computer, not on the firmware.

{% code title="mix.exs" %}

```elixir
defp deps do
  [
    # Dependencies for all targets
    {:nerves, "~> 1.10", runtime: false},
    {:shoehorn, "~> 0.9.1"},
    {:ring_logger, "~> 0.11.0"},
    {:toolshed, "~> 0.4.0"},

    # Allow Nerves.Runtime on host to support development, testing and CI.
    # See config/host.exs for usage.
    {:nerves_runtime, "~> 0.13.0"},

    # Dependencies for all targets except :host
    {:nerves_pack, "~> 0.7.1", targets: @all_targets},

    # Dependencies for specific targets
    # NOTE: It's generally low risk and recommended to follow minor version
    # bumps to Nerves systems. Since these include Linux kernel and Erlang
    # version updates, please review their release notes in case
    # changes to your application are needed.
    {:nerves_system_bbb, "~> 2.19", runtime: false, targets: :bbb},

    # Phoenix dependencies
    {:phoenix, "~> 1.7.14"},
    {:phoenix_html, "~> 4.1"},
    {:phoenix_live_reload, "~> 1.2", only: :dev},
    {:phoenix_live_view, "~> 1.0.0-rc.1", override: true},
    {:floki, ">= 0.30.0", only: :test},
    {:phoenix_live_dashboard, "~> 0.8.3"},
    {:esbuild, "~> 0.8", runtime: Mix.env() == :dev, targets: :host},
    {:tailwind, "~> 0.2", runtime: Mix.env() == :dev, targets: :host},
    {:heroicons,
     github: "tailwindlabs/heroicons",
     tag: "v2.1.1",
     sparse: "optimized",
     app: false,
     compile: false,
     depth: 1},
    {:telemetry_metrics, "~> 1.0"},
    {:telemetry_poller, "~> 1.0"},
    {:gettext, "~> 0.20"},
    {:jason, "~> 1.2"},
    {:dns_cluster, "~> 0.1.1"},
    {:bandit, "~> 1.5"}
  ]
end
```

{% endcode %}

Copy the Phoenix aliases to the Nerves project.

{% code title="mix.exs" %}

```elixir
def project do
  [
    # ...
    aliases: aliases()
  ]
end

defp aliases do
  [
    setup: ["deps.get", "assets.setup", "assets.build"],
    "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
    "assets.build": ["tailwind my_firmware", "esbuild my_firmware"],
    "assets.deploy": [
      "tailwind my_firmware --minify",
      "esbuild my_firmware --minify",
      "phx.digest"
    ]
  ]
```

{% endcode %}

In `lib/my_firmware/application.ex`, copy the child processes and `config_change` callback. `DNSCluster` can be removed from the list of children.

```elixir
defmodule MyFirmware.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children =
      [
        MyFirmwareWeb.Telemetry,
        {Phoenix.PubSub, name: MyFirmware.PubSub},
        MyFirmwareWeb.Endpoint
      ] ++ children(Nerves.Runtime.mix_target())

    opts = [strategy: :one_for_one, name: MyFirmware.Supervisor]
    Supervisor.start_link(children, opts)
  end

  defp children(:host) do
    []
  end

  defp children(_target) do
    []
  end

  @impl true
  def config_change(changed, _new, removed) do
    MyFirmwareWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end

```

For Nerves projects we like to use an Elixir config that is split out both by target and environment. There is a little more ceremony to set it up, but it provides for much more fine-grained configuration once set up, and will make it easier to migrate over the Phoenix configuration.

Change the Nerves project `config/config.exs` to use the following structure.

```elixir
import Config

# Configuration files are applied in the following order:
#
# 1. config/config.exs
# 2. config/target.exs
# 3. config/target/<platform>.exs
# 4. config/env/<environment>.exs

Application.start(:nerves_bootstrap)

config :my_firmware,
  target: Mix.target(),
  env: Mix.env()

config :logger, backends: [RingLogger]

config :nerves, :firmware, rootfs_overlay: "rootfs_overlay"

config :nerves, source_date_epoch: "1723330274"

if Mix.target() != :host,
  do: import_config "target.exs"

import_config "target/#{Mix.target()}.exs"
import_config "env/#{Mix.env()}.exs"

```

Change the `config` directory to look like the following tree. Move the `host.exs` file to the `target` directory, and create the remaining files. Phoenix config will be brought over in the next step.

```
config
├─ env
│  ├─ dev.exs
│  ├─ prod.exs
│  └─ test.exs
├─ target
│  ├─ bbb.exs
│  └─ host.exs
├─ config.exs
├─ runtime.exs
└─ target.exs
```

Merge `config.exs`

```elixir
import Config

# Configuration files are applied in the following order:
#
# 1. config/config.exs
# 2. config/target.exs
# 3. config/target/<platform>.exs
# 4. config/env/<environment>.exs

Application.start(:nerves_bootstrap)

config :my_firmware,
  target: Mix.target(),
  env: Mix.env(),
  generators: [timestamp_type: :utc_datetime]

config :logger, backends: [RingLogger]

config :nerves, :firmware, rootfs_overlay: "rootfs_overlay"

config :nerves, source_date_epoch: "1723330274"

# Configures the endpoint
config :my_firmware, MyFirmwareWeb.Endpoint,
  url: [host: "localhost"],
  adapter: Bandit.PhoenixAdapter,
  render_errors: [
    formats: [html: MyFirmwareWeb.ErrorHTML, json: MyFirmwareWeb.ErrorJSON],
    layout: false
  ],
  pubsub_server: MyFirmware.PubSub,
  live_view: [signing_salt: "CCcgfn6u"]

# Configure esbuild (the version is required)
config :esbuild,
  version: "0.17.11",
  my_firmware: [
    args:
      ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
  ]

# Configure tailwind (the version is required)
config :tailwind,
  version: "3.4.3",
  my_firmware: [
    args: ~w(
      --config=tailwind.config.js
      --input=css/app.css
      --output=../priv/static/assets/app.css
    ),
    cd: Path.expand("../assets", __DIR__)
  ]

# Configures Elixir's Logger
config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id]

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

if Mix.target() != :host,
  do: import_config "target.exs"

import_config "target/#{Mix.target()}.exs"
import_config "env/#{Mix.env()}.exs"
```

In `target.exs`, copy over the `Endpoint` config and modify it to run on the hardware. Generate a `secret_base_key` that will be used for development. `server: true` is set here so that the Phoenix server will start as part of the Erlang release bundle. `code_reloader` is disabled on the hardware since the assets won’t change and it could be an attack vector.

```elixir
config :my_firmware, MyFirmwareWeb.Endpoint,
  server: true,
  url: [host: "nerves.local", port: 80, scheme: "http"],
  http: [ip: {0, 0, 0, 0}, port: 80],
  code_reloader: false,
  check_origin: false,
  debug_errors: true,
  secret_key_base: "Si+zfZ2mAG9PptCX0OzAwXh0RzQ757VLFywjD33p0jEQDZFLjCBu33nw8q+QijoY"
```

The web server can also be added to the advertised mDNS services in `target.exs` if it should be discoverable on the network.

```elixir
# Advertise the following services over mDNS.
services: [
  %{
    protocol: "http",
    transport: "tcp",
    port: 80
  }
]
```

Merge `dev.exs`

```elixir
import Config

# Enable dev routes for dashboard and mailbox
config :my_firmware, dev_routes: true

# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"

# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20

# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime

config :phoenix_live_view,
  # Include HEEx debug annotations as HTML comments in rendered markup
  debug_heex_annotations: true,
  # Enable helpful, but potentially expensive runtime checks
  enable_expensive_runtime_checks: true
```

Merge `prod.exs`

```elixir
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the `mix assets.deploy` task,
# which you should run after static files are built and
# before starting your production server.
config :my_firmware, MyFirmwareWeb.Endpoint,
  cache_static_manifest: "priv/static/cache_manifest.json"

# Do not print debug messages in production
config :logger, level: :info
```

Merge `test.exs`. `code_reloader` is disabled.

```elixir
import Config

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :my_firmware, MyFirmwareWeb.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 4002],
  secret_key_base: "5JDEbgF+J3b4JxnV+Lnk2Awdk5PcjRcsKP234y3zaUB+T5lqI4NK6W/mKmuJePj/",
  code_reloader: false,
  server: false

# Print only warnings and errors during test
config :logger, level: :warning

# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime

# Enable helpful, but potentially expensive runtime checks
config :phoenix_live_view,
  enable_expensive_runtime_checks: true
```

Merge `runtime.exs`. Most of the Phoenix file has been configured elsewhere at this point and can be condensed to the following code. This will still use `SECRET_KEY_BASE` in production, which means this environment variable will need to be flashed into the bootloader during the factory provisioning process. Factory provisioning is out of the scope of this guide.

```elixir
import Config

if config_env() == :prod && config_target() != :host do
  secret_key_base =
    System.get_env("SECRET_KEY_BASE") ||
      raise """
      environment variable SECRET_KEY_BASE is missing.
      You can generate one by calling: mix phx.gen.secret
      """

  config :my_firmware, MyFirmwareWeb.Endpoint, secret_key_base: secret_key_base
end
```

Build the assets and firmware, then burn an SD card.

```
MIX_TARGET=bbb mix deps.get
mix assets.build
MIX_TARGET=bbb mix firmware.burn
```

## Background

There have been two prevalent methods for getting a Phoenix web server onto a Nerves embedded device: umbrella projects, and poncho projects. This is often done to host a browser-based user interface, although it can also host an API for a mobile app to connect to the device.

Umbrella projects are an Elixir design pattern to configure and manage multiple applications in a project. These applications are siblings of each other, rather than dependencies. Therefore a top-level project to organize them makes more sense than including them as any particular application’s dependencies. In a Nerves umbrella architecture, the Nerves project generated by `mix nerves.new` needs to be the umbrella application and contain the Nerves configuration. This is because when Nerves bundles the firmware image, you want it to bundle up all the applications below it. The firmware and Phoenix applications would then be siblings under the umbrella.

Poncho projects are a way to maintain the separation between the firmware and web applications, but without the need for the top-level shell application used in an umbrella project. In the poncho architecture, the project generated with `mix nerves.new` contains the Nerves configuration as well as the firmware application. A Phoenix project is generated and added as a firmware application dependency. Firmware and UI can still be maintained as separate projects.

### Challenges

Something to be aware of when working with ponchos is that the Nerves project configuration is the one that is used when bundling the firmware. This means that your nice isolated UI project also has config files that must be duplicated in the Nerves config. A Phoenix configuration isn’t exactly trivial.

The thing that doesn't sit right with us about poncho projects is the reverse dependency issue: A dependency on the Phoenix project is declared in the Nerves project `mix.exs` file. However, the Phoenix application is the one making function calls into the Nerves firmware. Although this physically works due to the way the BEAM compiles files, we feel like this setup is misleading and harder to understand the relationship between components.

There is another elephant in the room as well: The claim for separating the UI from the firmware is for ease of development, without being tied to the hardware. But Nerves can run on the host development machine, firmware and all. We use [resolve](https://github.com/redwirelabs/resolve) to inject virtual components on the host that physically exist on the hardware, like sensors. The other claim is that the UI can be versioned separately from the firmware. Although this is a nice thought, in practice we have seen the UI and firmware developed in lockstep. Agile product development focuses on delivering complete features, and that means a feature being developed in the UI is also going to have its firmware portion completed before that feature is released. This also allows for QA testing the vertical feature slice, rather than having dormant code lurking in the codebase.

### A new approach

Our preferred approach is to run Phoenix directly inside of the Nerves project. This looks very similar to a web-based Phoenix project: there is `lib/project` for the business logic and `lib/project_web` for the UI (presentation layer). The subtle difference in a Nerves firmware project is that instead of `lib/project` being context modules that interact with a database, this is your firmware, gathering data from sensors and interacting with other devices.

The downside to this approach is that it requires merging the output of two project generators. The good news is that it only happens when starting a new project, and becomes more comfortable the more you get familiar with Nerves and Phoenix.


# Update from a file on the device

From the Nerves application project directory, build the firmware.

```
MIX_TARGET=<target> mix firmware
```

Connect to the device with SFTP and transfer the firmware bundle (`.fw`) onto the device. Modify the lines below to match your project.

```
sftp <device ip address>
cd /data
lcd _build/<target>_dev/nerves/images/
lls
put <app>.fw
quit
```

Connect to the device's console and run `fwup` from IEx to apply the firmware update. Reboot once the update is complete. The device will come back up with the new firmware running.

```elixir
cmd "fwup -a -t upgrade -d /dev/<disk> -i /data/<app>.fw"
reboot
```

The firmware bundle can be removed from the device if it is no longer needed.

```elixir
File.rm "/data/<app>.fw"
```

> 🛈 Tip
>
> `scp` is an alternative to `sftp`. However, on some systems `scp` responds with `lost connection` and fails to transfer the file. The tool used for the file transfer is not important as long as the firmware file makes it onto the device.

## Finding the disk

The fwup `-d` flag specifies the disk to apply the update to. This is documented by the `NERVES_FW_DEVPATH` property in your Nerves system fwup config.

If you need to find the disk without being able to reference the config, run fwup on the device without the `-d` flag.

```elixir
cmd "fwup -a -t upgrade -i /data/<app>.fw"
```

This will attempt to auto-detect the disk and will print what it has discovered. However, the console will hang when doing so.

```
Use 7.95 GB memory card found at /dev/mmcblk0? [y/N]
```

Break out of the command with Ctrl+C. Copy the path to the disk and paste it after the `-d` flag in the full fwup command above.

## Background

A Nerves device doesn't have to receive a firmware update command remotely. [fwup](https://github.com/fwup-home/fwup?tab=readme-ov-file#overview) is used to manage firmware updates, and can be run directly on a Nerves device. This is actually how the OTA update process works under the hood.

In this runbook we manually place a firmware file on the device, and then call `fwup` on the device to perform the firmware update. Once the device is rebooted, it will come up with the new firmware.


