Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Nerves Project
BADGE HACKING WORKSHOP
▸ Introduction
▸ Definitions
▸ Nerves Intro
▸ Host Tools
▸ Targets
▸ Host / Target Prep
What are we going to Do?
▸ Interfacing with Hardware
▸ Boot to IEx
▸ Pin Muxing
▸ Blinky with elixir_ale
▸ Blinky with Firmata and Arduino
What are...
▸ Building the Badge
▸ Project Layout
▸ Arduino and Firmata
▸ Connecting to Twitter
▸ Web Interfaces
What are we going to ...
▸ Advanced Configuration
▸ Modifying a Nerves System
▸ Initialization with erlinit
▸ Firmware Updates
▸ Licensing
What are ...
Introduction
SECTION 1
▸ Introduction
▸ Definitions
▸ Nerves Intro
▸ Host Tools
▸ Targets
▸ Host / Target Prep
What are we going to Do?
DEFINITIONS
1.1
▸ host
▸ target
▸ toolchain
▸ system
Definitions
▸ artifact
▸ assemble
▸ firmware bundle
▸ firmware image
host
The computer on which you are editing source code,
compiling, and assembling firmware
Definitions
target
The platform for which your firmware is built (for
example, Raspberry Pi, Raspberry Pi 2, or Beaglebone
Black)
Defin...
toolchain
The tools required to build code for the target, such as
compilers, linkers, binutils, and C runtime
Definitions
system
A lean Buildroot-based Linux distribution that has
been customized and cross-compiled for a particular
target
Defin...
assembly
The process of combining system, application, and
configuration into a firmware bundle
Definitions
firmware bundle
A single file that contains an assembled version of
everything needed to burn firmware
Definitions
firmware image
Built from a firmware bundle and contains the partition
table, partitions, bootloader, etc.
Definitions
NERVES
1.2
Nerves
Your Elixir project
and dependencies
Linux, C libraries,
Erlang runtime
Nerves System
Image
OTP release
Firmware bu...
Compiling on your machine
YOUR APP
ELIXIR
C CODE
NIF / PORTS
MIX
BEAM
BINARY
YOUR APP
(ARCH SPECIFIC)
Mixing firmware
MIX
YOUR APP
(FOR RPI2)
TOOLCHAIN
SYSTEM
rpi2
Precompile
compile
Toolchains
TOOLCHAINTOOLCHAIN CONFIG
• crosstool-ng
• for target
• host configs
• compilers
• run on host
• compile for
tar...
What’s in a Nerves system package?
▸ Elixir build infrastructure
▸ mix.exs – build the system image via mix
▸ nerves.exs –...
Nerves Package Artifacts
SYSTEM
NERVES_SYSTEM_RPI3
Package
TOOLCHAIN
SYSTEM (TOOLCHAIN)
Artifact
Create a new Nerves app
New Projects
$ mix nerves.new my_app --target linkit
MIX FILE
defmodule MyApp.Mixfile do
use Mix.Project
@target System.get_env("NERVES_TARGET") || "linkit"
…
end
New Projects
MIX FILE
defmodule MyApp.Mixfile do
…
def project do
[app: :my_app,
version: "0.1.0",
archives: [nerves_bootstrap: "~> 0.1...
MIX FILE
defmodule MyApp.Mixfile do
…
def aliases do
["deps.precompile": ["nerves.precompile", "deps.precompile"],
"deps.l...
MIX FILE
defmodule MyApp.Mixfile do
…
def system(target) do
[{:"nerves_system_#{target}", ">= 0.0.0"}]
end
…
end
New Proje...
Firmware
$ mix firmware
$ mix firmware.burn
Flash layout
Master Boot Record
Bootloaders
Root Filesystem A
Read-only
Root Filesystem B
Read-only
Application Data
Read-...
Nerves root filesystem
.
├── bin
├── dev
├── etc
├── lib
├── mnt
├── proc
├── root
├── sbin
├── srv
├── sys
├── tmp
├── us...
HOST PREP
1.3
NERVES
▸ Erlang OTP ~> 19
▸ Elixir ~> 1.3.2
▸ fwup ~> 0.8.2
▸ squashfs-tools
▸ nerves_bootstrap
Host Tools
▸ Arduino IDE ~...
Host Tools
# copy and untar system and toolchain
$ export NERVES_SYSTEM=/path/to/uncompressed/system
$ export NERVES_TOOLC...
Installation Time
ERLANG & ELIXIR
Host Tools
$ brew update
$ brew install elixir
Mac OS
Linux
https://www.erlang-solutions.com/resources/dow...
FWUP
Host Tools
$ brew install fwup
Download Debian package from https://github.com/fhunleth/fwup/
$ sudo dpkg -i fwup_0.8...
ADDITIONAL TOOLS
Host Tools
Mac OS
Linux
$ brew install squashfs coreutils picocom
$ sudo apt-get install squashfs-tools p...
ADDITIONAL TOOLS
Host Tools
All
$ mix local.hex
$ mix local.rebar
$ mix archive.install https://github.com/nerves-project/...
CONSOLE CABLE DRIVERS
Host Tools
Mac OS
Linux
https://www.adafruit.com/images/product-files/954/
PL2303_MacOSX_1_6_0_20151...
ARDUINO TOOLS
Host Tools
▸ Install Arduino IDE ~> 1.6.0
▸ Arduino -> Preferences -> Additional Boards Manager
URLs
▸ http:...
SYSTEMS / TARGETS
1.4
Official Nerves systems
▸ nerves_system_bbb (black and green)
▸ nerves_system_rpi, nerves_system_rpi2,
nerves_system_rpi3
...
TARGET SPECIFIC CONFIG
# config.exs
use Mix.Config
import_config "#{Mix.Project.config[:target]}.exs"
Multi-Target
CHANGING TARGETS
NERVES_TARGET=linkit mix deps.get
export NERVES_TARGET=linkit
mix deps.get
mix deps.get
# @target System....
Systems
Linkit Smart Raspberry Pi 3
Linkit Smart Duo
ATMega32U4
Linkit Smart
LINKIT SMART
LINKIT SMART
1.5
LinkIt Smart 7688 Flash
▸ 32 MB NAND Flash
▸ Bootloader and Linux kernel stored here
▸ LinkIt bootloader doesn’t support F...
Typical Nerves Flash layout
Master Boot Record
Bootloaders
Root Filesystem A
Read-only
Root Filesystem B
Read-only
Applica...
Flash layout
Master Boot Record
Root Filesystem A
Read-only
Root Filesystem B
Read-only
Application Data
Read-write
Erlang...
The Console
▸ Black - Ground
▸ Red - 5v
▸ White - RX
▸ Green - TX
The Console
▸ Black - Ground
▸ Red - 5v
▸ White(RX) - TX
▸ Green(TX) - RX
Don’t worry about accidentally swapping RX and T...
The Console
▸ Baud: 57600
▸ Bits: 8
▸ Parity: None
▸ Stop Bits: 1
The Console
$ picocom -b 57600 /dev/tty.usbserial
BURN BOOTLOADER
▸ Connect a 3.3V FTDI cable (GND, RX, and TX) to the LinkIt
Smart. Power up the LinkIt Smart and verify th...
BURN BOOTLOADER
▸ Plug the USB Flash drive into the LinkIt Smart via the On-
the-go cable. Make sure that it's plugged int...
BURN LINUX KERNEL
▸ Press the '5' key repeatedly on the serial port while
rebooting the LinkIt Smart.
▸ Stop when you see ...
▸ http://labs.mediatek.com/site/global/developer_tools/
mediatek_linkit_smart_7688/hdk_intro/index.gsp
LinkIt Smart Revert
RECAP
▸ What is Nerves
▸ Definitions
▸ Differences In Targets
▸ Preparing your Host
▸ Preparing the Target
Title
Interfacing Hardware
SECTION 2
▸ Interfacing with Hardware
▸ Boot to IEx
▸ Pin Muxing
▸ Blinky with elixir_ale
▸ Blinky with Firmata and Arduino
What are...
BOOTING TO IEX
2.1
Booting to IEx
$ mix nerves.new console --target linkit
$ cd console
$ mix deps.get
$ mix firmware
$ mix firmware.burn
htt...
Code it up
Adding IEx Helpers
defmodule Console.IExHelpers do
def cat(file) do
File.read!(file)
|> IO.puts
end
end
Adding IEx Helpers
# config/rootfs-additions/etc/iex.exs
import Console.IExHelpers
Adding IEx Helpers
# config/config.exs
config :nerves, :firmware,
rootfs_additions: "config/rootfs-additions"
Adding IEx Helpers
# rel/vm.args
## Start the Elixir shell
-noshell
-user Elixir.IEx.CLI
-extra --no-halt
+iex --dot-iex /...
Adding IEx Helpers
$ mix firmware
$ mix firmware.burn
Adding IEx Helpers
iex(1)> cat "/etc/iex.exs"
import Console.IExHelpers
:ok
PIN MUXING
2.2
The problem
GPIO
SPI
I2C
Camera In
Ethernet
Timer
PWM
MMC
Display
More pins than

fit on a chip!
DRAM
Power
A layer of indirection
GPIO
SPI
I2C
Camera In
Ethernet
Timer
PWM
MMC
Display
DRAM
Power
Pin mux
Physical
pins
Beaglebone Black (TI AM335x)
http://www.embedded-things.com/bbb/beaglebone-black-pin-mux-spreadsheet/
LinkIt Smart (MT7688)
▸ Bootloader
▸ Some pins have to configured as early as possible
▸ Usually only deal with this on custom boards
▸ Linux ker...
▸ Device tree
▸ linux-4.4.14/arch/mips/boot/dts/ralink/LINKIT7688.dts
▸ Compiled to LINKIT7688.dtb
▸ Usermode app - pinmux...
BLINKY ELIXIR ALE
2.3
Positive
Negative
Host Tools
$ mix nerves.new blinky_ale --target linkit
https://github.com/mobileoverlord/blinky_ale
Fast track
MIX FILE
defmodule BlinkyAle.Mixfile do
…
def application do
[mod: {BlinkyAle, []},
applications: [:logger, :elixir_ale]]
...
defmodule BlinkyAle do
use Application
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more informa...
defmodule BlinkyAle do
...
def blink do
:os.cmd '/usr/bin/pinmux set ephy gpio'
{:ok, pid} = Gpio.start_link(43, :output)
...
Code it up
BLINKY FIRMATA
2.4
Arduino D13 LED
Arduino Serial / Power
Running on the Host
// SimpleFirmata.ino
Firmata.begin(57600);
while (!Serial) {
;
}
Arduino Firmata
▸ File -> Examples -> Firmata -> StandardFirmata
Write to the Arduino
Running on the Host
$ mix new blinky_firmata_host
https://github.com/mobileoverlord/blinky_firmata_host
Fast track
Running on the Host
defmodule BlinkyFirmataHost.Mixfile do
...
def application do
[applications: [:logger, :firmata],
mod:...
Running on the Host
defmodule BlinkyFirmataHost.Protocol do
use GenServer
use Firmata.Protocol.Mixin
alias Firmata.Board
d...
Running on the Host
defmodule BlinkyFirmataHost.Protocol do
...
def handle_info({:firmata, {:pin_map, _pin_map}}, s) do
IO...
Running on the Host
Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for
help)
iex(1)> Nerves.UART.enumer...
Running on the Host
iex(2)> BlinkyFirmataHost.Protocol.start_link "/dev/cu.usbmodem1421"
Init
{:ok, #PID<0.156.0>}
Set Pin...
It Blinks!
Running on the Target
// StandardFirmata.ino
// Connecting from LinkIt
Serial1.begin(57600);
Firmata.begin(Serial1);
// Co...
Write to the Arduino
Running on the Target
$ mix nerves.new blinky_firmata --target linkit
https://github.com/mobileoverlord/blinky_firmata
Fas...
Running on the Target
defmodule BlinkyFirmata.Mixfile do
...
def application do
[applications: [:logger, :firmata],
mod: {...
Running on the Target
defmodule BlinkyFirmata.Protocol do
use GenServer
use Firmata.Protocol.Mixin
alias Firmata.Board
def...
Running on the Target
defmodule BlinkyFirmata.Protocol do
...
def handle_info({:firmata, {:pin_map, _pin_map}}, s) do
IO.p...
Running on the Target
defmodule BlinkyFirmata do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: ...
Running on the Target
$ mix firmware
$ mix firmware.burn
Boot it up
It Blinks!
▸ Elixir Ale
▸ Firmata
▸ Ports
▸ When to use which strategy
Recap
Building the Badge
SECTION 3
▸ Building the Badge
▸ Project Layout
▸ Arduino and Firmata
▸ Connecting to Twitter
▸ Web Interfaces
What are we going to ...
PROJECT LAYOUT
3.1
UMBRELLA
▸ Organize facets of code in our project for isolation
▸ Ability to run aspects on Host
Project Layout
Project Layout
$ mix new badge --umbrella
└── badge
├── README.md
├── apps
├── config
│   └── config.exs
└── mix.exs
https...
Project Layout
$ cd badge/apps
$ mix nerves.new badge_fw --target linkit
$ mix new badge_lib
├── badge_fw
└── badge_lib
Project Layout
# apps/badge_fw/mix.exs
def application do
[mod: {BadgeFw, []},
applications: [:logger, :badge_lib]]
end
de...
Code it up
Project Layout
# apps/badge_fw
$ mix deps.get
$ mix firmware
Project Layout
# apps/badge_fw
$ mix deps.get
$ mix firmware
** (UndefinedFunctionError) function :relx.do/2 is undefined ...
INITIALIZATION
3.2
Initialization
# apps/badge_fw/mix.exs
def application do
[mod: {BadgeFw, []},
applications: [:logger, :badge_lib, :nerves...
Initialization
# apps/badge_fw/lib/badge_fw.ex
defmodule BadgeFw do
use Application
alias Nerves.InterimWiFi, as: WiFi
def...
Initialization
# apps/badge_fw/config/config.exs
use Mix.Config
config :badge_fw, :wlan0,
ssid: "Nerves",
key_mgmt: :"WPA-...
Test it out
:inet.gethostbyname 'nerves-project.org'
$ mix firmware
$ mix firmware.burn
Initialization
Initialization
# apps/badge_fw/mix.exs
def application do
[mod: {BadgeFw, []},
applications: [:logger, :badge_lib, :nerves...
# apps/badge_fw/config/config.exs
config :nerves_ntp, :ntpd, "/usr/sbin/ntpd"
config :nerves_ntp, :servers, [
"0.pool.ntp....
# badge_fw/rel/vm.args
-sname badge
-setcookie nerves
## Start the Elixir shell
-noshell
-user Elixir.IEx.CLI
-extra --no-...
remsh
iex --sname host --cookie nerves --remsh badge@nerves-244a
FIRMATA ARDUINO
3.3
Connecting the Components
Vibration motor Display
D9 I2C
$ git clone https://github.com/mobileoverlord/badge_firmata
Arduino Firmata
# badge_lib/mix.exs
def application do
[applications: [:logger, :firmata]]
end
defp deps do
[{:firmata, github: "mobileove...
badge/apps/badge_lib/lib/badge_lib/firmata.ex
New File
Arduino Firmata
Write to the Arduino
Arduino Firmata
defmodule BadgeLib.Firmata do
use GenServer
use Firmata.Protocol.Modes
alias Firmata.Board, as: Board
def ...
Arduino Firmata
defmodule BadgeLib.Firmata do
...
def handle_info({:firmata, {:pin_map, _pin_map}}, s) do
{:noreply, s}
en...
Arduino Firmata
defmodule BadgeLib.Firmata do
use GenServer
use Firmata.Protocol.Modes
alias Firmata.Board, as: Board
@hig...
Arduino Firmata
defmodule BadgeLib.Firmata do
...
def handle_call({:vibrate, state}, _from, s) do
Board.digital_write(s.bo...
Running on the host
Firmata.begin(57600);
while (!Serial) {
;
}
Running on the host
Firmata.begin(57600);
while (!Serial) {
;
}
Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h(...
Running on the host
%{"/dev/cu.Bluetooth-Incoming-Port" => %{}, "/dev/cu.MyQC-SPPDev" => %{},
"/dev/cu.MyQC-SPPDev-1" => %...
Running on the host
iex(3)> BadgeLib.Firmata.vibrate
Arduino Firmata
defmodule BadgeLib.Firmata do
...
@vibration_pulse 300
@vibration_times 7
def vibrate_pulse() do
GenServer...
Arduino Firmata
defmodule BadgeLib.Firmata do
...
def handle_call(:vibrate_pulse, _from, s) do
send(self, {:vibrate_pulse,...
Running on the host
Arduino Firmata
defmodule BadgeLib.Firmata do
...
@display_clear 0x81
@display_text 0x82
@display_time 20_000
def text(mes...
Arduino Firmata
defmodule BadgeLib.Firmata do
...
def handle_call({:text, message}, _from, s) do
Board.sysex_write(s.board...
Running on the host
Arduino Firmata
defmodule BadgeFw do
use Application
alias Nerves.InterimWiFi, as: WiFi
def start(_type, _args) do
import ...
Running on the target
TWITTER
3.4
Connecting to Twitter
# badge_lib/mix.exs
def application do
[applications: [:logger, :firmata, :oauth, :extwitter]]
end
d...
Connecting to Twitter
defmodule BadgeFw.Worker do
use GenServer
def start_link(opts  []) do
GenServer.start_link(__MODULE_...
defmodule BadgeFw do
use Application
alias Nerves.InterimWiFi, as: WiFi
def start(_type, _args) do
import Supervisor.Spec,...
defmodule BadgeFw.Worker do
use GenServer
@hashtag "#NervesBadge"
@handle "@ElixirConf"
@interval 25_000
def init([]) do
B...
def handle_info(:update, %{last: {lhash, luser}} = s) do
{hash, user} = {@handle, @hashtag}
new_hash = get_tweet(hash)
new...
def get_tweet(search) do
case ExTwitter.search(search, [count: 1]) do
[tweet] -> tweet
_ -> nil
end
end
def display_tweet(...
defmodule BadgeLib.Utf8ToASCII do
def convert(string), do: convert(string, <<>>)
def convert(<<c::utf8, rest::binary>>, re...
# badge_fw/config/config.exs
config :extwitter, :oauth, [
consumer_key: "vnBfkubUmv10QRcQjFU3lXKin",
consumer_secret: "XUk...
Running on the target
WEB INTERFACES
3.5
Web Interfaces
$ cd badge/apps
$ git clone https://github.com/lancehalvorsen/badge_settings
# badge_fw/mix.exs
def application do
[mod: {BadgeFw, []},
applications: [:logger, :badge_lib,
:nerves_interim_wifi, :nerv...
Web Interfaces
# badge_fw/config/config.exs
config :badge_settings, :nerves_settings, %{
settings_file: "/root/nerves_sett...
Web Interfaces
$ cd apps/badge_settings
$ mix deps.get
$ npm install
$ ./node_modules/brunch/bin/brunch build --production...
Running on the host
Running on the target
Web Interfaces
def network do
wlan_config =
case settings do
{:ok, settings} ->
[psk: settings.password, ssid: settings.ss...
Web Interfaces
def handle_info(:update, %{last: {lhash, luser}} = s) do
{hash, user} =
case BadgeFw.settings do
{:ok, sett...
RECAP
▸ Project Layout
▸ Initialization
▸ Interfacing with Arduino
▸ Connecting to Services
▸ Web Interfaces with Phoenix
...
Advanced Config
SECTION 4
▸ Advanced Configuration
▸ Modifying a Nerves System
▸ Initialization with erlinit
▸ Firmware Updates
▸ Licensing
What are ...
MODIFYING SYSTEMS
4.1
Reasons to make your own system
▸ Add a library or application that can’t be built using mix
▸ Postgres
▸ Qt
▸ Add a Linux...
Reasons NOT to make your own system
▸ Add files to the root filesystem
▸ These can be added via the rootfs-additions mecha...
Prereqs to working with systems
▸ Concepts
▸ Buildroot
▸ Linux kernel configuration
▸ Busybox
▸ Build and deploy requireme...
Buildroot
▸ Toolchain, bootloader, kernel, root filesystem

builder for embedded Linux
▸ Cross-compiled
▸ Support for buil...
World views
▸ Buildroot normally is the top-level project that builds firmware
images
▸ Nerves uses Buildroot to produce t...
Defconfigs and .configs
▸ Anything using Kconfig to manage configuration options uses
these (Linux, Buildroot, Busybox)
▸ ...
Activity: Start a system build
▸ Clone one of the official Nerves system images
▸ https://github.com/nerves-project/nerves...
Activity: Compile in a new package
▸ Go to the out directory from the last activity
▸ make menuconfig
▸ Find and enable po...
The Linux kernel
▸ Why Linux? Best device driver support for embedded
▸ Nerves uses a trimmed down Linux kernel to keep 

...
Activity: Enable a device driver
▸ Go to the out directory from before
▸ make linux-menuconfig
▸ Enable support for USB->s...
Busybox
▸ Provides tons of Unix apps in one small binary
▸ ls, sh, dd, ps, find, cat, tail, tar, cd, mkdir, etc.
▸ ntpd, d...
Activity: Modify the Busybox config
▸ Go to the out directory from before
▸ make busybox-menuconfig
▸ Enable the ping util...
Using your custom Nerves system
▸ Go to the out directory from before
▸ export NERVES_SYSTEM=$PWD
▸ Now go to your Elixir ...
Publishing a custom Nerves system
▸ What you’ll need
▸ Someplace to hold your nerves_system_xyz
▸ Someplace to hold the bu...
nerves.exs
config :nerves_system_linkit, :nerves_env,
type: :system,
version: version,
mirrors: [
"https://github.com/nerv...
Final steps
▸ Go back to your system’s out directory
▸ make system
▸ Publish the created tarball
▸ Publish the source on h...
ERLINIT
4.2
erlinit
▸ Replacement for /sbin/init that starts the Erlang virtual
machine
▸ Basic initialization of the Linux user land
...
erlinit debugging options
▸ --verbose
▸ --hang-on-exit
▸ Useful to capture error messages when the VM exits
▸ --run-on-exi...
erlinit features
▸ Mount filesystems
▸ Configure a unique hostname
▸ --hostname-pattern and --uniqueid-exec
▸ Nerves uses ...
erlinit pitfalls
▸ Running shell scripts to initialize the system
▸ Move initialization to Elixir to take advantage of OTP...
FIRMWARE UPDATE
4.3
fwup
▸ Firmware update packaging and application
▸ Packages
▸ Zip-formatted archives
▸ Metadata
▸ All data files protected ...
fwup processing
Master Boot Record
FAT Boot partition
Root Filesystem A
Read-only
Root Filesystem B
Read-only
Application ...
Anatomy of a fwup.conf file
▸ Resources
▸ Files that are included as part of the zip archive
▸ Not required to be used whe...
LinkIt Smart fwup.conf
# Let the rootfs have room to grow up to 64 MiB and align
# it to the nearest 1 MB boundary
define(...
LinkIt Smart fwup.conf
# Firmware metadata
meta-product = "Nerves Firmware"
meta-description = ""
meta-version = ${NERVES_...
LinkIt Smart fwup.conf
file-resource rootfs.img {
host-path = ${ROOTFS}
}
file-resource uImage {
host-path = "${NERVES_SYS...
LinkIt Smart fwup.conf
mbr mbr-a {
partition 0 {
block-offset = ${ROOTFS_A_PART_OFFSET}
block-count = ${ROOTFS_A_PART_COUN...
LinkIt Smart fwup.conf
# This firmware task writes everything to the destination media
task complete {
on-init {
mbr_write...
LinkIt Smart fwup.conf
task upgrade.a {
# This task upgrades the A partition
require-partition-offset(0,
${ROOTFS_B_PART_O...
LinkIt Smart fwup.conf
https://github.com/nerves-project/nerves_system_linkit/blob/develop/fwup.conf
task upgrade.b {
# Th...
FAT filesystem commands
Command Description
fat_mkfs(block_offset, block_count) Create a FAT file system
fat_write(block_o...
LICENSING
4.4
Shipping Nerves - Licensing
▸ Buildroot infrastructure in
Nerves can aid process
▸ make legal-info
▸ Other licenses
▸ nerv...
Elixir Conf 2016
Nerves: Connected beyond the Node
Thursday
1:30 PM - 2:15 PM
Track 1
Justin Schneck
Building "learn to to...
Elixir Conf 2016
Thank You
& Nerves
Badge Hacking with Nerves Workshop - ElixirConf 2016 - Justin Schneck and Frank Hunleth
Badge Hacking with Nerves Workshop - ElixirConf 2016 - Justin Schneck and Frank Hunleth
Badge Hacking with Nerves Workshop - ElixirConf 2016 - Justin Schneck and Frank Hunleth
Upcoming SlideShare
Loading in …5
×

Badge Hacking with Nerves Workshop - ElixirConf 2016 - Justin Schneck and Frank Hunleth

675 views

Published on

These are the slides from the Badge Hacking with Nerves workshop from ElixirConf 2016, presented by Justin Schneck and Frank Hunleth, with help from Garth Hitchens, Chris Dutton, and Greg Mefford.

Published in: Engineering
  • Be the first to comment

Badge Hacking with Nerves Workshop - ElixirConf 2016 - Justin Schneck and Frank Hunleth

  1. 1. Nerves Project BADGE HACKING WORKSHOP
  2. 2. ▸ Introduction ▸ Definitions ▸ Nerves Intro ▸ Host Tools ▸ Targets ▸ Host / Target Prep What are we going to Do?
  3. 3. ▸ Interfacing with Hardware ▸ Boot to IEx ▸ Pin Muxing ▸ Blinky with elixir_ale ▸ Blinky with Firmata and Arduino What are we going to Do?
  4. 4. ▸ Building the Badge ▸ Project Layout ▸ Arduino and Firmata ▸ Connecting to Twitter ▸ Web Interfaces What are we going to Do?
  5. 5. ▸ Advanced Configuration ▸ Modifying a Nerves System ▸ Initialization with erlinit ▸ Firmware Updates ▸ Licensing What are we going to Do?
  6. 6. Introduction SECTION 1
  7. 7. ▸ Introduction ▸ Definitions ▸ Nerves Intro ▸ Host Tools ▸ Targets ▸ Host / Target Prep What are we going to Do?
  8. 8. DEFINITIONS 1.1
  9. 9. ▸ host ▸ target ▸ toolchain ▸ system Definitions ▸ artifact ▸ assemble ▸ firmware bundle ▸ firmware image
  10. 10. host The computer on which you are editing source code, compiling, and assembling firmware Definitions
  11. 11. target The platform for which your firmware is built (for example, Raspberry Pi, Raspberry Pi 2, or Beaglebone Black) Definitions
  12. 12. toolchain The tools required to build code for the target, such as compilers, linkers, binutils, and C runtime Definitions
  13. 13. system A lean Buildroot-based Linux distribution that has been customized and cross-compiled for a particular target Definitions
  14. 14. assembly The process of combining system, application, and configuration into a firmware bundle Definitions
  15. 15. firmware bundle A single file that contains an assembled version of everything needed to burn firmware Definitions
  16. 16. firmware image Built from a firmware bundle and contains the partition table, partitions, bootloader, etc. Definitions
  17. 17. NERVES 1.2
  18. 18. Nerves Your Elixir project and dependencies Linux, C libraries, Erlang runtime Nerves System Image OTP release Firmware bundle
  19. 19. Compiling on your machine YOUR APP ELIXIR C CODE NIF / PORTS MIX BEAM BINARY YOUR APP (ARCH SPECIFIC)
  20. 20. Mixing firmware MIX YOUR APP (FOR RPI2) TOOLCHAIN SYSTEM rpi2 Precompile compile
  21. 21. Toolchains TOOLCHAINTOOLCHAIN CONFIG • crosstool-ng • for target • host configs • compilers • run on host • compile for target
  22. 22. What’s in a Nerves system package? ▸ Elixir build infrastructure ▸ mix.exs – build the system image via mix ▸ nerves.exs – additional system image information such as where to find pre-built system images ▸ Buildroot configuration ▸ nerves_defconfig – top level configuration options ▸ Custom package definitions ▸ Linux kernel configuration and patches ▸ Board-specific root file system additions
  23. 23. Nerves Package Artifacts SYSTEM NERVES_SYSTEM_RPI3 Package TOOLCHAIN SYSTEM (TOOLCHAIN) Artifact
  24. 24. Create a new Nerves app
  25. 25. New Projects $ mix nerves.new my_app --target linkit
  26. 26. MIX FILE defmodule MyApp.Mixfile do use Mix.Project @target System.get_env("NERVES_TARGET") || "linkit" … end New Projects
  27. 27. MIX FILE defmodule MyApp.Mixfile do … def project do [app: :my_app, version: "0.1.0", archives: [nerves_bootstrap: "~> 0.1"], target: @target, deps_path: "deps/#{@target}", build_path: "_build/#{@target}", aliases: aliases, deps: deps ++ system(@target)] end end New Projects
  28. 28. MIX FILE defmodule MyApp.Mixfile do … def aliases do ["deps.precompile": ["nerves.precompile", "deps.precompile"], "deps.loadpaths": ["deps.loadpaths", "nerves.loadpaths"]] end end New Projects
  29. 29. MIX FILE defmodule MyApp.Mixfile do … def system(target) do [{:"nerves_system_#{target}", ">= 0.0.0"}] end … end New Projects
  30. 30. Firmware $ mix firmware $ mix firmware.burn
  31. 31. Flash layout Master Boot Record Bootloaders Root Filesystem A Read-only Root Filesystem B Read-only Application Data Read-write Linux kernel Erlang C libraries and apps OTP release App settings Database Logs Other files
  32. 32. Nerves root filesystem . ├── bin ├── dev ├── etc ├── lib ├── mnt ├── proc ├── root ├── sbin ├── srv ├── sys ├── tmp ├── usr └── var ├── srv │ └── erlang │ ├── lib │ │ ├── compiler-7.0 │ │ ├── elixir-1.3.2 │ │ ├── kernel-5.0 │ │ ├── logger-1.3.1 │ │ ├── your_app-0.1.0 │ │ ├── sasl-3.0 │ │ └── stdlib-3.0 │ └── releases │ ├── 0.0.1 │ │ ├── your_app.boot │ │ ├── your_app.rel │ │ ├── your_app.script │ │ ├── sys.config │ │ └── vm.args │ ├── RELEASES │ └── start_erl.data
  33. 33. HOST PREP 1.3
  34. 34. NERVES ▸ Erlang OTP ~> 19 ▸ Elixir ~> 1.3.2 ▸ fwup ~> 0.8.2 ▸ squashfs-tools ▸ nerves_bootstrap Host Tools ▸ Arduino IDE ~> 1.6 BADGE HACKING
  35. 35. Host Tools # copy and untar system and toolchain $ export NERVES_SYSTEM=/path/to/uncompressed/system $ export NERVES_TOOLCHAIN=/path/to/uncompressed/toolchain Cached Nerves System and Nerves Toolchain Since bandwidth is limited Make sure you unset after the conference.
  36. 36. Installation Time
  37. 37. ERLANG & ELIXIR Host Tools $ brew update $ brew install elixir Mac OS Linux https://www.erlang-solutions.com/resources/download.html http://elixir-lang.org/install.html
  38. 38. FWUP Host Tools $ brew install fwup Download Debian package from https://github.com/fhunleth/fwup/ $ sudo dpkg -i fwup_0.8.2_amd64.deb Mac OS Linux
  39. 39. ADDITIONAL TOOLS Host Tools Mac OS Linux $ brew install squashfs coreutils picocom $ sudo apt-get install squashfs-tools picocom $ sudo vigr # Add yourself to the dialout group dialout:x:20:yourusername
  40. 40. ADDITIONAL TOOLS Host Tools All $ mix local.hex $ mix local.rebar $ mix archive.install https://github.com/nerves-project/archives/raw/master/ nerves_bootstrap.ez
  41. 41. CONSOLE CABLE DRIVERS Host Tools Mac OS Linux https://www.adafruit.com/images/product-files/954/ PL2303_MacOSX_1_6_0_20151022.zip "Everything just works on Linux" ~Frank Hunleth
  42. 42. ARDUINO TOOLS Host Tools ▸ Install Arduino IDE ~> 1.6.0 ▸ Arduino -> Preferences -> Additional Boards Manager URLs ▸ http://download.labs.mediatek.com/ package_mtk_linkit_smart_7688_test_index.json ▸ Tools -> Board: ... -> Boards Manager ▸ Search: linkit -> install 0.1.8
  43. 43. SYSTEMS / TARGETS 1.4
  44. 44. Official Nerves systems ▸ nerves_system_bbb (black and green) ▸ nerves_system_rpi, nerves_system_rpi2, nerves_system_rpi3 ▸ nerves_system_qemu_arm ▸ nerves_system_alix, nerves_system_ag150 ▸ nerves_system_galileo ▸ nerves_system_linkit
  45. 45. TARGET SPECIFIC CONFIG # config.exs use Mix.Config import_config "#{Mix.Project.config[:target]}.exs" Multi-Target
  46. 46. CHANGING TARGETS NERVES_TARGET=linkit mix deps.get export NERVES_TARGET=linkit mix deps.get mix deps.get # @target System.get_env("NERVES_TARGET") || "linkit" Multi-Target
  47. 47. Systems Linkit Smart Raspberry Pi 3
  48. 48. Linkit Smart Duo ATMega32U4
  49. 49. Linkit Smart
  50. 50. LINKIT SMART
  51. 51. LINKIT SMART 1.5
  52. 52. LinkIt Smart 7688 Flash ▸ 32 MB NAND Flash ▸ Bootloader and Linux kernel stored here ▸ LinkIt bootloader doesn’t support FTL so don’t update too many times ▸ Linux mtd driver provides access (/dev/mtdblock1, etc.) ▸ MicroSD card ▸ Root filesystems ▸ Application data
  53. 53. Typical Nerves Flash layout Master Boot Record Bootloaders Root Filesystem A Read-only Root Filesystem B Read-only Application Data Read-write Linux kernel Erlang C libraries and apps OTP release App settings Database Logs Other files
  54. 54. Flash layout Master Boot Record Root Filesystem A Read-only Root Filesystem B Read-only Application Data Read-write Erlang C libraries and apps OTP release App settings Database Logs Other files Bootloader Linux Kernel
  55. 55. The Console ▸ Black - Ground ▸ Red - 5v ▸ White - RX ▸ Green - TX
  56. 56. The Console ▸ Black - Ground ▸ Red - 5v ▸ White(RX) - TX ▸ Green(TX) - RX Don’t worry about accidentally swapping RX and TX. If nothing shows up on
 the console, just swap them.
  57. 57. The Console ▸ Baud: 57600 ▸ Bits: 8 ▸ Parity: None ▸ Stop Bits: 1
  58. 58. The Console $ picocom -b 57600 /dev/tty.usbserial
  59. 59. BURN BOOTLOADER ▸ Connect a 3.3V FTDI cable (GND, RX, and TX) to the LinkIt Smart. Power up the LinkIt Smart and verify that you can see text and type. You should be interacting with the default OpenWRT firmware. ▸ Remove power from the LinkIt Smart LinkIt Smart Prep
  60. 60. BURN BOOTLOADER ▸ Plug the USB Flash drive into the LinkIt Smart via the On- the-go cable. Make sure that it's plugged into the USB Host connector. ▸ Press the 'b' key repeatedly on the serial port while rebooting the LinkIt Smart. ▸ Stop when you see that it is programming the Flash. LinkIt Smart Prep
  61. 61. BURN LINUX KERNEL ▸ Press the '5' key repeatedly on the serial port while rebooting the LinkIt Smart. ▸ Stop when you see that it is programming the Flash. ▸ Should be looking for rootfs on SD LinkIt Smart Prep
  62. 62. ▸ http://labs.mediatek.com/site/global/developer_tools/ mediatek_linkit_smart_7688/hdk_intro/index.gsp LinkIt Smart Revert
  63. 63. RECAP ▸ What is Nerves ▸ Definitions ▸ Differences In Targets ▸ Preparing your Host ▸ Preparing the Target Title
  64. 64. Interfacing Hardware SECTION 2
  65. 65. ▸ Interfacing with Hardware ▸ Boot to IEx ▸ Pin Muxing ▸ Blinky with elixir_ale ▸ Blinky with Firmata and Arduino What are we going to Do?
  66. 66. BOOTING TO IEX 2.1
  67. 67. Booting to IEx $ mix nerves.new console --target linkit $ cd console $ mix deps.get $ mix firmware $ mix firmware.burn https://github.com/mobileoverlord/console Fast track
  68. 68. Code it up
  69. 69. Adding IEx Helpers defmodule Console.IExHelpers do def cat(file) do File.read!(file) |> IO.puts end end
  70. 70. Adding IEx Helpers # config/rootfs-additions/etc/iex.exs import Console.IExHelpers
  71. 71. Adding IEx Helpers # config/config.exs config :nerves, :firmware, rootfs_additions: "config/rootfs-additions"
  72. 72. Adding IEx Helpers # rel/vm.args ## Start the Elixir shell -noshell -user Elixir.IEx.CLI -extra --no-halt +iex --dot-iex /etc/iex.exs
  73. 73. Adding IEx Helpers $ mix firmware $ mix firmware.burn
  74. 74. Adding IEx Helpers iex(1)> cat "/etc/iex.exs" import Console.IExHelpers :ok
  75. 75. PIN MUXING 2.2
  76. 76. The problem GPIO SPI I2C Camera In Ethernet Timer PWM MMC Display More pins than
 fit on a chip! DRAM Power
  77. 77. A layer of indirection GPIO SPI I2C Camera In Ethernet Timer PWM MMC Display DRAM Power Pin mux Physical pins
  78. 78. Beaglebone Black (TI AM335x) http://www.embedded-things.com/bbb/beaglebone-black-pin-mux-spreadsheet/
  79. 79. LinkIt Smart (MT7688)
  80. 80. ▸ Bootloader ▸ Some pins have to configured as early as possible ▸ Usually only deal with this on custom boards ▸ Linux kernel device tree ▸ Textual description of the hardware configuration ▸ Compiled down and loaded early in the Linux boot process ▸ Device tree overlays may be loaded after boot (but not supported on the LinkIt Smart) ▸ Custom programs Configuring pinmux’d processors
  81. 81. ▸ Device tree ▸ linux-4.4.14/arch/mips/boot/dts/ralink/LINKIT7688.dts ▸ Compiled to LINKIT7688.dtb ▸ Usermode app - pinmux ▸ https://github.com/nerves-project/nerves_system_linkit/ tree/develop/package/mtk-linkit ▸ Invoke on the Linkit Smart as: LinkIt Smart pinmux configuration /usr/bin/pinmux set ephy gpio
  82. 82. BLINKY ELIXIR ALE 2.3
  83. 83. Positive Negative
  84. 84. Host Tools $ mix nerves.new blinky_ale --target linkit https://github.com/mobileoverlord/blinky_ale Fast track
  85. 85. MIX FILE defmodule BlinkyAle.Mixfile do … def application do [mod: {BlinkyAle, []}, applications: [:logger, :elixir_ale]] end def deps do [{:nerves, "~> 0.3.0"}, {:elixir_ale, "~> 0.5.6"}] end end Blinky Elixir Ale
  86. 86. defmodule BlinkyAle do use Application # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do import Supervisor.Spec, warn: false # Define workers and child supervisors to be supervised children = [ worker(Task, [fn -> blink end], restart: :transient), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: BlinkyAle.Supervisor] Supervisor.start_link(children, opts) end ... end Blinky Elixir Ale
  87. 87. defmodule BlinkyAle do ... def blink do :os.cmd '/usr/bin/pinmux set ephy gpio' {:ok, pid} = Gpio.start_link(43, :output) blink_forever(pid) end def blink_forever(pid) do Gpio.write(pid, 1) :timer.sleep(1000) Gpio.write(pid, 0) :timer.sleep(1000) blink_forever(pid) end end Blinky Elixir Ale
  88. 88. Code it up
  89. 89. BLINKY FIRMATA 2.4
  90. 90. Arduino D13 LED Arduino Serial / Power
  91. 91. Running on the Host // SimpleFirmata.ino Firmata.begin(57600); while (!Serial) { ; }
  92. 92. Arduino Firmata ▸ File -> Examples -> Firmata -> StandardFirmata
  93. 93. Write to the Arduino
  94. 94. Running on the Host $ mix new blinky_firmata_host https://github.com/mobileoverlord/blinky_firmata_host Fast track
  95. 95. Running on the Host defmodule BlinkyFirmataHost.Mixfile do ... def application do [applications: [:logger, :firmata], mod: {BlinkyFirmataHost, []}] end defp deps do [{:firmata, github: "mobileoverlord/firmata"}] end end
  96. 96. Running on the Host defmodule BlinkyFirmataHost.Protocol do use GenServer use Firmata.Protocol.Mixin alias Firmata.Board def start_link(tty, opts []) do GenServer.start_link(__MODULE__, [tty, opts], name: __MODULE__) end def init([tty, opts]) do IO.puts "Init" {:ok, board} = Board.start_link(tty, opts) {:ok, %{ board: board }} end ... end
  97. 97. Running on the Host defmodule BlinkyFirmataHost.Protocol do ... def handle_info({:firmata, {:pin_map, _pin_map}}, s) do IO.puts "Set Pin Map" Board.set_pin_mode(s.board, 13, @output) send(self, {:blink, 1}) {:noreply, s} end def handle_info({:blink, state}, s) do IO.puts "Blink" Board.digital_write(s.board, 13, state) state = if state == 1, do: 0, else: 1 Process.send_after(self, {:blink, state}, 1000) {:noreply, s} end def handle_info(_, s) do {:noreply, s} end end
  98. 98. Running on the Host Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> Nerves.UART.enumerate %{"/dev/cu.Bluetooth-Incoming-Port" => %{}, "/dev/cu.MyQC-SPPDev" => %{}, "/dev/cu.MyQC-SPPDev-1" => %{}, "/dev/cu.usbmodem1421" => %{manufacturer: "MediaTek Labs", product_id: 43777, vendor_id: 3725}}
  99. 99. Running on the Host iex(2)> BlinkyFirmataHost.Protocol.start_link "/dev/cu.usbmodem1421" Init {:ok, #PID<0.156.0>} Set Pin Map Blink
  100. 100. It Blinks!
  101. 101. Running on the Target // StandardFirmata.ino // Connecting from LinkIt Serial1.begin(57600); Firmata.begin(Serial1); // Connecting from Host // Firmata.begin(57600); // while (!Serial) { // ; // // }
  102. 102. Write to the Arduino
  103. 103. Running on the Target $ mix nerves.new blinky_firmata --target linkit https://github.com/mobileoverlord/blinky_firmata Fast track
  104. 104. Running on the Target defmodule BlinkyFirmata.Mixfile do ... def application do [applications: [:logger, :firmata], mod: {BlinkyFirmata, []}] end defp deps do [{:nerves, "~> 0.3.0"}, {:firmata, github: "mobileoverlord/firmata"}] end end
  105. 105. Running on the Target defmodule BlinkyFirmata.Protocol do use GenServer use Firmata.Protocol.Mixin alias Firmata.Board def start_link(tty, opts []) do GenServer.start_link(__MODULE__, [tty, opts], name: __MODULE__) end def init([tty, opts]) do IO.puts "Init" {:ok, board} = Board.start_link(tty, opts) {:ok, %{ board: board }} end ... end
  106. 106. Running on the Target defmodule BlinkyFirmata.Protocol do ... def handle_info({:firmata, {:pin_map, _pin_map}}, s) do IO.puts "Set Pin Map" Board.set_pin_mode(s.board, 13, @output) send(self, {:blink, 1}) {:noreply, s} end def handle_info({:blink, state}, s) do IO.puts "Blink" Board.digital_write(s.board, 13, state) state = if state == 1, do: 0, else: 1 Process.send_after(self, {:blink, state}, 1000) {:noreply, s} end def handle_info(_, s) do {:noreply, s} end end
  107. 107. Running on the Target defmodule BlinkyFirmata do use Application def start(_type, _args) do import Supervisor.Spec, warn: false # Define workers and child supervisors to be supervised children = [ worker(BlinkyFirmata.Protocol, ["ttyS0"]), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: BlinkyFirmata.Supervisor] Supervisor.start_link(children, opts) end end
  108. 108. Running on the Target $ mix firmware $ mix firmware.burn
  109. 109. Boot it up
  110. 110. It Blinks!
  111. 111. ▸ Elixir Ale ▸ Firmata ▸ Ports ▸ When to use which strategy Recap
  112. 112. Building the Badge SECTION 3
  113. 113. ▸ Building the Badge ▸ Project Layout ▸ Arduino and Firmata ▸ Connecting to Twitter ▸ Web Interfaces What are we going to Do?
  114. 114. PROJECT LAYOUT 3.1
  115. 115. UMBRELLA ▸ Organize facets of code in our project for isolation ▸ Ability to run aspects on Host Project Layout
  116. 116. Project Layout $ mix new badge --umbrella └── badge ├── README.md ├── apps ├── config │   └── config.exs └── mix.exs https://github.com/mobileoverlord/badge Fast track
  117. 117. Project Layout $ cd badge/apps $ mix nerves.new badge_fw --target linkit $ mix new badge_lib ├── badge_fw └── badge_lib
  118. 118. Project Layout # apps/badge_fw/mix.exs def application do [mod: {BadgeFw, []}, applications: [:logger, :badge_lib]] end def deps do [{:nerves, "~> 0.3.0"}, {:badge_lib, in_umbrella: true}] end
  119. 119. Code it up
  120. 120. Project Layout # apps/badge_fw $ mix deps.get $ mix firmware
  121. 121. Project Layout # apps/badge_fw $ mix deps.get $ mix firmware ** (UndefinedFunctionError) function :relx.do/2 is undefined (module :relx is not available) If you are at the top of the umbrella
  122. 122. INITIALIZATION 3.2
  123. 123. Initialization # apps/badge_fw/mix.exs def application do [mod: {BadgeFw, []}, applications: [:logger, :badge_lib, :nerves_interim_wifi]] end def deps do [{:nerves, "~> 0.3.0"}, {:nerves_interim_wifi, "~> 0.1"}, {:badge_lib, in_umbrella: true}] end # apps/badge_fw $ mix deps.get
  124. 124. Initialization # apps/badge_fw/lib/badge_fw.ex defmodule BadgeFw do use Application alias Nerves.InterimWiFi, as: WiFi def start(_type, _args) do import Supervisor.Spec, warn: false :os.cmd('modprobe mt7603e') # Define workers and child supervisors to be supervised children = [ worker(Task, [fn -> network end], restart: :transient), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: BadgeFw.Supervisor] Supervisor.start_link(children, opts) end def network do wlan_config = Application.get_env(:badge_fw, :wlan0) WiFi.setup "wlan0", wlan_config end end
  125. 125. Initialization # apps/badge_fw/config/config.exs use Mix.Config config :badge_fw, :wlan0, ssid: "Nerves", key_mgmt: :"WPA-PSK", psk: "nervesnet"
  126. 126. Test it out :inet.gethostbyname 'nerves-project.org' $ mix firmware $ mix firmware.burn Initialization
  127. 127. Initialization # apps/badge_fw/mix.exs def application do [mod: {BadgeFw, []}, applications: [:logger, :badge_lib, :nerves_interim_wifi, :nerves_ntp]] end def deps do [{:nerves, "~> 0.3.0"}, {:nerves_interim_wifi, "~> 0.1"}, {:nerves_ntp, "~> 0.1"}, {:badge_lib, in_umbrella: true}] end # apps/badge_fw $ mix deps.get
  128. 128. # apps/badge_fw/config/config.exs config :nerves_ntp, :ntpd, "/usr/sbin/ntpd" config :nerves_ntp, :servers, [ "0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org" ] Initialization
  129. 129. # badge_fw/rel/vm.args -sname badge -setcookie nerves ## Start the Elixir shell -noshell -user Elixir.IEx.CLI -extra --no-halt Initialization Enable distributed Erlang
  130. 130. remsh iex --sname host --cookie nerves --remsh badge@nerves-244a
  131. 131. FIRMATA ARDUINO 3.3
  132. 132. Connecting the Components Vibration motor Display D9 I2C
  133. 133. $ git clone https://github.com/mobileoverlord/badge_firmata Arduino Firmata
  134. 134. # badge_lib/mix.exs def application do [applications: [:logger, :firmata]] end defp deps do [{:firmata, github: "mobileoverlord/firmata"}] end Arduino Firmata $ mix deps.get
  135. 135. badge/apps/badge_lib/lib/badge_lib/firmata.ex New File Arduino Firmata
  136. 136. Write to the Arduino
  137. 137. Arduino Firmata defmodule BadgeLib.Firmata do use GenServer use Firmata.Protocol.Modes alias Firmata.Board, as: Board def start_link(opts []) do port = opts[:port] || "ttyS0" speed = opts[:speed] || 57600 serial_opts = [speed: speed] GenServer.start_link(__MODULE__, [port, serial_opts], name: __MODULE__) end def init([port, serial_opts]) do {:ok, board} = Board.start_link(port, serial_opts) {:ok, %{ board: board }} end ... end
  138. 138. Arduino Firmata defmodule BadgeLib.Firmata do ... def handle_info({:firmata, {:pin_map, _pin_map}}, s) do {:noreply, s} end def handle_info(_, s) do {:noreply, s} end end
  139. 139. Arduino Firmata defmodule BadgeLib.Firmata do use GenServer use Firmata.Protocol.Modes alias Firmata.Board, as: Board @high 1 @low 0 @vibration_pin 9 def start_link(opts []) def vibrate(state @high) do GenServer.call(__MODULE__, {:vibrate, state}) end ... end
  140. 140. Arduino Firmata defmodule BadgeLib.Firmata do ... def handle_call({:vibrate, state}, _from, s) do Board.digital_write(s.board, @vibration_pin, state) {:reply, :ok, s} end def handle_info({:firmata, {:pin_map, _pin_map}}, s) do Board.set_pin_mode(s.board, @vibration_pin, @output) {:noreply, s} end end
  141. 141. Running on the host Firmata.begin(57600); while (!Serial) { ; }
  142. 142. Running on the host Firmata.begin(57600); while (!Serial) { ; } Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> Nerves.UART.enumerate %{"/dev/cu.Bluetooth-Incoming-Port" => %{}, "/dev/cu.MyQC-SPPDev" => %{}, "/dev/cu.MyQC-SPPDev-1" => %{}, "/dev/cu.usbmodem1421" => %{manufacturer: "MediaTek Labs", product_id: 43777, vendor_id: 3725}}
  143. 143. Running on the host %{"/dev/cu.Bluetooth-Incoming-Port" => %{}, "/dev/cu.MyQC-SPPDev" => %{}, "/dev/cu.MyQC-SPPDev-1" => %{}, "/dev/cu.usbmodem1421" => %{manufacturer: "MediaTek Labs", product_id: 43777, vendor_id: 3725}} iex(2)> BadgeLib.Firmata.start_link(port: "/dev/cu.usbmodem1421")
  144. 144. Running on the host iex(3)> BadgeLib.Firmata.vibrate
  145. 145. Arduino Firmata defmodule BadgeLib.Firmata do ... @vibration_pulse 300 @vibration_times 7 def vibrate_pulse() do GenServer.call(__MODULE__, :vibrate_pulse) end ... end
  146. 146. Arduino Firmata defmodule BadgeLib.Firmata do ... def handle_call(:vibrate_pulse, _from, s) do send(self, {:vibrate_pulse, 0, 1}) {:reply, :ok, s} end def handle_info({:vibrate_pulse, @vibration_times, _},s) do Board.digital_write(s.board, @vibration_pin, @low) {:noreply, s} end def handle_info({:vibrate_pulse, times, state}, s) do Board.digital_write(s.board, @vibration_pin, state) state = if state == 0, do: 1, else: 0 Process.send_after(self, {:vibrate_pulse, times + 1, state}, @vibration_pulse) {:noreply, s} end end
  147. 147. Running on the host
  148. 148. Arduino Firmata defmodule BadgeLib.Firmata do ... @display_clear 0x81 @display_text 0x82 @display_time 20_000 def text(message) do GenServer.call(__MODULE__, {:text, message}) end def clear() do GenServer.call(__MODULE__, :clear) end ... end
  149. 149. Arduino Firmata defmodule BadgeLib.Firmata do ... def handle_call({:text, message}, _from, s) do Board.sysex_write(s.board, @display_clear, "") resp = Board.sysex_write(s.board, @display_text, message) Process.send_after(self, :clear_display, @display_time) {:reply, {:ok, resp}, s} end def handle_call(:clear, _from, s) do resp = Board.sysex_write(s.board, @display_clear, "") {:reply, {:ok, resp}, s} end def handle_info(:clear_display, s) do Board.sysex_write(s.board, @display_clear, "") {:noreply, s} end ... end
  150. 150. Running on the host
  151. 151. Arduino Firmata defmodule BadgeFw do use Application alias Nerves.InterimWiFi, as: WiFi def start(_type, _args) do import Supervisor.Spec, warn: false :os.cmd('modprobe mt7603e') # Define workers and child supervisors to be supervised children = [ worker(Task, [fn -> network end], restart: :transient), worker(BadgeLib.Firmata, []), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: BadgeFw.Supervisor] Supervisor.start_link(children, opts) end ... end
  152. 152. Running on the target
  153. 153. TWITTER 3.4
  154. 154. Connecting to Twitter # badge_lib/mix.exs def application do [applications: [:logger, :firmata, :oauth, :extwitter]] end defp deps do [{:firmata, github: "mobileoverlord/firmata"}, {:oauth, github: "tim/erlang-oauth"}, {:extwitter, "~> 0.6"}] end
  155. 155. Connecting to Twitter defmodule BadgeFw.Worker do use GenServer def start_link(opts []) do GenServer.start_link(__MODULE__, [], opts) end def init([]) do BadgeLib.Firmata.clear() {:ok, %{}} end end
  156. 156. defmodule BadgeFw do use Application alias Nerves.InterimWiFi, as: WiFi def start(_type, _args) do import Supervisor.Spec, warn: false :os.cmd('modprobe mt7603e') # Define workers and child supervisors to be supervised children = [ worker(Task, [fn -> network end], restart: :transient), worker(BadgeLib.Firmata, []), worker(BadgeFw.Worker, []), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: BadgeFw.Supervisor] Supervisor.start_link(children, opts) end ... end Connecting to Twitter
  157. 157. defmodule BadgeFw.Worker do use GenServer @hashtag "#NervesBadge" @handle "@ElixirConf" @interval 25_000 def init([]) do BadgeLib.Firmata.clear() Process.send_after(self, :update, @interval) {:ok, %{last: {nil, nil}}} end end Connecting to Twitter
  158. 158. def handle_info(:update, %{last: {lhash, luser}} = s) do {hash, user} = {@handle, @hashtag} new_hash = get_tweet(hash) new_user = get_tweet(user) last = cond do new_hash != lhash -> display_tweet(new_hash) {new_hash, luser} new_user != luser -> display_tweet(new_user) {lhash, new_user} true -> {lhash, luser} end Process.send_after(self, :update, @interval) {:noreply, %{s | last: last}} end Connecting to Twitter
  159. 159. def get_tweet(search) do case ExTwitter.search(search, [count: 1]) do [tweet] -> tweet _ -> nil end end def display_tweet(tweet) do IO.puts "display tweet" BadgeLib.Utf8ToASCII.convert(tweet.text) |> BadgeLib.Firmata.text BadgeLib.Firmata.vibrate_pulse end Connecting to Twitter
  160. 160. defmodule BadgeLib.Utf8ToASCII do def convert(string), do: convert(string, <<>>) def convert(<<c::utf8, rest::binary>>, result) when c <= 127 do convert(rest, result <> <<c::utf8>>) end def convert(<<_::utf8, rest::binary>>, result) do convert(rest, result) end def convert(<<>>, result) do result end end Connecting to Twitter
  161. 161. # badge_fw/config/config.exs config :extwitter, :oauth, [ consumer_key: "vnBfkubUmv10QRcQjFU3lXKin", consumer_secret: "XUk3fsulkfraaapUyMOfnVRtd8fXdlkKMQvhjDv5nnEVrsk7yA", access_token: System.get_env("TWITTER_ACCESS_TOKEN"), access_token_secret: System.get_env("TWITTER_ACCESS_TOKEN_SECRET") ] Connecting to Twitter
  162. 162. Running on the target
  163. 163. WEB INTERFACES 3.5
  164. 164. Web Interfaces $ cd badge/apps $ git clone https://github.com/lancehalvorsen/badge_settings
  165. 165. # badge_fw/mix.exs def application do [mod: {BadgeFw, []}, applications: [:logger, :badge_lib, :nerves_interim_wifi, :nerves_ntp, :badge_settings]] end def deps do [{:nerves, "~> 0.3.0"}, {:nerves_interim_wifi, "~> 0.1"}, {:nerves_ntp, "~> 0.1"}, {:badge_lib, in_umbrella: true}, {:badge_settings, in_umbrella: true}] end Web Interfaces
  166. 166. Web Interfaces # badge_fw/config/config.exs config :badge_settings, :nerves_settings, %{ settings_file: "/root/nerves_settings.txt", device_name: "My Awesome Device", application_password: System.get_env("BADGE_CONFIG_PASSWORD") || "nerves_rulz!" } config :badge_settings, BadgeSettings.Endpoint, url: [host: "0.0.0.0"], http: [port: 80], secret_key_base: "R02jL0Vi+tFH7YOecTua/oc0b2dETOQT8/ Sg9dD56EDKqmd8jRAdqa0CyZ7tOFIt", render_errors: [view: BadgeSettings.ErrorView, accepts: ~w(html json)], server: true, pubsub: [name: BadgeSettings.PubSub, adapter: Phoenix.PubSub.PG2]
  167. 167. Web Interfaces $ cd apps/badge_settings $ mix deps.get $ npm install $ ./node_modules/brunch/bin/brunch build --production $ MIX_ENV=prod mix phoenix.digest
  168. 168. Running on the host
  169. 169. Running on the target
  170. 170. Web Interfaces def network do wlan_config = case settings do {:ok, settings} -> [psk: settings.password, ssid: settings.ssid] _ -> Application.get_env(:badge_fw, :wlan0) end WiFi.setup "wlan0", wlan_config end def settings do settings_file = Application.get_env(:badge_settings, :nerves_settings).settings_file case File.read(settings_file) do {:error, :enoent} = error -> error {:ok, ""} -> {:error, :empty} {:ok, contents} -> unencoded = :erlang.binary_to_term(contents) {:ok, unencoded} end end
  171. 171. Web Interfaces def handle_info(:update, %{last: {lhash, luser}} = s) do {hash, user} = case BadgeFw.settings do {:ok, settings} -> {Map.get(settings, :handle), Map.get(settings, :hashtag)} _ -> {@handle, @hashtag} end ... end
  172. 172. RECAP ▸ Project Layout ▸ Initialization ▸ Interfacing with Arduino ▸ Connecting to Services ▸ Web Interfaces with Phoenix Building the Badge
  173. 173. Advanced Config SECTION 4
  174. 174. ▸ Advanced Configuration ▸ Modifying a Nerves System ▸ Initialization with erlinit ▸ Firmware Updates ▸ Licensing What are we going to Do?
  175. 175. MODIFYING SYSTEMS 4.1
  176. 176. Reasons to make your own system ▸ Add a library or application that can’t be built using mix ▸ Postgres ▸ Qt ▸ Add a Linux kernel module or patch the kernel ▸ WiFi drivers ▸ Audio, webcams, USB peripherals ▸ Patch or change the bootloader ▸ Enable a Busybox command ▸ Likely if you need to run a shell script ▸ Consider re-implementing in Elixir
  177. 177. Reasons NOT to make your own system ▸ Add files to the root filesystem ▸ These can be added via the rootfs-additions mechanism ▸ Change the iex terminal tty or alter how Erlang is started ▸ Create your own erlinit.config for your project ▸ Add or remove files from the firmware update files ▸ Create your own fwup.conf for your project ▸ Summary: Try to avoid modifying systems too much since it will take away the benefits of working with the Elixir tools
  178. 178. Prereqs to working with systems ▸ Concepts ▸ Buildroot ▸ Linux kernel configuration ▸ Busybox ▸ Build and deploy requirements ▸ Linux (native, VM, or Docker) ▸ Someplace to put the system image since it’s too large for hex.pm
  179. 179. Buildroot ▸ Toolchain, bootloader, kernel, root filesystem
 builder for embedded Linux ▸ Cross-compiled ▸ Support for building ~1800 programs and
 libraries ▸ Menu system for enabling and configuring packages ▸ Uses Makefiles, but not necessary to know make even to add packages ▸ Very well documented
  180. 180. World views ▸ Buildroot normally is the top-level project that builds firmware images ▸ Nerves uses Buildroot to produce the system images that later get combined with OTP releases ▸ Consequences ▸ Erlang packages in Buildroot aren’t usable (no ejabberd) ▸ Nerves images are much smaller due to not including all (or most) of OTP ▸ For most development, Nerves builds are faster and can be run on OSX
  181. 181. Defconfigs and .configs ▸ Anything using Kconfig to manage configuration options uses these (Linux, Buildroot, Busybox) ▸ .config ▸ Found in the root of the build directory for the project ▸ Has the values for ALL options – hidden, derived, etc. ▸ defconfig ▸ Subset of .config with only non-default options ▸ Usually stored in source control ▸ make savedefconfig
  182. 182. Activity: Start a system build ▸ Clone one of the official Nerves system images ▸ https://github.com/nerves-project/nerves_system_* ▸ Method 1 – official builds ▸ cd nerves_system_foo; mix deps.get; mix compile ▸ CTRL-C to stop (mix clean may be needed to start over) ▸ Method 2 – easier for development ▸ Clone https://github.com/nerves-project/nerves_system_br ▸ nerves_system_br/create-build.sh 
 nerves_system_foo/nerves_defconfig out ▸ cd out; make
  183. 183. Activity: Compile in a new package ▸ Go to the out directory from the last activity ▸ make menuconfig ▸ Find and enable postgresql ▸ Hint: Type the “/” key to search for postgresql. Press the number ▸ While you’re in menuconfig, take a look around ▸ Exit menuconfig. Be sure to save! ▸ Inspect .config and see that BR2_PACKAGE_POSTGRESQL is enabled ▸ make savedefconfig ▸ Inspect nerves_system_foo/nerves_defconfig has POSTGRESQL enabled ▸ Run make if you have time
  184. 184. The Linux kernel ▸ Why Linux? Best device driver support for embedded ▸ Nerves uses a trimmed down Linux kernel to keep 
 the image size reasonable ▸ Many device drivers compiled into the kernel so no
 need to load them at initialization ▸ Some device drivers still compiled as modules ▸ Module parameters unknown until runtime ▸ Don’t work when compiled into the kernel ▸ More generic images
  185. 185. Activity: Enable a device driver ▸ Go to the out directory from before ▸ make linux-menuconfig ▸ Enable support for USB->serial adapters – called USB Modem (CDC ACM) support in the kernel ▸ Use “/” to search for it if you’re not sure where it is ▸ Exit and save ▸ Inspect build/linux-x.y.z/.config to see that the option was saved ▸ make linux-savedefconfig ▸ Inspect build/linux-x.y.z/defconfig to verify the option again ▸ cp build/linux-x.y.z/defconfig to the Linux defconfig for the Nerves system. This is usually called linux-x.y.defconfig
  186. 186. Busybox ▸ Provides tons of Unix apps in one small binary ▸ ls, sh, dd, ps, find, cat, tail, tar, cd, mkdir, etc. ▸ ntpd, dhcpc ▸ vi (sorry, no emacs) ▸ Ideally, Nerves would not have to use Busybox ▸ Wishlist: project that adds common shell commands to the IEx shell and improves OS process inspection ▸ Removing Busybox is not trivial so it will be with us for a while
  187. 187. Activity: Modify the Busybox config ▸ Go to the out directory from before ▸ make busybox-menuconfig ▸ Enable the ping utility, exit and save ▸ Verify that CONFIG_PING is enabled in build/busybox-1.x.y/.config ▸ If your system overrides the default busybox configuration, copy the new one on top of it ▸ If not, ▸ Copy build/busybox-1.x.y/.config to ../busybox.config ▸ make menuconfig ▸ Set the Busybox configuration to ${NERVES_DEFCONFIG_DIR}/ busybox.config ▸ make savedefconfig
  188. 188. Using your custom Nerves system ▸ Go to the out directory from before ▸ export NERVES_SYSTEM=$PWD ▸ Now go to your Elixir project and run mix
  189. 189. Publishing a custom Nerves system ▸ What you’ll need ▸ Someplace to hold your nerves_system_xyz ▸ Someplace to hold the built tarball for the system ▸ GitHub supports both - attach the tarball to a release ▸ hex.pm only supports the source repository
  190. 190. nerves.exs config :nerves_system_linkit, :nerves_env, type: :system, version: version, mirrors: [ "https://github.com/nerves-project/nerves_system_linkit/releases/ download/v#{version}/nerves_system_linkit-v#{version}.tar.gz", "https://s3.amazonaws.com/nerves/artifacts/nerves_system_linkit- #{version}.tar.gz"],
 …
  191. 191. Final steps ▸ Go back to your system’s out directory ▸ make system ▸ Publish the created tarball ▸ Publish the source on hex.pm or git ▸ Let everyone know to reference your Nerves system image in their mix.exs
  192. 192. ERLINIT 4.2
  193. 193. erlinit ▸ Replacement for /sbin/init that starts the Erlang virtual machine ▸ Basic initialization of the Linux user land ▸ Loopback network connection ▸ Mounts /tmp, /proc, /sys ▸ Configures the tty ▸ Configuration stored in /etc/erlinit.conf ▸ Can be overridden by passing parameters to the Linux kernel from the bootloader
  194. 194. erlinit debugging options ▸ --verbose ▸ --hang-on-exit ▸ Useful to capture error messages when the VM exits ▸ --run-on-exit /bin/sh ▸ Drop into a shell if the VM exits ▸ Exiting the shell reboots or hangs ▸ --warn-unused-tty ▸ erlinit will tell you what options to pass it to use the shell on the terminal you’re looking at
  195. 195. erlinit features ▸ Mount filesystems ▸ Configure a unique hostname ▸ --hostname-pattern and --uniqueid-exec ▸ Nerves uses fhunleth/boardid to read serial numbers ▸ Wrap the launch of the Erlang VM in another program ▸ --alternate-exec ▸ Perform some custom system-specific initialization that can’t be done in Erlang or Elixir ▸ Capture the terminal with dtach to route it to a GUI ▸ Run the Erlang VM as a regular user
  196. 196. erlinit pitfalls ▸ Running shell scripts to initialize the system ▸ Move initialization to Elixir to take advantage of OTP supervision ▸ Currently no standard way of handing system initialization in Nerves ▸ Assuming writable filesystems always can be mounted ▸ Failures happen – must be handled in Elixir ▸ erlinit can’t report errors so Elixir must check
  197. 197. FIRMWARE UPDATE 4.3
  198. 198. fwup ▸ Firmware update packaging and application ▸ Packages ▸ Zip-formatted archives ▸ Metadata ▸ All data files protected by cryptographic 
 hashes ▸ Packages can be cryptographically signed ▸ Very simple scripts supported (lack of functionality is a feature)
  199. 199. fwup processing Master Boot Record FAT Boot partition Root Filesystem A Read-only Root Filesystem B Read-only Application Data Read-write rootfs.img uImage other files on_init on_finish .fw file fwup processing 1 2 3 4 5
  200. 200. Anatomy of a fwup.conf file ▸ Resources ▸ Files that are included as part of the zip archive ▸ Not required to be used when upgrading ▸ Tasks ▸ Instructions for applying updates ▸ Only one task is run at a time ▸ Tasks may have conditions for when they’re run ▸ Common task names: “complete” and “upgrade”
  201. 201. LinkIt Smart fwup.conf # Let the rootfs have room to grow up to 64 MiB and align # it to the nearest 1 MB boundary define(ROOTFS_A_PART_OFFSET, 2048) define(ROOTFS_A_PART_COUNT, 131072) define(ROOTFS_B_PART_OFFSET, 133120) define(ROOTFS_B_PART_COUNT, 131072) # Application partition # NOTE: Keep the total amount used under 1.78 GiB so that # everything fits in the "2 GB" eMMC. define(APP_PART_OFFSET, 264192) define(APP_PART_COUNT, 1048576) https://github.com/nerves-project/nerves_system_linkit/blob/develop/fwup.conf
  202. 202. LinkIt Smart fwup.conf # Firmware metadata meta-product = "Nerves Firmware" meta-description = "" meta-version = ${NERVES_SDK_VERSION} meta-platform = "linkit" meta-architecture = "mips" meta-author = "Frank Hunleth" https://github.com/nerves-project/nerves_system_linkit/blob/develop/fwup.conf
  203. 203. LinkIt Smart fwup.conf file-resource rootfs.img { host-path = ${ROOTFS} } file-resource uImage { host-path = "${NERVES_SYSTEM}/images/uImage" assert-size-lte = 30720 # 15 MiB } https://github.com/nerves-project/nerves_system_linkit/blob/develop/fwup.conf
  204. 204. LinkIt Smart fwup.conf mbr mbr-a { partition 0 { block-offset = ${ROOTFS_A_PART_OFFSET} block-count = ${ROOTFS_A_PART_COUNT} type = 0x83 # Linux } partition 1 { block-offset = ${APP_PART_OFFSET} block-count = ${APP_PART_COUNT} type = 0xc # FAT32 } # partition 2 and 3 are unused } https://github.com/nerves-project/nerves_system_linkit/blob/develop/fwup.conf
  205. 205. LinkIt Smart fwup.conf # This firmware task writes everything to the destination media task complete { on-init { mbr_write(mbr-a) } on-resource rootfs.img { # write to the first rootfs partition raw_write(${ROOTFS_A_PART_OFFSET}) } on-finish { fat_mkfs(${APP_PART_OFFSET}, ${APP_PART_COUNT}) fat_setlabel(${APP_PART_OFFSET}, "APPDATA") } } https://github.com/nerves-project/nerves_system_linkit/blob/develop/fwup.conf
  206. 206. LinkIt Smart fwup.conf task upgrade.a { # This task upgrades the A partition require-partition-offset(0, ${ROOTFS_B_PART_OFFSET}) on-resource rootfs.img { # write to the first rootfs partition raw_write(${ROOTFS_A_PART_OFFSET}) } on-finish { # Switch over to boot the new rootfs mbr_write(mbr-a) } } https://github.com/nerves-project/nerves_system_linkit/blob/develop/fwup.conf
  207. 207. LinkIt Smart fwup.conf https://github.com/nerves-project/nerves_system_linkit/blob/develop/fwup.conf task upgrade.b { # This task upgrades the B partition require-partition-offset(0, ${ROOTFS_A_PART_OFFSET}) on-resource rootfs.img { # write to the first rootfs partition raw_write(${ROOTFS_B_PART_OFFSET}) } on-finish { # Switch over to boot the new firmware mbr_write(mbr-b) } }
  208. 208. FAT filesystem commands Command Description fat_mkfs(block_offset, block_count) Create a FAT file system fat_write(block_offset, filename) Write the resource to the file system fat_mv(block_offset, oldname, newname) Rename a file fat_rm(block_offset, filename) Delete a file fat_mkdir(block_offset, filename) Create a directory fat_touch(block_offset, filename) Create an empty file if the file doesn't exist
  209. 209. LICENSING 4.4
  210. 210. Shipping Nerves - Licensing ▸ Buildroot infrastructure in Nerves can aid process ▸ make legal-info ▸ Other licenses ▸ nerves-toolchain ▸ gcc ▸ crosstool-ng ▸ All of your mix dependencies legal-info ├── buildroot.config ├── host-licenses │ ├── autoconf │ │ ├── COPYING.EXCEPTION │ │ └── COPYINGv3 │ └── ... │ └── README ├── host-licenses.txt ├── host-manifest.csv ├── host-sources ├── licenses │ ├── busybox │ │ └── LICENSE │ ├── erlang │ │ └── LICENSE.txt │ └── ... ├── licenses.txt ├── manifest.csv ├── README └── sources ├── boardid-v0.4.0.tar.gz ├── busybox-1.24.1.tar.bz2 ├── ... └── zlib-1.2.8.tar.xz
  211. 211. Elixir Conf 2016 Nerves: Connected beyond the Node Thursday 1:30 PM - 2:15 PM Track 1 Justin Schneck Building "learn to touch type" glove with Elixir and Arduino2:15 PM - 3:00 PM Track 2 Tetiana Dushenkivska Nerves + Phoenix Saves a Father's Sanity! Friday 1:30 PM - 2:15 PM Track 2 Joel Byler The Joy of Connecting Elixir to the Physical World3:30 PM - 4:15 PM Track 1 Frank Hunleth Keynote4:30 PM - 5:30 PM Boyd Multerer
  212. 212. Elixir Conf 2016 Thank You & Nerves

×