diff --git a/apps/iot/include/iot.hrl b/apps/iot/include/iot.hrl index da85104..4bb69bd 100644 --- a/apps/iot/include/iot.hrl +++ b/apps/iot/include/iot.hrl @@ -9,256 +9,15 @@ -author("licheng5"). %% 主机状态定义 --define(HOST_STATUS_INACTIVE, 0). --define(HOST_STATUS_ONLINE, 1). --define(HOST_STATUS_OFFLINE, 2). --define(HOST_STATUS_DELETE, 3). +-define(HOST_STATUS_INACTIVE, -1). %% 未接入 +-define(HOST_STATUS_OFFLINE, 0). %% 离线 +-define(HOST_STATUS_ONLINE, 1). %% 在线 -%% 终端状态 --define(TERMINAL_STATUS_OFFLINE, 0). --define(TERMINAL_STATUS_ONLINE, 1). +%% 下发的任务状态 +-define(TASK_STATUS_INIT, -1). %% 未接入 +-define(TASK_STATUS_FAILED, 0). %% 离线 +-define(TASK_STATUS_OK, 1). %% 在线 -%% cpu相关 --record(cpu_metric, { - %% cpu编号 - num = 1, - %% 负载 - load = 0 -}). - -%% 内存相关, 单位kb --record(memory_metric, { - %% 使用量 - used = 0, - %% 空余 - free = 0, - %% 总量 - total = 0 -}). - -%% 硬盘相关, 单位kb --record(disk_metric, { - %% 使用量 - used = 0, - %% 空余 - free = 0, - %% 总量 - total = 0 -}). - -%% 接口相关 --record(interface_metric, { - %% 接口名称 - name = <<>>, - %% 接口描述 - desc = <<>>, - %% 接口详情, 可以是元素的json - detail = <<>>, - %% 接口状态 - status = 0 -}). - -%% 微服务指标收集 --record(service_metric, { - %% 标识 - symbol :: binary(), - %% 名称 - name :: binary(), - %% 当前最新值 - last_value = 0 :: number(), - %% 单位, 如电压: a、v - unit :: any(), - %% 历史值序列, 队列;保存最近的100数值, 格式为: [{ts, value}] - queue = queue:new() :: queus:queue(), - %% 最后更新时间 - update_ts = 0 :: integer() -}). - -%% 主机指标, 主机和指标之间是一对一的关系,可以分离出新的表 --record(host_metric, { - %% cpu相关 - cpus = [], - %% cpu温度 - cpu_temperature = 0, - %% 内存状态 - memory = #memory_metric{}, - %% 硬盘状态 - disk = #disk_metric{}, - %% 接口状态 - interfaces = [] -}). - -%% id生成器 --record(id_generator, { - name :: atom(), - seq = 0 :: integer() -}). - -%% 主机定义 --record(host, { - %% ID - host_id :: binary(), - %% 序列号 - serial_number = <<>> :: binary(), - %% 名称 - name = <<>> :: binary(), - %% 型号 - model = <<>> :: binary(), - %% 单元网格编号 - cell_id :: integer(), - %% rsa公钥 - public_rsa = <<>> :: binary(), - %% aes的key, 后续通讯需要基于这个加密 - aes = <<>> :: binary(), - metric = #host_metric{}, - %% 接入时间 - activated_ts = 0 :: integer(), - %% 最后上线时间 - update_ts = 0 :: integer(), - %% 主机状态 - status = ?HOST_STATUS_INACTIVE :: integer() -}). - -%% 终端设备表 --record(terminal, { - %% 设备ID - terminal_id :: integer(), - %% 关联主机 - host_id :: binary(), - %% 序列号 - serial_number = <<>> :: binary(), - %% 名称 - name :: binary(), - %% 设备编码 - code :: binary(), - %% 接入协议 - access_protocol :: binary(), - %% 产品ID,枚举类型 - product_id = 0 :: integer(), - %% 厂商ID,枚举类型 - vendor_id = 0 :: integer(), - %% 型号 - model = <<>> :: binary(), - %% 所在单元ID,管理系统负责 - cell_id = 0 :: integer(), - %% 终端状态 - status = 0 :: integer(), - %% 最后上线时间 - update_ts = 0 :: integer() -}). - -%% 微服务表 --record(service, { - %% ID, 基于UUID生成 - service_id :: binary(), - %% 关联主机 - host_id :: binary(), - %% 名称 - name :: binary(), - %% 类型 - category :: binary(), - %% 应用对象 - apply_object :: binary(), - %% 版本 - version :: binary(), - %% 执行次数 - execute_count = 0, - %% 部署时间 - deploy_ts = 0 :: integer(), - %% 指标, 终端指标涉及到查询效率和更新效率的问题,因此合并到一起 - metrics = [] :: [#service_metric{}], - status = 0 -}). - -%% scenario 应用场景 --record(scenario, { - %% 场景id,自增id - scenario_id :: integer(), - %% 名称 - name :: binary(), - %% 场景描述 - desc :: binary(), - %% 场景规则数据 - rule :: binary(), - %% 最后更新时间 - update_ts = 0 :: integer(), - %% 状态 - status :: integer() -}). - -%% 场景部署关系表 --record(scenario_deploy, { - %% id,自增id - deploy_id :: integer(), - %% 场景id - scenario_id :: integer(), - %% 主机id - host_id :: binary(), - %% 创建时间 - create_ts = 0 :: integer(), - %% 更新时间 - update_ts = 0 :: integer(), - %% 状态 - status = 0:: integer() -}). - -%% 工单管理 --record(issue, { - issue_id :: integer(), - %% 工单名称 - name :: binary(), - %% 创建用户 - uid :: integer(), - %% 部署类型 - deploy_type :: integer(), - %% 关联的id, 微服务id或者场景id - assoc_id :: any(), - %% 主机的id列表 - hosts = [] :: list(), - %% 超时时间 - timeout = 0 :: integer(), - %% 部署结果 - results = [], - %% 创建时间 - create_ts = 0 :: integer(), - %% 工单状态 - status = 0 :: integer() -}). - --record(http_endpoint, { - url = <<>> -}). - --record(kafka_endpoint, { - -}). - -%% 数据转换规则表 -record(router, { - router_id :: integer(), - %% 名称 - name = <<>>, - %% 数据过滤规则 - rule = <<>>, - %% 对端 - endpoint, - %% 状态 - status = 0 -}). - -%% 设备分类 --define(DEVICE_HOST, 1). --define(DEVICE_TERMINAL, 2). - -%% 操作日志表 --record(log, { - log_id :: integer(), - %% 操作名称名称 - action_name = <<>>, - %% 设备分类名称 - device_type :: integer(), - %% 关联ID - assoc_id :: term(), - %% 创建时间 - create_ts = 0 :: integer() + router_id }). \ No newline at end of file diff --git a/apps/iot/src/database/device_bo.erl b/apps/iot/src/database/device_bo.erl new file mode 100644 index 0000000..bca4139 --- /dev/null +++ b/apps/iot/src/database/device_bo.erl @@ -0,0 +1,17 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @doc +%%% +%%% @end +%%% Created : 16. 5月 2023 12:48 +%%%------------------------------------------------------------------- +-module(device_bo). +-author("aresei"). +-include("iot.hrl"). + +%% API +-export([get_device_by_uuid/1]). + +get_device_by_uuid(UUID) when is_binary(UUID) -> + mysql_client:get_row(<<"SELECT * FROM device WHERE uuid = ? LIMIT 1">>, [UUID]). \ No newline at end of file diff --git a/apps/iot/src/database/host_bo.erl b/apps/iot/src/database/host_bo.erl new file mode 100644 index 0000000..d7e8450 --- /dev/null +++ b/apps/iot/src/database/host_bo.erl @@ -0,0 +1,46 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @doc +%%% +%%% @end +%%% Created : 16. 5月 2023 12:48 +%%%------------------------------------------------------------------- +-module(host_bo). +-author("aresei"). +-include("iot.hrl"). + +%% API +-export([get_all_hosts/0, change_status/2, is_authorized/1, get_host/1, get_host_by_uuid/1, create_host/1]). + +-spec get_all_hosts() -> UUIDList :: [binary()]. +get_all_hosts() -> + case mysql_client:get_all(<<"SELECT uuid FROM host where uuid != ''">>) of + {ok, Hosts} -> + lists:map(fun(#{<<"uuid">> := UUID}) -> UUID end, Hosts); + {error, _Reason} -> + [] + end. + +get_host(HostId) when is_integer(HostId) -> + mysql_client:get_row(<<"SELECT * FROM host WHERE id = ? LIMIT 1">>, [HostId]). + +get_host_by_uuid(UUID) when is_binary(UUID) -> + mysql_client:get_row(<<"SELECT * FROM host WHERE uuid = ? LIMIT 1">>, [UUID]). + +create_host(UUID) when is_binary(UUID) -> + mysql_client:insert(<<"host">>, #{<<"UUID">> => UUID, <<"status">> => ?HOST_STATUS_INACTIVE}, true). + +%% 修改主机的状态 +-spec change_status(UUID :: binary(), Status :: integer()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}. +change_status(UUID, Status) when is_binary(UUID), is_integer(Status) -> + mysql_client:update_by(<<"UPDATE host SET status = ? WHERE uuid = ? LIMIT 1">>, [Status, UUID]). + +%% 判断主机是否已经授权 +is_authorized(HostId) when is_integer(HostId) -> + case mysql_client:get_row(<<"SELECT * FROM host WHERE id = ? LIMIT 1">>, [HostId]) of + {ok, #{<<"status">> := Status}} when Status =/= ?HOST_STATUS_INACTIVE -> + true; + _ -> + false + end. \ No newline at end of file diff --git a/apps/iot/src/database/task_logs_bo.erl b/apps/iot/src/database/task_logs_bo.erl new file mode 100644 index 0000000..ea60ac1 --- /dev/null +++ b/apps/iot/src/database/task_logs_bo.erl @@ -0,0 +1,19 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @doc +%%% +%%% @end +%%% Created : 16. 5月 2023 12:48 +%%%------------------------------------------------------------------- +-module(task_logs_bo). +-author("aresei"). +-include("iot.hrl"). + +%% API +-export([change_status/2]). + +%% 修改主机的状态 +-spec change_status(TaskId :: integer(), Status :: integer()) -> {ok, AffectedRow :: integer()} | {error, Reason :: any()}. +change_status(TaskId, Status) when is_integer(TaskId), is_integer(Status) -> + mysql_client:update_by(<<"UPDATE task_logs SET status = ? WHERE id = ? LIMIT 1">>, [Status, TaskId]). \ No newline at end of file diff --git a/apps/iot/src/http_handler/http_host_handler.erl b/apps/iot/src/http_handler/http_host_handler.erl index f38ac8a..1e63077 100644 --- a/apps/iot/src/http_handler/http_host_handler.erl +++ b/apps/iot/src/http_handler/http_host_handler.erl @@ -17,131 +17,103 @@ %% helper methods %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% 添加主机 -handle_request("POST", "/host/create", _, HostInfo) -> - lager:debug("[host_handler] create post params: ~p", [HostInfo]), - - case convert_host_info(HostInfo) of - {ok, Host} -> - HostId = iot_util:uuid(), - case host_model:add_host(Host#host{host_id = HostId}) of - ok -> - {ok, 200, iot_util:json_data(HostId)}; - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(-1, Reason)}; - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(-1, <<"database error">>)} - end; - {error, Reason} -> - {ok, 200, iot_util:json_error(-1, Reason)} - end; - -%% 批量导入 -handle_request("POST", "/host/batch_import", _, HostInfos) -> - lager:debug("[host_handler] batch_create post params: ~p", [HostInfos]), - - {SuccessList, FailedList} = lists:foldl(fun(HostInfo, {SuccessList0, FailedList0}) -> - case convert_host_info(HostInfo) of - {ok, Host = #host{serial_number = SerialNumber}} -> - HostId = iot_util:uuid(), - case host_model:add_host(Host#host{host_id = HostId}) of - ok -> - SuccessItem = #{<<"serial_number">> => SerialNumber, <<"host_id">> => HostId}, - {[SuccessItem|SuccessList0], FailedList0}; - {error, Reason} when is_binary(Reason) -> - ErrorItem = maps:put(<<"error_message">>, Reason, HostInfo), - {SuccessList0, [ErrorItem|FailedList0]} - end; - {error, Reason} -> - ErrorItem = maps:put(<<"error_message">>, Reason, HostInfo), - {SuccessList0, [ErrorItem|FailedList0]} - end - end, {[], []}, HostInfos), - ImportResult = #{ - <<"success_list">> => lists:reverse(SuccessList), - <<"failed_list">> => lists:reverse(FailedList) - }, - - {ok, 200, iot_util:json_data(ImportResult)}; - -handle_request(_, "/host/list", GetParams, PostParams) -> - Page0 = maps:get(<<"page">>, GetParams, <<"1">>), - Size0 = maps:get(<<"size">>, GetParams, <<"10">>), - Page = binary_to_integer(Page0), - Size = binary_to_integer(Size0), - - true = Page > 0 andalso Size > 0, - Start = (Page - 1) * Size, - - %% 处理查询条件 - Model = maps:get(<<"model">>, PostParams, <<"">>), - CellId = maps:get(<<"cell_id">>, PostParams, <<"">>), - CellId1 = case CellId =/= <<>> of - true -> binary_to_integer(CellId); - false -> 0 - end, - - MatchSpec = host_model:match_spec([{model, Model}, {cell, CellId1}]), - case host_model:get_hosts(MatchSpec, Start, Size) of - {ok, Hosts, TotalNum} -> - Response = #{ - <<"hosts">> => lists:map(fun host_model:to_map/1, Hosts), - <<"stat">> => host_model:get_stat(), - <<"total_num">> => TotalNum - }, - - lager:debug("resp is: ~p", [Response]), - - {ok, 200, iot_util:json_data(Response)}; - {error, Reason} -> - lager:warning("[host_handler] get a error: ~p", [Reason]), - {ok, 200, iot_util:json_error(404, <<"database error">>)} - end; - -handle_request("GET", "/host/detail", #{<<"id">> := HostId}, _) -> - lager:debug("[host_handler] detail id is: ~p", [HostId]), - case host_model:get_host(HostId) of +handle_request("GET", "/host/metric", #{<<"uuid">> := UUID}, _) -> + lager:debug("[host_handler] get host metric uuid is: ~p", [UUID]), + case iot_host:get_pid(UUID) of undefined -> {ok, 200, iot_util:json_error(404, <<"host not found">>)}; - {ok, Host} -> - HostInfo = host_model:to_map(Host), - %% 获取终端信息 - {ok, Terminals0} = terminal_model:get_host_terminals(HostId), - Terminals = lists:map(fun terminal_model:to_map/1, Terminals0), - HostInfo1 = maps:put(<<"terminals">>, Terminals, HostInfo), - %% 获取微服务信息 - {ok, Services0} = service_model:get_host_services(HostId), - Services = lists:map(fun service_model:to_map/1, Services0), - HostInfo2 = maps:put(<<"services">>, Services, HostInfo1), - - %% 获取部署应用场景 - {ok, DeployList0} = scenario_deploy_model:get_host_deploy_list(HostId), - DeployList = lists:map(fun scenario_deploy_model:to_map/1, DeployList0), - %% 获取部署的场景信息 - DeployList1 = lists:map(fun(DeployInfo = #{<<"scenario_id">> := ScenarioId}) -> - case scenario_model:get_scenario(ScenarioId) of - {ok, Scenario} -> - ScenarioInfo = scenario_model:to_map(Scenario), - DeployInfo#{<<"scenario_info">> => ScenarioInfo}; - undefined -> - DeployInfo#{<<"scenario_info">> => #{}} - end - end, DeployList), - - HostInfo3 = maps:put(<<"deploy_list">>, DeployList1, HostInfo2), - - {ok, 200, iot_util:json_data(HostInfo3)} + Pid when is_pid(Pid) -> + {ok, MetricInfo} = iot_host:get_metric(Pid), + case map_size(MetricInfo) > 0 of + true -> + {ok, 200, iot_util:json_data(MetricInfo)}; + false -> + {ok, 200, iot_util:json_error(404, <<"no metric info">>)} + end end; -handle_request("POST", "/host/update", _, Fields0 = #{<<"host_id">> := HostId}) when is_binary(HostId) -> - lager:debug("[host_handler] post params is: ~p", [Fields0]), +%% 重新加载对应的主机信息 +handle_request("POST", "/host/reload", _, #{<<"uuid">> := UUID}) when is_binary(UUID) -> + lager:debug("[host_handler] will reload host uuid: ~p", [UUID]), + case iot_host_sup:ensured_host_started(UUID) of + {ok, Pid} when is_pid(Pid) -> + lager:debug("[host_handler] already_started reload host uuid: ~p, success", [UUID]), + case iot_host:reload(Pid) of + ok -> + {ok, 200, iot_util:json_data(<<"success">>)}; + {error, ReloadError} -> + lager:debug("[host_handler] reload host uuid: ~p, error: ~p", [UUID, ReloadError]), + {ok, 200, iot_util:json_error(400, <<"reload error">>)} + end; + Error -> + lager:debug("[host_handler] reload host uuid: ~p, error: ~p", [UUID, Error]), + {ok, 200, iot_util:json_error(404, <<"reload error">>)} + end; - Fields = maps:remove(<<"host_id">>, Fields0), - case host_model:update_host(HostId, Fields) of +%% 删除对应的主机信息 +handle_request("POST", "/host/delete", _, #{<<"uuid">> := UUID}) when is_binary(UUID) -> + lager:debug("[host_handler] will delete host uuid: ~p", [UUID]), + + case iot_host_sup:delete_host(UUID) of ok -> {ok, 200, iot_util:json_data(<<"success">>)}; - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(-1, Reason)} + {error, Reason} -> + lager:debug("[host_handler] delete host uuid: ~p, get error is: ~p", [UUID, Reason]), + {ok, 200, iot_util:json_error(404, <<"error">>)} + end; + +%% 下发参数 +handle_request("POST", "/host/publish_command", _, + PostParams = #{<<"uuid">> := UUID, <<"command_type">> := CommandType, <<"task_id">> := TaskId, <<"timeout">> := Timeout, <<"params">> := Params}) + when is_binary(UUID), is_integer(TaskId), is_integer(Timeout), is_integer(CommandType) -> + + lager:debug("[http_host_handler] publish_command body is: ~p", [PostParams]), + case iot_host:get_pid(UUID) of + undefined -> + {ok, 200, iot_util:json_error(404, <<"host not found">>)}; + Pid when is_pid(Pid) -> + Reply = #{ + <<"t_id">> => integer_to_binary(TaskId), + <<"t">> => Timeout, + <<"ts">> => iot_util:current_time(), + <<"m">> => Params + }, + BinReply = jiffy:encode(append_service_name(PostParams, Reply), [force_utf8]), + + case iot_host:build_command(Pid, CommandType, 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, Topic, BinCommand} -> + case iot_mqtt_publisher:publish(Topic, BinCommand, 2) of + {ok, Ref} -> + receive + {ok, Ref, _PacketId} -> + {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>>)} + end + end + end; + +%% 处理主机的授权, 激活处理 +handle_request("POST", "/host/activate", _, #{<<"uuid">> := UUID}) when is_binary(UUID) -> + case iot_host_sup:ensured_host_started(UUID) of + {error, Reason} -> + lager:debug("[host_handler] activate host_id: ~p, failed with reason: ~p", [UUID, Reason]), + {ok, 200, iot_util:json_error(-1, <<"host not found">>)}; + {ok, Pid} when is_pid(Pid) -> + lager:debug("[host_handler] reload host_id: ~p, success", [UUID]), + ok = iot_host:activate(Pid), + {ok, 200, iot_util:json_data(<<"success">>)} end; handle_request(_, Path, _, _) -> @@ -152,31 +124,8 @@ handle_request(_, Path, _, _) -> %% helper methods %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -convert_host_info(#{<<"name">> := Name, <<"model">> := Model, <<"cell_id">> := CellId, <<"serial_number">> := SerialNumber}) -> - CheckResult = if - Name == <<>> -> - {error, <<"name is empty">>}; - Model == <<>> -> - {error, <<"model is empty">>}; - SerialNumber == <<>> -> - {error, <<"serial_number is empty">>}; - not is_integer(CellId) orelse CellId < 0 -> - {ok, <<"cell_id is error">>}; - true -> - ok - end, - - case CheckResult of - ok -> - {ok, #host{ - serial_number = SerialNumber, - name = Name, - model = Model, - cell_id = CellId, - status = ?HOST_STATUS_INACTIVE - }}; - {error, Reason} -> - {error, Reason} - end; -convert_host_info(_) -> - {error, <<"miss required param">>}. \ No newline at end of file +%% 追加service_name参数 +append_service_name(#{<<"serivce_name">> := ServiceName}, Reply) when is_binary(ServiceName), ServiceName =/= <<"">> -> + Reply#{<<"to">> => ServiceName}; +append_service_name(_, Reply) -> + Reply. \ No newline at end of file diff --git a/apps/iot/src/http_handler/http_iot_handler.erl b/apps/iot/src/http_handler/http_iot_handler.erl deleted file mode 100644 index 4fa9b44..0000000 --- a/apps/iot/src/http_handler/http_iot_handler.erl +++ /dev/null @@ -1,160 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2023, -%%% @doc -%%% -%%% @end -%%% Created : 06. 3月 2023 14:29 -%%%------------------------------------------------------------------- --module(http_iot_handler). --author("licheng5"). --include("iot.hrl"). - -%% API --export([handle_request/4]). - -%% 下发参数 -handle_request("POST", "/iot/send_params", _, PostParams = #{<<"host_id">> := HostId, <<"service_name">> := ServiceName, <<"params">> := Params}) -> - lager:debug("body is: ~p", [PostParams]), - - case host_model:activate(HostId) of - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(404, Reason)}; - {ok, #host{aes = Aes}} -> - Reply = #{ - <<"t">> => 1, - <<"b">> => #{ - <<"t_id">> => iot_util:uuid() , - <<"to">> => ServiceName, - <<"t">> => 10, - <<"m">> => Params - } - }, - EncMsg = iot_cipher_aes:encrypt(Aes, Aes, Reply), - - iot_emqtt_client:publish(<<"clients.cmd.", HostId/binary>>, EncMsg, 1), - lager:debug("enc_reply is: ~p", [EncMsg]), - - {ok, 200, iot_util:json_data(<<"success">>)} - end; - -%% 下发采集项 -handle_request("POST", "/iot/send_metrics", _, - PostParams = #{<<"host_id">> := HostId, <<"service_name">> := ServiceName, <<"metrics">> := Metrics}) -> - - lager:debug("body is: ~p", [PostParams]), - - case host_model:activate(HostId) of - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(404, Reason)}; - {ok, #host{aes = Aes}} -> - Reply = #{ - <<"t">> => 2, - <<"b">> => #{ - <<"t_id">> => iot_util:uuid() , - <<"to">> => ServiceName, - <<"t">> => 10, - <<"m">> => Metrics - } - }, - EncMsg = iot_cipher_aes:encrypt(Aes, Aes, Reply), - - iot_emqtt_client:publish(<<"clients.cmd.", HostId/binary>>, EncMsg, 1), - lager:debug("enc_reply is: ~p", [EncMsg]), - - {ok, 200, iot_util:json_data(<<"success">>)} - end; - -%% 下发微服务 -handle_request("POST", "/iot/send_mirco_service", _, - PostParams = #{<<"host_id">> := HostId, <<"args">> := Args}) -> - - lager:debug("body is: ~p", [PostParams]), - - case host_model:activate(HostId) of - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(404, Reason)}; - {ok, #host{aes = Aes}} -> - Reply = #{ - <<"t">> => 3, - <<"b">> => #{ - <<"t_id">> => iot_util:uuid() , - <<"t">> => 10, - <<"m">> => Args - } - }, - EncMsg = iot_cipher_aes:encrypt(Aes, Aes, Reply), - - iot_emqtt_client:publish(<<"clients.cmd.", HostId/binary>>, EncMsg, 1), - lager:debug("enc_reply is: ~p", [EncMsg]), - - {ok, 200, iot_util:json_data(<<"success">>)} - end; - -%% 下发数据流图 -handle_request("POST", "/iot/send_data_flow", _, - PostParams = #{<<"host_id">> := HostId, <<"args">> := Args}) -> - - lager:debug("body is: ~p", [PostParams]), - - case host_model:activate(HostId) of - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(404, Reason)}; - {ok, #host{aes = Aes}} -> - Reply = #{ - <<"t">> => 4, - <<"b">> => #{ - <<"t_id">> => iot_util:uuid() , - <<"t">> => 10, - <<"m">> => Args - } - }, - EncMsg = iot_cipher_aes:encrypt(Aes, Aes, Reply), - - iot_emqtt_client:publish(<<"clients.cmd.", HostId/binary>>, EncMsg, 1), - lager:debug("enc_reply is: ~p", [EncMsg]), - - {ok, 200, iot_util:json_data(<<"success">>)} - end; - -%% 处理命令下发 -handle_request("POST", "/iot/send_command", _, Params = #{<<"host_id">> := HostId}) -> - lager:debug("body is: ~p", [Params]), - - case host_model:activate(HostId) of - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(404, Reason)}; - {ok, #host{aes = Aes, public_rsa = PubKey}} -> - Reply = #{ - <<"a">> => true, - <<"aes">> => Aes, - <<"reply">> => <<"client.reply.", HostId/binary>> - }, - EncReply = iot_cipher_rsa:encode(Reply, PubKey), - - %% TODO 发送消息到终端 - lager:debug("enc_reply is: ~p", [EncReply]), - - {ok, 200, iot_util:json_data(<<"success">>)} - end; - -%% 处理主机的授权 -handle_request("POST", "/iot/host_auth", _, Params = #{<<"host_id">> := HostId}) -> - lager:debug("body is: ~p", [Params]), - - case host_model:activate(HostId) of - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(404, Reason)}; - {ok, #host{aes = Aes, public_rsa = PubKey}} -> - Reply = #{ - <<"a">> => true, - <<"aes">> => Aes, - <<"reply">> => <<"client.reply.", HostId/binary>> - }, - EncReply = iot_cipher_rsa:encode(Reply, PubKey), - - %% TODO 发送消息到终端 - lager:debug("enc_reply is: ~p", [EncReply]), - - {ok, 200, iot_util:json_data(<<"success">>)} - end. \ No newline at end of file diff --git a/apps/iot/src/http_handler/http_log_handler.erl b/apps/iot/src/http_handler/http_log_handler.erl deleted file mode 100644 index 8c501a8..0000000 --- a/apps/iot/src/http_handler/http_log_handler.erl +++ /dev/null @@ -1,41 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2020, -%%% @doc -%%% -%%% @end -%%% Created : 26. 4月 2020 3:36 下午 -%%%------------------------------------------------------------------- --module(http_log_handler). --author("licheng5"). --include("iot.hrl"). - -%% API --export([handle_request/4]). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -handle_request(_, "/log/list", GetParams, _PostParams) -> - Size0 = maps:get(<<"size">>, GetParams, <<"10">>), - Size = binary_to_integer(Size0), - true = Size > 0, - - case log_model:get_last_logs(Size) of - {ok, Logs} -> - LogInfos = lists:map(fun log_model:to_map/1, Logs), - - {ok, 200, iot_util:json_data(LogInfos)}; - {error, Reason} -> - lager:warning("[host_handler] get a error: ~p", [Reason]), - {ok, 200, iot_util:json_error(-1, <<"database error">>)} - end; - -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/http_handler/http_router_handler.erl b/apps/iot/src/http_handler/http_router_handler.erl deleted file mode 100644 index 8fbec02..0000000 --- a/apps/iot/src/http_handler/http_router_handler.erl +++ /dev/null @@ -1,64 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2020, -%%% @doc -%%% -%%% @end -%%% Created : 26. 4月 2020 3:36 下午 -%%%------------------------------------------------------------------- --module(http_router_handler). --author("licheng5"). --include("iot.hrl"). - -%% API --export([handle_request/4]). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -handle_request(_, "/router/list", Params, PostParams) -> - Page0 = maps:get(<<"page">>, Params, <<"1">>), - Size0 = maps:get(<<"size">>, Params, <<"10">>), - Page = binary_to_integer(Page0), - Size = binary_to_integer(Size0), - - true = Page > 0 andalso Size > 0, - Start = (Page - 1) * Size, - - %% 处理查询条件 - Name = maps:get(<<"name">>, PostParams, <<"">>), - - MatchSpec = router_model:match_spec([{name, Name}]), - case router_model:get_routers(MatchSpec, Start, Size) of - {ok, Routers, TotalNum} -> - Response = #{ - <<"routers">> => lists:map(fun(R) -> router_model:to_map(R) end, Routers), - <<"total_num">> => TotalNum - }, - - {ok, 200, iot_util:json_data(Response)}; - {error, Reason} -> - lager:warning("[host_handler] get a error: ~p", [Reason]), - {ok, 200, iot_util:json_error(404, <<"database error">>)} - end; - -handle_request("GET", "/router/detail", #{<<"router_id">> := RouterId}, _) -> - case router_model:get_router(RouterId) of - undefined -> - {ok, 200, iot_util:json_error(404, <<"router not found">>)}; - {ok, Router} -> - RouterInfo = router_model:to_map(Router), - - {ok, 200, iot_util:json_data(RouterInfo)} - end; - -handle_request("POST", "/router/changer_status", _, Params = #{<<"router_id">> := RouterId, <<"status">> := NStatus}) -> - lager:debug("[router_handler] post params is: ~p", [Params]), - - case router_model:change_status(RouterId, NStatus) of - ok -> - {ok, 200, iot_util:json_data(<<"success">>)}; - {error, _} -> - {ok, 200, iot_util:json_error(404, <<"error">>)} - end. \ No newline at end of file diff --git a/apps/iot/src/http_handler/http_scenario_handler.erl b/apps/iot/src/http_handler/http_scenario_handler.erl deleted file mode 100644 index bf216ae..0000000 --- a/apps/iot/src/http_handler/http_scenario_handler.erl +++ /dev/null @@ -1,97 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2020, -%%% @doc -%%% -%%% @end -%%% Created : 26. 4月 2020 3:36 下午 -%%%------------------------------------------------------------------- --module(http_scenario_handler). --author("licheng5"). --include("iot.hrl"). - -%% API --export([handle_request/4]). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -handle_request(_, "/scenario/list", Params, PostParams) -> - Page0 = maps:get(<<"page">>, Params, <<"1">>), - Size0 = maps:get(<<"size">>, Params, <<"10">>), - Page = binary_to_integer(Page0), - Size = binary_to_integer(Size0), - - true = Page > 0 andalso Size > 0, - Start = (Page - 1) * Size, - - %% 处理查询条件 - Name = maps:get(<<"name">>, PostParams, <<"">>), - - MatchSpec = scenario_model:match_spec([{name, Name}]), - case scenario_model:get_scenario_list(MatchSpec, Start, Size) of - {ok, ScenarioList, TotalNum} -> - Response = #{ - <<"scenarios">> => lists:map(fun scenario_model:to_map/1, ScenarioList), - <<"total_num">> => TotalNum - }, - {ok, 200, iot_util:json_data(Response)}; - {error, Reason} -> - lager:warning("[http_scenario_handler] get a error: ~p", [Reason]), - {ok, 200, iot_util:json_error(404, <<"database error">>)} - end; - -handle_request("GET", "/scenario/detail", #{<<"id">> := ScenarioId0}, _) -> - ScenarioId = binary_to_integer(ScenarioId0), - case scenario_model:get_scenario(ScenarioId) of - undefined -> - {ok, 200, iot_util:json_error(404, <<"scenario not found">>)}; - {ok, Scenario} -> - ScenarioInfo = scenario_model:to_map(Scenario), - %% 获取场景下部署的主机列表 - {ok, DeployList0} = scenario_deploy_model:get_scenario_deploy_list(ScenarioId), - DeployList = lists:map(fun scenario_deploy_model:to_map/1, DeployList0), - ScenarioInfo1 = maps:put(<<"deploy_list">>, ScenarioInfo, DeployList), - %% 获取部署的主机信息 - ScenarioInfo2 = lists:map(fun(Info = #{<<"host_id">> := HostId}) -> - case host_model:get_host(HostId) of - {ok, Host} -> - HostInfo = host_model:to_map(Host), - Info#{<<"host_info">> => HostInfo}; - undefined -> - Info#{<<"host_info">> => #{}} - end - end, ScenarioInfo1), - - {ok, 200, iot_util:json_data(ScenarioInfo2)} - end; - -handle_request("POST", "/scenario/change_status", _, #{<<"scenario_id">> := ScenarioId, <<"status">> := Status}) when is_integer(ScenarioId), is_integer(Status) -> - case scenario_model:change_status(ScenarioId, Status) of - ok -> - {ok, 200, iot_util:json_data(<<"success">>)}; - {error, Reason} when is_binary(Reason) -> - lager:warning("[http_scenario_handler] change_status get a error: ~p", [Reason]), - {ok, 200, iot_util:json_error(404, Reason)} - end; - -%% 部署 -handle_request("POST", "/scenario/deploy", _, #{<<"scenario_id">> := ScenarioId, <<"status">> := Status}) when is_integer(ScenarioId), is_integer(Status) -> - case scenario_model:change_status(ScenarioId, Status) of - ok -> - {ok, 200, iot_util:json_data(<<"success">>)}; - {error, Reason} when is_binary(Reason) -> - lager:warning("[http_scenario_handler] change_status get a error: ~p", [Reason]), - {ok, 200, iot_util:json_error(404, Reason)} - end; - -%% 删除?? -handle_request("POST", "/scenario/change_status", _, #{<<"scenario_id">> := ScenarioId, <<"status">> := Status}) when is_integer(ScenarioId), is_integer(Status) -> - case scenario_model:change_status(ScenarioId, Status) of - ok -> - {ok, 200, iot_util:json_data(<<"success">>)}; - {error, Reason} when is_binary(Reason) -> - lager:warning("[http_scenario_handler] change_status get a error: ~p", [Reason]), - {ok, 200, iot_util:json_error(404, Reason)} - end. \ No newline at end of file diff --git a/apps/iot/src/http_handler/http_terminal_handler.erl b/apps/iot/src/http_handler/http_terminal_handler.erl deleted file mode 100644 index 806f10f..0000000 --- a/apps/iot/src/http_handler/http_terminal_handler.erl +++ /dev/null @@ -1,211 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2020, -%%% @doc -%%% -%%% @end -%%% Created : 26. 4月 2020 3:36 下午 -%%%------------------------------------------------------------------- --module(http_terminal_handler). --author("licheng5"). --include("iot.hrl"). - -%% API --export([handle_request/4]). - -%% 添加终端 -handle_request("POST", "/terminal/create", _, TerminalInfo) -> - lager:debug("[terminal_handler] create post params: ~p", [TerminalInfo]), - - case convert_terminal_info(TerminalInfo) of - {ok, Terminal} -> - TerminalId = terminal_model:next_id(), - case terminal_model:add_terminal(Terminal#terminal{terminal_id = TerminalId}) of - ok -> - {ok, 200, iot_util:json_data(TerminalId)}; - {error, Reason} when is_binary(Reason) -> - lager:warning("[host_handler] get a error: ~p", [Reason]), - {ok, 200, iot_util:json_error(-1, Reason)}; - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(-1, <<"database error">>)} - end; - {error, Error} -> - {ok, 200, iot_util:json_error(-1, Error)} - end; - -%% 批量导入 -handle_request("POST", "/terminal/batch_import", _, TerminalInfos) -> - lager:debug("[terminal_handler] batch_import post params: ~p", [TerminalInfos]), - - {SuccessList, FailedList} = lists:foldl(fun(TerminalInfo, {SuccessList0, FailedList0}) -> - case convert_terminal_info(TerminalInfo) of - {ok, Terminal = #terminal{serial_number = SerialNumber}} -> - TerminalId = terminal_model:next_id(), - case terminal_model:add_terminal(Terminal#terminal{terminal_id = TerminalId}) of - ok -> - SuccessItem = #{<<"serial_number">> => SerialNumber, <<"terminal_id">> => TerminalId}, - {[SuccessItem|SuccessList0], FailedList0}; - {error, Reason} when is_binary(Reason) -> - ErrorItem = maps:put(<<"error_message">>, Reason, TerminalInfo), - {SuccessList0, [ErrorItem|FailedList0]} - end; - {error, Reason} -> - ErrorItem = maps:put(<<"error_message">>, Reason, TerminalInfo), - {SuccessList0, [ErrorItem|FailedList0]} - end - end, {[], []}, TerminalInfos), - ImportResult = #{ - <<"success_list">> => lists:reverse(SuccessList), - <<"failed_list">> => lists:reverse(FailedList) - }, - - {ok, 200, iot_util:json_data(ImportResult)}; - -%% 获取终端的统计信息 -handle_request("GET", "/terminal/stat", _, _) -> - case terminal_model:get_stat() of - {ok, {TotalNum, Stat}} -> - StatInfo = #{ - <<"total_num">> => TotalNum, - <<"stat">> => lists:map(fun({Status, Num}) -> #{<<"status">> => Status, <<"num">> => Num} end, maps:to_list(Stat)) - }, - {ok, 200, iot_util:json_data(StatInfo)}; - {error, Reason} -> - lager:warning("[host_handler] get a error: ~p", [Reason]), - {ok, 200, iot_util:json_error(404, <<"database error">>)} - end; - -%% 获取终端的列表信息 -handle_request(_, "/terminal/list", GetParams, PostParams) -> - Page0 = maps:get(<<"page">>, GetParams, <<"1">>), - Size0 = maps:get(<<"size">>, GetParams, <<"10">>), - Page = binary_to_integer(Page0), - Size = binary_to_integer(Size0), - - true = Page > 0 andalso Size > 0, - Start = (Page - 1) * Size, - - %% 处理查询条件 - Model = maps:get(<<"model">>, PostParams, <<"">>), - CellId = maps:get(<<"cell_id">>, PostParams, <<"">>), - CellId1 = case CellId =/= <<>> of - true -> binary_to_integer(CellId); - false -> 0 - end, - - case terminal_model:find_terminals([{model, Model}, {cell, CellId1}], Start, Size) of - {ok, Terminals, TotalNum} -> - Response = #{ - <<"total_num">> => TotalNum, - <<"terminals">> => lists:map(fun terminal_model:to_map/1, Terminals) - }, - - lager:debug("resp is: ~p", [Response]), - - {ok, 200, iot_util:json_data(Response)}; - {error, Reason} -> - lager:warning("[host_handler] get a error: ~p", [Reason]), - {ok, 200, iot_util:json_error(404, <<"database error">>)} - end; - -%% 获取终端详情 -handle_request("GET", "/terminal/detail", #{<<"terminal_id">> := TerminalId}, _) -> - lager:debug("[terminal_handler] terminal detail id is: ~p", [TerminalId]), - case terminal_model:get_terminal(TerminalId) of - undefined -> - {ok, 200, iot_util:json_error(404, <<"terminal not found">>)}; - {ok, Terminal} -> - TerminalInfo = terminal_model:to_map(Terminal), - %% TODO 获取终端的数据信息 - - {ok, 200, iot_util:json_data(TerminalInfo)} - end; - -%% 更新状态信息 -handle_request("POST", "/terminal/change_status", _, #{<<"terminal_id">> := TerminalId, <<"status">> := Status}) when is_integer(TerminalId), is_integer(Status) -> - lager:debug("[terminal_handler] change_status id is: ~p", [TerminalId]), - case terminal_model:change_status(TerminalId, Status) of - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(-1, Reason)}; - ok -> - {ok, 200, iot_util:json_data(<<"success">>)} - end; - -%% 更新终端信息 -handle_request("POST", "/terminal/update", _, Fields0 = #{<<"terminal_id">> := TerminalId}) -> - lager:debug("[terminal_handler] update terminal params is: ~p", [Fields0]), - - Fields = maps:remove(<<"terminal_id">>, Fields0), - case terminal_model:update_terminal(TerminalId, Fields) of - ok -> - {ok, 200, iot_util:json_data(<<"success">>)}; - {error, Reason} when is_binary(Reason) -> - {ok, 200, iot_util:json_error(-1, Reason)} - end; - -handle_request(_, Path, _, _) -> - Path1 = list_to_binary(Path), - {ok, 200, iot_util:json_error(-1, <<"url: ", Path1/binary, " not found">>)}. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% 将map转换成record -convert_terminal_info(#{<<"host_id">> := HostId, <<"serial_number">> := SerialNumber, <<"name">> := Name, <<"code">> := Code, <<"access_protocol">> := AccessProtocol, - <<"product_id">> := ProductId, <<"vendor_id">> := VendorId, <<"model">> := Model, <<"cell_id">> := CellId}) -> - - CheckResult = if - Name == <<>> -> - {error, <<"name is empty">>}; - HostId == <<>> -> - {error, <<"host_id is empty">>}; - SerialNumber == <<>> -> - {error, <<"serial_number is empty">>}; - Code == <<>> -> - {error, <<"code is empty">>}; - AccessProtocol == <<>> -> - {error, <<"access_protocol is empty">>}; - Model == <<>> -> - {error, <<"model is empty">>}; - not is_integer(ProductId) orelse ProductId < 0 -> - {error, <<"product_id is error">>}; - not is_integer(VendorId) orelse VendorId < 0 -> - {error, <<"vendor_id is error">>}; - not is_integer(CellId) orelse CellId < 0 -> - {error, <<"cell_id is error">>}; - true -> - ok - end, - - case CheckResult of - ok -> - {ok, #terminal{ - %% 关联主机 - host_id = HostId, - %% 序列号 - serial_number = SerialNumber, - %% 名称 - name = Name, - %% 设备编码 - code = Code, - %% 接入协议 - access_protocol = AccessProtocol, - %% 产品ID,枚举类型 - product_id = ProductId, - %% 厂商ID,枚举类型 - vendor_id = VendorId, - %% 型号 - model = Model, - %% 所在单元ID - cell_id = CellId, - %% 终端状态 - status = ?TERMINAL_STATUS_OFFLINE, - %% 最后上线时间 - update_ts = iot_util:current_time() - }}; - {error, Reason} -> - {error, Reason} - end; -convert_terminal_info(_) -> - {error, <<"miss required param">>}. \ No newline at end of file diff --git a/apps/iot/src/influxdb/influx_client.erl b/apps/iot/src/influxdb/influx_client.erl new file mode 100644 index 0000000..87b41ad --- /dev/null +++ b/apps/iot/src/influxdb/influx_client.erl @@ -0,0 +1,190 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @doc +%%% +%%% @end +%%% Created : 30. 5月 2023 10:48 +%%%------------------------------------------------------------------- +-module(influx_client). +-author("aresei"). + +-behaviour(gen_server). + +%% API +-export([start_link/1, write/4, write/5, test/0]). +-export([get_precision/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(INFLUX_POOl, influx_pool). + +-record(state, { + host, + port, + token :: binary() +}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +test() -> + UUID = <<"device123123">>, + + lists:foreach(fun(Id) -> + Point = influx_point:new(<<"shui_biao">>, + [{<<"uuid">>, UUID}, {<<"service_name">>, <<"shui_biao">>}], + [{<<"cost">>, Id}], + iot_util:timestamp()), + + poolboy:transaction(influx_pool, fun(Pid) -> + write(Pid, <<"iot">>, <<"iot">>, [Point]) + end) + end, lists:seq(1, 100)). + +%% 获取时间标识符号 +-spec get_precision(Timestamp :: integer()) -> binary(). +get_precision(Timestamp) when is_integer(Timestamp) -> + case length(integer_to_list(Timestamp)) of + 10 -> + <<"s">>; + 13 -> + <<"ms">>; + 16 -> + <<"u">>; + 19 -> + <<"ns">>; + _ -> + <<"ms">> + end. + +-spec write(Pid :: pid(), Bucket :: binary(), Org :: binary(), Points :: list()) -> no_return(). +write(Pid, Bucket, Org, Points) when is_pid(Pid), is_binary(Bucket), is_binary(Org), is_list(Points) -> + write(Pid, Bucket, Org, <<"ms">>, Points). + +%% Precision的值为: ms|ns|s; 表示时间的精度,默认为毫秒(ms) +-spec write(Pid :: pid(), Bucket :: binary(), Org :: binary(), Precision :: binary(), Points :: list()) -> no_return(). +write(Pid, Bucket, Org, Precision, Points) when is_pid(Pid), is_binary(Bucket), is_binary(Org), is_binary(Precision), is_list(Points) -> + gen_server:cast(Pid, {write, Bucket, Org, Precision, Points}). + +%% @doc Spawns the server and registers the local name (unique) +-spec(start_link(Opts :: list()) -> + {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). +start_link(Opts) when is_list(Opts) -> + gen_server:start_link(?MODULE, [Opts], []). + +%%%=================================================================== +%%% 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([InfluxProps]) -> + Token = proplists:get_value(token, InfluxProps), + Host = proplists:get_value(host, InfluxProps), + Port = proplists:get_value(port, InfluxProps), + + {ok, #state{host = Host, port = Port, token = Token}}. + +%% @private +%% @doc Handling call messages +-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()}, + State :: #state{}) -> + {reply, Reply :: term(), NewState :: #state{}} | + {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} | + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | + {stop, Reason :: term(), NewState :: #state{}}). +handle_call(_Request, _From, State = #state{}) -> + {reply, ok, State}. + +%% @private +%% @doc Handling cast messages +-spec(handle_cast(Request :: term(), State :: #state{}) -> + {noreply, NewState :: #state{}} | + {noreply, NewState :: #state{}, timeout() | hibernate} | + {stop, Reason :: term(), NewState :: #state{}}). +handle_cast({write, Bucket, Org, Precision, Points}, State = #state{host = Host, port = Port, token = Token}) -> + %% 处理headers + Headers = [ + {<<"Accept">>, <<"application/json">>}, + {<<"Accept-Encoding">>, <<"identity">>}, + {<<"Content-Type">>, <<"text/plain; charset=utf-8">>}, + {<<"Content-Encoding">>, <<"gzip">>}, + {<<"Authorization">>, <<"Token ", Token/binary>>} + ], + + %% 处理points + PointLines = lists:map(fun influx_point:normalized/1, Points), + Body = iolist_to_binary(lists:join(<<"\n">>, PointLines)), + %% gzip压缩 + GZipBody = zlib:gzip(Body), + Query = uri_string:compose_query([{<<"bucket">>, Bucket}, {<<"org">>, Org}, {<<"precision">>, Precision}]), + + Url = uri_string:normalize(#{ + scheme => "http", + host => Host, + port => Port, + path => "/api/v2/write", + query => Query + }), + + lager:debug("[influx_client] url is: ~p, headers: ~p, body: ~p", [Url, Headers, Body]), + case hackney:request(post, Url, Headers, GZipBody, [{pool, influx_pool}]) of + {ok, StatusCode, _RespHeaders, ClientRef} -> + case StatusCode >= 200 andalso StatusCode < 300 of + true -> + case hackney:body(ClientRef) of + {ok, RespBody} -> + lager:debug("[influx_client] status_code: ~p, response body is: ~p", [StatusCode, RespBody]); + {error, Error} -> + lager:warning("[influx_client] response error is: ~p", [Error]) + end; + false -> + lager:debug("[influx_client] status_code: ~p, response body is: ~p", [StatusCode]), + hackney:close(ClientRef) + end; + {error, Reason} -> + lager:warning("[influx_client] request result is: ~p", [Reason]) + end, + + {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/influxdb/influx_point.erl b/apps/iot/src/influxdb/influx_point.erl new file mode 100644 index 0000000..dbaaf36 --- /dev/null +++ b/apps/iot/src/influxdb/influx_point.erl @@ -0,0 +1,54 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @doc +%%% +%%% @end +%%% Created : 30. 5月 2023 11:28 +%%%------------------------------------------------------------------- +-module(influx_point). +-author("aresei"). + +-record(point, { + measurement, + tags = [], + fields = [], + time = 0 :: integer() +}). + +%% API +-export([new/4, normalized/1]). + +new(Measurement, Tags, Fields, Timestamp) when is_binary(Measurement), is_list(Tags); is_map(Tags), is_list(Fields); is_map(Fields), is_integer(Timestamp) -> + #point{measurement = Measurement, tags = as_list(Tags), fields = as_list(Fields), time = Timestamp}. + +normalized(#point{measurement = Name, tags = Tags, fields = Fields, time = Time}) -> + NTags = lists:map(fun({N, V}) -> <> end, Tags), + NFields = lists:map(fun({K, V}) -> <> end, Fields), + + TagItems = lists:join(<<",">>, [Name | NTags]), + FieldItems = lists:join(<<",">>, NFields), + + NTime = case Time > 0 of + true -> Time; + false -> iot_util:timestamp() + end, + + erlang:iolist_to_binary([TagItems, <<" ">>, FieldItems, <<" ">>, integer_to_binary(NTime)]). + +field_val(V) when is_integer(V) -> + <<(integer_to_binary(V))/binary, "i">>; +field_val(V) when is_number(V) -> + <<(integer_to_binary(V))/binary, "u">>; +field_val(V) when is_binary(V) -> + <<$", V/binary, $">>; +field_val(true) -> + <<"true">>; +field_val(false) -> + <<"false">>. + +as_list(L) when is_list(L) -> + L; +as_list(L) when is_map(L) -> + maps:to_list(L). + diff --git a/apps/iot/src/iot.app.src b/apps/iot/src/iot.app.src index 27c94f7..63eac1e 100644 --- a/apps/iot/src/iot.app.src +++ b/apps/iot/src/iot.app.src @@ -13,6 +13,7 @@ parse_trans, hackney, poolboy, + mysql, emqtt, mnesia, crypto, diff --git a/apps/iot/src/iot_app.erl b/apps/iot/src/iot_app.erl index ee1f23c..6a48659 100644 --- a/apps/iot/src/iot_app.erl +++ b/apps/iot/src/iot_app.erl @@ -13,17 +13,16 @@ start(_StartType, _StartArgs) -> io:setopts([{encoding, unicode}]), %% 启动数据库 - 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), + % start_mnesia(), %% 加速内存的回收 erlang:system_flag(fullsweep_after, 16), %% 启动http服务 start_http_server(), + %% 启动连接池 + ok = hackney_pool:start_pool(influx_pool, [{timeout, 150000}, {max_connections, 100}]), + iot_sup:start_link(). stop(_State) -> @@ -41,19 +40,24 @@ start_http_server() -> Dispatcher = cowboy_router:compile([ {'_', [ - {"/host/[...]", http_protocol, [http_host_handler]}, - {"/terminal/[...]", http_protocol, [http_terminal_handler]}, - {"/scenario/[...]", http_protocol, [http_scenario_handler]}, - {"/log/[...]", http_protocol, [http_log_handler]}, - {"/iot/[...]", http_protocol, [http_iot_handler]}, - {"/router/[...]", http_protocol, [http_router_handler]} + {"/host/[...]", http_protocol, [http_host_handler]} ]} ]), + TransOpts = [ {port, Port}, {num_acceptors, Acceptors}, {backlog, Backlog}, {max_connections, MaxConnections} ], - {ok, _} = cowboy:start_clear(http_listener, TransOpts, #{env => #{dispatch => Dispatcher}}), - lager:debug("[booking_app] the http server start at: ~p", [Port]). + + {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 diff --git a/apps/iot/src/iot_cipher_aes.erl b/apps/iot/src/iot_cipher_aes.erl index 3bc1e80..005c5a9 100644 --- a/apps/iot/src/iot_cipher_aes.erl +++ b/apps/iot/src/iot_cipher_aes.erl @@ -10,14 +10,24 @@ -author("aresei"). %% API --export([encrypt/3, decrypt/3]). +-export([encrypt/2, decrypt/2]). +-export([test/0]). -%% 基于aes的加密算法 --spec encrypt(binary(), binary(), binary()) -> binary(). -encrypt(Key, IVec, PlainText) when is_binary(Key), is_binary(IVec), is_binary(PlainText) -> - crypto:crypto_one_time(aes_128_cfb128, Key, IVec, PlainText, true). +test() -> + Aes = list_to_binary(iot_util:rand_bytes(32)), + Enc = encrypt(Aes, <<"sdfsff hesdfs sfsdfsdffffffffffxyz yes call me">>), + Data = decrypt(Aes, Enc), + + lager:debug("enc: ~p, size: ~p, data len is: ~p, data: ~p", [Enc, byte_size(Enc), byte_size(Data), Data]). + +%% 基于aes的加密算法, aes_256_cbc +-spec encrypt(binary(), binary()) -> binary(). +encrypt(Key, PlainText) when is_binary(Key), is_binary(PlainText), byte_size(PlainText) >= 32 -> + IV = binary:part(Key, {0, 16}), + crypto:crypto_one_time(aes_256_cbc, Key, IV, PlainText, [{encrypt, true}, {padding, pkcs_padding}]). %% 基于aes的解密算法 --spec decrypt(binary(), binary(), binary()) -> binary(). -decrypt(Key, IVec, CipherText) when is_binary(Key), is_binary(IVec), is_binary(CipherText) -> - crypto:crypto_one_time(aes_128_cfb128, Key, IVec, CipherText, false). \ No newline at end of file +-spec decrypt(binary(), binary()) -> binary(). +decrypt(Key, CipherText) when is_binary(Key), is_binary(CipherText) -> + IV = binary:part(Key, {0, 16}), + crypto:crypto_one_time(aes_256_cbc, Key, IV, CipherText, [{encrypt, false}, {padding, pkcs_padding}]). \ No newline at end of file diff --git a/apps/iot/src/iot_cipher_rsa.erl b/apps/iot/src/iot_cipher_rsa.erl index 4b30393..2ec4e2f 100644 --- a/apps/iot/src/iot_cipher_rsa.erl +++ b/apps/iot/src/iot_cipher_rsa.erl @@ -10,17 +10,27 @@ -author("aresei"). %% API --export([decode/1, encode/2]). +-export([encode/2, decode/2]). %% 解密数据 -decode(EncBin) when is_binary(EncBin) -> - %PrivateKey = private_key(), - %public_key:decrypt_private(EncBin, PrivateKey). - ok. +decode(Data, PrivateKey) when is_binary(Data), is_binary(PrivateKey) -> + [Pri] = public_key:pem_decode(PrivateKey), + PriKeyEntry = public_key:pem_entry_decode(Pri), + public_key:decrypt_private(Data, PriKeyEntry). %% 解密数据 +encode(Data, PublicKey) when is_map(Data), is_binary(PublicKey) -> + BinData = jiffy:encode(Data, [force_utf8]), + encode(BinData, PublicKey); + encode(Data, PublicKey) when is_binary(Data), is_binary(PublicKey) -> - PubBin = <<"-----BEGIN PUBLIC KEY-----\n", PublicKey/binary, "-----END PUBLIC KEY-----">>, + PubBin = format_public_key(PublicKey), [Pub] = public_key:pem_decode(PubBin), PubKey = public_key:pem_entry_decode(Pub), - public_key:encrypt_public(Data, PubKey). \ No newline at end of file + public_key:encrypt_public(Data, PubKey). + +%% 格式化pubic_key +format_public_key(PubKey = <<"-----BEGIN PUBLIC KEY-----\n", _/binary>>) -> + PubKey; +format_public_key(PubKey) -> + <<"-----BEGIN PUBLIC KEY-----\n", PubKey/binary, "-----END PUBLIC KEY-----">>. \ No newline at end of file diff --git a/apps/iot/src/iot_commander.erl b/apps/iot/src/iot_commander.erl deleted file mode 100644 index d38e6f3..0000000 --- a/apps/iot/src/iot_commander.erl +++ /dev/null @@ -1,13 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2023, -%%% @doc -%%% 用来处理和封装通过mqtt协议下发的命令 -%%% @end -%%% Created : 17. 4月 2023 19:20 -%%%------------------------------------------------------------------- --module(iot_commander). --author("licheng5"). - -%% API --export([]). diff --git a/apps/iot/src/iot_config.erl b/apps/iot/src/iot_config.erl index e11b8a2..bb7238f 100644 --- a/apps/iot/src/iot_config.erl +++ b/apps/iot/src/iot_config.erl @@ -10,9 +10,9 @@ -author("licheng5"). %% API --export([emqt_opts/0]). +-export([emqt_opts/1]). -emqt_opts() -> +emqt_opts(ClientSuffix) when is_binary(ClientSuffix) -> %% 建立到emqx服务器的连接 {ok, Props} = application:get_env(iot, emqx_server), EMQXHost = proplists:get_value(host, Props), @@ -21,7 +21,11 @@ emqt_opts() -> Password = proplists:get_value(password, Props), RetryInterval = proplists:get_value(retry_interval, Props, 5), Keepalive = proplists:get_value(keepalive, Props, 86400), + + Node = atom_to_binary(node()), + ClientId = <<"mqtt-client-", Node/binary, "-", ClientSuffix/binary>>, [ + {clientid, ClientId}, {host, EMQXHost}, {port, EMQXPort}, {owner, self()}, @@ -32,4 +36,4 @@ emqt_opts() -> {auto_ack, true}, {proto_ver, v5}, {retry_interval, RetryInterval} - ]. \ No newline at end of file + ]. diff --git a/apps/iot/src/iot_host.erl b/apps/iot/src/iot_host.erl index 3d8a17c..5d6aef2 100644 --- a/apps/iot/src/iot_host.erl +++ b/apps/iot/src/iot_host.erl @@ -2,7 +2,8 @@ %%% @author aresei %%% @copyright (C) 2023, %%% @doc -%%% +%%% TODO +%%% 1. 指令下发是需要微服务名称的,微服务的名称里面是带copy的完整名称: (modbus:12345) %%% @end %%% Created : 12. 3月 2023 21:27 %%%------------------------------------------------------------------- @@ -12,9 +13,12 @@ -behaviour(gen_server). +%% 心跳包的间隔周期, 值需要比host上传的间隔大一些才行 +-define(TICKER_INTERVAL, 5000 * 2). + %% API --export([test/1]). --export([start_link/2, get_name/1, get_pid/1, publish/2, publish_result/2]). +-export([start_link/2, get_name/1, get_pid/1, handle/2, reload/1, activate/1]). +-export([get_metric/1, build_command/3, downstream_topic/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -22,47 +26,76 @@ -define(SERVER, ?MODULE). -record(state, { - host :: #host{}, - emqx_pid :: pid(), - %% 发送的未确认的包 - inflight = #{} -}). + %% 从数据库里面读取到的数据 + uuid :: binary(), + %% 当前的状态 + status :: integer(), -test(Msg) when is_binary(Msg) -> - Pid = get_pid(<<"1">>), - publish(Pid, Msg). + %% rsa公钥 + pub_key = <<>> :: binary(), + %% aes的key, 后续通讯需要基于这个加密 + aes = <<>> :: binary(), + + %% 主机的相关信息 + metrics = #{} :: map(), + + %% 是否获取到了ping请求 + is_answered = false :: boolean(), + + %% 标识当前主机是否已经注册,每次会话时主机需要重新协商会话; 通过status的值判断是否已经激活,值为:-1时,表示未激活 + is_activated :: boolean(), + + %% 会话状态 + has_session = false :: boolean() +}). %%%=================================================================== %%% API %%%=================================================================== -get_pid(HostId) when is_binary(HostId) -> - Name = get_name(HostId), - global:whereis_name(Name). -get_name(HostId) when is_binary(HostId) -> - binary_to_atom(<<"iot_host:", HostId/binary>>). +-spec get_pid(UUID :: binary()) -> undefined | pid(). +get_pid(UUID) when is_binary(UUID) -> + Name = get_name(UUID), + whereis(Name). --spec publish(pid(), binary()) -> {ok, Ref :: reference()} | {error, term()}. -publish(Pid, Message) when is_pid(Pid), is_binary(Message) -> - gen_server:call(Pid, {publish, self(), Message}). +-spec get_name(UUID :: binary()) -> atom(). +get_name(UUID) when is_binary(UUID) -> + binary_to_atom(<<"iot_host:", UUID/binary>>). -%% 获取publish的结果 --spec publish_result(tuple(), Timeout :: integer()) -> {ok, PacketId :: integer()} | {error, Reason :: any()}. -publish_result({error, Reason}, _) -> - {error, Reason}; -publish_result({ok, Ref}, Timeout) when is_reference(Ref), is_integer(Timeout) -> - receive - {ok, Ref, PacketId} -> - {ok, PacketId} - after Timeout -> - {error, timeout} - end. +%% 获取主机的下行主题 +-spec downstream_topic(UUID :: binary()) -> Topic :: binary(). +downstream_topic(UUID) when is_binary(UUID) -> + <<"host/downstream/", 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_server:cast(Pid, {handle, Payload}). + +%% 重新加载主机的基本信息 +-spec reload(Pid :: pid()) -> ok | {error, Reason :: any()}. +reload(Pid) when is_pid(Pid) -> + gen_server:call(Pid, reload). + +%% 激活主机 +-spec activate(Pid :: pid()) -> ok. +activate(Pid) when is_pid(Pid) -> + gen_server:call(Pid, activate). + +-spec get_metric(Pid :: pid()) -> {ok, MetricInfo :: map()}. +get_metric(Pid) when is_pid(Pid) -> + gen_server:call(Pid, get_metric). + +-spec build_command(Pid :: pid(), CommandType :: integer(), Params :: binary()) -> + {ok, Topic :: binary(), Command :: binary()} | {error, Reason :: any()}. +build_command(Pid, CommandType, Params) when is_pid(Pid), is_integer(CommandType), is_binary(Params) -> + gen_server:call(Pid, {build_command, CommandType, Params}). %% @doc Spawns the server and registers the local name (unique) --spec(start_link(Name :: atom(), Host :: #host{}) -> +-spec(start_link(Name :: atom(), UUID :: binary()) -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). -start_link(Name, Host = #host{}) -> - gen_server:start_link({global, Name}, ?MODULE, [Host], []). +start_link(Name, UUID) when is_atom(Name), is_binary(UUID) -> + gen_server:start_link({local, Name}, ?MODULE, [UUID], []). %%%=================================================================== %%% gen_server callbacks @@ -73,23 +106,16 @@ start_link(Name, Host = #host{}) -> -spec(init(Args :: term()) -> {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} | {stop, Reason :: term()} | ignore). -init([Host = #host{host_id = HostId}]) -> - lager:debug("[iot_host] host_id: ~p, host is: ~p", [HostId, Host]), +init([UUID]) -> + case host_bo:get_host_by_uuid(UUID) of + {ok, #{<<"status">> := Status}} -> + %% 启动心跳定时器 + erlang:start_timer(?TICKER_INTERVAL, self(), ping_ticker), - Opts = iot_config:emqt_opts(), - case emqtt:start_link(Opts) of - {ok, ConnPid} -> - lager:debug("[iot_host] connect success, pid: ~p", [ConnPid]), - %% 快速启动避免阻塞iot_host_sup的启动 - erlang:start_timer(0, self(), subscribe_ticker), - - {ok, #state{host = Host, emqx_pid = ConnPid}}; - ignore -> - lager:debug("[iot_host] connect emqx get ignore"), - {stop, ignore}; - {error, Reason} -> - lager:debug("[iot_host] connect emqx get error: ~p", [Reason]), - {stop, Reason} + {ok, #state{uuid = UUID, is_activated = (Status /= ?HOST_STATUS_INACTIVE), status = Status, has_session = false}}; + undefined -> + lager:warning("[iot_host] host uuid: ~p, loaded from mysql failed", [UUID]), + ignore end. %% @private @@ -102,15 +128,39 @@ init([Host = #host{host_id = HostId}]) -> {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} | {stop, Reason :: term(), NewState :: #state{}}). -handle_call({publish, ReceiverPid, Message}, _From, State = #state{emqx_pid = ConnPid, inflight = InFlight, host = #host{host_id = HostId}}) -> - Topic = <<"/host/", HostId/binary, "/downstream">>, - case emqtt:publish(ConnPid, Topic, #{}, Message, [{qos, 2}, {retain, true}]) 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(get_metric, _From, State = #state{metrics = Metrics}) -> + {reply, {ok, Metrics}, State}; + +%% 重新加载主机信息 +handle_call(reload, _From, State = #state{uuid = UUID}) -> + %% 重新加载主机信息 + case host_bo:get_host_by_uuid(UUID) of + {ok, Host = #{<<"status">> := Status}} -> + lager:debug("[iot_host] reload host uuid: ~p, successed", [Host]), + {reply, ok, State#state{is_activated = (Status /= ?HOST_STATUS_INACTIVE), status = Status}}; + undefined -> + lager:debug("[iot_host] reload host uuid: ~p, failed", [UUID]), + {reply, {error, <<"host not found">>}, State} + end; + +% 处理主机的激活 +handle_call(activate, _From, State = #state{uuid = UUID, is_activated = IsActivated}) -> + lager:debug("[iot_host] host uuid: ~p, activated, before status is: ~p", [UUID, IsActivated]), + {reply, ok, State#state{is_activated = true}}; + +%% 创建命令 +handle_call({build_command, _, _}, _From, State = #state{has_session = false}) -> + {reply, {error, <<"会话未建立,发送命令失败"/utf8>>}, State}; +handle_call({build_command, CommandType, Command}, _From, State = #state{aes = AES, uuid = UUID, has_session = true}) -> + DownstreamTopic = downstream_topic(UUID), + EncCommand = iot_cipher_aes:encrypt(AES, Command), + + {reply, {ok, DownstreamTopic, <>}, State}; + +handle_call(Info, _From, State = #state{}) -> + lager:debug("[iot_host] handle info: ~p", [Info]), + + {reply, ok, State}. %% @private %% @doc Handling cast messages @@ -118,8 +168,10 @@ handle_call({publish, ReceiverPid, Message}, _From, State = #state{emqx_pid = Co {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #state{}}). -handle_cast(_Request, State = #state{}) -> - {noreply, State}. +handle_cast({handle, Payload}, State) -> + %% 处理消息 + NState = handle_message(Payload, State), + {noreply, NState#state{is_answered = true}}. %% @private %% @doc Handling all non call/cast messages @@ -127,38 +179,34 @@ handle_cast(_Request, State = #state{}) -> {noreply, NewState :: #state{}} | {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #state{}}). -%% 处理topic的订阅事件 -handle_info({timeout, _, subscribe_ticker}, State = #state{host = #host{host_id = HostId}, emqx_pid = ConnPid}) -> - %% 监听和host相关的全部事件 - {ok, _} = emqtt:connect(ConnPid), - Topics = [ - {<<"/host/", HostId/binary, "/upstream">>, 1} - ], - SubscribeResult = emqtt:subscribe(ConnPid, Topics), - lager:debug("[iot_host] host_id: ~p, subscribe result is: ~p", [HostId, SubscribeResult]), +%% 超时,并且没有收到任何消息 +handle_info({timeout, _, ping_ticker}, 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 -> + {next_status, ?HOST_STATUS_OFFLINE}; + IsAnswered andalso Status == ?HOST_STATUS_OFFLINE -> + {next_status, ?HOST_STATUS_ONLINE}; + true -> + keep_status + end, - {noreply, State#state{emqx_pid = ConnPid}}; - -handle_info({disconnect, ReasonCode, Properties}, State = #state{host = #host{host_id = HostId}}) -> - - lager:debug("[iot_host] host: ~p, Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [HostId, ReasonCode, Properties]), - {stop, disconnected, State}; -handle_info({publish, Message = #{packet_id := _PacketId, payload := Payload}}, State = #state{emqx_pid = _ConnPid, host = #host{host_id = HostId}}) -> - lager:debug("[iot_host] host: ~p, Recv a publish packet: ~p, payload: ~p", [HostId, Message, Payload]), - {noreply, State}; -handle_info({puback, Packet = #{packet_id := PacketId}}, State = #state{inflight = Inflight, host = #host{host_id = HostId}}) -> - case maps:take(PacketId, Inflight) of - {{ReceiverPid, Ref, Message}, Inflight1} -> - lager:debug("[iot_host] host: ~p, receive puback packet: ~p, assoc message: ~p", [HostId, Packet, Message]), - ReceiverPid ! {ok, Ref, PacketId}, - {noreply, State#state{inflight = Inflight1}}; - error -> - lager:warning("[iot_host] host: ~p, receive unknown puback packet: ~p", [HostId, Packet]), - {noreply, State} + case NextStatus of + keep_status -> + {noreply, State}; + {next_status, NStatus} -> + case host_bo:change_status(UUID, NStatus) of + {ok, _} -> + {noreply, State#state{status = NStatus}}; + {error, Reason} -> + lager:warning("[iot_host] change host status of uuid: ~p, error: ~p", [UUID, Reason]), + {noreply, State} + end end; -handle_info(Info, State = #state{host = #host{host_id = HostId}}) -> - lager:debug("host_id: ~p, get info: ~p", [HostId, Info]), +handle_info(Info, State = #state{uuid = UUID}) -> + lager:warning("[iot_host] host uuid: ~p, get unknown info: ~p", [UUID, Info]), {noreply, State}. %% @private @@ -168,14 +216,10 @@ handle_info(Info, State = #state{host = #host{host_id = HostId}}) -> %% with Reason. The return value is ignored. -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), State :: #state{}) -> term()). -terminate(Reason, _State = #state{emqx_pid = ConnPid}) when is_pid(ConnPid) -> - {ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, <<"hello">>), - ok = emqtt:disconnect(ConnPid), - ok = emqtt:stop(ConnPid), - lager:debug("[iot_host] terminate with reason: ~p", [Reason]), - ok; -terminate(Reason, _State) -> +terminate(Reason, #state{uuid = UUID}) -> lager:debug("[iot_host] terminate with reason: ~p", [Reason]), + ChangeResult = host_bo:change_status(UUID, ?HOST_STATUS_OFFLINE), + lager:debug("[iot_host] change host: ~p, status result is: ~p", [UUID, ChangeResult]), ok. %% @private @@ -188,4 +232,72 @@ code_change(_OldVsn, State = #state{}, _Extra) -> %%%=================================================================== %%% Internal functions -%%%=================================================================== \ No newline at end of file +%%%=================================================================== + +%% 基于topic分离了,因此数据里面不需要uuid +handle_message(Payload, State) when is_binary(Payload) -> + Message = catch jiffy:decode(Payload, [return_maps]), + lager:debug("[iot_host] get message: ~p", [Message]), + handle_message(Message, State); + +%% create_session操作 +handle_message(#{<<"method">> := <<"create_session">>, <<"params">> := #{<<"pub_key">> := PubKey}}, State = #state{is_activated = IsActivated, uuid = UUID}) -> + lager:debug("[iot_host] host_id uuid: ~p, create_session", [UUID]), + + Aes = list_to_binary(iot_util:rand_bytes(32)), + Reply = case IsActivated of + true -> + #{<<"a">> => true, <<"aes">> => Aes}; + false -> + #{<<"a">> => false, <<"aes">> => <<"">>} + end, + EncReply = iot_cipher_rsa:encode(Reply, PubKey), + + {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]), + State#state{pub_key = PubKey, aes = Aes, has_session = true} + after 10000 -> + lager:debug("[iot_host] host_id uuid: ~p, publish register reply get error is: timeout", [UUID]), + State + end; + +%% 数据上传, TODO 数据格式需要重新约定 +handle_message(#{<<"method">> := <<"data">>, <<"params">> := Data}, State = #state{has_session = true, aes = AES, uuid = UUID}) -> + PlainData = iot_cipher_aes:decrypt(AES, base64:decode(Data)), + case catch jiffy:decode(PlainData, [return_maps]) of + Infos when is_list(Infos) -> + lager:debug("[iot_host] the data is: ~p", [Infos]), + insert_metrics(UUID, Infos); + _ -> + lager:debug("[iot_message_handler] the metric is invalid json") + end, + State; + +%% 处理服务器的ping +handle_message(#{<<"method">> := <<"ping">>, <<"params">> := CipherMetric}, State = #state{has_session = true, uuid = UUID, aes = AES}) -> + MetricsInfo = iot_cipher_aes:decrypt(AES, base64:decode(CipherMetric)), + Metrics = jiffy:decode(MetricsInfo, [return_maps]), + + lager:debug("[iot_host] host_id uuid: ~p, get ping: ~p", [UUID, Metrics]), + State#state{metrics = Metrics}; + +handle_message(Message, State = #state{uuid = UUID, has_session = HasSession}) -> + lager:warning("[iot_host] host_id uuid: ~p, get a unknown message: ~p, session: ~p", [UUID, Message, HasSession]), + State. + +%% 记录数据, TODO 转换点位信息, Fields里面包含了 <<"device_id">> 信息 +insert_metrics(UUID, Infos) when is_binary(UUID), is_list(Infos) -> + [insert_metrics0(UUID, Info) || Info <- Infos]. +insert_metrics0(UUID, 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}, + %% TODO 微服务名前缀作为measurement来保存数据 + [Measurement | _] = binary:split(ServiceName, <<":">>), + + Points = lists:map(fun(Fields) -> influx_point:new(Measurement, NTags, Fields, Timestamp) end, FieldsList), + Precision = influx_client:get_precision(Timestamp), + + poolboy:transaction(influx_pool, fun(Pid) -> influx_client:write(Pid, <<"iot">>, <<"iot">>, Precision, Points) end). \ No newline at end of file diff --git a/apps/iot/src/iot_host_mocker.erl b/apps/iot/src/iot_host_mocker.erl deleted file mode 100644 index 1ef3042..0000000 --- a/apps/iot/src/iot_host_mocker.erl +++ /dev/null @@ -1,163 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author aresei -%%% @copyright (C) 2023, -%%% @doc -%%% -%%% @end -%%% Created : 12. 3月 2023 21:27 -%%%------------------------------------------------------------------- --module(iot_host_mocker). --author("aresei"). --include("iot.hrl"). - --behaviour(gen_server). - -%% API --export([start_link/1, publish/2]). - -%% 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, { - host_id :: binary(), - emqx_pid :: pid() -}). - -%%%=================================================================== -%%% API -%%%=================================================================== - --spec publish(pid(), binary()) -> {ok, PacketId :: integer()} | {error, term()}. -publish(Pid, Message) when is_pid(Pid), is_binary(Message) -> - gen_server:call(Pid, {publish, Message}). - -%% @doc Spawns the server and registers the local name (unique) --spec(start_link(HostId :: binary()) -> - {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). -start_link(HostId) when is_binary(HostId) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [HostId], []). - -%%%=================================================================== -%%% 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([HostId]) -> - %% 建立到emqx服务器的连接 - Opts = iot_config:emqt_opts(), - case emqtt:start_link(Opts) of - {ok, ConnPid} -> - lager:debug("[iot_host_mocker] connect success, pid: ~p", [ConnPid]), - %% 快速启动避免阻塞iot_host_mocker_sup的启动 - erlang:start_timer(0, self(), subscribe_ticker), - - {ok, #state{host_id = HostId, emqx_pid = ConnPid}}; - ignore -> - lager:debug("[iot_host_mocker] connect emqx get ignore"), - {stop, ignore}; - {error, Reason} -> - lager:debug("[iot_host_mocker] 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({publish, Message}, _From, State = #state{emqx_pid = ConnPid, host_id = HostId}) -> - Topic = <<"/host/", HostId/binary, "/upstream">>, - Result = emqtt:publish(ConnPid, Topic, #{}, Message, [{qos, 1}]), - {reply, Result, 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{}}). -%% 处理topic的订阅事件 -handle_info({timeout, _, subscribe_ticker}, State = #state{emqx_pid = ConnPid, host_id = HostId}) -> - %% 监听和host相关的全部事件 - {ok, _} = emqtt:connect(ConnPid), - Topics = [ - {<<"/host/", HostId/binary, "/downstream">>, 2} - ], - SubscribeResult = emqtt:subscribe(ConnPid, Topics), - lager:debug("[iot_host_mocker] host_id: ~p, subscribe result is: ~p", [HostId, SubscribeResult]), - - {noreply, State#state{emqx_pid = ConnPid}}; - -handle_info({disconnect, ReasonCode, Properties}, State = #state{host_id = HostId}) -> - - lager:debug("[iot_host_mocker] host: ~p, Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [HostId, ReasonCode, Properties]), - {stop, disconnected, State}; -%% 收到qos为2的包 -handle_info({publish, Message = #{packet_id := PacketId, payload := Payload, reason_code := ReasonCode, qos := 2}}, State = #state{emqx_pid = ConnPid, host_id = HostId}) -> - lager:debug("[iot_host_mocker] host: ~p, qos: 2, Recv a publish packet: ~p, payload: ~p", [HostId, Message, Payload]), - %% 回复收到的请求信息 - % emqtt:pubrec(ConnPid, PacketId, ReasonCode), - - {noreply, State}; - -handle_info({publish, Message = #{packet_id := PacketId, payload := Payload}}, State = #state{emqx_pid = ConnPid, host_id = HostId}) -> - lager:debug("[iot_host_mocker] host: ~p, Recv a publish packet: ~p, payload: ~p, packet_id: ~p", [HostId, Message, Payload, PacketId]), - %% 回复收到的请求信息 - % emqtt:pubrec(ConnPid, PacketId), - {noreply, State}; -handle_info({puback, #{packet_id := PacketId, reason_code := ReasonCode}}, State = #state{host_id = HostId}) -> - lager:debug("[iot_host_mocker] host: ~p, receive puback packet_id: ~p, reason_code: ~p", [HostId, PacketId, ReasonCode]), - - {noreply, State}; - -handle_info(Info, State = #state{host_id = HostId}) -> - lager:debug("host_id: ~p, get info: ~p", [HostId, 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{emqx_pid = ConnPid}) when is_pid(ConnPid) -> - {ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, <<"hello">>), - ok = emqtt:disconnect(ConnPid), - ok = emqtt:stop(ConnPid), - ok; -terminate(Reason, _State) -> - lager:debug("[iot_host_mocker] 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_host_sup.erl b/apps/iot/src/iot_host_sup.erl index 8b0f028..c3e5b4a 100644 --- a/apps/iot/src/iot_host_sup.erl +++ b/apps/iot/src/iot_host_sup.erl @@ -9,25 +9,41 @@ -behaviour(supervisor). --export([start_link/0, init/1]). +-export([start_link/0, init/1, delete_host/1, ensured_host_started/1]). start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). init([]) -> - Specs = case host_model:get_all_hosts() of - {ok, Hosts} -> - lists:map(fun(Host = #host{host_id = HostId}) -> - Id = iot_host:get_name(HostId), - #{id => Id, - start => {iot_host, start_link, [Id, Host]}, - restart => permanent, - shutdown => 2000, - type => worker, - modules => ['iot_host']} - end, Hosts); - {error, _} -> - [] - end, + Specs = lists:map(fun child_spec/1, host_bo:get_all_hosts()), - {ok, {#{strategy => one_for_one, intensity => 1000, period => 3600}, Specs}}. \ No newline at end of file + {ok, {#{strategy => one_for_one, intensity => 1000, period => 3600}, Specs}}. + +-spec ensured_host_started(UUID :: binary()) -> {ok, Pid :: pid()} | {error, Reason :: any()}. +ensured_host_started(UUID) when is_binary(UUID) -> + case iot_host:get_pid(UUID) of + undefined -> + case supervisor:start_child(?MODULE, child_spec(UUID)) of + {ok, Pid} when is_pid(Pid) -> + {ok, Pid}; + {error, {'already_started', Pid}} when is_pid(Pid) -> + {ok, Pid}; + {error, Error} -> + {error, Error} + end; + Pid when is_pid(Pid) -> + {ok, Pid} + end. + +delete_host(UUID) when is_binary(UUID) -> + Id = iot_host:get_name(UUID), + supervisor:terminate_child(?MODULE, Id). + +child_spec(UUID) -> + Id = iot_host:get_name(UUID), + #{id => Id, + start => {iot_host, start_link, [Id, UUID]}, + restart => permanent, + shutdown => 2000, + type => worker, + modules => ['iot_host']}. \ No newline at end of file diff --git a/apps/iot/src/iot_issue.erl b/apps/iot/src/iot_issue.erl deleted file mode 100644 index 08b1c07..0000000 --- a/apps/iot/src/iot_issue.erl +++ /dev/null @@ -1,119 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2023, -%%% @doc -%%% -%%% @end -%%% Created : 10. 3月 2023 16:44 -%%%------------------------------------------------------------------- --module(iot_issue). --author("licheng5"). --include("iot.hrl"). - --behaviour(gen_server). - -%% API --export([start_link/1]). --export([get_pid/1]). - -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). - --define(SERVER, ?MODULE). - --record(state, { - issue :: #issue{}, - timer_ref -}). - -%%%=================================================================== -%%% API -%%%=================================================================== - -get_pid(IssueId) when is_integer(IssueId) -> - whereis(get_name(IssueId)). - -get_name(IssueId) when is_integer(IssueId) -> - list_to_atom("iot_issue:" ++ integer_to_list(IssueId)). - -%% @doc Spawns the server and registers the local name (unique) --spec(start_link(Issue :: #issue{}) -> - {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). -start_link(Issue = #issue{issue_id = IssueId}) -> - Name = get_name(IssueId), - gen_server:start_link({local, Name}, ?MODULE, [Issue], []). - -%%%=================================================================== -%%% 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([Issue = #issue{timeout = Timeout0, hosts = Hosts}]) -> - lager:debug("iot_issue started!!: ~p", [Issue]), - %% 启动任务定时器, 默认最长为1个小时 - Timeout = if Timeout0 > 0 -> Timeout0; true -> 3600 end, - TimerRef = erlang:start_timer(Timeout * 1000, self(), issue_task_timeout), - - %% TODO 下发数据到主机 - lists:map(fun(Host) -> - iot_emqtt_client:publish(Host, x, 1) - end, Hosts), - - {ok, #state{issue = Issue, timer_ref = TimerRef}}. - -%% @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(_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_issue_sup.erl b/apps/iot/src/iot_issue_sup.erl deleted file mode 100644 index 9c4a369..0000000 --- a/apps/iot/src/iot_issue_sup.erl +++ /dev/null @@ -1,73 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2023, -%%% @doc -%%% -%%% @end -%%% Created : 10. 3月 2023 16:44 -%%%------------------------------------------------------------------- --module(iot_issue_sup). --author("licheng5"). --include("iot.hrl"). - --behaviour(supervisor). - -%% API --export([start_link/0, start_issue/1]). - -%% Supervisor callbacks --export([init/1]). - --define(SERVER, ?MODULE). - -%%%=================================================================== -%%% API functions -%%%=================================================================== - -%% @doc Starts the supervisor --spec(start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). -start_link() -> - supervisor:start_link({local, ?SERVER}, ?MODULE, []). - -%%%=================================================================== -%%% Supervisor callbacks -%%%=================================================================== - -%% @private -%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], -%% this function is called by the new process to find out about -%% restart strategy, maximum restart frequency and child -%% specifications. --spec(init(Args :: term()) -> - {ok, {SupFlags :: {RestartStrategy :: supervisor:strategy(), - MaxR :: non_neg_integer(), MaxT :: non_neg_integer()}, - [ChildSpec :: supervisor:child_spec()]}} - | ignore | {error, Reason :: term()}). -init([]) -> - SupFlags = #{strategy => simple_one_for_one, intensity => 1000, period => 3600}, - AChild = #{ - id => 'iot_issue', - start => {'iot_issue', start_link, []}, - restart => temporary, - shutdown => 2000, - type => worker, - modules => ['iot_issue'] - }, - - {ok, {SupFlags, [AChild]}}. - -%%%=================================================================== -%%% Internal functions -%%%=================================================================== - -%% 启动一个任务 --spec start_issue(Issue :: #issue{}) -> {ok, Pid :: pid()} | {error, Reason :: any()}. -start_issue(Issue = #issue{}) -> - case supervisor:start_child(?MODULE, [Issue]) of - {ok, Pid} -> - {ok, Pid}; - {error, {'already_started', Pid}} -> - {ok, Pid}; - Error -> - Error - end. \ No newline at end of file diff --git a/apps/iot/src/iot_mnesia.erl b/apps/iot/src/iot_mnesia.erl index 077c7e4..2065a7a 100644 --- a/apps/iot/src/iot_mnesia.erl +++ b/apps/iot/src/iot_mnesia.erl @@ -24,77 +24,13 @@ init_database() -> ok = mnesia:start(), %% 创建数据库表 - %% id生成表 - mnesia:create_table(id_generator, [ - {attributes, record_info(fields, id_generator)}, - {record_name, id_generator}, - {disc_copies, [node()]}, - {type, ordered_set} - ]), - %% 主机表 - mnesia:create_table(host, [ - {attributes, record_info(fields, host)}, - {record_name, host}, - {disc_copies, [node()]}, - {type, ordered_set} - ]), - - %% 主机终端管理 - mnesia:create_table(terminal, [ - {attributes, record_info(fields, terminal)}, - {record_name, terminal}, - {disc_copies, [node()]}, - {type, ordered_set} - ]), - - %% 主机微服务管理 - mnesia:create_table(service, [ - {attributes, record_info(fields, service)}, - {record_name, service}, - {disc_copies, [node()]}, - {type, ordered_set} - ]), - - %% 转发规则表 - mnesia:create_table(router, [ - {attributes, record_info(fields, router)}, - {record_name, router}, - {disc_copies, [node()]}, - {type, ordered_set} - ]), - - %% 应用场景 - mnesia:create_table(scenario, [ - {attributes, record_info(fields, scenario)}, - {record_name, scenario}, - {disc_copies, [node()]}, - {type, ordered_set} - ]), - - %% 应用场景部署关系表 - mnesia:create_table(scenario_deploy, [ - {attributes, record_info(fields, scenario_deploy)}, - {record_name, scenario_deploy}, - {disc_copies, [node()]}, - {type, ordered_set} - ]), - - %% 工单表 - mnesia:create_table(issue, [ - {attributes, record_info(fields, issue)}, - {record_name, issue}, - {disc_copies, [node()]}, - {type, ordered_set} - ]), - - %% 操作日志表 - mnesia:create_table(log, [ - {attributes, record_info(fields, log)}, - {record_name, log}, - {disc_copies, [node()]}, - {type, ordered_set} - ]), + %mnesia:create_table(host, [ + % {attributes, record_info(fields, host)}, + % {record_name, host}, + % {disc_copies, [node()]}, + % {type, ordered_set} + %]), ok. @@ -114,11 +50,5 @@ copy_database(MasterNode) when is_atom(MasterNode) -> mnesia:change_table_copy_type(schema, node(), disc_copies), %% 增加表的分区复制 - mnesia:add_table_copy(host, node(), ram_copies), - mnesia:add_table_copy(terminal, node(), ram_copies), - mnesia:add_table_copy(service, node(), ram_copies), - mnesia:add_table_copy(scenario, node(), ram_copies), - mnesia:add_table_copy(scenario_deploy, node(), ram_copies), - mnesia:add_table_copy(issue, node(), ram_copies), - mnesia:add_table_copy(log, node(), ram_copies), + % mnesia:add_table_copy(host, node(), ram_copies), ok. \ No newline at end of file diff --git a/apps/iot/src/iot_mock.erl b/apps/iot/src/iot_mock.erl deleted file mode 100644 index 6555bd9..0000000 --- a/apps/iot/src/iot_mock.erl +++ /dev/null @@ -1,157 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2023, -%%% @doc -%%% -%%% @end -%%% Created : 14. 2月 2023 20:32 -%%%------------------------------------------------------------------- --module(iot_mock). --author("licheng5"). --include("iot.hrl"). - -%% API --export([insert_hosts/0, insert_services/1, insert_terminals/1, insert_routers/0, insert_logs/0]). --export([start_router/1]). --export([rsa_encode/1]). --export([start_issue/0]). - -start_issue() -> - iot_issue_sup:start_issue(#issue{ - issue_id = 1, - name = <<"issue 1">>, - uid = 1234, - deploy_type = 1, - assoc_id = 1, - hosts = [<<"host1">>, <<"host2">>], - timeout = 6, - create_ts = 0, - status = 0 - }). - -insert_hosts() -> - lists:foreach(fun(Id0) -> - %Id0 = iot_util:rand_bytes(16), - Host = #host{ - host_id = integer_to_binary(Id0), - name = <<"N1000_0001">>, - model = <<"N1000">>, - cell_id = rand:uniform(100), - status = 1 - }, - - host_model:add_host(Host) - end, lists:seq(1, 1)). - -insert_logs() -> - lists:foreach(fun(Id0) -> - Log = #log{ - log_id = Id0, - device_type = ?DEVICE_HOST, - action_name = <<"主机上线"/utf8>>, - assoc_id = <<"1">>, - create_ts = Id0 + 123456 - }, - log_model:add_log(Log) - end, lists:seq(1, 500000)). - -insert_services(HostId) -> - lists:foreach(fun(Id0) -> - Q0 = queue:new(), - Q = queue:in({1234, 21}, Q0), - Service = #service{ - service_id = integer_to_binary(Id0), - host_id = HostId, - name = <<"Service_0001">>, - category = <<"电器"/utf8>>, - %% 应用对象 - apply_object = <<"暂时不清楚"/utf8>>, - %% 版本 - version = <<"v1.1">>, - %% 执行次数 - execute_count = 10, - %% 部署时间 - deploy_ts = 0, - metrics = [#service_metric{symbol = <<"X10">>, name = <<"温度"/utf8>>, last_value = 10, unit = <<"E">>, queue = Q}], - status = 0 - }, - - service_model:add_service(Service) - end, lists:seq(1, 100)). - -insert_terminals(HostId) -> - lists:foreach(fun(Id0) -> - Terminal = #terminal{ - terminal_id = integer_to_binary(Id0), - host_id = HostId, - name = <<"Service_0001">>, - code = <<"1234">>, - access_protocol = <<"TCP/IP">>, - product_id = 12, - vendor_id = 13, - model = <<"1345">>, - cell_id = 12, - status = 0 - }, - - terminal_model:add_terminal(Terminal) - end, lists:seq(1, 100)). - -insert_routers() -> - lists:foreach(fun(Id0) -> - R = #router{ - router_id = Id0, - name = <<"计费电表"/utf8>>, - rule = <<"测试规则"/utf8>>, - endpoint = #http_endpoint{url = <<"http://127.0.0.1:8080/data">>}, - status = 1 - }, - router_model:add_router(R) - end, lists:seq(1, 100)). - -start_router(Id0) when is_integer(Id0) -> - R = #router{ - router_id = Id0, - name = <<"计费电表"/utf8>>, - rule = <<"测试规则"/utf8>>, - endpoint = #http_endpoint{url = <<"http://127.0.0.1:8080/data">>}, - status = 1 - }, - router_model:add_router(R), - iot_router_sup:start_new_router(R). - -rsa_encode(Data) when is_binary(Data) -> - %% 读取相关配置 - PublicPemFile = "/tmp/keys/public.pem", - - %% 私钥保存解析后的 - {ok, PubBin} = file:read_file(PublicPemFile), - lager:debug("pub bin is: ~p", [PubBin]), - [Pub] = public_key:pem_decode(PubBin), - lager:debug("pub pem bin is: ~p", [Pub]), - PubKey = public_key:pem_entry_decode(Pub), - lager:debug("the public key is: ~p", [PubKey]), - - EncData = public_key:encrypt_public(Data, PubKey), - lager:debug("enc data is: ~p", [EncData]), - - rsa_decode(EncData), - - ok. - -rsa_decode(EncData) when is_binary(EncData) -> - %% 读取相关配置 - PublicPemFile = "/tmp/keys/pri.pem", - - %% 私钥保存解析后的 - {ok, PubBin} = file:read_file(PublicPemFile), - lager:debug("pub bin is: ~p", [PubBin]), - [Pub] = public_key:pem_decode(PubBin), - lager:debug("pub pem bin is: ~p", [Pub]), - PubKey = public_key:pem_entry_decode(Pub), - lager:debug("the public key is: ~p", [PubKey]), - - PlainData = public_key:decrypt_private(EncData, PubKey), - lager:debug("plain data is: ~p", [PlainData]), - - ok. \ No newline at end of file diff --git a/apps/iot/src/iot_mqtt_message_handler.erl b/apps/iot/src/iot_mqtt_message_handler.erl deleted file mode 100644 index 4a3702c..0000000 --- a/apps/iot/src/iot_mqtt_message_handler.erl +++ /dev/null @@ -1,170 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author aresei -%%% @copyright (C) 2023, -%%% @doc -%%% -%%% @end -%%% Created : 18. 2月 2023 21:39 -%%%------------------------------------------------------------------- --module(iot_mqtt_message_handler). --author("aresei"). --include("iot.hrl"). - -%% API --export([handle/2]). - -handle(<<"server.register">>, Msg = #{<<"c_id">> := ClientId, <<"r">> := PubKey, - <<"m">> := #{<<"cpu_core">> := CpuCore, <<"memory">> := Memory, <<"disk">> := Disk, <<"boot_time">> := BootTime, <<"efka_version">> := EfkaVersion, <<"kernel_arch">> := KernelArch, <<"ipv4_1">> := Ip1, <<"ipv4_2">> := Ip2}}) -> - lager:debug("[iot_message_handler] get server register message: ~p", [Msg]), - - Aes = iot_util:rand_bytes(16), - Host = #host{ - host_id = ClientId, - aes = Aes, - metric = #host_metric{ - %% cpu相关 - cpus = [ - #cpu_metric{ - %% cpu编号 - num = 1, - %% 负载 - load = 0 - }, - #cpu_metric{ - %% cpu编号 - num = 2, - %% 负载 - load = 0 - } - ], - %% cpu温度 - cpu_temperature = 0, - %% 内存状态 - memory = #memory_metric{ - %% 使用量 - used = 0, - %% 总量 - total = Memory - }, - %% 硬盘状态 - disk = #disk_metric{ - total = Disk - }, - %% 接口状态 - interfaces = [] - }, - activated_ts = iot_util:current_time(), - update_ts = iot_util:current_time(), - status = ?HOST_STATUS_INACTIVE - }, - - case host_model:add_host(Host) of - ok -> - Reply = #{ - <<"a">> => true, - <<"aes">> => Aes, - <<"reply">> => <<"client.reply.", ClientId/binary>> - }, - EncReply = iot_cipher_rsa:encode(Reply, PubKey), - lager:debug("enc_reply is: ~p", [EncReply]); - {error, Reason} -> - lager:debug("register error is: ~p", [Reason]), - Reply = #{ - <<"a">> => false, - <<"aes">> => <<"">>, - <<"reply">> => <<"client.reply.", ClientId/binary>> - }, - EncReply = iot_cipher_rsa:encode(Reply, PubKey), - lager:debug("enc_reply is: ~p", [EncReply]) - end; - -handle(<<"server.data">>, #{<<"c_id">> := HostId, <<"d">> := Data}) -> - case host_model:get_host(HostId) of - {ok, #host{aes = Aes}} -> - Services = service_model:get_host_services(HostId), - PlainData = iot_cipher_aes:decrypt(Aes, Aes, Data), - case jiffy:decode(PlainData, [return_maps]) of - Infos when is_list(Infos) -> - lager:debug("[iot_message_handler] the data is: ~p", [Infos]), - %% 一次可能提前多组数据 - lists:foreach(fun(#{<<"service_name">> := ServiceName, <<"data">> := Items}) -> - case lists:search(fun(#service{name = Name}) -> Name =:= ServiceName end, Services) of - {value, #service{service_id = ServiceId, metrics = Metrics}} -> - %% 更新数据 - NMetrics = lists:foldl(fun(MetricData, MetricsAcc) -> append_metric(MetricsAcc, MetricData) end, Metrics, Items), - case service_model:update_metric(ServiceId, NMetrics) of - ok -> - lager:debug("[iot_message_handler] update metrics success"); - {ok, Reason} -> - lager:debug("[iot_message_handler] update metrics error: ~p", [Reason]) - end; - false -> - lager:warning("[iot_message_handler] host_id: ~p, not found service_name: ~p", [HostId, ServiceName]) - end - end, Infos); - _ -> - lager:debug("[iot_message_handler] the metric is invalid json") - end; - undefined -> - lager:warning("[iot_message_handler] host_id: ~p, not exists", [HostId]) -end; - -%% 处理服务器的ping -handle(<<"server.ping">>, #{<<"c">> := HostId, <<"at">> := At, - <<"h">> := #{<<"fm">> := FreeMemory, <<"fd">> := FreeDisk, <<"cp">> := CpuLoad}}) -> - - case host_model:get_host(HostId) of - {ok, Host=#host{metric = Metric = #host_metric{disk = Disk, memory = Memory}}} -> - NMetric = Metric#host_metric{ - disk = Disk#disk_metric{free = FreeDisk}, - memory = Memory#memory_metric{free = FreeMemory} - }, - - case mnesia:transaction(fun() -> mnesia:write(host, Host#host{metric = NMetric}, write) end) of - {atomic, ok} -> - ok; - {error, Reason} -> - lager:warning("[iot_message_handler] host_id: ~p, ping get error: ~p", [HostId, Reason]) - end; - - undefined -> - lager:warning("[iot_message_handler] host_id: ~p, not exists", [HostId]) - end. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -append_metric(Metrics, MetricData) when is_map(MetricData) -> - Name = maps:get(<<"name">>, MetricData, <<"">>), - Symbol = maps:get(<<"symbol">>, MetricData, <<"">>), - Value = maps:get(<<"value">>, MetricData), - Ts = maps:get(<<"time">>, MetricData, 0), - - case lists:any(fun(#service_metric{symbol = Symbol0}) -> Symbol =:= Symbol0 end, Metrics) of - true -> - lists:map(fun(Metric=#service_metric{symbol = Symbol0, queue = Q}) -> - case Symbol =:= Symbol0 of - true -> - Q1 = iot_util:queue_limited_in({Ts, Value}, Q, 100), - Metric#service_metric{ - name = Name, - last_value = Value, - update_ts = Ts, - queue = Q1 - }; - false -> - Metric - end - end, Metrics); - false -> - Q0 = queue:new(), - Metric = #service_metric{ - name = Name, - symbol = Symbol, - last_value = Value, - update_ts = Ts, - queue = queue:in({Ts, Value}, Q0) - }, - Metrics ++ [Metric] - end. diff --git a/apps/iot/src/iot_mqtt_publisher.erl b/apps/iot/src/iot_mqtt_publisher.erl index fd5bde6..437ebb3 100644 --- a/apps/iot/src/iot_mqtt_publisher.erl +++ b/apps/iot/src/iot_mqtt_publisher.erl @@ -13,7 +13,7 @@ -behaviour(gen_server). %% API --export([start_link/0, publish/3, publish_result/2]). +-export([start_link/0, publish/3]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -33,18 +33,6 @@ publish(Topic, Message, Qos) when is_binary(Topic), is_binary(Message), is_integer(Qos) -> gen_server:call(?MODULE, {publish, self(), Topic, Message, Qos}). -%% 获取publish的结果 --spec publish_result(tuple(), Timeout :: integer()) -> {ok, PacketId :: integer()} | {error, Reason :: any()}. -publish_result({error, Reason}, _) -> - {error, Reason}; -publish_result({ok, Ref}, Timeout) when is_reference(Ref), is_integer(Timeout) -> - receive - {ok, Ref, PacketId} -> - {ok, PacketId} - after Timeout -> - {error, timeout} - end. - %% @doc Spawns the server and registers the local name (unique) -spec(start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). @@ -62,11 +50,11 @@ start_link() -> {stop, Reason :: term()} | ignore). init([]) -> %% 建立到emqx服务器的连接 - Opts = iot_config:emqt_opts(), + Opts = iot_config:emqt_opts(<<"publisher">>), case emqtt:start_link(Opts) of {ok, ConnPid} -> lager:debug("[iot_mqtt_publisher] connect success, pid: ~p", [ConnPid]), - % {ok, _} = emqtt:connect(ConnPid), + {ok, _} = emqtt:connect(ConnPid), {ok, #state{conn_pid = ConnPid}}; ignore -> lager:debug("[iot_mqtt_publisher] connect emqx get ignore"), @@ -87,8 +75,8 @@ init([]) -> {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}) -> - lager:debug("call me publish"), - case emqtt:publish(ConnPid, Topic, #{}, Message, [{qos, Qos}, {retain, true}]) of + %% [{qos, Qos}, {retain, true}] + 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)}}; @@ -119,10 +107,10 @@ handle_info({publish, Message = #{packet_id := _PacketId, payload := Payload}}, {noreply, State}; handle_info({puback, Packet = #{packet_id := PacketId}}, State = #state{inflight = Inflight}) -> case maps:take(PacketId, Inflight) of - {{ReceiverPid, Ref, Message}, Inflight1} -> + {{ReceiverPid, Ref, Message}, RestInflight} -> lager:debug("[iot_mqtt_publisher] receive puback packet: ~p, assoc message: ~p", [Packet, Message]), ReceiverPid ! {ok, Ref, PacketId}, - {noreply, State#state{inflight = Inflight1}}; + {noreply, State#state{inflight = RestInflight}}; error -> lager:warning("[iot_mqtt_publisher] receive unknown puback packet: ~p", [Packet]), {noreply, State} @@ -140,7 +128,6 @@ handle_info(Info, State = #state{}) -> -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), State :: #state{}) -> term()). terminate(Reason, _State = #state{conn_pid = ConnPid}) when is_pid(ConnPid) -> - {ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, <<"hello">>), ok = emqtt:disconnect(ConnPid), ok = emqtt:stop(ConnPid), lager:debug("[iot_mqtt_publisher] terminate with reason: ~p", [Reason]), diff --git a/apps/iot/src/iot_mqtt_subscriber.erl b/apps/iot/src/iot_mqtt_subscriber.erl index 968467a..12f15db 100644 --- a/apps/iot/src/iot_mqtt_subscriber.erl +++ b/apps/iot/src/iot_mqtt_subscriber.erl @@ -2,7 +2,8 @@ %%% @author aresei %%% @copyright (C) 2023, %%% @doc -%%% +%%% 1. 需要考虑集群部署的相关问题,上行的数据可能在集群中共享 +%%% 2. host进程不能直接去监听topic,这样涉及到新增和下线的很多问题 %%% @end %%% Created : 12. 3月 2023 21:27 %%%------------------------------------------------------------------- @@ -13,47 +14,25 @@ -behaviour(gen_server). %% API --export([test/1]). --export([start_link/0, get_name/1, get_pid/1, publish/2, publish_result/2]). +-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,[ + {<<"host/upstream/+">>, 1} +]). + -record(state, { conn_pid :: pid() }). -test(Msg) when is_binary(Msg) -> - Pid = get_pid(<<"1">>), - publish(Pid, Msg). - %%%=================================================================== %%% API %%%=================================================================== -get_pid(HostId) when is_binary(HostId) -> - Name = get_name(HostId), - global:whereis_name(Name). - -get_name(HostId) when is_binary(HostId) -> - binary_to_atom(<<"iot_host:", HostId/binary>>). - --spec publish(pid(), binary()) -> {ok, Ref :: reference()} | {error, term()}. -publish(Pid, Message) when is_pid(Pid), is_binary(Message) -> - gen_server:call(Pid, {publish, self(), Message}). - -%% 获取publish的结果 --spec publish_result(tuple(), Timeout :: integer()) -> {ok, PacketId :: integer()} | {error, Reason :: any()}. -publish_result({error, Reason}, _) -> - {error, Reason}; -publish_result({ok, Ref}, Timeout) when is_reference(Ref), is_integer(Timeout) -> - receive - {ok, Ref, PacketId} -> - {ok, PacketId} - after Timeout -> - {error, timeout} - end. %% @doc Spawns the server and registers the local name (unique) -spec(start_link() -> @@ -72,26 +51,22 @@ start_link() -> {stop, Reason :: term()} | ignore). init([]) -> %% 建立到emqx服务器的连接 - Opts = iot_config:emqt_opts(), + Opts = iot_config:emqt_opts(<<"host-subscriber">>), case emqtt:start_link(Opts) of {ok, ConnPid} -> - lager:debug("[iot_mqtt_subscriber] start connecting"), %% 监听和host相关的全部事件 - %{ok, _} = emqtt:connect(ConnPid), - lager:debug("[iot_mqtt_subscriber] connect success, pid: ~p", [ConnPid]), - Topics = [ - {<<"$share/nodes_group//server/register">>, 1}, - {<<"$share/nodes_group//host/+/upstream">>, 1} - ], - %SubscribeResult = emqtt:subscribe(ConnPid, Topics), - % lager:debug("[iot_mqtt_subscriber] subscribe topics: ~p, result is: ~p", [Topics, SubscribeResult]), + {ok, _} = emqtt:connect(ConnPid), + lager:debug("[iot_mqtt_host_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]), {ok, #state{conn_pid = ConnPid}}; ignore -> - lager:debug("[iot_mqtt_subscriber] connect emqx get ignore"), + lager:debug("[iot_mqtt_host_subscriber] connect emqx get ignore"), {stop, ignore}; {error, Reason} -> - lager:debug("[iot_mqtt_subscriber] connect emqx get error: ~p", [Reason]), + lager:debug("[iot_mqtt_host_subscriber] connect emqx get error: ~p", [Reason]), {stop, Reason} end. @@ -124,10 +99,29 @@ handle_cast(_Request, State = #state{}) -> {noreply, NewState :: #state{}, timeout() | hibernate} | {stop, Reason :: term(), NewState :: #state{}}). handle_info({disconnect, ReasonCode, Properties}, State = #state{}) -> - lager:debug("[iot_mqtt_subscriber] Recv a DISONNECT packet - ReasonCode: ~p, Properties: ~p", [ReasonCode, Properties]), + lager:debug("[iot_mqtt_host_subscriber] 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_subscriber] Recv a publish packet: ~p, payload: ~p", [Message, Payload]), +%% 必须要做到消息的快速分发,数据的json反序列需要在host进程进行 +handle_info({publish, Message = #{packet_id := _PacketId, payload := Payload, qos := Qos, topic := Topic}}, State = #state{conn_pid = _ConnPid}) -> + lager:debug("[iot_mqtt_subscriber] Recv a publish packet: ~p, qos: ~p", [Message, 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 get error: ~p, assoc packet: ~p", [Reason, Message]) + end + end; + _ -> + lager:warning("[iot_mqtt_subscriber] invalid topic: ~p, packet: ~p, qos: ~p", [Topic, Message, Qos]) + end, {noreply, State}; handle_info({puback, Packet = #{packet_id := _PacketId}}, State = #state{}) -> lager:debug("[iot_mqtt_subscriber] receive puback packet: ~p", [Packet]), @@ -145,9 +139,11 @@ handle_info(Info, State = #state{}) -> -spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()), State :: #state{}) -> term()). terminate(Reason, _State = #state{conn_pid = ConnPid}) when is_pid(ConnPid) -> - {ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, <<"hello">>), + %% 取消topic的订阅 + TopicNames = lists:map(fun({Name, _}) -> Name end, ?Topics), + {ok, _Props, _ReasonCode} = emqtt:unsubscribe(ConnPid, #{}, TopicNames), + ok = emqtt:disconnect(ConnPid), - ok = emqtt:stop(ConnPid), lager:debug("[iot_mqtt_subscriber] terminate with reason: ~p", [Reason]), ok; terminate(Reason, _State) -> @@ -164,4 +160,4 @@ code_change(_OldVsn, State = #state{}, _Extra) -> %%%=================================================================== %%% Internal functions -%%%=================================================================== \ No newline at end of file +%%%=================================================================== diff --git a/apps/iot/src/iot_mqtt_sys_subscriber.erl b/apps/iot/src/iot_mqtt_sys_subscriber.erl new file mode 100644 index 0000000..f0e00c5 --- /dev/null +++ b/apps/iot/src/iot_mqtt_sys_subscriber.erl @@ -0,0 +1,223 @@ +%%%------------------------------------------------------------------- +%%% @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({disconnect, 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 deleted file mode 100644 index d295bbc..0000000 --- a/apps/iot/src/iot_router.erl +++ /dev/null @@ -1,105 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2023, -%%% @doc -%%% -%%% @end -%%% Created : 01. 3月 2023 16:03 -%%%------------------------------------------------------------------- --module(iot_router). --author("licheng5"). --include("iot.hrl"). - --behaviour(gen_server). - -%% API --export([start_link/2]). - --export([get_name/1]). -%% gen_server callbacks --export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). - --define(SERVER, ?MODULE). - --record(state, { - -}). - -get_name(Id) when is_integer(Id) -> - list_to_atom("iot_router:" ++ integer_to_list(Id)). - -%%%=================================================================== -%%% API -%%%=================================================================== - -%% @doc Spawns the server and registers the local name (unique) --spec(start_link(Name :: atom(), Router :: #router{}) -> - {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). -start_link(Name, Router = #router{}) -> - gen_server:start_link({global, Name}, ?MODULE, [Router], []). - -%%%=================================================================== -%%% 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([Router]) -> - lager:debug("router is: ~p", [Router]), - {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(_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_router_sup.erl b/apps/iot/src/iot_router_sup.erl deleted file mode 100644 index 3f85710..0000000 --- a/apps/iot/src/iot_router_sup.erl +++ /dev/null @@ -1,86 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2023, -%%% @doc -%%% -%%% @end -%%% Created : 01. 3月 2023 16:01 -%%%------------------------------------------------------------------- --module(iot_router_sup). --author("licheng5"). --include("iot.hrl"). - --behaviour(supervisor). - -%% API --export([start_link/0, start_new_router/1]). - -%% Supervisor callbacks --export([init/1]). - --define(SERVER, ?MODULE). - -%%%=================================================================== -%%% API functions -%%%=================================================================== - -%% @doc Starts the supervisor --spec(start_link() -> {ok, Pid :: pid()} | ignore | {error, Reason :: term()}). -start_link() -> - supervisor:start_link({local, ?SERVER}, ?MODULE, []). - -%%%=================================================================== -%%% Supervisor callbacks -%%%=================================================================== - -%% @private -%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], -%% this function is called by the new process to find out about -%% restart strategy, maximum restart frequency and child -%% specifications. --spec(init(Args :: term()) -> - {ok, {SupFlags :: {RestartStrategy :: supervisor:strategy(), - MaxR :: non_neg_integer(), MaxT :: non_neg_integer()}, - [ChildSpec :: supervisor:child_spec()]}} - | ignore | {error, Reason :: term()}). -init([]) -> - SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600}, - - %% 启动目前生效的全部转发规则 - Specs = case router_model:get_all_valid_routers() of - {ok, Routers} -> - lists:map(fun generate_router_spec/1, Routers); - error -> - [] - end, - - {ok, {SupFlags, Specs}}. - -%%%=================================================================== -%%% Internal functions -%%%=================================================================== - -%% 动态启动一个转发规则 --spec start_new_router(Router :: #router{}) -> {ok, Pid :: pid()} | {error, Reason :: any()}. -start_new_router(Router = #router{}) -> - Spec = generate_router_spec(Router), - case supervisor:start_child(?MODULE, Spec) of - {ok, Pid} -> - {ok, Pid}; - {error, {already_started, Pid}} -> - {ok, Pid}; - {error, Error} -> - lager:debug("start router get a error: ~p", [Error]), - {error, Error} - end. - -generate_router_spec(Router = #router{router_id = RouterId}) -> - Id = iot_router:get_name(RouterId), - #{ - id => Id, - start => {'iot_router', start_link, [Id, Router]}, - restart => permanent, - shutdown => 2000, - type => worker, - modules => ['iot_router'] - }. \ No newline at end of file diff --git a/apps/iot/src/iot_sup.erl b/apps/iot/src/iot_sup.erl index 94b1510..f048218 100644 --- a/apps/iot/src/iot_sup.erl +++ b/apps/iot/src/iot_sup.erl @@ -28,31 +28,13 @@ start_link() -> init([]) -> SupFlags = #{strategy => one_for_one, intensity => 1000, period => 3600}, ChildSpecs = [ - %#{ - % id => 'iot_host_sup', - % start => {'iot_host_sup', start_link, []}, - % restart => permanent, - % shutdown => 2000, - % type => supervisor, - % modules => ['iot_host_sup'] - %}, - #{ - id => 'iot_router_sup', - start => {'iot_router_sup', start_link, []}, + id => 'iot_host_sup', + start => {'iot_host_sup', start_link, []}, restart => permanent, shutdown => 2000, type => supervisor, - modules => ['iot_router_sup'] - }, - - #{ - id => 'iot_issue_sup', - start => {'iot_issue_sup', start_link, []}, - restart => permanent, - shutdown => 2000, - type => supervisor, - modules => ['iot_issue_sup'] + modules => ['iot_host_sup'] }, #{ @@ -63,6 +45,16 @@ init([]) -> type => worker, modules => ['iot_mqtt_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, []}, @@ -72,6 +64,12 @@ init([]) -> modules => ['iot_mqtt_publisher'] } ], - {ok, {SupFlags, ChildSpecs}}. + {ok, {SupFlags, pools() ++ ChildSpecs}}. %% internal functions + +pools() -> + {ok, Pools} = application:get_env(iot, pools), + lists:map(fun({Name, PoolArgs, WorkerArgs}) -> + poolboy:child_spec(Name, [{name, {local, Name}}|PoolArgs], WorkerArgs) + end, Pools). diff --git a/apps/iot/src/iot_util.erl b/apps/iot/src/iot_util.erl index ec01914..cf8f424 100644 --- a/apps/iot/src/iot_util.erl +++ b/apps/iot/src/iot_util.erl @@ -13,7 +13,7 @@ -export([timestamp/0, number_format/2, current_time/0]). -export([step/3, chunks/2, rand_bytes/1, uuid/0]). -export([json_data/1, json_error/2]). --export([queue_limited_in/3]). +-export([queue_limited_in/3, assert_call/2]). %% 时间,精确到毫秒 timestamp() -> @@ -67,6 +67,7 @@ json_error(ErrCode, ErrMessage) when is_integer(ErrCode), is_binary(ErrMessage) uuid() -> rand_bytes(16). +-spec rand_bytes(Size :: integer()) -> string(). rand_bytes(Size) when is_integer(Size), Size > 0 -> Size1 = erlang:ceil(Size / 2), Bytes = crypto:strong_rand_bytes(Size1), @@ -80,4 +81,9 @@ queue_limited_in(Item, Q, Num) when is_integer(Num) -> queue:in(Item, Q1); false -> queue:in(Item, Q) - end. \ No newline at end of file + end. + +assert_call(true, Fun) -> + Fun(); +assert_call(false, _) -> + ok. \ No newline at end of file diff --git a/apps/iot/src/mocker/host_mocker.erl b/apps/iot/src/mocker/host_mocker.erl new file mode 100644 index 0000000..555c2d9 --- /dev/null +++ b/apps/iot/src/mocker/host_mocker.erl @@ -0,0 +1,306 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2023, +%%% @doc +%%% +%%% @end +%%% Created : 14. 6月 2023 09:50 +%%%------------------------------------------------------------------- +-module(host_mocker). +-author("aresei"). + +-behaviour(gen_server). + +%% API +-export([start_link/1, stop/0]). +-export([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). +-define(TICKER_INTERVAL, 5000). + +-record(state, { + conn_pid, + topic :: binary(), + pub_key = <<"-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBMd0+XIwiBAIijwPets3N/MNP +zQwm/j6Zlmu5bC+NEDPwmz64OnoLkOTqSnerh7dN2pYeByEkTBu1prtb0cjEf0EP +2PP5liUQ6ykEnNfo20vMmSTlKFQZ2qb8jkRltBXTworR4cw92luFuJ0q9VUI3cx2 +JbOKI9SX52aXGYAc4QIDAQAB +-----END PUBLIC KEY-----">>, + pri_key = <<"-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMEx3T5cjCIEAiKP +A962zc38w0/NDCb+PpmWa7lsL40QM/CbPrg6eguQ5OpKd6uHt03alh4HISRMG7Wm +u1vRyMR/QQ/Y8/mWJRDrKQSc1+jbS8yZJOUoVBnapvyORGW0FdPCitHhzD3aW4W4 +nSr1VQjdzHYls4oj1JfnZpcZgBzhAgMBAAECgYEAuf49l6oVpzHgtFCnUyT+4c70 +YcFwb6HZtpegQjUrWk09E+kB6u8xTC1ElWL37wWLrcJBP8txVraG/sz0F6PR+dxz +D3pNaDtZNx1Ey5+LPBtvQRdW3PsUzHYOABX7oXaiB4Wsap9QVOlvjThfTjI+77Fq +jwgJr0b2hkrYMgqzUAECQQD2OnlRDmA2Y1kSBwfKyhfQfBBvcHlV/6Qlv/NfXqAo +lyl+okF4dK6TMHtuHstz6bf/VZ/7ImPKbCS8VtXuZN3BAkEAyNyaGIp7rbwb4Nee +hjeLcC9iB1BpglOFM92v3OGzQtHsUD5J2HrAw3IRKiMzO4unZ5SLF9fTNa/nhZjk +cr9HIQJAMXwQ85RTC7stpGzbSQsSfCji2LKfAASPhbKtA6atw1qV0UhkpgO1LgmZ +VMlFlDcNbnhT3ZHMwlq3i05cUjvdQQJAXjtwZ7cAUv9/LLq7ekgwuI7iNIA7H5ND +WJPWX4/bY6vMa9DtIAxmxsqK1vPwoyzfeq6rmqH8SqGdwoV6F4M5QQJAKZKnhlrx +1W9r2qGkn9Bds6CPYUwKZLLXJu32GFe+IXA184DpqBCo9xSxSQ+SN9FuSH5bUKcB +c17GntH5fO32Tg== +-----END PRIVATE KEY-----">>, + + aes = <<>> +}). + +%%%=================================================================== +%%% API +%%%=================================================================== + +test() -> + catch stop(), + host_mocker:start_link(<<"123123123123123">>). + +stop() -> + gen_server:stop(?MODULE). + +%% @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, pub_key = PubKey, pri_key = PrivateKey}) -> + + 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">>} -> + %% 建立到iot的会话 + Req = jiffy:encode(#{ + <<"method">> => <<"create_session">>, + <<"params">> => #{ + <<"pub_key">> => PubKey + } + }, [force_utf8]), + + {ok, Ref} = iot_mqtt_publisher:publish(Topic, Req, 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; + Info -> + Info + end, + {noreply, State}; + + <> -> + Command = iot_cipher_rsa:decode(Command0, PrivateKey), + 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}; + +%% 周期数据上传逻辑 +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), + + Msg = jiffy:encode(#{ + <<"method">> => <<"data">>, + <<"params">> => base64:encode(iot_cipher_aes:encrypt(Aes, Info)) + }, [force_utf8]), + + {ok, Ref} = iot_mqtt_publisher:publish(Topic, Msg, 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, + + erlang:start_timer(?TICKER_INTERVAL + 2000 + rand:uniform(5000), self(), data_ticker), + + {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), + + Msg = jiffy:encode(#{ + <<"method">> => <<"ping">>, + <<"params">> => base64:encode(Data) + }, [force_utf8]), + + {ok, Ref} = iot_mqtt_publisher:publish(Topic, Msg, 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, + + erlang:start_timer(?TICKER_INTERVAL, self(), ping_ticker), + + {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(10, #{<<"aes">> := Aes, <<"a">> := true}, State) -> + %% 启动周期ping + erlang:start_timer(?TICKER_INTERVAL, self(), ping_ticker), + + %% 数据收集 + erlang:start_timer(?TICKER_INTERVAL + 1000, self(), data_ticker), + + State#state{aes = Aes}. + diff --git a/apps/iot/src/mocker/iot_mock.erl b/apps/iot/src/mocker/iot_mock.erl new file mode 100644 index 0000000..89276b6 --- /dev/null +++ b/apps/iot/src/mocker/iot_mock.erl @@ -0,0 +1,65 @@ +%%%------------------------------------------------------------------- +%%% @author licheng5 +%%% @copyright (C) 2023, +%%% @doc +%%% +%%% @end +%%% Created : 14. 2月 2023 20:32 +%%%------------------------------------------------------------------- +-module(iot_mock). +-author("licheng5"). +-include("iot.hrl"). + +%% API +-export([rsa_encode/1]). +-export([insert_services/1]). + +insert_services(Num) -> + lists:foreach(fun(Id) -> + Res = mysql_client:insert(<<"micro_service">>, + #{ + <<"name">> => <<"微服务"/utf8, (integer_to_binary(Id))/binary>>, + <<"code">> => <<"1223423423423423"/utf8>>, + <<"type">> => 1, + <<"version">> => <<"v1.0">>, + <<"url">> => <<"https://www.baidu.com">>, + <<"detail">> => <<"这是一个关于测试的微服务"/utf8>> + }, false), + lager:debug("insert service result is: ~p", [Res]) + end, lists:seq(1, Num)). + +rsa_encode(Data) when is_binary(Data) -> + %% 读取相关配置 + PublicPemFile = "/tmp/keys/public.pem", + + %% 私钥保存解析后的 + {ok, PubBin} = file:read_file(PublicPemFile), + lager:debug("pub bin is: ~p", [PubBin]), + [Pub] = public_key:pem_decode(PubBin), + lager:debug("pub pem bin is: ~p", [Pub]), + PubKey = public_key:pem_entry_decode(Pub), + lager:debug("the public key is: ~p", [PubKey]), + + EncData = public_key:encrypt_public(Data, PubKey), + lager:debug("enc data is: ~p", [EncData]), + + rsa_decode(EncData), + + ok. + +rsa_decode(EncData) when is_binary(EncData) -> + %% 读取相关配置 + PublicPemFile = "/tmp/keys/pri.pem", + + %% 私钥保存解析后的 + {ok, PubBin} = file:read_file(PublicPemFile), + lager:debug("pub bin is: ~p", [PubBin]), + [Pub] = public_key:pem_decode(PubBin), + lager:debug("pub pem bin is: ~p", [Pub]), + PubKey = public_key:pem_entry_decode(Pub), + lager:debug("the public key is: ~p", [PubKey]), + + PlainData = public_key:decrypt_private(EncData, PubKey), + lager:debug("plain data is: ~p", [PlainData]), + + ok. \ No newline at end of file diff --git a/apps/iot/src/model/host_model.erl b/apps/iot/src/model/host_model.erl deleted file mode 100644 index 2005622..0000000 --- a/apps/iot/src/model/host_model.erl +++ /dev/null @@ -1,285 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2021, -%%% @doc -%%% -%%% @end -%%% Created : 27. 4月 2021 下午4:38 -%%%------------------------------------------------------------------- --module(host_model). --author("licheng5"). --include("iot.hrl"). --include_lib("stdlib/include/qlc.hrl"). - --define(TAB_NAME, host). - -%% API --export([get_host/1, get_hosts/3, get_all_hosts/0, get_stat/0, add_host/1, change_status/2, delete/1, table_size/0, find_hosts/3, activate/1, update_host/2]). --export([to_map/1, match_spec/1]). - -get_host(HostId) when is_binary(HostId) -> - case mnesia:dirty_read(?TAB_NAME, HostId) of - [Host = #host{}] -> - {ok, Host}; - _ -> - undefined - end. - -get_all_hosts() -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(?TAB_NAME), E#host.status == ?HOST_STATUS_ONLINE]), - qlc:e(Q) - end, - case mnesia:transaction(Fun) of - {atomic, Items} when is_list(Items) -> - {ok, sort(Items)}; - {aborted, Error} -> - {error, Error} - end. - -%% 获取app信息 --spec get_hosts(Spec :: tuple(), Start :: integer(), Limit :: integer()) -> - {ok, Items :: list(), TotalNum :: integer()} | - {error, Reason :: any()}. -get_hosts(Spec, Start, Limit) when is_integer(Limit), is_integer(Start), Start >= 0, Limit > 0 -> - Hosts0 = mnesia:dirty_select(?TAB_NAME, [Spec]), - SortedHosts = sort(Hosts0), - Len = length(SortedHosts), - case Len >= Start + 1 of - true -> - {ok, lists:sublist(SortedHosts, Start + 1, Limit), Len}; - false -> - {ok, [], Len} - end. - -%% 按照状态分组统计 -get_stat() -> - Fun = fun() -> - mnesia:foldl(fun(#host{status = Status}, Acc) -> - Num = maps:get(Status, Acc, 0), - Acc#{Status => Num + 1} - end, #{}, ?TAB_NAME) - end, - case mnesia:transaction(Fun) of - {atomic, Stat} when is_map(Stat) -> - lists:map(fun({Status, Num}) -> #{<<"status">> => Status, <<"num">> => Num} end, maps:to_list(Stat)); - {aborted, _} -> - #{} - end. - --spec find_hosts(Pred :: fun((#host{}) -> boolean()), Start :: integer(), Limit :: integer()) -> - {ok, Items :: [#host{}], Num :: integer()} | - {error, Reason :: any()}. -find_hosts(Pred, Start, Limit) when is_function(Pred, 1), is_integer(Limit), is_integer(Start), Start >= 0, Limit > 0 -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(?TAB_NAME)]), - qlc:fold(fun(Host, Acc) -> - case Pred(Host) of - true -> [Host|Acc]; - false -> Acc - end - end, [], Q) - - end, - case mnesia:transaction(Fun) of - {atomic, Hosts0} when is_list(Hosts0) -> - Hosts = sort(Hosts0), - Len = length(Hosts), - case Len >= Start + 1 of - true -> - {ok, lists:sublist(Hosts, Start + 1, Limit), Len}; - false -> - {ok, [], Len} - end; - {aborted, Error} -> - {error, Error} - end. - --spec add_host(Host :: #host{}) -> ok | {error, Reason :: binary()}. -add_host(Host = #host{serial_number = SerialNumber}) -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(?TAB_NAME), E#host.serial_number =:= SerialNumber]), - case qlc:e(Q) of - [_SomeHost|_] -> - mnesia:abort(<<"serial_number exists">>); - [] -> - mnesia:write(?TAB_NAME, Host, write) - end - end, - - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Error} -> - {error, Error} - end. - --spec change_status(HostId :: binary(), Status :: integer()) -> ok | {error, Reason :: any()}. -change_status(HostId, Status) when is_binary(HostId), is_integer(Status) -> - Fun = fun() -> - case mnesia:read(?TAB_NAME, HostId) of - [] -> - mnesia:abort(<<"host not found">>); - [Host] -> - mnesia:write(?TAB_NAME, Host#host{status = Status}, write) - end - end, - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - --spec update_host(HostId :: binary(), Fields :: #{}) -> ok | {error, Reason :: any()}. -update_host(HostId, Fields) when is_binary(HostId), is_map(Fields) -> - Fun = fun() -> - case mnesia:read(?TAB_NAME, HostId) of - [] -> - mnesia:abort(<<"host not found">>); - [Host] -> - NHost = lists:foldl(fun(E, Host0) -> - case E of - {<<"name">>, Name} when is_binary(Name) -> - Host0#host{name = Name}; - {<<"serial_number">>, SerialNumber} when is_binary(SerialNumber) -> - Host0#host{serial_number = SerialNumber}; - {<<"model">>, Model} when is_binary(Model) -> - Host0#host{model = Model}; - {<<"cell_id">>, CellId} when is_integer(CellId) -> - Host0#host{cell_id = CellId}; - {Name, _} -> - mnesia:abort(<<"invalid: ", Name/binary>>) - end - end, Host, maps:to_list(Fields)), - - mnesia:write(?TAB_NAME, NHost, write) - end - end, - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - --spec activate(HostId :: binary()) -> ok | {error, Reason :: any()}. -activate(HostId) when is_binary(HostId) -> - Fun = fun() -> - case mnesia:read(host, HostId) of - [] -> - mnesia:abort(<<"host not found">>); - [Host = #host{status = Status}] -> - case Status =:= ?HOST_STATUS_INACTIVE of - true -> - Aes = iot_util:rand_bytes(16), - NHost = Host#host{ - aes = Aes, - activated_ts = iot_util:current_time(), - update_ts = iot_util:current_time(), - status = ?HOST_STATUS_ONLINE - }, - ok = mnesia:write(host, NHost, write), - NHost; - false -> - mnesia:abort(<<"host status invalid">>) - end - end - end, - case mnesia:transaction(Fun) of - {atomic, Host} -> - {ok, Host}; - {aborted, Reason} -> - {error, Reason} - end. - --spec delete(HostId :: binary()) -> ok | {error, Reason :: any()}. -delete(HostId) when is_binary(HostId) -> - case mnesia:transaction(fun() -> mnesia:delete(host, HostId, write) end) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - -%% 获取查询的过滤条件 -match_spec(Specs) when is_list(Specs) -> - MatchHead = #host{model = '$1', cell_id = '$2', _ = '_'}, - Guard = guard(Specs), - Result = ['$_'], - {MatchHead, Guard, Result}. - -guard(Specs) when is_list(Specs) -> - guard(Specs, []). -guard([], Guard) -> - Guard; -guard([{model, Model}|Tail], Guard) when Model =/= <<"">> -> - guard(Tail, [{'=:=', '$1', Model}|Guard]); -guard([{cell, CellId}|Tail], Guard) when CellId > 0 -> - guard(Tail, [{'=:=', '$2', CellId}|Guard]); -guard([_|Tail], Guard) -> - guard(Tail, Guard). - -%% 获取app表的数据大小 -table_size() -> - mnesia:table_info(host, size). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% 主机信息的排序 -sort(Hosts) when is_list(Hosts) -> - lists:sort(fun(#host{name = N1}, #host{name = N2}) -> N1 < N2 end, Hosts). - -%% 将数据转换成hash -to_map(#host{host_id = HostId, name = Name, model = Model, cell_id = CellId, activated_ts = ActivatedTs, update_ts = UpdateTs, status = Status, metric = Metric}) -> - #{ - <<"host_id">> => HostId, - <<"name">> => Name, - <<"model">> => Model, - <<"cell_id">> => CellId, - <<"activated_ts">> => ActivatedTs, - <<"update_ts">> => UpdateTs, - <<"status">> => Status, - <<"metric">> => host_metric(Metric) - }. - -host_metric(undefined) -> - #{}; -host_metric(#host_metric{cpus = Cpus, cpu_temperature = CpuTemperature, memory = Memory, disk = Disk, interfaces = Interfaces}) -> - #{ - <<"cpus">> => host_cpus(Cpus), - <<"cpu_temperature">> => CpuTemperature, - <<"memory">> => host_memory(Memory), - <<"disk">> => host_disk(Disk), - <<"interfaces">> => host_interfaces(Interfaces) - }. - -host_cpus(Cpus) when is_list(Cpus) -> - lists:map(fun(#cpu_metric{num = Num, load = Load}) -> #{<<"num">> => Num, <<"load">> => Load } end, Cpus); -host_cpus(_) -> - []. - -host_memory(#memory_metric{used = MemoryUsed, total = MemoryTotal}) -> - #{<<"used">> => MemoryUsed, <<"total">> => MemoryTotal}; -host_memory(_) -> - #{}. - -host_disk(#disk_metric{used = DiskUsed, total = DiskTotal}) -> - #{<<"used">> => DiskUsed, <<"total">> => DiskTotal}; -host_disk(_) -> - #{}. - -%% 转换接口信息 -host_interfaces(Interfaces) when is_list(Interfaces) -> - lists:map(fun(#interface_metric{name = IName, desc = IDesc, status = IStatus, detail = IDetail}) -> - #{ - <<"name">> => IName, - <<"desc">> => IDesc, - <<"status">> => IStatus, - <<"detatil">> => IDetail - } - end, Interfaces); -host_interfaces(_) -> - []. \ No newline at end of file diff --git a/apps/iot/src/model/id_generator_model.erl b/apps/iot/src/model/id_generator_model.erl deleted file mode 100644 index 1010272..0000000 --- a/apps/iot/src/model/id_generator_model.erl +++ /dev/null @@ -1,23 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2021, -%%% @doc -%%% -%%% @end -%%% Created : 27. 4月 2021 下午4:38 -%%%------------------------------------------------------------------- --module(id_generator_model). --author("licheng5"). --include("iot.hrl"). - -%% API --export([generate/1, new_terminal_id/0]). - -%% 生成对应的自增id --spec generate(Name :: atom()) -> Id :: integer(). -generate(Name) when is_atom(Name) -> - mnesia:dirty_update_counter(id_generator, Name, 1). - --spec new_terminal_id() -> Id :: integer(). -new_terminal_id() -> - generate(terminal). \ No newline at end of file diff --git a/apps/iot/src/model/issue_model.erl b/apps/iot/src/model/issue_model.erl deleted file mode 100644 index 6c2ed47..0000000 --- a/apps/iot/src/model/issue_model.erl +++ /dev/null @@ -1,126 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2021, -%%% @doc -%%% -%%% @end -%%% Created : 27. 4月 2021 下午4:38 -%%%------------------------------------------------------------------- --module(issue_model). --author("licheng5"). --include("iot.hrl"). --include_lib("stdlib/include/qlc.hrl"). - --define(TAB_NAME, issue). - -%% API --export([get_issue/1, get_issues/3, add_issue/1, change_status/2, delete/1, table_size/0, get_user_issues/3]). --export([to_map/1]). - -get_issue(IssueId) when is_integer(IssueId) -> - case mnesia:dirty_read(?TAB_NAME, IssueId) of - [Issue = #issue{}] -> - {ok, Issue}; - _ -> - undefined - end. - -%% 获取app信息 --spec get_issues(Filter :: any(), Start :: integer(), Limit :: integer()) -> - {ok, Items :: list(), TotalNum :: integer()} | - {error, Reason :: any()}. -get_issues(Spec, Start, Limit) when is_integer(Limit), is_integer(Start), Start >= 0, Limit > 0 -> - Issues0 = mnesia:dirty_select(?TAB_NAME, [Spec]), - Issues = sort(Issues0), - Len = length(Issues), - case Len >= Start + 1 of - true -> - {ok, lists:sublist(Issues, Start + 1, Limit), Len}; - false -> - {ok, [], Len} - end. - --spec get_user_issues(UserId :: integer(), Start :: integer(), Limit :: integer()) -> - {ok, Items :: [#host{}], Num :: integer()} | - {error, Reason :: any()}. -get_user_issues(UserId, Start, Limit) when is_integer(UserId), is_integer(Limit), is_integer(Start), Start >= 0, Limit > 0 -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(?TAB_NAME), E#issue.uid =:= UserId]), - qlc:e(Q) - end, - case mnesia:transaction(Fun) of - {atomic, Issues0} when is_list(Issues0) -> - Issues = sort(Issues0), - Len = length(Issues), - case Len >= Start + 1 of - true -> - {ok, lists:sublist(Issues, Start + 1, Limit), Len}; - false -> - {ok, [], Len} - end; - {aborted, Error} -> - {error, Error} - end. - --spec add_issue(Issue :: #issue{}) -> ok | {error, Reason :: any()}. -add_issue(Issue = #issue{}) -> - case mnesia:transaction(fun() -> mnesia:write(?TAB_NAME, Issue, write) end) of - {atomic, ok} -> - ok; - {aborted, Error} -> - {error, Error} - end. - --spec change_status(IssueId :: integer(), Status :: integer()) -> ok | {error, Reason :: any()}. -change_status(IssueId, Status) when is_integer(IssueId), is_integer(Status) -> - Fun = fun() -> - case mnesia:read(?TAB_NAME, IssueId) of - [] -> - mnesia:abort(<<"issue not found">>); - [Issue] -> - mnesia:write(?TAB_NAME, Issue#issue{status = Status}, write) - end - end, - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - --spec delete(IssueId :: binary()) -> ok | {error, Reason :: any()}. -delete(IssueId) when is_integer(IssueId) -> - case mnesia:transaction(fun() -> mnesia:delete(?TAB_NAME, IssueId, write) end) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - -%% 获取app表的数据大小 -table_size() -> - mnesia:table_info(?TAB_NAME, size). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% 默认按照id倒序进行排序 -sort(Issues) when is_list(Issues) -> - lists:sort(fun(#issue{issue_id = Id0}, #issue{issue_id = Id1}) -> Id0 < Id1 end, Issues). - -to_map(#issue{issue_id = IssueId, name = Name, uid = Uid, deploy_type = DeployType, assoc_id = AssocId, hosts = Hosts, timeout = Timeout, - create_ts = CreateTs, results = Results, status = Status}) -> - - #{ - <<"issue_id">> => IssueId, - <<"name">> => Name, - <<"uid">> => Uid, - <<"deploy_type">> => DeployType, - <<"assoc_id">> => AssocId, - <<"hosts">> => Hosts, - <<"timeout">> => Timeout, - <<"status">> => Status, - <<"create_ts">> => CreateTs, - <<"results">> => Results - }. \ No newline at end of file diff --git a/apps/iot/src/model/log_model.erl b/apps/iot/src/model/log_model.erl deleted file mode 100644 index 92a74bb..0000000 --- a/apps/iot/src/model/log_model.erl +++ /dev/null @@ -1,126 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2021, -%%% @doc -%%% -%%% @end -%%% Created : 27. 4月 2021 下午4:38 -%%%------------------------------------------------------------------- --module(log_model). --author("licheng5"). --include("iot.hrl"). --include_lib("stdlib/include/qlc.hrl"). - --define(TAB_NAME, log). - -%% API --export([get_logs/1, add_log/1, delete/1, table_size/0, get_last_logs/1]). --export([to_map/1]). - -%% 获取app信息, 该函数的效率较低 --spec get_logs(Limit :: integer()) -> {ok, Logs :: list()} | {error, Reason :: any()}. -get_logs(Limit) when is_integer(Limit), Limit > 0 -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(?TAB_NAME)]), - %% 按照创建时间倒序排列 - Order = fun(A, B) -> A#log.create_ts > B#log.create_ts end, - Q1 = qlc:sort(Q, [{order, Order}]), - - QC = qlc:cursor(Q1), - qlc:next_answers(QC, Limit) - end, - case mnesia:transaction(Fun) of - {atomic, Logs} when is_list(Logs) -> - {ok, Logs}; - {aborted, Error} -> - {error, Error} - end. - -%% 获取最新的n条记录 --spec get_last_logs(N :: integer()) -> {ok, Logs :: [#log{}]} | {error, Reason :: any()}. -get_last_logs(N) when N > 0 -> - Fun = fun() -> - Keys = read_last_keys(N), - lists:flatmap(fun(Key) -> mnesia:read(?TAB_NAME, Key, read) end, lists:reverse(Keys)) - end, - case mnesia:transaction(Fun) of - {atomic, Logs} when is_list(Logs) -> - {ok, Logs}; - {aborted, Error} -> - {error, Error} - end. - --spec add_log(Log :: #log{}) -> ok | {error, Reason :: binary()}. -add_log(Log = #log{}) -> - case mnesia:transaction(fun() -> mnesia:write(?TAB_NAME, Log, write) end) of - {atomic, ok} -> - ok; - {aborted, Error} -> - {error, Error} - end. - --spec delete(LogId :: integer()) -> ok | {error, Reason :: any()}. -delete(LogId) when is_binary(LogId) -> - case mnesia:transaction(fun() -> mnesia:delete(?TAB_NAME, LogId, write) end) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - -%% 从后向前变量表 -read_last_keys(N) when N >= 1 -> - case mnesia:last(?TAB_NAME) of - '$end_of_table' -> - []; - LastKey -> - read_last_keys0(N - 1, LastKey, [LastKey]) - end. -read_last_keys0(0, _, Keys) -> - Keys; -read_last_keys0(N, Key, Keys) -> - case mnesia:prev(?TAB_NAME, Key) of - '$end_of_table' -> - Keys; - PrevKey -> - read_last_keys0(N - 1, PrevKey, [PrevKey|Keys]) - end. - -%% 获取app表的数据大小 -table_size() -> - mnesia:table_info(?TAB_NAME, size). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% 主机信息的排序 -sort(Logs) when is_list(Logs) -> - lists:sort(fun(#log{create_ts = Ts0}, #log{create_ts = Ts1}) -> Ts0 > Ts1 end, Logs). - -%% 将数据转换成hash -to_map(#log{log_id = LogId, action_name = ActionName, device_type = DeviceType, assoc_id = AssocId, create_ts = CreateTs}) -> - DeviceInfo = case DeviceType of - ?DEVICE_HOST -> - case host_model:get_host(AssocId) of - {ok, Host} -> - host_model:to_map(Host); - _ -> - #{} - end; - ?DEVICE_TERMINAL -> - case terminal_model:get_terminal(AssocId) of - {ok, Terminal} -> - terminal_model:to_map(Terminal); - _ -> - #{} - end - end, - - #{ - <<"log_id">> => LogId, - <<"action_name">> => ActionName, - <<"device_type">> => DeviceType, - <<"device_info">> => DeviceInfo, - <<"create_ts">> => CreateTs - }. \ No newline at end of file diff --git a/apps/iot/src/model/router_model.erl b/apps/iot/src/model/router_model.erl deleted file mode 100644 index 657317b..0000000 --- a/apps/iot/src/model/router_model.erl +++ /dev/null @@ -1,149 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2021, -%%% @doc -%%% -%%% @end -%%% Created : 27. 4月 2021 下午4:38 -%%%------------------------------------------------------------------- --module(router_model). --author("licheng5"). --include("iot.hrl"). --include_lib("stdlib/include/qlc.hrl"). - -%% API --export([get_router/1, get_routers/3, add_router/1, change_status/2, delete/1, table_size/0, get_all_routers/0, get_all_valid_routers/0]). --export([to_map/1, match_spec/1]). - -get_router(RouterId) when is_integer(RouterId) -> - case mnesia:dirty_read(router, RouterId) of - [Router] -> - {ok, Router}; - _ -> - undefined - end. - -%% 获取app信息 --spec get_all_routers() -> {ok, Routers :: list()} | {error, Reason :: any()}. -get_all_routers() -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(router)]), - qlc:e(Q) - end, - case mnesia:transaction(Fun) of - {atomic, Routers} -> - {ok, sort(Routers)}; - {aborted, Reason} -> - {error, Reason} - end. - -%% 获取app信息 --spec get_all_valid_routers() -> {ok, Routers :: list()} | {error, Reason :: any()}. -get_all_valid_routers() -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(router), E#router.status =:= 1]), - qlc:e(Q) - end, - case mnesia:transaction(Fun) of - {atomic, Routers} -> - {ok, sort(Routers)}; - {aborted, Reason} -> - {error, Reason} - end. - -%% 获取app信息 --spec get_routers(Filter :: any(), Start :: integer(), Limit :: integer()) -> - {ok, Items :: list(), TotalNum :: integer()} | - {error, Reason :: any()}. -get_routers(Spec, Start, Limit) when is_integer(Limit), is_integer(Start), Start >= 0, Limit > 0 -> - Routers0 = mnesia:dirty_select(router, [Spec]), - Routers = sort(Routers0), - Len = length(Routers), - case Len >= Start + 1 of - true -> - {ok, lists:sublist(Routers, Start + 1, Limit), Len}; - false -> - {ok, [], Len} - end. - --spec add_router(Router :: #router{}) -> ok | {error, Reason :: any()}. -add_router(Router = #router{}) -> - case mnesia:transaction(fun() -> mnesia:write(router, Router, write) end) of - {atomic, ok} -> - ok; - {aborted, Error} -> - {error, Error} - end. - --spec change_status(RouterId :: integer(), Status :: integer()) -> ok | {error, Reason :: any()}. -change_status(RouterId, Status) when is_integer(RouterId), is_integer(Status) -> - Fun = fun() -> - case mnesia:read(router, RouterId) of - [] -> - mnesia:abort(<<"router not found">>); - [Router] -> - mnesia:write(host, Router#router{status = Status}, write) - end - end, - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - --spec delete(HostId :: integer()) -> ok | {error, Reason :: any()}. -delete(RouterId) when is_integer(RouterId) -> - case mnesia:transaction(fun() -> mnesia:delete(router, RouterId, write) end) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - -%% 获取查询的过滤条件 -match_spec(Specs) when is_list(Specs) -> - MatchHead = #router{name = '$1', _ = '_'}, - Result = ['$_'], - Guard = guard(Specs), - Result = ['$_'], - {MatchHead, Guard, Result}. - -guard(Specs) when is_list(Specs) -> - guard(Specs, []). -guard([], Guard) -> - Guard; -guard([{name, Name}|Tail], Guard) when Name =/= <<"">> -> - guard(Tail, [{'=:=', '$1', Name}|Guard]); -guard([_|Tail], Guard) -> - guard(Tail, Guard). - -%% 获取app表的数据大小 -table_size() -> - mnesia:table_info(router, size). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -sort(Routers) when is_list(Routers) -> - lists:sort(fun(#router{router_id = Id0}, #router{router_id = Id1}) -> Id0 < Id1 end, Routers). - -to_map(#router{router_id = RouterId, name = Name, status = Status, endpoint = Endpoint}) -> - EndpointInfo = case Endpoint of - undefined -> - #{}; - #http_endpoint{url = Url} -> - #{ - <<"type">> => <<"http">>, - <<"url">> => Url - }; - #kafka_endpoint{} -> - #{} - end, - #{ - <<"router_id">> => RouterId, - <<"name">> => Name, - <<"status">> => Status, - <<"endpoint">> => EndpointInfo - }. \ No newline at end of file diff --git a/apps/iot/src/model/scenario_deploy_model.erl b/apps/iot/src/model/scenario_deploy_model.erl deleted file mode 100644 index 7f20cb3..0000000 --- a/apps/iot/src/model/scenario_deploy_model.erl +++ /dev/null @@ -1,100 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2021, -%%% @doc -%%% 场景部署关系表 -%%% @end -%%% Created : 27. 4月 2021 下午4:38 -%%%------------------------------------------------------------------- --module(scenario_deploy_model). --author("licheng5"). --include("iot.hrl"). --include_lib("stdlib/include/qlc.hrl"). - --define(TAB_NAME, scenario_deploy). - -%% API --export([get_host_deploy_list/1, get_scenario_deploy_list/1, add_deploy/1, change_status/2, delete/1, table_size/0]). --export([to_map/1]). - --spec get_host_deploy_list(HostId :: binary()) -> {ok, List :: [#scenario_deploy{}]} | {error, Reason :: any()}. -get_host_deploy_list(HostId) when is_binary(HostId) -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(?TAB_NAME), E#scenario_deploy.host_id =:= HostId]), - qlc:e(Q) - end, - case mnesia:transaction(Fun) of - {atomic, Items} -> - {ok, sort(Items)}; - {aborted, Reason} -> - {error, Reason} - end. - --spec get_scenario_deploy_list(ScenarioId :: integer()) -> {ok, List :: [#scenario_deploy{}]} | {error, Reason :: any()}. -get_scenario_deploy_list(ScenarioId) when is_integer(ScenarioId) -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(?TAB_NAME), E#scenario_deploy.scenario_id =:= ScenarioId]), - qlc:e(Q) - end, - case mnesia:transaction(Fun) of - {atomic, Items} -> - {ok, sort(Items)}; - {aborted, Reason} -> - {error, Reason} - end. - --spec add_deploy(Deploy :: #scenario_deploy{}) -> ok | {error, Reason :: any()}. -add_deploy(Deploy = #scenario_deploy{}) -> - case mnesia:transaction(fun() -> mnesia:write(?TAB_NAME, Deploy, write) end) of - {atomic, _} -> - ok; - {aborted, Error} -> - {error, Error} - end. - --spec change_status(DeployId :: integer(), Status :: integer()) -> ok | {error, Reason :: any()}. -change_status(DeployId, Status) when is_integer(DeployId), is_integer(Status) -> - Fun = fun() -> - case mnesia:read(?TAB_NAME, DeployId) of - [] -> - mnesia:abort(<<"deploy not found">>); - [Deploy] -> - mnesia:write(?TAB_NAME, Deploy#scenario_deploy{status = Status}, write) - end - end, - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - --spec delete(DeployId :: binary()) -> ok | {error, Reason :: any()}. -delete(DeployId) when is_integer(DeployId) -> - case mnesia:transaction(fun() -> mnesia:delete(?TAB_NAME, DeployId, write) end) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - -%% 获取app表的数据大小 -table_size() -> - mnesia:table_info(?TAB_NAME, size). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -sort(DeployList) when is_list(DeployList) -> - lists:sort(fun(#scenario_deploy{deploy_id = Id0}, #scenario_deploy{deploy_id = Id1}) -> Id0 < Id1 end, DeployList). - -to_map(#scenario_deploy{deploy_id = DeployId, scenario_id = ScenarioId, host_id = HostId, create_ts = CreateTs, update_ts = UpdateTs, status = Status}) -> - #{ - <<"deploy_id">> => DeployId, - <<"scenario_id">> => ScenarioId, - <<"host_id">> => HostId, - <<"create_ts">> => CreateTs, - <<"update_ts">> => UpdateTs, - <<"status">> => Status - }. \ No newline at end of file diff --git a/apps/iot/src/model/scenario_model.erl b/apps/iot/src/model/scenario_model.erl deleted file mode 100644 index 2b38aae..0000000 --- a/apps/iot/src/model/scenario_model.erl +++ /dev/null @@ -1,115 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2021, -%%% @doc -%%% -%%% @end -%%% Created : 27. 4月 2021 下午4:38 -%%%------------------------------------------------------------------- --module(scenario_model). --author("licheng5"). --include("iot.hrl"). --include_lib("stdlib/include/qlc.hrl"). - --define(TAB_NAME, scenario). - -%% API --export([get_scenario/1, get_scenario_list/3, add_scenario/1, change_status/2, delete/1, table_size/0]). --export([to_map/1, match_spec/1]). - --spec get_scenario(ScenarioId :: integer()) -> {ok, #scenario{}} | undefined. -get_scenario(ScenarioId) when is_integer(ScenarioId) -> - case mnesia:dirty_read(?TAB_NAME, ScenarioId) of - [Scenario = #scenario{}] -> - {ok, Scenario}; - _ -> - undefined - end. - -%% 获取app信息 --spec get_scenario_list(Filter :: any(), Start :: integer(), Limit :: integer()) -> - {ok, Items :: list(), TotalNum :: integer()} | - {error, Reason :: any()}. -get_scenario_list(Spec, Start, Limit) when is_integer(Limit), is_integer(Start), Start >= 0, Limit > 0 -> - Scenarios0 = mnesia:dirty_select(?TAB_NAME, [Spec]), - Scenarios = sort(Scenarios0), - Len = length(Scenarios), - case Len >= Start + 1 of - true -> - {ok, lists:sublist(Scenarios, Start + 1, Limit), Len}; - false -> - {ok, [], Len} - end. - --spec add_scenario(Scenario :: #scenario{}) -> ok | {error, Reason :: any()}. -add_scenario(Scenario = #scenario{}) -> - case mnesia:transaction(fun() -> mnesia:write(?TAB_NAME, Scenario, write) end) of - {atomic, ok} -> - ok; - {aborted, Error} -> - {error, Error} - end. - --spec change_status(ScenarioId :: integer(), Status :: integer()) -> ok | {error, Reason :: any()}. -change_status(ScenarioId, Status) when is_integer(ScenarioId), is_integer(Status) -> - Fun = fun() -> - case mnesia:read(scenario, ScenarioId) of - [] -> - mnesia:abort(<<"scenario not found">>); - [Scenario] -> - mnesia:write(?TAB_NAME, Scenario#scenario{status = Status}, write) - end - end, - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - --spec delete(ScenarioId :: binary()) -> ok | {error, Reason :: any()}. -delete(ScenarioId) when is_integer(ScenarioId) -> - case mnesia:transaction(fun() -> mnesia:delete(?TAB_NAME, ScenarioId, write) end) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - -%% 获取查询的过滤条件 -match_spec(Specs) when is_list(Specs) -> - MatchHead = #scenario{name = '$1', _ = '_'}, - Result = ['$_'], - Guard = guard(Specs), - Result = ['$_'], - {MatchHead, Guard, Result}. - -guard(Specs) when is_list(Specs) -> - guard(Specs, []). -guard([], Guard) -> - Guard; -guard([{name, Name}|Tail], Guard) when Name =/= <<"">> -> - guard(Tail, [{'=:=', '$1', Name}|Guard]); -guard([_|Tail], Guard) -> - guard(Tail, Guard). - -%% 获取app表的数据大小 -table_size() -> - mnesia:table_info(scenario, size). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -sort(Scenarios) when is_list(Scenarios) -> - lists:sort(fun(#scenario{scenario_id = Id0}, #scenario{scenario_id = Id1}) -> Id0 < Id1 end, Scenarios). - -to_map(#scenario{scenario_id = ScenarioId, name = Name, desc = Desc, rule = Rule, update_ts = UpdateTs, status = Status}) -> - #{ - <<"scenario_id">> => ScenarioId, - <<"name">> => Name, - <<"desc">> => Desc, - <<"rule">> => Rule, - <<"update_ts">> => UpdateTs, - <<"status">> => Status - }. \ No newline at end of file diff --git a/apps/iot/src/model/service_model.erl b/apps/iot/src/model/service_model.erl deleted file mode 100644 index 52d5a30..0000000 --- a/apps/iot/src/model/service_model.erl +++ /dev/null @@ -1,128 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2021, -%%% @doc -%%% -%%% @end -%%% Created : 27. 4月 2021 下午4:38 -%%%------------------------------------------------------------------- --module(service_model). --author("licheng5"). --include("iot.hrl"). --include_lib("stdlib/include/qlc.hrl"). - -%% API --export([get_host_services/1, get_service/2, add_service/1, change_status/2, delete/1, table_size/0]). --export([update_metric/2, to_map/1]). - -get_service(HostId, ServiceName) when is_binary(HostId), is_binary(ServiceName) -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(service), E#service.host_id =:= HostId, E#service.name =:= ServiceName]), - qlc:e(Q) - end, - case mnesia:transaction(Fun) of - [Service] -> - {ok, Service}; - _ -> - undefined - end. - -get_host_services(HostId) when is_binary(HostId) -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(service), E#service.host_id =:= HostId]), - qlc:e(Q) - end, - case mnesia:transaction(Fun) of - {atomic, Services} when is_list(Services) -> - {ok, Services}; - {aborted, Error} -> - {error, Error} - end. - -add_service(Service = #service{}) -> - case mnesia:transaction(fun() -> mnesia:write(service, Service, write) end) of - {atomic, _} -> - ok; - {aborted, Error} -> - {error, Error} - end. - -change_status(ServiceId, Status) when is_binary(ServiceId), is_integer(Status) -> - Fun = fun() -> - case mnesia:read(service, ServiceId) of - [] -> - mnesia:abort(<<"host not found">>); - [Service] -> - NService = Service#service{status = Status}, - mnesia:write(service, NService, write) - end - end, - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - -update_metric(ServiceId, Metrics) when is_binary(ServiceId), is_list(Metrics) -> - Fun = fun() -> - case mnesia:read(service, ServiceId) of - [] -> - mnesia:abort(<<"service not found">>); - [Service = #service{metrics = Metrics}] -> - NService = Service#service{metrics = Metrics}, - mnesia:write(service, NService, write) - end - end, - - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - --spec delete(HostId :: binary()) -> ok | {error, Reason :: any()}. -delete(ServiceId) when is_binary(ServiceId) -> - case mnesia:transaction(fun() -> mnesia:delete(service, ServiceId, write) end) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - -%% 获取app表的数据大小 -table_size() -> - mnesia:table_info(service, size). - -to_map(#service{service_id = ServiceId, host_id = HostId, name = Name, category = Category, apply_object = ApplyObject, - version = Version, execute_count = ExecuteCount, deploy_ts = DeployTs, metrics = Metrics, status = Status}) -> - - Metrics1 = lists:map(fun(#service_metric{symbol = MetricSymbol, name = MetricName, last_value = LastValue, unit = Unit, queue = Queue, update_ts = UpdateTs}) -> - Q = lists:map(fun({Ts, Val}) -> #{<<"time">> => Ts, <<"value">> => Val} end, queue:to_list(Queue)), - #{ - <<"symbol">> => MetricSymbol, - <<"name">> => MetricName, - <<"last_value">> => LastValue, - <<"queue">> => Q, - <<"unit">> => Unit, - <<"update_ts">> => UpdateTs - } - end, Metrics), - - #{ - <<"service_id">> => ServiceId, - <<"host_id">> => HostId, - <<"name">> => Name, - <<"category">> => Category, - <<"apply_object">> => ApplyObject, - <<"version">> => Version, - <<"execute_count">> => ExecuteCount, - <<"deploy_ts">> => DeployTs, - <<"metrics">> => Metrics1, - <<"status">> => Status - }. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/apps/iot/src/model/terminal_model.erl b/apps/iot/src/model/terminal_model.erl deleted file mode 100644 index 85c5d26..0000000 --- a/apps/iot/src/model/terminal_model.erl +++ /dev/null @@ -1,214 +0,0 @@ -%%%------------------------------------------------------------------- -%%% @author licheng5 -%%% @copyright (C) 2021, -%%% @doc -%%% -%%% @end -%%% Created : 27. 4月 2021 下午4:38 -%%%------------------------------------------------------------------- --module(terminal_model). --author("licheng5"). --include("iot.hrl"). --include_lib("stdlib/include/qlc.hrl"). - --define(TAB_NAME, terminal). - -%% API --export([next_id/0]). --export([get_terminal/1, get_all_terminals/0, get_host_terminals/1, find_terminals/3, get_stat/0, table_size/0, match_spec/1]). --export([change_status/2, delete/1, to_map/1, add_terminal/1, update_terminal/2]). - -%% 生成信息的id信息 --spec next_id() -> Id :: integer(). -next_id() -> - id_generator_model:new_terminal_id(). - --spec get_terminal(TerminalId :: integer()) -> {ok, #terminal{}} | undefined. -get_terminal(TerminalId) when is_integer(TerminalId) -> - case mnesia:dirty_read(?TAB_NAME, TerminalId) of - [Terminal] -> - {ok, Terminal}; - _ -> - undefined - end. - -%% 获取app信息 --spec get_all_terminals() -> {ok, Terminals :: [#terminal{}]} | {error, Reason :: any()}. -get_all_terminals() -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(?TAB_NAME)]), - qlc:e(Q) - end, - case mnesia:transaction(Fun) of - {atomic, Terminals} when is_list(Terminals) -> - {ok, sort(Terminals)}; - {aborted, Error} -> - {error, Error} - end. - --spec get_host_terminals(HostId :: binary()) -> {ok, Terminals :: [#terminal{}]} | {error, Reason :: any()}. -get_host_terminals(HostId) when is_binary(HostId) -> - Fun = fun() -> - Q = qlc:q([E || E <- mnesia:table(?TAB_NAME), E#terminal.host_id =:= HostId]), - qlc:e(Q) - end, - case mnesia:transaction(Fun) of - {atomic, Terminals} when is_list(Terminals) -> - {ok, sort(Terminals)}; - {aborted, Error} -> - {error, Error} - end. - -%% 获取app信息 --spec find_terminals(Matches :: [{Name :: atom(), Val :: any()}], Start :: integer(), Limit :: integer()) -> - {ok, Terminals :: list(), TotalNum :: integer()} | - {error, Reason :: any()}. -find_terminals(Matches, Start, Limit) when is_list(Matches), is_integer(Limit), is_integer(Start), Start >= 0, Limit > 0 -> - MatchSpec = terminal_model:match_spec(Matches), - Terminals0 = mnesia:dirty_select(?TAB_NAME, [MatchSpec]), - Terminals = sort(Terminals0), - Len = length(Terminals), - case Len >= Start + 1 of - true -> - {ok, lists:sublist(Terminals, Start + 1, Limit), Len}; - false -> - {ok, [], Len} - end. - -%% 获取状态分组统计信息 --spec get_stat() -> {ok, {Total :: integer(), Stat :: #{}}} | {error, Reason :: any()}. -get_stat() -> - Fun = fun() -> - mnesia:foldl(fun(#terminal{status = Status}, {Total, Acc}) -> - Num = maps:get(Status, Acc, 0), - {Total + 1, Acc#{Status => Num + 1}} - end, {0, #{}}, ?TAB_NAME) - end, - - case mnesia:transaction(Fun) of - {atomic, Stat} -> - {ok, Stat}; - {aborted, Reason} -> - {error, Reason} - end. - --spec add_terminal(Terminal :: #terminal{}) -> ok | {error, Reason :: any()}. -add_terminal(Terminal = #terminal{}) -> - case mnesia:transaction(fun() -> mnesia:write(?TAB_NAME, Terminal, write) end) of - {atomic, _} -> - ok; - {aborted, Error} -> - {error, Error} - end. - --spec update_terminal(TerminalId :: integer(), Fields :: #{}) -> ok | {error, Reason :: any()}. -update_terminal(TerminalId, Fields) when is_integer(TerminalId), is_map(Fields) -> - Fun = fun() -> - case mnesia:read(?TAB_NAME, TerminalId) of - [] -> - mnesia:abort(<<"terminal not found">>); - [Terminal] -> - NTerminal = lists:foldl(fun(E, Terminal0) -> - case E of - {<<"name">>, Name} when is_binary(Name) -> - Terminal0#terminal{name = Name}; - {<<"serial_number">>, SerialNumber} when is_binary(SerialNumber) -> - Terminal0#terminal{serial_number = SerialNumber}; - {<<"code">>, Code} when is_binary(Code) -> - Terminal0#terminal{code = Code}; - {<<"access_protocol">>, AccessProtocol} when is_binary(AccessProtocol) -> - Terminal0#terminal{access_protocol = AccessProtocol}; - {<<"product_id">>, ProductId} when is_integer(ProductId) -> - Terminal0#terminal{product_id = ProductId}; - {<<"vendor_id">>, VendorId} when is_integer(VendorId) -> - Terminal0#terminal{vendor_id = VendorId}; - {<<"model">>, Model} when is_binary(Model) -> - Terminal0#terminal{model = Model}; - {<<"cell_id">>, CellId} when is_integer(CellId) -> - Terminal0#terminal{cell_id = CellId}; - {Name, _} -> - mnesia:abort(<<"invalid: ", Name/binary>>) - end - end, Terminal, maps:to_list(Fields)), - - mnesia:write(?TAB_NAME, NTerminal, write) - end - end, - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - --spec change_status(TerminalId :: integer(), Status :: integer()) -> ok | {error, Reason :: any()}. -change_status(TerminalId, Status) when is_integer(TerminalId), is_integer(Status) -> - Fun = fun() -> - case mnesia:read(?TAB_NAME, TerminalId) of - [] -> - mnesia:abort(<<"terminal not found">>); - [Terminal] -> - mnesia:write(?TAB_NAME, Terminal#host{status = Status}, write) - end - end, - case mnesia:transaction(Fun) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - --spec delete(TerminalId :: integer()) -> ok | {error, Reason :: any()}. -delete(TerminalId) when is_integer(TerminalId) -> - case mnesia:transaction(fun() -> mnesia:delete(?TAB_NAME, TerminalId, write) end) of - {atomic, ok} -> - ok; - {aborted, Reason} -> - {error, Reason} - end. - -%% 获取app表的数据大小 -table_size() -> - mnesia:table_info(host, size). - -%% 获取查询的过滤条件 -match_spec(Specs) when is_list(Specs) -> - MatchHead = #host{model = '$1', cell_id = '$2', _ = '_'}, - Guard = guard(Specs), - Result = ['$_'], - {MatchHead, Guard, Result}. - -guard(Specs) when is_list(Specs) -> - guard(Specs, []). -guard([], Guard) -> - Guard; -guard([{model, Model}|Tail], Guard) when Model =/= <<"">> -> - guard(Tail, [{'=:=', '$1', Model}|Guard]); -guard([{cell, CellId}|Tail], Guard) when CellId > 0 -> - guard(Tail, [{'=:=', '$2', CellId}|Guard]); -guard([_|Tail], Guard) -> - guard(Tail, Guard). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% helper methods -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -sort(Terminals) when is_list(Terminals) -> - lists:sort(fun(#terminal{terminal_id = Id0}, #terminal{terminal_id = Id1}) -> Id0 < Id1 end, Terminals). - -to_map(#terminal{terminal_id = TerminalId, host_id = HostId, serial_number = SerialNumber, name = Name, code = Code, access_protocol = AccessProtocol, - product_id = ProductId, vendor_id = VendorId, model = Model, cell_id = CellId, status = Status, update_ts = UpdateTs}) -> - #{ - <<"terminal_id">> => TerminalId, - <<"host_id">> => HostId, - <<"serial_number">> => SerialNumber, - <<"name">> => Name, - <<"code">> => Code, - <<"access_protocol">> => AccessProtocol, - <<"product_id">> => ProductId, - <<"vendor_id">> => VendorId, - <<"model">> => Model, - <<"cell_id">> => CellId, - <<"status">> => Status, - <<"update_ts">> => UpdateTs - }. diff --git a/apps/iot/src/mysql_client.erl b/apps/iot/src/mysql_client.erl new file mode 100644 index 0000000..8e7770e --- /dev/null +++ b/apps/iot/src/mysql_client.erl @@ -0,0 +1,178 @@ +%%%------------------------------------------------------------------- +%%% @author aresei +%%% @copyright (C) 2018, +%%% @doc +%%% +%%% @end +%%% Created : 29. 九月 2018 17:01 +%%%------------------------------------------------------------------- +-module(mysql_client). +-author("aresei"). + +-define(POOL_NAME, mysql_pool). + +%% API +-export([get_row/1, get_row/2, get_all/1, get_all/2]). +-export([update/3, update_by/1, update_by/2, insert/3]). +-export([test/0]). + +test() -> + + Result = get_row(<<"select * from Carport limit 10">>), + lager:info("[mysql_client] the result is: ~p", [Result]), + + Result1 = get_all(<<"select * from Carport limit 10">>), + lager:info("[mysql_client] the result is: ~p", [Result1]), + + InsertResult = insert(<<"CarInfo">>, [{<<"owner_name">>, <<"anlicheng">>}], false), + lager:info("[mysql_client] the insert id is: ~p", [InsertResult]), + + UpdateResult = update_by(<<"update CarInfo set owner_name = AiYaLing123 where car_id = ? limit 1">>, [1]), + + lager:info("[mysql_client] the update result is: ~p", [UpdateResult]), + + ok. + +%% 从数据库中查找一行记录 +-spec get_row(Sql::binary()) -> {ok, Record::map()} | undefined. +get_row(Sql) when is_binary(Sql) -> + lager:debug("[mysql_client] the get_row sql is: ~p", [Sql]), + poolboy:transaction(?POOL_NAME, fun(ConnPid) -> + case mysql:query(ConnPid, Sql) of + {ok, Names, [Row | _]} -> + {ok, maps:from_list(lists:zip(Names, Row))}; + {ok, _, []} -> + undefined; + Error -> + lager:warning("[mysql_client] get error: ~p", [Error]), + undefined + end + end). + +-spec get_row(Sql::binary(), Params::list()) -> {ok, Record::map()} | undefined. +get_row(Sql, Params) when is_binary(Sql), is_list(Params) -> + lager:debug("[mysql_client] the get_row sql is: ~p, params: ~p", [Sql, Params]), + poolboy:transaction(?POOL_NAME, fun(ConnPid) -> + case mysql:query(ConnPid, Sql, Params) of + {ok, Names, [Row | _]} -> + {ok, maps:from_list(lists:zip(Names, Row))}; + {ok, _, []} -> + undefined; + Error -> + lager:warning("[mysql_client] get error: ~p", [Error]), + undefined + end + end). + +-spec get_all(Sql::binary()) -> {ok, Rows::list()} | {error, Reason :: any()}. +get_all(Sql) when is_binary(Sql) -> + lager:debug("[mysql_client] the get_all sql is: ~p", [Sql]), + poolboy:transaction(?POOL_NAME, fun(ConnPid) -> + case mysql:query(ConnPid, Sql) of + {ok, Names, Rows} -> + {ok, lists:map(fun(Row) -> maps:from_list(lists:zip(Names, Row)) end, Rows)}; + Error -> + lager:warning("[mysql_client] get error: ~p", [Error]), + Error + end + end). + +-spec get_all(Sql::binary(), Params::list()) -> {ok, Rows::list()} | {error, Reason::any()}. +get_all(Sql, Params) when is_binary(Sql), is_list(Params) -> + lager:debug("[mysql_client] the get_all sql is: ~p, params: ~p", [Sql, Params]), + poolboy:transaction(?POOL_NAME, fun(ConnPid) -> + case mysql:query(ConnPid, Sql, Params) of + {ok, Names, Rows} -> + {ok, lists:map(fun(Row) -> maps:from_list(lists:zip(Names, Row)) end, Rows)}; + Error -> + lager:warning("[mysql_client] get error: ~p", [Error]), + {ok, []} + end + end). + +-spec insert(Table :: binary(), Fields :: map() | list(), boolean()) -> ok | {ok, InsertId :: integer()} | {error, Reason :: any()}. +insert(Table, Fields, FetchInsertId) when is_binary(Table), is_map(Fields), is_boolean(FetchInsertId) -> + insert(Table, maps:to_list(Fields), FetchInsertId); +insert(Table, Fields, FetchInsertId) when is_binary(Table), is_list(Fields), is_boolean(FetchInsertId) -> + {Keys, Values} = kvs(Fields), + + FieldSql = iolist_to_binary(lists:join(<<", ">>, Keys)), + Placeholders = lists:duplicate(length(Keys), <<"?">>), + ValuesPlaceholder = iolist_to_binary(lists:join(<<", ">>, Placeholders)), + + Sql = <<"INSERT INTO ", Table/binary, "(", FieldSql/binary, ") VALUES(", ValuesPlaceholder/binary, ")">>, + lager:debug("[mysql_client] the insert sql is: ~p, params: ~p", [Sql, Values]), + poolboy:transaction(?POOL_NAME, fun(ConnPid) -> + case mysql:query(ConnPid, Sql, Values) of + ok -> + case FetchInsertId of + true -> + InsertId = mysql:insert_id(ConnPid), + {ok, InsertId}; + false -> + ok + end; + Error -> + Error + end + end). + +update_by(UpdateSql) when is_binary(UpdateSql) -> + lager:debug("[mysql_client] updateBySql sql: ~p", [UpdateSql]), + poolboy:transaction(?POOL_NAME, fun(ConnPid) -> + case mysql:query(ConnPid, UpdateSql) of + ok -> + AffectedRows = mysql:affected_rows(ConnPid), + {ok, AffectedRows}; + Error -> + Error + end + end). + +-spec update_by(UpdateSql :: binary(), Params :: list()) -> {ok, AffectedRows :: integer()} | {error, Reason :: any()}. +update_by(UpdateSql, Params) when is_binary(UpdateSql) -> + lager:debug("[mysql_client] updateBySql sql: ~p, params: ~p", [UpdateSql, Params]), + poolboy:transaction(?POOL_NAME, fun(ConnPid) -> + case mysql:query(ConnPid, UpdateSql, Params) of + ok -> + AffectedRows = mysql:affected_rows(ConnPid), + {ok, AffectedRows}; + Error -> + Error + end + end). + +-spec update(Sql :: binary(), Fields :: map(), WhereFields :: map()) -> {ok, AffectedRows::integer()} | {error, Reason::any()}. +update(Table, Fields, WhereFields) when is_binary(Table), is_map(Fields), is_map(WhereFields) -> + %% 拼接set + {SetKeys, SetVals} = kvs(Fields), + SetKeys1 = lists:map(fun(K) when is_binary(K) -> <<"`", K/binary, "` = ?">> end, SetKeys), + SetSql = iolist_to_binary(lists:join(<<", ">>, SetKeys1)), + + %% 拼接where + {WhereKeys, WhereVals} = kvs(WhereFields), + WhereKeys1 = lists:map(fun(K) when is_binary(K) -> <<"`", K/binary, "` = ?">> end, WhereKeys), + WhereSql = iolist_to_binary(lists:join(<<" AND ">>, WhereKeys1)), + + Params = SetVals ++ WhereVals, + + Sql = <<"UPDATE ", Table/binary, " SET ", SetSql/binary, " WHERE ", WhereSql/binary>>, + lager:debug("[mysql_client] update sql is: ~p, params: ~p", [Sql, Params]), + + poolboy:transaction(?POOL_NAME, fun(ConnPid) -> + case mysql:query(ConnPid, Sql, Params) of + ok -> + AffectedRows = mysql:affected_rows(ConnPid), + {ok, AffectedRows}; + Error -> + lager:error("[mysql_client] update sql: ~p, params: ~p, get a error: ~p", [Sql, Params, Error]), + Error + end + end). + +-spec kvs(Fields :: map() | list()) -> {Keys :: list(), Values :: list()}. +kvs(Fields) when is_map(Fields) -> + kvs(maps:to_list(Fields)); +kvs(Fields) when is_list(Fields) -> + {Keys0, Values0} = lists:foldl(fun({K, V}, {Acc0, Acc1}) -> {[K|Acc0], [V|Acc1]} end, {[], []}, Fields), + {lists:reverse(Keys0), lists:reverse(Values0)}. \ No newline at end of file diff --git a/config/sys.config b/config/sys.config index 757d194..be0c641 100644 --- a/config/sys.config +++ b/config/sys.config @@ -1,7 +1,7 @@ [ {iot, [ {http_server, [ - {port, 8080}, + {port, 18080}, {acceptors, 500}, {max_connections, 10240}, {backlog, 10240} @@ -9,17 +9,44 @@ %% 目标服务器地址 {emqx_server, [ - {host, {103, 113, 157, 7}}, + {host, {39, 98, 184, 67}}, {port, 1883}, {tcp_opts, []}, {username, "test"}, {password, "test1234"}, {keepalive, 86400}, {retry_interval, 5} + ]}, + + {pools, [ + %% mysql连接池配置 + {mysql_pool, + [{size, 10}, {max_overflow, 20}, {worker_module, mysql}], + [ + {host, {39, 98, 184, 67}}, + {port, 3306}, + {user, "nannonguser"}, + {password, "nannong@Fe7w"}, + {database, "nannong"}, + {queries, [<<"set names utf8">>]} + ] + }, + + %% influxdb数据库配置 + {influx_pool, + [{size, 100}, {max_overflow, 200}, {worker_module, influx_client}], + [ + {host, "39.98.184.67"}, + {port, 8086}, + {token, <<"HcnjcIaoPWGxnDOHpW2HV-xBsZSyqtC2UR98SO8LNgNhczuNyU6PQ4s1Blfz7vKy-CB_A-Kyjzed_EX7Ak2RAA==">>} + ] + } + ]} ]}, + %% 系统日志配置,系统日志为lager, 支持日志按日期自动分割 {lager, [ {colored, true}, diff --git a/docs/host-mqtt-jiaohu.md b/docs/host-mqtt-jiaohu.md new file mode 100644 index 0000000..e0d201a --- /dev/null +++ b/docs/host-mqtt-jiaohu.md @@ -0,0 +1,299 @@ +# 交互流程数据格式说明 + +## 主题说明 + +### system/upstream qos = 1 + 用于主机注册到平台 + +### host/upstream/$uuid qos = 1 + 用于主机向平台汇报数据 +### host/downstream/$uuid qos = 2 + 用于平台向主机下发所有的指令信息 + +## 需要注意的点 + 1. 主机的uuid是采用主动发现的机制,因此host主机在启动时需要先subscribe主题: host/downstream/${uuid} + +## register主机注册会话 + +1. 请求: 设备端向主题`system/upstream`发送注册信息 + +```json +{ + "method": "register", + "params": { + "uuid": "$uuid", + "os_version": "系统版本", + } +} +``` + +2. 响应: `host/downstream/$uuid` + +t = 0 + +```json + { + "code": 0 | 1, // 0表示失败,1:表示成功 + // 内容 + "m": "$bytes" +} +``` + +## create_session主机注册会话 + +register的时候,设备端向主题`host/upstream/${uuid}`发送注册信息,由于topic里面已经携带了主机标识,因此body里面不需要$uuid, 该信息格式如下: + +```json +{ + "method": "create_session", + "params": { + "pub_key": "该客户端自己生成的公钥" + } +} +``` + +其中,`pub_key`表示客户端自身的公钥,该公钥用于与服务端交换aes密钥。 + +需要注意,这里的交互逻辑有变更(topic: host/downstream/$uuid) +1. 当主机未激活时,返回: #{<<"a">> => false, <<"aes">> => <<"">>}, +2. 当主机已经激活时,返回: #{<<"a">> => true, <<"aes">> => Aes} +3. 当主机超过一定的时间没有收到任何返回的时候,需要重试执行create_session操作 + +t = 10 + +```json +{ + "a": true/false, // 表示是授权还是拒绝授权 + "aes": "", // 如果a字段为true,表示服务端允许授权,该aes为一个加密字符串,以后的通信,服务端和设备端都使用该aes密钥进行加解密。 +} +``` + +## 主机授权消息格式 + +t = 8 + +```json + +{ + "auth": true/false, // true表示授权,此时aes的值不为空;false表示取消授权 + "aes": "", + "reply": { + "topic": "", // 主机端操作成功后需要回写的topic名称 + "assoc": "" // 关联的信息,回写的时候需要带上 + } +} + +// 回写的消息格式, 其中topic为: reply.topic的值 + +{ + "code": 0 | 1, // 0表示操作失败,1表示成功 + messge: "", + assoc: "下发给你值" +} + +``` + +## 数据上传结构 +设备端在采集到数据之后,会向topic为`host/upstream/$uuid`发送消息,消息格式如下: + +```json +{ + "methods": "data", + "params": "$bytes" +} +``` + +其中`params`代表具体的采集信息,这部分数据通过与服务端交互商量的: base64:encode(aes加密),加密之前是一个列表,列表里面的数据格式如下: + +```json: +[ + { + "service_name": "从该设备端的哪个服务采集的数据", + "node_name": "节点名称", insert到influxdb的时候就是表名 + "at": int, 精确到毫秒 + // 该微服务采集的数据,是一个包含map的列表类型,map的内容可以由微服务自己指定 + // 目前一般的格式是"metric-name": $value样式的数据 + "fields": [ + { + "name1": "test" + "name2": 124, + "name3": false, + "device_id": $uuid 非设备产生的device_id为空 + } + ], + // 微服务自身可以生成tag,用于微服务指定自己的一些性质,目前使用得不多,以后可以扩展, + // 是一个map[string]string类型的数据 + "tags": { + "tag1": "value1", + "tag2", "value2" + } + // todo 在insert数据到influxdb的时候需要增加service_name + host_uuid + } +] +``` + +## 命令下发结构 +命令下发,用于服务端或者其他的系统,通过调用接口,向设备端发送消息,设备端会监听`host/downstream/$uuid`的消息。 +服务端在发送之前,应先用该客户端的aes密钥进行加密,将加密之后的二进制数据发送到该topic。 +TODO 命令下发是需要增加当前的时间戳,host主机用来协调任务的一致性 + +加密前的消息结构如下: +// 消息类型,目前支持四种消息类型: +// 1代表参数下发,就是向该设备端的微服务发送消息,该消息会辗转发送给微服务进行处理,比如,设置modbus微服务的波特率等消息 +// 2代表采集向下发,比如,设置某个设备短上的modbus微服务采集某个地址的数据 +// 3代表下发微服务文件。 +// 4代表下发数据流图,这个指令用于设置设备端上各个微服务之间的逐句流转。 + +占用数据项的第一个字节 + +"t": 1|2|3|4, + +```json +{ + // 针对不同的命令类型,这个字段里的`to`和`m`数据有所不同,具体在下面的小节描述 + // 任务id,服务端在下发数据的时候,需要生成一个唯一的uuid, + // 用于标识一个任务 + "t_id": "任务id", + // 表示发给哪个微服务,这里是服务的标识,即服务名称 + "to": "", + // 命令执行的超时时间,单位为秒 + "t": 10, + // 实际内容 + "m": "$bytes", +} + +``` + +下面介绍几种下发类型: + +### 参数下发的结构 +对于参数下发,下发内容中的m为一个`map[string]interface{}`结构,用于向某个微服务发送参数,具体参数内容由微服务的参数配置提供。 + +### 采集项下发的结构 +采集项下发时,下发内容中的m为一个`[]map[string]interface{}`结构的列表,每一个条目是一个采集项内容,具体采集向内容由微服务的采集项配置提供。 + +### 微服务下发的结构 +在微服务下发中,`to`字段会被忽略,可以填写空字符串,而m字段为json化之后的数据,json化之前结构如下: + +```json +{ + "f": "微服务名", + "v": "微服务版本" + "k": "微服务下载的token" + "md5": "微服务的md5值,用于验证下载完整性" + // ms表示是微服务,config表示配置文件,self表示efka的新版本 + "t": "ms|config|self" + // o代表oldversion,老版本,如果t为ms,且o不为空字符串, + // 则表示要升级微服务版本,老版本的内容会被删除和替换。 + "o": "old-version" +} +``` + +## ping结构 +ping结构会上传到topic为"host/upstream/$uuid", 结构体由msgpack格式编码, ping结构每隔20秒发送一次, 其中params的数据格式为: base64:encode(aes("加密")) + +```json +{ + "method": "ping", + "params": { + // 硬件信息,目前有剩余内存,剩余磁盘和cpu负载 + // 剩余内存,单位为mb + + "memory": { + "total": 1024, // 内存数,mb + "used": "$int" // 剩余内存数 + }, + "disk": { + "total": 1024, // 硬盘容量GB + "used": "$int" // 剩余硬盘内容GB + }, + + "cpu_load": $float, // 浮点数 + "cpu_temperature": $float // 稳定信息 + "cpu_core": $int, + + "boot_time": 2000, // 启动时间 + "efka_version": "1.0.0", // 客户端版本 + "kernel_arch": "arm64", // 客户端硬件架构 + "province": "", // 所在省 + "city": "", // 所在市 + "adcode": 100, // 所在城市的编号 + "ips": [ + "ip地址1", + "ip地址2" + ], + // 接口信息 + "interfaces": [ + { + "status": 0|1, "接口状态,0离线,1在线" + "name": "接口名称", + "desc": "接口描述", + } + ] + } +} +``` + +## 命令下发步骤上报结构 +在任务下发之后,设备端会根据命令到哪个环节,进行步骤上报,上报的消息写入到topic为`host/upstream/$uuid`,上报结构如下: + +```json +{ + "method": "feedback_step", + "params": { + "task_id": "任务的task id", + // sc为step code,具体地: + // 0代表该任务开始了,服务端创建该任务之后,是这个代码 + // 1代表任务被分发了,服务端向nats(mqtt)发送消息之后,是这个代码 + // 2代表任务被设备端接收到了 + // 3代表该任务已经被发送给微服务进行处理了 + // 4代表该任务已经被微服务收到了,微服务正在处理 + // 5代表任务已经完成,微服务已经处理完成。 + "code": $int + } +} +``` + +有了这个步骤,最后任务超时等情况,就可以知道任务卡在哪里。 + +## 命令下发结果上报结构 +任务在微服务处理完成之后,设备端会向服务端反馈任务执行的结果。该结果会写入`host/upstream/$uuid`,上报的结构如下: + +```json +{ + "method": "feedback_result", + "params": { + "task_id": "任务id", + // unix nano类型 + "time": $int, + // 返回的结果码,0代表成功,其他代表出错 + "code": $int, + "reason": "任务执行的结果", + "error": "错误消息,当c为非0时,这个字段会表示出错消息", + // 返回任务类型,1表示任务是微服务下发,0代表是命令下发 + "type": 0 | 1, + } +} +``` + +## inform结构 +inform用于客户端主动通知服务端本地事情的发生,比如,自身的某个微服务下线了,就会发送一个inform信息给服务端。 topic: host/upstream/$uuid + +inform信息会发送给``, 结构如下: + +```json +{ + "method": "inform", + "params": { + "at": $int64, + // 微服务信息 + "services": [{ + "name": "微服务名称", + "version": "微服务版本", + "version_copy": "微服务副本", + // 微服务是否在线,0表示离线,1表示在线 + "status": 0|1 + }] + } +} +``` diff --git a/docs/message0305.md b/docs/message0305.md deleted file mode 100644 index e68f47e..0000000 --- a/docs/message0305.md +++ /dev/null @@ -1,233 +0,0 @@ -# 交互流程数据格式说明 - -## register -register的时候,设备端向`server.register`发送注册信息,该信息格式如下: - -```json -{ - "c_id": "client_id", - "r": "该客户端自己生成的公钥", - "m": $object, -} -``` - -其中,`c_id`代表客户端本身的id,用于唯一标识一个客户端,`r`表示客户端自身的公钥,该公钥用于与服务端交换aes密钥。 - -`m`是设备端的一些固有属性,目前结构如下: - -```json -{ - "cpu_core": 4, // 设备cpu核心数 - "memory": 1024, // 内存数,MB - "disk": 1024, // 硬盘容量,GB - "boot_time": 2000, // 启动时间 - "efka_version": "1.0.0", // 客户端版本 - "kernel_arch": "arm64", // 客户端硬件架构 - "province": "", // 所在省 - "city": "", // 所在市 - "adcode": 100, // 所在城市的编号 - "IPv4 1": "ip地址1", - "IPv4 2": "ip地址2", - ... -} -``` - -在服务端,在第一次收到该设备端的注册请求之后,会先将该信息保存下来,当用户在操作界面上对该设备进行授权之后,服务端会生成一个AES密钥,用该设备端的公钥进行加密,然后向`client.auth.$clientid`写入授权信息,客户端收到之后,可以得到本身的授权情况。服务端用RSA公钥加密的结构如下: - -```json -{ - "a": true/false, // 表示是授权还是拒绝授权 - "aes": "", // 如果a字段为true,表示服务端允许授权,该aes为一个加密字符串,以后的通信,服务端和设备端都使用该aes密钥进行加解密。 - "reply": "client.reply.$uuid" // 服务端设置的一个客户端回复的topic,如果服务端在5秒之内没有回复,就认为这个动作失败,服务端就会反馈给操作界面的用户。 -} -``` - -## 数据上传结构 -设备端在采集到数据之后,会向`server.data`发送消息,消息格式如下: - -```json -{ - "c_id": "设备端ID", - "d": $bytes -} -``` - -其中`c_id`是自身的`clientid`,`d`代表具体的采集信息,这部分数据通过与服务端交互商量的aes加密,加密之前是一个列表,列表里面的数据格式如下: - -```json: -[ - { - "client_id": "设备端ID", - "service_name": "从该设备端的哪个服务采集的数据", - "time": $time, // 采集时间,unixnano的int64 - // 该微服务采集的数据,是一个包含map的列表类型,map的内容可以由微服务自己指定 - // 目前一般的格式是"metric-name": $value样式的数据 - "data": [ - { - "name1": "test" - "name2": 124, - "name3": false - } - ], - // 微服务自身可以生成tag,用于微服务指定自己的一些性质,目前使用得不多,以后可以扩展, - // 是一个map[string]string类型的数据 - "tag": { - "tag1": "value1", - "tag2", "value2" - } - } -] -``` - -## 命令下发结构 -命令下发,用于服务端或者其他的系统,通过调用接口,向设备端发送消息,设备端会监听`clients.cmd.$id`的消息。 - -服务端在发送之前,应先用该客户端的aes密钥进行加密,将加密之后的二进制数据发送到该topic。 - -加密前的消息结构如下: - -```json -{ - // 消息类型,目前支持四种消息类型: - // 1代表参数下发,就是向该设备端的微服务发送消息,该消息会辗转发送给微服务进行处理,比如,设置modbus微服务的波特率等消息 - // 2代表采集向下发,比如,设置某个设备短上的modbus微服务采集某个地址的数据 - // 3代表下发微服务文件。 - // 4代表下发数据流图,这个指令用于设置设备端上各个微服务之间的逐句流转。 - "t": 1|2|3|4, - // 针对不同的命令类型,这个字段里的`to`和`m`数据有所不同,具体在下面的小节描述 - "b": { - // 任务id,服务端在下发数据的时候,需要生成一个唯一的uuid, - // 用于标识一个任务 - "t_id": "任务id", - // 表示发给哪个微服务,这里是服务的标识,即服务名称 - "to": "", - // 命令执行的超时时间,单位为秒 - "t": 10, - // 实际内容 - "m": $bytes - } -} -``` - -下面介绍几种下发类型: - -### 参数下发的结构 -对于参数下发,下发内容中的m为一个`map[string]interface{}`结构,用于向某个微服务发送参数,具体参数内容由微服务的参数配置提供。 - -### 采集项下发的结构 -采集项下发时,下发内容中的m为一个`[]map[string]interface{}`结构的列表,每一个条目是一个采集项内容,具体采集向内容由微服务的采集项配置提供。 - - -### 微服务下发的结构 -在微服务下发中,`to`字段会被忽略,可以填写空字符串,而m字段为json化之后的数据,json化之前结构如下: - -```json -{ - "f": "微服务名", - "v": "微服务版本" - "k": "微服务下载的token" - "md5": "微服务的md5值,用于验证下载完整性" - // ms表示是微服务,config表示配置文件,self表示efka的新版本 - "t": "ms|config|self" - // o代表oldversion,老版本,如果t为ms,且o不为空字符串, - // 则表示要升级微服务版本,老版本的内容会被删除和替换。 - "o": "old-version" - -} -``` - -## ping结构 -ping结构会上传到"server.ping", 结构体由msgpack格式编码, ping结构每隔20秒发送一次 - -```json -{ - "c": "client_id", - // 这个ping上传的时间 - "at": $int64, - // 硬件信息,目前有剩余内存,剩余磁盘和cpu负载 - "h": { - // 剩余内存,单位为MB - "fm": $int - // 剩余磁盘,单位为MB - "fd": $int, - // cpu负载,是一个浮点数 - "cp": $float - } -} -``` - -## 命令下发步骤上报结构 -在任务下发之后,设备端会根据命令到哪个环节,进行步骤上报,上报的消息写入到`server.step.feedback`,上报结构如下: - -```json -{ - "c_id": "设备端id", - "d": "aes加密后的二进制数据" -} -``` - -其中,`d`是具体数据经过aes加密后的二进制数据,加密前的结构如下: - -```json -{ - "t_id": "任务的task id", - - // sc为step code,具体地: - // 0代表该任务开始了,服务端创建该任务之后,是这个代码 - // 1代表任务被分发了,服务端向nats(mqtt)发送消息之后,是这个代码 - // 2代表任务被设备端接收到了 - // 3代表该任务已经被发送给微服务进行处理了 - // 4代表该任务已经被微服务收到了,微服务正在处理 - // 5代表任务已经完成,微服务已经处理完成。 - "sc": $int -} -``` - -有了这个步骤,最后任务超时等情况,就可以知道任务卡在哪里。 - -## 命令下发结果上报结构 -任务在微服务处理完成之后,设备端会向服务端反馈任务执行的结果。该结果会写入`server.result.feedback`,上报的结构如下: - -```json -{ - "c_id": "设备端id", - "d": "aes加密后的二进制数据" -} -``` - -其中,`d`是aes加密后的二进制数据,加密前的结构如下: - -```json -{ - "t_id": "任务id", - // unix nano类型 - "t": $int, - // 返回的结果码,0代表成功,其他代表出错 - "c": $int, - "r": "任务执行的结果", - "e": "错误消息,当c为非0时,这个字段会表示出错消息", - // 返回任务类型,1表示任务是微服务下发,0代表是命令下发 - "t": 0 | 1, -} -``` - -## inform结构 -inform用于客户端主动通知服务端本地事情的发生,比如,自身的某个微服务下线了,就会发送一个inform信息给服务端。 - -inform信息会发送给``, 结构如下: - -```json -{ - "c": "client_id", - // 这个inform上传的时间 - "at": $int64, - // 微服务信息 - "s": [{ - "n": "微服务名称", - "v": "微服务版本", - "c": "微服务副本", - // 微服务是否在线,0表示离线,1表示在线 - "s": 0|1 - }] -} -``` diff --git a/rebar.config b/rebar.config index e04e0d7..499fec7 100644 --- a/rebar.config +++ b/rebar.config @@ -5,6 +5,7 @@ {sync, ".*", {git, "https://github.com/rustyio/sync.git", {branch, "master"}}}, {cowboy, ".*", {git, "https://github.com/ninenines/cowboy.git", {tag, "2.5.0"}}}, {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"}}} diff --git a/rebar.lock b/rebar.lock index e5a63a0..4deee4f 100644 --- a/rebar.lock +++ b/rebar.lock @@ -34,6 +34,10 @@ 0}, {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1}, {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.2.0">>},1}, + {<<"mysql">>, + {git,"https://github.com/mysql-otp/mysql-otp", + {ref,"caf5ff96c677a8fe0ce6f4082bc036c8fd27dd62"}}, + 0}, {<<"parse_trans">>, {git,"https://github.com/uwiger/parse_trans", {ref,"6f3645afb43c7c57d61b54ef59aecab288ce1013"}},