Merge pull request #11852 from lafirest/feat/gbt_gw
feat(gbt32960): Port the GBT32960 gateway from v4
This commit is contained in:
commit
17544dc410
|
@ -93,10 +93,9 @@ gateways(get, Request) ->
|
||||||
|
|
||||||
gateway(get, #{bindings := #{name := Name}}) ->
|
gateway(get, #{bindings := #{name := Name}}) ->
|
||||||
try
|
try
|
||||||
GwName = gw_name(Name),
|
case emqx_gateway:lookup(Name) of
|
||||||
case emqx_gateway:lookup(GwName) of
|
|
||||||
undefined ->
|
undefined ->
|
||||||
{200, #{name => GwName, status => unloaded}};
|
{200, #{name => Name, status => unloaded}};
|
||||||
Gateway ->
|
Gateway ->
|
||||||
GwConf = emqx_gateway_conf:gateway(Name),
|
GwConf = emqx_gateway_conf:gateway(Name),
|
||||||
GwInfo0 = emqx_gateway_utils:unix_ts_to_rfc3339(
|
GwInfo0 = emqx_gateway_utils:unix_ts_to_rfc3339(
|
||||||
|
@ -125,15 +124,14 @@ gateway(put, #{
|
||||||
}) ->
|
}) ->
|
||||||
GwConf = maps:without([<<"name">>], GwConf0),
|
GwConf = maps:without([<<"name">>], GwConf0),
|
||||||
try
|
try
|
||||||
GwName = gw_name(Name),
|
|
||||||
LoadOrUpdateF =
|
LoadOrUpdateF =
|
||||||
case emqx_gateway:lookup(GwName) of
|
case emqx_gateway:lookup(Name) of
|
||||||
undefined ->
|
undefined ->
|
||||||
fun emqx_gateway_conf:load_gateway/2;
|
fun emqx_gateway_conf:load_gateway/2;
|
||||||
_ ->
|
_ ->
|
||||||
fun emqx_gateway_conf:update_gateway/2
|
fun emqx_gateway_conf:update_gateway/2
|
||||||
end,
|
end,
|
||||||
case LoadOrUpdateF(GwName, GwConf) of
|
case LoadOrUpdateF(Name, GwConf) of
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{204};
|
{204};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
|
@ -148,12 +146,11 @@ gateway(put, #{
|
||||||
|
|
||||||
gateway_enable(put, #{bindings := #{name := Name, enable := Enable}}) ->
|
gateway_enable(put, #{bindings := #{name := Name, enable := Enable}}) ->
|
||||||
try
|
try
|
||||||
GwName = gw_name(Name),
|
case emqx_gateway:lookup(Name) of
|
||||||
case emqx_gateway:lookup(GwName) of
|
|
||||||
undefined ->
|
undefined ->
|
||||||
return_http_error(404, <<"NOT FOUND">>);
|
return_http_error(404, <<"NOT FOUND">>);
|
||||||
_Gateway ->
|
_Gateway ->
|
||||||
{ok, _} = emqx_gateway_conf:update_gateway(GwName, #{<<"enable">> => Enable}),
|
{ok, _} = emqx_gateway_conf:update_gateway(Name, #{<<"enable">> => Enable}),
|
||||||
{204}
|
{204}
|
||||||
end
|
end
|
||||||
catch
|
catch
|
||||||
|
@ -161,14 +158,6 @@ gateway_enable(put, #{bindings := #{name := Name, enable := Enable}}) ->
|
||||||
return_http_error(404, <<"NOT FOUND">>)
|
return_http_error(404, <<"NOT FOUND">>)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec gw_name(binary()) -> stomp | coap | lwm2m | mqttsn | exproto | no_return().
|
|
||||||
gw_name(<<"stomp">>) -> stomp;
|
|
||||||
gw_name(<<"coap">>) -> coap;
|
|
||||||
gw_name(<<"lwm2m">>) -> lwm2m;
|
|
||||||
gw_name(<<"mqttsn">>) -> mqttsn;
|
|
||||||
gw_name(<<"exproto">>) -> exproto;
|
|
||||||
gw_name(_Else) -> throw(not_found).
|
|
||||||
|
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% Swagger defines
|
%% Swagger defines
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
@ -249,7 +238,7 @@ params_gateway_name_in_path() ->
|
||||||
[
|
[
|
||||||
{name,
|
{name,
|
||||||
mk(
|
mk(
|
||||||
binary(),
|
hoconsc:enum(emqx_gateway_schema:gateway_names()),
|
||||||
#{
|
#{
|
||||||
in => path,
|
in => path,
|
||||||
desc => ?DESC(gateway_name_in_qs),
|
desc => ?DESC(gateway_name_in_qs),
|
||||||
|
@ -390,7 +379,8 @@ fields(Gw) when
|
||||||
Gw == mqttsn;
|
Gw == mqttsn;
|
||||||
Gw == coap;
|
Gw == coap;
|
||||||
Gw == lwm2m;
|
Gw == lwm2m;
|
||||||
Gw == exproto
|
Gw == exproto;
|
||||||
|
Gw == gbt32960
|
||||||
->
|
->
|
||||||
[{name, mk(Gw, #{desc => ?DESC(gateway_name)})}] ++
|
[{name, mk(Gw, #{desc => ?DESC(gateway_name)})}] ++
|
||||||
convert_listener_struct(emqx_gateway_schema:gateway_schema(Gw));
|
convert_listener_struct(emqx_gateway_schema:gateway_schema(Gw));
|
||||||
|
@ -399,7 +389,8 @@ fields(Gw) when
|
||||||
Gw == update_mqttsn;
|
Gw == update_mqttsn;
|
||||||
Gw == update_coap;
|
Gw == update_coap;
|
||||||
Gw == update_lwm2m;
|
Gw == update_lwm2m;
|
||||||
Gw == update_exproto
|
Gw == update_exproto;
|
||||||
|
Gw == update_gbt32960
|
||||||
->
|
->
|
||||||
"update_" ++ GwStr = atom_to_list(Gw),
|
"update_" ++ GwStr = atom_to_list(Gw),
|
||||||
Gw1 = list_to_existing_atom(GwStr),
|
Gw1 = list_to_existing_atom(GwStr),
|
||||||
|
@ -447,31 +438,30 @@ fields(gateway_stats) ->
|
||||||
[{key, mk(binary(), #{})}].
|
[{key, mk(binary(), #{})}].
|
||||||
|
|
||||||
schema_load_or_update_gateways_conf() ->
|
schema_load_or_update_gateways_conf() ->
|
||||||
|
Names = emqx_gateway_schema:gateway_names(),
|
||||||
emqx_dashboard_swagger:schema_with_examples(
|
emqx_dashboard_swagger:schema_with_examples(
|
||||||
hoconsc:union([
|
hoconsc:union(
|
||||||
ref(?MODULE, stomp),
|
[
|
||||||
ref(?MODULE, mqttsn),
|
ref(?MODULE, Name)
|
||||||
ref(?MODULE, coap),
|
|| Name <-
|
||||||
ref(?MODULE, lwm2m),
|
Names ++
|
||||||
ref(?MODULE, exproto),
|
[
|
||||||
ref(?MODULE, update_stomp),
|
erlang:list_to_existing_atom("update_" ++ erlang:atom_to_list(Name))
|
||||||
ref(?MODULE, update_mqttsn),
|
|| Name <- Names
|
||||||
ref(?MODULE, update_coap),
|
]
|
||||||
ref(?MODULE, update_lwm2m),
|
]
|
||||||
ref(?MODULE, update_exproto)
|
),
|
||||||
]),
|
|
||||||
examples_update_gateway_confs()
|
examples_update_gateway_confs()
|
||||||
).
|
).
|
||||||
|
|
||||||
schema_gateways_conf() ->
|
schema_gateways_conf() ->
|
||||||
emqx_dashboard_swagger:schema_with_examples(
|
emqx_dashboard_swagger:schema_with_examples(
|
||||||
hoconsc:union([
|
hoconsc:union(
|
||||||
ref(?MODULE, stomp),
|
[
|
||||||
ref(?MODULE, mqttsn),
|
ref(?MODULE, Name)
|
||||||
ref(?MODULE, coap),
|
|| Name <- emqx_gateway_schema:gateway_names()
|
||||||
ref(?MODULE, lwm2m),
|
]
|
||||||
ref(?MODULE, exproto)
|
),
|
||||||
]),
|
|
||||||
examples_gateway_confs()
|
examples_gateway_confs()
|
||||||
).
|
).
|
||||||
|
|
||||||
|
@ -756,6 +746,30 @@ examples_gateway_confs() ->
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
gbt32960_gateway =>
|
||||||
|
#{
|
||||||
|
summary => <<"A simple GBT32960 gateway config">>,
|
||||||
|
value =>
|
||||||
|
#{
|
||||||
|
enable => true,
|
||||||
|
name => <<"gbt32960">>,
|
||||||
|
enable_stats => true,
|
||||||
|
mountpoint => <<"gbt32960/${clientid}">>,
|
||||||
|
retry_interval => <<"8s">>,
|
||||||
|
max_retry_times => 3,
|
||||||
|
message_queue_len => 10,
|
||||||
|
listeners =>
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
type => <<"tcp">>,
|
||||||
|
name => <<"default">>,
|
||||||
|
bind => <<"7325">>,
|
||||||
|
max_connections => 1024000,
|
||||||
|
max_conn_rate => 1000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
||||||
|
@ -854,5 +868,18 @@ examples_update_gateway_confs() ->
|
||||||
handler =>
|
handler =>
|
||||||
#{address => <<"http://127.0.0.1:9001">>}
|
#{address => <<"http://127.0.0.1:9001">>}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
gbt32960_gateway =>
|
||||||
|
#{
|
||||||
|
summary => <<"A simple GBT32960 gateway config">>,
|
||||||
|
value =>
|
||||||
|
#{
|
||||||
|
enable => true,
|
||||||
|
enable_stats => true,
|
||||||
|
mountpoint => <<"gbt32960/${clientid}">>,
|
||||||
|
retry_interval => <<"8s">>,
|
||||||
|
max_retry_times => 3,
|
||||||
|
message_queue_len => 10
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.
|
}.
|
||||||
|
|
|
@ -327,7 +327,7 @@ params_gateway_name_in_path() ->
|
||||||
[
|
[
|
||||||
{name,
|
{name,
|
||||||
mk(
|
mk(
|
||||||
binary(),
|
hoconsc:enum(emqx_gateway_schema:gateway_names()),
|
||||||
#{
|
#{
|
||||||
in => path,
|
in => path,
|
||||||
desc => ?DESC(emqx_gateway_api, gateway_name_in_qs),
|
desc => ?DESC(emqx_gateway_api, gateway_name_in_qs),
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
api_spec() ->
|
api_spec() ->
|
||||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}).
|
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||||
|
|
||||||
paths() ->
|
paths() ->
|
||||||
[
|
[
|
||||||
|
@ -157,7 +157,7 @@ params_gateway_name_in_path() ->
|
||||||
[
|
[
|
||||||
{name,
|
{name,
|
||||||
mk(
|
mk(
|
||||||
binary(),
|
hoconsc:enum(emqx_gateway_schema:gateway_names()),
|
||||||
#{
|
#{
|
||||||
in => path,
|
in => path,
|
||||||
desc => ?DESC(emqx_gateway_api, gateway_name_in_qs),
|
desc => ?DESC(emqx_gateway_api, gateway_name_in_qs),
|
||||||
|
|
|
@ -700,7 +700,7 @@ params_gateway_name_in_path() ->
|
||||||
[
|
[
|
||||||
{name,
|
{name,
|
||||||
mk(
|
mk(
|
||||||
binary(),
|
hoconsc:enum(emqx_gateway_schema:gateway_names()),
|
||||||
#{
|
#{
|
||||||
in => path,
|
in => path,
|
||||||
desc => ?DESC(emqx_gateway_api, gateway_name)
|
desc => ?DESC(emqx_gateway_api, gateway_name)
|
||||||
|
|
|
@ -609,7 +609,7 @@ params_gateway_name_in_path() ->
|
||||||
[
|
[
|
||||||
{name,
|
{name,
|
||||||
mk(
|
mk(
|
||||||
binary(),
|
hoconsc:enum(emqx_gateway_schema:gateway_names()),
|
||||||
#{
|
#{
|
||||||
in => path,
|
in => path,
|
||||||
desc => ?DESC(emqx_gateway_api, gateway_name_in_qs),
|
desc => ?DESC(emqx_gateway_api, gateway_name_in_qs),
|
||||||
|
|
|
@ -513,29 +513,23 @@ codestr(501) -> 'NOT_IMPLEMENTED'.
|
||||||
fmtstr(Fmt, Args) ->
|
fmtstr(Fmt, Args) ->
|
||||||
lists:flatten(io_lib:format(Fmt, Args)).
|
lists:flatten(io_lib:format(Fmt, Args)).
|
||||||
|
|
||||||
-spec with_authn(binary(), function()) -> any().
|
-spec with_authn(atom(), function()) -> any().
|
||||||
with_authn(GwName0, Fun) ->
|
with_authn(GwName0, Fun) ->
|
||||||
with_gateway(GwName0, fun(GwName, _GwConf) ->
|
with_gateway(GwName0, fun(GwName, _GwConf) ->
|
||||||
Authn = emqx_gateway_http:authn(GwName),
|
Authn = emqx_gateway_http:authn(GwName),
|
||||||
Fun(GwName, Authn)
|
Fun(GwName, Authn)
|
||||||
end).
|
end).
|
||||||
|
|
||||||
-spec with_listener_authn(binary(), binary(), function()) -> any().
|
-spec with_listener_authn(atom(), binary(), function()) -> any().
|
||||||
with_listener_authn(GwName0, Id, Fun) ->
|
with_listener_authn(GwName0, Id, Fun) ->
|
||||||
with_gateway(GwName0, fun(GwName, _GwConf) ->
|
with_gateway(GwName0, fun(GwName, _GwConf) ->
|
||||||
Authn = emqx_gateway_http:authn(GwName, Id),
|
Authn = emqx_gateway_http:authn(GwName, Id),
|
||||||
Fun(GwName, Authn)
|
Fun(GwName, Authn)
|
||||||
end).
|
end).
|
||||||
|
|
||||||
-spec with_gateway(binary(), function()) -> any().
|
-spec with_gateway(atom(), function()) -> any().
|
||||||
with_gateway(GwName0, Fun) ->
|
with_gateway(GwName, Fun) ->
|
||||||
try
|
try
|
||||||
GwName =
|
|
||||||
try
|
|
||||||
binary_to_existing_atom(GwName0)
|
|
||||||
catch
|
|
||||||
_:_ -> error(badname)
|
|
||||||
end,
|
|
||||||
case emqx_gateway:lookup(GwName) of
|
case emqx_gateway:lookup(GwName) of
|
||||||
undefined ->
|
undefined ->
|
||||||
return_http_error(404, "Gateway not loaded");
|
return_http_error(404, "Gateway not loaded");
|
||||||
|
|
|
@ -48,12 +48,13 @@
|
||||||
ip_port/0
|
ip_port/0
|
||||||
]).
|
]).
|
||||||
-elvis([{elvis_style, dont_repeat_yourself, disable}]).
|
-elvis([{elvis_style, dont_repeat_yourself, disable}]).
|
||||||
|
-elvis([{elvis_style, invalid_dynamic_call, disable}]).
|
||||||
|
|
||||||
-export([namespace/0, roots/0, fields/1, desc/1, tags/0]).
|
-export([namespace/0, roots/0, fields/1, desc/1, tags/0]).
|
||||||
|
|
||||||
-export([proxy_protocol_opts/0]).
|
-export([proxy_protocol_opts/0]).
|
||||||
|
|
||||||
-export([mountpoint/0, mountpoint/1, gateway_common_options/0, gateway_schema/1]).
|
-export([mountpoint/0, mountpoint/1, gateway_common_options/0, gateway_schema/1, gateway_names/0]).
|
||||||
|
|
||||||
namespace() -> gateway.
|
namespace() -> gateway.
|
||||||
|
|
||||||
|
@ -337,12 +338,21 @@ proxy_protocol_opts() ->
|
||||||
%% dynamic schemas
|
%% dynamic schemas
|
||||||
|
|
||||||
%% FIXME: don't hardcode the gateway names
|
%% FIXME: don't hardcode the gateway names
|
||||||
gateway_schema(stomp) -> emqx_stomp_schema:fields(stomp);
|
gateway_schema(Name) ->
|
||||||
gateway_schema(mqttsn) -> emqx_mqttsn_schema:fields(mqttsn);
|
case emqx_gateway_utils:find_gateway_definition(Name) of
|
||||||
gateway_schema(coap) -> emqx_coap_schema:fields(coap);
|
{ok, #{config_schema_module := SchemaMod}} ->
|
||||||
gateway_schema(lwm2m) -> emqx_lwm2m_schema:fields(lwm2m);
|
SchemaMod:fields(Name);
|
||||||
gateway_schema(exproto) -> emqx_exproto_schema:fields(exproto).
|
{error, _} = Error ->
|
||||||
|
throw(Error)
|
||||||
|
end.
|
||||||
|
|
||||||
|
gateway_names() ->
|
||||||
|
Definations = emqx_gateway_utils:find_gateway_definitions(),
|
||||||
|
[
|
||||||
|
Name
|
||||||
|
|| #{name := Name} = Defination <- Definations,
|
||||||
|
emqx_gateway_utils:check_gateway_edition(Defination)
|
||||||
|
].
|
||||||
%%--------------------------------------------------------------------
|
%%--------------------------------------------------------------------
|
||||||
%% helpers
|
%% helpers
|
||||||
|
|
||||||
|
|
|
@ -45,8 +45,10 @@
|
||||||
global_chain/1,
|
global_chain/1,
|
||||||
listener_chain/3,
|
listener_chain/3,
|
||||||
find_gateway_definitions/0,
|
find_gateway_definitions/0,
|
||||||
|
find_gateway_definition/1,
|
||||||
plus_max_connections/2,
|
plus_max_connections/2,
|
||||||
random_clientid/1
|
random_clientid/1,
|
||||||
|
check_gateway_edition/1
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-export([stringfy/1]).
|
-export([stringfy/1]).
|
||||||
|
@ -538,6 +540,32 @@ find_gateway_definitions() ->
|
||||||
)
|
)
|
||||||
).
|
).
|
||||||
|
|
||||||
|
-spec find_gateway_definition(atom()) -> {ok, map()} | {error, term()}.
|
||||||
|
find_gateway_definition(Name) ->
|
||||||
|
ensure_gateway_loaded(),
|
||||||
|
find_gateway_definition(Name, ignore_lib_apps(application:loaded_applications())).
|
||||||
|
|
||||||
|
-dialyzer({no_match, [find_gateway_definition/2]}).
|
||||||
|
find_gateway_definition(Name, [App | T]) ->
|
||||||
|
Attrs = find_attrs(App, gateway),
|
||||||
|
SearchFun = fun({_App, _Mod, #{name := GwName}}) ->
|
||||||
|
GwName =:= Name
|
||||||
|
end,
|
||||||
|
case lists:search(SearchFun, Attrs) of
|
||||||
|
{value, {_App, _Mod, Defination}} ->
|
||||||
|
case check_gateway_edition(Defination) of
|
||||||
|
true ->
|
||||||
|
{ok, Defination};
|
||||||
|
_ ->
|
||||||
|
{error, invalid_edition}
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
find_gateway_definition(Name, T)
|
||||||
|
end;
|
||||||
|
find_gateway_definition(_Name, []) ->
|
||||||
|
{error, not_found}.
|
||||||
|
|
||||||
|
-dialyzer({no_match, [gateways/1]}).
|
||||||
gateways([]) ->
|
gateways([]) ->
|
||||||
[];
|
[];
|
||||||
gateways([
|
gateways([
|
||||||
|
@ -550,7 +578,20 @@ gateways([
|
||||||
}}
|
}}
|
||||||
| More
|
| More
|
||||||
]) when is_atom(Name), is_atom(CbMod), is_atom(SchemaMod) ->
|
]) when is_atom(Name), is_atom(CbMod), is_atom(SchemaMod) ->
|
||||||
[Defination | gateways(More)].
|
case check_gateway_edition(Defination) of
|
||||||
|
true ->
|
||||||
|
[Defination | gateways(More)];
|
||||||
|
_ ->
|
||||||
|
gateways(More)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-if(?EMQX_RELEASE_EDITION == ee).
|
||||||
|
check_gateway_edition(_Defination) ->
|
||||||
|
true.
|
||||||
|
-else.
|
||||||
|
check_gateway_edition(Defination) ->
|
||||||
|
ce == maps:get(edition, Defination, ce).
|
||||||
|
-endif.
|
||||||
|
|
||||||
find_attrs(App, Def) ->
|
find_attrs(App, Def) ->
|
||||||
[
|
[
|
||||||
|
|
|
@ -96,10 +96,8 @@ t_gateways(_) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
t_gateway(_) ->
|
t_gateway(_) ->
|
||||||
{404, GwNotFoundReq1} = request(get, "/gateways/not_a_known_atom"),
|
?assertMatch({400, #{code := <<"BAD_REQUEST">>}}, request(get, "/gateways/not_a_known_atom")),
|
||||||
assert_not_found(GwNotFoundReq1),
|
?assertMatch({400, #{code := <<"BAD_REQUEST">>}}, request(get, "/gateways/undefined")),
|
||||||
{404, GwNotFoundReq2} = request(get, "/gateways/undefined"),
|
|
||||||
assert_not_found(GwNotFoundReq2),
|
|
||||||
{204, _} = request(put, "/gateways/stomp", #{}),
|
{204, _} = request(put, "/gateways/stomp", #{}),
|
||||||
{200, StompGw} = request(get, "/gateways/stomp"),
|
{200, StompGw} = request(get, "/gateways/stomp"),
|
||||||
assert_fields_exist(
|
assert_fields_exist(
|
||||||
|
@ -110,7 +108,7 @@ t_gateway(_) ->
|
||||||
{200, #{enable := true}} = request(get, "/gateways/stomp"),
|
{200, #{enable := true}} = request(get, "/gateways/stomp"),
|
||||||
{204, _} = request(put, "/gateways/stomp", #{enable => false}),
|
{204, _} = request(put, "/gateways/stomp", #{enable => false}),
|
||||||
{200, #{enable := false}} = request(get, "/gateways/stomp"),
|
{200, #{enable := false}} = request(get, "/gateways/stomp"),
|
||||||
{404, _} = request(put, "/gateways/undefined", #{}),
|
?assertMatch({400, #{code := <<"BAD_REQUEST">>}}, request(put, "/gateways/undefined", #{})),
|
||||||
{400, _} = request(put, "/gateways/stomp", #{bad_key => "foo"}),
|
{400, _} = request(put, "/gateways/stomp", #{bad_key => "foo"}),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
@ -129,8 +127,14 @@ t_gateway_enable(_) ->
|
||||||
{200, #{enable := NotEnable}} = request(get, "/gateways/stomp"),
|
{200, #{enable := NotEnable}} = request(get, "/gateways/stomp"),
|
||||||
{204, _} = request(put, "/gateways/stomp/enable/" ++ atom_to_list(Enable), undefined),
|
{204, _} = request(put, "/gateways/stomp/enable/" ++ atom_to_list(Enable), undefined),
|
||||||
{200, #{enable := Enable}} = request(get, "/gateways/stomp"),
|
{200, #{enable := Enable}} = request(get, "/gateways/stomp"),
|
||||||
{404, _} = request(put, "/gateways/undefined/enable/true", undefined),
|
?assertMatch(
|
||||||
{404, _} = request(put, "/gateways/not_a_known_atom/enable/true", undefined),
|
{400, #{code := <<"BAD_REQUEST">>}},
|
||||||
|
request(put, "/gateways/undefined/enable/true", undefined)
|
||||||
|
),
|
||||||
|
?assertMatch(
|
||||||
|
{400, #{code := <<"BAD_REQUEST">>}},
|
||||||
|
request(put, "/gateways/not_a_known_atom/enable/true", undefined)
|
||||||
|
),
|
||||||
{404, _} = request(put, "/gateways/coap/enable/true", undefined),
|
{404, _} = request(put, "/gateways/coap/enable/true", undefined),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
Business Source License 1.1
|
||||||
|
|
||||||
|
Licensor: Hangzhou EMQ Technologies Co., Ltd.
|
||||||
|
Licensed Work: EMQX Enterprise Edition
|
||||||
|
The Licensed Work is (c) 2023
|
||||||
|
Hangzhou EMQ Technologies Co., Ltd.
|
||||||
|
Additional Use Grant: Students and educators are granted right to copy,
|
||||||
|
modify, and create derivative work for research
|
||||||
|
or education.
|
||||||
|
Change Date: 2027-02-01
|
||||||
|
Change License: Apache License, Version 2.0
|
||||||
|
|
||||||
|
For information about alternative licensing arrangements for the Software,
|
||||||
|
please contact Licensor: https://www.emqx.com/en/contact
|
||||||
|
|
||||||
|
Notice
|
||||||
|
|
||||||
|
The Business Source License (this document, or the “License”) is not an Open
|
||||||
|
Source license. However, the Licensed Work will eventually be made available
|
||||||
|
under an Open Source License, as stated in this License.
|
||||||
|
|
||||||
|
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
|
||||||
|
“Business Source License” is a trademark of MariaDB Corporation Ab.
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Business Source License 1.1
|
||||||
|
|
||||||
|
Terms
|
||||||
|
|
||||||
|
The Licensor hereby grants you the right to copy, modify, create derivative
|
||||||
|
works, redistribute, and make non-production use of the Licensed Work. The
|
||||||
|
Licensor may make an Additional Use Grant, above, permitting limited
|
||||||
|
production use.
|
||||||
|
|
||||||
|
Effective on the Change Date, or the fourth anniversary of the first publicly
|
||||||
|
available distribution of a specific version of the Licensed Work under this
|
||||||
|
License, whichever comes first, the Licensor hereby grants you rights under
|
||||||
|
the terms of the Change License, and the rights granted in the paragraph
|
||||||
|
above terminate.
|
||||||
|
|
||||||
|
If your use of the Licensed Work does not comply with the requirements
|
||||||
|
currently in effect as described in this License, you must purchase a
|
||||||
|
commercial license from the Licensor, its affiliated entities, or authorized
|
||||||
|
resellers, or you must refrain from using the Licensed Work.
|
||||||
|
|
||||||
|
All copies of the original and modified Licensed Work, and derivative works
|
||||||
|
of the Licensed Work, are subject to this License. This License applies
|
||||||
|
separately for each version of the Licensed Work and the Change Date may vary
|
||||||
|
for each version of the Licensed Work released by Licensor.
|
||||||
|
|
||||||
|
You must conspicuously display this License on each original or modified copy
|
||||||
|
of the Licensed Work. If you receive the Licensed Work in original or
|
||||||
|
modified form from a third party, the terms and conditions set forth in this
|
||||||
|
License apply to your use of that work.
|
||||||
|
|
||||||
|
Any use of the Licensed Work in violation of this License will automatically
|
||||||
|
terminate your rights under this License for the current and all other
|
||||||
|
versions of the Licensed Work.
|
||||||
|
|
||||||
|
This License does not grant you any right in any trademark or logo of
|
||||||
|
Licensor or its affiliates (provided that you may use a trademark or logo of
|
||||||
|
Licensor as expressly required by this License).
|
||||||
|
|
||||||
|
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
||||||
|
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
||||||
|
TITLE.
|
||||||
|
|
||||||
|
MariaDB hereby grants you permission to use this License’s text to license
|
||||||
|
your works, and to refer to it using the trademark “Business Source License”,
|
||||||
|
as long as you comply with the Covenants of Licensor below.
|
||||||
|
|
||||||
|
Covenants of Licensor
|
||||||
|
|
||||||
|
In consideration of the right to use this License’s text and the “Business
|
||||||
|
Source License” name and trademark, Licensor covenants to MariaDB, and to all
|
||||||
|
other recipients of the licensed work to be provided by Licensor:
|
||||||
|
|
||||||
|
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
||||||
|
or a license that is compatible with GPL Version 2.0 or a later version,
|
||||||
|
where “compatible” means that software provided under the Change License can
|
||||||
|
be included in a program with software provided under GPL Version 2.0 or a
|
||||||
|
later version. Licensor may specify additional Change Licenses without
|
||||||
|
limitation.
|
||||||
|
|
||||||
|
2. To either: (a) specify an additional grant of rights to use that does not
|
||||||
|
impose any additional restriction on the right granted in this License, as
|
||||||
|
the Additional Use Grant; or (b) insert the text “None”.
|
||||||
|
|
||||||
|
3. To specify a Change Date.
|
||||||
|
|
||||||
|
4. Not to modify this License in any other way.
|
|
@ -0,0 +1,24 @@
|
||||||
|
# emqx_gbt32960
|
||||||
|
|
||||||
|
The GBT32960 Gateway is based on the GBT32960 specification.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
In EMQX 5.0, GBT32960 gateway can be configured and enabled through the Dashboard.
|
||||||
|
|
||||||
|
It can also be enabled via the HTTP API or emqx.conf, e.g. In emqx.conf:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
gateway.gbt32960 {
|
||||||
|
|
||||||
|
mountpoint = "gbt32960/${clientid}"
|
||||||
|
|
||||||
|
listeners.tcp.default {
|
||||||
|
bind = 7325
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note:
|
||||||
|
> Configuring the gateway via emqx.conf requires changes on a per-node basis,
|
||||||
|
> but configuring it via Dashboard or the HTTP API will take effect across the cluster.
|
|
@ -0,0 +1,741 @@
|
||||||
|
# emqx-gbt32960
|
||||||
|
|
||||||
|
该文档定义了 Plugins **emqx_gbt32960** 和 **EMQX** 之间数据交换的格式
|
||||||
|
|
||||||
|
约定:
|
||||||
|
- Payload 采用 Json 格式进行组装
|
||||||
|
- Json Key 采用大驼峰格式命名
|
||||||
|
|
||||||
|
# Upstream
|
||||||
|
数据流向: Terminal -> emqx_gbt32960 -> EMQX
|
||||||
|
|
||||||
|
## 车辆登入
|
||||||
|
Topic: gbt32960/${vin}/upstream/vlogin
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 1,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"ICCID": "12345678901234567890",
|
||||||
|
"Id": "C",
|
||||||
|
"Length": 1,
|
||||||
|
"Num": 1,
|
||||||
|
"Seq": 1,
|
||||||
|
"Time": {
|
||||||
|
"Day": 29,
|
||||||
|
"Hour": 12,
|
||||||
|
"Minute": 19,
|
||||||
|
"Month": 12,
|
||||||
|
"Second": 20,
|
||||||
|
"Year": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| --------- | ------- | ------------------------------------------------------------ |
|
||||||
|
| `Cmd` | Integer | 命令单元; `1` 表示车辆登入 |
|
||||||
|
| `Encrypt` | Integer | 数据单元加密方式,`1` 表示不加密,`2` 数据经过 RSA 加密,`3` 数据经过 ASE128 算法加密;`254` 表示异常;`255` 表示无效;其他预留 |
|
||||||
|
| `Vin` | String | 唯一识别码,即车辆 VIN 码 |
|
||||||
|
| `Data` | Object | 数据单元, JSON 对象格式。 |
|
||||||
|
|
||||||
|
车辆登入的数据单元格式为
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| -------- | ------- | ------------------------------------------------------------ |
|
||||||
|
| `Time` | Object | 数据采集时间,按年,月,日,时,分,秒,格式见示例。 |
|
||||||
|
| `Seq` | Integer | 登入流水号 |
|
||||||
|
| `ICCID` | String | 长度为20的字符串,SIM 卡的 ICCID 号 |
|
||||||
|
| `Num` | Integer | 可充电储能子系统数,有效值 0 ~ 250 |
|
||||||
|
| `Length` | Integer | 可充电储能系统编码长度,有效值 0 ~ 50 |
|
||||||
|
| `Id` | String | 可充电储能系统编码,长度为 "子系统数" 与 "编码长度" 值的乘积 |
|
||||||
|
|
||||||
|
## 车辆登出
|
||||||
|
|
||||||
|
Topic: gbt32960/${vin}/upstream/vlogout
|
||||||
|
|
||||||
|
车辆登出的 `Cmd` 值为 4,其余字段含义与登入相同:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 4,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Seq": 1,
|
||||||
|
"Time": {
|
||||||
|
"Day": 1,
|
||||||
|
"Hour": 2,
|
||||||
|
"Minute": 59,
|
||||||
|
"Month": 1,
|
||||||
|
"Second": 0,
|
||||||
|
"Year": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实时信息上报
|
||||||
|
|
||||||
|
Topic: gbt32960/${vin}/upstream/info
|
||||||
|
|
||||||
|
> 不同信息类型上报,格式上只有 Infos 里面的对象属性不同,通过 `Type` 进行区分
|
||||||
|
> Infos 为数组,代表车载终端每次报文可以上报多个信息
|
||||||
|
|
||||||
|
### 整车数据
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 2,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Infos": [
|
||||||
|
{
|
||||||
|
"AcceleratorPedal": 90,
|
||||||
|
"BrakePedal": 0,
|
||||||
|
"Charging": 1,
|
||||||
|
"Current": 15000,
|
||||||
|
"DC": 1,
|
||||||
|
"Gear": 5,
|
||||||
|
"Mileage": 999999,
|
||||||
|
"Mode": 1,
|
||||||
|
"Resistance": 6000,
|
||||||
|
"SOC": 50,
|
||||||
|
"Speed": 2000,
|
||||||
|
"Status": 1,
|
||||||
|
"Type": "Vehicle",
|
||||||
|
"Voltage": 5000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 1,
|
||||||
|
"Hour": 2,
|
||||||
|
"Minute": 59,
|
||||||
|
"Month": 1,
|
||||||
|
"Second": 0,
|
||||||
|
"Year": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
其中,整车信息字段含义如下:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| ------------ | ------- | ------------------------------------------------------------ |
|
||||||
|
| `Type` | String | 数据类型,`Vehicle` 表示该结构为整车信息 |
|
||||||
|
| `Status` | Integer | 车辆状态,`1` 表示启动状态;`2` 表示熄火;`3` 表示其状态;`254` 表示异常;`255` 表示无效 |
|
||||||
|
| `Charging` | Integer | 充电状态,`1` 表示停车充电;`2` 行驶充电;`3` 未充电状态;`4` 充电完成;`254` 表示异常;`255` 表示无效 |
|
||||||
|
| `Mode` | Integer | 运行模式,`1` 表示纯电;`2` 混动;`3` 燃油;`254` 表示异常;`255` 表示无效 |
|
||||||
|
| `Speed` | Integer | 车速,有效值 ( 0~ 2200,表示 0 km/h ~ 220 km/h),单位 0.1 km/h |
|
||||||
|
| `Mileage` | Integer | 累计里程,有效值 0 ~9,999,999(表示 0 km ~ 999,999.9 km),单位 0.1 km |
|
||||||
|
| `Voltage` | Integer | 总电压,有效值范围 0 ~10000(表示 0 V ~ 1000 V)单位 0.1 V |
|
||||||
|
| `Current` | Integer | 总电流,有效值 0 ~ 20000 (偏移量 1000,表示 -1000 A ~ +1000 A,单位 0.1 A |
|
||||||
|
| `SOC` | Integer | SOC,有效值 0 ~ 100(表示 0% ~ 100%) |
|
||||||
|
| `DC` | Integer | DC,`1` 工作;`2` 断开;`254` 表示异常;`255` 表示无效 |
|
||||||
|
| `Gear` | Integer | 档位,参考原协议的 表 A.1,此值为其转换为整数的值 |
|
||||||
|
| `Resistance` | Integer | 绝缘电阻,有效范围 0 ~ 60000(表示 0 k欧姆 ~ 60000 k欧姆) |
|
||||||
|
|
||||||
|
### 驱动电机数据
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 2,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Infos": [
|
||||||
|
{
|
||||||
|
"Motors": [
|
||||||
|
{
|
||||||
|
"CtrlTemp": 125,
|
||||||
|
"DCBusCurrent": 31203,
|
||||||
|
"InputVoltage": 30012,
|
||||||
|
"MotorTemp": 125,
|
||||||
|
"No": 1,
|
||||||
|
"Rotating": 30000,
|
||||||
|
"Status": 1,
|
||||||
|
"Torque": 25000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CtrlTemp": 125,
|
||||||
|
"DCBusCurrent": 30200,
|
||||||
|
"InputVoltage": 32000,
|
||||||
|
"MotorTemp": 145,
|
||||||
|
"No": 2,
|
||||||
|
"Rotating": 30200,
|
||||||
|
"Status": 1,
|
||||||
|
"Torque": 25300
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Number": 2,
|
||||||
|
"Type": "DriveMotor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 1,
|
||||||
|
"Hour": 2,
|
||||||
|
"Minute": 59,
|
||||||
|
"Month": 1,
|
||||||
|
"Second": 0,
|
||||||
|
"Year": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中,驱动电机数据各个字段的含义是
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| -------- | ------- | ------------------------------ |
|
||||||
|
| `Type` | String | 数据类型,此处为 `DriveMotor` |
|
||||||
|
| `Number` | Integer | 驱动电机个数,有效值 1~253 |
|
||||||
|
| `Motors` | Array | 驱动电机数据列表 |
|
||||||
|
|
||||||
|
驱动电机数据字段为:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| -------------- | -------- | ------------------------------------------------------------ |
|
||||||
|
| `No` | Integer | 驱动电机序号,有效值 1~253 |
|
||||||
|
| `Status` | Integer | 驱动电机状态,`1` 表示耗电;`2`发电;`3` 关闭状态;`4` 准备状态;`254` 表示异常;`255` 表示无效 |
|
||||||
|
| `CtrlTemp` | Integer | 驱动电机控制器温度,有效值 0~250(数值偏移 40°C,表示 -40°C ~ +210°C)单位 °C |
|
||||||
|
| `Rotating` | Interger | 驱动电机转速,有效值 0~65531(数值偏移 20000表示 -20000 r/min ~ 45531 r/min)单位 1 r/min |
|
||||||
|
| `Torque` | Integer | 驱动电机转矩,有效值 0~65531(数据偏移量 20000,表示 - 2000 N·m ~ 4553.1 N·m)单位 0.1 N·m |
|
||||||
|
| `MotorTemp` | Integer | 驱动电机温度,有效值 0~250(数据偏移量 40 °C,表示 -40°C ~ +210°C)单位 1°C |
|
||||||
|
| `InputVoltage` | Integer | 电机控制器输入电压,有效值 0~60000(表示 0V ~ 6000V)单位 0.1 V |
|
||||||
|
| `DCBusCurrent` | Interger | 电机控制器直流母线电流,有效值 0~20000(数值偏移 1000A,表示 -1000A ~ +1000 A)单位 0.1 A |
|
||||||
|
|
||||||
|
### 燃料电池数据
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 2,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Infos": [
|
||||||
|
{
|
||||||
|
"CellCurrent": 12000,
|
||||||
|
"CellVoltage": 10000,
|
||||||
|
"DCStatus": 1,
|
||||||
|
"FuelConsumption": 45000,
|
||||||
|
"H_ConcSensorCode": 11,
|
||||||
|
"H_MaxConc": 35000,
|
||||||
|
"H_MaxPress": 500,
|
||||||
|
"H_MaxTemp": 12500,
|
||||||
|
"H_PressSensorCode": 12,
|
||||||
|
"H_TempProbeCode": 10,
|
||||||
|
"ProbeNum": 2,
|
||||||
|
"ProbeTemps": [120, 121],
|
||||||
|
"Type": "FuelCell"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 1,
|
||||||
|
"Hour": 2,
|
||||||
|
"Minute": 59,
|
||||||
|
"Month": 1,
|
||||||
|
"Second": 0,
|
||||||
|
"Year": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中,燃料电池数据各个字段的含义是
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| ------------------- | ------- | ------------------------------------------------------------ |
|
||||||
|
| `Type` | String | 数据类型,此处为 `FuleCell` |
|
||||||
|
| `CellVoltage` | Integer | 燃料电池电压,有效值范围 0~20000(表示 0V ~ 2000V)单位 0.1 V |
|
||||||
|
| `CellCurrent` | Integer | 燃料电池电流,有效值范围 0~20000(表示 0A~ +2000A)单位 0.1 A |
|
||||||
|
| `FuelConsumption` | Integer | 燃料消耗率,有效值范围 0~60000(表示 0kg/100km ~ 600 kg/100km) 单位 0.01 kg/100km |
|
||||||
|
| `ProbeNum` | Integer | 燃料电池探针总数,有效值范围 0~65531 |
|
||||||
|
| `ProbeTemps` | Array | 燃料电池每探针温度值 |
|
||||||
|
| `H_MaxTemp` | Integer | 氢系统最高温度,有效值 0~2400(偏移量40°C,表示 -40°C~200°C)单位 0.1 °C |
|
||||||
|
| `H_TempProbeCode` | Integer | 氢系统最高温度探针代号,有效值 1~252 |
|
||||||
|
| `H_MaxConc` | Integer | 氢气最高浓度,有效值 0~60000(表示 0mg/kg ~ 50000 mg/kg)单位 1mg/kg |
|
||||||
|
| `H_ConcSensorCode` | Integer | 氢气最高浓度传感器代号,有效值 1~252 |
|
||||||
|
| `H_MaxPress` | Integer | 氢气最高压力,有效值 0~1000(表示 0 MPa ~ 100 MPa)最小单位 0.1 MPa |
|
||||||
|
| `H_PressSensorCode` | Integer | 氢气最高压力传感器代号,有效值 1~252 |
|
||||||
|
| `DCStatus` | Integer | 高压 DC/DC状态,`1` 表示工作;`2`断开 |
|
||||||
|
|
||||||
|
### 发动机数据
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 2,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Infos": [
|
||||||
|
{
|
||||||
|
"CrankshaftSpeed": 2000,
|
||||||
|
"FuelConsumption": 200,
|
||||||
|
"Status": 1,
|
||||||
|
"Type": "Engine"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 1,
|
||||||
|
"Hour": 22,
|
||||||
|
"Minute": 59,
|
||||||
|
"Month": 10,
|
||||||
|
"Second": 0,
|
||||||
|
"Year": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中,发动机数据各个字段的含义是
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| ----------------- | ------- | ------------------------------------------------------------ |
|
||||||
|
| `Type` | String | 数据类型,此处为 `Engine` |
|
||||||
|
| `Status` | Integer | 发动机状态,`1` 表示启动;`2` 关闭 |
|
||||||
|
| `CrankshaftSpeed` | Integer | 曲轴转速,有效值 0~60000(表示 0r/min~60000r/min)单位 1r/min |
|
||||||
|
| `FuelConsumption` | Integer | 燃料消耗率,有效范围 0~60000(表示 0L/100km~600L/100km)单位 0.01 L/100km |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 车辆位置数据
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 2,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Infos": [
|
||||||
|
{
|
||||||
|
"Latitude": 100,
|
||||||
|
"Longitude": 10,
|
||||||
|
"Status": 0,
|
||||||
|
"Type": "Location"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 1,
|
||||||
|
"Hour": 22,
|
||||||
|
"Minute": 59,
|
||||||
|
"Month": 10,
|
||||||
|
"Second": 0,
|
||||||
|
"Year": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中,车辆位置数据各个字段的含义是
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| ----------- | ------- | ----------------------------------------------------- |
|
||||||
|
| `Type` | String | 数据类型,此处为 `Location` |
|
||||||
|
| `Status` | Integer | 定位状态,见原协议表15,此处为所有比特位的整型值 |
|
||||||
|
| `Longitude` | Integer | 经度,以度为单位的纬度值乘以 10^6,精确到百万分之一度 |
|
||||||
|
| `Latitude` | Integer | 纬度,以度为单位的纬度值乘以 10^6,精确到百万分之一度 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 极值数据
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 2,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Infos": [
|
||||||
|
{
|
||||||
|
"MaxBatteryVoltage": 7500,
|
||||||
|
"MaxTemp": 120,
|
||||||
|
"MaxTempProbeNo": 12,
|
||||||
|
"MaxTempSubsysNo": 14,
|
||||||
|
"MaxVoltageBatteryCode": 10,
|
||||||
|
"MaxVoltageBatterySubsysNo": 12,
|
||||||
|
"MinBatteryVoltage": 2000,
|
||||||
|
"MinTemp": 40,
|
||||||
|
"MinTempProbeNo": 13,
|
||||||
|
"MinTempSubsysNo": 15,
|
||||||
|
"MinVoltageBatteryCode": 11,
|
||||||
|
"MinVoltageBatterySubsysNo": 13,
|
||||||
|
"Type": "Extreme"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 30,
|
||||||
|
"Hour": 12,
|
||||||
|
"Minute": 22,
|
||||||
|
"Month": 5,
|
||||||
|
"Second": 59,
|
||||||
|
"Year": 17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中,极值数据各个字段的含义是
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| --------------------------- | ------- | ------------------------------------------------------------ |
|
||||||
|
| `Type` | String | 数据类型,此处为 `Extreme` |
|
||||||
|
| `MaxVoltageBatterySubsysNo` | Integer | 最高电压电池子系统号,有效值 1~250 |
|
||||||
|
| `MaxVoltageBatteryCode` | Integer | 最高电压电池单体代号,有效值 1~250 |
|
||||||
|
| `MaxBatteryVoltage` | Integer | 电池单体电压最高值,有效值 0~15000(表示 0V~15V)单位 0.001V |
|
||||||
|
| `MinVoltageBatterySubsysNo` | Integer | 最低电压电池子系统号,有效值 1~250 |
|
||||||
|
| `MinVoltageBatteryCode` | Integer | 最低电压电池单体代号,有效值 1~250 |
|
||||||
|
| `MinBatteryVoltage` | Integer | 电池单体电压最低值,有效值 0~15000(表示 0V~15V)单位 0.001V |
|
||||||
|
| `MaxTempSubsysNo` | Integer | 最高温度子系统号,有效值 1~250 |
|
||||||
|
| `MaxTempProbeNo` | Integer | 最高温度探针序号,有效值 1~250 |
|
||||||
|
| `MaxTemp` | Integer | 最高温度值,有效值范围 0~250(偏移量40,表示 -40°C~+210°C) |
|
||||||
|
| `MinTempSubsysNo` | Integer | 最低温度子系统号,有效值 1~250 |
|
||||||
|
| `MinTempProbeNo` | Integer | 最低温度探针序号,有效值 1~250 |
|
||||||
|
| `MinTemp` | Integer | 最低温度值,有效值范围 0~250(偏移量40,表示 -40°C~+210°C) |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 报警数据
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 2,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Infos": [
|
||||||
|
{
|
||||||
|
"FaultChargeableDeviceNum": 1,
|
||||||
|
"FaultChargeableDeviceList": ["00C8"],
|
||||||
|
"FaultDriveMotorNum": 0,
|
||||||
|
"FaultDriveMotorList": [],
|
||||||
|
"FaultEngineNum": 1,
|
||||||
|
"FaultEngineList": ["006F"],
|
||||||
|
"FaultOthersNum": 0,
|
||||||
|
"FaultOthersList": [],
|
||||||
|
"GeneralAlarmFlag": 3,
|
||||||
|
"MaxAlarmLevel": 1,
|
||||||
|
"Type": "Alarm"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 20,
|
||||||
|
"Hour": 22,
|
||||||
|
"Minute": 23,
|
||||||
|
"Month": 12,
|
||||||
|
"Second": 59,
|
||||||
|
"Year": 17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中,报警数据各个字段的含义是
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| --------------------------- | ------- | ------------------------------------------------------------ |
|
||||||
|
| `Type` | String | 数据类型,此处为 `Alarm` |
|
||||||
|
| `MaxAlarmLevel` | Integer | 最高报警等级,有效值范围 0~3,`0` 表示无故障,`1` 表示 `1` 级故障 |
|
||||||
|
| `GeneralAlarmFlag` | Integer | 通用报警标志位,见原协议表 18 |
|
||||||
|
| `FaultChargeableDeviceNum` | Integer | 可充电储能装置故障总数,有效值 0~252 |
|
||||||
|
| `FaultChargeableDeviceList` | Array | 可充电储能装置故障代码列表 |
|
||||||
|
| `FaultDriveMotorNum` | Integer | 驱动电机故障总数,有效置范围 0 ~252 |
|
||||||
|
| `FaultDriveMotorList` | Array | 驱动电机故障代码列表 |
|
||||||
|
| `FaultEngineNum` | Integer | 发动机故障总数,有效值范围 0~252 |
|
||||||
|
| `FaultEngineList` | Array | 发动机故障代码列表 |
|
||||||
|
| `FaultOthersNum` | Integer | 其他故障总数 |
|
||||||
|
| `FaultOthersList` | Array | 其他故障代码列表 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 可充电储能装置电压数据
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 2,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Infos": [
|
||||||
|
{
|
||||||
|
"Number": 2,
|
||||||
|
"SubSystems": [
|
||||||
|
{
|
||||||
|
"CellsTotal": 2,
|
||||||
|
"CellsVoltage": [5000],
|
||||||
|
"ChargeableCurrent": 10000,
|
||||||
|
"ChargeableSubsysNo": 1,
|
||||||
|
"ChargeableVoltage": 5000,
|
||||||
|
"FrameCellsCount": 1,
|
||||||
|
"FrameCellsIndex": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"CellsTotal": 2,
|
||||||
|
"CellsVoltage": [5001],
|
||||||
|
"ChargeableCurrent": 10001,
|
||||||
|
"ChargeableSubsysNo": 2,
|
||||||
|
"ChargeableVoltage": 5001,
|
||||||
|
"FrameCellsCount": 1,
|
||||||
|
"FrameCellsIndex": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Type": "ChargeableVoltage"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 1,
|
||||||
|
"Hour": 22,
|
||||||
|
"Minute": 59,
|
||||||
|
"Month": 10,
|
||||||
|
"Second": 0,
|
||||||
|
"Year": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
其中,字段定义如下
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| ----------- | ------- | ------------------------------------ |
|
||||||
|
| `Type` | String | 数据类型,此处位 `ChargeableVoltage` |
|
||||||
|
| `Number` | Integer | 可充电储能子系统个数,有效范围 1~250 |
|
||||||
|
| `SubSystem` | Object | 可充电储能子系统电压信息列表 |
|
||||||
|
|
||||||
|
可充电储能子系统电压信息数据格式:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| -------------------- | ------- | ------------------------------------------------------------ |
|
||||||
|
| `ChargeableSubsysNo` | Integer | 可充电储能子系统号,有效值范围,1~250 |
|
||||||
|
| `ChargeableVoltage` | Integer | 可充电储能装置电压,有效值范围,0~10000(表示 0V~1000V)单位 0.1 V |
|
||||||
|
| `ChargeableCurrent` | Integer | 可充电储能装置电流,有效值范围,0~20000(数值偏移量 1000A,表示 -1000A~+1000A)单位 0.1 A |
|
||||||
|
| `CellsTotal` | Integer | 单体电池总数,有效值范围 1~65531 |
|
||||||
|
| `FrameCellsIndex` | Integer | 本帧起始电池序号,当本帧单体个数超过 200 时,应该拆分多个帧进行传输,有效值范围 1~65531 |
|
||||||
|
| `FrameCellsCount` | Integer | 本帧单体电池总数,有效值范围 1~200 |
|
||||||
|
| `CellsVoltage` | Array | 单体电池电压,有效值范围 0~60000(表示 0V~60.000V)单位 0.001V |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 可充电储能装置温度数据
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 2,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Infos": [
|
||||||
|
{
|
||||||
|
"Number": 2,
|
||||||
|
"SubSystems": [
|
||||||
|
{
|
||||||
|
"ChargeableSubsysNo": 1,
|
||||||
|
"ProbeNum": 10,
|
||||||
|
"ProbesTemp": [0, 0, 0, 0, 0, 0, 0, 0, 19, 136]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ChargeableSubsysNo": 2,
|
||||||
|
"ProbeNum": 1,
|
||||||
|
"ProbesTemp": [100]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Type": "ChargeableTemp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 1,
|
||||||
|
"Hour": 22,
|
||||||
|
"Minute": 59,
|
||||||
|
"Month": 10,
|
||||||
|
"Second": 0,
|
||||||
|
"Year": 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
其中,数据格式为:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| ------------ | ------- | --------------------------------- |
|
||||||
|
| `Type` | String | 数据类型,此处为 `ChargeableTemp` |
|
||||||
|
| `Number` | Integer | 可充电储能子系统温度信息列表长度 |
|
||||||
|
| `SubSystems` | Object | 可充电储能子系统温度信息列表 |
|
||||||
|
|
||||||
|
可充电储能子系统温度信息格式为
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| -------------------- | -------- | ------------------------------------ |
|
||||||
|
| `ChargeableSubsysNo` | Ineteger | 可充电储能子系统号,有效值 1~250 |
|
||||||
|
| `ProbeNum` | Integer | 可充电储能温度探针个数 |
|
||||||
|
| `ProbesTemp` | Array | 可充电储能子系统各温度探针温度值列表 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 数据补发
|
||||||
|
|
||||||
|
Topic: gbt32960/${vin}/upstream/reinfo
|
||||||
|
|
||||||
|
**数据格式: 略** (与实时数据上报相同)
|
||||||
|
|
||||||
|
# Downstream
|
||||||
|
|
||||||
|
> 请求数据流向: EMQX -> emqx_gbt32960 -> Terminal
|
||||||
|
|
||||||
|
> 应答数据流向: Terminal -> emqx_gbt32960 -> EMQX
|
||||||
|
|
||||||
|
下行主题: gbt32960/${vin}/dnstream
|
||||||
|
上行应答主题: gbt32960/${vin}/upstream/response
|
||||||
|
|
||||||
|
## 参数查询
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Req:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Action": "Query",
|
||||||
|
"Total": 2,
|
||||||
|
"Ids": ["0x01", "0x02"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| -------- | ------- | -------------------------------------------------- |
|
||||||
|
| `Action` | String | 下发命令类型,此处为 `Query` |
|
||||||
|
| `Total` | Integer | 查询参数总数 |
|
||||||
|
| `Ids` | Array | 需查询参数的 ID 列表,具体 ID 含义见原协议 表 B.10 |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cmd": 128,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Total": 2,
|
||||||
|
"Params": [
|
||||||
|
{"0x01": 6000},
|
||||||
|
{"0x02": 10}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 2,
|
||||||
|
"Hour": 11,
|
||||||
|
"Minute": 12,
|
||||||
|
"Month": 2,
|
||||||
|
"Second": 12,
|
||||||
|
"Year": 17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 参数设置
|
||||||
|
|
||||||
|
**Req:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Action": "Setting",
|
||||||
|
"Total": 2,
|
||||||
|
"Params": [{"0x01": 5000},
|
||||||
|
{"0x02": 200}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| -------- | ------- | ------------------------------ |
|
||||||
|
| `Action` | String | 下发命令类型,此处为 `Setting` |
|
||||||
|
| `Total` | Integer | 设置参数总数 |
|
||||||
|
| `Params` | Array | 需设置参数的 ID 和 值 |
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
// fixme? 终端是按照这种方式返回?
|
||||||
|
{
|
||||||
|
"Cmd": 129,
|
||||||
|
"Encrypt": 1,
|
||||||
|
"Vin": "1G1BL52P7TR115520",
|
||||||
|
"Data": {
|
||||||
|
"Total": 2,
|
||||||
|
"Params": [
|
||||||
|
{"0x01": 5000},
|
||||||
|
{"0x02": 200}
|
||||||
|
],
|
||||||
|
"Time": {
|
||||||
|
"Day": 2,
|
||||||
|
"Hour": 11,
|
||||||
|
"Minute": 12,
|
||||||
|
"Month": 2,
|
||||||
|
"Second": 12,
|
||||||
|
"Year": 17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 终端控制
|
||||||
|
**命令的不同, 参数不同; 无参数时为空**
|
||||||
|
|
||||||
|
远程升级:
|
||||||
|
**Req:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Action": "Control",
|
||||||
|
"Command": "0x01",
|
||||||
|
"Param": {
|
||||||
|
"DialingName": "hz203",
|
||||||
|
"Username": "user001",
|
||||||
|
"Password": "password01",
|
||||||
|
"Ip": "192.168.199.1",
|
||||||
|
"Port": 8080,
|
||||||
|
"ManufacturerId": "BMWA",
|
||||||
|
"HardwareVer": "1.0.0",
|
||||||
|
"SoftwareVer": "1.0.0",
|
||||||
|
"UpgradeUrl": "ftp://emqtt.io/ftp/server",
|
||||||
|
"Timeout": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 描述 |
|
||||||
|
| --------- | ------- | ------------------------------ |
|
||||||
|
| `Action` | String | 下发命令类型,此处为 `Control` |
|
||||||
|
| `Command` | Integer | 下发指令 ID,见原协议表 B.15 |
|
||||||
|
| `Param` | Object | 命令参数 |
|
||||||
|
|
||||||
|
列表
|
||||||
|
|
||||||
|
车载终端关机:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Action": "Control",
|
||||||
|
"Command": "0x02"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
车载终端报警:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Action": "Control",
|
||||||
|
"Command": "0x06",
|
||||||
|
"Param": {"Level": 0, "Message": "alarm message"}
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,75 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-record(frame, {cmd, ack, vin, encrypt, length, data, check, rawdata}).
|
||||||
|
|
||||||
|
-define(CMD(CmdType), #frame{
|
||||||
|
cmd = CmdType,
|
||||||
|
ack = ?ACK_IS_CMD
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(CMD(CmdType, Data), #frame{
|
||||||
|
cmd = CmdType,
|
||||||
|
data = Data,
|
||||||
|
ack = ?ACK_IS_CMD
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(IS_ACK_CODE(C),
|
||||||
|
(C == ?ACK_SUCCESS orelse
|
||||||
|
C == ?ACK_ERROR orelse
|
||||||
|
C == ?ACK_VIN_REPEAT)
|
||||||
|
).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% CMD Feilds
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-define(CMD_VIHECLE_LOGIN, 16#01).
|
||||||
|
-define(CMD_INFO_REPORT, 16#02).
|
||||||
|
-define(CMD_INFO_RE_REPORT, 16#03).
|
||||||
|
-define(CMD_VIHECLE_LOGOUT, 16#04).
|
||||||
|
-define(CMD_PLATFORM_LOGIN, 16#05).
|
||||||
|
-define(CMD_PLATFORM_LOGOUT, 16#06).
|
||||||
|
-define(CMD_HEARTBEAT, 16#07).
|
||||||
|
-define(CMD_SCHOOL_TIME, 16#08).
|
||||||
|
% 0x09~0x7F: Reserved by upstream system
|
||||||
|
% 0x80~0x82: Reserved by terminal data
|
||||||
|
-define(CMD_PARAM_QUERY, 16#80).
|
||||||
|
-define(CMD_PARAM_SETTING, 16#81).
|
||||||
|
-define(CMD_TERMINAL_CTRL, 16#82).
|
||||||
|
|
||||||
|
% 0x83~0xBF: Reserved by downstream system
|
||||||
|
% 0xC0~0xFE: Customized data for Platform Exchange Protocol
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% ACK Feilds
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-define(ACK_SUCCESS, 16#01).
|
||||||
|
-define(ACK_ERROR, 16#02).
|
||||||
|
-define(ACK_VIN_REPEAT, 16#03).
|
||||||
|
-define(ACK_IS_CMD, 16#FE).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Encrypt Feilds
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-define(ENCRYPT_NONE, 16#01).
|
||||||
|
-define(ENCRYPT_RSA, 16#02).
|
||||||
|
-define(ENCRYPT_AES128, 16#03).
|
||||||
|
-define(ENCRYPT_ABNORMAL, 16#FE).
|
||||||
|
-define(ENCRYPT_INVAILD, 16#FF).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Info Type Flags
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-define(INFO_TYPE_VEHICLE, 16#01).
|
||||||
|
-define(INFO_TYPE_DRIVE_MOTOR, 16#02).
|
||||||
|
-define(INFO_TYPE_FUEL_CELL, 16#03).
|
||||||
|
-define(INFO_TYPE_ENGINE, 16#04).
|
||||||
|
-define(INFO_TYPE_LOCATION, 16#05).
|
||||||
|
-define(INFO_TYPE_EXTREME, 16#06).
|
||||||
|
-define(INFO_TYPE_ALARM, 16#07).
|
||||||
|
-define(INFO_TYPE_CHARGEABLE_VOLTAGE, 16#08).
|
||||||
|
-define(INFO_TYPE_CHARGEABLE_TEMP, 16#09).
|
||||||
|
% 0x0A~0x2F: Customized data for Platform Exchange Protocol
|
||||||
|
% 0x30~0x7F: Reserved
|
||||||
|
% 0x80~0xFE: Customized by user
|
|
@ -0,0 +1,6 @@
|
||||||
|
{erl_opts, [debug_info]}.
|
||||||
|
{deps, [
|
||||||
|
{emqx, {path, "../../apps/emqx"}},
|
||||||
|
{emqx_utils, {path, "../emqx_utils"}},
|
||||||
|
{emqx_gateway, {path, "../../apps/emqx_gateway"}}
|
||||||
|
]}.
|
|
@ -0,0 +1,10 @@
|
||||||
|
{application, emqx_gateway_gbt32960, [
|
||||||
|
{description, "GBT32960 Gateway"},
|
||||||
|
{vsn, "0.1.0"},
|
||||||
|
{registered, []},
|
||||||
|
{applications, [kernel, stdlib, emqx, emqx_gateway]},
|
||||||
|
{env, []},
|
||||||
|
{modules, []},
|
||||||
|
{licenses, ["BSL"]},
|
||||||
|
{links, []}
|
||||||
|
]}.
|
|
@ -0,0 +1,98 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc The GBT32960 Gateway implement
|
||||||
|
-module(emqx_gateway_gbt32960).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("emqx_gateway/include/emqx_gateway.hrl").
|
||||||
|
|
||||||
|
%% define a gateway named gbt32960
|
||||||
|
-gateway(#{
|
||||||
|
name => gbt32960,
|
||||||
|
callback_module => ?MODULE,
|
||||||
|
config_schema_module => emqx_gbt32960_schema,
|
||||||
|
edition => ee
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% callback_module must implement the emqx_gateway_impl behaviour
|
||||||
|
-behaviour(emqx_gateway_impl).
|
||||||
|
|
||||||
|
%% callback for emqx_gateway_impl
|
||||||
|
-export([
|
||||||
|
on_gateway_load/2,
|
||||||
|
on_gateway_update/3,
|
||||||
|
on_gateway_unload/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-import(
|
||||||
|
emqx_gateway_utils,
|
||||||
|
[
|
||||||
|
normalize_config/1,
|
||||||
|
start_listeners/4,
|
||||||
|
stop_listeners/2
|
||||||
|
]
|
||||||
|
).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% emqx_gateway_impl callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
on_gateway_load(
|
||||||
|
_Gateway = #{
|
||||||
|
name := GwName,
|
||||||
|
config := Config
|
||||||
|
},
|
||||||
|
Ctx
|
||||||
|
) ->
|
||||||
|
Listeners = normalize_config(Config),
|
||||||
|
ModCfg = #{
|
||||||
|
frame_mod => emqx_gbt32960_frame,
|
||||||
|
chann_mod => emqx_gbt32960_channel
|
||||||
|
},
|
||||||
|
case
|
||||||
|
start_listeners(
|
||||||
|
Listeners, GwName, Ctx, ModCfg
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, ListenerPids} ->
|
||||||
|
%% FIXME: How to throw an exception to interrupt the restart logic ?
|
||||||
|
%% FIXME: Assign ctx to GwState
|
||||||
|
{ok, ListenerPids, _GwState = #{ctx => Ctx}};
|
||||||
|
{error, {Reason, Listener}} ->
|
||||||
|
throw(
|
||||||
|
{badconf, #{
|
||||||
|
key => listeners,
|
||||||
|
value => Listener,
|
||||||
|
reason => Reason
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
end.
|
||||||
|
|
||||||
|
on_gateway_update(Config, Gateway, GwState = #{ctx := Ctx}) ->
|
||||||
|
GwName = maps:get(name, Gateway),
|
||||||
|
try
|
||||||
|
%% XXX: 1. How hot-upgrade the changes ???
|
||||||
|
%% XXX: 2. Check the New confs first before destroy old state???
|
||||||
|
on_gateway_unload(Gateway, GwState),
|
||||||
|
on_gateway_load(Gateway#{config => Config}, Ctx)
|
||||||
|
catch
|
||||||
|
Class:Reason:Stk ->
|
||||||
|
logger:error(
|
||||||
|
"Failed to update ~ts; "
|
||||||
|
"reason: {~0p, ~0p} stacktrace: ~0p",
|
||||||
|
[GwName, Class, Reason, Stk]
|
||||||
|
),
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
on_gateway_unload(
|
||||||
|
_Gateway = #{
|
||||||
|
name := GwName,
|
||||||
|
config := Config
|
||||||
|
},
|
||||||
|
_GwState
|
||||||
|
) ->
|
||||||
|
Listeners = normalize_config(Config),
|
||||||
|
stop_listeners(GwName, Listeners).
|
|
@ -0,0 +1,867 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_gbt32960_channel).
|
||||||
|
-behaviour(emqx_gateway_channel).
|
||||||
|
|
||||||
|
-include("emqx_gbt32960.hrl").
|
||||||
|
-include_lib("emqx/include/types.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
-include_lib("emqx/include/emqx.hrl").
|
||||||
|
-include_lib("emqx/include/emqx_mqtt.hrl").
|
||||||
|
|
||||||
|
-export([
|
||||||
|
info/1,
|
||||||
|
info/2,
|
||||||
|
stats/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
init/2,
|
||||||
|
handle_in/2,
|
||||||
|
handle_deliver/2,
|
||||||
|
handle_timeout/3,
|
||||||
|
terminate/2,
|
||||||
|
set_conn_state/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
handle_call/3,
|
||||||
|
handle_cast/2,
|
||||||
|
handle_info/2
|
||||||
|
]).
|
||||||
|
|
||||||
|
-record(channel, {
|
||||||
|
%% Context
|
||||||
|
ctx :: emqx_gateway_ctx:context(),
|
||||||
|
%% ConnInfo
|
||||||
|
conninfo :: emqx_types:conninfo(),
|
||||||
|
%% ClientInfo
|
||||||
|
clientinfo :: emqx_types:clientinfo(),
|
||||||
|
%% Session
|
||||||
|
session :: undefined | map(),
|
||||||
|
%% Keepalive
|
||||||
|
keepalive :: maybe(emqx_keepalive:keepalive()),
|
||||||
|
%% Conn State
|
||||||
|
conn_state :: conn_state(),
|
||||||
|
%% Timers
|
||||||
|
timers :: #{atom() => undefined | disabled | reference()},
|
||||||
|
%% Inflight
|
||||||
|
inflight :: emqx_inflight:inflight(),
|
||||||
|
%% Message Queue
|
||||||
|
mqueue :: queue:queue(),
|
||||||
|
retx_interval,
|
||||||
|
retx_max_times,
|
||||||
|
max_mqueue_len
|
||||||
|
}).
|
||||||
|
|
||||||
|
-type conn_state() :: idle | connecting | connected | disconnected.
|
||||||
|
|
||||||
|
-type channel() :: #channel{}.
|
||||||
|
|
||||||
|
-type reply() ::
|
||||||
|
{outgoing, emqx_types:packet()}
|
||||||
|
| {outgoing, [emqx_types:packet()]}
|
||||||
|
| {event, conn_state() | updated}
|
||||||
|
| {close, Reason :: atom()}.
|
||||||
|
|
||||||
|
-type replies() :: reply() | [reply()].
|
||||||
|
-type frame() :: emqx_gbt32960_frame:frame().
|
||||||
|
|
||||||
|
-define(TIMER_TABLE, #{
|
||||||
|
alive_timer => keepalive,
|
||||||
|
retry_timer => retry_delivery
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(INFO_KEYS, [conninfo, conn_state, clientinfo, session, will_msg]).
|
||||||
|
-define(DEFAULT_MOUNTPOINT, <<"gbt32960/${clientid}">>).
|
||||||
|
-define(DEFAULT_DOWNLINK_TOPIC, <<"/dnstream">>).
|
||||||
|
|
||||||
|
-dialyzer({nowarn_function, init/2}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Info, Attrs and Caps
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Get infos of the channel.
|
||||||
|
-spec info(channel()) -> emqx_types:infos().
|
||||||
|
info(Channel) ->
|
||||||
|
maps:from_list(info(?INFO_KEYS, Channel)).
|
||||||
|
|
||||||
|
-spec info(list(atom()) | atom(), channel()) -> term().
|
||||||
|
info(Keys, Channel) when is_list(Keys) ->
|
||||||
|
[{Key, info(Key, Channel)} || Key <- Keys];
|
||||||
|
info(ctx, #channel{ctx = Ctx}) ->
|
||||||
|
Ctx;
|
||||||
|
info(conninfo, #channel{conninfo = ConnInfo}) ->
|
||||||
|
ConnInfo;
|
||||||
|
info(zone, #channel{clientinfo = #{zone := Zone}}) ->
|
||||||
|
Zone;
|
||||||
|
info(clientid, #channel{clientinfo = #{clientid := ClientId}}) ->
|
||||||
|
ClientId;
|
||||||
|
info(clientinfo, #channel{clientinfo = ClientInfo}) ->
|
||||||
|
ClientInfo;
|
||||||
|
info(session, _) ->
|
||||||
|
#{};
|
||||||
|
info(conn_state, #channel{conn_state = ConnState}) ->
|
||||||
|
ConnState;
|
||||||
|
info(keepalive, #channel{keepalive = undefined}) ->
|
||||||
|
undefined;
|
||||||
|
info(keepalive, #channel{keepalive = Keepalive}) ->
|
||||||
|
emqx_keepalive:info(Keepalive);
|
||||||
|
info(will_msg, _) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
-spec stats(channel()) -> emqx_types:stats().
|
||||||
|
stats(#channel{inflight = Inflight, mqueue = Queue}) ->
|
||||||
|
%% XXX: A fake stats for managed by emqx_management
|
||||||
|
[
|
||||||
|
{subscriptions_cnt, 1},
|
||||||
|
{subscriptions_max, 0},
|
||||||
|
{inflight_cnt, emqx_inflight:size(Inflight)},
|
||||||
|
{inflight_max, emqx_inflight:max_size(Inflight)},
|
||||||
|
{mqueue_len, queue:len(Queue)},
|
||||||
|
{mqueue_max, 0},
|
||||||
|
{mqueue_dropped, 0},
|
||||||
|
{next_pkt_id, 0},
|
||||||
|
{awaiting_rel_cnt, 0},
|
||||||
|
{awaiting_rel_max, 0}
|
||||||
|
].
|
||||||
|
|
||||||
|
set_conn_state(ConnState, Channel) ->
|
||||||
|
Channel#channel{conn_state = ConnState}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Init the Channel
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init(
|
||||||
|
ConnInfo = #{
|
||||||
|
peername := {PeerHost, _Port},
|
||||||
|
sockname := {_Host, SockPort}
|
||||||
|
},
|
||||||
|
Options
|
||||||
|
) ->
|
||||||
|
% TODO: init rsa_key from user input
|
||||||
|
Peercert = maps:get(peercert, ConnInfo, undefined),
|
||||||
|
Mountpoint = maps:get(mountpoint, Options, ?DEFAULT_MOUNTPOINT),
|
||||||
|
ListenerId =
|
||||||
|
case maps:get(listener, Options, undefined) of
|
||||||
|
undefined -> undefined;
|
||||||
|
{GwName, Type, LisName} -> emqx_gateway_utils:listener_id(GwName, Type, LisName)
|
||||||
|
end,
|
||||||
|
EnableAuthn = maps:get(enable_authn, Options, true),
|
||||||
|
|
||||||
|
ClientInfo = setting_peercert_infos(
|
||||||
|
Peercert,
|
||||||
|
#{
|
||||||
|
zone => default,
|
||||||
|
listener => ListenerId,
|
||||||
|
protocol => gbt32960,
|
||||||
|
peerhost => PeerHost,
|
||||||
|
sockport => SockPort,
|
||||||
|
clientid => undefined,
|
||||||
|
username => undefined,
|
||||||
|
is_bridge => false,
|
||||||
|
is_superuser => false,
|
||||||
|
enable_authn => EnableAuthn,
|
||||||
|
mountpoint => Mountpoint
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
Ctx = maps:get(ctx, Options),
|
||||||
|
|
||||||
|
#{
|
||||||
|
retry_interval := RetxInterv,
|
||||||
|
max_retry_times := RetxMaxTime,
|
||||||
|
message_queue_len := MessageQueueLen
|
||||||
|
} = Options,
|
||||||
|
|
||||||
|
#channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
conninfo = ConnInfo,
|
||||||
|
clientinfo = ClientInfo,
|
||||||
|
inflight = emqx_inflight:new(1),
|
||||||
|
mqueue = queue:new(),
|
||||||
|
timers = #{},
|
||||||
|
conn_state = idle,
|
||||||
|
retx_interval = RetxInterv,
|
||||||
|
retx_max_times = RetxMaxTime,
|
||||||
|
max_mqueue_len = MessageQueueLen
|
||||||
|
}.
|
||||||
|
|
||||||
|
setting_peercert_infos(NoSSL, ClientInfo) when
|
||||||
|
NoSSL =:= nossl;
|
||||||
|
NoSSL =:= undefined
|
||||||
|
->
|
||||||
|
ClientInfo;
|
||||||
|
setting_peercert_infos(Peercert, ClientInfo) ->
|
||||||
|
{DN, CN} = {esockd_peercert:subject(Peercert), esockd_peercert:common_name(Peercert)},
|
||||||
|
ClientInfo#{dn => DN, cn => CN}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle incoming packet
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-spec handle_in(emqx_gbt32960_frame:frame() | {frame_error, any()}, channel()) ->
|
||||||
|
{ok, channel()}
|
||||||
|
| {ok, replies(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), replies(), channel()}.
|
||||||
|
|
||||||
|
handle_in(
|
||||||
|
Frame = ?CMD(?CMD_VIHECLE_LOGIN),
|
||||||
|
Channel
|
||||||
|
) ->
|
||||||
|
case
|
||||||
|
emqx_utils:pipeline(
|
||||||
|
[
|
||||||
|
fun enrich_clientinfo/2,
|
||||||
|
fun enrich_conninfo/2,
|
||||||
|
fun set_log_meta/2,
|
||||||
|
%% TODO: How to implement the banned in the gateway instance?
|
||||||
|
%, fun check_banned/2
|
||||||
|
fun auth_connect/2
|
||||||
|
],
|
||||||
|
Frame,
|
||||||
|
Channel#channel{conn_state = connecting}
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, _NPacket, NChannel} ->
|
||||||
|
process_connect(Frame, ensure_connected(NChannel));
|
||||||
|
{error, ReasonCode, NChannel} ->
|
||||||
|
log(warning, #{msg => "login_failed", reason => ReasonCode}, NChannel),
|
||||||
|
shutdown(ReasonCode, NChannel)
|
||||||
|
end;
|
||||||
|
handle_in(_Frame, Channel = #channel{conn_state = ConnState}) when
|
||||||
|
ConnState =/= connected
|
||||||
|
->
|
||||||
|
shutdown(protocol_error, Channel);
|
||||||
|
handle_in(Frame = ?CMD(?CMD_INFO_REPORT), Channel) ->
|
||||||
|
_ = upstreaming(Frame, Channel),
|
||||||
|
{ok, Channel};
|
||||||
|
handle_in(Frame = ?CMD(?CMD_INFO_RE_REPORT), Channel) ->
|
||||||
|
_ = upstreaming(Frame, Channel),
|
||||||
|
{ok, Channel};
|
||||||
|
handle_in(Frame = ?CMD(?CMD_VIHECLE_LOGOUT), Channel) ->
|
||||||
|
%% XXX: unsubscribe gbt32960/dnstream/${vin}?
|
||||||
|
_ = upstreaming(Frame, Channel),
|
||||||
|
{ok, Channel};
|
||||||
|
handle_in(Frame = ?CMD(?CMD_PLATFORM_LOGIN), Channel) ->
|
||||||
|
#{
|
||||||
|
<<"Username">> := _Username,
|
||||||
|
<<"Password">> := _Password
|
||||||
|
} = Frame#frame.data,
|
||||||
|
%% TODO:
|
||||||
|
_ = upstreaming(Frame, Channel),
|
||||||
|
{ok, Channel};
|
||||||
|
handle_in(Frame = ?CMD(?CMD_PLATFORM_LOGOUT), Channel) ->
|
||||||
|
%% TODO:
|
||||||
|
_ = upstreaming(Frame, Channel),
|
||||||
|
{ok, Channel};
|
||||||
|
handle_in(Frame = ?CMD(?CMD_HEARTBEAT), Channel) ->
|
||||||
|
handle_out({?ACK_SUCCESS, Frame}, Channel);
|
||||||
|
handle_in(Frame = ?CMD(?CMD_SCHOOL_TIME), Channel) ->
|
||||||
|
%% TODO: How verify this request
|
||||||
|
handle_out({?ACK_SUCCESS, Frame}, Channel);
|
||||||
|
handle_in(Frame = #frame{cmd = Cmd}, Channel = #channel{inflight = Inflight}) ->
|
||||||
|
{Outgoings, NChannel} = dispatch_frame(Channel#channel{inflight = ack_frame(Cmd, Inflight)}),
|
||||||
|
_ = upstreaming(Frame, NChannel),
|
||||||
|
{ok, [{outgoing, Outgoings}], NChannel};
|
||||||
|
handle_in(Frame, Channel) ->
|
||||||
|
log(warning, #{msg => "unexcepted_frame", frame => Frame}, Channel),
|
||||||
|
{ok, Channel}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle out
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
handle_out({AckCode, Frame}, Channel) when
|
||||||
|
?IS_ACK_CODE(AckCode)
|
||||||
|
->
|
||||||
|
{ok, [{outgoing, ack(AckCode, Frame)}], Channel}.
|
||||||
|
|
||||||
|
handle_out({AckCode, Frame}, Outgoings, Channel) when ?IS_ACK_CODE(AckCode) ->
|
||||||
|
{ok, [{outgoing, ack(AckCode, Frame)} | Outgoings], Channel}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle Delivers from broker to client
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
-spec handle_deliver(list(emqx_types:deliver()), channel()) ->
|
||||||
|
{ok, channel()}
|
||||||
|
| {ok, replies(), channel()}.
|
||||||
|
|
||||||
|
handle_deliver(
|
||||||
|
Messages0,
|
||||||
|
Channel = #channel{
|
||||||
|
clientinfo = #{clientid := ClientId, mountpoint := Mountpoint},
|
||||||
|
mqueue = Queue,
|
||||||
|
max_mqueue_len = MaxQueueLen
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
Messages = lists:map(
|
||||||
|
fun({deliver, _, M}) ->
|
||||||
|
emqx_mountpoint:unmount(Mountpoint, M)
|
||||||
|
end,
|
||||||
|
Messages0
|
||||||
|
),
|
||||||
|
case MaxQueueLen - queue:len(Queue) of
|
||||||
|
N when N =< 0 ->
|
||||||
|
discard_downlink_messages(Messages, Channel),
|
||||||
|
{ok, Channel};
|
||||||
|
N ->
|
||||||
|
{NMessages, Dropped} = split_by_pos(Messages, N),
|
||||||
|
log(debug, #{msg => "enqueue_messages", messages => NMessages}, Channel),
|
||||||
|
metrics_inc('messages.delivered', Channel, erlang:length(NMessages)),
|
||||||
|
discard_downlink_messages(Dropped, Channel),
|
||||||
|
Frames = msgs2frame(NMessages, ClientId, Channel),
|
||||||
|
NQueue = lists:foldl(fun(F, Q) -> queue:in(F, Q) end, Queue, Frames),
|
||||||
|
{Outgoings, NChannel} = dispatch_frame(Channel#channel{mqueue = NQueue}),
|
||||||
|
{ok, [{outgoing, Outgoings}], NChannel}
|
||||||
|
end.
|
||||||
|
|
||||||
|
split_by_pos(L, Pos) ->
|
||||||
|
split_by_pos(L, Pos, []).
|
||||||
|
|
||||||
|
split_by_pos([], _, A1) ->
|
||||||
|
{lists:reverse(A1), []};
|
||||||
|
split_by_pos(L, 0, A1) ->
|
||||||
|
{lists:reverse(A1), L};
|
||||||
|
split_by_pos([E | L], N, A1) ->
|
||||||
|
split_by_pos(L, N - 1, [E | A1]).
|
||||||
|
|
||||||
|
msgs2frame(Messages, Vin, Channel) ->
|
||||||
|
lists:filtermap(
|
||||||
|
fun(#message{payload = Payload}) ->
|
||||||
|
case emqx_utils_json:safe_decode(Payload, [return_maps]) of
|
||||||
|
{ok, Maps} ->
|
||||||
|
case msg2frame(Maps, Vin) of
|
||||||
|
{error, Reason} ->
|
||||||
|
log(
|
||||||
|
debug,
|
||||||
|
#{
|
||||||
|
msg => "convert_message_to_frame_error",
|
||||||
|
reason => Reason,
|
||||||
|
data => Maps
|
||||||
|
},
|
||||||
|
Channel
|
||||||
|
),
|
||||||
|
false;
|
||||||
|
Frame ->
|
||||||
|
{true, Frame}
|
||||||
|
end;
|
||||||
|
{error, Reason} ->
|
||||||
|
log(error, #{msg => "json_decode_error", reason => Reason}, Channel),
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Messages
|
||||||
|
).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle call
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec handle_call(Req :: term(), From :: term(), channel()) ->
|
||||||
|
{reply, Reply :: term(), channel()}
|
||||||
|
| {reply, Reply :: term(), replies(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), Reply :: term(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), Reply :: term(), frame(), channel()}.
|
||||||
|
|
||||||
|
handle_call(kick, _From, Channel) ->
|
||||||
|
Channel1 = ensure_disconnected(kicked, Channel),
|
||||||
|
disconnect_and_shutdown(kicked, ok, Channel1);
|
||||||
|
handle_call(discard, _From, Channel) ->
|
||||||
|
disconnect_and_shutdown(discarded, ok, Channel);
|
||||||
|
handle_call(Req, _From, Channel) ->
|
||||||
|
log(error, #{msg => "unexpected_call", call => Req}, Channel),
|
||||||
|
reply(ignored, Channel).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle cast
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec handle_cast(Req :: term(), channel()) ->
|
||||||
|
ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}.
|
||||||
|
handle_cast(_Req, Channel) ->
|
||||||
|
{ok, Channel}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle info
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec handle_info(Info :: term(), channel()) ->
|
||||||
|
ok | {ok, channel()} | {shutdown, Reason :: term(), channel()}.
|
||||||
|
|
||||||
|
handle_info({sock_closed, Reason}, Channel = #channel{conn_state = idle}) ->
|
||||||
|
shutdown(Reason, Channel);
|
||||||
|
handle_info({sock_closed, Reason}, Channel = #channel{conn_state = connecting}) ->
|
||||||
|
shutdown(Reason, Channel);
|
||||||
|
handle_info(
|
||||||
|
{sock_closed, Reason},
|
||||||
|
Channel =
|
||||||
|
#channel{
|
||||||
|
conn_state = connected
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
NChannel = ensure_disconnected(Reason, Channel),
|
||||||
|
shutdown(Reason, NChannel);
|
||||||
|
handle_info({sock_closed, Reason}, Channel = #channel{conn_state = disconnected}) ->
|
||||||
|
log(error, #{msg => "unexpected_sock_closed", reason => Reason}, Channel),
|
||||||
|
{ok, Channel};
|
||||||
|
handle_info(Info, Channel) ->
|
||||||
|
log(error, #{msg => "unexpected_info}", info => Info}, Channel),
|
||||||
|
{ok, Channel}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Handle timeout
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec handle_timeout(reference(), Msg :: term(), channel()) ->
|
||||||
|
{ok, channel()}
|
||||||
|
| {ok, replies(), channel()}
|
||||||
|
| {shutdown, Reason :: term(), channel()}.
|
||||||
|
|
||||||
|
handle_timeout(
|
||||||
|
_TRef,
|
||||||
|
{keepalive, _StatVal},
|
||||||
|
Channel = #channel{keepalive = undefined}
|
||||||
|
) ->
|
||||||
|
{ok, Channel};
|
||||||
|
handle_timeout(
|
||||||
|
_TRef,
|
||||||
|
{keepalive, _StatVal},
|
||||||
|
Channel = #channel{conn_state = disconnected}
|
||||||
|
) ->
|
||||||
|
{ok, Channel};
|
||||||
|
handle_timeout(
|
||||||
|
_TRef,
|
||||||
|
{keepalive, StatVal},
|
||||||
|
Channel = #channel{keepalive = Keepalive}
|
||||||
|
) ->
|
||||||
|
case emqx_keepalive:check(StatVal, Keepalive) of
|
||||||
|
{ok, NKeepalive} ->
|
||||||
|
NChannel = Channel#channel{keepalive = NKeepalive},
|
||||||
|
{ok, reset_timer(alive_timer, NChannel)};
|
||||||
|
{error, timeout} ->
|
||||||
|
shutdown(keepalive_timeout, Channel)
|
||||||
|
end;
|
||||||
|
handle_timeout(
|
||||||
|
_TRef,
|
||||||
|
retry_delivery,
|
||||||
|
Channel = #channel{inflight = Inflight, retx_interval = RetxInterv}
|
||||||
|
) ->
|
||||||
|
case emqx_inflight:is_empty(Inflight) of
|
||||||
|
true ->
|
||||||
|
{ok, clean_timer(retry_timer, Channel)};
|
||||||
|
false ->
|
||||||
|
Frames = emqx_inflight:to_list(Inflight),
|
||||||
|
{Outgoings, NInflight} = retry_delivery(
|
||||||
|
Frames, erlang:system_time(millisecond), RetxInterv, Inflight, []
|
||||||
|
),
|
||||||
|
{Outgoings2, NChannel} = dispatch_frame(Channel#channel{inflight = NInflight}),
|
||||||
|
{ok, [{outgoing, Outgoings ++ Outgoings2}], reset_timer(retry_timer, NChannel)}
|
||||||
|
end;
|
||||||
|
handle_timeout(_TRef, Msg, Channel) ->
|
||||||
|
log(error, #{msg => "unexpected_timeout", content => Msg}, Channel),
|
||||||
|
{ok, Channel}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Ensure timers
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
ensure_timer(Name, Channel = #channel{timers = Timers}) ->
|
||||||
|
TRef = maps:get(Name, Timers, undefined),
|
||||||
|
Time = interval(Name, Channel),
|
||||||
|
case TRef == undefined andalso Time > 0 of
|
||||||
|
true -> ensure_timer(Name, Time, Channel);
|
||||||
|
%% Timer disabled or exists
|
||||||
|
false -> Channel
|
||||||
|
end.
|
||||||
|
|
||||||
|
ensure_timer(Name, Time, Channel = #channel{timers = Timers}) ->
|
||||||
|
log(debug, #{msg => "start_timer", name => Name, time => Time}, Channel),
|
||||||
|
Msg = maps:get(Name, ?TIMER_TABLE),
|
||||||
|
TRef = emqx_utils:start_timer(Time, Msg),
|
||||||
|
Channel#channel{timers = Timers#{Name => TRef}}.
|
||||||
|
|
||||||
|
reset_timer(Name, Channel) ->
|
||||||
|
ensure_timer(Name, clean_timer(Name, Channel)).
|
||||||
|
|
||||||
|
clean_timer(Name, Channel = #channel{timers = Timers}) ->
|
||||||
|
Channel#channel{timers = maps:remove(Name, Timers)}.
|
||||||
|
|
||||||
|
interval(alive_timer, #channel{keepalive = KeepAlive}) ->
|
||||||
|
emqx_keepalive:info(interval, KeepAlive);
|
||||||
|
interval(retry_timer, #channel{retx_interval = RetxIntv}) ->
|
||||||
|
RetxIntv.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Terminate
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
terminate(Reason, #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
session = Session,
|
||||||
|
clientinfo = ClientInfo
|
||||||
|
}) ->
|
||||||
|
run_hooks(Ctx, 'session.terminated', [ClientInfo, Reason, Session]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Ensure connected
|
||||||
|
|
||||||
|
enrich_clientinfo(
|
||||||
|
Packet,
|
||||||
|
Channel = #channel{
|
||||||
|
clientinfo = ClientInfo
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
{ok, NPacket, NClientInfo} = emqx_utils:pipeline(
|
||||||
|
[
|
||||||
|
fun maybe_assign_clientid/2,
|
||||||
|
%% FIXME: CALL After authentication successfully
|
||||||
|
fun fix_mountpoint/2
|
||||||
|
],
|
||||||
|
Packet,
|
||||||
|
ClientInfo
|
||||||
|
),
|
||||||
|
{ok, NPacket, Channel#channel{clientinfo = NClientInfo}}.
|
||||||
|
|
||||||
|
enrich_conninfo(
|
||||||
|
_Packet,
|
||||||
|
Channel = #channel{
|
||||||
|
conninfo = ConnInfo,
|
||||||
|
clientinfo = ClientInfo
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
#{clientid := ClientId, username := Username} = ClientInfo,
|
||||||
|
NConnInfo = ConnInfo#{
|
||||||
|
proto_name => <<"GBT32960">>,
|
||||||
|
proto_ver => <<"">>,
|
||||||
|
clean_start => true,
|
||||||
|
keepalive => 0,
|
||||||
|
expiry_interval => 0,
|
||||||
|
conn_props => #{},
|
||||||
|
receive_maximum => 0,
|
||||||
|
clientid => ClientId,
|
||||||
|
username => Username
|
||||||
|
},
|
||||||
|
{ok, Channel#channel{conninfo = NConnInfo}}.
|
||||||
|
|
||||||
|
set_log_meta(_Packet, #channel{clientinfo = #{clientid := ClientId}}) ->
|
||||||
|
emqx_logger:set_metadata_clientid(ClientId),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
auth_connect(
|
||||||
|
_Packet,
|
||||||
|
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}};
|
||||||
|
{error, Reason} ->
|
||||||
|
?SLOG(warning, #{
|
||||||
|
msg => "client_login_failed",
|
||||||
|
clientid => ClientId,
|
||||||
|
username => Username,
|
||||||
|
reason => Reason
|
||||||
|
}),
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
ensure_connected(
|
||||||
|
Channel = #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
conninfo = ConnInfo,
|
||||||
|
clientinfo = ClientInfo
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
NConnInfo = ConnInfo#{connected_at => erlang:system_time(millisecond)},
|
||||||
|
ok = run_hooks(Ctx, 'client.connected', [ClientInfo, NConnInfo]),
|
||||||
|
Channel#channel{
|
||||||
|
conninfo = NConnInfo,
|
||||||
|
conn_state = connected
|
||||||
|
}.
|
||||||
|
|
||||||
|
process_connect(
|
||||||
|
Frame,
|
||||||
|
Channel = #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
conninfo = ConnInfo,
|
||||||
|
clientinfo = ClientInfo
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
SessFun = fun(_, _) -> #{} end,
|
||||||
|
case
|
||||||
|
emqx_gateway_ctx:open_session(
|
||||||
|
Ctx,
|
||||||
|
true,
|
||||||
|
ClientInfo,
|
||||||
|
ConnInfo,
|
||||||
|
SessFun
|
||||||
|
)
|
||||||
|
of
|
||||||
|
{ok, #{session := Session}} ->
|
||||||
|
NChannel = Channel#channel{session = Session},
|
||||||
|
subscribe_downlink(?DEFAULT_DOWNLINK_TOPIC, Channel),
|
||||||
|
_ = upstreaming(Frame, NChannel),
|
||||||
|
%% XXX: connection_accepted is not defined by stomp protocol
|
||||||
|
_ = run_hooks(Ctx, 'client.connack', [ConnInfo, connection_accepted, #{}]),
|
||||||
|
handle_out({?ACK_SUCCESS, Frame}, [{event, connected}], NChannel);
|
||||||
|
{error, Reason} ->
|
||||||
|
log(
|
||||||
|
error,
|
||||||
|
#{
|
||||||
|
msg => "failed_to_open_session",
|
||||||
|
reason => Reason
|
||||||
|
},
|
||||||
|
Channel
|
||||||
|
),
|
||||||
|
shutdown(Reason, Channel)
|
||||||
|
end.
|
||||||
|
|
||||||
|
maybe_assign_clientid(#frame{vin = Vin}, ClientInfo) ->
|
||||||
|
{ok, ClientInfo#{clientid => Vin, username => Vin}}.
|
||||||
|
|
||||||
|
fix_mountpoint(_Packet, #{mountpoint := undefined}) ->
|
||||||
|
ok;
|
||||||
|
fix_mountpoint(_Packet, ClientInfo = #{mountpoint := Mountpoint}) ->
|
||||||
|
%% TODO: Enrich the variable replacement????
|
||||||
|
%% i.e: ${ClientInfo.auth_result.productKey}
|
||||||
|
Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo),
|
||||||
|
{ok, ClientInfo#{mountpoint := Mountpoint1}}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Ensure disconnected
|
||||||
|
|
||||||
|
ensure_disconnected(
|
||||||
|
Reason,
|
||||||
|
Channel = #channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
conninfo = ConnInfo,
|
||||||
|
clientinfo = ClientInfo
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)},
|
||||||
|
ok = run_hooks(
|
||||||
|
Ctx,
|
||||||
|
'client.disconnected',
|
||||||
|
[ClientInfo, Reason, NConnInfo]
|
||||||
|
),
|
||||||
|
Channel#channel{conninfo = NConnInfo, conn_state = disconnected}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Helper functions
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
run_hooks(Ctx, Name, Args) ->
|
||||||
|
emqx_gateway_ctx:metrics_inc(Ctx, Name),
|
||||||
|
emqx_hooks:run(Name, Args).
|
||||||
|
|
||||||
|
reply(Reply, Channel) ->
|
||||||
|
{reply, Reply, Channel}.
|
||||||
|
|
||||||
|
shutdown(Reason, Channel) ->
|
||||||
|
{shutdown, Reason, Channel}.
|
||||||
|
|
||||||
|
shutdown(Reason, Reply, Channel) ->
|
||||||
|
{shutdown, Reason, Reply, Channel}.
|
||||||
|
|
||||||
|
disconnect_and_shutdown(Reason, Reply, Channel) ->
|
||||||
|
shutdown(Reason, Reply, Channel).
|
||||||
|
|
||||||
|
retry_delivery([], _Now, _Interval, Inflight, Acc) ->
|
||||||
|
{lists:reverse(Acc), Inflight};
|
||||||
|
retry_delivery([{Key, {_Frame, 0, _}} | Frames], Now, Interval, Inflight, Acc) ->
|
||||||
|
%% todo log(error, "has arrived max re-send times, drop ~p", [Frame]),
|
||||||
|
NInflight = emqx_inflight:delete(Key, Inflight),
|
||||||
|
retry_delivery(Frames, Now, Interval, NInflight, Acc);
|
||||||
|
retry_delivery([{Key, {Frame, RetxCount, Ts}} | Frames], Now, Interval, Inflight, Acc) ->
|
||||||
|
Diff = Now - Ts,
|
||||||
|
case Diff >= Interval of
|
||||||
|
true ->
|
||||||
|
NInflight = emqx_inflight:update(Key, {Frame, RetxCount - 1, Now}, Inflight),
|
||||||
|
retry_delivery(Frames, Now, Interval, NInflight, [Frame | Acc]);
|
||||||
|
_ ->
|
||||||
|
retry_delivery(Frames, Now, Interval, Inflight, Acc)
|
||||||
|
end.
|
||||||
|
|
||||||
|
upstreaming(
|
||||||
|
Frame, Channel = #channel{clientinfo = #{mountpoint := Mountpoint, clientid := ClientId}}
|
||||||
|
) ->
|
||||||
|
{Topic, Payload} = transform(Frame, Mountpoint),
|
||||||
|
log(debug, #{msg => "upstreaming_to_topic", topic => Topic, payload => Payload}, Channel),
|
||||||
|
emqx:publish(emqx_message:make(ClientId, ?QOS_1, Topic, Payload)).
|
||||||
|
|
||||||
|
transform(Frame = ?CMD(Cmd), Mountpoint) ->
|
||||||
|
Suffix =
|
||||||
|
case Cmd of
|
||||||
|
?CMD_VIHECLE_LOGIN -> <<"/upstream/vlogin">>;
|
||||||
|
?CMD_INFO_REPORT -> <<"/upstream/info">>;
|
||||||
|
?CMD_INFO_RE_REPORT -> <<"/upstream/reinfo">>;
|
||||||
|
?CMD_VIHECLE_LOGOUT -> <<"/upstream/vlogout">>;
|
||||||
|
?CMD_PLATFORM_LOGIN -> <<"/upstream/plogin">>;
|
||||||
|
?CMD_PLATFORM_LOGOUT -> <<"/upstream/plogout">>;
|
||||||
|
%CMD_HEARTBEAT, CMD_SCHOOL_TIME ...
|
||||||
|
_ -> <<"/upstream/transparent">>
|
||||||
|
end,
|
||||||
|
Topic = emqx_mountpoint:mount(Mountpoint, Suffix),
|
||||||
|
Payload = to_json(Frame),
|
||||||
|
{Topic, Payload};
|
||||||
|
transform(Frame = #frame{ack = Ack}, Mountpoint) when
|
||||||
|
?IS_ACK_CODE(Ack)
|
||||||
|
->
|
||||||
|
Topic = emqx_mountpoint:mount(Mountpoint, <<"/upstream/response">>),
|
||||||
|
Payload = to_json(Frame),
|
||||||
|
{Topic, Payload}.
|
||||||
|
|
||||||
|
to_json(#frame{cmd = Cmd, vin = Vin, encrypt = Encrypt, data = Data}) ->
|
||||||
|
emqx_utils_json:encode(#{'Cmd' => Cmd, 'Vin' => Vin, 'Encrypt' => Encrypt, 'Data' => Data}).
|
||||||
|
|
||||||
|
ack(Code, Frame = #frame{data = Data, ack = ?ACK_IS_CMD}) ->
|
||||||
|
% PROTO: Update time & ack feilds only
|
||||||
|
Frame#frame{ack = Code, data = Data#{<<"Time">> => gentime()}}.
|
||||||
|
|
||||||
|
ack_frame(Key, Inflight) ->
|
||||||
|
case emqx_inflight:contain(Key, Inflight) of
|
||||||
|
true -> emqx_inflight:delete(Key, Inflight);
|
||||||
|
false -> Inflight
|
||||||
|
end.
|
||||||
|
|
||||||
|
dispatch_frame(
|
||||||
|
Channel = #channel{
|
||||||
|
mqueue = Queue,
|
||||||
|
inflight = Inflight,
|
||||||
|
retx_max_times = RetxMax
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
case emqx_inflight:is_full(Inflight) orelse queue:is_empty(Queue) of
|
||||||
|
true ->
|
||||||
|
{[], Channel};
|
||||||
|
false ->
|
||||||
|
{{value, Frame}, NewQueue} = queue:out(Queue),
|
||||||
|
|
||||||
|
log(debug, #{msg => "delivery", frame => Frame}, Channel),
|
||||||
|
|
||||||
|
NewInflight = emqx_inflight:insert(
|
||||||
|
Frame#frame.cmd, {Frame, RetxMax, erlang:system_time(millisecond)}, Inflight
|
||||||
|
),
|
||||||
|
NChannel = Channel#channel{mqueue = NewQueue, inflight = NewInflight},
|
||||||
|
{[Frame], ensure_timer(retry_timer, NChannel)}
|
||||||
|
end.
|
||||||
|
|
||||||
|
gentime() ->
|
||||||
|
{Year, Mon, Day} = date(),
|
||||||
|
{Hour, Min, Sec} = time(),
|
||||||
|
Year1 = list_to_integer(string:substr(integer_to_list(Year), 3, 2)),
|
||||||
|
#{
|
||||||
|
<<"Year">> => Year1,
|
||||||
|
<<"Month">> => Mon,
|
||||||
|
<<"Day">> => Day,
|
||||||
|
<<"Hour">> => Hour,
|
||||||
|
<<"Minute">> => Min,
|
||||||
|
<<"Second">> => Sec
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Message to frame
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
msg2frame(#{<<"Action">> := <<"Query">>, <<"Total">> := Total, <<"Ids">> := Ids}, Vin) ->
|
||||||
|
% Ids = [<<"0x01">>, <<"0x02">>] --> [1, 2]
|
||||||
|
Data = #{
|
||||||
|
<<"Time">> => gentime(),
|
||||||
|
<<"Total">> => Total,
|
||||||
|
<<"Ids">> => lists:map(fun hexstring_to_byte/1, Ids)
|
||||||
|
},
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_PARAM_QUERY, ack = ?ACK_IS_CMD, vin = Vin, encrypt = ?ENCRYPT_NONE, data = Data
|
||||||
|
};
|
||||||
|
msg2frame(#{<<"Action">> := <<"Setting">>, <<"Total">> := Total, <<"Params">> := Params}, Vin) ->
|
||||||
|
% Params = [#{<<"0x01">> := 5000}, #{<<"0x02">> := 400}]
|
||||||
|
% Params1 = [#{1 := 5000}, #{2 := 400}]
|
||||||
|
Params1 = lists:foldr(
|
||||||
|
fun(M, Acc) ->
|
||||||
|
[{K, V}] = maps:to_list(M),
|
||||||
|
[#{hexstring_to_byte(K) => V} | Acc]
|
||||||
|
end,
|
||||||
|
[],
|
||||||
|
Params
|
||||||
|
),
|
||||||
|
Data = #{<<"Time">> => gentime(), <<"Total">> => Total, <<"Params">> => Params1},
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_PARAM_SETTING, ack = ?ACK_IS_CMD, vin = Vin, encrypt = ?ENCRYPT_NONE, data = Data
|
||||||
|
};
|
||||||
|
msg2frame(Data = #{<<"Action">> := <<"Control">>, <<"Command">> := Command}, Vin) ->
|
||||||
|
Param = maps:get(<<"Param">>, Data, <<>>),
|
||||||
|
Data1 = #{
|
||||||
|
<<"Time">> => gentime(),
|
||||||
|
<<"Command">> => hexstring_to_byte(Command),
|
||||||
|
<<"Param">> => Param
|
||||||
|
},
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_TERMINAL_CTRL,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = Vin,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = Data1
|
||||||
|
};
|
||||||
|
msg2frame(_Data, _Vin) ->
|
||||||
|
{error, unsupproted}.
|
||||||
|
|
||||||
|
hexstring_to_byte(S) when is_binary(S) ->
|
||||||
|
hexstring_to_byte(binary_to_list(S));
|
||||||
|
hexstring_to_byte("0x" ++ S) ->
|
||||||
|
tune_byte(list_to_integer(S, 16));
|
||||||
|
hexstring_to_byte(S) ->
|
||||||
|
tune_byte(list_to_integer(S)).
|
||||||
|
|
||||||
|
tune_byte(I) when I =< 16#FF -> I;
|
||||||
|
tune_byte(_) -> exit(invalid_byte).
|
||||||
|
|
||||||
|
discard_downlink_messages([], _Channel) ->
|
||||||
|
ok;
|
||||||
|
discard_downlink_messages(Messages, Channel) ->
|
||||||
|
log(
|
||||||
|
error,
|
||||||
|
#{
|
||||||
|
msg => "discard_new_downlink_messages",
|
||||||
|
reason =>
|
||||||
|
"Discard new downlink messages due to that too"
|
||||||
|
" many messages are waiting their ACKs.",
|
||||||
|
messages => Messages
|
||||||
|
},
|
||||||
|
Channel
|
||||||
|
),
|
||||||
|
metrics_inc('delivery.dropped', Channel, erlang:length(Messages)).
|
||||||
|
|
||||||
|
log(Level, Meta, #channel{clientinfo = #{clientid := ClientId, username := Username}} = _Channel) ->
|
||||||
|
?SLOG(Level, Meta#{clientid => ClientId, username => Username}).
|
||||||
|
|
||||||
|
metrics_inc(Name, #channel{ctx = Ctx}, Oct) ->
|
||||||
|
emqx_gateway_ctx:metrics_inc(Ctx, Name, Oct).
|
||||||
|
|
||||||
|
subscribe_downlink(
|
||||||
|
Topic,
|
||||||
|
#channel{
|
||||||
|
ctx = Ctx,
|
||||||
|
clientinfo =
|
||||||
|
ClientInfo =
|
||||||
|
#{
|
||||||
|
clientid := ClientId,
|
||||||
|
mountpoint := Mountpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) ->
|
||||||
|
{ParsedTopic, SubOpts0} = emqx_topic:parse(Topic),
|
||||||
|
SubOpts = maps:merge(emqx_gateway_utils:default_subopts(), SubOpts0),
|
||||||
|
MountedTopic = emqx_mountpoint:mount(Mountpoint, ParsedTopic),
|
||||||
|
_ = emqx_broker:subscribe(MountedTopic, ClientId, SubOpts),
|
||||||
|
run_hooks(Ctx, 'session.subscribed', [ClientInfo, MountedTopic, SubOpts]).
|
|
@ -0,0 +1,806 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_gbt32960_frame).
|
||||||
|
|
||||||
|
-behaviour(emqx_gateway_frame).
|
||||||
|
|
||||||
|
-include("emqx_gbt32960.hrl").
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
%% emqx_gateway_frame callbacks
|
||||||
|
-export([
|
||||||
|
initial_parse_state/1,
|
||||||
|
serialize_opts/0,
|
||||||
|
serialize_pkt/2,
|
||||||
|
parse/2,
|
||||||
|
format/1,
|
||||||
|
type/1,
|
||||||
|
is_message/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-define(FLAG, 1 / binary).
|
||||||
|
-define(BYTE, 8 / big - integer).
|
||||||
|
-define(WORD, 16 / big - integer).
|
||||||
|
-define(DWORD, 32 / big - integer).
|
||||||
|
%% CMD: 1, ACK: 1, VIN: 17, Enc: 1, Len: 2
|
||||||
|
-define(HEADER_SIZE, 22).
|
||||||
|
|
||||||
|
-define(IS_RESPONSE(Ack),
|
||||||
|
Ack == ?ACK_SUCCESS orelse
|
||||||
|
Ack == ?ACK_ERROR orelse
|
||||||
|
Ack == ?ACK_VIN_REPEAT
|
||||||
|
).
|
||||||
|
|
||||||
|
-type phase() :: search_heading | parse.
|
||||||
|
|
||||||
|
-type parser_state() :: #{
|
||||||
|
data := binary(),
|
||||||
|
phase := phase()
|
||||||
|
}.
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
-export([serialize/1]).
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Init a Parser
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec initial_parse_state(map()) -> parser_state().
|
||||||
|
initial_parse_state(_) ->
|
||||||
|
#{data => <<>>, phase => search_heading}.
|
||||||
|
|
||||||
|
-spec serialize_opts() -> emqx_gateway_frame:serialize_options().
|
||||||
|
serialize_opts() ->
|
||||||
|
#{}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Parse Message
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
parse(Bin, State) ->
|
||||||
|
case enter_parse(Bin, State) of
|
||||||
|
{ok, Message, Rest} ->
|
||||||
|
{ok, Message, Rest, State#{parse => search_heading}};
|
||||||
|
{error, Error} ->
|
||||||
|
{error, Error};
|
||||||
|
{more_data_follow, Partial} ->
|
||||||
|
{more, State#{data => Partial, phase => parse}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
enter_parse(Bin, #{phase := search_heading}) ->
|
||||||
|
case search_heading(Bin) of
|
||||||
|
{ok, Rest} ->
|
||||||
|
parse_msg(Rest);
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end;
|
||||||
|
enter_parse(Bin, #{data := Data}) ->
|
||||||
|
parse_msg(<<Data/binary, Bin/binary>>).
|
||||||
|
|
||||||
|
search_heading(<<16#23, 16#23, Rest/binary>>) ->
|
||||||
|
{ok, Rest};
|
||||||
|
search_heading(<<_, Rest/binary>>) ->
|
||||||
|
search_heading(Rest);
|
||||||
|
search_heading(<<>>) ->
|
||||||
|
{error, invalid_frame}.
|
||||||
|
|
||||||
|
parse_msg(Binary) ->
|
||||||
|
case byte_size(Binary) >= ?HEADER_SIZE of
|
||||||
|
true ->
|
||||||
|
{Frame, Rest2} = parse_header(Binary),
|
||||||
|
case byte_size(Rest2) >= Frame#frame.length + 1 of
|
||||||
|
true -> parse_body(Rest2, Frame);
|
||||||
|
false -> {more_data_follow, Binary}
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
{more_data_follow, Binary}
|
||||||
|
end.
|
||||||
|
|
||||||
|
parse_header(<<Cmd, Ack, VIN:17/binary, Encrypt, Length:?WORD, Rest2/binary>> = Binary) ->
|
||||||
|
Check = cal_check(Binary, ?HEADER_SIZE, undefined),
|
||||||
|
{
|
||||||
|
#frame{cmd = Cmd, ack = Ack, vin = VIN, encrypt = Encrypt, length = Length, check = Check},
|
||||||
|
Rest2
|
||||||
|
}.
|
||||||
|
|
||||||
|
parse_body(Binary, Frame = #frame{length = Length, check = OldCheck, encrypt = Encrypt}) ->
|
||||||
|
<<Data:Length/binary, CheckByte, Rest/binary>> = Binary,
|
||||||
|
Check = cal_check(Binary, Length, OldCheck),
|
||||||
|
case CheckByte == Check of
|
||||||
|
true ->
|
||||||
|
RawData = decipher(Data, Encrypt),
|
||||||
|
{ok, Frame#frame{data = parse_data(Frame, RawData), rawdata = RawData}, Rest};
|
||||||
|
false ->
|
||||||
|
{error, frame_check_error}
|
||||||
|
end.
|
||||||
|
|
||||||
|
% Algo: ?ENCRYPT_NONE, ENCRYPT_RSA, ENCRYPT_AES128
|
||||||
|
decipher(Data, _Algo) ->
|
||||||
|
% TODO: decypher data
|
||||||
|
Data.
|
||||||
|
|
||||||
|
% Algo: ?ENCRYPT_NONE, ENCRYPT_RSA, ENCRYPT_AES128
|
||||||
|
encipher(Data, _Algo) ->
|
||||||
|
% TODO: encipher data
|
||||||
|
Data.
|
||||||
|
|
||||||
|
parse_data(
|
||||||
|
#frame{cmd = ?CMD_VIHECLE_LOGIN},
|
||||||
|
<<Year:?BYTE, Month:?BYTE, Day:?BYTE, Hour:?BYTE, Minute:?BYTE, Second:?BYTE, Seq:?WORD,
|
||||||
|
ICCID:20/binary, Num:?BYTE, Length:?BYTE, Id/binary>>
|
||||||
|
) ->
|
||||||
|
#{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => Year,
|
||||||
|
<<"Month">> => Month,
|
||||||
|
<<"Day">> => Day,
|
||||||
|
<<"Hour">> => Hour,
|
||||||
|
<<"Minute">> => Minute,
|
||||||
|
<<"Second">> => Second
|
||||||
|
},
|
||||||
|
<<"Seq">> => Seq,
|
||||||
|
<<"ICCID">> => ICCID,
|
||||||
|
<<"Num">> => Num,
|
||||||
|
<<"Length">> => Length,
|
||||||
|
<<"Id">> => Id
|
||||||
|
};
|
||||||
|
parse_data(
|
||||||
|
#frame{cmd = ?CMD_INFO_REPORT},
|
||||||
|
<<Year:?BYTE, Month:?BYTE, Day:?BYTE, Hour:?BYTE, Minute:?BYTE, Second:?BYTE, Infos/binary>>
|
||||||
|
) ->
|
||||||
|
#{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => Year,
|
||||||
|
<<"Month">> => Month,
|
||||||
|
<<"Day">> => Day,
|
||||||
|
<<"Hour">> => Hour,
|
||||||
|
<<"Minute">> => Minute,
|
||||||
|
<<"Second">> => Second
|
||||||
|
},
|
||||||
|
<<"Infos">> => parse_info(Infos, [])
|
||||||
|
};
|
||||||
|
parse_data(
|
||||||
|
#frame{cmd = ?CMD_INFO_RE_REPORT},
|
||||||
|
<<Year:?BYTE, Month:?BYTE, Day:?BYTE, Hour:?BYTE, Minute:?BYTE, Second:?BYTE, Infos/binary>>
|
||||||
|
) ->
|
||||||
|
#{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => Year,
|
||||||
|
<<"Month">> => Month,
|
||||||
|
<<"Day">> => Day,
|
||||||
|
<<"Hour">> => Hour,
|
||||||
|
<<"Minute">> => Minute,
|
||||||
|
<<"Second">> => Second
|
||||||
|
},
|
||||||
|
<<"Infos">> => parse_info(Infos, [])
|
||||||
|
};
|
||||||
|
parse_data(
|
||||||
|
#frame{cmd = ?CMD_VIHECLE_LOGOUT},
|
||||||
|
<<Year:?BYTE, Month:?BYTE, Day:?BYTE, Hour:?BYTE, Minute:?BYTE, Second:?BYTE, Seq:?WORD>>
|
||||||
|
) ->
|
||||||
|
#{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => Year,
|
||||||
|
<<"Month">> => Month,
|
||||||
|
<<"Day">> => Day,
|
||||||
|
<<"Hour">> => Hour,
|
||||||
|
<<"Minute">> => Minute,
|
||||||
|
<<"Second">> => Second
|
||||||
|
},
|
||||||
|
<<"Seq">> => Seq
|
||||||
|
};
|
||||||
|
parse_data(
|
||||||
|
#frame{cmd = ?CMD_PLATFORM_LOGIN},
|
||||||
|
<<Year:?BYTE, Month:?BYTE, Day:?BYTE, Hour:?BYTE, Minute:?BYTE, Second:?BYTE, Seq:?WORD,
|
||||||
|
Username:12/binary, Password:20/binary, Encrypt:?BYTE>>
|
||||||
|
) ->
|
||||||
|
#{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => Year,
|
||||||
|
<<"Month">> => Month,
|
||||||
|
<<"Day">> => Day,
|
||||||
|
<<"Hour">> => Hour,
|
||||||
|
<<"Minute">> => Minute,
|
||||||
|
<<"Second">> => Second
|
||||||
|
},
|
||||||
|
<<"Seq">> => Seq,
|
||||||
|
<<"Username">> => Username,
|
||||||
|
<<"Password">> => Password,
|
||||||
|
<<"Encrypt">> => Encrypt
|
||||||
|
};
|
||||||
|
parse_data(
|
||||||
|
#frame{cmd = ?CMD_PLATFORM_LOGOUT},
|
||||||
|
<<Year:?BYTE, Month:?BYTE, Day:?BYTE, Hour:?BYTE, Minute:?BYTE, Second:?BYTE, Seq:?WORD>>
|
||||||
|
) ->
|
||||||
|
#{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => Year,
|
||||||
|
<<"Month">> => Month,
|
||||||
|
<<"Day">> => Day,
|
||||||
|
<<"Hour">> => Hour,
|
||||||
|
<<"Minute">> => Minute,
|
||||||
|
<<"Second">> => Second
|
||||||
|
},
|
||||||
|
<<"Seq">> => Seq
|
||||||
|
};
|
||||||
|
parse_data(#frame{cmd = ?CMD_HEARTBEAT}, <<>>) ->
|
||||||
|
#{};
|
||||||
|
parse_data(#frame{cmd = ?CMD_SCHOOL_TIME}, <<>>) ->
|
||||||
|
#{};
|
||||||
|
parse_data(
|
||||||
|
#frame{cmd = ?CMD_PARAM_QUERY},
|
||||||
|
<<Year:?BYTE, Month:?BYTE, Day:?BYTE, Hour:?BYTE, Minute:?BYTE, Second:?BYTE, Total:?BYTE,
|
||||||
|
Rest/binary>>
|
||||||
|
) ->
|
||||||
|
%% XXX: need check ACK filed?
|
||||||
|
#{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => Year,
|
||||||
|
<<"Month">> => Month,
|
||||||
|
<<"Day">> => Day,
|
||||||
|
<<"Hour">> => Hour,
|
||||||
|
<<"Minute">> => Minute,
|
||||||
|
<<"Second">> => Second
|
||||||
|
},
|
||||||
|
<<"Total">> => Total,
|
||||||
|
<<"Params">> => parse_params(Rest)
|
||||||
|
};
|
||||||
|
parse_data(
|
||||||
|
#frame{cmd = ?CMD_PARAM_SETTING},
|
||||||
|
<<Year:?BYTE, Month:?BYTE, Day:?BYTE, Hour:?BYTE, Minute:?BYTE, Second:?BYTE, Total:?BYTE,
|
||||||
|
Rest/binary>>
|
||||||
|
) ->
|
||||||
|
?SLOG(debug, #{msg => "rest", data => Rest}),
|
||||||
|
#{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => Year,
|
||||||
|
<<"Month">> => Month,
|
||||||
|
<<"Day">> => Day,
|
||||||
|
<<"Hour">> => Hour,
|
||||||
|
<<"Minute">> => Minute,
|
||||||
|
<<"Second">> => Second
|
||||||
|
},
|
||||||
|
<<"Total">> => Total,
|
||||||
|
<<"Params">> => parse_params(Rest)
|
||||||
|
};
|
||||||
|
parse_data(
|
||||||
|
#frame{cmd = ?CMD_TERMINAL_CTRL},
|
||||||
|
<<Year:?BYTE, Month:?BYTE, Day:?BYTE, Hour:?BYTE, Minute:?BYTE, Second:?BYTE, Command:?BYTE,
|
||||||
|
Rest/binary>>
|
||||||
|
) ->
|
||||||
|
#{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => Year,
|
||||||
|
<<"Month">> => Month,
|
||||||
|
<<"Day">> => Day,
|
||||||
|
<<"Hour">> => Hour,
|
||||||
|
<<"Minute">> => Minute,
|
||||||
|
<<"Second">> => Second
|
||||||
|
},
|
||||||
|
<<"Command">> => Command,
|
||||||
|
<<"Param">> => parse_ctrl_param(Command, Rest)
|
||||||
|
};
|
||||||
|
parse_data(Frame, Data) ->
|
||||||
|
?SLOG(error, #{msg => "invalid_frame", frame => Frame, data => Data}),
|
||||||
|
error(invalid_frame).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Parse Report Data Info
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_info(<<>>, Acc) ->
|
||||||
|
lists:reverse(Acc);
|
||||||
|
parse_info(<<?INFO_TYPE_VEHICLE, Body:20/binary, Rest/binary>>, Acc) ->
|
||||||
|
<<Status:?BYTE, Charging:?BYTE, Mode:?BYTE, Speed:?WORD, Mileage:?DWORD, Voltage:?WORD,
|
||||||
|
Current:?WORD, SOC:?BYTE, DC:?BYTE, Gear:?BYTE, Resistance:?WORD, AcceleratorPedal:?BYTE,
|
||||||
|
BrakePedal:?BYTE>> = Body,
|
||||||
|
parse_info(Rest, [
|
||||||
|
#{
|
||||||
|
<<"Type">> => <<"Vehicle">>,
|
||||||
|
<<"Status">> => Status,
|
||||||
|
<<"Charging">> => Charging,
|
||||||
|
<<"Mode">> => Mode,
|
||||||
|
<<"Speed">> => Speed,
|
||||||
|
<<"Mileage">> => Mileage,
|
||||||
|
<<"Voltage">> => Voltage,
|
||||||
|
<<"Current">> => Current,
|
||||||
|
<<"SOC">> => SOC,
|
||||||
|
<<"DC">> => DC,
|
||||||
|
<<"Gear">> => Gear,
|
||||||
|
<<"Resistance">> => Resistance,
|
||||||
|
<<"AcceleratorPedal">> => AcceleratorPedal,
|
||||||
|
<<"BrakePedal">> => BrakePedal
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]);
|
||||||
|
parse_info(<<?INFO_TYPE_DRIVE_MOTOR, Number, Rest/binary>>, Acc) ->
|
||||||
|
% 12 is packet len of per drive motor
|
||||||
|
Len = Number * 12,
|
||||||
|
<<Bodys:Len/binary, Rest1/binary>> = Rest,
|
||||||
|
parse_info(Rest1, [
|
||||||
|
#{
|
||||||
|
<<"Type">> => <<"DriveMotor">>,
|
||||||
|
<<"Number">> => Number,
|
||||||
|
<<"Motors">> => parse_drive_motor(Bodys, [])
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]);
|
||||||
|
parse_info(<<?INFO_TYPE_FUEL_CELL, Rest/binary>>, Acc) ->
|
||||||
|
<<CellVoltage:?WORD, CellCurrent:?WORD, FuelConsumption:?WORD, ProbeNum:?WORD, Rest1/binary>> =
|
||||||
|
Rest,
|
||||||
|
|
||||||
|
<<ProbeTemps:ProbeNum/binary, Rest2/binary>> = Rest1,
|
||||||
|
|
||||||
|
<<HMaxTemp:?WORD, HTempProbeCode:?BYTE, HMaxConc:?WORD, HConcSensorCode:?BYTE, HMaxPress:?WORD,
|
||||||
|
HPressSensorCode:?BYTE, DCStatus:?BYTE, Rest3/binary>> = Rest2,
|
||||||
|
parse_info(Rest3, [
|
||||||
|
#{
|
||||||
|
<<"Type">> => <<"FuelCell">>,
|
||||||
|
<<"CellVoltage">> => CellVoltage,
|
||||||
|
<<"CellCurrent">> => CellCurrent,
|
||||||
|
<<"FuelConsumption">> => FuelConsumption,
|
||||||
|
<<"ProbeNum">> => ProbeNum,
|
||||||
|
<<"ProbeTemps">> => binary_to_list(ProbeTemps),
|
||||||
|
<<"H_MaxTemp">> => HMaxTemp,
|
||||||
|
<<"H_TempProbeCode">> => HTempProbeCode,
|
||||||
|
<<"H_MaxConc">> => HMaxConc,
|
||||||
|
<<"H_ConcSensorCode">> => HConcSensorCode,
|
||||||
|
<<"H_MaxPress">> => HMaxPress,
|
||||||
|
<<"H_PressSensorCode">> => HPressSensorCode,
|
||||||
|
<<"DCStatus">> => DCStatus
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]);
|
||||||
|
parse_info(
|
||||||
|
<<?INFO_TYPE_ENGINE, Status:?BYTE, CrankshaftSpeed:?WORD, FuelConsumption:?WORD, Rest/binary>>,
|
||||||
|
Acc
|
||||||
|
) ->
|
||||||
|
parse_info(Rest, [
|
||||||
|
#{
|
||||||
|
<<"Type">> => <<"Engine">>,
|
||||||
|
<<"Status">> => Status,
|
||||||
|
<<"CrankshaftSpeed">> => CrankshaftSpeed,
|
||||||
|
<<"FuelConsumption">> => FuelConsumption
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]);
|
||||||
|
parse_info(
|
||||||
|
<<?INFO_TYPE_LOCATION, Status:?BYTE, Longitude:?DWORD, Latitude:?DWORD, Rest/binary>>, Acc
|
||||||
|
) ->
|
||||||
|
parse_info(Rest, [
|
||||||
|
#{
|
||||||
|
<<"Type">> => <<"Location">>,
|
||||||
|
<<"Status">> => Status,
|
||||||
|
<<"Longitude">> => Longitude,
|
||||||
|
<<"Latitude">> => Latitude
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]);
|
||||||
|
parse_info(<<?INFO_TYPE_EXTREME, Body:14/binary, Rest/binary>>, Acc) ->
|
||||||
|
<<MaxVoltageBatterySubsysNo:?BYTE, MaxVoltageBatteryCode:?BYTE, MaxBatteryVoltage:?WORD,
|
||||||
|
MinVoltageBatterySubsysNo:?BYTE, MinVoltageBatteryCode:?BYTE, MinBatteryVoltage:?WORD,
|
||||||
|
MaxTempSubsysNo:?BYTE, MaxTempProbeNo:?BYTE, MaxTemp:?BYTE, MinTempSubsysNo:?BYTE,
|
||||||
|
MinTempProbeNo:?BYTE, MinTemp:?BYTE>> = Body,
|
||||||
|
|
||||||
|
parse_info(Rest, [
|
||||||
|
#{
|
||||||
|
<<"Type">> => <<"Extreme">>,
|
||||||
|
<<"MaxVoltageBatterySubsysNo">> => MaxVoltageBatterySubsysNo,
|
||||||
|
<<"MaxVoltageBatteryCode">> => MaxVoltageBatteryCode,
|
||||||
|
<<"MaxBatteryVoltage">> => MaxBatteryVoltage,
|
||||||
|
<<"MinVoltageBatterySubsysNo">> => MinVoltageBatterySubsysNo,
|
||||||
|
<<"MinVoltageBatteryCode">> => MinVoltageBatteryCode,
|
||||||
|
<<"MinBatteryVoltage">> => MinBatteryVoltage,
|
||||||
|
<<"MaxTempSubsysNo">> => MaxTempSubsysNo,
|
||||||
|
<<"MaxTempProbeNo">> => MaxTempProbeNo,
|
||||||
|
<<"MaxTemp">> => MaxTemp,
|
||||||
|
<<"MinTempSubsysNo">> => MinTempSubsysNo,
|
||||||
|
<<"MinTempProbeNo">> => MinTempProbeNo,
|
||||||
|
<<"MinTemp">> => MinTemp
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]);
|
||||||
|
parse_info(<<?INFO_TYPE_ALARM, Rest/binary>>, Acc) ->
|
||||||
|
<<MaxAlarmLevel:?BYTE, GeneralAlarmFlag:?DWORD, FaultChargeableDeviceNum:?BYTE, Rest1/binary>> =
|
||||||
|
Rest,
|
||||||
|
N1 = FaultChargeableDeviceNum * 4,
|
||||||
|
<<FaultChargeableDeviceList:N1/binary, FaultDriveMotorNum:?BYTE, Rest2/binary>> = Rest1,
|
||||||
|
N2 = FaultDriveMotorNum * 4,
|
||||||
|
<<FaultDriveMotorList:N2/binary, FaultEngineNum:?BYTE, Rest3/binary>> = Rest2,
|
||||||
|
N3 = FaultEngineNum * 4,
|
||||||
|
<<FaultEngineList:N3/binary, FaultOthersNum:?BYTE, Rest4/binary>> = Rest3,
|
||||||
|
N4 = FaultOthersNum * 4,
|
||||||
|
<<FaultOthersList:N4/binary, Rest5/binary>> = Rest4,
|
||||||
|
parse_info(Rest5, [
|
||||||
|
#{
|
||||||
|
<<"Type">> => <<"Alarm">>,
|
||||||
|
<<"MaxAlarmLevel">> => MaxAlarmLevel,
|
||||||
|
<<"GeneralAlarmFlag">> => GeneralAlarmFlag,
|
||||||
|
<<"FaultChargeableDeviceNum">> => FaultChargeableDeviceNum,
|
||||||
|
<<"FaultChargeableDeviceList">> => tune_fault_codelist(FaultChargeableDeviceList),
|
||||||
|
<<"FaultDriveMotorNum">> => FaultDriveMotorNum,
|
||||||
|
<<"FaultDriveMotorList">> => tune_fault_codelist(FaultDriveMotorList),
|
||||||
|
<<"FaultEngineNum">> => FaultEngineNum,
|
||||||
|
<<"FaultEngineList">> => tune_fault_codelist(FaultEngineList),
|
||||||
|
<<"FaultOthersNum">> => FaultOthersNum,
|
||||||
|
<<"FaultOthersList">> => tune_fault_codelist(FaultOthersList)
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]);
|
||||||
|
parse_info(<<?INFO_TYPE_CHARGEABLE_VOLTAGE, Number:?BYTE, Rest/binary>>, Acc) ->
|
||||||
|
{Rest1, SubSystems} = parse_chargeable_voltage(Rest, Number, []),
|
||||||
|
parse_info(Rest1, [
|
||||||
|
#{
|
||||||
|
<<"Type">> => <<"ChargeableVoltage">>,
|
||||||
|
<<"Number">> => Number,
|
||||||
|
<<"SubSystems">> => SubSystems
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]);
|
||||||
|
parse_info(<<?INFO_TYPE_CHARGEABLE_TEMP, Number:?BYTE, Rest/binary>>, Acc) ->
|
||||||
|
{Rest1, SubSystems} = parse_chargeable_temp(Rest, Number, []),
|
||||||
|
parse_info(Rest1, [
|
||||||
|
#{
|
||||||
|
<<"Type">> => <<"ChargeableTemp">>,
|
||||||
|
<<"Number">> => Number,
|
||||||
|
<<"SubSystems">> => SubSystems
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]);
|
||||||
|
parse_info(Rest, Acc) ->
|
||||||
|
?SLOG(error, #{msg => "invalid_info_feild", rest => Rest, acc => Acc}),
|
||||||
|
error(invalid_info_feild).
|
||||||
|
|
||||||
|
parse_drive_motor(<<>>, Acc) ->
|
||||||
|
lists:reverse(Acc);
|
||||||
|
parse_drive_motor(
|
||||||
|
<<No:?BYTE, Status:?BYTE, CtrlTemp:?BYTE, Rotating:?WORD, Torque:?WORD, MotorTemp:?BYTE,
|
||||||
|
InputVoltage:?WORD, DCBusCurrent:?WORD, Rest/binary>>,
|
||||||
|
Acc
|
||||||
|
) ->
|
||||||
|
parse_drive_motor(Rest, [
|
||||||
|
#{
|
||||||
|
<<"No">> => No,
|
||||||
|
<<"Status">> => Status,
|
||||||
|
<<"CtrlTemp">> => CtrlTemp,
|
||||||
|
<<"Rotating">> => Rotating,
|
||||||
|
<<"Torque">> => Torque,
|
||||||
|
<<"MotorTemp">> => MotorTemp,
|
||||||
|
<<"InputVoltage">> => InputVoltage,
|
||||||
|
<<"DCBusCurrent">> => DCBusCurrent
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]).
|
||||||
|
|
||||||
|
parse_chargeable_voltage(Rest, 0, Acc) ->
|
||||||
|
{Rest, lists:reverse(Acc)};
|
||||||
|
parse_chargeable_voltage(
|
||||||
|
<<ChargeableSubsysNo:?BYTE, ChargeableVoltage:?WORD, ChargeableCurrent:?WORD, CellsTotal:?WORD,
|
||||||
|
FrameCellsIndex:?WORD, FrameCellsCount:?BYTE, Rest/binary>>,
|
||||||
|
Num,
|
||||||
|
Acc
|
||||||
|
) ->
|
||||||
|
Len = FrameCellsCount * 2,
|
||||||
|
<<CellsVoltage:Len/binary, Rest1/binary>> = Rest,
|
||||||
|
parse_chargeable_voltage(Rest1, Num - 1, [
|
||||||
|
#{
|
||||||
|
<<"ChargeableSubsysNo">> => ChargeableSubsysNo,
|
||||||
|
<<"ChargeableVoltage">> => ChargeableVoltage,
|
||||||
|
<<"ChargeableCurrent">> => ChargeableCurrent,
|
||||||
|
<<"CellsTotal">> => CellsTotal,
|
||||||
|
<<"FrameCellsIndex">> => FrameCellsIndex,
|
||||||
|
<<"FrameCellsCount">> => FrameCellsCount,
|
||||||
|
<<"CellsVoltage">> => tune_voltage(CellsVoltage)
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]).
|
||||||
|
|
||||||
|
parse_chargeable_temp(Rest, 0, Acc) ->
|
||||||
|
{Rest, lists:reverse(Acc)};
|
||||||
|
parse_chargeable_temp(<<ChargeableSubsysNo:?BYTE, ProbeNum:?WORD, Rest/binary>>, Num, Acc) ->
|
||||||
|
<<ProbesTemp:ProbeNum/binary, Rest1/binary>> = Rest,
|
||||||
|
parse_chargeable_temp(Rest1, Num - 1, [
|
||||||
|
#{
|
||||||
|
<<"ChargeableSubsysNo">> => ChargeableSubsysNo,
|
||||||
|
<<"ProbeNum">> => ProbeNum,
|
||||||
|
<<"ProbesTemp">> => binary_to_list(ProbesTemp)
|
||||||
|
}
|
||||||
|
| Acc
|
||||||
|
]).
|
||||||
|
tune_fault_codelist(<<>>) ->
|
||||||
|
[];
|
||||||
|
tune_fault_codelist(Data) ->
|
||||||
|
lists:flatten([list_to_binary(io_lib:format("~4.16.0B", [X])) || <<X:?DWORD>> <= Data]).
|
||||||
|
|
||||||
|
tune_voltage(Bin) -> tune_voltage_(Bin, []).
|
||||||
|
tune_voltage_(<<>>, Acc) -> lists:reverse(Acc);
|
||||||
|
tune_voltage_(<<V:?WORD, Rest/binary>>, Acc) -> tune_voltage_(Rest, [V | Acc]).
|
||||||
|
|
||||||
|
parse_params(Bin) -> parse_params_(Bin, []).
|
||||||
|
parse_params_(<<>>, Acc) ->
|
||||||
|
lists:reverse(Acc);
|
||||||
|
parse_params_(<<16#01, Val:?WORD, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x01">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#02, Val:?WORD, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x02">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#03, Val:?WORD, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x03">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#04, Val:?BYTE, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x04">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#05, Rest/binary>>, Acc) ->
|
||||||
|
case [V || #{<<"0x04">> := V} <- Acc] of
|
||||||
|
[Len] ->
|
||||||
|
<<Val:Len/binary, Rest1/binary>> = Rest,
|
||||||
|
parse_params_(Rest1, [#{<<"0x05">> => Val} | Acc]);
|
||||||
|
_ ->
|
||||||
|
?SLOG(error, #{
|
||||||
|
msg => "invalid_data", reason => "cmd_0x04 must appear ahead of cmd_0x05"
|
||||||
|
}),
|
||||||
|
lists:reverse(Acc)
|
||||||
|
end;
|
||||||
|
parse_params_(<<16#06, Val:?WORD, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x06">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#07, Val:5/binary, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x07">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#08, Val:5/binary, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x08">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#09, Val:?BYTE, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x09">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#0A, Val:?WORD, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x0A">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#0B, Val:?WORD, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x0B">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#0C, Val:?BYTE, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x0C">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#0D, Val:?BYTE, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x0D">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#0E, Rest/binary>>, Acc) ->
|
||||||
|
case [V || #{<<"0x0D">> := V} <- Acc] of
|
||||||
|
[Len] ->
|
||||||
|
<<Val:Len/binary, Rest1/binary>> = Rest,
|
||||||
|
parse_params_(Rest1, [#{<<"0x0E">> => Val} | Acc]);
|
||||||
|
_ ->
|
||||||
|
?SLOG(error, #{
|
||||||
|
msg => "invalid_data", reason => "cmd_0x0D must appear ahead of cmd_0x0E"
|
||||||
|
}),
|
||||||
|
lists:reverse(Acc)
|
||||||
|
end;
|
||||||
|
parse_params_(<<16#0F, Val:?WORD, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x0F">> => Val} | Acc]);
|
||||||
|
parse_params_(<<16#10, Val:?BYTE, Rest/binary>>, Acc) ->
|
||||||
|
parse_params_(Rest, [#{<<"0x10">> => Val} | Acc]);
|
||||||
|
parse_params_(Cmd, Acc) ->
|
||||||
|
?SLOG(error, #{msg => "unexcepted_param_identifier", cmd => Cmd}),
|
||||||
|
lists:reverse(Acc).
|
||||||
|
|
||||||
|
parse_ctrl_param(16#01, Param) ->
|
||||||
|
parse_upgrade_feild(Param);
|
||||||
|
parse_ctrl_param(16#02, _) ->
|
||||||
|
<<>>;
|
||||||
|
parse_ctrl_param(16#03, _) ->
|
||||||
|
<<>>;
|
||||||
|
parse_ctrl_param(16#04, _) ->
|
||||||
|
<<>>;
|
||||||
|
parse_ctrl_param(16#05, _) ->
|
||||||
|
<<>>;
|
||||||
|
parse_ctrl_param(16#06, <<Level:?BYTE, Msg/binary>>) ->
|
||||||
|
#{<<"Level">> => Level, <<"Message">> => Msg};
|
||||||
|
parse_ctrl_param(16#07, _) ->
|
||||||
|
<<>>;
|
||||||
|
parse_ctrl_param(Cmd, Param) ->
|
||||||
|
?SLOG(error, #{msg => "unexcepted_param", param => Param, cmd => Cmd}),
|
||||||
|
<<>>.
|
||||||
|
|
||||||
|
parse_upgrade_feild(Param) ->
|
||||||
|
[
|
||||||
|
DialingName,
|
||||||
|
Username,
|
||||||
|
Password,
|
||||||
|
<<0, 0, I1, I2, I3, I4>>,
|
||||||
|
<<Port:?WORD>>,
|
||||||
|
ManufacturerId,
|
||||||
|
HardwareVer,
|
||||||
|
SoftwareVer,
|
||||||
|
UpgradeUrl,
|
||||||
|
<<Timeout:?WORD>>
|
||||||
|
] = re:split(Param, ";", [{return, binary}]),
|
||||||
|
|
||||||
|
#{
|
||||||
|
<<"DialingName">> => DialingName,
|
||||||
|
<<"Username">> => Username,
|
||||||
|
<<"Password">> => Password,
|
||||||
|
<<"Ip">> => list_to_binary(inet:ntoa({I1, I2, I3, I4})),
|
||||||
|
<<"Port">> => Port,
|
||||||
|
<<"ManufacturerId">> => ManufacturerId,
|
||||||
|
<<"HardwareVer">> => HardwareVer,
|
||||||
|
<<"SoftwareVer">> => SoftwareVer,
|
||||||
|
<<"UpgradeUrl">> => UpgradeUrl,
|
||||||
|
<<"Timeout">> => Timeout
|
||||||
|
}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% serialize_pkt
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
serialize_pkt(Frame, _Opts) ->
|
||||||
|
serialize(Frame).
|
||||||
|
|
||||||
|
serialize(#frame{cmd = Cmd, ack = Ack, vin = Vin, encrypt = Encrypt, data = Data, rawdata = RawData}) ->
|
||||||
|
Encrypted = encipher(serialize_data(Cmd, Ack, RawData, Data), Encrypt),
|
||||||
|
Len = byte_size(Encrypted),
|
||||||
|
Stream = <<Cmd:?BYTE, Ack:?BYTE, Vin:17/binary, Encrypt:?BYTE, Len:?WORD, Encrypted/binary>>,
|
||||||
|
Crc = cal_check(Stream, byte_size(Stream), undefined),
|
||||||
|
<<"##", Stream/binary, Crc:?BYTE>>.
|
||||||
|
|
||||||
|
serialize_data(?CMD_PARAM_QUERY, ?ACK_IS_CMD, _, #{
|
||||||
|
<<"Time">> := Time,
|
||||||
|
<<"Total">> := Total,
|
||||||
|
<<"Ids">> := Ids
|
||||||
|
}) when length(Ids) == Total ->
|
||||||
|
T = tune_time(Time),
|
||||||
|
Ids1 = tune_ids(Ids),
|
||||||
|
<<T/binary, Total:?BYTE, Ids1/binary>>;
|
||||||
|
serialize_data(?CMD_PARAM_SETTING, ?ACK_IS_CMD, _, #{
|
||||||
|
<<"Time">> := Time,
|
||||||
|
<<"Total">> := Total,
|
||||||
|
<<"Params">> := Params
|
||||||
|
}) when length(Params) == Total ->
|
||||||
|
T = tune_time(Time),
|
||||||
|
Params1 = tune_params(Params),
|
||||||
|
<<T/binary, Total:?BYTE, Params1/binary>>;
|
||||||
|
serialize_data(?CMD_TERMINAL_CTRL, ?ACK_IS_CMD, _, #{
|
||||||
|
<<"Time">> := Time,
|
||||||
|
<<"Command">> := Cmd,
|
||||||
|
<<"Param">> := Param
|
||||||
|
}) ->
|
||||||
|
T = tune_time(Time),
|
||||||
|
Param1 = tune_ctrl_param(Cmd, Param),
|
||||||
|
<<T/binary, Cmd:?BYTE, Param1/binary>>;
|
||||||
|
serialize_data(_Cmd, Ack, RawData, #{<<"Time">> := Time}) when ?IS_RESPONSE(Ack) ->
|
||||||
|
Rest =
|
||||||
|
case byte_size(RawData) > 6 of
|
||||||
|
false -> <<>>;
|
||||||
|
true -> binary:part(RawData, 6, byte_size(RawData) - 6)
|
||||||
|
end,
|
||||||
|
T = tune_time(Time),
|
||||||
|
<<T/binary, Rest/binary>>.
|
||||||
|
|
||||||
|
tune_time(#{
|
||||||
|
<<"Year">> := Year,
|
||||||
|
<<"Month">> := Month,
|
||||||
|
<<"Day">> := Day,
|
||||||
|
<<"Hour">> := Hour,
|
||||||
|
<<"Minute">> := Min,
|
||||||
|
<<"Second">> := Sec
|
||||||
|
}) ->
|
||||||
|
<<Year:?BYTE, Month:?BYTE, Day:?BYTE, Hour:?BYTE, Min:?BYTE, Sec:?BYTE>>.
|
||||||
|
|
||||||
|
tune_ids(Ids) ->
|
||||||
|
lists:foldr(
|
||||||
|
fun
|
||||||
|
(Id, Acc) when is_integer(Id) ->
|
||||||
|
<<Id:8, Acc/binary>>;
|
||||||
|
(Id, Acc) when is_binary(Id) ->
|
||||||
|
<<Id/binary, Acc/binary>>
|
||||||
|
end,
|
||||||
|
<<>>,
|
||||||
|
Ids
|
||||||
|
).
|
||||||
|
|
||||||
|
tune_params(Params) ->
|
||||||
|
tune_params_(lists:reverse(Params), <<>>).
|
||||||
|
|
||||||
|
tune_params_([], Bin) ->
|
||||||
|
Bin;
|
||||||
|
tune_params_([#{16#01 := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest, <<16#01:?BYTE, Val:?WORD, Bin/binary>>);
|
||||||
|
tune_params_([#{16#02 := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest, <<16#02:?BYTE, Val:?WORD, Bin/binary>>);
|
||||||
|
tune_params_([#{16#03 := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest, <<16#03:?BYTE, Val:?WORD, Bin/binary>>);
|
||||||
|
tune_params_([#{16#04 := Val} | Rest], Bin) ->
|
||||||
|
{Val05, Rest1} = take_param(16#05, Rest),
|
||||||
|
tune_params_(Rest1, <<16#04:?BYTE, Val:?BYTE, 16#05, Val05:Val/binary, Bin/binary>>);
|
||||||
|
tune_params_([#{16#05 := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest ++ [#{16#05 => Val}], Bin);
|
||||||
|
tune_params_([#{16#06 := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest, <<16#06:?BYTE, Val:?WORD, Bin/binary>>);
|
||||||
|
tune_params_([#{16#07 := Val} | Rest], Bin) when byte_size(Val) == 5 ->
|
||||||
|
tune_params_(Rest, <<16#07:?BYTE, Val/binary, Bin/binary>>);
|
||||||
|
tune_params_([#{16#08 := Val} | Rest], Bin) when byte_size(Val) == 5 ->
|
||||||
|
tune_params_(Rest, <<16#08:?BYTE, Val/binary, Bin/binary>>);
|
||||||
|
tune_params_([#{16#09 := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest, <<16#09:?BYTE, Val:?BYTE, Bin/binary>>);
|
||||||
|
tune_params_([#{16#0A := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest, <<16#0A:?BYTE, Val:?WORD, Bin/binary>>);
|
||||||
|
tune_params_([#{16#0B := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest, <<16#0B:?BYTE, Val:?WORD, Bin/binary>>);
|
||||||
|
tune_params_([#{16#0C := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest, <<16#0C:?BYTE, Val:?BYTE, Bin/binary>>);
|
||||||
|
tune_params_([#{16#0D := Val} | Rest], Bin) ->
|
||||||
|
{Val0E, Rest1} = take_param(16#0E, Rest),
|
||||||
|
tune_params_(Rest1, <<16#0D:?BYTE, Val:?BYTE, 16#0E, Val0E:Val/binary, Bin/binary>>);
|
||||||
|
tune_params_([#{16#0E := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest ++ [#{16#0E => Val}], Bin);
|
||||||
|
tune_params_([#{16#0F := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest, <<16#0F:?BYTE, Val:?WORD, Bin/binary>>);
|
||||||
|
tune_params_([#{16#10 := Val} | Rest], Bin) ->
|
||||||
|
tune_params_(Rest, <<16#10:?BYTE, Val:?BYTE, Bin/binary>>).
|
||||||
|
|
||||||
|
tune_ctrl_param(16#00, _) ->
|
||||||
|
<<>>;
|
||||||
|
tune_ctrl_param(16#01, Param) ->
|
||||||
|
tune_upgrade_feild(Param);
|
||||||
|
tune_ctrl_param(16#02, _) ->
|
||||||
|
<<>>;
|
||||||
|
tune_ctrl_param(16#03, _) ->
|
||||||
|
<<>>;
|
||||||
|
tune_ctrl_param(16#04, _) ->
|
||||||
|
<<>>;
|
||||||
|
tune_ctrl_param(16#05, _) ->
|
||||||
|
<<>>;
|
||||||
|
tune_ctrl_param(16#06, #{<<"Level">> := Level, <<"Message">> := Msg}) ->
|
||||||
|
<<Level:?BYTE, Msg/binary>>;
|
||||||
|
tune_ctrl_param(16#07, _) ->
|
||||||
|
<<>>;
|
||||||
|
tune_ctrl_param(Cmd, Param) ->
|
||||||
|
?SLOG(error, #{msg => "unexcepted_cmd", cmd => Cmd, param => Param}),
|
||||||
|
<<>>.
|
||||||
|
|
||||||
|
tune_upgrade_feild(Param) ->
|
||||||
|
TuneBin = fun
|
||||||
|
(Bin, Len) when is_binary(Bin), byte_size(Bin) =:= Len -> Bin;
|
||||||
|
(undefined, _) -> undefined;
|
||||||
|
(Bin, _) -> error({invalid_param_length, Bin})
|
||||||
|
end,
|
||||||
|
TuneWrd = fun
|
||||||
|
(Val) when is_integer(Val), Val < 65535 -> <<Val:?WORD>>;
|
||||||
|
(undefined) -> undefined;
|
||||||
|
(_) -> error(invalid_param_word_value)
|
||||||
|
end,
|
||||||
|
TuneAdr = fun
|
||||||
|
(Ip) when is_binary(Ip) ->
|
||||||
|
{ok, {I1, I2, I3, I4}} = inet:parse_address(binary_to_list(Ip)),
|
||||||
|
<<0, 0, I1, I2, I3, I4>>;
|
||||||
|
(undefined) ->
|
||||||
|
undefined;
|
||||||
|
(_) ->
|
||||||
|
error(invalid_ip_address)
|
||||||
|
end,
|
||||||
|
L = [
|
||||||
|
maps:get(<<"DialingName">>, Param, undefined),
|
||||||
|
maps:get(<<"Username">>, Param, undefined),
|
||||||
|
maps:get(<<"Password">>, Param, undefined),
|
||||||
|
TuneAdr(maps:get(<<"Ip">>, Param, undefined)),
|
||||||
|
TuneWrd(maps:get(<<"Port">>, Param, undefined)),
|
||||||
|
TuneBin(maps:get(<<"ManufacturerId">>, Param, undefined), 4),
|
||||||
|
TuneBin(maps:get(<<"HardwareVer">>, Param, undefined), 5),
|
||||||
|
TuneBin(maps:get(<<"SoftwareVer">>, Param, undefined), 5),
|
||||||
|
maps:get(<<"UpgradeUrl">>, Param, undefined),
|
||||||
|
TuneWrd(maps:get(<<"Timeout">>, Param, undefined))
|
||||||
|
],
|
||||||
|
list_to_binary([I || I <- lists:join(";", L), I /= undefined]).
|
||||||
|
|
||||||
|
take_param(K, Params) ->
|
||||||
|
V = search_param(K, Params),
|
||||||
|
{V, Params -- [#{K => V}]}.
|
||||||
|
|
||||||
|
search_param(16#05, [#{16#05 := V} | _]) -> V;
|
||||||
|
search_param(16#0E, [#{16#0E := V} | _]) -> V;
|
||||||
|
search_param(K, [_ | Rest]) -> search_param(K, Rest).
|
||||||
|
|
||||||
|
cal_check(_, 0, Check) -> Check;
|
||||||
|
cal_check(<<C:8, Rest/binary>>, Size, undefined) -> cal_check(Rest, Size - 1, C);
|
||||||
|
cal_check(<<C:8, Rest/binary>>, Size, Check) -> cal_check(Rest, Size - 1, Check bxor C).
|
||||||
|
|
||||||
|
format(Msg) ->
|
||||||
|
io_lib:format("~p", [Msg]).
|
||||||
|
|
||||||
|
type(_) ->
|
||||||
|
gbt32960.
|
||||||
|
|
||||||
|
is_message(#frame{}) ->
|
||||||
|
true;
|
||||||
|
is_message(_) ->
|
||||||
|
false.
|
|
@ -0,0 +1,57 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_gbt32960_schema).
|
||||||
|
|
||||||
|
-include_lib("hocon/include/hoconsc.hrl").
|
||||||
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
|
-define(DEFAULT_MOUNTPOINT, <<"gbt32960/${clientid}">>).
|
||||||
|
|
||||||
|
%% config schema provides
|
||||||
|
-export([fields/1, desc/1]).
|
||||||
|
|
||||||
|
fields(gbt32960) ->
|
||||||
|
[
|
||||||
|
{mountpoint, emqx_gateway_schema:mountpoint(?DEFAULT_MOUNTPOINT)},
|
||||||
|
{retry_interval,
|
||||||
|
sc(
|
||||||
|
emqx_schema:duration_ms(),
|
||||||
|
#{
|
||||||
|
default => <<"8s">>,
|
||||||
|
desc => ?DESC(retry_interval)
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{max_retry_times,
|
||||||
|
sc(
|
||||||
|
non_neg_integer(),
|
||||||
|
#{
|
||||||
|
default => 3,
|
||||||
|
desc => ?DESC(max_retry_times)
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{message_queue_len,
|
||||||
|
sc(
|
||||||
|
non_neg_integer(),
|
||||||
|
#{
|
||||||
|
default => 10,
|
||||||
|
desc => ?DESC(message_queue_len)
|
||||||
|
}
|
||||||
|
)},
|
||||||
|
{listeners, sc(ref(emqx_gateway_schema, tcp_listeners), #{desc => ?DESC(tcp_listeners)})}
|
||||||
|
] ++ emqx_gateway_schema:gateway_common_options().
|
||||||
|
|
||||||
|
desc(gbt32960) ->
|
||||||
|
"The GBT-32960 gateway";
|
||||||
|
desc(_) ->
|
||||||
|
undefined.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% internal functions
|
||||||
|
|
||||||
|
sc(Type, Meta) ->
|
||||||
|
hoconsc:mk(Type, Meta).
|
||||||
|
|
||||||
|
ref(Mod, Field) ->
|
||||||
|
hoconsc:ref(Mod, Field).
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,924 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_gbt32960_parser_SUITE).
|
||||||
|
|
||||||
|
-compile(export_all).
|
||||||
|
-compile(nowarn_export_all).
|
||||||
|
|
||||||
|
-include("emqx_gbt32960.hrl").
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
-define(BYTE, 8 / big - integer).
|
||||||
|
-define(WORD, 16 / big - integer).
|
||||||
|
-define(DWORD, 32 / big - integer).
|
||||||
|
-define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)).
|
||||||
|
|
||||||
|
all() ->
|
||||||
|
[
|
||||||
|
case01_login,
|
||||||
|
case02_realtime_report_0x01,
|
||||||
|
case03_realtime_report_0x02,
|
||||||
|
case04_realtime_report_0x03,
|
||||||
|
case05_realtime_report_0x04,
|
||||||
|
case06_realtime_report_0x05,
|
||||||
|
case07_realtime_report_0x06,
|
||||||
|
case08_realtime_report_0x07,
|
||||||
|
case09_realtime_report_0x08,
|
||||||
|
case10_realtime_report_0x09,
|
||||||
|
case11_heartbeat,
|
||||||
|
case12_schooltime,
|
||||||
|
case13_param_query,
|
||||||
|
case14_param_setting,
|
||||||
|
case15_terminal_ctrl,
|
||||||
|
case16_serialize_ack,
|
||||||
|
case17_serialize_query,
|
||||||
|
case18_serialize_query,
|
||||||
|
case19_serialize_ctrl
|
||||||
|
].
|
||||||
|
|
||||||
|
init_per_suite(Config) ->
|
||||||
|
emqx_logger:set_log_level(debug),
|
||||||
|
Config.
|
||||||
|
|
||||||
|
end_per_suite(Config) ->
|
||||||
|
Config.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% helper functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
encode(Cmd, Vin, Data) ->
|
||||||
|
encode(Cmd, ?ACK_IS_CMD, Vin, ?ENCRYPT_NONE, Data).
|
||||||
|
|
||||||
|
encode(Cmd, Ack, Vin, Encrypt, Data) ->
|
||||||
|
Size = byte_size(Data),
|
||||||
|
S1 = <<Cmd:8, Ack:8, Vin:17/binary, Encrypt:8, Size:16, Data/binary>>,
|
||||||
|
Crc = make_crc(S1, undefined),
|
||||||
|
Stream = <<"##", S1/binary, Crc:8>>,
|
||||||
|
?LOGT("encode a packet=~p", [binary_to_hex_string(Stream)]),
|
||||||
|
Stream.
|
||||||
|
|
||||||
|
make_crc(<<>>, Xor) -> Xor;
|
||||||
|
make_crc(<<C:8, Rest/binary>>, undefined) -> make_crc(Rest, C);
|
||||||
|
make_crc(<<C:8, Rest/binary>>, Xor) -> make_crc(Rest, C bxor Xor).
|
||||||
|
|
||||||
|
make_time() ->
|
||||||
|
{Year, Mon, Day} = date(),
|
||||||
|
{Hour, Min, Sec} = time(),
|
||||||
|
Year1 = list_to_integer(string:substr(integer_to_list(Year), 3, 2)),
|
||||||
|
<<Year1:8, Mon:8, Day:8, Hour:8, Min:8, Sec:8>>.
|
||||||
|
|
||||||
|
binary_to_hex_string(Data) ->
|
||||||
|
lists:flatten([io_lib:format("~2.16.0B ", [X]) || <<X:8>> <= Data]).
|
||||||
|
|
||||||
|
to_json(#frame{cmd = Cmd, vin = Vin, encrypt = Encrypt, data = Data}) ->
|
||||||
|
emqx_utils_json:encode(#{'Cmd' => Cmd, 'Vin' => Vin, 'Encrypt' => Encrypt, 'Data' => Data}).
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% test case functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
case01_login(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<12, 12, 29, 12, 19, 20>>,
|
||||||
|
Data = <<Time/binary, 1:16, "12345678901234567890", 1, 1, "C">>,
|
||||||
|
Bin = encode(?CMD_VIHECLE_LOGIN, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_VIHECLE_LOGIN,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 12,
|
||||||
|
<<"Month">> := 12,
|
||||||
|
<<"Day">> := 29,
|
||||||
|
<<"Hour">> := 12,
|
||||||
|
<<"Minute">> := 19,
|
||||||
|
<<"Second">> := 20
|
||||||
|
},
|
||||||
|
<<"Seq">> := 1,
|
||||||
|
<<"ICCID">> := <<"12345678901234567890">>,
|
||||||
|
<<"Num">> := 1,
|
||||||
|
<<"Length">> := 1,
|
||||||
|
<<"Id">> := <<"C">>
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case02_realtime_report_0x01(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<16, 1, 1, 2, 59, 0>>,
|
||||||
|
VehicleState =
|
||||||
|
<<1:?BYTE, 1:?BYTE, 1:?BYTE, 2000:?WORD, 999999:?DWORD, 5000:?WORD, 15000:?WORD, 50:?BYTE,
|
||||||
|
1:?BYTE, 5:?BYTE, 6000:?WORD, 90:?BYTE, 0:?BYTE>>,
|
||||||
|
Data = <<Time/binary, 16#01, VehicleState/binary>>,
|
||||||
|
Bin = encode(?CMD_INFO_REPORT, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_INFO_REPORT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 16,
|
||||||
|
<<"Month">> := 1,
|
||||||
|
<<"Day">> := 1,
|
||||||
|
<<"Hour">> := 2,
|
||||||
|
<<"Minute">> := 59,
|
||||||
|
<<"Second">> := 0
|
||||||
|
},
|
||||||
|
<<"Infos">> := [
|
||||||
|
#{
|
||||||
|
<<"Type">> := <<"Vehicle">>,
|
||||||
|
<<"Status">> := 1,
|
||||||
|
<<"Charging">> := 1,
|
||||||
|
<<"Mode">> := 1,
|
||||||
|
<<"Speed">> := 2000,
|
||||||
|
<<"Mileage">> := 999999,
|
||||||
|
<<"Voltage">> := 5000,
|
||||||
|
<<"Current">> := 15000,
|
||||||
|
<<"SOC">> := 50,
|
||||||
|
<<"DC">> := 1,
|
||||||
|
<<"Gear">> := 5,
|
||||||
|
<<"Resistance">> := 6000,
|
||||||
|
<<"AcceleratorPedal">> := 90,
|
||||||
|
<<"BrakePedal">> := 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case03_realtime_report_0x02(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<16, 1, 1, 2, 59, 0>>,
|
||||||
|
DriveMotor1 =
|
||||||
|
<<1:?BYTE, 1:?BYTE, 125:?BYTE, 30000:?WORD, 25000:?WORD, 125:?BYTE, 30012:?WORD,
|
||||||
|
31203:?WORD>>,
|
||||||
|
DriveMotor2 =
|
||||||
|
<<2:?BYTE, 1:?BYTE, 125:?BYTE, 30200:?WORD, 25300:?WORD, 145:?BYTE, 32000:?WORD,
|
||||||
|
30200:?WORD>>,
|
||||||
|
Data = <<Time/binary, 16#02, 2:?BYTE, DriveMotor1/binary, DriveMotor2/binary>>,
|
||||||
|
Bin = encode(?CMD_INFO_REPORT, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_INFO_REPORT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 16,
|
||||||
|
<<"Month">> := 1,
|
||||||
|
<<"Day">> := 1,
|
||||||
|
<<"Hour">> := 2,
|
||||||
|
<<"Minute">> := 59,
|
||||||
|
<<"Second">> := 0
|
||||||
|
},
|
||||||
|
<<"Infos">> := [
|
||||||
|
#{
|
||||||
|
<<"Type">> := <<"DriveMotor">>,
|
||||||
|
<<"Number">> := 2,
|
||||||
|
<<"Motors">> := [
|
||||||
|
#{
|
||||||
|
<<"No">> := 1,
|
||||||
|
<<"Status">> := 1,
|
||||||
|
<<"CtrlTemp">> := 125,
|
||||||
|
<<"Rotating">> := 30000,
|
||||||
|
<<"Torque">> := 25000,
|
||||||
|
<<"MotorTemp">> := 125,
|
||||||
|
<<"InputVoltage">> := 30012,
|
||||||
|
<<"DCBusCurrent">> := 31203
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
<<"No">> := 2,
|
||||||
|
<<"Status">> := 1,
|
||||||
|
<<"CtrlTemp">> := 125,
|
||||||
|
<<"Rotating">> := 30200,
|
||||||
|
<<"Torque">> := 25300,
|
||||||
|
<<"MotorTemp">> := 145,
|
||||||
|
<<"InputVoltage">> := 32000,
|
||||||
|
<<"DCBusCurrent">> := 30200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case04_realtime_report_0x03(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<16, 1, 1, 2, 59, 0>>,
|
||||||
|
FuelCell =
|
||||||
|
<<10000:?WORD, 12000:?WORD, 45000:?WORD, 2:?WORD, 120:?BYTE, 121:?BYTE, 12500:?WORD,
|
||||||
|
10:?BYTE, 35000:?WORD, 11:?BYTE, 500:?WORD, 12:?BYTE, 1:?BYTE>>,
|
||||||
|
Data = <<Time/binary, 16#03, FuelCell/binary>>,
|
||||||
|
Bin = encode(?CMD_INFO_REPORT, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_INFO_REPORT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 16,
|
||||||
|
<<"Month">> := 1,
|
||||||
|
<<"Day">> := 1,
|
||||||
|
<<"Hour">> := 2,
|
||||||
|
<<"Minute">> := 59,
|
||||||
|
<<"Second">> := 0
|
||||||
|
},
|
||||||
|
<<"Infos">> := [
|
||||||
|
#{
|
||||||
|
<<"Type">> := <<"FuelCell">>,
|
||||||
|
<<"CellVoltage">> := 10000,
|
||||||
|
<<"CellCurrent">> := 12000,
|
||||||
|
<<"FuelConsumption">> := 45000,
|
||||||
|
<<"ProbeNum">> := 2,
|
||||||
|
<<"ProbeTemps">> := [120, 121],
|
||||||
|
<<"H_MaxTemp">> := 12500,
|
||||||
|
<<"H_TempProbeCode">> := 10,
|
||||||
|
<<"H_MaxConc">> := 35000,
|
||||||
|
<<"H_ConcSensorCode">> := 11,
|
||||||
|
<<"H_MaxPress">> := 500,
|
||||||
|
<<"H_PressSensorCode">> := 12,
|
||||||
|
<<"DCStatus">> := 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case05_realtime_report_0x04(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<16, 10, 1, 22, 59, 0>>,
|
||||||
|
Data = <<Time/binary, 16#04, 16#01, 2000:?WORD, 200:?WORD>>,
|
||||||
|
Bin = encode(?CMD_INFO_REPORT, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_INFO_REPORT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 16,
|
||||||
|
<<"Month">> := 10,
|
||||||
|
<<"Day">> := 1,
|
||||||
|
<<"Hour">> := 22,
|
||||||
|
<<"Minute">> := 59,
|
||||||
|
<<"Second">> := 0
|
||||||
|
},
|
||||||
|
<<"Infos">> := [
|
||||||
|
#{
|
||||||
|
<<"Type">> := <<"Engine">>,
|
||||||
|
<<"Status">> := 1,
|
||||||
|
<<"CrankshaftSpeed">> := 2000,
|
||||||
|
<<"FuelConsumption">> := 200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case06_realtime_report_0x05(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<16, 10, 1, 22, 59, 0>>,
|
||||||
|
Data = <<Time/binary, 16#05, 16#00, 10:?DWORD, 100:?DWORD>>,
|
||||||
|
Bin = encode(?CMD_INFO_REPORT, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_INFO_REPORT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 16,
|
||||||
|
<<"Month">> := 10,
|
||||||
|
<<"Day">> := 1,
|
||||||
|
<<"Hour">> := 22,
|
||||||
|
<<"Minute">> := 59,
|
||||||
|
<<"Second">> := 0
|
||||||
|
},
|
||||||
|
<<"Infos">> := [
|
||||||
|
#{
|
||||||
|
<<"Type">> := <<"Location">>,
|
||||||
|
<<"Status">> := 0,
|
||||||
|
<<"Longitude">> := 10,
|
||||||
|
<<"Latitude">> := 100
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case07_realtime_report_0x06(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<17, 5, 30, 12, 22, 59>>,
|
||||||
|
Extreme =
|
||||||
|
<<12:?BYTE, 10:?BYTE, 7500:?WORD, 13:?BYTE, 11:?BYTE, 2000:?WORD, 14:?BYTE, 12:?BYTE,
|
||||||
|
120:?BYTE, 15:?BYTE, 13:?BYTE, 40:?BYTE>>,
|
||||||
|
Data = <<Time/binary, 16#06, Extreme/binary>>,
|
||||||
|
Bin = encode(?CMD_INFO_REPORT, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_INFO_REPORT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 17,
|
||||||
|
<<"Month">> := 5,
|
||||||
|
<<"Day">> := 30,
|
||||||
|
<<"Hour">> := 12,
|
||||||
|
<<"Minute">> := 22,
|
||||||
|
<<"Second">> := 59
|
||||||
|
},
|
||||||
|
<<"Infos">> := [
|
||||||
|
#{
|
||||||
|
<<"Type">> := <<"Extreme">>,
|
||||||
|
<<"MaxVoltageBatterySubsysNo">> := 12,
|
||||||
|
<<"MaxVoltageBatteryCode">> := 10,
|
||||||
|
<<"MaxBatteryVoltage">> := 7500,
|
||||||
|
<<"MinVoltageBatterySubsysNo">> := 13,
|
||||||
|
<<"MinVoltageBatteryCode">> := 11,
|
||||||
|
<<"MinBatteryVoltage">> := 2000,
|
||||||
|
<<"MaxTempSubsysNo">> := 14,
|
||||||
|
<<"MaxTempProbeNo">> := 12,
|
||||||
|
<<"MaxTemp">> := 120,
|
||||||
|
<<"MinTempSubsysNo">> := 15,
|
||||||
|
<<"MinTempProbeNo">> := 13,
|
||||||
|
<<"MinTemp">> := 40
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case08_realtime_report_0x07(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<17, 12, 20, 22, 23, 59>>,
|
||||||
|
Alarm =
|
||||||
|
<<2:?BYTE, 0:?DWORD, 1:?BYTE, 123:?DWORD, 2:?BYTE, 123:?DWORD, 223:?DWORD, 1:?BYTE,
|
||||||
|
123:?DWORD, 1:?BYTE, 125:?DWORD>>,
|
||||||
|
Bin = encode(?CMD_INFO_REPORT, <<"1G1BL52P7TR115520">>, <<Time/binary, 16#07, Alarm/binary>>),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_INFO_REPORT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 17,
|
||||||
|
<<"Month">> := 12,
|
||||||
|
<<"Day">> := 20,
|
||||||
|
<<"Hour">> := 22,
|
||||||
|
<<"Minute">> := 23,
|
||||||
|
<<"Second">> := 59
|
||||||
|
},
|
||||||
|
<<"Infos">> := [
|
||||||
|
#{
|
||||||
|
<<"Type">> := <<"Alarm">>,
|
||||||
|
<<"MaxAlarmLevel">> := 2,
|
||||||
|
<<"GeneralAlarmFlag">> := 0,
|
||||||
|
<<"FaultChargeableDeviceNum">> := 1,
|
||||||
|
<<"FaultChargeableDeviceList">> := [<<"007B">>],
|
||||||
|
<<"FaultDriveMotorNum">> := 2,
|
||||||
|
<<"FaultDriveMotorList">> := [<<"007B">>, <<"00DF">>],
|
||||||
|
<<"FaultEngineNum">> := 1,
|
||||||
|
<<"FaultEngineList">> := [<<"007B">>],
|
||||||
|
<<"FaultOthersNum">> := 1,
|
||||||
|
<<"FaultOthersList">> := [<<"007D">>]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
|
||||||
|
Alarm1 = <<1:?BYTE, 3:?DWORD, 1:?BYTE, 200:?DWORD, 0:?BYTE, 1:?BYTE, 111:?DWORD, 0:?BYTE>>,
|
||||||
|
Bin1 = encode(
|
||||||
|
?CMD_INFO_RE_REPORT, <<"1G1BL52P7TR115520">>, <<Time/binary, 16#07, Alarm1/binary>>
|
||||||
|
),
|
||||||
|
{ok, Frame1, <<>>, _State1} = emqx_gbt32960_frame:parse(Bin1, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_INFO_RE_REPORT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 17,
|
||||||
|
<<"Month">> := 12,
|
||||||
|
<<"Day">> := 20,
|
||||||
|
<<"Hour">> := 22,
|
||||||
|
<<"Minute">> := 23,
|
||||||
|
<<"Second">> := 59
|
||||||
|
},
|
||||||
|
<<"Infos">> := [
|
||||||
|
#{
|
||||||
|
<<"Type">> := <<"Alarm">>,
|
||||||
|
<<"MaxAlarmLevel">> := 1,
|
||||||
|
<<"GeneralAlarmFlag">> := 3,
|
||||||
|
<<"FaultChargeableDeviceNum">> := 1,
|
||||||
|
<<"FaultChargeableDeviceList">> := [<<"00C8">>],
|
||||||
|
<<"FaultDriveMotorNum">> := 0,
|
||||||
|
<<"FaultDriveMotorList">> := [],
|
||||||
|
<<"FaultEngineNum">> := 1,
|
||||||
|
<<"FaultEngineList">> := [<<"006F">>],
|
||||||
|
<<"FaultOthersNum">> := 0,
|
||||||
|
<<"FaultOthersList">> := []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame1,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame1)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case09_realtime_report_0x08(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<16, 10, 1, 22, 59, 0>>,
|
||||||
|
VoltageSys1 = <<1:?BYTE, 5000:?WORD, 10000:?WORD, 2:?WORD, 0:?WORD, 1:?BYTE, 5000:?WORD>>,
|
||||||
|
VoltageSys2 = <<2:?BYTE, 5001:?WORD, 10001:?WORD, 2:?WORD, 1:?WORD, 1:?BYTE, 5001:?WORD>>,
|
||||||
|
Data = <<Time/binary, 16#08, 16#02, VoltageSys1/binary, VoltageSys2/binary>>,
|
||||||
|
Bin = encode(?CMD_INFO_REPORT, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_INFO_REPORT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 16,
|
||||||
|
<<"Month">> := 10,
|
||||||
|
<<"Day">> := 1,
|
||||||
|
<<"Hour">> := 22,
|
||||||
|
<<"Minute">> := 59,
|
||||||
|
<<"Second">> := 0
|
||||||
|
},
|
||||||
|
<<"Infos">> := [
|
||||||
|
#{
|
||||||
|
<<"Type">> := <<"ChargeableVoltage">>,
|
||||||
|
<<"Number">> := 2,
|
||||||
|
<<"SubSystems">> := [
|
||||||
|
#{
|
||||||
|
<<"ChargeableSubsysNo">> := 1,
|
||||||
|
<<"ChargeableVoltage">> := 5000,
|
||||||
|
<<"ChargeableCurrent">> := 10000,
|
||||||
|
<<"CellsTotal">> := 2,
|
||||||
|
<<"FrameCellsIndex">> := 0,
|
||||||
|
<<"FrameCellsCount">> := 1,
|
||||||
|
<<"CellsVoltage">> := [5000]
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
<<"ChargeableSubsysNo">> := 2,
|
||||||
|
<<"ChargeableVoltage">> := 5001,
|
||||||
|
<<"ChargeableCurrent">> := 10001,
|
||||||
|
<<"CellsTotal">> := 2,
|
||||||
|
<<"FrameCellsIndex">> := 1,
|
||||||
|
<<"FrameCellsCount">> := 1,
|
||||||
|
<<"CellsVoltage">> := [5001]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case10_realtime_report_0x09(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<16, 10, 1, 22, 59, 0>>,
|
||||||
|
Temp1 = <<1:?BYTE, 10:?WORD, 5000:80>>,
|
||||||
|
Temp2 = <<2:?BYTE, 1:?WORD, 100:?BYTE>>,
|
||||||
|
Data = <<Time/binary, 16#09, 16#02, Temp1/binary, Temp2/binary>>,
|
||||||
|
Bin = encode(?CMD_INFO_REPORT, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_INFO_REPORT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 16,
|
||||||
|
<<"Month">> := 10,
|
||||||
|
<<"Day">> := 1,
|
||||||
|
<<"Hour">> := 22,
|
||||||
|
<<"Minute">> := 59,
|
||||||
|
<<"Second">> := 0
|
||||||
|
},
|
||||||
|
<<"Infos">> := [
|
||||||
|
#{
|
||||||
|
<<"Type">> := <<"ChargeableTemp">>,
|
||||||
|
<<"Number">> := 2,
|
||||||
|
<<"SubSystems">> := [
|
||||||
|
#{
|
||||||
|
<<"ChargeableSubsysNo">> := 1,
|
||||||
|
<<"ProbeNum">> := 10,
|
||||||
|
<<"ProbesTemp">> := [0, 0, 0, 0, 0, 0, 0, 0, 19, 136]
|
||||||
|
},
|
||||||
|
#{
|
||||||
|
<<"ChargeableSubsysNo">> := 2,
|
||||||
|
<<"ProbeNum">> := 1,
|
||||||
|
<<"ProbesTemp">> := [100]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case11_heartbeat(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Bin = encode(?CMD_HEARTBEAT, <<"1G1BL52P7TR115520">>, <<>>),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_HEARTBEAT,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case12_schooltime(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Bin = encode(?CMD_SCHOOL_TIME, <<"1G1BL52P7TR115520">>, <<>>),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_SCHOOL_TIME,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case13_param_query(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<17, 12, 18, 9, 22, 30>>,
|
||||||
|
Data =
|
||||||
|
<<Time/binary, 5, 1, 5000:?WORD, 4, 10, 5, "google.com", 16#0D, 14, 16#0E,
|
||||||
|
"www.google.com">>,
|
||||||
|
Bin = encode(?CMD_PARAM_QUERY, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_PARAM_QUERY,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 17,
|
||||||
|
<<"Month">> := 12,
|
||||||
|
<<"Day">> := 18,
|
||||||
|
<<"Hour">> := 9,
|
||||||
|
<<"Minute">> := 22,
|
||||||
|
<<"Second">> := 30
|
||||||
|
},
|
||||||
|
<<"Total">> := 5,
|
||||||
|
<<"Params">> := [
|
||||||
|
#{<<"0x01">> := 5000},
|
||||||
|
#{<<"0x04">> := 10},
|
||||||
|
#{<<"0x05">> := <<"google.com">>},
|
||||||
|
#{<<"0x0D">> := 14},
|
||||||
|
#{<<"0x0E">> := <<"www.google.com">>}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case14_param_setting(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<17, 12, 18, 9, 22, 30>>,
|
||||||
|
Data =
|
||||||
|
<<Time/binary, 5, 1, 5000:?WORD, 4, 10, 5, "google.com", 16#0D, 14, 16#0E,
|
||||||
|
"www.google.com">>,
|
||||||
|
Bin = encode(?CMD_PARAM_SETTING, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_PARAM_SETTING,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 17,
|
||||||
|
<<"Month">> := 12,
|
||||||
|
<<"Day">> := 18,
|
||||||
|
<<"Hour">> := 9,
|
||||||
|
<<"Minute">> := 22,
|
||||||
|
<<"Second">> := 30
|
||||||
|
},
|
||||||
|
<<"Total">> := 5,
|
||||||
|
<<"Params">> := [
|
||||||
|
#{<<"0x01">> := 5000},
|
||||||
|
#{<<"0x04">> := 10},
|
||||||
|
#{<<"0x05">> := <<"google.com">>},
|
||||||
|
#{<<"0x0D">> := 14},
|
||||||
|
#{<<"0x0E">> := <<"www.google.com">>}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case15_terminal_ctrl(_Config) ->
|
||||||
|
Parser = emqx_gbt32960_frame:initial_parse_state(#{}),
|
||||||
|
Time = <<17, 12, 18, 9, 22, 30>>,
|
||||||
|
Data = <<Time/binary, 16#02>>,
|
||||||
|
Bin = encode(?CMD_TERMINAL_CTRL, <<"1G1BL52P7TR115520">>, Data),
|
||||||
|
{ok, Frame, <<>>, _State} = emqx_gbt32960_frame:parse(Bin, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_TERMINAL_CTRL,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 17,
|
||||||
|
<<"Month">> := 12,
|
||||||
|
<<"Day">> := 18,
|
||||||
|
<<"Hour">> := 9,
|
||||||
|
<<"Minute">> := 22,
|
||||||
|
<<"Second">> := 30
|
||||||
|
},
|
||||||
|
<<"Command">> := 2,
|
||||||
|
<<"Param">> := <<>>
|
||||||
|
}
|
||||||
|
} = Frame,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame)]),
|
||||||
|
|
||||||
|
Param1 =
|
||||||
|
<<"emqtt;eusername;password;", 0, 0, 192, 168, 1, 1, ";", 8080:?WORD,
|
||||||
|
";vhid;1.0.0;0.0.1;ftp://emqtt.io/ftp/server;", 3000:?WORD>>,
|
||||||
|
Data1 = <<Time/binary, 16#01, Param1/binary>>,
|
||||||
|
Bin1 = encode(?CMD_TERMINAL_CTRL, <<"1G1BL52P7TR115520">>, Data1),
|
||||||
|
{ok, Frame1, <<>>, _State1} = emqx_gbt32960_frame:parse(Bin1, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_TERMINAL_CTRL,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 17,
|
||||||
|
<<"Month">> := 12,
|
||||||
|
<<"Day">> := 18,
|
||||||
|
<<"Hour">> := 9,
|
||||||
|
<<"Minute">> := 22,
|
||||||
|
<<"Second">> := 30
|
||||||
|
},
|
||||||
|
<<"Command">> := 1,
|
||||||
|
<<"Param">> := #{
|
||||||
|
<<"DialingName">> := <<"emqtt">>,
|
||||||
|
<<"Username">> := <<"eusername">>,
|
||||||
|
<<"Password">> := <<"password">>,
|
||||||
|
<<"Ip">> := <<"192.168.1.1">>,
|
||||||
|
<<"Port">> := 8080,
|
||||||
|
<<"ManufacturerId">> := <<"vhid">>,
|
||||||
|
<<"HardwareVer">> := <<"1.0.0">>,
|
||||||
|
<<"SoftwareVer">> := <<"0.0.1">>,
|
||||||
|
<<"UpgradeUrl">> := <<"ftp://emqtt.io/ftp/server">>,
|
||||||
|
<<"Timeout">> := 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = Frame1,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame1)]),
|
||||||
|
|
||||||
|
Param2 = <<"This is a alarm text!!!">>,
|
||||||
|
Data2 = <<Time/binary, 16#06, 16#01, Param2/binary>>,
|
||||||
|
Bin2 = encode(?CMD_TERMINAL_CTRL, <<"1G1BL52P7TR115520">>, Data2),
|
||||||
|
{ok, Frame2, <<>>, _State2} = emqx_gbt32960_frame:parse(Bin2, Parser),
|
||||||
|
#frame{
|
||||||
|
cmd = ?CMD_TERMINAL_CTRL,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> := #{
|
||||||
|
<<"Year">> := 17,
|
||||||
|
<<"Month">> := 12,
|
||||||
|
<<"Day">> := 18,
|
||||||
|
<<"Hour">> := 9,
|
||||||
|
<<"Minute">> := 22,
|
||||||
|
<<"Second">> := 30
|
||||||
|
},
|
||||||
|
<<"Command">> := 6,
|
||||||
|
<<"Param">> := #{
|
||||||
|
<<"Level">> := 1,
|
||||||
|
<<"Message">> := Param2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} = Frame2,
|
||||||
|
?LOGT("frame: ~p", [to_json(Frame2)]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case16_serialize_ack(_Config) ->
|
||||||
|
% Vechile login
|
||||||
|
DataUnit = <<1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1>>,
|
||||||
|
Frame = #frame{
|
||||||
|
cmd = ?CMD_VIHECLE_LOGIN,
|
||||||
|
ack = ?ACK_SUCCESS,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => 11,
|
||||||
|
<<"Month">> => 10,
|
||||||
|
<<"Day">> => 25,
|
||||||
|
<<"Hour">> => 20,
|
||||||
|
<<"Minute">> => 5,
|
||||||
|
<<"Second">> => 51
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rawdata = <<17, 11, 23, 21, 4, 50, DataUnit/binary>>
|
||||||
|
},
|
||||||
|
Bin = emqx_gbt32960_frame:serialize(Frame),
|
||||||
|
BodyLen = byte_size(Bin) - 3,
|
||||||
|
<<"##", Body:BodyLen/binary, Crc:?BYTE>> = Bin,
|
||||||
|
<<?CMD_VIHECLE_LOGIN, ?ACK_SUCCESS, "1G1BL52P7TR115520", ?ENCRYPT_NONE, 26:?WORD, 11:?BYTE,
|
||||||
|
10:?BYTE, 25:?BYTE, 20:?BYTE, 5:?BYTE, 51:?BYTE, DataUnit/binary>> = Body,
|
||||||
|
Crc = make_crc(Body, undefined),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case17_serialize_query(_Config) ->
|
||||||
|
DataUnit = <<2, 1, 2>>,
|
||||||
|
Frame = #frame{
|
||||||
|
cmd = ?CMD_PARAM_QUERY,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => 11,
|
||||||
|
<<"Month">> => 10,
|
||||||
|
<<"Day">> => 25,
|
||||||
|
<<"Hour">> => 20,
|
||||||
|
<<"Minute">> => 5,
|
||||||
|
<<"Second">> => 51
|
||||||
|
},
|
||||||
|
<<"Total">> => 2,
|
||||||
|
<<"Ids">> => [1, 2]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Bin = emqx_gbt32960_frame:serialize(Frame),
|
||||||
|
BodyLen = byte_size(Bin) - 3,
|
||||||
|
<<"##", Body:BodyLen/binary, Crc:?BYTE>> = Bin,
|
||||||
|
<<?CMD_PARAM_QUERY, ?ACK_IS_CMD, "1G1BL52P7TR115520", ?ENCRYPT_NONE, 9:?WORD, 11, 10, 25, 20, 5,
|
||||||
|
51, DataUnit/binary>> = Body,
|
||||||
|
Crc = make_crc(Body, undefined),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case18_serialize_query(_Config) ->
|
||||||
|
DataUnit =
|
||||||
|
<<6, 1, 30000:?WORD, 4, 10, 5, "google.com", 7, "1.0.0", 16#0D, 14, 16#0E,
|
||||||
|
"www.google.com">>,
|
||||||
|
Frame = #frame{
|
||||||
|
cmd = ?CMD_PARAM_SETTING,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => 17,
|
||||||
|
<<"Month">> => 10,
|
||||||
|
<<"Day">> => 25,
|
||||||
|
<<"Hour">> => 23,
|
||||||
|
<<"Minute">> => 59,
|
||||||
|
<<"Second">> => 59
|
||||||
|
},
|
||||||
|
<<"Total">> => 6,
|
||||||
|
<<"Params">> => [
|
||||||
|
#{1 => 30000},
|
||||||
|
#{4 => 10},
|
||||||
|
#{5 => <<"google.com">>},
|
||||||
|
#{7 => <<"1.0.0">>},
|
||||||
|
#{16#0D => 14},
|
||||||
|
#{16#0E => <<"www.google.com">>}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Bin = emqx_gbt32960_frame:serialize(Frame),
|
||||||
|
BodyLen = byte_size(Bin) - 3,
|
||||||
|
<<"##", Body:BodyLen/binary, Crc:?BYTE>> = Bin,
|
||||||
|
<<?CMD_PARAM_SETTING, ?ACK_IS_CMD, "1G1BL52P7TR115520", ?ENCRYPT_NONE, 46:?WORD, 17, 10, 25, 23,
|
||||||
|
59, 59, DataUnit/binary>> = Body,
|
||||||
|
Crc = make_crc(Body, undefined),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
case19_serialize_ctrl(_Config) ->
|
||||||
|
Frame = #frame{
|
||||||
|
cmd = ?CMD_TERMINAL_CTRL,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => 17,
|
||||||
|
<<"Month">> => 10,
|
||||||
|
<<"Day">> => 25,
|
||||||
|
<<"Hour">> => 22,
|
||||||
|
<<"Minute">> => 5,
|
||||||
|
<<"Second">> => 51
|
||||||
|
},
|
||||||
|
<<"Command">> => 2,
|
||||||
|
<<"Param">> => <<>>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Bin = emqx_gbt32960_frame:serialize(Frame),
|
||||||
|
BodyLen = byte_size(Bin) - 3,
|
||||||
|
<<"##", Body:BodyLen/binary, Crc:?BYTE>> = Bin,
|
||||||
|
<<?CMD_TERMINAL_CTRL, ?ACK_IS_CMD, "1G1BL52P7TR115520", ?ENCRYPT_NONE, 7:?WORD, 17, 10, 25, 22,
|
||||||
|
5, 51, 2>> = Body,
|
||||||
|
Crc = make_crc(Body, undefined),
|
||||||
|
|
||||||
|
DataUnit1 = <<"The alarm has occured!">>,
|
||||||
|
Frame1 = #frame{
|
||||||
|
cmd = ?CMD_TERMINAL_CTRL,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => 17,
|
||||||
|
<<"Month">> => 10,
|
||||||
|
<<"Day">> => 25,
|
||||||
|
<<"Hour">> => 22,
|
||||||
|
<<"Minute">> => 5,
|
||||||
|
<<"Second">> => 51
|
||||||
|
},
|
||||||
|
<<"Command">> => 6,
|
||||||
|
<<"Param">> => #{
|
||||||
|
<<"Level">> => 1,
|
||||||
|
<<"Message">> => DataUnit1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Bin1 = emqx_gbt32960_frame:serialize(Frame1),
|
||||||
|
BodyLen1 = byte_size(Bin1) - 3,
|
||||||
|
<<"##", Body1:BodyLen1/binary, Crc1:?BYTE>> = Bin1,
|
||||||
|
<<?CMD_TERMINAL_CTRL, ?ACK_IS_CMD, "1G1BL52P7TR115520", ?ENCRYPT_NONE, 30:?WORD, 17, 10, 25, 22,
|
||||||
|
5, 51, 6, 1, DataUnit1/binary>> = Body1,
|
||||||
|
Crc1 = make_crc(Body1, undefined),
|
||||||
|
|
||||||
|
DataUnit2 =
|
||||||
|
<<"emqtt;eusername;password;", 0, 0, 192, 168, 1, 1, ";", 8080:?WORD,
|
||||||
|
";BWM1;1.0.0;0.0.1;ftp://emqtt.io/ftp/server;", 3000:?WORD>>,
|
||||||
|
Frame2 = #frame{
|
||||||
|
cmd = ?CMD_TERMINAL_CTRL,
|
||||||
|
ack = ?ACK_IS_CMD,
|
||||||
|
vin = <<"1G1BL52P7TR115520">>,
|
||||||
|
encrypt = ?ENCRYPT_NONE,
|
||||||
|
data = #{
|
||||||
|
<<"Time">> => #{
|
||||||
|
<<"Year">> => 17,
|
||||||
|
<<"Month">> => 10,
|
||||||
|
<<"Day">> => 25,
|
||||||
|
<<"Hour">> => 22,
|
||||||
|
<<"Minute">> => 5,
|
||||||
|
<<"Second">> => 51
|
||||||
|
},
|
||||||
|
<<"Command">> => 1,
|
||||||
|
<<"Param">> => #{
|
||||||
|
<<"DialingName">> => <<"emqtt">>,
|
||||||
|
<<"Username">> => <<"eusername">>,
|
||||||
|
<<"Password">> => <<"password">>,
|
||||||
|
<<"Ip">> => <<"192.168.1.1">>,
|
||||||
|
<<"Port">> => 8080,
|
||||||
|
<<"ManufacturerId">> => <<"BWM1">>,
|
||||||
|
<<"HardwareVer">> => <<"1.0.0">>,
|
||||||
|
<<"SoftwareVer">> => <<"0.0.1">>,
|
||||||
|
<<"UpgradeUrl">> => <<"ftp://emqtt.io/ftp/server">>,
|
||||||
|
<<"Timeout">> => 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Bin2 = emqx_gbt32960_frame:serialize(Frame2),
|
||||||
|
BodyLen2 = byte_size(Bin2) - 3,
|
||||||
|
<<"##", Body2:BodyLen2/binary, Crc2:?BYTE>> = Bin2,
|
||||||
|
<<?CMD_TERMINAL_CTRL, ?ACK_IS_CMD, "1G1BL52P7TR115520", ?ENCRYPT_NONE, 87:?WORD, 17, 10, 25, 22,
|
||||||
|
5, 51, 1, DataUnitSeried2/binary>> = Body2,
|
||||||
|
?assertEqual(DataUnit2, DataUnitSeried2),
|
||||||
|
?assertEqual(Crc2, make_crc(Body2, undefined)),
|
||||||
|
ok.
|
|
@ -125,7 +125,8 @@
|
||||||
emqx_gcp_device,
|
emqx_gcp_device,
|
||||||
emqx_dashboard_rbac,
|
emqx_dashboard_rbac,
|
||||||
emqx_dashboard_sso,
|
emqx_dashboard_sso,
|
||||||
emqx_audit
|
emqx_audit,
|
||||||
|
emqx_gateway_gbt32960
|
||||||
],
|
],
|
||||||
%% must always be of type `load'
|
%% must always be of type `load'
|
||||||
ce_business_apps =>
|
ce_business_apps =>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Introduced a new gateway for vehicles to access EMQX through the GBT32960 protocol.
|
3
mix.exs
3
mix.exs
|
@ -215,7 +215,8 @@ defmodule EMQXUmbrella.MixProject do
|
||||||
:emqx_gcp_device,
|
:emqx_gcp_device,
|
||||||
:emqx_dashboard_rbac,
|
:emqx_dashboard_rbac,
|
||||||
:emqx_dashboard_sso,
|
:emqx_dashboard_sso,
|
||||||
:emqx_audit
|
:emqx_audit,
|
||||||
|
:emqx_gateway_gbt32960
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,7 @@ is_community_umbrella_app("apps/emqx_gcp_device") -> false;
|
||||||
is_community_umbrella_app("apps/emqx_dashboard_rbac") -> false;
|
is_community_umbrella_app("apps/emqx_dashboard_rbac") -> false;
|
||||||
is_community_umbrella_app("apps/emqx_dashboard_sso") -> false;
|
is_community_umbrella_app("apps/emqx_dashboard_sso") -> false;
|
||||||
is_community_umbrella_app("apps/emqx_audit") -> false;
|
is_community_umbrella_app("apps/emqx_audit") -> false;
|
||||||
|
is_community_umbrella_app("apps/emqx_gateway_gbt32960") -> false;
|
||||||
is_community_umbrella_app(_) -> true.
|
is_community_umbrella_app(_) -> true.
|
||||||
|
|
||||||
is_jq_supported() ->
|
is_jq_supported() ->
|
||||||
|
|
|
@ -37,8 +37,7 @@ gateway_name.desc:
|
||||||
"""Gateway Name"""
|
"""Gateway Name"""
|
||||||
|
|
||||||
gateway_name_in_qs.desc:
|
gateway_name_in_qs.desc:
|
||||||
"""Gateway Name.<br/>
|
"""Gateway Name"""
|
||||||
It's enum with `stomp`, `mqttsn`, `coap`, `lwm2m`, `exproto`"""
|
|
||||||
|
|
||||||
gateway_node_status.desc:
|
gateway_node_status.desc:
|
||||||
"""The status of the gateway on each node in the cluster"""
|
"""The status of the gateway on each node in the cluster"""
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
emqx_gbt32960_schema {
|
||||||
|
|
||||||
|
retry_interval.desc:
|
||||||
|
"""Re-send time interval"""
|
||||||
|
|
||||||
|
max_retry_times.desc:
|
||||||
|
"""Re-send max times"""
|
||||||
|
|
||||||
|
message_queue_len.desc:
|
||||||
|
"""Max message queue length"""
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue