From 7e826591036f9f6801f2c527a8d27014e74b9de8 Mon Sep 17 00:00:00 2001 From: anlicheng Date: Mon, 24 Jul 2023 11:26:46 +0800 Subject: [PATCH] fix emqtt --- .gitignore | 1 + Dockerfile | 12 + apps/iot/include/emqtt.hrl | 535 +++++++ apps/iot/include/iot.hrl | 55 +- apps/iot/src/database/host_bo.erl | 16 + apps/iot/src/emqtt/emqtt.erl | 1316 +++++++++++++++++ apps/iot/src/emqtt/emqtt_frame.erl | 737 +++++++++ apps/iot/src/emqtt/emqtt_props.erl | 172 +++ apps/iot/src/emqtt/emqtt_sock.erl | 120 ++ .../iot/src/http_handler/endpoint_handler.erl | 168 +++ ...http_host_handler.erl => host_handler.erl} | 75 +- apps/iot/src/http_handler/test_handler.erl | 31 + apps/iot/src/influxdb/influx_point.erl | 3 + apps/iot/src/iot.app.src | 3 +- apps/iot/src/iot_app.erl | 89 +- apps/iot/src/iot_auth.erl | 31 + apps/iot/src/iot_endpoint.erl | 278 ++++ apps/iot/src/iot_endpoint_sup.erl | 51 + apps/iot/src/iot_host.erl | 222 +-- apps/iot/src/iot_mnesia.erl | 54 - apps/iot/src/iot_mqtt_reply_subscriber.erl | 172 --- apps/iot/src/iot_mqtt_subscriber.erl | 41 +- apps/iot/src/iot_mqtt_sys_subscriber.erl | 223 --- apps/iot/src/iot_router.erl | 33 + apps/iot/src/iot_sup.erl | 37 +- apps/iot/src/iot_util.erl | 34 +- apps/iot/src/mnesia/mnesia_endpoint.erl | 69 + apps/iot/src/mnesia/mnesia_id_generator.erl | 16 + apps/iot/src/mnesia/mnesia_kv.erl | 949 ++++++++++++ apps/iot/src/mocker/eval_test.erl | 25 + apps/iot/src/mocker/host_mocker.erl | 432 ------ apps/iot/src/mocker/iot_endpoint_mocker.erl | 119 ++ apps/iot/src/mocker/iot_mock.erl | 42 + apps/iot/src/postman/http_postman.erl | 124 ++ apps/iot/src/postman/http_postman_worker.erl | 119 ++ .../mqtt_postman.erl} | 85 +- apps/iot/src/redis/redis_handler.erl | 197 +++ apps/iot/src/redis/redis_protocol.erl | 106 ++ apps/iot/src/websocket/ws_channel.erl | 172 +++ config/{sys.config => sys-dev.config} | 12 + config/sys-prod.config | 98 ++ config/vm.args | 6 +- docker-compose.yml | 14 + docs/endpoint.md | 59 + docs/host_mocker.html | 31 + docs/websocket.md | 79 + rebar.config | 7 +- rebar.lock | 21 +- shell | 1 + 49 files changed, 6127 insertions(+), 1165 deletions(-) create mode 100644 Dockerfile create mode 100644 apps/iot/include/emqtt.hrl create mode 100644 apps/iot/src/emqtt/emqtt.erl create mode 100644 apps/iot/src/emqtt/emqtt_frame.erl create mode 100644 apps/iot/src/emqtt/emqtt_props.erl create mode 100644 apps/iot/src/emqtt/emqtt_sock.erl create mode 100644 apps/iot/src/http_handler/endpoint_handler.erl rename apps/iot/src/http_handler/{http_host_handler.erl => host_handler.erl} (72%) create mode 100644 apps/iot/src/http_handler/test_handler.erl create mode 100644 apps/iot/src/iot_auth.erl create mode 100644 apps/iot/src/iot_endpoint.erl create mode 100644 apps/iot/src/iot_endpoint_sup.erl delete mode 100644 apps/iot/src/iot_mnesia.erl delete mode 100644 apps/iot/src/iot_mqtt_reply_subscriber.erl delete mode 100644 apps/iot/src/iot_mqtt_sys_subscriber.erl create mode 100644 apps/iot/src/iot_router.erl create mode 100644 apps/iot/src/mnesia/mnesia_endpoint.erl create mode 100644 apps/iot/src/mnesia/mnesia_id_generator.erl create mode 100644 apps/iot/src/mnesia/mnesia_kv.erl create mode 100644 apps/iot/src/mocker/eval_test.erl delete mode 100644 apps/iot/src/mocker/host_mocker.erl create mode 100644 apps/iot/src/mocker/iot_endpoint_mocker.erl create mode 100644 apps/iot/src/postman/http_postman.erl create mode 100644 apps/iot/src/postman/http_postman_worker.erl rename apps/iot/src/{iot_mqtt_publisher.erl => postman/mqtt_postman.erl} (59%) create mode 100644 apps/iot/src/redis/redis_handler.erl create mode 100644 apps/iot/src/redis/redis_protocol.erl create mode 100644 apps/iot/src/websocket/ws_channel.erl rename config/{sys.config => sys-dev.config} (92%) create mode 100644 config/sys-prod.config create mode 100644 docker-compose.yml create mode 100644 docs/endpoint.md create mode 100644 docs/host_mocker.html create mode 100644 docs/websocket.md create mode 100644 shell diff --git a/.gitignore b/.gitignore index e2292f3..6a8e5c8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ logs *.iml rebar3.crashdump *~ +config/sys.config diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5497f0d --- /dev/null +++ b/Dockerfile @@ -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 \ No newline at end of file diff --git a/apps/iot/include/emqtt.hrl b/apps/iot/include/emqtt.hrl new file mode 100644 index 0000000..1fd4a76 --- /dev/null +++ b/apps/iot/include/emqtt.hrl @@ -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. diff --git a/apps/iot/include/iot.hrl b/apps/iot/include/iot.hrl index 6be7269..aef5e2f 100644 --- a/apps/iot/include/iot.hrl +++ b/apps/iot/include/iot.hrl @@ -19,10 +19,61 @@ -define(TASK_STATUS_OK, 1). %% 在线 %% 主机端上报数据类型标识 - +%% 建立到websocket的register关系 +-define(METHOD_REGISTER, 16#00). -define(METHOD_CREATE_SESSION, 16#01). + -define(METHOD_DATA, 16#02). -define(METHOD_PING, 16#03). -define(METHOD_INFORM, 16#04). -define(METHOD_FEEDBACK_STEP, 16#05). --define(METHOD_FEEDBACK_RESULT, 16#06). \ No newline at end of file +-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() +}). \ No newline at end of file diff --git a/apps/iot/src/database/host_bo.erl b/apps/iot/src/database/host_bo.erl index d7e8450..cf26f09 100644 --- a/apps/iot/src/database/host_bo.erl +++ b/apps/iot/src/database/host_bo.erl @@ -12,6 +12,7 @@ %% API -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()]. get_all_hosts() -> @@ -43,4 +44,19 @@ is_authorized(HostId) when is_integer(HostId) -> true; _ -> false + 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. \ No newline at end of file diff --git a/apps/iot/src/emqtt/emqtt.erl b/apps/iot/src/emqtt/emqtt.erl new file mode 100644 index 0000000..8ef6df9 --- /dev/null +++ b/apps/iot/src/emqtt/emqtt.erl @@ -0,0 +1,1316 @@ +%%-------------------------------------------------------------------- +%% 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). + +-behaviour(gen_statem). + +-include("emqtt.hrl"). + +-export([start_link/0, start_link/1]). + +-export([connect/1, connect/2, disconnect/1, disconnect/2, disconnect/3]). + +-export([ping/1]). + +%% PubSub +-export([ subscribe/2, subscribe/3, subscribe/4, publish/2, publish/3, publish/4, publish/5, unsubscribe/2, unsubscribe/3]). + +%% Puback... +-export([puback/2, puback/3, puback/4, pubrec/2, pubrec/3, pubrec/4, pubrel/2, pubrel/3, pubrel/4, pubcomp/2, pubcomp/3, pubcomp/4 ]). + +-export([subscriptions/1]). + +-export([info/1, stop/1]). + +%% For test cases +-export([ pause/1, resume/1 ]). + +-export([ initialized/3, waiting_for_connack/3, connected/3, inflight_full/3, random_client_id/0, reason_code_name/1 ]). + +-export([ init/1, callback_mode/0, handle_event/4, terminate/3, code_change/4 ]). + +-export_type([host/0 , option/0 , properties/0 , payload/0 , pubopt/0 , subopt/0 , mqtt_msg/0 , client/0]). + +-type(host() :: inet:ip_address() | inet:hostname()). + +%% Message handler is a set of callbacks defined to handle MQTT messages +%% as well as the disconnect event. +-define(NO_MSG_HDLR, undefined). + +-type(mfas() :: {module(), atom(), list()} | {function(), list()}). + +-type(msg_handler() :: #{puback := fun((_) -> any()) | mfas(), + publish := fun((emqx_types:message()) -> any()) | mfas(), + disconnected := fun(({reason_code(), _Properties :: term()}) -> any()) | mfas() + }). + +-type(option() :: {name, atom()} + | {owner, pid()} + | {msg_handler, msg_handler()} + | {host, host()} + | {hosts, [{host(), inet:port_number()}]} + | {port, inet:port_number()} + | {tcp_opts, [gen_tcp:option()]} + | {ssl, boolean()} + | {ssl_opts, [ssl:ssl_option()]} + | {ws_path, string()} + | {connect_timeout, pos_integer()} + | {bridge_mode, boolean()} + | {clientid, iodata()} + | {clean_start, boolean()} + | {username, iodata()} + | {password, iodata()} + | {proto_ver, v3 | v4 | v5} + | {keepalive, non_neg_integer()} + | {max_inflight, pos_integer()} + | {retry_interval, timeout()} + | {will_topic, iodata()} + | {will_payload, iodata()} + | {will_retain, boolean()} + | {will_qos, qos()} + | {will_props, properties()} + | {auto_ack, boolean()} + | {ack_timeout, pos_integer()} + | {force_ping, boolean()} + | {properties, properties()}). + +-type(maybe(T) :: undefined | T). +-type(topic() :: binary()). +-type(payload() :: iodata()). +-type(packet_id() :: 0..16#FFFF). +-type(reason_code() :: 0..16#FF). +-type(properties() :: #{atom() => term()}). +-type(version() :: ?MQTT_PROTO_V3 + | ?MQTT_PROTO_V4 + | ?MQTT_PROTO_V5). +-type(qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2). +-type(qos_name() :: qos0 | at_most_once | + qos1 | at_least_once | + qos2 | exactly_once). +-type(pubopt() :: {retain, boolean()} + | {qos, qos() | qos_name()}). +-type(subopt() :: {rh, 0 | 1 | 2} + | {rap, boolean()} + | {nl, boolean()} + | {qos, qos() | qos_name()}). + +-type(subscribe_ret() :: + {ok, properties(), [reason_code()]} | {error, term()}). + +-type(client() :: pid() | atom()). + +-opaque(mqtt_msg() :: #mqtt_msg{}). + +-record(state, { + name :: atom(), + owner :: pid(), + msg_handler :: ?NO_MSG_HDLR | msg_handler(), + host :: host(), + port :: inet:port_number(), + hosts :: [{host(), inet:port_number()}], + socket :: inet:socket() | pid(), + sock_opts :: [emqtt_sock:option()|emqtt_ws:option()], + connect_timeout :: pos_integer(), + bridge_mode :: boolean(), + clientid :: binary(), + clean_start :: boolean(), + username :: maybe(binary()), + password :: maybe(binary()), + proto_ver :: version(), + proto_name :: iodata(), + keepalive :: non_neg_integer(), + keepalive_timer :: maybe(reference()), + force_ping :: boolean(), + paused :: boolean(), + will_flag :: boolean(), + will_msg :: mqtt_msg(), + properties :: properties(), + pending_calls :: list(), + subscriptions :: map(), + max_inflight :: infinity | pos_integer(), + inflight :: #{packet_id() => term()}, + awaiting_rel :: map(), + auto_ack :: boolean(), + ack_timeout :: pos_integer(), + ack_timer :: reference(), + retry_interval :: pos_integer(), + retry_timer :: reference(), + session_present :: boolean(), + last_packet_id :: packet_id(), + parse_state :: emqtt_frame:parse_state() + }). + +-record(call, { + id, + from, + req, + ts +}). + +%% Default timeout +-define(DEFAULT_KEEPALIVE, 60). +-define(DEFAULT_RETRY_INTERVAL, 30000). +-define(DEFAULT_ACK_TIMEOUT, 30000). +-define(DEFAULT_CONNECT_TIMEOUT, 60000). + +-define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}). + +-define(WILL_MSG(QoS, Retain, Topic, Props, Payload), + #mqtt_msg{qos = QoS, + retain = Retain, + topic = Topic, + props = Props, + payload = Payload + }). + +-define(NO_CLIENT_ID, <<>>). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +-spec(start_link() -> gen_statem:start_ret()). +start_link() -> start_link([]). + +-spec(start_link(map() | [option()]) -> gen_statem:start_ret()). +start_link(Options) when is_map(Options) -> + start_link(maps:to_list(Options)); +start_link(Options) when is_list(Options) -> + ok = emqtt_props:validate( + proplists:get_value(properties, Options, #{})), + case proplists:get_value(name, Options) of + undefined -> + gen_statem:start_link(?MODULE, [with_owner(Options)], []); + Name when is_atom(Name) -> + gen_statem:start_link({local, Name}, ?MODULE, [with_owner(Options)], []) + end. + +with_owner(Options) -> + case proplists:get_value(owner, Options) of + Owner when is_pid(Owner) -> + Options; + undefined -> + [{owner, self()} | Options] + end. + +-spec(connect(client()) -> {ok, properties()} | {error, term()}). +connect(Client) -> + connect(Client, infinity). +connect(Client, Timeout) -> + gen_statem:call(Client, connect, Timeout). + +-spec(subscribe(client(), topic() | {topic(), qos() | qos_name() | [subopt()]} | [{topic(), qos()}]) + -> subscribe_ret()). +subscribe(Client, Topic) when is_binary(Topic) -> + subscribe(Client, {Topic, ?QOS_0}); +subscribe(Client, {Topic, QoS}) when is_binary(Topic), is_atom(QoS) -> + subscribe(Client, {Topic, ?QOS_I(QoS)}); +subscribe(Client, {Topic, QoS}) when is_binary(Topic), ?IS_QOS(QoS) -> + subscribe(Client, [{Topic, ?QOS_I(QoS)}]); +subscribe(Client, Topics) when is_list(Topics) -> + subscribe(Client, #{}, lists:map( + fun({Topic, QoS}) when is_binary(Topic), is_atom(QoS) -> + {Topic, [{qos, ?QOS_I(QoS)}]}; + ({Topic, QoS}) when is_binary(Topic), ?IS_QOS(QoS) -> + {Topic, [{qos, ?QOS_I(QoS)}]}; + ({Topic, Opts}) when is_binary(Topic), is_list(Opts) -> + {Topic, Opts} + end, Topics)). + +-spec(subscribe(client(), topic(), qos() | qos_name() | [subopt()]) -> + subscribe_ret(); + (client(), properties(), [{topic(), qos() | [subopt()]}]) -> + subscribe_ret()). +subscribe(Client, Topic, QoS) when is_binary(Topic), is_atom(QoS) -> + subscribe(Client, Topic, ?QOS_I(QoS)); +subscribe(Client, Topic, QoS) when is_binary(Topic), ?IS_QOS(QoS) -> + subscribe(Client, Topic, [{qos, QoS}]); +subscribe(Client, Topic, Opts) when is_binary(Topic), is_list(Opts) -> + subscribe(Client, #{}, [{Topic, Opts}]); +subscribe(Client, Properties, Topics) when is_map(Properties), is_list(Topics) -> + Topics1 = [{Topic, parse_subopt(Opts)} || {Topic, Opts} <- Topics], + gen_statem:call(Client, {subscribe, Properties, Topics1}). + +-spec(subscribe(client(), properties(), topic(), qos() | qos_name() | [subopt()]) + -> subscribe_ret()). +subscribe(Client, Properties, Topic, QoS) + when is_map(Properties), is_binary(Topic), is_atom(QoS) -> + subscribe(Client, Properties, Topic, ?QOS_I(QoS)); +subscribe(Client, Properties, Topic, QoS) + when is_map(Properties), is_binary(Topic), ?IS_QOS(QoS) -> + subscribe(Client, Properties, Topic, [{qos, QoS}]); +subscribe(Client, Properties, Topic, Opts) + when is_map(Properties), is_binary(Topic), is_list(Opts) -> + subscribe(Client, Properties, [{Topic, Opts}]). + +parse_subopt(Opts) -> + parse_subopt(Opts, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0}). + +parse_subopt([], Result) -> + Result; +parse_subopt([{rh, I} | Opts], Result) when I >= 0, I =< 2 -> + parse_subopt(Opts, Result#{rh := I}); +parse_subopt([{rap, true} | Opts], Result) -> + parse_subopt(Opts, Result#{rap := 1}); +parse_subopt([{rap, false} | Opts], Result) -> + parse_subopt(Opts, Result#{rap := 0}); +parse_subopt([{nl, true} | Opts], Result) -> + parse_subopt(Opts, Result#{nl := 1}); +parse_subopt([{nl, false} | Opts], Result) -> + parse_subopt(Opts, Result#{nl := 0}); +parse_subopt([{qos, QoS} | Opts], Result) -> + parse_subopt(Opts, Result#{qos := ?QOS_I(QoS)}); +parse_subopt([_ | Opts], Result) -> + parse_subopt(Opts, Result). + +-spec(publish(client(), topic(), payload()) -> ok | {error, term()}). +publish(Client, Topic, Payload) when is_binary(Topic) -> + publish(Client, #mqtt_msg{topic = Topic, qos = ?QOS_0, payload = iolist_to_binary(Payload)}). + +-spec(publish(client(), topic(), payload(), qos() | qos_name() | [pubopt()]) + -> ok | {ok, packet_id()} | {error, term()}). +publish(Client, Topic, Payload, QoS) when is_binary(Topic), is_atom(QoS) -> + publish(Client, Topic, Payload, [{qos, ?QOS_I(QoS)}]); +publish(Client, Topic, Payload, QoS) when is_binary(Topic), ?IS_QOS(QoS) -> + publish(Client, Topic, Payload, [{qos, QoS}]); +publish(Client, Topic, Payload, Opts) when is_binary(Topic), is_list(Opts) -> + publish(Client, Topic, #{}, Payload, Opts). + +-spec(publish(client(), topic(), properties(), payload(), [pubopt()]) + -> ok | {ok, packet_id()} | {error, term()}). +publish(Client, Topic, Properties, Payload, Opts) + when is_binary(Topic), is_map(Properties), is_list(Opts) -> + ok = emqtt_props:validate(Properties), + Retain = proplists:get_bool(retain, Opts), + QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)), + publish(Client, #mqtt_msg{qos = QoS, + retain = Retain, + topic = Topic, + props = Properties, + payload = iolist_to_binary(Payload)}). + +-spec(publish(client(), #mqtt_msg{}) -> ok | {ok, packet_id()} | {error, term()}). +publish(Client, Msg) -> + gen_statem:call(Client, {publish, Msg}). + +-spec(unsubscribe(client(), topic() | [topic()]) -> subscribe_ret()). +unsubscribe(Client, Topic) when is_binary(Topic) -> + unsubscribe(Client, [Topic]); +unsubscribe(Client, Topics) when is_list(Topics) -> + unsubscribe(Client, #{}, Topics). + +-spec(unsubscribe(client(), properties(), topic() | [topic()]) -> subscribe_ret()). +unsubscribe(Client, Properties, Topic) when is_map(Properties), is_binary(Topic) -> + unsubscribe(Client, Properties, [Topic]); +unsubscribe(Client, Properties, Topics) when is_map(Properties), is_list(Topics) -> + gen_statem:call(Client, {unsubscribe, Properties, Topics}). + +-spec(ping(client()) -> pong). +ping(Client) -> + gen_statem:call(Client, ping). + +-spec(disconnect(client()) -> ok). +disconnect(Client) -> + disconnect(Client, ?RC_SUCCESS). + +-spec(disconnect(client(), reason_code()) -> ok). +disconnect(Client, ReasonCode) -> + disconnect(Client, ReasonCode, #{}). + +-spec(disconnect(client(), reason_code(), properties()) -> ok). +disconnect(Client, ReasonCode, Properties) -> + gen_statem:call(Client, {disconnect, ReasonCode, Properties}). + +%%-------------------------------------------------------------------- +%% For test cases +%%-------------------------------------------------------------------- + +puback(Client, PacketId) when is_integer(PacketId) -> + puback(Client, PacketId, ?RC_SUCCESS). +puback(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> + puback(Client, PacketId, ReasonCode, #{}). +puback(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> + gen_statem:cast(Client, {puback, PacketId, ReasonCode, Properties}). + +pubrec(Client, PacketId) when is_integer(PacketId) -> + pubrec(Client, PacketId, ?RC_SUCCESS). +pubrec(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> + pubrec(Client, PacketId, ReasonCode, #{}). +pubrec(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> + gen_statem:cast(Client, {pubrec, PacketId, ReasonCode, Properties}). + +pubrel(Client, PacketId) when is_integer(PacketId) -> + pubrel(Client, PacketId, ?RC_SUCCESS). +pubrel(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> + pubrel(Client, PacketId, ReasonCode, #{}). +pubrel(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> + gen_statem:cast(Client, {pubrel, PacketId, ReasonCode, Properties}). + +pubcomp(Client, PacketId) when is_integer(PacketId) -> + pubcomp(Client, PacketId, ?RC_SUCCESS). +pubcomp(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> + pubcomp(Client, PacketId, ReasonCode, #{}). +pubcomp(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> + gen_statem:cast(Client, {pubcomp, PacketId, ReasonCode, Properties}). + +subscriptions(Client) -> + gen_statem:call(Client, subscriptions). + +info(Client) -> + gen_statem:call(Client, info). + +stop(Client) -> + gen_statem:call(Client, stop). + +pause(Client) -> + gen_statem:call(Client, pause). + +resume(Client) -> + gen_statem:call(Client, resume). + +%%-------------------------------------------------------------------- +%% gen_statem callbacks +%%-------------------------------------------------------------------- + +init([Options]) -> + process_flag(trap_exit, true), + ClientId = case {proplists:get_value(proto_ver, Options, v4), + proplists:get_value(clientid, Options)} of + {v5, undefined} -> ?NO_CLIENT_ID; + {_ver, undefined} -> random_client_id(); + {_ver, Id} -> iolist_to_binary(Id) + end, + State = init(Options, #state{host = {127,0,0,1}, + port = 1883, + hosts = [], + sock_opts = [], + bridge_mode = false, + clientid = ClientId, + clean_start = true, + proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>, + keepalive = ?DEFAULT_KEEPALIVE, + force_ping = false, + paused = false, + will_flag = false, + will_msg = #mqtt_msg{}, + pending_calls = [], + subscriptions = #{}, + max_inflight = infinity, + inflight = #{}, + awaiting_rel = #{}, + properties = #{}, + auto_ack = true, + ack_timeout = ?DEFAULT_ACK_TIMEOUT, + retry_interval = ?DEFAULT_RETRY_INTERVAL, + connect_timeout = ?DEFAULT_CONNECT_TIMEOUT, + last_packet_id = 1 + }), + {ok, initialized, init_parse_state(State)}. + +random_client_id() -> + rand:seed(exsplus, erlang:timestamp()), + I1 = rand:uniform(round(math:pow(2, 48))) - 1, + I2 = rand:uniform(round(math:pow(2, 32))) - 1, + {ok, Host} = inet:gethostname(), + RandId = io_lib:format("~12.16.0b~8.16.0b", [I1, I2]), + iolist_to_binary(["emqtt-", Host, "-", RandId]). + +init([], State) -> + State; +init([{name, Name} | Opts], State) -> + init(Opts, State#state{name = Name}); +init([{owner, Owner} | Opts], State) when is_pid(Owner) -> + link(Owner), + init(Opts, State#state{owner = Owner}); +init([{msg_handler, Hdlr} | Opts], State) -> + init(Opts, State#state{msg_handler = Hdlr}); +init([{host, Host} | Opts], State) -> + init(Opts, State#state{host = Host}); +init([{port, Port} | Opts], State) -> + init(Opts, State#state{port = Port}); +init([{hosts, Hosts} | Opts], State) -> + Hosts1 = + lists:foldl(fun({Host, Port}, Acc) -> + [{Host, Port}|Acc]; + (Host, Acc) -> + [{Host, 1883}|Acc] + end, [], Hosts), + init(Opts, State#state{hosts = Hosts1}); +init([{tcp_opts, TcpOpts} | Opts], State = #state{sock_opts = SockOpts}) -> + init(Opts, State#state{sock_opts = merge_opts(SockOpts, TcpOpts)}); +init([{ssl, EnableSsl} | Opts], State) -> + case lists:keytake(ssl_opts, 1, Opts) of + {value, SslOpts, WithOutSslOpts} -> + init([SslOpts, {ssl, EnableSsl}| WithOutSslOpts], State); + false -> + init([{ssl_opts, []}, {ssl, EnableSsl}| Opts], State) + end; +init([{ssl_opts, SslOpts} | Opts], State = #state{sock_opts = SockOpts}) -> + case lists:keytake(ssl, 1, Opts) of + {value, {ssl, true}, WithOutEnableSsl} -> + ok = ssl:start(), + SockOpts1 = merge_opts(SockOpts, [{ssl_opts, SslOpts}]), + init(WithOutEnableSsl, State#state{sock_opts = SockOpts1}); + {value, {ssl, false}, WithOutEnableSsl} -> + init(WithOutEnableSsl, State); + false -> + init(Opts, State) + end; +init([{ws_path, Path} | Opts], State = #state{sock_opts = SockOpts}) -> + init(Opts, State#state{sock_opts = [{ws_path, Path}|SockOpts]}); +init([{clientid, ClientId} | Opts], State) -> + init(Opts, State#state{clientid = iolist_to_binary(ClientId)}); +init([{clean_start, CleanStart} | Opts], State) when is_boolean(CleanStart) -> + init(Opts, State#state{clean_start = CleanStart}); +init([{username, Username} | Opts], State) -> + init(Opts, State#state{username = iolist_to_binary(Username)}); +init([{password, Password} | Opts], State) -> + init(Opts, State#state{password = iolist_to_binary(Password)}); +init([{keepalive, Secs} | Opts], State) -> + init(Opts, State#state{keepalive = Secs}); +init([{proto_ver, v3} | Opts], State) -> + init(Opts, State#state{proto_ver = ?MQTT_PROTO_V3, + proto_name = <<"MQIsdp">>}); +init([{proto_ver, v4} | Opts], State) -> + init(Opts, State#state{proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>}); +init([{proto_ver, v5} | Opts], State) -> + init(Opts, State#state{proto_ver = ?MQTT_PROTO_V5, + proto_name = <<"MQTT">>}); +init([{will_topic, Topic} | Opts], State = #state{will_msg = WillMsg}) -> + WillMsg1 = init_will_msg({topic, Topic}, WillMsg), + init(Opts, State#state{will_flag = true, will_msg = WillMsg1}); +init([{will_props, Properties} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({props, Properties}, WillMsg)}); +init([{will_payload, Payload} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({payload, Payload}, WillMsg)}); +init([{will_retain, Retain} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({retain, Retain}, WillMsg)}); +init([{will_qos, QoS} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({qos, QoS}, WillMsg)}); +init([{connect_timeout, Timeout}| Opts], State) -> + init(Opts, State#state{connect_timeout = timer:seconds(Timeout)}); +init([{ack_timeout, Timeout}| Opts], State) -> + init(Opts, State#state{ack_timeout = timer:seconds(Timeout)}); +init([force_ping | Opts], State) -> + init(Opts, State#state{force_ping = true}); +init([{force_ping, ForcePing} | Opts], State) when is_boolean(ForcePing) -> + init(Opts, State#state{force_ping = ForcePing}); +init([{properties, Properties} | Opts], State = #state{properties = InitProps}) -> + init(Opts, State#state{properties = maps:merge(InitProps, Properties)}); +init([{max_inflight, infinity} | Opts], State) -> + init(Opts, State#state{max_inflight = infinity, + inflight = #{}}); +init([{max_inflight, I} | Opts], State) when is_integer(I) -> + init(Opts, State#state{max_inflight = I, + inflight = #{}}); +init([auto_ack | Opts], State) -> + init(Opts, State#state{auto_ack = true}); +init([{auto_ack, AutoAck} | Opts], State) when is_boolean(AutoAck) -> + init(Opts, State#state{auto_ack = AutoAck}); +init([{retry_interval, I} | Opts], State) -> + init(Opts, State#state{retry_interval = timer:seconds(I)}); +init([{bridge_mode, Mode} | Opts], State) when is_boolean(Mode) -> + init(Opts, State#state{bridge_mode = Mode}); +init([_Opt | Opts], State) -> + init(Opts, State). + +init_will_msg({topic, Topic}, WillMsg) -> + WillMsg#mqtt_msg{topic = iolist_to_binary(Topic)}; +init_will_msg({props, Props}, WillMsg) -> + WillMsg#mqtt_msg{props = Props}; +init_will_msg({payload, Payload}, WillMsg) -> + WillMsg#mqtt_msg{payload = iolist_to_binary(Payload)}; +init_will_msg({retain, Retain}, WillMsg) when is_boolean(Retain) -> + WillMsg#mqtt_msg{retain = Retain}; +init_will_msg({qos, QoS}, WillMsg) -> + WillMsg#mqtt_msg{qos = ?QOS_I(QoS)}. + +init_parse_state(State = #state{proto_ver = Ver, properties = Properties}) -> + MaxSize = maps:get('Maximum-Packet-Size', Properties, ?MAX_PACKET_SIZE), + ParseState = emqtt_frame:initial_parse_state( + #{max_size => MaxSize, version => Ver}), + State#state{parse_state = ParseState}. + +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). + +callback_mode() -> state_functions. + +initialized({call, From}, connect, State = #state{sock_opts = SockOpts, connect_timeout = Timeout}) -> + case sock_connect(hosts(State), SockOpts, Timeout) of + {ok, Sock} -> + case mqtt_connect(run_sock(State#state{socket = Sock})) of + {ok, NewState} -> + {next_state, waiting_for_connack, + add_call(new_call(connect, From), NewState), [Timeout]}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + Error = {error, Reason} -> + {stop_and_reply, {shutdown, Reason}, [{reply, From, Error}]} + end; + +initialized(EventType, EventContent, State) -> + handle_event(EventType, EventContent, initialized, State). + +mqtt_connect(State = #state{clientid = ClientId, + clean_start = CleanStart, + bridge_mode = IsBridge, + username = Username, + password = Password, + proto_ver = ProtoVer, + proto_name = ProtoName, + keepalive = KeepAlive, + will_flag = WillFlag, + will_msg = WillMsg, + properties = Properties}) -> + ?WILL_MSG(WillQoS, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, + ConnProps = emqtt_props:filter(?CONNECT, Properties), + send(?CONNECT_PACKET( + #mqtt_packet_connect{proto_ver = ProtoVer, + proto_name = ProtoName, + is_bridge = IsBridge, + clean_start = CleanStart, + will_flag = WillFlag, + will_qos = WillQoS, + will_retain = WillRetain, + keepalive = KeepAlive, + properties = ConnProps, + clientid = ClientId, + will_props = WillProps, + will_topic = WillTopic, + will_payload = WillPayload, + username = Username, + password = Password}), State). + +waiting_for_connack(cast, ?CONNACK_PACKET(?RC_SUCCESS, + SessPresent, + Properties), + State = #state{properties = AllProps, + clientid = ClientId}) -> + case take_call(connect, State) of + {value, #call{from = From}, State1} -> + AllProps1 = case Properties of + undefined -> AllProps; + _ -> maps:merge(AllProps, Properties) + end, + Reply = {ok, Properties}, + State2 = State1#state{clientid = assign_id(ClientId, AllProps1), + properties = AllProps1, + session_present = SessPresent}, + {next_state, connected, ensure_keepalive_timer(State2), + [{reply, From, Reply}]}; + false -> + {stop, bad_connack} + end; + +waiting_for_connack(cast, ?CONNACK_PACKET(ReasonCode, + _SessPresent, + Properties), + State = #state{proto_ver = ProtoVer}) -> + Reason = reason_code_name(ReasonCode, ProtoVer), + case take_call(connect, State) of + {value, #call{from = From}, _State} -> + Reply = {error, {Reason, Properties}}, + {stop_and_reply, {shutdown, Reason}, [{reply, From, Reply}]}; + false -> {stop, connack_error} + end; + +waiting_for_connack(timeout, _Timeout, State) -> + case take_call(connect, State) of + {value, #call{from = From}, _State} -> + Reply = {error, connack_timeout}, + {stop_and_reply, connack_timeout, [{reply, From, Reply}]}; + false -> {stop, connack_timeout} + end; + +waiting_for_connack(EventType, EventContent, State) -> + case take_call(connect, State) of + {value, #call{from = From}, _State} -> + case handle_event(EventType, EventContent, waiting_for_connack, State) of + {stop, Reason, State} -> + Reply = {error, {Reason, EventContent}}, + {stop_and_reply, Reason, [{reply, From, Reply}]}; + StateCallbackResult -> + StateCallbackResult + end; + false -> + {stop, connack_timeout} + end. + +connected({call, From}, subscriptions, #state{subscriptions = Subscriptions}) -> + {keep_state_and_data, [{reply, From, maps:to_list(Subscriptions)}]}; + +connected({call, From}, info, State) -> + Info = lists:zip(record_info(fields, state), tl(tuple_to_list(State))), + {keep_state_and_data, [{reply, From, Info}]}; + +connected({call, From}, pause, State) -> + {keep_state, State#state{paused = true}, [{reply, From, ok}]}; + +connected({call, From}, resume, State) -> + {keep_state, State#state{paused = false}, [{reply, From, ok}]}; + +connected({call, From}, clientid, #state{clientid = ClientId}) -> + {keep_state_and_data, [{reply, From, ClientId}]}; + +connected({call, From}, SubReq = {subscribe, Properties, Topics}, + State = #state{last_packet_id = PacketId, subscriptions = Subscriptions}) -> + case send(?SUBSCRIBE_PACKET(PacketId, Properties, Topics), State) of + {ok, NewState} -> + Call = new_call({subscribe, PacketId}, From, SubReq), + Subscriptions1 = + lists:foldl(fun({Topic, Opts}, Acc) -> + maps:put(Topic, Opts, Acc) + end, Subscriptions, Topics), + {keep_state, ensure_ack_timer(add_call(Call,NewState#state{subscriptions = Subscriptions1}))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, {publish, Msg = #mqtt_msg{qos = ?QOS_0}}, State) -> + case send(Msg, State) of + {ok, NewState} -> + {keep_state, NewState, [{reply, From, ok}]}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, {publish, Msg = #mqtt_msg{qos = QoS}}, + State = #state{inflight = Inflight, last_packet_id = PacketId}) + when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2) -> + Msg1 = Msg#mqtt_msg{packet_id = PacketId}, + case send(Msg1, State) of + {ok, NewState} -> + Inflight1 = maps:put(PacketId, {publish, Msg1, os:timestamp()}, Inflight), + State1 = ensure_retry_timer(NewState#state{inflight = Inflight1}), + Actions = [{reply, From, {ok, PacketId}}], + case is_inflight_full(State1) of + true -> {next_state, inflight_full, State1, Actions}; + false -> {keep_state, State1, Actions} + end; + {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, {error, {PacketId, Reason}}}]} + end; + +connected({call, From}, UnsubReq = {unsubscribe, Properties, Topics}, + State = #state{last_packet_id = PacketId}) -> + case send(?UNSUBSCRIBE_PACKET(PacketId, Properties, Topics), State) of + {ok, NewState} -> + Call = new_call({unsubscribe, PacketId}, From, UnsubReq), + {keep_state, ensure_ack_timer(add_call(Call, NewState))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, ping, State) -> + case send(?PACKET(?PINGREQ), State) of + {ok, NewState} -> + Call = new_call(ping, From), + {keep_state, ensure_ack_timer(add_call(Call, NewState))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, {disconnect, ReasonCode, Properties}, State) -> + case send(?DISCONNECT_PACKET(ReasonCode, Properties), State) of + {ok, NewState} -> + {stop_and_reply, normal, [{reply, From, ok}], NewState}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected(cast, {puback, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBACK_PACKET(PacketId, ReasonCode, Properties), State); + +connected(cast, {pubrec, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBREC_PACKET(PacketId, ReasonCode, Properties), State); + +connected(cast, {pubrel, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBREL_PACKET(PacketId, ReasonCode, Properties), State); + +connected(cast, {pubcomp, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), State); + +connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), #state{paused = true}) -> + keep_state_and_data; + +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> + {keep_state, deliver(packet_to_msg(Packet), State)}; + +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> + publish_process(?QOS_1, Packet, State); + +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, _PacketId), State) -> + publish_process(?QOS_2, Packet, State); + +connected(cast, ?PUBACK_PACKET(_PacketId, _ReasonCode, _Properties) = PubAck, State) -> + {keep_state, delete_inflight(PubAck, State)}; + +connected(cast, ?PUBREC_PACKET(PacketId), State = #state{inflight = Inflight}) -> + NState = case maps:find(PacketId, Inflight) of + {ok, {publish, _Msg, _Ts}} -> + Inflight1 = maps:put(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight), + State#state{inflight = Inflight1}; + {ok, {pubrel, _Ref, _Ts}} -> + lager:notice("[emqtt] Duplicated PUBREC Packet: ~p, client_id: ~p", [PacketId, State#state.clientid]), + State; + error -> + lager:warning("[emqtt] Unexpected PUBREC Packet: ~p, client_id: ~p", [PacketId, State#state.clientid]), + State + end, + send_puback(?PUBREL_PACKET(PacketId), NState); + +%%TODO::... if auto_ack is false, should we take PacketId from the map? +connected(cast, ?PUBREL_PACKET(PacketId), + State = #state{awaiting_rel = AwaitingRel, auto_ack = AutoAck}) -> + case maps:take(PacketId, AwaitingRel) of + {Packet, AwaitingRel1} -> + NewState = deliver(packet_to_msg(Packet), State#state{awaiting_rel = AwaitingRel1}), + case AutoAck of + true -> send_puback(?PUBCOMP_PACKET(PacketId), NewState); + false -> {keep_state, NewState} + end; + error -> + lager:warning("[emqtt] Unexpected PUBREL: ~p, client_id: ~p", [PacketId, State#state.clientid]), + keep_state_and_data + end; + +connected(cast, ?PUBCOMP_PACKET(_PacketId, _ReasonCode, _Properties) = PubComp, State) -> + {keep_state, delete_inflight(PubComp, State)}; + +connected(cast, ?SUBACK_PACKET(PacketId, Properties, ReasonCodes), + State = #state{subscriptions = _Subscriptions}) -> + case take_call({subscribe, PacketId}, State) of + {value, #call{from = From}, NewState} -> + %%TODO: Merge reason codes to subscriptions? + Reply = {ok, Properties, ReasonCodes}, + {keep_state, NewState, [{reply, From, Reply}]}; + false -> + keep_state_and_data + end; + +connected(cast, ?UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), + State = #state{subscriptions = Subscriptions}) -> + case take_call({unsubscribe, PacketId}, State) of + {value, #call{from = From, req = {_, _, Topics}}, NewState} -> + Subscriptions1 = + lists:foldl(fun(Topic, Acc) -> + maps:remove(Topic, Acc) + end, Subscriptions, Topics), + {keep_state, NewState#state{subscriptions = Subscriptions1}, + [{reply, From, {ok, Properties, ReasonCodes}}]}; + false -> + keep_state_and_data + end; + +connected(cast, ?PACKET(?PINGRESP), #state{pending_calls = []}) -> + keep_state_and_data; +connected(cast, ?PACKET(?PINGRESP), State) -> + case take_call(ping, State) of + {value, #call{from = From}, NewState} -> + {keep_state, NewState, [{reply, From, pong}]}; + false -> + keep_state_and_data + end; + +connected(cast, ?DISCONNECT_PACKET(ReasonCode, Properties), State) -> + {stop, {disconnected, ReasonCode, Properties}, State}; + +connected(info, {timeout, _TRef, keepalive}, State = #state{force_ping = true}) -> + case send(?PACKET(?PINGREQ), State) of + {ok, NewState} -> + {keep_state, ensure_keepalive_timer(NewState)}; + Error -> {stop, Error} + end; + +connected(info, {timeout, TRef, keepalive}, State = #state{socket = Sock, paused = Paused, keepalive_timer = TRef}) -> + case (not Paused) andalso should_ping(Sock) of + true -> + case send(?PACKET(?PINGREQ), State) of + {ok, NewState} -> + {ok, [{send_oct, Val}]} = emqtt_sock:getstat(Sock, [send_oct]), + put(send_oct, Val), + {keep_state, ensure_keepalive_timer(NewState), [hibernate]}; + Error -> {stop, Error} + end; + false -> + {keep_state, ensure_keepalive_timer(State), [hibernate]}; + {error, Reason} -> + {stop, Reason} + end; + +connected(info, {timeout, TRef, ack}, State = #state{ack_timer = TRef, + ack_timeout = Timeout, + pending_calls = Calls}) -> + NewState = State#state{ack_timer = undefined, + pending_calls = timeout_calls(Timeout, Calls)}, + {keep_state, ensure_ack_timer(NewState)}; + +connected(info, {timeout, TRef, retry}, State = #state{retry_timer = TRef, inflight = Inflight}) -> + case maps:size(Inflight) == 0 of + true -> {keep_state, State#state{retry_timer = undefined}}; + false -> retry_send(State) + end; + +connected(EventType, EventContent, Data) -> + handle_event(EventType, EventContent, connected, Data). + +inflight_full({call, _From}, {publish, #mqtt_msg{qos = QoS}}, _State) when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2) -> + {keep_state_and_data, [postpone]}; +inflight_full(cast, ?PUBACK_PACKET(_PacketId, _ReasonCode, _Properties) = PubAck, State) -> + delete_inflight_when_full(PubAck, State); +inflight_full(cast, ?PUBCOMP_PACKET(_PacketId, _ReasonCode, _Properties) = PubComp, State) -> + delete_inflight_when_full(PubComp, State); +inflight_full(EventType, EventContent, Data) -> + %% inflight_full is a sub-state of connected state, + %% delegate all other events to connected state. + connected(EventType, EventContent, Data). + +handle_event({call, From}, stop, _StateName, _State) -> + {stop_and_reply, normal, [{reply, From, ok}]}; + +handle_event(info, {TcpOrSsL, _Sock, Data}, _StateName, State) when TcpOrSsL =:= tcp; TcpOrSsL =:= ssl -> + lager:debug("[emqtt] RECV Data: ~p, client_id: ~p", [Data, State#state.clientid]), + process_incoming(Data, [], run_sock(State)); + +handle_event(info, {Error, _Sock, Reason}, _StateName, State) when Error =:= tcp_error; Error =:= ssl_error -> + lager:error("[emqtt] The connection error occured ~p, reason:~p, client_id: ~p", [Error, Reason, State#state.clientid]), + {stop, {shutdown, Reason}, State}; + +handle_event(info, {Closed, _Sock}, _StateName, State) when Closed =:= tcp_closed; Closed =:= ssl_closed -> + lager:debug("[emqtt] sokcet closed: ~p, client_id: ~p", [Closed, State#state.clientid]), + {stop, {shutdown, Closed}, State}; + +handle_event(info, {'EXIT', Owner, Reason}, _, State = #state{owner = Owner}) -> + lager:warning("[emqtt] Got EXIT from owner, Reason: ~p, client_id: ~p", [Reason, State#state.clientid]), + {stop, {shutdown, Reason}, State}; + +handle_event(info, {inet_reply, _Sock, ok}, _, _State) -> + keep_state_and_data; + +handle_event(info, {inet_reply, _Sock, {error, Reason}}, _, State) -> + lager:error("[emqtt] Got tcp error: ~p, client_id: ~p", [Reason, State#state.clientid]), + {stop, {shutdown, Reason}, State}; + +handle_event(info, EventContent = {'EXIT', Pid, Reason}, StateName, State) -> + lager:warning("[emqtt] State: ~s, Unexpected Event: (info, ~p), from pid: ~p, client_id: ~p", [StateName, EventContent, Pid, State#state.clientid]), + {stop, {shutdown, Reason}, State}; + +handle_event(EventType, EventContent, StateName, State) -> + lager:error("[emqtt] State: ~s, Unexpected Event: (~p, ~p), client_id: ~p", [StateName, EventType, EventContent, State#state.clientid]), + keep_state_and_data. + +%% Mandatory callback functions +terminate(Reason, _StateName, State = #state{socket = Socket}) -> + case Reason of + {disconnected, ReasonCode, Properties} -> + %% backward compatible + ok = eval_msg_handler(State, disconnected, {ReasonCode, Properties}); + _ -> + ok = eval_msg_handler(State, disconnected, Reason) + end, + case Socket =:= undefined of + true -> ok; + _ -> emqtt_sock:close(Socket) + end. + +code_change(_Vsn, State, Data, _Extra) -> + {ok, State, Data}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +should_ping(Sock) -> + case emqtt_sock:getstat(Sock, [send_oct]) of + {ok, [{send_oct, Val}]} -> + OldVal = get(send_oct), put(send_oct, Val), + OldVal == undefined orelse OldVal == Val; + Error = {error, _Reason} -> + Error + end. + +is_inflight_full(#state{max_inflight = infinity}) -> + false; +is_inflight_full(#state{max_inflight = MaxLimit, inflight = Inflight}) -> + maps:size(Inflight) >= MaxLimit. + +delete_inflight(?PUBACK_PACKET(PacketId, ReasonCode, Properties), + State = #state{inflight = Inflight}) -> + case maps:find(PacketId, Inflight) of + {ok, {publish, #mqtt_msg{packet_id = PacketId}, _Ts}} -> + ok = eval_msg_handler(State, puback, #{packet_id => PacketId, + reason_code => ReasonCode, + properties => Properties}), + State#state{inflight = maps:remove(PacketId, Inflight)}; + error -> + lager:warning("[emqtt] Unexpected PUBACK: ~p, client_id: ~p", [PacketId, State#state.clientid]), + State + end; +delete_inflight(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), + State = #state{inflight = Inflight}) -> + case maps:find(PacketId, Inflight) of + {ok, {pubrel, _PacketId, _Ts}} -> + ok = eval_msg_handler(State, puback, #{packet_id => PacketId, + reason_code => ReasonCode, + properties => Properties}), + State#state{inflight = maps:remove(PacketId, Inflight)}; + error -> + lager:warning("[emqtt] Unexpected PUBCOMP Packet: ~p, client_id: ~p", [PacketId, State#state.clientid]), + State + end. + +delete_inflight_when_full(Packet, State) -> + State1 = delete_inflight(Packet, State), + case is_inflight_full(State1) of + true -> + {keep_state, State1}; + false -> + {next_state, connected, State1} + end. + +assign_id(?NO_CLIENT_ID, Props) -> + case maps:find('Assigned-Client-Identifier', Props) of + {ok, Value} -> + Value; + _ -> + error(bad_client_id) + end; +assign_id(Id, _Props) -> + Id. + +publish_process(?QOS_1, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), + State0 = #state{auto_ack = AutoAck}) -> + State = deliver(packet_to_msg(Packet), State0), + case AutoAck of + true -> + send_puback(?PUBACK_PACKET(PacketId), State); + false -> + {keep_state, State} + end; +publish_process(?QOS_2, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), + State = #state{awaiting_rel = AwaitingRel}) -> + case send_puback(?PUBREC_PACKET(PacketId), State) of + {keep_state, NewState} -> + AwaitingRel1 = maps:put(PacketId, Packet, AwaitingRel), + {keep_state, NewState#state{awaiting_rel = AwaitingRel1}}; + Stop -> + Stop + end. + +ensure_keepalive_timer(State = ?PROPERTY('Server-Keep-Alive', Secs)) -> + ensure_keepalive_timer(timer:seconds(Secs), State#state{keepalive = Secs}); +ensure_keepalive_timer(State = #state{keepalive = 0}) -> + State; +ensure_keepalive_timer(State = #state{keepalive = I}) -> + ensure_keepalive_timer(timer:seconds(I), State). +ensure_keepalive_timer(I, State) when is_integer(I) -> + State#state{keepalive_timer = erlang:start_timer(I, self(), keepalive)}. + +new_call(Id, From) -> + new_call(Id, From, undefined). +new_call(Id, From, Req) -> + #call{id = Id, from = From, req = Req, ts = os:timestamp()}. + +add_call(Call, Data = #state{pending_calls = Calls}) -> + Data#state{pending_calls = [Call | Calls]}. + +take_call(Id, Data = #state{pending_calls = Calls}) -> + case lists:keytake(Id, #call.id, Calls) of + {value, Call, Left} -> + {value, Call, Data#state{pending_calls = Left}}; + false -> false + end. + +timeout_calls(Timeout, Calls) -> + timeout_calls(os:timestamp(), Timeout, Calls). +timeout_calls(Now, Timeout, Calls) -> + lists:foldl(fun(C = #call{from = From, ts = Ts}, Acc) -> + case (timer:now_diff(Now, Ts) div 1000) >= Timeout of + true -> + gen_statem:reply(From, {error, ack_timeout}), + Acc; + false -> [C | Acc] + end + end, [], Calls). + +ensure_ack_timer(State = #state{ack_timer = undefined, + ack_timeout = Timeout, + pending_calls = Calls}) when length(Calls) > 0 -> + State#state{ack_timer = erlang:start_timer(Timeout, self(), ack)}; +ensure_ack_timer(State) -> State. + +ensure_retry_timer(State = #state{retry_interval = Interval}) -> + do_ensure_retry_timer(Interval, State). + +do_ensure_retry_timer(Interval, State = #state{retry_timer = undefined}) + when Interval > 0 -> + State#state{retry_timer = erlang:start_timer(Interval, self(), retry)}; +do_ensure_retry_timer(_Interval, State) -> + State. + +retry_send(State = #state{inflight = Inflight}) -> + SortFun = fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end, + Msgs = lists:sort(SortFun, maps:values(Inflight)), + retry_send(Msgs, os:timestamp(), State ). + +retry_send([], _Now, State) -> + {keep_state, ensure_retry_timer(State)}; +retry_send([{Type, Msg, Ts} | Msgs], Now, State = #state{retry_interval = Interval}) -> + Diff = timer:now_diff(Now, Ts) div 1000, %% micro -> ms + case (Diff >= Interval) of + true -> case retry_send(Type, Msg, Now, State) of + {ok, NewState} -> retry_send(Msgs, Now, NewState); + {error, Error} -> {stop, Error} + end; + false -> {keep_state, do_ensure_retry_timer(Interval - Diff, State)} + end. + +retry_send(publish, Msg = #mqtt_msg{qos = QoS, packet_id = PacketId}, + Now, State = #state{inflight = Inflight}) -> + Msg1 = Msg#mqtt_msg{dup = (QoS =:= ?QOS_1)}, + case send(Msg1, State) of + {ok, NewState} -> + Inflight1 = maps:put(PacketId, {publish, Msg1, Now}, Inflight), + {ok, NewState#state{inflight = Inflight1}}; + Error = {error, _Reason} -> + Error + end; +retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) -> + case send(?PUBREL_PACKET(PacketId), State) of + {ok, NewState} -> + Inflight1 = maps:put(PacketId, {pubrel, PacketId, Now}, Inflight), + {ok, NewState#state{inflight = Inflight1}}; + Error = {error, _Reason} -> + Error + end. + +deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}, + State) -> + Msg = #{qos => QoS, dup => Dup, retain => Retain, packet_id => PacketId, + topic => Topic, properties => Props, payload => Payload, + client_pid => self()}, + ok = eval_msg_handler(State, publish, Msg), + State. + +eval_msg_handler(#state{msg_handler = ?NO_MSG_HDLR, + owner = Owner}, + disconnected, {ReasonCode, Properties}) when is_integer(ReasonCode) -> + %% Special handling for disconnected message when there is no handler callback + Owner ! {disconnected, ReasonCode, Properties}, + ok; +eval_msg_handler(#state{msg_handler = ?NO_MSG_HDLR}, + disconnected, _OtherReason) -> + %% do nothing to be backward compatible + ok; +eval_msg_handler(#state{msg_handler = ?NO_MSG_HDLR, + owner = Owner}, Kind, Msg) -> + Owner ! {Kind, Msg}, + ok; +eval_msg_handler(#state{msg_handler = Handler}, Kind, Msg) -> + F = maps:get(Kind, Handler), + _ = apply_handler_function(F, Msg), + ok. + +apply_handler_function(F, Msg) + when is_function(F) -> + erlang:apply(F, [Msg]); +apply_handler_function({F, A}, Msg) + when is_function(F), + is_list(A) -> + erlang:apply(F, [Msg] ++ A); +apply_handler_function({M, F, A}, Msg) + when is_atom(M), + is_atom(F), + is_list(A) -> + erlang:apply(M, F, [Msg] ++ A). + +packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + dup = Dup, + qos = QoS, + retain = R}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId, + properties = Props}, + payload = Payload}) -> + #mqtt_msg{qos = QoS, retain = R, dup = Dup, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}. + +msg_to_packet(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}) -> + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = QoS, + retain = Retain, + dup = Dup}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId, + properties = Props}, + payload = Payload}. + +%%-------------------------------------------------------------------- +%% Socket Connect/Send + +sock_connect(Hosts, SockOpts, Timeout) -> + sock_connect(Hosts, SockOpts, Timeout, {error, no_hosts}). + +sock_connect([], _SockOpts, _Timeout, LastErr) -> + LastErr; +sock_connect([{Host, Port} | Hosts], SockOpts, Timeout, _LastErr) -> + case emqtt_sock:connect(Host, Port, SockOpts, Timeout) of + {ok, SockOrPid} -> + {ok, SockOrPid}; + Error = {error, _Reason} -> + sock_connect(Hosts, SockOpts, Timeout, Error) + end. + +hosts(#state{hosts = [], host = Host, port = Port}) -> + [{Host, Port}]; +hosts(#state{hosts = Hosts}) -> Hosts. + +send_puback(Packet, State) -> + case send(Packet, State) of + {ok, NewState} -> {keep_state, NewState}; + {error, Reason} -> {stop, {shutdown, Reason}} + end. + +send(Msg, State) when is_record(Msg, mqtt_msg) -> + send(msg_to_packet(Msg), State); + +send(Packet, State = #state{socket = Sock, proto_ver = Ver}) + when is_record(Packet, mqtt_packet) -> + Data = emqtt_frame:serialize(Packet, Ver), + lager:debug("[emqtt] SEND Data: ~1000p, client_id: ~p", [Packet, State#state.clientid]), + case emqtt_sock:send(Sock, Data) of + ok -> + {ok, bump_last_packet_id(State)}; + Error -> + Error + end. + +run_sock(State = #state{socket = Sock}) -> + emqtt_sock:setopts(Sock, [{active, once}]), State. + +%%-------------------------------------------------------------------- +%% Process incomming + +process_incoming(<<>>, Packets, State) -> + {keep_state, State, next_events(Packets)}; + +process_incoming(Bytes, Packets, State = #state{parse_state = ParseState}) -> + try emqtt_frame:parse(Bytes, ParseState) of + {ok, Packet, Rest, NParseState} -> + process_incoming(Rest, [Packet|Packets], State#state{parse_state = NParseState}); + {more, NParseState} -> + {keep_state, State#state{parse_state = NParseState}, next_events(Packets)} + catch + error:Error -> + {stop, Error} + end. + +-compile({inline, [next_events/1]}). +next_events([]) -> []; +next_events([Packet]) -> + {next_event, cast, Packet}; +next_events(Packets) -> + [{next_event, cast, Packet} || Packet <- lists:reverse(Packets)]. + +%%-------------------------------------------------------------------- +%% packet_id generation + +bump_last_packet_id(State = #state{last_packet_id = Id}) -> + State#state{last_packet_id = next_packet_id(Id)}. + +-spec next_packet_id(packet_id()) -> packet_id(). +next_packet_id(?MAX_PACKET_ID) -> + 1; +next_packet_id(Id) -> + Id + 1. + +%%-------------------------------------------------------------------- +%% ReasonCode Name + +reason_code_name(I, Ver) when Ver >= ?MQTT_PROTO_V5 -> + reason_code_name(I); +reason_code_name(0, _Ver) -> connection_accepted; +reason_code_name(1, _Ver) -> unacceptable_protocol_version; +reason_code_name(2, _Ver) -> client_identifier_not_valid; +reason_code_name(3, _Ver) -> server_unavaliable; +reason_code_name(4, _Ver) -> malformed_username_or_password; +reason_code_name(5, _Ver) -> unauthorized_client; +reason_code_name(_, _Ver) -> unknown_error. + +reason_code_name(16#00) -> success; +reason_code_name(16#01) -> granted_qos1; +reason_code_name(16#02) -> granted_qos2; +reason_code_name(16#04) -> disconnect_with_will_message; +reason_code_name(16#10) -> no_matching_subscribers; +reason_code_name(16#11) -> no_subscription_existed; +reason_code_name(16#18) -> continue_authentication; +reason_code_name(16#19) -> re_authenticate; +reason_code_name(16#80) -> unspecified_error; +reason_code_name(16#81) -> malformed_Packet; +reason_code_name(16#82) -> protocol_error; +reason_code_name(16#83) -> implementation_specific_error; +reason_code_name(16#84) -> unsupported_protocol_version; +reason_code_name(16#85) -> client_identifier_not_valid; +reason_code_name(16#86) -> bad_username_or_password; +reason_code_name(16#87) -> not_authorized; +reason_code_name(16#88) -> server_unavailable; +reason_code_name(16#89) -> server_busy; +reason_code_name(16#8A) -> banned; +reason_code_name(16#8B) -> server_shutting_down; +reason_code_name(16#8C) -> bad_authentication_method; +reason_code_name(16#8D) -> keepalive_timeout; +reason_code_name(16#8E) -> session_taken_over; +reason_code_name(16#8F) -> topic_filter_invalid; +reason_code_name(16#90) -> topic_name_invalid; +reason_code_name(16#91) -> packet_identifier_inuse; +reason_code_name(16#92) -> packet_identifier_not_found; +reason_code_name(16#93) -> receive_maximum_exceeded; +reason_code_name(16#94) -> topic_alias_invalid; +reason_code_name(16#95) -> packet_too_large; +reason_code_name(16#96) -> message_rate_too_high; +reason_code_name(16#97) -> quota_exceeded; +reason_code_name(16#98) -> administrative_action; +reason_code_name(16#99) -> payload_format_invalid; +reason_code_name(16#9A) -> retain_not_supported; +reason_code_name(16#9B) -> qos_not_supported; +reason_code_name(16#9C) -> use_another_server; +reason_code_name(16#9D) -> server_moved; +reason_code_name(16#9E) -> shared_subscriptions_not_supported; +reason_code_name(16#9F) -> connection_rate_exceeded; +reason_code_name(16#A0) -> maximum_connect_time; +reason_code_name(16#A1) -> subscription_identifiers_not_supported; +reason_code_name(16#A2) -> wildcard_subscriptions_not_supported; +reason_code_name(_Code) -> unknown_error. diff --git a/apps/iot/src/emqtt/emqtt_frame.erl b/apps/iot/src/emqtt/emqtt_frame.erl new file mode 100644 index 0000000..2b78895 --- /dev/null +++ b/apps/iot/src/emqtt/emqtt_frame.erl @@ -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(<>, + {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 + <> -> + 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(<>, 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), + <> = Rest, + % Note: Crash when reserved flag doesn't equal to 0, there is no strict + % compliance with the MQTT5.0. + <> = 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}, + <>, #{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}, <>, #{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}, <>, + #{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}, <>, + #{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}, <>, + #{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}, <>, + #{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}, <>, + #{strict_mode := StrictMode}) -> + StrictMode andalso validate_packet_id(PacketId), + #mqtt_packet_unsuback{packet_id = PacketId}; + +parse_packet(#mqtt_packet_header{type = ?UNSUBACK}, <>, + #{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}, <>, + #{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}, <>, + #{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, 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), + <> = 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}} + || <> <= Bin]; + +parse_topic_filters(unsubscribe, Bin) -> + [Topic || <> <= Bin]. + +parse_reason_codes(Bin) -> + [Code || <> <= Bin]. + +parse_utf8_pair(<>) -> + {{Key, Val}, Rest}. + +parse_utf8_string(Bin, false) -> + {undefined, Bin}; +parse_utf8_string(Bin, true) -> + parse_utf8_string(Bin). + +parse_utf8_string(<>) -> + {Str, Rest}. + +parse_binary_data(<>) -> + {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), + [<>, + 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 -> <> + end, + serialize_properties(Properties, Ver)]; + +serialize_variable(#mqtt_packet_puback{packet_id = PacketId}, Ver) + when Ver == ?MQTT_PROTO_V3; Ver == ?MQTT_PROTO_V4 -> + <>; +serialize_variable(#mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties + }, + Ver = ?MQTT_PROTO_V5) -> + [<>, ReasonCode, + serialize_properties(Properties, Ver)]; + +serialize_variable(#mqtt_packet_subscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}, Ver) -> + [<>, serialize_properties(Properties, Ver), + serialize_topic_filters(subscribe, TopicFilters, Ver)]; + +serialize_variable(#mqtt_packet_suback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}, Ver) -> + [<>, serialize_properties(Properties, Ver), + serialize_reason_codes(ReasonCodes)]; + +serialize_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}, Ver) -> + [<>, serialize_properties(Properties, Ver), + serialize_topic_filters(unsubscribe, TopicFilters, Ver)]; + +serialize_variable(#mqtt_packet_unsuback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}, Ver) -> + [<>, 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) -> + <>; +serialize_variable(PacketId, ?MQTT_PROTO_V4) when is_integer(PacketId) -> + <>; +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 <- 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), + <>. + +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. + diff --git a/apps/iot/src/emqtt/emqtt_props.erl b/apps/iot/src/emqtt/emqtt_props.erl new file mode 100644 index 0000000..a30f037 --- /dev/null +++ b/apps/iot/src/emqtt/emqtt_props.erl @@ -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. + diff --git a/apps/iot/src/emqtt/emqtt_sock.erl b/apps/iot/src/emqtt/emqtt_sock.erl new file mode 100644 index 0000000..05b234f --- /dev/null +++ b/apps/iot/src/emqtt/emqtt_sock.erl @@ -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). \ No newline at end of file diff --git a/apps/iot/src/http_handler/endpoint_handler.erl b/apps/iot/src/http_handler/endpoint_handler.erl new file mode 100644 index 0000000..d9c69ab --- /dev/null +++ b/apps/iot/src/http_handler/endpoint_handler.erl @@ -0,0 +1,168 @@ +%%%------------------------------------------------------------------- +%%% @author licheng5 +%%% @copyright (C) 2020, +%%% @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>>). diff --git a/apps/iot/src/http_handler/http_host_handler.erl b/apps/iot/src/http_handler/host_handler.erl similarity index 72% rename from apps/iot/src/http_handler/http_host_handler.erl rename to apps/iot/src/http_handler/host_handler.erl index d5386e8..290c023 100644 --- a/apps/iot/src/http_handler/http_host_handler.erl +++ b/apps/iot/src/http_handler/host_handler.erl @@ -6,7 +6,7 @@ %%% @end %%% Created : 26. 4月 2020 3:36 下午 %%%------------------------------------------------------------------- --module(http_host_handler). +-module(host_handler). -author("licheng5"). -include("iot.hrl"). @@ -83,27 +83,28 @@ handle_request("POST", "/host/publish_command", _, lager:debug("[http_host_handler] publish message is: ~p", [Reply1]), 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) -> task_logs_bo:change_status(TaskId, ?TASK_STATUS_FAILED), {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} -> - receive - {ok, Ref, _PacketId} -> + {ok, Ref} -> + receive + {response, Ref} -> + {ok, _} = task_logs_bo:change_status(TaskId, ?TASK_STATUS_OK), + {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">>)} - after Timeout * 1000 -> - 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, 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>>)} + {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 -> + 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, 200, iot_util:json_error(401, <<"命令执行超时, 请重试"/utf8>>)} 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, Pid} when is_pid(Pid) -> 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, <<"reply">> => #{<<"topic">> => ReplyTopic, <<"assoc">> => Assoc}}, [force_utf8]), + BinReply = jiffy:encode(#{<<"auth">> => true}, [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} -> receive - {ok, Ref, _PacketId} -> - receive - {host_reply, Assoc, #{<<"code">> := 1}} -> + {response, Ref, Response} -> + case jiffy:decode(Response, [return_maps]) of + #{<<"code">> := 1} -> ok = iot_host:activate(Pid, true), {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>>)} - after Timeout * 1000 -> - {ok, 200, iot_util:json_error(401, <<"操作超时,请重试: "/utf8>>)} end after Timeout * 1000 -> 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>>)} end; {error, Reason} -> - lager:debug("[iot_host] host_id uuid: ~p, publish topic get error: ~p", [UUID, Reason]), - {ok, 200, iot_util:json_error(402, <<"发送命令到mqtt服务失败"/utf8>>)} + lager:debug("[iot_host] host_id uuid: ~p, publish command get error: ~p", [UUID, Reason]), + {ok, 200, iot_util:json_error(402, Reason)} end end; @@ -153,29 +151,26 @@ handle_request("POST", "/host/activate", _, #{<<"uuid">> := UUID, <<"auth">> := case iot_host:has_session(Pid) of true -> 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, <<"reply">> => #{<<"topic">> => ReplyTopic, <<"assoc">> => Assoc}}, [force_utf8]), + BinReply = jiffy:encode(#{<<"auth">> => false}, [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} -> receive - {ok, Ref, _PacketId} -> - receive - {host_reply, Assoc, #{<<"code">> := 1}} -> + {response, Ref, Response} -> + case jiffy:decode(Response, [return_maps]) of + #{<<"code">> := 1} -> ok = iot_host:activate(Pid, false), {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>>)} - after Timeout * 1000 -> - {ok, 200, iot_util:json_error(401, <<"操作超时,请重试: "/utf8>>)} end after Timeout * 1000 -> 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>>)} end; {error, Reason} -> - lager:debug("[iot_host] host_id uuid: ~p, publish topic get error: ~p", [UUID, Reason]), - {ok, 200, iot_util:json_error(402, <<"发送命令到mqtt服务失败"/utf8>>)} + lager:debug("[iot_host] host_id uuid: ~p, publish command get error: ~p", [UUID, Reason]), + {ok, 200, iot_util:json_error(402, Reason)} end; false -> ok = iot_host:activate(Pid, false), diff --git a/apps/iot/src/http_handler/test_handler.erl b/apps/iot/src/http_handler/test_handler.erl new file mode 100644 index 0000000..bd34652 --- /dev/null +++ b/apps/iot/src/http_handler/test_handler.erl @@ -0,0 +1,31 @@ +%%%------------------------------------------------------------------- +%%% @author licheng5 +%%% @copyright (C) 2020, +%%% @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 +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/apps/iot/src/influxdb/influx_point.erl b/apps/iot/src/influxdb/influx_point.erl index dbaaf36..7b9b8ec 100644 --- a/apps/iot/src/influxdb/influx_point.erl +++ b/apps/iot/src/influxdb/influx_point.erl @@ -42,6 +42,9 @@ field_val(V) when is_number(V) -> <<(integer_to_binary(V))/binary, "u">>; field_val(V) when is_binary(V) -> <<$", V/binary, $">>; +field_val(V) when is_list(V); is_map(V) -> + S = jiffy:encode(V, [force_utf8]), + <<$", S/binary, $">>; field_val(true) -> <<"true">>; field_val(false) -> diff --git a/apps/iot/src/iot.app.src b/apps/iot/src/iot.app.src index 63eac1e..159b039 100644 --- a/apps/iot/src/iot.app.src +++ b/apps/iot/src/iot.app.src @@ -14,11 +14,12 @@ hackney, poolboy, mysql, - emqtt, + esockd, mnesia, crypto, public_key, ssl, + erts, kernel, stdlib ]}, diff --git a/apps/iot/src/iot_app.erl b/apps/iot/src/iot_app.erl index 6a48659..944829c 100644 --- a/apps/iot/src/iot_app.erl +++ b/apps/iot/src/iot_app.erl @@ -2,23 +2,26 @@ %% @doc iot public API %% @end %%%------------------------------------------------------------------- - -module(iot_app). -behaviour(application). +-include("iot.hrl"). + -export([start/2, stop/1]). -export([start_http_server/0]). start(_StartType, _StartArgs) -> io:setopts([{encoding, unicode}]), %% 启动数据库 - % start_mnesia(), + start_mnesia(), %% 加速内存的回收 erlang:system_flag(fullsweep_after, 16), %% 启动http服务 start_http_server(), + %% 启动redis服务器 + start_redis_server(), %% 启动连接池 ok = hackney_pool:start_pool(influx_pool, [{timeout, 150000}, {max_connections, 100}]), @@ -40,7 +43,10 @@ start_http_server() -> 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}}), + lager:debug("[iot_app] the http server start at: ~p, pid is: ~p", [Port, Pid]). -%start_mnesia() -> -% %% 启动数据库 -% mnesia:start(), -% Tables = mnesia:system_info(tables), -% %% 加载必须等待的数据库表 -% lists:member(router, Tables) andalso mnesia:wait_for_tables([router], infinity), -% lists:member(host, Tables) andalso mnesia:wait_for_tables([host], infinity). \ No newline at end of file +start_redis_server() -> + {ok, Props} = application:get_env(iot, redis_server), + Acceptors = proplists:get_value(acceptors, Props, 50), + MaxConnections = proplists:get_value(max_connections, Props, 10240), + Backlog = proplists:get_value(backlog, Props, 1024), + Port = proplists:get_value(port, Props), + + 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. \ No newline at end of file diff --git a/apps/iot/src/iot_auth.erl b/apps/iot/src/iot_auth.erl new file mode 100644 index 0000000..55c2805 --- /dev/null +++ b/apps/iot/src/iot_auth.erl @@ -0,0 +1,31 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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(<>) =:= Token + end; + false -> + false + end. diff --git a/apps/iot/src/iot_endpoint.erl b/apps/iot/src/iot_endpoint.erl new file mode 100644 index 0000000..466a903 --- /dev/null +++ b/apps/iot/src/iot_endpoint.erl @@ -0,0 +1,278 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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). \ No newline at end of file diff --git a/apps/iot/src/iot_endpoint_sup.erl b/apps/iot/src/iot_endpoint_sup.erl new file mode 100644 index 0000000..38b790d --- /dev/null +++ b/apps/iot/src/iot_endpoint_sup.erl @@ -0,0 +1,51 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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']}. \ No newline at end of file diff --git a/apps/iot/src/iot_host.erl b/apps/iot/src/iot_host.erl index 5d7fe35..9f66cc1 100644 --- a/apps/iot/src/iot_host.erl +++ b/apps/iot/src/iot_host.erl @@ -14,17 +14,12 @@ %% API -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([has_session/1]). +-export([get_metric/1, publish_message/3, get_aes/1, rsa_encode/3]). +-export([has_session/1, create_session/2, attach_channel/2]). %% gen_statem callbacks -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, { host_id :: integer(), %% 从数据库里面读取到的数据 @@ -37,11 +32,12 @@ %% aes的key, 后续通讯需要基于这个加密 aes = <<>> :: binary(), - %% 主机的相关信息 - metrics = #{} :: map(), + %% websocket相关 + 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) -> 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(). -handle(Pid, Payload) when is_pid(Pid), is_binary(Payload); is_map(Payload) -> - gen_statem:cast(Pid, {handle, Payload}). +-spec handle(Pid :: pid(), Packet :: {atom(), binary()} | {atom(), {binary(), binary()}}) -> no_return(). +handle(Pid, Packet) when is_pid(Pid) -> + gen_statem:cast(Pid, {handle, Packet}). %% 重新加载主机的基本信息 -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) -> 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加密的指令都是不需要会话存在的 -spec rsa_encode(Pid :: pid(), CommandType :: integer(), PlainText :: binary()) -> {ok, EncText :: binary()} | {error, Reason :: binary()}. rsa_encode(Pid, CommandType, PlainText) when is_pid(Pid), is_integer(CommandType), is_binary(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()}. -aes_encode(Pid, CommandType, Params) when is_pid(Pid), is_integer(CommandType), is_binary(Params) -> - gen_statem:call(Pid, {aes_encode, CommandType, Params}). - +publish_message(Pid, CommandType, Params) when is_pid(Pid), is_integer(CommandType) -> + gen_statem:call(Pid, {publish_message, self(), CommandType, Params}). %% @doc Creates a gen_statem process which calls Module:init/1 to %% 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]) -> case host_bo:get_host_by_uuid(UUID) of {ok, #{<<"status">> := Status, <<"id">> := HostId}} -> - %% 启动心跳定时器 - erlang:start_timer(?TICKER_INTERVAL, self(), ping_ticker), Aes = list_to_binary(iot_util:rand_bytes(32)), - %% 告知主机端需要重新授权 - gen_server:cast(self(), need_auth), StateName = case Status =:= ?HOST_STATUS_INACTIVE of - true -> denied; - false -> activated + true -> + denied; + false -> + %% 重启时,认为主机是离线状态; 等待主机主动建立连接 + {ok, _} = host_bo:change_status(UUID, ?HOST_STATUS_OFFLINE), + activated end, - {ok, StateName, #state{host_id = HostId, uuid = UUID, aes = Aes, status = Status}}; undefined -> lager:warning("[iot_host] host uuid: ~p, loaded from mysql failed", [UUID]), ignore end. - %% @private %% @doc This function is called by a gen_statem when it needs to find out %% 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) -> {keep_state, State, [{reply, From, {error, <<"会话未建立"/utf8>>}}]}; -%% 基于aes加密 -handle_event({call, From}, {aes_encode, CommandType, Command}, session, State = #state{aes = AES}) -> - EncCommand = iot_cipher_aes:encrypt(AES, Command), - {keep_state, State, [{reply, From, {ok, <>}}]}; -handle_event({call, From}, {aes_encode, _, _}, _, State) -> +%% 发送普通格式的消息 +handle_event({call, From}, {publish_message, ReceiverPid, CommandType, Command}, session, State = #state{aes = AES, channel_pid = ChannelPid}) -> + SendCommand = case Command of + {aes, Command0} -> + iot_cipher_aes:encrypt(AES, Command0); + Command0 -> + Command0 + end, + + %% 通过websocket发送请求 + Ref = ws_channel:publish(ChannelPid, ReceiverPid, <>), + + {keep_state, State, [{reply, From, {ok, Ref}}]}; +handle_event({call, From}, {publish_message, _, _, _}, _, State) -> {keep_state, State, [{reply, From, {error, <<"会话未建立"/utf8>>}}]}; 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) -> {keep_state, State, [{reply, From, ok}]}; -handle_event(cast, need_auth, _StateName, State = #state{uuid = UUID}) -> - Reply = jiffy:encode(#{<<"auth">> => false, <<"aes">> => <<"">>}, [force_utf8]), - {ok, Ref} = iot_mqtt_publisher:publish(downstream_topic(UUID), <<8:8, Reply/binary>>, 1), - receive - {ok, Ref, PacketId} -> - 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}; +%% 绑定channel +handle_event({call, From}, {attach_channel, ChannelPid}, _, State = #state{uuid = UUID, channel_pid = undefined}) -> + lager:debug("[iot_host] attach_channel host_id uuid: ~p, channel: ~p", [UUID, ChannelPid]), + MRef = erlang:monitor(process, ChannelPid), + {keep_state, State#state{channel_pid = ChannelPid, monitor_ref = MRef}, [{reply, From, ok}]}; -%% 需要将消息转换成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, <>}, 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]), Reply = #{<<"a">> => false, <<"aes">> => <<"">>}, 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), - 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, <>}, _StateName, State = #state{uuid = UUID, aes = Aes}) -> +handle_event({call, From}, {create_session, PubKey}, _StateName, State = #state{uuid = UUID, aes = Aes}) -> lager:debug("[iot_host] host_id uuid: ~p, create_session", [UUID]), Reply = #{<<"a">> => true, <<"aes">> => Aes}, 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), - 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; + {next_state, session, State#state{status = ?HOST_STATUS_ONLINE}, [{reply, From, {ok, <<10:8, EncReply/binary>>}}]}; -handle_event(cast, {handle, <>}, 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), case catch jiffy:decode(PlainData, [return_maps]) of Infos when is_list(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) -> Timestamp = maps:get(<<"at">>, Info, iot_util:timestamp()), - NTags = Tags#{<<"uuid">> => UUID, <<"service_name">> => ServiceName}, - %% 微服务名前缀作为measurement来保存数据 - [Measurement | _] = binary:split(ServiceName, <<":">>), + Measurement = <<"metric">>, Points = lists:map(fun(Fields) -> influx_point:new(Measurement, NTags, Fields, Timestamp) end, FieldsList), Precision = influx_client:get_precision(Timestamp), @@ -270,22 +257,51 @@ handle_event(cast, {handle, <>}, session, State = # poolboy:transaction(influx_pool, fun(Pid) -> influx_client:write(Pid, <<"iot">>, <<"iot">>, Precision, Points) end) end, Infos); 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, - {keep_state, State#state{is_answered = true}}; + {keep_state, State}; -handle_event(cast, {handle, <>}, 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), case catch jiffy:decode(MetricsInfo, [return_maps]) of Metrics when is_map(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 -> - lager:debug("[iot_message_handler] host_id: ~p, ping is invalid json: ~p", [UUID, Other]), - {keep_state, State#state{is_answered = true}} + lager:debug("[iot_host] host_id: ~p, ping is invalid json: ~p", [UUID, Other]), + {keep_state, State} end; -handle_event(cast, {handle, <>}, 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), case catch jiffy:decode(Info, [return_maps]) of #{<<"at">> := At, <<"services">> := ServiceInforms} -> @@ -310,9 +326,9 @@ handle_event(cast, {handle, <>}, session, State Error -> lager:warning("[iot_host] inform get error: ~p", [Error]) end, - {keep_state, State#state{is_answered = true}}; + {keep_state, State}; -handle_event(cast, {handle, <>}, session, State = #state{aes = AES}) -> +handle_event(cast, {handle, {feedback_step, Info0}}, session, State = #state{aes = AES}) -> Info = iot_cipher_aes:decrypt(AES, Info0), case catch jiffy:decode(Info, [return_maps]) of Data = #{<<"task_id">> := TaskId, <<"code">> := Code} -> @@ -327,7 +343,7 @@ handle_event(cast, {handle, <>}, session, end, {keep_state, State}; -handle_event(cast, {handle, <>}, session, State = #state{aes = AES}) -> +handle_event(cast, {handle, {feedback_result, Info0}}, session, State = #state{aes = AES}) -> Info = iot_cipher_aes:decrypt(AES, Info0), case catch jiffy:decode(Info, [return_maps]) of #{<<"task_id">> := TaskId, <<"time">> := Time, <<"code">> := Code, <<"reason">> := Reason, <<"error">> := Error, <<"type">> := Type} -> @@ -344,29 +360,17 @@ handle_event(cast, {handle, <>}, sessio end, {keep_state, State}; -handle_event(info, {timeout, _, ping_ticker}, _StateName, State = #state{uuid = UUID, is_answered = IsAnswered, status = Status}) -> - erlang:start_timer(?TICKER_INTERVAL, self(), ping_ticker), - %% 需要考虑到主机未激活的情况,主机未激活,返回: keep_status - NextStatus = if - 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, +%% 当websocket断开的时候,则设置主机状态为下线状态; 主机的状态需要转换 +handle_event(info, {'DOWN', Ref, process, ChannelPid, Reason}, StateName, State = #state{uuid = UUID, monitor_ref = Ref, channel_pid = ChannelPid}) -> + lager:warning("[iot_host] channel: ~p, down with reason: ~p, state name: ~p, state: ~p", [ChannelPid, Reason, StateName, State]), + {ok, _} = host_bo:change_status(UUID, ?HOST_STATUS_OFFLINE), - case NextStatus of - keep_status -> - {keep_state, State#state{is_answered = false}}; - {change_status, NStatus} -> - case host_bo:change_status(UUID, NStatus) of - {ok, _} -> - {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 + %% 会话状态如果链接丢失,需要切换到activated状态,其他情况保持不变 + case StateName =:= session of + true -> + {next_state, activated, State#state{status = ?HOST_STATUS_OFFLINE, channel_pid = undefined}}; + false -> + {keep_state, State#state{status = ?HOST_STATUS_OFFLINE, channel_pid = undefined}} end; handle_event(EventType, EventContent, StateName, State) -> diff --git a/apps/iot/src/iot_mnesia.erl b/apps/iot/src/iot_mnesia.erl deleted file mode 100644 index 2065a7a..0000000 --- a/apps/iot/src/iot_mnesia.erl +++ /dev/null @@ -1,54 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2020, -%%% @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. \ No newline at end of file diff --git a/apps/iot/src/iot_mqtt_reply_subscriber.erl b/apps/iot/src/iot_mqtt_reply_subscriber.erl deleted file mode 100644 index c7672ac..0000000 --- a/apps/iot/src/iot_mqtt_reply_subscriber.erl +++ /dev/null @@ -1,172 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author aresei -%%% @copyright (C) 2023, -%%% @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 = <>, - - {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 -%%%=================================================================== \ No newline at end of file diff --git a/apps/iot/src/iot_mqtt_subscriber.erl b/apps/iot/src/iot_mqtt_subscriber.erl index fe28ee7..5647d5d 100644 --- a/apps/iot/src/iot_mqtt_subscriber.erl +++ b/apps/iot/src/iot_mqtt_subscriber.erl @@ -23,7 +23,7 @@ %% 需要订阅的主题信息 -define(Topics,[ - {<<"host/upstream/+">>, 1} + {<<"CET/NX/+/upload">>, 2} ]). -record(state, { @@ -52,21 +52,23 @@ start_link() -> init([]) -> %% 建立到emqx服务器的连接 Opts = iot_config:emqt_opts(<<"host-subscriber">>), + lager:debug("[opts] is: ~p", [Opts]), case emqtt:start_link(Opts) of {ok, ConnPid} -> %% 监听和host相关的全部事件 + lager:debug("[iot_mqtt_subscriber] start conntecting, pid: ~p", [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), - 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}}; ignore -> - lager:debug("[iot_mqtt_host_subscriber] connect emqx get ignore"), + lager:debug("[iot_mqtt_subscriber] connect emqx get ignore"), {stop, ignore}; {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} end. @@ -98,32 +100,15 @@ handle_cast(_Request, 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_host_subscriber] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]), +handle_info({disconnect, ReasonCode, Properties}, State = #state{}) -> + lager:debug("[iot_mqtt_subscriber] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]), {stop, disconnected, State}; %% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行 -handle_info({publish, #{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]), +handle_info({publish, #{packet_id := _PacketId, payload := Payload, qos := Qos, topic := Topic}}, State = #state{conn_pid = _ConnPid}) -> + lager:debug("[iot_mqtt_subscriber] Recv a topic: ~p, publish packet: ~p, qos: ~p", [Topic, Payload, Qos]), %% 将消息分发到对应的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}; -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]), {noreply, State}; @@ -138,7 +123,7 @@ handle_info(Info, State = #state{}) -> %% with Reason. The return value is ignored. -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | 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的订阅 TopicNames = lists:map(fun({Name, _}) -> Name end, ?Topics), {ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, TopicNames), diff --git a/apps/iot/src/iot_mqtt_sys_subscriber.erl b/apps/iot/src/iot_mqtt_sys_subscriber.erl deleted file mode 100644 index e0fe0cc..0000000 --- a/apps/iot/src/iot_mqtt_sys_subscriber.erl +++ /dev/null @@ -1,223 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author aresei -%%% @copyright (C) 2023, -%%% @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. \ No newline at end of file diff --git a/apps/iot/src/iot_router.erl b/apps/iot/src/iot_router.erl new file mode 100644 index 0000000..9c9e1a2 --- /dev/null +++ b/apps/iot/src/iot_router.erl @@ -0,0 +1,33 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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. \ No newline at end of file diff --git a/apps/iot/src/iot_sup.erl b/apps/iot/src/iot_sup.erl index 9eb821b..d9946c3 100644 --- a/apps/iot/src/iot_sup.erl +++ b/apps/iot/src/iot_sup.erl @@ -28,42 +28,13 @@ start_link() -> init([]) -> SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600}, ChildSpecs = [ - - #{ - id => 'iot_mqtt_subscriber', - start => {'iot_mqtt_subscriber', start_link, []}, + id => 'iot_endpoint_sup', + start => {'iot_endpoint_sup', start_link, []}, restart => permanent, shutdown => 2000, - type => worker, - modules => ['iot_mqtt_subscriber'] - }, - - #{ - 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'] + type => supervisor, + modules => ['iot_endpoint_sup'] }, #{ diff --git a/apps/iot/src/iot_util.erl b/apps/iot/src/iot_util.erl index cf8f424..d0cb7be 100644 --- a/apps/iot/src/iot_util.erl +++ b/apps/iot/src/iot_util.erl @@ -10,8 +10,8 @@ -author("licheng5"). %% API --export([timestamp/0, number_format/2, current_time/0]). --export([step/3, chunks/2, rand_bytes/1, uuid/0]). +-export([timestamp/0, number_format/2, current_time/0, timestamp_of_seconds/0]). +-export([step/3, chunks/2, rand_bytes/1, uuid/0, md5/1, parse_mapper/1]). -export([json_data/1, json_error/2]). -export([queue_limited_in/3, assert_call/2]). @@ -24,6 +24,10 @@ current_time() -> {Mega, Seconds, _Micro} = os:timestamp(), Mega * 1000000 + Seconds. +timestamp_of_seconds() -> + {Mega, Seconds, _Micro} = os:timestamp(), + Mega * 1000000 + Seconds. + number_format(Num, _Decimals) when is_integer(Num) -> Num; number_format(Float, Decimals) when is_float(Float) -> @@ -86,4 +90,28 @@ queue_limited_in(Item, Q, Num) when is_integer(Num) -> assert_call(true, Fun) -> Fun(); assert_call(false, _) -> - ok. \ No newline at end of file + ok. + +-spec md5(Str :: binary()) -> binary(). +md5(Str) when is_binary(Str) -> + list_to_binary(lists:flatten([hex(X) || <> <= 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. \ No newline at end of file diff --git a/apps/iot/src/mnesia/mnesia_endpoint.erl b/apps/iot/src/mnesia/mnesia_endpoint.erl new file mode 100644 index 0000000..2265bfb --- /dev/null +++ b/apps/iot/src/mnesia/mnesia_endpoint.erl @@ -0,0 +1,69 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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 + }. + diff --git a/apps/iot/src/mnesia/mnesia_id_generator.erl b/apps/iot/src/mnesia/mnesia_id_generator.erl new file mode 100644 index 0000000..f911a9b --- /dev/null +++ b/apps/iot/src/mnesia/mnesia_id_generator.erl @@ -0,0 +1,16 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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). \ No newline at end of file diff --git a/apps/iot/src/mnesia/mnesia_kv.erl b/apps/iot/src/mnesia/mnesia_kv.erl new file mode 100644 index 0000000..0a8a819 --- /dev/null +++ b/apps/iot/src/mnesia/mnesia_kv.erl @@ -0,0 +1,949 @@ +%%%------------------------------------------------------------------- +%%% @author licheng5 +%%% @copyright (C) 2021, +%%% @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 + <> -> + 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)]. diff --git a/apps/iot/src/mocker/eval_test.erl b/apps/iot/src/mocker/eval_test.erl new file mode 100644 index 0000000..a7963b0 --- /dev/null +++ b/apps/iot/src/mocker/eval_test.erl @@ -0,0 +1,25 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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">>}). + + diff --git a/apps/iot/src/mocker/host_mocker.erl b/apps/iot/src/mocker/host_mocker.erl deleted file mode 100644 index 16d9378..0000000 --- a/apps/iot/src/mocker/host_mocker.erl +++ /dev/null @@ -1,432 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author aresei -%%% @copyright (C) 2023, -%%% @doc -%%% -%%% @end -%%% Created : 14. 6月 2023 09:50 -%%%------------------------------------------------------------------- --module(host_mocker). --author("aresei"). --include("iot.hrl"). - --behaviour(gen_server). - -%% API --export([start_link/1, stop/0]). --export([test/0, ping/0]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). - --define(SERVER, ?MODULE). --define(TICKER_INTERVAL, 5000). - --record(state, { - conn_pid, - topic :: binary(), - pub_key = <<"-----BEGIN RSA PUBLIC KEY----- -MIIBCgKCAQEAp5Ky0wCtWb5sq7s4wq8K+BNAINtwYDCnwdJWrPX1/ueubu+VwLf4 -EyNyghHCGrwntDbCPXmM8DYI99Mxfy2r8aaMgjwXdAzGpPkrzE6iGjLQmUHbSGBg -ZGVe3RgFhhIZC1c85VBtXqh9nrmgw9FuYlex0w9p1vODIw3IhmJDFAgP45reTO0l -yggZpfABi++R7HpF8uuQzc5GnFDGM3pESbGK6o7E5CYy5f+pKNAahJEfUf0onFsp -kneCGh/vBldcmFsXYQ3biAHmF8UPHf8NALU+FRnJx1dfkGTu7UudYvEUIfPO2cqW -2rvQ9BYPInerFw304hR2EyM1NJRa4idHtQIDAQAB ------END RSA PUBLIC KEY-----">>, - pri_key = <<"-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAp5Ky0wCtWb5sq7s4wq8K+BNAINtwYDCnwdJWrPX1/ueubu+V -wLf4EyNyghHCGrwntDbCPXmM8DYI99Mxfy2r8aaMgjwXdAzGpPkrzE6iGjLQmUHb -SGBgZGVe3RgFhhIZC1c85VBtXqh9nrmgw9FuYlex0w9p1vODIw3IhmJDFAgP45re -TO0lyggZpfABi++R7HpF8uuQzc5GnFDGM3pESbGK6o7E5CYy5f+pKNAahJEfUf0o -nFspkneCGh/vBldcmFsXYQ3biAHmF8UPHf8NALU+FRnJx1dfkGTu7UudYvEUIfPO -2cqW2rvQ9BYPInerFw304hR2EyM1NJRa4idHtQIDAQABAoIBAQCbhGP9u0UmYrnL -7ydQVs9hR8xeMglq2/z0vla+kk5I0Y9rWWKrxpCugllFKWHxGennMGK4VtRcImnU -ReZL14EZ9a21ODuz8h9w/+aL5/Y9Ried9CakVv1eb03I9wA5WxZvFfloAGpgTRK4 -eiIfWYCOOEDKViWt3bU4lRQi05LZRNmQg57CrDgX2UoxTMug7DK/FzVPmJ7azcgp -6AiBFLzZn/LwIgCENTvQ57X6UO7j/L6N7/Nz4Cz5w0om9Hv8pYxL01ML7xvFpVcn -o1UN3X1jIWOvS9njbJnfD600Hf+5I8ixFgxbGSsXEzDcbBy1/13pAxkexPx92ynQ -ktxNhnfBAoGBANUdNgCgv7LBlZYNivCf5WnJCO/0aRjunGu2xF3RTAffa/6HyjI8 -yarVZiMAcCXlLjK7ao3t/RQoiTi3NqatpxL1+iQ1f5a/nupMC9jYy6I7rTUOl7k7 -VNmX2LaCFEfixZbLz7yDwvUVfNDPc1QeihrqFSWJKAPcSGHfKuEzQIYZAoGBAMlL -ZL4hCmS/UpwQX1rTLHHZeVCCu0eL54DO+HWlRnrq0/7agGr+4m/HP0eAm8khnVFO -h3ySFabLA7pJo0H1+P1v5+un7l6wsZZp4yoXxD5cr9prorI+N40i0yqOIyxspo2a -1k71LJpeuv1ZltkTWXUIz/TxVu/iFj/m2wFaxun9AoGAQvBG6xGSxOoLOzOLxaLj -o0OS/BPQAxXHqgmhSjqYYAysVil+uCLh0TfwOeREVZLT3PmDMYtkJ7XHzDm3/8ih -ptH+POtU5RvRJZS3T+hgpdeKwxSPUY4yS5pnZoQbLK0tFP11haf5T5PtPYU7m1tw -U53dAIpBOF0zmxJG3K+Ff9kCgYAGv31IFml3ySYmzzGzJMMnqee0OD24/0qqecXA -g+Lh+f9TWtXVQGgs4RwQ9JHEY1kXwa8vEOKi7clZNGDBtFI9hMPclYubJwc9CJ2x -6owMnyTSCKuyl/1awOEdWxh4w8etlZQ7n2J4ZlaUaa1x54EnOD1oc7K7ZfPi/oU2 -/WkPrQKBgFvuP/xwkGldkvxVEP6EN6GDuP/BzumVYMSxku0MC4AgwgAwW7pgTynq -yH0hr6SJTlM2zEFQszmKdsg0fRXO6wrqN2mt8dZKEd51rp04hM47yM48stlE9Lmt -/NZsDqOnUgW07xFPQI3nfS5dG9dH9Qsk+OxuIt9YifKriKDpTHlc ------END RSA PRIVATE KEY-----">>, - - aes = <<>> -}). - -%%%=================================================================== -%%% API -%%%=================================================================== - -test() -> - catch stop(), - host_mocker:start_link(<<"123123123123123">>). - -stop() -> - gen_server:stop(?MODULE). - -ping() -> - Pid = whereis(?MODULE), - erlang:start_timer(0, Pid, feedback_step_ticker). - -%% @doc Spawns the server and registers the local name (unique) --spec(start_link(UUID :: binary()) -> - {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). -start_link(UUID) when is_binary(UUID) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [UUID], []). - -%%%=================================================================== -%%% gen_server callbacks -%%%=================================================================== - -%% @private -%% @doc Initializes the server --spec(init(Args :: term()) -> - {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | - {stop, Reason :: term()} | ignore). -init([UUID]) -> - %% 建立到emqx服务器的连接 - Opts = iot_config:emqt_opts(<<"host-subscriber", UUID/binary>>), - {ok, ConnPid} = emqtt:start_link(Opts), - %% 监听和host相关的全部事件 - {ok, _} = emqtt:connect(ConnPid), - lager:debug("[iot_mqtt_sys_subscriber] connect success, pid: ~p", [ConnPid]), - SubscribeResult = emqtt:subscribe(ConnPid, [ - {<<"host/downstream/", UUID/binary>>, 1} - ]), - lager:debug("[iot_mqtt_sys_subscriber] subscribe result is: ~p", [SubscribeResult]), - - %% 建立到主机的握手 - Message = #{ - <<"method">> => <<"register">>, - <<"params">> => #{ - <<"uuid">> => UUID - } - }, - Req = jiffy:encode(Message, [force_utf8]), - - {ok, Ref} = iot_mqtt_publisher:publish(<<"system/upstream">>, Req, 1), - receive - {ok, Ref, PacketId} -> - lager:debug("[host_mocker] send register success, packet_id: ~p", [PacketId]); - {error, Reason} -> - lager:debug("[host_mocker] send register failed, reason: ~p", [Reason]) - end, - - {ok, #state{conn_pid = ConnPid, topic = <<"host/upstream/", UUID/binary>>}}. - -%% @private -%% @doc Handling call messages --spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()}, - State :: #state{}) -> - {reply, Reply :: term(), NewState :: #state{}} | - {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | - {noreply, NewState :: #state{}} | - {noreply, NewState :: #state{}, timeout() | hibernate} | - {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | - {stop, Reason :: term(), NewState :: #state{}}). -handle_call(_Request, _From, State = #state{}) -> - {reply, ok, State}. - -%% @private -%% @doc Handling cast messages --spec(handle_cast(Request :: term(), State :: #state{}) -> - {noreply, NewState :: #state{}} | - {noreply, NewState :: #state{}, timeout() | hibernate} | - {stop, Reason :: term(), NewState :: #state{}}). -handle_cast(_Request, State = #state{}) -> - {noreply, State}. - -%% @private -%% @doc Handling all non call/cast messages --spec(handle_info(Info :: timeout() | term(), State :: #state{}) -> - {noreply, NewState :: #state{}} | - {noreply, NewState :: #state{}, timeout() | hibernate} | - {stop, Reason :: term(), NewState :: #state{}}). -handle_info({disconnect, ReasonCode, Properties}, State = #state{}) -> - lager:debug("[host_mocker] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]), - {stop, disconnected, State}; -%% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行 -handle_info({publish, #{payload := Payload, qos := Qos, topic := FromTopic}}, - State = #state{topic = Topic, pri_key = PrivateKey, aes = Aes0}) -> - - lager:debug("[host_mocker] Recv a publish packet: ~p, qos: ~p, from topic: ~p", [Payload, Qos, FromTopic]), - case Payload of - <<0:8, Reply/binary>> -> - Json = jiffy:decode(Reply, [return_maps]), - lager:debug("[host_mocker] get reply: ~p", [Json]), - case Json of - #{<<"code">> := 1, <<"message">> := <<"ok">>} -> - self() ! create_session; - Info -> - Info - end, - {noreply, State}; - - %% 10是通过rsa加密的数据,里面保存了aes;后续的所有通讯都是通过aes加密的 - <<10:8, Command0/binary>> -> - Command = iot_cipher_rsa:decode(Command0, PrivateKey), - case jiffy:decode(Command, [return_maps]) of - #{<<"aes">> := Aes, <<"a">> := true} -> - %% 启动周期ping - erlang:start_timer(?TICKER_INTERVAL, self(), ping_ticker), - %% 数据收集 - erlang:start_timer(?TICKER_INTERVAL + 1000, self(), data_ticker), - %% 触发inform上传 - erlang:start_timer(?TICKER_INTERVAL, self(), inform_ticker), - erlang:start_timer(?TICKER_INTERVAL, self(), feedback_ticker), - erlang:start_timer(?TICKER_INTERVAL, self(), feedback_step_ticker), - - {noreply, State#state{aes = Aes}}; - _ -> - lager:debug("[host_mocker] auth failed") - end; - - %% t = 8采用明文消息 - <<8:8, Command/binary>> -> - case jiffy:decode(Command, [return_maps]) of - #{<<"auth">> := true, <<"reply">> := #{<<"topic">> := ReplyTopic, <<"assoc">> := Assoc}} -> - Msg = jiffy:encode(#{ - <<"code">> => 1, - <<"message">> => "", - <<"assoc">> => Assoc - }, [force_utf8]), - - {ok, Ref} = iot_mqtt_publisher:publish(ReplyTopic, Msg, 1), - receive - {ok, Ref, PacketId} -> - lager:debug("[host_mocker] send reply success, packet_id: ~p", [PacketId]); - {error, Reason} -> - lager:debug("[host_mocker] send reply failed, reason: ~p", [Reason]) - end, - - self() ! create_session, - - {noreply, State}; - - #{<<"auth">> := false, <<"reply">> := #{<<"topic">> := Topic, <<"assoc">> := Assoc}} -> - Msg = jiffy:encode(#{ - <<"code">> => 1, - <<"message">> => "", - <<"assoc">> => Assoc - }, [force_utf8]), - - {ok, Ref} = iot_mqtt_publisher:publish(Topic, Msg, 1), - receive - {ok, Ref, PacketId} -> - lager:debug("[host_mocker] send reply success, packet_id: ~p", [PacketId]); - {error, Reason} -> - lager:debug("[host_mocker] send reply failed, reason: ~p", [Reason]) - end, - - {noreply, State#state{aes = <<>>}}; - #{<<"auth">> := false} -> - self() ! create_session, - - {noreply, State#state{aes = <<>>}} - end; - - %% 处理其他指令 - <> -> - 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, <>, 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, <>, 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, <>, 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, <>, 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, <>, 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, <>, 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. \ No newline at end of file diff --git a/apps/iot/src/mocker/iot_endpoint_mocker.erl b/apps/iot/src/mocker/iot_endpoint_mocker.erl new file mode 100644 index 0000000..179c2a5 --- /dev/null +++ b/apps/iot/src/mocker/iot_endpoint_mocker.erl @@ -0,0 +1,119 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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 +%%%=================================================================== diff --git a/apps/iot/src/mocker/iot_mock.erl b/apps/iot/src/mocker/iot_mock.erl index 89276b6..ab6bf51 100644 --- a/apps/iot/src/mocker/iot_mock.erl +++ b/apps/iot/src/mocker/iot_mock.erl @@ -13,6 +13,48 @@ %% API -export([rsa_encode/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) -> lists:foreach(fun(Id) -> diff --git a/apps/iot/src/postman/http_postman.erl b/apps/iot/src/postman/http_postman.erl new file mode 100644 index 0000000..dcbab9c --- /dev/null +++ b/apps/iot/src/postman/http_postman.erl @@ -0,0 +1,124 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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 +%%%=================================================================== diff --git a/apps/iot/src/postman/http_postman_worker.erl b/apps/iot/src/postman/http_postman_worker.erl new file mode 100644 index 0000000..3fd0471 --- /dev/null +++ b/apps/iot/src/postman/http_postman_worker.erl @@ -0,0 +1,119 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @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 +%%%=================================================================== diff --git a/apps/iot/src/iot_mqtt_publisher.erl b/apps/iot/src/postman/mqtt_postman.erl similarity index 59% rename from apps/iot/src/iot_mqtt_publisher.erl rename to apps/iot/src/postman/mqtt_postman.erl index b3cb8c8..af537ee 100644 --- a/apps/iot/src/iot_mqtt_publisher.erl +++ b/apps/iot/src/postman/mqtt_postman.erl @@ -6,22 +6,23 @@ %%% @end %%% Created : 12. 3月 2023 21:27 %%%------------------------------------------------------------------- --module(iot_mqtt_publisher). +-module(mqtt_postman). -author("aresei"). -include("iot.hrl"). -behaviour(gen_server). %% API --export([start_link/0, publish/3]). +-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(), conn_pid :: pid(), + topic :: binary(), + qos = 0 :: integer(), inflight = #{} }). @@ -29,15 +30,11 @@ %%% 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) --spec(start_link() -> +-spec(start_link(ParentPid :: pid(), Opts :: list(), Topic :: binary(), Qos :: integer()) -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). -start_link() -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). +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(?MODULE, [ParentPid, Opts, Topic, Qos], []). %%%=================================================================== %%% gen_server callbacks @@ -48,14 +45,14 @@ start_link() -> -spec(init(Args :: term()) -> {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | {stop, Reason :: term()} | ignore). -init([]) -> - %% 建立到emqx服务器的连接 - Opts = iot_config:emqt_opts(<<"publisher">>), - {ok, ConnPid} = emqtt:start_link(Opts), - lager:debug("[iot_mqtt_publisher] connect success, pid: ~p", [ConnPid]), +init([ParentPid, Opts, Topic, Qos]) -> + Opts1 = [{owner, self()} | Opts], + {ok, ConnPid} = emqtt:start_link(Opts1), + lager:debug("[mqtt_postman] start connect, options: ~p", [Opts1]), {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 %% @doc Handling call messages @@ -67,16 +64,8 @@ init([]) -> {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | {stop, Reason :: term(), NewState :: #state{}}). -handle_call({publish, ReceiverPid, Topic, Message, Qos}, _From, State = #state{conn_pid = ConnPid, inflight = InFlight}) -> - %% [{qos, Qos}, {retain, true}] - 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. +handle_call(_Info, _From, State) -> + {reply, ok, State}. %% @private %% @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{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #state{}}). -handle_cast(_Request, State = #state{}) -> +handle_cast(_Info, State) -> {noreply, State}. %% @private @@ -94,24 +83,42 @@ handle_cast(_Request, State = #state{}) -> {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #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}; 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}; -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 - {{ReceiverPid, Ref, Message}, RestInflight} -> - lager:debug("[iot_mqtt_publisher] receive puback packet: ~p, assoc message: ~p", [Packet, Message]), - ReceiverPid ! {ok, Ref, PacketId}, + {{Ref, Message}, RestInflight} -> + lager:debug("[mqtt_postman] receive puback packet: ~p, assoc message: ~p", [Packet, Message]), + ParentPid ! {ack, Ref}, {noreply, State#state{inflight = RestInflight}}; error -> - lager:warning("[iot_mqtt_publisher] receive unknown puback packet: ~p", [Packet]), + lager:warning("[mqtt_postman] receive unknown puback packet: ~p", [Packet]), {noreply, State} 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{}) -> - lager:debug("[iot_mqtt_publisher] get info: ~p", [Info]), + lager:debug("[mqtt_postman] get info: ~p", [Info]), {noreply, State}. %% @private @@ -121,12 +128,12 @@ handle_info(Info, State = #state{}) -> %% 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) -> +terminate(Reason, #state{conn_pid = ConnPid}) when is_pid(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; terminate(Reason, _State) -> - lager:debug("[iot_mqtt_publisher] terminate with reason: ~p", [Reason]), + lager:debug("[mqtt_postman] terminate with reason: ~p", [Reason]), ok. %% @private diff --git a/apps/iot/src/redis/redis_handler.erl b/apps/iot/src/redis/redis_handler.erl new file mode 100644 index 0000000..001352e --- /dev/null +++ b/apps/iot/src/redis/redis_handler.erl @@ -0,0 +1,197 @@ +%%%------------------------------------------------------------------- +%%% @author licheng5 +%%% @copyright (C) 2021, +%%% @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}). + + diff --git a/apps/iot/src/redis/redis_protocol.erl b/apps/iot/src/redis/redis_protocol.erl new file mode 100644 index 0000000..5f49695 --- /dev/null +++ b/apps/iot/src/redis/redis_protocol.erl @@ -0,0 +1,106 @@ +%%%------------------------------------------------------------------- +%%% @author licheng5 +%%% @copyright (C) 2020, +%%% @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 = <>, + 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 + <> -> + 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)). \ No newline at end of file diff --git a/apps/iot/src/websocket/ws_channel.erl b/apps/iot/src/websocket/ws_channel.erl new file mode 100644 index 0000000..c745f05 --- /dev/null +++ b/apps/iot/src/websocket/ws_channel.erl @@ -0,0 +1,172 @@ +%%%------------------------------------------------------------------- +%%% @author licheng5 +%%% @copyright (C) 2021, +%%% @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, <>}, 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, <>}, 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, <>}, 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, <>}, State}; + +websocket_handle({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, <>}, 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, <>}, 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, <>}, 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, <>}, 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, <>}, 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, <>}, 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, <>}, 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, <>}, 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. \ No newline at end of file diff --git a/config/sys.config b/config/sys-dev.config similarity index 92% rename from config/sys.config rename to config/sys-dev.config index db156e1..bb89e82 100644 --- a/config/sys.config +++ b/config/sys-dev.config @@ -7,6 +7,13 @@ {backlog, 10240} ]}, + {redis_server, [ + {port, 16379}, + {acceptors, 500}, + {max_connections, 10240}, + {backlog, 10240} + ]}, + %% 目标服务器地址 {emqx_server, [ {host, {39, 98, 184, 67}}, @@ -18,6 +25,11 @@ {retry_interval, 5} ]}, + %% 权限检验时的预埋token + {pre_tokens, [ + {<<"test">>, <<"iot2023">>} + ]}, + {pools, [ %% mysql连接池配置 {mysql_pool, diff --git a/config/sys-prod.config b/config/sys-prod.config new file mode 100644 index 0000000..9533af7 --- /dev/null +++ b/config/sys-prod.config @@ -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}]} + ]} + ]} + +]. diff --git a/config/vm.args b/config/vm.args index 0ad9675..d7b21cd 100644 --- a/config/vm.args +++ b/config/vm.args @@ -1,11 +1,11 @@ --name iot +-sname iot -setcookie iot_cookie +K true +A30 --mnesia dir '"/usr/local/code/data/iot"' +-mnesia dir '"/usr/local/var/mnesia/iot"' -mnesia dump_log_write_threshold 50000 -mnesia dc_dump_limit 40 @@ -14,4 +14,4 @@ +K true +A 128 +P 1048576 -+t 10485760 \ No newline at end of file ++t 10485760 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..db83e87 --- /dev/null +++ b/docker-compose.yml @@ -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/ diff --git a/docs/endpoint.md b/docs/endpoint.md new file mode 100644 index 0000000..8e50a0f --- /dev/null +++ b/docs/endpoint.md @@ -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}; 发送的时候会替换成对应的点位编码 +``` \ No newline at end of file diff --git a/docs/host_mocker.html b/docs/host_mocker.html new file mode 100644 index 0000000..6edd5c8 --- /dev/null +++ b/docs/host_mocker.html @@ -0,0 +1,31 @@ + + + + + Title + + +
+

Hello World

+
+ + + + + + \ No newline at end of file diff --git a/docs/websocket.md b/docs/websocket.md new file mode 100644 index 0000000..1f74c1b --- /dev/null +++ b/docs/websocket.md @@ -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 \ No newline at end of file diff --git a/rebar.config b/rebar.config index 499fec7..e077e67 100644 --- a/rebar.config +++ b/rebar.config @@ -3,11 +3,11 @@ {poolboy, ".*", {git, "https://github.com/devinus/poolboy.git", {tag, "1.5.1"}}}, {hackney, ".*", {git, "https://github.com/benoitc/hackney.git", {tag, "1.16.0"}}}, {sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}}, - {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"}}}, {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"}}}, - {emqtt, ".*", {git, "https://github.com/emqx/emqtt", {tag, "v1.2.0"}}}, {lager, ".*", {git,"https://github.com/erlang-lager/lager.git", {tag, "3.9.2"}}} ]}. @@ -43,3 +43,6 @@ }]}]}. {erl_opts, [{parse_transform,lager_transform}]}. + +{rebar_packages_cdn, "https://hexpm.upyun.com"}. + diff --git a/rebar.lock b/rebar.lock index 4deee4f..0ab5093 100644 --- a/rebar.lock +++ b/rebar.lock @@ -2,23 +2,18 @@ [{<<"certifi">>,{pkg,<<"certifi">>,<<"2.5.2">>},1}, {<<"cowboy">>, {git,"https://github.com/ninenines/cowboy.git", - {ref,"c998673eb009da2ea4dc0e6ef0332534cf679cc4"}}, + {ref,"9e600f6c1df3c440bc196b66ebbc005d70107217"}}, 0}, {<<"cowlib">>, {git,"https://github.com/ninenines/cowlib", - {ref,"106ba84bb04537879d8ce59321a04e0682110b91"}}, + {ref,"cc04201c1d0e1d5603cd1cde037ab729b192634c"}}, 1}, - {<<"emqtt">>, - {git,"https://github.com/emqx/emqtt", - {ref,"55e50041cc5b3416067c120eadb8774f1d3d1f4a"}}, + {<<"esockd">>, + {git,"https://github.com/emqx/esockd.git", + {ref,"d9ce4024cc42a65e9a05001997031e743442f955"}}, 0}, {<<"fs">>,{pkg,<<"fs">>,<<"6.1.1">>},1}, - {<<"getopt">>,{pkg,<<"getopt">>,<<"1.0.1">>},1}, {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1}, - {<<"gun">>, - {git,"https://github.com/ninenines/gun", - {ref,"e7dd9f227e46979d8073e71c683395a809b78cb4"}}, - 1}, {<<"hackney">>, {git,"https://github.com/benoitc/hackney.git", {ref,"f3e9292db22c807e73f57a8422402d6b423ddf5f"}}, @@ -48,19 +43,18 @@ 0}, {<<"ranch">>, {git,"https://github.com/ninenines/ranch", - {ref,"9b8ed47d789412b0021bfc1f94f1c17c387c721c"}}, + {ref,"a692f44567034dacf5efcaa24a24183788594eb7"}}, 1}, {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},1}, {<<"sync">>, {git,"https://github.com/rustyio/sync.git", - {ref,"3f0049e809ffe303ae2cd395217a025ce6e758ae"}}, + {ref,"f13e61a79623290219d7c10dff1dd94d91eee963"}}, 0}, {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.5.0">>},2}]}. [ {pkg_hash,[ {<<"certifi">>, <<"B7CFEAE9D2ED395695DD8201C57A2D019C0C43ECAF8B8BCB9320B40D6662F340">>}, {<<"fs">>, <<"9D147B944D60CFA48A349F12D06C8EE71128F610C90870BDF9A6773206452ED0">>}, - {<<"getopt">>, <<"C73A9FA687B217F2FF79F68A3B637711BB1936E712B521D8CE466B29CBF7808A">>}, {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>}, {<<"idna">>, <<"1D038FB2E7668CE41FBF681D2C45902E52B3CB9E9C77B55334353B222C2EE50C">>}, {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, @@ -70,7 +64,6 @@ {pkg_hash_ext,[ {<<"certifi">>, <<"3B3B5F36493004AC3455966991EAF6E768CE9884693D9968055AEEEB1E575040">>}, {<<"fs">>, <<"EF94E95FFE79916860649FED80AC62B04C322B0BB70F5128144C026B4D171F8B">>}, - {<<"getopt">>, <<"53E1AB83B9CEB65C9672D3E7A35B8092E9BDC9B3EE80721471A161C10C59959C">>}, {<<"goldrush">>, <<"99CB4128CFFCB3227581E5D4D803D5413FA643F4EB96523F77D9E6937D994CEB">>}, {<<"idna">>, <<"A02C8A1C4FD601215BB0B0324C8A6986749F807CE35F25449EC9E69758708122">>}, {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>}, diff --git a/shell b/shell new file mode 100644 index 0000000..c6a5023 --- /dev/null +++ b/shell @@ -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 \ No newline at end of file