IoT with Elixir and CoAp Part 2: Easily Prototype, Build IoT Platform

Michał Podwórny

IoT with Elixir and CoAp Part 2: Easily Prototype, Build IoT Platform

We are back with the second part of our IoT development series. Please check out the first part for a brief description of the CoAP protocol and introduction to what we are trying to achieve.

Where we are and what we want to do

Let us first summarize what we have done so far. First, we have implemented CoapNode - an Elixir app that mocks software that would be running on an embedded device like ESP8266. CoapNode is a CoAP server that exposes resources to the outside world. It can register its resources in CoapDirectory which is something we ultimately want to put on a Raspberry PI using Nerves. We have finished implementing the tools in the CoapDirectory to make requests to given nodes. But the directory itself does not expose any interface. And that is what we are going to do now. We will expose an HTTP API.

This is the flow we are going to achieve:

  1. Using the CoAP protocol, CoapNode registers a resource in the CoapDirectory under a path, e.g. switches/1.
  2. We hit the CoapDirectory’s HTTP API, for instance issuing a GET http://(directory_ip)/switches/1 request.
  3. The directory translates the request to corresponding CoAP request and routes it to appropriate CoapNode that has registered the resource in step 1: GET coap://(node_ip)/switches/1.
  4. CoapDirectory responds to the HTTP request from step 2 with the response to the CoAP request from step 3.

The whole idea is that any static IP that anyone needs to know is the IP of the CoapDirectory. At the end, we will also hook up a Phoenix application to the setup and allow a resource observation in the browser.

HTTP API

Cowboy seems like a natural choice for the HTTP server. I have decided not to use Plug since we only need one simple endpoint. The directory is going to take the request path and check whether it has a resource registered under it and if it does, forward the request to the resource’s node.

Let us start by adding :cowboy to the list of applications and dependencies in mix.exs. Then we can start the simplest server in our main application module:

# coap_directory/lib/coap_directory.ex
def start(_type, _args) do
  dispatch = :cowboy_router.compile([{:_, [{"/[...]", CoapDirectory.HttpRequestHandler, []}]}])
  {:ok, _} = :cowboy.start_http(:http, 100, [port: 8080], [env: [dispatch: dispatch]])
  # ...
end

It will route all requests to the given handler. If you remember, in part 1 we not only allowed regular CoAP requests but also observations. For now, let us just forward all HTTP requests as their CoAP counterparts and we will handle observations in a bit:

# coap_directory/lib/coap_directory/http_request_handler.ex
defmodule CoapDirectory.HttpRequestHandler do
  import Coap.Records
  alias CoapDirectory.Client

  def init(req, _options) do
    {response_code, response_body} = handle_request(req)
    req = :cowboy_req.reply(response_code, [{"content-type", "text/plain"}], response_body, req)
    {:ok, req, nil}
  end

  def terminate(_reason, _req, _state), do: :ok

  # private

  defp handle_request(req) do
    forward_request(req) # for now we just forward the HTTP request as CoAP
  end

  defp forward_request(req) do
    case Client.request(extract_path(req), extract_method(req), extract_content(req)) do
      task = %Task{} ->
        {:ok, _response_type, {:coap_content, _etag, _max_age, _format, payload}} = Task.await(task)
        {200, payload}
      :not_found -> {404, "not found"}
    end
  end

  defp extract_path(req) do
    :cowboy_req.path(req) |> String.slice(1, String.length(:cowboy_req.path(req)))
  end

  defp extract_method(req) do
    :cowboy_req.method(req) |> String.downcase |> String.to_atom
  end

  defp extract_content(req) do
    {:ok, request_body, _req} = :cowboy_req.body(req)
    coap_content(payload: request_body)
  end
end

All the hard work was already done in part 1. CoapDirectory.Client allows us to make a CoAP request. The only thing to do here is to extract the path, method and body of the HTTP request and wrap the body in the :coap_content record using the coap_content/1 macro imported from Coap.Records.

But that is not all we can do with CoAP. It also allows us to start an observation. In part 1 we implemented the observer as a GenServer CoapDirectory.Observer. It is started by a supervisor with a :simple_one_for_one strategy. It uses the gen_coap application underneath and accepts a target_pid on initialization as its state. It responds to CoAP notifications by forwarding their payloads to the process with this given target_pid.

The question is: how can our HTTP API allow an observation? I have decided to solve this by allowing the outside world to attach a special header to the request. If a request has an “observe” header, instead of forwarding the HTTP request as a CoAP request, we will start an observation. The value of this header would be a callback address used to forward the notifications. And since our observer already sends all the notifications to the process with given PID, all we need is the actual process. It will be a GenServer making an HTTP request to the given callback URL whenever it receives a notification. We use HTTPoison as an HTTP client and Poison for JSON encoding.

# coap_directory/lib/coap_directory/http_request_handler.ex
defp handle_request(req) do
  # now we either forward the request or start an observation
  case :cowboy_req.header("observe", req) do
    :undefined -> forward_request(req)
    callback_url -> start_observation(req, callback_url)
  end
end

defp start_observation(req, callback_url) do
  {:ok, responder_pid} = ObservationResponderSupervisor.start_observation_responder(callback_url)
  case ObserverSupervisor.start_observer(extract_path(req), responder_pid) do
    {:ok, _observer_pid} -> {200, "observation started"}
    {:error, :not_found} -> {404, "not found"}
  end
end

# coap_directory/lib/coap_directory/observation_responder_supervisor.ex
defmodule CoapDirectory.ObservationResponderSupervisor do
  use Supervisor

  @name CoapDirectory.ObservationResponderSupervisor

  def start_link do
    Supervisor.start_link(__MODULE__, [], name: @name)
  end

  def start_observation_responder(callback_url) do
    Supervisor.start_child(@name, [callback_url])
  end

  def init([]) do
    children = [
      worker(CoapDirectory.ObservationResponder, [], restart: :temporary)
    ]

    supervise(children, strategy: :simple_one_for_one)
  end
end

# coap_directory/lib/coap_directory/observation_responder.ex
defmodule CoapDirectory.ObservationResponder do
  use GenServer
  use HTTPoison.Base

  def start_link(callback_url) do
    GenServer.start_link(__MODULE__, [callback_url], [])
  end

  # GenServer handlers

  def init([callback_url]) do
    {:ok, callback_url}
  end

  def handle_info(resource_observation, callback_url) do
    [resource_name, resource_state] = String.split(resource_observation, " ")
    post!(callback_url, %{resource: %{name: resource_name, state: resource_state}})
    {:noreply, callback_url}
  end

  defp process_request_body(body) do
    Poison.encode!(body)
  end

  defp process_request_headers(headers) do
    [{"content-type", "application/json"} | headers]
  end
end

The easiest way to see the API in action now is to issue HTTP requests via curl or a client with a GUI like Postman. At the end of part 1, I showed how to manipulate resources by directly using our CoAP client. Now we can do the exact same thing, but instead of directly invoking CoapDirectory.Client.put("switches/1", "toggle") for instance, we can issue a PUT HTTP request to localhost:8080/switches/1 with a “toggle” payload. To test the observation, however, we need a server that will provide a callback endpoint to be hit whenever a notification is received.

Phoenix webserver

We will use a tiny Phoenix app for this task. What we need is an input box for typing in a resource name, a button to start an observation and that’s it. We will be printing out the notifications in the console.

Starting with mix phoenix.new coap_webserver, we can move on to creating a resource channel. On joining the channel, we will be making a request with the special “observe” header that I have mentioned before.

# coap_webserver/web/channels/user_socket.ex
channel "resource:*", CoapWebserver.ResourceChannel

# coap_webserver/web/channels/resource_channel.ex
defmodule CoapWebserver.ResourceChannel do
  use Phoenix.Channel
  alias CoapWebserver.ResourceClient

  @callback_path "/api/resource"

  def join("resource:" <> resource_name, _params, socket) do
    case start_observation!(resource_name) do
      :ok -> {:ok, socket}
      {:error, error_msg} -> {:error, %{reason: error_msg}}
    end
  end

  defp start_observation!(resource_name) do
    observation_header = {"observe", callback_url}
    case ResourceClient.get!(resource_name, [observation_header]) do
      %{:status_code => 200, :body => "observation started"} -> :ok
      %{:status_code => _, :body => error_msg} -> {:error, error_msg}
    end
  end

  defp callback_url do
    CoapWebserver.Endpoint.url <> @callback_path
  end
end

defmodule CoapWebserver.ResourceClient do
  use HTTPoison.Base

  @directory_addr "localhost:8080"

  defp process_url(url) do
    "http://" <> @directory_addr <> "/" <> url
  end
end

The browser can start an observation of a resource, e.g. switches/1, by joining the resource:switches/1 address. We use the part after the colon to make a request to our brand new HTTP API.

For now, we have a hard-coded directory address. In a real application, it would not only be extracted into an env variable but probably we would have a way of using multiple directories. I can imagine a dashboard listing available directories with their resources. But one step at a time - this is enough for now.

We have specified our callback_url as CoapWebserver.Endpoint.url <> "/api/resource". Let’s now implement this endpoint.

# coap_webserver/web/router.ex
scope "/api", CoapWebserver do
  pipe_through :api

  post "/resource", ResourceController, :update
end

# coap_webserver/web/controllers/resource_controller.ex
defmodule CoapWebserver.ResourceController do
  use CoapWebserver.Web, :controller

  def update(conn, %{"resource" => resource}) do
    CoapWebserver.Endpoint.broadcast!(
      "resource:" <> resource["name"], "resource_state_updated", resource
    )
    render conn, "update.json", resource: resource
  end
end

The endpoint expects to receive a resource with a name and it broadcasts this resource on the WebSocket channels. All that is left to do is to use this subscription in the browser.

<input id="resource-name-input"></input>
<button id="observe-resource-button">Observe!</button>
import socket from "./socket"

const resourceInput = document.getElementById("resource-name-input")
const observeButton = document.getElementById("observe-resource-button")

observeButton.addEventListener("click", () => {
  const resourcePath = resourceInput.value
  const channel = socket.channel("resource:" + resourcePath, {})

  channel.on("resource_state_updated", (resource) => {
    console.log("name: " + resource.name + " state: " + resource.state);
  })

  channel.join()
    .receive("ok", resp => { console.log("Joined successfully", resp) })
    .receive("error", resp => { console.log("Unable to join", resp) })
})

Here’s how we can demonstrate the whole setup:

  1. Start a CoapDirectory and add a resource via CoapNode as described in part 1.
  2. Visit your Phoenix app in the browser.
  3. Open the browser console.
  4. Start an observation of a resource by typing its path in the input box, e.g. switches/2.
  5. If the CoapDirectory is running and such a resource has been registered, clicking the Observe! button will start an observation. From now on you will see the updates to this resource logged in the console.
  6. Try changing the resource's state by making a PUT request toggling the resource: iex(4)> CoapWebserver.ResourceClient.put("switches/2", "toggle").
  7. Check the console - you should be asynchronously informed of the update to the resource.

Summing up

We have achieved a connection from a node through a directory using CoAP, through a webserver using HTTP all the way to the browser using WebSockets. The setup is basic and simple, but it could be expanded and built upon.

In the next parts, we will be doing just that. Additionally, we will try to put CoapDirectory on a Raspberry PI using Nerves or maybe even implement CoapNode natively. Stay tuned!

Repositories:

Craving for more knowledge on IoT? Check out other posts:

Thinking of an IoT development?

Our devs are so communicative and diligent you’ll feel they are your in-house team. Work with JavaScript experts who will push hard to understand your business and meet certain deadlines.

Talk to our team and confidently build your next big thing.

Michał Podwórny avatar
Michał Podwórny