220 lines
8.9 KiB
Erlang
220 lines
8.9 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% @copyright (C) 2023, <COMPANY>
|
|
%%% @doc
|
|
%%%
|
|
%%% @end
|
|
%%% Created : 14. 8月 2023 11:40
|
|
%%%-------------------------------------------------------------------
|
|
-module(iot_device).
|
|
-author("aresei").
|
|
-include("iot.hrl").
|
|
|
|
-behaviour(gen_statem).
|
|
|
|
%% API
|
|
-export([get_name/1, get_pid/1]).
|
|
-export([start_link/2, is_activated/1, is_alive/1, change_status/2, reload/1, auth/2]).
|
|
|
|
%% gen_statem callbacks
|
|
-export([init/1, handle_event/4, terminate/3, code_change/4, callback_mode/0]).
|
|
|
|
%% 终端是否授权
|
|
-define(DEVICE_AUTH_DENIED, 0).
|
|
-define(DEVICE_AUTH_AUTHED, 1).
|
|
|
|
%% 状态
|
|
-define(STATE_DENIED, denied).
|
|
-define(STATE_ACTIVATED, activated).
|
|
|
|
-record(state, {
|
|
device_uuid :: binary(),
|
|
status = ?DEVICE_OFFLINE
|
|
}).
|
|
|
|
%%%===================================================================
|
|
%%% API
|
|
%%%===================================================================
|
|
|
|
-spec is_alive(DeviceUUID :: binary()) -> error | {ok, Pid :: pid()}.
|
|
is_alive(DeviceUUID) when is_binary(DeviceUUID) ->
|
|
case iot_device:get_pid(DeviceUUID) of
|
|
undefined ->
|
|
error;
|
|
DevicePid when is_pid(DevicePid) ->
|
|
case iot_device:is_activated(DevicePid) of
|
|
true ->
|
|
{ok, DevicePid};
|
|
false ->
|
|
error
|
|
end
|
|
end.
|
|
|
|
-spec get_pid(DeviceUUID :: binary()) -> Pid :: pid() | undefined.
|
|
get_pid(DeviceUUID) when is_binary(DeviceUUID) ->
|
|
whereis(get_name(DeviceUUID)).
|
|
|
|
-spec get_name(DeviceUUID :: binary()) -> atom().
|
|
get_name(DeviceUUID) when is_binary(DeviceUUID) ->
|
|
binary_to_atom(<<"iot_device:", DeviceUUID/binary>>).
|
|
|
|
-spec is_activated(Pid :: pid() | undefined) -> boolean().
|
|
is_activated(undefined) ->
|
|
false;
|
|
is_activated(Pid) when is_pid(Pid) ->
|
|
gen_statem:call(Pid, is_activated).
|
|
|
|
-spec change_status(Pid :: pid() | undefined, NewStatus :: integer()) -> no_return().
|
|
change_status(undefined, _) ->
|
|
ok;
|
|
change_status(Pid, NewStatus) when is_pid(Pid), is_integer(NewStatus) ->
|
|
gen_statem:cast(Pid, {change_status, NewStatus}).
|
|
|
|
-spec reload(Pid :: pid()) -> no_return().
|
|
reload(Pid) when is_pid(Pid) ->
|
|
gen_statem:cast(Pid, reload).
|
|
|
|
-spec auth(Pid :: pid(), Auth :: boolean()) -> no_return().
|
|
auth(Pid, Auth) when is_pid(Pid), is_boolean(Auth) ->
|
|
gen_statem:cast(Pid, {auth, Auth}).
|
|
|
|
%% @doc Creates a gen_statem process which calls Module:init/1 to
|
|
%% initialize. To ensure a synchronized start-up procedure, this
|
|
%% function does not return until Module:init/1 has returned.
|
|
start_link(Name, DeviceUUID) when is_atom(Name), is_binary(DeviceUUID) ->
|
|
gen_statem:start_link({local, Name}, ?MODULE, [DeviceUUID], []);
|
|
start_link(Name, DeviceInfo) when is_atom(Name), is_map(DeviceInfo) ->
|
|
gen_statem:start_link({local, Name}, ?MODULE, [DeviceInfo], []).
|
|
|
|
%%%===================================================================
|
|
%%% gen_statem callbacks
|
|
%%%===================================================================
|
|
|
|
%% @private
|
|
%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or
|
|
%% gen_statem:start_link/[3,4], this function is called by the new
|
|
%% process to initialize.
|
|
init([DeviceUUID]) when is_binary(DeviceUUID) ->
|
|
case device_bo:get_device_by_uuid(DeviceUUID) of
|
|
{ok, DeviceInfo} ->
|
|
init([DeviceInfo]);
|
|
undefined ->
|
|
lager:warning("[iot_device] device uuid: ~p, loaded from mysql failed", [DeviceUUID]),
|
|
ignore
|
|
end;
|
|
init([DeviceInfo = #{<<"device_uuid">> := DeviceUUID, <<"authorize_status">> := AuthorizeStatus, <<"status">> := Status}]) when is_map(DeviceInfo) ->
|
|
case AuthorizeStatus =:= ?DEVICE_AUTH_AUTHED of
|
|
true ->
|
|
{ok, ?STATE_ACTIVATED, #state{device_uuid = DeviceUUID, status = Status}};
|
|
false ->
|
|
{ok, ?STATE_DENIED, #state{device_uuid = DeviceUUID, status = Status}}
|
|
end.
|
|
|
|
%% @private
|
|
%% @doc This function is called by a gen_statem when it needs to find out
|
|
%% the callback mode of the callback module.
|
|
callback_mode() ->
|
|
handle_event_function.
|
|
|
|
%% @private
|
|
%% @doc If callback_mode is handle_event_function, then whenever a
|
|
%% gen_statem receives an event from call/2, cast/2, or as a normal
|
|
%% process message, this function is called.
|
|
|
|
%% 判断是否是激活状态
|
|
handle_event({call, From}, is_activated, StateName, State = #state{}) ->
|
|
{keep_state, State, [{reply, From, StateName =:= ?STATE_ACTIVATED}]};
|
|
|
|
%% 改变为在线状态,但是数据库中的状态已经是在线状态,忽略
|
|
handle_event(cast, {change_status, ?DEVICE_ONLINE}, _, State = #state{status = ?DEVICE_ONLINE}) ->
|
|
{keep_state, State};
|
|
%% 改变数据库的状态, 其他情况下执行次数都很少
|
|
handle_event(cast, {change_status, ?DEVICE_ONLINE}, _, State = #state{device_uuid = DeviceUUID}) ->
|
|
{ok, _} = device_bo:change_status(DeviceUUID, ?DEVICE_ONLINE),
|
|
report_event(DeviceUUID, ?DEVICE_ONLINE),
|
|
{keep_state, State#state{status = ?DEVICE_ONLINE}};
|
|
|
|
handle_event(cast, {change_status, ?DEVICE_OFFLINE}, _, State = #state{device_uuid = DeviceUUID}) ->
|
|
{ok, #{<<"status">> := Status}} = device_bo:get_device_by_uuid(DeviceUUID),
|
|
case Status of
|
|
?DEVICE_NOT_JOINED ->
|
|
lager:debug("[iot_device] device: ~p, device_maybe_offline, not joined, can not change to offline", [DeviceUUID]),
|
|
{keep_state, State#state{status = ?DEVICE_NOT_JOINED}};
|
|
?DEVICE_OFFLINE ->
|
|
lager:debug("[iot_device] device: ~p, device_maybe_offline, is offline, do nothing", [DeviceUUID]),
|
|
{keep_state, State#state{status = ?DEVICE_OFFLINE}};
|
|
?DEVICE_ONLINE ->
|
|
{ok, _} = device_bo:change_status(DeviceUUID, ?DEVICE_OFFLINE),
|
|
report_event(DeviceUUID, ?DEVICE_OFFLINE),
|
|
{keep_state, State#state{status = ?DEVICE_OFFLINE}}
|
|
end;
|
|
|
|
%% 重新加载数据库数据
|
|
handle_event(cast, reload, _, State = #state{device_uuid = DeviceUUID}) ->
|
|
lager:debug("[iot_device] will reload: ~p", [DeviceUUID]),
|
|
case device_bo:get_device_by_uuid(DeviceUUID) of
|
|
{ok, #{<<"authorize_status">> := AuthorizeStatus, <<"status">> := Status}} ->
|
|
case AuthorizeStatus =:= ?DEVICE_AUTH_AUTHED of
|
|
true ->
|
|
{next_state, ?STATE_ACTIVATED, State#state{status = Status}};
|
|
false ->
|
|
{next_state, ?STATE_DENIED, State#state{status = Status}}
|
|
end;
|
|
undefined ->
|
|
lager:warning("[iot_device] device uuid: ~p, loaded from mysql failed", [DeviceUUID]),
|
|
{stop, normal, State}
|
|
end;
|
|
|
|
%% 处理授权
|
|
handle_event(cast, {auth, Auth}, StateName, State = #state{device_uuid = DeviceUUID}) ->
|
|
case {StateName, Auth} of
|
|
{?STATE_DENIED, false} ->
|
|
lager:debug("[iot_device] device_uuid: ~p, auth: false, will keep state_name: ~p", [DeviceUUID, ?STATE_DENIED]),
|
|
{keep_state, State};
|
|
{?STATE_DENIED, true} ->
|
|
{next_state, ?STATE_ACTIVATED, State};
|
|
|
|
{?STATE_ACTIVATED, false} ->
|
|
lager:debug("[iot_device] device_uuid: ~p, auth: false, state_name from: ~p, to: ~p", [DeviceUUID, ?STATE_ACTIVATED, ?STATE_DENIED]),
|
|
{next_state, ?STATE_DENIED, State};
|
|
{?STATE_ACTIVATED, true} ->
|
|
lager:debug("[iot_device] device_uuid: ~p, auth: true, will keep state_name: ~p", [DeviceUUID, ?STATE_ACTIVATED]),
|
|
{keep_state, State}
|
|
end.
|
|
|
|
%% @private
|
|
%% @doc This function is called by a gen_statem when it is about to
|
|
%% terminate. It should be the opposite of Module:init/1 and do any
|
|
%% necessary cleaning up. When it returns, the gen_statem terminates with
|
|
%% Reason. The return value is ignored.
|
|
terminate(Reason, StateName, #state{device_uuid = DeviceUUID}) ->
|
|
lager:notice("[iot_device] device_uuid: ~p, state_name: ~p, terminate with reason: ~p", [DeviceUUID, StateName, Reason]),
|
|
ok.
|
|
|
|
%% @private
|
|
%% @doc Convert process state when code is changed
|
|
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
|
|
{ok, StateName, State}.
|
|
|
|
%%%===================================================================
|
|
%%% Internal functions
|
|
%%%===================================================================
|
|
|
|
-spec report_event(DeviceUUID :: binary(), NewStatus :: integer()) -> no_return().
|
|
report_event(DeviceUUID, NewStatus) when is_binary(DeviceUUID), is_integer(NewStatus) ->
|
|
TextMap = #{
|
|
0 => <<"离线"/utf8>>,
|
|
1 => <<"在线"/utf8>>
|
|
},
|
|
%% 设备的状态信息上报给中电
|
|
Timestamp = iot_util:timestamp_of_seconds(),
|
|
FieldsList = [#{
|
|
<<"key">> => <<"device_status">>,
|
|
<<"value">> => NewStatus,
|
|
<<"value_text">> => maps:get(NewStatus, TextMap),
|
|
<<"unit">> => 0,
|
|
<<"type">> => <<"DI">>,
|
|
<<"name">> => <<"设备状态"/utf8>>,
|
|
<<"timestamp">> => Timestamp
|
|
}],
|
|
iot_router:route_uuid(DeviceUUID, FieldsList, Timestamp),
|
|
lager:debug("[iot_device] device_uuid: ~p, route fields: ~p", [DeviceUUID, FieldsList]). |