dwyl / learn-nerves

Learn how to use the Nerves embedded framework to build incredibly efficient & fault-tollerant network devices (IoT)
https://youtu.be/zBzKoV2iFdQ
GNU General Public License v2.0
7 stars 1 forks source link
# Nerves LED Blinking tutorial A complete tutorial of how to deploy a Nerves application on a Raspberry Pi



Why?

Nerves is a simple to use (IoT) framework that is rock-solid thanks to it running on the BEAM (Erlang virtual machine). However, in order to start building and working on systems it's useful to have a knowledge of how the whole framework fits together and how it interacts with the wider Elixir ecosystem.

Although I have refered to Nerves as a framework, Nerves is better described as a platform, in that you write pure Elixir code and almost never call Nerves functions. Nerves packages up your code and creates Linux firmware image that includes everything you need and nothing more. When the Raspberry Pi has finished booting Nerves starts your Elixir application and its dependencies.

Nerves includes lots of features such as Over-The-Air firmware updates that means you can truly "fire and forget".

What?

This application is deliberately built with extra features expandability in mind. There are simpler tutorials for Blinking lights, but we're aiming to give a broad overview of Nerves and Elixir.

A simple step-by-step that will show you how to:

For simple Nerves applications, thats it! But (Intermediate knowledge of Elixir recommended) we can also add a web-based GUI by:

Who?

This example is for people who are complete beginners with Nerves but some Elixir knowledge will be useful for understanding whats going on. For the second part of the guide a basic knowledge of how GenServers and the BEAM works is recommened, although you should still be able to follow along and work out whats going on.

If you get stuck, open an issue on this GitHub repository and we'll try and fix it. If you get stuck, it's probaly an issue with our guide!

How?

These instructions will demonstrate how to get started from scratch.

0. Pre-requisites

  1. Ensure you have Elixir installed on your localhost (main computer) see: elixir-lang.org/install

Check by running the following command in your terminal:

elixir -v

You should see:

Erlang/OTP 23 [erts-11.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1]
Elixir 1.10.3 (compiled with Erlang/OTP 22)

If your version is higher than 1.10.3 that's good!

  1. Install Nerves on your localhost. Follow the offical documentation to install it on your operating system: https://hexdocs.pm/nerves/installation.html

e.g on Mac you need install a few build utilities in order to compile your IoT App for the target device. Using Homebrew run the following commands:

brew update
brew install fwup squashfs coreutils xz pkg-config

In case you are curious what all those packages are (as we were), here's a summary:

  • fwup is a configurable image-based software update utility for embedded Linux-based systems. It primarily supports software upgrade strategies that update entire root filesystem images at once.
  • squashfs is a compressed read-only file system for Linux.
  • coreutils are the basic file, shell, and text manipulation utilities of the GNU operating system.
  • xz a lossless data compression file format based on the LZMA algorithm. Used to compress the Nerves embedded image during deployment to reduce bandwidth.
  • pkg-config defines and supports a unified interface for querying installed libraries for the purpose of compiling software that depends on them. It takes the hassle out of locating libraries required by Nerves to compile binaries for your target device.

Once you have all the build tools installed, run the following command to install nerves_bootstrap:

mix archive.install hex nerves_bootstrap
  1. Equipment needed: we're using a Raspberry Pi Zero and an external LED. This is a deliberate choice to show how the entire system fits together. Any Recent Raspberry Pi will work, we use the Pi Zero because it's the cheapest one.

    If you want to use the internal LED on your Raspberry Pi, see the example on the Nerve's platform Blinky project: https://github.com/nerves-project/nerves_examples/tree/main/blinky

    The resistor is very important as the LED has almost no internal resistance it could seriously damage your Raspberry Pi as it will try and draw an unlimited amout of current.

1. Wiring Up

Note: this section uses content from the Pi Hut, found at: https://thepihut.com/blogs/raspberry-pi-tutorials/27968772-turning-on-an-led-with-your-raspberry-pis-gpio-pins

To connect the LED to the Raspberry Pi, we are going to use the General Purpose Input/Output (GPIO) pins. The GPIO pins (or holes in the case of the Pi Zero) are located on the right of the board:

Raspberry-Pi-GPIO-Pinout-Diagram

On the Pi Zero (or Zero W) the GPIO layout is identical:

Raspberry-Pi-Zero-GPIO-Pinout-Diagram

If any of these keywords are unfamiliar to you, Google is your friend.

For more info, see: https://www.raspberrypi.org/documentation/usage/gpio

To power the LED we will need to use a GPIO pin, which when turned on outputs 3.3v and a ground pin which is at a constant 0v. For this guide, we'll use Pin 18.

Wire everything up like this:

Wiring Layout

The "black" wire needs to be plugged into a ground pin, and the "orange wire" needs to be pluged into a GPIO pin (Ideally 18 for the ease of following this tutorial).

TODO: Explain more about breadboards

2. Creating a nerves application.

If you managed to install Nerves correctly in Step #0, you should be able to create a new Nerves application by running:

mix nerves.new smart_led

where smart_led is the name of our project.

When prompted to install required dependencies, type Y followed by Enter to install everything.

Lets break down the message returned by the project generator:

Your Nerves project was created successfully.

Yay! - If you get any errors here, ready them carefully, it will normally say what went wrong.
If you are still unsure, please create an issue.

You should now pick a target. 
See https://hexdocs.pm/nerves/targets.html#content
for supported targets. 
If your target is on the list, 
set `MIX_TARGET` to its tag name:

For example, for the Raspberry Pi Zero you can either
  $ export MIX_TARGET=rpi0
Or prefix `mix` commands like the following:
  $ MIX_TARGET=rpi0 mix firmware

If you will be using a custom system, update the `mix.exs`
dependencies to point to desired system's package.

Whats a target? The nerves documentation says:

The platform for which your firmware is built (for example, Raspberry Pi, Raspberry Pi 2, or Beaglebone Black).

From this we can see that we need to specify a target to develop our application for. We want to be able to develop on our current computer, but run the finished code on our deployment target. Nerves has a solution for this. Our current, and default target is host, or our current computer. This means we can easily test and run our nerves code in development.

Open a new terminal tab. We'll use two terminal tabs, one with our MIX_TARGET var set to host to develop with, and one with our MIX_TARGET set to the device your going to deploy on. Lets visit the link that was suggested to us, https://hexdocs.pm/nerves/targets.html#content and pick our target tag.

Find your device tag, this will look something like rpi0 and use it to setup the second terminal tab.

export MIX_TARGET=<Your tag>

In our case we are using a Raspberry Pi Zero W, so our export is:

export MIX_TARGET=rpi0

Now download the dependencies and build a firmware archive:

cd smart_led
mix deps.get

You should see output similar to the following:

Nerves environment
  MIX_TARGET:   rpi0
  MIX_ENV:      dev

Resolving Nerves artifacts...
  Resolving nerves_system_rpi0
  => Trying https://github.com/nerves-project/nerves_system_rpi0/releases/download/v1.12.1/nerves_system_rpi0-portable-1.12.1-0D8B7B0.tar.gz
|==================================================| 100% (141 / 141) MB
  => Success
  Resolving nerves_toolchain_armv6_rpi_linux_gnueabi
  => Trying https://github.com/nerves-project/toolchains/releases/download/v1.3.2/nerves_toolchain_armv6_rpi_linux_gnueabi-darwin_x86_64-1.3.2-CDA7B05.tar.xz
|==================================================| 100% (55 / 55) MB
  => Success

This tells us that the nerves toolchain for compiling for the Raspberry Pi downloaded successfully.

Next run:

mix firmware

If your target boots up using an SDCard (like the Raspberry Pi), then insert an SDCard into a reader on your computer and run:

mix firmware.burn

You will see something similar to this in your terminal:

Building /Users/yourname/smart_led/_build/rpi0_dev/nerves/images/smart_led.fw...
Use 15.05 GiB memory card found at /dev/rdisk4? [Yn] y
100% [====================================] 31.75 MB in / 34.24 MB out
Success!
Elapsed time: 9.024 s
n@MBP smart_led %

Plug the SDCard into the target and power it up.

3. Simple blinking LED module

Open up the project folder we just created in your favourite code editor, in our case we ran:

code smart_led/ # open the project in VSCodium

In this folder you'll find the standard Elixir/Mix project layout. We'll leave the config folder for now as Nerves ships with some sane defaults. In the next chapter we'll look at adding target specific configs and features.

If you look in the lib/smart_led folder and open application.ex you'll find a supervisor tree that looks a little different than usual. Take the time to read through the comments in this file. There are 3 lists defined in this file, one for Children for all targets, one for Children for the host, and one for any other targets. This lets us fine tune how are processes run.

We want to be able to blink an LED, this won't work on the host computer so we only want to run it on the target. To do this lets define a child process in the last function for our targets like so:

def children(_target) do
  [
    SmartLed.LedController
  ]
end

e.g: smart_led/lib/smart_led/application.ex#L17

By doing this the BEAM will start and supervise the SmartLed.LedController module when our application starts. If it crashes, it will just get restarted and we won't have to worry.

If we attempt to run the application now it won't work as we haven't yet defined the module SmartLed.LedController. Lets do that now.

Create a new file with the path lib/smart_led/led_controller.ex
In this file we are going to define a GenServer that will control our lights. Having a GenServer will help us call our lights from another process later.

Start by defining our standard Elixir boilerplate by creating our module.
Add use GenServer to import all of the required GenServer functions and define some good defaults.

defmodule SmartLed.LedController do
  use GenServer
end

Those of you with well setup errors will get a linting error now as GenServer expects init/1 to be defined, so lets do that now.

  def init(_opts) do
    send(self(), :blink)
    {:ok, opts}
  end

init/1 takes one argument that contains options passed to it by the function that starts the GenServer. In the function body we send a :blink message to ourselves. This will get queued up in our GenServer's mailbox and processed once we're fully set up. We then finish setting up our GenServer by telling returning a tuple with the :ok message and our GenServer's state. In this case we'll just set to opts in case we need to access these later.

Our GenServer then needs to be able to handle the :blink message. GenServers handle incoming messages through the handle_info/2 callback so lets create one for our blink message:

  def handle_info(:blink, state) do
    Process.send_after(self(), :blink, 2000)
    blink_led()

    {:noreply, state}
  end 

Lets break this down line by line:

To Blink the LED we need to call a library to run the barebones code of telling the Raspberry Pi to open and and close the GPIO Pin. Luckily, Elixir Circuits will do this for us. Add {:circuits_gpio, "~> 0.4"} to your mix deps in mix.exs like so:

defp deps do
  [
    ...
    {:circuits_gpio, "~> 0.4"},
    ...
  ]

We don't need to worry about only using this on specific targets as Circuits automatically works out on what type of device its running on. Lets go back to our code and implement our LED Blinking.

We need to define blink_led/0:

  alias Circuits.GPIO

  defp blink_led() do
    {:ok, gpio} = GPIO.open(18, :output)
    GPIO.write(gpio, 1)
    :timer.sleep(100)
    GPIO.write(gpio, 0)
    GPIO.close(gpio)
  end

Once again, lets break this down line by line.

Finally, we need to write one more function that will connect our new GenServer to the application supervisor, if you have written GenServers before this will look very familiar

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

This starts the GenServer process with the current module, gives it the options from the supervisor and names the new process the same as the module.

4. Deploying your application.

In the terminal where you set your MIX_TARGET environment variables, build and deploy the firmware.

Lost it? just run export MIX_TARGET=<Your tag> again

First of all, lets download the dependencies for the target

mix deps.get

This will take a while as it will download the firmware needed for your device.

Next, Plug your SD card into your main computer and run:

mix firmware.burn

It will ask you to confirm the correct SD card, double check this as you could overwrite something important!

Remove the SD Card from your computer and insert it into the Raspberry Pi, power up the Pi and within 30 seconds it the LED should start to blink!

TODO: Add networking and GUI

See: https://github.com/dwyl/learn-nerves/issues/3