diff --git a/apps/emqx/test/emqx_proper_types.erl b/apps/emqx/test/emqx_proper_types.erl index 0a66b3628..6c2ad56f9 100644 --- a/apps/emqx/test/emqx_proper_types.erl +++ b/apps/emqx/test/emqx_proper_types.erl @@ -511,13 +511,7 @@ peercert() -> conn_mod() -> oneof([ emqx_connection, - emqx_ws_connection, - emqx_coap_mqtt_adapter, - emqx_sn_gateway, - emqx_lwm2m_protocol, - emqx_gbt32960_conn, - emqx_jt808_connection, - emqx_tcp_connection + emqx_ws_connection ]). proto_name() -> diff --git a/apps/emqx/test/emqx_topic_index_SUITE.erl b/apps/emqx/test/emqx_topic_index_SUITE.erl index 9df9743f1..71e508306 100644 --- a/apps/emqx/test/emqx_topic_index_SUITE.erl +++ b/apps/emqx/test/emqx_topic_index_SUITE.erl @@ -209,9 +209,6 @@ t_match_fast_forward(Config) -> M:insert(<<"a/b/1/2/3/4/5/6/7/8/9/#">>, id1, <<>>, Tab), M:insert(<<"z/y/x/+/+">>, id2, <<>>, Tab), M:insert(<<"a/b/c/+">>, id3, <<>>, Tab), - % dbg:tracer(), - % dbg:p(all, c), - % dbg:tpl({ets, next, '_'}, x), ?assertEqual(id1, id(match(M, <<"a/b/1/2/3/4/5/6/7/8/9/0">>, Tab))), ?assertEqual([id1], [id(X) || X <- matches(M, <<"a/b/1/2/3/4/5/6/7/8/9/0">>, Tab)]). diff --git a/apps/emqx_gateway/src/emqx_gateway_api.erl b/apps/emqx_gateway/src/emqx_gateway_api.erl index 7195943a3..ae2533f97 100644 --- a/apps/emqx_gateway/src/emqx_gateway_api.erl +++ b/apps/emqx_gateway/src/emqx_gateway_api.erl @@ -381,7 +381,8 @@ fields(Gw) when Gw == lwm2m; Gw == exproto; Gw == gbt32960; - Gw == ocpp + Gw == ocpp; + Gw == jt808 -> [{name, mk(Gw, #{desc => ?DESC(gateway_name)})}] ++ convert_listener_struct(emqx_gateway_schema:gateway_schema(Gw)); @@ -392,7 +393,8 @@ fields(Gw) when Gw == update_lwm2m; Gw == update_exproto; Gw == update_gbt32960; - Gw == update_ocpp + Gw == update_ocpp; + Gw == update_jt808 -> "update_" ++ GwStr = atom_to_list(Gw), Gw1 = list_to_existing_atom(GwStr), diff --git a/apps/emqx_gateway/src/emqx_gateway_cm.erl b/apps/emqx_gateway/src/emqx_gateway_cm.erl index 7df3b2552..2c8d708df 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm.erl @@ -23,7 +23,7 @@ -behaviour(gen_server). --include("include/emqx_gateway.hrl"). +-include("emqx_gateway.hrl"). -include_lib("emqx/include/logger.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl"). diff --git a/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl b/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl index f5bede084..f7a72af5f 100644 --- a/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl +++ b/apps/emqx_gateway/src/emqx_gateway_cm_registry.erl @@ -17,7 +17,7 @@ %% @doc The gateway connection registry -module(emqx_gateway_cm_registry). --include("include/emqx_gateway.hrl"). +-include("emqx_gateway.hrl"). -behaviour(gen_server). diff --git a/apps/emqx_gateway/src/emqx_gateway_ctx.erl b/apps/emqx_gateway/src/emqx_gateway_ctx.erl index 11ad55d3e..6df1a8aae 100644 --- a/apps/emqx_gateway/src/emqx_gateway_ctx.erl +++ b/apps/emqx_gateway/src/emqx_gateway_ctx.erl @@ -17,7 +17,7 @@ %% @doc The gateway instance context -module(emqx_gateway_ctx). --include("include/emqx_gateway.hrl"). +-include("emqx_gateway.hrl"). %% @doc The running context for a Connection/Channel process. %% diff --git a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl index 0f7ff4ffc..345d94432 100644 --- a/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_gw_sup.erl @@ -23,7 +23,7 @@ -behaviour(supervisor). --include("include/emqx_gateway.hrl"). +-include("emqx_gateway.hrl"). -export([start_link/1]). diff --git a/apps/emqx_gateway/src/emqx_gateway_http.erl b/apps/emqx_gateway/src/emqx_gateway_http.erl index dc9e6bb49..677176acc 100644 --- a/apps/emqx_gateway/src/emqx_gateway_http.erl +++ b/apps/emqx_gateway/src/emqx_gateway_http.erl @@ -17,7 +17,7 @@ %% @doc Gateway Interface Module for HTTP-APIs -module(emqx_gateway_http). --include("include/emqx_gateway.hrl"). +-include("emqx_gateway.hrl"). -include_lib("emqx/include/logger.hrl"). -include_lib("emqx_auth/include/emqx_authn_chains.hrl"). diff --git a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl index 8dce8582d..2898298a7 100644 --- a/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_insta_sup.erl @@ -19,7 +19,7 @@ -behaviour(gen_server). --include("include/emqx_gateway.hrl"). +-include("emqx_gateway.hrl"). -include_lib("emqx/include/logger.hrl"). %% APIs diff --git a/apps/emqx_gateway/src/emqx_gateway_registry.erl b/apps/emqx_gateway/src/emqx_gateway_registry.erl index 50aad9445..20a3e1c42 100644 --- a/apps/emqx_gateway/src/emqx_gateway_registry.erl +++ b/apps/emqx_gateway/src/emqx_gateway_registry.erl @@ -17,7 +17,7 @@ %% @doc The Registry Centre of Gateway -module(emqx_gateway_registry). --include("include/emqx_gateway.hrl"). +-include("emqx_gateway.hrl"). -behaviour(gen_server). diff --git a/apps/emqx_gateway/src/emqx_gateway_sup.erl b/apps/emqx_gateway/src/emqx_gateway_sup.erl index 4e928bbf9..ffb7d9220 100644 --- a/apps/emqx_gateway/src/emqx_gateway_sup.erl +++ b/apps/emqx_gateway/src/emqx_gateway_sup.erl @@ -18,7 +18,7 @@ -behaviour(supervisor). --include("include/emqx_gateway.hrl"). +-include("emqx_gateway.hrl"). -export([start_link/0]). diff --git a/apps/emqx_gateway_coap/README.md b/apps/emqx_gateway_coap/README.md index 405366e89..653fd7433 100644 --- a/apps/emqx_gateway_coap/README.md +++ b/apps/emqx_gateway_coap/README.md @@ -5,7 +5,7 @@ with [Publish-Subscribe Broker for the CoAP](https://datatracker.ietf.org/doc/ht ## Quick Start -In EMQX 5.0, CoAP gateways can be configured and enabled through the Dashboard. +In EMQX 5.0, CoAP 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: diff --git a/apps/emqx_gateway_coap/rebar.config b/apps/emqx_gateway_coap/rebar.config index 3b070a72a..493ebe04f 100644 --- a/apps/emqx_gateway_coap/rebar.config +++ b/apps/emqx_gateway_coap/rebar.config @@ -1,3 +1,4 @@ +%% -*- mode: erlang -*- {erl_opts, [debug_info]}. {deps, [ {emqx, {path, "../emqx"}}, diff --git a/apps/emqx_gateway_coap/src/emqx_gateway_coap.app.src b/apps/emqx_gateway_coap/src/emqx_gateway_coap.app.src index 755532723..f9bc57722 100644 --- a/apps/emqx_gateway_coap/src/emqx_gateway_coap.app.src +++ b/apps/emqx_gateway_coap/src/emqx_gateway_coap.app.src @@ -1,3 +1,4 @@ +%% -*- mode: erlang -*- {application, emqx_gateway_coap, [ {description, "CoAP Gateway"}, {vsn, "0.1.4"}, diff --git a/apps/emqx_gateway_exproto/rebar.config b/apps/emqx_gateway_exproto/rebar.config index 473fa9b67..aafbe4e13 100644 --- a/apps/emqx_gateway_exproto/rebar.config +++ b/apps/emqx_gateway_exproto/rebar.config @@ -1,3 +1,4 @@ +%% -*- mode: erlang -*- {erl_opts, [debug_info]}. {deps, [ {emqx, {path, "../emqx"}}, diff --git a/apps/emqx_gateway_exproto/src/emqx_gateway_exproto.app.src b/apps/emqx_gateway_exproto/src/emqx_gateway_exproto.app.src index 09622763b..890435d59 100644 --- a/apps/emqx_gateway_exproto/src/emqx_gateway_exproto.app.src +++ b/apps/emqx_gateway_exproto/src/emqx_gateway_exproto.app.src @@ -1,3 +1,4 @@ +%% -*- mode: erlang -*- {application, emqx_gateway_exproto, [ {description, "ExProto Gateway"}, {vsn, "0.1.4"}, diff --git a/apps/emqx_gateway_gbt32960/include/emqx_gbt32960.hrl b/apps/emqx_gateway_gbt32960/include/emqx_gbt32960.hrl index ce1a3f135..2649f3f98 100644 --- a/apps/emqx_gateway_gbt32960/include/emqx_gbt32960.hrl +++ b/apps/emqx_gateway_gbt32960/include/emqx_gbt32960.hrl @@ -4,6 +4,8 @@ -record(frame, {cmd, ack, vin, encrypt, length, data, check, rawdata}). +-type frame() :: #frame{}. + -define(CMD(CmdType), #frame{ cmd = CmdType, ack = ?ACK_IS_CMD @@ -73,3 +75,6 @@ % 0x0A~0x2F: Customized data for Platform Exchange Protocol % 0x30~0x7F: Reserved % 0x80~0xFE: Customized by user + +-define(DEFAULT_MOUNTPOINT, <<"gbt32960/${clientid}/">>). +-define(DEFAULT_DOWNLINK_TOPIC, <<"dnstream">>). diff --git a/apps/emqx_gateway_gbt32960/rebar.config b/apps/emqx_gateway_gbt32960/rebar.config index cfeb0a195..456746d25 100644 --- a/apps/emqx_gateway_gbt32960/rebar.config +++ b/apps/emqx_gateway_gbt32960/rebar.config @@ -1,6 +1,7 @@ +%% -*- mode: erlang -*- {erl_opts, [debug_info]}. {deps, [ - {emqx, {path, "../../apps/emqx"}}, - {emqx_utils, {path, "../emqx_utils"}}, - {emqx_gateway, {path, "../../apps/emqx_gateway"}} + {emqx, {path, "../../apps/emqx"}}, + {emqx_utils, {path, "../emqx_utils"}}, + {emqx_gateway, {path, "../../apps/emqx_gateway"}} ]}. diff --git a/apps/emqx_gateway_gbt32960/src/emqx_gateway_gbt32960.app.src b/apps/emqx_gateway_gbt32960/src/emqx_gateway_gbt32960.app.src index ee6cf30d8..0ed2dca39 100644 --- a/apps/emqx_gateway_gbt32960/src/emqx_gateway_gbt32960.app.src +++ b/apps/emqx_gateway_gbt32960/src/emqx_gateway_gbt32960.app.src @@ -1,3 +1,4 @@ +%% -*- mode: erlang -*- {application, emqx_gateway_gbt32960, [ {description, "GBT32960 Gateway"}, {vsn, "0.1.0"}, diff --git a/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_channel.erl b/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_channel.erl index b5ac046ce..5cb65f104 100644 --- a/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_channel.erl +++ b/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_channel.erl @@ -67,7 +67,6 @@ | {close, Reason :: atom()}. -type replies() :: reply() | [reply()]. --type frame() :: emqx_gbt32960_frame:frame(). -define(TIMER_TABLE, #{ alive_timer => keepalive, @@ -75,8 +74,6 @@ }). -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}). @@ -203,7 +200,7 @@ setting_peercert_infos(Peercert, ClientInfo) -> %%-------------------------------------------------------------------- %% Handle incoming packet %%-------------------------------------------------------------------- --spec handle_in(emqx_gbt32960_frame:frame() | {frame_error, any()}, channel()) -> +-spec handle_in(frame() | {frame_error, any()}, channel()) -> {ok, channel()} | {ok, replies(), channel()} | {shutdown, Reason :: term(), channel()} @@ -703,14 +700,14 @@ upstreaming( 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_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">> + _ -> <<"upstream/transparent">> end, Topic = emqx_mountpoint:mount(Mountpoint, Suffix), Payload = to_json(Frame), @@ -718,7 +715,7 @@ transform(Frame = ?CMD(Cmd), Mountpoint) -> transform(Frame = #frame{ack = Ack}, Mountpoint) when ?IS_ACK_CODE(Ack) -> - Topic = emqx_mountpoint:mount(Mountpoint, <<"/upstream/response">>), + Topic = emqx_mountpoint:mount(Mountpoint, <<"upstream/response">>), Payload = to_json(Frame), {Topic, Payload}. diff --git a/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_frame.erl b/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_frame.erl index f43749a7c..f4b679711 100644 --- a/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_frame.erl +++ b/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_frame.erl @@ -62,7 +62,7 @@ serialize_opts() -> parse(Bin, State) -> case enter_parse(Bin, State) of {ok, Message, Rest} -> - {ok, Message, Rest, State#{parse => search_heading}}; + {ok, Message, Rest, State#{data => <<>>, phase => search_heading}}; {error, Error} -> {error, Error}; {more_data_follow, Partial} -> diff --git a/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_schema.erl b/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_schema.erl index abc1623f6..743c74e70 100644 --- a/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_schema.erl +++ b/apps/emqx_gateway_gbt32960/src/emqx_gbt32960_schema.erl @@ -4,11 +4,10 @@ -module(emqx_gbt32960_schema). +-include("emqx_gbt32960.hrl"). -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]). diff --git a/apps/emqx_gateway_gbt32960/test/emqx_gbt32960_SUITE.erl b/apps/emqx_gateway_gbt32960/test/emqx_gbt32960_SUITE.erl index 16d56e28a..56184fc5f 100644 --- a/apps/emqx_gateway_gbt32960/test/emqx_gbt32960_SUITE.erl +++ b/apps/emqx_gateway_gbt32960/test/emqx_gbt32960_SUITE.erl @@ -29,7 +29,8 @@ "}\n" >>). -all() -> emqx_common_test_helpers:all(?MODULE). +all() -> + emqx_common_test_helpers:all(?MODULE). init_per_suite(Config) -> application:load(emqx_gateway_gbt32960), diff --git a/apps/emqx_gateway_jt808/.gitignore b/apps/emqx_gateway_jt808/.gitignore new file mode 100644 index 000000000..c09296035 --- /dev/null +++ b/apps/emqx_gateway_jt808/.gitignore @@ -0,0 +1,27 @@ +.eunit +deps +*.o +*.beam +*.plt +erl_crash.dump +ebin/* +rel/example_project +.concrete/DEV_MODE +.rebar +.erlang.mk/ +emqx_connect_jt808.d +data/ +!data/app.config +.idea/ +*.iml +emqx_gateway_jt808.d +logs/ +cover/ +ct.coverdata +eunit.coverdata +test/ct.cover.spec +_build/ +etc/emqx_gateway_jt808.conf.rendered +*.swp +rebar.lock +.rebar3/ diff --git a/apps/emqx_gateway_jt808/BSL.txt b/apps/emqx_gateway_jt808/BSL.txt new file mode 100644 index 000000000..0acc0e696 --- /dev/null +++ b/apps/emqx_gateway_jt808/BSL.txt @@ -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. diff --git a/apps/emqx_gateway_jt808/README.md b/apps/emqx_gateway_jt808/README.md new file mode 100644 index 000000000..1eb296d34 --- /dev/null +++ b/apps/emqx_gateway_jt808/README.md @@ -0,0 +1,26 @@ +# emqx_jt808 + +The JT/T 808 protocol is designed for data interaction between vehicle-mounted satellite terminals and IoT platforms. + +The **JT/T 808 Gateway** in EMQX can accept JT/T 808 clients and translate their events +and messages into MQTT Publish messages. + +In the current implementation, it has the following limitations: +- Only supports JT/T 808-2013 protocol, JT/T 808-2019 is not supported yet. +- Based TCP/TLS transport. +- Third-party authentication/registration http service required. + +## Quick Start + +In EMQX 5.0, JT/T 808 gateway can be configured and enabled through the Dashboard. + +It can also be enabled via the HTTP API, and emqx.conf e.g, In emqx.conf: + +``` +``` + +> 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. + +More documentations: [JT/T 808 Gateway](https://www.emqx.io/docs/en/v5.0/gateway/jt808.html) diff --git a/apps/emqx_gateway_jt808/doc/Data_Exchange_Guide_CN.md b/apps/emqx_gateway_jt808/doc/Data_Exchange_Guide_CN.md new file mode 100644 index 000000000..d786369d2 --- /dev/null +++ b/apps/emqx_gateway_jt808/doc/Data_Exchange_Guide_CN.md @@ -0,0 +1,840 @@ +# emqx-jt808 + +JT/T 808 2013 协议接入网关 + +该文档定义了 Plugins **emqx_jt808** 和 **EMQX** 之间数据交换的格式 + +约定: +- Payload 采用 Json 格式进行组装 +- Json Key 采用全小写格式命名 + +Json 结构示例 + +## 终端到服务器 +```json +{ + "header" : { + "msg_id" : 1, + "encrypt": 0, + "len": VAL, + "phone": 13900000000, + "msg_sn": 0 + }, + "body": { + "seq": 1, + "id": 1, + "result": 0 + } +} +``` + +## 服务器到终端 +```json +{ + "header": { + "msg_id": 32769, + "encrypt": 0, + "phone": 13900000000, + "msg_sn": 0 + }, + "body": { + "seq": 1, + "id": 1, + "result": 0 + } +} +``` + +## 数据类型对照表 +| JT808 Defined Type | In Json Type | Comment | +|:------------------:|:------------:|:----------:| +| BYTE | integer | in decimal | +| WORD | integer | in decimal | +| DWORD | integer | in decimal | +| BYTE(n) | string | | +| BCD(n) | string | | +| STRING | string | | + +## 字段对照表 + +### 消息头字段对照表 + +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:------------------:| +| 消息 ID | msg_id | word | integer | +| 数据加密方式 | encrypt | word | integer | +| 终端手机号 | phone | bcd(6) | string | +| 消息流水号 | msg_sn | word | integer | + +| Optional Field | Json Key name | Value Type | Value Type in JSON | +|:--------------:|:-------------:|:----------:|:------------------:| +| 消息总包数 | frag_total | word | integer | +| 消息包序号 | frag_sn | word | integer | + +- 存在 `frag_total` 与 `frag_sn` 时表示消息体为长消息,进行分包处理 + +### 消息体字段对照表 + +- 终端通用应答 `"msg_id": 1` 0x0001 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------:|:------------------:| +| 应答流水号 | seq | word | integer | +| 应答 ID | id | word | integer | +| 结果 | result | byte | integer | + + +- 平台通用应答 `"msg_id": 32769` 0x8001 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------:|:------------------:| +| 应答流水号 | seq | word | integer | +| 应答 ID | id | word | integer | +| 结果 | result | byte | integer | + + +- 终端心跳 `"msg_id": 2` 0x0002 +空 Json + + +- 补传分包请求 `"msg_id": 32771` 0x8003 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------:|:-------------:|:--------------:|:------------------:| +| 原始消息流水号 | seq | word | integer | +| 重传包总数 | length | byte | integer | +| 重传包 ID 列表 | ids | byte(2*length) | list of integer | + + +- 终端注册 `"msg_id": 256` 0x0100 +| Field | Json Key name | Value Type | Value Type in Json | +|:---------:|:--------------:|:----------:|:------------------:| +| 省域 ID | province | word | integer | +| 市县域 ID | city | word | integer | +| 制造商 ID | manufacture | byte(5) | string | +| 终端型号 | model | byte(20) | string | +| 终端 ID | dev_id | byte(7) | string | +| 车牌颜色 | color | byte | integer | +| 车辆标识 | license_number | string | string | + + +- 终端注册应答 `"msg_id": 33024` 0x8100 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------:|:------------------:| +| 应答流水号 | seq | word | integer | +| 结果 | result | byte | integer | + +只有成功后才有此字段 +| Optional Field | Json Key name | Value Type | Value Type in JSON | +| 鉴权码 | auth_code | string | string | + + +- 终端注销 `"msg_id": 3` 0x0003 +空 Json + + +- 终端鉴权 `"msg_id": 258` 0x0102 +| Field | Json Key name | Value Type | Value Type in Json | +|:------:|:-------------:|:----------:|:------------------:| +| 鉴权码 | code | string | string | + + +- 设置终端参数 `"msg_id": 33027` 0x8103 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------:|:------------------------------------------------------:| +| 参数总数 | length | byte | integer | +| 参数项列表 | params | list | list of id and value. `[{"id":ID, "value": VAL}, ...]` | +| 参数项 | id | dword | integer | +| 参数值 | value | byte | integer | + +参数 ID 说明见协议规定. + + +- 查询终端参数 `"msg_id": 33028` 0x8104 +空 Json + + +- 查询指定终端参数 `"msg_id": 33030` 0x8106 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:--------------:|:--------------------------------:| +| 参数总数 | length | byte | integer | +| 参数 ID 列表 | ids | byte(2*length) | list of id. `[1, 2, 3, 4, ...]` | + +参数 ID 列表中元素为 integer + + +- 查询终端应答参数 `"msg_id": 260` 0x0104 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:------------------------------------------------------:| +| 应答流水号 | seq | word | integer | +| 应答参数个数 | length | byte | integer | +| 参数项列表 | params | list | list of id and value. `[{"id":ID, "value": VAL}, ...]` | +| 参数项 | id | dword | integer | +| 参数值 | value | byte | integer | + +参数 ID 说明见协议规定. + + +- 终端控制 `"msg_id": 33029 ` 0x8105 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------:|:-------------:|:----------:|:------------------:| +| 命令字 | command | byte | integer | +| 命令参数 | param | string | string | + + +- 查询终端属性 `"msg_id": 33031` 0x8107 +空 Json + + +- 查询终端属性应答 `"msg_id": 263` 0x0107 +| Field | Json Key name | Value Type | Value Type in Json | +|:-----------------:|:----------------:|:----------:|:------------------:| +| 终端类型 | type | word | integer | +| 制造商 ID | manufacture | byte(5) | string | +| 终端型号 | model | byte(20) | string | +| 终端 ID | id | byte(7) | string | +| 终端 SIM 卡 ICCID | iccid | byte(10) | string | +| 终端硬件版本号 | hardware_version | string | string | +| 终端硬件固件号 | firmware_version | string | string | +| GNSS 模块属性 | gnss_prop | byte | integer | +| 通信模块属性 | comm_prop | byte | integer | + +-- 终端硬件版本号长度、终端固件版本号长度,将被用于二进制报文解析,不向上暴露 + + +- 下发终端升级包 `"msg_id": 33032` 0x8108 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------:|:-------------:|:----------:|:----------------------:| +| 升级类型 | type | byte | integer | +| 制造商 ID | manufacturer | byte(5) | string | +| 版本号长度 | ver_len | byte | integer | +| 版本号 | version | string | string | +| 升级数据包长度 | fw_len | dword | integer | +| 升级数据包 | firmware | binary | string(base64 encoded) | + + +- 终端升级结果通知 `"msg_id": 264` 0x0108 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------:|:-------------:|:----------:|:------------------:| +| 升级类型 | type | byte | integer | +| 升级结果 | result | byte | integer | + + +- 位置信息汇报 `"msg_id": 512` 0x0200 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------------:|:-------------:|:----------:|:------------------:| +| 报警标志 | alarm | dword | integer | +| 状态 | status | dword | integer | +| 纬度 | latitude | dword | integer | +| 经度 | longitude | dword | integer | +| 高程 | altitude | word | integer | +| 速度 | speed | word | integer | +| 方向 | direction | word | integer | +| 时间 | time | bcd(6) | string | + +| Optional Field | Json Key name | Value Type | Value Type in JSON | +| 位置附加信息项列表 | extra | - | map | + +%% TODO: refine alarm mroe details + +位置附加信息项列表, 在 `extra` 中 +| Field (附加信息描述) | Json Key name | Value Type | Value Type in Json | +|:---------------------------------:|:---------------:|:----------:|:----------------------:| +| 里程 | mileage | dword | integer | +| 油量 | fuel_meter | word | integer | +| 行驶记录功能获取的速度 | speed | word | integer | +| 需要人工确认报警事件的 ID | alarm_id | word | integer | +| 超速报警附加信息(长度1或5) | overspeed_alarm | - | map | +| 进出区域/路线报警附加信息 | in_out_alarm | - | map | +| 路段行驶时间不足/过长报警附加信息 | path_time_alarm | - | map | +| 扩展车辆信号状态位 | 见状态位附表 | - | - | +| IO 状态位 | io_status | - | map | +| 模拟量 | analog | - | map | +| 无线通信网络信号强度 | rssi | byte | integer | +| GNSS 定位卫星数 | gnss_sat_num | byte | integer | +| 后续自定义信息长度 | custome | - | string(base64 encoded) | +| %% TODO 自定义区域 | | | | + +超速报警附加信息(长度1或5), 置于 map `overspeed_alarm` 内 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------:|:-------------:|:----------:|:------------------:| +| 位置类型 | type | byte | integer | + +| Optional Field | Json Key name | Value Type | Value Type in JSON | +| 区域或路段 ID | id | dword | integer | + + +进出区域/路线报警附加信息, 置于 map `in_out_alarm` 内 +| Field | Json Key name | Value Type | Value Type in Json | +|:-------------:|:-------------:|:----------:|:------------------:| +| 位置类型 | type | byte | integer | +| 区域或路段 ID | id | dword | integer | +| 方向 | direction | byte | integer | + +路段行驶时间不足/过长报警附加信息, 置于 map `path_time_alarm` 内 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:------------------:| +| 路段 ID | id | dword | integer | +| 路段行驶时间 | time | word | integer | +| 结果 | result | byte | integer | + +IO 状态位, 置于 map `io_status` 内 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:------------------:| +| 深度休眠状态 | deep_sleep | 1 bit | integer | +| 休眠状态 | sleep | 1 bit | integer | + +模拟量, 置于 map `analog` 内 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------:|:-------------:|:----------:|:------------------:| +| 模拟量 0 | ad0 | 16 bits | integer | +| 模拟量 1 | ad1 | 16 bits | integer | + +扩展车辆信号状态位, 置于 map `extra` 内 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:---------------:|:----------:|:------------------------------------------:| +| 信号 | signal | - 2 bits | map, `{"low_beam": VAL, "high_beam": VAL}` | +| 右转向灯信号 | right_turn | 1 bit | integer | +| 左转向灯信号 | left_turn | 1 bit | integer | +| 制动信号 | brake | 1 bit | integer | +| 倒档信号 | reverse | 1 bit | integer | +| 雾灯信号 | fog | 1 bit | integer | +| 示廓灯 | side_marker | 1 bit | integer | +| 喇叭状态 | horn | 1 bit | integer | +| 空调状态 | air_conditioner | 1 bit | integer | +| 空档信号 | neutral | 1 bit | integer | +| 缓速器工作 | retarder | 1 bit | integer | +| ABS 工作 | abs | 1 bit | integer | +| 加热器工作 | heater | 1 bit | integer | +| 离合器状态 | cluth | 1 bit | integer | + +信号状态, 置于 map `signal` 内 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------:|:------------------:| +| 近光灯信号 | low_beam | 1 bit | integer | +| 远光灯信号 | high_beam | 1 bit | integer | + +例 +example: +``` +{ + "header" : { + "msg_id" : 1, + "encrypt": 0, + "len": VAL, + "phone": 13900000000, + "msg_sn": 0 + }, + "body": { + "alarm": VAL, + "status": VAL, + "latitude": VAL, + "longitude": VAL, + "altitude": VAL, + "speed": VAL, + "direction": VAL, + "time": VAL, + "extra": { + "mileage": VAL, + "fuel_unit": VAL, + "speed": VAL, + "alarm_id": VAL, + "overspeed_alarm": { + "type": VAL, + "id": VAL + }, + "in_out_alarm": { + "type": VAL, + "id": VAL, + "direction": VAL + }, + "path_time_alarm": { + "id": VAL, + "time": VAL, + "result": VAL + }, + "signal": { + "low_beam": VAL, + "high_beam": VAL + }, + "right_turn": VAL, + "left_turn": VAL, + "break": VAL, + "reverse": VAL, + "fog": VAL, + "side_marker": VAL, + "horn": VAL, + "air_conditioner": VAL, + "neutral": VAL, + "retarder": VAL, + "abs": VAL, + "heater": VAL, + "cluth": VAL, + "io_status": { + "deep_sleep": VAL, + "sleep": VAL + }, + "analog": { + "ad0": VAL, + "ad1": VAL + } + } + } +} +``` + + +- 位置信息查询 `"msg_id": 33281` 0x8201 +空 Json + + +- 位置信息查询应答 `"msg_id": 513` 0x0201 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:------------------:| +| 应答流水号 | seq | word | integer | +| 位置信息汇报 | params | - | map | + + +- 临时位置跟踪控制 `"msg_id": 33282` 0x8202 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------:|:-------------:|:----------:|:------------------:| +| 时间间隔 | period | word | integer | +| 跟踪位置有效期 | expiry | dword | integer | + + +- 人工确认报警消息 `"msg_id": 33283` 0x8203 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------------:|:-------------:|:----------:|:------------------:| +| 报警消息流水号 | seq | word | integer | +| 人工确认报警类型 | type | dword | integer | + + +- 文本信息下发 `"msg_id": 33536` 0x8300 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------:|:-------------:|:----------:|:------------------:| +| 标志 | flag | byte | integer | +| 文本信息 | text | string | string | + + +- 事件设置 `"msg_id": 33537` 0x8301 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:-----------------------------------------------------------------:| +| 设置类型 | type | byte | integer | +| 设置总数 | length | byte | integer | +| 事件项列表 | events | list | list of event. `[{"id": ID, "length": LEN, "content": CON}, ...]` | +| 事件 ID | id | byte | integer | +| 事件内容长度 | length | byte | integer | +| 事件内容 | content | string | string | + + +- 事件报告 `"msg_id": 769` 0x0301 +| Field | Json Key name | Value Type | Value Type in Json | +|:-------:|:-------------:|------------|:------------------:| +| 事件 ID | id | byte | integer | + + +- 提问下发 `"msg_id": 33538` 0x8302 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:---------------------------------------------------------------:| +| 标志 | flag | byte | integer | +| 问题内容长度 | length | byte | integer | +| 问题 | question | string | string | +| 候选答案列表 | answers | list | list of answer. `[{"id": ID, "len": LEN, "answer": ANS}, ...]` | +| 答案 ID | id | byte | integer | +| 答案内容长度 | len | byte | integer | +| 答案内容 | answer | string | string | + +%% TODO: len -> length or other length -> len + +- 提问应答 `"msg_id": 770` 0x0302 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------:|:------------------:| +| 应答流水号 | seq | word | integer | +| 答案 ID | id | byte | integer | + + +- 信息点播菜单设置 `"msg_id": 33539` 0x8303 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:------------------:| +| 设置类型 | type | byte | integer | +| 信息项总数 | length | byte | integer | +| 信息项列表 | menus | list | list of menu | +| 信息类型 | type | byte | integer | +| 信息名称长度 | length | word | integer | +| 信息名称 | info | string | string | + + +- 信息点播/取消 `"msg_id": 771` 0x0303 +| Field | Json Key name | Value Type | Value Type in Json | +|:-------------:|:-------------:|:----------:|:------------------:| +| 信息类型 | id | byte | integer | +| 点拨/取消标志 | flag | byte | integer | + + +- 信息服务 `"msg_id": 33540` 0x8304 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------:|:-------------:|:----------:|:------------------:| +| 信息类型 | type | byte | integer | +| 信息长度 | length | word | integer | +| 信息内容 | info | string | string | + + +- 电话回拨 `"msg_id": 33792` 0x8400 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------:|:-------------:|:----------:|:------------------:| +| 标志 | type | byte | integer | +| 电话号码 | phone | string | string | + + +- `"msg_id": 33793` 0x8401 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------:|:------------------:| +| 设置类型 | type | byte | integer | +| 联系人总数 | length | byte | integer | +| 联系人项 | contacts | list | list of contact. | +| 标志 | type | byte | integer | +| 号码长度 | phone_len | byte | integer | +| 电话号码 | phone | string | string | +| 联系人长度 | name_len | byte | integer | +| 联系人 | name | string | string | + +联系人项示例 +`[{"type": TYPE, "phone_len", PH_LEN, "phone": PHONE, "name_len": NAME_LEN, "name": NAME}, ...]` + + +- `"msg_id": 34048` 0x8500 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------:|:-------------:|:----------:|:------------------:| +| 标志控制 | flag | byte | integer | + + +- `"msg_id": 1280` 0x0500 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------------:|:-------------:|:----------:|:------------------:| +| 应答流水号 | seq | word | integer | +| 位置信息汇报消息体 | location | map | map of location | + + +- `"msg_id": 34304` 0x8600 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:------------------:|:----------:|:------------------:| +| 设置属性 | type | byte | integer | +| 区域总数 | length | byte | integer | +| 区域项 | areas | list | list of area. | +| 区域 ID | id | dword | integer | +| 区域属性 | flag | dword | integer | +| 中心点纬度 | center_latitude | dword | integer | +| 中心点经度 | center_longitude | dword | integer | +| 半径 | radius | dword | integer | +| 起始时间 | start_time | string | string | +| 结束时间 | end_time | string | string | +| 最高速度 | max_speed | word | integer | +| 超速持续时间 | overspeed_duration | byte | integer | + +区域列表示例 +`[{"id": ID, + "flag": FLAG, + "center_latitude": CEN_LAT, + "center_longitude": CEN_LON, + "radius": RADIUS, + "start_time": START_TIME, + "end_time": END_TIME, + "max_speed", MAX_SPEED, + "overspeed_duration", OVERSPEED_DURATION + }, + ... + ]` + + +- 删除圆形区域 `"msg_id": 34305` 0x8601 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:------------------:| +| 区域数 | length | byte | integer | +| 区域 ID 列表 | ids | list | list of id. | +| 区域 ID 1~n | - | dword | integer | + +`[ID1, ID2, ...]` + + +- 设置矩形区域 `"msg_id": 34306` 0x8602 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:------------------:|:----------:|:------------------------:| +| 设置属性 | type | byte | integer | +| 区域总数 | length | byte | integer | +| 区域项 | areas | list | list of rectangle area. | +| 区域 ID | id | dword | integer | +| 区域属性 | flag | dword | integer | +| 左上点纬度 | lt_lat | dword | integer | +| 左上点经度 | lt_lng | dword | integer | +| 右下点纬度 | rb_lat | dword | integer | +| 右下点经度 | rb_lng | dword | integer | +| 起始时间 | start_time | string | string | +| 结束时间 | end_time | string | string | +| 最高速度 | max_speed | word | integer | +| 超速持续时间 | overspeed_duration | byte | integer | + + +- 删除矩形区域 `"msg_id": 34307` 0x8603 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:------------------:| +| 区域数 | length | byte | integer | +| 区域 ID 列表 | ids | list | list of id. | +| 区域 ID 1~n | - | dword | integer | + + +- 设置多边形区域 `"msg_id": 34308` 0x8604 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:------------------:|:----------:|:------------------:| +| 区域 ID | id | dword | integer | +| 区域属性 | flag | dword | integer | +| 起始时间 | start_time | string | string | +| 结束时间 | end_time | string | string | +| 最高速度 | max_speed | word | integer | +| 超速持续时间 | overspeed_duration | byte | integer | +| 区域总顶点数 | length | word | integer | +| 顶点项列表 | points | list | list of point. | +| 顶点纬度 | lat | dword | integer | +| 顶点经度 | lng | dword | integer | + + +- 删除多边形区域 `"msg_id": 34309` 0x8605 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:------------------:| +| 区域数 | length | byte | integer | +| 区域 ID 列表 | ids | list | list of id. | +| 区域 ID 1~n | - | dword | integer | + + +- 设置路线 `"msg_id": 34310` 0x8606 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------------:|:------------------:|:----------:|:------------------:| +| 路线 ID | id | dword | integer | +| 路线属性 | flag | word | integer | +| 起始时间 | start_time | string | string | +| 结束时间 | end_time | string | string | +| 路线总拐点数 | length | word | integer | +| 拐点项 | points | list | list of point. | +| 拐点 ID | point_id | dword | integer | +| 路段 ID | path_id | dword | integer | +| 拐点纬度 | point_lat | dword | integer | +| 拐点经度 | point_lng | dword | integer | +| 路段宽度 | width | byte | integer | +| 路段属性 | attrib | byte | integer | +| 路段行驶过长阈值 | passed | word | integer | +| 路段行驶不足阈值 | uncovered | word | integer | +| 路段最高速度 | max_speed | word | integer | +| 路段超速持续时间 | overspeed_duration | byte | integer | + + +- `"msg_id": 34311` 0x8607 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------:|:-------------:|:----------:|:------------------:| +| 路线数 | length | byte | integer | +| 路线列表 | ids | list | list of id | +| 路线 ID | - | dword | integer | + + +- 行驶记录数据采集命令 `"msg_id": 34560` 0x8700 +| Field | Json Key name | Value Type | Value Type in Json | +|:------:|:-------------:|:----------------------:|:------------------:| +| 命令字 | command | byte | integer | +| 数据块 | param | string(base64 encoded) | string | + + +- 行驶记录数据上传 `"msg_id": 1792` 0x0700 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------------------:|:------------------:| +| 应答流水号 | seq | word | integer | +| 命令字 | command | byte | integer | +| 数据块 | data | string(base64 encoded) | string | + + +- 行驶记录参数下传命令 `"msg_id": 34561` 0x8701 +| Field | Json Key name | Value Type | Value Type in Json | +|:------:|:-------------:|:----------------------:|:------------------:| +| 命令字 | command | byte | integer | +| 数据块 | param | string(base64 encoded) | string | + + +- 电子运单上报 `"msg_id": 1793` 0x0701 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------------------:|:------------------:| +| 电子运单长度 | length | dword | integer | +| 电子运单内容 | data | string(base64 encoded) | string | + + +- 上报驾驶员身份信息请求 `"msg_id": 34562` 0x8702 +空 Json + + +- 驾驶员身份信息采集上报 `"msg_id": 1794` 0x0702 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------:|:-------------:|:----------:|:------------------:| +| 状态 | status | byte | integer | +| 时间 | time | string | string | +| IC 卡读取结果 | ic_result | byte | integer | +| 驾驶员姓名 | driver_name | string | string | +| 从业资格证编码 | certificate | string | string | +| 发证机构名称 | organization | string | string | +| 证件有效期 | cert_expiry | string | string | + + +- 定位数据批量上传 `"msg_id": 1796` 0x0704 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------:|:-------------:|:----------:|:------------------:| +| 位置数据类型 | type | byte | integer | +| 数据项个数 | length | word | integer | +| 位置汇报数据项 | location | list | list of location | + + +- `"msg_id": 1797` 0x0705 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------------:|:-------------:|:----------:|:----------------------:| +| 数据项个数 | length | word | integer | +| CAN 总线数据接收时间 | time | bcd(5) | integer | +| CAN 总线数据项 | can_data | list | list of can data. | +| CAN 总线通道号 | channel | 1 bit | integer | +| 帧类型 | frame_type | 1 bit | integer | +| 数据采集方式 | data_method | 1 bit | integer | +| CAN 总线 ID | id | 29 bits | integer | +| CAN 数据 | data | binary | string(base64 encoded) | + + +- 多媒体时间信息上传 `"msg_id": 2048` 0x0800 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------:|:-------------:|:----------:|:------------------:| +| 多媒体数据 ID | id | dword | integer | +| 多媒体类型 | type | byte | integer | +| 多媒体编码格式 | format | byte | integer | +| 事件项编码 | event | byte | integer | +| 通道 ID | channel | byte | integer | + + +- 多媒体数据上传 `"msg_id": 2049` 0x0801 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------:|:-------------:|:----------:|:----------------------:| +| 多媒体 ID | id | dword | integer | +| 多媒体类型 | type | byte | integer | +| 多媒体编码格式 | format | byte | integer | +| 事件项编码 | event | byte | integer | +| 通道 ID | channel | byte | integer | +| 位置信息汇报 | location | byte(28) | map | +| 多媒体数据包 | multimedia | binary | string(base64 encoded) | + + + +- 多媒体数据上传应答 `"msg_id": 34816` 0x8800 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------:|:-------------:|:----------:|:------------------:| +| 多媒体 ID | mm_id | dword | integer | +| 重传包总数 | length | byte | integer | +| 重传包 ID 列表 | retx_ids | list | list of retry IDs | + + +- 摄像头立即拍摄命令 `"msg_id": 34817` 0x8801 +| Field | Json Key name | Value Type | Value Type in Json | +|:-----------------:|:-------------:|:----------:|:------------------:| +| 通道 ID | channel_id | byte | integer | +| 拍摄命令 | command | word | integer | +| 拍照间隔/录像时间 | period | word | integer | +| 保存标志 | save | byte | integer | +| 分辨率 | resolution | byte | integer | +| 图像/视频质量 | quality | byte | integer | +| 亮度 | bright | byte | integer | +| 对比度 | contrast | byte | integer | +| 饱和度 | saturate | byte | integer | +| 色度 | chromaticity | byte | integer | + + +- 摄像头立即拍摄应答 `"msg_id": 2053` 0x0805 +| Field | Json Key name | Value Type | Value Type in Json | +|:--------------:|:-------------:|:--------------:|:------------------:| +| 应答流水号 | seq | word | integer | +| 结果 | result | byte | integer | +| 多媒体 ID 个数 | length | word | integer | +| 多媒体 ID 列表 | ids | byte(4*length) | integer | + + +- 存储多媒体数据检索 `"msg_id": 34818` 0x8802 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------:|:------------------:| +| 多媒体类型 | | byte | | +| 通道 ID | | byte | | +| 事件项编码 | | byte | | +| 起始时间 | | string | | +| 结束时间 | | string | | + + +- 存储多媒体数据检索应答 `"msg_id": 2050` 0x0802 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------------:|:-------------:|:----------:|:---------------------:| +| 应答流水号 | seq | word | integer | +| 多媒体数据项总数 | length | word | integer | +| 检索项 | result | list | list of search result | +| 多媒体 ID | id | dword | integer | +| 多媒体类型 | type | byte | integer | +| 通道 ID | channel | byte | integer | +| 事件项编码 | event | byte | integer | +| 位置信息汇报 | location | byte(28) | map | + + +- 存储多媒体数据上传命令 `"msg_id": 34819` 0x8803 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------:|:------------------:| +| 多媒体类型 | type | byte | integer | +| 通道 ID | channel | byte | integer | +| 事件项编码 | event | byte | integer | +| 起始时间 | start_time | string | string | +| 结束时间 | end_time | string | string | +| 删除标志 | delete | byte | integer | + + +- 录音开始命令 `"msg_id": 34820` 0x8804 +| Field | Json Key name | Value Type | Value Type in Json | +|:----------:|:-------------:|:----------:|:------------------:| +| 录音命令 | command | byte | integer | +| 录音时间 | time | word | integer | +| 保存标志 | save | byte | integer | +| 音频采样率 | rate | byte | integer | + + +- 单条存储多媒体j叔叔检索上传命令 `"msg_id": 34821` 0x8805 +| Field | Json Key name | Value Type | Value Type in Json | +|:---------:|:-------------:|:----------:|:------------------:| +| 多媒体 ID | id | dword | integer | +| 删除标志 | flag | byte | integer | + + +- 数据下行透传 `"msg_id": 35072` 0x8900 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:----------------------:| +| 透传消息类型 | type | byte | integer | +| 透传消息内容 | data | binary | string(base64 encoded) | + + +- 数据上行透传 `"msg_id": 2304` 0x0900 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:----------------------:| +| 透传消息类型 | type | byte | integer | +| 透传消息内容 | data | binary | string(base64 encoded) | + + +- 数据压缩上报 `"msg_id": 2305` 0x0901 +| Field | Json Key name | Value Type | Value Type in Json | +|:------------:|:-------------:|:----------:|:----------------------:| +| 压缩消息长度 | length | dword | integer | +| 压缩消息体 | data | binary | string(base64 encoded) | + + +- 平台 RSA 公钥 `"msg_id": 35328` 0x8A00 +| Field | Json Key name | Value Type | Value Type in Json | +|:-----:|:-------------:|:----------:|:----------------------:| +| e | e | dword | integer | +| n | n | byte(128) | string(base64 encoded) | + + +- 终端 RSA 公钥 `"msg_id": 2560` 0x0A00 +| Field | Json Key name | Value Type | Value Type in Json | +|:-----:|:-------------:|:----------:|:----------------------:| +| e | e | dword | integer | +| n | n | byte(128) | string(base64 encoded) | + +- 0x8F00 ~ 0x8FFF +- 0x0F00 ~ 0x0FFF diff --git a/apps/emqx_gateway_jt808/include/emqx_jt808.hrl b/apps/emqx_gateway_jt808/include/emqx_jt808.hrl new file mode 100644 index 000000000..a7422a4fe --- /dev/null +++ b/apps/emqx_gateway_jt808/include/emqx_jt808.hrl @@ -0,0 +1,195 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2017-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-ifndef(EMQX_JT808_HRL). +-define(EMQX_JT808_HRL, true). + +%%-------------------------------------------------------------------- +%% Message Ids +%%-------------------------------------------------------------------- + +%% Message Ids of client to server +-define(MC_GENERAL_RESPONSE, 16#0001). +-define(MC_HEARTBEAT, 16#0002). +-define(MC_REGISTER, 16#0100). +-define(MC_DEREGISTER, 16#0003). +-define(MC_AUTH, 16#0102). +-define(MC_QUERY_PARAM_ACK, 16#0104). +-define(MC_QUERY_ATTRIB_ACK, 16#0107). +-define(MC_OTA_ACK, 16#0108). +-define(MC_LOCATION_REPORT, 16#0200). +-define(MC_QUERY_LOCATION_ACK, 16#0201). +-define(MC_EVENT_REPORT, 16#0301). +-define(MC_QUESTION_ACK, 16#0302). +-define(MC_INFO_REQ_CANCEL, 16#0303). +-define(MC_VEHICLE_CTRL_ACK, 16#0500). +-define(MC_DRIVE_RECORD_REPORT, 16#0700). +-define(MC_WAYBILL_REPORT, 16#0701). +-define(MC_DRIVER_ID_REPORT, 16#0702). +-define(MC_BULK_LOCATION_REPORT, 16#0704). +-define(MC_CAN_BUS_REPORT, 16#0705). +-define(MC_MULTIMEDIA_EVENT_REPORT, 16#0800). +-define(MC_MULTIMEDIA_DATA_REPORT, 16#0801). +-define(MC_CAMERA_SHOT_ACK, 16#0805). +-define(MC_MM_DATA_SEARCH_ACK, 16#0802). +-define(MC_SEND_TRANSPARENT_DATA, 16#0900). +-define(MC_SEND_ZIP_DATA, 16#0901). +-define(MC_RSA_KEY, 16#0A00). + +%% Message Ids of server to client +-define(MS_GENERAL_RESPONSE, 16#8001). +-define(MS_REQUEST_FRAGMENT, 16#8003). +-define(MS_REGISTER_ACK, 16#8100). +-define(MS_SET_CLIENT_PARAM, 16#8103). +-define(MS_QUERY_CLIENT_ALL_PARAM, 16#8104). +-define(MS_CLIENT_CONTROL, 16#8105). +-define(MS_QUERY_CLIENT_PARAM, 16#8106). +-define(MS_QUERY_CLIENT_ATTRIB, 16#8107). +-define(MS_OTA, 16#8108). +-define(MS_QUERY_LOCATION, 16#8201). +-define(MS_TRACE_LOCATION, 16#8202). +-define(MS_CONFIRM_ALARM, 16#8203). +-define(MS_SEND_TEXT, 16#8300). +-define(MS_SET_EVENT, 16#8301). +-define(MS_SEND_QUESTION, 16#8302). +-define(MS_SET_MENU, 16#8303). +-define(MS_INFO_CONTENT, 16#8304). +-define(MS_PHONE_CALLBACK, 16#8400). +-define(MS_SET_PHONE_NUMBER, 16#8401). +-define(MS_VEHICLE_CONTROL, 16#8500). +-define(MS_SET_CIRCLE_AREA, 16#8600). +-define(MS_DEL_CIRCLE_AREA, 16#8601). +-define(MS_SET_RECT_AREA, 16#8602). +-define(MS_DEL_RECT_AREA, 16#8603). +-define(MS_SET_POLY_AREA, 16#8604). +-define(MS_DEL_POLY_AREA, 16#8605). +-define(MS_SET_PATH, 16#8606). +-define(MS_DEL_PATH, 16#8607). +-define(MS_DRIVE_RECORD_CAPTURE, 16#8700). +-define(MS_DRIVE_REC_PARAM_SEND, 16#8701). +-define(MS_REQ_DRIVER_ID, 16#8702). +-define(MS_MULTIMEDIA_DATA_ACK, 16#8800). +-define(MS_CAMERA_SHOT, 16#8801). +-define(MS_MM_DATA_SEARCH, 16#8802). +-define(MS_MM_DATA_UPLOAD, 16#8803). +-define(MS_VOICE_RECORD, 16#8804). +-define(MS_SINGLE_MM_DATA_CTRL, 16#8805). +-define(MS_SEND_TRANSPARENT_DATA, 16#8900). +-define(MS_RSA_KEY, 16#8A00). + +%% Client Params +-define(CP_HEARTBEAT_DURATION, 16#0001). +-define(CP_TCP_TIMEOUT, 16#0002). +-define(CP_TCP_RETX, 16#0003). +-define(CP_UDP_TIMEOUT, 16#0004). +-define(CP_UDP_RETX, 16#0005). +-define(CP_SMS_TIMEOUT, 16#0006). +-define(CP_SMS_RETX, 16#0007). +-define(CP_SERVER_APN, 16#0010). +-define(CP_DIAL_USERNAME, 16#0011). +-define(CP_DIAL_PASSWORD, 16#0012). +-define(CP_SERVER_ADDRESS, 16#0013). +-define(CP_BACKUP_SERVER_APN, 16#0014). +-define(CP_BACKUP_DIAL_USERNAME, 16#0015). +-define(CP_BACKUP_DIAL_PASSWORD, 16#0016). +-define(CP_BACKUP_SERVER_ADDRESS, 16#0017). +-define(CP_SERVER_TCP_PORT, 16#0018). +-define(CP_SERVER_UDP_PORT, 16#0019). +-define(CP_IC_CARD_SERVER_ADDRESS, 16#001A). +-define(CP_IC_CARD_SERVER_TCP_PORT, 16#001B). +-define(CP_IC_CARD_SERVER_UDP_PORT, 16#001C). +-define(CP_IC_CARD_BACKUP_SERVER_ADDRESS, 16#001D). +-define(CP_POS_REPORT_POLICY, 16#0020). +-define(CP_POS_REPORT_CONTROL, 16#0021). +-define(CP_DRIVER_NLOGIN_REPORT_INTERVAL, 16#0022). +-define(CP_REPORT_INTERVAL_DURING_SLEEP, 16#0027). +-define(CP_EMERGENCY_ALARM_REPORT_INTERVAL, 16#0028). +-define(CP_DEFAULT_REPORT_INTERVAL, 16#0029). +-define(CP_DEFAULT_DISTANCE_INTERVAL, 16#002C). +-define(CP_DRIVER_NLOGIN_DISTANCE_INTERVAL, 16#002D). +-define(CP_DISTANCE_INTERVAL_DURING_SLEEP, 16#002E). +-define(CP_EMERGENCY_ALARM_DISTANCE_INTERVAL, 16#002F). +-define(CP_SET_TURN_ANGLE, 16#0030). +-define(CP_EFENCE_RADIUS, 16#0031). +-define(CP_MONITOR_PHONE, 16#0040). +-define(CP_RESETING_PHONE, 16#0041). +-define(CP_RECOVERY_PHONE, 16#0042). +-define(CP_SMS_MONITOR_PHONE, 16#0043). +-define(CP_EMERGENCY_SMS_PHONE, 16#0044). +-define(CP_ACCEPT_CALL_POLICY, 16#0045). +-define(CP_MAX_CALL_DURATION, 16#0046). +-define(CP_MAX_CALL_DURATION_OF_MONTH, 16#0047). +-define(CP_SPY_PHONE, 16#0048). +-define(CP_PRIVILEGE_SMS_PHONE, 16#0049). +-define(CP_ALARM_MASK, 16#0050). +-define(CP_ALARM_SEND_SMS_MASK, 16#0051). +-define(CP_ALARM_CAMERA_SHOT_MASK, 16#0052). +-define(CP_ALARM_PICTURE_SAVE_MASK, 16#0053). +-define(CP_ALARM_KEY_MASK, 16#0054). +-define(CP_MAX_SPEED, 16#0055). +-define(CP_OVERSPEED_ELAPSED, 16#0056). +-define(CP_CONT_DRIVE_THRESHOLD, 16#0057). +-define(CP_ACC_DRIVE_TIME_ONE_DAY_THRESHOLD, 16#0058). +-define(CP_MIN_BREAK_TIME, 16#0059). +-define(CP_MAX_PARK_TIME, 16#005A). +-define(CP_OVERSPEED_ALARM_DELTA, 16#005B). +-define(CP_DRIVER_FATIGUE_ALARM_DELTA, 16#005C). +-define(CP_SET_CRASH_ALARM_PARAM, 16#005D). +-define(CP_SET_ROLLOVER_PARAM, 16#005E). +-define(CP_TIME_CONTROLED_CAMERA, 16#0064). +-define(CP_DISTANCE_CONTROLED_CAMERA, 16#0065). +-define(CP_PICTURE_QUALITY, 16#0070). +-define(CP_PICTURE_BRIGHTNESS, 16#0071). +-define(CP_PICTURE_CONTRAST, 16#0072). +-define(CP_PICTURE_SATURATE, 16#0073). +-define(CP_PICTURE_CHROMATICITY, 16#0074). +-define(CP_ODOMETER, 16#0080). +-define(CP_REGISTERED_PROVINCE, 16#0081). +-define(CP_REGISTERED_CITY, 16#0082). +-define(CP_VEHICLE_LICENSE_NUMBER, 16#0083). +-define(CP_VEHICLE_LICENSE_PLATE_COLOR, 16#0084). +-define(CP_GNSS_MODE, 16#0090). +-define(CP_GNSS_BAUDRATE, 16#0091). +-define(CP_GNSS_OUTPUT_RATE, 16#0092). +-define(CP_GNSS_SAMPLING_RATE, 16#0093). +-define(CP_GNSS_UPLOAD_MODE, 16#0094). +-define(CP_GNSS_UPLOAD_UNIT, 16#0095). +-define(CP_CAN_BUS_CH1_SAMPLING, 16#0100). +-define(CP_CAN_BUS_CH1_UPLOAD, 16#0101). +-define(CP_CAN_BUS_CH2_SAMPLING, 16#0102). +-define(CP_CAN_BUS_CH2_UPLOAD, 16#0103). +-define(CP_SET_CAN_BUS_ID_PARAM, 16#0110). + +%% Extra info types in Position Report +-define(CP_POS_EXTRA_MILEAGE, 16#01). +-define(CP_POS_EXTRA_FUEL_METER, 16#02). +-define(CP_POS_EXTRA_SPEED, 16#03). +-define(CP_POS_EXTRA_ALARM_ID, 16#04). +-define(CP_POS_EXTRA_OVERSPEED_ALARM, 16#11). +-define(CP_POS_EXTRA_IN_OUT_ALARM, 16#12). +-define(CP_POS_EXTRA_PATH_TIME_ALARM, 16#13). +-define(CP_POS_EXTRA_EXPANDED_SIGNAL, 16#25). +-define(CP_POS_EXTRA_IO_STATUS, 16#2A). +-define(CP_POS_EXTRA_ANALOG, 16#2B). +-define(CP_POS_EXTRA_RSSI, 16#30). +-define(CP_POS_EXTRA_GNSS_SAT_NUM, 16#31). +-define(CP_POS_EXTRA_CUSTOME, 16#E0). + +%% Default Configs +-define(DEFAULT_MOUNTPOINT, <<"jt808/${clientid}/">>). +-define(DEFAULT_UP_TOPIC, <>). +-define(DEFAULT_DN_TOPIC, <>). + +%% Supported placeholders +-define(PH_CLIENTID, <<"${clientid}">>). +-define(PH_PHONE, <<"${phone}">>). + +-record(auth, { + allow_anonymous :: boolean(), + registry :: emqx_schema:url() | undefined, + authentication :: emqx_schema:url() | undefined +}). +-type auth() :: #auth{}. + +-endif. diff --git a/apps/emqx_gateway_jt808/rebar.config b/apps/emqx_gateway_jt808/rebar.config new file mode 100644 index 000000000..456746d25 --- /dev/null +++ b/apps/emqx_gateway_jt808/rebar.config @@ -0,0 +1,7 @@ +%% -*- mode: erlang -*- +{erl_opts, [debug_info]}. +{deps, [ + {emqx, {path, "../../apps/emqx"}}, + {emqx_utils, {path, "../emqx_utils"}}, + {emqx_gateway, {path, "../../apps/emqx_gateway"}} +]}. diff --git a/apps/emqx_gateway_jt808/src/emqx_gateway_jt808.app.src b/apps/emqx_gateway_jt808/src/emqx_gateway_jt808.app.src new file mode 100644 index 000000000..3d64366ff --- /dev/null +++ b/apps/emqx_gateway_jt808/src/emqx_gateway_jt808.app.src @@ -0,0 +1,11 @@ +%% -*- mode: erlang -*- +{application, emqx_gateway_jt808, [ + {description, "JT/T 808 Gateway"}, + {vsn, "0.0.1"}, + {registered, []}, + {applications, [kernel, stdlib, emqx, emqx_gateway]}, + {env, []}, + {modules, []}, + {licenses, ["BSL"]}, + {links, []} +]}. diff --git a/apps/emqx_gateway_jt808/src/emqx_gateway_jt808.erl b/apps/emqx_gateway_jt808/src/emqx_gateway_jt808.erl new file mode 100644 index 000000000..90c77dc36 --- /dev/null +++ b/apps/emqx_gateway_jt808/src/emqx_gateway_jt808.erl @@ -0,0 +1,99 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +%% @doc The JT/T 808 Gateway implement + +-module(emqx_gateway_jt808). + +-include_lib("emqx/include/logger.hrl"). +-include_lib("emqx_gateway/include/emqx_gateway.hrl"). + +%% define a gateway named jt808 +-gateway(#{ + name => jt808, + callback_module => ?MODULE, + config_schema_module => emqx_jt808_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_jt808_frame, + chann_mod => emqx_jt808_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). diff --git a/apps/emqx_gateway_jt808/src/emqx_jt808_auth.erl b/apps/emqx_gateway_jt808/src/emqx_jt808_auth.erl new file mode 100644 index 000000000..aeba537e8 --- /dev/null +++ b/apps/emqx_gateway_jt808/src/emqx_jt808_auth.erl @@ -0,0 +1,101 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_jt808_auth). + +-include("emqx_jt808.hrl"). + +-export([ + init/1, + register/2, + authenticate/2 +]). + +-export_type([auth/0]). + +init(#{allow_anonymous := true}) -> + #auth{registry = undefined, authentication = undefined, allow_anonymous = true}; +init(#{registry := Reg, authentication := Auth, allow_anonymous := Anonymous}) -> + #auth{registry = Reg, authentication = Auth, allow_anonymous = Anonymous}. + +register(_RegFrame, #auth{registry = undefined, allow_anonymous = true}) -> + {ok, anonymous}; +register(_RegFrame, #auth{registry = undefined, allow_anonymous = false}) -> + {error, registry_server_not_existed}; +register(RegFrame, #auth{registry = RegUrl}) -> + #{ + <<"header">> := #{<<"phone">> := Phone}, + <<"body">> := FBody + } = RegFrame, + Params = maps:merge(FBody, #{<<"phone">> => Phone}), + case request(RegUrl, Params) of + {ok, 200, Body} -> + case emqx_utils_json:safe_decode(Body, [return_maps]) of + {ok, #{<<"code">> := 0, <<"authcode">> := Authcode}} -> + {ok, Authcode}; + {ok, #{<<"code">> := Code}} -> + {error, Code}; + _ -> + {error, {invailed_resp, Body}} + end; + {ok, Code, Body} -> + {error, {unknown_resp, Code, Body}}; + {error, Reason} -> + {error, Reason} + end. + +authenticate(_AuthFrame, #auth{authentication = undefined, allow_anonymous = true}) -> + {ok, #{auth_result => true, anonymous => true}}; +authenticate(_AuthFrame, #auth{authentication = undefined, allow_anonymous = false}) -> + {ok, #{auth_result => false, anonymous => false}}; +authenticate(AuthFrame, #auth{authentication = AuthUrl}) -> + #{ + <<"header">> := #{<<"phone">> := Phone}, + <<"body">> := #{<<"code">> := AuthCode} + } = AuthFrame, + case request(AuthUrl, #{<<"code">> => AuthCode, <<"phone">> => Phone}) of + {ok, 200, _} -> + {ok, #{auth_result => true, anonymous => false}}; + {ok, _, _} -> + {ok, #{auth_result => false, anonymous => false}}; + {error, Reason} -> + {error, Reason} + end. + +%%-------------------------------------------------------------------- +%% Inernal functions +%%-------------------------------------------------------------------- + +request(Url, Params) -> + RetryOpts = #{times => 3, interval => 1000, backoff => 2.0}, + Req = {Url, [], "application/json", emqx_utils_json:encode(Params)}, + reply(request_(post, Req, [{autoredirect, true}], [{body_format, binary}], RetryOpts)). + +request_( + Method, + Req, + HTTPOpts, + Opts, + RetryOpts = #{ + times := Times, + interval := Interval, + backoff := BackOff + } +) -> + case httpc:request(Method, Req, HTTPOpts, Opts) of + {error, _Reason} when Times > 0 -> + timer:sleep(trunc(Interval)), + RetryOpts1 = RetryOpts#{ + times := Times - 1, + interval := Interval * BackOff + }, + request_(Method, Req, HTTPOpts, Opts, RetryOpts1); + Other -> + Other + end. + +reply({ok, {{_, Code, _}, _Headers, Body}}) -> + {ok, Code, Body}; +reply({error, Error}) -> + {error, Error}. diff --git a/apps/emqx_gateway_jt808/src/emqx_jt808_channel.erl b/apps/emqx_gateway_jt808/src/emqx_jt808_channel.erl new file mode 100644 index 000000000..5708efe55 --- /dev/null +++ b/apps/emqx_gateway_jt808/src/emqx_jt808_channel.erl @@ -0,0 +1,951 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_jt808_channel). +-behaviour(emqx_gateway_channel). + +-include("emqx_jt808.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"). + +%% behaviour callbacks +-export([ + info/1, + info/2, + stats/1 +]). + +-export([ + init/2, + handle_in/2, + handle_deliver/2, + handle_timeout/3, + handle_call/3, + handle_cast/2, + handle_info/2 +]). + +-export([ + terminate/2 +]). + +-record(channel, { + %% Context + ctx :: emqx_gateway_ctx:context(), + %% ConnInfo + conninfo :: emqx_types:conninfo(), + %% ClientInfo + clientinfo :: emqx_types:clientinfo(), + %% Session + session :: undefined | map(), + %% Conn State + conn_state :: conn_state(), + %% Timers + timers :: #{atom() => undefined | disabled | reference()}, + %% AuthCode + authcode :: undefined | anonymous | binary(), + %% Keepalive + keepalive, + %% Msg SN + msg_sn, + %% Down Topic + dn_topic, + %% Up Topic + up_topic, + %% Auth + auth :: emqx_jt808_auth:auth(), + %% Inflight + inflight :: emqx_inflight:inflight(), + mqueue :: queue:queue(), + max_mqueue_len, + rsa_key, + retx_interval, + retx_max_times +}). + +-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()]. + +-define(TIMER_TABLE, #{ + alive_timer => keepalive, + retry_timer => retry_delivery +}). + +-define(INFO_KEYS, [ctx, conninfo, zone, clientid, clientinfo, session, conn_state, authcode]). + +-define(RETX_INTERVAL, 8000). +-define(RETX_MAX_TIME, 5). + +-define(DEFAULT_KEEPALIVE, 300). + +-define(MSG(MsgId), #{<<"header">> := #{<<"msg_id">> := MsgId}}). + +-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(authcode, #channel{authcode = AuthCode}) -> + AuthCode. + +stats(_Channel) -> + []. + +%%-------------------------------------------------------------------- +%% Init the Channel +%%-------------------------------------------------------------------- + +-spec init(emqx_types:conninfo(), map()) -> channel(). +init( + ConnInfo = #{ + peername := {PeerHost, _Port}, + sockname := {_Host, SockPort} + }, + Options = #{ + ctx := Ctx, + message_queue_len := MessageQueueLen, + proto := ProtoConf + } +) -> + % 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, + ClientInfo = setting_peercert_infos( + Peercert, + #{ + zone => default, + listener => ListenerId, + protocol => jt808, + peerhost => PeerHost, + sockport => SockPort, + clientid => undefined, + username => undefined, + is_bridge => false, + is_superuser => false, + mountpoint => Mountpoint + } + ), + + #channel{ + ctx = Ctx, + conninfo = ConnInfo, + clientinfo = ClientInfo, + session = undefined, + conn_state = idle, + timers = #{}, + authcode = undefined, + keepalive = maps:get(keepalive, Options, ?DEFAULT_KEEPALIVE), + msg_sn = 0, + % TODO: init rsa_key from user input + dn_topic = maps:get(dn_topic, ProtoConf, ?DEFAULT_DN_TOPIC), + up_topic = maps:get(up_topic, ProtoConf, ?DEFAULT_UP_TOPIC), + auth = emqx_jt808_auth:init(ProtoConf), + inflight = emqx_inflight:new(128), + mqueue = queue:new(), + max_mqueue_len = MessageQueueLen, + rsa_key = [0, <<0:1024>>], + retx_interval = maps:get(retry_interval, Options, ?RETX_INTERVAL), + retx_max_times = maps:get(max_retry_times, Options, ?RETX_MAX_TIME) + }. + +setting_peercert_infos(NoSSL, ClientInfo) when + NoSSL =:= nossl; + NoSSL =:= undefined +-> + ClientInfo; +setting_peercert_infos(Peercert, ClientInfo) -> + DN = esockd_peercert:subject(Peercert), + CN = esockd_peercert:common_name(Peercert), + ClientInfo#{dn => DN, cn => CN}. + +%%-------------------------------------------------------------------- +%% Handle incoming packet +%%-------------------------------------------------------------------- + +-spec handle_in(emqx_jt808_frame:frame() | {frame_error, any()}, channel()) -> + {ok, channel()} + | {ok, replies(), channel()} + | {shutdown, Reason :: term(), channel()} + | {shutdown, Reason :: term(), replies(), channel()}. +handle_in(Frame = ?MSG(MType), Channel = #channel{conn_state = ConnState}) when + ConnState /= connected, MType =:= ?MC_REGISTER; + ConnState /= connected, MType =:= ?MC_AUTH +-> + ?SLOG(debug, #{msg => "recv_frame", frame => Frame}), + do_handle_in(Frame, Channel#channel{conn_state = connecting}); +handle_in(Frame, Channel = #channel{conn_state = connected}) -> + ?SLOG(debug, #{msg => "recv_frame", frame => Frame}), + do_handle_in(Frame, Channel); +handle_in(Frame, Channel) -> + ?SLOG(error, #{msg => "unexpected_frame", frame => Frame}), + {stop, unexpected_frame, Channel}. + +%% @private +do_handle_in(Frame = ?MSG(?MC_GENERAL_RESPONSE), Channel = #channel{inflight = Inflight}) -> + #{<<"body">> := #{<<"seq">> := Seq, <<"id">> := Id}} = Frame, + NewInflight = ack_msg(?MC_GENERAL_RESPONSE, {Id, Seq}, Inflight), + {ok, Channel#channel{inflight = NewInflight}}; +do_handle_in(Frame = ?MSG(?MC_REGISTER), Channel0) -> + #{<<"header">> := #{<<"msg_sn">> := MsgSn}} = Frame, + case emqx_jt808_auth:register(Frame, Channel0#channel.auth) of + {ok, Authcode} -> + Channel = enrich_clientinfo( + Frame, enrich_conninfo(Frame, Channel0#channel{authcode = Authcode}) + ), + handle_out({?MS_REGISTER_ACK, 0}, MsgSn, Channel); + {error, Reason} -> + ?SLOG(error, #{msg => "register_failed", reason => Reason}), + ResCode = + case is_integer(Reason) of + true -> Reason; + false -> 1 + end, + handle_out({?MS_REGISTER_ACK, ResCode}, MsgSn, Channel0) + end; +do_handle_in(Frame = ?MSG(?MC_AUTH), Channel0) -> + #{<<"header">> := #{<<"msg_sn">> := MsgSn}} = Frame, + Channel = + #channel{clientinfo = #{clientid := ClientId}} = + enrich_clientinfo(Frame, enrich_conninfo(Frame, Channel0)), + authack( + case authenticate(Frame, Channel0) of + true -> + NChannel = prepare_adapter_topic(ensure_connected(Channel)), + emqx_logger:set_metadata_clientid(ClientId), + %% Auto subscribe downlink topics + autosubcribe(NChannel), + _ = start_keepalive(?DEFAULT_KEEPALIVE, NChannel), + %% 0: Successful + {0, MsgSn, NChannel}; + false -> + ?SLOG(error, #{msg => "authenticated_failed"}), + %% 1: Failure + {1, MsgSn, Channel} + end + ); +do_handle_in(Frame = ?MSG(?MC_HEARTBEAT), Channel) -> + handle_out({?MS_GENERAL_RESPONSE, 0, ?MC_HEARTBEAT}, msgsn(Frame), Channel); +do_handle_in(?MSG(?MC_RSA_KEY), Channel = #channel{rsa_key = [E, N]}) -> + Response = #{ + <<"header">> => build_frame_header(?MS_RSA_KEY, Channel), + <<"body">> => #{<<"e">> => E, <<"n">> => N} + }, + % TODO: how to use client's RSA key? + {ok, [{outgoing, Response}], state_inc_sn(Channel)}; +do_handle_in(?MSG(?MC_MULTIMEDIA_DATA_REPORT), Channel = #channel{rsa_key = [_E, _N]}) -> + Response = #{ + <<"header">> => build_frame_header(?MS_MULTIMEDIA_DATA_ACK, Channel), + <<"body">> => #{} + }, + % TODO: how to fill ? + {ok, [{outgoing, Response}], state_inc_sn(Channel)}; +do_handle_in( + Frame = ?MSG(?MC_DRIVER_ID_REPORT), + Channel = #channel{ + up_topic = Topic, + inflight = Inflight + } +) -> + {MsgId, MsgSn} = msgidsn(Frame), + _ = do_publish(Topic, Frame), + case is_driver_id_req_exist(Channel) of + % this is an device passive command + false -> + handle_out({?MS_GENERAL_RESPONSE, 0, MsgId}, MsgSn, Channel); + % this is a response to MS_REQ_DRIVER_ID(0x8702) + true -> + {ok, Channel#channel{inflight = ack_msg(?MC_DRIVER_ID_REPORT, none, Inflight)}} + end; +do_handle_in(?MSG(?MC_DEREGISTER), Channel) -> + {stop, normal, Channel}; +do_handle_in(Frame = #{}, Channel = #channel{up_topic = Topic, inflight = Inflight}) -> + {MsgId, MsgSn} = msgidsn(Frame), + _ = do_publish(Topic, Frame), + case is_general_response_needed(MsgId) of + % these frames device passive request + true -> + handle_out({?MS_GENERAL_RESPONSE, 0, MsgId}, MsgSn, Channel); + % these frames are response to server's request + false -> + {ok, Channel#channel{inflight = ack_msg(MsgId, seq(Frame), Inflight)}} + end; +do_handle_in(Frame, Channel) -> + ?SLOG(error, #{msg => "ignore_unknown_frame", frame => Frame}), + {ok, Channel}. + +do_publish(Topic, Frame) -> + ?SLOG(debug, #{msg => "publish_msg", to_topic => Topic, farme => Frame}), + emqx:publish(emqx_message:make(jt808, ?QOS_1, Topic, emqx_utils_json:encode(Frame))). + +%%-------------------------------------------------------------------- +%% 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 = #{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, 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, Channel) -> + lists:filtermap( + fun(#message{payload = Payload}) -> + case emqx_utils_json:safe_decode(Payload, [return_maps]) of + {ok, Map} -> + MsgId = msgid(Map), + NewHeader = build_frame_header(MsgId, Channel), + Frame = maps:put(<<"header">>, NewHeader, Map), + {true, Frame}; + {error, Reason} -> + log(error, #{msg => "json_decode_error", reason => Reason}, Channel), + false + end + end, + Messages + ). + +authack( + {Code, MsgSn, + Channel = #channel{ + conninfo = ConnInfo, + clientinfo = ClientInfo + }} +) -> + Code == 0 andalso emqx_hooks:run('client.connected', [ClientInfo, ConnInfo]), + handle_out({?MS_GENERAL_RESPONSE, Code, ?MC_AUTH}, MsgSn, Channel). + +handle_out({?MS_GENERAL_RESPONSE, Result, InMsgId}, MsgSn, Channel) -> + Frame = #{ + <<"header">> => build_frame_header(?MS_GENERAL_RESPONSE, Channel), + <<"body">> => #{<<"seq">> => MsgSn, <<"result">> => Result, <<"id">> => InMsgId} + }, + {ok, [{outgoing, Frame}], state_inc_sn(Channel)}; +handle_out({?MS_REGISTER_ACK, 0}, MsgSn, Channel = #channel{authcode = Authcode0}) -> + Authcode = + case Authcode0 == anonymous of + true -> <<>>; + false -> Authcode0 + end, + Frame = #{ + <<"header">> => build_frame_header(?MS_REGISTER_ACK, Channel), + <<"body">> => #{<<"seq">> => MsgSn, <<"result">> => 0, <<"auth_code">> => Authcode} + }, + {ok, [{outgoing, Frame}], state_inc_sn(Channel)}; +handle_out({?MS_REGISTER_ACK, ResCode}, MsgSn, Channel) -> + Frame = #{ + <<"header">> => build_frame_header(?MS_REGISTER_ACK, Channel), + <<"body">> => #{<<"seq">> => MsgSn, <<"result">> => ResCode} + }, + {ok, [{outgoing, Frame}], state_inc_sn(Channel)}. + +%%-------------------------------------------------------------------- +%% 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(), emqx_jt808_frame: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 = RetxInterval} +) -> + case emqx_inflight:is_empty(Inflight) of + true -> + {ok, clean_timer(retry_timer, Channel)}; + false -> + Frames = lists:sort(sortfun(), emqx_inflight:to_list(Inflight)), + {Outgoings, NInflight} = retry_delivery( + Frames, erlang:system_time(millisecond), RetxInterval, Inflight, [] + ), + {Outgoings2, NChannel} = dispatch_frame(Channel#channel{inflight = NInflight}), + {ok, [{outgoing, Outgoings ++ Outgoings2}], reset_timer(retry_timer, NChannel)} + end. + +sortfun() -> + fun({_, {_, _, Ts1}}, {_, {_, _, Ts2}}) -> Ts1 < Ts2 end. + +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. + +dispatch_frame( + Channel = #channel{ + msg_sn = TxMsgSn, + 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( + set_msg_ack(msgid(Frame), TxMsgSn), + {Frame, RetxMax, erlang:system_time(millisecond)}, + Inflight + ), + NChannel = Channel#channel{mqueue = NewQueue, inflight = NewInflight}, + {[Frame], ensure_timer(retry_timer, NChannel)} + end. + +%%-------------------------------------------------------------------- +%% 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{clientinfo = #{clientid := undefined}}) -> + ok; +terminate(_Reason, #channel{conn_state = disconnected}) -> + ok; +terminate(Reason, #channel{clientinfo = ClientInfo, conninfo = ConnInfo}) -> + ?SLOG(info, #{msg => "connection_shutdown", reason => Reason}), + NConnInfo = ConnInfo#{disconnected_at => erlang:system_time(millisecond)}, + ok = emqx_hooks:run('client.disconnected', [ClientInfo, Reason, NConnInfo]). + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +maybe_fix_mountpoint(ClientInfo = #{mountpoint := undefined}) -> + ClientInfo; +maybe_fix_mountpoint(ClientInfo = #{mountpoint := Mountpoint}) -> + %% TODO: Enrich the variable replacement???? + %% i.e: ${ClientInfo.auth_result.productKey} + Mountpoint1 = emqx_mountpoint:replvar(Mountpoint, ClientInfo), + ClientInfo#{mountpoint := Mountpoint1}. + +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 + }. + +%% 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}. + +ack_msg(MsgId, KeyParam, Inflight) -> + Key = get_msg_ack(MsgId, KeyParam), + case emqx_inflight:contain(Key, Inflight) of + true -> emqx_inflight:delete(Key, Inflight); + false -> Inflight + end. + +set_msg_ack(?MS_SET_CLIENT_PARAM, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SET_CLIENT_PARAM, MsgSn}}; +set_msg_ack(?MS_QUERY_CLIENT_ALL_PARAM, MsgSn) -> + {?MC_QUERY_PARAM_ACK, MsgSn}; +set_msg_ack(?MS_QUERY_CLIENT_PARAM, MsgSn) -> + {?MC_QUERY_PARAM_ACK, MsgSn}; +set_msg_ack(?MS_CLIENT_CONTROL, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_CLIENT_CONTROL, MsgSn}}; +set_msg_ack(?MS_QUERY_CLIENT_ATTRIB, _MsgSn) -> + {?MC_QUERY_ATTRIB_ACK, none}; +set_msg_ack(?MS_OTA, _MsgSn) -> + {?MC_OTA_ACK, none}; +set_msg_ack(?MS_QUERY_LOCATION, MsgSn) -> + {?MC_QUERY_LOCATION_ACK, MsgSn}; +set_msg_ack(?MS_TRACE_LOCATION, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_TRACE_LOCATION, MsgSn}}; +set_msg_ack(?MS_CONFIRM_ALARM, _MsgSn) -> + % TODO: how to ack this message? + {}; +set_msg_ack(?MS_SEND_TEXT, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SEND_TEXT, MsgSn}}; +set_msg_ack(?MS_SET_EVENT, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SET_EVENT, MsgSn}}; +set_msg_ack(?MS_SEND_QUESTION, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SEND_QUESTION, MsgSn}}; +set_msg_ack(?MS_SET_MENU, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SET_MENU, MsgSn}}; +set_msg_ack(?MS_INFO_CONTENT, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_INFO_CONTENT, MsgSn}}; +set_msg_ack(?MS_PHONE_CALLBACK, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_PHONE_CALLBACK, MsgSn}}; +set_msg_ack(?MS_SET_PHONE_NUMBER, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SET_PHONE_NUMBER, MsgSn}}; +set_msg_ack(?MS_VEHICLE_CONTROL, MsgSn) -> + {?MC_VEHICLE_CTRL_ACK, MsgSn}; +set_msg_ack(?MS_SET_CIRCLE_AREA, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SET_CIRCLE_AREA, MsgSn}}; +set_msg_ack(?MS_DEL_CIRCLE_AREA, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_DEL_CIRCLE_AREA, MsgSn}}; +set_msg_ack(?MS_SET_RECT_AREA, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SET_RECT_AREA, MsgSn}}; +set_msg_ack(?MS_DEL_RECT_AREA, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_DEL_RECT_AREA, MsgSn}}; +set_msg_ack(?MS_SET_POLY_AREA, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SET_POLY_AREA, MsgSn}}; +set_msg_ack(?MS_DEL_POLY_AREA, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_DEL_POLY_AREA, MsgSn}}; +set_msg_ack(?MS_SET_PATH, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SET_PATH, MsgSn}}; +set_msg_ack(?MS_DEL_PATH, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_DEL_PATH, MsgSn}}; +set_msg_ack(?MS_DRIVE_RECORD_CAPTURE, MsgSn) -> + {?MC_DRIVE_RECORD_REPORT, MsgSn}; +set_msg_ack(?MS_DRIVE_REC_PARAM_SEND, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_DRIVE_REC_PARAM_SEND, MsgSn}}; +set_msg_ack(?MS_REQ_DRIVER_ID, _MsgSn) -> + {?MC_DRIVER_ID_REPORT, none}; +set_msg_ack(?MS_CAMERA_SHOT, MsgSn) -> + % TODO: spec has two conflicted statement about this ack + % section 7.9.3 requires general ack + % section 8.55 requires 0x0805 + {?MC_CAMERA_SHOT_ACK, MsgSn}; +set_msg_ack(?MS_MM_DATA_SEARCH, MsgSn) -> + {?MC_MM_DATA_SEARCH_ACK, MsgSn}; +set_msg_ack(?MS_MM_DATA_UPLOAD, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_MM_DATA_UPLOAD, MsgSn}}; +set_msg_ack(?MS_VOICE_RECORD, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_VOICE_RECORD, MsgSn}}; +set_msg_ack(?MS_SINGLE_MM_DATA_CTRL, MsgSn) -> + % TODO: right? + {?MC_MM_DATA_SEARCH_ACK, MsgSn}; +set_msg_ack(?MS_SEND_TRANSPARENT_DATA, MsgSn) -> + {?MC_GENERAL_RESPONSE, {?MS_SEND_TRANSPARENT_DATA, MsgSn}}; +set_msg_ack(MsgId, Param) -> + error({invalid_message_type, MsgId, Param}). + +get_msg_ack(?MC_GENERAL_RESPONSE, {MsgId, MsgSn}) -> + {?MC_GENERAL_RESPONSE, {MsgId, MsgSn}}; +get_msg_ack(?MC_QUERY_PARAM_ACK, MsgSn) -> + {?MC_QUERY_PARAM_ACK, MsgSn}; +get_msg_ack(?MC_QUERY_ATTRIB_ACK, _MsgSn) -> + {?MC_QUERY_ATTRIB_ACK, none}; +get_msg_ack(?MC_OTA_ACK, _MsgSn) -> + {?MC_OTA_ACK, none}; +get_msg_ack(?MC_QUERY_LOCATION_ACK, MsgSn) -> + {?MC_QUERY_LOCATION_ACK, MsgSn}; +get_msg_ack(?MC_QUESTION_ACK, MsgSn) -> + {?MC_QUESTION_ACK, MsgSn}; +get_msg_ack(?MC_VEHICLE_CTRL_ACK, MsgSn) -> + {?MC_VEHICLE_CTRL_ACK, MsgSn}; +get_msg_ack(?MC_DRIVE_RECORD_REPORT, MsgSn) -> + {?MC_DRIVE_RECORD_REPORT, MsgSn}; +get_msg_ack(?MC_CAMERA_SHOT_ACK, MsgSn) -> + {?MC_CAMERA_SHOT_ACK, MsgSn}; +get_msg_ack(?MC_MM_DATA_SEARCH_ACK, MsgSn) -> + {?MC_MM_DATA_SEARCH_ACK, MsgSn}; +get_msg_ack(?MC_DRIVER_ID_REPORT, _MsgSn) -> + {?MC_DRIVER_ID_REPORT, none}; +get_msg_ack(MsgId, MsgSn) -> + error({invalid_message_type, MsgId, MsgSn}). + +build_frame_header(MsgId, #channel{clientinfo = #{phone := Phone}, msg_sn = TxMsgSn}) -> + build_frame_header(MsgId, 0, Phone, TxMsgSn). + +build_frame_header(MsgId, Encrypt, Phone, TxMsgSn) -> + #{ + <<"msg_id">> => MsgId, + <<"encrypt">> => Encrypt, + <<"phone">> => Phone, + <<"msg_sn">> => TxMsgSn + }. + +seq(#{<<"body">> := #{<<"seq">> := MsgSn}}) -> MsgSn; +seq(#{}) -> 0. + +msgsn(#{<<"header">> := #{<<"msg_sn">> := MsgSn}}) -> MsgSn. + +msgid(#{<<"header">> := #{<<"msg_id">> := MsgId}}) -> MsgId. + +msgidsn(#{ + <<"header">> := #{ + <<"msg_id">> := MsgId, + <<"msg_sn">> := MsgSn + } +}) -> + {MsgId, MsgSn}. + +state_inc_sn(Channel = #channel{msg_sn = Sn}) -> + Channel#channel{msg_sn = next_msg_sn(Sn)}. + +next_msg_sn(16#FFFF) -> 0; +next_msg_sn(Sn) -> Sn + 1. + +is_general_response_needed(?MC_EVENT_REPORT) -> true; +is_general_response_needed(?MC_LOCATION_REPORT) -> true; +is_general_response_needed(?MC_INFO_REQ_CANCEL) -> true; +is_general_response_needed(?MC_WAYBILL_REPORT) -> true; +is_general_response_needed(?MC_BULK_LOCATION_REPORT) -> true; +is_general_response_needed(?MC_CAN_BUS_REPORT) -> true; +is_general_response_needed(?MC_MULTIMEDIA_EVENT_REPORT) -> true; +is_general_response_needed(?MC_SEND_TRANSPARENT_DATA) -> true; +is_general_response_needed(?MC_SEND_ZIP_DATA) -> true; +is_general_response_needed(_) -> false. + +is_driver_id_req_exist(#channel{inflight = Inflight}) -> + % if there is a MS_REQ_DRIVER_ID (0x8702) command in re-tx queue + Key = get_msg_ack(?MC_DRIVER_ID_REPORT, none), + emqx_inflight:contain(Key, Inflight). + +authenticate(_AuthFrame, #channel{authcode = anonymous}) -> + true; +authenticate(AuthFrame, #channel{authcode = undefined, auth = Auth}) -> + %% Try request authentication server + case emqx_jt808_auth:authenticate(AuthFrame, Auth) of + {ok, #{auth_result := IsAuth}} -> + IsAuth; + {error, Reason} -> + ?SLOG(error, #{msg => "request_auth_server_failed", reason => Reason}), + false + end; +authenticate( + #{<<"body">> := #{<<"code">> := InCode}}, + #channel{authcode = Authcode} +) -> + InCode == Authcode. + +enrich_conninfo( + #{<<"header">> := #{<<"phone">> := Phone}}, + Channel = #channel{conninfo = ConnInfo} +) -> + NConnInfo = ConnInfo#{ + proto_name => <<"jt808">>, + proto_ver => <<"2013">>, + clean_start => true, + clientid => Phone, + username => undefined, + conn_props => #{}, + connected => true, + connected_at => erlang:system_time(millisecond), + keepalive => ?DEFAULT_KEEPALIVE, + receive_maximum => 0, + expiry_interval => 0 + }, + Channel#channel{conninfo = NConnInfo}. + +%% Register +enrich_clientinfo( + #{ + <<"header">> := #{<<"phone">> := Phone}, + <<"body">> := #{ + <<"manufacturer">> := Manu, + <<"dev_id">> := DevId + } + }, + Channel = #channel{clientinfo = ClientInfo} +) -> + NClientInfo = maybe_fix_mountpoint(ClientInfo#{ + phone => Phone, + clientid => Phone, + manufacturer => Manu, + terminal_id => DevId + }), + Channel#channel{clientinfo = NClientInfo}; +%% Auth +enrich_clientinfo( + #{<<"header">> := #{<<"phone">> := Phone}}, + Channel = #channel{clientinfo = ClientInfo} +) -> + NClientInfo = ClientInfo#{ + phone => Phone, + clientid => Phone + }, + Channel#channel{clientinfo = NClientInfo}. + +prepare_adapter_topic(Channel = #channel{up_topic = UpTopic, dn_topic = DnTopic}) -> + Channel#channel{ + up_topic = replvar(UpTopic, Channel), + dn_topic = replvar(DnTopic, Channel) + }. + +replvar(undefined, _Channel) -> + undefined; +replvar(Topic, #channel{clientinfo = #{clientid := ClientId, phone := Phone}}) when + is_binary(Topic) +-> + do_replvar(Topic, #{clientid => ClientId, phone => Phone}). + +do_replvar(Topic, Vars) -> + ClientID = maps:get(clientid, Vars, undefined), + Phone = maps:get(phone, Vars, undefined), + List = [ + {?PH_CLIENTID, ClientID}, + {?PH_PHONE, Phone} + ], + lists:foldl(fun feed_var/2, Topic, List). + +feed_var({_PH, undefined}, Topic) -> + Topic; +feed_var({PH, Value}, Topic) -> + emqx_topic:feed_var(PH, Value, Topic). + +autosubcribe(#channel{dn_topic = Topic}) when + Topic == undefined; + Topic == "" +-> + ok; +autosubcribe(#channel{ + clientinfo = + ClientInfo = + #{clientid := ClientId}, + dn_topic = Topic +}) -> + SubOpts = #{rap => 0, nl => 0, qos => 0, rh => 0}, + emqx:subscribe(Topic, ClientId, SubOpts), + ok = emqx_hooks:run('session.subscribed', [ClientInfo, Topic, SubOpts#{is_new => true}]). + +start_keepalive(Secs, _Channel) when Secs > 0 -> + self() ! {keepalive, start, round(Secs) * 1000}. + +run_hooks(Ctx, Name, Args) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name), + emqx_hooks:run(Name, Args). + +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)). + +metrics_inc(Name, #channel{ctx = Ctx}, Oct) -> + emqx_gateway_ctx:metrics_inc(Ctx, Name, Oct). + +log(Level, Meta, #channel{clientinfo = #{clientid := ClientId, username := Username}} = _Channel) -> + ?SLOG(Level, Meta#{clientid => ClientId, username => Username}). + +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). diff --git a/apps/emqx_gateway_jt808/src/emqx_jt808_frame.erl b/apps/emqx_gateway_jt808/src/emqx_jt808_frame.erl new file mode 100644 index 000000000..02422235b --- /dev/null +++ b/apps/emqx_gateway_jt808/src/emqx_jt808_frame.erl @@ -0,0 +1,1105 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_jt808_frame). + +-behaviour(emqx_gateway_frame). + +-include("emqx_jt808.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). + +-define(NO_FRAGMENT, 0). +-define(HAS_FRAGMENT, 1). + +-type frame() :: map(). + +-type phase() :: searching_head_hex7e | {escaping_hex7d, binary()}. + +-type parser_state() :: #{ + data => binary(), + phase => phase() +}. + +-export_type([frame/0]). + +%%-------------------------------------------------------------------- +%% Callback APIs +%%-------------------------------------------------------------------- + +-spec initial_parse_state(map()) -> parser_state(). +initial_parse_state(_) -> + #{data => <<>>, phase => searching_head_hex7e}. + +-spec serialize_opts() -> emqx_gateway_frame:serialize_options(). +serialize_opts() -> + #{}. + +-spec parse(binary(), parser_state()) -> + emqx_gateway_frame:parse_result(). + +parse(Bin, State) -> + do_parse(Bin, State). + +serialize_pkt(Frame, _Opts) -> + serialize(Frame). + +format(Msg) -> + io_lib:format("~p", [Msg]). + +type(_) -> + jt808. + +is_message(#{}) -> + true; +is_message(_) -> + false. + +%%-------------------------------------------------------------------- +%% Parse Message +%%-------------------------------------------------------------------- +do_parse(Packet, State) -> + escape_head_hex7e(Packet, State). + +escape_head_hex7e(<<16#7e, Rest/binary>>, State = #{phase := searching_head_hex7e}) -> + %% 0x7e is start of a valid message + escape_frame(Rest, State); +escape_head_hex7e(<<_C, Rest/binary>>, State = #{phase := searching_head_hex7e}) -> + %% discard char other than 0x7e which is the start flag + escape_head_hex7e(Rest, State); +escape_head_hex7e(<<16#02, Rest/binary>>, State = #{data := Acc, phase := escaping_hex7d}) -> + %% corner case: 0x7d has been received in the end of last frame segment + escape_frame(Rest, State#{data => <>}); +escape_head_hex7e(<<16#01, Rest/binary>>, State = #{data := Acc, phase := escaping_hex7d}) -> + %% corner case: 0x7d has been received in the end of last frame segment + escape_frame(Rest, State#{data => <>}); +escape_head_hex7e(Rest, State = #{data := _Acc, phase := escaping_hex7d}) -> + %% continue parsing to escape 0x7d + escape_frame(Rest, State). + +escape_frame(Rest, State = #{data := Acc}) -> + case do_escape_frame(Rest, Acc) of + {ok, Msg, NRest} -> + {ok, parse_message(Msg), NRest, State#{data => <<>>, phase => searching_head_hex7e}}; + {error, _E} = Err -> + Err; + {more_data_follow, NRest} -> + {more, #{data => NRest, phase => escaping_hex7d}} + end. + +do_escape_frame(<<16#7d, 16#02, Rest/binary>>, Acc) -> + do_escape_frame(Rest, <>); +do_escape_frame(<<16#7d, 16#01, Rest/binary>>, Acc) -> + do_escape_frame(Rest, <>); +do_escape_frame(<<16#7d, _Other:8, _Rest/binary>>, _Acc) -> + %% only 0x02 and 0x01 is allowed to follow 0x7d + {error, invalid_message}; +do_escape_frame(<<16#7d>>, Acc) -> + %% corner case: last byte of the frame segment is 0x7d, + %% 0x01 or 0x02 is expected in next frame segment + {more_data_follow, Acc}; +do_escape_frame(<<16#7e, _Rest/binary>>, <<>>) -> + %% empty message + {error, invalid_message}; +do_escape_frame(<<16#7e, Rest/binary>>, Acc) -> + %% end of a normal message + case check(Acc) of + {error, _} = Err -> + Err; + Msg -> + {ok, Msg, Rest} + end; +do_escape_frame(<>, Acc) -> + do_escape_frame(Rest, <>); +do_escape_frame(<<>>, Acc) -> + {more_data_follow, Acc}. + +parse_message(Binary) -> + case parse_message_header(Binary) of + {ok, Header = #{<<"msg_id">> := MsgId}, RestBinary} -> + #{<<"header">> => Header, <<"body">> => parse_message_body(MsgId, RestBinary)}; + invalid_message -> + {error, invalid_message} + end. + +parse_message_header( + <> +) -> + {ok, + #{ + <<"msg_id">> => MsgId, + <<"encrypt">> => Encypt, + <<"len">> => Length, + <<"phone">> => from_bcd(PhoneBCD, []), + <<"msg_sn">> => MsgSn + }, + Rest}; +parse_message_header( + <> +) -> + {ok, + #{ + <<"msg_id">> => MsgId, + <<"encrypt">> => Encypt, + <<"len">> => Length, + <<"phone">> => from_bcd(PhoneBCD, []), + <<"msg_sn">> => MsgSn, + <<"frag_total">> => FragTotal, + <<"frag_sn">> => FragSeq + }, + Rest}; +parse_message_header(_) -> + invalid_message. + +parse_message_body(?MC_GENERAL_RESPONSE, <>) -> + #{<<"seq">> => Seq, <<"id">> => Id, <<"result">> => Result}; +parse_message_body(?MC_HEARTBEAT, <<>>) -> + #{}; +parse_message_body( + ?MC_REGISTER, + <> +) -> + #{ + <<"province">> => Province, + <<"city">> => City, + <<"manufacturer">> => Manufacturer, + <<"model">> => Model, + <<"dev_id">> => DevId, + <<"color">> => Color, + <<"license_number">> => LicNumber + }; +parse_message_body(?MC_DEREGISTER, <<>>) -> + #{}; +parse_message_body(?MC_AUTH, Binary) -> + #{<<"code">> => Binary}; +parse_message_body(?MC_QUERY_PARAM_ACK, <>) -> + {Length, Params} = parse_client_params(Rest), + #{<<"seq">> => Seq, <<"length">> => Length, <<"params">> => Params}; +parse_message_body( + ?MC_QUERY_ATTRIB_ACK, + <> +) -> + <> = Rest, + <> = Rest2, + #{ + <<"type">> => Type, + <<"manufacturer">> => Manufacturer, + <<"model">> => Model, + <<"id">> => Id, + <<"iccid">> => from_bcd(ICCID, []), + <<"hardware_version">> => HV, + <<"firmware_version">> => FV, + <<"gnss_prop">> => GNSSProp, + <<"comm_prop">> => CommProp + }; +parse_message_body(?MC_OTA_ACK, <>) -> + #{<<"type">> => Type, <<"result">> => Result}; +parse_message_body(?MC_LOCATION_REPORT, Binary) -> + parse_location_report(Binary); +parse_message_body(?MC_QUERY_LOCATION_ACK, <>) -> + Params = parse_location_report(Rest), + #{<<"seq">> => Seq, <<"params">> => Params}; +parse_message_body(?MC_EVENT_REPORT, <>) -> + #{<<"id">> => Id}; +parse_message_body(?MC_QUESTION_ACK, <>) -> + #{<<"seq">> => Seq, <<"id">> => Id}; +parse_message_body(?MC_INFO_REQ_CANCEL, <>) -> + #{<<"id">> => Id, <<"flag">> => Flag}; +parse_message_body(?MC_VEHICLE_CTRL_ACK, <>) -> + #{<<"seq">> => Seq, <<"location">> => parse_location_report(Location)}; +parse_message_body(?MC_DRIVE_RECORD_REPORT, <>) -> + #{<<"seq">> => Seq, <<"command">> => Command, <<"data">> => base64:encode(Data)}; +parse_message_body(?MC_WAYBILL_REPORT, <>) -> + #{<<"length">> => Length, <<"data">> => base64:encode(Data)}; +parse_message_body( + ?MC_DRIVER_ID_REPORT, + <> +) -> + <> = Rest, + <> = Rest2, + #{ + <<"status">> => Status, + <<"time">> => from_bcd(TimeBCD, []), + <<"ic_result">> => IcResult, + <<"driver_name">> => Name, + <<"certificate">> => Certificate, + <<"organization">> => Orgnization, + <<"cert_expiry">> => from_bcd(CertExpiryBCD, []) + }; +parse_message_body(?MC_BULK_LOCATION_REPORT, <>) -> + #{ + <<"type">> => Type, + <<"length">> => Count, + <<"location">> => parse_bulk_location_report(Count, Rest, []) + }; +parse_message_body(?MC_CAN_BUS_REPORT, <>) -> + CanData = parse_can_data(Count, Rest, []), + #{<<"length">> => Count, <<"time">> => from_bcd(TimeBCD, []), <<"can_data">> => CanData}; +parse_message_body( + ?MC_MULTIMEDIA_EVENT_REPORT, <> +) -> + #{ + <<"id">> => Id, + <<"type">> => Type, + <<"format">> => Format, + <<"event">> => Event, + <<"channel">> => Channel + }; +parse_message_body( + ?MC_MULTIMEDIA_DATA_REPORT, + <> +) -> + #{ + <<"id">> => Id, + <<"type">> => Type, + <<"format">> => Format, + <<"event">> => Event, + <<"channel">> => Channel, + <<"location">> => parse_location_report(Location), + <<"multimedia">> => base64:encode(Multimedia) + }; +parse_message_body(?MC_CAMERA_SHOT_ACK, <>) when + Result =:= 0 +-> + %% if Result is 0, means suceeded, "length" & "ids" present + {Array, _} = dword_array(Count, Rest, []), + #{<<"seq">> => Seq, <<"result">> => Result, <<"length">> => Count, <<"ids">> => Array}; +parse_message_body(?MC_CAMERA_SHOT_ACK, <>) -> + %% if Result is not 0, means failed, no "length" & "ids" + #{<<"seq">> => Seq, <<"result">> => Result}; +parse_message_body(?MC_MM_DATA_SEARCH_ACK, <>) -> + #{ + <<"seq">> => Seq, + <<"length">> => Count, + <<"result">> => parse_multimedia_search_result(Count, Rest, []) + }; +parse_message_body(?MC_SEND_TRANSPARENT_DATA, <>) -> + #{<<"type">> => Type, <<"data">> => base64:encode(Data)}; +parse_message_body(?MC_SEND_ZIP_DATA, <>) -> + #{<<"length">> => Length, <<"data">> => base64:encode(Data)}; +parse_message_body(?MC_RSA_KEY, <>) -> + #{<<"e">> => E, <<"n">> => base64:encode(N)}; +parse_message_body(UnknownId, Binary) -> + ?SLOG(error, #{msg => "unknow_message", id => UnknownId, msg_body => Binary}), + {error, invalid_message}. + +parse_client_params(<>) -> + {Count, parse_client_params2(Count, Rest, [])}. + +parse_client_params2(0, _Rest, Acc) -> + lists:reverse(Acc); +parse_client_params2(Count, <>, Acc) -> + {Value, Rest3} = + case client_param_data_type(Id) of + dword -> decode_cp_dword(Rest); + word -> decode_cp_word(Rest); + byte -> decode_cp_byte(Rest); + string -> decode_cp_string(Length, Rest); + reserved -> decode_cp_reserved(Length, Rest) + end, + parse_client_params2(Count - 1, Rest3, [#{<<"id">> => Id, <<"value">> => Value} | Acc]). + +decode_cp_dword(<>) -> + {Value, Rest}. + +decode_cp_word(<>) -> + {Value, Rest}. + +decode_cp_byte(<>) -> + {Value, Rest}. + +decode_cp_string(Length, Binary) -> + <> = Binary, + {Value, Rest}. + +decode_cp_reserved(Length, Binary) -> + <> = Binary, + {base64:encode(Value), Rest}. + +parse_location_report( + <> +) -> + Ret = #{ + <<"alarm">> => Alarm, + <<"status">> => Status, + <<"latitude">> => Latitude, + <<"longitude">> => Longitude, + <<"altitude">> => Altitude, + <<"speed">> => Speed, + <<"direction">> => Direction, + <<"time">> => from_bcd(TimeBCD, []) + }, + case Rest of + <<>> -> Ret; + _ -> Ret#{<<"extra">> => parse_location_report_extra(Rest, #{})} + end. + +parse_location_report_extra(<<>>, Acc) -> + Acc; +parse_location_report_extra( + <>, Acc +) -> + parse_location_report_extra(Rest, Acc#{<<"mileage">> => MileAge}); +parse_location_report_extra( + <>, Acc +) -> + parse_location_report_extra(Rest, Acc#{<<"fuel_meter">> => FuelMeter}); +parse_location_report_extra(<>, Acc) -> + parse_location_report_extra(Rest, Acc#{<<"speed">> => Speed}); +parse_location_report_extra( + <>, Acc +) -> + parse_location_report_extra(Rest, Acc#{<<"alarm_id">> => AlarmID}); +parse_location_report_extra( + <>, Acc +) -> + case Length of + 1 -> + <> = Rest, + parse_location_report_extra(Rest2, Acc#{<<"overspeed_alarm">> => #{<<"type">> => Type}}); + 5 -> + <> = Rest, + parse_location_report_extra(Rest2, Acc#{ + <<"overspeed_alarm">> => #{<<"type">> => Type, <<"id">> => Id} + }) + end; +parse_location_report_extra( + <>, + Acc +) -> + parse_location_report_extra(Rest, Acc#{ + <<"in_out_alarm">> => #{<<"type">> => Type, <<"id">> => Id, <<"direction">> => Direction} + }); +parse_location_report_extra( + <>, + Acc +) -> + parse_location_report_extra(Rest, Acc#{ + <<"path_time_alarm">> => #{<<"id">> => ID, <<"time">> => Time, <<"result">> => Result} + }); +parse_location_report_extra( + <>, Acc +) -> + <> = Signal, + parse_location_report_extra(Rest, Acc#{ + <<"signal">> => #{ + <<"low_beam">> => LowBeam, + <<"high_beam">> => HighBeam, + <<"right_turn">> => RightTurnSignal, + <<"left_turn">> => LeftTurnSignal, + <<"brake">> => Brake, + <<"reverse">> => Reverse, + <<"fog">> => Fog, + <<"side_marker">> => SideMarker, + <<"horn">> => Horn, + <<"air_conditioner">> => AirConditioner, + <<"neutral">> => Neutral, + <<"retarder">> => Retarder, + <<"abs">> => ABS, + <<"heater">> => Heater, + <<"cluth">> => Cluth + } + }); +parse_location_report_extra( + <>, Acc +) -> + parse_location_report_extra(Rest, Acc#{ + <<"io_status">> => #{<<"deep_sleep">> => DeepSleep, <<"sleep">> => Sleep} + }); +parse_location_report_extra( + <>, Acc +) -> + parse_location_report_extra(Rest, Acc#{<<"analog">> => #{<<"ad0">> => AD0, <<"ad1">> => AD1}}); +parse_location_report_extra(<>, Acc) -> + parse_location_report_extra(Rest, Acc#{<<"rssi">> => Rssi}); +parse_location_report_extra( + <>, Acc +) -> + parse_location_report_extra(Rest, Acc#{<<"gnss_sat_num">> => SatNum}); +%% TODO: ensure custom data +parse_location_report_extra(<>, Acc) -> + <> = Rest, + parse_location_report_extra(Rest, Acc#{<<"custome">> => base64:encode(Data)}); +parse_location_report_extra(<>, Acc) when + CustomeId >= 16#E0, CustomeId =< 16#FF +-> + <> = Rest, + Custome = maps:get(<<"custome">>, Acc, #{}), + NCustomeId = integer_to_binary(CustomeId), + parse_location_report_extra( + Rest2, + Acc#{<<"custome">> => maps:put(NCustomeId, base64:encode(Data), Custome)} + ); +parse_location_report_extra(<>, Acc) -> + <> = Rest, + ReservedId = integer_to_binary(ReservedId0), + parse_location_report_extra( + Rest2, + Acc#{ReservedId => base64:encode(Data)} + ). + +parse_bulk_location_report(0, _Binary, Acc) -> + lists:reverse(Acc); +parse_bulk_location_report(Count, <>, Acc) -> + <> = Rest, + parse_bulk_location_report(Count - 1, Rest2, [parse_location_report(Data) | Acc]). + +parse_can_data(0, _, Acc) -> + lists:reverse(Acc); +parse_can_data( + Count, + <>, + Acc +) -> + parse_can_data(Count - 1, Rest, [ + #{ + <<"channel">> => CanCh, + <<"frame_type">> => CanFrameType, + <<"data_method">> => CanDataMethod, + <<"id">> => CanId, + <<"data">> => base64:encode(Data) + } + | Acc + ]). + +dword_array(0, Binary, Acc) -> + {lists:reverse(Acc), Binary}; +dword_array(Count, <>, Acc) -> + dword_array(Count - 1, Rest, [Value | Acc]). + +parse_multimedia_search_result(0, _, Acc) -> + lists:reverse(Acc); +parse_multimedia_search_result( + Count, + <>, + Acc +) -> + parse_multimedia_search_result(Count - 1, Rest, [ + #{ + <<"id">> => Id, + <<"type">> => Type, + <<"channel">> => Channel, + <<"event">> => Event, + <<"location">> => parse_location_report(Location) + } + | Acc + ]). + +%%-------------------------------------------------------------------- +%% Serialize JT808 Message +%%-------------------------------------------------------------------- +serialize(Json) -> + Header = maps:get(<<"header">>, Json), + Body = + case maps:is_key(<<"body">>, Json) of + true -> maps:get(<<"body">>, Json); + false -> <<>> + end, + BodyStream = serialize_body(maps:get(<<"msg_id">>, Header), Body), + %% TODO: encrypt body here + Header2 = maps:put(<<"len">>, size(BodyStream), Header), + HeaderStream = serialize_header(Header2), + packet(<>). + +serialize_header( + Header = #{ + <<"msg_id">> := MsgId, + <<"encrypt">> := Encrypt, + <<"len">> := Length, + <<"phone">> := Phone, + <<"msg_sn">> := MsgSn + } +) -> + PhoneBCD = to_bcd(Phone, 6), + {Fragment, Total, Seq} = + case maps:is_key(<<"frag_total">>, Header) of + true -> {1, maps:get(<<"frag_total">>, Header), maps:get(<<"frag_sn">>, Header)}; + false -> {0, 0, 0} + end, + Binary = + <>, + case Fragment of + 0 -> Binary; + 1 -> <> + end. + +serialize_body(?MS_GENERAL_RESPONSE, Body) -> + Seq = maps:get(<<"seq">>, Body), + Id = maps:get(<<"id">>, Body), + Result = maps:get(<<"result">>, Body), + <>; +serialize_body(?MS_REQUEST_FRAGMENT, Body) -> + Seq = maps:get(<<"seq">>, Body), + Length = maps:get(<<"length">>, Body), + Ids = maps:get(<<"ids">>, Body), + LastStream = encode_word_array(Length, Ids, <<>>), + <>; +serialize_body(?MS_REGISTER_ACK, Body) -> + Seq = maps:get(<<"seq">>, Body), + %% XXX: replaced by maroc? + Result = maps:get(<<"result">>, Body), + case maps:is_key(<<"auth_code">>, Body) of + true -> + Code = maps:get(<<"auth_code">>, Body), + <>; + false -> + %% If the terminal regiter failed, it don't contain auth code + <> + end; +serialize_body(?MS_SET_CLIENT_PARAM, Body) -> + Length = maps:get(<<"length">>, Body), + ParamList = maps:get(<<"params">>, Body), + serialize_client_param(<>, ParamList); +serialize_body(?MS_QUERY_CLIENT_ALL_PARAM, _Body) -> + <<>>; +serialize_body(?MS_QUERY_CLIENT_PARAM, Body) -> + Length = maps:get(<<"length">>, Body), + List = maps:get(<<"ids">>, Body), + encode_dword_array(Length, List, <>); +serialize_body(?MS_CLIENT_CONTROL, Body) -> + Command = maps:get(<<"command">>, Body), + Param = maps:get(<<"param">>, Body), + <>; +serialize_body(?MS_QUERY_CLIENT_ATTRIB, _Body) -> + <<>>; +serialize_body(?MS_OTA, Body) -> + %% TODO: OTA in this way? + Type = maps:get(<<"type">>, Body), + Manuf = maps:get(<<"manufacturer">>, Body), + VerLength = maps:get(<<"ver_len">>, Body), + Version = maps:get(<<"version">>, Body), + FwLen = maps:get(<<"fw_len">>, Body), + Firmware = maps:get(<<"firmware">>, Body), + <>; +serialize_body(?MS_QUERY_LOCATION, _Body) -> + <<>>; +serialize_body(?MS_TRACE_LOCATION, Body) -> + Period = maps:get(<<"period">>, Body), + Expiry = maps:get(<<"expiry">>, Body), + <>; +serialize_body(?MS_CONFIRM_ALARM, Body) -> + Seq = maps:get(<<"seq">>, Body), + Type = maps:get(<<"type">>, Body), + <>; +serialize_body(?MS_SEND_TEXT, Body) -> + Flag = maps:get(<<"flag">>, Body), + Text = maps:get(<<"text">>, Body), + <>; +serialize_body(?MS_SET_EVENT, Body) -> + Type = maps:get(<<"type">>, Body), + %% FIXME: If the type is 0, the length and events is empty + Length = maps:get(<<"length">>, Body), + Events = maps:get(<<"events">>, Body), + serialize_events(Events, <>); +serialize_body(?MS_SEND_QUESTION, Body) -> + Flag = maps:get(<<"flag">>, Body), + Length = maps:get(<<"length">>, Body), + Question = maps:get(<<"question">>, Body), + Answers = maps:get(<<"answers">>, Body), + serialize_candidate_answers(Answers, <>); +serialize_body(?MS_SET_MENU, Body) -> + %% XXX: If the tpye is delete all menu, the remaining bytes should be drop? + Type = maps:get(<<"type">>, Body), + Length = maps:get(<<"length">>, Body), + Menus = maps:get(<<"menus">>, Body), + serialize_menus(Menus, <>); +serialize_body(?MS_INFO_CONTENT, Body) -> + Type = maps:get(<<"type">>, Body), + Length = maps:get(<<"length">>, Body), + Info = maps:get(<<"info">>, Body), + <>; +serialize_body(?MS_PHONE_CALLBACK, Body) -> + Type = maps:get(<<"type">>, Body), + Phone = maps:get(<<"phone">>, Body), + <>; +serialize_body(?MS_SET_PHONE_NUMBER, Body) -> + Type = maps:get(<<"type">>, Body), + Length = maps:get(<<"length">>, Body), + Contacts = maps:get(<<"contacts">>, Body), + serialize_contacts(Contacts, <>); +serialize_body(?MS_VEHICLE_CONTROL, Body) -> + Flag = maps:get(<<"flag">>, Body), + <>; +serialize_body(?MS_SET_CIRCLE_AREA, Body) -> + Type = maps:get(<<"type">>, Body), + Length = maps:get(<<"length">>, Body), + Areas = maps:get(<<"areas">>, Body), + serialize_circle_area(Areas, <>); +serialize_body(?MS_DEL_CIRCLE_AREA, Body) -> + Length = maps:get(<<"length">>, Body), + Ids = maps:get(<<"ids">>, Body), + encode_dword_array(Length, Ids, <>); +serialize_body(?MS_SET_RECT_AREA, Body) -> + Type = maps:get(<<"type">>, Body), + Length = maps:get(<<"length">>, Body), + Areas = maps:get(<<"areas">>, Body), + serialize_rect_area(Areas, <>); +serialize_body(?MS_DEL_RECT_AREA, Body) -> + Length = maps:get(<<"length">>, Body), + Ids = maps:get(<<"ids">>, Body), + encode_dword_array(Length, Ids, <>); +serialize_body(?MS_SET_POLY_AREA, Body) -> + Id = maps:get(<<"id">>, Body), + Flag = maps:get(<<"flag">>, Body), + StartTime = maps:get(<<"start_time">>, Body), + EndTime = maps:get(<<"end_time">>, Body), + MaxSpeed = maps:get(<<"max_speed">>, Body), + Overspeed = maps:get(<<"overspeed_duration">>, Body), + Length = maps:get(<<"length">>, Body), + Points = maps:get(<<"points">>, Body), + StartBCD = to_bcd(StartTime, 6), + EndBCD = to_bcd(EndTime, 6), + serialize_poly_point( + Length, + Points, + <> + ); +serialize_body(?MS_DEL_POLY_AREA, Body) -> + Length = maps:get(<<"length">>, Body), + Ids = maps:get(<<"ids">>, Body), + encode_dword_array(Length, Ids, <>); +serialize_body(?MS_SET_PATH, Body) -> + Id = maps:get(<<"id">>, Body), + Flag = maps:get(<<"flag">>, Body), + StartTime = maps:get(<<"start_time">>, Body), + EndTime = maps:get(<<"end_time">>, Body), + Length = maps:get(<<"length">>, Body), + Points = maps:get(<<"points">>, Body), + StartBCD = to_bcd(StartTime, 6), + EndBCD = to_bcd(EndTime, 6), + serialize_corner_point( + Length, Points, <> + ); +serialize_body(?MS_DEL_PATH, Body) -> + Length = maps:get(<<"length">>, Body), + Ids = maps:get(<<"ids">>, Body), + encode_dword_array(Length, Ids, <>); +serialize_body(?MS_DRIVE_RECORD_CAPTURE, Body) -> + Command = maps:get(<<"command">>, Body), + Param = maps:get(<<"param">>, Body), + RawParam = base64:decode(Param), + <>; +serialize_body(?MS_DRIVE_REC_PARAM_SEND, Body) -> + Command = maps:get(<<"command">>, Body), + Param = maps:get(<<"param">>, Body), + RawParam = base64:decode(Param), + <>; +serialize_body(?MS_REQ_DRIVER_ID, _Body) -> + <<>>; +serialize_body(?MS_MULTIMEDIA_DATA_ACK, Body) -> + MmId = maps:get(<<"mm_id">>, Body), + Length = maps:get(<<"length">>, Body), + RetxIds = maps:get(<<"retx_ids">>, Body), + encode_word_array(Length, RetxIds, <>); +serialize_body(?MS_CAMERA_SHOT, Body) -> + ChId = maps:get(<<"channel_id">>, Body), + Command = maps:get(<<"command">>, Body), + Period = maps:get(<<"period">>, Body), + Save = maps:get(<<"save">>, Body), + Resolution = maps:get(<<"resolution">>, Body), + Quality = maps:get(<<"quality">>, Body), + Bright = maps:get(<<"bright">>, Body), + Contrast = maps:get(<<"contrast">>, Body), + Saturate = maps:get(<<"saturate">>, Body), + Chromaticity = maps:get(<<"chromaticity">>, Body), + <>; +serialize_body(?MS_MM_DATA_SEARCH, Body) -> + Type = maps:get(<<"type">>, Body), + Channel = maps:get(<<"channel">>, Body), + Event = maps:get(<<"event">>, Body), + StartTime = maps:get(<<"start_time">>, Body), + EndTime = maps:get(<<"end_time">>, Body), + StartBCD = to_bcd(StartTime, 6), + EndBCD = to_bcd(EndTime, 6), + <>; +serialize_body(?MS_MM_DATA_UPLOAD, Body) -> + Type = maps:get(<<"type">>, Body), + ChId = maps:get(<<"channel">>, Body), + Event = maps:get(<<"event">>, Body), + Start = maps:get(<<"start_time">>, Body), + End = maps:get(<<"end_time">>, Body), + Delete = maps:get(<<"delete">>, Body), + StartBCD = to_bcd(Start, 6), + EndBCD = to_bcd(End, 6), + <>; +serialize_body(?MS_VOICE_RECORD, Body) -> + Command = maps:get(<<"command">>, Body), + Time = maps:get(<<"time">>, Body), + Save = maps:get(<<"save">>, Body), + Rate = maps:get(<<"rate">>, Body), + <>; +serialize_body(?MS_SINGLE_MM_DATA_CTRL, Body) -> + Id = maps:get(<<"id">>, Body), + Flag = maps:get(<<"flag">>, Body), + <>; +serialize_body(?MS_SEND_TRANSPARENT_DATA, Body) -> + Type = maps:get(<<"type">>, Body), + DataBase64 = maps:get(<<"data">>, Body), + Data = base64:decode(DataBase64), + <>; +serialize_body(?MS_RSA_KEY, Body) -> + E = maps:get(<<"e">>, Body), + N = maps:get(<<"n">>, Body), + <>; +serialize_body(_UnkonwnMsgId, _Body) -> + {error, invalid_input}. + +serialize_corner_point(0, [], Acc) -> + Acc; +serialize_corner_point( + Count, + [ + #{ + <<"point_id">> := PointId, + <<"path_id">> := PathId, + <<"point_lat">> := Lat, + <<"point_lng">> := Lng, + <<"width">> := Width, + <<"attrib">> := Attrib, + <<"passed">> := Passed, + <<"uncovered">> := Uncovered, + <<"max_speed">> := MaxSpeed, + <<"overspeed_duration">> := Overspeed + } + | T + ], + Acc +) -> + serialize_corner_point( + Count - 1, + T, + <> + ). + +serialize_poly_point(0, _, Acc) -> + Acc; +serialize_poly_point(Count, [#{<<"lat">> := Lat, <<"lng">> := Lng} | T], Acc) -> + serialize_poly_point(Count - 1, T, <>). + +serialize_rect_area([], Acc) -> + Acc; +serialize_rect_area( + [ + #{ + <<"id">> := Id, + <<"flag">> := Flag, + <<"lt_lat">> := LtLatitude, + <<"lt_lng">> := LtLongitude, + <<"rb_lat">> := RbLatitude, + <<"rb_lng">> := RbLongitude, + <<"start_time">> := StartTime, + <<"end_time">> := EndTime, + <<"max_speed">> := MaxSpeed, + <<"overspeed_duration">> := Overspeed + } + | T + ], + Acc +) -> + StartBCD = to_bcd(StartTime, 6), + EndBCD = to_bcd(EndTime, 6), + serialize_rect_area( + T, + <> + ). + +serialize_circle_area([], Acc) -> + Acc; +serialize_circle_area( + [ + H = #{ + <<"id">> := Id, + <<"flag">> := Flag, + <<"center_latitude">> := Latitude, + <<"center_longitude">> := Longitude, + <<"radius">> := Radius + } + | T + ], + Acc +) -> + First = <>, + Second = + case maps:is_key(<<"start_time">>, H) of + true -> + #{<<"start_time">> := StartTime, <<"end_time">> := EndTime} = H, + StartBCD = to_bcd(StartTime, 6), + EndBCD = to_bcd(EndTime, 6), + <>; + false -> + First + end, + Third = + case maps:is_key(<<"max_speed">>, H) of + true -> + #{<<"max_speed">> := MaxSpeed, <<"overspeed_duration">> := Overspeed} = H, + <>; + false -> + Second + end, + serialize_circle_area(T, Third). + +serialize_contacts([], Acc) -> + Acc; +serialize_contacts( + [ + #{ + <<"type">> := Type, + <<"phone_len">> := PhoneLen, + <<"phone">> := Phone, + <<"name_len">> := NameLen, + <<"name">> := Name + } + | T + ], + Acc +) -> + serialize_contacts( + T, <> + ). + +serialize_menus([], Acc) -> + Acc; +serialize_menus([#{<<"type">> := Type, <<"length">> := Length, <<"info">> := Info} | T], Acc) -> + serialize_menus(T, <>). + +serialize_candidate_answers([], Acc) -> + Acc; +serialize_candidate_answers([#{<<"id">> := Id, <<"len">> := Len, <<"answer">> := Answer} | T], Acc) -> + serialize_candidate_answers(T, <>). + +serialize_events([], Acc) -> + Acc; +serialize_events([#{<<"id">> := Id, <<"length">> := Len, <<"content">> := Content} | T], Acc) -> + serialize_events(T, <>). + +serialize_client_param(Acc, []) -> + Acc; +serialize_client_param(Acc, [#{<<"id">> := Id, <<"value">> := Value} | T]) -> + NewAcc = encode_client_param(Id, Value, Acc), + serialize_client_param(NewAcc, T). + +encode_client_param(Id, Value, Acc) -> + case client_param_data_type(Id) of + dword -> encode_cp_dword(Id, Value, Acc); + word -> encode_cp_word(Id, Value, Acc); + byte -> encode_cp_byte(Id, Value, Acc); + string -> encode_cp_string(Id, Value, Acc); + reserved -> encode_cp_reserved(Id, Value, Acc) + end. + +client_param_data_type(?CP_HEARTBEAT_DURATION) -> dword; +client_param_data_type(?CP_TCP_TIMEOUT) -> dword; +client_param_data_type(?CP_TCP_RETX) -> dword; +client_param_data_type(?CP_UDP_TIMEOUT) -> dword; +client_param_data_type(?CP_UDP_RETX) -> dword; +client_param_data_type(?CP_SMS_TIMEOUT) -> dword; +client_param_data_type(?CP_SMS_RETX) -> dword; +client_param_data_type(?CP_SERVER_APN) -> string; +client_param_data_type(?CP_DIAL_USERNAME) -> string; +client_param_data_type(?CP_DIAL_PASSWORD) -> string; +client_param_data_type(?CP_SERVER_ADDRESS) -> string; +client_param_data_type(?CP_BACKUP_SERVER_APN) -> string; +client_param_data_type(?CP_BACKUP_DIAL_USERNAME) -> string; +client_param_data_type(?CP_BACKUP_DIAL_PASSWORD) -> string; +client_param_data_type(?CP_BACKUP_SERVER_ADDRESS) -> string; +client_param_data_type(?CP_SERVER_TCP_PORT) -> dword; +client_param_data_type(?CP_SERVER_UDP_PORT) -> dword; +client_param_data_type(?CP_IC_CARD_SERVER_ADDRESS) -> string; +client_param_data_type(?CP_IC_CARD_SERVER_TCP_PORT) -> dword; +client_param_data_type(?CP_IC_CARD_SERVER_UDP_PORT) -> dword; +client_param_data_type(?CP_IC_CARD_BACKUP_SERVER_ADDRESS) -> string; +client_param_data_type(?CP_POS_REPORT_POLICY) -> dword; +client_param_data_type(?CP_POS_REPORT_CONTROL) -> dword; +client_param_data_type(?CP_DRIVER_NLOGIN_REPORT_INTERVAL) -> dword; +client_param_data_type(?CP_REPORT_INTERVAL_DURING_SLEEP) -> dword; +client_param_data_type(?CP_EMERGENCY_ALARM_REPORT_INTERVAL) -> dword; +client_param_data_type(?CP_DEFAULT_REPORT_INTERVAL) -> dword; +client_param_data_type(?CP_DEFAULT_DISTANCE_INTERVAL) -> dword; +client_param_data_type(?CP_DRIVER_NLOGIN_DISTANCE_INTERVAL) -> dword; +client_param_data_type(?CP_DISTANCE_INTERVAL_DURING_SLEEP) -> dword; +client_param_data_type(?CP_EMERGENCY_ALARM_DISTANCE_INTERVAL) -> dword; +client_param_data_type(?CP_SET_TURN_ANGLE) -> dword; +client_param_data_type(?CP_EFENCE_RADIUS) -> word; +client_param_data_type(?CP_MONITOR_PHONE) -> string; +client_param_data_type(?CP_RESETING_PHONE) -> string; +client_param_data_type(?CP_RECOVERY_PHONE) -> string; +client_param_data_type(?CP_SMS_MONITOR_PHONE) -> string; +client_param_data_type(?CP_EMERGENCY_SMS_PHONE) -> string; +client_param_data_type(?CP_ACCEPT_CALL_POLICY) -> dword; +client_param_data_type(?CP_MAX_CALL_DURATION) -> dword; +client_param_data_type(?CP_MAX_CALL_DURATION_OF_MONTH) -> dword; +client_param_data_type(?CP_SPY_PHONE) -> string; +client_param_data_type(?CP_PRIVILEGE_SMS_PHONE) -> string; +client_param_data_type(?CP_ALARM_MASK) -> dword; +client_param_data_type(?CP_ALARM_SEND_SMS_MASK) -> dword; +client_param_data_type(?CP_ALARM_CAMERA_SHOT_MASK) -> dword; +client_param_data_type(?CP_ALARM_PICTURE_SAVE_MASK) -> dword; +client_param_data_type(?CP_ALARM_KEY_MASK) -> dword; +client_param_data_type(?CP_MAX_SPEED) -> dword; +client_param_data_type(?CP_OVERSPEED_ELAPSED) -> dword; +client_param_data_type(?CP_CONT_DRIVE_THRESHOLD) -> dword; +client_param_data_type(?CP_ACC_DRIVE_TIME_ONE_DAY_THRESHOLD) -> dword; +client_param_data_type(?CP_MIN_BREAK_TIME) -> dword; +client_param_data_type(?CP_MAX_PARK_TIME) -> dword; +client_param_data_type(?CP_OVERSPEED_ALARM_DELTA) -> word; +client_param_data_type(?CP_DRIVER_FATIGUE_ALARM_DELTA) -> word; +client_param_data_type(?CP_SET_CRASH_ALARM_PARAM) -> word; +client_param_data_type(?CP_SET_ROLLOVER_PARAM) -> word; +client_param_data_type(?CP_TIME_CONTROLED_CAMERA) -> dword; +client_param_data_type(?CP_DISTANCE_CONTROLED_CAMERA) -> dword; +client_param_data_type(?CP_PICTURE_QUALITY) -> dword; +client_param_data_type(?CP_PICTURE_BRIGHTNESS) -> dword; +client_param_data_type(?CP_PICTURE_CONTRAST) -> dword; +client_param_data_type(?CP_PICTURE_SATURATE) -> dword; +client_param_data_type(?CP_PICTURE_CHROMATICITY) -> dword; +client_param_data_type(?CP_ODOMETER) -> dword; +client_param_data_type(?CP_REGISTERED_PROVINCE) -> word; +client_param_data_type(?CP_REGISTERED_CITY) -> word; +client_param_data_type(?CP_VEHICLE_LICENSE_NUMBER) -> string; +client_param_data_type(?CP_VEHICLE_LICENSE_PLATE_COLOR) -> byte; +client_param_data_type(?CP_GNSS_MODE) -> byte; +client_param_data_type(?CP_GNSS_BAUDRATE) -> byte; +client_param_data_type(?CP_GNSS_OUTPUT_RATE) -> byte; +client_param_data_type(?CP_GNSS_SAMPLING_RATE) -> dword; +client_param_data_type(?CP_GNSS_UPLOAD_MODE) -> byte; +client_param_data_type(?CP_GNSS_UPLOAD_UNIT) -> dword; +client_param_data_type(?CP_CAN_BUS_CH1_SAMPLING) -> dword; +client_param_data_type(?CP_CAN_BUS_CH1_UPLOAD) -> word; +client_param_data_type(?CP_CAN_BUS_CH2_SAMPLING) -> dword; +client_param_data_type(?CP_CAN_BUS_CH2_UPLOAD) -> word; +client_param_data_type(?CP_SET_CAN_BUS_ID_PARAM) -> string; +client_param_data_type(_) -> reserved. + +-spec encode_cp_byte(integer(), integer(), binary()) -> binary(). +encode_cp_byte(Id, Value, Acc) -> + <>. + +-spec encode_cp_word(integer(), integer(), binary()) -> binary(). +encode_cp_word(Id, Value, Acc) -> + <>. + +-spec encode_cp_dword(integer(), integer(), binary()) -> binary(). +encode_cp_dword(Id, Value, Acc) -> + <>. + +-spec encode_cp_string(integer(), binary(), binary()) -> binary(). +encode_cp_string(Id, StringBinary, Acc) -> + Length = size(StringBinary), + <>. + +-spec encode_cp_reserved(integer(), binary(), binary()) -> binary(). +encode_cp_reserved(Id, Base64Binary0, Acc) -> + Binary = base64:decode(Base64Binary0), + Length = size(Binary), + <>. + +packet(Binary) -> + packet2(Binary, undefined, <<16#7e:?BYTE>>). + +packet2(<<>>, Check, Acc) -> + Stream = pack(Acc, Check), + <>; +packet2(<>, Check, Acc) -> + NewCheck = cal_xor(C, Check), + packet2(Rest, NewCheck, pack(Acc, C)). + +pack(Stream, 16#7e) -> + <>; +pack(Stream, 16#7d) -> + <>; +pack(Stream, C) -> + <>. + +encode_word_array(0, _, Acc) -> + Acc; +encode_word_array(Count, [H | T], Acc) -> + encode_word_array(Count - 1, T, <>). + +encode_dword_array(0, _, Acc) -> + Acc; +encode_dword_array(Count, [H | T], Acc) -> + encode_dword_array(Count - 1, T, <>). + +from_bcd(<<>>, Acc) -> + list_to_binary(Acc); +from_bcd(<>, Acc) -> + from_bcd(Rest, Acc ++ [$0 + N1, $0 + N2]). + +to_bcd(String, BCDMaxSize) -> + StringSize = size(String), + Prefix = + case StringSize < BCDMaxSize of + true -> padding_zero(BCDMaxSize * 2 - StringSize, <<>>); + false -> <<>> + end, + encode_bcd(String, Prefix). + +padding_zero(0, Acc) -> + Acc; +padding_zero(Count, Acc) -> + padding_zero(Count - 1, <>). + +encode_bcd(<<>>, Acc) -> + Acc; +encode_bcd(<>, Acc) -> + C = H - $0, + encode_bcd(Rest, <>). + +check(Bin) -> + case check(Bin, undefined) of + true -> + Size = size(Bin) - 1, + <> = Bin, + Msg; + false -> + {error, invalid_message} + end. + +check(<<>>, _) -> + false; +check(<<_Byte:8>>, undefined) -> + false; +check(<>, XorValue) -> + Byte == XorValue; +check(<>, undefined) -> + check(Rest, Byte); +check(<>, XorValue) -> + check(Rest, Byte bxor XorValue). + +cal_xor(C, undefined) -> + C; +cal_xor(C, XorValue) -> + C bxor XorValue. diff --git a/apps/emqx_gateway_jt808/src/emqx_jt808_schema.erl b/apps/emqx_gateway_jt808/src/emqx_jt808_schema.erl new file mode 100644 index 000000000..35d8f962d --- /dev/null +++ b/apps/emqx_gateway_jt808/src/emqx_jt808_schema.erl @@ -0,0 +1,119 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_jt808_schema). + +-include("emqx_jt808.hrl"). +-include_lib("hocon/include/hoconsc.hrl"). +-include_lib("typerefl/include/types.hrl"). + +-export([fields/1, desc/1]). + +-define(NOT_EMPTY(MSG), emqx_resource_validator:not_empty(MSG)). + +fields(jt808) -> + [ + {frame, sc(ref(jt808_frame))}, + {proto, sc(ref(jt808_proto))}, + {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(); +fields(jt808_frame) -> + [ + {max_length, fun jt808_frame_max_length/1} + ]; +fields(jt808_proto) -> + [ + {allow_anonymous, fun allow_anonymous/1}, + {registry, fun registry_url/1}, + {authentication, fun authentication_url/1}, + {up_topic, fun up_topic/1}, + {dn_topic, fun dn_topic/1} + ]. + +jt808_frame_max_length(type) -> non_neg_integer(); +jt808_frame_max_length(desc) -> ?DESC(?FUNCTION_NAME); +jt808_frame_max_length(default) -> 8192; +jt808_frame_max_length(required) -> false; +jt808_frame_max_length(_) -> undefined. + +allow_anonymous(type) -> boolean(); +allow_anonymous(desc) -> ?DESC(?FUNCTION_NAME); +allow_anonymous(default) -> true; +allow_anonymous(required) -> false; +allow_anonymous(_) -> undefined. + +registry_url(type) -> binary(); +registry_url(desc) -> ?DESC(?FUNCTION_NAME); +registry_url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")]; +registry_url(required) -> false; +registry_url(_) -> undefined. + +authentication_url(type) -> binary(); +authentication_url(desc) -> ?DESC(?FUNCTION_NAME); +authentication_url(validator) -> [?NOT_EMPTY("the value of the field 'url' cannot be empty")]; +authentication_url(required) -> false; +authentication_url(_) -> undefined. + +up_topic(type) -> binary(); +up_topic(desc) -> ?DESC(?FUNCTION_NAME); +up_topic(default) -> ?DEFAULT_UP_TOPIC; +up_topic(validator) -> [?NOT_EMPTY("the value of the field 'up_topic' cannot be empty")]; +up_topic(required) -> true; +up_topic(_) -> undefined. + +dn_topic(type) -> binary(); +dn_topic(desc) -> ?DESC(?FUNCTION_NAME); +dn_topic(default) -> ?DEFAULT_DN_TOPIC; +dn_topic(validator) -> [?NOT_EMPTY("the value of the field 'dn_topic' cannot be empty")]; +dn_topic(required) -> true; +dn_topic(_) -> undefined. + +desc(jt808) -> + "The JT/T 808 protocol gateway provides EMQX with the ability to access JT/T 808 protocol devices."; +desc(jt808_frame) -> + "Limits for the JT/T 808 frames."; +desc(jt808_proto) -> + "The JT/T 808 protocol options."; +desc(_) -> + undefined. + +%%-------------------------------------------------------------------- +%% internal functions + +sc(Type) -> + sc(Type, #{}). + +sc(Type, Meta) -> + hoconsc:mk(Type, Meta). + +ref(StructName) -> + ref(?MODULE, StructName). + +ref(Mod, Field) -> + hoconsc:ref(Mod, Field). diff --git a/apps/emqx_gateway_jt808/test/emqx_jt808_SUITE.erl b/apps/emqx_gateway_jt808/test/emqx_jt808_SUITE.erl new file mode 100644 index 000000000..08ba9b8f8 --- /dev/null +++ b/apps/emqx_gateway_jt808/test/emqx_jt808_SUITE.erl @@ -0,0 +1,2687 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_jt808_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_jt808.hrl"). +-include_lib("emqx/include/emqx.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-define(FRM_FLAG, 16#7e:8). +-define(RESERVE, 0). +-define(NO_FRAGMENT, 0). +-define(WITH_FRAGMENT, 1). +-define(NO_ENCRYPT, 0). +-define(MSG_SIZE(X), X:10 / big - integer). + +-define(WORD, 16 / big - integer). +-define(DWORD, 32 / big - integer). + +-define(PORT, 6207). +-define(PORT_STR, "6207"). +-define(LOGT(Format, Args), ct:pal("TEST_SUITE: " ++ Format, Args)). + +-define(PROTO_REG_SERVER_HOST, "http://127.0.0.1:8991"). +-define(PROTO_REG_AUTH_PATH, "/jt808/auth"). +-define(PROTO_REG_REGISTRY_PATH, "/jt808/registry"). + +-define(JT808_PHONE, "000123456789"). +%% <<"jt808/000123456789/">> +-define(JT808_MOUNTPOINT, "jt808/" ?JT808_PHONE "/"). +%% <<"jt808/000123456789/000123456789/up">> +-define(JT808_UP_TOPIC, <>). +%% <<"jt808/000123456789/000123456789/dn">> +-define(JT808_DN_TOPIC, <>). + +-define(CONF_DEFAULT, << + "\n" + "gateway.jt808 {\n" + " listeners.tcp.default {\n" + " bind = " + ?PORT_STR + "\n" + " }\n" + " proto {\n" + " allow_anonymous = false\n" + " registry = " + "\"" + ?PROTO_REG_SERVER_HOST + ?PROTO_REG_REGISTRY_PATH + "\"\n" + " authentication = " + "\"" + ?PROTO_REG_SERVER_HOST + ?PROTO_REG_AUTH_PATH + "\"\n" + " }\n" + "}\n" +>>). + +-define(CONF_ANONYMOUS, << + "\n" + "gateway.jt808 {\n" + " listeners.tcp.default {\n" + " bind = " + ?PORT_STR + "\n" + " }\n" + " proto {\n" + " allow_anonymous = true\n" + " }\n" + "}\n" +>>). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_testcase(Case = t_case02_anonymous_register_and_auth, Config) -> + Apps = boot_apps(Case, ?CONF_ANONYMOUS, Config), + [{suite_apps, Apps} | Config]; +init_per_testcase(Case, Config) -> + Apps = boot_apps(Case, ?CONF_DEFAULT, Config), + [{suite_apps, Apps} | Config]. + +end_per_testcase(_Case, Config) -> + try + ok = emqx_jt808_auth_http_test_server:stop() + catch + exit:noproc -> + ok + end, + ok = emqx_cth_suite:stop(?config(suite_apps, Config)), + ok. + +boot_apps(Case, JT808Conf, Config) -> + application:load(emqx_gateway_jt808), + Apps = emqx_cth_suite:start( + [ + cowboy, + {emqx_conf, JT808Conf}, + emqx_gateway + ], + #{work_dir => emqx_cth_suite:work_dir(Case, Config)} + ), + {ok, _Pid} = emqx_jt808_auth_http_test_server:start_link(), + timer:sleep(1000), + Apps. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% helper functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +gen_packet(Header, Body) -> + S1 = <
>, + Crc = make_crc(S1, undefined), + S2 = do_escape(<>), + Stream = <<16#7e:8, S2/binary, 16#7e:8>>, + ?LOGT("encode a packet=~p", [binary_to_hex_string(Stream)]), + Stream. + +make_crc(<<>>, Xor) -> + ?LOGT("crc is ~p", [Xor]), + Xor; +make_crc(<>, undefined) -> + make_crc(Rest, C); +make_crc(<>, Xor) -> + make_crc(Rest, C bxor Xor). + +do_escape(Binary) -> + do_escape(Binary, <<>>). + +do_escape(<<>>, Acc) -> + Acc; +do_escape(<<16#7e, Rest/binary>>, Acc) -> + do_escape(Rest, <>); +do_escape(<<16#7d, Rest/binary>>, Acc) -> + do_escape(Rest, <>); +do_escape(<>, Acc) -> + do_escape(Rest, <>). + +client_regi_procedure(Socket) -> + client_regi_procedure(Socket, <<"123456">>). + +client_regi_procedure(Socket, ExpectedCode) -> + % + % send REGISTER + % + Manuf = <<"examp">>, + Model = <<"33333333333333333333">>, + DevId = <<"1234567">>, + + Color = 3, + Plate = <<"ujvl239">>, + RegisterPacket = + <<58:?WORD, 59:?WORD, Manuf/binary, Model/binary, DevId/binary, Color, Plate/binary>>, + MsgId = ?MC_REGISTER, + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + MsgSn = 78, + Size = size(RegisterPacket), + Header = + <>, + S1 = gen_packet(Header, RegisterPacket), + + ok = gen_tcp:send(Socket, S1), + {ok, Packet} = gen_tcp:recv(Socket, 0, 500), + + AckPacket = <>, + Size2 = size(AckPacket), + MsgId2 = ?MS_REGISTER_ACK, + MsgSn2 = 0, + Header2 = + <>, + S2 = gen_packet(Header2, AckPacket), + ?LOGT("S2=~p", [binary_to_hex_string(S2)]), + ?LOGT("Packet=~p", [binary_to_hex_string(Packet)]), + ?assertEqual(S2, Packet), + {ok, ExpectedCode}. + +client_auth_procedure(Socket, AuthCode) -> + ?LOGT("start auth procedure", []), + % + % send AUTH + % + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + MsgId = ?MC_AUTH, + MsgSn = 78, + Size = size(AuthCode), + Header = + <>, + S1 = gen_packet(Header, AuthCode), + ?LOGT("auth S1=~p", [S1]), + + ok = gen_tcp:send(Socket, S1), + %% timer:sleep(200), + {ok, Packet} = gen_tcp:recv(Socket, 0, 500), + + % receive general response + GenAckPacket = <>, + Size2 = size(GenAckPacket), + MsgId2 = ?MS_GENERAL_RESPONSE, + MsgSn2 = 1, + Header2 = + <>, + S2 = gen_packet(Header2, GenAckPacket), + ?assertEqual(S2, Packet), + ?assert(lists:member(?JT808_DN_TOPIC, emqx:topics())), + + ?LOGT("============= auth procedure success ===============", []). + +client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD) -> + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MC_GENERAL_RESPONSE, + MsgSn4 = 1, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + ok. + +location_report() -> + Alarm = 2, + Status = 3, + Latitude = 1000, + Longitude = 1001, + Altitude = 2000, + Speed = 135, + Direction = 32, + TimeBCD = <<16#17, 16#10, 16#22, 16#11, 16#15, 16#53>>, + Time = <<"171022111553">>, + MileAge = 12379, + FuelMeter = 972, + Speed2 = 29, + AlarmID = 18, + OA_Type = 2, + OA_Id = 29, + IOA_Type = 8, + IOA_Id = 23, + IOA_Direction = 89, + PTA_ID = 45, + PTA_Time = 21, + PTA_Result = 1, + Binary = + <>, + Json = #{ + <<"alarm">> => Alarm, + <<"status">> => Status, + <<"latitude">> => Latitude, + <<"longitude">> => Longitude, + <<"altitude">> => Altitude, + <<"speed">> => Speed, + <<"direction">> => Direction, + <<"time">> => Time, + <<"extra">> => #{ + <<"mileage">> => MileAge, + <<"fuel_meter">> => FuelMeter, + <<"speed">> => Speed2, + <<"alarm_id">> => AlarmID, + <<"overspeed_alarm">> => + #{ + <<"type">> => OA_Type, + <<"id">> => OA_Id + }, + <<"in_out_alarm">> => + #{ + <<"type">> => IOA_Type, + <<"id">> => IOA_Id, + <<"direction">> => IOA_Direction + }, + <<"path_time_alarm">> => + #{ + <<"id">> => PTA_ID, + <<"time">> => PTA_Time, + <<"result">> => PTA_Result + } + } + }, + {Binary, Json}. + +location_report_28bytes() -> + Alarm = 2, + Status = 3, + Latitude = 1000, + Longitude = 1001, + Altitude = 2000, + Speed = 135, + Direction = 32, + TimeBCD = <<16#17, 16#10, 16#22, 16#11, 16#15, 16#53>>, + Time = <<"171022111553">>, + Binary = + <>, + Json = #{ + <<"alarm">> => Alarm, + <<"status">> => Status, + <<"latitude">> => Latitude, + <<"longitude">> => Longitude, + <<"altitude">> => Altitude, + <<"speed">> => Speed, + <<"direction">> => Direction, + <<"time">> => Time + }, + {Binary, Json}. + +binary_to_hex_string(Data) -> + lists:flatten([io_lib:format("~2.16.0B ", [X]) || <> <= Data]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%% test cases %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +t_case00_register(_) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + + {ok, AuthCode} = client_regi_procedure(Socket), + ?assertEqual(AuthCode, <<"123456">>), + + ok = gen_tcp:close(Socket). + +t_case01_auth(_) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}, {nodelay, true}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + + ok = gen_tcp:close(Socket). + +t_case02_anonymous_register_and_auth(_) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + + {ok, AuthCode} = client_regi_procedure(Socket, <<>>), + ?assertEqual(AuthCode, <<>>), + + ok = client_auth_procedure(Socket, AuthCode), + + ok = gen_tcp:close(Socket). + +t_case03_heartbeat(_) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + % + % send heartbeat + % + MsgId = ?MC_HEARTBEAT, + MsgSn = 78, + Size = 0, + Header = + <>, + S1 = gen_packet(Header, <<>>), + + ok = gen_tcp:send(Socket, S1), + %% timer:sleep(200), + {ok, Packet} = gen_tcp:recv(Socket, 0, 500), + + GenAckPacket = <>, + Size2 = size(GenAckPacket), + MsgId2 = ?MS_GENERAL_RESPONSE, + MsgSn2 = 2, + Header2 = + <>, + S2 = gen_packet(Header2, GenAckPacket), + ?assertEqual(S2, Packet), + + ok = gen_tcp:close(Socket). + +t_case04(_) -> + ok = emqx:subscribe(?JT808_UP_TOPIC), + + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + %% send event report + EventReportId = 98, + MsgBody3 = <>, + MsgId3 = ?MC_EVENT_REPORT, + MsgSn3 = 79, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + ok = gen_tcp:send(Socket, S3), + {ok, Packet4} = gen_tcp:recv(Socket, 0, 500), + + % receive general response + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MS_GENERAL_RESPONSE, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?assertEqual(S4, Packet4), + timer:sleep(100), + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId3, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size3, + <<"msg_sn">> => MsgSn3 + }, + <<"body">> => #{<<"id">> => EventReportId} + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + ok = gen_tcp:close(Socket). + +t_case05(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + Flag = 15, + Text = <<"who are you">>, + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SEND_TEXT}, + <<"body">> => #{<<"flag">> => Flag, <<"text">> => Text} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + % + % client get downlink "send text" + % + + MsgBody3 = <>, + MsgId3 = ?MS_SEND_TEXT, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + ?LOGT("S3=~p", [S3]), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?assertEqual(S3, Packet3), + + % client send "client general response" + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MC_GENERAL_RESPONSE, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + %% timer:sleep(300), + + ok = gen_tcp:close(Socket). + +t_case06_downlink_retx(_) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + Flag = 15, + Text = <<"who are you">>, + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SEND_TEXT}, + <<"body">> => #{<<"flag">> => Flag, <<"text">> => Text} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + % + % client get downlink "send text" + % + + MsgBody3 = <>, + MsgId3 = ?MS_SEND_TEXT, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + ?LOGT("S3=~p", [S3]), + + %% wait emqx-jt808 to retx "send text" + timer:sleep(100), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?assertEqual(S3, Packet3), + + % client send "client general response" + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MC_GENERAL_RESPONSE, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + %% timer:sleep(300), + + % wait again, there should be no retx packet + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case07_dl_0x8302_send_question(_) -> + ok = emqx:subscribe(?JT808_UP_TOPIC), + + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + Flag = 16#10, + Question = <<"who are you">>, + Length = size(Question), + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SEND_QUESTION}, + <<"body">> => #{ + <<"flag">> => Flag, + <<"length">> => Length, + <<"question">> => Question, + <<"answers">> => [ + #{<<"id">> => 1, <<"len">> => 3, <<"answer">> => <<"Tom">>}, + #{<<"id">> => 2, <<"len">> => 4, <<"answer">> => <<"Mike">>} + ] + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + %% client get downlink "send text" + MsgBody3 = + <>/binary, 2:8, 4:?WORD, + <<"Mike">>/binary>>, + MsgId3 = ?MS_SEND_QUESTION, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?assertEqual(S3, Packet3), + + % client send "client answer" + Answer = <>, + Size4 = size(Answer), + MsgId4 = ?MC_QUESTION_ACK, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, Answer), + + ok = gen_tcp:send(Socket, S4), + timer:sleep(100), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId4, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size4, + <<"msg_sn">> => MsgSn4 + }, + <<"body">> => #{<<"seq">> => MsgSn3, <<"id">> => 2} + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + ok = gen_tcp:close(Socket). + +t_case08_dl_0x8500_vehicle_ctrl(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + Flag = 16#0, + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_VEHICLE_CONTROL}, + <<"body">> => #{<<"flag">> => Flag} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink "vehicle ctrl" + % + MsgBody3 = <>, + MsgId3 = ?MS_VEHICLE_CONTROL, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?assertEqual(S3, Packet3), + + % client send "client answer" + CtrlAck = + <<126, 5, 0, 0, 30, 1, 136, 118, 99, 137, 114, 1, 244, 0, 7, 0, 0, 0, 0, 0, 4, 0, 0, 1, 49, + 122, 103, 6, 147, 104, 81, 0, 24, 0, 0, 0, 121, 23, 16, 32, 18, 3, 25, 69, 126>>, + ?LOGT("S4 = ~p", [CtrlAck]), + + ok = gen_tcp:send(Socket, CtrlAck), + timer:sleep(300), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => + #{ + <<"encrypt">> => 0, + <<"len">> => 30, + <<"msg_id">> => 1280, + <<"msg_sn">> => 500, + <<"phone">> => <<"018876638972">> + }, + <<"body">> => + #{ + <<"seq">> => 7, + <<"location">> => #{ + <<"alarm">> => 0, + <<"altitude">> => 24, + <<"direction">> => 121, + <<"latitude">> => 20019815, + <<"longitude">> => 110323793, + <<"speed">> => 0, + <<"status">> => 262144, + <<"time">> => <<"171020120319">> + } + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + ok = gen_tcp:close(Socket). + +t_case09_dl_0x8103_set_client_param(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + Length = 2, + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SET_CLIENT_PARAM}, + <<"body">> => #{ + <<"length">> => Length, + <<"params">> => [ + #{<<"id">> => 16#0013, <<"value">> => <<"www.example.com">>}, + #{<<"id">> => 16#0059, <<"value">> => 1200} + ] + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink "vehicle ctrl" + % + MsgBody3 = + <>/binary, 16#0059:?DWORD, 4:8, + 1200:?DWORD>>, + MsgId3 = ?MS_SET_CLIENT_PARAM, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + % client send "general response" + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MC_GENERAL_RESPONSE, + MsgSn4 = 1, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + + % no retrasmition of 0x8103 + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case10_dl_0x8104_query_client_all_param(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_CLIENT_CONTROL}, + <<"body">> => #{<<"command">> => 200, <<"param">> => <<"ABCD">>} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<200:8, <<"ABCD">>/binary>>, + MsgId3 = ?MS_CLIENT_CONTROL, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + % client send ack + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + %% timer:sleep(200), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case11_dl_0x8106_query_client_param(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_QUERY_CLIENT_PARAM}, + <<"body">> => #{<<"length">> => 2, <<"ids">> => [16#0092, 16#0031]} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<2:8, 16#0092:?DWORD, 16#0031:?DWORD>>, + MsgId3 = ?MS_QUERY_CLIENT_PARAM, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + UlPacket4 = <>, + Size4 = size(UlPacket4), + MsgId4 = ?MC_QUERY_PARAM_ACK, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, UlPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"encrypt">> => 0, + <<"len">> => Size4, + <<"msg_id">> => ?MC_QUERY_PARAM_ACK, + <<"msg_sn">> => 2, + <<"phone">> => <<"000123456789">> + }, + <<"body">> => #{ + <<"seq">> => MsgSn3, + <<"length">> => 2, + <<"params">> => [ + #{<<"id">> => 16#0031, <<"value">> => 379}, + #{<<"id">> => 16#0092, <<"value">> => 2} + ] + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + %% timer:sleep(200), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case11_dl_0x8107_query_client_attrib(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + DlCommand = #{<<"header">> => #{<<"msg_id">> => ?MS_QUERY_CLIENT_ATTRIB}}, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<>>, + MsgId3 = ?MS_QUERY_CLIENT_ATTRIB, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + timer:sleep(100), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + UlPacket4 = + <<12:?WORD, <<"manu3">>/binary, <<"A1B2C3D4E5F6G7H8I9J0">>:20/binary, + <<"dev1234">>:7/binary, + <<16#33, 16#33, 16#33, 16#33, 16#33, 16#44, 16#44, 16#44, 16#44, 16#44>>:10/binary, 6:8, + <<"v2.3.7">>:6/binary, 5:8, <<"v1.26">>:5/binary, 101:8, 102:8>>, + Size4 = size(UlPacket4), + MsgId4 = ?MC_QUERY_ATTRIB_ACK, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, UlPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + timer:sleep(100), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"encrypt">> => 0, + <<"len">> => Size4, + <<"msg_id">> => ?MC_QUERY_ATTRIB_ACK, + <<"msg_sn">> => 2, + <<"phone">> => <<"000123456789">> + }, + <<"body">> => #{ + <<"type">> => 12, + <<"manufacturer">> => <<"manu3">>, + <<"model">> => <<"A1B2C3D4E5F6G7H8I9J0">>, + <<"id">> => <<"dev1234">>, + <<"iccid">> => <<"33333333334444444444">>, + <<"hardware_version">> => <<"v2.3.7">>, + <<"firmware_version">> => <<"v1.26">>, + <<"gnss_prop">> => 101, + <<"comm_prop">> => 102 + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case15_dl_0x8201_query_location(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + DlCommand = #{<<"header">> => #{<<"msg_id">> => ?MS_QUERY_LOCATION}}, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<>>, + MsgId3 = ?MS_QUERY_LOCATION, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + {LocationReportBinary, LocationReportJson} = location_report(), + UlPacket4 = <>, + Size4 = size(UlPacket4), + MsgId4 = ?MC_QUERY_LOCATION_ACK, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, UlPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + timer:sleep(100), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"encrypt">> => 0, + <<"len">> => Size4, + <<"msg_id">> => ?MC_QUERY_LOCATION_ACK, + <<"msg_sn">> => 2, + <<"phone">> => <<"000123456789">> + }, + <<"body">> => #{ + <<"seq">> => MsgSn3, + <<"params">> => LocationReportJson + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + %% timer:sleep(200), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_location_report(_) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + {LocationReportBinary, LocationReportJson} = location_report(), + UlPacket = <>, + Size = size(UlPacket), + MsgId = ?MC_LOCATION_REPORT, + MsgSn = 1, + Header = + <>, + S = gen_packet(Header, UlPacket), + ?LOGT("S = ~p", [S]), + + ok = gen_tcp:send(Socket, S), + timer:sleep(100), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"encrypt">> => 0, + <<"len">> => Size, + <<"msg_id">> => ?MC_LOCATION_REPORT, + <<"msg_sn">> => MsgSn, + <<"phone">> => <<"000123456789">> + }, + <<"body">> => LocationReportJson + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + % receive general response + {ok, Packet} = gen_tcp:recv(Socket, 0, 500), + GenAckPacket = <>, + Size2 = size(GenAckPacket), + MsgId2 = ?MS_GENERAL_RESPONSE, + MsgSn2 = 2, + Header2 = + <>, + S2 = gen_packet(Header2, GenAckPacket), + ?assertEqual(S2, Packet), + + % no retrasmition of downlink message + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case15_dl_0x8202_trace_location(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_TRACE_LOCATION}, + <<"body">> => #{<<"period">> => 23, <<"expiry">> => 183} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<23:?WORD, 183:?DWORD>>, + MsgId3 = ?MS_TRACE_LOCATION, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case50_ul_0x0303_info_request_cancel(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + % + % send event report + % + MsgBody3 = <<1:8, 6:8>>, + MsgId3 = ?MC_INFO_REQ_CANCEL, + MsgSn3 = 79, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + ok = gen_tcp:send(Socket, S3), + %%timer:sleep(600), + {ok, Packet4} = gen_tcp:recv(Socket, 0, 500), + + % receive general response + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MS_GENERAL_RESPONSE, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?assertEqual(S4, Packet4), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId3, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size3, + <<"msg_sn">> => MsgSn3 + }, + <<"body">> => #{<<"id">> => 1, <<"flag">> => 6} + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + ok = gen_tcp:close(Socket). + +t_case51_ul_0x0701_waybill_report(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + % + % send event report + % + MsgBody3 = <<7:?DWORD, <<"ABCDEFG">>/binary>>, + MsgId3 = ?MC_WAYBILL_REPORT, + MsgSn3 = 79, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + ok = gen_tcp:send(Socket, S3), + %% timer:sleep(600), + {ok, Packet4} = gen_tcp:recv(Socket, 0, 500), + + % receive general response + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MS_GENERAL_RESPONSE, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?assertEqual(S4, Packet4), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId3, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size3, + <<"msg_sn">> => MsgSn3 + }, + <<"body">> => #{<<"length">> => 7, <<"data">> => base64:encode(<<"ABCDEFG">>)} + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + ok = gen_tcp:close(Socket). + +t_case52_ul_0x0705_can_bus_report(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + % + % send event report + % + MsgBody3 = + <<2:?WORD, <<16#09, 16#23, 16#46, 16#07, 16#25>>/binary, 0:1, 1:1, 0:1, 35:29, + <<"11111111">>/binary, 1:1, 0:1, 1:1, 36:29, <<"22222222">>/binary>>, + MsgId3 = ?MC_CAN_BUS_REPORT, + MsgSn3 = 79, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + ok = gen_tcp:send(Socket, S3), + %% timer:sleep(600), + {ok, Packet4} = gen_tcp:recv(Socket, 0, 500), + + % receive general response + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MS_GENERAL_RESPONSE, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?assertEqual(S4, Packet4), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId3, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size3, + <<"msg_sn">> => MsgSn3 + }, + <<"body">> => #{ + <<"length">> => 2, + <<"time">> => <<"0923460725">>, + <<"can_data">> => [ + #{ + <<"channel">> => 0, + <<"frame_type">> => 1, + <<"data_method">> => 0, + <<"id">> => 35, + <<"data">> => base64:encode(<<"11111111">>) + }, + #{ + <<"channel">> => 1, + <<"frame_type">> => 0, + <<"data_method">> => 1, + <<"id">> => 36, + <<"data">> => base64:encode(<<"22222222">>) + } + ] + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + ok = gen_tcp:close(Socket). + +t_case53_ul_0x0800_multimedia_event_report(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + % + % send event report + % + MsgBody3 = <<65:?DWORD, 2:8, 1:8, 4:8, 103:8>>, + MsgId3 = ?MC_MULTIMEDIA_EVENT_REPORT, + MsgSn3 = 79, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + ok = gen_tcp:send(Socket, S3), + %% timer:sleep(600), + {ok, Packet4} = gen_tcp:recv(Socket, 0, 500), + + % receive general response + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MS_GENERAL_RESPONSE, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?assertEqual(S4, Packet4), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId3, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size3, + <<"msg_sn">> => MsgSn3 + }, + <<"body">> => #{ + <<"id">> => 65, + <<"type">> => 2, + <<"format">> => 1, + <<"event">> => 4, + <<"channel">> => 103 + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + ok = gen_tcp:close(Socket). + +t_case54_ul_0x0900_send_transparent_data(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + % + % send event report + % + MsgBody3 = <<39:8, <<"oufwei">>/binary>>, + MsgId3 = ?MC_SEND_TRANSPARENT_DATA, + MsgSn3 = 79, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + ok = gen_tcp:send(Socket, S3), + %% timer:sleep(600), + {ok, Packet4} = gen_tcp:recv(Socket, 0, 500), + + % Receive general response + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MS_GENERAL_RESPONSE, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?assertEqual(S4, Packet4), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId3, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size3, + <<"msg_sn">> => MsgSn3 + }, + <<"body">> => #{<<"type">> => 39, <<"data">> => base64:encode(<<"oufwei">>)} + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + ok = gen_tcp:close(Socket). + +t_case55_ul_0x0901_send_zip_data(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + % + % send event report + % + MsgBody3 = <<4:?DWORD, <<"1234">>/binary>>, + MsgId3 = ?MC_SEND_ZIP_DATA, + MsgSn3 = 79, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + ok = gen_tcp:send(Socket, S3), + %% timer:sleep(600), + {ok, Packet4} = gen_tcp:recv(Socket, 0, 500), + + % receive general response + GenAckPacket4 = <>, + Size4 = size(GenAckPacket4), + MsgId4 = ?MS_GENERAL_RESPONSE, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, GenAckPacket4), + ?assertEqual(S4, Packet4), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId3, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size3, + <<"msg_sn">> => MsgSn3 + }, + <<"body">> => #{ + <<"length">> => 4, + <<"data">> => base64:encode(<<"1234">>) + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + ok = gen_tcp:close(Socket). + +t_case16_dl_0x8301_set_event(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SET_EVENT}, + <<"body">> => #{ + <<"type">> => 3, + <<"length">> => 2, + <<"events">> => + [ + #{<<"id">> => 56, <<"length">> => 3, <<"content">> => <<"111">>}, + #{<<"id">> => 7, <<"length">> => 5, <<"content">> => <<"nwKdmww">>} + ] + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<3:8, 2:8, 56:8, 3:8, <<"111">>/binary, 7:8, 5:8, <<"nwKdmww">>/binary>>, + MsgId3 = ?MS_SET_EVENT, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case17_dl_0x8303_set_menu(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SET_MENU}, + <<"body">> => #{ + <<"type">> => 3, + <<"length">> => 2, + <<"menus">> => + [ + #{<<"type">> => 56, <<"length">> => 3, <<"info">> => <<"111">>}, + #{<<"type">> => 7, <<"length">> => 5, <<"info">> => <<"nwKdmww">>} + ] + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + %% client get downlink command + MsgBody3 = <<3:8, 2:8, 56:8, 3:?WORD, <<"111">>/binary, 7:8, 5:?WORD, <<"nwKdmww">>/binary>>, + MsgId3 = ?MS_SET_MENU, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case18_dl_0x8304_info_content(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_INFO_CONTENT}, + <<"body">> => #{<<"type">> => 3, <<"length">> => 2, <<"info">> => <<"NY">>} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + %% client get downlink command + MsgBody3 = <<3:8, 2:?WORD, <<"NY">>/binary>>, + MsgId3 = ?MS_INFO_CONTENT, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case19_dl_0x8400_phone_callback(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_PHONE_CALLBACK}, + <<"body">> => #{<<"type">> => 0, <<"phone">> => <<"15632597856">>} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + %% client get downlink command + MsgBody3 = <<0:8, <<"15632597856">>/binary>>, + MsgId3 = ?MS_PHONE_CALLBACK, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case20_dl_0x8401_set_phone_number(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SET_PHONE_NUMBER}, + <<"body">> => #{ + <<"type">> => 2, + <<"length">> => 2, + <<"contacts">> => + [ + #{ + <<"type">> => 2, + <<"phone_len">> => 10, + <<"phone">> => <<"13011112222">>, + <<"name_len">> => 3, + <<"name">> => <<"Tom">> + }, + #{ + <<"type">> => 3, + <<"phone_len">> => 11, + <<"phone">> => <<"013011113333">>, + <<"name_len">> => 4, + <<"name">> => <<"Mike">> + } + ] + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = + <<2:8, 2:8, 2:8, 10:8, <<"13011112222">>/binary, 3:8, <<"Tom">>/binary, 3:8, 11:8, + <<"013011113333">>/binary, 4:8, <<"Mike">>/binary>>, + MsgId3 = ?MS_SET_PHONE_NUMBER, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case21_dl_0x8600_set_circle_area(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SET_CIRCLE_AREA}, + <<"body">> => #{ + <<"type">> => 0, + <<"length">> => 2, + <<"areas">> => + [ + #{ + <<"id">> => 267, + <<"flag">> => 36, + <<"center_latitude">> => 20057, + <<"center_longitude">> => 4529, + <<"radius">> => 279, + <<"start_time">> => <<"170912103253">>, + <<"end_time">> => <<"170913103253">>, + <<"max_speed">> => 120, + <<"overspeed_duration">> => 36 + }, + #{ + <<"id">> => 355, + <<"flag">> => 36, + <<"center_latitude">> => 20057, + <<"center_longitude">> => 4529, + <<"radius">> => 132, + <<"start_time">> => <<"170912103253">>, + <<"end_time">> => <<"170913103253">>, + <<"max_speed">> => 120, + <<"overspeed_duration">> => 36 + } + ] + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + %% client get downlink command + MsgBody3 = + <<0:8, 2:8, 267:?DWORD, 36:?WORD, 20057:?DWORD, 4529:?DWORD, 279:?DWORD, + <<16#17, 16#09, 16#12, 16#10, 16#32, 16#53>>/binary, + <<16#17, 16#09, 16#13, 16#10, 16#32, 16#53>>/binary, 120:?WORD, 36:8, 355:?DWORD, + 36:?WORD, 20057:?DWORD, 4529:?DWORD, 132:?DWORD, + <<16#17, 16#09, 16#12, 16#10, 16#32, 16#53>>/binary, + <<16#17, 16#09, 16#13, 16#10, 16#32, 16#53>>/binary, 120:?WORD, 36:8>>, + MsgId3 = ?MS_SET_CIRCLE_AREA, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case22_dl_0x8601_del_circle_area(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_DEL_CIRCLE_AREA}, + <<"body">> => #{<<"length">> => 2, <<"ids">> => [3, 78]} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<2:8, 3:?DWORD, 78:?DWORD>>, + MsgId3 = ?MS_DEL_CIRCLE_AREA, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case23_dl_0x8602_set_rect_area(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SET_RECT_AREA}, + <<"body">> => #{ + <<"type">> => 0, + <<"length">> => 2, + <<"areas">> => + [ + #{ + <<"id">> => 267, + <<"flag">> => 36, + <<"lt_lat">> => 20057, + <<"lt_lng">> => 4529, + <<"rb_lat">> => 30057, + <<"rb_lng">> => 5529, + <<"start_time">> => <<"170912103253">>, + <<"end_time">> => <<"170913103253">>, + <<"max_speed">> => 120, + <<"overspeed_duration">> => 36 + }, + #{ + <<"id">> => 355, + <<"flag">> => 36, + <<"lt_lat">> => 20057, + <<"lt_lng">> => 4529, + <<"rb_lat">> => 30057, + <<"rb_lng">> => 5529, + <<"start_time">> => <<"170912103253">>, + <<"end_time">> => <<"170913103253">>, + <<"max_speed">> => 120, + <<"overspeed_duration">> => 36 + } + ] + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = + <<0:8, 2:8, 267:?DWORD, 36:?WORD, 20057:?DWORD, 4529:?DWORD, 30057:?DWORD, 5529:?DWORD, + <<16#17, 16#09, 16#12, 16#10, 16#32, 16#53>>/binary, + <<16#17, 16#09, 16#13, 16#10, 16#32, 16#53>>/binary, 120:?WORD, 36:8, 355:?DWORD, + 36:?WORD, 20057:?DWORD, 4529:?DWORD, 30057:?DWORD, 5529:?DWORD, + <<16#17, 16#09, 16#12, 16#10, 16#32, 16#53>>/binary, + <<16#17, 16#09, 16#13, 16#10, 16#32, 16#53>>/binary, 120:?WORD, 36:8>>, + MsgId3 = ?MS_SET_RECT_AREA, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case24_dl_0x8603_del_circle_area(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_DEL_RECT_AREA}, + <<"body">> => #{<<"length">> => 2, <<"ids">> => [3, 78]} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<2:8, 3:?DWORD, 78:?DWORD>>, + MsgId3 = ?MS_DEL_RECT_AREA, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case25_dl_0x8604_set_poly_area(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SET_POLY_AREA}, + <<"body">> => #{ + <<"id">> => 267, + <<"flag">> => 36, + <<"start_time">> => <<"170912103253">>, + <<"end_time">> => <<"170913103253">>, + <<"max_speed">> => 120, + <<"overspeed_duration">> => 36, + <<"length">> => 3, + <<"points">> => + [ + #{<<"lat">> => 20057, <<"lng">> => 4529}, + #{<<"lat">> => 21057, <<"lng">> => 14569}, + #{<<"lat">> => 7032, <<"lng">> => 429} + ] + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = + <<267:?DWORD, 36:?WORD, <<16#17, 16#09, 16#12, 16#10, 16#32, 16#53>>/binary, + <<16#17, 16#09, 16#13, 16#10, 16#32, 16#53>>/binary, 120:?WORD, 36:8, 3:?WORD, + 20057:?DWORD, 4529:?DWORD, 21057:?DWORD, 14569:?DWORD, 7032:?DWORD, 429:?DWORD>>, + MsgId3 = ?MS_SET_POLY_AREA, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case26_dl_0x8605_del_poly_area(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_DEL_POLY_AREA}, + <<"body">> => #{<<"length">> => 2, <<"ids">> => [3, 78]} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<2:8, 3:?DWORD, 78:?DWORD>>, + MsgId3 = ?MS_DEL_POLY_AREA, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case27_dl_0x8606_set_path(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SET_PATH}, + <<"body">> => #{ + <<"id">> => 267, + <<"flag">> => 36, + <<"start_time">> => <<"170912103253">>, + <<"end_time">> => <<"170913103253">>, + <<"length">> => 2, + <<"points">> => + [ + #{ + <<"point_id">> => 3, + <<"path_id">> => 71, + <<"point_lat">> => 7324, + <<"point_lng">> => 9732, + <<"width">> => 54, + <<"attrib">> => 23, + <<"passed">> => 0, + <<"uncovered">> => 1, + <<"max_speed">> => 132, + <<"overspeed_duration">> => 4 + }, + #{ + <<"point_id">> => 4, + <<"path_id">> => 72, + <<"point_lat">> => 7324, + <<"point_lng">> => 9732, + <<"width">> => 54, + <<"attrib">> => 23, + <<"passed">> => 0, + <<"uncovered">> => 1, + <<"max_speed">> => 169, + <<"overspeed_duration">> => 69 + } + ] + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = + <<267:?DWORD, 36:?WORD, <<16#17, 16#09, 16#12, 16#10, 16#32, 16#53>>/binary, + <<16#17, 16#09, 16#13, 16#10, 16#32, 16#53>>/binary, 2:?WORD, 3:?DWORD, 71:?DWORD, + 7324:?DWORD, 9732:?DWORD, 54:8, 23:8, 0:?WORD, 1:?WORD, 132:?WORD, 4:8, 4:?DWORD, + 72:?DWORD, 7324:?DWORD, 9732:?DWORD, 54:8, 23:8, 0:?WORD, 1:?WORD, 169:?WORD, 69:8>>, + MsgId3 = ?MS_SET_PATH, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case26_dl_0x8607_del_path(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_DEL_PATH}, + <<"body">> => #{<<"length">> => 2, <<"ids">> => [3, 78]} + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<2:8, 3:?DWORD, 78:?DWORD>>, + MsgId3 = ?MS_DEL_PATH, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case27_dl_0x8700_drive_record_capture(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + CaptureCmd = 2, + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_DRIVE_RECORD_CAPTURE}, + <<"body">> => #{ + <<"command">> => CaptureCmd, <<"param">> => base64:encode(<<"000123456789">>) + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<2:8, <<"000123456789">>/binary>>, + MsgId3 = ?MS_DRIVE_RECORD_CAPTURE, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + % client send "drive record report" + UlPacket4 = <>/binary>>, + Size4 = size(UlPacket4), + MsgId4 = ?MC_DRIVE_RECORD_REPORT, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, UlPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + timer:sleep(100), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"encrypt">> => 0, + <<"len">> => Size4, + <<"msg_id">> => ?MC_DRIVE_RECORD_REPORT, + <<"msg_sn">> => 2, + <<"phone">> => <<"000123456789">> + }, + <<"body">> => #{ + <<"seq">> => MsgSn3, + <<"command">> => CaptureCmd, + <<"data">> => base64:encode(<<"77777">>) + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + % no retrasmition of downlink message + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case28_dl_0x8701_drive_record_param_send(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + CaptureCmd = 2, + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_DRIVE_REC_PARAM_SEND}, + <<"body">> => #{ + <<"command">> => CaptureCmd, <<"param">> => base64:encode(<<"000123456789">>) + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <>/binary>>, + MsgId3 = ?MS_DRIVE_REC_PARAM_SEND, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case29_dl_0x8702_request_driver_id(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + DlCommand = #{<<"header">> => #{<<"msg_id">> => ?MS_REQ_DRIVER_ID}}, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<>>, + MsgId3 = ?MS_REQ_DRIVER_ID, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + % client send "drive record report" + UlPacket4 = + <<1:8, 16#17, 16#10, 16#12, 16#09, 16#03, 16#52, 0:8, 3:8, <<"Tom">>/binary, + <<"77778888999900001111">>/binary, 6:8, <<"org123">>/binary, 16#20, 16#30, 16#12, + 16#31>>, + Size4 = size(UlPacket4), + MsgId4 = ?MC_DRIVER_ID_REPORT, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, UlPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + timer:sleep(100), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"encrypt">> => 0, + <<"len">> => Size4, + <<"msg_id">> => ?MC_DRIVER_ID_REPORT, + <<"msg_sn">> => 2, + <<"phone">> => <<"000123456789">> + }, + <<"body">> => #{ + <<"status">> => 1, + <<"time">> => <<"171012090352">>, + <<"ic_result">> => 0, + <<"driver_name">> => <<"Tom">>, + <<"certificate">> => <<"77778888999900001111">>, + <<"organization">> => <<"org123">>, + <<"cert_expiry">> => <<"20301231">> + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case30_dl_0x8801_camera_shot(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_CAMERA_SHOT}, + <<"body">> => #{ + <<"channel_id">> => 172, + <<"command">> => 5, + <<"period">> => 2, + <<"save">> => 1, + <<"resolution">> => 8, + <<"quality">> => 3, + <<"bright">> => 4, + <<"contrast">> => 5, + <<"saturate">> => 6, + <<"chromaticity">> => 7 + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<172:8, 5:?WORD, 2:?WORD, 1:8, 8:8, 3:8, 4:8, 5:8, 6:8, 7:8>>, + MsgId3 = ?MS_CAMERA_SHOT, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + % client send "camera shot ack" + UlPacket4 = <>, + Size4 = size(UlPacket4), + MsgId4 = ?MC_CAMERA_SHOT_ACK, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, UlPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + timer:sleep(100), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"encrypt">> => 0, + <<"len">> => Size4, + <<"msg_id">> => ?MC_CAMERA_SHOT_ACK, + <<"msg_sn">> => 2, + <<"phone">> => <<"000123456789">> + }, + <<"body">> => #{ + <<"seq">> => MsgSn3, + <<"result">> => 0, + <<"length">> => 2, + <<"ids">> => [220, 221] + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + % No retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case31_dl_0x8802_mm_data_search(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_MM_DATA_SEARCH}, + <<"body">> => #{ + <<"type">> => 0, + <<"channel">> => 17, + <<"event">> => 2, + <<"start_time">> => <<"170923144607">>, + <<"end_time">> => <<"170923145826">> + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = + <<0:8, 17:8, 2:8, 16#17, 16#09, 16#23, 16#14, 16#46, 16#07, 16#17, 16#09, 16#23, 16#14, + 16#58, 16#26>>, + MsgId3 = ?MS_MM_DATA_SEARCH, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + % client send "mm data search ack" + {LocBin, LocJson} = location_report_28bytes(), + UlPacket4 = + <>, + Size4 = size(UlPacket4), + MsgId4 = ?MC_MM_DATA_SEARCH_ACK, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, UlPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + timer:sleep(100), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"encrypt">> => 0, + <<"len">> => Size4, + <<"msg_id">> => ?MC_MM_DATA_SEARCH_ACK, + <<"msg_sn">> => 2, + <<"phone">> => <<"000123456789">> + }, + <<"body">> => #{ + <<"seq">> => MsgSn3, + <<"length">> => 2, + <<"result">> => [ + #{ + <<"id">> => 25, + <<"type">> => 1, + <<"channel">> => 97, + <<"event">> => 1, + <<"location">> => LocJson + }, + #{ + <<"id">> => 26, + <<"type">> => 2, + <<"channel">> => 98, + <<"event">> => 3, + <<"location">> => LocJson + } + ] + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + %% No retrasmition of downlink message + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case32_dl_0x8803_mm_data_upload(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_MM_DATA_UPLOAD}, + <<"body">> => #{ + <<"type">> => 0, + <<"channel">> => 17, + <<"event">> => 2, + <<"start_time">> => <<"170923144607">>, + <<"end_time">> => <<"170923145826">>, + <<"delete">> => 1 + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = + <<0:8, 17:8, 2:8, 16#17, 16#09, 16#23, 16#14, 16#46, 16#07, 16#17, 16#09, 16#23, 16#14, + 16#58, 16#26, 1:8>>, + MsgId3 = ?MS_MM_DATA_UPLOAD, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case33_dl_0x8804_voice_record(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_VOICE_RECORD}, + <<"body">> => #{ + <<"command">> => 1, + <<"time">> => 2, + <<"save">> => 3, + <<"rate">> => 4 + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + % + % client get downlink command + % + MsgBody3 = <<1:8, 2:?WORD, 3:8, 4:8>>, + MsgId3 = ?MS_VOICE_RECORD, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + client_send_general_response(Socket, MsgId3, MsgSn3, PhoneBCD), + + % no retrasmition of downlink message + %% timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +t_case34_dl_0x8805_single_mm_data_ctrl(_Config) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, ?PORT, [binary, {active, false}]), + {ok, AuthCode} = client_regi_procedure(Socket), + ok = client_auth_procedure(Socket, AuthCode), + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + ok = emqx:subscribe(?JT808_UP_TOPIC), + + DlCommand = #{ + <<"header">> => #{<<"msg_id">> => ?MS_SINGLE_MM_DATA_CTRL}, + <<"body">> => #{ + <<"id">> => 30, + <<"flag">> => 40 + } + }, + emqx:publish(emqx_message:make(?JT808_DN_TOPIC, emqx_utils_json:encode(DlCommand))), + + %% client get downlink command + MsgBody3 = <<30:?DWORD, 40:8>>, + MsgId3 = ?MS_SINGLE_MM_DATA_CTRL, + MsgSn3 = 2, + Size3 = size(MsgBody3), + Header3 = + <>, + S3 = gen_packet(Header3, MsgBody3), + + %% timer:sleep(600), + {ok, Packet3} = gen_tcp:recv(Socket, 0, 500), + ?LOGT(" S3=~p", [binary_to_hex_string(S3)]), + ?LOGT("Packet3=~p", [binary_to_hex_string(Packet3)]), + ?assertEqual(S3, Packet3), + + ?LOGT("client receive command from server ~p", [S3]), + + {LocBin, LocJson} = location_report_28bytes(), + UlPacket4 = + <>, + Size4 = size(UlPacket4), + MsgId4 = ?MC_MM_DATA_SEARCH_ACK, + MsgSn4 = 2, + Header4 = + <>, + S4 = gen_packet(Header4, UlPacket4), + ?LOGT("S4 = ~p", [S4]), + + ok = gen_tcp:send(Socket, S4), + timer:sleep(100), + + {?JT808_UP_TOPIC, Payload} = receive_msg(), + ?assertEqual( + #{ + <<"header">> => #{ + <<"encrypt">> => 0, + <<"len">> => Size4, + <<"msg_id">> => ?MC_MM_DATA_SEARCH_ACK, + <<"msg_sn">> => 2, + <<"phone">> => <<"000123456789">> + }, + <<"body">> => #{ + <<"seq">> => MsgSn3, + <<"length">> => 2, + <<"result">> => [ + #{ + <<"id">> => 25, + <<"type">> => 1, + <<"channel">> => 97, + <<"event">> => 1, + <<"location">> => LocJson + }, + #{ + <<"id">> => 26, + <<"type">> => 2, + <<"channel">> => 98, + <<"event">> => 3, + <<"location">> => LocJson + } + ] + } + }, + emqx_utils_json:decode(Payload, [return_maps]) + ), + + % no retrasmition of downlink message + %%timer:sleep(10000), + {error, timeout} = gen_tcp:recv(Socket, 0, 500), + + ok = gen_tcp:close(Socket). + +receive_msg() -> + receive + {deliver, Topic, #message{payload = Payload}} -> + {Topic, Payload} + after 100 -> + {error, timeout} + end. diff --git a/apps/emqx_gateway_jt808/test/emqx_jt808_auth_http_test_server.erl b/apps/emqx_gateway_jt808/test/emqx_jt808_auth_http_test_server.erl new file mode 100644 index 000000000..22dc118b5 --- /dev/null +++ b/apps/emqx_gateway_jt808/test/emqx_jt808_auth_http_test_server.erl @@ -0,0 +1,100 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_jt808_auth_http_test_server). + +-behaviour(supervisor). +-behaviour(cowboy_handler). + +% cowboy_server callbacks +-export([init/2]). + +% supervisor callbacks +-export([init/1]). + +% API +-export([ + start_link/0, start_link/1, start_link/2, + stop/0, + set_handler/1 +]). + +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ + +start_link() -> + start_link(8991). + +start_link(Port) -> + start_link(Port, "/[...]"). + +start_link(Port, Path) -> + supervisor:start_link({local, ?MODULE}, ?MODULE, [Port, Path]). + +stop() -> + gen_server:stop(?MODULE). + +set_handler(F) when is_function(F, 2) -> + true = ets:insert(?MODULE, {handler, F}), + ok. + +%%------------------------------------------------------------------------------ +%% supervisor API +%%------------------------------------------------------------------------------ + +init([Port, Path]) -> + Dispatch = cowboy_router:compile( + [ + {'_', [{Path, ?MODULE, []}]} + ] + ), + TransOpts = #{ + socket_opts => [{port, Port}], + connection_type => supervisor + }, + ProtoOpts = #{env => #{dispatch => Dispatch}}, + + Tab = ets:new(?MODULE, [set, named_table, public]), + ets:insert(Tab, {handler, fun default_handler/2}), + + ChildSpec = ranch:child_spec(?MODULE, ranch_tcp, TransOpts, cowboy_clear, ProtoOpts), + {ok, {{one_for_one, 10, 10}, [ChildSpec]}}. + +%%------------------------------------------------------------------------------ +%% cowboy_server API +%%------------------------------------------------------------------------------ + +init(Req, State) -> + [{handler, Handler}] = ets:lookup(?MODULE, handler), + Handler(Req, State). + +%%------------------------------------------------------------------------------ +%% Internal functions +%%------------------------------------------------------------------------------ + +default_handler(Req0 = #{method := <<"POST">>, path := <<"/jt808/registry">>}, State) -> + Req = cowboy_req:reply( + 200, + #{<<"content-type">> => <<"application/json">>}, + emqx_utils_json:encode(#{code => 0, authcode => <<"123456">>}), + Req0 + ), + {ok, Req, State}; +default_handler(Req0 = #{method := <<"POST">>, path := <<"/jt808/auth">>}, State) -> + Req = cowboy_req:reply( + 200, + #{<<"content-type">> => <<"application/json">>}, + emqx_utils_json:encode(#{client_id => <<"abcdef">>}), + Req0 + ), + {ok, Req, State}; +default_handler(Req0, State) -> + Req = cowboy_req:reply( + 400, + #{<<"content-type">> => <<"text/plain">>}, + <<"">>, + Req0 + ), + {ok, Req, State}. diff --git a/apps/emqx_gateway_jt808/test/emqx_jt808_parser_SUITE.erl b/apps/emqx_gateway_jt808/test/emqx_jt808_parser_SUITE.erl new file mode 100644 index 000000000..ea7bc3748 --- /dev/null +++ b/apps/emqx_gateway_jt808/test/emqx_jt808_parser_SUITE.erl @@ -0,0 +1,721 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%%-------------------------------------------------------------------- + +-module(emqx_jt808_parser_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqx_jt808.hrl"). + +-define(LOGT(Format, Args), ct:print("TEST_SUITE: " ++ Format, Args)). + +-define(FRM_FLAG, 16#7e:8). +-define(RESERVE, 0). +-define(NO_FRAGMENT, 0). +-define(WITH_FRAGMENT, 1). +-define(NO_ENCRYPT, 0). +-define(MSG_SIZE(X), X:10 / big - integer). + +-define(word, 16 / big - integer). +-define(dword, 32 / big - integer). + +-include_lib("eunit/include/eunit.hrl"). + +all() -> + emqx_common_test_helpers:all(?MODULE). + +init_per_suite(Config) -> + Config. + +end_per_suite(Config) -> + Config. + +t_case01_register(_Config) -> + Parser = emqx_jt808_frame:initial_parse_state(#{}), + Manuf = <<"examp">>, + Model = <<"33333333333333333333">>, + DevId = <<"1234567">>, + Color = 3, + Plate = <<"ujvl239">>, + RegisterPacket = + <<58:?word, 59:?word, Manuf/binary, Model/binary, DevId/binary, Color, Plate/binary>>, + MsgId = 16#0100, + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + MsgSn = 78, + Size = size(RegisterPacket), + Header = + <>, + Stream = encode(Header, RegisterPacket), + + {ok, Map, Rest, State} = emqx_jt808_frame:parse(Stream, Parser), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size, + <<"msg_sn">> => MsgSn + }, + <<"body">> => #{ + <<"province">> => 58, + <<"city">> => 59, + <<"manufacturer">> => Manuf, + <<"model">> => Model, + <<"dev_id">> => DevId, + <<"color">> => Color, + <<"license_number">> => Plate + } + }, + Map + ), + ?assertEqual(<<>>, Rest), + ?assertEqual(#{data => <<>>, phase => searching_head_hex7e}, State), + ok. + +t_case02_register_ack(_Config) -> + % register ack + MsgId = 16#8100, + MsgSn = 35, + Seq = 22, + Result = 1, + Code = <<"abcdef">>, + DownlinkJson = #{ + <<"header">> => #{ + <<"msg_id">> => MsgId, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"msg_sn">> => MsgSn + }, + <<"body">> => #{<<"seq">> => Seq, <<"result">> => Result, <<"auth_code">> => Code} + }, + Stream = emqx_jt808_frame:serialize_pkt(DownlinkJson, #{}), + + RegisterAckPacket = <>, + + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + + Size = size(RegisterAckPacket), + Header = + <>, + StreamByHand = encode(Header, RegisterAckPacket), + + ?assertEqual(StreamByHand, Stream), + ok. + +t_case04_MC_LOCATION_REPORT(_Config) -> + Parser = emqx_jt808_frame:initial_parse_state(#{}), + Data = + <<126, 2, 0, 0, 60, 1, 136, 118, 99, 137, 114, 0, 229, 0, 0, 0, 0, 0, 4, 0, 0, 1, 49, 122, + 103, 6, 147, 104, 81, 0, 14, 0, 0, 0, 39, 23, 16, 25, 25, 53, 56, 1, 4, 0, 0, 63, 178, + 3, 2, 0, 0, 37, 4, 0, 0, 0, 0, 42, 2, 0, 0, 43, 4, 0, 0, 0, 0, 48, 1, 31, 49, 1, 0, 171, + 126>>, + {ok, Map, Rest, State} = emqx_jt808_frame:parse(Data, Parser), + ?assertEqual( + #{ + <<"header">> => + #{ + <<"encrypt">> => 0, + <<"len">> => 60, + <<"msg_id">> => 512, + <<"msg_sn">> => 229, + <<"phone">> => <<"018876638972">> + }, + <<"body">> => + #{ + <<"alarm">> => 0, + <<"altitude">> => 14, + <<"direction">> => 39, + <<"extra">> => + #{ + <<"analog">> => #{<<"ad0">> => 0, <<"ad1">> => 0}, + <<"gnss_sat_num">> => 0, + <<"io_status">> => #{<<"deep_sleep">> => 0, <<"sleep">> => 0}, + <<"mileage">> => 16306, + <<"rssi">> => 31, + <<"signal">> => + #{ + <<"abs">> => 0, + <<"air_conditioner">> => 0, + <<"brake">> => 0, + <<"cluth">> => 0, + <<"fog">> => 0, + <<"heater">> => 0, + <<"high_beam">> => 0, + <<"horn">> => 0, + <<"left_turn">> => 0, + <<"low_beam">> => 0, + <<"neutral">> => 0, + <<"retarder">> => 0, + <<"reverse">> => 0, + <<"right_turn">> => 0, + <<"side_marker">> => 0 + }, + <<"speed">> => 0 + }, + <<"latitude">> => 20019815, + <<"longitude">> => 110323793, + <<"speed">> => 0, + <<"status">> => 262144, + <<"time">> => <<"171019193538">> + } + }, + Map + ), + ?assertEqual(<<>>, Rest), + ?assertEqual(#{data => <<>>, phase => searching_head_hex7e}, State), + ok. + +t_case05_MC_BULK_LOCATION_REPORT(_Config) -> + Parser = emqx_jt808_frame:initial_parse_state(#{}), + Data = + <<126, 7, 4, 1, 57, 1, 136, 118, 99, 137, 114, 0, 231, 0, 5, 1, 0, 60, 0, 0, 0, 0, 0, 4, 0, + 3, 1, 49, 115, 43, 6, 145, 211, 81, 0, 31, 0, 166, 0, 171, 23, 8, 49, 16, 73, 51, 1, 4, + 0, 0, 60, 85, 3, 2, 0, 0, 37, 4, 0, 0, 0, 0, 42, 2, 0, 0, 43, 4, 0, 0, 0, 0, 48, 1, 0, + 49, 1, 12, 0, 60, 0, 0, 0, 0, 0, 4, 0, 3, 1, 49, 114, 74, 6, 145, 212, 2, 0, 29, 0, 222, + 0, 125, 2, 23, 8, 49, 16, 73, 56, 1, 4, 0, 0, 60, 85, 3, 2, 0, 0, 37, 4, 0, 0, 0, 0, 42, + 2, 0, 0, 43, 4, 0, 0, 0, 0, 48, 1, 0, 49, 1, 12, 0, 60, 0, 0, 0, 0, 0, 4, 0, 3, 1, 49, + 109, 222, 6, 145, 225, 80, 0, 37, 1, 169, 0, 109, 23, 8, 49, 16, 80, 17, 1, 4, 0, 0, 60, + 89, 3, 2, 0, 0, 37, 4, 0, 0, 0, 0, 42, 2, 0, 0, 43, 4, 0, 0, 0, 0, 48, 1, 0, 49, 1, 12, + 0, 60, 0, 0, 0, 0, 0, 4, 0, 3, 1, 49, 90, 235, 6, 146, 0, 24, 0, 43, 2, 136, 0, 114, 23, + 8, 49, 16, 81, 17, 1, 4, 0, 0, 60, 99, 3, 2, 0, 0, 37, 4, 0, 0, 0, 0, 42, 2, 0, 0, 43, + 4, 0, 0, 0, 0, 48, 1, 0, 49, 1, 12, 0, 60, 0, 0, 0, 0, 0, 4, 0, 3, 1, 49, 91, 120, 6, + 146, 43, 21, 0, 43, 2, 247, 0, 83, 23, 8, 49, 16, 82, 20, 1, 4, 0, 0, 60, 111, 3, 2, 0, + 0, 37, 4, 0, 0, 0, 0, 42, 2, 0, 0, 43, 4, 0, 0, 0, 0, 48, 1, 0, 49, 1, 12, 132, 126>>, + {ok, Map, Rest, State} = emqx_jt808_frame:parse(Data, Parser), + ?assertEqual( + #{ + <<"body">> => + #{ + <<"length">> => 5, + <<"location">> => + [ + #{ + <<"alarm">> => 0, + <<"altitude">> => 31, + <<"direction">> => 171, + <<"extra">> => + #{ + <<"analog">> => + #{ + <<"ad0">> => 0, + <<"ad1">> => 0 + }, + <<"gnss_sat_num">> => 12, + <<"io_status">> => + #{ + <<"deep_sleep">> => 0, + <<"sleep">> => 0 + }, + <<"mileage">> => 15445, + <<"rssi">> => 0, + <<"signal">> => + #{ + <<"abs">> => 0, + <<"air_conditioner">> => + 0, + <<"brake">> => 0, + <<"cluth">> => 0, + <<"fog">> => 0, + <<"heater">> => 0, + <<"high_beam">> => 0, + <<"horn">> => 0, + <<"left_turn">> => 0, + <<"low_beam">> => 0, + <<"neutral">> => 0, + <<"retarder">> => 0, + <<"reverse">> => 0, + <<"right_turn">> => 0, + <<"side_marker">> => 0 + }, + <<"speed">> => 0 + }, + <<"latitude">> => 20017963, + <<"longitude">> => 110220113, + <<"speed">> => 166, + <<"status">> => 262147, + <<"time">> => <<"170831104933">> + }, + #{ + <<"alarm">> => 0, + <<"altitude">> => 29, + <<"direction">> => 126, + <<"extra">> => + #{ + <<"analog">> => + #{ + <<"ad0">> => 0, + <<"ad1">> => 0 + }, + <<"gnss_sat_num">> => 12, + <<"io_status">> => + #{ + <<"deep_sleep">> => 0, + <<"sleep">> => 0 + }, + <<"mileage">> => 15445, + <<"rssi">> => 0, + <<"signal">> => + #{ + <<"abs">> => 0, + <<"air_conditioner">> => + 0, + <<"brake">> => 0, + <<"cluth">> => 0, + <<"fog">> => 0, + <<"heater">> => 0, + <<"high_beam">> => 0, + <<"horn">> => 0, + <<"left_turn">> => 0, + <<"low_beam">> => 0, + <<"neutral">> => 0, + <<"retarder">> => 0, + <<"reverse">> => 0, + <<"right_turn">> => 0, + <<"side_marker">> => 0 + }, + <<"speed">> => 0 + }, + <<"latitude">> => 20017738, + <<"longitude">> => 110220290, + <<"speed">> => 222, + <<"status">> => 262147, + <<"time">> => <<"170831104938">> + }, + #{ + <<"alarm">> => 0, + <<"altitude">> => 37, + <<"direction">> => 109, + <<"extra">> => + #{ + <<"analog">> => + #{ + <<"ad0">> => 0, + <<"ad1">> => 0 + }, + <<"gnss_sat_num">> => 12, + <<"io_status">> => + #{ + <<"deep_sleep">> => 0, + <<"sleep">> => 0 + }, + <<"mileage">> => 15449, + <<"rssi">> => 0, + <<"signal">> => + #{ + <<"abs">> => 0, + <<"air_conditioner">> => + 0, + <<"brake">> => 0, + <<"cluth">> => 0, + <<"fog">> => 0, + <<"heater">> => 0, + <<"high_beam">> => 0, + <<"horn">> => 0, + <<"left_turn">> => 0, + <<"low_beam">> => 0, + <<"neutral">> => 0, + <<"retarder">> => 0, + <<"reverse">> => 0, + <<"right_turn">> => 0, + <<"side_marker">> => 0 + }, + <<"speed">> => 0 + }, + <<"latitude">> => 20016606, + <<"longitude">> => 110223696, + <<"speed">> => 425, + <<"status">> => 262147, + <<"time">> => <<"170831105011">> + }, + #{ + <<"alarm">> => 0, + <<"altitude">> => 43, + <<"direction">> => 114, + <<"extra">> => + #{ + <<"analog">> => + #{ + <<"ad0">> => 0, + <<"ad1">> => 0 + }, + <<"gnss_sat_num">> => 12, + <<"io_status">> => + #{ + <<"deep_sleep">> => 0, + <<"sleep">> => 0 + }, + <<"mileage">> => 15459, + <<"rssi">> => 0, + <<"signal">> => + #{ + <<"abs">> => 0, + <<"air_conditioner">> => + 0, + <<"brake">> => 0, + <<"cluth">> => 0, + <<"fog">> => 0, + <<"heater">> => 0, + <<"high_beam">> => 0, + <<"horn">> => 0, + <<"left_turn">> => 0, + <<"low_beam">> => 0, + <<"neutral">> => 0, + <<"retarder">> => 0, + <<"reverse">> => 0, + <<"right_turn">> => 0, + <<"side_marker">> => 0 + }, + <<"speed">> => 0 + }, + <<"latitude">> => 20011755, + <<"longitude">> => 110231576, + <<"speed">> => 648, + <<"status">> => 262147, + <<"time">> => <<"170831105111">> + }, + #{ + <<"alarm">> => 0, + <<"altitude">> => 43, + <<"direction">> => 83, + <<"extra">> => + #{ + <<"analog">> => + #{ + <<"ad0">> => 0, + <<"ad1">> => 0 + }, + <<"gnss_sat_num">> => 12, + <<"io_status">> => + #{ + <<"deep_sleep">> => 0, + <<"sleep">> => 0 + }, + <<"mileage">> => 15471, + <<"rssi">> => 0, + <<"signal">> => + #{ + <<"abs">> => 0, + <<"air_conditioner">> => + 0, + <<"brake">> => 0, + <<"cluth">> => 0, + <<"fog">> => 0, + <<"heater">> => 0, + <<"high_beam">> => 0, + <<"horn">> => 0, + <<"left_turn">> => 0, + <<"low_beam">> => 0, + <<"neutral">> => 0, + <<"retarder">> => 0, + <<"reverse">> => 0, + <<"right_turn">> => 0, + <<"side_marker">> => 0 + }, + <<"speed">> => 0 + }, + <<"latitude">> => 20011896, + <<"longitude">> => 110242581, + <<"speed">> => 759, + <<"status">> => 262147, + <<"time">> => <<"170831105214">> + } + ], + <<"type">> => 1 + }, + <<"header">> => + #{ + <<"encrypt">> => 0, + <<"len">> => 313, + <<"msg_id">> => 1796, + <<"msg_sn">> => 231, + <<"phone">> => <<"018876638972">> + } + }, + Map + ), + ?assertEqual(<<>>, Rest), + ?assertEqual(#{data => <<>>, phase => searching_head_hex7e}, State), + ok. + +t_case10_segmented_packet(_Config) -> + Parser = emqx_jt808_frame:initial_parse_state(#{}), + Manuf = <<"examp">>, + Model = <<"33333333333333333333">>, + DevId = <<"1234567">>, + Color = 3, + Plate = <<"ujvl239">>, + RegisterPacket = + <<58:?word, 59:?word, Manuf/binary, Model/binary, DevId/binary, Color, Plate/binary>>, + MsgId = 16#0100, + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + MsgSn = 78, + Size = size(RegisterPacket), + Header = + <>, + Stream = encode(Header, RegisterPacket), + + <> = Stream, + {more, Parser2} = emqx_jt808_frame:parse(Part1, Parser), + ?assertMatch(#{phase := escaping_hex7d}, Parser2), + {ok, Map, Rest, State} = emqx_jt808_frame:parse(Part2, Parser2), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size, + <<"msg_sn">> => MsgSn + }, + <<"body">> => #{ + <<"province">> => 58, + <<"city">> => 59, + <<"manufacturer">> => Manuf, + <<"model">> => Model, + <<"dev_id">> => DevId, + <<"color">> => Color, + <<"license_number">> => Plate + } + }, + Map + ), + ?assertEqual(<<>>, Rest), + ?assertEqual(#{data => <<>>, phase => searching_head_hex7e}, State), + ok. + +t_case11_prefix_register(_Config) -> + Parser = emqx_jt808_frame:initial_parse_state(#{}), + Manuf = <<"examp">>, + Model = <<"33333333333333333333">>, + DevId = <<"1234567">>, + Color = 3, + Plate = <<"ujvl239">>, + RegisterPacket = + <<58:?word, 59:?word, Manuf/binary, Model/binary, DevId/binary, Color, Plate/binary>>, + MsgId = 16#0100, + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + MsgSn = 78, + Size = size(RegisterPacket), + Header = + <>, + Stream = encode(Header, RegisterPacket), + MessBinary = <<0, 1, 2, 3, Stream/binary>>, + ?LOGT("MessBinary=~p", [binary_to_hex_string(MessBinary)]), + + {ok, Map, Rest, State} = emqx_jt808_frame:parse(MessBinary, Parser), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size, + <<"msg_sn">> => MsgSn + }, + <<"body">> => #{ + <<"province">> => 58, + <<"city">> => 59, + <<"manufacturer">> => Manuf, + <<"model">> => Model, + <<"dev_id">> => DevId, + <<"color">> => Color, + <<"license_number">> => Plate + } + }, + Map + ), + ?assertEqual(<<>>, Rest), + ?assertEqual(#{data => <<>>, phase => searching_head_hex7e}, State), + ok. + +t_case12_0x7e_in_message(_Config) -> + Parser = emqx_jt808_frame:initial_parse_state(#{}), + Manuf = <<"examp">>, + Model = <<"33333333333333333333">>, + DevId = <<"1234567">>, + Color = 3, + Plate = <<"ujvl239">>, + % pay attention to this + AlarmInt = 16#7e, + AlarmDigit = 16#7d, + RegisterPacket = + <>, + MsgId = 16#0100, + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + MsgSn = 78, + Size = size(RegisterPacket), + Header = + <>, + Stream = encode(Header, RegisterPacket), + + {ok, Map, Rest, State} = emqx_jt808_frame:parse(Stream, Parser), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size, + <<"msg_sn">> => MsgSn + }, + <<"body">> => #{ + <<"province">> => AlarmInt, + <<"city">> => AlarmDigit, + <<"manufacturer">> => Manuf, + <<"model">> => Model, + <<"dev_id">> => DevId, + <<"color">> => Color, + <<"license_number">> => Plate + } + }, + Map + ), + ?assertEqual(<<>>, Rest), + ?assertEqual(#{data => <<>>, phase => searching_head_hex7e}, State), + ok. + +t_case13_partial_0x7d_in_message(_Config) -> + Parser = emqx_jt808_frame:initial_parse_state(#{}), + Manuf = <<"examp">>, + Model = <<"33333333333333333333">>, + DevId = <<"1234567">>, + Color = 3, + Plate = <<"ujvl239">>, + % pay attention to this + AlarmInt = 16#7e, + AlarmDigit = 16#7d, + RegisterPacket = + <>, + MsgId = 16#0100, + PhoneBCD = <<16#00, 16#01, 16#23, 16#45, 16#67, 16#89>>, + MsgSn = 78, + Size = size(RegisterPacket), + Header = + <>, + Stream = encode(Header, RegisterPacket), + + <> = Stream, + <<_:14/binary, 16#7d:8>> = Part1, + + {more, Parser2} = emqx_jt808_frame:parse(Part1, Parser), + ?assertMatch(#{phase := escaping_hex7d}, Parser2), + {ok, Map, Rest, State} = emqx_jt808_frame:parse(Part2, Parser2), + ?assertEqual( + #{ + <<"header">> => #{ + <<"msg_id">> => MsgId, + <<"encrypt">> => ?NO_ENCRYPT, + <<"phone">> => <<"000123456789">>, + <<"len">> => Size, + <<"msg_sn">> => MsgSn + }, + <<"body">> => #{ + <<"province">> => AlarmInt, + <<"city">> => AlarmDigit, + <<"manufacturer">> => Manuf, + <<"model">> => Model, + <<"dev_id">> => DevId, + <<"color">> => Color, + <<"license_number">> => Plate + } + }, + Map + ), + ?assertEqual(<<>>, Rest), + ?assertEqual(#{data => <<>>, phase => searching_head_hex7e}, State), + ok. + +t_case14_custome_location_data(_) -> + Bin = + <<126, 2, 0, 0, 64, 1, 65, 72, 7, 53, 80, 3, 106, 0, 0, 0, 0, 0, 0, 0, 1, 1, 195, 232, 22, + 6, 89, 10, 16, 0, 0, 0, 0, 0, 0, 33, 6, 34, 9, 21, 69, 1, 4, 0, 0, 0, 0, 48, 1, 23, 49, + 1, 10, 235, 22, 0, 12, 0, 178, 137, 134, 4, 66, 25, 25, 144, 147, 71, 153, 0, 6, 0, 137, + 255, 255, 255, 255, 36, 126>>, + Parser = emqx_jt808_frame:initial_parse_state(#{}), + {ok, Packet, Rest, State} = emqx_jt808_frame:parse(Bin, Parser), + ?assertEqual(<<>>, Rest), + ?assertEqual(#{data => <<>>, phase => searching_head_hex7e}, State), + _ = emqx_utils_json:encode(Packet). + +t_case14_reserved_location_data(_) -> + Bin = + <<126, 2, 0, 0, 49, 1, 145, 114, 3, 130, 104, 2, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1, 211, 181, + 215, 6, 51, 228, 71, 0, 4, 0, 0, 0, 138, 34, 4, 25, 22, 5, 70, 1, 4, 0, 0, 0, 0, 5, 3, + 0, 0, 0, 48, 1, 31, 49, 1, 15, 130, 2, 0, 125, 2, 23, 126>>, + Parser = emqx_jt808_frame:initial_parse_state(#{}), + {ok, Packet, Rest, State} = emqx_jt808_frame:parse(Bin, Parser), + ?assertEqual(<<>>, Rest), + ?assertEqual(#{data => <<>>, phase => searching_head_hex7e}, State), + _ = emqx_utils_json:encode(Packet). + +t_case15_custome_client_query_ack(_) -> + Bin = + <<126, 1, 4, 2, 17, 78, 244, 128, 18, 137, 25, 0, 43, 0, 42, 52, 0, 0, 0, 1, 4, 0, 0, 0, + 180, 0, 0, 0, 8, 4, 0, 0, 1, 44, 0, 0, 0, 16, 5, 99, 109, 110, 101, 116, 0, 0, 0, 17, 0, + 0, 0, 0, 18, 0, 0, 0, 0, 19, 12, 52, 55, 46, 57, 57, 46, 57, 56, 46, 50, 53, 52, 0, 0, + 0, 23, 7, 48, 46, 48, 46, 48, 46, 48, 0, 0, 0, 24, 4, 0, 0, 31, 249, 0, 0, 0, 32, 4, 0, + 0, 0, 0, 0, 0, 0, 39, 4, 0, 0, 0, 30, 0, 0, 0, 41, 4, 0, 0, 0, 30, 0, 0, 0, 48, 4, 0, 0, + 0, 20, 0, 0, 0, 64, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 68, 1, 0, 0, 0, 0, 69, 4, 0, 0, 0, + 0, 0, 0, 0, 72, 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 0, 85, 4, 0, 0, 0, 120, 0, 0, 0, 86, 4, + 0, 0, 0, 20, 0, 0, 0, 93, 2, 0, 30, 0, 0, 0, 128, 4, 0, 0, 136, 203, 0, 0, 0, 129, 2, 0, + 0, 0, 0, 0, 130, 2, 0, 0, 0, 0, 0, 131, 0, 0, 0, 0, 132, 1, 0, 0, 0, 240, 1, 2, 16, 0, + 0, 0, 240, 2, 2, 3, 57, 0, 0, 240, 3, 1, 41, 0, 0, 240, 4, 2, 0, 116, 0, 0, 240, 5, 4, + 0, 53, 111, 127, 0, 0, 240, 6, 4, 0, 3, 51, 18, 0, 0, 240, 7, 1, 1, 0, 0, 240, 8, 2, 0, + 30, 0, 0, 240, 10, 2, 0, 30, 0, 0, 240, 11, 6, 0, 0, 0, 0, 0, 0, 0, 0, 240, 12, 1, 1, 0, + 0, 240, 13, 18, 117, 112, 103, 114, 97, 100, 101, 46, 99, 112, 115, 100, 110, 97, 46, + 99, 111, 109, 0, 0, 240, 14, 4, 0, 0, 123, 40, 0, 0, 240, 15, 1, 1, 0, 0, 240, 16, 3, 0, + 16, 3, 0, 0, 240, 17, 2, 0, 32, 0, 0, 240, 18, 3, 0, 22, 40, 0, 0, 240, 19, 4, 0, 17, + 25, 32, 0, 0, 240, 20, 3, 55, 0, 10, 0, 0, 240, 21, 1, 50, 0, 0, 240, 22, 2, 3, 25, 0, + 0, 240, 23, 2, 30, 50, 0, 0, 240, 24, 21, 48, 44, 48, 44, 53, 56, 46, 50, 49, 53, 46, + 53, 48, 46, 53, 48, 44, 54, 48, 48, 56, 0, 0, 241, 1, 1, 160, 0, 0, 241, 2, 1, 130, 0, + 0, 255, 148, 93, 14, 22, 1, 56, 57, 56, 54, 48, 52, 57, 53, 49, 48, 50, 49, 55, 48, 51, + 48, 52, 53, 53, 56, 17, 2, 52, 54, 48, 48, 56, 49, 53, 56, 51, 52, 48, 52, 53, 53, 56, + 4, 3, 0, 1, 3, 5, 0, 3, 6, 0, 3, 7, 0, 5, 8, 3, 0, 1, 5, 8, 14, 0, 0, 5, 8, 15, 0, 0, 5, + 8, 41, 0, 0, 5, 8, 48, 0, 0, 5, 8, 42, 0, 0, 5, 8, 43, 0, 0, 5, 8, 4, 0, 0, 138, 126>>, + Parser = emqx_jt808_frame:initial_parse_state(#{}), + {ok, Packet, Rest, State} = emqx_jt808_frame:parse(Bin, Parser), + ?assertEqual(<<>>, Rest), + ?assertEqual(#{data => <<>>, phase => searching_head_hex7e}, State), + _ = emqx_utils_json:encode(Packet). + +encode(Header, Body) -> + S1 = <
>, + Crc = make_crc(S1, undefined), + S2 = do_escape(<>), + Stream = <<16#7e:8, S2/binary, 16#7e:8>>, + %?LOGT("encode a packet=~p", [binary_to_hex_string(Stream)]), + Stream. + +make_crc(<<>>, Xor) -> + ?LOGT("crc is ~p", [Xor]), + Xor; +make_crc(<>, undefined) -> + make_crc(Rest, C); +make_crc(<>, Xor) -> + make_crc(Rest, C bxor Xor). + +do_escape(Binary) -> + do_escape(Binary, <<>>). + +do_escape(<<>>, Acc) -> + Acc; +do_escape(<<16#7e, Rest/binary>>, Acc) -> + do_escape(Rest, <>); +do_escape(<<16#7d, Rest/binary>>, Acc) -> + do_escape(Rest, <>); +do_escape(<>, Acc) -> + do_escape(Rest, <>). + +binary_to_hex_string(Data) -> + lists:flatten([io_lib:format("~2.16.0B ", [X]) || <> <= Data]). diff --git a/apps/emqx_gateway_lwm2m/README.md b/apps/emqx_gateway_lwm2m/README.md index 678d74dcf..b69ae44db 100644 --- a/apps/emqx_gateway_lwm2m/README.md +++ b/apps/emqx_gateway_lwm2m/README.md @@ -5,7 +5,7 @@ is a protocol designed for IoT devices and machine-to-machine communication. It is a lightweight protocol that supports devices with limited processing power and memory. -The **LwM2M Gateway** in EMQX can accept LwM2M clients and translate theirevents +The **LwM2M Gateway** in EMQX can accept LwM2M clients and translate their events and messages into MQTT Publish messages. In the current implementation, it has the following limitations: @@ -15,7 +15,7 @@ In the current implementation, it has the following limitations: ## Quick Start -In EMQX 5.0, LwM2M gateways can be configured and enabled through the Dashboard. +In EMQX 5.0, LwM2M gateway can be configured and enabled through the Dashboard. It can also be enabled via the HTTP API, and emqx.conf e.g, In emqx.conf: @@ -52,7 +52,6 @@ gateway.lwm2m { > 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. -::: ## Object definations diff --git a/apps/emqx_gateway_lwm2m/rebar.config b/apps/emqx_gateway_lwm2m/rebar.config index c8675c3ba..28c3c85e5 100644 --- a/apps/emqx_gateway_lwm2m/rebar.config +++ b/apps/emqx_gateway_lwm2m/rebar.config @@ -1,3 +1,4 @@ +%% -*- mode: erlang -*- {erl_opts, [debug_info]}. {deps, [ {emqx, {path, "../../apps/emqx"}}, {emqx_gateway, {path, "../../apps/emqx_gateway"}} diff --git a/apps/emqx_gateway_lwm2m/src/emqx_gateway_lwm2m.app.src b/apps/emqx_gateway_lwm2m/src/emqx_gateway_lwm2m.app.src index e5afd7871..7e502632c 100644 --- a/apps/emqx_gateway_lwm2m/src/emqx_gateway_lwm2m.app.src +++ b/apps/emqx_gateway_lwm2m/src/emqx_gateway_lwm2m.app.src @@ -1,3 +1,4 @@ +%% -*- mode: erlang -*- {application, emqx_gateway_lwm2m, [ {description, "LwM2M Gateway"}, {vsn, "0.1.3"}, diff --git a/apps/emqx_gateway_lwm2m/src/emqx_lwm2m_channel.erl b/apps/emqx_gateway_lwm2m/src/emqx_lwm2m_channel.erl index 0f225b59b..cd36b94e6 100644 --- a/apps/emqx_gateway_lwm2m/src/emqx_lwm2m_channel.erl +++ b/apps/emqx_gateway_lwm2m/src/emqx_lwm2m_channel.erl @@ -15,6 +15,7 @@ %%-------------------------------------------------------------------- -module(emqx_lwm2m_channel). +-behaviour(emqx_gateway_channel). -include("emqx_lwm2m.hrl"). -include_lib("emqx/include/emqx.hrl"). diff --git a/apps/emqx_gateway_mqttsn/src/emqx_gateway_mqttsn.app.src b/apps/emqx_gateway_mqttsn/src/emqx_gateway_mqttsn.app.src index c2f6d642b..da7c45133 100644 --- a/apps/emqx_gateway_mqttsn/src/emqx_gateway_mqttsn.app.src +++ b/apps/emqx_gateway_mqttsn/src/emqx_gateway_mqttsn.app.src @@ -1,3 +1,4 @@ +%% -*- mode: erlang -*- {application, emqx_gateway_mqttsn, [ {description, "MQTT-SN Gateway"}, {vsn, "0.1.5"}, diff --git a/apps/emqx_gateway_stomp/rebar.config b/apps/emqx_gateway_stomp/rebar.config index cfeb0a195..55e6f52f2 100644 --- a/apps/emqx_gateway_stomp/rebar.config +++ b/apps/emqx_gateway_stomp/rebar.config @@ -1,3 +1,4 @@ +%% -*- mode: erlang -*- {erl_opts, [debug_info]}. {deps, [ {emqx, {path, "../../apps/emqx"}}, diff --git a/apps/emqx_gateway_stomp/src/emqx_gateway_stomp.app.src b/apps/emqx_gateway_stomp/src/emqx_gateway_stomp.app.src index 22dd4efde..01d18b607 100644 --- a/apps/emqx_gateway_stomp/src/emqx_gateway_stomp.app.src +++ b/apps/emqx_gateway_stomp/src/emqx_gateway_stomp.app.src @@ -1,3 +1,4 @@ +%% -*- mode: erlang -*- {application, emqx_gateway_stomp, [ {description, "Stomp Gateway"}, {vsn, "0.1.3"}, diff --git a/apps/emqx_gateway_stomp/src/emqx_stomp_schema.erl b/apps/emqx_gateway_stomp/src/emqx_stomp_schema.erl index b1c6a92e2..4941634b7 100644 --- a/apps/emqx_gateway_stomp/src/emqx_stomp_schema.erl +++ b/apps/emqx_gateway_stomp/src/emqx_stomp_schema.erl @@ -35,7 +35,7 @@ fields(stomp_frame) -> non_neg_integer(), #{ default => 10, - desc => ?DESC(stom_frame_max_headers) + desc => ?DESC(stomp_frame_max_headers) } )}, {max_headers_length, @@ -51,7 +51,7 @@ fields(stomp_frame) -> integer(), #{ default => 65536, - desc => ?DESC(stom_frame_max_body_length) + desc => ?DESC(stomp_frame_max_body_length) } )} ]. diff --git a/apps/emqx_machine/priv/reboot_lists.eterm b/apps/emqx_machine/priv/reboot_lists.eterm index 190ef1afa..ccb61d762 100644 --- a/apps/emqx_machine/priv/reboot_lists.eterm +++ b/apps/emqx_machine/priv/reboot_lists.eterm @@ -128,6 +128,7 @@ emqx_audit, emqx_gateway_gbt32960, emqx_gateway_ocpp, + emqx_gateway_jt808, emqx_bridge_syskeeper ], %% must always be of type `load' diff --git a/changes/ee/feat-11883.en.md b/changes/ee/feat-11883.en.md new file mode 100644 index 000000000..9cfacd42b --- /dev/null +++ b/changes/ee/feat-11883.en.md @@ -0,0 +1 @@ +Introduced a new gateway for vehicles to access EMQX through the JT/T 808 protocol. diff --git a/changes/ee/feat-11885.en.md b/changes/ee/feat-11885.en.md new file mode 100644 index 000000000..d5b40cd2e --- /dev/null +++ b/changes/ee/feat-11885.en.md @@ -0,0 +1 @@ +Introduced a new gateway for Electric vehicle (EV) charging stations to access EMQX through the OCPP (Open Charge Point Protocol). diff --git a/mix.exs b/mix.exs index 23cef80e3..8a0f93dd6 100644 --- a/mix.exs +++ b/mix.exs @@ -218,6 +218,7 @@ defmodule EMQXUmbrella.MixProject do :emqx_audit, :emqx_gateway_gbt32960, :emqx_gateway_ocpp, + :emqx_gateway_jt808, :emqx_bridge_syskeeper ]) end diff --git a/rebar.config.erl b/rebar.config.erl index 93ad9fa99..6bb2fb985 100644 --- a/rebar.config.erl +++ b/rebar.config.erl @@ -113,6 +113,7 @@ 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("apps/emqx_gateway_ocpp") -> false; +is_community_umbrella_app("apps/emqx_gateway_jt808") -> false; is_community_umbrella_app("apps/emqx_bridge_syskeeper") -> false; is_community_umbrella_app(_) -> true. diff --git a/rel/i18n/emqx_gateway_schema.hocon b/rel/i18n/emqx_gateway_schema.hocon index 2f0a012f2..1a04d57ac 100644 --- a/rel/i18n/emqx_gateway_schema.hocon +++ b/rel/i18n/emqx_gateway_schema.hocon @@ -44,7 +44,7 @@ gateway_common_listener_enable.desc: """Enable the listener.""" gateway_common_listener_enable_authn.desc: -"""Set true (default) to enable client authentication on this listener. +"""Set true (default) to enable client authentication on this listener. When set to false clients will be allowed to connect without authentication.""" gateway_common_listener_max_conn_rate.desc: @@ -62,9 +62,10 @@ then the client actually subscribes to the topic `some_tenant/t`. Similarly, if another client B (connected to the same listener as the client A) sends a message to topic `t`, the message is routed to all the clients subscribed `some_tenant/t`, so client A will receive the message, with topic name `t`. Set to `""` to disable the feature. -Variables in mountpoint string:
+Supported placeholders in mountpoint string:
- ${clientid}: clientid
- - ${username}: username""" + - ${username}: username
+ - ${endpoint_name}: endpoint name""" listener_name_to_settings_map.desc: """A map from listener names to listener settings.""" diff --git a/rel/i18n/emqx_jt808_schema.hocon b/rel/i18n/emqx_jt808_schema.hocon new file mode 100644 index 000000000..cd853df4d --- /dev/null +++ b/rel/i18n/emqx_jt808_schema.hocon @@ -0,0 +1,30 @@ +emqx_jt808_schema { + +jt808_frame_max_length.desc: +"""The maximum length of the JT/T 808 frame.""" + +jt808_allow_anonymous.desc: +"""Allow anonymous access to the JT/T 808 Gateway.""" + +registry_url.desc +"""The JT/T 808 device registry central URL.""" + +authentication_url.desc +"""The JT/T 808 device authentication central URL.""" + +jt808_up_topic.desc +"""The topic of the JT/T 808 protocol upstream message.""" + +jt808_dn_topic.desc +"""The topic of the JT/T 808 protocol downstream message.""" + +retry_interval.desc: +"""Re-send time interval""" + +max_retry_times.desc: +"""Re-send max times""" + +message_queue_len.desc: +"""Max message queue length""" + +} diff --git a/rel/i18n/emqx_stomp_schema.hocon b/rel/i18n/emqx_stomp_schema.hocon index 05d5b9d18..36220d158 100644 --- a/rel/i18n/emqx_stomp_schema.hocon +++ b/rel/i18n/emqx_stomp_schema.hocon @@ -1,16 +1,16 @@ emqx_stomp_schema { -stom_frame_max_body_length.desc: -"""Maximum number of bytes of Body allowed per Stomp packet""" - -stom_frame_max_headers.desc: -"""The maximum number of Header""" - stomp.desc: """The Stomp Gateway configuration. This gateway supports v1.2/1.1/1.0""" +stomp_frame_max_headers.desc: +"""The maximum number of Header""" + stomp_frame_max_headers_length.desc: """The maximum string length of the Header Value""" +stomp_frame_max_body_length.desc: +"""Maximum number of bytes of Body allowed per Stomp packet""" + }