Skip to content

Commit 13e78df

Browse files
author
Jean Parpaillon
committed
Rewrite client (request/response) layer, using new Message layer
1 parent c5beb06 commit 13e78df

6 files changed

Lines changed: 333 additions & 276 deletions

File tree

lib/coap/client.ex

Lines changed: 114 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,162 @@
11
defmodule CoAP.Client do
22
@moduledoc """
3-
CoAP Client interface
3+
CoAP Client interface
44
"""
55
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()]
933
@type request_url :: binary
10-
@type request_type :: :con | :non | :ack | :reset
1134
@type request_method :: :get | :post | :put | :delete
1235
@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}
3637

3738
@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
4141
42-
CoAP.Client.get("coap://localhost:5683/api/")
42+
CoAP.Client.get("coap://localhost:5683/api/")
4343
"""
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)
4646

4747
@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
5151
52-
CoAP.Client.post("coap://localhost:5683/api/", <<0x00, 0x01, …>>)
52+
CoAP.Client.post("coap://localhost:5683/api/", <<0x00, 0x01, …>>)
5353
"""
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)
5656

5757
@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
6161
62-
CoAP.Client.put("coap://localhost:5683/api/", "somepayload")
62+
CoAP.Client.put("coap://localhost:5683/api/", "somepayload")
6363
"""
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)
6666

6767
@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
7070
71-
CoAP.Client.delete("coap://localhost:5683/api/")
71+
CoAP.Client.delete("coap://localhost:5683/api/")
7272
"""
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)
7575

7676
@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
7978
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
8784
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
9986
"""
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)
11190
{code_class, code_detail} = Message.encode_method(method)
11291

11392
message = %Message{
11493
request: true,
115-
type: type,
94+
type: if(Keyword.get(options, :confirmable, true), do: :con, else: :non),
11695
method: method,
117-
token: token,
96+
token: :crypto.strong_rand_bytes(4),
11897
code_class: code_class,
11998
code_detail: code_detail,
12099
payload: content,
121-
options: %{uri_path: String.split(uri[:path], "/")}
100+
options: %{uri_path: String.split(uri.path, "/")}
122101
}
123102

124-
debug("Client Request: #{inspect(message)}")
103+
fn -> do_request(uri, message, options) end
104+
|> Task.async()
105+
|> Task.await(:infinity)
106+
end
125107

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
127125

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}
129130

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
131147
end
132148

133-
defp await_response(_message, timeout) do
149+
defp waiting_separate(transport, token, timeout) do
134150
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
137157
after
138-
timeout -> {:error, {:timeout, :await_response}}
158+
timeout ->
159+
{:error, {:timeout, :await_response}}
139160
end
140161
end
141162
end

0 commit comments

Comments
 (0)