fix emqtt
This commit is contained in:
parent
a5fa1d1bc5
commit
7e82659103
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,3 +16,4 @@ logs
|
|||||||
*.iml
|
*.iml
|
||||||
rebar3.crashdump
|
rebar3.crashdump
|
||||||
*~
|
*~
|
||||||
|
config/sys.config
|
||||||
|
|||||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM erlang:25.3
|
||||||
|
|
||||||
|
RUN mkdir -p /usr/local/var/mnesia/
|
||||||
|
|
||||||
|
ADD _build/default/rel/iot/iot-0.1.0.tar.gz /data/iot/
|
||||||
|
|
||||||
|
VOLUME /data/iot/log/
|
||||||
|
VOLUME /usr/local/var/mnesia/
|
||||||
|
|
||||||
|
WORKDIR /data/iot
|
||||||
|
|
||||||
|
CMD /data/iot/bin/iot foreground
|
||||||
535
apps/iot/include/emqtt.hrl
Normal file
535
apps/iot/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.
|
||||||
@ -19,10 +19,61 @@
|
|||||||
-define(TASK_STATUS_OK, 1). %% 在线
|
-define(TASK_STATUS_OK, 1). %% 在线
|
||||||
|
|
||||||
%% 主机端上报数据类型标识
|
%% 主机端上报数据类型标识
|
||||||
|
%% 建立到websocket的register关系
|
||||||
|
-define(METHOD_REGISTER, 16#00).
|
||||||
-define(METHOD_CREATE_SESSION, 16#01).
|
-define(METHOD_CREATE_SESSION, 16#01).
|
||||||
|
|
||||||
-define(METHOD_DATA, 16#02).
|
-define(METHOD_DATA, 16#02).
|
||||||
-define(METHOD_PING, 16#03).
|
-define(METHOD_PING, 16#03).
|
||||||
-define(METHOD_INFORM, 16#04).
|
-define(METHOD_INFORM, 16#04).
|
||||||
-define(METHOD_FEEDBACK_STEP, 16#05).
|
-define(METHOD_FEEDBACK_STEP, 16#05).
|
||||||
-define(METHOD_FEEDBACK_RESULT, 16#06).
|
-define(METHOD_FEEDBACK_RESULT, 16#06).
|
||||||
|
|
||||||
|
%% 北向数据
|
||||||
|
-define(METHOD_NORTH_DATA, 16#08).
|
||||||
|
|
||||||
|
%% 消息体类型
|
||||||
|
-define(PACKET_REQUEST, 16#01).
|
||||||
|
-define(PACKET_RESPONSE, 16#02).
|
||||||
|
-define(PACKET_PUBLISH, 16#03).
|
||||||
|
-define(PACKET_PUBLISH_RESPONSE, 16#04).
|
||||||
|
|
||||||
|
%% 缓存数据库表
|
||||||
|
-record(kv, {
|
||||||
|
key :: binary(),
|
||||||
|
val :: binary() | list() | map() | sets:set(),
|
||||||
|
expire_at = 0 :: integer(),
|
||||||
|
type :: atom()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% 对端配置
|
||||||
|
-record(endpoint, {
|
||||||
|
%% 不同的对端名字要唯一
|
||||||
|
name = <<>> :: binary(),
|
||||||
|
%% 标题描述
|
||||||
|
title = <<>> :: binary(),
|
||||||
|
%% 匹配规则, 固定了满足点位信息的前缀匹配的数据的转发规则
|
||||||
|
matcher = <<>> :: binary(),
|
||||||
|
mapper = <<>> :: binary(),
|
||||||
|
%% 数据转换规则,基于function
|
||||||
|
mapper_fun = fun(_, Data) -> Data end :: fun((binary(), any()) -> any()),
|
||||||
|
%% 配置项, 格式: #{<<"protocol">> => <<"http|https|ws|kafka|mqtt">>, <<"args">> => #{}}
|
||||||
|
config = #{} :: #{},
|
||||||
|
%% 更新时间
|
||||||
|
updated_at = 0 :: integer(),
|
||||||
|
%% 创建时间
|
||||||
|
created_at = 0 :: integer()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% id生成器
|
||||||
|
-record(id_generator, {
|
||||||
|
tab :: atom(),
|
||||||
|
increment_id = 0 :: integer()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% 北向数据
|
||||||
|
-record(north_data, {
|
||||||
|
ref :: reference(),
|
||||||
|
location_code :: binary(),
|
||||||
|
body :: binary()
|
||||||
|
}).
|
||||||
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([get_all_hosts/0, change_status/2, is_authorized/1, get_host/1, get_host_by_uuid/1, create_host/1]).
|
-export([get_all_hosts/0, change_status/2, is_authorized/1, get_host/1, get_host_by_uuid/1, create_host/1]).
|
||||||
|
-export([ensured_host/1]).
|
||||||
|
|
||||||
-spec get_all_hosts() -> UUIDList :: [binary()].
|
-spec get_all_hosts() -> UUIDList :: [binary()].
|
||||||
get_all_hosts() ->
|
get_all_hosts() ->
|
||||||
@ -44,3 +45,18 @@ is_authorized(HostId) when is_integer(HostId) ->
|
|||||||
_ ->
|
_ ->
|
||||||
false
|
false
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
-spec ensured_host(UUID :: binary()) -> ok | {error, Reason :: any()}.
|
||||||
|
ensured_host(UUID) when is_binary(UUID) ->
|
||||||
|
%% 查找数据库,如果没有则插入
|
||||||
|
case get_host_by_uuid(UUID) of
|
||||||
|
{ok, _} ->
|
||||||
|
ok;
|
||||||
|
undefined ->
|
||||||
|
case create_host(UUID) of
|
||||||
|
{ok, _} ->
|
||||||
|
ok;
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end
|
||||||
|
end.
|
||||||
1316
apps/iot/src/emqtt/emqtt.erl
Normal file
1316
apps/iot/src/emqtt/emqtt.erl
Normal file
File diff suppressed because it is too large
Load Diff
737
apps/iot/src/emqtt/emqtt_frame.erl
Normal file
737
apps/iot/src/emqtt/emqtt_frame.erl
Normal file
@ -0,0 +1,737 @@
|
|||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% 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/iot/src/emqtt/emqtt_props.erl
Normal file
172
apps/iot/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/iot/src/emqtt/emqtt_sock.erl
Normal file
120
apps/iot/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).
|
||||||
168
apps/iot/src/http_handler/endpoint_handler.erl
Normal file
168
apps/iot/src/http_handler/endpoint_handler.erl
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author licheng5
|
||||||
|
%%% @copyright (C) 2020, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 26. 4月 2020 3:36 下午
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(endpoint_handler).
|
||||||
|
-author("licheng5").
|
||||||
|
-include("iot.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([handle_request/4]).
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% helper methods
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 可以根据name进行过滤
|
||||||
|
handle_request("GET", "/endpoint/all", GetParams, _) ->
|
||||||
|
Endpoints0 = mnesia_endpoint:get_all_endpoints(),
|
||||||
|
Endpoints = case maps:is_key(<<"name">>, GetParams) of
|
||||||
|
true ->
|
||||||
|
Name = maps:get(<<"name">>, GetParams),
|
||||||
|
lists:filter(fun(#endpoint{name = Name0}) -> Name == Name0 end, Endpoints0);
|
||||||
|
false ->
|
||||||
|
Endpoints0
|
||||||
|
end,
|
||||||
|
EndpointInfos = lists:map(fun mnesia_endpoint:to_map/1, Endpoints),
|
||||||
|
|
||||||
|
{ok, 200, iot_util:json_data(EndpointInfos)};
|
||||||
|
|
||||||
|
%% 重新加载对应的主机信息
|
||||||
|
handle_request("POST", "/endpoint/create", _, Params = #{<<"name">> := Name}) ->
|
||||||
|
case lists:all(fun(Key) -> maps:is_key(Key, Params) end, [<<"name">>, <<"title">>, <<"matcher">>, <<"mapper">>, <<"config">>]) of
|
||||||
|
true ->
|
||||||
|
ok;
|
||||||
|
false ->
|
||||||
|
throw(<<"missed required param">>)
|
||||||
|
end,
|
||||||
|
|
||||||
|
case mnesia_endpoint:get_endpoint(Name) of
|
||||||
|
undefined ->
|
||||||
|
Endpoint0 = make_endpoint(maps:to_list(Params), #endpoint{}),
|
||||||
|
Endpoint = Endpoint0#endpoint{created_at = iot_util:timestamp_of_seconds()},
|
||||||
|
case mnesia_endpoint:insert(Endpoint) of
|
||||||
|
ok ->
|
||||||
|
{ok, _} = iot_endpoint_sup:ensured_endpoint_started(Endpoint),
|
||||||
|
|
||||||
|
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:debug("[endpoint_handler] create router, get error is: ~p", [Reason]),
|
||||||
|
{ok, 200, iot_util:json_error(404, <<"error">>)}
|
||||||
|
end;
|
||||||
|
{ok, _} ->
|
||||||
|
{ok, 200, iot_util:json_error(404, <<"endpoint name exists">>)}
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% 更新规则
|
||||||
|
handle_request("POST", "/endpoint/update", _, Params = #{<<"name">> := Name}) when is_binary(Name) ->
|
||||||
|
case mnesia_endpoint:get_endpoint(Name) of
|
||||||
|
undefined ->
|
||||||
|
lager:debug("[endpoint_handler] update endpoint, name: ~p not found", [Name]),
|
||||||
|
{ok, 200, iot_util:json_error(404, <<"endpoint not found">>)};
|
||||||
|
{ok, Endpoint} ->
|
||||||
|
Params1 = maps:remove(<<"name">>, Params),
|
||||||
|
NEndpoint = make_endpoint(maps:to_list(Params1), Endpoint),
|
||||||
|
NEndpoint1 = NEndpoint#endpoint{updated_at = iot_util:timestamp_of_seconds()},
|
||||||
|
|
||||||
|
case mnesia_endpoint:insert(NEndpoint1) of
|
||||||
|
ok ->
|
||||||
|
case iot_endpoint:get_pid(Name) of
|
||||||
|
undefined ->
|
||||||
|
%% 重新启动endpoint
|
||||||
|
{ok, _} = iot_endpoint_sup:ensured_endpoint_started(NEndpoint1);
|
||||||
|
Pid when is_pid(Pid) ->
|
||||||
|
iot_endpoint:reload(Pid, NEndpoint1)
|
||||||
|
end,
|
||||||
|
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:debug("[endpoint_handler] update endpoint, get error is: ~p", [Reason]),
|
||||||
|
{ok, 200, iot_util:json_error(404, <<"error">>)}
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% 删除对应的主机信息
|
||||||
|
handle_request("POST", "/endpoint/delete", _, #{<<"name">> := Name}) when is_binary(Name) ->
|
||||||
|
case mnesia_endpoint:delete(Name) of
|
||||||
|
ok ->
|
||||||
|
iot_endpoint_sup:delete_endpoint(Name),
|
||||||
|
|
||||||
|
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:debug("[endpoint_handler] delete endpoint id: ~p, get error is: ~p", [Name, Reason]),
|
||||||
|
{ok, 200, iot_util:json_error(404, <<"error">>)}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle_request(_, Path, _, _) ->
|
||||||
|
Path1 = list_to_binary(Path),
|
||||||
|
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% helper methods
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
-spec make_endpoint(Params :: list(), #endpoint{}) -> #endpoint{}.
|
||||||
|
make_endpoint([], Endpoint) ->
|
||||||
|
Endpoint;
|
||||||
|
make_endpoint([{<<"name">>, Name} | Params], Endpoint) when is_binary(Name) andalso Name /= <<>> ->
|
||||||
|
make_endpoint(Params, Endpoint#endpoint{name = Name});
|
||||||
|
make_endpoint([{<<"name">>, _} | _], _) ->
|
||||||
|
throw(<<"invalid name">>);
|
||||||
|
make_endpoint([{<<"title">>, Title} | Params], Endpoint) when is_binary(Title) andalso Title /= <<>> ->
|
||||||
|
make_endpoint(Params, Endpoint#endpoint{title = Title});
|
||||||
|
make_endpoint([{<<"title">>, _} | _], _) ->
|
||||||
|
throw(<<"invalid title">>);
|
||||||
|
make_endpoint([{<<"matcher">>, Matcher} | Params], Endpoint) when is_binary(Matcher) andalso Matcher /= <<>> ->
|
||||||
|
%% 检测matcher是否是合法的正则表达式
|
||||||
|
case re:compile(Matcher) of
|
||||||
|
{ok, _} ->
|
||||||
|
make_endpoint(Params, Endpoint#endpoint{matcher = Matcher});
|
||||||
|
{error, _} ->
|
||||||
|
throw(<<"invalid regexp">>)
|
||||||
|
end;
|
||||||
|
make_endpoint([{<<"matcher">>, _} | _], _) ->
|
||||||
|
throw(<<"invalid matcher">>);
|
||||||
|
make_endpoint([{<<"mapper">>, Mapper} | Params], Endpoint) when is_binary(Mapper) andalso Mapper /= <<>> ->
|
||||||
|
%% 检测mapper是否是合理的erlang表达式
|
||||||
|
case catch iot_util:parse_mapper(Mapper) of
|
||||||
|
{ok, MapperFun} ->
|
||||||
|
make_endpoint(Params, Endpoint#endpoint{mapper = Mapper, mapper_fun = MapperFun});
|
||||||
|
error ->
|
||||||
|
throw(<<"invalid mapper">>);
|
||||||
|
Error ->
|
||||||
|
lager:debug("[endpoint_handler] parse_mapper get error: ~p", [Error]),
|
||||||
|
throw(<<"invalid mapper">>)
|
||||||
|
end;
|
||||||
|
make_endpoint([{<<"mapper">>, _} | _], _) ->
|
||||||
|
throw(<<"invalid mapper">>);
|
||||||
|
|
||||||
|
make_endpoint([{<<"config">>, Config = #{<<"protocol">> := <<"http">>, <<"args">> := #{<<"url">> := Url}}} | Params], Endpoint) when Url /= <<>> ->
|
||||||
|
make_endpoint(Params, Endpoint#endpoint{config = Config});
|
||||||
|
make_endpoint([{<<"config">>, Config = #{<<"protocol">> := <<"https">>, <<"args">> := #{<<"url">> := Url}}} | Params], Endpoint) when Url /= <<>> ->
|
||||||
|
make_endpoint(Params, Endpoint#endpoint{config = Config});
|
||||||
|
make_endpoint([{<<"config">>, Config = #{<<"protocol">> := <<"ws">>, <<"args">> := #{<<"url">> := Url}}} | Params], Endpoint) when Url /= <<>> ->
|
||||||
|
make_endpoint(Params, Endpoint#endpoint{config = Config});
|
||||||
|
make_endpoint([{<<"config">>, Config = #{<<"protocol">> := <<"kafka">>, <<"args">> := #{<<"username">> := Username, <<"password">> := Password, <<"bootstrap_servers">> := BootstrapServers, <<"topic">> := Topic}}} | Params], Endpoint)
|
||||||
|
when is_binary(Username) andalso Username /= <<>>
|
||||||
|
andalso is_binary(Password) andalso Password /= <<>>
|
||||||
|
andalso is_list(BootstrapServers) andalso length(BootstrapServers) > 0
|
||||||
|
andalso is_binary(Topic) andalso Topic /= <<>> ->
|
||||||
|
make_endpoint(Params, Endpoint#endpoint{config = Config});
|
||||||
|
|
||||||
|
make_endpoint([{<<"config">>, Config = #{<<"protocol">> := <<"mqtt">>, <<"args">> := #{<<"host">> := Host, <<"port">> := Port, <<"username">> := Username, <<"password">> := Password, <<"topic">> := Topic, <<"qos">> := Qos}}} | Params], Endpoint)
|
||||||
|
when is_binary(Username) andalso Username /= <<>>
|
||||||
|
andalso is_binary(Password) andalso Password /= <<>>
|
||||||
|
andalso is_binary(Host) andalso Host /= <<>>
|
||||||
|
andalso is_integer(Port) andalso Port > 0
|
||||||
|
andalso (Qos == 0 orelse Qos == 1 orelse Qos == 2)
|
||||||
|
andalso is_binary(Topic) andalso Topic /= <<>> ->
|
||||||
|
make_endpoint(Params, Endpoint#endpoint{config = Config});
|
||||||
|
|
||||||
|
make_endpoint([{<<"config">>, Config} | _], _) ->
|
||||||
|
lager:warning("[endpoint_handler] unsupport config: ~p", [Config]),
|
||||||
|
throw(<<"invalid config">>);
|
||||||
|
make_endpoint([{Key, _} | _], _) ->
|
||||||
|
throw(<<"unsupport param: ", Key/binary>>).
|
||||||
@ -6,7 +6,7 @@
|
|||||||
%%% @end
|
%%% @end
|
||||||
%%% Created : 26. 4月 2020 3:36 下午
|
%%% Created : 26. 4月 2020 3:36 下午
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(http_host_handler).
|
-module(host_handler).
|
||||||
-author("licheng5").
|
-author("licheng5").
|
||||||
-include("iot.hrl").
|
-include("iot.hrl").
|
||||||
|
|
||||||
@ -83,27 +83,28 @@ handle_request("POST", "/host/publish_command", _,
|
|||||||
lager:debug("[http_host_handler] publish message is: ~p", [Reply1]),
|
lager:debug("[http_host_handler] publish message is: ~p", [Reply1]),
|
||||||
BinReply = iolist_to_binary(jiffy:encode(Reply1, [force_utf8])),
|
BinReply = iolist_to_binary(jiffy:encode(Reply1, [force_utf8])),
|
||||||
|
|
||||||
case iot_host:aes_encode(Pid, CommandType, BinReply) of
|
case iot_host:publish_message(Pid, CommandType, {aes, BinReply}) of
|
||||||
{error, Reason} when is_binary(Reason) ->
|
{error, Reason} when is_binary(Reason) ->
|
||||||
task_logs_bo:change_status(TaskId, ?TASK_STATUS_FAILED),
|
task_logs_bo:change_status(TaskId, ?TASK_STATUS_FAILED),
|
||||||
{ok, 200, iot_util:json_error(400, Reason)};
|
{ok, 200, iot_util:json_error(400, Reason)};
|
||||||
{ok, BinCommand} ->
|
{ok, Ref} ->
|
||||||
Topic = iot_host:downstream_topic(UUID),
|
receive
|
||||||
case iot_mqtt_publisher:publish(Topic, BinCommand, 2) of
|
{response, Ref} ->
|
||||||
{ok, Ref} ->
|
{ok, _} = task_logs_bo:change_status(TaskId, ?TASK_STATUS_OK),
|
||||||
receive
|
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||||
{ok, Ref, _PacketId} ->
|
{response, Ref, Response} ->
|
||||||
|
case jiffy:decode(Response, [return_maps]) of
|
||||||
|
#{<<"code">> := 1} ->
|
||||||
{ok, _} = task_logs_bo:change_status(TaskId, ?TASK_STATUS_OK),
|
{ok, _} = task_logs_bo:change_status(TaskId, ?TASK_STATUS_OK),
|
||||||
{ok, 200, iot_util:json_data(<<"success">>)}
|
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||||
after Timeout * 1000 ->
|
#{<<"code">> := 0, <<"message">> := Message} when is_binary(Message) ->
|
||||||
lager:debug("[iot_host_handler] host_id uuid: ~p, publish topic success, but get ack timeout", [UUID]),
|
{ok, _} = task_logs_bo:change_status(TaskId, ?TASK_STATUS_FAILED),
|
||||||
{ok, _} = task_logs_bo:change_status(TaskId, ?TASK_STATUS_FAILED),
|
{ok, 200, iot_util:json_error(401, <<"操作失败: "/utf8, Message/binary>>)}
|
||||||
{ok, 200, iot_util:json_error(401, <<"命令执行超时, 请重试"/utf8>>)}
|
end
|
||||||
end;
|
after Timeout * 1000 ->
|
||||||
{error, Reason} ->
|
lager:debug("[iot_host_handler] host_id uuid: ~p, publish topic success, but get ack timeout", [UUID]),
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, publish topic get error: ~p", [UUID, Reason]),
|
{ok, _} = task_logs_bo:change_status(TaskId, ?TASK_STATUS_FAILED),
|
||||||
{ok, _} = task_logs_bo:change_status(TaskId, ?TASK_STATUS_FAILED),
|
{ok, 200, iot_util:json_error(401, <<"命令执行超时, 请重试"/utf8>>)}
|
||||||
{ok, 200, iot_util:json_error(402, <<"发送命令到mqtt服务失败"/utf8>>)}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
@ -116,29 +117,26 @@ handle_request("POST", "/host/activate", _, #{<<"uuid">> := UUID, <<"auth">> :=
|
|||||||
{ok, 200, iot_util:json_error(400, <<"host not found">>)};
|
{ok, 200, iot_util:json_error(400, <<"host not found">>)};
|
||||||
{ok, Pid} when is_pid(Pid) ->
|
{ok, Pid} when is_pid(Pid) ->
|
||||||
lager:debug("[host_handler] activate host_id: ~p, start", [UUID]),
|
lager:debug("[host_handler] activate host_id: ~p, start", [UUID]),
|
||||||
{ok, Assoc, ReplyTopic} = iot_mqtt_reply_subscriber:make_assoc(UUID),
|
BinReply = jiffy:encode(#{<<"auth">> => true}, [force_utf8]),
|
||||||
BinReply = jiffy:encode(#{<<"auth">> => true, <<"reply">> => #{<<"topic">> => ReplyTopic, <<"assoc">> => Assoc}}, [force_utf8]),
|
|
||||||
|
|
||||||
case iot_mqtt_publisher:publish(iot_host:downstream_topic(UUID), <<8:8, BinReply/binary>>, 2) of
|
case iot_host:publish_message(Pid, 8, BinReply) of
|
||||||
{ok, Ref} ->
|
{ok, Ref} ->
|
||||||
receive
|
receive
|
||||||
{ok, Ref, _PacketId} ->
|
{response, Ref, Response} ->
|
||||||
receive
|
case jiffy:decode(Response, [return_maps]) of
|
||||||
{host_reply, Assoc, #{<<"code">> := 1}} ->
|
#{<<"code">> := 1} ->
|
||||||
ok = iot_host:activate(Pid, true),
|
ok = iot_host:activate(Pid, true),
|
||||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||||
{host_reply, Assoc, #{<<"code">> := 0, <<"message">> := Message}} when is_binary(Message) ->
|
#{<<"code">> := 0, <<"message">> := Message} when is_binary(Message) ->
|
||||||
{ok, 200, iot_util:json_error(401, <<"操作失败: "/utf8, Message/binary>>)}
|
{ok, 200, iot_util:json_error(401, <<"操作失败: "/utf8, Message/binary>>)}
|
||||||
after Timeout * 1000 ->
|
|
||||||
{ok, 200, iot_util:json_error(401, <<"操作超时,请重试: "/utf8>>)}
|
|
||||||
end
|
end
|
||||||
after Timeout * 1000 ->
|
after Timeout * 1000 ->
|
||||||
lager:debug("[iot_host_handler] host_id uuid: ~p, publish topic success, but get ack timeout", [UUID]),
|
lager:debug("[iot_host_handler] host_id uuid: ~p, publish topic success, but get ack timeout", [UUID]),
|
||||||
{ok, 200, iot_util:json_error(401, <<"命令执行超时, 请重试"/utf8>>)}
|
{ok, 200, iot_util:json_error(401, <<"命令执行超时, 请重试"/utf8>>)}
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, publish topic get error: ~p", [UUID, Reason]),
|
lager:debug("[iot_host] host_id uuid: ~p, publish command get error: ~p", [UUID, Reason]),
|
||||||
{ok, 200, iot_util:json_error(402, <<"发送命令到mqtt服务失败"/utf8>>)}
|
{ok, 200, iot_util:json_error(402, Reason)}
|
||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@ -153,29 +151,26 @@ handle_request("POST", "/host/activate", _, #{<<"uuid">> := UUID, <<"auth">> :=
|
|||||||
case iot_host:has_session(Pid) of
|
case iot_host:has_session(Pid) of
|
||||||
true ->
|
true ->
|
||||||
lager:debug("[host_handler] activate host_id: ~p, start", [UUID]),
|
lager:debug("[host_handler] activate host_id: ~p, start", [UUID]),
|
||||||
{ok, Assoc, ReplyTopic} = iot_mqtt_reply_subscriber:make_assoc(UUID),
|
BinReply = jiffy:encode(#{<<"auth">> => false}, [force_utf8]),
|
||||||
BinReply = jiffy:encode(#{<<"auth">> => false, <<"reply">> => #{<<"topic">> => ReplyTopic, <<"assoc">> => Assoc}}, [force_utf8]),
|
|
||||||
|
|
||||||
case iot_mqtt_publisher:publish(iot_host:downstream_topic(UUID), <<8:8, BinReply/binary>>, 2) of
|
case iot_host:publish_message(Pid, 8, BinReply) of
|
||||||
{ok, Ref} ->
|
{ok, Ref} ->
|
||||||
receive
|
receive
|
||||||
{ok, Ref, _PacketId} ->
|
{response, Ref, Response} ->
|
||||||
receive
|
case jiffy:decode(Response, [return_maps]) of
|
||||||
{host_reply, Assoc, #{<<"code">> := 1}} ->
|
#{<<"code">> := 1} ->
|
||||||
ok = iot_host:activate(Pid, false),
|
ok = iot_host:activate(Pid, false),
|
||||||
{ok, 200, iot_util:json_data(<<"success">>)};
|
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||||
{host_reply, Assoc, #{<<"code">> := 0, <<"message">> := Message}} when is_binary(Message) ->
|
#{<<"code">> := 0, <<"message">> := Message} when is_binary(Message) ->
|
||||||
{ok, 200, iot_util:json_error(401, <<"操作失败: "/utf8, Message/binary>>)}
|
{ok, 200, iot_util:json_error(401, <<"操作失败: "/utf8, Message/binary>>)}
|
||||||
after Timeout * 1000 ->
|
|
||||||
{ok, 200, iot_util:json_error(401, <<"操作超时,请重试: "/utf8>>)}
|
|
||||||
end
|
end
|
||||||
after Timeout * 1000 ->
|
after Timeout * 1000 ->
|
||||||
lager:debug("[iot_host_handler] host_id uuid: ~p, publish topic success, but get ack timeout", [UUID]),
|
lager:debug("[iot_host_handler] host_id uuid: ~p, publish topic success, but get ack timeout", [UUID]),
|
||||||
{ok, 200, iot_util:json_error(401, <<"命令执行超时, 请重试"/utf8>>)}
|
{ok, 200, iot_util:json_error(401, <<"命令执行超时, 请重试"/utf8>>)}
|
||||||
end;
|
end;
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, publish topic get error: ~p", [UUID, Reason]),
|
lager:debug("[iot_host] host_id uuid: ~p, publish command get error: ~p", [UUID, Reason]),
|
||||||
{ok, 200, iot_util:json_error(402, <<"发送命令到mqtt服务失败"/utf8>>)}
|
{ok, 200, iot_util:json_error(402, Reason)}
|
||||||
end;
|
end;
|
||||||
false ->
|
false ->
|
||||||
ok = iot_host:activate(Pid, false),
|
ok = iot_host:activate(Pid, false),
|
||||||
31
apps/iot/src/http_handler/test_handler.erl
Normal file
31
apps/iot/src/http_handler/test_handler.erl
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author licheng5
|
||||||
|
%%% @copyright (C) 2020, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 26. 4月 2020 3:36 下午
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(test_handler).
|
||||||
|
-author("licheng5").
|
||||||
|
-include("iot.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([handle_request/4]).
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% helper methods
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 重新加载对应的主机信息
|
||||||
|
handle_request("POST", "/test/receiver", _, PostParams) ->
|
||||||
|
lager:debug("[test_handler] get post params: ~p", [PostParams]),
|
||||||
|
{ok, 200, iot_util:json_data(<<"success">>)};
|
||||||
|
|
||||||
|
handle_request(_, Path, _, _) ->
|
||||||
|
Path1 = list_to_binary(Path),
|
||||||
|
{ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% helper methods
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
@ -42,6 +42,9 @@ field_val(V) when is_number(V) ->
|
|||||||
<<(integer_to_binary(V))/binary, "u">>;
|
<<(integer_to_binary(V))/binary, "u">>;
|
||||||
field_val(V) when is_binary(V) ->
|
field_val(V) when is_binary(V) ->
|
||||||
<<$", V/binary, $">>;
|
<<$", V/binary, $">>;
|
||||||
|
field_val(V) when is_list(V); is_map(V) ->
|
||||||
|
S = jiffy:encode(V, [force_utf8]),
|
||||||
|
<<$", S/binary, $">>;
|
||||||
field_val(true) ->
|
field_val(true) ->
|
||||||
<<"true">>;
|
<<"true">>;
|
||||||
field_val(false) ->
|
field_val(false) ->
|
||||||
|
|||||||
@ -14,11 +14,12 @@
|
|||||||
hackney,
|
hackney,
|
||||||
poolboy,
|
poolboy,
|
||||||
mysql,
|
mysql,
|
||||||
emqtt,
|
esockd,
|
||||||
mnesia,
|
mnesia,
|
||||||
crypto,
|
crypto,
|
||||||
public_key,
|
public_key,
|
||||||
ssl,
|
ssl,
|
||||||
|
erts,
|
||||||
kernel,
|
kernel,
|
||||||
stdlib
|
stdlib
|
||||||
]},
|
]},
|
||||||
|
|||||||
@ -2,23 +2,26 @@
|
|||||||
%% @doc iot public API
|
%% @doc iot public API
|
||||||
%% @end
|
%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
|
|
||||||
-module(iot_app).
|
-module(iot_app).
|
||||||
|
|
||||||
-behaviour(application).
|
-behaviour(application).
|
||||||
|
|
||||||
|
-include("iot.hrl").
|
||||||
|
|
||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
-export([start_http_server/0]).
|
-export([start_http_server/0]).
|
||||||
|
|
||||||
start(_StartType, _StartArgs) ->
|
start(_StartType, _StartArgs) ->
|
||||||
io:setopts([{encoding, unicode}]),
|
io:setopts([{encoding, unicode}]),
|
||||||
%% 启动数据库
|
%% 启动数据库
|
||||||
% start_mnesia(),
|
start_mnesia(),
|
||||||
|
|
||||||
%% 加速内存的回收
|
%% 加速内存的回收
|
||||||
erlang:system_flag(fullsweep_after, 16),
|
erlang:system_flag(fullsweep_after, 16),
|
||||||
%% 启动http服务
|
%% 启动http服务
|
||||||
start_http_server(),
|
start_http_server(),
|
||||||
|
%% 启动redis服务器
|
||||||
|
start_redis_server(),
|
||||||
|
|
||||||
%% 启动连接池
|
%% 启动连接池
|
||||||
ok = hackney_pool:start_pool(influx_pool, [{timeout, 150000}, {max_connections, 100}]),
|
ok = hackney_pool:start_pool(influx_pool, [{timeout, 150000}, {max_connections, 100}]),
|
||||||
@ -40,7 +43,10 @@ start_http_server() ->
|
|||||||
|
|
||||||
Dispatcher = cowboy_router:compile([
|
Dispatcher = cowboy_router:compile([
|
||||||
{'_', [
|
{'_', [
|
||||||
{"/host/[...]", http_protocol, [http_host_handler]}
|
{"/host/[...]", http_protocol, [host_handler]},
|
||||||
|
{"/endpoint/[...]", http_protocol, [endpoint_handler]},
|
||||||
|
{"/test/[...]", http_protocol, [test_handler]},
|
||||||
|
{"/ws", ws_channel, []}
|
||||||
]}
|
]}
|
||||||
]),
|
]),
|
||||||
|
|
||||||
@ -52,12 +58,75 @@ start_http_server() ->
|
|||||||
],
|
],
|
||||||
|
|
||||||
{ok, Pid} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}),
|
{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]).
|
lager:debug("[iot_app] the http server start at: ~p, pid is: ~p", [Port, Pid]).
|
||||||
|
|
||||||
%start_mnesia() ->
|
start_redis_server() ->
|
||||||
% %% 启动数据库
|
{ok, Props} = application:get_env(iot, redis_server),
|
||||||
% mnesia:start(),
|
Acceptors = proplists:get_value(acceptors, Props, 50),
|
||||||
% Tables = mnesia:system_info(tables),
|
MaxConnections = proplists:get_value(max_connections, Props, 10240),
|
||||||
% %% 加载必须等待的数据库表
|
Backlog = proplists:get_value(backlog, Props, 1024),
|
||||||
% lists:member(router, Tables) andalso mnesia:wait_for_tables([router], infinity),
|
Port = proplists:get_value(port, Props),
|
||||||
% lists:member(host, Tables) andalso mnesia:wait_for_tables([host], infinity).
|
|
||||||
|
TransOpts = [
|
||||||
|
{tcp_options, [
|
||||||
|
binary,
|
||||||
|
{reuseaddr, true},
|
||||||
|
{active, false},
|
||||||
|
{packet, raw},
|
||||||
|
{nodelay, false},
|
||||||
|
{backlog, Backlog}
|
||||||
|
]},
|
||||||
|
{acceptors, Acceptors},
|
||||||
|
{max_connections, MaxConnections}
|
||||||
|
],
|
||||||
|
{ok, _} = esockd:open(redis_server, Port, TransOpts, {redis_protocol, start_link, []}),
|
||||||
|
|
||||||
|
lager:debug("[iot_app] the rpc server start at: ~p", [Port]).
|
||||||
|
|
||||||
|
%% 启动内存数据库
|
||||||
|
start_mnesia() ->
|
||||||
|
%% 启动数据库
|
||||||
|
ok = mnesia:start(),
|
||||||
|
Tables = mnesia:system_info(tables),
|
||||||
|
LoadTables = [id_generator, kv, endpoint],
|
||||||
|
case lists:all(fun(Tab) -> lists:member(Tab, Tables) end, LoadTables) of
|
||||||
|
true ->
|
||||||
|
%% 加载必须等待的数据库表
|
||||||
|
mnesia:wait_for_tables(LoadTables, infinity);
|
||||||
|
false ->
|
||||||
|
lager:warning("[iot_app] tables: ~p not exists, recreate mnesia schema", [LoadTables]),
|
||||||
|
%% 清理掉以前的schema
|
||||||
|
mnesia:stop(),
|
||||||
|
mnesia:delete_schema([node()]),
|
||||||
|
|
||||||
|
%% 创建schema
|
||||||
|
ok = mnesia:create_schema([node()]),
|
||||||
|
ok = mnesia:start(),
|
||||||
|
|
||||||
|
%% 创建数据库表
|
||||||
|
|
||||||
|
%% 缓存表
|
||||||
|
mnesia:create_table(id_generator, [
|
||||||
|
{attributes, record_info(fields, id_generator)},
|
||||||
|
{record_name, id_generator},
|
||||||
|
{disc_copies, [node()]},
|
||||||
|
{type, ordered_set}
|
||||||
|
]),
|
||||||
|
|
||||||
|
%% 缓存表
|
||||||
|
mnesia:create_table(kv, [
|
||||||
|
{attributes, record_info(fields, kv)},
|
||||||
|
{record_name, kv},
|
||||||
|
{disc_copies, [node()]},
|
||||||
|
{type, ordered_set}
|
||||||
|
]),
|
||||||
|
|
||||||
|
%% 对端配置表
|
||||||
|
mnesia:create_table(endpoint, [
|
||||||
|
{attributes, record_info(fields, endpoint)},
|
||||||
|
{record_name, endpoint},
|
||||||
|
{disc_copies, [node()]},
|
||||||
|
{type, ordered_set}
|
||||||
|
])
|
||||||
|
end.
|
||||||
31
apps/iot/src/iot_auth.erl
Normal file
31
apps/iot/src/iot_auth.erl
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%% 授权校验模块
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 27. 6月 2023 09:48
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(iot_auth).
|
||||||
|
-author("aresei").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([check/5]).
|
||||||
|
|
||||||
|
%% 检测token是否是合法值
|
||||||
|
-spec check(Username :: binary(), Token :: binary(), UUID :: binary(), Salt :: binary(), Timestamp :: integer()) -> boolean().
|
||||||
|
check(Username, Token, UUID, Salt, Timestamp) when is_binary(Username), is_binary(Token), is_binary(UUID), is_binary(Salt), is_integer(Timestamp) ->
|
||||||
|
BinTimestamp = integer_to_binary(Timestamp),
|
||||||
|
%% 1分钟内有效
|
||||||
|
case iot_util:current_time() - Timestamp =< 60 of
|
||||||
|
true ->
|
||||||
|
{ok, PreTokens} = application:get_env(iot, pre_tokens),
|
||||||
|
case proplists:get_value(Username, PreTokens) of
|
||||||
|
undefined ->
|
||||||
|
false;
|
||||||
|
PreToken when is_binary(PreToken) ->
|
||||||
|
iot_util:md5(<<Salt/binary, "!", PreToken/binary, "!", UUID/binary, "!", BinTimestamp/binary>>) =:= Token
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
false
|
||||||
|
end.
|
||||||
278
apps/iot/src/iot_endpoint.erl
Normal file
278
apps/iot/src/iot_endpoint.erl
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 06. 7月 2023 12:02
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(iot_endpoint).
|
||||||
|
-author("aresei").
|
||||||
|
-include("iot.hrl").
|
||||||
|
|
||||||
|
-behaviour(gen_statem).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/2]).
|
||||||
|
-export([get_name/1, get_pid/1, forward/3, get_stat/1, reload/2]).
|
||||||
|
|
||||||
|
%% gen_statem callbacks
|
||||||
|
-export([init/1, handle_event/4, terminate/3, code_change/4, callback_mode/0]).
|
||||||
|
|
||||||
|
%% 消息重发间隔
|
||||||
|
-define(RETRY_INTERVAL, 5000).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
endpoint :: #endpoint{},
|
||||||
|
postman :: undefined | {atom(), pid()},
|
||||||
|
%% 发送后未确认的消息
|
||||||
|
ack_map = #{},
|
||||||
|
%% 定时器
|
||||||
|
timer_map = #{},
|
||||||
|
%% 记录成功处理的消息数
|
||||||
|
acc_num = 0,
|
||||||
|
%% 当postman进程异常时,需要建立缓存区
|
||||||
|
q = queue:new()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-spec get_name(Name :: binary() | #endpoint{}) -> atom().
|
||||||
|
get_name(#endpoint{name = Name}) when is_binary(Name) ->
|
||||||
|
get_name(Name);
|
||||||
|
get_name(EndpointName) when is_binary(EndpointName) ->
|
||||||
|
binary_to_atom(<<"iot_endpoint:", EndpointName/binary>>).
|
||||||
|
|
||||||
|
-spec get_pid(Name :: binary()) -> undefined | pid().
|
||||||
|
get_pid(Name) when is_binary(Name) ->
|
||||||
|
whereis(get_name(Name)).
|
||||||
|
|
||||||
|
-spec forward(Pid :: undefined | pid(), LocationCode :: binary(), Data :: binary()) -> no_return().
|
||||||
|
forward(undefined, _, _) ->
|
||||||
|
ok;
|
||||||
|
forward(Pid, LocationCode, Data) when is_pid(Pid), is_binary(LocationCode) ->
|
||||||
|
gen_statem:cast(Pid, {forward, LocationCode, Data}).
|
||||||
|
|
||||||
|
reload(Pid, NEndpoint = #endpoint{}) when is_pid(Pid) ->
|
||||||
|
gen_statem:cast(Pid, {reload, NEndpoint}).
|
||||||
|
|
||||||
|
-spec get_stat(Pid :: pid()) -> {ok, Stat :: #{}}.
|
||||||
|
get_stat(Pid) when is_pid(Pid) ->
|
||||||
|
gen_statem:call(Pid, get_stat, 5000).
|
||||||
|
|
||||||
|
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
||||||
|
%% initialize. To ensure a synchronized start-up procedure, this
|
||||||
|
%% function does not return until Module:init/1 has returned.
|
||||||
|
start_link(Name, Endpoint = #endpoint{}) ->
|
||||||
|
gen_statem:start_link({local, Name}, ?MODULE, [Endpoint], []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% gen_statem callbacks
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
|
||||||
|
%% gen_statem:start_link/[3,4], this function is called by the new
|
||||||
|
%% process to initialize.
|
||||||
|
init([Endpoint]) ->
|
||||||
|
erlang:process_flag(trap_exit, true),
|
||||||
|
%% 创建转发器
|
||||||
|
erlang:start_timer(0, self(), recreate_postman),
|
||||||
|
|
||||||
|
{ok, disconnected, #state{endpoint = Endpoint, postman = undefined}}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc This function is called by a gen_statem when it needs to find out
|
||||||
|
%% the callback mode of the callback module.
|
||||||
|
callback_mode() ->
|
||||||
|
handle_event_function.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc There should be one instance of this function for each possible
|
||||||
|
%% state name. If callback_mode is state_functions, one of these
|
||||||
|
%% functions is called when gen_statem receives and event from
|
||||||
|
%% call/2, cast/2, or as a normal process message.
|
||||||
|
|
||||||
|
%% 重新加载新的终端配置
|
||||||
|
handle_event(cast, {reload, NEndpoint}, disconnected, State = #state{endpoint = Endpoint}) ->
|
||||||
|
lager:warning("[iot_endpoint] reload endpoint, old: ~p, new: ~p", [Endpoint, Endpoint, NEndpoint]),
|
||||||
|
{keep_state, State#state{endpoint = NEndpoint}};
|
||||||
|
|
||||||
|
handle_event(cast, {reload, NEndpoint}, connected, State = #state{endpoint = Endpoint, postman = {_, PostmanPid}}) ->
|
||||||
|
lager:warning("[iot_endpoint] reload endpoint, old: ~p, new: ~p", [Endpoint, Endpoint, NEndpoint]),
|
||||||
|
%% 解除和postman的link关系
|
||||||
|
unlink(PostmanPid),
|
||||||
|
%% 关闭postman进程
|
||||||
|
PostmanPid ! stop,
|
||||||
|
%% 未确认的消息需要暂存
|
||||||
|
NState = stash(State),
|
||||||
|
%% 重新建立新的postman
|
||||||
|
erlang:start_timer(0, self(), recreate_postman),
|
||||||
|
|
||||||
|
{next_state, disconnected, NState#state{endpoint = NEndpoint, postman = undefined}};
|
||||||
|
|
||||||
|
handle_event(cast, {forward, LocationCode, Data}, _, State = #state{endpoint = Endpoint = #endpoint{mapper_fun = MapperFun}}) ->
|
||||||
|
try
|
||||||
|
Body = MapperFun(LocationCode, Data),
|
||||||
|
NorthData = #north_data{ref = make_ref(), location_code = LocationCode, body = Body},
|
||||||
|
gen_statem:cast(self(), {post, NorthData})
|
||||||
|
catch _:Reason ->
|
||||||
|
lager:debug("[iot_endpoint] endpoint: ~p, mapper data get error: ~p", [Endpoint, Reason])
|
||||||
|
end,
|
||||||
|
{keep_state, State};
|
||||||
|
|
||||||
|
handle_event(cast, {post, NorthData = #north_data{ref = Ref}}, connected, State = #state{endpoint = Endpoint, postman = {_, PostmanPid}, ack_map = AckMap, timer_map = TimerMap}) ->
|
||||||
|
lager:debug("[iot_endpoint] endpoint: ~p, postman online, north data is: ~p", [Endpoint, NorthData]),
|
||||||
|
|
||||||
|
PostmanPid ! {post, NorthData},
|
||||||
|
%% 重发机制
|
||||||
|
TimerRef = erlang:start_timer(?RETRY_INTERVAL, self(), {repost_ticker, NorthData}),
|
||||||
|
|
||||||
|
{keep_state, State#state{ack_map = maps:put(Ref, NorthData, AckMap), timer_map = maps:put(Ref, TimerRef, TimerMap)}};
|
||||||
|
|
||||||
|
handle_event(cast, {post, NorthData}, disconnected, State = #state{endpoint = Endpoint, q = Q}) ->
|
||||||
|
lager:debug("[iot_endpoint] endpoint: ~p, postman offline, data in queue", [Endpoint]),
|
||||||
|
{keep_state, State#state{q = queue:in(NorthData, Q)}};
|
||||||
|
|
||||||
|
%% 收到确认消息
|
||||||
|
handle_event(info, {ack, Ref}, _, State = #state{ack_map = AckMap, timer_map = TimerMap, acc_num = AccNum}) ->
|
||||||
|
NAckMap = maps:remove(Ref, AckMap),
|
||||||
|
NTimerMap = case maps:take(Ref, TimerMap) of
|
||||||
|
error ->
|
||||||
|
TimerMap;
|
||||||
|
{TimerRef, TimerMap0} ->
|
||||||
|
catch erlang:cancel_timer(TimerRef),
|
||||||
|
TimerMap0
|
||||||
|
end,
|
||||||
|
{keep_state, State#state{ack_map = NAckMap, timer_map = NTimerMap, acc_num = AccNum + 1}};
|
||||||
|
|
||||||
|
%% 收到重发过期请求
|
||||||
|
handle_event(info, {timeout, _, {repost_ticker, NorthData = #north_data{ref = Ref}}}, connected, State = #state{postman = {_, PostmanPid}, timer_map = TimerMap}) ->
|
||||||
|
PostmanPid ! {post, NorthData},
|
||||||
|
%% 5秒后重发
|
||||||
|
TimerRef = erlang:start_timer(?RETRY_INTERVAL, self(), {repost_ticker, NorthData}),
|
||||||
|
|
||||||
|
{keep_state, State#state{timer_map = maps:put(Ref, TimerRef, TimerMap)}};
|
||||||
|
|
||||||
|
%% 离线时,忽略超时逻辑
|
||||||
|
handle_event(info, {timeout, _, {repost_ticker, _}}, disconnected, State) ->
|
||||||
|
{keep_state, State};
|
||||||
|
|
||||||
|
handle_event(info, {timeout, _, recreate_postman}, disconnected, State = #state{endpoint = Endpoint, ack_map = AckMap, timer_map = TimerMap, q = Q}) ->
|
||||||
|
lager:debug("[iot_endpoint] recreate postman: ~p", [Endpoint]),
|
||||||
|
try create_postman(Endpoint) of
|
||||||
|
{ok, Postman = {_, PostmanPid}} ->
|
||||||
|
lager:debug("[iot_endpoint] queue data is: ~p", [queue:to_list(Q)]),
|
||||||
|
%% 发送缓存区中的所有数据
|
||||||
|
{NAckMap, NTimerMap} = lists:foldl(fun(NorthData = #north_data{ref = Ref}, {AckMap0, TimerMap0}) ->
|
||||||
|
PostmanPid ! {post, NorthData},
|
||||||
|
TimerRef = erlang:start_timer(?RETRY_INTERVAL, self(), {repost_ticker, NorthData}),
|
||||||
|
{maps:put(Ref, NorthData, AckMap0), maps:put(Ref, TimerRef, TimerMap0)}
|
||||||
|
end, {AckMap, TimerMap}, queue:to_list(Q)),
|
||||||
|
%% 需要清空当前的队列
|
||||||
|
{next_state, connected, State#state{endpoint = Endpoint, postman = Postman, ack_map = NAckMap, timer_map = NTimerMap, q = queue:new()}}
|
||||||
|
catch _:Reason ->
|
||||||
|
lager:warning("[iot_endpoint] recreate postman get error: ~p", [Reason]),
|
||||||
|
erlang:start_timer(?RETRY_INTERVAL, self(), recreate_postman),
|
||||||
|
|
||||||
|
{keep_state, State#state{endpoint = Endpoint, postman = undefined}}
|
||||||
|
end;
|
||||||
|
|
||||||
|
%% 获取当前统计信息
|
||||||
|
handle_event({call, From}, get_stat, StateName, State = #state{acc_num = AccNum, ack_map = AckMap, q = Q}) ->
|
||||||
|
Stat = #{
|
||||||
|
<<"acc_num">> => AccNum,
|
||||||
|
<<"unconfirmed_num">> => maps:size(AckMap),
|
||||||
|
<<"queue_num">> => queue:len(Q),
|
||||||
|
<<"state">> => atom_to_binary(StateName)
|
||||||
|
},
|
||||||
|
{keep_state, State, [{reply, From, Stat}]};
|
||||||
|
|
||||||
|
%% 所有未确认的消息进入队列里面, 这里不保证消息的顺序
|
||||||
|
handle_event(info, {'EXIT', PostmanPid, Reason}, connected, State = #state{ack_map = AckMap, postman = {_, PostmanPid}}) ->
|
||||||
|
lager:warning("[iot_endpoint] postman exited, current ack_map: ~p, reason: ~p", [AckMap, Reason]),
|
||||||
|
NState = stash(State),
|
||||||
|
|
||||||
|
erlang:start_timer(?RETRY_INTERVAL, self(), recreate_postman),
|
||||||
|
{next_state, disconnected, NState#state{postman = undefined}};
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc If callback_mode is handle_event_function, then whenever a
|
||||||
|
%% gen_statem receives an event from call/2, cast/2, or as a normal
|
||||||
|
%% process message, this function is called.
|
||||||
|
handle_event(EventType, Event, StateName, State) ->
|
||||||
|
lager:warning("[iot_endpoint] unknown message, event_type: ~p, event: ~p, state_name: ~p, state: ~p", [EventType, Event, StateName, State]),
|
||||||
|
{keep_state, State}.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc This function is called by a gen_statem 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_statem terminates with
|
||||||
|
%% Reason. The return value is ignored.
|
||||||
|
terminate(_Reason, _StateName, _State) ->
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% @private
|
||||||
|
%% @doc Convert process state when code is changed
|
||||||
|
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
|
||||||
|
{ok, StateName, State}.
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% Internal functions
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% 将所有未处理的消息暂存
|
||||||
|
stash(State = #state{ack_map = AckMap, timer_map = TimerMap, q = Q}) ->
|
||||||
|
Q1 = lists:foldl(fun({_, NorthData}, Q0) -> queue:in(NorthData, Q0) end, Q, maps:to_list(AckMap)),
|
||||||
|
%% 清空所有的timer
|
||||||
|
lists:foreach(fun({_, TimerRef}) -> catch erlang:cancel_timer(TimerRef) end, maps:to_list(TimerMap)),
|
||||||
|
|
||||||
|
State#state{q = Q1, ack_map = #{}, postman = undefined}.
|
||||||
|
|
||||||
|
%% 对http和https协议的支持
|
||||||
|
create_postman(#endpoint{name = Name, config = Config = #{<<"protocol">> := <<"http">>, <<"args">> := #{<<"url">> := Url}}}) ->
|
||||||
|
PoolSize = maps:get(<<"pool_size">>, Config, 10),
|
||||||
|
PoolName = binary_to_atom(<<"http_pool:", Name/binary>>),
|
||||||
|
{ok, PostmanPid} = http_postman:start_link(self(), Url, PoolName, PoolSize),
|
||||||
|
|
||||||
|
{ok, {http, PostmanPid}};
|
||||||
|
|
||||||
|
%% 对mqtt协议的支持
|
||||||
|
create_postman(#endpoint{name = Name, config = Config = #{<<"protocol">> := <<"mqtt">>,
|
||||||
|
<<"args">> := #{<<"host">> := Host, <<"port">> := Port, <<"username">> := Username, <<"password">> := Password, <<"topic">> := Topic, <<"qos">> := Qos}}}) ->
|
||||||
|
|
||||||
|
ClientId = case maps:is_key(<<"client_id">>, Config) of
|
||||||
|
true ->
|
||||||
|
maps:get(<<"client_id">>, Config);
|
||||||
|
false ->
|
||||||
|
Node = atom_to_binary(node()),
|
||||||
|
<<"mqtt-client-", Node/binary, "-", Name/binary>>
|
||||||
|
end,
|
||||||
|
Keepalive = maps:get(<<"keepalive">>, Config, 86400),
|
||||||
|
RetryInterval = maps:get(<<"retry_interval">>, Config, 5),
|
||||||
|
Opts = [
|
||||||
|
{clientid, ClientId},
|
||||||
|
{host, as_string(Host)},
|
||||||
|
{port, Port},
|
||||||
|
{tcp_opts, []},
|
||||||
|
{username, as_string(Username)},
|
||||||
|
{password, as_string(Password)},
|
||||||
|
{keepalive, Keepalive},
|
||||||
|
{auto_ack, true},
|
||||||
|
{connect_timeout, 5},
|
||||||
|
{retry_interval, RetryInterval}
|
||||||
|
],
|
||||||
|
|
||||||
|
{ok, PostmanPid} = mqtt_postman:start_link(self(), Opts, Topic, Qos),
|
||||||
|
|
||||||
|
{ok, {mqtt, PostmanPid}};
|
||||||
|
create_postman(#endpoint{}) ->
|
||||||
|
throw(<<"not supported">>).
|
||||||
|
|
||||||
|
%% 转出成string
|
||||||
|
as_string(S) when is_list(S) ->
|
||||||
|
S;
|
||||||
|
as_string(S) when is_binary(S) ->
|
||||||
|
unicode:characters_to_list(S).
|
||||||
51
apps/iot/src/iot_endpoint_sup.erl
Normal file
51
apps/iot/src/iot_endpoint_sup.erl
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(iot_endpoint_sup).
|
||||||
|
-include("iot.hrl").
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0, init/1, delete_endpoint/1, ensured_endpoint_started/1, stat/0]).
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
Specs = lists:map(fun child_spec/1, mnesia_endpoint:get_all_endpoints()),
|
||||||
|
|
||||||
|
{ok, {#{strategy => one_for_one, intensity => 1000, period => 3600}, []}}.
|
||||||
|
|
||||||
|
-spec ensured_endpoint_started(Name :: #endpoint{}) -> {ok, Pid :: pid()} | {error, Reason :: any()}.
|
||||||
|
ensured_endpoint_started(Endpoint = #endpoint{}) ->
|
||||||
|
case supervisor:start_child(?MODULE, child_spec(Endpoint)) of
|
||||||
|
{ok, Pid} when is_pid(Pid) ->
|
||||||
|
{ok, Pid};
|
||||||
|
{error, {'already_started', Pid}} when is_pid(Pid) ->
|
||||||
|
{ok, Pid};
|
||||||
|
{error, Error} ->
|
||||||
|
{error, Error}
|
||||||
|
end.
|
||||||
|
|
||||||
|
stat() ->
|
||||||
|
Children = supervisor:which_children(?MODULE),
|
||||||
|
lists:foreach(fun({Id, Pid, _, _}) ->
|
||||||
|
Stat = catch iot_endpoint:get_stat(Pid),
|
||||||
|
lager:debug("[iot_endpoint] id: ~p, stat: ~p", [Id, Stat])
|
||||||
|
end, Children).
|
||||||
|
|
||||||
|
delete_endpoint(Name) when is_binary(Name) ->
|
||||||
|
Id = iot_endpoint:get_name(Name),
|
||||||
|
supervisor:delete_child(?MODULE, Id).
|
||||||
|
|
||||||
|
child_spec(Endpoint) ->
|
||||||
|
Id = iot_endpoint:get_name(Endpoint),
|
||||||
|
#{id => Id,
|
||||||
|
start => {iot_endpoint, start_link, [Id, Endpoint]},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 2000,
|
||||||
|
type => worker,
|
||||||
|
modules => ['iot_endpoint']}.
|
||||||
@ -14,17 +14,12 @@
|
|||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([start_link/2, get_name/1, get_pid/1, handle/2, reload/1, activate/2]).
|
-export([start_link/2, get_name/1, get_pid/1, handle/2, reload/1, activate/2]).
|
||||||
-export([get_metric/1, aes_encode/3, downstream_topic/1, upstream_topic/1, get_aes/1, rsa_encode/3]).
|
-export([get_metric/1, publish_message/3, get_aes/1, rsa_encode/3]).
|
||||||
-export([has_session/1]).
|
-export([has_session/1, create_session/2, attach_channel/2]).
|
||||||
|
|
||||||
%% gen_statem callbacks
|
%% gen_statem callbacks
|
||||||
-export([init/1, format_status/2, handle_event/4, terminate/3, code_change/4, callback_mode/0]).
|
-export([init/1, format_status/2, handle_event/4, terminate/3, code_change/4, callback_mode/0]).
|
||||||
|
|
||||||
-define(SERVER, ?MODULE).
|
|
||||||
|
|
||||||
%% 心跳包的间隔周期, 值需要比host上传的间隔大一些才行; ping的间隔为20s
|
|
||||||
-define(TICKER_INTERVAL, 30000).
|
|
||||||
|
|
||||||
-record(state, {
|
-record(state, {
|
||||||
host_id :: integer(),
|
host_id :: integer(),
|
||||||
%% 从数据库里面读取到的数据
|
%% 从数据库里面读取到的数据
|
||||||
@ -37,11 +32,12 @@
|
|||||||
%% aes的key, 后续通讯需要基于这个加密
|
%% aes的key, 后续通讯需要基于这个加密
|
||||||
aes = <<>> :: binary(),
|
aes = <<>> :: binary(),
|
||||||
|
|
||||||
%% 主机的相关信息
|
%% websocket相关
|
||||||
metrics = #{} :: map(),
|
channel_pid :: undefined | pid(),
|
||||||
|
monitor_ref :: undefined | reference(),
|
||||||
|
|
||||||
%% 是否获取到了ping请求
|
%% 主机的相关信息
|
||||||
is_answered = false :: boolean()
|
metrics = #{} :: map()
|
||||||
}).
|
}).
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
@ -56,19 +52,10 @@ get_pid(UUID) when is_binary(UUID) ->
|
|||||||
get_name(UUID) when is_binary(UUID) ->
|
get_name(UUID) when is_binary(UUID) ->
|
||||||
binary_to_atom(<<"iot_host:", UUID/binary>>).
|
binary_to_atom(<<"iot_host:", UUID/binary>>).
|
||||||
|
|
||||||
%% 获取主机的下行主题
|
|
||||||
-spec downstream_topic(UUID :: binary()) -> Topic :: binary().
|
|
||||||
downstream_topic(UUID) when is_binary(UUID) ->
|
|
||||||
<<"host/downstream/", UUID/binary>>.
|
|
||||||
|
|
||||||
-spec upstream_topic(UUID :: binary()) -> Topic :: binary().
|
|
||||||
upstream_topic(UUID) when is_binary(UUID) ->
|
|
||||||
<<"host/upstream/", UUID/binary>>.
|
|
||||||
|
|
||||||
%% 处理消息
|
%% 处理消息
|
||||||
-spec handle(Pid :: pid(), Payload :: binary() | map()) -> no_return().
|
-spec handle(Pid :: pid(), Packet :: {atom(), binary()} | {atom(), {binary(), binary()}}) -> no_return().
|
||||||
handle(Pid, Payload) when is_pid(Pid), is_binary(Payload); is_map(Payload) ->
|
handle(Pid, Packet) when is_pid(Pid) ->
|
||||||
gen_statem:cast(Pid, {handle, Payload}).
|
gen_statem:cast(Pid, {handle, Packet}).
|
||||||
|
|
||||||
%% 重新加载主机的基本信息
|
%% 重新加载主机的基本信息
|
||||||
-spec reload(Pid :: pid()) -> ok | {error, Reason :: any()}.
|
-spec reload(Pid :: pid()) -> ok | {error, Reason :: any()}.
|
||||||
@ -92,17 +79,24 @@ get_metric(Pid) when is_pid(Pid) ->
|
|||||||
has_session(Pid) when is_pid(Pid) ->
|
has_session(Pid) when is_pid(Pid) ->
|
||||||
gen_statem:call(Pid, has_session).
|
gen_statem:call(Pid, has_session).
|
||||||
|
|
||||||
|
-spec attach_channel(pid(), pid()) -> ok.
|
||||||
|
attach_channel(Pid, ChannelPid) when is_pid(Pid), is_pid(ChannelPid) ->
|
||||||
|
gen_statem:call(Pid, {attach_channel, ChannelPid}).
|
||||||
|
|
||||||
|
-spec create_session(Pid :: pid(), PubKey :: binary()) -> {ok, Reply :: binary()}.
|
||||||
|
create_session(Pid, PubKey) when is_pid(Pid), is_binary(PubKey) ->
|
||||||
|
gen_statem:call(Pid, {create_session, PubKey}).
|
||||||
|
|
||||||
%% 基于rsa加密的指令都是不需要会话存在的
|
%% 基于rsa加密的指令都是不需要会话存在的
|
||||||
-spec rsa_encode(Pid :: pid(), CommandType :: integer(), PlainText :: binary()) ->
|
-spec rsa_encode(Pid :: pid(), CommandType :: integer(), PlainText :: binary()) ->
|
||||||
{ok, EncText :: binary()} | {error, Reason :: binary()}.
|
{ok, EncText :: binary()} | {error, Reason :: binary()}.
|
||||||
rsa_encode(Pid, CommandType, PlainText) when is_pid(Pid), is_integer(CommandType), is_binary(PlainText) ->
|
rsa_encode(Pid, CommandType, PlainText) when is_pid(Pid), is_integer(CommandType), is_binary(PlainText) ->
|
||||||
gen_statem:call(Pid, {rsa_encode, CommandType, PlainText}).
|
gen_statem:call(Pid, {rsa_encode, CommandType, PlainText}).
|
||||||
|
|
||||||
-spec aes_encode(Pid :: pid(), CommandType :: integer(), Params :: binary()) ->
|
-spec publish_message(Pid :: pid(), CommandType :: integer(), Params :: binary() | {Encrypt :: atom(), Params :: binary()}) ->
|
||||||
{ok, Command :: binary()} | {error, Reason :: any()}.
|
{ok, Command :: binary()} | {error, Reason :: any()}.
|
||||||
aes_encode(Pid, CommandType, Params) when is_pid(Pid), is_integer(CommandType), is_binary(Params) ->
|
publish_message(Pid, CommandType, Params) when is_pid(Pid), is_integer(CommandType) ->
|
||||||
gen_statem:call(Pid, {aes_encode, CommandType, Params}).
|
gen_statem:call(Pid, {publish_message, self(), CommandType, Params}).
|
||||||
|
|
||||||
|
|
||||||
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
||||||
%% initialize. To ensure a synchronized start-up procedure, this
|
%% initialize. To ensure a synchronized start-up procedure, this
|
||||||
@ -121,24 +115,22 @@ start_link(Name, UUID) when is_atom(Name), is_binary(UUID) ->
|
|||||||
init([UUID]) ->
|
init([UUID]) ->
|
||||||
case host_bo:get_host_by_uuid(UUID) of
|
case host_bo:get_host_by_uuid(UUID) of
|
||||||
{ok, #{<<"status">> := Status, <<"id">> := HostId}} ->
|
{ok, #{<<"status">> := Status, <<"id">> := HostId}} ->
|
||||||
%% 启动心跳定时器
|
|
||||||
erlang:start_timer(?TICKER_INTERVAL, self(), ping_ticker),
|
|
||||||
Aes = list_to_binary(iot_util:rand_bytes(32)),
|
Aes = list_to_binary(iot_util:rand_bytes(32)),
|
||||||
%% 告知主机端需要重新授权
|
|
||||||
gen_server:cast(self(), need_auth),
|
|
||||||
|
|
||||||
StateName = case Status =:= ?HOST_STATUS_INACTIVE of
|
StateName = case Status =:= ?HOST_STATUS_INACTIVE of
|
||||||
true -> denied;
|
true ->
|
||||||
false -> activated
|
denied;
|
||||||
|
false ->
|
||||||
|
%% 重启时,认为主机是离线状态; 等待主机主动建立连接
|
||||||
|
{ok, _} = host_bo:change_status(UUID, ?HOST_STATUS_OFFLINE),
|
||||||
|
activated
|
||||||
end,
|
end,
|
||||||
|
|
||||||
{ok, StateName, #state{host_id = HostId, uuid = UUID, aes = Aes, status = Status}};
|
{ok, StateName, #state{host_id = HostId, uuid = UUID, aes = Aes, status = Status}};
|
||||||
undefined ->
|
undefined ->
|
||||||
lager:warning("[iot_host] host uuid: ~p, loaded from mysql failed", [UUID]),
|
lager:warning("[iot_host] host uuid: ~p, loaded from mysql failed", [UUID]),
|
||||||
ignore
|
ignore
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
%% @doc This function is called by a gen_statem when it needs to find out
|
%% @doc This function is called by a gen_statem when it needs to find out
|
||||||
%% the callback mode of the callback module.
|
%% the callback mode of the callback module.
|
||||||
@ -176,11 +168,20 @@ handle_event({call, From}, {rsa_encode, CommandType, PlainText}, session, State
|
|||||||
handle_event({call, From}, {rsa_encode, _, _}, _, State) ->
|
handle_event({call, From}, {rsa_encode, _, _}, _, State) ->
|
||||||
{keep_state, State, [{reply, From, {error, <<"会话未建立"/utf8>>}}]};
|
{keep_state, State, [{reply, From, {error, <<"会话未建立"/utf8>>}}]};
|
||||||
|
|
||||||
%% 基于aes加密
|
%% 发送普通格式的消息
|
||||||
handle_event({call, From}, {aes_encode, CommandType, Command}, session, State = #state{aes = AES}) ->
|
handle_event({call, From}, {publish_message, ReceiverPid, CommandType, Command}, session, State = #state{aes = AES, channel_pid = ChannelPid}) ->
|
||||||
EncCommand = iot_cipher_aes:encrypt(AES, Command),
|
SendCommand = case Command of
|
||||||
{keep_state, State, [{reply, From, {ok, <<CommandType:8, EncCommand/binary>>}}]};
|
{aes, Command0} ->
|
||||||
handle_event({call, From}, {aes_encode, _, _}, _, State) ->
|
iot_cipher_aes:encrypt(AES, Command0);
|
||||||
|
Command0 ->
|
||||||
|
Command0
|
||||||
|
end,
|
||||||
|
|
||||||
|
%% 通过websocket发送请求
|
||||||
|
Ref = ws_channel:publish(ChannelPid, ReceiverPid, <<CommandType:8, SendCommand/binary>>),
|
||||||
|
|
||||||
|
{keep_state, State, [{reply, From, {ok, Ref}}]};
|
||||||
|
handle_event({call, From}, {publish_message, _, _, _}, _, State) ->
|
||||||
{keep_state, State, [{reply, From, {error, <<"会话未建立"/utf8>>}}]};
|
{keep_state, State, [{reply, From, {error, <<"会话未建立"/utf8>>}}]};
|
||||||
|
|
||||||
handle_event({call, From}, reload, StateName, State = #state{uuid = UUID}) ->
|
handle_event({call, From}, reload, StateName, State = #state{uuid = UUID}) ->
|
||||||
@ -208,61 +209,47 @@ handle_event({call, From}, {activate, true}, denied, State) ->
|
|||||||
handle_event({call, From}, {activate, true}, _, State) ->
|
handle_event({call, From}, {activate, true}, _, State) ->
|
||||||
{keep_state, State, [{reply, From, ok}]};
|
{keep_state, State, [{reply, From, ok}]};
|
||||||
|
|
||||||
handle_event(cast, need_auth, _StateName, State = #state{uuid = UUID}) ->
|
%% 绑定channel
|
||||||
Reply = jiffy:encode(#{<<"auth">> => false, <<"aes">> => <<"">>}, [force_utf8]),
|
handle_event({call, From}, {attach_channel, ChannelPid}, _, State = #state{uuid = UUID, channel_pid = undefined}) ->
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(downstream_topic(UUID), <<8:8, Reply/binary>>, 1),
|
lager:debug("[iot_host] attach_channel host_id uuid: ~p, channel: ~p", [UUID, ChannelPid]),
|
||||||
receive
|
MRef = erlang:monitor(process, ChannelPid),
|
||||||
{ok, Ref, PacketId} ->
|
{keep_state, State#state{channel_pid = ChannelPid, monitor_ref = MRef}, [{reply, From, ok}]};
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, packet_id: ~p, publish need_auth reply success", [UUID, PacketId])
|
|
||||||
after 5000 ->
|
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, publish need_auth reply get error is: timeout", [UUID])
|
|
||||||
end,
|
|
||||||
{keep_state, State};
|
|
||||||
|
|
||||||
%% 需要将消息转换成json格式然后再处理, 需要在host进程里面处理
|
handle_event({call, From}, {attach_channel, ChannelPid}, _, State = #state{uuid = UUID, monitor_ref = MRef0, channel_pid = ChannelPid0}) when is_pid(ChannelPid0) ->
|
||||||
%% 收到消息则认为主机端已经发送了心跳包
|
lager:debug("[iot_host] attach_channel host_id uuid: ~p, old channel: ~p replace with: ~p", [UUID, ChannelPid0, ChannelPid]),
|
||||||
|
%% 取消之前的monitor
|
||||||
|
erlang:demonitor(MRef0),
|
||||||
|
ws_channel:stop(ChannelPid0, closed),
|
||||||
|
%% 建立到新的channel的monitor
|
||||||
|
MRef = erlang:monitor(process, ChannelPid),
|
||||||
|
{keep_state, State#state{channel_pid = ChannelPid, monitor_ref = MRef}, [{reply, From, ok}]};
|
||||||
|
|
||||||
handle_event(cast, {handle, <<?METHOD_CREATE_SESSION:8, PubKey/binary>>}, denied, State = #state{uuid = UUID}) ->
|
%% 授权通过后,才能将主机的状态设置为在线状态
|
||||||
|
handle_event({call, From}, {create_session, PubKey}, denied, State = #state{uuid = UUID}) ->
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, create_session", [UUID]),
|
lager:debug("[iot_host] host_id uuid: ~p, create_session", [UUID]),
|
||||||
Reply = #{<<"a">> => false, <<"aes">> => <<"">>},
|
Reply = #{<<"a">> => false, <<"aes">> => <<"">>},
|
||||||
EncReply = iot_cipher_rsa:encode(Reply, PubKey),
|
EncReply = iot_cipher_rsa:encode(Reply, PubKey),
|
||||||
|
{keep_state, State, [{reply, From, {ok, <<10:8, EncReply/binary>>}}]};
|
||||||
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(downstream_topic(UUID), <<10:8, EncReply/binary>>, 1),
|
handle_event({call, From}, {create_session, PubKey}, _StateName, State = #state{uuid = UUID, aes = Aes}) ->
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, packet_id: ~p, publish register reply success", [UUID, PacketId])
|
|
||||||
after 10000 ->
|
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, publish register reply get error is: timeout", [UUID])
|
|
||||||
end,
|
|
||||||
{keep_state, State#state{is_answered = true}};
|
|
||||||
|
|
||||||
handle_event(cast, {handle, <<?METHOD_CREATE_SESSION:8, PubKey/binary>>}, _StateName, State = #state{uuid = UUID, aes = Aes}) ->
|
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, create_session", [UUID]),
|
lager:debug("[iot_host] host_id uuid: ~p, create_session", [UUID]),
|
||||||
Reply = #{<<"a">> => true, <<"aes">> => Aes},
|
Reply = #{<<"a">> => true, <<"aes">> => Aes},
|
||||||
EncReply = iot_cipher_rsa:encode(Reply, PubKey),
|
EncReply = iot_cipher_rsa:encode(Reply, PubKey),
|
||||||
|
{ok, _} = host_bo:change_status(UUID, ?HOST_STATUS_ONLINE),
|
||||||
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(downstream_topic(UUID), <<10:8, EncReply/binary>>, 1),
|
{next_state, session, State#state{status = ?HOST_STATUS_ONLINE}, [{reply, From, {ok, <<10:8, EncReply/binary>>}}]};
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, packet_id: ~p, publish register reply success", [UUID, PacketId]),
|
|
||||||
{next_state, session, State#state{pub_key = PubKey, is_answered = true}}
|
|
||||||
after 10000 ->
|
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, publish register reply get error is: timeout", [UUID]),
|
|
||||||
{keep_state, State#state{is_answered = true}}
|
|
||||||
end;
|
|
||||||
|
|
||||||
handle_event(cast, {handle, <<?METHOD_DATA:8, Data/binary>>}, session, State = #state{uuid = UUID, aes = AES}) ->
|
%% 需要将消息转换成json格式然后再处理, 需要在host进程里面处理
|
||||||
|
handle_event(cast, {handle, {data, Data}}, session, State = #state{uuid = UUID, aes = AES}) ->
|
||||||
PlainData = iot_cipher_aes:decrypt(AES, Data),
|
PlainData = iot_cipher_aes:decrypt(AES, Data),
|
||||||
case catch jiffy:decode(PlainData, [return_maps]) of
|
case catch jiffy:decode(PlainData, [return_maps]) of
|
||||||
Infos when is_list(Infos) ->
|
Infos when is_list(Infos) ->
|
||||||
lager:debug("[iot_host] the data is: ~p", [Infos]),
|
lager:debug("[iot_host] the data is: ~p", [Infos]),
|
||||||
%% 记录数据, TODO 转换点位信息, Fields里面包含了 <<"device_id">> 信息
|
%% 记录数据, tags里面包含了 <<"device_uuid">> 信息
|
||||||
lists:foreach(fun(Info = #{<<"service_name">> := ServiceName, <<"fields">> := FieldsList, <<"tags">> := Tags}) when is_binary(ServiceName) ->
|
lists:foreach(fun(Info = #{<<"service_name">> := ServiceName, <<"fields">> := FieldsList, <<"tags">> := Tags}) when is_binary(ServiceName) ->
|
||||||
Timestamp = maps:get(<<"at">>, Info, iot_util:timestamp()),
|
Timestamp = maps:get(<<"at">>, Info, iot_util:timestamp()),
|
||||||
|
|
||||||
NTags = Tags#{<<"uuid">> => UUID, <<"service_name">> => ServiceName},
|
NTags = Tags#{<<"uuid">> => UUID, <<"service_name">> => ServiceName},
|
||||||
%% 微服务名前缀作为measurement来保存数据
|
Measurement = <<"metric">>,
|
||||||
[Measurement | _] = binary:split(ServiceName, <<":">>),
|
|
||||||
|
|
||||||
Points = lists:map(fun(Fields) -> influx_point:new(Measurement, NTags, Fields, Timestamp) end, FieldsList),
|
Points = lists:map(fun(Fields) -> influx_point:new(Measurement, NTags, Fields, Timestamp) end, FieldsList),
|
||||||
Precision = influx_client:get_precision(Timestamp),
|
Precision = influx_client:get_precision(Timestamp),
|
||||||
@ -270,22 +257,51 @@ handle_event(cast, {handle, <<?METHOD_DATA:8, Data/binary>>}, session, State = #
|
|||||||
poolboy:transaction(influx_pool, fun(Pid) -> influx_client:write(Pid, <<"iot">>, <<"iot">>, Precision, Points) end)
|
poolboy:transaction(influx_pool, fun(Pid) -> influx_client:write(Pid, <<"iot">>, <<"iot">>, Precision, Points) end)
|
||||||
end, Infos);
|
end, Infos);
|
||||||
Other ->
|
Other ->
|
||||||
lager:debug("[iot_message_handler] the data is invalid json: ~p", [Other])
|
lager:debug("[iot_host] the data is invalid json: ~p", [Other])
|
||||||
end,
|
end,
|
||||||
{keep_state, State#state{is_answered = true}};
|
{keep_state, State};
|
||||||
|
|
||||||
handle_event(cast, {handle, <<?METHOD_PING:8, CipherMetric/binary>>}, session, State = #state{uuid = UUID, aes = AES}) ->
|
%% TODO 处理微服务的北向数据
|
||||||
|
handle_event(cast, {handle, {north_data, Data}}, session, State = #state{uuid = UUID, aes = AES}) ->
|
||||||
|
PlainData = iot_cipher_aes:decrypt(AES, Data),
|
||||||
|
lager:debug("[iot_host] the north_data is: ~p", [PlainData]),
|
||||||
|
%% 查找主机对应的点位信息
|
||||||
|
case mnesia_kv:hget(UUID, <<"location_code">>) of
|
||||||
|
none ->
|
||||||
|
lager:debug("[iot_host] the north_data hget location_code uuid: ~p, not found", [UUID]);
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:debug("[iot_host] the north_data hget location_code uuid: ~p, get error: ~p", [UUID, Reason]);
|
||||||
|
{ok, LocationCode} ->
|
||||||
|
iot_router:route(LocationCode, Data)
|
||||||
|
end,
|
||||||
|
{keep_state, State};
|
||||||
|
|
||||||
|
handle_event(cast, {handle, {north_data, {DeviceUUID, Data}}}, session, State = #state{uuid = UUID, aes = AES}) ->
|
||||||
|
PlainData = iot_cipher_aes:decrypt(AES, Data),
|
||||||
|
lager:debug("[iot_host] the north_data uuid: ~p, device_uuid: ~p, is: ~p", [UUID, DeviceUUID, PlainData]),
|
||||||
|
%% 查找终端设备对应的点位信息
|
||||||
|
case mnesia_kv:hget(DeviceUUID, <<"location_code">>) of
|
||||||
|
none ->
|
||||||
|
lager:debug("[iot_host] the north_data hget location_code uuid: ~p, device_uuid: ~p, not found", [UUID, DeviceUUID]);
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:debug("[iot_host] the north_data hget location_code uuid: ~p, device_uuid: ~p, get error: ~p", [UUID, DeviceUUID, Reason]);
|
||||||
|
{ok, LocationCode} ->
|
||||||
|
iot_router:route(LocationCode, Data)
|
||||||
|
end,
|
||||||
|
{keep_state, State};
|
||||||
|
|
||||||
|
handle_event(cast, {handle, {ping, CipherMetric}}, session, State = #state{uuid = UUID, aes = AES}) ->
|
||||||
MetricsInfo = iot_cipher_aes:decrypt(AES, CipherMetric),
|
MetricsInfo = iot_cipher_aes:decrypt(AES, CipherMetric),
|
||||||
case catch jiffy:decode(MetricsInfo, [return_maps]) of
|
case catch jiffy:decode(MetricsInfo, [return_maps]) of
|
||||||
Metrics when is_map(Metrics) ->
|
Metrics when is_map(Metrics) ->
|
||||||
lager:debug("[iot_host] host_id uuid: ~p, get ping: ~p", [UUID, Metrics]),
|
lager:debug("[iot_host] host_id uuid: ~p, get ping: ~p", [UUID, Metrics]),
|
||||||
{keep_state, State#state{metrics = Metrics, is_answered = true}};
|
{keep_state, State#state{metrics = Metrics}};
|
||||||
Other ->
|
Other ->
|
||||||
lager:debug("[iot_message_handler] host_id: ~p, ping is invalid json: ~p", [UUID, Other]),
|
lager:debug("[iot_host] host_id: ~p, ping is invalid json: ~p", [UUID, Other]),
|
||||||
{keep_state, State#state{is_answered = true}}
|
{keep_state, State}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
handle_event(cast, {handle, <<?METHOD_INFORM:8, Info0/binary>>}, session, State = #state{host_id = HostId, aes = AES}) ->
|
handle_event(cast, {handle, {inform, Info0}}, session, State = #state{host_id = HostId, aes = AES}) ->
|
||||||
Info = iot_cipher_aes:decrypt(AES, Info0),
|
Info = iot_cipher_aes:decrypt(AES, Info0),
|
||||||
case catch jiffy:decode(Info, [return_maps]) of
|
case catch jiffy:decode(Info, [return_maps]) of
|
||||||
#{<<"at">> := At, <<"services">> := ServiceInforms} ->
|
#{<<"at">> := At, <<"services">> := ServiceInforms} ->
|
||||||
@ -310,9 +326,9 @@ handle_event(cast, {handle, <<?METHOD_INFORM:8, Info0/binary>>}, session, State
|
|||||||
Error ->
|
Error ->
|
||||||
lager:warning("[iot_host] inform get error: ~p", [Error])
|
lager:warning("[iot_host] inform get error: ~p", [Error])
|
||||||
end,
|
end,
|
||||||
{keep_state, State#state{is_answered = true}};
|
{keep_state, State};
|
||||||
|
|
||||||
handle_event(cast, {handle, <<?METHOD_FEEDBACK_STEP:8, Info0/binary>>}, session, State = #state{aes = AES}) ->
|
handle_event(cast, {handle, {feedback_step, Info0}}, session, State = #state{aes = AES}) ->
|
||||||
Info = iot_cipher_aes:decrypt(AES, Info0),
|
Info = iot_cipher_aes:decrypt(AES, Info0),
|
||||||
case catch jiffy:decode(Info, [return_maps]) of
|
case catch jiffy:decode(Info, [return_maps]) of
|
||||||
Data = #{<<"task_id">> := TaskId, <<"code">> := Code} ->
|
Data = #{<<"task_id">> := TaskId, <<"code">> := Code} ->
|
||||||
@ -327,7 +343,7 @@ handle_event(cast, {handle, <<?METHOD_FEEDBACK_STEP:8, Info0/binary>>}, session,
|
|||||||
end,
|
end,
|
||||||
{keep_state, State};
|
{keep_state, State};
|
||||||
|
|
||||||
handle_event(cast, {handle, <<?METHOD_FEEDBACK_RESULT:8, Info0/binary>>}, session, State = #state{aes = AES}) ->
|
handle_event(cast, {handle, {feedback_result, Info0}}, session, State = #state{aes = AES}) ->
|
||||||
Info = iot_cipher_aes:decrypt(AES, Info0),
|
Info = iot_cipher_aes:decrypt(AES, Info0),
|
||||||
case catch jiffy:decode(Info, [return_maps]) of
|
case catch jiffy:decode(Info, [return_maps]) of
|
||||||
#{<<"task_id">> := TaskId, <<"time">> := Time, <<"code">> := Code, <<"reason">> := Reason, <<"error">> := Error, <<"type">> := Type} ->
|
#{<<"task_id">> := TaskId, <<"time">> := Time, <<"code">> := Code, <<"reason">> := Reason, <<"error">> := Error, <<"type">> := Type} ->
|
||||||
@ -344,29 +360,17 @@ handle_event(cast, {handle, <<?METHOD_FEEDBACK_RESULT:8, Info0/binary>>}, sessio
|
|||||||
end,
|
end,
|
||||||
{keep_state, State};
|
{keep_state, State};
|
||||||
|
|
||||||
handle_event(info, {timeout, _, ping_ticker}, _StateName, State = #state{uuid = UUID, is_answered = IsAnswered, status = Status}) ->
|
%% 当websocket断开的时候,则设置主机状态为下线状态; 主机的状态需要转换
|
||||||
erlang:start_timer(?TICKER_INTERVAL, self(), ping_ticker),
|
handle_event(info, {'DOWN', Ref, process, ChannelPid, Reason}, StateName, State = #state{uuid = UUID, monitor_ref = Ref, channel_pid = ChannelPid}) ->
|
||||||
%% 需要考虑到主机未激活的情况,主机未激活,返回: keep_status
|
lager:warning("[iot_host] channel: ~p, down with reason: ~p, state name: ~p, state: ~p", [ChannelPid, Reason, StateName, State]),
|
||||||
NextStatus = if
|
{ok, _} = host_bo:change_status(UUID, ?HOST_STATUS_OFFLINE),
|
||||||
not IsAnswered andalso Status == ?HOST_STATUS_ONLINE ->
|
|
||||||
{change_status, ?HOST_STATUS_OFFLINE};
|
|
||||||
IsAnswered andalso Status == ?HOST_STATUS_OFFLINE ->
|
|
||||||
{change_status, ?HOST_STATUS_ONLINE};
|
|
||||||
true ->
|
|
||||||
keep_status
|
|
||||||
end,
|
|
||||||
|
|
||||||
case NextStatus of
|
%% 会话状态如果链接丢失,需要切换到activated状态,其他情况保持不变
|
||||||
keep_status ->
|
case StateName =:= session of
|
||||||
{keep_state, State#state{is_answered = false}};
|
true ->
|
||||||
{change_status, NStatus} ->
|
{next_state, activated, State#state{status = ?HOST_STATUS_OFFLINE, channel_pid = undefined}};
|
||||||
case host_bo:change_status(UUID, NStatus) of
|
false ->
|
||||||
{ok, _} ->
|
{keep_state, State#state{status = ?HOST_STATUS_OFFLINE, channel_pid = undefined}}
|
||||||
{keep_state, State#state{status = NStatus, is_answered = false}};
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:warning("[iot_host] change host status of uuid: ~p, error: ~p", [UUID, Reason]),
|
|
||||||
{keep_state, State#state{is_answered = false}}
|
|
||||||
end
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
handle_event(EventType, EventContent, StateName, State) ->
|
handle_event(EventType, EventContent, StateName, State) ->
|
||||||
|
|||||||
@ -1,54 +0,0 @@
|
|||||||
%%%-------------------------------------------------------------------
|
|
||||||
%%% @author licheng5
|
|
||||||
%%% @copyright (C) 2020, <COMPANY>
|
|
||||||
%%% @doc
|
|
||||||
%%%
|
|
||||||
%%% @end
|
|
||||||
%%% Created : 04. 12月 2020 下午3:55
|
|
||||||
%%%-------------------------------------------------------------------
|
|
||||||
-module(iot_mnesia).
|
|
||||||
-author("licheng5").
|
|
||||||
-include("iot.hrl").
|
|
||||||
|
|
||||||
%% API
|
|
||||||
-export([init_database/0, copy_database/1, join/1]).
|
|
||||||
|
|
||||||
%% 只能调用一次
|
|
||||||
init_database() ->
|
|
||||||
%% 清理掉以前的schema
|
|
||||||
mnesia:stop(),
|
|
||||||
mnesia:delete_schema([node()]),
|
|
||||||
|
|
||||||
%% 创建schema
|
|
||||||
ok = mnesia:create_schema([node()]),
|
|
||||||
ok = mnesia:start(),
|
|
||||||
|
|
||||||
%% 创建数据库表
|
|
||||||
%% 主机表
|
|
||||||
%mnesia:create_table(host, [
|
|
||||||
% {attributes, record_info(fields, host)},
|
|
||||||
% {record_name, host},
|
|
||||||
% {disc_copies, [node()]},
|
|
||||||
% {type, ordered_set}
|
|
||||||
%]),
|
|
||||||
|
|
||||||
ok.
|
|
||||||
|
|
||||||
%% 加入集群
|
|
||||||
join(MasterNode) when is_atom(MasterNode) ->
|
|
||||||
net_kernel:connect_node(MasterNode).
|
|
||||||
|
|
||||||
%% 初始化slave数据库
|
|
||||||
copy_database(MasterNode) when is_atom(MasterNode) ->
|
|
||||||
%% 清理旧的schema
|
|
||||||
mnesia:stop(),
|
|
||||||
mnesia:delete_schema([node()]),
|
|
||||||
%% 重新启动数据库
|
|
||||||
mnesia:start(),
|
|
||||||
|
|
||||||
rpc:call(MasterNode, mnesia, change_config, [extra_db_nodes, [node()]]),
|
|
||||||
mnesia:change_table_copy_type(schema, node(), disc_copies),
|
|
||||||
|
|
||||||
%% 增加表的分区复制
|
|
||||||
% mnesia:add_table_copy(host, node(), ram_copies),
|
|
||||||
ok.
|
|
||||||
@ -1,172 +0,0 @@
|
|||||||
%%%-------------------------------------------------------------------
|
|
||||||
%%% @author aresei
|
|
||||||
%%% @copyright (C) 2023, <COMPANY>
|
|
||||||
%%% @doc
|
|
||||||
%%% 1. 需要考虑集群部署的相关问题,上行的数据可能在集群中共享
|
|
||||||
%%% 2. host进程不能直接去监听topic,这样涉及到新增和下线的很多问题
|
|
||||||
%%% @end
|
|
||||||
%%% Created : 12. 3月 2023 21:27
|
|
||||||
%%%-------------------------------------------------------------------
|
|
||||||
-module(iot_mqtt_reply_subscriber).
|
|
||||||
-author("aresei").
|
|
||||||
-include("iot.hrl").
|
|
||||||
|
|
||||||
-behaviour(gen_server).
|
|
||||||
|
|
||||||
%% API
|
|
||||||
-export([start_link/0, make_assoc/1]).
|
|
||||||
|
|
||||||
%% gen_server callbacks
|
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
|
||||||
|
|
||||||
-define(SERVER, ?MODULE).
|
|
||||||
|
|
||||||
%% 需要订阅的主题信息
|
|
||||||
-define(Topic, <<"system/assoc_reply">>).
|
|
||||||
|
|
||||||
-record(state, {
|
|
||||||
conn_pid :: pid(),
|
|
||||||
|
|
||||||
%% 发送完成但是还未收到响应的请求
|
|
||||||
inflight = #{} :: map(),
|
|
||||||
|
|
||||||
%% 关联数据
|
|
||||||
assoc_map = #{}
|
|
||||||
}).
|
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% API
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
-spec make_assoc(UUID :: binary()) -> {ok, Assoc :: binary(), Topic :: binary()}.
|
|
||||||
make_assoc(UUID) when is_binary(UUID) ->
|
|
||||||
gen_server:call(?MODULE, {make_assoc, UUID, self()}).
|
|
||||||
|
|
||||||
%% @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 = iot_config:emqt_opts(<<"assoc-reply-subscriber">>),
|
|
||||||
{ok, ConnPid} = emqtt:start_link(Opts),
|
|
||||||
%% 监听和host相关的全部事件
|
|
||||||
{ok, _} = emqtt:connect(ConnPid),
|
|
||||||
lager:debug("[iot_mqtt_reply_subscriber] connect success, pid: ~p", [ConnPid]),
|
|
||||||
SubscribeResult = emqtt:subscribe(ConnPid, {?Topic, 1}),
|
|
||||||
|
|
||||||
lager:debug("[iot_mqtt_reply_subscriber] subscribe topics: ~p, result is: ~p", [?Topic, SubscribeResult]),
|
|
||||||
|
|
||||||
{ok, #state{conn_pid = ConnPid}}.
|
|
||||||
|
|
||||||
%% @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({make_assoc, UUID, ReceiverPid}, _From, State = #state{conn_pid = _ConnPid, assoc_map = AssocMap}) ->
|
|
||||||
Rand = list_to_binary(iot_util:rand_bytes(16)),
|
|
||||||
Assoc = <<UUID/binary, ":assoc:", Rand/binary>>,
|
|
||||||
|
|
||||||
{reply, {ok, Assoc, ?Topic}, State#state{assoc_map = maps:put(Assoc, ReceiverPid, AssocMap)}}.
|
|
||||||
|
|
||||||
%% @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({disconnected, ReasonCode, Properties}, State = #state{}) ->
|
|
||||||
lager:debug("[iot_mqtt_reply_subscriber] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
|
||||||
{stop, disconnected, State};
|
|
||||||
%% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行
|
|
||||||
handle_info({publish, #{payload := Payload, qos := Qos}}, State = #state{assoc_map = AssocMap}) ->
|
|
||||||
lager:debug("[iot_mqtt_reply_subscriber] Recv a reply packet: ~p, qos: ~p", [Payload, Qos]),
|
|
||||||
|
|
||||||
%% 处理客户端激活的响应, 完整格式为: {"code": 0|1, "message": "", "assoc": string}
|
|
||||||
case catch jiffy:decode(Payload, [return_maps]) of
|
|
||||||
Msg = #{<<"code">> := _Code, <<"assoc">> := Assoc} ->
|
|
||||||
case maps:take(Assoc, AssocMap) of
|
|
||||||
error ->
|
|
||||||
{noreply, State};
|
|
||||||
{ReceiverPid, NAssocMap} ->
|
|
||||||
ReceiverPid ! {host_reply, Assoc, Msg},
|
|
||||||
{noreply, State#state{assoc_map = NAssocMap}}
|
|
||||||
end;
|
|
||||||
_ ->
|
|
||||||
{noreply, State}
|
|
||||||
end;
|
|
||||||
|
|
||||||
handle_info({puback, Packet = #{packet_id := _PacketId}}, State = #state{}) ->
|
|
||||||
lager:debug("[iot_mqtt_reply_subscriber] receive puback packet: ~p", [Packet]),
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
%% 收到任务的反馈信息
|
|
||||||
handle_info({ok, Ref, _PacketId}, State = #state{inflight = Inflight}) ->
|
|
||||||
case maps:take(Ref, Inflight) of
|
|
||||||
error ->
|
|
||||||
{noreply, State};
|
|
||||||
{{UUID, Msg}, NInflight} ->
|
|
||||||
lager:debug("[iot_mqtt_reply_subscriber] send message: ~p, to uuid: ~p, success", [Msg, UUID]),
|
|
||||||
|
|
||||||
{noreply, State#state{inflight = NInflight}}
|
|
||||||
end;
|
|
||||||
|
|
||||||
handle_info(Info, State = #state{}) ->
|
|
||||||
lager:debug("[iot_mqtt_reply_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的订阅
|
|
||||||
{ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, ?Topic),
|
|
||||||
|
|
||||||
ok = emqtt:disconnect(ConnPid),
|
|
||||||
lager:debug("[iot_mqtt_reply_subscriber] terminate with reason: ~p", [Reason]),
|
|
||||||
ok;
|
|
||||||
terminate(Reason, _State) ->
|
|
||||||
lager:debug("[iot_mqtt_reply_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
|
|
||||||
%%%===================================================================
|
|
||||||
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
%% 需要订阅的主题信息
|
%% 需要订阅的主题信息
|
||||||
-define(Topics,[
|
-define(Topics,[
|
||||||
{<<"host/upstream/+">>, 1}
|
{<<"CET/NX/+/upload">>, 2}
|
||||||
]).
|
]).
|
||||||
|
|
||||||
-record(state, {
|
-record(state, {
|
||||||
@ -52,21 +52,23 @@ start_link() ->
|
|||||||
init([]) ->
|
init([]) ->
|
||||||
%% 建立到emqx服务器的连接
|
%% 建立到emqx服务器的连接
|
||||||
Opts = iot_config:emqt_opts(<<"host-subscriber">>),
|
Opts = iot_config:emqt_opts(<<"host-subscriber">>),
|
||||||
|
lager:debug("[opts] is: ~p", [Opts]),
|
||||||
case emqtt:start_link(Opts) of
|
case emqtt:start_link(Opts) of
|
||||||
{ok, ConnPid} ->
|
{ok, ConnPid} ->
|
||||||
%% 监听和host相关的全部事件
|
%% 监听和host相关的全部事件
|
||||||
|
lager:debug("[iot_mqtt_subscriber] start conntecting, pid: ~p", [ConnPid]),
|
||||||
{ok, _} = emqtt:connect(ConnPid),
|
{ok, _} = emqtt:connect(ConnPid),
|
||||||
lager:debug("[iot_mqtt_host_subscriber] connect success, pid: ~p", [ConnPid]),
|
lager:debug("[iot_mqtt_subscriber] connect success, pid: ~p", [ConnPid]),
|
||||||
SubscribeResult = emqtt:subscribe(ConnPid, ?Topics),
|
SubscribeResult = emqtt:subscribe(ConnPid, ?Topics),
|
||||||
|
|
||||||
lager:debug("[iot_mqtt_host_subscriber] subscribe topics: ~p, result is: ~p", [?Topics, SubscribeResult]),
|
lager:debug("[iot_mqtt_subscriber] subscribe topics: ~p, result is: ~p", [?Topics, SubscribeResult]),
|
||||||
|
|
||||||
{ok, #state{conn_pid = ConnPid}};
|
{ok, #state{conn_pid = ConnPid}};
|
||||||
ignore ->
|
ignore ->
|
||||||
lager:debug("[iot_mqtt_host_subscriber] connect emqx get ignore"),
|
lager:debug("[iot_mqtt_subscriber] connect emqx get ignore"),
|
||||||
{stop, ignore};
|
{stop, ignore};
|
||||||
{error, Reason} ->
|
{error, Reason} ->
|
||||||
lager:debug("[iot_mqtt_host_subscriber] connect emqx get error: ~p", [Reason]),
|
lager:debug("[iot_mqtt_subscriber] connect emqx get error: ~p", [Reason]),
|
||||||
{stop, Reason}
|
{stop, Reason}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@ -98,32 +100,15 @@ handle_cast(_Request, State = #state{}) ->
|
|||||||
{noreply, NewState :: #state{}} |
|
{noreply, NewState :: #state{}} |
|
||||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
{stop, Reason :: term(), NewState :: #state{}}).
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
handle_info({disconnected, ReasonCode, Properties}, State = #state{}) ->
|
handle_info({disconnect, ReasonCode, Properties}, State = #state{}) ->
|
||||||
lager:debug("[iot_mqtt_host_subscriber] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
lager:debug("[iot_mqtt_subscriber] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
||||||
{stop, disconnected, State};
|
{stop, disconnected, State};
|
||||||
%% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行
|
%% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行
|
||||||
handle_info({publish, #{payload := Payload, qos := Qos, topic := Topic}}, State = #state{conn_pid = _ConnPid}) ->
|
handle_info({publish, #{packet_id := _PacketId, payload := Payload, qos := Qos, topic := Topic}}, State = #state{conn_pid = _ConnPid}) ->
|
||||||
lager:debug("[iot_mqtt_subscriber] Recv a publish from topic: ~p, qos: ~p", [Topic, Qos]),
|
lager:debug("[iot_mqtt_subscriber] Recv a topic: ~p, publish packet: ~p, qos: ~p", [Topic, Payload, Qos]),
|
||||||
%% 将消息分发到对应的host进程去处理
|
%% 将消息分发到对应的host进程去处理
|
||||||
case Topic of
|
|
||||||
<<"host/upstream/", UUID/binary>> ->
|
|
||||||
case iot_host:get_pid(UUID) of
|
|
||||||
HostPid when is_pid(HostPid) ->
|
|
||||||
iot_host:handle(HostPid, Payload);
|
|
||||||
undefined ->
|
|
||||||
%% 尝试加载主机信息,并提交任务
|
|
||||||
case iot_host_sup:ensured_host_started(UUID) of
|
|
||||||
{ok, NewHostPid} ->
|
|
||||||
iot_host:handle(NewHostPid, Payload);
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:warning("[iot_mqtt_subscriber] try start_new_host uuid: ~p, get error: ~p", [UUID, Reason])
|
|
||||||
end
|
|
||||||
end;
|
|
||||||
_ ->
|
|
||||||
lager:warning("[iot_mqtt_subscriber] invalid topic: ~p, qos: ~p", [Topic, Qos])
|
|
||||||
end,
|
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
handle_info({puback, Packet}, State = #state{}) ->
|
handle_info({puback, Packet = #{packet_id := _PacketId}}, State = #state{}) ->
|
||||||
lager:debug("[iot_mqtt_subscriber] receive puback packet: ~p", [Packet]),
|
lager:debug("[iot_mqtt_subscriber] receive puback packet: ~p", [Packet]),
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
|
|
||||||
@ -138,7 +123,7 @@ handle_info(Info, State = #state{}) ->
|
|||||||
%% with Reason. The return value is ignored.
|
%% with Reason. The return value is ignored.
|
||||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||||
State :: #state{}) -> term()).
|
State :: #state{}) -> term()).
|
||||||
terminate(Reason, #state{conn_pid = ConnPid}) when is_pid(ConnPid) ->
|
terminate(Reason, _State = #state{conn_pid = ConnPid}) when is_pid(ConnPid) ->
|
||||||
%% 取消topic的订阅
|
%% 取消topic的订阅
|
||||||
TopicNames = lists:map(fun({Name, _}) -> Name end, ?Topics),
|
TopicNames = lists:map(fun({Name, _}) -> Name end, ?Topics),
|
||||||
{ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, TopicNames),
|
{ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, TopicNames),
|
||||||
|
|||||||
@ -1,223 +0,0 @@
|
|||||||
%%%-------------------------------------------------------------------
|
|
||||||
%%% @author aresei
|
|
||||||
%%% @copyright (C) 2023, <COMPANY>
|
|
||||||
%%% @doc
|
|
||||||
%%% 1. 需要考虑集群部署的相关问题,上行的数据可能在集群中共享
|
|
||||||
%%% 2. host进程不能直接去监听topic,这样涉及到新增和下线的很多问题
|
|
||||||
%%% @end
|
|
||||||
%%% Created : 12. 3月 2023 21:27
|
|
||||||
%%%-------------------------------------------------------------------
|
|
||||||
-module(iot_mqtt_sys_subscriber).
|
|
||||||
-author("aresei").
|
|
||||||
-include("iot.hrl").
|
|
||||||
|
|
||||||
-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,[
|
|
||||||
{<<"system/upstream">>, 1}
|
|
||||||
]).
|
|
||||||
|
|
||||||
-record(state, {
|
|
||||||
conn_pid :: pid(),
|
|
||||||
|
|
||||||
%% 发送完成但是还未收到响应的请求
|
|
||||||
inflight = #{} :: map()
|
|
||||||
}).
|
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% 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 = iot_config:emqt_opts(<<"system-subscriber">>),
|
|
||||||
case emqtt:start_link(Opts) of
|
|
||||||
{ok, ConnPid} ->
|
|
||||||
%% 监听和host相关的全部事件
|
|
||||||
{ok, _} = emqtt:connect(ConnPid),
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] connect success, pid: ~p", [ConnPid]),
|
|
||||||
SubscribeResult = emqtt:subscribe(ConnPid, ?Topics),
|
|
||||||
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] subscribe topics: ~p, result is: ~p", [?Topics, SubscribeResult]),
|
|
||||||
|
|
||||||
{ok, #state{conn_pid = ConnPid}};
|
|
||||||
ignore ->
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] connect emqx get ignore"),
|
|
||||||
{stop, ignore};
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[iot_mqtt_sys_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({disconnected, ReasonCode, Properties}, State = #state{}) ->
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
|
||||||
{stop, disconnected, State};
|
|
||||||
%% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行
|
|
||||||
handle_info({publish, #{payload := Payload, qos := Qos, topic := <<"system/upstream">>}}, State) ->
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] Recv a register packet: ~p, qos: ~p", [Payload, Qos]),
|
|
||||||
|
|
||||||
Message = catch jiffy:decode(Payload, [return_maps]),
|
|
||||||
NState = handle_message(Message, State),
|
|
||||||
|
|
||||||
{noreply, NState};
|
|
||||||
|
|
||||||
handle_info({puback, Packet = #{packet_id := _PacketId}}, State = #state{}) ->
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] receive puback packet: ~p", [Packet]),
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
%% 收到任务的反馈信息
|
|
||||||
handle_info({ok, Ref, _PacketId}, State = #state{inflight = Inflight}) ->
|
|
||||||
case maps:take(Ref, Inflight) of
|
|
||||||
error ->
|
|
||||||
{noreply, State};
|
|
||||||
{{UUID, Msg}, NInflight} ->
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] send message: ~p, to uuid: ~p, success", [Msg, UUID]),
|
|
||||||
|
|
||||||
{noreply, State#state{inflight = NInflight}}
|
|
||||||
end;
|
|
||||||
|
|
||||||
handle_info(Info, State = #state{}) ->
|
|
||||||
lager:debug("[iot_mqtt_sys_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_sys_subscriber] terminate with reason: ~p", [Reason]),
|
|
||||||
ok;
|
|
||||||
terminate(Reason, _State) ->
|
|
||||||
lager:debug("[iot_mqtt_sys_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
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
%% 处理系统消息
|
|
||||||
handle_message(#{<<"method">> := <<"register">>, <<"params">> := #{<<"uuid">> := UUID}}, State = #state{inflight = Inflight}) when is_binary(UUID) ->
|
|
||||||
Topic = iot_host:downstream_topic(UUID),
|
|
||||||
Qos = 2,
|
|
||||||
%% 查找数据库,如果没有则插入
|
|
||||||
case host_bo:get_host_by_uuid(UUID) of
|
|
||||||
{ok, Host} ->
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] register, host uuid: ~p, info: ~p, exists", [UUID, Host]),
|
|
||||||
%% 尝试启动主机的服务进程
|
|
||||||
{ok, _} = iot_host_sup:ensured_host_started(UUID),
|
|
||||||
|
|
||||||
Reply = jiffy:encode(#{
|
|
||||||
<<"code">> => 1,
|
|
||||||
<<"message">> => <<"ok">>
|
|
||||||
}),
|
|
||||||
case iot_mqtt_publisher:publish(Topic, <<0:8, Reply/binary>>, Qos) of
|
|
||||||
{ok, Ref} ->
|
|
||||||
State#state{inflight = maps:put(Ref, {UUID, Reply}, Inflight)};
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[iot_host] publish topic get error: ~p", [Reason]),
|
|
||||||
State
|
|
||||||
end;
|
|
||||||
undefined ->
|
|
||||||
case host_bo:create_host(UUID) of
|
|
||||||
{ok, HostId} ->
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] create host success, uuid: ~p, host_id: ~p", [UUID, HostId]),
|
|
||||||
%% 尝试启动主机的服务进程
|
|
||||||
{ok, _} = iot_host_sup:ensured_host_started(UUID),
|
|
||||||
|
|
||||||
Reply = jiffy:encode(#{
|
|
||||||
<<"code">> => 1,
|
|
||||||
<<"message">> => <<"ok">>
|
|
||||||
}),
|
|
||||||
case iot_mqtt_publisher:publish(Topic, <<0:8, Reply/binary>>, Qos) of
|
|
||||||
{ok, Ref} ->
|
|
||||||
State#state{inflight = maps:put(Ref, {UUID, Reply}, Inflight)};
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[iot_host] publish topic get error: ~p", [Reason]),
|
|
||||||
State
|
|
||||||
end;
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] create host failed, reason: ~p", [Reason]),
|
|
||||||
Reply = jiffy:encode(#{
|
|
||||||
<<"code">> => 0,
|
|
||||||
<<"message">> => <<"create host failed">>
|
|
||||||
}),
|
|
||||||
case iot_mqtt_publisher:publish(Topic, <<0:8, Reply/binary>>, Qos) of
|
|
||||||
{ok, Ref} ->
|
|
||||||
State#state{inflight = maps:put(Ref, {UUID, Reply}, Inflight)};
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[iot_host] publish topic get error: ~p", [Reason]),
|
|
||||||
State
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end;
|
|
||||||
handle_message(Msg, State) ->
|
|
||||||
lager:warning("[iot_mqtt_sys_subscriber] get invalid message: ~p", [Msg]),
|
|
||||||
State.
|
|
||||||
33
apps/iot/src/iot_router.erl
Normal file
33
apps/iot/src/iot_router.erl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 04. 7月 2023 11:30
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(iot_router).
|
||||||
|
-author("aresei").
|
||||||
|
-include("iot.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([route/2]).
|
||||||
|
|
||||||
|
-spec route(LocationCode :: binary(), Data :: binary()) -> ok.
|
||||||
|
route(LocationCode, Data) when is_binary(LocationCode), is_binary(Data) ->
|
||||||
|
Endpoints = mnesia_endpoint:get_all_endpoints(),
|
||||||
|
router0(Endpoints, LocationCode, Data).
|
||||||
|
router0([], _, _) ->
|
||||||
|
ok;
|
||||||
|
router0([#endpoint{matcher = Regexp, name = Name}|Endpoints], LocationCode, Data) ->
|
||||||
|
{ok, MP} = re:compile(Regexp),
|
||||||
|
case re:run(LocationCode, MP, [{capture, all, list}]) of
|
||||||
|
nomatch ->
|
||||||
|
router0(Endpoints, LocationCode, Data);
|
||||||
|
{match, _} ->
|
||||||
|
lager:debug("[iot_router] match endpoint: ~p", [Name]),
|
||||||
|
Pid = iot_endpoint:get_pid(Name),
|
||||||
|
iot_endpoint:forward(Pid, LocationCode, Data),
|
||||||
|
%% 继续匹配其他的Endpoint
|
||||||
|
router0(Endpoints, LocationCode, Data)
|
||||||
|
end.
|
||||||
@ -28,42 +28,13 @@ start_link() ->
|
|||||||
init([]) ->
|
init([]) ->
|
||||||
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600},
|
||||||
ChildSpecs = [
|
ChildSpecs = [
|
||||||
|
|
||||||
|
|
||||||
#{
|
#{
|
||||||
id => 'iot_mqtt_subscriber',
|
id => 'iot_endpoint_sup',
|
||||||
start => {'iot_mqtt_subscriber', start_link, []},
|
start => {'iot_endpoint_sup', start_link, []},
|
||||||
restart => permanent,
|
restart => permanent,
|
||||||
shutdown => 2000,
|
shutdown => 2000,
|
||||||
type => worker,
|
type => supervisor,
|
||||||
modules => ['iot_mqtt_subscriber']
|
modules => ['iot_endpoint_sup']
|
||||||
},
|
|
||||||
|
|
||||||
#{
|
|
||||||
id => 'iot_mqtt_reply_subscriber',
|
|
||||||
start => {'iot_mqtt_reply_subscriber', start_link, []},
|
|
||||||
restart => permanent,
|
|
||||||
shutdown => 2000,
|
|
||||||
type => worker,
|
|
||||||
modules => ['iot_mqtt_reply_subscriber']
|
|
||||||
},
|
|
||||||
|
|
||||||
#{
|
|
||||||
id => 'iot_mqtt_sys_subscriber',
|
|
||||||
start => {'iot_mqtt_sys_subscriber', start_link, []},
|
|
||||||
restart => permanent,
|
|
||||||
shutdown => 2000,
|
|
||||||
type => worker,
|
|
||||||
modules => ['iot_mqtt_sys_subscriber']
|
|
||||||
},
|
|
||||||
|
|
||||||
#{
|
|
||||||
id => 'iot_mqtt_publisher',
|
|
||||||
start => {'iot_mqtt_publisher', start_link, []},
|
|
||||||
restart => permanent,
|
|
||||||
shutdown => 2000,
|
|
||||||
type => worker,
|
|
||||||
modules => ['iot_mqtt_publisher']
|
|
||||||
},
|
},
|
||||||
|
|
||||||
#{
|
#{
|
||||||
|
|||||||
@ -10,8 +10,8 @@
|
|||||||
-author("licheng5").
|
-author("licheng5").
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([timestamp/0, number_format/2, current_time/0]).
|
-export([timestamp/0, number_format/2, current_time/0, timestamp_of_seconds/0]).
|
||||||
-export([step/3, chunks/2, rand_bytes/1, uuid/0]).
|
-export([step/3, chunks/2, rand_bytes/1, uuid/0, md5/1, parse_mapper/1]).
|
||||||
-export([json_data/1, json_error/2]).
|
-export([json_data/1, json_error/2]).
|
||||||
-export([queue_limited_in/3, assert_call/2]).
|
-export([queue_limited_in/3, assert_call/2]).
|
||||||
|
|
||||||
@ -24,6 +24,10 @@ current_time() ->
|
|||||||
{Mega, Seconds, _Micro} = os:timestamp(),
|
{Mega, Seconds, _Micro} = os:timestamp(),
|
||||||
Mega * 1000000 + Seconds.
|
Mega * 1000000 + Seconds.
|
||||||
|
|
||||||
|
timestamp_of_seconds() ->
|
||||||
|
{Mega, Seconds, _Micro} = os:timestamp(),
|
||||||
|
Mega * 1000000 + Seconds.
|
||||||
|
|
||||||
number_format(Num, _Decimals) when is_integer(Num) ->
|
number_format(Num, _Decimals) when is_integer(Num) ->
|
||||||
Num;
|
Num;
|
||||||
number_format(Float, Decimals) when is_float(Float) ->
|
number_format(Float, Decimals) when is_float(Float) ->
|
||||||
@ -87,3 +91,27 @@ assert_call(true, Fun) ->
|
|||||||
Fun();
|
Fun();
|
||||||
assert_call(false, _) ->
|
assert_call(false, _) ->
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
|
-spec md5(Str :: binary()) -> binary().
|
||||||
|
md5(Str) when is_binary(Str) ->
|
||||||
|
list_to_binary(lists:flatten([hex(X) || <<X:4>> <= erlang:md5(Str)])).
|
||||||
|
|
||||||
|
hex(N) when N < 10 ->
|
||||||
|
$0 + N;
|
||||||
|
hex(N) ->
|
||||||
|
$a + (N - 10).
|
||||||
|
|
||||||
|
%% 转换映射器
|
||||||
|
-spec parse_mapper(Mapper :: binary() | string()) -> error | {ok, F :: fun((binary(), any()) -> any())}.
|
||||||
|
parse_mapper(Mapper) when is_binary(Mapper) ->
|
||||||
|
parse_mapper(binary_to_list(Mapper));
|
||||||
|
parse_mapper(Mapper) when is_list(Mapper) ->
|
||||||
|
{ok, Tokens, _} = erl_scan:string(Mapper),
|
||||||
|
{ok, ExprList} = erl_parse:parse_exprs(Tokens),
|
||||||
|
{value, F, _} = erl_eval:exprs(ExprList, []),
|
||||||
|
case is_function(F, 2) of
|
||||||
|
true ->
|
||||||
|
{ok, F};
|
||||||
|
false ->
|
||||||
|
error
|
||||||
|
end.
|
||||||
69
apps/iot/src/mnesia/mnesia_endpoint.erl
Normal file
69
apps/iot/src/mnesia/mnesia_endpoint.erl
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 04. 7月 2023 11:31
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(mnesia_endpoint).
|
||||||
|
-author("aresei").
|
||||||
|
-include_lib("stdlib/include/qlc.hrl").
|
||||||
|
-include("iot.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([get_all_endpoints/0, get_endpoint/1, insert/1, delete/1]).
|
||||||
|
-export([to_map/1]).
|
||||||
|
|
||||||
|
-spec get_all_endpoints() -> [#endpoint{}].
|
||||||
|
get_all_endpoints() ->
|
||||||
|
Fun = fun() ->
|
||||||
|
Q = qlc:q([E || E <- mnesia:table(endpoint)]),
|
||||||
|
qlc:e(Q)
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, Endpoints} ->
|
||||||
|
Endpoints;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
lager:warning("[mnesia_endpoint] get_all_endpoints get a error: ~p", [Reason]),
|
||||||
|
[]
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec get_endpoint(Name :: binary()) -> undefined | {ok, #endpoint{}}.
|
||||||
|
get_endpoint(Name) when is_binary(Name) ->
|
||||||
|
case mnesia:dirty_read(endpoint, Name) of
|
||||||
|
[Endpoint | _] ->
|
||||||
|
{ok, Endpoint};
|
||||||
|
[] ->
|
||||||
|
undefined
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec insert(Endpoint :: #endpoint{}) -> ok | {error, Reason :: any()}.
|
||||||
|
insert(Endpoint = #endpoint{}) ->
|
||||||
|
case mnesia:transaction(fun() -> mnesia:write(endpoint, Endpoint, write) end) of
|
||||||
|
{atomic, ok} ->
|
||||||
|
ok;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec delete(Name :: binary()) -> ok | {error, Reason :: any()}.
|
||||||
|
delete(Name) when is_binary(Name) ->
|
||||||
|
case mnesia:transaction(fun() -> mnesia:delete(endpoint, Name, write) end) of
|
||||||
|
{atomic, ok} ->
|
||||||
|
ok;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
to_map(#endpoint{name = Name, title = Title, matcher = Matcher, mapper = Mapper, config = Config, updated_at = UpdatedAt, created_at = CreatedAt}) ->
|
||||||
|
#{
|
||||||
|
<<"name">> => Name,
|
||||||
|
<<"title">> => Title,
|
||||||
|
<<"matcher">> => Matcher,
|
||||||
|
<<"mapper">> => Mapper,
|
||||||
|
<<"config">> => Config,
|
||||||
|
<<"updated_at">> => UpdatedAt,
|
||||||
|
<<"created_at">> => CreatedAt
|
||||||
|
}.
|
||||||
|
|
||||||
16
apps/iot/src/mnesia/mnesia_id_generator.erl
Normal file
16
apps/iot/src/mnesia/mnesia_id_generator.erl
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 04. 7月 2023 12:31
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(mnesia_id_generator).
|
||||||
|
-author("aresei").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([next_id/1]).
|
||||||
|
|
||||||
|
next_id(Tab) when is_atom(Tab) ->
|
||||||
|
mnesia:dirty_update_counter(id_generator, Tab, 1).
|
||||||
949
apps/iot/src/mnesia/mnesia_kv.erl
Normal file
949
apps/iot/src/mnesia/mnesia_kv.erl
Normal file
@ -0,0 +1,949 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author licheng5
|
||||||
|
%%% @copyright (C) 2021, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 21. 1月 2021 下午2:17
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(mnesia_kv).
|
||||||
|
-author("licheng5").
|
||||||
|
-include_lib("stdlib/include/qlc.hrl").
|
||||||
|
-include("iot.hrl").
|
||||||
|
|
||||||
|
%% 错误类型
|
||||||
|
-define(WRONG_KIND, <<"Operation against a key holding the wrong kind of value">>).
|
||||||
|
|
||||||
|
-type(wrong_kind() :: binary()).
|
||||||
|
-type(redis_nil() :: none).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([all_expireable_keys/0, all_expired_keys/1, clean_expired_keys/0]).
|
||||||
|
-export([del/1, exists/1, expire/2, keys/1, persist/1, ttl/1, type/1]).
|
||||||
|
-export([get/1, set/2, setnx/2]).
|
||||||
|
-export([hexists/2, hdel/2, hkeys/1, hget/2, hmget/2, hset/3, hmset/2, hgetall/1, hlen/1]).
|
||||||
|
-export([sadd/2, scard/1, sdiff/2, sismember/2, smembers/1, sinter/2, sunion/2, spop/1, srandmember/2, srem/2]).
|
||||||
|
-export([lindex/2, linsert/4, llen/1, lpop/1, lpush/2, lpushx/2, lrange/3, lrem/3, lset/3, ltrim/3, rpop/1, rpush/2, rpushx/2]).
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% 扫描整个表,用于构建过期机制
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 清理掉过期的keys
|
||||||
|
-spec clean_expired_keys() -> ok | {error, Reason :: any()}.
|
||||||
|
clean_expired_keys() ->
|
||||||
|
%NowSecond = iot_util:timestamp_of_seconds(),
|
||||||
|
%case redis_mnesia_kv:all_expired_keys(NowSecond) of
|
||||||
|
% {ok, []} ->
|
||||||
|
% ok;
|
||||||
|
% {ok, Keys} ->
|
||||||
|
% lists:foreach(fun(Key) -> mnesia:transaction(fun() -> mnesia:delete(kv, Key, write) end) end, Keys),
|
||||||
|
% ok
|
||||||
|
%end.
|
||||||
|
ok.
|
||||||
|
|
||||||
|
%% 获取全部可过期的keys
|
||||||
|
-spec all_expireable_keys() -> {ok, [{Key :: binary(), ExpireAt :: integer()}]} | {error, Reason :: any()}.
|
||||||
|
all_expireable_keys() ->
|
||||||
|
Fun = fun() ->
|
||||||
|
Q = qlc:q([{E#kv.key, E#kv.expire_at} || E <- mnesia:table(kv), E#kv.expire_at > 0]),
|
||||||
|
qlc:e(Q)
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, Items} ->
|
||||||
|
{ok, Items};
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec all_expired_keys(ExpireAt :: integer()) -> {ok, [Key :: binary()]} | {error, Reason :: any()}.
|
||||||
|
all_expired_keys(ExpireAt) when is_integer(ExpireAt) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
Q = qlc:q([E#kv.key || E <- mnesia:table(kv),
|
||||||
|
E#kv.expire_at > 0, E#kv.expire_at =< ExpireAt]),
|
||||||
|
qlc:e(Q)
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, Items} ->
|
||||||
|
{ok, Items};
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% Key管理
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 命令用于删除已存在的键。不存在的 key 会被忽略
|
||||||
|
-spec del(Key :: binary()) -> 0 | 1.
|
||||||
|
del(Key) when is_binary(Key) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[#kv{expire_at = ExpireAt}] ->
|
||||||
|
ok = mnesia:delete(kv, Key, write),
|
||||||
|
{1, ExpireAt}
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, {N, _ExpireAt}} ->
|
||||||
|
N;
|
||||||
|
{atomic, N} when is_integer(N) ->
|
||||||
|
N;
|
||||||
|
{aborted, _Reason} ->
|
||||||
|
0
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 若 key 存在返回 1 ,否则返回 0
|
||||||
|
-spec exists(Key :: binary()) -> 0 | 1.
|
||||||
|
exists(Key) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[#kv{}] ->
|
||||||
|
1
|
||||||
|
end.
|
||||||
|
|
||||||
|
% 设置成功返回 1 。 当 key 不存在或者不能为 key 设置过期时间时(比如在低于 2.1.3 版本的 Redis 中你尝试更新 key 的过期时间)返回 0
|
||||||
|
-spec expire(Key :: binary(), Second :: integer()) -> 0 | 1.
|
||||||
|
expire(Key, Second) when is_binary(Key), is_integer(Second) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[KV] ->
|
||||||
|
NExpireAt = iot_util:timestamp_of_seconds() + Second,
|
||||||
|
ok = mnesia:write(kv, KV#kv{expire_at = NExpireAt}, write),
|
||||||
|
1
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, _Reason} ->
|
||||||
|
0
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 只支持前缀的匹配
|
||||||
|
-spec keys(Pattern :: binary()) -> Keys :: list().
|
||||||
|
keys(Pattern) when is_binary(Pattern) ->
|
||||||
|
Keys = mnesia:dirty_all_keys(kv),
|
||||||
|
case Pattern of
|
||||||
|
<<"*">> ->
|
||||||
|
Keys;
|
||||||
|
_ ->
|
||||||
|
case binary:split(Pattern, <<"*">>) of
|
||||||
|
[<<>> | _] ->
|
||||||
|
[];
|
||||||
|
[Prefix | _] ->
|
||||||
|
Len = byte_size(Prefix),
|
||||||
|
lists:filter(fun(Key) ->
|
||||||
|
case Key of
|
||||||
|
<<Prefix:Len/binary, _/binary>> ->
|
||||||
|
true;
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end, Keys)
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 当过期时间移除成功时,返回 1 。 如果 key 不存在或 key 没有设置过期时间,返回 0
|
||||||
|
-spec persist(Key :: binary()) -> 0 | 1.
|
||||||
|
persist(Key) when is_binary(Key) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[#kv{expire_at = 0}] ->
|
||||||
|
0;
|
||||||
|
[KV] ->
|
||||||
|
ok = mnesia:write(kv, KV#kv{expire_at = 0}, write),
|
||||||
|
1
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, _} ->
|
||||||
|
0
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1 。 否则,以秒为单位,返回 key 的剩余生存时间。
|
||||||
|
-spec ttl(Key :: binary()) -> TTL :: integer().
|
||||||
|
ttl(Key) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
-2;
|
||||||
|
[#kv{expire_at = 0}] ->
|
||||||
|
-1;
|
||||||
|
[#kv{expire_at = ExpireAt}] ->
|
||||||
|
NowSeconds = iot_util:timestamp_of_seconds(),
|
||||||
|
ExpireAt - NowSeconds
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 获取key的类型
|
||||||
|
%%-spec type(Key :: binary()) -> None :: redis_nil() | Type :: atom().
|
||||||
|
type(Key) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
none;
|
||||||
|
[#kv{type = Type}] ->
|
||||||
|
Type
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% 字符串处理
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%%-spec get(Key :: binary()) -> redis_nil() | Val :: binary() | {error, Reason :: binary()} .
|
||||||
|
get(Key) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
none;
|
||||||
|
[#kv{val = Val, type = string}] ->
|
||||||
|
Val;
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec set(Key :: binary(), Val :: binary()) -> boolean().
|
||||||
|
set(Key, Val) when is_binary(Key), is_binary(Val) ->
|
||||||
|
KV = #kv{key = Key, val = Val, type = string},
|
||||||
|
case mnesia:transaction(fun() -> mnesia:write(kv, KV, write) end) of
|
||||||
|
{atomic, ok} ->
|
||||||
|
true;
|
||||||
|
{aborted, _} ->
|
||||||
|
false
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec setnx(Key :: binary(), Val :: binary()) -> boolean().
|
||||||
|
setnx(Key, Val) when is_binary(Key), is_binary(Val) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
KV = #kv{key = Key, val = Val, type = string},
|
||||||
|
ok = mnesia:write(kv, KV, write),
|
||||||
|
1;
|
||||||
|
[#kv{}] ->
|
||||||
|
0
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% HashTable处理
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 如果字段是哈希表中的一个新建字段,并且值设置成功,返回 1 。 如果哈希表中域字段已经存在且旧值已被新值覆盖,返回 0
|
||||||
|
-spec hset(Key :: binary(), Field :: binary(), Val :: binary()) -> N :: integer() | {error, Reason :: binary()}.
|
||||||
|
hset(Key, Field, Val) when is_binary(Key), is_binary(Field), is_binary(Val) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
KV = #kv{key = Key, val = #{Field => Val}, type = hash},
|
||||||
|
ok = mnesia:write(kv, KV, write),
|
||||||
|
1;
|
||||||
|
[KV = #kv{val = Map0, type = hash}] ->
|
||||||
|
IsKey = maps:is_key(Field, Map0),
|
||||||
|
Map = maps:put(Field, Val, Map0),
|
||||||
|
ok = mnesia:write(kv, KV#kv{key = Key, val = Map}, write),
|
||||||
|
case IsKey of
|
||||||
|
true -> 0;
|
||||||
|
false -> 1
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec hmset(Key :: binary(), Map :: map()) -> ok | {error, Reason :: binary()}.
|
||||||
|
hmset(Key, Map) when is_binary(Key), is_map(Map) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
KV = #kv{key = Key, val = Map, type = hash},
|
||||||
|
mnesia:write(kv, KV, write);
|
||||||
|
[KV = #kv{val = Map0, type = hash}] ->
|
||||||
|
Map1 = maps:merge(Map, Map0),
|
||||||
|
mnesia:write(kv, KV#kv{key = Key, val = Map1}, write);
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, ok} ->
|
||||||
|
<<"OK">>;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 返回给定字段的值。如果给定的字段或 key 不存在时,返回 nil
|
||||||
|
-spec hget(Key :: binary(), Field :: binary()) -> redis_nil() | {ok, Val :: any()} | {error, Reason :: binary()}.
|
||||||
|
hget(Key, Field) when is_binary(Key), is_binary(Field) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
none;
|
||||||
|
[#kv{val = #{Field := Val}, type = hash}] ->
|
||||||
|
Val;
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec hmget(Key :: binary(), [binary()]) -> list() | {error, Reason :: binary()}.
|
||||||
|
hmget(Key, Fields) when is_binary(Key), is_list(Fields) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
[none || _ <- Fields];
|
||||||
|
[#kv{val = Map0, type = hash}] ->
|
||||||
|
[maps:get(Field, Map0, none) || Field <- Fields];
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec hdel(Key :: binary(), [Field :: binary()]) -> Num :: integer() | {error, Reason :: binary()}.
|
||||||
|
hdel(Key, Fields) when is_binary(Key), is_list(Fields) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[KV = #kv{val = Map, type = hash}] ->
|
||||||
|
Map1 = lists:foldl(fun(Field, Map0) -> maps:remove(Field, Map0) end, Map, Fields),
|
||||||
|
ok = mnesia:write(kv, KV#kv{key = Key, val = Map1}, write),
|
||||||
|
map_size(Map) - map_size(Map1);
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 如果哈希表含有给定字段,返回 1 。 如果哈希表不含有给定字段,或 key 不存在,返回 0
|
||||||
|
-spec hexists(Key :: binary(), Field :: binary()) -> 0 | 1 | {error, Reason :: binary()}.
|
||||||
|
hexists(Key, Field) when is_binary(Key), is_binary(Field) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[#kv{val = Map0, type = hash}] ->
|
||||||
|
case maps:is_key(Field, Map0) of
|
||||||
|
true -> 1;
|
||||||
|
false -> 0
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 以列表形式返回哈希表的字段及字段值。 若 key 不存在,返回空列表。
|
||||||
|
-spec hgetall(Key :: binary()) -> Map :: map() | {error, Reason :: binary()}.
|
||||||
|
hgetall(Key) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
[];
|
||||||
|
[#kv{val = Map, type = hash}] ->
|
||||||
|
lists:foldl(fun({Field, Val}, Acc) -> [Field, Val | Acc] end, [], maps:to_list(Map));
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 包含哈希表中所有域(field)列表。 当 key 不存在时,返回一个空列表。
|
||||||
|
-spec hkeys(Key :: binary()) -> Keys :: list() | {error, Reason :: binary()}.
|
||||||
|
hkeys(Key) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
[];
|
||||||
|
[#kv{val = Map, type = hash}] ->
|
||||||
|
maps:keys(Map);
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 哈希表中字段的数量。 当 key 不存在时,返回 0
|
||||||
|
-spec hlen(Key :: binary()) -> 0 | 1 | {error, Reason :: binary()}.
|
||||||
|
hlen(Key) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[#kv{val = Map, type = hash}] ->
|
||||||
|
map_size(Map);
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% set处理
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 被添加到集合中的新元素的数量,不包括被忽略的元素。
|
||||||
|
-spec sadd(Key :: binary(), Members :: list()) -> Num :: integer() | {error, Reason :: binary()}.
|
||||||
|
sadd(Key, Members) when is_binary(Key), is_list(Members) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
S = sets:from_list(Members),
|
||||||
|
KV = #kv{key = Key, val = S, type = set},
|
||||||
|
ok = mnesia:write(kv, KV, write),
|
||||||
|
sets:size(S);
|
||||||
|
[KV = #kv{val = Set0, type = set}] ->
|
||||||
|
Set1 = lists:foldl(fun(E, S0) -> sets:add_element(E, S0) end, Set0, Members),
|
||||||
|
ok = mnesia:write(kv, KV#kv{key = Key, val = Set1}, write),
|
||||||
|
sets:size(Set1) - sets:size(Set0);
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 集合的数量。 当集合 key 不存在时,返回 0
|
||||||
|
-spec scard(Key :: binary()) -> Num :: integer() | {error, Reason :: wrong_kind()}.
|
||||||
|
scard(Key) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[#kv{val = Set0, type = set}] ->
|
||||||
|
sets:size(Set0);
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 如果成员元素是集合的成员,返回 1 。 如果成员元素不是集合的成员,或 key 不存在,返回 0
|
||||||
|
-spec sismember(Key :: binary(), Member :: binary()) -> boolean() | {error, wrong_kind()}.
|
||||||
|
sismember(Key, Member) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[#kv{val = S, type = set}] ->
|
||||||
|
case sets:is_element(Member, S) of
|
||||||
|
true -> 1;
|
||||||
|
false -> 0
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec sdiff(Key1 :: binary(), Key2 :: binary()) -> list() | {error, wrong_kind()}.
|
||||||
|
sdiff(Key1, Key2) when is_binary(Key1), is_binary(Key2) ->
|
||||||
|
case {mnesia:dirty_read(kv, Key1), mnesia:dirty_read(kv, Key2)} of
|
||||||
|
{[#kv{val = S1, type = set}], [#kv{val = S2, type = set}]} ->
|
||||||
|
sets:to_list(S1) -- sets:to_list(S2);
|
||||||
|
{[#kv{val = S1, type = set}], []} ->
|
||||||
|
sets:to_list(S1);
|
||||||
|
{[], [#kv{type = set}]} ->
|
||||||
|
[];
|
||||||
|
{[], []} ->
|
||||||
|
[];
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec sinter(Key1 :: binary(), Key2 :: binary()) -> list() | {error, wrong_kind()}.
|
||||||
|
sinter(Key1, Key2) when is_binary(Key1), is_binary(Key2) ->
|
||||||
|
case {mnesia:dirty_read(kv, Key1), mnesia:dirty_read(kv, Key2)} of
|
||||||
|
{[#kv{val = S1, type = set}], [#kv{val = S2, type = set}]} ->
|
||||||
|
sets:to_list(sets:intersection(S1, S2));
|
||||||
|
{[#kv{type = set}], []} ->
|
||||||
|
[];
|
||||||
|
{[], [#kv{type = set}]} ->
|
||||||
|
[];
|
||||||
|
{[], []} ->
|
||||||
|
[];
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 集合中的所有的成员。 不存在的集合 key 被视为空集合
|
||||||
|
-spec smembers(Key :: binary()) -> list() | {error, wrong_kind()}.
|
||||||
|
smembers(Key) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[#kv{val = S, type = set}] ->
|
||||||
|
sets:to_list(S);
|
||||||
|
[] ->
|
||||||
|
[];
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 被移除的随机元素。 当集合不存在或是空集时,返回 nil
|
||||||
|
%%-spec spop(Key :: binary()) -> redis_nil() | Member :: binary() | {error, wrong_kind()}.
|
||||||
|
spop(Key) when is_binary(Key) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
none;
|
||||||
|
[KV = #kv{val = S0, type = set}] ->
|
||||||
|
case sets:size(S0) of
|
||||||
|
0 ->
|
||||||
|
none;
|
||||||
|
Size ->
|
||||||
|
E = lists:nth(rand:uniform(Size), sets:to_list(S0)),
|
||||||
|
S1 = sets:del_element(E, S0),
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = S1}, write),
|
||||||
|
E
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, E} ->
|
||||||
|
E;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 只提供集合 key 参数时,返回一个元素;如果集合为空,返回 nil 。 如果提供了 count 参数,那么返回一个数组;如果集合为空,返回空数组。
|
||||||
|
-spec srandmember(Key :: binary(), Count :: integer()) -> list() | {error, wrong_kind()}.
|
||||||
|
srandmember(Key, Count) when is_binary(Key), is_integer(Count), Count > 0 ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
[];
|
||||||
|
[#kv{val = S, type = set}] ->
|
||||||
|
Size = sets:size(S),
|
||||||
|
L = sets:to_list(S),
|
||||||
|
case Size =< Count of
|
||||||
|
true ->
|
||||||
|
L;
|
||||||
|
false ->
|
||||||
|
lists:sublist(L, rand:uniform(Size - Count + 1), Count)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 被成功移除的元素的数量,不包括被忽略的元素
|
||||||
|
-spec srem(Key :: binary(), Members :: list()) -> Num :: integer() | {error, wrong_kind()}.
|
||||||
|
srem(Key, Members) when is_binary(Key), is_list(Members) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[KV = #kv{val = S, type = set}] ->
|
||||||
|
Size = sets:size(S),
|
||||||
|
S1 = lists:foldl(fun(E, S0) -> sets:del_element(E, S0) end, S, Members),
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = S1}, write),
|
||||||
|
Size - sets:size(S1);
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 命令返回给定集合的并集。不存在的集合 key 被视为空集
|
||||||
|
-spec sunion(Key :: binary(), Key2 :: binary()) -> Members :: list() | {error, wrong_kind()}.
|
||||||
|
sunion(Key1, Key2) when is_binary(Key1), is_binary(Key2) ->
|
||||||
|
case {mnesia:dirty_read(kv, Key1), mnesia:dirty_read(kv, Key2)} of
|
||||||
|
{[#kv{val = S1, type = set}], [#kv{val = S2, type = set}]} ->
|
||||||
|
sets:to_list(sets:union(S1, S2));
|
||||||
|
{[#kv{val = S1, type = set}], []} ->
|
||||||
|
sets:to_list(S1);
|
||||||
|
{[], [#kv{val = S2, type = set}]} ->
|
||||||
|
sets:to_list(S2);
|
||||||
|
{[], []} ->
|
||||||
|
[];
|
||||||
|
{_, _} ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% List 处理
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,列表中下标为指定索引值的元素。 如果指定索引值不在列表的区间范围内,返回 nil
|
||||||
|
%%-spec lindex(Key :: binary(), Idx :: integer()) -> redis_nil() | Member :: binary() | {error, wrong_kind()}.
|
||||||
|
lindex(Key, Idx) when is_binary(Key), is_integer(Idx) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
none;
|
||||||
|
[#kv{val = L, type = list}] ->
|
||||||
|
Idx1 = fix_pos(Idx, length(L)),
|
||||||
|
case Idx1 >= 1 andalso Idx1 =< length(L) of
|
||||||
|
true ->
|
||||||
|
lists:nth(fix_pos(Idx, length(L)), L);
|
||||||
|
false ->
|
||||||
|
none
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec llen(Key :: binary()) -> Size :: integer() | {error, wrong_kind()}.
|
||||||
|
llen(Key) when is_binary(Key) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[#kv{val = L, type = list}] ->
|
||||||
|
length(L);
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 列表的第一个元素。 当列表 key 不存在时,返回 nil
|
||||||
|
%%-spec lpop(Key :: binary()) -> redis_nil() | E :: binary() | {error, wrong_kind()}.
|
||||||
|
lpop(Key) when is_binary(Key) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
none;
|
||||||
|
[#kv{val = [], type = list}] ->
|
||||||
|
none;
|
||||||
|
[KV = #kv{val = [H | Tail], type = list}] ->
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = Tail}, write),
|
||||||
|
H;
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, E} ->
|
||||||
|
E;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 列表的最后一个元素,返回值为移除的元素
|
||||||
|
%%-spec rpop(Key :: binary()) -> redis_nil() | E :: binary() | {error, wrong_kind()}.
|
||||||
|
rpop(Key) when is_binary(Key) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
none;
|
||||||
|
[#kv{val = [], type = list}] ->
|
||||||
|
none;
|
||||||
|
[KV = #kv{val = L0, type = list}] ->
|
||||||
|
[H | Tail] = lists:reverse(L0),
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = lists:reverse(Tail)}, write),
|
||||||
|
H;
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, E} ->
|
||||||
|
E;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 命令将一个或多个值插入到列表头部。 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。 当 key 存在但不是列表类型时,返回一个错误
|
||||||
|
%% 执行 LPUSH 命令后,列表的长度
|
||||||
|
-spec lpush(Key :: binary(), Members :: list()) -> Num :: integer() | {error, wrong_kind()}.
|
||||||
|
lpush(Key, Members) when is_binary(Key), is_list(Members) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
ok = mnesia:write(kv, #kv{key = Key, val = Members, type = list}, write),
|
||||||
|
length(Members);
|
||||||
|
[KV = #kv{val = L0, type = list}] ->
|
||||||
|
L = Members ++ L0,
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = L, type = list}, write),
|
||||||
|
length(L);
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 将一个值插入到已存在的列表头部,列表不存在时操作无效
|
||||||
|
%% LPUSHX 命令执行之后,列表的长度
|
||||||
|
-spec lpushx(Key :: binary(), Members :: list()) -> Num :: integer() | {error, wrong_kind()}.
|
||||||
|
lpushx(Key, Members) when is_binary(Key), is_list(Members) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[KV = #kv{val = L0, type = list}] ->
|
||||||
|
L = Members ++ L0,
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = L}, write),
|
||||||
|
length(L);
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 将一个或多个值插入到列表的尾部(最右边)。
|
||||||
|
%% 如果列表不存在,一个空列表会被创建并执行 RPUSH 操作。 当列表存在但不是列表类型时,返回一个错误
|
||||||
|
%% 执行 RPUSH 操作后,列表的长度
|
||||||
|
-spec rpush(Key :: binary(), Members :: list()) -> Num :: integer() | {error, wrong_kind()}.
|
||||||
|
rpush(Key, Members) when is_binary(Key), is_list(Members) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
ok = mnesia:write(kv, #kv{key = Key, val = Members, type = list}, write),
|
||||||
|
length(Members);
|
||||||
|
[KV = #kv{val = L0, type = list}] ->
|
||||||
|
L = L0 ++ Members,
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = L}, write),
|
||||||
|
length(L);
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 将一个或多个值插入到列表的尾部(最右边)。
|
||||||
|
%% 当列表存在但不是列表类型时,返回一个错误
|
||||||
|
%% 执行 RPUSH 操作后,列表的长度
|
||||||
|
-spec rpushx(Key :: binary(), Members :: list()) -> Num :: integer() | {error, wrong_kind()}.
|
||||||
|
rpushx(Key, Members) when is_binary(Key), is_list(Members) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[KV = #kv{val = L0, type = list}] ->
|
||||||
|
L = L0 ++ Members,
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = L, type = list}, write),
|
||||||
|
length(L);
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。
|
||||||
|
%% 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
|
||||||
|
-spec lrange(Key :: binary(), Start :: integer(), End :: integer()) -> list() | {error, wrong_kind()}.
|
||||||
|
lrange(Key, Start, End) when is_binary(Key), is_integer(Start), is_integer(End) ->
|
||||||
|
case mnesia:dirty_read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
[];
|
||||||
|
[#kv{val = L, type = list}] ->
|
||||||
|
Len = length(L),
|
||||||
|
Start1 = fix_pos(Start, Len),
|
||||||
|
End1 = fix_pos(End, Len),
|
||||||
|
case Start1 =< End1 of
|
||||||
|
true ->
|
||||||
|
lists:sublist(L, Start1, End1 - Start1 + 1);
|
||||||
|
false ->
|
||||||
|
[]
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{error, ?WRONG_KIND}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 根据参数 COUNT 的值,移除列表中与参数 VALUE 相等的元素。
|
||||||
|
%% COUNT 的值可以是以下几种:
|
||||||
|
%% count > 0 : 从表头开始向表尾搜索,移除与 VALUE 相等的元素,数量为 COUNT 。
|
||||||
|
%% count < 0 : 从表尾开始向表头搜索,移除与 VALUE 相等的元素,数量为 COUNT 的绝对值。
|
||||||
|
%% count = 0 : 移除表中所有与 VALUE 相等的值
|
||||||
|
%% 被移除元素的数量。 列表不存在时返回 0
|
||||||
|
-spec lrem(Key :: binary(), Count :: integer(), Val :: binary()) -> Num :: integer() | {error, wrong_kind()}.
|
||||||
|
lrem(Key, Count, Val) when is_binary(Key), is_integer(Count) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[KV = #kv{val = L0, type = list}] ->
|
||||||
|
if
|
||||||
|
Count > 0 ->
|
||||||
|
L1 = lists:foldl(fun(_, L) -> lists:delete(Val, L) end, L0, lists:seq(1, Count)),
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = L1}, write),
|
||||||
|
length(L0) - length(L1);
|
||||||
|
Count =:= 0 ->
|
||||||
|
{DeletedVals, L1} = lists:partition(fun(E) -> E =:= Val end, L0),
|
||||||
|
case DeletedVals =/= [] of
|
||||||
|
true ->
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = L1}, write);
|
||||||
|
false ->
|
||||||
|
ok
|
||||||
|
end,
|
||||||
|
length(DeletedVals);
|
||||||
|
Count < 0 ->
|
||||||
|
L1 = lists:foldl(fun(_, L) ->
|
||||||
|
lists:delete(Val, L) end, lists:reverse(L0), lists:seq(1, abs(Count))),
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = lists:reverse(L1)}, write),
|
||||||
|
length(L0) - length(L1)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 当索引参数超出范围,或对一个空列表进行 LSET 时,返回一个错误
|
||||||
|
%% 操作成功返回 ok ,否则返回错误信息
|
||||||
|
-spec lset(Key :: binary(), Idx :: integer(), Val :: binary()) -> ok | {error, wrong_kind()}.
|
||||||
|
lset(Key, Idx, Val) when is_binary(Key), is_integer(Idx) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
mnesia:abort(?WRONG_KIND);
|
||||||
|
[KV = #kv{val = L0, type = list}] ->
|
||||||
|
case length(L0) < Idx of
|
||||||
|
true ->
|
||||||
|
mnesia:abort(<<"Index out of bounds">>);
|
||||||
|
false ->
|
||||||
|
L1 = lists_update(L0, Idx, Val),
|
||||||
|
mnesia:write(kv, KV#kv{val = L1}, write)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, ok} ->
|
||||||
|
<<"OK">>;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
|
||||||
|
%% 下标 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。
|
||||||
|
%% 以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推
|
||||||
|
%% 命令执行成功时,返回 ok
|
||||||
|
-spec ltrim(Key :: binary(), Start :: integer(), End :: integer()) -> ok | {error, wrong_kind()}.
|
||||||
|
ltrim(Key, Start, End) when is_binary(Key), is_integer(Start), is_integer(End) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
ok;
|
||||||
|
[KV = #kv{val = L0, type = list}] ->
|
||||||
|
Len = length(L0),
|
||||||
|
Start1 = fix_pos(Start, Len),
|
||||||
|
End1 = fix_pos(End, Len),
|
||||||
|
case Start1 =< End1 of
|
||||||
|
true ->
|
||||||
|
L1 = lists:sublist(L0, Start1, End1 - Start1 + 1),
|
||||||
|
mnesia:write(kv, KV#kv{val = L1}, write);
|
||||||
|
false ->
|
||||||
|
mnesia:write(kv, KV#kv{val = []}, write)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, ok} ->
|
||||||
|
<<"OK">>;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 命令用于在列表的元素前或者后插入元素。当指定元素不存在于列表中时,不执行任何操作。
|
||||||
|
%% 当列表不存在时,被视为空列表,不执行任何操作。
|
||||||
|
%% 如果 key 不是列表类型,返回一个错误
|
||||||
|
%% 如果命令执行成功,返回插入操作完成之后,列表的长度。 如果没有找到指定元素 ,返回 -1 。 如果 key 不存在或为空列表,返回 0
|
||||||
|
-spec linsert(Key :: binary(), Position :: binary(), Pivot :: binary(), Value :: binary()) ->
|
||||||
|
Num :: integer() |
|
||||||
|
{error, wrong_kind()}.
|
||||||
|
linsert(Key, Position, Pivot, Value) when is_binary(Key), is_binary(Position) ->
|
||||||
|
Fun = fun() ->
|
||||||
|
case mnesia:read(kv, Key) of
|
||||||
|
[] ->
|
||||||
|
0;
|
||||||
|
[KV = #kv{val = L0, type = list}] ->
|
||||||
|
L = case Position of
|
||||||
|
<<"BEFORE">> ->
|
||||||
|
lists_insert_before(L0, Pivot, Value);
|
||||||
|
<<"AFTER">> ->
|
||||||
|
lists_insert_after(L0, Pivot, Value)
|
||||||
|
end,
|
||||||
|
ok = mnesia:write(kv, KV#kv{val = L}, write),
|
||||||
|
case L0 =:= L of
|
||||||
|
true -> -1;
|
||||||
|
false -> length(L)
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
mnesia:abort(?WRONG_KIND)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
case mnesia:transaction(Fun) of
|
||||||
|
{atomic, N} ->
|
||||||
|
N;
|
||||||
|
{aborted, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% helper methods
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 计算位置
|
||||||
|
fix_pos(Pos, Len) when is_integer(Pos), is_integer(Len) ->
|
||||||
|
case Pos >= 1 of
|
||||||
|
true -> Pos;
|
||||||
|
false -> Len + Pos
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% 更新数组中的元素
|
||||||
|
-spec lists_update(L :: list(), N :: integer(), Val :: any()) -> L1 :: list().
|
||||||
|
lists_update(L, N, Val) when is_integer(N), N > 0, is_list(L) ->
|
||||||
|
case length(L) < N of
|
||||||
|
true -> L;
|
||||||
|
false -> lists_update0(L, N, Val)
|
||||||
|
end.
|
||||||
|
lists_update0([_ | Tail], 1, Val) ->
|
||||||
|
[Val | Tail];
|
||||||
|
lists_update0([Hd | Tail], N, Val) ->
|
||||||
|
[Hd | lists_update0(Tail, N - 1, Val)].
|
||||||
|
|
||||||
|
%% 在元素前插入
|
||||||
|
lists_insert_before(L, Pivot, Val) when is_list(L) ->
|
||||||
|
lists_insert_before0(L, Pivot, Val).
|
||||||
|
lists_insert_before0([], _Pivot, _Val) ->
|
||||||
|
[];
|
||||||
|
lists_insert_before0([Pivot | Tail], Pivot, Val) ->
|
||||||
|
[Val, Pivot | Tail];
|
||||||
|
lists_insert_before0([H | Tail], Pivot, Val) ->
|
||||||
|
[H | lists_insert_before0(Tail, Pivot, Val)].
|
||||||
|
|
||||||
|
%% 在元素后插入
|
||||||
|
lists_insert_after(L, Pivot, Val) when is_list(L) ->
|
||||||
|
lists_insert_after0(L, Pivot, Val).
|
||||||
|
lists_insert_after0([], _Pivot, _Val) ->
|
||||||
|
[];
|
||||||
|
lists_insert_after0([Pivot | Tail], Pivot, Val) ->
|
||||||
|
[Pivot, Val | Tail];
|
||||||
|
lists_insert_after0([H | Tail], Pivot, Val) ->
|
||||||
|
[H | lists_insert_after0(Tail, Pivot, Val)].
|
||||||
25
apps/iot/src/mocker/eval_test.erl
Normal file
25
apps/iot/src/mocker/eval_test.erl
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 17. 7月 2023 15:11
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(eval_test).
|
||||||
|
-author("aresei").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([test/0]).
|
||||||
|
|
||||||
|
test() ->
|
||||||
|
|
||||||
|
{ok, Content} = file:read_file("/tmp/test.erl"),
|
||||||
|
|
||||||
|
{ok, Tokens, _} = erl_scan:string(binary_to_list(Content)),
|
||||||
|
{ok, ExprList} = erl_parse:parse_exprs(Tokens),
|
||||||
|
|
||||||
|
{value, F, NewBindings} = erl_eval:exprs(ExprList, []),
|
||||||
|
F(#{name => <<"test">>}).
|
||||||
|
|
||||||
|
|
||||||
@ -1,432 +0,0 @@
|
|||||||
%%%-------------------------------------------------------------------
|
|
||||||
%%% @author aresei
|
|
||||||
%%% @copyright (C) 2023, <COMPANY>
|
|
||||||
%%% @doc
|
|
||||||
%%%
|
|
||||||
%%% @end
|
|
||||||
%%% Created : 14. 6月 2023 09:50
|
|
||||||
%%%-------------------------------------------------------------------
|
|
||||||
-module(host_mocker).
|
|
||||||
-author("aresei").
|
|
||||||
-include("iot.hrl").
|
|
||||||
|
|
||||||
-behaviour(gen_server).
|
|
||||||
|
|
||||||
%% API
|
|
||||||
-export([start_link/1, stop/0]).
|
|
||||||
-export([test/0, ping/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(TICKER_INTERVAL, 5000).
|
|
||||||
|
|
||||||
-record(state, {
|
|
||||||
conn_pid,
|
|
||||||
topic :: binary(),
|
|
||||||
pub_key = <<"-----BEGIN RSA PUBLIC KEY-----
|
|
||||||
MIIBCgKCAQEAp5Ky0wCtWb5sq7s4wq8K+BNAINtwYDCnwdJWrPX1/ueubu+VwLf4
|
|
||||||
EyNyghHCGrwntDbCPXmM8DYI99Mxfy2r8aaMgjwXdAzGpPkrzE6iGjLQmUHbSGBg
|
|
||||||
ZGVe3RgFhhIZC1c85VBtXqh9nrmgw9FuYlex0w9p1vODIw3IhmJDFAgP45reTO0l
|
|
||||||
yggZpfABi++R7HpF8uuQzc5GnFDGM3pESbGK6o7E5CYy5f+pKNAahJEfUf0onFsp
|
|
||||||
kneCGh/vBldcmFsXYQ3biAHmF8UPHf8NALU+FRnJx1dfkGTu7UudYvEUIfPO2cqW
|
|
||||||
2rvQ9BYPInerFw304hR2EyM1NJRa4idHtQIDAQAB
|
|
||||||
-----END RSA PUBLIC KEY-----">>,
|
|
||||||
pri_key = <<"-----BEGIN RSA PRIVATE KEY-----
|
|
||||||
MIIEowIBAAKCAQEAp5Ky0wCtWb5sq7s4wq8K+BNAINtwYDCnwdJWrPX1/ueubu+V
|
|
||||||
wLf4EyNyghHCGrwntDbCPXmM8DYI99Mxfy2r8aaMgjwXdAzGpPkrzE6iGjLQmUHb
|
|
||||||
SGBgZGVe3RgFhhIZC1c85VBtXqh9nrmgw9FuYlex0w9p1vODIw3IhmJDFAgP45re
|
|
||||||
TO0lyggZpfABi++R7HpF8uuQzc5GnFDGM3pESbGK6o7E5CYy5f+pKNAahJEfUf0o
|
|
||||||
nFspkneCGh/vBldcmFsXYQ3biAHmF8UPHf8NALU+FRnJx1dfkGTu7UudYvEUIfPO
|
|
||||||
2cqW2rvQ9BYPInerFw304hR2EyM1NJRa4idHtQIDAQABAoIBAQCbhGP9u0UmYrnL
|
|
||||||
7ydQVs9hR8xeMglq2/z0vla+kk5I0Y9rWWKrxpCugllFKWHxGennMGK4VtRcImnU
|
|
||||||
ReZL14EZ9a21ODuz8h9w/+aL5/Y9Ried9CakVv1eb03I9wA5WxZvFfloAGpgTRK4
|
|
||||||
eiIfWYCOOEDKViWt3bU4lRQi05LZRNmQg57CrDgX2UoxTMug7DK/FzVPmJ7azcgp
|
|
||||||
6AiBFLzZn/LwIgCENTvQ57X6UO7j/L6N7/Nz4Cz5w0om9Hv8pYxL01ML7xvFpVcn
|
|
||||||
o1UN3X1jIWOvS9njbJnfD600Hf+5I8ixFgxbGSsXEzDcbBy1/13pAxkexPx92ynQ
|
|
||||||
ktxNhnfBAoGBANUdNgCgv7LBlZYNivCf5WnJCO/0aRjunGu2xF3RTAffa/6HyjI8
|
|
||||||
yarVZiMAcCXlLjK7ao3t/RQoiTi3NqatpxL1+iQ1f5a/nupMC9jYy6I7rTUOl7k7
|
|
||||||
VNmX2LaCFEfixZbLz7yDwvUVfNDPc1QeihrqFSWJKAPcSGHfKuEzQIYZAoGBAMlL
|
|
||||||
ZL4hCmS/UpwQX1rTLHHZeVCCu0eL54DO+HWlRnrq0/7agGr+4m/HP0eAm8khnVFO
|
|
||||||
h3ySFabLA7pJo0H1+P1v5+un7l6wsZZp4yoXxD5cr9prorI+N40i0yqOIyxspo2a
|
|
||||||
1k71LJpeuv1ZltkTWXUIz/TxVu/iFj/m2wFaxun9AoGAQvBG6xGSxOoLOzOLxaLj
|
|
||||||
o0OS/BPQAxXHqgmhSjqYYAysVil+uCLh0TfwOeREVZLT3PmDMYtkJ7XHzDm3/8ih
|
|
||||||
ptH+POtU5RvRJZS3T+hgpdeKwxSPUY4yS5pnZoQbLK0tFP11haf5T5PtPYU7m1tw
|
|
||||||
U53dAIpBOF0zmxJG3K+Ff9kCgYAGv31IFml3ySYmzzGzJMMnqee0OD24/0qqecXA
|
|
||||||
g+Lh+f9TWtXVQGgs4RwQ9JHEY1kXwa8vEOKi7clZNGDBtFI9hMPclYubJwc9CJ2x
|
|
||||||
6owMnyTSCKuyl/1awOEdWxh4w8etlZQ7n2J4ZlaUaa1x54EnOD1oc7K7ZfPi/oU2
|
|
||||||
/WkPrQKBgFvuP/xwkGldkvxVEP6EN6GDuP/BzumVYMSxku0MC4AgwgAwW7pgTynq
|
|
||||||
yH0hr6SJTlM2zEFQszmKdsg0fRXO6wrqN2mt8dZKEd51rp04hM47yM48stlE9Lmt
|
|
||||||
/NZsDqOnUgW07xFPQI3nfS5dG9dH9Qsk+OxuIt9YifKriKDpTHlc
|
|
||||||
-----END RSA PRIVATE KEY-----">>,
|
|
||||||
|
|
||||||
aes = <<>>
|
|
||||||
}).
|
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% API
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
test() ->
|
|
||||||
catch stop(),
|
|
||||||
host_mocker:start_link(<<"123123123123123">>).
|
|
||||||
|
|
||||||
stop() ->
|
|
||||||
gen_server:stop(?MODULE).
|
|
||||||
|
|
||||||
ping() ->
|
|
||||||
Pid = whereis(?MODULE),
|
|
||||||
erlang:start_timer(0, Pid, feedback_step_ticker).
|
|
||||||
|
|
||||||
%% @doc Spawns the server and registers the local name (unique)
|
|
||||||
-spec(start_link(UUID :: binary()) ->
|
|
||||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
|
||||||
start_link(UUID) when is_binary(UUID) ->
|
|
||||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [UUID], []).
|
|
||||||
|
|
||||||
%%%===================================================================
|
|
||||||
%%% 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([UUID]) ->
|
|
||||||
%% 建立到emqx服务器的连接
|
|
||||||
Opts = iot_config:emqt_opts(<<"host-subscriber", UUID/binary>>),
|
|
||||||
{ok, ConnPid} = emqtt:start_link(Opts),
|
|
||||||
%% 监听和host相关的全部事件
|
|
||||||
{ok, _} = emqtt:connect(ConnPid),
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] connect success, pid: ~p", [ConnPid]),
|
|
||||||
SubscribeResult = emqtt:subscribe(ConnPid, [
|
|
||||||
{<<"host/downstream/", UUID/binary>>, 1}
|
|
||||||
]),
|
|
||||||
lager:debug("[iot_mqtt_sys_subscriber] subscribe result is: ~p", [SubscribeResult]),
|
|
||||||
|
|
||||||
%% 建立到主机的握手
|
|
||||||
Message = #{
|
|
||||||
<<"method">> => <<"register">>,
|
|
||||||
<<"params">> => #{
|
|
||||||
<<"uuid">> => UUID
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Req = jiffy:encode(Message, [force_utf8]),
|
|
||||||
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(<<"system/upstream">>, Req, 1),
|
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[host_mocker] send register success, packet_id: ~p", [PacketId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[host_mocker] send register failed, reason: ~p", [Reason])
|
|
||||||
end,
|
|
||||||
|
|
||||||
{ok, #state{conn_pid = ConnPid, topic = <<"host/upstream/", UUID/binary>>}}.
|
|
||||||
|
|
||||||
%% @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(_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("[host_mocker] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
|
||||||
{stop, disconnected, State};
|
|
||||||
%% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行
|
|
||||||
handle_info({publish, #{payload := Payload, qos := Qos, topic := FromTopic}},
|
|
||||||
State = #state{topic = Topic, pri_key = PrivateKey, aes = Aes0}) ->
|
|
||||||
|
|
||||||
lager:debug("[host_mocker] Recv a publish packet: ~p, qos: ~p, from topic: ~p", [Payload, Qos, FromTopic]),
|
|
||||||
case Payload of
|
|
||||||
<<0:8, Reply/binary>> ->
|
|
||||||
Json = jiffy:decode(Reply, [return_maps]),
|
|
||||||
lager:debug("[host_mocker] get reply: ~p", [Json]),
|
|
||||||
case Json of
|
|
||||||
#{<<"code">> := 1, <<"message">> := <<"ok">>} ->
|
|
||||||
self() ! create_session;
|
|
||||||
Info ->
|
|
||||||
Info
|
|
||||||
end,
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
%% 10是通过rsa加密的数据,里面保存了aes;后续的所有通讯都是通过aes加密的
|
|
||||||
<<10:8, Command0/binary>> ->
|
|
||||||
Command = iot_cipher_rsa:decode(Command0, PrivateKey),
|
|
||||||
case jiffy:decode(Command, [return_maps]) of
|
|
||||||
#{<<"aes">> := Aes, <<"a">> := true} ->
|
|
||||||
%% 启动周期ping
|
|
||||||
erlang:start_timer(?TICKER_INTERVAL, self(), ping_ticker),
|
|
||||||
%% 数据收集
|
|
||||||
erlang:start_timer(?TICKER_INTERVAL + 1000, self(), data_ticker),
|
|
||||||
%% 触发inform上传
|
|
||||||
erlang:start_timer(?TICKER_INTERVAL, self(), inform_ticker),
|
|
||||||
erlang:start_timer(?TICKER_INTERVAL, self(), feedback_ticker),
|
|
||||||
erlang:start_timer(?TICKER_INTERVAL, self(), feedback_step_ticker),
|
|
||||||
|
|
||||||
{noreply, State#state{aes = Aes}};
|
|
||||||
_ ->
|
|
||||||
lager:debug("[host_mocker] auth failed")
|
|
||||||
end;
|
|
||||||
|
|
||||||
%% t = 8采用明文消息
|
|
||||||
<<8:8, Command/binary>> ->
|
|
||||||
case jiffy:decode(Command, [return_maps]) of
|
|
||||||
#{<<"auth">> := true, <<"reply">> := #{<<"topic">> := ReplyTopic, <<"assoc">> := Assoc}} ->
|
|
||||||
Msg = jiffy:encode(#{
|
|
||||||
<<"code">> => 1,
|
|
||||||
<<"message">> => "",
|
|
||||||
<<"assoc">> => Assoc
|
|
||||||
}, [force_utf8]),
|
|
||||||
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(ReplyTopic, Msg, 1),
|
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[host_mocker] send reply success, packet_id: ~p", [PacketId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[host_mocker] send reply failed, reason: ~p", [Reason])
|
|
||||||
end,
|
|
||||||
|
|
||||||
self() ! create_session,
|
|
||||||
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
#{<<"auth">> := false, <<"reply">> := #{<<"topic">> := Topic, <<"assoc">> := Assoc}} ->
|
|
||||||
Msg = jiffy:encode(#{
|
|
||||||
<<"code">> => 1,
|
|
||||||
<<"message">> => "",
|
|
||||||
<<"assoc">> => Assoc
|
|
||||||
}, [force_utf8]),
|
|
||||||
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(Topic, Msg, 1),
|
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[host_mocker] send reply success, packet_id: ~p", [PacketId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[host_mocker] send reply failed, reason: ~p", [Reason])
|
|
||||||
end,
|
|
||||||
|
|
||||||
{noreply, State#state{aes = <<>>}};
|
|
||||||
#{<<"auth">> := false} ->
|
|
||||||
self() ! create_session,
|
|
||||||
|
|
||||||
{noreply, State#state{aes = <<>>}}
|
|
||||||
end;
|
|
||||||
|
|
||||||
%% 处理其他指令
|
|
||||||
<<Type:8, Command0/binary>> ->
|
|
||||||
Command = iot_cipher_aes:decrypt(Aes0, Command0),
|
|
||||||
CommandJson = jiffy:decode(Command, [return_maps]),
|
|
||||||
|
|
||||||
lager:debug("[host_mocker] get command: ~p, json: ~p, type: ~p", [Command, CommandJson, Type]),
|
|
||||||
NState = handle_command(Type, CommandJson, State),
|
|
||||||
{noreply, NState}
|
|
||||||
end;
|
|
||||||
|
|
||||||
handle_info({puback, Packet = #{packet_id := _PacketId}}, State = #state{}) ->
|
|
||||||
lager:debug("[iot_mqtt_subscriber] receive puback packet: ~p", [Packet]),
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
%% 建立到iot的会话
|
|
||||||
handle_info(create_session, State = #state{topic = Topic, pub_key = PubKey}) ->
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(Topic, <<?METHOD_CREATE_SESSION:8, PubKey/binary>>, 1),
|
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[host_mocker] send create_session success, packet_id: ~p", [PacketId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[host_mocker] send create_session failed, reason: ~p", [Reason])
|
|
||||||
end,
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
%% 周期数据上传逻辑
|
|
||||||
handle_info({timeout, _, data_ticker}, State = #state{aes = Aes, topic = Topic}) ->
|
|
||||||
InfoList = [
|
|
||||||
#{
|
|
||||||
<<"service_name">> => <<"shuibiao">>,
|
|
||||||
<<"at">> => iot_util:timestamp(),
|
|
||||||
<<"fields">> => [
|
|
||||||
#{<<"used">> => rand:uniform(2048 * 2)}
|
|
||||||
],
|
|
||||||
<<"tags">> => #{}
|
|
||||||
},
|
|
||||||
#{
|
|
||||||
<<"service_name">> => <<"shuibiao:123">>,
|
|
||||||
<<"at">> => iot_util:timestamp(),
|
|
||||||
<<"fields">> => [
|
|
||||||
#{<<"used">> => rand:uniform(2048 * 2)}
|
|
||||||
],
|
|
||||||
<<"tags">> => #{}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
Info = jiffy:encode(InfoList),
|
|
||||||
EncInfo = iot_cipher_aes:encrypt(Aes, Info),
|
|
||||||
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(Topic, <<?METHOD_DATA:8, EncInfo/binary>>, 1),
|
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[host_mocker] send data success, packet_id: ~p", [PacketId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[host_mocker] send data failed, reason: ~p", [Reason])
|
|
||||||
end,
|
|
||||||
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
%% 上传微服务的inform信息
|
|
||||||
handle_info({timeout, _, inform_ticker}, State = #state{aes = Aes, topic = Topic}) ->
|
|
||||||
Info = jiffy:encode(#{
|
|
||||||
<<"at">> => iot_util:current_time(),
|
|
||||||
<<"services">> => [
|
|
||||||
#{
|
|
||||||
<<"props">> => <<"1:2:3">>,
|
|
||||||
<<"name">> => <<"测试微服务"/utf8>>,
|
|
||||||
<<"version">> => <<"V1.0">>,
|
|
||||||
<<"version_copy">> => <<"CopyV1.0">>,
|
|
||||||
<<"status">> => 1
|
|
||||||
},
|
|
||||||
|
|
||||||
#{
|
|
||||||
<<"props">> => <<"1:2:3">>,
|
|
||||||
<<"name">> => <<"水表"/utf8>>,
|
|
||||||
<<"version">> => <<"V1.0">>,
|
|
||||||
<<"version_copy">> => <<"CopyV1.0">>,
|
|
||||||
<<"status">> => 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, [force_utf8]),
|
|
||||||
|
|
||||||
EncInfo = iot_cipher_aes:encrypt(Aes, Info),
|
|
||||||
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(Topic, <<?METHOD_INFORM:8, EncInfo/binary>>, 1),
|
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[host_mocker] send inform success, packet_id: ~p", [PacketId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[host_mocker] send inform failed, reason: ~p", [Reason])
|
|
||||||
end,
|
|
||||||
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
%% 上传微服务的feedback_result信息
|
|
||||||
handle_info({timeout, _, feedback_ticker}, State = #state{aes = Aes, topic = Topic}) ->
|
|
||||||
Info = jiffy:encode(#{
|
|
||||||
<<"task_id">> => 1,
|
|
||||||
<<"type">> => 2,
|
|
||||||
<<"code">> => 200,
|
|
||||||
<<"reason">> => <<"ok">>,
|
|
||||||
<<"error">> => <<"">>,
|
|
||||||
<<"time">> => iot_util:current_time()
|
|
||||||
}, [force_utf8]),
|
|
||||||
EncInfo = iot_cipher_aes:encrypt(Aes, Info),
|
|
||||||
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(Topic, <<?METHOD_FEEDBACK_RESULT:8, EncInfo/binary>>, 1),
|
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[host_mocker] send feedback success, packet_id: ~p", [PacketId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[host_mocker] send feedback failed, reason: ~p", [Reason])
|
|
||||||
end,
|
|
||||||
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
%% 上传微服务的feedback_result信息
|
|
||||||
handle_info({timeout, _, feedback_step_ticker}, State = #state{aes = Aes, topic = Topic}) ->
|
|
||||||
Info = jiffy:encode(#{
|
|
||||||
<<"task_id">> => 1,
|
|
||||||
<<"code">> => 2
|
|
||||||
}, [force_utf8]),
|
|
||||||
EncInfo = iot_cipher_aes:encrypt(Aes, Info),
|
|
||||||
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(Topic, <<?METHOD_FEEDBACK_STEP:8, EncInfo/binary>>, 1),
|
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[host_mocker] send feedback step success, packet_id: ~p", [PacketId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[host_mocker] send feedback step failed, reason: ~p", [Reason])
|
|
||||||
end,
|
|
||||||
|
|
||||||
{noreply, State};
|
|
||||||
|
|
||||||
%% 周期行的ping逻辑
|
|
||||||
handle_info({timeout, _, ping_ticker}, State = #state{aes = Aes, topic = Topic}) ->
|
|
||||||
Metric = jiffy:encode(#{
|
|
||||||
<<"cpu_load">> => rand:uniform(100),
|
|
||||||
<<"cpu_temperature">> => rand:uniform(100),
|
|
||||||
<<"memory">> => #{
|
|
||||||
<<"used">> => rand:uniform(2048 * 2),
|
|
||||||
<<"total">> => 2048 * 2
|
|
||||||
},
|
|
||||||
<<"disk">> => #{
|
|
||||||
<<"used">> => rand:uniform(2048 * 2),
|
|
||||||
<<"total">> => 2048 * 2
|
|
||||||
},
|
|
||||||
|
|
||||||
<<"interfaces">> => [
|
|
||||||
#{
|
|
||||||
<<"name">> => <<"WiFi无线网络"/utf8>>,
|
|
||||||
<<"detail">> => <<"Managed">>,
|
|
||||||
<<"status">> => 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, [force_utf8]),
|
|
||||||
|
|
||||||
Data = iot_cipher_aes:encrypt(Aes, Metric),
|
|
||||||
|
|
||||||
{ok, Ref} = iot_mqtt_publisher:publish(Topic, <<?METHOD_PING:8, Data/binary>>, 1),
|
|
||||||
receive
|
|
||||||
{ok, Ref, PacketId} ->
|
|
||||||
lager:debug("[host_mocker] send ping success, packet_id: ~p", [PacketId]);
|
|
||||||
{error, Reason} ->
|
|
||||||
lager:debug("[host_mocker] send ping failed, reason: ~p", [Reason])
|
|
||||||
end,
|
|
||||||
|
|
||||||
{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
|
|
||||||
%%%===================================================================
|
|
||||||
|
|
||||||
handle_command(Type, Info, State) ->
|
|
||||||
lager:debug("[host_mocker] command type: ~p, is: ~p", [Type, Info]),
|
|
||||||
State.
|
|
||||||
119
apps/iot/src/mocker/iot_endpoint_mocker.erl
Normal file
119
apps/iot/src/mocker/iot_endpoint_mocker.erl
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 05. 7月 2023 23:22
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(iot_endpoint_mocker).
|
||||||
|
-author("aresei").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/0, test/0]).
|
||||||
|
|
||||||
|
%% 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, {
|
||||||
|
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
test() ->
|
||||||
|
{ok, Pid} = start_link(),
|
||||||
|
Data = #{
|
||||||
|
<<"name">> => <<"anlicheng">>
|
||||||
|
},
|
||||||
|
gen_server:cast(Pid, {forward, Data}),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
|
||||||
|
%% @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(?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, #state{}}.
|
||||||
|
|
||||||
|
%% @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({forward, Data}, State = #state{}) ->
|
||||||
|
Name = <<"zhongguodianli">>,
|
||||||
|
Pid = iot_endpoint:get_pid(Name),
|
||||||
|
Body = jiffy:encode(Data),
|
||||||
|
|
||||||
|
iot_endpoint:forward(Pid, <<"abc123">>, Body),
|
||||||
|
|
||||||
|
erlang:start_timer(5000, self(), {resend, Pid, Body, 1}),
|
||||||
|
|
||||||
|
{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({timeout, _, {resend, Pid, Body, Id}}, State = #state{}) ->
|
||||||
|
iot_endpoint:forward(Pid, <<"abc123">>, jiffy:encode(#{<<"id">> => Id})),
|
||||||
|
erlang:start_timer(5000, self(), {resend, Pid, Body, Id + 1}),
|
||||||
|
|
||||||
|
{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
|
||||||
|
%%%===================================================================
|
||||||
@ -13,6 +13,48 @@
|
|||||||
%% API
|
%% API
|
||||||
-export([rsa_encode/1]).
|
-export([rsa_encode/1]).
|
||||||
-export([insert_services/1]).
|
-export([insert_services/1]).
|
||||||
|
-export([insert_endpoints/0]).
|
||||||
|
|
||||||
|
insert_endpoints() ->
|
||||||
|
Mapper0 = "fun(LocationCode, Data) ->
|
||||||
|
Json = jiffy:decode(Data, [return_maps]),
|
||||||
|
Bin = jiffy:encode(Json#{<<\"location_code\">> => LocationCode}, [force_utf8]),
|
||||||
|
iolist_to_binary(Bin)
|
||||||
|
end.",
|
||||||
|
|
||||||
|
Mapper = list_to_binary(Mapper0),
|
||||||
|
{ok, F} = iot_util:parse_mapper(Mapper),
|
||||||
|
mnesia_endpoint:insert(#endpoint{
|
||||||
|
name = <<"zhongguodianli">>,
|
||||||
|
title = <<"中国电力"/utf8>>,
|
||||||
|
matcher = <<"test12*">>,
|
||||||
|
mapper = Mapper,
|
||||||
|
mapper_fun = F,
|
||||||
|
config = #{<<"protocol">> => <<"http">>, <<"args">> => #{<<"url">> => <<"http://localhost:18080/test/receiver">>}},
|
||||||
|
created_at = iot_util:timestamp_of_seconds()
|
||||||
|
}),
|
||||||
|
|
||||||
|
mnesia_endpoint:insert(#endpoint{
|
||||||
|
name = <<"mytest">>,
|
||||||
|
title = <<"测试数据"/utf8>>,
|
||||||
|
matcher = <<"test*">>,
|
||||||
|
mapper = Mapper,
|
||||||
|
mapper_fun = F,
|
||||||
|
config = #{
|
||||||
|
<<"protocol">> => <<"mqtt">>,
|
||||||
|
<<"args">> => #{
|
||||||
|
<<"host">> => <<"39.98.184.67">>,
|
||||||
|
<<"port">> => 1883,
|
||||||
|
<<"username">> => <<"test">>,
|
||||||
|
<<"password">> => <<"test1234">>,
|
||||||
|
<<"topic">> => <<"CET/NX/${location_code}/upload">>,
|
||||||
|
<<"qos">> => 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created_at = iot_util:timestamp_of_seconds()
|
||||||
|
}),
|
||||||
|
|
||||||
|
{Mapper, F}.
|
||||||
|
|
||||||
insert_services(Num) ->
|
insert_services(Num) ->
|
||||||
lists:foreach(fun(Id) ->
|
lists:foreach(fun(Id) ->
|
||||||
|
|||||||
124
apps/iot/src/postman/http_postman.erl
Normal file
124
apps/iot/src/postman/http_postman.erl
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 04. 7月 2023 15:41
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(http_postman).
|
||||||
|
-author("aresei").
|
||||||
|
-include("iot.hrl").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/4]).
|
||||||
|
|
||||||
|
%% 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, {
|
||||||
|
parent_pid :: pid(),
|
||||||
|
url :: binary(),
|
||||||
|
pool_name :: atom(),
|
||||||
|
worker_pool_pid :: pid()
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
%% @doc Spawns the server and registers the local name (unique)
|
||||||
|
-spec(start_link(ParentPid :: pid(), Url :: binary(), PoolName :: atom(), PoolSize :: integer()) ->
|
||||||
|
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link(ParentPid, Url, PoolName, PoolSize) when is_pid(ParentPid), is_binary(Url), is_atom(PoolName), is_integer(PoolSize) ->
|
||||||
|
gen_server:start_link(?MODULE, [ParentPid, Url, PoolName, PoolSize], []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% 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([ParentPid, Url, PoolName, PoolSize]) ->
|
||||||
|
ok = hackney_pool:start_pool(PoolName, [{timeout, 150000}, {max_connections, PoolSize}]),
|
||||||
|
%% 启动工作的线程池
|
||||||
|
{ok, WorkerPoolPid} = poolboy:start_link([{size, PoolSize}, {max_overflow, PoolSize}, {worker_module, http_postman_worker}], []),
|
||||||
|
{ok, #state{parent_pid = ParentPid, url = Url, pool_name = PoolName, worker_pool_pid = WorkerPoolPid}}.
|
||||||
|
|
||||||
|
%% @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(_Info, 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(stop, State = #state{pool_name = PoolName, worker_pool_pid = WorkerPoolPid}) ->
|
||||||
|
hackney_pool:stop_pool(PoolName),
|
||||||
|
poolboy:stop(WorkerPoolPid),
|
||||||
|
|
||||||
|
{stop, normal, State};
|
||||||
|
|
||||||
|
handle_info({post, #north_data{body = Body, ref = Ref}}, State = #state{parent_pid = ParentPid, url = Url, pool_name = PoolName, worker_pool_pid = WorkerPoolPid}) ->
|
||||||
|
poolboy:transaction(WorkerPoolPid, fun(Pid) ->
|
||||||
|
case http_postman_worker:post(Pid, Url, Body, PoolName) of
|
||||||
|
ok ->
|
||||||
|
ParentPid ! {ack, Ref};
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:debug("[http_postman] post url: ~p, body: ~p, get error: ~p", [Url, Body, Reason])
|
||||||
|
end
|
||||||
|
end),
|
||||||
|
{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{pool_name = PoolName, worker_pool_pid = WorkerPoolPid}) ->
|
||||||
|
lager:debug("[http_postman] terminate with reasson: ~p", [Reason]),
|
||||||
|
catch hackney_pool:stop_pool(PoolName),
|
||||||
|
catch poolboy:stop(WorkerPoolPid),
|
||||||
|
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
|
||||||
|
%%%===================================================================
|
||||||
119
apps/iot/src/postman/http_postman_worker.erl
Normal file
119
apps/iot/src/postman/http_postman_worker.erl
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author aresei
|
||||||
|
%%% @copyright (C) 2023, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 06. 7月 2023 16:23
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(http_postman_worker).
|
||||||
|
-author("aresei").
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/1]).
|
||||||
|
-export([post/4]).
|
||||||
|
|
||||||
|
%% 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, {
|
||||||
|
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% API
|
||||||
|
%%%===================================================================
|
||||||
|
|
||||||
|
-spec post(Pid :: pid(), Url :: binary(), Body :: binary(), PoolName :: atom()) -> ok | {error, Reason :: any()}.
|
||||||
|
post(Pid, Url, Body, PoolName) when is_pid(Pid), is_binary(Url), is_binary(Body), is_atom(PoolName) ->
|
||||||
|
gen_server:call(Pid, {post, Url, Body, PoolName}).
|
||||||
|
|
||||||
|
%% @doc Spawns the server and registers the local name (unique)
|
||||||
|
-spec(start_link(Args :: proplists:proplist()) ->
|
||||||
|
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
|
start_link(Args) when is_list(Args) ->
|
||||||
|
gen_server:start_link(?MODULE, [Args], []).
|
||||||
|
|
||||||
|
%%%===================================================================
|
||||||
|
%%% 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, #state{}}.
|
||||||
|
|
||||||
|
%% @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({post, Url, Body, PoolName}, _From, State = #state{}) ->
|
||||||
|
Headers = [
|
||||||
|
{<<"content-type">>, <<"application/json">>}
|
||||||
|
],
|
||||||
|
case hackney:request(post, Url, Headers, Body, [{pool, PoolName}]) of
|
||||||
|
{ok, 200, _, ClientRef} ->
|
||||||
|
{ok, RespBody} = hackney:body(ClientRef),
|
||||||
|
lager:debug("[iot_http_client] url: ~p, response is: ~p", [Url, RespBody]),
|
||||||
|
{reply, ok, State};
|
||||||
|
{ok, HttpCode, _, ClientRef} ->
|
||||||
|
{ok, RespBody} = hackney:body(ClientRef),
|
||||||
|
lager:debug("[iot_http_client] url: ~p, http_code: ~p, response is: ~p", [Url, HttpCode, RespBody]),
|
||||||
|
{reply, {error, {http_code, HttpCode}}, State};
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:warning("[iot_http_client] url: ~p, get error: ~p", [Url, Reason]),
|
||||||
|
{reply, {error, Reason}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% @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(_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
|
||||||
|
%%%===================================================================
|
||||||
@ -6,22 +6,23 @@
|
|||||||
%%% @end
|
%%% @end
|
||||||
%%% Created : 12. 3月 2023 21:27
|
%%% Created : 12. 3月 2023 21:27
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(iot_mqtt_publisher).
|
-module(mqtt_postman).
|
||||||
-author("aresei").
|
-author("aresei").
|
||||||
-include("iot.hrl").
|
-include("iot.hrl").
|
||||||
|
|
||||||
-behaviour(gen_server).
|
-behaviour(gen_server).
|
||||||
|
|
||||||
%% API
|
%% API
|
||||||
-export([start_link/0, publish/3]).
|
-export([start_link/4]).
|
||||||
|
|
||||||
%% gen_server callbacks
|
%% gen_server callbacks
|
||||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||||
|
|
||||||
-define(SERVER, ?MODULE).
|
|
||||||
|
|
||||||
-record(state, {
|
-record(state, {
|
||||||
|
parent_pid :: pid(),
|
||||||
conn_pid :: pid(),
|
conn_pid :: pid(),
|
||||||
|
topic :: binary(),
|
||||||
|
qos = 0 :: integer(),
|
||||||
inflight = #{}
|
inflight = #{}
|
||||||
}).
|
}).
|
||||||
|
|
||||||
@ -29,15 +30,11 @@
|
|||||||
%%% API
|
%%% API
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|
||||||
-spec publish(binary(), binary(), integer()) -> {ok, Ref :: reference()} | {error, term()}.
|
|
||||||
publish(Topic, Message, Qos) when is_binary(Topic), is_binary(Message), is_integer(Qos) ->
|
|
||||||
gen_server:call(?MODULE, {publish, self(), Topic, Message, Qos}).
|
|
||||||
|
|
||||||
%% @doc Spawns the server and registers the local name (unique)
|
%% @doc Spawns the server and registers the local name (unique)
|
||||||
-spec(start_link() ->
|
-spec(start_link(ParentPid :: pid(), Opts :: list(), Topic :: binary(), Qos :: integer()) ->
|
||||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||||
start_link() ->
|
start_link(ParentPid, Opts, Topic, Qos) when is_pid(ParentPid), is_list(Opts), is_binary(Topic), Qos == 0; Qos == 1; Qos == 2 ->
|
||||||
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
gen_server:start_link(?MODULE, [ParentPid, Opts, Topic, Qos], []).
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% gen_server callbacks
|
%%% gen_server callbacks
|
||||||
@ -48,14 +45,14 @@ start_link() ->
|
|||||||
-spec(init(Args :: term()) ->
|
-spec(init(Args :: term()) ->
|
||||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||||
{stop, Reason :: term()} | ignore).
|
{stop, Reason :: term()} | ignore).
|
||||||
init([]) ->
|
init([ParentPid, Opts, Topic, Qos]) ->
|
||||||
%% 建立到emqx服务器的连接
|
Opts1 = [{owner, self()} | Opts],
|
||||||
Opts = iot_config:emqt_opts(<<"publisher">>),
|
{ok, ConnPid} = emqtt:start_link(Opts1),
|
||||||
{ok, ConnPid} = emqtt:start_link(Opts),
|
lager:debug("[mqtt_postman] start connect, options: ~p", [Opts1]),
|
||||||
lager:debug("[iot_mqtt_publisher] connect success, pid: ~p", [ConnPid]),
|
|
||||||
{ok, _} = emqtt:connect(ConnPid),
|
{ok, _} = emqtt:connect(ConnPid),
|
||||||
|
lager:debug("[mqtt_postman] connect success, pid: ~p", [ConnPid]),
|
||||||
|
|
||||||
{ok, #state{conn_pid = ConnPid}}.
|
{ok, #state{parent_pid = ParentPid, conn_pid = ConnPid, topic = Topic, qos = Qos}}.
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
%% @doc Handling call messages
|
%% @doc Handling call messages
|
||||||
@ -67,16 +64,8 @@ init([]) ->
|
|||||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
{stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
|
||||||
{stop, Reason :: term(), NewState :: #state{}}).
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
handle_call({publish, ReceiverPid, Topic, Message, Qos}, _From, State = #state{conn_pid = ConnPid, inflight = InFlight}) ->
|
handle_call(_Info, _From, State) ->
|
||||||
%% [{qos, Qos}, {retain, true}]
|
{reply, ok, State}.
|
||||||
lager:debug("[iot_mqtt_publisher] will publish message: ~p, topic: ~p, qos: ~p", [Message, Topic, Qos]),
|
|
||||||
case emqtt:publish(ConnPid, Topic, #{}, Message, [{qos, Qos}]) of
|
|
||||||
{ok, PacketId} ->
|
|
||||||
Ref = make_ref(),
|
|
||||||
{reply, {ok, Ref}, State#state{inflight = maps:put(PacketId, {ReceiverPid, Ref, Message}, InFlight)}};
|
|
||||||
{error, Reason} ->
|
|
||||||
{reply, {error, Reason}, State}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
%% @doc Handling cast messages
|
%% @doc Handling cast messages
|
||||||
@ -84,7 +73,7 @@ handle_call({publish, ReceiverPid, Topic, Message, Qos}, _From, State = #state{c
|
|||||||
{noreply, NewState :: #state{}} |
|
{noreply, NewState :: #state{}} |
|
||||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
{stop, Reason :: term(), NewState :: #state{}}).
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
handle_cast(_Request, State = #state{}) ->
|
handle_cast(_Info, State) ->
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
@ -94,24 +83,42 @@ handle_cast(_Request, State = #state{}) ->
|
|||||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||||
{stop, Reason :: term(), NewState :: #state{}}).
|
{stop, Reason :: term(), NewState :: #state{}}).
|
||||||
handle_info({disconnected, ReasonCode, Properties}, State = #state{}) ->
|
handle_info({disconnected, ReasonCode, Properties}, State = #state{}) ->
|
||||||
lager:debug("[iot_mqtt_publisher] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
lager:debug("[mqtt_postman] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]),
|
||||||
{stop, disconnected, State};
|
{stop, disconnected, State};
|
||||||
handle_info({publish, Message = #{packet_id := _PacketId, payload := Payload}}, State = #state{conn_pid = _ConnPid}) ->
|
handle_info({publish, Message = #{packet_id := _PacketId, payload := Payload}}, State = #state{conn_pid = _ConnPid}) ->
|
||||||
lager:debug("[iot_mqtt_publisher] Recv a publish packet: ~p, payload: ~p", [Message, Payload]),
|
lager:debug("[mqtt_postman] Recv a publish packet: ~p, payload: ~p", [Message, Payload]),
|
||||||
{noreply, State};
|
{noreply, State};
|
||||||
handle_info({puback, Packet = #{packet_id := PacketId}}, State = #state{inflight = Inflight}) ->
|
handle_info({puback, Packet = #{packet_id := PacketId}}, State = #state{parent_pid = ParentPid, inflight = Inflight}) ->
|
||||||
case maps:take(PacketId, Inflight) of
|
case maps:take(PacketId, Inflight) of
|
||||||
{{ReceiverPid, Ref, Message}, RestInflight} ->
|
{{Ref, Message}, RestInflight} ->
|
||||||
lager:debug("[iot_mqtt_publisher] receive puback packet: ~p, assoc message: ~p", [Packet, Message]),
|
lager:debug("[mqtt_postman] receive puback packet: ~p, assoc message: ~p", [Packet, Message]),
|
||||||
ReceiverPid ! {ok, Ref, PacketId},
|
ParentPid ! {ack, Ref},
|
||||||
{noreply, State#state{inflight = RestInflight}};
|
{noreply, State#state{inflight = RestInflight}};
|
||||||
error ->
|
error ->
|
||||||
lager:warning("[iot_mqtt_publisher] receive unknown puback packet: ~p", [Packet]),
|
lager:warning("[mqtt_postman] receive unknown puback packet: ~p", [Packet]),
|
||||||
{noreply, State}
|
{noreply, State}
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
%% 转发信息
|
||||||
|
handle_info({post, #north_data{ref = Ref, location_code = LocationCode, body = Message}}, State = #state{parent_pid = ParentPid, conn_pid = ConnPid, inflight = InFlight, topic = Topic0, qos = Qos}) ->
|
||||||
|
Topic = re:replace(Topic0, <<"\\${location_code}">>, LocationCode, [global, {return, binary}]),
|
||||||
|
lager:debug("[mqtt_postman] will publish topic: ~p, message: ~p, qos: ~p", [Topic, Message, Qos]),
|
||||||
|
case emqtt:publish(ConnPid, Topic, #{}, Message, [{qos, Qos}, {retain, true}]) of
|
||||||
|
ok ->
|
||||||
|
ParentPid ! {ack, Ref},
|
||||||
|
{noreply, State};
|
||||||
|
{ok, PacketId} ->
|
||||||
|
lager:debug("[mqtt_postman] send success, packet_id: ~p", [PacketId]),
|
||||||
|
{noreply, State#state{inflight = maps:put(PacketId, {Ref, Message}, InFlight)}};
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:warning("[mqtt_postman] send message to topic: ~p, get error: ~p", [Topic, Reason]),
|
||||||
|
{stop, Reason, State}
|
||||||
|
end;
|
||||||
|
handle_info(stop, State) ->
|
||||||
|
{stop, normal, State};
|
||||||
|
|
||||||
handle_info(Info, State = #state{}) ->
|
handle_info(Info, State = #state{}) ->
|
||||||
lager:debug("[iot_mqtt_publisher] get info: ~p", [Info]),
|
lager:debug("[mqtt_postman] get info: ~p", [Info]),
|
||||||
{noreply, State}.
|
{noreply, State}.
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
@ -121,12 +128,12 @@ handle_info(Info, State = #state{}) ->
|
|||||||
%% with Reason. The return value is ignored.
|
%% with Reason. The return value is ignored.
|
||||||
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
|
||||||
State :: #state{}) -> term()).
|
State :: #state{}) -> term()).
|
||||||
terminate(Reason, _State = #state{conn_pid = ConnPid}) when is_pid(ConnPid) ->
|
terminate(Reason, #state{conn_pid = ConnPid}) when is_pid(ConnPid) ->
|
||||||
ok = emqtt:disconnect(ConnPid),
|
ok = emqtt:disconnect(ConnPid),
|
||||||
lager:debug("[iot_mqtt_publisher] terminate with reason: ~p", [Reason]),
|
lager:debug("[mqtt_postman] terminate with reason: ~p", [Reason]),
|
||||||
ok;
|
ok;
|
||||||
terminate(Reason, _State) ->
|
terminate(Reason, _State) ->
|
||||||
lager:debug("[iot_mqtt_publisher] terminate with reason: ~p", [Reason]),
|
lager:debug("[mqtt_postman] terminate with reason: ~p", [Reason]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
%% @private
|
%% @private
|
||||||
197
apps/iot/src/redis/redis_handler.erl
Normal file
197
apps/iot/src/redis/redis_handler.erl
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author licheng5
|
||||||
|
%%% @copyright (C) 2021, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 21. 1月 2021 上午11:23
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(redis_handler).
|
||||||
|
-author("licheng5").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([handle/1]).
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% Key管理
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
handle([<<"DEL">>, Key]) when is_binary(Key) ->
|
||||||
|
N = mnesia_kv:del(Key),
|
||||||
|
{reply, N};
|
||||||
|
|
||||||
|
handle([<<"EXISTS">>, Key]) when is_binary(Key) ->
|
||||||
|
N = mnesia_kv:exists(Key),
|
||||||
|
{reply, N};
|
||||||
|
|
||||||
|
handle([<<"EXPIRE">>, Key, Second0]) when is_binary(Key), is_binary(Second0) ->
|
||||||
|
Second = binary_to_integer(Second0),
|
||||||
|
N = mnesia_kv:expire(Key, Second),
|
||||||
|
{reply, N};
|
||||||
|
|
||||||
|
handle([<<"KEYS">>, Pattern]) when is_binary(Pattern) andalso Pattern =/= <<>> ->
|
||||||
|
Keys = mnesia_kv:keys(Pattern),
|
||||||
|
{reply, Keys};
|
||||||
|
|
||||||
|
handle([<<"PERSIST">>, Key]) when is_binary(Key) ->
|
||||||
|
N = mnesia_kv:persist(Key),
|
||||||
|
{reply, N};
|
||||||
|
|
||||||
|
handle([<<"TTL">>, Key]) when is_binary(Key) ->
|
||||||
|
TTL = mnesia_kv:ttl(Key),
|
||||||
|
{reply, TTL};
|
||||||
|
|
||||||
|
handle([<<"TYPE">>, Key]) when is_binary(Key) ->
|
||||||
|
Type = mnesia_kv:type(Key),
|
||||||
|
{reply, atom_to_binary(Type)};
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% 字符串处理
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
handle([<<"GET">>, Key]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:get(Key)};
|
||||||
|
|
||||||
|
handle([<<"SET">>, Key, Val]) when is_binary(Key), is_binary(Val) ->
|
||||||
|
case mnesia_kv:set(Key, Val) of
|
||||||
|
true ->
|
||||||
|
{reply, <<"OK">>};
|
||||||
|
false ->
|
||||||
|
{reply, <<"FAILED">>}
|
||||||
|
end;
|
||||||
|
|
||||||
|
handle([<<"SETNX">>, Key, Val]) when is_binary(Key), is_binary(Val) ->
|
||||||
|
{reply, mnesia_kv:setnx(Key, Val)};
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% HashTable处理
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
handle([<<"HSET">>, Key, Field, Val]) when is_binary(Key), is_binary(Field), is_binary(Val) ->
|
||||||
|
{reply, mnesia_kv:hset(Key, Field, Val)};
|
||||||
|
|
||||||
|
handle([<<"HMSET">>, Key | KvPairs]) when is_binary(Key), length(KvPairs) rem 2 =:= 0 ->
|
||||||
|
{reply, mnesia_kv:hmset(Key, lists_to_map(KvPairs))};
|
||||||
|
|
||||||
|
handle([<<"HGET">>, Key, Field]) when is_binary(Key), is_binary(Field) ->
|
||||||
|
{reply, mnesia_kv:hget(Key, Field)};
|
||||||
|
|
||||||
|
handle([<<"HMGET">>, Key | Fields]) when is_binary(Key), is_list(Fields) ->
|
||||||
|
{reply, mnesia_kv:hmget(Key, Fields)};
|
||||||
|
|
||||||
|
handle([<<"HDEL">>, Key | Fields]) when is_binary(Key), is_list(Fields) ->
|
||||||
|
{reply, mnesia_kv:hdel(Key, Fields)};
|
||||||
|
|
||||||
|
handle([<<"HEXISTS">>, Key, Field]) when is_binary(Key), is_binary(Field) ->
|
||||||
|
{reply, mnesia_kv:hexists(Key, Field)};
|
||||||
|
|
||||||
|
handle([<<"HGETALL">>, Key]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:hgetall(Key)};
|
||||||
|
|
||||||
|
handle([<<"HKEYS">>, Key]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:hkeys(Key)};
|
||||||
|
|
||||||
|
handle([<<"HLEN">>, Key]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:hlen(Key)};
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% set处理
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
handle([<<"SADD">>, Key | Members]) when is_binary(Key), is_list(Members) ->
|
||||||
|
{reply, mnesia_kv:sadd(Key, Members)};
|
||||||
|
|
||||||
|
handle([<<"SCARD">>, Key]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:scard(Key)};
|
||||||
|
|
||||||
|
handle([<<"SISMEMBER">>, Key, Member]) when is_binary(Key), is_binary(Member) ->
|
||||||
|
{reply, mnesia_kv:sismember(Key, Member)};
|
||||||
|
|
||||||
|
handle([<<"SDIFF">>, Key1, Key2]) when is_binary(Key1), is_binary(Key2) ->
|
||||||
|
{reply, mnesia_kv:sdiff(Key1, Key2)};
|
||||||
|
|
||||||
|
handle([<<"SINTER">>, Key1, Key2]) when is_binary(Key1), is_binary(Key2) ->
|
||||||
|
{reply, mnesia_kv:sinter(Key1, Key2)};
|
||||||
|
|
||||||
|
handle([<<"SUNION">>, Key1, Key2]) when is_binary(Key1), is_binary(Key2) ->
|
||||||
|
{reply, mnesia_kv:sunion(Key1, Key2)};
|
||||||
|
|
||||||
|
handle([<<"SMEMBERS">>, Key]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:smembers(Key)};
|
||||||
|
|
||||||
|
handle([<<"SPOP">>, Key]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:spop(Key)};
|
||||||
|
|
||||||
|
handle([<<"SRANDMEMBER">>, Key, Count0]) when is_binary(Key), is_binary(Count0) ->
|
||||||
|
Count = binary_to_integer(Count0),
|
||||||
|
{reply, mnesia_kv:srandmember(Key, Count)};
|
||||||
|
|
||||||
|
handle([<<"SREM">>, Key | Members]) when is_binary(Key), is_list(Members) ->
|
||||||
|
{reply, mnesia_kv:srem(Key, Members)};
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% List 处理
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
handle([<<"LINDEX">>, Key, Idx0]) when is_binary(Key), is_binary(Idx0) ->
|
||||||
|
Idx = binary_to_integer(Idx0),
|
||||||
|
{reply, mnesia_kv:lindex(Key, Idx + 1)};
|
||||||
|
|
||||||
|
handle([<<"LLEN">>, Key]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:llen(Key)};
|
||||||
|
|
||||||
|
handle([<<"LPOP">>, Key]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:lpop(Key)};
|
||||||
|
|
||||||
|
handle([<<"RPOP">>, Key]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:rpop(Key)};
|
||||||
|
|
||||||
|
handle([<<"LPUSH">>, Key | Members]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:lpush(Key, Members)};
|
||||||
|
|
||||||
|
handle([<<"LPUSHX">>, Key | Members]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:lpushx(Key, Members)};
|
||||||
|
|
||||||
|
handle([<<"RPUSH">>, Key | Members]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:rpush(Key, Members)};
|
||||||
|
|
||||||
|
handle([<<"RPUSHX">>, Key | Members]) when is_binary(Key) ->
|
||||||
|
{reply, mnesia_kv:rpushx(Key, Members)};
|
||||||
|
|
||||||
|
handle([<<"LRANGE">>, Key, Start0, End0]) when is_binary(Key), is_binary(Start0), is_binary(End0) ->
|
||||||
|
Start = binary_to_integer(Start0),
|
||||||
|
End = binary_to_integer(End0),
|
||||||
|
{reply, mnesia_kv:lrange(Key, Start + 1, End + 1)};
|
||||||
|
|
||||||
|
handle([<<"LREM">>, Key, Count0, Val]) when is_binary(Key), is_binary(Count0), is_binary(Val) ->
|
||||||
|
Count = binary_to_integer(Count0),
|
||||||
|
{reply, mnesia_kv:lrem(Key, Count, Val)};
|
||||||
|
|
||||||
|
handle([<<"LSET">>, Key, Idx0, Val]) when is_binary(Key), is_binary(Idx0), is_binary(Val) ->
|
||||||
|
Idx = binary_to_integer(Idx0),
|
||||||
|
{reply, mnesia_kv:lset(Key, Idx + 1, Val)};
|
||||||
|
|
||||||
|
handle([<<"LTRIM">>, Key, Start0, End0]) when is_binary(Key), is_binary(Start0), is_binary(End0) ->
|
||||||
|
Start = binary_to_integer(Start0),
|
||||||
|
End = binary_to_integer(End0),
|
||||||
|
{reply, mnesia_kv:ltrim(Key, Start + 1, End + 1)};
|
||||||
|
|
||||||
|
handle([<<"LINSERT">>, Key, Position, Pivot, Val]) when is_binary(Key), Position =:= <<"BEFORE">>; Position =:= <<"AFTER">> ->
|
||||||
|
{reply, mnesia_kv:linsert(Key, Position, Pivot, Val)};
|
||||||
|
|
||||||
|
handle(_) ->
|
||||||
|
{reply, {error, <<"Unsuported Command">>}}.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% helper methods
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 将数组转换成map
|
||||||
|
lists_to_map(L) when is_list(L) ->
|
||||||
|
lists_to_map(L, #{}).
|
||||||
|
lists_to_map([], Map) ->
|
||||||
|
Map;
|
||||||
|
lists_to_map([K, V | Tail], Map) ->
|
||||||
|
lists_to_map(Tail, Map#{K => V}).
|
||||||
|
|
||||||
|
|
||||||
106
apps/iot/src/redis/redis_protocol.erl
Normal file
106
apps/iot/src/redis/redis_protocol.erl
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author licheng5
|
||||||
|
%%% @copyright (C) 2020, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 10. 12月 2020 上午11:17
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(redis_protocol).
|
||||||
|
-author("licheng5").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([start_link/2, init/2]).
|
||||||
|
|
||||||
|
-record(command, {
|
||||||
|
data = <<>>,
|
||||||
|
stage = parse_arg_num,
|
||||||
|
arg_num = 0,
|
||||||
|
args = []
|
||||||
|
}).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% esockd callback
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
start_link(Transport, Sock) ->
|
||||||
|
{ok, spawn_link(?MODULE, init, [Transport, Sock])}.
|
||||||
|
|
||||||
|
init(Transport, Sock) ->
|
||||||
|
{ok, NewSock} = Transport:wait(Sock),
|
||||||
|
loop(Transport, NewSock, #command{data = <<>>, arg_num = 0, args = []}).
|
||||||
|
|
||||||
|
loop(Transport, Sock, Command = #command{data = Data}) ->
|
||||||
|
Transport:setopts(Sock, [{active, once}]),
|
||||||
|
receive
|
||||||
|
{tcp, _, Packet} ->
|
||||||
|
%% 收到数据的第一个包,才开始记录处理时间, redis基于长连接,请求不是连续处理的
|
||||||
|
NData = <<Data/binary, Packet/binary>>,
|
||||||
|
case parse(Command#command{data = NData}) of
|
||||||
|
{ok, #command{args = [Method0|Args]}} ->
|
||||||
|
Method = string:uppercase(Method0),
|
||||||
|
lager:debug("[redis_protocol] get a command: ~p", [[Method|Args]]),
|
||||||
|
{reply, Reply} = redis_handler:handle([Method | Args]),
|
||||||
|
Transport:send(Sock, encode(Reply)),
|
||||||
|
%% 等待下一次请求
|
||||||
|
loop(Transport, Sock, #command{});
|
||||||
|
{more_data, NCommand} ->
|
||||||
|
%% 请求的数据包过大,一次接受不完整
|
||||||
|
loop(Transport, Sock, NCommand)
|
||||||
|
end;
|
||||||
|
{tcp_error, _} ->
|
||||||
|
exit(normal);
|
||||||
|
{tcp_closed, _} ->
|
||||||
|
exit(normal)
|
||||||
|
end.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% helper methods
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
%% 解析请求的包, 支持请求不在一个包里面的情况, 基于状态机
|
||||||
|
parse(Command = #command{stage = parse_arg_num, data = <<$*, Rest/binary>>}) ->
|
||||||
|
[ArgNum0, ArgBin] = binary:split(Rest, <<$\r, $\n>>),
|
||||||
|
ArgNum = binary_to_integer(ArgNum0),
|
||||||
|
parse(Command#command{arg_num = ArgNum, data = ArgBin, stage = parse_arg});
|
||||||
|
%% 解析请求的参数
|
||||||
|
parse(Command = #command{stage = parse_arg, args = Args, arg_num = 0, data = <<>>}) ->
|
||||||
|
{ok, Command#command{args = lists:reverse(Args)}};
|
||||||
|
parse(Command = #command{stage = parse_arg, args = Args, arg_num = ArgNum, data = ArgBin}) ->
|
||||||
|
case binary:split(ArgBin, <<$\r, $\n>>) of
|
||||||
|
[<<"$", ArgLen0/binary>>, RestArgBin] ->
|
||||||
|
ArgLen = binary_to_integer(ArgLen0),
|
||||||
|
case RestArgBin of
|
||||||
|
<<Arg:ArgLen/binary, $\r, $\n, RestArgBin1/binary>> ->
|
||||||
|
parse(Command#command{arg_num = ArgNum - 1, args = [Arg | Args], data = RestArgBin1});
|
||||||
|
_ ->
|
||||||
|
{more_data, Command}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{more_data, Command}
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% redis数据返回格式化
|
||||||
|
-spec encode(tuple() | binary() | list()) -> iolist().
|
||||||
|
encode({single_line, Arg}) when is_binary(Arg) ->
|
||||||
|
[<<$+>>, Arg, <<$\r, $\n>>];
|
||||||
|
encode({error, Arg}) when is_binary(Arg) ->
|
||||||
|
[<<$->>, Arg, <<$\r, $\n>>];
|
||||||
|
encode(Arg) when is_integer(Arg) ->
|
||||||
|
[<<$:>>, integer_to_list(Arg), <<$\r, $\n>>];
|
||||||
|
encode(Arg) when is_binary(Arg) ->
|
||||||
|
[<<$$>>, integer_to_list(iolist_size(Arg)), <<$\r, $\n>>, Arg, <<$\r, $\n>>];
|
||||||
|
encode(Args) when is_list(Args) ->
|
||||||
|
ArgCount = [<<$*>>, integer_to_list(length(Args)), <<$\r, $\n>>],
|
||||||
|
ArgsBin = lists:map(fun encode/1, lists:map(fun to_binary/1, Args)),
|
||||||
|
[ArgCount, ArgsBin].
|
||||||
|
|
||||||
|
%% 将数据转换成binary
|
||||||
|
to_binary(X) when is_list(X) ->
|
||||||
|
unicode:characters_to_binary(X);
|
||||||
|
to_binary(X) when is_atom(X) ->
|
||||||
|
list_to_binary(atom_to_list(X));
|
||||||
|
to_binary(X) when is_binary(X) ->
|
||||||
|
X;
|
||||||
|
to_binary(X) when is_integer(X) ->
|
||||||
|
list_to_binary(integer_to_list(X)).
|
||||||
172
apps/iot/src/websocket/ws_channel.erl
Normal file
172
apps/iot/src/websocket/ws_channel.erl
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @author licheng5
|
||||||
|
%%% @copyright (C) 2021, <COMPANY>
|
||||||
|
%%% @doc
|
||||||
|
%%%
|
||||||
|
%%% @end
|
||||||
|
%%% Created : 11. 1月 2021 上午12:17
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(ws_channel).
|
||||||
|
-author("licheng5").
|
||||||
|
-include("iot.hrl").
|
||||||
|
|
||||||
|
%% API
|
||||||
|
-export([init/2]).
|
||||||
|
-export([websocket_init/1, websocket_handle/2, websocket_info/2, terminate/3]).
|
||||||
|
-export([publish/3, stop/2]).
|
||||||
|
|
||||||
|
-record(state, {
|
||||||
|
uuid :: undefined | binary(),
|
||||||
|
%% 用户进程id
|
||||||
|
host_pid = undefined,
|
||||||
|
%% 发送消息对应的id
|
||||||
|
packet_id = 1 :: integer(),
|
||||||
|
|
||||||
|
%% 请求响应的对应关系
|
||||||
|
inflight = #{}
|
||||||
|
}).
|
||||||
|
|
||||||
|
%% 向通道中写入消息
|
||||||
|
-spec publish(Pid :: pid(), ReceiverPid :: pid(), Msg :: binary()) -> Ref :: reference().
|
||||||
|
publish(Pid, ReceiverPid, Msg) when is_pid(Pid), is_binary(Msg) ->
|
||||||
|
Ref = make_ref(),
|
||||||
|
Pid ! {publish, ReceiverPid, Ref, Msg},
|
||||||
|
Ref.
|
||||||
|
|
||||||
|
%% 关闭方法
|
||||||
|
-spec stop(Pid :: pid(), Reason :: any()) -> no_return().
|
||||||
|
stop(undefined, _Reason) ->
|
||||||
|
ok;
|
||||||
|
stop(Pid, Reason) when is_pid(Pid) ->
|
||||||
|
Pid ! {stop, Reason}.
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
%% 逻辑处理方法
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
init(Req, Opts) ->
|
||||||
|
{cowboy_websocket, Req, Opts}.
|
||||||
|
|
||||||
|
websocket_init(_State) ->
|
||||||
|
lager:debug("[ws_channel] get a new connection"),
|
||||||
|
%% 初始状态为true
|
||||||
|
{ok, #state{packet_id = 1}}.
|
||||||
|
|
||||||
|
websocket_handle({binary, <<?PACKET_REQUEST, PacketId:32, ?METHOD_REGISTER:8, Data/binary>>}, State) ->
|
||||||
|
#{<<"uuid">> := UUID, <<"timestamp">> := Timestamp, <<"salt">> := Salt, <<"username">> := Username, <<"token">> := Token} = jiffy:decode(Data, [return_maps]),
|
||||||
|
lager:debug("[ws_channel] register uuid: ~p, messag: ~p", [UUID, Data]),
|
||||||
|
case iot_auth:check(Username, Token, UUID, Salt, Timestamp) of
|
||||||
|
true ->
|
||||||
|
%% 查找数据库,如果没有则插入
|
||||||
|
case host_bo:ensured_host(UUID) of
|
||||||
|
ok ->
|
||||||
|
lager:debug("[ws_channel] register success, host uuid: ~p", [UUID]),
|
||||||
|
%% 尝试启动主机的服务进程
|
||||||
|
{ok, HostPid} = iot_host_sup:ensured_host_started(UUID),
|
||||||
|
ok = iot_host:attach_channel(HostPid, self()),
|
||||||
|
%% 建立到host的monitor
|
||||||
|
erlang:monitor(process, HostPid),
|
||||||
|
|
||||||
|
Reply = jiffy:encode(#{
|
||||||
|
<<"code">> => 1,
|
||||||
|
<<"message">> => <<"ok">>
|
||||||
|
}),
|
||||||
|
{reply, {binary, <<?PACKET_RESPONSE, PacketId:32, 0:8, Reply/binary>>}, State#state{uuid = UUID, host_pid = HostPid}};
|
||||||
|
|
||||||
|
{error, Reason} ->
|
||||||
|
lager:warning("[ws_channel] register failed, uuid: ~p, reason: ~p", [UUID, Reason]),
|
||||||
|
{stop, State}
|
||||||
|
end;
|
||||||
|
false ->
|
||||||
|
lager:warning("[ws_channel] uuid: ~p, user: ~p, auth failed", [UUID, Username]),
|
||||||
|
{stop, State}
|
||||||
|
end;
|
||||||
|
|
||||||
|
websocket_handle({binary, <<?PACKET_REQUEST, PacketId:32, ?METHOD_CREATE_SESSION:8, PubKey/binary>>}, State = #state{host_pid = HostPid, uuid = UUID}) when is_pid(HostPid) ->
|
||||||
|
lager:debug("[ws_channel] create session, uuid: ~p", [UUID]),
|
||||||
|
{ok, Reply} = iot_host:create_session(HostPid, PubKey),
|
||||||
|
{reply, {binary, <<?PACKET_RESPONSE, PacketId:32, Reply/binary>>}, State};
|
||||||
|
|
||||||
|
websocket_handle({binary, <<?PACKET_REQUEST, _PacketId:32, ?METHOD_DATA:8, Data/binary>>}, State = #state{uuid = UUID, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||||
|
lager:debug("[ws_channel] data uuid: ~p, data: ~p", [UUID, Data]),
|
||||||
|
iot_host:handle(HostPid, {data, Data}),
|
||||||
|
{ok, State};
|
||||||
|
|
||||||
|
%% 北向数据处理
|
||||||
|
websocket_handle({binary, <<?PACKET_REQUEST, _PacketId:32, ?METHOD_NORTH_DATA:8, 0:8, Data/binary>>}, State = #state{uuid = UUID, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||||
|
lager:debug("[ws_channel] north_data uuid: ~p, data: ~p", [UUID, Data]),
|
||||||
|
iot_host:handle(HostPid, {north_data, Data}),
|
||||||
|
{ok, State};
|
||||||
|
|
||||||
|
websocket_handle({binary, <<?PACKET_REQUEST, _PacketId:32, ?METHOD_NORTH_DATA:8, 1:1, Len:7, DeviceUUID:Len/binary, Data/binary>>}, State = #state{uuid = UUID, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||||
|
lager:debug("[ws_channel] north_data uuid: ~p, data: ~p", [UUID, Data]),
|
||||||
|
iot_host:handle(HostPid, {north_data, {DeviceUUID, Data}}),
|
||||||
|
{ok, State};
|
||||||
|
|
||||||
|
websocket_handle({binary, <<?PACKET_REQUEST, _PacketId:32, ?METHOD_PING:8, CipherMetric/binary>>}, State = #state{uuid = UUID, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||||
|
lager:debug("[ws_channel] ping uuid: ~p", [UUID]),
|
||||||
|
iot_host:handle(HostPid, {ping, CipherMetric}),
|
||||||
|
{ok, State};
|
||||||
|
|
||||||
|
websocket_handle({binary, <<?PACKET_REQUEST, _PacketId:32, ?METHOD_INFORM:8, CipherInfo/binary>>}, State = #state{uuid = UUID, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||||
|
lager:debug("[ws_channel] inform uuid: ~p", [UUID]),
|
||||||
|
iot_host:handle(HostPid, {inform, CipherInfo}),
|
||||||
|
{ok, State};
|
||||||
|
|
||||||
|
websocket_handle({binary, <<?PACKET_REQUEST, _PacketId:32, ?METHOD_FEEDBACK_STEP:8, CipherInfo/binary>>}, State = #state{uuid = UUID, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||||
|
lager:debug("[ws_channel] feedback step uuid: ~p", [UUID]),
|
||||||
|
iot_host:handle(HostPid, {feedback_step, CipherInfo}),
|
||||||
|
{ok, State};
|
||||||
|
|
||||||
|
websocket_handle({binary, <<?PACKET_REQUEST, _PacketId:32, ?METHOD_FEEDBACK_RESULT:8, CipherInfo/binary>>}, State = #state{uuid = UUID, host_pid = HostPid}) when is_pid(HostPid) ->
|
||||||
|
lager:debug("[ws_channel] feedback result uuid: ~p", [UUID]),
|
||||||
|
iot_host:handle(HostPid, {feedback_result, CipherInfo}),
|
||||||
|
{ok, State};
|
||||||
|
|
||||||
|
%% 主机端的消息响应
|
||||||
|
websocket_handle({binary, <<?PACKET_PUBLISH_RESPONSE, PacketId:32, Body/binary>>}, State = #state{uuid = UUID, inflight = Inflight}) when PacketId > 0 ->
|
||||||
|
lager:debug("[ws_channel] uuid: ~p, get publish response message: ~p, packet_id: ~p", [UUID, Body, PacketId]),
|
||||||
|
case maps:take(PacketId, Inflight) of
|
||||||
|
error ->
|
||||||
|
lager:warning("[ws_channel] get unknown publish response message: ~p, packet_id: ~p", [Body, PacketId]),
|
||||||
|
{ok, State};
|
||||||
|
{{ReceiverPid, Ref}, NInflight} ->
|
||||||
|
case is_pid(ReceiverPid) andalso is_process_alive(ReceiverPid) of
|
||||||
|
true when Body == <<>> ->
|
||||||
|
ReceiverPid ! {response, Ref};
|
||||||
|
true ->
|
||||||
|
ReceiverPid ! {response, Ref, Body};
|
||||||
|
false ->
|
||||||
|
lager:warning("[ws_channel] get publish response message: ~p, packet_id: ~p, but receiver_pid is dead", [Body, PacketId])
|
||||||
|
end,
|
||||||
|
{ok, State#state{inflight = NInflight}}
|
||||||
|
end;
|
||||||
|
|
||||||
|
websocket_handle(Info, State) ->
|
||||||
|
lager:debug("[ws_channel] get a error messag: ~p", [Info]),
|
||||||
|
{stop, State}.
|
||||||
|
|
||||||
|
%% 处理关闭信号
|
||||||
|
websocket_info({stop, Reason}, State) ->
|
||||||
|
lager:debug("[ws_channel] the channel will be closed with reason: ~p", [Reason]),
|
||||||
|
{stop, State};
|
||||||
|
|
||||||
|
%% 发送消息
|
||||||
|
websocket_info({publish, ReceiverPid, Ref, Msg}, State = #state{packet_id = PacketId, inflight = Inflight}) when is_binary(Msg) ->
|
||||||
|
NInflight = maps:put(PacketId, {ReceiverPid, Ref}, Inflight),
|
||||||
|
{reply, {binary, <<?PACKET_PUBLISH, PacketId:32, Msg/binary>>}, State#state{packet_id = PacketId + 1, inflight = NInflight}};
|
||||||
|
|
||||||
|
%% 用户进程关闭,则关闭通道
|
||||||
|
websocket_info({'DOWN', _, process, HostPid, Reason}, State = #state{uuid = UUID, host_pid = HostPid}) ->
|
||||||
|
lager:debug("[ws_channel] uuid: ~p, channel will close because user exited with reason: ~p", [UUID, Reason]),
|
||||||
|
{stop, State};
|
||||||
|
|
||||||
|
%% 处理其他未知消息
|
||||||
|
websocket_info(Info, State = #state{uuid = UUID}) ->
|
||||||
|
lager:debug("[ws_channel] channel get unknown info: ~p, uuid: ~p", [Info, UUID]),
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%% 进程关闭事件
|
||||||
|
terminate(Reason, _Req, State) ->
|
||||||
|
lager:debug("[ws_channel] channel close with reason: ~p, state is: ~p", [Reason, State]),
|
||||||
|
ok.
|
||||||
@ -7,6 +7,13 @@
|
|||||||
{backlog, 10240}
|
{backlog, 10240}
|
||||||
]},
|
]},
|
||||||
|
|
||||||
|
{redis_server, [
|
||||||
|
{port, 16379},
|
||||||
|
{acceptors, 500},
|
||||||
|
{max_connections, 10240},
|
||||||
|
{backlog, 10240}
|
||||||
|
]},
|
||||||
|
|
||||||
%% 目标服务器地址
|
%% 目标服务器地址
|
||||||
{emqx_server, [
|
{emqx_server, [
|
||||||
{host, {39, 98, 184, 67}},
|
{host, {39, 98, 184, 67}},
|
||||||
@ -18,6 +25,11 @@
|
|||||||
{retry_interval, 5}
|
{retry_interval, 5}
|
||||||
]},
|
]},
|
||||||
|
|
||||||
|
%% 权限检验时的预埋token
|
||||||
|
{pre_tokens, [
|
||||||
|
{<<"test">>, <<"iot2023">>}
|
||||||
|
]},
|
||||||
|
|
||||||
{pools, [
|
{pools, [
|
||||||
%% mysql连接池配置
|
%% mysql连接池配置
|
||||||
{mysql_pool,
|
{mysql_pool,
|
||||||
98
config/sys-prod.config
Normal file
98
config/sys-prod.config
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
[
|
||||||
|
{iot, [
|
||||||
|
{http_server, [
|
||||||
|
{port, 18080},
|
||||||
|
{acceptors, 500},
|
||||||
|
{max_connections, 10240},
|
||||||
|
{backlog, 10240}
|
||||||
|
]},
|
||||||
|
|
||||||
|
{redis_server, [
|
||||||
|
{port, 16379},
|
||||||
|
{acceptors, 500},
|
||||||
|
{max_connections, 10240},
|
||||||
|
{backlog, 10240}
|
||||||
|
]},
|
||||||
|
|
||||||
|
%% 目标服务器地址
|
||||||
|
%{emqx_server, [
|
||||||
|
% {host, {39, 98, 184, 67}},
|
||||||
|
% {port, 1883},
|
||||||
|
% {tcp_opts, []},
|
||||||
|
% {username, "test"},
|
||||||
|
% {password, "test1234"},
|
||||||
|
% {keepalive, 86400},
|
||||||
|
% {retry_interval, 5}
|
||||||
|
%]},
|
||||||
|
|
||||||
|
%% 权限检验时的预埋token
|
||||||
|
{pre_tokens, [
|
||||||
|
{<<"test">>, <<"iot2023">>}
|
||||||
|
]},
|
||||||
|
|
||||||
|
{pools, [
|
||||||
|
%% mysql连接池配置
|
||||||
|
{mysql_pool,
|
||||||
|
[{size, 10}, {max_overflow, 20}, {worker_module, mysql}],
|
||||||
|
[
|
||||||
|
{host, {172, 19, 0, 2}},
|
||||||
|
{port, 3306},
|
||||||
|
{user, "root"},
|
||||||
|
{connect_mode, lazy},
|
||||||
|
{keep_alive, true},
|
||||||
|
{password, "nnpwd@Fe7w"},
|
||||||
|
{database, "nannong"},
|
||||||
|
{queries, [<<"set names utf8">>]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
%% influxdb数据库配置
|
||||||
|
{influx_pool,
|
||||||
|
[{size, 100}, {max_overflow, 200}, {worker_module, influx_client}],
|
||||||
|
[
|
||||||
|
{host, "127.0.0.1"},
|
||||||
|
{port, 8086},
|
||||||
|
{token, <<"r9wZmzf1hu3g1_AWsNiGi88p1DNeypDcBzuuAYBdzB6SEYK1CeeMkwQqHQ6y_2qqV4o3ZZqnnhSJ5mLXu8Feiw==">>}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
]}
|
||||||
|
|
||||||
|
]},
|
||||||
|
|
||||||
|
|
||||||
|
%% 系统日志配置,系统日志为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, "error.log"}, {level, error}]},
|
||||||
|
{lager_file_backend, [{file, "debug.log"}, {level, debug}]},
|
||||||
|
{lager_file_backend, [{file, "info.log"}, {level, info}]}
|
||||||
|
]}
|
||||||
|
]}
|
||||||
|
|
||||||
|
].
|
||||||
@ -1,11 +1,11 @@
|
|||||||
-name iot
|
-sname iot
|
||||||
|
|
||||||
-setcookie iot_cookie
|
-setcookie iot_cookie
|
||||||
|
|
||||||
+K true
|
+K true
|
||||||
+A30
|
+A30
|
||||||
|
|
||||||
-mnesia dir '"/usr/local/code/data/iot"'
|
-mnesia dir '"/usr/local/var/mnesia/iot"'
|
||||||
-mnesia dump_log_write_threshold 50000
|
-mnesia dump_log_write_threshold 50000
|
||||||
-mnesia dc_dump_limit 40
|
-mnesia dc_dump_limit 40
|
||||||
|
|
||||||
|
|||||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: '3.6'
|
||||||
|
|
||||||
|
services:
|
||||||
|
iot:
|
||||||
|
container_name: iot
|
||||||
|
image: "iot:1.0"
|
||||||
|
hostname: 'iot'
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 18080:18080/tcp
|
||||||
|
- 16379:16379/tcp
|
||||||
|
volumes:
|
||||||
|
- /var/log/iot/:/data/iot/log/
|
||||||
|
- /usr/local/var/mnesia/iot/:/usr/local/var/mnesia/iot/
|
||||||
59
docs/endpoint.md
Normal file
59
docs/endpoint.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
## Endpoint管理
|
||||||
|
|
||||||
|
### 获取全部的Endpoint
|
||||||
|
|
||||||
|
```html
|
||||||
|
method: GET
|
||||||
|
url: /endpoint/all
|
||||||
|
|
||||||
|
返回数据:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "名称",
|
||||||
|
"title": "中电集团"
|
||||||
|
"matcher": "匹配的正则表达式",
|
||||||
|
"protocol": "http|https|websocket|mqtt|kafka",
|
||||||
|
"config": "参考config格式说明"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建Endpoint
|
||||||
|
|
||||||
|
```html
|
||||||
|
method: POST
|
||||||
|
url: /endpoint/create
|
||||||
|
body: (content-type: application/json)
|
||||||
|
{"name": $name, "matcher": $matcher, "title": $title, "protocol": "http|https|websocket|kafka|mqtt", "config": "参考config格式说明"}
|
||||||
|
|
||||||
|
说明:
|
||||||
|
name是唯一的,不同的终端名称代表不同的接受端
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 删除Endpoint
|
||||||
|
```html
|
||||||
|
method: POST
|
||||||
|
url: /endpoint/delete
|
||||||
|
body: (content-type: application/json)
|
||||||
|
{"name": $name}
|
||||||
|
```
|
||||||
|
|
||||||
|
### config格式说明
|
||||||
|
```html
|
||||||
|
|
||||||
|
http|https
|
||||||
|
{"url": "http(s)://xx.com"}
|
||||||
|
|
||||||
|
websocket
|
||||||
|
{"url": "ws://xx.com/ws"}
|
||||||
|
|
||||||
|
kafka:
|
||||||
|
{"bootstrap_server": ["localhost:9092"], "topic": "test", "username": "test", "password": "password1234"}
|
||||||
|
mqtt:
|
||||||
|
{"host": "localhost", port: 1883, "username": "test", "password": "test1234", "topic": "CET/NX/${location_code}/upload", "qos": 0|1|2}
|
||||||
|
|
||||||
|
topic中支持预定义变量: ${location_code}; 发送的时候会替换成对应的点位编码
|
||||||
|
```
|
||||||
31
docs/host_mocker.html
Normal file
31
docs/host_mocker.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
<p>Hello World</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let webSocket = new WebSocket("ws://localhost:18080/ws")
|
||||||
|
webSocket.binaryType = "blob"
|
||||||
|
|
||||||
|
webSocket.onopen = function () {
|
||||||
|
console.log("socket is open")
|
||||||
|
}
|
||||||
|
webSocket.onclose = function() {
|
||||||
|
console.log("socket closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
webSocket.onmessage = function (message) {
|
||||||
|
console.log(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
docs/websocket.md
Normal file
79
docs/websocket.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# websocket通讯格式逻辑说明
|
||||||
|
|
||||||
|
## 消息体有四种格式
|
||||||
|
1. 主机发送的请求 (0x01)
|
||||||
|
2. 服务端对主机请求的响应 (0x02)
|
||||||
|
3. 服务端对主机的消息推送 (0x03)
|
||||||
|
4. 主机对服务器推送消息的响应 (0x04)
|
||||||
|
|
||||||
|
## 消息体的格式说明
|
||||||
|
<<消息体类型/1byte, PacketId/4byte, Packet/任意长度>>
|
||||||
|
|
||||||
|
## 特殊说明
|
||||||
|
* 服务器端处理异常时,直接关闭websocket连接
|
||||||
|
|
||||||
|
## 消息类型说明
|
||||||
|
|
||||||
|
### register消息,
|
||||||
|
|
||||||
|
#### 请求
|
||||||
|
<<0x01, PacketId:4, Method:1, Body:任意长度>>
|
||||||
|
|
||||||
|
PacketId: 4字节整数, 值必须大于0;
|
||||||
|
Method: 0x00
|
||||||
|
Body: {uuid: string, salt: string, username: string, token: string}, json序列化后的二级制数据,明文
|
||||||
|
|
||||||
|
### 响应
|
||||||
|
<<0x02, PacketId:4, Reply>>
|
||||||
|
Reply: {code: 1, message: "ok"}
|
||||||
|
|
||||||
|
### create_session消息
|
||||||
|
|
||||||
|
#### 请求
|
||||||
|
<<0x01, PacketId:4, 0x01, PubKey:任意长度(公钥信息)>>
|
||||||
|
|
||||||
|
PacketId: 4字节整数, 值必须大于0;
|
||||||
|
|
||||||
|
#### 响应
|
||||||
|
<<0x02, PacketId:4, Reply>>
|
||||||
|
Reply: {a: bool, aes: "服务器生成的aes的值"}
|
||||||
|
|
||||||
|
### data数据上传(无响应)
|
||||||
|
<<0x01, PacketId:4, 0x02, Body:任意长度>>
|
||||||
|
|
||||||
|
PacketId: 4字节整数, 值为0;
|
||||||
|
|
||||||
|
### ping数据上传(无响应)
|
||||||
|
<<0x01, PacketId:4, 0x03, Body:任意长度>>
|
||||||
|
|
||||||
|
PacketId: 4字节整数, 值为0;
|
||||||
|
Body: 公钥信息
|
||||||
|
|
||||||
|
### inform数据上传(无响应)
|
||||||
|
<<0x01, PacketId:4, 0x04, Body:任意长度>>
|
||||||
|
|
||||||
|
PacketId: 4字节整数, 值为0;
|
||||||
|
Body: 公钥信息
|
||||||
|
|
||||||
|
### feedback_step数据上传(无响应)
|
||||||
|
<<0x01, PacketId:4, 0x05, Body:任意长度>>
|
||||||
|
|
||||||
|
PacketId: 4字节整数, 值为0;
|
||||||
|
Body: 公钥信息
|
||||||
|
|
||||||
|
### feedback_result数据上传(无响应)
|
||||||
|
<<0x01, PacketId:4, 0x06, Body:任意长度>>
|
||||||
|
|
||||||
|
PacketId: 4字节整数, 值为0;
|
||||||
|
Body: 公钥信息
|
||||||
|
|
||||||
|
### data北向数据上传 (无响应)
|
||||||
|
|
||||||
|
#### 微服务产生的数据,点位信息为主机的点位信息
|
||||||
|
<<0x01, PacketId:4, 0x08, 0x00, Body:任意长度>>
|
||||||
|
PacketId: 4字节整数, 值为0;
|
||||||
|
|
||||||
|
#### 终端设备产生的数据,点位信息为设备信息点位信息
|
||||||
|
<<0x01, PacketId:4, 0x08, Type:1byte, Body:任意长度>>
|
||||||
|
PacketId: 4字节整数, 值为0;
|
||||||
|
Type: 第一bit为1,后7个bit标识后面的device_uuid的长度, 即: <<1:1, Len:7>>, 总共8个bit
|
||||||
@ -3,11 +3,11 @@
|
|||||||
{poolboy, ".*", {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.1"}}},
|
{poolboy, ".*", {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.1"}}},
|
||||||
{hackney, ".*", {git, "https://github.com/benoitc/hackney.git", {tag, "1.16.0"}}},
|
{hackney, ".*", {git, "https://github.com/benoitc/hackney.git", {tag, "1.16.0"}}},
|
||||||
{sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}},
|
{sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}},
|
||||||
{cowboy, ".*", {git, "https://github.com/ninenines/cowboy.git", {tag, "2.5.0"}}},
|
{cowboy, ".*", {git, "https://github.com/ninenines/cowboy.git", {tag, "2.10.0"}}},
|
||||||
|
{esockd, ".*", {git, "https://github.com/emqx/esockd.git", {tag, "v5.7.3"}}},
|
||||||
{jiffy, ".*", {git, "https://github.com/davisp/jiffy.git", {tag, "1.1.1"}}},
|
{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"}}},
|
{mysql, ".*", {git, "https://github.com/mysql-otp/mysql-otp", {tag, "1.8.0"}}},
|
||||||
{parse_trans, ".*", {git, "https://github.com/uwiger/parse_trans", {tag, "3.0.0"}}},
|
{parse_trans, ".*", {git, "https://github.com/uwiger/parse_trans", {tag, "3.0.0"}}},
|
||||||
{emqtt, ".*", {git, "https://github.com/emqx/emqtt", {tag, "v1.2.0"}}},
|
|
||||||
{lager, ".*", {git,"https://github.com/erlang-lager/lager.git", {tag, "3.9.2"}}}
|
{lager, ".*", {git,"https://github.com/erlang-lager/lager.git", {tag, "3.9.2"}}}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
@ -43,3 +43,6 @@
|
|||||||
}]}]}.
|
}]}]}.
|
||||||
|
|
||||||
{erl_opts, [{parse_transform,lager_transform}]}.
|
{erl_opts, [{parse_transform,lager_transform}]}.
|
||||||
|
|
||||||
|
{rebar_packages_cdn, "https://hexpm.upyun.com"}.
|
||||||
|
|
||||||
|
|||||||
21
rebar.lock
21
rebar.lock
@ -2,23 +2,18 @@
|
|||||||
[{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.2">>},1},
|
[{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.2">>},1},
|
||||||
{<<"cowboy">>,
|
{<<"cowboy">>,
|
||||||
{git,"https://github.com/ninenines/cowboy.git",
|
{git,"https://github.com/ninenines/cowboy.git",
|
||||||
{ref,"c998673eb009da2ea4dc0e6ef0332534cf679cc4"}},
|
{ref,"9e600f6c1df3c440bc196b66ebbc005d70107217"}},
|
||||||
0},
|
0},
|
||||||
{<<"cowlib">>,
|
{<<"cowlib">>,
|
||||||
{git,"https://github.com/ninenines/cowlib",
|
{git,"https://github.com/ninenines/cowlib",
|
||||||
{ref,"106ba84bb04537879d8ce59321a04e0682110b91"}},
|
{ref,"cc04201c1d0e1d5603cd1cde037ab729b192634c"}},
|
||||||
1},
|
1},
|
||||||
{<<"emqtt">>,
|
{<<"esockd">>,
|
||||||
{git,"https://github.com/emqx/emqtt",
|
{git,"https://github.com/emqx/esockd.git",
|
||||||
{ref,"55e50041cc5b3416067c120eadb8774f1d3d1f4a"}},
|
{ref,"d9ce4024cc42a65e9a05001997031e743442f955"}},
|
||||||
0},
|
0},
|
||||||
{<<"fs">>,{pkg,<<"fs">>,<<"6.1.1">>},1},
|
{<<"fs">>,{pkg,<<"fs">>,<<"6.1.1">>},1},
|
||||||
{<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},1},
|
|
||||||
{<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1},
|
{<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1},
|
||||||
{<<"gun">>,
|
|
||||||
{git,"https://github.com/ninenines/gun",
|
|
||||||
{ref,"e7dd9f227e46979d8073e71c683395a809b78cb4"}},
|
|
||||||
1},
|
|
||||||
{<<"hackney">>,
|
{<<"hackney">>,
|
||||||
{git,"https://github.com/benoitc/hackney.git",
|
{git,"https://github.com/benoitc/hackney.git",
|
||||||
{ref,"f3e9292db22c807e73f57a8422402d6b423ddf5f"}},
|
{ref,"f3e9292db22c807e73f57a8422402d6b423ddf5f"}},
|
||||||
@ -48,19 +43,18 @@
|
|||||||
0},
|
0},
|
||||||
{<<"ranch">>,
|
{<<"ranch">>,
|
||||||
{git,"https://github.com/ninenines/ranch",
|
{git,"https://github.com/ninenines/ranch",
|
||||||
{ref,"9b8ed47d789412b0021bfc1f94f1c17c387c721c"}},
|
{ref,"a692f44567034dacf5efcaa24a24183788594eb7"}},
|
||||||
1},
|
1},
|
||||||
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},1},
|
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},1},
|
||||||
{<<"sync">>,
|
{<<"sync">>,
|
||||||
{git,"https://github.com/rustyio/sync.git",
|
{git,"https://github.com/rustyio/sync.git",
|
||||||
{ref,"3f0049e809ffe303ae2cd395217a025ce6e758ae"}},
|
{ref,"f13e61a79623290219d7c10dff1dd94d91eee963"}},
|
||||||
0},
|
0},
|
||||||
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.5.0">>},2}]}.
|
{<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.5.0">>},2}]}.
|
||||||
[
|
[
|
||||||
{pkg_hash,[
|
{pkg_hash,[
|
||||||
{<<"certifi">>, <<"B7CFEAE9D2ED395695DD8201C57A2D019C0C43ECAF8B8BCB9320B40D6662F340">>},
|
{<<"certifi">>, <<"B7CFEAE9D2ED395695DD8201C57A2D019C0C43ECAF8B8BCB9320B40D6662F340">>},
|
||||||
{<<"fs">>, <<"9D147B944D60CFA48A349F12D06C8EE71128F610C90870BDF9A6773206452ED0">>},
|
{<<"fs">>, <<"9D147B944D60CFA48A349F12D06C8EE71128F610C90870BDF9A6773206452ED0">>},
|
||||||
{<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>},
|
|
||||||
{<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>},
|
{<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>},
|
||||||
{<<"idna">>, <<"1D038FB2E7668CE41FBF681D2C45902E52B3CB9E9C77B55334353B222C2EE50C">>},
|
{<<"idna">>, <<"1D038FB2E7668CE41FBF681D2C45902E52B3CB9E9C77B55334353B222C2EE50C">>},
|
||||||
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
|
{<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>},
|
||||||
@ -70,7 +64,6 @@
|
|||||||
{pkg_hash_ext,[
|
{pkg_hash_ext,[
|
||||||
{<<"certifi">>, <<"3B3B5F36493004AC3455966991EAF6E768CE9884693D9968055AEEEB1E575040">>},
|
{<<"certifi">>, <<"3B3B5F36493004AC3455966991EAF6E768CE9884693D9968055AEEEB1E575040">>},
|
||||||
{<<"fs">>, <<"EF94E95FFE79916860649FED80AC62B04C322B0BB70F5128144C026B4D171F8B">>},
|
{<<"fs">>, <<"EF94E95FFE79916860649FED80AC62B04C322B0BB70F5128144C026B4D171F8B">>},
|
||||||
{<<"getopt">>, <<"53E1AB83B9CEB65C9672D3E7A35B8092E9BDC9B3EE80721471A161C10C59959C">>},
|
|
||||||
{<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>},
|
{<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>},
|
||||||
{<<"idna">>, <<"A02C8A1C4FD601215BB0B0324C8A6986749F807CE35F25449EC9E69758708122">>},
|
{<<"idna">>, <<"A02C8A1C4FD601215BB0B0324C8A6986749F807CE35F25449EC9E69758708122">>},
|
||||||
{<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
|
{<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user