Merge pull request #5598 from lafirest/refactor/emqx_lwm2m_c
refactor(emqx_lwm2m): port lwm2m into emqx_gateway framework
This commit is contained in:
commit
187f878baf
|
@ -134,7 +134,7 @@ gateway.lwm2m {
|
|||
enable_stats = true
|
||||
|
||||
## When publishing or subscribing, prefix all topics with a mountpoint string.
|
||||
mountpoint = "lwm2m/%e/"
|
||||
mountpoint = "lwm2m"
|
||||
|
||||
xml_dir = "{{ platform_etc_dir }}/lwm2m_xml"
|
||||
|
||||
|
@ -146,12 +146,32 @@ gateway.lwm2m {
|
|||
## always | contains_object_list
|
||||
update_msg_publish_condition = contains_object_list
|
||||
|
||||
|
||||
translators {
|
||||
command = "dn/#"
|
||||
response = "up/resp"
|
||||
notify = "up/notify"
|
||||
register = "up/resp"
|
||||
update = "up/resp"
|
||||
command {
|
||||
topic = "dn/#"
|
||||
qos = 0
|
||||
}
|
||||
|
||||
response {
|
||||
topic = "up/resp"
|
||||
qos = 0
|
||||
}
|
||||
|
||||
notify {
|
||||
topic = "up/notify"
|
||||
qos = 0
|
||||
}
|
||||
|
||||
register {
|
||||
topic = "up/resp"
|
||||
qos = 0
|
||||
}
|
||||
|
||||
update {
|
||||
topic = "up/resp"
|
||||
qos = 0
|
||||
}
|
||||
}
|
||||
|
||||
listeners.udp.default {
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
4. [Query String](#org9a6b996)
|
||||
2. [Implementation](#org9985dfe)
|
||||
1. [Request/Response flow](#orge94210c)
|
||||
3. [Example](#ref_example)
|
||||
|
||||
|
||||
|
||||
|
@ -401,3 +402,33 @@ CoAP gateway uses some options in query string to conversion between MQTT CoAP.
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a id="ref_example"></a>
|
||||
|
||||
## Example
|
||||
1. Create Connection
|
||||
```
|
||||
coap-client -m post -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public"
|
||||
```
|
||||
Server will return token **X** in payload
|
||||
|
||||
2. Update Connection
|
||||
```
|
||||
coap-client -m put -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public&token=X"
|
||||
```
|
||||
|
||||
3. Publish
|
||||
```
|
||||
coap-client -m post -e "Hellow" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public"
|
||||
```
|
||||
if you want to publish with auth, you must first establish a connection, and then post publish request on the same socket, so libcoap client can't simulation publish with a token
|
||||
|
||||
4. Subscribe
|
||||
```
|
||||
coap-client -m get -s 60 -O 6,0x00 -o - -T "obstoken" "coap://127.0.0.1/ps/coap/test?clientid=123&username=admin&password=public"
|
||||
```
|
||||
|
||||
5. Close Connection
|
||||
```
|
||||
coap-client -m delete -e "" "coap://127.0.0.1/mqtt/connection?clientid=123&username=admin&password=public&token=X"
|
||||
```
|
Binary file not shown.
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 75 KiB |
|
@ -22,17 +22,10 @@
|
|||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
|
||||
%% API
|
||||
-export([]).
|
||||
|
||||
-export([ info/1
|
||||
, info/2
|
||||
, stats/1
|
||||
, validator/3
|
||||
, get_clientinfo/1
|
||||
, get_config/2
|
||||
, get_config/3
|
||||
, result_keys/0
|
||||
, transfer_result/3]).
|
||||
, validator/4]).
|
||||
|
||||
-export([ init/2
|
||||
, handle_in/2
|
||||
|
@ -61,20 +54,17 @@
|
|||
keepalive :: emqx_keepalive:keepalive() | undefined,
|
||||
%% Timer
|
||||
timers :: #{atom() => disable | undefined | reference()},
|
||||
token :: binary() | undefined,
|
||||
config :: hocon:config()
|
||||
|
||||
conn_state :: idle | connected,
|
||||
|
||||
token :: binary() | undefined
|
||||
}).
|
||||
|
||||
%% the execuate context for session call
|
||||
-record(exec_ctx, { config :: hocon:config(),
|
||||
ctx :: emqx_gateway_ctx:context(),
|
||||
clientinfo :: emqx_types:clientinfo()
|
||||
}).
|
||||
|
||||
-type channel() :: #channel{}.
|
||||
-define(DISCONNECT_WAIT_TIME, timer:seconds(10)).
|
||||
-define(TOKEN_MAXIMUM, 4294967295).
|
||||
-define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]).
|
||||
|
||||
-import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]).
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -87,8 +77,8 @@ info(Keys, Channel) when is_list(Keys) ->
|
|||
|
||||
info(conninfo, #channel{conninfo = ConnInfo}) ->
|
||||
ConnInfo;
|
||||
info(conn_state, _) ->
|
||||
connected;
|
||||
info(conn_state, #channel{conn_state = CState}) ->
|
||||
CState;
|
||||
info(clientinfo, #channel{clientinfo = ClientInfo}) ->
|
||||
ClientInfo;
|
||||
info(session, #channel{session = Session}) ->
|
||||
|
@ -106,18 +96,13 @@ init(ConnInfo = #{peername := {PeerHost, _},
|
|||
#{ctx := Ctx} = Config) ->
|
||||
Peercert = maps:get(peercert, ConnInfo, undefined),
|
||||
Mountpoint = maps:get(mountpoint, Config, undefined),
|
||||
EnableAuth = is_authentication_enabled(Config),
|
||||
ClientInfo = set_peercert_infos(
|
||||
Peercert,
|
||||
#{ zone => default
|
||||
, protocol => 'coap'
|
||||
, peerhost => PeerHost
|
||||
, sockport => SockPort
|
||||
, clientid => if EnableAuth ->
|
||||
undefined;
|
||||
true ->
|
||||
emqx_guid:to_base62(emqx_guid:gen())
|
||||
end
|
||||
, clientid => emqx_guid:to_base62(emqx_guid:gen())
|
||||
, username => undefined
|
||||
, is_bridge => false
|
||||
, is_superuser => false
|
||||
|
@ -125,56 +110,29 @@ init(ConnInfo = #{peername := {PeerHost, _},
|
|||
}
|
||||
),
|
||||
|
||||
Heartbeat = emqx:get_config([gateway, coap, idle_timeout]),
|
||||
#channel{ ctx = Ctx
|
||||
, conninfo = ConnInfo
|
||||
, clientinfo = ClientInfo
|
||||
, timers = #{}
|
||||
, config = Config
|
||||
, session = emqx_coap_session:new()
|
||||
, keepalive = emqx_keepalive:init(maps:get(heartbeat, Config))
|
||||
, keepalive = emqx_keepalive:init(Heartbeat)
|
||||
, conn_state = idle
|
||||
}.
|
||||
|
||||
is_authentication_enabled(Cfg) ->
|
||||
case maps:get(authentication, Cfg, #{enable => false}) of
|
||||
AuthCfg when is_map(AuthCfg) ->
|
||||
maps:get(enable, AuthCfg, true);
|
||||
_ -> false
|
||||
end.
|
||||
|
||||
validator(Type, Topic, #exec_ctx{ctx = Ctx,
|
||||
clientinfo = ClientInfo}) ->
|
||||
validator(Type, Topic, Ctx, ClientInfo) ->
|
||||
emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic).
|
||||
|
||||
get_clientinfo(#exec_ctx{clientinfo = ClientInfo}) ->
|
||||
ClientInfo.
|
||||
|
||||
get_config(Key, Ctx) ->
|
||||
get_config(Key, Ctx, undefined).
|
||||
|
||||
get_config(Key, #exec_ctx{config = Cfg}, Def) ->
|
||||
maps:get(Key, Cfg, Def).
|
||||
|
||||
result_keys() ->
|
||||
[out, connection].
|
||||
|
||||
transfer_result(From, Value, Result) ->
|
||||
?TRANSFER_RESULT(From, Value, Result).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Handle incoming packet
|
||||
%%--------------------------------------------------------------------
|
||||
handle_in(Msg, ChannleT) ->
|
||||
Channel = ensure_keepalive_timer(ChannleT),
|
||||
case convert_queries(Msg) of
|
||||
{ok, Msg2} ->
|
||||
case emqx_coap_message:is_request(Msg2) of
|
||||
true ->
|
||||
check_auth_state(Msg2, Channel);
|
||||
_ ->
|
||||
call_session(handle_response, Msg2, Channel)
|
||||
end;
|
||||
case emqx_coap_message:is_request(Msg) of
|
||||
true ->
|
||||
check_auth_state(Msg, Channel);
|
||||
_ ->
|
||||
response({error, bad_request}, <<"bad uri_query">>, Msg, Channel)
|
||||
call_session(handle_response, Msg, Channel)
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -258,94 +216,57 @@ make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) ->
|
|||
ensure_keepalive_timer(Channel) ->
|
||||
ensure_keepalive_timer(fun ensure_timer/4, Channel).
|
||||
|
||||
ensure_keepalive_timer(Fun, #channel{config = Cfg} = Channel) ->
|
||||
Interval = maps:get(heartbeat, Cfg),
|
||||
Fun(keepalive, Interval, keepalive, Channel).
|
||||
ensure_keepalive_timer(Fun, Channel) ->
|
||||
Heartbeat = emqx:get_config([gateway, coap, idle_timeout]),
|
||||
Fun(keepalive, Heartbeat, keepalive, Channel).
|
||||
|
||||
call_session(Fun,
|
||||
Msg,
|
||||
#channel{session = Session} = Channel) ->
|
||||
Ctx = new_exec_ctx(Channel),
|
||||
Result = erlang:apply(emqx_coap_session, Fun, [Msg, Ctx, Session]),
|
||||
process_result([session, connection, out], Result, Msg, Channel).
|
||||
|
||||
process_result([Key | T], Result, Msg, Channel) ->
|
||||
case handle_result(Key, Result, Msg, Channel) of
|
||||
{ok, Channel2} ->
|
||||
process_result(T, Result, Msg, Channel2);
|
||||
Other ->
|
||||
Other
|
||||
end;
|
||||
|
||||
process_result(_, _, _, Channel) ->
|
||||
{ok, Channel}.
|
||||
|
||||
handle_result(session, #{session := Session}, _, Channel) ->
|
||||
{ok, Channel#channel{session = Session}};
|
||||
|
||||
handle_result(connection, #{connection := open}, Msg, Channel) ->
|
||||
do_connect(Msg, Channel);
|
||||
|
||||
handle_result(connection, #{connection := close}, Msg, Channel) ->
|
||||
Reply = emqx_coap_message:piggyback({ok, deleted}, Msg),
|
||||
{shutdown, close, {outgoing, Reply}, Channel};
|
||||
|
||||
handle_result(out, #{out := Out}, _, Channel) ->
|
||||
{ok, {outgoing, Out}, Channel};
|
||||
|
||||
handle_result(_, _, _, Channel) ->
|
||||
{ok, Channel}.
|
||||
|
||||
check_auth_state(Msg, #channel{config = Cfg} = Channel) ->
|
||||
Enable = is_authentication_enabled(Cfg),
|
||||
check_auth_state(Msg, Channel) ->
|
||||
Enable = emqx:get_config([gateway, coap, enable_stats]),
|
||||
check_token(Enable, Msg, Channel).
|
||||
|
||||
check_token(true,
|
||||
#coap_message{options = Options} = Msg,
|
||||
Msg,
|
||||
#channel{token = Token,
|
||||
clientinfo = ClientInfo} = Channel) ->
|
||||
clientinfo = ClientInfo,
|
||||
conn_state = CState} = Channel) ->
|
||||
#{clientid := ClientId} = ClientInfo,
|
||||
case maps:get(uri_query, Options, undefined) of
|
||||
case emqx_coap_message:get_option(uri_query, Msg) of
|
||||
#{<<"clientid">> := ClientId,
|
||||
<<"token">> := Token} ->
|
||||
call_session(handle_request, Msg, Channel);
|
||||
#{<<"clientid">> := DesireId} ->
|
||||
try_takeover(ClientId, DesireId, Msg, Channel);
|
||||
try_takeover(CState, DesireId, Msg, Channel);
|
||||
_ ->
|
||||
response({error, unauthorized}, Msg, Channel)
|
||||
Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg),
|
||||
{ok, {outgoing, Reply}, Msg}
|
||||
end;
|
||||
|
||||
check_token(false,
|
||||
#coap_message{options = Options} = Msg,
|
||||
Channel) ->
|
||||
case maps:get(uri_query, Options, undefined) of
|
||||
check_token(false, Msg, Channel) ->
|
||||
case emqx_coap_message:get_option(uri_query, Msg) of
|
||||
#{<<"clientid">> := _} ->
|
||||
response({error, unauthorized}, Msg, Channel);
|
||||
Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg),
|
||||
{ok, {outgoing, Reply}, Msg};
|
||||
#{<<"token">> := _} ->
|
||||
response({error, unauthorized}, Msg, Channel);
|
||||
Reply = emqx_coap_message:piggyback({error, unauthorized}, Msg),
|
||||
{ok, {outgoing, Reply}, Msg};
|
||||
_ ->
|
||||
call_session(handle_request, Msg, Channel)
|
||||
end.
|
||||
|
||||
response(Method, Req, Channel) ->
|
||||
response(Method, <<>>, Req, Channel).
|
||||
|
||||
response(Method, Payload, Req, Channel) ->
|
||||
Reply = emqx_coap_message:piggyback(Method, Payload, Req),
|
||||
call_session(handle_out, Reply, Channel).
|
||||
|
||||
try_takeover(undefined,
|
||||
DesireId,
|
||||
#coap_message{options = Opts} = Msg,
|
||||
Channel) ->
|
||||
case maps:get(uri_path, Opts, []) of
|
||||
[<<"mqtt">>, <<"connection">> | _] ->
|
||||
try_takeover(idle, DesireId, Msg, Channel) ->
|
||||
case emqx_coap_message:get_option(uri_path, Msg, []) of
|
||||
[<<"mqtt">>, <<"connection">> | _] ->
|
||||
%% may be is a connect request
|
||||
%% TODO need check repeat connect, unless we implement the
|
||||
%% udp connection baseon the clientid
|
||||
call_session(handle_request, Msg, Channel);
|
||||
_ ->
|
||||
do_takeover(DesireId, Msg, Channel)
|
||||
case emqx:get_config([gateway, coap, authentication], undefined) of
|
||||
undefined ->
|
||||
call_session(handle_request, Msg, Channel);
|
||||
_ ->
|
||||
do_takeover(DesireId, Msg, Channel)
|
||||
end
|
||||
end;
|
||||
|
||||
try_takeover(_, DesireId, Msg, Channel) ->
|
||||
|
@ -354,31 +275,7 @@ try_takeover(_, DesireId, Msg, Channel) ->
|
|||
do_takeover(_DesireId, Msg, Channel) ->
|
||||
%% TODO completed the takeover, now only reset the message
|
||||
Reset = emqx_coap_message:reset(Msg),
|
||||
call_session(handle_out, Reset, Channel).
|
||||
|
||||
new_exec_ctx(#channel{config = Cfg,
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo}) ->
|
||||
#exec_ctx{config = Cfg,
|
||||
ctx = Ctx,
|
||||
clientinfo = ClientInfo}.
|
||||
|
||||
do_connect(#coap_message{options = Opts} = Req, Channel) ->
|
||||
Queries = maps:get(uri_query, Opts),
|
||||
case emqx_misc:pipeline(
|
||||
[ fun run_conn_hooks/2
|
||||
, fun enrich_clientinfo/2
|
||||
, fun set_log_meta/2
|
||||
, fun auth_connect/2
|
||||
],
|
||||
{Queries, Req},
|
||||
Channel) of
|
||||
{ok, _Input, NChannel} ->
|
||||
process_connect(ensure_connected(NChannel), Req);
|
||||
{error, ReasonCode, NChannel} ->
|
||||
ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]),
|
||||
response({error, bad_request}, ErrMsg, Req, NChannel)
|
||||
end.
|
||||
{ok, {outgoing, Reset}, Channel}.
|
||||
|
||||
run_conn_hooks(Input, Channel = #channel{ctx = Ctx,
|
||||
conninfo = ConnInfo}) ->
|
||||
|
@ -439,11 +336,11 @@ ensure_connected(Channel = #channel{ctx = Ctx,
|
|||
ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]),
|
||||
Channel#channel{conninfo = NConnInfo}.
|
||||
|
||||
process_connect(Channel = #channel{ctx = Ctx,
|
||||
session = Session,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo},
|
||||
Msg) ->
|
||||
process_connect(#channel{ctx = Ctx,
|
||||
session = Session,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo} = Channel,
|
||||
Msg, Result, Iter) ->
|
||||
%% inherit the old session
|
||||
SessFun = fun(_,_) -> Session end,
|
||||
case emqx_gateway_ctx:open_session(
|
||||
|
@ -455,10 +352,14 @@ process_connect(Channel = #channel{ctx = Ctx,
|
|||
emqx_coap_session
|
||||
) of
|
||||
{ok, _Sess} ->
|
||||
response({ok, created}, <<"connected">>, Msg, Channel);
|
||||
RandVal = rand:uniform(?TOKEN_MAXIMUM),
|
||||
Token = erlang:list_to_binary(erlang:integer_to_list(RandVal)),
|
||||
iter(Iter,
|
||||
reply({ok, created}, Token, Msg, Result),
|
||||
Channel#channel{token = Token});
|
||||
{error, Reason} ->
|
||||
?LOG(error, "Failed to open session du to ~p", [Reason]),
|
||||
response({error, bad_request}, Msg, Channel)
|
||||
iter(Iter, reply({error, bad_request}, Msg, Result), Channel)
|
||||
end.
|
||||
|
||||
run_hooks(Ctx, Name, Args) ->
|
||||
|
@ -469,20 +370,93 @@ run_hooks(Ctx, Name, Args, Acc) ->
|
|||
emqx_gateway_ctx:metrics_inc(Ctx, Name),
|
||||
emqx_hooks:run_fold(Name, Args, Acc).
|
||||
|
||||
convert_queries(#coap_message{options = Opts} = Msg) ->
|
||||
case maps:get(uri_query, Opts, undefined) of
|
||||
undefined ->
|
||||
{ok, Msg#coap_message{options = Opts#{uri_query => #{}}}};
|
||||
Queries ->
|
||||
convert_queries(Queries, #{}, Msg)
|
||||
end.
|
||||
%%--------------------------------------------------------------------
|
||||
%% Call Chain
|
||||
%%--------------------------------------------------------------------
|
||||
call_session(Fun,
|
||||
Msg,
|
||||
#channel{session = Session} = Channel) ->
|
||||
iter([ session, fun process_session/4
|
||||
, proto, fun process_protocol/4
|
||||
, reply, fun process_reply/4
|
||||
, out, fun process_out/4
|
||||
, fun process_nothing/3
|
||||
],
|
||||
emqx_coap_session:Fun(Msg, Session),
|
||||
Channel).
|
||||
|
||||
convert_queries([H | T], Queries, Msg) ->
|
||||
case re:split(H, "=") of
|
||||
[Key, Val] ->
|
||||
convert_queries(T, Queries#{Key => Val}, Msg);
|
||||
_ ->
|
||||
error
|
||||
call_handler(request, Msg, Result,
|
||||
#channel{ctx = Ctx,
|
||||
clientinfo = ClientInfo} = Channel, Iter) ->
|
||||
HandlerResult =
|
||||
case emqx_coap_message:get_option(uri_path, Msg) of
|
||||
[<<"ps">> | RestPath] ->
|
||||
emqx_coap_pubsub_handler:handle_request(RestPath, Msg, Ctx, ClientInfo);
|
||||
[<<"mqtt">> | RestPath] ->
|
||||
emqx_coap_mqtt_handler:handle_request(RestPath, Msg, Ctx, ClientInfo);
|
||||
_ ->
|
||||
reply({error, bad_request}, Msg)
|
||||
end,
|
||||
iter([ connection, fun process_connection/4
|
||||
, subscribe, fun process_subscribe/4 | Iter],
|
||||
maps:merge(Result, HandlerResult),
|
||||
Channel);
|
||||
|
||||
call_handler(_, _, Result, Channel, Iter) ->
|
||||
iter(Iter, Result, Channel).
|
||||
|
||||
process_session(Session, Result, Channel, Iter) ->
|
||||
iter(Iter, Result, Channel#channel{session = Session}).
|
||||
|
||||
process_protocol({Type, Msg}, Result, Channel, Iter) ->
|
||||
call_handler(Type, Msg, Result, Channel, Iter).
|
||||
|
||||
%% leaf node
|
||||
process_out(Outs, Result, Channel, _) ->
|
||||
Outs2 = lists:reverse(Outs),
|
||||
Outs3 = case maps:get(reply, Result, undefined) of
|
||||
undefined ->
|
||||
Outs2;
|
||||
Reply ->
|
||||
[Reply | Outs2]
|
||||
end,
|
||||
{ok, {outgoing, Outs3}, Channel}.
|
||||
|
||||
%% leaf node
|
||||
process_nothing(_, _, Channel) ->
|
||||
{ok, Channel}.
|
||||
|
||||
process_connection({open, Req}, Result, Channel, Iter) ->
|
||||
Queries = emqx_coap_message:get_option(uri_query, Req),
|
||||
case emqx_misc:pipeline(
|
||||
[ fun run_conn_hooks/2
|
||||
, fun enrich_clientinfo/2
|
||||
, fun set_log_meta/2
|
||||
, fun auth_connect/2
|
||||
],
|
||||
{Queries, Req},
|
||||
Channel) of
|
||||
{ok, _Input, NChannel} ->
|
||||
process_connect(ensure_connected(NChannel), Req, Result, Iter);
|
||||
{error, ReasonCode, NChannel} ->
|
||||
ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]),
|
||||
Payload = erlang:list_to_binary(lists:flatten(ErrMsg)),
|
||||
iter(Iter,
|
||||
reply({error, bad_request}, Payload, Req, Result),
|
||||
NChannel)
|
||||
end;
|
||||
convert_queries([], Queries, #coap_message{options = Opts} = Msg) ->
|
||||
{ok, Msg#coap_message{options = Opts#{uri_query => Queries}}}.
|
||||
|
||||
process_connection({close, Msg}, _, Channel, _) ->
|
||||
Reply = emqx_coap_message:piggyback({ok, deleted}, Msg),
|
||||
{shutdown, close, Reply, Channel}.
|
||||
|
||||
process_subscribe({Sub, Msg}, Result, #channel{session = Session} = Channel, Iter) ->
|
||||
Result2 = emqx_coap_session:process_subscribe(Sub, Msg, Result, Session),
|
||||
iter([session, fun process_session/4 | Iter], Result2, Channel).
|
||||
|
||||
%% leaf node
|
||||
process_reply(Reply, Result, #channel{session = Session} = Channel, _) ->
|
||||
Session2 = emqx_coap_session:set_reply(Reply, Session),
|
||||
Outs = maps:get(out, Result, []),
|
||||
Outs2 = lists:reverse(Outs),
|
||||
{ok, {outgoing, [Reply | Outs2]}, Channel#channel{session = Session2}}.
|
||||
|
|
|
@ -103,11 +103,7 @@ flatten_options([{OptId, OptVal} | T], Acc) ->
|
|||
false ->
|
||||
[encode_option(OptId, OptVal) | Acc];
|
||||
_ ->
|
||||
lists:foldl(fun(undefined, InnerAcc) ->
|
||||
InnerAcc;
|
||||
(E, InnerAcc) ->
|
||||
[encode_option(OptId, E) | InnerAcc]
|
||||
end, Acc, OptVal)
|
||||
try_encode_repeatable(OptId, OptVal) ++ Acc
|
||||
end);
|
||||
|
||||
flatten_options([], Acc) ->
|
||||
|
@ -141,6 +137,19 @@ encode_option_list([], _LastNum, Acc, <<>>) ->
|
|||
encode_option_list([], _, Acc, Payload) ->
|
||||
<<Acc/binary, 16#FF, Payload/binary>>.
|
||||
|
||||
try_encode_repeatable(uri_query, Val) when is_map(Val) ->
|
||||
maps:fold(fun(K, V, Acc) ->
|
||||
[encode_option(uri_query, <<K/binary, $=, V/binary>>) | Acc]
|
||||
end,
|
||||
[], Val);
|
||||
|
||||
try_encode_repeatable(K, Val) ->
|
||||
lists:foldr(fun(undefined, Acc) ->
|
||||
Acc;
|
||||
(E, Acc) ->
|
||||
[encode_option(K, E) | Acc]
|
||||
end, [], Val).
|
||||
|
||||
%% RFC 7252
|
||||
encode_option(if_match, OptVal) -> {?OPTION_IF_MATCH, OptVal};
|
||||
encode_option(uri_host, OptVal) -> {?OPTION_URI_HOST, OptVal};
|
||||
|
@ -188,6 +197,8 @@ content_format_to_code(<<"application/octet-stream">>) -> 42;
|
|||
content_format_to_code(<<"application/exi">>) -> 47;
|
||||
content_format_to_code(<<"application/json">>) -> 50;
|
||||
content_format_to_code(<<"application/cbor">>) -> 60;
|
||||
content_format_to_code(<<"application/vnd.oma.lwm2m+tlv">>) -> 11542;
|
||||
content_format_to_code(<<"application/vnd.oma.lwm2m+json">>) -> 11543;
|
||||
content_format_to_code(_) -> 42. %% use octet-stream as default
|
||||
|
||||
method_to_class_code(get) -> {0, 01};
|
||||
|
@ -235,12 +246,7 @@ parse(<<?VERSION:2, Type:2, TKL:4, Class:3, Code:5, MsgId:16, Token:TKL/binary,
|
|||
ParseState) ->
|
||||
{Options, Payload} = decode_option_list(Tail),
|
||||
Options2 = maps:fold(fun(K, V, Acc) ->
|
||||
case is_repeatable_option(K) of
|
||||
true ->
|
||||
Acc#{K => lists:reverse(V)};
|
||||
_ ->
|
||||
Acc#{K => V}
|
||||
end
|
||||
Acc#{K => get_option_val(K, V)}
|
||||
end,
|
||||
#{},
|
||||
Options),
|
||||
|
@ -255,6 +261,24 @@ parse(<<?VERSION:2, Type:2, TKL:4, Class:3, Code:5, MsgId:16, Token:TKL/binary,
|
|||
<<>>,
|
||||
ParseState}.
|
||||
|
||||
get_option_val(uri_query, V) ->
|
||||
KVList = lists:foldl(fun(E, Acc) ->
|
||||
[Key, Val] = re:split(E, "="),
|
||||
[{Key, Val} | Acc]
|
||||
|
||||
end,
|
||||
[],
|
||||
V),
|
||||
maps:from_list(KVList);
|
||||
|
||||
get_option_val(K, V) ->
|
||||
case is_repeatable_option(K) of
|
||||
true ->
|
||||
lists:reverse(V);
|
||||
_ ->
|
||||
V
|
||||
end.
|
||||
|
||||
-spec decode_type(X) -> message_type()
|
||||
when X :: 0 .. 3.
|
||||
decode_type(0) -> con;
|
||||
|
@ -359,6 +383,8 @@ content_code_to_format(42) -> <<"application/octet-stream">>;
|
|||
content_code_to_format(47) -> <<"application/exi">>;
|
||||
content_code_to_format(50) -> <<"application/json">>;
|
||||
content_code_to_format(60) -> <<"application/cbor">>;
|
||||
content_code_to_format(11542) -> <<"application/vnd.oma.lwm2m+tlv">>;
|
||||
content_code_to_format(11543) -> <<"application/vnd.oma.lwm2m+json">>;
|
||||
content_code_to_format(_) -> <<"application/octet-stream">>. %% use octet as default
|
||||
|
||||
%% RFC 7252
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
%% Simplified semi-automatic CPS mode tree for coap
|
||||
%% The tree must have a terminal leaf node, and it's return is the result of the entire tree.
|
||||
%% This module currently only supports simple linear operation
|
||||
|
||||
-module(emqx_coap_medium).
|
||||
|
||||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
|
||||
%% API
|
||||
-export([ empty/0, reset/1, reset/2
|
||||
, out/1, out/2, proto_out/1
|
||||
, proto_out/2, iter/3, iter/4
|
||||
, reply/2, reply/3, reply/4]).
|
||||
|
||||
%%-type result() :: map() | empty.
|
||||
-define(DEFINE_DEF(Name), Name(Msg) -> Name(Msg, #{})).
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
empty() -> #{}.
|
||||
|
||||
?DEFINE_DEF(reset).
|
||||
|
||||
reset(Msg, Result) ->
|
||||
out(emqx_coap_message:reset(Msg), Result).
|
||||
|
||||
out(Msg) ->
|
||||
#{out => [Msg]}.
|
||||
|
||||
out(Msg, #{out := Outs} = Result) ->
|
||||
Result#{out := [Msg | Outs]};
|
||||
|
||||
out(Msg, Result) ->
|
||||
Result#{out => [Msg]}.
|
||||
|
||||
?DEFINE_DEF(proto_out).
|
||||
|
||||
proto_out(Proto, Resut) ->
|
||||
Resut#{proto => Proto}.
|
||||
|
||||
reply(Method, Req) when not is_record(Method, coap_message) ->
|
||||
reply(Method, <<>>, Req);
|
||||
|
||||
reply(Reply, Result) ->
|
||||
Result#{reply => Reply}.
|
||||
|
||||
reply(Method, Req, Result) when is_record(Req, coap_message) ->
|
||||
reply(Method, <<>>, Req, Result);
|
||||
|
||||
reply(Method, Payload, Req) ->
|
||||
reply(Method, Payload, Req, #{}).
|
||||
|
||||
reply(Method, Payload, Req, Result) ->
|
||||
Result#{reply => emqx_coap_message:piggyback(Method, Payload, Req)}.
|
||||
|
||||
%% run a tree
|
||||
iter([Key, Fun | T], Input, State) ->
|
||||
case maps:get(Key, Input, undefined) of
|
||||
undefined ->
|
||||
iter(T, Input, State);
|
||||
Val ->
|
||||
Fun(Val, maps:remove(Key, Input), State, T)
|
||||
%% reserved
|
||||
%% if is_function(Fun) ->
|
||||
%% Fun(Val, maps:remove(Key, Input), State, T);
|
||||
%% true ->
|
||||
%% %% switch to sub branch
|
||||
%% [FunH | FunT] = Fun,
|
||||
%% FunH(Val, maps:remove(Key, Input), State, FunT)
|
||||
%% end
|
||||
end;
|
||||
|
||||
%% terminal node
|
||||
iter([Fun], Input, State) ->
|
||||
Fun(undefined, Input, State).
|
||||
|
||||
%% run a tree with argument
|
||||
iter([Key, Fun | T], Input, Arg, State) ->
|
||||
case maps:get(Key, Input, undefined) of
|
||||
undefined ->
|
||||
iter(T, Input, Arg, State);
|
||||
Val ->
|
||||
Fun(Val, maps:remove(Key, Input), Arg, State, T)
|
||||
end;
|
||||
|
||||
iter([Fun], Input, Arg, State) ->
|
||||
Fun(undefined, Input, Arg, State).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
|
@ -31,7 +31,8 @@
|
|||
|
||||
-export([is_request/1]).
|
||||
|
||||
-export([set/3, set_payload/2, get_content/1, set_content/2, set_content/3, get_option/2]).
|
||||
-export([ set/3, set_payload/2, get_option/2
|
||||
, get_option/3, set_payload_block/3, set_payload_block/4]).
|
||||
|
||||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
|
||||
|
@ -42,11 +43,10 @@ request(Type, Method, Payload) ->
|
|||
request(Type, Method, Payload, []).
|
||||
|
||||
request(Type, Method, Payload, Options) when is_binary(Payload) ->
|
||||
#coap_message{type = Type, method = Method, payload = Payload, options = Options};
|
||||
|
||||
request(Type, Method, Content=#coap_content{}, Options) ->
|
||||
set_content(Content,
|
||||
#coap_message{type = Type, method = Method, options = Options}).
|
||||
#coap_message{type = Type,
|
||||
method = Method,
|
||||
payload = Payload,
|
||||
options = to_options(Options)}.
|
||||
|
||||
ack(#coap_message{id = Id}) ->
|
||||
#coap_message{type = ack, id = Id}.
|
||||
|
@ -55,20 +55,20 @@ reset(#coap_message{id = Id}) ->
|
|||
#coap_message{type = reset, id = Id}.
|
||||
|
||||
%% just make a response
|
||||
response(#coap_message{type = Type,
|
||||
id = Id,
|
||||
token = Token}) ->
|
||||
#coap_message{type = Type,
|
||||
id = Id,
|
||||
token = Token}.
|
||||
response(Request) ->
|
||||
response(undefined, Request).
|
||||
|
||||
response(Method, Request) ->
|
||||
set_method(Method, response(Request)).
|
||||
response(Method, <<>>, Request).
|
||||
|
||||
response(Method, Payload, Request) ->
|
||||
set_method(Method,
|
||||
set_payload(Payload,
|
||||
response(Request))).
|
||||
response(Method, Payload, #coap_message{type = Type,
|
||||
id = Id,
|
||||
token = Token}) ->
|
||||
#coap_message{type = Type,
|
||||
id = Id,
|
||||
token = Token,
|
||||
method = Method,
|
||||
payload = Payload}.
|
||||
|
||||
%% make a response which maybe is a piggyback ack
|
||||
piggyback(Method, Request) ->
|
||||
|
@ -90,14 +90,11 @@ set(max_age, ?DEFAULT_MAX_AGE, Msg) -> Msg;
|
|||
set(Option, Value, Msg = #coap_message{options = Options}) ->
|
||||
Msg#coap_message{options = Options#{Option => Value}}.
|
||||
|
||||
get_option(Option, #coap_message{options = Options}) ->
|
||||
maps:get(Option, Options, undefined).
|
||||
get_option(Option, Msg) ->
|
||||
get_option(Option, Msg, undefined).
|
||||
|
||||
set_method(Method, Msg) ->
|
||||
Msg#coap_message{method = Method}.
|
||||
|
||||
set_payload(Payload = #coap_content{}, Msg) ->
|
||||
set_content(Payload, undefined, Msg);
|
||||
get_option(Option, #coap_message{options = Options}, Def) ->
|
||||
maps:get(Option, Options, Def).
|
||||
|
||||
set_payload(Payload, Msg) when is_binary(Payload) ->
|
||||
Msg#coap_message{payload = Payload};
|
||||
|
@ -105,49 +102,6 @@ set_payload(Payload, Msg) when is_binary(Payload) ->
|
|||
set_payload(Payload, Msg) when is_list(Payload) ->
|
||||
Msg#coap_message{payload = list_to_binary(Payload)}.
|
||||
|
||||
get_content(#coap_message{options = Options, payload = Payload}) ->
|
||||
#coap_content{etag = maps:get(etag, Options, undefined),
|
||||
max_age = maps:get(max_age, Options, ?DEFAULT_MAX_AGE),
|
||||
format = maps:get(content_format, Options, undefined),
|
||||
location_path = maps:get(location_path, Options, []),
|
||||
payload = Payload}.
|
||||
|
||||
set_content(Content, Msg) ->
|
||||
set_content(Content, undefined, Msg).
|
||||
|
||||
%% segmentation not requested and not required
|
||||
set_content(#coap_content{etag = ETag,
|
||||
max_age = MaxAge,
|
||||
format = Format,
|
||||
location_path = LocPath,
|
||||
payload = Payload},
|
||||
undefined,
|
||||
Msg)
|
||||
when byte_size(Payload) =< ?MAX_BLOCK_SIZE ->
|
||||
#coap_message{options = Options} = Msg2 = set_payload(Payload, Msg),
|
||||
Options2 = Options#{etag => [ETag],
|
||||
max_age => MaxAge,
|
||||
content_format => Format,
|
||||
location_path => LocPath},
|
||||
Msg2#coap_message{options = Options2};
|
||||
|
||||
%% segmentation not requested, but required (late negotiation)
|
||||
set_content(Content, undefined, Msg) ->
|
||||
set_content(Content, {0, true, ?MAX_BLOCK_SIZE}, Msg);
|
||||
|
||||
%% segmentation requested (early negotiation)
|
||||
set_content(#coap_content{etag = ETag,
|
||||
max_age = MaxAge,
|
||||
format = Format,
|
||||
payload = Payload},
|
||||
Block,
|
||||
Msg) ->
|
||||
#coap_message{options = Options} = Msg2 = set_payload_block(Payload, Block, Msg),
|
||||
Options2 = Options#{etag => [ETag],
|
||||
max => MaxAge,
|
||||
content_format => Format},
|
||||
Msg2#coap_message{options = Options2}.
|
||||
|
||||
set_payload_block(Content, Block, Msg = #coap_message{method = Method}) when is_atom(Method) ->
|
||||
set_payload_block(Content, block1, Block, Msg);
|
||||
|
||||
|
@ -172,3 +126,8 @@ is_request(#coap_message{method = Method}) when is_atom(Method) ->
|
|||
|
||||
is_request(_) ->
|
||||
false.
|
||||
|
||||
to_options(Opts) when is_map(Opts) ->
|
||||
Opts;
|
||||
to_options(Opts) ->
|
||||
maps:from_list(Opts).
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_coap_resource).
|
||||
|
||||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
|
||||
-type context() :: any().
|
||||
-type topic() :: binary().
|
||||
-type token() :: token().
|
||||
|
||||
-type register() :: {topic(), token()}
|
||||
| topic()
|
||||
| undefined.
|
||||
|
||||
-type result() :: emqx_coap_message()
|
||||
| {has_sub, emqx_coap_message(), register()}.
|
||||
|
||||
-callback init(hocon:confg()) -> context().
|
||||
-callback stop(context()) -> ok.
|
||||
-callback get(emqx_coap_message(), hocon:config()) -> result().
|
||||
-callback put(emqx_coap_message(), hocon:config()) -> result().
|
||||
-callback post(emqx_coap_message(), hocon:config()) -> result().
|
||||
-callback delete(emqx_coap_message(), hocon:config()) -> result().
|
|
@ -21,24 +21,25 @@
|
|||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
|
||||
%% API
|
||||
-export([new/0, transfer_result/3]).
|
||||
-export([ new/0
|
||||
, process_subscribe/4]).
|
||||
|
||||
-export([ info/1
|
||||
, info/2
|
||||
, stats/1
|
||||
]).
|
||||
|
||||
-export([ handle_request/3
|
||||
, handle_response/3
|
||||
, handle_out/3
|
||||
, deliver/3
|
||||
, timeout/3]).
|
||||
-export([ handle_request/2
|
||||
, handle_response/2
|
||||
, handle_out/2
|
||||
, set_reply/2
|
||||
, deliver/2
|
||||
, timeout/2]).
|
||||
|
||||
-export_type([session/0]).
|
||||
|
||||
-record(session, { transport_manager :: emqx_coap_tm:manager()
|
||||
, observe_manager :: emqx_coap_observe_res:manager()
|
||||
, next_msg_id :: coap_message_id()
|
||||
, created_at :: pos_integer()
|
||||
}).
|
||||
|
||||
|
@ -64,6 +65,8 @@
|
|||
awaiting_rel_max
|
||||
]).
|
||||
|
||||
-import(emqx_coap_medium, [iter/3]).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% API
|
||||
%%%-------------------------------------------------------------------
|
||||
|
@ -72,7 +75,6 @@ new() ->
|
|||
_ = emqx_misc:rand_seed(),
|
||||
#session{ transport_manager = emqx_coap_tm:new()
|
||||
, observe_manager = emqx_coap_observe_res:new_manager()
|
||||
, next_msg_id = rand:uniform(?MAX_MESSAGE_ID)
|
||||
, created_at = erlang:system_time(millisecond)}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -110,8 +112,8 @@ info(mqueue_max, _) ->
|
|||
0;
|
||||
info(mqueue_dropped, _) ->
|
||||
0;
|
||||
info(next_pkt_id, #session{next_msg_id = PacketId}) ->
|
||||
PacketId;
|
||||
info(next_pkt_id, _) ->
|
||||
0;
|
||||
info(awaiting_rel, _) ->
|
||||
#{};
|
||||
info(awaiting_rel_cnt, _) ->
|
||||
|
@ -130,105 +132,87 @@ stats(Session) -> info(?STATS_KEYS, Session).
|
|||
%%%-------------------------------------------------------------------
|
||||
%%% Process Message
|
||||
%%%-------------------------------------------------------------------
|
||||
handle_request(Msg, Ctx, Session) ->
|
||||
handle_request(Msg, Session) ->
|
||||
call_transport_manager(?FUNCTION_NAME,
|
||||
Msg,
|
||||
Ctx,
|
||||
[fun process_tm/3, fun process_subscribe/3],
|
||||
Session).
|
||||
|
||||
handle_response(Msg, Ctx, Session) ->
|
||||
call_transport_manager(?FUNCTION_NAME, Msg, Ctx, [fun process_tm/3], Session).
|
||||
handle_response(Msg, Session) ->
|
||||
call_transport_manager(?FUNCTION_NAME, Msg, Session).
|
||||
|
||||
handle_out(Msg, Ctx, Session) ->
|
||||
call_transport_manager(?FUNCTION_NAME, Msg, Ctx, [fun process_tm/3], Session).
|
||||
handle_out(Msg, Session) ->
|
||||
call_transport_manager(?FUNCTION_NAME, Msg, Session).
|
||||
|
||||
deliver(Delivers, Ctx, Session) ->
|
||||
Fun = fun({_, Topic, Message},
|
||||
#{out := OutAcc,
|
||||
session := #session{observe_manager = OM,
|
||||
next_msg_id = MsgId,
|
||||
transport_manager = TM} = SAcc} = Acc) ->
|
||||
case emqx_coap_observe_res:res_changed(Topic, OM) of
|
||||
set_reply(Msg, #session{transport_manager = TM} = Session) ->
|
||||
TM2 = emqx_coap_tm:set_reply(Msg, TM),
|
||||
Session#session{transport_manager = TM2}.
|
||||
|
||||
deliver(Delivers, #session{observe_manager = OM,
|
||||
transport_manager = TM} = Session) ->
|
||||
Fun = fun({_, Topic, Message}, {OutAcc, OMAcc, TMAcc} = Acc) ->
|
||||
case emqx_coap_observe_res:res_changed(Topic, OMAcc) of
|
||||
undefined ->
|
||||
Acc;
|
||||
{Token, SeqId, OM2} ->
|
||||
Msg = mqtt_to_coap(Message, MsgId, Token, SeqId, Ctx),
|
||||
SAcc2 = SAcc#session{next_msg_id = next_msg_id(MsgId, TM),
|
||||
observe_manager = OM2},
|
||||
#{out := Out} = Result = handle_out(Msg, Ctx, SAcc2),
|
||||
Result#{out := [Out | OutAcc]}
|
||||
Msg = mqtt_to_coap(Message, Token, SeqId),
|
||||
#{out := Out, tm := TM2} = emqx_coap_tm:handle_out(Msg, TMAcc),
|
||||
{Out ++ OutAcc, OM2, TM2}
|
||||
end
|
||||
end,
|
||||
lists:foldl(Fun,
|
||||
#{out => [], session => Session},
|
||||
lists:reverse(Delivers)).
|
||||
{Outs, OM2, TM2} = lists:foldl(Fun, {[], OM, TM}, lists:reverse(Delivers)),
|
||||
|
||||
timeout(Timer, Ctx, Session) ->
|
||||
call_transport_manager(?FUNCTION_NAME, Timer, Ctx, [fun process_tm/3], Session).
|
||||
#{out => lists:reverse(Outs),
|
||||
session => Session#session{observe_manager = OM2,
|
||||
transport_manager = TM2}}.
|
||||
|
||||
result_keys() ->
|
||||
[tm, subscribe] ++ emqx_coap_channel:result_keys().
|
||||
|
||||
transfer_result(From, Value, Result) ->
|
||||
?TRANSFER_RESULT(From, Value, Result).
|
||||
timeout(Timer, Session) ->
|
||||
call_transport_manager(?FUNCTION_NAME, Timer, Session).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% Internal functions
|
||||
%%%-------------------------------------------------------------------
|
||||
call_transport_manager(Fun,
|
||||
Msg,
|
||||
Ctx,
|
||||
Processor,
|
||||
#session{transport_manager = TM} = Session) ->
|
||||
try
|
||||
Result = emqx_coap_tm:Fun(Msg, Ctx, TM),
|
||||
{ok, Result2, Session2} = pipeline(Processor,
|
||||
Result,
|
||||
Msg,
|
||||
Session),
|
||||
emqx_coap_channel:transfer_result(session, Session2, Result2)
|
||||
catch Type:Reason:Stack ->
|
||||
?ERROR("process transmission with, message:~p failed~nType:~p,Reason:~p~n,StackTrace:~p~n",
|
||||
[Msg, Type, Reason, Stack]),
|
||||
?REPLY({error, internal_server_error}, Msg)
|
||||
end.
|
||||
Result = emqx_coap_tm:Fun(Msg, TM),
|
||||
iter([tm, fun process_tm/4, fun process_session/3],
|
||||
Result,
|
||||
Session).
|
||||
|
||||
process_tm(#{tm := TM}, _, Session) ->
|
||||
{ok, Session#session{transport_manager = TM}};
|
||||
process_tm(_, _, Session) ->
|
||||
{ok, Session}.
|
||||
process_tm(TM, Result, Session, Cursor) ->
|
||||
iter(Cursor, Result, Session#session{transport_manager = TM}).
|
||||
|
||||
process_subscribe(#{subscribe := Sub} = Result,
|
||||
Msg,
|
||||
#session{observe_manager = OM} = Session) ->
|
||||
process_session(_, Result, Session) ->
|
||||
Result#{session => Session}.
|
||||
|
||||
process_subscribe(Sub, Msg, Result,
|
||||
#session{observe_manager = OM} = Session) ->
|
||||
case Sub of
|
||||
undefined ->
|
||||
{ok, Result, Session};
|
||||
Result;
|
||||
{Topic, Token} ->
|
||||
{SeqId, OM2} = emqx_coap_observe_res:insert(Topic, Token, OM),
|
||||
Replay = emqx_coap_message:piggyback({ok, content}, Msg),
|
||||
Replay2 = Replay#coap_message{options = #{observe => SeqId}},
|
||||
{ok, Result#{reply => Replay2}, Session#session{observe_manager = OM2}};
|
||||
Result#{reply => Replay2,
|
||||
session => Session#session{observe_manager = OM2}};
|
||||
Topic ->
|
||||
OM2 = emqx_coap_observe_res:remove(Topic, OM),
|
||||
Replay = emqx_coap_message:piggyback({ok, nocontent}, Msg),
|
||||
{ok, Result#{reply => Replay}, Session#session{observe_manager = OM2}}
|
||||
end;
|
||||
process_subscribe(Result, _, Session) ->
|
||||
{ok, Result, Session}.
|
||||
Result#{reply => Replay,
|
||||
session => Session#session{observe_manager = OM2}}
|
||||
end.
|
||||
|
||||
mqtt_to_coap(MQTT, MsgId, Token, SeqId, Ctx) ->
|
||||
mqtt_to_coap(MQTT, Token, SeqId) ->
|
||||
#message{payload = Payload} = MQTT,
|
||||
#coap_message{type = get_notify_type(MQTT, Ctx),
|
||||
#coap_message{type = get_notify_type(MQTT),
|
||||
method = {ok, content},
|
||||
id = MsgId,
|
||||
token = Token,
|
||||
payload = Payload,
|
||||
options = #{observe => SeqId}}.
|
||||
|
||||
get_notify_type(#message{qos = Qos}, Ctx) ->
|
||||
case emqx_coap_channel:get_config(notify_type, Ctx) of
|
||||
get_notify_type(#message{qos = Qos}) ->
|
||||
case emqx:get_config([gateway, coap, notify_qos], non) of
|
||||
qos ->
|
||||
case Qos of
|
||||
?QOS_0 ->
|
||||
|
@ -239,32 +223,3 @@ get_notify_type(#message{qos = Qos}, Ctx) ->
|
|||
Other ->
|
||||
Other
|
||||
end.
|
||||
|
||||
next_msg_id(MsgId, TM) ->
|
||||
next_msg_id(MsgId + 1, MsgId, TM).
|
||||
|
||||
next_msg_id(MsgId, MsgId, _) ->
|
||||
erlang:throw("too many message in delivering");
|
||||
next_msg_id(MsgId, BeginId, TM) when MsgId >= ?MAX_MESSAGE_ID ->
|
||||
check_is_inused(1, BeginId, TM);
|
||||
next_msg_id(MsgId, BeginId, TM) ->
|
||||
check_is_inused(MsgId, BeginId, TM).
|
||||
|
||||
check_is_inused(NewMsgId, BeginId, TM) ->
|
||||
case emqx_coap_tm:is_inused(out, NewMsgId, TM) of
|
||||
false ->
|
||||
NewMsgId;
|
||||
_ ->
|
||||
next_msg_id(NewMsgId + 1, BeginId, TM)
|
||||
end.
|
||||
|
||||
pipeline([Fun | T], Result, Msg, Session) ->
|
||||
case Fun(Result, Msg, Session) of
|
||||
{ok, Session2} ->
|
||||
pipeline(T, Result, Msg, Session2);
|
||||
{ok, Result2, Session2} ->
|
||||
pipeline(T, Result2, Msg, Session2)
|
||||
end;
|
||||
|
||||
pipeline([], Result, _, Session) ->
|
||||
{ok, Result, Session}.
|
||||
|
|
|
@ -18,11 +18,12 @@
|
|||
-module(emqx_coap_tm).
|
||||
|
||||
-export([ new/0
|
||||
, handle_request/3
|
||||
, handle_response/3
|
||||
, handle_request/2
|
||||
, handle_response/2
|
||||
, handle_out/2
|
||||
, handle_out/3
|
||||
, timeout/3
|
||||
, is_inused/3]).
|
||||
, set_reply/2
|
||||
, timeout/2]).
|
||||
|
||||
-export_type([manager/0, event_result/1]).
|
||||
|
||||
|
@ -30,17 +31,28 @@
|
|||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
|
||||
-type direction() :: in | out.
|
||||
-type state_machine_id() :: {direction(), non_neg_integer()}.
|
||||
|
||||
-record(state_machine, { id :: state_machine_id()
|
||||
-record(state_machine, { seq_id :: seq_id()
|
||||
, id :: state_machine_key()
|
||||
, token :: token() | undefined
|
||||
, observe :: 0 | 1 | undefined | observed
|
||||
, state :: atom()
|
||||
, timers :: maps:map()
|
||||
, transport :: emqx_coap_transport:transport()}).
|
||||
-type state_machine() :: #state_machine{}.
|
||||
|
||||
-type message_id() :: 0 .. ?MAX_MESSAGE_ID.
|
||||
-type token_key() :: {token, token()}.
|
||||
-type state_machine_key() :: {direction(), message_id()}.
|
||||
-type seq_id() :: pos_integer().
|
||||
-type manager_key() :: token_key() | state_machine_key() | seq_id().
|
||||
|
||||
-type manager() :: #{message_id() => state_machine()}.
|
||||
-type manager() :: #{ seq_id => seq_id()
|
||||
, next_msg_id => coap_message_id()
|
||||
, token_key() => seq_id()
|
||||
, state_machine_key() => seq_id()
|
||||
, seq_id() => state_machine()
|
||||
}.
|
||||
|
||||
-type ttimeout() :: {state_timeout, pos_integer(), any()}
|
||||
| {stop_timeout, pos_integer()}.
|
||||
|
@ -48,6 +60,7 @@
|
|||
-type topic() :: binary().
|
||||
-type token() :: binary().
|
||||
-type sub_register() :: {topic(), token()} | topic().
|
||||
|
||||
-type event_result(State) ::
|
||||
#{next => State,
|
||||
outgoing => emqx_coap_message(),
|
||||
|
@ -55,108 +68,161 @@
|
|||
has_sub => undefined | sub_register(),
|
||||
transport => emqx_coap_transport:transprot()}.
|
||||
|
||||
-define(TOKEN_ID(T), {token, T}).
|
||||
|
||||
-import(emqx_coap_medium, [empty/0, iter/4, reset/1, proto_out/2]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
new() ->
|
||||
#{}.
|
||||
#{ seq_id => 1
|
||||
, next_msg_id => rand:uniform(?MAX_MESSAGE_ID)
|
||||
}.
|
||||
|
||||
handle_request(#coap_message{id = MsgId} = Msg, Ctx, TM) ->
|
||||
%% client request
|
||||
handle_request(#coap_message{id = MsgId} = Msg, TM) ->
|
||||
Id = {in, MsgId},
|
||||
case maps:get(Id, TM, undefined) of
|
||||
case find_machine(Id, TM) of
|
||||
undefined ->
|
||||
Transport = emqx_coap_transport:new(),
|
||||
Machine = new_state_machine(Id, Transport),
|
||||
process_event(in, Msg, TM, Ctx, Machine);
|
||||
{Machine, TM2} = new_in_machine(Id, TM),
|
||||
process_event(in, Msg, TM2, Machine);
|
||||
Machine ->
|
||||
process_event(in, Msg, TM, Ctx, Machine)
|
||||
process_event(in, Msg, TM, Machine)
|
||||
end.
|
||||
|
||||
handle_response(#coap_message{type = Type, id = MsgId} = Msg, Ctx, TM) ->
|
||||
%% client response
|
||||
handle_response(#coap_message{type = Type, id = MsgId, token = Token} = Msg, TM) ->
|
||||
Id = {out, MsgId},
|
||||
case maps:get(Id, TM, undefined) of
|
||||
TokenId = ?TOKEN_ID(Token),
|
||||
case find_machine_by_keys([Id, TokenId], TM) of
|
||||
undefined ->
|
||||
case Type of
|
||||
reset ->
|
||||
?EMPTY_RESULT;
|
||||
empty();
|
||||
_ ->
|
||||
?RESET(Msg)
|
||||
reset(Msg)
|
||||
end;
|
||||
Machine ->
|
||||
process_event(in, Msg, TM, Ctx, Machine)
|
||||
process_event(in, Msg, TM, Machine)
|
||||
end.
|
||||
|
||||
handle_out(#coap_message{id = MsgId} = Msg, Ctx, TM) ->
|
||||
%% send to a client, msg can be request/piggyback/separate/notify
|
||||
handle_out(Msg, TM) ->
|
||||
handle_out(Msg, undefined, TM).
|
||||
|
||||
handle_out(#coap_message{token = Token} = MsgT, Ctx, TM) ->
|
||||
{MsgId, TM2} = alloc_message_id(TM),
|
||||
Msg = MsgT#coap_message{id = MsgId},
|
||||
Id = {out, MsgId},
|
||||
case maps:get(Id, TM, undefined) of
|
||||
TokenId = ?TOKEN_ID(Token),
|
||||
%% TODO why find by token ?
|
||||
case find_machine_by_keys([Id, TokenId], TM2) of
|
||||
undefined ->
|
||||
Transport = emqx_coap_transport:new(),
|
||||
Machine = new_state_machine(Id, Transport),
|
||||
process_event(out, Msg, TM, Ctx, Machine);
|
||||
{Machine, TM3} = new_out_machine(Id, Msg, TM),
|
||||
process_event(out, {Ctx, Msg}, TM3, Machine);
|
||||
_ ->
|
||||
%% ignore repeat send
|
||||
?EMPTY_RESULT
|
||||
empty()
|
||||
end.
|
||||
|
||||
timeout({Id, Type, Msg}, Ctx, TM) ->
|
||||
case maps:get(Id, TM, undefined) of
|
||||
set_reply(#coap_message{id = MsgId} = Msg, TM) ->
|
||||
Id = {in, MsgId},
|
||||
case find_machine(Id, TM) of
|
||||
undefined ->
|
||||
?EMPTY_RESULT;
|
||||
TM;
|
||||
#state_machine{transport = Transport,
|
||||
seq_id = SeqId} = Machine ->
|
||||
Transport2 = emqx_coap_transport:set_cache(Msg, Transport),
|
||||
Machine2 = Machine#state_machine{transport = Transport2},
|
||||
TM#{SeqId => Machine2}
|
||||
end.
|
||||
|
||||
timeout({SeqId, Type, Msg}, TM) ->
|
||||
case maps:get(SeqId, TM, undefined) of
|
||||
undefined ->
|
||||
empty();
|
||||
#state_machine{timers = Timers} = Machine ->
|
||||
%% maybe timer has been canceled
|
||||
case maps:is_key(Type, Timers) of
|
||||
true ->
|
||||
process_event(Type, Msg, TM, Ctx, Machine);
|
||||
process_event(Type, Msg, TM, Machine);
|
||||
_ ->
|
||||
?EMPTY_RESULT
|
||||
empty()
|
||||
end
|
||||
end.
|
||||
|
||||
-spec is_inused(direction(), message_id(), manager()) -> boolean().
|
||||
is_inused(Dir, Msg, Manager) ->
|
||||
maps:is_key({Dir, Msg}, Manager).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
new_state_machine(Id, Transport) ->
|
||||
#state_machine{id = Id,
|
||||
state = idle,
|
||||
timers = #{},
|
||||
transport = Transport}.
|
||||
process_event(stop_timeout, _, TM, Machine) ->
|
||||
process_manager(stop, #{}, Machine, TM);
|
||||
|
||||
process_event(stop_timeout,
|
||||
_,
|
||||
TM,
|
||||
_,
|
||||
#state_machine{id = Id,
|
||||
timers = Timers}) ->
|
||||
lists:foreach(fun({_, Ref}) ->
|
||||
emqx_misc:cancel_timer(Ref)
|
||||
end,
|
||||
maps:to_list(Timers)),
|
||||
#{tm => maps:remove(Id, TM)};
|
||||
process_event(Event, Msg, TM, #state_machine{state = State,
|
||||
transport = Transport} = Machine) ->
|
||||
Result = emqx_coap_transport:State(Event, Msg, Transport),
|
||||
iter([ proto, fun process_observe_response/5
|
||||
, next, fun process_state_change/5
|
||||
, transport, fun process_transport_change/5
|
||||
, timeouts, fun process_timeouts/5
|
||||
, fun process_manager/4],
|
||||
Result,
|
||||
Machine,
|
||||
TM).
|
||||
|
||||
process_event(Event,
|
||||
Msg,
|
||||
TM,
|
||||
Ctx,
|
||||
#state_machine{id = Id,
|
||||
state = State,
|
||||
transport = Transport} = Machine) ->
|
||||
Result = emqx_coap_transport:State(Event, Msg, Ctx, Transport),
|
||||
{ok, _, Machine2} = emqx_misc:pipeline([fun process_state_change/2,
|
||||
fun process_transport_change/2,
|
||||
fun process_timeouts/2],
|
||||
Result,
|
||||
Machine),
|
||||
TM2 = TM#{Id => Machine2},
|
||||
emqx_coap_session:transfer_result(tm, TM2, Result).
|
||||
process_observe_response({response, {_, Msg}} = Response,
|
||||
Result,
|
||||
#state_machine{observe = 0} = Machine,
|
||||
TM,
|
||||
Iter) ->
|
||||
Result2 = proto_out(Response, Result),
|
||||
case Msg#coap_message.method of
|
||||
{ok, _} ->
|
||||
iter(Iter,
|
||||
Result2#{next => observe},
|
||||
Machine#state_machine{observe = observed},
|
||||
TM);
|
||||
_ ->
|
||||
iter(Iter, Result2, Machine, TM)
|
||||
end;
|
||||
|
||||
process_state_change(#{next := Next}, Machine) ->
|
||||
{ok, cancel_state_timer(Machine#state_machine{state = Next})};
|
||||
process_state_change(_, Machine) ->
|
||||
{ok, Machine}.
|
||||
process_observe_response(Proto, Result, Machine, TM, Iter) ->
|
||||
iter(Iter, proto_out(Proto, Result), Machine, TM).
|
||||
|
||||
process_state_change(Next, Result, Machine, TM, Iter) ->
|
||||
case Next of
|
||||
stop ->
|
||||
process_manager(stop, Result, Machine, TM);
|
||||
_ ->
|
||||
iter(Iter,
|
||||
Result,
|
||||
cancel_state_timer(Machine#state_machine{state = Next}),
|
||||
TM)
|
||||
end.
|
||||
|
||||
process_transport_change(Transport, Result, Machine, TM, Iter) ->
|
||||
iter(Iter, Result, Machine#state_machine{transport = Transport}, TM).
|
||||
|
||||
process_timeouts([], Result, Machine, TM, Iter) ->
|
||||
iter(Iter, Result, Machine, TM);
|
||||
|
||||
process_timeouts(Timeouts, Result,
|
||||
#state_machine{seq_id = SeqId,
|
||||
timers = Timers} = Machine, TM, Iter) ->
|
||||
NewTimers = lists:foldl(fun({state_timeout, _, _} = Timer, Acc) ->
|
||||
process_timer(SeqId, Timer, Acc);
|
||||
({stop_timeout, I}, Acc) ->
|
||||
process_timer(SeqId, {stop_timeout, I, stop}, Acc)
|
||||
end,
|
||||
Timers,
|
||||
Timeouts),
|
||||
iter(Iter, Result, Machine#state_machine{timers = NewTimers}, TM).
|
||||
|
||||
process_manager(stop, Result, #state_machine{seq_id = SeqId}, TM) ->
|
||||
Result#{tm => delete_machine(SeqId, TM)};
|
||||
|
||||
process_manager(_, Result, #state_machine{seq_id = SeqId} = Machine2, TM) ->
|
||||
Result#{tm => TM#{SeqId => Machine2}}.
|
||||
|
||||
cancel_state_timer(#state_machine{timers = Timers} = Machine) ->
|
||||
case maps:get(state_timer, Timers, undefined) of
|
||||
|
@ -167,27 +233,118 @@ cancel_state_timer(#state_machine{timers = Timers} = Machine) ->
|
|||
Machine#state_machine{timers = maps:remove(state_timer, Timers)}
|
||||
end.
|
||||
|
||||
process_transport_change(#{transport := Transport}, Machine) ->
|
||||
{ok, Machine#state_machine{transport = Transport}};
|
||||
process_transport_change(_, Machine) ->
|
||||
{ok, Machine}.
|
||||
|
||||
process_timeouts(#{timeouts := []}, Machine) ->
|
||||
{ok, Machine};
|
||||
process_timeouts(#{timeouts := Timeouts},
|
||||
#state_machine{id = Id, timers = Timers} = Machine) ->
|
||||
NewTimers = lists:foldl(fun({state_timeout, _, _} = Timer, Acc) ->
|
||||
process_timer(Id, Timer, Acc);
|
||||
({stop_timeout, I}, Acc) ->
|
||||
process_timer(Id, {stop_timeout, I, stop}, Acc)
|
||||
end,
|
||||
Timers,
|
||||
Timeouts),
|
||||
{ok, Machine#state_machine{timers = NewTimers}};
|
||||
|
||||
process_timeouts(_, Machine) ->
|
||||
{ok, Machine}.
|
||||
|
||||
process_timer(Id, {Type, Interval, Msg}, Timers) ->
|
||||
Ref = emqx_misc:start_timer(Interval, {state_machine, {Id, Type, Msg}}),
|
||||
process_timer(SeqId, {Type, Interval, Msg}, Timers) ->
|
||||
Ref = emqx_misc:start_timer(Interval, {state_machine, {SeqId, Type, Msg}}),
|
||||
Timers#{Type => Ref}.
|
||||
|
||||
-spec delete_machine(manager_key(), manager()) -> manager().
|
||||
delete_machine(Id, Manager) ->
|
||||
case find_machine(Id, Manager) of
|
||||
undefined ->
|
||||
Manager;
|
||||
#state_machine{seq_id = SeqId,
|
||||
id = MachineId,
|
||||
token = Token,
|
||||
timers = Timers} ->
|
||||
lists:foreach(fun({_, Ref}) ->
|
||||
emqx_misc:cancel_timer(Ref)
|
||||
end,
|
||||
maps:to_list(Timers)),
|
||||
maps:without([SeqId, MachineId, ?TOKEN_ID(Token)], Manager)
|
||||
end.
|
||||
|
||||
-spec find_machine(manager_key(), manager()) -> state_machine() | undefined.
|
||||
find_machine({_, _} = Id, Manager) ->
|
||||
find_machine_by_seqid(maps:get(Id, Manager, undefined), Manager);
|
||||
find_machine(SeqId, Manager) ->
|
||||
find_machine_by_seqid(SeqId, Manager).
|
||||
|
||||
-spec find_machine_by_seqid(seq_id() | undefined, manager()) ->
|
||||
state_machine() | undefined.
|
||||
find_machine_by_seqid(SeqId, Manager) ->
|
||||
maps:get(SeqId, Manager, undefined).
|
||||
|
||||
-spec find_machine_by_keys(list(manager_key()),
|
||||
manager()) -> state_machine() | undefined.
|
||||
find_machine_by_keys([H | T], Manager) ->
|
||||
case H of
|
||||
?TOKEN_ID(<<>>) ->
|
||||
find_machine_by_keys(T, Manager);
|
||||
_ ->
|
||||
case find_machine(H, Manager) of
|
||||
undefined ->
|
||||
find_machine_by_keys(T, Manager);
|
||||
Machine ->
|
||||
Machine
|
||||
end
|
||||
end;
|
||||
find_machine_by_keys(_, _) ->
|
||||
undefined.
|
||||
|
||||
-spec new_in_machine(state_machine_key(), manager()) ->
|
||||
{state_machine(), manager()}.
|
||||
new_in_machine(MachineId, #{seq_id := SeqId} = Manager) ->
|
||||
Machine = #state_machine{ seq_id = SeqId
|
||||
, id = MachineId
|
||||
, state = idle
|
||||
, timers = #{}
|
||||
, transport = emqx_coap_transport:new()},
|
||||
{Machine, Manager#{seq_id := SeqId + 1,
|
||||
SeqId => Machine,
|
||||
MachineId => SeqId}}.
|
||||
|
||||
-spec new_out_machine(state_machine_key(), emqx_coap_message(), manager()) ->
|
||||
{state_machine(), manager()}.
|
||||
new_out_machine(MachineId,
|
||||
#coap_message{type = Type, token = Token, options = Opts},
|
||||
#{seq_id := SeqId} = Manager) ->
|
||||
Observe = maps:get(observe, Opts, undefined),
|
||||
Machine = #state_machine{ seq_id = SeqId
|
||||
, id = MachineId
|
||||
, token = Token
|
||||
, observe = Observe
|
||||
, state = idle
|
||||
, timers = #{}
|
||||
, transport = emqx_coap_transport:new()},
|
||||
|
||||
Manager2 = Manager#{seq_id := SeqId + 1,
|
||||
SeqId => Machine,
|
||||
MachineId => SeqId},
|
||||
{Machine,
|
||||
if Token =:= <<>> ->
|
||||
Manager2;
|
||||
Observe =:= 1 ->
|
||||
TokenId = ?TOKEN_ID(Token),
|
||||
delete_machine(TokenId, Manager2);
|
||||
Type =:= con orelse Observe =:= 0 ->
|
||||
TokenId = ?TOKEN_ID(Token),
|
||||
case maps:get(TokenId, Manager, undefined) of
|
||||
undefined ->
|
||||
Manager2#{TokenId => SeqId};
|
||||
_ ->
|
||||
throw("token conflict")
|
||||
end;
|
||||
true ->
|
||||
Manager2
|
||||
end
|
||||
}.
|
||||
|
||||
alloc_message_id(#{next_msg_id := MsgId} = TM) ->
|
||||
alloc_message_id(MsgId, TM).
|
||||
|
||||
alloc_message_id(MsgId, TM) ->
|
||||
Id = {out, MsgId},
|
||||
case maps:get(Id, TM, undefined) of
|
||||
undefined ->
|
||||
{MsgId, TM#{next_msg_id => next_message_id(MsgId)}};
|
||||
_ ->
|
||||
alloc_message_id(next_message_id(MsgId), TM)
|
||||
end.
|
||||
|
||||
next_message_id(MsgId) ->
|
||||
Next = MsgId + 1,
|
||||
if Next >= ?MAX_MESSAGE_ID ->
|
||||
1;
|
||||
true ->
|
||||
Next
|
||||
end.
|
||||
|
|
|
@ -9,19 +9,27 @@
|
|||
-define(EXCHANGE_LIFETIME, 247000).
|
||||
-define(NON_LIFETIME, 145000).
|
||||
|
||||
-type request_context() :: any().
|
||||
|
||||
-record(transport, { cache :: undefined | emqx_coap_message()
|
||||
, req_context :: request_context()
|
||||
, retry_interval :: non_neg_integer()
|
||||
, retry_count :: non_neg_integer()
|
||||
, observe :: non_neg_integer() | undefined
|
||||
}).
|
||||
|
||||
-type transport() :: #transport{}.
|
||||
|
||||
-export([ new/0, idle/4, maybe_reset/4
|
||||
, maybe_resend/4, wait_ack/4, until_stop/4]).
|
||||
-export([ new/0, idle/3, maybe_reset/3, set_cache/2
|
||||
, maybe_resend_4request/3, wait_ack/3, until_stop/3
|
||||
, observe/3, maybe_resend_4response/3]).
|
||||
|
||||
-export_type([transport/0]).
|
||||
|
||||
-import(emqx_coap_message, [reset/1]).
|
||||
-import(emqx_coap_medium, [ empty/0, reset/2, proto_out/2
|
||||
, out/1, out/2, proto_out/1
|
||||
, reply/2]).
|
||||
|
||||
-spec new() -> transport().
|
||||
new() ->
|
||||
|
@ -31,96 +39,152 @@ new() ->
|
|||
|
||||
idle(in,
|
||||
#coap_message{type = non, method = Method} = Msg,
|
||||
Ctx,
|
||||
_) ->
|
||||
Ret = #{next => until_stop,
|
||||
timeouts => [{stop_timeout, ?NON_LIFETIME}]},
|
||||
case Method of
|
||||
undefined ->
|
||||
?RESET(Msg);
|
||||
reset(Msg, #{next => stop});
|
||||
_ ->
|
||||
Result = call_handler(Msg, Ctx),
|
||||
maps:merge(Ret, Result)
|
||||
proto_out({request, Msg},
|
||||
#{next => until_stop,
|
||||
timeouts =>
|
||||
[{stop_timeout, ?NON_LIFETIME}]})
|
||||
end;
|
||||
|
||||
idle(in,
|
||||
#coap_message{type = con, method = Method} = Msg,
|
||||
Ctx,
|
||||
Transport) ->
|
||||
Ret = #{next => maybe_resend,
|
||||
timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]},
|
||||
_) ->
|
||||
case Method of
|
||||
undefined ->
|
||||
ResetMsg = reset(Msg),
|
||||
Ret#{transport => Transport#transport{cache = ResetMsg},
|
||||
out => ResetMsg};
|
||||
reset(Msg, #{next => stop});
|
||||
_ ->
|
||||
Result = call_handler(Msg, Ctx),
|
||||
maps:merge(Ret, Result)
|
||||
proto_out({request, Msg},
|
||||
#{next => maybe_resend_4request,
|
||||
timeouts =>[{stop_timeout, ?EXCHANGE_LIFETIME}]})
|
||||
end;
|
||||
|
||||
idle(out, #coap_message{type = non} = Msg, _, _) ->
|
||||
#{next => maybe_reset,
|
||||
out => Msg,
|
||||
timeouts => [{stop_timeout, ?NON_LIFETIME}]};
|
||||
idle(out, {Ctx, Msg}, Transport) ->
|
||||
idle(out, Msg, Transport#transport{req_context = Ctx});
|
||||
|
||||
idle(out, Msg, _, Transport) ->
|
||||
idle(out, #coap_message{type = non} = Msg, _) ->
|
||||
out(Msg, #{next => maybe_reset,
|
||||
timeouts => [{stop_timeout, ?NON_LIFETIME}]});
|
||||
|
||||
idle(out, Msg, Transport) ->
|
||||
_ = emqx_misc:rand_seed(),
|
||||
Timeout = ?ACK_TIMEOUT + rand:uniform(?ACK_RANDOM_FACTOR),
|
||||
#{next => wait_ack,
|
||||
transport => Transport#transport{cache = Msg},
|
||||
out => Msg,
|
||||
timeouts => [ {state_timeout, Timeout, ack_timeout}
|
||||
, {stop_timeout, ?EXCHANGE_LIFETIME}]}.
|
||||
out(Msg, #{next => wait_ack,
|
||||
transport => Transport#transport{cache = Msg},
|
||||
timeouts => [ {state_timeout, Timeout, ack_timeout}
|
||||
, {stop_timeout, ?EXCHANGE_LIFETIME}]}).
|
||||
|
||||
maybe_reset(in, Message, _, _) ->
|
||||
case Message of
|
||||
#coap_message{type = reset} ->
|
||||
?INFO("Reset Message:~p~n", [Message]);
|
||||
maybe_resend_4request(in, Msg, Transport) ->
|
||||
maybe_resend(Msg, true, Transport).
|
||||
|
||||
maybe_resend_4response(in, Msg, Transport) ->
|
||||
maybe_resend(Msg, false, Transport).
|
||||
|
||||
maybe_resend(Msg, IsExpecteReq, #transport{cache = Cache}) ->
|
||||
IsExpected = emqx_coap_message:is_request(Msg) =:= IsExpecteReq,
|
||||
case IsExpected of
|
||||
true ->
|
||||
case Cache of
|
||||
undefined ->
|
||||
%% handler in processing, ignore
|
||||
empty();
|
||||
_ ->
|
||||
out(Cache)
|
||||
end;
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
?EMPTY_RESULT.
|
||||
reset(Msg, #{next => stop})
|
||||
end.
|
||||
|
||||
maybe_resend(in, _, _, #transport{cache = Cache}) ->
|
||||
#{out => Cache}.
|
||||
maybe_reset(in, #coap_message{type = Type, method = Method} = Message,
|
||||
#transport{req_context = Ctx} = Transport) ->
|
||||
Ret = #{next => stop},
|
||||
CtxMsg = {Ctx, Message},
|
||||
if Type =:= reset ->
|
||||
proto_out({reset, CtxMsg}, Ret);
|
||||
is_tuple(Method) ->
|
||||
on_response(Message,
|
||||
Transport,
|
||||
if Type =:= non -> until_stop;
|
||||
true -> maybe_resend_4response
|
||||
end);
|
||||
true ->
|
||||
reset(Message, Ret)
|
||||
end.
|
||||
|
||||
wait_ack(in, #coap_message{type = Type}, _, _) ->
|
||||
wait_ack(in, #coap_message{type = Type, method = Method} = Msg, #transport{req_context = Ctx}) ->
|
||||
CtxMsg = {Ctx, Msg},
|
||||
case Type of
|
||||
ack ->
|
||||
#{next => until_stop};
|
||||
reset ->
|
||||
#{next => until_stop};
|
||||
proto_out({reset, CtxMsg}, #{next => stop});
|
||||
_ ->
|
||||
?EMPTY_RESULT
|
||||
case Method of
|
||||
undefined ->
|
||||
%% empty ack, keep transport to recv response
|
||||
proto_out({ack, CtxMsg});
|
||||
{_, _} ->
|
||||
%% ack with payload
|
||||
proto_out({response, CtxMsg}, #{next => stop});
|
||||
_ ->
|
||||
reset(Msg, #{next => stop})
|
||||
end
|
||||
end;
|
||||
|
||||
wait_ack(state_timeout,
|
||||
ack_timeout,
|
||||
_,
|
||||
#transport{cache = Msg,
|
||||
retry_interval = Timeout,
|
||||
retry_count = Count} =Transport) ->
|
||||
case Count < ?MAX_RETRANSMIT of
|
||||
true ->
|
||||
Timeout2 = Timeout * 2,
|
||||
#{transport => Transport#transport{retry_interval = Timeout2,
|
||||
retry_count = Count + 1},
|
||||
out => Msg,
|
||||
timeouts => [{state_timeout, Timeout2, ack_timeout}]};
|
||||
out(Msg,
|
||||
#{transport => Transport#transport{retry_interval = Timeout2,
|
||||
retry_count = Count + 1},
|
||||
timeouts => [{state_timeout, Timeout2, ack_timeout}]});
|
||||
_ ->
|
||||
#{next_state => until_stop}
|
||||
proto_out({ack_failure, Msg}, #{next_state => stop})
|
||||
end.
|
||||
|
||||
until_stop(_, _, _, _) ->
|
||||
?EMPTY_RESULT.
|
||||
|
||||
call_handler(#coap_message{options = Opts} = Msg, Ctx) ->
|
||||
case maps:get(uri_path, Opts, undefined) of
|
||||
[<<"ps">> | RestPath] ->
|
||||
emqx_coap_pubsub_handler:handle_request(RestPath, Msg, Ctx);
|
||||
[<<"mqtt">> | RestPath] ->
|
||||
emqx_coap_mqtt_handler:handle_request(RestPath, Msg, Ctx);
|
||||
observe(in,
|
||||
#coap_message{method = Method} = Message,
|
||||
#transport{observe = Observe} = Transport) ->
|
||||
case Method of
|
||||
{ok, _} ->
|
||||
case emqx_coap_message:get_option(observe, Message, Observe) of
|
||||
Observe ->
|
||||
%% repeatd notify, ignore
|
||||
empty();
|
||||
NewObserve ->
|
||||
on_response(Message,
|
||||
Transport#transport{observe = NewObserve},
|
||||
?FUNCTION_NAME)
|
||||
end;
|
||||
{error, _} ->
|
||||
#{next => stop};
|
||||
_ ->
|
||||
?REPLY({error, bad_request}, Msg)
|
||||
reset(Message)
|
||||
end.
|
||||
|
||||
until_stop(_, _, _) ->
|
||||
empty().
|
||||
|
||||
set_cache(Cache, Transport) ->
|
||||
Transport#transport{cache = Cache}.
|
||||
|
||||
on_response(#coap_message{type = Type} = Message,
|
||||
#transport{req_context = Ctx} = Transport,
|
||||
NextState) ->
|
||||
CtxMsg = {Ctx, Message},
|
||||
if Type =:= non ->
|
||||
proto_out({response, CtxMsg}, #{next => NextState});
|
||||
Type =:= con ->
|
||||
Ack = emqx_coap_message:ack(Message),
|
||||
proto_out({response, CtxMsg},
|
||||
out(Ack, #{next => NextState,
|
||||
transport => Transport#transport{cache = Ack}}));
|
||||
true ->
|
||||
reset(Message)
|
||||
end.
|
||||
|
|
|
@ -18,23 +18,24 @@
|
|||
|
||||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
|
||||
-export([handle_request/3]).
|
||||
-export([handle_request/4]).
|
||||
-import(emqx_coap_message, [response/2, response/3]).
|
||||
-import(emqx_coap_medium, [reply/2]).
|
||||
|
||||
handle_request([<<"connection">>], #coap_message{method = Method} = Msg, _) ->
|
||||
handle_request([<<"connection">>], #coap_message{method = Method} = Msg, _Ctx, _CInfo) ->
|
||||
handle_method(Method, Msg);
|
||||
|
||||
handle_request(_, Msg, _) ->
|
||||
?REPLY({error, bad_request}, Msg).
|
||||
handle_request(_, Msg, _, _) ->
|
||||
reply({error, bad_request}, Msg).
|
||||
|
||||
handle_method(put, Msg) ->
|
||||
?REPLY({ok, changed}, Msg);
|
||||
reply({ok, changed}, Msg);
|
||||
|
||||
handle_method(post, _) ->
|
||||
#{connection => open};
|
||||
handle_method(post, Msg) ->
|
||||
#{connection => {open, Msg}};
|
||||
|
||||
handle_method(delete, _) ->
|
||||
#{connection => close};
|
||||
handle_method(delete, Msg) ->
|
||||
#{connection => {close, Msg}};
|
||||
|
||||
handle_method(_, Msg) ->
|
||||
?REPLY({error, method_not_allowed}, Msg).
|
||||
reply({error, method_not_allowed}, Msg).
|
||||
|
|
|
@ -20,48 +20,48 @@
|
|||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
|
||||
-export([handle_request/3]).
|
||||
-export([handle_request/4]).
|
||||
|
||||
-import(emqx_coap_message, [response/2, response/3]).
|
||||
-import(emqx_coap_medium, [reply/2, reply/3]).
|
||||
|
||||
-define(UNSUB(Topic), #{subscribe => Topic}).
|
||||
-define(SUB(Topic, Token), #{subscribe => {Topic, Token}}).
|
||||
-define(UNSUB(Topic, Msg), #{subscribe => {Topic, Msg}}).
|
||||
-define(SUB(Topic, Token, Msg), #{subscribe => {{Topic, Token}, Msg}}).
|
||||
-define(SUBOPTS, #{qos => 0, rh => 0, rap => 0, nl => 0, is_new => false}).
|
||||
|
||||
handle_request(Path, #coap_message{method = Method} = Msg, Ctx) ->
|
||||
handle_request(Path, #coap_message{method = Method} = Msg, Ctx, CInfo) ->
|
||||
case check_topic(Path) of
|
||||
{ok, Topic} ->
|
||||
handle_method(Method, Topic, Msg, Ctx);
|
||||
handle_method(Method, Topic, Msg, Ctx, CInfo);
|
||||
_ ->
|
||||
?REPLY({error, bad_request}, <<"invalid topic">>, Msg)
|
||||
reply({error, bad_request}, <<"invalid topic">>, Msg)
|
||||
end.
|
||||
|
||||
handle_method(get, Topic, #coap_message{options = Opts} = Msg, Ctx) ->
|
||||
case maps:get(observe, Opts, undefined) of
|
||||
handle_method(get, Topic, Msg, Ctx, CInfo) ->
|
||||
case emqx_coap_message:get_option(observe, Msg) of
|
||||
0 ->
|
||||
subscribe(Msg, Topic, Ctx);
|
||||
subscribe(Msg, Topic, Ctx, CInfo);
|
||||
1 ->
|
||||
unsubscribe(Topic, Ctx);
|
||||
unsubscribe(Msg, Topic, CInfo);
|
||||
_ ->
|
||||
?REPLY({error, bad_request}, <<"invalid observe value">>, Msg)
|
||||
reply({error, bad_request}, <<"invalid observe value">>, Msg)
|
||||
end;
|
||||
|
||||
handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx) ->
|
||||
case emqx_coap_channel:validator(publish, Topic, Ctx) of
|
||||
handle_method(post, Topic, #coap_message{payload = Payload} = Msg, Ctx, CInfo) ->
|
||||
case emqx_coap_channel:validator(publish, Topic, Ctx, CInfo) of
|
||||
allow ->
|
||||
ClientInfo = emqx_coap_channel:get_clientinfo(Ctx),
|
||||
#{clientid := ClientId} = ClientInfo,
|
||||
QOS = get_publish_qos(Msg, Ctx),
|
||||
#{clientid := ClientId} = CInfo,
|
||||
QOS = get_publish_qos(Msg),
|
||||
MQTTMsg = emqx_message:make(ClientId, QOS, Topic, Payload),
|
||||
MQTTMsg2 = apply_publish_opts(Msg, MQTTMsg),
|
||||
_ = emqx_broker:publish(MQTTMsg2),
|
||||
?REPLY({ok, changed}, Msg);
|
||||
reply({ok, changed}, Msg);
|
||||
_ ->
|
||||
?REPLY({error, unauthorized}, Msg)
|
||||
reply({error, unauthorized}, Msg)
|
||||
end;
|
||||
|
||||
handle_method(_, _, Msg, _) ->
|
||||
?REPLY({error, method_not_allowed}, Msg).
|
||||
handle_method(_, _, Msg, _, _) ->
|
||||
reply({error, method_not_allowed}, Msg).
|
||||
|
||||
check_topic([]) ->
|
||||
error;
|
||||
|
@ -76,13 +76,13 @@ check_topic(Path) ->
|
|||
<<>>,
|
||||
Path))}.
|
||||
|
||||
get_sub_opts(#coap_message{options = Opts} = Msg, Ctx) ->
|
||||
get_sub_opts(#coap_message{options = Opts} = Msg) ->
|
||||
SubOpts = maps:fold(fun parse_sub_opts/3, #{}, Opts),
|
||||
case SubOpts of
|
||||
#{qos := _} ->
|
||||
maps:merge(SubOpts, ?SUBOPTS);
|
||||
_ ->
|
||||
CfgType = emqx_coap_channel:get_config(subscribe_qos, Ctx),
|
||||
CfgType = emqx:get_config([gateway, coap, subscribe_qos], ?QOS_0),
|
||||
maps:merge(SubOpts, ?SUBOPTS#{qos => type_to_qos(CfgType, Msg)})
|
||||
end.
|
||||
|
||||
|
@ -106,16 +106,16 @@ type_to_qos(coap, #coap_message{type = Type}) ->
|
|||
?QOS_1
|
||||
end.
|
||||
|
||||
get_publish_qos(#coap_message{options = Opts} = Msg, Ctx) ->
|
||||
case maps:get(uri_query, Opts) of
|
||||
get_publish_qos(Msg) ->
|
||||
case emqx_coap_message:get_option(uri_query, Msg) of
|
||||
#{<<"qos">> := QOS} ->
|
||||
erlang:binary_to_integer(QOS);
|
||||
_ ->
|
||||
CfgType = emqx_coap_channel:get_config(publish_qos, Ctx),
|
||||
CfgType = emqx:get_config([gateway, coap, publish_qos], ?QOS_0),
|
||||
type_to_qos(CfgType, Msg)
|
||||
end.
|
||||
|
||||
apply_publish_opts(#coap_message{options = Opts}, MQTTMsg) ->
|
||||
apply_publish_opts(Msg, MQTTMsg) ->
|
||||
maps:fold(fun(<<"retain">>, V, Acc) ->
|
||||
Val = erlang:binary_to_atom(V),
|
||||
emqx_message:set_flag(retain, Val, Acc);
|
||||
|
@ -129,27 +129,25 @@ apply_publish_opts(#coap_message{options = Opts}, MQTTMsg) ->
|
|||
Acc
|
||||
end,
|
||||
MQTTMsg,
|
||||
maps:get(uri_query, Opts)).
|
||||
emqx_coap_message:get_option(uri_query, Msg)).
|
||||
|
||||
subscribe(#coap_message{token = <<>>} = Msg, _, _) ->
|
||||
?REPLY({error, bad_request}, <<"observe without token">>, Msg);
|
||||
subscribe(#coap_message{token = <<>>} = Msg, _, _, _) ->
|
||||
reply({error, bad_request}, <<"observe without token">>, Msg);
|
||||
|
||||
subscribe(#coap_message{token = Token} = Msg, Topic, Ctx) ->
|
||||
case emqx_coap_channel:validator(subscribe, Topic, Ctx) of
|
||||
subscribe(#coap_message{token = Token} = Msg, Topic, Ctx, CInfo) ->
|
||||
case emqx_coap_channel:validator(subscribe, Topic, Ctx, CInfo) of
|
||||
allow ->
|
||||
ClientInfo = emqx_coap_channel:get_clientinfo(Ctx),
|
||||
#{clientid := ClientId} = ClientInfo,
|
||||
SubOpts = get_sub_opts(Msg, Ctx),
|
||||
#{clientid := ClientId} = CInfo,
|
||||
SubOpts = get_sub_opts(Msg),
|
||||
emqx_broker:subscribe(Topic, ClientId, SubOpts),
|
||||
emqx_hooks:run('session.subscribed',
|
||||
[ClientInfo, Topic, SubOpts]),
|
||||
?SUB(Topic, Token);
|
||||
[CInfo, Topic, SubOpts]),
|
||||
?SUB(Topic, Token, Msg);
|
||||
_ ->
|
||||
?REPLY({error, unauthorized}, Msg)
|
||||
reply({error, unauthorized}, Msg)
|
||||
end.
|
||||
|
||||
unsubscribe(Topic, Ctx) ->
|
||||
ClientInfo = emqx_coap_channel:get_clientinfo(Ctx),
|
||||
unsubscribe(Msg, Topic, CInfo) ->
|
||||
emqx_broker:unsubscribe(Topic),
|
||||
emqx_hooks:run('session.unsubscribed', [ClientInfo, Topic, ?SUBOPTS]),
|
||||
?UNSUB(Topic).
|
||||
emqx_hooks:run('session.unsubscribed', [CInfo, Topic, ?SUBOPTS]),
|
||||
?UNSUB(Topic, Msg).
|
||||
|
|
|
@ -22,18 +22,6 @@
|
|||
-define(DEFAULT_MAX_AGE, 60).
|
||||
-define(MAXIMUM_MAX_AGE, 4294967295).
|
||||
|
||||
-define(EMPTY_RESULT, #{}).
|
||||
-define(TRANSFER_RESULT(From, Value, R1),
|
||||
begin
|
||||
Keys = result_keys(),
|
||||
R2 = maps:with(Keys, R1),
|
||||
R2#{From => Value}
|
||||
end).
|
||||
|
||||
-define(RESET(Msg), #{out => emqx_coap_message:reset(Msg)}).
|
||||
-define(REPLY(Resp, Payload, Msg), #{out => emqx_coap_message:piggyback(Resp, Payload, Msg)}).
|
||||
-define(REPLY(Resp, Msg), ?REPLY(Resp, <<>>, Msg)).
|
||||
|
||||
-type coap_message_id() :: 1 .. ?MAX_MESSAGE_ID.
|
||||
-type message_type() :: con | non | ack | reset.
|
||||
-type max_age() :: 1 .. ?MAXIMUM_MAX_AGE.
|
||||
|
@ -66,7 +54,7 @@
|
|||
, uri_path => list(binary())
|
||||
, content_format => 0 .. 65535
|
||||
, max_age => non_neg_integer()
|
||||
, uri_query => list(binary())
|
||||
, uri_query => list(binary()) | map()
|
||||
, 'accept' => 0 .. 65535
|
||||
, location_query => list(binary())
|
||||
, proxy_uri => binary()
|
||||
|
@ -85,7 +73,4 @@
|
|||
, options = #{}
|
||||
, payload = <<>>}).
|
||||
|
||||
-record(coap_content, {etag, max_age = ?DEFAULT_MAX_AGE, format, location_path = [], payload = <<>>}).
|
||||
|
||||
-type emqx_coap_message() :: #coap_message{}.
|
||||
-type coap_content() :: #coap_content{}.
|
||||
|
|
|
@ -94,6 +94,7 @@ fields(lwm2m_structs) ->
|
|||
, {lifetime_max, t(duration())}
|
||||
, {qmode_time_windonw, t(integer())}
|
||||
, {auto_observe, t(boolean())}
|
||||
, {mountpoint, t(string())}
|
||||
, {update_msg_publish_condition, t(union([always, contains_object_list]))}
|
||||
, {translators, t(ref(translators))}
|
||||
, {listeners, t(ref(udp_listener_group))}
|
||||
|
@ -122,7 +123,17 @@ fields(clientinfo_override) ->
|
|||
];
|
||||
|
||||
fields(translators) ->
|
||||
[{"$name", t(binary())}];
|
||||
[ {command, t(ref(translator))}
|
||||
, {response, t(ref(translator))}
|
||||
, {notify, t(ref(translator))}
|
||||
, {register, t(ref(translator))}
|
||||
, {update, t(ref(translator))}
|
||||
];
|
||||
|
||||
fields(translator) ->
|
||||
[ {topic, t(binary())}
|
||||
, {qos, t(range(0, 2))}
|
||||
];
|
||||
|
||||
fields(udp_listener_group) ->
|
||||
[ {udp, t(ref(udp_listener))}
|
||||
|
@ -160,7 +171,7 @@ fields(listener_settings) ->
|
|||
, {max_connections, t(integer(), undefined, 1024)}
|
||||
, {max_conn_rate, t(integer())}
|
||||
, {active_n, t(integer(), undefined, 100)}
|
||||
%, {rate_limit, t(comma_separated_list())}
|
||||
%, {rate_limit, t(comma_separated_list())}
|
||||
, {access, t(ref(access))}
|
||||
, {proxy_protocol, t(boolean())}
|
||||
, {proxy_protocol_timeout, t(duration())}
|
||||
|
@ -183,24 +194,24 @@ fields(tcp_listener_settings) ->
|
|||
|
||||
fields(ssl_listener_settings) ->
|
||||
[
|
||||
%% some special confs for ssl listener
|
||||
%% some special confs for ssl listener
|
||||
] ++
|
||||
ssl(undefined, #{handshake_timeout => <<"15s">>
|
||||
, depth => 10
|
||||
, reuse_sessions => true}) ++ fields(listener_settings);
|
||||
ssl(undefined, #{handshake_timeout => <<"15s">>
|
||||
, depth => 10
|
||||
, reuse_sessions => true}) ++ fields(listener_settings);
|
||||
|
||||
fields(udp_listener_settings) ->
|
||||
[
|
||||
%% some special confs for udp listener
|
||||
%% some special confs for udp listener
|
||||
] ++ fields(listener_settings);
|
||||
|
||||
fields(dtls_listener_settings) ->
|
||||
[
|
||||
%% some special confs for dtls listener
|
||||
%% some special confs for dtls listener
|
||||
] ++
|
||||
ssl(undefined, #{handshake_timeout => <<"15s">>
|
||||
, depth => 10
|
||||
, reuse_sessions => true}) ++ fields(listener_settings);
|
||||
ssl(undefined, #{handshake_timeout => <<"15s">>
|
||||
, depth => 10
|
||||
, reuse_sessions => true}) ++ fields(listener_settings);
|
||||
|
||||
fields(access) ->
|
||||
[ {"$id", #{type => binary(),
|
||||
|
@ -270,7 +281,7 @@ ref(Field) ->
|
|||
%% ...
|
||||
ssl(Mapping, Defaults) ->
|
||||
M = fun (Field) ->
|
||||
case (Mapping) of
|
||||
case (Mapping) of
|
||||
undefined -> undefined;
|
||||
_ -> Mapping ++ "." ++ Field
|
||||
end end,
|
||||
|
|
|
@ -55,7 +55,8 @@ list(#{node := Node }, Params) ->
|
|||
end;
|
||||
|
||||
list(#{}, _Params) ->
|
||||
Channels = emqx_lwm2m_cm:all_channels(),
|
||||
%% Channels = emqx_lwm2m_cm:all_channels(),
|
||||
Channels = [],
|
||||
return({ok, format(Channels)}).
|
||||
|
||||
lookup_cmd(#{ep := Ep, node := Node}, Params) ->
|
||||
|
@ -64,26 +65,27 @@ lookup_cmd(#{ep := Ep, node := Node}, Params) ->
|
|||
_ -> rpc_call(Node, lookup_cmd, [#{ep => Ep}, Params])
|
||||
end;
|
||||
|
||||
lookup_cmd(#{ep := Ep}, Params) ->
|
||||
MsgType = proplists:get_value(<<"msgType">>, Params),
|
||||
Path0 = proplists:get_value(<<"path">>, Params),
|
||||
case emqx_lwm2m_cm:lookup_cmd(Ep, Path0, MsgType) of
|
||||
[] -> return({ok, []});
|
||||
[{_, undefined} | _] -> return({ok, []});
|
||||
[{{IMEI, Path, MsgType}, undefined}] ->
|
||||
return({ok, [{imei, IMEI},
|
||||
{'msgType', IMEI},
|
||||
{'code', <<"6.01">>},
|
||||
{'codeMsg', <<"reply_not_received">>},
|
||||
{'path', Path}]});
|
||||
[{{IMEI, Path, MsgType}, {Code, CodeMsg, Content}}] ->
|
||||
Payload1 = format_cmd_content(Content, MsgType),
|
||||
return({ok, [{imei, IMEI},
|
||||
{'msgType', IMEI},
|
||||
{'code', Code},
|
||||
{'codeMsg', CodeMsg},
|
||||
{'path', Path}] ++ Payload1})
|
||||
end.
|
||||
lookup_cmd(#{ep := _Ep}, Params) ->
|
||||
_MsgType = proplists:get_value(<<"msgType">>, Params),
|
||||
_Path0 = proplists:get_value(<<"path">>, Params),
|
||||
%% case emqx_lwm2m_cm:lookup_cmd(Ep, Path0, MsgType) of
|
||||
%% [] -> return({ok, []});
|
||||
%% [{_, undefined} | _] -> return({ok, []});
|
||||
%% [{{IMEI, Path, MsgType}, undefined}] ->
|
||||
%% return({ok, [{imei, IMEI},
|
||||
%% {'msgType', IMEI},
|
||||
%% {'code', <<"6.01">>},
|
||||
%% {'codeMsg', <<"reply_not_received">>},
|
||||
%% {'path', Path}]});
|
||||
%% [{{IMEI, Path, MsgType}, {Code, CodeMsg, Content}}] ->
|
||||
%% Payload1 = format_cmd_content(Content, MsgType),
|
||||
%% return({ok, [{imei, IMEI},
|
||||
%% {'msgType', IMEI},
|
||||
%% {'code', Code},
|
||||
%% {'codeMsg', CodeMsg},
|
||||
%% {'path', Path}] ++ Payload1})
|
||||
%% end.
|
||||
return({ok, []}).
|
||||
|
||||
rpc_call(Node, Fun, Args) ->
|
||||
case rpc:call(Node, ?MODULE, Fun, Args) of
|
||||
|
@ -115,36 +117,37 @@ format(Channels) ->
|
|||
{'objectList', ObjectList}]
|
||||
end, Channels).
|
||||
|
||||
format_cmd_content(undefined, _MsgType) -> [];
|
||||
format_cmd_content(Content, <<"discover">>) ->
|
||||
[H | Content1] = Content,
|
||||
{_, [HObjId]} = emqx_lwm2m_coap_resource:parse_object_list(H),
|
||||
[ObjId | _]= path_list(HObjId),
|
||||
ObjectList = case Content1 of
|
||||
[Content2 | _] ->
|
||||
{_, ObjL} = emqx_lwm2m_coap_resource:parse_object_list(Content2),
|
||||
ObjL;
|
||||
[] -> []
|
||||
end,
|
||||
R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of
|
||||
{error, _} ->
|
||||
lists:map(fun(Object) -> {Object, Object} end, ObjectList);
|
||||
ObjDefinition ->
|
||||
lists:map(fun(Object) ->
|
||||
[_, _, ResId| _] = path_list(Object),
|
||||
Operations = case emqx_lwm2m_xml_object:get_resource_operations(binary_to_integer(ResId), ObjDefinition) of
|
||||
"E" -> [{operations, list_to_binary("E")}];
|
||||
Oper -> [{'dataType', list_to_binary(emqx_lwm2m_xml_object:get_resource_type(binary_to_integer(ResId), ObjDefinition))},
|
||||
{operations, list_to_binary(Oper)}]
|
||||
end,
|
||||
[{path, Object},
|
||||
{name, list_to_binary(emqx_lwm2m_xml_object:get_resource_name(binary_to_integer(ResId), ObjDefinition))}
|
||||
] ++ Operations
|
||||
end, ObjectList)
|
||||
end,
|
||||
[{content, R}];
|
||||
format_cmd_content(Content, _) ->
|
||||
[{content, Content}].
|
||||
%% format_cmd_content(undefined, _MsgType) -> [];
|
||||
%% format_cmd_content(_Content, <<"discover">>) ->
|
||||
%% %% [H | Content1] = Content,
|
||||
%% %% {_, [HObjId]} = emqx_lwm2m_coap_resource:parse_object_list(H),
|
||||
%% %% [ObjId | _]= path_list(HObjId),
|
||||
%% %% ObjectList = case Content1 of
|
||||
%% %% [Content2 | _] ->
|
||||
%% %% {_, ObjL} = emqx_lwm2m_coap_resource:parse_object_list(Content2),
|
||||
%% %% ObjL;
|
||||
%% %% [] -> []
|
||||
%% %% end,
|
||||
%% %% R = case emqx_lwm2m_xml_object:get_obj_def(binary_to_integer(ObjId), true) of
|
||||
%% %% {error, _} ->
|
||||
%% %% lists:map(fun(Object) -> {Object, Object} end, ObjectList);
|
||||
%% %% ObjDefinition ->
|
||||
%% %% lists:map(fun(Object) ->
|
||||
%% %% [_, _, ResId| _] = path_list(Object),
|
||||
%% %% Operations = case emqx_lwm2m_xml_object:get_resource_operations(binary_to_integer(ResId), ObjDefinition) of
|
||||
%% %% "E" -> [{operations, list_to_binary("E")}];
|
||||
%% %% Oper -> [{'dataType', list_to_binary(emqx_lwm2m_xml_object:get_resource_type(binary_to_integer(ResId), ObjDefinition))},
|
||||
%% %% {operations, list_to_binary(Oper)}]
|
||||
%% %% end,
|
||||
%% %% [{path, Object},
|
||||
%% %% {name, list_to_binary(emqx_lwm2m_xml_object:get_resource_name(binary_to_integer(ResId), ObjDefinition))}
|
||||
%% %% ] ++ Operations
|
||||
%% %% end, ObjectList)
|
||||
%% %% end,
|
||||
%% %% [{content, R}];
|
||||
%% [];
|
||||
%% format_cmd_content(Content, _) ->
|
||||
%% [{content, Content}].
|
||||
|
||||
ntoa({0,0,0,0,0,16#ffff,AB,CD}) ->
|
||||
inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256});
|
||||
|
|
|
@ -0,0 +1,459 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_lwm2m_channel).
|
||||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
%% API
|
||||
-export([ info/1
|
||||
, info/2
|
||||
, stats/1
|
||||
, validator/2
|
||||
, validator/4
|
||||
, do_takeover/3]).
|
||||
|
||||
-export([ init/2
|
||||
, handle_in/2
|
||||
, handle_deliver/2
|
||||
, handle_timeout/3
|
||||
, terminate/2
|
||||
]).
|
||||
|
||||
-export([ handle_call/2
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
]).
|
||||
|
||||
-record(channel, {
|
||||
%% Context
|
||||
ctx :: emqx_gateway_ctx:context(),
|
||||
%% Connection Info
|
||||
conninfo :: emqx_types:conninfo(),
|
||||
%% Client Info
|
||||
clientinfo :: emqx_types:clientinfo(),
|
||||
%% Session
|
||||
session :: emqx_lwm2m_session:session() | undefined,
|
||||
|
||||
%% Timer
|
||||
timers :: #{atom() => disable | undefined | reference()},
|
||||
|
||||
validator :: function()
|
||||
}).
|
||||
|
||||
-define(INFO_KEYS, [conninfo, conn_state, clientinfo, session]).
|
||||
|
||||
-import(emqx_coap_medium, [reply/2, reply/3, reply/4, iter/3, iter/4]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
info(Channel) ->
|
||||
maps:from_list(info(?INFO_KEYS, Channel)).
|
||||
|
||||
info(Keys, Channel) when is_list(Keys) ->
|
||||
[{Key, info(Key, Channel)} || Key <- Keys];
|
||||
|
||||
info(conninfo, #channel{conninfo = ConnInfo}) ->
|
||||
ConnInfo;
|
||||
info(conn_state, _) ->
|
||||
connected;
|
||||
info(clientinfo, #channel{clientinfo = ClientInfo}) ->
|
||||
ClientInfo;
|
||||
info(session, #channel{session = Session}) ->
|
||||
emqx_misc:maybe_apply(fun emqx_session:info/1, Session);
|
||||
info(clientid, #channel{clientinfo = #{clientid := ClientId}}) ->
|
||||
ClientId;
|
||||
info(ctx, #channel{ctx = Ctx}) ->
|
||||
Ctx.
|
||||
|
||||
stats(_) ->
|
||||
[].
|
||||
|
||||
init(ConnInfo = #{peername := {PeerHost, _},
|
||||
sockname := {_, SockPort}},
|
||||
#{ctx := Ctx} = Config) ->
|
||||
Peercert = maps:get(peercert, ConnInfo, undefined),
|
||||
Mountpoint = maps:get(mountpoint, Config, undefined),
|
||||
ClientInfo = set_peercert_infos(
|
||||
Peercert,
|
||||
#{ zone => default
|
||||
, protocol => lwm2m
|
||||
, peerhost => PeerHost
|
||||
, sockport => SockPort
|
||||
, username => undefined
|
||||
, clientid => undefined
|
||||
, is_bridge => false
|
||||
, is_superuser => false
|
||||
, mountpoint => Mountpoint
|
||||
}
|
||||
),
|
||||
|
||||
#channel{ ctx = Ctx
|
||||
, conninfo = ConnInfo
|
||||
, clientinfo = ClientInfo
|
||||
, timers = #{}
|
||||
, session = emqx_lwm2m_session:new()
|
||||
, validator = validator(Ctx, ClientInfo)
|
||||
}.
|
||||
|
||||
validator(_Type, _Topic, _Ctx, _ClientInfo) ->
|
||||
allow.
|
||||
%emqx_gateway_ctx:authorize(Ctx, ClientInfo, Type, Topic).
|
||||
|
||||
validator(Ctx, ClientInfo) ->
|
||||
fun(Type, Topic) ->
|
||||
validator(Type, Topic, Ctx, ClientInfo)
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Handle incoming packet
|
||||
%%--------------------------------------------------------------------
|
||||
handle_in(Msg, ChannleT) ->
|
||||
Channel = update_life_timer(ChannleT),
|
||||
call_session(handle_coap_in, Msg, Channel).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Handle Delivers from broker to client
|
||||
%%--------------------------------------------------------------------
|
||||
handle_deliver(Delivers, Channel) ->
|
||||
call_session(handle_deliver, Delivers, Channel).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Handle timeout
|
||||
%%--------------------------------------------------------------------
|
||||
handle_timeout(_, lifetime, Channel) ->
|
||||
{shutdown, timeout, Channel};
|
||||
|
||||
handle_timeout(_, {transport, _} = Msg, Channel) ->
|
||||
call_session(timeout, Msg, Channel);
|
||||
|
||||
handle_timeout(_, disconnect, Channel) ->
|
||||
{shutdown, normal, Channel};
|
||||
|
||||
handle_timeout(_, _, Channel) ->
|
||||
{ok, Channel}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Handle call
|
||||
%%--------------------------------------------------------------------
|
||||
handle_call(Req, Channel) ->
|
||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
||||
{reply, ignored, Channel}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Handle Cast
|
||||
%%--------------------------------------------------------------------
|
||||
handle_cast(Req, Channel) ->
|
||||
?LOG(error, "Unexpected cast: ~p", [Req]),
|
||||
{ok, Channel}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Handle Info
|
||||
%%--------------------------------------------------------------------
|
||||
handle_info(Info, Channel) ->
|
||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
||||
{ok, Channel}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Terminate
|
||||
%%--------------------------------------------------------------------
|
||||
terminate(_Reason, #channel{session = Session}) ->
|
||||
emqx_lwm2m_session:on_close(Session).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
set_peercert_infos(NoSSL, ClientInfo)
|
||||
when NoSSL =:= nossl;
|
||||
NoSSL =:= undefined ->
|
||||
ClientInfo;
|
||||
set_peercert_infos(Peercert, ClientInfo) ->
|
||||
{DN, CN} = {esockd_peercert:subject(Peercert),
|
||||
esockd_peercert:common_name(Peercert)},
|
||||
ClientInfo#{dn => DN, cn => CN}.
|
||||
|
||||
make_timer(Name, Time, Msg, Channel = #channel{timers = Timers}) ->
|
||||
TRef = emqx_misc:start_timer(Time, Msg),
|
||||
Channel#channel{timers = Timers#{Name => TRef}}.
|
||||
|
||||
update_life_timer(#channel{session = Session, timers = Timers} = Channel) ->
|
||||
LifeTime = emqx_lwm2m_session:info(lifetime, Session),
|
||||
_ = case maps:get(lifetime, Timers, undefined) of
|
||||
undefined -> ok;
|
||||
Ref -> erlang:cancel_timer(Ref)
|
||||
end,
|
||||
make_timer(lifetime, LifeTime, lifetime, Channel).
|
||||
|
||||
check_location(Location, #channel{session = Session}) ->
|
||||
SLocation = emqx_lwm2m_session:info(location_path, Session),
|
||||
Location =:= SLocation.
|
||||
|
||||
do_takeover(_DesireId, Msg, Channel) ->
|
||||
%% TODO completed the takeover, now only reset the message
|
||||
Reset = emqx_coap_message:reset(Msg),
|
||||
call_session(handle_out, Reset, Channel).
|
||||
|
||||
do_connect(Req, Result, Channel, Iter) ->
|
||||
case emqx_misc:pipeline(
|
||||
[ fun check_lwm2m_version/2
|
||||
, fun run_conn_hooks/2
|
||||
, fun enrich_clientinfo/2
|
||||
, fun set_log_meta/2
|
||||
, fun auth_connect/2
|
||||
],
|
||||
Req,
|
||||
Channel) of
|
||||
{ok, _Input, #channel{session = Session,
|
||||
validator = Validator} = NChannel} ->
|
||||
case emqx_lwm2m_session:info(reg_info, Session) of
|
||||
undefined ->
|
||||
process_connect(ensure_connected(NChannel), Req, Result, Iter);
|
||||
_ ->
|
||||
NewResult = emqx_lwm2m_session:reregister(Req, Validator, Session),
|
||||
iter(Iter, maps:merge(Result, NewResult), NChannel)
|
||||
end;
|
||||
{error, ReasonCode, NChannel} ->
|
||||
ErrMsg = io_lib:format("Login Failed: ~s", [ReasonCode]),
|
||||
Payload = erlang:list_to_binary(lists:flatten(ErrMsg)),
|
||||
iter(Iter,
|
||||
reply({error, bad_request}, Payload, Req, Result),
|
||||
NChannel)
|
||||
end.
|
||||
|
||||
check_lwm2m_version(#coap_message{options = Opts},
|
||||
#channel{conninfo = ConnInfo} = Channel) ->
|
||||
Ver = gets([uri_query, <<"lwm2m">>], Opts),
|
||||
IsValid = case Ver of
|
||||
<<"1.0">> ->
|
||||
true;
|
||||
<<"1">> ->
|
||||
true;
|
||||
<<"1.1">> ->
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end,
|
||||
if IsValid ->
|
||||
NConnInfo = ConnInfo#{ connected_at => erlang:system_time(millisecond)
|
||||
, proto_name => <<"lwm2m">>
|
||||
, proto_ver => Ver
|
||||
},
|
||||
{ok, Channel#channel{conninfo = NConnInfo}};
|
||||
true ->
|
||||
?LOG(error, "Reject REGISTER due to unsupported version: ~0p", [Ver]),
|
||||
{error, "invalid lwm2m version", Channel}
|
||||
end.
|
||||
|
||||
run_conn_hooks(Input, Channel = #channel{ctx = Ctx,
|
||||
conninfo = ConnInfo}) ->
|
||||
ConnProps = #{},
|
||||
case run_hooks(Ctx, 'client.connect', [ConnInfo], ConnProps) of
|
||||
Error = {error, _Reason} -> Error;
|
||||
_NConnProps ->
|
||||
{ok, Input, Channel}
|
||||
end.
|
||||
|
||||
enrich_clientinfo(#coap_message{options = Options} = Msg,
|
||||
Channel = #channel{clientinfo = ClientInfo0}) ->
|
||||
Query = maps:get(uri_query, Options, #{}),
|
||||
case Query of
|
||||
#{<<"ep">> := Epn} ->
|
||||
UserName = maps:get(<<"imei">>, Query, undefined),
|
||||
Password = maps:get(<<"password">>, Query, undefined),
|
||||
ClientId = maps:get(<<"device_id">>, Query, Epn),
|
||||
ClientInfo =
|
||||
ClientInfo0#{username => UserName,
|
||||
password => Password,
|
||||
clientid => ClientId},
|
||||
{ok, NClientInfo} = fix_mountpoint(Msg, ClientInfo),
|
||||
{ok, Channel#channel{clientinfo = NClientInfo}};
|
||||
_ ->
|
||||
?LOG(error, "Reject REGISTER due to wrong parameters, Query=~p", [Query]),
|
||||
{error, "invalid queries", Channel}
|
||||
end.
|
||||
|
||||
set_log_meta(_Input, #channel{clientinfo = #{clientid := ClientId}}) ->
|
||||
emqx_logger:set_metadata_clientid(ClientId),
|
||||
ok.
|
||||
|
||||
auth_connect(_Input, Channel = #channel{ctx = Ctx,
|
||||
clientinfo = ClientInfo}) ->
|
||||
#{clientid := ClientId, username := Username} = ClientInfo,
|
||||
case emqx_gateway_ctx:authenticate(Ctx, ClientInfo) of
|
||||
{ok, NClientInfo} ->
|
||||
{ok, Channel#channel{clientinfo = NClientInfo,
|
||||
validator = validator(Ctx, ClientInfo)}};
|
||||
{error, Reason} ->
|
||||
?LOG(warning, "Client ~s (Username: '~s') login failed for ~0p",
|
||||
[ClientId, Username, Reason]),
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
fix_mountpoint(_Packet, #{mountpoint := undefined} = ClientInfo) ->
|
||||
{ok, ClientInfo};
|
||||
fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) ->
|
||||
%% TODO: Enrich the varibale replacement????
|
||||
%% i.e: ${ClientInfo.auth_result.productKey}
|
||||
Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo),
|
||||
{ok, ClientInfo#{mountpoint := Mountpoint1}}.
|
||||
|
||||
ensure_connected(Channel = #channel{ctx = Ctx,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo}) ->
|
||||
ok = run_hooks(Ctx, 'client.connected', [ClientInfo, ConnInfo]),
|
||||
Channel.
|
||||
|
||||
process_connect(Channel = #channel{ctx = Ctx,
|
||||
session = Session,
|
||||
conninfo = ConnInfo,
|
||||
clientinfo = ClientInfo,
|
||||
validator = Validator},
|
||||
Msg, Result, Iter) ->
|
||||
%% inherit the old session
|
||||
SessFun = fun(_,_) -> #{} end,
|
||||
case emqx_gateway_ctx:open_session(
|
||||
Ctx,
|
||||
true,
|
||||
ClientInfo,
|
||||
ConnInfo,
|
||||
SessFun,
|
||||
emqx_lwm2m_session
|
||||
) of
|
||||
{ok, _} ->
|
||||
NewResult = emqx_lwm2m_session:init(Msg, Validator, Session),
|
||||
iter(Iter, maps:merge(Result, NewResult), Channel);
|
||||
{error, Reason} ->
|
||||
?LOG(error, "Failed to open session du to ~p", [Reason]),
|
||||
iter(Iter, reply({error, bad_request}, Msg, Result), Channel)
|
||||
end.
|
||||
|
||||
run_hooks(Ctx, Name, Args) ->
|
||||
emqx_gateway_ctx:metrics_inc(Ctx, Name),
|
||||
emqx_hooks:run(Name, Args).
|
||||
|
||||
run_hooks(Ctx, Name, Args, Acc) ->
|
||||
emqx_gateway_ctx:metrics_inc(Ctx, Name),
|
||||
emqx_hooks:run_fold(Name, Args, Acc).
|
||||
|
||||
gets(_, undefined) ->
|
||||
undefined;
|
||||
gets([H | T], Map) ->
|
||||
gets(T, maps:get(H, Map, undefined));
|
||||
gets([], Val) ->
|
||||
Val.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Call Chain
|
||||
%%--------------------------------------------------------------------
|
||||
call_session(Fun,
|
||||
Msg,
|
||||
#channel{session = Session,
|
||||
validator = Validator} = Channel) ->
|
||||
iter([ session, fun process_session/4
|
||||
, proto, fun process_protocol/4
|
||||
, return, fun process_return/4
|
||||
, lifetime, fun process_lifetime/4
|
||||
, reply, fun process_reply/4
|
||||
, out, fun process_out/4
|
||||
, fun process_nothing/3
|
||||
],
|
||||
emqx_lwm2m_session:Fun(Msg, Validator, Session),
|
||||
Channel).
|
||||
|
||||
process_session(Session, Result, Channel, Iter) ->
|
||||
iter(Iter, Result, Channel#channel{session = Session}).
|
||||
|
||||
process_protocol({request, Msg}, Result, Channel, Iter) ->
|
||||
#coap_message{method = Method} = Msg,
|
||||
handle_request_protocol(Method, Msg, Result, Channel, Iter);
|
||||
|
||||
process_protocol(Msg, Result,
|
||||
#channel{validator = Validator, session = Session} = Channel, Iter) ->
|
||||
ProtoResult = emqx_lwm2m_session:handle_protocol_in(Msg, Validator, Session),
|
||||
iter(Iter, maps:merge(Result, ProtoResult), Channel).
|
||||
|
||||
handle_request_protocol(post, #coap_message{options = Opts} = Msg,
|
||||
Result, Channel, Iter) ->
|
||||
case Opts of
|
||||
#{uri_path := [?REG_PREFIX]} ->
|
||||
do_connect(Msg, Result, Channel, Iter);
|
||||
#{uri_path := Location} ->
|
||||
do_update(Location, Msg, Result, Channel, Iter);
|
||||
_ ->
|
||||
iter(Iter, reply({error, not_found}, Msg, Result), Channel)
|
||||
end;
|
||||
|
||||
handle_request_protocol(delete, #coap_message{options = Opts} = Msg,
|
||||
Result, Channel, Iter) ->
|
||||
case Opts of
|
||||
#{uri_path := Location} ->
|
||||
case check_location(Location, Channel) of
|
||||
true ->
|
||||
Reply = emqx_coap_message:piggyback({ok, deleted}, Msg),
|
||||
{shutdown, close, Reply, Channel};
|
||||
_ ->
|
||||
iter(Iter, reply({error, not_found}, Msg, Result), Channel)
|
||||
end;
|
||||
_ ->
|
||||
iter(Iter, reply({error, bad_request}, Msg, Result), Channel)
|
||||
end.
|
||||
|
||||
do_update(Location, Msg, Result,
|
||||
#channel{session = Session, validator = Validator} = Channel, Iter) ->
|
||||
case check_location(Location, Channel) of
|
||||
true ->
|
||||
NewResult = emqx_lwm2m_session:update(Msg, Validator, Session),
|
||||
iter(Iter, maps:merge(Result, NewResult), Channel);
|
||||
_ ->
|
||||
iter(Iter, reply({error, not_found}, Msg, Result), Channel)
|
||||
end.
|
||||
|
||||
process_return({Outs, Session}, Result, Channel, Iter) ->
|
||||
OldOuts = maps:get(out, Result, []),
|
||||
iter(Iter,
|
||||
Result#{out => Outs ++ OldOuts},
|
||||
Channel#channel{session = Session}).
|
||||
|
||||
process_out(Outs, Result, Channel, _) ->
|
||||
Outs2 = lists:reverse(Outs),
|
||||
Outs3 = case maps:get(reply, Result, undefined) of
|
||||
undefined ->
|
||||
Outs2;
|
||||
Reply ->
|
||||
[Reply | Outs2]
|
||||
end,
|
||||
%% emqx_gateway_conn bug, work around
|
||||
case Outs3 of
|
||||
[] ->
|
||||
{ok, Channel};
|
||||
_ ->
|
||||
{ok, {outgoing, Outs3}, Channel}
|
||||
end.
|
||||
|
||||
process_reply(Reply, Result, #channel{session = Session} = Channel, _) ->
|
||||
Session2 = emqx_lwm2m_session:set_reply(Reply, Session),
|
||||
Outs = maps:get(out, Result, []),
|
||||
Outs2 = lists:reverse(Outs),
|
||||
{ok, {outgoing, [Reply | Outs2]}, Channel#channel{session = Session2}}.
|
||||
|
||||
process_lifetime(_, Result, Channel, Iter) ->
|
||||
iter(Iter, Result, update_life_timer(Channel)).
|
||||
|
||||
process_nothing(_, _, Channel) ->
|
||||
{ok, Channel}.
|
|
@ -1,153 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_lwm2m_cm).
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([ register_channel/5
|
||||
, update_reg_info/2
|
||||
, unregister_channel/1
|
||||
]).
|
||||
|
||||
-export([ lookup_channel/1
|
||||
, all_channels/0
|
||||
]).
|
||||
|
||||
-export([ register_cmd/3
|
||||
, register_cmd/4
|
||||
, lookup_cmd/3
|
||||
, lookup_cmd_by_imei/1
|
||||
]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-CM: " ++ Format, Args)).
|
||||
|
||||
%% Server name
|
||||
-define(CM, ?MODULE).
|
||||
|
||||
-define(LWM2M_CHANNEL_TAB, emqx_lwm2m_channel).
|
||||
-define(LWM2M_CMD_TAB, emqx_lwm2m_cmd).
|
||||
|
||||
%% Batch drain
|
||||
-define(BATCH_SIZE, 100000).
|
||||
|
||||
%% @doc Start the channel manager.
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?CM}, ?MODULE, [], []).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
register_channel(IMEI, RegInfo, LifeTime, Ver, Peername) ->
|
||||
Info = #{
|
||||
reg_info => RegInfo,
|
||||
lifetime => LifeTime,
|
||||
version => Ver,
|
||||
peername => Peername
|
||||
},
|
||||
true = ets:insert(?LWM2M_CHANNEL_TAB, {IMEI, Info}),
|
||||
cast({registered, {IMEI, self()}}).
|
||||
|
||||
update_reg_info(IMEI, RegInfo) ->
|
||||
case lookup_channel(IMEI) of
|
||||
[{_, RegInfo0}] ->
|
||||
true = ets:insert(?LWM2M_CHANNEL_TAB, {IMEI, RegInfo0#{reg_info => RegInfo}}),
|
||||
ok;
|
||||
[] ->
|
||||
ok
|
||||
end.
|
||||
|
||||
unregister_channel(IMEI) when is_binary(IMEI) ->
|
||||
true = ets:delete(?LWM2M_CHANNEL_TAB, IMEI),
|
||||
ok.
|
||||
|
||||
lookup_channel(IMEI) ->
|
||||
ets:lookup(?LWM2M_CHANNEL_TAB, IMEI).
|
||||
|
||||
all_channels() ->
|
||||
ets:tab2list(?LWM2M_CHANNEL_TAB).
|
||||
|
||||
register_cmd(IMEI, Path, Type) ->
|
||||
true = ets:insert(?LWM2M_CMD_TAB, {{IMEI, Path, Type}, undefined}).
|
||||
|
||||
register_cmd(_IMEI, undefined, _Type, _Result) ->
|
||||
ok;
|
||||
register_cmd(IMEI, Path, Type, Result) ->
|
||||
true = ets:insert(?LWM2M_CMD_TAB, {{IMEI, Path, Type}, Result}).
|
||||
|
||||
lookup_cmd(IMEI, Path, Type) ->
|
||||
ets:lookup(?LWM2M_CMD_TAB, {IMEI, Path, Type}).
|
||||
|
||||
lookup_cmd_by_imei(IMEI) ->
|
||||
ets:select(?LWM2M_CHANNEL_TAB, [{{{IMEI, '_', '_'}, '$1'}, [], ['$_']}]).
|
||||
|
||||
%% @private
|
||||
cast(Msg) -> gen_server:cast(?CM, Msg).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% gen_server callbacks
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
init([]) ->
|
||||
TabOpts = [public, {write_concurrency, true}, {read_concurrency, true}],
|
||||
ok = emqx_tables:new(?LWM2M_CHANNEL_TAB, [set, compressed | TabOpts]),
|
||||
ok = emqx_tables:new(?LWM2M_CMD_TAB, [set, compressed | TabOpts]),
|
||||
{ok, #{chan_pmon => emqx_pmon:new()}}.
|
||||
|
||||
handle_call(Req, _From, State) ->
|
||||
?LOG(error, "Unexpected call: ~p", [Req]),
|
||||
{reply, ignored, State}.
|
||||
|
||||
handle_cast({registered, {IMEI, ChanPid}}, State = #{chan_pmon := PMon}) ->
|
||||
PMon1 = emqx_pmon:monitor(ChanPid, IMEI, PMon),
|
||||
{noreply, State#{chan_pmon := PMon1}};
|
||||
|
||||
handle_cast(Msg, State) ->
|
||||
?LOG(error, "Unexpected cast: ~p", [Msg]),
|
||||
{noreply, State}.
|
||||
|
||||
handle_info({'DOWN', _MRef, process, Pid, _Reason}, State = #{chan_pmon := PMon}) ->
|
||||
ChanPids = [Pid | emqx_misc:drain_down(?BATCH_SIZE)],
|
||||
{Items, PMon1} = emqx_pmon:erase_all(ChanPids, PMon),
|
||||
ok = emqx_pool:async_submit(fun lists:foreach/2, [fun clean_down/1, Items]),
|
||||
{noreply, State#{chan_pmon := PMon1}};
|
||||
|
||||
handle_info(Info, State) ->
|
||||
?LOG(error, "Unexpected info: ~p", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, _State) ->
|
||||
emqx_stats:cancel_update(chan_stats).
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
clean_down({_ChanPid, IMEI}) ->
|
||||
unregister_channel(IMEI).
|
|
@ -0,0 +1,410 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2016-2017 EMQ Enterprise, Inc. (http://emqtt.io)
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_lwm2m_cmd).
|
||||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
-export([ mqtt_to_coap/2
|
||||
, coap_to_mqtt/4
|
||||
, empty_ack_to_mqtt/1
|
||||
, coap_failure_to_mqtt/2
|
||||
]).
|
||||
|
||||
-export([path_list/1, extract_path/1]).
|
||||
|
||||
-define(STANDARD, 1).
|
||||
|
||||
%-type msg_type() :: <<"create">>
|
||||
% | <<"delete">>
|
||||
% | <<"read">>
|
||||
% | <<"write">>
|
||||
% | <<"execute">>
|
||||
% | <<"discover">>
|
||||
% | <<"write-attr">>
|
||||
% | <<"observe">>
|
||||
% | <<"cancel-observe">>.
|
||||
%
|
||||
%-type cmd() :: #{ <<"msgType">> := msg_type()
|
||||
% , <<"data">> := maps()
|
||||
% %% more keys?
|
||||
% }.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"create">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"basePath">>, Data, <<"/">>)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, maps:get(<<"content">>, Data)),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
CoapRequest = emqx_coap_message:request(con, post, Payload,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}]),
|
||||
{CoapRequest, InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"delete">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{emqx_coap_message:request(con, delete, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList}]), InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"read">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{emqx_coap_message:request(con, get, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList}]), InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write">>, <<"data">> := Data}) ->
|
||||
CoapRequest =
|
||||
case maps:get(<<"basePath">>, Data, <<"/">>) of
|
||||
<<"/">> ->
|
||||
single_write_request(AlternatePath, Data);
|
||||
BasePath ->
|
||||
batch_write_request(AlternatePath, BasePath, maps:get(<<"content">>, Data))
|
||||
end,
|
||||
{CoapRequest, InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"execute">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
Args =
|
||||
case maps:get(<<"args">>, Data, <<>>) of
|
||||
<<"undefined">> -> <<>>;
|
||||
undefined -> <<>>;
|
||||
Arg1 -> Arg1
|
||||
end,
|
||||
{emqx_coap_message:request(con, post, Args,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{content_format, <<"text/plain">>}]), InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"discover">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{emqx_coap_message:request(con, get, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{'accept', ?LWM2M_FORMAT_LINK}]), InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write-attr">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
Query = attr_query_list(Data),
|
||||
{emqx_coap_message:request(con, put, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{uri_query, Query}]), InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"observe">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{emqx_coap_message:request(con, get, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{observe, 0}]), InputCmd};
|
||||
|
||||
mqtt_to_coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"cancel-observe">>, <<"data">> := Data}) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{emqx_coap_message:request(con, get, <<>>,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{observe, 1}]), InputCmd}.
|
||||
|
||||
coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"create">>}) ->
|
||||
make_response(Code, Ref);
|
||||
|
||||
coap_to_mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"delete">>}) ->
|
||||
make_response(Code, Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"read">>}) ->
|
||||
read_resp_to_mqtt(Method, CoapPayload, data_format(Options), Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write">>}) ->
|
||||
write_resp_to_mqtt(Method, CoapPayload, Ref);
|
||||
|
||||
coap_to_mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"execute">>}) ->
|
||||
execute_resp_to_mqtt(Method, Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"discover">>}) ->
|
||||
discover_resp_to_mqtt(Method, CoapPayload, Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write-attr">>}) ->
|
||||
writeattr_resp_to_mqtt(Method, CoapPayload, Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"observe">>}) ->
|
||||
observe_resp_to_mqtt(Method, CoapPayload, data_format(Options), observe_seq(Options), Ref);
|
||||
|
||||
coap_to_mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"cancel-observe">>}) ->
|
||||
cancel_observe_resp_to_mqtt(Method, CoapPayload, data_format(Options), Ref).
|
||||
|
||||
read_resp_to_mqtt({error, ErrorCode}, _CoapPayload, _Format, Ref) ->
|
||||
make_response(ErrorCode, Ref);
|
||||
|
||||
read_resp_to_mqtt({ok, SuccessCode}, CoapPayload, Format, Ref) ->
|
||||
try
|
||||
Result = content_to_mqtt(CoapPayload, Format, Ref),
|
||||
make_response(SuccessCode, Ref, Format, Result)
|
||||
catch
|
||||
error:not_implemented -> make_response(not_implemented, Ref);
|
||||
_:Ex:_ST ->
|
||||
?LOG(error, "~0p, bad payload format: ~0p", [Ex, CoapPayload]),
|
||||
make_response(bad_request, Ref)
|
||||
end.
|
||||
|
||||
empty_ack_to_mqtt(Ref) ->
|
||||
make_base_response(maps:put(<<"msgType">>, <<"ack">>, Ref)).
|
||||
|
||||
coap_failure_to_mqtt(Ref, MsgType) ->
|
||||
make_base_response(maps:put(<<"msgType">>, MsgType, Ref)).
|
||||
|
||||
content_to_mqtt(CoapPayload, <<"text/plain">>, Ref) ->
|
||||
emqx_lwm2m_message:text_to_json(extract_path(Ref), CoapPayload);
|
||||
|
||||
content_to_mqtt(CoapPayload, <<"application/octet-stream">>, Ref) ->
|
||||
emqx_lwm2m_message:opaque_to_json(extract_path(Ref), CoapPayload);
|
||||
|
||||
content_to_mqtt(CoapPayload, <<"application/vnd.oma.lwm2m+tlv">>, Ref) ->
|
||||
emqx_lwm2m_message:tlv_to_json(extract_path(Ref), CoapPayload);
|
||||
|
||||
content_to_mqtt(CoapPayload, <<"application/vnd.oma.lwm2m+json">>, _Ref) ->
|
||||
emqx_lwm2m_message:translate_json(CoapPayload).
|
||||
|
||||
write_resp_to_mqtt({ok, changed}, _CoapPayload, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
|
||||
write_resp_to_mqtt({ok, content}, CoapPayload, Ref) when CoapPayload =:= <<>> ->
|
||||
make_response(method_not_allowed, Ref);
|
||||
|
||||
write_resp_to_mqtt({ok, content}, _CoapPayload, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
|
||||
write_resp_to_mqtt({error, Error}, _CoapPayload, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
execute_resp_to_mqtt({ok, changed}, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
|
||||
execute_resp_to_mqtt({error, Error}, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
discover_resp_to_mqtt({ok, content}, CoapPayload, Ref) ->
|
||||
Links = binary:split(CoapPayload, <<",">>, [global]),
|
||||
make_response(content, Ref, <<"application/link-format">>, Links);
|
||||
|
||||
discover_resp_to_mqtt({error, Error}, _CoapPayload, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
writeattr_resp_to_mqtt({ok, changed}, _CoapPayload, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
|
||||
writeattr_resp_to_mqtt({error, Error}, _CoapPayload, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
observe_resp_to_mqtt({error, Error}, _CoapPayload, _Format, _ObserveSeqNum, Ref) ->
|
||||
make_response(Error, Ref);
|
||||
|
||||
observe_resp_to_mqtt({ok, content}, CoapPayload, Format, 0, Ref) ->
|
||||
read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref);
|
||||
|
||||
observe_resp_to_mqtt({ok, content}, CoapPayload, Format, ObserveSeqNum, Ref) ->
|
||||
read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref#{<<"seqNum">> => ObserveSeqNum}).
|
||||
|
||||
cancel_observe_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref) ->
|
||||
read_resp_to_mqtt({ok, content}, CoapPayload, Format, Ref);
|
||||
|
||||
cancel_observe_resp_to_mqtt({error, Error}, _CoapPayload, _Format, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
make_response(Code, Ref=#{}) ->
|
||||
BaseRsp = make_base_response(Ref),
|
||||
make_data_response(BaseRsp, Code).
|
||||
|
||||
make_response(Code, Ref=#{}, _Format, Result) ->
|
||||
BaseRsp = make_base_response(Ref),
|
||||
make_data_response(BaseRsp, Code, _Format, Result).
|
||||
|
||||
%% The base response format is what included in the request:
|
||||
%%
|
||||
%% #{
|
||||
%% <<"seqNum">> => SeqNum,
|
||||
%% <<"imsi">> => maps:get(<<"imsi">>, Ref, null),
|
||||
%% <<"imei">> => maps:get(<<"imei">>, Ref, null),
|
||||
%% <<"requestID">> => maps:get(<<"requestID">>, Ref, null),
|
||||
%% <<"cacheID">> => maps:get(<<"cacheID">>, Ref, null),
|
||||
%% <<"msgType">> => maps:get(<<"msgType">>, Ref, null)
|
||||
%% }
|
||||
|
||||
make_base_response(Ref=#{}) ->
|
||||
remove_tmp_fields(Ref).
|
||||
|
||||
make_data_response(BaseRsp, Code) ->
|
||||
BaseRsp#{
|
||||
<<"data">> => #{
|
||||
<<"reqPath">> => extract_path(BaseRsp),
|
||||
<<"code">> => code(Code),
|
||||
<<"codeMsg">> => Code
|
||||
}
|
||||
}.
|
||||
|
||||
make_data_response(BaseRsp, Code, _Format, Result) ->
|
||||
BaseRsp#{
|
||||
<<"data">> =>
|
||||
#{
|
||||
<<"reqPath">> => extract_path(BaseRsp),
|
||||
<<"code">> => code(Code),
|
||||
<<"codeMsg">> => Code,
|
||||
<<"content">> => Result
|
||||
}
|
||||
}.
|
||||
|
||||
remove_tmp_fields(Ref) ->
|
||||
maps:remove(observe_type, Ref).
|
||||
|
||||
-spec path_list(Path::binary()) -> {[PathWord::binary()], [Query::binary()]}.
|
||||
path_list(Path) ->
|
||||
case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, ResId, LastPart] ->
|
||||
{ResInstId, QueryList} = query_list(LastPart),
|
||||
{[ObjId, ObjInsId, ResId, ResInstId], QueryList};
|
||||
[ObjId, ObjInsId, LastPart] ->
|
||||
{ResId, QueryList} = query_list(LastPart),
|
||||
{[ObjId, ObjInsId, ResId], QueryList};
|
||||
[ObjId, LastPart] ->
|
||||
{ObjInsId, QueryList} = query_list(LastPart),
|
||||
{[ObjId, ObjInsId], QueryList};
|
||||
[LastPart] ->
|
||||
{ObjId, QueryList} = query_list(LastPart),
|
||||
{[ObjId], QueryList}
|
||||
end.
|
||||
|
||||
query_list(PathWithQuery) ->
|
||||
case binary:split(PathWithQuery, [<<$?>>], []) of
|
||||
[Path] -> {Path, []};
|
||||
[Path, Querys] ->
|
||||
{Path, binary:split(Querys, [<<$&>>], [global])}
|
||||
end.
|
||||
|
||||
attr_query_list(Data) ->
|
||||
attr_query_list(Data, valid_attr_keys(), []).
|
||||
|
||||
attr_query_list(QueryJson = #{}, ValidAttrKeys, QueryList) ->
|
||||
maps:fold(
|
||||
fun
|
||||
(_K, null, Acc) -> Acc;
|
||||
(K, V, Acc) ->
|
||||
case lists:member(K, ValidAttrKeys) of
|
||||
true ->
|
||||
KV = <<K/binary, "=", V/binary>>,
|
||||
Acc ++ [KV];
|
||||
false ->
|
||||
Acc
|
||||
end
|
||||
end, QueryList, QueryJson).
|
||||
|
||||
valid_attr_keys() ->
|
||||
[<<"pmin">>, <<"pmax">>, <<"gt">>, <<"lt">>, <<"st">>].
|
||||
|
||||
data_format(Options) ->
|
||||
maps:get(content_format, Options, <<"text/plain">>).
|
||||
|
||||
observe_seq(Options) ->
|
||||
maps:get(observe, Options, rand:uniform(1000000) + 1 ).
|
||||
|
||||
add_alternate_path_prefix(<<"/">>, PathList) ->
|
||||
PathList;
|
||||
|
||||
add_alternate_path_prefix(AlternatePath, PathList) ->
|
||||
[binary_util:trim(AlternatePath, $/) | PathList].
|
||||
|
||||
extract_path(Ref = #{}) ->
|
||||
drop_query(
|
||||
case Ref of
|
||||
#{<<"data">> := Data} ->
|
||||
case maps:get(<<"path">>, Data, nil) of
|
||||
nil -> maps:get(<<"basePath">>, Data, undefined);
|
||||
Path -> Path
|
||||
end;
|
||||
#{<<"path">> := Path} ->
|
||||
Path
|
||||
end).
|
||||
|
||||
|
||||
batch_write_request(AlternatePath, BasePath, Content) ->
|
||||
{PathList, QueryList} = path_list(BasePath),
|
||||
Method = case length(PathList) of
|
||||
2 -> post;
|
||||
3 -> put
|
||||
end,
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Content),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
emqx_coap_message:request(con, Method, Payload,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}]).
|
||||
|
||||
single_write_request(AlternatePath, Data) ->
|
||||
{PathList, QueryList} = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
%% TO DO: handle write to resource instance, e.g. /4/0/1/0
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, [Data]),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
emqx_coap_message:request(con, put, Payload,
|
||||
[{uri_path, FullPathList},
|
||||
{uri_query, QueryList},
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}]).
|
||||
|
||||
drop_query(Path) ->
|
||||
case binary:split(Path, [<<$?>>]) of
|
||||
[Path] -> Path;
|
||||
[PathOnly, _Query] -> PathOnly
|
||||
end.
|
||||
|
||||
code(get) -> <<"0.01">>;
|
||||
code(post) -> <<"0.02">>;
|
||||
code(put) -> <<"0.03">>;
|
||||
code(delete) -> <<"0.04">>;
|
||||
code(created) -> <<"2.01">>;
|
||||
code(deleted) -> <<"2.02">>;
|
||||
code(valid) -> <<"2.03">>;
|
||||
code(changed) -> <<"2.04">>;
|
||||
code(content) -> <<"2.05">>;
|
||||
code(continue) -> <<"2.31">>;
|
||||
code(bad_request) -> <<"4.00">>;
|
||||
code(unauthorized) -> <<"4.01">>;
|
||||
code(bad_option) -> <<"4.02">>;
|
||||
code(forbidden) -> <<"4.03">>;
|
||||
code(not_found) -> <<"4.04">>;
|
||||
code(method_not_allowed) -> <<"4.05">>;
|
||||
code(not_acceptable) -> <<"4.06">>;
|
||||
code(request_entity_incomplete) -> <<"4.08">>;
|
||||
code(precondition_failed) -> <<"4.12">>;
|
||||
code(request_entity_too_large) -> <<"4.13">>;
|
||||
code(unsupported_content_format) -> <<"4.15">>;
|
||||
code(internal_server_error) -> <<"5.00">>;
|
||||
code(not_implemented) -> <<"5.01">>;
|
||||
code(bad_gateway) -> <<"5.02">>;
|
||||
code(service_unavailable) -> <<"5.03">>;
|
||||
code(gateway_timeout) -> <<"5.04">>;
|
||||
code(proxying_not_supported) -> <<"5.05">>.
|
|
@ -1,310 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_lwm2m_cmd_handler).
|
||||
|
||||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
-include_lib("lwm2m_coap/include/coap.hrl").
|
||||
|
||||
-export([ mqtt2coap/2
|
||||
, coap2mqtt/4
|
||||
, ack2mqtt/1
|
||||
, extract_path/1
|
||||
]).
|
||||
|
||||
-export([path_list/1]).
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-CMD: " ++ Format, Args)).
|
||||
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"create">>, <<"data">> := Data}) ->
|
||||
PathList = path_list(maps:get(<<"basePath">>, Data, <<"/">>)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, maps:get(<<"content">>, Data)),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
CoapRequest = lwm2m_coap_message:request(con, post, Payload, [{uri_path, FullPathList},
|
||||
{content_format, <<"application/vnd.oma.lwm2m+tlv">>}]),
|
||||
{CoapRequest, InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"delete">>, <<"data">> := Data}) ->
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))),
|
||||
{lwm2m_coap_message:request(con, delete, <<>>, [{uri_path, FullPathList}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"read">>, <<"data">> := Data}) ->
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))),
|
||||
{lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write">>, <<"data">> := Data}) ->
|
||||
Encoding = maps:get(<<"encoding">>, InputCmd, <<"plain">>),
|
||||
CoapRequest =
|
||||
case maps:get(<<"basePath">>, Data, <<"/">>) of
|
||||
<<"/">> ->
|
||||
single_write_request(AlternatePath, Data, Encoding);
|
||||
BasePath ->
|
||||
batch_write_request(AlternatePath, BasePath, maps:get(<<"content">>, Data), Encoding)
|
||||
end,
|
||||
{CoapRequest, InputCmd};
|
||||
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"execute">>, <<"data">> := Data}) ->
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))),
|
||||
Args =
|
||||
case maps:get(<<"args">>, Data, <<>>) of
|
||||
<<"undefined">> -> <<>>;
|
||||
undefined -> <<>>;
|
||||
Arg1 -> Arg1
|
||||
end,
|
||||
{lwm2m_coap_message:request(con, post, Args, [{uri_path, FullPathList}, {content_format, <<"text/plain">>}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"discover">>, <<"data">> := Data}) ->
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))),
|
||||
{lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {'accept', ?LWM2M_FORMAT_LINK}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"write-attr">>, <<"data">> := Data}) ->
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, path_list(maps:get(<<"path">>, Data))),
|
||||
Query = attr_query_list(Data),
|
||||
{lwm2m_coap_message:request(con, put, <<>>, [{uri_path, FullPathList}, {uri_query, Query}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"observe">>, <<"data">> := Data}) ->
|
||||
PathList = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {observe, 0}]), InputCmd};
|
||||
mqtt2coap(AlternatePath, InputCmd = #{<<"msgType">> := <<"cancel-observe">>, <<"data">> := Data}) ->
|
||||
PathList = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
{lwm2m_coap_message:request(con, get, <<>>, [{uri_path, FullPathList}, {observe, 1}]), InputCmd}.
|
||||
|
||||
coap2mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"create">>}) ->
|
||||
make_response(Code, Ref);
|
||||
coap2mqtt(_Method = {_, Code}, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"delete">>}) ->
|
||||
make_response(Code, Ref);
|
||||
coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"read">>}) ->
|
||||
coap_read_to_mqtt(Method, CoapPayload, data_format(Options), Ref);
|
||||
coap2mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write">>}) ->
|
||||
coap_write_to_mqtt(Method, Ref);
|
||||
coap2mqtt(Method, _CoapPayload, _Options, Ref=#{<<"msgType">> := <<"execute">>}) ->
|
||||
coap_execute_to_mqtt(Method, Ref);
|
||||
coap2mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"discover">>}) ->
|
||||
coap_discover_to_mqtt(Method, CoapPayload, Ref);
|
||||
coap2mqtt(Method, CoapPayload, _Options, Ref=#{<<"msgType">> := <<"write-attr">>}) ->
|
||||
coap_writeattr_to_mqtt(Method, CoapPayload, Ref);
|
||||
coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"observe">>}) ->
|
||||
coap_observe_to_mqtt(Method, CoapPayload, data_format(Options), observe_seq(Options), Ref);
|
||||
coap2mqtt(Method, CoapPayload, Options, Ref=#{<<"msgType">> := <<"cancel-observe">>}) ->
|
||||
coap_cancel_observe_to_mqtt(Method, CoapPayload, data_format(Options), Ref).
|
||||
|
||||
coap_read_to_mqtt({error, ErrorCode}, _CoapPayload, _Format, Ref) ->
|
||||
make_response(ErrorCode, Ref);
|
||||
coap_read_to_mqtt({ok, SuccessCode}, CoapPayload, Format, Ref) ->
|
||||
try
|
||||
Result = coap_content_to_mqtt_payload(CoapPayload, Format, Ref),
|
||||
make_response(SuccessCode, Ref, Format, Result)
|
||||
catch
|
||||
error:not_implemented -> make_response(not_implemented, Ref);
|
||||
C:R:Stack ->
|
||||
?LOG(error, "~p, bad payload format: ~p, stacktrace: ~p", [{C, R}, CoapPayload, Stack]),
|
||||
make_response(bad_request, Ref)
|
||||
end.
|
||||
|
||||
ack2mqtt(Ref) ->
|
||||
make_base_response(Ref).
|
||||
|
||||
coap_content_to_mqtt_payload(CoapPayload, <<"text/plain">>, Ref) ->
|
||||
emqx_lwm2m_message:text_to_json(extract_path(Ref), CoapPayload);
|
||||
coap_content_to_mqtt_payload(CoapPayload, <<"application/octet-stream">>, Ref) ->
|
||||
emqx_lwm2m_message:opaque_to_json(extract_path(Ref), CoapPayload);
|
||||
coap_content_to_mqtt_payload(CoapPayload, <<"application/vnd.oma.lwm2m+tlv">>, Ref) ->
|
||||
emqx_lwm2m_message:tlv_to_json(extract_path(Ref), CoapPayload);
|
||||
coap_content_to_mqtt_payload(CoapPayload, <<"application/vnd.oma.lwm2m+json">>, _Ref) ->
|
||||
emqx_lwm2m_message:translate_json(CoapPayload).
|
||||
|
||||
coap_write_to_mqtt({ok, changed}, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
coap_write_to_mqtt({error, Error}, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
coap_execute_to_mqtt({ok, changed}, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
coap_execute_to_mqtt({error, Error}, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
coap_discover_to_mqtt({ok, content}, CoapPayload, Ref) ->
|
||||
Links = binary:split(CoapPayload, <<",">>),
|
||||
make_response(content, Ref, <<"application/link-format">>, Links);
|
||||
coap_discover_to_mqtt({error, Error}, _CoapPayload, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
coap_writeattr_to_mqtt({ok, changed}, _CoapPayload, Ref) ->
|
||||
make_response(changed, Ref);
|
||||
coap_writeattr_to_mqtt({error, Error}, _CoapPayload, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
coap_observe_to_mqtt({error, Error}, _CoapPayload, _Format, _ObserveSeqNum, Ref) ->
|
||||
make_response(Error, Ref);
|
||||
coap_observe_to_mqtt({ok, content}, CoapPayload, Format, 0, Ref) ->
|
||||
coap_read_to_mqtt({ok, content}, CoapPayload, Format, Ref);
|
||||
coap_observe_to_mqtt({ok, content}, CoapPayload, Format, ObserveSeqNum, Ref) ->
|
||||
RefWithObserve = maps:put(<<"seqNum">>, ObserveSeqNum, Ref),
|
||||
RefNotify = maps:put(<<"msgType">>, <<"notify">>, RefWithObserve),
|
||||
coap_read_to_mqtt({ok, content}, CoapPayload, Format, RefNotify).
|
||||
|
||||
coap_cancel_observe_to_mqtt({ok, content}, CoapPayload, Format, Ref) ->
|
||||
coap_read_to_mqtt({ok, content}, CoapPayload, Format, Ref);
|
||||
coap_cancel_observe_to_mqtt({error, Error}, _CoapPayload, _Format, Ref) ->
|
||||
make_response(Error, Ref).
|
||||
|
||||
make_response(Code, Ref=#{}) ->
|
||||
BaseRsp = make_base_response(Ref),
|
||||
make_data_response(BaseRsp, Code).
|
||||
make_response(Code, Ref=#{}, _Format, Result) ->
|
||||
BaseRsp = make_base_response(Ref),
|
||||
make_data_response(BaseRsp, Code, _Format, Result).
|
||||
|
||||
%% The base response format is what included in the request:
|
||||
%%
|
||||
%% #{
|
||||
%% <<"seqNum">> => SeqNum,
|
||||
%% <<"requestID">> => maps:get(<<"requestID">>, Ref, null),
|
||||
%% <<"cacheID">> => maps:get(<<"cacheID">>, Ref, null),
|
||||
%% <<"msgType">> => maps:get(<<"msgType">>, Ref, null)
|
||||
%% }
|
||||
|
||||
make_base_response(Ref=#{}) ->
|
||||
remove_tmp_fields(Ref).
|
||||
|
||||
make_data_response(BaseRsp, Code) ->
|
||||
BaseRsp#{
|
||||
<<"data">> => #{
|
||||
<<"reqPath">> => extract_path(BaseRsp),
|
||||
<<"code">> => code(Code),
|
||||
<<"codeMsg">> => Code
|
||||
}
|
||||
}.
|
||||
make_data_response(BaseRsp, Code, _Format, Result) ->
|
||||
BaseRsp#{
|
||||
<<"data">> => #{
|
||||
<<"reqPath">> => extract_path(BaseRsp),
|
||||
<<"code">> => code(Code),
|
||||
<<"codeMsg">> => Code,
|
||||
<<"content">> => Result
|
||||
}
|
||||
}.
|
||||
|
||||
remove_tmp_fields(Ref) ->
|
||||
maps:remove(observe_type, Ref).
|
||||
|
||||
path_list(Path) ->
|
||||
case binary:split(binary_util:trim(Path, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, ResId, ResInstId] -> [ObjId, ObjInsId, ResId, ResInstId];
|
||||
[ObjId, ObjInsId, ResId] -> [ObjId, ObjInsId, ResId];
|
||||
[ObjId, ObjInsId] -> [ObjId, ObjInsId];
|
||||
[ObjId] -> [ObjId]
|
||||
end.
|
||||
|
||||
attr_query_list(Data) ->
|
||||
attr_query_list(Data, valid_attr_keys(), []).
|
||||
attr_query_list(QueryJson = #{}, ValidAttrKeys, QueryList) ->
|
||||
maps:fold(
|
||||
fun
|
||||
(_K, null, Acc) -> Acc;
|
||||
(K, V, Acc) ->
|
||||
case lists:member(K, ValidAttrKeys) of
|
||||
true ->
|
||||
Val = bin(V),
|
||||
KV = <<K/binary, "=", Val/binary>>,
|
||||
Acc ++ [KV];
|
||||
false ->
|
||||
Acc
|
||||
end
|
||||
end, QueryList, QueryJson).
|
||||
|
||||
valid_attr_keys() ->
|
||||
[<<"pmin">>, <<"pmax">>, <<"gt">>, <<"lt">>, <<"st">>].
|
||||
|
||||
data_format(Options) ->
|
||||
proplists:get_value(content_format, Options, <<"text/plain">>).
|
||||
observe_seq(Options) ->
|
||||
proplists:get_value(observe, Options, rand:uniform(1000000) + 1 ).
|
||||
|
||||
add_alternate_path_prefix(<<"/">>, PathList) ->
|
||||
PathList;
|
||||
add_alternate_path_prefix(AlternatePath, PathList) ->
|
||||
[binary_util:trim(AlternatePath, $/) | PathList].
|
||||
|
||||
extract_path(Ref = #{}) ->
|
||||
case Ref of
|
||||
#{<<"data">> := Data} ->
|
||||
case maps:get(<<"path">>, Data, nil) of
|
||||
nil -> maps:get(<<"basePath">>, Data, undefined);
|
||||
Path -> Path
|
||||
end;
|
||||
#{<<"path">> := Path} ->
|
||||
Path
|
||||
end.
|
||||
|
||||
batch_write_request(AlternatePath, BasePath, Content, Encoding) ->
|
||||
PathList = path_list(BasePath),
|
||||
Method = case length(PathList) of
|
||||
2 -> post;
|
||||
3 -> put
|
||||
end,
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
Content1 = decoding(Content, Encoding),
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Content1),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
lwm2m_coap_message:request(con, Method, Payload, [{uri_path, FullPathList}, {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]).
|
||||
|
||||
single_write_request(AlternatePath, Data, Encoding) ->
|
||||
PathList = path_list(maps:get(<<"path">>, Data)),
|
||||
FullPathList = add_alternate_path_prefix(AlternatePath, PathList),
|
||||
Datas = decoding([Data], Encoding),
|
||||
TlvData = emqx_lwm2m_message:json_to_tlv(PathList, Datas),
|
||||
Payload = emqx_lwm2m_tlv:encode(TlvData),
|
||||
lwm2m_coap_message:request(con, put, Payload, [{uri_path, FullPathList}, {content_format, <<"application/vnd.oma.lwm2m+tlv">>}]).
|
||||
|
||||
|
||||
code(get) -> <<"0.01">>;
|
||||
code(post) -> <<"0.02">>;
|
||||
code(put) -> <<"0.03">>;
|
||||
code(delete) -> <<"0.04">>;
|
||||
code(created) -> <<"2.01">>;
|
||||
code(deleted) -> <<"2.02">>;
|
||||
code(valid) -> <<"2.03">>;
|
||||
code(changed) -> <<"2.04">>;
|
||||
code(content) -> <<"2.05">>;
|
||||
code(continue) -> <<"2.31">>;
|
||||
code(bad_request) -> <<"4.00">>;
|
||||
code(uauthorized) -> <<"4.01">>;
|
||||
code(bad_option) -> <<"4.02">>;
|
||||
code(forbidden) -> <<"4.03">>;
|
||||
code(not_found) -> <<"4.04">>;
|
||||
code(method_not_allowed) -> <<"4.05">>;
|
||||
code(not_acceptable) -> <<"4.06">>;
|
||||
code(request_entity_incomplete) -> <<"4.08">>;
|
||||
code(precondition_failed) -> <<"4.12">>;
|
||||
code(request_entity_too_large) -> <<"4.13">>;
|
||||
code(unsupported_content_format) -> <<"4.15">>;
|
||||
code(internal_server_error) -> <<"5.00">>;
|
||||
code(not_implemented) -> <<"5.01">>;
|
||||
code(bad_gateway) -> <<"5.02">>;
|
||||
code(service_unavailable) -> <<"5.03">>;
|
||||
code(gateway_timeout) -> <<"5.04">>;
|
||||
code(proxying_not_supported) -> <<"5.05">>.
|
||||
|
||||
bin(Bin) when is_binary(Bin) -> Bin;
|
||||
bin(Str) when is_list(Str) -> list_to_binary(Str);
|
||||
bin(Int) when is_integer(Int) -> integer_to_binary(Int);
|
||||
bin(Float) when is_float(Float) -> float_to_binary(Float).
|
||||
|
||||
decoding(Datas, <<"hex">>) ->
|
||||
lists:map(fun(Data = #{<<"value">> := Value}) ->
|
||||
Data#{<<"value">> => emqx_misc:hexstr2bin(Value)}
|
||||
end, Datas);
|
||||
decoding(Datas, _) ->
|
||||
Datas.
|
|
@ -1,386 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_lwm2m_coap_resource).
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
-include_lib("lwm2m_coap/include/coap.hrl").
|
||||
|
||||
% -behaviour(lwm2m_coap_resource).
|
||||
|
||||
-export([ coap_discover/2
|
||||
, coap_get/5
|
||||
, coap_post/5
|
||||
, coap_put/5
|
||||
, coap_delete/4
|
||||
, coap_observe/5
|
||||
, coap_unobserve/1
|
||||
, coap_response/7
|
||||
, coap_ack/3
|
||||
, handle_info/2
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, terminate/2
|
||||
]).
|
||||
|
||||
-export([parse_object_list/1]).
|
||||
|
||||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
-define(PREFIX, <<"rd">>).
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-RESOURCE: " ++ Format, Args)).
|
||||
|
||||
-dialyzer([{nowarn_function, [coap_discover/2]}]).
|
||||
% we use {'absolute', list(binary()), [{atom(), binary()}]} as coap_uri()
|
||||
% https://github.com/emqx/lwm2m-coap/blob/258e9bd3762124395e83c1e68a1583b84718230f/src/lwm2m_coap_resource.erl#L61
|
||||
% resource operations
|
||||
coap_discover(_Prefix, _Args) ->
|
||||
[{absolute, [<<"mqtt">>], []}].
|
||||
|
||||
coap_get(ChId, [?PREFIX], Query, Content, Lwm2mState) ->
|
||||
?LOG(debug, "~p ~p GET Query=~p, Content=~p", [self(),ChId, Query, Content]),
|
||||
{ok, #coap_content{}, Lwm2mState};
|
||||
coap_get(ChId, Prefix, Query, Content, Lwm2mState) ->
|
||||
?LOG(error, "ignore bad put request ChId=~p, Prefix=~p, Query=~p, Content=~p", [ChId, Prefix, Query, Content]),
|
||||
{error, bad_request, Lwm2mState}.
|
||||
|
||||
% LWM2M REGISTER COMMAND
|
||||
coap_post(ChId, [?PREFIX], Query, Content = #coap_content{uri_path = [?PREFIX]}, Lwm2mState) ->
|
||||
?LOG(debug, "~p ~p REGISTER command Query=~p, Content=~p", [self(), ChId, Query, Content]),
|
||||
case parse_options(Query) of
|
||||
{error, {bad_opt, _CustomOption}} ->
|
||||
?LOG(error, "Reject REGISTER from ~p due to wrong option", [ChId]),
|
||||
{error, bad_request, Lwm2mState};
|
||||
{ok, LwM2MQuery} ->
|
||||
process_register(ChId, LwM2MQuery, Content#coap_content.payload, Lwm2mState)
|
||||
end;
|
||||
|
||||
% LWM2M UPDATE COMMAND
|
||||
coap_post(ChId, [?PREFIX], Query, Content = #coap_content{uri_path = LocationPath}, Lwm2mState) ->
|
||||
?LOG(debug, "~p ~p UPDATE command location=~p, Query=~p, Content=~p", [self(), ChId, LocationPath, Query, Content]),
|
||||
case parse_options(Query) of
|
||||
{error, {bad_opt, _CustomOption}} ->
|
||||
?LOG(error, "Reject UPDATE from ~p due to wrong option, Query=~p", [ChId, Query]),
|
||||
{error, bad_request, Lwm2mState};
|
||||
{ok, LwM2MQuery} ->
|
||||
process_update(ChId, LwM2MQuery, LocationPath, Content#coap_content.payload, Lwm2mState)
|
||||
end;
|
||||
|
||||
coap_post(ChId, Prefix, Query, Content, Lwm2mState) ->
|
||||
?LOG(error, "bad post request ChId=~p, Prefix=~p, Query=~p, Content=~p", [ChId, Prefix, Query, Content]),
|
||||
{error, bad_request, Lwm2mState}.
|
||||
|
||||
coap_put(_ChId, Prefix, Query, Content, Lwm2mState) ->
|
||||
?LOG(error, "put has error, Prefix=~p, Query=~p, Content=~p", [Prefix, Query, Content]),
|
||||
{error, bad_request, Lwm2mState}.
|
||||
|
||||
% LWM2M DE-REGISTER COMMAND
|
||||
coap_delete(ChId, [?PREFIX], #coap_content{uri_path = Location}, Lwm2mState) ->
|
||||
LocationPath = binary_util:join_path(Location),
|
||||
?LOG(debug, "~p ~p DELETE command location=~p", [self(), ChId, LocationPath]),
|
||||
case get(lwm2m_context) of
|
||||
#lwm2m_context{location = LocationPath} ->
|
||||
lwm2m_coap_responder:stop(deregister),
|
||||
{ok, Lwm2mState};
|
||||
undefined ->
|
||||
?LOG(error, "Reject DELETE from ~p, Location: ~p not found", [ChId, Location]),
|
||||
{error, forbidden, Lwm2mState};
|
||||
TrueLocation ->
|
||||
?LOG(error, "Reject DELETE from ~p, Wrong Location: ~p, registered location record: ~p", [ChId, Location, TrueLocation]),
|
||||
{error, not_found, Lwm2mState}
|
||||
end;
|
||||
coap_delete(_ChId, _Prefix, _Content, Lwm2mState) ->
|
||||
{error, forbidden, Lwm2mState}.
|
||||
|
||||
coap_observe(ChId, Prefix, Name, Ack, Lwm2mState) ->
|
||||
?LOG(error, "unsupported observe request ChId=~p, Prefix=~p, Name=~p, Ack=~p", [ChId, Prefix, Name, Ack]),
|
||||
{error, method_not_allowed, Lwm2mState}.
|
||||
|
||||
coap_unobserve(Lwm2mState) ->
|
||||
?LOG(error, "unsupported unobserve request: ~p", [Lwm2mState]),
|
||||
{ok, Lwm2mState}.
|
||||
|
||||
coap_response(ChId, Ref, CoapMsgType, CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Lwm2mState) ->
|
||||
?LOG(info, "~p, RCV CoAP response, CoapMsgType: ~p, CoapMsgMethod: ~p, CoapMsgPayload: ~p,
|
||||
CoapMsgOpts: ~p, Ref: ~p",
|
||||
[ChId, CoapMsgType, CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ref]),
|
||||
MqttPayload = emqx_lwm2m_cmd_handler:coap2mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ref),
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:send_ul_data(maps:get(<<"msgType">>, MqttPayload), MqttPayload, Lwm2mState),
|
||||
{noreply, Lwm2mState2}.
|
||||
|
||||
coap_ack(_ChId, Ref, Lwm2mState) ->
|
||||
?LOG(info, "~p, RCV CoAP Empty ACK, Ref: ~p", [_ChId, Ref]),
|
||||
AckRef = maps:put(<<"msgType">>, <<"ack">>, Ref),
|
||||
MqttPayload = emqx_lwm2m_cmd_handler:ack2mqtt(AckRef),
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:send_ul_data(maps:get(<<"msgType">>, MqttPayload), MqttPayload, Lwm2mState),
|
||||
{ok, Lwm2mState2}.
|
||||
|
||||
%% Batch deliver
|
||||
handle_info({deliver, Topic, Msgs}, Lwm2mState) when is_list(Msgs) ->
|
||||
{noreply, lists:foldl(fun(Msg, NewState) ->
|
||||
element(2, handle_info({deliver, Topic, Msg}, NewState))
|
||||
end, Lwm2mState, Msgs)};
|
||||
%% Handle MQTT Message
|
||||
handle_info({deliver, _Topic, MqttMsg}, Lwm2mState) ->
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:deliver(MqttMsg, Lwm2mState),
|
||||
{noreply, Lwm2mState2};
|
||||
|
||||
%% Deliver Coap Message to Device
|
||||
handle_info({deliver_to_coap, CoapRequest, Ref}, Lwm2mState) ->
|
||||
{send_request, CoapRequest, Ref, Lwm2mState};
|
||||
|
||||
handle_info({'EXIT', _Pid, Reason}, Lwm2mState) ->
|
||||
?LOG(info, "~p, received exit from: ~p, reason: ~p, quit now!", [self(), _Pid, Reason]),
|
||||
{stop, Reason, Lwm2mState};
|
||||
|
||||
handle_info(post_init, Lwm2mState) ->
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:post_init(Lwm2mState),
|
||||
{noreply, Lwm2mState2};
|
||||
|
||||
handle_info(auto_observe, Lwm2mState) ->
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:auto_observe(Lwm2mState),
|
||||
{noreply, Lwm2mState2};
|
||||
|
||||
handle_info({life_timer, expired}, Lwm2mState) ->
|
||||
?LOG(debug, "lifetime expired, shutdown", []),
|
||||
{stop, life_timer_expired, Lwm2mState};
|
||||
|
||||
handle_info({shutdown, Error}, Lwm2mState) ->
|
||||
{stop, Error, Lwm2mState};
|
||||
|
||||
handle_info({shutdown, conflict, {ClientId, NewPid}}, Lwm2mState) ->
|
||||
?LOG(warning, "lwm2m '~s' conflict with ~p, shutdown", [ClientId, NewPid]),
|
||||
{stop, conflict, Lwm2mState};
|
||||
|
||||
handle_info({suback, _MsgId, [_GrantedQos]}, Lwm2mState) ->
|
||||
{noreply, Lwm2mState};
|
||||
|
||||
handle_info(emit_stats, Lwm2mState) ->
|
||||
{noreply, Lwm2mState};
|
||||
|
||||
handle_info(Message, Lwm2mState) ->
|
||||
?LOG(error, "Unknown Message ~p", [Message]),
|
||||
{noreply, Lwm2mState}.
|
||||
|
||||
|
||||
handle_call(info, _From, Lwm2mState) ->
|
||||
{Info, Lwm2mState2} = emqx_lwm2m_protocol:get_info(Lwm2mState),
|
||||
{reply, Info, Lwm2mState2};
|
||||
|
||||
handle_call(stats, _From, Lwm2mState) ->
|
||||
{Stats, Lwm2mState2} = emqx_lwm2m_protocol:get_stats(Lwm2mState),
|
||||
{reply, Stats, Lwm2mState2};
|
||||
|
||||
handle_call(kick, _From, Lwm2mState) ->
|
||||
{stop, kick, Lwm2mState};
|
||||
|
||||
handle_call({set_rate_limit, _Rl}, _From, Lwm2mState) ->
|
||||
?LOG(error, "set_rate_limit is not support", []),
|
||||
{reply, ok, Lwm2mState};
|
||||
|
||||
handle_call(get_rate_limit, _From, Lwm2mState) ->
|
||||
?LOG(error, "get_rate_limit is not support", []),
|
||||
{reply, ok, Lwm2mState};
|
||||
|
||||
handle_call(session, _From, Lwm2mState) ->
|
||||
?LOG(error, "get_session is not support", []),
|
||||
{reply, ok, Lwm2mState};
|
||||
|
||||
handle_call(Request, _From, Lwm2mState) ->
|
||||
?LOG(error, "adapter unexpected call ~p", [Request]),
|
||||
{reply, ok, Lwm2mState}.
|
||||
|
||||
handle_cast(Msg, Lwm2mState) ->
|
||||
?LOG(error, "unexpected cast ~p", [Msg]),
|
||||
{noreply, Lwm2mState, hibernate}.
|
||||
|
||||
terminate(Reason, Lwm2mState) ->
|
||||
emqx_lwm2m_protocol:terminate(Reason, Lwm2mState).
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%
|
||||
%% Internal Functions
|
||||
%%%%%%%%%%%%%%%%%%%%%%
|
||||
process_register(ChId, LwM2MQuery, LwM2MPayload, Lwm2mState) ->
|
||||
Epn = maps:get(<<"ep">>, LwM2MQuery, undefined),
|
||||
LifeTime = maps:get(<<"lt">>, LwM2MQuery, undefined),
|
||||
Ver = maps:get(<<"lwm2m">>, LwM2MQuery, undefined),
|
||||
case check_lwm2m_version(Ver) of
|
||||
false ->
|
||||
?LOG(error, "Reject REGISTER from ~p due to unsupported version: ~p", [ChId, Ver]),
|
||||
lwm2m_coap_responder:stop(invalid_version),
|
||||
{error, precondition_failed, Lwm2mState};
|
||||
true ->
|
||||
case check_epn(Epn) andalso check_lifetime(LifeTime) of
|
||||
true ->
|
||||
init_lwm2m_emq_client(ChId, LwM2MQuery, LwM2MPayload, Lwm2mState);
|
||||
false ->
|
||||
?LOG(error, "Reject REGISTER from ~p due to wrong parameters, epn=~p, lifetime=~p", [ChId, Epn, LifeTime]),
|
||||
lwm2m_coap_responder:stop(invalid_query_params),
|
||||
{error, bad_request, Lwm2mState}
|
||||
end
|
||||
end.
|
||||
|
||||
process_update(ChId, LwM2MQuery, Location, LwM2MPayload, Lwm2mState) ->
|
||||
LocationPath = binary_util:join_path(Location),
|
||||
case get(lwm2m_context) of
|
||||
#lwm2m_context{location = LocationPath} ->
|
||||
RegInfo = append_object_list(LwM2MQuery, LwM2MPayload),
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:update_reg_info(RegInfo, Lwm2mState),
|
||||
?LOG(info, "~p, UPDATE Success, assgined location: ~p", [ChId, LocationPath]),
|
||||
{ok, changed, #coap_content{}, Lwm2mState2};
|
||||
undefined ->
|
||||
?LOG(error, "Reject UPDATE from ~p, Location: ~p not found", [ChId, Location]),
|
||||
{error, forbidden, Lwm2mState};
|
||||
TrueLocation ->
|
||||
?LOG(error, "Reject UPDATE from ~p, Wrong Location: ~p, registered location record: ~p", [ChId, Location, TrueLocation]),
|
||||
{error, not_found, Lwm2mState}
|
||||
end.
|
||||
|
||||
init_lwm2m_emq_client(ChId, LwM2MQuery = #{<<"ep">> := Epn}, LwM2MPayload, _Lwm2mState = undefined) ->
|
||||
RegInfo = append_object_list(LwM2MQuery, LwM2MPayload),
|
||||
case emqx_lwm2m_protocol:init(self(), Epn, ChId, RegInfo) of
|
||||
{ok, Lwm2mState} ->
|
||||
LocationPath = assign_location_path(Epn),
|
||||
?LOG(info, "~p, REGISTER Success, assgined location: ~p", [ChId, LocationPath]),
|
||||
{ok, created, #coap_content{location_path = LocationPath}, Lwm2mState};
|
||||
{error, Error} ->
|
||||
lwm2m_coap_responder:stop(Error),
|
||||
?LOG(error, "~p, REGISTER Failed, error: ~p", [ChId, Error]),
|
||||
{error, forbidden, undefined}
|
||||
end;
|
||||
init_lwm2m_emq_client(ChId, LwM2MQuery = #{<<"ep">> := Epn}, LwM2MPayload, Lwm2mState) ->
|
||||
RegInfo = append_object_list(LwM2MQuery, LwM2MPayload),
|
||||
LocationPath = assign_location_path(Epn),
|
||||
?LOG(info, "~p, RE-REGISTER Success, location: ~p", [ChId, LocationPath]),
|
||||
Lwm2mState2 = emqx_lwm2m_protocol:replace_reg_info(RegInfo, Lwm2mState),
|
||||
{ok, created, #coap_content{location_path = LocationPath}, Lwm2mState2}.
|
||||
|
||||
append_object_list(LwM2MQuery, <<>>) when map_size(LwM2MQuery) == 0 -> #{};
|
||||
append_object_list(LwM2MQuery, <<>>) -> LwM2MQuery;
|
||||
append_object_list(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) ->
|
||||
{AlterPath, ObjList} = parse_object_list(LwM2MPayload),
|
||||
LwM2MQuery#{
|
||||
<<"alternatePath">> => AlterPath,
|
||||
<<"objectList">> => ObjList
|
||||
}.
|
||||
|
||||
parse_options(InputQuery) ->
|
||||
parse_options(InputQuery, maps:new()).
|
||||
|
||||
parse_options([], Query) -> {ok, Query};
|
||||
parse_options([<<"ep=", Epn/binary>>|T], Query) ->
|
||||
parse_options(T, maps:put(<<"ep">>, Epn, Query));
|
||||
parse_options([<<"lt=", Lt/binary>>|T], Query) ->
|
||||
parse_options(T, maps:put(<<"lt">>, binary_to_integer(Lt), Query));
|
||||
parse_options([<<"lwm2m=", Ver/binary>>|T], Query) ->
|
||||
parse_options(T, maps:put(<<"lwm2m">>, Ver, Query));
|
||||
parse_options([<<"b=", Binding/binary>>|T], Query) ->
|
||||
parse_options(T, maps:put(<<"b">>, Binding, Query));
|
||||
parse_options([CustomOption|T], Query) ->
|
||||
case binary:split(CustomOption, <<"=">>) of
|
||||
[OptKey, OptValue] when OptKey =/= <<>> ->
|
||||
?LOG(debug, "non-standard option: ~p", [CustomOption]),
|
||||
parse_options(T, maps:put(OptKey, OptValue, Query));
|
||||
_BadOpt ->
|
||||
?LOG(error, "bad option: ~p", [CustomOption]),
|
||||
{error, {bad_opt, CustomOption}}
|
||||
end.
|
||||
|
||||
parse_object_list(<<>>) -> {<<"/">>, <<>>};
|
||||
parse_object_list(ObjLinks) when is_binary(ObjLinks) ->
|
||||
parse_object_list(binary:split(ObjLinks, <<",">>, [global]));
|
||||
|
||||
parse_object_list(FullObjLinkList) when is_list(FullObjLinkList) ->
|
||||
case drop_attr(FullObjLinkList) of
|
||||
{<<"/">>, _} = RootPrefixedLinks ->
|
||||
RootPrefixedLinks;
|
||||
{AlterPath, ObjLinkList} ->
|
||||
LenAlterPath = byte_size(AlterPath),
|
||||
WithOutPrefix =
|
||||
lists:map(
|
||||
fun
|
||||
(<<Prefix:LenAlterPath/binary, Link/binary>>) when Prefix =:= AlterPath ->
|
||||
trim(Link);
|
||||
(Link) -> Link
|
||||
end, ObjLinkList),
|
||||
{AlterPath, WithOutPrefix}
|
||||
end.
|
||||
|
||||
drop_attr(LinkList) ->
|
||||
lists:foldr(
|
||||
fun(Link, {AlternatePath, LinkAcc}) ->
|
||||
{MainLink, LinkAttrs} = parse_link(Link),
|
||||
case is_alternate_path(LinkAttrs) of
|
||||
false -> {AlternatePath, [MainLink | LinkAcc]};
|
||||
true -> {MainLink, LinkAcc}
|
||||
end
|
||||
end, {<<"/">>, []}, LinkList).
|
||||
|
||||
is_alternate_path(#{<<"rt">> := ?OMA_ALTER_PATH_RT}) -> true;
|
||||
is_alternate_path(_) -> false.
|
||||
|
||||
parse_link(Link) ->
|
||||
[MainLink | Attrs] = binary:split(trim(Link), <<";">>, [global]),
|
||||
{delink(trim(MainLink)), parse_link_attrs(Attrs)}.
|
||||
|
||||
parse_link_attrs(LinkAttrs) when is_list(LinkAttrs) ->
|
||||
lists:foldl(
|
||||
fun(Attr, Acc) ->
|
||||
case binary:split(trim(Attr), <<"=">>) of
|
||||
[AttrKey, AttrValue] when AttrKey =/= <<>> ->
|
||||
maps:put(AttrKey, AttrValue, Acc);
|
||||
_BadAttr -> throw({bad_attr, _BadAttr})
|
||||
end
|
||||
end, maps:new(), LinkAttrs).
|
||||
|
||||
trim(Str)-> binary_util:trim(Str, $ ).
|
||||
delink(Str) ->
|
||||
Ltrim = binary_util:ltrim(Str, $<),
|
||||
binary_util:rtrim(Ltrim, $>).
|
||||
|
||||
check_lwm2m_version(<<"1">>) -> true;
|
||||
check_lwm2m_version(<<"1.", _PatchVerNum/binary>>) -> true;
|
||||
check_lwm2m_version(_) -> false.
|
||||
|
||||
check_epn(undefined) -> false;
|
||||
check_epn(_) -> true.
|
||||
|
||||
check_lifetime(undefined) -> false;
|
||||
check_lifetime(LifeTime0) when is_integer(LifeTime0) ->
|
||||
LifeTime = timer:seconds(LifeTime0),
|
||||
Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}),
|
||||
Max = maps:get(lifetime_max, Envs, 315360000),
|
||||
Min = maps:get(lifetime_min, Envs, 0),
|
||||
|
||||
if
|
||||
LifeTime >= Min, LifeTime =< Max ->
|
||||
true;
|
||||
true ->
|
||||
false
|
||||
end;
|
||||
check_lifetime(_) -> false.
|
||||
|
||||
|
||||
assign_location_path(Epn) ->
|
||||
%Location = list_to_binary(io_lib:format("~.16B", [rand:uniform(65535)])),
|
||||
%LocationPath = <<"/rd/", Location/binary>>,
|
||||
Location = [<<"rd">>, Epn],
|
||||
put(lwm2m_context, #lwm2m_context{epn = Epn, location = binary_util:join_path(Location)}),
|
||||
Location.
|
|
@ -50,24 +50,13 @@ unreg() ->
|
|||
on_gateway_load(_Gateway = #{ name := GwName,
|
||||
config := Config
|
||||
}, Ctx) ->
|
||||
|
||||
%% Handler
|
||||
_ = lwm2m_coap_server:start_registry(),
|
||||
lwm2m_coap_server_registry:add_handler(
|
||||
[<<"rd">>],
|
||||
emqx_lwm2m_coap_resource, undefined
|
||||
),
|
||||
%% Xml registry
|
||||
{ok, _} = emqx_lwm2m_xml_object_db:start_link(maps:get(xml_dir, Config)),
|
||||
|
||||
%% XXX: Self managed table?
|
||||
%% TODO: Improve it later
|
||||
{ok, _} = emqx_lwm2m_cm:start_link(),
|
||||
|
||||
Listeners = emqx_gateway_utils:normalize_config(Config),
|
||||
ListenerPids = lists:map(fun(Lis) ->
|
||||
start_listener(GwName, Ctx, Lis)
|
||||
end, Listeners),
|
||||
start_listener(GwName, Ctx, Lis)
|
||||
end, Listeners),
|
||||
{ok, ListenerPids, _GwState = #{ctx => Ctx}}.
|
||||
|
||||
on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) ->
|
||||
|
@ -88,12 +77,6 @@ on_gateway_update(NewGateway, OldGateway, GwState = #{ctx := Ctx}) ->
|
|||
on_gateway_unload(_Gateway = #{ name := GwName,
|
||||
config := Config
|
||||
}, _GwState) ->
|
||||
%% XXX:
|
||||
lwm2m_coap_server_registry:remove_handler(
|
||||
[<<"rd">>],
|
||||
emqx_lwm2m_coap_resource, undefined
|
||||
),
|
||||
|
||||
Listeners = emqx_gateway_utils:normalize_config(Config),
|
||||
lists:foreach(fun(Lis) ->
|
||||
stop_listener(GwName, Lis)
|
||||
|
@ -118,18 +101,13 @@ start_listener(GwName, Ctx, {Type, LisName, ListenOn, SocketOpts, Cfg}) ->
|
|||
|
||||
start_listener(GwName, Ctx, Type, LisName, ListenOn, SocketOpts, Cfg) ->
|
||||
Name = name(GwName, LisName, udp),
|
||||
NCfg = Cfg#{ctx => Ctx},
|
||||
NCfg = Cfg#{ ctx => Ctx
|
||||
, frame_mod => emqx_coap_frame
|
||||
, chann_mod => emqx_lwm2m_channel
|
||||
},
|
||||
NSocketOpts = merge_default(SocketOpts),
|
||||
Options = [{config, NCfg}|NSocketOpts],
|
||||
case Type of
|
||||
udp ->
|
||||
lwm2m_coap_server:start_udp(Name, ListenOn, Options);
|
||||
dtls ->
|
||||
lwm2m_coap_server:start_dtls(Name, ListenOn, Options)
|
||||
end.
|
||||
|
||||
name(GwName, LisName, Type) ->
|
||||
list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])).
|
||||
MFA = {emqx_gateway_conn, start_link, [NCfg]},
|
||||
do_start_listener(Type, Name, ListenOn, NSocketOpts, MFA).
|
||||
|
||||
merge_default(Options) ->
|
||||
Default = emqx_gateway_utils:default_udp_options(),
|
||||
|
@ -141,6 +119,16 @@ merge_default(Options) ->
|
|||
[{udp_options, Default} | Options]
|
||||
end.
|
||||
|
||||
name(GwName, LisName, Type) ->
|
||||
list_to_atom(lists:concat([GwName, ":", Type, ":", LisName])).
|
||||
|
||||
do_start_listener(udp, Name, ListenOn, SocketOpts, MFA) ->
|
||||
esockd:open_udp(Name, ListenOn, SocketOpts, MFA);
|
||||
|
||||
do_start_listener(dtls, Name, ListenOn, SocketOpts, MFA) ->
|
||||
esockd:open_dtls(Name, ListenOn, SocketOpts, MFA).
|
||||
|
||||
|
||||
stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) ->
|
||||
StopRet = stop_listener(GwName, Type, LisName, ListenOn, SocketOpts, Cfg),
|
||||
ListenOnStr = emqx_gateway_utils:format_listenon(ListenOn),
|
||||
|
@ -155,9 +143,4 @@ stop_listener(GwName, {Type, LisName, ListenOn, SocketOpts, Cfg}) ->
|
|||
|
||||
stop_listener(GwName, Type, LisName, ListenOn, _SocketOpts, _Cfg) ->
|
||||
Name = name(GwName, LisName, Type),
|
||||
case Type of
|
||||
udp ->
|
||||
lwm2m_coap_server:stop_udp(Name, ListenOn);
|
||||
dtls ->
|
||||
lwm2m_coap_server:stop_dtls(Name, ListenOn)
|
||||
end.
|
||||
esockd:close(Name, ListenOn).
|
||||
|
|
|
@ -1,351 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_lwm2m_json).
|
||||
|
||||
-export([ tlv_to_json/2
|
||||
, json_to_tlv/2
|
||||
, text_to_json/2
|
||||
, opaque_to_json/2
|
||||
]).
|
||||
|
||||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-JSON: " ++ Format, Args)).
|
||||
|
||||
tlv_to_json(BaseName, TlvData) ->
|
||||
DecodedTlv = emqx_lwm2m_tlv:parse(TlvData),
|
||||
ObjectId = object_id(BaseName),
|
||||
ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true),
|
||||
case DecodedTlv of
|
||||
[#{tlv_resource_with_value:=Id, value:=Value}] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, Id, 3),
|
||||
encode_json(TrueBaseName, tlv_single_resource(Id, Value, ObjDefinition));
|
||||
List1 = [#{tlv_resource_with_value:=_Id}, _|_] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2),
|
||||
encode_json(TrueBaseName, tlv_level2(<<>>, List1, ObjDefinition, []));
|
||||
List2 = [#{tlv_multiple_resource:=_Id}|_] ->
|
||||
TrueBaseName = basename(BaseName, undefined, undefined, undefined, 2),
|
||||
encode_json(TrueBaseName, tlv_level2(<<>>, List2, ObjDefinition, []));
|
||||
[#{tlv_object_instance:=Id, value:=Value}] ->
|
||||
TrueBaseName = basename(BaseName, undefined, Id, undefined, 2),
|
||||
encode_json(TrueBaseName, tlv_level2(<<>>, Value, ObjDefinition, []));
|
||||
List3=[#{tlv_object_instance:=Id, value:=_Value}, _|_] ->
|
||||
TrueBaseName = basename(BaseName, Id, undefined, undefined, 1),
|
||||
encode_json(TrueBaseName, tlv_level1(List3, ObjDefinition, []))
|
||||
end.
|
||||
|
||||
|
||||
tlv_level1([], _ObjDefinition, Acc) ->
|
||||
Acc;
|
||||
tlv_level1([#{tlv_object_instance:=Id, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
New = tlv_level2(integer_to_binary(Id), Value, ObjDefinition, []),
|
||||
tlv_level1(T, ObjDefinition, Acc++New).
|
||||
|
||||
tlv_level2(_, [], _, Acc) ->
|
||||
Acc;
|
||||
tlv_level2(RelativePath, [#{tlv_resource_with_value:=ResourceId, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
{K, V} = value(Value, ResourceId, ObjDefinition),
|
||||
Name = name(RelativePath, ResourceId),
|
||||
New = #{n => Name, K => V},
|
||||
tlv_level2(RelativePath, T, ObjDefinition, Acc++[New]);
|
||||
tlv_level2(RelativePath, [#{tlv_multiple_resource:=ResourceId, value:=Value}|T], ObjDefinition, Acc) ->
|
||||
NewRelativePath = name(RelativePath, ResourceId),
|
||||
SubList = tlv_level3(NewRelativePath, Value, ResourceId, ObjDefinition, []),
|
||||
tlv_level2(RelativePath, T, ObjDefinition, Acc++SubList).
|
||||
|
||||
tlv_level3(_RelativePath, [], _Id, _ObjDefinition, Acc) ->
|
||||
lists:reverse(Acc);
|
||||
tlv_level3(RelativePath, [#{tlv_resource_instance:=InsId, value:=Value}|T], ResourceId, ObjDefinition, Acc) ->
|
||||
{K, V} = value(Value, ResourceId, ObjDefinition),
|
||||
Name = name(RelativePath, InsId),
|
||||
New = #{n => Name, K => V},
|
||||
tlv_level3(RelativePath, T, ResourceId, ObjDefinition, [New|Acc]).
|
||||
|
||||
tlv_single_resource(Id, Value, ObjDefinition) ->
|
||||
{K, V} = value(Value, Id, ObjDefinition),
|
||||
[#{K=>V}].
|
||||
|
||||
basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 3) ->
|
||||
?LOG(debug, "basename3 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]),
|
||||
case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, ResId/binary>>;
|
||||
[ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary, $/, (integer_to_binary(ResourceId))/binary>>;
|
||||
[ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary, $/, (integer_to_binary(ResourceId))/binary>>
|
||||
end;
|
||||
basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 2) ->
|
||||
?LOG(debug, "basename2 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]),
|
||||
case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId, ObjInsId, _ResId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>;
|
||||
[ObjId, ObjInsId] -> <<$/, ObjId/binary, $/, ObjInsId/binary>>;
|
||||
[ObjId] -> <<$/, ObjId/binary, $/, (integer_to_binary(ObjectInstanceId))/binary>>
|
||||
end;
|
||||
basename(OldBaseName, ObjectId, ObjectInstanceId, ResourceId, 1) ->
|
||||
?LOG(debug, "basename1 OldBaseName=~p, ObjectId=~p, ObjectInstanceId=~p, ResourceId=~p", [OldBaseName, ObjectId, ObjectInstanceId, ResourceId]),
|
||||
case binary:split(binary_util:trim(OldBaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId, _ObjInsId, _ResId] -> <<$/, ObjId/binary>>;
|
||||
[ObjId, _ObjInsId] -> <<$/, ObjId/binary>>;
|
||||
[ObjId] -> <<$/, ObjId/binary>>
|
||||
end.
|
||||
|
||||
|
||||
name(RelativePath, Id) ->
|
||||
case RelativePath of
|
||||
<<>> -> integer_to_binary(Id);
|
||||
_ -> <<RelativePath/binary, $/, (integer_to_binary(Id))/binary>>
|
||||
end.
|
||||
|
||||
|
||||
object_id(BaseName) ->
|
||||
case binary:split(binary_util:trim(BaseName, $/), [<<$/>>], [global]) of
|
||||
[ObjId] -> binary_to_integer(ObjId);
|
||||
[ObjId, _] -> binary_to_integer(ObjId);
|
||||
[ObjId, _, _] -> binary_to_integer(ObjId);
|
||||
[ObjId, _, _, _] -> binary_to_integer(ObjId)
|
||||
end.
|
||||
|
||||
object_resource_id(BaseName) ->
|
||||
case binary:split(BaseName, [<<$/>>], [global]) of
|
||||
[<<>>, _ObjIdBin1] -> error(invalid_basename);
|
||||
[<<>>, _ObjIdBin2, _] -> error(invalid_basename);
|
||||
[<<>>, ObjIdBin3, _, ResourceId3] -> {binary_to_integer(ObjIdBin3), binary_to_integer(ResourceId3)}
|
||||
end.
|
||||
|
||||
% TLV binary to json text
|
||||
value(Value, ResourceId, ObjDefinition) ->
|
||||
case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of
|
||||
"String" ->
|
||||
{sv, Value}; % keep binary type since it is same as a string for jsx
|
||||
"Integer" ->
|
||||
Size = byte_size(Value)*8,
|
||||
<<IntResult:Size>> = Value,
|
||||
{v, IntResult};
|
||||
"Float" ->
|
||||
Size = byte_size(Value)*8,
|
||||
<<FloatResult:Size/float>> = Value,
|
||||
{v, FloatResult};
|
||||
"Boolean" ->
|
||||
B = case Value of
|
||||
<<0>> -> false;
|
||||
<<1>> -> true
|
||||
end,
|
||||
{bv, B};
|
||||
"Opaque" ->
|
||||
{sv, base64:decode(Value)};
|
||||
"Time" ->
|
||||
Size = byte_size(Value)*8,
|
||||
<<IntResult:Size>> = Value,
|
||||
{v, IntResult};
|
||||
"Objlnk" ->
|
||||
<<ObjId:16, ObjInsId:16>> = Value,
|
||||
{ov, list_to_binary(io_lib:format("~b:~b", [ObjId, ObjInsId]))}
|
||||
end.
|
||||
|
||||
|
||||
encode_json(BaseName, E) ->
|
||||
?LOG(debug, "encode_json BaseName=~p, E=~p", [BaseName, E]),
|
||||
#{bn=>BaseName, e=>E}.
|
||||
|
||||
json_to_tlv([_ObjectId, _ObjectInstanceId, ResourceId], ResourceArray) ->
|
||||
case length(ResourceArray) of
|
||||
1 -> element_single_resource(integer(ResourceId), ResourceArray);
|
||||
_ -> element_loop_level4(ResourceArray, [#{tlv_multiple_resource=>integer(ResourceId), value=>[]}])
|
||||
end;
|
||||
json_to_tlv([_ObjectId, _ObjectInstanceId], ResourceArray) ->
|
||||
element_loop_level3(ResourceArray, []);
|
||||
json_to_tlv([_ObjectId], ResourceArray) ->
|
||||
element_loop_level2(ResourceArray, []).
|
||||
|
||||
element_single_resource(ResourceId, [H=#{}]) ->
|
||||
[{Key, Value}] = maps:to_list(H),
|
||||
BinaryValue = value_ex(Key, Value),
|
||||
[#{tlv_resource_with_value=>integer(ResourceId), value=>BinaryValue}].
|
||||
|
||||
element_loop_level2([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level2([H|T], Acc) ->
|
||||
NewAcc = insert(object, H, Acc),
|
||||
element_loop_level2(T, NewAcc).
|
||||
|
||||
element_loop_level3([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level3([H|T], Acc) ->
|
||||
NewAcc = insert(object_instance, H, Acc),
|
||||
element_loop_level3(T, NewAcc).
|
||||
|
||||
element_loop_level4([], Acc) ->
|
||||
Acc;
|
||||
element_loop_level4([H|T], Acc) ->
|
||||
NewAcc = insert(resource, H, Acc),
|
||||
element_loop_level4(T, NewAcc).
|
||||
|
||||
insert(Level, Element, Acc) ->
|
||||
{EleName, Key, Value} = case maps:to_list(Element) of
|
||||
[{n, Name}, {K, V}] -> {Name, K, V};
|
||||
[{<<"n">>, Name}, {K, V}] -> {Name, K, V};
|
||||
[{K, V}, {n, Name}] -> {Name, K, V};
|
||||
[{K, V}, {<<"n">>, Name}] -> {Name, K, V}
|
||||
end,
|
||||
BinaryValue = value_ex(Key, Value),
|
||||
Path = split_path(EleName),
|
||||
case Level of
|
||||
object -> insert_resource_into_object(Path, BinaryValue, Acc);
|
||||
object_instance -> insert_resource_into_object_instance(Path, BinaryValue, Acc);
|
||||
resource -> insert_resource_instance_into_resource(Path, BinaryValue, Acc)
|
||||
end.
|
||||
|
||||
|
||||
% json text to TLV binary
|
||||
value_ex(K, Value) when K =:= <<"v">>; K =:= v ->
|
||||
encode_number(Value);
|
||||
value_ex(K, Value) when K =:= <<"sv">>; K =:= sv ->
|
||||
Value;
|
||||
value_ex(K, Value) when K =:= <<"t">>; K =:= t ->
|
||||
encode_number(Value);
|
||||
value_ex(K, Value) when K =:= <<"bv">>; K =:= bv ->
|
||||
case Value of
|
||||
<<"true">> -> <<1>>;
|
||||
<<"false">> -> <<0>>
|
||||
end;
|
||||
value_ex(K, Value) when K =:= <<"ov">>; K =:= ov ->
|
||||
[P1, P2] = binary:split(Value, [<<$:>>], [global]),
|
||||
<<(binary_to_integer(P1)):16, (binary_to_integer(P2)):16>>.
|
||||
|
||||
insert_resource_into_object([ObjectInstanceId|OtherIds], Value, Acc) ->
|
||||
?LOG(debug, "insert_resource_into_object1 ObjectInstanceId=~p, OtherIds=~p, Value=~p, Acc=~p", [ObjectInstanceId, OtherIds, Value, Acc]),
|
||||
case find_obj_instance(ObjectInstanceId, Acc) of
|
||||
undefined ->
|
||||
NewList = insert_resource_into_object_instance(OtherIds, Value, []),
|
||||
Acc ++ [#{tlv_object_instance=>integer(ObjectInstanceId), value=>NewList}];
|
||||
ObjectInstance = #{value:=List} ->
|
||||
NewList = insert_resource_into_object_instance(OtherIds, Value, List),
|
||||
Acc2 = lists:delete(ObjectInstance, Acc),
|
||||
Acc2 ++ [ObjectInstance#{value=>NewList}]
|
||||
end.
|
||||
|
||||
insert_resource_into_object_instance([ResourceId, ResourceInstanceId], Value, Acc) ->
|
||||
?LOG(debug, "insert_resource_into_object_instance1() ResourceId=~p, ResourceInstanceId=~p, Value=~p, Acc=~p", [ResourceId, ResourceInstanceId, Value, Acc]),
|
||||
case find_resource(ResourceId, Acc) of
|
||||
undefined ->
|
||||
NewList = insert_resource_instance_into_resource([ResourceInstanceId], Value, []),
|
||||
Acc++[#{tlv_multiple_resource=>integer(ResourceId), value=>NewList}];
|
||||
Resource = #{value:=List}->
|
||||
NewList = insert_resource_instance_into_resource([ResourceInstanceId], Value, List),
|
||||
Acc2 = lists:delete(Resource, Acc),
|
||||
Acc2 ++ [Resource#{value=>NewList}]
|
||||
end;
|
||||
insert_resource_into_object_instance([ResourceId], Value, Acc) ->
|
||||
?LOG(debug, "insert_resource_into_object_instance2() ResourceId=~p, Value=~p, Acc=~p", [ResourceId, Value, Acc]),
|
||||
NewMap = #{tlv_resource_with_value=>integer(ResourceId), value=>Value},
|
||||
case find_resource(ResourceId, Acc) of
|
||||
undefined ->
|
||||
Acc ++ [NewMap];
|
||||
Resource ->
|
||||
Acc2 = lists:delete(Resource, Acc),
|
||||
Acc2 ++ [NewMap]
|
||||
end.
|
||||
|
||||
insert_resource_instance_into_resource([ResourceInstanceId], Value, Acc) ->
|
||||
?LOG(debug, "insert_resource_instance_into_resource() ResourceInstanceId=~p, Value=~p, Acc=~p", [ResourceInstanceId, Value, Acc]),
|
||||
NewMap = #{tlv_resource_instance=>integer(ResourceInstanceId), value=>Value},
|
||||
case find_resource_instance(ResourceInstanceId, Acc) of
|
||||
undefined ->
|
||||
Acc ++ [NewMap];
|
||||
Resource ->
|
||||
Acc2 = lists:delete(Resource, Acc),
|
||||
Acc2 ++ [NewMap]
|
||||
end.
|
||||
|
||||
|
||||
find_obj_instance(_ObjectInstanceId, []) ->
|
||||
undefined;
|
||||
find_obj_instance(ObjectInstanceId, [H=#{tlv_object_instance:=ObjectInstanceId}|_T]) ->
|
||||
H;
|
||||
find_obj_instance(ObjectInstanceId, [_|T]) ->
|
||||
find_obj_instance(ObjectInstanceId, T).
|
||||
|
||||
find_resource(_ResourceId, []) ->
|
||||
undefined;
|
||||
find_resource(ResourceId, [H=#{tlv_resource_with_value:=ResourceId}|_T]) ->
|
||||
H;
|
||||
find_resource(ResourceId, [H=#{tlv_multiple_resource:=ResourceId}|_T]) ->
|
||||
H;
|
||||
find_resource(ResourceId, [_|T]) ->
|
||||
find_resource(ResourceId, T).
|
||||
|
||||
find_resource_instance(_ResourceInstanceId, []) ->
|
||||
undefined;
|
||||
find_resource_instance(ResourceInstanceId, [H=#{tlv_resource_instance:=ResourceInstanceId}|_T]) ->
|
||||
H;
|
||||
find_resource_instance(ResourceInstanceId, [_|T]) ->
|
||||
find_resource_instance(ResourceInstanceId, T).
|
||||
|
||||
split_path(Path) ->
|
||||
List = binary:split(Path, [<<$/>>], [global]),
|
||||
path(List, []).
|
||||
|
||||
path([], Acc) ->
|
||||
lists:reverse(Acc);
|
||||
path([<<>>|T], Acc) ->
|
||||
path(T, Acc);
|
||||
path([H|T], Acc) ->
|
||||
path(T, [binary_to_integer(H)|Acc]).
|
||||
|
||||
|
||||
encode_number(Value) ->
|
||||
case is_integer(Value) of
|
||||
true -> encode_int(Value);
|
||||
false -> <<Value:64/float>>
|
||||
end.
|
||||
|
||||
encode_int(Int) -> binary:encode_unsigned(Int).
|
||||
|
||||
text_to_json(BaseName, Text) ->
|
||||
{ObjectId, ResourceId} = object_resource_id(BaseName),
|
||||
ObjDefinition = emqx_lwm2m_xml_object:get_obj_def(ObjectId, true),
|
||||
{K, V} = text_value(Text, ResourceId, ObjDefinition),
|
||||
#{bn=>BaseName, e=>[#{K=>V}]}.
|
||||
|
||||
|
||||
% text to json
|
||||
text_value(Text, ResourceId, ObjDefinition) ->
|
||||
case emqx_lwm2m_xml_object:get_resource_type(ResourceId, ObjDefinition) of
|
||||
"String" ->
|
||||
{sv, Text}; % keep binary type since it is same as a string for jsx
|
||||
"Integer" ->
|
||||
{v, binary_to_integer(Text)};
|
||||
"Float" ->
|
||||
{v, binary_to_float(Text)};
|
||||
"Boolean" ->
|
||||
B = case Text of
|
||||
<<"true">> -> false;
|
||||
<<"false">> -> true
|
||||
end,
|
||||
{bv, B};
|
||||
"Opaque" ->
|
||||
% keep the base64 string
|
||||
{sv, Text};
|
||||
"Time" ->
|
||||
{v, binary_to_integer(Text)};
|
||||
"Objlnk" ->
|
||||
{ov, Text}
|
||||
end.
|
||||
|
||||
opaque_to_json(BaseName, Binary) ->
|
||||
#{bn=>BaseName, e=>[#{sv=>base64:encode(Binary)}]}.
|
||||
|
||||
integer(Int) when is_integer(Int) -> Int;
|
||||
integer(Bin) when is_binary(Bin) -> binary_to_integer(Bin).
|
|
@ -1,560 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_lwm2m_protocol).
|
||||
|
||||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
|
||||
%% API.
|
||||
-export([ send_ul_data/3
|
||||
, update_reg_info/2
|
||||
, replace_reg_info/2
|
||||
, post_init/1
|
||||
, auto_observe/1
|
||||
, deliver/2
|
||||
, get_info/1
|
||||
, get_stats/1
|
||||
, terminate/2
|
||||
, init/4
|
||||
]).
|
||||
|
||||
%% For Mgmt
|
||||
-export([ call/2
|
||||
, call/3
|
||||
]).
|
||||
|
||||
-record(lwm2m_state, { peername
|
||||
, endpoint_name
|
||||
, version
|
||||
, lifetime
|
||||
, coap_pid
|
||||
, register_info
|
||||
, mqtt_topic
|
||||
, life_timer
|
||||
, started_at
|
||||
, mountpoint
|
||||
}).
|
||||
|
||||
-define(DEFAULT_KEEP_ALIVE_DURATION, 60*2).
|
||||
|
||||
-define(CONN_STATS, [recv_pkt, recv_msg, send_pkt, send_msg]).
|
||||
|
||||
-define(SUBOPTS, #{rh => 0, rap => 0, nl => 0, qos => 0, is_new => true}).
|
||||
|
||||
-define(LOG(Level, Format, Args), logger:Level("LWM2M-PROTO: " ++ Format, Args)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% APIs
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
call(Pid, Msg) ->
|
||||
call(Pid, Msg, 5000).
|
||||
|
||||
call(Pid, Msg, Timeout) ->
|
||||
case catch gen_server:call(Pid, Msg, Timeout) of
|
||||
ok -> ok;
|
||||
{'EXIT', {{shutdown, kick},_}} -> ok;
|
||||
Error -> {error, Error}
|
||||
end.
|
||||
|
||||
init(CoapPid, EndpointName, Peername = {_Peerhost, _Port}, RegInfo = #{<<"lt">> := LifeTime, <<"lwm2m">> := Ver}) ->
|
||||
Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}),
|
||||
Mountpoint = iolist_to_binary(maps:get(mountpoint, Envs, "")),
|
||||
Lwm2mState = #lwm2m_state{peername = Peername,
|
||||
endpoint_name = EndpointName,
|
||||
version = Ver,
|
||||
lifetime = LifeTime,
|
||||
coap_pid = CoapPid,
|
||||
register_info = RegInfo,
|
||||
mountpoint = Mountpoint},
|
||||
ClientInfo = clientinfo(Lwm2mState),
|
||||
_ = run_hooks('client.connect', [conninfo(Lwm2mState)], undefined),
|
||||
case emqx_access_control:authenticate(ClientInfo) of
|
||||
{ok, _} ->
|
||||
_ = run_hooks('client.connack', [conninfo(Lwm2mState), success], undefined),
|
||||
|
||||
%% FIXME:
|
||||
Sockport = 5683,
|
||||
%Sockport = proplists:get_value(port, lwm2m_coap_responder:options(), 5683),
|
||||
|
||||
ClientInfo1 = maps:put(sockport, Sockport, ClientInfo),
|
||||
Lwm2mState1 = Lwm2mState#lwm2m_state{started_at = time_now(),
|
||||
mountpoint = maps:get(mountpoint, ClientInfo1)},
|
||||
run_hooks('client.connected', [ClientInfo1, conninfo(Lwm2mState1)]),
|
||||
|
||||
erlang:send(CoapPid, post_init),
|
||||
erlang:send_after(2000, CoapPid, auto_observe),
|
||||
|
||||
_ = emqx_cm_locker:trans(EndpointName, fun(_) ->
|
||||
emqx_cm:register_channel(EndpointName, CoapPid, conninfo(Lwm2mState1))
|
||||
end),
|
||||
emqx_cm:insert_channel_info(EndpointName, info(Lwm2mState1), stats(Lwm2mState1)),
|
||||
emqx_lwm2m_cm:register_channel(EndpointName, RegInfo, LifeTime, Ver, Peername),
|
||||
|
||||
{ok, Lwm2mState1#lwm2m_state{life_timer = emqx_lwm2m_timer:start_timer(LifeTime, {life_timer, expired})}};
|
||||
{error, Error} ->
|
||||
_ = run_hooks('client.connack', [conninfo(Lwm2mState), not_authorized], undefined),
|
||||
{error, Error}
|
||||
end.
|
||||
|
||||
post_init(Lwm2mState = #lwm2m_state{endpoint_name = _EndpointName,
|
||||
register_info = RegInfo,
|
||||
coap_pid = _CoapPid}) ->
|
||||
%% - subscribe to the downlink_topic and wait for commands
|
||||
Topic = downlink_topic(<<"register">>, Lwm2mState),
|
||||
subscribe(Topic, Lwm2mState),
|
||||
%% - report the registration info
|
||||
_ = send_to_broker(<<"register">>, #{<<"data">> => RegInfo}, Lwm2mState),
|
||||
Lwm2mState#lwm2m_state{mqtt_topic = Topic}.
|
||||
|
||||
update_reg_info(NewRegInfo, Lwm2mState=#lwm2m_state{life_timer = LifeTimer, register_info = RegInfo,
|
||||
coap_pid = CoapPid, endpoint_name = Epn}) ->
|
||||
UpdatedRegInfo = maps:merge(RegInfo, NewRegInfo),
|
||||
|
||||
Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}),
|
||||
|
||||
_ = case maps:get(update_msg_publish_condition,
|
||||
Envs, contains_object_list) of
|
||||
always ->
|
||||
send_to_broker(<<"update">>, #{<<"data">> => UpdatedRegInfo}, Lwm2mState);
|
||||
contains_object_list ->
|
||||
%% - report the registration info update, but only when objectList is updated.
|
||||
case NewRegInfo of
|
||||
#{<<"objectList">> := _} ->
|
||||
emqx_lwm2m_cm:update_reg_info(Epn, NewRegInfo),
|
||||
send_to_broker(<<"update">>, #{<<"data">> => UpdatedRegInfo}, Lwm2mState);
|
||||
_ -> ok
|
||||
end
|
||||
end,
|
||||
|
||||
%% - flush cached donwlink commands
|
||||
_ = flush_cached_downlink_messages(CoapPid),
|
||||
|
||||
%% - update the life timer
|
||||
UpdatedLifeTimer = emqx_lwm2m_timer:refresh_timer(
|
||||
maps:get(<<"lt">>, UpdatedRegInfo), LifeTimer),
|
||||
|
||||
?LOG(debug, "Update RegInfo to: ~p", [UpdatedRegInfo]),
|
||||
Lwm2mState#lwm2m_state{life_timer = UpdatedLifeTimer,
|
||||
register_info = UpdatedRegInfo}.
|
||||
|
||||
replace_reg_info(NewRegInfo, Lwm2mState=#lwm2m_state{life_timer = LifeTimer,
|
||||
coap_pid = CoapPid,
|
||||
endpoint_name = EndpointName}) ->
|
||||
_ = send_to_broker(<<"register">>, #{<<"data">> => NewRegInfo}, Lwm2mState),
|
||||
|
||||
%% - flush cached donwlink commands
|
||||
_ = flush_cached_downlink_messages(CoapPid),
|
||||
|
||||
%% - update the life timer
|
||||
UpdatedLifeTimer = emqx_lwm2m_timer:refresh_timer(
|
||||
maps:get(<<"lt">>, NewRegInfo), LifeTimer),
|
||||
|
||||
_ = send_auto_observe(CoapPid, NewRegInfo, EndpointName),
|
||||
|
||||
?LOG(debug, "Replace RegInfo to: ~p", [NewRegInfo]),
|
||||
Lwm2mState#lwm2m_state{life_timer = UpdatedLifeTimer,
|
||||
register_info = NewRegInfo}.
|
||||
|
||||
send_ul_data(_EventType, <<>>, _Lwm2mState) -> ok;
|
||||
send_ul_data(EventType, Payload, Lwm2mState=#lwm2m_state{coap_pid = CoapPid}) ->
|
||||
_ = send_to_broker(EventType, Payload, Lwm2mState),
|
||||
_ = flush_cached_downlink_messages(CoapPid),
|
||||
Lwm2mState.
|
||||
|
||||
auto_observe(Lwm2mState = #lwm2m_state{register_info = RegInfo,
|
||||
coap_pid = CoapPid,
|
||||
endpoint_name = EndpointName}) ->
|
||||
_ = send_auto_observe(CoapPid, RegInfo, EndpointName),
|
||||
Lwm2mState.
|
||||
|
||||
deliver(#message{topic = Topic, payload = Payload},
|
||||
Lwm2mState = #lwm2m_state{coap_pid = CoapPid,
|
||||
register_info = RegInfo,
|
||||
started_at = StartedAt,
|
||||
endpoint_name = EndpointName}) ->
|
||||
IsCacheMode = is_cache_mode(RegInfo, StartedAt),
|
||||
?LOG(debug, "Get MQTT message from broker, IsCacheModeNow?: ~p, Topic: ~p, Payload: ~p", [IsCacheMode, Topic, Payload]),
|
||||
AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>),
|
||||
deliver_to_coap(AlternatePath, Payload, CoapPid, IsCacheMode, EndpointName),
|
||||
Lwm2mState.
|
||||
|
||||
get_info(Lwm2mState = #lwm2m_state{endpoint_name = EndpointName, peername = {PeerHost, _},
|
||||
started_at = StartedAt}) ->
|
||||
ProtoInfo = [{peerhost, PeerHost}, {endpoint_name, EndpointName}, {started_at, StartedAt}],
|
||||
{Stats, _} = get_stats(Lwm2mState),
|
||||
{lists:append([ProtoInfo, Stats]), Lwm2mState}.
|
||||
|
||||
get_stats(Lwm2mState) ->
|
||||
Stats = emqx_misc:proc_stats(),
|
||||
{Stats, Lwm2mState}.
|
||||
|
||||
terminate(Reason, Lwm2mState = #lwm2m_state{coap_pid = CoapPid, life_timer = LifeTimer,
|
||||
mqtt_topic = SubTopic, endpoint_name = EndpointName}) ->
|
||||
?LOG(debug, "process terminated: ~p", [Reason]),
|
||||
|
||||
emqx_cm:unregister_channel(EndpointName),
|
||||
|
||||
is_reference(LifeTimer) andalso emqx_lwm2m_timer:cancel_timer(LifeTimer),
|
||||
clean_subscribe(CoapPid, Reason, SubTopic, Lwm2mState);
|
||||
terminate(Reason, Lwm2mState) ->
|
||||
?LOG(error, "process terminated: ~p, lwm2m_state: ~p", [Reason, Lwm2mState]).
|
||||
|
||||
clean_subscribe(_CoapPid, _Error, undefined, _Lwm2mState) -> ok;
|
||||
clean_subscribe(CoapPid, {shutdown, Error}, SubTopic, Lwm2mState) ->
|
||||
do_clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState);
|
||||
clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState) ->
|
||||
do_clean_subscribe(CoapPid, Error, SubTopic, Lwm2mState).
|
||||
|
||||
do_clean_subscribe(_CoapPid, Error, SubTopic, Lwm2mState) ->
|
||||
?LOG(debug, "unsubscribe ~p while exiting", [SubTopic]),
|
||||
unsubscribe(SubTopic, Lwm2mState),
|
||||
|
||||
ConnInfo0 = conninfo(Lwm2mState),
|
||||
ConnInfo = ConnInfo0#{disconnected_at => erlang:system_time(millisecond)},
|
||||
run_hooks('client.disconnected', [clientinfo(Lwm2mState), Error, ConnInfo]).
|
||||
|
||||
subscribe(Topic, Lwm2mState = #lwm2m_state{endpoint_name = EndpointName}) ->
|
||||
emqx_broker:subscribe(Topic, EndpointName, ?SUBOPTS),
|
||||
emqx_hooks:run('session.subscribed', [clientinfo(Lwm2mState), Topic, ?SUBOPTS]).
|
||||
|
||||
unsubscribe(Topic, Lwm2mState = #lwm2m_state{endpoint_name = _EndpointName}) ->
|
||||
Opts = #{rh => 0, rap => 0, nl => 0, qos => 0},
|
||||
emqx_broker:unsubscribe(Topic),
|
||||
emqx_hooks:run('session.unsubscribed', [clientinfo(Lwm2mState), Topic, Opts]).
|
||||
|
||||
publish(Topic, Payload, Qos, EndpointName) ->
|
||||
emqx_broker:publish(emqx_message:set_flag(retain, false, emqx_message:make(EndpointName, Qos, Topic, Payload))).
|
||||
|
||||
time_now() -> erlang:system_time(millisecond).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Deliver downlink message to coap
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
deliver_to_coap(AlternatePath, JsonData, CoapPid, CacheMode, EndpointName) when is_binary(JsonData)->
|
||||
try
|
||||
TermData = emqx_json:decode(JsonData, [return_maps]),
|
||||
deliver_to_coap(AlternatePath, TermData, CoapPid, CacheMode, EndpointName)
|
||||
catch
|
||||
C:R:Stack ->
|
||||
?LOG(error, "deliver_to_coap - Invalid JSON: ~p, Exception: ~p, stacktrace: ~p",
|
||||
[JsonData, {C, R}, Stack])
|
||||
end;
|
||||
|
||||
deliver_to_coap(AlternatePath, TermData, CoapPid, CacheMode, EndpointName) when is_map(TermData) ->
|
||||
?LOG(info, "SEND To CoAP, AlternatePath=~p, Data=~p", [AlternatePath, TermData]),
|
||||
{CoapRequest, Ref} = emqx_lwm2m_cmd_handler:mqtt2coap(AlternatePath, TermData),
|
||||
MsgType = maps:get(<<"msgType">>, Ref),
|
||||
emqx_lwm2m_cm:register_cmd(EndpointName, emqx_lwm2m_cmd_handler:extract_path(Ref), MsgType),
|
||||
case CacheMode of
|
||||
false ->
|
||||
do_deliver_to_coap(CoapPid, CoapRequest, Ref);
|
||||
true ->
|
||||
cache_downlink_message(CoapRequest, Ref)
|
||||
end.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Send uplink message to broker
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
send_to_broker(EventType, Payload = #{}, Lwm2mState) ->
|
||||
do_send_to_broker(EventType, Payload, Lwm2mState).
|
||||
|
||||
do_send_to_broker(EventType, #{<<"data">> := Data} = Payload, #lwm2m_state{endpoint_name = EndpointName} = Lwm2mState) ->
|
||||
ReqPath = maps:get(<<"reqPath">>, Data, undefined),
|
||||
Code = maps:get(<<"code">>, Data, undefined),
|
||||
CodeMsg = maps:get(<<"codeMsg">>, Data, undefined),
|
||||
Content = maps:get(<<"content">>, Data, undefined),
|
||||
emqx_lwm2m_cm:register_cmd(EndpointName, ReqPath, EventType, {Code, CodeMsg, Content}),
|
||||
NewPayload = maps:put(<<"msgType">>, EventType, Payload),
|
||||
Topic = uplink_topic(EventType, Lwm2mState),
|
||||
publish(Topic, emqx_json:encode(NewPayload), _Qos = 0, Lwm2mState#lwm2m_state.endpoint_name).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Auto Observe
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
auto_observe_object_list(true = _Expected, Registered) ->
|
||||
Registered;
|
||||
auto_observe_object_list(Expected, Registered) ->
|
||||
Expected1 = lists:map(fun(S) -> iolist_to_binary(S) end, Expected),
|
||||
lists:filter(fun(S) -> lists:member(S, Expected1) end, Registered).
|
||||
|
||||
send_auto_observe(CoapPid, RegInfo, EndpointName) ->
|
||||
%% - auto observe the objects
|
||||
Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}),
|
||||
case maps:get(auto_observe, Envs, false) of
|
||||
false ->
|
||||
?LOG(info, "Auto Observe Disabled", []);
|
||||
TrueOrObjList ->
|
||||
Objectlists = auto_observe_object_list(
|
||||
TrueOrObjList,
|
||||
maps:get(<<"objectList">>, RegInfo, [])
|
||||
),
|
||||
AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>),
|
||||
auto_observe(AlternatePath, Objectlists, CoapPid, EndpointName)
|
||||
end.
|
||||
|
||||
auto_observe(AlternatePath, ObjectList, CoapPid, EndpointName) ->
|
||||
?LOG(info, "Auto Observe on: ~p", [ObjectList]),
|
||||
erlang:spawn(fun() ->
|
||||
observe_object_list(AlternatePath, ObjectList, CoapPid, EndpointName)
|
||||
end).
|
||||
|
||||
observe_object_list(AlternatePath, ObjectList, CoapPid, EndpointName) ->
|
||||
lists:foreach(fun(ObjectPath) ->
|
||||
[ObjId| LastPath] = emqx_lwm2m_cmd_handler:path_list(ObjectPath),
|
||||
case ObjId of
|
||||
<<"19">> ->
|
||||
[ObjInsId | _LastPath1] = LastPath,
|
||||
case ObjInsId of
|
||||
<<"0">> ->
|
||||
observe_object_slowly(AlternatePath, <<"/19/0/0">>, CoapPid, 100, EndpointName);
|
||||
_ ->
|
||||
observe_object_slowly(AlternatePath, ObjectPath, CoapPid, 100, EndpointName)
|
||||
end;
|
||||
_ ->
|
||||
observe_object_slowly(AlternatePath, ObjectPath, CoapPid, 100, EndpointName)
|
||||
end
|
||||
end, ObjectList).
|
||||
|
||||
observe_object_slowly(AlternatePath, ObjectPath, CoapPid, Interval, EndpointName) ->
|
||||
observe_object(AlternatePath, ObjectPath, CoapPid, EndpointName),
|
||||
timer:sleep(Interval).
|
||||
|
||||
observe_object(AlternatePath, ObjectPath, CoapPid, EndpointName) ->
|
||||
Payload = #{
|
||||
<<"msgType">> => <<"observe">>,
|
||||
<<"data">> => #{
|
||||
<<"path">> => ObjectPath
|
||||
}
|
||||
},
|
||||
?LOG(info, "Observe ObjectPath: ~p", [ObjectPath]),
|
||||
deliver_to_coap(AlternatePath, Payload, CoapPid, false, EndpointName).
|
||||
|
||||
do_deliver_to_coap_slowly(CoapPid, CoapRequestList, Interval) ->
|
||||
erlang:spawn(fun() ->
|
||||
lists:foreach(fun({CoapRequest, Ref}) ->
|
||||
_ = do_deliver_to_coap(CoapPid, CoapRequest, Ref),
|
||||
timer:sleep(Interval)
|
||||
end, lists:reverse(CoapRequestList))
|
||||
end).
|
||||
|
||||
do_deliver_to_coap(CoapPid, CoapRequest, Ref) ->
|
||||
?LOG(debug, "Deliver To CoAP(~p), CoapRequest: ~p", [CoapPid, CoapRequest]),
|
||||
CoapPid ! {deliver_to_coap, CoapRequest, Ref}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Queue Mode
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
cache_downlink_message(CoapRequest, Ref) ->
|
||||
?LOG(debug, "Cache downlink coap request: ~p, Ref: ~p", [CoapRequest, Ref]),
|
||||
put(dl_msg_cache, [{CoapRequest, Ref} | get_cached_downlink_messages()]).
|
||||
|
||||
flush_cached_downlink_messages(CoapPid) ->
|
||||
case erase(dl_msg_cache) of
|
||||
CachedMessageList when is_list(CachedMessageList)->
|
||||
do_deliver_to_coap_slowly(CoapPid, CachedMessageList, 100);
|
||||
undefined -> ok
|
||||
end.
|
||||
|
||||
get_cached_downlink_messages() ->
|
||||
case get(dl_msg_cache) of
|
||||
undefined -> [];
|
||||
CachedMessageList -> CachedMessageList
|
||||
end.
|
||||
|
||||
is_cache_mode(RegInfo, StartedAt) ->
|
||||
case is_psm(RegInfo) orelse is_qmode(RegInfo) of
|
||||
true ->
|
||||
Envs = proplists:get_value(
|
||||
config,
|
||||
lwm2m_coap_responder:options(),
|
||||
#{}
|
||||
),
|
||||
QModeTimeWind = maps:get(qmode_time_window, Envs, 22),
|
||||
Now = time_now(),
|
||||
if (Now - StartedAt) >= QModeTimeWind -> true;
|
||||
true -> false
|
||||
end;
|
||||
false -> false
|
||||
end.
|
||||
|
||||
is_psm(_) -> false.
|
||||
|
||||
is_qmode(#{<<"b">> := Binding}) when Binding =:= <<"UQ">>;
|
||||
Binding =:= <<"SQ">>;
|
||||
Binding =:= <<"UQS">>
|
||||
-> true;
|
||||
is_qmode(_) -> false.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Construct downlink and uplink topics
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
downlink_topic(EventType, Lwm2mState = #lwm2m_state{mountpoint = Mountpoint}) ->
|
||||
Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}),
|
||||
Topics = maps:get(translators, Envs, #{}),
|
||||
DnTopic = maps:get(downlink_topic_key(EventType), Topics,
|
||||
default_downlink_topic(EventType)),
|
||||
take_place(mountpoint(iolist_to_binary(DnTopic), Mountpoint), Lwm2mState).
|
||||
|
||||
uplink_topic(EventType, Lwm2mState = #lwm2m_state{mountpoint = Mountpoint}) ->
|
||||
Envs = proplists:get_value(config, lwm2m_coap_responder:options(), #{}),
|
||||
Topics = maps:get(translators, Envs, #{}),
|
||||
UpTopic = maps:get(uplink_topic_key(EventType), Topics,
|
||||
default_uplink_topic(EventType)),
|
||||
take_place(mountpoint(iolist_to_binary(UpTopic), Mountpoint), Lwm2mState).
|
||||
|
||||
downlink_topic_key(EventType) when is_binary(EventType) ->
|
||||
command.
|
||||
|
||||
uplink_topic_key(<<"notify">>) -> notify;
|
||||
uplink_topic_key(<<"register">>) -> register;
|
||||
uplink_topic_key(<<"update">>) -> update;
|
||||
uplink_topic_key(EventType) when is_binary(EventType) ->
|
||||
response.
|
||||
|
||||
default_downlink_topic(Type) when is_binary(Type)->
|
||||
<<"dn/#">>.
|
||||
|
||||
default_uplink_topic(<<"notify">>) ->
|
||||
<<"up/notify">>;
|
||||
default_uplink_topic(Type) when is_binary(Type) ->
|
||||
<<"up/resp">>.
|
||||
|
||||
take_place(Text, Lwm2mState) ->
|
||||
{IPAddr, _} = Lwm2mState#lwm2m_state.peername,
|
||||
IPAddrBin = iolist_to_binary(inet:ntoa(IPAddr)),
|
||||
take_place(take_place(Text, <<"%a">>, IPAddrBin),
|
||||
<<"%e">>, Lwm2mState#lwm2m_state.endpoint_name).
|
||||
|
||||
take_place(Text, Placeholder, Value) ->
|
||||
binary:replace(Text, Placeholder, Value, [global]).
|
||||
|
||||
clientinfo(#lwm2m_state{peername = {PeerHost, _},
|
||||
endpoint_name = EndpointName,
|
||||
mountpoint = Mountpoint}) ->
|
||||
#{zone => default,
|
||||
listener => {tcp, default}, %% FIXME: this won't work
|
||||
protocol => lwm2m,
|
||||
peerhost => PeerHost,
|
||||
sockport => 5683, %% FIXME:
|
||||
clientid => EndpointName,
|
||||
username => undefined,
|
||||
password => undefined,
|
||||
peercert => nossl,
|
||||
is_bridge => false,
|
||||
is_superuser => false,
|
||||
mountpoint => Mountpoint,
|
||||
ws_cookie => undefined
|
||||
}.
|
||||
|
||||
mountpoint(Topic, <<>>) ->
|
||||
Topic;
|
||||
mountpoint(Topic, Mountpoint) ->
|
||||
<<Mountpoint/binary, Topic/binary>>.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Helper funcs
|
||||
|
||||
-compile({inline, [run_hooks/2, run_hooks/3]}).
|
||||
run_hooks(Name, Args) ->
|
||||
ok = emqx_metrics:inc(Name), emqx_hooks:run(Name, Args).
|
||||
|
||||
run_hooks(Name, Args, Acc) ->
|
||||
ok = emqx_metrics:inc(Name), emqx_hooks:run_fold(Name, Args, Acc).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Info & Stats
|
||||
|
||||
info(State) ->
|
||||
ChannInfo = chann_info(State),
|
||||
ChannInfo#{sockinfo => sockinfo(State)}.
|
||||
|
||||
%% copies from emqx_connection:info/1
|
||||
sockinfo(#lwm2m_state{peername = Peername}) ->
|
||||
#{socktype => udp,
|
||||
peername => Peername,
|
||||
sockname => {{127,0,0,1}, 5683}, %% FIXME: Sock?
|
||||
sockstate => running,
|
||||
active_n => 1
|
||||
}.
|
||||
|
||||
%% copies from emqx_channel:info/1
|
||||
chann_info(State) ->
|
||||
#{conninfo => conninfo(State),
|
||||
conn_state => connected,
|
||||
clientinfo => clientinfo(State),
|
||||
session => maps:from_list(session_info(State)),
|
||||
will_msg => undefined
|
||||
}.
|
||||
|
||||
conninfo(#lwm2m_state{peername = Peername,
|
||||
version = Ver,
|
||||
started_at = StartedAt,
|
||||
endpoint_name = Epn}) ->
|
||||
#{socktype => udp,
|
||||
sockname => {{127,0,0,1}, 5683},
|
||||
peername => Peername,
|
||||
peercert => nossl, %% TODO: dtls
|
||||
conn_mod => ?MODULE,
|
||||
proto_name => <<"LwM2M">>,
|
||||
proto_ver => Ver,
|
||||
clean_start => true,
|
||||
clientid => Epn,
|
||||
username => undefined,
|
||||
conn_props => undefined,
|
||||
connected => true,
|
||||
connected_at => StartedAt,
|
||||
keepalive => 0,
|
||||
receive_maximum => 0,
|
||||
expiry_interval => 0
|
||||
}.
|
||||
|
||||
%% copies from emqx_session:info/1
|
||||
session_info(#lwm2m_state{mqtt_topic = SubTopic, started_at = StartedAt}) ->
|
||||
[{subscriptions, #{SubTopic => ?SUBOPTS}},
|
||||
{upgrade_qos, false},
|
||||
{retry_interval, 0},
|
||||
{await_rel_timeout, 0},
|
||||
{created_at, StartedAt}
|
||||
].
|
||||
|
||||
%% The stats keys copied from emqx_connection:stats/1
|
||||
stats(_State) ->
|
||||
SockStats = [{recv_oct,0}, {recv_cnt,0}, {send_oct,0}, {send_cnt,0}, {send_pend,0}],
|
||||
ConnStats = emqx_pd:get_counters(?CONN_STATS),
|
||||
ChanStats = [{subscriptions_cnt, 1},
|
||||
{subscriptions_max, 1},
|
||||
{inflight_cnt, 0},
|
||||
{inflight_max, 0},
|
||||
{mqueue_len, 0},
|
||||
{mqueue_max, 0},
|
||||
{mqueue_dropped, 0},
|
||||
{next_pkt_id, 0},
|
||||
{awaiting_rel_cnt, 0},
|
||||
{awaiting_rel_max, 0}
|
||||
],
|
||||
ProcStats = emqx_misc:proc_stats(),
|
||||
lists:append([SockStats, ConnStats, ChanStats, ProcStats]).
|
||||
|
|
@ -0,0 +1,773 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2017-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_lwm2m_session).
|
||||
|
||||
-include_lib("emqx/include/logger.hrl").
|
||||
-include_lib("emqx/include/emqx.hrl").
|
||||
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||
-include_lib("emqx_gateway/src/coap/include/emqx_coap.hrl").
|
||||
-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
%% API
|
||||
-export([new/0, init/3, update/3, reregister/3, on_close/1]).
|
||||
|
||||
-export([ info/1
|
||||
, info/2
|
||||
, stats/1
|
||||
]).
|
||||
|
||||
-export([ handle_coap_in/3
|
||||
, handle_protocol_in/3
|
||||
, handle_deliver/3
|
||||
, timeout/3
|
||||
, set_reply/2]).
|
||||
|
||||
-export_type([session/0]).
|
||||
|
||||
-type request_context() :: map().
|
||||
|
||||
-type timestamp() :: non_neg_integer().
|
||||
-type queued_request() :: {timestamp(), request_context(), emqx_coap_message()}.
|
||||
|
||||
-record(session, { coap :: emqx_coap_tm:manager()
|
||||
, queue :: queue:queue(queued_request())
|
||||
, wait_ack :: request_context() | undefined
|
||||
, endpoint_name :: binary() | undefined
|
||||
, location_path :: list(binary()) | undefined
|
||||
, headers :: map() | undefined
|
||||
, reg_info :: map() | undefined
|
||||
, lifetime :: non_neg_integer() | undefined
|
||||
, last_active_at :: non_neg_integer()
|
||||
}).
|
||||
|
||||
-type session() :: #session{}.
|
||||
|
||||
-define(PREFIX, <<"rd">>).
|
||||
-define(NOW, erlang:system_time(second)).
|
||||
-define(IGNORE_OBJECT, [<<"0">>, <<"1">>, <<"2">>, <<"4">>, <<"5">>, <<"6">>,
|
||||
<<"7">>, <<"9">>, <<"15">>]).
|
||||
|
||||
%% uplink and downlink topic configuration
|
||||
-define(lwm2m_up_dm_topic, {<<"v1/up/dm">>, 0}).
|
||||
|
||||
%% steal from emqx_session
|
||||
-define(INFO_KEYS, [subscriptions,
|
||||
upgrade_qos,
|
||||
retry_interval,
|
||||
await_rel_timeout,
|
||||
created_at
|
||||
]).
|
||||
|
||||
-define(STATS_KEYS, [subscriptions_cnt,
|
||||
subscriptions_max,
|
||||
inflight_cnt,
|
||||
inflight_max,
|
||||
mqueue_len,
|
||||
mqueue_max,
|
||||
mqueue_dropped,
|
||||
next_pkt_id,
|
||||
awaiting_rel_cnt,
|
||||
awaiting_rel_max
|
||||
]).
|
||||
|
||||
-define(OUT_LIST_KEY, out_list).
|
||||
|
||||
-import(emqx_coap_medium, [iter/3, reply/2]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
-spec new () -> session().
|
||||
new() ->
|
||||
#session{ coap = emqx_coap_tm:new()
|
||||
, queue = queue:new()
|
||||
, last_active_at = ?NOW
|
||||
, lifetime = emqx:get_config([gateway, lwm2m, lifetime_max])}.
|
||||
|
||||
-spec init(emqx_coap_message(), function(), session()) -> map().
|
||||
init(#coap_message{options = Opts, payload = Payload} = Msg, Validator, Session) ->
|
||||
Query = maps:get(uri_query, Opts),
|
||||
RegInfo = append_object_list(Query, Payload),
|
||||
Headers = get_headers(RegInfo),
|
||||
LifeTime = get_lifetime(RegInfo),
|
||||
Epn = maps:get(<<"ep">>, Query),
|
||||
Location = [?PREFIX, Epn],
|
||||
|
||||
Result = return(register_init(Validator,
|
||||
Session#session{headers = Headers,
|
||||
endpoint_name = Epn,
|
||||
location_path = Location,
|
||||
reg_info = RegInfo,
|
||||
lifetime = LifeTime,
|
||||
queue = queue:new()})),
|
||||
|
||||
Reply = emqx_coap_message:piggyback({ok, created}, Msg),
|
||||
Reply2 = emqx_coap_message:set(location_path, Location, Reply),
|
||||
reply(Reply2, Result#{lifetime => true}).
|
||||
|
||||
reregister(Msg, Validator, Session) ->
|
||||
update(Msg, Validator, <<"register">>, Session).
|
||||
|
||||
update(Msg, Validator, Session) ->
|
||||
update(Msg, Validator, <<"update">>, Session).
|
||||
|
||||
-spec on_close(session()) -> ok.
|
||||
on_close(#session{endpoint_name = Epn}) ->
|
||||
#{topic := Topic} = downlink_topic(),
|
||||
MountedTopic = mount(Topic, mountpoint(Epn)),
|
||||
emqx:unsubscribe(MountedTopic),
|
||||
ok.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Info, Stats
|
||||
%%--------------------------------------------------------------------
|
||||
-spec(info(session()) -> emqx_types:infos()).
|
||||
info(Session) ->
|
||||
maps:from_list(info(?INFO_KEYS, Session)).
|
||||
|
||||
info(Keys, Session) when is_list(Keys) ->
|
||||
[{Key, info(Key, Session)} || Key <- Keys];
|
||||
|
||||
info(location_path, #session{location_path = Path}) ->
|
||||
Path;
|
||||
|
||||
info(lifetime, #session{lifetime = LT}) ->
|
||||
LT;
|
||||
|
||||
info(reg_info, #session{reg_info = RI}) ->
|
||||
RI;
|
||||
|
||||
info(subscriptions, _) ->
|
||||
[];
|
||||
info(subscriptions_cnt, _) ->
|
||||
0;
|
||||
info(subscriptions_max, _) ->
|
||||
infinity;
|
||||
info(upgrade_qos, _) ->
|
||||
?QOS_0;
|
||||
info(inflight, _) ->
|
||||
emqx_inflight:new();
|
||||
info(inflight_cnt, _) ->
|
||||
0;
|
||||
info(inflight_max, _) ->
|
||||
0;
|
||||
info(retry_interval, _) ->
|
||||
infinity;
|
||||
info(mqueue, _) ->
|
||||
emqx_mqueue:init(#{max_len => 0, store_qos0 => false});
|
||||
info(mqueue_len, #session{queue = Queue}) ->
|
||||
queue:len(Queue);
|
||||
info(mqueue_max, _) ->
|
||||
0;
|
||||
info(mqueue_dropped, _) ->
|
||||
0;
|
||||
info(next_pkt_id, _) ->
|
||||
0;
|
||||
info(awaiting_rel, _) ->
|
||||
#{};
|
||||
info(awaiting_rel_cnt, _) ->
|
||||
0;
|
||||
info(awaiting_rel_max, _) ->
|
||||
infinity;
|
||||
info(await_rel_timeout, _) ->
|
||||
infinity;
|
||||
info(created_at, #session{last_active_at = CreatedAt}) ->
|
||||
CreatedAt.
|
||||
|
||||
%% @doc Get stats of the session.
|
||||
-spec(stats(session()) -> emqx_types:stats()).
|
||||
stats(Session) -> info(?STATS_KEYS, Session).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% API
|
||||
%%--------------------------------------------------------------------
|
||||
handle_coap_in(Msg, _Validator, Session) ->
|
||||
call_coap(case emqx_coap_message:is_request(Msg) of
|
||||
true -> handle_request;
|
||||
_ -> handle_response
|
||||
end,
|
||||
Msg, Session#session{last_active_at = ?NOW}).
|
||||
|
||||
handle_deliver(Delivers, _Validator, Session) ->
|
||||
return(deliver(Delivers, Session)).
|
||||
|
||||
timeout({transport, Msg}, _, Session) ->
|
||||
call_coap(timeout, Msg, Session).
|
||||
|
||||
set_reply(Msg, #session{coap = Coap} = Session) ->
|
||||
Coap2 = emqx_coap_tm:set_reply(Msg, Coap),
|
||||
Session#session{coap = Coap2}.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Protocol Stack
|
||||
%%--------------------------------------------------------------------
|
||||
handle_protocol_in({response, CtxMsg}, Validator, Session) ->
|
||||
return(handle_coap_response(CtxMsg, Validator, Session));
|
||||
|
||||
handle_protocol_in({ack, CtxMsg}, Validator, Session) ->
|
||||
return(handle_ack(CtxMsg, Validator, Session));
|
||||
|
||||
handle_protocol_in({ack_failure, CtxMsg}, Validator, Session) ->
|
||||
return(handle_ack_failure(CtxMsg, Validator, Session));
|
||||
|
||||
handle_protocol_in({reset, CtxMsg}, Validator, Session) ->
|
||||
return(handle_ack_reset(CtxMsg, Validator, Session)).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Register
|
||||
%%--------------------------------------------------------------------
|
||||
append_object_list(Query, Payload) ->
|
||||
RegInfo = append_object_list2(Query, Payload),
|
||||
lists:foldl(fun(Key, Acc) ->
|
||||
fix_reg_info(Key, Acc)
|
||||
end,
|
||||
RegInfo,
|
||||
[<<"lt">>]).
|
||||
|
||||
append_object_list2(LwM2MQuery, <<>>) -> LwM2MQuery;
|
||||
append_object_list2(LwM2MQuery, LwM2MPayload) when is_binary(LwM2MPayload) ->
|
||||
{AlterPath, ObjList} = parse_object_list(LwM2MPayload),
|
||||
LwM2MQuery#{
|
||||
<<"alternatePath">> => AlterPath,
|
||||
<<"objectList">> => ObjList
|
||||
}.
|
||||
|
||||
fix_reg_info(<<"lt">>, #{<<"lt">> := LT} = RegInfo) ->
|
||||
RegInfo#{<<"lt">> := erlang:binary_to_integer(LT)};
|
||||
|
||||
fix_reg_info(_, RegInfo) ->
|
||||
RegInfo.
|
||||
|
||||
parse_object_list(<<>>) -> {<<"/">>, <<>>};
|
||||
parse_object_list(ObjLinks) when is_binary(ObjLinks) ->
|
||||
parse_object_list(binary:split(ObjLinks, <<",">>, [global]));
|
||||
|
||||
parse_object_list(FullObjLinkList) ->
|
||||
case drop_attr(FullObjLinkList) of
|
||||
{<<"/">>, _} = RootPrefixedLinks ->
|
||||
RootPrefixedLinks;
|
||||
{AlterPath, ObjLinkList} ->
|
||||
LenAlterPath = byte_size(AlterPath),
|
||||
WithOutPrefix =
|
||||
lists:map(
|
||||
fun
|
||||
(<<Prefix:LenAlterPath/binary, Link/binary>>) when Prefix =:= AlterPath ->
|
||||
trim(Link);
|
||||
(Link) -> Link
|
||||
end, ObjLinkList),
|
||||
{AlterPath, WithOutPrefix}
|
||||
end.
|
||||
|
||||
drop_attr(LinkList) ->
|
||||
lists:foldr(
|
||||
fun(Link, {AlternatePath, LinkAcc}) ->
|
||||
case parse_link(Link) of
|
||||
{false, MainLink} -> {AlternatePath, [MainLink | LinkAcc]};
|
||||
{true, MainLink} -> {MainLink, LinkAcc}
|
||||
end
|
||||
end, {<<"/">>, []}, LinkList).
|
||||
|
||||
parse_link(Link) ->
|
||||
[MainLink | Attrs] = binary:split(trim(Link), <<";">>, [global]),
|
||||
{is_alternate_path(Attrs), delink(trim(MainLink))}.
|
||||
|
||||
is_alternate_path(LinkAttrs) ->
|
||||
lists:any(fun(Attr) ->
|
||||
case binary:split(trim(Attr), <<"=">>) of
|
||||
[<<"rt">>, ?OMA_ALTER_PATH_RT] ->
|
||||
true;
|
||||
[AttrKey, _] when AttrKey =/= <<>> ->
|
||||
false;
|
||||
_BadAttr -> throw({bad_attr, _BadAttr})
|
||||
end
|
||||
end,
|
||||
LinkAttrs).
|
||||
|
||||
trim(Str)-> binary_util:trim(Str, $ ).
|
||||
|
||||
delink(Str) ->
|
||||
Ltrim = binary_util:ltrim(Str, $<),
|
||||
binary_util:rtrim(Ltrim, $>).
|
||||
|
||||
get_headers(RegInfo) ->
|
||||
lists:foldl(fun(K, Acc) ->
|
||||
get_header(K, RegInfo, Acc)
|
||||
end,
|
||||
extract_module_params(RegInfo),
|
||||
[<<"apn">>, <<"im">>, <<"ct">>, <<"mv">>, <<"mt">>]).
|
||||
|
||||
get_header(Key, RegInfo, Headers) ->
|
||||
case maps:get(Key, RegInfo, undefined) of
|
||||
undefined ->
|
||||
Headers;
|
||||
Val ->
|
||||
AtomKey = erlang:binary_to_atom(Key),
|
||||
Headers#{AtomKey => Val}
|
||||
end.
|
||||
|
||||
extract_module_params(RegInfo) ->
|
||||
Keys = [<<"module">>, <<"sv">>, <<"chip">>, <<"imsi">>, <<"iccid">>],
|
||||
case lists:any(fun(K) -> maps:get(K, RegInfo, undefined) =:= undefined end, Keys) of
|
||||
true -> #{module_params => undefined};
|
||||
false ->
|
||||
Extras = [<<"rsrp">>, <<"sinr">>, <<"txpower">>, <<"cellid">>],
|
||||
case lists:any(fun(K) -> maps:get(K, RegInfo, undefined) =:= undefined end, Extras) of
|
||||
true ->
|
||||
#{module_params =>
|
||||
#{module => maps:get(<<"module">>, RegInfo),
|
||||
softversion => maps:get(<<"sv">>, RegInfo),
|
||||
chiptype => maps:get(<<"chip">>, RegInfo),
|
||||
imsi => maps:get(<<"imsi">>, RegInfo),
|
||||
iccid => maps:get(<<"iccid">>, RegInfo)}};
|
||||
false ->
|
||||
#{module_params =>
|
||||
#{module => maps:get(<<"module">>, RegInfo),
|
||||
softversion => maps:get(<<"sv">>, RegInfo),
|
||||
chiptype => maps:get(<<"chip">>, RegInfo),
|
||||
imsi => maps:get(<<"imsi">>, RegInfo),
|
||||
iccid => maps:get(<<"iccid">>, RegInfo),
|
||||
rsrp => maps:get(<<"rsrp">>, RegInfo),
|
||||
sinr => maps:get(<<"sinr">>, RegInfo),
|
||||
txpower => maps:get(<<"txpower">>, RegInfo),
|
||||
cellid => maps:get(<<"cellid">>, RegInfo)}}
|
||||
end
|
||||
end.
|
||||
|
||||
get_lifetime(#{<<"lt">> := LT}) ->
|
||||
case LT of
|
||||
0 -> emqx:get_config([gateway, lwm2m, lifetime_max]);
|
||||
_ -> LT * 1000
|
||||
end;
|
||||
get_lifetime(_) ->
|
||||
emqx:get_config([gateway, lwm2m, lifetime_max]).
|
||||
|
||||
get_lifetime(#{<<"lt">> := _} = NewRegInfo, _) ->
|
||||
get_lifetime(NewRegInfo);
|
||||
|
||||
get_lifetime(_, OldRegInfo) ->
|
||||
get_lifetime(OldRegInfo).
|
||||
|
||||
-spec update(emqx_coap_message(), function(), binary(), session()) -> map().
|
||||
update(#coap_message{options = Opts, payload = Payload} = Msg,
|
||||
Validator,
|
||||
CmdType,
|
||||
#session{reg_info = OldRegInfo} = Session) ->
|
||||
Query = maps:get(uri_query, Opts),
|
||||
RegInfo = append_object_list(Query, Payload),
|
||||
UpdateRegInfo = maps:merge(OldRegInfo, RegInfo),
|
||||
LifeTime = get_lifetime(UpdateRegInfo, OldRegInfo),
|
||||
|
||||
Session2 = proto_subscribe(Validator,
|
||||
Session#session{reg_info = UpdateRegInfo,
|
||||
lifetime = LifeTime}),
|
||||
Session3 = send_dl_msg(Session2),
|
||||
RegPayload = #{<<"data">> => UpdateRegInfo},
|
||||
Session4 = send_to_mqtt(#{}, CmdType, RegPayload, Validator, Session3),
|
||||
|
||||
Result = return(Session4),
|
||||
|
||||
Reply = emqx_coap_message:piggyback({ok, changed}, Msg),
|
||||
reply(Reply, Result#{lifetime => true}).
|
||||
|
||||
register_init(Validator, #session{reg_info = RegInfo,
|
||||
endpoint_name = Epn} = Session) ->
|
||||
|
||||
Session2 = send_auto_observe(RegInfo, Session),
|
||||
%% - subscribe to the downlink_topic and wait for commands
|
||||
#{topic := Topic, qos := Qos} = downlink_topic(),
|
||||
MountedTopic = mount(Topic, mountpoint(Epn)),
|
||||
Session3 = subscribe(MountedTopic, Qos, Validator, Session2),
|
||||
Session4 = send_dl_msg(Session3),
|
||||
|
||||
%% - report the registration info
|
||||
RegPayload = #{<<"data">> => RegInfo},
|
||||
send_to_mqtt(#{}, <<"register">>, RegPayload, Validator, Session4).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Subscribe
|
||||
%%--------------------------------------------------------------------
|
||||
proto_subscribe(Validator, #session{endpoint_name = Epn, wait_ack = WaitAck} = Session) ->
|
||||
#{topic := Topic, qos := Qos} = downlink_topic(),
|
||||
MountedTopic = mount(Topic, mountpoint(Epn)),
|
||||
Session2 = case WaitAck of
|
||||
undefined ->
|
||||
Session;
|
||||
Ctx ->
|
||||
MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, <<"coap_timeout">>),
|
||||
send_to_mqtt(Ctx, <<"coap_timeout">>, MqttPayload, Validator, Session)
|
||||
end,
|
||||
subscribe(MountedTopic, Qos, Validator, Session2).
|
||||
|
||||
subscribe(Topic, Qos, Validator,
|
||||
#session{headers = Headers, endpoint_name = EndpointName} = Session) ->
|
||||
case Validator(subscribe, Topic) of
|
||||
allow ->
|
||||
ClientId = maps:get(device_id, Headers, undefined),
|
||||
Opts = get_sub_opts(Qos),
|
||||
?LOG(debug, "Subscribe topic: ~0p, Opts: ~0p, EndpointName: ~0p", [Topic, Opts, EndpointName]),
|
||||
emqx:subscribe(Topic, ClientId, Opts);
|
||||
_ ->
|
||||
?LOG(error, "Topic: ~0p not allow to subscribe", [Topic])
|
||||
end,
|
||||
Session.
|
||||
|
||||
send_auto_observe(RegInfo, Session) ->
|
||||
%% - auto observe the objects
|
||||
case is_auto_observe() of
|
||||
true ->
|
||||
AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>),
|
||||
ObjectList = maps:get(<<"objectList">>, RegInfo, []),
|
||||
observe_object_list(AlternatePath, ObjectList, Session);
|
||||
_ ->
|
||||
?LOG(info, "Auto Observe Disabled", []),
|
||||
Session
|
||||
end.
|
||||
|
||||
observe_object_list(_, [], Session) ->
|
||||
Session;
|
||||
observe_object_list(AlternatePath, ObjectList, Session) ->
|
||||
Fun = fun(ObjectPath, Acc) ->
|
||||
{[ObjId| _], _} = emqx_lwm2m_cmd:path_list(ObjectPath),
|
||||
case lists:member(ObjId, ?IGNORE_OBJECT) of
|
||||
true -> Acc;
|
||||
false ->
|
||||
try
|
||||
emqx_lwm2m_xml_object_db:find_objectid(binary_to_integer(ObjId)),
|
||||
observe_object(AlternatePath, ObjectPath, Acc)
|
||||
catch error:no_xml_definition ->
|
||||
Acc
|
||||
end
|
||||
end
|
||||
end,
|
||||
lists:foldl(Fun, Session, ObjectList).
|
||||
|
||||
observe_object(AlternatePath, ObjectPath, Session) ->
|
||||
Payload = #{<<"msgType">> => <<"observe">>,
|
||||
<<"data">> => #{<<"path">> => ObjectPath},
|
||||
<<"is_auto_observe">> => true
|
||||
},
|
||||
deliver_auto_observe_to_coap(AlternatePath, Payload, Session).
|
||||
|
||||
deliver_auto_observe_to_coap(AlternatePath, TermData, Session) ->
|
||||
?LOG(info, "Auto Observe, SEND To CoAP, AlternatePath=~0p, Data=~0p ", [AlternatePath, TermData]),
|
||||
{Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData),
|
||||
maybe_do_deliver_to_coap(Ctx, Req, 0, false, Session).
|
||||
|
||||
get_sub_opts(Qos) ->
|
||||
#{
|
||||
qos => Qos,
|
||||
rap => 0,
|
||||
nl => 0,
|
||||
rh => 0,
|
||||
is_new => false
|
||||
}.
|
||||
|
||||
is_auto_observe() ->
|
||||
emqx:get_config([gateway, lwm2m, auto_observe]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Response
|
||||
%%--------------------------------------------------------------------
|
||||
handle_coap_response({Ctx = #{<<"msgType">> := EventType},
|
||||
#coap_message{method = CoapMsgMethod,
|
||||
type = CoapMsgType,
|
||||
payload = CoapMsgPayload,
|
||||
options = CoapMsgOpts}},
|
||||
Validator,
|
||||
Session) ->
|
||||
MqttPayload = emqx_lwm2m_cmd:coap_to_mqtt(CoapMsgMethod, CoapMsgPayload, CoapMsgOpts, Ctx),
|
||||
{ReqPath, _} = emqx_lwm2m_cmd:path_list(emqx_lwm2m_cmd:extract_path(Ctx)),
|
||||
Session2 =
|
||||
case {ReqPath, MqttPayload, EventType, CoapMsgType} of
|
||||
{[<<"5">>| _], _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack ->
|
||||
%% this is a notification for status update during NB firmware upgrade.
|
||||
%% need to reply to DM http callbacks
|
||||
send_to_mqtt(Ctx, <<"notify">>, MqttPayload, ?lwm2m_up_dm_topic, Validator, Session);
|
||||
{_ReqPath, _, <<"observe">>, CoapMsgType} when CoapMsgType =/= ack ->
|
||||
%% this is actually a notification, correct the msgType
|
||||
send_to_mqtt(Ctx, <<"notify">>, MqttPayload, Validator, Session);
|
||||
_ ->
|
||||
send_to_mqtt(Ctx, EventType, MqttPayload, Validator, Session)
|
||||
end,
|
||||
send_dl_msg(Ctx, Session2).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Ack
|
||||
%%--------------------------------------------------------------------
|
||||
handle_ack({Ctx, _}, Validator, Session) ->
|
||||
Session2 = send_dl_msg(Ctx, Session),
|
||||
MqttPayload = emqx_lwm2m_cmd:empty_ack_to_mqtt(Ctx),
|
||||
send_to_mqtt(Ctx, <<"ack">>, MqttPayload, Validator, Session2).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Ack Failure(Timeout/Reset)
|
||||
%%--------------------------------------------------------------------
|
||||
handle_ack_failure({Ctx, _}, Validator, Session) ->
|
||||
handle_ack_failure(Ctx, <<"coap_timeout">>, Validator, Session).
|
||||
|
||||
handle_ack_reset({Ctx, _}, Validator, Session) ->
|
||||
handle_ack_failure(Ctx, <<"coap_reset">>, Validator, Session).
|
||||
|
||||
handle_ack_failure(Ctx, MsgType, Validator, Session) ->
|
||||
Session2 = may_send_dl_msg(coap_timeout, Ctx, Session),
|
||||
MqttPayload = emqx_lwm2m_cmd:coap_failure_to_mqtt(Ctx, MsgType),
|
||||
send_to_mqtt(Ctx, MsgType, MqttPayload, Validator, Session2).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Send To CoAP
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
may_send_dl_msg(coap_timeout, Ctx, #session{headers = Headers,
|
||||
reg_info = RegInfo,
|
||||
wait_ack = WaitAck} = Session) ->
|
||||
Lwm2mMode = maps:get(lwm2m_model, Headers, undefined),
|
||||
case is_cache_mode(Lwm2mMode, RegInfo, Session) of
|
||||
false -> send_dl_msg(Ctx, Session);
|
||||
true ->
|
||||
case WaitAck of
|
||||
Ctx ->
|
||||
Session#session{wait_ack = undefined};
|
||||
_ ->
|
||||
Session
|
||||
end
|
||||
end.
|
||||
|
||||
is_cache_mode(Lwm2mMode, RegInfo, #session{last_active_at = LastActiveAt}) ->
|
||||
case Lwm2mMode =:= psm orelse is_psm(RegInfo) orelse is_qmode(RegInfo) of
|
||||
true ->
|
||||
QModeTimeWind = emqx:get_config([gateway, lwm2m, qmode_time_window]),
|
||||
Now = ?NOW,
|
||||
(Now - LastActiveAt) >= QModeTimeWind;
|
||||
false -> false
|
||||
end.
|
||||
|
||||
is_psm(#{<<"apn">> := APN}) when APN =:= <<"Ctnb">>;
|
||||
APN =:= <<"psmA.eDRX0.ctnb">>;
|
||||
APN =:= <<"psmC.eDRX0.ctnb">>;
|
||||
APN =:= <<"psmF.eDRXC.ctnb">>
|
||||
-> true;
|
||||
is_psm(_) -> false.
|
||||
|
||||
is_qmode(#{<<"b">> := Binding}) when Binding =:= <<"UQ">>;
|
||||
Binding =:= <<"SQ">>;
|
||||
Binding =:= <<"UQS">>
|
||||
-> true;
|
||||
is_qmode(_) -> false.
|
||||
|
||||
send_dl_msg(Session) ->
|
||||
%% if has in waiting donot send
|
||||
case Session#session.wait_ack of
|
||||
undefined ->
|
||||
send_to_coap(Session);
|
||||
_ ->
|
||||
Session
|
||||
end.
|
||||
|
||||
send_dl_msg(Ctx, Session) ->
|
||||
case Session#session.wait_ack of
|
||||
undefined ->
|
||||
send_to_coap(Session);
|
||||
Ctx ->
|
||||
send_to_coap(Session#session{wait_ack = undefined});
|
||||
_ ->
|
||||
Session
|
||||
end.
|
||||
|
||||
send_to_coap(#session{queue = Queue} = Session) ->
|
||||
case queue:out(Queue) of
|
||||
{{value, {Timestamp, Ctx, Req}}, Q2} ->
|
||||
Now = ?NOW,
|
||||
if Timestamp =:= 0 orelse Timestamp > Now ->
|
||||
send_to_coap(Ctx, Req, Session#session{queue = Q2});
|
||||
true ->
|
||||
send_to_coap(Session#session{queue = Q2})
|
||||
end;
|
||||
{empty, _} ->
|
||||
Session
|
||||
end.
|
||||
|
||||
send_to_coap(Ctx, Req, Session) ->
|
||||
?LOG(debug, "Deliver To CoAP, CoapRequest: ~0p", [Req]),
|
||||
out_to_coap(Ctx, Req, Session#session{wait_ack = Ctx}).
|
||||
|
||||
send_msg_not_waiting_ack(Ctx, Req, Session) ->
|
||||
?LOG(debug, "Deliver To CoAP not waiting ack, CoapRequest: ~0p", [Req]),
|
||||
%% cmd_sent(Ref, LwM2MOpts).
|
||||
out_to_coap(Ctx, Req, Session).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Send To MQTT
|
||||
%%--------------------------------------------------------------------
|
||||
send_to_mqtt(Ref, EventType, Payload, Validator, Session = #session{headers = Headers}) ->
|
||||
#{topic := Topic, qos := Qos} = uplink_topic(EventType),
|
||||
NHeaders = extract_ext_flags(Headers),
|
||||
Mheaders = maps:get(mheaders, Ref, #{}),
|
||||
NHeaders1 = maps:merge(NHeaders, Mheaders),
|
||||
proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, NHeaders1, Validator, Session).
|
||||
|
||||
send_to_mqtt(Ctx, EventType, Payload, {Topic, Qos},
|
||||
Validator, #session{headers = Headers} = Session) ->
|
||||
Mheaders = maps:get(mheaders, Ctx, #{}),
|
||||
NHeaders = extract_ext_flags(Headers),
|
||||
NHeaders1 = maps:merge(NHeaders, Mheaders),
|
||||
proto_publish(Topic, Payload#{<<"msgType">> => EventType}, Qos, NHeaders1, Validator, Session).
|
||||
|
||||
proto_publish(Topic, Payload, Qos, Headers, Validator,
|
||||
#session{endpoint_name = Epn} = Session) ->
|
||||
MountedTopic = mount(Topic, mountpoint(Epn)),
|
||||
_ = case Validator(publish, MountedTopic) of
|
||||
allow ->
|
||||
Msg = emqx_message:make(Epn, Qos, MountedTopic,
|
||||
emqx_json:encode(Payload), #{}, Headers),
|
||||
emqx:publish(Msg);
|
||||
_ ->
|
||||
?LOG(error, "topic:~p not allow to publish ", [MountedTopic])
|
||||
end,
|
||||
Session.
|
||||
|
||||
mountpoint(Epn) ->
|
||||
Prefix = emqx:get_config([gateway, lwm2m, mountpoint]),
|
||||
<<Prefix/binary, "/", Epn/binary, "/">>.
|
||||
|
||||
mount(Topic, MountPoint) when is_binary(Topic), is_binary(MountPoint) ->
|
||||
<<MountPoint/binary, Topic/binary>>.
|
||||
|
||||
extract_ext_flags(Headers) ->
|
||||
Header0 = #{is_tr => maps:get(is_tr, Headers, true)},
|
||||
check(Header0, Headers, [sota_type, appId, nbgwFlag]).
|
||||
|
||||
check(Params, _Headers, []) -> Params;
|
||||
check(Params, Headers, [Key | Rest]) ->
|
||||
case maps:get(Key, Headers, null) of
|
||||
V when V == undefined; V == null ->
|
||||
check(Params, Headers, Rest);
|
||||
Value ->
|
||||
Params1 = Params#{Key => Value},
|
||||
check(Params1, Headers, Rest)
|
||||
end.
|
||||
|
||||
downlink_topic() ->
|
||||
emqx:get_config([gateway, lwm2m, translators, command]).
|
||||
|
||||
uplink_topic(<<"notify">>) ->
|
||||
emqx:get_config([gateway, lwm2m, translators, notify]);
|
||||
|
||||
uplink_topic(<<"register">>) ->
|
||||
emqx:get_config([gateway, lwm2m, translators, register]);
|
||||
|
||||
uplink_topic(<<"update">>) ->
|
||||
emqx:get_config([gateway, lwm2m, translators, update]);
|
||||
|
||||
uplink_topic(_) ->
|
||||
emqx:get_config([gateway, lwm2m, translators, response]).
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Deliver
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
deliver(Delivers, #session{headers = Headers, reg_info = RegInfo} = Session) ->
|
||||
Lwm2mMode = maps:get(lwm2m_model, Headers, undefined),
|
||||
IsCacheMode = is_cache_mode(Lwm2mMode, RegInfo, Session),
|
||||
AlternatePath = maps:get(<<"alternatePath">>, RegInfo, <<"/">>),
|
||||
lists:foldl(fun({deliver, _, MQTT}, Acc) ->
|
||||
deliver_to_coap(AlternatePath,
|
||||
MQTT#message.payload, MQTT, IsCacheMode, Acc)
|
||||
end,
|
||||
Session,
|
||||
Delivers).
|
||||
|
||||
deliver_to_coap(AlternatePath, JsonData, MQTT, CacheMode, Session) when is_binary(JsonData)->
|
||||
try
|
||||
TermData = emqx_json:decode(JsonData, [return_maps]),
|
||||
deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, Session)
|
||||
catch
|
||||
ExClass:Error:ST ->
|
||||
?LOG(error, "deliver_to_coap - Invalid JSON: ~0p, Exception: ~0p, stacktrace: ~0p",
|
||||
[JsonData, {ExClass, Error}, ST]),
|
||||
Session
|
||||
end;
|
||||
|
||||
deliver_to_coap(AlternatePath, TermData, MQTT, CacheMode, Session) when is_map(TermData) ->
|
||||
{Req, Ctx} = emqx_lwm2m_cmd:mqtt_to_coap(AlternatePath, TermData),
|
||||
ExpiryTime = get_expiry_time(MQTT),
|
||||
maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode, Session).
|
||||
|
||||
maybe_do_deliver_to_coap(Ctx, Req, ExpiryTime, CacheMode,
|
||||
#session{wait_ack = WaitAck,
|
||||
queue = Queue} = Session) ->
|
||||
MHeaders = maps:get(mheaders, Ctx, #{}),
|
||||
TTL = maps:get(<<"ttl">>, MHeaders, 7200),
|
||||
case TTL of
|
||||
0 ->
|
||||
send_msg_not_waiting_ack(Ctx, Req, Session);
|
||||
_ ->
|
||||
case not CacheMode
|
||||
andalso queue:is_empty(Queue) andalso WaitAck =:= undefined of
|
||||
true ->
|
||||
send_to_coap(Ctx, Req, Session);
|
||||
false ->
|
||||
Session#session{queue = queue:in({ExpiryTime, Ctx, Req}, Queue)}
|
||||
end
|
||||
end.
|
||||
|
||||
get_expiry_time(#message{headers = #{properties := #{'Message-Expiry-Interval' := Interval}},
|
||||
timestamp = Ts}) ->
|
||||
Ts + Interval * 1000;
|
||||
get_expiry_time(_) ->
|
||||
0.
|
||||
|
||||
%%--------------------------------------------------------------------
|
||||
%% Call CoAP
|
||||
%%--------------------------------------------------------------------
|
||||
call_coap(Fun, Msg, #session{coap = Coap} = Session) ->
|
||||
iter([tm, fun process_tm/4, fun process_session/3],
|
||||
emqx_coap_tm:Fun(Msg, Coap),
|
||||
Session).
|
||||
|
||||
process_tm(TM, Result, Session, Cursor) ->
|
||||
iter(Cursor, Result, Session#session{coap = TM}).
|
||||
|
||||
process_session(_, Result, Session) ->
|
||||
Result#{session => Session}.
|
||||
|
||||
out_to_coap(Context, Msg, Session) ->
|
||||
out_to_coap({Context, Msg}, Session).
|
||||
|
||||
out_to_coap(Msg, Session) ->
|
||||
Outs = get_outs(),
|
||||
erlang:put(?OUT_LIST_KEY, [Msg | Outs]),
|
||||
Session.
|
||||
|
||||
get_outs() ->
|
||||
case erlang:get(?OUT_LIST_KEY) of
|
||||
undefined -> [];
|
||||
Any -> Any
|
||||
end.
|
||||
|
||||
return(#session{coap = CoAP} = Session) ->
|
||||
Outs = get_outs(),
|
||||
erlang:put(?OUT_LIST_KEY, []),
|
||||
{ok, Coap2, Msgs} = do_out(Outs, CoAP, []),
|
||||
#{return => {Msgs, Session#session{coap = Coap2}}}.
|
||||
|
||||
do_out([{Ctx, Out} | T], TM, Msgs) ->
|
||||
%% TODO maybe set a special token?
|
||||
#{out := [Msg],
|
||||
tm := TM2} = emqx_coap_tm:handle_out(Out, Ctx, TM),
|
||||
do_out(T, TM2, [Msg | Msgs]);
|
||||
|
||||
do_out(_, TM, Msgs) ->
|
||||
{ok, TM, Msgs}.
|
|
@ -1,47 +0,0 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_lwm2m_timer).
|
||||
|
||||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
|
||||
-export([ cancel_timer/1
|
||||
, start_timer/2
|
||||
, refresh_timer/1
|
||||
, refresh_timer/2
|
||||
]).
|
||||
|
||||
-record(timer_state, { interval
|
||||
, tref
|
||||
, message
|
||||
}).
|
||||
|
||||
-define(LOG(Level, Format, Args),
|
||||
logger:Level("LWM2M-TIMER: " ++ Format, Args)).
|
||||
|
||||
cancel_timer(#timer_state{tref = TRef}) when is_reference(TRef) ->
|
||||
_ = erlang:cancel_timer(TRef), ok.
|
||||
|
||||
refresh_timer(State=#timer_state{interval = Interval, message = Msg}) ->
|
||||
cancel_timer(State), start_timer(Interval, Msg).
|
||||
refresh_timer(NewInterval, State=#timer_state{message = Msg}) ->
|
||||
cancel_timer(State), start_timer(NewInterval, Msg).
|
||||
|
||||
%% start timer in seconds
|
||||
start_timer(Interval, Msg) ->
|
||||
?LOG(debug, "start_timer of ~p secs", [Interval]),
|
||||
TRef = erlang:send_after(timer:seconds(Interval), self(), Msg),
|
||||
#timer_state{interval = Interval, tref = TRef, message = Msg}.
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
-module(emqx_lwm2m_xml_object).
|
||||
|
||||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
-include_lib("xmerl/include/xmerl.hrl").
|
||||
|
||||
-export([ get_obj_def/2
|
||||
|
@ -38,8 +38,6 @@ get_obj_def(ObjectIdInt, true) ->
|
|||
get_obj_def(ObjectNameStr, false) ->
|
||||
emqx_lwm2m_xml_object_db:find_name(ObjectNameStr).
|
||||
|
||||
|
||||
|
||||
get_object_id(ObjDefinition) ->
|
||||
[#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition),
|
||||
ObjectId.
|
||||
|
@ -48,7 +46,6 @@ get_object_name(ObjDefinition) ->
|
|||
[#xmlText{value=ObjectName}] = xmerl_xpath:string("Name/text()", ObjDefinition),
|
||||
ObjectName.
|
||||
|
||||
|
||||
get_object_and_resource_id(ResourceNameBinary, ObjDefinition) ->
|
||||
ResourceNameString = binary_to_list(ResourceNameBinary),
|
||||
[#xmlText{value=ObjectId}] = xmerl_xpath:string("ObjectID/text()", ObjDefinition),
|
||||
|
@ -56,7 +53,6 @@ get_object_and_resource_id(ResourceNameBinary, ObjDefinition) ->
|
|||
?LOG(debug, "get_object_and_resource_id ObjectId=~p, ResourceId=~p", [ObjectId, ResourceId]),
|
||||
{ObjectId, ResourceId}.
|
||||
|
||||
|
||||
get_resource_type(ResourceIdInt, ObjDefinition) ->
|
||||
ResourceIdString = integer_to_list(ResourceIdInt),
|
||||
[#xmlText{value=DataType}] = xmerl_xpath:string("Resources/Item[@ID=\""++ResourceIdString++"\"]/Type/text()", ObjDefinition),
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
-module(emqx_lwm2m_xml_object_db).
|
||||
|
||||
-include("src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
-include_lib("emqx_gateway/src/lwm2m/include/emqx_lwm2m.hrl").
|
||||
-include_lib("xmerl/include/xmerl.hrl").
|
||||
|
||||
% This module is for future use. Disabled now.
|
||||
|
@ -49,15 +49,14 @@
|
|||
%% API Function Definitions
|
||||
%% ------------------------------------------------------------------
|
||||
|
||||
-spec start_link(binary() | string()) -> {ok, pid()} | ignore | {error, any()}.
|
||||
start_link(XmlDir) ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [XmlDir], []).
|
||||
|
||||
find_objectid(ObjectId) ->
|
||||
ObjectIdInt = case is_list(ObjectId) of
|
||||
true -> list_to_integer(ObjectId);
|
||||
false -> ObjectId
|
||||
end,
|
||||
ObjectIdInt = case is_list(ObjectId) of
|
||||
true -> list_to_integer(ObjectId);
|
||||
false -> ObjectId
|
||||
end,
|
||||
case ets:lookup(?LWM2M_OBJECT_DEF_TAB, ObjectIdInt) of
|
||||
[] -> {error, no_xml_definition};
|
||||
[{ObjectId, Xml}] -> Xml
|
||||
|
@ -81,15 +80,14 @@ find_name(Name) ->
|
|||
stop() ->
|
||||
gen_server:stop(?MODULE).
|
||||
|
||||
|
||||
%% ------------------------------------------------------------------
|
||||
%% gen_server Function Definitions
|
||||
%% ------------------------------------------------------------------
|
||||
|
||||
init([XmlDir0]) ->
|
||||
init([XmlDir]) ->
|
||||
_ = ets:new(?LWM2M_OBJECT_DEF_TAB, [set, named_table, protected]),
|
||||
_ = ets:new(?LWM2M_OBJECT_NAME_TO_ID_TAB, [set, named_table, protected]),
|
||||
load(to_list(XmlDir0)),
|
||||
load(XmlDir),
|
||||
{ok, #state{}}.
|
||||
|
||||
handle_call(_Request, _From, State) ->
|
||||
|
@ -113,11 +111,13 @@ code_change(_OldVsn, State, _Extra) ->
|
|||
%% Internal functions
|
||||
%%--------------------------------------------------------------------
|
||||
load(BaseDir) ->
|
||||
Wild = case lists:last(BaseDir) == $/ of
|
||||
true -> BaseDir++"*.xml";
|
||||
false -> BaseDir++"/*.xml"
|
||||
end,
|
||||
case filelib:wildcard(Wild) of
|
||||
Wild = filename:join(BaseDir, "*.xml"),
|
||||
Wild2 = if is_binary(Wild) ->
|
||||
erlang:binary_to_list(Wild);
|
||||
true ->
|
||||
Wild
|
||||
end,
|
||||
case filelib:wildcard(Wild2) of
|
||||
[] -> error(no_xml_files_found, BaseDir);
|
||||
AllXmlFiles -> load_loop(AllXmlFiles)
|
||||
end.
|
||||
|
@ -135,13 +135,7 @@ load_loop([FileName|T]) ->
|
|||
ets:insert(?LWM2M_OBJECT_NAME_TO_ID_TAB, {NameBinary, ObjectId}),
|
||||
load_loop(T).
|
||||
|
||||
|
||||
load_xml(FileName) ->
|
||||
{Xml, _Rest} = xmerl_scan:file(FileName),
|
||||
[ObjectXml] = xmerl_xpath:string("/LWM2M/Object", Xml),
|
||||
ObjectXml.
|
||||
|
||||
to_list(B) when is_binary(B) ->
|
||||
binary_to_list(B);
|
||||
to_list(S) when is_list(S) ->
|
||||
S.
|
||||
|
|
|
@ -14,15 +14,8 @@
|
|||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-define(APP, emqx_lwm2m).
|
||||
-define(LWAPP, emqx_lwm2m).
|
||||
|
||||
-record(coap_mqtt_auth, { clientid
|
||||
, username
|
||||
, password
|
||||
}).
|
||||
-record(lwm2m_context, { epn
|
||||
, location
|
||||
}).
|
||||
|
||||
-define(OMA_ALTER_PATH_RT, <<"\"oma.lwm2m\"">>).
|
||||
|
||||
|
@ -42,7 +35,7 @@
|
|||
-define(ERR_NOT_FOUND, <<"Not Found">>).
|
||||
-define(ERR_UNAUTHORIZED, <<"Unauthorized">>).
|
||||
-define(ERR_BAD_REQUEST, <<"Bad Request">>).
|
||||
|
||||
-define(REG_PREFIX, <<"rd">>).
|
||||
|
||||
-define(LWM2M_FORMAT_PLAIN_TEXT, 0).
|
||||
-define(LWM2M_FORMAT_LINK, 40).
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue