fix
This commit is contained in:
parent
f8fd7d0c43
commit
92ea32568b
@ -3,6 +3,9 @@ modbus
|
||||
|
||||
An OTP application
|
||||
|
||||
## 编译serial可执行文件
|
||||
gcc serial.c -o serial
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
|
||||
@ -9,11 +9,11 @@
|
||||
-author("anlicheng").
|
||||
|
||||
-record(modbus_transport_rtu, {
|
||||
port :: string(),
|
||||
port :: binary(),
|
||||
baudrate :: integer(),
|
||||
parity :: any(),
|
||||
parity = 0 :: integer(), %% 0: none; 1: odd; 2: even
|
||||
stopbits :: integer(),
|
||||
timeout = 0 :: integer()
|
||||
timeout = 0 :: integer() %% 将配置的: 1s, 100ms等格式全部转换成毫秒
|
||||
}).
|
||||
|
||||
-record(modbus_transport_tcp, {
|
||||
@ -45,11 +45,18 @@
|
||||
retry_timeout :: integer(),
|
||||
|
||||
%% 数据定义
|
||||
variables = #{} :: map(),
|
||||
metrics = #{} :: map(),
|
||||
|
||||
controls = #{} :: map()
|
||||
}).
|
||||
|
||||
-record(modbus_metric, {
|
||||
name,
|
||||
address,
|
||||
type,
|
||||
unit
|
||||
}).
|
||||
|
||||
-record(modbus_processor, {
|
||||
name :: string(),
|
||||
input :: string(),
|
||||
|
||||
99
apps/modbus/src/modbus_device.erl
Normal file
99
apps/modbus/src/modbus_device.erl
Normal file
@ -0,0 +1,99 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author anlicheng
|
||||
%%% @copyright (C) 2025, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 20. 6月 2025 15:19
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(modbus_device).
|
||||
-author("anlicheng").
|
||||
-include("modbus_ast.hrl").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/1]).
|
||||
|
||||
%% gen_server callbacks
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
-record(state, {
|
||||
device :: #modbus_device{}
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
%% @doc Spawns the server and registers the local name (unique)
|
||||
-spec(start_link(Device :: #modbus_device{}) ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link(Device = #modbus_device{}) ->
|
||||
gen_server:start_link(?MODULE, [], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% gen_server callbacks
|
||||
%%%===================================================================
|
||||
|
||||
%% @private
|
||||
%% @doc Initializes the server
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([Device]) ->
|
||||
|
||||
{ok, #state{device = Device}}.
|
||||
|
||||
%% @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
|
||||
%%%===================================================================
|
||||
128
apps/modbus/src/modbus_logger.erl
Normal file
128
apps/modbus/src/modbus_logger.erl
Normal file
@ -0,0 +1,128 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @author aresei
|
||||
%%% @copyright (C) 2023, <COMPANY>
|
||||
%%% @doc
|
||||
%%%
|
||||
%%% @end
|
||||
%%% Created : 07. 9月 2023 17:07
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(modbus_logger).
|
||||
-author("aresei").
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% API
|
||||
-export([start_link/1, write/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, {
|
||||
file_name :: string(),
|
||||
file :: file:io_device()
|
||||
}).
|
||||
|
||||
%%%===================================================================
|
||||
%%% API
|
||||
%%%===================================================================
|
||||
|
||||
-spec write(Pid :: pid(), Data :: any()) -> no_return().
|
||||
write(Pid, Data) when is_pid(Pid) ->
|
||||
gen_server:cast(Pid, {write, Data}).
|
||||
|
||||
-spec(start_link(FileName :: string()) ->
|
||||
{ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
|
||||
start_link(FileName) when is_list(FileName) ->
|
||||
gen_server:start_link(?MODULE, [FileName], []).
|
||||
|
||||
%%%===================================================================
|
||||
%%% 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([FileName]) ->
|
||||
ensure_dir(filename:dirname(FileName)),
|
||||
{ok, IoDevice} = file:open(FileName, [append, binary]),
|
||||
|
||||
{ok, #state{file = IoDevice, file_name = FileName}}.
|
||||
|
||||
%% @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, Data}, State = #state{file = IoDevice}) ->
|
||||
Line = iolist_to_binary([time_prefix(), <<" ">>, format(Data), <<$\n>>]),
|
||||
ok = file:write(IoDevice, Line),
|
||||
{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
|
||||
%%%===================================================================
|
||||
|
||||
-spec format(Data :: binary() | list()) -> binary().
|
||||
format(Data) when is_binary(Data) ->
|
||||
iolist_to_binary(Data);
|
||||
format(Items) when is_list(Items) ->
|
||||
iolist_to_binary(lists:join(<<"\t">>, Items)).
|
||||
|
||||
-spec time_prefix() -> binary().
|
||||
time_prefix() ->
|
||||
{{Y, M, D}, {H, I, S}} = calendar:local_time(),
|
||||
iolist_to_binary(io_lib:format("[~b-~2..0b-~2..0b ~2..0b:~2..0b:~2..0b]", [Y, M, D, H, I, S])).
|
||||
|
||||
-spec ensure_dir(RootDir :: string()) -> no_return().
|
||||
ensure_dir(RootDir) when is_list(RootDir) ->
|
||||
case filelib:is_dir(RootDir) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
ok = file:make_dir(RootDir)
|
||||
end.
|
||||
@ -54,8 +54,13 @@ parse(Input) when is_binary(Input) ->
|
||||
end
|
||||
end, Trees),
|
||||
|
||||
AST = #ast{modbus = Modbus, devices = Devices, processors = Processors, alarms = Alarms },
|
||||
{ok, AST}
|
||||
case length(Modbus) == 1 of
|
||||
true ->
|
||||
AST = #ast{modbus = hd(Modbus), devices = Devices, processors = Processors, alarms = Alarms },
|
||||
{ok, AST};
|
||||
false ->
|
||||
{error, modbus_block_error}
|
||||
end
|
||||
catch throw:Reason ->
|
||||
{error, Reason}
|
||||
end.
|
||||
@ -163,7 +168,7 @@ parse_ast0(#block{ident = <<"device", Name0/binary>>, props = Props}) ->
|
||||
poll_interval = maps:get(<<"poll_interval">>, MapProps, undefined),
|
||||
retries = map_of_integer(<<"retries">>, MapProps, 0),
|
||||
retry_timeout = maps:get(<<"retry_timeout">>, MapProps, undefined),
|
||||
variables = maps:get(<<"variables">>, MapProps, undefined),
|
||||
metrics = maps:get(<<"metrics">>, MapProps, undefined),
|
||||
controls = maps:get(<<"controls">>, MapProps, undefined)
|
||||
};
|
||||
parse_ast0(#block{ident = <<"processor", Name0/binary>>, props = Props}) ->
|
||||
@ -196,8 +201,22 @@ parse_ast1([#block{ident = <<"recovery_actions">>, props = Props}|T], Acc) ->
|
||||
parse_ast1(T, [{<<"recovery_actions">>, Props}|Acc]);
|
||||
parse_ast1([#block{ident = <<"transform">>, props = Props}|T], Acc) ->
|
||||
parse_ast1(T, [{<<"transform">>, Props}|Acc]);
|
||||
parse_ast1([#block{ident = <<"variables">>, props = Props}|T], Acc) ->
|
||||
parse_ast1(T, [{<<"variables">>, Props}|Acc]);
|
||||
parse_ast1([#block{ident = <<"metrics">>, props = Metrics0}|T], Acc) ->
|
||||
Metrics = lists:map(fun(#block{ident = MetricName, props = Props0}) ->
|
||||
Props = lists:map(fun(Prop0) ->
|
||||
[Name|Vars] = binary:split(Prop0, <<" ">>, [trim]),
|
||||
{Name, Vars}
|
||||
end, Props0),
|
||||
PropsMap = maps:from_list(Props),
|
||||
#modbus_metric{
|
||||
name = MetricName,
|
||||
address = map_of_integer(<<"address">>, PropsMap, 0),
|
||||
type = map_of_string(<<"type">>, PropsMap, ""),
|
||||
unit = strip_quotes(map_of_string(<<"unit">>, PropsMap, ""))
|
||||
}
|
||||
end, Metrics0),
|
||||
parse_ast1(T, [{<<"metrics">>, Metrics}|Acc]);
|
||||
|
||||
parse_ast1([#block{ident = <<"controls">>, props = Props}|T], Acc) ->
|
||||
parse_ast1(T, [{<<"controls">>, Props}|Acc]);
|
||||
parse_ast1([#block{ident = <<"transport", Name0/binary>>, props = Props}|T], Acc) ->
|
||||
@ -242,7 +261,7 @@ validate([#modbus{transport = undefined}|_]) ->
|
||||
|
||||
validate([#modbus_device{slave_id = Id}|_]) when Id < 1 orelse Id > 247 ->
|
||||
throw({invalid_slave_id, Id});
|
||||
validate([#modbus_device{variables = _Vars}|T]) ->
|
||||
validate([#modbus_device{metrics = _Vars}|T]) ->
|
||||
validate(T);
|
||||
validate([_|T]) ->
|
||||
validate(T).
|
||||
@ -288,3 +307,9 @@ map_of_time(Key, M, Def) ->
|
||||
false ->
|
||||
Def
|
||||
end.
|
||||
|
||||
strip_quotes(Str) when is_list(Str) ->
|
||||
Str1 = string:trim(Str, leading, "\""),
|
||||
Str2 = string:trim(Str1, trailing, "\""),
|
||||
Str3 = string:trim(Str2, leading, "'"),
|
||||
string:trim(Str3, trailing, "'").
|
||||
@ -12,6 +12,9 @@
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
%% rtu指令
|
||||
-define(CONNECT, 16#01).
|
||||
|
||||
%% API
|
||||
-export([start_link/1]).
|
||||
-export([test/0]).
|
||||
@ -22,7 +25,9 @@
|
||||
-define(SERVER, ?MODULE).
|
||||
|
||||
-record(state, {
|
||||
|
||||
port,
|
||||
access_log_pid :: pid() | undefined,
|
||||
error_log_pid :: pid() | undefined
|
||||
}).
|
||||
|
||||
test() ->
|
||||
@ -50,11 +55,19 @@ start_link(AST) ->
|
||||
-spec(init(Args :: term()) ->
|
||||
{ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term()} | ignore).
|
||||
init([AST = #ast{modbus = Modbus}]) ->
|
||||
init([AST = #ast{modbus = Modbus, devices = Devices}]) ->
|
||||
|
||||
lager:debug("modbus is: ~p", [Modbus]),
|
||||
Device = hd(Devices),
|
||||
lager:debug("devices is: ~p", [Device#modbus_device.metrics]),
|
||||
% Res = connect(Transport),
|
||||
% lager:debug("connect res: ~p", [Res]),
|
||||
|
||||
{ok, #state{}}.
|
||||
|
||||
%{ok, AccessLogPid} = modbus_logger:start_link(AccessLog),
|
||||
%{ok, ErrorLogPid} = modbus_logger:start_link(ErrorLog),
|
||||
|
||||
{ok, #state{access_log_pid = undefined, error_log_pid = undefined}}.
|
||||
|
||||
%% @private
|
||||
%% @doc Handling call messages
|
||||
@ -84,6 +97,10 @@ handle_cast(_Request, State = #state{}) ->
|
||||
{noreply, NewState :: #state{}} |
|
||||
{noreply, NewState :: #state{}, timeout() | hibernate} |
|
||||
{stop, Reason :: term(), NewState :: #state{}}).
|
||||
handle_info({Port, {data, Data}}, State = #state{port = Port}) ->
|
||||
lager:debug("port data is: ~p", [Data]),
|
||||
{noreply, State};
|
||||
|
||||
handle_info(_Info, State = #state{}) ->
|
||||
{noreply, State}.
|
||||
|
||||
@ -109,9 +126,17 @@ code_change(_OldVsn, State = #state{}, _Extra) ->
|
||||
%%% Internal functions
|
||||
%%%===================================================================
|
||||
|
||||
x(#modbus{transport = #modbus_transport_rtu{port = Port, baudrate = Baudrate, stopbits = Stopbits, timeout = Timeout}}) ->
|
||||
connect(#modbus{transport = #modbus_transport_rtu{port = Port, baudrate = Baudrate, stopbits = Stopbits, parity = Parity, timeout = Timeout}}) ->
|
||||
RealExecCmd = "",
|
||||
Port = erlang:open_port({spawn_executable, RealExecCmd}, [binary, {packet, 2}, exit_status]),
|
||||
|
||||
Len0 = byte_size(Port),
|
||||
ConnectCmd = <<?CONNECT:8, Len0:8, Port/binary, Baudrate:32, Stopbits:8, Parity:8, Timeout:32>>,
|
||||
%% 建立连接
|
||||
Port ! {self(), {command, ConnectCmd}},
|
||||
ok;
|
||||
x(#modbus{transport = #modbus_transport_tcp{host = Host, port = Port, timeout = Timeout0}}) ->
|
||||
|
||||
connect(#modbus{transport = #modbus_transport_tcp{host = Host, port = Port, timeout = Timeout0}}) ->
|
||||
Timeout = case is_integer(Timeout0) andalso Timeout0 > 0 of
|
||||
true ->
|
||||
Timeout0;
|
||||
@ -119,4 +144,3 @@ x(#modbus{transport = #modbus_transport_tcp{host = Host, port = Port, timeout =
|
||||
2000
|
||||
end,
|
||||
gen_tcp:connect(Host, Port, [binary], Timeout).
|
||||
|
||||
|
||||
48
modbus.conf
48
modbus.conf
@ -16,7 +16,7 @@ modbus {
|
||||
}
|
||||
|
||||
# 日志设置
|
||||
error_log /var/log/modbus_error.log warn;
|
||||
error_log /var/log/modbus_error.log;
|
||||
access_log /var/log/modbus_access.log;
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ device boiler_controller {
|
||||
retry_timeout 2s;
|
||||
|
||||
# 数据点定义
|
||||
variables {
|
||||
metrics {
|
||||
# 温度读取(保持寄存器)
|
||||
temperature {
|
||||
address 40001; # Modbus地址表示法
|
||||
@ -54,16 +54,16 @@ device boiler_controller {
|
||||
}
|
||||
|
||||
# 状态位(线圈)
|
||||
alarm_status {
|
||||
address 00001;
|
||||
type bool;
|
||||
bits {
|
||||
0 "overheat";
|
||||
1 "low_pressure";
|
||||
2 "pump_failure";
|
||||
}
|
||||
poll on;
|
||||
}
|
||||
#alarm_status {
|
||||
# address 00001;
|
||||
# type bool;
|
||||
# bits {
|
||||
# 0 "overheat";
|
||||
# 1 "low_pressure";
|
||||
# 2 "pump_failure";
|
||||
# }
|
||||
# poll on;
|
||||
#}
|
||||
}
|
||||
|
||||
# 写入控制
|
||||
@ -100,13 +100,13 @@ device xyz {
|
||||
retry_timeout 2s;
|
||||
|
||||
# 数据点定义
|
||||
variables {
|
||||
metrics {
|
||||
# 温度读取(保持寄存器)
|
||||
temperature {
|
||||
address 40001; # Modbus地址表示法
|
||||
type int16;
|
||||
scale 0.1;
|
||||
unit "°C";
|
||||
unit "° C";
|
||||
poll on;
|
||||
}
|
||||
|
||||
@ -120,16 +120,16 @@ device xyz {
|
||||
}
|
||||
|
||||
# 状态位(线圈)
|
||||
alarm_status {
|
||||
address 00001;
|
||||
type bool;
|
||||
bits {
|
||||
0 "overheat";
|
||||
1 "low_pressure";
|
||||
2 "pump_failure";
|
||||
}
|
||||
poll on;
|
||||
}
|
||||
#alarm_status {
|
||||
# address 00001;
|
||||
# type bool;
|
||||
# bits {
|
||||
# 0 "overheat";
|
||||
# 1 "low_pressure";
|
||||
# 2 "pump_failure";
|
||||
# }
|
||||
# poll on;
|
||||
#}
|
||||
}
|
||||
|
||||
# 写入控制
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user