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="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>
</html>
I am available for Elixir/Phoenix consulting work – get in touch to learn more.