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:

Using the CoAP protocol, CoapNode registers a resource in the CoapDirectory under a path, e.g. switches/1 . We hit the CoapDirectory’s HTTP API, for instance issuing a GET http://(directory_ip)/switches/1 request. 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 . 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:

Start a CoapDirectory and add a resource via CoapNode as described in part 1. Visit your Phoenix app in the browser. Open the browser console. Start an observation of a resource by typing its path in the input box, e.g. switches/2 . 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. Try changing the resource's state by making a PUT request toggling the resource: iex(4)> CoapWebserver.ResourceClient.put("switches/2", "toggle") . 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: