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
  • Publish...
    • MQTT Messages
      • Listen with Lambda

Special Thanks

  • Daniel Spofford
  • Daniel Searles
  • Michael Roach
  • You

From Thing to Internet

By dlindema

From Thing to Internet

  • 379