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:
- https://github.com/mskv/coap_webserver
- https://github.com/mskv/coap_directory
- https://github.com/mskv/coap_node
- https://github.com/mskv/coap
Craving for more knowledge on IoT? Check out other posts:
- Speed up your IoT development with ready to go middlewares. Round one: Kii, Round two: Samsung ARTIK, Round three: IBM Bluemix
- Monterale Breweree: How we merge passion of brewing beer with IoT
- IoT Healthcare App: Heartbeat detector using phone camera
- Ideas For Potential IoT Applications Using LED Light Bulbs
- How to visualize complex, real-time IoT data: Design and UX principles
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.