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