fix emqtt

This commit is contained in:
anlicheng 2023-07-24 11:26:46 +08:00
parent a5fa1d1bc5
commit 7e82659103
49 changed files with 6127 additions and 1165 deletions

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ logs
*.iml *.iml
rebar3.crashdump rebar3.crashdump
*~ *~
config/sys.config

12
Dockerfile Normal file
View 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
View 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.

View File

@ -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()
}).

View File

@ -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

File diff suppressed because it is too large Load Diff

View 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.

View 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.

View 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).

View 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>>).

View File

@ -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} ->
Topic = iot_host:downstream_topic(UUID),
case iot_mqtt_publisher:publish(Topic, BinCommand, 2) of
{ok, Ref} -> {ok, Ref} ->
receive receive
{ok, Ref, _PacketId} -> {response, Ref} ->
{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">>)};
{response, Ref, Response} ->
case jiffy:decode(Response, [return_maps]) of
#{<<"code">> := 1} ->
{ok, _} = task_logs_bo:change_status(TaskId, ?TASK_STATUS_OK),
{ok, 200, iot_util:json_data(<<"success">>)};
#{<<"code">> := 0, <<"message">> := Message} when is_binary(Message) ->
{ok, _} = task_logs_bo:change_status(TaskId, ?TASK_STATUS_FAILED),
{ok, 200, iot_util:json_error(401, <<"操作失败: "/utf8, Message/binary>>)}
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, _} = 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(401, <<"命令执行超时, 请重试"/utf8>>)}
end;
{error, Reason} ->
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, 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),

View 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
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

View File

@ -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) ->

View File

@ -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
]}, ]},

View File

@ -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
View 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.

View 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).

View 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']}.

View File

@ -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) ->

View File

@ -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.

View File

@ -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
%%%===================================================================

View File

@ -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),

View File

@ -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.

View 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.

View File

@ -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']
}, },
#{ #{

View File

@ -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.

View 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
}.

View 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).

View 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)].

View 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">>}).

View File

@ -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};
%% 10rsa加密的数据aesaes加密的
<<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.

View 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
%%%===================================================================

View File

@ -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) ->

View 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
%%%===================================================================

View 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
%%%===================================================================

View File

@ -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

View 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}).

View 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)).

View 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.

View File

@ -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
View 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}]}
]}
]}
].

View File

@ -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
View 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
View 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
View 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
View 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

View File

@ -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"}.

View File

@ -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">>},

1
shell Normal file
View File

@ -0,0 +1 @@
docker run -p 18080:18080/tcp -p 16379:16379/tcp -v /usr/local/var/mnesia/iot:/usr/local/var/mnesia/iot -v /var/log/iot:/data/iot/log -d iot:1.0