%%%------------------------------------------------------------------- %%% @copyright (C) 2023, %%% @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]).