From Thing to Internet
Up and Running with Elixir, Nerves, and Serverless.

@lindemda
DanLindeman
- Software Engineer @ Very



Who I think You are

Journey
- Thing...
- Anything we want
- Interface to...
- A computer
- Program...
- Firmware
- Application
- Publish...
- Information

Characters
- Things
- RFID sticker
- RC522 (RFID Reader)
- Raspberry Pi
- Elixir/Nerves
- Internet
- AWS IoT
- Serverless

Motivation
- Scary
- Dusty Pi
- IoTiddlywinks
"This is so much harder than it needs to be."


Ahoy!
Thing
RFID + Raspberry pi
RFID
- Radio Frequency Identification
- Everywhere
- Library Checkout
- Amiibo (NFC)
- Access Keycards
- Fyre Festival



RFID "Things"
- Power
- Data
- Size

Buy Things



SPI
- RPI3 is the "Master"
- RFID Reader is the "Slave"
- We know the packet size
- Don't need async



In short

Reality

hard Way
- Read specs
- Standards
- Datasheets
- Registers

Fun Way
- Google a lot
- Slack messages
- Meet cool people

Adventure awaits

Elixir
and nerves
Elixir
- Erlang VM
- Friendly syntax
- Functional
defmodule Greetings do
def hello do
IO.puts "Hello World"
end
def hello(name) do
IO.puts "Hello #{name}"
end
end
iex(1)> Greetings.hello()
Hello world
:ok
iex(2)> Greetings.hello("Dan")
Hello Dan

Nerves
- Over the Air Updates "OTA"
- Low-level Interfaces
- ex: SPI
- Erlang VM
- "Let it crash"
Raspbian
- Yocto, "Roll your own"
- Enable & Reboot
- ex: SPI
- Any Language
- Maybe you made a good choice here?


INit
{:tortoise, "~> 0.9"}, # MQTT
{:x509, "~> 0.5.4"}, # Certificates
{:rc522, "~> 0.1.0", github: "mroach/rc522_elixir"}, # RFID
>>> mix nerves.new hello_nerves
>>> cd hello_nerves
>>> export MIX_TARGET=rpi3
>>> mix deps.get
Then add...
Entry Point
MyIoT.Application do
use Application
def start(_type, _args) do
children = []
opts = [strategy: :one_for_one]
Supervisor.start_link(children, opts)
end
end
Processes
In Elixir, all code runs inside processes. Processes are isolated from each other, run concurrent to one another and communicate via message passing.
Mailbox
When a message is sent to a process, the message is stored in the process mailbox.
Processes can send messages to any process including themselves.

iex> send self(), {:hello, "world"}
{:hello, "world"}
iex> receive do
...> {:hello, msg} -> msg
...> {:world, msg} -> "won't match"
...> end
"world"
"Looping"
- Genserver
- Continue "Hook"
- Info
- Check mailbox
- Do work
defmodule Mygadget.Worker do
use GenServer
require Logger
## Client API
def start_link([]) do
GenServer.start_link(__MODULE__, [])
end
## Server Callbacks
def init([]) do
{:ok, %{}, {:continue, "Initialized!"}}
end
def handle_continue(_, state) do
do_work()
{:noreply, state}
end
def handle_info(:check, state) do
do_work()
{:noreply, state}
end
def do_work() do
Logger.info("Doing work!")
Process.send_after(self(), :check, 10_000)
end
end
Application
#MyIot.Application do
#use Application
#def start(_type, _args) do
children = [
{ThingToInternet.App, [%{handler: ThingToInternet.Handler}]},
]
opts = [strategy: :one_for_one, name: ThingToInternet.Supervisor]
Supervisor.start_link(children, opts)
#end
#end
def init(opts) do
device = opts[:device] || default_spi_bus()
handler = opts[:handler]
Logger.debug("Connecting to RC522 device on #{device}")
{:ok, spi} = SPI.open(device)
RC522.initialize(spi)
hwver = RC522.hardware_version(spi)
Logger.info("Connected to #{hwver.chip_type} reader version #{hwver.version}")
schedule_card_check()
{:ok, %State{spi: spi, handler: handler}}
end
defp schedule_card_check(delay \\ @card_check_every_ms) do
Process.send_after(self(), :card_check, delay)
end
"Hook"
RC522
{:rc522, "~> 0.1.0", github: "mroach/rc522_elixir"},
@impl true
def handle_info(:card_check, %State{spi: spi} = state) do
{:ok, data} = RC522.read_tag_id(spi)
state =
case process_tag_id(data) do
{:ok, tag_id} ->
maybe_notify(tag_id, state)
Map.put(state, :last_scan, Scan.new(tag_id))
{:error, _} ->
state
end
schedule_card_check()
{:noreply, state}
end
- do_work
defp schedule_card_check(delay \\ @card_check_every_ms) do
Process.send_after(self(), :card_check, delay)
end
- Loop
Maybe_Notify?
defp maybe_notify(tag_id, %State{handler: handler, last_scan: last_scan}) do
# only notify if the card ID changed or the repeat delay elapsed
case notify?(tag_id, last_scan) do
true ->
apply(handler, :tag_scanned, [tag_id])
:ok
_ ->
:noop
end
end
MQTT
- Broker
- Publish
- Subscribe

A lightweight messaging protocol for small sensors and mobile devices, optimized for high-latency or unreliable networks
{:tortoise, "~> 0.9"},
Tortoise

#MyIot.Application do
#use Application
#def start(_type, _args) do
{:ok, _} = Tortoise.Supervisor.start_child(
client_id: "rfid-scanner",
handler: {Tortoise.Handler.Logger, []},
server: {
Tortoise.Transport.SSL,
host: "somecrazyletters-ats.iot.us-east-1.amazonaws.com",
port: 8883,
cacerts: [aws_cert()],
key: {:RSAPrivateKey, device_key()},
cert: device_cert(),
verify: :verify_none
},
subscriptions: [
{"/dev/ThingToInternet/rfid-scanner/events", 0}
]
)
#children = [
# {ThingToInternet.App, [%{handler: ThingToInternet.Handler}]},
#]
#opts = [strategy: :one_for_one, name: ThingToInternet.Supervisor]
#Supervisor.start_link(children, opts)
#end
#end
- Recall
- Internet!
defp maybe_notify(tag_id, %State{handler: handler, last_scan: last_scan}) do
# only notify if the card ID changed or the repeat delay elapsed
case notify?(tag_id, last_scan) do
true ->
apply(handler, :tag_scanned, [tag_id])
:ok
_ ->
:noop
end
end
defmodule ThingToInternet.Handler do
require Logger
def tag_scanned(tag_id) when is_number(tag_id), do: tag_id |> to_string |> tag_scanned
def tag_scanned(tag_id) when is_binary(tag_id) do
Logger.info("Scanned RFID tag #{tag_id}")
Tortoise.publish("rfid-scanner",
"/dev/ThingToInternet/rfid-scanner/events",
"Hello from RFID tag #{tag_id}!",
qos: 0)
end
end
To the Cloud!


AWS IOT
Glutton for Pain
AWS iot
- Certificates
- Root
- Device

- Things
- Types
- Provisioning
- MQTT Broker
- Pub/Sub
- Topics
Certs
- Connect to broker
- Provide Security
- Headaches

{:x509, "~> 0.5.4"},
Things
- ThingType

aws iot create-thing-type --thing-type-name ThingToInternet
Things
- JITP role
Resources:
JITPRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument: |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "iot.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AWSIoTThingsRegistration"
- "arn:aws:iam::aws:policy/service-role/AWSIoTLogging"
- "arn:aws:iam::aws:policy/service-role/AWSIoTRuleActions"
Things
- Provisioning config
- Yuck
{
"roleArn": JITP role,
"templateBody": insane escaped JSON nightmare madness. Probably Dragons.
}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["iot:Connect"],
"Resource": [
"arn:aws:iot:*:*:client/${ThingName}"
]
},
{
"Effect": "Allow",
"Action": ["iot:Subscribe"],
"Resource": [
"arn:aws:iot:*:*:topicfilter/*/${ThingTypeName}/${ThingName}/events"
]
},
{
"Effect": "Allow",
"Action": ["iot:Receive"],
"Resource": [
"arn:aws:iot:*:*:topic/*/${ThingTypeName}/${ThingName}/events"
]
},
{
"Effect": "Allow",
"Action": ["iot:Publish"],
"Resource": [
"arn:aws:iot:*:*:topic/*/${ThingTypeName}/${ThingName}/events"
]
}
]
}
This...
{
"roleArn": JITP-role-arn
}
{
"Parameters": {
"AWS::IoT::Certificate::Country": { "Type": "String" },
"AWS::IoT::Certificate::Id": { "Type": "String" },
"AWS::IoT::Certificate::CommonName": { "Type": "String" }
},
"Resources": {
"thing": {
"Type": "AWS::IoT::Thing",
"Properties": {
"ThingName": { "Ref": "AWS::IoT::Certificate::CommonName" },
"ThingTypeName": "ThingToInternet",
"AttributePayload": {
"version": "v1",
"country": { "Ref": "AWS::IoT::Certificate::Country" }
}
}
},
"certificate": {
"Type": "AWS::IoT::Certificate",
"Properties": {
"CertificateId": { "Ref": "AWS::IoT::Certificate::Id" },
"Status": "ACTIVE"
}
},
"policy": {
"Type": "AWS::IoT::Policy",
"Properties": {}
}
}
}
...plus These
aws iot register-ca-certificate \
--ca-certificate file://rootCA.pem \
--verification-certificate file://verificationCert.pem \
--set-as-active \
--allow-auto-registration \
--registration-config file://provisioning_config.json
aws iot update-ca-certificate \
--certificate-id relevant_ca_cert_id \
--registration-config file://provisioning_config.json \
--region us-east-1
Finally!
Generated

Registered

Auto Registered

Made it

Pro Tip
aws iot set-v2-logging-options \
--role-arn arn:aws:iam::<your-aws-account-num>:role/<IoTLoggingRole> \
--default-log-level DEBUG
Serverless
Not enough buzz
Internet


Serverless
service: thing-to-internet-listener
provider:
name: aws
runtime: python3.7
iamRoleStatements:
- Effect: "Allow"
Action:
- "iot:Subscribe"
Resource: "arn:aws:iot:*:*:topicfilter/dev/ThingToInternet/rfid-scanner/events"
- Effect: "Allow"
Action:
- "iot:Receive"
Resource: "arn:aws:iot:*:*:topic/dev/ThingToInternet/rfid-scanner/events"
functions:
listener:
handler: handler.listener
events:
- iot: # https://forums.aws.amazon.com/thread.jspa?messageID=795332
sql: "SELECT clientId() as client_id,
topic() as topic,
encode(* , 'base64') as info
FROM '/dev/ThingToInternet/rfid-scanner/events'"
sqlVersion: "2016-03-23"
Lambda
- Extract the message
- Cloud's the limit
import json
from base64 import b64decode
def listener(event, context):
topic = event.get("topic", "no_topic")
info = event.get("info", "no_info")
payload = b64decode(info).decode("utf-8")
print(topic, payload)
# Whatever we want!
~/Programs/thing-to-internet-listener:
] sls logs -f listener -t
START ...
/dev/ThingToInternet/rfid-scanner/events Hello from RFID tag 713338471099!
END ...
REPORT ...
START ...
/dev/ThingToInternet/rfid-scanner/events Hello from RFID tag 507180040811!
END ...
REPORT ...
Wrapping Up
We've Done a ton
Putting a Thing on the Internet.

Journey
- Thing...
- RFID Tag
- RFID Reader
- Interface to...
- A raspberry Pi
- Programmed in...
- Elixir
- using Nerves
- Elixir
- Publish...
- MQTT Messages
- Listen with Lambda
- MQTT Messages

Special Thanks
- Daniel Spofford
- Daniel Searles
- Michael Roach
- You

From Thing to Internet
By dlindema
From Thing to Internet
- 379