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}}) ->
|
||||
try
|
||||
GwName = gw_name(Name),
|
||||
case emqx_gateway:lookup(GwName) of
|
||||
case emqx_gateway:lookup(Name) of
|
||||
undefined ->
|
||||
{200, #{name => GwName, status => unloaded}};
|
||||
{200, #{name => Name, status => unloaded}};
|
||||
Gateway ->
|
||||
GwConf = emqx_gateway_conf:gateway(Name),
|
||||
GwInfo0 = emqx_gateway_utils:unix_ts_to_rfc3339(
|
||||
|
@ -125,15 +124,14 @@ gateway(put, #{
|
|||
}) ->
|
||||
GwConf = maps:without([<<"name">>], GwConf0),
|
||||
try
|
||||
GwName = gw_name(Name),
|
||||
LoadOrUpdateF =
|
||||
case emqx_gateway:lookup(GwName) of
|
||||
case emqx_gateway:lookup(Name) of
|
||||
undefined ->
|
||||
fun emqx_gateway_conf:load_gateway/2;
|
||||
_ ->
|
||||
fun emqx_gateway_conf:update_gateway/2
|
||||
end,
|
||||
case LoadOrUpdateF(GwName, GwConf) of
|
||||
case LoadOrUpdateF(Name, GwConf) of
|
||||
{ok, _} ->
|
||||
{204};
|
||||
{error, Reason} ->
|
||||
|
@ -148,12 +146,11 @@ gateway(put, #{
|
|||
|
||||
gateway_enable(put, #{bindings := #{name := Name, enable := Enable}}) ->
|
||||
try
|
||||
GwName = gw_name(Name),
|
||||
case emqx_gateway:lookup(GwName) of
|
||||
case emqx_gateway:lookup(Name) of
|
||||
undefined ->
|
||||
return_http_error(404, <<"NOT FOUND">>);
|
||||
_Gateway ->
|
||||
{ok, _} = emqx_gateway_conf:update_gateway(GwName, #{<<"enable">> => Enable}),
|
||||
{ok, _} = emqx_gateway_conf:update_gateway(Name, #{<<"enable">> => Enable}),
|
||||
{204}
|
||||
end
|
||||
catch
|
||||
|
@ -161,14 +158,6 @@ gateway_enable(put, #{bindings := #{name := Name, enable := Enable}}) ->
|
|||
return_http_error(404, <<"NOT FOUND">>)
|
||||
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
|
||||
%%--------------------------------------------------------------------
|
||||
|
@ -249,7 +238,7 @@ params_gateway_name_in_path() ->
|
|||
[
|
||||
{name,
|
||||
mk(
|
||||
binary(),
|
||||
hoconsc:enum(emqx_gateway_schema:gateway_names()),
|
||||
#{
|
||||
in => path,
|
||||
desc => ?DESC(gateway_name_in_qs),
|
||||
|
@ -390,7 +379,8 @@ fields(Gw) when
|
|||
Gw == mqttsn;
|
||||
Gw == coap;
|
||||
Gw == lwm2m;
|
||||
Gw == exproto
|
||||
Gw == exproto;
|
||||
Gw == gbt32960
|
||||
->
|
||||
[{name, mk(Gw, #{desc => ?DESC(gateway_name)})}] ++
|
||||
convert_listener_struct(emqx_gateway_schema:gateway_schema(Gw));
|
||||
|
@ -399,7 +389,8 @@ fields(Gw) when
|
|||
Gw == update_mqttsn;
|
||||
Gw == update_coap;
|
||||
Gw == update_lwm2m;
|
||||
Gw == update_exproto
|
||||
Gw == update_exproto;
|
||||
Gw == update_gbt32960
|
||||
->
|
||||
"update_" ++ GwStr = atom_to_list(Gw),
|
||||
Gw1 = list_to_existing_atom(GwStr),
|
||||
|
@ -447,31 +438,30 @@ fields(gateway_stats) ->
|
|||
[{key, mk(binary(), #{})}].
|
||||
|
||||
schema_load_or_update_gateways_conf() ->
|
||||
Names = emqx_gateway_schema:gateway_names(),
|
||||
emqx_dashboard_swagger:schema_with_examples(
|
||||
hoconsc:union([
|
||||
ref(?MODULE, stomp),
|
||||
ref(?MODULE, mqttsn),
|
||||
ref(?MODULE, coap),
|
||||
ref(?MODULE, lwm2m),
|
||||
ref(?MODULE, exproto),
|
||||
ref(?MODULE, update_stomp),
|
||||
ref(?MODULE, update_mqttsn),
|
||||
ref(?MODULE, update_coap),
|
||||
ref(?MODULE, update_lwm2m),
|
||||
ref(?MODULE, update_exproto)
|
||||
]),
|
||||
hoconsc:union(
|
||||
[
|
||||
ref(?MODULE, Name)
|
||||
|| Name <-
|
||||
Names ++
|
||||
[
|
||||
erlang:list_to_existing_atom("update_" ++ erlang:atom_to_list(Name))
|
||||
|| Name <- Names
|
||||
]
|
||||
]
|
||||
),
|
||||
examples_update_gateway_confs()
|
||||
).
|
||||
|
||||
schema_gateways_conf() ->
|
||||
emqx_dashboard_swagger:schema_with_examples(
|
||||
hoconsc:union([
|
||||
ref(?MODULE, stomp),
|
||||
ref(?MODULE, mqttsn),
|
||||
ref(?MODULE, coap),
|
||||
ref(?MODULE, lwm2m),
|
||||
ref(?MODULE, exproto)
|
||||
]),
|
||||
hoconsc:union(
|
||||
[
|
||||
ref(?MODULE, Name)
|
||||
|| Name <- emqx_gateway_schema:gateway_names()
|
||||
]
|
||||
),
|
||||
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 =>
|
||||
#{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,
|
||||
mk(
|
||||
binary(),
|
||||
hoconsc:enum(emqx_gateway_schema:gateway_names()),
|
||||
#{
|
||||
in => path,
|
||||
desc => ?DESC(emqx_gateway_api, gateway_name_in_qs),
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
%%--------------------------------------------------------------------
|
||||
|
||||
api_spec() ->
|
||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}).
|
||||
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
|
||||
|
||||
paths() ->
|
||||
[
|
||||
|
@ -157,7 +157,7 @@ params_gateway_name_in_path() ->
|
|||
[
|
||||
{name,
|
||||
mk(
|
||||
binary(),
|
||||
hoconsc:enum(emqx_gateway_schema:gateway_names()),
|
||||
#{
|
||||
in => path,
|
||||
desc => ?DESC(emqx_gateway_api, gateway_name_in_qs),
|
||||
|
|
|
@ -700,7 +700,7 @@ params_gateway_name_in_path() ->
|
|||
[
|
||||
{name,
|
||||
mk(
|
||||
binary(),
|
||||
hoconsc:enum(emqx_gateway_schema:gateway_names()),
|
||||
#{
|
||||
in => path,
|
||||
desc => ?DESC(emqx_gateway_api, gateway_name)
|
||||
|
|
|
@ -609,7 +609,7 @@ params_gateway_name_in_path() ->
|
|||
[
|
||||
{name,
|
||||
mk(
|
||||
binary(),
|
||||
hoconsc:enum(emqx_gateway_schema:gateway_names()),
|
||||
#{
|
||||
in => path,
|
||||
desc => ?DESC(emqx_gateway_api, gateway_name_in_qs),
|
||||
|
|
|
@ -513,29 +513,23 @@ codestr(501) -> 'NOT_IMPLEMENTED'.
|
|||
fmtstr(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_gateway(GwName0, fun(GwName, _GwConf) ->
|
||||
Authn = emqx_gateway_http:authn(GwName),
|
||||
Fun(GwName, Authn)
|
||||
end).
|
||||
|
||||
-spec with_listener_authn(binary(), binary(), function()) -> any().
|
||||
-spec with_listener_authn(atom(), binary(), function()) -> any().
|
||||
with_listener_authn(GwName0, Id, Fun) ->
|
||||
with_gateway(GwName0, fun(GwName, _GwConf) ->
|
||||
Authn = emqx_gateway_http:authn(GwName, Id),
|
||||
Fun(GwName, Authn)
|
||||
end).
|
||||
|
||||
-spec with_gateway(binary(), function()) -> any().
|
||||
with_gateway(GwName0, Fun) ->
|
||||
-spec with_gateway(atom(), function()) -> any().
|
||||
with_gateway(GwName, Fun) ->
|
||||
try
|
||||
GwName =
|
||||
try
|
||||
binary_to_existing_atom(GwName0)
|
||||
catch
|
||||
_:_ -> error(badname)
|
||||
end,
|
||||
case emqx_gateway:lookup(GwName) of
|
||||
undefined ->
|
||||
return_http_error(404, "Gateway not loaded");
|
||||
|
|
|
@ -48,12 +48,13 @@
|
|||
ip_port/0
|
||||
]).
|
||||
-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([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.
|
||||
|
||||
|
@ -337,12 +338,21 @@ proxy_protocol_opts() ->
|
|||
%% dynamic schemas
|
||||
|
||||
%% FIXME: don't hardcode the gateway names
|
||||
gateway_schema(stomp) -> emqx_stomp_schema:fields(stomp);
|
||||
gateway_schema(mqttsn) -> emqx_mqttsn_schema:fields(mqttsn);
|
||||
gateway_schema(coap) -> emqx_coap_schema:fields(coap);
|
||||
gateway_schema(lwm2m) -> emqx_lwm2m_schema:fields(lwm2m);
|
||||
gateway_schema(exproto) -> emqx_exproto_schema:fields(exproto).
|
||||
gateway_schema(Name) ->
|
||||
case emqx_gateway_utils:find_gateway_definition(Name) of
|
||||
{ok, #{config_schema_module := SchemaMod}} ->
|
||||
SchemaMod:fields(Name);
|
||||
{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
|
||||
|
||||
|
|
|
@ -45,8 +45,10 @@
|
|||
global_chain/1,
|
||||
listener_chain/3,
|
||||
find_gateway_definitions/0,
|
||||
find_gateway_definition/1,
|
||||
plus_max_connections/2,
|
||||
random_clientid/1
|
||||
random_clientid/1,
|
||||
check_gateway_edition/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([
|
||||
|
@ -550,7 +578,20 @@ gateways([
|
|||
}}
|
||||
| More
|
||||
]) 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) ->
|
||||
[
|
||||
|
|
|
@ -96,10 +96,8 @@ t_gateways(_) ->
|
|||
ok.
|
||||
|
||||
t_gateway(_) ->
|
||||
{404, GwNotFoundReq1} = request(get, "/gateways/not_a_known_atom"),
|
||||
assert_not_found(GwNotFoundReq1),
|
||||
{404, GwNotFoundReq2} = request(get, "/gateways/undefined"),
|
||||
assert_not_found(GwNotFoundReq2),
|
||||
?assertMatch({400, #{code := <<"BAD_REQUEST">>}}, request(get, "/gateways/not_a_known_atom")),
|
||||
?assertMatch({400, #{code := <<"BAD_REQUEST">>}}, request(get, "/gateways/undefined")),
|
||||
{204, _} = request(put, "/gateways/stomp", #{}),
|
||||
{200, StompGw} = request(get, "/gateways/stomp"),
|
||||
assert_fields_exist(
|
||||
|
@ -110,7 +108,7 @@ t_gateway(_) ->
|
|||
{200, #{enable := true}} = request(get, "/gateways/stomp"),
|
||||
{204, _} = request(put, "/gateways/stomp", #{enable => false}),
|
||||
{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"}),
|
||||
ok.
|
||||
|
||||
|
@ -129,8 +127,14 @@ t_gateway_enable(_) ->
|
|||
{200, #{enable := NotEnable}} = request(get, "/gateways/stomp"),
|
||||
{204, _} = request(put, "/gateways/stomp/enable/" ++ atom_to_list(Enable), undefined),
|
||||
{200, #{enable := Enable}} = request(get, "/gateways/stomp"),
|
||||
{404, _} = request(put, "/gateways/undefined/enable/true", undefined),
|
||||
{404, _} = request(put, "/gateways/not_a_known_atom/enable/true", undefined),
|
||||
?assertMatch(
|
||||
{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),
|
||||
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_dashboard_rbac,
|
||||
emqx_dashboard_sso,
|
||||
emqx_audit
|
||||
emqx_audit,
|
||||
emqx_gateway_gbt32960
|
||||
],
|
||||
%% must always be of type `load'
|
||||
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_dashboard_rbac,
|
||||
:emqx_dashboard_sso,
|
||||
:emqx_audit
|
||||
:emqx_audit,
|
||||
:emqx_gateway_gbt32960
|
||||
])
|
||||
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_sso") -> false;
|
||||
is_community_umbrella_app("apps/emqx_audit") -> false;
|
||||
is_community_umbrella_app("apps/emqx_gateway_gbt32960") -> false;
|
||||
is_community_umbrella_app(_) -> true.
|
||||
|
||||
is_jq_supported() ->
|
||||
|
|
|
@ -37,8 +37,7 @@ gateway_name.desc:
|
|||
"""Gateway Name"""
|
||||
|
||||
gateway_name_in_qs.desc:
|
||||
"""Gateway Name.<br/>
|
||||
It's enum with `stomp`, `mqttsn`, `coap`, `lwm2m`, `exproto`"""
|
||||
"""Gateway Name"""
|
||||
|
||||
gateway_node_status.desc:
|
||||
"""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