Merge pull request #5598 from lafirest/refactor/emqx_lwm2m_c

refactor(emqx_lwm2m): port lwm2m into emqx_gateway framework
This commit is contained in:
lafirest 2021-09-02 16:40:54 +08:00 committed by GitHub
commit 187f878baf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 3243 additions and 3171 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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}}.

View File

@ -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

View File

@ -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
%%--------------------------------------------------------------------

View File

@ -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).

View File

@ -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().

View File

@ -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}.

View File

@ -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.

View File

@ -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.

View File

@ -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).

View File

@ -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).

View File

@ -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{}.

View File

@ -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,

View File

@ -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});

View File

@ -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}.

View File

@ -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).

View File

@ -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">>.

View File

@ -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.

View File

@ -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.

View File

@ -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).

View File

@ -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).

View File

@ -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]).

View File

@ -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}.

View File

@ -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}.

View File

@ -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),

View File

@ -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.

View File

@ -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