Up and Running with Elixir, Nerves, and Serverless.
"This is so much harder than it needs to be."
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
{: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...
MyIoT.Application do
use Application
def start(_type, _args) do
children = []
opts = [strategy: :one_for_one]
Supervisor.start_link(children, opts)
end
end
In Elixir, all code runs inside processes. Processes are isolated from each other, run concurrent to one another and communicate via message passing.
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"
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
#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, "~> 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
defp schedule_card_check(delay \\ @card_check_every_ms) do
Process.send_after(self(), :card_check, delay)
end
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
A lightweight messaging protocol for small sensors and mobile devices, optimized for high-latency or unreliable networks
{:tortoise, "~> 0.9"},
#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
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
{:x509, "~> 0.5.4"},
aws iot create-thing-type --thing-type-name ThingToInternet
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"
{
"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
aws iot set-v2-logging-options \
--role-arn arn:aws:iam::<your-aws-account-num>:role/<IoTLoggingRole> \
--default-log-level DEBUG
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"
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 ...