Isnor Creative
Isnor Creative Blog
Ruby, Ruby on Rails, Ember, Elm, Phoenix, Elixir, React, Vue

Feb 8, 2016

Raspberry Pi Phoenix Embedded with Nerves and Bake

I’m just getting started with Nerves – which allows you to easily build stripped down firmware images for your microcomputer that boot directly into Elixir very quickly – Raspberry Pi and the world of embedded computing.

Garth Hitchens gave an incendiary talk at ElixirConf 2015 which is a great starting point on the subject: https://www.youtube.com/watch?v=kpzQrFC55q4


Follow the typical steps for a new Phoenix application:

  mix phoenix.new pi_phoenix
  mix do deps.get, compile

Add nerves and nervesoethernet to mix.exs:

  • add nerves and nervesioethernet to def application
  • add nerves and nervesioethernet to deps:
{:nerves, github: "nerves-project/nerves"}
{:nerves_io_ethernet, github: "nerves-project/nerves_io_ethernet"}

Start ethernet: in lib/pi.ex:

def start(_type, _args) do
  {:ok, _pid} = Nerves.IO.Ethernet.setup :eth0

config/config.exs:

config :pi_phoenix, PiPhoenix.Endpoint, 
http: [port: 80],
server: true,
check_origin: ["localhost","127.0.0.1"]

Install Bake

wget https://bakeware.herokuapp.com/bake/install
ruby -e "$(curl -fsSL https://bakeware.herokuapp.com/bake/install)"

Add Bakefile to project root:

use Bake.Config

platform :nerves
default_target :rpi2

target :rpi2,
  recipe: {"nerves/rpi2", "~> 0.1"}
bake system get
bake toolchain get
MIX_ENV=prod bake firmware

Stick your SD card into the drive

Install fwup and then burn the image to your SD

brew install fwup
sudo fwup -a -i _images/pi_phoenix-rpi2.fw -t complete
  • Remove the SD card when ready
  • Put the SD card into the Raspberry Pi
  • Connect the Raspberry Pi to network via ethernet cable
  • Connect the Raspberry Pi to monitor via HDMI cable
  • Power up Raspberry Pi
  • You’ll need to discover the port of your Raspberry Pi – I did this by visiting by my router control panel
  • When you visit the IP of your Raspberry Pi you should now see the typical Welcome to Phoenix startup screen.

Broadcasting LED blinks on Phoenix channels

Once I had this in place, I started blinking GPIO leds, and broadcasting that on a channel, so that I could see the LEDs lighting up on a breadboard at the same time as they were lighting up on any network connected computers who visited the Raspberry PI server IP address.

lib/app.ex example:

defmodule BlinkyChannels do
  use Application
  require Logger
  alias Nerves.IO.Ethernet

  def start(_type, _args) do

    {:ok, _pid} = Nerves.IO.Ethernet.setup :eth0
  
    import Supervisor.Spec, warn: false

    children = [supervisor(BlinkyChannels.Endpoint, [])]
    opts = [strategy: :one_for_one, name: BlinkyChannels.Supervisor]
    Supervisor.start_link(children, opts)
    
    # GPIO LED setup
    leds = [17, 27, 22, 23]
    Enum.each(leds, fn(led) -> start_led(led) end)
    {:ok, spawn(fn -> blink_forever(leds) end)}

  end

  defp start_led(led) do
    :os.cmd('echo #{led} > /sys/class/gpio/export')
    :os.cmd('echo out > /sys/class/gpio/gpio#{led}/direction')
  end

  defp blink_forever(leds) do
    Enum.each(leds, fn(led) -> blink(led) end)
    blink_forever(leds)
  end

  defp blink(led) do
    blink_on(led)
    blink_off(led)
  end
  
  defp blink_on(led) do
    set_gpio_val(led, 1)
    BlinkyChannels.Endpoint.broadcast!("blinko:alert", "new:blink_it", %{led: led})    
    sleep(1000)
  end

  def blink_off(led) do
    set_gpio_val(led, 0)
    BlinkyChannels.Endpoint.broadcast!("blinko:alert", "new:unblink_it", %{})
    sleep(1000)
  end

  defp set_gpio_val(gpio, val) do
    :os.cmd('echo #{val} > /sys/class/gpio/gpio#{gpio}/value')
  end
  
  defp sleep(time) do
    :timer.sleep(time)
  end
  
  def config_change(changed, _new, removed) do
    BlinkyPhoenix.Endpoint.config_change(changed, removed)
    :ok
  end

end

web/static/js/socket.js example

import { Socket } from "deps/phoenix/web/static/js/phoenix";

const socket = new Socket("/socket", {params: {token: window.userToken}});

socket.connect();

const channel = socket.channel("blinko:alert", {});
const dot = document.getElementById("dot");

channel.on("new:blink_it", payload => {
  console.info("new blink on ok");
    let color;
    switch(payload.led) {
    case 17:
       color = 'red'; break;
    case 22:
       color = 'green'; break;
    case 23:
         color = 'yellow'; break;
    case 27:
        color = 'blue'; break;
    }
    dot.className = color;
});

channel.on("new:unblink_it", () => {
  dot.className = "";
});

channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) });

export default socket;

web/channels/user_socket.ex example:

defmodule BlinkyChannels.UserSocket do
  use Phoenix.Socket
  
  channel "blinko:*", BlinkyChannels.BlinkoChannel

  transport :websocket, Phoenix.Transports.WebSocket, check_origin: false

  def connect(_params, socket) do
    {:ok, socket}
  end

  def id(_socket), do: nil

end

channel example:

defmodule BlinkyChannels.BlinkoChannel do

  use BlinkyChannels.Web, :channel

  def join("blinko:alert", _params, socket) do
    {:ok, socket}
  end

  def handle_out(event, payload, socket) do
    push socket, event, payload
    {:noreply, socket}
  end

end

web/template/layout/app.html.eex example

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">
    <title></title>
    <style>
      body    { width: 100vw; height: 100vh; background-color: black; }
      .container { display: flex; justify-content: center; align-items:center; height: 100vh; width: 100vw; }
      .dot { width:300px; border-radius: 300px; height: 300px; border: 1px solid #EEE; }
      .red    { background-color: red; }
      .green  { background-color: green; }
      .yellow { background-color: yellow; }
      .blue   { background-color: blue; }
    </style>
  </head>
  <body>
    <div class="container" role="main"><div id="dot"></div></div>
    <script src="&lt;%= static_path(@conn, "/js/app.js") %&gt;"></script>
  </body>
</html>

I am available for Elixir/Phoenix consulting work – get in touch to learn more.

Gordon B. Isnor

Gordon B. Isnor writes about Ruby on Rails, Ember.js, Elm, Elixir, Phoenix, React, Vue and the web.
If you enjoyed this article, you may be interested in the occasional newsletter.

I am now available for project work. I have availability to build greenfield sites and applications, to maintain and update/upgrade existing applications, team augmentation. I offer website/web application assessment packages that can help with SEO/security/performance/accessibility and best practices. Let’s talk

comments powered by Disqus