init project
This commit is contained in:
commit
d9833e6340
26
.args.yaml
Normal file
26
.args.yaml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
metrics:
|
||||||
|
-
|
||||||
|
key: serial
|
||||||
|
type: string
|
||||||
|
label: "智慧照明设备id"
|
||||||
|
config:
|
||||||
|
default: ""
|
||||||
|
maxlen: 20
|
||||||
|
-
|
||||||
|
key: device_uuid
|
||||||
|
type: string
|
||||||
|
label: "设备的uuid"
|
||||||
|
config:
|
||||||
|
default: ""
|
||||||
|
maxlen: 50
|
||||||
|
|
||||||
|
boot: "./boot.sh"
|
||||||
|
# stop can be used for execute stop action
|
||||||
|
stop: "./stop.sh"
|
||||||
|
|
||||||
|
# coll / collect for collection, has data upload
|
||||||
|
# stream has transform
|
||||||
|
# up / upload for data upload
|
||||||
|
service_type: "coll"
|
||||||
|
|
||||||
|
run_as_root: true
|
||||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.rebar3
|
||||||
|
_*
|
||||||
|
.eunit
|
||||||
|
*.o
|
||||||
|
*.beam
|
||||||
|
*.plt
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.erlang.cookie
|
||||||
|
ebin
|
||||||
|
log
|
||||||
|
erl_crash.dump
|
||||||
|
.rebar
|
||||||
|
logs
|
||||||
|
_build
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
rebar3.crashdump
|
||||||
|
*~
|
||||||
|
config/sys.config
|
||||||
3
Dockerfile
Normal file
3
Dockerfile
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
FROM erlang:25.3
|
||||||
|
|
||||||
|
CMD /data/power_gateway/bin/power_gateway foreground
|
||||||
191
LICENSE
Normal file
191
LICENSE
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2023, anlicheng <anlicheng@xywy.com>.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
9
README.md
Normal file
9
README.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
power_gateway
|
||||||
|
=====
|
||||||
|
|
||||||
|
An OTP application
|
||||||
|
|
||||||
|
Build
|
||||||
|
-----
|
||||||
|
|
||||||
|
$ rebar3 compile
|
||||||
535
apps/power_gateway/include/emqtt.hrl
Normal file
535
apps/power_gateway/include/emqtt.hrl
Normal file
@ -0,0 +1,535 @@
|
|||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-ifndef(EMQTT_HRL).
|
||||||
|
-define(EMQTT_HRL, true).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT Protocol Version and Names
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(MQTT_PROTO_V3, 3).
|
||||||
|
-define(MQTT_PROTO_V4, 4).
|
||||||
|
-define(MQTT_PROTO_V5, 5).
|
||||||
|
|
||||||
|
-define(PROTOCOL_NAMES, [
|
||||||
|
{?MQTT_PROTO_V3, <<"MQIsdp">>},
|
||||||
|
{?MQTT_PROTO_V4, <<"MQTT">>},
|
||||||
|
{?MQTT_PROTO_V5, <<"MQTT">>}]).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT QoS Levels
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(QOS_0, 0). %% At most once
|
||||||
|
-define(QOS_1, 1). %% At least once
|
||||||
|
-define(QOS_2, 2). %% Exactly once
|
||||||
|
|
||||||
|
-define(IS_QOS(I), (I >= ?QOS_0 andalso I =< ?QOS_2)).
|
||||||
|
|
||||||
|
-define(QOS_I(Name),
|
||||||
|
begin
|
||||||
|
(case Name of
|
||||||
|
?QOS_0 -> ?QOS_0;
|
||||||
|
qos0 -> ?QOS_0;
|
||||||
|
at_most_once -> ?QOS_0;
|
||||||
|
?QOS_1 -> ?QOS_1;
|
||||||
|
qos1 -> ?QOS_1;
|
||||||
|
at_least_once -> ?QOS_1;
|
||||||
|
?QOS_2 -> ?QOS_2;
|
||||||
|
qos2 -> ?QOS_2;
|
||||||
|
exactly_once -> ?QOS_2
|
||||||
|
end)
|
||||||
|
end).
|
||||||
|
|
||||||
|
-define(IS_QOS_NAME(I),
|
||||||
|
(I =:= qos0 orelse I =:= at_most_once orelse
|
||||||
|
I =:= qos1 orelse I =:= at_least_once orelse
|
||||||
|
I =:= qos2 orelse I =:= exactly_once)).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Maximum ClientId Length.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(MAX_CLIENTID_LEN, 65535).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT Control Packet Types
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(RESERVED, 0). %% Reserved
|
||||||
|
-define(CONNECT, 1). %% Client request to connect to Server
|
||||||
|
-define(CONNACK, 2). %% Server to Client: Connect acknowledgment
|
||||||
|
-define(PUBLISH, 3). %% Publish message
|
||||||
|
-define(PUBACK, 4). %% Publish acknowledgment
|
||||||
|
-define(PUBREC, 5). %% Publish received (assured delivery part 1)
|
||||||
|
-define(PUBREL, 6). %% Publish release (assured delivery part 2)
|
||||||
|
-define(PUBCOMP, 7). %% Publish complete (assured delivery part 3)
|
||||||
|
-define(SUBSCRIBE, 8). %% Client subscribe request
|
||||||
|
-define(SUBACK, 9). %% Server Subscribe acknowledgment
|
||||||
|
-define(UNSUBSCRIBE, 10). %% Unsubscribe request
|
||||||
|
-define(UNSUBACK, 11). %% Unsubscribe acknowledgment
|
||||||
|
-define(PINGREQ, 12). %% PING request
|
||||||
|
-define(PINGRESP, 13). %% PING response
|
||||||
|
-define(DISCONNECT, 14). %% Client or Server is disconnecting
|
||||||
|
-define(AUTH, 15). %% Authentication exchange
|
||||||
|
|
||||||
|
-define(TYPE_NAMES, [
|
||||||
|
'CONNECT',
|
||||||
|
'CONNACK',
|
||||||
|
'PUBLISH',
|
||||||
|
'PUBACK',
|
||||||
|
'PUBREC',
|
||||||
|
'PUBREL',
|
||||||
|
'PUBCOMP',
|
||||||
|
'SUBSCRIBE',
|
||||||
|
'SUBACK',
|
||||||
|
'UNSUBSCRIBE',
|
||||||
|
'UNSUBACK',
|
||||||
|
'PINGREQ',
|
||||||
|
'PINGRESP',
|
||||||
|
'DISCONNECT',
|
||||||
|
'AUTH']).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT V3.1.1 Connect Return Codes
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(CONNACK_ACCEPT, 0). %% Connection accepted
|
||||||
|
-define(CONNACK_PROTO_VER, 1). %% Unacceptable protocol version
|
||||||
|
-define(CONNACK_INVALID_ID, 2). %% Client Identifier is correct UTF-8 but not allowed by the Server
|
||||||
|
-define(CONNACK_SERVER, 3). %% Server unavailable
|
||||||
|
-define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed
|
||||||
|
-define(CONNACK_AUTH, 5). %% Client is not authorized to connect
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT V5.0 Reason Codes
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(RC_SUCCESS, 16#00).
|
||||||
|
-define(RC_NORMAL_DISCONNECTION, 16#00).
|
||||||
|
-define(RC_GRANTED_QOS_0, 16#00).
|
||||||
|
-define(RC_GRANTED_QOS_1, 16#01).
|
||||||
|
-define(RC_GRANTED_QOS_2, 16#02).
|
||||||
|
-define(RC_DISCONNECT_WITH_WILL_MESSAGE, 16#04).
|
||||||
|
-define(RC_NO_MATCHING_SUBSCRIBERS, 16#10).
|
||||||
|
-define(RC_NO_SUBSCRIPTION_EXISTED, 16#11).
|
||||||
|
-define(RC_CONTINUE_AUTHENTICATION, 16#18).
|
||||||
|
-define(RC_RE_AUTHENTICATE, 16#19).
|
||||||
|
-define(RC_UNSPECIFIED_ERROR, 16#80).
|
||||||
|
-define(RC_MALFORMED_PACKET, 16#81).
|
||||||
|
-define(RC_PROTOCOL_ERROR, 16#82).
|
||||||
|
-define(RC_IMPLEMENTATION_SPECIFIC_ERROR, 16#83).
|
||||||
|
-define(RC_UNSUPPORTED_PROTOCOL_VERSION, 16#84).
|
||||||
|
-define(RC_CLIENT_IDENTIFIER_NOT_VALID, 16#85).
|
||||||
|
-define(RC_BAD_USER_NAME_OR_PASSWORD, 16#86).
|
||||||
|
-define(RC_NOT_AUTHORIZED, 16#87).
|
||||||
|
-define(RC_SERVER_UNAVAILABLE, 16#88).
|
||||||
|
-define(RC_SERVER_BUSY, 16#89).
|
||||||
|
-define(RC_BANNED, 16#8A).
|
||||||
|
-define(RC_SERVER_SHUTTING_DOWN, 16#8B).
|
||||||
|
-define(RC_BAD_AUTHENTICATION_METHOD, 16#8C).
|
||||||
|
-define(RC_KEEP_ALIVE_TIMEOUT, 16#8D).
|
||||||
|
-define(RC_SESSION_TAKEN_OVER, 16#8E).
|
||||||
|
-define(RC_TOPIC_FILTER_INVALID, 16#8F).
|
||||||
|
-define(RC_TOPIC_NAME_INVALID, 16#90).
|
||||||
|
-define(RC_PACKET_IDENTIFIER_IN_USE, 16#91).
|
||||||
|
-define(RC_PACKET_IDENTIFIER_NOT_FOUND, 16#92).
|
||||||
|
-define(RC_RECEIVE_MAXIMUM_EXCEEDED, 16#93).
|
||||||
|
-define(RC_TOPIC_ALIAS_INVALID, 16#94).
|
||||||
|
-define(RC_PACKET_TOO_LARGE, 16#95).
|
||||||
|
-define(RC_MESSAGE_RATE_TOO_HIGH, 16#96).
|
||||||
|
-define(RC_QUOTA_EXCEEDED, 16#97).
|
||||||
|
-define(RC_ADMINISTRATIVE_ACTION, 16#98).
|
||||||
|
-define(RC_PAYLOAD_FORMAT_INVALID, 16#99).
|
||||||
|
-define(RC_RETAIN_NOT_SUPPORTED, 16#9A).
|
||||||
|
-define(RC_QOS_NOT_SUPPORTED, 16#9B).
|
||||||
|
-define(RC_USE_ANOTHER_SERVER, 16#9C).
|
||||||
|
-define(RC_SERVER_MOVED, 16#9D).
|
||||||
|
-define(RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED, 16#9E).
|
||||||
|
-define(RC_CONNECTION_RATE_EXCEEDED, 16#9F).
|
||||||
|
-define(RC_MAXIMUM_CONNECT_TIME, 16#A0).
|
||||||
|
-define(RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED, 16#A1).
|
||||||
|
-define(RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED, 16#A2).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Maximum MQTT Packet ID and Length
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(MAX_PACKET_ID, 16#ffff).
|
||||||
|
-define(MAX_PACKET_SIZE, 16#fffffff).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT Frame Mask
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(HIGHBIT, 2#10000000).
|
||||||
|
-define(LOWBITS, 2#01111111).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT Packet Fixed Header
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-record(mqtt_packet_header, {
|
||||||
|
type = ?RESERVED,
|
||||||
|
dup = false,
|
||||||
|
qos = ?QOS_0,
|
||||||
|
retain = false
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT Packets
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling
|
||||||
|
rap => 0, %% Retain as Publish
|
||||||
|
nl => 0, %% No Local
|
||||||
|
qos => 0 %% QoS
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(mqtt_packet_connect, {
|
||||||
|
proto_name = <<"MQTT">>,
|
||||||
|
proto_ver = ?MQTT_PROTO_V4,
|
||||||
|
is_bridge = false,
|
||||||
|
clean_start = true,
|
||||||
|
will_flag = false,
|
||||||
|
will_qos = ?QOS_0,
|
||||||
|
will_retain = false,
|
||||||
|
keepalive = 0,
|
||||||
|
properties = undefined,
|
||||||
|
clientid = <<>>,
|
||||||
|
will_props = undefined,
|
||||||
|
will_topic = undefined,
|
||||||
|
will_payload = undefined,
|
||||||
|
username = undefined,
|
||||||
|
password = undefined
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(mqtt_packet_connack, {
|
||||||
|
ack_flags,
|
||||||
|
reason_code,
|
||||||
|
properties
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(mqtt_packet_publish, {
|
||||||
|
topic_name,
|
||||||
|
packet_id,
|
||||||
|
properties
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(mqtt_packet_puback, {
|
||||||
|
packet_id,
|
||||||
|
reason_code,
|
||||||
|
properties
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(mqtt_packet_subscribe, {
|
||||||
|
packet_id,
|
||||||
|
properties,
|
||||||
|
topic_filters
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(mqtt_packet_suback, {
|
||||||
|
packet_id,
|
||||||
|
properties,
|
||||||
|
reason_codes
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(mqtt_packet_unsubscribe, {
|
||||||
|
packet_id,
|
||||||
|
properties,
|
||||||
|
topic_filters
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(mqtt_packet_unsuback, {
|
||||||
|
packet_id,
|
||||||
|
properties,
|
||||||
|
reason_codes
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(mqtt_packet_disconnect, {
|
||||||
|
reason_code,
|
||||||
|
properties
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(mqtt_packet_auth, {
|
||||||
|
reason_code,
|
||||||
|
properties
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT Message
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-record(mqtt_msg, {
|
||||||
|
qos = ?QOS_0 :: emqtt:qos(),
|
||||||
|
retain = false :: boolean(),
|
||||||
|
dup = false :: boolean(),
|
||||||
|
packet_id :: emqtt:packet_id(),
|
||||||
|
topic :: emqtt:topic(),
|
||||||
|
props :: emqtt:properties(),
|
||||||
|
payload :: binary()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT Control Packet
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-record(mqtt_packet, {
|
||||||
|
header :: #mqtt_packet_header{},
|
||||||
|
variable :: #mqtt_packet_connect{}
|
||||||
|
| #mqtt_packet_connack{}
|
||||||
|
| #mqtt_packet_publish{}
|
||||||
|
| #mqtt_packet_puback{}
|
||||||
|
| #mqtt_packet_subscribe{}
|
||||||
|
| #mqtt_packet_suback{}
|
||||||
|
| #mqtt_packet_unsubscribe{}
|
||||||
|
| #mqtt_packet_unsuback{}
|
||||||
|
| #mqtt_packet_disconnect{}
|
||||||
|
| #mqtt_packet_auth{}
|
||||||
|
| pos_integer()
|
||||||
|
| undefined,
|
||||||
|
payload :: binary() | undefined
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% MQTT Packet Match
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-define(CONNECT_PACKET(Var),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT},
|
||||||
|
variable = Var}).
|
||||||
|
|
||||||
|
-define(CONNACK_PACKET(ReasonCode),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK},
|
||||||
|
variable = #mqtt_packet_connack{ack_flags = 0,
|
||||||
|
reason_code = ReasonCode}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(CONNACK_PACKET(ReasonCode, SessPresent),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK},
|
||||||
|
variable = #mqtt_packet_connack{ack_flags = SessPresent,
|
||||||
|
reason_code = ReasonCode}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(CONNACK_PACKET(ReasonCode, SessPresent, Properties),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK},
|
||||||
|
variable = #mqtt_packet_connack{ack_flags = SessPresent,
|
||||||
|
reason_code = ReasonCode,
|
||||||
|
properties = Properties}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(AUTH_PACKET(),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?AUTH},
|
||||||
|
variable = #mqtt_packet_auth{reason_code = 0}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(AUTH_PACKET(ReasonCode),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?AUTH},
|
||||||
|
variable = #mqtt_packet_auth{reason_code = ReasonCode}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(AUTH_PACKET(ReasonCode, Properties),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?AUTH},
|
||||||
|
variable = #mqtt_packet_auth{reason_code = ReasonCode,
|
||||||
|
properties = Properties}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBLISH_PACKET(QoS),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = QoS}}).
|
||||||
|
|
||||||
|
-define(PUBLISH_PACKET(QoS, PacketId),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
|
||||||
|
qos = QoS},
|
||||||
|
variable = #mqtt_packet_publish{packet_id = PacketId}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
|
||||||
|
qos = QoS},
|
||||||
|
variable = #mqtt_packet_publish{topic_name = Topic,
|
||||||
|
packet_id = PacketId},
|
||||||
|
payload = Payload
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBLISH_PACKET(QoS, Topic, PacketId, Properties, Payload),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH,
|
||||||
|
qos = QoS},
|
||||||
|
variable = #mqtt_packet_publish{topic_name = Topic,
|
||||||
|
packet_id = PacketId,
|
||||||
|
properties = Properties},
|
||||||
|
payload = Payload
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBACK_PACKET(PacketId),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = 0}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBACK_PACKET(PacketId, ReasonCode),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = ReasonCode}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBACK_PACKET(PacketId, ReasonCode, Properties),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = ReasonCode,
|
||||||
|
properties = Properties}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBREC_PACKET(PacketId),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = 0}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBREC_PACKET(PacketId, ReasonCode),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = ReasonCode}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBREC_PACKET(PacketId, ReasonCode, Properties),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = ReasonCode,
|
||||||
|
properties = Properties}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBREL_PACKET(PacketId),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL,
|
||||||
|
qos = ?QOS_1},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = 0}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBREL_PACKET(PacketId, ReasonCode),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL,
|
||||||
|
qos = ?QOS_1},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = ReasonCode}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBREL_PACKET(PacketId, ReasonCode, Properties),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL,
|
||||||
|
qos = ?QOS_1},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = ReasonCode,
|
||||||
|
properties = Properties}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBCOMP_PACKET(PacketId),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = 0}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBCOMP_PACKET(PacketId, ReasonCode),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = ReasonCode}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PUBCOMP_PACKET(PacketId, ReasonCode, Properties),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP},
|
||||||
|
variable = #mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = ReasonCode,
|
||||||
|
properties = Properties}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(SUBSCRIBE_PACKET(PacketId, TopicFilters),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE,
|
||||||
|
qos = ?QOS_1},
|
||||||
|
variable = #mqtt_packet_subscribe{packet_id = PacketId,
|
||||||
|
topic_filters = TopicFilters}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE,
|
||||||
|
qos = ?QOS_1},
|
||||||
|
variable = #mqtt_packet_subscribe{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
topic_filters = TopicFilters}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(SUBACK_PACKET(PacketId, ReasonCodes),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK},
|
||||||
|
variable = #mqtt_packet_suback{packet_id = PacketId,
|
||||||
|
reason_codes = ReasonCodes}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(SUBACK_PACKET(PacketId, Properties, ReasonCodes),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK},
|
||||||
|
variable = #mqtt_packet_suback{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
reason_codes = ReasonCodes}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(UNSUBSCRIBE_PACKET(PacketId, TopicFilters),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE,
|
||||||
|
qos = ?QOS_1},
|
||||||
|
variable = #mqtt_packet_unsubscribe{packet_id = PacketId,
|
||||||
|
topic_filters = TopicFilters}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE,
|
||||||
|
qos = ?QOS_1},
|
||||||
|
variable = #mqtt_packet_unsubscribe{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
topic_filters = TopicFilters}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(UNSUBACK_PACKET(PacketId),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK},
|
||||||
|
variable = #mqtt_packet_unsuback{packet_id = PacketId}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(UNSUBACK_PACKET(PacketId, ReasonCodes),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK},
|
||||||
|
variable = #mqtt_packet_unsuback{packet_id = PacketId,
|
||||||
|
reason_codes = ReasonCodes}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(UNSUBACK_PACKET(PacketId, Properties, ReasonCodes),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK},
|
||||||
|
variable = #mqtt_packet_unsuback{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
reason_codes = ReasonCodes}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(DISCONNECT_PACKET(),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT},
|
||||||
|
variable = #mqtt_packet_disconnect{reason_code = 0}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(DISCONNECT_PACKET(ReasonCode),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT},
|
||||||
|
variable = #mqtt_packet_disconnect{reason_code = ReasonCode}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(DISCONNECT_PACKET(ReasonCode, Properties),
|
||||||
|
#mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT},
|
||||||
|
variable = #mqtt_packet_disconnect{reason_code = ReasonCode,
|
||||||
|
properties = Properties}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}).
|
||||||
|
|
||||||
|
-endif.
|
||||||
0
apps/power_gateway/include/power_gateway.hrl
Normal file
0
apps/power_gateway/include/power_gateway.hrl
Normal file
431
apps/power_gateway/src/efka_client.erl
Normal file
431
apps/power_gateway/src/efka_client.erl
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 28. 8月 2023 15:39
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(efka_client).
|
||||||
|
-author("aresei").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
%% 消息包
|
||||||
|
-record(efka_packet, {
|
||||||
|
packet_id :: integer(),
|
||||||
|
type :: integer(),
|
||||||
|
message :: any()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% 请求超时时间
|
||||||
|
-define(EFKA_REQUEST_TIMEOUT, 5000).
|
||||||
|
|
||||||
|
%% 消息类型
|
||||||
|
|
||||||
|
%% 服务注册
|
||||||
|
-define(PACKET_TYPE_REGISTER, 16).
|
||||||
|
%% 上传数据
|
||||||
|
-define(PACKET_TYPE_METRIC_DATA, 3).
|
||||||
|
%% 调用其他微服务
|
||||||
|
-define(PACKET_TYPE_INVOKE, 4).
|
||||||
|
%% 消息响应
|
||||||
|
-define(PACKET_TYPE_RESPONSE, 7).
|
||||||
|
%% efka下发给微服务参数
|
||||||
|
-define(PACKET_TYPE_PUSH_PARAM, 5).
|
||||||
|
%% efka下发给微服务采集项
|
||||||
|
-define(PACKET_TYPE_PUSH_METRIC, 6).
|
||||||
|
%% 设备状态轮询: 增加日期: 2025-4-16
|
||||||
|
-define(PACKET_TYPE_POLL, 20).
|
||||||
|
%% 微服务给efka发送log消息
|
||||||
|
-define(PACKET_TYPE_LOG, 9).
|
||||||
|
%% 微服务从efka获取自身的采集项
|
||||||
|
-define(PACKET_TYPE_REQUEST_METRIC, 10).
|
||||||
|
%% 微服务从efka获取自身的参数
|
||||||
|
-define(PACKET_TYPE_REQUEST_PARAM, 12).
|
||||||
|
%% efka向微服务发送stream-call消息
|
||||||
|
-define(PACKET_TYPE_PUSH_STREAM_CALL, 11).
|
||||||
|
%% 微服务事件上报
|
||||||
|
-define(PACKET_TYPE_EVENT, 15).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/3]).
|
||||||
|
-export([device_offline/1, device_online/1]).
|
||||||
|
-export([send_metric_data/2, invoke_service/3, send_log/1, request_metric/0, request_param/0, send_event/2]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
packet_id = 0 :: integer(),
|
||||||
|
host :: string(),
|
||||||
|
port :: integer(),
|
||||||
|
%% 请求后未完成的请求
|
||||||
|
inflight = #{} :: map(),
|
||||||
|
socket :: gen_tcp:socket()
|
||||||
|
}).
|
||||||
|
|
||||||
|
-spec send_metric_data(Fields :: list(), Tags :: #{}) -> {ok, Result :: any()} | {error, Reason :: any()}.
|
||||||
|
send_metric_data(Fields, Tags) when is_list(Fields), is_map(Tags) ->
|
||||||
|
{ok, Ref} = gen_server:call(?MODULE, {send_metric_data, self(), Fields, Tags}),
|
||||||
|
await_reply(Ref, ?EFKA_REQUEST_TIMEOUT).
|
||||||
|
|
||||||
|
-spec invoke_service(ToService :: binary(), Message :: map(), Timeout :: integer()) ->
|
||||||
|
{ok, Result :: any()} | {error, Reason :: any()}.
|
||||||
|
invoke_service(ToService, Message, Timeout) when is_binary(ToService), is_map(Message), is_integer(Timeout) ->
|
||||||
|
{ok, Ref} = gen_server:call(?MODULE, {invoke_service, self(), ToService, Message, Timeout}),
|
||||||
|
await_reply(Ref, ?EFKA_REQUEST_TIMEOUT).
|
||||||
|
|
||||||
|
-spec send_log(Message :: binary() | map()) -> no_return().
|
||||||
|
send_log(Message) when is_binary(Message); is_map(Message) ->
|
||||||
|
gen_server:cast(?MODULE, {send_log, Message}).
|
||||||
|
|
||||||
|
%% efka_server为了统一,r对象为字符串;需要2次json_decode
|
||||||
|
-spec request_metric() -> {ok, Result :: list()} | {error, Reason :: any()}.
|
||||||
|
request_metric() ->
|
||||||
|
{ok, Ref} = gen_server:call(?MODULE, {request_metric, self()}),
|
||||||
|
case await_reply(Ref, ?EFKA_REQUEST_TIMEOUT) of
|
||||||
|
{ok, Reply} ->
|
||||||
|
{ok, jiffy:decode(Reply, [return_maps])};
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec request_param() -> {ok, Result :: map()} | {error, Reason :: any()}.
|
||||||
|
request_param() ->
|
||||||
|
{ok, Ref} = gen_server:call(?MODULE, {request_param, self()}),
|
||||||
|
case await_reply(Ref, ?EFKA_REQUEST_TIMEOUT) of
|
||||||
|
{ok, Reply} ->
|
||||||
|
{ok, jiffy:decode(Reply, [return_maps])};
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec device_offline(DeviceUUID :: binary()) -> no_return().
|
||||||
|
device_offline(DeviceUUID) when is_binary(DeviceUUID) ->
|
||||||
|
send_event(1, #{<<"device_uuid">> => DeviceUUID, <<"status">> => 0}).
|
||||||
|
|
||||||
|
-spec device_online(DeviceUUID :: binary()) -> no_return().
|
||||||
|
device_online(DeviceUUID) when is_binary(DeviceUUID) ->
|
||||||
|
send_event(1, #{<<"device_uuid">> => DeviceUUID, <<"status">> => 1}).
|
||||||
|
|
||||||
|
-spec send_event(EventType :: integer(), Params :: binary() | map()) -> no_return().
|
||||||
|
send_event(EventType, Params) when is_integer(EventType), is_binary(Params); is_map(Params) ->
|
||||||
|
gen_server:cast(?MODULE, {send_event, EventType, Params}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-spec await_reply(Ref :: reference(), Timeout :: integer()) -> {ok, Result :: any()} | {error, Reason :: any()}.
|
||||||
|
await_reply(Ref, Timeout) when is_reference(Ref), is_integer(Timeout) ->
|
||||||
|
receive
|
||||||
|
{response, Ref, {ok, Reply}} ->
|
||||||
|
{ok, Reply};
|
||||||
|
{response, Ref, {error, Reason}} ->
|
||||||
|
{error, Reason}
|
||||||
|
after
|
||||||
|
Timeout ->
|
||||||
|
{error, timeout}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @doc Spawns the server and registers the local name (unique)
|
||||||
|
-spec(start_link(RegisterName :: binary(), Host :: string(), Port :: integer()) ->
|
||||||
|
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link(RegisterName, Host, Port) when is_binary(RegisterName), is_list(Host), is_integer(Port) ->
|
||||||
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [RegisterName, Host, Port], []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% gen_server callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Initializes the server
|
||||||
|
-spec(init(Args :: term()) ->
|
||||||
|
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term()} | ignore).
|
||||||
|
init([RegisterName, Host, Port]) ->
|
||||||
|
{ok, Socket} = gen_tcp:connect(Host, Port, [binary, {packet, 4}, {active, true}]),
|
||||||
|
ok = gen_tcp:controlling_process(Socket, self()),
|
||||||
|
case do_register(RegisterName, Socket) of
|
||||||
|
ok ->
|
||||||
|
{ok, #state{packet_id = 1, host = Host, port = Port, socket = Socket}};
|
||||||
|
{error, Reason} ->
|
||||||
|
{stop, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 执行到efka服务器的注册
|
||||||
|
do_register(RegisterName, Socket) ->
|
||||||
|
PacketId = 0,
|
||||||
|
Body = #{<<"name">> => RegisterName},
|
||||||
|
Packet = pack(PacketId, ?PACKET_TYPE_REGISTER, Body),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
receive
|
||||||
|
{tcp, Socket, Data} ->
|
||||||
|
RegisterPacket = unpack(Data),
|
||||||
|
lager:debug("[efka_client] get register reply packet: ~p", [RegisterPacket]),
|
||||||
|
case RegisterPacket of
|
||||||
|
#efka_packet{packet_id = PacketId, type = ?PACKET_TYPE_RESPONSE, message = #{<<"c">> := 1, <<"r">> := <<"ok">>}} ->
|
||||||
|
ok;
|
||||||
|
#efka_packet{packet_id = PacketId, type = ?PACKET_TYPE_RESPONSE, message = #{<<"c">> := 0, <<"e">> := Error}} ->
|
||||||
|
{error, Error};
|
||||||
|
_ ->
|
||||||
|
{error, invalid_register_packet}
|
||||||
|
end
|
||||||
|
after
|
||||||
|
?EFKA_REQUEST_TIMEOUT ->
|
||||||
|
{error, timeout}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling call messages
|
||||||
|
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||||
|
State :: #state{}) ->
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}} |
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_call({send_metric_data, ReceiverPid, Fields, Tags}, _From, State = #state{socket = Socket, packet_id = PacketId, inflight = Inflight}) ->
|
||||||
|
Body = #{<<"data">> => Fields, <<"tag">> => Tags},
|
||||||
|
Packet = pack(PacketId, ?PACKET_TYPE_METRIC_DATA, Body),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
Ref = make_ref(),
|
||||||
|
{reply, {ok, Ref}, State#state{packet_id = next_packet_id(PacketId), inflight = maps:put(PacketId, {Ref, ReceiverPid}, Inflight)}};
|
||||||
|
|
||||||
|
handle_call({invoke_service, ReceiverPid, ToService, Message, Timeout}, _From, State = #state{socket = Socket, packet_id = PacketId, inflight = Inflight}) ->
|
||||||
|
Body = #{
|
||||||
|
<<"to">> => ToService,
|
||||||
|
<<"t">> => Timeout,
|
||||||
|
<<"m">> => Message
|
||||||
|
},
|
||||||
|
Packet = pack(PacketId, ?PACKET_TYPE_INVOKE, Body),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
Ref = make_ref(),
|
||||||
|
{reply, {ok, Ref}, State#state{packet_id = next_packet_id(PacketId), inflight = maps:put(PacketId, {Ref, ReceiverPid}, Inflight)}};
|
||||||
|
|
||||||
|
handle_call({request_metric, ReceiverPid}, _From, State = #state{socket = Socket, packet_id = PacketId, inflight = Inflight}) ->
|
||||||
|
Packet = pack(PacketId, ?PACKET_TYPE_REQUEST_METRIC),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
Ref = make_ref(),
|
||||||
|
{reply, {ok, Ref}, State#state{packet_id = next_packet_id(PacketId), inflight = maps:put(PacketId, {Ref, ReceiverPid}, Inflight)}};
|
||||||
|
|
||||||
|
handle_call({request_param, ReceiverPid}, _From, State = #state{socket = Socket, packet_id = PacketId, inflight = Inflight}) ->
|
||||||
|
Packet = pack(PacketId, ?PACKET_TYPE_REQUEST_PARAM),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
Ref = make_ref(),
|
||||||
|
{reply, {ok, Ref}, State#state{packet_id = next_packet_id(PacketId), inflight = maps:put(PacketId, {Ref, ReceiverPid}, Inflight)}}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling cast messages
|
||||||
|
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_cast({send_log, Message}, State = #state{socket = Socket, packet_id = PacketId}) ->
|
||||||
|
Body = #{<<"l">> => Message},
|
||||||
|
Packet = pack(PacketId, ?PACKET_TYPE_LOG, Body),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
|
||||||
|
{noreply, State#state{packet_id = next_packet_id(PacketId)}};
|
||||||
|
|
||||||
|
handle_cast({send_event, EventType, Params}, State = #state{socket = Socket}) ->
|
||||||
|
Body = #{
|
||||||
|
<<"event_type">> => EventType,
|
||||||
|
<<"params">> => Params
|
||||||
|
},
|
||||||
|
Packet = pack(0, ?PACKET_TYPE_EVENT, Body),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
handle_cast(_Info, State = #state{}) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling all non call/cast messages
|
||||||
|
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_info({tcp, Socket, Data}, State = #state{socket = Socket}) ->
|
||||||
|
Packet = unpack(Data),
|
||||||
|
self() ! {handle_packet, Packet},
|
||||||
|
{noreply, State};
|
||||||
|
handle_info({tcp_closed, Socket}, State = #state{socket = Socket}) ->
|
||||||
|
{stop, tcp_closed, State};
|
||||||
|
|
||||||
|
%% 收到请求的响应
|
||||||
|
handle_info({handle_packet, #efka_packet{packet_id = PacketId, type = ?PACKET_TYPE_RESPONSE, message = Message}}, State = #state{inflight = Inflight}) ->
|
||||||
|
case maps:take(PacketId, Inflight) of
|
||||||
|
error ->
|
||||||
|
{noreply, State};
|
||||||
|
{{Ref, ReceiverPid}, NInflight} ->
|
||||||
|
case Message of
|
||||||
|
#{<<"c">> := 1, <<"r">> := Result} ->
|
||||||
|
ReceiverPid ! {response, Ref, {ok, Result}};
|
||||||
|
#{<<"c">> := 0, <<"e">> := Error} ->
|
||||||
|
ReceiverPid ! {response, Ref, {error, Error}}
|
||||||
|
end,
|
||||||
|
{noreply, State#state{inflight = NInflight}}
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% 收到efka推送的参数设置
|
||||||
|
handle_info({handle_packet, #efka_packet{packet_id = PacketId, type = ?PACKET_TYPE_PUSH_PARAM, message = Params}},
|
||||||
|
State = #state{socket = Socket}) when is_map(Params) ->
|
||||||
|
|
||||||
|
Message = case handle_param(Params) of
|
||||||
|
ok ->
|
||||||
|
#{<<"c">> => 1, <<"r">> => <<"ok">>};
|
||||||
|
{error, Reason} ->
|
||||||
|
#{<<"c">> => 0, <<"e">> => Reason}
|
||||||
|
end,
|
||||||
|
Packet = pack(PacketId, ?PACKET_TYPE_RESPONSE, Message),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
%% 收到efka推送的采集项消息
|
||||||
|
handle_info({handle_packet, #efka_packet{packet_id = PacketId, type = ?PACKET_TYPE_PUSH_METRIC, message = Metrics}},
|
||||||
|
State = #state{socket = Socket}) when is_list(Metrics) ->
|
||||||
|
|
||||||
|
Message = case handle_metric(Metrics) of
|
||||||
|
ok ->
|
||||||
|
#{<<"c">> => 1, <<"r">> => <<"ok">>};
|
||||||
|
{error, Reason} when is_binary(Reason) ->
|
||||||
|
#{<<"c">> => 0, <<"e">> => Reason}
|
||||||
|
end,
|
||||||
|
Packet = pack(PacketId, ?PACKET_TYPE_RESPONSE, Message),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
%% 收到设备状态的轮询请求
|
||||||
|
handle_info({handle_packet, #efka_packet{packet_id = PacketId, type = ?PACKET_TYPE_POLL, message = Command}}, State = #state{socket = Socket}) ->
|
||||||
|
Message = handle_poll_command(Command),
|
||||||
|
Packet = pack(PacketId, ?PACKET_TYPE_RESPONSE, Message),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
%% 收到efka的stream-call消息
|
||||||
|
handle_info({handle_packet, #efka_packet{packet_id = PacketId, type = ?PACKET_TYPE_PUSH_STREAM_CALL,
|
||||||
|
message = Msg = #{<<"service_name">> := ServiceName, <<"data">> := Data, <<"tag">> := Tag}}}, State = #state{socket = Socket}) ->
|
||||||
|
|
||||||
|
Message = case handle_stream_call(ServiceName, Data, Tag) of
|
||||||
|
{continue, NewServiceName, NewData, NewTag} ->
|
||||||
|
#{
|
||||||
|
<<"c">> => 1,
|
||||||
|
<<"r">> => Msg#{<<"service_name">> := NewServiceName, <<"data">> => NewData, <<"tag">> => NewTag},
|
||||||
|
<<"k">> => true
|
||||||
|
};
|
||||||
|
%% 处理到当前节点为止,不继续往下传递
|
||||||
|
{break, NewServiceName, NewData, NewTag} ->
|
||||||
|
#{
|
||||||
|
<<"c">> => 1,
|
||||||
|
<<"r">> => Msg#{<<"service_name">> := NewServiceName, <<"data">> => NewData, <<"tag">> => NewTag},
|
||||||
|
<<"k">> => false
|
||||||
|
};
|
||||||
|
error ->
|
||||||
|
#{
|
||||||
|
<<"c">> => 0,
|
||||||
|
<<"r">> => Msg,
|
||||||
|
<<"k">> => true
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
Packet = pack(PacketId, ?PACKET_TYPE_RESPONSE, Message),
|
||||||
|
ok = gen_tcp:send(Socket, Packet),
|
||||||
|
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
%% 其他消息为非法消息
|
||||||
|
handle_info({handle_packet, _Packet}, State = #state{}) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc This function is called by a gen_server when it is about to
|
||||||
|
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||||
|
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||||
|
%% with Reason. The return value is ignored.
|
||||||
|
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||||
|
State :: #state{}) -> term()).
|
||||||
|
terminate(_Reason, _State = #state{socket = Socket}) ->
|
||||||
|
gen_tcp:close(Socket),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Convert process state when code is changed
|
||||||
|
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||||
|
Extra :: term()) ->
|
||||||
|
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||||
|
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-spec next_packet_id(PacketId :: integer()) -> NextPacketId :: integer().
|
||||||
|
next_packet_id(PacketId) when PacketId >= 65535 ->
|
||||||
|
0;
|
||||||
|
next_packet_id(PacketId) ->
|
||||||
|
PacketId + 1.
|
||||||
|
|
||||||
|
-spec pack(PacketId :: integer(), Type :: integer()) -> binary().
|
||||||
|
pack(PacketId, Type) when is_integer(PacketId), is_integer(Type) ->
|
||||||
|
<<PacketId:16, Type:8>>.
|
||||||
|
|
||||||
|
-spec pack(PacketId :: integer(), Type :: integer(), Body :: map()) -> binary().
|
||||||
|
pack(PacketId, Type, Body) when is_integer(PacketId), is_integer(Type), is_map(Body) ->
|
||||||
|
Message = iolist_to_binary(jiffy:encode(Body, [force_utf8])),
|
||||||
|
<<PacketId:16, Type:8, Message/binary>>.
|
||||||
|
|
||||||
|
-spec unpack(binary()) -> #efka_packet{}.
|
||||||
|
unpack(<<PacketId:16, Type:8, Body/binary>>) ->
|
||||||
|
Message = catch jiffy:decode(Body, [return_maps]),
|
||||||
|
#efka_packet{packet_id = PacketId, type = Type, message = Message}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% simple callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
handle_poll_command(#{<<"device_uuid">> := DeviceUUID, <<"command">> := <<"query_status">>}) when is_binary(DeviceUUID) ->
|
||||||
|
case power_device:get_pid(DeviceUUID) of
|
||||||
|
undefined ->
|
||||||
|
#{
|
||||||
|
<<"c">> => 1,
|
||||||
|
<<"r">> => #{
|
||||||
|
<<"edge_status">> => -1,
|
||||||
|
<<"message">> => <<"设备信息不存在"/utf8>>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Pid ->
|
||||||
|
StatusMap = #{
|
||||||
|
0 => <<"离线"/utf8>>,
|
||||||
|
1 => <<"在线"/utf8>>
|
||||||
|
},
|
||||||
|
{ok, Status} = power_device:poll_status(Pid),
|
||||||
|
#{
|
||||||
|
<<"c">> => 1,
|
||||||
|
<<"r">> => #{
|
||||||
|
<<"edge_status">> => Status,
|
||||||
|
<<"message">> => maps:get(Status, StatusMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec handle_param(Params :: map()) -> ok | {error, Reason :: binary()}.
|
||||||
|
handle_param(Params) when is_map(Params) ->
|
||||||
|
power_gateway_args:push_param(Params),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
-spec handle_metric(Metric :: list()) -> ok | {error, Reason :: binary()}.
|
||||||
|
handle_metric(Metric) when is_list(Metric) ->
|
||||||
|
power_gateway_args:push_metric(Metric),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
-spec handle_stream_call(ServiceName :: binary(), Fields :: list(), Tag :: map()) ->
|
||||||
|
{continue, NewServiceName :: binary(), NewFields :: list(), NewTag :: map()}
|
||||||
|
| {break, NewServiceName :: binary(), NewFields :: list(), NewTag :: map()}
|
||||||
|
| error.
|
||||||
|
handle_stream_call(ServiceName, Fields, Tag) when is_binary(ServiceName), is_list(Fields), is_map(Tag) ->
|
||||||
|
{continue, ServiceName, Fields, Tag}.
|
||||||
1319
apps/power_gateway/src/emqtt/emqtt.erl
Normal file
1319
apps/power_gateway/src/emqtt/emqtt.erl
Normal file
File diff suppressed because it is too large
Load Diff
738
apps/power_gateway/src/emqtt/emqtt_frame.erl
Normal file
738
apps/power_gateway/src/emqtt/emqtt_frame.erl
Normal file
@ -0,0 +1,738 @@
|
|||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqtt_frame).
|
||||||
|
|
||||||
|
-include("emqtt.hrl").
|
||||||
|
|
||||||
|
-export([initial_parse_state/0, initial_parse_state/1]).
|
||||||
|
|
||||||
|
-export([parse/1, parse/2, serialize_fun/0, serialize_fun/1, serialize/1, serialize/2 ]).
|
||||||
|
|
||||||
|
-export_type([options/0, parse_state/0, parse_result/0, serialize_fun/0]).
|
||||||
|
|
||||||
|
-type(version() :: ?MQTT_PROTO_V3
|
||||||
|
| ?MQTT_PROTO_V4
|
||||||
|
| ?MQTT_PROTO_V5).
|
||||||
|
|
||||||
|
-type(options() :: #{strict_mode => boolean(),
|
||||||
|
max_size => 1..?MAX_PACKET_SIZE,
|
||||||
|
version => version()}).
|
||||||
|
|
||||||
|
-opaque(parse_state() :: {none, options()} | cont_fun()).
|
||||||
|
|
||||||
|
-opaque(parse_result() :: {more, cont_fun()}
|
||||||
|
| {ok, #mqtt_packet{}, binary(), parse_state()}).
|
||||||
|
|
||||||
|
-type(cont_fun() :: fun((binary()) -> parse_result())).
|
||||||
|
|
||||||
|
-type(serialize_fun() :: fun((emqx_types:packet()) -> iodata())).
|
||||||
|
|
||||||
|
-define(none(Options), {none, Options}).
|
||||||
|
|
||||||
|
-define(DEFAULT_OPTIONS,
|
||||||
|
#{strict_mode => false,
|
||||||
|
max_size => ?MAX_PACKET_SIZE,
|
||||||
|
version => ?MQTT_PROTO_V4
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Init Parse State
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec(initial_parse_state() -> {none, options()}).
|
||||||
|
initial_parse_state() ->
|
||||||
|
initial_parse_state(#{}).
|
||||||
|
|
||||||
|
-spec(initial_parse_state(options()) -> {none, options()}).
|
||||||
|
initial_parse_state(Options) when is_map(Options) ->
|
||||||
|
?none(merge_opts(Options)).
|
||||||
|
|
||||||
|
%% @pivate
|
||||||
|
merge_opts(Options) ->
|
||||||
|
maps:merge(?DEFAULT_OPTIONS, Options).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Parse MQTT Frame
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-spec(parse(binary()) -> parse_result()).
|
||||||
|
parse(Bin) ->
|
||||||
|
parse(Bin, initial_parse_state()).
|
||||||
|
|
||||||
|
-spec(parse(binary(), parse_state()) -> parse_result()).
|
||||||
|
parse(<<>>, {none, Options}) ->
|
||||||
|
{more, fun(Bin) -> parse(Bin, {none, Options}) end};
|
||||||
|
parse(<<Type:4, Dup:1, QoS:2, Retain:1, Rest/binary>>,
|
||||||
|
{none, Options = #{strict_mode := StrictMode}}) ->
|
||||||
|
%% Validate header if strict mode.
|
||||||
|
StrictMode andalso validate_header(Type, Dup, QoS, Retain),
|
||||||
|
Header = #mqtt_packet_header{type = Type,
|
||||||
|
dup = bool(Dup),
|
||||||
|
qos = QoS,
|
||||||
|
retain = bool(Retain)
|
||||||
|
},
|
||||||
|
Header1 = case fixqos(Type, QoS) of
|
||||||
|
QoS -> Header;
|
||||||
|
FixedQoS -> Header#mqtt_packet_header{qos = FixedQoS}
|
||||||
|
end,
|
||||||
|
parse_remaining_len(Rest, Header1, Options);
|
||||||
|
parse(Bin, Cont) when is_binary(Bin), is_function(Cont) ->
|
||||||
|
Cont(Bin).
|
||||||
|
|
||||||
|
parse_remaining_len(<<>>, Header, Options) ->
|
||||||
|
{more, fun(Bin) -> parse_remaining_len(Bin, Header, Options) end};
|
||||||
|
parse_remaining_len(Rest, Header, Options) ->
|
||||||
|
parse_remaining_len(Rest, Header, 1, 0, Options).
|
||||||
|
|
||||||
|
parse_remaining_len(_Bin, _Header, _Multiplier, Length, #{max_size := MaxSize}) when Length > MaxSize ->
|
||||||
|
error(frame_too_large);
|
||||||
|
parse_remaining_len(<<>>, Header, Multiplier, Length, Options) ->
|
||||||
|
{more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, Options) end};
|
||||||
|
%% Match DISCONNECT without payload
|
||||||
|
parse_remaining_len(<<0:8, Rest/binary>>, Header = #mqtt_packet_header{type = ?DISCONNECT}, 1, 0, Options) ->
|
||||||
|
Packet = packet(Header, #mqtt_packet_disconnect{reason_code = ?RC_SUCCESS}),
|
||||||
|
{ok, Packet, Rest, ?none(Options)};
|
||||||
|
%% Match PINGREQ.
|
||||||
|
parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, Options) ->
|
||||||
|
parse_frame(Rest, Header, 0, Options);
|
||||||
|
%% Match PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK...
|
||||||
|
parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, Options) ->
|
||||||
|
parse_frame(Rest, Header, 2, Options);
|
||||||
|
parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, Options) ->
|
||||||
|
parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, Options);
|
||||||
|
parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value,
|
||||||
|
Options = #{max_size := MaxSize}) ->
|
||||||
|
FrameLen = Value + Len * Multiplier,
|
||||||
|
if
|
||||||
|
FrameLen > MaxSize ->
|
||||||
|
error(frame_too_large);
|
||||||
|
true ->
|
||||||
|
parse_frame(Rest, Header, FrameLen, Options)
|
||||||
|
end.
|
||||||
|
|
||||||
|
parse_frame(Bin, Header, 0, Options) ->
|
||||||
|
{ok, packet(Header), Bin, ?none(Options)};
|
||||||
|
|
||||||
|
parse_frame(Bin, Header, Length, Options) ->
|
||||||
|
case Bin of
|
||||||
|
<<FrameBin:Length/binary, Rest/binary>> ->
|
||||||
|
case parse_packet(Header, FrameBin, Options) of
|
||||||
|
{Variable, Payload} ->
|
||||||
|
{ok, packet(Header, Variable, Payload), Rest, ?none(Options)};
|
||||||
|
Variable = #mqtt_packet_connect{proto_ver = Ver} ->
|
||||||
|
{ok, packet(Header, Variable), Rest, ?none(Options#{version := Ver})};
|
||||||
|
Variable ->
|
||||||
|
{ok, packet(Header, Variable), Rest, ?none(Options)}
|
||||||
|
end;
|
||||||
|
TooShortBin ->
|
||||||
|
{more, fun(BinMore) ->
|
||||||
|
parse_frame(<<TooShortBin/binary, BinMore/binary>>, Header, Length, Options)
|
||||||
|
end}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-compile({inline, [packet/1, packet/2, packet/3]}).
|
||||||
|
packet(Header) ->
|
||||||
|
#mqtt_packet{header = Header}.
|
||||||
|
packet(Header, Variable) ->
|
||||||
|
#mqtt_packet{header = Header, variable = Variable}.
|
||||||
|
packet(Header, Variable, Payload) ->
|
||||||
|
#mqtt_packet{header = Header, variable = Variable, payload = Payload}.
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) ->
|
||||||
|
{ProtoName, Rest} = parse_utf8_string(FrameBin),
|
||||||
|
<<BridgeTag:4, ProtoVer:4, Rest1/binary>> = Rest,
|
||||||
|
% Note: Crash when reserved flag doesn't equal to 0, there is no strict
|
||||||
|
% compliance with the MQTT5.0.
|
||||||
|
<<UsernameFlag : 1,
|
||||||
|
PasswordFlag : 1,
|
||||||
|
WillRetain : 1,
|
||||||
|
WillQoS : 2,
|
||||||
|
WillFlag : 1,
|
||||||
|
CleanStart : 1,
|
||||||
|
0 : 1,
|
||||||
|
KeepAlive : 16/big,
|
||||||
|
Rest2/binary>> = Rest1,
|
||||||
|
|
||||||
|
{Properties, Rest3} = parse_properties(Rest2, ProtoVer),
|
||||||
|
{ClientId, Rest4} = parse_utf8_string(Rest3),
|
||||||
|
ConnPacket = #mqtt_packet_connect{proto_name = ProtoName,
|
||||||
|
proto_ver = ProtoVer,
|
||||||
|
is_bridge = (BridgeTag =:= 8),
|
||||||
|
clean_start = bool(CleanStart),
|
||||||
|
will_flag = bool(WillFlag),
|
||||||
|
will_qos = WillQoS,
|
||||||
|
will_retain = bool(WillRetain),
|
||||||
|
keepalive = KeepAlive,
|
||||||
|
properties = Properties,
|
||||||
|
clientid = ClientId
|
||||||
|
},
|
||||||
|
{ConnPacket1, Rest5} = parse_will_message(ConnPacket, Rest4),
|
||||||
|
{Username, Rest6} = parse_utf8_string(Rest5, bool(UsernameFlag)),
|
||||||
|
{Passsword, <<>>} = parse_utf8_string(Rest6, bool(PasswordFlag)),
|
||||||
|
ConnPacket1#mqtt_packet_connect{username = Username, password = Passsword};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = ?CONNACK},
|
||||||
|
<<AckFlags:8, ReasonCode:8, Rest/binary>>, #{version := Ver}) ->
|
||||||
|
{Properties, <<>>} = parse_properties(Rest, Ver),
|
||||||
|
#mqtt_packet_connack{ack_flags = AckFlags,
|
||||||
|
reason_code = ReasonCode,
|
||||||
|
properties = Properties
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = ?PUBLISH, qos = QoS}, Bin,
|
||||||
|
#{strict_mode := StrictMode, version := Ver}) ->
|
||||||
|
{TopicName, Rest} = parse_utf8_string(Bin),
|
||||||
|
{PacketId, Rest1} = case QoS of
|
||||||
|
?QOS_0 -> {undefined, Rest};
|
||||||
|
_ -> parse_packet_id(Rest)
|
||||||
|
end,
|
||||||
|
(PacketId =/= undefined) andalso
|
||||||
|
StrictMode andalso validate_packet_id(PacketId),
|
||||||
|
{Properties, Payload} = parse_properties(Rest1, Ver),
|
||||||
|
Publish = #mqtt_packet_publish{topic_name = TopicName,
|
||||||
|
packet_id = PacketId,
|
||||||
|
properties = Properties
|
||||||
|
},
|
||||||
|
{Publish, Payload};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = PubAck}, <<PacketId:16/big>>, #{strict_mode := StrictMode})
|
||||||
|
when ?PUBACK =< PubAck, PubAck =< ?PUBCOMP ->
|
||||||
|
StrictMode andalso validate_packet_id(PacketId),
|
||||||
|
#mqtt_packet_puback{packet_id = PacketId, reason_code = 0};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = PubAck}, <<PacketId:16/big, ReasonCode, Rest/binary>>,
|
||||||
|
#{strict_mode := StrictMode, version := Ver = ?MQTT_PROTO_V5})
|
||||||
|
when ?PUBACK =< PubAck, PubAck =< ?PUBCOMP ->
|
||||||
|
StrictMode andalso validate_packet_id(PacketId),
|
||||||
|
{Properties, <<>>} = parse_properties(Rest, Ver),
|
||||||
|
#mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = ReasonCode,
|
||||||
|
properties = Properties
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = ?SUBSCRIBE}, <<PacketId:16/big, Rest/binary>>,
|
||||||
|
#{strict_mode := StrictMode, version := Ver}) ->
|
||||||
|
StrictMode andalso validate_packet_id(PacketId),
|
||||||
|
{Properties, Rest1} = parse_properties(Rest, Ver),
|
||||||
|
TopicFilters = parse_topic_filters(subscribe, Rest1),
|
||||||
|
ok = validate_subqos([QoS || {_, #{qos := QoS}} <- TopicFilters]),
|
||||||
|
#mqtt_packet_subscribe{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
topic_filters = TopicFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = ?SUBACK}, <<PacketId:16/big, Rest/binary>>,
|
||||||
|
#{strict_mode := StrictMode, version := Ver}) ->
|
||||||
|
StrictMode andalso validate_packet_id(PacketId),
|
||||||
|
{Properties, Rest1} = parse_properties(Rest, Ver),
|
||||||
|
ReasonCodes = parse_reason_codes(Rest1),
|
||||||
|
#mqtt_packet_suback{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
reason_codes = ReasonCodes
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = ?UNSUBSCRIBE}, <<PacketId:16/big, Rest/binary>>,
|
||||||
|
#{strict_mode := StrictMode, version := Ver}) ->
|
||||||
|
StrictMode andalso validate_packet_id(PacketId),
|
||||||
|
{Properties, Rest1} = parse_properties(Rest, Ver),
|
||||||
|
TopicFilters = parse_topic_filters(unsubscribe, Rest1),
|
||||||
|
#mqtt_packet_unsubscribe{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
topic_filters = TopicFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = ?UNSUBACK}, <<PacketId:16/big>>,
|
||||||
|
#{strict_mode := StrictMode}) ->
|
||||||
|
StrictMode andalso validate_packet_id(PacketId),
|
||||||
|
#mqtt_packet_unsuback{packet_id = PacketId};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = ?UNSUBACK}, <<PacketId:16/big, Rest/binary>>,
|
||||||
|
#{strict_mode := StrictMode, version := Ver}) ->
|
||||||
|
StrictMode andalso validate_packet_id(PacketId),
|
||||||
|
{Properties, Rest1} = parse_properties(Rest, Ver),
|
||||||
|
ReasonCodes = parse_reason_codes(Rest1),
|
||||||
|
#mqtt_packet_unsuback{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
reason_codes = ReasonCodes
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = ?DISCONNECT}, <<ReasonCode, Rest/binary>>,
|
||||||
|
#{version := ?MQTT_PROTO_V5}) ->
|
||||||
|
{Properties, <<>>} = parse_properties(Rest, ?MQTT_PROTO_V5),
|
||||||
|
#mqtt_packet_disconnect{reason_code = ReasonCode,
|
||||||
|
properties = Properties
|
||||||
|
};
|
||||||
|
|
||||||
|
parse_packet(#mqtt_packet_header{type = ?AUTH}, <<ReasonCode, Rest/binary>>,
|
||||||
|
#{version := ?MQTT_PROTO_V5}) ->
|
||||||
|
{Properties, <<>>} = parse_properties(Rest, ?MQTT_PROTO_V5),
|
||||||
|
#mqtt_packet_auth{reason_code = ReasonCode, properties = Properties}.
|
||||||
|
|
||||||
|
parse_will_message(Packet = #mqtt_packet_connect{will_flag = true,
|
||||||
|
proto_ver = Ver}, Bin) ->
|
||||||
|
{Props, Rest} = parse_properties(Bin, Ver),
|
||||||
|
{Topic, Rest1} = parse_utf8_string(Rest),
|
||||||
|
{Payload, Rest2} = parse_binary_data(Rest1),
|
||||||
|
{Packet#mqtt_packet_connect{will_props = Props,
|
||||||
|
will_topic = Topic,
|
||||||
|
will_payload = Payload
|
||||||
|
}, Rest2};
|
||||||
|
parse_will_message(Packet, Bin) -> {Packet, Bin}.
|
||||||
|
|
||||||
|
-compile({inline, [parse_packet_id/1]}).
|
||||||
|
parse_packet_id(<<PacketId:16/big, Rest/binary>>) ->
|
||||||
|
{PacketId, Rest}.
|
||||||
|
|
||||||
|
parse_properties(Bin, Ver) when Ver =/= ?MQTT_PROTO_V5 ->
|
||||||
|
{undefined, Bin};
|
||||||
|
%% TODO: version mess?
|
||||||
|
parse_properties(<<>>, ?MQTT_PROTO_V5) ->
|
||||||
|
{#{}, <<>>};
|
||||||
|
parse_properties(<<0, Rest/binary>>, ?MQTT_PROTO_V5) ->
|
||||||
|
{#{}, Rest};
|
||||||
|
parse_properties(Bin, ?MQTT_PROTO_V5) ->
|
||||||
|
{Len, Rest} = parse_variable_byte_integer(Bin),
|
||||||
|
<<PropsBin:Len/binary, Rest1/binary>> = Rest,
|
||||||
|
{parse_property(PropsBin, #{}), Rest1}.
|
||||||
|
|
||||||
|
parse_property(<<>>, Props) ->
|
||||||
|
Props;
|
||||||
|
parse_property(<<16#01, Val, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Payload-Format-Indicator' => Val});
|
||||||
|
parse_property(<<16#02, Val:32/big, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Message-Expiry-Interval' => Val});
|
||||||
|
parse_property(<<16#03, Bin/binary>>, Props) ->
|
||||||
|
{Val, Rest} = parse_utf8_string(Bin),
|
||||||
|
parse_property(Rest, Props#{'Content-Type' => Val});
|
||||||
|
parse_property(<<16#08, Bin/binary>>, Props) ->
|
||||||
|
{Val, Rest} = parse_utf8_string(Bin),
|
||||||
|
parse_property(Rest, Props#{'Response-Topic' => Val});
|
||||||
|
parse_property(<<16#09, Len:16/big, Val:Len/binary, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Correlation-Data' => Val});
|
||||||
|
parse_property(<<16#0B, Bin/binary>>, Props) ->
|
||||||
|
{Val, Rest} = parse_variable_byte_integer(Bin),
|
||||||
|
parse_property(Rest, Props#{'Subscription-Identifier' => Val});
|
||||||
|
parse_property(<<16#11, Val:32/big, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Session-Expiry-Interval' => Val});
|
||||||
|
parse_property(<<16#12, Bin/binary>>, Props) ->
|
||||||
|
{Val, Rest} = parse_utf8_string(Bin),
|
||||||
|
parse_property(Rest, Props#{'Assigned-Client-Identifier' => Val});
|
||||||
|
parse_property(<<16#13, Val:16, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Server-Keep-Alive' => Val});
|
||||||
|
parse_property(<<16#15, Bin/binary>>, Props) ->
|
||||||
|
{Val, Rest} = parse_utf8_string(Bin),
|
||||||
|
parse_property(Rest, Props#{'Authentication-Method' => Val});
|
||||||
|
parse_property(<<16#16, Len:16/big, Val:Len/binary, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Authentication-Data' => Val});
|
||||||
|
parse_property(<<16#17, Val, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Request-Problem-Information' => Val});
|
||||||
|
parse_property(<<16#18, Val:32, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Will-Delay-Interval' => Val});
|
||||||
|
parse_property(<<16#19, Val, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Request-Response-Information' => Val});
|
||||||
|
parse_property(<<16#1A, Bin/binary>>, Props) ->
|
||||||
|
{Val, Rest} = parse_utf8_string(Bin),
|
||||||
|
parse_property(Rest, Props#{'Response-Information' => Val});
|
||||||
|
parse_property(<<16#1C, Bin/binary>>, Props) ->
|
||||||
|
{Val, Rest} = parse_utf8_string(Bin),
|
||||||
|
parse_property(Rest, Props#{'Server-Reference' => Val});
|
||||||
|
parse_property(<<16#1F, Bin/binary>>, Props) ->
|
||||||
|
{Val, Rest} = parse_utf8_string(Bin),
|
||||||
|
parse_property(Rest, Props#{'Reason-String' => Val});
|
||||||
|
parse_property(<<16#21, Val:16/big, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Receive-Maximum' => Val});
|
||||||
|
parse_property(<<16#22, Val:16/big, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Topic-Alias-Maximum' => Val});
|
||||||
|
parse_property(<<16#23, Val:16/big, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Topic-Alias' => Val});
|
||||||
|
parse_property(<<16#24, Val, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Maximum-QoS' => Val});
|
||||||
|
parse_property(<<16#25, Val, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Retain-Available' => Val});
|
||||||
|
parse_property(<<16#26, Bin/binary>>, Props) ->
|
||||||
|
{Pair, Rest} = parse_utf8_pair(Bin),
|
||||||
|
case maps:find('User-Property', Props) of
|
||||||
|
{ok, UserProps} ->
|
||||||
|
UserProps1 = lists:append(UserProps, [Pair]),
|
||||||
|
parse_property(Rest, Props#{'User-Property' := UserProps1});
|
||||||
|
error ->
|
||||||
|
parse_property(Rest, Props#{'User-Property' => [Pair]})
|
||||||
|
end;
|
||||||
|
parse_property(<<16#27, Val:32, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Maximum-Packet-Size' => Val});
|
||||||
|
parse_property(<<16#28, Val, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Wildcard-Subscription-Available' => Val});
|
||||||
|
parse_property(<<16#29, Val, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Subscription-Identifier-Available' => Val});
|
||||||
|
parse_property(<<16#2A, Val, Bin/binary>>, Props) ->
|
||||||
|
parse_property(Bin, Props#{'Shared-Subscription-Available' => Val}).
|
||||||
|
|
||||||
|
parse_variable_byte_integer(Bin) ->
|
||||||
|
parse_variable_byte_integer(Bin, 1, 0).
|
||||||
|
parse_variable_byte_integer(<<1:1, Len:7, Rest/binary>>, Multiplier, Value) ->
|
||||||
|
parse_variable_byte_integer(Rest, Multiplier * ?HIGHBIT, Value + Len * Multiplier);
|
||||||
|
parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) ->
|
||||||
|
{Value + Len * Multiplier, Rest}.
|
||||||
|
|
||||||
|
parse_topic_filters(subscribe, Bin) ->
|
||||||
|
[{Topic, #{rh => Rh, rap => Rap, nl => Nl, qos => QoS}}
|
||||||
|
|| <<Len:16/big, Topic:Len/binary, _:2, Rh:2, Rap:1, Nl:1, QoS:2>> <= Bin];
|
||||||
|
|
||||||
|
parse_topic_filters(unsubscribe, Bin) ->
|
||||||
|
[Topic || <<Len:16/big, Topic:Len/binary>> <= Bin].
|
||||||
|
|
||||||
|
parse_reason_codes(Bin) ->
|
||||||
|
[Code || <<Code>> <= Bin].
|
||||||
|
|
||||||
|
parse_utf8_pair(<<Len1:16/big, Key:Len1/binary,
|
||||||
|
Len2:16/big, Val:Len2/binary, Rest/binary>>) ->
|
||||||
|
{{Key, Val}, Rest}.
|
||||||
|
|
||||||
|
parse_utf8_string(Bin, false) ->
|
||||||
|
{undefined, Bin};
|
||||||
|
parse_utf8_string(Bin, true) ->
|
||||||
|
parse_utf8_string(Bin).
|
||||||
|
|
||||||
|
parse_utf8_string(<<Len:16/big, Str:Len/binary, Rest/binary>>) ->
|
||||||
|
{Str, Rest}.
|
||||||
|
|
||||||
|
parse_binary_data(<<Len:16/big, Data:Len/binary, Rest/binary>>) ->
|
||||||
|
{Data, Rest}.
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Serialize MQTT Packet
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
serialize_fun() -> serialize_fun(?DEFAULT_OPTIONS).
|
||||||
|
|
||||||
|
serialize_fun(#mqtt_packet_connect{proto_ver = ProtoVer, properties = ConnProps}) ->
|
||||||
|
MaxSize = get_property('Maximum-Packet-Size', ConnProps, ?MAX_PACKET_SIZE),
|
||||||
|
serialize_fun(#{version => ProtoVer, max_size => MaxSize});
|
||||||
|
|
||||||
|
serialize_fun(#{version := Ver, max_size := MaxSize}) ->
|
||||||
|
fun(Packet) ->
|
||||||
|
IoData = serialize(Packet, Ver),
|
||||||
|
case is_too_large(IoData, MaxSize) of
|
||||||
|
true -> <<>>;
|
||||||
|
false -> IoData
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec(serialize(#mqtt_packet{}) -> iodata()).
|
||||||
|
serialize(Packet) -> serialize(Packet, ?MQTT_PROTO_V4).
|
||||||
|
|
||||||
|
-spec(serialize(#mqtt_packet{}, version()) -> iodata()).
|
||||||
|
serialize(#mqtt_packet{header = Header,
|
||||||
|
variable = Variable,
|
||||||
|
payload = Payload}, Ver) ->
|
||||||
|
serialize(Header, serialize_variable(Variable, Ver), serialize_payload(Payload)).
|
||||||
|
|
||||||
|
serialize(#mqtt_packet_header{type = Type,
|
||||||
|
dup = Dup,
|
||||||
|
qos = QoS,
|
||||||
|
retain = Retain
|
||||||
|
}, VariableBin, PayloadBin)
|
||||||
|
when ?CONNECT =< Type andalso Type =< ?AUTH ->
|
||||||
|
Len = iolist_size(VariableBin) + iolist_size(PayloadBin),
|
||||||
|
[<<Type:4, (flag(Dup)):1, (flag(QoS)):2, (flag(Retain)):1>>,
|
||||||
|
serialize_remaining_len(Len), VariableBin, PayloadBin].
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_connect{
|
||||||
|
proto_name = ProtoName,
|
||||||
|
proto_ver = ProtoVer,
|
||||||
|
is_bridge = IsBridge,
|
||||||
|
clean_start = CleanStart,
|
||||||
|
will_flag = WillFlag,
|
||||||
|
will_qos = WillQoS,
|
||||||
|
will_retain = WillRetain,
|
||||||
|
keepalive = KeepAlive,
|
||||||
|
properties = Properties,
|
||||||
|
clientid = ClientId,
|
||||||
|
will_props = WillProps,
|
||||||
|
will_topic = WillTopic,
|
||||||
|
will_payload = WillPayload,
|
||||||
|
username = Username,
|
||||||
|
password = Password}, _Ver) ->
|
||||||
|
[serialize_binary_data(ProtoName),
|
||||||
|
<<(case IsBridge of
|
||||||
|
true -> 16#80 + ProtoVer;
|
||||||
|
false -> ProtoVer
|
||||||
|
end):8,
|
||||||
|
(flag(Username)):1,
|
||||||
|
(flag(Password)):1,
|
||||||
|
(flag(WillRetain)):1,
|
||||||
|
WillQoS:2,
|
||||||
|
(flag(WillFlag)):1,
|
||||||
|
(flag(CleanStart)):1,
|
||||||
|
0:1,
|
||||||
|
KeepAlive:16/big-unsigned-integer>>,
|
||||||
|
serialize_properties(Properties, ProtoVer),
|
||||||
|
serialize_utf8_string(ClientId),
|
||||||
|
case WillFlag of
|
||||||
|
true -> [serialize_properties(WillProps, ProtoVer),
|
||||||
|
serialize_utf8_string(WillTopic),
|
||||||
|
serialize_binary_data(WillPayload)];
|
||||||
|
false -> <<>>
|
||||||
|
end,
|
||||||
|
serialize_utf8_string(Username, true),
|
||||||
|
serialize_utf8_string(Password, true)];
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_connack{ack_flags = AckFlags,
|
||||||
|
reason_code = ReasonCode,
|
||||||
|
properties = Properties}, Ver) ->
|
||||||
|
[AckFlags, ReasonCode, serialize_properties(Properties, Ver)];
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_publish{topic_name = TopicName,
|
||||||
|
packet_id = PacketId,
|
||||||
|
properties = Properties}, Ver) ->
|
||||||
|
[serialize_utf8_string(TopicName),
|
||||||
|
if
|
||||||
|
PacketId =:= undefined -> <<>>;
|
||||||
|
true -> <<PacketId:16/big-unsigned-integer>>
|
||||||
|
end,
|
||||||
|
serialize_properties(Properties, Ver)];
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_puback{packet_id = PacketId}, Ver)
|
||||||
|
when Ver == ?MQTT_PROTO_V3; Ver == ?MQTT_PROTO_V4 ->
|
||||||
|
<<PacketId:16/big-unsigned-integer>>;
|
||||||
|
serialize_variable(#mqtt_packet_puback{packet_id = PacketId,
|
||||||
|
reason_code = ReasonCode,
|
||||||
|
properties = Properties
|
||||||
|
},
|
||||||
|
Ver = ?MQTT_PROTO_V5) ->
|
||||||
|
[<<PacketId:16/big-unsigned-integer>>, ReasonCode,
|
||||||
|
serialize_properties(Properties, Ver)];
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_subscribe{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
topic_filters = TopicFilters}, Ver) ->
|
||||||
|
[<<PacketId:16/big-unsigned-integer>>, serialize_properties(Properties, Ver),
|
||||||
|
serialize_topic_filters(subscribe, TopicFilters, Ver)];
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_suback{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
reason_codes = ReasonCodes}, Ver) ->
|
||||||
|
[<<PacketId:16/big-unsigned-integer>>, serialize_properties(Properties, Ver),
|
||||||
|
serialize_reason_codes(ReasonCodes)];
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_unsubscribe{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
topic_filters = TopicFilters}, Ver) ->
|
||||||
|
[<<PacketId:16/big-unsigned-integer>>, serialize_properties(Properties, Ver),
|
||||||
|
serialize_topic_filters(unsubscribe, TopicFilters, Ver)];
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_unsuback{packet_id = PacketId,
|
||||||
|
properties = Properties,
|
||||||
|
reason_codes = ReasonCodes}, Ver) ->
|
||||||
|
[<<PacketId:16/big-unsigned-integer>>, serialize_properties(Properties, Ver),
|
||||||
|
serialize_reason_codes(ReasonCodes)];
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_disconnect{}, Ver)
|
||||||
|
when Ver == ?MQTT_PROTO_V3; Ver == ?MQTT_PROTO_V4 ->
|
||||||
|
<<>>;
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_disconnect{reason_code = ReasonCode,
|
||||||
|
properties = Properties},
|
||||||
|
Ver = ?MQTT_PROTO_V5) ->
|
||||||
|
[ReasonCode, serialize_properties(Properties, Ver)];
|
||||||
|
serialize_variable(#mqtt_packet_disconnect{}, _Ver) ->
|
||||||
|
<<>>;
|
||||||
|
|
||||||
|
serialize_variable(#mqtt_packet_auth{reason_code = ReasonCode,
|
||||||
|
properties = Properties},
|
||||||
|
Ver = ?MQTT_PROTO_V5) ->
|
||||||
|
[ReasonCode, serialize_properties(Properties, Ver)];
|
||||||
|
|
||||||
|
serialize_variable(PacketId, ?MQTT_PROTO_V3) when is_integer(PacketId) ->
|
||||||
|
<<PacketId:16/big-unsigned-integer>>;
|
||||||
|
serialize_variable(PacketId, ?MQTT_PROTO_V4) when is_integer(PacketId) ->
|
||||||
|
<<PacketId:16/big-unsigned-integer>>;
|
||||||
|
serialize_variable(undefined, _Ver) ->
|
||||||
|
<<>>.
|
||||||
|
|
||||||
|
serialize_payload(undefined) -> <<>>;
|
||||||
|
serialize_payload(Bin) -> Bin.
|
||||||
|
|
||||||
|
serialize_properties(_Props, Ver) when Ver =/= ?MQTT_PROTO_V5 ->
|
||||||
|
<<>>;
|
||||||
|
serialize_properties(Props, ?MQTT_PROTO_V5) ->
|
||||||
|
serialize_properties(Props).
|
||||||
|
|
||||||
|
serialize_properties(undefined) ->
|
||||||
|
<<0>>;
|
||||||
|
serialize_properties(Props) when map_size(Props) == 0 ->
|
||||||
|
<<0>>;
|
||||||
|
serialize_properties(Props) when is_map(Props) ->
|
||||||
|
Bin = << <<(serialize_property(Prop, Val))/binary>> || {Prop, Val} <- maps:to_list(Props) >>,
|
||||||
|
[serialize_variable_byte_integer(byte_size(Bin)), Bin].
|
||||||
|
|
||||||
|
serialize_property(_, undefined) ->
|
||||||
|
<<>>;
|
||||||
|
serialize_property('Payload-Format-Indicator', Val) ->
|
||||||
|
<<16#01, Val>>;
|
||||||
|
serialize_property('Message-Expiry-Interval', Val) ->
|
||||||
|
<<16#02, Val:32/big>>;
|
||||||
|
serialize_property('Content-Type', Val) ->
|
||||||
|
<<16#03, (serialize_utf8_string(Val))/binary>>;
|
||||||
|
serialize_property('Response-Topic', Val) ->
|
||||||
|
<<16#08, (serialize_utf8_string(Val))/binary>>;
|
||||||
|
serialize_property('Correlation-Data', Val) ->
|
||||||
|
<<16#09, (byte_size(Val)):16, Val/binary>>;
|
||||||
|
serialize_property('Subscription-Identifier', Val) ->
|
||||||
|
<<16#0B, (serialize_variable_byte_integer(Val))/binary>>;
|
||||||
|
serialize_property('Session-Expiry-Interval', Val) ->
|
||||||
|
<<16#11, Val:32/big>>;
|
||||||
|
serialize_property('Assigned-Client-Identifier', Val) ->
|
||||||
|
<<16#12, (serialize_utf8_string(Val))/binary>>;
|
||||||
|
serialize_property('Server-Keep-Alive', Val) ->
|
||||||
|
<<16#13, Val:16/big>>;
|
||||||
|
serialize_property('Authentication-Method', Val) ->
|
||||||
|
<<16#15, (serialize_utf8_string(Val))/binary>>;
|
||||||
|
serialize_property('Authentication-Data', Val) ->
|
||||||
|
<<16#16, (iolist_size(Val)):16, Val/binary>>;
|
||||||
|
serialize_property('Request-Problem-Information', Val) ->
|
||||||
|
<<16#17, Val>>;
|
||||||
|
serialize_property('Will-Delay-Interval', Val) ->
|
||||||
|
<<16#18, Val:32/big>>;
|
||||||
|
serialize_property('Request-Response-Information', Val) ->
|
||||||
|
<<16#19, Val>>;
|
||||||
|
serialize_property('Response-Information', Val) ->
|
||||||
|
<<16#1A, (serialize_utf8_string(Val))/binary>>;
|
||||||
|
serialize_property('Server-Reference', Val) ->
|
||||||
|
<<16#1C, (serialize_utf8_string(Val))/binary>>;
|
||||||
|
serialize_property('Reason-String', Val) ->
|
||||||
|
<<16#1F, (serialize_utf8_string(Val))/binary>>;
|
||||||
|
serialize_property('Receive-Maximum', Val) ->
|
||||||
|
<<16#21, Val:16/big>>;
|
||||||
|
serialize_property('Topic-Alias-Maximum', Val) ->
|
||||||
|
<<16#22, Val:16/big>>;
|
||||||
|
serialize_property('Topic-Alias', Val) ->
|
||||||
|
<<16#23, Val:16/big>>;
|
||||||
|
serialize_property('Maximum-QoS', Val) ->
|
||||||
|
<<16#24, Val>>;
|
||||||
|
serialize_property('Retain-Available', Val) ->
|
||||||
|
<<16#25, Val>>;
|
||||||
|
serialize_property('User-Property', {Key, Val}) ->
|
||||||
|
<<16#26, (serialize_utf8_pair({Key, Val}))/binary>>;
|
||||||
|
serialize_property('User-Property', Props) when is_list(Props) ->
|
||||||
|
<< <<(serialize_property('User-Property', {Key, Val}))/binary>>
|
||||||
|
|| {Key, Val} <- Props >>;
|
||||||
|
serialize_property('Maximum-Packet-Size', Val) ->
|
||||||
|
<<16#27, Val:32/big>>;
|
||||||
|
serialize_property('Wildcard-Subscription-Available', Val) ->
|
||||||
|
<<16#28, Val>>;
|
||||||
|
serialize_property('Subscription-Identifier-Available', Val) ->
|
||||||
|
<<16#29, Val>>;
|
||||||
|
serialize_property('Shared-Subscription-Available', Val) ->
|
||||||
|
<<16#2A, Val>>.
|
||||||
|
|
||||||
|
serialize_topic_filters(subscribe, TopicFilters, ?MQTT_PROTO_V5) ->
|
||||||
|
<< <<(serialize_utf8_string(Topic))/binary,
|
||||||
|
?RESERVED:2, Rh:2, (flag(Rap)):1,(flag(Nl)):1, QoS:2 >>
|
||||||
|
|| {Topic, #{rh := Rh, rap := Rap, nl := Nl, qos := QoS}} <- TopicFilters >>;
|
||||||
|
|
||||||
|
serialize_topic_filters(subscribe, TopicFilters, _Ver) ->
|
||||||
|
<< <<(serialize_utf8_string(Topic))/binary, ?RESERVED:6, QoS:2>>
|
||||||
|
|| {Topic, #{qos := QoS}} <- TopicFilters >>;
|
||||||
|
|
||||||
|
serialize_topic_filters(unsubscribe, TopicFilters, _Ver) ->
|
||||||
|
<< <<(serialize_utf8_string(Topic))/binary>> || Topic <- TopicFilters >>.
|
||||||
|
|
||||||
|
serialize_reason_codes(undefined) ->
|
||||||
|
<<>>;
|
||||||
|
serialize_reason_codes(ReasonCodes) when is_list(ReasonCodes) ->
|
||||||
|
<< <<Code>> || Code <- ReasonCodes >>.
|
||||||
|
|
||||||
|
serialize_utf8_pair({Name, Value}) ->
|
||||||
|
<< (serialize_utf8_string(Name))/binary, (serialize_utf8_string(Value))/binary >>.
|
||||||
|
|
||||||
|
serialize_binary_data(Bin) ->
|
||||||
|
[<<(byte_size(Bin)):16/big-unsigned-integer>>, Bin].
|
||||||
|
|
||||||
|
serialize_utf8_string(undefined, false) ->
|
||||||
|
error(utf8_string_undefined);
|
||||||
|
serialize_utf8_string(undefined, true) ->
|
||||||
|
<<>>;
|
||||||
|
serialize_utf8_string(String, _AllowNull) ->
|
||||||
|
serialize_utf8_string(String).
|
||||||
|
|
||||||
|
serialize_utf8_string(String) ->
|
||||||
|
StringBin = unicode:characters_to_binary(String),
|
||||||
|
Len = byte_size(StringBin),
|
||||||
|
true = (Len =< 16#ffff),
|
||||||
|
<<Len:16/big, StringBin/binary>>.
|
||||||
|
|
||||||
|
serialize_remaining_len(I) ->
|
||||||
|
serialize_variable_byte_integer(I).
|
||||||
|
|
||||||
|
serialize_variable_byte_integer(N) when N =< ?LOWBITS ->
|
||||||
|
<<0:1, N:7>>;
|
||||||
|
serialize_variable_byte_integer(N) ->
|
||||||
|
<<1:1, (N rem ?HIGHBIT):7, (serialize_variable_byte_integer(N div ?HIGHBIT))/binary>>.
|
||||||
|
|
||||||
|
%% Is the frame too large?
|
||||||
|
-spec(is_too_large(iodata(), pos_integer()) -> boolean()).
|
||||||
|
is_too_large(IoData, MaxSize) ->
|
||||||
|
iolist_size(IoData) >= MaxSize.
|
||||||
|
|
||||||
|
get_property(_Key, undefined, Default) ->
|
||||||
|
Default;
|
||||||
|
get_property(Key, Props, Default) ->
|
||||||
|
maps:get(Key, Props, Default).
|
||||||
|
|
||||||
|
%% Validate header if sctrict mode. See: mqtt-v5.0: 2.1.3 Flags
|
||||||
|
validate_header(?CONNECT, 0, 0, 0) -> ok;
|
||||||
|
validate_header(?CONNACK, 0, 0, 0) -> ok;
|
||||||
|
validate_header(?PUBLISH, 0, ?QOS_0, _) -> ok;
|
||||||
|
validate_header(?PUBLISH, _, ?QOS_1, _) -> ok;
|
||||||
|
validate_header(?PUBLISH, 0, ?QOS_2, _) -> ok;
|
||||||
|
validate_header(?PUBACK, 0, 0, 0) -> ok;
|
||||||
|
validate_header(?PUBREC, 0, 0, 0) -> ok;
|
||||||
|
validate_header(?PUBREL, 0, 1, 0) -> ok;
|
||||||
|
validate_header(?PUBCOMP, 0, 0, 0) -> ok;
|
||||||
|
validate_header(?SUBSCRIBE, 0, 1, 0) -> ok;
|
||||||
|
validate_header(?SUBACK, 0, 0, 0) -> ok;
|
||||||
|
validate_header(?UNSUBSCRIBE, 0, 1, 0) -> ok;
|
||||||
|
validate_header(?UNSUBACK, 0, 0, 0) -> ok;
|
||||||
|
validate_header(?PINGREQ, 0, 0, 0) -> ok;
|
||||||
|
validate_header(?PINGRESP, 0, 0, 0) -> ok;
|
||||||
|
validate_header(?DISCONNECT, 0, 0, 0) -> ok;
|
||||||
|
validate_header(?AUTH, 0, 0, 0) -> ok;
|
||||||
|
validate_header(_Type, _Dup, _QoS, _Rt) -> error(bad_frame_header).
|
||||||
|
|
||||||
|
-compile({inline, [validate_packet_id/1]}).
|
||||||
|
validate_packet_id(0) -> error(bad_packet_id);
|
||||||
|
validate_packet_id(_) -> ok.
|
||||||
|
|
||||||
|
validate_subqos([3|_]) -> error(bad_subqos);
|
||||||
|
validate_subqos([_|T]) -> validate_subqos(T);
|
||||||
|
validate_subqos([]) -> ok.
|
||||||
|
|
||||||
|
bool(0) -> false;
|
||||||
|
bool(1) -> true.
|
||||||
|
|
||||||
|
flag(undefined) -> ?RESERVED;
|
||||||
|
flag(false) -> 0;
|
||||||
|
flag(true) -> 1;
|
||||||
|
flag(X) when is_integer(X) -> X;
|
||||||
|
flag(B) when is_binary(B) -> 1.
|
||||||
|
|
||||||
|
fixqos(?PUBREL, 0) -> 1;
|
||||||
|
fixqos(?SUBSCRIBE, 0) -> 1;
|
||||||
|
fixqos(?UNSUBSCRIBE, 0) -> 1;
|
||||||
|
fixqos(_Type, QoS) -> QoS.
|
||||||
|
|
||||||
172
apps/power_gateway/src/emqtt/emqtt_props.erl
Normal file
172
apps/power_gateway/src/emqtt/emqtt_props.erl
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc MQTT5 Properties
|
||||||
|
-module(emqtt_props).
|
||||||
|
|
||||||
|
-include("emqtt.hrl").
|
||||||
|
|
||||||
|
-export([id/1, name/1, filter/2, validate/1]).
|
||||||
|
|
||||||
|
%% For tests
|
||||||
|
-export([all/0]).
|
||||||
|
|
||||||
|
-type(prop_name() :: atom()).
|
||||||
|
-type(prop_id() :: pos_integer()).
|
||||||
|
|
||||||
|
-define(PROPS_TABLE,
|
||||||
|
#{16#01 => {'Payload-Format-Indicator', 'Byte', [?PUBLISH]},
|
||||||
|
16#02 => {'Message-Expiry-Interval', 'Four-Byte-Integer', [?PUBLISH]},
|
||||||
|
16#03 => {'Content-Type', 'UTF8-Encoded-String', [?PUBLISH]},
|
||||||
|
16#08 => {'Response-Topic', 'UTF8-Encoded-String', [?PUBLISH]},
|
||||||
|
16#09 => {'Correlation-Data', 'Binary-Data', [?PUBLISH]},
|
||||||
|
16#0B => {'Subscription-Identifier', 'Variable-Byte-Integer', [?PUBLISH, ?SUBSCRIBE]},
|
||||||
|
16#11 => {'Session-Expiry-Interval', 'Four-Byte-Integer', [?CONNECT, ?CONNACK, ?DISCONNECT]},
|
||||||
|
16#12 => {'Assigned-Client-Identifier', 'UTF8-Encoded-String', [?CONNACK]},
|
||||||
|
16#13 => {'Server-Keep-Alive', 'Two-Byte-Integer', [?CONNACK]},
|
||||||
|
16#15 => {'Authentication-Method', 'UTF8-Encoded-String', [?CONNECT, ?CONNACK, ?AUTH]},
|
||||||
|
16#16 => {'Authentication-Data', 'Binary-Data', [?CONNECT, ?CONNACK, ?AUTH]},
|
||||||
|
16#17 => {'Request-Problem-Information', 'Byte', [?CONNECT]},
|
||||||
|
16#18 => {'Will-Delay-Interval', 'Four-Byte-Integer', ['WILL']},
|
||||||
|
16#19 => {'Request-Response-Information', 'Byte', [?CONNECT]},
|
||||||
|
16#1A => {'Response-Information', 'UTF8-Encoded-String', [?CONNACK]},
|
||||||
|
16#1C => {'Server-Reference', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT]},
|
||||||
|
16#1F => {'Reason-String', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT, ?PUBACK,
|
||||||
|
?PUBREC, ?PUBREL, ?PUBCOMP,
|
||||||
|
?SUBACK, ?UNSUBACK, ?AUTH]},
|
||||||
|
16#21 => {'Receive-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]},
|
||||||
|
16#22 => {'Topic-Alias-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]},
|
||||||
|
16#23 => {'Topic-Alias', 'Two-Byte-Integer', [?PUBLISH]},
|
||||||
|
16#24 => {'Maximum-QoS', 'Byte', [?CONNACK]},
|
||||||
|
16#25 => {'Retain-Available', 'Byte', [?CONNACK]},
|
||||||
|
16#26 => {'User-Property', 'UTF8-String-Pair', 'ALL'},
|
||||||
|
16#27 => {'Maximum-Packet-Size', 'Four-Byte-Integer', [?CONNECT, ?CONNACK]},
|
||||||
|
16#28 => {'Wildcard-Subscription-Available', 'Byte', [?CONNACK]},
|
||||||
|
16#29 => {'Subscription-Identifier-Available', 'Byte', [?CONNACK]},
|
||||||
|
16#2A => {'Shared-Subscription-Available', 'Byte', [?CONNACK]}
|
||||||
|
}).
|
||||||
|
|
||||||
|
-spec(id(prop_name()) -> prop_id()).
|
||||||
|
id('Payload-Format-Indicator') -> 16#01;
|
||||||
|
id('Message-Expiry-Interval') -> 16#02;
|
||||||
|
id('Content-Type') -> 16#03;
|
||||||
|
id('Response-Topic') -> 16#08;
|
||||||
|
id('Correlation-Data') -> 16#09;
|
||||||
|
id('Subscription-Identifier') -> 16#0B;
|
||||||
|
id('Session-Expiry-Interval') -> 16#11;
|
||||||
|
id('Assigned-Client-Identifier') -> 16#12;
|
||||||
|
id('Server-Keep-Alive') -> 16#13;
|
||||||
|
id('Authentication-Method') -> 16#15;
|
||||||
|
id('Authentication-Data') -> 16#16;
|
||||||
|
id('Request-Problem-Information') -> 16#17;
|
||||||
|
id('Will-Delay-Interval') -> 16#18;
|
||||||
|
id('Request-Response-Information') -> 16#19;
|
||||||
|
id('Response-Information') -> 16#1A;
|
||||||
|
id('Server-Reference') -> 16#1C;
|
||||||
|
id('Reason-String') -> 16#1F;
|
||||||
|
id('Receive-Maximum') -> 16#21;
|
||||||
|
id('Topic-Alias-Maximum') -> 16#22;
|
||||||
|
id('Topic-Alias') -> 16#23;
|
||||||
|
id('Maximum-QoS') -> 16#24;
|
||||||
|
id('Retain-Available') -> 16#25;
|
||||||
|
id('User-Property') -> 16#26;
|
||||||
|
id('Maximum-Packet-Size') -> 16#27;
|
||||||
|
id('Wildcard-Subscription-Available') -> 16#28;
|
||||||
|
id('Subscription-Identifier-Available') -> 16#29;
|
||||||
|
id('Shared-Subscription-Available') -> 16#2A;
|
||||||
|
id(Name) -> error({bad_property, Name}).
|
||||||
|
|
||||||
|
-spec(name(prop_id()) -> prop_name()).
|
||||||
|
name(16#01) -> 'Payload-Format-Indicator';
|
||||||
|
name(16#02) -> 'Message-Expiry-Interval';
|
||||||
|
name(16#03) -> 'Content-Type';
|
||||||
|
name(16#08) -> 'Response-Topic';
|
||||||
|
name(16#09) -> 'Correlation-Data';
|
||||||
|
name(16#0B) -> 'Subscription-Identifier';
|
||||||
|
name(16#11) -> 'Session-Expiry-Interval';
|
||||||
|
name(16#12) -> 'Assigned-Client-Identifier';
|
||||||
|
name(16#13) -> 'Server-Keep-Alive';
|
||||||
|
name(16#15) -> 'Authentication-Method';
|
||||||
|
name(16#16) -> 'Authentication-Data';
|
||||||
|
name(16#17) -> 'Request-Problem-Information';
|
||||||
|
name(16#18) -> 'Will-Delay-Interval';
|
||||||
|
name(16#19) -> 'Request-Response-Information';
|
||||||
|
name(16#1A) -> 'Response-Information';
|
||||||
|
name(16#1C) -> 'Server-Reference';
|
||||||
|
name(16#1F) -> 'Reason-String';
|
||||||
|
name(16#21) -> 'Receive-Maximum';
|
||||||
|
name(16#22) -> 'Topic-Alias-Maximum';
|
||||||
|
name(16#23) -> 'Topic-Alias';
|
||||||
|
name(16#24) -> 'Maximum-QoS';
|
||||||
|
name(16#25) -> 'Retain-Available';
|
||||||
|
name(16#26) -> 'User-Property';
|
||||||
|
name(16#27) -> 'Maximum-Packet-Size';
|
||||||
|
name(16#28) -> 'Wildcard-Subscription-Available';
|
||||||
|
name(16#29) -> 'Subscription-Identifier-Available';
|
||||||
|
name(16#2A) -> 'Shared-Subscription-Available';
|
||||||
|
name(Id) -> error({unsupported_property, Id}).
|
||||||
|
|
||||||
|
filter(PacketType, Props) when is_map(Props) ->
|
||||||
|
maps:from_list(filter(PacketType, maps:to_list(Props)));
|
||||||
|
|
||||||
|
filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_list(Props) ->
|
||||||
|
Filter = fun(Name) ->
|
||||||
|
case maps:find(id(Name), ?PROPS_TABLE) of
|
||||||
|
{ok, {Name, _Type, 'ALL'}} ->
|
||||||
|
true;
|
||||||
|
{ok, {Name, _Type, AllowedTypes}} ->
|
||||||
|
lists:member(PacketType, AllowedTypes);
|
||||||
|
error -> false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
[Prop || Prop = {Name, _} <- Props, Filter(Name)].
|
||||||
|
|
||||||
|
validate(Props) when is_map(Props) ->
|
||||||
|
lists:foreach(fun validate_prop/1, maps:to_list(Props)).
|
||||||
|
|
||||||
|
validate_prop(Prop = {Name, Val}) ->
|
||||||
|
case maps:find(id(Name), ?PROPS_TABLE) of
|
||||||
|
{ok, {Name, Type, _}} ->
|
||||||
|
validate_value(Type, Val)
|
||||||
|
orelse error(bad_property, Prop);
|
||||||
|
error ->
|
||||||
|
error({bad_property, Prop})
|
||||||
|
end.
|
||||||
|
|
||||||
|
validate_value('Byte', Val) ->
|
||||||
|
is_integer(Val) andalso Val =< 16#FF;
|
||||||
|
validate_value('Two-Byte-Integer', Val) ->
|
||||||
|
is_integer(Val) andalso 0 =< Val andalso Val =< 16#FFFF;
|
||||||
|
validate_value('Four-Byte-Integer', Val) ->
|
||||||
|
is_integer(Val) andalso 0 =< Val andalso Val =< 16#FFFFFFFF;
|
||||||
|
validate_value('Variable-Byte-Integer', Val) ->
|
||||||
|
is_integer(Val) andalso 0 =< Val andalso Val =< 16#7FFFFFFF;
|
||||||
|
validate_value('UTF8-String-Pair', {Name, Val}) ->
|
||||||
|
validate_value('UTF8-Encoded-String', Name)
|
||||||
|
andalso validate_value('UTF8-Encoded-String', Val);
|
||||||
|
validate_value('UTF8-String-Pair', Pairs) when is_list(Pairs) ->
|
||||||
|
lists:foldl(fun(Pair, OK) ->
|
||||||
|
OK andalso validate_value('UTF8-String-Pair', Pair)
|
||||||
|
end, true, Pairs);
|
||||||
|
validate_value('UTF8-Encoded-String', Val) ->
|
||||||
|
is_binary(Val);
|
||||||
|
validate_value('Binary-Data', Val) ->
|
||||||
|
is_binary(Val);
|
||||||
|
validate_value(_Type, _Val) -> false.
|
||||||
|
|
||||||
|
-spec(all() -> map()).
|
||||||
|
all() -> ?PROPS_TABLE.
|
||||||
|
|
||||||
120
apps/power_gateway/src/emqtt/emqtt_sock.erl
Normal file
120
apps/power_gateway/src/emqtt/emqtt_sock.erl
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqtt_sock).
|
||||||
|
|
||||||
|
-export([connect/4, send/2, recv/2, close/1 ]).
|
||||||
|
|
||||||
|
-export([ sockname/1, setopts/2, getstat/2 ]).
|
||||||
|
|
||||||
|
-record(ssl_socket, {
|
||||||
|
tcp,
|
||||||
|
ssl
|
||||||
|
}).
|
||||||
|
|
||||||
|
-type(socket() :: inet:socket() | #ssl_socket{}).
|
||||||
|
|
||||||
|
-type(sockname() :: {inet:ip_address(), inet:port_number()}).
|
||||||
|
|
||||||
|
-type(option() :: gen_tcp:connect_option() | {ssl_opts, [ssl:ssl_option()]}).
|
||||||
|
|
||||||
|
-export_type([socket/0, option/0]).
|
||||||
|
|
||||||
|
-define(DEFAULT_TCP_OPTIONS, [binary, {packet, raw}, {active, false},
|
||||||
|
{nodelay, true}]).
|
||||||
|
|
||||||
|
-spec(connect(inet:ip_address() | inet:hostname(),
|
||||||
|
inet:port_number(), [option()], timeout())
|
||||||
|
-> {ok, socket()} | {error, term()}).
|
||||||
|
connect(Host, Port, SockOpts, Timeout) ->
|
||||||
|
TcpOpts = merge_opts(?DEFAULT_TCP_OPTIONS,
|
||||||
|
lists:keydelete(ssl_opts, 1, SockOpts)),
|
||||||
|
case gen_tcp:connect(Host, Port, TcpOpts, Timeout) of
|
||||||
|
{ok, Sock} ->
|
||||||
|
case lists:keyfind(ssl_opts, 1, SockOpts) of
|
||||||
|
{ssl_opts, SslOpts} ->
|
||||||
|
ssl_upgrade(Sock, SslOpts, Timeout);
|
||||||
|
false ->
|
||||||
|
{ok, Sock}
|
||||||
|
end;
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
ssl_upgrade(Sock, SslOpts, Timeout) ->
|
||||||
|
TlsVersions = proplists:get_value(versions, SslOpts, []),
|
||||||
|
Ciphers = proplists:get_value(ciphers, SslOpts, default_ciphers(TlsVersions)),
|
||||||
|
SslOpts2 = merge_opts(SslOpts, [{ciphers, Ciphers}]),
|
||||||
|
case ssl:connect(Sock, SslOpts2, Timeout) of
|
||||||
|
{ok, SslSock} ->
|
||||||
|
ok = ssl:controlling_process(SslSock, self()),
|
||||||
|
{ok, #ssl_socket{tcp = Sock, ssl = SslSock}};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec(send(socket(), iodata()) -> ok | {error, einval | closed}).
|
||||||
|
send(Sock, Data) when is_port(Sock) ->
|
||||||
|
gen_tcp:send(Sock, Data);
|
||||||
|
send(#ssl_socket{ssl = SslSock}, Data) ->
|
||||||
|
ssl:send(SslSock, Data).
|
||||||
|
|
||||||
|
-spec(recv(socket(), non_neg_integer())
|
||||||
|
-> {ok, iodata()} | {error, closed | inet:posix()}).
|
||||||
|
recv(Sock, Length) when is_port(Sock) ->
|
||||||
|
gen_tcp:recv(Sock, Length);
|
||||||
|
recv(#ssl_socket{ssl = SslSock}, Length) ->
|
||||||
|
ssl:recv(SslSock, Length).
|
||||||
|
|
||||||
|
-spec(close(socket()) -> ok).
|
||||||
|
close(Sock) when is_port(Sock) ->
|
||||||
|
gen_tcp:close(Sock);
|
||||||
|
close(#ssl_socket{ssl = SslSock}) ->
|
||||||
|
ssl:close(SslSock).
|
||||||
|
|
||||||
|
-spec(setopts(socket(), [gen_tcp:option() | ssl:socketoption()]) -> ok).
|
||||||
|
setopts(Sock, Opts) when is_port(Sock) ->
|
||||||
|
inet:setopts(Sock, Opts);
|
||||||
|
setopts(#ssl_socket{ssl = SslSock}, Opts) ->
|
||||||
|
ssl:setopts(SslSock, Opts).
|
||||||
|
|
||||||
|
-spec(getstat(socket(), [atom()])
|
||||||
|
-> {ok, [{atom(), integer()}]} | {error, term()}).
|
||||||
|
getstat(Sock, Options) when is_port(Sock) ->
|
||||||
|
inet:getstat(Sock, Options);
|
||||||
|
getstat(#ssl_socket{tcp = Sock}, Options) ->
|
||||||
|
inet:getstat(Sock, Options).
|
||||||
|
|
||||||
|
-spec(sockname(socket()) -> {ok, sockname()} | {error, term()}).
|
||||||
|
sockname(Sock) when is_port(Sock) ->
|
||||||
|
inet:sockname(Sock);
|
||||||
|
sockname(#ssl_socket{ssl = SslSock}) ->
|
||||||
|
ssl:sockname(SslSock).
|
||||||
|
|
||||||
|
-spec(merge_opts(list(), list()) -> list()).
|
||||||
|
merge_opts(Defaults, Options) ->
|
||||||
|
lists:foldl(
|
||||||
|
fun({Opt, Val}, Acc) ->
|
||||||
|
lists:keystore(Opt, 1, Acc, {Opt, Val});
|
||||||
|
(Opt, Acc) ->
|
||||||
|
lists:usort([Opt | Acc])
|
||||||
|
end, Defaults, Options).
|
||||||
|
|
||||||
|
default_ciphers(TlsVersions) ->
|
||||||
|
lists:foldl(
|
||||||
|
fun(TlsVer, Ciphers) ->
|
||||||
|
Ciphers ++ ssl:cipher_suites(all, TlsVer)
|
||||||
|
end, [], TlsVersions).
|
||||||
92
apps/power_gateway/src/http_protocol.erl
Normal file
92
apps/power_gateway/src/http_protocol.erl
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author licheng5
|
||||||
|
%%% @copyright (C) 2020, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 26. 4月 2020 3:36 下午
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(http_protocol).
|
||||||
|
-author("licheng5").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([init/2]).
|
||||||
|
-export([json_data/1, json_error/2]).
|
||||||
|
|
||||||
|
init(Req0, Opts = [Mod|_]) ->
|
||||||
|
Method = binary_to_list(cowboy_req:method(Req0)),
|
||||||
|
Path = binary_to_list(cowboy_req:path(Req0)),
|
||||||
|
|
||||||
|
QueryStr = cowboy_req:qs(Req0),
|
||||||
|
{ok, Body, Req1} = parse_body(Req0),
|
||||||
|
|
||||||
|
try Mod:handle_request(Method, Path, QueryStr, Body) of
|
||||||
|
{ok, StatusCode, Resp} ->
|
||||||
|
AcceptEncoding = cowboy_req:header(<<"accept-encoding">>, Req1, <<>>),
|
||||||
|
Req2 = case iolist_size(Resp) >= 1024 andalso supported_gzip(AcceptEncoding) of
|
||||||
|
true ->
|
||||||
|
Resp1 = zlib:gzip(Resp),
|
||||||
|
cowboy_req:reply(StatusCode, #{
|
||||||
|
<<"Content-Type">> => <<"application/json;charset=utf-8">>,
|
||||||
|
<<"Content-Encoding">> => <<"gzip">>
|
||||||
|
}, Resp1, Req1);
|
||||||
|
false ->
|
||||||
|
cowboy_req:reply(StatusCode, #{
|
||||||
|
<<"Content-Type">> => <<"application/json;charset=utf-8">>
|
||||||
|
}, Resp, Req1)
|
||||||
|
end,
|
||||||
|
{ok, Req2, Opts}
|
||||||
|
catch
|
||||||
|
throw:Error ->
|
||||||
|
ErrResp = json_error(-1, Error),
|
||||||
|
Req2 = cowboy_req:reply(404, #{
|
||||||
|
<<"Content-Type">> => <<"application/json;charset=utf-8">>
|
||||||
|
}, ErrResp, Req1),
|
||||||
|
{ok, Req2, Opts};
|
||||||
|
_:Error:Stack ->
|
||||||
|
lager:warning("[http_handler] get error: ~p, stack: ~p", [Error, Stack]),
|
||||||
|
Req2 = cowboy_req:reply(500, #{
|
||||||
|
<<"Content-Type">> => <<"text/html;charset=utf-8">>
|
||||||
|
}, <<"Internal Server Error">>, Req1),
|
||||||
|
{ok, Req2, Opts}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 判断是否支持gzip
|
||||||
|
supported_gzip(AcceptEncoding) when is_binary(AcceptEncoding) ->
|
||||||
|
binary:match(AcceptEncoding, <<"gzip">>) =/= nomatch.
|
||||||
|
|
||||||
|
parse_body(Req0) ->
|
||||||
|
ContentType = cowboy_req:header(<<"content-type">>, Req0),
|
||||||
|
case ContentType of
|
||||||
|
<<"application/x-www-form-urlencoded">> ->
|
||||||
|
{ok, PostParams0, Req1} = cowboy_req:read_urlencoded_body(Req0),
|
||||||
|
PostParams = maps:from_list(PostParams0),
|
||||||
|
{ok, jiffy:encode(PostParams, [force_utf8]), Req1};
|
||||||
|
_ ->
|
||||||
|
{ok, Body, Req1} = read_body(Req0),
|
||||||
|
{ok, Body, Req1}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 读取请求体
|
||||||
|
read_body(Req) ->
|
||||||
|
read_body(Req, <<>>).
|
||||||
|
read_body(Req, AccData) ->
|
||||||
|
case cowboy_req:read_body(Req) of
|
||||||
|
{ok, Data, Req1} ->
|
||||||
|
{ok, <<AccData/binary, Data/binary>>, Req1};
|
||||||
|
{more, Data, Req1} ->
|
||||||
|
read_body(Req1, <<AccData/binary, Data/binary>>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
json_data(Data) ->
|
||||||
|
jiffy:encode(#{
|
||||||
|
<<"result">> => Data
|
||||||
|
}, [force_utf8]).
|
||||||
|
|
||||||
|
json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) ->
|
||||||
|
jiffy:encode(#{
|
||||||
|
<<"error">> => #{
|
||||||
|
<<"code">> => ErrCode,
|
||||||
|
<<"message">> => ErrMessage
|
||||||
|
}
|
||||||
|
}, [force_utf8]).
|
||||||
177
apps/power_gateway/src/power_device.erl
Normal file
177
apps/power_gateway/src/power_device.erl
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author anlicheng
|
||||||
|
%%% @copyright (C) 2024, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 04. 9月 2024 16:13
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(power_device).
|
||||||
|
-author("anlicheng").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/2]).
|
||||||
|
-export([get_pid/1, get_name/1]).
|
||||||
|
-export([metric_data/2, poll_status/1]).
|
||||||
|
|
||||||
|
-define(UNIT0, 16#10).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
device_uuid :: binary(),
|
||||||
|
heartbeat_ticker :: integer(),
|
||||||
|
%% 消息计数器
|
||||||
|
data_counter = 0,
|
||||||
|
%% 状态,1: 在线,0: 离线
|
||||||
|
status = 0
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-spec get_pid(DeviceUUID :: binary()) -> undefined | pid().
|
||||||
|
get_pid(DeviceUUID) when is_binary(DeviceUUID) ->
|
||||||
|
whereis(get_name(DeviceUUID)).
|
||||||
|
|
||||||
|
-spec get_name(DeviceUUID :: binary()) -> atom().
|
||||||
|
get_name(DeviceUUID) when is_binary(DeviceUUID) ->
|
||||||
|
binary_to_atom(<<"power_device:", DeviceUUID/binary>>).
|
||||||
|
|
||||||
|
-spec metric_data(Pid :: pid(), Message :: binary()) -> no_return().
|
||||||
|
metric_data(Pid, Message) when is_pid(Pid), is_binary(Message) ->
|
||||||
|
gen_server:cast(Pid, {metric_data, Message}).
|
||||||
|
|
||||||
|
%% 查询设备的当前状态, 1: 在线,0: 离线
|
||||||
|
-spec poll_status(Pid :: pid()) -> {ok, Status :: integer()}.
|
||||||
|
poll_status(Pid) when is_pid(Pid) ->
|
||||||
|
gen_server:call(Pid, poll_status).
|
||||||
|
|
||||||
|
%% @doc Spawns the server and registers the local name (unique)
|
||||||
|
-spec(start_link(Name :: atom(), DeviceUUID :: binary()) ->
|
||||||
|
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link(Name, DeviceUUID) when is_atom(Name), is_binary(DeviceUUID) ->
|
||||||
|
gen_server:start_link({local, get_name(DeviceUUID)}, ?MODULE, [DeviceUUID], []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% gen_server callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Initializes the server
|
||||||
|
-spec(init(Args :: term()) ->
|
||||||
|
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term()} | ignore).
|
||||||
|
init([DeviceUUID]) ->
|
||||||
|
{ok, HeartbeatTicker0} = application:get_env(power_gateway, heartbeat_ticker),
|
||||||
|
HeartbeatTicker = HeartbeatTicker0 * 1000,
|
||||||
|
|
||||||
|
erlang:start_timer(HeartbeatTicker, self(), heartbeat_ticker),
|
||||||
|
{ok, #state{device_uuid = DeviceUUID, heartbeat_ticker = HeartbeatTicker, status = 0}}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling call messages
|
||||||
|
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||||
|
State :: #state{}) ->
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}} |
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_call(poll_status, _From, State = #state{status = Status}) ->
|
||||||
|
{reply, {ok, Status}, State};
|
||||||
|
handle_call(_Request, _From, State = #state{}) ->
|
||||||
|
{reply, ok, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling cast messages
|
||||||
|
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_cast({metric_data, Message}, State = #state{device_uuid = DeviceUUID, data_counter = DataCounter, status = Status}) ->
|
||||||
|
case catch jiffy:decode(Message, [return_maps]) of
|
||||||
|
#{<<"properties">> := Props0} ->
|
||||||
|
Props = lists:map(fun(Fields) -> transform(Fields#{<<"device_uuid">> => DeviceUUID}) end, Props0),
|
||||||
|
Info = iolist_to_binary(jiffy:encode(Props, [force_utf8])),
|
||||||
|
case catch efka_client:send_metric_data(Props, #{}) of
|
||||||
|
{ok, _} ->
|
||||||
|
power_logger:write([<<"OK">>, Info]);
|
||||||
|
_ ->
|
||||||
|
power_logger:write([<<"ERROR">>, Info])
|
||||||
|
end,
|
||||||
|
|
||||||
|
%% 如果设备当前是离线状态,则需要发送上线消息
|
||||||
|
Status == 0 andalso efka_client:device_online(DeviceUUID),
|
||||||
|
|
||||||
|
{noreply, State#state{data_counter = DataCounter + 1, status = 1}};
|
||||||
|
M when is_map(M) ->
|
||||||
|
lager:notice("[power_device] invalid map: ~p", [M]);
|
||||||
|
Error ->
|
||||||
|
lager:notice("[power_device] jiffy decode error: ~p", [Error]),
|
||||||
|
{noreply, State}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling all non call/cast messages
|
||||||
|
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_info({timeout, _, heartbeat_ticker}, State = #state{device_uuid = DeviceUUID, heartbeat_ticker = HeartbeatTicker, data_counter = DataCounter, status = Status}) ->
|
||||||
|
erlang:start_timer(HeartbeatTicker, self(), heartbeat_ticker),
|
||||||
|
case DataCounter > 0 of
|
||||||
|
true ->
|
||||||
|
{noreply, State};
|
||||||
|
false ->
|
||||||
|
Status == 1 andalso efka_client:device_offline(DeviceUUID),
|
||||||
|
{noreply, State#state{status = 0}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc This function is called by a gen_server when it is about to
|
||||||
|
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||||
|
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||||
|
%% with Reason. The return value is ignored.
|
||||||
|
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||||
|
State :: #state{}) -> term()).
|
||||||
|
terminate(_Reason, _State = #state{}) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Convert process state when code is changed
|
||||||
|
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||||
|
Extra :: term()) ->
|
||||||
|
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||||
|
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% 网关上来的数据写反了: value是类型,unit才是数值
|
||||||
|
%% [{"value":"int","unit":44081,"type":"AI","timestamp":1730799797,"name":"使用次数","label":"次","key":"use_times","device_uuid":"30409239002349977617259608405456"}]
|
||||||
|
-spec transform(Prop :: map()) -> map().
|
||||||
|
transform(Prop = #{<<"key">> := <<"total_power">>, <<"unit">> := Unit}) ->
|
||||||
|
Prop#{<<"name">> => <<"总能耗"/utf8>>, <<"value">> => Unit, <<"unit">> => 16#02};
|
||||||
|
transform(Prop = #{<<"key">> := <<"total_runtime">>, <<"unit">> := Unit}) ->
|
||||||
|
Prop#{<<"name">> => <<"总运行时间"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0};
|
||||||
|
transform(Prop = #{<<"key">> := <<"use_times">>, <<"unit">> := Unit}) ->
|
||||||
|
Prop#{<<"name">> => <<"使用次数"/utf8>>, <<"label">> => <<"次"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0};
|
||||||
|
transform(Prop = #{<<"key">> := <<"light_switch">>, <<"unit">> := Unit}) ->
|
||||||
|
Prop#{<<"name">> => <<"开关"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0};
|
||||||
|
transform(Prop = #{<<"key">> := <<"light_brightness">>, <<"unit">> := Unit}) when is_integer(Unit) ->
|
||||||
|
Label = iolist_to_binary([<<"亮度设置为:"/utf8>>, integer_to_binary(Unit)]),
|
||||||
|
Prop#{<<"name">> => <<"亮度"/utf8>>, <<"label">> => Label, <<"value">> => Unit, <<"unit">> => ?UNIT0};
|
||||||
|
transform(Prop = #{<<"key">> := <<"light_change_time">>, <<"unit">> := Unit}) ->
|
||||||
|
Prop#{<<"name">> => <<"变亮变暗时间"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0};
|
||||||
|
transform(Prop = #{<<"key">> := <<"light_status">>, <<"unit">> := Unit}) ->
|
||||||
|
Prop#{<<"name">> => <<"是否损坏"/utf8>>, <<"value">> => Unit, <<"unit">> => ?UNIT0};
|
||||||
|
transform(Prop = #{<<"unit">> := Unit}) ->
|
||||||
|
Prop#{<<"value">> => Unit, <<"unit">> => ?UNIT0}.
|
||||||
80
apps/power_gateway/src/power_device_sup.erl
Normal file
80
apps/power_gateway/src/power_device_sup.erl
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author anlicheng
|
||||||
|
%%% @copyright (C) 2024, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 04. 9月 2024 15:39
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(power_device_sup).
|
||||||
|
-author("anlicheng").
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/0]).
|
||||||
|
-export([ensure_device_started/1, delete_device/1]).
|
||||||
|
|
||||||
|
%% Supervisor callbacks
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @doc Starts the supervisor
|
||||||
|
-spec(start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Supervisor callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3],
|
||||||
|
%% this function is called by the new process to find out about
|
||||||
|
%% restart strategy, maximum restart frequency and child
|
||||||
|
%% specifications.
|
||||||
|
init([]) ->
|
||||||
|
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||||
|
|
||||||
|
{ok, {SupFlags, []}}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-spec ensure_device_started(DeviceUUID :: binary()) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||||
|
ensure_device_started(DeviceUUID) when is_binary(DeviceUUID) ->
|
||||||
|
case power_device:get_pid(DeviceUUID) of
|
||||||
|
DevicePid when is_pid(DevicePid) ->
|
||||||
|
{ok, DevicePid};
|
||||||
|
undefined ->
|
||||||
|
case supervisor:start_child(?MODULE, child_spec(DeviceUUID)) of
|
||||||
|
{ok, Pid} when is_pid(Pid) ->
|
||||||
|
{ok, Pid};
|
||||||
|
{error, {'already_started', Pid}} when is_pid(Pid) ->
|
||||||
|
{ok, Pid};
|
||||||
|
{error, Error} ->
|
||||||
|
{error, Error}
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
delete_device(DeviceUUID) when is_binary(DeviceUUID) ->
|
||||||
|
Id = power_device:get_name(DeviceUUID),
|
||||||
|
ok = supervisor:terminate_child(?MODULE, Id),
|
||||||
|
supervisor:delete_child(?MODULE, Id).
|
||||||
|
|
||||||
|
child_spec(DeviceUUID) when is_binary(DeviceUUID) ->
|
||||||
|
Name = power_device:get_name(DeviceUUID),
|
||||||
|
#{
|
||||||
|
id => Name,
|
||||||
|
start => {power_device, start_link, [Name, DeviceUUID]},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 2000,
|
||||||
|
type => worker,
|
||||||
|
modules => ['power_device']
|
||||||
|
}.
|
||||||
22
apps/power_gateway/src/power_gateway.app.src
Normal file
22
apps/power_gateway/src/power_gateway.app.src
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{application, power_gateway,
|
||||||
|
[{description, "An OTP application"},
|
||||||
|
{vsn, "1.0"},
|
||||||
|
{registered, []},
|
||||||
|
{mod, {power_gateway_app, []}},
|
||||||
|
{applications,
|
||||||
|
[
|
||||||
|
hackney,
|
||||||
|
sync,
|
||||||
|
jiffy,
|
||||||
|
ranch,
|
||||||
|
cowboy,
|
||||||
|
lager,
|
||||||
|
kernel,
|
||||||
|
stdlib
|
||||||
|
]},
|
||||||
|
{env,[]},
|
||||||
|
{modules, []},
|
||||||
|
|
||||||
|
{licenses, ["Apache-2.0"]},
|
||||||
|
{links, []}
|
||||||
|
]}.
|
||||||
44
apps/power_gateway/src/power_gateway_app.erl
Normal file
44
apps/power_gateway/src/power_gateway_app.erl
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%% @doc power_gateway public API
|
||||||
|
%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(power_gateway_app).
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
|
||||||
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
|
start(_StartType, _StartArgs) ->
|
||||||
|
SupResult = power_gateway_sup:start_link(),
|
||||||
|
start_http_server(),
|
||||||
|
SupResult.
|
||||||
|
|
||||||
|
stop(_State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% internal functions
|
||||||
|
|
||||||
|
start_http_server() ->
|
||||||
|
{ok, Props} = application:get_env(power_gateway, http_server),
|
||||||
|
Acceptors = proplists:get_value(acceptors, Props, 50),
|
||||||
|
MaxConnections = proplists:get_value(max_connections, Props, 10240),
|
||||||
|
Backlog = proplists:get_value(backlog, Props, 1024),
|
||||||
|
Port = proplists:get_value(port, Props),
|
||||||
|
|
||||||
|
Dispatcher = cowboy_router:compile([
|
||||||
|
{'_', [
|
||||||
|
%{"/api/[...]", http_protocol, [http_push_handler]}
|
||||||
|
]}
|
||||||
|
]),
|
||||||
|
|
||||||
|
TransOpts = [
|
||||||
|
{port, Port},
|
||||||
|
{num_acceptors, Acceptors},
|
||||||
|
{backlog, Backlog},
|
||||||
|
{max_connections, MaxConnections}
|
||||||
|
],
|
||||||
|
|
||||||
|
{ok, Pid} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
|
||||||
|
|
||||||
|
lager:debug("[iot_app] the http server start at: ~p, pid is: ~p", [Port, Pid]).
|
||||||
159
apps/power_gateway/src/power_gateway_args.erl
Normal file
159
apps/power_gateway/src/power_gateway_args.erl
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 06. 9月 2023 16:37
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(power_gateway_args).
|
||||||
|
-author("aresei").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/0]).
|
||||||
|
-export([get_metric/0, get_param/0, push_metric/1, push_param/1, get_device_uuid/1]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
%% #{device_id => device_uuid}, 下发的时候是:serial
|
||||||
|
metrics = #{},
|
||||||
|
param = #{}
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-spec get_param() -> {ok, Param :: #{}}.
|
||||||
|
get_param() ->
|
||||||
|
gen_server:call(?MODULE, get_param).
|
||||||
|
|
||||||
|
-spec get_metric() -> {ok, Metric :: map()}.
|
||||||
|
get_metric() ->
|
||||||
|
gen_server:call(?MODULE, get_metric).
|
||||||
|
|
||||||
|
-spec get_device_uuid(DeviceId :: binary()) -> error | {ok, DeviceUUID :: binary()}.
|
||||||
|
get_device_uuid(DeviceId) when is_binary(DeviceId) ->
|
||||||
|
gen_server:call(?MODULE, {get_device_uuid, DeviceId}).
|
||||||
|
|
||||||
|
-spec push_param(Param :: map()) -> no_return().
|
||||||
|
push_param(Param) when is_map(Param) ->
|
||||||
|
gen_server:cast(?MODULE, {push_param, Param}).
|
||||||
|
|
||||||
|
-spec push_metric(Metrics :: list()) -> no_return().
|
||||||
|
push_metric(Metrics) when is_list(Metrics) ->
|
||||||
|
gen_server:cast(?MODULE, {push_metric, Metrics}).
|
||||||
|
|
||||||
|
%% @doc Spawns the server and registers the local name (unique)
|
||||||
|
-spec(start_link() ->
|
||||||
|
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% gen_server callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Initializes the server
|
||||||
|
-spec(init(Args :: term()) ->
|
||||||
|
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term()} | ignore).
|
||||||
|
init([]) ->
|
||||||
|
{ok, Metrics} = efka_client:request_metric(),
|
||||||
|
try convert_metric(Metrics) of
|
||||||
|
{ok, MetricMap} ->
|
||||||
|
lager:debug("[power_gateway_args] init load metric_map: ~p", [MetricMap]),
|
||||||
|
{ok, Param} = efka_client:request_param(),
|
||||||
|
|
||||||
|
{ok, #state{metrics = MetricMap, param = Param}}
|
||||||
|
catch
|
||||||
|
_:Error:Stack->
|
||||||
|
lager:warning("[power_gateway_args] request_metric get error: ~p, stack: ~p", [Error, Stack]),
|
||||||
|
{ok, #state{metrics = #{}, param = #{}}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling call messages
|
||||||
|
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||||
|
State :: #state{}) ->
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}} |
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_call(get_param, _From, State = #state{param = Param}) ->
|
||||||
|
{reply, {ok, Param}, State};
|
||||||
|
handle_call({get_device_uuid, DeviceId}, _From, State = #state{metrics = Metrics}) when is_map(Metrics) ->
|
||||||
|
Result = maps:find(DeviceId, Metrics),
|
||||||
|
{reply, Result, State};
|
||||||
|
handle_call({get_device_uuid, _DeviceId}, _From, State) ->
|
||||||
|
{reply, error, State};
|
||||||
|
|
||||||
|
handle_call(get_metric, _From, State = #state{metrics = Metrics}) ->
|
||||||
|
{reply, {ok, Metrics}, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling cast messages
|
||||||
|
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_cast({push_param, Param}, State = #state{}) ->
|
||||||
|
{noreply, State#state{param = Param}};
|
||||||
|
handle_cast({push_metric, Metrics}, State = #state{}) ->
|
||||||
|
try convert_metric(Metrics) of
|
||||||
|
{ok, MetricMap} ->
|
||||||
|
lager:debug("[power_gateway_args] push metric_map: ~p", [MetricMap]),
|
||||||
|
{noreply, State#state{metrics = MetricMap}}
|
||||||
|
catch _:_ ->
|
||||||
|
{noreply, State}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling all non call/cast messages
|
||||||
|
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_info(_Info, State = #state{}) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc This function is called by a gen_server when it is about to
|
||||||
|
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||||
|
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||||
|
%% with Reason. The return value is ignored.
|
||||||
|
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||||
|
State :: #state{}) -> term()).
|
||||||
|
terminate(_Reason, _State = #state{}) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Convert process state when code is changed
|
||||||
|
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||||
|
Extra :: term()) ->
|
||||||
|
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||||
|
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-spec convert_metric(Metrics :: list()) -> {ok, map()}.
|
||||||
|
convert_metric(Metrics) when is_list(Metrics) ->
|
||||||
|
MetricTuples = lists:flatmap(fun
|
||||||
|
(#{<<"serial">> := DeviceId, <<"device_uuid">> := DeviceUUID}) ->
|
||||||
|
[{DeviceId, DeviceUUID}];
|
||||||
|
(_) ->
|
||||||
|
[]
|
||||||
|
end, Metrics),
|
||||||
|
{ok, maps:from_list(MetricTuples)}.
|
||||||
195
apps/power_gateway/src/power_gateway_mqtt_subscriber.erl
Normal file
195
apps/power_gateway/src/power_gateway_mqtt_subscriber.erl
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%% 1. 需要考虑集群部署的相关问题,上行的数据可能在集群中共享
|
||||||
|
%%% 2. host进程不能直接去监听topic,这样涉及到新增和下线的很多问题
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 12. 3月 2023 21:27
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(power_gateway_mqtt_subscriber).
|
||||||
|
-author("aresei").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
%% 需要订阅的主题信息
|
||||||
|
-define(Topics,[
|
||||||
|
{<<"/lighting/+/data">>, 2}
|
||||||
|
]).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
conn_pid :: pid()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @doc Spawns the server and registers the local name (unique)
|
||||||
|
-spec(start_link() ->
|
||||||
|
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% gen_server callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Initializes the server
|
||||||
|
-spec(init(Args :: term()) ->
|
||||||
|
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term()} | ignore).
|
||||||
|
init([]) ->
|
||||||
|
%% 建立到emqx服务器的连接
|
||||||
|
Opts = emqx_opts(<<"power-subscriber">>),
|
||||||
|
lager:debug("[opts] is: ~p", [Opts]),
|
||||||
|
case emqtt:start_link(Opts) of
|
||||||
|
{ok, ConnPid} ->
|
||||||
|
lager:debug("[mqtt_subscriber] start conntecting, pid: ~p", [ConnPid]),
|
||||||
|
{ok, _} = emqtt:connect(ConnPid),
|
||||||
|
lager:debug("[mqtt_subscriber] connect success"),
|
||||||
|
|
||||||
|
%% 监听和设备的全部事件
|
||||||
|
SubscribeResult = emqtt:subscribe(ConnPid, ?Topics),
|
||||||
|
lager:debug("[mqtt_subscriber] subscribe topics: ~p, result is: ~p", [?Topics, SubscribeResult]),
|
||||||
|
|
||||||
|
{ok, #state{conn_pid = ConnPid}};
|
||||||
|
ignore ->
|
||||||
|
lager:debug("[mqtt_subscriber] connect emqx get ignore"),
|
||||||
|
{stop, ignore};
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:debug("[mqtt_subscriber] connect emqx get error: ~p", [Reason]),
|
||||||
|
{stop, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling call messages
|
||||||
|
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||||
|
State :: #state{}) ->
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}} |
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_call(_Info, _From, State = #state{conn_pid = _ConnPid}) ->
|
||||||
|
{reply, ok, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling cast messages
|
||||||
|
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_cast(_Request, State = #state{}) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling all non call/cast messages
|
||||||
|
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_info({disconnect, ReasonCode, Properties}, State = #state{}) ->
|
||||||
|
lager:debug("[mqtt_subscriber] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
||||||
|
{stop, disconnected, State};
|
||||||
|
%% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行
|
||||||
|
handle_info({publish, #{packet_id := _PacketId, payload := Payload, qos := Qos, topic := Topic}}, State = #state{conn_pid = _ConnPid}) ->
|
||||||
|
lager:debug("[mqtt_subscriber] Recv a topic: ~p, publish packet: ~p, qos: ~p", [Topic, Payload, Qos]),
|
||||||
|
%% 将消息分发到对应的publisher进程去处理, 提高消息的处理速度; topic格式: /lighting/C0AA213BD7F0/data
|
||||||
|
case Topic of
|
||||||
|
<<"/lighting/", DeviceInfo/binary>> ->
|
||||||
|
[DeviceMac, <<"data">>] = binary:split(DeviceInfo, <<"/">>),
|
||||||
|
dispatch(DeviceMac, Payload);
|
||||||
|
_ ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
{noreply, State};
|
||||||
|
handle_info({puback, Packet = #{packet_id := _PacketId}}, State = #state{}) ->
|
||||||
|
lager:debug("[mqtt_subscriber] receive puback packet: ~p", [Packet]),
|
||||||
|
{noreply, State};
|
||||||
|
|
||||||
|
handle_info(Info, State = #state{}) ->
|
||||||
|
lager:debug("[mqtt_subscriber] get info: ~p", [Info]),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc This function is called by a gen_server when it is about to
|
||||||
|
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||||
|
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||||
|
%% with Reason. The return value is ignored.
|
||||||
|
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||||
|
State :: #state{}) -> term()).
|
||||||
|
terminate(Reason, _State = #state{conn_pid = ConnPid}) when is_pid(ConnPid) ->
|
||||||
|
%% 取消topic的订阅
|
||||||
|
TopicNames = lists:map(fun({Name, _}) -> Name end, ?Topics),
|
||||||
|
{ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, TopicNames),
|
||||||
|
|
||||||
|
ok = emqtt:disconnect(ConnPid),
|
||||||
|
lager:debug("[iot_mqtt_subscriber] terminate with reason: ~p", [Reason]),
|
||||||
|
ok;
|
||||||
|
terminate(Reason, _State) ->
|
||||||
|
lager:debug("[iot_mqtt_subscriber] terminate with reason: ~p", [Reason]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Convert process state when code is changed
|
||||||
|
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||||
|
Extra :: term()) ->
|
||||||
|
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||||
|
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-spec emqx_opts(ClientSuffix :: binary()) -> list().
|
||||||
|
emqx_opts(ClientSuffix) when is_binary(ClientSuffix) ->
|
||||||
|
%% 建立到emqx服务器的连接
|
||||||
|
{ok, Props} = application:get_env(power_gateway, emqx_server),
|
||||||
|
EMQXHost = proplists:get_value(host, Props),
|
||||||
|
EMQXPort = proplists:get_value(port, Props, 1883),
|
||||||
|
Username = proplists:get_value(username, Props),
|
||||||
|
Password = proplists:get_value(password, Props),
|
||||||
|
RetryInterval = proplists:get_value(retry_interval, Props, 5),
|
||||||
|
Keepalive = proplists:get_value(keepalive, Props, 86400),
|
||||||
|
|
||||||
|
Node = atom_to_binary(node()),
|
||||||
|
ClientId = <<"mqtt-client-", Node/binary, "-", ClientSuffix/binary>>,
|
||||||
|
[
|
||||||
|
{clientid, ClientId},
|
||||||
|
{host, EMQXHost},
|
||||||
|
{port, EMQXPort},
|
||||||
|
{owner, self()},
|
||||||
|
{tcp_opts, []},
|
||||||
|
{username, Username},
|
||||||
|
{password, Password},
|
||||||
|
{keepalive, Keepalive},
|
||||||
|
{auto_ack, true},
|
||||||
|
{proto_ver, v3},
|
||||||
|
{retry_interval, RetryInterval}
|
||||||
|
].
|
||||||
|
|
||||||
|
-spec dispatch(DeviceMac :: binary(), Message :: binary()) -> no_return().
|
||||||
|
dispatch(DeviceMac, Message) when is_binary(DeviceMac), is_binary(Message) ->
|
||||||
|
case power_gateway_args:get_device_uuid(DeviceMac) of
|
||||||
|
error ->
|
||||||
|
lager:notice("[mqtt_subscriber] device_mac: ~p, device_uuid not found", [DeviceMac]);
|
||||||
|
{ok, DeviceUUID} ->
|
||||||
|
case power_device_sup:ensure_device_started(DeviceUUID) of
|
||||||
|
{ok, DevicePid} ->
|
||||||
|
power_device:metric_data(DevicePid, Message);
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:notice("[mqtt_subscriber] start device get error: ~p", [Reason])
|
||||||
|
end
|
||||||
|
end.
|
||||||
103
apps/power_gateway/src/power_gateway_sup.erl
Normal file
103
apps/power_gateway/src/power_gateway_sup.erl
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%% @doc power_gateway top level supervisor.
|
||||||
|
%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(power_gateway_sup).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
|
||||||
|
|
||||||
|
%% sup_flags() = #{strategy => strategy(), % optional
|
||||||
|
%% intensity => non_neg_integer(), % optional
|
||||||
|
%% period => pos_integer()} % optional
|
||||||
|
%% child_spec() = #{id => child_id(), % mandatory
|
||||||
|
%% start => mfargs(), % mandatory
|
||||||
|
%% restart => restart(), % optional
|
||||||
|
%% shutdown => shutdown(), % optional
|
||||||
|
%% type => worker(), % optional
|
||||||
|
%% modules => modules()} % optional
|
||||||
|
init([]) ->
|
||||||
|
SupFlags = #{strategy => one_for_one,
|
||||||
|
intensity => 1000,
|
||||||
|
period => 3600},
|
||||||
|
|
||||||
|
{ok, EfkaServer} = application:get_env(power_gateway, efka_server),
|
||||||
|
Host = proplists:get_value(host, EfkaServer),
|
||||||
|
Port = proplists:get_value(port, EfkaServer),
|
||||||
|
|
||||||
|
%% 获取配置的名称
|
||||||
|
RegisterName = read_service_name(),
|
||||||
|
|
||||||
|
ChildSpecs = [
|
||||||
|
#{
|
||||||
|
id => 'efka_client',
|
||||||
|
start => {'efka_client', start_link, [RegisterName, Host, Port]},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 2000,
|
||||||
|
type => worker,
|
||||||
|
modules => ['efka_client']
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
id => 'power_logger',
|
||||||
|
start => {'power_logger', start_link, ["device_data"]},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 2000,
|
||||||
|
type => worker,
|
||||||
|
modules => ['power_logger']
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
id => 'power_gateway_args',
|
||||||
|
start => {'power_gateway_args', start_link, []},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 2000,
|
||||||
|
type => worker,
|
||||||
|
modules => ['power_gateway_args']
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
id => 'power_device_sup',
|
||||||
|
start => {'power_device_sup', start_link, []},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 2000,
|
||||||
|
type => worker,
|
||||||
|
modules => ['power_device_sup']
|
||||||
|
},
|
||||||
|
|
||||||
|
#{
|
||||||
|
id => 'power_gateway_mqtt_subscriber',
|
||||||
|
start => {'power_gateway_mqtt_subscriber', start_link, []},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 2000,
|
||||||
|
type => worker,
|
||||||
|
modules => ['power_gateway_mqtt_subscriber']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
{ok, {SupFlags, ChildSpecs}}.
|
||||||
|
|
||||||
|
%% internal functions
|
||||||
|
|
||||||
|
%% 获取根目录下的配置文件
|
||||||
|
read_service_name() ->
|
||||||
|
%% 获取配置的名称
|
||||||
|
RootDir = code:root_dir(),
|
||||||
|
VersionFile = RootDir ++ "/.version",
|
||||||
|
lager:debug("[gateway_sup] version path is: ~p", [VersionFile]),
|
||||||
|
case file:read_file(VersionFile) of
|
||||||
|
{ok, RegisterName0} ->
|
||||||
|
string:trim(RegisterName0);
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:warning("[power_app] read .version file get error: ~p", [Reason]),
|
||||||
|
<<"power_gateway">>
|
||||||
|
end.
|
||||||
169
apps/power_gateway/src/power_logger.erl
Normal file
169
apps/power_gateway/src/power_logger.erl
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 07. 9月 2023 17:07
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(power_logger).
|
||||||
|
-author("aresei").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/1, write/1]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
|
|
||||||
|
-define(SERVER, ?MODULE).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
file_name :: string(),
|
||||||
|
date :: calendar:date(),
|
||||||
|
file
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
write(Data) ->
|
||||||
|
gen_server:cast(?SERVER, {write, Data}).
|
||||||
|
|
||||||
|
%% @doc Spawns the server and registers the local name (unique)
|
||||||
|
-spec(start_link(FileName :: string()) ->
|
||||||
|
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link(FileName) when is_list(FileName) ->
|
||||||
|
gen_server:start_link({local, ?SERVER}, ?MODULE, [FileName], []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% gen_server callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Initializes the server
|
||||||
|
-spec(init(Args :: term()) ->
|
||||||
|
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term()} | ignore).
|
||||||
|
init([FileName]) ->
|
||||||
|
ensure_dir(),
|
||||||
|
FilePath = make_file(FileName, erlang:date()),
|
||||||
|
{ok, File} = file:open(FilePath, [append, binary]),
|
||||||
|
|
||||||
|
{ok, #state{file = File, file_name = FileName, date = get_date()}}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling call messages
|
||||||
|
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
|
||||||
|
State :: #state{}) ->
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}} |
|
||||||
|
{reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_call(_Request, _From, State = #state{}) ->
|
||||||
|
{reply, ok, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling cast messages
|
||||||
|
-spec(handle_cast(Request :: term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_cast({write, Data}, State = #state{file = OldFile, file_name = FileName, date = Date}) ->
|
||||||
|
Line = iolist_to_binary([time_prefix(), <<" ">>, format(Data), <<$\n>>]),
|
||||||
|
NDate = erlang:date(),
|
||||||
|
case maybe_new_file(Date, NDate) of
|
||||||
|
true ->
|
||||||
|
file:close(OldFile),
|
||||||
|
|
||||||
|
FilePath = make_file(FileName, NDate),
|
||||||
|
{ok, File} = file:open(FilePath, [append, binary]),
|
||||||
|
ok = file:write(File, Line),
|
||||||
|
%% 清理历史的文件, 单位: 天
|
||||||
|
delete_old_files(FileName, 10),
|
||||||
|
|
||||||
|
{noreply, State#state{file = File, date = get_date()}};
|
||||||
|
false ->
|
||||||
|
ok = file:write(OldFile, Line),
|
||||||
|
{noreply, State}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Handling all non call/cast messages
|
||||||
|
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
|
||||||
|
{noreply, NewState :: #state{}} |
|
||||||
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
|
handle_info(_Info, State = #state{}) ->
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc This function is called by a gen_server when it is about to
|
||||||
|
%% terminate. It should be the opposite of Module:init/1 and do any
|
||||||
|
%% necessary cleaning up. When it returns, the gen_server terminates
|
||||||
|
%% with Reason. The return value is ignored.
|
||||||
|
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||||
|
State :: #state{}) -> term()).
|
||||||
|
terminate(_Reason, _State = #state{}) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Convert process state when code is changed
|
||||||
|
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
|
||||||
|
Extra :: term()) ->
|
||||||
|
{ok, NewState :: #state{}} | {error, Reason :: term()}).
|
||||||
|
code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
format(Data) when is_binary(Data) ->
|
||||||
|
iolist_to_binary(Data);
|
||||||
|
format(Items) when is_list(Items) ->
|
||||||
|
iolist_to_binary(lists:join(<<"\t">>, Items)).
|
||||||
|
|
||||||
|
time_prefix() ->
|
||||||
|
{{Y, M, D}, {H, I, S}} = calendar:local_time(),
|
||||||
|
iolist_to_binary(io_lib:format("[~b-~2..0b-~2..0b ~2..0b:~2..0b:~2..0b]", [Y, M, D, H, I, S])).
|
||||||
|
|
||||||
|
-spec make_file(LogFile :: string(), {Year :: integer(), Month :: integer(), Day :: integer()}) -> string().
|
||||||
|
make_file(LogFile, {Year, Month, Day}) when is_list(LogFile) ->
|
||||||
|
Suffix = io_lib:format("~b~2..0b~2..0b", [Year, Month, Day]),
|
||||||
|
RootDir = code:root_dir() ++ "/log/",
|
||||||
|
lists:flatten(RootDir ++ LogFile ++ "." ++ Suffix).
|
||||||
|
|
||||||
|
ensure_dir() ->
|
||||||
|
RootDir = code:root_dir() ++ "/log/",
|
||||||
|
case filelib:is_dir(RootDir) of
|
||||||
|
true ->
|
||||||
|
ok;
|
||||||
|
false ->
|
||||||
|
ok = file:make_dir(RootDir)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 获取日期信息
|
||||||
|
-spec get_date() -> Date :: calendar:date().
|
||||||
|
get_date() ->
|
||||||
|
{Date, _} = calendar:local_time(),
|
||||||
|
Date.
|
||||||
|
|
||||||
|
%% 通过日志判断是否需要生成新的日志文件
|
||||||
|
-spec maybe_new_file(Date :: calendar:date(), Date0 :: calendar:date()) -> boolean().
|
||||||
|
maybe_new_file({Y, M, D}, {Y0, M0, D0}) ->
|
||||||
|
not (Y =:= Y0 andalso M =:= M0 andalso D =:= D0).
|
||||||
|
|
||||||
|
-spec delete_old_files(FileName :: string(), Days :: integer()) -> no_return().
|
||||||
|
delete_old_files(FileName, Days) when is_list(FileName), is_integer(Days) ->
|
||||||
|
Seconds0 = calendar:datetime_to_gregorian_seconds(calendar:local_time()),
|
||||||
|
Seconds = Seconds0 - Days * 86400,
|
||||||
|
lists:foreach(fun(Day) ->
|
||||||
|
{Date, _} = calendar:gregorian_seconds_to_datetime(Seconds - Day * 86400),
|
||||||
|
FilePath = make_file(FileName, Date),
|
||||||
|
filelib:is_file(FilePath) andalso file:delete(FilePath)
|
||||||
|
end, lists:seq(1, 10)).
|
||||||
3
boot.sh
Executable file
3
boot.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker run -e TZ=Asia/Shanghai --hostname=power_gateway --net=host --restart=always -v /data/docker/power_gateway/:/data/power_gateway/ power_gateway:latest
|
||||||
4
build.sh
Executable file
4
build.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
rebar3 compile && rebar3 release && rebar3 tar
|
||||||
|
|
||||||
|
tar -czvf power_gateway.tgz boot.sh stop.sh Dockerfile .args.yaml
|
||||||
66
config/sys-dev.config
Normal file
66
config/sys-dev.config
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
[
|
||||||
|
{power_gateway, [
|
||||||
|
|
||||||
|
{http_server, [
|
||||||
|
{port, 18082},
|
||||||
|
{acceptors, 500},
|
||||||
|
{max_connections, 10240},
|
||||||
|
{backlog, 10240}
|
||||||
|
]},
|
||||||
|
|
||||||
|
%% 离线判断时间间隔,单位:(秒)
|
||||||
|
{heartbeat_ticker, 120},
|
||||||
|
|
||||||
|
{emqx_server, [
|
||||||
|
{host, "39.98.184.67"},
|
||||||
|
{port, 1883},
|
||||||
|
{tcp_opts, []},
|
||||||
|
{username, "test"},
|
||||||
|
{password, "test1234"},
|
||||||
|
{keepalive, 86400},
|
||||||
|
{retry_interval, 5}
|
||||||
|
]},
|
||||||
|
|
||||||
|
{efka_server, [
|
||||||
|
{host, "39.98.184.67"},
|
||||||
|
{port, 3361}
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
|
||||||
|
%% 系统日志配置,系统日志为lager, 支持日志按日期自动分割
|
||||||
|
{lager, [
|
||||||
|
{colored, true},
|
||||||
|
%% Whether to write a crash log, and where. Undefined means no crash logger.
|
||||||
|
{crash_log, "trade_hub.crash.log"},
|
||||||
|
%% Maximum size in bytes of events in the crash log - defaults to 65536
|
||||||
|
{crash_log_msg_size, 65536},
|
||||||
|
%% Maximum size of the crash log in bytes, before its rotated, set
|
||||||
|
%% to 0 to disable rotation - default is 0
|
||||||
|
{crash_log_size, 10485760},
|
||||||
|
%% What time to rotate the crash log - default is no time
|
||||||
|
%% rotation. See the README for a description of this format.
|
||||||
|
{crash_log_date, "$D0"},
|
||||||
|
%% Number of rotated crash logs to keep, 0 means keep only the
|
||||||
|
%% current one - default is 0
|
||||||
|
{crash_log_count, 5},
|
||||||
|
%% Whether to redirect error_logger messages into lager - defaults to true
|
||||||
|
{error_logger_redirect, true},
|
||||||
|
|
||||||
|
%% How big the gen_event mailbox can get before it is switched into sync mode
|
||||||
|
{async_threshold, 20},
|
||||||
|
%% Switch back to async mode, when gen_event mailbox size decrease from `async_threshold'
|
||||||
|
%% to async_threshold - async_threshold_window
|
||||||
|
{async_threshold_window, 5},
|
||||||
|
|
||||||
|
{handlers, [
|
||||||
|
%% debug | info | warning | error, 日志级别
|
||||||
|
{lager_console_backend, debug},
|
||||||
|
{lager_file_backend, [{file, "debug.log"}, {level, debug}, {size, 314572800}]},
|
||||||
|
{lager_file_backend, [{file, "info.log"}, {level, info}, {size, 314572800}]},
|
||||||
|
{lager_file_backend, [{file, "notice.log"}, {level, notice}, {size, 314572800}]},
|
||||||
|
{lager_file_backend, [{file, "warning.log"}, {level, warning}, {size, 314572800}]},
|
||||||
|
{lager_file_backend, [{file, "error.log"}, {level, error}, {size, 314572800}]}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
|
||||||
|
].
|
||||||
66
config/sys-prod.config
Normal file
66
config/sys-prod.config
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
[
|
||||||
|
{power_gateway, [
|
||||||
|
|
||||||
|
{http_server, [
|
||||||
|
{port, 18082},
|
||||||
|
{acceptors, 500},
|
||||||
|
{max_connections, 10240},
|
||||||
|
{backlog, 10240}
|
||||||
|
]},
|
||||||
|
|
||||||
|
%% 离线判断时间间隔,单位:(秒)
|
||||||
|
{heartbeat_ticker, 5400},
|
||||||
|
|
||||||
|
{emqx_server, [
|
||||||
|
{host, "172.30.37.212"},
|
||||||
|
{port, 1883},
|
||||||
|
{tcp_opts, []},
|
||||||
|
{username, "power_gateway"},
|
||||||
|
{password, "PqRsTuVwXyZ!@#xy"},
|
||||||
|
{keepalive, 86400},
|
||||||
|
{retry_interval, 5}
|
||||||
|
]},
|
||||||
|
|
||||||
|
{efka_server, [
|
||||||
|
{host, "localhost"},
|
||||||
|
{port, 3361}
|
||||||
|
]}
|
||||||
|
]},
|
||||||
|
|
||||||
|
%% 系统日志配置,系统日志为lager, 支持日志按日期自动分割
|
||||||
|
{lager, [
|
||||||
|
{colored, true},
|
||||||
|
%% Whether to write a crash log, and where. Undefined means no crash logger.
|
||||||
|
{crash_log, "trade_hub.crash.log"},
|
||||||
|
%% Maximum size in bytes of events in the crash log - defaults to 65536
|
||||||
|
{crash_log_msg_size, 65536},
|
||||||
|
%% Maximum size of the crash log in bytes, before its rotated, set
|
||||||
|
%% to 0 to disable rotation - default is 0
|
||||||
|
{crash_log_size, 10485760},
|
||||||
|
%% What time to rotate the crash log - default is no time
|
||||||
|
%% rotation. See the README for a description of this format.
|
||||||
|
{crash_log_date, "$D0"},
|
||||||
|
%% Number of rotated crash logs to keep, 0 means keep only the
|
||||||
|
%% current one - default is 0
|
||||||
|
{crash_log_count, 5},
|
||||||
|
%% Whether to redirect error_logger messages into lager - defaults to true
|
||||||
|
{error_logger_redirect, true},
|
||||||
|
|
||||||
|
%% How big the gen_event mailbox can get before it is switched into sync mode
|
||||||
|
{async_threshold, 20},
|
||||||
|
%% Switch back to async mode, when gen_event mailbox size decrease from `async_threshold'
|
||||||
|
%% to async_threshold - async_threshold_window
|
||||||
|
{async_threshold_window, 5},
|
||||||
|
|
||||||
|
{handlers, [
|
||||||
|
%% debug | info | warning | error, 日志级别
|
||||||
|
% {lager_console_backend, debug},
|
||||||
|
{lager_file_backend, [{file, "debug.log"}, {level, debug}, {size, 314572800}]},
|
||||||
|
{lager_file_backend, [{file, "info.log"}, {level, info}, {size, 314572800}]},
|
||||||
|
{lager_file_backend, [{file, "notice.log"}, {level, notice}, {size, 314572800}]},
|
||||||
|
{lager_file_backend, [{file, "warning.log"}, {level, warning}, {size, 314572800}]},
|
||||||
|
{lager_file_backend, [{file, "error.log"}, {level, error}, {size, 314572800}]}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
|
||||||
|
].
|
||||||
6
config/vm.args
Normal file
6
config/vm.args
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
-sname power_gateway
|
||||||
|
|
||||||
|
-setcookie power_gateway_cookie
|
||||||
|
|
||||||
|
+K true
|
||||||
|
+A30
|
||||||
42
docs/recevie_data.md
Normal file
42
docs/recevie_data.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
## 接受到的数据类型包括
|
||||||
|
### total_power
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"value": 30074,
|
||||||
|
"unit": 2,
|
||||||
|
"type": "AI",
|
||||||
|
"timestamp": 1763395200,
|
||||||
|
"name": "总能耗",
|
||||||
|
"label": "W.h",
|
||||||
|
"key": "total_power",
|
||||||
|
"device_uuid": "33806774076388556817340611953938"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### total_runtime
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"value": 65,
|
||||||
|
"unit": 16,
|
||||||
|
"type": "AI",
|
||||||
|
"timestamp": 1763395200,
|
||||||
|
"name": "总运行时间",
|
||||||
|
"label": "h",
|
||||||
|
"key": "total_runtime",
|
||||||
|
"device_uuid": "33806774076388556817340611953938"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### use_times
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"value": 15301,
|
||||||
|
"unit": 16,
|
||||||
|
"type": "AI",
|
||||||
|
"timestamp": 1763395200,
|
||||||
|
"name": "使用次数",
|
||||||
|
"label": "次",
|
||||||
|
"key": "use_times",
|
||||||
|
"device_uuid": "33806741326004633617340611173101"
|
||||||
|
}
|
||||||
|
```
|
||||||
BIN
docs/灯联网网关推送规格书.pdf
Normal file
BIN
docs/灯联网网关推送规格书.pdf
Normal file
Binary file not shown.
46
rebar.config
Normal file
46
rebar.config
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
{erl_opts, [debug_info]}.
|
||||||
|
{deps, [
|
||||||
|
{poolboy, ".*", {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.1"}}},
|
||||||
|
{hackney, ".*", {git, "https://github.com/benoitc/hackney.git", {tag, "1.16.0"}}},
|
||||||
|
{sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}},
|
||||||
|
{jiffy, ".*", {git, "https://github.com/davisp/jiffy.git", {tag, "1.1.1"}}},
|
||||||
|
{mysql, ".*", {git, "https://github.com/mysql-otp/mysql-otp", {tag, "1.8.0"}}},
|
||||||
|
{cowboy, ".*", {git, "https://github.com/ninenines/cowboy.git", {tag, "2.10.0"}}},
|
||||||
|
{parse_trans, ".*", {git, "https://github.com/uwiger/parse_trans", {tag, "3.0.0"}}},
|
||||||
|
{lager, ".*", {git,"https://github.com/erlang-lager/lager.git", {tag, "3.9.2"}}}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{relx, [{release, {power_gateway, "1.0"},
|
||||||
|
[power_gateway,
|
||||||
|
sasl]},
|
||||||
|
|
||||||
|
{mode, dev},
|
||||||
|
{include_erts, false},
|
||||||
|
|
||||||
|
%% automatically picked up if the files
|
||||||
|
%% exist but can be set manually, which
|
||||||
|
%% is required if the names aren't exactly
|
||||||
|
%% sys.config and vm.args
|
||||||
|
{sys_config, "./config/sys.config"},
|
||||||
|
{vm_args, "./config/vm.args"}
|
||||||
|
|
||||||
|
%% the .src form of the configuration files do
|
||||||
|
%% not require setting RELX_REPLACE_OS_VARS
|
||||||
|
%% {sys_config_src, "./config/sys.config.src"},
|
||||||
|
%% {vm_args_src, "./config/vm.args.src"}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
{profiles, [{prod, [{relx,
|
||||||
|
[%% prod is the default mode when prod
|
||||||
|
%% profile is used, so does not have
|
||||||
|
%% to be explicitly included like this
|
||||||
|
{mode, prod}
|
||||||
|
|
||||||
|
%% use minimal mode to exclude ERTS
|
||||||
|
%% {mode, minimal}
|
||||||
|
]
|
||||||
|
}]}]}.
|
||||||
|
|
||||||
|
{erl_opts, [{parse_transform,lager_transform}]}.
|
||||||
|
|
||||||
|
{rebar_packages_cdn, "https://hexpm.upyun.com"}.
|
||||||
69
rebar.lock
Normal file
69
rebar.lock
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{"1.2.0",
|
||||||
|
[{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.2">>},1},
|
||||||
|
{<<"cowboy">>,
|
||||||
|
{git,"https://github.com/ninenines/cowboy.git",
|
||||||
|
{ref,"9e600f6c1df3c440bc196b66ebbc005d70107217"}},
|
||||||
|
0},
|
||||||
|
{<<"cowlib">>,
|
||||||
|
{git,"https://github.com/ninenines/cowlib",
|
||||||
|
{ref,"cc04201c1d0e1d5603cd1cde037ab729b192634c"}},
|
||||||
|
1},
|
||||||
|
{<<"fs">>,{pkg,<<"fs">>,<<"6.1.1">>},1},
|
||||||
|
{<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1},
|
||||||
|
{<<"hackney">>,
|
||||||
|
{git,"https://github.com/benoitc/hackney.git",
|
||||||
|
{ref,"f3e9292db22c807e73f57a8422402d6b423ddf5f"}},
|
||||||
|
0},
|
||||||
|
{<<"idna">>,{pkg,<<"idna">>,<<"6.0.1">>},1},
|
||||||
|
{<<"jiffy">>,
|
||||||
|
{git,"https://github.com/davisp/jiffy.git",
|
||||||
|
{ref,"9ea1b35b6e60ba21dfd4adbd18e7916a831fd7d4"}},
|
||||||
|
0},
|
||||||
|
{<<"lager">>,
|
||||||
|
{git,"https://github.com/erlang-lager/lager.git",
|
||||||
|
{ref,"459a3b2cdd9eadd29e5a7ce5c43932f5ccd6eb88"}},
|
||||||
|
0},
|
||||||
|
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1},
|
||||||
|
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},1},
|
||||||
|
{<<"mysql">>,
|
||||||
|
{git,"https://github.com/mysql-otp/mysql-otp",
|
||||||
|
{ref,"caf5ff96c677a8fe0ce6f4082bc036c8fd27dd62"}},
|
||||||
|
0},
|
||||||
|
{<<"parse_trans">>,
|
||||||
|
{git,"https://github.com/uwiger/parse_trans",
|
||||||
|
{ref,"6f3645afb43c7c57d61b54ef59aecab288ce1013"}},
|
||||||
|
0},
|
||||||
|
{<<"poolboy">>,
|
||||||
|
{git,"https://github.com/devinus/poolboy.git",
|
||||||
|
{ref,"3bb48a893ff5598f7c73731ac17545206d259fac"}},
|
||||||
|
0},
|
||||||
|
{<<"ranch">>,
|
||||||
|
{git,"https://github.com/ninenines/ranch",
|
||||||
|
{ref,"a692f44567034dacf5efcaa24a24183788594eb7"}},
|
||||||
|
1},
|
||||||
|
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},1},
|
||||||
|
{<<"sync">>,
|
||||||
|
{git,"https://github.com/rustyio/sync.git",
|
||||||
|
{ref,"7dc303ed4ce8d26db82e171dbbd7c41067852c65"}},
|
||||||
|
0},
|
||||||
|
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.5.0">>},2}]}.
|
||||||
|
[
|
||||||
|
{pkg_hash,[
|
||||||
|
{<<"certifi">>, <<"B7CFEAE9D2ED395695DD8201C57A2D019C0C43ECAF8B8BCB9320B40D6662F340">>},
|
||||||
|
{<<"fs">>, <<"9D147B944D60CFA48A349F12D06C8EE71128F610C90870BDF9A6773206452ED0">>},
|
||||||
|
{<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>},
|
||||||
|
{<<"idna">>, <<"1D038FB2E7668CE41FBF681D2C45902E52B3CB9E9C77B55334353B222C2EE50C">>},
|
||||||
|
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
|
||||||
|
{<<"mimerl">>, <<"67E2D3F571088D5CFD3E550C383094B47159F3EEE8FFA08E64106CDF5E981BE3">>},
|
||||||
|
{<<"ssl_verify_fun">>, <<"CF344F5692C82D2CD7554F5EC8FD961548D4FD09E7D22F5B62482E5AEAEBD4B0">>},
|
||||||
|
{<<"unicode_util_compat">>, <<"8516502659002CEC19E244EBD90D312183064BE95025A319A6C7E89F4BCCD65B">>}]},
|
||||||
|
{pkg_hash_ext,[
|
||||||
|
{<<"certifi">>, <<"3B3B5F36493004AC3455966991EAF6E768CE9884693D9968055AEEEB1E575040">>},
|
||||||
|
{<<"fs">>, <<"EF94E95FFE79916860649FED80AC62B04C322B0BB70F5128144C026B4D171F8B">>},
|
||||||
|
{<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>},
|
||||||
|
{<<"idna">>, <<"A02C8A1C4FD601215BB0B0324C8A6986749F807CE35F25449EC9E69758708122">>},
|
||||||
|
{<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
|
||||||
|
{<<"mimerl">>, <<"F278585650AA581986264638EBF698F8BB19DF297F66AD91B18910DFC6E19323">>},
|
||||||
|
{<<"ssl_verify_fun">>, <<"BDB0D2471F453C88FF3908E7686F86F9BE327D065CC1EC16FA4540197EA04680">>},
|
||||||
|
{<<"unicode_util_compat">>, <<"D48D002E15F5CC105A696CF2F1BBB3FC72B4B770A184D8420C8DB20DA2674B38">>}]}
|
||||||
|
].
|
||||||
5
run
Executable file
5
run
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#! /bin/sh
|
||||||
|
rebar3 compile
|
||||||
|
rebar3 release
|
||||||
|
|
||||||
|
_build/default/rel/power_gateway/bin/power_gateway console
|
||||||
Loading…
x
Reference in New Issue
Block a user