Packages

Lib for interacting with roku devices

Current section

Files

Jump to
nervoku lib ssdp.ex
Raw

lib/ssdp.ex

defmodule SSDP do
@doc ~S"""
M-SEARCH * HTTP/1.1
Host: 239.255.255.250:1900
Man: "ssdp:discover"
ST: roku:ecp
"""
@moduledoc """
Implements a simple discovery client for SSDP, not the full UPNP spec.
- Supports M-SEARCH and gathering unicast responses.
- Does not support prolonged listens and gathered notifies at this time.
"""
@default_target "ssdp:all"
@roku_target "roku:ecp"
@default_seconds 2
@default_slack 250
def roku(), do: @roku_target
@doc """
Issue an SSDP "M-SEARCH" request to the network, and listen for a bit for
responses, reporting back discovered nodes in a hash.
Parameters is Dict with the following defined keyword atoms:
`target` - the search target, as defined by the ssdp spec. Defaults to "ssdp:all"
`seconds` - sent with the search request to tell nodes how long they have to
respond. Defaults to 2.
`slack` - Number of milliseconds to wait __after__ the seconds parameter for responses.
Defaults to 250, but can be set higher for high-latency networks
"""
def discover(params \\ []) do
target = params[:target] || @default_target
seconds = params[:seconds] || @default_seconds
slack_ms = params[:slack] || @default_slack
timeout_ms = seconds * 1000 + slack_ms
message = search_msg(target, seconds)
{:ok, socket} = :gen_udp.open(0, [{:reuseaddr, true}])
:gen_udp.send(socket, {239, 255, 255, 250}, 1900, message)
Process.send_after(self(), :timeout, timeout_ms)
gather_responses_until_timeout(%{}, socket)
end
defp gather_responses_until_timeout(gathered, socket) do
receive do
{:udp, socket, host, _port, msg} ->
gathered
|> Map.merge(decoded_response(host, msg))
|> gather_responses_until_timeout(socket)
:timeout ->
:gen_udp.close(socket)
gathered
end
end
# returns a dict with usn as key, like [usn: %{host: ..., ...., ...}]
defp decoded_response(host, msg) do
a_host =
host
|> :inet.ntoa()
|> :erlang.list_to_binary()
msg
|> :erlang.list_to_binary()
|> decode_ssdp_packet
|> Map.put(:host, a_host)
|> keyed_by_usn()
end
# returns a dict where key is usn, and value is rest of dict
defp keyed_by_usn(dict) do
Map.put(%{}, dict.usn, dict)
end
# returns keys/values given an ssdp packet
defp decode_ssdp_packet(packet) do
{[_raw_http_line], raw_params} =
packet
|> String.split(["\r\n", "\n"])
|> Enum.split(1)
# http_line = String.downcase(raw_http_line) |> String.strip
# {[http_verb, {full_uri], _rest} = String.split(http_line) |> Enum.split(2)
mapped_params =
Enum.map(raw_params, fn x ->
case String.split(x, ":", parts: 2) do
[k, v] -> {String.to_atom(String.downcase(k)), String.trim(v)}
_ -> nil
end
end)
resp = Enum.reject(mapped_params, &(&1 == nil))
# convert to map, REVIEW better way?
Enum.into(resp, %{})
end
defp search_msg(search_target, max_seconds) do
"M-SEARCH * HTTP/1.1\r\n" <>
"Host: 239.255.255.250:1900\r\n" <>
"MAN: \"ssdp:discover\"\r\n" <>
"ST: #{search_target}\r\nMX: #{max_seconds}\r\n\r\n"
end
end