|
1 | 1 | defmodule CoAP.Client do |
2 | 2 | @moduledoc """ |
3 | | - CoAP Client interface |
| 3 | + CoAP Client interface |
4 | 4 | """ |
5 | 5 | alias CoAP.Message |
6 | | - |
7 | | - import Logger, only: [debug: 1] |
8 | | - |
| 6 | + alias CoAP.Transport |
| 7 | + |
| 8 | + # Default timeout: 5 sec |
| 9 | + @default_timeout 5_000 |
| 10 | + @message_opts [:ack_timeout, :max_retransmit, :socket_adapter, :socket_opts] |
| 11 | + |
| 12 | + @type option() :: |
| 13 | + {:ack_timeout, integer()} |
| 14 | + | {:max_retransmit, integer()} |
| 15 | + | {:socket_adapter, module()} |
| 16 | + | {:socket_opts, any()} |
| 17 | + | {:timeout, integer()} |
| 18 | + | {:confirmable, boolean()} |
| 19 | + |
| 20 | + @typedoc """ |
| 21 | + Options related to Message layer behaviour: |
| 22 | + * `ack_timeout`: initial timeout for receiving ack |
| 23 | + * `max_retransmit`: max retransmission attempts |
| 24 | + * `socket_adapter`: module implementing `CoAP.Transport` behaviour. Default |
| 25 | + is to infer from peer URI. |
| 26 | + * `socket_opts`: options passed to socket adapter |
| 27 | +
|
| 28 | + Options related to Request/Response layer |
| 29 | + * `timeout`: timeout for receiving response |
| 30 | + * `confirmable`: if true, request is confirmable |
| 31 | + """ |
| 32 | + @type options :: [option()] |
9 | 33 | @type request_url :: binary |
10 | | - @type request_type :: :con | :non | :ack | :reset |
11 | 34 | @type request_method :: :get | :post | :put | :delete |
12 | 35 | @type response_error :: {:timeout, :await_response} | any |
13 | | - @type response :: CoAP.Message.t() | {:error, response_error} |
14 | | - |
15 | | - defmodule Options do |
16 | | - # spec default for max_retransmits |
17 | | - @max_retries 4 |
18 | | - @wait_timeout 10_000 |
19 | | - |
20 | | - @type t :: %__MODULE__{ |
21 | | - retries: integer, |
22 | | - retry_timeout: integer, |
23 | | - timeout: integer, |
24 | | - ack_timeout: integer, |
25 | | - tag: any |
26 | | - } |
27 | | - |
28 | | - defstruct retries: @max_retries, |
29 | | - retry_timeout: nil, |
30 | | - timeout: @wait_timeout, |
31 | | - ack_timeout: nil, |
32 | | - tag: nil |
33 | | - end |
34 | | - |
35 | | - # _TODO: options: headers/params? |
| 36 | + @type response :: Message.t() | {:error, response_error} |
36 | 37 |
|
37 | 38 | @doc """ |
38 | | - Perform a confirmable, GET request to a URL |
39 | | - Returns a `CoAP.Message` response, or an error tuple |
40 | | - Optionally takes a binary content payload |
| 39 | + Perform GET request to a URL |
| 40 | + Returns a `CoAP.Message` response, or an error tuple |
41 | 41 |
|
42 | | - CoAP.Client.get("coap://localhost:5683/api/") |
| 42 | + CoAP.Client.get("coap://localhost:5683/api/") |
43 | 43 | """ |
44 | | - @spec get(request_url, binary) :: response |
45 | | - def get(url, content \\ <<>>), do: con(:get, url, content) |
| 44 | + @spec get(request_url, options) :: response |
| 45 | + def get(url, options \\ []), do: request(:get, url, <<>>, options) |
46 | 46 |
|
47 | 47 | @doc """ |
48 | | - Perform a confirmable, POST request to a URL |
49 | | - Returns a `CoAP.Message` response, or an error tuple |
50 | | - Optionally takes a binary content payload |
| 48 | + Perform POST request to a URL |
| 49 | + Returns a `CoAP.Message` response, or an error tuple |
| 50 | + Optionally takes a binary content payload |
51 | 51 |
|
52 | | - CoAP.Client.post("coap://localhost:5683/api/", <<0x00, 0x01, …>>) |
| 52 | + CoAP.Client.post("coap://localhost:5683/api/", <<0x00, 0x01, …>>) |
53 | 53 | """ |
54 | | - @spec post(request_url, binary) :: response |
55 | | - def post(url, content \\ <<>>), do: con(:post, url, content) |
| 54 | + @spec post(request_url, binary, options) :: response |
| 55 | + def post(url, content \\ <<>>, options), do: request(:post, url, content, options) |
56 | 56 |
|
57 | 57 | @doc """ |
58 | | - Perform a confirmable, PUT request to a URL |
59 | | - Returns a `CoAP.Message` response, or an error tuple |
60 | | - Optionally takes a binary content payload |
| 58 | + Perform PUT request to a URL |
| 59 | + Returns a `CoAP.Message` response, or an error tuple |
| 60 | + Optionally takes a binary content payload |
61 | 61 |
|
62 | | - CoAP.Client.put("coap://localhost:5683/api/", "somepayload") |
| 62 | + CoAP.Client.put("coap://localhost:5683/api/", "somepayload") |
63 | 63 | """ |
64 | | - @spec put(request_url, binary) :: response |
65 | | - def put(url, content \\ <<>>), do: con(:put, url, content) |
| 64 | + @spec put(request_url, binary, options) :: response |
| 65 | + def put(url, content \\ <<>>, options \\ []), do: request(:put, url, content, options) |
66 | 66 |
|
67 | 67 | @doc """ |
68 | | - Perform a confirmable, DELETE request to a URL |
69 | | - Returns a `CoAP.Message` response, or an error tuple |
| 68 | + Perform a DELETE request to a URL |
| 69 | + Returns a `CoAP.Message` response, or an error tuple |
70 | 70 |
|
71 | | - CoAP.Client.delete("coap://localhost:5683/api/") |
| 71 | + CoAP.Client.delete("coap://localhost:5683/api/") |
72 | 72 | """ |
73 | | - @spec delete(request_url) :: response |
74 | | - def delete(url), do: con(:delete, url) |
| 73 | + @spec delete(request_url, options) :: response |
| 74 | + def delete(url, options \\ []), do: request(:delete, url, <<>>, options) |
75 | 75 |
|
76 | 76 | @doc """ |
77 | | - Perform a confirmable request of any method (get/post/put/delete) |
78 | | - Returns a `CoAP.Message` response, or an error tuple |
| 77 | + Perform a request |
79 | 78 |
|
80 | | - CoAP.Client.con(:get, "coap://localhost:5683/api/", "somepayload") |
81 | | - """ |
82 | | - @spec con(request_method, request_url, binary) :: response |
83 | | - def con(method, url, content \\ <<>>), do: request(:con, method, url, content) |
84 | | - # defp non(method, url), do: request(:non, method, url) |
85 | | - # defp ack(method, url), do: request(:ack, method, url) |
86 | | - # defp reset(method, url), do: request(:reset, method, url) |
| 79 | + Accepts 3-5 arguments: |
| 80 | + * method: 1 of :get, :post, :put :delete |
| 81 | + * url: binary, parseable by `URI.parse()` |
| 82 | + * content (optional): a binary payload |
| 83 | + * options (optional): see `options()` typespec |
87 | 84 |
|
88 | | - @doc """ |
89 | | - Perform a request |
90 | | -
|
91 | | - Accepts 3-5 arguments: |
92 | | - * type: 1 of :con, :non, :ack, :reset |
93 | | - * method: 1 of :get, :post, :put :delete |
94 | | - * url: binary, parseable by `:uri_string.parse` |
95 | | - * optional content: a binary payload |
96 | | - * optional options: a map of options - retries, retry_timeout, and timeout |
97 | | -
|
98 | | - Returns the binary of the response |
| 85 | + Returns response message or error tuple |
99 | 86 | """ |
100 | | - @spec request(request_type, request_method, request_url, binary, map) :: |
101 | | - response |
102 | | - def request(type, method, url, content \\ <<>>, options \\ %{}) do |
103 | | - uri = :uri_string.parse(url) |
104 | | - |
105 | | - host = uri[:host] |
106 | | - port = uri[:port] |
107 | | - token = :crypto.strong_rand_bytes(4) |
108 | | - |
109 | | - options = struct(Options, options) |
110 | | - |
| 87 | + @spec request(request_method, request_url, binary, options) :: response |
| 88 | + def request(method, url, content \\ <<>>, options \\ []) do |
| 89 | + uri = URI.parse(url) |
111 | 90 | {code_class, code_detail} = Message.encode_method(method) |
112 | 91 |
|
113 | 92 | message = %Message{ |
114 | 93 | request: true, |
115 | | - type: type, |
| 94 | + type: if(Keyword.get(options, :confirmable, true), do: :con, else: :non), |
116 | 95 | method: method, |
117 | | - token: token, |
| 96 | + token: :crypto.strong_rand_bytes(4), |
118 | 97 | code_class: code_class, |
119 | 98 | code_detail: code_detail, |
120 | 99 | payload: content, |
121 | | - options: %{uri_path: String.split(uri[:path], "/")} |
| 100 | + options: %{uri_path: String.split(uri.path, "/")} |
122 | 101 | } |
123 | 102 |
|
124 | | - debug("Client Request: #{inspect(message)}") |
| 103 | + fn -> do_request(uri, message, options) end |
| 104 | + |> Task.async() |
| 105 | + |> Task.await(:infinity) |
| 106 | + end |
125 | 107 |
|
126 | | - {:ok, connection} = CoAP.Connection.start_link([self(), {host, port, token}, options]) |
| 108 | + ### |
| 109 | + ### Private |
| 110 | + ### |
| 111 | + defp do_request(uri, message, options) do |
| 112 | + {message_opts, rr_opts} = Keyword.split(options, @message_opts) |
| 113 | + {:ok, transport} = Transport.start_link(self(), [{:peer, uri} | message_opts]) |
| 114 | + |
| 115 | + try do |
| 116 | + send(transport, message) |
| 117 | + timeout = Keyword.get(rr_opts, :timeout, @default_timeout) |
| 118 | + %Message{token: token, message_id: mid} = message |
| 119 | + start_time = System.monotonic_time(:millisecond) |
| 120 | + waiting(transport, mid, token, start_time, timeout) |
| 121 | + after |
| 122 | + _ = Transport.stop(transport) |
| 123 | + end |
| 124 | + end |
127 | 125 |
|
128 | | - send(connection, {:deliver, message}) |
| 126 | + defp waiting(transport, mid, token, start_time, timeout) do |
| 127 | + receive do |
| 128 | + {:rr_fail, ^mid, reason} -> |
| 129 | + {:error, reason} |
129 | 130 |
|
130 | | - await_response(message, options.timeout) |
| 131 | + {:rr_rx, %Message{type: :ack, message_id: ^mid, token: ^token, payload: <<>>}, _peer} -> |
| 132 | + # Separate response |
| 133 | + timeout = max(0, timeout - (System.monotonic_time(:millisecond) - start_time)) |
| 134 | + waiting_separate(transport, token, timeout) |
| 135 | + |
| 136 | + {:rr_rx, %Message{type: :ack, message_id: ^mid, token: ^token} = m, _peer} -> |
| 137 | + # Piggybacked response |
| 138 | + m |
| 139 | + |
| 140 | + {:rr_rx, %Message{type: :non, token: ^token} = m, _peer} -> |
| 141 | + # Non confirmable response |
| 142 | + m |
| 143 | + after |
| 144 | + timeout -> |
| 145 | + {:error, {:timeout, :await_response}} |
| 146 | + end |
131 | 147 | end |
132 | 148 |
|
133 | | - defp await_response(_message, timeout) do |
| 149 | + defp waiting_separate(transport, token, timeout) do |
134 | 150 | receive do |
135 | | - {:deliver, response, _peer} -> response |
136 | | - {:error, reason} -> {:error, reason} |
| 151 | + {:rr_rx, %Message{type: :non, token: ^token} = m, _peer} -> |
| 152 | + m |
| 153 | + |
| 154 | + {:rr_rx, %Message{type: :con, token: ^token} = m, _peer} -> |
| 155 | + send(transport, Message.response_for(m)) |
| 156 | + m |
137 | 157 | after |
138 | | - timeout -> {:error, {:timeout, :await_response}} |
| 158 | + timeout -> |
| 159 | + {:error, {:timeout, :await_response}} |
139 | 160 | end |
140 | 161 | end |
141 | 162 | end |
0 commit comments